Browse Source

初始化

wangxichen 3 months ago
commit
6fe1478929
100 changed files with 15110 additions and 0 deletions
  1. 69 0
      .cursor/rules/confirm-before-modify.mdc
  2. 32 0
      .cursor/rules/no-documentation.mdc
  3. 18 0
      .gitignore
  4. 69 0
      .kiro/steering/confirm-before-modify.md
  5. 3 0
      .vscode/settings.json
  6. 178 0
      docs/workflow-flowchart.md
  7. 212 0
      docs/workflow-steps.md
  8. BIN
      lib/commons-codec-1.15.jar
  9. BIN
      lib/core-3.5.1.jar
  10. BIN
      lib/fastjson2-2.0.16.jar
  11. BIN
      lib/gson-2.10.jar
  12. BIN
      lib/iot-communication-1.4.4.jar
  13. BIN
      lib/jSerialComm-2.6.2.jar
  14. BIN
      lib/javase-3.5.1.jar
  15. BIN
      lib/jfreechart-1.5.4.jar
  16. BIN
      lib/jlibmodbus-1.2.9.2.jar
  17. BIN
      lib/jshortcut-0.4-oberzalek.jar
  18. BIN
      lib/kotlin-stdlib-1.8.10.jar
  19. BIN
      lib/logback-classic-1.2.13.jar
  20. BIN
      lib/logback-core-1.2.13.jar
  21. BIN
      lib/netty-all-4.1.48.Final.jar
  22. BIN
      lib/okhttp-4.10.0.jar
  23. BIN
      lib/okio-jvm-3.3.0.jar
  24. BIN
      lib/s7connector-2.1.jar
  25. BIN
      lib/slf4j-api-1.7.36.jar
  26. BIN
      lib/snakeyaml-2.0.jar
  27. BIN
      lib/sqlite-jdbc-3.36.0.3.jar
  28. 3 0
      src/META-INF/MANIFEST.MF
  29. 74 0
      src/com/mes/App.java
  30. 199 0
      src/com/mes/core/ConditionInterceptor.java
  31. 184 0
      src/com/mes/core/DebugConfigManager.java
  32. 786 0
      src/com/mes/core/StationConfig.java
  33. 683 0
      src/com/mes/core/StationContext.java
  34. 38 0
      src/com/mes/core/WorkflowCondition.java
  35. 498 0
      src/com/mes/core/WorkflowEngine.java
  36. 159 0
      src/com/mes/device/DeviceDriverFactory.java
  37. 152 0
      src/com/mes/device/IDeviceDriver.java
  38. 410 0
      src/com/mes/device/ModbusTcpDriver.java
  39. 90 0
      src/com/mes/device/NullDriver.java
  40. 218 0
      src/com/mes/device/RelayDriver.java
  41. 282 0
      src/com/mes/device/S7Driver.java
  42. 222 0
      src/com/mes/step/AbstractStep.java
  43. 126 0
      src/com/mes/step/BindMaterialStep.java
  44. 190 0
      src/com/mes/step/CheckQualityStep.java
  45. 98 0
      src/com/mes/step/CreateOrderStep.java
  46. 64 0
      src/com/mes/step/DeviceStartStep.java
  47. 197 0
      src/com/mes/step/IWorkflowStep.java
  48. 90 0
      src/com/mes/step/ReadDeviceDataStep.java
  49. 51 0
      src/com/mes/step/RelayCloseStep.java
  50. 50 0
      src/com/mes/step/RelayOpenStep.java
  51. 146 0
      src/com/mes/step/ResetDeviceCountStep.java
  52. 72 0
      src/com/mes/step/ResetStationStep.java
  53. 128 0
      src/com/mes/step/ScanMaterialStep.java
  54. 94 0
      src/com/mes/step/ScanProductStep.java
  55. 51 0
      src/com/mes/step/StartHeartbeatStep.java
  56. 115 0
      src/com/mes/step/StartWorkStep.java
  57. 157 0
      src/com/mes/step/StepFactory.java
  58. 54 0
      src/com/mes/step/StopHeartbeatStep.java
  59. 248 0
      src/com/mes/step/UploadResultStep.java
  60. 117 0
      src/com/mes/step/ValidateMaterialStep.java
  61. 320 0
      src/com/mes/step/WaitCompleteStep.java
  62. 136 0
      src/com/mes/step/WaitDeviceStartStep.java
  63. 136 0
      src/com/mes/tcp/MesClientHandler.java
  64. 183 0
      src/com/mes/tcp/MesMessageDecoder.java
  65. 456 0
      src/com/mes/tcp/MesTcpClient.java
  66. 132 0
      src/com/mes/tcp/MessageDispatcher.java
  67. 121 0
      src/com/mes/tcp/MessageSender.java
  68. 296 0
      src/com/mes/ui/LoginFrame.java
  69. 1051 0
      src/com/mes/ui/MainFrame.java
  70. 155 0
      src/com/mes/ui/UIFactory.java
  71. 118 0
      src/com/mes/ui/component/MesWebView.java
  72. 969 0
      src/com/mes/ui/component/WorkflowMonitorPanel.java
  73. 2519 0
      src/com/mes/ui/component/WorkstationPanel.java
  74. 436 0
      src/com/mes/ui/layout/ComponentResizer.java
  75. 593 0
      src/com/mes/ui/layout/DraggableComponentWrapper.java
  76. 554 0
      src/com/mes/ui/layout/UILayoutConfig.java
  77. 43 0
      src/com/mes/util/Base64Utils.java
  78. 61 0
      src/com/mes/util/DateLocalUtils.java
  79. 202 0
      src/com/mes/util/HttpUtils.java
  80. 2 0
      src/resources/image/.gitignore
  81. BIN
      src/resources/image/background.png
  82. BIN
      src/resources/image/bg/a_side.png
  83. BIN
      src/resources/image/bg/b_side.png
  84. BIN
      src/resources/image/bg/bar_add.png
  85. BIN
      src/resources/image/bg/bar_bookmark.png
  86. BIN
      src/resources/image/bg/bar_cmd_go.png
  87. BIN
      src/resources/image/bg/bar_edit.png
  88. BIN
      src/resources/image/bg/bar_pause.png
  89. BIN
      src/resources/image/bg/bar_sampling.png
  90. BIN
      src/resources/image/bg/bar_setting.png
  91. BIN
      src/resources/image/bg/bar_stop.png
  92. BIN
      src/resources/image/bg/close_bt.png
  93. BIN
      src/resources/image/bg/company_setting_logo.png
  94. BIN
      src/resources/image/bg/delete.png
  95. BIN
      src/resources/image/bg/department_setting_logo.png
  96. BIN
      src/resources/image/bg/download.png
  97. BIN
      src/resources/image/bg/equipment_setting_logo.png
  98. BIN
      src/resources/image/bg/gj_001_1_20.png
  99. BIN
      src/resources/image/bg/gj_001_21_40.png
  100. 0 0
      src/resources/image/bg/gj_001_41_60.png

+ 69 - 0
.cursor/rules/confirm-before-modify.mdc

@@ -0,0 +1,69 @@
+# 修改前确认需求规则
+
+## 核心原则
+
+**在修改任何代码之前,必须先确认用户的真实需求。**
+
+## 禁止行为
+
+- **永远不要**在未理解用户需求的情况下开始修改代码
+- **永远不要**假设用户的意图然后直接行动
+- **永远不要**一次性做大量修改而不先确认方向
+
+## 正确流程
+
+1. **先理解** - 仔细阅读用户的描述,确保理解问题
+2. **再确认** - 如果有任何不确定,先向用户确认:
+   - 用户想要达到什么效果?
+   - 现有逻辑是什么?
+   - 需要改变什么?
+3. **后执行** - 确认清楚后再进行修改
+
+## 示例
+
+❌ 错误:
+```
+用户: show_preset_count这个呢
+助手: [直接开始修改代码添加功能]
+```
+
+✅ 正确:
+```
+用户: show_preset_count这个呢
+助手: 这个配置项目前被定义了但没有实际使用。您是想让我:
+1. 让它生效来控制预设数量显示?
+2. 删掉这个冗余配置?
+3. 还是只是想了解它的含义?
+```
+
+## 特别注意
+
+当用户提问关于某个配置/功能时,可能是:
+- 询问含义(只需解释)
+- 报告问题(需要先分析)
+- 请求修改(需要先确认范围)
+
+**不要默认为"请求修改"然后直接动手!**
+
+
+## 代码修改规则
+
+### 1. 文档生成
+- **不要生成总结说明文档**,除非用户明确要求
+- 不要创建 README、CHANGELOG 等文档文件
+- 完成工作后只需简短说明即可
+
+### 2. 测试类管理
+- **不要编写大量测试类**,尽量复用现有测试类
+- 除非用户明确要求创建新的测试类
+- 优先在已有测试类中添加测试方法
+
+### 3. 修改前确认
+- **修改代码之前先展示修改方案**或提出疑问
+- 除非用户直接明确要求立即修改
+- 让用户确认方案后再执行
+
+### 4. 语言使用
+- **使用中文回答**用户的所有问题
+- 代码注释和变量名保持原有风格
+- 文档和说明使用中文

+ 32 - 0
.cursor/rules/no-documentation.mdc

@@ -0,0 +1,32 @@
+---
+description: 不要自动生成文档文件
+alwaysApply: true
+---
+
+# 文档生成规则
+
+## 禁止行为
+
+- **永远不要**自动创建 README.md、GUIDE.md、IMPLEMENTATION.md 等文档文件
+- **永远不要**在代码修改后主动生成使用说明文档
+- **永远不要**创建项目说明性的 Markdown 文件
+
+## 允许行为
+
+- 只有在用户**明确要求**创建文档时才创建
+- 可以在代码中添加必要的注释
+- 可以在响应中用文字说明修改内容,但不创建文档文件
+
+## 示例
+
+❌ 错误:修改代码后自动创建
+```
+我已经修改了代码,现在让我创建一个使用指南...
+```
+
+✅ 正确:仅说明修改内容
+```
+我已经修复了问题:
+1. 增加了边缘检测厚度到15像素
+2. 调整了手柄检测区域
+```

+ 18 - 0
.gitignore

@@ -0,0 +1,18 @@
+# 编译输出
+out/
+target/
+
+# IDEA 配置(可选,看你需不需要共享)
+.idea/
+*.iml
+
+# 运行时配置
+config/
+
+# 日志
+*.log
+logs/
+
+# 系统文件
+.DS_Store
+Thumbs.db

+ 69 - 0
.kiro/steering/confirm-before-modify.md

@@ -0,0 +1,69 @@
+# 修改前确认需求规则
+
+## 核心原则
+
+**在修改任何代码之前,必须先确认用户的真实需求。**
+
+## 禁止行为
+
+- **永远不要**在未理解用户需求的情况下开始修改代码
+- **永远不要**假设用户的意图然后直接行动
+- **永远不要**一次性做大量修改而不先确认方向
+
+## 正确流程
+
+1. **先理解** - 仔细阅读用户的描述,确保理解问题
+2. **再确认** - 如果有任何不确定,先向用户确认:
+   - 用户想要达到什么效果?
+   - 现有逻辑是什么?
+   - 需要改变什么?
+3. **后执行** - 确认清楚后再进行修改
+
+## 示例
+
+❌ 错误:
+```
+用户: show_preset_count这个呢
+助手: [直接开始修改代码添加功能]
+```
+
+✅ 正确:
+```
+用户: show_preset_count这个呢
+助手: 这个配置项目前被定义了但没有实际使用。您是想让我:
+1. 让它生效来控制预设数量显示?
+2. 删掉这个冗余配置?
+3. 还是只是想了解它的含义?
+```
+
+## 特别注意
+
+当用户提问关于某个配置/功能时,可能是:
+- 询问含义(只需解释)
+- 报告问题(需要先分析)
+- 请求修改(需要先确认范围)
+
+**不要默认为"请求修改"然后直接动手!**
+
+
+## 代码修改规则
+
+### 1. 文档生成
+- **不要生成总结说明文档**,除非用户明确要求
+- 不要创建 README、CHANGELOG 等文档文件
+- 完成工作后只需简短说明即可
+
+### 2. 测试类管理
+- **不要编写大量测试类**,尽量复用现有测试类
+- 除非用户明确要求创建新的测试类
+- 优先在已有测试类中添加测试方法
+
+### 3. 修改前确认
+- **修改代码之前先展示修改方案**或提出疑问
+- 除非用户直接明确要求立即修改
+- 让用户确认方案后再执行
+
+### 4. 语言使用
+- **使用中文回答**用户的所有问题
+- 代码注释和变量名保持原有风格
+- 文档和说明使用中文

+ 3 - 0
.vscode/settings.json

@@ -0,0 +1,3 @@
+{
+    "kiroAgent.configureMCP": "Disabled"
+}

+ 178 - 0
docs/workflow-flowchart.md

@@ -0,0 +1,178 @@
+# OP060 工位自动模式工作流程图
+
+## 当前配置
+- `submit_mode: auto`
+- `device.type: modbus_tcp`
+- `complete_condition.type: count`
+
+## 流程图
+
+```
+┌─────────────────────────────────────────────────────────────────┐
+│                        系统启动                                  │
+│  · 加载 station.yaml 配置                                       │
+│  · 连接 MES TCP 服务器 (192.168.110.99:3000)                    │
+│  · 创建 ModbusTcpDriver 连接 PLC (192.168.0.6:502)              │
+│  · 加载 debug_rules.json 持续规则                                │
+│  · 启动心跳定时器 (60s)                                          │
+└──────────────────────┬──────────────────────────────────────────┘
+                       ▼
+┌──────────────────────────────────────────────────────────────────┐
+│  步骤0: scan_product (扫描工件码)                                 │
+│  ─────────────────────────────────────────                       │
+│  前置条件: state == IDLE 或 ERROR                                 │
+│  触发方式: 用户扫码枪扫描 / 手动输入                                │
+│  执行逻辑:                                                       │
+│    · 读取工件码,36位码取前20位                                    │
+│    · 保存到 context.productSn                                    │
+│    · 状态变更: IDLE → SCANNING                                   │
+│  同步步骤,成功后立即进入下一步                                     │
+└──────────────────────┬──────────────────────────────────────────┘
+                       ▼
+┌──────────────────────────────────────────────────────────────────┐
+│  步骤1: check_quality (质量检查)                                  │
+│  ─────────────────────────────────────────                       │
+│  前置条件: productSn 不为空                                       │
+│  执行逻辑:                                                       │
+│    · 状态变更: SCANNING → CHECKING                               │
+│    · 发送 AQDW 报文到 MES 服务器                                  │
+│    · 等待服务器响应 (异步)                                        │
+│                                                                  │
+│  服务器响应处理:                                                  │
+│    · UD → 质量通过,设 qualityPassed=true                        │
+│    · OK → 已加工过,不允许重复                                    │
+│    · NG/QD/NE/LK... → 质量不合格,流程中断                       │
+│                                                                  │
+│  通过后显示: "可以加工,等待设备启动"                               │
+└──────────────────────┬──────────────────────────────────────────┘
+                       ▼
+┌──────────────────────────────────────────────────────────────────┐
+│  步骤2: wait_device_start (等待设备启动)                          │
+│  ─────────────────────────────────────────                       │
+│  前置条件: qualityPassed == true                                  │
+│  执行逻辑:                                                       │
+│    · 状态变更: CHECKING → WAITING                                │
+│    · 启动轮询线程 (WaitStart-OP060A, 间隔200ms)                  │
+│    · 轮询检查:                                                   │
+│      1. 先检查 context.deviceStarted (支持调试面板控制)            │
+│      2. 再检查 driver.isDeviceStarted() (读PLC信号)              │
+│    · 超时: 5分钟                                                 │
+│                                                                  │
+│  ⚠ 注意: 此步骤只是"等待"PLC启动信号                              │
+│    并不主动给PLC发送就绪信号(signalReady)                         │
+│                                                                  │
+│  调试模式: 手动设置 deviceStarted=true 可跳过                     │
+└──────────────────────┬──────────────────────────────────────────┘
+                       ▼
+┌──────────────────────────────────────────────────────────────────┐
+│  步骤3: wait_device_complete (等待设备完成)                        │
+│  ─────────────────────────────────────────                       │
+│  前置条件: qualityPassed == true                                  │
+│  执行逻辑:                                                       │
+│    · 状态保持: WAITING                                           │
+│    · 启动轮询线程 (DevicePoll-OP060A, 间隔500ms)                 │
+│    · 完成条件判断 (type=count):                                   │
+│      ┌─────────────────────────────────────────────┐             │
+│      │ 读取 finishedCount (Modbus地址66)            │             │
+│      │ 读取 presetCount  (Modbus地址4534)           │             │
+│      │ 判断: presetCount > 0 && finished >= preset  │             │
+│      │                                              │             │
+│      │ ConditionInterceptor 拦截:                   │             │
+│      │   如果有持续规则,Modbus读取失败后             │             │
+│      │   用规则值替换 (如 finishedCount: 0→10)       │             │
+│      └─────────────────────────────────────────────┘             │
+│    · 完成后读取:                                                  │
+│      - finishedCount → context                                   │
+│      - qualifiedCount (Modbus地址68) → context                   │
+│    · 设置 workStarted = true (作为upload_result前置条件)          │
+│    · 超时: 5分钟                                                 │
+│                                                                  │
+│  调试模式: 锁定 presetCount=10, finishedCount=10 可自动满足       │
+└──────────────────────┬──────────────────────────────────────────┘
+                       ▼
+┌──────────────────────────────────────────────────────────────────┐
+│  步骤4: upload_result (上传结果)                                  │
+│  ─────────────────────────────────────────                       │
+│  前置条件 (自动模式+有设备):                                      │
+│    · qualityPassed == true                                       │
+│    · workStarted == true (由wait_device_complete设置)            │
+│    · resultUploaded == false                                     │
+│  执行逻辑:                                                       │
+│    · 状态变更: WAITING → UPLOADING                               │
+│    · 发送 MQDW 报文到 MES 服务器                                  │
+│      (包含: 工位号、工艺号、工件码、结果OK/NG、时间)               │
+│    · 等待服务器响应 (异步)                                        │
+│                                                                  │
+│  服务器响应处理:                                                  │
+│    · OK → resultUploaded=true, 提交成功                          │
+│    · 其他 → 提交失败,提示重试                                    │
+│                                                                  │
+│  ⚠ 自动模式: OK/NG按钮始终禁用                                   │
+│    手动模式: 等待用户点击OK/NG后触发                               │
+└──────────────────────┬──────────────────────────────────────────┘
+                       ▼
+┌──────────────────────────────────────────────────────────────────┐
+│  步骤5: reset_station (重置工位)                                  │
+│  ─────────────────────────────────────────                       │
+│  前置条件: 无 (任何状态都可执行)                                   │
+│  执行逻辑:                                                       │
+│    · 调用 context.reset() 清空所有状态:                           │
+│      productSn=null, qualityPassed=false,                        │
+│      workStarted=false, resultUploaded=false,                    │
+│      deviceStarted=false, finishedCount=0, presetCount=0...     │
+│    · 状态变更: UPLOADING → IDLE                                  │
+│    · 显示: "请扫描工件码"                                         │
+│  同步步骤                                                        │
+└──────────────────────┬──────────────────────────────────────────┘
+                       ▼
+┌──────────────────────────────────────────────────────────────────┐
+│  流程完成                                                        │
+│    · 状态变更: IDLE → COMPLETED → IDLE                           │
+│    · 再次 context.reset()                                        │
+│    · 显示: "结果已提交,请扫下一件"                                │
+│    · 步骤索引重置为0,等待下一次扫码                               │
+│                                                                  │
+│  ⚠ 持续规则(ConditionInterceptor)不会被reset清除                 │
+│    下一轮流程中拦截器仍然生效                                     │
+└──────────────────────┬──────────────────────────────────────────┘
+                       │
+                       └──────→ 回到步骤0,等待扫码
+```
+
+## 关键变量流转
+
+```
+步骤                    | productSn | qualityPassed | deviceStarted | workStarted | resultUploaded
+─────────────────────── | --------- | ------------- | ------------- | ----------- | --------------
+scan_product 完成后     | ✅ 有值    | false         | false         | false       | false
+check_quality 通过后    | ✅ 有值    | ✅ true        | false         | false       | false
+wait_device_start 完成后| ✅ 有值    | ✅ true        | ✅ true        | false       | false
+wait_device_complete后  | ✅ 有值    | ✅ true        | ✅ true        | ✅ true      | false
+upload_result 成功后    | ✅ 有值    | ✅ true        | ✅ true        | ✅ true      | ✅ true
+reset_station 后        | null      | false         | false         | false       | false
+```
+
+## 通信时序
+
+```
+客户端                          MES服务器                    PLC (Modbus)
+  │                                │                           │
+  │──── 心跳 AXTW (每60s) ───────→│                           │
+  │                                │                           │
+  │──── AQDW (质量查询) ─────────→│                           │
+  │←─── AQDW 响应 (UD=可加工) ────│                           │
+  │                                │                           │
+  │                                │    轮询 isDeviceStarted() │
+  │                                │  ←────────────────────────│
+  │                                │                           │
+  │                                │    轮询 finishedCount     │
+  │                                │  ←────────────────────────│
+  │                                │    轮询 presetCount       │
+  │                                │  ←────────────────────────│
+  │                                │    轮询 qualifiedCount    │
+  │                                │  ←────────────────────────│
+  │                                │                           │
+  │──── MQDW (结果上传) ─────────→│                           │
+  │←─── MQDW 响应 (OK) ──────────│                           │
+  │                                │                           │
+```

+ 212 - 0
docs/workflow-steps.md

@@ -0,0 +1,212 @@
+# MES工位系统 - 工作流步骤说明
+
+## 概述
+
+工作流由可组装的步骤(Step)组成,通过 `station.yaml` 配置文件定义执行顺序。每个步骤独立、可插拔,方便根据不同工位需求灵活组合。
+
+## 步骤清单
+
+| 步骤ID | 名称 | 说明 |
+|--------|------|------|
+| `scan_product` | 扫描产品 | 扫描产品条码,获取产品信息 |
+| `check_quality` | 质量检查 | 向MES服务器查询产品是否允许加工 |
+| `relay_open` | 开继电器 | 打开继电器,控制设备通电/启动 |
+| `relay_close` | 关继电器 | 关闭继电器,控制设备断电/停止 |
+| `device_start` | 启动设备 | 向PLC发送启动/就绪信号 |
+| `start_heartbeat` | 启动心跳 | 启动与设备的心跳通信 |
+| `stop_heartbeat` | 停止心跳 | 停止心跳通信 |
+| `wait_device_start` | 等待设备启动 | 等待设备发出启动信号(如CNC启动) |
+| `wait_device_complete` | 等待设备完成 | 等待设备加工完成 |
+| `upload_result` | 上传结果 | 将加工结果上传到MES服务器 |
+| `reset_station` | 重置工位 | 清空工位状态,准备下一轮 |
+
+## 步骤详细说明
+
+### scan_product - 扫描产品
+- **功能**:接收扫码枪输入,解析产品条码
+- **前置条件**:无
+- **输出**:设置 `context.productSn`
+
+### check_quality - 质量检查
+- **功能**:调用MES接口检查产品是否允许在本工位加工
+- **前置条件**:已扫描产品
+- **输出**:设置 `context.qualityPassed`
+
+### relay_open - 开继电器
+- **功能**:通过Modbus RTU发送开继电器命令
+- **前置条件**:质量检查通过
+- **配置依赖**:`device.relay` 配置
+- **可跳过**:未配置继电器时自动跳过
+
+### relay_close - 关继电器
+- **功能**:通过Modbus RTU发送关继电器命令
+- **前置条件**:无(任何时候都可执行)
+- **配置依赖**:`device.relay` 配置
+- **可跳过**:未配置继电器时自动跳过
+
+### device_start - 启动设备
+- **功能**:向PLC发送就绪/启动信号
+- **前置条件**:质量检查通过
+- **配置依赖**:`device.addresses.start_command`
+- **可跳过**:设备未启用时自动跳过
+
+### start_heartbeat - 启动心跳
+- **功能**:启动定时心跳,保持与设备的通信
+- **前置条件**:质量检查通过
+- **配置依赖**:`device.heartbeat` 配置
+- **可跳过**:设备未启用时自动跳过
+
+### stop_heartbeat - 停止心跳
+- **功能**:停止心跳定时器
+- **前置条件**:无
+- **可跳过**:心跳未启动时自动跳过
+
+### wait_device_start - 等待设备启动
+- **功能**:轮询检测设备启动信号
+- **前置条件**:质量检查通过
+- **配置依赖**:`device.addresses.start_signal`
+- **输出**:设置 `context.deviceStarted` 和 `context.deviceStartTime`
+- **可跳过**:设备未启用时自动跳过
+
+### wait_device_complete - 等待设备完成
+- **功能**:轮询检测设备加工完成
+- **前置条件**:质量检查通过
+- **完成条件类型**(通过 `device.complete_condition.type` 配置):
+  - `signal`:检测完成信号
+  - `count`:检测完成数量达到预设值
+  - `signal_with_time`:检测设备停止 + 满足最小运行时间
+- **可跳过**:设备未启用时自动跳过
+
+### upload_result - 上传结果
+- **功能**:将加工结果(OK/NG)上传到MES服务器
+- **前置条件**:
+  - 手动模式:用户点击OK/NG按钮
+  - 自动模式:设备完成信号触发
+- **配置依赖**:`workflow.submit_mode`
+
+### reset_station - 重置工位
+- **功能**:清空所有状态,准备下一个产品
+- **前置条件**:结果已上传
+
+## 配置示例
+
+### 示例1:标准S7工位(等完成信号)
+```yaml
+workflow:
+  submit_mode: auto
+  steps:
+    - id: scan_product
+    - id: check_quality
+    - id: device_start
+    - id: start_heartbeat
+    - id: wait_device_complete
+    - id: stop_heartbeat
+    - id: upload_result
+    - id: reset_station
+
+device:
+  enabled: true
+  type: s7
+  complete_condition:
+    type: signal
+```
+
+### 示例2:金玉CNC工位(Modbus TCP + 时间条件完成)
+```yaml
+workflow:
+  submit_mode: auto
+  steps:
+    - id: scan_product
+    - id: check_quality
+    - id: wait_device_start
+    - id: wait_device_complete
+    - id: upload_result
+    - id: reset_station
+
+device:
+  enabled: true
+  type: modbus_tcp
+  complete_condition:
+    type: signal_with_time
+    running_value: 1
+    stopped_value: 0
+    min_running_time: 3000
+```
+
+### 示例3:继电器控制工位(手动提交)
+```yaml
+workflow:
+  submit_mode: manual
+  steps:
+    - id: scan_product
+    - id: check_quality
+    - id: relay_open
+    - id: upload_result
+    - id: relay_close
+    - id: reset_station
+
+device:
+  enabled: false
+  relay:
+    enabled: true
+    type: modbus_rtu
+    port: COM2
+    baudrate: 38400
+```
+
+### 示例4:拉铆工位(继电器+心跳+数量完成)
+```yaml
+workflow:
+  submit_mode: auto
+  steps:
+    - id: scan_product
+    - id: check_quality
+    - id: relay_open
+    - id: start_heartbeat
+    - id: wait_device_complete
+    - id: stop_heartbeat
+    - id: upload_result
+    - id: relay_close
+    - id: reset_station
+
+device:
+  enabled: true
+  type: modbus_tcp
+  complete_condition:
+    type: count
+  relay:
+    enabled: true
+    type: modbus_rtu
+    port: COM2
+```
+
+## 设备配置说明
+
+### device.type - 设备通信协议
+- `s7`:西门子S7协议
+- `modbus_tcp`:Modbus TCP协议
+- `modbus_rtu`:Modbus RTU协议(串口)
+
+### device.complete_condition.type - 完成条件类型
+- `signal`:通过完成信号判断(读取 `complete_signal` 地址)
+- `count`:通过完成数量判断(`finished_count >= preset_count`)
+- `signal_with_time`:信号+时间判断(设备停止且运行时间 >= `min_running_time`)
+
+### device.addresses - 地址配置
+```yaml
+addresses:
+  enable_signal: "DB9.18.1"      # S7格式:允许信号
+  start_signal: "DB1.2.2"        # S7格式:启动信号
+  complete_signal: "DB1.2.5"     # S7格式:完成信号
+  device_status: 15001           # Modbus格式:设备状态
+  finished_count: 4162           # 完成数量寄存器
+  preset_count: 4164             # 预设数量寄存器
+  start_command: 8               # 启动命令寄存器
+```
+
+## 扩展新步骤
+
+1. 创建新的Step类,继承 `AbstractStep`
+2. 实现 `execute()` 方法
+3. 在 `StepFactory.registerDefaultSteps()` 中注册
+4. 在配置文件中使用新步骤ID

BIN
lib/commons-codec-1.15.jar


BIN
lib/core-3.5.1.jar


BIN
lib/fastjson2-2.0.16.jar


BIN
lib/gson-2.10.jar


BIN
lib/iot-communication-1.4.4.jar


BIN
lib/jSerialComm-2.6.2.jar


BIN
lib/javase-3.5.1.jar


BIN
lib/jfreechart-1.5.4.jar


BIN
lib/jlibmodbus-1.2.9.2.jar


BIN
lib/jshortcut-0.4-oberzalek.jar


BIN
lib/kotlin-stdlib-1.8.10.jar


BIN
lib/logback-classic-1.2.13.jar


BIN
lib/logback-core-1.2.13.jar


BIN
lib/netty-all-4.1.48.Final.jar


BIN
lib/okhttp-4.10.0.jar


BIN
lib/okio-jvm-3.3.0.jar


BIN
lib/s7connector-2.1.jar


BIN
lib/slf4j-api-1.7.36.jar


BIN
lib/snakeyaml-2.0.jar


BIN
lib/sqlite-jdbc-3.36.0.3.jar


+ 3 - 0
src/META-INF/MANIFEST.MF

@@ -0,0 +1,3 @@
+Manifest-Version: 1.0
+Main-Class: com.mes.App
+

+ 74 - 0
src/com/mes/App.java

@@ -0,0 +1,74 @@
+package com.mes;
+
+import com.mes.core.StationConfig;
+import com.mes.ui.LoginFrame;
+import com.mes.ui.MainFrame;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.swing.*;
+import java.awt.*;
+
+/**
+ * 通用MES客户端启动入口
+ */
+public class App {
+    private static final Logger log = LoggerFactory.getLogger(App.class);
+
+    private static MainFrame mainFrame;
+
+    public static void main(String[] args) {
+        log.info("========================================");
+        log.info("通用MES客户端启动");
+        log.info("========================================");
+
+        // 加载配置
+        StationConfig config;
+        try {
+            config = StationConfig.getInstance();
+            log.info("配置加载成功:");
+            log.info("  - 模式: {}", config.getMode());
+            log.info("  - 工位数: {}", config.getStationCount());
+            for (int i = 0; i < config.getStationCount(); i++) {
+                StationConfig.StationInfo station = config.getStation(i);
+                log.info("  - 工位{}: {}", i + 1, station.getCode());
+            }
+            log.info("  - 服务器: {}:{}", config.getServerIp(), config.getTcpPort());
+            log.info("  - 设备: {} ({})", config.isDeviceEnabled() ? "启用" : "禁用", config.getDeviceType());
+        } catch (Exception e) {
+            log.error("配置加载失败: {}", e.getMessage(), e);
+            JOptionPane.showMessageDialog(null,
+                    "配置文件加载失败: " + e.getMessage(),
+                    "错误",
+                    JOptionPane.ERROR_MESSAGE);
+            return;
+        }
+
+        // 启动UI
+        EventQueue.invokeLater(() -> {
+            try {
+                // 创建主窗口
+                mainFrame = new MainFrame(config);
+                mainFrame.setVisible(false);
+
+                // 显示登录窗口
+                LoginFrame loginFrame = new LoginFrame(mainFrame);
+                loginFrame.setVisible(true);
+
+            } catch (Exception e) {
+                log.error("启动失败: {}", e.getMessage(), e);
+                JOptionPane.showMessageDialog(null,
+                        "程序启动失败: " + e.getMessage(),
+                        "错误",
+                        JOptionPane.ERROR_MESSAGE);
+            }
+        });
+    }
+
+    /**
+     * 获取主窗口
+     */
+    public static MainFrame getMainFrame() {
+        return mainFrame;
+    }
+}

+ 199 - 0
src/com/mes/core/ConditionInterceptor.java

@@ -0,0 +1,199 @@
+package com.mes.core;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.lang.reflect.Field;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 条件拦截器 - 拦截条件变量的设置,支持持续控制
+ * 
+ * 功能:
+ * 1. 注册持续控制规则(某个变量持续保持某个值)
+ * 2. 在 setter 被调用时检查是否有拦截规则
+ * 3. 如果有规则,则强制使用规则指定的值
+ */
+public class ConditionInterceptor {
+    private static final Logger log = LoggerFactory.getLogger(ConditionInterceptor.class);
+    
+    // 持续控制规则: fieldName -> 强制值
+    private final Map<String, Object> persistentRules = new ConcurrentHashMap<>();
+    
+    // 拦截监听器
+    private InterceptListener listener;
+    
+    /**
+     * 拦截监听器接口
+     */
+    public interface InterceptListener {
+        void onIntercept(String fieldName, Object originalValue, Object forcedValue);
+    }
+    
+    /**
+     * 添加持续控制规则
+     * @param fieldName 字段名
+     * @param value 强制值
+     */
+    public void addRule(String fieldName, Object value) {
+        persistentRules.put(fieldName, value);
+        log.info("[ConditionInterceptor] 添加规则: {} = {}", fieldName, value);
+    }
+    
+    /**
+     * 移除持续控制规则
+     * @param fieldName 字段名
+     */
+    public void removeRule(String fieldName) {
+        persistentRules.remove(fieldName);
+        log.info("[ConditionInterceptor] 移除规则: {}", fieldName);
+    }
+    
+    /**
+     * 清空所有规则
+     */
+    public void clearRules() {
+        persistentRules.clear();
+        log.info("[ConditionInterceptor] 清空所有规则");
+    }
+    
+    /**
+     * 检查是否有规则
+     */
+    public boolean hasRule(String fieldName) {
+        return persistentRules.containsKey(fieldName);
+    }
+    
+    /**
+     * 获取规则值
+     */
+    public Object getRuleValue(String fieldName) {
+        return persistentRules.get(fieldName);
+    }
+    
+    /**
+     * 获取所有规则
+     */
+    public Map<String, Object> getAllRules() {
+        return Collections.unmodifiableMap(persistentRules);
+    }
+    
+    /**
+     * 拦截并返回最终值
+     * 如果有持续控制规则,返回规则值;否则返回原始值
+     * 
+     * @param fieldName 字段名
+     * @param originalValue 原始值
+     * @return 最终值
+     */
+    @SuppressWarnings("unchecked")
+    public <T> T intercept(String fieldName, T originalValue) {
+        if (persistentRules.containsKey(fieldName)) {
+            Object forcedValue = persistentRules.get(fieldName);
+            if (listener != null) {
+                listener.onIntercept(fieldName, originalValue, forcedValue);
+            }
+            log.debug("[ConditionInterceptor] 拦截 {}: {} -> {}", fieldName, originalValue, forcedValue);
+            return (T) forcedValue;
+        }
+        return originalValue;
+    }
+    
+    /**
+     * 设置监听器
+     */
+    public void setListener(InterceptListener listener) {
+        this.listener = listener;
+    }
+    
+    // ========== 条件元数据扫描 ==========
+    
+    /**
+     * 条件元数据
+     */
+    public static class ConditionMeta {
+        private final String fieldName;
+        private final String name;
+        private final String description;
+        private final String group;
+        private final String[] affectedSteps;
+        private final Class<?> type;
+        private final boolean controllable;
+        
+        public ConditionMeta(Field field, WorkflowCondition annotation) {
+            this.fieldName = field.getName();
+            this.name = annotation.name();
+            this.description = annotation.description();
+            this.group = annotation.group();
+            this.affectedSteps = annotation.affectedSteps();
+            this.type = field.getType();
+            this.controllable = annotation.controllable();
+        }
+        
+        public String getFieldName() { return fieldName; }
+        public String getName() { return name; }
+        public String getDescription() { return description; }
+        public String getGroup() { return group; }
+        public String[] getAffectedSteps() { return affectedSteps; }
+        public Class<?> getType() { return type; }
+        public boolean isControllable() { return controllable; }
+        
+        public boolean isBoolean() { 
+            return type == boolean.class || type == Boolean.class; 
+        }
+        
+        public boolean isNumeric() {
+            return type == int.class || type == Integer.class ||
+                   type == long.class || type == Long.class ||
+                   type == double.class || type == Double.class;
+        }
+    }
+    
+    /**
+     * 扫描类中所有带 @WorkflowCondition 注解的字段
+     */
+    public static List<ConditionMeta> scanConditions(Class<?> clazz) {
+        List<ConditionMeta> conditions = new ArrayList<>();
+        
+        for (Field field : clazz.getDeclaredFields()) {
+            WorkflowCondition annotation = field.getAnnotation(WorkflowCondition.class);
+            if (annotation != null) {
+                conditions.add(new ConditionMeta(field, annotation));
+            }
+        }
+        
+        // 按分组排序
+        conditions.sort(Comparator.comparing(ConditionMeta::getGroup)
+                .thenComparing(ConditionMeta::getName));
+        
+        return conditions;
+    }
+    
+    /**
+     * 通过反射获取字段值
+     */
+    public static Object getFieldValue(Object obj, String fieldName) {
+        try {
+            Field field = obj.getClass().getDeclaredField(fieldName);
+            field.setAccessible(true);
+            return field.get(obj);
+        } catch (Exception e) {
+            log.error("获取字段值失败: {}", fieldName, e);
+            return null;
+        }
+    }
+    
+    /**
+     * 通过反射设置字段值
+     */
+    public static void setFieldValue(Object obj, String fieldName, Object value) {
+        try {
+            Field field = obj.getClass().getDeclaredField(fieldName);
+            field.setAccessible(true);
+            field.set(obj, value);
+        } catch (Exception e) {
+            log.error("设置字段值失败: {}", fieldName, e);
+        }
+    }
+}

+ 184 - 0
src/com/mes/core/DebugConfigManager.java

@@ -0,0 +1,184 @@
+package com.mes.core;
+
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONObject;
+import com.alibaba.fastjson2.JSONWriter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.*;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.*;
+import java.util.*;
+
+/**
+ * 调试配置管理器 - 持久化调试规则到文件
+ */
+public class DebugConfigManager {
+    private static final Logger log = LoggerFactory.getLogger(DebugConfigManager.class);
+    
+    private static final String CONFIG_DIR = "config";
+    private static final String CONFIG_FILE = "debug_rules.json";
+    
+    private final Path configPath;
+    private final Map<String, StationDebugConfig> stationConfigs = new HashMap<>();
+    
+    /**
+     * 单个工位的调试配置
+     */
+    public static class StationDebugConfig {
+        private String stationCode;
+        private Map<String, Object> persistentRules = new HashMap<>();
+        private boolean enabled = false;
+        
+        public StationDebugConfig() {}
+        
+        public StationDebugConfig(String stationCode) {
+            this.stationCode = stationCode;
+        }
+        
+        public String getStationCode() { return stationCode; }
+        public void setStationCode(String stationCode) { this.stationCode = stationCode; }
+        
+        public Map<String, Object> getPersistentRules() { return persistentRules; }
+        public void setPersistentRules(Map<String, Object> rules) { this.persistentRules = rules; }
+        
+        public boolean isEnabled() { return enabled; }
+        public void setEnabled(boolean enabled) { this.enabled = enabled; }
+        
+        public void addRule(String fieldName, Object value) {
+            persistentRules.put(fieldName, value);
+        }
+        
+        public void removeRule(String fieldName) {
+            persistentRules.remove(fieldName);
+        }
+    }
+    
+    public DebugConfigManager() {
+        // 配置文件路径:当前目录/config/debug_rules.json
+        this.configPath = Paths.get(CONFIG_DIR, CONFIG_FILE);
+        load();
+    }
+    
+    public DebugConfigManager(String configDir) {
+        this.configPath = Paths.get(configDir, CONFIG_FILE);
+        load();
+    }
+    
+    /**
+     * 加载配置
+     */
+    public void load() {
+        if (!Files.exists(configPath)) {
+            log.info("[DebugConfig] 配置文件不存在,使用默认配置: {}", configPath);
+            return;
+        }
+        
+        try {
+            String content = new String(Files.readAllBytes(configPath), StandardCharsets.UTF_8);
+            JSONObject json = JSON.parseObject(content);
+            
+            if (json != null && json.containsKey("stations")) {
+                JSONObject stations = json.getJSONObject("stations");
+                for (String stationCode : stations.keySet()) {
+                    JSONObject stationJson = stations.getJSONObject(stationCode);
+                    StationDebugConfig config = new StationDebugConfig(stationCode);
+                    config.setEnabled(stationJson.getBooleanValue("enabled"));
+                    
+                    JSONObject rules = stationJson.getJSONObject("rules");
+                    if (rules != null) {
+                        for (String fieldName : rules.keySet()) {
+                            config.addRule(fieldName, rules.get(fieldName));
+                        }
+                    }
+                    
+                    stationConfigs.put(stationCode, config);
+                }
+            }
+            
+            log.info("[DebugConfig] 加载配置成功,共 {} 个工位配置", stationConfigs.size());
+            
+        } catch (Exception e) {
+            log.error("[DebugConfig] 加载配置失败: {}", e.getMessage(), e);
+        }
+    }
+    
+    /**
+     * 保存配置
+     */
+    public void save() {
+        try {
+            // 确保目录存在
+            Files.createDirectories(configPath.getParent());
+            
+            JSONObject root = new JSONObject();
+            JSONObject stations = new JSONObject();
+            
+            for (Map.Entry<String, StationDebugConfig> entry : stationConfigs.entrySet()) {
+                StationDebugConfig config = entry.getValue();
+                JSONObject stationJson = new JSONObject();
+                stationJson.put("enabled", config.isEnabled());
+                stationJson.put("rules", config.getPersistentRules());
+                stations.put(entry.getKey(), stationJson);
+            }
+            
+            root.put("stations", stations);
+            root.put("lastModified", System.currentTimeMillis());
+            
+            String content = JSON.toJSONString(root, JSONWriter.Feature.PrettyFormat);
+            Files.write(configPath, content.getBytes(StandardCharsets.UTF_8));
+            
+            log.info("[DebugConfig] 保存配置成功: {}", configPath);
+            
+        } catch (Exception e) {
+            log.error("[DebugConfig] 保存配置失败: {}", e.getMessage(), e);
+        }
+    }
+    
+    /**
+     * 获取工位配置
+     */
+    public StationDebugConfig getStationConfig(String stationCode) {
+        return stationConfigs.computeIfAbsent(stationCode, StationDebugConfig::new);
+    }
+    
+    /**
+     * 添加规则
+     */
+    public void addRule(String stationCode, String fieldName, Object value) {
+        getStationConfig(stationCode).addRule(fieldName, value);
+        save();
+    }
+    
+    /**
+     * 移除规则
+     */
+    public void removeRule(String stationCode, String fieldName) {
+        getStationConfig(stationCode).removeRule(fieldName);
+        save();
+    }
+    
+    /**
+     * 设置工位调试启用状态
+     */
+    public void setEnabled(String stationCode, boolean enabled) {
+        getStationConfig(stationCode).setEnabled(enabled);
+        save();
+    }
+    
+    /**
+     * 应用配置到拦截器
+     */
+    public void applyToInterceptor(String stationCode, ConditionInterceptor interceptor) {
+        StationDebugConfig config = stationConfigs.get(stationCode);
+        if (config != null && config.isEnabled()) {
+            interceptor.clearRules();
+            for (Map.Entry<String, Object> rule : config.getPersistentRules().entrySet()) {
+                interceptor.addRule(rule.getKey(), rule.getValue());
+            }
+            log.info("[DebugConfig] 应用 {} 条规则到工位 {}", 
+                    config.getPersistentRules().size(), stationCode);
+        }
+    }
+}

+ 786 - 0
src/com/mes/core/StationConfig.java

@@ -0,0 +1,786 @@
+package com.mes.core;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.yaml.snakeyaml.Yaml;
+
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 工位配置类 - 从YAML配置文件加载工位配置
+ * 解决工位号硬编码问题,支持单工位/双工位动态切换
+ */
+public class StationConfig {
+    private static final Logger log = LoggerFactory.getLogger(StationConfig.class);
+
+    // 单例实例
+    private static StationConfig instance;
+
+    // ========== 工位配置 ==========
+    private String mode;                    // single | dual
+    private String stationName;             // 工位/工序名称(如"液冷板安装+激光点固")
+    private List<StationInfo> stations;     // 工位列表
+    private String lineSn;                  // 线体号
+
+    // ========== 服务器配置 ==========
+    private String serverIp;
+    private int tcpPort;
+    private int httpPort;                // HTTP服务端口
+    private int heartBeatCycle;
+
+    // ========== 流程配置 ==========
+    private List<StepConfig> workflowSteps;
+    private String submitMode;               // manual | auto (手动点击OK/NG | 自动提交)
+
+    // ========== 设备配置 ==========
+    private boolean deviceEnabled;
+    private String deviceType;              // modbus_tcp | modbus_rtu | s7 | none
+    private List<DeviceConnection> deviceConnections;
+    private boolean timeoutEnabled;
+    private int connectTimeout = 3000;      // Modbus连接超时(ms)
+    private int heartbeatAddress;
+    private int readySignalAddress;
+    
+    // 设备地址配置
+    private DeviceAddresses deviceAddresses;
+    // 完成条件配置
+    private CompleteCondition completeCondition;
+    // 心跳配置
+    private HeartbeatConfig heartbeatConfig;
+    // 继电器配置
+    private RelayConfig relayConfig;
+
+    // ========== UI配置 ==========
+    private boolean showDeviceInfo;
+    private boolean showDeviceInfo2;        // 是否显示第二行设备信息
+    private boolean showMaterialInput;
+    private String materialLabel;
+    private List<DeviceInfoRow> deviceInfoRows;  // 设备信息行配置
+
+    // 原始配置Map,用于获取未解析的配置
+    private Map<String, Object> rawConfig;
+
+    private StationConfig() {
+        stations = new ArrayList<>();
+        workflowSteps = new ArrayList<>();
+        deviceConnections = new ArrayList<>();
+        deviceInfoRows = new ArrayList<>();
+    }
+
+    /**
+     * 获取单例实例
+     */
+    public static synchronized StationConfig getInstance() {
+        if (instance == null) {
+            instance = new StationConfig();
+            instance.loadFromResource("resources/config/station.yaml");
+        }
+        return instance;
+    }
+
+    /**
+     * 重新加载配置(用于热更新)
+     */
+    public static synchronized void reload() {
+        instance = new StationConfig();
+        instance.loadFromResource("resources/config/station.yaml");
+    }
+
+    /**
+     * 从指定路径加载配置
+     */
+    public static synchronized StationConfig loadFrom(String resourcePath) {
+        instance = new StationConfig();
+        instance.loadFromResource(resourcePath);
+        return instance;
+    }
+
+    /**
+     * 从资源文件加载配置
+     */
+    private void loadFromResource(String resourcePath) {
+        try (InputStream is = getClass().getClassLoader().getResourceAsStream(resourcePath)) {
+            if (is == null) {
+                log.error("配置文件不存在: {}", resourcePath);
+                throw new RuntimeException("配置文件不存在: " + resourcePath);
+            }
+
+            Yaml yaml = new Yaml();
+            Map<String, Object> config = yaml.load(new InputStreamReader(is, StandardCharsets.UTF_8));
+            parseConfig(config);
+            log.info("配置加载成功: mode={}, stations={}", mode, stations.size());
+
+        } catch (Exception e) {
+            log.error("加载配置文件失败: {}", e.getMessage(), e);
+            throw new RuntimeException("加载配置文件失败", e);
+        }
+    }
+
+    /**
+     * 解析YAML配置
+     */
+    @SuppressWarnings("unchecked")
+    private void parseConfig(Map<String, Object> config) {
+        rawConfig = config;
+
+        // 解析工位配置
+        Map<String, Object> stationObj = (Map<String, Object>) config.get("station");
+        if (stationObj != null) {
+            mode = (String) stationObj.get("mode");
+            stationName = (String) stationObj.get("name");
+            lineSn = (String) stationObj.get("line_sn");
+
+            List<Map<String, Object>> stationsArr = (List<Map<String, Object>>) stationObj.get("stations");
+            if (stationsArr != null) {
+                for (int i = 0; i < stationsArr.size(); i++) {
+                    Map<String, Object> s = stationsArr.get(i);
+                    stations.add(new StationInfo(
+                            i,
+                            (String) s.get("code")
+                    ));
+                }
+            }
+        }
+
+        // 解析服务器配置
+        Map<String, Object> serverObj = (Map<String, Object>) config.get("server");
+        if (serverObj != null) {
+            serverIp = (String) serverObj.get("ip");
+            tcpPort = getIntValue(serverObj, "tcp_port", 3000);
+            httpPort = getIntValue(serverObj, "http_port", 8980);
+            heartBeatCycle = getIntValue(serverObj, "heart_beat_cycle", 60);
+        }
+
+        // 解析流程配置
+        Map<String, Object> workflowObj = (Map<String, Object>) config.get("workflow");
+        if (workflowObj != null) {
+            // 提交模式:manual(手动点击OK/NG) | auto(设备信号自动提交)
+            submitMode = (String) workflowObj.get("submit_mode");
+            if (submitMode == null || submitMode.isEmpty()) {
+                submitMode = "manual";  // 默认手动模式
+            }
+            
+            List<Map<String, Object>> stepsArr = (List<Map<String, Object>>) workflowObj.get("steps");
+            if (stepsArr != null) {
+                for (Map<String, Object> step : stepsArr) {
+                    StepConfig stepConfig = new StepConfig();
+                    stepConfig.setId((String) step.get("id"));
+                    stepConfig.setName((String) step.get("name"));
+                    stepConfig.setCraft((String) step.get("craft"));
+
+                    // 解析步骤特定配置
+                    Map<String, Object> stepConfigMap = (Map<String, Object>) step.get("config");
+                    if (stepConfigMap != null) {
+                        stepConfig.setConfig(stepConfigMap);
+                    }
+
+                    workflowSteps.add(stepConfig);
+                }
+            }
+        }
+
+        // 解析设备配置
+        Map<String, Object> deviceObj = (Map<String, Object>) config.get("device");
+        if (deviceObj != null) {
+            deviceEnabled = getBooleanValue(deviceObj, "enabled", false);
+            deviceType = (String) deviceObj.get("type");
+            timeoutEnabled = getBooleanValue(deviceObj, "timeout_enabled", false);
+            connectTimeout = getIntValue(deviceObj, "connect_timeout", 3000);
+            heartbeatAddress = getIntValue(deviceObj, "heartbeat_address", 4160);
+            readySignalAddress = getIntValue(deviceObj, "ready_signal_address", 8);
+
+            List<Map<String, Object>> connArr = (List<Map<String, Object>>) deviceObj.get("connections");
+            if (connArr != null) {
+                for (int i = 0; i < connArr.size(); i++) {
+                    Map<String, Object> conn = connArr.get(i);
+                    deviceConnections.add(new DeviceConnection(
+                            getIntValue(conn, "station_index", i),
+                            (String) conn.get("ip"),
+                            getIntValue(conn, "port", 502)
+                    ));
+                }
+            }
+            
+            // 解析设备地址配置
+            Map<String, Object> addressesObj = (Map<String, Object>) deviceObj.get("addresses");
+            if (addressesObj != null) {
+                deviceAddresses = new DeviceAddresses();
+                deviceAddresses.setEnableSignal((String) addressesObj.get("enable_signal"));
+                deviceAddresses.setStartSignal((String) addressesObj.get("start_signal"));
+                deviceAddresses.setCompleteSignal((String) addressesObj.get("complete_signal"));
+                deviceAddresses.setDeviceStatus((String) addressesObj.get("device_status"));
+                deviceAddresses.setFinishedCount(getIntValue(addressesObj, "finished_count", 0));
+                deviceAddresses.setPresetCount(getIntValue(addressesObj, "preset_count", 0));
+                deviceAddresses.setQualifiedCount(getIntValue(addressesObj, "qualified_count", 0));
+                deviceAddresses.setStartCommand(getIntValue(addressesObj, "start_command", 0));
+            }
+            
+            // 解析完成条件配置
+            Map<String, Object> conditionObj = (Map<String, Object>) deviceObj.get("complete_condition");
+            if (conditionObj != null) {
+                completeCondition = new CompleteCondition();
+                completeCondition.setType((String) conditionObj.get("type"));
+                completeCondition.setRunningValue(getIntValue(conditionObj, "running_value", 1));
+                completeCondition.setStoppedValue(getIntValue(conditionObj, "stopped_value", 0));
+                completeCondition.setMinRunningTime(getIntValue(conditionObj, "min_running_time", 0));
+            } else {
+                // 默认使用信号判断
+                completeCondition = new CompleteCondition();
+                completeCondition.setType("signal");
+            }
+            
+            // 解析心跳配置
+            Map<String, Object> heartbeatObj = (Map<String, Object>) deviceObj.get("heartbeat");
+            if (heartbeatObj != null) {
+                heartbeatConfig = new HeartbeatConfig();
+                heartbeatConfig.setEnabled(getBooleanValue(heartbeatObj, "enabled", true));
+                heartbeatConfig.setAddress(getIntValue(heartbeatObj, "address", 4160));
+                heartbeatConfig.setInterval(getIntValue(heartbeatObj, "interval", 500));
+            }
+            
+            // 解析继电器配置
+            Map<String, Object> relayObj = (Map<String, Object>) deviceObj.get("relay");
+            if (relayObj != null) {
+                relayConfig = new RelayConfig();
+                relayConfig.setEnabled(getBooleanValue(relayObj, "enabled", true));
+                relayConfig.setType((String) relayObj.get("type"));
+                relayConfig.setPort((String) relayObj.get("port"));
+                relayConfig.setBaudrate(getIntValue(relayObj, "baudrate", 38400));
+                relayConfig.setOpenCommand((String) relayObj.get("open_command"));
+                relayConfig.setCloseCommand((String) relayObj.get("close_command"));
+            }
+        }
+
+        // 解析UI配置
+        Map<String, Object> uiObj = (Map<String, Object>) config.get("ui");
+        if (uiObj != null) {
+            showMaterialInput = getBooleanValue(uiObj, "show_material_input", false);
+            materialLabel = (String) uiObj.get("material_label");
+
+            // 解析设备信息行配置
+            List<Map<String, Object>> rowsArr = (List<Map<String, Object>>) uiObj.get("device_info_rows");
+            if (rowsArr != null && !rowsArr.isEmpty()) {
+                for (int i = 0; i < Math.min(rowsArr.size(), 2); i++) {
+                    Map<String, Object> row = rowsArr.get(i);
+                    DeviceInfoRow infoRow = new DeviceInfoRow();
+                    infoRow.setLabel((String) row.get("label"));
+                    infoRow.setConnectionIndex(getIntValue(row, "connection_index", 0));
+                    infoRow.setPresetAddress(getIntValue(row, "preset_address", 0));
+                    infoRow.setFinishedAddress(getIntValue(row, "finished_address", 0));
+                    deviceInfoRows.add(infoRow);
+                }
+                showDeviceInfo = true;
+                showDeviceInfo2 = deviceInfoRows.size() > 1;
+            } else {
+                // 兼容旧配置
+                showDeviceInfo = getBooleanValue(uiObj, "show_device_info", false);
+                showDeviceInfo2 = false;
+            }
+        }
+    }
+
+    /**
+     * 从Map中获取整数值,支持默认值
+     */
+    private int getIntValue(Map<String, Object> map, String key, int defaultValue) {
+        Object value = map.get(key);
+        if (value instanceof Number) {
+            return ((Number) value).intValue();
+        }
+        return defaultValue;
+    }
+
+    /**
+     * 从Map中获取布尔值,支持默认值
+     */
+    private boolean getBooleanValue(Map<String, Object> map, String key, boolean defaultValue) {
+        Object value = map.get(key);
+        if (value instanceof Boolean) {
+            return (Boolean) value;
+        }
+        return defaultValue;
+    }
+
+    // ========== 便捷方法 ==========
+
+    /**
+     * 是否为单工位模式
+     */
+    public boolean isSingleMode() {
+        return "single".equalsIgnoreCase(mode);
+    }
+
+    /**
+     * 是否为双工位模式
+     */
+    public boolean isDualMode() {
+        return "dual".equalsIgnoreCase(mode);
+    }
+
+    /**
+     * 获取工位数量
+     */
+    public int getStationCount() {
+        return stations.size();
+    }
+
+    /**
+     * 根据索引获取工位信息
+     */
+    public StationInfo getStation(int index) {
+        if (index >= 0 && index < stations.size()) {
+            return stations.get(index);
+        }
+        return null;
+    }
+
+    /**
+     * 根据工位号获取工位信息
+     */
+    public StationInfo getStationByCode(String code) {
+        for (StationInfo station : stations) {
+            if (station.getCode().equalsIgnoreCase(code)) {
+                return station;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * 获取工位号(兼容旧代码)
+     */
+    public String getStationCode(int index) {
+        StationInfo station = getStation(index);
+        return station != null ? station.getCode() : null;
+    }
+
+    /**
+     * 检查是否有指定功能
+     */
+    public boolean hasFeature(String featureName) {
+        for (StepConfig step : workflowSteps) {
+            if (step.getId().contains(featureName)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * 检查是否有物料绑定步骤
+     */
+    public boolean hasMaterialBinding() {
+        return hasFeature("scan_material") || hasFeature("bind_material");
+    }
+
+    /**
+     * 检查是否有物料校验步骤
+     */
+    public boolean hasMaterialValidation() {
+        return hasFeature("validate_material");
+    }
+
+    /**
+     * 是否为手动提交模式
+     */
+    public boolean isManualSubmitMode() {
+        return "manual".equalsIgnoreCase(submitMode);
+    }
+
+    /**
+     * 是否为自动提交模式
+     */
+    public boolean isAutoSubmitMode() {
+        return "auto".equalsIgnoreCase(submitMode);
+    }
+
+    /**
+     * 获取提交模式
+     */
+    public String getSubmitMode() {
+        return submitMode;
+    }
+
+    /**
+     * 获取设备连接配置
+     */
+    public DeviceConnection getDeviceConnection(int stationIndex) {
+        for (DeviceConnection conn : deviceConnections) {
+            if (conn.getStationIndex() == stationIndex) {
+                return conn;
+            }
+        }
+        return null;
+    }
+
+    // ========== Getters ==========
+
+    public String getMode() {
+        return mode;
+    }
+
+    public List<StationInfo> getStations() {
+        return stations;
+    }
+
+    public String getLineSn() {
+        return lineSn;
+    }
+
+    public String getServerIp() {
+        return serverIp;
+    }
+
+    public int getTcpPort() {
+        return tcpPort;
+    }
+
+    public int getHttpPort() {
+        return httpPort;
+    }
+
+    public int getHeartBeatCycle() {
+        return heartBeatCycle;
+    }
+
+    public List<StepConfig> getWorkflowSteps() {
+        return workflowSteps;
+    }
+
+    public boolean isDeviceEnabled() {
+        return deviceEnabled;
+    }
+
+    public String getDeviceType() {
+        return deviceType;
+    }
+
+    public List<DeviceConnection> getDeviceConnections() {
+        return deviceConnections;
+    }
+
+    public boolean isTimeoutEnabled() {
+        return timeoutEnabled;
+    }
+
+    public int getConnectTimeout() {
+        return connectTimeout;
+    }
+
+    public int getHeartbeatAddress() {
+        return heartbeatAddress;
+    }
+
+    public int getReadySignalAddress() {
+        return readySignalAddress;
+    }
+
+    public String getStationName() {
+        return stationName;
+    }
+
+    public boolean isShowDeviceInfo() {
+        return showDeviceInfo;
+    }
+
+    public boolean isShowDeviceInfo2() {
+        return showDeviceInfo2;
+    }
+
+    public List<DeviceInfoRow> getDeviceInfoRows() {
+        return deviceInfoRows;
+    }
+
+    public DeviceInfoRow getDeviceInfoRow(int index) {
+        if (index >= 0 && index < deviceInfoRows.size()) {
+            return deviceInfoRows.get(index);
+        }
+        return null;
+    }
+
+    public boolean isShowMaterialInput() {
+        return showMaterialInput;
+    }
+
+    public String getMaterialLabel() {
+        return materialLabel;
+    }
+
+    public Map<String, Object> getRawConfig() {
+        return rawConfig;
+    }
+
+    public DeviceAddresses getDeviceAddresses() {
+        return deviceAddresses;
+    }
+
+    public CompleteCondition getCompleteCondition() {
+        return completeCondition;
+    }
+
+    public HeartbeatConfig getHeartbeatConfig() {
+        return heartbeatConfig;
+    }
+
+    public RelayConfig getRelayConfig() {
+        return relayConfig;
+    }
+
+    /**
+     * 检查是否配置了心跳
+     */
+    public boolean hasHeartbeat() {
+        return heartbeatConfig != null && heartbeatConfig.isEnabled();
+    }
+
+    /**
+     * 检查是否配置了继电器
+     */
+    public boolean hasRelay() {
+        return relayConfig != null && relayConfig.isEnabled();
+    }
+
+    // ========== 内部类 ==========
+
+    /**
+     * 工位信息
+     */
+    public static class StationInfo {
+        private int index;
+        private String code;
+
+        public StationInfo(int index, String code) {
+            this.index = index;
+            this.code = code;
+        }
+
+        public int getIndex() {
+            return index;
+        }
+
+        public String getCode() {
+            return code;
+        }
+
+        @Override
+        public String toString() {
+            return "StationInfo{code='" + code + "'}";
+        }
+    }
+
+    /**
+     * 步骤配置
+     */
+    public static class StepConfig {
+        private String id;
+        private String name;
+        private String craft;
+        private Map<String, Object> config;
+
+        public StepConfig() {
+            this.config = new HashMap<>();
+        }
+
+        public String getId() {
+            return id;
+        }
+
+        public void setId(String id) {
+            this.id = id;
+        }
+
+        public String getName() {
+            return name;
+        }
+
+        public void setName(String name) {
+            this.name = name;
+        }
+
+        public String getCraft() {
+            return craft;
+        }
+
+        public void setCraft(String craft) {
+            this.craft = craft;
+        }
+
+        public Map<String, Object> getConfig() {
+            return config;
+        }
+
+        public void setConfig(Map<String, Object> config) {
+            this.config = config != null ? config : new HashMap<>();
+        }
+
+        public String getConfigString(String key) {
+            Object value = config.get(key);
+            return value != null ? value.toString() : null;
+        }
+
+        public boolean getConfigBoolean(String key, boolean defaultValue) {
+            Object value = config.get(key);
+            if (value instanceof Boolean) {
+                return (Boolean) value;
+            }
+            return defaultValue;
+        }
+
+        @Override
+        public String toString() {
+            return "StepConfig{id='" + id + "', name='" + name + "'}";
+        }
+    }
+
+    /**
+     * 设备连接配置
+     */
+    public static class DeviceConnection {
+        private int stationIndex;
+        private String ip;
+        private int port;
+
+        public DeviceConnection(int stationIndex, String ip, int port) {
+            this.stationIndex = stationIndex;
+            this.ip = ip;
+            this.port = port;
+        }
+
+        public int getStationIndex() {
+            return stationIndex;
+        }
+
+        public String getIp() {
+            return ip;
+        }
+
+        public int getPort() {
+            return port;
+        }
+
+        @Override
+        public String toString() {
+            return "DeviceConnection{stationIndex=" + stationIndex + ", ip='" + ip + "', port=" + port + "}";
+        }
+    }
+
+    /**
+     * 设备地址配置
+     */
+    public static class DeviceAddresses {
+        private String enableSignal;      // 允许信号地址(S7格式如 DB9.18.1)
+        private String startSignal;       // 启动信号地址
+        private String completeSignal;    // 完成信号地址
+        private String deviceStatus;      // 设备状态地址(Modbus格式如 15001)
+        private int finishedCount;        // 完成数量地址
+        private int presetCount;          // 预设数量地址
+        private int qualifiedCount;       // 合格数量地址
+        private int startCommand;         // 启动命令地址
+
+        public String getEnableSignal() { return enableSignal; }
+        public void setEnableSignal(String enableSignal) { this.enableSignal = enableSignal; }
+        public String getStartSignal() { return startSignal; }
+        public void setStartSignal(String startSignal) { this.startSignal = startSignal; }
+        public String getCompleteSignal() { return completeSignal; }
+        public void setCompleteSignal(String completeSignal) { this.completeSignal = completeSignal; }
+        public String getDeviceStatus() { return deviceStatus; }
+        public void setDeviceStatus(String deviceStatus) { this.deviceStatus = deviceStatus; }
+        public int getFinishedCount() { return finishedCount; }
+        public void setFinishedCount(int finishedCount) { this.finishedCount = finishedCount; }
+        public int getPresetCount() { return presetCount; }
+        public void setPresetCount(int presetCount) { this.presetCount = presetCount; }
+        public int getQualifiedCount() { return qualifiedCount; }
+        public void setQualifiedCount(int qualifiedCount) { this.qualifiedCount = qualifiedCount; }
+        public int getStartCommand() { return startCommand; }
+        public void setStartCommand(int startCommand) { this.startCommand = startCommand; }
+    }
+
+    /**
+     * 完成条件配置
+     */
+    public static class CompleteCondition {
+        private String type = "signal";   // signal | count | signal_with_time
+        private int runningValue = 1;     // 运行中的值
+        private int stoppedValue = 0;     // 停止的值
+        private int minRunningTime = 0;   // 最小运行时间(ms)
+
+        public String getType() { return type; }
+        public void setType(String type) { this.type = type != null ? type : "signal"; }
+        public int getRunningValue() { return runningValue; }
+        public void setRunningValue(int runningValue) { this.runningValue = runningValue; }
+        public int getStoppedValue() { return stoppedValue; }
+        public void setStoppedValue(int stoppedValue) { this.stoppedValue = stoppedValue; }
+        public int getMinRunningTime() { return minRunningTime; }
+        public void setMinRunningTime(int minRunningTime) { this.minRunningTime = minRunningTime; }
+    }
+
+    /**
+     * 心跳配置
+     */
+    public static class HeartbeatConfig {
+        private boolean enabled = true;
+        private int address = 4160;
+        private int interval = 500;       // 心跳间隔(ms)
+
+        public boolean isEnabled() { return enabled; }
+        public void setEnabled(boolean enabled) { this.enabled = enabled; }
+        public int getAddress() { return address; }
+        public void setAddress(int address) { this.address = address; }
+        public int getInterval() { return interval; }
+        public void setInterval(int interval) { this.interval = interval; }
+    }
+
+    /**
+     * 继电器配置
+     */
+    public static class RelayConfig {
+        private boolean enabled = true;
+        private String type;              // modbus_rtu
+        private String port;              // COM2
+        private int baudrate = 38400;
+        private String openCommand;       // 开继电器命令(十六进制)
+        private String closeCommand;      // 关继电器命令(十六进制)
+
+        public boolean isEnabled() { return enabled; }
+        public void setEnabled(boolean enabled) { this.enabled = enabled; }
+        public String getType() { return type; }
+        public void setType(String type) { this.type = type; }
+        public String getPort() { return port; }
+        public void setPort(String port) { this.port = port; }
+        public int getBaudrate() { return baudrate; }
+        public void setBaudrate(int baudrate) { this.baudrate = baudrate; }
+        public String getOpenCommand() { return openCommand; }
+        public void setOpenCommand(String openCommand) { this.openCommand = openCommand; }
+        public String getCloseCommand() { return closeCommand; }
+        public void setCloseCommand(String closeCommand) { this.closeCommand = closeCommand; }
+    }
+
+    /**
+     * 设备信息行配置
+     */
+    public static class DeviceInfoRow {
+        private String label;               // 可选前置标签(如"M6"),为空或null则不显示
+        private int connectionIndex;        // 对应 connections 中的索引,指定从哪个设备读数据
+        private int presetAddress;           // 预设数量寄存器地址
+        private int finishedAddress;         // 完成数量寄存器地址
+
+        public String getLabel() { return label; }
+        public void setLabel(String label) { this.label = label; }
+        public int getConnectionIndex() { return connectionIndex; }
+        public void setConnectionIndex(int connectionIndex) { this.connectionIndex = connectionIndex; }
+        public int getPresetAddress() { return presetAddress; }
+        public void setPresetAddress(int presetAddress) { this.presetAddress = presetAddress; }
+        public int getFinishedAddress() { return finishedAddress; }
+        public void setFinishedAddress(int finishedAddress) { this.finishedAddress = finishedAddress; }
+
+        public boolean hasLabel() {
+            return label != null && !label.trim().isEmpty();
+        }
+    }
+}

+ 683 - 0
src/com/mes/core/StationContext.java

@@ -0,0 +1,683 @@
+package com.mes.core;
+
+import com.mes.device.IDeviceDriver;
+import com.mes.ui.component.WorkstationPanel;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 工位上下文 - 管理单个工位的所有状态信息
+ * 解决状态分散、全局变量污染问题
+ */
+public class StationContext {
+    private static final Logger log = LoggerFactory.getLogger(StationContext.class);
+
+    // ========== 工位标识 ==========
+    private final int stationIndex;         // 工位索引 0=工位1, 1=工位2
+    private final String stationCode;       // 工位号 OP150A(从配置读取)
+    private final String stationName;       // 工位名称
+    private final String lineSn;            // 线体号 XT
+
+    // ========== 当前工作数据 ==========
+    private String productSn;               // 工件码
+    private String materialSn;              // 物料码(底护板/冷板)
+    private String user;                    // 操作用户
+
+    // ========== 流程状态 ==========
+    private int currentStepIndex;           // 当前步骤索引
+    private WorkflowState state;            // 流程状态
+    private String lastError;               // 最后错误信息
+
+    // ========== 检查状态(带注解,支持调试控制) ==========
+    @WorkflowCondition(name = "质量检查通过", description = "质量检查是否通过", 
+            group = "检查状态", affectedSteps = {"start_work", "device_start", "upload_result"})
+    private boolean qualityPassed;          // 质量是否通过
+    
+    @WorkflowCondition(name = "物料已校验", description = "物料是否校验通过", 
+            group = "检查状态", affectedSteps = {"bind_material"})
+    private boolean materialValidated;      // 物料是否校验通过
+    
+    @WorkflowCondition(name = "物料已绑定", description = "物料是否已绑定", 
+            group = "检查状态", affectedSteps = {"bind_material"})
+    private boolean materialBound;          // 物料是否已绑定
+    
+    @WorkflowCondition(name = "工作已开始", description = "是否已开始工作", 
+            group = "流程状态", affectedSteps = {"wait_complete", "upload_result"})
+    private boolean workStarted;            // 是否已开始工作
+    
+    @WorkflowCondition(name = "结果已上传", description = "结果是否已上传", 
+            group = "流程状态", affectedSteps = {"upload_result"})
+    private boolean resultUploaded;         // 结果是否已上传
+    
+    @WorkflowCondition(name = "正在提交", description = "是否正在提交中", 
+            group = "流程状态", affectedSteps = {"upload_result"})
+    private boolean isSubmitting;           // 是否正在提交
+    
+    @WorkflowCondition(name = "等待用户操作", description = "是否等待用户点击OK/NG", 
+            group = "流程状态", affectedSteps = {"start_work"})
+    private boolean waitingForUserAction;   // 是否等待用户操作(手动模式)
+
+    // ========== 设备相关 ==========
+    private IDeviceDriver deviceDriver;     // 设备驱动实例
+    private IDeviceDriver deviceDriver2;    // 第二行设备驱动实例(可能来自不同IP的设备)
+    private com.mes.device.RelayDriver relayDriver;  // 继电器驱动实例
+    
+    @WorkflowCondition(name = "设备启用", description = "设备是否启用", 
+            group = "设备状态", affectedSteps = {"device_start", "wait_complete"})
+    private boolean deviceEnabled;          // 设备是否启用
+    
+    @WorkflowCondition(name = "心跳激活", description = "设备心跳是否激活", 
+            group = "设备状态", affectedSteps = {})
+    private boolean heartbeatActive;        // 心跳是否激活
+    
+    @WorkflowCondition(name = "设备已启动", description = "设备是否已启动", 
+            group = "设备状态", affectedSteps = {"wait_device_complete"})
+    private boolean deviceStarted;          // 设备是否已启动
+    
+    // 设备运行时间记录(用于signal_with_time完成条件)
+    private long deviceStartTime;           // 设备启动时间戳
+    
+    // ========== 设备数据(拉铆等) ==========
+    @WorkflowCondition(name = "预设数量", description = "拉铆预设数量", 
+            group = "设备数据", affectedSteps = {"wait_device_complete"})
+    private int presetCount;                // 预设数量
+    
+    @WorkflowCondition(name = "完成数量", description = "拉铆完成数量", 
+            group = "设备数据", affectedSteps = {"wait_device_complete"})
+    private int finishedCount;              // 完成数量
+    
+    @WorkflowCondition(name = "合格数量", description = "拉铆合格数量", 
+            group = "设备数据", affectedSteps = {"wait_device_complete"})
+    private int qualifiedCount;             // 合格数量
+
+    // ========== 设备数据第二行 ==========
+    private int presetCount2;               // 第二行预设数量
+    private int finishedCount2;             // 第二行完成数量
+    private boolean deviceConnected2;       // 第二行设备连接状态
+
+    // ========== UI相关 ==========
+    private WorkstationPanel uiPanel;       // 对应的UI面板
+    private String statusMessage;           // 状态消息
+    private int statusLevel;                // 状态级别 0=正常 1=警告 -1=错误
+
+    // ========== 扩展数据 ==========
+    private Map<String, Object> extraData;  // 扩展数据存储
+
+    // ========== 回调接口 ==========
+    private ContextChangeListener changeListener;
+    
+    // ========== 调试拦截器 ==========
+    private ConditionInterceptor conditionInterceptor;
+
+    /**
+     * 流程状态枚举
+     */
+    public enum WorkflowState {
+        IDLE,           // 空闲,等待扫码
+        SCANNING,       // 正在扫码
+        CHECKING,       // 正在检查(质量/物料)
+        WORKING,        // 工作中
+        WAITING,        // 等待设备完成
+        UPLOADING,      // 正在上传结果
+        COMPLETED,      // 已完成
+        ERROR           // 错误状态
+    }
+
+    /**
+     * 状态变化监听器
+     */
+    public interface ContextChangeListener {
+        void onStateChanged(StationContext context, WorkflowState oldState, WorkflowState newState);
+        void onStatusMessageChanged(StationContext context, String message, int level);
+        void onDataChanged(StationContext context, String field, Object oldValue, Object newValue);
+    }
+
+    /**
+     * 构造函数
+     */
+    public StationContext(int stationIndex, String stationCode, String stationName, String lineSn) {
+        this.stationIndex = stationIndex;
+        this.stationCode = stationCode;
+        this.stationName = stationName != null ? stationName : stationCode;
+        this.lineSn = lineSn;
+        this.extraData = new ConcurrentHashMap<>();
+        this.state = WorkflowState.IDLE;
+        this.currentStepIndex = 0;
+        this.statusMessage = "请扫描工件码";
+        this.statusLevel = 0;
+    }
+
+    /**
+     * 从配置创建上下文
+     */
+    public static StationContext fromConfig(StationConfig.StationInfo stationInfo, String lineSn) {
+        return new StationContext(
+                stationInfo.getIndex(),
+                stationInfo.getCode(),
+                stationInfo.getCode(),
+                lineSn
+        );
+    }
+
+    // ========== 状态管理方法 ==========
+
+    /**
+     * 重置到初始状态(完成一件后重置)
+     * 注意:不重置 currentStepIndex,由流程引擎控制
+     */
+    public void reset() {
+        log.info("[{}] 重置工位状态", stationCode);
+
+        // 清空工作数据
+        this.productSn = null;
+        this.materialSn = null;
+
+        // 重置流程状态(不重置 currentStepIndex)
+        setState(WorkflowState.IDLE);
+        this.lastError = null;
+
+        // 重置检查状态
+        this.qualityPassed = false;
+        this.materialValidated = false;
+        this.materialBound = false;
+        this.workStarted = false;
+        this.resultUploaded = false;
+        this.isSubmitting = false;
+        this.waitingForUserAction = false;
+
+        // 重置设备状态
+        this.heartbeatActive = false;
+        this.deviceStarted = false;
+        this.deviceStartTime = 0;
+        this.presetCount = 0;
+        this.finishedCount = 0;
+        this.qualifiedCount = 0;
+        this.presetCount2 = 0;
+        this.finishedCount2 = 0;
+        this.deviceConnected2 = false;
+
+        // 清空扩展数据
+        this.extraData.clear();
+
+        // 设置初始提示
+        setStatusMessage("请扫描工件码", 0);
+
+        // 刷新UI
+        updateUI();
+    }
+
+    /**
+     * 设置流程状态
+     */
+    public void setState(WorkflowState newState) {
+        WorkflowState oldState = this.state;
+        this.state = newState;
+        log.debug("[{}] 状态变更: {} -> {}", stationCode, oldState, newState);
+
+        if (changeListener != null && oldState != newState) {
+            changeListener.onStateChanged(this, oldState, newState);
+        }
+    }
+
+    /**
+     * 设置状态消息
+     */
+    public void setStatusMessage(String message, int level) {
+        this.statusMessage = message;
+        this.statusLevel = level;
+        log.info("[{}] 状态消息: {} (level={})", stationCode, message, level);
+
+        if (changeListener != null) {
+            changeListener.onStatusMessageChanged(this, message, level);
+        }
+
+        // 刷新UI状态栏
+        if (uiPanel != null) {
+            uiPanel.updateStatus(message, level);
+        }
+    }
+
+    /**
+     * 刷新UI显示
+     */
+    public void updateUI() {
+        if (uiPanel != null) {
+            uiPanel.refresh(this);
+        }
+    }
+
+    // ========== 流程控制方法 ==========
+
+    /**
+     * 进入下一步
+     */
+    public void nextStep() {
+        currentStepIndex++;
+        log.debug("[{}] 进入步骤 {}", stationCode, currentStepIndex);
+    }
+
+    /**
+     * 回退到上一步
+     */
+    public void previousStep() {
+        if (currentStepIndex > 0) {
+            currentStepIndex--;
+            log.debug("[{}] 回退到步骤 {}", stationCode, currentStepIndex);
+        }
+    }
+
+    /**
+     * 跳转到指定步骤
+     */
+    public void goToStep(int stepIndex) {
+        this.currentStepIndex = stepIndex;
+        log.debug("[{}] 跳转到步骤 {}", stationCode, stepIndex);
+    }
+
+    /**
+     * 标记错误
+     */
+    public void markError(String errorMessage) {
+        this.lastError = errorMessage;
+        setState(WorkflowState.ERROR);
+        setStatusMessage(errorMessage, -1);
+    }
+
+    // ========== 数据访问方法 ==========
+
+    /**
+     * 设置工件码
+     */
+    public void setProductSn(String productSn) {
+        String oldValue = this.productSn;
+        this.productSn = productSn;
+        log.info("[{}] 工件码: {}", stationCode, productSn);
+
+        if (changeListener != null) {
+            changeListener.onDataChanged(this, "productSn", oldValue, productSn);
+        }
+    }
+
+    /**
+     * 设置物料码
+     */
+    public void setMaterialSn(String materialSn) {
+        String oldValue = this.materialSn;
+        this.materialSn = materialSn;
+        log.info("[{}] 物料码: {}", stationCode, materialSn);
+
+        if (changeListener != null) {
+            changeListener.onDataChanged(this, "materialSn", oldValue, materialSn);
+        }
+    }
+
+    /**
+     * 获取处理后的工件码(36位码处理)
+     */
+    public String getProcessedProductSn() {
+        if (productSn == null) {
+            return null;
+        }
+        // 36位码取前20位
+        if (productSn.length() == 36) {
+            return productSn.substring(0, 20);
+        }
+        return productSn;
+    }
+
+    /**
+     * 存储扩展数据
+     */
+    public void setExtra(String key, Object value) {
+        extraData.put(key, value);
+    }
+
+    /**
+     * 获取扩展数据
+     */
+    @SuppressWarnings("unchecked")
+    public <T> T getExtra(String key, T defaultValue) {
+        Object value = extraData.get(key);
+        if (value == null) {
+            return defaultValue;
+        }
+        try {
+            return (T) value;
+        } catch (ClassCastException e) {
+            return defaultValue;
+        }
+    }
+
+    /**
+     * 获取扩展数据(字符串)
+     */
+    public String getExtraString(String key) {
+        Object value = extraData.get(key);
+        return value != null ? value.toString() : null;
+    }
+
+    // ========== Getters & Setters ==========
+
+    public int getStationIndex() {
+        return stationIndex;
+    }
+
+    public String getStationCode() {
+        return stationCode;
+    }
+
+    public String getStationName() {
+        return stationName;
+    }
+
+    public String getLineSn() {
+        return lineSn;
+    }
+
+    public String getProductSn() {
+        return productSn;
+    }
+
+    public String getMaterialSn() {
+        return materialSn;
+    }
+
+    public String getUser() {
+        return user;
+    }
+
+    public void setUser(String user) {
+        this.user = user;
+    }
+
+    public int getCurrentStepIndex() {
+        return currentStepIndex;
+    }
+
+    public void setCurrentStepIndex(int currentStepIndex) {
+        this.currentStepIndex = currentStepIndex;
+    }
+
+    public WorkflowState getState() {
+        return state;
+    }
+
+    public String getLastError() {
+        return lastError;
+    }
+
+    public boolean isQualityPassed() {
+        return interceptBooleanGet("qualityPassed", qualityPassed);
+    }
+
+    public void setQualityPassed(boolean qualityPassed) {
+        this.qualityPassed = interceptBoolean("qualityPassed", qualityPassed);
+    }
+
+    public boolean isMaterialValidated() {
+        return interceptBooleanGet("materialValidated", materialValidated);
+    }
+
+    public void setMaterialValidated(boolean materialValidated) {
+        this.materialValidated = interceptBoolean("materialValidated", materialValidated);
+    }
+
+    public boolean isMaterialBound() {
+        return interceptBooleanGet("materialBound", materialBound);
+    }
+
+    public void setMaterialBound(boolean materialBound) {
+        this.materialBound = interceptBoolean("materialBound", materialBound);
+    }
+
+    public boolean isWorkStarted() {
+        return interceptBooleanGet("workStarted", workStarted);
+    }
+
+    public void setWorkStarted(boolean workStarted) {
+        this.workStarted = interceptBoolean("workStarted", workStarted);
+    }
+
+    public boolean isResultUploaded() {
+        return interceptBooleanGet("resultUploaded", resultUploaded);
+    }
+
+    public void setResultUploaded(boolean resultUploaded) {
+        this.resultUploaded = interceptBoolean("resultUploaded", resultUploaded);
+    }
+
+    public boolean isSubmitting() {
+        return interceptBooleanGet("isSubmitting", isSubmitting);
+    }
+
+    public void setSubmitting(boolean submitting) {
+        isSubmitting = interceptBoolean("isSubmitting", submitting);
+    }
+
+    public boolean isWaitingForUserAction() {
+        return interceptBooleanGet("waitingForUserAction", waitingForUserAction);
+    }
+
+    public void setWaitingForUserAction(boolean waitingForUserAction) {
+        this.waitingForUserAction = interceptBoolean("waitingForUserAction", waitingForUserAction);
+    }
+
+    public IDeviceDriver getDeviceDriver() {
+        return deviceDriver;
+    }
+
+    public void setDeviceDriver(IDeviceDriver deviceDriver) {
+        this.deviceDriver = deviceDriver;
+    }
+
+    public IDeviceDriver getDeviceDriver2() {
+        return deviceDriver2;
+    }
+
+    public void setDeviceDriver2(IDeviceDriver deviceDriver2) {
+        this.deviceDriver2 = deviceDriver2;
+    }
+
+    public boolean isDeviceEnabled() {
+        return interceptBooleanGet("deviceEnabled", deviceEnabled);
+    }
+
+    public void setDeviceEnabled(boolean deviceEnabled) {
+        this.deviceEnabled = interceptBoolean("deviceEnabled", deviceEnabled);
+    }
+
+    public boolean isHeartbeatActive() {
+        return interceptBooleanGet("heartbeatActive", heartbeatActive);
+    }
+
+    public void setHeartbeatActive(boolean heartbeatActive) {
+        this.heartbeatActive = interceptBoolean("heartbeatActive", heartbeatActive);
+    }
+
+    public boolean isDeviceStarted() {
+        return interceptBooleanGet("deviceStarted", deviceStarted);
+    }
+
+    public void setDeviceStarted(boolean deviceStarted) {
+        this.deviceStarted = interceptBoolean("deviceStarted", deviceStarted);
+        if (deviceStarted && deviceStartTime == 0) {
+            this.deviceStartTime = System.currentTimeMillis();
+        }
+    }
+
+    public long getDeviceStartTime() {
+        return deviceStartTime;
+    }
+
+    public void setDeviceStartTime(long deviceStartTime) {
+        this.deviceStartTime = deviceStartTime;
+    }
+
+    /**
+     * 获取设备运行时间(毫秒)
+     */
+    public long getDeviceRunningTime() {
+        if (deviceStartTime == 0) {
+            return 0;
+        }
+        return System.currentTimeMillis() - deviceStartTime;
+    }
+
+    public com.mes.device.RelayDriver getRelayDriver() {
+        return relayDriver;
+    }
+
+    public void setRelayDriver(com.mes.device.RelayDriver relayDriver) {
+        this.relayDriver = relayDriver;
+    }
+
+    public int getPresetCount() {
+        return interceptIntGet("presetCount", presetCount);
+    }
+
+    public void setPresetCount(int presetCount) {
+        this.presetCount = interceptInt("presetCount", presetCount);
+    }
+
+    public int getFinishedCount() {
+        return interceptIntGet("finishedCount", finishedCount);
+    }
+
+    public void setFinishedCount(int finishedCount) {
+        this.finishedCount = interceptInt("finishedCount", finishedCount);
+    }
+
+    public int getQualifiedCount() {
+        return interceptIntGet("qualifiedCount", qualifiedCount);
+    }
+
+    public void setQualifiedCount(int qualifiedCount) {
+        this.qualifiedCount = interceptInt("qualifiedCount", qualifiedCount);
+    }
+
+    public int getPresetCount2() {
+        return presetCount2;
+    }
+
+    public void setPresetCount2(int presetCount2) {
+        this.presetCount2 = presetCount2;
+    }
+
+    public int getFinishedCount2() {
+        return finishedCount2;
+    }
+
+    public void setFinishedCount2(int finishedCount2) {
+        this.finishedCount2 = finishedCount2;
+    }
+
+    public boolean isDeviceConnected2() {
+        return deviceConnected2;
+    }
+
+    public void setDeviceConnected2(boolean deviceConnected2) {
+        this.deviceConnected2 = deviceConnected2;
+    }
+    
+    // ========== 拦截器相关方法 ==========
+    
+    /**
+     * 设置条件拦截器
+     */
+    public void setConditionInterceptor(ConditionInterceptor interceptor) {
+        this.conditionInterceptor = interceptor;
+    }
+    
+    /**
+     * 获取条件拦截器
+     */
+    public ConditionInterceptor getConditionInterceptor() {
+        return conditionInterceptor;
+    }
+    
+    /**
+     * 拦截布尔值(setter用)
+     */
+    private boolean interceptBoolean(String fieldName, boolean value) {
+        if (conditionInterceptor != null) {
+            return conditionInterceptor.intercept(fieldName, value);
+        }
+        return value;
+    }
+    
+    /**
+     * 拦截整数值(setter用)
+     */
+    private int interceptInt(String fieldName, int value) {
+        if (conditionInterceptor != null) {
+            return conditionInterceptor.intercept(fieldName, value);
+        }
+        return value;
+    }
+    
+    /**
+     * 拦截布尔值读取(getter用)
+     * 如果有调试规则,返回规则值;否则返回实际值
+     */
+    private boolean interceptBooleanGet(String fieldName, boolean actualValue) {
+        if (conditionInterceptor != null && conditionInterceptor.hasRule(fieldName)) {
+            Object ruleValue = conditionInterceptor.getRuleValue(fieldName);
+            if (ruleValue instanceof Boolean) {
+                return (Boolean) ruleValue;
+            }
+        }
+        return actualValue;
+    }
+    
+    /**
+     * 拦截整数值读取(getter用)
+     * 如果有调试规则,返回规则值;否则返回实际值
+     */
+    private int interceptIntGet(String fieldName, int actualValue) {
+        if (conditionInterceptor != null && conditionInterceptor.hasRule(fieldName)) {
+            Object ruleValue = conditionInterceptor.getRuleValue(fieldName);
+            if (ruleValue instanceof Number) {
+                return ((Number) ruleValue).intValue();
+            }
+        }
+        return actualValue;
+    }
+
+    public WorkstationPanel getUiPanel() {
+        return uiPanel;
+    }
+
+    public void setUiPanel(WorkstationPanel uiPanel) {
+        this.uiPanel = uiPanel;
+    }
+
+    public String getStatusMessage() {
+        return statusMessage;
+    }
+
+    public int getStatusLevel() {
+        return statusLevel;
+    }
+
+    public ContextChangeListener getChangeListener() {
+        return changeListener;
+    }
+
+    public void setChangeListener(ContextChangeListener changeListener) {
+        this.changeListener = changeListener;
+    }
+
+    @Override
+    public String toString() {
+        return "StationContext{" +
+                "stationCode='" + stationCode + '\'' +
+                ", state=" + state +
+                ", productSn='" + productSn + '\'' +
+                ", currentStep=" + currentStepIndex +
+                '}';
+    }
+}

+ 38 - 0
src/com/mes/core/WorkflowCondition.java

@@ -0,0 +1,38 @@
+package com.mes.core;
+
+import java.lang.annotation.*;
+
+/**
+ * 流程条件注解 - 标记影响流程执行的字段
+ * 用于调试面板自动识别可控制的条件变量
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.FIELD)
+@Documented
+public @interface WorkflowCondition {
+    
+    /**
+     * 条件名称(用于显示)
+     */
+    String name();
+    
+    /**
+     * 条件描述
+     */
+    String description() default "";
+    
+    /**
+     * 影响的步骤ID列表(可选,用于提示)
+     */
+    String[] affectedSteps() default {};
+    
+    /**
+     * 条件分组(用于UI分组显示)
+     */
+    String group() default "default";
+    
+    /**
+     * 是否可以被调试控制
+     */
+    boolean controllable() default true;
+}

+ 498 - 0
src/com/mes/core/WorkflowEngine.java

@@ -0,0 +1,498 @@
+package com.mes.core;
+
+import com.mes.step.IWorkflowStep;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.*;
+import java.util.concurrent.*;
+
+/**
+ * 流程引擎 - 管理工作流步骤的执行
+ * 支持配置化流程组装、异步步骤处理、超时控制
+ */
+public class WorkflowEngine {
+    private static final Logger log = LoggerFactory.getLogger(WorkflowEngine.class);
+
+    // 工位上下文
+    private final StationContext context;
+
+    // 步骤列表(按执行顺序)
+    private final List<IWorkflowStep> steps;
+
+    // 步骤快速查找表 (stepId -> step)
+    private final Map<String, IWorkflowStep> stepMap;
+
+    // 消息类型到步骤的映射 (messageType -> step)
+    private final Map<String, IWorkflowStep> messageStepMap;
+
+    // 流程状态
+    private boolean running;
+    private boolean paused;
+
+    // 超时控制
+    private ScheduledExecutorService timeoutExecutor;
+    private ScheduledFuture<?> currentTimeoutTask;
+
+    // 回调接口
+    private WorkflowListener listener;
+
+    /**
+     * 流程事件监听器
+     */
+    public interface WorkflowListener {
+        void onStepStarted(WorkflowEngine engine, IWorkflowStep step);
+        void onStepCompleted(WorkflowEngine engine, IWorkflowStep step, boolean success);
+        void onWorkflowCompleted(WorkflowEngine engine);
+        void onWorkflowError(WorkflowEngine engine, String error);
+    }
+
+    public WorkflowEngine(StationContext context) {
+        this.context = context;
+        this.steps = new ArrayList<>();
+        this.stepMap = new LinkedHashMap<>();
+        this.messageStepMap = new HashMap<>();
+        this.running = false;
+        this.paused = false;
+        this.timeoutExecutor = Executors.newSingleThreadScheduledExecutor(r -> {
+            Thread t = new Thread(r, "WorkflowTimeout-" + context.getStationCode());
+            t.setDaemon(true);
+            return t;
+        });
+    }
+
+    /**
+     * 从配置构建流程
+     */
+    public void buildFromConfig(List<StationConfig.StepConfig> stepConfigs, StepFactory stepFactory) {
+        steps.clear();
+        stepMap.clear();
+        messageStepMap.clear();
+
+        for (StationConfig.StepConfig config : stepConfigs) {
+            IWorkflowStep step = stepFactory.createStep(config.getId());
+            if (step != null) {
+                // 配置步骤
+                step.configure(config.getConfig());
+                if (config.getCraft() != null) {
+                    step.setCraft(config.getCraft());
+                }
+
+                // 添加到列表
+                steps.add(step);
+                stepMap.put(step.getStepId(), step);
+
+                // 建立消息类型映射
+                String msgType = step.getMessageType();
+                if (msgType != null && !msgType.isEmpty()) {
+                    messageStepMap.put(msgType, step);
+                }
+
+                log.debug("[{}] 加载步骤: {} ({})", context.getStationCode(), step.getStepName(), step.getStepId());
+            } else {
+                log.warn("[{}] 未知步骤类型: {}", context.getStationCode(), config.getId());
+            }
+        }
+
+        log.info("[{}] 流程构建完成, 共 {} 个步骤", context.getStationCode(), steps.size());
+    }
+
+    /**
+     * 添加步骤
+     */
+    public void addStep(IWorkflowStep step) {
+        steps.add(step);
+        stepMap.put(step.getStepId(), step);
+
+        String msgType = step.getMessageType();
+        if (msgType != null && !msgType.isEmpty()) {
+            messageStepMap.put(msgType, step);
+        }
+    }
+
+    /**
+     * 启动流程
+     */
+    public void start() {
+        if (running) {
+            log.warn("[{}] 流程已在运行中", context.getStationCode());
+            return;
+        }
+
+        log.info("[{}] 启动流程", context.getStationCode());
+        running = true;
+        paused = false;
+        context.setCurrentStepIndex(0);
+        context.setState(StationContext.WorkflowState.IDLE);
+
+        // 执行第一步(如果不需要用户交互)
+        executeCurrentStepIfReady();
+    }
+
+    /**
+     * 停止流程
+     */
+    public void stop() {
+        log.info("[{}] 停止流程", context.getStationCode());
+        running = false;
+        paused = false;
+        cancelTimeout();
+    }
+
+    /**
+     * 暂停流程
+     */
+    public void pause() {
+        log.info("[{}] 暂停流程", context.getStationCode());
+        paused = true;
+    }
+
+    /**
+     * 恢复流程
+     */
+    public void resume() {
+        log.info("[{}] 恢复流程", context.getStationCode());
+        paused = false;
+        executeCurrentStepIfReady();
+    }
+
+    /**
+     * 重置流程
+     */
+    public void reset() {
+        log.info("[{}] 重置流程", context.getStationCode());
+        cancelTimeout();
+        context.reset();
+        context.setCurrentStepIndex(0);  // 重置步骤索引到第一步
+        running = true;
+        paused = false;
+    }
+
+    /**
+     * 获取当前步骤
+     */
+    public IWorkflowStep getCurrentStep() {
+        int index = context.getCurrentStepIndex();
+        if (index >= 0 && index < steps.size()) {
+            return steps.get(index);
+        }
+        return null;
+    }
+
+    /**
+     * 根据ID获取步骤
+     */
+    public IWorkflowStep getStep(String stepId) {
+        return stepMap.get(stepId);
+    }
+
+    /**
+     * 触发当前步骤执行(用户交互触发)
+     * 例如:用户扫码后调用此方法
+     */
+    public void triggerCurrentStep() {
+        if (!running || paused) {
+            log.warn("[{}] 流程未运行或已暂停", context.getStationCode());
+            return;
+        }
+
+        IWorkflowStep currentStep = getCurrentStep();
+        if (currentStep == null) {
+            log.warn("[{}] 当前没有待执行的步骤", context.getStationCode());
+            return;
+        }
+
+        executeStep(currentStep);
+    }
+
+    /**
+     * 执行指定步骤
+     */
+    private void executeStep(IWorkflowStep step) {
+        log.info("[{}] 执行步骤: {} ({})", context.getStationCode(), step.getStepName(), step.getStepId());
+
+        // 检查前置条件
+        if (!step.canExecute(context)) {
+            log.warn("[{}] 步骤 {} 前置条件不满足", context.getStationCode(), step.getStepId());
+            // 如果已经有错误消息,不要用"前置条件不满足"覆盖它
+            if (context.getStatusLevel() != -1) {
+                step.onFailure(context, "前置条件不满足");
+            } else {
+                // 已有错误消息,只记录日志不覆盖状态消息
+                log.error("[{}] 步骤 {} 执行失败: 前置条件不满足 (保留原错误消息: {})", 
+                        context.getStationCode(), step.getStepId(), context.getStatusMessage());
+            }
+            return;
+        }
+
+        // 通知监听器
+        if (listener != null) {
+            listener.onStepStarted(this, step);
+        }
+
+        try {
+            // 执行步骤
+            boolean result = step.execute(context);
+
+            if (result) {
+                // 异步步骤:设置超时,等待服务端响应或轮询完成
+                if (step.isAsync()) {
+                    // 注册异步完成回调(用于轮询类步骤主动通知完成)
+                    step.setAsyncCompleteCallback(success -> {
+                        cancelTimeout();
+                        onStepComplete(step, success);
+                    });
+                    startTimeout(step);
+                } else {
+                    // 同步步骤:直接完成,进入下一步
+                    onStepComplete(step, true);
+                }
+            } else {
+                // 执行失败
+                onStepComplete(step, false);
+            }
+        } catch (Exception e) {
+            log.error("[{}] 步骤 {} 执行异常: {}", context.getStationCode(), step.getStepId(), e.getMessage(), e);
+            step.onFailure(context, "执行异常: " + e.getMessage());
+            onStepComplete(step, false);
+        }
+    }
+
+    /**
+     * 处理服务端消息
+     * @param messageType 消息类型(AQDW, MKSW等)
+     * @param result 结果码
+     * @param message 完整消息
+     */
+    public void onServerMessage(String messageType, String result, String message) {
+        if (!running) {
+            log.warn("[{}] 流程未运行,忽略消息: {}", context.getStationCode(), messageType);
+            return;
+        }
+
+        // 取消超时
+        cancelTimeout();
+
+        // 查找对应的步骤
+        IWorkflowStep step = messageStepMap.get(messageType);
+        if (step == null) {
+            log.warn("[{}] 未找到消息类型 {} 对应的步骤", context.getStationCode(), messageType);
+            return;
+        }
+
+        // 检查是否是当前步骤
+        IWorkflowStep currentStep = getCurrentStep();
+        if (currentStep == null || !currentStep.getStepId().equals(step.getStepId())) {
+            log.warn("[{}] 收到非当前步骤的响应: 期望={}, 实际={}", 
+                    context.getStationCode(), 
+                    currentStep != null ? currentStep.getStepId() : "null",
+                    step.getStepId());
+            // 仍然处理响应,但不自动进入下一步
+            step.onServerResponse(context, result, message);
+            return;
+        }
+
+        // 处理响应
+        step.onServerResponse(context, result, message);
+
+        // 判断是否成功
+        boolean success = "OK".equalsIgnoreCase(result) || "UD".equalsIgnoreCase(result);
+        onStepComplete(step, success);
+    }
+
+    /**
+     * 步骤完成处理
+     */
+    private void onStepComplete(IWorkflowStep step, boolean success) {
+        log.info("[{}] 步骤 {} 完成, 成功={}", context.getStationCode(), step.getStepId(), success);
+
+        // 通知监听器
+        if (listener != null) {
+            listener.onStepCompleted(this, step, success);
+        }
+
+        if (success) {
+            // 进入下一步
+            moveToNextStep();
+        } else {
+            // 失败处理:根据步骤类型决定是否停止流程
+            // 默认行为:停留在当前步骤,等待重试
+            context.setState(StationContext.WorkflowState.ERROR);
+        }
+    }
+
+    /**
+     * 进入下一步
+     */
+    private void moveToNextStep() {
+        context.nextStep();
+        int nextIndex = context.getCurrentStepIndex();
+
+        if (nextIndex >= steps.size()) {
+            // 流程完成,重置到初始状态准备下一轮
+            onWorkflowComplete();
+            // 重置步骤索引到0,等待用户扫码开始新的流程
+            context.setCurrentStepIndex(0);
+            log.info("[{}] 流程重置,等待下一件工件", context.getStationCode());
+        } else {
+            // 执行下一步(如果不需要用户交互)
+            executeCurrentStepIfReady();
+        }
+    }
+
+    /**
+     * 如果当前步骤不需要用户交互,则自动执行
+     */
+    private void executeCurrentStepIfReady() {
+        IWorkflowStep currentStep = getCurrentStep();
+        if (currentStep == null) {
+            return;
+        }
+
+        // 检查是否可以跳过
+        if (currentStep.canSkip(context)) {
+            log.info("[{}] 跳过步骤: {}", context.getStationCode(), currentStep.getStepId());
+            currentStep.onSkip(context);
+            moveToNextStep();
+            return;
+        }
+
+        // 如果不需要用户交互,自动执行
+        if (!currentStep.requiresUserInteraction()) {
+            executeStep(currentStep);
+        } else {
+            // 需要用户交互的步骤:检查是否已经满足条件可以直接执行
+            // 例如:scan_product 步骤需要工件码,但用户可能已经扫过码了
+            if (currentStep.canExecute(context)) {
+                log.debug("[{}] 步骤 {} 需要用户交互,但条件已满足,尝试执行", 
+                         context.getStationCode(), currentStep.getStepName());
+                executeStep(currentStep);
+            } else {
+                // 等待用户操作
+                log.debug("[{}] 等待用户操作: {}", context.getStationCode(), currentStep.getStepName());
+            }
+        }
+    }
+
+    /**
+     * 流程完成
+     */
+    private void onWorkflowComplete() {
+        log.info("[{}] 流程完成", context.getStationCode());
+        context.setState(StationContext.WorkflowState.COMPLETED);
+
+        if (listener != null) {
+            listener.onWorkflowCompleted(this);
+        }
+    }
+
+    /**
+     * 启动超时计时器
+     */
+    private void startTimeout(IWorkflowStep step) {
+        int timeout = step.getTimeoutMs();
+        if (timeout <= 0) {
+            return;
+        }
+
+        cancelTimeout();
+
+        currentTimeoutTask = timeoutExecutor.schedule(() -> {
+            log.warn("[{}] 步骤 {} 超时", context.getStationCode(), step.getStepId());
+            step.onTimeout(context);
+            onStepComplete(step, false);
+        }, timeout, TimeUnit.MILLISECONDS);
+    }
+
+    /**
+     * 取消超时计时器
+     */
+    private void cancelTimeout() {
+        if (currentTimeoutTask != null && !currentTimeoutTask.isDone()) {
+            currentTimeoutTask.cancel(false);
+            currentTimeoutTask = null;
+        }
+    }
+
+    /**
+     * 跳过当前步骤(通过模拟执行成功)
+     * 用于流程监控面板的"跳过"功能
+     * @return true=跳过成功,false=无法跳过
+     */
+    public boolean skipCurrentStep() {
+        if (!running) {
+            log.warn("[{}] 流程未运行,无法跳过步骤", context.getStationCode());
+            return false;
+        }
+
+        IWorkflowStep currentStep = getCurrentStep();
+        if (currentStep == null) {
+            log.warn("[{}] 当前没有待执行的步骤", context.getStationCode());
+            return false;
+        }
+
+        log.info("[{}] 跳过步骤: {} ({})", context.getStationCode(), currentStep.getStepName(), currentStep.getStepId());
+
+        // 取消超时
+        cancelTimeout();
+
+        // 调用 simulateSuccess 模拟执行成功
+        currentStep.simulateSuccess(context);
+
+        // 通知监听器
+        if (listener != null) {
+            listener.onStepCompleted(this, currentStep, true);
+        }
+
+        // 进入下一步
+        moveToNextStep();
+
+        return true;
+    }
+
+    /**
+     * 获取当前步骤索引
+     */
+    public int getCurrentStepIndex() {
+        return context.getCurrentStepIndex();
+    }
+
+    /**
+     * 销毁引擎
+     */
+    public void destroy() {
+        stop();
+        if (timeoutExecutor != null && !timeoutExecutor.isShutdown()) {
+            timeoutExecutor.shutdownNow();
+        }
+    }
+
+    // ========== Getters & Setters ==========
+
+    public StationContext getContext() {
+        return context;
+    }
+
+    public List<IWorkflowStep> getSteps() {
+        return Collections.unmodifiableList(steps);
+    }
+
+    public boolean isRunning() {
+        return running;
+    }
+
+    public boolean isPaused() {
+        return paused;
+    }
+
+    public void setListener(WorkflowListener listener) {
+        this.listener = listener;
+    }
+
+    /**
+     * 步骤工厂接口
+     */
+    public interface StepFactory {
+        IWorkflowStep createStep(String stepId);
+    }
+}

+ 159 - 0
src/com/mes/device/DeviceDriverFactory.java

@@ -0,0 +1,159 @@
+package com.mes.device;
+
+import com.mes.core.StationConfig;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 设备驱动工厂 - 根据配置创建设备驱动实例
+ */
+public class DeviceDriverFactory {
+    private static final Logger log = LoggerFactory.getLogger(DeviceDriverFactory.class);
+
+    /**
+     * 根据配置创建设备驱动
+     * @param config 配置
+     * @param stationIndex 工位索引
+     * @return 设备驱动实例
+     */
+    public static IDeviceDriver createDriver(StationConfig config, int stationIndex) {
+        if (!config.isDeviceEnabled()) {
+            log.info("设备未启用,使用NullDriver");
+            return new NullDriver();
+        }
+
+        String deviceType = config.getDeviceType();
+        if (deviceType == null || deviceType.isEmpty() || "none".equalsIgnoreCase(deviceType)) {
+            return new NullDriver();
+        }
+
+        // 获取设备连接配置
+        StationConfig.DeviceConnection conn = config.getDeviceConnection(stationIndex);
+
+        switch (deviceType.toLowerCase()) {
+            case "modbus_tcp":
+                return createModbusTcpDriver(config, conn);
+
+            case "modbus_rtu":
+                // TODO: 实现ModbusRtuDriver
+                log.warn("Modbus RTU驱动尚未实现,使用NullDriver");
+                return new NullDriver();
+
+            case "s7":
+                return createS7Driver(config, conn);
+
+            case "relay":
+                return createRelayDriver(config);
+
+            default:
+                log.warn("未知的设备类型: {},使用NullDriver", deviceType);
+                return new NullDriver();
+        }
+    }
+
+    /**
+     * 创建Modbus TCP驱动
+     */
+    private static IDeviceDriver createModbusTcpDriver(StationConfig config, StationConfig.DeviceConnection conn) {
+        ModbusTcpDriver driver = new ModbusTcpDriver();
+
+        // 配置连接参数
+        Map<String, Object> connConfig = new HashMap<>();
+        if (conn != null) {
+            connConfig.put("ip", conn.getIp());
+            connConfig.put("port", conn.getPort());
+        }
+
+        // 配置地址
+        connConfig.put("ready_signal_address", config.getReadySignalAddress());
+        connConfig.put("heartbeat_address", config.getHeartbeatAddress());
+        connConfig.put("connect_timeout", config.getConnectTimeout());
+
+        try {
+            driver.connect(connConfig);
+            log.info("Modbus TCP驱动创建成功: {}:{}", conn != null ? conn.getIp() : "?", conn != null ? conn.getPort() : "?");
+        } catch (Exception e) {
+            log.error("Modbus TCP驱动创建失败: {}", e.getMessage());
+            return new NullDriver();
+        }
+
+        return driver;
+    }
+
+    /**
+     * 创建S7驱动
+     */
+    private static IDeviceDriver createS7Driver(StationConfig config, StationConfig.DeviceConnection conn) {
+        S7Driver driver = new S7Driver();
+
+        Map<String, Object> connConfig = new HashMap<>();
+        if (conn != null) {
+            connConfig.put("ip", conn.getIp());
+            connConfig.put("port", conn.getPort());
+        }
+
+        // 配置地址
+        StationConfig.DeviceAddresses addresses = config.getDeviceAddresses();
+        if (addresses != null) {
+            connConfig.put("enable_signal_address", addresses.getEnableSignal());
+            connConfig.put("start_signal_address", addresses.getStartSignal());
+            connConfig.put("complete_signal_address", addresses.getCompleteSignal());
+        }
+
+        try {
+            driver.connect(connConfig);
+            log.info("S7驱动创建成功: {}", conn != null ? conn.getIp() : "?");
+        } catch (Exception e) {
+            log.error("S7驱动创建失败: {}", e.getMessage());
+            return new NullDriver();
+        }
+
+        return driver;
+    }
+
+    /**
+     * 创建继电器驱动
+     */
+    public static RelayDriver createRelayDriver(StationConfig config) {
+        StationConfig.RelayConfig relayConfig = config.getRelayConfig();
+        if (relayConfig == null || !relayConfig.isEnabled()) {
+            return null;
+        }
+
+        RelayDriver driver = new RelayDriver();
+        Map<String, Object> connConfig = new HashMap<>();
+        connConfig.put("port", relayConfig.getPort());
+        connConfig.put("baudrate", relayConfig.getBaudrate());
+        connConfig.put("open_command", relayConfig.getOpenCommand());
+        connConfig.put("close_command", relayConfig.getCloseCommand());
+
+        try {
+            driver.connect(connConfig);
+            log.info("继电器驱动创建成功: {}", relayConfig.getPort());
+        } catch (Exception e) {
+            log.error("继电器驱动创建失败: {}", e.getMessage());
+            return null;
+        }
+
+        return driver;
+    }
+
+    /**
+     * 批量创建设备驱动(用于多工位)
+     * @param config 配置
+     * @return 工位索引 -> 驱动实例映射
+     */
+    public static Map<Integer, IDeviceDriver> createDrivers(StationConfig config) {
+        Map<Integer, IDeviceDriver> drivers = new HashMap<>();
+
+        for (int i = 0; i < config.getStationCount(); i++) {
+            IDeviceDriver driver = createDriver(config, i);
+            drivers.put(i, driver);
+        }
+
+        return drivers;
+    }
+}

+ 152 - 0
src/com/mes/device/IDeviceDriver.java

@@ -0,0 +1,152 @@
+package com.mes.device;
+
+import java.util.Map;
+
+/**
+ * 设备驱动接口 - 抽象设备通信行为
+ * 支持Modbus TCP、Modbus RTU、S7等协议
+ */
+public interface IDeviceDriver {
+
+    /**
+     * 连接设备
+     * @param config 连接配置(ip, port, 串口等)
+     */
+    void connect(Map<String, Object> config);
+
+    /**
+     * 断开连接
+     */
+    void disconnect();
+
+    /**
+     * 检查是否已连接
+     */
+    boolean isConnected();
+
+    /**
+     * 发送就绪信号(通知设备可以开始加工)
+     */
+    void signalReady();
+
+    /**
+     * 启动心跳
+     */
+    void startHeartbeat();
+
+    /**
+     * 停止心跳
+     */
+    void stopHeartbeat();
+
+    /**
+     * 读取设备状态
+     * @return 状态数据Map
+     */
+    Map<String, Object> readStatus();
+
+    /**
+     * 检查加工是否完成
+     */
+    boolean isWorkComplete();
+
+    /**
+     * 获取预设数量
+     */
+    int getPresetCount();
+
+    /**
+     * 获取完成数量
+     */
+    int getFinishedCount();
+
+    /**
+     * 获取合格数量
+     */
+    int getQualifiedCount();
+
+    /**
+     * 设置预设数量
+     */
+    void setPresetCount(int count);
+
+    /**
+     * 写入寄存器
+     * @param address 地址
+     * @param value 值
+     */
+    void writeRegister(int address, int value);
+
+    /**
+     * 读取寄存器
+     * @param address 地址
+     * @return 值
+     */
+    int readRegister(int address);
+
+    /**
+     * 获取驱动类型
+     */
+    String getDriverType();
+
+    // ========== 新增方法 ==========
+
+    /**
+     * 读取位(布尔值)
+     * @param address 地址(S7格式如 DB1.2.2 或 Modbus格式如 15001)
+     * @return 位值
+     */
+    default boolean readBit(String address) {
+        return false;
+    }
+
+    /**
+     * 写入位(布尔值)
+     * @param address 地址
+     * @param value 值
+     */
+    default void writeBit(String address, boolean value) {
+    }
+
+    /**
+     * 读取整数
+     * @param address 地址
+     * @return 整数值
+     */
+    default int readInt(String address) {
+        return 0;
+    }
+
+    /**
+     * 写入整数
+     * @param address 地址
+     * @param value 值
+     */
+    default void writeInt(String address, int value) {
+    }
+
+    /**
+     * 检查设备是否已启动(读取启动信号)
+     */
+    default boolean isDeviceStarted() {
+        return false;
+    }
+
+    /**
+     * 发送启动命令
+     */
+    default void sendStartCommand() {
+    }
+
+    /**
+     * 发送停止命令
+     */
+    default void sendStopCommand() {
+    }
+
+    /**
+     * 重置设备状态(清零计数等)
+     */
+    default void resetDevice() {
+    }
+}

+ 410 - 0
src/com/mes/device/ModbusTcpDriver.java

@@ -0,0 +1,410 @@
+package com.mes.device;
+
+import com.github.xingshuangs.iot.protocol.modbus.service.ModbusTcp;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Timer;
+import java.util.TimerTask;
+
+/**
+ * Modbus TCP设备驱动
+ * 用于与PLC通信(如拉铆工位)
+ */
+public class ModbusTcpDriver implements IDeviceDriver {
+    private static final Logger log = LoggerFactory.getLogger(ModbusTcpDriver.class);
+
+    private ModbusTcp plc;
+    private String ip;
+    private int port = 502;
+
+    // 地址配置
+    private int readySignalAddress = 8;      // 就绪信号地址
+    private int heartbeatAddress = 4160;     // 心跳地址
+    private int presetCountAddress = 4534;   // 预设数量地址
+    private int finishedCountAddress = 66;   // 完成数量地址
+    private int qualifiedCountAddress = 68;  // 合格数量地址
+    private int completeSignalAddress = 0;   // 完成信号地址(需要根据实际配置)
+
+    // 心跳
+    private Timer heartbeatTimer;
+    private short heartbeatValue = 1;
+    private int heartbeatIntervalMs = 500;
+
+    // 超时配置
+    private int connectTimeout = 3000;     // 连接超时(ms)
+
+    // 状态
+    private volatile boolean connected = false;
+
+    public ModbusTcpDriver() {
+    }
+
+    public ModbusTcpDriver(String ip, int port) {
+        this.ip = ip;
+        this.port = port;
+    }
+
+    @Override
+    public void connect(Map<String, Object> config) {
+        if (config != null) {
+            if (config.containsKey("ip")) {
+                this.ip = config.get("ip").toString();
+            }
+            if (config.containsKey("port")) {
+                this.port = ((Number) config.get("port")).intValue();
+            }
+            if (config.containsKey("connect_timeout")) {
+                this.connectTimeout = ((Number) config.get("connect_timeout")).intValue();
+            }
+            // 配置地址
+            if (config.containsKey("ready_signal_address")) {
+                this.readySignalAddress = ((Number) config.get("ready_signal_address")).intValue();
+            }
+            if (config.containsKey("heartbeat_address")) {
+                this.heartbeatAddress = ((Number) config.get("heartbeat_address")).intValue();
+            }
+        }
+
+        try {
+            log.info("连接PLC: {}:{}, 超时={}ms", ip, port, connectTimeout);
+            plc = new ModbusTcp(1, ip, port);
+            plc.setConnectTimeout(connectTimeout);
+            connected = true;
+            log.info("PLC连接成功");
+        } catch (Exception e) {
+            log.error("PLC连接失败: {}", e.getMessage());
+            connected = false;
+            throw new RuntimeException("PLC连接失败: " + e.getMessage(), e);
+        }
+    }
+
+    @Override
+    public void disconnect() {
+        stopHeartbeat();
+        if (plc != null) {
+            try {
+                plc.close();
+            } catch (Exception e) {
+                log.error("关闭PLC连接异常: {}", e.getMessage());
+            }
+            plc = null;
+        }
+        connected = false;
+        log.info("PLC连接已断开");
+    }
+
+    @Override
+    public boolean isConnected() {
+        return connected && plc != null;
+    }
+
+    @Override
+    public void signalReady() {
+        if (!isConnected()) {
+            log.warn("PLC未连接,无法发送就绪信号");
+            return;
+        }
+
+        try {
+            plc.writeInt16(readySignalAddress, (short) 1);
+            log.info("已向地址{}写入就绪信号1", readySignalAddress);
+
+            // 验证写入
+            Short value = plc.readInt16(readySignalAddress);
+            log.debug("验证读取地址{}的值: {}", readySignalAddress, value);
+        } catch (Exception e) {
+            log.error("发送就绪信号失败: {}", e.getMessage());
+        }
+    }
+
+    @Override
+    public void startHeartbeat() {
+        stopHeartbeat();
+
+        heartbeatTimer = new Timer("PLCHeartbeat", true);
+        heartbeatTimer.schedule(new TimerTask() {
+            @Override
+            public void run() {
+                if (isConnected()) {
+                    try {
+                        plc.writeInt16(heartbeatAddress, heartbeatValue);
+                        // 心跳值循环: 1 -> 2 -> 3 -> 1
+                        heartbeatValue = (short) (heartbeatValue % 3 + 1);
+                    } catch (Exception e) {
+                        log.error("心跳写入失败: {}", e.getMessage());
+                    }
+                }
+            }
+        }, 100, heartbeatIntervalMs);
+
+        log.info("PLC心跳已启动,间隔{}ms", heartbeatIntervalMs);
+    }
+
+    @Override
+    public void stopHeartbeat() {
+        if (heartbeatTimer != null) {
+            heartbeatTimer.cancel();
+            heartbeatTimer = null;
+            log.info("PLC心跳已停止");
+        }
+    }
+
+    @Override
+    public Map<String, Object> readStatus() {
+        Map<String, Object> status = new HashMap<>();
+        if (!isConnected()) {
+            return status;
+        }
+
+        try {
+            status.put("preset", getPresetCount());
+            status.put("finished", getFinishedCount());
+            status.put("qualified", getQualifiedCount());
+        } catch (Exception e) {
+            log.error("读取PLC状态失败: {}", e.getMessage());
+        }
+
+        return status;
+    }
+
+    @Override
+    public boolean isWorkComplete() {
+        if (!isConnected()) {
+            return false;
+        }
+
+        try {
+            int finished = getFinishedCount();
+            int preset = getPresetCount();
+            // 完成数量达到预设数量视为完成
+            return preset > 0 && finished >= preset;
+        } catch (Exception e) {
+            log.error("检查工作完成状态失败: {}", e.getMessage());
+            return false;
+        }
+    }
+
+    @Override
+    public int getPresetCount() {
+        if (!isConnected()) {
+            return 0;
+        }
+        try {
+            return plc.readInt32(presetCountAddress);
+        } catch (Exception e) {
+            log.error("读取预设数量失败: {}", e.getMessage());
+            return 0;
+        }
+    }
+
+    @Override
+    public int getFinishedCount() {
+        if (!isConnected()) {
+            return 0;
+        }
+        try {
+            return plc.readInt16(finishedCountAddress);
+        } catch (Exception e) {
+            log.error("读取完成数量失败: {}", e.getMessage());
+            return 0;
+        }
+    }
+
+    @Override
+    public int getQualifiedCount() {
+        if (!isConnected()) {
+            return 0;
+        }
+        try {
+            return plc.readInt16(qualifiedCountAddress);
+        } catch (Exception e) {
+            log.error("读取合格数量失败: {}", e.getMessage());
+            return 0;
+        }
+    }
+
+    @Override
+    public void setPresetCount(int count) {
+        if (!isConnected()) {
+            return;
+        }
+        try {
+            plc.writeInt32(presetCountAddress, count);
+            log.info("设置预设数量: {}", count);
+        } catch (Exception e) {
+            log.error("设置预设数量失败: {}", e.getMessage());
+        }
+    }
+
+    @Override
+    public void writeRegister(int address, int value) {
+        if (!isConnected()) {
+            return;
+        }
+        try {
+            plc.writeInt16(address, (short) value);
+            log.debug("写入地址{}: {}", address, value);
+        } catch (Exception e) {
+            log.error("写入寄存器失败: {}", e.getMessage());
+        }
+    }
+
+    @Override
+    public int readRegister(int address) {
+        if (!isConnected()) {
+            return 0;
+        }
+        try {
+            return plc.readInt16(address);
+        } catch (Exception e) {
+            log.error("读取寄存器失败: {}", e.getMessage());
+            return 0;
+        }
+    }
+
+    @Override
+    public String getDriverType() {
+        return "modbus_tcp";
+    }
+
+    // ========== 新增方法实现 ==========
+
+    @Override
+    public boolean readBit(String address) {
+        if (!isConnected()) {
+            return false;
+        }
+        try {
+            int addr = Integer.parseInt(address);
+            return plc.readInt16(addr) != 0;
+        } catch (Exception e) {
+            log.error("读取位失败: {}", e.getMessage());
+            return false;
+        }
+    }
+
+    @Override
+    public void writeBit(String address, boolean value) {
+        if (!isConnected()) {
+            return;
+        }
+        try {
+            int addr = Integer.parseInt(address);
+            plc.writeInt16(addr, (short) (value ? 1 : 0));
+        } catch (Exception e) {
+            log.error("写入位失败: {}", e.getMessage());
+        }
+    }
+
+    @Override
+    public int readInt(String address) {
+        if (!isConnected()) {
+            return 0;
+        }
+        try {
+            int addr = Integer.parseInt(address);
+            return plc.readInt16(addr);
+        } catch (Exception e) {
+            log.error("读取整数失败: {}", e.getMessage());
+            return 0;
+        }
+    }
+
+    @Override
+    public void writeInt(String address, int value) {
+        if (!isConnected()) {
+            return;
+        }
+        try {
+            int addr = Integer.parseInt(address);
+            plc.writeInt16(addr, (short) value);
+        } catch (Exception e) {
+            log.error("写入整数失败: {}", e.getMessage());
+        }
+    }
+
+    @Override
+    public void sendStartCommand() {
+        if (!isConnected()) {
+            return;
+        }
+        try {
+            plc.writeInt16(readySignalAddress, (short) 1);
+            log.info("发送启动命令到地址{}", readySignalAddress);
+        } catch (Exception e) {
+            log.error("发送启动命令失败: {}", e.getMessage());
+        }
+    }
+
+    @Override
+    public void sendStopCommand() {
+        if (!isConnected()) {
+            return;
+        }
+        try {
+            plc.writeInt16(readySignalAddress, (short) 0);
+            log.info("发送停止命令到地址{}", readySignalAddress);
+        } catch (Exception e) {
+            log.error("发送停止命令失败: {}", e.getMessage());
+        }
+    }
+
+    @Override
+    public void resetDevice() {
+        if (!isConnected()) {
+            return;
+        }
+        try {
+            // 重置完成数量
+            plc.writeInt16(finishedCountAddress, (short) 0);
+            log.info("重置设备状态");
+        } catch (Exception e) {
+            log.error("重置设备失败: {}", e.getMessage());
+        }
+    }
+
+    // ========== Getters & Setters ==========
+
+    public String getIp() {
+        return ip;
+    }
+
+    public void setIp(String ip) {
+        this.ip = ip;
+    }
+
+    public int getPort() {
+        return port;
+    }
+
+    public void setPort(int port) {
+        this.port = port;
+    }
+
+    public int getReadySignalAddress() {
+        return readySignalAddress;
+    }
+
+    public void setReadySignalAddress(int readySignalAddress) {
+        this.readySignalAddress = readySignalAddress;
+    }
+
+    public int getHeartbeatAddress() {
+        return heartbeatAddress;
+    }
+
+    public void setHeartbeatAddress(int heartbeatAddress) {
+        this.heartbeatAddress = heartbeatAddress;
+    }
+
+    public int getHeartbeatIntervalMs() {
+        return heartbeatIntervalMs;
+    }
+
+    public void setHeartbeatIntervalMs(int heartbeatIntervalMs) {
+        this.heartbeatIntervalMs = heartbeatIntervalMs;
+    }
+}

+ 90 - 0
src/com/mes/device/NullDriver.java

@@ -0,0 +1,90 @@
+package com.mes.device;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 空设备驱动 - 用于无设备的手动工位
+ * 所有操作为空实现或返回默认值
+ */
+public class NullDriver implements IDeviceDriver {
+    private static final Logger log = LoggerFactory.getLogger(NullDriver.class);
+
+    @Override
+    public void connect(Map<String, Object> config) {
+        log.debug("NullDriver: connect (no-op)");
+    }
+
+    @Override
+    public void disconnect() {
+        log.debug("NullDriver: disconnect (no-op)");
+    }
+
+    @Override
+    public boolean isConnected() {
+        return true;  // 总是返回已连接
+    }
+
+    @Override
+    public void signalReady() {
+        log.debug("NullDriver: signalReady (no-op)");
+    }
+
+    @Override
+    public void startHeartbeat() {
+        log.debug("NullDriver: startHeartbeat (no-op)");
+    }
+
+    @Override
+    public void stopHeartbeat() {
+        log.debug("NullDriver: stopHeartbeat (no-op)");
+    }
+
+    @Override
+    public Map<String, Object> readStatus() {
+        return new HashMap<>();
+    }
+
+    @Override
+    public boolean isWorkComplete() {
+        return true;  // 无设备时总是视为完成
+    }
+
+    @Override
+    public int getPresetCount() {
+        return 0;
+    }
+
+    @Override
+    public int getFinishedCount() {
+        return 0;
+    }
+
+    @Override
+    public int getQualifiedCount() {
+        return 0;
+    }
+
+    @Override
+    public void setPresetCount(int count) {
+        log.debug("NullDriver: setPresetCount (no-op)");
+    }
+
+    @Override
+    public void writeRegister(int address, int value) {
+        log.debug("NullDriver: writeRegister (no-op)");
+    }
+
+    @Override
+    public int readRegister(int address) {
+        return 0;
+    }
+
+    @Override
+    public String getDriverType() {
+        return "none";
+    }
+}

+ 218 - 0
src/com/mes/device/RelayDriver.java

@@ -0,0 +1,218 @@
+package com.mes.device;
+
+import com.fazecast.jSerialComm.SerialPort;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.math.BigInteger;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 继电器驱动(Modbus RTU串口通信)
+ * 用于控制继电器开关
+ */
+public class RelayDriver implements IDeviceDriver {
+    private static final Logger log = LoggerFactory.getLogger(RelayDriver.class);
+
+    private SerialPort serialPort;
+    private String portName = "COM2";
+    private int baudrate = 38400;
+    private String openCommand = "010600000000";   // 开继电器命令
+    private String closeCommand = "010600000001";  // 关继电器命令
+
+    private volatile boolean connected = false;
+
+    public RelayDriver() {
+    }
+
+    public RelayDriver(String portName, int baudrate) {
+        this.portName = portName;
+        this.baudrate = baudrate;
+    }
+
+    @Override
+    public void connect(Map<String, Object> config) {
+        if (config != null) {
+            if (config.containsKey("port")) {
+                this.portName = config.get("port").toString();
+            }
+            if (config.containsKey("baudrate")) {
+                this.baudrate = ((Number) config.get("baudrate")).intValue();
+            }
+            if (config.containsKey("open_command")) {
+                this.openCommand = config.get("open_command").toString();
+            }
+            if (config.containsKey("close_command")) {
+                this.closeCommand = config.get("close_command").toString();
+            }
+        }
+
+        try {
+            serialPort = SerialPort.getCommPort(portName);
+            serialPort.setComPortParameters(baudrate, 8, SerialPort.ONE_STOP_BIT, SerialPort.NO_PARITY);
+            serialPort.setComPortTimeouts(SerialPort.TIMEOUT_READ_BLOCKING, 1000, 0);
+
+            if (serialPort.openPort()) {
+                connected = true;
+                log.info("继电器串口{}打开成功", portName);
+            } else {
+                connected = false;
+                log.error("继电器串口{}打开失败", portName);
+            }
+        } catch (Exception e) {
+            log.error("继电器连接失败: {}", e.getMessage());
+            connected = false;
+        }
+    }
+
+    @Override
+    public void disconnect() {
+        if (serialPort != null && serialPort.isOpen()) {
+            serialPort.closePort();
+            log.info("继电器串口已关闭");
+        }
+        connected = false;
+    }
+
+    @Override
+    public boolean isConnected() {
+        return connected && serialPort != null && serialPort.isOpen();
+    }
+
+    /**
+     * 开继电器
+     */
+    public void open() {
+        sendCommand(openCommand);
+        log.info("继电器已开启");
+    }
+
+    /**
+     * 关继电器
+     */
+    public void close() {
+        sendCommand(closeCommand);
+        log.info("继电器已关闭");
+    }
+
+    /**
+     * 发送命令
+     */
+    private void sendCommand(String command) {
+        if (!isConnected()) {
+            reconnect();
+        }
+        if (!isConnected()) {
+            log.error("继电器未连接,无法发送命令");
+            return;
+        }
+
+        try {
+            String crc = getCRC16ModbusStr(command);
+            byte[] writeBuffer = hexStringToByteArray(command + crc);
+            serialPort.writeBytes(writeBuffer, writeBuffer.length);
+
+            // 读取响应
+            byte[] readBuffer = new byte[512];
+            int numRead = serialPort.readBytes(readBuffer, readBuffer.length);
+            log.debug("继电器响应: {} bytes", numRead);
+        } catch (Exception e) {
+            log.error("发送继电器命令失败: {}", e.getMessage());
+        }
+    }
+
+    /**
+     * 重新连接
+     */
+    private void reconnect() {
+        disconnect();
+        Map<String, Object> config = new HashMap<>();
+        config.put("port", portName);
+        config.put("baudrate", baudrate);
+        connect(config);
+    }
+
+    /**
+     * CRC16 Modbus校验
+     */
+    private String getCRC16ModbusStr(String data) {
+        data = data.replace(" ", "");
+        int len = data.length();
+        if (len % 2 != 0) {
+            return "0000";
+        }
+        int num = len / 2;
+        byte[] para = new byte[num];
+        for (int i = 0; i < num; i++) {
+            int value = Integer.valueOf(data.substring(i * 2, 2 * (i + 1)), 16);
+            para[i] = (byte) value;
+        }
+        return getCRC(para);
+    }
+
+    private String getCRC(byte[] bytes) {
+        int CRC = 0x0000ffff;
+        int POLYNOMIAL = 0x0000a001;
+        for (byte b : bytes) {
+            CRC ^= ((int) b & 0x000000ff);
+            for (int j = 0; j < 8; j++) {
+                if ((CRC & 0x00000001) != 0) {
+                    CRC >>= 1;
+                    CRC ^= POLYNOMIAL;
+                } else {
+                    CRC >>= 1;
+                }
+            }
+        }
+        String result = Integer.toHexString(CRC).toUpperCase();
+        if (result.length() != 4) {
+            StringBuilder sb = new StringBuilder("0000");
+            result = sb.replace(4 - result.length(), 4, result).toString();
+        }
+        return result.substring(2, 4) + result.substring(0, 2);
+    }
+
+    private byte[] hexStringToByteArray(String hexString) {
+        hexString = hexString.trim();
+        if (hexString.length() % 2 != 0) {
+            throw new IllegalArgumentException("Invalid hex string");
+        }
+        BigInteger bigInt = new BigInteger(hexString, 16);
+        byte[] byteArray = bigInt.toByteArray();
+        if (byteArray.length > hexString.length() / 2) {
+            byte[] trimmedArray = new byte[hexString.length() / 2];
+            System.arraycopy(byteArray, 1, trimmedArray, 0, trimmedArray.length);
+            return trimmedArray;
+        }
+        return byteArray;
+    }
+
+    // 以下方法继电器驱动不支持
+    @Override public void signalReady() { }
+    @Override public void startHeartbeat() { }
+    @Override public void stopHeartbeat() { }
+    @Override public Map<String, Object> readStatus() { return new HashMap<>(); }
+    @Override public boolean isWorkComplete() { return false; }
+    @Override public int getPresetCount() { return 0; }
+    @Override public int getFinishedCount() { return 0; }
+    @Override public int getQualifiedCount() { return 0; }
+    @Override public void setPresetCount(int count) { }
+    @Override public void writeRegister(int address, int value) { }
+    @Override public int readRegister(int address) { return 0; }
+
+    @Override
+    public String getDriverType() {
+        return "relay";
+    }
+
+    // Getters & Setters
+    public String getPortName() { return portName; }
+    public void setPortName(String portName) { this.portName = portName; }
+    public int getBaudrate() { return baudrate; }
+    public void setBaudrate(int baudrate) { this.baudrate = baudrate; }
+    public String getOpenCommand() { return openCommand; }
+    public void setOpenCommand(String openCommand) { this.openCommand = openCommand; }
+    public String getCloseCommand() { return closeCommand; }
+    public void setCloseCommand(String closeCommand) { this.closeCommand = closeCommand; }
+}

+ 282 - 0
src/com/mes/device/S7Driver.java

@@ -0,0 +1,282 @@
+package com.mes.device;
+
+import com.github.xingshuangs.iot.protocol.s7.enums.EPlcType;
+import com.github.xingshuangs.iot.protocol.s7.service.S7PLC;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Timer;
+import java.util.TimerTask;
+
+/**
+ * 西门子S7协议设备驱动
+ * 用于与西门子PLC通信(如OP60-CMT、OP120-Siemens)
+ */
+public class S7Driver implements IDeviceDriver {
+    private static final Logger log = LoggerFactory.getLogger(S7Driver.class);
+
+    private S7PLC plc;
+    private String ip;
+    private int port = 102;
+    private EPlcType plcType = EPlcType.S1200;  // 默认S1200
+
+    // 地址配置
+    private String enableSignalAddress;     // 允许信号地址(如 DB9.18.1)
+    private String startSignalAddress;      // 启动信号地址(如 DB1.2.2)
+    private String completeSignalAddress;   // 完成信号地址(如 DB1.2.5)
+
+    // 心跳
+    private Timer heartbeatTimer;
+    private int heartbeatAddress;
+    private int heartbeatIntervalMs = 500;
+    private short heartbeatValue = 1;
+
+    // 状态
+    private volatile boolean connected = false;
+
+    public S7Driver() {
+    }
+
+    public S7Driver(String ip) {
+        this.ip = ip;
+    }
+
+    @Override
+    public void connect(Map<String, Object> config) {
+        if (config != null) {
+            if (config.containsKey("ip")) {
+                this.ip = config.get("ip").toString();
+            }
+            if (config.containsKey("port")) {
+                this.port = ((Number) config.get("port")).intValue();
+            }
+            if (config.containsKey("plc_type")) {
+                String typeStr = config.get("plc_type").toString().toUpperCase();
+                try {
+                    this.plcType = EPlcType.valueOf(typeStr);
+                } catch (Exception e) {
+                    log.warn("未知的PLC类型: {}, 使用默认S1200", typeStr);
+                }
+            }
+            // 地址配置
+            if (config.containsKey("enable_signal_address")) {
+                this.enableSignalAddress = config.get("enable_signal_address").toString();
+            }
+            if (config.containsKey("start_signal_address")) {
+                this.startSignalAddress = config.get("start_signal_address").toString();
+            }
+            if (config.containsKey("complete_signal_address")) {
+                this.completeSignalAddress = config.get("complete_signal_address").toString();
+            }
+            if (config.containsKey("heartbeat_address")) {
+                this.heartbeatAddress = ((Number) config.get("heartbeat_address")).intValue();
+            }
+        }
+
+        try {
+            log.info("连接S7 PLC: {}:{}, 类型: {}", ip, port, plcType);
+            plc = new S7PLC(plcType, ip, port);
+            connected = true;
+            log.info("S7 PLC连接成功");
+        } catch (Exception e) {
+            log.error("S7 PLC连接失败: {}", e.getMessage());
+            connected = false;
+            throw new RuntimeException("S7 PLC连接失败: " + e.getMessage(), e);
+        }
+    }
+
+    @Override
+    public void disconnect() {
+        stopHeartbeat();
+        if (plc != null) {
+            try {
+                plc.close();
+            } catch (Exception e) {
+                log.error("关闭S7 PLC连接异常: {}", e.getMessage());
+            }
+            plc = null;
+        }
+        connected = false;
+        log.info("S7 PLC连接已断开");
+    }
+
+    @Override
+    public boolean isConnected() {
+        return connected && plc != null;
+    }
+
+    @Override
+    public void signalReady() {
+        if (!isConnected() || enableSignalAddress == null) {
+            return;
+        }
+        try {
+            plc.writeBoolean(enableSignalAddress, true);
+            log.info("已向地址{}写入允许信号", enableSignalAddress);
+        } catch (Exception e) {
+            log.error("发送允许信号失败: {}", e.getMessage());
+        }
+    }
+
+    @Override
+    public void startHeartbeat() {
+        if (heartbeatAddress <= 0) {
+            return;
+        }
+        stopHeartbeat();
+        heartbeatTimer = new Timer("S7Heartbeat", true);
+        heartbeatTimer.schedule(new TimerTask() {
+            @Override
+            public void run() {
+                if (isConnected()) {
+                    try {
+                        plc.writeInt16("DB" + heartbeatAddress + ".0", heartbeatValue);
+                        heartbeatValue = (short) (heartbeatValue % 3 + 1);
+                    } catch (Exception e) {
+                        log.error("心跳写入失败: {}", e.getMessage());
+                    }
+                }
+            }
+        }, 100, heartbeatIntervalMs);
+        log.info("S7心跳已启动,间隔{}ms", heartbeatIntervalMs);
+    }
+
+    @Override
+    public void stopHeartbeat() {
+        if (heartbeatTimer != null) {
+            heartbeatTimer.cancel();
+            heartbeatTimer = null;
+            log.info("S7心跳已停止");
+        }
+    }
+
+    @Override
+    public Map<String, Object> readStatus() {
+        Map<String, Object> status = new HashMap<>();
+        if (!isConnected()) {
+            return status;
+        }
+        try {
+            if (startSignalAddress != null) {
+                status.put("started", plc.readBoolean(startSignalAddress));
+            }
+            if (completeSignalAddress != null) {
+                status.put("completed", plc.readBoolean(completeSignalAddress));
+            }
+        } catch (Exception e) {
+            log.error("读取S7状态失败: {}", e.getMessage());
+        }
+        return status;
+    }
+
+    @Override
+    public boolean isWorkComplete() {
+        if (!isConnected() || completeSignalAddress == null) {
+            return false;
+        }
+        try {
+            return plc.readBoolean(completeSignalAddress);
+        } catch (Exception e) {
+            log.error("检查完成状态失败: {}", e.getMessage());
+            return false;
+        }
+    }
+
+    @Override
+    public boolean isDeviceStarted() {
+        if (!isConnected() || startSignalAddress == null) {
+            return false;
+        }
+        try {
+            return plc.readBoolean(startSignalAddress);
+        } catch (Exception e) {
+            log.error("检查启动状态失败: {}", e.getMessage());
+            return false;
+        }
+    }
+
+    @Override
+    public boolean readBit(String address) {
+        if (!isConnected()) {
+            return false;
+        }
+        try {
+            return plc.readBoolean(address);
+        } catch (Exception e) {
+            log.error("读取位{}失败: {}", address, e.getMessage());
+            return false;
+        }
+    }
+
+    @Override
+    public void writeBit(String address, boolean value) {
+        if (!isConnected()) {
+            return;
+        }
+        try {
+            plc.writeBoolean(address, value);
+            log.debug("写入位{}: {}", address, value);
+        } catch (Exception e) {
+            log.error("写入位{}失败: {}", address, e.getMessage());
+        }
+    }
+
+    @Override
+    public int readInt(String address) {
+        if (!isConnected()) {
+            return 0;
+        }
+        try {
+            return plc.readInt16(address);
+        } catch (Exception e) {
+            log.error("读取整数{}失败: {}", address, e.getMessage());
+            return 0;
+        }
+    }
+
+    @Override
+    public void writeInt(String address, int value) {
+        if (!isConnected()) {
+            return;
+        }
+        try {
+            plc.writeInt16(address, (short) value);
+            log.debug("写入整数{}: {}", address, value);
+        } catch (Exception e) {
+            log.error("写入整数{}失败: {}", address, e.getMessage());
+        }
+    }
+
+    // 以下方法S7驱动暂不支持,返回默认值
+    @Override
+    public int getPresetCount() { return 0; }
+    @Override
+    public int getFinishedCount() { return 0; }
+    @Override
+    public int getQualifiedCount() { return 0; }
+    @Override
+    public void setPresetCount(int count) { }
+    @Override
+    public void writeRegister(int address, int value) { }
+    @Override
+    public int readRegister(int address) { return 0; }
+
+    @Override
+    public String getDriverType() {
+        return "s7";
+    }
+
+    // Getters & Setters
+    public String getIp() { return ip; }
+    public void setIp(String ip) { this.ip = ip; }
+    public int getPort() { return port; }
+    public void setPort(int port) { this.port = port; }
+    public String getEnableSignalAddress() { return enableSignalAddress; }
+    public void setEnableSignalAddress(String enableSignalAddress) { this.enableSignalAddress = enableSignalAddress; }
+    public String getStartSignalAddress() { return startSignalAddress; }
+    public void setStartSignalAddress(String startSignalAddress) { this.startSignalAddress = startSignalAddress; }
+    public String getCompleteSignalAddress() { return completeSignalAddress; }
+    public void setCompleteSignalAddress(String completeSignalAddress) { this.completeSignalAddress = completeSignalAddress; }
+}

+ 222 - 0
src/com/mes/step/AbstractStep.java

@@ -0,0 +1,222 @@
+package com.mes.step;
+
+import com.mes.core.StationContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 流程步骤抽象基类 - 提供通用实现
+ * 具体步骤类继承此类,只需实现核心逻辑
+ */
+public abstract class AbstractStep implements IWorkflowStep {
+    protected final Logger log = LoggerFactory.getLogger(getClass());
+
+    protected String stepId;
+    protected String stepName;
+    protected String messageType;
+    protected String craft;
+    protected boolean async;
+    protected boolean requiresUserInteraction;
+    protected int timeoutMs;
+    protected Map<String, Object> config;
+    protected AsyncCompleteCallback asyncCompleteCallback;
+
+    public AbstractStep(String stepId, String stepName) {
+        this.stepId = stepId;
+        this.stepName = stepName;
+        this.config = new HashMap<>();
+        this.async = false;
+        this.requiresUserInteraction = false;
+        this.timeoutMs = 0; // 默认无超时
+    }
+
+    @Override
+    public String getStepId() {
+        return stepId;
+    }
+
+    @Override
+    public String getStepName() {
+        return stepName;
+    }
+
+    @Override
+    public String getMessageType() {
+        return messageType;
+    }
+
+    @Override
+    public boolean isAsync() {
+        return async;
+    }
+
+    @Override
+    public boolean requiresUserInteraction() {
+        return requiresUserInteraction;
+    }
+
+    @Override
+    public int getTimeoutMs() {
+        return timeoutMs;
+    }
+
+    @Override
+    public void configure(Map<String, Object> config) {
+        if (config != null) {
+            this.config.putAll(config);
+        }
+    }
+
+    @Override
+    public void setCraft(String craft) {
+        this.craft = craft;
+    }
+
+    @Override
+    public String getCraft() {
+        return craft;
+    }
+
+    @Override
+    public void onServerResponse(StationContext context, String result, String message) {
+        // 默认实现:根据结果码判断成功/失败
+        if ("OK".equalsIgnoreCase(result) || "UD".equalsIgnoreCase(result)) {
+            log.info("[{}] 步骤 {} 服务端响应成功: {}", context.getStationCode(), stepId, result);
+            onSuccess(context);
+        } else {
+            String errorMsg = getErrorMessage(result, message);
+            log.warn("[{}] 步骤 {} 服务端响应失败: {}", context.getStationCode(), stepId, errorMsg);
+            onFailure(context, errorMsg);
+        }
+    }
+
+    @Override
+    public void onSuccess(StationContext context) {
+        log.info("[{}] 步骤 {} 执行成功", context.getStationCode(), stepId);
+    }
+
+    @Override
+    public void onFailure(StationContext context, String error) {
+        log.error("[{}] 步骤 {} 执行失败: {}", context.getStationCode(), stepId, error);
+        // 如果已有错误消息(level=-1),不覆盖它(保留更具体的错误信息)
+        if (context.getStatusLevel() != -1) {
+            context.setStatusMessage(error, -1);
+        } else {
+            log.debug("[{}] 保留原错误消息: {}", context.getStationCode(), context.getStatusMessage());
+        }
+    }
+
+    /**
+     * 获取配置值(字符串)
+     */
+    protected String getConfigString(String key, String defaultValue) {
+        Object value = config.get(key);
+        return value != null ? value.toString() : defaultValue;
+    }
+
+    /**
+     * 获取配置值(整数)
+     */
+    protected int getConfigInt(String key, int defaultValue) {
+        Object value = config.get(key);
+        if (value instanceof Number) {
+            return ((Number) value).intValue();
+        }
+        if (value instanceof String) {
+            try {
+                return Integer.parseInt((String) value);
+            } catch (NumberFormatException e) {
+                return defaultValue;
+            }
+        }
+        return defaultValue;
+    }
+
+    /**
+     * 获取配置值(布尔)
+     */
+    protected boolean getConfigBoolean(String key, boolean defaultValue) {
+        Object value = config.get(key);
+        if (value instanceof Boolean) {
+            return (Boolean) value;
+        }
+        if (value instanceof String) {
+            return Boolean.parseBoolean((String) value);
+        }
+        return defaultValue;
+    }
+
+    /**
+     * 根据错误码获取错误消息
+     * 可以在子类中覆盖以提供更详细的错误信息
+     */
+    protected String getErrorMessage(String resultCode, String message) {
+        // 通用错误码映射
+        switch (resultCode.toUpperCase()) {
+            case "QD":
+                return "前道工序未完成";
+            case "CF":
+                return "重复提交";
+            case "NE":
+                return "工件不存在";
+            case "NG":
+                return "检测不合格";
+            case "BD":
+                return "绑定失败";
+            case "VF":
+                return "校验失败";
+            case "ER":
+                return "系统错误";
+            default:
+                return "操作失败: " + resultCode;
+        }
+    }
+
+    @Override
+    public String toString() {
+        return String.format("%s[%s]", stepName, stepId);
+    }
+
+    // ========== 流程监控扩展方法 ==========
+
+    /**
+     * 获取执行条件列表(默认实现,子类可覆盖)
+     */
+    @Override
+    public java.util.List<StepCondition> getConditions(StationContext context) {
+        return new java.util.ArrayList<>();
+    }
+
+    /**
+     * 模拟执行成功(默认实现,子类可覆盖)
+     */
+    @Override
+    public void simulateSuccess(StationContext context) {
+        log.info("[{}] 步骤 {} 模拟执行成功", context.getStationCode(), stepId);
+        onSuccess(context);
+    }
+
+    /**
+     * 辅助方法:创建条件对象
+     */
+    protected StepCondition condition(String name, String description, boolean satisfied) {
+        return new StepCondition(name, description, satisfied);
+    }
+
+    @Override
+    public void setAsyncCompleteCallback(AsyncCompleteCallback callback) {
+        this.asyncCompleteCallback = callback;
+    }
+    
+    /**
+     * 通知异步完成(子类在轮询完成后调用)
+     */
+    protected void notifyAsyncComplete(boolean success) {
+        if (asyncCompleteCallback != null) {
+            asyncCompleteCallback.onAsyncComplete(success);
+        }
+    }
+}

+ 126 - 0
src/com/mes/step/BindMaterialStep.java

@@ -0,0 +1,126 @@
+package com.mes.step;
+
+import com.mes.core.StationContext;
+import com.mes.tcp.MessageSender;
+
+/**
+ * 物料绑定步骤
+ * 发送MBDW消息将物料与工件绑定
+ */
+public class BindMaterialStep extends AbstractStep {
+
+    private MessageSender messageSender;
+    private boolean requireValidation = true;  // 是否需要先校验
+
+    public BindMaterialStep() {
+        super("bind_material", "物料绑定");
+        this.messageType = "MBDW";
+        this.async = true;  // 需要等待服务端响应
+        this.requiresUserInteraction = false;
+        this.craft = "400004";  // 默认工艺号
+    }
+
+    public void setMessageSender(MessageSender messageSender) {
+        this.messageSender = messageSender;
+    }
+
+    @Override
+    public void configure(java.util.Map<String, Object> config) {
+        super.configure(config);
+        this.requireValidation = getConfigBoolean("require_validation", true);
+    }
+
+    @Override
+    public boolean execute(StationContext context) {
+        String productSn = context.getProcessedProductSn();
+        String materialSn = context.getMaterialSn();
+
+        if (productSn == null || productSn.isEmpty()) {
+            log.error("[{}] 物料绑定失败: 工件码为空", context.getStationCode());
+            return false;
+        }
+
+        if (materialSn == null || materialSn.isEmpty()) {
+            log.error("[{}] 物料绑定失败: 物料码为空", context.getStationCode());
+            return false;
+        }
+
+        // 检查是否需要先校验
+        if (requireValidation && !context.isMaterialValidated()) {
+            log.error("[{}] 物料绑定失败: 物料未校验", context.getStationCode());
+            return false;
+        }
+
+        context.setStatusMessage("正在绑定物料...", 0);
+
+        log.info("[{}] 发送物料绑定: product={}, material={}, craft={}",
+                context.getStationCode(), productSn, materialSn, craft);
+
+        // 获取不带后缀的工位号(绑定时使用)
+        String stationCodeWithoutSuffix = context.getStationCode().replaceAll("[A-Z]$", "");
+
+        // 发送绑定消息
+        if (messageSender != null) {
+            return messageSender.sendBindMaterial(
+                    productSn,
+                    materialSn,
+                    context.getUser(),
+                    craft,
+                    stationCodeWithoutSuffix
+            );
+        } else {
+            log.error("[{}] MessageSender未设置", context.getStationCode());
+            return false;
+        }
+    }
+
+    @Override
+    public void onServerResponse(StationContext context, String result, String message) {
+        if ("OK".equalsIgnoreCase(result)) {
+            context.setMaterialBound(true);
+            context.setStatusMessage("物料绑定成功", 0);
+            log.info("[{}] 物料绑定成功", context.getStationCode());
+            onSuccess(context);
+        } else {
+            context.setMaterialBound(false);
+            String errorMsg = getErrorMessage(result, message);
+            context.setStatusMessage(errorMsg, -1);
+            log.warn("[{}] 物料绑定失败: {}", context.getStationCode(), errorMsg);
+            onFailure(context, errorMsg);
+        }
+    }
+
+    @Override
+    protected String getErrorMessage(String resultCode, String message) {
+        switch (resultCode.toUpperCase()) {
+            case "BD":
+                return "绑定失败";
+            case "AE":
+                return "物料已绑定其他工件";
+            case "NE":
+                return "工件或物料不存在";
+            default:
+                return "物料绑定失败: " + resultCode;
+        }
+    }
+
+    @Override
+    public boolean canExecute(StationContext context) {
+        if (requireValidation) {
+            return context.isMaterialValidated() && !context.isMaterialBound();
+        } else {
+            return context.getMaterialSn() != null && 
+                   !context.getMaterialSn().isEmpty() &&
+                   !context.isMaterialBound();
+        }
+    }
+
+    // Getters & Setters
+    public boolean isRequireValidation() {
+        return requireValidation;
+    }
+
+    public void setRequireValidation(boolean requireValidation) {
+        this.requireValidation = requireValidation;
+    }
+}

+ 190 - 0
src/com/mes/step/CheckQualityStep.java

@@ -0,0 +1,190 @@
+package com.mes.step;
+
+import com.mes.core.StationConfig;
+import com.mes.core.StationContext;
+import com.mes.tcp.MessageSender;
+
+/**
+ * 质量检查步骤
+ * 发送AQDW消息查询工件质量状态
+ * 
+ * 支持两种提交模式:
+ * - manual: 质量检查通过后显示"可以加工",用户手动点击OK/NG上传结果
+ * - auto: 质量检查通过后显示"等待设备启动",设备信号触发自动上传结果
+ */
+public class CheckQualityStep extends AbstractStep {
+
+    private MessageSender messageSender;
+    private boolean manualMode = true;  // 默认手动模式
+
+    public CheckQualityStep() {
+        super("check_quality", "质量检查");
+        this.messageType = "AQDW";
+        this.async = true;  // 需要等待服务端响应
+        this.requiresUserInteraction = false;  // 自动执行
+        this.craft = "100000";  // 默认工艺号
+        
+        // 从配置读取提交模式
+        try {
+            StationConfig config = StationConfig.getInstance();
+            this.manualMode = config.isManualSubmitMode();
+        } catch (Exception e) {
+            // 配置未加载时使用默认值
+        }
+    }
+
+    public void setMessageSender(MessageSender messageSender) {
+        this.messageSender = messageSender;
+    }
+
+    public void setManualMode(boolean manualMode) {
+        this.manualMode = manualMode;
+    }
+
+    @Override
+    public boolean execute(StationContext context) {
+        String productSn = context.getProcessedProductSn();
+        
+        if (productSn == null || productSn.trim().isEmpty()) {
+            log.error("[{}] 质量检查失败: 工件码为空", context.getStationCode());
+            return false;
+        }
+
+        context.setState(StationContext.WorkflowState.CHECKING);
+        context.setStatusMessage("正在查询质量...", 0);
+
+        log.info("[{}] 发送质量查询: sn={}, craft={}, mode={}", 
+                context.getStationCode(), productSn, craft, manualMode ? "manual" : "auto");
+
+        // 发送质量查询消息
+        if (messageSender != null) {
+            return messageSender.sendCheckQuality(
+                    productSn,
+                    context.getUser(),
+                    context.getStationCode()
+            );
+        } else {
+            log.error("[{}] MessageSender未设置", context.getStationCode());
+            return false;
+        }
+    }
+
+    @Override
+    public void onServerResponse(StationContext context, String result, String message) {
+        if ("UD".equalsIgnoreCase(result)) {
+            // 只有 UD 表示可以加工(OK表示已加工过,不允许重复加工)
+            context.setQualityPassed(true);
+            
+            if (context.isDeviceEnabled()) {
+                // 有设备:等待设备启动
+                context.setStatusMessage("可以加工,等待设备启动", 0);
+                log.info("[{}] 质量检查通过,等待设备信号", context.getStationCode());
+            } else {
+                // 无设备:根据提交模式显示不同提示
+                if (manualMode) {
+                    // 手工位 + 手动提交:显示按钮,等待用户点击OK/NG
+                    context.setStatusMessage("该工件可以加工,请点击OK或NG", 0);
+                    context.setWaitingForUserAction(true);  // 标记等待用户操作
+                    context.updateUI();  // 刷新UI,启用OK/NG按钮
+                    log.info("[{}] 质量检查通过(手工位/手动),等待用户点击OK/NG", context.getStationCode());
+                } else {
+                    // 手工位 + 自动提交:准备自动提交
+                    context.setStatusMessage("质量检查通过,准备提交", 0);
+                    log.info("[{}] 质量检查通过(手工位/自动),准备自动提交", context.getStationCode());
+                }
+            }
+            
+            onSuccess(context);
+        } else {
+            // 质量不合格
+            context.setQualityPassed(false);
+            String errorMsg = getErrorMessage(result, message);
+            context.setStatusMessage(errorMsg, -1);
+            log.warn("[{}] 质量检查不通过: {}", context.getStationCode(), errorMsg);
+            onFailure(context, errorMsg);
+        }
+    }
+
+    @Override
+    protected String getErrorMessage(String resultCode, String message) {
+        switch (resultCode.toUpperCase()) {
+            case "OK":
+                return "该工件本工位已加工,结果:OK";
+            case "NG":
+                return "该工件本工位已加工,结果:NG";
+            case "QD":
+                return "前道工序未完成";
+            case "NE":
+                return "工件不存在";
+            case "LK":
+                return "工件已锁定";
+            case "FN":
+                return "工件已完成,无需加工";
+            case "NN":
+                return "该工件跳过该工位";
+            case "QN":
+                return "前道工序加工NG";
+            case "NF":
+                return "该工件已合格下线";
+            case "NR":
+                return "该工件离线返修中";
+            case "NB":
+                return "该工件已报废";
+            case "ND":
+                return "该工件NG待处理";
+            case "DJ":
+                return "未进行开班点检";
+            case "BM":
+                return "未绑定物料";
+            case "PL":
+                return "配件寿命不足";
+            case "CF":
+                return "工件码重复";
+            case "GS":
+                return "工件码格式不正确";
+            case "SB":
+                return "设备信息不存在";
+            default:
+                return super.getErrorMessage(resultCode, message);
+        }
+    }
+
+    @Override
+    public boolean canExecute(StationContext context) {
+        // 必须有工件码
+        return context.getProductSn() != null && !context.getProductSn().isEmpty();
+    }
+
+    // ========== 流程监控扩展 ==========
+
+    @Override
+    public java.util.List<StepCondition> getConditions(StationContext context) {
+        java.util.List<StepCondition> conditions = new java.util.ArrayList<>();
+        
+        boolean hasSn = context.getProductSn() != null && !context.getProductSn().isEmpty();
+        conditions.add(condition("productSn", 
+                "工件码: " + (hasSn ? context.getProductSn() : "(空)"), 
+                hasSn));
+        
+        // 显示当前质量检查状态(执行后的结果)
+        conditions.add(condition("qualityPassed", 
+                "质量检查结果: " + (context.isQualityPassed() ? "通过" : "未检查/不通过"), 
+                context.isQualityPassed()));
+        
+        return conditions;
+    }
+
+    @Override
+    public void simulateSuccess(StationContext context) {
+        log.info("[{}] 模拟质量检查通过", context.getStationCode());
+        context.setQualityPassed(true);
+        if (manualMode) {
+            context.setStatusMessage("(模拟) 该工件可以加工,请点击OK或NG", 0);
+            context.setWaitingForUserAction(true);
+        } else {
+            context.setStatusMessage("(模拟) 可以加工,等待设备启动", 0);
+        }
+        context.updateUI();
+        onSuccess(context);
+    }
+}

+ 98 - 0
src/com/mes/step/CreateOrderStep.java

@@ -0,0 +1,98 @@
+package com.mes.step;
+
+import com.mes.core.StationContext;
+import com.mes.tcp.MessageSender;
+
+/**
+ * 创建工件步骤
+ * 用于OP40等创建工件的工位
+ * 发送MCJW消息创建工件记录
+ */
+public class CreateOrderStep extends AbstractStep {
+
+    private MessageSender messageSender;
+    private String defaultResult = "OK";  // 默认结果
+
+    public CreateOrderStep() {
+        super("create_order", "创建工件");
+        this.messageType = "MCJW";  // 假设创建工件的消息类型
+        this.async = true;  // 需要等待服务端响应
+        this.requiresUserInteraction = false;
+    }
+
+    public void setMessageSender(MessageSender messageSender) {
+        this.messageSender = messageSender;
+    }
+
+    @Override
+    public boolean execute(StationContext context) {
+        String productSn = context.getProcessedProductSn();
+
+        if (productSn == null || productSn.trim().isEmpty()) {
+            log.error("[{}] 创建工件失败: 工件码为空", context.getStationCode());
+            return false;
+        }
+
+        context.setStatusMessage("正在创建工件...", 0);
+
+        log.info("[{}] 创建工件: sn={}", context.getStationCode(), productSn);
+
+        // 发送创建工件消息
+        if (messageSender != null) {
+            return messageSender.sendCreate(
+                    productSn,
+                    defaultResult,
+                    context.getUser(),
+                    context.getStationCode()
+            );
+        } else {
+            log.error("[{}] MessageSender未设置", context.getStationCode());
+            return false;
+        }
+    }
+
+    @Override
+    public void onServerResponse(StationContext context, String result, String message) {
+        if ("OK".equalsIgnoreCase(result)) {
+            context.setStatusMessage("工件创建成功,请扫下一件", 0);
+            log.info("[{}] 工件创建成功", context.getStationCode());
+            onSuccess(context);
+        } else if ("CF".equalsIgnoreCase(result)) {
+            // 重复提交
+            context.setStatusMessage("工件码重复", -1);
+            log.warn("[{}] 工件创建失败: 重复提交", context.getStationCode());
+            onFailure(context, "工件码重复提交失败");
+        } else {
+            String errorMsg = getErrorMessage(result, message);
+            context.setStatusMessage(errorMsg, -1);
+            log.warn("[{}] 工件创建失败: {}", context.getStationCode(), errorMsg);
+            onFailure(context, errorMsg);
+        }
+    }
+
+    @Override
+    protected String getErrorMessage(String resultCode, String message) {
+        switch (resultCode.toUpperCase()) {
+            case "CF":
+                return "工件码重复";
+            case "ER":
+                return "创建失败,系统错误";
+            default:
+                return "创建失败: " + resultCode;
+        }
+    }
+
+    @Override
+    public boolean canExecute(StationContext context) {
+        return context.getProductSn() != null && !context.getProductSn().isEmpty();
+    }
+
+    // Getters & Setters
+    public String getDefaultResult() {
+        return defaultResult;
+    }
+
+    public void setDefaultResult(String defaultResult) {
+        this.defaultResult = defaultResult;
+    }
+}

+ 64 - 0
src/com/mes/step/DeviceStartStep.java

@@ -0,0 +1,64 @@
+package com.mes.step;
+
+import com.mes.core.StationContext;
+import com.mes.device.IDeviceDriver;
+
+/**
+ * 设备启动步骤
+ * 向PLC发送启动信号/就绪信号
+ * 注意:心跳启动已移至单独的 StartHeartbeatStep
+ */
+public class DeviceStartStep extends AbstractStep {
+
+    public DeviceStartStep() {
+        super("device_start", "启动设备");
+        this.async = false;
+        this.requiresUserInteraction = false;
+    }
+
+    @Override
+    public boolean execute(StationContext context) {
+        // 检查设备是否启用
+        if (!context.isDeviceEnabled()) {
+            log.info("[{}] 设备未启用,跳过设备启动", context.getStationCode());
+            return true;
+        }
+
+        IDeviceDriver driver = context.getDeviceDriver();
+        if (driver == null) {
+            log.warn("[{}] 设备驱动未配置", context.getStationCode());
+            return true;  // 无设备时跳过
+        }
+
+        try {
+            // 发送就绪/启动信号
+            driver.signalReady();
+            log.info("[{}] 设备启动信号已发送", context.getStationCode());
+
+            context.setStatusMessage("设备已启动", 0);
+            return true;
+
+        } catch (Exception e) {
+            log.error("[{}] 设备启动失败: {}", context.getStationCode(), e.getMessage(), e);
+            context.setStatusMessage("设备启动失败: " + e.getMessage(), -1);
+            return false;
+        }
+    }
+
+    @Override
+    public boolean canExecute(StationContext context) {
+        // 质量检查通过才能启动设备
+        return context.isQualityPassed();
+    }
+
+    @Override
+    public boolean canSkip(StationContext context) {
+        // 设备未启用时跳过
+        return !context.isDeviceEnabled();
+    }
+
+    @Override
+    public void onSkip(StationContext context) {
+        log.info("[{}] 设备启动步骤已跳过(设备未启用)", context.getStationCode());
+    }
+}

+ 197 - 0
src/com/mes/step/IWorkflowStep.java

@@ -0,0 +1,197 @@
+package com.mes.step;
+
+import com.mes.core.StationContext;
+
+import java.util.Map;
+
+/**
+ * 流程步骤接口 - 定义工作流中每个步骤的标准行为
+ * 所有具体步骤(扫码、质检、绑定等)都需要实现此接口
+ */
+public interface IWorkflowStep {
+
+    /**
+     * 获取步骤ID(唯一标识)
+     * 例如:scan_product, check_quality, start_work
+     */
+    String getStepId();
+
+    /**
+     * 获取步骤名称(用于显示)
+     * 例如:扫描工件码、质量检查、开始工作
+     */
+    String getStepName();
+
+    /**
+     * 获取关联的消息类型
+     * 例如:AQDW(质量查询)、MKSW(开始工作)、MQDW(上传结果)
+     * 返回null表示不需要等待服务端响应
+     */
+    String getMessageType();
+
+    /**
+     * 执行步骤
+     * @param context 工位上下文
+     * @return true=执行成功/已发送请求,false=执行失败
+     */
+    boolean execute(StationContext context);
+
+    /**
+     * 处理服务端响应
+     * @param context 工位上下文
+     * @param result 结果码(如 OK, UD, CF 等)
+     * @param message 完整消息
+     */
+    void onServerResponse(StationContext context, String result, String message);
+
+    /**
+     * 步骤成功完成时的回调
+     * @param context 工位上下文
+     */
+    void onSuccess(StationContext context);
+
+    /**
+     * 步骤失败时的回调
+     * @param context 工位上下文
+     * @param error 错误信息
+     */
+    void onFailure(StationContext context, String error);
+
+    /**
+     * 是否为异步步骤(需要等待服务端响应)
+     * true = 需要等待服务端响应后才能进入下一步
+     * false = 执行完立即进入下一步
+     */
+    boolean isAsync();
+
+    /**
+     * 是否需要用户交互
+     * true = 需要用户操作(如扫码、点击按钮)
+     * false = 自动执行
+     */
+    boolean requiresUserInteraction();
+
+    /**
+     * 获取超时时间(毫秒)
+     * 仅对异步步骤有效
+     * @return 超时时间,0表示不超时
+     */
+    int getTimeoutMs();
+
+    /**
+     * 配置步骤参数
+     * @param config 配置参数(来自JSON配置文件)
+     */
+    void configure(Map<String, Object> config);
+
+    /**
+     * 设置工艺号
+     * @param craft 工艺号
+     */
+    void setCraft(String craft);
+
+    /**
+     * 获取工艺号
+     */
+    String getCraft();
+
+    /**
+     * 检查是否可以执行(前置条件检查)
+     * @param context 工位上下文
+     * @return true=可以执行,false=条件不满足
+     */
+    default boolean canExecute(StationContext context) {
+        return true;
+    }
+
+    /**
+     * 检查是否可以跳过此步骤
+     * @param context 工位上下文
+     * @return true=可以跳过,false=必须执行
+     */
+    default boolean canSkip(StationContext context) {
+        return false;
+    }
+
+    /**
+     * 步骤被跳过时的回调
+     * @param context 工位上下文
+     */
+    default void onSkip(StationContext context) {
+        // 默认空实现
+    }
+
+    /**
+     * 超时时的回调
+     * @param context 工位上下文
+     */
+    default void onTimeout(StationContext context) {
+        onFailure(context, "操作超时");
+    }
+
+    /**
+     * 设置异步完成回调(引擎在执行异步步骤时注册)
+     * 用于轮询类异步步骤(如等待设备启动/完成)主动通知引擎步骤已完成
+     */
+    default void setAsyncCompleteCallback(AsyncCompleteCallback callback) {
+        // 默认空实现,需要的步骤覆盖此方法
+    }
+
+    /**
+     * 异步步骤完成回调接口
+     */
+    interface AsyncCompleteCallback {
+        void onAsyncComplete(boolean success);
+    }
+
+    // ========== 流程监控扩展方法 ==========
+
+    /**
+     * 获取执行条件列表(用于流程监控显示)
+     * @return 条件列表,每个条件包含:名称、描述、是否满足
+     */
+    default java.util.List<StepCondition> getConditions(StationContext context) {
+        return java.util.Collections.emptyList();
+    }
+
+    /**
+     * 模拟执行成功(跳过时设置该步骤会设置的状态标志)
+     * @param context 工位上下文
+     */
+    default void simulateSuccess(StationContext context) {
+        // 默认实现:调用 onSuccess
+        onSuccess(context);
+    }
+
+    /**
+     * 重置步骤状态(清理后台任务、释放资源)
+     * 在流程重置时调用,用于停止轮询线程等后台任务
+     */
+    default void reset() {
+        // 默认空实现
+    }
+
+    /**
+     * 步骤条件信息类
+     */
+    class StepCondition {
+        private final String name;        // 条件名称(如:qualityPassed)
+        private final String description; // 条件描述(如:质量检查通过)
+        private final boolean satisfied;  // 是否满足
+
+        public StepCondition(String name, String description, boolean satisfied) {
+            this.name = name;
+            this.description = description;
+            this.satisfied = satisfied;
+        }
+
+        public String getName() { return name; }
+        public String getDescription() { return description; }
+        public boolean isSatisfied() { return satisfied; }
+
+        @Override
+        public String toString() {
+            return description + (satisfied ? " ✓" : " ✗");
+        }
+    }
+}

+ 90 - 0
src/com/mes/step/ReadDeviceDataStep.java

@@ -0,0 +1,90 @@
+package com.mes.step;
+
+import com.mes.core.StationConfig;
+import com.mes.core.StationContext;
+import com.mes.device.IDeviceDriver;
+
+/**
+ * 读取设备数据步骤
+ * 在流程开始时读取设备数据并显示,不阻塞流程
+ */
+public class ReadDeviceDataStep extends AbstractStep {
+
+    public ReadDeviceDataStep() {
+        super("read_device_data", "读取设备数据");
+        this.async = false;
+        this.requiresUserInteraction = false;
+    }
+
+    @Override
+    public boolean execute(StationContext context) {
+        // 检查设备是否启用
+        if (!context.isDeviceEnabled()) {
+            log.debug("[{}] 设备未启用,跳过读取", context.getStationCode());
+            return true;
+        }
+
+        IDeviceDriver driver = context.getDeviceDriver();
+        if (driver == null || !driver.isConnected()) {
+            log.warn("[{}] 设备未连接,无法读取数据", context.getStationCode());
+            return true;  // 不阻塞流程
+        }
+
+        try {
+            StationConfig config = StationConfig.getInstance();
+            
+            // 读取第一行设备数据
+            StationConfig.DeviceInfoRow row1 = config.getDeviceInfoRow(0);
+            if (row1 != null) {
+                int presetCount = driver.readRegister(row1.getPresetAddress());
+                int finishedCount = driver.readRegister(row1.getFinishedAddress());
+                context.setPresetCount(presetCount);
+                context.setFinishedCount(finishedCount);
+                log.debug("[{}] 读取第一行设备数据: 预设={}, 完成={}", 
+                         context.getStationCode(), presetCount, finishedCount);
+            }
+            
+            // 读取第二行设备数据(如果配置了)
+            StationConfig.DeviceInfoRow row2 = config.getDeviceInfoRow(1);
+            if (row2 != null) {
+                IDeviceDriver driver2 = context.getDeviceDriver2();
+                if (driver2 != null && driver2.isConnected()) {
+                    int presetCount2 = driver2.readRegister(row2.getPresetAddress());
+                    int finishedCount2 = driver2.readRegister(row2.getFinishedAddress());
+                    context.setPresetCount2(presetCount2);
+                    context.setFinishedCount2(finishedCount2);
+                    context.setDeviceConnected2(true);
+                    log.debug("[{}] 读取第二行设备数据: 预设={}, 完成={}", 
+                             context.getStationCode(), presetCount2, finishedCount2);
+                } else {
+                    // 第二个设备未连接
+                    context.setPresetCount2(-1);
+                    context.setFinishedCount2(-1);
+                    context.setDeviceConnected2(false);
+                    log.debug("[{}] 第二行设备未连接", context.getStationCode());
+                }
+            }
+            
+            // 更新UI显示
+            context.updateUI();
+            
+        } catch (Exception e) {
+            log.error("[{}] 读取设备数据失败: {}", context.getStationCode(), e.getMessage());
+            // 不阻塞流程
+        }
+
+        return true;
+    }
+
+    @Override
+    public boolean canExecute(StationContext context) {
+        // 总是可以执行
+        return true;
+    }
+
+    @Override
+    public boolean canSkip(StationContext context) {
+        // 设备未启用时可以跳过
+        return !context.isDeviceEnabled();
+    }
+}

+ 51 - 0
src/com/mes/step/RelayCloseStep.java

@@ -0,0 +1,51 @@
+package com.mes.step;
+
+import com.mes.core.StationContext;
+import com.mes.device.RelayDriver;
+
+/**
+ * 关继电器步骤
+ * 关闭继电器停止设备
+ */
+public class RelayCloseStep extends AbstractStep {
+
+    public RelayCloseStep() {
+        super("relay_close", "关继电器");
+        this.async = false;
+        this.requiresUserInteraction = false;
+    }
+
+    @Override
+    public boolean execute(StationContext context) {
+        RelayDriver relayDriver = context.getRelayDriver();
+        if (relayDriver == null) {
+            log.debug("[{}] 继电器驱动未配置,跳过关继电器", context.getStationCode());
+            return true;
+        }
+
+        if (!relayDriver.isConnected()) {
+            log.warn("[{}] 继电器未连接,跳过关继电器", context.getStationCode());
+            return true;
+        }
+
+        try {
+            relayDriver.close();
+            log.info("[{}] 继电器已关闭", context.getStationCode());
+            return true;
+        } catch (Exception e) {
+            log.error("[{}] 关继电器失败: {}", context.getStationCode(), e.getMessage());
+            // 不阻塞流程,继续执行
+            return true;
+        }
+    }
+
+    @Override
+    public boolean canExecute(StationContext context) {
+        return true;  // 任何时候都可以执行
+    }
+
+    @Override
+    public boolean canSkip(StationContext context) {
+        return context.getRelayDriver() == null;
+    }
+}

+ 50 - 0
src/com/mes/step/RelayOpenStep.java

@@ -0,0 +1,50 @@
+package com.mes.step;
+
+import com.mes.core.StationContext;
+import com.mes.device.RelayDriver;
+
+/**
+ * 开继电器步骤
+ * 打开继电器控制设备启动
+ */
+public class RelayOpenStep extends AbstractStep {
+
+    public RelayOpenStep() {
+        super("relay_open", "开继电器");
+        this.async = false;
+        this.requiresUserInteraction = false;
+    }
+
+    @Override
+    public boolean execute(StationContext context) {
+        RelayDriver relayDriver = context.getRelayDriver();
+        if (relayDriver == null) {
+            log.warn("[{}] 继电器驱动未配置,跳过开继电器", context.getStationCode());
+            return true;
+        }
+
+        if (!relayDriver.isConnected()) {
+            log.warn("[{}] 继电器未连接,跳过开继电器", context.getStationCode());
+            return true;
+        }
+
+        try {
+            relayDriver.open();
+            log.info("[{}] 继电器已打开", context.getStationCode());
+            return true;
+        } catch (Exception e) {
+            log.error("[{}] 开继电器失败: {}", context.getStationCode(), e.getMessage());
+            return false;
+        }
+    }
+
+    @Override
+    public boolean canExecute(StationContext context) {
+        return context.isQualityPassed();
+    }
+
+    @Override
+    public boolean canSkip(StationContext context) {
+        return context.getRelayDriver() == null;
+    }
+}

+ 146 - 0
src/com/mes/step/ResetDeviceCountStep.java

@@ -0,0 +1,146 @@
+package com.mes.step;
+
+import com.mes.core.StationContext;
+import com.mes.device.IDeviceDriver;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 重置设备计数步骤
+ * 向PLC指定地址写入指定值,用于重置设备计数器(如拉铆枪计数)
+ * 支持配置多个地址的写入操作
+ */
+public class ResetDeviceCountStep extends AbstractStep {
+
+    private int address = 8;   // 默认地址8
+    private int value = 1;     // 默认值1
+    
+    // 支持清零多个地址
+    private List<AddressValue> clearAddresses = new ArrayList<>();
+    
+    /**
+     * 地址-值对
+     */
+    private static class AddressValue {
+        int address;
+        int value;
+        
+        AddressValue(int address, int value) {
+            this.address = address;
+            this.value = value;
+        }
+    }
+
+    public ResetDeviceCountStep() {
+        super("reset_device_count", "重置设备计数");
+        this.async = false;
+        this.requiresUserInteraction = false;
+    }
+
+    @Override
+    public void configure(Map<String, Object> config) {
+        super.configure(config);
+        this.address = getConfigInt("address", 8);
+        this.value = getConfigInt("value", 1);
+        
+        // 读取清零地址配置(可选)
+        Object clearAddressesObj = config.get("clear_addresses");
+        if (clearAddressesObj instanceof List) {
+            List<?> list = (List<?>) clearAddressesObj;
+            for (Object item : list) {
+                if (item instanceof Map) {
+                    Map<?, ?> map = (Map<?, ?>) item;
+                    int addr = getIntValue(map.get("address"), 0);
+                    int val = getIntValue(map.get("value"), 0);
+                    if (addr > 0) {
+                        clearAddresses.add(new AddressValue(addr, val));
+                    }
+                }
+            }
+        }
+    }
+    
+    private int getIntValue(Object obj, int defaultValue) {
+        if (obj instanceof Number) {
+            return ((Number) obj).intValue();
+        }
+        return defaultValue;
+    }
+
+    @Override
+    public boolean execute(StationContext context) {
+        // 检查设备是否启用
+        if (!context.isDeviceEnabled()) {
+            log.info("[{}] 设备未启用,跳过重置计数", context.getStationCode());
+            return true;
+        }
+
+        IDeviceDriver driver = context.getDeviceDriver();
+        if (driver == null) {
+            log.warn("[{}] 设备驱动未配置,跳过重置计数", context.getStationCode());
+            return true;
+        }
+
+        try {
+            // 1. 向指定地址写入指定值(如地址8写1,告诉PLC可以开始)
+            driver.writeRegister(address, value);
+            log.info("[{}] 重置设备计数:向地址{}写入{}", context.getStationCode(), address, value);
+
+            // 可选:读取验证
+            int readValue = driver.readRegister(address);
+            log.debug("[{}] 验证读取地址{}的值:{}", context.getStationCode(), address, readValue);
+
+            // 2. 清零其他地址(如地址66、68、70,清零拉铆枪计数器)
+            if (!clearAddresses.isEmpty()) {
+                for (AddressValue av : clearAddresses) {
+                    driver.writeRegister(av.address, av.value);
+                    log.info("[{}] 清零地址{},写入值:{}", context.getStationCode(), av.address, av.value);
+                }
+                log.info("[{}] 已清零{}个地址", context.getStationCode(), clearAddresses.size());
+            }
+
+            context.setStatusMessage("设备计数已重置", 0);
+            return true;
+
+        } catch (Exception e) {
+            log.error("[{}] 重置设备计数失败:{}", context.getStationCode(), e.getMessage());
+            context.setStatusMessage("重置设备计数失败", -1);
+            return false;
+        }
+    }
+
+    @Override
+    public void onSuccess(StationContext context) {
+        super.onSuccess(context);
+    }
+
+    @Override
+    public boolean canExecute(StationContext context) {
+        // 质量检查通过且设备已启用
+        return context.isQualityPassed() && context.isDeviceEnabled();
+    }
+
+    @Override
+    public boolean canSkip(StationContext context) {
+        // 设备未启用时跳过
+        return !context.isDeviceEnabled();
+    }
+
+    // ========== 流程监控扩展 ==========
+
+    @Override
+    public java.util.List<StepCondition> getConditions(StationContext context) {
+        java.util.List<StepCondition> conditions = new java.util.ArrayList<>();
+        conditions.add(condition("qualityPassed", "质量检查通过", context.isQualityPassed()));
+        conditions.add(condition("deviceEnabled", "设备已启用", context.isDeviceEnabled()));
+        return conditions;
+    }
+
+    @Override
+    public void simulateSuccess(StationContext context) {
+        log.info("[{}] 模拟重置设备计数", context.getStationCode());
+        context.setStatusMessage("设备计数已重置(模拟)", 0);
+        onSuccess(context);
+    }
+}

+ 72 - 0
src/com/mes/step/ResetStationStep.java

@@ -0,0 +1,72 @@
+package com.mes.step;
+
+import com.mes.core.StationContext;
+import com.mes.ui.component.WorkstationPanel;
+
+/**
+ * 重置工位步骤
+ * 清空工位状态,准备处理下一件工件
+ */
+public class ResetStationStep extends AbstractStep {
+
+    public ResetStationStep() {
+        super("reset_station", "重置工位");
+        this.async = false;
+        this.requiresUserInteraction = false;  // 自动执行
+    }
+
+    @Override
+    public boolean execute(StationContext context) {
+        log.info("[{}] 执行工位重置", context.getStationCode());
+
+        // 重置上下文状态
+        context.reset();
+
+        // 刷新UI
+        context.updateUI();
+
+        // 自动弹出扫码框,准备下一件
+        WorkstationPanel panel = context.getUiPanel();
+        if (panel != null) {
+            panel.requestScan();
+        }
+
+        return true;
+    }
+
+    @Override
+    public void onSuccess(StationContext context) {
+        super.onSuccess(context);
+    }
+
+    @Override
+    public boolean canExecute(StationContext context) {
+        // 任何状态都可以重置
+        return true;
+    }
+
+    @Override
+    public boolean canSkip(StationContext context) {
+        // 重置步骤不能跳过
+        return false;
+    }
+
+    // ========== 流程监控扩展 ==========
+
+    @Override
+    public java.util.List<StepCondition> getConditions(StationContext context) {
+        java.util.List<StepCondition> conditions = new java.util.ArrayList<>();
+        // 重置步骤无条件限制
+        conditions.add(condition("none", "无条件限制 (总是可以执行)", true));
+        return conditions;
+    }
+
+    @Override
+    public void simulateSuccess(StationContext context) {
+        log.info("[{}] 模拟重置工位", context.getStationCode());
+        // 重置步骤的模拟执行就是真正执行重置
+        context.reset();
+        context.updateUI();
+        onSuccess(context);
+    }
+}

+ 128 - 0
src/com/mes/step/ScanMaterialStep.java

@@ -0,0 +1,128 @@
+package com.mes.step;
+
+import com.mes.core.StationContext;
+import com.mes.ui.component.WorkstationPanel;
+
+/**
+ * 扫描物料码步骤
+ * 用于底护板绑定、冷板绑定等场景
+ * 执行时自动弹出扫码框
+ */
+public class ScanMaterialStep extends AbstractStep {
+
+    private String label = "物料码";  // 显示标签
+    private String prefix = "";       // 期望的前缀
+    private boolean required = true;  // 是否必须
+    private WorkstationPanel panel;   // 工位面板引用
+
+    public ScanMaterialStep() {
+        super("scan_material", "扫描物料码");
+        this.async = false;
+        this.requiresUserInteraction = false;  // 改为false,让步骤自动执行来弹出扫码框
+    }
+
+    @Override
+    public void configure(java.util.Map<String, Object> config) {
+        super.configure(config);
+        this.label = getConfigString("label", "物料码");
+        this.prefix = getConfigString("prefix", "");
+        this.required = getConfigBoolean("required", true);
+
+        // 更新步骤名称
+        this.stepName = "扫描" + label;
+    }
+
+    /**
+     * 设置工位面板引用(用于弹出扫码框)
+     */
+    public void setPanel(WorkstationPanel panel) {
+        this.panel = panel;
+    }
+
+    @Override
+    public boolean execute(StationContext context) {
+        // 第一次执行:弹出扫码框
+        if (context.getMaterialSn() == null || context.getMaterialSn().trim().isEmpty()) {
+            log.info("[{}] 自动弹出{}扫码框", context.getStationCode(), label);
+            context.setStatusMessage("请扫描" + label, 1);
+            
+            // 弹出扫码框
+            if (panel != null) {
+                panel.requestMaterialScan();
+            }
+            
+            return false;  // 等待用户扫码
+        }
+
+        // 用户扫码后:验证物料码
+        String materialSn = context.getMaterialSn().trim();
+
+        // 前缀校验
+        if (prefix != null && !prefix.isEmpty() && !materialSn.startsWith(prefix)) {
+            log.warn("[{}] {}格式错误,期望前缀: {}", context.getStationCode(), label, prefix);
+            context.setStatusMessage(label + "格式错误", -1);
+            context.setMaterialSn(null);
+            
+            // 格式错误,重新弹出扫码框
+            if (panel != null) {
+                panel.requestMaterialScan();
+            }
+            
+            return false;
+        }
+
+        log.info("[{}] 扫描{}完成: {}", context.getStationCode(), label, materialSn);
+        context.setStatusMessage(label + ": " + materialSn, 0);
+
+        return true;
+    }
+
+    @Override
+    public void onSuccess(StationContext context) {
+        super.onSuccess(context);
+    }
+
+    @Override
+    public void onFailure(StationContext context, String error) {
+        super.onFailure(context, error);
+        context.setMaterialSn(null);
+    }
+
+    @Override
+    public boolean canExecute(StationContext context) {
+        // 必须质量检查通过
+        return context.isQualityPassed();
+    }
+
+    @Override
+    public boolean canSkip(StationContext context) {
+        // 如果不是必须的,可以跳过
+        return !required;
+    }
+
+    // Getters & Setters
+    public String getLabel() {
+        return label;
+    }
+
+    public void setLabel(String label) {
+        this.label = label;
+        this.stepName = "扫描" + label;
+    }
+
+    public String getPrefix() {
+        return prefix;
+    }
+
+    public void setPrefix(String prefix) {
+        this.prefix = prefix;
+    }
+
+    public boolean isRequired() {
+        return required;
+    }
+
+    public void setRequired(boolean required) {
+        this.required = required;
+    }
+}

+ 94 - 0
src/com/mes/step/ScanProductStep.java

@@ -0,0 +1,94 @@
+package com.mes.step;
+
+import com.mes.core.StationContext;
+
+/**
+ * 扫描工件码步骤
+ * 等待用户扫描工件码,然后自动进入下一步
+ */
+public class ScanProductStep extends AbstractStep {
+
+    public ScanProductStep() {
+        super("scan_product", "扫描工件码");
+        this.async = false;  // 扫码完成后同步进入下一步
+        this.requiresUserInteraction = true;  // 需要用户扫码
+    }
+
+    @Override
+    public boolean execute(StationContext context) {
+        String productSn = context.getProductSn();
+        
+        if (productSn == null || productSn.trim().isEmpty()) {
+            log.warn("[{}] 工件码为空,等待用户扫码", context.getStationCode());
+            context.setStatusMessage("请扫描工件码", 1);
+            return false;
+        }
+
+        // 处理36位码(取前20位)
+        String processedSn = productSn.trim();
+        if (processedSn.length() == 36) {
+            processedSn = processedSn.substring(0, 20);
+            log.info("[{}] 36位码处理: {} -> {}", context.getStationCode(), productSn, processedSn);
+        }
+
+        // 保存处理后的工件码
+        context.setProductSn(processedSn);
+        context.setState(StationContext.WorkflowState.SCANNING);
+        context.setStatusMessage("工件码: " + processedSn, 0);
+
+        log.info("[{}] 扫描工件码完成: {}", context.getStationCode(), processedSn);
+        return true;
+    }
+
+    @Override
+    public void onSuccess(StationContext context) {
+        super.onSuccess(context);
+        // 扫码成功,自动进入质量检查
+    }
+
+    @Override
+    public void onFailure(StationContext context, String error) {
+        super.onFailure(context, error);
+        context.setProductSn(null);
+    }
+
+    @Override
+    public boolean canExecute(StationContext context) {
+        // 只有在空闲状态才能扫码
+        return context.getState() == StationContext.WorkflowState.IDLE ||
+               context.getState() == StationContext.WorkflowState.ERROR;
+    }
+
+    // ========== 流程监控扩展 ==========
+
+    @Override
+    public java.util.List<StepCondition> getConditions(StationContext context) {
+        java.util.List<StepCondition> conditions = new java.util.ArrayList<>();
+        
+        boolean stateOk = context.getState() == StationContext.WorkflowState.IDLE ||
+                          context.getState() == StationContext.WorkflowState.ERROR;
+        conditions.add(condition("state", 
+                "工位状态: " + context.getState() + " (需要 IDLE 或 ERROR)", 
+                stateOk));
+        
+        boolean hasSn = context.getProductSn() != null && !context.getProductSn().isEmpty();
+        conditions.add(condition("productSn", 
+                "工件码: " + (hasSn ? context.getProductSn() : "(未扫描)"), 
+                hasSn));
+        
+        return conditions;
+    }
+
+    @Override
+    public void simulateSuccess(StationContext context) {
+        log.info("[{}] 模拟扫码成功", context.getStationCode());
+        // 如果没有工件码,设置一个模拟的
+        if (context.getProductSn() == null || context.getProductSn().isEmpty()) {
+            String mockSn = "MOCK_" + System.currentTimeMillis();
+            context.setProductSn(mockSn);
+            log.info("[{}] 设置模拟工件码: {}", context.getStationCode(), mockSn);
+        }
+        context.setState(StationContext.WorkflowState.SCANNING);
+        onSuccess(context);
+    }
+}

+ 51 - 0
src/com/mes/step/StartHeartbeatStep.java

@@ -0,0 +1,51 @@
+package com.mes.step;
+
+import com.mes.core.StationContext;
+import com.mes.device.IDeviceDriver;
+
+/**
+ * 启动心跳步骤
+ * 启动设备心跳定时器
+ */
+public class StartHeartbeatStep extends AbstractStep {
+
+    public StartHeartbeatStep() {
+        super("start_heartbeat", "启动心跳");
+        this.async = false;
+        this.requiresUserInteraction = false;
+    }
+
+    @Override
+    public boolean execute(StationContext context) {
+        if (!context.isDeviceEnabled()) {
+            log.info("[{}] 设备未启用,跳过启动心跳", context.getStationCode());
+            return true;
+        }
+
+        IDeviceDriver driver = context.getDeviceDriver();
+        if (driver == null) {
+            log.warn("[{}] 设备驱动未配置,跳过启动心跳", context.getStationCode());
+            return true;
+        }
+
+        try {
+            driver.startHeartbeat();
+            context.setHeartbeatActive(true);
+            log.info("[{}] 心跳已启动", context.getStationCode());
+            return true;
+        } catch (Exception e) {
+            log.error("[{}] 启动心跳失败: {}", context.getStationCode(), e.getMessage());
+            return false;
+        }
+    }
+
+    @Override
+    public boolean canExecute(StationContext context) {
+        return context.isQualityPassed();
+    }
+
+    @Override
+    public boolean canSkip(StationContext context) {
+        return !context.isDeviceEnabled();
+    }
+}

+ 115 - 0
src/com/mes/step/StartWorkStep.java

@@ -0,0 +1,115 @@
+package com.mes.step;
+
+import com.mes.core.StationConfig;
+import com.mes.core.StationContext;
+import com.mes.tcp.MessageSender;
+
+/**
+ * 开始工作步骤
+ * 发送MKSW消息通知服务端开始加工
+ * 
+ * 支持两种模式:
+ * - manual: 需要用户点击OK/NG后才执行
+ * - auto: 设备信号触发自动执行
+ */
+public class StartWorkStep extends AbstractStep {
+
+    private MessageSender messageSender;
+    private boolean manualMode = true;  // 默认手动模式
+
+    public StartWorkStep() {
+        super("start_work", "开始工作");
+        this.messageType = "MKSW";
+        this.async = false;  // 开始工作消息通常不等待响应
+        
+        // 从配置读取提交模式
+        try {
+            StationConfig config = StationConfig.getInstance();
+            this.manualMode = config.isManualSubmitMode();
+        } catch (Exception e) {
+            // 配置未加载时使用默认值
+        }
+        
+        // 手动模式下需要用户点击OK/NG才执行
+        this.requiresUserInteraction = manualMode;
+    }
+    
+    public void setManualMode(boolean manualMode) {
+        this.manualMode = manualMode;
+        this.requiresUserInteraction = manualMode;
+    }
+
+    public void setMessageSender(MessageSender messageSender) {
+        this.messageSender = messageSender;
+    }
+
+    @Override
+    public boolean execute(StationContext context) {
+        String productSn = context.getProcessedProductSn();
+
+        if (productSn == null || productSn.trim().isEmpty()) {
+            log.error("[{}] 开始工作失败: 工件码为空", context.getStationCode());
+            return false;
+        }
+
+        if (!context.isQualityPassed()) {
+            log.warn("[{}] 开始工作失败: 质量检查未通过", context.getStationCode());
+            return false;
+        }
+
+        context.setState(StationContext.WorkflowState.WORKING);
+        context.setWorkStarted(true);
+        context.setStatusMessage("工作已开始", 0);
+
+        log.info("[{}] 发送开始工作: sn={}", context.getStationCode(), productSn);
+
+        // 发送开始工作消息
+        if (messageSender != null) {
+            boolean result = messageSender.sendStartWork(
+                    productSn,
+                    context.getUser(),
+                    context.getStationCode()
+            );
+            if (result) {
+                onSuccess(context);
+            }
+            return result;
+        } else {
+            log.error("[{}] MessageSender未设置", context.getStationCode());
+            return false;
+        }
+    }
+
+    @Override
+    public void onSuccess(StationContext context) {
+        super.onSuccess(context);
+        context.setWorkStarted(true);
+    }
+
+    @Override
+    public boolean canExecute(StationContext context) {
+        // 质量检查必须通过,且未开始工作
+        if (!context.isQualityPassed() || context.isWorkStarted()) {
+            log.debug("[{}] StartWorkStep.canExecute: qualityPassed={}, workStarted={}, result=false", 
+                    context.getStationCode(), context.isQualityPassed(), context.isWorkStarted());
+            return false;
+        }
+        
+        // 手动模式下,需要用户已确认(waitingForUserAction = false 表示用户已点击OK/NG)
+        if (manualMode && context.isWaitingForUserAction()) {
+            log.debug("[{}] StartWorkStep.canExecute: 手动模式等待用户确认, waitingForUserAction={}", 
+                    context.getStationCode(), context.isWaitingForUserAction());
+            return false;
+        }
+        
+        log.debug("[{}] StartWorkStep.canExecute: qualityPassed={}, workStarted={}, manualMode={}, waitingForUserAction={}, result=true", 
+                context.getStationCode(), context.isQualityPassed(), context.isWorkStarted(), manualMode, context.isWaitingForUserAction());
+        return true;
+    }
+
+    @Override
+    public boolean canSkip(StationContext context) {
+        // 如果已经开始工作,可以跳过
+        return context.isWorkStarted();
+    }
+}

+ 157 - 0
src/com/mes/step/StepFactory.java

@@ -0,0 +1,157 @@
+package com.mes.step;
+
+import com.mes.core.WorkflowEngine;
+import com.mes.tcp.MessageSender;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Supplier;
+
+/**
+ * 步骤工厂 - 根据步骤ID创建对应的步骤实例
+ * 支持注册自定义步骤类型
+ */
+public class StepFactory implements WorkflowEngine.StepFactory {
+    private static final Logger log = LoggerFactory.getLogger(StepFactory.class);
+
+    // 步骤注册表
+    private final Map<String, Supplier<IWorkflowStep>> stepRegistry;
+
+    // 消息发送器(用于需要TCP通信的步骤)
+    private MessageSender messageSender;
+
+    public StepFactory() {
+        this.stepRegistry = new HashMap<>();
+        registerDefaultSteps();
+    }
+
+    public StepFactory(MessageSender messageSender) {
+        this();
+        this.messageSender = messageSender;
+    }
+
+    /**
+     * 注册默认步骤类型
+     */
+    private void registerDefaultSteps() {
+        // 基础流程步骤
+        register("scan_product", ScanProductStep::new);
+        register("check_quality", CheckQualityStep::new);
+        register("start_work", StartWorkStep::new);
+        register("upload_result", UploadResultStep::new);
+        register("reset_station", ResetStationStep::new);
+
+        // 物料绑定步骤
+        register("scan_material", ScanMaterialStep::new);
+        register("validate_material", ValidateMaterialStep::new);
+        register("bind_material", BindMaterialStep::new);
+
+        // 设备相关步骤
+        register("read_device_data", ReadDeviceDataStep::new);  // 读取设备数据
+        register("device_start", DeviceStartStep::new);
+        register("wait_complete", WaitCompleteStep::new);
+        register("wait_device_complete", WaitCompleteStep::new);  // 新名称
+        register("wait_device_start", WaitDeviceStartStep::new);  // 等待设备启动信号
+        register("reset_device_count", ResetDeviceCountStep::new);  // 重置设备计数
+        
+        // 心跳步骤
+        register("start_heartbeat", StartHeartbeatStep::new);
+        register("stop_heartbeat", StopHeartbeatStep::new);
+        
+        // 继电器步骤
+        register("relay_open", RelayOpenStep::new);
+        register("relay_close", RelayCloseStep::new);
+
+        // 特殊工位步骤
+        register("create_order", CreateOrderStep::new);
+
+        log.info("默认步骤注册完成,共 {} 种", stepRegistry.size());
+    }
+
+    /**
+     * 注册步骤类型
+     * @param stepId 步骤ID
+     * @param supplier 步骤创建函数
+     */
+    public void register(String stepId, Supplier<IWorkflowStep> supplier) {
+        stepRegistry.put(stepId, supplier);
+    }
+
+    /**
+     * 创建步骤实例
+     * @param stepId 步骤ID
+     * @return 步骤实例,如果未注册返回null
+     */
+    @Override
+    public IWorkflowStep createStep(String stepId) {
+        Supplier<IWorkflowStep> supplier = stepRegistry.get(stepId);
+        if (supplier == null) {
+            log.warn("未注册的步骤类型: {}", stepId);
+            return null;
+        }
+
+        IWorkflowStep step = supplier.get();
+
+        // 注入消息发送器
+        if (messageSender != null) {
+            injectMessageSender(step);
+        }
+
+        return step;
+    }
+
+    /**
+     * 注入消息发送器
+     */
+    private void injectMessageSender(IWorkflowStep step) {
+        if (step instanceof CheckQualityStep) {
+            ((CheckQualityStep) step).setMessageSender(messageSender);
+        } else if (step instanceof StartWorkStep) {
+            ((StartWorkStep) step).setMessageSender(messageSender);
+        } else if (step instanceof UploadResultStep) {
+            ((UploadResultStep) step).setMessageSender(messageSender);
+        } else if (step instanceof ValidateMaterialStep) {
+            ((ValidateMaterialStep) step).setMessageSender(messageSender);
+        } else if (step instanceof BindMaterialStep) {
+            ((BindMaterialStep) step).setMessageSender(messageSender);
+        } else if (step instanceof CreateOrderStep) {
+            ((CreateOrderStep) step).setMessageSender(messageSender);
+        }
+    }
+
+    /**
+     * 注入工位面板引用(用于弹出扫码框)
+     */
+    public void injectPanel(IWorkflowStep step, com.mes.ui.component.WorkstationPanel panel) {
+        if (step instanceof ScanMaterialStep) {
+            ((ScanMaterialStep) step).setPanel(panel);
+        } else if (step instanceof ValidateMaterialStep) {
+            ((ValidateMaterialStep) step).setPanel(panel);
+        }
+    }
+
+    /**
+     * 检查步骤是否已注册
+     */
+    public boolean isRegistered(String stepId) {
+        return stepRegistry.containsKey(stepId);
+    }
+
+    /**
+     * 获取所有已注册的步骤ID
+     */
+    public java.util.Set<String> getRegisteredStepIds() {
+        return stepRegistry.keySet();
+    }
+
+    // Getters & Setters
+    public MessageSender getMessageSender() {
+        return messageSender;
+    }
+
+    public void setMessageSender(MessageSender messageSender) {
+        this.messageSender = messageSender;
+    }
+}

+ 54 - 0
src/com/mes/step/StopHeartbeatStep.java

@@ -0,0 +1,54 @@
+package com.mes.step;
+
+import com.mes.core.StationContext;
+import com.mes.device.IDeviceDriver;
+
+/**
+ * 停止心跳步骤
+ * 停止设备心跳定时器
+ */
+public class StopHeartbeatStep extends AbstractStep {
+
+    public StopHeartbeatStep() {
+        super("stop_heartbeat", "停止心跳");
+        this.async = false;
+        this.requiresUserInteraction = false;
+    }
+
+    @Override
+    public boolean execute(StationContext context) {
+        if (!context.isHeartbeatActive()) {
+            log.debug("[{}] 心跳未启动,跳过停止", context.getStationCode());
+            return true;
+        }
+
+        IDeviceDriver driver = context.getDeviceDriver();
+        if (driver == null) {
+            log.warn("[{}] 设备驱动未配置,跳过停止心跳", context.getStationCode());
+            context.setHeartbeatActive(false);
+            return true;
+        }
+
+        try {
+            driver.stopHeartbeat();
+            context.setHeartbeatActive(false);
+            log.info("[{}] 心跳已停止", context.getStationCode());
+            return true;
+        } catch (Exception e) {
+            log.error("[{}] 停止心跳失败: {}", context.getStationCode(), e.getMessage());
+            // 即使失败也标记为非活动状态
+            context.setHeartbeatActive(false);
+            return true;  // 不阻塞流程
+        }
+    }
+
+    @Override
+    public boolean canExecute(StationContext context) {
+        return true;  // 任何时候都可以执行
+    }
+
+    @Override
+    public boolean canSkip(StationContext context) {
+        return !context.isHeartbeatActive();
+    }
+}

+ 248 - 0
src/com/mes/step/UploadResultStep.java

@@ -0,0 +1,248 @@
+package com.mes.step;
+
+import com.mes.core.StationConfig;
+import com.mes.core.StationContext;
+import com.mes.tcp.MessageSender;
+
+/**
+ * 上传结果步骤
+ * 发送MQDW消息上传加工结果
+ * 
+ * 支持两种提交模式:
+ * - manual: 用户点击OK/NG后直接上传,不需要workStarted
+ * - auto: 需要设备信号触发workStarted后才能上传
+ */
+public class UploadResultStep extends AbstractStep {
+
+    private MessageSender messageSender;
+    private String result = "OK";  // 默认结果
+    private boolean manualMode = true;  // 默认手动模式
+
+    public UploadResultStep() {
+        super("upload_result", "上传结果");
+        this.messageType = "MQDW";
+        this.async = true;  // 需要等待服务端确认
+        this.craft = "100000";  // 默认工艺号
+        
+        // 从配置读取提交模式
+        try {
+            StationConfig config = StationConfig.getInstance();
+            this.manualMode = config.isManualSubmitMode();
+            // 手动模式需要用户点击OK/NG,自动模式不需要用户交互
+            this.requiresUserInteraction = this.manualMode;
+        } catch (Exception e) {
+            // 配置未加载时使用默认值(手动模式)
+            this.requiresUserInteraction = true;
+        }
+    }
+
+    public void setMessageSender(MessageSender messageSender) {
+        this.messageSender = messageSender;
+    }
+
+    /**
+     * 设置上传的结果
+     * @param result OK 或 NG
+     */
+    public void setResult(String result) {
+        this.result = result;
+    }
+
+    @Override
+    public boolean execute(StationContext context) {
+        String productSn = context.getProcessedProductSn();
+
+        if (productSn == null || productSn.trim().isEmpty()) {
+            log.error("[{}] 上传结果失败: 工件码为空", context.getStationCode());
+            return false;
+        }
+
+        // 防止重复提交
+        if (context.isSubmitting()) {
+            log.warn("[{}] 正在提交中,请勿重复操作", context.getStationCode());
+            return false;
+        }
+
+        context.setSubmitting(true);
+        context.setState(StationContext.WorkflowState.UPLOADING);
+
+        // 步骤1: 如果有物料且未绑定,先绑定物料
+        StationConfig config = StationConfig.getInstance();
+        if (config.isShowMaterialInput() && context.getMaterialSn() != null && !context.getMaterialSn().isEmpty()) {
+            if (!context.isMaterialBound()) {
+                log.info("[{}] 开始绑定物料: {}", context.getStationCode(), context.getMaterialSn());
+                context.setStatusMessage("正在绑定物料...", 0);
+                
+                if (messageSender != null) {
+                    boolean bindResult = messageSender.sendBindMaterial(
+                            productSn,
+                            context.getMaterialSn(),
+                            context.getUser(),
+                            "400004",  // 物料绑定工艺号
+                            context.getStationCode()
+                    );
+                    
+                    if (!bindResult) {
+                        log.error("[{}] 物料绑定失败", context.getStationCode());
+                        context.setStatusMessage("物料绑定失败,自动提交中止", -1);
+                        context.setSubmitting(false);
+                        return false;
+                    }
+                    
+                    context.setMaterialBound(true);
+                    log.info("[{}] 物料绑定成功", context.getStationCode());
+                } else {
+                    log.error("[{}] MessageSender未设置", context.getStationCode());
+                    context.setSubmitting(false);
+                    return false;
+                }
+            }
+        }
+
+        // 步骤2: 提交质量结果
+        context.setStatusMessage("正在提交结果...", 0);
+        log.info("[{}] 上传结果: sn={}, result={}, craft={}", context.getStationCode(), productSn, result, craft);
+
+        if (messageSender != null) {
+            return messageSender.sendQuality(
+                    productSn,
+                    result,
+                    context.getUser(),
+                    craft,
+                    context.getStationCode()
+            );
+        } else {
+            log.error("[{}] MessageSender未设置", context.getStationCode());
+            context.setSubmitting(false);
+            return false;
+        }
+    }
+
+    @Override
+    public void onServerResponse(StationContext context, String resultCode, String message) {
+        context.setSubmitting(false);
+
+        if ("OK".equalsIgnoreCase(resultCode)) {
+            context.setResultUploaded(true);
+            log.info("[{}] 结果上传成功", context.getStationCode());
+            
+            // 步骤3: 清零PLC计数器(地址66、68、70)
+            if (context.getDeviceDriver() != null) {
+                try {
+                    log.info("[{}] 开始清零PLC计数器", context.getStationCode());
+                    context.getDeviceDriver().writeRegister(66, 0);  // 完成数量
+                    context.getDeviceDriver().writeRegister(68, 0);  // 合格数量
+                    context.getDeviceDriver().writeRegister(70, 0);  // 不合格数量
+                    log.info("[{}] PLC计数器清零成功", context.getStationCode());
+                } catch (Exception e) {
+                    log.error("[{}] PLC计数器清零失败: {}", context.getStationCode(), e.getMessage());
+                }
+            }
+            
+            context.setStatusMessage("结果提交成功,请扫下一件", 0);
+            onSuccess(context);
+        } else {
+            context.setResultUploaded(false);
+            String errorMsg = "结果提交失败,请重试";
+            context.setStatusMessage(errorMsg, -1);
+            log.warn("[{}] 结果上传失败: {}", context.getStationCode(), resultCode);
+            onFailure(context, errorMsg);
+        }
+    }
+
+    @Override
+    public void onTimeout(StationContext context) {
+        context.setSubmitting(false);
+        super.onTimeout(context);
+    }
+
+    public void setManualMode(boolean manualMode) {
+        this.manualMode = manualMode;
+        // 手动模式需要用户交互,自动模式不需要
+        this.requiresUserInteraction = manualMode;
+    }
+
+    @Override
+    public boolean canExecute(StationContext context) {
+        // 前置校验:工件码必须存在
+        String productSn = context.getProcessedProductSn();
+        if (productSn == null || productSn.trim().isEmpty()) {
+            log.debug("[{}] UploadResultStep.canExecute: 工件码为空,不允许提交", context.getStationCode());
+            return false;
+        }
+
+        // 基本条件:质量检查通过 && 结果未上传
+        boolean basicCondition = context.isQualityPassed() && !context.isResultUploaded();
+        
+        // 如果配置了物料,需要检查物料校验状态
+        StationConfig config = StationConfig.getInstance();
+        if (config.isShowMaterialInput()) {
+            // 有物料配置:需要物料已校验
+            boolean materialCondition = context.isMaterialValidated();
+            log.debug("[{}] UploadResultStep.canExecute (with material): qualityPassed={}, materialValidated={}, resultUploaded={}", 
+                    context.getStationCode(), context.isQualityPassed(), context.isMaterialValidated(), context.isResultUploaded());
+            
+            // 判断是否需要等待设备完成
+            if (context.isDeviceEnabled()) {
+                // 有设备:需要等待设备完成
+                boolean result = basicCondition && materialCondition && context.isWorkStarted();
+                log.debug("[{}] UploadResultStep.canExecute (device enabled): workStarted={}, result={}", 
+                        context.getStationCode(), context.isWorkStarted(), result);
+                return result;
+            } else {
+                // 无设备:质量检查通过 + 物料已校验即可
+                return basicCondition && materialCondition;
+            }
+        } else {
+            // 无物料配置:按原逻辑
+            if (context.isDeviceEnabled()) {
+                // 有设备:需要等待设备完成
+                boolean result = basicCondition && context.isWorkStarted();
+                log.debug("[{}] UploadResultStep.canExecute (device enabled, no material): qualityPassed={}, workStarted={}, resultUploaded={}, result={}", 
+                        context.getStationCode(), context.isQualityPassed(), context.isWorkStarted(), context.isResultUploaded(), result);
+                return result;
+            } else {
+                // 无设备:质量检查通过即可
+                log.debug("[{}] UploadResultStep.canExecute (device disabled, no material): qualityPassed={}, resultUploaded={}, result={}", 
+                        context.getStationCode(), context.isQualityPassed(), context.isResultUploaded(), basicCondition);
+                return basicCondition;
+            }
+        }
+    }
+
+    // ========== 流程监控扩展 ==========
+
+    @Override
+    public java.util.List<StepCondition> getConditions(StationContext context) {
+        java.util.List<StepCondition> conditions = new java.util.ArrayList<>();
+        
+        if (manualMode) {
+            conditions.add(condition("qualityPassed", 
+                    "质量检查通过: " + (context.isQualityPassed() ? "是" : "否"), 
+                    context.isQualityPassed()));
+        } else {
+            conditions.add(condition("workStarted", 
+                    "工作已开始: " + (context.isWorkStarted() ? "是" : "否"), 
+                    context.isWorkStarted()));
+        }
+        
+        conditions.add(condition("resultUploaded", 
+                "结果已上传: " + (context.isResultUploaded() ? "是 (不能重复上传)" : "否"), 
+                !context.isResultUploaded()));
+        
+        conditions.add(condition("isSubmitting", 
+                "正在提交: " + (context.isSubmitting() ? "是 (请等待)" : "否"), 
+                !context.isSubmitting()));
+        
+        return conditions;
+    }
+
+    @Override
+    public void simulateSuccess(StationContext context) {
+        log.info("[{}] 模拟结果上传成功", context.getStationCode());
+        context.setSubmitting(false);
+        context.setResultUploaded(true);
+        context.setStatusMessage("(模拟) 结果提交成功,请扫下一件", 0);
+        onSuccess(context);
+    }
+}

+ 117 - 0
src/com/mes/step/ValidateMaterialStep.java

@@ -0,0 +1,117 @@
+package com.mes.step;
+
+import com.mes.core.StationContext;
+import com.mes.tcp.MessageSender;
+import com.mes.ui.component.WorkstationPanel;
+
+/**
+ * 物料校验步骤
+ * 发送VLDW消息校验物料(底护板、冷板等)是否可用
+ * 校验失败后自动重新弹出扫码框
+ */
+public class ValidateMaterialStep extends AbstractStep {
+
+    private MessageSender messageSender;
+    private WorkstationPanel panel;   // 工位面板引用
+
+    public ValidateMaterialStep() {
+        super("validate_material", "物料校验");
+        this.messageType = "VLDW";
+        this.async = true;  // 需要等待服务端响应
+        this.requiresUserInteraction = false;
+        this.craft = "400004";  // 默认工艺号
+    }
+
+    public void setMessageSender(MessageSender messageSender) {
+        this.messageSender = messageSender;
+    }
+
+    /**
+     * 设置工位面板引用(用于弹出扫码框)
+     */
+    public void setPanel(WorkstationPanel panel) {
+        this.panel = panel;
+    }
+
+    @Override
+    public boolean execute(StationContext context) {
+        String productSn = context.getProcessedProductSn();
+        String materialSn = context.getMaterialSn();
+
+        if (productSn == null || productSn.isEmpty()) {
+            log.error("[{}] 物料校验失败: 工件码为空", context.getStationCode());
+            return false;
+        }
+
+        if (materialSn == null || materialSn.isEmpty()) {
+            log.error("[{}] 物料校验失败: 物料码为空", context.getStationCode());
+            return false;
+        }
+
+        context.setStatusMessage("正在校验物料...", 0);
+
+        log.info("[{}] 发送物料校验: product={}, material={}, craft={}", 
+                context.getStationCode(), productSn, materialSn, craft);
+
+        // 发送校验消息
+        if (messageSender != null) {
+            return messageSender.sendValidateMaterial(
+                    productSn,
+                    materialSn,
+                    context.getUser(),
+                    craft,
+                    context.getStationCode()
+            );
+        } else {
+            log.error("[{}] MessageSender未设置", context.getStationCode());
+            return false;
+        }
+    }
+
+    @Override
+    public void onServerResponse(StationContext context, String result, String message) {
+        if ("OK".equalsIgnoreCase(result)) {
+            context.setMaterialValidated(true);
+            context.setStatusMessage("物料校验通过", 0);
+            log.info("[{}] 物料校验通过", context.getStationCode());
+            onSuccess(context);
+        } else {
+            context.setMaterialValidated(false);
+            context.setMaterialSn(null);  // 清空物料码
+            String errorMsg = getErrorMessage(result, message);
+            context.setStatusMessage(errorMsg, -1);
+            log.warn("[{}] 物料校验失败: {}", context.getStationCode(), errorMsg);
+            
+            // 校验失败后自动重新弹出扫码框
+            if (panel != null) {
+                log.info("[{}] 物料校验失败,自动重新弹出扫码框", context.getStationCode());
+                javax.swing.SwingUtilities.invokeLater(() -> panel.requestMaterialScan());
+            }
+            
+            onFailure(context, errorMsg);
+        }
+    }
+
+    @Override
+    protected String getErrorMessage(String resultCode, String message) {
+        switch (resultCode.toUpperCase()) {
+            case "NE":
+                return "物料不存在";
+            case "US":
+                return "物料已被使用";
+            case "EX":
+                return "物料已过期";
+            case "NM":
+                return "物料不匹配";
+            default:
+                return "物料校验失败: " + resultCode;
+        }
+    }
+
+    @Override
+    public boolean canExecute(StationContext context) {
+        return context.isQualityPassed() && 
+               context.getMaterialSn() != null && 
+               !context.getMaterialSn().isEmpty();
+    }
+}

+ 320 - 0
src/com/mes/step/WaitCompleteStep.java

@@ -0,0 +1,320 @@
+package com.mes.step;
+
+import com.mes.core.StationConfig;
+import com.mes.core.StationContext;
+import com.mes.device.IDeviceDriver;
+
+import java.util.concurrent.*;
+
+/**
+ * 等待完成步骤(重命名为 wait_device_complete)
+ * 支持多种完成条件:
+ * - signal: 通过完成信号判断
+ * - count: 通过完成数量判断
+ * - signal_with_time: 信号+最小运行时间判断
+ */
+public class WaitCompleteStep extends AbstractStep {
+
+    private ScheduledExecutorService pollExecutor;
+    private ScheduledFuture<?> pollTask;
+    private int pollIntervalMs = 500;  // 轮询间隔,默认500ms
+    private int maxWaitTimeMs = 0;  // 最大等待时间,默认0(无限等待)
+    private volatile boolean waiting = false;
+    
+    // 完成条件类型
+    private String completeConditionType = "signal";  // signal | count | signal_with_time
+    private int minRunningTime = 0;  // 最小运行时间(ms),用于signal_with_time
+    private int runningValue = 1;    // 运行中的值
+    private int stoppedValue = 0;    // 停止的值
+    
+    // 完成回调
+    private CompletionCallback callback;
+
+    public interface CompletionCallback {
+        void onComplete(StationContext context, boolean success, String result);
+    }
+
+    public WaitCompleteStep() {
+        super("wait_device_complete", "等待设备完成");
+        this.async = true;  // 异步等待
+        this.requiresUserInteraction = false;
+        this.timeoutMs = 0;  // 默认无超时,无限等待
+    }
+
+    @Override
+    public void configure(java.util.Map<String, Object> config) {
+        super.configure(config);
+        this.pollIntervalMs = getConfigInt("poll_interval", 500);
+        this.maxWaitTimeMs = getConfigInt("max_wait_time", 0);  // 默认0,无限等待
+        this.timeoutMs = maxWaitTimeMs;
+        
+        // 从配置加载完成条件
+        StationConfig stationConfig = StationConfig.getInstance();
+        StationConfig.CompleteCondition condition = stationConfig.getCompleteCondition();
+        if (condition != null) {
+            this.completeConditionType = condition.getType();
+            this.minRunningTime = condition.getMinRunningTime();
+            this.runningValue = condition.getRunningValue();
+            this.stoppedValue = condition.getStoppedValue();
+        }
+    }
+
+    public void setCallback(CompletionCallback callback) {
+        this.callback = callback;
+    }
+
+    @Override
+    public boolean execute(StationContext context) {
+        // 检查设备是否启用
+        if (!context.isDeviceEnabled()) {
+            log.info("[{}] 设备未启用,跳过等待", context.getStationCode());
+            return true;
+        }
+
+        IDeviceDriver driver = context.getDeviceDriver();
+        if (driver == null) {
+            log.warn("[{}] 设备驱动未配置,跳过等待", context.getStationCode());
+            return true;
+        }
+
+        context.setState(StationContext.WorkflowState.WAITING);
+        context.setStatusMessage("等待加工完成...", 0);
+
+        // 启动轮询
+        startPolling(context, driver);
+
+        waiting = true;
+        return true;
+    }
+
+    /**
+     * 启动轮询检查
+     */
+    private void startPolling(StationContext context, IDeviceDriver driver) {
+        if (pollExecutor == null || pollExecutor.isShutdown()) {
+            pollExecutor = Executors.newSingleThreadScheduledExecutor(r -> {
+                Thread t = new Thread(r, "DevicePoll-" + context.getStationCode());
+                t.setDaemon(true);
+                return t;
+            });
+        }
+
+        pollTask = pollExecutor.scheduleAtFixedRate(() -> {
+            try {
+                boolean isComplete = checkCompleteCondition(context, driver);
+                
+                if (isComplete) {
+                    log.info("[{}] 设备加工完成 (条件类型: {})", context.getStationCode(), completeConditionType);
+                    stopPolling();
+
+                    // 读取完成数据
+                    int finishedCount = driver.getFinishedCount();
+                    int qualifiedCount = driver.getQualifiedCount();
+                    context.setFinishedCount(finishedCount);
+                    context.setQualifiedCount(qualifiedCount);
+
+                    // 通知完成
+                    waiting = false;
+                    
+                    if (callback != null) {
+                        // 判断是否合格
+                        String result = qualifiedCount >= context.getPresetCount() ? "OK" : "NG";
+                        callback.onComplete(context, true, result);
+                    }
+
+                    onSuccess(context);
+                } else {
+                    // 更新显示
+                    int finishedCount = driver.getFinishedCount();
+                    int presetCount = driver.getPresetCount();
+                    context.setFinishedCount(finishedCount);
+                    context.setPresetCount(presetCount);
+
+                    // 读取第二行设备数据(如果配置了)
+                    StationConfig config = StationConfig.getInstance();
+                    StationConfig.DeviceInfoRow row2 = config.getDeviceInfoRow(1);
+                    if (row2 != null) {
+                        // 获取第二行专用驱动(不同IP设备)
+                        IDeviceDriver driver2 = context.getDeviceDriver2();
+                        if (driver2 != null && driver2.isConnected()) {
+                            // 第二个设备已连接,读取数据
+                            context.setPresetCount2(driver2.readRegister(row2.getPresetAddress()));
+                            context.setFinishedCount2(driver2.readRegister(row2.getFinishedAddress()));
+                            context.setDeviceConnected2(true);
+                        } else {
+                            // 第二个设备未连接,设置为-1表示未连接
+                            context.setPresetCount2(-1);
+                            context.setFinishedCount2(-1);
+                            context.setDeviceConnected2(false);
+                            if (driver2 == null) {
+                                log.debug("[{}] 第二行设备未配置驱动 (connection_index={})", 
+                                         context.getStationCode(), row2.getConnectionIndex());
+                            } else {
+                                log.warn("[{}] 第二行设备连接断开", context.getStationCode());
+                            }
+                        }
+                    }
+
+                    context.updateUI();
+                }
+            } catch (Exception e) {
+                log.error("[{}] 轮询设备状态异常: {}", context.getStationCode(), e.getMessage());
+            }
+        }, 0, pollIntervalMs, TimeUnit.MILLISECONDS);
+    }
+    
+    /**
+     * 根据配置的完成条件类型检查是否完成
+     */
+    private boolean checkCompleteCondition(StationContext context, IDeviceDriver driver) {
+        switch (completeConditionType) {
+            case "signal":
+                // 通过完成信号判断
+                // 先检查context(支持调试控制),再检查driver(读PLC)
+                return driver.isWorkComplete();
+                
+            case "count":
+                // 通过完成数量判断
+                // 优先使用context的值(经过拦截器,支持调试控制)
+                int finishedCount = context.getFinishedCount();
+                if (finishedCount == 0) {
+                    finishedCount = driver.getFinishedCount();
+                }
+                int presetCount = context.getPresetCount();
+                return presetCount > 0 && finishedCount >= presetCount;
+                
+            case "signal_with_time":
+                // 信号+最小运行时间判断
+                // 先检查设备是否已启动(context经过拦截器,支持调试控制)
+                if (!context.isDeviceStarted()) {
+                    // 检查设备是否正在运行
+                    if (driver.isDeviceStarted()) {
+                        context.setDeviceStarted(true);
+                        context.setDeviceStartTime(System.currentTimeMillis());
+                        log.info("[{}] 检测到设备启动", context.getStationCode());
+                    }
+                    return false;
+                }
+                
+                // 设备已启动,检查是否停止
+                boolean deviceStopped = !driver.isDeviceStarted();
+                if (deviceStopped) {
+                    // 检查是否满足最小运行时间
+                    long runningTime = System.currentTimeMillis() - context.getDeviceStartTime();
+                    if (runningTime >= minRunningTime) {
+                        log.info("[{}] 设备停止,运行时间: {}ms >= {}ms", 
+                                context.getStationCode(), runningTime, minRunningTime);
+                        return true;
+                    } else {
+                        log.debug("[{}] 设备停止但运行时间不足: {}ms < {}ms", 
+                                context.getStationCode(), runningTime, minRunningTime);
+                    }
+                }
+                return false;
+                
+            default:
+                log.warn("[{}] 未知的完成条件类型: {}, 使用默认signal", 
+                        context.getStationCode(), completeConditionType);
+                return driver.isWorkComplete();
+        }
+    }
+
+    /**
+     * 停止轮询
+     */
+    private void stopPolling() {
+        if (pollTask != null && !pollTask.isDone()) {
+            pollTask.cancel(false);
+            pollTask = null;
+        }
+    }
+
+    @Override
+    public void onSuccess(StationContext context) {
+        stopPolling();
+        super.onSuccess(context);
+        // 标记工作已完成(复用 workStarted 状态)
+        context.setWorkStarted(true);
+        context.setStatusMessage("加工完成", 0);
+        notifyAsyncComplete(true);
+    }
+
+    @Override
+    public void onFailure(StationContext context, String error) {
+        stopPolling();
+        super.onFailure(context, error);
+        waiting = false;
+    }
+
+    @Override
+    public void onTimeout(StationContext context) {
+        stopPolling();
+        waiting = false;
+        super.onTimeout(context);
+    }
+
+    @Override
+    public boolean canExecute(StationContext context) {
+        // 质量检查通过即可执行(不再依赖 workStarted,因为 start_work 步骤已弃用)
+        return context.isQualityPassed();
+    }
+
+    @Override
+    public boolean canSkip(StationContext context) {
+        // 设备未启用时跳过
+        return !context.isDeviceEnabled();
+    }
+
+    /**
+     * 手动触发完成(用于手动工位或按钮触发)
+     */
+    public void manualComplete(StationContext context, String result) {
+        if (waiting) {
+            stopPolling();
+            waiting = false;
+            
+            if (callback != null) {
+                callback.onComplete(context, true, result);
+            }
+            onSuccess(context);
+        }
+    }
+
+    /**
+     * 销毁
+     */
+    public void destroy() {
+        stopPolling();
+        if (pollExecutor != null && !pollExecutor.isShutdown()) {
+            pollExecutor.shutdownNow();
+        }
+    }
+
+    // Getters & Setters
+    public int getPollIntervalMs() {
+        return pollIntervalMs;
+    }
+
+    public void setPollIntervalMs(int pollIntervalMs) {
+        this.pollIntervalMs = pollIntervalMs;
+    }
+
+    public int getMaxWaitTimeMs() {
+        return maxWaitTimeMs;
+    }
+
+    public void setMaxWaitTimeMs(int maxWaitTimeMs) {
+        this.maxWaitTimeMs = maxWaitTimeMs;
+        this.timeoutMs = maxWaitTimeMs;
+    }
+
+    public boolean isWaiting() {
+        return waiting;
+    }
+
+    @Override
+    public void reset() {
+        stopPolling();
+        waiting = false;
+    }
+}

+ 136 - 0
src/com/mes/step/WaitDeviceStartStep.java

@@ -0,0 +1,136 @@
+package com.mes.step;
+
+import com.mes.core.StationContext;
+import com.mes.device.IDeviceDriver;
+
+import java.util.concurrent.*;
+
+/**
+ * 等待设备启动步骤
+ * 轮询PLC检查设备是否已启动
+ */
+public class WaitDeviceStartStep extends AbstractStep {
+
+    private ScheduledExecutorService pollExecutor;
+    private ScheduledFuture<?> pollTask;
+    private int pollIntervalMs = 200;
+    private int maxWaitTimeMs = 0;  // 最大等待时间,默认0(无限等待)
+    private volatile boolean waiting = false;
+
+    public WaitDeviceStartStep() {
+        super("wait_device_start", "等待设备启动");
+        this.async = true;
+        this.requiresUserInteraction = false;
+        this.timeoutMs = 0;  // 默认无超时,无限等待
+    }
+
+    @Override
+    public void configure(java.util.Map<String, Object> config) {
+        super.configure(config);
+        this.pollIntervalMs = getConfigInt("poll_interval", 200);
+        this.maxWaitTimeMs = getConfigInt("max_wait_time", 0);  // 默认0,无限等待
+        this.timeoutMs = maxWaitTimeMs;
+    }
+
+    @Override
+    public boolean execute(StationContext context) {
+        if (!context.isDeviceEnabled()) {
+            log.info("[{}] 设备未启用,跳过等待启动", context.getStationCode());
+            return true;
+        }
+
+        IDeviceDriver driver = context.getDeviceDriver();
+        if (driver == null) {
+            log.warn("[{}] 设备驱动未配置,跳过等待启动", context.getStationCode());
+            return true;
+        }
+
+        context.setState(StationContext.WorkflowState.WAITING);
+        context.setStatusMessage("等待设备启动...", 0);
+
+        startPolling(context, driver);
+        waiting = true;
+        return true;
+    }
+
+    private void startPolling(StationContext context, IDeviceDriver driver) {
+        if (pollExecutor == null || pollExecutor.isShutdown()) {
+            pollExecutor = Executors.newSingleThreadScheduledExecutor(r -> {
+                Thread t = new Thread(r, "WaitStart-" + context.getStationCode());
+                t.setDaemon(true);
+                return t;
+            });
+        }
+
+        pollTask = pollExecutor.scheduleAtFixedRate(() -> {
+            try {
+                // 先检查context(经过拦截器,支持调试面板控制)
+                if (context.isDeviceStarted()) {
+                    log.info("[{}] 设备已启动(调试/context)", context.getStationCode());
+                    stopPolling();
+                    waiting = false;
+                    context.setStatusMessage("设备运行中...", 0);
+                    onSuccess(context);
+                    return;
+                }
+                
+                if (driver.isDeviceStarted()) {
+                    log.info("[{}] 设备已启动", context.getStationCode());
+                    stopPolling();
+                    waiting = false;
+
+                    context.setDeviceStarted(true);
+                    context.setStatusMessage("设备运行中...", 0);
+                    onSuccess(context);
+                }
+            } catch (Exception e) {
+                log.error("[{}] 轮询设备启动状态异常: {}", context.getStationCode(), e.getMessage());
+            }
+        }, 0, pollIntervalMs, TimeUnit.MILLISECONDS);
+    }
+
+    private void stopPolling() {
+        if (pollTask != null && !pollTask.isDone()) {
+            pollTask.cancel(false);
+            pollTask = null;
+        }
+    }
+
+    @Override
+    public void onSuccess(StationContext context) {
+        stopPolling();
+        super.onSuccess(context);
+        notifyAsyncComplete(true);
+    }
+
+    @Override
+    public void onFailure(StationContext context, String error) {
+        stopPolling();
+        super.onFailure(context, error);
+        waiting = false;
+    }
+
+    @Override
+    public void onTimeout(StationContext context) {
+        stopPolling();
+        waiting = false;
+        super.onTimeout(context);
+    }
+
+    @Override
+    public boolean canExecute(StationContext context) {
+        return context.isQualityPassed();
+    }
+
+    @Override
+    public boolean canSkip(StationContext context) {
+        return !context.isDeviceEnabled();
+    }
+
+    public void destroy() {
+        stopPolling();
+        if (pollExecutor != null && !pollExecutor.isShutdown()) {
+            pollExecutor.shutdownNow();
+        }
+    }
+}

+ 136 - 0
src/com/mes/tcp/MesClientHandler.java

@@ -0,0 +1,136 @@
+package com.mes.tcp;
+
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInboundHandlerAdapter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * MES客户端消息处理器
+ * 接收服务端响应,解析后分发到对应工位
+ */
+public class MesClientHandler extends ChannelInboundHandlerAdapter {
+    private static final Logger log = LoggerFactory.getLogger(MesClientHandler.class);
+
+    private final MesTcpClient tcpClient;
+    private final MessageDispatcher dispatcher;
+
+    public MesClientHandler(MesTcpClient tcpClient, MessageDispatcher dispatcher) {
+        this.tcpClient = tcpClient;
+        this.dispatcher = dispatcher;
+    }
+
+    @Override
+    public void channelActive(ChannelHandlerContext ctx) throws Exception {
+        log.info("TCP连接已建立: {}", ctx.channel().remoteAddress());
+    }
+
+    @Override
+    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
+        log.info("TCP连接已断开");
+        if (tcpClient != null) {
+            tcpClient.onDisconnected();
+        }
+    }
+
+    @Override
+    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
+        String rawMessage = msg.toString();
+        log.info("==============================================================================");
+        log.info("| TCP返回报文内容: {}", rawMessage);
+
+        // 格式化消息(移除前缀等)
+        String mesMessage = formatResult(rawMessage);
+        if (mesMessage.isEmpty()) {
+            log.info("| TCP返回报文内容格式错误, 弃用!");
+            log.info("==============================================================================");
+            return;
+        }
+
+        // 解析消息类型
+        String msgType = getMsgType(mesMessage);
+        log.info("| TCP返回报文类型: {}", msgType);
+
+        // 解析结果码
+        String result = getResult(mesMessage);
+        log.info("| TCP返回报文结果: {}", result);
+
+        // 解析工位号
+        String stationCode = getStationCode(mesMessage);
+        log.info("| TCP返回工位号: {}", stationCode);
+        log.info("==============================================================================");
+
+        // 分发消息
+        if (dispatcher != null && stationCode != null) {
+            dispatcher.dispatch(stationCode, msgType, result, mesMessage);
+        }
+    }
+
+    @Override
+    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
+        log.error("TCP通信异常: {}", cause.getMessage());
+        ctx.close();
+    }
+
+    /**
+     * 格式化消息(移除状态前缀)
+     */
+    private String formatResult(String msg) {
+        if (msg == null || msg.length() < 3) {
+            return "";
+        }
+
+        String prefix = msg.substring(0, 2).trim();
+        if ("OK".equals(prefix)) {
+            return msg.substring(3);
+        }
+        return "";
+    }
+
+    /**
+     * 获取消息类型
+     * 消息格式: aaaabbbbbABW[MSGTYPE]GW...
+     * 消息类型位于位置12-16
+     */
+    private String getMsgType(String msg) {
+        if (msg.length() < 16) {
+            return "";
+        }
+        return msg.substring(12, 16);
+    }
+
+    /**
+     * 获取工位号
+     * 工位号位于固定位置18-24
+     */
+    private String getStationCode(String msg) {
+        // 固定位置解析(与OP150版本一致)
+        if (msg.length() >= 24) {
+            return msg.substring(18, 24).trim();
+        }
+        // 备用方案:查找GW字段
+        int gwIndex = msg.indexOf("GW");
+        if (gwIndex >= 0 && msg.length() >= gwIndex + 8) {
+            return msg.substring(gwIndex + 2, gwIndex + 8).trim();
+        }
+        return null;
+    }
+
+    /**
+     * 获取结果码
+     * 结果码位于固定位置72-74
+     */
+    private String getResult(String msg) {
+        // 固定位置解析(与OP150版本一致)
+        if (msg.length() >= 74) {
+            return msg.substring(72, 74).trim();
+        }
+        // 备用方案:查找RS字段
+        int rsIndex = msg.indexOf("RS");
+        if (rsIndex >= 0 && msg.length() >= rsIndex + 4) {
+            return msg.substring(rsIndex + 2, rsIndex + 4).trim();
+        }
+        return "";
+    }
+}

+ 183 - 0
src/com/mes/tcp/MesMessageDecoder.java

@@ -0,0 +1,183 @@
+package com.mes.tcp;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufUtil;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.handler.codec.ByteToMessageDecoder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+
+/**
+ * MES消息解码器 - 解析TCP消息
+ * 处理粘包/拆包问题,与XDecoder功能相同
+ */
+public class MesMessageDecoder extends ByteToMessageDecoder {
+    private static final Logger log = LoggerFactory.getLogger(MesMessageDecoder.class);
+    
+    private static final int PACKET_SIZE = 46;       // 最短包长度
+    private static final int PACKET_MAX_SIZE = 1000; // 最长包长度
+    private static final int FIXED_LENGTH = 96;      // 标准报文长度
+    private static final String SPLIT = "bbbbfffffARW";  // 报文分隔符
+
+    // 用来临时保留没有处理过的请求报文
+    private ByteBuf tempMsg = Unpooled.buffer();
+
+    @Override
+    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
+        int readableBytes = in.readableBytes();
+        log.info("收到TCP数据, 长度: {}", readableBytes);
+        
+        // 合并报文
+        ByteBuf message;
+        int tmpMsgSize = tempMsg.readableBytes();
+        
+        // 如果暂存有上一次余下的请求报文,则合并
+        if (tmpMsgSize > 0) {
+            message = Unpooled.buffer();
+            message.writeBytes(tempMsg);
+            message.writeBytes(in);
+            log.debug("合并暂存数据, 暂存长度: {}, 合并后长度: {}", tmpMsgSize, message.readableBytes());
+        } else {
+            message = in;
+        }
+
+        int size;
+        while (true) {
+            size = message.readableBytes();
+            if (size >= PACKET_SIZE) {
+                String retType = "NG:";
+                int qcsize = 0;
+                String str = hexStringToAscii(ByteBufUtil.hexDump(message));
+                String[] lists = str.split(SPLIT);
+
+                if (lists.length == 1 && size > PACKET_MAX_SIZE) {
+                    // 大于最大包长度
+                    byte[] request = new byte[size - PACKET_MAX_SIZE];
+                    message.readBytes(request);
+                    out.add(retType + hexStringToAscii(ByteBufUtil.hexDump(Unpooled.copiedBuffer(request))));
+                    break;
+                }
+
+                if (lists[0].length() > 0) {
+                    byte[] request = new byte[lists[0].length()];
+                    message.readBytes(request);
+                    out.add(retType + hexStringToAscii(ByteBufUtil.hexDump(Unpooled.copiedBuffer(request))));
+                } else {
+                    if (lists.length > 1) {
+                        String ss = lists[1];
+                        if (ss.length() <= 4) {
+                            qcsize += ss.length();
+                        } else {
+                            String msgType = getMsgType(SPLIT + ss);
+                            if (!isMsgTypeOk(msgType)) {
+                                qcsize += SPLIT.length() + ss.length();
+                            } else {
+                                int ppsize = checkResultSize(ss, msgType);
+                                if (ppsize > 0) {
+                                    qcsize = ppsize;
+                                    retType = "OK:";
+                                } else {
+                                    qcsize += SPLIT.length() + ss.length();
+                                }
+                            }
+                        }
+                    }
+                }
+
+                if (qcsize > 0) {
+                    byte[] request = new byte[qcsize];
+                    message.readBytes(request);
+                    String decoded = retType + hexStringToAscii(ByteBufUtil.hexDump(Unpooled.copiedBuffer(request)));
+                    log.info("解码消息: {}", decoded);
+                    out.add(decoded);
+                }
+            } else {
+                break;
+            }
+        }
+
+        // 多余的报文存起来
+        size = message.readableBytes();
+        if (size != 0) {
+            tempMsg.clear();
+            tempMsg.writeBytes(message.readBytes(size));
+        }
+    }
+
+    /**
+     * 检查结果报文大小
+     */
+    private int checkResultSize(String str, String msgType) {
+        str = SPLIT + str;
+        int tpsize = 0;
+        
+        switch (msgType) {
+            case "SYNR": // 同步
+            case "AXTW": // 心跳
+            case "ACLW": // 重连
+                if (str.length() >= 46) {
+                    tpsize = 46;
+                }
+                break;
+            case "MCJW":
+            case "AQDW":
+            case "MBDW":
+            case "MJBW":
+            case "MQDW":
+            case "MKSW":
+            case "MSBW":
+            case "MCSW":
+            case "AQRW":
+            case "VLDW":
+            default:
+                if (str.length() >= FIXED_LENGTH) {
+                    tpsize = FIXED_LENGTH;
+                }
+                break;
+        }
+        return tpsize;
+    }
+
+    /**
+     * 获取消息类型
+     */
+    private String getMsgType(String msg) {
+        if (msg.length() < 16) {
+            return "";
+        }
+        return msg.substring(12, 16);
+    }
+
+    /**
+     * 判断报文类型是否有效
+     */
+    private boolean isMsgTypeOk(String msgType) {
+        String[] validTypes = {
+            "SYNR", "AXTW", "ACLW", "MCJW", "AQDW", "MBDW", 
+            "MJBW", "VLDW", "MQDW", "MKSW", "MSBW", "MCSW", "AQRW"
+        };
+        for (String type : validTypes) {
+            if (type.equals(msgType)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * 16进制字符串转ASCII
+     */
+    private String hexStringToAscii(String hexString) {
+        StringBuilder sbuilder = new StringBuilder();
+        for (int i = 0; i < hexString.length(); i += 2) {
+            String hexByte = hexString.substring(i, i + 2);
+            int byteValue = Integer.parseInt(hexByte, 16);
+            char c = (char) byteValue;
+            sbuilder.append(c);
+        }
+        return sbuilder.toString();
+    }
+}

+ 456 - 0
src/com/mes/tcp/MesTcpClient.java

@@ -0,0 +1,456 @@
+package com.mes.tcp;
+
+import com.mes.core.StationConfig;
+import com.mes.util.DateLocalUtils;
+import io.netty.bootstrap.Bootstrap;
+import io.netty.channel.*;
+import io.netty.channel.nio.NioEventLoopGroup;
+import io.netty.channel.socket.SocketChannel;
+import io.netty.channel.socket.nio.NioSocketChannel;
+import io.netty.handler.codec.string.StringEncoder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Timer;
+import java.util.TimerTask;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * MES TCP客户端 - 封装Netty通信
+ * 实现MessageSender接口,提供消息发送功能
+ */
+public class MesTcpClient implements MessageSender {
+    private static final Logger log = LoggerFactory.getLogger(MesTcpClient.class);
+
+    // 配置
+    private final String serverIp;
+    private final int serverPort;
+    private final String lineSn;
+
+    // Netty组件
+    private EventLoopGroup group;
+    private Bootstrap bootstrap;
+    private Channel channel;
+    private ChannelFuture future;
+
+    // 状态
+    private volatile boolean connected = false;
+    private volatile boolean reconnecting = false;
+    private volatile boolean allowReconnect = false;  // 是否允许重新同步(参考OP150)
+
+    // 心跳
+    private Timer heartBeatTimer;
+    private int heartBeatCycle = 60;  // 秒
+    
+    // 断线检测定时器(参考OP150的heartBeatIconTimer)
+    private Timer reconnectCheckTimer;
+    private int reconnectCheckCycle = 1;  // 秒,参考OP150的heart_icon_cycle
+
+    // 消息分发器
+    private MessageDispatcher dispatcher;
+
+    // 连接监听器
+    private ConnectionListener connectionListener;
+
+    public interface ConnectionListener {
+        void onConnected();
+        void onDisconnected();
+        void onReconnecting();
+    }
+
+    public MesTcpClient(String serverIp, int serverPort, String lineSn) {
+        this.serverIp = serverIp;
+        this.serverPort = serverPort;
+        this.lineSn = lineSn;
+    }
+
+    public MesTcpClient(StationConfig config) {
+        this(config.getServerIp(), config.getTcpPort(), config.getLineSn());
+        this.heartBeatCycle = config.getHeartBeatCycle();
+    }
+
+    /**
+     * 连接服务器
+     */
+    public void connect() {
+        if (connected) {
+            log.warn("TCP已连接,无需重复连接");
+            return;
+        }
+
+        group = new NioEventLoopGroup();
+        bootstrap = new Bootstrap();
+
+        MesTcpClient self = this;
+
+        bootstrap.group(group)
+                .channel(NioSocketChannel.class)
+                .option(ChannelOption.SO_KEEPALIVE, true)
+                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
+                .handler(new ChannelInitializer<SocketChannel>() {
+                    @Override
+                    protected void initChannel(SocketChannel ch) {
+                        ch.pipeline()
+                                .addLast(new MesMessageDecoder())
+                                .addLast(new StringEncoder())
+                                .addLast(new MesClientHandler(self, dispatcher));
+                    }
+                });
+
+        doConnect();
+    }
+
+    /**
+     * 执行连接
+     */
+    private void doConnect() {
+        try {
+            log.info("正在连接MES服务器: {}:{}", serverIp, serverPort);
+            future = bootstrap.connect(serverIp, serverPort);
+            
+            // 添加连接监听器,参考OP150实现持续重连
+            future.addListener((ChannelFutureListener) future1 -> {
+                if (future1.isSuccess()) {
+                    // 连接成功
+                    channel = future1.channel();
+                    connected = true;
+                    reconnecting = false;
+                    allowReconnect = true;  // 连接成功后,允许下次断开时重新同步
+
+                    log.info("MES服务器连接成功");
+
+                    if (connectionListener != null) {
+                        connectionListener.onConnected();
+                    }
+
+                    // 启动心跳
+                    startHeartBeat();
+                    
+                } else {
+                    // 连接失败,持续重连(参考OP150)
+                    connected = false;
+                    log.error("MES服务器连接失败: {}", future1.cause().getMessage());
+                    log.info("连接失败,10秒后进行断线重连");
+                    
+                    // 10秒后递归调用doConnect(),持续重连
+                    future1.channel().eventLoop().schedule(() -> {
+                        doConnect();
+                    }, 10, TimeUnit.SECONDS);
+                }
+            });
+
+        } catch (Exception e) {
+            log.error("MES服务器连接异常: {}", e.getMessage());
+            connected = false;
+            reconnecting = false;
+            
+            // 异常情况下也要持续重连
+            if (group != null && !group.isShutdown()) {
+                group.schedule(() -> {
+                    doConnect();
+                }, 10, TimeUnit.SECONDS);
+            }
+        }
+    }
+
+    /**
+     * 断开连接
+     */
+    public void disconnect() {
+        stopHeartBeat();
+
+        if (channel != null && channel.isOpen()) {
+            channel.close();
+        }
+
+        if (group != null) {
+            group.shutdownGracefully();
+        }
+
+        connected = false;
+        log.info("TCP连接已断开");
+
+        if (connectionListener != null) {
+            connectionListener.onDisconnected();
+        }
+    }
+
+    /**
+     * 安排重连
+     */
+    private void scheduleReconnect() {
+        if (reconnecting) {
+            return;
+        }
+
+        reconnecting = true;
+
+        if (connectionListener != null) {
+            connectionListener.onReconnecting();
+        }
+
+        // 5秒后重连(doConnect内部会持续重连直到成功)
+        if (group != null && !group.isShutdown()) {
+            group.schedule(() -> {
+                log.info("尝试重新连接...");
+                doConnect();
+            }, 5, TimeUnit.SECONDS);
+        }
+    }
+
+    /**
+     * 启动心跳
+     */
+    private void startHeartBeat() {
+        stopHeartBeat();
+
+        // 定时器1:心跳发送定时器(60秒)- 只负责发送心跳
+        heartBeatTimer = new Timer("HeartBeat", true);
+        heartBeatTimer.schedule(new TimerTask() {
+            @Override
+            public void run() {
+                if (connected && dispatcher != null) {
+                    // TCP连接正常,向所有注册的工位发送心跳
+                    for (String stationCode : dispatcher.getRegisteredStations()) {
+                        sendHeartBeat(stationCode);
+                    }
+                }
+            }
+        }, 100, heartBeatCycle * 1000L);
+        
+        // 定时器2:断线检测定时器(1秒)- 快速检测并重新同步(参考OP150)
+        startReconnectCheck();
+    }
+    
+    /**
+     * 启动断线检测定时器(参考OP150的heartBeatIconTimer)
+     */
+    private void startReconnectCheck() {
+        stopReconnectCheck();
+        
+        reconnectCheckTimer = new Timer("ReconnectCheck", true);
+        reconnectCheckTimer.schedule(new TimerTask() {
+            @Override
+            public void run() {
+                if (!connected && allowReconnect) {
+                    // TCP连接已断开,触发重新连接(参考OP150)
+                    log.info("TCP连接已断开, 在心跳循环中, 触发重新连接");
+                    allowReconnect = false;  // 只允许重新同步一次
+                    
+                    // 不是直接发送消息,而是触发重连
+                    // 重连成功后会在onConnected()中自动发送SYNR
+                    scheduleReconnect();
+                }
+            }
+        }, 100, reconnectCheckCycle * 1000L);
+    }
+
+    /**
+     * 停止心跳
+     */
+    private void stopHeartBeat() {
+        if (heartBeatTimer != null) {
+            heartBeatTimer.cancel();
+            heartBeatTimer = null;
+        }
+        stopReconnectCheck();
+    }
+    
+    /**
+     * 停止断线检测定时器
+     */
+    private void stopReconnectCheck() {
+        if (reconnectCheckTimer != null) {
+            reconnectCheckTimer.cancel();
+            reconnectCheckTimer = null;
+        }
+    }
+
+    /**
+     * 处理断线
+     */
+    void onDisconnected() {
+        connected = false;
+        reconnecting = false;  // 重置重连标志,允许scheduleReconnect()执行
+        // 注意:不重置 allowReconnect,保持为 true,以便心跳定时器检测到断开时重新同步
+        if (connectionListener != null) {
+            connectionListener.onDisconnected();
+        }
+        scheduleReconnect();
+    }
+    
+    /**
+     * 检查是否需要重新同步(参考OP150)
+     * 用于心跳定时器中检测TCP断开并重新发送同步消息
+     */
+    public boolean shouldReconnect() {
+        return !connected && allowReconnect;
+    }
+    
+    /**
+     * 重置重新同步标志(参考OP150)
+     * 重新同步后调用,避免重复同步
+     */
+    public void resetReconnectFlag() {
+        allowReconnect = false;
+    }
+
+    // ========== MessageSender 接口实现 ==========
+
+    @Override
+    public boolean sendSync(String stationCode) {
+        String msgType = "SYNR";
+        String gw = "GW" + rightPad(stationCode, 6);
+        String da = "DA" + DateLocalUtils.getCurrentDate();
+        String zt = "ZT" + DateLocalUtils.getCurrentTimeHMS();
+        String msg = "aaaabbbbbABW" + msgType + gw + da + zt;
+        return send(msg);
+    }
+
+    @Override
+    public boolean sendHeartBeat(String stationCode) {
+        String msgType = "AXTW";
+        String gw = "GW" + rightPad(stationCode, 6);
+        String da = "DA" + DateLocalUtils.getCurrentDate();
+        String zt = "ZT" + DateLocalUtils.getCurrentTimeHMS();
+        String msg = "aaaabbbbbABW" + msgType + gw + da + zt;
+        return send(msg);
+    }
+
+    @Override
+    public boolean sendCheckQuality(String sn, String user, String stationCode) {
+        return sendMessage("AQDW", "100000", "", sn, "", user, "00", "", stationCode);
+    }
+
+    @Override
+    public boolean sendStartWork(String sn, String user, String stationCode) {
+        return sendMessage("MKSW", "", "", sn, "", user, "00", "", stationCode);
+    }
+
+    @Override
+    public boolean sendQuality(String sn, String result, String user, String stationCode) {
+        return sendQuality(sn, result, user, "100000", stationCode);
+    }
+
+    @Override
+    public boolean sendQuality(String sn, String result, String user, String craft, String stationCode) {
+        String gw = "GW" + rightPad(stationCode, 6);
+        String start = "aaaabbbbbABW";
+        String msgType = "MQDW";
+        String gy = "GY" + rightPad(craft, 6);
+        String reslx = "LX" + rightPad("", 2);
+        String id = lineSn + rightPad(sn, 36);
+        String rs = "RS" + rightPad(result, 2);
+        String da = "DA" + DateLocalUtils.getCurrentDate();
+        String zt = "ZT" + DateLocalUtils.getCurrentTimeHMS();
+        String yh = "YH" + rightPad(user, 20);
+        String msg = start + msgType + gw + gy + reslx + id + rs + da + zt + yh + "00";
+        log.info("发送质量结果: craft={}, {}", craft, msg);
+        return send(msg);
+    }
+
+    @Override
+    public boolean sendBindMaterial(String sn, String materialSn, String user, String craft, String stationCode) {
+        String param = rightPad(materialSn, 36);
+        return sendMessage("MBDW", craft, "", sn, "", user, "01", param, stationCode);
+    }
+
+    @Override
+    public boolean sendUnbindMaterial(String sn, String materialSn, String user, String craft, String stationCode) {
+        String param = rightPad(materialSn, 36);
+        return sendMessage("MJBW", craft, "", sn, "", user, "01", param, stationCode);
+    }
+
+    @Override
+    public boolean sendValidateMaterial(String sn, String materialSn, String user, String craft, String stationCode) {
+        String param = rightPad(materialSn, 36);
+        return sendMessage("VLDW", craft, "", sn, "", user, "01", param, stationCode);
+    }
+
+    @Override
+    public boolean sendCreate(String sn, String result, String user, String stationCode) {
+        // 创建工件使用MQDW,工艺号可能不同
+        return sendMessage("MQDW", "100000", "", sn, result, user, "00", "", stationCode);
+    }
+
+    @Override
+    public boolean sendMessage(String msgType, String craft, String lx, String sn,
+                               String result, String user, String paramNums, String params,
+                               String stationCode) {
+        String gw = "GW" + rightPad(stationCode, 6);
+        String start = "aaaabbbbbABW";
+        String gy = "GY" + rightPad(craft, 6);
+        String reslx = "LX" + rightPad(lx, 2);
+        String id = lineSn + rightPad(sn, 36);
+        String rs = "RS" + rightPad(result, 2);
+        String da = "DA" + DateLocalUtils.getCurrentDate();
+        String zt = "ZT" + DateLocalUtils.getCurrentTimeHMS();
+        String yh = "YH" + rightPad(user, 20);
+        String msg = start + msgType + gw + gy + reslx + id + rs + da + zt + yh + paramNums + params;
+
+        log.info("发送TCP消息: type={}, station={}", msgType, stationCode);
+        return send(msg);
+    }
+
+    /**
+     * 发送消息
+     */
+    private boolean send(String message) {
+        if (!connected || channel == null || !channel.isActive()) {
+            log.error("TCP未连接,无法发送消息");
+            return false;
+        }
+
+        try {
+            channel.writeAndFlush(message);
+            log.debug("TCP消息已发送: {}", message);
+            return true;
+        } catch (Exception e) {
+            log.error("TCP消息发送失败: {}", e.getMessage());
+            return false;
+        }
+    }
+
+    /**
+     * 右填充字符串
+     */
+    private String rightPad(String str, int size) {
+        if (str == null) {
+            str = "";
+        }
+        if (str.length() > size) {
+            str = str.substring(0, size);
+        }
+        return String.format("%-" + size + "s", str);
+    }
+
+    // ========== Getters & Setters ==========
+
+    public boolean isConnected() {
+        return connected;
+    }
+
+    public MessageDispatcher getDispatcher() {
+        return dispatcher;
+    }
+
+    public void setDispatcher(MessageDispatcher dispatcher) {
+        this.dispatcher = dispatcher;
+    }
+
+    public ConnectionListener getConnectionListener() {
+        return connectionListener;
+    }
+
+    public void setConnectionListener(ConnectionListener connectionListener) {
+        this.connectionListener = connectionListener;
+    }
+
+    public int getHeartBeatCycle() {
+        return heartBeatCycle;
+    }
+
+    public void setHeartBeatCycle(int heartBeatCycle) {
+        this.heartBeatCycle = heartBeatCycle;
+    }
+}

+ 132 - 0
src/com/mes/tcp/MessageDispatcher.java

@@ -0,0 +1,132 @@
+package com.mes.tcp;
+
+import com.mes.core.StationContext;
+import com.mes.core.WorkflowEngine;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 消息分发器 - 将服务端响应分发到对应的工位流程引擎
+ * 解决工位号硬编码问题,实现动态路由
+ */
+public class MessageDispatcher {
+    private static final Logger log = LoggerFactory.getLogger(MessageDispatcher.class);
+
+    // 工位号 -> 流程引擎映射
+    private final Map<String, WorkflowEngine> engineMap;
+
+    // 工位号 -> 上下文映射
+    private final Map<String, StationContext> contextMap;
+
+    // 消息监听器
+    private MessageListener messageListener;
+
+    /**
+     * 消息监听器接口
+     */
+    public interface MessageListener {
+        void onMessageReceived(String stationCode, String msgType, String result, String message);
+        void onUnknownStation(String stationCode, String msgType, String message);
+    }
+
+    public MessageDispatcher() {
+        this.engineMap = new ConcurrentHashMap<>();
+        this.contextMap = new ConcurrentHashMap<>();
+    }
+
+    /**
+     * 注册工位
+     * @param stationCode 工位号
+     * @param engine 流程引擎
+     * @param context 工位上下文
+     */
+    public void registerStation(String stationCode, WorkflowEngine engine, StationContext context) {
+        engineMap.put(stationCode.toUpperCase(), engine);
+        contextMap.put(stationCode.toUpperCase(), context);
+        log.info("注册工位: {}", stationCode);
+    }
+
+    /**
+     * 注销工位
+     * @param stationCode 工位号
+     */
+    public void unregisterStation(String stationCode) {
+        engineMap.remove(stationCode.toUpperCase());
+        contextMap.remove(stationCode.toUpperCase());
+        log.info("注销工位: {}", stationCode);
+    }
+
+    /**
+     * 分发消息到对应工位
+     * @param stationCode 工位号
+     * @param msgType 消息类型(AQDW, MKSW, MQDW等)
+     * @param result 结果码(OK, UD, CF等)
+     * @param message 完整消息
+     */
+    public void dispatch(String stationCode, String msgType, String result, String message) {
+        String upperCode = stationCode.toUpperCase().trim();
+
+        log.info("分发消息: 工位={}, 类型={}, 结果={}", upperCode, msgType, result);
+
+        // 通知监听器
+        if (messageListener != null) {
+            messageListener.onMessageReceived(upperCode, msgType, result, message);
+        }
+
+        // 查找对应的流程引擎
+        WorkflowEngine engine = engineMap.get(upperCode);
+        if (engine != null) {
+            // 分发到流程引擎
+            engine.onServerMessage(msgType, result, message);
+        } else {
+            log.warn("未找到工位 {} 的流程引擎", upperCode);
+            if (messageListener != null) {
+                messageListener.onUnknownStation(upperCode, msgType, message);
+            }
+        }
+    }
+
+    /**
+     * 获取工位上下文
+     * @param stationCode 工位号
+     * @return 工位上下文,如果不存在返回null
+     */
+    public StationContext getContext(String stationCode) {
+        return contextMap.get(stationCode.toUpperCase());
+    }
+
+    /**
+     * 获取流程引擎
+     * @param stationCode 工位号
+     * @return 流程引擎,如果不存在返回null
+     */
+    public WorkflowEngine getEngine(String stationCode) {
+        return engineMap.get(stationCode.toUpperCase());
+    }
+
+    /**
+     * 检查工位是否已注册
+     */
+    public boolean hasStation(String stationCode) {
+        return engineMap.containsKey(stationCode.toUpperCase());
+    }
+
+    /**
+     * 获取所有已注册的工位号
+     */
+    public java.util.Set<String> getRegisteredStations() {
+        return engineMap.keySet();
+    }
+
+    // Getters & Setters
+    public MessageListener getMessageListener() {
+        return messageListener;
+    }
+
+    public void setMessageListener(MessageListener messageListener) {
+        this.messageListener = messageListener;
+    }
+}

+ 121 - 0
src/com/mes/tcp/MessageSender.java

@@ -0,0 +1,121 @@
+package com.mes.tcp;
+
+/**
+ * 消息发送器接口 - 定义TCP消息发送方法
+ * 由具体的TCP客户端实现
+ */
+public interface MessageSender {
+
+    /**
+     * 发送同步报文
+     * @param stationCode 工位号
+     * @return 是否发送成功
+     */
+    boolean sendSync(String stationCode);
+
+    /**
+     * 发送心跳报文
+     * @param stationCode 工位号
+     * @return 是否发送成功
+     */
+    boolean sendHeartBeat(String stationCode);
+
+    /**
+     * 发送质量查询(AQDW)
+     * @param sn 工件码
+     * @param user 用户
+     * @param stationCode 工位号
+     * @return 是否发送成功
+     */
+    boolean sendCheckQuality(String sn, String user, String stationCode);
+
+    /**
+     * 发送开始工作(MKSW)
+     * @param sn 工件码
+     * @param user 用户
+     * @param stationCode 工位号
+     * @return 是否发送成功
+     */
+    boolean sendStartWork(String sn, String user, String stationCode);
+
+    /**
+     * 发送质量结果(MQDW)
+     * @param sn 工件码
+     * @param result 结果 OK/NG
+     * @param user 用户
+     * @param stationCode 工位号
+     * @return 是否发送成功
+     */
+    boolean sendQuality(String sn, String result, String user, String stationCode);
+
+    /**
+     * 发送质量结果(MQDW)- 支持自定义工艺号
+     * @param sn 工件码
+     * @param result 结果 OK/NG
+     * @param user 用户
+     * @param craft 工艺号(如500001、500002、100000)
+     * @param stationCode 工位号
+     * @return 是否发送成功
+     */
+    boolean sendQuality(String sn, String result, String user, String craft, String stationCode);
+
+    /**
+     * 发送物料绑定(MBDW)
+     * @param sn 工件码
+     * @param materialSn 物料码
+     * @param user 用户
+     * @param craft 工艺号
+     * @param stationCode 工位号
+     * @return 是否发送成功
+     */
+    boolean sendBindMaterial(String sn, String materialSn, String user, String craft, String stationCode);
+
+    /**
+     * 发送物料解绑(MJBW)
+     * @param sn 工件码
+     * @param materialSn 物料码
+     * @param user 用户
+     * @param craft 工艺号
+     * @param stationCode 工位号
+     * @return 是否发送成功
+     */
+    boolean sendUnbindMaterial(String sn, String materialSn, String user, String craft, String stationCode);
+
+    /**
+     * 发送物料校验(VLDW)
+     * @param sn 工件码
+     * @param materialSn 物料码
+     * @param user 用户
+     * @param craft 工艺号
+     * @param stationCode 工位号
+     * @return 是否发送成功
+     */
+    boolean sendValidateMaterial(String sn, String materialSn, String user, String craft, String stationCode);
+
+    /**
+     * 发送创建工件(MCJW)
+     * @param sn 工件码
+     * @param result 结果
+     * @param user 用户
+     * @param stationCode 工位号
+     * @return 是否发送成功
+     */
+    boolean sendCreate(String sn, String result, String user, String stationCode);
+
+    /**
+     * 发送通用消息
+     * @param msgType 消息类型
+     * @param craft 工艺号
+     * @param lx 类型
+     * @param sn 工件码
+     * @param result 结果
+     * @param user 用户
+     * @param paramNums 参数数量
+     * @param params 参数
+     * @param stationCode 工位号
+     * @return 是否发送成功
+     */
+    boolean sendMessage(String msgType, String craft, String lx, String sn, 
+                       String result, String user, String paramNums, String params, 
+                       String stationCode);
+}

+ 296 - 0
src/com/mes/ui/LoginFrame.java

@@ -0,0 +1,296 @@
+package com.mes.ui;
+
+import com.alibaba.fastjson2.JSONObject;
+import com.mes.core.StationConfig;
+import com.mes.util.Base64Utils;
+import com.mes.util.HttpUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import java.time.LocalDateTime;
+import java.util.Objects;
+
+/**
+ * 登录窗口 - 与OP150版本UI保持一致
+ * 支持账号密码登录和扫码登录
+ */
+public class LoginFrame extends JFrame {
+    private static final Logger log = LoggerFactory.getLogger(LoginFrame.class);
+
+    // 主窗口引用
+    private MainFrame mainFrame;
+    
+    // 配置
+    private StationConfig config;
+    
+    // 会话信息
+    private String sessionId;
+    private int userAuth;
+    private int loginHour;
+
+    // UI组件
+    private JTextField userNameTxt;
+    private JPasswordField userPasswordTxt;
+    private JButton loginButton;
+    private JButton scanLoginButton;
+
+    public LoginFrame(MainFrame mainFrame) {
+        this.mainFrame = mainFrame;
+        this.config = StationConfig.getInstance();
+        initUI();
+    }
+
+    /**
+     * 初始化UI - 参照OP150版本
+     */
+    private void initUI() {
+        // 设置标题
+        String title = "MES系统客户端:" + getStationTitle();
+        setTitle(title);
+
+        // 设置背景图片
+        ImageIcon bg = new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/background.png")));
+        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+        
+        JLabel imgLabel = new JLabel(bg);
+        getLayeredPane().add(imgLabel, Integer.valueOf(Integer.MIN_VALUE));
+        imgLabel.setBounds(0, 0, bg.getIconWidth(), bg.getIconHeight());
+        
+        Container contentPane = getContentPane();
+        contentPane.setLayout(null);
+        
+        // 欢迎面板
+        JPanel welcomePanel = new JPanel();
+        welcomePanel.setLayout(null);
+        welcomePanel.setBounds(30, 330, 890, 300);
+        welcomePanel.setOpaque(false);
+        contentPane.add(welcomePanel);
+
+        // 用户名标签
+        JLabel userNameLabel = new JLabel("<html><body>用户名:</body></html>", JLabel.LEFT);
+        userNameLabel.setBounds(300, 100, 120, 40);
+        userNameLabel.setFont(new Font("Dialog", Font.BOLD, 16));
+        welcomePanel.add(userNameLabel);
+        
+        // 密码标签
+        JLabel userPasswordLabel = new JLabel("<html><body>密码:</body></html>", JLabel.LEFT);
+        userPasswordLabel.setBounds(300, 150, 120, 40);
+        userPasswordLabel.setFont(new Font("Dialog", Font.BOLD, 16));
+        welcomePanel.add(userPasswordLabel);
+        
+        // 用户名输入框
+        userNameTxt = new JTextField(20);
+        userNameTxt.setText("system");  // 默认用户名
+        userNameTxt.setBounds(400, 105, 150, 30);
+        welcomePanel.add(userNameTxt);
+        
+        // 密码输入框
+        userPasswordTxt = new JPasswordField(20);
+        userPasswordTxt.setBounds(400, 155, 150, 30);
+        userPasswordTxt.setText("Aa111111");  // 默认密码
+        welcomePanel.add(userPasswordTxt);
+
+        // 账号密码登录按钮
+        loginButton = new JButton("用户密码登录");
+        loginButton.setFont(new Font("Dialog", Font.BOLD, 16));
+        loginButton.setBounds(300, 200, 255, 40);
+        loginButton.setIcon(new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/user.png"))));
+        loginButton.addActionListener(e -> doLogin());
+        welcomePanel.add(loginButton);
+
+        // 扫码登录按钮
+        scanLoginButton = new JButton("扫  码  登  录");
+        scanLoginButton.setFont(new Font("Dialog", Font.BOLD, 16));
+        scanLoginButton.setBounds(300, 250, 255, 40);
+        scanLoginButton.setIcon(new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/scan_barcode.png"))));
+        scanLoginButton.addActionListener(e -> doScanLogin());
+        welcomePanel.add(scanLoginButton);
+
+        ((JPanel) contentPane).setOpaque(false);
+        setResizable(false);
+        setIconImage(Toolkit.getDefaultToolkit().getImage(getClass().getResource("/resources/image/bg/logo.png")));
+
+        // 居中显示
+        Dimension d = Toolkit.getDefaultToolkit().getScreenSize();
+        int width = 902;
+        int height = 678;
+        setBounds((d.width - width) / 2, (d.height - height) / 2 - (d.height - height) / 4, width, height);
+
+        // 窗口关闭事件
+        addWindowListener(new WindowAdapter() {
+            @Override
+            public void windowClosing(WindowEvent e) {
+                System.exit(0);
+            }
+        });
+    }
+
+    /**
+     * 获取工位标题
+     */
+    private String getStationTitle() {
+        if (config.getStationCount() > 0) {
+            StringBuilder sb = new StringBuilder();
+            sb.append(config.getStation(0).getCode());
+            if (config.getStationCount() > 1) {
+                sb.append("/").append(config.getStation(1).getCode());
+            }
+            // 添加工位描述(如果有)
+            String stationName = config.getStationName();
+            if (stationName != null && !stationName.isEmpty()) {
+                sb.append(" - ").append(stationName);
+            }
+            return sb.toString();
+        }
+        return "通用工位";
+    }
+
+    /**
+     * 账号密码登录
+     */
+    private void doLogin() {
+        String userName = userNameTxt.getText().trim();
+        String password = new String(userPasswordTxt.getPassword());
+
+        if (userName.isEmpty() || password.isEmpty()) {
+            JOptionPane.showMessageDialog(this, "用户名或密码不能为空", "提示窗口", JOptionPane.INFORMATION_MESSAGE);
+            return;
+        }
+
+        // Base64编码
+        String encodedUser = Base64Utils.encode(userName);
+        String encodedPass = Base64Utils.encode(password);
+
+        log.info("登录 --> 用户名: {}", userName);
+
+        // 构建登录URL
+        String url = String.format(
+                "http://%s:%d/js/a/login?__ajax=json&username=%s&password=%s&validCode=&__sid=",
+                config.getServerIp(),
+                config.getHttpPort(),
+                encodedUser,
+                encodedPass
+        );
+
+        String loginResult = HttpUtils.sendRequest(url);
+        
+        if ("false".equalsIgnoreCase(loginResult)) {
+            JOptionPane.showMessageDialog(this, "登录异常,请检查网络或联系网络管理员", "提示窗口", JOptionPane.INFORMATION_MESSAGE);
+            return;
+        }
+
+        JSONObject retObj = JSONObject.parseObject(loginResult);
+        if (retObj != null && retObj.get("result") != null && "true".equalsIgnoreCase(retObj.get("result").toString())) {
+            checkUserAuthority(retObj);
+        } else {
+            JOptionPane.showMessageDialog(this, "登录失败,用户名或密码错误", "提示窗口", JOptionPane.INFORMATION_MESSAGE);
+        }
+    }
+
+    /**
+     * 扫码登录
+     */
+    private void doScanLogin() {
+        String scanContent = JOptionPane.showInputDialog(null, "请扫码工牌二维码");
+        log.info("scanContent={}", scanContent);
+        
+        if (scanContent != null && !scanContent.isEmpty()) {
+            String url = String.format(
+                    "http://%s:%d/js/a/mes/mesLogin/login?__login=true&__ajax=json&username=%s",
+                    config.getServerIp(),
+                    config.getHttpPort(),
+                    scanContent
+            );
+            
+            String loginResult = HttpUtils.sendRequest(url);
+            log.info("loginResult={}", loginResult);
+            
+            if ("false".equalsIgnoreCase(loginResult)) {
+                JOptionPane.showMessageDialog(this, "登录异常,请检查网络或联系网络管理员", "提示窗口", JOptionPane.INFORMATION_MESSAGE);
+                return;
+            }
+
+            JSONObject retObj = JSONObject.parseObject(loginResult);
+            if (retObj != null && retObj.get("result") != null && "true".equalsIgnoreCase(retObj.get("result").toString())) {
+                checkUserAuthority(retObj);
+            } else {
+                JOptionPane.showMessageDialog(this, "登录失败,用户名或密码错误", "提示窗口", JOptionPane.INFORMATION_MESSAGE);
+            }
+        } else {
+            JOptionPane.showMessageDialog(this, "扫码内容错误", "提示窗口", JOptionPane.INFORMATION_MESSAGE);
+        }
+    }
+
+    /**
+     * 检查用户权限
+     */
+    private void checkUserAuthority(JSONObject retObj) {
+        // 获取用户信息
+        JSONObject userObj = JSONObject.parseObject(retObj.get("user").toString());
+        String userId = userObj.getString("id");
+
+        // 获取session id
+        sessionId = retObj.get("sessionid").toString();
+        
+        if (sessionId != null && !sessionId.isEmpty()) {
+            // 获取第一个工位号用于权限检查
+            String stationCode = config.getStationCount() > 0 ? config.getStation(0).getCode() : "";
+            
+            // 请求权限
+            String authUrl = String.format(
+                    "http://%s:%d/js/a/mes/mesLineProcess/userAuth?__ajax=json&type=0&__sid=%s&oprno=%s&lineSn=%s",
+                    config.getServerIp(),
+                    config.getHttpPort(),
+                    sessionId,
+                    stationCode,
+                    config.getLineSn()
+            );
+            
+            String authorityResult = HttpUtils.sendRequest(authUrl);
+            JSONObject authorityObj = JSONObject.parseObject(authorityResult);
+            
+            if (authorityObj != null && authorityObj.get("result") != null && 
+                "true".equalsIgnoreCase(authorityObj.get("result").toString())) {
+                
+                JSONObject authObjTmp = JSONObject.parseObject(authorityObj.get("data").toString());
+                userAuth = Integer.parseInt(authObjTmp.getString("auth"));
+                
+                if (userAuth == 0) {
+                    JOptionPane.showMessageDialog(this, "您无权登录该工位", "提示窗口", JOptionPane.INFORMATION_MESSAGE);
+                } else if (userAuth == 1 || userAuth == 2) {
+                    // 记录登录时间
+                    loginHour = LocalDateTime.now().getHour();
+                    
+                    // 登录成功,显示主窗口
+                    onLoginSuccess(userId);
+                }
+            } else {
+                JOptionPane.showMessageDialog(this, "权限验证失败", "提示窗口", JOptionPane.INFORMATION_MESSAGE);
+            }
+        } else {
+            JOptionPane.showMessageDialog(this, "登录失败,用户名或密码错误", "提示窗口", JOptionPane.INFORMATION_MESSAGE);
+        }
+    }
+
+    /**
+     * 登录成功处理
+     */
+    private void onLoginSuccess(String userId) {
+        log.info("用户 {} 登录成功, auth={}", userId, userAuth);
+        
+        // 设置会话信息到主窗口
+        mainFrame.setCurrentUser(userId);
+        mainFrame.setSessionId(sessionId);
+        mainFrame.setUserAuth(userAuth);
+        mainFrame.setLoginHour(loginHour);
+
+        // 隐藏登录窗口,显示主窗口
+        this.setVisible(false);
+        mainFrame.setVisible(true);
+        mainFrame.start();
+    }
+}

File diff suppressed because it is too large
+ 1051 - 0
src/com/mes/ui/MainFrame.java


+ 155 - 0
src/com/mes/ui/UIFactory.java

@@ -0,0 +1,155 @@
+package com.mes.ui;
+
+import com.mes.core.StationConfig;
+import com.mes.ui.component.WorkstationPanel;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.swing.*;
+import java.awt.*;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * UI工厂 - 根据配置创建工位UI
+ * 支持单工位/双工位布局自动切换
+ */
+public class UIFactory {
+    private static final Logger log = LoggerFactory.getLogger(UIFactory.class);
+
+    /**
+     * 创建主面板
+     * @param config 工位配置
+     * @return 主面板和工位面板列表
+     */
+    public static UIBuildResult createMainPanel(StationConfig config) {
+        List<WorkstationPanel> panels = new ArrayList<>();
+        JPanel mainPanel;
+
+        if (config.isSingleMode()) {
+            mainPanel = createSingleStationLayout(config, panels);
+        } else {
+            mainPanel = createDualStationLayout(config, panels);
+        }
+
+        return new UIBuildResult(mainPanel, panels);
+    }
+
+    /**
+     * 创建单工位布局
+     */
+    private static JPanel createSingleStationLayout(StationConfig config, List<WorkstationPanel> panels) {
+        JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
+        mainPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
+
+        // 获取工位信息
+        StationConfig.StationInfo stationInfo = config.getStation(0);
+        if (stationInfo != null) {
+            WorkstationPanel panel = WorkstationPanel.fromConfig(stationInfo, config);
+            panels.add(panel);
+            mainPanel.add(panel, BorderLayout.CENTER);
+        }
+
+        log.info("创建单工位布局");
+        return mainPanel;
+    }
+
+    /**
+     * 创建双工位布局
+     */
+    private static JPanel createDualStationLayout(StationConfig config, List<WorkstationPanel> panels) {
+        JPanel mainPanel = new JPanel(new GridLayout(1, 2, 20, 0));
+        mainPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
+
+        // 工位1
+        StationConfig.StationInfo station1 = config.getStation(0);
+        if (station1 != null) {
+            WorkstationPanel panel1 = WorkstationPanel.fromConfig(station1, config);
+            panels.add(panel1);
+            mainPanel.add(panel1);
+        }
+
+        // 工位2
+        StationConfig.StationInfo station2 = config.getStation(1);
+        if (station2 != null) {
+            WorkstationPanel panel2 = WorkstationPanel.fromConfig(station2, config);
+            panels.add(panel2);
+            mainPanel.add(panel2);
+        }
+
+        log.info("创建双工位布局");
+        return mainPanel;
+    }
+
+    /**
+     * 创建顶部工具栏
+     */
+    public static JPanel createToolbar(StationConfig config) {
+        JPanel toolbar = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 5));
+        toolbar.setBorder(BorderFactory.createEtchedBorder());
+
+        // 用户信息
+        JLabel userLabel = new JLabel("用户: ");
+        toolbar.add(userLabel);
+
+        // 心跳指示器
+        JButton heartbeatIndicator = new JButton("●");
+        heartbeatIndicator.setForeground(Color.GRAY);
+        heartbeatIndicator.setBorderPainted(false);
+        heartbeatIndicator.setContentAreaFilled(false);
+        heartbeatIndicator.setFocusable(false);
+        heartbeatIndicator.setToolTipText("TCP连接状态");
+        toolbar.add(heartbeatIndicator);
+
+        // 工位信息
+        JLabel stationLabel = new JLabel("工位: " + getStationCodesString(config));
+        toolbar.add(stationLabel);
+
+        return toolbar;
+    }
+
+    /**
+     * 获取工位号字符串
+     */
+    private static String getStationCodesString(StationConfig config) {
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < config.getStationCount(); i++) {
+            if (i > 0) {
+                sb.append(" / ");
+            }
+            StationConfig.StationInfo station = config.getStation(i);
+            if (station != null) {
+                sb.append(station.getCode());
+            }
+        }
+        return sb.toString();
+    }
+
+    /**
+     * UI构建结果
+     */
+    public static class UIBuildResult {
+        private final JPanel mainPanel;
+        private final List<WorkstationPanel> workstationPanels;
+
+        public UIBuildResult(JPanel mainPanel, List<WorkstationPanel> workstationPanels) {
+            this.mainPanel = mainPanel;
+            this.workstationPanels = workstationPanels;
+        }
+
+        public JPanel getMainPanel() {
+            return mainPanel;
+        }
+
+        public List<WorkstationPanel> getWorkstationPanels() {
+            return workstationPanels;
+        }
+
+        public WorkstationPanel getPanel(int index) {
+            if (index >= 0 && index < workstationPanels.size()) {
+                return workstationPanels.get(index);
+            }
+            return null;
+        }
+    }
+}

+ 118 - 0
src/com/mes/ui/component/MesWebView.java

@@ -0,0 +1,118 @@
+package com.mes.ui.component;
+
+import javafx.application.Platform;
+import javafx.beans.value.ObservableValue;
+import javafx.concurrent.Worker;
+import javafx.embed.swing.JFXPanel;
+import javafx.scene.Scene;
+import javafx.scene.web.WebEngine;
+import javafx.scene.web.WebView;
+import netscape.javascript.JSObject;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.swing.*;
+import java.awt.*;
+
+/**
+ * MES WebView 组件
+ * 使用 JavaFX WebView 嵌入到 Swing 中显示网页内容
+ * 参考 op150 实现
+ */
+public class MesWebView extends JPanel {
+
+    private static final Logger log = LoggerFactory.getLogger(MesWebView.class);
+
+    private JFXPanel jfxPanel;
+    private WebEngine engine;
+    private String currentUrl;
+
+    /**
+     * 构造函数
+     * @param url 要加载的网页URL
+     */
+    public MesWebView(String url) {
+        this.currentUrl = url;
+        setLayout(new BorderLayout());
+        
+        // 创建 JFXPanel 并添加到面板
+        jfxPanel = new JFXPanel();
+        add(jfxPanel, BorderLayout.CENTER);
+        
+        // 在 JavaFX 线程中初始化 WebView
+        Platform.runLater(() -> {
+            try {
+                WebView webView = new WebView();
+                jfxPanel.setScene(new Scene(webView));
+                engine = webView.getEngine();
+                
+                // 设置 JS 桥接
+                engine.getLoadWorker().stateProperty().addListener(
+                        (ObservableValue<? extends Worker.State> ov, Worker.State oldState, Worker.State newState) -> {
+                            if (newState == Worker.State.SUCCEEDED) {
+                                try {
+                                    JSObject window = (JSObject) engine.executeScript("window");
+                                    window.setMember("sout", System.out);
+                                } catch (Exception e) {
+                                    log.warn("设置JS桥接失败: {}", e.getMessage());
+                                }
+                            }
+                        }
+                );
+                
+                log.info("加载WebView: {}", url);
+                engine.load(url);
+            } catch (Exception e) {
+                log.error("初始化WebView失败: {}", e.getMessage());
+            }
+        });
+    }
+
+    /**
+     * 重新加载当前页面
+     */
+    public void reload() {
+        if (engine != null) {
+            Platform.runLater(() -> {
+                try {
+                    engine.reload();
+                    log.info("WebView重新加载");
+                } catch (Exception e) {
+                    log.error("WebView重新加载失败: {}", e.getMessage());
+                }
+            });
+        }
+    }
+
+    /**
+     * 加载新的URL
+     * @param url 新的URL
+     */
+    public void loadUrl(String url) {
+        this.currentUrl = url;
+        if (engine != null) {
+            Platform.runLater(() -> {
+                try {
+                    engine.load(url);
+                    log.info("WebView加载新URL: {}", url);
+                } catch (Exception e) {
+                    log.error("WebView加载URL失败: {}", e.getMessage());
+                }
+            });
+        }
+    }
+
+    /**
+     * 获取当前URL
+     */
+    public String getCurrentUrl() {
+        return currentUrl;
+    }
+
+    /**
+     * 获取WebEngine
+     */
+    public WebEngine getEngine() {
+        return engine;
+    }
+}

+ 969 - 0
src/com/mes/ui/component/WorkflowMonitorPanel.java

@@ -0,0 +1,969 @@
+package com.mes.ui.component;
+
+import com.mes.core.*;
+import com.mes.step.IWorkflowStep;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.swing.*;
+import javax.swing.border.EmptyBorder;
+import javax.swing.border.TitledBorder;
+import java.awt.*;
+import java.util.List;
+import java.util.ArrayList;
+import java.util.Map;
+
+/**
+ * 流程监控面板 - 显示工作流步骤状态,支持条件检测和跳过操作
+ * 采用垂直列表方案,每个步骤显示:序号、名称、状态、条件、跳过按钮
+ */
+public class WorkflowMonitorPanel extends JPanel {
+    private static final Logger log = LoggerFactory.getLogger(WorkflowMonitorPanel.class);
+
+    // 颜色定义
+    private static final Color COLOR_COMPLETED = new Color(46, 204, 113);   // 绿色 - 已完成
+    private static final Color COLOR_CURRENT = new Color(52, 152, 219);     // 蓝色 - 当前步骤
+    private static final Color COLOR_PENDING = new Color(149, 165, 166);    // 灰色 - 待执行
+    private static final Color COLOR_ERROR = new Color(231, 76, 60);        // 红色 - 错误
+    private static final Color COLOR_CONDITION_OK = new Color(46, 204, 113);  // 条件满足
+    private static final Color COLOR_CONDITION_FAIL = new Color(231, 76, 60); // 条件不满足
+
+    // 工位选择(多工位模式)
+    private List<WorkflowEngine> engines;
+    private List<StationContext> contexts;
+    private int selectedStationIndex = 0;
+
+    // UI组件
+    private JComboBox<String> stationSelector;
+    private JPanel stepsContainer;
+    private List<StepPanel> stepPanels;
+    private JLabel statusLabel;
+    private Timer refreshTimer;
+    
+    // ========== 调试控制相关 ==========
+    private DebugConfigManager debugConfigManager;
+    private JPanel debugControlPanel;
+    private JCheckBox debugEnabledCheckbox;
+    private JPanel conditionsPanel;
+    private JPanel rulesPanel;
+    private List<ConditionInterceptor.ConditionMeta> conditionMetas;
+
+    public WorkflowMonitorPanel() {
+        this.engines = new ArrayList<>();
+        this.contexts = new ArrayList<>();
+        this.stepPanels = new ArrayList<>();
+        this.debugConfigManager = new DebugConfigManager();
+        this.conditionMetas = ConditionInterceptor.scanConditions(StationContext.class);
+        initUI();
+    }
+
+    /**
+     * 设置工位列表
+     */
+    public void setWorkstations(List<WorkflowEngine> engines, List<StationContext> contexts) {
+        this.engines = engines != null ? engines : new ArrayList<>();
+        this.contexts = contexts != null ? contexts : new ArrayList<>();
+        
+        // 为每个工位初始化拦截器
+        for (StationContext ctx : this.contexts) {
+            if (ctx.getConditionInterceptor() == null) {
+                ConditionInterceptor interceptor = new ConditionInterceptor();
+                ctx.setConditionInterceptor(interceptor);
+                // 应用已保存的配置
+                debugConfigManager.applyToInterceptor(ctx.getStationCode(), interceptor);
+            }
+        }
+        
+        // 更新工位选择器
+        stationSelector.removeAllItems();
+        for (int i = 0; i < this.contexts.size(); i++) {
+            StationContext ctx = this.contexts.get(i);
+            stationSelector.addItem(ctx.getStationCode() + " - " + ctx.getStationName());
+        }
+        
+        if (!this.engines.isEmpty()) {
+            stationSelector.setSelectedIndex(0);
+            selectedStationIndex = 0;
+            refreshSteps();
+            refreshDebugPanel();
+        }
+    }
+
+    /**
+     * 初始化UI
+     */
+    private void initUI() {
+        setLayout(new BorderLayout(10, 10));
+        setBorder(new EmptyBorder(10, 10, 10, 10));
+
+        // 顶部:工位选择器和状态
+        JPanel topPanel = createTopPanel();
+        add(topPanel, BorderLayout.NORTH);
+
+        // 中间:使用分割面板,左边步骤列表,右边调试控制
+        JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT);
+        splitPane.setResizeWeight(0.6);
+        
+        // 左侧:步骤列表(滚动)
+        stepsContainer = new JPanel();
+        stepsContainer.setLayout(new BoxLayout(stepsContainer, BoxLayout.Y_AXIS));
+        
+        JScrollPane stepsScrollPane = new JScrollPane(stepsContainer);
+        stepsScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
+        stepsScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
+        stepsScrollPane.getVerticalScrollBar().setUnitIncrement(16);
+        stepsScrollPane.getVerticalScrollBar().setBlockIncrement(80);
+        stepsScrollPane.setBorder(BorderFactory.createTitledBorder(
+                BorderFactory.createLineBorder(Color.GRAY), 
+                "流程步骤", 
+                TitledBorder.LEFT, 
+                TitledBorder.TOP,
+                new Font("微软雅黑", Font.BOLD, 14)));
+        splitPane.setLeftComponent(stepsScrollPane);
+        
+        // 右侧:调试控制面板
+        debugControlPanel = createDebugControlPanel();
+        JScrollPane debugScrollPane = new JScrollPane(debugControlPanel);
+        debugScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
+        debugScrollPane.getVerticalScrollBar().setUnitIncrement(16);
+        debugScrollPane.getVerticalScrollBar().setBlockIncrement(80);
+        debugScrollPane.setBorder(BorderFactory.createTitledBorder(
+                BorderFactory.createLineBorder(new Color(255, 152, 0)), 
+                "调试控制", 
+                TitledBorder.LEFT, 
+                TitledBorder.TOP,
+                new Font("微软雅黑", Font.BOLD, 14),
+                new Color(255, 152, 0)));
+        splitPane.setRightComponent(debugScrollPane);
+        
+        add(splitPane, BorderLayout.CENTER);
+
+        // 底部:刷新按钮和状态
+        JPanel bottomPanel = createBottomPanel();
+        add(bottomPanel, BorderLayout.SOUTH);
+
+        // 自动刷新定时器(每秒刷新一次)
+        refreshTimer = new Timer(1000, e -> refresh());
+        refreshTimer.start();
+    }
+
+    /**
+     * 创建顶部面板
+     */
+    private JPanel createTopPanel() {
+        JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 5));
+        
+        JLabel label = new JLabel("工位:");
+        label.setFont(new Font("微软雅黑", Font.PLAIN, 14));
+        panel.add(label);
+
+        stationSelector = new JComboBox<>();
+        stationSelector.setFont(new Font("微软雅黑", Font.PLAIN, 14));
+        stationSelector.setPreferredSize(new Dimension(200, 30));
+        stationSelector.addActionListener(e -> {
+            int index = stationSelector.getSelectedIndex();
+            if (index >= 0 && index != selectedStationIndex) {
+                selectedStationIndex = index;
+                refreshSteps();
+                refreshDebugPanel();
+            }
+        });
+        panel.add(stationSelector);
+
+        // 手动刷新按钮
+        JButton refreshBtn = new JButton("刷新");
+        refreshBtn.setFont(new Font("微软雅黑", Font.PLAIN, 14));
+        refreshBtn.addActionListener(e -> refresh());
+        panel.add(refreshBtn);
+
+        return panel;
+    }
+
+    /**
+     * 创建底部面板
+     */
+    private JPanel createBottomPanel() {
+        JPanel panel = new JPanel(new BorderLayout(10, 5));
+        panel.setBorder(new EmptyBorder(5, 0, 0, 0));
+
+        // 状态标签
+        statusLabel = new JLabel("就绪");
+        statusLabel.setFont(new Font("微软雅黑", Font.PLAIN, 12));
+        panel.add(statusLabel, BorderLayout.WEST);
+
+        // 说明
+        JLabel helpLabel = new JLabel("提示:点击\"跳过\"将模拟该步骤执行成功");
+        helpLabel.setFont(new Font("微软雅黑", Font.ITALIC, 11));
+        helpLabel.setForeground(Color.GRAY);
+        panel.add(helpLabel, BorderLayout.EAST);
+
+        return panel;
+    }
+    
+    // ========== 调试控制面板相关方法 ==========
+    
+    /**
+     * 创建调试控制面板
+     */
+    private JPanel createDebugControlPanel() {
+        JPanel panel = new JPanel();
+        panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
+        panel.setBorder(new EmptyBorder(5, 5, 5, 5));
+        
+        // 启用调试复选框
+        JPanel enablePanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
+        debugEnabledCheckbox = new JCheckBox("启用调试拦截");
+        debugEnabledCheckbox.setFont(new Font("微软雅黑", Font.BOLD, 12));
+        debugEnabledCheckbox.setForeground(new Color(255, 87, 34));
+        debugEnabledCheckbox.addActionListener(e -> onDebugEnabledChanged());
+        enablePanel.add(debugEnabledCheckbox);
+        enablePanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, 35));
+        panel.add(enablePanel);
+        
+        // 条件变量区域
+        conditionsPanel = new JPanel();
+        conditionsPanel.setLayout(new BoxLayout(conditionsPanel, BoxLayout.Y_AXIS));
+        conditionsPanel.setBorder(BorderFactory.createTitledBorder(
+                BorderFactory.createLineBorder(Color.LIGHT_GRAY),
+                "条件变量",
+                TitledBorder.LEFT, TitledBorder.TOP,
+                new Font("微软雅黑", Font.PLAIN, 12)));
+        panel.add(conditionsPanel);
+        
+        panel.add(Box.createRigidArea(new Dimension(0, 10)));
+        
+        // 持续控制规则区域
+        rulesPanel = new JPanel();
+        rulesPanel.setLayout(new BoxLayout(rulesPanel, BoxLayout.Y_AXIS));
+        rulesPanel.setBorder(BorderFactory.createTitledBorder(
+                BorderFactory.createLineBorder(new Color(76, 175, 80)),
+                "持续控制规则",
+                TitledBorder.LEFT, TitledBorder.TOP,
+                new Font("微软雅黑", Font.PLAIN, 12),
+                new Color(76, 175, 80)));
+        panel.add(rulesPanel);
+        
+        return panel;
+    }
+    
+    /**
+     * 刷新调试面板
+     */
+    private void refreshDebugPanel() {
+        if (selectedStationIndex < 0 || selectedStationIndex >= contexts.size()) {
+            return;
+        }
+        
+        StationContext context = contexts.get(selectedStationIndex);
+        String stationCode = context.getStationCode();
+        
+        // 更新启用状态
+        DebugConfigManager.StationDebugConfig config = debugConfigManager.getStationConfig(stationCode);
+        debugEnabledCheckbox.setSelected(config.isEnabled());
+        
+        // 切换工位时需要重建条件面板
+        rebuildConditionsPanel(context);
+        
+        // 刷新规则显示
+        refreshRulesPanel(context);
+    }
+    
+    // 存储值标签的映射,用于刷新时只更新值而不重建
+    private java.util.Map<String, JLabel> valueLabels = new java.util.HashMap<>();
+    private boolean conditionsPanelInitialized = false;
+    
+    /**
+     * 刷新条件变量面板
+     */
+    private void refreshConditionsPanel(StationContext context) {
+        // 如果面板未初始化,则完整构建
+        if (!conditionsPanelInitialized || conditionsPanel.getComponentCount() == 0) {
+            buildConditionsPanel(context);
+            conditionsPanelInitialized = true;
+            return;
+        }
+        
+        // 只更新值标签,不重建面板
+        java.util.Set<String> stepIds = getCurrentStepIds();
+        for (ConditionInterceptor.ConditionMeta meta : conditionMetas) {
+            if (!isConditionRelevant(meta, stepIds)) {
+                continue;
+            }
+            JLabel valueLabel = valueLabels.get(meta.getFieldName());
+            if (valueLabel != null) {
+                Object value = ConditionInterceptor.getFieldValue(context, meta.getFieldName());
+                valueLabel.setText(String.valueOf(value));
+                if (meta.isBoolean()) {
+                    valueLabel.setForeground(Boolean.TRUE.equals(value) ? 
+                            new Color(76, 175, 80) : new Color(244, 67, 54));
+                }
+            }
+        }
+    }
+    
+    /**
+     * 获取当前工位流程中实际配置的步骤ID集合
+     */
+    private java.util.Set<String> getCurrentStepIds() {
+        java.util.Set<String> stepIds = new java.util.HashSet<>();
+        if (selectedStationIndex >= 0 && selectedStationIndex < engines.size()) {
+            for (IWorkflowStep step : engines.get(selectedStationIndex).getSteps()) {
+                stepIds.add(step.getStepId());
+            }
+        }
+        return stepIds;
+    }
+    
+    /**
+     * 判断条件变量是否与当前流程相关
+     * affectedSteps为空 → 不关联任何步骤,隐藏
+     * affectedSteps与当前步骤ID有交集 → 相关,显示
+     */
+    private boolean isConditionRelevant(ConditionInterceptor.ConditionMeta meta, java.util.Set<String> stepIds) {
+        String[] affected = meta.getAffectedSteps();
+        if (affected == null || affected.length == 0) {
+            return false;
+        }
+        for (String s : affected) {
+            if (stepIds.contains(s)) {
+                return true;
+            }
+        }
+        return false;
+    }
+    
+    /**
+     * 构建条件变量面板(只在初始化时调用)
+     * 只显示与当前工位流程相关的条件变量
+     */
+    private void buildConditionsPanel(StationContext context) {
+        conditionsPanel.removeAll();
+        valueLabels.clear();
+        
+        java.util.Set<String> stepIds = getCurrentStepIds();
+        
+        String currentGroup = "";
+        for (ConditionInterceptor.ConditionMeta meta : conditionMetas) {
+            // 过滤掉与当前流程无关的条件变量
+            if (!isConditionRelevant(meta, stepIds)) {
+                continue;
+            }
+            
+            // 分组标题
+            if (!meta.getGroup().equals(currentGroup)) {
+                currentGroup = meta.getGroup();
+                JLabel groupLabel = new JLabel("【" + currentGroup + "】");
+                groupLabel.setFont(new Font("微软雅黑", Font.BOLD, 11));
+                groupLabel.setForeground(new Color(33, 150, 243));
+                groupLabel.setBorder(new EmptyBorder(5, 0, 2, 0));
+                conditionsPanel.add(groupLabel);
+            }
+            
+            // 条件行
+            JPanel rowPanel = createConditionRow(context, meta);
+            conditionsPanel.add(rowPanel);
+        }
+        
+        conditionsPanel.revalidate();
+        conditionsPanel.repaint();
+    }
+    
+    /**
+     * 强制重建条件面板(切换工位时调用)
+     */
+    private void rebuildConditionsPanel(StationContext context) {
+        conditionsPanelInitialized = false;
+        refreshConditionsPanel(context);
+    }
+    
+    /**
+     * 检查字段是否有持续规则
+     */
+    private boolean hasRule(StationContext context, String fieldName) {
+        ConditionInterceptor interceptor = context.getConditionInterceptor();
+        return interceptor != null && interceptor.hasRule(fieldName);
+    }
+    
+    /**
+     * 创建锁定/解锁按钮
+     */
+    private JButton createLockButton(StationContext context, ConditionInterceptor.ConditionMeta meta) {
+        boolean locked = hasRule(context, meta.getFieldName());
+        JButton lockBtn = new JButton(locked ? "解锁" : "锁定");
+        lockBtn.setFont(new Font("微软雅黑", Font.BOLD, 11));
+        lockBtn.setPreferredSize(new Dimension(50, 22));
+        lockBtn.setMargin(new java.awt.Insets(1, 2, 1, 2));
+        if (locked) {
+            lockBtn.setForeground(new Color(244, 67, 54));
+            lockBtn.setToolTipText("点击解除持续控制");
+        } else {
+            lockBtn.setToolTipText("添加持续控制规则");
+        }
+        lockBtn.addActionListener(e -> {
+            if (hasRule(context, meta.getFieldName())) {
+                removePersistentRule(context, meta.getFieldName());
+            } else {
+                showAddRuleDialog(context, meta);
+            }
+        });
+        return lockBtn;
+    }
+    
+    /**
+     * 创建单个条件变量行
+     */
+    private JPanel createConditionRow(StationContext context, ConditionInterceptor.ConditionMeta meta) {
+        JPanel row = new JPanel(new FlowLayout(FlowLayout.LEFT, 5, 2));
+        row.setMaximumSize(new Dimension(Integer.MAX_VALUE, 30));
+        
+        // 条件名称
+        JLabel nameLabel = new JLabel(meta.getName() + ":");
+        nameLabel.setFont(new Font("微软雅黑", Font.PLAIN, 11));
+        nameLabel.setPreferredSize(new Dimension(90, 20));
+        row.add(nameLabel);
+        
+        // 当前值(如果有持续规则,显示规则值)
+        Object value = ConditionInterceptor.getFieldValue(context, meta.getFieldName());
+        ConditionInterceptor interceptor = context.getConditionInterceptor();
+        boolean locked = interceptor != null && interceptor.hasRule(meta.getFieldName());
+        if (locked) {
+            value = interceptor.getRuleValue(meta.getFieldName());
+        }
+        
+        JLabel valueLabel = new JLabel(String.valueOf(value));
+        valueLabel.setFont(new Font("Consolas", Font.BOLD, 11));
+        valueLabel.setPreferredSize(new Dimension(50, 20));
+        if (meta.isBoolean()) {
+            valueLabel.setForeground(Boolean.TRUE.equals(value) ? 
+                    new Color(76, 175, 80) : new Color(244, 67, 54));
+        }
+        row.add(valueLabel);
+        
+        // 存储值标签引用,用于后续刷新
+        valueLabels.put(meta.getFieldName(), valueLabel);
+        
+        // 控制按钮
+        if (meta.isControllable()) {
+            if (meta.isBoolean()) {
+                JButton trueBtn = new JButton("真");
+                trueBtn.setFont(new Font("微软雅黑", Font.BOLD, 11));
+                trueBtn.setPreferredSize(new Dimension(45, 22));
+                trueBtn.setMargin(new java.awt.Insets(1, 2, 1, 2));
+                trueBtn.setToolTipText("设置为 true");
+                trueBtn.addActionListener(e -> setConditionValue(context, meta.getFieldName(), true));
+                row.add(trueBtn);
+                
+                JButton falseBtn = new JButton("假");
+                falseBtn.setFont(new Font("微软雅黑", Font.BOLD, 11));
+                falseBtn.setPreferredSize(new Dimension(45, 22));
+                falseBtn.setMargin(new java.awt.Insets(1, 2, 1, 2));
+                falseBtn.setToolTipText("设置为 false");
+                falseBtn.addActionListener(e -> setConditionValue(context, meta.getFieldName(), false));
+                row.add(falseBtn);
+                
+                row.add(createLockButton(context, meta));
+            } else if (meta.isNumeric()) {
+                JTextField inputField = new JTextField(4);
+                inputField.setFont(new Font("Consolas", Font.PLAIN, 11));
+                // 如果有持续规则,输入框显示规则值
+                if (locked) {
+                    inputField.setText(String.valueOf(interceptor.getRuleValue(meta.getFieldName())));
+                } else {
+                    inputField.setText(String.valueOf(ConditionInterceptor.getFieldValue(context, meta.getFieldName())));
+                }
+                row.add(inputField);
+                
+                JButton setBtn = new JButton("设置");
+                setBtn.setFont(new Font("微软雅黑", Font.BOLD, 11));
+                setBtn.setPreferredSize(new Dimension(50, 22));
+                setBtn.setMargin(new java.awt.Insets(1, 2, 1, 2));
+                setBtn.addActionListener(e -> {
+                    try {
+                        int intValue = Integer.parseInt(inputField.getText().trim());
+                        setConditionValue(context, meta.getFieldName(), intValue);
+                    } catch (NumberFormatException ex) {
+                        JOptionPane.showMessageDialog(this, "请输入有效数字", "错误", JOptionPane.ERROR_MESSAGE);
+                    }
+                });
+                row.add(setBtn);
+                
+                row.add(createLockButton(context, meta));
+            }
+        }
+        
+        return row;
+    }
+    
+    /**
+     * 设置条件值
+     */
+    private void setConditionValue(StationContext context, String fieldName, Object value) {
+        ConditionInterceptor.setFieldValue(context, fieldName, value);
+        log.info("[Debug] 设置 {} = {}", fieldName, value);
+        context.updateUI();
+        refreshDebugPanel();
+    }
+    
+    /**
+     * 显示添加规则对话框
+     */
+    private void showAddRuleDialog(StationContext context, ConditionInterceptor.ConditionMeta meta) {
+        Object[] options;
+        String message;
+        
+        if (meta.isBoolean()) {
+            options = new Object[]{"持续 true", "持续 false", "取消"};
+            message = String.format("为 \"%s\" 添加持续控制规则:", meta.getName());
+        } else {
+            String input = JOptionPane.showInputDialog(this, 
+                    String.format("为 \"%s\" 设置持续值:", meta.getName()),
+                    "添加持续控制规则",
+                    JOptionPane.QUESTION_MESSAGE);
+            if (input != null && !input.trim().isEmpty()) {
+                try {
+                    int value = Integer.parseInt(input.trim());
+                    addPersistentRule(context, meta.getFieldName(), value);
+                } catch (NumberFormatException e) {
+                    JOptionPane.showMessageDialog(this, "请输入有效数字", "错误", JOptionPane.ERROR_MESSAGE);
+                }
+            }
+            return;
+        }
+        
+        int result = JOptionPane.showOptionDialog(this, message, "添加持续控制规则",
+                JOptionPane.DEFAULT_OPTION, JOptionPane.QUESTION_MESSAGE, null, options, options[0]);
+        
+        if (result == 0) {
+            addPersistentRule(context, meta.getFieldName(), true);
+        } else if (result == 1) {
+            addPersistentRule(context, meta.getFieldName(), false);
+        }
+    }
+    
+    /**
+     * 添加持续控制规则
+     */
+    private void addPersistentRule(StationContext context, String fieldName, Object value) {
+        String stationCode = context.getStationCode();
+        
+        // 保存到配置
+        debugConfigManager.addRule(stationCode, fieldName, value);
+        
+        // 应用到拦截器
+        ConditionInterceptor interceptor = context.getConditionInterceptor();
+        if (interceptor != null) {
+            interceptor.addRule(fieldName, value);
+        }
+        
+        // 立即设置当前值
+        ConditionInterceptor.setFieldValue(context, fieldName, value);
+        
+        log.info("[Debug] 添加持续规则: {} = {}", fieldName, value);
+        refreshDebugPanel();
+    }
+    
+    /**
+     * 移除持续控制规则
+     */
+    private void removePersistentRule(StationContext context, String fieldName) {
+        String stationCode = context.getStationCode();
+        
+        // 从配置移除
+        debugConfigManager.removeRule(stationCode, fieldName);
+        
+        // 从拦截器移除
+        ConditionInterceptor interceptor = context.getConditionInterceptor();
+        if (interceptor != null) {
+            interceptor.removeRule(fieldName);
+        }
+        
+        log.info("[Debug] 移除持续规则: {}", fieldName);
+        refreshDebugPanel();
+    }
+    
+    /**
+     * 刷新规则面板
+     */
+    private void refreshRulesPanel(StationContext context) {
+        rulesPanel.removeAll();
+        
+        String stationCode = context.getStationCode();
+        DebugConfigManager.StationDebugConfig config = debugConfigManager.getStationConfig(stationCode);
+        Map<String, Object> rules = config.getPersistentRules();
+        
+        if (rules.isEmpty()) {
+            JLabel emptyLabel = new JLabel("  (无持续控制规则)");
+            emptyLabel.setFont(new Font("微软雅黑", Font.ITALIC, 11));
+            emptyLabel.setForeground(Color.GRAY);
+            rulesPanel.add(emptyLabel);
+        } else {
+            for (Map.Entry<String, Object> entry : rules.entrySet()) {
+                JPanel ruleRow = new JPanel(new FlowLayout(FlowLayout.LEFT, 5, 2));
+                ruleRow.setMaximumSize(new Dimension(Integer.MAX_VALUE, 28));
+                
+                // 规则图标
+                JLabel iconLabel = new JLabel("🔒");
+                ruleRow.add(iconLabel);
+                
+                // 规则内容
+                JLabel ruleLabel = new JLabel(entry.getKey() + " = " + entry.getValue());
+                ruleLabel.setFont(new Font("Consolas", Font.PLAIN, 11));
+                ruleLabel.setForeground(new Color(76, 175, 80));
+                ruleRow.add(ruleLabel);
+                
+                // 删除按钮
+                JButton deleteBtn = new JButton("×");
+                deleteBtn.setFont(new Font("微软雅黑", Font.BOLD, 12));
+                deleteBtn.setPreferredSize(new Dimension(30, 20));
+                deleteBtn.setForeground(Color.RED);
+                deleteBtn.setToolTipText("删除此规则");
+                String fieldName = entry.getKey();
+                deleteBtn.addActionListener(e -> removePersistentRule(context, fieldName));
+                ruleRow.add(deleteBtn);
+                
+                rulesPanel.add(ruleRow);
+            }
+        }
+        
+        // 清空所有规则按钮
+        if (!rules.isEmpty()) {
+            JPanel btnPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
+            JButton clearAllBtn = new JButton("清空所有规则");
+            clearAllBtn.setFont(new Font("微软雅黑", Font.PLAIN, 11));
+            clearAllBtn.addActionListener(e -> {
+                int result = JOptionPane.showConfirmDialog(this, 
+                        "确定要清空所有持续控制规则吗?", "确认", JOptionPane.YES_NO_OPTION);
+                if (result == JOptionPane.YES_OPTION) {
+                    ConditionInterceptor interceptor = context.getConditionInterceptor();
+                    if (interceptor != null) {
+                        interceptor.clearRules();
+                    }
+                    DebugConfigManager.StationDebugConfig cfg = debugConfigManager.getStationConfig(stationCode);
+                    cfg.getPersistentRules().clear();
+                    debugConfigManager.save();
+                    refreshDebugPanel();
+                }
+            });
+            btnPanel.add(clearAllBtn);
+            rulesPanel.add(btnPanel);
+        }
+        
+        rulesPanel.revalidate();
+        rulesPanel.repaint();
+    }
+    
+    /**
+     * 调试启用状态变化
+     */
+    private void onDebugEnabledChanged() {
+        if (selectedStationIndex < 0 || selectedStationIndex >= contexts.size()) {
+            return;
+        }
+        
+        StationContext context = contexts.get(selectedStationIndex);
+        String stationCode = context.getStationCode();
+        boolean enabled = debugEnabledCheckbox.isSelected();
+        
+        debugConfigManager.setEnabled(stationCode, enabled);
+        
+        ConditionInterceptor interceptor = context.getConditionInterceptor();
+        if (interceptor != null) {
+            if (enabled) {
+                debugConfigManager.applyToInterceptor(stationCode, interceptor);
+            } else {
+                interceptor.clearRules();
+            }
+        }
+        
+        log.info("[Debug] 工位 {} 调试模式: {}", stationCode, enabled ? "启用" : "禁用");
+        refreshDebugPanel();
+    }
+
+    /**
+     * 刷新步骤列表(重建UI)
+     */
+    private void refreshSteps() {
+        stepsContainer.removeAll();
+        stepPanels.clear();
+
+        if (selectedStationIndex < 0 || selectedStationIndex >= engines.size()) {
+            stepsContainer.revalidate();
+            stepsContainer.repaint();
+            return;
+        }
+
+        WorkflowEngine engine = engines.get(selectedStationIndex);
+        StationContext context = contexts.get(selectedStationIndex);
+        List<IWorkflowStep> steps = engine.getSteps();
+        int currentIndex = engine.getCurrentStepIndex();
+
+        for (int i = 0; i < steps.size(); i++) {
+            IWorkflowStep step = steps.get(i);
+            StepPanel stepPanel = new StepPanel(i, step, context, i == currentIndex, i < currentIndex);
+            stepPanels.add(stepPanel);
+            stepsContainer.add(stepPanel);
+            stepsContainer.add(Box.createRigidArea(new Dimension(0, 5)));
+        }
+
+        stepsContainer.revalidate();
+        stepsContainer.repaint();
+        updateStatus();
+    }
+
+    /**
+     * 刷新(只更新状态,不重建UI)
+     */
+    public void refresh() {
+        if (selectedStationIndex < 0 || selectedStationIndex >= engines.size()) {
+            return;
+        }
+
+        WorkflowEngine engine = engines.get(selectedStationIndex);
+        StationContext context = contexts.get(selectedStationIndex);
+        int currentIndex = engine.getCurrentStepIndex();
+
+        // 检查是否需要重建(步骤数量变化)
+        if (stepPanels.size() != engine.getSteps().size()) {
+            refreshSteps();
+            return;
+        }
+
+        // 更新每个步骤面板
+        for (int i = 0; i < stepPanels.size(); i++) {
+            StepPanel panel = stepPanels.get(i);
+            panel.update(context, i == currentIndex, i < currentIndex);
+        }
+
+        updateStatus();
+        
+        // 刷新调试面板的条件值显示
+        refreshConditionsPanel(context);
+    }
+
+    /**
+     * 更新状态栏
+     */
+    private void updateStatus() {
+        if (selectedStationIndex < 0 || selectedStationIndex >= contexts.size()) {
+            statusLabel.setText("无工位");
+            return;
+        }
+
+        StationContext context = contexts.get(selectedStationIndex);
+        WorkflowEngine engine = engines.get(selectedStationIndex);
+        
+        String state = engine.isRunning() ? (engine.isPaused() ? "已暂停" : "运行中") : "已停止";
+        int current = engine.getCurrentStepIndex() + 1;
+        int total = engine.getSteps().size();
+        
+        statusLabel.setText(String.format("状态: %s | 进度: %d/%d | 工件码: %s", 
+                state, current, total, 
+                context.getProductSn() != null ? context.getProductSn() : "(无)"));
+    }
+
+    /**
+     * 跳过指定步骤
+     */
+    private void skipStep(int stepIndex) {
+        if (selectedStationIndex < 0 || selectedStationIndex >= engines.size()) {
+            return;
+        }
+
+        WorkflowEngine engine = engines.get(selectedStationIndex);
+        int currentIndex = engine.getCurrentStepIndex();
+
+        if (stepIndex != currentIndex) {
+            JOptionPane.showMessageDialog(this, 
+                    "只能跳过当前步骤", 
+                    "提示", 
+                    JOptionPane.WARNING_MESSAGE);
+            return;
+        }
+
+        IWorkflowStep step = engine.getSteps().get(stepIndex);
+        int result = JOptionPane.showConfirmDialog(this,
+                String.format("确定要跳过步骤 \"%s\" 吗?\n\n跳过将模拟该步骤执行成功。", step.getStepName()),
+                "确认跳过",
+                JOptionPane.YES_NO_OPTION,
+                JOptionPane.QUESTION_MESSAGE);
+
+        if (result == JOptionPane.YES_OPTION) {
+            boolean success = engine.skipCurrentStep();
+            if (success) {
+                log.info("跳过步骤成功: {}", step.getStepId());
+                refresh();
+            } else {
+                JOptionPane.showMessageDialog(this,
+                        "跳过步骤失败",
+                        "错误",
+                        JOptionPane.ERROR_MESSAGE);
+            }
+        }
+    }
+
+    /**
+     * 停止刷新定时器
+     */
+    public void stopRefresh() {
+        if (refreshTimer != null) {
+            refreshTimer.stop();
+        }
+    }
+
+    /**
+     * 启动刷新定时器
+     */
+    public void startRefresh() {
+        if (refreshTimer != null) {
+            refreshTimer.start();
+        }
+    }
+
+    // ========== 内部类:步骤面板 ==========
+
+    /**
+     * 单个步骤的显示面板
+     */
+    private class StepPanel extends JPanel {
+        private final int stepIndex;
+        private final IWorkflowStep step;
+        
+        private JLabel indexLabel;
+        private JLabel nameLabel;
+        private JLabel statusLabel;
+        private JPanel conditionsPanel;
+        private JButton skipButton;
+
+        public StepPanel(int index, IWorkflowStep step, StationContext context, 
+                         boolean isCurrent, boolean isCompleted) {
+            this.stepIndex = index;
+            this.step = step;
+            initUI();
+            update(context, isCurrent, isCompleted);
+        }
+
+        private void initUI() {
+            setLayout(new BorderLayout(10, 5));
+            setBorder(BorderFactory.createCompoundBorder(
+                    BorderFactory.createLineBorder(Color.LIGHT_GRAY),
+                    new EmptyBorder(8, 10, 8, 10)));
+            setMaximumSize(new Dimension(Integer.MAX_VALUE, 150));
+
+            // 左侧:序号和名称
+            JPanel leftPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 0));
+            leftPanel.setOpaque(false);
+
+            indexLabel = new JLabel();
+            indexLabel.setFont(new Font("微软雅黑", Font.BOLD, 16));
+            indexLabel.setPreferredSize(new Dimension(30, 30));
+            indexLabel.setHorizontalAlignment(SwingConstants.CENTER);
+            leftPanel.add(indexLabel);
+
+            nameLabel = new JLabel(step.getStepName());
+            nameLabel.setFont(new Font("微软雅黑", Font.BOLD, 14));
+            leftPanel.add(nameLabel);
+
+            statusLabel = new JLabel();
+            statusLabel.setFont(new Font("微软雅黑", Font.PLAIN, 12));
+            leftPanel.add(statusLabel);
+
+            add(leftPanel, BorderLayout.NORTH);
+
+            // 中间:条件列表
+            conditionsPanel = new JPanel();
+            conditionsPanel.setLayout(new BoxLayout(conditionsPanel, BoxLayout.Y_AXIS));
+            conditionsPanel.setOpaque(false);
+            conditionsPanel.setBorder(new EmptyBorder(5, 40, 5, 10));
+            add(conditionsPanel, BorderLayout.CENTER);
+
+            // 右侧:跳过按钮
+            JPanel rightPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 5, 0));
+            rightPanel.setOpaque(false);
+
+            skipButton = new JButton("跳过");
+            skipButton.setFont(new Font("微软雅黑", Font.PLAIN, 12));
+            skipButton.setPreferredSize(new Dimension(70, 28));
+            skipButton.addActionListener(e -> skipStep(stepIndex));
+            rightPanel.add(skipButton);
+
+            add(rightPanel, BorderLayout.EAST);
+        }
+
+        /**
+         * 更新显示状态
+         */
+        public void update(StationContext context, boolean isCurrent, boolean isCompleted) {
+            // 更新背景色
+            if (isCompleted) {
+                setBackground(new Color(232, 245, 233));  // 浅绿
+                indexLabel.setForeground(COLOR_COMPLETED);
+                statusLabel.setText("[已完成]");
+                statusLabel.setForeground(COLOR_COMPLETED);
+            } else if (isCurrent) {
+                setBackground(new Color(227, 242, 253));  // 浅蓝
+                indexLabel.setForeground(COLOR_CURRENT);
+                statusLabel.setText("[当前]");
+                statusLabel.setForeground(COLOR_CURRENT);
+            } else {
+                setBackground(Color.WHITE);
+                indexLabel.setForeground(COLOR_PENDING);
+                statusLabel.setText("[待执行]");
+                statusLabel.setForeground(COLOR_PENDING);
+            }
+
+            // 更新序号
+            indexLabel.setText(String.valueOf(stepIndex + 1));
+
+            // 更新条件
+            updateConditions(context, isCurrent);
+
+            // 只有当前步骤可以跳过
+            skipButton.setEnabled(isCurrent);
+            skipButton.setVisible(isCurrent);
+
+            revalidate();
+            repaint();
+        }
+
+        /**
+         * 更新条件显示
+         */
+        private void updateConditions(StationContext context, boolean isCurrent) {
+            conditionsPanel.removeAll();
+
+            // 只有当前步骤显示详细条件
+            if (isCurrent) {
+                List<IWorkflowStep.StepCondition> conditions = step.getConditions(context);
+                
+                if (conditions.isEmpty()) {
+                    JLabel noCondLabel = new JLabel("  无特殊条件");
+                    noCondLabel.setFont(new Font("微软雅黑", Font.ITALIC, 11));
+                    noCondLabel.setForeground(Color.GRAY);
+                    conditionsPanel.add(noCondLabel);
+                } else {
+                    for (IWorkflowStep.StepCondition cond : conditions) {
+                        JLabel condLabel = new JLabel();
+                        String icon = cond.isSatisfied() ? "✓" : "✗";
+                        condLabel.setText("  " + icon + " " + cond.getDescription());
+                        condLabel.setFont(new Font("微软雅黑", Font.PLAIN, 11));
+                        condLabel.setForeground(cond.isSatisfied() ? COLOR_CONDITION_OK : COLOR_CONDITION_FAIL);
+                        conditionsPanel.add(condLabel);
+                    }
+                }
+            } else {
+                // 非当前步骤:简单显示步骤信息
+                String info = String.format("  ID: %s | 异步: %s | 需交互: %s", 
+                        step.getStepId(),
+                        step.isAsync() ? "是" : "否",
+                        step.requiresUserInteraction() ? "是" : "否");
+                JLabel infoLabel = new JLabel(info);
+                infoLabel.setFont(new Font("微软雅黑", Font.PLAIN, 10));
+                infoLabel.setForeground(Color.GRAY);
+                conditionsPanel.add(infoLabel);
+            }
+
+            conditionsPanel.revalidate();
+            conditionsPanel.repaint();
+        }
+    }
+}

File diff suppressed because it is too large
+ 2519 - 0
src/com/mes/ui/component/WorkstationPanel.java


+ 436 - 0
src/com/mes/ui/layout/ComponentResizer.java

@@ -0,0 +1,436 @@
+package com.mes.ui.layout;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.util.*;
+import javax.swing.JComponent;
+import javax.swing.SwingUtilities;
+
+/**
+ * The ComponentResizer allows you to resize a component by dragging a border
+ * of the component.
+ * 
+ * 来源: https://tips4java.wordpress.com/2009/09/13/resizing-components/
+ */
+public class ComponentResizer extends MouseAdapter {
+    private final static Dimension MINIMUM_SIZE = new Dimension(10, 10);
+    private final static Dimension MAXIMUM_SIZE =
+        new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE);
+
+    private static Map<Integer, Integer> cursors = new HashMap<Integer, Integer>();
+    {
+        cursors.put(1, Cursor.N_RESIZE_CURSOR);
+        cursors.put(2, Cursor.W_RESIZE_CURSOR);
+        cursors.put(4, Cursor.S_RESIZE_CURSOR);
+        cursors.put(8, Cursor.E_RESIZE_CURSOR);
+        cursors.put(3, Cursor.NW_RESIZE_CURSOR);
+        cursors.put(9, Cursor.NE_RESIZE_CURSOR);
+        cursors.put(6, Cursor.SW_RESIZE_CURSOR);
+        cursors.put(12, Cursor.SE_RESIZE_CURSOR);
+    }
+
+    private Insets dragInsets;
+    private Dimension snapSize;
+
+    private int direction;
+    protected static final int NORTH = 1;
+    protected static final int WEST = 2;
+    protected static final int SOUTH = 4;
+    protected static final int EAST = 8;
+
+    private Cursor sourceCursor;
+    private boolean resizing;
+    private Rectangle bounds;
+    private Point pressed;
+    private boolean autoscrolls;
+
+    private Dimension minimumSize = MINIMUM_SIZE;
+    private Dimension maximumSize = MAXIMUM_SIZE;
+    
+    // 性能优化:减少重绘频率
+    private long lastUpdateTime = 0;
+    private static final long UPDATE_INTERVAL_MS = 16; // 约60fps
+
+    /**
+     * Convenience contructor. All borders are resizable in increments of
+     * a single pixel. Components must be registered separately.
+     */
+    public ComponentResizer() {
+        this(new Insets(5, 5, 5, 5), new Dimension(1, 1));
+    }
+
+    /**
+     * Convenience contructor. All borders are resizable in increments of
+     * a single pixel. Components can be registered when the class is created
+     * or they can be registered separately afterwards.
+     *
+     * @param components components to be automatically registered
+     */
+    public ComponentResizer(Component... components) {
+        this(new Insets(5, 5, 5, 5), new Dimension(1, 1), components);
+    }
+
+    /**
+     * Convenience contructor. Eligible borders are resisable in increments of
+     * a single pixel. Components can be registered when the class is created
+     * or they can be registered separately afterwards.
+     *
+     * @param dragInsets Insets specifying which borders are eligible to be
+     *                   resized.
+     * @param components components to be automatically registered
+     */
+    public ComponentResizer(Insets dragInsets, Component... components) {
+        this(dragInsets, new Dimension(1, 1), components);
+    }
+
+    /**
+     * Create a ComponentResizer.
+     *
+     * @param dragInsets Insets specifying which borders are eligible to be
+     *                   resized.
+     * @param snapSize   Specify the dimension to which the border will snap to
+     *                   when being dragged. Snapping occurs at the halfway mark.
+     * @param components components to be automatically registered
+     */
+    public ComponentResizer(Insets dragInsets, Dimension snapSize, Component... components) {
+        setDragInsets(dragInsets);
+        setSnapSize(snapSize);
+        registerComponent(components);
+    }
+
+    /**
+     * Get the drag insets
+     *
+     * @return the drag insets
+     */
+    public Insets getDragInsets() {
+        return dragInsets;
+    }
+
+    /**
+     * Set the drag dragInsets. The insets specify an area where mouseDragged
+     * events are recognized from the edge of the border inwards. A value of
+     * 0 for any size will imply that the border is not resizable. Otherwise
+     * the appropriate drag cursor will appear when the mouse is inside the
+     * resizable border area.
+     *
+     * @param dragInsets Insets to control which borders are resizeable.
+     */
+    public void setDragInsets(Insets dragInsets) {
+        validateMinimumAndInsets(minimumSize, dragInsets);
+        this.dragInsets = dragInsets;
+    }
+
+    /**
+     * Get the components maximum size.
+     *
+     * @return the maximum size
+     */
+    public Dimension getMaximumSize() {
+        return maximumSize;
+    }
+
+    /**
+     * Specify the maximum size for the component. The component will still
+     * be constrained by the size of its parent.
+     *
+     * @param maximumSize the maximum size for a component.
+     */
+    public void setMaximumSize(Dimension maximumSize) {
+        this.maximumSize = maximumSize;
+    }
+
+    /**
+     * Get the components minimum size.
+     *
+     * @return the minimum size
+     */
+    public Dimension getMinimumSize() {
+        return minimumSize;
+    }
+
+    /**
+     * Specify the minimum size for the component. The minimum size is
+     * constrained by the drag insets.
+     *
+     * @param minimumSize the minimum size for a component.
+     */
+    public void setMinimumSize(Dimension minimumSize) {
+        validateMinimumAndInsets(minimumSize, dragInsets);
+        this.minimumSize = minimumSize;
+    }
+
+    /**
+     * Remove listeners from the specified component
+     *
+     * @param component the component the listeners are removed from
+     */
+    public void deregisterComponent(Component... components) {
+        for (Component component : components) {
+            component.removeMouseListener(this);
+            component.removeMouseMotionListener(this);
+        }
+    }
+
+    /**
+     * Add the required listeners to the specified component
+     *
+     * @param component the component the listeners are added to
+     */
+    public void registerComponent(Component... components) {
+        for (Component component : components) {
+            component.addMouseListener(this);
+            component.addMouseMotionListener(this);
+        }
+    }
+
+    /**
+     * Get the snap size.
+     *
+     * @return the snap size.
+     */
+    public Dimension getSnapSize() {
+        return snapSize;
+    }
+
+    /**
+     * Control how many pixels a border must be dragged before the size of
+     * the component is changed. The border will snap to the size once
+     * dragging has passed the halfway mark.
+     *
+     * @param snapSize Dimension object allows you to separately spcify a
+     *                 horizontal and vertical snap size.
+     */
+    public void setSnapSize(Dimension snapSize) {
+        this.snapSize = snapSize;
+    }
+
+    /**
+     * When the components minimum size is less than the drag insets then
+     * we can't determine which border should be resized so we need to
+     * prevent this from happening.
+     */
+    private void validateMinimumAndInsets(Dimension minimum, Insets drag) {
+        int minimumWidth = drag.left + drag.right;
+        int minimumHeight = drag.top + drag.bottom;
+
+        if (minimum.width < minimumWidth
+                || minimum.height < minimumHeight) {
+            String message = "Minimum size cannot be less than drag insets";
+            throw new IllegalArgumentException(message);
+        }
+    }
+
+    @Override
+    public void mouseMoved(MouseEvent e) {
+        Component source = e.getComponent();
+        Point location = e.getPoint();
+        direction = 0;
+
+        if (location.x < dragInsets.left)
+            direction += WEST;
+
+        if (location.x > source.getWidth() - dragInsets.right - 1)
+            direction += EAST;
+
+        if (location.y < dragInsets.top)
+            direction += NORTH;
+
+        if (location.y > source.getHeight() - dragInsets.bottom - 1)
+            direction += SOUTH;
+
+        // Mouse is no longer over a resizable border
+        if (direction == 0) {
+            source.setCursor(sourceCursor);
+        } else { // use the appropriate resizable cursor
+            int cursorType = cursors.get(direction);
+            Cursor cursor = Cursor.getPredefinedCursor(cursorType);
+            source.setCursor(cursor);
+        }
+    }
+
+    @Override
+    public void mouseEntered(MouseEvent e) {
+        if (!resizing) {
+            Component source = e.getComponent();
+            sourceCursor = source.getCursor();
+        }
+    }
+
+    @Override
+    public void mouseExited(MouseEvent e) {
+        if (!resizing) {
+            Component source = e.getComponent();
+            source.setCursor(sourceCursor);
+        }
+    }
+
+    @Override
+    public void mousePressed(MouseEvent e) {
+        // The mouseMoved event continually updates this variable
+        if (direction == 0)
+            return;
+
+        // Setup for resizing. All future dragging calculations are done based
+        // on the original bounds of the component and mouse pressed location.
+        resizing = true;
+        lastUpdateTime = 0; // 重置时间,允许立即更新
+
+        Component source = e.getComponent();
+        pressed = e.getPoint();
+        SwingUtilities.convertPointToScreen(pressed, source);
+        bounds = source.getBounds();
+
+        // Making sure autoscrolls is false will allow for smoother resizing
+        // of components
+        if (source instanceof JComponent) {
+            JComponent jc = (JComponent) source;
+            autoscrolls = jc.getAutoscrolls();
+            jc.setAutoscrolls(false);
+        }
+    }
+
+    /**
+     * Restore the original state of the Component
+     */
+    @Override
+    public void mouseReleased(MouseEvent e) {
+        resizing = false;
+
+        Component source = e.getComponent();
+        source.setCursor(sourceCursor);
+
+        if (source instanceof JComponent) {
+            ((JComponent) source).setAutoscrolls(autoscrolls);
+        }
+        
+        // 通知DraggableComponentWrapper调整大小完成
+        if (source instanceof DraggableComponentWrapper) {
+            ((DraggableComponentWrapper) source).onResizeComplete();
+        }
+    }
+
+    /**
+     * Resize the component ensuring location and size is within the bounds
+     * of the parent container and that the size is within the minimum and
+     * maximum constraints.
+     *
+     * All calculations are done using the bounds of the component when the
+     * resizing started.
+     */
+    @Override
+    public void mouseDragged(MouseEvent e) {
+        if (resizing == false)
+            return;
+
+        // 性能优化:限制更新频率,避免过度重绘
+        long currentTime = System.currentTimeMillis();
+        if (currentTime - lastUpdateTime < UPDATE_INTERVAL_MS) {
+            return;
+        }
+        lastUpdateTime = currentTime;
+
+        Component source = e.getComponent();
+        Point dragged = e.getPoint();
+        SwingUtilities.convertPointToScreen(dragged, source);
+
+        changeBounds(source, direction, bounds, pressed, dragged);
+        
+        // 通知DraggableComponentWrapper更新配置
+        if (source instanceof DraggableComponentWrapper) {
+            ((DraggableComponentWrapper) source).onResizing();
+        }
+    }
+
+    protected void changeBounds(Component source, int direction, Rectangle bounds, Point pressed, Point current) {
+        // Start with original locaton and size
+        int x = bounds.x;
+        int y = bounds.y;
+        int width = bounds.width;
+        int height = bounds.height;
+
+        // Resizing the West or North border affects the size and location
+        if (WEST == (direction & WEST)) {
+            int drag = getDragDistance(pressed.x, current.x, snapSize.width);
+            int maximum = Math.min(width + x, maximumSize.width);
+            drag = getDragBounded(drag, snapSize.width, width, minimumSize.width, maximum);
+
+            x -= drag;
+            width += drag;
+        }
+
+        if (NORTH == (direction & NORTH)) {
+            int drag = getDragDistance(pressed.y, current.y, snapSize.height);
+            int maximum = Math.min(height + y, maximumSize.height);
+            drag = getDragBounded(drag, snapSize.height, height, minimumSize.height, maximum);
+
+            y -= drag;
+            height += drag;
+        }
+
+        // Resizing the East or South border only affects the size
+        if (EAST == (direction & EAST)) {
+            int drag = getDragDistance(current.x, pressed.x, snapSize.width);
+            Dimension boundingSize = getBoundingSize(source);
+            int maximum = Math.min(boundingSize.width - x, maximumSize.width);
+            drag = getDragBounded(drag, snapSize.width, width, minimumSize.width, maximum);
+            width += drag;
+        }
+
+        if (SOUTH == (direction & SOUTH)) {
+            int drag = getDragDistance(current.y, pressed.y, snapSize.height);
+            Dimension boundingSize = getBoundingSize(source);
+            int maximum = Math.min(boundingSize.height - y, maximumSize.height);
+            drag = getDragBounded(drag, snapSize.height, height, minimumSize.height, maximum);
+            height += drag;
+        }
+
+        // 使用单次setBounds调用,避免多次重绘
+        source.setBounds(x, y, width, height);
+        
+        // 使用revalidate()代替validate()以提高性能
+        if (source instanceof JComponent) {
+            ((JComponent) source).revalidate();
+        } else {
+            source.validate();
+        }
+    }
+
+    /*
+     * Determine how far the mouse has moved from where dragging started
+     */
+    private int getDragDistance(int larger, int smaller, int snapSize) {
+        int halfway = snapSize / 2;
+        int drag = larger - smaller;
+        drag += (drag < 0) ? -halfway : halfway;
+        drag = (drag / snapSize) * snapSize;
+
+        return drag;
+    }
+
+    /*
+     * Adjust the drag value to be within the minimum and maximum range.
+     */
+    private int getDragBounded(int drag, int snapSize, int dimension, int minimum, int maximum) {
+        while (dimension + drag < minimum)
+            drag += snapSize;
+
+        while (dimension + drag > maximum)
+            drag -= snapSize;
+
+        return drag;
+    }
+
+    /*
+     * Keep the size of the component within the bounds of its parent.
+     */
+    private Dimension getBoundingSize(Component source) {
+        if (source instanceof Window) {
+            GraphicsEnvironment env = GraphicsEnvironment.getLocalGraphicsEnvironment();
+            Rectangle bounds = env.getMaximumWindowBounds();
+            return new Dimension(bounds.width, bounds.height);
+        } else {
+            return source.getParent().getSize();
+        }
+    }
+}

+ 593 - 0
src/com/mes/ui/layout/DraggableComponentWrapper.java

@@ -0,0 +1,593 @@
+package com.mes.ui.layout;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.swing.*;
+import javax.swing.border.LineBorder;
+import java.awt.*;
+import java.awt.event.*;
+
+/**
+ * 可拖拽组件包装器
+ * 在编辑模式下包装原始组件,添加拖拽、调整大小功能
+ * 使用成熟的ComponentResizer处理调整大小
+ */
+public class DraggableComponentWrapper extends JPanel {
+    private static final Logger log = LoggerFactory.getLogger(DraggableComponentWrapper.class);
+    
+    // 原始组件
+    private JComponent wrappedComponent;
+    
+    // 组件ID
+    private String componentId;
+    
+    // 布局配置引用
+    private UILayoutConfig.ComponentLayout layoutConfig;
+    
+    // 父容器(用于计算相对位置)
+    private Container parentContainer;
+    
+    // 拖拽状态
+    private boolean editMode = false;
+    private boolean dragging = false;
+    private boolean resizing = false;  // 标记是否正在调整大小
+    private Point dragStartPoint;
+    private Point componentStartPoint;
+    
+    // 调整大小处理器
+    private ComponentResizer resizer;
+    
+    // 保存监听器引用以便移除
+    private MouseAdapter dragListener;
+    
+    // 网格吸附
+    private boolean snapToGrid = true;
+    private int gridSize = 10;
+    
+    // 状态监听器
+    private LayoutChangeListener layoutChangeListener;
+    
+    // 性能优化:限制更新频率
+    private long lastDragUpdateTime = 0;
+    private static final long DRAG_UPDATE_INTERVAL_MS = 16; // 约60fps
+    
+    public DraggableComponentWrapper(JComponent component, String componentId, 
+                                     UILayoutConfig.ComponentLayout layoutConfig,
+                                     Container parentContainer) {
+        this.wrappedComponent = component;
+        this.componentId = componentId;
+        this.layoutConfig = layoutConfig;
+        this.parentContainer = parentContainer;
+        
+        initUI();
+        setupListeners();
+    }
+    
+    /**
+     * 初始化UI
+     */
+    private void initUI() {
+        setLayout(new BorderLayout());
+        setOpaque(false);
+        add(wrappedComponent, BorderLayout.CENTER);
+        
+        // 创建调整大小处理器
+        resizer = new ComponentResizer();
+        resizer.setMinimumSize(new Dimension(50, 30));
+        resizer.setDragInsets(new Insets(15, 15, 15, 15));  // 15像素边缘
+        resizer.setSnapSize(new Dimension(gridSize, gridSize));
+        
+        // 编辑模式下显示边框
+        updateBorder();
+    }
+    
+    /**
+     * 设置编辑模式
+     */
+    public void setEditMode(boolean editMode) {
+        this.editMode = editMode;
+        updateBorder();
+        
+        if (editMode) {
+            // 编辑模式:禁用子组件以防止点击触发功能
+            // 但保持可见状态
+            if (wrappedComponent instanceof JButton) {
+                // 禁用按钮,防止触发ActionListener
+                wrappedComponent.setEnabled(false);
+            }
+            wrappedComponent.setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
+            setCursor(Cursor.getDefaultCursor());
+        } else {
+            // 恢复子组件状态
+            wrappedComponent.setEnabled(layoutConfig.isEnabled());
+            wrappedComponent.setCursor(Cursor.getDefaultCursor());
+            
+            // 移除监听器
+            if (dragListener != null) {
+                this.removeMouseListener(dragListener);
+                this.removeMouseMotionListener(dragListener);
+                wrappedComponent.removeMouseListener(dragListener);
+                wrappedComponent.removeMouseMotionListener(dragListener);
+            }
+            
+            // 禁用调整大小
+            if (resizer != null) {
+                resizer.deregisterComponent(this);
+            }
+            setCursor(Cursor.getDefaultCursor());
+            
+            // 重置拖拽状态
+            dragging = false;
+            resizing = false;
+        }
+    }
+    
+    /**
+     * 更新边框显示
+     */
+    private void updateBorder() {
+        if (editMode) {
+            setBorder(new LineBorder(new Color(66, 165, 245), 2, false));
+            setOpaque(true);
+            setBackground(new Color(66, 165, 245, 30));  // 半透明蓝色
+        } else {
+            setBorder(null);
+            setOpaque(false);
+        }
+    }
+    
+    /**
+     * 设置监听器
+     */
+    private void setupListeners() {
+        // 拖拽移动监听器
+        dragListener = new MouseAdapter() {
+            @Override
+            public void mousePressed(MouseEvent e) {
+                if (!editMode) return;
+                
+                // 编辑模式下消费事件,防止按钮触发点击
+                e.consume();
+                
+                // 中心区域点击:启动拖拽移动
+                dragging = true;
+                resizing = false;
+                lastDragUpdateTime = 0;
+                
+                componentStartPoint = getLocation();
+                dragStartPoint = e.getLocationOnScreen();
+            }
+            
+            @Override
+            public void mouseReleased(MouseEvent e) {
+                if (!editMode) return;
+                
+                // 编辑模式下消费事件
+                e.consume();
+                
+                if (dragging) {
+                    updateLayoutConfig();
+                    
+                    if (layoutChangeListener != null) {
+                        layoutChangeListener.onLayoutChanged(componentId, layoutConfig);
+                    }
+                }
+                
+                dragging = false;
+                resizing = false;
+            }
+            
+            @Override
+            public void mouseClicked(MouseEvent e) {
+                if (!editMode) return;
+                // 编辑模式下消费点击事件,防止按钮触发
+                e.consume();
+            }
+            
+            @Override
+            public void mouseDragged(MouseEvent e) {
+                if (!editMode) return;
+                
+                // 编辑模式下消费事件
+                e.consume();
+                
+                if (dragging && !resizing) {
+                    handleDrag(e);
+                }
+            }
+            
+            @Override
+            public void mouseMoved(MouseEvent e) {
+                if (!editMode) return;
+                
+                // 子组件区域显示移动光标
+                wrappedComponent.setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
+            }
+        };
+        
+        // 在包装器上添加监听器(编辑模式下子组件被禁用,事件会冒泡到包装器)
+        this.addMouseListener(dragListener);
+        this.addMouseMotionListener(dragListener);
+        
+        // 同时在子组件上添加监听器(以防万一)
+        wrappedComponent.addMouseListener(dragListener);
+        wrappedComponent.addMouseMotionListener(dragListener);
+        
+        // 在包装器上注册ComponentResizer(处理边缘区域的调整大小)
+        resizer.registerComponent(this);
+        
+        // 右键菜单
+        setComponentPopupMenu(createPopupMenu());
+    }
+    
+    /**
+     * 处理拖拽移动
+     */
+    private void handleDrag(MouseEvent e) {
+        try {
+            // 性能优化:限制更新频率
+            long currentTime = System.currentTimeMillis();
+            if (currentTime - lastDragUpdateTime < DRAG_UPDATE_INTERVAL_MS) {
+                return;
+            }
+            lastDragUpdateTime = currentTime;
+            
+            Point currentPoint = e.getLocationOnScreen();
+            int dx = currentPoint.x - dragStartPoint.x;
+            int dy = currentPoint.y - dragStartPoint.y;
+            
+            int newX = componentStartPoint.x + dx;
+            int newY = componentStartPoint.y + dy;
+            
+            // 网格吸附
+            if (snapToGrid) {
+                newX = Math.round(newX / (float)gridSize) * gridSize;
+                newY = Math.round(newY / (float)gridSize) * gridSize;
+            }
+            
+            // 边界检查(使用实际父组件的大小)
+            Container actualParent = getParent();
+            if (actualParent == null) {
+                actualParent = parentContainer;
+            }
+            if (actualParent != null) {
+                int maxX = actualParent.getWidth() - getWidth();
+                int maxY = actualParent.getHeight() - getHeight();
+                newX = Math.max(0, Math.min(newX, maxX));
+                newY = Math.max(0, Math.min(newY, maxY));
+            }
+            
+            // 只移动位置,不改变大小
+            setLocation(newX, newY);
+            
+        } catch (Exception ex) {
+            log.error("拖拽处理出错", ex);
+        }
+    }
+    
+    /**
+     * 更新布局配置(将像素坐标转换为相对坐标)
+     */
+    private void updateLayoutConfig() {
+        // 使用实际父组件(gridOverlay)的大小
+        Container actualParent = getParent();
+        if (actualParent == null) {
+            actualParent = parentContainer;
+        }
+        if (actualParent == null) return;
+        
+        int parentWidth = actualParent.getWidth();
+        int parentHeight = actualParent.getHeight();
+        
+        if (parentWidth == 0 || parentHeight == 0) return;
+        
+        // 从 WorkstationPanel 获取布局偏移量
+        int offsetLeft = 10;
+        int offsetTop = 40;
+        int offsetRight = 10;
+        int offsetBottom = 10;
+        
+        // 尝试从 parentContainer 获取正确的偏移量
+        if (parentContainer instanceof com.mes.ui.component.WorkstationPanel) {
+            int[] offsets = ((com.mes.ui.component.WorkstationPanel) parentContainer).getLayoutOffsets();
+            offsetLeft = offsets[0];
+            offsetTop = offsets[1];
+            offsetRight = offsets[2];
+            offsetBottom = offsets[3];
+        }
+        
+        // 计算模拟的内容区域大小
+        int contentWidth = parentWidth - offsetLeft - offsetRight;
+        int contentHeight = parentHeight - offsetTop - offsetBottom;
+        
+        if (contentWidth <= 0 || contentHeight <= 0) return;
+        
+        // 计算相对位置 (0-1),减去偏移量后再计算
+        double relX = (double)(getX() - offsetLeft) / contentWidth;
+        double relY = (double)(getY() - offsetTop) / contentHeight;
+        double relWidth = (double) getWidth() / contentWidth;
+        double relHeight = (double) getHeight() / contentHeight;
+        
+        // 确保值在合理范围内
+        relX = Math.max(0, Math.min(1, relX));
+        relY = Math.max(0, Math.min(1, relY));
+        relWidth = Math.max(0.01, Math.min(1, relWidth));
+        relHeight = Math.max(0.01, Math.min(1, relHeight));
+        
+        // 更新配置
+        layoutConfig.getPosition().setX(relX);
+        layoutConfig.getPosition().setY(relY);
+        layoutConfig.getSize().setWidth(relWidth);
+        layoutConfig.getSize().setHeight(relHeight);
+    }
+    
+    /**
+     * 创建右键菜单
+     */
+    private JPopupMenu createPopupMenu() {
+        JPopupMenu menu = new JPopupMenu();
+        
+        // 对齐选项
+        JMenu alignMenu = new JMenu("对齐");
+        alignMenu.add(createMenuItem("左对齐", e -> alignLeft()));
+        alignMenu.add(createMenuItem("水平居中", e -> alignCenter()));
+        alignMenu.add(createMenuItem("右对齐", e -> alignRight()));
+        alignMenu.addSeparator();
+        alignMenu.add(createMenuItem("顶部对齐", e -> alignTop()));
+        alignMenu.add(createMenuItem("垂直居中", e -> alignMiddle()));
+        alignMenu.add(createMenuItem("底部对齐", e -> alignBottom()));
+        menu.add(alignMenu);
+        
+        menu.addSeparator();
+        
+        // 尺寸选项
+        menu.add(createMenuItem("适应内容", e -> fitToContent()));
+        menu.add(createMenuItem("重置尺寸", e -> resetSize()));
+        
+        menu.addSeparator();
+        
+        // 显示/隐藏
+        JCheckBoxMenuItem visibleItem = new JCheckBoxMenuItem("显示", layoutConfig.isVisible());
+        visibleItem.addActionListener(e -> {
+            layoutConfig.setVisible(visibleItem.isSelected());
+            setVisible(visibleItem.isSelected());
+        });
+        menu.add(visibleItem);
+        
+        // 启用/禁用
+        JCheckBoxMenuItem enabledItem = new JCheckBoxMenuItem("启用", layoutConfig.isEnabled());
+        enabledItem.addActionListener(e -> {
+            layoutConfig.setEnabled(enabledItem.isSelected());
+            wrappedComponent.setEnabled(enabledItem.isSelected());
+        });
+        menu.add(enabledItem);
+        
+        menu.addSeparator();
+        
+        // 属性编辑
+        menu.add(createMenuItem("编辑属性...", e -> showPropertiesDialog()));
+        
+        return menu;
+    }
+    
+    private JMenuItem createMenuItem(String text, ActionListener listener) {
+        JMenuItem item = new JMenuItem(text);
+        item.addActionListener(listener);
+        return item;
+    }
+    
+    // 获取实际父容器
+    private Container getActualParent() {
+        Container actualParent = getParent();
+        return actualParent != null ? actualParent : parentContainer;
+    }
+    
+    // 对齐方法
+    private void alignLeft() {
+        setLocation(0, getY());
+        updateLayoutConfig();
+    }
+    
+    private void alignCenter() {
+        Container parent = getActualParent();
+        if (parent != null) {
+            int centerX = (parent.getWidth() - getWidth()) / 2;
+            setLocation(centerX, getY());
+            updateLayoutConfig();
+        }
+    }
+    
+    private void alignRight() {
+        Container parent = getActualParent();
+        if (parent != null) {
+            int rightX = parent.getWidth() - getWidth();
+            setLocation(rightX, getY());
+            updateLayoutConfig();
+        }
+    }
+    
+    private void alignTop() {
+        setLocation(getX(), 0);
+        updateLayoutConfig();
+    }
+    
+    private void alignMiddle() {
+        Container parent = getActualParent();
+        if (parent != null) {
+            int middleY = (parent.getHeight() - getHeight()) / 2;
+            setLocation(getX(), middleY);
+            updateLayoutConfig();
+        }
+    }
+    
+    private void alignBottom() {
+        Container parent = getActualParent();
+        if (parent != null) {
+            int bottomY = parent.getHeight() - getHeight();
+            setLocation(getX(), bottomY);
+            updateLayoutConfig();
+        }
+    }
+    
+    private void fitToContent() {
+        Dimension prefSize = wrappedComponent.getPreferredSize();
+        setSize(prefSize);
+        updateLayoutConfig();
+    }
+    
+    private void resetSize() {
+        Container parent = getActualParent();
+        if (parent != null) {
+            // 恢复默认尺寸
+            int defaultWidth = (int)(layoutConfig.getSize().getWidth() * parent.getWidth());
+            int defaultHeight = (int)(layoutConfig.getSize().getHeight() * parent.getHeight());
+            setSize(defaultWidth, defaultHeight);
+            updateLayoutConfig();
+        }
+    }
+    
+    /**
+     * 显示属性编辑对话框
+     */
+    private void showPropertiesDialog() {
+        JDialog dialog = new JDialog((Frame)SwingUtilities.getWindowAncestor(this), 
+                                     "组件属性 - " + componentId, true);
+        dialog.setSize(450, 400);
+        dialog.setLocationRelativeTo(this);
+        
+        // 创建属性编辑面板
+        JPanel panel = new JPanel(new GridLayout(0, 2, 10, 10));
+        panel.setBorder(BorderFactory.createEmptyBorder(15, 15, 15, 15));
+        
+        // 字体大小
+        panel.add(new JLabel("字体大小:"));
+        JSpinner fontSizeSpinner = new JSpinner(new SpinnerNumberModel(
+            layoutConfig.getStyle().getFontSize(), 8, 72, 1));
+        panel.add(fontSizeSpinner);
+        
+        // 前景色
+        panel.add(new JLabel("文字颜色:"));
+        JButton fgColorBtn = new JButton("选择颜色");
+        fgColorBtn.addActionListener(e -> {
+            Color color = JColorChooser.showDialog(dialog, "选择文字颜色", 
+                Color.decode(layoutConfig.getStyle().getForeground()));
+            if (color != null) {
+                layoutConfig.getStyle().setForeground(String.format("#%06X", 
+                    color.getRGB() & 0xFFFFFF));
+                wrappedComponent.setForeground(color);
+            }
+        });
+        panel.add(fgColorBtn);
+        
+        // 背景色
+        panel.add(new JLabel("背景颜色:"));
+        JButton bgColorBtn = new JButton("选择颜色");
+        bgColorBtn.addActionListener(e -> {
+            Color color = JColorChooser.showDialog(dialog, "选择背景颜色",
+                Color.decode(layoutConfig.getStyle().getBackground()));
+            if (color != null) {
+                layoutConfig.getStyle().setBackground(String.format("#%06X", 
+                    color.getRGB() & 0xFFFFFF));
+                wrappedComponent.setBackground(color);
+            }
+        });
+        panel.add(bgColorBtn);
+        
+        // 确定取消按钮
+        JPanel btnPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 10, 10));
+        
+        JButton okBtn = new JButton("确定");
+        okBtn.addActionListener(e -> {
+            // 应用更改
+            int fontSize = (int)fontSizeSpinner.getValue();
+            layoutConfig.getStyle().setFontSize(fontSize);
+            Font font = wrappedComponent.getFont();
+            wrappedComponent.setFont(font.deriveFont((float)fontSize));
+            
+            if (layoutChangeListener != null) {
+                layoutChangeListener.onLayoutChanged(componentId, layoutConfig);
+            }
+            
+            dialog.dispose();
+        });
+        btnPanel.add(okBtn);
+        
+        JButton cancelBtn = new JButton("取消");
+        cancelBtn.addActionListener(e -> dialog.dispose());
+        btnPanel.add(cancelBtn);
+        
+        dialog.setLayout(new BorderLayout());
+        dialog.add(panel, BorderLayout.CENTER);
+        dialog.add(btnPanel, BorderLayout.SOUTH);
+        dialog.setVisible(true);
+    }
+    
+    // Getters & Setters
+    
+    public JComponent getWrappedComponent() {
+        return wrappedComponent;
+    }
+    
+    public String getComponentId() {
+        return componentId;
+    }
+    
+    public UILayoutConfig.ComponentLayout getLayoutConfig() {
+        return layoutConfig;
+    }
+    
+    public void setLayoutConfig(UILayoutConfig.ComponentLayout layoutConfig) {
+        this.layoutConfig = layoutConfig;
+    }
+    
+    public void setSnapToGrid(boolean snapToGrid) {
+        this.snapToGrid = snapToGrid;
+        if (resizer != null) {
+            resizer.setSnapSize(new Dimension(gridSize, gridSize));
+        }
+    }
+    
+    public void setGridSize(int gridSize) {
+        this.gridSize = gridSize;
+        if (resizer != null) {
+            resizer.setSnapSize(new Dimension(gridSize, gridSize));
+        }
+    }
+    
+    public void setLayoutChangeListener(LayoutChangeListener listener) {
+        this.layoutChangeListener = listener;
+    }
+    
+    /**
+     * 调整大小中(由ComponentResizer调用)
+     */
+    public void onResizing() {
+        resizing = true;
+        dragging = false;  // 确保不会同时拖拽和调整大小
+    }
+    
+    /**
+     * 调整大小完成(由ComponentResizer调用)
+     */
+    public void onResizeComplete() {
+        // 直接保存配置,不检查 resizing 标志
+        // 因为 ComponentResizer 只会在真正调整大小完成时调用此方法
+        updateLayoutConfig();
+        
+        // 触发布局变更事件
+        if (layoutChangeListener != null) {
+            layoutChangeListener.onLayoutChanged(componentId, layoutConfig);
+        }
+        
+        log.debug("调整大小完成: {}, 新尺寸: {}x{}", componentId, getWidth(), getHeight());
+        resizing = false;
+    }
+    
+    /**
+     * 布局变更监听器
+     */
+    public interface LayoutChangeListener {
+        void onLayoutChanged(String componentId, UILayoutConfig.ComponentLayout layout);
+    }
+}

+ 554 - 0
src/com/mes/ui/layout/UILayoutConfig.java

@@ -0,0 +1,554 @@
+package com.mes.ui.layout;
+
+import org.yaml.snakeyaml.DumperOptions;
+import org.yaml.snakeyaml.LoaderOptions;
+import org.yaml.snakeyaml.Yaml;
+import org.yaml.snakeyaml.constructor.Constructor;
+import org.yaml.snakeyaml.representer.Representer;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * UI布局配置 - 保存组件位置、大小、样式
+ * 支持两种布局模式:free(自由定位) 和 flex(弹性行布局)
+ */
+public class UILayoutConfig {
+    // 布局版本
+    private String version = "1.0";
+    
+    // 布局模式: "free" = 自由定位(默认), "flex" = 弹性行布局
+    private String layoutMode = "free";
+    
+    // 组件布局配置 (free模式使用)
+    private Map<String, ComponentLayout> components = new LinkedHashMap<>();
+    
+    // 单工位模式布局
+    private Map<String, ComponentLayout> singleComponents = new LinkedHashMap<>();
+    
+    // 双工位模式布局
+    private Map<String, ComponentLayout> dualComponents = new LinkedHashMap<>();
+    
+    // 行布局配置 (flex模式使用)
+    private List<RowLayout> rows = new ArrayList<>();
+    
+    // 单工位模式行布局
+    private List<RowLayout> singleRows = new ArrayList<>();
+    
+    // 双工位模式行布局
+    private List<RowLayout> dualRows = new ArrayList<>();
+    
+    // 全局设置
+    private GlobalSettings globalSettings = new GlobalSettings();
+    
+    /**
+     * 行布局配置 (flex模式)
+     */
+    public static class RowLayout {
+        private String id;
+        private int order = 0;
+        private double flex = 1.0;
+        private boolean visible = true;
+        private int gap = 10;
+        private String alignItems = "stretch";
+        private List<RowComponentLayout> components = new ArrayList<>();
+        
+        public String getId() { return id; }
+        public void setId(String id) { this.id = id; }
+        public int getOrder() { return order; }
+        public void setOrder(int order) { this.order = order; }
+        public double getFlex() { return flex; }
+        public void setFlex(double flex) { this.flex = flex; }
+        public boolean isVisible() { return visible; }
+        public void setVisible(boolean visible) { this.visible = visible; }
+        public int getGap() { return gap; }
+        public void setGap(int gap) { this.gap = gap; }
+        public String getAlignItems() { return alignItems; }
+        public void setAlignItems(String alignItems) { this.alignItems = alignItems; }
+        public List<RowComponentLayout> getComponents() { return components; }
+        public void setComponents(List<RowComponentLayout> components) { this.components = components; }
+        
+        public List<RowComponentLayout> getVisibleComponents() {
+            List<RowComponentLayout> visibleList = new ArrayList<>();
+            for (RowComponentLayout comp : components) {
+                if (comp.isVisible()) visibleList.add(comp);
+            }
+            return visibleList;
+        }
+    }
+    
+    /**
+     * 行内组件布局配置
+     */
+    public static class RowComponentLayout {
+        private String id;
+        private double flex = 1.0;
+        private int fixedWidth = 0;
+        private int minWidth = 50;
+        private boolean visible = true;
+        private boolean enabled = true;
+        private Style style;
+        
+        public String getId() { return id; }
+        public void setId(String id) { this.id = id; }
+        public double getFlex() { return flex; }
+        public void setFlex(double flex) { this.flex = flex; }
+        public int getFixedWidth() { return fixedWidth; }
+        public void setFixedWidth(int fixedWidth) { this.fixedWidth = fixedWidth; }
+        public int getMinWidth() { return minWidth; }
+        public void setMinWidth(int minWidth) { this.minWidth = minWidth; }
+        public boolean isVisible() { return visible; }
+        public void setVisible(boolean visible) { this.visible = visible; }
+        public boolean isEnabled() { return enabled; }
+        public void setEnabled(boolean enabled) { this.enabled = enabled; }
+        public Style getStyle() { return style; }
+        public void setStyle(Style style) { this.style = style; }
+    }
+    
+    /**
+     * 组件布局配置
+     */
+    public static class ComponentLayout {
+        // 组件ID (唯一标识)
+        private String id;
+        
+        // 组件类型 (text_field, button, panel等)
+        private String type;
+        
+        // 显示名称
+        private String displayName;
+        
+        // 是否可见
+        private boolean visible = true;
+        
+        // 是否启用
+        private boolean enabled = true;
+        
+        // 位置 (相对坐标 0-1)
+        private Position position = new Position();
+        
+        // 尺寸 (相对尺寸 0-1)
+        private Size size = new Size();
+        
+        // 样式
+        private Style style = new Style();
+        
+        // 约束 (对齐、锚点等)
+        private Constraints constraints = new Constraints();
+        
+        // 自定义属性
+        private Map<String, Object> properties = new HashMap<>();
+
+        // Getters & Setters
+        public String getId() { return id; }
+        public void setId(String id) { this.id = id; }
+        
+        public String getType() { return type; }
+        public void setType(String type) { this.type = type; }
+        
+        public String getDisplayName() { return displayName; }
+        public void setDisplayName(String displayName) { this.displayName = displayName; }
+        
+        public boolean isVisible() { return visible; }
+        public void setVisible(boolean visible) { this.visible = visible; }
+        
+        public boolean isEnabled() { return enabled; }
+        public void setEnabled(boolean enabled) { this.enabled = enabled; }
+        
+        public Position getPosition() { return position; }
+        public void setPosition(Position position) { this.position = position; }
+        
+        public Size getSize() { return size; }
+        public void setSize(Size size) { this.size = size; }
+        
+        public Style getStyle() { return style; }
+        public void setStyle(Style style) { this.style = style; }
+        
+        public Constraints getConstraints() { return constraints; }
+        public void setConstraints(Constraints constraints) { this.constraints = constraints; }
+        
+        public Map<String, Object> getProperties() { return properties; }
+        public void setProperties(Map<String, Object> properties) { this.properties = properties; }
+    }
+    
+    /**
+     * 位置 (相对坐标)
+     */
+    public static class Position {
+        private double x = 0.0;  // 0-1, 相对于父容器宽度
+        private double y = 0.0;  // 0-1, 相对于父容器高度
+        private String anchor = "top_left";  // 锚点: top_left, center, bottom_right等
+
+        public double getX() { return x; }
+        public void setX(double x) { this.x = x; }
+        
+        public double getY() { return y; }
+        public void setY(double y) { this.y = y; }
+        
+        public String getAnchor() { return anchor; }
+        public void setAnchor(String anchor) { this.anchor = anchor; }
+    }
+    
+    /**
+     * 尺寸 (相对尺寸)
+     */
+    public static class Size {
+        private double width = 0.5;   // 0-1, 相对于父容器宽度
+        private double height = 0.1;  // 0-1, 相对于父容器高度
+        private int minWidth = 100;   // 最小宽度(像素)
+        private int minHeight = 30;   // 最小高度(像素)
+        private int maxWidth = 2000;  // 最大宽度(像素)
+        private int maxHeight = 500;  // 最大高度(像素)
+        private boolean autoSize = false;  // 自适应内容大小
+
+        public double getWidth() { return width; }
+        public void setWidth(double width) { this.width = width; }
+        
+        public double getHeight() { return height; }
+        public void setHeight(double height) { this.height = height; }
+        
+        public int getMinWidth() { return minWidth; }
+        public void setMinWidth(int minWidth) { this.minWidth = minWidth; }
+        
+        public int getMinHeight() { return minHeight; }
+        public void setMinHeight(int minHeight) { this.minHeight = minHeight; }
+        
+        public int getMaxWidth() { return maxWidth; }
+        public void setMaxWidth(int maxWidth) { this.maxWidth = maxWidth; }
+        
+        public int getMaxHeight() { return maxHeight; }
+        public void setMaxHeight(int maxHeight) { this.maxHeight = maxHeight; }
+        
+        public boolean isAutoSize() { return autoSize; }
+        public void setAutoSize(boolean autoSize) { this.autoSize = autoSize; }
+    }
+    
+    /**
+     * 样式
+     */
+    public static class Style {
+        private String fontName = "微软雅黑";
+        private int fontSize = 20;
+        private int fontStyle = 0;  // 0=PLAIN, 1=BOLD, 2=ITALIC
+        private String foreground = "#000000";  // 前景色(文字颜色)
+        private String background = "#FFFFFF";  // 背景色
+        private int borderWidth = 1;
+        private String borderColor = "#CCCCCC";
+        private int borderRadius = 0;  // 圆角半径
+        private int[] padding = {5, 5, 5, 5};  // 内边距 [top, right, bottom, left]
+        private int[] margin = {0, 0, 0, 0};   // 外边距
+
+        public String getFontName() { return fontName; }
+        public void setFontName(String fontName) { this.fontName = fontName; }
+        
+        public int getFontSize() { return fontSize; }
+        public void setFontSize(int fontSize) { this.fontSize = fontSize; }
+        
+        public int getFontStyle() { return fontStyle; }
+        public void setFontStyle(int fontStyle) { this.fontStyle = fontStyle; }
+        
+        public String getForeground() { return foreground; }
+        public void setForeground(String foreground) { this.foreground = foreground; }
+        
+        public String getBackground() { return background; }
+        public void setBackground(String background) { this.background = background; }
+        
+        public int getBorderWidth() { return borderWidth; }
+        public void setBorderWidth(int borderWidth) { this.borderWidth = borderWidth; }
+        
+        public String getBorderColor() { return borderColor; }
+        public void setBorderColor(String borderColor) { this.borderColor = borderColor; }
+        
+        public int getBorderRadius() { return borderRadius; }
+        public void setBorderRadius(int borderRadius) { this.borderRadius = borderRadius; }
+        
+        public int[] getPadding() { return padding; }
+        public void setPadding(int[] padding) { this.padding = padding; }
+        
+        public int[] getMargin() { return margin; }
+        public void setMargin(int[] margin) { this.margin = margin; }
+    }
+    
+    /**
+     * 约束条件
+     */
+    public static class Constraints {
+        private String alignX = "left";    // left, center, right
+        private String alignY = "top";     // top, center, bottom
+        private boolean snapToGrid = true;  // 吸附到网格
+        private int gridSize = 10;         // 网格大小(像素)
+        private String relativeTo = null;  // 相对于哪个组件定位
+        private int offsetX = 0;           // X偏移量
+        private int offsetY = 0;           // Y偏移量
+        private int zIndex = 0;            // 层级(越大越在上层)
+
+        public String getAlignX() { return alignX; }
+        public void setAlignX(String alignX) { this.alignX = alignX; }
+        
+        public String getAlignY() { return alignY; }
+        public void setAlignY(String alignY) { this.alignY = alignY; }
+        
+        public boolean isSnapToGrid() { return snapToGrid; }
+        public void setSnapToGrid(boolean snapToGrid) { this.snapToGrid = snapToGrid; }
+        
+        public int getGridSize() { return gridSize; }
+        public void setGridSize(int gridSize) { this.gridSize = gridSize; }
+        
+        public String getRelativeTo() { return relativeTo; }
+        public void setRelativeTo(String relativeTo) { this.relativeTo = relativeTo; }
+        
+        public int getOffsetX() { return offsetX; }
+        public void setOffsetX(int offsetX) { this.offsetX = offsetX; }
+        
+        public int getOffsetY() { return offsetY; }
+        public void setOffsetY(int offsetY) { this.offsetY = offsetY; }
+        
+        public int getZIndex() { return zIndex; }
+        public void setZIndex(int zIndex) { this.zIndex = zIndex; }
+    }
+    
+    /**
+     * 全局设置
+     */
+    public static class GlobalSettings {
+        private int baseWidth = 800;   // 基准宽度
+        private int baseHeight = 600;  // 基准高度
+        private boolean showGrid = false;  // 显示网格
+        private int gridSize = 20;     // 网格大小
+        private String gridColor = "#EEEEEE";
+        private boolean enableSnap = true;  // 启用吸附
+        private int snapThreshold = 5;  // 吸附阈值(像素)
+
+        public int getBaseWidth() { return baseWidth; }
+        public void setBaseWidth(int baseWidth) { this.baseWidth = baseWidth; }
+        
+        public int getBaseHeight() { return baseHeight; }
+        public void setBaseHeight(int baseHeight) { this.baseHeight = baseHeight; }
+        
+        public boolean isShowGrid() { return showGrid; }
+        public void setShowGrid(boolean showGrid) { this.showGrid = showGrid; }
+        
+        public int getGridSize() { return gridSize; }
+        public void setGridSize(int gridSize) { this.gridSize = gridSize; }
+        
+        public String getGridColor() { return gridColor; }
+        public void setGridColor(String gridColor) { this.gridColor = gridColor; }
+        
+        public boolean isEnableSnap() { return enableSnap; }
+        public void setEnableSnap(boolean enableSnap) { this.enableSnap = enableSnap; }
+        
+        public int getSnapThreshold() { return snapThreshold; }
+        public void setSnapThreshold(int snapThreshold) { this.snapThreshold = snapThreshold; }
+    }
+    
+    // Getters & Setters
+    public String getVersion() { return version; }
+    public void setVersion(String version) { this.version = version; }
+    
+    public String getLayoutMode() { return layoutMode; }
+    public void setLayoutMode(String layoutMode) { this.layoutMode = layoutMode; }
+    
+    public Map<String, ComponentLayout> getComponents() { return components; }
+    public void setComponents(Map<String, ComponentLayout> components) { this.components = components; }
+    
+    public Map<String, ComponentLayout> getSingleComponents() { return singleComponents; }
+    public void setSingleComponents(Map<String, ComponentLayout> singleComponents) { this.singleComponents = singleComponents; }
+    
+    public Map<String, ComponentLayout> getDualComponents() { return dualComponents; }
+    public void setDualComponents(Map<String, ComponentLayout> dualComponents) { this.dualComponents = dualComponents; }
+    
+    /**
+     * 根据模式获取组件配置
+     * @param isSingleMode true=单工位模式, false=双工位模式
+     */
+    public Map<String, ComponentLayout> getComponentsForMode(boolean isSingleMode) {
+        Map<String, ComponentLayout> modeComponents = isSingleMode ? singleComponents : dualComponents;
+        // 如果模式专用配置为空,回退到通用配置
+        return (modeComponents != null && !modeComponents.isEmpty()) ? modeComponents : components;
+    }
+    
+    /**
+     * 根据模式获取单个组件配置
+     */
+    public ComponentLayout getComponentForMode(String id, boolean isSingleMode) {
+        Map<String, ComponentLayout> modeComponents = getComponentsForMode(isSingleMode);
+        return modeComponents.get(id);
+    }
+    
+    /**
+     * 根据模式设置组件配置
+     */
+    public void setComponentForMode(String id, ComponentLayout layout, boolean isSingleMode) {
+        if (isSingleMode) {
+            singleComponents.put(id, layout);
+        } else {
+            dualComponents.put(id, layout);
+        }
+    }
+    
+    public List<RowLayout> getRows() { return rows; }
+    public void setRows(List<RowLayout> rows) { this.rows = rows; }
+    
+    public List<RowLayout> getSingleRows() { return singleRows; }
+    public void setSingleRows(List<RowLayout> singleRows) { this.singleRows = singleRows; }
+    
+    public List<RowLayout> getDualRows() { return dualRows; }
+    public void setDualRows(List<RowLayout> dualRows) { this.dualRows = dualRows; }
+    
+    /**
+     * 根据模式获取行配置
+     * @param isSingleMode true=单工位模式, false=双工位模式
+     */
+    public List<RowLayout> getRowsForMode(boolean isSingleMode) {
+        List<RowLayout> modeRows = isSingleMode ? singleRows : dualRows;
+        // 如果模式专用配置为空,回退到通用配置
+        return (modeRows != null && !modeRows.isEmpty()) ? modeRows : rows;
+    }
+    
+    /**
+     * 获取可见的行列表(按order排序)
+     */
+    public List<RowLayout> getVisibleRowsForMode(boolean isSingleMode) {
+        List<RowLayout> allRows = getRowsForMode(isSingleMode);
+        List<RowLayout> visibleRows = new ArrayList<>();
+        for (RowLayout row : allRows) {
+            if (row.isVisible()) {
+                visibleRows.add(row);
+            }
+        }
+        visibleRows.sort((a, b) -> Integer.compare(a.getOrder(), b.getOrder()));
+        return visibleRows;
+    }
+    
+    /**
+     * 设置行可见性(指定模式)
+     */
+    public void setRowVisibleForMode(String rowId, boolean visible, boolean isSingleMode) {
+        List<RowLayout> modeRows = getRowsForMode(isSingleMode);
+        for (RowLayout row : modeRows) {
+            if (row.getId() != null && row.getId().equals(rowId)) {
+                row.setVisible(visible);
+                return;
+            }
+        }
+    }
+    
+    public GlobalSettings getGlobalSettings() { return globalSettings; }
+    public void setGlobalSettings(GlobalSettings globalSettings) { this.globalSettings = globalSettings; }
+    
+    /**
+     * 是否为弹性布局模式
+     */
+    public boolean isFlexMode() {
+        return "flex".equalsIgnoreCase(layoutMode);
+    }
+    
+    /**
+     * 添加组件布局 (free模式)
+     */
+    public void addComponent(String id, ComponentLayout layout) {
+        components.put(id, layout);
+    }
+    
+    /**
+     * 获取组件布局 (free模式)
+     */
+    public ComponentLayout getComponent(String id) {
+        return components.get(id);
+    }
+    
+    /**
+     * 移除组件 (free模式)
+     */
+    public void removeComponent(String id) {
+        components.remove(id);
+    }
+    
+    /**
+     * 获取所有组件 (free模式)
+     */
+    public Collection<ComponentLayout> getAllComponents() {
+        return components.values();
+    }
+    
+    /**
+     * 获取行 (flex模式)
+     */
+    public RowLayout getRow(String id) {
+        for (RowLayout row : rows) {
+            if (row.getId() != null && row.getId().equals(id)) {
+                return row;
+            }
+        }
+        return null;
+    }
+    
+    /**
+     * 获取可见的行列表 (flex模式)
+     */
+    public List<RowLayout> getVisibleRows() {
+        List<RowLayout> visibleRows = new ArrayList<>();
+        for (RowLayout row : rows) {
+            if (row.isVisible()) {
+                visibleRows.add(row);
+            }
+        }
+        visibleRows.sort((a, b) -> Integer.compare(a.getOrder(), b.getOrder()));
+        return visibleRows;
+    }
+    
+    /**
+     * 设置行可见性 (flex模式)
+     */
+    public void setRowVisible(String rowId, boolean visible) {
+        RowLayout row = getRow(rowId);
+        if (row != null) {
+            row.setVisible(visible);
+        }
+    }
+    
+    /**
+     * 转换为YAML(不带类型标签)
+     */
+    public String toYaml() {
+        DumperOptions options = new DumperOptions();
+        options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
+        options.setPrettyFlow(true);
+        options.setIndent(2);
+        options.setIndicatorIndent(0);
+        
+        // 使用Representer跳过类型标签
+        Representer representer = new Representer(options);
+        representer.getPropertyUtils().setSkipMissingProperties(true);
+        
+        Yaml yaml = new Yaml(representer, options);
+        return yaml.dump(this);
+    }
+    
+    /**
+     * 从YAML加载(支持带/不带类型标签)
+     */
+    public static UILayoutConfig fromYaml(String yamlContent) {
+        // 移除可能存在的类型标签
+        String cleanYaml = yamlContent;
+        if (cleanYaml.startsWith("!!")) {
+            int newLineIndex = cleanYaml.indexOf('\n');
+            if (newLineIndex > 0) {
+                cleanYaml = cleanYaml.substring(newLineIndex + 1);
+            }
+        }
+        
+        // 配置加载选项,允许全局标签
+        LoaderOptions loaderOptions = new LoaderOptions();
+        loaderOptions.setAllowDuplicateKeys(false);
+        
+        Constructor constructor = new Constructor(UILayoutConfig.class, loaderOptions);
+        Yaml yaml = new Yaml(constructor);
+        
+        return yaml.load(cleanYaml);
+    }
+}

+ 43 - 0
src/com/mes/util/Base64Utils.java

@@ -0,0 +1,43 @@
+package com.mes.util;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+
+/**
+ * Base64编码工具类
+ */
+public class Base64Utils {
+    private static final Logger log = LoggerFactory.getLogger(Base64Utils.class);
+
+    /**
+     * 将字符串进行Base64编码
+     */
+    public static String encode(String str) {
+        if (str == null || str.isEmpty()) {
+            return "";
+        }
+        byte[] binaryData = str.getBytes(StandardCharsets.UTF_8);
+        return Base64.getEncoder().encodeToString(binaryData);
+    }
+
+    /**
+     * 将Base64编码的字符串解码
+     */
+    public static String decode(String base64Str) {
+        if (base64Str == null || base64Str.isEmpty()) {
+            return "";
+        }
+        byte[] decodedBytes = Base64.getDecoder().decode(base64Str);
+        return new String(decodedBytes, StandardCharsets.UTF_8);
+    }
+
+    /**
+     * 兼容旧接口
+     */
+    public static String getBase64(String str) {
+        return encode(str);
+    }
+}

+ 61 - 0
src/com/mes/util/DateLocalUtils.java

@@ -0,0 +1,61 @@
+package com.mes.util;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+
+/**
+ * 日期时间工具类
+ * 注意:日期格式必须与服务器协议一致(带分隔符)
+ */
+public class DateLocalUtils {
+
+    // TCP报文使用的格式(带分隔符,与OP150版本一致)
+    private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+    private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss");
+    private static final DateTimeFormatter DATETIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+
+    /**
+     * 获取当前日期字符串(yyyy-MM-dd)
+     * 用于TCP报文
+     */
+    public static String getCurrentDate() {
+        return LocalDate.now().format(DATE_FORMATTER);
+    }
+
+    /**
+     * 获取当前时间字符串(HH:mm:ss)
+     * 用于TCP报文
+     */
+    public static String getCurrentTimeHMS() {
+        return LocalDateTime.now().format(TIME_FORMATTER);
+    }
+
+    /**
+     * 获取当前日期时间字符串(yyyy-MM-dd HH:mm:ss)
+     */
+    public static String getCurrentDateTime() {
+        return LocalDateTime.now().format(DATETIME_FORMATTER);
+    }
+
+    /**
+     * 获取当前时间(完整格式,含毫秒)
+     */
+    public static String getCurrentTime() {
+        return LocalDateTime.now().format(DATETIME_FORMATTER);
+    }
+
+    /**
+     * 格式化日期
+     */
+    public static String formatDate(LocalDate date) {
+        return date.format(DATE_FORMATTER);
+    }
+
+    /**
+     * 格式化日期时间
+     */
+    public static String formatDateTime(LocalDateTime dateTime) {
+        return dateTime.format(DATETIME_FORMATTER);
+    }
+}

+ 202 - 0
src/com/mes/util/HttpUtils.java

@@ -0,0 +1,202 @@
+package com.mes.util;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.*;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * HTTP请求工具类
+ */
+public class HttpUtils {
+    private static final Logger log = LoggerFactory.getLogger(HttpUtils.class);
+    
+    private static final int DEFAULT_TIMEOUT = 5000;
+
+    /**
+     * 发送GET请求
+     */
+    public static String sendGet(String urlString) {
+        return sendGet(urlString, DEFAULT_TIMEOUT);
+    }
+
+    /**
+     * 发送GET请求
+     */
+    public static String sendGet(String urlString, int timeoutMs) {
+        HttpURLConnection conn = null;
+        try {
+            URL url = new URL(urlString);
+            conn = (HttpURLConnection) url.openConnection();
+            conn.setRequestMethod("GET");
+            conn.setConnectTimeout(timeoutMs);
+            conn.setReadTimeout(timeoutMs);
+            conn.setRequestProperty("Accept", "application/json");
+
+            int responseCode = conn.getResponseCode();
+            if (responseCode == HttpURLConnection.HTTP_OK) {
+                return readResponse(conn.getInputStream());
+            } else {
+                log.error("HTTP GET请求失败, 状态码: {}", responseCode);
+                return "false";
+            }
+        } catch (Exception e) {
+            log.error("HTTP GET请求异常: {}", e.getMessage());
+            return "false";
+        } finally {
+            if (conn != null) {
+                conn.disconnect();
+            }
+        }
+    }
+
+    /**
+     * 发送POST请求(URL参数形式)
+     */
+    public static String sendRequest(String urlParam) {
+        log.info("HTTP请求路径: {}", urlParam);
+        HttpURLConnection conn = null;
+        try {
+            URL url = new URL(urlParam);
+            conn = (HttpURLConnection) url.openConnection();
+            conn.setRequestMethod("POST");
+            conn.setRequestProperty("Content-Type", "application/json;charset=UTF-8");
+            conn.setDoOutput(true);
+            conn.setDoInput(true);
+            conn.setUseCaches(false);
+            conn.setConnectTimeout(DEFAULT_TIMEOUT);
+            conn.setReadTimeout(DEFAULT_TIMEOUT);
+
+            int responseCode = conn.getResponseCode();
+            if (responseCode == HttpURLConnection.HTTP_OK) {
+                String result = readResponse(conn.getInputStream());
+                log.info("HTTP响应结果: {}", result);
+                return result;
+            } else {
+                log.error("HTTP请求失败, 状态码: {}", responseCode);
+                return "false";
+            }
+        } catch (Exception e) {
+            log.error("HTTP请求异常: {}", e.getMessage());
+            return "false";
+        } finally {
+            if (conn != null) {
+                conn.disconnect();
+            }
+        }
+    }
+
+    /**
+     * 发送POST请求(带请求体)
+     */
+    public static String sendPost(String urlString, String params) {
+        log.info("HTTP POST请求: {}", urlString);
+        HttpURLConnection conn = null;
+        try {
+            URL url = new URL(urlString);
+            conn = (HttpURLConnection) url.openConnection();
+            conn.setRequestMethod("POST");
+            conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8");
+            conn.setDoOutput(true);
+            conn.setDoInput(true);
+            conn.setUseCaches(false);
+            conn.setConnectTimeout(DEFAULT_TIMEOUT);
+            conn.setReadTimeout(DEFAULT_TIMEOUT);
+
+            // 发送请求参数
+            try (OutputStreamWriter out = new OutputStreamWriter(conn.getOutputStream(), StandardCharsets.UTF_8)) {
+                out.write(params);
+                out.flush();
+            }
+
+            int responseCode = conn.getResponseCode();
+            if (responseCode == HttpURLConnection.HTTP_OK) {
+                String result = readResponse(conn.getInputStream());
+                log.info("HTTP响应结果: {}", result);
+                return result;
+            } else {
+                log.error("HTTP请求失败, 状态码: {}", responseCode);
+                return "false";
+            }
+        } catch (Exception e) {
+            log.error("HTTP请求异常: {}", e.getMessage());
+            return "false";
+        } finally {
+            if (conn != null) {
+                conn.disconnect();
+            }
+        }
+    }
+
+    /**
+     * 发送JSON格式的POST请求
+     */
+    public static String sendJsonPost(String urlString, String jsonInput) {
+        return sendJsonPost(urlString, jsonInput, DEFAULT_TIMEOUT);
+    }
+
+    /**
+     * 发送JSON格式的POST请求
+     */
+    public static String sendJsonPost(String urlString, String jsonInput, int timeoutMs) {
+        log.info("HTTP JSON POST请求: {}", urlString);
+        HttpURLConnection conn = null;
+        try {
+            URL url = new URL(urlString);
+            conn = (HttpURLConnection) url.openConnection();
+            conn.setRequestMethod("POST");
+            conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
+            conn.setRequestProperty("Accept", "application/json");
+            conn.setConnectTimeout(timeoutMs);
+            conn.setReadTimeout(timeoutMs);
+            conn.setDoOutput(true);
+
+            // 发送JSON数据
+            try (OutputStream os = conn.getOutputStream()) {
+                byte[] input = jsonInput.getBytes(StandardCharsets.UTF_8);
+                os.write(input, 0, input.length);
+            }
+
+            int responseCode = conn.getResponseCode();
+            if (responseCode >= 200 && responseCode < 300) {
+                String result = readResponse(conn.getInputStream());
+                log.info("HTTP响应结果: {}", result);
+                return result;
+            } else {
+                // 尝试读取错误信息
+                InputStream errorStream = conn.getErrorStream();
+                if (errorStream != null) {
+                    String error = readResponse(errorStream);
+                    log.error("HTTP请求失败, 状态码: {}, 错误: {}", responseCode, error);
+                } else {
+                    log.error("HTTP请求失败, 状态码: {}", responseCode);
+                }
+                return null;
+            }
+        } catch (Exception e) {
+            log.error("HTTP请求异常: {}", e.getMessage());
+            return null;
+        } finally {
+            if (conn != null) {
+                conn.disconnect();
+            }
+        }
+    }
+
+    /**
+     * 读取响应内容
+     */
+    private static String readResponse(InputStream inputStream) throws IOException {
+        StringBuilder result = new StringBuilder();
+        try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
+            String line;
+            while ((line = reader.readLine()) != null) {
+                result.append(line);
+            }
+        }
+        return result.toString();
+    }
+}

+ 2 - 0
src/resources/image/.gitignore

@@ -0,0 +1,2 @@
+META-INF
+_system~.ini

BIN
src/resources/image/background.png


BIN
src/resources/image/bg/a_side.png


BIN
src/resources/image/bg/b_side.png


BIN
src/resources/image/bg/bar_add.png


BIN
src/resources/image/bg/bar_bookmark.png


BIN
src/resources/image/bg/bar_cmd_go.png


BIN
src/resources/image/bg/bar_edit.png


BIN
src/resources/image/bg/bar_pause.png


BIN
src/resources/image/bg/bar_sampling.png


BIN
src/resources/image/bg/bar_setting.png


BIN
src/resources/image/bg/bar_stop.png


BIN
src/resources/image/bg/close_bt.png


BIN
src/resources/image/bg/company_setting_logo.png


BIN
src/resources/image/bg/delete.png


BIN
src/resources/image/bg/department_setting_logo.png


BIN
src/resources/image/bg/download.png


BIN
src/resources/image/bg/equipment_setting_logo.png


BIN
src/resources/image/bg/gj_001_1_20.png


BIN
src/resources/image/bg/gj_001_21_40.png


+ 0 - 0
src/resources/image/bg/gj_001_41_60.png


Some files were not shown because too many files changed in this diff