From 49f113dcf3e40a40548be5b761551bdf29ba9258 Mon Sep 17 00:00:00 2001 From: "yunxiao.zhu" Date: Fri, 17 Apr 2026 10:43:51 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20=E5=88=9D=E5=A7=8B=E5=8C=96?= =?UTF-8?q?=20PCB=20=E6=A3=80=E6=B5=8B=20WPF=20=E5=BA=94=E7=94=A8=E7=A8=8B?= =?UTF-8?q?=E5=BA=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 创建 AxiOmron.PcbCheck 项目主框架及解决方案 * 添加 Dashboard 和系统设置页面 * 实现 Modbus TCP PLC、扫码枪、SFTP 查询等核心服务 * 集成 Andon 报警、工作流托管服务与日志配置 * 补充项目文档和 UI 设计规范 --- .claude/settings.local.json | 20 + .editorconfig | 58 + .gitattributes | 19 + .gitignore | 64 + AGENTS.md | 248 ++++ AxiOmron.PcbCheck.slnx | 2 + docs/2026-04-16-pcb-check-flow-design.md | 897 +++++++++++++ .../specs/2026-04-16-axi-omron-ui-design.md | 59 + src/AxiOmron.PcbCheck/App.xaml | 17 + src/AxiOmron.PcbCheck/App.xaml.cs | 130 ++ src/AxiOmron.PcbCheck/AssemblyInfo.cs | 10 + .../AxiOmron.PcbCheck.csproj | 33 + .../Converters/BooleanToBrushConverter.cs | 61 + .../BooleanToVisibilityConverter.cs | 67 + .../ResultCodeToTagTextConverter.cs | 54 + .../DesignTime/DesignTimeAppConfigService.cs | 120 ++ .../DesignTime/DesignTimeAppStateStore.cs | 206 +++ .../DesignTime/DesignTimeDispatcherService.cs | 22 + .../DesignTime/DesignTimeViewModelLocator.cs | 39 + .../DesignTimeWorkflowControlService.cs | 57 + src/AxiOmron.PcbCheck/MainWindow.xaml | 38 + src/AxiOmron.PcbCheck/MainWindow.xaml.cs | 46 + .../Models/RuntimeSnapshot.cs | 138 ++ .../Models/WorkflowModels.cs | 579 +++++++++ src/AxiOmron.PcbCheck/NLog.config | 16 + src/AxiOmron.PcbCheck/Options/AppConfig.cs | 415 ++++++ .../Services/Implementations/AndonService.cs | 118 ++ .../Implementations/AppConfigService.cs | 65 + .../Services/Implementations/AppLogger.cs | 154 +++ .../Services/Implementations/AppStateStore.cs | 79 ++ .../Implementations/DispatcherService.cs | 23 + .../Implementations/ModbusTcpPlcService.cs | 257 ++++ .../Implementations/SerialScannerService.cs | 319 +++++ .../Implementations/SftpLookupService.cs | 200 +++ .../Implementations/WorkflowHostedService.cs | 1047 +++++++++++++++ .../Services/Interfaces/CoreInterfaces.cs | 261 ++++ .../Utils/TextAndPatternHelpers.cs | 135 ++ .../ViewModels/MainWindowViewModel.cs | 505 ++++++++ .../ViewModels/SystemSettingViewModel.cs | 80 ++ .../ViewModels/ViewModelLocator.cs | 21 + .../Views/Pages/DashboardPage.xaml | 1130 +++++++++++++++++ .../Views/Pages/DashboardPage.xaml.cs | 17 + .../Views/Pages/SystemSettingsPage.xaml | 100 ++ .../Views/Pages/SystemSettingsPage.xaml.cs | 17 + .../appConfig.Development.json | 15 + src/AxiOmron.PcbCheck/appConfig.json | 84 ++ 46 files changed, 8042 insertions(+) create mode 100644 .claude/settings.local.json create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 AGENTS.md create mode 100644 AxiOmron.PcbCheck.slnx create mode 100644 docs/2026-04-16-pcb-check-flow-design.md create mode 100644 src/AxiOmron.PcbCheck/App.xaml create mode 100644 src/AxiOmron.PcbCheck/App.xaml.cs create mode 100644 src/AxiOmron.PcbCheck/AssemblyInfo.cs create mode 100644 src/AxiOmron.PcbCheck/AxiOmron.PcbCheck.csproj create mode 100644 src/AxiOmron.PcbCheck/Converters/BooleanToBrushConverter.cs create mode 100644 src/AxiOmron.PcbCheck/Converters/BooleanToVisibilityConverter.cs create mode 100644 src/AxiOmron.PcbCheck/Converters/ResultCodeToTagTextConverter.cs create mode 100644 src/AxiOmron.PcbCheck/DesignTime/DesignTimeAppConfigService.cs create mode 100644 src/AxiOmron.PcbCheck/DesignTime/DesignTimeAppStateStore.cs create mode 100644 src/AxiOmron.PcbCheck/DesignTime/DesignTimeDispatcherService.cs create mode 100644 src/AxiOmron.PcbCheck/DesignTime/DesignTimeViewModelLocator.cs create mode 100644 src/AxiOmron.PcbCheck/DesignTime/DesignTimeWorkflowControlService.cs create mode 100644 src/AxiOmron.PcbCheck/MainWindow.xaml create mode 100644 src/AxiOmron.PcbCheck/MainWindow.xaml.cs create mode 100644 src/AxiOmron.PcbCheck/Models/RuntimeSnapshot.cs create mode 100644 src/AxiOmron.PcbCheck/Models/WorkflowModels.cs create mode 100644 src/AxiOmron.PcbCheck/NLog.config create mode 100644 src/AxiOmron.PcbCheck/Options/AppConfig.cs create mode 100644 src/AxiOmron.PcbCheck/Services/Implementations/AndonService.cs create mode 100644 src/AxiOmron.PcbCheck/Services/Implementations/AppConfigService.cs create mode 100644 src/AxiOmron.PcbCheck/Services/Implementations/AppLogger.cs create mode 100644 src/AxiOmron.PcbCheck/Services/Implementations/AppStateStore.cs create mode 100644 src/AxiOmron.PcbCheck/Services/Implementations/DispatcherService.cs create mode 100644 src/AxiOmron.PcbCheck/Services/Implementations/ModbusTcpPlcService.cs create mode 100644 src/AxiOmron.PcbCheck/Services/Implementations/SerialScannerService.cs create mode 100644 src/AxiOmron.PcbCheck/Services/Implementations/SftpLookupService.cs create mode 100644 src/AxiOmron.PcbCheck/Services/Implementations/WorkflowHostedService.cs create mode 100644 src/AxiOmron.PcbCheck/Services/Interfaces/CoreInterfaces.cs create mode 100644 src/AxiOmron.PcbCheck/Utils/TextAndPatternHelpers.cs create mode 100644 src/AxiOmron.PcbCheck/ViewModels/MainWindowViewModel.cs create mode 100644 src/AxiOmron.PcbCheck/ViewModels/SystemSettingViewModel.cs create mode 100644 src/AxiOmron.PcbCheck/ViewModels/ViewModelLocator.cs create mode 100644 src/AxiOmron.PcbCheck/Views/Pages/DashboardPage.xaml create mode 100644 src/AxiOmron.PcbCheck/Views/Pages/DashboardPage.xaml.cs create mode 100644 src/AxiOmron.PcbCheck/Views/Pages/SystemSettingsPage.xaml create mode 100644 src/AxiOmron.PcbCheck/Views/Pages/SystemSettingsPage.xaml.cs create mode 100644 src/AxiOmron.PcbCheck/appConfig.Development.json create mode 100644 src/AxiOmron.PcbCheck/appConfig.json diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..433e36a --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,20 @@ +{ + "permissions": { + "allow": [ + "Bash(git:*)", + "Bash(printf '\\\\n---\\\\n')", + "Bash(dotnet new:*)", + "Bash(dotnet sln:*)", + "Bash(dotnet build:*)", + "Bash(dotnet add:*)", + "Bash(powershell -NoProfile -Command \"[Reflection.Assembly]::LoadFrom\\('C:/Users/yunxiao.zhu/.nuget/packages/iotclient/1.0.42/lib/netstandard2.0/IoTClient.dll'\\).GetTypes\\(\\) | Where-Object { $_.Name -like '*Modbus*' -or $_.FullName -like '*Modbus*' } | Select-Object -ExpandProperty FullName\")", + "Bash(powershell -NoProfile -Command '[Reflection.Assembly]::LoadFrom\\(\"C:/Users/yunxiao.zhu/.nuget/packages/iotclient/1.0.42/lib/netstandard2.0/IoTClient.dll\"\\).GetType\\(\"IoTClient.Clients.Modbus.ModbusTcpClient\"\\).GetMethods\\(\\) | Where-Object { $_.Name -in @\\(\"Connect\",\"ReadDiscrete\",\"Write\",\"ReadUInt16\"\\) } | ForEach-Object { $_.ToString\\(\\) }')", + "Bash(powershell -NoProfile -Command '$asm=[Reflection.Assembly]::LoadFrom\\(\"C:/Users/yunxiao.zhu/.nuget/packages/iotclient/1.0.42/lib/netstandard2.0/IoTClient.dll\"\\); $type=$asm.GetType\\(\"IoTClient.Clients.Modbus.ModbusTcpClient\"\\); $type.GetMethods\\([System.Reflection.BindingFlags]::Instance -bor [System.Reflection.BindingFlags]::Public -bor [System.Reflection.BindingFlags]::DeclaredOnly\\) | ForEach-Object { $_.ToString\\(\\) }')", + "Bash(powershell -NoProfile -Command '$asm=[Reflection.Assembly]::LoadFrom\\(\"C:/Users/yunxiao.zhu/.nuget/packages/iotclient/1.0.42/lib/netstandard2.0/IoTClient.dll\"\\); $type=$asm.GetType\\(\"IoTClient.Clients.Modbus.ModbusTcpClient\"\\); $type.GetInterfaces\\(\\) | ForEach-Object { $_.FullName }')", + "Bash(powershell -NoProfile -Command '$asm=[Reflection.Assembly]::LoadFrom\\(\"C:/Users/yunxiao.zhu/.nuget/packages/iotclient/1.0.42/lib/netstandard2.0/IoTClient.dll\"\\); $type=$asm.GetType\\(\"IoTClient.Clients.Modbus.ModbusTcpClient\"\\); \"BASE: $\\($type.BaseType.FullName\\)\"; $type.GetMethods\\([System.Reflection.BindingFlags]::Instance -bor [System.Reflection.BindingFlags]::Public\\) | Where-Object { $_.Name -match \"Connect|Close|Dispose|Disconnect|Open|Start|Stop|Release\" } | ForEach-Object { $_.DeclaringType.FullName + \" :: \" + $_.ToString\\(\\) }')", + "Bash(powershell -NoProfile -Command '$asm=[Reflection.Assembly]::LoadFrom\\(\"C:/Users/yunxiao.zhu/.nuget/packages/iotclient/1.0.42/lib/netstandard2.0/IoTClient.dll\"\\); $type=$asm.GetType\\(\"IoTClient.Clients.Modbus.ModbusTcpClient\"\\); $type.GetConstructors\\(\\) | ForEach-Object { $_.ToString\\(\\) }')", + "Read(//d D:/Dev/Codes/MFD_Solution/Axi_Omron/src/**)", + "Bash(dotnet restore:*)" + ] + } +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..77086e6 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,58 @@ + +[*.{c,c++,c++m,cc,ccm,cginc,compute,cp,cpp,cppm,cu,cuh,cxx,cxxm,fx,fxh,h,hh,hlsl,hlsli,hlslinc,hpp,hxx,inc,inl,ino,ipp,ixx,mpp,mq4,mq5,mqh,mxx,tpp,usf,ush}] +indent_style = tab +indent_size = tab +tab_width = 4 + +[*.{asax,ascx,aspx,axaml,cshtml,htm,html,master,paml,razor,skin,vb,xaml,xamlx,xoml}] +indent_style = space +indent_size = 4 +tab_width = 4 + +[*.{appxmanifest,axml,build,config,csproj,dbml,discomap,dtd,jsproj,lsproj,njsproj,nuspec,proj,props,resw,resx,StyleCop,targets,tasks,vbproj,xml,xsd}] +indent_style = space +indent_size = 2 +tab_width = 2 + +[*.cs] +indent_style = space +indent_size = 4 +tab_width = 4 +resharper_csharp_keep_multiple_properties_on_single_line = false +resharper_csharp_keep_multiple_accessors_on_single_line = false +[*] + +# Microsoft .NET properties +csharp_new_line_before_members_in_object_initializers = false +csharp_preferred_modifier_order = public, private, protected, internal, file, new, static, abstract, virtual, sealed, readonly, override, extern, unsafe, volatile, async, required:suggestion +csharp_style_prefer_utf8_string_literals = true:suggestion +csharp_style_var_elsewhere = true:suggestion +csharp_style_var_for_built_in_types = true:suggestion +csharp_style_var_when_type_is_apparent = true:suggestion +dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:none +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:none +dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:none +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion +dotnet_style_predefined_type_for_member_access = true:suggestion +dotnet_style_qualification_for_event = false:suggestion +dotnet_style_qualification_for_field = false:suggestion +dotnet_style_qualification_for_method = false:suggestion +dotnet_style_qualification_for_property = false:suggestion +dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion + +# ReSharper inspection severities +resharper_arrange_redundant_parentheses_highlighting = hint +resharper_arrange_this_qualifier_highlighting = hint +resharper_arrange_type_member_modifiers_highlighting = hint +resharper_arrange_type_modifiers_highlighting = hint +resharper_built_in_type_reference_style_for_member_access_highlighting = hint +resharper_built_in_type_reference_style_highlighting = hint +resharper_redundant_base_qualifier_highlighting = warning +resharper_suggest_var_or_type_built_in_types_highlighting = hint +resharper_suggest_var_or_type_elsewhere_highlighting = hint +resharper_suggest_var_or_type_simple_types_highlighting = hint + +# ReSharper properties +resharper_place_accessorholder_attribute_on_same_line = false +resharper_place_accessor_attribute_on_same_line = false +resharper_place_field_attribute_on_same_line = false diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..b1e57c4 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,19 @@ +# Normalize all text files to LF +# Normalize common .NET text files to CRLF +*.cs text eol=crlf +*.xaml text eol=crlf +*.csproj text eol=crlf +*.sln text eol=crlf +*.config text eol=crlf +*.json text eol=crlf +*.md text eol=crlf +*.pubxml text eol=crlf +*.targets text eol=crlf +*.props text eol=crlf +*.tt text eol=crlf + +# Default: leave unspecified types untouched +* -text + +# Do not normalize binary DLLs +*.dll binary diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b9a9d11 --- /dev/null +++ b/.gitignore @@ -0,0 +1,64 @@ +# .NET Core +bin/ +obj/ +*.user +*.userosscache +*.suo +*.userprefs + +# Visual Studio +.vs/ +.vscode/ +*.swp +*.*~ +project.lock.json +.DS_Store +*.pyc +nupkg/ + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio cache/options directory +.vs/ + +# SQLite database files +*.db +*.db-shm +*.db-wal + +# Log files +logs/ +*.log + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# NuGet +*.nupkg +*.snupkg +**/packages/* +!**/packages/build/ +*.nuget.props +*.nuget.targets +.packages/ +.dotnet/ +.worktrees/ +参考程序/* +.superpowers/* diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..c93022f --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,248 @@ +# AGENTS – C# WPF 上位机项目初始化规范 + +本文件为在本仓库中创建和维护 **新的 C# WPF 上位机项目** 的智能体提供统一操作规范,作用域覆盖整个仓库。 +若某个子目录存在更深层级的 `AGENTS.md`,则以更具体的规则为准。 + +## 适用说明 + +- 本仓库中的历史项目、样例项目、参考项目仅用于借鉴工程结构与协作方式,不视为新项目的默认业务实现。 +- 新项目初始化时,优先采用 **.NET 8 + WPF + MVVM Toolkit + Generic Host/DI + NLog** 这一默认技术基线。 +- 不要直接复制参考项目中的业务命名、设备协议、数据库模型、接口定义、配置项或目录命名,除非用户明确要求。 +- 若用户仅要求“初始化项目”或“生成上位机骨架”,默认理解为生成一个结构清晰、可扩展、适合工业桌面应用的 WPF 工程,而不是复制旧项目的业务代码。 + +## 推荐技术基线 + +- 平台:Windows +- SDK:.NET 8 SDK +- UI:WPF +- 项目文件:SDK-style `.csproj` +- 目标框架:`net8.0-windows` +- 语言特性:`enable`、`enable` +- MVVM:`CommunityToolkit.Mvvm` +- 宿主与依赖注入:`Microsoft.Extensions.Hosting` +- 日志:`NLog` + `NLog.Extensions.Logging` +- 配置:`appConfig.json` + `appConfig.{Environment}.json` +- 开发期敏感配置:`UserSecrets` 或环境变量 +- 测试:优先单独测试项目,推荐 xUnit;UI 难测逻辑应下沉到 ViewModel 或 Service + +## 新项目初始化骨架 + +若无额外约束,推荐使用如下仓库结构: + +```text +src/ + / + .csproj + App.xaml + App.xaml.cs + MainWindow.xaml + MainWindow.xaml.cs + appConfig.json + appConfig.Development.json + NLog.config + Assets/ + Models/ + ViewModels/ + Views/ + Pages/ + UserControls/ + Services/ + Interfaces/ + Implementations/ + Modules/ + Utils/ +tests/ + .Tests/ +``` + +约束如下: + +- `App.xaml` / `App.xaml.cs` 负责应用启动、Host 创建、全局异常处理、配置与日志装配。 +- `MainWindow.xaml` / `.cs` 只负责主窗口装配,不承载核心业务逻辑。 +- `ViewModels/` 保存可绑定状态、命令和页面协调逻辑。 +- `Views/Pages/`、`Views/UserControls/` 保存页面和可复用视图组件。 +- `Services/Interfaces/` 与 `Services/Implementations/` 用于硬件通信、文件处理、MES/PLC/条码/图像处理等业务服务。 +- `Modules/` 用于较独立的能力模块,例如数据库、设备协议封装、审计上传、报表导出等。 +- `Utils/` 仅保留轻量、通用、无业务语义的辅助工具;不要把主要业务逻辑塞入工具类。 +- `tests/` 必须与应用项目分离,避免把测试代码混入正式程序集。 + +若仓库当前只有单个应用项目,也可暂时不建 `src/` 目录,直接在仓库根下放置 `/`;但若无明确要求,优先采用 `src/ + tests/` 结构。 + +## 项目初始化最低要求 + +新建项目时,至少保证以下内容一次到位: + +- WPF 应用可以成功还原、编译、启动。 +- `App.xaml.cs` 中建立统一的 Host/DI 启动入口。 +- 主窗口与主要 ViewModel 通过依赖注入创建。 +- 日志系统已接入,且启动阶段异常可落日志。 +- 配置文件支持环境分层加载。 +- 输出目录包含 `appConfig.json`、`appConfig.Development.json`、`NLog.config`。 +- 为未来扩展预留 `Services`、`ViewModels`、`Views`、`Utils`、`Modules` 目录。 +- 若涉及设备通信、轮询、文件监控、批处理等后台任务,必须预先考虑取消、限流、异常记录与 UI 线程切换。 + +## 构建 / 还原 / 运行 / 发布 + +以下命令中的 `` 代表新建 WPF 项目名: + +- 创建解决方案:`dotnet new sln -n ` +- 创建 WPF 项目:`dotnet new wpf -n --framework net8.0` +- 还原:`dotnet restore src//.csproj` +- 调试构建:`dotnet build src//.csproj -c Debug` +- 发布构建:`dotnet build src//.csproj -c Release` +- 运行(UI):`dotnet run --project src//.csproj -c Debug` +- 发布示例:`dotnet publish src//.csproj -c Release -r win-x64 --self-contained false` + +初始化或修改 `.csproj` 时,至少保持以下属性: + +- `true` +- `net8.0-windows` +- `enable` +- `enable` + +若项目包含配置、日志、资源或字典文件,需要同步设置 `CopyToOutputDirectory`,确保运行目录完整。 + +## 测试 / 验证 + +- 新项目默认应预留独立测试项目:`tests/.Tests/`。 +- 优先测试 ViewModel、Service、配置装配、解析逻辑、队列/调度逻辑,不要把核心逻辑压在难以测试的 code-behind 中。 +- 若功能与 UI 强绑定,应先抽离为接口或服务,再进行单元测试或集成测试。 +- 若暂未建立测试项目,至少执行一次构建验证,并手动完成关键 UI 流程检查。 +- 单元测试示例:`dotnet test --filter FullyQualifiedName~命名空间.类名.方法名` +- 提交前最低要求:`dotnet build` 成功;涉及关键逻辑时应补充对应测试。 + +## Lint / 格式 + +- 若仓库尚未提供统一格式化工具或 `.editorconfig`,保持现有风格一致,不擅自引入新的格式化方案。 +- 保持 using 简洁,移除未使用引用。 +- 使用文件作用域命名空间,除非现有项目明确不采用。 +- 避免只为“看起来更优雅”而大范围重排代码格式。 + +## XML 文档注释规则 + +- 所有 `public` / `internal` / `protected` 类、接口、记录、方法、属性必须有 **中文 XML 文档注释**。 +- 私有成员若逻辑不简单,或方法体超过 10 行,也应补充 XML 注释。 +- 方法注释必须包含: + - `` + - 每个参数的 `` + - 有返回值时的 `` + - 可能抛出异常时的 `` +- 异步方法名必须以 `Async` 结尾。 +- 若方法接收 `CancellationToken`,需要在注释中写明取消行为及影响。 +- 涉及命令、绑定、Dispatcher、后台线程、Task.Run、定时器、轮询、文件监控时,注释必须明确线程模型和触发行为。 +- 已有 XML 注释不得删除;修改实现后必须同步更新注释内容。 + +## 代码风格(通用 C#) + +- 命名空间使用文件作用域写法。 +- 可读性优先:类型不明显时使用显式类型,明显时可使用 `var`。 +- 字段优先使用 `private readonly`,字段命名采用 `_camelCase`。 +- 常量使用 `const PascalCase`。 +- 参数校验只放在系统边界:用户输入、外部接口、文件内容、网络返回、硬件返回等位置。 +- 不要为当前需求之外的场景做预留抽象。 +- 不要新增只被调用一次的帮助类、工具类或接口。 +- 不要为了兼容历史命名而保留无意义的中间层,确认无用即可删除。 + +## MVVM / WPF 实践 + +- 业务逻辑放在 ViewModel 或 Service,code-behind 仅做视图装配、事件桥接、生命周期对接。 +- 命令优先使用 `RelayCommand` / `AsyncRelayCommand`。 +- 可绑定属性优先使用 `[ObservableProperty]` 生成,不手写重复样板代码。 +- 长耗时操作必须使用 `async/await`,不得阻塞 UI 线程。 +- 后台线程不得直接更新绑定到 UI 的对象;涉及 UI 更新时必须切回 Dispatcher。 +- 不要在 View 中直接 `new` 业务服务。 +- 不要在 code-behind 中直接访问数据库、文件系统、MES、PLC、串口、相机或网络接口。 +- 页面切换、对话框协调、状态共享应优先通过 ViewModel 或应用级服务完成。 +- 若项目包含托盘、单实例、后台常驻、自动重连、设备轮询等能力,应将其建模为显式服务并通过 DI 管理生命周期。 + +## 宿主 / DI / 启动约定 + +- 在 `App.xaml.cs` 中集中创建 `HostApplicationBuilder` 或等价 Host 对象。 +- 所有服务、ViewModel、Window、Page 的注册集中管理,不要分散在多个随机文件中。 +- 优先使用构造函数注入,不使用服务定位器模式。 +- 应用启动顺序应清晰:配置加载 → 日志初始化 → Host 构建 → 服务注册 → 主窗口创建与显示。 +- 若涉及全局异常捕获、未观察任务异常、UI 线程异常,应在启动阶段完成统一挂接。 +- 若项目需要单实例约束,应采用明确、可维护的单实例方案,并保证启动顺序可追踪。 + +## 日志 / 异常 / 用户提示 + +- 注入 `ILogger` 进行日志记录,优先使用结构化日志。 +- 记录错误时使用包含异常对象的重载,例如:`_logger.LogError(ex, "消息 {上下文}", value)`。 +- 不要静默吞异常;至少按 Debug / Warning / Error 级别记录。 +- 对用户可感知的失败,要同时提供用户友好提示与详细日志。 +- 对启动失败、配置错误、设备离线、文件访问失败、网络错误等场景,应明确记录上下文。 +- 对于可恢复错误,应在日志中记录恢复动作;对于不可恢复错误,应阻止继续执行并给出清晰提示。 + +## 配置与环境管理 + +- 配置文件默认采用: + - `appConfig.json` + - `appConfig.Development.json` + - 如有必要再增加 `appConfig.Production.json`、`appConfig.Local.json` 等 +- 运行环境通过 `DOTNET_ENVIRONMENT` 或 `ASPNETCORE_ENVIRONMENT` 识别,默认 `Production`。 +- 不要硬编码密钥、口令、连接串、IP、账号。 +- 开发期敏感配置优先使用 `UserSecrets` 或环境变量。 +- 新增配置文件、资源文件、字典文件时,必须同步 `.csproj` 的输出复制规则。 +- 读取配置时优先建立强类型 Options 或清晰的数据模型,不散落魔法字符串。 + +## 集合、并发与 I/O + +- 文件处理优先使用流式 API,如 `Directory.EnumerateFiles`,避免一次性加载大量文件。 +- 批处理、轮询、文件监控、设备通信等后台任务需具备可取消性。 +- 对共享状态使用明确的并发策略:`SemaphoreSlim`、`ConcurrentQueue`、`ConcurrentDictionary`、Channel 或串行队列。 +- 不要锁住 UI 线程等待后台结果。 +- 对缓冲区、日志列表、待处理队列要设置上限,避免内存无限增长。 +- 所有后台任务都必须定义“谁创建、谁取消、谁记录异常、谁负责收尾”。 + +## 命名约定 + +- 类 / 结构 / 记录 / 枚举:`PascalCase` +- 接口:`I` 前缀 + `PascalCase` +- 方法 / 属性 / 事件:`PascalCase` +- 私有字段:`_camelCase` +- 局部变量 / 参数:`camelCase` +- 命令:`*Command` +- 异步方法:`*Async` +- 配置类型名应与配置节含义一致,不使用模糊命名如 `ConfigHelper`、`DataManager`、`CommonUtil` + +## 新项目初始化完成的判定标准 + +一个新建 WPF 上位机项目,至少达到以下状态才可认为“初始化完成”: + +- 可以成功 `restore`、`build`、`run` +- 主窗口可以正常显示 +- Host / DI 已接入,主窗口和主 ViewModel 不依赖手工拼装 +- 配置文件可被读取,环境分层生效 +- 日志文件或日志输出可验证 +- 核心目录结构已建立 +- 至少保留一个可扩展的 Service、一个可扩展的 ViewModel、一个基础页面或主窗口示例 +- 若已经接入设备/文件监听/后台任务,则具备最基本的取消与异常记录机制 + +## 贡献检查清单 + +- 是否遵循本文件规定的初始化骨架与技术基线。 +- 是否保持业务逻辑不进入 code-behind。 +- 是否为公开成员补齐中文 XML 注释。 +- 是否已接入 Host/DI、配置和日志。 +- 是否移除未使用 using、无意义抽象和一次性工具层。 +- 是否避免硬编码敏感信息。 +- 是否在涉及后台任务时明确线程切换、取消与异常处理。 +- 是否为新增配置/资源设置输出复制规则。 +- 是否在提交前至少执行一次 `dotnet build`。 + +## 快速命令速查 + +- 新建解决方案:`dotnet new sln -n ` +- 新建 WPF 项目:`dotnet new wpf -n --framework net8.0` +- 添加到解决方案:`dotnet sln add src//.csproj` +- 还原:`dotnet restore src//.csproj` +- 调试构建:`dotnet build src//.csproj -c Debug` +- 发布构建:`dotnet build src//.csproj -c Release` +- 运行调试:`dotnet run --project src//.csproj -c Debug` +- 发布示例:`dotnet publish src//.csproj -c Release -r win-x64 --self-contained false` +- 单测示例:`dotnet test --filter FullyQualifiedName~命名空间.类名.方法名` + +## 额外说明 + +- 如果后续在某个新项目目录下生成了更具体的 `AGENTS.md`,应把该项目特有的设备协议、数据库约束、部署方式、配置项和 UI 行为写入子级文档,而不是污染仓库级规则。 +- 仓库级 `AGENTS.md` 应保持“可复用、可初始化、不过度绑定具体业务”的定位。 diff --git a/AxiOmron.PcbCheck.slnx b/AxiOmron.PcbCheck.slnx new file mode 100644 index 0000000..a32a5fc --- /dev/null +++ b/AxiOmron.PcbCheck.slnx @@ -0,0 +1,2 @@ + + diff --git a/docs/2026-04-16-pcb-check-flow-design.md b/docs/2026-04-16-pcb-check-flow-design.md new file mode 100644 index 0000000..1209665 --- /dev/null +++ b/docs/2026-04-16-pcb-check-flow-design.md @@ -0,0 +1,897 @@ +# PCB 目检软件需求与方案设计 + +## 1. 文档目的 + +本文档用于整理当前 PCB 简易目检上位机的软件需求、业务流程、通信接口、异常处理和 Modbus TCP 点位设计,作为后续开发、联调和现场验收的依据。 + +当前版本仅覆盖以下能力: + +- PLC 到位信号接入 +- 串口触发扫码枪扫码 +- SFTP 文件存在性校验 +- PLC 放行信号回写 +- HTTP 安灯报警接口调用 +- 关键过程日志与状态记录 + +本文档不包含以下能力: + +- 相机采图 +- 图像算法判定 +- 人工复判 +- 多工位并行处理 +- MES/WMS 深度集成 + +--- + +## 2. 系统范围与边界 + +### 2.1 系统角色 + +系统由以下四个外部对象和一个上位机应用组成: + +1. **PLC** + - 通过 Modbus TCP 与上位机通信 + - 提供 PCB 到位、复位、运行允许等状态 + - 接收上位机放行、流程完成、故障/报警等结果信号 + +2. **扫码枪** + - 通过串口与上位机通信 + - 接收上位机触发扫描指令 + - 返回二维码内容或超时失败 + +3. **SFTP 服务器** + - 用于存放与 PCB 二维码 ID 对应的判定文件或放行文件 + - 上位机根据扫码结果访问指定目录并查找目标文件 + +4. **安灯系统** + - 通过 HTTP 接口接收报警请求 + - 用于扫码失败等异常场景的现场告警 + +5. **上位机软件** + - 负责流程编排、设备通信、状态机控制、日志记录、异常处理和 PLC 结果回写 + +### 2.2 运行约束 + +- 运行模式为**单件串行处理** +- 同一时刻只处理一块 PCB +- 上位机作为 **Modbus TCP Client** +- PLC 作为 **Modbus TCP Server** +- 扫码最多尝试 **3 次(含首次触发)** +- SFTP 采用 **1 次首次查询 + 最多 N 次重试** 的方式检查目标文件 +- 扫码失败 3 次后仍 **放行**,同时触发安灯报警 +- SFTP 文件最终未找到时仍 **放行**,默认仅记录日志,不强制安灯报警 +- SFTP 连接失败、认证失败、目录配置错误等**系统级异常**不等同于“文件不存在”,默认进入系统故障,不自动放行 + +--- + +## 3. 需求整理 + +### 3.1 功能需求 + +#### 3.1.1 到位触发 + +- 当 PCB 到达指定工位后,PLC 通过 Modbus TCP 点位通知上位机“PCB 已到位” +- 上位机在检测到到位信号后,启动本次单板处理流程 +- 上位机应避免同一块板被重复触发处理 + +#### 3.1.2 扫码控制 + +- 上位机通过串口向扫码枪发送触发命令 +- 扫码枪返回二维码字符串 +- 如果单次扫码失败,上位机允许继续重试 +- 当前版本最大尝试次数固定为 **3 次(含首次触发)** + +#### 3.1.3 扫码失败处理 + +- 若连续 3 次扫码失败: + - 上位机调用 HTTP 安灯报警接口 + - 上位机记录失败原因和报警结果 + - 上位机仍向 PLC 发送放行信号 + - 本次流程结果标记为“扫码失败放行” + +#### 3.1.4 SFTP 文件校验 + +- 扫码成功后,上位机根据二维码 ID 到配置好的 SFTP 目录中查找对应文件 +- 当前版本采用 **首次查询 1 次 + 文件未命中后最多重试 N 次** 的规则,`N` 为可配置项 +- 若文件存在: + - 视为校验通过 + - 上位机立即发送放行信号给 PLC +- 若文件不存在: + - 等待 X 秒后再次检查 + - 达到重试上限后按“文件未找到超时处理”执行 + +#### 3.1.5 文件未找到超时处理 + +- 若达到 SFTP 查询重试上限后仍未找到对应文件: + - 上位机记录“文件未找到超时放行” + - 上位机仍向 PLC 发送放行信号 + - 默认不强制调用安灯接口,但应保留后续扩展为可配置报警策略的设计空间 + +#### 3.1.6 二维码内容处理 + +- 二维码字符串在用于 SFTP 查询前,应先执行基础清洗: + - 去除首尾空白字符 + - 去除回车换行等控制字符 +- 清洗后若为空字符串,则视为本次扫码失败 +- 当前版本不增加更复杂的正则或长度业务校验,避免引入未确认的规则 + +#### 3.1.7 PLC 交互 + +- 上位机需向 PLC 提供以下反馈能力: + - 忙碌状态 + - 当前流程完成状态 + - 放行信号 + - 扫码成功/失败状态 + - 文件存在/不存在状态 + - 故障或报警状态 + - 异常代码/结果代码 + +#### 3.1.8 日志与追溯 + +- 上位机应记录以下关键日志: + - 到位触发时间 + - 扫码每次尝试结果 + - 最终二维码内容 + - SFTP 连接与查找结果 + - HTTP 报警调用结果 + - PLC 点位写入动作 + - 本次流程最终结果 + +### 3.2 非功能需求 + +- 支持稳定连续运行 +- 所有外部接口都应具备超时控制 +- 所有异常都必须记录日志,不允许静默吞掉 +- 同一时刻只能处理一块 PCB,避免并发混板 +- 配置项应集中管理,避免硬编码 IP、端口、账号、目录和重试参数 + +--- + +## 4. 推荐总体方案 + +### 4.1 总体架构 + +推荐采用**单工位串行状态机架构**。 + +该方案由一个主流程控制器串联以下模块: + +- PLC 通信模块 +- 扫码枪控制模块 +- SFTP 校验模块 +- 安灯报警模块 +- 流程状态机模块 +- 日志与结果记录模块 + +### 4.2 方案特点 + +- 适合当前“单件串行”场景 +- 逻辑清晰,便于现场联调 +- 点位数量可控,便于 PLC 程序实现 +- 后续可在不推翻主体结构的前提下扩展更多结果码、报警策略和 UI 状态展示 + +### 4.3 不推荐的当前方案 + +当前阶段不建议直接采用多任务队列、多工位并行或由 PLC 承担大部分业务编排的重型架构,因为会显著增加联调复杂度,不符合“简单目检软件”的目标。 + +--- + +## 5. 软件模块设计 + +### 5.1 流程控制模块 + +负责: + +- 接收 PLC 到位信号 +- 驱动整个业务状态机流转 +- 管理当前板的处理上下文 +- 决定何时触发扫码、何时检查文件、何时报警、何时放行 + +建议职责边界: + +- 一个流程控制器只负责一块当前 PCB +- 所有外部服务都由流程控制器统一调度 +- 不允许 PLC、扫码、SFTP、报警模块彼此直接耦合调用 + +### 5.2 PLC 通信模块 + +负责: + +- 周期性读取 PLC 点位 +- 写入上位机结果点位 +- 做好写脉冲、保持位和复位逻辑 +- 管理断线重连和通信状态 + +建议接口: + +- `ReadSignalsAsync()`:读取 PLC 输入状态 +- `WriteHandshakeAsync()`:写入握手和结果点位 +- `PulseReleaseAsync()`:发送放行脉冲 +- `ResetResultBitsAsync()`:清理本次流程结果位 + +### 5.3 扫码枪控制模块 + +负责: + +- 管理串口打开、关闭、重连 +- 发送扫码命令 +- 接收扫码返回值 +- 执行单次扫码超时控制 +- 将扫码结果以统一对象返回给流程控制器 + +建议输出统一结果: + +- 是否成功 +- 扫码内容 +- 原始返回报文 +- 失败原因 +- 本次耗时 + +### 5.4 SFTP 校验模块 + +负责: + +- 根据配置建立 SFTP 连接 +- 按二维码 ID 查找目标文件 +- 支持按目录、文件名模板或通配方式查找 +- 执行重试、等待和超时控制 +- 输出最终查找结果 + +建议支持的配置项: + +- 服务器地址 +- 端口 +- 用户名 +- 密码/密钥 +- 根目录 +- 文件名匹配规则 +- 单次连接超时 +- 查询等待秒数 +- 最大重试次数 + +### 5.5 安灯报警模块 + +负责: + +- 调用 HTTP 安灯接口 +- 发送报警编码、报警内容、工位号、二维码等参数 +- 处理响应结果并记录日志 + +建议支持的配置项: + +- 接口 URL +- 请求方法 +- 请求头 +- 超时时间 +- 报警编码 +- 工位名称 +- 是否启用扫码失败报警 +- 是否启用文件未找到报警(预留) + +### 5.6 日志与结果记录模块 + +负责: + +- 写入运行日志 +- 保存每板处理结果摘要 +- 输出 UI 可显示的当前状态、结果和错误信息 + +建议记录字段: + +- 触发时间 +- 完成时间 +- 条码内容 +- 扫码次数 +- SFTP 重试次数 +- 最终结果代码 +- 最终结果描述 +- 是否调用安灯 +- PLC 放行发送时间 + +--- + +## 6. 业务状态机设计 + +### 6.1 状态定义 + +建议定义以下流程状态: + +1. **Idle**:空闲,等待 PLC 到位 +2. **Triggered**:接收到到位信号,准备启动流程 +3. **Scanning**:正在触发扫码枪扫码 +4. **ScanRetrying**:扫码失败,等待下一次扫码尝试 +5. **ScanFailedReleased**:扫码失败 3 次,已报警并决定放行 +6. **CheckingSftp**:正在检查 SFTP 文件是否存在 +7. **WaitingSftpRetry**:文件未找到,等待下一次轮询 +8. **SftpPassed**:文件找到,允许放行 +9. **SftpTimeoutReleased**:文件未找到超时,决定放行 +10. **Releasing**:向 PLC 发送放行信号 +11. **Completed**:本次流程结束 +12. **Faulted**:出现系统级故障,如 PLC 通信异常、配置错误、串口不可用、SFTP 连接异常等 + +### 6.2 状态流转 + +#### 正常流转 + +`Idle -> Triggered -> Scanning -> CheckingSftp -> SftpPassed -> Releasing -> Completed -> Idle` + +#### 扫码失败放行流转 + +`Idle -> Triggered -> Scanning -> ScanRetrying -> Scanning -> ScanRetrying -> Scanning -> ScanFailedReleased -> Releasing -> Completed -> Idle` + +#### 文件未找到超时放行流转 + +`Idle -> Triggered -> Scanning -> CheckingSftp -> WaitingSftpRetry -> CheckingSftp -> ... -> SftpTimeoutReleased -> Releasing -> Completed -> Idle` + +#### 系统故障流转 + +`任意流程态 -> Faulted -> 人工复位/PLC复位 -> Idle` + +### 6.3 状态机约束 + +- 只有 `Idle` 状态才允许接收新的 PCB 到位触发 +- 系统进入 `Faulted` 后,不允许自动接收下一块板,必须在故障恢复后由人工复位或 PLC 复位解除 +- 允许后台自动执行断线重连,但在 `Faulted` 未解除前不得恢复业务处理 +- 每次开始新流程前必须清理上一板的过程结果位 +- `Release` 动作必须具备防重复发送保护 + +### 6.4 启动前置条件 + +系统只有在以下条件同时满足时,才视为处于可接板的 `Idle` 状态: + +- PLC 通信已建立 +- `PlcReady = 1` +- `AutoMode = 1` +- `StationEnable = 1` +- 上位机不存在未解除的 `SystemFault` + +若上述条件不满足,上位机仅保持监视,不启动新板流程。 + +--- + +## 7. 详细流程说明 + +### 7.1 主流程 + +1. 上位机轮询 PLC 到位点位 +2. 当检测到“到位”且当前状态为 `Idle` 时: + - 记录开始时间 + - 置 Busy 位 + - 清理上次结果位 + - 写入当前流程状态码 + - 进入扫码流程 +3. 扫码成功后,进入 SFTP 校验流程 +4. 根据 SFTP 结果决定立即放行或超时后放行 +5. 发送放行信号给 PLC +6. 置流程完成位,并将最终结果码保持为稳定值 +7. 等待 PLC 应答或到位信号撤销 +8. 回到空闲状态 + +### 7.2 扫码流程 + +1. 发送扫码枪触发指令 +2. 等待扫码结果,单次扫码应设置超时 +3. 对扫码结果进行基础清洗:去掉空白和控制字符 +4. 若成功: + - 保存二维码内容 + - 清除 `ScanNg` + - 置 `ScanOk` + - 写入当前流程状态码 + - 进入 SFTP 校验流程 +5. 若失败: + - 累加尝试次数 + - 记录日志 + - 若未达到最大尝试次数,则进入下一轮扫码 +6. 达到 3 次后仍失败: + - 清除 `ScanOk` + - 置 `ScanNg` + - 调用安灯报警 + - 写入结果代码“扫码失败放行” + - 进入放行流程 + +说明: + +- `ScanNg` 表示**最终扫码失败**,不用于表示中间某一次扫码失败 +- 当前版本“3 次”定义为**总共最多 3 次尝试**,不是“首次 1 次 + 额外重试 3 次” + +### 7.3 SFTP 校验流程 + +1. 按配置建立 SFTP 连接 +2. 根据二维码 ID 拼接目标文件名或查找规则 +3. 立即执行首次查询 +4. 若文件存在: + - 清除 `FileNotFound` + - 置 `FileFound` + - 写入结果代码“文件存在放行” + - 进入放行流程 +5. 若文件不存在: + - 记录当前查询未命中 + - 若未达到重试上限,则等待 X 秒再次查询 + - 若达到重试上限,则清除 `FileFound`、置 `FileNotFound`、写入结果代码“文件未找到超时放行”,进入放行流程 +6. 若出现 SFTP 连接失败、认证失败、目录不存在等系统级异常: + - 置 `SystemFault` + - 写入故障结果码 + - 进入 `Faulted` + +说明: + +- 当前版本 `MaxRetryCount = N` 表示**首次未命中后的最多重试次数** +- 因此总查询次数为 **1 + N** +- `FileNotFound` 表示**最终未找到**,不用于表示中间某次查询未命中 + +### 7.4 放行流程 + +1. 上位机向 PLC 写入放行请求位 `ReleasePermit` +2. 推荐采用**脉冲方式**输出放行信号,默认脉冲时长建议为 **500ms**,可配置 +3. 同时写入最终结果码、流程完成位和稳定的状态位 +4. 若 PLC 提供 `PlcAckRelease`: + - 上位机等待 PLC 应答 + - 最长等待时间建议为 **2000ms**,超时则记录告警并自动清除放行脉冲 +5. 若 PLC 不提供 `PlcAckRelease`: + - 上位机保持 `ReleasePermit` 至配置脉冲时长结束后自动清除 +6. `ProcessDone = 1` 时,表示 `ResultCode`、`AlarmCode`、`ScanTryCount`、`SftpTryCount` 均已为最终稳定值 + +--- + +## 8. Modbus TCP 通信设计 + +### 8.1 通信角色 + +- 上位机:Modbus TCP Client +- PLC:Modbus TCP Server +- 轮询周期建议:100ms ~ 300ms +- 布尔量优先使用位点位,结果码优先使用 Holding Register + +### 8.2 点位设计原则 + +- 输入点和输出点职责分离 +- 过程状态位与最终结果码同时保留 +- 对 PLC 需要快速判断的信号,优先给单独布尔位 +- 对上位机 UI 和日志需要详细表达的结果,使用数值结果码补充 +- 放行信号使用脉冲,忙碌位和完成位可使用保持方式 +- 本文档采用**PLC -> 上位机为 Discrete Input、上位机 -> PLC 为 Coil** 的常规表达方式;若现场 PLC 地址区定义不同,可在实施阶段做地址映射,不改变信号语义 + +### 8.3 PLC -> 上位机点位(建议) + +以下点位由 PLC 提供,上位机读取: + +| 序号 | 地址类型 | 地址 | 点位名称 | 方向 | 说明 | +| --- | --- | --- | --- | --- | --- | +| 1 | Discrete Input | 10001 | PlcReady | PLC -> PC | PLC 就绪,允许上位机参与流程 | +| 2 | Discrete Input | 10002 | PcbArrived | PLC -> PC | PCB 已到位,请求上位机处理 | +| 3 | Discrete Input | 10003 | PlcReset | PLC -> PC | PLC 请求上位机清状态/复位 | +| 4 | Discrete Input | 10004 | PlcAckRelease | PLC -> PC | PLC 已接收到放行信号 | +| 5 | Discrete Input | 10005 | AutoMode | PLC -> PC | 设备当前处于自动模式 | +| 6 | Discrete Input | 10006 | StationEnable | PLC -> PC | 当前工位使能 | + +说明: + +- `PcbArrived` 为本流程主触发点 +- `PlcAckRelease` 为可选应答点,若 PLC 侧不需要可取消 +- `PlcReset` 用于人工清故障、恢复空闲态或清除保持位 +- `10007 ~ 10050` 预留给后续 PLC -> 上位机扩展信号 + +### 8.4 上位机 -> PLC 点位(建议) + +以下点位由上位机写入,PLC 读取: + +| 序号 | 地址类型 | 地址 | 点位名称 | 方向 | 说明 | +| --- | --- | --- | --- | --- | --- | +| 1 | Coil | 00051 | PcOnline | PC -> PLC | 上位机在线心跳位 | +| 2 | Coil | 00052 | PcBusy | PC -> PLC | 上位机正在处理当前 PCB | +| 3 | Coil | 00053 | ScanOk | PC -> PLC | 本次最终扫码成功 | +| 4 | Coil | 00054 | ScanNg | PC -> PLC | 本次最终扫码失败 | +| 5 | Coil | 00055 | FileFound | PC -> PLC | 最终找到对应 SFTP 文件 | +| 6 | Coil | 00056 | FileNotFound | PC -> PLC | 到达重试上限后仍未找到文件 | +| 7 | Coil | 00057 | AlarmRaised | PC -> PLC | 本次流程已触发安灯报警 | +| 8 | Coil | 00058 | ReleasePermit | PC -> PLC | 放行脉冲信号 | +| 9 | Coil | 00059 | ProcessDone | PC -> PLC | 本次流程已结束,结果码稳定 | +| 10 | Coil | 00060 | SystemFault | PC -> PLC | 上位机系统故障 | + +说明: + +- `ScanOk/ScanNg` 互斥 +- `FileFound/FileNotFound` 互斥 +- `ReleasePermit` 采用脉冲输出,不建议长时间保持 +- `ProcessDone = 1` 表示寄存器中的结果值已经稳定,PLC 可以在此时读取 `ResultCode` +- `AlarmRaised` 建议保持到下一板开始或收到 `PlcReset` 后再清除 +- `SystemFault` 用于表示上位机自身流程无法继续,例如 PLC 断连、串口异常、配置缺失、SFTP 连接异常等系统级故障 + +### 8.5 心跳建议 + +- `PcOnline` 不建议常亮,建议采用**翻转心跳**方式 +- 上位机每 **500ms** 翻转一次 `PcOnline` +- PLC 若在 **3 秒** 内未检测到该位变化,则可判定上位机离线或通信异常 + +### 8.6 结果寄存器设计(建议) + +建议额外使用 Holding Register 表达结果码和统计值。 + +| 序号 | 地址类型 | 地址 | 名称 | 说明 | +| --- | --- | --- | --- | --- | +| 1 | Holding Register | 40001 | ResultCode | 本次最终结果代码 | +| 2 | Holding Register | 40002 | ScanTryCount | 本次扫码尝试次数 | +| 3 | Holding Register | 40003 | SftpTryCount | 本次 SFTP 查询次数 | +| 4 | Holding Register | 40004 | AlarmCode | 本次报警代码,未报警时为 0 | +| 5 | Holding Register | 40005 | FlowStateCode | 当前流程状态码 | + +### 8.7 推荐结果代码定义 + +| 代码 | 含义 | +| --- | --- | +| 0 | Idle / 无结果 | +| 1 | 处理中 | +| 10 | 扫码成功,文件存在,正常放行 | +| 20 | 扫码失败 3 次后放行 | +| 30 | 扫码成功,文件未找到超时放行 | +| 40 | PLC 通信异常 | +| 41 | 串口异常 | +| 42 | SFTP 连接或认证异常 | +| 43 | 安灯接口调用异常 | +| 44 | 配置异常 | + +### 8.8 推荐流程状态码定义 + +| 代码 | 含义 | +| --- | --- | +| 0 | Idle | +| 1 | Triggered | +| 2 | Scanning | +| 3 | ScanRetrying | +| 4 | CheckingSftp | +| 5 | WaitingSftpRetry | +| 6 | Releasing | +| 7 | Completed | +| 8 | Faulted | + +### 8.9 点位清理策略 + +建议按如下规则清理点位: + +- 新板开始前清理:`ScanOk`、`ScanNg`、`FileFound`、`FileNotFound`、`AlarmRaised`、`ProcessDone` +- 流程处理中保持:`PcBusy` +- 放行时脉冲输出:`ReleasePermit` +- 流程结束后清理:`PcBusy` +- `PcOnline` 由心跳任务周期性翻转 +- `SystemFault` 在故障解除且收到人工复位或 `PlcReset` 后清除 + +--- + +## 9. 串口扫码设计建议 + +### 9.1 配置项 + +建议配置以下参数: + +- 串口号 +- 波特率 +- 数据位 +- 校验位 +- 停止位 +- 单次扫码超时毫秒数 +- 触发命令 +- 结束符/返回报文规则 + +### 9.2 抽象接口建议 + +扫码服务建议统一为: + +- `TriggerScanAsync()` +- 返回 `ScanResult` + +`ScanResult` 建议包含: + +- `IsSuccess` +- `Barcode` +- `RawMessage` +- `ErrorMessage` +- `DurationMs` + +### 9.3 行为建议 + +- 串口打开失败时立即记录系统故障 +- 每次扫码都必须单独设置超时 +- 返回空字符串、格式错误或无结束符都视为失败 +- 对扫码值仅做基础清洗,不额外加入未确认的业务规则 + +--- + +## 10. SFTP 文件查找设计建议 + +### 10.1 查找策略 + +建议支持以下任一方式,由配置指定: + +1. **完整文件名匹配** + - 例如:`${barcode}.txt` +2. **前缀/后缀匹配** + - 例如:`${barcode}_OK.xml` +3. **目录内模糊匹配** + - 例如:包含二维码 ID 的文件名即视为命中 + +初版推荐采用**完整文件名匹配**,规则最清晰,误判概率最低。 + +### 10.2 条码到文件名映射 + +- 当前版本以**清洗后的二维码字符串**作为查询键 +- 推荐默认文件名规则为:`${barcode}.txt` +- 实际文件名后缀或模板通过配置项 `FileNamePattern` 指定 +- 若后续现场规则变更,可通过配置调整,不修改流程主逻辑 + +### 10.3 配置项 + +- `Host` +- `Port` +- `Username` +- `Password` 或私钥 +- `RootPath` +- `FileNamePattern` +- `RetryIntervalSeconds` +- `MaxRetryCount` +- `ConnectTimeoutMs` + +### 10.4 行为建议 + +- 每次查询都要记录当前第几次尝试 +- 若 SFTP 连接失败,应区分“连接失败”和“文件不存在” +- 对于连接失败、认证失败、目录错误,不按“文件不存在超时放行”处理,而按系统异常处理 +- 若目录配置错误,应作为配置异常处理 + +--- + +## 11. 安灯 HTTP 接口设计建议 + +### 11.1 报警触发场景 + +当前版本建议至少在以下场景调用安灯接口: + +- 扫码连续失败 3 次 + +后续可扩展场景: + +- SFTP 文件未找到超时 +- PLC 通信异常 +- 串口设备离线 + +### 11.2 请求内容建议 + +建议请求体包含: + +- 工位编码 +- 工位名称 +- 报警类型 +- 报警编码 +- 报警描述 +- 二维码内容(若有) +- 触发时间 +- 设备名/IP + +### 11.3 报警代码建议 + +| 代码 | 含义 | +| --- | --- | +| 1001 | 扫码连续失败 3 次 | +| 1002 | SFTP 文件超时未找到 | +| 1003 | SFTP 连接异常 | +| 1004 | 串口设备异常 | +| 1005 | PLC 通信异常 | + +### 11.4 设计要求 + +- HTTP 请求必须有超时控制 +- 请求失败必须记录日志 +- 若报警接口失败,不影响当前“放行”主流程,但需在日志和结果码中体现 + +--- + +## 12. 配置设计建议 + +建议在配置文件中划分如下配置节: + +### 12.1 PlcOptions + +- IP 地址 +- 端口 +- 轮询周期 +- 连接超时 +- 点位地址映射 +- 放行脉冲时长 +- 放行应答超时时间 + +### 12.2 ScannerOptions + +- 串口参数 +- 触发命令 +- 单次扫码超时 +- 最大扫码次数(当前固定建议值为 3) + +### 12.3 SftpOptions + +- 服务器连接参数 +- 根目录 +- 文件匹配规则 +- 重试等待秒数 +- 最大重试次数 + +### 12.4 AndonOptions + +- 是否启用 +- 接口地址 +- 请求方式 +- 超时时间 +- 工位编码 +- 默认报警码映射 + +### 12.5 WorkflowOptions + +- 启动前是否要求 `PlcReady = 1` +- 启动前是否要求自动模式 +- 故障恢复后是否必须人工复位 +- 日志保留天数 + +说明: + +- 当前版本“扫码失败 3 次后放行”和“文件未找到超时后放行”属于**固定业务规则**,不作为普通配置开关 +- 若未来需要改成拦截模式,应通过需求变更单独评审 + +--- + +## 13. UI 展示建议 + +当前系统虽为简化版,仍建议主界面至少展示以下信息: + +- PLC 连接状态 +- 扫码枪连接状态 +- SFTP 连接状态 +- 安灯接口状态 +- 当前流程状态 +- 当前二维码内容 +- 扫码次数 +- SFTP 查询次数 +- 当前结果描述 +- 最近若干条运行日志 + +建议增加以下按钮: + +- 手动复位 +- 手动重连 PLC +- 手动重连扫码枪 +- 测试安灯接口 +- 打开配置 + +--- + +## 14. 异常处理策略 + +### 14.1 业务异常 + +业务异常指当前板流程内的可接受结果: + +- 扫码失败 3 次 +- 文件未找到超时 + +处理原则: + +- 记录日志 +- 写入明确结果码 +- 当前版本仍放行 + +### 14.2 系统异常 + +系统异常指上位机自身能力不可用: + +- PLC 无法连接 +- 串口打开失败 +- SFTP 配置缺失或无法认证 +- SFTP 根目录不存在 +- 安灯接口配置错误 + +处理原则: + +- 置 `SystemFault` +- 写入故障结果码 +- 日志详细记录异常上下文 +- 进入 `Faulted` +- 故障恢复后需人工复位或 PLC 复位后方可重新接板 + +### 14.3 防重入要求 + +- 流程执行中必须禁止重复触发新板流程 +- 放行写入必须有一次性保护 +- SFTP 查询不得并发重复执行 +- 报警接口同一事件只发送一次 + +--- + +## 15. 日志与追溯要求 + +### 15.1 日志级别建议 + +- `Info`:流程开始、扫码结果、SFTP命中、放行完成 +- `Warning`:扫码重试、文件未找到等待重试、报警接口失败、放行应答超时 +- `Error`:PLC/串口/SFTP/HTTP 调用异常、配置异常 + +### 15.2 单板结果摘要建议 + +每块 PCB 建议形成一条结果记录,包含: + +- 流水时间戳 +- 条码 +- 扫码总次数 +- SFTP 查询次数 +- 最终结果码 +- 最终结果描述 +- 是否放行 +- 是否报警 +- 异常摘要 + +--- + +## 16. 联调与验收建议 + +### 16.1 联调顺序建议 + +1. 先打通 PLC 基础读写 +2. 再联调扫码枪串口触发 +3. 再联调 SFTP 文件查找 +4. 再联调安灯 HTTP 接口 +5. 最后做整流程串联测试 + +### 16.2 关键测试场景 + +1. PLC 到位,扫码一次成功,SFTP 一次命中,正常放行 +2. PLC 到位,扫码前两次失败,第三次成功,SFTP 命中,正常放行 +3. PLC 到位,扫码三次失败,安灯报警成功,最终放行 +4. PLC 到位,扫码成功,SFTP 多次未命中,超时后放行 +5. PLC 到位,扫码成功,但 SFTP 连接异常,系统进入故障,禁止自动放行 +6. 安灯接口调用失败,但扫码失败场景仍按既定规则放行,并有异常日志 +7. 流程处理中重复收到到位信号,不允许重复触发 +8. PLC 复位后,上位机状态位正确清除 +9. PLC 未进入自动模式或未给 `PlcReady` 时,上位机不接板 + +### 16.3 验收重点 + +- 是否稳定识别到位触发 +- 是否正确限制扫码 3 次总尝试 +- 是否按配置进行 SFTP 首次查询和后续 N 次重试 +- 是否在规定场景下正确放行 +- 是否正确触发安灯报警 +- 是否正确区分“文件未找到”和“SFTP 系统异常” +- 是否具备清晰日志和结果码追溯能力 + +--- + +## 17. 后续扩展建议 + +当前设计已为以下扩展预留空间: + +- 将扫码失败放行改为配置策略 +- 将文件未找到是否报警改为配置策略 +- 增加工位编号和多站点配置 +- 增加结果明细落库 +- 增加人工复位与权限控制 +- 增加多工位或多缓存位流程 +- 增加 MES 校验或工单绑定 + +--- + +## 18. 最终结论 + +当前项目适合采用**单工位串行状态机 + Modbus TCP + 串口扫码 + SFTP 文件校验 + HTTP 安灯报警**的轻量化上位机方案。 + +该方案具备以下特点: + +- 与当前需求完全匹配 +- 软件结构简单清晰 +- PLC 点位数量少,易联调 +- 能满足“扫码失败放行”和“文件未找到超时放行”的业务要求 +- 对 SFTP 系统异常与普通文件未命中进行了明确区分 +- 具备后续扩展为更完整目检系统的演进空间 + +建议后续开发时优先落地: + +1. 配置模型 +2. PLC 点位通信服务 +3. 扫码服务 +4. SFTP 校验服务 +5. 主流程状态机 +6. 主界面状态展示 diff --git a/docs/superpowers/specs/2026-04-16-axi-omron-ui-design.md b/docs/superpowers/specs/2026-04-16-axi-omron-ui-design.md index f12e03f..b6a285c 100644 --- a/docs/superpowers/specs/2026-04-16-axi-omron-ui-design.md +++ b/docs/superpowers/specs/2026-04-16-axi-omron-ui-design.md @@ -39,6 +39,64 @@ - 列宽与绑定不变。 - **流程状态区**:将“流程状态”卡片改为 `hc:Card` 高亮展示,错误信息使用红色文字(不变)。 +#### Dashboard 下半区布局方案确认 + +- 已确认采用 **方案 A:双栏中枢布局**。 +- 设计目标: + - 消除当前页面下半区的大面积空白。 + - 强化“当前板状态”和“过程追踪”两个核心视觉中心。 + - 将运行日志降为辅助信息,而不是和主业务区争抢注意力。 +- 布局原则: + - 左侧承担“当前状态 + 运行日志”主视角,右侧承担“处理记录 + 追踪摘要”副视角。 + - 下半区不再平均切成多个小卡片,而是改为“少块、大面、强层级”。 + - 通过不同卡片高度、标题层级、留白和分组,建立更清晰的信息优先级。 + +#### Dashboard 下半区具体布局 + +- **整体分栏**: + - 下半区采用 `1.3 : 0.9` 左右双栏。 + - 左栏为主工作区,右栏为过程追踪区。 +- **左栏上部**: + - 使用两个并排摘要卡片。 + - 卡片 1:`当前二维码 + 结果码 / 报警码` + - 卡片 2:`关键标志 + 扫码次数 + SFTP 次数` + - 当前二维码信息作为左栏上部的第一视觉重点,字体明显大于普通统计值。 +- **左栏下部**: + - 放置 `最近运行日志`,占据左栏主要高度。 + - DataGrid 保留,但应提升表头、行高、内边距和空状态表现,避免“开发态表格感”过强。 +- **右栏上部**: + - 放置 `最近处理记录摘要`,展示: + - 最近触发时间 + - 最近完成时间 + - 最后刷新时间 + - 该区域应做成信息摘要卡,而不是散落的三行文字。 +- **右栏下部**: + - 放置 `最近处理记录列表`。 + - 列表可以保留 DataGrid,但视觉上应更轻,重点突出时间、条码、结果三列。 + - 若 HandyControl 样式允许,可适当弱化网格线,提升卡片式整洁感。 + +#### Dashboard 下半区视觉风格约束 + +- 不新增花哨装饰,不改为互联网运营后台风格,保持工业桌面应用的稳重感。 +- 卡片层次应依靠以下手段建立,而不是依赖过多边框: + - 标题字号差异 + - 数值字号差异 + - 卡片阴影和圆角 + - 区块留白 + - 轻量分隔 +- `关键标志` 建议改为更有秩序的纵向状态列表,保留圆点语义,但需统一间距、字号、对齐。 +- `结果码 / 报警码` 不再作为独立弱卡片存在,应并入主摘要区,提高信息密度。 +- `最近处理记录` 与 `最近运行日志` 必须形成主次关系: + - 主日志区更宽,更适合排查问题。 + - 处理记录区更紧凑,更适合回看节拍与结果。 + +#### 本次改造范围 + +- 仅重构 `DashboardPage.xaml` 的下半区布局与视觉层级。 +- 不修改 ViewModel 属性命名、命令绑定和业务逻辑。 +- 不新增复杂动画、主题切换、图表控件或第三方可视化组件。 +- 若需要新增局部样式或页面资源,应优先放在页面内,避免污染全局资源。 + ### 4. SystemSettingsPage.xaml - **TabControl**:使用 HandyControl 的 `TabControl` 样式(默认已覆盖),或显式使用 `hc:TabControl`。 - **表单项**: @@ -66,3 +124,4 @@ - 不改写业务逻辑、ViewModel、服务层。 - 不引入 HandyControl 的高级控件(Growl、Timeline、Pagination 等),避免过度设计。 - 不替换为其他 UI 库(MahApps、MaterialDesign、WPFUI 等)。 + diff --git a/src/AxiOmron.PcbCheck/App.xaml b/src/AxiOmron.PcbCheck/App.xaml new file mode 100644 index 0000000..2a4400f --- /dev/null +++ b/src/AxiOmron.PcbCheck/App.xaml @@ -0,0 +1,17 @@ + + + + + + + + + + + + diff --git a/src/AxiOmron.PcbCheck/App.xaml.cs b/src/AxiOmron.PcbCheck/App.xaml.cs new file mode 100644 index 0000000..39df095 --- /dev/null +++ b/src/AxiOmron.PcbCheck/App.xaml.cs @@ -0,0 +1,130 @@ +using System.IO; +using System.Text; +using System.Windows; +using AxiOmron.PcbCheck.Options; +using AxiOmron.PcbCheck.Services.Implementations; +using AxiOmron.PcbCheck.Services.Interfaces; +using AxiOmron.PcbCheck.ViewModels; +using AxiOmron.PcbCheck.Views.Pages; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using NLog.Extensions.Logging; + +namespace AxiOmron.PcbCheck; + +/// +/// 表示 WPF 应用入口,负责 Host/DI、配置和日志初始化。 +/// +public partial class App : Application +{ + private IHost? _host; + + /// + /// 获取当前应用服务容器。 + /// + public static IServiceProvider Services { get; private set; } = null!; + + /// + /// 应用启动入口。 + /// + /// 启动事件参数。 + protected override async void OnStartup(StartupEventArgs e) + { + base.OnStartup(e); + try + { + Console.OutputEncoding = Encoding.UTF8; + } + catch (IOException) + { + // WinExe 在没有控制台句柄时忽略 + } + + try + { + _host = BuildHost(); + await _host.StartAsync().ConfigureAwait(true); + Services = _host.Services; + var mainWindow = Services.GetRequiredService(); + MainWindow = mainWindow; + mainWindow.Show(); + } + catch (Exception ex) + { + MessageBox.Show($"应用启动失败: {ex.Message}", "Axi Omron PCB Check", MessageBoxButton.OK, MessageBoxImage.Error); + Shutdown(-1); + } + } + + /// + /// 应用退出入口。 + /// + /// 退出事件参数。 + protected override async void OnExit(ExitEventArgs e) + { + if (_host is not null) + { + try + { + await _host.StopAsync(TimeSpan.FromSeconds(2)).ConfigureAwait(false); + } + finally + { + _host.Dispose(); + } + } + + base.OnExit(e); + } + + /// + /// 构建应用 Host 与依赖注入容器。 + /// + /// 已构建的 Host 实例。 + private static IHost BuildHost() + { + return Host.CreateDefaultBuilder() + .ConfigureAppConfiguration((context, configurationBuilder) => + { + configurationBuilder.SetBasePath(AppContext.BaseDirectory); + configurationBuilder.AddJsonFile("appConfig.json", optional: true, reloadOnChange: false); + configurationBuilder.AddJsonFile($"appConfig.{context.HostingEnvironment.EnvironmentName}.json", optional: true, reloadOnChange: false); + }) + .ConfigureLogging(logging => + { + logging.ClearProviders(); + logging.SetMinimumLevel(LogLevel.Information); + logging.AddNLog(); + }) + .ConfigureServices((context, services) => + { + var appConfig = new AppConfig(); + context.Configuration.Bind(appConfig); + + services.AddSingleton(context.Configuration); + services.AddSingleton(); + services.AddSingleton(appConfig); + services.AddSingleton(); + services.AddSingleton(typeof(IAppLogger<>), typeof(AppLogger<>)); + services.AddSingleton(); + services.AddHttpClient(nameof(AndonService)); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(sp => sp.GetRequiredService()); + services.AddHostedService(sp => sp.GetRequiredService()); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + }) + .Build(); + } +} diff --git a/src/AxiOmron.PcbCheck/AssemblyInfo.cs b/src/AxiOmron.PcbCheck/AssemblyInfo.cs new file mode 100644 index 0000000..c73865b --- /dev/null +++ b/src/AxiOmron.PcbCheck/AssemblyInfo.cs @@ -0,0 +1,10 @@ +using System.Windows; + +[assembly:ThemeInfo( + ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located + //(used if a resource is not found in the page, + // or application resource dictionaries) + ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located + //(used if a resource is not found in the page, + // app, or any theme specific resource dictionaries) +)] diff --git a/src/AxiOmron.PcbCheck/AxiOmron.PcbCheck.csproj b/src/AxiOmron.PcbCheck/AxiOmron.PcbCheck.csproj new file mode 100644 index 0000000..adaf74a --- /dev/null +++ b/src/AxiOmron.PcbCheck/AxiOmron.PcbCheck.csproj @@ -0,0 +1,33 @@ + + + WinExe + net8.0-windows + enable + enable + true + + + + + + + + + + + + + + + + + Always + + + PreserveNewest + + + Always + + + diff --git a/src/AxiOmron.PcbCheck/Converters/BooleanToBrushConverter.cs b/src/AxiOmron.PcbCheck/Converters/BooleanToBrushConverter.cs new file mode 100644 index 0000000..2c7276e --- /dev/null +++ b/src/AxiOmron.PcbCheck/Converters/BooleanToBrushConverter.cs @@ -0,0 +1,61 @@ +using System.Globalization; +using System.Windows.Data; +using System.Windows.Media; + +namespace AxiOmron.PcbCheck.Converters; + +/// +/// 将布尔状态转换为界面画刷,颜色与设计色板保持一致。 +/// +public sealed class BooleanToBrushConverter : IValueConverter +{ + /// + /// true 状态使用的柔和绿色画刷(对应 Tag 前景色 #15803D)。 + /// + private static readonly SolidColorBrush TrueBrush = CreateFrozen(0x15, 0x80, 0x3D); + + /// + /// false 状态使用的柔和红色画刷(对应 Tag 前景色 #B91C1C)。 + /// + private static readonly SolidColorBrush FalseBrush = CreateFrozen(0xB9, 0x1C, 0x1C); + + /// + /// 将布尔值转换为画刷。 + /// + /// 源值。 + /// 目标类型。 + /// 扩展参数。 + /// 当前区域信息。 + /// 状态画刷。 + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return value is true ? TrueBrush : FalseBrush; + } + + /// + /// 创建并冻结指定颜色的画刷,便于跨线程复用。 + /// + /// 红通道。 + /// 绿通道。 + /// 蓝通道。 + /// 已冻结的画刷实例。 + private static SolidColorBrush CreateFrozen(byte r, byte g, byte b) + { + SolidColorBrush brush = new(Color.FromRgb(r, g, b)); + brush.Freeze(); + return brush; + } + + /// + /// 不支持反向转换。 + /// + /// 源值。 + /// 目标类型。 + /// 扩展参数。 + /// 当前区域信息。 + /// 抛出不支持异常。 + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotSupportedException(); + } +} diff --git a/src/AxiOmron.PcbCheck/Converters/BooleanToVisibilityConverter.cs b/src/AxiOmron.PcbCheck/Converters/BooleanToVisibilityConverter.cs new file mode 100644 index 0000000..605dedb --- /dev/null +++ b/src/AxiOmron.PcbCheck/Converters/BooleanToVisibilityConverter.cs @@ -0,0 +1,67 @@ +using System.Globalization; +using System.Windows; +using System.Windows.Data; + +namespace AxiOmron.PcbCheck.Converters; + +/// +/// 将布尔值转换为 ,支持通过 ConverterParameter 反转判定。 +/// +/// +/// 当 ConverterParameter 为 "Invert"、"Inverse" 或 "!" 时,布尔值的真假意义取反。 +/// +public sealed class BooleanToVisibilityConverter : IValueConverter +{ + /// + /// 将布尔值转换为可见性。 + /// + /// 源布尔值。 + /// 目标类型。 + /// 若为 "Invert"/"!" 则反转判定。 + /// 当前区域信息。 + /// + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + bool flag = value is bool b && b; + if (IsInvert(parameter)) + { + flag = !flag; + } + return flag ? Visibility.Visible : Visibility.Collapsed; + } + + /// + /// 将可见性反向转换为布尔值。 + /// + /// 目标可见性。 + /// 目标类型。 + /// 若为 "Invert"/"!" 则反转判定。 + /// 当前区域信息。 + /// 布尔值。 + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + bool flag = value is Visibility v && v == Visibility.Visible; + if (IsInvert(parameter)) + { + flag = !flag; + } + return flag; + } + + /// + /// 判断参数是否要求反转判定。 + /// + /// 参数值。 + /// 是否反转。 + private static bool IsInvert(object? parameter) + { + if (parameter is null) + { + return false; + } + string token = parameter.ToString() ?? string.Empty; + return token.Equals("Invert", StringComparison.OrdinalIgnoreCase) + || token.Equals("Inverse", StringComparison.OrdinalIgnoreCase) + || token.Equals("!", StringComparison.Ordinal); + } +} diff --git a/src/AxiOmron.PcbCheck/Converters/ResultCodeToTagTextConverter.cs b/src/AxiOmron.PcbCheck/Converters/ResultCodeToTagTextConverter.cs new file mode 100644 index 0000000..51bdde0 --- /dev/null +++ b/src/AxiOmron.PcbCheck/Converters/ResultCodeToTagTextConverter.cs @@ -0,0 +1,54 @@ +using System.Globalization; +using System.Windows.Data; +using AxiOmron.PcbCheck.Models; + +namespace AxiOmron.PcbCheck.Converters; + +/// +/// 将 (以 存储)映射为简短 Tag 文本。 +/// +/// +/// 映射规则:Passed → "OK";Processing → "处理中";None → "-";其余视为 "NG"。 +/// +public sealed class ResultCodeToTagTextConverter : IValueConverter +{ + /// + /// 将结果码转换为 Tag 文本。 + /// + /// 结果码值。 + /// 目标类型。 + /// 扩展参数。 + /// 当前区域信息。 + /// 简短标签文本。 + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + ushort code = value switch + { + ushort u => u, + int i => (ushort)i, + WorkflowResultCode rc => (ushort)rc, + _ => (ushort)0 + }; + + return code switch + { + (ushort)WorkflowResultCode.Passed => "OK", + (ushort)WorkflowResultCode.Processing => "处理中", + (ushort)WorkflowResultCode.None => "-", + _ => "NG" + }; + } + + /// + /// 不支持反向转换。 + /// + /// 源值。 + /// 目标类型。 + /// 扩展参数。 + /// 当前区域信息。 + /// 抛出不支持异常。 + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotSupportedException(); + } +} diff --git a/src/AxiOmron.PcbCheck/DesignTime/DesignTimeAppConfigService.cs b/src/AxiOmron.PcbCheck/DesignTime/DesignTimeAppConfigService.cs new file mode 100644 index 0000000..02a7851 --- /dev/null +++ b/src/AxiOmron.PcbCheck/DesignTime/DesignTimeAppConfigService.cs @@ -0,0 +1,120 @@ +using AxiOmron.PcbCheck.Options; +using AxiOmron.PcbCheck.Services.Interfaces; + +namespace AxiOmron.PcbCheck.DesignTime; + +/// +/// 提供设计时配置服务,返回固定的示例配置数据。 +/// +public sealed class DesignTimeAppConfigService : IAppConfigService +{ + private readonly AppConfig _config; + + /// + /// 初始化设计时配置服务。 + /// + public DesignTimeAppConfigService() + { + _config = CreateSampleConfig(); + } + + /// + /// 读取设计时配置副本。 + /// + /// 示例根配置对象。 + public AppConfig Load() + { + return CreateSampleConfig(); + } + + /// + /// 保存设计时配置,占位实现,仅更新内存中的副本。 + /// + /// 待保存的配置对象。 + /// 时抛出。 + public void Save(AppConfig config) + { + ArgumentNullException.ThrowIfNull(config); + _config.Plc = config.Plc; + _config.Scanner = config.Scanner; + _config.Sftp = config.Sftp; + _config.Andon = config.Andon; + _config.Workflow = config.Workflow; + } + + /// + /// 获取设计时展示用的示例配置路径。 + /// + /// 固定的设计时配置路径文本。 + public string GetConfigPath() + { + return @"D:\DesignTime\appConfig.Development.json"; + } + + /// + /// 创建设计器使用的示例配置对象。 + /// + /// 填充默认值后的配置对象。 + private static AppConfig CreateSampleConfig() + { + return new AppConfig + { + Plc = new PlcOptions + { + Host = "192.168.10.25", + Port = 502, + UnitId = 1, + PollIntervalMs = 200, + ConnectTimeoutMs = 3000, + HeartbeatIntervalMs = 500, + ReleasePulseMs = 450, + ReleaseAckTimeoutMs = 2500 + }, + Scanner = new ScannerOptions + { + PortName = "COM3", + BaudRate = 9600, + DataBits = 8, + Parity = "None", + StopBits = "One", + ReadTimeoutMs = 2500, + TriggerCommand = "SCAN\\r", + ResponseTerminator = "\\r", + MaxScanAttempts = 3 + }, + Sftp = new SftpOptions + { + Host = "10.10.20.35", + Port = 22, + Username = "pcb_user", + Password = "******", + PrivateKeyPath = @"C:\Keys\pcb-check.ppk", + RootPath = "/data/pcb", + FileNamePattern = "${barcode}.txt", + RetryIntervalSeconds = 2, + MaxRetryCount = 3, + ConnectTimeoutMs = 3000 + }, + Andon = new AndonOptions + { + Enable = true, + Url = "http://10.10.20.50/api/andon/alarm", + Method = "POST", + TimeoutMs = 3000, + StationCode = "OMRON-L01", + StationName = "欧姆龙 PCB 检测", + EnableScanFailAlarm = true, + EnableFileNotFoundAlarm = true + }, + Workflow = new WorkflowOptions + { + RequirePlcReady = true, + RequireAutoMode = true, + RequireStationEnable = true, + RequireManualResetAfterFault = true, + MaxUiLogEntries = 200, + MaxBoardRecords = 100 + } + }; + } +} diff --git a/src/AxiOmron.PcbCheck/DesignTime/DesignTimeAppStateStore.cs b/src/AxiOmron.PcbCheck/DesignTime/DesignTimeAppStateStore.cs new file mode 100644 index 0000000..8ee4c70 --- /dev/null +++ b/src/AxiOmron.PcbCheck/DesignTime/DesignTimeAppStateStore.cs @@ -0,0 +1,206 @@ +using AxiOmron.PcbCheck.Models; +using AxiOmron.PcbCheck.Services.Interfaces; + +namespace AxiOmron.PcbCheck.DesignTime; + +/// +/// 提供设计时运行态存储,向真实 ViewModel 回放固定的演示数据。 +/// +public sealed class DesignTimeAppStateStore : IAppStateStore +{ + private readonly RuntimeSnapshot _snapshot; + private readonly IReadOnlyList _logs; + private readonly IReadOnlyList _records; + private EventHandler? _snapshotChanged; + private EventHandler? _logAdded; + private EventHandler? _recordAdded; + + /// + /// 初始化设计时运行态存储。 + /// + public DesignTimeAppStateStore() + { + DateTimeOffset now = DateTimeOffset.Now; + _snapshot = new RuntimeSnapshot + { + PlcStatus = "已连接", + ScannerStatus = "在线", + SftpStatus = "可访问", + AndonStatus = "接口正常", + WorkflowState = WorkflowState.CheckingSftp, + WorkflowStateText = WorkflowState.CheckingSftp.ToDisplayText(), + CurrentBarcode = "PCB240417000128", + ResultDescription = "已扫码,等待 SFTP 文件确认", + FaultMessage = string.Empty, + ScanTryCount = 1, + SftpTryCount = 2, + ResultCode = (ushort)WorkflowResultCode.Processing, + AlarmCode = (ushort)AlarmCode.None, + LastTriggeredAt = now.AddSeconds(-18), + LastCompletedAt = now.AddMinutes(-2), + IsBusy = true, + ProcessDone = false, + SystemFault = false, + AlarmRaised = false, + LastUpdatedAt = now + }; + + _logs = new List + { + new() { Timestamp = now.AddSeconds(-2), Level = "Info", Message = "SFTP 第 2 次查询:正在检查目标文件。" }, + new() { Timestamp = now.AddSeconds(-9), Level = "Warning", Message = "第 1 次 SFTP 查询未命中,准备重试。" }, + new() { Timestamp = now.AddSeconds(-16), Level = "Info", Message = "扫码成功,条码=PCB240417000128" }, + new() { Timestamp = now.AddSeconds(-21), Level = "Info", Message = "检测到 PCB 到位,流程开始执行。" }, + new() { Timestamp = now.AddMinutes(-1), Level = "Error", Message = "上一片文件查询超时,已按规则放行。" } + }; + + _records = new List + { + new() + { + StartedAt = now.AddMinutes(-4), + CompletedAt = now.AddMinutes(-3).AddSeconds(-18), + Barcode = "PCB240417000125", + ScanTryCount = 1, + SftpTryCount = 1, + ResultCode = (ushort)WorkflowResultCode.Passed, + ResultDescription = "OK 放行", + ReleaseSent = true, + AlarmRaised = false, + ExceptionSummary = string.Empty + }, + new() + { + StartedAt = now.AddMinutes(-3), + CompletedAt = now.AddMinutes(-2).AddSeconds(-12), + Barcode = "PCB240417000126", + ScanTryCount = 3, + SftpTryCount = 0, + ResultCode = (ushort)WorkflowResultCode.ScanFailedReleased, + ResultDescription = "扫码失败后放行", + ReleaseSent = true, + AlarmRaised = true, + ExceptionSummary = "扫码连续失败三次" + }, + new() + { + StartedAt = now.AddMinutes(-2), + CompletedAt = now.AddMinutes(-1).AddSeconds(-25), + Barcode = "PCB240417000127", + ScanTryCount = 1, + SftpTryCount = 3, + ResultCode = (ushort)WorkflowResultCode.FileNotFoundReleased, + ResultDescription = "文件超时未找到后放行", + ReleaseSent = true, + AlarmRaised = true, + ExceptionSummary = "SFTP 文件查询超时" + } + }; + } + + /// + /// 当运行态快照发生变化时触发。 + /// + public event EventHandler? SnapshotChanged + { + add + { + _snapshotChanged += value; + } + remove + { + _snapshotChanged -= value; + } + } + + /// + /// 当新增日志时触发;订阅时会立即回放现有设计时日志。 + /// + public event EventHandler? LogAdded + { + add + { + if (value is null) + { + return; + } + + _logAdded += value; + foreach (UiLogEntry entry in _logs) + { + value(this, entry); + } + } + remove + { + _logAdded -= value; + } + } + + /// + /// 当新增单板记录时触发;订阅时会立即回放现有设计时记录。 + /// + public event EventHandler? RecordAdded + { + add + { + if (value is null) + { + return; + } + + _recordAdded += value; + foreach (BoardProcessRecord record in _records) + { + value(this, record); + } + } + remove + { + _recordAdded -= value; + } + } + + /// + /// 获取当前设计时快照副本。 + /// + /// 当前快照副本。 + public RuntimeSnapshot GetSnapshot() + { + return _snapshot.Clone(); + } + + /// + /// 更新设计时快照并通知订阅者。 + /// + /// 用于修改快照的委托。 + /// 时抛出。 + public void UpdateSnapshot(Action updateAction) + { + ArgumentNullException.ThrowIfNull(updateAction); + updateAction(_snapshot); + _snapshotChanged?.Invoke(this, _snapshot.Clone()); + } + + /// + /// 追加一条设计时日志并通知订阅者。 + /// + /// 待追加的日志对象。 + /// 时抛出。 + public void AddLog(UiLogEntry entry) + { + ArgumentNullException.ThrowIfNull(entry); + _logAdded?.Invoke(this, entry); + } + + /// + /// 追加一条设计时处理记录并通知订阅者。 + /// + /// 待追加的记录对象。 + /// 时抛出。 + public void AddRecord(BoardProcessRecord record) + { + ArgumentNullException.ThrowIfNull(record); + _recordAdded?.Invoke(this, record); + } +} diff --git a/src/AxiOmron.PcbCheck/DesignTime/DesignTimeDispatcherService.cs b/src/AxiOmron.PcbCheck/DesignTime/DesignTimeDispatcherService.cs new file mode 100644 index 0000000..fd4e29a --- /dev/null +++ b/src/AxiOmron.PcbCheck/DesignTime/DesignTimeDispatcherService.cs @@ -0,0 +1,22 @@ +using AxiOmron.PcbCheck.Services.Interfaces; + +namespace AxiOmron.PcbCheck.DesignTime; + +/// +/// 提供设计时 Dispatcher 调度能力,直接在当前线程执行委托。 +/// +public sealed class DesignTimeDispatcherService : IDispatcherService +{ + /// + /// 在当前线程中立即执行指定动作。 + /// + /// 待执行的动作。 + /// 表示执行完成的任务。 + /// 时抛出。 + public Task InvokeAsync(Action action) + { + ArgumentNullException.ThrowIfNull(action); + action(); + return Task.CompletedTask; + } +} diff --git a/src/AxiOmron.PcbCheck/DesignTime/DesignTimeViewModelLocator.cs b/src/AxiOmron.PcbCheck/DesignTime/DesignTimeViewModelLocator.cs new file mode 100644 index 0000000..4d90a26 --- /dev/null +++ b/src/AxiOmron.PcbCheck/DesignTime/DesignTimeViewModelLocator.cs @@ -0,0 +1,39 @@ +using AxiOmron.PcbCheck.Options; +using AxiOmron.PcbCheck.ViewModels; + +namespace AxiOmron.PcbCheck.DesignTime; + +/// +/// 为 XAML 设计器提供基于真实 ViewModel 的设计时定位器。 +/// +public sealed class DesignTimeViewModelLocator +{ + private readonly DesignTimeAppStateStore _appStateStore = new(); + private readonly DesignTimeDispatcherService _dispatcherService = new(); + private readonly DesignTimeWorkflowControlService _workflowControlService = new(); + private readonly DesignTimeAppConfigService _appConfigService = new(); + private MainWindowViewModel? _mainWindowViewModel; + private SystemSettingViewModel? _systemSettingViewModel; + + /// + /// 获取首页设计时视图模型。 + /// + public MainWindowViewModel MainWindowViewModel + => _mainWindowViewModel ??= CreateMainWindowViewModel(); + + /// + /// 获取系统设置设计时视图模型。 + /// + public SystemSettingViewModel SystemSettingViewModel + => _systemSettingViewModel ??= new SystemSettingViewModel(_appConfigService); + + /// + /// 创建首页设计时视图模型。 + /// + /// 填充了设计时演示数据的真实视图模型实例。 + private MainWindowViewModel CreateMainWindowViewModel() + { + AppConfig config = _appConfigService.Load(); + return new MainWindowViewModel(_appStateStore, _dispatcherService, _workflowControlService, config); + } +} diff --git a/src/AxiOmron.PcbCheck/DesignTime/DesignTimeWorkflowControlService.cs b/src/AxiOmron.PcbCheck/DesignTime/DesignTimeWorkflowControlService.cs new file mode 100644 index 0000000..76d1c4e --- /dev/null +++ b/src/AxiOmron.PcbCheck/DesignTime/DesignTimeWorkflowControlService.cs @@ -0,0 +1,57 @@ +using AxiOmron.PcbCheck.Services.Interfaces; + +namespace AxiOmron.PcbCheck.DesignTime; + +/// +/// 提供设计时流程控制服务,所有命令均为无副作用占位实现。 +/// +public sealed class DesignTimeWorkflowControlService : IWorkflowControlService +{ + /// + /// 模拟手动复位流程命令,不执行实际业务操作。 + /// + /// 取消令牌;若已取消则立即返回已取消任务。 + /// 表示命令已完成的任务。 + public Task ResetAsync(CancellationToken cancellationToken) + { + return cancellationToken.IsCancellationRequested + ? Task.FromCanceled(cancellationToken) + : Task.CompletedTask; + } + + /// + /// 模拟 PLC 重连命令,不执行实际设备通信。 + /// + /// 取消令牌;若已取消则立即返回已取消任务。 + /// 表示命令已完成的任务。 + public Task ReconnectPlcAsync(CancellationToken cancellationToken) + { + return cancellationToken.IsCancellationRequested + ? Task.FromCanceled(cancellationToken) + : Task.CompletedTask; + } + + /// + /// 模拟扫码枪重连命令,不执行实际设备通信。 + /// + /// 取消令牌;若已取消则立即返回已取消任务。 + /// 表示命令已完成的任务。 + public Task ReconnectScannerAsync(CancellationToken cancellationToken) + { + return cancellationToken.IsCancellationRequested + ? Task.FromCanceled(cancellationToken) + : Task.CompletedTask; + } + + /// + /// 模拟安灯测试命令,不执行实际网络请求。 + /// + /// 取消令牌;若已取消则立即返回已取消任务。 + /// 表示命令已完成的任务。 + public Task TestAndonAsync(CancellationToken cancellationToken) + { + return cancellationToken.IsCancellationRequested + ? Task.FromCanceled(cancellationToken) + : Task.CompletedTask; + } +} diff --git a/src/AxiOmron.PcbCheck/MainWindow.xaml b/src/AxiOmron.PcbCheck/MainWindow.xaml new file mode 100644 index 0000000..14538d0 --- /dev/null +++ b/src/AxiOmron.PcbCheck/MainWindow.xaml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + +