commit b9fd07c94c4c3b4ff63f70d759354199248d00b5 Author: huangxianguo Date: Fri Aug 16 15:20:09 2024 +0800 添加点胶机框架内容 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1ff0c42 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8afdcb6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,454 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# Tye +.tye/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +## +## Visual studio for Mac +## + + +# globs +Makefile.in +*.userprefs +*.usertasks +config.make +config.status +aclocal.m4 +install-sh +autom4te.cache/ +*.tar.gz +tarballs/ +test-results/ + +# Mac bundle stuff +*.dmg +*.app + +# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# JetBrains Rider +.idea/ +*.sln.iml + +## +## Visual Studio Code +## +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..dff0fbd --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,6 @@ + + + enable + 11.0.2 + + diff --git a/Dispenser.sln b/Dispenser.sln new file mode 100644 index 0000000..6baac3c --- /dev/null +++ b/Dispenser.sln @@ -0,0 +1,67 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.9.34714.143 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DispenserUI", "DispenserUI\DispenserUI.csproj", "{E08E72A8-4334-4871-AAE0-244B16ADE556}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DispenserDesktop", "DispenserDesktop\DispenserDesktop.csproj", "{A52ACF73-71A1-48A6-9B7F-E06761B26AC9}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DispenserCommon", "DispenserCommon\DispenserCommon.csproj", "{A8587675-8F8D-4F3E-8809-DDF77743FEFD}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DispenserCommon", "DispenserCore\DispenserCore.csproj", "{2150E333-8FDC-42A3-9474-1A3956D46DE8}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DispenserDriver", "DispenserDriver", "{42401CD7-B4C8-4F06-AD46-5D987CF4CA53}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DispenserHal", "DispenserHal\DispenserHal.csproj", "{78E3094F-66F1-470A-BA89-37F43E7F3737}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DispenserDriver.CameraMV", "DispenserDriver.CameraMV\DispenserDriver.CameraMV.csproj", "{FFED5A1A-44B8-4C89-B659-9B86A3FF800C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DispenserAlgorithm", "DispenserAlgorithm\DispenserAlgorithm.csproj", "{921E3C5C-1347-402E-9838-3B30170F7489}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {E08E72A8-4334-4871-AAE0-244B16ADE556}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E08E72A8-4334-4871-AAE0-244B16ADE556}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E08E72A8-4334-4871-AAE0-244B16ADE556}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E08E72A8-4334-4871-AAE0-244B16ADE556}.Release|Any CPU.Build.0 = Release|Any CPU + {E08E72A8-4334-4871-AAE0-244B16ADE556}.Debug|Any CPU.Deploy.0 = Debug|Any CPU + {2150E333-8FDC-42A3-9474-1A3956D46DE8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2150E333-8FDC-42A3-9474-1A3956D46DE8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2150E333-8FDC-42A3-9474-1A3956D46DE8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2150E333-8FDC-42A3-9474-1A3956D46DE8}.Release|Any CPU.Build.0 = Release|Any CPU + {2150E333-8FDC-42A3-9474-1A3956D46DE8}.Debug|Any CPU.Deploy.0 = Debug|Any CPU + {A52ACF73-71A1-48A6-9B7F-E06761B26AC9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A52ACF73-71A1-48A6-9B7F-E06761B26AC9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A52ACF73-71A1-48A6-9B7F-E06761B26AC9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A52ACF73-71A1-48A6-9B7F-E06761B26AC9}.Release|Any CPU.Build.0 = Release|Any CPU + {A52ACF73-71A1-48A6-9B7F-E06761B26AC9}.Debug|Any CPU.Deploy.0 = Debug|Any CPU + {A8587675-8F8D-4F3E-8809-DDF77743FEFD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A8587675-8F8D-4F3E-8809-DDF77743FEFD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A8587675-8F8D-4F3E-8809-DDF77743FEFD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A8587675-8F8D-4F3E-8809-DDF77743FEFD}.Release|Any CPU.Build.0 = Release|Any CPU + {A8587675-8F8D-4F3E-8809-DDF77743FEFD}.Debug|Any CPU.Deploy.0 = Debug|Any CPU + {78E3094F-66F1-470A-BA89-37F43E7F3737}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {78E3094F-66F1-470A-BA89-37F43E7F3737}.Debug|Any CPU.Build.0 = Debug|Any CPU + {78E3094F-66F1-470A-BA89-37F43E7F3737}.Release|Any CPU.ActiveCfg = Release|Any CPU + {78E3094F-66F1-470A-BA89-37F43E7F3737}.Release|Any CPU.Build.0 = Release|Any CPU + {FFED5A1A-44B8-4C89-B659-9B86A3FF800C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FFED5A1A-44B8-4C89-B659-9B86A3FF800C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FFED5A1A-44B8-4C89-B659-9B86A3FF800C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FFED5A1A-44B8-4C89-B659-9B86A3FF800C}.Release|Any CPU.Build.0 = Release|Any CPU + {921E3C5C-1347-402E-9838-3B30170F7489}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {921E3C5C-1347-402E-9838-3B30170F7489}.Debug|Any CPU.Build.0 = Debug|Any CPU + {921E3C5C-1347-402E-9838-3B30170F7489}.Release|Any CPU.ActiveCfg = Release|Any CPU + {921E3C5C-1347-402E-9838-3B30170F7489}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {FFED5A1A-44B8-4C89-B659-9B86A3FF800C} = {42401CD7-B4C8-4F06-AD46-5D987CF4CA53} + EndGlobalSection +EndGlobal diff --git a/DispenserAlgorithm/DispenserAlgorithm.csproj b/DispenserAlgorithm/DispenserAlgorithm.csproj new file mode 100644 index 0000000..ca3ed59 --- /dev/null +++ b/DispenserAlgorithm/DispenserAlgorithm.csproj @@ -0,0 +1,29 @@ + + + + net7.0 + enable + enable + DispenserAlgorithm + + + + + + + + + + DispenserAlgorithms.Halcon.dll + + + ..\DispenserDesktop\Libs\DispenserVision.Halcon.dll + + + + + + halcondotnet.dll + + + diff --git a/DispenserCommon/Aop/AlertWhenException.cs b/DispenserCommon/Aop/AlertWhenException.cs new file mode 100644 index 0000000..4995291 --- /dev/null +++ b/DispenserCommon/Aop/AlertWhenException.cs @@ -0,0 +1,46 @@ +using AspectInjector.Broker; +using DispenserCommon.Utils; +using Serilog; + +namespace DispenserCommon.Aop; + +/// +/// 用于捕获方法执行过程中的异常,并弹窗 +/// +[Aspect(Scope.Global)] +[Injection(typeof(AlertWhenException))] +public class AlertWhenException(string title = "异常提示", string contentTitle = "") : Attribute +{ + public AlertWhenException() : this("异常提示") + { + } + + /// + /// 通过AOP实现方法执行前后的时间消耗 + /// + /// + /// + /// + /// + /// + /// + [Advice(Kind.Around)] + public object Around([Argument(Source.Name)] string name, + [Argument(Source.Arguments)] object[] args, + [Argument(Source.Type)] Type hostType, + [Argument(Source.Target)] Func target, + [Argument(Source.Triggers)] Attribute[] triggers) + { + try + { + var result = target(args); + return result; + } + catch (Exception e) + { + Log.Error(e, $"方法 {name} 执行异常"); + MessageBoxHelper.Error(e.Message, contentTitle, title, false); + return null!; + } + } +} \ No newline at end of file diff --git a/DispenserCommon/Aop/AsyncOperation.cs b/DispenserCommon/Aop/AsyncOperation.cs new file mode 100644 index 0000000..d0fbb22 --- /dev/null +++ b/DispenserCommon/Aop/AsyncOperation.cs @@ -0,0 +1,15 @@ +using AspectInjector.Broker; + +namespace DispenserCommon.Aop; + +[Aspect(Scope.Global)] +[Injection(typeof(AsyncOperation))] +public class AsyncOperation(string name) : Attribute +{ + public string Name { get; set; } = name; + + public AsyncOperation() : this("") + { + Name = ""; + } +} \ No newline at end of file diff --git a/DispenserCommon/Aop/ConsumeTime.cs b/DispenserCommon/Aop/ConsumeTime.cs new file mode 100644 index 0000000..9edd769 --- /dev/null +++ b/DispenserCommon/Aop/ConsumeTime.cs @@ -0,0 +1,47 @@ +using System.Diagnostics; +using AspectInjector.Broker; +using Serilog; + +namespace DispenserCommon.Aop; + +[Aspect(Scope.Global)] +[Injection(typeof(ConsumeTime))] +public class ConsumeTime(string title) : Attribute +{ + public ConsumeTime() : this("") + { + Title = ""; + } + + public string Title { get; set; } = title; + + /// + /// 通过AOP实现方法执行前后的时间消耗 + /// + /// + /// + /// + /// + /// + /// + [Advice(Kind.Around)] + public object Around([Argument(Source.Name)] string name, + [Argument(Source.Arguments)] object[] args, + [Argument(Source.Type)] Type hostType, + [Argument(Source.Target)] Func target, + [Argument(Source.Triggers)] Attribute[] triggers) + { + var sw = Stopwatch.StartNew(); + try + { + var result = target(args); + return result; + } + finally + { + sw.Stop(); + Log.Information( + $"方法 {(Title != "" ? Title : name)} 执行时间:{sw.Elapsed.TotalMilliseconds}ms"); + } + } +} \ No newline at end of file diff --git a/DispenserCommon/Aop/GlobalTry.cs b/DispenserCommon/Aop/GlobalTry.cs new file mode 100644 index 0000000..8a7eed6 --- /dev/null +++ b/DispenserCommon/Aop/GlobalTry.cs @@ -0,0 +1,39 @@ +using System.Reflection; +using AspectInjector.Broker; +using DispenserCommon.Utils; +using Serilog; + +namespace DispenserCommon.Aop; + +[Aspect(Scope.Global)] +[AttributeUsage(AttributeTargets.Class)] +[Injection(typeof(GlobalTry), Inherited = true)] +public class GlobalTry : Attribute +{ + /// + /// 通过AOP实现方法执行前后的时间消耗 + /// + /// + /// + /// + /// + /// + [Advice(Kind.Around, Targets = Target.Method)] + public object? HandleMethod([Argument(Source.Name)] string name, + [Argument(Source.Arguments)] object[] args, + [Argument(Source.Target)] Func target, + [Argument(Source.Metadata)] MethodBase method) + { + try + { + var result = target(args); + return result; + } + catch (Exception e) + { + Log.Error(e, $"方法 {name} 执行异常: {e.Message}"); + ToastUtil.Error("操作异常: " + e.Message); + return default; + } + } +} \ No newline at end of file diff --git a/DispenserCommon/Aop/Operation.cs b/DispenserCommon/Aop/Operation.cs new file mode 100644 index 0000000..20ee52d --- /dev/null +++ b/DispenserCommon/Aop/Operation.cs @@ -0,0 +1,63 @@ +using AspectInjector.Broker; +using DispenserCommon.DTO; +using DispenserCommon.Enums; +using DispenserCommon.Events; +using DispenserCommon.Utils; +using DispenserUI.Exceptions; +using Serilog; + +namespace DispenserCommon.Aop; + +[Aspect(Scope.Global)] +[Injection(typeof(Operation))] +public class Operation(string name) : Attribute +{ + public string Name { get; set; } = name; + + public Operation() : this("") + { + Name = ""; + } + + /// + /// 通过AOP实现方法执行过程拦截 + /// + /// + /// + /// + /// + [Advice(Kind.Around, Targets = Target.Method)] + public object? Around( + [Argument(Source.Arguments)] object[] args, + [Argument(Source.Target)] Func target, + [Argument(Source.Triggers)] Attribute[] triggers) + { + var trigger = (Operation)triggers[0]; + + var log = new ActionLog + { + Name = trigger.Name, + Params = args + }; + try + { + Log.Information($"正在执行: {trigger.Name}"); + return target(args); + } + catch (Exception e) + { + if (e is BizException { Level: ExceptionLevel.NORMAL }) + { + ToastUtil.Error($"{trigger.Name}操作失败: {e.Message}"); + } + + log.Exception = e; + Log.Error($"{trigger.Name}操作失败: {e.Message}"); + return default; + } + finally + { + EventBus.Publish(EventType.OperationLog, log); + } + } +} \ No newline at end of file diff --git a/DispenserCommon/Atrributes/BlockTask.cs b/DispenserCommon/Atrributes/BlockTask.cs new file mode 100644 index 0000000..6554f68 --- /dev/null +++ b/DispenserCommon/Atrributes/BlockTask.cs @@ -0,0 +1,7 @@ +namespace DispenserCommon.Atrributes; + +[AttributeUsage(AttributeTargets.Method)] +public class BlockTask(string name) : Attribute +{ + public string Name { get; } = name; +} \ No newline at end of file diff --git a/DispenserCommon/Atrributes/Hide.cs b/DispenserCommon/Atrributes/Hide.cs new file mode 100644 index 0000000..a8b2a3a --- /dev/null +++ b/DispenserCommon/Atrributes/Hide.cs @@ -0,0 +1,6 @@ +namespace DispenserCommon.Atrributes; + +[AttributeUsage(AttributeTargets.Property)] +public class Hide : Attribute +{ +} \ No newline at end of file diff --git a/DispenserCommon/Atrributes/Property.cs b/DispenserCommon/Atrributes/Property.cs new file mode 100644 index 0000000..9c09225 --- /dev/null +++ b/DispenserCommon/Atrributes/Property.cs @@ -0,0 +1,30 @@ +namespace DispenserCommon.Atrributes; + +[AttributeUsage(AttributeTargets.Property)] +public class Property : Attribute +{ + public bool IsReadOnly { get; set; } = false; + + // 字符串格式 + public string? Format { get; set; } + + // 显示控件的宽 + public double Width { get; set; } + + // 显示控件的高 + public double Height { get; set; } + + public double Max { get; set; } + + public double Min { get; set; } + + public string Group { get; set; } + + public bool IsPassword { get; set; } + + public string Variable { get; set; } + + public int Axis { get; set; } = -1; + + public int Index { get; set; } = -1; +} \ No newline at end of file diff --git a/DispenserCommon/Constant/Topics.cs b/DispenserCommon/Constant/Topics.cs new file mode 100644 index 0000000..7930962 --- /dev/null +++ b/DispenserCommon/Constant/Topics.cs @@ -0,0 +1,12 @@ +namespace DispenserCommon.Constant; + +/// +/// 这里管理MQTT 所有的topic +/// +public class Topics +{ + // 锁机 + public const string Locked = "device/{deviceId}/locked"; + + +} \ No newline at end of file diff --git a/DispenserCommon/Converter/StringNullOrEmptyToVisibilityConverter.cs b/DispenserCommon/Converter/StringNullOrEmptyToVisibilityConverter.cs new file mode 100644 index 0000000..80abc1b --- /dev/null +++ b/DispenserCommon/Converter/StringNullOrEmptyToVisibilityConverter.cs @@ -0,0 +1,18 @@ +using System.Globalization; +using Avalonia.Data.Converters; + +namespace DispenserCommon.Converter; + +public class StringNullOrEmptyToVisibilityConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + var str = value as string; + return string.IsNullOrEmpty(str); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/DispenserCommon/DB/DatabaseInitializer.cs b/DispenserCommon/DB/DatabaseInitializer.cs new file mode 100644 index 0000000..a263040 --- /dev/null +++ b/DispenserCommon/DB/DatabaseInitializer.cs @@ -0,0 +1,83 @@ +using System.Reflection; +using DispenserCommon.Events; +using DispenserCommon.Utils; +using SQLite; + +namespace DispenserCommon.DB; + +/// +/// 在系统启动的时候记进行数据库初始化校验 +/// +public class DatabaseInitializer +{ + private static readonly SqliteHelper Sqlite = ServiceLocator.GetService(); + + public static void Initialize() + { + EventBus.Publish(EventType.SetupNotify, "正在初始化数据库"); + + // 扫描所有带有[Table]特性的类 + var tables = GetAssemblies() + .SelectMany(a => a.GetTypes() + .Where(t => t.GetCustomAttributes(typeof(TableAttribute), true).Length > 0)) + .ToList(); + + + tables.ForEach(table => + { + // 判断当前的表是否存在 + var tableName = table.GetCustomAttribute()?.Name ?? table.Name; + + var tableInfo = Sqlite.GetTableInfo(tableName); + + if (!tableInfo.Any()) + { + Sqlite.CreateTable(table); + } + else + { + // 读取当前类的变量是否新增了属性 + var properties = table.GetProperties(); + var colNames = tableInfo.Select(col => col.Name.ToUpper()).ToHashSet(); + + foreach (var property in properties) + { + var colName = property.GetCustomAttribute()?.Name ?? property.Name; + + // 统一转为大写进行判断 + if (colNames.Contains(colName.ToUpper())) continue; + // 重新进行DDL时,会自动的判断是否存在新增的列,如果有则自动进行新增 + Sqlite.CreateTable(table); + } + } + }); + } + + + private static IEnumerable GetAssemblies() + { + var assemblies = new List(); + foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) + { + var name = assembly.GetName().Name; + if (name != null && name.ToLower().Contains("dispenser")) GetReferenceAssemblies(assembly, assemblies); + } + + return assemblies; + } + + private static void GetReferenceAssemblies(Assembly assembly, ICollection assemblies) + { + foreach (var assemblyName in assembly.GetReferencedAssemblies()) + { + var name = assemblyName.Name; + if (name != null && name.ToLower().Contains("dispenser")) + { + var ass = Assembly.Load(assemblyName); + if (assemblies.Contains(ass)) continue; + assemblies.Add(ass); + GetReferenceAssemblies(ass, assemblies); + } + } + } +} \ No newline at end of file diff --git a/DispenserCommon/DB/SqliteHelper.cs b/DispenserCommon/DB/SqliteHelper.cs new file mode 100644 index 0000000..2a854fd --- /dev/null +++ b/DispenserCommon/DB/SqliteHelper.cs @@ -0,0 +1,228 @@ +using System.Reflection; +using DispenserCommon.DTO; +using DispenserCommon.Ioc; +using Masuit.Tools; +using Masuit.Tools.Systems; +using SQLite; + +namespace DispenserCommon.DB; + +/// +/// SQLite 数据库ORM工具类 +/// +[Component] +public class SqliteHelper +{ + // 默认的数据库密码 + private const string Password = "88888888"; + private readonly SQLiteConnection _db; + + public SqliteHelper() + { + var profile = Environment.GetEnvironmentVariable("USERPROFILE"); + var path = Path.Combine(profile, "dispenser", "dispenser.db"); + + if (!Directory.Exists(Path.GetDirectoryName(path)!)) + { + Directory.CreateDirectory(Path.GetDirectoryName(path)!); + + // 将文件复模板数据库复制到目标目录下 + var template = Path.Combine(Environment.CurrentDirectory, "dispenser.db"); + File.Copy(template, path); + } + + + _db = new SQLiteConnection(path); + } + + + /// + /// 插入数据 + /// + /// 待插入的数据 + /// 数据类型 + public int Insert(T item) + { + var id = item?.GetType().GetProperty("Id"); + if (id != null && id.CanWrite) id.SetValue(item, SnowFlakeNew.LongId.ToString()); + + var createTime = item?.GetType().GetProperty("CreateTime"); + if (createTime != null && createTime.CanWrite) createTime.SetValue(item, DateTime.Now); + + + return _db.Insert(item); + } + + /// + /// 更新数据 + /// + /// 待更新的数据 + /// 数据类型 + public int Update(T item) + { + var updateTime = item?.GetType().GetProperty("UpdateTime"); + if (updateTime != null && updateTime.CanWrite) updateTime.SetValue(item, DateTime.Now); + + return _db.Update(item); + } + + /// + /// 删除数据 + /// + /// 待删除数据的ID + /// 数据类型 + public int DeleteById(object id) where T : new() + { + var type = typeof(T); + var tableName = type.GetCustomAttribute()!.Name ?? type.Name; + + return _db.Execute($"delete from {tableName} where id = {id}"); + } + + /// + /// 删除数据 + /// + /// 待删除数据的对象 + /// 数据类型 + public int Delete(T item) + { + return _db.Delete(item); + } + + /// + /// 根据ID获取数据 + /// + /// 数据ID + /// 数据类型 + public T GetById(object id) where T : new() + { + return _db.Get(id); + } + + /// + /// 查询数据 + /// + /// 查询语句 + /// 查询参数 + /// 数据类型 + public List Query(string sql, params object[] args) where T : new() + { + return _db.Query(sql, args); + } + + /// + /// 查询所有数据 + /// + /// 数据类型 + public List ListAll() where T : new() + { + return _db.Table().ToList(); + } + + public T SaveOrUpdate(T item) where T : new() + { + var id = item?.GetType().GetProperty("Id"); + if (id != null && id.GetValue(item).IsNullOrEmpty()) + // 插入 + Insert(item); + else + // 否则是更新操作 + Update(item); + + return item; + } + + /// + /// 执行SQL语句 + /// + /// SQL语句 + /// + public int Execute(string sql, params object[] args) + { + return _db.Execute(sql, args); + } + + /// + /// 获取当前的表信息 + /// + /// + /// + public List GetTableInfo(string tableName) + { + return _db.GetTableInfo(tableName); + } + + /// + /// 创建表 + /// + /// + public void CreateTable(Type entity) + { + _db.CreateTable(entity); + } + + /// + /// 分页查询结果 + /// + /// + /// + /// + /// + /// + /// + public Page Page(int page, int pageSize, string sql, params object[] args) where T : new() + { + var countSql = "select count(*) from ( " + sql + " ) as c"; + var total = _db.ExecuteScalar(countSql, args); + var data = Query(sql + " limit " + (page - 1) * pageSize + "," + pageSize, args); + return new Page + { + CurrentPage = page, + PageSize = pageSize, + Total = total, + Pages = total / pageSize + (total % pageSize == 0 ? 0 : 1), + Data = data + }; + } + + /// + /// 批量插入 + /// + /// + /// + /// + public void BatchInsert(IEnumerable objects, bool runInTransaction = false) where T : new() + { + if (runInTransaction) + { + _db.RunInTransaction((Action)(() => + { + foreach (object obj in objects) + Insert(obj); + })); + } + else + { + foreach (object obj in objects) + Insert(obj); + } + } + + + /// + /// 更新指定字段 + /// + /// + /// + /// + /// + public void UpdateFieldById(object? id, string field, object? value) where T : new() + { + var type = typeof(T); + var tableName = type.GetCustomAttribute()!.Name ?? type.Name; + + var sql = $"update {tableName} set {field} = ? where id = ?"; + + _db.Execute(sql, value, id); + } +} \ No newline at end of file diff --git a/DispenserCommon/DTO/ActionLog.cs b/DispenserCommon/DTO/ActionLog.cs new file mode 100644 index 0000000..a427b1e --- /dev/null +++ b/DispenserCommon/DTO/ActionLog.cs @@ -0,0 +1,15 @@ +namespace DispenserCommon.DTO; + +/// +/// 通过AOP方式拦截获取用户操作日志 +/// +public class ActionLog +{ + public string Name { get; set; } + + public object[] Params { get; set; } + + public Exception Exception { get; set; } + + public DateTime OperateTime { get; set; } = DateTime.Now; +} \ No newline at end of file diff --git a/DispenserCommon/DTO/LogMessage.cs b/DispenserCommon/DTO/LogMessage.cs new file mode 100644 index 0000000..6178aec --- /dev/null +++ b/DispenserCommon/DTO/LogMessage.cs @@ -0,0 +1,21 @@ +using Serilog.Events; + +namespace DispenserCommon.DTO; + +/// +/// 日志信息 +/// +/// +public class LogMessage(string message) +{ + public LogMessage(string message, LogEventLevel level) : this(message) + { + Message = message; + Level = level; + } + + public DateTime Timestamp { get; set; } = DateTime.Now; + public string Message { get; set; } = message; + + public LogEventLevel Level { get; set; } = LogEventLevel.Information; +} \ No newline at end of file diff --git a/DispenserCommon/DTO/Page.cs b/DispenserCommon/DTO/Page.cs new file mode 100644 index 0000000..0aab66a --- /dev/null +++ b/DispenserCommon/DTO/Page.cs @@ -0,0 +1,77 @@ +using System.ComponentModel; +using System.Runtime.CompilerServices; + +namespace DispenserCommon.DTO; + +/// +/// 分页结果 +/// +public class Page : INotifyPropertyChanged +{ + private int _total; + + private int _pages; + + private int _currentPage; + + private List _data = []; + + private int _pageSize; + + public int CurrentPage + { + get => _currentPage; + set + { + _currentPage = value; + OnPropertyChanged(); + } + } + + public int PageSize + { + get => _pageSize; + set + { + _pageSize = value; + OnPropertyChanged(); + } + } + + public int Total + { + get => _total; + set + { + _total = value; + OnPropertyChanged(); + } + } + + public int Pages + { + get => _pages; + set + { + _pages = value; + OnPropertyChanged(); + } + } + + public List Data + { + get => _data; + set + { + _data = value; + OnPropertyChanged(); + } + } + + public event PropertyChangedEventHandler? PropertyChanged; + + protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } +} \ No newline at end of file diff --git a/DispenserCommon/DTO/Result.cs b/DispenserCommon/DTO/Result.cs new file mode 100644 index 0000000..a97f7b4 --- /dev/null +++ b/DispenserCommon/DTO/Result.cs @@ -0,0 +1,48 @@ +namespace DispenserCommon.DTO; + +/// +/// plc请求结果 +/// +/// +public class Result +{ + public Result() + { + Ok = true; + Code = 0; + Message = "成功"; + } + + public Result(T data) + { + Ok = true; + Code = 0; + Message = "成功"; + Data = data; + } + + public Result(int code, string message) + { + Ok = false; + Code = code; + Message = message; + } + + public Result(int code, string message, Exception? exception) + { + Ok = false; + Code = code; + Message = message; + Exception = exception; + } + + public bool Ok { get; set; } + + public int Code { get; set; } + + public T? Data { get; set; } + + public string Message { get; set; } + + public Exception? Exception { get; set; } +} \ No newline at end of file diff --git a/DispenserCommon/DispenserCommon.csproj b/DispenserCommon/DispenserCommon.csproj new file mode 100644 index 0000000..d768af5 --- /dev/null +++ b/DispenserCommon/DispenserCommon.csproj @@ -0,0 +1,34 @@ + + + + net7.0 + enable + enable + preview + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/DispenserCommon/Enums/BinCoordinateTransformStrategyEnum.cs b/DispenserCommon/Enums/BinCoordinateTransformStrategyEnum.cs new file mode 100644 index 0000000..9afb483 --- /dev/null +++ b/DispenserCommon/Enums/BinCoordinateTransformStrategyEnum.cs @@ -0,0 +1,13 @@ +namespace DispenserCommon.Enums; + +public enum BinCoordinateTransformStrategyEnum +{ + /// + /// 基于角度 + /// + ByAngel, + /// + /// 基于矩阵 + /// + ByMatrix +} \ No newline at end of file diff --git a/DispenserCommon/Enums/ExceptionLevel.cs b/DispenserCommon/Enums/ExceptionLevel.cs new file mode 100644 index 0000000..fd844a4 --- /dev/null +++ b/DispenserCommon/Enums/ExceptionLevel.cs @@ -0,0 +1,8 @@ +namespace DispenserCommon.Enums; + +public enum ExceptionLevel : int +{ + NORMAL, + WARN, + ERROR, +} \ No newline at end of file diff --git a/DispenserCommon/Enums/ScannerTypeEnum.cs b/DispenserCommon/Enums/ScannerTypeEnum.cs new file mode 100644 index 0000000..46aacf0 --- /dev/null +++ b/DispenserCommon/Enums/ScannerTypeEnum.cs @@ -0,0 +1,7 @@ +namespace DispenserCommon.Enums; + +public enum ScannerTypeEnum +{ + Wafer, + Pcb, +} \ No newline at end of file diff --git a/DispenserCommon/Events/EventAction.cs b/DispenserCommon/Events/EventAction.cs new file mode 100644 index 0000000..36394d0 --- /dev/null +++ b/DispenserCommon/Events/EventAction.cs @@ -0,0 +1,10 @@ +namespace DispenserCommon.Events; + +/// +/// 用于声明当前方法为事件处理器 +/// +[AttributeUsage(AttributeTargets.Method)] +public class EventAction(params EventType[] types) : Attribute +{ + public EventType[] Types => types; +} \ No newline at end of file diff --git a/DispenserCommon/Events/EventBus.cs b/DispenserCommon/Events/EventBus.cs new file mode 100644 index 0000000..17555ea --- /dev/null +++ b/DispenserCommon/Events/EventBus.cs @@ -0,0 +1,65 @@ +using Serilog; + +namespace DispenserCommon.Events; + +/// +/// 事件总线 +/// +/// +public abstract class EventBus +{ + private static readonly Dictionary> Subscribers = new(); + + /// + /// 添加订阅 + /// + /// + /// + public static void AddEventHandler(EventType type, Delegate action) + { + if (Subscribers.TryGetValue(type, out var subscribers)) + { + var any = subscribers.Any(item => item.Equals(action)); + if (!any) subscribers.Add(action); + } + else + { + Subscribers.Add(type, [action]); + } + } + + /// + /// 移除订阅逻辑 + /// + /// 事件类型 + /// 回调方法 + public static void RemoveEventHandler(EventType type, Delegate action) + { + if (!Subscribers.TryGetValue(type, out var handlers)) return; + + handlers.Remove(action); + } + + /// + /// 发布事件 + /// + /// + /// + public static void Publish(EventType type, T data) + { + if (!Subscribers.TryGetValue(type, out var subscribers)) return; + + // 创建一个副本,避免在回调中修改订阅列表导致迭代异常 + var actions = subscribers.ToList(); + + foreach (var action in actions) + try + { + action.DynamicInvoke(type, data); + } + catch (Exception e) + { + Log.Error(e, e.Message); + } + } +} \ No newline at end of file diff --git a/DispenserCommon/Events/EventListener.cs b/DispenserCommon/Events/EventListener.cs new file mode 100644 index 0000000..f7c7090 --- /dev/null +++ b/DispenserCommon/Events/EventListener.cs @@ -0,0 +1,10 @@ +namespace DispenserCommon.Events; + +/// +/// 用于声明当前类为事件监听 +/// +[AttributeUsage(AttributeTargets.Class)] +public class EventListener(string name = "") : Attribute +{ + private string Name => name; +} \ No newline at end of file diff --git a/DispenserCommon/Events/EventManager.cs b/DispenserCommon/Events/EventManager.cs new file mode 100644 index 0000000..3a7c397 --- /dev/null +++ b/DispenserCommon/Events/EventManager.cs @@ -0,0 +1,59 @@ +using System.Reflection; +using DispenserCommon.Ioc; +using DispenserCommon.Utils; + +namespace DispenserCommon.Events; + +/// +/// 通过事件管理者的方式来扫描系统内所有添加了注解的事件订阅者,并注册到事件总线上 +/// +public class EventManager +{ + /// + /// 扫描所有带有 EventListener 注解的类,并注册到事件总线上 + /// Action回调方法 为 所有带有 EventAction 注解的方法 + /// 方法必须为两个参数的委托,第一个参数为事件类型,第二个参数为事件数据 + /// + /// + public static void RegListeners() + { + var listeners = AppDomain.CurrentDomain.GetAssemblies() + .SelectMany(a => a.GetTypes() + .Where(t => t.GetCustomAttributes(typeof(EventListener), true).Length > 0 + || t.GetCustomAttributes(typeof(Component), true).Length > 0)) + .ToList(); + + foreach (var listener in listeners) + { + // 扫描当前类中所有的带有 EventAction 特性的方法 + var methods = listener + .GetMethods(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance) + .Where(m => m.GetCustomAttributes(typeof(EventAction), true).Length > 0) + .ToList(); + foreach (var method in methods) + { + var handler = method.GetCustomAttribute(); + + // 获取方法的参数类型 + var parameters = method.GetParameters(); + if (parameters.Length != 2) throw new Exception("订阅方法的参数必须为两个,第一个参数为事件类型,第二个参数为事件数据"); + + // 获取参数类型 + var parameterType = parameters[1].ParameterType; + + var types = handler?.Types; + if (types == null) continue; + foreach (var type in types) + { + var actionType = typeof(Action<,>).MakeGenericType(typeof(EventType), parameterType); + + var instance = ServiceLocator.GetService(listener); + var @delegate = method.CreateDelegate(actionType, instance); + var eventBus = typeof(EventBus<>).MakeGenericType(parameterType); + var addEventHandler = eventBus.GetMethod("AddEventHandler"); + addEventHandler?.Invoke(null, new object[] { type, @delegate }); + } + } + } + } +} \ No newline at end of file diff --git a/DispenserCommon/Events/EventType.cs b/DispenserCommon/Events/EventType.cs new file mode 100644 index 0000000..caeac2e --- /dev/null +++ b/DispenserCommon/Events/EventType.cs @@ -0,0 +1,111 @@ +using System.ComponentModel; + +namespace DispenserCommon.Events; + +/// +/// 通过事件发布订阅模式实现系统各组件的解耦 +/// 这里定义的是事件的驱动类型 +/// +public enum EventType +{ + // 系统初始化事件 + [Description("系统初始化事件")] SetupNotify, + + [Description("系统已经启动事件")] StartUp, + + // 锁机事件 + [Description("锁机事件")] LockEvent, + + [Description("许可无效事件")] LicenseInvalidEvent, + + [Description("异常信息")] Exception, + + [Description("解决异常信息")] SolveFault, + + [Description("操作日志")] OperationLog, + + [Description("动打状态")] StrikeState, + + [Description("生产作业过程状态信息")] ProductionState, + + [Description("动打路径范围")] StrokePath, + + [Description("传感器状态更新")] SensorState, + + // 相机数据 + [Description("测距仪数据")] CameraData, + + //参数修改 + [Description("参数修改")] ConfigChange, + + // 晶环上料确认事件 + [Description("晶环上料确认事件")] WaferLoadingConfirmed, + + // 芯片缺料事件 + [Description("芯片缺料事件")] ChipLacking, + + // 晶环上料事件 + [Description("晶环上料事件")] WaferLoading, + + // 晶环旋转事件 + [Description("晶环旋转事件")] WaferRotation, + + // 晶环旋转失败事件 + [Description("晶环旋转失败事件")] WaferRotationFailure, + + // 晶环下料事件 + [Description("晶环下料事件")] WaferUnloading, + + // 基板上料确认事件 + [Description("PCB上料确认事件")] PcbLoadingConfirmed, + + // 基板上料事件 + [Description("PCB上料事件")] PcbLoading, + + // 基板下料事件 + [Description("PCB下料事件")] PcbUnloading, + + // 针刺组件移动到指定位置事件 + [Description("针刺组件移动")] NeedleMoveTo, + + // 晶环组件移动到自定位置事件 + [Description("晶环组件移动")] WaferMoveTo, + + // pcb上料完成事件 + [Description("基板上料完成")] PcbLoadingCompleted, + + // pcb下料完成事件 + [Description("基板下料完成")] PcbUnloadingCompleted, + + // 晶圆上料完成事件 + [Description("基板上料完成")] WaferLoadingCompleted, + + // 晶圆下料完成事件 + [Description("基板下料完成")] WaferUnloadingCompleted, + + // 整机回零完成事件 + [Description("整机回零完成")] TotalResetCompleted, + + [Description("基板扫描完成")] PcbScanCompleted, + + [Description("晶圆扫描完成")] WaferScanCompleted, + + [Description("整体扫描完成")] ScanCompleted, + + [Description("步序信息")] StepInfo, + + [Description("生产配置界面切换")] SettingChanged, + + [Description("切换步骤")] StepChanged, + + [Description("切换菜单")] MenuChanged, + + [Description("菜单按钮切换事件")] PageChanged, + + [Description("MQTT 接收到数据")] MqttMessage, + + StartScan, + + [Description("错误日志")] PopLog, + [Description("滚动日志")] RollingLog +} \ No newline at end of file diff --git a/DispenserCommon/Exceptions/AssertException.cs b/DispenserCommon/Exceptions/AssertException.cs new file mode 100644 index 0000000..0d6c8c8 --- /dev/null +++ b/DispenserCommon/Exceptions/AssertException.cs @@ -0,0 +1,13 @@ +using DispenserCommon.Enums; +using DispenserUI.Exceptions; + +namespace DispenserCommon.Exceptions; + +public class AssertException( + string message, + string? code = null, + string? module = null, + Exception? exception = null, + ExceptionLevel? level = null) : BizException(message, code, module, exception, level) +{ +} \ No newline at end of file diff --git a/DispenserCommon/Exceptions/BizException.cs b/DispenserCommon/Exceptions/BizException.cs new file mode 100644 index 0000000..494aa99 --- /dev/null +++ b/DispenserCommon/Exceptions/BizException.cs @@ -0,0 +1,20 @@ +using DispenserCommon.Enums; + +namespace DispenserUI.Exceptions; + +/// +/// 业务异常 +/// +public class BizException( + string message, + string? code = null, + string? module = null, + Exception? exception = null, + ExceptionLevel? level = null) : ApplicationException(message, exception) +{ + public ExceptionLevel Level { get; set; } = level ?? ExceptionLevel.NORMAL; + + public string? Module { get; set; } = module; + + public string? Code { get; set; } = code; +} \ No newline at end of file diff --git a/DispenserCommon/Exceptions/CameraException.cs b/DispenserCommon/Exceptions/CameraException.cs new file mode 100644 index 0000000..498679a --- /dev/null +++ b/DispenserCommon/Exceptions/CameraException.cs @@ -0,0 +1,11 @@ +using DispenserCommon.Enums; +using DispenserUI.Exceptions; + +namespace DispenserCommon.Exceptions; + +public class CameraException( + string message, + string? code = null, + Exception? exception = null, + ExceptionLevel? level = null) + : BizException(message, code, "视觉相机异常", exception, level); \ No newline at end of file diff --git a/DispenserCommon/Exceptions/ScannerException.cs b/DispenserCommon/Exceptions/ScannerException.cs new file mode 100644 index 0000000..9078d82 --- /dev/null +++ b/DispenserCommon/Exceptions/ScannerException.cs @@ -0,0 +1,15 @@ +using DispenserCommon.Enums; +using DispenserUI.Exceptions; + +namespace DispenserCommon.Exceptions; + +public class ScannerException( + ScannerTypeEnum type, + string message, + string? code = null, + Exception? exception = null, + ExceptionLevel? level = null) + : BizException(message, code, "扫码枪异常", exception, level) +{ + public ScannerTypeEnum Type { get; set; } = type; +} \ No newline at end of file diff --git a/DispenserCommon/Interface/Instant.cs b/DispenserCommon/Interface/Instant.cs new file mode 100644 index 0000000..5487018 --- /dev/null +++ b/DispenserCommon/Interface/Instant.cs @@ -0,0 +1,5 @@ +namespace DispenserCommon.Interface; + +public interface Instant +{ +} \ No newline at end of file diff --git a/DispenserCommon/Ioc/Component.cs b/DispenserCommon/Ioc/Component.cs new file mode 100644 index 0000000..179801a --- /dev/null +++ b/DispenserCommon/Ioc/Component.cs @@ -0,0 +1,12 @@ +namespace DispenserCommon.Ioc; + +/// +/// 用于声明当前类需要交由IOC管理 +/// +[AttributeUsage(AttributeTargets.Class)] +public class Component(Type? type = null, string? name = null) : Attribute +{ + public Type? Type => type; + + public string? Name => name; +} \ No newline at end of file diff --git a/DispenserCommon/Lazy/ServiceCollectionLazyExtendtions.cs b/DispenserCommon/Lazy/ServiceCollectionLazyExtendtions.cs new file mode 100644 index 0000000..30ae5df --- /dev/null +++ b/DispenserCommon/Lazy/ServiceCollectionLazyExtendtions.cs @@ -0,0 +1,21 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace DispenserCommon.Lazy; + +public static class IServiceCollectionExtendtions +{ + public static IServiceCollection AddLazyResolution(this IServiceCollection services) + { + return services.AddTransient( + typeof(Lazy<>), + typeof(LazilyResolved<>)); + } + + private class LazilyResolved : Lazy + { + public LazilyResolved(IServiceProvider serviceProvider) + : base(serviceProvider.GetRequiredService) + { + } + } +} \ No newline at end of file diff --git a/DispenserCommon/LogUtils/DispenserLogSink.cs b/DispenserCommon/LogUtils/DispenserLogSink.cs new file mode 100644 index 0000000..d4b714f --- /dev/null +++ b/DispenserCommon/LogUtils/DispenserLogSink.cs @@ -0,0 +1,32 @@ +using System.Text; +using DispenserCommon.DTO; +using DispenserCommon.Events; +using Serilog.Core; +using Serilog.Events; + +namespace DispenserCommon.LogUtils; + +public class DispenserLogSink : ILogEventSink +{ + public void Emit(LogEvent logEvent) + { + //错误日志,用于弹窗提示 + if (IsPopLog(logEvent)) + EventBus.Publish(EventType.PopLog, + new LogMessage(logEvent.RenderMessage(), LogEventLevel.Error)); + + var sb = new StringBuilder(logEvent.RenderMessage()); + if (logEvent.Exception != null) + { + sb.Append(logEvent.Exception.Message); + } + + EventBus.Publish(EventType.RollingLog, new LogMessage(sb.ToString(), logEvent.Level)); + } + + private bool IsPopLog(LogEvent logEvent) + { + return logEvent.Properties.ContainsKey("IsPop") && + logEvent.Properties["IsPop"].ToString() == true.ToString(); + } +} \ No newline at end of file diff --git a/DispenserCommon/Queue/BitmapQueue.cs b/DispenserCommon/Queue/BitmapQueue.cs new file mode 100644 index 0000000..38da397 --- /dev/null +++ b/DispenserCommon/Queue/BitmapQueue.cs @@ -0,0 +1,63 @@ +using System.Collections.Concurrent; +using System.Drawing; + +namespace DispenserCommon.Queue; + +public class BitmapQueue +{ + private bool _isProcessing; + private readonly ConcurrentQueue _queue = new(); + private readonly Action ImageProcessor; + + public BitmapQueue(Delegate @delegate) + { + ImageProcessor = (Action)@delegate; + ; + } + + public void Clear() + { + _queue.Clear(); + } + + // 将BitmapItem加入队列 + public void Enqueue(Bitmap item) + { + _queue.Enqueue(item); + StartProcessing(); // 尝试开始处理(如果尚未开始) + } + + // 开始处理队列中的BitmapItem + private void StartProcessing() + { + if (_isProcessing) return; + _isProcessing = true; + + Task.Run(ProcessQueue).ContinueWith(t => + { + // 处理完成后的逻辑(如果有的话) + _isProcessing = false; + }); + } + + // 异步处理队列 + private async Task ProcessQueue() + { + while (_queue.TryDequeue(out var item)) + try + { + // 这里处理Bitmap,例如保存、修改等 + await ProcessBitmapAsync(item); + } + finally + { + // 确保释放资源 + item.Dispose(); + } + } + + private Task ProcessBitmapAsync(Bitmap item) + { + return Task.Run(() => { ImageProcessor(item); }); + } +} \ No newline at end of file diff --git a/DispenserCommon/Scheduler/DelayScheduler.cs b/DispenserCommon/Scheduler/DelayScheduler.cs new file mode 100644 index 0000000..6c47347 --- /dev/null +++ b/DispenserCommon/Scheduler/DelayScheduler.cs @@ -0,0 +1,39 @@ +using Serilog; + +namespace DispenserCommon.Scheduler; + +/// +/// 延时定时任务 +/// +public class DelayScheduler +{ + /// + /// 设定延时任务 + /// + /// + /// + /// + /// + /// + public static async void Delay(Action action, TimeSpan delay, CancellationToken cancellationToken = default) + { + try + { + if (action == null) throw new ArgumentNullException(nameof(action)); + if (delay.TotalMilliseconds < 0) + throw new ArgumentOutOfRangeException(nameof(delay), "延时时间不能为负数"); + + await Task.Delay(delay, cancellationToken); + if (cancellationToken.IsCancellationRequested) + { + return; + } + + action(); + } + catch (Exception e) + { + Log.Error(e, "延时任务执行失败"); + } + } +} \ No newline at end of file diff --git a/DispenserCommon/Scheduler/ExecuteTask.cs b/DispenserCommon/Scheduler/ExecuteTask.cs new file mode 100644 index 0000000..20d8b1f --- /dev/null +++ b/DispenserCommon/Scheduler/ExecuteTask.cs @@ -0,0 +1,39 @@ +namespace DispenserCommon.Scheduler; + +/// +/// 通过定时执行某个委托方法 +/// +public class ExecuteTask : ITask +{ + private readonly Timer _timer; + + public ExecuteTask(string name, Action action, int interval = 100) + { + Name = name; + Interval = interval; + Action = action; + _timer = new Timer(_ => { Run(); }, null, TimeSpan.Zero, TimeSpan.FromMilliseconds(Interval)); + } + + public int Interval { get; set; } + + public string Name { get; set; } + + public Action Action { get; set; } + + /// + /// 移除任务进行定时任务释放 + /// + public void Dispose() + { + _timer.Dispose(); + } + + /// + /// 定义一个虚方法, 子类可以重写该方法实现具体的轮询逻辑 + /// + public void Run() + { + Action.Invoke(); + } +} \ No newline at end of file diff --git a/DispenserCommon/Scheduler/ITask.cs b/DispenserCommon/Scheduler/ITask.cs new file mode 100644 index 0000000..aec94e6 --- /dev/null +++ b/DispenserCommon/Scheduler/ITask.cs @@ -0,0 +1,6 @@ +namespace DispenserCommon.Scheduler; + +public interface ITask : IDisposable +{ + public void Run(); +} \ No newline at end of file diff --git a/DispenserCommon/Scheduler/JobScheduler.cs b/DispenserCommon/Scheduler/JobScheduler.cs new file mode 100644 index 0000000..b696734 --- /dev/null +++ b/DispenserCommon/Scheduler/JobScheduler.cs @@ -0,0 +1,47 @@ +using System.Collections.Concurrent; +using Serilog; + +namespace DispenserCommon.Scheduler; + +public class JobScheduler +{ + private static readonly ConcurrentDictionary Tasks = new(); + + /// + /// 添加调度任务 + /// + /// + /// + /// + /// + public static void AddTask(string name, Action action, int interval = 100, int delay = 0) + { + try + { + Task.Run(async () => + { + if (Tasks.ContainsKey(name)) return; + + if (delay > 0) + { + await Task.Delay(delay); + } + + Tasks[name] = new ExecuteTask(name, action, interval); + }); + } + catch (Exception e) + { + Log.Error(e, $"添加 {name} 任务失败"); + } + } + + /// + /// 移除任务 + /// + /// + public static void RemoveTask(string name) + { + if (Tasks.TryRemove(name, out var task)) task.Dispose(); + } +} \ No newline at end of file diff --git a/DispenserCommon/Scheduler/PollingTask.cs b/DispenserCommon/Scheduler/PollingTask.cs new file mode 100644 index 0000000..af24473 --- /dev/null +++ b/DispenserCommon/Scheduler/PollingTask.cs @@ -0,0 +1,35 @@ +namespace DispenserCommon.Scheduler; + +/// +/// 轮询任务 +/// +public abstract class PollingTask : ITask +{ + private readonly Timer _timer; + + protected PollingTask(string name, int interval = 100) + { + Interval = interval; + Name = name; + _timer = new Timer(_ => { Run(); }, null, TimeSpan.Zero, TimeSpan.FromMilliseconds(Interval)); + } + + public int Interval { get; set; } + + public string Name { get; set; } + + /// + /// 移除任务进行定时任务释放 + /// + public void Dispose() + { + _timer.Dispose(); + } + + /// + /// 定义一个虚方法, 子类可以重写该方法实现具体的轮询逻辑 + /// + public virtual void Run() + { + } +} \ No newline at end of file diff --git a/DispenserCommon/Scheduler/SchedulerHelper.cs b/DispenserCommon/Scheduler/SchedulerHelper.cs new file mode 100644 index 0000000..16669bd --- /dev/null +++ b/DispenserCommon/Scheduler/SchedulerHelper.cs @@ -0,0 +1,98 @@ +using Quartz; +using Quartz.Impl; +using Serilog; + +namespace DispenserCommon.scheduler; + +public class SchedulerHelper +{ + private static readonly Lazy lazyScheduler = new(() => InitSchedulerAsync().GetAwaiter().GetResult()); + + private static readonly Dictionary _jobDetails = new(); + + private static IScheduler Scheduler => lazyScheduler.Value; + + private static async Task InitSchedulerAsync() + { + try + { + return await new StdSchedulerFactory().GetScheduler(); + } + catch (Exception ex) + { + Log.Error($"Failed to initialize scheduler: {ex.Message}"); + throw; + } + } + + public static async Task Start() + { + await Scheduler.Start(); + } + + public static async Task SchedulerInterval(Dictionary? data, int interval, + string group = "defaultGroup") where T : IJob + { + var job = CreateJob(data, group); + + var trigger = TriggerBuilder.Create() + .WithIdentity(typeof(T).Name, group) + .StartNow() + .WithSimpleSchedule(x => x.WithIntervalInSeconds(interval).RepeatForever()) + .Build(); + + await Scheduler.ScheduleJob(job, trigger); + } + + public static async Task SchedulerCorn(Dictionary? data, string? cronExpression, + string group = "defaultGroup") where T : IJob + { + var job = CreateJob(data, group); + + var trigger = TriggerBuilder.Create() + .WithIdentity(typeof(T).Name, group) + .StartNow() + .WithSchedule(CronScheduleBuilder.CronSchedule(cronExpression)) + .Build(); + + await Scheduler.ScheduleJob(job, trigger); + } + + private static IJobDetail CreateJob(Dictionary? data, string group) where T : IJob + { + if (_jobDetails.ContainsKey(typeof(T).Name)) return _jobDetails[typeof(T).Name]; + + var job = JobBuilder.Create() + .WithIdentity(typeof(T).Name, group) + .Build(); + + if (data != null && data.Count > 0) + foreach (var item in data) + job.JobDataMap.Add(item.Key, item.Value); + + _jobDetails[typeof(T).Name] = job; + return job; + } + + public static async Task PauseJob(string group = "defaultGroup") + { + if (_jobDetails.ContainsKey(typeof(T).Name)) await Scheduler.PauseJob(JobKey.Create(typeof(T).Name, group)); + } + + public static async Task ResumeJob(string group = "defaultGroup") + { + if (_jobDetails.ContainsKey(typeof(T).Name)) await Scheduler.ResumeJob(JobKey.Create(typeof(T).Name, group)); + } + + public static async Task Shutdown() + { + if (!Scheduler.IsShutdown) await Scheduler.Shutdown(); + } + + public static async Task TriggerOnceImmediately(string group = "defaultGroup") where T : IJob + { + if (!_jobDetails.ContainsKey(typeof(T).Name)) return; + + await Scheduler.TriggerJob(new JobKey(typeof(T).Name, group)); + } +} \ No newline at end of file diff --git a/DispenserCommon/Scheduler/TaskWaiter.cs b/DispenserCommon/Scheduler/TaskWaiter.cs new file mode 100644 index 0000000..7bcfd00 --- /dev/null +++ b/DispenserCommon/Scheduler/TaskWaiter.cs @@ -0,0 +1,47 @@ +using Castle.Core.Internal; +using DispenserCommon.Atrributes; +using Serilog; + +namespace DispenserCommon.Scheduler; + +public class TaskWaiter +{ + /// + /// 在 + /// + /// + /// + /// + /// + public static async Task WaitingFor(Func action, long timeout = 60000, + CancellationTokenSource? cts = null) + { + var method = action.Method; + var blockTask = method.GetAttribute(); + + var taskName = blockTask?.Name ?? method.Name; + var span = TimeSpan.FromMilliseconds(timeout); + var endTime = DateTime.Now.Add(span); + + return await Task.Run(() => + { + while (DateTime.Now < endTime) + { + if (cts is { IsCancellationRequested: true }) + { + return false; + } + + if (action()) + { + Log.Information($"{taskName} 任务等待完成"); + return true; + } + + Thread.Sleep(200); + } + + return false; + }); + } +} \ No newline at end of file diff --git a/DispenserCommon/Utils/AffineTransformCalculator.cs b/DispenserCommon/Utils/AffineTransformCalculator.cs new file mode 100644 index 0000000..b9b2b41 --- /dev/null +++ b/DispenserCommon/Utils/AffineTransformCalculator.cs @@ -0,0 +1,34 @@ +using MathNet.Numerics.LinearAlgebra; + +namespace DispenserCommon.Utils; + +public class AffineTransformCalculator +{ + public static Matrix Calculate3x2AffineTransform(List> acupunctureCoordinates, + List> binCoordinates) + { + if (acupunctureCoordinates.Count != binCoordinates.Count || acupunctureCoordinates.Count < 3) + throw new ArgumentException("Both sets must have the same number of points and at least three points."); + + // 创建矩阵X和Y + var rowCount = acupunctureCoordinates.Count; + var matrixX = Matrix.Build.Dense(rowCount, 3); + var matrixY = Matrix.Build.Dense(rowCount, 2); + + for (var i = 0; i < rowCount; i++) + { + matrixX[i, 0] = acupunctureCoordinates[i].Item1; // x-coordinate + matrixX[i, 1] = acupunctureCoordinates[i].Item2; // y-coordinate + matrixX[i, 2] = 1; // homogeneous coordinate + + matrixY[i, 0] = binCoordinates[i].Item1; // x'-coordinate + matrixY[i, 1] = binCoordinates[i].Item2; // y'-coordinate + } + + // 计算仿射变换矩阵 A = Y * X^(-1) + var pseudoInverseX = matrixX.PseudoInverse(); + var transformMatrix = matrixY.Transpose() * pseudoInverseX.Transpose(); + + return transformMatrix; + } +} \ No newline at end of file diff --git a/DispenserCommon/Utils/ApiClient.cs b/DispenserCommon/Utils/ApiClient.cs new file mode 100644 index 0000000..2201ebb --- /dev/null +++ b/DispenserCommon/Utils/ApiClient.cs @@ -0,0 +1,75 @@ +using System.Net.Http.Json; + +namespace DispenserCommon.Utils; + +public class ApiClient : IDisposable +{ + private static readonly HttpClient Client; + + static ApiClient() + { + Client = new HttpClient(); + Client.DefaultRequestHeaders.Accept.Clear(); + Client.DefaultRequestHeaders.Add("Accept", "application/json"); + Client.DefaultRequestHeaders.Add("Content-Type", "application/json"); + } + + /// + /// 异步Get请求 + /// + /// + /// + /// + public static async Task GetAsync(string url) + { + using var response = await Client.GetAsync(url); + response.EnsureSuccessStatusCode(); + return await response.Content.ReadFromJsonAsync(); + } + + /// + /// 异步 Post请求 + /// + /// + /// + /// + /// + public static async Task PostAsync(string url, object? data) + { + using var response = await Client.PostAsJsonAsync(url, data); + response.EnsureSuccessStatusCode(); + return await response.Content.ReadFromJsonAsync(); + } + + /// + /// 异步 Put请求 + /// + /// + /// + /// + /// + public static async Task PutAsync(string url, object? data) + { + using var response = await Client.PutAsJsonAsync(url, data); + response.EnsureSuccessStatusCode(); + return await response.Content.ReadFromJsonAsync(); + } + + /// + /// 异步 Delete请求 + /// + /// + /// + /// + public static async Task DeleteAsync(string url) + { + using var response = await Client.DeleteAsync(url); + response.EnsureSuccessStatusCode(); + return await response.Content.ReadFromJsonAsync(); + } + + public void Dispose() + { + Client.Dispose(); + } +} \ No newline at end of file diff --git a/DispenserCommon/Utils/AssertUtil.cs b/DispenserCommon/Utils/AssertUtil.cs new file mode 100644 index 0000000..0af4618 --- /dev/null +++ b/DispenserCommon/Utils/AssertUtil.cs @@ -0,0 +1,105 @@ +using DispenserCommon.Exceptions; + +namespace DispenserCommon.Utils; + +/// +/// 断言工具类 +/// +public class AssertUtil +{ + /// + /// 判断条件是否为真 + /// + /// + /// + public static void IsTrue(bool expression, string msg) + { + if (!expression) + { + throw new AssertException(msg); + } + } + + /// + /// 判断条件是否为假 + /// + /// + /// + public static void IsFalse(bool expression, string msg) + { + if (expression) + { + throw new AssertException(msg); + } + } + + /// + /// 集合非空断言 + /// + /// + /// + public static void NotEmpty(List? list, string msg) + { + if (list == null || list.Count == 0) + { + throw new AssertException(msg); + } + } + + /// + /// 集合空断言 + /// + /// + /// + public static void IsEmpty(List list, string msg) + { + if (list is { Count: > 0 }) + { + throw new AssertException(msg); + } + } + + + /// + /// 对象非空断言 + /// + /// + /// + /// + public static void NotNull(object? model, string msg) + { + if (model == null) + { + throw new AssertException(msg); + } + } + + + /// + /// 对象空断言 + /// + /// + /// + /// + public static void IsNull(object? model, string msg) + { + if (model != null) + { + throw new AssertException(msg); + } + } + + /// + /// 字符串非空断言 + /// + /// + /// + /// + public static void StringNotNullOrEmpty(string str, string msg) + { + if (string.IsNullOrEmpty(str)) + { + throw new AssertException(msg); + } + } +} \ No newline at end of file diff --git a/DispenserCommon/Utils/BeanUtil.cs b/DispenserCommon/Utils/BeanUtil.cs new file mode 100644 index 0000000..e80eb7c --- /dev/null +++ b/DispenserCommon/Utils/BeanUtil.cs @@ -0,0 +1,39 @@ +namespace DispenserCommon.Utils; + +/// +/// bean 工具类 +/// +public class BeanUtil +{ + /// + /// 实现Bean的属性复制 + /// + /// + /// + public static void CopyProperties(object source, object target) + { + var sourceProperties = source.GetType().GetProperties(); + var targetProperties = target.GetType().GetProperties(); + + foreach (var sourceProperty in sourceProperties) + { + var targetProperty = Array.Find(targetProperties, p => p.Name == sourceProperty.Name && + p.PropertyType == sourceProperty.PropertyType); + + if (targetProperty != null && targetProperty.CanWrite) + targetProperty.SetValue(target, sourceProperty.GetValue(source)); + } + } + + public static T CopyProperties(object source) + { + var instance = Activator.CreateInstance(); + CopyProperties(source, instance); + return instance; + } + + public static List CopyProperties(IEnumerable source) + { + return source.Select(CopyProperties).ToList(); + } +} \ No newline at end of file diff --git a/DispenserCommon/Utils/BufferQueue.cs b/DispenserCommon/Utils/BufferQueue.cs new file mode 100644 index 0000000..2a54d9a --- /dev/null +++ b/DispenserCommon/Utils/BufferQueue.cs @@ -0,0 +1,50 @@ +namespace DispenserCommon.Utils; + +/// +/// 缓冲队列 +/// +public class BufferQueue(int capacity) +{ + private readonly Queue _queue = new(); + private readonly int _capacity = Math.Min(capacity, 10); + private readonly object _lock = new(); + + // 尝试添加元素到队列中 + public bool TryEnqueue(T? item) + { + lock (_lock) + { + if (_queue.Count >= _capacity) return false; + _queue.Enqueue(item); + return true; + } + } + + // 尝试从队列中移除并返回元素 + public bool TryDequeue(out T? result) + { + lock (_lock) + { + if (_queue.Count > 0) + { + result = _queue.Dequeue(); + return true; + } + + result = default(T); + return false; + } + } + + // 获取当前队列中的元素数量 + public int Count + { + get + { + lock (_lock) + { + return _queue.Count; + } + } + } +} \ No newline at end of file diff --git a/DispenserCommon/Utils/ConfirmDialogHelper.cs b/DispenserCommon/Utils/ConfirmDialogHelper.cs new file mode 100644 index 0000000..1a45ca0 --- /dev/null +++ b/DispenserCommon/Utils/ConfirmDialogHelper.cs @@ -0,0 +1,10 @@ +namespace DispenserCommon.Utils; + +/// +/// 弹窗工具类 +/// +public interface ConfirmDialogHelper +{ + Task ShowConfirm(string title, bool showCancel = true, bool showConfirm = true, + string cancelText = "取消", string confirmText = "确认"); +} \ No newline at end of file diff --git a/DispenserCommon/Utils/CoordinateUtil.cs b/DispenserCommon/Utils/CoordinateUtil.cs new file mode 100644 index 0000000..c34b4fd --- /dev/null +++ b/DispenserCommon/Utils/CoordinateUtil.cs @@ -0,0 +1,577 @@ +using Masuit.Tools; + +namespace DispenserCommon.Utils; + +/// +/// 数组工具类 +/// +public class CoordinateUtil +{ + /// + /// 将一个二维坐标数组拆分成两个一维数组 + /// + /// 二维坐标数组 + /// X轴坐标数组 + /// Y轴坐标数组 + public static void SplitCoordinates(double[][] coordinates, out double[] xAxis, out double[] yAxis) + { + var totalPoints = coordinates.GetLength(0); + xAxis = new double[totalPoints]; + yAxis = new double[totalPoints]; + + for (var i = 0; i < totalPoints; i++) + { + // 提取x坐标 + xAxis[i] = coordinates[i][0]; + // 提取y坐标 + yAxis[i] = coordinates[i][1]; + } + } + + public static void SplitCoordinates(double[][] coordinates, out double[] xAxis, out double[] yAxis, + out double[] depth) + { + var totalPoints = coordinates.GetLength(0); + xAxis = new double[totalPoints]; + yAxis = new double[totalPoints]; + depth = new double[totalPoints]; + + for (var i = 0; i < totalPoints; i++) + { + // 提取x坐标 + xAxis[i] = coordinates[i][0]; + // 提取y坐标 + yAxis[i] = coordinates[i][1]; + // 提取针刺深度 + depth[i] = coordinates[i][3]; + } + } + + /// + /// 将两个坐标数组合并成一个二维坐标数组 + /// + /// + /// + /// + public static double[][] MergeArray(IEnumerable coordinates1, double[] coordinates2) + { + List merged = []; + merged.AddRange(coordinates1.Select((t, i) => (double[]) [t, coordinates2[i]])); + + return merged.ToArray(); + } + + + /// + /// 判断数组是否为空 + /// + /// + /// + public static bool IsEmpty(double[]? array) + { + return array == null || !array.Any(); + } + + /// + /// 判断数组是否为空 + /// + /// + /// + public static bool IsEmpty(double[][]? array) + { + return array == null || !array.Any(); + } + + /// + /// 判断数组是否为空 + /// + /// + /// + public static bool IsEmpty(double[,]? array) + { + return array == null || array.GetLength(0) == 0 || array.GetLength(1) == 0; + } + + /// + /// 合并相似点 + /// + /// + /// 相似划分窗口 + /// 行相似值判断阈值 + /// 列相似值判断阈值 + /// + public static List MergeSimilarPoints(List points, string window, + double rowSimilarPitchThreshold, + double colSimilarPitchThreshold) + { + var windowConfig = window; + var windows = windowConfig.Split(","); + points = windows + .Select(int.Parse) + .Aggregate(points, + (current, windowSize) => + MergeSimilarPoints(current, windowSize, rowSimilarPitchThreshold, colSimilarPitchThreshold)); + + return points; + } + + + /// + /// 对坐标进行分区计算,提高计算效率 + /// + /// 合并前的坐标点 + /// 分区数 + /// + private static List>> DivideIntoRegions(List points, int space) + { + // 找到坐标范围 + var minX = points.Min(p => p[0]); + var maxX = points.Max(p => p[0]); + var minY = points.Min(p => p[1]); + var maxY = points.Max(p => p[1]); + + // 计算每个区域的尺寸 + var regionWidth = (maxX - minX) / space; + var regionHeight = (maxY - minY) / space; + + // 初始化区域列表 + var regions = new List>>(space); + for (var i = 0; i < space; i++) + { + regions.Add(new List>(space)); + for (var j = 0; j < space; j++) regions[i].Add([]); + } + + // 将点分配到相应的区域 + foreach (var point in points) + { + var x = point[0]; + var y = point[1]; + var row = (int)Math.Floor((y - minY) / regionHeight); + var col = (int)Math.Floor((x - minX) / regionWidth); + + // 防止由于边界值导致的索引超出范围 + row = Math.Min(row, space - 1); + col = Math.Min(col, space - 1); + + regions[row][col].Add(point); + } + + return regions; + } + + + /// + /// 合并相似点 + /// + /// 合并前的坐标点集合 + /// 区域数 + /// 行相似值判断阈值 + /// 列相似值判断阈值 + /// + private static List MergeSimilarPoints(List points, int space, double rowSimilarPitchThreshold, + double colSimilarPitchThreshold) + { + // 将整个区域划分为 若干个区域 + var regions = DivideIntoRegions(points, space); + + var merged = new List(); + // 遍历每个区域,对每个区域进行相似点合并 + foreach (var region in regions) + foreach (var col in region) + merged.AddRange(MergePoints(col, rowSimilarPitchThreshold, colSimilarPitchThreshold)); + + return merged; + } + + /// + /// 对区域内的点进行合并处理 + /// + /// 待合并区域 + /// 行相似值判断阈值 + /// 列相似值判断阈值 + /// + private static List MergePoints(List points, double rowSimilarPitchThreshold, + double colSimilarPitchThreshold) + { + if (points.Count == 0) + return new List(); + + // 如果两个两个点的 abs(x2-x1) <= 0.2 && abs(y2-y1) <= 0.35, 那么可以认为这两个点是相似的,只保留第一个点即可 + var merged = new List(); + foreach (var point in points) + // 同当前区域的其他点进行比较 + if (merged.Count == 0) + { + merged.Add(point); + } + else + { + var similar = false; + foreach (var other in merged) + if (IsSimilar(point, other, 0.1, 0.1)) + { + Console.WriteLine( + $"{point[0]},{point[1]} 与 {other[0]},{other[1]} 相似, X差值{point[0] - other[0]}, y差值{point[1] - other[1]}"); + similar = true; + break; + } + + if (!similar) merged.Add(point); + } + + return merged; + } + + /// + /// 判断两个点是否相似 + /// + /// 第一个点 + /// 第二个点 + /// 行相似值判断阈值 + /// 列相似值判断阈值 + /// + private static bool IsSimilar(double[] p1, double[] p2, double rowSimilarPitchThreshold, + double colSimilarPitchThreshold) + { + return Math.Abs(p2[0] - p1[0]) <= rowSimilarPitchThreshold && + Math.Abs(p2[1] - p1[1]) <= colSimilarPitchThreshold; + } + + + /// + /// 从原数组中根据匹配点数组查找相似点 + /// + /// + /// + /// 行相似值判断阈值 + /// 列相似值判断阈值 + /// + public static List FindSimilarPoints(List referencePoints, List comparisonPoints, + double rowSimilarPitchThreshold, + double colSimilarPitchThreshold) + { + // 从原数组中根据匹配点数组查找相似点 + return referencePoints.AsParallel().Where(sourcePoint => + comparisonPoints.Any(matchPoint => + IsSimilar(sourcePoint, matchPoint, rowSimilarPitchThreshold, colSimilarPitchThreshold)) + ).ToList(); + } + + /// + /// 晶环和pcb坐标长度对齐 + /// + /// 原始pcb坐标 + /// 原始晶环坐标 + /// 对齐后的pcb坐标 + /// 对齐后的晶环坐标 + public static void CoordinateAlign(List pcbCoordinates, List waferCoordinates, + out List pcbAlignedCoordinates, out List waferAlignedCoordinates) + { + pcbAlignedCoordinates = []; + waferAlignedCoordinates = []; + var pcbStartIndex = 0; + var waferStartIndex = 0; + var pStart = 0; + var wStart = 0; + while (pcbStartIndex <= pcbCoordinates.Count && waferStartIndex <= waferCoordinates.Count) + if ((IsReal(pcbCoordinates, pcbStartIndex) && IsReal(waferCoordinates, waferStartIndex)) || + (!IsReal(pcbCoordinates, pcbStartIndex) && !IsReal(waferCoordinates, waferStartIndex))) + { + pcbAlignedCoordinates[pStart++] = pcbCoordinates[pcbStartIndex++]; + waferAlignedCoordinates[wStart++] = waferCoordinates[waferStartIndex++]; + } + else if (IsReal(pcbCoordinates, pcbStartIndex) && !IsReal(waferCoordinates, waferStartIndex)) + { + var pcbCoordinate = new double[3]; + Array.Copy(pcbCoordinates[pcbStartIndex], pcbCoordinate, 3); + pcbCoordinate[2] = 0; + pcbAlignedCoordinates[pStart++] = pcbCoordinate; + waferAlignedCoordinates[wStart++] = waferCoordinates[waferStartIndex++]; + } + else + { + pcbAlignedCoordinates[pStart++] = pcbCoordinates[pcbStartIndex++]; + var waferCoordinate = new double[3]; + Array.Copy(waferCoordinates[waferStartIndex], waferCoordinate, 3); + waferCoordinate[2] = 0; + waferAlignedCoordinates[wStart++] = waferCoordinate; + } + } + + /// + /// 判断是否是真实坐标 + /// + /// + /// + /// + private static bool IsReal(IReadOnlyList pcbCoordinates, int pcbStartIndex) + { + return pcbCoordinates[pcbStartIndex][2] != 0; + } + + /// + /// 从插值路径中提取动打坐标信息 + /// + /// 插值后的坐标数组 + /// 开始的坐标 + /// 借宿的坐标 + public static List SubCurrentBatchCoordinate(IEnumerable coordinates, double[] startPoint, + double[] endPoint) + { + // 保存当前截取后的新坐标 + List newCoordinates = []; + var target = false; + + foreach (var coordinate in coordinates) + { + if (Equals(coordinate, startPoint)) + { + target = true; + } + else if (Equals(coordinate, endPoint)) + { + newCoordinates.Add(coordinate); + break; + } + + if (target) + // 都是目标值 + newCoordinates.Add(coordinate); + } + + return newCoordinates; + } + + public static bool Equals(double[] a, double[] b) + { + if (IsEmpty(a) || IsEmpty(b)) return false; + + return Math.Abs(a[0] - b[0]) < 0.00001 && Math.Abs(a[1] - b[1]) < 0.00001; + } + + public static double[][] FillDefaultFlagIfNotExist(double[][] path, double defaultFlag = 1) + { + if (path.IsNullOrEmpty()) return path; + if (path[0].Length == 2) return path.Select(v => new[] { v[0], v[1], defaultFlag }).ToArray(); + + return path; + } + + public static bool IsRealPoint(double flag) + { + return Math.Abs(Math.Round(flag, MidpointRounding.ToEven) - 1) < 0.0001; + } + + public static double GetTowCoordinatesDistinct(double[] p1, double[] p2) + { + return Math.Sqrt(Math.Pow(p1[0] - p2[0], 2) + Math.Pow(p1[1] - p2[1], 2)); + } + + public static double[][] SubPcbPath(double[][] path, int rowAmount, int startColumn, int pathRowAmount) + { + List result = new(); + + if ((startColumn + pathRowAmount - 1) > rowAmount) + { + pathRowAmount = rowAmount - startColumn + 1; + } + + var rows = RowBy(path.ToList(), rowAmount); + + for (var i = 0; i < rows.Count; i++) + { + var row = rows[i]; + List chunkRow; + if (i % 2 == 0) + { + chunkRow = row.Skip(startColumn - 1).Take(pathRowAmount).ToList(); + } + else + { + chunkRow = row.Skip(rowAmount - (pathRowAmount + startColumn - 1)).Take(pathRowAmount).ToList(); + } + + result.AddRange(chunkRow); + } + + return result.ToArray(); + } + + static List> RowBy(List source, int chunkSize) + { + List> result = new List>(); + + for (int i = 0; i < source.Count; i += chunkSize) + { + result.Add(source.GetRange(i, Math.Min(chunkSize, source.Count - i))); + } + + return result; + } + + public static double[] AffineTransform(double[,] matrix, IReadOnlyList point) + { + if (matrix.GetLength(0) != 3 || matrix.GetLength(1) != 3) + throw new ArgumentException("Matrix must be a 3x3 array."); + + return new double[] + { + matrix[0, 0] * point[0] + matrix[0, 1] * point[1] + matrix[0, 2] * point[2], + matrix[1, 0] * point[0] + matrix[1, 1] * point[1] + matrix[1, 2] * point[2] + }; + } + + public static double[][] Convert2DArrayToJaggedArray(double[,] twoDArray) + { + int rows = twoDArray.GetLength(0); // 获取第一维度的长度,即行数 + int cols = twoDArray.GetLength(1); // 获取第二维度的长度,即列数 + + double[][] jaggedArray = new double[rows][]; // 创建外层数组 + + for (int i = 0; i < rows; i++) + { + jaggedArray[i] = new double[cols]; // 每一行创建一个新的内层数组 + for (int j = 0; j < cols; j++) + { + jaggedArray[i][j] = twoDArray[i, j]; // 复制元素 + } + } + + return jaggedArray; + } + + public static List> GroupPointsIntoRows(double[][] pointsArray, double yThreshold) + { + // 对点数组进行排序 + var pointsArraySorted = pointsArray.OrderBy(p => p[0]).ToArray(); + List> rows = new List>(); + + foreach (var point in pointsArraySorted) + { + bool placed = false; + foreach (var row in rows) + { + if (Math.Abs(row.Last()[1] - point[1]) <= yThreshold && point[0] > row.Last()[0]) + { + row.Add(point); + placed = true; + break; + } + } + + if (!placed) + { + rows.Add(new List { point }); + } + } + + return rows.OrderBy(p => p[0][1]).ToList(); + } + + public static List ConvertMatrixToList(double[,] matrix) + { + int rows = matrix.GetLength(0); + int columns = matrix.GetLength(1); + var list = new List(); + + for (int i = 0; i < rows; i++) + { + double[] rowArray = new double[columns]; + for (int j = 0; j < columns; j++) + { + rowArray[j] = matrix[i, j]; + } + + list.Add(rowArray); + } + + return list; + } + + + /// + /// 将 坐标数组转为 矩阵 + /// + /// 坐标数组 + /// 标准分列数 + public static double[,][] Array2Matrix(double[][] array, int cols) + { + // 总的行数 + var rows = array.Length / cols; + + var matrix = new double[rows, cols][]; + + for (var i = 0; i < rows; i++) + { + for (var j = 0; j < cols; j++) + { + matrix[i, j] = array[i * cols + j]; + } + } + + return matrix; + } + + /// + /// 从矩阵的中心位置将其一分为二 + /// + /// + /// + public static (double[,][], double[,][]) SplitMatrixByHalf(double[,][] array) + { + var rows = array.GetLength(0); + var cols = array.GetLength(1); + + var middle = cols / 2; + + var part1 = new double[rows, middle][]; + var part2 = new double[rows, cols - middle][]; + + for (var i = 0; i < rows; i++) + { + for (var j = 0; j < cols; j++) + { + if (j < middle) + part1[i, j] = array[i, j]; + else + part2[i, j - middle] = array[i, j]; + } + } + + return (part1, part2); + } + + /// + /// 将矩阵数组转为弓字形数组 + /// + /// + /// + public static double[][] ConvertMatrix2BowShapeArray(double[,][] matrix) + { + var rows = matrix.GetLength(0); + var cols = matrix.GetLength(1); + var result = new double[rows * cols][]; + var index = 0; + + for (var d = 0; d < rows + cols - 1; d++) + { + if (d % 2 == 0) // 从左下角到右上角 + { + for (var i = Math.Min(d, rows - 1); i >= Math.Max(0, d - cols + 1); i--) + { + result[index++] = matrix[i, d - i]; + } + } + else // 从右上角到左下角 + { + for (var i = Math.Max(0, d - cols + 1); i <= Math.Min(d, rows - 1); i++) + { + result[index++] = matrix[i, d - i]; + } + } + } + + return result; + } +} \ No newline at end of file diff --git a/DispenserCommon/Utils/INIFileReader.cs b/DispenserCommon/Utils/INIFileReader.cs new file mode 100644 index 0000000..9d41030 --- /dev/null +++ b/DispenserCommon/Utils/INIFileReader.cs @@ -0,0 +1,38 @@ +using System.Runtime.InteropServices; +using System.Text; + +namespace DispenserCommon.Utils; + +public class INIFileReader +{ + [DllImport("kernel32")] + private static extern long WritePrivateProfileString(string section, string key, string val, string filePath); + + // 声明INI文件的读操作函数 GetPrivateProfileString() + [DllImport("kernel32")] + private static extern int GetPrivateProfileString(string section, string key, string def, StringBuilder retVal, + int size, string filePath); + + /// 写入INI的方法 + public void INIWrite(string section, string key, string value, string path) + { + // section=配置节点名称,key=键名,value=返回键值,path=路径 + WritePrivateProfileString(section, key, value, path); + } + + //读取INI的方法 + public string INIRead(string section, string key, string path) + { + // 每次从ini中读取多少字节 + var temp = new StringBuilder(255); + // section=配置节点名称,key=键名,temp=上面,path=路径 + GetPrivateProfileString(section, key, "", temp, 255, path); + return temp.ToString(); + } + + //删除一个INI文件 + public void INIDelete(string FilePath) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/DispenserCommon/Utils/JsonUtil.cs b/DispenserCommon/Utils/JsonUtil.cs new file mode 100644 index 0000000..f0e107e --- /dev/null +++ b/DispenserCommon/Utils/JsonUtil.cs @@ -0,0 +1,68 @@ +using Newtonsoft.Json; +using Serilog; + +namespace DispenserCommon.Utils; + +public class JsonUtil +{ + public static string ToJson(object obj) + { + try + { + return JsonConvert.SerializeObject(obj); + } + catch (Exception e) + { + throw new ArgumentException($" 无效的json 对象 {obj} "); + } + } + + public static Dictionary? ToDictionary(string json) + { + try + { + return JsonConvert.DeserializeObject>(json); + } + catch (Exception e) + { + throw new ArgumentException($" 无效的json 对象 {json} "); + } + } + + public static T FromJson(string json) + { + try + { + return JsonConvert.DeserializeObject(json); + } + catch (Exception e) + { + Log.Error($"json解析异常 {e.Message}"); + return default; + } + } + + public static object? FromJson(Type type, string json) + { + try + { + return JsonConvert.DeserializeObject(json, type); + } + catch (Exception e) + { + throw new ArgumentException($" 无效的json 字符串 {json} "); + } + } + + public static T FromJsonOrDefault(string json) + { + try + { + return JsonConvert.DeserializeObject(json); + } + catch (Exception e) + { + return default; + } + } +} \ No newline at end of file diff --git a/DispenserCommon/Utils/LineEquationUtil.cs b/DispenserCommon/Utils/LineEquationUtil.cs new file mode 100644 index 0000000..e8baf58 --- /dev/null +++ b/DispenserCommon/Utils/LineEquationUtil.cs @@ -0,0 +1,46 @@ +namespace DispenserCommon.Utils; + +public class LineEquationUtil +{ + public static double CalculateX(LineEquation lineEquation, double y) + { + return lineEquation.CalculateXCoordinate(y); + } + + public static double CalculateY(LineEquation lineEquation, double x) + { + return lineEquation.CalculateYCoordinate(x); + } + + public static LineEquation CreateLineEquation(double x1, double y1, double x2, double y2) + { + return new LineEquation(x1, y1, x2, y2); + } + + public class LineEquation + { + public LineEquation(double x1, double y1, double x2, double y2) + { + CalculateSlopeAndIntercept(x1, y1, x2, y2); + } + + public double Slope { get; private set; } + public double Intercept { get; private set; } + + private void CalculateSlopeAndIntercept(double x1, double y1, double x2, double y2) + { + Slope = (y2 - y1) / (x2 - x1); + Intercept = y1 - Slope * x1; + } + + public double CalculateXCoordinate(double y) + { + return (y - Intercept) / Slope; + } + + public double CalculateYCoordinate(double x) + { + return Slope * x + Intercept; + } + } +} \ No newline at end of file diff --git a/DispenserCommon/Utils/Md5Util.cs b/DispenserCommon/Utils/Md5Util.cs new file mode 100644 index 0000000..1ee3932 --- /dev/null +++ b/DispenserCommon/Utils/Md5Util.cs @@ -0,0 +1,24 @@ +using System.Security.Cryptography; +using System.Text; + +namespace DispenserCommon.Utils; + +/// +/// MD5 加密工具类 +/// +public class Md5Util +{ + public static string Md5(string input) + { + // 创建一个MD5对象 + // 将输入字符串转换为字节数组 + var inputBytes = Encoding.UTF8.GetBytes(input); + // 计算输入字节数组的哈希值 + var bytes = MD5.HashData(inputBytes); + // 将字节数组转换为字符串 + var hashString = new StringBuilder(); + foreach (var b in bytes) hashString.Append($"{b:x2}"); + + return hashString.ToString(); + } +} \ No newline at end of file diff --git a/DispenserCommon/Utils/MessageBoxHelper.cs b/DispenserCommon/Utils/MessageBoxHelper.cs new file mode 100644 index 0000000..8bf901b --- /dev/null +++ b/DispenserCommon/Utils/MessageBoxHelper.cs @@ -0,0 +1,154 @@ +using Avalonia.Controls; +using Avalonia.Threading; +using MsBox.Avalonia; +using MsBox.Avalonia.Base; +using MsBox.Avalonia.Dto; +using MsBox.Avalonia.Models; +using Serilog; + +namespace DispenserCommon.Utils; + +/// +/// 用于弹窗提示 +/// +public static class MessageBoxHelper +{ + private const string SUCCESS = "Assets/success.png"; + private const string INFO = "Assets/info.png"; + private const string WARNING = "Assets/warning.png"; + private const string ERROR = "Assets/error.png"; + private const string NOTIFY = "Assets/notify.png"; + + // 定义 5分钟的滑动时间窗口 + private static readonly SlidingWindow SlidingWindow = new(5 * 60 * 1000); + + + /// + /// 构建弹窗对象 + /// + /// 消息标题,用于显示在弹窗最顶方 + /// 消息内容 + /// 内容标题,用于显示在内容的顶部 + /// 弹窗左上角icon + /// + private static IMsBox CreateMessageBox( + string title, + string msg, + string contentTitle = "", + string icon = "Assets/info.png") + { + return MessageBoxManager.GetMessageBoxCustom( + new MessageBoxCustomParams + { + ButtonDefinitions = new List + { + new() { Name = "关闭", IsCancel = true } + }, + WindowIcon = new WindowIcon(icon), + ContentTitle = title, + ContentHeader = contentTitle, + ContentMessage = msg, + WindowStartupLocation = WindowStartupLocation.CenterOwner, + CanResize = false, + MinWidth = 300, + MinHeight = 200, + MaxWidth = 800, + MaxHeight = 1000, + SizeToContent = SizeToContent.WidthAndHeight, + ShowInCenter = true, + Topmost = true + }); + } + + private static async void Show(string title, string msg, string icon, string contentTitle = "", bool filter = true, + Action? callback = null) + { + try + { + // 对弹窗内容进行滤波,减少相同内容的弹窗频次 + if (filter && !SlidingWindow.AllowValue(msg)) return; + + await Dispatcher.UIThread.InvokeAsync(async () => + { + var box = CreateMessageBox(title, msg, contentTitle, icon); + await box.ShowWindowAsync(); + }); + if (callback != null) callback(); + } + catch (Exception e) + { + Log.Error(e, "弹窗出现异常"); + } + } + + + /// + /// 消息级别弹窗 + /// + /// 消息内容 + /// 消息标题,默认为空字符串,不显示 + /// 弹窗标题 + /// 是否过滤弹窗 + /// + public static void Info(string msg, string contentTitle = "", string title = "提示", bool filter = true, + Action? callback = null) + { + Show(title, msg, INFO, contentTitle, filter, callback); + } + + /// + /// 告警级别弹窗 + /// + /// 消息内容 + /// 消息标题,默认为空字符串,不显示 + /// 弹窗标题 + /// 是否过滤弹窗 + /// + public static void Warning(string msg, string contentTitle = "", string title = "警告", bool filter = true, + Action? callback = null) + { + Show(title, msg, WARNING, contentTitle, filter, callback); + } + + /// + /// 错误级别弹窗 + /// + /// 消息内容 + /// 消息标题,默认为空字符串,不显示 + /// 弹窗标题 + /// 是否过滤弹窗 + /// + public static void Error(string msg, string contentTitle = "", string title = "错误", bool filter = true, + Action? callback = null) + { + Show(title, msg, ERROR, contentTitle, filter, callback); + } + + /// + /// 通知级别弹窗 + /// + /// 消息内容 + /// 消息标题,默认为空字符串,不显示 + /// 弹窗标题 + /// 是否过滤弹窗 + /// + public static void Notify(string msg, string contentTitle = "", string title = "通知", bool filter = true, + Action? callback = null) + { + Show(title, msg, NOTIFY, contentTitle, filter, callback); + } + + /// + /// 成功通知级别弹窗 + /// + /// 消息内容 + /// 消息标题,默认为空字符串,不显示 + /// 弹窗标题 + /// 是否过滤弹窗 + /// + public static void Success(string msg, string contentTitle = "", string title = "成功", bool filter = true, + Action? callback = null) + { + Show(title, msg, SUCCESS, contentTitle, filter, callback); + } +} \ No newline at end of file diff --git a/DispenserCommon/Utils/RegistryHelper.cs b/DispenserCommon/Utils/RegistryHelper.cs new file mode 100644 index 0000000..3201a99 --- /dev/null +++ b/DispenserCommon/Utils/RegistryHelper.cs @@ -0,0 +1,32 @@ +using Microsoft.Win32; + +namespace DispenserCommon.Utils; + +/// +/// 注册表工具类 +/// +public static class RegistryHelper +{ + public static void WriteValue(string keyPath, string valueName, object value, RegistryValueKind valueKind) + { + using var key = Registry.CurrentUser.CreateSubKey(keyPath); + key.SetValue(valueName, value, valueKind); + } + + public static object? ReadValue(string keyPath, string valueName) + { + using var key = Registry.CurrentUser.OpenSubKey(keyPath); + return key?.GetValue(valueName); + } + + public static void DeleteValue(string keyPath, string valueName) + { + using var key = Registry.CurrentUser.OpenSubKey(keyPath, writable: true); + key?.DeleteValue(valueName, false); + } + + public static void DeleteKey(string keyPath) + { + Registry.CurrentUser.DeleteSubKeyTree(keyPath, throwOnMissingSubKey: false); + } +} \ No newline at end of file diff --git a/DispenserCommon/Utils/RetryHelper.cs b/DispenserCommon/Utils/RetryHelper.cs new file mode 100644 index 0000000..638d29d --- /dev/null +++ b/DispenserCommon/Utils/RetryHelper.cs @@ -0,0 +1,38 @@ +namespace DispenserCommon.Utils; + +/// +/// 重试工具类 +/// +public class RetryHelper +{ + /// + /// 进行重试 + /// + /// + /// + /// + public static bool Retry(Action action, int maxRetries) + { + for (var attempt = 0; attempt < maxRetries; attempt++) + { + try + { + action(); + // 成功后返回 true + return true; + } + catch + { + if (attempt == maxRetries - 1) + { + // 超过重试次数返回 false + return false; + } + } + + Thread.Sleep(200); + } + + return false; + } +} \ No newline at end of file diff --git a/DispenserCommon/Utils/ServiceLocator.cs b/DispenserCommon/Utils/ServiceLocator.cs new file mode 100644 index 0000000..d941d48 --- /dev/null +++ b/DispenserCommon/Utils/ServiceLocator.cs @@ -0,0 +1,75 @@ +using DispenserCommon.Interface; +using Microsoft.Extensions.DependencyInjection; + +namespace DispenserCommon.Utils; + +/// +/// 获取服务实例工具类 +/// +public class ServiceLocator +{ + private static IServiceProvider? _serviceProvider; + + + /// + /// 注册 IServiceProvider + /// + /// + public static void Initialize(IServiceProvider? serviceProvider, IServiceCollection services) + { + _serviceProvider = serviceProvider; + + // 注册服务定位器 + foreach (var service in services) + { + var serviceType = service.ServiceType; + if (typeof(Instant).IsAssignableFrom(serviceType)) serviceProvider.GetService(service.ServiceType); + } + } + + /// + /// 获取服务实例 + /// + /// + /// + /// + public static T GetService() where T : class + { + return _serviceProvider.GetService(typeof(T)) is not T service + ? throw new ArgumentException( + $"{typeof(T)} needs to be registered in ConfigureServices within App.axaml.cs.") + : service; + } + + /// + /// 根据指定的实现类型获取服务实例 + /// + /// + /// + /// + /// + public static T GetService(Type impl) where T : class + { + var services = GetServices(); + + return services.FirstOrDefault(x => x.GetType() == impl) ?? throw new ArgumentException( + $"{typeof(T)} needs to be registered in ConfigureServices within App.axaml.cs."); + } + + + public static object? GetService(Type type) + { + return _serviceProvider.GetService(type); + } + + /// + /// 根据类型获取服务实例 + /// + /// + /// + public static IEnumerable GetServices() where T : class + { + var services = _serviceProvider.GetServices(); + return services; + } +} \ No newline at end of file diff --git a/DispenserCommon/Utils/SlidingWindow.cs b/DispenserCommon/Utils/SlidingWindow.cs new file mode 100644 index 0000000..1348403 --- /dev/null +++ b/DispenserCommon/Utils/SlidingWindow.cs @@ -0,0 +1,41 @@ +namespace DispenserCommon.Utils; + +/// +/// 基于滑动时间窗口算法来判断指定时间窗口内是否存在目标值 +/// +public class SlidingWindow(int milliseconds, int thresold = 1) +{ + private readonly Queue<(DateTime time, object value)> _window = new(); + private readonly TimeSpan _windowSize = TimeSpan.FromMilliseconds(milliseconds); + + + /// + /// 添加值 + /// + /// 待添加值 + public bool AllowValue(object value) + { + var currentTime = DateTime.Now; + + while (_window.Count > 0 && currentTime - _window.Peek().time > _windowSize) _window.Dequeue(); + + if (_window.Count < thresold) + { + _window.Enqueue((currentTime, value)); + return true; + } + + return false; + } + + + /// + /// 判断是否已经包含了该值 + /// + /// 待匹配值 + /// + public bool Contains(object targetValue) + { + return _window.Any(item => item.value.Equals(targetValue)); + } +} \ No newline at end of file diff --git a/DispenserCommon/Utils/TimeUtil.cs b/DispenserCommon/Utils/TimeUtil.cs new file mode 100644 index 0000000..4336b65 --- /dev/null +++ b/DispenserCommon/Utils/TimeUtil.cs @@ -0,0 +1,88 @@ +using System.Text; + +namespace DispenserCommon.Utils; + +public class TimeUtil +{ + public static void Sleep(int milliseconds) + { + Thread.Sleep(milliseconds); + } + + /// + /// 获取当前的时间戳 + /// + /// + public static long Now() + { + return new DateTimeOffset(DateTime.UtcNow).ToUnixTimeMilliseconds(); + } + + + /// + /// 获取当前的时间,格式化为目标格式 + /// + /// + /// + public static string GetNowTime(string format = "yyyy-MM-dd HH:mm:ss") + { + return DateTime.Now.ToString(format); + } + + + /// + /// 格式化时间 + /// + /// + /// + /// + public static string FormatTime(long time, string format = "yyyy-MM-dd HH:mm:ss") + { + var dateTime = DateTimeOffset.FromUnixTimeMilliseconds(time).DateTime; + return dateTime.ToString(format); + } + + public static string ToTimeSpan(long time) + { + // 使用TimeSpan.FromMilliseconds来创建TimeSpan对象 + var timeSpan = TimeSpan.FromMilliseconds(time); + + // 获取小时、分钟和秒 + var hours = timeSpan.Hours; + var minutes = timeSpan.Minutes; + var seconds = timeSpan.Seconds; + + var sb = new StringBuilder(); + if (hours > 0) + { + sb.Append(hours).Append("小时"); + } + + if (minutes > 0) + { + sb.Append(minutes).Append('分'); + } + + if (seconds > 0) + { + sb.Append(seconds).Append('秒'); + } + + return sb.ToString(); + } + + + /// + /// 将时间戳转为DateTime对象 + /// + /// + /// + public static DateTime TimeStampToDateTime(long timestamp) + { + var dateTimeOffset = (timestamp + "").Length == 10 + ? DateTimeOffset.FromUnixTimeSeconds(timestamp) + : DateTimeOffset.FromUnixTimeMilliseconds(timestamp); + + return dateTimeOffset.DateTime; + } +} \ No newline at end of file diff --git a/DispenserCommon/Utils/ToastUtil.cs b/DispenserCommon/Utils/ToastUtil.cs new file mode 100644 index 0000000..6c51cda --- /dev/null +++ b/DispenserCommon/Utils/ToastUtil.cs @@ -0,0 +1,39 @@ +using Avalonia.Controls.Notifications; +using Avalonia.Threading; +using Serilog; + +namespace DispenserCommon.Utils; + +public class ToastUtil +{ + private static WindowNotificationManager? _manager; + + public static void SetManager(WindowNotificationManager manager) + { + _manager = manager; + } + + public static void Info(string msg) + { + Log.Information(msg); + Dispatcher.UIThread.Invoke(() => { _manager?.Show(new Notification("", msg)); }); + } + + public static void Error(string msg) + { + Log.Error(msg); + Dispatcher.UIThread.Invoke(() => { _manager?.Show(new Notification("", msg, NotificationType.Error)); }); + } + + public static void Warn(string msg) + { + Log.Warning(msg); + Dispatcher.UIThread.Invoke(() => { _manager?.Show(new Notification("", msg, NotificationType.Warning)); }); + } + + public static void Success(string msg) + { + Log.Information(msg); + Dispatcher.UIThread.Invoke(() => { _manager?.Show(new Notification("", msg, NotificationType.Success)); }); + } +} \ No newline at end of file diff --git a/DispenserCommon/Utils/ValueUtil.cs b/DispenserCommon/Utils/ValueUtil.cs new file mode 100644 index 0000000..489efbb --- /dev/null +++ b/DispenserCommon/Utils/ValueUtil.cs @@ -0,0 +1,42 @@ +namespace DispenserCommon.Utils; + +public static class ValueUtil +{ + /// + /// 将 int 转为 8 bit 的二进制字符串,前面不足的补 0 + /// + public static string Int2BitStr(int val, int bitLen) + { + return new string(Int2BitChars(val, bitLen)); + } + + /// + /// + /// + /// + /// + public static char[] Int2BitChars(int val, int bitLen) + { + // 直接将整数val转换为二进制字符串,不考虑十六进制。 + var binaryString = Convert.ToString(val, 2); + + // 如果val为负数,binaryString将包含二进制补码形式的字符串,其长度可能超过bitLen。 + // 根据需要裁剪或填充字符串以适应指定的位长度(bitLen)。 + if (binaryString.Length > bitLen) + // 对于负数,去除多余的前导'1'。 + binaryString = binaryString.Substring(binaryString.Length - bitLen); + else + // 填充以达到所需长度。 + binaryString = binaryString.PadLeft(bitLen, '0'); + + // 反转和转换为字符数组。 + var chars = binaryString.Reverse().ToArray(); + + return chars; + } + + public static int BitChars2Int(char[] chars) + { + return Convert.ToInt32(new string(chars), 2); + } +} \ No newline at end of file diff --git a/DispenserCommon/Utils/WindowUtil.cs b/DispenserCommon/Utils/WindowUtil.cs new file mode 100644 index 0000000..9be1ca4 --- /dev/null +++ b/DispenserCommon/Utils/WindowUtil.cs @@ -0,0 +1,62 @@ +using Avalonia.Controls; + +namespace DispenserCommon.Utils; + +public class WindowUtil +{ + private static Window? _mainWindow; + + public static void SetMainWindow(Window window) + { + _mainWindow = window; + } + + /// + /// 显示弹窗 + /// + /// 弹窗 + /// 弹窗类型 + public static void ShowDialog(TD dialog) where TD : Window + { + if (_mainWindow == null) return; + dialog.ShowDialog(_mainWindow); + } + + /// + /// 显示弹窗 + /// + /// 弹窗 + /// 弹窗回调 + /// 弹窗类型 + public static void ShowDialog(TD dialog, Action action) where TD : Window + { + if (_mainWindow == null) return; + dialog.ShowDialog(_mainWindow); + action(dialog); + } + + /// + /// 显示弹窗 + /// + /// 弹窗实体 + /// 弹窗类型 + /// 弹窗回调信息 + /// + public static async Task ShowDialog(TD dialog) where TD : Window + { + if (_mainWindow == null) return default; + return await dialog.ShowDialog(_mainWindow); + } + + + /// + /// 无返回值的弹窗 + /// + /// + public static void ShowDialog() where TD : Window + { + if (_mainWindow == null) return; + var dialog = Activator.CreateInstance(); + dialog.ShowDialog(_mainWindow); + } +} \ No newline at end of file diff --git a/DispenserCore/Configures/LogConfiguration.cs b/DispenserCore/Configures/LogConfiguration.cs new file mode 100644 index 0000000..5e50b86 --- /dev/null +++ b/DispenserCore/Configures/LogConfiguration.cs @@ -0,0 +1,80 @@ +using DispenserCommon.LogUtils; +using DispenserCore.Service; +using Serilog; +using Serilog.Core; +using Serilog.Events; +using Serilog.Exceptions; + +namespace DispenserCommon.LogConfig; + +/// +/// 日志配置类 +/// +public class LogConfiguration +{ + private static LogParamsService _logParamsService = new(); + + /// + /// 获取日志配置对象 + /// + /// + public static Logger GetLogger() + { + var logParams = _logParamsService.GetLogParams(); + + // 日志输出目录 + var basePath = logParams!.Path ?? AppDomain.CurrentDomain.BaseDirectory; + + // 适配最小日志级别 + Enum.TryParse(logParams.Level, true, out LogEventLevel miniLevel); + + return new LoggerConfiguration() +#if DEBUG + // 测试环境的话,输出debug级别 + .MinimumLevel.Debug() +#else + // 其他环境输出info 级别 + .MinimumLevel.Information() +#endif + .MinimumLevel.Override("Microsoft", miniLevel) + .Enrich.FromLogContext() + .Enrich.WithExceptionDetails() + .WriteTo.Logger( + l => + l.Filter.ByIncludingOnly(e => e.Level == LogEventLevel.Debug) + .WriteTo.File( + Path.Combine(basePath, "logs", "debug", "debug-.log"), + rollingInterval: RollingInterval.Hour, + retainedFileCountLimit: 24 + ) + ) + .WriteTo.Logger( + l => l.Filter.ByIncludingOnly(e => e.Level == LogEventLevel.Information) + .WriteTo.File( + Path.Combine(basePath, "logs", "info", "info-.log"), + rollingInterval: RollingInterval.Hour, + retainedFileCountLimit: 72 + ) + ) + .WriteTo.Logger( + l => l.Filter.ByIncludingOnly(e => e.Level == LogEventLevel.Warning) + .WriteTo.File( + Path.Combine(basePath, "logs", "warning", "warning-.log"), + rollingInterval: RollingInterval.Day, + retainedFileCountLimit: 30 + ) + ) + .WriteTo.Logger( + l => l.Filter.ByIncludingOnly(e => e.Level == LogEventLevel.Error) + .WriteTo.File( + Path.Combine(basePath, "logs", "error", "error-.log"), + rollingInterval: RollingInterval.Day, + retainedFileCountLimit: 90 + ) + ) + // 测试环境同步输出到控制台 + .WriteTo.Console() + .WriteTo.Sink(new DispenserLogSink()) + .CreateLogger(); + } +} \ No newline at end of file diff --git a/DispenserCore/Context/GlobalSessionHolder.cs b/DispenserCore/Context/GlobalSessionHolder.cs new file mode 100644 index 0000000..e1f3159 --- /dev/null +++ b/DispenserCore/Context/GlobalSessionHolder.cs @@ -0,0 +1,60 @@ +using System.ComponentModel; +using System.Runtime.CompilerServices; +using DispenserCore.Model.DTO; + +namespace DispenserCore.Context; + +[DispenserCommon.Ioc.Component] +public class GlobalSessionHolder : INotifyPropertyChanged +{ + private Session _session; + + private Session Session + { + get => _session; + set + { + _session = value; + OnPropertyChanged(); + } + } + + public event PropertyChangedEventHandler? PropertyChanged; + + /// + /// 设置用户会话 + /// + /// + public void SetSession(Session session) + { + Session = session; + } + + + /// + /// 获取用户会话信息 + /// + /// + public Session GetSession() + { + return Session; + } + + public bool Logged() + { + return Session != null; + } + + /// + /// 清理会话会话 + /// + public void ClearSession() + { + Session = null; + } + + protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } +} \ No newline at end of file diff --git a/DispenserCore/DispenserCore.csproj b/DispenserCore/DispenserCore.csproj new file mode 100644 index 0000000..91a0034 --- /dev/null +++ b/DispenserCore/DispenserCore.csproj @@ -0,0 +1,32 @@ + + + + net7.0 + enable + enable + preview + + + + + + + + + + + + + + + + + + + + + ..\DispenserDesktop\Libs\DispenserVision.Halcon.dll + + + + diff --git a/DispenserCore/IOC/IocScanner.cs b/DispenserCore/IOC/IocScanner.cs new file mode 100644 index 0000000..7c31ad4 --- /dev/null +++ b/DispenserCore/IOC/IocScanner.cs @@ -0,0 +1,52 @@ +using System.Reflection; +using DispenserCommon.Ioc; + +namespace DispenserCore.IOC; + +public class IocScanner +{ + /// + /// 扫描所有的类,将所有带有Component的类注册到IOC容器中 + /// + public static List Scan() + { + var components = GetAssemblies() + .SelectMany(a => a.GetTypes() + .Where(t => t.GetCustomAttributes(typeof(Component), true).Length > 0)) + .ToList(); + List services = []; + components.ForEach(component => + { + var attribute = component.GetCustomAttribute(); + services.Add(new IocService(attribute?.Type ?? component, component)); + }); + return services; + } + + private static IEnumerable GetAssemblies() + { + var assemblies = new List(); + foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) + { + var name = assembly.GetName().Name; + if (name != null && name.ToLower().Contains("dispenser")) GetReferenceAssemblies(assembly, assemblies); + } + + return assemblies; + } + + private static void GetReferenceAssemblies(Assembly assembly, ICollection assemblies) + { + foreach (var assemblyName in assembly.GetReferencedAssemblies()) + { + var name = assemblyName.Name; + if (name != null && name.ToLower().Contains("dispenser")) + { + var ass = Assembly.Load(assemblyName); + if (assemblies.Contains(ass)) continue; + assemblies.Add(ass); + GetReferenceAssemblies(ass, assemblies); + } + } + } +} \ No newline at end of file diff --git a/DispenserCore/IOC/IocService.cs b/DispenserCore/IOC/IocService.cs new file mode 100644 index 0000000..0872582 --- /dev/null +++ b/DispenserCore/IOC/IocService.cs @@ -0,0 +1,8 @@ +namespace DispenserCore.IOC; + +public class IocService(Type type, Type implement) +{ + public Type Type => type; + + public Type Implement => implement; +} \ No newline at end of file diff --git a/DispenserCore/Job/LogUploadJob.cs b/DispenserCore/Job/LogUploadJob.cs new file mode 100644 index 0000000..e69f014 --- /dev/null +++ b/DispenserCore/Job/LogUploadJob.cs @@ -0,0 +1,57 @@ +using System.IO.Compression; +using DispenserCore.Service; +using Quartz; +using Serilog; + +namespace DispenserCore.Job; + +public class LogUploadJob : IJob +{ + private static LogParamsService _logParamsService = new(); + + public Task Execute(IJobExecutionContext context) + { + var logParams = _logParamsService.GetLogParams(); + + var basePath = logParams!.Path ?? AppDomain.CurrentDomain.BaseDirectory; + //前一天的日期 + var date = DateTime.Now.AddDays(-1).ToString("yyyyMMdd"); + var levels = logParams.UploadLevels!.Split(",").ToList(); + string? sourceFolder = null; + string? zipFilePath = null; + foreach (var level in levels) + try + { + sourceFolder = Path.Combine(basePath, "logs", level); + zipFilePath = Path.Combine(sourceFolder, $"{level}-{date}.log"); + var searchPattern = $"*{level}-{date}*.log"; + using (var zipFile = new FileStream(zipFilePath, FileMode.Create)) + { + using (var archive = new ZipArchive(zipFile, ZipArchiveMode.Create)) + { + foreach (var file in Directory.GetFiles(sourceFolder, searchPattern)) + { + var fileName = Path.GetFileName(file); + var entry = archive.CreateEntry(fileName); + using (var entryStream = entry.Open()) + using (var fileStream = File.OpenRead(file)) + { + fileStream.CopyTo(entryStream); + } + } + } + } + //TODO 上传到云端 + } + catch (Exception e) + { + Log.Error(e, "日志{0}上传失败", sourceFolder); + } + finally + { + if (zipFilePath != null && File.Exists(zipFilePath)) File.Delete(zipFilePath); + } + + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/DispenserCore/Job/SchedulerManager.cs b/DispenserCore/Job/SchedulerManager.cs new file mode 100644 index 0000000..5e349ad --- /dev/null +++ b/DispenserCore/Job/SchedulerManager.cs @@ -0,0 +1,37 @@ +using DispenserCommon.Events; +using DispenserCommon.scheduler; +using DispenserCore.Service; +using Serilog; + +namespace DispenserCore.Job; + +public class SchedulerManager +{ + private static readonly LogParamsService LogParamsService = new(); + + public static async Task StartAll() + { + Log.Information("启动所有定时任务"); + EventBus.Publish(EventType.SetupNotify, "正在启动定时任务"); + + var logParams = LogParamsService.GetLogParams(); + await SchedulerHelper.Start(); + + await SchedulerHelper.SchedulerCorn(null, logParams?.UploadCorn); + + return true; + } + + public static async void Shutdown() + { + try + { + Log.Information("关闭任务调度器"); + await SchedulerHelper.Shutdown(); + } + catch (Exception e) + { + Log.Error(e, "关闭任务调度器"); + } + } +} \ No newline at end of file diff --git a/DispenserCore/Model/DTO/QueryOperationLog.cs b/DispenserCore/Model/DTO/QueryOperationLog.cs new file mode 100644 index 0000000..98d2645 --- /dev/null +++ b/DispenserCore/Model/DTO/QueryOperationLog.cs @@ -0,0 +1,6 @@ +namespace DispenserCore.Model.DTO; + +public class QueryOperationLog : QueryPage +{ + public string? UserName { get; set; } +} \ No newline at end of file diff --git a/DispenserCore/Model/DTO/QueryPage.cs b/DispenserCore/Model/DTO/QueryPage.cs new file mode 100644 index 0000000..a900540 --- /dev/null +++ b/DispenserCore/Model/DTO/QueryPage.cs @@ -0,0 +1,62 @@ +using System.ComponentModel; +using System.Runtime.CompilerServices; + +namespace DispenserCore.Model.DTO; + +public class QueryPage : INotifyPropertyChanged +{ + private int _currentPage = 1; + + private int _pageSize = 10; + + private DateTime? _startTime; + + private DateTime? _endTime; + + public int CurrentPage + { + get => _currentPage; + set + { + _currentPage = value; + OnPropertyChanged(); + } + } + + public int PageSize + { + get => _pageSize; + set + { + _pageSize = value; + OnPropertyChanged(); + } + } + + public DateTime? StartTime + { + get => _startTime; + set + { + _startTime = value; + OnPropertyChanged(); + } + } + + public DateTime? EndTime + { + get => _endTime; + set + { + _endTime = value; + OnPropertyChanged(); + } + } + + public event PropertyChangedEventHandler? PropertyChanged; + + protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } +} \ No newline at end of file diff --git a/DispenserCore/Model/DTO/Session.cs b/DispenserCore/Model/DTO/Session.cs new file mode 100644 index 0000000..51cd993 --- /dev/null +++ b/DispenserCore/Model/DTO/Session.cs @@ -0,0 +1,15 @@ +using DispenserCore.Model.Entity; + +namespace DispenserCore.Model.DTO; + +/// +/// 用户登录会话信息 +/// +public class Session(User user) +{ + // 用户信息 + public User User { get; set; } = user; + + // 登录时间 + public DateTime LoginTime { get; set; } = DateTime.Now; +} \ No newline at end of file diff --git a/DispenserCore/Model/Entity/CameraInternalParams.cs b/DispenserCore/Model/Entity/CameraInternalParams.cs new file mode 100644 index 0000000..994b193 --- /dev/null +++ b/DispenserCore/Model/Entity/CameraInternalParams.cs @@ -0,0 +1,60 @@ +using System.ComponentModel; +using DispenserCommon.Atrributes; +using DispenserCore.Model.Enum; +using SQLite; + +namespace DispenserCore.Model.Entity; + +[Table("camera_internal_params"), Description("相机内部参数")] +public class CameraInternalParams : Entity +{ + [Column("type"), Description("参数类型"), Hide] + public string Type { get; set; } + + [Column("balance_ratio"), Description("白平衡值")] + public int BalanceRatio { get; set; } + + [Column("exposure_time"), Description("曝光时间"), Property(Min = 15, Max = 2000)] + public int ExposureTime { get; set; } + + [Column("exposure_auto"), Description("自动曝光")] + public bool ExposureAuto { get; set; } + + [Column("gain"), Description("增益")] public float Gain { get; set; } + + [Column("gain_auto"), Description("自动增益")] + public GainAutoEnum GainAuto { get; set; } + + [Column("black_level"), Description("灰度值")] + public float BlackLevel { get; set; } + + [Column("black_level_enable"), Description("黑电平调节使能")] + public bool BlackLevelEnable { get; set; } + + [Column("balance_white_auto"), Description("自动白平衡")] + public bool BalanceWhiteAuto { get; set; } + + [Column("resulting_frame_rate"), Description("实际采集帧率fps")] + public float ResultingFrameRate { get; set; } + + [Column("gamma"), Description("gamma值"), Property(Min = 0, Max = 4)] + public float Gamma { get; set; } + + [Column("gamma_enable"), Description("是否gamma使能")] + public bool GammaEnable { get; set; } + + [Column("line_mode"), Description("IO 模式")] + public LineModeEnum LineMode { get; set; } + + [Column("line_selector"), Description("IO 选择")] + public LineSelectorEnum LineSelector { get; set; } + + [Column("trigger_activation"), Description("触发激活")] + public TriggerActivationEnum TriggerActivation { get; set; } + + [Column("trigger_mode"), Description("触发模式")] + public TriggerModeEnum TriggerMode { get; set; } + + [Column("trigger_source"), Description("触发源")] + public TriggerSourceEnum TriggerSource { get; set; } +} \ No newline at end of file diff --git a/DispenserCore/Model/Entity/CameraParams.cs b/DispenserCore/Model/Entity/CameraParams.cs new file mode 100644 index 0000000..721d21a --- /dev/null +++ b/DispenserCore/Model/Entity/CameraParams.cs @@ -0,0 +1,32 @@ +using System.ComponentModel; +using DispenserCommon.Atrributes; +using SQLite; + +namespace DispenserCore.Model.Entity; + +/// +/// 相机参数 +/// +[Table("camera_params")] +[Description("相机参数")] +public class CameraParams : Entity +{ + [Column("camera_sn"), Description("相机序列号")] + public string CameraSn { get; set; } + + [Column("sdk"), Description("SDK")] public string Sdk { get; set; } + + [Column("dll"), Description("DLL")] public string Dll { get; set; } + + [Column("scale_ratio"), Description("视频缩放比例")] + public double ScaleRatio { get; set; } + + [Column("pixel_length"), Description("像素长度"), Property(Format = "0.########")] + public double PixelLength { get; set; } + + [Column("deflect_angle"), Description("相机偏转角度"), Property(Format = "0.#####")] + public double DeflectAngle { get; set; } + + [Column("camera_inner_param_template"), Description("相机内参模板")] + public string CameraInnerParamTemplate { get; set; } +} \ No newline at end of file diff --git a/DispenserCore/Model/Entity/Entity.cs b/DispenserCore/Model/Entity/Entity.cs new file mode 100644 index 0000000..2b46a0b --- /dev/null +++ b/DispenserCore/Model/Entity/Entity.cs @@ -0,0 +1,15 @@ +using SQLite; + +namespace DispenserCore.Model.Entity; + +/// +/// 数据库实体的父类 +/// +public class Entity +{ + [PrimaryKey] public string? Id { get; set; } + + [Column("create_time")] public DateTime CreateTime { get; set; } = DateTime.Now; + + [Column("update_time")] public DateTime UpdateTime { get; set; } = DateTime.Now; +} \ No newline at end of file diff --git a/DispenserCore/Model/Entity/LogParams.cs b/DispenserCore/Model/Entity/LogParams.cs new file mode 100644 index 0000000..614863b --- /dev/null +++ b/DispenserCore/Model/Entity/LogParams.cs @@ -0,0 +1,23 @@ +using System.ComponentModel; +using SQLite; + +namespace DispenserCore.Model.Entity; + +/// +/// 日志参数 +/// +[Table("log_params")] +[Description("日志参数")] +public class LogParams : Entity +{ + [Column("level"), Description("日志级别")] public string? Level { get; set; } + + [Column("path"), Description("日志存放路径")] + public string? Path { get; set; } + + [Column("upload_corn"), Description("日志上传时间")] + public string? UploadCorn { get; set; } + + [Column("upload_levels"), Description("日志上传级别")] + public string? UploadLevels { get; set; } +} \ No newline at end of file diff --git a/DispenserCore/Model/Entity/MinioParams.cs b/DispenserCore/Model/Entity/MinioParams.cs new file mode 100644 index 0000000..85b1776 --- /dev/null +++ b/DispenserCore/Model/Entity/MinioParams.cs @@ -0,0 +1,20 @@ +using System.ComponentModel; +using SQLite; + +namespace DispenserCore.Model.Entity; + +[Table("minio_params"), Description("MinIO参数")] +public class MinioParams : Entity +{ + [Column("minio_access_key"), Description("Minio AccessKey")] + public string MinioAccessKey { get; set; } + + [Column("minio_secret_key"), Description("Minio SecretKey")] + public string MinioSecretKey { get; set; } + + [Column("minio_bucket"), Description("Minio Bucket")] + public string MinioBucket { get; set; } + + [Column("minio_endpoint"), Description("Minio Endpoint")] + public string MinioEndpoint { get; set; } +} \ No newline at end of file diff --git a/DispenserCore/Model/Entity/MqttParams.cs b/DispenserCore/Model/Entity/MqttParams.cs new file mode 100644 index 0000000..37d0f07 --- /dev/null +++ b/DispenserCore/Model/Entity/MqttParams.cs @@ -0,0 +1,20 @@ +using System.ComponentModel; +using DispenserCommon.Atrributes; +using SQLite; + +namespace DispenserCore.Model.Entity; + +[Table("mqtt_params"), Description("Mqtt连接参数")] +public class MqttParams : Entity +{ + [Column("server_address"), Description("服务器地址")] + public string ServerAddress { get; set; } + + [Column("port"), Description("端口")] public int Port { get; set; } + + [Column("user_name"), Description("用户名")] + public string UserName { get; set; } + + [Column("password"), Description("密码"), Property(IsPassword = true)] + public string Password { get; set; } +} \ No newline at end of file diff --git a/DispenserCore/Model/Entity/OperationLog.cs b/DispenserCore/Model/Entity/OperationLog.cs new file mode 100644 index 0000000..b011a31 --- /dev/null +++ b/DispenserCore/Model/Entity/OperationLog.cs @@ -0,0 +1,29 @@ +using System.ComponentModel; +using DispenserCommon.Atrributes; +using SQLite; + +namespace DispenserCore.Model.Entity; + +/// +/// 用户操作日志 +/// +[Table("operation_logs")] +public class OperationLog : Entity +{ + [Column("user_id"), Description("用户ID"), Hide] + public string UserId { get; set; } + + [Column("user_name"), Description("用户名")] + public string UserName { get; set; } + + [Column("action"), Description("操作")] public string Action { get; set; } + + [Column("params"), Description("参数"), Hide] + public string? Params { get; set; } + + [Column("exception"), Description("异常信息"), Hide] + public string? Exception { get; set; } + + [Column("operate_time"), Description("操作时间")] + public DateTime OperateTime { get; set; } +} \ No newline at end of file diff --git a/DispenserCore/Model/Entity/SystemParams.cs b/DispenserCore/Model/Entity/SystemParams.cs new file mode 100644 index 0000000..278dd50 --- /dev/null +++ b/DispenserCore/Model/Entity/SystemParams.cs @@ -0,0 +1,36 @@ +using System.ComponentModel; +using DispenserCommon.Atrributes; +using SQLite; + +namespace DispenserCore.Model.Entity; + +/// +/// 系统参数 +/// +[Table("system_params")] +[Description("系统参数")] +public class SystemParams : Entity +{ + [Column("device_type"), Description("设备类型")] + public string? DeviceType { get; set; } + + [Column("version"), Description("版本号"), Property(IsReadOnly = true)] + public string? Version { get; set; } + + [Column("name"), Description("名称")] public string? Name { get; set; } + + [Column("acs_ip"), Description("ACS 控制器IP")] + public string AcsIp { get; set; } + + [Column("image_storage_path"), Description("照片存储路径")] + public string? ImageStoragePath { get; set; } + + [Column("camera_viewer_storage_path"), Description("相机预览控件图片存储路径")] + public string? CameraViewerStoragePath { get; set; } + + [Column("enable_auto_clear_image"), Description("是否自动清除历史照片")] + public bool EnableAutoClearImage { get; set; } + + [Column("retained_day"), Description("照片保留天数")] + public int RetainedDay { get; set; } +} \ No newline at end of file diff --git a/DispenserCore/Model/Entity/User.cs b/DispenserCore/Model/Entity/User.cs new file mode 100644 index 0000000..759e8d1 --- /dev/null +++ b/DispenserCore/Model/Entity/User.cs @@ -0,0 +1,18 @@ +using SQLite; + +namespace DispenserCore.Model.Entity; + +/// +/// 用户信息 +/// +[Table("users")] +public class User : Entity +{ + [Column("user_name")] public string UserName { get; set; } + + [Column("nick_name")] public string NickName { get; set; } + + [Column("password")] public string Password { get; set; } + + [Column("role")] public int Role { get; set; } +} \ No newline at end of file diff --git a/DispenserCore/Model/Enum/ChipColorEnum.cs b/DispenserCore/Model/Enum/ChipColorEnum.cs new file mode 100644 index 0000000..2137b6c --- /dev/null +++ b/DispenserCore/Model/Enum/ChipColorEnum.cs @@ -0,0 +1,8 @@ +namespace DispenserCore.Model.Enum; + +public enum ChipColorEnum +{ + R = 1, + G = 2, + B = 3 +} \ No newline at end of file diff --git a/DispenserCore/Model/Enum/DirectionEnum.cs b/DispenserCore/Model/Enum/DirectionEnum.cs new file mode 100644 index 0000000..405153f --- /dev/null +++ b/DispenserCore/Model/Enum/DirectionEnum.cs @@ -0,0 +1,9 @@ +using System.ComponentModel; + +namespace DispenserCore.Model.Enum; + +public enum DirectionEnum +{ + [Description("行方向")] ROW = 1, + [Description("列方向")] COLUMN = 2, +} \ No newline at end of file diff --git a/DispenserCore/Model/Enum/GainAutoEnum.cs b/DispenserCore/Model/Enum/GainAutoEnum.cs new file mode 100644 index 0000000..d4bda0a --- /dev/null +++ b/DispenserCore/Model/Enum/GainAutoEnum.cs @@ -0,0 +1,8 @@ +namespace DispenserCore.Model.Enum; + +public enum GainAutoEnum +{ + Off, + Once, + Continuous +} \ No newline at end of file diff --git a/DispenserCore/Model/Enum/JobStateEnum.cs b/DispenserCore/Model/Enum/JobStateEnum.cs new file mode 100644 index 0000000..8f3e494 --- /dev/null +++ b/DispenserCore/Model/Enum/JobStateEnum.cs @@ -0,0 +1,19 @@ +using System.ComponentModel; + +namespace DispenserCore.Model.Enum; + +/// +/// 生产作业状态 +/// +public enum JobStateEnum +{ + [Description("待生产")] Waiting = 0, + + [Description("生产中")] Producing = 1, + + [Description("已完成")] Completed = 2, + + [Description("已取消")] Canceled = 3, + + [Description("生产异常")] Abnormal = 4, +} \ No newline at end of file diff --git a/DispenserCore/Model/Enum/LineModeEnum.cs b/DispenserCore/Model/Enum/LineModeEnum.cs new file mode 100644 index 0000000..8fd9415 --- /dev/null +++ b/DispenserCore/Model/Enum/LineModeEnum.cs @@ -0,0 +1,6 @@ +namespace DispenserCore.Model.Enum; + +public enum LineModeEnum +{ + Strobe +} \ No newline at end of file diff --git a/DispenserCore/Model/Enum/LineSelectorEnum.cs b/DispenserCore/Model/Enum/LineSelectorEnum.cs new file mode 100644 index 0000000..a54c15c --- /dev/null +++ b/DispenserCore/Model/Enum/LineSelectorEnum.cs @@ -0,0 +1,8 @@ +namespace DispenserCore.Model.Enum; + +public enum LineSelectorEnum +{ + Line0, + Line1, + Line2, +} \ No newline at end of file diff --git a/DispenserCore/Model/Enum/MixBinStrategyEnum.cs b/DispenserCore/Model/Enum/MixBinStrategyEnum.cs new file mode 100644 index 0000000..54aa7a4 --- /dev/null +++ b/DispenserCore/Model/Enum/MixBinStrategyEnum.cs @@ -0,0 +1,12 @@ +using System.ComponentModel; + +namespace DispenserCore.Model.Enum; + +/// +/// 动打策略 +/// +public enum MixBinStrategyEnum +{ + [Description("混Bin")] Mix = 1, + [Description("不混")] NotMix = 2 +} \ No newline at end of file diff --git a/DispenserCore/Model/Enum/PcbDetectStrategyEnum.cs b/DispenserCore/Model/Enum/PcbDetectStrategyEnum.cs new file mode 100644 index 0000000..68fce75 --- /dev/null +++ b/DispenserCore/Model/Enum/PcbDetectStrategyEnum.cs @@ -0,0 +1,9 @@ +using System.ComponentModel; + +namespace DispenserCore.Model.Enum; + +public enum PcbDetectStrategyEnum +{ + [Description("Mark点识别")]ByMark = 1, + [Description("焊点模板匹配")]TemplateMatch = 2, +} \ No newline at end of file diff --git a/DispenserCore/Model/Enum/ScannerInterfaceEnum.cs b/DispenserCore/Model/Enum/ScannerInterfaceEnum.cs new file mode 100644 index 0000000..b3419b4 --- /dev/null +++ b/DispenserCore/Model/Enum/ScannerInterfaceEnum.cs @@ -0,0 +1,34 @@ +using System.ComponentModel; + +namespace DispenserCore.Model.Enum; + +/// +/// 扫码枪接口类型 +/// +public enum ScannerInterfaceEnum +{ + /// + /// 串口 + /// + [Description("串口")] Serial, + + /// + /// TCP + /// + [Description("网口")] Tcp, + + /// + /// USB + /// + [Description("USB")] Usb, + + /// + /// 蓝牙 + /// + [Description("蓝牙")] Bluetooth, + + /// + /// 无线 + /// + [Description("无线")] Wireless +} \ No newline at end of file diff --git a/DispenserCore/Model/Enum/SubstrateTypeEnum.cs b/DispenserCore/Model/Enum/SubstrateTypeEnum.cs new file mode 100644 index 0000000..86521c0 --- /dev/null +++ b/DispenserCore/Model/Enum/SubstrateTypeEnum.cs @@ -0,0 +1,12 @@ +using System.ComponentModel; + +namespace DispenserCore.Model.Enum; + +/// +/// 基材类型 +/// +public enum SubstrateTypeEnum +{ + [Description("PCB")] PCB = 1, + [Description("玻璃")] Glass = 2 +} \ No newline at end of file diff --git a/DispenserCore/Model/Enum/TlsProtocolsEnum.cs b/DispenserCore/Model/Enum/TlsProtocolsEnum.cs new file mode 100644 index 0000000..f899331 --- /dev/null +++ b/DispenserCore/Model/Enum/TlsProtocolsEnum.cs @@ -0,0 +1,7 @@ +namespace DispenserCore.Model.Enum; + +public enum TlsProtocolsEnum +{ + TLS_1_2, + TLS_1_3 +} \ No newline at end of file diff --git a/DispenserCore/Model/Enum/TriggerActivationEnum.cs b/DispenserCore/Model/Enum/TriggerActivationEnum.cs new file mode 100644 index 0000000..4195e74 --- /dev/null +++ b/DispenserCore/Model/Enum/TriggerActivationEnum.cs @@ -0,0 +1,10 @@ +namespace DispenserCore.Model.Enum; + +public enum TriggerActivationEnum +{ + RisingEdge, + FallingEdge, + LevelHigh, + LevelLow, + AnyEdge +} \ No newline at end of file diff --git a/DispenserCore/Model/Enum/TriggerModeEnum.cs b/DispenserCore/Model/Enum/TriggerModeEnum.cs new file mode 100644 index 0000000..571b15d --- /dev/null +++ b/DispenserCore/Model/Enum/TriggerModeEnum.cs @@ -0,0 +1,7 @@ +namespace DispenserCore.Model.Enum; + +public enum TriggerModeEnum +{ + Off, + On +} \ No newline at end of file diff --git a/DispenserCore/Model/Enum/TriggerSourceEnum.cs b/DispenserCore/Model/Enum/TriggerSourceEnum.cs new file mode 100644 index 0000000..a447fb4 --- /dev/null +++ b/DispenserCore/Model/Enum/TriggerSourceEnum.cs @@ -0,0 +1,10 @@ +namespace DispenserCore.Model.Enum; + +public enum TriggerSourceEnum +{ + Software, + Line0, + Line2, + Counter0, + Anyway +} \ No newline at end of file diff --git a/DispenserCore/Model/Enum/WaferScanStrategyEnum.cs b/DispenserCore/Model/Enum/WaferScanStrategyEnum.cs new file mode 100644 index 0000000..2a56345 --- /dev/null +++ b/DispenserCore/Model/Enum/WaferScanStrategyEnum.cs @@ -0,0 +1,9 @@ +using System.ComponentModel; + +namespace DispenserCore.Model.Enum; + +public enum WaferScanStrategyEnum +{ + [Description("快速二值化")]FastThreshold = 1, + [Description("模板匹配")]TemplateMatch = 2, +} \ No newline at end of file diff --git a/DispenserCore/Service/CameraInternalParamsService.cs b/DispenserCore/Service/CameraInternalParamsService.cs new file mode 100644 index 0000000..9a740e2 --- /dev/null +++ b/DispenserCore/Service/CameraInternalParamsService.cs @@ -0,0 +1,112 @@ +using DispenserCommon.DB; +using DispenserCommon.Ioc; +using DispenserCommon.Utils; +using DispenserCore.Model.Entity; +using DispenserHal.Camera.Factory; + +namespace DispenserCore.Service; + +[Component] +public class CameraInternalParamsService +{ + private static readonly SqliteHelper Db = ServiceLocator.GetService(); + + + /// + /// 获取wafer相机参数 + /// + /// + public CameraInternalParams? GetWaferParams() + { + return Db.Query("select * from camera_internal_params where type = 'wafer'") + .FirstOrDefault(); + } + + /// + /// 获取基材相机参数 + /// + /// + public CameraInternalParams? GetSubstrateParams() + { + return Db.Query("select * from camera_internal_params where type = 'substrate'") + .FirstOrDefault(); + } + + /// + /// 保存或者更新参数 + /// + /// + /// + public CameraInternalParams SaveOrUpdate(CameraInternalParams cameraInternalParams) + { + return Db.SaveOrUpdate(cameraInternalParams); + } + + /// + /// 将晶圆拍照参数写入相机 + /// + public void FlushWaferParamsToCamera() + { + var waferParams = GetWaferParams(); + if (waferParams != null) + { + FlushParamsToCamera(waferParams); + } + } + + /// + /// 将基材拍照参数写入相机 + /// + public void FlushSubstrateParamsToCamera() + { + var substrateParams = GetSubstrateParams(); + if (substrateParams != null) + { + FlushParamsToCamera(substrateParams); + } + } + + /// + /// 将参数写入相机 + /// + /// + private void FlushParamsToCamera(CameraInternalParams cameraInternalParams) + { + var camera = CameraManager.GetCamera()!; + + var properties = cameraInternalParams.GetType().GetProperties(); + + foreach (var property in properties) + { + var propertyName = property.Name; + if (propertyName == nameof(CameraInternalParams.Type)) continue; + var type = property.PropertyType.Name; + if (type.ToLower().Contains("enum")) + { + type = "Enum"; + } + + var value = property.GetValue(cameraInternalParams); + + if (value == null) continue; + + switch (type) + { + case "Int32": + camera.SetIntValue(propertyName, (int)value); + break; + case "Boolean": + camera.SetBoolValue(propertyName, (bool)value); + break; + case "Single": + var @decimal = (float)Convert.ToDecimal(value); + camera.SetFloatValue(propertyName, @decimal); + break; + case "Enum": + var val = Convert.ToUInt32(value); + camera.SetEnumValue(propertyName, val); + break; + } + } + } +} \ No newline at end of file diff --git a/DispenserCore/Service/CameraParamsService.cs b/DispenserCore/Service/CameraParamsService.cs new file mode 100644 index 0000000..877cfa4 --- /dev/null +++ b/DispenserCore/Service/CameraParamsService.cs @@ -0,0 +1,50 @@ +using DispenserCommon.DB; +using DispenserCommon.Ioc; +using DispenserCommon.Utils; +using DispenserCore.Model.Entity; +using DispenserHal.Camera.DTO; + +namespace DispenserCore.Service; + +/// +/// 相机参数业务接口 +/// +[Component] +public class CameraParamsService +{ + private readonly SqliteHelper _db = ServiceLocator.GetService(); + + /// + /// 获取算法参数 + /// + /// + public CameraParams? GetCameraParams() + { + return _db.Query("select * from camera_params limit 1") + .FirstOrDefault(); + } + + /// + /// 保存或者更新系统参数 + /// + /// + /// + public CameraParams SaveOrUpdate(CameraParams @params) + { + // 每次更新配置都要刷新一下配置 + CameraConfig.Dll = @params.Dll; + CameraConfig.Sdk = @params.Sdk; + CameraConfig.CameraSn = @params.CameraSn; + CameraConfig.ScaleRatio = @params.ScaleRatio; + return _db.SaveOrUpdate(@params); + } + + /// + /// 清除历史纪录 + /// + /// + public void Delete(string id) + { + _db.DeleteById(id); + } +} \ No newline at end of file diff --git a/DispenserCore/Service/ImageService.cs b/DispenserCore/Service/ImageService.cs new file mode 100644 index 0000000..56c1dae --- /dev/null +++ b/DispenserCore/Service/ImageService.cs @@ -0,0 +1,188 @@ +using System.Diagnostics.CodeAnalysis; +using System.Drawing.Imaging; +using System.Runtime.InteropServices; +using Masuit.Tools; +using Serilog; +using Bitmap = System.Drawing.Bitmap; + +namespace DispenserCore.Service; + +[SuppressMessage("Interoperability", "CA1416:验证平台兼容性")] +public class ImageService +{ + private const string BasePath = "Dispenser"; + + private static readonly SystemParamsService SystemParamsService = new(); + + /// + /// 保存 芯片飞拍照片 + /// + public static void SaveWaferImage(string batchCode, string waferCode, Bitmap image) + { + Task.Run(() => + { + try + { + // 文件保存路径的规则是 {user BasePath}/Dispenser/images/{yyyyMMdd}/{batchCode}/wafer/{waferCode}/{timestamp}.bmp + + var today = DateTime.Now.ToString("yyyyMMdd"); + + var systemParams = SystemParamsService.GetSystemParams(); + var imageStoragePath = + systemParams!.ImageStoragePath ?? Environment.GetEnvironmentVariable("USERPROFILE"); + + var dir = Path.Combine(imageStoragePath, BasePath, today, batchCode, + "wafer", waferCode); + + SaveImageToFile(image, dir); + } + catch (Exception e) + { + Log.Error(e, "保存芯片飞拍照片失败"); + } + + return Task.CompletedTask; + }); + } + + /// + /// 保存PCB图片 + /// + /// + /// + /// + /// + public static void SavePcbImage(string batchCode, string pcbCode, Bitmap image) + { + Task.Run(() => + { + try + { + // 文件保存路径的规则是 {user BasePath}/{yyyyMMdd}/{batchCode}/pcb/{pcbCode}/{timestamp}.bmp + var today = DateTime.Now.ToString("yyyyMMdd"); + + var systemParams = SystemParamsService.GetSystemParams(); + var imageStoragePath = + systemParams!.ImageStoragePath ?? Environment.GetEnvironmentVariable("USERPROFILE"); + + var dir = Path.Combine(imageStoragePath, BasePath, today, batchCode, "pcb", + pcbCode); + + SaveImageToFile(image, dir); + } + catch (Exception e) + { + Log.Error(e, "保存基板图片失败"); + } + + return Task.CompletedTask; + }); + } + + /// + /// 保存动打检测图片 + /// + /// + /// + /// + /// + public static void SaveDetectImage(string batchCode, string pcbCode, Bitmap image) + { + Task.Run(() => + { + try + { + // 文件保存路径的规则是 {user BasePath}/{yyyyMMdd}/{batchCode}/pcb/{pcbCode}/{timestamp}.bmp + var today = DateTime.Now.ToString("yyyyMMdd"); + + var systemParams = SystemParamsService.GetSystemParams(); + var imageStoragePath = + systemParams!.ImageStoragePath ?? Environment.GetEnvironmentVariable("USERPROFILE"); + + var dir = Path.Combine(imageStoragePath, BasePath, today, batchCode, "detect", + pcbCode); + + SaveImageToFile(image, dir); + } + catch (Exception e) + { + Log.Error(e, "保存动打检测图片失败"); + } + + return Task.CompletedTask; + }); + } + + private static void SaveImageToFile(Bitmap bitmap, string dir) + { + dir = dir.Replace("\n","").TrimEnd(); + + if (!Directory.Exists(dir)) + { + Directory.CreateDirectory(dir); + } + + var timestamp = DateTime.Now.Ticks; + var fileName = $"{timestamp}.bmp"; + + // 保持到文件 + SaveToFile(bitmap, Path.Combine(dir, fileName), ImageFormat.Bmp); + } + + + [DllImport("kernel32.dll", EntryPoint = "RtlMoveMemory", SetLastError = false)] + private static extern void CopyMemory(IntPtr dest, IntPtr src, uint count); + + /// + /// 将照片保存到文件 + /// + /// + /// + /// + /// + /// + public static void SaveToFile(Bitmap bitmap, string filePath, ImageFormat format) + { + if (bitmap == null) throw new ArgumentNullException(nameof(bitmap)); + if (string.IsNullOrEmpty(filePath)) + throw new ArgumentException("File path cannot be null or empty", nameof(filePath)); + if (format == null) throw new ArgumentNullException(nameof(format)); + + // 将 Bitmap 转换为字节数组 + using (MemoryStream memoryStream = new MemoryStream()) + { + bitmap.Save(memoryStream, format); + var imageBytes = memoryStream.ToArray(); + + // 分配未管理的内存 + var unmanagedPointer = Marshal.AllocHGlobal(imageBytes.Length); + + try + { + // 将托管数组复制到未管理的内存中 + Marshal.Copy(imageBytes, 0, unmanagedPointer, imageBytes.Length); + + // 创建目标缓冲区并使用 RtlMoveMemory 进行内存复制 + var destinationArray = new byte[imageBytes.Length]; + var handle = GCHandle.Alloc(destinationArray, GCHandleType.Pinned); + try + { + var destPointer = handle.AddrOfPinnedObject(); + CopyMemory(destPointer, unmanagedPointer, (uint)imageBytes.Length); + } + finally + { + handle.Free(); + } + + // 将目标缓冲区写入文件 + File.WriteAllBytes(filePath, destinationArray); + } + finally + { + // 释放未管理的内存 + Marshal.FreeHGlobal(unmanagedPointer); + } + } + } +} \ No newline at end of file diff --git a/DispenserCore/Service/LogParamsService.cs b/DispenserCore/Service/LogParamsService.cs new file mode 100644 index 0000000..9d84cad --- /dev/null +++ b/DispenserCore/Service/LogParamsService.cs @@ -0,0 +1,44 @@ +using DispenserCommon.DB; +using DispenserCommon.Ioc; +using DispenserCommon.Utils; +using DispenserCore.Model.Entity; + +namespace DispenserCore.Service; + +/// +/// 日志参数业务接口 +/// +[Component] +public class LogParamsService +{ + private readonly SqliteHelper _db = ServiceLocator.GetService(); + + /// + /// 获取算法参数 + /// + /// + public LogParams? GetLogParams() + { + return _db.Query("select * from log_params limit 1") + .FirstOrDefault(); + } + + /// + /// 保存或者更新系统参数 + /// + /// + /// + public LogParams SaveOrUpdate(LogParams @params) + { + return _db.SaveOrUpdate(@params); + } + + /// + /// 清除历史纪录 + /// + /// + public void Delete(string id) + { + _db.DeleteById(id); + } +} \ No newline at end of file diff --git a/DispenserCore/Service/MinioParamsService.cs b/DispenserCore/Service/MinioParamsService.cs new file mode 100644 index 0000000..6c27560 --- /dev/null +++ b/DispenserCore/Service/MinioParamsService.cs @@ -0,0 +1,44 @@ +using DispenserCommon.DB; +using DispenserCommon.Ioc; +using DispenserCommon.Utils; +using DispenserCore.Model.Entity; + +namespace DispenserCore.Service; + +/// +/// 边端连接参数参数业务接口 +/// +[Component] +public class MinioParamsService +{ + private readonly SqliteHelper _db = ServiceLocator.GetService(); + + /// + /// 获取算法参数 + /// + /// + public MinioParams? GetEdgeParams() + { + return _db.Query("select * from minio_params limit 1") + .FirstOrDefault(); + } + + /// + /// 保存或者更新算法参数 + /// + /// + /// + public MinioParams SaveOrUpdate(MinioParams @params) + { + return _db.SaveOrUpdate(@params); + } + + /// + /// 清除历史纪录 + /// + /// + public void Delete(string id) + { + _db.DeleteById(id); + } +} \ No newline at end of file diff --git a/DispenserCore/Service/MqttParamsService.cs b/DispenserCore/Service/MqttParamsService.cs new file mode 100644 index 0000000..5a6e350 --- /dev/null +++ b/DispenserCore/Service/MqttParamsService.cs @@ -0,0 +1,44 @@ +using DispenserCommon.DB; +using DispenserCommon.Ioc; +using DispenserCommon.Utils; +using DispenserCore.Model.Entity; + +namespace DispenserCore.Service; + +/// +/// Mqtt连接参数参数业务接口 +/// +[Component] +public class MqttParamsService +{ + private readonly SqliteHelper _db = ServiceLocator.GetService(); + + /// + /// 获取算法参数 + /// + /// + public MqttParams? GetMqttConnectParams() + { + return _db.Query("select * from mqtt_params limit 1") + .FirstOrDefault(); + } + + /// + /// 保存或者更新算法参数 + /// + /// + /// + public MqttParams SaveOrUpdate(MqttParams @params) + { + return _db.SaveOrUpdate(@params); + } + + /// + /// 清除历史纪录 + /// + /// + public void Delete(string id) + { + _db.DeleteById(id); + } +} \ No newline at end of file diff --git a/DispenserCore/Service/OperationLogHandler.cs b/DispenserCore/Service/OperationLogHandler.cs new file mode 100644 index 0000000..e46492b --- /dev/null +++ b/DispenserCore/Service/OperationLogHandler.cs @@ -0,0 +1,110 @@ +using System.Collections.Concurrent; +using DispenserCommon.DTO; +using DispenserCommon.Events; +using DispenserCommon.Ioc; +using DispenserCommon.Utils; +using DispenserCore.Context; +using DispenserCore.Model.Entity; +using Masuit.Tools; +using Serilog; + +namespace DispenserCore.Service; + +/// +/// 操作日志处理器 +/// +[Component] +public class OperationLogHandler +{ + private static readonly BlockingCollection Queue = new(); + + private readonly OperationLogService _service = ServiceLocator.GetService(); + + private readonly GlobalSessionHolder _session = ServiceLocator.GetService(); + + private bool _started; + + private readonly object _lock = new(); + + // 批量罗盘阈值 + private const int Batch = 1; + + /// + /// 往队列里面添加操作日志 + /// + /// + /// + [EventAction(EventType.OperationLog)] + public void Record(EventType _, ActionLog actionLog) + { + Queue.Add(actionLog); + if (!_started) + { + Start(); + } + } + + + /// + /// 启动日志记录处理器 + /// + private void Start() + { + lock (_lock) + { + Task.Run(() => + { + try + { + _started = true; + while (!Queue.IsCompleted) + { + List logs = []; + + var session = _session.GetSession(); + + while (Queue.TryTake(out var log)) + { + if (log.IsNullOrEmpty()) continue; + + var operationLog = new OperationLog + { + Action = log.Name, + Params = JsonUtil.ToJson(log.Params), + Exception = JsonUtil.ToJson(log.Exception), + OperateTime = log.OperateTime + }; + + if (session != null) + { + operationLog.UserId = session.User.Id; + operationLog.UserName = session.User.UserName; + } + + // 将log 写入日志 + logs.Add(operationLog); + + if (logs.Count < Batch) continue; + // 每10条一批刷新到数据库 + FlushToDb(logs); + logs = []; + } + } + } + catch (Exception e) + { + Log.Error(e,"操作日志保存异常: "); + } + + }).ConfigureAwait(false); + } + } + + /// + /// 保存到数据库 + /// + private void FlushToDb(IEnumerable logs) + { + _service.BatchInsert(logs); + } +} \ No newline at end of file diff --git a/DispenserCore/Service/OperationLogService.cs b/DispenserCore/Service/OperationLogService.cs new file mode 100644 index 0000000..1fbc50d --- /dev/null +++ b/DispenserCore/Service/OperationLogService.cs @@ -0,0 +1,62 @@ +using DispenserCommon.DB; +using DispenserCommon.DTO; +using DispenserCommon.Ioc; +using DispenserCommon.Utils; +using DispenserCore.Model.DTO; +using DispenserCore.Model.Entity; +using Masuit.Tools; + +namespace DispenserCore.Service; + +/// +/// 操作日志业务类 +/// +[Component] +public class OperationLogService +{ + private readonly SqliteHelper _db = ServiceLocator.GetService(); + + + /// + /// 批量插入操作日志 + /// + /// + public void BatchInsert(IEnumerable logs) + { + _db.BatchInsert(logs); + } + + /// + /// 分页查询 + /// + /// + /// + public Page QueryPage(QueryOperationLog query) + { + // 构建查询sql + var sql = "select * from operation_logs where 1 = 1 "; + if (!query.StartTime.IsDefaultValue() && !query.EndTime.IsDefaultValue()) + { + sql += "and operate_time between '" + query.StartTime + "' and '" + + query.EndTime + "' "; + } + else if (!query.StartTime.IsDefaultValue()) + { + sql += "and operate_time >= '" + query.StartTime + "'"; + } + else if (!query.EndTime.IsDefaultValue()) + { + sql += "and operate_time <= '" + query.EndTime + "'"; + } + + if (!query.UserName.IsNullOrEmpty()) + { + sql += "and user_name like '%" + query.UserName + "%' "; + } + + sql += " order by operate_time desc "; + + Console.WriteLine(sql); + return _db.Page(query.CurrentPage, query.PageSize, sql); + } +} \ No newline at end of file diff --git a/DispenserCore/Service/SystemParamsService.cs b/DispenserCore/Service/SystemParamsService.cs new file mode 100644 index 0000000..3473d94 --- /dev/null +++ b/DispenserCore/Service/SystemParamsService.cs @@ -0,0 +1,44 @@ +using DispenserCommon.DB; +using DispenserCommon.Ioc; +using DispenserCommon.Utils; +using DispenserCore.Model.Entity; + +namespace DispenserCore.Service; + +/// +/// 系统参数业务接口 +/// +[Component] +public class SystemParamsService +{ + private readonly SqliteHelper _db = ServiceLocator.GetService(); + + /// + /// 获取系统参数 + /// + /// + public SystemParams? GetSystemParams() + { + return _db.Query("select * from system_params limit 1") + .FirstOrDefault(); + } + + /// + /// 保存或者更新系统参数 + /// + /// + /// + public SystemParams SaveOrUpdate(SystemParams @params) + { + return _db.SaveOrUpdate(@params); + } + + /// + /// 清除历史纪录 + /// + /// + public void Delete(string id) + { + _db.DeleteById(id); + } +} \ No newline at end of file diff --git a/DispenserCore/Service/UserService.cs b/DispenserCore/Service/UserService.cs new file mode 100644 index 0000000..c9cdb70 --- /dev/null +++ b/DispenserCore/Service/UserService.cs @@ -0,0 +1,131 @@ +using DispenserCommon.DB; +using DispenserCommon.Interface; +using DispenserCommon.Ioc; +using DispenserCommon.Utils; +using DispenserCore.Model.Entity; +using DispenserUI.Exceptions; +using Masuit.Tools; + +namespace DispenserCore.Service; + +[Component] +public class UserService : Instant +{ + private const string DefaultPassword = "88888888"; + private readonly SqliteHelper _db = ServiceLocator.GetService(); + + /// + /// 添加用户 + /// + /// 待添加的用户 + public User AddUser(User user) + { + var userName = user.UserName; + // 校验用户名是否已经存在 + if (_db.Query("select * from users where user_name = ?", userName).Any()) + throw new BizException($"用户名{userName}已存在"); + + // 这里用到了一个很神奇的工具库 masuit.tools ,这个家伙有点像java 的 hutool + if (user.Password.IsNullOrEmpty()) user.Password = DefaultPassword; + + // 对密码进行加密 + user.Password = EncryptPassword(user.Password); + + // 进行插入,返回插入后的用户对象 + _db.Insert(user); + + // 返回插入后的用户对象前,将密码清空 + user.Password = ""; + + return user; + } + + /// + /// 对密码进行加密 + /// + /// 明文 + /// 加密后的密码 + private static string EncryptPassword(string password) + { + return Md5Util.Md5(password + "_mass-transfer"); + } + + + /// + /// 根据账号来获取用户信息 + /// + /// 用户账号 + /// + public User? GetUserByUserName(string userName) + { + return _db.Query("select * from users where user_name = ?", userName).FirstOrDefault(); + } + + /// + /// 获取所有的用户列表 + /// + /// + public List GetAllUsers() + { + return _db.ListAll(); + } + + /// + /// 更新用户信息 + /// + /// + public void UpdateUser(User user) + { + _db.Update(user); + } + + /// + /// 删除用户 + /// + /// + public void DeleteUser(string id) + { + _db.DeleteById(id); + } + + + /// + /// 用户登录校验 + /// + /// 用户账号 + /// 用户密码 + /// 用户信息 + /// 登录异常信息 + public User Login(string userName, string password) + { + var user = GetUserByUserName(userName); + if (user == null) throw new BizException("用户名或密码有误"); + + if (user.Password != EncryptPassword(password)) throw new BizException("用户名或密码有误"); + + // 返回插入后的用户对象前,将密码清空 + user.Password = ""; + + return user; + } + + /// + /// 根据用户id来获取用户信息 + /// + /// 用户id + public User GetUserById(string userId) + { + return _db.GetById(userId); + } + + /// + /// 进行重置密码 + /// + /// + public void ResetPassword(string userId) + { + var user = GetUserById(userId); + user.Password = EncryptPassword(DefaultPassword); + UpdateUser(user); + } +} \ No newline at end of file diff --git a/DispenserCore/Templates/TemplateManager.cs b/DispenserCore/Templates/TemplateManager.cs new file mode 100644 index 0000000..f7ecfae --- /dev/null +++ b/DispenserCore/Templates/TemplateManager.cs @@ -0,0 +1,8 @@ +namespace DispenserCore.Templates; + +/// +/// 模版管理器 +/// +public class TemplateManager +{ +} \ No newline at end of file diff --git a/DispenserDesktop/DispenserDesktop.csproj b/DispenserDesktop/DispenserDesktop.csproj new file mode 100644 index 0000000..b4291fb --- /dev/null +++ b/DispenserDesktop/DispenserDesktop.csproj @@ -0,0 +1,49 @@ + + + WinExe + + net7.0 + enable + true + app.manifest + latest + Dispenser + favicon.ico + 1.0.0.11 + 1.0.0.11 + + + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + Always + + + PreserveNewest + + + + + + + + diff --git a/DispenserDesktop/Libs/halcondotnet.dll b/DispenserDesktop/Libs/halcondotnet.dll new file mode 100644 index 0000000..8cf3efa Binary files /dev/null and b/DispenserDesktop/Libs/halcondotnet.dll differ diff --git a/DispenserDesktop/Program.cs b/DispenserDesktop/Program.cs new file mode 100644 index 0000000..c2e7211 --- /dev/null +++ b/DispenserDesktop/Program.cs @@ -0,0 +1,80 @@ +using System; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Logging; +using Avalonia.ReactiveUI; +using DispenserUI; +using Serilog; + +namespace DispenserDesktop; + +internal class Program +{ + // Initialization code. Don't use any Avalonia, third-party APIs or any + // SynchronizationContext-reliant code before AppMain is called: things aren't initialized + // yet and stuff might break. + [STAThread] + public static void Main(string[] args) + { + try + { + AppDomain.CurrentDomain.UnhandledException += UnhandledExceptionCatcher; + TaskScheduler.UnobservedTaskException += UnhandledTaskExceptionCatcher; + + var mutex = new Mutex(true, Process.GetCurrentProcess().ProcessName, out var createdNew); + // 通过互斥锁来实现进制应用多开 + if (!createdNew) + { + Environment.Exit(1); + } + else + { + // 获取到锁 + BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); + } + + // 释放信号量 + mutex.ReleaseMutex(); + } + catch (Exception e) + { + Console.WriteLine($"程序捕获崩溃异常: {e.StackTrace}"); + Log.Fatal(e, $"程序捕获崩溃异常: {e.Message}"); + } + finally + { + Log.CloseAndFlush(); + } + } + + private static void UnhandledExceptionCatcher(object? sender, UnhandledExceptionEventArgs e) + { + Log.Fatal(e.ExceptionObject as Exception, $"捕获到未处理异常: {((Exception)e.ExceptionObject).Message}"); + } + + /// + /// 捕获全局异步线程异常 + /// + /// + /// + private static void UnhandledTaskExceptionCatcher(object? sender, UnobservedTaskExceptionEventArgs e) + { + Log.Fatal(e.Exception, $"捕获到异步线程异常: {e.Exception.Message}"); + e.SetObserved(); + } + + + // Avalonia configuration, don't remove; also used by visual designer. + public static AppBuilder BuildAvaloniaApp() + { + return AppBuilder.Configure() + .UsePlatformDetect() + .With(new SkiaOptions { MaxGpuResourceSizeBytes = 8096000 }) + // 开启debug日志级别 + .LogToTrace(LogEventLevel.Debug) + .WithInterFont() + .UseReactiveUI(); + } +} \ No newline at end of file diff --git a/DispenserDesktop/Templates/523十字中心.shm b/DispenserDesktop/Templates/523十字中心.shm new file mode 100644 index 0000000..1c01b17 Binary files /dev/null and b/DispenserDesktop/Templates/523十字中心.shm differ diff --git a/DispenserDesktop/Templates/523圆中心.shm b/DispenserDesktop/Templates/523圆中心.shm new file mode 100644 index 0000000..10bc34c Binary files /dev/null and b/DispenserDesktop/Templates/523圆中心.shm differ diff --git a/DispenserDesktop/Templates/7_26mark1.shm b/DispenserDesktop/Templates/7_26mark1.shm new file mode 100644 index 0000000..2c58942 Binary files /dev/null and b/DispenserDesktop/Templates/7_26mark1.shm differ diff --git a/DispenserDesktop/Templates/Calibration6_24.shm b/DispenserDesktop/Templates/Calibration6_24.shm new file mode 100644 index 0000000..3f256d0 Binary files /dev/null and b/DispenserDesktop/Templates/Calibration6_24.shm differ diff --git a/DispenserDesktop/Templates/Calibration7_11(ch250).shm b/DispenserDesktop/Templates/Calibration7_11(ch250).shm new file mode 100644 index 0000000..0f53245 Binary files /dev/null and b/DispenserDesktop/Templates/Calibration7_11(ch250).shm differ diff --git a/DispenserDesktop/Templates/MARK点十字框模板.shm b/DispenserDesktop/Templates/MARK点十字框模板.shm new file mode 100644 index 0000000..1ce3717 Binary files /dev/null and b/DispenserDesktop/Templates/MARK点十字框模板.shm differ diff --git a/DispenserDesktop/Templates/MARK点十字框模板6_13.shm b/DispenserDesktop/Templates/MARK点十字框模板6_13.shm new file mode 100644 index 0000000..0962dfe Binary files /dev/null and b/DispenserDesktop/Templates/MARK点十字框模板6_13.shm differ diff --git a/DispenserDesktop/Templates/MARK点十字框模板6_3.shm b/DispenserDesktop/Templates/MARK点十字框模板6_3.shm new file mode 100644 index 0000000..53cc163 Binary files /dev/null and b/DispenserDesktop/Templates/MARK点十字框模板6_3.shm differ diff --git a/DispenserDesktop/Templates/MARK点十字框模板7_16.shm b/DispenserDesktop/Templates/MARK点十字框模板7_16.shm new file mode 100644 index 0000000..b4cc83f Binary files /dev/null and b/DispenserDesktop/Templates/MARK点十字框模板7_16.shm differ diff --git a/DispenserDesktop/Templates/MARK点框模板.shm b/DispenserDesktop/Templates/MARK点框模板.shm new file mode 100644 index 0000000..5c82762 Binary files /dev/null and b/DispenserDesktop/Templates/MARK点框模板.shm differ diff --git a/DispenserDesktop/Templates/MARK点框模板6_6.shm b/DispenserDesktop/Templates/MARK点框模板6_6.shm new file mode 100644 index 0000000..9b8eb57 Binary files /dev/null and b/DispenserDesktop/Templates/MARK点框模板6_6.shm differ diff --git a/DispenserDesktop/Templates/Mark2_0805.shm b/DispenserDesktop/Templates/Mark2_0805.shm new file mode 100644 index 0000000..3833aa7 Binary files /dev/null and b/DispenserDesktop/Templates/Mark2_0805.shm differ diff --git a/DispenserDesktop/Templates/Mark3_0805.shm b/DispenserDesktop/Templates/Mark3_0805.shm new file mode 100644 index 0000000..865465c Binary files /dev/null and b/DispenserDesktop/Templates/Mark3_0805.shm differ diff --git a/DispenserDesktop/Templates/Mark_圆心有毛刺.shm b/DispenserDesktop/Templates/Mark_圆心有毛刺.shm new file mode 100644 index 0000000..bbcece9 Binary files /dev/null and b/DispenserDesktop/Templates/Mark_圆心有毛刺.shm differ diff --git a/DispenserDesktop/Templates/Mark_圆心有毛刺0712.shm b/DispenserDesktop/Templates/Mark_圆心有毛刺0712.shm new file mode 100644 index 0000000..77231f0 Binary files /dev/null and b/DispenserDesktop/Templates/Mark_圆心有毛刺0712.shm differ diff --git a/DispenserDesktop/Templates/Mark十字7-24(中心).shm b/DispenserDesktop/Templates/Mark十字7-24(中心).shm new file mode 100644 index 0000000..923e6c3 Binary files /dev/null and b/DispenserDesktop/Templates/Mark十字7-24(中心).shm differ diff --git a/DispenserDesktop/Templates/Mark十字7-24.shm b/DispenserDesktop/Templates/Mark十字7-24.shm new file mode 100644 index 0000000..5c9f135 Binary files /dev/null and b/DispenserDesktop/Templates/Mark十字7-24.shm differ diff --git a/DispenserDesktop/Templates/Mark圆7-24.shm b/DispenserDesktop/Templates/Mark圆7-24.shm new file mode 100644 index 0000000..44a8331 Binary files /dev/null and b/DispenserDesktop/Templates/Mark圆7-24.shm differ diff --git a/DispenserDesktop/Templates/Pcb_mark(十字不带边框).shm b/DispenserDesktop/Templates/Pcb_mark(十字不带边框).shm new file mode 100644 index 0000000..992fb62 Binary files /dev/null and b/DispenserDesktop/Templates/Pcb_mark(十字不带边框).shm differ diff --git a/DispenserDesktop/Templates/Pcb_mark(圆不带边框).shm b/DispenserDesktop/Templates/Pcb_mark(圆不带边框).shm new file mode 100644 index 0000000..5e3d6a5 Binary files /dev/null and b/DispenserDesktop/Templates/Pcb_mark(圆不带边框).shm differ diff --git a/DispenserDesktop/Templates/TMark-os.shm b/DispenserDesktop/Templates/TMark-os.shm new file mode 100644 index 0000000..d1b4294 Binary files /dev/null and b/DispenserDesktop/Templates/TMark-os.shm differ diff --git a/DispenserDesktop/Templates/TMark.shm b/DispenserDesktop/Templates/TMark.shm new file mode 100644 index 0000000..00a432d Binary files /dev/null and b/DispenserDesktop/Templates/TMark.shm differ diff --git a/DispenserDesktop/Templates/chip-hx.shm b/DispenserDesktop/Templates/chip-hx.shm new file mode 100644 index 0000000..e913963 Binary files /dev/null and b/DispenserDesktop/Templates/chip-hx.shm differ diff --git a/DispenserDesktop/Templates/chip.shm b/DispenserDesktop/Templates/chip.shm new file mode 100644 index 0000000..34f9a02 Binary files /dev/null and b/DispenserDesktop/Templates/chip.shm differ diff --git a/DispenserDesktop/Templates/chip0517.shm b/DispenserDesktop/Templates/chip0517.shm new file mode 100644 index 0000000..1fdd648 Binary files /dev/null and b/DispenserDesktop/Templates/chip0517.shm differ diff --git a/DispenserDesktop/Templates/chip0527.shm b/DispenserDesktop/Templates/chip0527.shm new file mode 100644 index 0000000..717a9e3 Binary files /dev/null and b/DispenserDesktop/Templates/chip0527.shm differ diff --git a/DispenserDesktop/Templates/chip0618.shm b/DispenserDesktop/Templates/chip0618.shm new file mode 100644 index 0000000..a7b2bbd Binary files /dev/null and b/DispenserDesktop/Templates/chip0618.shm differ diff --git a/DispenserDesktop/Templates/chip0725.shm b/DispenserDesktop/Templates/chip0725.shm new file mode 100644 index 0000000..041f8be Binary files /dev/null and b/DispenserDesktop/Templates/chip0725.shm differ diff --git a/DispenserDesktop/Templates/ncc_chip_6-18.shm b/DispenserDesktop/Templates/ncc_chip_6-18.shm new file mode 100644 index 0000000..17f9724 Binary files /dev/null and b/DispenserDesktop/Templates/ncc_chip_6-18.shm differ diff --git a/DispenserDesktop/Templates/pcb-glass.shm b/DispenserDesktop/Templates/pcb-glass.shm new file mode 100644 index 0000000..fede5de Binary files /dev/null and b/DispenserDesktop/Templates/pcb-glass.shm differ diff --git a/DispenserDesktop/Templates/pcb-horizontal.shm b/DispenserDesktop/Templates/pcb-horizontal.shm new file mode 100644 index 0000000..1950145 Binary files /dev/null and b/DispenserDesktop/Templates/pcb-horizontal.shm differ diff --git a/DispenserDesktop/Templates/pcb-hx.shm b/DispenserDesktop/Templates/pcb-hx.shm new file mode 100644 index 0000000..d87cc70 Binary files /dev/null and b/DispenserDesktop/Templates/pcb-hx.shm differ diff --git a/DispenserDesktop/Templates/pcb-solderPaste.shm b/DispenserDesktop/Templates/pcb-solderPaste.shm new file mode 100644 index 0000000..9539162 Binary files /dev/null and b/DispenserDesktop/Templates/pcb-solderPaste.shm differ diff --git a/DispenserDesktop/Templates/pcb.shm b/DispenserDesktop/Templates/pcb.shm new file mode 100644 index 0000000..6ee8eb6 Binary files /dev/null and b/DispenserDesktop/Templates/pcb.shm differ diff --git a/DispenserDesktop/Templates/pcb_6-19_3.shm b/DispenserDesktop/Templates/pcb_6-19_3.shm new file mode 100644 index 0000000..193fd97 Binary files /dev/null and b/DispenserDesktop/Templates/pcb_6-19_3.shm differ diff --git a/DispenserDesktop/Templates/pcb_纯净版6-19.shm b/DispenserDesktop/Templates/pcb_纯净版6-19.shm new file mode 100644 index 0000000..25738be Binary files /dev/null and b/DispenserDesktop/Templates/pcb_纯净版6-19.shm differ diff --git a/DispenserDesktop/Templates/pcbmark(十字)7_26.shm b/DispenserDesktop/Templates/pcbmark(十字)7_26.shm new file mode 100644 index 0000000..f9e9602 Binary files /dev/null and b/DispenserDesktop/Templates/pcbmark(十字)7_26.shm differ diff --git a/DispenserDesktop/Templates/十字架.shm b/DispenserDesktop/Templates/十字架.shm new file mode 100644 index 0000000..c037cde Binary files /dev/null and b/DispenserDesktop/Templates/十字架.shm differ diff --git a/DispenserDesktop/Templates/标7_24中心圆.shm b/DispenserDesktop/Templates/标7_24中心圆.shm new file mode 100644 index 0000000..df159ba Binary files /dev/null and b/DispenserDesktop/Templates/标7_24中心圆.shm differ diff --git a/DispenserDesktop/Templates/矩形_十字架.shm b/DispenserDesktop/Templates/矩形_十字架.shm new file mode 100644 index 0000000..f5f830b Binary files /dev/null and b/DispenserDesktop/Templates/矩形_十字架.shm differ diff --git a/DispenserDesktop/app.manifest b/DispenserDesktop/app.manifest new file mode 100644 index 0000000..0ec540f --- /dev/null +++ b/DispenserDesktop/app.manifest @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + diff --git a/DispenserDesktop/dispenser.db b/DispenserDesktop/dispenser.db new file mode 100644 index 0000000..0b22326 Binary files /dev/null and b/DispenserDesktop/dispenser.db differ diff --git a/DispenserDesktop/favicon.ico b/DispenserDesktop/favicon.ico new file mode 100644 index 0000000..676868d Binary files /dev/null and b/DispenserDesktop/favicon.ico differ diff --git a/DispenserDesktop/halcon/poly_param.cal b/DispenserDesktop/halcon/poly_param.cal new file mode 100644 index 0000000..84952f9 Binary files /dev/null and b/DispenserDesktop/halcon/poly_param.cal differ diff --git a/DispenserDesktop/halcon/poly_param_dvt1.cal b/DispenserDesktop/halcon/poly_param_dvt1.cal new file mode 100644 index 0000000..f301a17 Binary files /dev/null and b/DispenserDesktop/halcon/poly_param_dvt1.cal differ diff --git a/DispenserDesktop/height.json b/DispenserDesktop/height.json new file mode 100644 index 0000000..b8b5fd0 --- /dev/null +++ b/DispenserDesktop/height.json @@ -0,0 +1,3008 @@ +[ + [ + 21.060699999999997, + 110.09089999999999, + 199.1214, + 288.15160000000003, + 21.01549999999999, + 110.0458, + 199.0762, + 288.1064, + 20.970299999999995, + 110.0008, + 199.0311, + 288.0615, + 20.925999999999988, + 109.9563, + 198.9869, + 288.017, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 141.69230000000002, + 141.7435, + 141.7947, + 141.8459, + 192.08950000000002, + 192.14075000000003, + 192.192, + 192.24315, + 242.48675, + 242.5379, + 242.5892, + 242.64035, + 292.04415, + 292.0953, + 292.1465, + 292.19765, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 8.611, + 8.606, + 8.581, + 8.542, + 8.643, + 8.619, + 8.591999999999999, + 8.554, + 8.66, + 8.641, + 8.613, + 8.565, + 8.669, + 8.668, + 8.636, + 8.567, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ] +] \ No newline at end of file diff --git a/DispenserDesktop/package/after-install.txt b/DispenserDesktop/package/after-install.txt new file mode 100644 index 0000000..36217d5 --- /dev/null +++ b/DispenserDesktop/package/after-install.txt @@ -0,0 +1 @@ +תƿϵͳѾװɣʹ˵в \ No newline at end of file diff --git a/DispenserDesktop/package/before-install.txt b/DispenserDesktop/package/before-install.txt new file mode 100644 index 0000000..3c13f8c --- /dev/null +++ b/DispenserDesktop/package/before-install.txt @@ -0,0 +1,17 @@ +װʾתƿϵͳ + +ӭװתƿϵͳһרΪԲתƹƵӦóּЧʺ͹վȡڿʼװ֮ǰȷļϵͳҪ + +ϵͳWindows 7/8/10 +Intel Core i5ܴͬ +ڴ棺4GB RAM +洢ռ䣺500MBÿռ +ʾֱʣ1280x720 +װ裺 + +غתƿϵͳװ +˫аװ򣬰ʾаװ +ڰװУҪԱɰװ +װɺԴ򿪺תƿϵͳûֲнһúáڰװκ⣬ϵǵļ֧Ŷӻȡ + +лѡ񺣾תƿϵͳףھԲתƹ̿Ʒȡóɹ \ No newline at end of file diff --git a/DispenserDesktop/package/install.iss b/DispenserDesktop/package/install.iss new file mode 100644 index 0000000..8a40fa9 --- /dev/null +++ b/DispenserDesktop/package/install.iss @@ -0,0 +1,358 @@ +; Script generated by the Inno Setup Script Wizard. +; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! + + +; �����Ǵ����ʱ����Ҫ����Ϊ��Ӧ����ԴĿ¼λ�� +#define DeployDir "D:\workspace\deploy" + + + +#define MyAppName "Dispenser" +#define MyAppVersion "1.0.0-alpha-20240131" +#define MyAppPublisher "Beijing Haiju, Inc." +#define MyAppURL "https://www.haijuDispenser.com/" +#define MyAppExeName "Dispenser.exe" +#define MyAppAssocName MyAppName + " File" +#define MyAppAssocExt ".myp" +#define MyAppAssocKey StringChange(MyAppAssocName, " ", "") + MyAppAssocExt +#define SourceDir DeployDir + "\program" +#define SetupIcon SourceDir + "\favicon.ico" +#define UninstallIcon SourceDir + "\favicon.ico" +#define GUID "8D5DB57B-B0AE-49F6-A3D2-F502A656825C" + + +[Setup] +; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications. +; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) +AppId={{{#GUID}} +AppName={#MyAppName} +AppVersion={#MyAppVersion} +;AppVerName={#MyAppName} {#MyAppVersion} +AppPublisher={#MyAppPublisher} +AppPublisherURL={#MyAppURL} +AppSupportURL={#MyAppURL} +AppUpdatesURL={#MyAppURL} +DefaultDirName={autopf}\{#MyAppName} +ChangesAssociations=yes +DefaultGroupName={#MyAppName} +AllowNoIcons=yes +LicenseFile={#SourceDir}\package\license.txt +InfoBeforeFile={#SourceDir}\package\before-install.txt +InfoAfterFile={#SourceDir}\package\after-install.txt +; Uncomment the following line to run in non administrative install mode (install for current user only.) +;PrivilegesRequired=lowest +OutputDir={#DeployDir} +OutputBaseFilename={#MyAppName}-{#MyAppVersion} +Compression=lzma +SolidCompression=yes +WizardStyle=modern +;ָ����װ��ж�س���ͼ�� +SetupIconFile={#SetupIcon} +;�������ж��ͼ�� +UninstallDisplayIcon={#UninstallIcon} + +[Languages] +Name: "chinesesimplified"; MessagesFile: "compiler:Languages\ChineseSimplified.isl" + +[Tasks] +Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked + +[Files] +Source: "{#SourceDir}\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Assets\*"; DestDir: "{app}\Assets\"; Flags: ignoreversion recursesubdirs createallsubdirs +Source: "{#SourceDir}\runtimes\win\*"; DestDir: "{app}\runtimes\win\"; Flags: ignoreversion recursesubdirs createallsubdirs +Source: "{#SourceDir}\runtimes\win-x64\*"; DestDir: "{app}\runtimes\win-x64\"; Flags: ignoreversion recursesubdirs createallsubdirs +Source: "{#SourceDir}\runtimes\win-x86\*"; DestDir: "{app}\runtimes\win-x86\"; Flags: ignoreversion recursesubdirs createallsubdirs +Source: "{#SourceDir}\zh-Hans\*"; DestDir: "{app}\zh-Hans\"; Flags: ignoreversion recursesubdirs createallsubdirs +Source: "{#SourceDir}\AnimatedImage.Avalonia.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\AnimatedImage.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\AspectInjector.Broker.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Avalonia.Base.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Avalonia.Controls.ColorPicker.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Avalonia.Controls.DataGrid.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Avalonia.Controls.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Avalonia.Controls.ItemsRepeater.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Avalonia.DesignerSupport.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Avalonia.Desktop.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Avalonia.Diagnostics.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Avalonia.Dialogs.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Avalonia.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Avalonia.Fonts.Inter.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Avalonia.FreeDesktop.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Avalonia.Markup.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Avalonia.Markup.Xaml.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Avalonia.Metal.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Avalonia.MicroCom.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Avalonia.Native.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Avalonia.OpenGL.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Avalonia.ReactiveUI.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Avalonia.Remote.Protocol.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Avalonia.Skia.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Avalonia.Themes.Fluent.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Avalonia.Themes.Simple.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Avalonia.Win32.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Avalonia.X11.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\BouncyCastle.Crypto.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\ColorTextBlock.Avalonia.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\CommunityToolkit.Mvvm.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\DialogHost.Avalonia.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\DynamicData.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\EntityFramework.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\EntityFramework.SqlServer.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\GxIAPINET.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\HarfBuzzSharp.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Markdown.Avalonia.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Dispenser.deps.json"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Dispenser.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Dispenser.exe"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Dispenser.pdb"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Dispenser.runtimeconfig.json"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\DispenserCommon.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\DispenserCommon.pdb"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\DispenserUI.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\DispenserUI.pdb"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\MessageBoxSlim.Avalonia.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\MicroCom.Runtime.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Microsoft.AspNetCore.Server.Kestrel.Https.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Microsoft.CodeAnalysis.CSharp.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Microsoft.CodeAnalysis.CSharp.Scripting.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Microsoft.CodeAnalysis.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Microsoft.CodeAnalysis.Scripting.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Microsoft.Extensions.Configuration.Abstractions.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Microsoft.Extensions.Configuration.Binder.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Microsoft.Extensions.Configuration.CommandLine.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Microsoft.Extensions.Configuration.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Microsoft.Extensions.Configuration.EnvironmentVariables.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Microsoft.Extensions.Configuration.FileExtensions.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Microsoft.Extensions.Configuration.Ini.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Microsoft.Extensions.Configuration.Json.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Microsoft.Extensions.Configuration.UserSecrets.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Microsoft.Extensions.DependencyInjection.Abstractions.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Microsoft.Extensions.DependencyInjection.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Microsoft.Extensions.DependencyModel.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Microsoft.Extensions.Diagnostics.Abstractions.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Microsoft.Extensions.Diagnostics.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Microsoft.Extensions.FileProviders.Abstractions.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Microsoft.Extensions.FileProviders.Physical.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Microsoft.Extensions.FileSystemGlobbing.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Microsoft.Extensions.Hosting.Abstractions.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Microsoft.Extensions.Hosting.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Microsoft.Extensions.Logging.Abstractions.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Microsoft.Extensions.Logging.Configuration.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Microsoft.Extensions.Logging.Console.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Microsoft.Extensions.Logging.Debug.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Microsoft.Extensions.Logging.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Microsoft.Extensions.Logging.EventLog.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Microsoft.Extensions.Logging.EventSource.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Microsoft.Extensions.Options.ConfigurationExtensions.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Microsoft.Extensions.Options.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Microsoft.Extensions.Primitives.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Microsoft.Win32.SystemEvents.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\MsBox.Avalonia.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Newtonsoft.Json.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Opc.Ua.Bindings.Https.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Opc.Ua.Client.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Opc.Ua.Configuration.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Opc.Ua.Core.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Opc.Ua.Gds.Client.Common.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Opc.Ua.Gds.Server.Common.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Opc.Ua.Security.Certificates.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Opc.Ua.Server.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\OpcUaHelper.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\OpcUaSdk.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\OpcUaSdk.pdb"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\ReactiveUI.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Serilog.AspNetCore.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Serilog.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Serilog.Exceptions.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Serilog.Extensions.Hosting.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Serilog.Extensions.Logging.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Serilog.Formatting.Compact.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Serilog.Settings.Configuration.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Serilog.Sinks.Console.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Serilog.Sinks.Debug.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Serilog.Sinks.File.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\settings.ini"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\SkiaSharp.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Splat.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\System.CodeDom.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\System.Configuration.ConfigurationManager.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\System.Data.SqlClient.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\System.Data.SQLite.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\System.Data.SQLite.EF6.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\System.Diagnostics.DiagnosticSource.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\System.Diagnostics.EventLog.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\System.Drawing.Common.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\System.IO.Ports.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\System.Private.ServiceModel.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\System.Reactive.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\System.Security.Cryptography.ProtectedData.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\System.Security.Permissions.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\System.ServiceModel.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\System.ServiceModel.Primitives.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\System.Text.Encodings.Web.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\System.Text.Json.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\System.Windows.Extensions.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\Tmds.DBus.Protocol.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#DeployDir}\dotnet-runtime-7.0.15-win-x64.exe"; DestDir: "{app}"; Flags: noencryption deleteafterinstall +Source: "{#DeployDir}\windowsdesktop-runtime-7.0.15-win-x64.exe"; DestDir: "{app}"; Flags: noencryption deleteafterinstall +Source: "{#DeployDir}\aspnetcore-runtime-7.0.15-win-x64.exe"; DestDir: "{app}"; Flags: noencryption deleteafterinstall +Source: "{#DeployDir}\Galaxy_Windows_Runtime.exe"; DestDir: "{app}"; Flags: noencryption deleteafterinstall +; NOTE: Don't use "Flags: ignoreversion" on any shared system files + +[Run] +Filename: "{app}\dotnet-runtime-7.0.15-win-x64.exe"; Parameters: "/norestart"; StatusMsg: "���Ժ�!���ڼ�鰲װ��Ҫ�����л��� .NET RUNTIME 7.0"; Check: not IsDotNetRuntimeInstalled('Microsoft.NETCore.App 7.') +Filename: "{app}\windowsdesktop-runtime-7.0.15-win-x64.exe"; Parameters: "/norestart"; StatusMsg: "���Ժ�!���ڼ�鰲װ��Ҫ�����л��� DESKTOP RUNTIME Core 7.0"; Check: not IsDotNetRuntimeInstalled('Microsoft.WindowsDesktop.App 7.') +Filename: "{app}\aspnetcore-runtime-7.0.15-win-x64.exe"; Parameters: "/norestart"; StatusMsg: "���Ժ�!���ڼ�鰲װ��Ҫ�����л��� ASP.NET RUNTIME 7.0"; Check: not IsDotNetRuntimeInstalled('Microsoft.AspNetCore.App 7.') + + +[Run] +Filename: "{app}\Galaxy_Windows_Runtime.exe"; Parameters: "/norestart"; StatusMsg: "���Ժ�!���ڼ�鰲װ�������"; Check: not IsGalaxyInstalled + + +[Registry] +Root: HKA; Subkey: "Software\Classes\{#MyAppAssocExt}\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppAssocKey}"; ValueData: ""; Flags: uninsdeletevalue +Root: HKA; Subkey: "Software\Classes\{#MyAppAssocKey}"; ValueType: string; ValueName: ""; ValueData: "{#MyAppAssocName}"; Flags: uninsdeletekey +Root: HKA; Subkey: "Software\Classes\{#MyAppAssocKey}\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#MyAppExeName},0" +Root: HKA; Subkey: "Software\Classes\{#MyAppAssocKey}\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#MyAppExeName}"" ""%1""" +Root: HKA; Subkey: "Software\Classes\Applications\{#MyAppExeName}\SupportedTypes"; ValueType: string; ValueName: ".myp"; ValueData: "" +Root: HKLM; Subkey: "SOFTWARE\Microsoft\Windows\CurrentVersion\Run"; ValueType: string; ValueName: "{#MyAppName}"; ValueData: "{app}\{#MyAppExeName}" + +[Icons] +Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" +Name: "{group}\{cm:ProgramOnTheWeb,{#MyAppName}}"; Filename: "{#MyAppURL}" +Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}" +Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon + +[Run] +// [Run]- ����ָ���ڳ�����ɰ�װ���ڰ�װ������ʾ���նԻ���ǰҪִ�е�һЩ���� +Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent + + + +[Code] +// �ж��Ƿ��Ѿ���װ���� .NET RUNTIME ���� +function IsDotNetRuntimeInstalled(DotNetName: string): Boolean; + var + Cmd, Args: string; + FileName: string; + Output: AnsiString; + Command: string; + ResultCode: Integer; + begin + FileName := ExpandConstant('{tmp}\dotnet.txt'); + Cmd := ExpandConstant('{cmd}'); + Command := 'dotnet --list-runtimes'; + // �� dotnet --list-runtimes �Ľ������� dotnet.txt �ļ� + Args := '/C ' + Command + ' > "' + FileName + '" 2>&1'; + if Exec(Cmd, Args, '', SW_HIDE, ewWaitUntilTerminated, ResultCode) and (ResultCode = 0) then + begin + // �� dotnet.txt �ļ� �ж�ȡ���� + if LoadStringFromFile(FileName, Output) then + begin + if Pos(DotNetName, Output) > 0 then + begin + Result := True; + end + else + begin + MsgBox(DotNetName + ' �汾������Ҫ���밲װ��', mbError, MB_OK); + Result := False; + end; + end + end + else + begin + MsgBox(DotNetName + ' �汾������Ҫ���밲װ��', mbError, MB_OK); + Result := False; + end; + // ɾ�� dotnet.txt �ļ� + DeleteFile(FileName); +end; + +// �жϴ����������Ƿ�װ�ɹ� +function IsGalaxyInstalled: Boolean; +begin + Result := True; + + if RegValueExists(HKLM64, 'SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Galaxy Runtime SDK_is1', 'DisplayName') then + begin + Result := True; + Exit; + end + else if RegValueExists(HKLM64, 'SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Galaxy SDK_is1', 'DisplayName') then + begin + Result := True; + Exit; + end + else + begin + MsgBox('ȱ�ٱ�Ҫ���������������װĿ��������', mbError, MB_OK); + Result := False; + Exit; + end; +end; + + +//�������Ƿ����� +function IsAppRunning(const FileName: string): Boolean; +var + FWMIService: Variant; + FSWbemLocator: Variant; + FWbemObjectSet: Variant; +begin + Result := false; + FSWbemLocator := CreateOleObject('WBEMScripting.SWBEMLocator'); + FWMIService := FSWbemLocator.ConnectServer('', 'root\CIMV2', '', ''); + FWbemObjectSet := FWMIService.ExecQuery(Format('SELECT Name FROM Win32_Process Where Name="%s"',[FileName])); + Result := (FWbemObjectSet.Count > 0); + FWbemObjectSet := Unassigned; + FWMIService := Unassigned; + FSWbemLocator := Unassigned; +end; + +//׼����װ +function InitializeSetup(): Boolean; +var + ResultStr: String; + ResultCode: Integer; +begin + result := IsAppRunning('{#MyAppExeName}'); + if result then + begin + MsgBox('��⵽{#MyAppName}�������У����ȹرճ��������! ', mbError, MB_OK); + result:=false; + end + else if RegQueryStringValue(HKLM, 'SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{#GUID}_is1', 'UninstallString', ResultStr) then + begin + if MsgBox('�Ƿ�ж���Ѱ�װ��{#MyAppName}����������ʷ���ݣ�', mbConfirmation, MB_YESNO) = IDYES then + begin + ResultStr := RemoveQuotes(ResultStr); + Exec(ResultStr, '/silent', '', SW_HIDE, ewWaitUntilTerminated, ResultCode); + end; + result:=true; + end + else + begin + result:=true; + end; +end; + +//׼��ж�� +function InitializeUninstall(): Boolean; +begin + result := IsAppRunning('{#MyAppExeName}'); + if result then + begin + MsgBox('��⵽{#MyAppName}�������У����ȹرճ��������! ', mbError, MB_OK); + result:=false; + end + else + begin + result:=true; + end +end; + + + diff --git a/DispenserDesktop/package/license.txt b/DispenserDesktop/package/license.txt new file mode 100644 index 0000000..0c91547 --- /dev/null +++ b/DispenserDesktop/package/license.txt @@ -0,0 +1,23 @@ +Э + +Ӧ: תƿϵͳ + +ɷƼ޹˾ + +ɷΧ +תƿϵͳ¼ơΪƼ޹˾ӵеľԲתƿϵͳɷû޵ġתõɣûڵһ豸ϰװʹñ + +ʹã +ûԽڿƾԲתƹ̣ҽûʹڼڵĺϷҵʹñ + +֪ʶȨ +֪ʶȨ麣Ƽ޹˾Сûøơ޸ġַۡκʽ򹤳̡򷴱 + + +ûͬԱԴ뼰صļϺϢܣй¶ + + +÷ķΧڣƼ޹˾ʹû޷ʹñµκֱӻʧ + +ֹ +ûΥЭκƼ޹˾ȨֹЭ飬׷Ȩ \ No newline at end of file diff --git a/DispenserDesktop/settings.ini b/DispenserDesktop/settings.ini new file mode 100644 index 0000000..c928a2c --- /dev/null +++ b/DispenserDesktop/settings.ini @@ -0,0 +1,3 @@ +[AppSettings] +Language = en-US +ContentPath = c:\content diff --git a/DispenserDriver.CameraMV/DispenserDriver.CameraMV.csproj b/DispenserDriver.CameraMV/DispenserDriver.CameraMV.csproj new file mode 100644 index 0000000..7354c44 --- /dev/null +++ b/DispenserDriver.CameraMV/DispenserDriver.CameraMV.csproj @@ -0,0 +1,23 @@ + + + + net7.0 + enable + enable + + + + + + + + + MvCamCtrl.Net.dll + + + + + + + + diff --git a/DispenserDriver.CameraMV/Impl/CameraMVDriver.cs b/DispenserDriver.CameraMV/Impl/CameraMVDriver.cs new file mode 100644 index 0000000..3db345d --- /dev/null +++ b/DispenserDriver.CameraMV/Impl/CameraMVDriver.cs @@ -0,0 +1,316 @@ +using DispenserCommon.Utils; +using DispenserHal.Camera.DTO; +using DispenserHal.Camera.Enum; +using DispenserHal.Camera.Interface; +using DispenserHal.Camera.VO; +using DispenserUI.Exceptions; +using MvCamCtrl.NET; +using MvCamCtrl.NET.CameraParams; +using Serilog; + +namespace DispenserDriver.CameraMV.Impl; + +public class CameraMVDriver : AbstractCamera +{ + private CCameraInfo _cameraInfo; + private cbOutputExdelegate _cbOutputExdelegate; + private readonly CCamera _mvCamera = new(); + + public CameraMVDriver(string sn, Dictionary paramMap) : base(sn, paramMap) + { + } + + protected override CameraResponseVO DoConnect(long timeout) + { + _cameraInfo = EnumDeviceBySn(SN); + if (_cameraInfo == null) return CameraResponseVO.OfFailed($"无法找到{SN}相机"); + + if (!CSystem.IsDeviceAccessible(ref _cameraInfo, MV_ACCESS_MODE.MV_ACCESS_CONTROL)) + return CameraResponseVO.OfFailed($"{SN}相机不可访问"); + + var createHandleResult = _mvCamera.CreateHandle(ref _cameraInfo); + if (!IsSuccess(createHandleResult)) return CameraResponseVO.OfFailed($"{SN}创建句柄失败"); + + var openDeviceResult = _mvCamera.OpenDevice(); + if (!IsSuccess(openDeviceResult)) + { + _mvCamera.DestroyHandle(); + CameraResponseVO.OfFailed($"打开{SN}相机失败"); + } + + _cbOutputExdelegate = ImageCallbackFunc; + var registerImageCallbackResult = + _mvCamera.RegisterImageCallBackEx(_cbOutputExdelegate, IntPtr.Zero); + if (!IsSuccess(registerImageCallbackResult)) + { + Close(); + CameraResponseVO.OfFailed("注册回调函数失败"); + } + + return CameraResponseVO.OfSuccess($"连接{SN}相机成功"); + } + + protected override CameraResponseVO DoClose() + { + StopSteaming(); + _mvCamera.CloseDevice(); + _mvCamera.DestroyHandle(); + MarkConnect(false); + return CameraResponseVO.OfSuccess($"关闭{SN}相机成功"); + } + + + protected override CameraResponseVO DoStartSteaming() + { + var result = _mvCamera.StartGrabbing(); + if (!IsSuccess(result)) return CameraResponseVO.OfFailed($"{SN}开启采集失败,错误码{result}"); + + Started = true; + return CameraResponseVO.OfSuccess($"{SN}开启采集成功"); + } + + protected override CameraResponseVO DoStopSteaming() + { + var result = _mvCamera.StopGrabbing(); + if (!IsSuccess(result)) return CameraResponseVO.OfFailed($"{SN}关闭采集失败,错误码{result}"); + + Started = false; + return CameraResponseVO.OfSuccess($"{SN}关闭采集成功"); + } + + /// + /// 获取一帧的图像 + /// + /// + protected override CameraResponseVO DoTokePhoto() + { + var resp = SwitchSoftwareTriggerMode(); + // 现切换到软触发模式 + if (resp.IsSuccess()) + // 进行软触发拍照 + SoftwareTrigger(); + + // 拍完之后切换到硬触发状态 + SwitchHardTriggerMode(); + + return CameraResponseVO.OfSuccess(); + } + + /// + /// 开始连续采集 + /// + /// + protected override CameraResponseVO DoStartCaptureVideo() + { + _mvCamera.SetEnumValue("TriggerMode", 0); + CapturingVideo = true; + return CameraResponseVO.OfSuccess(); + } + + /// + /// 停止连续采集 + /// + /// + protected override CameraResponseVO DoStopCaptureVideo() + { + SetAcquisitionControlToHardTrigger(); + CapturingVideo = false; + return CameraResponseVO.OfSuccess(); + } + + public override bool GetBoolValue(string key) + { + var value = false; + _mvCamera.GetBoolValue(key, ref value); + return value; + } + + public override IntValue GetIntValue(string key) + { + var value = new CIntValue(); + _mvCamera.GetIntValue(key, ref value); + return BeanUtil.CopyProperties(value); + } + + public override StringValue? GetStringValue(string key) + { + var value = new CStringValue(); + _mvCamera.GetStringValue(key, ref value); + return BeanUtil.CopyProperties(value); + } + + public override EnumItem GetEnumEntrySymbolic(string key, uint supportIndex) + { + var entry = new CEnumEntry(); + entry.Value = supportIndex; + _mvCamera.GetEnumEntrySymbolic(key, ref entry); + return BeanUtil.CopyProperties(entry); + } + + public override EnumValue? GetEnumValue(string key) + { + var value = new CEnumValue(); + _mvCamera.GetEnumValue(key, ref value); + return BeanUtil.CopyProperties(value); + } + + public override FloatValue? GetFloatValue(string key) + { + var value = new CFloatValue(); + _mvCamera.GetFloatValue(key, ref value); + return BeanUtil.CopyProperties(value); + } + + public override CameraResponseVO SwitchHardTriggerMode() + { + SetDigitalIOControlToHardTrigger(); + + SetAcquisitionControlToHardTrigger(); + + return CameraResponseVO.OfSuccess(); + } + + public override CameraResponseVO SwitchContinuousMode() + { + //开启触发模式 0:OFF, 1: ON + _mvCamera.SetEnumValue("TriggerMode", 0); + return CameraResponseVO.OfSuccess(); + } + + public override CameraResponseVO SwitchSoftwareTriggerMode() + { + //开启触发模式 0:OFF, 1: ON + _mvCamera.SetEnumValue("TriggerMode", 1); + //0:Line0 + //1:Line1 + //2:Line2 + //3.Line3 + //4:Counter0 + //7:Software + //8:FrequencyConverter + _mvCamera.SetEnumValue("TriggerSource", 7); + + return CameraResponseVO.OfSuccess(); + } + + public override CameraResponseVO SoftwareTrigger() + { + var nRet = _mvCamera.SetCommandValue("TriggerSoftware"); + if (CErrorDefine.MV_OK != nRet) + { + Log.Error($"{SN}软触发失败"); + return CameraResponseVO.OfFailed($"{SN}软触发失败"); + } + + return CameraResponseVO.OfSuccess(); + } + + public override CameraResponseVO SetEnumValue(string strKey, uint nvalue) + { + _mvCamera.SetEnumValue(strKey, nvalue); + return CameraResponseVO.OfSuccess(); + } + + public override CameraResponseVO SetIntValue(string strKey, long nvalue) + { + _mvCamera.SetIntValue(strKey, nvalue); + return CameraResponseVO.OfSuccess(); + } + + public override CameraResponseVO SetBoolValue(string strKey, bool nvalue) + { + _mvCamera.SetBoolValue(strKey, nvalue); + return CameraResponseVO.OfSuccess(); + } + + public override CameraResponseVO SetFloatValue(string strKey, float nvalue) + { + _mvCamera.SetFloatValue(strKey, nvalue); + return CameraResponseVO.OfSuccess(); + } + + private void SetAcquisitionControlToHardTrigger() + { + //开启触发模式 0:OFF, 1: ON + _mvCamera.SetEnumValue("TriggerMode", 1); + //0:Line0 + //1:Line1 + //2:Line2 + //3.Line3 + //4:Counter0 + //7:Software + //8:FrequencyConverter + _mvCamera.SetEnumValue("TriggerSource", 0); + //0:RisingEdge 触发上升沿 + //1:FallingEdge 下降沿 + //2:LevelHigh 高电平 + //3:LevelLow 低电平等 + _mvCamera.SetEnumValue("TriggerActivation", 0); + } + + /// + /// 光源控制器IO硬触发设置 + /// + private void SetDigitalIOControlToHardTrigger() + { + //IO触发源 + //0:Line0 + //1:Line1 + //2:Line2 + //3:Line3 + //4:Line4 + _mvCamera.SetEnumValue("LineSelector", 1); + + //LineMode + //0:Input + //1:Output + //2:Trigger + //8:Strobe 频闪模式 + _mvCamera.SetEnumValue("LineMode", 8); + + //开启频闪模式 + _mvCamera.SetBoolValue("StrobeEnable", true); + } + + private void ImageCallbackFunc(IntPtr pData, ref MV_FRAME_OUT_INFO_EX pFrameInfo, IntPtr pUser) + { + var bitmap = _mvCamera.ImageToBitmap(pData, ref pFrameInfo); + Callback(SN, bitmap); + } + + private CCameraInfo? EnumDeviceBySn(string sn) + { + var deviceList = new List(); + var result = CSystem.EnumDevices(CSystem.MV_GIGE_DEVICE | CSystem.MV_USB_DEVICE, ref deviceList); + if (!IsSuccess(result)) return null; + + CCameraInfo _currentCamera = null; + foreach (var cCameraInfo in deviceList) + { + var _sn = FindSn(cCameraInfo); + if (sn.Equals(_sn)) + { + _currentCamera = cCameraInfo; + break; + } + } + + return _currentCamera; + } + + private string FindSn(CCameraInfo cCameraInfo) + { + if (cCameraInfo.nTLayerType == CSystem.MV_USB_DEVICE) + return ((CUSBCameraInfo)cCameraInfo).chSerialNumber; + if (cCameraInfo.nTLayerType == CSystem.MV_GIGE_DEVICE) + return ((CGigECameraInfo)cCameraInfo).chSerialNumber; + if (cCameraInfo.nTLayerType == CSystem.MV_CAMERALINK_DEVICE) + return ((CCamLCameraInfo)cCameraInfo).chSerialNumber; + throw new BizException($"不支持的相机类型{cCameraInfo.nTLayerType}"); + } + + private bool IsSuccess(int result) + { + return result == CErrorDefine.MV_OK; + } +} \ No newline at end of file diff --git a/DispenserDriver.CameraMV/MvCamCtrl.Net.dll b/DispenserDriver.CameraMV/MvCamCtrl.Net.dll new file mode 100644 index 0000000..b48d2fb Binary files /dev/null and b/DispenserDriver.CameraMV/MvCamCtrl.Net.dll differ diff --git a/DispenserHal/Camera/Client/CameraClient.cs b/DispenserHal/Camera/Client/CameraClient.cs new file mode 100644 index 0000000..b0223ce --- /dev/null +++ b/DispenserHal/Camera/Client/CameraClient.cs @@ -0,0 +1,164 @@ +using System.Drawing; +using DispenserHal.Camera.DTO; +using DispenserHal.Camera.Interface; +using DispenserHal.Camera.VO; +using Serilog; + +namespace DispenserHal.Camera.Client; + +public class CameraClient : ICamera +{ + private readonly AbstractCamera delegateClient; + + public CameraClient(AbstractCamera delegateClient) + { + this.delegateClient = delegateClient; + } + + /// + /// 设备号 + /// + public string SN => delegateClient.SN; + + /// + /// 连接状态 + /// + public bool Connected => delegateClient.Connected; + + /// + /// 标识当前是否在采集视频中 + /// + public bool CapturingVideo => delegateClient.CapturingVideo; + + public CameraResponseVO Connect() + { + return delegateClient.Connect(); + } + + public CameraResponseVO Connect(long timeout) + { + return delegateClient.Connect(timeout); + } + + public CameraResponseVO Close() + { + return delegateClient.Close(); + } + + public CameraResponseVO RegisterCallback(Action callback) + { + return delegateClient.RegisterCallback(callback); + } + + public CameraResponseVO StartSteaming() + { + return delegateClient.StartSteaming(); + } + + /// + /// 相机拍照 + /// + /// + public CameraResponseVO TokePhoto() + { + return delegateClient.TokePhoto(); + } + + /// + /// 开始连续采集 + /// + /// + public CameraResponseVO StartCaptureVideo() + { + return delegateClient.StartCaptureVideo(); + } + + /// + /// 停止连续采集 + /// + /// + public CameraResponseVO StopCaptureVideo() + { + return delegateClient.StopCaptureVideo(); + } + + public CameraResponseVO SwitchHardTriggerMode() + { + return delegateClient.SwitchHardTriggerMode(); + } + + public CameraResponseVO StopSteaming() + { + return delegateClient.StopSteaming(); + } + + public CameraResponseVO SwitchContinuousMode() + { + return delegateClient.SwitchContinuousMode(); + } + + public CameraResponseVO SwitchSoftwareTriggerMode() + { + return delegateClient.SwitchSoftwareTriggerMode(); + } + + public CameraResponseVO SoftwareTrigger() + { + return delegateClient.SoftwareTrigger(); + } + + //写入参数 + public CameraResponseVO SetEnumValue(string strKey, uint nvalue) + { + Log.Information($"正在更新相机参数: {strKey},值为 {nvalue}"); + return delegateClient.SetEnumValue(strKey, nvalue); + } + + public CameraResponseVO SetIntValue(string strKey, long nvalue) + { + Log.Information($"正在更新相机参数: {strKey},值为 {nvalue}"); + return delegateClient.SetIntValue(strKey, nvalue); + } + + public CameraResponseVO SetBoolValue(string strKey, bool nvalue) + { + Log.Information($"正在更新相机参数: {strKey},值为 {nvalue}"); + return delegateClient.SetBoolValue(strKey, nvalue); + } + + public CameraResponseVO SetFloatValue(string strKey, float nvalue) + { + Log.Information($"正在更新相机参数: {strKey},值为 {nvalue}"); + return delegateClient.SetFloatValue(strKey, nvalue); + } + + public bool GetBoolValue(string key) + { + return delegateClient.GetBoolValue(key); + } + + public IntValue GetIntValue(string key) + { + return delegateClient.GetIntValue(key); + } + + public FloatValue? GetFloatValue(string key) + { + return delegateClient.GetFloatValue(key); + } + + public EnumValue? GetEnumValue(string key) + { + return delegateClient.GetEnumValue(key); + } + + public StringValue? GetStringValue(string key) + { + return delegateClient.GetStringValue(key); + } + + public EnumItem GetEnumEntrySymbolic(string key, uint supportIndex) + { + return delegateClient.GetEnumEntrySymbolic(key, supportIndex); + } +} \ No newline at end of file diff --git a/DispenserHal/Camera/Client/CameraContainer.cs b/DispenserHal/Camera/Client/CameraContainer.cs new file mode 100644 index 0000000..212c63b --- /dev/null +++ b/DispenserHal/Camera/Client/CameraContainer.cs @@ -0,0 +1,21 @@ +namespace DispenserHal.Camera.Client; + +public class CameraContainer +{ + private static readonly Dictionary clientMap = new(); + + public static CameraClient GetClient(string sn) + { + return clientMap[sn]; + } + + public static void AddClient(string sn, CameraClient client) + { + clientMap[sn] = client; + } + + public static bool ContainsClient(string sn) + { + return clientMap.ContainsKey(sn); + } +} \ No newline at end of file diff --git a/DispenserHal/Camera/DTO/CameraConfig.cs b/DispenserHal/Camera/DTO/CameraConfig.cs new file mode 100644 index 0000000..26a9e40 --- /dev/null +++ b/DispenserHal/Camera/DTO/CameraConfig.cs @@ -0,0 +1,12 @@ +namespace DispenserHal.Camera.DTO; + +public class CameraConfig +{ + public static string CameraSn { get; set; } + + public static string Sdk { get; set; } + + public static string Dll { get; set; } + + public static double ScaleRatio { get; set; } +} \ No newline at end of file diff --git a/DispenserHal/Camera/DTO/EnumItem.cs b/DispenserHal/Camera/DTO/EnumItem.cs new file mode 100644 index 0000000..54fc5f9 --- /dev/null +++ b/DispenserHal/Camera/DTO/EnumItem.cs @@ -0,0 +1,37 @@ +using System.ComponentModel; +using System.Runtime.CompilerServices; + +namespace DispenserHal.Camera.DTO; + +public class EnumItem : INotifyPropertyChanged +{ + private string _symbolic; + private uint _value; + + public uint Value + { + get => _value; + set + { + _value = value; + OnPropertyChanged(); + } + } + + public string Symbolic + { + get => _symbolic; + set + { + _symbolic = value; + OnPropertyChanged(); + } + } + + public event PropertyChangedEventHandler? PropertyChanged; + + protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } +} \ No newline at end of file diff --git a/DispenserHal/Camera/DTO/EnumValue.cs b/DispenserHal/Camera/DTO/EnumValue.cs new file mode 100644 index 0000000..040656a --- /dev/null +++ b/DispenserHal/Camera/DTO/EnumValue.cs @@ -0,0 +1,85 @@ +using System.ComponentModel; +using System.Runtime.CompilerServices; + +namespace DispenserHal.Camera.DTO; + +public class EnumValue : INotifyPropertyChanged +{ + private int _curIndex; + private uint _curValue; + + private List _items = []; + + private List _labels = []; + + private uint _supportedNum; + + private uint[] _supportValue = []; + + public uint CurValue + { + get => _curValue; + set + { + _curValue = value; + OnPropertyChanged(); + } + } + + public uint SupportedNum + { + get => _supportedNum; + set + { + _supportedNum = value; + OnPropertyChanged(); + } + } + + public uint[] SupportValue + { + get => _supportValue; + set + { + _supportValue = value; + OnPropertyChanged(); + } + } + + public int CurIndex + { + get => _curIndex; + set + { + _curIndex = value; + OnPropertyChanged(); + } + } + + public List Items + { + get => _items; + set + { + _items = value; + OnPropertyChanged(); + } + } + + public List Labels + { + get => _labels; + set + { + _labels = value; + OnPropertyChanged(); + } + } + + public event PropertyChangedEventHandler? PropertyChanged; + + protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } +} \ No newline at end of file diff --git a/DispenserHal/Camera/DTO/FloatValue.cs b/DispenserHal/Camera/DTO/FloatValue.cs new file mode 100644 index 0000000..c24d4fc --- /dev/null +++ b/DispenserHal/Camera/DTO/FloatValue.cs @@ -0,0 +1,50 @@ +using System.ComponentModel; +using System.Runtime.CompilerServices; + +namespace DispenserHal.Camera.DTO; + +public class FloatValue : INotifyPropertyChanged +{ + private float _curValue; + + private float _max; + + private float _min; + + public float CurValue + { + get => _curValue; + set + { + _curValue = value; + OnPropertyChanged(); + } + } + + public float Max + { + get => _max; + set + { + _max = value; + OnPropertyChanged(); + } + } + + public float Min + { + get => _min; + set + { + _min = value; + OnPropertyChanged(); + } + } + + public event PropertyChangedEventHandler? PropertyChanged; + + protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } +} \ No newline at end of file diff --git a/DispenserHal/Camera/DTO/IntValue.cs b/DispenserHal/Camera/DTO/IntValue.cs new file mode 100644 index 0000000..ea566e2 --- /dev/null +++ b/DispenserHal/Camera/DTO/IntValue.cs @@ -0,0 +1,62 @@ +using System.ComponentModel; +using System.Runtime.CompilerServices; + +namespace DispenserHal.Camera.DTO; + +public class IntValue : INotifyPropertyChanged +{ + private long _curValue; + + private long _inc; + + private long _max; + + private long _min; + + public long CurValue + { + get => _curValue; + set + { + _curValue = value; + OnPropertyChanged(); + } + } + + public long Max + { + get => _max; + set + { + _max = value; + OnPropertyChanged(); + } + } + + public long Min + { + get => _min; + set + { + _min = value; + OnPropertyChanged(); + } + } + + public long Inc + { + get => _inc; + set + { + _inc = value; + OnPropertyChanged(); + } + } + + public event PropertyChangedEventHandler? PropertyChanged; + + protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } +} \ No newline at end of file diff --git a/DispenserHal/Camera/DTO/StringValue.cs b/DispenserHal/Camera/DTO/StringValue.cs new file mode 100644 index 0000000..ebdab69 --- /dev/null +++ b/DispenserHal/Camera/DTO/StringValue.cs @@ -0,0 +1,38 @@ +using System.ComponentModel; +using System.Runtime.CompilerServices; + +namespace DispenserHal.Camera.DTO; + +public class StringValue : INotifyPropertyChanged +{ + private string _curValue; + + private long _maxLength; + + public string CurValue + { + get => _curValue; + set + { + _curValue = value; + OnPropertyChanged(); + } + } + + public long MaxLength + { + get => _maxLength; + set + { + _maxLength = value; + OnPropertyChanged(); + } + } + + public event PropertyChangedEventHandler? PropertyChanged; + + protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } +} \ No newline at end of file diff --git a/DispenserHal/Camera/Enum/ParamTypeEnum.cs b/DispenserHal/Camera/Enum/ParamTypeEnum.cs new file mode 100644 index 0000000..9bb6477 --- /dev/null +++ b/DispenserHal/Camera/Enum/ParamTypeEnum.cs @@ -0,0 +1,12 @@ +namespace DispenserHal.Camera.Enum; + +public enum ParamTypeEnum +{ + INT, + LONG, + DOUBLE, + STRING, + FLOAT, + BOOL, + ENUM +} \ No newline at end of file diff --git a/DispenserHal/Camera/Factory/CameraFactory.cs b/DispenserHal/Camera/Factory/CameraFactory.cs new file mode 100644 index 0000000..b5bec3e --- /dev/null +++ b/DispenserHal/Camera/Factory/CameraFactory.cs @@ -0,0 +1,86 @@ +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using DispenserHal.Camera.Client; +using DispenserHal.Camera.DTO; +using DispenserHal.Camera.Enum; +using DispenserHal.Camera.Interface; +using Serilog; + +namespace DispenserHal.Camera.Factory; + +public sealed class CameraFactory +{ + private static AbstractCamera cameraSDK; + private static Type instanceType; + + private static CameraConfig _config; + + static CameraFactory() + { + InitType(); + } + + private static void InitType() + { + var asmb = Assembly.LoadFrom(CameraConfig.Dll); + instanceType = asmb.GetType(CameraConfig.Sdk); + if (instanceType == null) + { + Log.Error("加载sdk{0}失败", CameraConfig.Sdk); + throw new FileNotFoundException(CameraConfig.Sdk); + } + } + + public static Builder ToBuilder(string sn) + { + return new Builder(sn); + } + + private static CameraClient GetClient([NotNull] string sn, Dictionary paramMap) + { + if (!CameraContainer.ContainsClient(sn)) + { + var client = InitClient(sn, paramMap); + CameraContainer.AddClient(sn, client); + } + + return CameraContainer.GetClient(sn); + } + + private static CameraClient InitClient(string sn, Dictionary paramMap) + { + object[] constructorArgs = [sn, paramMap]; + cameraSDK = Activator.CreateInstance(instanceType, constructorArgs) as AbstractCamera; + var client = new CameraClient(cameraSDK); + Log.Information($"{sn}相机客户端初始化成功"); + return client; + } + + public class Builder + { + private Dictionary _param; + private string _sn; + + public Builder(string sn) + { + _sn = sn; + } + + public Builder SN(string sn) + { + _sn = sn; + return this; + } + + public Builder Param(Dictionary param) + { + _param = param; + return this; + } + + public CameraClient build() + { + return GetClient(_sn, _param); + } + } +} \ No newline at end of file diff --git a/DispenserHal/Camera/Factory/CameraManager.cs b/DispenserHal/Camera/Factory/CameraManager.cs new file mode 100644 index 0000000..161bf40 --- /dev/null +++ b/DispenserHal/Camera/Factory/CameraManager.cs @@ -0,0 +1,111 @@ +using System.Drawing; +using DispenserCommon.Enums; +using DispenserCommon.Events; +using DispenserCommon.Exceptions; +using DispenserCommon.Utils; +using DispenserHal.Camera.Client; +using DispenserHal.Camera.DTO; +using DispenserHal.Camera.VO; +using DispenserUI.Exceptions; +using Serilog; + +namespace DispenserHal.Camera.Factory; + +/// +/// 相机管理器,用于全局管理相机的连接 +/// +public class CameraManager +{ + private static CameraClient? _camera; + + /// + /// 全局初始化连接相机 + /// + public static void Connect() + { + InitCamera(); + } + + /// + /// 对相机进行重连 + /// + public static void ReConnect() + { + ToastUtil.Info("正在尝试重新连接相机"); + Log.Information("正在尝试重新连接相机"); + + if (_camera != null) + { + Close(); + } + + InitCamera(); + } + + private static void InitCamera() + { + try + { + EventBus.Publish(EventType.SetupNotify, "正在连接相机"); + _camera = CreateCamera(CameraConfig.CameraSn, CallBack); + var cameraResponseVo = _camera.Connect(); + + if (!cameraResponseVo.IsSuccess()) + { + ToastUtil.Error($"无法连接到相机 {CameraConfig.CameraSn}"); + throw new CameraException($"无法连接到相机 {CameraConfig.CameraSn}"); + } + + ToastUtil.Success($"相机 {CameraConfig.CameraSn} 连接成功"); + // 连接成功之后开启接收流 + _camera.StartSteaming(); + _camera.SwitchHardTriggerMode(); + Console.WriteLine(cameraResponseVo); + + + // 开个子线程来处理接收的图片 + // Task.Run(HandleImage); + } + catch (Exception e) + { + EventBus.Publish(EventType.SetupNotify, $"相机连接失败,错误信息:{e.Message}"); + } + } + + private static CameraClient CreateCamera(string sn, Action action) + { + var client = CameraFactory.ToBuilder(sn).build(); + client.RegisterCallback(action); + return client; + } + + /// + /// 收到相机数据后统一向外发送数据 + /// + /// 相机sn + /// 当前相机回调的数据 + private static void CallBack(string name, Bitmap bitmap) + { + EventBus.Publish(EventType.CameraData, new CameraData(name, bitmap)); + } + + public static void Close() + { + _camera!.Close(); + } + + /// + /// 获取相机对象 + /// + public static CameraClient? GetCamera() + { + if (_camera!.Connected) + { + return _camera; + } + + EventBus.Publish(EventType.Exception, + new CameraException($"无法链接到相机 {CameraConfig.CameraSn}", level: ExceptionLevel.ERROR)); + return null; + } +} \ No newline at end of file diff --git a/DispenserHal/Camera/Interface/AbstractCamera.cs b/DispenserHal/Camera/Interface/AbstractCamera.cs new file mode 100644 index 0000000..335e2b5 --- /dev/null +++ b/DispenserHal/Camera/Interface/AbstractCamera.cs @@ -0,0 +1,213 @@ +using System.Drawing; +using DispenserHal.Camera.DTO; +using DispenserHal.Camera.Enum; +using DispenserHal.Camera.VO; +using Serilog; + +namespace DispenserHal.Camera.Interface; + +public abstract class AbstractCamera : ICamera +{ + public Dictionary BoolParam = new(); + private readonly long defaultTimeout = 10000; + public Dictionary DoubleParam = new(); + public Dictionary EnumParam = new(); + public Dictionary FloatParam = new(); + + + public Dictionary IntParam = new(); + public Dictionary LongParam = new(); + public Dictionary StringParam = new(); + + protected AbstractCamera(string sn, Dictionary paramMap) + { + SN = sn; + ParseParam(paramMap); + } + + private bool isConnected => Connected; + private bool isStart => Started; + public string SN { get; } + + public bool CapturingVideo { get; set; } + + protected Action Callback { get; private set; } + + public bool Connected { get; set; } + public bool Started { get; set; } + + public CameraResponseVO Connect() + { + return Connect(defaultTimeout); + } + + public CameraResponseVO Connect(long timeout) + { + if (isConnected) return CameraResponseVO.OfFailed("相机已经连接"); + + var resp = DoConnect(timeout); + if (resp.IsSuccess()) MarkConnect(true); + + return resp; + } + + + public CameraResponseVO Close() + { + if (!isConnected) return CameraResponseVO.OfFailed($"{SN}已关闭"); + + var resp = DoClose(); + if (resp.IsSuccess()) MarkConnect(false); + + return resp; + } + + + public CameraResponseVO RegisterCallback(Action callback) + { + Callback = callback; + return CameraResponseVO.OfSuccess("注册回调函数成功"); + } + + + public CameraResponseVO StartSteaming() + { + if (!isConnected) return CameraResponseVO.OfFailed($"{SN}未连接"); + + if (isStart) return CameraResponseVO.OfFailed($"{SN}已开启采集"); + + var resp = DoStartSteaming(); + if (resp.IsSuccess()) MarkStarted(true); + + return resp; + } + + public CameraResponseVO TokePhoto() + { + if (!isConnected) + { + Log.Error($"{SN}未连接"); + return CameraResponseVO.OfFailed($"{SN}未连接"); + } + + return DoTokePhoto(); + } + + public CameraResponseVO StartCaptureVideo() + { + if (!isConnected) + { + Log.Error($"{SN}未连接"); + return CameraResponseVO.OfFailed($"{SN}未连接"); + } + + return DoStartCaptureVideo(); + } + + public CameraResponseVO StopCaptureVideo() + { + if (!isConnected) + { + Log.Error($"{SN}未连接"); + return CameraResponseVO.OfFailed($"{SN}未连接"); + } + + return DoStopCaptureVideo(); + } + + public abstract CameraResponseVO SwitchHardTriggerMode(); + + + public CameraResponseVO StopSteaming() + { + if (!isConnected) return CameraResponseVO.OfFailed($"{SN}未连接"); + + if (!isStart) return CameraResponseVO.OfFailed($"{SN}已关闭采集"); + + var resp = DoStopSteaming(); + if (resp.IsSuccess()) MarkStarted(false); + + return resp; + } + + public abstract CameraResponseVO SwitchContinuousMode(); + public abstract CameraResponseVO SwitchSoftwareTriggerMode(); + public abstract CameraResponseVO SoftwareTrigger(); + + public abstract CameraResponseVO SetEnumValue(string name, uint value); + public abstract CameraResponseVO SetIntValue(string strKey, long nvalue); + public abstract CameraResponseVO SetBoolValue(string strKey, bool nvalue); + public abstract CameraResponseVO SetFloatValue(string strKey, float nvalue); + + public abstract bool GetBoolValue(string key); + + public abstract EnumValue? GetEnumValue(string key); + public abstract FloatValue? GetFloatValue(string key); + public abstract IntValue GetIntValue(string key); + public abstract StringValue? GetStringValue(string key); + + public abstract EnumItem GetEnumEntrySymbolic(string key, uint supportIndex); + + protected void MarkConnect(bool value) + { + Connected = value; + } + + protected void MarkStarted(bool value) + { + Started = value; + } + + private void ParseParam(Dictionary paramMap) + { + if (paramMap == null || !paramMap.Any()) return; + + foreach (var key in paramMap.Keys) DoParseParam(key, paramMap[key]); + } + + private void DoParseParam(ParamTypeEnum key, string value) + { + if (string.IsNullOrEmpty(value)) return; + + var keyPairs = value.Split(','); + foreach (var kp in keyPairs) + { + var kvArr = kp.Split(':'); + if (kvArr.Length == 2) + switch (key) + { + case ParamTypeEnum.INT: + IntParam[kvArr[0]] = int.Parse(kvArr[1]); + break; + case ParamTypeEnum.LONG: + LongParam[kvArr[0]] = long.Parse(kvArr[1]); + break; + case ParamTypeEnum.DOUBLE: + DoubleParam[kvArr[0]] = double.Parse(kvArr[1]); + break; + case ParamTypeEnum.FLOAT: + FloatParam[kvArr[0]] = float.Parse(kvArr[1]); + break; + case ParamTypeEnum.STRING: + StringParam[kvArr[0]] = kvArr[1]; + break; + case ParamTypeEnum.BOOL: + BoolParam[kvArr[0]] = bool.Parse(kvArr[1]); + break; + case ParamTypeEnum.ENUM: + EnumParam[kvArr[0]] = kvArr[1]; + break; + } + } + } + + protected abstract CameraResponseVO DoClose(); + protected abstract CameraResponseVO DoConnect(long timeout); + protected abstract CameraResponseVO DoStartSteaming(); + protected abstract CameraResponseVO DoStopSteaming(); + protected abstract CameraResponseVO DoTokePhoto(); + + protected abstract CameraResponseVO DoStartCaptureVideo(); + + protected abstract CameraResponseVO DoStopCaptureVideo(); +} \ No newline at end of file diff --git a/DispenserHal/Camera/Interface/ICamera.cs b/DispenserHal/Camera/Interface/ICamera.cs new file mode 100644 index 0000000..5af32be --- /dev/null +++ b/DispenserHal/Camera/Interface/ICamera.cs @@ -0,0 +1,103 @@ +using System.Drawing; +using DispenserHal.Camera.DTO; +using DispenserHal.Camera.VO; + +namespace DispenserHal.Camera.Interface; + +public interface ICamera +{ + /// + /// 连接相机 + /// + public CameraResponseVO Connect(); + + /// + /// 连接相机带超时时间 + /// + public CameraResponseVO Connect(long timeout); + + /// + /// 关闭相机 + /// + public CameraResponseVO Close(); + + /// + /// 注册回调函数 + /// + /// + public CameraResponseVO RegisterCallback(Action callback); + + /// + /// 开启采集(回调方式) + /// + public CameraResponseVO StartSteaming(); + + /// + /// 获取一帧图像 + /// + /// + CameraResponseVO TokePhoto(); + + /// + /// 开始连续采集 + /// + /// + CameraResponseVO StartCaptureVideo(); + + /// + /// 停止连续采集 + /// + /// + public CameraResponseVO StopCaptureVideo(); + + /// + /// 切换硬触发模式 + /// + /// + public CameraResponseVO SwitchHardTriggerMode(); + + /// + /// 关闭采集 + /// + public CameraResponseVO StopSteaming(); + + /// + /// 切换连续触发模式 + /// + /// + public CameraResponseVO SwitchContinuousMode(); + + /// + /// 切换软发模式 + /// + /// + public CameraResponseVO SwitchSoftwareTriggerMode(); + + /// + /// 软触发 + /// + /// + public CameraResponseVO SoftwareTrigger(); + + //写入参数 + public CameraResponseVO SetEnumValue(string strKey, uint nvalue); + public CameraResponseVO SetIntValue(string strKey, long nvalue); + public CameraResponseVO SetBoolValue(string strKey, bool nvalue); + public CameraResponseVO SetFloatValue(string strKey, float nvalue); + + bool GetBoolValue(string key); + + IntValue GetIntValue(string key); + FloatValue? GetFloatValue(string key); + + EnumValue? GetEnumValue(string key); + StringValue? GetStringValue(string key); + + /// + /// 获取枚举值 + /// + /// + /// + /// + EnumItem GetEnumEntrySymbolic(string key, uint supportIndex); +} \ No newline at end of file diff --git a/DispenserHal/Camera/VO/CameraData.cs b/DispenserHal/Camera/VO/CameraData.cs new file mode 100644 index 0000000..9ed00e0 --- /dev/null +++ b/DispenserHal/Camera/VO/CameraData.cs @@ -0,0 +1,13 @@ +using System.Drawing; + +namespace DispenserHal.Camera.VO; + +/// +/// 监听的摄像头数据 +/// +public class CameraData(string name, Bitmap image) +{ + public string Name { get; set; } = name; + + public Bitmap Image { get; set; } = image; +} \ No newline at end of file diff --git a/DispenserHal/Camera/VO/CameraResponseVO.cs b/DispenserHal/Camera/VO/CameraResponseVO.cs new file mode 100644 index 0000000..3a6f2a4 --- /dev/null +++ b/DispenserHal/Camera/VO/CameraResponseVO.cs @@ -0,0 +1,59 @@ +namespace DispenserHal.Camera.VO; + +public class CameraResponseVO +{ + private const string _defaultSuccessMessage = "操作成功"; + private const string _defaultFailedMessage = "操作失败"; + private readonly bool _success; + + public CameraResponseVO(bool result, string message, ResponseData data) + { + _success = result; + Message = message; + Data = data; + } + + public ResponseData Data { get; } + + public string Message { get; } + + public static CameraResponseVO OfSuccess() + { + return new CameraResponseVO(true, _defaultSuccessMessage, ResponseData.Empty()); + } + + public static CameraResponseVO OfSuccess(string message) + { + return new CameraResponseVO(true, message, ResponseData.Empty()); + } + + public static CameraResponseVO OfSuccess(string message, ResponseData data) + { + return new CameraResponseVO(true, message, data); + } + + public static CameraResponseVO OfFailed() + { + return new CameraResponseVO(false, _defaultFailedMessage, ResponseData.Empty()); + } + + public static CameraResponseVO OfFailed(string message) + { + return new CameraResponseVO(false, message, ResponseData.Empty()); + } + + public static CameraResponseVO OfFailed(string message, ResponseData data) + { + return new CameraResponseVO(false, message, data); + } + + public bool IsSuccess() + { + return _success; + } + + public override string ToString() + { + return $"CameraResponseVO: IsSuccess={_success}, Message={Message}, Data={Data}"; + } +} \ No newline at end of file diff --git a/DispenserHal/Camera/VO/ResponseData.cs b/DispenserHal/Camera/VO/ResponseData.cs new file mode 100644 index 0000000..0180219 --- /dev/null +++ b/DispenserHal/Camera/VO/ResponseData.cs @@ -0,0 +1,9 @@ +namespace DispenserHal.Camera.VO; + +public class ResponseData +{ + public static ResponseData Empty() + { + return new ResponseData(); + } +} \ No newline at end of file diff --git a/DispenserHal/DispenserHal.csproj b/DispenserHal/DispenserHal.csproj new file mode 100644 index 0000000..ebd03d1 --- /dev/null +++ b/DispenserHal/DispenserHal.csproj @@ -0,0 +1,19 @@ + + + + net7.0 + enable + enable + preview + + + + + + + + + + + + diff --git a/DispenserUI/App.axaml b/DispenserUI/App.axaml new file mode 100644 index 0000000..8bdadc9 --- /dev/null +++ b/DispenserUI/App.axaml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/DispenserUI/App.axaml.cs b/DispenserUI/App.axaml.cs new file mode 100644 index 0000000..a253b0e --- /dev/null +++ b/DispenserUI/App.axaml.cs @@ -0,0 +1,185 @@ +using System; +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Data.Core.Plugins; +using Avalonia.Markup.Xaml; +using DispenserCommon.Events; +using DispenserCommon.Lazy; +using DispenserCommon.LogConfig; +using DispenserCommon.Utils; +using DispenserCore.IOC; +using DispenserCore.Job; +using DispenserHal.Camera.Factory; +using DispenserUI.Utils; +using DispenserUI.Views; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Serilog; +using SplashWindow = DispenserUI.Views.Windows.SplashWindow; + +namespace DispenserUI; + +public class App : Application +{ + private readonly IHostBuilder _builder; + + private IServiceCollection _services; + + public App() + { + // Let's do this in the constructor + _builder = Host.CreateDefaultBuilder(); + AppHost = _builder + // 开启加载配置文件 + .ConfigureAppConfiguration((host, config) => + { + var env = host.HostingEnvironment; + config.Sources.Clear(); + config.AddIniFile("settings.ini", false, true) + .AddIniFile($"settings.{env.EnvironmentName}.ini", true, true) + .AddEnvironmentVariables(); + }) + .UseContentRoot(AppContext.BaseDirectory) + .ConfigureServices((context, services) => + { + // Register services with the DI container here. + var components = IocScanner.Scan(); + components.ForEach(component => { services.AddSingleton(component.Type, component.Implement); }); + services.AddLazyResolution(); + _services = services; + }) + .Build(); + } + + /// + /// test + /// + public IHost AppHost { get; } + + // Check if the service is registered in ConfigureServices within App.axaml.cs. + public static T GetService() + where T : class + { + if ((Current as App)!.AppHost.Services.GetService(typeof(T)) is not T service) + throw new ArgumentException( + $"{typeof(T)} needs to be registered in ConfigureServices within App.axaml.cs."); + + return service; + } + + public override void Initialize() + { + // 对需要进行即时加载的类进行实例化 + InstantInit(); + + AvaloniaXamlLoader.Load(this); + } + + public override async void OnFrameworkInitializationCompleted() + { + // remove each entry found + BindingPlugins.DataValidators.Insert(0, new CustomExceptionValidationPlugin()); + + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + // 先配置启动页 + var splashWindow = new SplashWindow(); + desktop.MainWindow = splashWindow; + desktop.Exit += Shutdown; + try + { + // 进行系统初始化操作 + await splashWindow.Init(); + + // 设置主窗口 + var mainWindow = new MainWindow(); + desktop.MainWindow = mainWindow; + mainWindow.Show(); + splashWindow.Close(); + } + catch (Exception e) + { + Console.WriteLine($"系统初始化异常: {e}"); + splashWindow.Close(); + return; + } + } + + // 开启serilog 收集日志 + Log.Logger = LogConfiguration.GetLogger(); + _builder.UseSerilog(Log.Logger); + + base.OnFrameworkInitializationCompleted(); + + + // 进行初始化完成通知 + Startup(); + } + + /// + /// 对需要进行即时加载的类进行实例化 + /// + private void InstantInit() + { + ServiceLocator.Initialize(AppHost.Services, _services); + + // 初始化事件总线 + EventManager.RegListeners(); + } + + /// + /// 应用启动事件回调 + /// + private static void Startup() + { + Log.Information( + """ + + _ _ _____ _ _ _ __ __ _______ + | | | | /\ |_ _| | | | | | | \/ |__ __| + | |__| | / \ | | | | | | | | \ / | | | + | __ | / /\ \ | | _ | | | | | | |\/| | | | + | | | |/ ____ \ _| |_ | |__| | |__| | | | | | | | + |_| |_/_/ \_\_____| \____/ \____/ |_| |_| |_| + 海炬巨量转移-应用启动完成 {0} + + """, DateTime.Now); + + // 发布系统启动完成通知 + EventBus.Publish(EventType.StartUp, true); + } + + /// + /// 应用关闭事件回调 + /// + /// + /// + private static void Shutdown(object? sender, + ControlledApplicationLifetimeExitEventArgs controlledApplicationLifetimeExitEventArgs) + { + Log.Information(""" + + ____ _ + | __ ) _ _ ___ | | + | _ \| | | |/ _ \ | | + | |_) | |_| | __/ |_| + |____/ \__, |\___| (_) + |___/ + 海炬点胶机-应用正在退出 {0} + """, DateTime.Now); + + + // 关闭相机连接 + CameraManager.Close(); + + //关闭调度任务 + SchedulerManager.Shutdown(); + + // 释放Serilog 资源 + Log.CloseAndFlush(); + + // 系统退出 + Environment.Exit(0); + } +} \ No newline at end of file diff --git a/DispenserUI/Assets/UI/1_1_white.png b/DispenserUI/Assets/UI/1_1_white.png new file mode 100644 index 0000000..df48d10 Binary files /dev/null and b/DispenserUI/Assets/UI/1_1_white.png differ diff --git a/DispenserUI/Assets/UI/BG.png b/DispenserUI/Assets/UI/BG.png new file mode 100644 index 0000000..e1482ab Binary files /dev/null and b/DispenserUI/Assets/UI/BG.png differ diff --git a/DispenserUI/Assets/UI/BG@2x.png b/DispenserUI/Assets/UI/BG@2x.png new file mode 100644 index 0000000..e1d3887 Binary files /dev/null and b/DispenserUI/Assets/UI/BG@2x.png differ diff --git a/DispenserUI/Assets/UI/BigAI.png b/DispenserUI/Assets/UI/BigAI.png new file mode 100644 index 0000000..ed0602b Binary files /dev/null and b/DispenserUI/Assets/UI/BigAI.png differ diff --git a/DispenserUI/Assets/UI/BigAI@2x.png b/DispenserUI/Assets/UI/BigAI@2x.png new file mode 100644 index 0000000..fd8a842 Binary files /dev/null and b/DispenserUI/Assets/UI/BigAI@2x.png differ diff --git a/DispenserUI/Assets/UI/Error.png b/DispenserUI/Assets/UI/Error.png new file mode 100644 index 0000000..f73500e Binary files /dev/null and b/DispenserUI/Assets/UI/Error.png differ diff --git a/DispenserUI/Assets/UI/Error@2x.png b/DispenserUI/Assets/UI/Error@2x.png new file mode 100644 index 0000000..ddf3da4 Binary files /dev/null and b/DispenserUI/Assets/UI/Error@2x.png differ diff --git a/DispenserUI/Assets/UI/Frame 40885.png b/DispenserUI/Assets/UI/Frame 40885.png new file mode 100644 index 0000000..46b9b6d Binary files /dev/null and b/DispenserUI/Assets/UI/Frame 40885.png differ diff --git a/DispenserUI/Assets/UI/Lockicon.png b/DispenserUI/Assets/UI/Lockicon.png new file mode 100644 index 0000000..645fd02 Binary files /dev/null and b/DispenserUI/Assets/UI/Lockicon.png differ diff --git a/DispenserUI/Assets/UI/Lockicon@2x.png b/DispenserUI/Assets/UI/Lockicon@2x.png new file mode 100644 index 0000000..7cc5e9f Binary files /dev/null and b/DispenserUI/Assets/UI/Lockicon@2x.png differ diff --git a/DispenserUI/Assets/UI/axis.png b/DispenserUI/Assets/UI/axis.png new file mode 100644 index 0000000..83e7c4c Binary files /dev/null and b/DispenserUI/Assets/UI/axis.png differ diff --git a/DispenserUI/Assets/UI/blank.png b/DispenserUI/Assets/UI/blank.png new file mode 100644 index 0000000..cf1f832 Binary files /dev/null and b/DispenserUI/Assets/UI/blank.png differ diff --git a/DispenserUI/Assets/UI/calibration.png b/DispenserUI/Assets/UI/calibration.png new file mode 100644 index 0000000..9cd8177 Binary files /dev/null and b/DispenserUI/Assets/UI/calibration.png differ diff --git a/DispenserUI/Assets/UI/camera.png b/DispenserUI/Assets/UI/camera.png new file mode 100644 index 0000000..e06cdd9 Binary files /dev/null and b/DispenserUI/Assets/UI/camera.png differ diff --git a/DispenserUI/Assets/UI/camera@2x.png b/DispenserUI/Assets/UI/camera@2x.png new file mode 100644 index 0000000..65fd1db Binary files /dev/null and b/DispenserUI/Assets/UI/camera@2x.png differ diff --git a/DispenserUI/Assets/UI/camera_white.png b/DispenserUI/Assets/UI/camera_white.png new file mode 100644 index 0000000..16a43e0 Binary files /dev/null and b/DispenserUI/Assets/UI/camera_white.png differ diff --git a/DispenserUI/Assets/UI/capturing_rainbow.gif b/DispenserUI/Assets/UI/capturing_rainbow.gif new file mode 100644 index 0000000..8f68f58 Binary files /dev/null and b/DispenserUI/Assets/UI/capturing_rainbow.gif differ diff --git a/DispenserUI/Assets/UI/chip.bmp b/DispenserUI/Assets/UI/chip.bmp new file mode 100644 index 0000000..c7cf273 Binary files /dev/null and b/DispenserUI/Assets/UI/chip.bmp differ diff --git a/DispenserUI/Assets/UI/clone.png b/DispenserUI/Assets/UI/clone.png new file mode 100644 index 0000000..ea2d206 Binary files /dev/null and b/DispenserUI/Assets/UI/clone.png differ diff --git a/DispenserUI/Assets/UI/close@2x.png b/DispenserUI/Assets/UI/close@2x.png new file mode 100644 index 0000000..288c3ff Binary files /dev/null and b/DispenserUI/Assets/UI/close@2x.png differ diff --git a/DispenserUI/Assets/UI/codepen.png b/DispenserUI/Assets/UI/codepen.png new file mode 100644 index 0000000..ebe38d7 Binary files /dev/null and b/DispenserUI/Assets/UI/codepen.png differ diff --git a/DispenserUI/Assets/UI/codepen@2x.png b/DispenserUI/Assets/UI/codepen@2x.png new file mode 100644 index 0000000..f9180aa Binary files /dev/null and b/DispenserUI/Assets/UI/codepen@2x.png differ diff --git a/DispenserUI/Assets/UI/confirmed.png b/DispenserUI/Assets/UI/confirmed.png new file mode 100644 index 0000000..d5f0566 Binary files /dev/null and b/DispenserUI/Assets/UI/confirmed.png differ diff --git a/DispenserUI/Assets/UI/crosshair_black.png b/DispenserUI/Assets/UI/crosshair_black.png new file mode 100644 index 0000000..85b9c71 Binary files /dev/null and b/DispenserUI/Assets/UI/crosshair_black.png differ diff --git a/DispenserUI/Assets/UI/crosshair_red.png b/DispenserUI/Assets/UI/crosshair_red.png new file mode 100644 index 0000000..078d766 Binary files /dev/null and b/DispenserUI/Assets/UI/crosshair_red.png differ diff --git a/DispenserUI/Assets/UI/crosshair_white.png b/DispenserUI/Assets/UI/crosshair_white.png new file mode 100644 index 0000000..0d88833 Binary files /dev/null and b/DispenserUI/Assets/UI/crosshair_white.png differ diff --git a/DispenserUI/Assets/UI/cycle_1_white.png b/DispenserUI/Assets/UI/cycle_1_white.png new file mode 100644 index 0000000..a2583d6 Binary files /dev/null and b/DispenserUI/Assets/UI/cycle_1_white.png differ diff --git a/DispenserUI/Assets/UI/cycle_white.png b/DispenserUI/Assets/UI/cycle_white.png new file mode 100644 index 0000000..fa7419f Binary files /dev/null and b/DispenserUI/Assets/UI/cycle_white.png differ diff --git a/DispenserUI/Assets/UI/disc.png b/DispenserUI/Assets/UI/disc.png new file mode 100644 index 0000000..19dfc75 Binary files /dev/null and b/DispenserUI/Assets/UI/disc.png differ diff --git a/DispenserUI/Assets/UI/disc@2x.png b/DispenserUI/Assets/UI/disc@2x.png new file mode 100644 index 0000000..79a487d Binary files /dev/null and b/DispenserUI/Assets/UI/disc@2x.png differ diff --git a/DispenserUI/Assets/UI/down.png b/DispenserUI/Assets/UI/down.png new file mode 100644 index 0000000..6de900e Binary files /dev/null and b/DispenserUI/Assets/UI/down.png differ diff --git a/DispenserUI/Assets/UI/down_orange.png b/DispenserUI/Assets/UI/down_orange.png new file mode 100644 index 0000000..3e609b0 Binary files /dev/null and b/DispenserUI/Assets/UI/down_orange.png differ diff --git a/DispenserUI/Assets/UI/edit-2.png b/DispenserUI/Assets/UI/edit-2.png new file mode 100644 index 0000000..bff74e8 Binary files /dev/null and b/DispenserUI/Assets/UI/edit-2.png differ diff --git a/DispenserUI/Assets/UI/edit-2@2x.png b/DispenserUI/Assets/UI/edit-2@2x.png new file mode 100644 index 0000000..3f96f09 Binary files /dev/null and b/DispenserUI/Assets/UI/edit-2@2x.png differ diff --git a/DispenserUI/Assets/UI/enlarge_white.png b/DispenserUI/Assets/UI/enlarge_white.png new file mode 100644 index 0000000..61cae77 Binary files /dev/null and b/DispenserUI/Assets/UI/enlarge_white.png differ diff --git a/DispenserUI/Assets/UI/fault_red.png b/DispenserUI/Assets/UI/fault_red.png new file mode 100644 index 0000000..e318ccb Binary files /dev/null and b/DispenserUI/Assets/UI/fault_red.png differ diff --git a/DispenserUI/Assets/UI/graid_red.png b/DispenserUI/Assets/UI/graid_red.png new file mode 100644 index 0000000..fc31aa9 Binary files /dev/null and b/DispenserUI/Assets/UI/graid_red.png differ diff --git a/DispenserUI/Assets/UI/graid_white.png b/DispenserUI/Assets/UI/graid_white.png new file mode 100644 index 0000000..d5a0ae1 Binary files /dev/null and b/DispenserUI/Assets/UI/graid_white.png differ diff --git a/DispenserUI/Assets/UI/hand-click.png b/DispenserUI/Assets/UI/hand-click.png new file mode 100644 index 0000000..a94568b Binary files /dev/null and b/DispenserUI/Assets/UI/hand-click.png differ diff --git a/DispenserUI/Assets/UI/hand-click@2x.png b/DispenserUI/Assets/UI/hand-click@2x.png new file mode 100644 index 0000000..ae26d2e Binary files /dev/null and b/DispenserUI/Assets/UI/hand-click@2x.png differ diff --git a/DispenserUI/Assets/UI/left.png b/DispenserUI/Assets/UI/left.png new file mode 100644 index 0000000..b20b1c3 Binary files /dev/null and b/DispenserUI/Assets/UI/left.png differ diff --git a/DispenserUI/Assets/UI/left_orange.png b/DispenserUI/Assets/UI/left_orange.png new file mode 100644 index 0000000..db740bf Binary files /dev/null and b/DispenserUI/Assets/UI/left_orange.png differ diff --git a/DispenserUI/Assets/UI/life-buoy.png b/DispenserUI/Assets/UI/life-buoy.png new file mode 100644 index 0000000..022a14e Binary files /dev/null and b/DispenserUI/Assets/UI/life-buoy.png differ diff --git a/DispenserUI/Assets/UI/life-buoy@2x.png b/DispenserUI/Assets/UI/life-buoy@2x.png new file mode 100644 index 0000000..3ad828b Binary files /dev/null and b/DispenserUI/Assets/UI/life-buoy@2x.png differ diff --git a/DispenserUI/Assets/UI/loader.png b/DispenserUI/Assets/UI/loader.png new file mode 100644 index 0000000..c79a9cd Binary files /dev/null and b/DispenserUI/Assets/UI/loader.png differ diff --git a/DispenserUI/Assets/UI/loader@2x.png b/DispenserUI/Assets/UI/loader@2x.png new file mode 100644 index 0000000..0d2acbe Binary files /dev/null and b/DispenserUI/Assets/UI/loader@2x.png differ diff --git a/DispenserUI/Assets/UI/locate_red.png b/DispenserUI/Assets/UI/locate_red.png new file mode 100644 index 0000000..5024eb2 Binary files /dev/null and b/DispenserUI/Assets/UI/locate_red.png differ diff --git a/DispenserUI/Assets/UI/log.png b/DispenserUI/Assets/UI/log.png new file mode 100644 index 0000000..b8f72e9 Binary files /dev/null and b/DispenserUI/Assets/UI/log.png differ diff --git a/DispenserUI/Assets/UI/logo.png b/DispenserUI/Assets/UI/logo.png new file mode 100644 index 0000000..750b671 Binary files /dev/null and b/DispenserUI/Assets/UI/logo.png differ diff --git a/DispenserUI/Assets/UI/logo@2x.png b/DispenserUI/Assets/UI/logo@2x.png new file mode 100644 index 0000000..b75e9df Binary files /dev/null and b/DispenserUI/Assets/UI/logo@2x.png differ diff --git a/DispenserUI/Assets/UI/mail.png b/DispenserUI/Assets/UI/mail.png new file mode 100644 index 0000000..78b8326 Binary files /dev/null and b/DispenserUI/Assets/UI/mail.png differ diff --git a/DispenserUI/Assets/UI/mail@2x.png b/DispenserUI/Assets/UI/mail@2x.png new file mode 100644 index 0000000..273159b Binary files /dev/null and b/DispenserUI/Assets/UI/mail@2x.png differ diff --git a/DispenserUI/Assets/UI/map-pin-search.png b/DispenserUI/Assets/UI/map-pin-search.png new file mode 100644 index 0000000..69deb38 Binary files /dev/null and b/DispenserUI/Assets/UI/map-pin-search.png differ diff --git a/DispenserUI/Assets/UI/map-pin-search@2x.png b/DispenserUI/Assets/UI/map-pin-search@2x.png new file mode 100644 index 0000000..461563f Binary files /dev/null and b/DispenserUI/Assets/UI/map-pin-search@2x.png differ diff --git a/DispenserUI/Assets/UI/motor.png b/DispenserUI/Assets/UI/motor.png new file mode 100644 index 0000000..ae201b3 Binary files /dev/null and b/DispenserUI/Assets/UI/motor.png differ diff --git a/DispenserUI/Assets/UI/narrow_white.png b/DispenserUI/Assets/UI/narrow_white.png new file mode 100644 index 0000000..9a7d20e Binary files /dev/null and b/DispenserUI/Assets/UI/narrow_white.png differ diff --git a/DispenserUI/Assets/UI/ok.png b/DispenserUI/Assets/UI/ok.png new file mode 100644 index 0000000..bd72fa8 Binary files /dev/null and b/DispenserUI/Assets/UI/ok.png differ diff --git a/DispenserUI/Assets/UI/pause.png b/DispenserUI/Assets/UI/pause.png new file mode 100644 index 0000000..6fe6c55 Binary files /dev/null and b/DispenserUI/Assets/UI/pause.png differ diff --git a/DispenserUI/Assets/UI/pause@2x.png b/DispenserUI/Assets/UI/pause@2x.png new file mode 100644 index 0000000..4ce7fcf Binary files /dev/null and b/DispenserUI/Assets/UI/pause@2x.png differ diff --git a/DispenserUI/Assets/UI/pcb.png b/DispenserUI/Assets/UI/pcb.png new file mode 100644 index 0000000..c4dba19 Binary files /dev/null and b/DispenserUI/Assets/UI/pcb.png differ diff --git a/DispenserUI/Assets/UI/play.png b/DispenserUI/Assets/UI/play.png new file mode 100644 index 0000000..fc83209 Binary files /dev/null and b/DispenserUI/Assets/UI/play.png differ diff --git a/DispenserUI/Assets/UI/play@2x.png b/DispenserUI/Assets/UI/play@2x.png new file mode 100644 index 0000000..a3af97d Binary files /dev/null and b/DispenserUI/Assets/UI/play@2x.png differ diff --git a/DispenserUI/Assets/UI/plus-circle.png b/DispenserUI/Assets/UI/plus-circle.png new file mode 100644 index 0000000..b3a494a Binary files /dev/null and b/DispenserUI/Assets/UI/plus-circle.png differ diff --git a/DispenserUI/Assets/UI/plus-circle@2x.png b/DispenserUI/Assets/UI/plus-circle@2x.png new file mode 100644 index 0000000..07cb523 Binary files /dev/null and b/DispenserUI/Assets/UI/plus-circle@2x.png differ diff --git a/DispenserUI/Assets/UI/question.png b/DispenserUI/Assets/UI/question.png new file mode 100644 index 0000000..834c7a1 Binary files /dev/null and b/DispenserUI/Assets/UI/question.png differ diff --git a/DispenserUI/Assets/UI/question_white.png b/DispenserUI/Assets/UI/question_white.png new file mode 100644 index 0000000..28b1f58 Binary files /dev/null and b/DispenserUI/Assets/UI/question_white.png differ diff --git a/DispenserUI/Assets/UI/recover_to_origin_white.png b/DispenserUI/Assets/UI/recover_to_origin_white.png new file mode 100644 index 0000000..47d5613 Binary files /dev/null and b/DispenserUI/Assets/UI/recover_to_origin_white.png differ diff --git a/DispenserUI/Assets/UI/refresh.png b/DispenserUI/Assets/UI/refresh.png new file mode 100644 index 0000000..5f79074 Binary files /dev/null and b/DispenserUI/Assets/UI/refresh.png differ diff --git a/DispenserUI/Assets/UI/refresh@2x.png b/DispenserUI/Assets/UI/refresh@2x.png new file mode 100644 index 0000000..23c4e3c Binary files /dev/null and b/DispenserUI/Assets/UI/refresh@2x.png differ diff --git a/DispenserUI/Assets/UI/resize_img_white.png b/DispenserUI/Assets/UI/resize_img_white.png new file mode 100644 index 0000000..4f20bc2 Binary files /dev/null and b/DispenserUI/Assets/UI/resize_img_white.png differ diff --git a/DispenserUI/Assets/UI/resize_white.png b/DispenserUI/Assets/UI/resize_white.png new file mode 100644 index 0000000..be278b0 Binary files /dev/null and b/DispenserUI/Assets/UI/resize_white.png differ diff --git a/DispenserUI/Assets/UI/right.png b/DispenserUI/Assets/UI/right.png new file mode 100644 index 0000000..9562eca Binary files /dev/null and b/DispenserUI/Assets/UI/right.png differ diff --git a/DispenserUI/Assets/UI/right_orange.png b/DispenserUI/Assets/UI/right_orange.png new file mode 100644 index 0000000..8e112ef Binary files /dev/null and b/DispenserUI/Assets/UI/right_orange.png differ diff --git a/DispenserUI/Assets/UI/route.png b/DispenserUI/Assets/UI/route.png new file mode 100644 index 0000000..a31b572 Binary files /dev/null and b/DispenserUI/Assets/UI/route.png differ diff --git a/DispenserUI/Assets/UI/route@2x.png b/DispenserUI/Assets/UI/route@2x.png new file mode 100644 index 0000000..6685922 Binary files /dev/null and b/DispenserUI/Assets/UI/route@2x.png differ diff --git a/DispenserUI/Assets/UI/scanner.png b/DispenserUI/Assets/UI/scanner.png new file mode 100644 index 0000000..b16ca6e Binary files /dev/null and b/DispenserUI/Assets/UI/scanner.png differ diff --git a/DispenserUI/Assets/UI/settings.png b/DispenserUI/Assets/UI/settings.png new file mode 100644 index 0000000..ff59e88 Binary files /dev/null and b/DispenserUI/Assets/UI/settings.png differ diff --git a/DispenserUI/Assets/UI/settings_white.png b/DispenserUI/Assets/UI/settings_white.png new file mode 100644 index 0000000..f3034e3 Binary files /dev/null and b/DispenserUI/Assets/UI/settings_white.png differ diff --git a/DispenserUI/Assets/UI/smallAI.png b/DispenserUI/Assets/UI/smallAI.png new file mode 100644 index 0000000..c3200ce Binary files /dev/null and b/DispenserUI/Assets/UI/smallAI.png differ diff --git a/DispenserUI/Assets/UI/smallAI@2x.png b/DispenserUI/Assets/UI/smallAI@2x.png new file mode 100644 index 0000000..b5f6c8d Binary files /dev/null and b/DispenserUI/Assets/UI/smallAI@2x.png differ diff --git a/DispenserUI/Assets/UI/stop-big.png b/DispenserUI/Assets/UI/stop-big.png new file mode 100644 index 0000000..037abbe Binary files /dev/null and b/DispenserUI/Assets/UI/stop-big.png differ diff --git a/DispenserUI/Assets/UI/stop.png b/DispenserUI/Assets/UI/stop.png new file mode 100644 index 0000000..905ce7d Binary files /dev/null and b/DispenserUI/Assets/UI/stop.png differ diff --git a/DispenserUI/Assets/UI/stretch_uniform.png b/DispenserUI/Assets/UI/stretch_uniform.png new file mode 100644 index 0000000..274214c Binary files /dev/null and b/DispenserUI/Assets/UI/stretch_uniform.png differ diff --git a/DispenserUI/Assets/UI/stretch_uniform_fill.png b/DispenserUI/Assets/UI/stretch_uniform_fill.png new file mode 100644 index 0000000..7554341 Binary files /dev/null and b/DispenserUI/Assets/UI/stretch_uniform_fill.png differ diff --git a/DispenserUI/Assets/UI/substrate.png b/DispenserUI/Assets/UI/substrate.png new file mode 100644 index 0000000..2bbb1a0 Binary files /dev/null and b/DispenserUI/Assets/UI/substrate.png differ diff --git a/DispenserUI/Assets/UI/success.png b/DispenserUI/Assets/UI/success.png new file mode 100644 index 0000000..3f685d6 Binary files /dev/null and b/DispenserUI/Assets/UI/success.png differ diff --git a/DispenserUI/Assets/UI/success_big.png b/DispenserUI/Assets/UI/success_big.png new file mode 100644 index 0000000..3197b3e Binary files /dev/null and b/DispenserUI/Assets/UI/success_big.png differ diff --git a/DispenserUI/Assets/UI/switch_off.png b/DispenserUI/Assets/UI/switch_off.png new file mode 100644 index 0000000..ac315da Binary files /dev/null and b/DispenserUI/Assets/UI/switch_off.png differ diff --git a/DispenserUI/Assets/UI/switch_on.png b/DispenserUI/Assets/UI/switch_on.png new file mode 100644 index 0000000..22cf752 Binary files /dev/null and b/DispenserUI/Assets/UI/switch_on.png differ diff --git a/DispenserUI/Assets/UI/toggle-left.png b/DispenserUI/Assets/UI/toggle-left.png new file mode 100644 index 0000000..8d6cdf4 Binary files /dev/null and b/DispenserUI/Assets/UI/toggle-left.png differ diff --git a/DispenserUI/Assets/UI/toggle-left@2x.png b/DispenserUI/Assets/UI/toggle-left@2x.png new file mode 100644 index 0000000..441bf60 Binary files /dev/null and b/DispenserUI/Assets/UI/toggle-left@2x.png differ diff --git a/DispenserUI/Assets/UI/top_orange.png b/DispenserUI/Assets/UI/top_orange.png new file mode 100644 index 0000000..c39c3eb Binary files /dev/null and b/DispenserUI/Assets/UI/top_orange.png differ diff --git a/DispenserUI/Assets/UI/trash-x.png b/DispenserUI/Assets/UI/trash-x.png new file mode 100644 index 0000000..5c1e8ee Binary files /dev/null and b/DispenserUI/Assets/UI/trash-x.png differ diff --git a/DispenserUI/Assets/UI/trash-x@2x.png b/DispenserUI/Assets/UI/trash-x@2x.png new file mode 100644 index 0000000..48bdd2c Binary files /dev/null and b/DispenserUI/Assets/UI/trash-x@2x.png differ diff --git a/DispenserUI/Assets/UI/up.png b/DispenserUI/Assets/UI/up.png new file mode 100644 index 0000000..49295e8 Binary files /dev/null and b/DispenserUI/Assets/UI/up.png differ diff --git a/DispenserUI/Assets/UI/user-plus.png b/DispenserUI/Assets/UI/user-plus.png new file mode 100644 index 0000000..42b228a Binary files /dev/null and b/DispenserUI/Assets/UI/user-plus.png differ diff --git a/DispenserUI/Assets/UI/user-plus@2x.png b/DispenserUI/Assets/UI/user-plus@2x.png new file mode 100644 index 0000000..9ac721b Binary files /dev/null and b/DispenserUI/Assets/UI/user-plus@2x.png differ diff --git a/DispenserUI/Assets/UI/video.png b/DispenserUI/Assets/UI/video.png new file mode 100644 index 0000000..bd07edf Binary files /dev/null and b/DispenserUI/Assets/UI/video.png differ diff --git a/DispenserUI/Assets/UI/video_red.png b/DispenserUI/Assets/UI/video_red.png new file mode 100644 index 0000000..b015ba0 Binary files /dev/null and b/DispenserUI/Assets/UI/video_red.png differ diff --git a/DispenserUI/Assets/UI/video_white.png b/DispenserUI/Assets/UI/video_white.png new file mode 100644 index 0000000..b4d921b Binary files /dev/null and b/DispenserUI/Assets/UI/video_white.png differ diff --git a/DispenserUI/Assets/UI/wafer.png b/DispenserUI/Assets/UI/wafer.png new file mode 100644 index 0000000..ea2b86b Binary files /dev/null and b/DispenserUI/Assets/UI/wafer.png differ diff --git a/DispenserUI/Assets/UI/zoom_in_white.png b/DispenserUI/Assets/UI/zoom_in_white.png new file mode 100644 index 0000000..7558ba7 Binary files /dev/null and b/DispenserUI/Assets/UI/zoom_in_white.png differ diff --git a/DispenserUI/Assets/UI/zoom_out_white.png b/DispenserUI/Assets/UI/zoom_out_white.png new file mode 100644 index 0000000..6c9a31a Binary files /dev/null and b/DispenserUI/Assets/UI/zoom_out_white.png differ diff --git a/DispenserUI/Assets/capturing.gif b/DispenserUI/Assets/capturing.gif new file mode 100644 index 0000000..b08613d Binary files /dev/null and b/DispenserUI/Assets/capturing.gif differ diff --git a/DispenserUI/Assets/error.png b/DispenserUI/Assets/error.png new file mode 100644 index 0000000..ea48fad Binary files /dev/null and b/DispenserUI/Assets/error.png differ diff --git a/DispenserUI/Assets/favicon.ico b/DispenserUI/Assets/favicon.ico new file mode 100644 index 0000000..676868d Binary files /dev/null and b/DispenserUI/Assets/favicon.ico differ diff --git a/DispenserUI/Assets/info.png b/DispenserUI/Assets/info.png new file mode 100644 index 0000000..50e1b16 Binary files /dev/null and b/DispenserUI/Assets/info.png differ diff --git a/DispenserUI/Assets/loading.gif b/DispenserUI/Assets/loading.gif new file mode 100644 index 0000000..7b714bc Binary files /dev/null and b/DispenserUI/Assets/loading.gif differ diff --git a/DispenserUI/Assets/loading_b.gif b/DispenserUI/Assets/loading_b.gif new file mode 100644 index 0000000..dc29600 Binary files /dev/null and b/DispenserUI/Assets/loading_b.gif differ diff --git a/DispenserUI/Assets/notify.png b/DispenserUI/Assets/notify.png new file mode 100644 index 0000000..8d5a62d Binary files /dev/null and b/DispenserUI/Assets/notify.png differ diff --git a/DispenserUI/Assets/splash.png b/DispenserUI/Assets/splash.png new file mode 100644 index 0000000..f2ee4dc Binary files /dev/null and b/DispenserUI/Assets/splash.png differ diff --git a/DispenserUI/Assets/warning.png b/DispenserUI/Assets/warning.png new file mode 100644 index 0000000..d186663 Binary files /dev/null and b/DispenserUI/Assets/warning.png differ diff --git a/DispenserUI/DispenserUI.csproj b/DispenserUI/DispenserUI.csproj new file mode 100644 index 0000000..91ae3c6 --- /dev/null +++ b/DispenserUI/DispenserUI.csproj @@ -0,0 +1,102 @@ + + + net7.0 + enable + latest + true + true + + + preview + + + preview + + + + + + + + + + + + SplashWindow.axaml + Code + + + DeviceStatusView.axaml + Code + + + LoginView.axaml + Code + + + SettingsView.axaml + Code + + + MainView.axaml + Code + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PreserveNewest + + + + + + + + + + + + + ..\DispenserDesktop\Libs\halcondotnet.dll + + + ..\DispenserDesktop\Libs\DispenserAlgorithms.Halcon.dll + + + C:\Windows\Microsoft.NET\assembly\GAC_MSIL\System.Windows.Forms\v4.0_4.0.0.0__b77a5c561934e089\System.Windows.Forms.dll + + + + diff --git a/DispenserUI/Models/DTO/TreeNode.cs b/DispenserUI/Models/DTO/TreeNode.cs new file mode 100644 index 0000000..d32dac7 --- /dev/null +++ b/DispenserUI/Models/DTO/TreeNode.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Runtime.CompilerServices; + +namespace DispenserUI.Models.DTO; + +/// +/// 用于显示树状菜单 +/// +public class TreeNode(string title = "", Type? viewModel = null) : INotifyPropertyChanged +{ + private ObservableCollection _children = []; + + public string Title => title; + + public ObservableCollection Children + { + get => _children; + set + { + _children = value; + OnPropertyChanged(); + } + } + + public Type? ViewModel => viewModel; + + public event PropertyChangedEventHandler? PropertyChanged; + + protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } +} \ No newline at end of file diff --git a/DispenserUI/Models/VO/ConfigGroupVO.cs b/DispenserUI/Models/VO/ConfigGroupVO.cs new file mode 100644 index 0000000..e1503c3 --- /dev/null +++ b/DispenserUI/Models/VO/ConfigGroupVO.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace DispenserUI.Models.VO; + +public class ConfigGroupVO +{ + public string GroupCode { get; set; } + public string GroupName { get; set; } + + public bool Enable { get; set; } = true; + + public List Definitions { get; set; } +} \ No newline at end of file diff --git a/DispenserUI/Models/VO/ConfigMenuVO.cs b/DispenserUI/Models/VO/ConfigMenuVO.cs new file mode 100644 index 0000000..7597b01 --- /dev/null +++ b/DispenserUI/Models/VO/ConfigMenuVO.cs @@ -0,0 +1,8 @@ +namespace DispenserUI.Models.VO; + +public class ConfigMenuVO +{ + public string Name { get; set; } + + public string Code { get; set; } +} \ No newline at end of file diff --git a/DispenserUI/Models/VO/DefinitionVO.cs b/DispenserUI/Models/VO/DefinitionVO.cs new file mode 100644 index 0000000..9c3493a --- /dev/null +++ b/DispenserUI/Models/VO/DefinitionVO.cs @@ -0,0 +1,55 @@ +using System.ComponentModel; + +namespace DispenserUI.Models.VO; + +public class DefinitionVO : INotifyPropertyChanged +{ + public string _validateDesc; + + public string _value; + public string Name { get; set; } + public string Code { get; set; } + public string InputType { get; set; } + public bool Nullable { get; set; } + public bool Enable { get; set; } + public bool Writable { get; set; } + + public string ValidateDesc + { + get => _validateDesc; + set + { + if (_validateDesc != value) + { + _validateDesc = value; + OnPropertyChanged(nameof(ValidateDesc)); + } + } + } + + public string DefaultValue { get; set; } + + public string Value + { + get => _value; + set + { + if (_value != value) + { + _value = value; + OnPropertyChanged(nameof(Value)); + } + } + } + + public int MaxLength { get; set; } + public decimal MinValue { get; set; } + public decimal MaxValue { get; set; } + + public event PropertyChangedEventHandler PropertyChanged; + + protected virtual void OnPropertyChanged(string propertyName) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } +} \ No newline at end of file diff --git a/DispenserUI/Models/VO/ValidateResultVO.cs b/DispenserUI/Models/VO/ValidateResultVO.cs new file mode 100644 index 0000000..7f55f78 --- /dev/null +++ b/DispenserUI/Models/VO/ValidateResultVO.cs @@ -0,0 +1,7 @@ +namespace DispenserUI.Models.VO; + +public class ValidateResultVO +{ + public bool Valid { get; set; } + public string Message { get; set; } +} \ No newline at end of file diff --git a/DispenserUI/Styles/DataValidationErrors.axaml b/DispenserUI/Styles/DataValidationErrors.axaml new file mode 100644 index 0000000..3de324d --- /dev/null +++ b/DispenserUI/Styles/DataValidationErrors.axaml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/DispenserUI/Utils/ConfirmDialog.cs b/DispenserUI/Utils/ConfirmDialog.cs new file mode 100644 index 0000000..629f523 --- /dev/null +++ b/DispenserUI/Utils/ConfirmDialog.cs @@ -0,0 +1,49 @@ +using System.Threading.Tasks; +using DispenserCommon.Aop; +using DispenserCommon.Ioc; +using DispenserCommon.Utils; +using DispenserUI.Views.Windows; + +namespace DispenserUI.Utils; + +[Component(typeof(ConfirmDialogHelper)), GlobalTry] +public class ConfirmDialog : ConfirmDialogHelper +{ + /// + /// 显示弹出并等待用户确认 + /// + /// + /// + /// + /// + /// + /// + public static async Task ToConfirm(string title, bool showCancel = true, bool showConfirm = true, + string cancelText = "取消", string confirmText = "确认") + { + var confirmDialogWindow = new ConfirmDialogWindow + { + Content = title, + ShowCancel = showCancel, + ShowConfirm = showConfirm, + CancelText = cancelText, + ConfirmText = confirmText + }; + return await WindowUtil.ShowDialog(confirmDialogWindow); + } + + /// + /// 现实确认弹窗 + /// + /// + /// + /// + /// + /// + /// + public Task ShowConfirm(string title, bool showCancel = true, bool showConfirm = true, + string cancelText = "取消", string confirmText = "确认") + { + return ToConfirm(title, showCancel, showConfirm, cancelText, confirmText); + } +} \ No newline at end of file diff --git a/DispenserUI/Utils/CustomExceptionValidationPlugin.cs b/DispenserUI/Utils/CustomExceptionValidationPlugin.cs new file mode 100644 index 0000000..32a44be --- /dev/null +++ b/DispenserUI/Utils/CustomExceptionValidationPlugin.cs @@ -0,0 +1,43 @@ +using System; +using System.Reflection; +using Avalonia.Data; +using Avalonia.Data.Core.Plugins; +using Serilog; + +namespace DispenserUI.Utils; + +public class CustomExceptionValidationPlugin : IDataValidationPlugin +{ + public bool Match(WeakReference reference, string memberName) => true; + + public IPropertyAccessor Start(WeakReference reference, string name, IPropertyAccessor inner) + { + return new Validator(reference, name, inner); + } + + private sealed class Validator : DataValidationBase + { + public Validator(WeakReference reference, string name, IPropertyAccessor inner) + : base(inner) + { + } + + public override bool SetValue(object? value, BindingPriority priority) + { + try + { + return base.SetValue(value, priority); + } + catch (TargetInvocationException ex) when (ex.InnerException is not null) + { + Log.Error(ex.InnerException, "TargetInvocationException Exception"); + } + catch (Exception ex) + { + Log.Error(ex, "DataValidation Exception"); + } + + return false; + } + } +} \ No newline at end of file diff --git a/DispenserUI/Utils/ImageHelper.cs b/DispenserUI/Utils/ImageHelper.cs new file mode 100644 index 0000000..9576d67 --- /dev/null +++ b/DispenserUI/Utils/ImageHelper.cs @@ -0,0 +1,179 @@ +using System; +using System.Drawing; +using System.Drawing.Imaging; +using System.IO; +using System.Net.Http; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Media.Imaging; +using Avalonia.Platform; +using Bitmap = System.Drawing.Bitmap; +using PixelFormat = Avalonia.Platform.PixelFormat; + +namespace DispenserUI.Utils; + +#pragma warning disable CA1416 +public static class ImageHelper +{ + private static readonly ImageCodecInfo JpgEncoder = GetEncoderInfo("image/jpeg"); + private static readonly ImageCodecInfo PngEncoder = GetEncoderInfo("image/png"); + private static readonly Encoder Encoder = Encoder.Quality; + private static readonly EncoderParameter[] parameterList = new EncoderParameter[101]; + + /// + /// 加载指定路径的资源,转换成Bitmap格式 + /// + /// + /// + public static Bitmap LoadFromResource(Uri resourceUri) + { + return new Bitmap(AssetLoader.Open(resourceUri)); + } + + /// + /// 从网络上加载资源 + /// + /// + /// + public static async Task LoadFromWeb(Uri url) + { + using var httpClient = new HttpClient(); + try + { + var response = await httpClient.GetAsync(url); + response.EnsureSuccessStatusCode(); + var data = await response.Content.ReadAsByteArrayAsync(); + return new Bitmap(new MemoryStream(data)); + } + catch (HttpRequestException ex) + { + Console.WriteLine($"加载图片失败 '{url}' : {ex.Message}"); + return null; + } + } + + public static Avalonia.Media.Imaging.Bitmap ToJpg(Image image) + { + // 将System.Drawing.Image保存到内存流中 + using var memoryStream = new MemoryStream(); + image.Save(memoryStream, ImageFormat.Jpeg); // 保存为PNG格式 + memoryStream.Position = 0; // 重置流位置 + + // 从内存流创建Avalonia的Bitmap + return new Avalonia.Media.Imaging.Bitmap(memoryStream); + } + + /// + /// 将 bmp 转为 jpg + /// + /// + /// + /// + public static Bitmap ConvertBmpToJpg(Image bmp, int quality) + { + // 创建一个新的Bitmap对象,复制原始BMP图像的内容 + using var bmpImage = new Bitmap(bmp); + var encoderParams = new EncoderParameters(1); + var encoderParam = new EncoderParameter(Encoder.Quality, quality); + encoderParams.Param[0] = encoderParam; + + // 将BMP转化成JPG + var ms = new MemoryStream(); + bmpImage.Save(ms, JpgEncoder, encoderParams); + + // 重新从内存加载已转换的JPG图像,返回Bitmap对象 + var convertedImage = new Bitmap(ms); + + // 清理资源 + ms.Close(); + ms.Dispose(); + return convertedImage; + } + + + //获取图像编解码器 + private static ImageCodecInfo GetEncoderInfo(string type) + { + int j; + ImageCodecInfo[] encoders; + encoders = ImageCodecInfo.GetImageEncoders(); + for (j = 0; j < encoders.Length; ++j) + if (encoders[j].MimeType == type) + return encoders[j]; + + return null; + } + + //该方法根据参数返回包含指定画质的编码信息,value的范围是: [0,100] + private static EncoderParameter GetParameter(long value) + { + var v = (int)value; + //为了提高性能,可以将使用过的编码信息保存起来,仅当数组中没有时才重新获取 + if (parameterList[v] == null) parameterList[v] = new EncoderParameter(Encoder, value); + + return parameterList[v]; + } + + public static Avalonia.Media.Imaging.Bitmap LoadBitmap(string path) + { + if (path.StartsWith("/") || path.StartsWith("\\")) path = path.Substring(1); + using var memoryStream = new MemoryStream(); + var bitmap = new Bitmap(path); + bitmap.Save(memoryStream, ImageFormat.Png); + + memoryStream.Seek(0, SeekOrigin.Begin); + + return new Avalonia.Media.Imaging.Bitmap(memoryStream); + } + + public static Avalonia.Media.Imaging.Bitmap LoadPng(string path) + { + if (path.StartsWith("/") || path.StartsWith("\\")) path = path.Substring(1); + var bitmap = new Bitmap(path); + + // 创建一个内存流存储 System.Drawing.Bitmap 的数据 + using (var memoryStream = new MemoryStream()) + { + // 将 System.Drawing.Bitmap 保存到内存流中 + bitmap.Save(memoryStream, ImageFormat.Png); + + // 重置流的位置到开始 + memoryStream.Position = 0; + + // 使用内存流中的数据创建 Avalonia Bitmap + return new Avalonia.Media.Imaging.Bitmap(memoryStream); + } + } + + [DllImport("kernel32.dll", EntryPoint = "RtlMoveMemory")] + private static extern void CopyMemory(IntPtr dest, IntPtr src, uint count); + + /// + /// 将System.Drawing.Bitmap转换为Avalonia.Media.Imaging.Bitmap + /// + /// + /// + public static Avalonia.Media.Imaging.Bitmap ConvertToAvaloniaBitmap(Bitmap source) + { + var width = source.Width; + var height = source.Height; + + var image = new WriteableBitmap(new PixelSize(4096, 3000), new Vector(96, 96), + PixelFormat.Bgra8888, AlphaFormat.Premul); + + var bitmapData = source.LockBits(new Rectangle(0, 0, width, height), + ImageLockMode.ReadOnly, System.Drawing.Imaging.PixelFormat.Format32bppArgb); + + using (var frameBuffer = image.Lock()) + { + var sourcePtr = bitmapData.Scan0; + var destPtr = frameBuffer.Address; + var count = (uint)(bitmapData.Stride * source.Height); + CopyMemory(destPtr, sourcePtr, count); + } + + source.UnlockBits(bitmapData); + return image; + } +} \ No newline at end of file diff --git a/DispenserUI/Utils/ScreenRecorder.cs b/DispenserUI/Utils/ScreenRecorder.cs new file mode 100644 index 0000000..3fe6b8d --- /dev/null +++ b/DispenserUI/Utils/ScreenRecorder.cs @@ -0,0 +1,183 @@ +using System; +using System.Diagnostics; +using System.Drawing; +using System.Drawing.Imaging; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using SharpAvi; +using SharpAvi.Codecs; +using SharpAvi.Output; + +namespace DispenserUI.ViewModels.Recorder; + +public class ScreenRecorder +{ + private static readonly FourCC MJPEG_IMAGE_SHARP = "IMG#"; + + private readonly AviWriter _writer; + private IAviVideoStream _videoStream; + private Thread _screenThread; + private readonly ManualResetEvent _stopThread = new(false); + private readonly AutoResetEvent _videoFrameWritten = new(false); + + private readonly FourCC _codec; + private readonly int _quality; + + private readonly string _fileName; + public int Width { get; set; } + public int Height { get; set; } + public int Left { get; set; } + public int Top { get; set; } + + public bool IsRecording { get; set; } + + public ScreenRecorder(string fileName, FourCC codec, int fps, int quality) + { + _codec = codec; + _quality = quality; + _fileName = fileName; + + // Create AVI writer and specify FPS + _writer = new AviWriter(fileName) + { + FramesPerSecond = fps, + EmitIndex1 = true, + }; + } + + public void Start() + { + // Create video stream + _videoStream = CreateVideoStream(_codec, _quality); + // Set only name. Other properties were when creating stream, + // either explicitly by arguments or implicitly by the encoder used + _videoStream.Name = "Screencast"; + + _screenThread = new Thread(RecordScreen) + { + Name = nameof(ScreenRecorder) + ".RecordScreen", + IsBackground = true + }; + IsRecording = true; + _screenThread.Start(); + } + + private IAviVideoStream CreateVideoStream(FourCC codec, int quality) + { + // Select encoder type based on FOURCC of codec + if (codec == CodecIds.Uncompressed) + { + return _writer.AddUncompressedVideoStream(Width, Height); + } + + if (codec == MJPEG_IMAGE_SHARP) + { + // Use M-JPEG based on the SixLabors.ImageSharp package (cross-platform) + // Included in the SharpAvi.ImageSharp package + return AddMJpegImageSharpVideoStream(_writer, Width, Height, quality); + } + + return _writer.AddMpeg4VcmVideoStream(Width, Height, (double)_writer.FramesPerSecond, + // It seems that all tested MPEG-4 VfW codecs ignore the quality affecting parameters passed through VfW API + // They only respect the settings from their own configuration dialogs, and Mpeg4VideoEncoder currently has no support for this + quality: quality, + codec: codec, + // Most of VfW codecs expect single-threaded use, so we wrap this encoder to special wrapper + // Thus all calls to the encoder (including its instantiation) will be invoked on a single thread although encoding (and writing) is performed asynchronously + forceSingleThreadedAccess: true); + } + + private static IAviVideoStream AddMJpegImageSharpVideoStream(AviWriter writer, + int width, + int height, + int quality = 70) + { + var encoder = new MJpegImageVideoEncoder(width, height, quality); + return writer.AddEncodingVideoStream(encoder, width: width, height: height); + } + + + public void Stop() + { + IsRecording = false; + _stopThread.Set(); + _screenThread.Join(); + + // Close writer: the remaining data is written to a file and file is closed + _writer.Close(); + _stopThread.Close(); + } + + private void RecordScreen() + { + var stopwatch = new Stopwatch(); + var buffer = new byte[Width * Height * 4]; + Task videoWriteTask = null; + var isFirstFrame = true; + + var shotsTaken = 0; + var timeTillNextFrame = TimeSpan.Zero; + stopwatch.Start(); + + while (!_stopThread.WaitOne(timeTillNextFrame)) + { + try + { + GetScreenshot(buffer); + } + catch (Exception _) + { + continue; + } + + shotsTaken++; + + // Wait for the previous frame is written + if (!isFirstFrame) + { + videoWriteTask.Wait(); + _videoFrameWritten.Set(); + } + + videoWriteTask = _videoStream.WriteFrameAsync(true, buffer.AsMemory(0, buffer.Length)); + + timeTillNextFrame = + TimeSpan.FromSeconds(shotsTaken / (double)_writer.FramesPerSecond - stopwatch.Elapsed.TotalSeconds); + + if (timeTillNextFrame < TimeSpan.Zero) + timeTillNextFrame = TimeSpan.Zero; + + isFirstFrame = false; + } + + stopwatch.Stop(); + + // Wait for the last frame is written + if (!isFirstFrame) + { + videoWriteTask.Wait(); + } + } + + private void GetScreenshot(byte[] buffer) + { + using (var bitmap = new Bitmap(Width, Height)) + { + using (var graphics = Graphics.FromImage(bitmap)) + { + graphics.CopyFromScreen(Left, Top, 0, 0, new Size(Width, Height)); + var bits = bitmap.LockBits(new Rectangle(0, 0, Width, Height), ImageLockMode.ReadOnly, + PixelFormat.Format32bppRgb); + Marshal.Copy(bits.Scan0, buffer, 0, buffer.Length); + bitmap.UnlockBits(bits); + } + } + } + + public string? GetDirectory() + { + return Path.GetDirectoryName(_fileName); + } +} \ No newline at end of file diff --git a/DispenserUI/ViewModels/Common/SystemStatusVM.cs b/DispenserUI/ViewModels/Common/SystemStatusVM.cs new file mode 100644 index 0000000..fd5e407 --- /dev/null +++ b/DispenserUI/ViewModels/Common/SystemStatusVM.cs @@ -0,0 +1,191 @@ +using System.Reactive; +using Avalonia.Media; +using Avalonia.Threading; +using DispenserCommon.Aop; +using DispenserCommon.Enums; +using DispenserCommon.Events; +using DispenserCommon.Exceptions; +using DispenserCommon.Ioc; +using DispenserHal.Camera.Factory; +using DispenserUI.Exceptions; +using DispenserUI.ViewModels.DTO; +using DispenserUI.Views.Common; +using DispenserUI.Views.Windows; +using ReactiveUI; + +namespace DispenserUI.ViewModels.Common; + +/// +/// 系统变量 +/// +[Component, GlobalTry] +public class SystemStatusVM : ViewModelBase +{ + private static readonly string[] MotionStatus = ["作业中", "待机中", "故障"]; + + private static readonly IBrush[] LightColors = + [ + new SolidColorBrush(Color.Parse("#31D05E")), new SolidColorBrush(Colors.DodgerBlue), + new SolidColorBrush(Colors.Red) + ]; + + private static readonly BoxShadows[] LightShadows = + [ + BoxShadows.Parse("0 0 20 5 LightGreen"), + BoxShadows.Parse("0 0 20 5 DodgerBlue"), + BoxShadows.Parse("0 0 20 5 IndianRed") + ]; + + private SystemStatusView View { get; set; } + + private SystemStatus _camera = new("视觉相机"); + + private string _lightState = MotionStatus[1]; + + private IBrush _lightColor = LightColors[1]; + + private BoxShadows _lightShadow = LightShadows[1]; + + private EmergencyWindow? _emergencyWindow; + + private CameraViewerWindow? _cameraViewerWindow; + + private int _lastErrorId; + + public void BindView(SystemStatusView view) + { + View = view; + } + + + public SystemStatus Camera + { + get => _camera; + set => this.RaiseAndSetIfChanged(ref _camera, value); + } + + public string LightState + { + get => _lightState; + set => this.RaiseAndSetIfChanged(ref _lightState, value); + } + + public IBrush LightColor + { + get => _lightColor; + set => this.RaiseAndSetIfChanged(ref _lightColor, value); + } + + public BoxShadows LightShadow + { + get => _lightShadow; + set => this.RaiseAndSetIfChanged(ref _lightShadow, value); + } + + public SystemStatusVM() + { + ToCameraViewer = ReactiveCommand.Create(OpenCameraViewer); + EventBus.AddEventHandler(EventType.Exception, ExceptionHandler); + } + + /// + /// 处理接收到的异常信息 + /// + /// + /// + private void ExceptionHandler(EventType eventType, BizException exception) + { + switch (exception) + { + case CameraException when exception.Level == ExceptionLevel.ERROR: + CameraFault(); + break; + } + + // 只记录告警以上级别的异常 + if (exception.Level <= ExceptionLevel.NORMAL) return; + } + + + public ReactiveCommand ToDisableAll { get; } + + public ReactiveCommand ToCameraViewer { get; } + + + public ReactiveCommand ToTest { get; } + + [Operation("开启相机预览")] + private void OpenCameraViewer() + { + if (CameraManager.GetCamera() == null) return; + if (_cameraViewerWindow == null) + { + _cameraViewerWindow = new CameraViewerWindow(); + _cameraViewerWindow.Show(); + _cameraViewerWindow.Closed += (_, _) => { _cameraViewerWindow = null; }; + } + else + { + _cameraViewerWindow.Activate(); + } + } + + private void CameraFault() + { + Camera.Background = "#EAD2D7"; + Camera.IsNormal = false; + Camera.Tip = "连接异常"; + } + + private void CameraNormal() + { + Camera.Background = "Transparent"; + Camera.IsNormal = true; + } + + + /// + /// 更新状态UI + /// + private void UpdateStateView(bool normal) + { + if (View == null) return; + + Dispatcher.UIThread.InvokeAsync(() => + { + if (!normal) return; + CameraNormal(); + }); + } + + + /// + /// 待机状态 + /// + private void NormalState() + { + LightState = MotionStatus[1]; + LightColor = LightColors[1]; + LightShadow = LightShadows[1]; + } + + /// + /// 作业中 + /// + private void WorkingState() + { + LightState = MotionStatus[0]; + LightColor = LightColors[0]; + LightShadow = LightShadows[0]; + } + + /// + /// 故障中 + /// + private void FaultState() + { + LightState = MotionStatus[2]; + LightColor = LightColors[2]; + LightShadow = LightShadows[2]; + } +} \ No newline at end of file diff --git a/DispenserUI/ViewModels/Components/CameraParamsVM.cs b/DispenserUI/ViewModels/Components/CameraParamsVM.cs new file mode 100644 index 0000000..4a7a254 --- /dev/null +++ b/DispenserUI/ViewModels/Components/CameraParamsVM.cs @@ -0,0 +1,114 @@ +using System; +using DispenserCommon.Aop; +using DispenserCommon.Ioc; +using DispenserHal.Camera.DTO; +using DispenserHal.Camera.Factory; +using DispenserUI.ViewModels.DTO; +using Newtonsoft.Json; +using ReactiveUI; + +namespace DispenserUI.ViewModels.Components; + +[Component, GlobalTry] +public class CameraParamsVM : ViewModelBase +{ + private CameraParams _params = new(); + + public CameraParams Params + { + get => _params; + set => this.RaiseAndSetIfChanged(ref _params, value); + } + + + /// + /// 重新刷新相机参数信息 + /// + public void ReloadCameraParams() + { + var camera = CameraManager.GetCamera()!; + + var properties = Params.GetType().GetProperties(); + + foreach (var property in properties) + { + var propertyType = property.PropertyType; + var propertyName = property.Name; + switch (propertyType.Name) + { + case nameof(StringValue): + property.SetValue(Params, camera.GetStringValue(propertyName)); + break; + case nameof(IntValue): + property.SetValue(Params, camera.GetIntValue(propertyName)); + break; + case nameof(Boolean): + property.SetValue(Params, camera.GetBoolValue(propertyName)); + break; + case nameof(FloatValue): + property.SetValue(Params, camera.GetFloatValue(propertyName)); + break; + case nameof(EnumValue): + property.SetValue(Params, camera.GetEnumValue(propertyName)); + // 如果是枚举类型,需要特殊处理,去获取每个枚举项的值 + if (property.GetValue(Params) is EnumValue enumValue) + { + for (uint i = 0; i < enumValue.SupportedNum; i++) + { + // 获取枚举项 + var item = camera.GetEnumEntrySymbolic(propertyName, enumValue.SupportValue[i]); + enumValue.Items.Add(item); + enumValue.Labels.Add(item.Symbolic); + } + + for (uint i = 0; i < enumValue.SupportedNum; i++) + { + if (enumValue.CurValue != enumValue.SupportValue[i]) continue; + enumValue.CurIndex = (int)i; + break; + } + } + + break; + } + } + + Console.WriteLine(JsonConvert.SerializeObject(Params)); + } + + + /// + /// 更新相机配置 + /// + /// + /// + [Operation("更新相机配置参数")] + public void UpdateCameraParams(string property, object value) + { + var camera = CameraManager.GetCamera()!; + + var properties = Params.GetType().GetProperties(); + + foreach (var propertyInfo in properties) + { + if (propertyInfo.Name != property) continue; + switch (propertyInfo.PropertyType.Name) + { + case nameof(IntValue): + camera.SetIntValue(property, (long)value); + break; + case nameof(Boolean): + camera.SetBoolValue(property, (bool)value); + break; + case nameof(FloatValue): + var @decimal = (float)Convert.ToDecimal(value); + camera.SetFloatValue(property, @decimal); + break; + case nameof(EnumValue): + var val = Convert.ToUInt32(value); + camera.SetEnumValue(property, val); + break; + } + } + } +} \ No newline at end of file diff --git a/DispenserUI/ViewModels/Components/CameraViewerVM.cs b/DispenserUI/ViewModels/Components/CameraViewerVM.cs new file mode 100644 index 0000000..aff077b --- /dev/null +++ b/DispenserUI/ViewModels/Components/CameraViewerVM.cs @@ -0,0 +1,586 @@ +using System; +using System.Drawing; +using System.Drawing.Imaging; +using System.IO; +using System.Reactive; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Threading; +using DispenserCommon.Aop; +using DispenserCommon.Events; +using DispenserCommon.Scheduler; +using DispenserCore.Service; +using DispenserHal.Camera.Factory; +using DispenserHal.Camera.VO; +using DispenserUI.Utils; +using DispenserUI.ViewModels.Recorder; +using DispenserUI.Views.Controls; +using DispenserUI.Views.Windows; +using Masuit.Tools; +using ReactiveUI; +using Serilog; +using SharpAvi; +using Bitmap = System.Drawing.Bitmap; +using Point = Avalonia.Point; + +namespace DispenserUI.ViewModels.Components; + +/// +/// 相机查看器 +/// +[GlobalTry] +public class CameraViewerVM : ViewModelBase +{ + private readonly SystemParamsService _systemParamsService = new(); + + + private const string TaskName = "CameraViewer#UpdateDisplay"; + + private bool _blank = true; + + private Bitmap _captureImage; + + private bool _capturingPicture; + + private bool _capturingVideo; + + private bool _recordingVideo; + + private bool _isFullScreen; + + private bool _showingCrosshair; + + private bool _showingGrid; + + private double _acquisitionFps; + + private int _imageCount; + + private int _displayImageCount; + + private string _bandwidth; + + private string _originSize; + + private string _pixelCoordinate; + + private Point _centerPixelPoint = new(2048, 1500); + + private PixelFormat _pixelFormat; + + private int _receivedPerSecond; + + private int _tuningStep = 1; + + private Stretch _stretch = Stretch.Uniform; + + private readonly Stretch[] _stretchs = + [ + Stretch.None, Stretch.Uniform, Stretch.UniformToFill + ]; + + private int _stretchIndex = 1; + + private string _scaleRatio = "100%"; + + private ImageViewer _imageViewer; + + private CameraViewerWindow _window; + + private ScreenRecorder? _recorder; + + private double _cameraPositionX; + + private double _cameraPositionY; + + public CameraViewerVM() + { + ToCapturePhoto = ReactiveCommand.CreateFromTask(CapturePhoto); + ToggleCaptureVideo = ReactiveCommand.Create(CaptureVideo); + ToToggleRecordingVideo = ReactiveCommand.Create(ToggleRecordingVideo); + ToggleShowCrosshair = ReactiveCommand.Create(ShowCrosshair); + ToggleShowGrid = ReactiveCommand.Create(ShowGrid); + ToChangeStretch = ReactiveCommand.Create(ChangeStretch); + ToSavePhoto = ReactiveCommand.Create(SavePhoto); + } + + + public bool Blank + { + get => _blank; + private set => this.RaiseAndSetIfChanged(ref _blank, value); + } + + public bool CapturingVideo + { + get => _capturingVideo; + set => this.RaiseAndSetIfChanged(ref _capturingVideo, value); + } + + public bool CapturingPicture + { + get => _capturingPicture; + set => this.RaiseAndSetIfChanged(ref _capturingPicture, value); + } + + public Bitmap CaptureImage + { + get => _captureImage; + set => this.RaiseAndSetIfChanged(ref _captureImage, value); + } + + public bool ShowingCrosshair + { + get => _showingCrosshair; + set => this.RaiseAndSetIfChanged(ref _showingCrosshair, value); + } + + public bool ShowingGrid + { + get => _showingGrid; + set => this.RaiseAndSetIfChanged(ref _showingGrid, value); + } + + public double AcquisitionFps + { + get => _acquisitionFps; + set => this.RaiseAndSetIfChanged(ref _acquisitionFps, value); + } + + public int ImageCount + { + get => _imageCount; + set => this.RaiseAndSetIfChanged(ref _imageCount, value); + } + + public int DisplayImageCount + { + get => _displayImageCount; + set => this.RaiseAndSetIfChanged(ref _displayImageCount, value); + } + + public string Bandwidth + { + get => _bandwidth; + set => this.RaiseAndSetIfChanged(ref _bandwidth, value); + } + + public string OriginSize + { + get => _originSize; + set => this.RaiseAndSetIfChanged(ref _originSize, value); + } + + public string PixelCoordinate + { + get => _pixelCoordinate; + set => this.RaiseAndSetIfChanged(ref _pixelCoordinate, value); + } + + public Point CenterPixelPoint + { + get => _centerPixelPoint; + set => this.RaiseAndSetIfChanged(ref _centerPixelPoint, value); + } + + public int TuningStep + { + get => _tuningStep; + set => this.RaiseAndSetIfChanged(ref _tuningStep, value); + } + + public bool IsFullScreen + { + get => _isFullScreen; + set => this.RaiseAndSetIfChanged(ref _isFullScreen, value); + } + + public Stretch Stretch + { + get => _stretch; + set => this.RaiseAndSetIfChanged(ref _stretch, value); + } + + public int StretchIndex + { + get => _stretchIndex; + set => this.RaiseAndSetIfChanged(ref _stretchIndex, value); + } + + public bool RecordingVideo + { + get => _recordingVideo; + set => this.RaiseAndSetIfChanged(ref _recordingVideo, value); + } + + public string ScaleRatio + { + get => _scaleRatio; + set => this.RaiseAndSetIfChanged(ref _scaleRatio, value); + } + + public double CameraPositionX + { + get => _cameraPositionX; + set => this.RaiseAndSetIfChanged(ref _cameraPositionX, value); + } + + public double CameraPositionY + { + get => _cameraPositionY; + set => this.RaiseAndSetIfChanged(ref _cameraPositionY, value); + } + + public ReactiveCommand ToCapturePhoto { get; } + + public ReactiveCommand ToSavePhoto { get; } + + public ReactiveCommand ToggleCaptureVideo { get; } + public ReactiveCommand ToToggleRecordingVideo { get; } + + public ReactiveCommand ToChangeStretch { get; } + + public ReactiveCommand ToggleShowCrosshair { get; } + + public ReactiveCommand ToggleShowGrid { get; } + + public ReactiveCommand ToMoveToWaferHeight { get; } + + public ReactiveCommand ToMoveToCrosshairCenter { get; } + + public ReactiveCommand ToMoveToSubstrateHeight { get; } + + public void InitViewer(ImageViewer imageViewer) + { + _imageViewer = imageViewer; + } + + public void InitWindow(CameraViewerWindow window) + { + _window = window; + } + + /// + /// 切换图片拉伸模式 + /// + private void ChangeStretch() + { + StretchIndex = ++StretchIndex % _stretchs.Length; + Stretch = _stretchs[StretchIndex]; + } + + /// + /// 保存截图 + /// + private void SavePhoto() + { + Task.Run(() => + { + try + { + var systemParams = _systemParamsService.GetSystemParams(); + var dir = systemParams.CameraViewerStoragePath ?? + Environment.GetFolderPath(Environment.SpecialFolder.MyPictures); + + var viewBox = _imageViewer.GetViewer(); + + var pixelSize = new PixelSize((int)viewBox.Bounds.Width, (int)viewBox.Bounds.Height); + var dpi = new Vector(96, 96); + var renderTargetBitmap = new RenderTargetBitmap(pixelSize, dpi); + var context = renderTargetBitmap.CreateDrawingContext(); + Dispatcher.UIThread.Invoke(() => { viewBox.Render(context); }); + + dir = Path.Combine(dir, "Dispenser", $"{DateTime.Now:yyyyMMdd}"); + + if (!Directory.Exists(dir)) + { + Directory.CreateDirectory(dir); + } + + using (var stream = File.OpenWrite(Path.Combine(dir, $"{DateTime.Now:yyyyMMddHHmmss}.bmp"))) + { + renderTargetBitmap.Save(stream); + } + + Dispatcher.UIThread.InvokeAsync(() => { new CameraViewerNotifyWindow("图片保存成功", dir).Show(); }); + } + catch (Exception e) + { + Log.Error(e, "保存图片失败"); + } + }); + } + + /// + /// 采集单张图片 + /// + [Operation("采集单张图片")] + private async Task CapturePhoto() + { + var camera = CameraManager.GetCamera()!; + if (CapturingVideo) + { + // 如果正在采集则提示停止 + var confirm = await ConfirmDialog.ToConfirm("正在采集中,是否停止采集?"); + if (!confirm) return; + // // 先停止采集 + camera.StopCaptureVideo(); + } + + // 采集单张图片 + camera.TokePhoto(); + + if (camera.CapturingVideo) + { + camera.StartCaptureVideo(); + } + + CapturingPicture = true; + ShowingCrosshair = false; + } + + /// + /// 连续采集 + /// + [Operation("开启/停止连续采集")] + private void CaptureVideo() + { + ImageCount = 0; + DisplayImageCount = 0; + _receivedPerSecond = 0; + + var camera = CameraManager.GetCamera()!; + if (CapturingVideo) + { + // 先停止采集 + camera.StopCaptureVideo(); + // 开启定时任务去更新显示信息 + JobScheduler.RemoveTask(TaskName); + } + else + { + // 开始连续采集 + camera.StartCaptureVideo(); + // 开启定时任务去更新显示信息 + JobScheduler.AddTask(TaskName, UpdateDisplayInfo, 1000); + } + + CapturingVideo = !CapturingVideo; + ShowingCrosshair = false; + } + + public void StopCaptureVideo() + { + CapturingVideo = false; + var camera = CameraManager.GetCamera()!; + // 先停止采集 + camera.StopCaptureVideo(); + // 开启定时任务去更新显示信息 + JobScheduler.RemoveTask(TaskName); + ShowingCrosshair = false; + } + + /// + /// 接受图像回调 + /// + /// + /// + private async Task CameraCallBack(EventType type, CameraData data) + { + await Task.Run(() => + { + if (!CapturingPicture && !CapturingVideo) return; + + var image = data.Image; + if (image.IsNullOrEmpty()) return; + + _pixelFormat = image.PixelFormat; + _receivedPerSecond++; + // 更新接收到的图片数 + ImageCount = CapturingVideo ? ImageCount + 1 : 1; + // 照片尺寸 + OriginSize = $"{image.Width}*{image.Height}"; + + // 更新展示的图片数 + DisplayImageCount++; + + Dispatcher.UIThread.Invoke(() => + { + if (Blank) Blank = false; + CaptureImage = image; + if (CapturingPicture) CapturingPicture = false; + }); + }); + } + +// 定时更新显示信息 + private void UpdateDisplayInfo() + { + if (OriginSize.IsNullOrEmpty()) return; + var pixelFormatSize = Image.GetPixelFormatSize(_pixelFormat) / 8; + var size = OriginSize.Split("*"); + + // 开启定时任务,每隔1秒更新一次 + Dispatcher.UIThread.InvokeAsync(() => + { + Bandwidth = GetByteSizeString( + int.Parse(size[0]) * int.Parse(size[1]) * pixelFormatSize * _receivedPerSecond); + AcquisitionFps = _receivedPerSecond; + + _receivedPerSecond = 0; + }); + } + + /// + /// 动态就算带宽 + /// + /// + /// + static string GetByteSizeString(long sizeInBytes) + { + string[] sizeSuffixes = ["Bps", "Kps", "Mps", "Gps"]; + + if (sizeInBytes == 0) + return "0" + sizeSuffixes[0]; + + var magnitude = (int)Math.Log(sizeInBytes, 1024); + var adjustedSize = sizeInBytes / Math.Pow(1024, magnitude); + + return $"{adjustedSize:0.##} {sizeSuffixes[magnitude]}"; + } + + /// + /// 控制是否显示十字辅助现 + /// + private void ShowCrosshair() + { + ShowingCrosshair = !ShowingCrosshair; + } + + /// + /// 屏幕录屏 + /// + private void ToggleRecordingVideo() + { + if (RecordingVideo) + { + Task.Run(StopRecordingVideo); + } + else + { + Task.Run(StartRecordingVideo); + } + } + + + /// + /// 开始录屏 + /// + [Operation("开始录屏")] + private void StartRecordingVideo() + { + if (RecordingVideo) return; + RecordingVideo = true; + + var systemParams = _systemParamsService.GetSystemParams(); + var dir = systemParams.CameraViewerStoragePath ?? + Environment.GetFolderPath(Environment.SpecialFolder.MyPictures); + + dir = Path.Combine(dir, "Dispenser", $"{DateTime.Now:yyyyMMdd}"); + + if (!Directory.Exists(dir)) + { + Directory.CreateDirectory(dir); + } + + var lastFileName = Path.Combine(dir, DateTime.Now.ToString("yyyy-MM-dd-HH-mm-ss") + ".avi"); + + // ReSharper disable once InconsistentNaming + FourCC MJPEG_IMAGE_SHARP = "IMG#"; + + _recorder = new ScreenRecorder(lastFileName, MJPEG_IMAGE_SHARP, 10, 100); + OnWindowResize(); + _recorder.Start(); + } + + // 停止录屏 + [Operation("停止录屏")] + public void StopRecordingVideo() + { + if (!RecordingVideo || _recorder == null) return; + RecordingVideo = false; + var dir = _recorder.GetDirectory(); + _recorder.Stop(); + Dispatcher.UIThread.InvokeAsync(() => { new CameraViewerNotifyWindow("录屏保存成功", dir!).Show(); }); + } + + public void OnWindowResize() + { + // 支取中间的90%画面内容 + if (_recorder != null) + { + var container = _imageViewer.GetContainer(); + var width = container.Bounds.Width - 50; + var height = container.Bounds.Height - 50; + + var left = _window.Position.X + 20; + var top = _window.Position.Y + 60; + + _recorder.Width = (int)width; + _recorder.Height = (int)height; + _recorder.Left = left; + _recorder.Top = top; + } + } + + public void UpdateScaleRatio(double ratio) + { + if (ratio < 1) + { + ScaleRatio = ((1 - ratio) * -100).ToString("0.##") + "%"; + } + else + { + ScaleRatio = (ratio * 100).ToString("0.##") + "%"; + } + } + + + /// + /// 控制是否显示网格线 + /// + private void ShowGrid() + { + } + + + /// + /// 移除监听 + /// + public void DisableCameraListener() + { + EventBus.RemoveEventHandler(EventType.CameraData, CameraCallBack); + JobScheduler.RemoveTask("CameraViewer#WatchCameraPosition"); + } + + + private void TriggerTakePhoto() + { + try + { + var camera = CameraManager.GetCamera()!; + camera.TokePhoto(); + + if (camera.CapturingVideo) + { + camera.StartCaptureVideo(); + } + } + catch (Exception e) + { + Log.Error(e, "坐标验证采集照片失败: "); + } + } +} \ No newline at end of file diff --git a/DispenserUI/ViewModels/Components/ConsoleLogVM.cs b/DispenserUI/ViewModels/Components/ConsoleLogVM.cs new file mode 100644 index 0000000..7bf76d4 --- /dev/null +++ b/DispenserUI/ViewModels/Components/ConsoleLogVM.cs @@ -0,0 +1,61 @@ +using System.Collections.Concurrent; +using System.Collections.ObjectModel; +using System.Threading.Tasks; +using Avalonia.Threading; +using DispenserCommon.Aop; +using DispenserCommon.DTO; +using DispenserCommon.Events; +using DispenserCommon.Ioc; +using ReactiveUI; + +namespace DispenserUI.ViewModels.Components; + +/// +/// 控制台日志 +/// +[Component, GlobalTry] +public class ConsoleLogVM : ViewModelBase +{ + // 实现的最大行数 + private const int MaxLines = 200; + private ObservableCollection _logs = []; + + public ObservableCollection Logs + { + get => _logs; + set => this.RaiseAndSetIfChanged(ref _logs, value); + } + + private readonly BlockingCollection _buffer = new(100); + + /// + /// 监听控制台日志 + /// + /// + /// + [EventAction(EventType.RollingLog)] + public void Listening(EventType type, LogMessage log) + { + _buffer.TryAdd(log); + } + + /// + /// 将日志渲染到界面 + /// + public void RenderLog() + { + Task.Run(() => + { + foreach (var log in _buffer.GetConsumingEnumerable()) + { + Dispatcher.UIThread.InvokeAsync(() => + { + if (Logs.Count >= MaxLines) Logs.RemoveAt(0); + + Logs.Add(log); + }); + Task.Delay(100).Wait(); + } + }); + } +} \ No newline at end of file diff --git a/DispenserUI/ViewModels/ContainerViewModel.cs b/DispenserUI/ViewModels/ContainerViewModel.cs new file mode 100644 index 0000000..c3bd6de --- /dev/null +++ b/DispenserUI/ViewModels/ContainerViewModel.cs @@ -0,0 +1,64 @@ +using System; +using System.Reactive; +using DispenserCommon.Aop; +using DispenserCommon.Events; +using DispenserCommon.Ioc; +using DispenserUI.ViewModels.Product; +using DispenserUI.ViewModels.Setting; +using ReactiveUI; + +namespace DispenserUI.ViewModels; + +/// +/// 内容页面视图模型 +/// +[Component, GlobalTry] +public class ContainerViewModel : DynamicViewModel +{ + private string _currentPage = "IndexView"; + + + /// + /// 去首页 + /// + public ReactiveCommand ToIndexView { get; } + + /// + /// 去设置页 + /// + public ReactiveCommand ToSettingsView { get; } + + public ContainerViewModel() + { + Visible = true; + ToIndexView = ReactiveCommand.Create(ToIndexViewCmd); + ToSettingsView = ReactiveCommand.Create(ToSettingsViewCmd); + } + + public string CurrentPage + { + get => _currentPage; + set => this.RaiseAndSetIfChanged(ref _currentPage, value); + } + + + /// + /// 处理跳转首页命令 + /// + [Operation("主菜单跳转-前往主页面")] + private void ToIndexViewCmd() + { + CurrentPage = "IndexView"; + EventBus.Publish(EventType.PageChanged, typeof(ProductIndexVM)); + } + + /// + /// 处理跳转设置页命令 + /// + [Operation("主菜单跳转-前往设置页面")] + private void ToSettingsViewCmd() + { + CurrentPage = "SettingView"; + EventBus.Publish(EventType.PageChanged, typeof(SettingsVM)); + } +} \ No newline at end of file diff --git a/DispenserUI/ViewModels/Converter/BoolConverter.cs b/DispenserUI/ViewModels/Converter/BoolConverter.cs new file mode 100644 index 0000000..e645b67 --- /dev/null +++ b/DispenserUI/ViewModels/Converter/BoolConverter.cs @@ -0,0 +1,18 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; + +namespace DispenserUI.ViewModels.Converter; + +public class BoolConverter : IValueConverter +{ + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + return Equals(value, bool.TryParse(parameter?.ToString(), out var result) ? result : parameter); + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + return parameter ?? value; + } +} \ No newline at end of file diff --git a/DispenserUI/ViewModels/Converter/ChipColorConverter.cs b/DispenserUI/ViewModels/Converter/ChipColorConverter.cs new file mode 100644 index 0000000..9860fe9 --- /dev/null +++ b/DispenserUI/ViewModels/Converter/ChipColorConverter.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using Avalonia.Data.Converters; +using Avalonia.Media; + +namespace DispenserUI.ViewModels.Converter; + +public class ChipColorConverter : IValueConverter +{ + private static readonly List Types = [Brushes.Gray,Brushes.Red, Brushes.Green, Brushes.Blue]; + + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + return Types[(int)value!]; + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + for (var i = 0; i < Types.Count; i++) + if (ReferenceEquals(Types[i], value)) + return i; + + return 0; + } +} \ No newline at end of file diff --git a/DispenserUI/ViewModels/Converter/DetectStrategyConverter.cs b/DispenserUI/ViewModels/Converter/DetectStrategyConverter.cs new file mode 100644 index 0000000..83217f7 --- /dev/null +++ b/DispenserUI/ViewModels/Converter/DetectStrategyConverter.cs @@ -0,0 +1,24 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; +using Masuit.Tools; + +namespace DispenserUI.ViewModels.Converter; + +public class DetectStrategyConverter : IValueConverter +{ + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value.IsNullOrEmpty() || value?.ToString() == "0") + { + return "不质检"; + } + + return $"每{value}颗"; + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/DispenserUI/ViewModels/Converter/DoubleToPrecisionConverter.cs b/DispenserUI/ViewModels/Converter/DoubleToPrecisionConverter.cs new file mode 100644 index 0000000..c653730 --- /dev/null +++ b/DispenserUI/ViewModels/Converter/DoubleToPrecisionConverter.cs @@ -0,0 +1,38 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; + +namespace DispenserUI.ViewModels.Converter; + +/// +/// 对象的数值转换为指定精度的字符串 +/// +public class DoubleToPrecisionConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + int digits = 3; + if (int.TryParse(parameter as string, out int _digits)) + { + digits = _digits; + } + + return value switch + { + + // 确保输入值是浮点数 + int => value, + double val => Math.Round(val, digits), + _ => value + }; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + // 从字符串转换回浮点数 + if (value is string stringValue && double.TryParse(stringValue, out var result)) return result; + + // 如果转换失败,则返回0 + return 0d; + } +} \ No newline at end of file diff --git a/DispenserUI/ViewModels/Converter/EqualsConverter.cs b/DispenserUI/ViewModels/Converter/EqualsConverter.cs new file mode 100644 index 0000000..362532e --- /dev/null +++ b/DispenserUI/ViewModels/Converter/EqualsConverter.cs @@ -0,0 +1,23 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; + +namespace DispenserUI.ViewModels.Converter; + +/// +/// 用户比较输入的值和目标值是否一致的转换器 +/// +public class EqualsConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is int) return value.ToString()!.Equals(parameter); + + return value.Equals(parameter); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/DispenserUI/ViewModels/Converter/JobTypeConverter.cs b/DispenserUI/ViewModels/Converter/JobTypeConverter.cs new file mode 100644 index 0000000..e08ade8 --- /dev/null +++ b/DispenserUI/ViewModels/Converter/JobTypeConverter.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using Avalonia.Data.Converters; + +namespace DispenserUI.ViewModels.Converter; + +public class JobTypeConverter : IValueConverter +{ + private static readonly List Types = ["P2P", "动打"]; + + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + return Types[(int)value!]; + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + for (var i = 0; i < Types.Count; i++) + if (ReferenceEquals(Types[i], value)) + return i; + + return 0; + } +} \ No newline at end of file diff --git a/DispenserUI/ViewModels/Converter/LicenseTypeConverter.cs b/DispenserUI/ViewModels/Converter/LicenseTypeConverter.cs new file mode 100644 index 0000000..18e7b78 --- /dev/null +++ b/DispenserUI/ViewModels/Converter/LicenseTypeConverter.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using Avalonia.Data.Converters; + +namespace DispenserUI.ViewModels.Converter; + +public class LicenseTypeConverter : IValueConverter +{ + private static readonly List Types = ["临时许可", "正式许可"]; + + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + return Types[(int)value!]; + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + for (var i = 0; i < Types.Count; i++) + if (ReferenceEquals(Types[i], value)) + return i; + + return 0; + } +} \ No newline at end of file diff --git a/DispenserUI/ViewModels/Converter/LogColorConverter.cs b/DispenserUI/ViewModels/Converter/LogColorConverter.cs new file mode 100644 index 0000000..f0694a5 --- /dev/null +++ b/DispenserUI/ViewModels/Converter/LogColorConverter.cs @@ -0,0 +1,38 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; +using Avalonia.Media; +using Serilog.Events; + +namespace DispenserUI.ViewModels.Converter; + +public class LogColorConverter : IValueConverter +{ + private static readonly IBrush Gray = Brush.Parse("#BEBEBE"); + private static readonly IBrush White = Brush.Parse("#FFFFFF"); + private static readonly IBrush Golden = Brush.Parse("#FFD700"); + private static readonly IBrush Red = Brush.Parse("#FF6347"); + private static readonly IBrush DeepRed = Brush.Parse("#DC143C"); + + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is LogEventLevel level) + return level switch + { + LogEventLevel.Verbose => Gray, + LogEventLevel.Debug => Gray, + LogEventLevel.Information => White, + LogEventLevel.Warning => Golden, + LogEventLevel.Error => Red, + LogEventLevel.Fatal => DeepRed, + _ => White + }; + + return White; + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/DispenserUI/ViewModels/Converter/MenuBgConverter.cs b/DispenserUI/ViewModels/Converter/MenuBgConverter.cs new file mode 100644 index 0000000..d4b7dbd --- /dev/null +++ b/DispenserUI/ViewModels/Converter/MenuBgConverter.cs @@ -0,0 +1,19 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; +using Avalonia.Media; + +namespace DispenserUI.ViewModels.Converter; + +public class MenuBgConverter : IValueConverter +{ + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + return Equals(value, parameter) ? Brush.Parse("#A19DF1") : Brushes.White; + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/DispenserUI/ViewModels/Converter/MenuBorderConverter.cs b/DispenserUI/ViewModels/Converter/MenuBorderConverter.cs new file mode 100644 index 0000000..bf2e92b --- /dev/null +++ b/DispenserUI/ViewModels/Converter/MenuBorderConverter.cs @@ -0,0 +1,19 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; +using Avalonia.Media; + +namespace DispenserUI.ViewModels.Converter; + +public class MenuBorderConverter : IValueConverter +{ + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + return Equals(value, parameter) ? Brush.Parse("#A19DF1") : Brush.Parse("#0D0D12"); + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/DispenserUI/ViewModels/Converter/MenuFgConverter.cs b/DispenserUI/ViewModels/Converter/MenuFgConverter.cs new file mode 100644 index 0000000..fd093c6 --- /dev/null +++ b/DispenserUI/ViewModels/Converter/MenuFgConverter.cs @@ -0,0 +1,19 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; +using Avalonia.Media; + +namespace DispenserUI.ViewModels.Converter; + +public class MenuFgConverter : IValueConverter +{ + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + return Equals(value, parameter) ? Brushes.White : Brushes.Black; + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/DispenserUI/ViewModels/Converter/NullToBoolConverter.cs b/DispenserUI/ViewModels/Converter/NullToBoolConverter.cs new file mode 100644 index 0000000..0a716c5 --- /dev/null +++ b/DispenserUI/ViewModels/Converter/NullToBoolConverter.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections; +using System.Globalization; +using Avalonia.Data.Converters; +using Masuit.Tools; + +namespace DispenserUI.ViewModels.Converter; + +/// +/// 用户比较输入的值和目标值是否一致的转换器 +/// +public class NullToBoolConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return value switch + { + string => !value.IsNullOrEmpty(), + IEnumerable => !value.IsNullOrEmpty(), + bool => value, + _ => !value.IsNullOrEmpty() + }; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/DispenserUI/ViewModels/Converter/PcbTypeConverter.cs b/DispenserUI/ViewModels/Converter/PcbTypeConverter.cs new file mode 100644 index 0000000..d79cac0 --- /dev/null +++ b/DispenserUI/ViewModels/Converter/PcbTypeConverter.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using Avalonia.Data.Converters; + +namespace DispenserUI.ViewModels.Converter; + +public class PcbTypeConverter : IValueConverter +{ + private static readonly List Types = ["未选择", "PCB基板", "玻璃基板"]; + + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + return Types[(int)value!]; + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + for (var i = 0; i < Types.Count; i++) + if (ReferenceEquals(Types[i], value)) + return i; + + return 0; + } +} \ No newline at end of file diff --git a/DispenserUI/ViewModels/Converter/PropertyChangedConverter.cs b/DispenserUI/ViewModels/Converter/PropertyChangedConverter.cs new file mode 100644 index 0000000..d0c7f51 --- /dev/null +++ b/DispenserUI/ViewModels/Converter/PropertyChangedConverter.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using Avalonia.Data.Converters; +using Masuit.Tools; + +namespace DispenserUI.ViewModels.Converter; + +public class PropertyChangedConverter : IValueConverter +{ + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is Dictionary changed && !parameter.IsNullOrEmpty()) + { + changed.TryGetValue(parameter.ToString(), out var val); + return val; + } + + return false; + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/DispenserUI/ViewModels/Converter/RgbConverter.cs b/DispenserUI/ViewModels/Converter/RgbConverter.cs new file mode 100644 index 0000000..72751be --- /dev/null +++ b/DispenserUI/ViewModels/Converter/RgbConverter.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using Avalonia.Data.Converters; + +namespace DispenserUI.ViewModels.Converter; + +public class RgbConverter : IValueConverter +{ + private static readonly List Colors = ["未选择", "R", "G", "B"]; + + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + return Colors[(int)value!]; + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + for (var i = 0; i < Colors.Count; i++) + if (ReferenceEquals(Colors[i], value)) + return i; + + return 0; + } +} \ No newline at end of file diff --git a/DispenserUI/ViewModels/Converter/StatusColorConverter.cs b/DispenserUI/ViewModels/Converter/StatusColorConverter.cs new file mode 100644 index 0000000..319c9c9 --- /dev/null +++ b/DispenserUI/ViewModels/Converter/StatusColorConverter.cs @@ -0,0 +1,25 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; +using Avalonia.Media; + +namespace DispenserUI.ViewModels.Converter; + +public class StatusColorConverter : IValueConverter +{ + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (parameter is null) + { + return value is true ? Brushes.Green : Brushes.Gray; + } + + var color = (string)parameter; + return value is true ? Brush.Parse(color) : Brushes.Gray; + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/DispenserUI/ViewModels/Converter/StrikeDirectionConverter.cs b/DispenserUI/ViewModels/Converter/StrikeDirectionConverter.cs new file mode 100644 index 0000000..6a6fca3 --- /dev/null +++ b/DispenserUI/ViewModels/Converter/StrikeDirectionConverter.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using Avalonia.Data.Converters; + +namespace DispenserUI.ViewModels.Converter; + +public class StrikeDirectionConverter : IValueConverter +{ + private static readonly List Directions = ["未选择", "行方向", "列方向"]; + + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + return Directions[(int)value!]; + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + for (var i = 0; i < Directions.Count; i++) + if (ReferenceEquals(Directions[i], value)) + return i; + + return 0; + } +} \ No newline at end of file diff --git a/DispenserUI/ViewModels/Converter/StringNullOrEmptyToVisibilityConverter.cs b/DispenserUI/ViewModels/Converter/StringNullOrEmptyToVisibilityConverter.cs new file mode 100644 index 0000000..34f2b87 --- /dev/null +++ b/DispenserUI/ViewModels/Converter/StringNullOrEmptyToVisibilityConverter.cs @@ -0,0 +1,19 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; + +namespace DispenserUI.ViewModels.Converter; + +public class StringNullOrEmptyToVisibilityConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + var str = value as string; + return !string.IsNullOrEmpty(str); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/DispenserUI/ViewModels/Converter/StringToDoubleArrayConverter.cs b/DispenserUI/ViewModels/Converter/StringToDoubleArrayConverter.cs new file mode 100644 index 0000000..1e30903 --- /dev/null +++ b/DispenserUI/ViewModels/Converter/StringToDoubleArrayConverter.cs @@ -0,0 +1,40 @@ +using System; +using System.Globalization; +using System.Linq; +using Avalonia.Data.Converters; + +namespace DispenserUI.ViewModels.Converter; + +public class StringToDoubleArrayConverter : IValueConverter +{ + public object? Convert(object? value, Type targetType, object parameter, CultureInfo culture) + { + // 从double[] 转换到 string + if (value is double[] doubleArray) return doubleArray; + + return null; + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + // 从string转换到 double[] + if (value is string str) + try + { + str = str.Replace(" ", ",").Replace(",", ","); + var doubleArray = str.Split(',') + .Where(s => s.Length > 0) + .Select(s => double.Parse(s, culture)) + .ToArray(); + return doubleArray; + } + catch (Exception e) + { + Console.WriteLine(e.StackTrace); + // 处理转换异常 + return Array.Empty(); + } + + return null; + } +} \ No newline at end of file diff --git a/DispenserUI/ViewModels/Converter/StringToDoubleConverter.cs b/DispenserUI/ViewModels/Converter/StringToDoubleConverter.cs new file mode 100644 index 0000000..7788f04 --- /dev/null +++ b/DispenserUI/ViewModels/Converter/StringToDoubleConverter.cs @@ -0,0 +1,27 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; + +namespace DispenserUI.ViewModels.Converter; + +public class StringToDoubleConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + // 从ViewModel到View的转换逻辑 + if (value is double doubleValue) return doubleValue.ToString(); + + return value.ToString(); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + // 从View到ViewModel的转换逻辑 + if (value is not string stringValue) return value; + if (double.TryParse(stringValue, out var result)) return result; + + // 处理无效输入的情况,例如返回0或保留上一有效值等 + // 这里简单处理为返回0,视具体需求调整 + return 0; + } +} \ No newline at end of file diff --git a/DispenserUI/ViewModels/Converter/TimeSpanConverter.cs b/DispenserUI/ViewModels/Converter/TimeSpanConverter.cs new file mode 100644 index 0000000..2890d37 --- /dev/null +++ b/DispenserUI/ViewModels/Converter/TimeSpanConverter.cs @@ -0,0 +1,21 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; +using DispenserCommon.Utils; + +namespace DispenserUI.ViewModels.Converter; + +public class TimeSpanConverter : IValueConverter +{ + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + var time = (long)value; + + return time == 0 ? "0秒" : TimeUtil.ToTimeSpan(time); + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/DispenserUI/ViewModels/Converter/UserRoleConverter.cs b/DispenserUI/ViewModels/Converter/UserRoleConverter.cs new file mode 100644 index 0000000..1ed009c --- /dev/null +++ b/DispenserUI/ViewModels/Converter/UserRoleConverter.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using Avalonia.Data.Converters; + +namespace DispenserUI.ViewModels.Converter; + +public class UserRoleConverter : IValueConverter +{ + private static readonly List Roles = ["普通用户", "管理员"]; + + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + return Roles[(int)value!]; + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + for (var i = 0; i < Roles.Count; i++) + if (ReferenceEquals(Roles[i], value)) + return i; + + return 0; + } +} \ No newline at end of file diff --git a/DispenserUI/ViewModels/DTO/CameraParams.cs b/DispenserUI/ViewModels/DTO/CameraParams.cs new file mode 100644 index 0000000..515cf6f --- /dev/null +++ b/DispenserUI/ViewModels/DTO/CameraParams.cs @@ -0,0 +1,296 @@ +using System.ComponentModel; +using System.Runtime.CompilerServices; +using DispenserCommon.Atrributes; +using DispenserHal.Camera.DTO; + +namespace DispenserUI.ViewModels.DTO; + +public class CameraParams : INotifyPropertyChanged +{ + // 白平衡值 + private IntValue? _balanceRatio; + + // 是否自动白平衡 + private bool? _balanceWhiteAuto; + + //灰度值 + private FloatValue? _blackLevel; + + // 黑电平调节使能 + private bool _blackLevelEnable; + + // 设备序列号 + private StringValue? _deviceSerialNumber; + + /// + /// 自动曝光 + /// + private bool? _exposureAuto; + + // 曝光时间 + private FloatValue? _exposureTime; + + // 增益 + private FloatValue? _gain; + + /// + /// 自动增益 + /// 0:Off + /// 1:Once + /// 2:Continuous + /// + private EnumValue? _gainAuto; + + // gamma 值 + private FloatValue? _gamma; + + /// + /// 是否gamma使能 + /// + private bool _gammaEnable; + + /// + /// IO 模式 + /// 0:Input + /// 1:Output + /// 2:Trigger + /// 8:Strobe + /// + private EnumValue? _lineMode; + + /// + /// IO 选择 + /// 0:Line0 + /// 1:Line1 + /// 2:Line2 + /// 3:Line3 + /// 4:Line4 + /// + private EnumValue? _lineSelector; + + //帧率-待确定 + private FloatValue? _resultingFrameRate; + + /// + /// 0:RisingEdge + /// 1:FallingEdge + /// 2:LevelHigh + /// 3:LevelLow + /// + private EnumValue? _triggerActivation; + + + /// + /// 触发模式 + /// 0:Off 1:On + /// + private EnumValue? _triggerMode; + + /// + /// 0:Line0 + /// 1:Line1 + /// 2:Line2 + /// 3.Line3 + /// 4:Counter0 + /// 7:Software + /// 8:FrequencyConverter + /// + private EnumValue? _triggerSource; + + + [Description("设备序列号")] + public StringValue? DeviceSerialNumber + { + get => _deviceSerialNumber; + set + { + _deviceSerialNumber = value; + OnPropertyChanged(); + } + } + + + [Description("曝光时间"), Property(Max = 2000)] + public FloatValue? ExposureTime + { + get => _exposureTime; + set + { + _exposureTime = value; + OnPropertyChanged(); + } + } + + [Description("自动曝光")] + public bool? ExposureAuto + { + get => _exposureAuto; + set + { + _exposureAuto = value; + OnPropertyChanged(); + } + } + + [Description("增益")] + public FloatValue? Gain + { + get => _gain; + set + { + _gain = value; + OnPropertyChanged(); + } + } + + [Description("自动增益")] + public EnumValue? GainAuto + { + get => _gainAuto; + set + { + _gainAuto = value; + OnPropertyChanged(); + } + } + + [Description("灰度值")] + public FloatValue? BlackLevel + { + get => _blackLevel; + set + { + _blackLevel = value; + OnPropertyChanged(); + } + } + + [Description("黑电平调节使能")] + public bool BlackLevelEnable + { + get => _blackLevelEnable; + set + { + _blackLevelEnable = value; + OnPropertyChanged(); + } + } + + [Description("白平衡值")] + public IntValue? BalanceRatio + { + get => _balanceRatio; + set + { + _balanceRatio = value; + OnPropertyChanged(); + } + } + + [Description("自动白平衡")] + public bool? BalanceWhiteAuto + { + get => _balanceWhiteAuto; + set + { + _balanceWhiteAuto = value; + OnPropertyChanged(); + } + } + + [Description("触发激活")] + public EnumValue? TriggerActivation + { + get => _triggerActivation; + set + { + _triggerActivation = value; + OnPropertyChanged(); + } + } + + [Description("实际采集帧率fps")] + public FloatValue? ResultingFrameRate + { + get => _resultingFrameRate; + set + { + _resultingFrameRate = value; + OnPropertyChanged(); + } + } + + [Description("gamma值")] + public FloatValue? Gamma + { + get => _gamma; + set + { + _gamma = value; + OnPropertyChanged(); + } + } + + [Description("是否gamma使能")] + public bool GammaEnable + { + get => _gammaEnable; + set + { + _gammaEnable = value; + OnPropertyChanged(); + } + } + + + [Description("IO 选择")] + public EnumValue? LineSelector + { + get => _lineSelector; + set + { + _lineSelector = value; + OnPropertyChanged(); + } + } + + [Description("IO 模式")] + public EnumValue? LineMode + { + get => _lineMode; + set + { + _lineMode = value; + OnPropertyChanged(); + } + } + + [Description("触发模式")] + public EnumValue? TriggerMode + { + get => _triggerMode; + set + { + _triggerMode = value; + OnPropertyChanged(); + } + } + + [Description("触发源")] + public EnumValue? TriggerSource + { + get => _triggerSource; + set + { + _triggerSource = value; + OnPropertyChanged(); + } + } + + public event PropertyChangedEventHandler? PropertyChanged; + + protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } +} \ No newline at end of file diff --git a/DispenserUI/ViewModels/DTO/LogFile.cs b/DispenserUI/ViewModels/DTO/LogFile.cs new file mode 100644 index 0000000..1575d55 --- /dev/null +++ b/DispenserUI/ViewModels/DTO/LogFile.cs @@ -0,0 +1,19 @@ +using System; +using Avalonia.Logging; + +namespace DispenserUI.ViewModels.DTO; + +public class LogFile +{ + public string FileName { get; set; } + + public string FilePath { get; set; } + + public long FileSize { get; set; } + + public LogEventLevel Level { get; set; } + + public DateTime CreateTime { get; set; } + + public DateTime ModifyTime { get; set; } +} \ No newline at end of file diff --git a/DispenserUI/ViewModels/DTO/SystemStatus.cs b/DispenserUI/ViewModels/DTO/SystemStatus.cs new file mode 100644 index 0000000..45d3c2f --- /dev/null +++ b/DispenserUI/ViewModels/DTO/SystemStatus.cs @@ -0,0 +1,82 @@ +using System.ComponentModel; +using System.Runtime.CompilerServices; + +namespace DispenserUI.ViewModels.DTO; + +/// +/// 系统设备状态 +/// +public class SystemStatus(string name) : INotifyPropertyChanged +{ + private bool _isNormal = true; + + private string _tip; + + private string _icon; + + private string _background = "Transparent"; + + private string _foreground = "#36394A"; + + public string Name + { + get => name; + } + + public bool IsNormal + { + get => _isNormal; + set + { + _isNormal = value; + OnPropertyChanged(); + } + } + + public string Tip + { + get => _tip; + set + { + _tip = value; + OnPropertyChanged(); + } + } + + public string Icon + { + get => _icon; + set + { + _icon = value; + OnPropertyChanged(); + } + } + + public string Background + { + get => _background; + set + { + _background = value; + OnPropertyChanged(); + } + } + + public string Foreground + { + get => _foreground; + set + { + _foreground = value; + OnPropertyChanged(); + } + } + + public event PropertyChangedEventHandler? PropertyChanged; + + protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } +} \ No newline at end of file diff --git a/DispenserUI/ViewModels/DynamicViewModel.cs b/DispenserUI/ViewModels/DynamicViewModel.cs new file mode 100644 index 0000000..6980c2c --- /dev/null +++ b/DispenserUI/ViewModels/DynamicViewModel.cs @@ -0,0 +1,38 @@ +using System; +using DispenserCommon.Events; +using ReactiveUI; + +namespace DispenserUI.ViewModels; + +/// +/// 可用于动态的切换页面是否显示的view model +/// +public abstract class DynamicViewModel : ViewModelBase +{ + private bool _visible; + + protected DynamicViewModel(EventType type = EventType.PageChanged) + { + EventBus.AddEventHandler(type, WhenPageChanged); + } + + /// + /// 动态控制是否显示 + /// + public bool Visible + { + get => _visible; + set + { + _visible = value; + this.RaisePropertyChanged(); + } + } + + public virtual void WhenPageChanged(EventType type, Type data) + { + var me = GetType(); + // 默认是当前是当前的data为当前ViewModel的类型 + Visible = data == me; + } +} \ No newline at end of file diff --git a/DispenserUI/ViewModels/IDynamicPage.cs b/DispenserUI/ViewModels/IDynamicPage.cs new file mode 100644 index 0000000..25b8bc3 --- /dev/null +++ b/DispenserUI/ViewModels/IDynamicPage.cs @@ -0,0 +1,16 @@ +using DispenserCommon.Events; + +namespace DispenserUI.ViewModels; + +/// +/// 定义动态页面监听接口 +/// +public interface IDynamicPage +{ + /// + /// 监听页面切换事件 + /// + /// 事件类型 + /// 回调值 + void OnPageChanged(EventType type, T data); +} \ No newline at end of file diff --git a/DispenserUI/ViewModels/IDynamicParams.cs b/DispenserUI/ViewModels/IDynamicParams.cs new file mode 100644 index 0000000..cceb5a8 --- /dev/null +++ b/DispenserUI/ViewModels/IDynamicParams.cs @@ -0,0 +1,8 @@ +namespace DispenserUI.ViewModels; + +public interface IDynamicParams +{ + void PropertyHasChanged(string propertyName); + + void SaveOrUpdateParams(string propertyName); +} \ No newline at end of file diff --git a/DispenserUI/ViewModels/Login/LoginVM.cs b/DispenserUI/ViewModels/Login/LoginVM.cs new file mode 100644 index 0000000..8dad2e0 --- /dev/null +++ b/DispenserUI/ViewModels/Login/LoginVM.cs @@ -0,0 +1,145 @@ +using System; +using System.Collections.Generic; +using System.Reactive; +using System.Windows.Forms; +using DispenserCommon.Aop; +using DispenserCommon.Ioc; +using DispenserCommon.Utils; +using DispenserCore.Context; +using DispenserCore.Model.DTO; +using DispenserCore.Service; +using ReactiveUI; +using LoginErrorWindow = DispenserUI.Views.Windows.LoginErrorWindow; + +namespace DispenserUI.ViewModels.Login; + +[Component, GlobalTry] +public class LoginVM : DynamicViewModel +{ + private readonly Dictionary _errorTimes = new(); + + private readonly GlobalSessionHolder _sessionHolder = ServiceLocator.GetService(); + + // 用来记录登录错误告警情况,10分钟内异常超过3次则添加到小黑屋里面去 + private readonly SlidingWindow _slidingWindow = new(10 * 60 * 1000); + private readonly UserService _userService = ServiceLocator.GetService(); + private bool _isCapsLockIndicatorVisible; + + //订阅IsCapsLockOn、IsPwdTextBoxFocus + private bool _isCapsLockOn; + private bool _isPwdTextBoxFocus; + + private string _password; + + private bool _showError; + + private string _userName; + + public LoginVM() + { + Visible = false; + CapsLockUpCommand = ReactiveCommand.Create(CapsLockUp); + PwdTextBoxGotFocusCommand = ReactiveCommand.Create(PwdTextBoxGotFocus); + PwdTextBoxLostFocusCommand = ReactiveCommand.Create(PwdTextBoxLostFocus); + ToLogin = ReactiveCommand.Create(Login); + } + + public string UserName + { + get => _userName; + set => this.RaiseAndSetIfChanged(ref _userName, value); + } + + public string Password + { + get => _password; + set => this.RaiseAndSetIfChanged(ref _password, value); + } + + + public bool IsCapsLockIndicatorVisible + { + get => _isCapsLockIndicatorVisible; + set => this.RaiseAndSetIfChanged(ref _isCapsLockIndicatorVisible, value); + } + + public bool ShowError + { + get => _showError; + set => this.RaiseAndSetIfChanged(ref _showError, value); + } + + + public ReactiveCommand CapsLockUpCommand { get; } + public ReactiveCommand PwdTextBoxGotFocusCommand { get; } + + public ReactiveCommand PwdTextBoxLostFocusCommand { get; } + + public ReactiveCommand ToLogin { get; } + + private void CapsLockUp() + { + _isCapsLockOn = Control.IsKeyLocked(Keys.CapsLock); + UpdateCapsLockIndicator(); + } + + private void PwdTextBoxGotFocus() + { + _isPwdTextBoxFocus = true; + ShowError = false; + UpdateCapsLockIndicator(); + } + + private void PwdTextBoxLostFocus() + { + _isPwdTextBoxFocus = false; + ShowError = false; + UpdateCapsLockIndicator(); + } + + private void UpdateCapsLockIndicator() + { + IsCapsLockIndicatorVisible = _isCapsLockOn && _isPwdTextBoxFocus; + } + + /// + /// 登录 + /// + [Operation("用户登录")] + private void Login() + { + try + { + // 如果登录异常次数超过3次,则提示用户 + if (_slidingWindow.Contains(UserName)) + { + WindowUtil.ShowDialog(new LoginErrorWindow()); + // 清空当前的错误统计 + _errorTimes.Remove(UserName); + return; + } + + ShowError = false; + + // 进行登录验证 + var user = _userService.Login(UserName, Password); + + // 将登录结果记录下来 + _sessionHolder.SetSession(new Session(user)); + } + catch (Exception e) + { + ShowError = true; + _errorTimes.TryGetValue(UserName, out var times); + times += 1; + if (times >= 3) + { + // 添加到小黑屋,10分钟内不能进行登录 + _slidingWindow.AllowValue(UserName); + return; + } + + _errorTimes[UserName] = times; + } + } +} \ No newline at end of file diff --git a/DispenserUI/ViewModels/MainVM.cs b/DispenserUI/ViewModels/MainVM.cs new file mode 100644 index 0000000..8c9ad78 --- /dev/null +++ b/DispenserUI/ViewModels/MainVM.cs @@ -0,0 +1,130 @@ +using System; +using System.Reactive; +using System.Timers; +using Avalonia.Threading; +using DispenserCommon.Aop; +using DispenserCommon.Events; +using DispenserCommon.Ioc; +using DispenserCommon.Utils; +using DispenserCore.Context; +using DispenserCore.Model.Entity; +using DispenserUI.ViewModels.Login; +using ReactiveUI; +using Serilog; + +namespace DispenserUI.ViewModels; + +/// +/// 主界面 UI view model +/// +[Component, GlobalTry] +public class MainVM : ViewModelBase +{ + private readonly GlobalSessionHolder _holder = ServiceLocator.GetService(); + + private bool _logged; + + private bool _locked; + + private string _now = GetNowTime(); + + private User _who; + + public MainVM() + { + // 开启时钟 + StartClock(); + ToLogout = ReactiveCommand.Create(ToLogoutCmd); + // 监听用户登录状态,然后进行页面跳转 + _holder.PropertyChanged += (holder, args) => + { + var session = ((GlobalSessionHolder)holder).GetSession(); + + if (session == null) + { + EventBus.Publish(EventType.PageChanged, typeof(LoginVM)); + return; + } + + Who = session.User; + Logged = true; + EventBus.Publish(EventType.PageChanged, typeof(ContainerViewModel)); + }; + if (!_holder.Logged()) + { + // 如果没有登录,则跳转到登录页面 + + // EventBus.Publish(EventType.PageChanged, typeof(LoginVM)); + } + } + + /// + /// 去首页 + /// + public ReactiveCommand ToLogout { get; } + + public string Now + { + get => _now; + set => this.RaiseAndSetIfChanged(ref _now, value); + } + + public User Who + { + get => _who; + set => this.RaiseAndSetIfChanged(ref _who, value); + } + + public bool Logged + { + get => _logged; + set => this.RaiseAndSetIfChanged(ref _logged, value); + } + + public bool Locked + { + get => _locked; + set => this.RaiseAndSetIfChanged(ref _locked, value); + } + + /// + /// 处理登出页面 + /// + private void ToLogoutCmd() + { + _holder.ClearSession(); + } + + private void StartClock() + { + var timer = new Timer(1000); + timer.Elapsed += (sender, args) => { Dispatcher.UIThread.InvokeAsync(() => { Now = GetNowTime(); }); }; + timer.AutoReset = true; + timer.Enabled = true; + } + + private static string GetNowTime() + { + return DateTime.Now.ToString("yyyy年M月d日 dddd HH:mm:ss"); + } + + + /// + /// 监听远程锁机和解锁指令 + /// + /// + /// + [EventAction(EventType.LockEvent)] + public void ListeningLocked(EventType type, bool locked) + { + Locked = locked; + if (locked) + { + Log.Warning("已远程锁机"); + } + else + { + Log.Information("已远程解锁"); + } + } +} \ No newline at end of file diff --git a/DispenserUI/ViewModels/MainViewModel.cs b/DispenserUI/ViewModels/MainViewModel.cs new file mode 100644 index 0000000..fbc10b8 --- /dev/null +++ b/DispenserUI/ViewModels/MainViewModel.cs @@ -0,0 +1,8 @@ +using DispenserCommon.Ioc; + +namespace DispenserUI.ViewModels; + +[Component] +public class MainViewModel : ViewModelBase +{ +} \ No newline at end of file diff --git a/DispenserUI/ViewModels/Product/ProductIndexVM.cs b/DispenserUI/ViewModels/Product/ProductIndexVM.cs new file mode 100644 index 0000000..bd25d9e --- /dev/null +++ b/DispenserUI/ViewModels/Product/ProductIndexVM.cs @@ -0,0 +1,13 @@ +using DispenserCommon.Events; +using DispenserCommon.Ioc; + +namespace DispenserUI.ViewModels.Product; + +[Component] +public class ProductIndexVM : DynamicViewModel +{ + public ProductIndexVM() + { + Visible = true; + } +} \ No newline at end of file diff --git a/DispenserUI/ViewModels/Recorder/MJpegImageVideoEncoder.cs b/DispenserUI/ViewModels/Recorder/MJpegImageVideoEncoder.cs new file mode 100644 index 0000000..a478c0e --- /dev/null +++ b/DispenserUI/ViewModels/Recorder/MJpegImageVideoEncoder.cs @@ -0,0 +1,84 @@ +using System; +using System.IO; +using SharpAvi; +using SharpAvi.Codecs; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.PixelFormats; + +namespace DispenserUI.ViewModels.Recorder; + +public class MJpegImageVideoEncoder : IVideoEncoder +{ + private readonly int _width; + private readonly int _height; + private readonly JpegEncoder _jpegEncoder; + private readonly MemoryStream _buffer; + + /// + /// Creates a new instance of . + /// + /// Frame width. + /// Frame height. + /// + /// Compression quality in the range [1..100]. + /// Less values mean less size and lower image quality. + /// + public MJpegImageVideoEncoder(int width, int height, int quality) + { + this._width = width; + this._height = height; + _buffer = new MemoryStream(this.MaxEncodedSize); + _jpegEncoder = new JpegEncoder + { + Quality = quality, + }; + } + + /// Video codec. + public FourCC Codec => CodecIds.MotionJpeg; + + /// Number of bits per pixel in encoded image. + public BitsPerPixel BitsPerPixel => BitsPerPixel.Bpp24; + + /// Maximum size of encoded frmae. + public int MaxEncodedSize => Math.Max(_width * _height * 3, 1024); + + /// Encodes a frame. + public int EncodeFrame( + byte[] source, + int srcOffset, + byte[] destination, + int destOffset, + out bool isKeyFrame) + { + int num; + using (MemoryStream destination1 = new MemoryStream(destination)) + { + destination1.Position = destOffset; + num = LoadAndEncodeImage(source.AsSpan(srcOffset), destination1); + } + + isKeyFrame = true; + return num; + } + + /// Encodes a frame. + public int EncodeFrame(ReadOnlySpan source, Span destination, out bool isKeyFrame) + { + _buffer.SetLength(0L); + var length = LoadAndEncodeImage(source, _buffer); + _buffer.GetBuffer().AsSpan(0, length).CopyTo(destination); + isKeyFrame = true; + return length; + } + + private int LoadAndEncodeImage(ReadOnlySpan source, Stream destination) + { + var position = (int)destination.Position; + using (var image = Image.LoadPixelData(source, _width, _height)) + _jpegEncoder.Encode(image, destination); + destination.Flush(); + return (int)(destination.Position - position); + } +} \ No newline at end of file diff --git a/DispenserUI/ViewModels/Setting/SettingsVM.cs b/DispenserUI/ViewModels/Setting/SettingsVM.cs new file mode 100644 index 0000000..82a57cc --- /dev/null +++ b/DispenserUI/ViewModels/Setting/SettingsVM.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.ObjectModel; +using DispenserCommon.Aop; +using DispenserCommon.Events; +using DispenserCommon.Ioc; +using DispenserUI.Models.DTO; +using ReactiveUI; + +namespace DispenserUI.ViewModels.Setting; + +/// +/// 设置界面 +/// +[Component, GlobalTry] +public class SettingsVM : DynamicViewModel +{ + private ObservableCollection _nodes = []; + + public SettingsVM() + { + Visible = true; + Nodes.Add(new TreeNode("系统参数设置页面", typeof(SystemSettingVM))); + } + + public ObservableCollection Nodes + { + get => _nodes; + set => this.RaiseAndSetIfChanged(ref _nodes, value); + } + + /// + /// 点击了那个节点 + /// + /// + public void ClickedNode(TreeNode node) + { + var viewModel = node.ViewModel; + + if (viewModel != null) EventBus.Publish(EventType.MenuChanged, viewModel); + } +} \ No newline at end of file diff --git a/DispenserUI/ViewModels/Setting/SystemSettingVM.cs b/DispenserUI/ViewModels/Setting/SystemSettingVM.cs new file mode 100644 index 0000000..a07627a --- /dev/null +++ b/DispenserUI/ViewModels/Setting/SystemSettingVM.cs @@ -0,0 +1,12 @@ +using DispenserCommon.Aop; +using DispenserCommon.Events; + +namespace DispenserUI.ViewModels.Setting; + +/// +/// 系统设置页面 +/// +[GlobalTry] +public class SystemSettingVM() : DynamicViewModel(EventType.MenuChanged) +{ +} \ No newline at end of file diff --git a/DispenserUI/ViewModels/SplashVM.cs b/DispenserUI/ViewModels/SplashVM.cs new file mode 100644 index 0000000..afd4b56 --- /dev/null +++ b/DispenserUI/ViewModels/SplashVM.cs @@ -0,0 +1,135 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Avalonia.Controls; +using DispenserCommon.Aop; +using DispenserCommon.DB; +using DispenserCommon.Events; +using DispenserCommon.Ioc; +using DispenserCore.Service; +using DispenserHal.Camera.DTO; +using DispenserHal.Camera.Factory; +using DispenserUI.Utils; +using ReactiveUI; + +namespace DispenserUI.ViewModels; + +/// +/// 启动页 +/// +[Component, GlobalTry] +public class SplashVM : ViewModelBase +{ + // 这个值后续可以通过配置文件来读取 + private string _applicationName = "海炬固晶机HG50"; + + private readonly CancellationTokenSource _cts = new(); + private string _startUpMessage = "系统初始化中..."; + + + private readonly CameraParamsService _cameraParamsService = new(); + + + public string StartUpMessage + { + get => _startUpMessage; + set => this.RaiseAndSetIfChanged(ref _startUpMessage, value); + } + + public string ApplicationName + { + get => _applicationName; + set => this.RaiseAndSetIfChanged(ref _applicationName, value); + } + + public CancellationToken CancellationToken => _cts.Token; + + public void Cancel() + { + _cts.Cancel(); + } + + /// + /// 监听系统初始化 + /// + /// + /// + [EventAction(EventType.SetupNotify)] + public void SetupCallBack(EventType type, string message) + { + StartUpMessage = message; + } + + /// + /// 这里进行系统初始化操作 + /// 这里的异常需要抛出到上层 + /// + public async Task Init(Window window) + { + var initTask = DoInit(); + + if (await Task.WhenAny(initTask, Task.Delay(100000, CancellationToken)) == initTask) + { + var res = await initTask; + + if (!res) + { + await ConfirmDialog.ToConfirm("系统初始化失败!"); + Environment.Exit(1); + } + + StartUpMessage = "系统初始化完成,正在进入系统!"; + + await Task.Delay(500, CancellationToken); + } + else + { + // 启动超时,弹出弹出,确认后关闭程序 + StartUpMessage = "系统初始化超时,请检查网络连接!"; + var confirm = await ConfirmDialog.ToConfirm("系统初始化超时,请检查配置后重试!"); + if (confirm) Environment.Exit(1); + } + } + + /// + /// 执行初始化 + /// + /// + private Task DoInit() + { + return Task.Run(() => + { + try + { + // 进行数据库初始化校验 + DatabaseInitializer.Initialize(); + Thread.Sleep(300); + + // 初始化相机连接 + InitCameraParams(); + CameraManager.Connect(); + Thread.Sleep(300); + + return Task.FromResult(true); + } + catch (Exception e) + { + Console.WriteLine($"系统初始化异常: {e}"); + return Task.FromResult(false); + } + }, CancellationToken); + } + + /// + /// 初始化相机参数 + /// + private void InitCameraParams() + { + var cameraParams = _cameraParamsService.GetCameraParams(); + if (cameraParams == null) return; + CameraConfig.Dll = cameraParams.Dll; + CameraConfig.Sdk = cameraParams.Sdk; + CameraConfig.CameraSn = cameraParams.CameraSn; + CameraConfig.ScaleRatio = cameraParams.ScaleRatio; + } +} \ No newline at end of file diff --git a/DispenserUI/ViewModels/ViewModelBase.cs b/DispenserUI/ViewModels/ViewModelBase.cs new file mode 100644 index 0000000..79aeb23 --- /dev/null +++ b/DispenserUI/ViewModels/ViewModelBase.cs @@ -0,0 +1,10 @@ +using DispenserCommon.Aop; +using DispenserCommon.Interface; +using ReactiveUI; + +namespace DispenserUI.ViewModels; + +[GlobalTry] +public class ViewModelBase : ReactiveObject, Instant +{ +} \ No newline at end of file diff --git a/DispenserUI/Views/Common/SystemStatusView.axaml b/DispenserUI/Views/Common/SystemStatusView.axaml new file mode 100644 index 0000000..49e4251 --- /dev/null +++ b/DispenserUI/Views/Common/SystemStatusView.axaml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/DispenserUI/Views/Common/SystemStatusView.axaml.cs b/DispenserUI/Views/Common/SystemStatusView.axaml.cs new file mode 100644 index 0000000..4473c9e --- /dev/null +++ b/DispenserUI/Views/Common/SystemStatusView.axaml.cs @@ -0,0 +1,28 @@ +using System; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Markup.Xaml; +using DispenserCommon.Scheduler; +using DispenserCommon.Utils; +using DispenserUI.ViewModels.Common; + +namespace DispenserUI.Views.Common; + +public partial class SystemStatusView : UserControl +{ + public SystemStatusVM ViewModel { get; } = ServiceLocator.GetService(); + + public SystemStatusView() + { + DataContext = ViewModel; + InitializeComponent(); + ViewModel.BindView(this); + } + + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } +} \ No newline at end of file diff --git a/DispenserUI/Views/ContainerView.axaml b/DispenserUI/Views/ContainerView.axaml new file mode 100644 index 0000000..51cab73 --- /dev/null +++ b/DispenserUI/Views/ContainerView.axaml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + +