✨ feat(*): 添加扫码枪启动探活、全局退出助手及 README
- 添加扫码枪串口启动探活,检测端口占用并更新 UI 状态 - 新增 ShutdownHelper 安全停止 Host 扩展方法 - 新增 README.md 项目说明文档 - 更新 WorkflowHostedService 启动探活逻辑 - 补充 ShutdownHelper 与 WorkflowHostedService 单元测试 - 优化 DashboardPage 与 SystemSettingsPage 界面布局 - 调整 ModbusTcpPlcService 监控镜像读取逻辑
This commit is contained in:
@@ -23,7 +23,9 @@
|
|||||||
"Bash(sudo apt-get *)",
|
"Bash(sudo apt-get *)",
|
||||||
"Bash(sudo -n true)",
|
"Bash(sudo -n true)",
|
||||||
"Bash(mkdir -p /tmp/pcb)",
|
"Bash(mkdir -p /tmp/pcb)",
|
||||||
"Read(//tmp/**)"
|
"Read(//tmp/**)",
|
||||||
|
"Bash(taskkill /F /IM \"AxiOmron.PcbCheck.exe\")",
|
||||||
|
"Bash(powershell -Command \"Stop-Process -Id 23292 -Force -ErrorAction SilentlyContinue\")"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
174
README.md
Normal file
174
README.md
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
# AxiOmron.PcbCheck
|
||||||
|
|
||||||
|
基于 `.NET 8 + WPF + MVVM Toolkit + Generic Host/DI + NLog` 的 PCB 目检上位机示例工程,包含 PLC 通信、扫码枪触发、SFTP 文件校验、安灯报警和运行态监控。
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
|
```text
|
||||||
|
Axi_Omron/
|
||||||
|
src/
|
||||||
|
AxiOmron.PcbCheck/
|
||||||
|
tests/
|
||||||
|
AxiOmron.PcbCheck.Tests/
|
||||||
|
docs/
|
||||||
|
AxiOmron.PcbCheck.slnx
|
||||||
|
```
|
||||||
|
|
||||||
|
主程序位于 [src/AxiOmron.PcbCheck](d:/Dev/Codes/MFD_Solution/Axi_Omron/src/AxiOmron.PcbCheck),测试位于 [tests/AxiOmron.PcbCheck.Tests](d:/Dev/Codes/MFD_Solution/Axi_Omron/tests/AxiOmron.PcbCheck.Tests)。
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
- `.NET 8` / `WPF`
|
||||||
|
- `CommunityToolkit.Mvvm`
|
||||||
|
- `Microsoft.Extensions.Hosting`
|
||||||
|
- `NLog`
|
||||||
|
- `IoTClient` Modbus TCP
|
||||||
|
- `SSH.NET`
|
||||||
|
- `HandyControl`
|
||||||
|
|
||||||
|
## 配置说明
|
||||||
|
|
||||||
|
主配置文件位于 [src/AxiOmron.PcbCheck/appConfig.json](d:/Dev/Codes/MFD_Solution/Axi_Omron/src/AxiOmron.PcbCheck/appConfig.json)。
|
||||||
|
|
||||||
|
当前 PLC 相关默认配置示例:
|
||||||
|
|
||||||
|
- `Plc.Host`: `127.0.0.1`
|
||||||
|
- `Plc.Port`: `502`
|
||||||
|
- `Plc.UnitId`: `1`
|
||||||
|
- `Plc.PollIntervalMs`: `200`
|
||||||
|
- `Plc.ReleasePulseMs`: `500`
|
||||||
|
- `Plc.ReleaseAckTimeoutMs`: `2000`
|
||||||
|
|
||||||
|
点位映射示例:
|
||||||
|
|
||||||
|
- 输入点位
|
||||||
|
- `PcbArrived = 0`
|
||||||
|
- `PlcReset = 1`
|
||||||
|
- `PlcAckRelease = 2`
|
||||||
|
- 输出点位
|
||||||
|
- `PcBusy = 51`
|
||||||
|
- `ReleasePermit = 52`
|
||||||
|
- 寄存器
|
||||||
|
- `ResultCode = 0`
|
||||||
|
|
||||||
|
系统设置页可修改 PLC 参数,界面入口在 [SystemSettingsPage.xaml](d:/Dev/Codes/MFD_Solution/Axi_Omron/src/AxiOmron.PcbCheck/Views/Pages/SystemSettingsPage.xaml#L58)。
|
||||||
|
|
||||||
|
注意:配置保存后会写入运行目录下的 `appConfig.json`,当前界面提示为“重启应用后完全生效”。
|
||||||
|
|
||||||
|
## 构建与运行
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
dotnet restore .\src\AxiOmron.PcbCheck\AxiOmron.PcbCheck.csproj
|
||||||
|
dotnet build .\src\AxiOmron.PcbCheck\AxiOmron.PcbCheck.csproj -c Debug
|
||||||
|
dotnet run --project .\src\AxiOmron.PcbCheck\AxiOmron.PcbCheck.csproj -c Debug
|
||||||
|
dotnet test .\tests\AxiOmron.PcbCheck.Tests\AxiOmron.PcbCheck.Tests.csproj
|
||||||
|
```
|
||||||
|
|
||||||
|
## PLC 后台轮询调用链
|
||||||
|
|
||||||
|
### 总览
|
||||||
|
|
||||||
|
```text
|
||||||
|
App 启动
|
||||||
|
-> BuildHost 注册 WorkflowHostedService 为 HostedService
|
||||||
|
-> Host.StartAsync()
|
||||||
|
-> WorkflowHostedService.ExecuteAsync()
|
||||||
|
|
||||||
|
ExecuteAsync 主循环
|
||||||
|
-> ProbePlcOnStartupAsync() 启动先探活
|
||||||
|
-> while (...)
|
||||||
|
-> _plcService.ReadSignalsAsync() 读取 PLC 输入
|
||||||
|
-> HandleSignalSnapshot() 刷 PLC 连接状态
|
||||||
|
-> RefreshPlcMonitorSnapshotAsync() 刷监控区
|
||||||
|
-> if PlcReset = true
|
||||||
|
-> ResetAsync()
|
||||||
|
else if ShouldStartWorkflow() 命中上升沿
|
||||||
|
-> StartWorkflowInBackground()
|
||||||
|
-> RunWorkflowOnceAsync()
|
||||||
|
|
||||||
|
RunWorkflowOnceAsync 单板流程
|
||||||
|
-> InitializeBoardState()
|
||||||
|
-> ApplyProcessStateAsync(Triggered, PcBusy=true)
|
||||||
|
-> ExecuteScanFlowAsync()
|
||||||
|
-> _scannerService.TriggerScanAsync()
|
||||||
|
-> ExecuteSftpFlowAsync(barcode)
|
||||||
|
-> _sftpLookupService.CheckFileAsync()
|
||||||
|
-> ReleaseAndCompleteAsync()
|
||||||
|
-> ApplyProcessStateAsync(Releasing, ReleasePermit=true)
|
||||||
|
-> 循环 _plcService.ReadSignalsAsync() 等 PlcAckRelease
|
||||||
|
-> Delay(ReleasePulseMs)
|
||||||
|
-> ApplyProcessStateAsync(Completed, ReleasePermit=false, PcBusy=false)
|
||||||
|
-> _stateStore.AddRecord(...)
|
||||||
|
|
||||||
|
状态/UI 更新支线
|
||||||
|
-> PublishRuntimeState() / UpdateSnapshot()
|
||||||
|
-> AppStateStore.SnapshotChanged
|
||||||
|
-> MainWindowViewModel.OnSnapshotChanged()
|
||||||
|
-> DashboardPage / SystemSettingsPage 绑定刷新
|
||||||
|
```
|
||||||
|
|
||||||
|
### 关键代码入口
|
||||||
|
|
||||||
|
- 后台服务注册:
|
||||||
|
[App.xaml.cs](d:/Dev/Codes/MFD_Solution/Axi_Omron/src/AxiOmron.PcbCheck/App.xaml.cs#L125)
|
||||||
|
[App.xaml.cs](d:/Dev/Codes/MFD_Solution/Axi_Omron/src/AxiOmron.PcbCheck/App.xaml.cs#L127)
|
||||||
|
- 轮询主循环:
|
||||||
|
[WorkflowHostedService.cs](d:/Dev/Codes/MFD_Solution/Axi_Omron/src/AxiOmron.PcbCheck/Services/Implementations/WorkflowHostedService.cs#L70)
|
||||||
|
- PLC 输入读取:
|
||||||
|
[WorkflowHostedService.cs](d:/Dev/Codes/MFD_Solution/Axi_Omron/src/AxiOmron.PcbCheck/Services/Implementations/WorkflowHostedService.cs#L81)
|
||||||
|
- 轮询周期:
|
||||||
|
[WorkflowHostedService.cs](d:/Dev/Codes/MFD_Solution/Axi_Omron/src/AxiOmron.PcbCheck/Services/Implementations/WorkflowHostedService.cs#L110)
|
||||||
|
- 启动流程判定:
|
||||||
|
[WorkflowHostedService.cs](d:/Dev/Codes/MFD_Solution/Axi_Omron/src/AxiOmron.PcbCheck/Services/Implementations/WorkflowHostedService.cs#L366)
|
||||||
|
- 启动单板流程:
|
||||||
|
[WorkflowHostedService.cs](d:/Dev/Codes/MFD_Solution/Axi_Omron/src/AxiOmron.PcbCheck/Services/Implementations/WorkflowHostedService.cs#L388)
|
||||||
|
- 单板总流程:
|
||||||
|
[WorkflowHostedService.cs](d:/Dev/Codes/MFD_Solution/Axi_Omron/src/AxiOmron.PcbCheck/Services/Implementations/WorkflowHostedService.cs#L496)
|
||||||
|
- 扫码流程:
|
||||||
|
[WorkflowHostedService.cs](d:/Dev/Codes/MFD_Solution/Axi_Omron/src/AxiOmron.PcbCheck/Services/Implementations/WorkflowHostedService.cs#L542)
|
||||||
|
- SFTP 校验流程:
|
||||||
|
[WorkflowHostedService.cs](d:/Dev/Codes/MFD_Solution/Axi_Omron/src/AxiOmron.PcbCheck/Services/Implementations/WorkflowHostedService.cs#L630)
|
||||||
|
- 放行及等待 PLC 应答:
|
||||||
|
[WorkflowHostedService.cs](d:/Dev/Codes/MFD_Solution/Axi_Omron/src/AxiOmron.PcbCheck/Services/Implementations/WorkflowHostedService.cs#L727)
|
||||||
|
- 流程状态写回 PLC:
|
||||||
|
[WorkflowHostedService.cs](d:/Dev/Codes/MFD_Solution/Axi_Omron/src/AxiOmron.PcbCheck/Services/Implementations/WorkflowHostedService.cs#L942)
|
||||||
|
[WorkflowHostedService.cs](d:/Dev/Codes/MFD_Solution/Axi_Omron/src/AxiOmron.PcbCheck/Services/Implementations/WorkflowHostedService.cs#L954)
|
||||||
|
|
||||||
|
### PLC 底层读写落点
|
||||||
|
|
||||||
|
- `IPlcService` 接口:
|
||||||
|
[CoreInterfaces.cs](d:/Dev/Codes/MFD_Solution/Axi_Omron/src/AxiOmron.PcbCheck/Services/Interfaces/CoreInterfaces.cs#L97)
|
||||||
|
- `ReadSignalsAsync` 实现:
|
||||||
|
[ModbusTcpPlcService.cs](d:/Dev/Codes/MFD_Solution/Axi_Omron/src/AxiOmron.PcbCheck/Services/Implementations/ModbusTcpPlcService.cs#L37)
|
||||||
|
- 实际读取的输入位:
|
||||||
|
[ModbusTcpPlcService.cs](d:/Dev/Codes/MFD_Solution/Axi_Omron/src/AxiOmron.PcbCheck/Services/Implementations/ModbusTcpPlcService.cs#L47)
|
||||||
|
[ModbusTcpPlcService.cs](d:/Dev/Codes/MFD_Solution/Axi_Omron/src/AxiOmron.PcbCheck/Services/Implementations/ModbusTcpPlcService.cs#L48)
|
||||||
|
[ModbusTcpPlcService.cs](d:/Dev/Codes/MFD_Solution/Axi_Omron/src/AxiOmron.PcbCheck/Services/Implementations/ModbusTcpPlcService.cs#L49)
|
||||||
|
- 监控区镜像读取:
|
||||||
|
[ModbusTcpPlcService.cs](d:/Dev/Codes/MFD_Solution/Axi_Omron/src/AxiOmron.PcbCheck/Services/Implementations/ModbusTcpPlcService.cs#L69)
|
||||||
|
- 输出状态写入:
|
||||||
|
[ModbusTcpPlcService.cs](d:/Dev/Codes/MFD_Solution/Axi_Omron/src/AxiOmron.PcbCheck/Services/Implementations/ModbusTcpPlcService.cs#L113)
|
||||||
|
|
||||||
|
### UI 刷新链
|
||||||
|
|
||||||
|
- 后台更新运行态快照:
|
||||||
|
[AppStateStore.cs](d:/Dev/Codes/MFD_Solution/Axi_Omron/src/AxiOmron.PcbCheck/Services/Implementations/AppStateStore.cs#L45)
|
||||||
|
- 主界面监听快照变化:
|
||||||
|
[MainWindowViewModel.cs](d:/Dev/Codes/MFD_Solution/Axi_Omron/src/AxiOmron.PcbCheck/ViewModels/MainWindowViewModel.cs#L532)
|
||||||
|
- 设置页监听快照变化:
|
||||||
|
[SystemSettingViewModel.cs](d:/Dev/Codes/MFD_Solution/Axi_Omron/src/AxiOmron.PcbCheck/ViewModels/SystemSettingViewModel.cs#L150)
|
||||||
|
|
||||||
|
### 常用排查入口
|
||||||
|
|
||||||
|
- `PlcReset` 触发软件复位:
|
||||||
|
[WorkflowHostedService.cs](d:/Dev/Codes/MFD_Solution/Axi_Omron/src/AxiOmron.PcbCheck/Services/Implementations/WorkflowHostedService.cs#L85)
|
||||||
|
- 轮询异常进入故障锁存:
|
||||||
|
[WorkflowHostedService.cs](d:/Dev/Codes/MFD_Solution/Axi_Omron/src/AxiOmron.PcbCheck/Services/Implementations/WorkflowHostedService.cs#L103)
|
||||||
|
- 流程异常进入故障锁存:
|
||||||
|
[WorkflowHostedService.cs](d:/Dev/Codes/MFD_Solution/Axi_Omron/src/AxiOmron.PcbCheck/Services/Implementations/WorkflowHostedService.cs#L432)
|
||||||
|
- 故障状态写回:
|
||||||
|
[WorkflowHostedService.cs](d:/Dev/Codes/MFD_Solution/Axi_Omron/src/AxiOmron.PcbCheck/Services/Implementations/WorkflowHostedService.cs#L823)
|
||||||
|
|
||||||
|
## 说明
|
||||||
|
|
||||||
|
本 README 主要补充程序整体说明与 PLC 后台轮询调用链,便于排查“PLC 轮询在哪里启动、如何触发单板流程、状态如何回写和刷新 UI”这类问题。
|
||||||
@@ -444,10 +444,9 @@
|
|||||||
### 8.2 点位设计原则
|
### 8.2 点位设计原则
|
||||||
|
|
||||||
- 输入点和输出点职责分离
|
- 输入点和输出点职责分离
|
||||||
- 过程状态位与最终结果码同时保留
|
- 只保留流程闭环必需的少量点位
|
||||||
- 对 PLC 需要快速判断的信号,优先给单独布尔位
|
- 详细错误原因由上位机日志与界面表达,不强依赖 PLC 点位细分
|
||||||
- 对上位机 UI 和日志需要详细表达的结果,使用数值结果码补充
|
- 放行信号使用脉冲,忙碌位使用保持方式
|
||||||
- 放行信号使用脉冲,忙碌位和完成位可使用保持方式
|
|
||||||
- 本文档采用**PLC -> 上位机为 Discrete Input、上位机 -> PLC 为 Coil** 的常规表达方式;若现场 PLC 地址区定义不同,可在实施阶段做地址映射,不改变信号语义
|
- 本文档采用**PLC -> 上位机为 Discrete Input、上位机 -> PLC 为 Coil** 的常规表达方式;若现场 PLC 地址区定义不同,可在实施阶段做地址映射,不改变信号语义
|
||||||
|
|
||||||
### 8.3 PLC -> 上位机点位(建议)
|
### 8.3 PLC -> 上位机点位(建议)
|
||||||
@@ -456,19 +455,16 @@
|
|||||||
|
|
||||||
| 序号 | 地址类型 | 地址 | 点位名称 | 方向 | 说明 |
|
| 序号 | 地址类型 | 地址 | 点位名称 | 方向 | 说明 |
|
||||||
| --- | --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- | --- |
|
||||||
| 1 | Discrete Input | 10001 | PlcReady | PLC -> PC | PLC 就绪,允许上位机参与流程 |
|
| 1 | Discrete Input | 10001 | PcbArrived | PLC -> PC | PCB 已到位,请求上位机处理 |
|
||||||
| 2 | Discrete Input | 10002 | PcbArrived | PLC -> PC | PCB 已到位,请求上位机处理 |
|
| 2 | Discrete Input | 10002 | PlcReset | PLC -> PC | PLC 请求上位机清状态/复位 |
|
||||||
| 3 | Discrete Input | 10003 | PlcReset | PLC -> PC | PLC 请求上位机清状态/复位 |
|
| 3 | Discrete Input | 10003 | PlcAckRelease | 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` 为本流程主触发点
|
- `PcbArrived` 为本流程主触发点
|
||||||
- `PlcAckRelease` 为可选应答点,若 PLC 侧不需要可取消
|
- `PlcAckRelease` 为可选应答点,若 PLC 侧不需要可取消
|
||||||
- `PlcReset` 用于人工清故障、恢复空闲态或清除保持位
|
- `PlcReset` 用于人工清故障、恢复空闲态或清除保持位
|
||||||
- `10007 ~ 10050` 预留给后续 PLC -> 上位机扩展信号
|
- `10004 ~ 10050` 预留给后续 PLC -> 上位机扩展信号
|
||||||
|
|
||||||
### 8.4 上位机 -> PLC 点位(建议)
|
### 8.4 上位机 -> PLC 点位(建议)
|
||||||
|
|
||||||
@@ -476,83 +472,42 @@
|
|||||||
|
|
||||||
| 序号 | 地址类型 | 地址 | 点位名称 | 方向 | 说明 |
|
| 序号 | 地址类型 | 地址 | 点位名称 | 方向 | 说明 |
|
||||||
| --- | --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- | --- |
|
||||||
| 1 | Coil | 00051 | PcOnline | PC -> PLC | 上位机在线心跳位 |
|
| 1 | Coil | 00051 | PcBusy | PC -> PLC | 上位机正在处理当前 PCB |
|
||||||
| 2 | Coil | 00052 | PcBusy | PC -> PLC | 上位机正在处理当前 PCB |
|
| 2 | Coil | 00052 | ReleasePermit | PC -> PLC | 放行脉冲信号 |
|
||||||
| 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` 采用脉冲输出,不建议长时间保持
|
- `ReleasePermit` 采用脉冲输出,不建议长时间保持
|
||||||
- `ProcessDone = 1` 表示寄存器中的结果值已经稳定,PLC 可以在此时读取 `ResultCode`
|
- `PcBusy = 1` 表示当前板卡流程尚未结束
|
||||||
- `AlarmRaised` 建议保持到下一板开始或收到 `PlcReset` 后再清除
|
- `ResultCode` 在流程进行中和结束后都可读取,不依赖额外完成位
|
||||||
- `SystemFault` 用于表示上位机自身流程无法继续,例如 PLC 断连、串口异常、配置缺失、SFTP 连接异常等系统级故障
|
|
||||||
|
|
||||||
### 8.5 心跳建议
|
### 8.5 结果寄存器设计(建议)
|
||||||
|
|
||||||
- `PcOnline` 不建议常亮,建议采用**翻转心跳**方式
|
建议仅保留一个 Holding Register 表达最终结果。
|
||||||
- 上位机每 **500ms** 翻转一次 `PcOnline`
|
|
||||||
- PLC 若在 **3 秒** 内未检测到该位变化,则可判定上位机离线或通信异常
|
|
||||||
|
|
||||||
### 8.6 结果寄存器设计(建议)
|
|
||||||
|
|
||||||
建议额外使用 Holding Register 表达结果码和统计值。
|
|
||||||
|
|
||||||
| 序号 | 地址类型 | 地址 | 名称 | 说明 |
|
| 序号 | 地址类型 | 地址 | 名称 | 说明 |
|
||||||
| --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- |
|
||||||
| 1 | Holding Register | 40001 | ResultCode | 本次最终结果代码 |
|
| 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 推荐结果代码定义
|
### 8.6 推荐结果代码定义
|
||||||
|
|
||||||
| 代码 | 含义 |
|
| 代码 | 含义 |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| 0 | Idle / 无结果 |
|
| 0 | Idle / 无结果 |
|
||||||
| 1 | 处理中 |
|
| 1 | 处理中 |
|
||||||
| 10 | 扫码成功,文件存在,正常放行 |
|
| 10 | 扫码成功,文件存在,正常放行 |
|
||||||
| 20 | 扫码失败 3 次后放行 |
|
| 20 | 本次处理结果 NG,但按规则放行 |
|
||||||
| 30 | 扫码成功,文件未找到超时放行 |
|
| 90 | 系统故障 |
|
||||||
| 40 | PLC 通信异常 |
|
|
||||||
| 41 | 串口异常 |
|
|
||||||
| 42 | SFTP 连接或认证异常 |
|
|
||||||
| 43 | 安灯接口调用异常 |
|
|
||||||
| 44 | 配置异常 |
|
|
||||||
|
|
||||||
### 8.8 推荐流程状态码定义
|
### 8.7 点位清理策略
|
||||||
|
|
||||||
| 代码 | 含义 |
|
|
||||||
| --- | --- |
|
|
||||||
| 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`、`ResultCode`
|
||||||
- 流程处理中保持:`PcBusy`
|
- 流程处理中保持:`PcBusy`
|
||||||
- 放行时脉冲输出:`ReleasePermit`
|
- 放行时脉冲输出:`ReleasePermit`
|
||||||
- 流程结束后清理:`PcBusy`
|
- 流程结束后清理:`PcBusy`
|
||||||
- `PcOnline` 由心跳任务周期性翻转
|
- 故障恢复后在人工复位或 `PlcReset` 时清理 `ResultCode`
|
||||||
- `SystemFault` 在故障解除且收到人工复位或 `PlcReset` 后清除
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using System.Windows.Threading;
|
|||||||
using AxiOmron.PcbCheck.Options;
|
using AxiOmron.PcbCheck.Options;
|
||||||
using AxiOmron.PcbCheck.Services.Implementations;
|
using AxiOmron.PcbCheck.Services.Implementations;
|
||||||
using AxiOmron.PcbCheck.Services.Interfaces;
|
using AxiOmron.PcbCheck.Services.Interfaces;
|
||||||
|
using AxiOmron.PcbCheck.Utils;
|
||||||
using AxiOmron.PcbCheck.ViewModels;
|
using AxiOmron.PcbCheck.ViewModels;
|
||||||
using AxiOmron.PcbCheck.Views.Pages;
|
using AxiOmron.PcbCheck.Views.Pages;
|
||||||
using AxiOmron.PcbCheck.Views.Windows;
|
using AxiOmron.PcbCheck.Views.Windows;
|
||||||
@@ -74,7 +75,7 @@ public partial class App : Application
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _host.StopAsync(TimeSpan.FromSeconds(2)).ConfigureAwait(false);
|
await ShutdownHelper.StopHostAsync(_host, TimeSpan.FromSeconds(2));
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
using System.Windows;
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Windows;
|
||||||
[assembly:ThemeInfo(
|
|
||||||
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
|
[assembly:InternalsVisibleTo("AxiOmron.PcbCheck.Tests")]
|
||||||
//(used if a resource is not found in the page,
|
[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)
|
// or application resource dictionaries)
|
||||||
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
|
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
|
||||||
//(used if a resource is not found in the page,
|
//(used if a resource is not found in the page,
|
||||||
|
|||||||
@@ -67,7 +67,6 @@ public sealed class DesignTimeAppConfigService : IAppConfigService
|
|||||||
UnitId = 1,
|
UnitId = 1,
|
||||||
PollIntervalMs = 200,
|
PollIntervalMs = 200,
|
||||||
ConnectTimeoutMs = 3000,
|
ConnectTimeoutMs = 3000,
|
||||||
HeartbeatIntervalMs = 500,
|
|
||||||
ReleasePulseMs = 450,
|
ReleasePulseMs = 450,
|
||||||
ReleaseAckTimeoutMs = 2500
|
ReleaseAckTimeoutMs = 2500
|
||||||
},
|
},
|
||||||
@@ -111,9 +110,6 @@ public sealed class DesignTimeAppConfigService : IAppConfigService
|
|||||||
},
|
},
|
||||||
Workflow = new WorkflowOptions
|
Workflow = new WorkflowOptions
|
||||||
{
|
{
|
||||||
RequirePlcReady = true,
|
|
||||||
RequireAutoMode = true,
|
|
||||||
RequireStationEnable = true,
|
|
||||||
RequireManualResetAfterFault = true,
|
RequireManualResetAfterFault = true,
|
||||||
MaxUiLogEntries = 200,
|
MaxUiLogEntries = 200,
|
||||||
MaxBoardRecords = 100
|
MaxBoardRecords = 100
|
||||||
|
|||||||
@@ -32,19 +32,20 @@ public sealed class DesignTimeAppStateStore : IAppStateStore
|
|||||||
CurrentBarcode = "PCB240417000128",
|
CurrentBarcode = "PCB240417000128",
|
||||||
ResultDescription = "已扫码,等待 SFTP 文件确认",
|
ResultDescription = "已扫码,等待 SFTP 文件确认",
|
||||||
FaultMessage = string.Empty,
|
FaultMessage = string.Empty,
|
||||||
ScanTryCount = 1,
|
|
||||||
SftpTryCount = 2,
|
|
||||||
ResultCode = (ushort)WorkflowResultCode.Processing,
|
ResultCode = (ushort)WorkflowResultCode.Processing,
|
||||||
AlarmCode = (ushort)AlarmCode.None,
|
|
||||||
LastTriggeredAt = now.AddSeconds(-18),
|
LastTriggeredAt = now.AddSeconds(-18),
|
||||||
LastCompletedAt = now.AddMinutes(-2),
|
LastCompletedAt = now.AddMinutes(-2),
|
||||||
IsBusy = true,
|
IsBusy = true,
|
||||||
ProcessDone = false,
|
|
||||||
SystemFault = false,
|
|
||||||
AlarmRaised = false,
|
|
||||||
LastUpdatedAt = now
|
LastUpdatedAt = now
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_snapshot.PlcMonitorItems.Add(new PlcMonitorItem { GroupName = "Inputs", Name = "PcbArrived", Address = "10001", CurrentValue = "True", LastUpdatedAt = now.AddSeconds(-1) });
|
||||||
|
_snapshot.PlcMonitorItems.Add(new PlcMonitorItem { GroupName = "Inputs", Name = "PlcReset", Address = "10002", CurrentValue = "False", LastUpdatedAt = now.AddSeconds(-1) });
|
||||||
|
_snapshot.PlcMonitorItems.Add(new PlcMonitorItem { GroupName = "Inputs", Name = "PlcAckRelease", Address = "10003", CurrentValue = "False", LastUpdatedAt = now.AddSeconds(-1) });
|
||||||
|
_snapshot.PlcMonitorItems.Add(new PlcMonitorItem { GroupName = "Outputs", Name = "PcBusy", Address = "51", CurrentValue = "True", LastUpdatedAt = now.AddSeconds(-1) });
|
||||||
|
_snapshot.PlcMonitorItems.Add(new PlcMonitorItem { GroupName = "Outputs", Name = "ReleasePermit", Address = "52", CurrentValue = "False", LastUpdatedAt = now.AddSeconds(-1) });
|
||||||
|
_snapshot.PlcMonitorItems.Add(new PlcMonitorItem { GroupName = "Registers", Name = "ResultCode", Address = "40001", CurrentValue = ((ushort)WorkflowResultCode.Processing).ToString(), LastUpdatedAt = now.AddSeconds(-1) });
|
||||||
|
|
||||||
_logs = new List<UiLogEntry>
|
_logs = new List<UiLogEntry>
|
||||||
{
|
{
|
||||||
new() { Timestamp = now.AddSeconds(-2), Level = "Info", Message = "SFTP 第 2 次查询:正在检查目标文件。" },
|
new() { Timestamp = now.AddSeconds(-2), Level = "Info", Message = "SFTP 第 2 次查询:正在检查目标文件。" },
|
||||||
@@ -66,7 +67,6 @@ public sealed class DesignTimeAppStateStore : IAppStateStore
|
|||||||
ResultCode = (ushort)WorkflowResultCode.Passed,
|
ResultCode = (ushort)WorkflowResultCode.Passed,
|
||||||
ResultDescription = "OK 放行",
|
ResultDescription = "OK 放行",
|
||||||
ReleaseSent = true,
|
ReleaseSent = true,
|
||||||
AlarmRaised = false,
|
|
||||||
ExceptionSummary = string.Empty
|
ExceptionSummary = string.Empty
|
||||||
},
|
},
|
||||||
new()
|
new()
|
||||||
@@ -76,10 +76,9 @@ public sealed class DesignTimeAppStateStore : IAppStateStore
|
|||||||
Barcode = "PCB240417000126",
|
Barcode = "PCB240417000126",
|
||||||
ScanTryCount = 3,
|
ScanTryCount = 3,
|
||||||
SftpTryCount = 0,
|
SftpTryCount = 0,
|
||||||
ResultCode = (ushort)WorkflowResultCode.ScanFailedReleased,
|
ResultCode = (ushort)WorkflowResultCode.NgReleased,
|
||||||
ResultDescription = "扫码失败后放行",
|
ResultDescription = "扫码失败后放行",
|
||||||
ReleaseSent = true,
|
ReleaseSent = true,
|
||||||
AlarmRaised = true,
|
|
||||||
ExceptionSummary = "扫码连续失败三次"
|
ExceptionSummary = "扫码连续失败三次"
|
||||||
},
|
},
|
||||||
new()
|
new()
|
||||||
@@ -89,10 +88,9 @@ public sealed class DesignTimeAppStateStore : IAppStateStore
|
|||||||
Barcode = "PCB240417000127",
|
Barcode = "PCB240417000127",
|
||||||
ScanTryCount = 1,
|
ScanTryCount = 1,
|
||||||
SftpTryCount = 3,
|
SftpTryCount = 3,
|
||||||
ResultCode = (ushort)WorkflowResultCode.FileNotFoundReleased,
|
ResultCode = (ushort)WorkflowResultCode.NgReleased,
|
||||||
ResultDescription = "文件超时未找到后放行",
|
ResultDescription = "处理结果 NG,随后放行",
|
||||||
ReleaseSent = true,
|
ReleaseSent = true,
|
||||||
AlarmRaised = true,
|
|
||||||
ExceptionSummary = "SFTP 文件查询超时"
|
ExceptionSummary = "SFTP 文件查询超时"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ public sealed class DesignTimeViewModelLocator
|
|||||||
/// 获取系统设置设计时视图模型。
|
/// 获取系统设置设计时视图模型。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public SystemSettingViewModel SystemSettingViewModel
|
public SystemSettingViewModel SystemSettingViewModel
|
||||||
=> _systemSettingViewModel ??= new SystemSettingViewModel(_appConfigService, _sftpLookupService);
|
=> _systemSettingViewModel ??= new SystemSettingViewModel(_appConfigService, _sftpLookupService, _appStateStore, _dispatcherService);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 创建首页设计时视图模型。
|
/// 创建首页设计时视图模型。
|
||||||
|
|||||||
@@ -5,6 +5,11 @@ namespace AxiOmron.PcbCheck.Models;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class RuntimeSnapshot
|
public sealed class RuntimeSnapshot
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取 PLC 监控变量集合。
|
||||||
|
/// </summary>
|
||||||
|
public IList<PlcMonitorItem> PlcMonitorItems { get; } = new List<PlcMonitorItem>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取或设置 PLC 连接状态文本。
|
/// 获取或设置 PLC 连接状态文本。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -50,26 +55,11 @@ public sealed class RuntimeSnapshot
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string FaultMessage { get; set; } = string.Empty;
|
public string FaultMessage { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取或设置扫码次数。
|
|
||||||
/// </summary>
|
|
||||||
public int ScanTryCount { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取或设置 SFTP 查询次数。
|
|
||||||
/// </summary>
|
|
||||||
public int SftpTryCount { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取或设置结果代码。
|
/// 获取或设置结果代码。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ushort ResultCode { get; set; }
|
public ushort ResultCode { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取或设置报警代码。
|
|
||||||
/// </summary>
|
|
||||||
public ushort AlarmCode { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取或设置上次触发时间。
|
/// 获取或设置上次触发时间。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -85,21 +75,6 @@ public sealed class RuntimeSnapshot
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsBusy { get; set; }
|
public bool IsBusy { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取或设置是否已完成。
|
|
||||||
/// </summary>
|
|
||||||
public bool ProcessDone { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取或设置是否存在系统故障。
|
|
||||||
/// </summary>
|
|
||||||
public bool SystemFault { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取或设置是否已触发报警。
|
|
||||||
/// </summary>
|
|
||||||
public bool AlarmRaised { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取或设置上次状态刷新时间。
|
/// 获取或设置上次状态刷新时间。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -111,7 +86,7 @@ public sealed class RuntimeSnapshot
|
|||||||
/// <returns>新的运行态快照副本。</returns>
|
/// <returns>新的运行态快照副本。</returns>
|
||||||
public RuntimeSnapshot Clone()
|
public RuntimeSnapshot Clone()
|
||||||
{
|
{
|
||||||
return new RuntimeSnapshot
|
var clone = new RuntimeSnapshot
|
||||||
{
|
{
|
||||||
PlcStatus = PlcStatus,
|
PlcStatus = PlcStatus,
|
||||||
ScannerStatus = ScannerStatus,
|
ScannerStatus = ScannerStatus,
|
||||||
@@ -122,17 +97,18 @@ public sealed class RuntimeSnapshot
|
|||||||
CurrentBarcode = CurrentBarcode,
|
CurrentBarcode = CurrentBarcode,
|
||||||
ResultDescription = ResultDescription,
|
ResultDescription = ResultDescription,
|
||||||
FaultMessage = FaultMessage,
|
FaultMessage = FaultMessage,
|
||||||
ScanTryCount = ScanTryCount,
|
|
||||||
SftpTryCount = SftpTryCount,
|
|
||||||
ResultCode = ResultCode,
|
ResultCode = ResultCode,
|
||||||
AlarmCode = AlarmCode,
|
|
||||||
LastTriggeredAt = LastTriggeredAt,
|
LastTriggeredAt = LastTriggeredAt,
|
||||||
LastCompletedAt = LastCompletedAt,
|
LastCompletedAt = LastCompletedAt,
|
||||||
IsBusy = IsBusy,
|
IsBusy = IsBusy,
|
||||||
ProcessDone = ProcessDone,
|
|
||||||
SystemFault = SystemFault,
|
|
||||||
AlarmRaised = AlarmRaised,
|
|
||||||
LastUpdatedAt = LastUpdatedAt
|
LastUpdatedAt = LastUpdatedAt
|
||||||
};
|
};
|
||||||
|
|
||||||
|
foreach (PlcMonitorItem item in PlcMonitorItems)
|
||||||
|
{
|
||||||
|
clone.PlcMonitorItems.Add(item.Clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
return clone;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,34 +92,14 @@ public enum WorkflowResultCode : ushort
|
|||||||
ScanFailedReleased = 20,
|
ScanFailedReleased = 20,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 文件未找到超时后放行。
|
/// 处理完成但结果为 NG,随后放行。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
FileNotFoundReleased = 30,
|
NgReleased = 20,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// PLC 通信异常。
|
/// 系统故障。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
PlcCommunicationFault = 40,
|
Fault = 90
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 串口异常。
|
|
||||||
/// </summary>
|
|
||||||
ScannerFault = 41,
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// SFTP 连接或认证异常。
|
|
||||||
/// </summary>
|
|
||||||
SftpFault = 42,
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 安灯接口调用异常。
|
|
||||||
/// </summary>
|
|
||||||
AndonFault = 43,
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 配置异常。
|
|
||||||
/// </summary>
|
|
||||||
ConfigurationFault = 44
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -163,11 +143,6 @@ public enum AlarmCode : ushort
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class PlcSignalSnapshot
|
public sealed class PlcSignalSnapshot
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// 获取或设置 PLC 是否就绪。
|
|
||||||
/// </summary>
|
|
||||||
public bool PlcReady { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取或设置 PCB 是否到位。
|
/// 获取或设置 PCB 是否到位。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -183,16 +158,6 @@ public sealed class PlcSignalSnapshot
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool PlcAckRelease { get; set; }
|
public bool PlcAckRelease { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取或设置是否为自动模式。
|
|
||||||
/// </summary>
|
|
||||||
public bool AutoMode { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取或设置工位是否使能。
|
|
||||||
/// </summary>
|
|
||||||
public bool StationEnable { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取或设置本次快照采集时间。
|
/// 获取或设置本次快照采集时间。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -204,81 +169,21 @@ public sealed class PlcSignalSnapshot
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class PlcProcessState
|
public sealed class PlcProcessState
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// 获取或设置 PC 在线位。
|
|
||||||
/// </summary>
|
|
||||||
public bool PcOnline { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取或设置 PC 忙碌位。
|
/// 获取或设置 PC 忙碌位。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool PcBusy { get; set; }
|
public bool PcBusy { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取或设置扫码成功位。
|
|
||||||
/// </summary>
|
|
||||||
public bool ScanOk { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取或设置扫码失败位。
|
|
||||||
/// </summary>
|
|
||||||
public bool ScanNg { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取或设置文件找到位。
|
|
||||||
/// </summary>
|
|
||||||
public bool FileFound { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取或设置文件未找到位。
|
|
||||||
/// </summary>
|
|
||||||
public bool FileNotFound { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取或设置报警位。
|
|
||||||
/// </summary>
|
|
||||||
public bool AlarmRaised { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取或设置放行位。
|
/// 获取或设置放行位。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool ReleasePermit { get; set; }
|
public bool ReleasePermit { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取或设置流程完成位。
|
|
||||||
/// </summary>
|
|
||||||
public bool ProcessDone { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取或设置系统故障位。
|
|
||||||
/// </summary>
|
|
||||||
public bool SystemFault { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取或设置结果代码寄存器值。
|
/// 获取或设置结果代码寄存器值。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ushort ResultCode { get; set; }
|
public ushort ResultCode { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取或设置扫码次数寄存器值。
|
|
||||||
/// </summary>
|
|
||||||
public ushort ScanTryCount { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取或设置 SFTP 查询次数寄存器值。
|
|
||||||
/// </summary>
|
|
||||||
public ushort SftpTryCount { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取或设置报警代码寄存器值。
|
|
||||||
/// </summary>
|
|
||||||
public ushort AlarmCode { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取或设置流程状态代码寄存器值。
|
|
||||||
/// </summary>
|
|
||||||
public ushort FlowStateCode { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 创建当前状态对象的浅拷贝。
|
/// 创建当前状态对象的浅拷贝。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -287,25 +192,81 @@ public sealed class PlcProcessState
|
|||||||
{
|
{
|
||||||
return new PlcProcessState
|
return new PlcProcessState
|
||||||
{
|
{
|
||||||
PcOnline = PcOnline,
|
|
||||||
PcBusy = PcBusy,
|
PcBusy = PcBusy,
|
||||||
ScanOk = ScanOk,
|
|
||||||
ScanNg = ScanNg,
|
|
||||||
FileFound = FileFound,
|
|
||||||
FileNotFound = FileNotFound,
|
|
||||||
AlarmRaised = AlarmRaised,
|
|
||||||
ReleasePermit = ReleasePermit,
|
ReleasePermit = ReleasePermit,
|
||||||
ProcessDone = ProcessDone,
|
ResultCode = ResultCode
|
||||||
SystemFault = SystemFault,
|
|
||||||
ResultCode = ResultCode,
|
|
||||||
ScanTryCount = ScanTryCount,
|
|
||||||
SftpTryCount = SftpTryCount,
|
|
||||||
AlarmCode = AlarmCode,
|
|
||||||
FlowStateCode = FlowStateCode
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 表示 PLC 监控区域中的单个变量实时镜像。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PlcMonitorItem
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置变量分组。
|
||||||
|
/// </summary>
|
||||||
|
public string GroupName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置变量名称。
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置 PLC 变量地址。
|
||||||
|
/// </summary>
|
||||||
|
public string Address { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置当前值文本。
|
||||||
|
/// </summary>
|
||||||
|
public string CurrentValue { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置最近一次更新时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTimeOffset LastUpdatedAt { get; set; } = DateTimeOffset.Now;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建当前监控项的副本。
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>新的监控项对象。</returns>
|
||||||
|
public PlcMonitorItem Clone()
|
||||||
|
{
|
||||||
|
return new PlcMonitorItem
|
||||||
|
{
|
||||||
|
GroupName = GroupName,
|
||||||
|
Name = Name,
|
||||||
|
Address = Address,
|
||||||
|
CurrentValue = CurrentValue,
|
||||||
|
LastUpdatedAt = LastUpdatedAt
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 表示一次从 PLC 读取到的完整监控镜像快照。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PlcMonitorSnapshot
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置输入信号镜像。
|
||||||
|
/// </summary>
|
||||||
|
public PlcSignalSnapshot Inputs { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置输出与寄存器镜像。
|
||||||
|
/// </summary>
|
||||||
|
public PlcProcessState Outputs { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置本次镜像采集时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTimeOffset CapturedAt { get; set; } = DateTimeOffset.Now;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 表示扫码执行结果。
|
/// 表示扫码执行结果。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -559,35 +520,10 @@ public sealed class BoardProcessRecord
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 提供流程状态与 PLC 流程代码之间的映射方法。
|
/// 提供流程状态到界面文案的映射方法。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static class WorkflowStateExtensions
|
internal static class WorkflowStateExtensions
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// 将流程状态映射为 PLC 流程状态码。
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="state">待映射的流程状态。</param>
|
|
||||||
/// <returns>对应的流程状态码。</returns>
|
|
||||||
public static ushort ToFlowStateCode(this WorkflowState state)
|
|
||||||
{
|
|
||||||
return state switch
|
|
||||||
{
|
|
||||||
WorkflowState.Idle => 0,
|
|
||||||
WorkflowState.Triggered => 1,
|
|
||||||
WorkflowState.Scanning => 2,
|
|
||||||
WorkflowState.ScanRetrying => 3,
|
|
||||||
WorkflowState.CheckingSftp => 4,
|
|
||||||
WorkflowState.WaitingSftpRetry => 5,
|
|
||||||
WorkflowState.Releasing => 6,
|
|
||||||
WorkflowState.Completed => 7,
|
|
||||||
WorkflowState.Faulted => 8,
|
|
||||||
WorkflowState.ScanFailedReleased => 6,
|
|
||||||
WorkflowState.SftpPassed => 6,
|
|
||||||
WorkflowState.SftpTimeoutReleased => 6,
|
|
||||||
_ => 0
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 将流程状态转换为界面显示文本。
|
/// 将流程状态转换为界面显示文本。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -66,11 +66,6 @@ public sealed class PlcOptions
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int ConnectTimeoutMs { get; set; } = 3000;
|
public int ConnectTimeoutMs { get; set; } = 3000;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取或设置 PC 在线心跳翻转周期,单位为毫秒。
|
|
||||||
/// </summary>
|
|
||||||
public int HeartbeatIntervalMs { get; set; } = 500;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取或设置放行脉冲持续时间,单位为毫秒。
|
/// 获取或设置放行脉冲持续时间,单位为毫秒。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -275,21 +270,6 @@ public sealed class AndonOptions
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class WorkflowOptions
|
public sealed class WorkflowOptions
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// 获取或设置启动流程前是否要求 PLC 就绪。
|
|
||||||
/// </summary>
|
|
||||||
public bool RequirePlcReady { get; set; } = true;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取或设置启动流程前是否要求自动模式。
|
|
||||||
/// </summary>
|
|
||||||
public bool RequireAutoMode { get; set; } = true;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取或设置启动流程前是否要求工位使能。
|
|
||||||
/// </summary>
|
|
||||||
public bool RequireStationEnable { get; set; } = true;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取或设置故障后是否必须人工复位。
|
/// 获取或设置故障后是否必须人工复位。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -327,35 +307,20 @@ public sealed class SecurityOptions
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class PlcInputAddressOptions
|
public sealed class PlcInputAddressOptions
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// 获取或设置 PLC 就绪点位地址。
|
|
||||||
/// </summary>
|
|
||||||
public int PlcReady { get; set; } = 10001;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取或设置 PCB 到位点位地址。
|
/// 获取或设置 PCB 到位点位地址。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int PcbArrived { get; set; } = 10002;
|
public int PcbArrived { get; set; } = 10001;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取或设置 PLC 复位点位地址。
|
/// 获取或设置 PLC 复位点位地址。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int PlcReset { get; set; } = 10003;
|
public int PlcReset { get; set; } = 10002;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取或设置 PLC 放行应答点位地址。
|
/// 获取或设置 PLC 放行应答点位地址。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int PlcAckRelease { get; set; } = 10004;
|
public int PlcAckRelease { get; set; } = 10003;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取或设置自动模式点位地址。
|
|
||||||
/// </summary>
|
|
||||||
public int AutoMode { get; set; } = 10005;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取或设置工位使能点位地址。
|
|
||||||
/// </summary>
|
|
||||||
public int StationEnable { get; set; } = 10006;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -363,55 +328,15 @@ public sealed class PlcInputAddressOptions
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class PlcOutputAddressOptions
|
public sealed class PlcOutputAddressOptions
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// 获取或设置 PC 在线心跳位地址。
|
|
||||||
/// </summary>
|
|
||||||
public int PcOnline { get; set; } = 51;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取或设置 PC 忙碌位地址。
|
/// 获取或设置 PC 忙碌位地址。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int PcBusy { get; set; } = 52;
|
public int PcBusy { get; set; } = 51;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取或设置扫码成功位地址。
|
|
||||||
/// </summary>
|
|
||||||
public int ScanOk { get; set; } = 53;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取或设置扫码失败位地址。
|
|
||||||
/// </summary>
|
|
||||||
public int ScanNg { get; set; } = 54;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取或设置文件存在位地址。
|
|
||||||
/// </summary>
|
|
||||||
public int FileFound { get; set; } = 55;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取或设置文件未找到位地址。
|
|
||||||
/// </summary>
|
|
||||||
public int FileNotFound { get; set; } = 56;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取或设置报警位地址。
|
|
||||||
/// </summary>
|
|
||||||
public int AlarmRaised { get; set; } = 57;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取或设置放行位地址。
|
/// 获取或设置放行位地址。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int ReleasePermit { get; set; } = 58;
|
public int ReleasePermit { get; set; } = 52;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取或设置流程完成位地址。
|
|
||||||
/// </summary>
|
|
||||||
public int ProcessDone { get; set; } = 59;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取或设置系统故障位地址。
|
|
||||||
/// </summary>
|
|
||||||
public int SystemFault { get; set; } = 60;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -425,22 +350,4 @@ public sealed class PlcRegisterAddressOptions
|
|||||||
public int ResultCode { get; set; } = 40001;
|
public int ResultCode { get; set; } = 40001;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取或设置扫码次数寄存器地址。
|
|
||||||
/// </summary>
|
|
||||||
public int ScanTryCount { get; set; } = 40002;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取或设置 SFTP 查询次数寄存器地址。
|
|
||||||
/// </summary>
|
|
||||||
public int SftpTryCount { get; set; } = 40003;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取或设置报警代码寄存器地址。
|
|
||||||
/// </summary>
|
|
||||||
public int AlarmCode { get; set; } = 40004;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取或设置流程状态代码寄存器地址。
|
|
||||||
/// </summary>
|
|
||||||
public int FlowStateCode { get; set; } = 40005;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Project>
|
||||||
|
<PropertyGroup>
|
||||||
|
<Configuration>Release</Configuration>
|
||||||
|
<Platform>Any CPU</Platform>
|
||||||
|
<PublishDir>bin\Release\net8.0-windows\publish\</PublishDir>
|
||||||
|
<PublishProtocol>FileSystem</PublishProtocol>
|
||||||
|
<TargetFramework>net8.0-windows</TargetFramework>
|
||||||
|
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||||
|
<SelfContained>false</SelfContained>
|
||||||
|
<PublishSingleFile>true</PublishSingleFile>
|
||||||
|
<IncludeNativeLibrariesForSelfExtract>false</IncludeNativeLibrariesForSelfExtract>
|
||||||
|
<EnableCompressionInSingleFile>false</EnableCompressionInSingleFile>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
10
src/AxiOmron.PcbCheck/Properties/launchSettings.json
Normal file
10
src/AxiOmron.PcbCheck/Properties/launchSettings.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"profiles": {
|
||||||
|
"AxiOmron.PcbCheck": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"environmentVariables": {
|
||||||
|
"DOTNET_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,9 +29,9 @@ public sealed class AppLogger<TCategoryName> : IAppLogger<TCategoryName>
|
|||||||
/// 记录一条信息日志。
|
/// 记录一条信息日志。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="message">日志消息模板或文本。</param>
|
/// <param name="message">日志消息模板或文本。</param>
|
||||||
/// <param name="showInUi">是否同步显示到前台日志区域。</param>
|
/// <param name="showInUi">是否同步显示到前台日志区域。默认 <c>true</c>。</param>
|
||||||
/// <param name="args">日志模板参数。</param>
|
/// <param name="args">日志模板参数。</param>
|
||||||
public void LogInformation(string message, bool showInUi = false, params object?[] args)
|
public void LogInformation(string message, bool showInUi = true, params object?[] args)
|
||||||
{
|
{
|
||||||
_logger.LogInformation(message, args);
|
_logger.LogInformation(message, args);
|
||||||
PublishUiLog(LogLevel.Information, message, null, showInUi, args);
|
PublishUiLog(LogLevel.Information, message, null, showInUi, args);
|
||||||
@@ -41,9 +41,9 @@ public sealed class AppLogger<TCategoryName> : IAppLogger<TCategoryName>
|
|||||||
/// 记录一条警告日志。
|
/// 记录一条警告日志。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="message">日志消息模板或文本。</param>
|
/// <param name="message">日志消息模板或文本。</param>
|
||||||
/// <param name="showInUi">是否同步显示到前台日志区域。</param>
|
/// <param name="showInUi">是否同步显示到前台日志区域。默认 <c>true</c>。</param>
|
||||||
/// <param name="args">日志模板参数。</param>
|
/// <param name="args">日志模板参数。</param>
|
||||||
public void LogWarning(string message, bool showInUi = false, params object?[] args)
|
public void LogWarning(string message, bool showInUi = true, params object?[] args)
|
||||||
{
|
{
|
||||||
_logger.LogWarning(message, args);
|
_logger.LogWarning(message, args);
|
||||||
PublishUiLog(LogLevel.Warning, message, null, showInUi, args);
|
PublishUiLog(LogLevel.Warning, message, null, showInUi, args);
|
||||||
@@ -54,10 +54,10 @@ public sealed class AppLogger<TCategoryName> : IAppLogger<TCategoryName>
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="exception">异常对象。</param>
|
/// <param name="exception">异常对象。</param>
|
||||||
/// <param name="message">日志消息模板或文本。</param>
|
/// <param name="message">日志消息模板或文本。</param>
|
||||||
/// <param name="showInUi">是否同步显示到前台日志区域。</param>
|
/// <param name="showInUi">是否同步显示到前台日志区域。默认 <c>true</c>。</param>
|
||||||
/// <param name="args">日志模板参数。</param>
|
/// <param name="args">日志模板参数。</param>
|
||||||
/// <exception cref="ArgumentNullException">当 <paramref name="exception"/> 为 <see langword="null"/> 时抛出。</exception>
|
/// <exception cref="ArgumentNullException">当 <paramref name="exception"/> 为 <see langword="null"/> 时抛出。</exception>
|
||||||
public void LogWarning(Exception exception, string message, bool showInUi = false, params object?[] args)
|
public void LogWarning(Exception exception, string message, bool showInUi = true, params object?[] args)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(exception);
|
ArgumentNullException.ThrowIfNull(exception);
|
||||||
_logger.LogWarning(exception, message, args);
|
_logger.LogWarning(exception, message, args);
|
||||||
@@ -68,9 +68,9 @@ public sealed class AppLogger<TCategoryName> : IAppLogger<TCategoryName>
|
|||||||
/// 记录一条错误日志。
|
/// 记录一条错误日志。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="message">日志消息模板或文本。</param>
|
/// <param name="message">日志消息模板或文本。</param>
|
||||||
/// <param name="showInUi">是否同步显示到前台日志区域。</param>
|
/// <param name="showInUi">是否同步显示到前台日志区域。默认 <c>true</c>。</param>
|
||||||
/// <param name="args">日志模板参数。</param>
|
/// <param name="args">日志模板参数。</param>
|
||||||
public void LogError(string message, bool showInUi = false, params object?[] args)
|
public void LogError(string message, bool showInUi = true, params object?[] args)
|
||||||
{
|
{
|
||||||
_logger.LogError(message, args);
|
_logger.LogError(message, args);
|
||||||
PublishUiLog(LogLevel.Error, message, null, showInUi, args);
|
PublishUiLog(LogLevel.Error, message, null, showInUi, args);
|
||||||
@@ -81,10 +81,10 @@ public sealed class AppLogger<TCategoryName> : IAppLogger<TCategoryName>
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="exception">异常对象。</param>
|
/// <param name="exception">异常对象。</param>
|
||||||
/// <param name="message">日志消息模板或文本。</param>
|
/// <param name="message">日志消息模板或文本。</param>
|
||||||
/// <param name="showInUi">是否同步显示到前台日志区域。</param>
|
/// <param name="showInUi">是否同步显示到前台日志区域。默认 <c>true</c>。</param>
|
||||||
/// <param name="args">日志模板参数。</param>
|
/// <param name="args">日志模板参数。</param>
|
||||||
/// <exception cref="ArgumentNullException">当 <paramref name="exception"/> 为 <see langword="null"/> 时抛出。</exception>
|
/// <exception cref="ArgumentNullException">当 <paramref name="exception"/> 为 <see langword="null"/> 时抛出。</exception>
|
||||||
public void LogError(Exception exception, string message, bool showInUi = false, params object?[] args)
|
public void LogError(Exception exception, string message, bool showInUi = true, params object?[] args)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(exception);
|
ArgumentNullException.ThrowIfNull(exception);
|
||||||
_logger.LogError(exception, message, args);
|
_logger.LogError(exception, message, args);
|
||||||
|
|||||||
@@ -44,12 +44,9 @@ public sealed class ModbusTcpPlcService : IPlcService, IDisposable
|
|||||||
|
|
||||||
return new PlcSignalSnapshot
|
return new PlcSignalSnapshot
|
||||||
{
|
{
|
||||||
PlcReady = ReadDiscrete(_options.Inputs.PlcReady),
|
|
||||||
PcbArrived = ReadDiscrete(_options.Inputs.PcbArrived),
|
PcbArrived = ReadDiscrete(_options.Inputs.PcbArrived),
|
||||||
PlcReset = ReadDiscrete(_options.Inputs.PlcReset),
|
PlcReset = ReadDiscrete(_options.Inputs.PlcReset),
|
||||||
PlcAckRelease = ReadDiscrete(_options.Inputs.PlcAckRelease),
|
PlcAckRelease = ReadDiscrete(_options.Inputs.PlcAckRelease),
|
||||||
AutoMode = ReadDiscrete(_options.Inputs.AutoMode),
|
|
||||||
StationEnable = ReadDiscrete(_options.Inputs.StationEnable),
|
|
||||||
CapturedAt = DateTimeOffset.Now
|
CapturedAt = DateTimeOffset.Now
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -64,6 +61,49 @@ public sealed class ModbusTcpPlcService : IPlcService, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 读取 PLC 当前监控镜像,包括输入、输出和寄存器的实际值。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cancellationToken">取消令牌。</param>
|
||||||
|
/// <returns>监控镜像快照。</returns>
|
||||||
|
public async Task<PlcMonitorSnapshot> ReadMonitorSnapshotAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await _ioLock.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
EnsureConnected();
|
||||||
|
|
||||||
|
DateTimeOffset capturedAt = DateTimeOffset.Now;
|
||||||
|
return new PlcMonitorSnapshot
|
||||||
|
{
|
||||||
|
Inputs = new PlcSignalSnapshot
|
||||||
|
{
|
||||||
|
PcbArrived = ReadDiscrete(_options.Inputs.PcbArrived),
|
||||||
|
PlcReset = ReadDiscrete(_options.Inputs.PlcReset),
|
||||||
|
PlcAckRelease = ReadDiscrete(_options.Inputs.PlcAckRelease),
|
||||||
|
CapturedAt = capturedAt
|
||||||
|
},
|
||||||
|
Outputs = new PlcProcessState
|
||||||
|
{
|
||||||
|
PcBusy = ReadCoil(_options.Outputs.PcBusy),
|
||||||
|
ReleasePermit = ReadCoil(_options.Outputs.ReleasePermit),
|
||||||
|
ResultCode = ReadRegister(_options.Registers.ResultCode)
|
||||||
|
},
|
||||||
|
CapturedAt = capturedAt
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
DisconnectUnsafe();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_ioLock.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 写入 PLC 输出状态与寄存器值。
|
/// 写入 PLC 输出状态与寄存器值。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -160,6 +200,32 @@ public sealed class ModbusTcpPlcService : IPlcService, IDisposable
|
|||||||
return result.Value;
|
return result.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 读取单个输出线圈位。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="address">线圈地址。</param>
|
||||||
|
/// <returns>读取到的布尔值。</returns>
|
||||||
|
private bool ReadCoil(int address)
|
||||||
|
{
|
||||||
|
var client = _client ?? throw new InvalidOperationException("PLC 客户端尚未初始化。");
|
||||||
|
var result = client.ReadCoil(address, _options.UnitId, 1);
|
||||||
|
EnsureSuccess(result.IsSucceed, result.Err, $"读取线圈失败,地址={address}");
|
||||||
|
return result.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 读取单个保持寄存器。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="address">寄存器地址。</param>
|
||||||
|
/// <returns>读取到的寄存器值。</returns>
|
||||||
|
private ushort ReadRegister(int address)
|
||||||
|
{
|
||||||
|
var client = _client ?? throw new InvalidOperationException("PLC 客户端尚未初始化。");
|
||||||
|
var result = client.ReadUInt16(address, _options.UnitId, 3);
|
||||||
|
EnsureSuccess(result.IsSucceed, result.Err, $"读取保持寄存器失败,地址={address}");
|
||||||
|
return result.Value;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 写入所有发生变化的线圈位与寄存器。
|
/// 写入所有发生变化的线圈位与寄存器。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -168,22 +234,10 @@ public sealed class ModbusTcpPlcService : IPlcService, IDisposable
|
|||||||
{
|
{
|
||||||
var previous = _lastWrittenState;
|
var previous = _lastWrittenState;
|
||||||
|
|
||||||
WriteSingleCoilIfChanged(previous?.PcOnline, state.PcOnline, _options.Outputs.PcOnline);
|
|
||||||
WriteSingleCoilIfChanged(previous?.PcBusy, state.PcBusy, _options.Outputs.PcBusy);
|
WriteSingleCoilIfChanged(previous?.PcBusy, state.PcBusy, _options.Outputs.PcBusy);
|
||||||
WriteSingleCoilIfChanged(previous?.ScanOk, state.ScanOk, _options.Outputs.ScanOk);
|
|
||||||
WriteSingleCoilIfChanged(previous?.ScanNg, state.ScanNg, _options.Outputs.ScanNg);
|
|
||||||
WriteSingleCoilIfChanged(previous?.FileFound, state.FileFound, _options.Outputs.FileFound);
|
|
||||||
WriteSingleCoilIfChanged(previous?.FileNotFound, state.FileNotFound, _options.Outputs.FileNotFound);
|
|
||||||
WriteSingleCoilIfChanged(previous?.AlarmRaised, state.AlarmRaised, _options.Outputs.AlarmRaised);
|
|
||||||
WriteSingleCoilIfChanged(previous?.ReleasePermit, state.ReleasePermit, _options.Outputs.ReleasePermit);
|
WriteSingleCoilIfChanged(previous?.ReleasePermit, state.ReleasePermit, _options.Outputs.ReleasePermit);
|
||||||
WriteSingleCoilIfChanged(previous?.ProcessDone, state.ProcessDone, _options.Outputs.ProcessDone);
|
|
||||||
WriteSingleCoilIfChanged(previous?.SystemFault, state.SystemFault, _options.Outputs.SystemFault);
|
|
||||||
|
|
||||||
WriteSingleRegisterIfChanged(previous?.ResultCode, state.ResultCode, _options.Registers.ResultCode);
|
WriteSingleRegisterIfChanged(previous?.ResultCode, state.ResultCode, _options.Registers.ResultCode);
|
||||||
WriteSingleRegisterIfChanged(previous?.ScanTryCount, state.ScanTryCount, _options.Registers.ScanTryCount);
|
|
||||||
WriteSingleRegisterIfChanged(previous?.SftpTryCount, state.SftpTryCount, _options.Registers.SftpTryCount);
|
|
||||||
WriteSingleRegisterIfChanged(previous?.AlarmCode, state.AlarmCode, _options.Registers.AlarmCode);
|
|
||||||
WriteSingleRegisterIfChanged(previous?.FlowStateCode, state.FlowStateCode, _options.Registers.FlowStateCode);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -217,8 +271,8 @@ public sealed class ModbusTcpPlcService : IPlcService, IDisposable
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var client = _client ?? throw new InvalidOperationException("PLC 客户端尚未初始化。");
|
var client = _client ?? throw new InvalidOperationException("PLC 客户端尚未初始化。");
|
||||||
var result = client.Write(address.ToString(), current, _options.UnitId, 6);
|
var result = client.Write(address.ToString(), current, _options.UnitId);
|
||||||
EnsureSuccess(result.IsSucceed, result.Err, $"写入保持寄存器失败,地址={address},值={current}");
|
EnsureSuccess(result.IsSucceed, result.Err, $"写入保持寄存器失败,地址={address},值={current}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,8 +22,6 @@ public sealed class WorkflowHostedService : BackgroundService, IWorkflowControlS
|
|||||||
private readonly SemaphoreSlim _plcWriteLock = new(1, 1);
|
private readonly SemaphoreSlim _plcWriteLock = new(1, 1);
|
||||||
private readonly object _stateSyncRoot = new();
|
private readonly object _stateSyncRoot = new();
|
||||||
private PlcProcessState _plcState = new();
|
private PlcProcessState _plcState = new();
|
||||||
private bool _heartbeatState;
|
|
||||||
private DateTimeOffset _lastHeartbeatToggleAt = DateTimeOffset.MinValue;
|
|
||||||
private bool _faultLatched;
|
private bool _faultLatched;
|
||||||
private bool _processingActive;
|
private bool _processingActive;
|
||||||
private bool _lastPcbArrived;
|
private bool _lastPcbArrived;
|
||||||
@@ -33,7 +31,6 @@ public sealed class WorkflowHostedService : BackgroundService, IWorkflowControlS
|
|||||||
private int _scanTryCount;
|
private int _scanTryCount;
|
||||||
private int _sftpTryCount;
|
private int _sftpTryCount;
|
||||||
private WorkflowResultCode _resultCode = WorkflowResultCode.None;
|
private WorkflowResultCode _resultCode = WorkflowResultCode.None;
|
||||||
private AlarmCode _alarmCode = AlarmCode.None;
|
|
||||||
private CancellationTokenSource? _activeWorkflowCts;
|
private CancellationTokenSource? _activeWorkflowCts;
|
||||||
private Task? _activeWorkflowTask;
|
private Task? _activeWorkflowTask;
|
||||||
|
|
||||||
@@ -73,16 +70,18 @@ public sealed class WorkflowHostedService : BackgroundService, IWorkflowControlS
|
|||||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
{
|
{
|
||||||
_appLogger.LogInformation("流程服务已启动,等待 PLC 到位信号。", true);
|
_appLogger.LogInformation("流程服务已启动,等待 PLC 到位信号。", true);
|
||||||
|
await ProbePlcOnStartupAsync(stoppingToken).ConfigureAwait(false);
|
||||||
await ProbeSftpOnStartupAsync(stoppingToken).ConfigureAwait(false);
|
await ProbeSftpOnStartupAsync(stoppingToken).ConfigureAwait(false);
|
||||||
|
await ProbeScannerOnStartupAsync(stoppingToken).ConfigureAwait(false);
|
||||||
PublishRuntimeState(WorkflowState.Idle, "等待 PCB 到位");
|
PublishRuntimeState(WorkflowState.Idle, "等待 PCB 到位");
|
||||||
|
|
||||||
while (!stoppingToken.IsCancellationRequested)
|
while (!stoppingToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await UpdateHeartbeatIfDueAsync(stoppingToken).ConfigureAwait(false);
|
|
||||||
var signals = await _plcService.ReadSignalsAsync(stoppingToken).ConfigureAwait(false);
|
var signals = await _plcService.ReadSignalsAsync(stoppingToken).ConfigureAwait(false);
|
||||||
HandleSignalSnapshot(signals);
|
HandleSignalSnapshot(signals);
|
||||||
|
await RefreshPlcMonitorSnapshotAsync(stoppingToken).ConfigureAwait(false);
|
||||||
|
|
||||||
if (signals.PlcReset)
|
if (signals.PlcReset)
|
||||||
{
|
{
|
||||||
@@ -105,7 +104,8 @@ public sealed class WorkflowHostedService : BackgroundService, IWorkflowControlS
|
|||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_appLogger.LogError(ex, "后台流程轮询异常");
|
_appLogger.LogError(ex, "后台流程轮询异常");
|
||||||
await EnterFaultedAsync(WorkflowResultCode.PlcCommunicationFault, AlarmCode.PlcFault, $"后台轮询异常: {ex.Message}", CancellationToken.None).ConfigureAwait(false);
|
UpdateSnapshot(snapshot => snapshot.PlcStatus = $"连接失败: {ex.Message}");
|
||||||
|
await EnterFaultedAsync(WorkflowResultCode.Fault, AlarmCode.PlcFault, $"后台轮询异常: {ex.Message}", CancellationToken.None).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
await Task.Delay(_config.Plc.PollIntervalMs, stoppingToken).ConfigureAwait(false);
|
await Task.Delay(_config.Plc.PollIntervalMs, stoppingToken).ConfigureAwait(false);
|
||||||
@@ -114,6 +114,34 @@ public sealed class WorkflowHostedService : BackgroundService, IWorkflowControlS
|
|||||||
await CancelActiveWorkflowAsync(CancellationToken.None).ConfigureAwait(false);
|
await CancelActiveWorkflowAsync(CancellationToken.None).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 在应用启动后执行一次 PLC 探活,主动建立连接并刷新 PLC 实时镜像。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cancellationToken">取消令牌。若应用正在关闭,则中止本次探活。</param>
|
||||||
|
/// <returns>表示探活完成的任务。</returns>
|
||||||
|
public async Task ProbePlcOnStartupAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _plcService.ForceReconnectAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
await RefreshPlcMonitorSnapshotAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
UpdateSnapshot(snapshot => snapshot.PlcStatus = "已连接");
|
||||||
|
_appLogger.LogInformation("启动时 PLC 探活成功。", true);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
_appLogger.LogInformation("启动时 PLC 探活已取消。");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_appLogger.LogError(ex, "启动时 PLC 探活异常");
|
||||||
|
UpdateSnapshot(snapshot =>
|
||||||
|
{
|
||||||
|
snapshot.PlcStatus = $"连接失败: {ex.Message}";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 在应用启动后执行一次 SFTP 探活,仅更新状态与日志,不阻断程序运行。
|
/// 在应用启动后执行一次 SFTP 探活,仅更新状态与日志,不阻断程序运行。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -172,11 +200,7 @@ public sealed class WorkflowHostedService : BackgroundService, IWorkflowControlS
|
|||||||
{
|
{
|
||||||
await CancelActiveWorkflowAsync(cancellationToken).ConfigureAwait(false);
|
await CancelActiveWorkflowAsync(cancellationToken).ConfigureAwait(false);
|
||||||
ResetProcessStateCore();
|
ResetProcessStateCore();
|
||||||
await WritePlcStateAsync(state =>
|
await WritePlcStateAsync(ResetResultBits, cancellationToken).ConfigureAwait(false);
|
||||||
{
|
|
||||||
ResetResultBits(state);
|
|
||||||
state.FlowStateCode = WorkflowState.Idle.ToFlowStateCode();
|
|
||||||
}, cancellationToken).ConfigureAwait(false);
|
|
||||||
PublishRuntimeState(WorkflowState.Idle, "已复位,等待 PCB 到位");
|
PublishRuntimeState(WorkflowState.Idle, "已复位,等待 PCB 到位");
|
||||||
_appLogger.LogInformation("已执行流程复位。", true);
|
_appLogger.LogInformation("已执行流程复位。", true);
|
||||||
}
|
}
|
||||||
@@ -236,6 +260,41 @@ public sealed class WorkflowHostedService : BackgroundService, IWorkflowControlS
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 在应用启动后执行一次扫码枪串口探活,检测端口是否被占用并更新状态。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cancellationToken">取消令牌。若应用正在关闭,则中止本次探活。</param>
|
||||||
|
/// <returns>表示探活完成的任务。</returns>
|
||||||
|
public async Task ProbeScannerOnStartupAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
bool connected = await _scannerService.TestConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
UpdateSnapshot(snapshot =>
|
||||||
|
{
|
||||||
|
snapshot.ScannerStatus = connected ? "在线" : "离线";
|
||||||
|
});
|
||||||
|
|
||||||
|
if (connected)
|
||||||
|
{
|
||||||
|
_appLogger.LogInformation("启动时扫码枪串口探活成功。", true);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_appLogger.LogWarning("启动时扫码枪串口探活失败: 端口可能被占用或设备未连接。", true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
_appLogger.LogInformation("启动时扫码枪串口探活已取消。");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_appLogger.LogError(ex, "启动时扫码枪串口探活异常");
|
||||||
|
UpdateSnapshot(snapshot => snapshot.ScannerStatus = "异常");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 手动重连扫码枪。
|
/// 手动重连扫码枪。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -317,11 +376,21 @@ public sealed class WorkflowHostedService : BackgroundService, IWorkflowControlS
|
|||||||
{
|
{
|
||||||
UpdateSnapshot(snapshot =>
|
UpdateSnapshot(snapshot =>
|
||||||
{
|
{
|
||||||
snapshot.PlcStatus = signals.PlcReady ? "已连接 / PLC 就绪" : "已连接 / PLC 未就绪";
|
snapshot.PlcStatus = "已连接";
|
||||||
if (!_processingActive && signals.AutoMode && signals.StationEnable)
|
});
|
||||||
{
|
}
|
||||||
snapshot.ResultDescription = "满足接板条件";
|
|
||||||
}
|
/// <summary>
|
||||||
|
/// 读取 PLC 实时镜像,并刷新首页监控区展示数据。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cancellationToken">取消令牌。</param>
|
||||||
|
/// <returns>表示刷新完成的任务。</returns>
|
||||||
|
private async Task RefreshPlcMonitorSnapshotAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
PlcMonitorSnapshot monitorSnapshot = await _plcService.ReadMonitorSnapshotAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
UpdateSnapshot(snapshot =>
|
||||||
|
{
|
||||||
|
ReplacePlcMonitorItems(snapshot, monitorSnapshot);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -344,21 +413,6 @@ public sealed class WorkflowHostedService : BackgroundService, IWorkflowControlS
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_config.Workflow.RequirePlcReady && !signals.PlcReady)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_config.Workflow.RequireAutoMode && !signals.AutoMode)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_config.Workflow.RequireStationEnable && !signals.StationEnable)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -416,7 +470,7 @@ public sealed class WorkflowHostedService : BackgroundService, IWorkflowControlS
|
|||||||
_appLogger.LogError(ex, "后台流程执行异常");
|
_appLogger.LogError(ex, "后台流程执行异常");
|
||||||
if (!stoppingToken.IsCancellationRequested)
|
if (!stoppingToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
await EnterFaultedAsync(WorkflowResultCode.PlcCommunicationFault, AlarmCode.PlcFault, $"后台流程执行异常: {ex.Message}", CancellationToken.None).ConfigureAwait(false);
|
await EnterFaultedAsync(WorkflowResultCode.Fault, AlarmCode.PlcFault, $"后台流程执行异常: {ex.Message}", CancellationToken.None).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@@ -540,12 +594,11 @@ public sealed class WorkflowHostedService : BackgroundService, IWorkflowControlS
|
|||||||
UpdateSnapshot(snapshot =>
|
UpdateSnapshot(snapshot =>
|
||||||
{
|
{
|
||||||
snapshot.ScannerStatus = scanResult.DeviceConnected ? "在线" : "离线";
|
snapshot.ScannerStatus = scanResult.DeviceConnected ? "在线" : "离线";
|
||||||
snapshot.ScanTryCount = attempt;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (scanResult.IsSystemError)
|
if (scanResult.IsSystemError)
|
||||||
{
|
{
|
||||||
await EnterFaultedAsync(WorkflowResultCode.ScannerFault, AlarmCode.ScannerFault, scanResult.ErrorMessage, cancellationToken).ConfigureAwait(false);
|
await EnterFaultedAsync(WorkflowResultCode.Fault, AlarmCode.ScannerFault, scanResult.ErrorMessage, cancellationToken).ConfigureAwait(false);
|
||||||
return string.Empty;
|
return string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -554,17 +607,12 @@ public sealed class WorkflowHostedService : BackgroundService, IWorkflowControlS
|
|||||||
lock (_stateSyncRoot)
|
lock (_stateSyncRoot)
|
||||||
{
|
{
|
||||||
_currentBarcode = scanResult.Barcode;
|
_currentBarcode = scanResult.Barcode;
|
||||||
_alarmCode = AlarmCode.None;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await ApplyProcessStateAsync(
|
await ApplyProcessStateAsync(
|
||||||
WorkflowState.CheckingSftp,
|
WorkflowState.CheckingSftp,
|
||||||
$"扫码成功: {scanResult.Barcode}",
|
$"扫码成功: {scanResult.Barcode}",
|
||||||
state =>
|
_ => { },
|
||||||
{
|
|
||||||
state.ScanOk = true;
|
|
||||||
state.ScanNg = false;
|
|
||||||
},
|
|
||||||
cancellationToken).ConfigureAwait(false);
|
cancellationToken).ConfigureAwait(false);
|
||||||
_appLogger.LogInformation($"第 {attempt} 次扫码成功,条码={scanResult.Barcode}", true);
|
_appLogger.LogInformation($"第 {attempt} 次扫码成功,条码={scanResult.Barcode}", true);
|
||||||
return scanResult.Barcode;
|
return scanResult.Barcode;
|
||||||
@@ -576,29 +624,20 @@ public sealed class WorkflowHostedService : BackgroundService, IWorkflowControlS
|
|||||||
await ApplyProcessStateAsync(
|
await ApplyProcessStateAsync(
|
||||||
WorkflowState.ScanRetrying,
|
WorkflowState.ScanRetrying,
|
||||||
$"扫码失败,等待第 {attempt + 1} 次重试",
|
$"扫码失败,等待第 {attempt + 1} 次重试",
|
||||||
state =>
|
_ => { },
|
||||||
{
|
|
||||||
state.ScanOk = false;
|
|
||||||
state.ScanNg = false;
|
|
||||||
},
|
|
||||||
cancellationToken).ConfigureAwait(false);
|
cancellationToken).ConfigureAwait(false);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (_stateSyncRoot)
|
lock (_stateSyncRoot)
|
||||||
{
|
{
|
||||||
_resultCode = WorkflowResultCode.ScanFailedReleased;
|
_resultCode = WorkflowResultCode.NgReleased;
|
||||||
_alarmCode = _config.Andon.EnableScanFailAlarm ? AlarmCode.ScanFailed : AlarmCode.None;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await ApplyProcessStateAsync(
|
await ApplyProcessStateAsync(
|
||||||
WorkflowState.ScanFailedReleased,
|
WorkflowState.ScanFailedReleased,
|
||||||
"扫码连续失败 3 次,进入报警放行",
|
"扫码连续失败 3 次,进入报警放行",
|
||||||
state =>
|
_ => { },
|
||||||
{
|
|
||||||
state.ScanOk = false;
|
|
||||||
state.ScanNg = true;
|
|
||||||
},
|
|
||||||
cancellationToken).ConfigureAwait(false);
|
cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
await RaiseAlarmIfNeededAsync(
|
await RaiseAlarmIfNeededAsync(
|
||||||
@@ -643,7 +682,6 @@ public sealed class WorkflowHostedService : BackgroundService, IWorkflowControlS
|
|||||||
UpdateSnapshot(snapshot =>
|
UpdateSnapshot(snapshot =>
|
||||||
{
|
{
|
||||||
snapshot.SftpStatus = outcome.ConnectionSucceeded ? "在线" : (outcome.IsSystemError ? "异常" : "未验证");
|
snapshot.SftpStatus = outcome.ConnectionSucceeded ? "在线" : (outcome.IsSystemError ? "异常" : "未验证");
|
||||||
snapshot.SftpTryCount = _sftpTryCount;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (outcome.Exists)
|
if (outcome.Exists)
|
||||||
@@ -651,17 +689,12 @@ public sealed class WorkflowHostedService : BackgroundService, IWorkflowControlS
|
|||||||
lock (_stateSyncRoot)
|
lock (_stateSyncRoot)
|
||||||
{
|
{
|
||||||
_resultCode = WorkflowResultCode.Passed;
|
_resultCode = WorkflowResultCode.Passed;
|
||||||
_alarmCode = AlarmCode.None;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await ApplyProcessStateAsync(
|
await ApplyProcessStateAsync(
|
||||||
WorkflowState.SftpPassed,
|
WorkflowState.SftpPassed,
|
||||||
$"文件已找到: {outcome.MatchedFilePath}",
|
$"文件已找到: {outcome.MatchedFilePath}",
|
||||||
state =>
|
_ => { },
|
||||||
{
|
|
||||||
state.FileFound = true;
|
|
||||||
state.FileNotFound = false;
|
|
||||||
},
|
|
||||||
cancellationToken).ConfigureAwait(false);
|
cancellationToken).ConfigureAwait(false);
|
||||||
_appLogger.LogInformation($"SFTP 文件命中成功: {outcome.MatchedFilePath}", true);
|
_appLogger.LogInformation($"SFTP 文件命中成功: {outcome.MatchedFilePath}", true);
|
||||||
return;
|
return;
|
||||||
@@ -669,13 +702,13 @@ public sealed class WorkflowHostedService : BackgroundService, IWorkflowControlS
|
|||||||
|
|
||||||
if (outcome.IsConfigurationError)
|
if (outcome.IsConfigurationError)
|
||||||
{
|
{
|
||||||
await EnterFaultedAsync(WorkflowResultCode.ConfigurationFault, AlarmCode.SftpFault, outcome.ErrorMessage, cancellationToken).ConfigureAwait(false);
|
await EnterFaultedAsync(WorkflowResultCode.Fault, AlarmCode.SftpFault, outcome.ErrorMessage, cancellationToken).ConfigureAwait(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (outcome.IsSystemError)
|
if (outcome.IsSystemError)
|
||||||
{
|
{
|
||||||
await EnterFaultedAsync(WorkflowResultCode.SftpFault, AlarmCode.SftpFault, outcome.ErrorMessage, cancellationToken).ConfigureAwait(false);
|
await EnterFaultedAsync(WorkflowResultCode.Fault, AlarmCode.SftpFault, outcome.ErrorMessage, cancellationToken).ConfigureAwait(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -693,18 +726,13 @@ public sealed class WorkflowHostedService : BackgroundService, IWorkflowControlS
|
|||||||
|
|
||||||
lock (_stateSyncRoot)
|
lock (_stateSyncRoot)
|
||||||
{
|
{
|
||||||
_resultCode = WorkflowResultCode.FileNotFoundReleased;
|
_resultCode = WorkflowResultCode.NgReleased;
|
||||||
_alarmCode = _config.Andon.EnableFileNotFoundAlarm ? AlarmCode.FileNotFound : AlarmCode.None;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await ApplyProcessStateAsync(
|
await ApplyProcessStateAsync(
|
||||||
WorkflowState.SftpTimeoutReleased,
|
WorkflowState.SftpTimeoutReleased,
|
||||||
"文件未找到超时,按规则放行",
|
"文件未找到超时,按规则放行",
|
||||||
state =>
|
_ => { },
|
||||||
{
|
|
||||||
state.FileFound = false;
|
|
||||||
state.FileNotFound = true;
|
|
||||||
},
|
|
||||||
cancellationToken).ConfigureAwait(false);
|
cancellationToken).ConfigureAwait(false);
|
||||||
_appLogger.LogWarning($"SFTP 文件未命中达到上限,条码={barcode}", true);
|
_appLogger.LogWarning($"SFTP 文件未命中达到上限,条码={barcode}", true);
|
||||||
|
|
||||||
@@ -787,7 +815,6 @@ public sealed class WorkflowHostedService : BackgroundService, IWorkflowControlS
|
|||||||
state =>
|
state =>
|
||||||
{
|
{
|
||||||
state.ReleasePermit = false;
|
state.ReleasePermit = false;
|
||||||
state.ProcessDone = true;
|
|
||||||
state.PcBusy = false;
|
state.PcBusy = false;
|
||||||
},
|
},
|
||||||
cancellationToken).ConfigureAwait(false);
|
cancellationToken).ConfigureAwait(false);
|
||||||
@@ -795,7 +822,6 @@ public sealed class WorkflowHostedService : BackgroundService, IWorkflowControlS
|
|||||||
var completedAt = DateTimeOffset.Now;
|
var completedAt = DateTimeOffset.Now;
|
||||||
UpdateSnapshot(snapshot =>
|
UpdateSnapshot(snapshot =>
|
||||||
{
|
{
|
||||||
snapshot.ProcessDone = true;
|
|
||||||
snapshot.IsBusy = false;
|
snapshot.IsBusy = false;
|
||||||
snapshot.LastCompletedAt = completedAt;
|
snapshot.LastCompletedAt = completedAt;
|
||||||
});
|
});
|
||||||
@@ -813,7 +839,7 @@ public sealed class WorkflowHostedService : BackgroundService, IWorkflowControlS
|
|||||||
ResultCode = (ushort)_resultCode,
|
ResultCode = (ushort)_resultCode,
|
||||||
ResultDescription = GetResultDescription(_resultCode),
|
ResultDescription = GetResultDescription(_resultCode),
|
||||||
ReleaseSent = _releaseSent,
|
ReleaseSent = _releaseSent,
|
||||||
AlarmRaised = _plcState.AlarmRaised,
|
AlarmRaised = false,
|
||||||
ExceptionSummary = _faultLatched ? "流程故障" : string.Empty
|
ExceptionSummary = _faultLatched ? "流程故障" : string.Empty
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -836,7 +862,6 @@ public sealed class WorkflowHostedService : BackgroundService, IWorkflowControlS
|
|||||||
{
|
{
|
||||||
_faultLatched = true;
|
_faultLatched = true;
|
||||||
_resultCode = resultCode;
|
_resultCode = resultCode;
|
||||||
_alarmCode = alarmCode;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -846,10 +871,8 @@ public sealed class WorkflowHostedService : BackgroundService, IWorkflowControlS
|
|||||||
faultMessage,
|
faultMessage,
|
||||||
state =>
|
state =>
|
||||||
{
|
{
|
||||||
state.SystemFault = true;
|
|
||||||
state.PcBusy = false;
|
state.PcBusy = false;
|
||||||
state.ReleasePermit = false;
|
state.ReleasePermit = false;
|
||||||
state.ProcessDone = false;
|
|
||||||
},
|
},
|
||||||
cancellationToken).ConfigureAwait(false);
|
cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
@@ -861,7 +884,6 @@ public sealed class WorkflowHostedService : BackgroundService, IWorkflowControlS
|
|||||||
_appLogger.LogError($"系统进入故障状态: {faultMessage}", true);
|
_appLogger.LogError($"系统进入故障状态: {faultMessage}", true);
|
||||||
UpdateSnapshot(snapshot =>
|
UpdateSnapshot(snapshot =>
|
||||||
{
|
{
|
||||||
snapshot.SystemFault = true;
|
|
||||||
snapshot.FaultMessage = faultMessage;
|
snapshot.FaultMessage = faultMessage;
|
||||||
snapshot.ResultDescription = faultMessage;
|
snapshot.ResultDescription = faultMessage;
|
||||||
});
|
});
|
||||||
@@ -878,22 +900,13 @@ public sealed class WorkflowHostedService : BackgroundService, IWorkflowControlS
|
|||||||
{
|
{
|
||||||
if (!enabled)
|
if (!enabled)
|
||||||
{
|
{
|
||||||
await WritePlcStateAsync(state => state.AlarmRaised = false, cancellationToken).ConfigureAwait(false);
|
|
||||||
UpdateSnapshot(snapshot =>
|
|
||||||
{
|
|
||||||
snapshot.AlarmRaised = false;
|
|
||||||
snapshot.AlarmCode = 0;
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = await _andonService.RaiseAlarmAsync(request, cancellationToken).ConfigureAwait(false);
|
var result = await _andonService.RaiseAlarmAsync(request, cancellationToken).ConfigureAwait(false);
|
||||||
await WritePlcStateAsync(state => state.AlarmRaised = result.IsSuccess, cancellationToken).ConfigureAwait(false);
|
|
||||||
UpdateSnapshot(snapshot =>
|
UpdateSnapshot(snapshot =>
|
||||||
{
|
{
|
||||||
snapshot.AndonStatus = result.IsSuccess ? "调用成功" : $"调用失败: {result.ErrorMessage}";
|
snapshot.AndonStatus = result.IsSuccess ? "调用成功" : $"调用失败: {result.ErrorMessage}";
|
||||||
snapshot.AlarmRaised = result.IsSuccess;
|
|
||||||
snapshot.AlarmCode = request.AlarmCode;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.IsSuccess)
|
if (result.IsSuccess)
|
||||||
@@ -919,7 +932,6 @@ public sealed class WorkflowHostedService : BackgroundService, IWorkflowControlS
|
|||||||
_scanTryCount = 0;
|
_scanTryCount = 0;
|
||||||
_sftpTryCount = 0;
|
_sftpTryCount = 0;
|
||||||
_resultCode = WorkflowResultCode.Processing;
|
_resultCode = WorkflowResultCode.Processing;
|
||||||
_alarmCode = AlarmCode.None;
|
|
||||||
_faultLatched = false;
|
_faultLatched = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -933,16 +945,14 @@ public sealed class WorkflowHostedService : BackgroundService, IWorkflowControlS
|
|||||||
{
|
{
|
||||||
_faultLatched = false;
|
_faultLatched = false;
|
||||||
_processingActive = false;
|
_processingActive = false;
|
||||||
_lastPcbArrived = false;
|
// PcbArrived 为 PLC 离散输入,软件复位后仍需等待其先回落再重新产生上升沿。
|
||||||
_releaseSent = false;
|
_releaseSent = false;
|
||||||
_currentBoardStartedAt = null;
|
_currentBoardStartedAt = null;
|
||||||
_currentBarcode = string.Empty;
|
_currentBarcode = string.Empty;
|
||||||
_scanTryCount = 0;
|
_scanTryCount = 0;
|
||||||
_sftpTryCount = 0;
|
_sftpTryCount = 0;
|
||||||
_resultCode = WorkflowResultCode.None;
|
_resultCode = WorkflowResultCode.None;
|
||||||
_alarmCode = AlarmCode.None;
|
|
||||||
ResetResultBits(_plcState);
|
ResetResultBits(_plcState);
|
||||||
_plcState.FlowStateCode = WorkflowState.Idle.ToFlowStateCode();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -953,18 +963,8 @@ public sealed class WorkflowHostedService : BackgroundService, IWorkflowControlS
|
|||||||
private static void ResetResultBits(PlcProcessState state)
|
private static void ResetResultBits(PlcProcessState state)
|
||||||
{
|
{
|
||||||
state.PcBusy = false;
|
state.PcBusy = false;
|
||||||
state.ScanOk = false;
|
|
||||||
state.ScanNg = false;
|
|
||||||
state.FileFound = false;
|
|
||||||
state.FileNotFound = false;
|
|
||||||
state.AlarmRaised = false;
|
|
||||||
state.ReleasePermit = false;
|
state.ReleasePermit = false;
|
||||||
state.ProcessDone = false;
|
|
||||||
state.SystemFault = false;
|
|
||||||
state.ResultCode = 0;
|
state.ResultCode = 0;
|
||||||
state.ScanTryCount = 0;
|
|
||||||
state.SftpTryCount = 0;
|
|
||||||
state.AlarmCode = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -977,11 +977,7 @@ public sealed class WorkflowHostedService : BackgroundService, IWorkflowControlS
|
|||||||
/// <returns>表示写入完成的任务。</returns>
|
/// <returns>表示写入完成的任务。</returns>
|
||||||
private async Task ApplyProcessStateAsync(WorkflowState state, string description, Action<PlcProcessState> extraUpdate, CancellationToken cancellationToken)
|
private async Task ApplyProcessStateAsync(WorkflowState state, string description, Action<PlcProcessState> extraUpdate, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
await WritePlcStateAsync(plcState =>
|
await WritePlcStateAsync(extraUpdate, cancellationToken).ConfigureAwait(false);
|
||||||
{
|
|
||||||
plcState.FlowStateCode = state.ToFlowStateCode();
|
|
||||||
extraUpdate(plcState);
|
|
||||||
}, cancellationToken).ConfigureAwait(false);
|
|
||||||
PublishRuntimeState(state, description);
|
PublishRuntimeState(state, description);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1001,9 +997,6 @@ public sealed class WorkflowHostedService : BackgroundService, IWorkflowControlS
|
|||||||
{
|
{
|
||||||
updateAction(_plcState);
|
updateAction(_plcState);
|
||||||
_plcState.ResultCode = (ushort)_resultCode;
|
_plcState.ResultCode = (ushort)_resultCode;
|
||||||
_plcState.ScanTryCount = checked((ushort)_scanTryCount);
|
|
||||||
_plcState.SftpTryCount = checked((ushort)_sftpTryCount);
|
|
||||||
_plcState.AlarmCode = (ushort)_alarmCode;
|
|
||||||
snapshot = _plcState.Clone();
|
snapshot = _plcState.Clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1015,33 +1008,6 @@ public sealed class WorkflowHostedService : BackgroundService, IWorkflowControlS
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 在达到设定周期时翻转 PLC 在线心跳位。
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="cancellationToken">取消令牌。</param>
|
|
||||||
/// <returns>表示更新完成的任务。</returns>
|
|
||||||
private async Task UpdateHeartbeatIfDueAsync(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var now = DateTimeOffset.Now;
|
|
||||||
var shouldToggle = false;
|
|
||||||
lock (_stateSyncRoot)
|
|
||||||
{
|
|
||||||
if (now - _lastHeartbeatToggleAt >= TimeSpan.FromMilliseconds(_config.Plc.HeartbeatIntervalMs))
|
|
||||||
{
|
|
||||||
_heartbeatState = !_heartbeatState;
|
|
||||||
_lastHeartbeatToggleAt = now;
|
|
||||||
shouldToggle = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!shouldToggle)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await WritePlcStateAsync(state => state.PcOnline = _heartbeatState, cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 发布当前运行态快照到 UI 存储。
|
/// 发布当前运行态快照到 UI 存储。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -1051,29 +1017,17 @@ public sealed class WorkflowHostedService : BackgroundService, IWorkflowControlS
|
|||||||
{
|
{
|
||||||
DateTimeOffset? startedAt;
|
DateTimeOffset? startedAt;
|
||||||
string barcode;
|
string barcode;
|
||||||
int scanTryCount;
|
|
||||||
int sftpTryCount;
|
|
||||||
ushort resultCode;
|
ushort resultCode;
|
||||||
ushort alarmCode;
|
|
||||||
bool isBusy;
|
bool isBusy;
|
||||||
bool processDone;
|
|
||||||
bool systemFault;
|
|
||||||
bool alarmRaised;
|
|
||||||
string faultMessage;
|
string faultMessage;
|
||||||
|
|
||||||
lock (_stateSyncRoot)
|
lock (_stateSyncRoot)
|
||||||
{
|
{
|
||||||
startedAt = _currentBoardStartedAt;
|
startedAt = _currentBoardStartedAt;
|
||||||
barcode = _currentBarcode;
|
barcode = _currentBarcode;
|
||||||
scanTryCount = _scanTryCount;
|
|
||||||
sftpTryCount = _sftpTryCount;
|
|
||||||
resultCode = (ushort)_resultCode;
|
resultCode = (ushort)_resultCode;
|
||||||
alarmCode = (ushort)_alarmCode;
|
|
||||||
isBusy = _plcState.PcBusy;
|
isBusy = _plcState.PcBusy;
|
||||||
processDone = _plcState.ProcessDone;
|
faultMessage = _faultLatched ? description : string.Empty;
|
||||||
systemFault = _plcState.SystemFault;
|
|
||||||
alarmRaised = _plcState.AlarmRaised;
|
|
||||||
faultMessage = systemFault ? description : string.Empty;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateSnapshot(snapshot =>
|
UpdateSnapshot(snapshot =>
|
||||||
@@ -1083,14 +1037,8 @@ public sealed class WorkflowHostedService : BackgroundService, IWorkflowControlS
|
|||||||
snapshot.CurrentBarcode = barcode;
|
snapshot.CurrentBarcode = barcode;
|
||||||
snapshot.ResultDescription = description;
|
snapshot.ResultDescription = description;
|
||||||
snapshot.FaultMessage = faultMessage;
|
snapshot.FaultMessage = faultMessage;
|
||||||
snapshot.ScanTryCount = scanTryCount;
|
|
||||||
snapshot.SftpTryCount = sftpTryCount;
|
|
||||||
snapshot.ResultCode = resultCode;
|
snapshot.ResultCode = resultCode;
|
||||||
snapshot.AlarmCode = alarmCode;
|
|
||||||
snapshot.IsBusy = isBusy;
|
snapshot.IsBusy = isBusy;
|
||||||
snapshot.ProcessDone = processDone;
|
|
||||||
snapshot.SystemFault = systemFault;
|
|
||||||
snapshot.AlarmRaised = alarmRaised;
|
|
||||||
if (startedAt.HasValue)
|
if (startedAt.HasValue)
|
||||||
{
|
{
|
||||||
snapshot.LastTriggeredAt = startedAt;
|
snapshot.LastTriggeredAt = startedAt;
|
||||||
@@ -1131,6 +1079,44 @@ public sealed class WorkflowHostedService : BackgroundService, IWorkflowControlS
|
|||||||
_stateStore.UpdateSnapshot(action);
|
_stateStore.UpdateSnapshot(action);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 使用最新 PLC 镜像替换快照中的监控集合。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="snapshot">运行态快照。</param>
|
||||||
|
/// <param name="monitorSnapshot">PLC 监控镜像。</param>
|
||||||
|
private void ReplacePlcMonitorItems(RuntimeSnapshot snapshot, PlcMonitorSnapshot monitorSnapshot)
|
||||||
|
{
|
||||||
|
snapshot.PlcMonitorItems.Clear();
|
||||||
|
|
||||||
|
AddMonitorItem(snapshot, "Inputs", "PcbArrived", _config.Plc.Inputs.PcbArrived.ToString(), monitorSnapshot.Inputs.PcbArrived.ToString(), monitorSnapshot.CapturedAt);
|
||||||
|
AddMonitorItem(snapshot, "Inputs", "PlcReset", _config.Plc.Inputs.PlcReset.ToString(), monitorSnapshot.Inputs.PlcReset.ToString(), monitorSnapshot.CapturedAt);
|
||||||
|
AddMonitorItem(snapshot, "Inputs", "PlcAckRelease", _config.Plc.Inputs.PlcAckRelease.ToString(), monitorSnapshot.Inputs.PlcAckRelease.ToString(), monitorSnapshot.CapturedAt);
|
||||||
|
AddMonitorItem(snapshot, "Outputs", "PcBusy", _config.Plc.Outputs.PcBusy.ToString(), monitorSnapshot.Outputs.PcBusy.ToString(), monitorSnapshot.CapturedAt);
|
||||||
|
AddMonitorItem(snapshot, "Outputs", "ReleasePermit", _config.Plc.Outputs.ReleasePermit.ToString(), monitorSnapshot.Outputs.ReleasePermit.ToString(), monitorSnapshot.CapturedAt);
|
||||||
|
AddMonitorItem(snapshot, "Registers", "ResultCode", _config.Plc.Registers.ResultCode.ToString(), monitorSnapshot.Outputs.ResultCode.ToString(), monitorSnapshot.CapturedAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 向快照监控集合追加一个监控项。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="snapshot">运行态快照。</param>
|
||||||
|
/// <param name="groupName">变量分组。</param>
|
||||||
|
/// <param name="name">变量名称。</param>
|
||||||
|
/// <param name="address">PLC 变量地址。</param>
|
||||||
|
/// <param name="currentValue">当前值。</param>
|
||||||
|
/// <param name="lastUpdatedAt">最近更新时间。</param>
|
||||||
|
private static void AddMonitorItem(RuntimeSnapshot snapshot, string groupName, string name, string address, string currentValue, DateTimeOffset lastUpdatedAt)
|
||||||
|
{
|
||||||
|
snapshot.PlcMonitorItems.Add(new PlcMonitorItem
|
||||||
|
{
|
||||||
|
GroupName = groupName,
|
||||||
|
Name = name,
|
||||||
|
Address = address,
|
||||||
|
CurrentValue = currentValue,
|
||||||
|
LastUpdatedAt = lastUpdatedAt
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 根据结果代码获取中文描述。
|
/// 根据结果代码获取中文描述。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -1143,13 +1129,8 @@ public sealed class WorkflowHostedService : BackgroundService, IWorkflowControlS
|
|||||||
WorkflowResultCode.None => "空闲 / 无结果",
|
WorkflowResultCode.None => "空闲 / 无结果",
|
||||||
WorkflowResultCode.Processing => "处理中",
|
WorkflowResultCode.Processing => "处理中",
|
||||||
WorkflowResultCode.Passed => "扫码成功,文件存在,正常放行",
|
WorkflowResultCode.Passed => "扫码成功,文件存在,正常放行",
|
||||||
WorkflowResultCode.ScanFailedReleased => "扫码失败 3 次后放行",
|
WorkflowResultCode.NgReleased => "处理完成,结果 NG,随后放行",
|
||||||
WorkflowResultCode.FileNotFoundReleased => "扫码成功,文件未找到超时放行",
|
WorkflowResultCode.Fault => "系统故障",
|
||||||
WorkflowResultCode.PlcCommunicationFault => "PLC 通信异常",
|
|
||||||
WorkflowResultCode.ScannerFault => "串口异常",
|
|
||||||
WorkflowResultCode.SftpFault => "SFTP 连接或认证异常",
|
|
||||||
WorkflowResultCode.AndonFault => "安灯接口调用异常",
|
|
||||||
WorkflowResultCode.ConfigurationFault => "配置异常",
|
|
||||||
_ => "未知结果"
|
_ => "未知结果"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,45 +50,45 @@ public interface IAppLogger<TCategoryName>
|
|||||||
/// 记录一条信息日志。
|
/// 记录一条信息日志。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="message">日志消息模板或文本。</param>
|
/// <param name="message">日志消息模板或文本。</param>
|
||||||
/// <param name="showInUi">是否同步显示到前台日志区域。默认不显示。</param>
|
/// <param name="showInUi">是否同步显示到前台日志区域。默认 <c>true</c>。</param>
|
||||||
/// <param name="args">日志模板参数。</param>
|
/// <param name="args">日志模板参数。</param>
|
||||||
void LogInformation(string message, bool showInUi = false, params object?[] args);
|
void LogInformation(string message, bool showInUi = true, params object?[] args);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 记录一条警告日志。
|
/// 记录一条警告日志。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="message">日志消息模板或文本。</param>
|
/// <param name="message">日志消息模板或文本。</param>
|
||||||
/// <param name="showInUi">是否同步显示到前台日志区域。默认不显示。</param>
|
/// <param name="showInUi">是否同步显示到前台日志区域。默认 <c>true</c>。</param>
|
||||||
/// <param name="args">日志模板参数。</param>
|
/// <param name="args">日志模板参数。</param>
|
||||||
void LogWarning(string message, bool showInUi = false, params object?[] args);
|
void LogWarning(string message, bool showInUi = true, params object?[] args);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 记录一条带异常的警告日志。
|
/// 记录一条带异常的警告日志。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="exception">异常对象。</param>
|
/// <param name="exception">异常对象。</param>
|
||||||
/// <param name="message">日志消息模板或文本。</param>
|
/// <param name="message">日志消息模板或文本。</param>
|
||||||
/// <param name="showInUi">是否同步显示到前台日志区域。默认不显示。</param>
|
/// <param name="showInUi">是否同步显示到前台日志区域。默认 <c>true</c>。</param>
|
||||||
/// <param name="args">日志模板参数。</param>
|
/// <param name="args">日志模板参数。</param>
|
||||||
/// <exception cref="ArgumentNullException">当 <paramref name="exception"/> 为 <see langword="null"/> 时抛出。</exception>
|
/// <exception cref="ArgumentNullException">当 <paramref name="exception"/> 为 <see langword="null"/> 时抛出。</exception>
|
||||||
void LogWarning(Exception exception, string message, bool showInUi = false, params object?[] args);
|
void LogWarning(Exception exception, string message, bool showInUi = true, params object?[] args);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 记录一条错误日志。
|
/// 记录一条错误日志。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="message">日志消息模板或文本。</param>
|
/// <param name="message">日志消息模板或文本。</param>
|
||||||
/// <param name="showInUi">是否同步显示到前台日志区域。默认不显示。</param>
|
/// <param name="showInUi">是否同步显示到前台日志区域。默认 <c>true</c>。</param>
|
||||||
/// <param name="args">日志模板参数。</param>
|
/// <param name="args">日志模板参数。</param>
|
||||||
void LogError(string message, bool showInUi = false, params object?[] args);
|
void LogError(string message, bool showInUi = true, params object?[] args);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 记录一条带异常的错误日志。
|
/// 记录一条带异常的错误日志。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="exception">异常对象。</param>
|
/// <param name="exception">异常对象。</param>
|
||||||
/// <param name="message">日志消息模板或文本。</param>
|
/// <param name="message">日志消息模板或文本。</param>
|
||||||
/// <param name="showInUi">是否同步显示到前台日志区域。默认不显示。</param>
|
/// <param name="showInUi">是否同步显示到前台日志区域。默认 <c>true</c>。</param>
|
||||||
/// <param name="args">日志模板参数。</param>
|
/// <param name="args">日志模板参数。</param>
|
||||||
/// <exception cref="ArgumentNullException">当 <paramref name="exception"/> 为 <see langword="null"/> 时抛出。</exception>
|
/// <exception cref="ArgumentNullException">当 <paramref name="exception"/> 为 <see langword="null"/> 时抛出。</exception>
|
||||||
void LogError(Exception exception, string message, bool showInUi = false, params object?[] args);
|
void LogError(Exception exception, string message, bool showInUi = true, params object?[] args);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -103,6 +103,13 @@ public interface IPlcService
|
|||||||
/// <returns>输入信号快照。</returns>
|
/// <returns>输入信号快照。</returns>
|
||||||
Task<PlcSignalSnapshot> ReadSignalsAsync(CancellationToken cancellationToken);
|
Task<PlcSignalSnapshot> ReadSignalsAsync(CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 读取 PLC 当前监控镜像,包括输入、输出和寄存器的实际值。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cancellationToken">取消令牌。</param>
|
||||||
|
/// <returns>监控镜像快照。</returns>
|
||||||
|
Task<PlcMonitorSnapshot> ReadMonitorSnapshotAsync(CancellationToken cancellationToken);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 写入 PLC 输出状态与寄存器值。
|
/// 写入 PLC 输出状态与寄存器值。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
26
src/AxiOmron.PcbCheck/Utils/ShutdownHelper.cs
Normal file
26
src/AxiOmron.PcbCheck/Utils/ShutdownHelper.cs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
|
||||||
|
namespace AxiOmron.PcbCheck.Utils;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 提供应用退出阶段的异步宿主停止辅助能力,确保 await 后续逻辑保留在捕获到的同步上下文中。
|
||||||
|
/// </summary>
|
||||||
|
internal static class ShutdownHelper
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 停止指定 Host,并在停止完成后于捕获的调用上下文中执行收尾回调。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="host">待停止的宿主实例。</param>
|
||||||
|
/// <param name="timeout">停止超时时间。</param>
|
||||||
|
/// <param name="afterStop">停止完成后的收尾回调。</param>
|
||||||
|
/// <returns>表示停止流程完成的任务。</returns>
|
||||||
|
/// <exception cref="ArgumentNullException">当 <paramref name="host"/> 为空时抛出。</exception>
|
||||||
|
internal static async Task StopHostAsync(IHost host, TimeSpan timeout, Action? afterStop = null)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(host);
|
||||||
|
|
||||||
|
using var cancellationTokenSource = new CancellationTokenSource(timeout);
|
||||||
|
await host.StopAsync(cancellationTokenSource.Token);
|
||||||
|
afterStop?.Invoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -54,6 +54,7 @@ public partial class MainWindowViewModel : ObservableObject
|
|||||||
Title = "Axi Omron PCB Check";
|
Title = "Axi Omron PCB Check";
|
||||||
Logs = new ObservableCollection<UiLogEntry>();
|
Logs = new ObservableCollection<UiLogEntry>();
|
||||||
RecentBoards = new ObservableCollection<BoardProcessRecord>();
|
RecentBoards = new ObservableCollection<BoardProcessRecord>();
|
||||||
|
PlcMonitorItems = new ObservableCollection<PlcMonitorItem>();
|
||||||
AdminUnlockStatus = "管理员功能已锁定";
|
AdminUnlockStatus = "管理员功能已锁定";
|
||||||
|
|
||||||
Logs.CollectionChanged += OnLogsCollectionChanged;
|
Logs.CollectionChanged += OnLogsCollectionChanged;
|
||||||
@@ -77,6 +78,11 @@ public partial class MainWindowViewModel : ObservableObject
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public ObservableCollection<BoardProcessRecord> RecentBoards { get; }
|
public ObservableCollection<BoardProcessRecord> RecentBoards { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取 PLC 实时监控集合。
|
||||||
|
/// </summary>
|
||||||
|
public ObservableCollection<PlcMonitorItem> PlcMonitorItems { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取最近运行日志集合(与 <see cref="Logs"/> 为同一集合,供 UI 绑定语义更清晰)。
|
/// 获取最近运行日志集合(与 <see cref="Logs"/> 为同一集合,供 UI 绑定语义更清晰)。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -141,30 +147,12 @@ public partial class MainWindowViewModel : ObservableObject
|
|||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string _faultMessage = string.Empty;
|
private string _faultMessage = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取或设置扫码次数。
|
|
||||||
/// </summary>
|
|
||||||
[ObservableProperty]
|
|
||||||
private int _scanTryCount;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取或设置 SFTP 查询次数。
|
|
||||||
/// </summary>
|
|
||||||
[ObservableProperty]
|
|
||||||
private int _sftpTryCount;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取或设置结果代码。
|
/// 获取或设置结果代码。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private ushort _resultCode;
|
private ushort _resultCode;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取或设置报警代码。
|
|
||||||
/// </summary>
|
|
||||||
[ObservableProperty]
|
|
||||||
private ushort _alarmCode;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取或设置最近触发时间。
|
/// 获取或设置最近触发时间。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -183,24 +171,6 @@ public partial class MainWindowViewModel : ObservableObject
|
|||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private bool _isBusy;
|
private bool _isBusy;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取或设置是否存在系统故障。
|
|
||||||
/// </summary>
|
|
||||||
[ObservableProperty]
|
|
||||||
private bool _isFaulted;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取或设置是否已完成。
|
|
||||||
/// </summary>
|
|
||||||
[ObservableProperty]
|
|
||||||
private bool _isDone;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取或设置是否已触发报警。
|
|
||||||
/// </summary>
|
|
||||||
[ObservableProperty]
|
|
||||||
private bool _isAlarmRaised;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取或设置最近更新时间。
|
/// 获取或设置最近更新时间。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -219,12 +189,6 @@ public partial class MainWindowViewModel : ObservableObject
|
|||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private int _todayErrorCount;
|
private int _todayErrorCount;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取或设置当前活跃告警数量。
|
|
||||||
/// </summary>
|
|
||||||
[ObservableProperty]
|
|
||||||
private int _activeAlarmCount;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取或设置最近一次异常日志的时间文本。
|
/// 获取或设置最近一次异常日志的时间文本。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -430,27 +394,17 @@ public partial class MainWindowViewModel : ObservableObject
|
|||||||
CurrentBarcode = snapshot.CurrentBarcode;
|
CurrentBarcode = snapshot.CurrentBarcode;
|
||||||
ResultDescription = snapshot.ResultDescription;
|
ResultDescription = snapshot.ResultDescription;
|
||||||
FaultMessage = snapshot.FaultMessage;
|
FaultMessage = snapshot.FaultMessage;
|
||||||
ScanTryCount = snapshot.ScanTryCount;
|
|
||||||
SftpTryCount = snapshot.SftpTryCount;
|
|
||||||
ResultCode = snapshot.ResultCode;
|
ResultCode = snapshot.ResultCode;
|
||||||
AlarmCode = snapshot.AlarmCode;
|
|
||||||
LastTriggeredAt = snapshot.LastTriggeredAt?.ToString("yyyy-MM-dd HH:mm:ss") ?? "-";
|
LastTriggeredAt = snapshot.LastTriggeredAt?.ToString("yyyy-MM-dd HH:mm:ss") ?? "-";
|
||||||
LastCompletedAt = snapshot.LastCompletedAt?.ToString("yyyy-MM-dd HH:mm:ss") ?? "-";
|
LastCompletedAt = snapshot.LastCompletedAt?.ToString("yyyy-MM-dd HH:mm:ss") ?? "-";
|
||||||
IsBusy = snapshot.IsBusy;
|
IsBusy = snapshot.IsBusy;
|
||||||
IsFaulted = snapshot.SystemFault;
|
|
||||||
IsDone = snapshot.ProcessDone;
|
|
||||||
IsAlarmRaised = snapshot.AlarmRaised;
|
|
||||||
ActiveAlarmCount = snapshot.AlarmRaised ? 1 : 0;
|
|
||||||
LastUpdatedAt = snapshot.LastUpdatedAt.ToString("yyyy-MM-dd HH:mm:ss");
|
LastUpdatedAt = snapshot.LastUpdatedAt.ToString("yyyy-MM-dd HH:mm:ss");
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
PlcMonitorItems.Clear();
|
||||||
/// 当 <see cref="IsAlarmRaised"/> 发生变化时同步活跃告警计数。
|
foreach (PlcMonitorItem item in snapshot.PlcMonitorItems)
|
||||||
/// </summary>
|
{
|
||||||
/// <param name="value">最新告警状态。</param>
|
PlcMonitorItems.Add(item.Clone());
|
||||||
partial void OnIsAlarmRaisedChanged(bool value)
|
}
|
||||||
{
|
|
||||||
ActiveAlarmCount = value ? 1 : 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using CommunityToolkit.Mvvm.Input;
|
|||||||
using AxiOmron.PcbCheck.Options;
|
using AxiOmron.PcbCheck.Options;
|
||||||
using AxiOmron.PcbCheck.Services.Interfaces;
|
using AxiOmron.PcbCheck.Services.Interfaces;
|
||||||
using AxiOmron.PcbCheck.Models;
|
using AxiOmron.PcbCheck.Models;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
|
||||||
namespace AxiOmron.PcbCheck.ViewModels;
|
namespace AxiOmron.PcbCheck.ViewModels;
|
||||||
|
|
||||||
@@ -13,21 +14,39 @@ public partial class SystemSettingViewModel : ObservableObject
|
|||||||
{
|
{
|
||||||
private readonly IAppConfigService _appConfigService;
|
private readonly IAppConfigService _appConfigService;
|
||||||
private readonly ISftpLookupService _sftpLookupService;
|
private readonly ISftpLookupService _sftpLookupService;
|
||||||
|
private readonly IAppStateStore _appStateStore;
|
||||||
|
private readonly IDispatcherService _dispatcherService;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 初始化系统设置视图模型。
|
/// 初始化系统设置视图模型。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="appConfigService">配置读写服务。</param>
|
/// <param name="appConfigService">配置读写服务。</param>
|
||||||
/// <param name="sftpLookupService">SFTP 连接测试服务。</param>
|
/// <param name="sftpLookupService">SFTP 连接测试服务。</param>
|
||||||
public SystemSettingViewModel(IAppConfigService appConfigService, ISftpLookupService sftpLookupService)
|
/// <param name="appStateStore">运行态快照存储。</param>
|
||||||
|
/// <param name="dispatcherService">UI 线程调度服务。</param>
|
||||||
|
public SystemSettingViewModel(
|
||||||
|
IAppConfigService appConfigService,
|
||||||
|
ISftpLookupService sftpLookupService,
|
||||||
|
IAppStateStore appStateStore,
|
||||||
|
IDispatcherService dispatcherService)
|
||||||
{
|
{
|
||||||
_appConfigService = appConfigService ?? throw new ArgumentNullException(nameof(appConfigService));
|
_appConfigService = appConfigService ?? throw new ArgumentNullException(nameof(appConfigService));
|
||||||
_sftpLookupService = sftpLookupService ?? throw new ArgumentNullException(nameof(sftpLookupService));
|
_sftpLookupService = sftpLookupService ?? throw new ArgumentNullException(nameof(sftpLookupService));
|
||||||
|
_appStateStore = appStateStore ?? throw new ArgumentNullException(nameof(appStateStore));
|
||||||
|
_dispatcherService = dispatcherService ?? throw new ArgumentNullException(nameof(dispatcherService));
|
||||||
EditableConfig = _appConfigService.Load();
|
EditableConfig = _appConfigService.Load();
|
||||||
ConfigPath = _appConfigService.GetConfigPath();
|
ConfigPath = _appConfigService.GetConfigPath();
|
||||||
|
PlcMonitorItems = new ObservableCollection<PlcMonitorItem>();
|
||||||
|
ApplySnapshot(_appStateStore.GetSnapshot());
|
||||||
|
_appStateStore.SnapshotChanged += OnSnapshotChanged;
|
||||||
StatusMessage = "已加载配置。";
|
StatusMessage = "已加载配置。";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取 PLC 实时监控集合。
|
||||||
|
/// </summary>
|
||||||
|
public ObservableCollection<PlcMonitorItem> PlcMonitorItems { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取或设置可编辑配置对象。
|
/// 获取或设置可编辑配置对象。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -116,4 +135,27 @@ public partial class SystemSettingViewModel : ObservableObject
|
|||||||
/// 判断当前是否允许执行 SFTP 测试连接。
|
/// 判断当前是否允许执行 SFTP 测试连接。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private bool CanTestSftpConnection => !IsTestingSftpConnection;
|
private bool CanTestSftpConnection => !IsTestingSftpConnection;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 将运行态快照中的 PLC 监控数据同步到当前视图模型。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="snapshot">运行态快照。</param>
|
||||||
|
private void ApplySnapshot(RuntimeSnapshot snapshot)
|
||||||
|
{
|
||||||
|
PlcMonitorItems.Clear();
|
||||||
|
foreach (PlcMonitorItem item in snapshot.PlcMonitorItems)
|
||||||
|
{
|
||||||
|
PlcMonitorItems.Add(item.Clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 处理运行态快照变更事件,刷新 PLC 监控集合。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="sender">事件源。</param>
|
||||||
|
/// <param name="snapshot">最新快照。</param>
|
||||||
|
private async void OnSnapshotChanged(object? sender, RuntimeSnapshot snapshot)
|
||||||
|
{
|
||||||
|
await _dispatcherService.InvokeAsync(() => ApplySnapshot(snapshot)).ConfigureAwait(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -407,6 +407,7 @@
|
|||||||
<Setter Property="AutoGenerateColumns" Value="False" />
|
<Setter Property="AutoGenerateColumns" Value="False" />
|
||||||
<Setter Property="IsReadOnly" Value="True" />
|
<Setter Property="IsReadOnly" Value="True" />
|
||||||
<Setter Property="SelectionMode" Value="Single" />
|
<Setter Property="SelectionMode" Value="Single" />
|
||||||
|
<Setter Property="ClipboardCopyMode" Value="IncludeHeader" />
|
||||||
<Setter Property="RowStyle" Value="{StaticResource ModernDataGridRowStyle}" />
|
<Setter Property="RowStyle" Value="{StaticResource ModernDataGridRowStyle}" />
|
||||||
<Setter Property="CellStyle" Value="{StaticResource ModernDataGridCellStyle}" />
|
<Setter Property="CellStyle" Value="{StaticResource ModernDataGridCellStyle}" />
|
||||||
<Setter Property="ColumnHeaderStyle" Value="{StaticResource ModernDataGridColumnHeaderStyle}" />
|
<Setter Property="ColumnHeaderStyle" Value="{StaticResource ModernDataGridColumnHeaderStyle}" />
|
||||||
@@ -426,69 +427,69 @@
|
|||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<Grid Margin="20,20,20,16">
|
<Grid Margin="20,20,20,16">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="Auto" />
|
<ColumnDefinition Width="Auto" />
|
||||||
<ColumnDefinition Width="12" />
|
<ColumnDefinition Width="12" />
|
||||||
<ColumnDefinition Width="Auto" />
|
<ColumnDefinition Width="Auto" />
|
||||||
<ColumnDefinition Width="12" />
|
<ColumnDefinition Width="12" />
|
||||||
<ColumnDefinition Width="Auto" />
|
<ColumnDefinition Width="Auto" />
|
||||||
<ColumnDefinition Width="12" />
|
<ColumnDefinition Width="12" />
|
||||||
<ColumnDefinition Width="Auto" />
|
<ColumnDefinition Width="Auto" />
|
||||||
<ColumnDefinition Width="12" />
|
<ColumnDefinition Width="12" />
|
||||||
<ColumnDefinition Width="Auto" />
|
<ColumnDefinition Width="Auto" />
|
||||||
<ColumnDefinition Width="16" />
|
<ColumnDefinition Width="16" />
|
||||||
<ColumnDefinition Width="*" />
|
<ColumnDefinition Width="*" />
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
<Button Grid.Column="0"
|
<Button Grid.Column="0"
|
||||||
Style="{StaticResource ToolbarButtonStyle}"
|
Style="{StaticResource ToolbarButtonStyle}"
|
||||||
Content="管理员解锁"
|
Content="管理员解锁"
|
||||||
Command="{Binding UnlockAdminCommand}" />
|
Command="{Binding UnlockAdminCommand}" />
|
||||||
<Button Grid.Column="2"
|
<Button Grid.Column="2"
|
||||||
Style="{StaticResource PrimaryActionButtonStyle}"
|
Style="{StaticResource PrimaryActionButtonStyle}"
|
||||||
Content="手动复位"
|
Content="手动复位"
|
||||||
Command="{Binding ResetCommand}"
|
Command="{Binding ResetCommand}"
|
||||||
Visibility="{Binding IsAdminUnlocked, Converter={StaticResource BooleanToVisibilityConverter}}" />
|
Visibility="{Binding IsAdminUnlocked, Converter={StaticResource BooleanToVisibilityConverter}}" />
|
||||||
<Button Grid.Column="4"
|
<Button Grid.Column="4"
|
||||||
Style="{StaticResource PrimaryActionButtonStyle}"
|
Style="{StaticResource PrimaryActionButtonStyle}"
|
||||||
Content="重连 PLC"
|
Content="重连 PLC"
|
||||||
Command="{Binding ReconnectPlcCommand}"
|
Command="{Binding ReconnectPlcCommand}"
|
||||||
Visibility="{Binding IsAdminUnlocked, Converter={StaticResource BooleanToVisibilityConverter}}" />
|
Visibility="{Binding IsAdminUnlocked, Converter={StaticResource BooleanToVisibilityConverter}}" />
|
||||||
<Button Grid.Column="6"
|
<Button Grid.Column="6"
|
||||||
Style="{StaticResource PrimaryActionButtonStyle}"
|
Style="{StaticResource PrimaryActionButtonStyle}"
|
||||||
Content="重连扫码枪"
|
Content="重连扫码枪"
|
||||||
Command="{Binding ReconnectScannerCommand}"
|
Command="{Binding ReconnectScannerCommand}"
|
||||||
Visibility="{Binding IsAdminUnlocked, Converter={StaticResource BooleanToVisibilityConverter}}" />
|
Visibility="{Binding IsAdminUnlocked, Converter={StaticResource BooleanToVisibilityConverter}}" />
|
||||||
<Button Grid.Column="8"
|
<Button Grid.Column="8"
|
||||||
Style="{StaticResource ToolbarButtonStyle}"
|
Style="{StaticResource ToolbarButtonStyle}"
|
||||||
Content="测试安灯接口"
|
Content="测试安灯接口"
|
||||||
Command="{Binding TestAndonCommand}"
|
Command="{Binding TestAndonCommand}"
|
||||||
Visibility="{Binding IsAdminUnlocked, Converter={StaticResource BooleanToVisibilityConverter}}" />
|
Visibility="{Binding IsAdminUnlocked, Converter={StaticResource BooleanToVisibilityConverter}}" />
|
||||||
<Border Grid.Column="10"
|
<Border Grid.Column="10"
|
||||||
Background="#FFFFFF"
|
Background="#FFFFFF"
|
||||||
BorderBrush="{StaticResource CardBorderBrush}"
|
BorderBrush="{StaticResource CardBorderBrush}"
|
||||||
BorderThickness="1"
|
BorderThickness="1"
|
||||||
CornerRadius="999"
|
CornerRadius="999"
|
||||||
Padding="12,6"
|
Padding="12,6"
|
||||||
HorizontalAlignment="Left"
|
HorizontalAlignment="Left"
|
||||||
VerticalAlignment="Center">
|
VerticalAlignment="Center">
|
||||||
<StackPanel Orientation="Horizontal">
|
<StackPanel Orientation="Horizontal">
|
||||||
<TextBlock Text="{Binding AdminUnlockStatus}"
|
<TextBlock Text="{Binding AdminUnlockStatus}"
|
||||||
FontSize="12"
|
FontSize="12"
|
||||||
FontWeight="SemiBold"
|
FontWeight="SemiBold"
|
||||||
Foreground="{StaticResource SecondaryTitleBrush}" />
|
Foreground="{StaticResource SecondaryTitleBrush}" />
|
||||||
<TextBlock Text=" | "
|
<TextBlock Text=" | "
|
||||||
FontSize="12"
|
FontSize="12"
|
||||||
FontWeight="SemiBold"
|
FontWeight="SemiBold"
|
||||||
Foreground="#CBD5E1"
|
Foreground="#CBD5E1"
|
||||||
Margin="8,0" />
|
Margin="8,0" />
|
||||||
<TextBlock Text="{Binding ManualActionStatus}"
|
<TextBlock Text="{Binding ManualActionStatus}"
|
||||||
FontSize="12"
|
FontSize="12"
|
||||||
FontWeight="SemiBold"
|
FontWeight="SemiBold"
|
||||||
Foreground="{StaticResource AccentBlueBrush}" />
|
Foreground="{StaticResource AccentBlueBrush}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid Grid.Row="1" Margin="20,0,20,20">
|
<Grid Grid.Row="1" Margin="20,0,20,20">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
@@ -497,7 +498,7 @@
|
|||||||
<ColumnDefinition Width="1*" />
|
<ColumnDefinition Width="1*" />
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="120" />
|
||||||
<RowDefinition Height="10" />
|
<RowDefinition Height="10" />
|
||||||
<RowDefinition Height="*" />
|
<RowDefinition Height="*" />
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
@@ -597,7 +598,7 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<TextBlock Grid.Row="2"
|
<TextBlock Grid.Row="2"
|
||||||
Text="{Binding AndonStatus}"
|
Text="{Binding AndonStatus}" FontSize="10"
|
||||||
Style="{StaticResource CardPrimaryValueStyle}" />
|
Style="{StaticResource CardPrimaryValueStyle}" />
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
@@ -690,8 +691,8 @@
|
|||||||
|
|
||||||
<Border Grid.Column="2" Style="{StaticResource CompactSummaryBlockStyle}">
|
<Border Grid.Column="2" Style="{StaticResource CompactSummaryBlockStyle}">
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
<TextBlock Text="报警码" Style="{StaticResource SummaryCaptionStyle}" />
|
<TextBlock Text="忙碌状态" Style="{StaticResource SummaryCaptionStyle}" />
|
||||||
<TextBlock Text="{Binding AlarmCode}"
|
<TextBlock Text="{Binding IsBusy}"
|
||||||
Style="{StaticResource SummaryNumberStyle}"
|
Style="{StaticResource SummaryNumberStyle}"
|
||||||
FontSize="18" />
|
FontSize="18" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
@@ -713,8 +714,8 @@
|
|||||||
<StackPanel Grid.Row="0" Orientation="Horizontal">
|
<StackPanel Grid.Row="0" Orientation="Horizontal">
|
||||||
<Border Style="{StaticResource AccentBarStyle}" />
|
<Border Style="{StaticResource AccentBarStyle}" />
|
||||||
<StackPanel VerticalAlignment="Center">
|
<StackPanel VerticalAlignment="Center">
|
||||||
<TextBlock Text="关键标志" Style="{StaticResource ModernCardTitleStyle}" />
|
<TextBlock Text="流程概览" Style="{StaticResource ModernCardTitleStyle}" />
|
||||||
<TextBlock Text="系统关键运行标志" Style="{StaticResource ModernCardSubtitleStyle}" />
|
<TextBlock Text="当前运行状态与关键信息" Style="{StaticResource ModernCardSubtitleStyle}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
@@ -724,49 +725,23 @@
|
|||||||
<ColumnDefinition Width="12" />
|
<ColumnDefinition Width="12" />
|
||||||
<ColumnDefinition Width="*" />
|
<ColumnDefinition Width="*" />
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
<RowDefinition Height="12" />
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
|
|
||||||
<Border Grid.Row="0" Grid.Column="0" Style="{StaticResource StatusFlagTileStyle}">
|
<Border Grid.Column="0" Style="{StaticResource StatusFlagTileStyle}">
|
||||||
<StackPanel Orientation="Horizontal">
|
<StackPanel>
|
||||||
<Ellipse Width="10" Height="10"
|
<TextBlock Text="当前流程" Style="{StaticResource SummaryCaptionStyle}" />
|
||||||
Fill="{Binding IsBusy, Converter={StaticResource BooleanToBrushConverter}}"
|
<TextBlock Text="{Binding WorkflowStateText}"
|
||||||
Margin="0,0,8,0"
|
Style="{StaticResource SummaryNumberStyle}"
|
||||||
VerticalAlignment="Center" />
|
FontSize="18" />
|
||||||
<TextBlock Text="忙碌" Style="{StaticResource StatusIndicatorLabelStyle}" />
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<Border Grid.Row="0" Grid.Column="2" Style="{StaticResource StatusFlagTileStyle}">
|
<Border Grid.Column="2" Style="{StaticResource StatusFlagTileStyle}">
|
||||||
<StackPanel Orientation="Horizontal">
|
<StackPanel>
|
||||||
<Ellipse Width="10" Height="10"
|
<TextBlock Text="故障信息" Style="{StaticResource SummaryCaptionStyle}" />
|
||||||
Fill="{Binding IsDone, Converter={StaticResource BooleanToBrushConverter}}"
|
<TextBlock Text="{Binding FaultMessage}"
|
||||||
Margin="0,0,8,0"
|
Style="{StaticResource SummaryNumberStyle}"
|
||||||
VerticalAlignment="Center" />
|
FontSize="16"
|
||||||
<TextBlock Text="完成" Style="{StaticResource StatusIndicatorLabelStyle}" />
|
TextWrapping="Wrap" />
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<Border Grid.Row="2" Grid.Column="0" Style="{StaticResource StatusFlagTileStyle}">
|
|
||||||
<StackPanel Orientation="Horizontal">
|
|
||||||
<Ellipse Width="10" Height="10"
|
|
||||||
Fill="{Binding IsFaulted, Converter={StaticResource BooleanToBrushConverter}}"
|
|
||||||
Margin="0,0,8,0"
|
|
||||||
VerticalAlignment="Center" />
|
|
||||||
<TextBlock Text="故障" Style="{StaticResource StatusIndicatorLabelStyle}" />
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<Border Grid.Row="2" Grid.Column="2" Style="{StaticResource StatusFlagTileStyle}">
|
|
||||||
<StackPanel Orientation="Horizontal">
|
|
||||||
<Ellipse Width="10" Height="10"
|
|
||||||
Fill="{Binding IsAlarmRaised, Converter={StaticResource BooleanToBrushConverter}}"
|
|
||||||
Margin="0,0,8,0"
|
|
||||||
VerticalAlignment="Center" />
|
|
||||||
<TextBlock Text="报警" Style="{StaticResource StatusIndicatorLabelStyle}" />
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
</Grid>
|
</Grid>
|
||||||
@@ -780,19 +755,19 @@
|
|||||||
|
|
||||||
<Border Grid.Column="0" Style="{StaticResource CompactSummaryBlockStyle}">
|
<Border Grid.Column="0" Style="{StaticResource CompactSummaryBlockStyle}">
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
<TextBlock Text="扫码次数" Style="{StaticResource SummaryCaptionStyle}" />
|
<TextBlock Text="最近触发" Style="{StaticResource SummaryCaptionStyle}" />
|
||||||
<TextBlock Text="{Binding ScanTryCount}"
|
<TextBlock Text="{Binding LastTriggeredAt}"
|
||||||
Style="{StaticResource SummaryNumberStyle}"
|
Style="{StaticResource SummaryNumberStyle}"
|
||||||
FontSize="18" />
|
FontSize="14" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<Border Grid.Column="2" Style="{StaticResource CompactSummaryBlockStyle}">
|
<Border Grid.Column="2" Style="{StaticResource CompactSummaryBlockStyle}">
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
<TextBlock Text="SFTP 次数" Style="{StaticResource SummaryCaptionStyle}" />
|
<TextBlock Text="最近完成" Style="{StaticResource SummaryCaptionStyle}" />
|
||||||
<TextBlock Text="{Binding SftpTryCount}"
|
<TextBlock Text="{Binding LastCompletedAt}"
|
||||||
Style="{StaticResource SummaryNumberStyle}"
|
Style="{StaticResource SummaryNumberStyle}"
|
||||||
FontSize="18" />
|
FontSize="14" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
</Grid>
|
</Grid>
|
||||||
@@ -869,8 +844,8 @@
|
|||||||
|
|
||||||
<Border Grid.Column="2" Style="{StaticResource SummaryBlockStyle}">
|
<Border Grid.Column="2" Style="{StaticResource SummaryBlockStyle}">
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
<TextBlock Text="当前活跃告警" Style="{StaticResource SummaryCaptionStyle}" />
|
<TextBlock Text="当前结果码" Style="{StaticResource SummaryCaptionStyle}" />
|
||||||
<TextBlock Text="{Binding ActiveAlarmCount}" Style="{StaticResource SummaryNumberStyle}" />
|
<TextBlock Text="{Binding ResultCode}" Style="{StaticResource SummaryNumberStyle}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
@@ -907,10 +882,12 @@
|
|||||||
<DataGridTemplateColumn Header="消息" Width="*">
|
<DataGridTemplateColumn Header="消息" Width="*">
|
||||||
<DataGridTemplateColumn.CellTemplate>
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
<DataTemplate>
|
<DataTemplate>
|
||||||
<TextBlock Text="{Binding Message}"
|
<TextBox Text="{Binding Message}"
|
||||||
ToolTip="{Binding Message}"
|
IsReadOnly="True"
|
||||||
TextTrimming="CharacterEllipsis"
|
BorderThickness="0"
|
||||||
VerticalAlignment="Center" />
|
Background="Transparent"
|
||||||
|
ToolTip="{Binding Message}"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</DataGridTemplateColumn.CellTemplate>
|
</DataGridTemplateColumn.CellTemplate>
|
||||||
</DataGridTemplateColumn>
|
</DataGridTemplateColumn>
|
||||||
|
|||||||
@@ -11,6 +11,39 @@
|
|||||||
d:DataContext="{Binding Source={StaticResource DesignTimeLocator}, Path=SystemSettingViewModel}"
|
d:DataContext="{Binding Source={StaticResource DesignTimeLocator}, Path=SystemSettingViewModel}"
|
||||||
DataContext="{Binding Source={StaticResource Locator}, Path=SystemSettingViewModel}">
|
DataContext="{Binding Source={StaticResource Locator}, Path=SystemSettingViewModel}">
|
||||||
|
|
||||||
|
<Page.Resources>
|
||||||
|
<Style x:Key="MonitorDataGridColumnHeaderStyle" TargetType="DataGridColumnHeader">
|
||||||
|
<Setter Property="Background" Value="#F2F5FA" />
|
||||||
|
<Setter Property="Foreground" Value="#4B5563" />
|
||||||
|
<Setter Property="FontSize" Value="12" />
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold" />
|
||||||
|
<Setter Property="BorderBrush" Value="Transparent" />
|
||||||
|
<Setter Property="BorderThickness" Value="0" />
|
||||||
|
<Setter Property="Padding" Value="14,10" />
|
||||||
|
<Setter Property="HorizontalContentAlignment" Value="Left" />
|
||||||
|
</Style>
|
||||||
|
<Style x:Key="MonitorDataGridStyle" TargetType="DataGrid">
|
||||||
|
<Setter Property="Background" Value="Transparent" />
|
||||||
|
<Setter Property="BorderBrush" Value="Transparent" />
|
||||||
|
<Setter Property="BorderThickness" Value="0" />
|
||||||
|
<Setter Property="RowBackground" Value="Transparent" />
|
||||||
|
<Setter Property="AlternatingRowBackground" Value="#FAFBFD" />
|
||||||
|
<Setter Property="HeadersVisibility" Value="Column" />
|
||||||
|
<Setter Property="GridLinesVisibility" Value="Horizontal" />
|
||||||
|
<Setter Property="HorizontalGridLinesBrush" Value="#EEF1F6" />
|
||||||
|
<Setter Property="VerticalGridLinesBrush" Value="Transparent" />
|
||||||
|
<Setter Property="ColumnHeaderHeight" Value="40" />
|
||||||
|
<Setter Property="RowHeight" Value="42" />
|
||||||
|
<Setter Property="CanUserResizeRows" Value="False" />
|
||||||
|
<Setter Property="AutoGenerateColumns" Value="False" />
|
||||||
|
<Setter Property="IsReadOnly" Value="True" />
|
||||||
|
<Setter Property="SelectionMode" Value="Single" />
|
||||||
|
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Auto" />
|
||||||
|
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto" />
|
||||||
|
<Setter Property="ColumnHeaderStyle" Value="{StaticResource MonitorDataGridColumnHeaderStyle}" />
|
||||||
|
</Style>
|
||||||
|
</Page.Resources>
|
||||||
|
|
||||||
<Grid Margin="16">
|
<Grid Margin="16">
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="*" />
|
<RowDefinition Height="*" />
|
||||||
@@ -18,76 +51,123 @@
|
|||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<TabControl Grid.Row="0" Style="{StaticResource TabControlInLine}">
|
<TabControl Grid.Row="0" Style="{StaticResource TabControlInLine}">
|
||||||
<TabItem Header="PLC">
|
<TabItem Header="基础设置">
|
||||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
<TabControl Style="{StaticResource TabControlInLine}">
|
||||||
<StackPanel Margin="16">
|
<TabItem Header="PLC">
|
||||||
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="PLC 主机" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Plc.Host, UpdateSourceTrigger=PropertyChanged}" />
|
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||||
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="端口" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Plc.Port, UpdateSourceTrigger=PropertyChanged}" />
|
<StackPanel Margin="16">
|
||||||
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="从站号" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Plc.UnitId, UpdateSourceTrigger=PropertyChanged}" />
|
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="PLC 主机" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Plc.Host, UpdateSourceTrigger=PropertyChanged}" />
|
||||||
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="轮询周期(ms)" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Plc.PollIntervalMs, UpdateSourceTrigger=PropertyChanged}" />
|
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="端口" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Plc.Port, UpdateSourceTrigger=PropertyChanged}" />
|
||||||
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="连接超时(ms)" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Plc.ConnectTimeoutMs, UpdateSourceTrigger=PropertyChanged}" />
|
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="从站号" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Plc.UnitId, UpdateSourceTrigger=PropertyChanged}" />
|
||||||
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="心跳周期(ms)" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Plc.HeartbeatIntervalMs, UpdateSourceTrigger=PropertyChanged}" />
|
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="轮询周期(ms)" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Plc.PollIntervalMs, UpdateSourceTrigger=PropertyChanged}" />
|
||||||
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="放行脉冲(ms)" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Plc.ReleasePulseMs, UpdateSourceTrigger=PropertyChanged}" />
|
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="连接超时(ms)" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Plc.ConnectTimeoutMs, UpdateSourceTrigger=PropertyChanged}" />
|
||||||
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="放行应答超时(ms)" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Plc.ReleaseAckTimeoutMs, UpdateSourceTrigger=PropertyChanged}" />
|
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="放行脉冲(ms)" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Plc.ReleasePulseMs, UpdateSourceTrigger=PropertyChanged}" />
|
||||||
</StackPanel>
|
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="放行应答超时(ms)" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Plc.ReleaseAckTimeoutMs, UpdateSourceTrigger=PropertyChanged}" />
|
||||||
</ScrollViewer>
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
|
<TabItem Header="扫码枪">
|
||||||
|
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||||
|
<StackPanel Margin="16">
|
||||||
|
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="串口号" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Scanner.PortName, UpdateSourceTrigger=PropertyChanged}" />
|
||||||
|
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="波特率" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Scanner.BaudRate, UpdateSourceTrigger=PropertyChanged}" />
|
||||||
|
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="数据位" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Scanner.DataBits, UpdateSourceTrigger=PropertyChanged}" />
|
||||||
|
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="校验位" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Scanner.Parity, UpdateSourceTrigger=PropertyChanged}" />
|
||||||
|
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="停止位" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Scanner.StopBits, UpdateSourceTrigger=PropertyChanged}" />
|
||||||
|
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="超时(ms)" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Scanner.ReadTimeoutMs, UpdateSourceTrigger=PropertyChanged}" />
|
||||||
|
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="触发命令" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Scanner.TriggerCommand, UpdateSourceTrigger=PropertyChanged}" />
|
||||||
|
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="返回结束符" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Scanner.ResponseTerminator, UpdateSourceTrigger=PropertyChanged}" />
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
|
<TabItem Header="SFTP">
|
||||||
|
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||||
|
<StackPanel Margin="16">
|
||||||
|
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="主机" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Sftp.Host, UpdateSourceTrigger=PropertyChanged}" />
|
||||||
|
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="端口" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Sftp.Port, UpdateSourceTrigger=PropertyChanged}" />
|
||||||
|
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="用户名" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Sftp.Username, UpdateSourceTrigger=PropertyChanged}" />
|
||||||
|
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="密码" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Sftp.Password, UpdateSourceTrigger=PropertyChanged}" />
|
||||||
|
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="私钥路径" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Sftp.PrivateKeyPath, UpdateSourceTrigger=PropertyChanged}" />
|
||||||
|
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="根目录" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Sftp.RootPath, UpdateSourceTrigger=PropertyChanged}" />
|
||||||
|
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="文件名模板" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Sftp.FileNamePattern, UpdateSourceTrigger=PropertyChanged}" />
|
||||||
|
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="重试间隔(秒)" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Sftp.RetryIntervalSeconds, UpdateSourceTrigger=PropertyChanged}" />
|
||||||
|
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="最大重试次数" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Sftp.MaxRetryCount, UpdateSourceTrigger=PropertyChanged}" />
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,12,0,0">
|
||||||
|
<Button Style="{StaticResource ButtonDefault}"
|
||||||
|
Padding="18,8"
|
||||||
|
MinHeight="36"
|
||||||
|
Content="测试连接"
|
||||||
|
Command="{Binding TestSftpConnectionCommand}" />
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
|
<TabItem Header="安灯 & 流程">
|
||||||
|
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||||
|
<StackPanel Margin="16">
|
||||||
|
<CheckBox Content="启用安灯接口" IsChecked="{Binding EditableConfig.Andon.Enable}" Margin="0,6,0,0" />
|
||||||
|
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="安灯 URL" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Andon.Url, UpdateSourceTrigger=PropertyChanged}" />
|
||||||
|
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="请求方法" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Andon.Method, UpdateSourceTrigger=PropertyChanged}" />
|
||||||
|
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="超时(ms)" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Andon.TimeoutMs, UpdateSourceTrigger=PropertyChanged}" />
|
||||||
|
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="安灯主题" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Andon.AndonTheme, UpdateSourceTrigger=PropertyChanged}" />
|
||||||
|
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="设备 EID" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Andon.Eid, UpdateSourceTrigger=PropertyChanged}" />
|
||||||
|
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="来源标记" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Andon.FromSign, UpdateSourceTrigger=PropertyChanged}" />
|
||||||
|
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="服务 ID" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Andon.ServiceId, UpdateSourceTrigger=PropertyChanged}" />
|
||||||
|
<CheckBox Content="扫码失败时报警" IsChecked="{Binding EditableConfig.Andon.EnableScanFailAlarm}" Margin="0,6,0,0" />
|
||||||
|
<CheckBox Content="文件未找到时报警" IsChecked="{Binding EditableConfig.Andon.EnableFileNotFoundAlarm}" Margin="0,6,0,0" />
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
</TabItem>
|
||||||
|
</TabControl>
|
||||||
</TabItem>
|
</TabItem>
|
||||||
|
|
||||||
<TabItem Header="扫码枪">
|
<TabItem Header="PLC监控">
|
||||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
<Grid Margin="16">
|
||||||
<StackPanel Margin="16">
|
<Grid.RowDefinitions>
|
||||||
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="串口号" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Scanner.PortName, UpdateSourceTrigger=PropertyChanged}" />
|
<RowDefinition Height="Auto" />
|
||||||
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="波特率" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Scanner.BaudRate, UpdateSourceTrigger=PropertyChanged}" />
|
<RowDefinition Height="12" />
|
||||||
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="数据位" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Scanner.DataBits, UpdateSourceTrigger=PropertyChanged}" />
|
<RowDefinition Height="*" />
|
||||||
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="校验位" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Scanner.Parity, UpdateSourceTrigger=PropertyChanged}" />
|
</Grid.RowDefinitions>
|
||||||
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="停止位" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Scanner.StopBits, UpdateSourceTrigger=PropertyChanged}" />
|
|
||||||
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="超时(ms)" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Scanner.ReadTimeoutMs, UpdateSourceTrigger=PropertyChanged}" />
|
|
||||||
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="触发命令" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Scanner.TriggerCommand, UpdateSourceTrigger=PropertyChanged}" />
|
|
||||||
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="返回结束符" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Scanner.ResponseTerminator, UpdateSourceTrigger=PropertyChanged}" />
|
|
||||||
</StackPanel>
|
|
||||||
</ScrollViewer>
|
|
||||||
</TabItem>
|
|
||||||
|
|
||||||
<TabItem Header="SFTP">
|
<StackPanel Grid.Row="0">
|
||||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
<TextBlock Text="PLC 实时变量监控" FontSize="18" FontWeight="SemiBold" Foreground="#1F2937" />
|
||||||
<StackPanel Margin="16">
|
<TextBlock Text="显示 PLC 当前实际回读值,不使用应用内部缓存。"
|
||||||
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="主机" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Sftp.Host, UpdateSourceTrigger=PropertyChanged}" />
|
Margin="0,4,0,0"
|
||||||
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="端口" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Sftp.Port, UpdateSourceTrigger=PropertyChanged}" />
|
Foreground="#6B7280" />
|
||||||
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="用户名" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Sftp.Username, UpdateSourceTrigger=PropertyChanged}" />
|
|
||||||
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="密码" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Sftp.Password, UpdateSourceTrigger=PropertyChanged}" />
|
|
||||||
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="私钥路径" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Sftp.PrivateKeyPath, UpdateSourceTrigger=PropertyChanged}" />
|
|
||||||
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="根目录" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Sftp.RootPath, UpdateSourceTrigger=PropertyChanged}" />
|
|
||||||
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="文件名模板" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Sftp.FileNamePattern, UpdateSourceTrigger=PropertyChanged}" />
|
|
||||||
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="重试间隔(秒)" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Sftp.RetryIntervalSeconds, UpdateSourceTrigger=PropertyChanged}" />
|
|
||||||
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="最大重试次数" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Sftp.MaxRetryCount, UpdateSourceTrigger=PropertyChanged}" />
|
|
||||||
<StackPanel Orientation="Horizontal" Margin="0,12,0,0">
|
|
||||||
<Button Style="{StaticResource ButtonDefault}"
|
|
||||||
Padding="18,8"
|
|
||||||
MinHeight="36"
|
|
||||||
Content="测试连接"
|
|
||||||
Command="{Binding TestSftpConnectionCommand}" />
|
|
||||||
</StackPanel>
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</ScrollViewer>
|
|
||||||
</TabItem>
|
|
||||||
|
|
||||||
<TabItem Header="安灯 & 流程">
|
<Border Grid.Row="2"
|
||||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
Background="White"
|
||||||
<StackPanel Margin="16">
|
BorderBrush="#E8ECF3"
|
||||||
<CheckBox Content="启用安灯接口" IsChecked="{Binding EditableConfig.Andon.Enable}" Margin="0,6,0,0" />
|
BorderThickness="1"
|
||||||
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="安灯 URL" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Andon.Url, UpdateSourceTrigger=PropertyChanged}" />
|
CornerRadius="10"
|
||||||
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="请求方法" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Andon.Method, UpdateSourceTrigger=PropertyChanged}" />
|
Padding="8">
|
||||||
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="超时(ms)" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Andon.TimeoutMs, UpdateSourceTrigger=PropertyChanged}" />
|
<DataGrid ItemsSource="{Binding PlcMonitorItems}"
|
||||||
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="安灯主题" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Andon.AndonTheme, UpdateSourceTrigger=PropertyChanged}" />
|
Style="{StaticResource MonitorDataGridStyle}"
|
||||||
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="设备 EID" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Andon.Eid, UpdateSourceTrigger=PropertyChanged}" />
|
CanUserSortColumns="False">
|
||||||
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="来源标记" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Andon.FromSign, UpdateSourceTrigger=PropertyChanged}" />
|
<DataGrid.Columns>
|
||||||
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="服务 ID" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Andon.ServiceId, UpdateSourceTrigger=PropertyChanged}" />
|
<DataGridTextColumn Header="变量名"
|
||||||
<CheckBox Content="扫码失败时报警" IsChecked="{Binding EditableConfig.Andon.EnableScanFailAlarm}" Margin="0,6,0,0" />
|
Binding="{Binding Name}"
|
||||||
<CheckBox Content="文件未找到时报警" IsChecked="{Binding EditableConfig.Andon.EnableFileNotFoundAlarm}" Margin="0,6,0,0" />
|
Width="1.2*" />
|
||||||
<CheckBox Content="要求 PLC Ready" IsChecked="{Binding EditableConfig.Workflow.RequirePlcReady}" Margin="0,6,0,0" />
|
<DataGridTextColumn Header="地址"
|
||||||
<CheckBox Content="要求 AutoMode & StationEnable" IsChecked="{Binding EditableConfig.Workflow.RequireAutoMode}" Margin="0,6,0,0" />
|
Binding="{Binding Address}"
|
||||||
</StackPanel>
|
Width="0.9*" />
|
||||||
</ScrollViewer>
|
<DataGridTextColumn Header="分组"
|
||||||
|
Binding="{Binding GroupName}"
|
||||||
|
Width="0.9*" />
|
||||||
|
<DataGridTextColumn Header="当前值"
|
||||||
|
Binding="{Binding CurrentValue}"
|
||||||
|
Width="0.9*" />
|
||||||
|
<DataGridTextColumn Header="最近更新时间"
|
||||||
|
Binding="{Binding LastUpdatedAt, StringFormat=yyyy-MM-dd HH:mm:ss}"
|
||||||
|
Width="1.4*" />
|
||||||
|
</DataGrid.Columns>
|
||||||
|
</DataGrid>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
</TabItem>
|
</TabItem>
|
||||||
</TabControl>
|
</TabControl>
|
||||||
|
|
||||||
|
|||||||
@@ -5,35 +5,19 @@
|
|||||||
"UnitId": 1,
|
"UnitId": 1,
|
||||||
"PollIntervalMs": 200,
|
"PollIntervalMs": 200,
|
||||||
"ConnectTimeoutMs": 3000,
|
"ConnectTimeoutMs": 3000,
|
||||||
"HeartbeatIntervalMs": 500,
|
|
||||||
"ReleasePulseMs": 500,
|
"ReleasePulseMs": 500,
|
||||||
"ReleaseAckTimeoutMs": 2000,
|
"ReleaseAckTimeoutMs": 2000,
|
||||||
"Inputs": {
|
"Inputs": {
|
||||||
"PlcReady": 10001,
|
"PcbArrived": 0,
|
||||||
"PcbArrived": 10002,
|
"PlcReset": 1,
|
||||||
"PlcReset": 10003,
|
"PlcAckRelease": 2
|
||||||
"PlcAckRelease": 10004,
|
|
||||||
"AutoMode": 10005,
|
|
||||||
"StationEnable": 10006
|
|
||||||
},
|
},
|
||||||
"Outputs": {
|
"Outputs": {
|
||||||
"PcOnline": 51,
|
"PcBusy": 51,
|
||||||
"PcBusy": 52,
|
"ReleasePermit": 52
|
||||||
"ScanOk": 53,
|
|
||||||
"ScanNg": 54,
|
|
||||||
"FileFound": 55,
|
|
||||||
"FileNotFound": 56,
|
|
||||||
"AlarmRaised": 57,
|
|
||||||
"ReleasePermit": 58,
|
|
||||||
"ProcessDone": 59,
|
|
||||||
"SystemFault": 60
|
|
||||||
},
|
},
|
||||||
"Registers": {
|
"Registers": {
|
||||||
"ResultCode": 40001,
|
"ResultCode": 0
|
||||||
"ScanTryCount": 40002,
|
|
||||||
"SftpTryCount": 40003,
|
|
||||||
"AlarmCode": 40004,
|
|
||||||
"FlowStateCode": 40005
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Scanner": {
|
"Scanner": {
|
||||||
@@ -76,9 +60,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Workflow": {
|
"Workflow": {
|
||||||
"RequirePlcReady": true,
|
|
||||||
"RequireAutoMode": true,
|
|
||||||
"RequireStationEnable": true,
|
|
||||||
"RequireManualResetAfterFault": true,
|
"RequireManualResetAfterFault": true,
|
||||||
"MaxUiLogEntries": 200,
|
"MaxUiLogEntries": 200,
|
||||||
"MaxBoardRecords": 100
|
"MaxBoardRecords": 100
|
||||||
|
|||||||
149
tests/AxiOmron.PcbCheck.Tests/ShutdownHelperTests.cs
Normal file
149
tests/AxiOmron.PcbCheck.Tests/ShutdownHelperTests.cs
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
|
using AxiOmron.PcbCheck.Utils;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace AxiOmron.PcbCheck.Tests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证应用退出阶段的异步清理流程不会丢失原始线程上下文。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ShutdownHelperTests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 停止 Host 后,后续收尾回调应继续在捕获到的同步上下文线程上执行。
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>异步测试任务。</returns>
|
||||||
|
[Fact]
|
||||||
|
public async Task StopHostAsync_ShouldResumeOnCapturedSynchronizationContext()
|
||||||
|
{
|
||||||
|
await SingleThreadSynchronizationContext.RunAsync(async () =>
|
||||||
|
{
|
||||||
|
int expectedThreadId = Environment.CurrentManagedThreadId;
|
||||||
|
int callbackThreadId = -1;
|
||||||
|
var host = new FakeHost();
|
||||||
|
|
||||||
|
await ShutdownHelper.StopHostAsync(
|
||||||
|
host,
|
||||||
|
TimeSpan.FromMilliseconds(200),
|
||||||
|
() => callbackThreadId = Environment.CurrentManagedThreadId);
|
||||||
|
|
||||||
|
Assert.True(host.StopAsyncCalled);
|
||||||
|
Assert.Equal(expectedThreadId, callbackThreadId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class FakeHost : IHost
|
||||||
|
{
|
||||||
|
public IServiceProvider Services => throw new NotSupportedException();
|
||||||
|
|
||||||
|
public bool StopAsyncCalled { get; private set; }
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task StopAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
StopAsyncCalled = true;
|
||||||
|
await Task.Yield();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 提供仅在单线程内串行泵送回调的同步上下文,便于验证 await 后的线程恢复行为。
|
||||||
|
/// </summary>
|
||||||
|
private sealed class SingleThreadSynchronizationContext : SynchronizationContext
|
||||||
|
{
|
||||||
|
private readonly BlockingCollection<(SendOrPostCallback Callback, object? State)> _queue = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 在专用线程同步上下文中执行异步委托。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="action">待执行的异步委托。</param>
|
||||||
|
/// <returns>表示执行完成的任务。</returns>
|
||||||
|
public static Task RunAsync(Func<Task> action)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(action);
|
||||||
|
|
||||||
|
var completionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
var thread = new Thread(() =>
|
||||||
|
{
|
||||||
|
var context = new SingleThreadSynchronizationContext();
|
||||||
|
SynchronizationContext.SetSynchronizationContext(context);
|
||||||
|
Task task;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
task = action();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
completionSource.SetException(ex);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
task.ContinueWith(
|
||||||
|
completedTask =>
|
||||||
|
{
|
||||||
|
if (completedTask.IsFaulted)
|
||||||
|
{
|
||||||
|
completionSource.SetException(completedTask.Exception!.InnerExceptions);
|
||||||
|
}
|
||||||
|
else if (completedTask.IsCanceled)
|
||||||
|
{
|
||||||
|
completionSource.SetCanceled();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
completionSource.SetResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
context._queue.CompleteAdding();
|
||||||
|
},
|
||||||
|
CancellationToken.None,
|
||||||
|
TaskContinuationOptions.None,
|
||||||
|
TaskScheduler.Default);
|
||||||
|
|
||||||
|
foreach ((SendOrPostCallback callback, object? state) in context._queue.GetConsumingEnumerable())
|
||||||
|
{
|
||||||
|
callback(state);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
{
|
||||||
|
IsBackground = true
|
||||||
|
};
|
||||||
|
|
||||||
|
thread.SetApartmentState(ApartmentState.STA);
|
||||||
|
thread.Start();
|
||||||
|
return completionSource.Task;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 将委托异步排入当前上下文队列。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="d">待执行回调。</param>
|
||||||
|
/// <param name="state">状态对象。</param>
|
||||||
|
public override void Post(SendOrPostCallback d, object? state)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(d);
|
||||||
|
_queue.Add((d, state));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 在当前线程直接执行同步回调。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="d">待执行回调。</param>
|
||||||
|
/// <param name="state">状态对象。</param>
|
||||||
|
public override void Send(SendOrPostCallback d, object? state)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(d);
|
||||||
|
d(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,12 @@
|
|||||||
using AxiOmron.PcbCheck.Models;
|
using AxiOmron.PcbCheck.Models;
|
||||||
using AxiOmron.PcbCheck.Options;
|
using AxiOmron.PcbCheck.Options;
|
||||||
|
using AxiOmron.PcbCheck.Services.Implementations;
|
||||||
using AxiOmron.PcbCheck.Services.Interfaces;
|
using AxiOmron.PcbCheck.Services.Interfaces;
|
||||||
using AxiOmron.PcbCheck.ViewModels;
|
using AxiOmron.PcbCheck.ViewModels;
|
||||||
|
using System.Runtime.ExceptionServices;
|
||||||
|
using System.Windows.Data;
|
||||||
|
using System.Windows.Threading;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
namespace AxiOmron.PcbCheck.Tests;
|
namespace AxiOmron.PcbCheck.Tests;
|
||||||
|
|
||||||
@@ -18,6 +23,8 @@ public sealed class SystemSettingViewModelTests
|
|||||||
public async Task TestSftpConnectionAsync_ShouldReportSuccess_WhenConnectionSucceeds()
|
public async Task TestSftpConnectionAsync_ShouldReportSuccess_WhenConnectionSucceeds()
|
||||||
{
|
{
|
||||||
var configService = new FakeAppConfigService();
|
var configService = new FakeAppConfigService();
|
||||||
|
var stateStore = new AppStateStore();
|
||||||
|
var dispatcherService = new ImmediateDispatcherService();
|
||||||
var sftpLookupService = new FakeSftpLookupService
|
var sftpLookupService = new FakeSftpLookupService
|
||||||
{
|
{
|
||||||
TestOutcome = new SftpConnectionTestOutcome
|
TestOutcome = new SftpConnectionTestOutcome
|
||||||
@@ -27,7 +34,7 @@ public sealed class SystemSettingViewModelTests
|
|||||||
StatusMessage = "SFTP 连接成功,根目录可访问。"
|
StatusMessage = "SFTP 连接成功,根目录可访问。"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
var viewModel = new SystemSettingViewModel(configService, sftpLookupService);
|
var viewModel = new SystemSettingViewModel(configService, sftpLookupService, stateStore, dispatcherService);
|
||||||
|
|
||||||
await viewModel.TestSftpConnectionCommand.ExecuteAsync(null);
|
await viewModel.TestSftpConnectionCommand.ExecuteAsync(null);
|
||||||
|
|
||||||
@@ -42,6 +49,8 @@ public sealed class SystemSettingViewModelTests
|
|||||||
public async Task TestSftpConnectionAsync_ShouldReportFailure_WhenConnectionFails()
|
public async Task TestSftpConnectionAsync_ShouldReportFailure_WhenConnectionFails()
|
||||||
{
|
{
|
||||||
var configService = new FakeAppConfigService();
|
var configService = new FakeAppConfigService();
|
||||||
|
var stateStore = new AppStateStore();
|
||||||
|
var dispatcherService = new ImmediateDispatcherService();
|
||||||
var sftpLookupService = new FakeSftpLookupService
|
var sftpLookupService = new FakeSftpLookupService
|
||||||
{
|
{
|
||||||
TestOutcome = new SftpConnectionTestOutcome
|
TestOutcome = new SftpConnectionTestOutcome
|
||||||
@@ -51,13 +60,167 @@ public sealed class SystemSettingViewModelTests
|
|||||||
StatusMessage = "SFTP 连接失败: timeout"
|
StatusMessage = "SFTP 连接失败: timeout"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
var viewModel = new SystemSettingViewModel(configService, sftpLookupService);
|
var viewModel = new SystemSettingViewModel(configService, sftpLookupService, stateStore, dispatcherService);
|
||||||
|
|
||||||
await viewModel.TestSftpConnectionCommand.ExecuteAsync(null);
|
await viewModel.TestSftpConnectionCommand.ExecuteAsync(null);
|
||||||
|
|
||||||
Assert.Equal("SFTP 连接失败: timeout", viewModel.StatusMessage);
|
Assert.Equal("SFTP 连接失败: timeout", viewModel.StatusMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 当运行态快照包含 PLC 监控项时,应同步到系统设置页监控列表。
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void Constructor_ShouldLoadPlcMonitorItems_FromStateStoreSnapshot()
|
||||||
|
{
|
||||||
|
var configService = new FakeAppConfigService();
|
||||||
|
var stateStore = new AppStateStore();
|
||||||
|
var dispatcherService = new ImmediateDispatcherService();
|
||||||
|
stateStore.UpdateSnapshot(snapshot =>
|
||||||
|
{
|
||||||
|
snapshot.PlcMonitorItems.Add(new PlcMonitorItem
|
||||||
|
{
|
||||||
|
GroupName = "Inputs",
|
||||||
|
Name = "PcbArrived",
|
||||||
|
CurrentValue = "True",
|
||||||
|
LastUpdatedAt = DateTimeOffset.Now
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
var viewModel = new SystemSettingViewModel(configService, new FakeSftpLookupService(), stateStore, dispatcherService);
|
||||||
|
|
||||||
|
Assert.Single(viewModel.PlcMonitorItems);
|
||||||
|
Assert.Equal("PcbArrived", viewModel.PlcMonitorItems[0].Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 当后台线程发布运行态快照时,不应直接在后台线程修改已绑定到 CollectionView 的 PLC 监控集合。
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void UpdateSnapshot_ShouldNotThrow_WhenSnapshotChangesFromBackgroundThread()
|
||||||
|
{
|
||||||
|
Exception? backgroundException = RunInSta(() =>
|
||||||
|
{
|
||||||
|
var configService = new FakeAppConfigService();
|
||||||
|
var stateStore = new AppStateStore();
|
||||||
|
var dispatcherService = new CapturedDispatcherService(Dispatcher.CurrentDispatcher);
|
||||||
|
var viewModel = new SystemSettingViewModel(configService, new FakeSftpLookupService(), stateStore, dispatcherService);
|
||||||
|
_ = CollectionViewSource.GetDefaultView(viewModel.PlcMonitorItems);
|
||||||
|
|
||||||
|
Exception? capturedException = null;
|
||||||
|
using var completed = new ManualResetEventSlim(false);
|
||||||
|
var workerThread = new Thread(() =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
stateStore.UpdateSnapshot(snapshot =>
|
||||||
|
{
|
||||||
|
snapshot.PlcMonitorItems.Clear();
|
||||||
|
snapshot.PlcMonitorItems.Add(new PlcMonitorItem
|
||||||
|
{
|
||||||
|
GroupName = "Inputs",
|
||||||
|
Name = "PcbArrived",
|
||||||
|
CurrentValue = "True",
|
||||||
|
LastUpdatedAt = DateTimeOffset.Now
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
capturedException = ex;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
completed.Set();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
workerThread.IsBackground = true;
|
||||||
|
workerThread.Start();
|
||||||
|
|
||||||
|
Assert.True(completed.Wait(TimeSpan.FromSeconds(5)), "后台线程未在预期时间内完成快照更新。");
|
||||||
|
workerThread.Join();
|
||||||
|
PumpDispatcherUntil(() => viewModel.PlcMonitorItems.Count == 1, TimeSpan.FromSeconds(5));
|
||||||
|
Assert.Single(viewModel.PlcMonitorItems);
|
||||||
|
return capturedException;
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.Null(backgroundException);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 在 STA 线程中执行指定委托,并将内部异常重新抛回当前测试线程。
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">委托返回值类型。</typeparam>
|
||||||
|
/// <param name="action">待执行的委托。</param>
|
||||||
|
/// <returns>委托返回结果。</returns>
|
||||||
|
/// <exception cref="ArgumentNullException">当 <paramref name="action"/> 为 <see langword="null"/> 时抛出。</exception>
|
||||||
|
private static T RunInSta<T>(Func<T> action)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(action);
|
||||||
|
|
||||||
|
T? result = default;
|
||||||
|
Exception? capturedException = null;
|
||||||
|
using var completed = new ManualResetEventSlim(false);
|
||||||
|
var thread = new Thread(() =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
result = action();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
capturedException = ex;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
completed.Set();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
thread.SetApartmentState(ApartmentState.STA);
|
||||||
|
thread.Start();
|
||||||
|
|
||||||
|
Assert.True(completed.Wait(TimeSpan.FromSeconds(5)), "STA 测试线程未在预期时间内完成。");
|
||||||
|
thread.Join();
|
||||||
|
|
||||||
|
if (capturedException is not null)
|
||||||
|
{
|
||||||
|
ExceptionDispatchInfo.Capture(capturedException).Throw();
|
||||||
|
}
|
||||||
|
|
||||||
|
return result!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 在当前 STA 线程中循环处理 Dispatcher 队列,直到满足指定条件。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="condition">停止等待的条件。</param>
|
||||||
|
/// <param name="timeout">最大等待时长。</param>
|
||||||
|
/// <exception cref="ArgumentNullException">当 <paramref name="condition"/> 为 <see langword="null"/> 时抛出。</exception>
|
||||||
|
/// <exception cref="TimeoutException">当超时仍未满足条件时抛出。</exception>
|
||||||
|
private static void PumpDispatcherUntil(Func<bool> condition, TimeSpan timeout)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(condition);
|
||||||
|
|
||||||
|
DateTime deadline = DateTime.UtcNow.Add(timeout);
|
||||||
|
while (!condition())
|
||||||
|
{
|
||||||
|
if (DateTime.UtcNow > deadline)
|
||||||
|
{
|
||||||
|
throw new TimeoutException("Dispatcher 队列在预期时间内未完成处理。");
|
||||||
|
}
|
||||||
|
|
||||||
|
var frame = new DispatcherFrame();
|
||||||
|
_ = Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Background, new DispatcherOperationCallback(_ =>
|
||||||
|
{
|
||||||
|
frame.Continue = false;
|
||||||
|
return null;
|
||||||
|
}), null);
|
||||||
|
Dispatcher.PushFrame(frame);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private sealed class FakeAppConfigService : IAppConfigService
|
private sealed class FakeAppConfigService : IAppConfigService
|
||||||
{
|
{
|
||||||
public AppConfig Config { get; } = new();
|
public AppConfig Config { get; } = new();
|
||||||
@@ -92,4 +255,36 @@ public sealed class SystemSettingViewModelTests
|
|||||||
return Task.FromResult(TestOutcome);
|
return Task.FromResult(TestOutcome);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 提供立即执行的测试用 Dispatcher,实现与设计时调度一致的同步行为。
|
||||||
|
/// </summary>
|
||||||
|
private sealed class ImmediateDispatcherService : IDispatcherService
|
||||||
|
{
|
||||||
|
public Task InvokeAsync(Action action)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(action);
|
||||||
|
action();
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 提供绑定到指定 Dispatcher 的测试调度服务,用于验证跨线程回切行为。
|
||||||
|
/// </summary>
|
||||||
|
private sealed class CapturedDispatcherService : IDispatcherService
|
||||||
|
{
|
||||||
|
private readonly Dispatcher _dispatcher;
|
||||||
|
|
||||||
|
public CapturedDispatcherService(Dispatcher dispatcher)
|
||||||
|
{
|
||||||
|
_dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task InvokeAsync(Action action)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(action);
|
||||||
|
return _dispatcher.InvokeAsync(action).Task;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using AxiOmron.PcbCheck.Models;
|
|||||||
using AxiOmron.PcbCheck.Options;
|
using AxiOmron.PcbCheck.Options;
|
||||||
using AxiOmron.PcbCheck.Services.Implementations;
|
using AxiOmron.PcbCheck.Services.Implementations;
|
||||||
using AxiOmron.PcbCheck.Services.Interfaces;
|
using AxiOmron.PcbCheck.Services.Interfaces;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
namespace AxiOmron.PcbCheck.Tests;
|
namespace AxiOmron.PcbCheck.Tests;
|
||||||
|
|
||||||
@@ -40,13 +41,312 @@ public sealed class WorkflowHostedServiceTests
|
|||||||
Assert.Equal("异常", stateStore.GetSnapshot().SftpStatus);
|
Assert.Equal("异常", stateStore.GetSnapshot().SftpStatus);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 启动探活 PLC 成功时,应立即将 PLC 状态更新为已连接。
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>异步测试任务。</returns>
|
||||||
|
[Fact]
|
||||||
|
public async Task ProbePlcOnStartupAsync_ShouldUpdateSnapshot_WhenConnectionSucceeds()
|
||||||
|
{
|
||||||
|
var stateStore = new AppStateStore();
|
||||||
|
var plcService = new ProbingPlcService();
|
||||||
|
var service = new WorkflowHostedService(
|
||||||
|
plcService,
|
||||||
|
new FakeScannerService(),
|
||||||
|
new FakeSftpLookupService(),
|
||||||
|
new FakeAndonService(),
|
||||||
|
stateStore,
|
||||||
|
new AppConfig(),
|
||||||
|
new FakeAppLogger<WorkflowHostedService>());
|
||||||
|
|
||||||
|
await service.ProbePlcOnStartupAsync(CancellationToken.None);
|
||||||
|
|
||||||
|
RuntimeSnapshot snapshot = stateStore.GetSnapshot();
|
||||||
|
Assert.Equal("已连接", snapshot.PlcStatus);
|
||||||
|
Assert.True(plcService.ForceReconnectCalled);
|
||||||
|
Assert.Contains(snapshot.PlcMonitorItems, item => item.Name == "PcbArrived" && item.CurrentValue == "True");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 启动探活 PLC 失败时,应将状态更新为连接失败而不是保留默认未连接。
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>异步测试任务。</returns>
|
||||||
|
[Fact]
|
||||||
|
public async Task ProbePlcOnStartupAsync_ShouldUpdateSnapshot_WhenConnectionFails()
|
||||||
|
{
|
||||||
|
var stateStore = new AppStateStore();
|
||||||
|
var service = new WorkflowHostedService(
|
||||||
|
new FailingPlcService(),
|
||||||
|
new FakeScannerService(),
|
||||||
|
new FakeSftpLookupService(),
|
||||||
|
new FakeAndonService(),
|
||||||
|
stateStore,
|
||||||
|
new AppConfig(),
|
||||||
|
new FakeAppLogger<WorkflowHostedService>());
|
||||||
|
|
||||||
|
await service.ProbePlcOnStartupAsync(CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.StartsWith("连接失败:", stateStore.GetSnapshot().PlcStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 当 PLC 仅提供到位信号时,流程仍应启动,不再依赖就绪、自动模式和工位使能位。
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>异步测试任务。</returns>
|
||||||
|
[Fact]
|
||||||
|
public async Task ExecuteAsync_ShouldStartWorkflow_WhenOnlyPcbArrivedIsProvided()
|
||||||
|
{
|
||||||
|
var stateStore = new AppStateStore();
|
||||||
|
var plcService = new SequencedPlcService(new[]
|
||||||
|
{
|
||||||
|
new PlcSignalSnapshot { PcbArrived = true, PlcAckRelease = true },
|
||||||
|
new PlcSignalSnapshot { PcbArrived = false, PlcAckRelease = true }
|
||||||
|
});
|
||||||
|
var scannerService = new CountingScannerService
|
||||||
|
{
|
||||||
|
Result = new ScanOperationResult
|
||||||
|
{
|
||||||
|
IsSuccess = true,
|
||||||
|
DeviceConnected = true,
|
||||||
|
Barcode = "PCB-001"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var sftpService = new FakeSftpLookupService
|
||||||
|
{
|
||||||
|
CheckOutcome = new SftpCheckOutcome
|
||||||
|
{
|
||||||
|
Exists = true,
|
||||||
|
ConnectionSucceeded = true,
|
||||||
|
MatchedFilePath = "/pcb/PCB-001.txt"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var service = new WorkflowHostedService(
|
||||||
|
plcService,
|
||||||
|
scannerService,
|
||||||
|
sftpService,
|
||||||
|
new FakeAndonService(),
|
||||||
|
stateStore,
|
||||||
|
new AppConfig
|
||||||
|
{
|
||||||
|
Plc = new PlcOptions
|
||||||
|
{
|
||||||
|
PollIntervalMs = 20,
|
||||||
|
ReleaseAckTimeoutMs = 100,
|
||||||
|
ReleasePulseMs = 10
|
||||||
|
},
|
||||||
|
Scanner = new ScannerOptions
|
||||||
|
{
|
||||||
|
MaxScanAttempts = 1
|
||||||
|
},
|
||||||
|
Sftp = new SftpOptions
|
||||||
|
{
|
||||||
|
MaxRetryCount = 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new FakeAppLogger<WorkflowHostedService>());
|
||||||
|
|
||||||
|
await service.StartAsync(CancellationToken.None);
|
||||||
|
await Task.Delay(200);
|
||||||
|
await service.StopAsync(CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.True(scannerService.TriggerCount > 0);
|
||||||
|
Assert.Contains(plcService.WrittenStates, state => state.ReleasePermit);
|
||||||
|
Assert.Contains(plcService.WrittenStates, state => state.ResultCode == (ushort)WorkflowResultCode.Passed);
|
||||||
|
|
||||||
|
RuntimeSnapshot snapshot = stateStore.GetSnapshot();
|
||||||
|
Assert.Contains(snapshot.PlcMonitorItems, item => item.Name == "PcbArrived" && item.CurrentValue == "False");
|
||||||
|
Assert.Contains(snapshot.PlcMonitorItems, item => item.Name == "PcBusy" && item.CurrentValue == "False");
|
||||||
|
Assert.Contains(snapshot.PlcMonitorItems, item => item.Name == "ResultCode" && item.CurrentValue == ((ushort)WorkflowResultCode.Passed).ToString());
|
||||||
|
Assert.All(snapshot.PlcMonitorItems, item => Assert.NotEqual(default, item.LastUpdatedAt));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 当到位信号保持高电平时,即使执行软件复位,也不应被视为新的上升沿重复触发流程。
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>异步测试任务。</returns>
|
||||||
|
[Fact]
|
||||||
|
public async Task ResetAsync_ShouldNotRestartWorkflow_WhenPcbArrivedRemainsHigh()
|
||||||
|
{
|
||||||
|
var stateStore = new AppStateStore();
|
||||||
|
var plcService = new SequencedPlcService(new[]
|
||||||
|
{
|
||||||
|
new PlcSignalSnapshot { PcbArrived = true, PlcAckRelease = true },
|
||||||
|
new PlcSignalSnapshot { PcbArrived = true, PlcAckRelease = true },
|
||||||
|
new PlcSignalSnapshot { PcbArrived = true, PlcAckRelease = true },
|
||||||
|
new PlcSignalSnapshot { PcbArrived = true, PlcAckRelease = true },
|
||||||
|
new PlcSignalSnapshot { PcbArrived = true, PlcAckRelease = true }
|
||||||
|
});
|
||||||
|
var scannerService = new CountingScannerService
|
||||||
|
{
|
||||||
|
Result = new ScanOperationResult
|
||||||
|
{
|
||||||
|
IsSuccess = true,
|
||||||
|
DeviceConnected = true,
|
||||||
|
Barcode = "PCB-EDGE-001"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var service = new WorkflowHostedService(
|
||||||
|
plcService,
|
||||||
|
scannerService,
|
||||||
|
new FakeSftpLookupService
|
||||||
|
{
|
||||||
|
CheckOutcome = new SftpCheckOutcome
|
||||||
|
{
|
||||||
|
Exists = true,
|
||||||
|
ConnectionSucceeded = true,
|
||||||
|
MatchedFilePath = "/pcb/PCB-EDGE-001.txt"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new FakeAndonService(),
|
||||||
|
stateStore,
|
||||||
|
new AppConfig
|
||||||
|
{
|
||||||
|
Plc = new PlcOptions
|
||||||
|
{
|
||||||
|
PollIntervalMs = 20,
|
||||||
|
ReleaseAckTimeoutMs = 100,
|
||||||
|
ReleasePulseMs = 10
|
||||||
|
},
|
||||||
|
Scanner = new ScannerOptions
|
||||||
|
{
|
||||||
|
MaxScanAttempts = 1
|
||||||
|
},
|
||||||
|
Sftp = new SftpOptions
|
||||||
|
{
|
||||||
|
MaxRetryCount = 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new FakeAppLogger<WorkflowHostedService>());
|
||||||
|
|
||||||
|
await service.StartAsync(CancellationToken.None);
|
||||||
|
await Task.Delay(120);
|
||||||
|
await service.ResetAsync(CancellationToken.None);
|
||||||
|
await Task.Delay(120);
|
||||||
|
await service.StopAsync(CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.Equal(1, scannerService.TriggerCount);
|
||||||
|
}
|
||||||
|
|
||||||
private sealed class FakePlcService : IPlcService
|
private sealed class FakePlcService : IPlcService
|
||||||
{
|
{
|
||||||
public Task ForceReconnectAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
public Task ForceReconnectAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
|
public Task<PlcMonitorSnapshot> ReadMonitorSnapshotAsync(CancellationToken cancellationToken) => Task.FromResult(new PlcMonitorSnapshot());
|
||||||
public Task<PlcSignalSnapshot> ReadSignalsAsync(CancellationToken cancellationToken) => Task.FromResult(new PlcSignalSnapshot());
|
public Task<PlcSignalSnapshot> ReadSignalsAsync(CancellationToken cancellationToken) => Task.FromResult(new PlcSignalSnapshot());
|
||||||
public Task WriteStateAsync(PlcProcessState state, CancellationToken cancellationToken) => Task.CompletedTask;
|
public Task WriteStateAsync(PlcProcessState state, CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class ProbingPlcService : IPlcService
|
||||||
|
{
|
||||||
|
public bool ForceReconnectCalled { get; private set; }
|
||||||
|
|
||||||
|
public Task ForceReconnectAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
ForceReconnectCalled = true;
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<PlcMonitorSnapshot> ReadMonitorSnapshotAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return Task.FromResult(new PlcMonitorSnapshot
|
||||||
|
{
|
||||||
|
Inputs = new PlcSignalSnapshot
|
||||||
|
{
|
||||||
|
PcbArrived = true,
|
||||||
|
PlcReset = false,
|
||||||
|
PlcAckRelease = false,
|
||||||
|
CapturedAt = DateTimeOffset.Now
|
||||||
|
},
|
||||||
|
Outputs = new PlcProcessState
|
||||||
|
{
|
||||||
|
PcBusy = false,
|
||||||
|
ReleasePermit = false,
|
||||||
|
ResultCode = 0
|
||||||
|
},
|
||||||
|
CapturedAt = DateTimeOffset.Now
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<PlcSignalSnapshot> ReadSignalsAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return Task.FromResult(new PlcSignalSnapshot());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task WriteStateAsync(PlcProcessState state, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class FailingPlcService : IPlcService
|
||||||
|
{
|
||||||
|
public Task ForceReconnectAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("PLC unreachable");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<PlcMonitorSnapshot> ReadMonitorSnapshotAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("PLC unreachable");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<PlcSignalSnapshot> ReadSignalsAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("PLC unreachable");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task WriteStateAsync(PlcProcessState state, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class SequencedPlcService : IPlcService
|
||||||
|
{
|
||||||
|
private readonly PlcSignalSnapshot[] _signals;
|
||||||
|
private int _index;
|
||||||
|
|
||||||
|
public SequencedPlcService(PlcSignalSnapshot[] signals)
|
||||||
|
{
|
||||||
|
_signals = signals;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<PlcProcessState> WrittenStates { get; } = new();
|
||||||
|
|
||||||
|
public Task ForceReconnectAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
|
|
||||||
|
public Task<PlcMonitorSnapshot> ReadMonitorSnapshotAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
PlcSignalSnapshot current = _signals[Math.Min(Math.Max(_index - 1, 0), _signals.Length - 1)];
|
||||||
|
PlcProcessState lastWritten = WrittenStates.Count > 0 ? WrittenStates[^1] : new PlcProcessState();
|
||||||
|
return Task.FromResult(new PlcMonitorSnapshot
|
||||||
|
{
|
||||||
|
Inputs = new PlcSignalSnapshot
|
||||||
|
{
|
||||||
|
PcbArrived = current.PcbArrived,
|
||||||
|
PlcReset = current.PlcReset,
|
||||||
|
PlcAckRelease = current.PlcAckRelease,
|
||||||
|
CapturedAt = DateTimeOffset.Now
|
||||||
|
},
|
||||||
|
Outputs = lastWritten.Clone(),
|
||||||
|
CapturedAt = DateTimeOffset.Now
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<PlcSignalSnapshot> ReadSignalsAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var current = _signals[Math.Min(_index, _signals.Length - 1)];
|
||||||
|
_index++;
|
||||||
|
return Task.FromResult(current);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task WriteStateAsync(PlcProcessState state, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
WrittenStates.Add(state.Clone());
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private sealed class FakeScannerService : IScannerService
|
private sealed class FakeScannerService : IScannerService
|
||||||
{
|
{
|
||||||
public Task ForceReconnectAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
public Task ForceReconnectAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
@@ -54,11 +354,30 @@ public sealed class WorkflowHostedServiceTests
|
|||||||
public Task<ScanOperationResult> TriggerScanAsync(CancellationToken cancellationToken) => Task.FromResult(new ScanOperationResult());
|
public Task<ScanOperationResult> TriggerScanAsync(CancellationToken cancellationToken) => Task.FromResult(new ScanOperationResult());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class CountingScannerService : IScannerService
|
||||||
|
{
|
||||||
|
public int TriggerCount { get; private set; }
|
||||||
|
|
||||||
|
public ScanOperationResult Result { get; set; } = new();
|
||||||
|
|
||||||
|
public Task ForceReconnectAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
|
|
||||||
|
public Task<bool> TestConnectionAsync(CancellationToken cancellationToken) => Task.FromResult(true);
|
||||||
|
|
||||||
|
public Task<ScanOperationResult> TriggerScanAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
TriggerCount++;
|
||||||
|
return Task.FromResult(Result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private sealed class FakeSftpLookupService : ISftpLookupService
|
private sealed class FakeSftpLookupService : ISftpLookupService
|
||||||
{
|
{
|
||||||
public SftpConnectionTestOutcome TestOutcome { get; set; } = new();
|
public SftpConnectionTestOutcome TestOutcome { get; set; } = new();
|
||||||
|
|
||||||
public Task<SftpCheckOutcome> CheckFileAsync(string barcode, CancellationToken cancellationToken) => Task.FromResult(new SftpCheckOutcome());
|
public SftpCheckOutcome CheckOutcome { get; set; } = new();
|
||||||
|
|
||||||
|
public Task<SftpCheckOutcome> CheckFileAsync(string barcode, CancellationToken cancellationToken) => Task.FromResult(CheckOutcome);
|
||||||
|
|
||||||
public Task<SftpConnectionTestOutcome> TestConnectionAsync(SftpOptions? options, CancellationToken cancellationToken)
|
public Task<SftpConnectionTestOutcome> TestConnectionAsync(SftpOptions? options, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user