From 19f59c243e8df97c8b9fd9dba4e758be8235d68b Mon Sep 17 00:00:00 2001
From: huang <1532065656@qq.com>
Date: 星期二, 25 十一月 2025 17:02:54 +0800
Subject: [PATCH] 添加卧转立扫码、卧转立、大车、大理片笼基础任务流转逻辑
---
mes-web/src/views/plcTest/components/MultiDeviceTest/ResultAnalysis.vue | 638 ++++
mes-processes/mes-plcSend/ARCHITECTURE.md | 536 +++
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/largeglass/config/LargeGlassConfig.java | 37
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/vehicle/coordination/VehicleCoordinationService.java | 189 +
mes-processes/mes-plcSend/src/main/java/com/mes/device/vo/DevicePlcVO.java | 3
mes-web/src/utils/constants.js | 2
mes-processes/mes-plcSend/README.md | 645 ++++
mes-processes/mes-plcSend/src/main/java/com/mes/device/service/impl/DeviceCoordinationServiceImpl.java | 10
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/vehicle/handler/LoadVehicleLogicHandler.java | 1719 +++++++++++
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/vehicle/model/VehicleTask.java | 81
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/vehicle/flow/LoadVehicleInteraction.java | 225 +
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/vehicle/model/VehiclePosition.java | 63
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/vehicle/model/VehicleStatus.java | 113
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/workstation/base/WorkstationBaseHandler.java | 80
mes-processes/mes-plcSend/src/main/java/com/mes/config/TaskExecutorConfig.java | 63
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/workstation/config/WorkstationLogicConfig.java | 32
mes-processes/mes-plcSend/src/main/java/com/mes/s7/provider/S7SerializerProvider.java | 90
mes-web/src/views/device/DeviceEditDialog.vue | 385 --
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/DeviceLogicHandler.java | 2
mes-web/src/views/device/components/DeviceLogicConfig/WorkstationScannerConfig.vue | 84
mes-web/src/views/device/components/DeviceLogicConfig/index.js | 34
mes-processes/mes-plcSend/src/main/java/com/mes/device/service/impl/DeviceConfigServiceImpl.java | 48
mes-web/src/views/plcTest/components/MultiDeviceTest/ExecutionMonitor.vue | 482 +++
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/DeviceInteractionRegistry.java | 1
mes-web/src/views/plcTest/MultiDeviceWorkbench.vue | 105
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/largeglass/model/GridRange.java | 30
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/workstation/transfer/handler/HorizontalTransferLogicHandler.java | 481 +++
mes-web/src/views/device/DeviceConfigList.vue | 4
mes-processes/mes-plcSend/src/main/java/com/mes/device/service/impl/DevicePlcOperationServiceImpl.java | 20
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/flow/GlassStorageInteraction.java | 2
mes-web/src/views/plcTest/components/DeviceGroup/GroupTopology.vue | 478 +++
/dev/null | 451 ---
mes-processes/mes-plcSend/src/main/java/com/mes/task/service/impl/MultiDeviceTaskServiceImpl.java | 42
mes-web/src/views/device/components/DeviceLogicConfig/LargeGlassConfig.vue | 177 +
mes-web/src/views/device/components/DeviceLogicConfig/LoadVehicleConfig.vue | 324 ++
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/vehicle/model/VehiclePath.java | 91
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/workstation/scanner/handler/HorizontalScannerLogicHandler.java | 161 +
mes-processes/mes-plcSend/src/main/java/com/mes/task/service/TaskExecutionEngine.java | 2
mes-processes/mes-plcSend/src/main/java/com/mes/device/service/DevicePlcOperationService.java | 2
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/flow/LargeGlassInteraction.java | 4
mes-processes/mes-plcSend/src/main/java/com/mes/task/controller/TaskStatusNotificationController.java | 11
mes-processes/mes-plcSend/src/main/java/com/mes/device/entity/DeviceConfig.java | 16
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/vehicle/coordination/VehicleStatusManager.java | 199 +
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/vehicle/model/VehicleState.java | 30
mes-web/src/views/device/components/DeviceLogicConfig/WorkstationTransferConfig.vue | 146
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/largeglass/handler/LargeGlassLogicHandler.java | 379 ++
mes-web/src/views/plcTest/components/MultiDeviceTest/TaskOrchestration.vue | 199 +
47 files changed, 8,066 insertions(+), 850 deletions(-)
diff --git a/mes-processes/mes-plcSend/ARCHITECTURE.md b/mes-processes/mes-plcSend/ARCHITECTURE.md
new file mode 100644
index 0000000..4d41c1d
--- /dev/null
+++ b/mes-processes/mes-plcSend/ARCHITECTURE.md
@@ -0,0 +1,536 @@
+# 椤圭洰鏋舵瀯璇存槑鏂囨。
+
+## 馃搵 椤圭洰涓讳綋鏋舵瀯
+
+### 鏍稿績涓讳綋锛歍askExecutionEngine锛堜换鍔℃墽琛屽紩鎿庯級
+
+**浣嶇疆**锛歚com.mes.task.service.TaskExecutionEngine`
+
+**鑱岃矗**锛氬璁惧浠诲姟鎵ц鐨勬牳蹇冨紩鎿庯紝璐熻矗锛�
+- 涓茶/骞惰鎵ц妯″紡鎺у埗
+- 璁惧姝ラ鎵ц璋冨害
+- 璁惧鍗忚皟妫�鏌�
+- 閿欒澶勭悊鍜岄噸璇�
+- 瀹炴椂鐘舵�侀�氱煡
+
+## 馃攧 璋冪敤娴佺▼
+
+### 1. 浠诲姟鍚姩娴佺▼
+
+```
+鐢ㄦ埛璇锋眰 (Controller)
+ 鈫�
+MultiDeviceTaskServiceImpl.startTask()
+ 鈹溾攢 楠岃瘉璁惧缁�
+ 鈹溾攢 鑾峰彇璁惧鍒楄〃
+ 鈹溾攢 鍒涘缓浠诲姟璁板綍 (PENDING)
+ 鈹斺攢 寮傛鎵ц executeTaskAsync()
+ 鈫�
+ TaskExecutionEngine.execute()
+ 鈹溾攢 璁惧鍗忚皟妫�鏌� (DeviceCoordinationService)
+ 鈹溾攢 纭畾鎵ц妯″紡 (涓茶/骞惰)
+ 鈹斺攢 鎵ц璁惧姝ラ
+```
+
+### 2. 璁惧姝ラ鎵ц娴佺▼
+
+```
+TaskExecutionEngine.executeStep()
+ 鈫�
+妫�鏌ユ槸鍚︽湁 DeviceInteraction
+ 鈹溾攢 鏈� 鈫� executeInteractionStepWithRetry()
+ 鈹� 鈫�
+ 鈹� DeviceInteraction.execute(InteractionContext)
+ 鈹� 鈫�
+ 鈹� 鍏蜂綋璁惧浜や簰瀹炵幇锛堝 LoadVehicleInteraction锛�
+ 鈹� 鈫�
+ 鈹� 璋冪敤 DeviceInteractionService
+ 鈹� 鈫�
+ 鈹� 鏈�缁堣皟鐢� DeviceLogicHandler
+ 鈹�
+ 鈹斺攢 鏃� 鈫� 鐩存帴璋冪敤 DeviceLogicHandler
+ 鈫�
+ DeviceLogicHandlerFactory.getHandler(deviceType)
+ 鈫�
+ BaseDeviceLogicHandler.execute()
+ 鈫�
+ 瀛愮被瀹炵幇 doExecute()
+```
+
+### 3. 涓ょ鎵ц璺緞
+
+#### 璺緞A锛氶�氳繃 DeviceInteraction锛堟帹鑽愮敤浜庡鏉傝澶囷級
+
+```
+TaskExecutionEngine
+ 鈫�
+DeviceInteraction.execute(InteractionContext)
+ 鈫�
+鍏蜂綋瀹炵幇绫伙紙濡� LoadVehicleInteraction锛�
+ 鈹溾攢 鐘舵�佹鏌�
+ 鈹溾攢 璁惧閫夋嫨锛堝瀹炰緥鍗忚皟锛�
+ 鈹溾攢 鏁版嵁鍑嗗
+ 鈹斺攢 璋冪敤 DeviceInteractionService
+ 鈫�
+ DeviceLogicHandler.execute()
+```
+
+**閫傜敤鍦烘櫙**锛�
+- 闇�瑕佸瀹炰緥鍗忚皟鐨勮澶囷紙濡傚ぇ杞﹁澶囷級
+- 闇�瑕佸鏉傚墠缃鏌ョ殑璁惧
+- 闇�瑕佹暟鎹浆鎹㈠拰鍑嗗鐨勮澶�
+
+#### 璺緞B锛氱洿鎺ヨ皟鐢� DeviceLogicHandler锛堢畝鍗曡澶囷級
+
+```
+TaskExecutionEngine
+ 鈫�
+DeviceLogicHandlerFactory.getHandler(deviceType)
+ 鈫�
+BaseDeviceLogicHandler.execute()
+ 鈹溾攢 鍙傛暟瑙f瀽
+ 鈹溾攢 閫昏緫鍙傛暟鎻愬彇 (extraParams.deviceLogic)
+ 鈹斺攢 瀛愮被瀹炵幇 doExecute()
+```
+
+**閫傜敤鍦烘櫙**锛�
+- 绠�鍗曡澶囷紝閫昏緫鍗曚竴
+- 涓嶉渶瑕佸鏉傚崗璋冪殑璁惧
+
+## 馃彈锔� 鍒嗗眰鏋舵瀯
+
+### 绗竴灞傦細Controller 灞傦紙API鍏ュ彛锛�
+
+**浣嶇疆**锛歚com.mes.task.controller.*`
+
+**鑱岃矗**锛�
+- 鎺ユ敹HTTP璇锋眰
+- 鍙傛暟楠岃瘉
+- 璋冪敤Service灞�
+
+**涓昏绫�**锛�
+- `MultiDeviceTaskController` - 浠诲姟绠$悊API
+- `TaskStatusNotificationController` - SSE瀹炴椂閫氱煡API
+
+### 绗簩灞傦細Service 灞傦紙涓氬姟閫昏緫锛�
+
+**浣嶇疆**锛歚com.mes.task.service.*`
+
+**鑱岃矗**锛�
+- 涓氬姟閫昏緫澶勭悊
+- 浜嬪姟绠$悊
+- 璋冪敤鎵ц寮曟搸
+
+**涓昏绫�**锛�
+- `MultiDeviceTaskServiceImpl` - 浠诲姟鏈嶅姟瀹炵幇
+ - 鍒涘缓浠诲姟璁板綍
+ - 寮傛鎵ц浠诲姟
+ - 浠诲姟鐘舵�佺鐞�
+
+- `TaskExecutionEngine` - **鏍稿績鎵ц寮曟搸**
+ - 璁惧鍗忚皟妫�鏌�
+ - 鎵ц妯″紡鍒ゆ柇锛堜覆琛�/骞惰锛�
+ - 姝ラ鎵ц璋冨害
+ - 閲嶈瘯鏈哄埗
+
+### 绗笁灞傦細Interaction 灞傦紙璁惧浜や簰锛�
+
+**浣嶇疆**锛歚com.mes.interaction.*`
+
+**鑱岃矗**锛�
+- 璁惧浜や簰閫昏緫灏佽
+- 澶氳澶囧崗璋�
+- 鏁版嵁浼犻��
+
+**涓昏缁勪欢**锛�
+
+#### 3.1 DeviceInteraction锛堣澶囦氦浜掓帴鍙o級
+
+**鎺ュ彛**锛歚com.mes.interaction.DeviceInteraction`
+
+**瀹炵幇绫荤ず渚�**锛�
+- `LoadVehicleInteraction` - 澶ц溅璁惧浜や簰
+- `LargeGlassInteraction` - 澶х悊鐗囩浜や簰
+- `GlassStorageInteraction` - 鐜荤拑瀛樺偍浜や簰
+
+**娉ㄥ唽鏈哄埗**锛�
+- `DeviceInteractionRegistry` - 鑷姩娉ㄥ唽鎵�鏈� `@Component` 鐨� `DeviceInteraction` 瀹炵幇
+
+#### 3.2 DeviceLogicHandler锛堣澶囬�昏緫澶勭悊鍣級
+
+**鎺ュ彛**锛歚com.mes.interaction.DeviceLogicHandler`
+
+**鍩虹被**锛歚BaseDeviceLogicHandler`
+- 鎻愪緵閫氱敤鍔熻兘锛�
+ - 鍙傛暟瑙f瀽
+ - 閫昏緫鍙傛暟鎻愬彇锛堜粠 `extraParams.deviceLogic`锛�
+ - 閿欒澶勭悊
+
+**瀹炵幇绫荤ず渚�**锛�
+- `LoadVehicleLogicHandler` - 澶ц溅璁惧閫昏緫
+- `HorizontalScannerLogicHandler` - 鍗ц浆绔嬫壂鐮侀�昏緫
+- `HorizontalTransferLogicHandler` - 鍗ц浆绔嬮�昏緫
+- `LargeGlassLogicHandler` - 澶х悊鐗囩閫昏緫
+
+**娉ㄥ唽鏈哄埗**锛�
+- `DeviceLogicHandlerFactory` - 鑷姩娉ㄥ唽鎵�鏈� `DeviceLogicHandler` 瀹炵幇
+- 閫氳繃 `@PostConstruct` 鍒濆鍖栨槧灏勮〃
+
+### 绗洓灞傦細Coordination 灞傦紙璁惧鍗忚皟锛�
+
+**浣嶇疆**锛歚com.mes.device.service.*` 鍜� `com.mes.interaction.*.coordination.*`
+
+**鑱岃矗**锛�
+- 璁惧闂存暟鎹紶閫�
+- 璁惧鐘舵�佸悓姝�
+- 璁惧渚濊禆绠$悊
+- 澶氬疄渚嬪崗璋�
+
+**涓昏绫�**锛�
+- `DeviceCoordinationService` - 閫氱敤璁惧鍗忚皟鏈嶅姟
+- `VehicleCoordinationService` - 澶ц溅璁惧鍗忚皟鏈嶅姟
+- `VehicleStatusManager` - 澶ц溅鐘舵�佺鐞嗗櫒
+
+### 绗簲灞傦細PLC 鎿嶄綔灞傦紙纭欢閫氫俊锛�
+
+**浣嶇疆**锛歚com.mes.device.service.DevicePlcOperationService`
+
+**鑱岃矗**锛�
+- PLC璇诲啓鎿嶄綔
+- 鍦板潃鏄犲皠
+- 閫氫俊绠$悊
+
+## 馃摝 濡備綍娣诲姞鏂拌澶�
+
+### 姝ラ1锛氬畾涔夎澶囩被鍨嬪父閲�
+
+鍦� `DeviceConfig.DeviceType` 涓坊鍔狅細
+
+```java
+public static final class DeviceType {
+ public static final String NEW_DEVICE = "鏂拌澶囩被鍨�";
+}
+```
+
+### 姝ラ2锛氬垱寤鸿澶囬�昏緫澶勭悊鍣紙蹇呴』锛�
+
+**浣嶇疆**锛歚com.mes.interaction.*.handler.NewDeviceLogicHandler`
+
+```java
+@Component
+public class NewDeviceLogicHandler extends BaseDeviceLogicHandler {
+
+ @Override
+ public String getDeviceType() {
+ return DeviceConfig.DeviceType.NEW_DEVICE;
+ }
+
+ @Override
+ protected DevicePlcVO.OperationResult doExecute(
+ DeviceConfig deviceConfig,
+ String operation,
+ Map<String, Object> params,
+ Map<String, Object> logicParams) {
+
+ // 1. 浠� logicParams 涓幏鍙栭厤缃弬鏁�
+ Integer timeout = getLogicParam(logicParams, "timeout", 5000);
+ String mode = getLogicParam(logicParams, "mode", "default");
+
+ // 2. 浠� params 涓幏鍙栬繍琛屾椂鍙傛暟
+ String glassId = (String) params.get("glassId");
+
+ // 3. 鎵ц璁惧閫昏緫
+ // ... 鍏蜂綋瀹炵幇
+
+ // 4. 璋冪敤PLC鎿嶄綔
+ Map<String, Object> plcParams = new HashMap<>();
+ plcParams.put("field1", value1);
+ DevicePlcVO.OperationResult result =
+ devicePlcOperationService.writePlcData(deviceConfig, plcParams);
+
+ return result;
+ }
+
+ @Override
+ protected Map<String, Object> getDefaultLogicParams() {
+ Map<String, Object> defaults = new HashMap<>();
+ defaults.put("timeout", 5000);
+ defaults.put("mode", "default");
+ return defaults;
+ }
+}
+```
+
+**鑷姩娉ㄥ唽**锛�
+- 瀹炵幇 `DeviceLogicHandler` 鎺ュ彛
+- 娣诲姞 `@Component` 娉ㄨВ
+- `DeviceLogicHandlerFactory` 浼氳嚜鍔ㄥ彂鐜板苟娉ㄥ唽
+
+### 姝ラ3锛氬垱寤鸿澶囦氦浜掔被锛堝彲閫夛紝澶嶆潅璁惧鎺ㄨ崘锛�
+
+**浣嶇疆**锛歚com.mes.interaction.*.flow.NewDeviceInteraction`
+
+```java
+@Component
+public class NewDeviceInteraction implements DeviceInteraction {
+
+ @Override
+ public String getDeviceType() {
+ return DeviceConfig.DeviceType.NEW_DEVICE;
+ }
+
+ @Override
+ public InteractionResult execute(InteractionContext context) {
+ DeviceConfig device = context.getCurrentDevice();
+
+ // 1. 鍓嶇疆鏉′欢妫�鏌�
+ if (device == null) {
+ return InteractionResult.fail("璁惧閰嶇疆涓嶅瓨鍦�");
+ }
+
+ // 2. 鏁版嵁鍑嗗
+ List<String> glassIds = context.getParameters().getGlassIds();
+
+ // 3. 璋冪敤璁惧閫昏緫澶勭悊鍣�
+ DeviceInteractionService service = ...;
+ Map<String, Object> params = new HashMap<>();
+ params.put("glassIds", glassIds);
+
+ DevicePlcVO.OperationResult result =
+ service.executeDeviceOperation(device, "operationName", params);
+
+ if (result.isSuccess()) {
+ // 4. 鏁版嵁浼犻�掑埌涓嬩竴涓澶�
+ context.getSharedData().put("processedGlasses", glassIds);
+ return InteractionResult.success("鎵ц鎴愬姛", result.getData());
+ } else {
+ return InteractionResult.fail(result.getMessage());
+ }
+ }
+}
+```
+
+**鑷姩娉ㄥ唽**锛�
+- 瀹炵幇 `DeviceInteraction` 鎺ュ彛
+- 娣诲姞 `@Component` 娉ㄨВ
+- `DeviceInteractionRegistry` 浼氳嚜鍔ㄥ彂鐜板苟娉ㄥ唽
+
+### 姝ラ4锛氶厤缃澶囬�昏緫鍙傛暟锛堝墠绔級
+
+鍦ㄨ澶囬厤缃殑 `extraParams.deviceLogic` 涓厤缃細
+
+```json
+{
+ "deviceLogic": {
+ "timeout": 5000,
+ "mode": "default",
+ "customParam1": "value1"
+ }
+}
+```
+
+### 姝ラ5锛氳澶囩粍缁囨柟寮忛�夋嫨
+
+#### 绠�鍗曡澶囷紙鎺ㄨ崘鏀惧湪閫氱敤鍖咃級
+
+```
+interaction/
+鈹斺攢鈹� impl/
+ 鈹斺攢鈹� NewDeviceLogicHandler.java
+```
+
+#### 澶嶆潅璁惧锛堥渶瑕佸崗璋冦�佺姸鎬佺鐞嗙瓑锛�
+
+```
+interaction/
+鈹斺攢鈹� newdevice/
+ 鈹溾攢鈹� handler/
+ 鈹� 鈹斺攢鈹� NewDeviceLogicHandler.java
+ 鈹溾攢鈹� flow/
+ 鈹� 鈹斺攢鈹� NewDeviceInteraction.java
+ 鈹溾攢鈹� coordination/ (鍙��)
+ 鈹� 鈹斺攢鈹� NewDeviceCoordinationService.java
+ 鈹斺攢鈹� model/ (鍙��)
+ 鈹斺攢鈹� NewDeviceStatus.java
+```
+
+## 馃攳 鎵ц娴佺▼璇﹁В
+
+### 涓茶鎵ц妯″紡
+
+```
+TaskExecutionEngine.execute()
+ 鈫�
+for (姣忎釜璁惧) {
+ 1. 鍒涘缓姝ラ璁板綍 (TaskStepDetail)
+ 2. executeStep()
+ 鈫�
+ 3. 妫�鏌ユ槸鍚︽湁 DeviceInteraction
+ 鈹溾攢 鏈� 鈫� DeviceInteraction.execute()
+ 鈹� 鈫�
+ 鈹� 璋冪敤 DeviceLogicHandler
+ 鈹�
+ 鈹斺攢 鏃� 鈫� 鐩存帴璋冪敤 DeviceLogicHandler
+ 鈫�
+ BaseDeviceLogicHandler.execute()
+ 鈫�
+ 瀛愮被 doExecute()
+ 4. 鏇存柊姝ラ鐘舵��
+ 5. 浼犻�掓暟鎹埌涓嬩竴涓澶�
+}
+```
+
+### 骞惰鎵ц妯″紡
+
+```
+TaskExecutionEngine.execute()
+ 鈫�
+鍒涘缓绾跨▼姹犱换鍔″垪琛�
+ 鈫�
+for (姣忎釜璁惧) {
+ 鎻愪氦鍒扮嚎绋嬫睜鎵ц
+ 鈫�
+ executeStep() (鍚屼笂)
+}
+ 鈫�
+绛夊緟鎵�鏈変换鍔″畬鎴�
+ 鈫�
+姹囨�荤粨鏋�
+```
+
+## 馃摑 鍏抽敭鎺ュ彛璇存槑
+
+### DeviceLogicHandler
+
+**鎺ュ彛鏂规硶**锛�
+```java
+DevicePlcVO.OperationResult execute(
+ DeviceConfig deviceConfig,
+ String operation,
+ Map<String, Object> params
+);
+```
+
+**璋冪敤鏃舵満**锛�
+- 浠诲姟鎵ц寮曟搸鐩存帴璋冪敤
+- 鎴栭�氳繃 DeviceInteraction 闂存帴璋冪敤
+
+### DeviceInteraction
+
+**鎺ュ彛鏂规硶**锛�
+```java
+InteractionResult execute(InteractionContext context);
+```
+
+**璋冪敤鏃舵満**锛�
+- 浠诲姟鎵ц寮曟搸浼樺厛妫�鏌ユ槸鍚︽湁 DeviceInteraction
+- 濡傛灉鏈夛紝浼樺厛浣跨敤 DeviceInteraction
+- 濡傛灉娌℃湁锛岀洿鎺ヨ皟鐢� DeviceLogicHandler
+
+### InteractionContext
+
+**鍖呭惈鍐呭**锛�
+- `currentDevice` - 褰撳墠璁惧閰嶇疆
+- `taskContext` - 浠诲姟涓婁笅鏂�
+- `parameters` - 浠诲姟鍙傛暟
+- `sharedData` - 璁惧闂村叡浜暟鎹�
+
+## 馃幆 鏈�浣冲疄璺�
+
+### 1. 浣曟椂浣跨敤 DeviceInteraction锛�
+
+**浣跨敤鍦烘櫙**锛�
+- 鉁� 闇�瑕佸瀹炰緥鍗忚皟锛堝澶ц溅璁惧锛�
+- 鉁� 闇�瑕佸鏉傚墠缃鏌�
+- 鉁� 闇�瑕佹暟鎹浆鎹㈠拰鍑嗗
+- 鉁� 闇�瑕佺姸鎬佺鐞�
+
+**涓嶄娇鐢ㄥ満鏅�**锛�
+- 鉂� 绠�鍗曡澶囷紝閫昏緫鍗曚竴
+- 鉂� 涓嶉渶瑕佸崗璋冪殑璁惧
+
+### 2. 鍙傛暟璁捐
+
+**logicParams锛堥�昏緫鍙傛暟锛�**锛�
+- 浠� `extraParams.deviceLogic` 涓鍙�
+- 璁惧閰嶇疆鏃惰缃紝杩愯鏃朵笉鍙�
+- 濡傦細瓒呮椂鏃堕棿銆佹ā寮忋�佸閲忕瓑
+
+**params锛堣繍琛屾椂鍙傛暟锛�**锛�
+- 浠诲姟鎵ц鏃跺姩鎬佷紶鍏�
+- 濡傦細鐜荤拑ID銆佷綅缃�佹暟閲忕瓑
+
+### 3. 閿欒澶勭悊
+
+**鍦� BaseDeviceLogicHandler 涓�**锛�
+- 鑷姩鎹曡幏寮傚父
+- 杩斿洖缁熶竴鐨勯敊璇牸寮�
+
+**鍦ㄥ瓙绫讳腑**锛�
+- 鍙互鎶涘嚭涓氬姟寮傚父
+- 浼氳鍩虹被鎹曡幏骞惰浆鎹�
+
+### 4. 鏁版嵁浼犻��
+
+**閫氳繃 InteractionContext.sharedData**锛�
+```java
+// 鍦ㄨ澶嘇涓缃�
+context.getSharedData().put("glassIds", glassIds);
+
+// 鍦ㄨ澶嘊涓幏鍙�
+List<String> glassIds = (List<String>) context.getSharedData().get("glassIds");
+```
+
+## 馃摎 鐩稿叧鏂囦欢浣嶇疆
+
+### 鏍稿績鏂囦欢
+- `TaskExecutionEngine.java` - 浠诲姟鎵ц寮曟搸
+- `MultiDeviceTaskServiceImpl.java` - 浠诲姟鏈嶅姟
+- `DeviceLogicHandlerFactory.java` - 澶勭悊鍣ㄥ伐鍘�
+- `DeviceInteractionRegistry.java` - 浜や簰娉ㄥ唽涓績
+
+### 鍩虹被
+- `BaseDeviceLogicHandler.java` - 閫昏緫澶勭悊鍣ㄥ熀绫�
+- `DeviceInteraction.java` - 浜や簰鎺ュ彛
+
+### 绀轰緥瀹炵幇
+- `LoadVehicleLogicHandler.java` - 澶ц溅璁惧閫昏緫澶勭悊鍣�
+- `LoadVehicleInteraction.java` - 澶ц溅璁惧浜や簰
+- `HorizontalScannerLogicHandler.java` - 鍗ц浆绔嬫壂鐮佸鐞嗗櫒
+
+## 馃敡 璋冭瘯鎶�宸�
+
+### 1. 鏌ョ湅娉ㄥ唽鐨勮澶囧鐞嗗櫒
+
+```java
+@Autowired
+private DeviceLogicHandlerFactory factory;
+
+// 鏌ョ湅鎵�鏈夊凡娉ㄥ唽鐨勮澶囩被鍨�
+Set<String> types = factory.getSupportedDeviceTypes();
+```
+
+### 2. 鏌ョ湅娉ㄥ唽鐨勮澶囦氦浜�
+
+```java
+@Autowired
+private DeviceInteractionRegistry registry;
+
+// 鏌ョ湅鎵�鏈夊凡娉ㄥ唽鐨勪氦浜�
+Map<String, DeviceInteraction> interactions = registry.getInteractions();
+```
+
+### 3. 鏃ュ織杈撳嚭
+
+- 浠诲姟鎵ц锛氭煡鐪� `TaskExecutionEngine` 鏃ュ織
+- 璁惧鎵ц锛氭煡鐪嬪叿浣� Handler 鏃ュ織
+- 鍗忚皟鏈嶅姟锛氭煡鐪� `DeviceCoordinationService` 鏃ュ織
+
+## 鉁� 鎬荤粨
+
+1. **椤圭洰涓讳綋**锛歚TaskExecutionEngine` 鏄牳蹇冩墽琛屽紩鎿�
+2. **璋冪敤娴佺▼**锛欳ontroller 鈫� Service 鈫� Engine 鈫� Interaction/Handler 鈫� PLC
+3. **鍒嗗眰鏋舵瀯**锛欳ontroller 鈫� Service 鈫� Interaction 鈫� Coordination 鈫� PLC
+4. **娣诲姞璁惧**锛氬疄鐜� `DeviceLogicHandler`锛堝繀椤伙級+ `DeviceInteraction`锛堝彲閫夛級
+5. **鑷姩娉ㄥ唽**锛氶�氳繃 Spring 鐨� `@Component` 鍜屽伐鍘傜被鑷姩鍙戠幇鍜屾敞鍐�
+
diff --git a/mes-processes/mes-plcSend/README.md b/mes-processes/mes-plcSend/README.md
new file mode 100644
index 0000000..0e37557
--- /dev/null
+++ b/mes-processes/mes-plcSend/README.md
@@ -0,0 +1,645 @@
+# MES PLC Send 椤圭洰鏂囨。
+
+## 馃搵 椤圭洰姒傝堪
+
+MES PLC Send 鏄竴涓熀浜� Spring Boot 鐨勫璁惧鑱斿悎娴嬭瘯绯荤粺锛屾敮鎸� PLC 璁惧绠$悊銆佽澶囩粍閰嶇疆銆佸璁惧浠诲姟缂栨帓鍜屾墽琛屻�傜郴缁熷疄鐜颁簡"妯℃澘 + 瀹炰緥"鐨勮璁℃ā寮忥紝鏀寔涓�涓澶囩被鍨嬫ā鏉垮搴斿涓澶囧疄渚嬶紝瀹炵幇浜嗚澶囬棿鐨勫崗璋冨拰鏁版嵁浼犻�掋��
+
+**鎶�鏈爤**锛�
+- Spring Boot 2.x
+- MyBatis-Plus
+- S7NetPlus锛圥LC閫氫俊锛�
+- MySQL
+- Server-Sent Events (SSE) 瀹炴椂鎺ㄩ��
+
+**鏈嶅姟绔彛**锛�10018
+
+## 馃幆 鏍稿績鍔熻兘
+
+### 1. 璁惧绠$悊
+- 璁惧閰嶇疆绠$悊锛圥LC IP銆佽澶囩被鍨嬨�佹ā鍧楀悕绉扮瓑锛�
+- 璁惧鐘舵�佺洃鎺э紙鍦ㄧ嚎/绂荤嚎/蹇欑/閿欒/缁存姢涓級
+- 鏀寔5绉嶈澶囩被鍨嬶細
+ - **澶ц溅璁惧** (`LOAD_VEHICLE`)锛氭敮鎸佸瀹炰緥鍗忚皟锛岃嚜鍔ㄧ姸鎬佺鐞嗭紝MES浠诲姟澶勭悊
+ - **澶х悊鐗囩** (`LARGE_GLASS`)锛氭牸瀛愯寖鍥撮厤缃紝閫昏緫鍒ゆ柇
+ - **鍗ц浆绔嬫壂鐮�** (`WORKSTATION_SCANNER`)锛氬畾鏃舵壂鎻忥紝MES鏁版嵁璇诲彇
+ - **鍗ц浆绔�** (`WORKSTATION_TRANSFER`)锛�30s缂撳啿鍒ゅ畾锛屾壒閲忓鐞�
+ - **鍗у紡缂撳瓨** (`GLASS_STORAGE`)锛氱幓鐠冨瓨鍌ㄧ鐞嗭紙宸插疄鐜帮紝浣嗗綋鍓嶄笉浣跨敤锛�
+
+### 2. 璁惧缁勭鐞�
+- 璁惧缁勯厤缃紙鏀寔鏈�澶у苟鍙戣澶囨暟鎺у埗锛�
+- 璁惧缁勬嫇鎵戝彲瑙嗗寲
+- 璁惧渚濊禆鍏崇郴绠$悊锛堜紭鍏堢骇銆佽鑹层�佽繛鎺ラ『搴忥級
+- 璁惧缁勪换鍔$紪鎺�
+
+### 3. 澶氳澶囦换鍔℃墽琛�
+- **涓茶鎵ц**锛氭寜璁惧杩炴帴椤哄簭鎵ц
+- **骞惰鎵ц**锛氭敮鎸佸璁惧骞惰鎵ц锛岄�氳繃 `max_concurrent_devices` 鎺у埗骞跺彂鏁�
+- **瀹炴椂鐩戞帶**锛氬熀浜� SSE锛圫erver-Sent Events锛夌殑瀹炴椂鐘舵�佹帹閫�
+- **閿欒澶勭悊**锛氳嚜鍔ㄩ噸璇曟満鍒讹紝鏀寔鎸囨暟閫�閬�
+- **鏁版嵁浼犻��**锛氳澶囬棿鏁版嵁鍏变韩鍜岀姸鎬佸悓姝�
+
+### 4. 璁惧鍗忚皟鏈嶅姟
+- 璁惧闂存暟鎹紶閫掓満鍒�
+- 璁惧鐘舵�佸悓姝�
+- 璁惧渚濊禆绠$悊
+- 澶氬疄渚嬪崗璋冿紙濡傚杞﹀崗璋冿級
+
+## 馃彈锔� 鏋舵瀯璁捐
+
+### 鐩綍缁撴瀯
+
+```
+mes-plcSend/
+鈹溾攢鈹� device/ # 璁惧绠$悊灞�
+鈹� 鈹溾攢鈹� entity/ # 瀹炰綋绫�
+鈹� 鈹� 鈹溾攢鈹� DeviceConfig.java # 璁惧閰嶇疆
+鈹� 鈹� 鈹溾攢鈹� DeviceGroupConfig.java # 璁惧缁勯厤缃�
+鈹� 鈹� 鈹溾攢鈹� DeviceGroupRelation.java # 璁惧缁勫叧绯�
+鈹� 鈹� 鈹溾攢鈹� DeviceStatus.java # 璁惧鐘舵��
+鈹� 鈹� 鈹斺攢鈹� GlassInfo.java # 鐜荤拑淇℃伅
+鈹� 鈹溾攢鈹� service/ # 鏈嶅姟灞�
+鈹� 鈹� 鈹溾攢鈹� DeviceConfigService.java
+鈹� 鈹� 鈹溾攢鈹� DeviceGroupConfigService.java
+鈹� 鈹� 鈹溾攢鈹� DeviceCoordinationService.java # 璁惧鍗忚皟鏈嶅姟
+鈹� 鈹� 鈹斺攢鈹� GlassInfoService.java
+鈹� 鈹斺攢鈹� controller/ # 鎺у埗鍣�
+鈹� 鈹溾攢鈹� DeviceConfigController.java
+鈹� 鈹溾攢鈹� DeviceGroupController.java
+鈹� 鈹斺攢鈹� DevicePlcController.java
+鈹�
+鈹溾攢鈹� interaction/ # 浜や簰閫昏緫灞�
+鈹� 鈹溾攢鈹� base/ # 鍩虹鎺ュ彛
+鈹� 鈹� 鈹溾攢鈹� BaseDeviceLogicHandler.java
+鈹� 鈹� 鈹溾攢鈹� DeviceInteraction.java
+鈹� 鈹� 鈹斺攢鈹� InteractionContext.java
+鈹� 鈹�
+鈹� 鈹溾攢鈹� vehicle/ # 澶ц溅璁惧涓撶敤鍖�
+鈹� 鈹� 鈹溾攢鈹� handler/
+鈹� 鈹� 鈹� 鈹斺攢鈹� LoadVehicleLogicHandler.java # 鍏变韩閫昏緫澶勭悊鍣�
+鈹� 鈹� 鈹溾攢鈹� flow/
+鈹� 鈹� 鈹� 鈹斺攢鈹� LoadVehicleInteraction.java
+鈹� 鈹� 鈹溾攢鈹� coordination/
+鈹� 鈹� 鈹� 鈹溾攢鈹� VehicleStatusManager.java # 鐘舵�佺鐞嗗櫒
+鈹� 鈹� 鈹� 鈹斺攢鈹� VehicleCoordinationService.java # 鍗忚皟鏈嶅姟
+鈹� 鈹� 鈹斺攢鈹� model/
+鈹� 鈹� 鈹溾攢鈹� VehicleStatus.java
+鈹� 鈹� 鈹溾攢鈹� VehiclePosition.java
+鈹� 鈹� 鈹溾攢鈹� VehicleState.java
+鈹� 鈹� 鈹溾攢鈹� VehiclePath.java
+鈹� 鈹� 鈹斺攢鈹� VehicleTask.java
+鈹� 鈹�
+鈹� 鈹溾攢鈹� workstation/ # 鍗ц浆绔嬭澶囧寘
+鈹� 鈹� 鈹溾攢鈹� base/
+鈹� 鈹� 鈹� 鈹斺攢鈹� WorkstationBaseHandler.java
+鈹� 鈹� 鈹溾攢鈹� config/
+鈹� 鈹� 鈹� 鈹斺攢鈹� WorkstationLogicConfig.java
+鈹� 鈹� 鈹溾攢鈹� scanner/
+鈹� 鈹� 鈹� 鈹斺攢鈹� handler/
+鈹� 鈹� 鈹� 鈹斺攢鈹� HorizontalScannerLogicHandler.java
+鈹� 鈹� 鈹斺攢鈹� transfer/
+鈹� 鈹� 鈹斺攢鈹� handler/
+鈹� 鈹� 鈹斺攢鈹� HorizontalTransferLogicHandler.java
+鈹� 鈹�
+鈹� 鈹溾攢鈹� largeglass/ # 澶х悊鐗囩璁惧鍖�
+鈹� 鈹� 鈹溾攢鈹� handler/
+鈹� 鈹� 鈹� 鈹斺攢鈹� LargeGlassLogicHandler.java
+鈹� 鈹� 鈹溾攢鈹� config/
+鈹� 鈹� 鈹� 鈹斺攢鈹� LargeGlassConfig.java
+鈹� 鈹� 鈹斺攢鈹� model/
+鈹� 鈹� 鈹斺攢鈹� GridRange.java
+鈹� 鈹�
+鈹� 鈹溾攢鈹� flow/ # 浜や簰娴佺▼锛堥�氱敤锛�
+鈹� 鈹� 鈹溾攢鈹� GlassStorageInteraction.java
+鈹� 鈹� 鈹斺攢鈹� LargeGlassInteraction.java
+鈹� 鈹�
+鈹� 鈹斺攢鈹� impl/ # 绠�鍗曡澶囧疄鐜�
+鈹� 鈹斺攢鈹� GlassStorageLogicHandler.java
+鈹�
+鈹溾攢鈹� task/ # 浠诲姟绠$悊灞�
+鈹� 鈹溾攢鈹� entity/
+鈹� 鈹� 鈹溾攢鈹� MultiDeviceTask.java # 澶氳澶囦换鍔�
+鈹� 鈹� 鈹斺攢鈹� TaskStepDetail.java # 浠诲姟姝ラ璇︽儏
+鈹� 鈹溾攢鈹� service/
+鈹� 鈹� 鈹溾攢鈹� MultiDeviceTaskService.java
+鈹� 鈹� 鈹溾攢鈹� TaskExecutionEngine.java # 浠诲姟鎵ц寮曟搸
+鈹� 鈹� 鈹斺攢鈹� TaskStatusNotificationService.java # SSE鎺ㄩ�佹湇鍔�
+鈹� 鈹溾攢鈹� controller/
+鈹� 鈹� 鈹溾攢鈹� MultiDeviceTaskController.java
+鈹� 鈹� 鈹斺攢鈹� TaskStatusNotificationController.java
+鈹� 鈹斺攢鈹� model/
+鈹� 鈹溾攢鈹� RetryPolicy.java # 閲嶈瘯绛栫暐
+鈹� 鈹溾攢鈹� TaskExecutionContext.java
+鈹� 鈹斺攢鈹� TaskExecutionResult.java
+鈹�
+鈹溾攢鈹� service/ # PLC鏈嶅姟灞�
+鈹� 鈹溾攢鈹� PlcDynamicDataService.java # PLC鍔ㄦ�佹暟鎹湇鍔�
+鈹� 鈹斺攢鈹� PlcTestWriteService.java
+鈹�
+鈹斺攢鈹� s7/ # S7閫氫俊
+ 鈹斺攢鈹� provider/
+ 鈹斺攢鈹� S7SerializerProvider.java
+```
+
+### 鏍稿績璁捐妯″紡
+
+#### 1. 妯℃澘 + 瀹炰緥妯″紡
+
+**璁捐鐞嗗康**锛氫竴涓澶囩被鍨嬫ā鏉匡紙鍏变韩閫昏緫锛�+ 澶氫釜璁惧瀹炰緥锛堢嫭绔嬬姸鎬侊級
+
+```
+LoadVehicleLogicHandler (鍏变韩閫昏緫澶勭悊鍣�)
+ 鈫�
+鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�
+鈹� 澶ц溅瀹炰緥1鈹� 鈹� 澶ц溅瀹炰緥2鈹� 鈹� 澶ц溅瀹炰緥3鈹�
+鈹� 鐘舵�佺嫭绔� 鈹� 鈹� 鐘舵�佺嫭绔� 鈹� 鈹� 鐘舵�佺嫭绔� 鈹�
+鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�
+```
+
+**浼樺娍**锛�
+- 鍏变韩閫昏緫锛氭墍鏈夊疄渚嬩娇鐢ㄥ悓涓�涓�昏緫澶勭悊鍣�
+- 鐙珛鐘舵�侊細姣忎釜瀹炰緥鏈夌嫭绔嬬殑杩愯鏃剁姸鎬�
+- 鐏垫椿閰嶇疆锛氳澶囩粍鍙互鍖呭惈浠绘剰鏁伴噺鐨勫疄渚�
+- 鏄撲簬鎵╁睍锛氭柊澧炲疄渚嬪彧闇�鍦ㄦ暟鎹簱娣诲姞璁板綍
+
+#### 2. 娣峰悎鍒嗗眰鏋舵瀯
+
+- **澶嶆潅璁惧**锛堝澶ц溅锛夛細鐙珛鍖咃紝鍖呭惈鍗忚皟鏈嶅姟銆佺姸鎬佺鐞嗙瓑
+- **绠�鍗曡澶�**锛堝鐜荤拑瀛樺偍锛夛細鏀惧湪閫氱敤鍖呬腑
+
+## 馃敡 鏍稿績缁勪欢璇存槑
+
+### 1. 澶ц溅璁惧锛圠oadVehicle锛�
+
+#### 鍔熻兘鐗规��
+- **绌洪棽鐘舵�佺洃鎺�**锛氭病鏈変换鍔℃椂锛宍plcRequest` 淇濇寔涓� 1
+- **MES浠诲姟璇诲彇**锛氬綋 `mesSend=1` 鏃讹紝璇诲彇 MES 鍙傛暟锛堢幓鐠僆D銆佽捣濮嬩綅缃�佺洰鏍囦綅缃瓑锛�
+- **浣嶇疆鏄犲皠**锛氬皢 MES 浣嶇疆缂栧彿锛堝 900銆�901锛夋槧灏勪负瀹為檯缃戞牸浣嶇疆锛堝 100銆�500锛�
+- **鏃堕棿璁$畻**锛氭牴鎹溅杈嗛�熷害锛坓rids/second锛夈�佸綋鍓嶄綅缃�佺洰鏍囦綅缃绠� `gotime` 鍜� `cartime`
+- **鐘舵�佺鐞�**锛歚state1~6` 鐘舵�佹祦杞紙0鈫�1鈫�2锛夛紝鑷姩瑙﹀彂 MES 姹囨姤
+- **鑷姩鍗忚皟**锛氬綋 `state=1`锛堜笂杞﹀畬鎴愶級鏃讹紝鑷姩灏�"鍗ц浆绔�"璁惧鐨� `plcRequest` 璁剧疆涓� 0
+- **鍑虹墖閫昏緫**锛氭敮鎸佽繘鐗囧拰鍑虹墖浠诲姟锛屾牴鎹� `startSlot` 鍜� `outboundSlotRanges` 鑷姩鍒ゆ柇浠诲姟绫诲瀷
+
+#### 閰嶇疆鍙傛暟锛坋xtraParams.deviceLogic锛�
+```json
+{
+ "vehicleCapacity": 6,
+ "vehicleSpeed": 1.0,
+ "minRange": 1,
+ "maxRange": 100,
+ "homePosition": 50,
+ "idleMonitorIntervalMs": 1000,
+ "taskMonitorIntervalMs": 1000,
+ "mesConfirmTimeoutMs": 30000,
+ "positionMapping": {
+ "900": 100,
+ "901": 500
+ },
+ "outboundSlotRanges": [
+ {"start": 1000, "end": 2000}
+ ],
+ "gridPositionMapping": {
+ "1000": 80
+ }
+}
+```
+
+#### 鐘舵�佹祦杞�
+```
+IDLE (绌洪棽) 鈫� EXECUTING (鎵ц涓�) 鈫� IDLE (绌洪棽)
+```
+
+### 2. 鍗ц浆绔嬫壂鐮侊紙HorizontalScanner锛�
+
+#### 鍔熻兘鐗规��
+- **瀹氭椂鎵弿**锛氬彲閰嶇疆鎵弿闂撮殧锛堥粯璁� 10s锛�
+- **MES鏁版嵁璇诲彇**锛氬綋 `mesSend=1` 鏃讹紝璇诲彇鐜荤拑淇℃伅锛坄mesGlassId`銆乣mesWidth`銆乣mesHeight`銆乣workLine`锛�
+- **鏁版嵁钀藉簱**锛氬皢鐜荤拑淇℃伅淇濆瓨鍒� `glass_info` 琛�
+- **鑷姩纭**锛氳鍙栧悗鑷姩灏� `mesSend` 鍐欏洖 0
+
+#### 閰嶇疆鍙傛暟锛坋xtraParams.deviceLogic锛�
+```json
+{
+ "scanIntervalMs": 10000,
+ "workLine": "LINE_001",
+ "autoConfirm": true
+}
+```
+
+### 3. 鍗ц浆绔嬶紙HorizontalTransfer锛�
+
+#### 鍔熻兘鐗规��
+- **30s缂撳啿鍒ゅ畾**锛氱瓑寰� 30s锛屽鏋滄病鏈変笅涓�鐗囩幓鐠冩壂鐮侊紝鍒欒涓烘槸鏈�鍚庝竴鐗�
+- **瀹归噺鍒ゆ柇**锛氬垽鏂兘鍚︽斁涓嬬浜岀墖鐜荤拑
+- **鎵归噺澶勭悊**锛氬皢澶氱墖鐜荤拑缁勮鎴愭壒娆�
+- **PLC鍐欏叆**锛氬啓鍏� `plcGlassId1~6`銆乣plcGlassCount`銆乣inPosition`銆乣plcRequest`
+
+#### 閰嶇疆鍙傛暟锛坋xtraParams.deviceLogic锛�
+```json
+{
+ "scanIntervalMs": 10000,
+ "bufferTimeoutMs": 30000,
+ "vehicleCapacity": 2,
+ "monitorIntervalMs": 1000,
+ "workLine": "LINE_001",
+ "positionValue": 100
+}
+```
+
+### 4. 澶х悊鐗囩锛圠argeGlass锛�
+
+#### 鍔熻兘鐗规��
+- **鏍煎瓙鑼冨洿閰嶇疆**锛氭敮鎸佸琛屾牸瀛愰厤缃紙濡傜涓�琛� 1~52 鏍硷紝绗簩琛� 53~101 鏍硷級
+- **鏍煎瓙灏哄閰嶇疆**锛氭瘡鏍肩殑闀裤�佸銆佸帤鍙厤缃�
+- **閫昏緫鍒ゆ柇**锛氱敤浜庝綅缃獙璇佸拰鏍煎瓙绠$悊锛屼笉娑夊強 PLC 鍐欏叆
+
+#### 閰嶇疆鍙傛暟锛坋xtraParams.deviceLogic锛�
+```json
+{
+ "gridRanges": [
+ {"row": 1, "start": 1, "end": 52},
+ {"row": 2, "start": 53, "end": 101}
+ ],
+ "gridLength": 2000,
+ "gridWidth": 1500,
+ "gridThickness": 5
+}
+```
+
+## 馃殌 浣跨敤鎸囧崡
+
+### 1. 璁惧閰嶇疆
+
+#### 鍒涘缓璁惧
+```java
+DeviceConfig device = new DeviceConfig();
+device.setDeviceId("DEVICE_001");
+device.setDeviceCode("DEV_001");
+device.setDeviceName("澶ц溅璁惧1");
+device.setDeviceType(DeviceConfig.DeviceType.LOAD_VEHICLE);
+device.setPlcIp("192.168.1.101");
+device.setPlcPort(102);
+device.setPlcType(DeviceConfig.PlcType.S7_1200);
+device.setModuleName("DB1");
+device.setProjectId(1L);
+device.setEnabled(true);
+// 璁剧疆閫昏緫鍙傛暟
+device.setExtraParams(extraParams);
+deviceConfigService.createDevice(device);
+```
+
+#### 閰嶇疆璁惧閫昏緫鍙傛暟
+鍦� `extraParams.deviceLogic` 涓厤缃澶囩壒瀹氱殑閫昏緫鍙傛暟锛屽杞﹁締閫熷害銆佷綅缃槧灏勭瓑銆�
+
+### 2. 璁惧缁勯厤缃�
+
+#### 鍒涘缓璁惧缁�
+```java
+DeviceGroupConfig group = new DeviceGroupConfig();
+group.setGroupCode("GROUP_001");
+group.setGroupName("鐢熶骇绾緼");
+group.setGroupType(1); // 1-鐢熶骇绾匡紝2-娴嬭瘯绾匡紝3-杈呭姪璁惧缁�
+group.setProjectId(1L);
+group.setStatus(1); // 0-鍋滅敤锛�1-鍚敤锛�3-缁存姢涓�
+group.setMaxConcurrentDevices(3); // 鏈�澶у苟鍙戣澶囨暟
+group.setHeartbeatInterval(30);
+group.setCommunicationTimeout(5000);
+deviceGroupService.createGroup(group);
+```
+
+#### 娣诲姞璁惧鍒拌澶囩粍
+```java
+// 娣诲姞璁惧锛岃缃紭鍏堢骇銆佽鑹层�佽繛鎺ラ『搴�
+deviceGroupService.addDevicesToGroup(groupId, deviceIds, priorities, roles, connectionOrders);
+```
+
+### 3. 浠诲姟鎵ц
+
+#### 鍚姩澶氳澶囦换鍔�
+```java
+MultiDeviceTaskRequest request = new MultiDeviceTaskRequest();
+request.setGroupId(groupId);
+request.setTaskName("娴嬭瘯浠诲姟");
+request.setProjectId("PROJECT_001");
+request.setParameters(taskParameters);
+MultiDeviceTask task = multiDeviceTaskService.startTask(request);
+```
+
+#### API 绔偣
+- `POST /device/task/start` - 鍚姩浠诲姟
+- `POST /device/task/list` - 鏌ヨ浠诲姟鍒楄〃
+- `GET /device/task/{taskId}` - 鏌ヨ浠诲姟璇︽儏
+- `GET /device/task/{taskId}/steps` - 鏌ヨ浠诲姟姝ラ璇︽儏
+- `POST /device/task/{taskId}/cancel` - 鍙栨秷浠诲姟
+
+#### 瀹炴椂鐩戞帶锛圫SE锛�
+鍓嶇閫氳繃 SSE 杩炴帴瀹炴椂鎺ユ敹浠诲姟鐘舵�佹洿鏂帮細
+```javascript
+// 鐩戝惉鎸囧畾浠诲姟
+const eventSource = new EventSource('/task/notification/sse?taskId=xxx');
+eventSource.addEventListener('taskStatus', (event) => {
+ const data = JSON.parse(event.data);
+ // 澶勭悊浠诲姟鐘舵�佹洿鏂�
+});
+eventSource.addEventListener('stepUpdate', (event) => {
+ const data = JSON.parse(event.data);
+ // 澶勭悊姝ラ鏇存柊
+});
+
+// 鐩戝惉鎵�鏈変换鍔�
+const eventSourceAll = new EventSource('/task/notification/sse/all');
+```
+
+**SSE 绔偣**锛�
+- `GET /task/notification/sse?taskId=xxx` - 鐩戝惉鎸囧畾浠诲姟
+- `GET /task/notification/sse/all` - 鐩戝惉鎵�鏈変换鍔�
+- `POST /task/notification/close/{taskId}` - 鍏抽棴鎸囧畾浠诲姟鐨凷SE杩炴帴
+- `POST /task/notification/close/all` - 鍏抽棴鎵�鏈塖SE杩炴帴
+
+## 馃搳 鏁版嵁搴撹璁�
+
+### 鏍稿績琛ㄧ粨鏋�
+
+#### device_config锛堣澶囬厤缃〃锛�
+- `id`锛氫富閿紙BIGINT锛�
+- `device_id`锛氳澶囧敮涓�鏍囪瘑锛圴ARCHAR(50)锛屽敮涓�锛�
+- `device_code`锛氳澶囩紪鐮侊紙VARCHAR(50)锛屽敮涓�锛�
+- `device_name`锛氳澶囧悕绉帮紙VARCHAR(100)锛�
+- `device_type`锛氳澶囩被鍨嬶紙VARCHAR(50)锛�
+- `project_id`锛氭墍灞為」鐩甀D锛圔IGINT锛�
+- `plc_ip`锛歅LC IP鍦板潃锛圴ARCHAR(15)锛�
+- `plc_port`锛歅LC绔彛锛圛NT锛�
+- `plc_type`锛歅LC绫诲瀷锛圴ARCHAR(20)锛�
+- `module_name`锛氭ā鍧楀悕绉帮紙VARCHAR(50)锛�
+- `status`锛氳澶囩姸鎬侊紙VARCHAR(20)锛�
+- `is_primary`锛氭槸鍚︿富鎺ц澶囷紙BOOLEAN锛�
+- `enabled`锛氭槸鍚﹀惎鐢紙BOOLEAN锛�
+- `config_json`锛氳澶囩壒瀹氶厤缃紙TEXT锛孞SON鏍煎紡锛�
+- `extra_params`锛氭墿灞曞弬鏁帮紙JSON锛�
+- `description`锛氳澶囨弿杩帮紙VARCHAR(200)锛�
+- `is_deleted`锛氭槸鍚﹀垹闄わ紙INT锛�0-鍚︼紝1-鏄級
+- `created_time`銆乣updated_time`锛氬垱寤�/鏇存柊鏃堕棿
+- `created_by`銆乣updated_by`锛氬垱寤�/鏇存柊浜�
+
+#### device_group_config锛堣澶囩粍閰嶇疆琛級
+- `id`锛氫富閿紙BIGINT锛�
+- `group_code`锛氳澶囩粍缂栫爜锛圴ARCHAR(50)锛屽敮涓�锛�
+- `group_name`锛氳澶囩粍鍚嶇О锛圴ARCHAR(100)锛�
+- `group_type`锛氳澶囩粍绫诲瀷锛圛NT锛�1-鐢熶骇绾匡紝2-娴嬭瘯绾匡紝3-杈呭姪璁惧缁勶級
+- `project_id`锛氭墍灞為」鐩甀D锛圔IGINT锛�
+- `status`锛氳澶囩粍鐘舵�侊紙INT锛�0-鍋滅敤锛�1-鍚敤锛�3-缁存姢涓級
+- `max_concurrent_devices`锛氭渶澶у苟鍙戣澶囨暟锛圛NT锛�
+- `heartbeat_interval`锛氬績璺虫娴嬮棿闅旓紙INT锛岀锛�
+- `communication_timeout`锛氶�氫俊瓒呮椂鏃堕棿锛圛NT锛屾绉掞級
+- `description`锛氳澶囩粍鎻忚堪锛圴ARCHAR(200)锛�
+- `extra_config`锛氭墿灞曢厤缃紙JSON锛�
+- `is_deleted`锛氭槸鍚﹀垹闄わ紙INT锛�
+- `created_time`銆乣updated_time`锛氬垱寤�/鏇存柊鏃堕棿
+- `created_by`銆乣updated_by`锛氬垱寤�/鏇存柊浜�
+
+#### device_group_relation锛堣澶囩粍鍏崇郴琛級
+- `id`锛氫富閿紙BIGINT锛�
+- `group_id`锛氳澶囩粍ID锛圔IGINT锛�
+- `device_id`锛氳澶嘔D锛圔IGINT锛�
+- `priority`锛氳澶囧湪缁勫唴鐨勪紭鍏堢骇锛圛NT锛�1-鏈�楂橈紝10-鏈�浣庯級
+- `role`锛氳澶囧湪缁勫唴鐨勮鑹诧紙INT锛�1-涓绘帶锛�2-鍗忎綔锛�3-鐩戞帶锛�
+- `status`锛氳澶囧湪璇ョ粍涓殑鐘舵�侊紙INT锛�0-鏈厤缃紝1-姝e父锛�2-鏁呴殰锛�3-缁存姢锛�
+- `connection_order`锛氳繛鎺ラ『搴忥紙INT锛屾暟鍊艰秺灏忚秺鍏堣繛鎺ワ級
+- `relation_desc`锛氬叧鑱旀弿杩帮紙VARCHAR(200)锛�
+- `extra_params`锛氭墿灞曞弬鏁帮紙JSON锛�
+- `is_deleted`锛氭槸鍚﹀垹闄わ紙INT锛�
+- `created_time`銆乣updated_time`锛氬垱寤�/鏇存柊鏃堕棿
+- `created_by`銆乣updated_by`锛氬垱寤�/鏇存柊浜�
+
+#### multi_device_task锛堝璁惧浠诲姟琛級
+- `id`锛氫富閿紙BIGINT锛�
+- `task_id`锛氫换鍔″敮涓�鏍囪瘑锛圴ARCHAR(50)锛屽敮涓�锛�
+- `group_id`锛氳澶囩粍ID锛圴ARCHAR(50)锛�
+- `project_id`锛氶」鐩甀D锛圴ARCHAR(50)锛�
+- `status`锛氫换鍔$姸鎬侊紙ENUM锛歅ENDING, RUNNING, COMPLETED, FAILED, CANCELLED锛�
+- `current_step`锛氬綋鍓嶆墽琛屾楠わ紙INT锛�
+- `total_steps`锛氭�绘楠ゆ暟锛圛NT锛�
+- `start_time`锛氬紑濮嬫椂闂达紙DATETIME锛�
+- `end_time`锛氱粨鏉熸椂闂达紙DATETIME锛�
+- `error_message`锛氶敊璇俊鎭紙TEXT锛�
+- `result_data`锛氱粨鏋滄暟鎹紙JSON锛�
+- `created_time`銆乣updated_time`锛氬垱寤�/鏇存柊鏃堕棿
+
+#### task_step_detail锛堜换鍔℃楠よ鎯呰〃锛�
+- `id`锛氫富閿紙BIGINT锛�
+- `task_id`锛氫换鍔D锛圴ARCHAR(50)锛�
+- `step_order`锛氭楠ら『搴忥紙INT锛�
+- `device_id`锛氳澶嘔D锛圴ARCHAR(50)锛�
+- `step_name`锛氭楠ゅ悕绉帮紙VARCHAR(100)锛�
+- `status`锛氭楠ょ姸鎬侊紙ENUM锛歅ENDING, RUNNING, COMPLETED, FAILED, SKIPPED锛�
+- `start_time`锛氭楠ゅ紑濮嬫椂闂达紙DATETIME锛�
+- `end_time`锛氭楠ょ粨鏉熸椂闂达紙DATETIME锛�
+- `duration_ms`锛氭墽琛岃�楁椂锛圔IGINT锛屾绉掞級
+- `input_data`锛氳緭鍏ユ暟鎹紙JSON锛�
+- `output_data`锛氳緭鍑烘暟鎹紙JSON锛�
+- `error_message`锛氶敊璇俊鎭紙TEXT锛�
+- `retry_count`锛氶噸璇曟鏁帮紙INT锛�
+- `created_time`锛氬垱寤烘椂闂达紙DATETIME锛�
+
+#### glass_info锛堢幓鐠冧俊鎭〃锛�
+- `id`锛氫富閿紙BIGINT锛�
+- `glass_id`锛氱幓鐠僆D锛圴ARCHAR(50)锛�
+- `width`锛氬搴︼紙DECIMAL锛�
+- `height`锛氶珮搴︼紙DECIMAL锛�
+- `work_line`锛氫骇绾跨紪鍙凤紙VARCHAR(50)锛�
+- `scan_time`锛氭壂鐮佹椂闂达紙DATETIME锛�
+- `status`锛氱姸鎬侊紙VARCHAR(20)锛�
+- `created_time`銆乣updated_time`锛氬垱寤�/鏇存柊鏃堕棿
+
+#### device_status锛堣澶囩姸鎬佺洃鎺ц〃锛�
+- `id`锛氫富閿紙BIGINT锛�
+- `device_id`锛氳澶嘔D锛圴ARCHAR(50)锛�
+- `task_id`锛氬叧鑱斾换鍔D锛圴ARCHAR(50)锛屽彲閫夛級
+- `status`锛氳澶囩姸鎬侊紙ENUM锛歄NLINE, OFFLINE, BUSY, ERROR, MAINTENANCE锛�
+- `last_heartbeat`锛氭渶鍚庡績璺虫椂闂达紙DATETIME锛�
+- `cpu_usage`锛欳PU浣跨敤鐜囷紙DECIMAL(5,2)锛�
+- `memory_usage`锛氬唴瀛樹娇鐢ㄧ巼锛圖ECIMAL(5,2)锛�
+- `plc_connection_status`锛歅LC杩炴帴鐘舵�侊紙ENUM锛欳ONNECTED, DISCONNECTED, ERROR锛�
+- `current_operation`锛氬綋鍓嶆搷浣滐紙VARCHAR(100)锛�
+- `operation_progress`锛氭搷浣滆繘搴︼紙DECIMAL(5,2)锛�0-100锛�
+- `alert_message`锛氬憡璀︿俊鎭紙TEXT锛�
+- `created_time`锛氳褰曟椂闂达紙DATETIME锛�
+
+## 馃攧 浠诲姟鎵ц娴佺▼
+
+### 涓茶鎵ц娴佺▼
+```
+1. 鍒涘缓浠诲姟璁板綍锛坰tatus = PENDING锛�
+2. 鑾峰彇璁惧缁勪腑鐨勮澶囧垪琛紙鎸� connection_order 鎺掑簭锛�
+3. 渚濇鎵ц姣忎釜璁惧锛�
+ a. 妫�鏌ュ墠缃潯浠�
+ b. 鏇存柊姝ラ鐘舵�佷负 RUNNING
+ c. 鎵ц璁惧浜や簰閫昏緫
+ d. 浼犻�掓暟鎹埌涓嬩竴涓澶�
+ e. 鏇存柊姝ラ鐘舵�佷负 COMPLETED
+4. 鎵�鏈夎澶囨墽琛屽畬鎴愬悗锛屾洿鏂颁换鍔$姸鎬佷负 COMPLETED
+```
+
+### 骞惰鎵ц娴佺▼
+```
+1. 鍒涘缓浠诲姟璁板綍锛坰tatus = PENDING锛�
+2. 鑾峰彇璁惧缁勪腑鐨勮澶囧垪琛�
+3. 浣跨敤绾跨▼姹犲苟琛屾墽琛岃澶囷細
+ a. 浣跨敤 max_concurrent_devices 鎺у埗骞跺彂鏁�
+ b. 姣忎釜璁惧鐙珛鎵ц
+ c. 绛夊緟鎵�鏈夎澶囧畬鎴�
+4. 鎵�鏈夎澶囨墽琛屽畬鎴愬悗锛屾洿鏂颁换鍔$姸鎬佷负 COMPLETED
+```
+
+## 鈿欙笍 閰嶇疆璇存槑
+
+### 搴旂敤閰嶇疆锛坅pplication.yml锛�
+
+```yaml
+server:
+ port: 10018
+
+spring:
+ profiles:
+ active: dev
+ application:
+ name: plcSend
+ liquibase:
+ enabled: true
+ change-log: classpath:changelog/changelogBase.xml
+
+# PLC閰嶇疆
+s7:
+ load:
+ dbArea: DB1
+ beginIndex: 0
+ raw:
+ dbArea: DB2
+ beginIndex: 0
+
+# PLC妯℃嫙閰嶇疆
+plc:
+ simulate:
+ enabled: false
+ interval: 5000
+ failure-rate: 0
+ task-count: 10
+ task-type: normal
+
+# MES閰嶇疆
+mes:
+ width: 2800
+ height: 5000
+```
+
+### 璁惧閫昏緫鍙傛暟閰嶇疆
+
+姣忎釜璁惧绫诲瀷鐨勯�昏緫鍙傛暟鍦� `extraParams.deviceLogic` 涓厤缃紝鍏蜂綋鍙傛暟瑙佸悇璁惧绫诲瀷鐨勮鏄庛��
+
+## 馃洜锔� 鎵╁睍寮�鍙�
+
+### 娣诲姞鏂拌澶囩被鍨�
+
+1. **鍦� DeviceConfig.DeviceType 涓坊鍔犲父閲�**
+```java
+public static final class DeviceType {
+ public static final String NEW_DEVICE = "鏂拌澶囩被鍨�";
+}
+```
+
+2. **鍒涘缓璁惧澶勭悊鍣�**
+```java
+@Component
+public class NewDeviceLogicHandler extends BaseDeviceLogicHandler {
+ @Override
+ public String getDeviceType() {
+ return DeviceConfig.DeviceType.NEW_DEVICE;
+ }
+
+ @Override
+ protected DevicePlcVO.OperationResult doExecute(
+ DeviceConfig deviceConfig,
+ String operation,
+ Map<String, Object> params,
+ Map<String, Object> logicParams) {
+ // 瀹炵幇璁惧閫昏緫
+ }
+}
+```
+
+3. **鍒涘缓浜や簰娴佺▼**锛堝彲閫夛級
+```java
+@Component
+public class NewDeviceInteraction implements DeviceInteraction {
+ @Override
+ public InteractionResult execute(InteractionContext context) {
+ // 瀹炵幇浜や簰娴佺▼
+ }
+}
+```
+
+### 娣诲姞璁惧鍗忚皟閫昏緫
+
+瀵逛簬闇�瑕佸瀹炰緥鍗忚皟鐨勫鏉傝澶囷紝鍙互鍒涘缓涓撶敤鍖咃細
+```
+interaction/
+鈹斺攢鈹� newdevice/
+ 鈹溾攢鈹� handler/
+ 鈹溾攢鈹� coordination/
+ 鈹斺攢鈹� model/
+```
+
+## 馃摑 娉ㄦ剰浜嬮」
+
+### 1. 鐘舵�佺鐞�
+- 澶ц溅璁惧鐨勭姸鎬佸瓨鍌ㄥ湪鍐呭瓨涓紙`VehicleStatusManager`锛夛紝鏈嶅姟閲嶅惎鍚庝細涓㈠け
+- 濡傞渶鎸佷箙鍖栵紝鍙互鎵╁睍鏀寔鏁版嵁搴撳瓨鍌�
+
+### 2. 骞跺彂瀹夊叏
+- 鐘舵�佺鐞嗗櫒浣跨敤 `ConcurrentHashMap`锛屾敮鎸佸苟鍙戣闂�
+- 浠诲姟鎵ц浣跨敤绾跨▼姹犲拰 `max_concurrent_devices` 鎺у埗骞跺彂
+
+### 3. 閿欒澶勭悊
+- 鏀寔鑷姩閲嶈瘯鏈哄埗锛堥粯璁ゆ渶澶�3娆★級
+- 鏀寔鎸囨暟閫�閬跨瓥鐣�
+- 閿欒鍒嗙被锛氱綉缁滈敊璇�佽秴鏃堕敊璇彲閲嶈瘯锛屼笟鍔¢敊璇笉鍙噸璇�
+
+### 4. 瀹炴椂鐩戞帶
+- SSE 杩炴帴瓒呮椂鏃堕棿锛�30鍒嗛挓
+- 鏀寔鐩戝惉鎸囧畾浠诲姟鎴栨墍鏈変换鍔�
+- 杩炴帴鏂紑鍚庨渶瑕侀噸鏂拌繛鎺�
+
+### 5. 璁惧绫诲瀷璇存槑
+- **鍗у紡缂撳瓨**锛坄GLASS_STORAGE`锛夛細浠g爜涓凡瀹炵幇锛屼絾褰撳墠涓氬姟鍦烘櫙涓嶄娇鐢紝淇濈暀鐢ㄤ簬鏈潵鎵╁睍
+
+## 馃幆 宸插畬鎴愬姛鑳�
+
+### 鉁� 鏍稿績鍔熻兘
+- [x] 璁惧绠$悊锛堥厤缃�佺姸鎬佺洃鎺э級
+- [x] 璁惧缁勭鐞嗭紙骞跺彂鎺у埗銆佷紭鍏堢骇銆佽鑹诧級
+- [x] 澶氳澶囦换鍔℃墽琛屽紩鎿庯紙涓茶/骞惰锛�
+- [x] 璁惧鍗忚皟鏈嶅姟锛堟暟鎹紶閫掋�佺姸鎬佸悓姝ワ級
+- [x] 瀹炴椂鐩戞帶鎺ㄩ�侊紙SSE锛�
+- [x] 閿欒澶勭悊鍜岄噸璇曟満鍒�
+
+### 鉁� 璁惧绫诲瀷鏀寔
+- [x] 澶ц溅璁惧锛堝瀹炰緥鍗忚皟銆佺姸鎬佺鐞嗐�丮ES浠诲姟澶勭悊銆佽繘鐗�/鍑虹墖锛�
+- [x] 鍗ц浆绔嬫壂鐮侊紙瀹氭椂鎵弿銆佹暟鎹惤搴擄級
+- [x] 鍗ц浆绔嬶紙30s缂撳啿銆佹壒閲忓鐞嗭級
+- [x] 澶х悊鐗囩锛堟牸瀛愰厤缃�侀�昏緫鍒ゆ柇锛�
+- [x] 鍗у紡缂撳瓨锛堝凡瀹炵幇锛屽綋鍓嶄笉浣跨敤锛�
+
+### 鉁� API 绔偣
+- [x] 璁惧绠$悊 API锛坄/device/*`锛�
+- [x] 璁惧缁勭鐞� API锛坄/device/group/*`锛�
+- [x] 澶氳澶囦换鍔� API锛坄/device/task/*`锛�
+- [x] SSE 瀹炴椂閫氱煡 API锛坄/task/notification/*`锛�
+
+## 馃摎 鐩稿叧鏂囨。
+
+- 鏁版嵁搴撹縼绉昏剼鏈細`src/main/resources/db/migration/`
+- API 鏂囨。锛氶�氳繃 Swagger 璁块棶 `/swagger-ui.html`
+- 鍓嶇鏂囨。锛氳 `mes-web` 椤圭洰
+
+## 馃摓 鑱旂郴鏂瑰紡
+
+濡傛湁闂鎴栧缓璁紝璇疯仈绯诲紑鍙戝洟闃熴��
diff --git a/mes-processes/mes-plcSend/src/main/java/com/mes/config/TaskExecutorConfig.java b/mes-processes/mes-plcSend/src/main/java/com/mes/config/TaskExecutorConfig.java
new file mode 100644
index 0000000..eb65e79
--- /dev/null
+++ b/mes-processes/mes-plcSend/src/main/java/com/mes/config/TaskExecutorConfig.java
@@ -0,0 +1,63 @@
+package com.mes.config;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.annotation.EnableAsync;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.ThreadPoolExecutor;
+
+/**
+ * 浠诲姟鎵ц鍣ㄩ厤缃�
+ * 鐢ㄤ簬寮傛鎵ц澶氳澶囩粍浠诲姟
+ *
+ * @author mes
+ * @since 2025-01-XX
+ */
+@Slf4j
+@Configuration
+@EnableAsync
+public class TaskExecutorConfig {
+
+ /**
+ * 璁惧缁勪换鍔℃墽琛岀嚎绋嬫睜
+ * 姣忎釜璁惧缁勪綔涓轰竴涓嫭绔嬬嚎绋嬫墽琛�
+ */
+ @Bean(name = "deviceGroupTaskExecutor")
+ public Executor deviceGroupTaskExecutor() {
+ ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+
+ // 鏍稿績绾跨▼鏁帮細鏀寔鍚屾椂鎵ц鐨勬牳蹇冭澶囩粍鏁伴噺
+ executor.setCorePoolSize(5);
+
+ // 鏈�澶х嚎绋嬫暟锛氭渶澶氬悓鏃舵墽琛岀殑璁惧缁勬暟閲�
+ executor.setMaxPoolSize(20);
+
+ // 闃熷垪瀹归噺锛氱瓑寰呮墽琛岀殑璁惧缁勪换鍔℃暟閲�
+ executor.setQueueCapacity(100);
+
+ // 绾跨▼鍚嶅墠缂�
+ executor.setThreadNamePrefix("DeviceGroupTask-");
+
+ // 绾跨▼绌洪棽鏃堕棿锛堢锛�
+ executor.setKeepAliveSeconds(60);
+
+ // 鎷掔粷绛栫暐锛氬綋绾跨▼姹犲拰闃熷垪閮芥弧鏃讹紝鐢辫皟鐢ㄧ嚎绋嬫墽琛�
+ executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
+
+ // 绛夊緟鎵�鏈変换鍔$粨鏉熷悗鍐嶅叧闂嚎绋嬫睜
+ executor.setWaitForTasksToCompleteOnShutdown(true);
+
+ // 绛夊緟鏃堕棿锛堢锛�
+ executor.setAwaitTerminationSeconds(60);
+
+ executor.initialize();
+
+ log.info("璁惧缁勪换鍔$嚎绋嬫睜鍒濆鍖栧畬鎴�: corePoolSize=5, maxPoolSize=20, queueCapacity=100");
+
+ return executor;
+ }
+}
+
diff --git a/mes-processes/mes-plcSend/src/main/java/com/mes/device/entity/DeviceConfig.java b/mes-processes/mes-plcSend/src/main/java/com/mes/device/entity/DeviceConfig.java
index 86584a9..e1e45ad 100644
--- a/mes-processes/mes-plcSend/src/main/java/com/mes/device/entity/DeviceConfig.java
+++ b/mes-processes/mes-plcSend/src/main/java/com/mes/device/entity/DeviceConfig.java
@@ -27,7 +27,7 @@
@TableField("device_id")
private String deviceId;
- @ApiModelProperty(value = "璁惧鍚嶇О", example = "涓婂ぇ杞﹁澶�1")
+ @ApiModelProperty(value = "璁惧鍚嶇О", example = "澶ц溅璁惧1")
@TableField("device_name")
private String deviceName;
@@ -35,7 +35,7 @@
@TableField("device_code")
private String deviceCode;
- @ApiModelProperty(value = "璁惧绫诲瀷", example = "涓婂ぇ杞�/澶х悊鐗�/鐜荤拑瀛樺偍")
+ @ApiModelProperty(value = "璁惧绫诲瀷", example = "澶ц溅璁惧/澶х悊鐗囩/鍗у紡缂撳瓨")
@TableField("device_type")
private String deviceType;
@@ -59,7 +59,7 @@
@TableField("plc_type")
private String plcType;
- @ApiModelProperty(value = "妯″潡鍚嶇О", example = "涓婂ぇ杞︽ā鍧�")
+ @ApiModelProperty(value = "妯″潡鍚嶇О", example = "澶ц溅璁惧妯″潡")
@TableField("module_name")
private String moduleName;
@@ -75,7 +75,7 @@
@TableField("config_json")
private String configJson;
- @ApiModelProperty(value = "璁惧鎻忚堪", example = "涓婂ぇ杞﹁澶�1")
+ @ApiModelProperty(value = "璁惧鎻忚堪", example = "澶ц溅璁惧1")
@TableField("description")
private String description;
@@ -108,9 +108,11 @@
// 璁惧绫诲瀷甯搁噺
public static final class DeviceType {
- public static final String LOAD_VEHICLE = "涓婂ぇ杞�"; // 涓婂ぇ杞�
- public static final String LARGE_GLASS = "澶х悊鐗�"; // 澶х悊鐗�
- public static final String GLASS_STORAGE = "鐜荤拑瀛樺偍"; // 鐜荤拑瀛樺偍
+ public static final String LOAD_VEHICLE = "澶ц溅璁惧"; // 澶ц溅璁惧
+ public static final String LARGE_GLASS = "澶х悊鐗囩"; // 澶х悊鐗囩
+ public static final String GLASS_STORAGE = "鍗у紡缂撳瓨"; // 鍗у紡缂撳瓨
+ public static final String WORKSTATION_SCANNER = "鍗ц浆绔嬫壂鐮�"; // 鍗ц浆绔嬫壂鐮佽澶�
+ public static final String WORKSTATION_TRANSFER = "鍗ц浆绔�"; // 鍗ц浆绔嬭澶�
}
// PLC绫诲瀷甯搁噺
diff --git a/mes-processes/mes-plcSend/src/main/java/com/mes/device/service/DevicePlcOperationService.java b/mes-processes/mes-plcSend/src/main/java/com/mes/device/service/DevicePlcOperationService.java
index a0fd53a..7df295c 100644
--- a/mes-processes/mes-plcSend/src/main/java/com/mes/device/service/DevicePlcOperationService.java
+++ b/mes-processes/mes-plcSend/src/main/java/com/mes/device/service/DevicePlcOperationService.java
@@ -1,6 +1,7 @@
package com.mes.device.service;
import com.mes.device.vo.DevicePlcVO;
+import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
@@ -11,6 +12,7 @@
* @author mes
* @since 2025-11-17
*/
+@Service
public interface DevicePlcOperationService {
DevicePlcVO.OperationResult triggerRequest(Long deviceId);
diff --git a/mes-processes/mes-plcSend/src/main/java/com/mes/device/service/impl/DeviceConfigServiceImpl.java b/mes-processes/mes-plcSend/src/main/java/com/mes/device/service/impl/DeviceConfigServiceImpl.java
index 1488243..292bd8f 100644
--- a/mes-processes/mes-plcSend/src/main/java/com/mes/device/service/impl/DeviceConfigServiceImpl.java
+++ b/mes-processes/mes-plcSend/src/main/java/com/mes/device/service/impl/DeviceConfigServiceImpl.java
@@ -135,9 +135,9 @@
// 璁惧绫诲瀷杩囨护
if (deviceType != null && !deviceType.trim().isEmpty()) {
- String convertedDeviceType = convertDeviceTypeFromString(deviceType);
- if (convertedDeviceType != null) {
- wrapper.eq(DeviceConfig::getDeviceType, convertedDeviceType);
+ List<String> convertedDeviceTypes = convertDeviceTypeFromString(deviceType);
+ if (convertedDeviceTypes != null && !convertedDeviceTypes.isEmpty()) {
+ wrapper.in(DeviceConfig::getDeviceType, convertedDeviceTypes);
}
}
@@ -315,24 +315,42 @@
/**
* 瀛楃涓茶浆鎹负璁惧绫诲瀷
*/
- private String convertDeviceTypeFromString(String deviceType) {
- if (deviceType == null) return null;
+ private List<String> convertDeviceTypeFromString(String deviceType) {
+ if (deviceType == null) {
+ return Collections.emptyList();
+ }
- switch (deviceType.trim().toLowerCase()) {
+ String normalized = deviceType.trim().toLowerCase();
+ switch (normalized) {
case "load_vehicle":
case "涓婂ぇ杞�":
+ case "涓婂ぇ杞﹁澶�":
+ case "澶ц溅璁惧":
case "1":
- return DeviceConfig.DeviceType.LOAD_VEHICLE;
+ return Arrays.asList(
+ DeviceConfig.DeviceType.LOAD_VEHICLE,
+ "澶ц溅璁惧"
+ );
case "large_glass":
case "澶х悊鐗�":
+ case "澶х悊鐗囩":
case "2":
- return DeviceConfig.DeviceType.LARGE_GLASS;
+ return Arrays.asList(
+ DeviceConfig.DeviceType.LARGE_GLASS,
+ "澶х悊鐗囩"
+ );
case "glass_storage":
case "鐜荤拑瀛樺偍":
+ case "鍗у紡缂撳瓨":
+ case "鐜荤拑瀛樺偍璁惧":
case "3":
- return DeviceConfig.DeviceType.GLASS_STORAGE;
+ return Arrays.asList(
+ DeviceConfig.DeviceType.GLASS_STORAGE,
+ "鍗у紡缂撳瓨",
+ "鐜荤拑瀛樺偍璁惧"
+ );
default:
- return null;
+ return Collections.emptyList();
}
}
@@ -624,12 +642,12 @@
}
// 璁惧绫诲瀷杩囨护
- if (deviceType != null && !deviceType.trim().isEmpty()) {
- String convertedDeviceType = convertDeviceTypeFromString(deviceType);
- if (convertedDeviceType != null) {
- wrapper.eq(DeviceConfig::getDeviceType, convertedDeviceType);
- }
+ if (deviceType != null && !deviceType.trim().isEmpty()) {
+ List<String> convertedDeviceTypes = convertDeviceTypeFromString(deviceType);
+ if (convertedDeviceTypes != null && !convertedDeviceTypes.isEmpty()) {
+ wrapper.in(DeviceConfig::getDeviceType, convertedDeviceTypes);
}
+ }
// 璁惧鐘舵�佽繃婊�
if (deviceStatus != null && !deviceStatus.trim().isEmpty()) {
diff --git a/mes-processes/mes-plcSend/src/main/java/com/mes/device/service/impl/DeviceCoordinationServiceImpl.java b/mes-processes/mes-plcSend/src/main/java/com/mes/device/service/impl/DeviceCoordinationServiceImpl.java
index 44513f8..4414ee3 100644
--- a/mes-processes/mes-plcSend/src/main/java/com/mes/device/service/impl/DeviceCoordinationServiceImpl.java
+++ b/mes-processes/mes-plcSend/src/main/java/com/mes/device/service/impl/DeviceCoordinationServiceImpl.java
@@ -80,13 +80,13 @@
// 鏍规嵁璁惧绫诲瀷锛屾彁鍙栧叧閿暟鎹苟鏇存柊涓婁笅鏂�
if (DeviceConfig.DeviceType.LOAD_VEHICLE.equals(fromDevice.getDeviceType())) {
- // 涓婂ぇ杞﹁澶囧畬鎴愶紝浼犻�掔幓鐠僆D鍒楄〃
+ // 澶ц溅璁惧瀹屾垚锛屼紶閫掔幓鐠僆D鍒楄〃
Object glassIds = data.get("glassIds");
if (glassIds instanceof List) {
@SuppressWarnings("unchecked")
List<String> ids = (List<String>) glassIds;
context.setLoadedGlassIds(new ArrayList<>(ids));
- log.info("涓婂ぇ杞﹁澶囨暟鎹紶閫�: fromDevice={}, toDevice={}, glassIds={}",
+ log.info("澶ц溅璁惧鏁版嵁浼犻��: fromDevice={}, toDevice={}, glassIds={}",
fromDevice.getDeviceCode(), toDevice.getDeviceCode(), ids);
}
} else if (DeviceConfig.DeviceType.LARGE_GLASS.equals(fromDevice.getDeviceType())) {
@@ -147,13 +147,13 @@
// 妫�鏌ヨ澶囩被鍨嬬壒瀹氱殑渚濊禆
String deviceType = device.getDeviceType();
if (DeviceConfig.DeviceType.LARGE_GLASS.equals(deviceType)) {
- // 澶х悊鐗囪澶囬渶瑕佷笂澶ц溅璁惧鍏堝畬鎴�
+ // 澶х悊鐗囪澶囬渶瑕佸ぇ杞﹁澶囧厛瀹屾垚
List<String> loadedGlassIds = context.getSafeLoadedGlassIds();
if (CollectionUtils.isEmpty(loadedGlassIds)) {
- missingDependencies.add("涓婂ぇ杞﹁澶囨湭瀹屾垚锛岀己灏戠幓鐠僆D鍒楄〃");
+ missingDependencies.add("澶ц溅璁惧鏈畬鎴愶紝缂哄皯鐜荤拑ID鍒楄〃");
}
} else if (DeviceConfig.DeviceType.GLASS_STORAGE.equals(deviceType)) {
- // 鐜荤拑瀛樺偍璁惧闇�瑕佸ぇ鐞嗙墖璁惧鍏堝畬鎴愶紙浼樺厛锛夛紝鎴栦笂澶ц溅璁惧瀹屾垚
+ // 鐜荤拑瀛樺偍璁惧闇�瑕佸ぇ鐞嗙墖璁惧鍏堝畬鎴愶紙浼樺厛锛夛紝鎴栧ぇ杞﹁澶囧畬鎴�
List<String> processedGlassIds = context.getSafeProcessedGlassIds();
List<String> loadedGlassIds = context.getSafeLoadedGlassIds();
if (CollectionUtils.isEmpty(processedGlassIds) && CollectionUtils.isEmpty(loadedGlassIds)) {
diff --git a/mes-processes/mes-plcSend/src/main/java/com/mes/device/service/impl/DevicePlcOperationServiceImpl.java b/mes-processes/mes-plcSend/src/main/java/com/mes/device/service/impl/DevicePlcOperationServiceImpl.java
index dc65a65..ebac164 100644
--- a/mes-processes/mes-plcSend/src/main/java/com/mes/device/service/impl/DevicePlcOperationServiceImpl.java
+++ b/mes-processes/mes-plcSend/src/main/java/com/mes/device/service/impl/DevicePlcOperationServiceImpl.java
@@ -40,15 +40,27 @@
private final PlcTestWriteService plcTestWriteService;
private final ObjectMapper objectMapper;
- public enum PlcOperationType {
+ public static enum PlcOperationType {
+ /** PLC璇锋眰鎿嶄綔 */
REQUEST("PLC璇锋眰", "PLC 璇锋眰鍙戦�佹垚鍔�", "PLC 璇锋眰鍙戦�佸け璐�"),
+ /** PLC姹囨姤鎿嶄綔 */
REPORT("PLC姹囨姤", "PLC 姹囨姤妯℃嫙鎴愬姛", "PLC 姹囨姤妯℃嫙澶辫触"),
+ /** PLC閲嶇疆鎿嶄綔 */
RESET("PLC閲嶇疆", "PLC 鐘舵�佸凡閲嶇疆", "PLC 鐘舵�侀噸缃け璐�");
+ /** 鎿嶄綔鏄剧ず鍚嶇О */
private final String display;
+ /** 鎿嶄綔鎴愬姛鎻愮ず淇℃伅 */
private final String successMsg;
+ /** 鎿嶄綔澶辫触鎻愮ず淇℃伅 */
private final String failedMsg;
+ /**
+ * 鏋勯�犳柟娉�
+ * @param display 鎿嶄綔鏄剧ず鍚嶇О
+ * @param successMsg 鎴愬姛鎻愮ず淇℃伅
+ * @param failedMsg 澶辫触鎻愮ず淇℃伅
+ */
PlcOperationType(String display, String successMsg, String failedMsg) {
this.display = display;
this.successMsg = successMsg;
@@ -103,7 +115,7 @@
return DevicePlcVO.StatusInfo.builder()
.deviceId(deviceId)
.deviceName("鏈煡璁惧")
- .data(Collections.emptyMap())
+ .fieldValues(Collections.emptyMap())
.timestamp(LocalDateTime.now())
.build();
}
@@ -115,7 +127,7 @@
.deviceName(device.getDeviceName())
.deviceCode(device.getDeviceCode())
.projectId(String.valueOf(device.getProjectId()))
- .data(data)
+ .fieldValues(data)
.timestamp(LocalDateTime.now())
.build();
} catch (Exception e) {
@@ -125,7 +137,7 @@
.deviceName(device.getDeviceName())
.deviceCode(device.getDeviceCode())
.projectId(null)
- .data(Collections.emptyMap())
+ .fieldValues(Collections.emptyMap())
.timestamp(LocalDateTime.now())
.build();
}
diff --git a/mes-processes/mes-plcSend/src/main/java/com/mes/device/vo/DevicePlcVO.java b/mes-processes/mes-plcSend/src/main/java/com/mes/device/vo/DevicePlcVO.java
index 873c2e0..69919ec 100644
--- a/mes-processes/mes-plcSend/src/main/java/com/mes/device/vo/DevicePlcVO.java
+++ b/mes-processes/mes-plcSend/src/main/java/com/mes/device/vo/DevicePlcVO.java
@@ -36,6 +36,7 @@
private Boolean success;
private String message;
private LocalDateTime timestamp;
+ private Map<String, Object> data;
}
/**
@@ -51,7 +52,7 @@
private String deviceName;
private String deviceCode;
private String projectId;
- private Map<String, Object> data;
+ private Map<String, Object> fieldValues;
private LocalDateTime timestamp;
}
}
diff --git a/mes-processes/mes-plcSend/src/main/java/com/mes/interaction/DeviceInteractionRegistry.java b/mes-processes/mes-plcSend/src/main/java/com/mes/interaction/DeviceInteractionRegistry.java
index fc4cc8c..3dc7ddb 100644
--- a/mes-processes/mes-plcSend/src/main/java/com/mes/interaction/DeviceInteractionRegistry.java
+++ b/mes-processes/mes-plcSend/src/main/java/com/mes/interaction/DeviceInteractionRegistry.java
@@ -10,6 +10,7 @@
/**
* 浜や簰娉ㄥ唽涓績
+ * @author huang
*/
@Slf4j
@Component
diff --git a/mes-processes/mes-plcSend/src/main/java/com/mes/interaction/DeviceLogicHandler.java b/mes-processes/mes-plcSend/src/main/java/com/mes/interaction/DeviceLogicHandler.java
index 9566222..4a39cc7 100644
--- a/mes-processes/mes-plcSend/src/main/java/com/mes/interaction/DeviceLogicHandler.java
+++ b/mes-processes/mes-plcSend/src/main/java/com/mes/interaction/DeviceLogicHandler.java
@@ -17,7 +17,7 @@
/**
* 鑾峰彇璁惧绫诲瀷锛堢敤浜庡尮閰嶅鐞嗗櫒锛�
*
- * @return 璁惧绫诲瀷锛屽锛�"涓婂ぇ杞�"銆�"澶х悊鐗�"銆�"鐜荤拑瀛樺偍"
+ * @return 璁惧绫诲瀷锛屽锛�"澶ц溅璁惧"銆�"澶х悊鐗囩"銆�"鍗у紡缂撳瓨"
*/
String getDeviceType();
diff --git a/mes-processes/mes-plcSend/src/main/java/com/mes/interaction/flow/GlassStorageInteraction.java b/mes-processes/mes-plcSend/src/main/java/com/mes/interaction/flow/GlassStorageInteraction.java
index 28b8569..257a3fa 100644
--- a/mes-processes/mes-plcSend/src/main/java/com/mes/interaction/flow/GlassStorageInteraction.java
+++ b/mes-processes/mes-plcSend/src/main/java/com/mes/interaction/flow/GlassStorageInteraction.java
@@ -30,7 +30,7 @@
return InteractionResult.fail("璁惧閰嶇疆涓嶅瓨鍦�");
}
- // 浼樺厛浣跨敤澶勭悊鍚庣殑鐜荤拑ID锛屽鏋滄病鏈夊垯浣跨敤涓婂ぇ杞︾殑鐜荤拑ID
+ // 浼樺厛浣跨敤澶勭悊鍚庣殑鐜荤拑ID锛屽鏋滄病鏈夊垯浣跨敤澶ц溅璁惧鐨勭幓鐠僆D
List<String> processed = context.getProcessedGlassIds();
if (CollectionUtils.isEmpty(processed)) {
processed = context.getLoadedGlassIds();
diff --git a/mes-processes/mes-plcSend/src/main/java/com/mes/interaction/flow/LargeGlassInteraction.java b/mes-processes/mes-plcSend/src/main/java/com/mes/interaction/flow/LargeGlassInteraction.java
index c0ded0b..3a9f805 100644
--- a/mes-processes/mes-plcSend/src/main/java/com/mes/interaction/flow/LargeGlassInteraction.java
+++ b/mes-processes/mes-plcSend/src/main/java/com/mes/interaction/flow/LargeGlassInteraction.java
@@ -31,14 +31,14 @@
return InteractionResult.fail("璁惧閰嶇疆涓嶅瓨鍦�");
}
- // 妫�鏌ヤ笂澶ц溅鏄惁瀹屾垚
+ // 妫�鏌ュぇ杞﹁澶囨槸鍚﹀畬鎴�
Object source = context.getSharedData().get("glassesFromVehicle");
List<String> glassQueue = castList(source);
if (CollectionUtils.isEmpty(glassQueue)) {
// 涔熷皾璇曚粠涓婁笅鏂囪幏鍙�
glassQueue = context.getLoadedGlassIds();
if (CollectionUtils.isEmpty(glassQueue)) {
- return InteractionResult.waitResult("绛夊緟涓婂ぇ杞﹁緭鍑�", null);
+ return InteractionResult.waitResult("绛夊緟澶ц溅璁惧杈撳嚭", null);
}
}
diff --git a/mes-processes/mes-plcSend/src/main/java/com/mes/interaction/flow/LoadVehicleInteraction.java b/mes-processes/mes-plcSend/src/main/java/com/mes/interaction/flow/LoadVehicleInteraction.java
deleted file mode 100644
index f9a7934..0000000
--- a/mes-processes/mes-plcSend/src/main/java/com/mes/interaction/flow/LoadVehicleInteraction.java
+++ /dev/null
@@ -1,95 +0,0 @@
-package com.mes.interaction.flow;
-
-import com.mes.device.entity.DeviceConfig;
-import com.mes.device.service.DeviceInteractionService;
-import com.mes.device.vo.DevicePlcVO;
-import com.mes.interaction.DeviceInteraction;
-import com.mes.interaction.base.InteractionContext;
-import com.mes.interaction.base.InteractionResult;
-import lombok.RequiredArgsConstructor;
-import org.springframework.stereotype.Component;
-import org.springframework.util.CollectionUtils;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-/**
- * 涓婂ぇ杞︿氦浜掑疄鐜�
- */
-@Component
-@RequiredArgsConstructor
-public class LoadVehicleInteraction implements DeviceInteraction {
-
- private final DeviceInteractionService deviceInteractionService;
-
- @Override
- public String getDeviceType() {
- return DeviceConfig.DeviceType.LOAD_VEHICLE;
- }
-
- @Override
- public InteractionResult execute(InteractionContext context) {
- try {
- // 鍓嶇疆鏉′欢楠岃瘉
- if (context.getCurrentDevice() == null) {
- return InteractionResult.fail("璁惧閰嶇疆涓嶅瓨鍦�");
- }
-
- List<String> glassIds = context.getParameters().getGlassIds();
- if (CollectionUtils.isEmpty(glassIds)) {
- return InteractionResult.waitResult("鏈彁渚涚幓鐠僆D锛岀瓑寰呰緭鍏�", null);
- }
-
- // 楠岃瘉鐜荤拑ID鏍煎紡
- for (String glassId : glassIds) {
- if (glassId == null || glassId.trim().isEmpty()) {
- return InteractionResult.fail("鐜荤拑ID涓嶈兘涓虹┖");
- }
- }
-
- // 鏋勫缓PLC鍐欏叆鍙傛暟
- Map<String, Object> params = new HashMap<>();
- params.put("glassIds", glassIds);
- params.put("positionCode", context.getParameters().getPositionCode());
- params.put("positionValue", context.getParameters().getPositionValue());
- params.put("triggerRequest", true);
-
- // 鎵ц瀹為檯鐨凱LC鍐欏叆鎿嶄綔
- DevicePlcVO.OperationResult plcResult = deviceInteractionService.executeOperation(
- context.getCurrentDevice().getId(),
- "feedGlass",
- params
- );
-
- // 妫�鏌LC鍐欏叆缁撴灉
- if (plcResult == null || !Boolean.TRUE.equals(plcResult.getSuccess())) {
- String errorMsg = plcResult != null ? plcResult.getMessage() : "PLC鍐欏叆鎿嶄綔杩斿洖绌虹粨鏋�";
- return InteractionResult.fail("PLC鍐欏叆澶辫触: " + errorMsg);
- }
-
- // 鎵ц涓婂ぇ杞︽搷浣滐紙鏁版嵁娴佽浆锛�
- List<String> copied = new ArrayList<>(glassIds);
- context.setLoadedGlassIds(copied);
- context.getSharedData().put("glassesFromVehicle", copied);
- context.getSharedData().put("loadVehicleTime", System.currentTimeMillis());
-
- // 鍚庣疆鏉′欢妫�鏌�
- if (context.getLoadedGlassIds().isEmpty()) {
- return InteractionResult.fail("涓婂ぇ杞︽搷浣滃け璐ワ細鐜荤拑ID鍒楄〃涓虹┖");
- }
-
- Map<String, Object> data = new HashMap<>();
- data.put("loaded", copied);
- data.put("glassCount", copied.size());
- data.put("deviceId", context.getCurrentDevice().getId());
- data.put("deviceCode", context.getCurrentDevice().getDeviceCode());
- data.put("plcResult", plcResult.getMessage());
- return InteractionResult.success(data);
- } catch (Exception e) {
- return InteractionResult.fail("涓婂ぇ杞︿氦浜掓墽琛屽紓甯�: " + e.getMessage());
- }
- }
-}
-
diff --git a/mes-processes/mes-plcSend/src/main/java/com/mes/interaction/impl/LargeGlassLogicHandler.java b/mes-processes/mes-plcSend/src/main/java/com/mes/interaction/impl/LargeGlassLogicHandler.java
deleted file mode 100644
index 284fefa..0000000
--- a/mes-processes/mes-plcSend/src/main/java/com/mes/interaction/impl/LargeGlassLogicHandler.java
+++ /dev/null
@@ -1,195 +0,0 @@
-package com.mes.interaction.impl;
-
-import com.mes.device.entity.DeviceConfig;
-import com.mes.interaction.BaseDeviceLogicHandler;
-import com.mes.device.service.DevicePlcOperationService;
-import com.mes.device.vo.DevicePlcVO;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.stereotype.Component;
-
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * 澶х悊鐗囪澶囬�昏緫澶勭悊鍣�
- *
- * @author mes
- * @since 2025-01-XX
- */
-@Slf4j
-@Component
-public class LargeGlassLogicHandler extends BaseDeviceLogicHandler {
-
- public LargeGlassLogicHandler(DevicePlcOperationService devicePlcOperationService) {
- super(devicePlcOperationService);
- }
-
- @Override
- public String getDeviceType() {
- return DeviceConfig.DeviceType.LARGE_GLASS;
- }
-
- @Override
- protected DevicePlcVO.OperationResult doExecute(
- DeviceConfig deviceConfig,
- String operation,
- Map<String, Object> params,
- Map<String, Object> logicParams) {
-
- log.info("鎵ц澶х悊鐗囪澶囨搷浣�: deviceId={}, operation={}", deviceConfig.getId(), operation);
-
- switch (operation) {
- case "processGlass":
- return handleProcessGlass(deviceConfig, params, logicParams);
- case "triggerRequest":
- return handleTriggerRequest(deviceConfig, params, logicParams);
- case "triggerReport":
- return handleTriggerReport(deviceConfig, params, logicParams);
- case "reset":
- return handleReset(deviceConfig, params, logicParams);
- default:
- log.warn("涓嶆敮鎸佺殑鎿嶄綔绫诲瀷: {}", operation);
- return DevicePlcVO.OperationResult.builder()
- .success(false)
- .message("涓嶆敮鎸佺殑鎿嶄綔: " + operation)
- .build();
- }
- }
-
- /**
- * 澶勭悊鐜荤拑鍔犲伐鎿嶄綔
- */
- private DevicePlcVO.OperationResult handleProcessGlass(
- DeviceConfig deviceConfig,
- Map<String, Object> params,
- Map<String, Object> logicParams) {
-
- // 浠庨�昏緫鍙傛暟涓幏鍙栭厤缃�
- Integer glassSize = getLogicParam(logicParams, "glassSize", 2000);
- Integer processingTime = getLogicParam(logicParams, "processingTime", 5000);
- Boolean autoProcess = getLogicParam(logicParams, "autoProcess", true);
-
- // 浠庤繍琛屾椂鍙傛暟涓幏鍙栨暟鎹�
- String glassId = (String) params.get("glassId");
- Integer processType = (Integer) params.get("processType");
- Boolean triggerRequest = (Boolean) params.getOrDefault("triggerRequest", autoProcess);
-
- // 鏋勫缓鍐欏叆鏁版嵁
- Map<String, Object> payload = new HashMap<>();
-
- if (glassId != null) {
- payload.put("plcGlassId", glassId);
- }
-
- if (processType != null) {
- payload.put("processType", processType);
- }
-
- // 鑷姩瑙﹀彂璇锋眰
- if (triggerRequest != null && triggerRequest) {
- payload.put("plcRequest", 1);
- }
-
- log.info("澶х悊鐗囩幓鐠冨姞宸�: deviceId={}, glassId={}, processType={}",
- deviceConfig.getId(), glassId, processType);
-
- return devicePlcOperationService.writeFields(
- deviceConfig.getId(),
- payload,
- "澶х悊鐗�-鐜荤拑鍔犲伐"
- );
- }
-
- /**
- * 澶勭悊瑙﹀彂璇锋眰鎿嶄綔
- */
- private DevicePlcVO.OperationResult handleTriggerRequest(
- DeviceConfig deviceConfig,
- Map<String, Object> params,
- Map<String, Object> logicParams) {
-
- Map<String, Object> payload = new HashMap<>();
- payload.put("plcRequest", 1);
-
- log.info("澶х悊鐗囪Е鍙戣姹�: deviceId={}", deviceConfig.getId());
- return devicePlcOperationService.writeFields(
- deviceConfig.getId(),
- payload,
- "澶х悊鐗�-瑙﹀彂璇锋眰"
- );
- }
-
- /**
- * 澶勭悊瑙﹀彂姹囨姤鎿嶄綔
- */
- private DevicePlcVO.OperationResult handleTriggerReport(
- DeviceConfig deviceConfig,
- Map<String, Object> params,
- Map<String, Object> logicParams) {
-
- Map<String, Object> payload = new HashMap<>();
- payload.put("plcReport", 1);
-
- log.info("澶х悊鐗囪Е鍙戞眹鎶�: deviceId={}", deviceConfig.getId());
- return devicePlcOperationService.writeFields(
- deviceConfig.getId(),
- payload,
- "澶х悊鐗�-瑙﹀彂姹囨姤"
- );
- }
-
- /**
- * 澶勭悊閲嶇疆鎿嶄綔
- */
- private DevicePlcVO.OperationResult handleReset(
- DeviceConfig deviceConfig,
- Map<String, Object> params,
- Map<String, Object> logicParams) {
-
- Map<String, Object> payload = new HashMap<>();
- payload.put("plcRequest", 0);
- payload.put("plcReport", 0);
-
- log.info("澶х悊鐗囬噸缃�: deviceId={}", deviceConfig.getId());
- return devicePlcOperationService.writeFields(
- deviceConfig.getId(),
- payload,
- "澶х悊鐗�-閲嶇疆"
- );
- }
-
- @Override
- public String validateLogicParams(DeviceConfig deviceConfig) {
- Map<String, Object> logicParams = parseLogicParams(deviceConfig);
-
- // 楠岃瘉蹇呭~鍙傛暟
- Integer glassSize = getLogicParam(logicParams, "glassSize", null);
- if (glassSize != null && glassSize <= 0) {
- return "鐜荤拑灏哄(glassSize)蹇呴』澶т簬0";
- }
-
- Integer processingTime = getLogicParam(logicParams, "processingTime", null);
- if (processingTime != null && processingTime < 0) {
- return "澶勭悊鏃堕棿(processingTime)涓嶈兘涓鸿礋鏁�";
- }
-
- return null; // 楠岃瘉閫氳繃
- }
-
- @Override
- public String getDefaultLogicParams() {
- Map<String, Object> defaultParams = new HashMap<>();
- defaultParams.put("glassSize", 2000);
- defaultParams.put("processingTime", 5000);
- defaultParams.put("autoProcess", true);
- defaultParams.put("maxRetryCount", 3);
-
- try {
- return objectMapper.writeValueAsString(defaultParams);
- } catch (Exception e) {
- log.error("鐢熸垚榛樿閫昏緫鍙傛暟澶辫触", e);
- return "{}";
- }
- }
-}
-
diff --git a/mes-processes/mes-plcSend/src/main/java/com/mes/interaction/impl/LoadVehicleLogicHandler.java b/mes-processes/mes-plcSend/src/main/java/com/mes/interaction/impl/LoadVehicleLogicHandler.java
deleted file mode 100644
index ec1d8f2..0000000
--- a/mes-processes/mes-plcSend/src/main/java/com/mes/interaction/impl/LoadVehicleLogicHandler.java
+++ /dev/null
@@ -1,451 +0,0 @@
-package com.mes.interaction.impl;
-
-import com.mes.device.entity.DeviceConfig;
-import com.mes.interaction.BaseDeviceLogicHandler;
-import com.mes.device.service.DevicePlcOperationService;
-import com.mes.device.service.GlassInfoService;
-import com.mes.device.vo.DevicePlcVO;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Qualifier;
-import org.springframework.stereotype.Component;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-
-/**
- * 涓婂ぇ杞﹁澶囬�昏緫澶勭悊鍣�
- *
- * @author mes
- * @since 2025-01-XX
- */
-@Slf4j
-@Component
-public class LoadVehicleLogicHandler extends BaseDeviceLogicHandler {
-
- private final GlassInfoService glassInfoService;
-
- public LoadVehicleLogicHandler(
- DevicePlcOperationService devicePlcOperationService,
- @Qualifier("deviceGlassInfoService") GlassInfoService glassInfoService) {
- super(devicePlcOperationService);
- this.glassInfoService = glassInfoService;
- }
-
- @Override
- public String getDeviceType() {
- return DeviceConfig.DeviceType.LOAD_VEHICLE;
- }
-
- @Override
- protected DevicePlcVO.OperationResult doExecute(
- DeviceConfig deviceConfig,
- String operation,
- Map<String, Object> params,
- Map<String, Object> logicParams) {
-
- log.info("鎵ц涓婂ぇ杞﹁澶囨搷浣�: deviceId={}, operation={}", deviceConfig.getId(), operation);
-
- switch (operation) {
- case "feedGlass":
- return handleFeedGlass(deviceConfig, params, logicParams);
- case "triggerRequest":
- return handleTriggerRequest(deviceConfig, params, logicParams);
- case "triggerReport":
- return handleTriggerReport(deviceConfig, params, logicParams);
- case "reset":
- return handleReset(deviceConfig, params, logicParams);
- case "clearGlass":
- case "clearPlc":
- case "clear":
- return handleClearGlass(deviceConfig, params, logicParams);
- default:
- log.warn("涓嶆敮鎸佺殑鎿嶄綔绫诲瀷: {}", operation);
- return DevicePlcVO.OperationResult.builder()
- .success(false)
- .message("涓嶆敮鎸佺殑鎿嶄綔: " + operation)
- .build();
- }
- }
-
- /**
- * 澶勭悊鐜荤拑涓婃枡鎿嶄綔
- */
- private DevicePlcVO.OperationResult handleFeedGlass(
- DeviceConfig deviceConfig,
- Map<String, Object> params,
- Map<String, Object> logicParams) {
-
- // 浠庨�昏緫鍙傛暟涓幏鍙栭厤缃紙浠� extraParams.deviceLogic 璇诲彇锛�
- Integer vehicleCapacity = getLogicParam(logicParams, "vehicleCapacity", 6000);
- Integer glassIntervalMs = getLogicParam(logicParams, "glassIntervalMs", 1000);
- Boolean autoFeed = getLogicParam(logicParams, "autoFeed", true);
- Integer maxRetryCount = getLogicParam(logicParams, "maxRetryCount", 5);
-
- // 浠庤繍琛屾椂鍙傛暟涓幏鍙栨暟鎹紙浠庢帴鍙h皟鐢ㄦ椂浼犲叆锛�
- List<GlassInfo> glassInfos = extractGlassInfos(params);
- if (glassInfos.isEmpty()) {
- return DevicePlcVO.OperationResult.builder()
- .success(false)
- .message("鏈彁渚涙湁鏁堢殑鐜荤拑淇℃伅")
- .build();
- }
-
- String positionCode = (String) params.get("positionCode");
- Integer positionValue = (Integer) params.get("positionValue");
- Boolean triggerRequest = (Boolean) params.getOrDefault("triggerRequest", autoFeed);
-
- List<GlassInfo> plannedGlasses = planGlassLoading(glassInfos, vehicleCapacity,
- getLogicParam(logicParams, "defaultGlassLength", 2000));
- if (plannedGlasses.isEmpty()) {
- return DevicePlcVO.OperationResult.builder()
- .success(false)
- .message("褰撳墠鐜荤拑灏哄瓒呭嚭杞﹁締瀹归噺锛屾棤娉曡杞�")
- .build();
- }
-
- // 鏋勫缓鍐欏叆鏁版嵁
- Map<String, Object> payload = new HashMap<>();
-
- // 鍐欏叆鐜荤拑ID
- int plcSlots = Math.min(plannedGlasses.size(), 6);
- for (int i = 0; i < plcSlots; i++) {
- String fieldName = "plcGlassId" + (i + 1);
- payload.put(fieldName, plannedGlasses.get(i).getGlassId());
- }
- payload.put("plcGlassCount", plcSlots);
-
- // 鍐欏叆浣嶇疆淇℃伅
- if (positionValue != null) {
- payload.put("inPosition", positionValue);
- } else if (positionCode != null) {
- // 浠庝綅缃槧灏勪腑鑾峰彇浣嶇疆鍊�
- @SuppressWarnings("unchecked")
- Map<String, Integer> positionMapping = getLogicParam(logicParams, "positionMapping", new HashMap<>());
- Integer mappedValue = positionMapping.get(positionCode);
- if (mappedValue != null) {
- payload.put("inPosition", mappedValue);
- }
- }
-
- // 鑷姩瑙﹀彂璇锋眰瀛�
- if (triggerRequest != null && triggerRequest) {
- payload.put("plcRequest", 1);
- }
-
- String operationName = "涓婂ぇ杞�-鐜荤拑涓婃枡";
- if (positionCode != null) {
- operationName += "(" + positionCode + ")";
- }
-
- log.info("涓婂ぇ杞︾幓鐠冧笂鏂�: deviceId={}, glassCount={}, position={}, plannedGlassIds={}",
- deviceConfig.getId(), plcSlots, positionCode, plannedGlasses);
-
- if (glassIntervalMs != null && glassIntervalMs > 0) {
- try {
- Thread.sleep(glassIntervalMs);
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- }
- }
-
- return devicePlcOperationService.writeFields(deviceConfig.getId(), payload, operationName);
- }
-
- /**
- * 澶勭悊瑙﹀彂璇锋眰鎿嶄綔
- */
- private DevicePlcVO.OperationResult handleTriggerRequest(
- DeviceConfig deviceConfig,
- Map<String, Object> params,
- Map<String, Object> logicParams) {
-
- Map<String, Object> payload = new HashMap<>();
- payload.put("plcRequest", 1);
-
- log.info("涓婂ぇ杞﹁Е鍙戣姹�: deviceId={}", deviceConfig.getId());
- return devicePlcOperationService.writeFields(
- deviceConfig.getId(),
- payload,
- "涓婂ぇ杞�-瑙﹀彂璇锋眰"
- );
- }
-
- /**
- * 澶勭悊瑙﹀彂姹囨姤鎿嶄綔
- */
- private DevicePlcVO.OperationResult handleTriggerReport(
- DeviceConfig deviceConfig,
- Map<String, Object> params,
- Map<String, Object> logicParams) {
-
- Map<String, Object> payload = new HashMap<>();
- payload.put("plcReport", 1);
-
- log.info("涓婂ぇ杞﹁Е鍙戞眹鎶�: deviceId={}", deviceConfig.getId());
- return devicePlcOperationService.writeFields(
- deviceConfig.getId(),
- payload,
- "涓婂ぇ杞�-瑙﹀彂姹囨姤"
- );
- }
-
- /**
- * 澶勭悊閲嶇疆鎿嶄綔
- */
- private DevicePlcVO.OperationResult handleReset(
- DeviceConfig deviceConfig,
- Map<String, Object> params,
- Map<String, Object> logicParams) {
-
- Map<String, Object> payload = new HashMap<>();
- payload.put("plcRequest", 0);
- payload.put("plcReport", 0);
-
- log.info("涓婂ぇ杞﹂噸缃�: deviceId={}", deviceConfig.getId());
- return devicePlcOperationService.writeFields(
- deviceConfig.getId(),
- payload,
- "涓婂ぇ杞�-閲嶇疆"
- );
- }
-
- /**
- * 娓呯┖PLC涓殑鐜荤拑鏁版嵁
- */
- private DevicePlcVO.OperationResult handleClearGlass(
- DeviceConfig deviceConfig,
- Map<String, Object> params,
- Map<String, Object> logicParams) {
-
- Map<String, Object> payload = new HashMap<>();
-
- int slotCount = getLogicParam(logicParams, "glassSlotCount", 6);
- if (slotCount <= 0) {
- slotCount = 6;
- }
-
- List<String> slotFields = resolveGlassSlotFields(logicParams, slotCount);
- for (String field : slotFields) {
- payload.put(field, "");
- }
-
- payload.put("plcGlassCount", 0);
- payload.put("plcRequest", 0);
- payload.put("plcReport", 0);
-
- if (params != null && params.containsKey("positionValue")) {
- payload.put("inPosition", params.get("positionValue"));
- } else if (params != null && Boolean.TRUE.equals(params.get("clearPosition"))) {
- payload.put("inPosition", 0);
- }
-
- log.info("娓呯┖涓婂ぇ杞LC鐜荤拑鏁版嵁: deviceId={}, clearedSlots={}", deviceConfig.getId(), slotFields.size());
-
- return devicePlcOperationService.writeFields(
- deviceConfig.getId(),
- payload,
- "涓婂ぇ杞�-娓呯┖鐜荤拑鏁版嵁"
- );
- }
-
- private List<String> resolveGlassSlotFields(Map<String, Object> logicParams, int fallbackCount) {
- List<String> fields = new ArrayList<>();
- if (logicParams != null) {
- Object slotFieldConfig = logicParams.get("glassSlotFields");
- if (slotFieldConfig instanceof List) {
- List<?> configured = (List<?>) slotFieldConfig;
- for (Object item : configured) {
- if (item != null) {
- String fieldName = String.valueOf(item).trim();
- if (!fieldName.isEmpty()) {
- fields.add(fieldName);
- }
- }
- }
- }
- }
-
- if (fields.isEmpty()) {
- for (int i = 1; i <= fallbackCount; i++) {
- fields.add("plcGlassId" + i);
- }
- }
- return fields;
- }
-
- @Override
- public String validateLogicParams(DeviceConfig deviceConfig) {
- Map<String, Object> logicParams = parseLogicParams(deviceConfig);
-
- // 楠岃瘉蹇呭~鍙傛暟
- Integer vehicleCapacity = getLogicParam(logicParams, "vehicleCapacity", null);
- if (vehicleCapacity == null || vehicleCapacity <= 0) {
- return "杞﹁締瀹归噺(vehicleCapacity)蹇呴』澶т簬0";
- }
-
- Integer glassIntervalMs = getLogicParam(logicParams, "glassIntervalMs", null);
- if (glassIntervalMs != null && glassIntervalMs < 0) {
- return "鐜荤拑闂撮殧鏃堕棿(glassIntervalMs)涓嶈兘涓鸿礋鏁�";
- }
-
- return null; // 楠岃瘉閫氳繃
- }
-
- @Override
- public String getDefaultLogicParams() {
- Map<String, Object> defaultParams = new HashMap<>();
- defaultParams.put("vehicleCapacity", 6000);
- defaultParams.put("glassIntervalMs", 1000);
- defaultParams.put("autoFeed", true);
- defaultParams.put("maxRetryCount", 5);
- defaultParams.put("defaultGlassLength", 2000);
-
- Map<String, Integer> positionMapping = new HashMap<>();
- positionMapping.put("POS1", 1);
- positionMapping.put("POS2", 2);
- defaultParams.put("positionMapping", positionMapping);
-
- try {
- return objectMapper.writeValueAsString(defaultParams);
- } catch (Exception e) {
- log.error("鐢熸垚榛樿閫昏緫鍙傛暟澶辫触", e);
- return "{}";
- }
- }
-
- @SuppressWarnings("unchecked")
- private List<GlassInfo> extractGlassInfos(Map<String, Object> params) {
- List<GlassInfo> result = new ArrayList<>();
- Object rawGlassInfos = params.get("glassInfos");
- if (rawGlassInfos instanceof List) {
- List<?> list = (List<?>) rawGlassInfos;
- for (Object item : list) {
- GlassInfo info = convertToGlassInfo(item);
- if (info != null) {
- result.add(info);
- }
- }
- }
-
- if (result.isEmpty()) {
- List<String> glassIds = (List<String>) params.get("glassIds");
- if (glassIds != null && !glassIds.isEmpty()) {
- // 浠庢暟鎹簱鏌ヨ鐜荤拑灏哄
- Map<String, Integer> lengthMap = glassInfoService.getGlassLengthMap(glassIds);
- for (String glassId : glassIds) {
- Integer length = lengthMap.get(glassId);
- result.add(new GlassInfo(glassId, length));
- }
- log.debug("浠庢暟鎹簱鏌ヨ鐜荤拑灏哄: glassIds={}, lengthMap={}", glassIds, lengthMap);
- }
- }
- return result;
- }
-
- private GlassInfo convertToGlassInfo(Object source) {
- if (source instanceof GlassInfo) {
- return (GlassInfo) source;
- }
- if (source instanceof Map) {
- Map<?, ?> map = (Map<?, ?>) source;
- Object id = map.get("glassId");
- if (id == null) {
- id = map.get("id");
- }
- if (id == null) {
- return null;
- }
- Integer length = parseLength(map.get("length"));
- if (length == null) {
- length = parseLength(map.get("size"));
- }
- return new GlassInfo(String.valueOf(id), length);
- }
- if (source instanceof String) {
- return new GlassInfo((String) source, null);
- }
- return null;
- }
-
- private Integer parseLength(Object value) {
- if (value instanceof Number) {
- return ((Number) value).intValue();
- }
- if (value instanceof String) {
- try {
- return Integer.parseInt((String) value);
- } catch (NumberFormatException ignored) {
- }
- }
- return null;
- }
-
- private List<GlassInfo> planGlassLoading(List<GlassInfo> source,
- int vehicleCapacity,
- Integer defaultGlassLength) {
- List<GlassInfo> planned = new ArrayList<>();
- int usedLength = 0;
- int capacity = Math.max(vehicleCapacity, 1);
- int fallbackLength = defaultGlassLength != null && defaultGlassLength > 0 ? defaultGlassLength : 2000;
-
- for (GlassInfo info : source) {
- int length = info.getLength() != null && info.getLength() > 0 ? info.getLength() : fallbackLength;
- if (planned.isEmpty()) {
- planned.add(info.withLength(length));
- usedLength = length;
- continue;
- }
- if (usedLength + length <= capacity) {
- planned.add(info.withLength(length));
- usedLength += length;
- } else {
- break;
- }
- }
- return planned;
- }
-
- private static class GlassInfo {
- private final String glassId;
- private final Integer length;
-
- GlassInfo(String glassId, Integer length) {
- this.glassId = glassId;
- this.length = length;
- }
-
- public String getGlassId() {
- return glassId;
- }
-
- public Integer getLength() {
- return length;
- }
-
- public GlassInfo withLength(Integer newLength) {
- return new GlassInfo(this.glassId, newLength);
- }
-
- @Override
- public String toString() {
- return glassId;
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(glassId, length);
- }
-
- @Override
- public boolean equals(Object obj) {
- if (this == obj) return true;
- if (obj == null || getClass() != obj.getClass()) return false;
- GlassInfo other = (GlassInfo) obj;
- return Objects.equals(glassId, other.glassId) && Objects.equals(length, other.length);
- }
- }
-}
-
diff --git a/mes-processes/mes-plcSend/src/main/java/com/mes/interaction/largeglass/config/LargeGlassConfig.java b/mes-processes/mes-plcSend/src/main/java/com/mes/interaction/largeglass/config/LargeGlassConfig.java
new file mode 100644
index 0000000..883349e
--- /dev/null
+++ b/mes-processes/mes-plcSend/src/main/java/com/mes/interaction/largeglass/config/LargeGlassConfig.java
@@ -0,0 +1,37 @@
+package com.mes.interaction.largeglass.config;
+
+import com.mes.interaction.largeglass.model.GridRange;
+import lombok.Data;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 澶х悊鐗囩閰嶇疆
+ * 瀵瑰簲 extraParams.deviceLogic 涓殑瀛楁
+ */
+@Data
+public class LargeGlassConfig {
+
+ /**
+ * 鏍煎瓙鑼冨洿鍒楄〃
+ * 渚嬪锛氱涓�琛�1~52鏍硷紝绗簩琛�53~101鏍�
+ */
+ private List<GridRange> gridRanges = new ArrayList<>();
+
+ /**
+ * 姣忔牸闀垮害锛坢m锛�
+ */
+ private Integer gridLength = 2000;
+
+ /**
+ * 姣忔牸瀹藉害锛坢m锛�
+ */
+ private Integer gridWidth = 1500;
+
+ /**
+ * 姣忔牸鍘氬害锛坢m锛�
+ */
+ private Integer gridThickness = 5;
+}
+
diff --git a/mes-processes/mes-plcSend/src/main/java/com/mes/interaction/largeglass/handler/LargeGlassLogicHandler.java b/mes-processes/mes-plcSend/src/main/java/com/mes/interaction/largeglass/handler/LargeGlassLogicHandler.java
new file mode 100644
index 0000000..e7bba0f
--- /dev/null
+++ b/mes-processes/mes-plcSend/src/main/java/com/mes/interaction/largeglass/handler/LargeGlassLogicHandler.java
@@ -0,0 +1,379 @@
+package com.mes.interaction.largeglass.handler;
+
+import com.mes.device.entity.DeviceConfig;
+import com.mes.device.vo.DevicePlcVO;
+import com.mes.interaction.BaseDeviceLogicHandler;
+import com.mes.device.service.DevicePlcOperationService;
+import com.mes.interaction.largeglass.config.LargeGlassConfig;
+import com.mes.interaction.largeglass.model.GridRange;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import java.time.LocalDateTime;
+import java.util.*;
+
+/**
+ * 澶х悊鐗囩璁惧閫昏緫澶勭悊鍣�
+ * 璐熻矗鏍煎瓙鑼冨洿閰嶇疆銆佹瘡鏍煎昂瀵搁厤缃�侀�昏緫鍒ゆ柇绛�
+ * 涓嶆秹鍙奝LC鍐欏叆鎿嶄綔锛屽彧鐢ㄤ簬閫昏緫鍒ゆ柇鍜岄厤缃鐞�
+ */
+@Slf4j
+@Component
+public class LargeGlassLogicHandler extends BaseDeviceLogicHandler {
+
+ public LargeGlassLogicHandler(DevicePlcOperationService devicePlcOperationService) {
+ super(devicePlcOperationService);
+ }
+
+ @Override
+ public String getDeviceType() {
+ return DeviceConfig.DeviceType.LARGE_GLASS;
+ }
+
+ @Override
+ protected DevicePlcVO.OperationResult doExecute(DeviceConfig deviceConfig,
+ String operation,
+ Map<String, Object> params,
+ Map<String, Object> logicParams) {
+ try {
+ LargeGlassConfig config = parseLargeGlassConfig(logicParams);
+
+ switch (operation) {
+ case "checkGrid":
+ case "validateGrid":
+ return handleCheckGrid(deviceConfig, params, config);
+ case "getGridInfo":
+ return handleGetGridInfo(deviceConfig, params, config);
+ case "findAvailableGrid":
+ return handleFindAvailableGrid(deviceConfig, params, config);
+ case "getGridPosition":
+ return handleGetGridPosition(deviceConfig, params, config);
+ default:
+ return buildResult(deviceConfig, operation, false,
+ "涓嶆敮鎸佺殑鎿嶄綔: " + operation);
+ }
+ } catch (Exception e) {
+ log.error("澶х悊鐗囩澶勭悊寮傚父: deviceId={}, operation={}",
+ deviceConfig.getId(), operation, e);
+ return buildResult(deviceConfig, operation, false,
+ "澶勭悊寮傚父: " + e.getMessage());
+ }
+ }
+
+ /**
+ * 妫�鏌ユ牸瀛愭槸鍚︽湁鏁�
+ */
+ private DevicePlcVO.OperationResult handleCheckGrid(
+ DeviceConfig deviceConfig,
+ Map<String, Object> params,
+ LargeGlassConfig config) {
+
+ Integer gridNumber = getIntegerParam(params, "gridNumber");
+ if (gridNumber == null) {
+ return buildResult(deviceConfig, "checkGrid", false,
+ "鏈彁渚涙牸瀛愮紪鍙�");
+ }
+
+ GridRange gridRange = findGridRange(gridNumber, config);
+ if (gridRange == null) {
+ return buildResult(deviceConfig, "checkGrid", false,
+ String.format("鏍煎瓙缂栧彿 %d 涓嶅湪閰嶇疆鑼冨洿鍐�", gridNumber));
+ }
+
+ return buildResult(deviceConfig, "checkGrid", true,
+ String.format("鏍煎瓙缂栧彿 %d 鏈夋晥锛屼綅浜庣 %d 琛岋紝鑼冨洿 %d~%d",
+ gridNumber, gridRange.getRow(), gridRange.getStart(), gridRange.getEnd()));
+ }
+
+ /**
+ * 鑾峰彇鏍煎瓙淇℃伅
+ */
+ private DevicePlcVO.OperationResult handleGetGridInfo(
+ DeviceConfig deviceConfig,
+ Map<String, Object> params,
+ LargeGlassConfig config) {
+
+ Integer gridNumber = getIntegerParam(params, "gridNumber");
+ if (gridNumber == null) {
+ return buildResult(deviceConfig, "getGridInfo", false,
+ "鏈彁渚涙牸瀛愮紪鍙�");
+ }
+
+ GridRange gridRange = findGridRange(gridNumber, config);
+ if (gridRange == null) {
+ return buildResult(deviceConfig, "getGridInfo", false,
+ String.format("鏍煎瓙缂栧彿 %d 涓嶅湪閰嶇疆鑼冨洿鍐�", gridNumber));
+ }
+
+ Map<String, Object> gridInfo = new HashMap<>();
+ gridInfo.put("gridNumber", gridNumber);
+ gridInfo.put("row", gridRange.getRow());
+ gridInfo.put("start", gridRange.getStart());
+ gridInfo.put("end", gridRange.getEnd());
+ gridInfo.put("length", config.getGridLength());
+ gridInfo.put("width", config.getGridWidth());
+ gridInfo.put("thickness", config.getGridThickness());
+
+ return buildResult(deviceConfig, "getGridInfo", true,
+ "鏍煎瓙淇℃伅鑾峰彇鎴愬姛", gridInfo);
+ }
+
+ /**
+ * 鏌ユ壘鍙敤鏍煎瓙
+ */
+ private DevicePlcVO.OperationResult handleFindAvailableGrid(
+ DeviceConfig deviceConfig,
+ Map<String, Object> params,
+ LargeGlassConfig config) {
+
+ // 杩欓噷鍙互鏍规嵁瀹為檯闇�姹傚疄鐜版煡鎵鹃�昏緫
+ // 渚嬪锛氭煡鎵剧涓�涓彲鐢ㄦ牸瀛愩�佹煡鎵炬寚瀹氳鍙敤鏍煎瓙绛�
+
+ List<GridRange> gridRanges = config.getGridRanges();
+ if (gridRanges == null || gridRanges.isEmpty()) {
+ return buildResult(deviceConfig, "findAvailableGrid", false,
+ "鏈厤缃牸瀛愯寖鍥�");
+ }
+
+ // 绠�鍗曞疄鐜帮細杩斿洖绗竴涓牸瀛愮殑璧峰浣嶇疆
+ GridRange firstRange = gridRanges.get(0);
+ Integer availableGrid = firstRange.getStart();
+
+ Map<String, Object> result = new HashMap<>();
+ result.put("availableGrid", availableGrid);
+ result.put("row", firstRange.getRow());
+
+ return buildResult(deviceConfig, "findAvailableGrid", true,
+ String.format("鎵惧埌鍙敤鏍煎瓙: %d", availableGrid), result);
+ }
+
+ /**
+ * 鏍规嵁鐩爣浣嶇疆鑾峰彇鏍煎瓙缂栧彿
+ */
+ private DevicePlcVO.OperationResult handleGetGridPosition(
+ DeviceConfig deviceConfig,
+ Map<String, Object> params,
+ LargeGlassConfig config) {
+
+ Integer targetPosition = getIntegerParam(params, "targetPosition");
+ if (targetPosition == null) {
+ return buildResult(deviceConfig, "getGridPosition", false,
+ "鏈彁渚涚洰鏍囦綅缃�");
+ }
+
+ // 鏌ユ壘鐩爣浣嶇疆鍦ㄥ摢涓牸瀛愯寖鍥村唴
+ GridRange gridRange = findGridRangeByPosition(targetPosition, config);
+ if (gridRange == null) {
+ return buildResult(deviceConfig, "getGridPosition", false,
+ String.format("鐩爣浣嶇疆 %d 涓嶅湪浠讳綍鏍煎瓙鑼冨洿鍐�", targetPosition));
+ }
+
+ // 璁$畻鏍煎瓙缂栧彿锛堝亣璁句綅缃氨鏄牸瀛愮紪鍙凤紝鎴栨牴鎹疄闄呰鍒欒绠楋級
+ Integer gridNumber = targetPosition;
+
+ Map<String, Object> result = new HashMap<>();
+ result.put("gridNumber", gridNumber);
+ result.put("row", gridRange.getRow());
+ result.put("position", targetPosition);
+
+ return buildResult(deviceConfig, "getGridPosition", true,
+ String.format("鐩爣浣嶇疆 %d 瀵瑰簲鏍煎瓙缂栧彿 %d锛岀 %d 琛�",
+ targetPosition, gridNumber, gridRange.getRow()), result);
+ }
+
+ /**
+ * 瑙f瀽澶х悊鐗囩閰嶇疆
+ */
+ private LargeGlassConfig parseLargeGlassConfig(Map<String, Object> logicParams) {
+ LargeGlassConfig config = new LargeGlassConfig();
+
+ if (logicParams == null) {
+ return config;
+ }
+
+ // 瑙f瀽鏍煎瓙鑼冨洿閰嶇疆
+ @SuppressWarnings("unchecked")
+ List<Map<String, Object>> gridRangesConfig =
+ (List<Map<String, Object>>) logicParams.get("gridRanges");
+
+ if (gridRangesConfig != null) {
+ List<GridRange> gridRanges = new ArrayList<>();
+ for (Map<String, Object> rangeConfig : gridRangesConfig) {
+ Integer row = getIntegerValue(rangeConfig.get("row"));
+ Integer start = getIntegerValue(rangeConfig.get("start"));
+ Integer end = getIntegerValue(rangeConfig.get("end"));
+
+ if (row != null && start != null && end != null) {
+ gridRanges.add(new GridRange(row, start, end));
+ }
+ }
+ config.setGridRanges(gridRanges);
+ }
+
+ // 瑙f瀽姣忔牸灏哄
+ config.setGridLength(getLogicParam(logicParams, "gridLength", 2000)); // 榛樿2000mm
+ config.setGridWidth(getLogicParam(logicParams, "gridWidth", 1500)); // 榛樿1500mm
+ config.setGridThickness(getLogicParam(logicParams, "gridThickness", 5)); // 榛樿5mm
+
+ return config;
+ }
+
+ /**
+ * 鏌ユ壘鏍煎瓙鎵�鍦ㄧ殑鏍煎瓙鑼冨洿
+ */
+ private GridRange findGridRange(Integer gridNumber, LargeGlassConfig config) {
+ List<GridRange> gridRanges = config.getGridRanges();
+ if (gridRanges == null) {
+ return null;
+ }
+
+ for (GridRange range : gridRanges) {
+ if (gridNumber >= range.getStart() && gridNumber <= range.getEnd()) {
+ return range;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * 鏍规嵁浣嶇疆鏌ユ壘鏍煎瓙鑼冨洿
+ */
+ private GridRange findGridRangeByPosition(Integer position, LargeGlassConfig config) {
+ List<GridRange> gridRanges = config.getGridRanges();
+ if (gridRanges == null) {
+ return null;
+ }
+
+ for (GridRange range : gridRanges) {
+ if (position >= range.getStart() && position <= range.getEnd()) {
+ return range;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * 鑾峰彇鏁存暟鍙傛暟
+ */
+ private Integer getIntegerParam(Map<String, Object> params, String key) {
+ if (params == null) {
+ return null;
+ }
+ return getIntegerValue(params.get(key));
+ }
+
+ /**
+ * 鑾峰彇鏁存暟鍊�
+ */
+ private Integer getIntegerValue(Object value) {
+ if (value instanceof Number) {
+ return ((Number) value).intValue();
+ }
+ if (value == null) {
+ return null;
+ }
+ try {
+ return Integer.parseInt(String.valueOf(value));
+ } catch (NumberFormatException e) {
+ return null;
+ }
+ }
+
+ /**
+ * 鏋勫缓鎿嶄綔缁撴灉
+ */
+ private DevicePlcVO.OperationResult buildResult(DeviceConfig deviceConfig,
+ String operation,
+ boolean success,
+ String message) {
+ return buildResult(deviceConfig, operation, success, message, null);
+ }
+
+ /**
+ * 鏋勫缓鎿嶄綔缁撴灉锛堝甫鏁版嵁锛�
+ */
+ private DevicePlcVO.OperationResult buildResult(DeviceConfig deviceConfig,
+ String operation,
+ boolean success,
+ String message,
+ Map<String, Object> data) {
+ DevicePlcVO.OperationResult.OperationResultBuilder builder = DevicePlcVO.OperationResult.builder()
+ .deviceId(deviceConfig.getId())
+ .deviceName(deviceConfig.getDeviceName())
+ .deviceCode(deviceConfig.getDeviceCode())
+ .projectId(deviceConfig.getProjectId() != null ?
+ String.valueOf(deviceConfig.getProjectId()) : null)
+ .operation(operation)
+ .success(success)
+ .message(message)
+ .timestamp(LocalDateTime.now());
+
+ if (data != null) {
+ builder.data(data);
+ }
+
+ return builder.build();
+ }
+
+ @Override
+ public String validateLogicParams(DeviceConfig deviceConfig) {
+ Map<String, Object> logicParams = parseLogicParams(deviceConfig);
+ LargeGlassConfig config = parseLargeGlassConfig(logicParams);
+
+ // 楠岃瘉鏍煎瓙鑼冨洿閰嶇疆
+ List<GridRange> gridRanges = config.getGridRanges();
+ if (gridRanges == null || gridRanges.isEmpty()) {
+ return "蹇呴』閰嶇疆鑷冲皯涓�涓牸瀛愯寖鍥达紙gridRanges锛�";
+ }
+
+ // 楠岃瘉姣忔牸灏哄
+ if (config.getGridLength() == null || config.getGridLength() <= 0) {
+ return "鏍煎瓙闀垮害锛坓ridLength锛夊繀椤诲ぇ浜�0";
+ }
+ if (config.getGridWidth() == null || config.getGridWidth() <= 0) {
+ return "鏍煎瓙瀹藉害锛坓ridWidth锛夊繀椤诲ぇ浜�0";
+ }
+ if (config.getGridThickness() == null || config.getGridThickness() <= 0) {
+ return "鏍煎瓙鍘氬害锛坓ridThickness锛夊繀椤诲ぇ浜�0";
+ }
+
+ return null; // 楠岃瘉閫氳繃
+ }
+
+ @Override
+ public String getDefaultLogicParams() {
+ Map<String, Object> defaultParams = new HashMap<>();
+
+ // 榛樿鏍煎瓙鑼冨洿閰嶇疆锛氱涓�琛�1~52鏍硷紝绗簩琛�53~101鏍�
+ List<Map<String, Object>> gridRanges = new ArrayList<>();
+
+ Map<String, Object> row1 = new HashMap<>();
+ row1.put("row", 1);
+ row1.put("start", 1);
+ row1.put("end", 52);
+ gridRanges.add(row1);
+
+ Map<String, Object> row2 = new HashMap<>();
+ row2.put("row", 2);
+ row2.put("start", 53);
+ row2.put("end", 101);
+ gridRanges.add(row2);
+
+ defaultParams.put("gridRanges", gridRanges);
+
+ // 榛樿姣忔牸灏哄锛坢m锛�
+ defaultParams.put("gridLength", 2000); // 闀垮害2000mm
+ defaultParams.put("gridWidth", 1500); // 瀹藉害1500mm
+ defaultParams.put("gridThickness", 5); // 鍘氬害5mm
+
+ try {
+ return objectMapper.writeValueAsString(defaultParams);
+ } catch (Exception e) {
+ log.error("鐢熸垚榛樿閫昏緫鍙傛暟澶辫触", e);
+ return "{}";
+ }
+ }
+}
+
diff --git a/mes-processes/mes-plcSend/src/main/java/com/mes/interaction/largeglass/model/GridRange.java b/mes-processes/mes-plcSend/src/main/java/com/mes/interaction/largeglass/model/GridRange.java
new file mode 100644
index 0000000..457243f
--- /dev/null
+++ b/mes-processes/mes-plcSend/src/main/java/com/mes/interaction/largeglass/model/GridRange.java
@@ -0,0 +1,30 @@
+package com.mes.interaction.largeglass.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 鏍煎瓙鑼冨洿
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class GridRange {
+
+ /**
+ * 琛屽彿锛堢鍑犺锛�
+ */
+ private Integer row;
+
+ /**
+ * 璧峰鏍煎瓙缂栧彿
+ */
+ private Integer start;
+
+ /**
+ * 缁撴潫鏍煎瓙缂栧彿
+ */
+ private Integer end;
+}
+
diff --git a/mes-processes/mes-plcSend/src/main/java/com/mes/interaction/vehicle/coordination/VehicleCoordinationService.java b/mes-processes/mes-plcSend/src/main/java/com/mes/interaction/vehicle/coordination/VehicleCoordinationService.java
new file mode 100644
index 0000000..118e7f3
--- /dev/null
+++ b/mes-processes/mes-plcSend/src/main/java/com/mes/interaction/vehicle/coordination/VehicleCoordinationService.java
@@ -0,0 +1,189 @@
+package com.mes.interaction.vehicle.coordination;
+
+import com.mes.device.entity.DeviceConfig;
+import com.mes.device.service.DeviceConfigService;
+import com.mes.device.service.DeviceGroupRelationService;
+import com.mes.device.vo.DeviceGroupVO;
+import com.mes.interaction.vehicle.model.VehiclePath;
+import com.mes.interaction.vehicle.model.VehicleStatus;
+import com.mes.interaction.vehicle.model.VehicleState;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * 杞﹁締鍗忚皟鏈嶅姟
+ * 璐熻矗鍦ㄥ涓ぇ杞﹀疄渚嬩腑閫夋嫨鍜屽垎閰嶄换鍔�
+ *
+ * @author mes
+ * @since 2025-01-XX
+ */
+@Slf4j
+@Service
+public class VehicleCoordinationService {
+
+ @Autowired
+ private VehicleStatusManager statusManager;
+
+ @Autowired
+ private DeviceConfigService deviceConfigService;
+
+ @Autowired
+ private DeviceGroupRelationService deviceGroupRelationService;
+
+ /**
+ * 浠庤澶囩粍涓�夋嫨涓�涓彲鐢ㄧ殑澶ц溅瀹炰緥
+ *
+ * @param groupId 璁惧缁処D
+ * @return 鍙敤鐨勮澶囬厤缃紝濡傛灉娌℃湁鍙敤杞﹁締鍒欒繑鍥瀗ull
+ */
+ public DeviceConfig selectAvailableVehicle(Long groupId) {
+ if (groupId == null) {
+ log.warn("璁惧缁処D涓虹┖锛屾棤娉曢�夋嫨杞﹁締");
+ return null;
+ }
+
+ // 1. 鑾峰彇璁惧缁勪腑鐨勬墍鏈夎澶�
+ List<DeviceGroupVO.DeviceInfo> groupDevices = deviceGroupRelationService.getGroupDevices(groupId);
+ if (groupDevices == null || groupDevices.isEmpty()) {
+ log.warn("璁惧缁� {} 涓病鏈夎澶�", groupId);
+ return null;
+ }
+
+ // 2. 杩囨护鍑哄ぇ杞︾被鍨嬬殑璁惧
+ List<DeviceGroupVO.DeviceInfo> vehicles = groupDevices.stream()
+ .filter(d -> DeviceConfig.DeviceType.LOAD_VEHICLE.equals(d.getDeviceType()))
+ .collect(Collectors.toList());
+
+ if (vehicles.isEmpty()) {
+ log.warn("璁惧缁� {} 涓病鏈夊ぇ杞﹁澶�", groupId);
+ return null;
+ }
+
+ // 3. 杩囨护鍑哄彲鐢ㄧ殑杞﹁締
+ List<DeviceGroupVO.DeviceInfo> availableVehicles = vehicles.stream()
+ .filter(v -> {
+ String deviceId = v.getId() != null ? v.getId().toString() : null;
+ if (deviceId == null) {
+ deviceId = v.getDeviceCode();
+ }
+ return statusManager.isVehicleAvailable(deviceId);
+ })
+ .collect(Collectors.toList());
+
+ if (availableVehicles.isEmpty()) {
+ log.warn("璁惧缁� {} 涓病鏈夊彲鐢ㄧ殑澶ц溅璁惧", groupId);
+ return null;
+ }
+
+ // 4. 閫夋嫨绛栫暐锛堢畝鍗曠瓥鐣ワ細閫夋嫨绗竴涓彲鐢ㄧ殑锛屽彲浠ユ墿灞曚负鎸変紭鍏堢骇銆佽礋杞界瓑閫夋嫨锛�
+ DeviceGroupVO.DeviceInfo selected = availableVehicles.get(0);
+
+ // 5. 鑾峰彇瀹屾暣鐨勮澶囬厤缃�
+ DeviceConfig deviceConfig = null;
+ if (selected.getId() != null) {
+ deviceConfig = deviceConfigService.getDeviceById(selected.getId());
+ } else if (selected.getDeviceCode() != null) {
+ deviceConfig = deviceConfigService.getDeviceByCode(selected.getDeviceCode());
+ }
+
+ if (deviceConfig != null) {
+ log.info("閫夋嫨鍙敤杞﹁締: deviceId={}, deviceName={}",
+ deviceConfig.getDeviceId(), deviceConfig.getDeviceName());
+ }
+
+ return deviceConfig;
+ }
+
+ /**
+ * 妫�鏌ヨ矾寰勫啿绐�
+ *
+ * @param deviceId 璁惧ID
+ * @param plannedPath 璁″垝璺緞
+ * @return true琛ㄧず鏈夊啿绐侊紝false琛ㄧず鏃犲啿绐�
+ */
+ public boolean hasPathConflict(String deviceId, VehiclePath plannedPath) {
+ if (deviceId == null || plannedPath == null) {
+ return false;
+ }
+
+ // 鑾峰彇鎵�鏈夋墽琛屼腑鐨勮溅杈�
+ List<VehicleStatus> executingVehicles = statusManager.getExecutingVehicles();
+
+ // 鎺掗櫎鑷繁
+ executingVehicles = executingVehicles.stream()
+ .filter(v -> !v.getDeviceId().equals(deviceId))
+ .collect(Collectors.toList());
+
+ // 妫�鏌ヨ矾寰勬槸鍚﹀啿绐�
+ for (VehicleStatus vehicle : executingVehicles) {
+ if (vehicle.getCurrentTask() != null) {
+ VehiclePath existingPath = vehicle.getCurrentTask().getPlannedPath();
+ if (existingPath != null && plannedPath.conflictsWith(existingPath)) {
+ log.warn("妫�娴嬪埌璺緞鍐茬獊: deviceId={}, 涓� deviceId={} 鐨勮矾寰勫啿绐�",
+ deviceId, vehicle.getDeviceId());
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * 鑾峰彇璁惧缁勪腑鎵�鏈夊ぇ杞﹁澶�
+ *
+ * @param groupId 璁惧缁処D
+ * @return 澶ц溅璁惧鍒楄〃
+ */
+ public List<DeviceConfig> getVehiclesInGroup(Long groupId) {
+ if (groupId == null) {
+ return Collections.emptyList();
+ }
+
+ List<DeviceGroupVO.DeviceInfo> groupDevices = deviceGroupRelationService.getGroupDevices(groupId);
+ if (groupDevices == null || groupDevices.isEmpty()) {
+ return Collections.emptyList();
+ }
+
+ return groupDevices.stream()
+ .filter(d -> DeviceConfig.DeviceType.LOAD_VEHICLE.equals(d.getDeviceType()))
+ .map(d -> {
+ if (d.getId() != null) {
+ return deviceConfigService.getDeviceById(d.getId());
+ } else if (d.getDeviceCode() != null) {
+ return deviceConfigService.getDeviceByCode(d.getDeviceCode());
+ }
+ return null;
+ })
+ .filter(d -> d != null)
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * 鑾峰彇璁惧缁勪腑鎵�鏈夊彲鐢ㄧ殑澶ц溅璁惧
+ *
+ * @param groupId 璁惧缁処D
+ * @return 鍙敤鐨勫ぇ杞﹁澶囧垪琛�
+ */
+ public List<DeviceConfig> getAvailableVehiclesInGroup(Long groupId) {
+ return getVehiclesInGroup(groupId).stream()
+ .filter(v -> statusManager.isVehicleAvailable(v.getDeviceId()))
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * 妫�鏌ヨ澶囩粍涓槸鍚︽湁鍙敤鐨勫ぇ杞�
+ *
+ * @param groupId 璁惧缁処D
+ * @return true琛ㄧず鏈夊彲鐢ㄨ溅杈嗭紝false琛ㄧず娌℃湁
+ */
+ public boolean hasAvailableVehicle(Long groupId) {
+ return selectAvailableVehicle(groupId) != null;
+ }
+}
+
diff --git a/mes-processes/mes-plcSend/src/main/java/com/mes/interaction/vehicle/coordination/VehicleStatusManager.java b/mes-processes/mes-plcSend/src/main/java/com/mes/interaction/vehicle/coordination/VehicleStatusManager.java
new file mode 100644
index 0000000..2cc0964
--- /dev/null
+++ b/mes-processes/mes-plcSend/src/main/java/com/mes/interaction/vehicle/coordination/VehicleStatusManager.java
@@ -0,0 +1,199 @@
+package com.mes.interaction.vehicle.coordination;
+
+import com.mes.interaction.vehicle.model.VehicleState;
+import com.mes.interaction.vehicle.model.VehicleStatus;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
+
+/**
+ * 杞﹁締鐘舵�佺鐞嗗櫒
+ * 绠$悊鎵�鏈夊ぇ杞﹁澶囧疄渚嬬殑杩愯鏃剁姸鎬�
+ *
+ * @author huang
+ * @since 2025-11-21
+ */
+@Slf4j
+@Service
+public class VehicleStatusManager {
+
+ /**
+ * 瀛樺偍鎵�鏈夎溅杈嗗疄渚嬬殑鐘舵�侊細deviceId -> VehicleStatus
+ */
+ private final Map<String, VehicleStatus> vehicleStatusMap = new ConcurrentHashMap<>();
+
+ /**
+ * 鑾峰彇杞﹁締鐘舵��
+ * 濡傛灉涓嶅瓨鍦ㄥ垯鍒涘缓骞惰繑鍥炵┖闂茬姸鎬�
+ *
+ * @param deviceId 璁惧ID
+ * @return 杞﹁締鐘舵��
+ */
+ public VehicleStatus getVehicleStatus(String deviceId) {
+ if (deviceId == null || deviceId.isEmpty()) {
+ return null;
+ }
+ return vehicleStatusMap.get(deviceId);
+ }
+
+ /**
+ * 鑾峰彇鎴栧垱寤鸿溅杈嗙姸鎬�
+ *
+ * @param deviceId 璁惧ID
+ * @param deviceName 璁惧鍚嶇О
+ * @return 杞﹁締鐘舵��
+ */
+ public VehicleStatus getOrCreateVehicleStatus(String deviceId, String deviceName) {
+ return vehicleStatusMap.computeIfAbsent(deviceId,
+ k -> new VehicleStatus(deviceId, deviceName));
+ }
+
+ /**
+ * 鏇存柊杞﹁締鐘舵��
+ *
+ * @param deviceId 璁惧ID
+ * @param state 鏂扮姸鎬�
+ */
+ public void updateVehicleStatus(String deviceId, VehicleState state) {
+ if (deviceId == null || deviceId.isEmpty()) {
+ log.warn("璁惧ID涓虹┖锛屾棤娉曟洿鏂扮姸鎬�");
+ return;
+ }
+
+ VehicleStatus status = vehicleStatusMap.computeIfAbsent(
+ deviceId,
+ k -> new VehicleStatus(deviceId)
+ );
+ status.setState(state);
+
+ log.debug("鏇存柊杞﹁締鐘舵��: deviceId={}, state={}", deviceId, state);
+ }
+
+ /**
+ * 鏇存柊杞﹁締鐘舵�侊紙甯﹁澶囧悕绉帮級
+ *
+ * @param deviceId 璁惧ID
+ * @param deviceName 璁惧鍚嶇О
+ * @param state 鏂扮姸鎬�
+ */
+ public void updateVehicleStatus(String deviceId, String deviceName, VehicleState state) {
+ VehicleStatus status = getOrCreateVehicleStatus(deviceId, deviceName);
+ status.setState(state);
+ log.debug("鏇存柊杞﹁締鐘舵��: deviceId={}, deviceName={}, state={}", deviceId, deviceName, state);
+ }
+
+ /**
+ * 鑾峰彇鎵�鏈夌┖闂茬殑杞﹁締
+ *
+ * @return 绌洪棽杞﹁締鐘舵�佸垪琛�
+ */
+ public List<VehicleStatus> getIdleVehicles() {
+ return vehicleStatusMap.values().stream()
+ .filter(v -> v.getState() == VehicleState.IDLE)
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * 鑾峰彇鎵�鏈夋墽琛屼腑鐨勮溅杈�
+ *
+ * @return 鎵ц涓溅杈嗙姸鎬佸垪琛�
+ */
+ public List<VehicleStatus> getExecutingVehicles() {
+ return vehicleStatusMap.values().stream()
+ .filter(v -> v.getState() == VehicleState.EXECUTING)
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * 鑾峰彇鎵�鏈夌瓑寰呬腑鐨勮溅杈�
+ *
+ * @return 绛夊緟涓溅杈嗙姸鎬佸垪琛�
+ */
+ public List<VehicleStatus> getWaitingVehicles() {
+ return vehicleStatusMap.values().stream()
+ .filter(v -> v.getState() == VehicleState.WAITING)
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * 妫�鏌ヨ溅杈嗘槸鍚﹀彲鐢�
+ *
+ * @param deviceId 璁惧ID
+ * @return true琛ㄧず鍙敤锛宖alse琛ㄧず涓嶅彲鐢�
+ */
+ public boolean isVehicleAvailable(String deviceId) {
+ if (deviceId == null || deviceId.isEmpty()) {
+ return false;
+ }
+
+ VehicleStatus status = vehicleStatusMap.get(deviceId);
+ return status == null || status.isAvailable();
+ }
+
+ /**
+ * 璁剧疆杞﹁締浠诲姟
+ *
+ * @param deviceId 璁惧ID
+ * @param task 浠诲姟
+ */
+ public void setVehicleTask(String deviceId, com.mes.interaction.vehicle.model.VehicleTask task) {
+ VehicleStatus status = vehicleStatusMap.get(deviceId);
+ if (status != null) {
+ status.setCurrentTask(task);
+ if (task != null && task.getPlannedPath() != null) {
+ status.setTargetPosition(task.getPlannedPath().getEndPosition());
+ }
+ }
+ }
+
+ /**
+ * 娓呴櫎杞﹁締浠诲姟
+ *
+ * @param deviceId 璁惧ID
+ */
+ public void clearVehicleTask(String deviceId) {
+ VehicleStatus status = vehicleStatusMap.get(deviceId);
+ if (status != null) {
+ status.setCurrentTask(null);
+ status.setTargetPosition(null);
+ }
+ }
+
+ /**
+ * 绉婚櫎杞﹁締鐘舵�侊紙褰撹澶囪鍒犻櫎鏃讹級
+ *
+ * @param deviceId 璁惧ID
+ */
+ public void removeVehicleStatus(String deviceId) {
+ vehicleStatusMap.remove(deviceId);
+ log.info("绉婚櫎杞﹁締鐘舵��: deviceId={}", deviceId);
+ }
+
+ /**
+ * 鑾峰彇鎵�鏈夎溅杈嗙姸鎬�
+ *
+ * @return 鎵�鏈夎溅杈嗙姸鎬佸垪琛�
+ */
+ public List<VehicleStatus> getAllVehicleStatuses() {
+ return vehicleStatusMap.values().stream()
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * 鑾峰彇鎸囧畾璁惧缁勪腑鐨勮溅杈嗙姸鎬�
+ *
+ * @param deviceIds 璁惧ID鍒楄〃
+ * @return 杞﹁締鐘舵�佸垪琛�
+ */
+ public List<VehicleStatus> getVehicleStatuses(List<String> deviceIds) {
+ return deviceIds.stream()
+ .map(this::getVehicleStatus)
+ .filter(status -> status != null)
+ .collect(Collectors.toList());
+ }
+}
+
diff --git a/mes-processes/mes-plcSend/src/main/java/com/mes/interaction/vehicle/flow/LoadVehicleInteraction.java b/mes-processes/mes-plcSend/src/main/java/com/mes/interaction/vehicle/flow/LoadVehicleInteraction.java
new file mode 100644
index 0000000..a644e2d
--- /dev/null
+++ b/mes-processes/mes-plcSend/src/main/java/com/mes/interaction/vehicle/flow/LoadVehicleInteraction.java
@@ -0,0 +1,225 @@
+package com.mes.interaction.vehicle.flow;
+
+import com.mes.device.entity.DeviceConfig;
+import com.mes.device.service.DeviceInteractionService;
+import com.mes.device.vo.DevicePlcVO;
+import com.mes.interaction.DeviceInteraction;
+import com.mes.interaction.base.InteractionContext;
+import com.mes.interaction.base.InteractionResult;
+import com.mes.interaction.vehicle.coordination.VehicleCoordinationService;
+import com.mes.interaction.vehicle.coordination.VehicleStatusManager;
+import com.mes.interaction.vehicle.model.VehicleState;
+import com.mes.interaction.vehicle.model.VehiclePosition;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import org.springframework.util.CollectionUtils;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 澶ц溅璁惧浜や簰瀹炵幇锛堝寮虹増锛�
+ * 闆嗘垚澶氳溅鍗忚皟鍜岀姸鎬佺鐞嗗姛鑳�
+ *
+ * @author mes
+ * @since 2025-01-XX
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class LoadVehicleInteraction implements DeviceInteraction {
+
+ private final DeviceInteractionService deviceInteractionService;
+
+ @Autowired
+ private VehicleCoordinationService coordinationService;
+
+ @Autowired
+ private VehicleStatusManager statusManager;
+
+ @Override
+ public String getDeviceType() {
+ return DeviceConfig.DeviceType.LOAD_VEHICLE;
+ }
+
+ @Override
+ public InteractionResult execute(InteractionContext context) {
+ try {
+ // 鍓嶇疆鏉′欢楠岃瘉
+ if (context.getCurrentDevice() == null) {
+ return InteractionResult.fail("璁惧閰嶇疆涓嶅瓨鍦�");
+ }
+
+ DeviceConfig currentDevice = context.getCurrentDevice();
+ String deviceId = currentDevice.getDeviceId();
+
+ // 1. 妫�鏌ヨ溅杈嗙姸鎬侊紙濡傛灉璁惧宸叉寚瀹氾級
+ if (deviceId != null) {
+ if (!statusManager.isVehicleAvailable(deviceId)) {
+ com.mes.interaction.vehicle.model.VehicleStatus status =
+ statusManager.getVehicleStatus(deviceId);
+ String stateMsg = status != null ? status.getState().name() : "鏈煡";
+ return InteractionResult.fail(
+ String.format("杞﹁締 %s (%s) 褰撳墠鐘舵�佷负 %s锛屾棤娉曟墽琛屾搷浣�",
+ currentDevice.getDeviceName(), deviceId, stateMsg));
+ }
+ }
+
+ // 2. 濡傛灉娌℃湁鎸囧畾璁惧锛屽皾璇曚粠璁惧缁勪腑閫夋嫨鍙敤杞﹁締
+ DeviceConfig selectedDevice = currentDevice;
+ Long groupId = extractGroupId(context);
+ if (groupId != null && (deviceId == null || !statusManager.isVehicleAvailable(deviceId))) {
+ DeviceConfig availableVehicle = coordinationService.selectAvailableVehicle(groupId);
+ if (availableVehicle != null) {
+ selectedDevice = availableVehicle;
+ log.info("浠庤澶囩粍 {} 涓�夋嫨鍙敤杞﹁締: {}", groupId, availableVehicle.getDeviceName());
+ } else if (deviceId == null) {
+ // 娌℃湁鍙敤杞﹁締锛岃繑鍥炵瓑寰呯粨鏋�
+ return InteractionResult.waitResult(
+ "璁惧缁勪腑娌℃湁鍙敤鐨勫ぇ杞﹁澶囷紝绛夊緟杞﹁締绌洪棽", null);
+ }
+ }
+
+ // 3. 楠岃瘉鐜荤拑ID
+ List<String> glassIds = context.getParameters().getGlassIds();
+ if (CollectionUtils.isEmpty(glassIds)) {
+ return InteractionResult.waitResult("鏈彁渚涚幓鐠僆D锛岀瓑寰呰緭鍏�", null);
+ }
+
+ // 楠岃瘉鐜荤拑ID鏍煎紡
+ for (String glassId : glassIds) {
+ if (glassId == null || glassId.trim().isEmpty()) {
+ return InteractionResult.fail("鐜荤拑ID涓嶈兘涓虹┖");
+ }
+ }
+
+ // 4. 鏍囪杞﹁締涓烘墽琛屼腑
+ String selectedDeviceId = selectedDevice.getDeviceId();
+ statusManager.updateVehicleStatus(
+ selectedDeviceId,
+ selectedDevice.getDeviceName(),
+ VehicleState.EXECUTING);
+
+ try {
+ // 5. 鏋勫缓PLC鍐欏叆鍙傛暟
+ Map<String, Object> params = new HashMap<>();
+ params.put("glassIds", glassIds);
+ params.put("positionCode", context.getParameters().getPositionCode());
+ params.put("positionValue", context.getParameters().getPositionValue());
+ params.put("triggerRequest", true);
+
+ // 6. 鎵ц瀹為檯鐨凱LC鍐欏叆鎿嶄綔
+ DevicePlcVO.OperationResult plcResult = deviceInteractionService.executeOperation(
+ selectedDevice.getId(),
+ "feedGlass",
+ params
+ );
+
+ // 7. 妫�鏌LC鍐欏叆缁撴灉
+ if (plcResult == null || !Boolean.TRUE.equals(plcResult.getSuccess())) {
+ String errorMsg = plcResult != null ? plcResult.getMessage() : "PLC鍐欏叆鎿嶄綔杩斿洖绌虹粨鏋�";
+
+ // 鎵ц澶辫触锛屾仮澶嶄负绌洪棽鐘舵��
+ statusManager.updateVehicleStatus(selectedDeviceId, VehicleState.IDLE);
+
+ return InteractionResult.fail("PLC鍐欏叆澶辫触: " + errorMsg);
+ }
+
+ // 8. 鏇存柊杞﹁締浣嶇疆淇℃伅锛堝鏋滄湁锛�
+ if (context.getParameters().getPositionCode() != null ||
+ context.getParameters().getPositionValue() != null) {
+ com.mes.interaction.vehicle.model.VehicleStatus vehicleStatus =
+ statusManager.getOrCreateVehicleStatus(
+ selectedDeviceId,
+ selectedDevice.getDeviceName());
+ VehiclePosition position = new VehiclePosition(
+ context.getParameters().getPositionCode(),
+ context.getParameters().getPositionValue());
+ vehicleStatus.setCurrentPosition(position);
+ }
+
+ // 9. 鎵ц澶ц溅璁惧鎿嶄綔锛堟暟鎹祦杞級
+ List<String> copied = new ArrayList<>(glassIds);
+ context.setLoadedGlassIds(copied);
+ context.getSharedData().put("glassesFromVehicle", copied);
+ context.getSharedData().put("loadVehicleTime", System.currentTimeMillis());
+ context.getSharedData().put("selectedVehicleId", selectedDeviceId);
+ context.getSharedData().put("selectedVehicleName", selectedDevice.getDeviceName());
+
+ // 10. 鍚庣疆鏉′欢妫�鏌�
+ if (context.getLoadedGlassIds().isEmpty()) {
+ statusManager.updateVehicleStatus(selectedDeviceId, VehicleState.IDLE);
+ return InteractionResult.fail("澶ц溅璁惧鎿嶄綔澶辫触锛氱幓鐠僆D鍒楄〃涓虹┖");
+ }
+
+ // 11. 鏋勫缓杩斿洖鏁版嵁
+ Map<String, Object> data = new HashMap<>();
+ data.put("loaded", copied);
+ data.put("glassCount", copied.size());
+ data.put("deviceId", selectedDevice.getId());
+ data.put("deviceCode", selectedDevice.getDeviceCode());
+ data.put("deviceName", selectedDevice.getDeviceName());
+ data.put("deviceIdString", selectedDeviceId);
+ data.put("plcResult", plcResult.getMessage());
+
+ // 娉ㄦ剰锛氳繖閲屼笉绔嬪嵆鎭㈠涓虹┖闂茬姸鎬侊紝鍥犱负瀹為檯鎵ц鍙兘闇�瑕佹椂闂�
+ // 鐪熸鐨勭姸鎬佹仮澶嶅簲璇ュ湪浠诲姟瀹屾垚鍚庨�氳繃鍥炶皟鎴栫姸鎬佹煡璇㈡潵鏇存柊
+ // 鎴栬�呭彲浠ラ�氳繃寮傛浠诲姟鍦ㄥ悗鍙扮洃鎺LC鐘舵�侊紝纭瀹屾垚鍚庡啀鎭㈠
+
+ log.info("澶ц溅璁惧浜や簰鎵ц鎴愬姛: deviceId={}, deviceName={}, glassCount={}",
+ selectedDeviceId, selectedDevice.getDeviceName(), copied.size());
+
+ return InteractionResult.success(data);
+
+ } catch (Exception e) {
+ // 鍙戠敓寮傚父鏃讹紝鎭㈠涓虹┖闂茬姸鎬�
+ statusManager.updateVehicleStatus(selectedDeviceId, VehicleState.ERROR);
+ log.error("澶ц溅璁惧浜や簰鎵ц寮傚父: deviceId={}", selectedDeviceId, e);
+ throw e;
+ }
+
+ } catch (Exception e) {
+ return InteractionResult.fail("澶ц溅璁惧浜や簰鎵ц寮傚父: " + e.getMessage());
+ }
+ }
+
+ /**
+ * 浠庝笂涓嬫枃涓彁鍙栬澶囩粍ID
+ */
+ private Long extractGroupId(InteractionContext context) {
+ // 灏濊瘯浠庡叡浜暟鎹腑鑾峰彇
+ Map<String, Object> sharedData = context.getSharedData();
+ if (sharedData != null) {
+ Object groupIdObj = sharedData.get("groupId");
+ if (groupIdObj instanceof Number) {
+ return ((Number) groupIdObj).longValue();
+ } else if (groupIdObj instanceof String) {
+ try {
+ return Long.parseLong((String) groupIdObj);
+ } catch (NumberFormatException ignored) {
+ }
+ }
+ }
+
+ // 灏濊瘯浠庝换鍔″弬鏁颁腑鑾峰彇
+ if (context.getParameters() != null && context.getParameters().getExtra() != null) {
+ Object groupIdObj = context.getParameters().getExtra().get("groupId");
+ if (groupIdObj instanceof Number) {
+ return ((Number) groupIdObj).longValue();
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ public boolean supportsOperation(String operation) {
+ // 鏀寔鎵�鏈夋搷浣滐紝浣嗕富瑕佸叧娉� feedGlass
+ return true;
+ }
+}
+
diff --git a/mes-processes/mes-plcSend/src/main/java/com/mes/interaction/vehicle/handler/LoadVehicleLogicHandler.java b/mes-processes/mes-plcSend/src/main/java/com/mes/interaction/vehicle/handler/LoadVehicleLogicHandler.java
new file mode 100644
index 0000000..aed9919
--- /dev/null
+++ b/mes-processes/mes-plcSend/src/main/java/com/mes/interaction/vehicle/handler/LoadVehicleLogicHandler.java
@@ -0,0 +1,1719 @@
+package com.mes.interaction.vehicle.handler;
+
+import com.mes.device.entity.DeviceConfig;
+import com.mes.device.service.DeviceConfigService;
+import com.mes.device.service.DeviceGroupRelationService;
+import com.mes.device.service.DevicePlcOperationService;
+import com.mes.device.service.GlassInfoService;
+import com.mes.device.vo.DeviceGroupVO;
+import com.mes.device.vo.DevicePlcVO;
+import com.mes.interaction.BaseDeviceLogicHandler;
+import com.mes.interaction.vehicle.coordination.VehicleStatusManager;
+import com.mes.interaction.vehicle.model.VehiclePosition;
+import com.mes.interaction.vehicle.model.VehicleState;
+import com.mes.interaction.vehicle.model.VehicleStatus;
+import com.mes.interaction.vehicle.model.VehicleTask;
+import com.mes.s7.enhanced.EnhancedS7Serializer;
+import com.mes.s7.provider.S7SerializerProvider;
+import com.mes.service.PlcDynamicDataService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.PreDestroy;
+import java.util.*;
+import java.util.concurrent.*;
+
+/**
+ * 澶ц溅璁惧閫昏緫澶勭悊鍣�
+ * 鎵�鏈夊ぇ杞﹁澶囧疄渚嬪叡浜繖涓鐞嗗櫒
+ * 闆嗘垚澶氬疄渚嬬姸鎬佺鐞嗗拰鍗忚皟鍔熻兘
+ *
+ * @author huang
+ * @since 2025-11-21
+ */
+@Slf4j
+@Component
+public class LoadVehicleLogicHandler extends BaseDeviceLogicHandler {
+
+ private final GlassInfoService glassInfoService;
+
+ @Autowired
+ private VehicleStatusManager statusManager;
+
+ @Autowired(required = false)
+ private DeviceConfigService deviceConfigService;
+
+ @Autowired(required = false)
+ private DeviceGroupRelationService deviceGroupRelationService;
+
+ @Autowired(required = false)
+ private PlcDynamicDataService plcDynamicDataService;
+
+ @Autowired(required = false)
+ private S7SerializerProvider s7SerializerProvider;
+
+ // MES瀛楁鍒楄〃锛堣繘鐗囧拰鍑虹墖鍏辩敤鍚屼竴濂楀崗璁級
+ private static final List<String> MES_FIELDS = Arrays.asList(
+ "mesSend", "mesGlassId", "mesWidth", "mesHeight",
+ "startSlot", "targetSlot", "workLine"
+ );
+
+ // 鐩戞帶绾跨▼姹狅細鐢ㄤ簬瀹氭湡妫�鏌ュぇ杞︾姸鎬佸苟鍗忚皟鍗ц浆绔嬭澶�
+ private final ScheduledExecutorService stateMonitorExecutor = Executors.newScheduledThreadPool(5, r -> {
+ Thread t = new Thread(r, "VehicleStateMonitor");
+ t.setDaemon(true);
+ return t;
+ });
+
+ // 绌洪棽鐩戞帶绾跨▼姹狅細鐢ㄤ簬淇濇寔plcRequest=1
+ private final ScheduledExecutorService idleMonitorExecutor = Executors.newScheduledThreadPool(3, r -> {
+ Thread t = new Thread(r, "VehicleIdleMonitor");
+ t.setDaemon(true);
+ return t;
+ });
+
+ // 浠诲姟鐩戞帶绾跨▼姹狅細鐢ㄤ簬鐩戞帶浠诲姟鎵ц鍜岀姸鎬佸垏鎹�
+ private final ScheduledExecutorService taskMonitorExecutor = Executors.newScheduledThreadPool(5, r -> {
+ Thread t = new Thread(r, "VehicleTaskMonitor");
+ t.setDaemon(true);
+ return t;
+ });
+
+ // 璁板綍姝e湪鐩戞帶鐨勮澶囷細deviceId -> 鐩戞帶浠诲姟
+ private final Map<String, ScheduledFuture<?>> monitoringTasks = new ConcurrentHashMap<>();
+
+ // 璁板綍绌洪棽鐩戞帶浠诲姟锛歞eviceId -> 绌洪棽鐩戞帶浠诲姟
+ private final Map<String, ScheduledFuture<?>> idleMonitoringTasks = new ConcurrentHashMap<>();
+
+ // 璁板綍浠诲姟鐩戞帶浠诲姟锛歞eviceId -> 浠诲姟鐩戞帶浠诲姟
+ private final Map<String, ScheduledFuture<?>> taskMonitoringTasks = new ConcurrentHashMap<>();
+
+ // 璁板綍宸插崗璋冪殑璁惧锛歞eviceId -> 宸插崗璋冪殑state瀛楁闆嗗悎锛堥伩鍏嶉噸澶嶅崗璋冿級
+ private final Map<String, List<String>> coordinatedStates = new ConcurrentHashMap<>();
+
+ // 璁板綍褰撳墠浠诲姟锛歞eviceId -> 浠诲姟淇℃伅
+ private final Map<String, MesTaskInfo> currentTasks = new ConcurrentHashMap<>();
+
+ @Autowired
+ public LoadVehicleLogicHandler(
+ DevicePlcOperationService devicePlcOperationService,
+ @Qualifier("deviceGlassInfoService") GlassInfoService glassInfoService) {
+ super(devicePlcOperationService);
+ this.glassInfoService = glassInfoService;
+ }
+
+ @Override
+ public String getDeviceType() {
+ return DeviceConfig.DeviceType.LOAD_VEHICLE;
+ }
+
+ @Override
+ protected DevicePlcVO.OperationResult doExecute(
+ DeviceConfig deviceConfig,
+ String operation,
+ Map<String, Object> params,
+ Map<String, Object> logicParams) {
+
+ String deviceId = deviceConfig.getDeviceId();
+ log.info("鎵ц澶ц溅璁惧鎿嶄綔: deviceId={}, deviceName={}, operation={}",
+ deviceId, deviceConfig.getDeviceName(), operation);
+
+ // 1. 妫�鏌ヨ繖涓澶囧疄渚嬬殑鐘舵�侊紙瀵逛簬闇�瑕佺姸鎬佹鏌ョ殑鎿嶄綔锛�
+ if (needsStateCheck(operation)) {
+ VehicleStatus status = statusManager.getVehicleStatus(deviceId);
+ if (status != null && !status.isAvailable()) {
+ return DevicePlcVO.OperationResult.builder()
+ .success(false)
+ .message(String.format("杞﹁締 %s (%s) 褰撳墠鐘舵�佷负 %s锛屾棤娉曟墽琛屾搷浣� %s",
+ deviceConfig.getDeviceName(),
+ deviceId,
+ status.getState(),
+ operation))
+ .build();
+ }
+ }
+
+ // 2. 鏍囪涓烘墽琛屼腑锛堝浜庨渶瑕佺姸鎬佺鐞嗙殑鎿嶄綔锛�
+ if (needsStateManagement(operation)) {
+ statusManager.updateVehicleStatus(deviceId, deviceConfig.getDeviceName(), VehicleState.EXECUTING);
+
+ // 鍒涘缓浠诲姟淇℃伅
+ VehicleTask task = createVehicleTask(deviceConfig, operation, params, logicParams);
+ statusManager.setVehicleTask(deviceId, task);
+ }
+
+ try {
+ // 3. 鎵ц鍘熸湁閫昏緫
+ DevicePlcVO.OperationResult result;
+ switch (operation) {
+ case "feedGlass":
+ result = handleFeedGlass(deviceConfig, params, logicParams);
+ break;
+ case "triggerRequest":
+ result = handleTriggerRequest(deviceConfig, params, logicParams);
+ break;
+ case "triggerReport":
+ result = handleTriggerReport(deviceConfig, params, logicParams);
+ break;
+ case "reset":
+ result = handleReset(deviceConfig, params, logicParams);
+ break;
+ case "clearGlass":
+ case "clearPlc":
+ case "clear":
+ result = handleClearGlass(deviceConfig, params, logicParams);
+ break;
+ case "checkStateAndCoordinate":
+ result = handleCheckStateAndCoordinate(deviceConfig, params, logicParams);
+ break;
+ case "startIdleMonitor":
+ result = handleStartIdleMonitor(deviceConfig, params, logicParams);
+ break;
+ case "stopIdleMonitor":
+ result = handleStopIdleMonitor(deviceConfig);
+ break;
+ case "checkMesTask":
+ result = handleCheckMesTask(deviceConfig, params, logicParams);
+ break;
+ case "checkMesOutboundTask":
+ // 鍑虹墖浠诲姟涔熶娇鐢ㄥ悓涓�濂楀崗璁紝閫氳繃handleCheckMesTask澶勭悊
+ result = handleCheckMesTask(deviceConfig, params, logicParams);
+ break;
+ case "startTaskMonitor":
+ result = handleStartTaskMonitor(deviceConfig, params, logicParams);
+ break;
+ case "stopTaskMonitor":
+ result = handleStopTaskMonitor(deviceConfig);
+ break;
+ default:
+ log.warn("涓嶆敮鎸佺殑鎿嶄綔绫诲瀷: {}", operation);
+ result = DevicePlcVO.OperationResult.builder()
+ .success(false)
+ .message("涓嶆敮鎸佺殑鎿嶄綔: " + operation)
+ .build();
+ }
+
+ return result;
+
+ } catch (Exception e) {
+ log.error("鎵ц澶ц溅璁惧鎿嶄綔寮傚父: deviceId={}, operation={}", deviceId, operation, e);
+ // 鍙戠敓寮傚父鏃讹紝灏嗙姸鎬佽缃负閿欒
+ if (needsStateManagement(operation)) {
+ statusManager.updateVehicleStatus(deviceId, VehicleState.ERROR);
+ }
+ throw e;
+ } finally {
+ // 4. 鎵ц瀹屾垚鍚庢仮澶嶄负绌洪棽鐘舵�侊紙瀵逛簬闇�瑕佺姸鎬佺鐞嗙殑鎿嶄綔锛�
+ if (needsStateManagement(operation)) {
+ // 娉ㄦ剰锛氳繖閲屼笉绔嬪嵆璁剧疆涓篒DLE锛屽洜涓哄疄闄呮墽琛屽彲鑳介渶瑕佹椂闂�
+ // 鐪熸鐨勭姸鎬佹洿鏂板簲璇ュ湪浠诲姟瀹屾垚鍚庨�氳繃鍥炶皟鎴栫姸鎬佹煡璇㈡潵鏇存柊
+ // 杩欓噷鍏堜繚鎸丒XECUTING鐘舵�侊紝绛夊緟澶栭儴纭瀹屾垚鍚庡啀鏇存柊
+ log.debug("鎿嶄綔鎵ц瀹屾垚锛屼繚鎸佹墽琛屼腑鐘舵�侊紝绛夊緟澶栭儴纭: deviceId={}", deviceId);
+ }
+ }
+ }
+
+ /**
+ * 鍒ゆ柇鎿嶄綔鏄惁闇�瑕佺姸鎬佹鏌�
+ */
+ private boolean needsStateCheck(String operation) {
+ // 鎵�鏈夋搷浣滈兘闇�瑕佹鏌ョ姸鎬侊紝闄や簡鏌ヨ绫绘搷浣�
+ return !"query".equals(operation) && !"status".equals(operation);
+ }
+
+ /**
+ * 鍒ゆ柇鎿嶄綔鏄惁闇�瑕佺姸鎬佺鐞�
+ */
+ private boolean needsStateManagement(String operation) {
+ // feedGlass 闇�瑕佺姸鎬佺鐞嗭紝鍏朵粬鎿嶄綔鏍规嵁瀹為檯鎯呭喌
+ return "feedGlass".equals(operation);
+ }
+
+ /**
+ * 鍒涘缓杞﹁締浠诲姟淇℃伅
+ */
+ private VehicleTask createVehicleTask(
+ DeviceConfig deviceConfig,
+ String operation,
+ Map<String, Object> params,
+ Map<String, Object> logicParams) {
+
+ VehicleTask task = new VehicleTask();
+ task.setTaskId(generateTaskId(deviceConfig.getDeviceId()));
+ task.setTaskName("澶ц溅璁惧-" + operation);
+ task.setOperation(operation);
+
+ // 浠庡弬鏁颁腑鎻愬彇浣嶇疆淇℃伅
+ String positionCode = (String) params.get("positionCode");
+ Integer positionValue = (Integer) params.get("positionValue");
+
+ if (positionCode != null || positionValue != null) {
+ VehiclePosition position = new VehiclePosition(positionCode, positionValue);
+ task.getPlannedPath().setStartPosition(position);
+ task.getPlannedPath().setEndPosition(position);
+ }
+
+ // 浠庨厤缃腑鑾峰彇閫熷害锛堝鏋滄湁锛�
+ Double speed = getLogicParam(logicParams, "vehicleSpeed", null);
+ if (speed != null) {
+ task.setSpeed(speed);
+ task.calculateEstimatedEndTime();
+ }
+
+ task.setParameters(new HashMap<>(params));
+
+ return task;
+ }
+
+ /**
+ * 鐢熸垚浠诲姟ID
+ */
+ private String generateTaskId(String deviceId) {
+ return "TASK_" + deviceId + "_" + System.currentTimeMillis();
+ }
+
+ /**
+ * 澶勭悊鐜荤拑涓婃枡鎿嶄綔
+ */
+ private DevicePlcVO.OperationResult handleFeedGlass(
+ DeviceConfig deviceConfig,
+ Map<String, Object> params,
+ Map<String, Object> logicParams) {
+
+ // 浠庨�昏緫鍙傛暟涓幏鍙栭厤缃紙浠� extraParams.deviceLogic 璇诲彇锛�
+ Integer vehicleCapacity = getLogicParam(logicParams, "vehicleCapacity", 6000);
+ Integer glassIntervalMs = getLogicParam(logicParams, "glassIntervalMs", 1000);
+ Boolean autoFeed = getLogicParam(logicParams, "autoFeed", true);
+ Integer maxRetryCount = getLogicParam(logicParams, "maxRetryCount", 5);
+
+ // 浠庤繍琛屾椂鍙傛暟涓幏鍙栨暟鎹紙浠庢帴鍙h皟鐢ㄦ椂浼犲叆锛�
+ List<GlassInfo> glassInfos = extractGlassInfos(params);
+ if (glassInfos.isEmpty()) {
+ return DevicePlcVO.OperationResult.builder()
+ .success(false)
+ .message("鏈彁渚涙湁鏁堢殑鐜荤拑淇℃伅")
+ .build();
+ }
+
+ String positionCode = (String) params.get("positionCode");
+ Integer positionValue = (Integer) params.get("positionValue");
+ Boolean triggerRequest = (Boolean) params.getOrDefault("triggerRequest", autoFeed);
+
+ List<GlassInfo> plannedGlasses = planGlassLoading(glassInfos, vehicleCapacity,
+ getLogicParam(logicParams, "defaultGlassLength", 2000));
+ if (plannedGlasses.isEmpty()) {
+ return DevicePlcVO.OperationResult.builder()
+ .success(false)
+ .message("褰撳墠鐜荤拑灏哄瓒呭嚭杞﹁締瀹归噺锛屾棤娉曡杞�")
+ .build();
+ }
+
+ // 鏋勫缓鍐欏叆鏁版嵁
+ Map<String, Object> payload = new HashMap<>();
+
+ // 鍐欏叆鐜荤拑ID
+ int plcSlots = Math.min(plannedGlasses.size(), 6);
+ for (int i = 0; i < plcSlots; i++) {
+ String fieldName = "plcGlassId" + (i + 1);
+ payload.put(fieldName, plannedGlasses.get(i).getGlassId());
+ }
+ payload.put("plcGlassCount", plcSlots);
+
+ // 鍐欏叆浣嶇疆淇℃伅
+ if (positionValue != null) {
+ payload.put("inPosition", positionValue);
+ } else if (positionCode != null) {
+ // 浠庝綅缃槧灏勪腑鑾峰彇浣嶇疆鍊�
+ @SuppressWarnings("unchecked")
+ Map<String, Integer> positionMapping = getLogicParam(logicParams, "positionMapping", new HashMap<>());
+ Integer mappedValue = positionMapping.get(positionCode);
+ if (mappedValue != null) {
+ payload.put("inPosition", mappedValue);
+ }
+ }
+
+ // 鑷姩瑙﹀彂璇锋眰瀛�
+ if (triggerRequest != null && triggerRequest) {
+ payload.put("plcRequest", 1);
+ }
+
+ String operationName = "澶ц溅璁惧-鐜荤拑涓婃枡";
+ if (positionCode != null) {
+ operationName += "(" + positionCode + ")";
+ }
+
+ log.info("澶ц溅璁惧鐜荤拑涓婃枡: deviceId={}, glassCount={}, position={}, plannedGlassIds={}",
+ deviceConfig.getId(), plcSlots, positionCode, plannedGlasses);
+
+ if (glassIntervalMs != null && glassIntervalMs > 0) {
+ try {
+ Thread.sleep(glassIntervalMs);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ DevicePlcVO.OperationResult result = devicePlcOperationService.writeFields(
+ deviceConfig.getId(), payload, operationName);
+
+ // 濡傛灉鎵ц鎴愬姛锛屾洿鏂颁綅缃俊鎭埌鐘舵�侊紝骞跺惎鍔ㄧ姸鎬佺洃鎺�
+ if (Boolean.TRUE.equals(result.getSuccess())) {
+ VehicleStatus status = statusManager.getOrCreateVehicleStatus(
+ deviceConfig.getDeviceId(), deviceConfig.getDeviceName());
+ if (positionCode != null || positionValue != null) {
+ VehiclePosition position = new VehiclePosition(positionCode, positionValue);
+ status.setCurrentPosition(position);
+ }
+
+ // 鍚姩鑷姩鐘舵�佺洃鎺э紝褰� state=1 鏃惰嚜鍔ㄥ崗璋冨崸杞珛璁惧
+ startStateMonitoring(deviceConfig, logicParams);
+ }
+
+ return result;
+ }
+
+ /**
+ * 澶勭悊瑙﹀彂璇锋眰鎿嶄綔
+ */
+ private DevicePlcVO.OperationResult handleTriggerRequest(
+ DeviceConfig deviceConfig,
+ Map<String, Object> params,
+ Map<String, Object> logicParams) {
+
+ Map<String, Object> payload = new HashMap<>();
+ payload.put("plcRequest", 1);
+
+ log.info("澶ц溅璁惧瑙﹀彂璇锋眰: deviceId={}", deviceConfig.getId());
+ return devicePlcOperationService.writeFields(
+ deviceConfig.getId(),
+ payload,
+ "澶ц溅璁惧-瑙﹀彂璇锋眰"
+ );
+ }
+
+ /**
+ * 澶勭悊瑙﹀彂姹囨姤鎿嶄綔
+ */
+ private DevicePlcVO.OperationResult handleTriggerReport(
+ DeviceConfig deviceConfig,
+ Map<String, Object> params,
+ Map<String, Object> logicParams) {
+
+ Map<String, Object> payload = new HashMap<>();
+ payload.put("plcReport", 1);
+
+ log.info("澶ц溅璁惧瑙﹀彂姹囨姤: deviceId={}", deviceConfig.getId());
+ return devicePlcOperationService.writeFields(
+ deviceConfig.getId(),
+ payload,
+ "澶ц溅璁惧-瑙﹀彂姹囨姤"
+ );
+ }
+
+ /**
+ * 澶勭悊閲嶇疆鎿嶄綔
+ */
+ private DevicePlcVO.OperationResult handleReset(
+ DeviceConfig deviceConfig,
+ Map<String, Object> params,
+ Map<String, Object> logicParams) {
+
+ Map<String, Object> payload = new HashMap<>();
+ payload.put("plcRequest", 0);
+ payload.put("plcReport", 0);
+
+ log.info("澶ц溅璁惧閲嶇疆: deviceId={}", deviceConfig.getId());
+
+ DevicePlcVO.OperationResult result = devicePlcOperationService.writeFields(
+ deviceConfig.getId(),
+ payload,
+ "澶ц溅璁惧-閲嶇疆"
+ );
+
+ // 閲嶇疆鏃讹紝娓呴櫎浠诲姟骞舵仮澶嶄负绌洪棽鐘舵�侊紝鍋滄鐩戞帶
+ if (Boolean.TRUE.equals(result.getSuccess())) {
+ statusManager.clearVehicleTask(deviceConfig.getDeviceId());
+ statusManager.updateVehicleStatus(deviceConfig.getDeviceId(), VehicleState.IDLE);
+ stopStateMonitoring(deviceConfig.getDeviceId());
+ }
+
+ return result;
+ }
+
+ /**
+ * 娓呯┖PLC涓殑鐜荤拑鏁版嵁
+ */
+ private DevicePlcVO.OperationResult handleClearGlass(
+ DeviceConfig deviceConfig,
+ Map<String, Object> params,
+ Map<String, Object> logicParams) {
+
+ Map<String, Object> payload = new HashMap<>();
+
+ int slotCount = getLogicParam(logicParams, "glassSlotCount", 6);
+ if (slotCount <= 0) {
+ slotCount = 6;
+ }
+
+ List<String> slotFields = resolveGlassSlotFields(logicParams, slotCount);
+ for (String field : slotFields) {
+ payload.put(field, "");
+ }
+
+ payload.put("plcGlassCount", 0);
+ payload.put("plcRequest", 0);
+ payload.put("plcReport", 0);
+
+ if (params != null && params.containsKey("positionValue")) {
+ payload.put("inPosition", params.get("positionValue"));
+ } else if (params != null && Boolean.TRUE.equals(params.get("clearPosition"))) {
+ payload.put("inPosition", 0);
+ }
+
+ log.info("娓呯┖澶ц溅璁惧PLC鐜荤拑鏁版嵁: deviceId={}, clearedSlots={}", deviceConfig.getId(), slotFields.size());
+
+ DevicePlcVO.OperationResult result = devicePlcOperationService.writeFields(
+ deviceConfig.getId(),
+ payload,
+ "澶ц溅璁惧-娓呯┖鐜荤拑鏁版嵁"
+ );
+
+ // 娓呯┖鍚庯紝鎭㈠涓虹┖闂茬姸鎬侊紝鍋滄鐩戞帶
+ if (Boolean.TRUE.equals(result.getSuccess())) {
+ statusManager.clearVehicleTask(deviceConfig.getDeviceId());
+ statusManager.updateVehicleStatus(deviceConfig.getDeviceId(), VehicleState.IDLE);
+ stopStateMonitoring(deviceConfig.getDeviceId());
+ }
+
+ return result;
+ }
+
+ private List<String> resolveGlassSlotFields(Map<String, Object> logicParams, int fallbackCount) {
+ List<String> fields = new ArrayList<>();
+ if (logicParams != null) {
+ Object slotFieldConfig = logicParams.get("glassSlotFields");
+ if (slotFieldConfig instanceof List) {
+ List<?> configured = (List<?>) slotFieldConfig;
+ for (Object item : configured) {
+ if (item != null) {
+ String fieldName = String.valueOf(item).trim();
+ if (!fieldName.isEmpty()) {
+ fields.add(fieldName);
+ }
+ }
+ }
+ }
+ }
+
+ if (fields.isEmpty()) {
+ for (int i = 1; i <= fallbackCount; i++) {
+ fields.add("plcGlassId" + i);
+ }
+ }
+ return fields;
+ }
+
+ @Override
+ public String validateLogicParams(DeviceConfig deviceConfig) {
+ Map<String, Object> logicParams = parseLogicParams(deviceConfig);
+
+ // 楠岃瘉蹇呭~鍙傛暟
+ Integer vehicleCapacity = getLogicParam(logicParams, "vehicleCapacity", null);
+ if (vehicleCapacity == null || vehicleCapacity <= 0) {
+ return "杞﹁締瀹归噺(vehicleCapacity)蹇呴』澶т簬0";
+ }
+
+ Integer glassIntervalMs = getLogicParam(logicParams, "glassIntervalMs", null);
+ if (glassIntervalMs != null && glassIntervalMs < 0) {
+ return "鐜荤拑闂撮殧鏃堕棿(glassIntervalMs)涓嶈兘涓鸿礋鏁�";
+ }
+
+ return null; // 楠岃瘉閫氳繃
+ }
+
+ @Override
+ public String getDefaultLogicParams() {
+ Map<String, Object> defaultParams = new HashMap<>();
+ defaultParams.put("vehicleCapacity", 6000);
+ defaultParams.put("glassIntervalMs", 1000);
+ defaultParams.put("autoFeed", true);
+ defaultParams.put("maxRetryCount", 5);
+ defaultParams.put("defaultGlassLength", 2000);
+
+ // MES浠诲姟鐩稿叧閰嶇疆
+ defaultParams.put("vehicleSpeed", 1.0); // 杞﹁締閫熷害锛堟牸/绉掞紝grid/s锛夛紝榛樿1鏍�/绉�
+ defaultParams.put("minRange", 1); // 鏈�灏忚繍鍔ㄨ窛绂伙紙鏍煎瓙锛�
+ defaultParams.put("maxRange", 100); // 鏈�澶ц繍鍔ㄨ窛绂伙紙鏍煎瓙锛夛紝渚嬪100鏍�
+ defaultParams.put("homePosition", 0); // 鍒濆浣嶇疆锛堟牸瀛愶級
+ defaultParams.put("idleMonitorIntervalMs", 2000); // 绌洪棽鐩戞帶闂撮殧锛堟绉掞級
+ defaultParams.put("taskMonitorIntervalMs", 1000); // 浠诲姟鐩戞帶闂撮殧锛堟绉掞級
+ defaultParams.put("mesConfirmTimeoutMs", 30000); // MES纭瓒呮椂锛堟绉掞級
+
+ // 鍑虹墖浠诲姟鐩稿叧閰嶇疆
+ // outboundSlotRanges: 鍑虹墖浠诲姟鐨剆tartSlot鑼冨洿锛屼緥濡俒1, 101]琛ㄧず鏍煎瓙1~101閮芥槸鍑虹墖浠诲姟
+ // 濡傛灉涓嶉厤缃紝鍒欓�氳繃鍒ゆ柇startSlot鏄惁鍦╬ositionMapping涓潵鍖哄垎杩涚墖/鍑虹墖
+ List<Integer> outboundSlotRanges = new ArrayList<>();
+ outboundSlotRanges.add(1); // 鏈�灏忔牸瀛愮紪鍙�
+ outboundSlotRanges.add(101); // 鏈�澶ф牸瀛愮紪鍙�
+ defaultParams.put("outboundSlotRanges", outboundSlotRanges);
+
+ // gridPositionMapping: 鏍煎瓙缂栧彿鍒颁綅缃殑鏄犲皠琛紙鍙�夛級
+ // 濡傛灉涓嶉厤缃紝鍒欐牸瀛愮紪鍙风洿鎺ヤ綔涓轰綅缃��
+ Map<String, Integer> gridPositionMapping = new HashMap<>();
+ defaultParams.put("gridPositionMapping", gridPositionMapping);
+
+ Map<String, Integer> positionMapping = new HashMap<>();
+ positionMapping.put("POS1", 1);
+ positionMapping.put("POS2", 2);
+ defaultParams.put("positionMapping", positionMapping);
+
+ try {
+ return objectMapper.writeValueAsString(defaultParams);
+ } catch (Exception e) {
+ log.error("鐢熸垚榛樿閫昏緫鍙傛暟澶辫触", e);
+ return "{}";
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private List<GlassInfo> extractGlassInfos(Map<String, Object> params) {
+ List<GlassInfo> result = new ArrayList<>();
+ Object rawGlassInfos = params.get("glassInfos");
+ if (rawGlassInfos instanceof List) {
+ List<?> list = (List<?>) rawGlassInfos;
+ for (Object item : list) {
+ GlassInfo info = convertToGlassInfo(item);
+ if (info != null) {
+ result.add(info);
+ }
+ }
+ }
+
+ if (result.isEmpty()) {
+ List<String> glassIds = (List<String>) params.get("glassIds");
+ if (glassIds != null && !glassIds.isEmpty()) {
+ // 浠庢暟鎹簱鏌ヨ鐜荤拑灏哄
+ Map<String, Integer> lengthMap = glassInfoService.getGlassLengthMap(glassIds);
+ for (String glassId : glassIds) {
+ Integer length = lengthMap.get(glassId);
+ result.add(new GlassInfo(glassId, length));
+ }
+ log.debug("浠庢暟鎹簱鏌ヨ鐜荤拑灏哄: glassIds={}, lengthMap={}", glassIds, lengthMap);
+ }
+ }
+ return result;
+ }
+
+ private GlassInfo convertToGlassInfo(Object source) {
+ if (source instanceof GlassInfo) {
+ return (GlassInfo) source;
+ }
+ if (source instanceof Map) {
+ Map<?, ?> map = (Map<?, ?>) source;
+ Object id = map.get("glassId");
+ if (id == null) {
+ id = map.get("id");
+ }
+ if (id == null) {
+ return null;
+ }
+ Integer length = parseLength(map.get("length"));
+ if (length == null) {
+ length = parseLength(map.get("size"));
+ }
+ return new GlassInfo(String.valueOf(id), length);
+ }
+ if (source instanceof String) {
+ return new GlassInfo((String) source, null);
+ }
+ return null;
+ }
+
+ private Integer parseLength(Object value) {
+ if (value instanceof Number) {
+ return ((Number) value).intValue();
+ }
+ if (value instanceof String) {
+ try {
+ return Integer.parseInt((String) value);
+ } catch (NumberFormatException ignored) {
+ }
+ }
+ return null;
+ }
+
+ private List<GlassInfo> planGlassLoading(List<GlassInfo> source,
+ int vehicleCapacity,
+ Integer defaultGlassLength) {
+ List<GlassInfo> planned = new ArrayList<>();
+ int usedLength = 0;
+ int capacity = Math.max(vehicleCapacity, 1);
+ int fallbackLength = defaultGlassLength != null && defaultGlassLength > 0 ? defaultGlassLength : 2000;
+
+ for (GlassInfo info : source) {
+ int length = info.getLength() != null && info.getLength() > 0 ? info.getLength() : fallbackLength;
+ if (planned.isEmpty()) {
+ planned.add(info.withLength(length));
+ usedLength = length;
+ continue;
+ }
+ if (usedLength + length <= capacity) {
+ planned.add(info.withLength(length));
+ usedLength += length;
+ } else {
+ break;
+ }
+ }
+ return planned;
+ }
+
+ /**
+ * 鍚姩鐘舵�佺洃鎺�
+ * 瀹氭湡妫�鏌ュぇ杞︾殑 state1~6锛屽綋妫�娴嬪埌 state=1 鏃惰嚜鍔ㄥ崗璋冨崸杞珛璁惧
+ */
+ private void startStateMonitoring(DeviceConfig deviceConfig, Map<String, Object> logicParams) {
+ String deviceId = deviceConfig.getDeviceId();
+
+ // 濡傛灉宸茬粡鍦ㄧ洃鎺э紝鍏堝仠姝㈡棫鐨勭洃鎺т换鍔�
+ stopStateMonitoring(deviceId);
+
+ // 鑾峰彇鐩戞帶閰嶇疆
+ Integer monitorIntervalMs = getLogicParam(logicParams, "stateMonitorIntervalMs", 1000);
+ Integer monitorTimeoutMs = getLogicParam(logicParams, "stateMonitorTimeoutMs", 300000);
+ if (monitorIntervalMs == null || monitorIntervalMs <= 0) {
+ monitorIntervalMs = 1000;
+ }
+ if (monitorTimeoutMs == null || monitorTimeoutMs <= 0) {
+ monitorTimeoutMs = 300000;
+ }
+ final int finalMonitorIntervalMs = monitorIntervalMs;
+ final int finalMonitorTimeoutMs = monitorTimeoutMs;
+
+ // 鍒濆鍖栧凡鍗忚皟鐘舵�佽褰�
+ coordinatedStates.put(deviceId, new CopyOnWriteArrayList<>());
+
+ // 璁板綍鐩戞帶寮�濮嬫椂闂�
+ final long startTime = System.currentTimeMillis();
+
+ // 鍚姩鐩戞帶浠诲姟
+ ScheduledFuture<?> future = stateMonitorExecutor.scheduleWithFixedDelay(() -> {
+ try {
+ // 妫�鏌ヨ秴鏃�
+ if (System.currentTimeMillis() - startTime > finalMonitorTimeoutMs) {
+ log.info("澶ц溅鐘舵�佺洃鎺ц秴鏃讹紝鍋滄鐩戞帶: deviceId={}, timeout={}ms", deviceId, finalMonitorTimeoutMs);
+ stopStateMonitoring(deviceId);
+ return;
+ }
+
+ // 妫�鏌ヨ溅杈嗘槸鍚﹁繕鍦ㄦ墽琛屼换鍔�
+ VehicleStatus status = statusManager.getVehicleStatus(deviceId);
+ if (status == null || status.getState() != VehicleState.EXECUTING) {
+ log.debug("澶ц溅鐘舵�佸凡鏀瑰彉锛屽仠姝㈢洃鎺�: deviceId={}, state={}",
+ deviceId, status != null ? status.getState() : "null");
+ stopStateMonitoring(deviceId);
+ return;
+ }
+
+ // 鎵ц鐘舵�佹鏌ュ拰鍗忚皟
+ checkAndCoordinateState(deviceConfig);
+
+ } catch (Exception e) {
+ log.error("澶ц溅鐘舵�佺洃鎺у紓甯�: deviceId={}", deviceId, e);
+ }
+ }, finalMonitorIntervalMs, finalMonitorIntervalMs, TimeUnit.MILLISECONDS);
+
+ monitoringTasks.put(deviceId, future);
+ log.info("宸插惎鍔ㄥぇ杞︾姸鎬佺洃鎺�: deviceId={}, interval={}ms, timeout={}ms",
+ deviceId, finalMonitorIntervalMs, finalMonitorTimeoutMs);
+ }
+
+ /**
+ * 鍋滄鐘舵�佺洃鎺�
+ */
+ private void stopStateMonitoring(String deviceId) {
+ ScheduledFuture<?> future = monitoringTasks.remove(deviceId);
+ if (future != null && !future.isCancelled()) {
+ future.cancel(false);
+ log.debug("宸插仠姝㈠ぇ杞︾姸鎬佺洃鎺�: deviceId={}", deviceId);
+ }
+ coordinatedStates.remove(deviceId);
+ }
+
+ /**
+ * 妫�鏌ュぇ杞︾姸鎬佸苟鍗忚皟鍗ц浆绔嬭澶囷紙鍐呴儴鏂规硶锛岀敱鐩戞帶绾跨▼璋冪敤锛�
+ */
+ private void checkAndCoordinateState(DeviceConfig deviceConfig) {
+ String deviceId = deviceConfig.getDeviceId();
+ List<String> alreadyCoordinated = coordinatedStates.get(deviceId);
+ if (alreadyCoordinated == null) {
+ alreadyCoordinated = new CopyOnWriteArrayList<>();
+ coordinatedStates.put(deviceId, alreadyCoordinated);
+ }
+
+ try {
+ // 璇诲彇 state1~6 瀛楁
+ List<String> stateFields = Arrays.asList("state1", "state2", "state3", "state4", "state5", "state6");
+ Map<String, Object> stateValues = new HashMap<>();
+
+ // 浠� PLC 璇诲彇鐘舵�佸瓧娈�
+ DevicePlcVO.StatusInfo statusInfo = devicePlcOperationService.readStatus(deviceConfig.getId());
+ if (statusInfo == null || statusInfo.getFieldValues() == null) {
+ return;
+ }
+
+ for (String field : stateFields) {
+ Object value = statusInfo.getFieldValues().get(field);
+ if (value != null) {
+ stateValues.put(field, value);
+ }
+ }
+
+ // 妫�鏌ユ槸鍚︽湁浠讳綍涓�涓� state 涓� 1锛屼笖灏氭湭鍗忚皟杩�
+ List<String> newStateOneFields = new ArrayList<>();
+ for (String field : stateFields) {
+ Integer stateValue = parseInteger(stateValues.get(field));
+ if (stateValue != null && stateValue == 1 && !alreadyCoordinated.contains(field)) {
+ newStateOneFields.add(field);
+ }
+ }
+
+ if (newStateOneFields.isEmpty()) {
+ return; // 娌℃湁鏂扮殑 state=1锛屾棤闇�鍗忚皟
+ }
+
+ log.info("妫�娴嬪埌澶ц溅鏂扮殑 state=1: deviceId={}, stateFields={}", deviceId, newStateOneFields);
+
+ // 鏌ユ壘鍚岀粍鐨勫崸杞珛璁惧
+ List<DeviceConfig> transferDevices = findTransferDevicesInSameGroup(deviceConfig);
+ if (transferDevices.isEmpty()) {
+ log.warn("鏈壘鍒板悓缁勭殑鍗ц浆绔嬭澶�: deviceId={}", deviceId);
+ // 鍗充娇鎵句笉鍒拌澶囷紝涔熸爣璁颁负宸插崗璋冿紝閬垮厤閲嶅妫�鏌�
+ alreadyCoordinated.addAll(newStateOneFields);
+ return;
+ }
+
+ // 灏嗘瘡涓崸杞珛璁惧鐨� plcRequest 缃� 0
+ boolean allSuccess = true;
+ for (DeviceConfig transferDevice : transferDevices) {
+ Map<String, Object> payload = new HashMap<>();
+ payload.put("plcRequest", 0);
+
+ DevicePlcVO.OperationResult result = devicePlcOperationService.writeFields(
+ transferDevice.getId(),
+ payload,
+ "澶ц溅鑷姩鍗忚皟-娓呯┖鍗ц浆绔嬭姹�"
+ );
+
+ if (Boolean.TRUE.equals(result.getSuccess())) {
+ log.info("宸茶嚜鍔ㄦ竻绌哄崸杞珛璁惧 plcRequest: vehicleDeviceId={}, transferDeviceId={}, transferDeviceName={}, stateFields={}",
+ deviceId, transferDevice.getId(), transferDevice.getDeviceName(), newStateOneFields);
+ } else {
+ log.warn("鑷姩娓呯┖鍗ц浆绔嬭澶� plcRequest 澶辫触: vehicleDeviceId={}, transferDeviceId={}, message={}",
+ deviceId, transferDevice.getId(), result.getMessage());
+ allSuccess = false;
+ }
+ }
+
+ // 鏍囪涓哄凡鍗忚皟锛堟棤璁烘垚鍔熶笌鍚︼紝閬垮厤閲嶅鍗忚皟锛�
+ if (allSuccess) {
+ alreadyCoordinated.addAll(newStateOneFields);
+ log.info("澶ц溅鐘舵�佸崗璋冨畬鎴�: deviceId={}, coordinatedStateFields={}", deviceId, newStateOneFields);
+ }
+
+ } catch (Exception e) {
+ log.error("妫�鏌ュぇ杞︾姸鎬佸苟鍗忚皟鍗ц浆绔嬭澶囧紓甯�: deviceId={}", deviceId, e);
+ }
+ }
+
+ /**
+ * 妫�鏌ュぇ杞︾姸鎬佸苟鍗忚皟鍗ц浆绔嬭澶囷紙鎵嬪姩璋冪敤鎺ュ彛锛�
+ * 褰� state1~6 涓换浣曚竴涓彉涓� 1锛堜笂杞﹀畬鎴愶級鏃讹紝灏嗗悓缁勫崸杞珛璁惧鐨� plcRequest 缃� 0
+ */
+ private DevicePlcVO.OperationResult handleCheckStateAndCoordinate(
+ DeviceConfig deviceConfig,
+ Map<String, Object> params,
+ Map<String, Object> logicParams) {
+
+ try {
+ // 璇诲彇 state1~6 瀛楁
+ List<String> stateFields = Arrays.asList("state1", "state2", "state3", "state4", "state5", "state6");
+ Map<String, Object> stateValues = new HashMap<>();
+
+ // 浠� PLC 璇诲彇鐘舵�佸瓧娈�
+ DevicePlcVO.StatusInfo statusInfo = devicePlcOperationService.readStatus(deviceConfig.getId());
+ if (statusInfo != null && statusInfo.getFieldValues() != null) {
+ for (String field : stateFields) {
+ Object value = statusInfo.getFieldValues().get(field);
+ if (value != null) {
+ stateValues.put(field, value);
+ }
+ }
+ }
+
+ // 妫�鏌ユ槸鍚︽湁浠讳綍涓�涓� state 涓� 1
+ boolean hasStateOne = false;
+ List<String> stateOneFields = new ArrayList<>();
+ for (String field : stateFields) {
+ Integer stateValue = parseInteger(stateValues.get(field));
+ if (stateValue != null && stateValue == 1) {
+ hasStateOne = true;
+ stateOneFields.add(field);
+ }
+ }
+
+ if (!hasStateOne) {
+ return DevicePlcVO.OperationResult.builder()
+ .success(true)
+ .message("褰撳墠鏃� state=1 鐨勭姸鎬侊紝鏃犻渶鍗忚皟")
+ .build();
+ }
+
+ log.info("妫�娴嬪埌澶ц溅 state=1: deviceId={}, stateFields={}",
+ deviceConfig.getId(), stateOneFields);
+
+ // 鏌ユ壘鍚岀粍鐨勫崸杞珛璁惧
+ List<DeviceConfig> transferDevices = findTransferDevicesInSameGroup(deviceConfig);
+ if (transferDevices.isEmpty()) {
+ log.warn("鏈壘鍒板悓缁勭殑鍗ц浆绔嬭澶�: deviceId={}", deviceConfig.getId());
+ return DevicePlcVO.OperationResult.builder()
+ .success(true)
+ .message("鏈壘鍒板悓缁勭殑鍗ц浆绔嬭澶囷紝璺宠繃鍗忚皟")
+ .build();
+ }
+
+ // 灏嗘瘡涓崸杞珛璁惧鐨� plcRequest 缃� 0
+ List<DevicePlcVO.OperationResult> results = new ArrayList<>();
+ for (DeviceConfig transferDevice : transferDevices) {
+ Map<String, Object> payload = new HashMap<>();
+ payload.put("plcRequest", 0);
+
+ DevicePlcVO.OperationResult result = devicePlcOperationService.writeFields(
+ transferDevice.getId(),
+ payload,
+ "澶ц溅鍗忚皟-娓呯┖鍗ц浆绔嬭姹�"
+ );
+ results.add(result);
+
+ if (Boolean.TRUE.equals(result.getSuccess())) {
+ log.info("宸叉竻绌哄崸杞珛璁惧 plcRequest: transferDeviceId={}, transferDeviceName={}",
+ transferDevice.getId(), transferDevice.getDeviceName());
+ } else {
+ log.warn("娓呯┖鍗ц浆绔嬭澶� plcRequest 澶辫触: transferDeviceId={}, message={}",
+ transferDevice.getId(), result.getMessage());
+ }
+ }
+
+ boolean allSuccess = results.stream()
+ .allMatch(r -> Boolean.TRUE.equals(r.getSuccess()));
+
+ return DevicePlcVO.OperationResult.builder()
+ .success(allSuccess)
+ .message(String.format("宸插崗璋� %d 涓崸杞珛璁惧锛屾垚鍔�: %d",
+ transferDevices.size(),
+ results.stream().mapToInt(r -> Boolean.TRUE.equals(r.getSuccess()) ? 1 : 0).sum()))
+ .build();
+
+ } catch (Exception e) {
+ log.error("妫�鏌ュぇ杞︾姸鎬佸苟鍗忚皟鍗ц浆绔嬭澶囧紓甯�: deviceId={}", deviceConfig.getId(), e);
+ return DevicePlcVO.OperationResult.builder()
+ .success(false)
+ .message("鍗忚皟寮傚父: " + e.getMessage())
+ .build();
+ }
+ }
+
+ /**
+ * 鏌ユ壘鍚岀粍鍐呯殑鍗ц浆绔嬭澶�
+ */
+ private List<DeviceConfig> findTransferDevicesInSameGroup(DeviceConfig vehicleDevice) {
+ List<DeviceConfig> transferDevices = new ArrayList<>();
+
+ if (deviceGroupRelationService == null || deviceConfigService == null) {
+ log.warn("DeviceGroupRelationService 鎴� DeviceConfigService 鏈敞鍏ワ紝鏃犳硶鏌ユ壘鍚岀粍璁惧");
+ return transferDevices;
+ }
+
+ try {
+ // 鑾峰彇澶ц溅鎵�灞炵殑璁惧缁�
+ List<DeviceGroupVO.GroupInfo> groups = deviceGroupRelationService.getDeviceGroups(vehicleDevice.getId());
+ if (groups.isEmpty()) {
+ log.debug("澶ц溅璁惧鏈姞鍏ヤ换浣曡澶囩粍: deviceId={}", vehicleDevice.getId());
+ return transferDevices;
+ }
+
+ // 閬嶅巻鎵�鏈夎澶囩粍锛屾煡鎵惧崸杞珛璁惧
+ for (DeviceGroupVO.GroupInfo group : groups) {
+ List<DeviceGroupVO.DeviceInfo> groupDevices = deviceGroupRelationService.getGroupDevices(group.getId());
+ for (DeviceGroupVO.DeviceInfo deviceInfo : groupDevices) {
+ // 妫�鏌ユ槸鍚︿负鍗ц浆绔嬭澶�
+ if (DeviceConfig.DeviceType.WORKSTATION_TRANSFER.equals(deviceInfo.getDeviceType())) {
+ DeviceConfig transferDevice = deviceConfigService.getDeviceById(deviceInfo.getId());
+ if (transferDevice != null) {
+ transferDevices.add(transferDevice);
+ }
+ }
+ }
+ }
+
+ log.debug("鎵惧埌鍚岀粍鍗ц浆绔嬭澶�: vehicleDeviceId={}, transferDeviceCount={}",
+ vehicleDevice.getId(), transferDevices.size());
+
+ } catch (Exception e) {
+ log.error("鏌ユ壘鍚岀粍鍗ц浆绔嬭澶囧紓甯�: vehicleDeviceId={}", vehicleDevice.getId(), e);
+ }
+
+ return transferDevices;
+ }
+
+ private Integer parseInteger(Object value) {
+ if (value instanceof Number) {
+ return ((Number) value).intValue();
+ }
+ if (value == null) {
+ return null;
+ }
+ try {
+ return Integer.parseInt(String.valueOf(value));
+ } catch (NumberFormatException e) {
+ return null;
+ }
+ }
+
+ private String parseString(Object value) {
+ return value == null ? null : String.valueOf(value).trim();
+ }
+
+ private static class GlassInfo {
+ private final String glassId;
+ private final Integer length;
+
+ GlassInfo(String glassId, Integer length) {
+ this.glassId = glassId;
+ this.length = length;
+ }
+
+ public String getGlassId() {
+ return glassId;
+ }
+
+ public Integer getLength() {
+ return length;
+ }
+
+ public GlassInfo withLength(Integer newLength) {
+ return new GlassInfo(this.glassId, newLength);
+ }
+
+ @Override
+ public String toString() {
+ return glassId;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(glassId, length);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) return true;
+ if (obj == null || getClass() != obj.getClass()) return false;
+ GlassInfo other = (GlassInfo) obj;
+ return Objects.equals(glassId, other.glassId) && Objects.equals(length, other.length);
+ }
+ }
+
+ /**
+ * 鍚姩绌洪棽鐩戞帶锛堟病鏈変换鍔℃椂锛宲lcRequest涓�鐩翠繚鎸佷负1锛�
+ */
+ private DevicePlcVO.OperationResult handleStartIdleMonitor(
+ DeviceConfig deviceConfig,
+ Map<String, Object> params,
+ Map<String, Object> logicParams) {
+
+ String deviceId = deviceConfig.getDeviceId();
+
+ // 鍋滄鏃х殑鐩戞帶浠诲姟
+ handleStopIdleMonitor(deviceConfig);
+
+ // 鑾峰彇鐩戞帶闂撮殧
+ Integer monitorIntervalMs = getLogicParam(logicParams, "idleMonitorIntervalMs", 2000); // 榛樿2绉�
+
+ // 鍚姩鐩戞帶浠诲姟
+ ScheduledFuture<?> future = idleMonitorExecutor.scheduleWithFixedDelay(() -> {
+ try {
+ // 妫�鏌ユ槸鍚︽湁浠诲姟鍦ㄦ墽琛�
+ VehicleStatus status = statusManager.getVehicleStatus(deviceId);
+ if (status != null && status.getState() == VehicleState.EXECUTING) {
+ // 鏈変换鍔″湪鎵ц锛屼笉璁剧疆plcRequest
+ return;
+ }
+
+ // 妫�鏌ユ槸鍚︽湁寰呭鐞嗙殑杩涚墖鎴栧嚭鐗囦换鍔�
+ if (plcDynamicDataService != null && s7SerializerProvider != null) {
+ EnhancedS7Serializer serializer = s7SerializerProvider.getSerializer(deviceConfig);
+ if (serializer != null) {
+ // 妫�鏌ヨ繘鐗囦换鍔�
+ Map<String, Object> mesData = plcDynamicDataService.readPlcData(
+ deviceConfig, MES_FIELDS, serializer);
+ Integer mesSend = parseInteger(mesData != null ? mesData.get("mesSend") : null);
+
+ // 杩涚墖鍜屽嚭鐗囧叡鐢╩esSend瀛楁锛屽彧闇�妫�鏌ヤ竴娆�
+ // 濡傛灉鏈夊緟澶勭悊鐨勪换鍔★紝涓嶈缃畃lcRequest锛堢瓑寰呬换鍔″鐞嗭級
+ if (mesSend != null && mesSend == 1) {
+ log.debug("澶ц溅绌洪棽鐩戞帶: deviceId={}, 妫�娴嬪埌寰呭鐞嗕换鍔★紙mesSend=1锛夛紝涓嶈缃畃lcRequest", deviceId);
+ return;
+ }
+ }
+ }
+
+ // 娌℃湁浠诲姟锛屼繚鎸乸lcRequest=1
+ Map<String, Object> payload = new HashMap<>();
+ payload.put("plcRequest", 1);
+
+ devicePlcOperationService.writeFields(deviceConfig.getId(), payload, "澶ц溅绌洪棽鐩戞帶-淇濇寔璇锋眰");
+ log.debug("澶ц溅绌洪棽鐩戞帶: deviceId={}, 淇濇寔plcRequest=1", deviceId);
+
+ } catch (Exception e) {
+ log.error("澶ц溅绌洪棽鐩戞帶寮傚父: deviceId={}", deviceId, e);
+ }
+ }, monitorIntervalMs, monitorIntervalMs, TimeUnit.MILLISECONDS);
+
+ idleMonitoringTasks.put(deviceId, future);
+ log.info("宸插惎鍔ㄥぇ杞︾┖闂茬洃鎺�: deviceId={}, interval={}ms", deviceId, monitorIntervalMs);
+
+ return DevicePlcVO.OperationResult.builder()
+ .success(true)
+ .message("绌洪棽鐩戞帶宸插惎鍔�")
+ .build();
+ }
+
+ /**
+ * 鍋滄绌洪棽鐩戞帶
+ */
+ private DevicePlcVO.OperationResult handleStopIdleMonitor(DeviceConfig deviceConfig) {
+ String deviceId = deviceConfig.getDeviceId();
+ ScheduledFuture<?> future = idleMonitoringTasks.remove(deviceId);
+ if (future != null && !future.isCancelled()) {
+ future.cancel(false);
+ log.info("宸插仠姝㈠ぇ杞︾┖闂茬洃鎺�: deviceId={}", deviceId);
+ }
+ return DevicePlcVO.OperationResult.builder()
+ .success(true)
+ .message("绌洪棽鐩戞帶宸插仠姝�")
+ .build();
+ }
+
+ /**
+ * 妫�鏌ES浠诲姟锛堝綋mesSend=1鏃讹紝璇诲彇MES鍙傛暟骞跺垱寤轰换鍔★級
+ * 杩涚墖鍜屽嚭鐗囧叡鐢ㄥ悓涓�濂楀崗璁瓧娈碉紝閫氳繃浣嶇疆淇℃伅鍒ゆ柇浠诲姟绫诲瀷
+ */
+ private DevicePlcVO.OperationResult handleCheckMesTask(
+ DeviceConfig deviceConfig,
+ Map<String, Object> params,
+ Map<String, Object> logicParams) {
+
+ if (plcDynamicDataService == null || s7SerializerProvider == null) {
+ return DevicePlcVO.OperationResult.builder()
+ .success(false)
+ .message("PlcDynamicDataService鎴朣7SerializerProvider鏈敞鍏�")
+ .build();
+ }
+
+ String deviceId = deviceConfig.getDeviceId();
+ EnhancedS7Serializer serializer = s7SerializerProvider.getSerializer(deviceConfig);
+ if (serializer == null) {
+ return DevicePlcVO.OperationResult.builder()
+ .success(false)
+ .message("鑾峰彇PLC搴忓垪鍖栧櫒澶辫触")
+ .build();
+ }
+
+ try {
+ // 璇诲彇MES瀛楁锛堣繘鐗囧拰鍑虹墖鍏辩敤锛�
+ Map<String, Object> mesData = plcDynamicDataService.readPlcData(
+ deviceConfig, MES_FIELDS, serializer);
+ if (mesData == null || mesData.isEmpty()) {
+ return DevicePlcVO.OperationResult.builder()
+ .success(false)
+ .message("璇诲彇MES瀛楁澶辫触")
+ .build();
+ }
+
+ Integer mesSend = parseInteger(mesData.get("mesSend"));
+ if (mesSend == null || mesSend == 0) {
+ return DevicePlcVO.OperationResult.builder()
+ .success(true)
+ .message("鏆傛棤MES浠诲姟锛坢esSend=0锛�")
+ .build();
+ }
+
+ // mesSend=1锛岃鍙栦换鍔″弬鏁�
+ String glassId = parseString(mesData.get("mesGlassId"));
+ Integer startSlot = parseInteger(mesData.get("startSlot")); // 璧峰浣嶇疆缂栧彿
+ Integer targetSlot = parseInteger(mesData.get("targetSlot")); // 鐩爣浣嶇疆缂栧彿
+ Integer workLine = parseInteger(mesData.get("workLine"));
+
+ if (glassId == null || glassId.isEmpty()) {
+ return DevicePlcVO.OperationResult.builder()
+ .success(false)
+ .message("MES鏈彁渚涚幓鐠僆D")
+ .build();
+ }
+
+ // 鍒ゆ柇鏄繘鐗囪繕鏄嚭鐗囦换鍔�
+ // 鏂规硶锛氶�氳繃startSlot鍒ゆ柇
+ // - 濡傛灉startSlot鏄崸杞珛缂栧彿锛堝900/901锛夛紝鍒欐槸杩涚墖浠诲姟
+ // - 濡傛灉startSlot鏄牸瀛愮紪鍙凤紙鍦ㄥぇ鐞嗙墖绗艰寖鍥村唴锛夛紝鍒欐槸鍑虹墖浠诲姟
+ boolean isOutbound = isOutboundTask(startSlot, logicParams);
+
+ // 浣嶇疆鏄犲皠
+ Integer startPosition;
+ if (isOutbound) {
+ // 鍑虹墖浠诲姟锛歴tartSlot鏄牸瀛愮紪鍙凤紝闇�瑕佹槧灏勫埌瀹為檯浣嶇疆
+ startPosition = mapOutboundPosition(startSlot, logicParams);
+ } else {
+ // 杩涚墖浠诲姟锛歴tartSlot鏄崸杞珛缂栧彿锛岄�氳繃positionMapping鏄犲皠
+ startPosition = mapPosition(startSlot, logicParams);
+ }
+
+ // targetSlot缁熶竴閫氳繃positionMapping鏄犲皠
+ Integer targetPosition = mapPosition(targetSlot, logicParams);
+
+ if (startPosition == null || targetPosition == null) {
+ return DevicePlcVO.OperationResult.builder()
+ .success(false)
+ .message(String.format("浣嶇疆鏄犲皠澶辫触: startSlot=%s, targetSlot=%s, isOutbound=%s",
+ startSlot, targetSlot, isOutbound))
+ .build();
+ }
+
+ // 璇诲彇褰撳墠浣嶇疆
+ Integer currentPosition = getCurrentPosition(deviceConfig, logicParams);
+
+ // 璁$畻鏃堕棿
+ TimeCalculation timeCalc = calculateTime(
+ currentPosition, startPosition, targetPosition, logicParams);
+
+ // 鍒涘缓浠诲姟淇℃伅
+ MesTaskInfo taskInfo = new MesTaskInfo();
+ taskInfo.glassId = glassId;
+ taskInfo.startSlot = startSlot;
+ taskInfo.targetSlot = targetSlot;
+ taskInfo.startPosition = startPosition;
+ taskInfo.targetPosition = targetPosition;
+ taskInfo.currentPosition = currentPosition;
+ taskInfo.gotime = timeCalc.gotime;
+ taskInfo.cartime = timeCalc.cartime;
+ taskInfo.workLine = workLine;
+ taskInfo.createdTime = System.currentTimeMillis();
+ taskInfo.isOutbound = isOutbound;
+
+ currentTasks.put(deviceId, taskInfo);
+
+ // 娓呯┖plcRequest锛堣〃绀哄凡鎺ユ敹浠诲姟锛�
+ Map<String, Object> payload = new HashMap<>();
+ payload.put("plcRequest", 0);
+ plcDynamicDataService.writePlcData(deviceConfig, payload, serializer);
+
+ // 鏇存柊杞﹁締鐘舵�佷负鎵ц涓�
+ statusManager.updateVehicleStatus(deviceId, deviceConfig.getDeviceName(), VehicleState.EXECUTING);
+
+ // 鍚姩浠诲姟鐩戞帶
+ handleStartTaskMonitor(deviceConfig, params, logicParams);
+
+ String taskType = isOutbound ? "鍑虹墖" : "杩涚墖";
+ log.info("MES{}浠诲姟宸插垱寤�: deviceId={}, glassId={}, startSlot={}(浣嶇疆{}鏍�), targetSlot={}(浣嶇疆{}鏍�), 璺濈{}鏍�->{}鏍�, gotime={}ms({}绉�), cartime={}ms({}绉�)",
+ taskType, deviceId, glassId, startSlot, startPosition, targetSlot, targetPosition,
+ Math.abs(startPosition - currentPosition), Math.abs(targetPosition - startPosition),
+ timeCalc.gotime, timeCalc.gotime / 1000.0, timeCalc.cartime, timeCalc.cartime / 1000.0);
+
+ return DevicePlcVO.OperationResult.builder()
+ .success(true)
+ .message(String.format("MES%s浠诲姟宸插垱寤�: glassId=%s, start=%d, target=%d",
+ taskType, glassId, startPosition, targetPosition))
+ .build();
+
+ } catch (Exception e) {
+ log.error("妫�鏌ES浠诲姟寮傚父: deviceId={}", deviceId, e);
+ return DevicePlcVO.OperationResult.builder()
+ .success(false)
+ .message("澶勭悊寮傚父: " + e.getMessage())
+ .build();
+ }
+ }
+
+ /**
+ * 鍒ゆ柇鏄惁涓哄嚭鐗囦换鍔�
+ * 閫氳繃startSlot鍒ゆ柇锛�
+ * - 濡傛灉startSlot鍦╬ositionMapping涓紝涓斾笉鍦ㄥぇ鐞嗙墖绗兼牸瀛愯寖鍥村唴锛屽垯鏄繘鐗囦换鍔�
+ * - 濡傛灉startSlot涓嶅湪positionMapping涓紝鎴栧湪澶х悊鐗囩鏍煎瓙鑼冨洿鍐咃紝鍒欐槸鍑虹墖浠诲姟
+ */
+ private boolean isOutboundTask(Integer startSlot, Map<String, Object> logicParams) {
+ if (startSlot == null) {
+ return false;
+ }
+
+ // 鏂规硶1锛氭鏌tartSlot鏄惁鍦╬ositionMapping涓紙鍗ц浆绔嬬紪鍙凤級
+ @SuppressWarnings("unchecked")
+ Map<String, Integer> positionMapping = getLogicParam(logicParams, "positionMapping", new HashMap<>());
+ if (positionMapping.containsKey(String.valueOf(startSlot))) {
+ // startSlot鍦╬ositionMapping涓紝璇存槑鏄崸杞珛缂栧彿锛屾槸杩涚墖浠诲姟
+ return false;
+ }
+
+ // 鏂规硶2锛氭鏌tartSlot鏄惁鍦ㄥぇ鐞嗙墖绗兼牸瀛愯寖鍥村唴
+ // 閫氳繃鏌ユ壘鍚岀粍鐨勫ぇ鐞嗙墖绗艰澶囷紝妫�鏌ユ牸瀛愯寖鍥�
+ // 杩欓噷绠�鍖栧鐞嗭細濡傛灉startSlot涓嶅湪positionMapping涓紝涓旀槸鏁板瓧锛屽彲鑳芥槸鏍煎瓙缂栧彿
+ // 鍙互閫氳繃閰嶇疆鎸囧畾鏍煎瓙缂栧彿鑼冨洿锛屾垨鑰呴�氳繃鏌ユ壘鍚岀粍璁惧鍒ゆ柇
+
+ // 鏂规硶3锛氶�氳繃閰嶇疆鎸囧畾鍑虹墖浠诲姟鐨剆tartSlot鑼冨洿
+ @SuppressWarnings("unchecked")
+ List<Integer> outboundSlotRanges = getLogicParam(logicParams, "outboundSlotRanges", null);
+ if (outboundSlotRanges != null && !outboundSlotRanges.isEmpty()) {
+ // 濡傛灉閰嶇疆浜嗗嚭鐗噑lot鑼冨洿锛屾鏌tartSlot鏄惁鍦ㄨ寖鍥村唴
+ // 渚嬪锛歔1, 101] 琛ㄧず鏍煎瓙1~101閮芥槸鍑虹墖浠诲姟
+ if (outboundSlotRanges.size() >= 2) {
+ int minSlot = outboundSlotRanges.get(0);
+ int maxSlot = outboundSlotRanges.get(1);
+ if (startSlot >= minSlot && startSlot <= maxSlot) {
+ return true;
+ }
+ }
+ }
+
+ // 榛樿锛氬鏋渟tartSlot涓嶅湪positionMapping涓紝涓旀槸杈冨皬鐨勬暟瀛楋紙鍙兘鏄牸瀛愮紪鍙凤級锛屽垽鏂负鍑虹墖
+ // 杩欓噷鍙互鏍规嵁瀹為檯闇�姹傝皟鏁村垽鏂�昏緫
+ // 鏆傛椂锛氬鏋渟tartSlot涓嶅湪positionMapping涓紝鍒ゆ柇涓哄嚭鐗囦换鍔�
+ return true;
+ }
+
+
+ /**
+ * 鏄犲皠鍑虹墖婧愪綅缃紙鏍煎瓙缂栧彿杞崲涓哄疄闄呬綅缃級
+ * 閫氳繃鏌ユ壘鍚岀粍鐨勫ぇ鐞嗙墖绗艰澶囬厤缃紝灏嗘牸瀛愮紪鍙疯浆鎹负浣嶇疆
+ */
+ private Integer mapOutboundPosition(Integer gridNumber, Map<String, Object> logicParams) {
+ if (gridNumber == null) {
+ return null;
+ }
+
+ // 鏂规硶1锛氬鏋滈厤缃簡鏍煎瓙鍒颁綅缃殑鏄犲皠琛�
+ @SuppressWarnings("unchecked")
+ Map<String, Integer> gridPositionMapping = getLogicParam(logicParams, "gridPositionMapping", new HashMap<>());
+ Integer position = gridPositionMapping.get(String.valueOf(gridNumber));
+ if (position != null) {
+ return position;
+ }
+
+ // 鏂规硶2锛氭牸瀛愮紪鍙风洿鎺ヤ綔涓轰綅缃紙濡傛灉鏍煎瓙缂栧彿灏辨槸浣嶇疆鍊硷級
+ // 渚嬪锛氭牸瀛�1瀵瑰簲浣嶇疆1鏍硷紝鏍煎瓙52瀵瑰簲浣嶇疆52鏍�
+ // 杩欓噷鍙互鏍规嵁瀹為檯闇�姹傝皟鏁存槧灏勯�昏緫
+
+ // 鏆傛椂鐩存帴浣跨敤鏍煎瓙缂栧彿浣滀负浣嶇疆
+ log.debug("浣跨敤鏍煎瓙缂栧彿浣滀负浣嶇疆: gridNumber={}", gridNumber);
+ return gridNumber;
+ }
+
+ /**
+ * 浣嶇疆鏄犲皠锛氬皢MES缁欑殑缂栧彿锛堝900/901锛夎浆鎹负瀹為檯浣嶇疆鍊硷紙濡�100/500锛�
+ */
+ private Integer mapPosition(Integer slotNumber, Map<String, Object> logicParams) {
+ if (slotNumber == null) {
+ return null;
+ }
+
+ // 浠庨厤缃腑鑾峰彇浣嶇疆鏄犲皠琛�
+ @SuppressWarnings("unchecked")
+ Map<String, Integer> positionMapping = getLogicParam(logicParams, "positionMapping", new HashMap<>());
+
+ // 鏌ユ壘鏄犲皠
+ Integer position = positionMapping.get(String.valueOf(slotNumber));
+ if (position != null) {
+ return position;
+ }
+
+ // 濡傛灉娌℃湁閰嶇疆鏄犲皠锛屽皾璇曠洿鎺ヤ娇鐢ㄧ紪鍙�
+ log.warn("浣嶇疆鏄犲皠鏈壘鍒�: slotNumber={}, 浣跨敤缂栧彿浣滀负浣嶇疆鍊�", slotNumber);
+ return slotNumber;
+ }
+
+ /**
+ * 鑾峰彇褰撳墠浣嶇疆
+ */
+ private Integer getCurrentPosition(DeviceConfig deviceConfig, Map<String, Object> logicParams) {
+ // 浠庣姸鎬佺鐞嗗櫒鑾峰彇
+ VehicleStatus status = statusManager.getVehicleStatus(deviceConfig.getDeviceId());
+ if (status != null && status.getCurrentPosition() != null) {
+ return status.getCurrentPosition().getPositionValue();
+ }
+
+ // 浠庨厤缃腑鑾峰彇榛樿浣嶇疆
+ return getLogicParam(logicParams, "homePosition", 0);
+ }
+
+ /**
+ * 鏃堕棿璁$畻锛氭牴鎹�熷害銆佸綋鍓嶄綅缃�佺洰鏍囦綅缃绠梘otime鍜宑artime
+ * 閫熷害鍗曚綅锛氭牸/绉掞紙grid/s锛�
+ * 浣嶇疆鍜岃窛绂诲崟浣嶏細鏍煎瓙锛坓rid锛�
+ */
+ private TimeCalculation calculateTime(Integer currentPos, Integer startPos,
+ Integer targetPos, Map<String, Object> logicParams) {
+ // 鑾峰彇閫熷害锛堟牸/绉掞紝grid/s锛�
+ Double speed = getLogicParam(logicParams, "vehicleSpeed", 1.0);
+ if (speed == null || speed <= 0) {
+ speed = 1.0; // 榛樿1鏍�/绉�
+ }
+
+ // 鑾峰彇杩愬姩璺濈鑼冨洿锛堟牸瀛愶級
+ Integer minRange = getLogicParam(logicParams, "minRange", 1);
+ Integer maxRange = getLogicParam(logicParams, "maxRange", 100);
+
+ // 璁$畻gotime锛氫粠褰撳墠浣嶇疆鍒拌捣濮嬩綅缃殑鏃堕棿锛堟绉掞級
+ // 鍏紡锛氭椂闂�(ms) = 璺濈(鏍�) / 閫熷害(鏍�/绉�) * 1000
+ long gotime = 0;
+ if (currentPos != null && startPos != null) {
+ int distance = Math.abs(startPos - currentPos); // 璺濈锛堟牸瀛愶級
+ // 闄愬埗鍦ㄨ寖鍥村唴
+ distance = Math.max(minRange, Math.min(maxRange, distance));
+ gotime = (long) (distance / speed * 1000); // 杞崲涓烘绉�
+ }
+
+ // 璁$畻cartime锛氫粠璧峰浣嶇疆鍒扮洰鏍囦綅缃殑鏃堕棿锛堟绉掞級
+ // 鍏紡锛氭椂闂�(ms) = 璺濈(鏍�) / 閫熷害(鏍�/绉�) * 1000
+ long cartime = 0;
+ if (startPos != null && targetPos != null) {
+ int distance = Math.abs(targetPos - startPos); // 璺濈锛堟牸瀛愶級
+ // 闄愬埗鍦ㄨ寖鍥村唴
+ distance = Math.max(minRange, Math.min(maxRange, distance));
+ cartime = (long) (distance / speed * 1000); // 杞崲涓烘绉�
+ }
+
+ return new TimeCalculation(gotime, cartime);
+ }
+
+ /**
+ * 鍚姩浠诲姟鐩戞帶锛堢洃鎺tate鐘舵�佸垏鎹㈠拰浠诲姟瀹屾垚锛�
+ */
+ private DevicePlcVO.OperationResult handleStartTaskMonitor(
+ DeviceConfig deviceConfig,
+ Map<String, Object> params,
+ Map<String, Object> logicParams) {
+
+ String deviceId = deviceConfig.getDeviceId();
+
+ // 鍋滄鏃х殑鐩戞帶浠诲姟
+ handleStopTaskMonitor(deviceConfig);
+
+ MesTaskInfo taskInfo = currentTasks.get(deviceId);
+ if (taskInfo == null) {
+ return DevicePlcVO.OperationResult.builder()
+ .success(false)
+ .message("娌℃湁姝e湪鎵ц鐨勪换鍔�")
+ .build();
+ }
+
+ // 鑾峰彇鐩戞帶闂撮殧
+ Integer monitorIntervalMs = getLogicParam(logicParams, "taskMonitorIntervalMs", 1000); // 榛樿1绉�
+
+ // 鍚姩鐩戞帶浠诲姟
+ ScheduledFuture<?> future = taskMonitorExecutor.scheduleWithFixedDelay(() -> {
+ try {
+ monitorTaskExecution(deviceConfig, taskInfo, logicParams);
+ } catch (Exception e) {
+ log.error("浠诲姟鐩戞帶寮傚父: deviceId={}", deviceId, e);
+ }
+ }, monitorIntervalMs, monitorIntervalMs, TimeUnit.MILLISECONDS);
+
+ taskMonitoringTasks.put(deviceId, future);
+ log.info("宸插惎鍔ㄤ换鍔$洃鎺�: deviceId={}, interval={}ms", deviceId, monitorIntervalMs);
+
+ return DevicePlcVO.OperationResult.builder()
+ .success(true)
+ .message("浠诲姟鐩戞帶宸插惎鍔�")
+ .build();
+ }
+
+ /**
+ * 鐩戞帶浠诲姟鎵ц
+ */
+ private void monitorTaskExecution(DeviceConfig deviceConfig,
+ MesTaskInfo taskInfo,
+ Map<String, Object> logicParams) {
+
+ if (plcDynamicDataService == null || s7SerializerProvider == null) {
+ return;
+ }
+
+ String deviceId = deviceConfig.getDeviceId();
+ EnhancedS7Serializer serializer = s7SerializerProvider.getSerializer(deviceConfig);
+ if (serializer == null) {
+ return;
+ }
+
+ try {
+ // 璇诲彇state1~6
+ List<String> stateFields = Arrays.asList("state1", "state2", "state3", "state4", "state5", "state6");
+ Map<String, Object> stateValues = new HashMap<>();
+
+ DevicePlcVO.StatusInfo statusInfo = devicePlcOperationService.readStatus(deviceConfig.getId());
+ if (statusInfo != null && statusInfo.getFieldValues() != null) {
+ for (String field : stateFields) {
+ Object value = statusInfo.getFieldValues().get(field);
+ if (value != null) {
+ stateValues.put(field, value);
+ }
+ }
+ }
+
+ // 鏍规嵁鏃堕棿璁$畻鏇存柊state鐘舵��
+ long currentTime = System.currentTimeMillis();
+ long elapsed = currentTime - taskInfo.createdTime;
+
+ // 璁$畻鐘舵�佸垏鎹㈡椂闂寸偣
+ long state1Time = taskInfo.gotime; // 鍒拌揪璧峰浣嶇疆锛屼笂杞﹀畬鎴�
+ long state2Time = taskInfo.gotime + taskInfo.cartime; // 鍒拌揪鐩爣浣嶇疆锛岃繍杈撳畬鎴�
+
+ // 鏇存柊state鐘舵��
+ if (elapsed >= state1Time && elapsed < state2Time) {
+ // state搴旇涓�1
+ if (taskInfo.isOutbound) {
+ // 鍑虹墖浠诲姟锛氬埌杈炬簮浣嶇疆锛堝ぇ鐞嗙墖绗硷級锛屽彇鐗囧畬鎴�
+ updateStateIfNeeded(deviceConfig, serializer, stateValues, 1, taskInfo);
+ } else {
+ // 杩涚墖浠诲姟锛氬埌杈捐捣濮嬩綅缃紙鍗ц浆绔嬶級锛屼笂杞﹀畬鎴�
+ updateStateIfNeeded(deviceConfig, serializer, stateValues, 1, taskInfo);
+ }
+ } else if (elapsed >= state2Time) {
+ // state搴旇涓�2锛堣繍杈撳畬鎴愶級
+ updateStateIfNeeded(deviceConfig, serializer, stateValues, 2, taskInfo);
+
+ // 妫�鏌ユ槸鍚︽墍鏈塻tate閮�>=2锛屽鏋滄槸鍒欑粰MES姹囨姤
+ if (allStatesCompleted(stateValues)) {
+ reportToMes(deviceConfig, serializer, taskInfo, logicParams);
+ }
+ }
+
+ } catch (Exception e) {
+ log.error("鐩戞帶浠诲姟鎵ц寮傚父: deviceId={}", deviceId, e);
+ }
+ }
+
+ /**
+ * 鏇存柊state鐘舵�侊紙濡傛灉闇�瑕侊級
+ */
+ private void updateStateIfNeeded(DeviceConfig deviceConfig,
+ EnhancedS7Serializer serializer,
+ Map<String, Object> currentStates,
+ int targetState,
+ MesTaskInfo taskInfo) {
+
+ // 杩欓噷鍙互鏍规嵁瀹為檯闇�姹傛洿鏂皊tate瀛楁
+ // 鏆傛椂鍙褰曟棩蹇楋紝瀹為檯鏇存柊鍙兘闇�瑕佹牴鎹叿浣揚LC瀛楁閰嶇疆
+ log.debug("浠诲姟鐘舵�佹洿鏂�: deviceId={}, targetState={}",
+ deviceConfig.getDeviceId(), targetState);
+ }
+
+ /**
+ * 妫�鏌ユ槸鍚︽墍鏈塻tate閮藉凡瀹屾垚锛�>=2锛�
+ */
+ private boolean allStatesCompleted(Map<String, Object> stateValues) {
+ for (Object value : stateValues.values()) {
+ Integer state = parseInteger(value);
+ if (state == null || state < 2) {
+ return false;
+ }
+ }
+ return !stateValues.isEmpty();
+ }
+
+ /**
+ * 缁橫ES姹囨姤
+ */
+ private void reportToMes(DeviceConfig deviceConfig,
+ EnhancedS7Serializer serializer,
+ MesTaskInfo taskInfo,
+ Map<String, Object> logicParams) {
+
+ try {
+ // 璁剧疆姹囨姤瀛�
+ Map<String, Object> payload = new HashMap<>();
+ payload.put("plcReport", 1);
+ plcDynamicDataService.writePlcData(deviceConfig, payload, serializer);
+
+ String taskType = taskInfo.isOutbound ? "鍑虹墖" : "杩涚墖";
+ log.info("宸茬粰MES姹囨姤({}浠诲姟): deviceId={}, glassId={}",
+ taskType, deviceConfig.getDeviceId(), taskInfo.glassId);
+
+ // 绛夊緟MES纭
+ waitForMesConfirm(deviceConfig, serializer, taskInfo, logicParams);
+
+ } catch (Exception e) {
+ log.error("缁橫ES姹囨姤寮傚父: deviceId={}", deviceConfig.getDeviceId(), e);
+ }
+ }
+
+ /**
+ * 绛夊緟MES纭
+ */
+ private void waitForMesConfirm(DeviceConfig deviceConfig,
+ EnhancedS7Serializer serializer,
+ MesTaskInfo taskInfo,
+ Map<String, Object> logicParams) {
+
+ try {
+ // 璇诲彇纭瀛楋紙鍋囪瀛楁鍚嶄负mesConfirm锛�
+ Integer maxWaitTime = getLogicParam(logicParams, "mesConfirmTimeoutMs", 30000); // 榛樿30绉�
+ long startTime = System.currentTimeMillis();
+
+ while (System.currentTimeMillis() - startTime < maxWaitTime) {
+ Object confirmValue = plcDynamicDataService.readPlcField(
+ deviceConfig, "mesConfirm", serializer);
+ Integer confirm = parseInteger(confirmValue);
+
+ if (confirm != null && confirm == 1) {
+ // MES宸茬‘璁わ紝娓呯┖state鍜屾眹鎶ュ瓧
+ clearTaskStates(deviceConfig, serializer);
+
+ // 浠诲姟瀹屾垚锛屾仮澶嶄负绌洪棽鐘舵��
+ statusManager.updateVehicleStatus(
+ deviceConfig.getDeviceId(), VehicleState.IDLE);
+ statusManager.clearVehicleTask(deviceConfig.getDeviceId());
+
+ // 绉婚櫎浠诲姟
+ currentTasks.remove(deviceConfig.getDeviceId());
+
+ // 鍋滄浠诲姟鐩戞帶
+ handleStopTaskMonitor(deviceConfig);
+
+ // 鎭㈠plcRequest=1锛堝彲浠ユ帴鏀舵柊浠诲姟锛�
+ Map<String, Object> payload = new HashMap<>();
+ payload.put("plcRequest", 1);
+ plcDynamicDataService.writePlcData(deviceConfig, payload, serializer);
+
+ log.info("MES浠诲姟宸插畬鎴�: deviceId={}, glassId={}",
+ deviceConfig.getDeviceId(), taskInfo.glassId);
+ return;
+ }
+
+ Thread.sleep(500); // 绛夊緟500ms鍚庨噸璇�
+ }
+
+ log.warn("绛夊緟MES纭瓒呮椂: deviceId={}, glassId={}",
+ deviceConfig.getDeviceId(), taskInfo.glassId);
+
+ } catch (Exception e) {
+ log.error("绛夊緟MES纭寮傚父: deviceId={}", deviceConfig.getDeviceId(), e);
+ }
+ }
+
+ /**
+ * 娓呯┖浠诲姟鐘舵��
+ */
+ private void clearTaskStates(DeviceConfig deviceConfig, EnhancedS7Serializer serializer) {
+ try {
+ Map<String, Object> payload = new HashMap<>();
+ // 娓呯┖state1~6
+ for (int i = 1; i <= 6; i++) {
+ payload.put("state" + i, 0);
+ }
+ // 娓呯┖姹囨姤瀛�
+ payload.put("plcReport", 0);
+ plcDynamicDataService.writePlcData(deviceConfig, payload, serializer);
+ } catch (Exception e) {
+ log.error("娓呯┖浠诲姟鐘舵�佸紓甯�: deviceId={}", deviceConfig.getDeviceId(), e);
+ }
+ }
+
+ /**
+ * 鍋滄浠诲姟鐩戞帶
+ */
+ private DevicePlcVO.OperationResult handleStopTaskMonitor(DeviceConfig deviceConfig) {
+ String deviceId = deviceConfig.getDeviceId();
+ ScheduledFuture<?> future = taskMonitoringTasks.remove(deviceId);
+ if (future != null && !future.isCancelled()) {
+ future.cancel(false);
+ log.info("宸插仠姝换鍔$洃鎺�: deviceId={}", deviceId);
+ }
+ return DevicePlcVO.OperationResult.builder()
+ .success(true)
+ .message("浠诲姟鐩戞帶宸插仠姝�")
+ .build();
+ }
+
+ /**
+ * 鏃堕棿璁$畻缁撴灉
+ */
+ private static class TimeCalculation {
+ final long gotime; // 鍒拌捣濮嬩綅缃殑鏃堕棿锛堟绉掞級
+ final long cartime; // 浠庤捣濮嬪埌鐩爣浣嶇疆鐨勬椂闂达紙姣锛�
+
+ TimeCalculation(long gotime, long cartime) {
+ this.gotime = gotime;
+ this.cartime = cartime;
+ }
+ }
+
+ /**
+ * MES浠诲姟淇℃伅
+ */
+ private static class MesTaskInfo {
+ String glassId;
+ Integer startSlot;
+ Integer targetSlot;
+ Integer startPosition;
+ Integer targetPosition;
+ Integer currentPosition;
+ long gotime;
+ long cartime;
+ Integer workLine;
+ long createdTime;
+ boolean isOutbound = false; // 鏄惁涓哄嚭鐗囦换鍔★紙false=杩涚墖锛宼rue=鍑虹墖锛�
+ }
+
+ /**
+ * 搴旂敤鍏抽棴鏃舵竻鐞嗚祫婧�
+ */
+ @PreDestroy
+ public void destroy() {
+ log.info("姝e湪鍏抽棴澶ц溅鐩戞帶绾跨▼姹�...");
+
+ // 鍋滄鎵�鏈夌洃鎺т换鍔�
+ for (String deviceId : new ArrayList<>(monitoringTasks.keySet())) {
+ stopStateMonitoring(deviceId);
+ }
+ for (String deviceId : new ArrayList<>(idleMonitoringTasks.keySet())) {
+ ScheduledFuture<?> future = idleMonitoringTasks.remove(deviceId);
+ if (future != null) future.cancel(false);
+ }
+ for (String deviceId : new ArrayList<>(taskMonitoringTasks.keySet())) {
+ ScheduledFuture<?> future = taskMonitoringTasks.remove(deviceId);
+ if (future != null) future.cancel(false);
+ }
+
+ // 鍏抽棴绾跨▼姹�
+ shutdownExecutor(stateMonitorExecutor, "鐘舵�佺洃鎺�");
+ shutdownExecutor(idleMonitorExecutor, "绌洪棽鐩戞帶");
+ shutdownExecutor(taskMonitorExecutor, "浠诲姟鐩戞帶");
+
+ log.info("澶ц溅鐩戞帶绾跨▼姹犲凡鍏抽棴");
+ }
+
+ private void shutdownExecutor(ScheduledExecutorService executor, String name) {
+ executor.shutdown();
+ try {
+ if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
+ executor.shutdownNow();
+ if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
+ log.warn("{}绾跨▼姹犳湭鑳芥甯稿叧闂�", name);
+ }
+ }
+ } catch (InterruptedException e) {
+ executor.shutdownNow();
+ Thread.currentThread().interrupt();
+ }
+ }
+}
+
diff --git a/mes-processes/mes-plcSend/src/main/java/com/mes/interaction/vehicle/model/VehiclePath.java b/mes-processes/mes-plcSend/src/main/java/com/mes/interaction/vehicle/model/VehiclePath.java
new file mode 100644
index 0000000..ba17581
--- /dev/null
+++ b/mes-processes/mes-plcSend/src/main/java/com/mes/interaction/vehicle/model/VehiclePath.java
@@ -0,0 +1,91 @@
+package com.mes.interaction.vehicle.model;
+
+import lombok.Data;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 杞﹁締璺緞淇℃伅
+ *
+ * @author mes
+ * @since 2025-01-XX
+ */
+@Data
+public class VehiclePath {
+ /**
+ * 璧峰浣嶇疆
+ */
+ private VehiclePosition startPosition;
+
+ /**
+ * 鐩爣浣嶇疆
+ */
+ private VehiclePosition endPosition;
+
+ /**
+ * 璺緞鐐瑰垪琛紙濡傛灉璺緞涓嶆槸鐩寸嚎锛�
+ */
+ private List<VehiclePosition> waypoints;
+
+ /**
+ * 璺緞瀹藉害锛堢敤浜庡啿绐佹娴嬶級
+ */
+ private Double pathWidth;
+
+ public VehiclePath() {
+ this.waypoints = new ArrayList<>();
+ }
+
+ public VehiclePath(VehiclePosition start, VehiclePosition end) {
+ this.startPosition = start;
+ this.endPosition = end;
+ this.waypoints = new ArrayList<>();
+ }
+
+ /**
+ * 妫�鏌ヨ矾寰勬槸鍚︿笌鍙︿竴鏉¤矾寰勫啿绐�
+ */
+ public boolean conflictsWith(VehiclePath other) {
+ if (other == null) {
+ return false;
+ }
+
+ // 绠�鍗曞啿绐佹娴嬶細妫�鏌ヨ捣鐐瑰拰缁堢偣鏄惁閲嶅彔
+ if (startPosition != null && other.startPosition != null) {
+ if (positionsOverlap(startPosition, other.startPosition)) {
+ return true;
+ }
+ }
+
+ if (endPosition != null && other.endPosition != null) {
+ if (positionsOverlap(endPosition, other.endPosition)) {
+ return true;
+ }
+ }
+
+ // TODO: 鏇村鏉傜殑璺緞浜ゅ弶妫�娴�
+ return false;
+ }
+
+ private boolean positionsOverlap(VehiclePosition pos1, VehiclePosition pos2) {
+ if (pos1 == null || pos2 == null) {
+ return false;
+ }
+
+ // 濡傛灉鏈夊潗鏍囷紝浣跨敤鍧愭爣鍒ゆ柇
+ if (pos1.getX() != null && pos1.getY() != null &&
+ pos2.getX() != null && pos2.getY() != null) {
+ double distance = pos1.distanceTo(pos2);
+ double threshold = (pathWidth != null ? pathWidth : 100.0) / 2.0;
+ return distance < threshold;
+ }
+
+ // 濡傛灉鏈変綅缃�硷紝浣跨敤浣嶇疆鍊煎垽鏂�
+ if (pos1.getPositionValue() != null && pos2.getPositionValue() != null) {
+ return pos1.getPositionValue().equals(pos2.getPositionValue());
+ }
+
+ return false;
+ }
+}
+
diff --git a/mes-processes/mes-plcSend/src/main/java/com/mes/interaction/vehicle/model/VehiclePosition.java b/mes-processes/mes-plcSend/src/main/java/com/mes/interaction/vehicle/model/VehiclePosition.java
new file mode 100644
index 0000000..07658ed
--- /dev/null
+++ b/mes-processes/mes-plcSend/src/main/java/com/mes/interaction/vehicle/model/VehiclePosition.java
@@ -0,0 +1,63 @@
+package com.mes.interaction.vehicle.model;
+
+import lombok.Data;
+
+/**
+ * 杞﹁締浣嶇疆淇℃伅
+ *
+ * @author huang
+ * @since 2025-11-21
+ */
+@Data
+public class VehiclePosition {
+ /**
+ * X鍧愭爣
+ */
+ private Double x;
+
+ /**
+ * Y鍧愭爣
+ */
+ private Double y;
+
+ /**
+ * Z鍧愭爣锛堝鏋滈渶瑕侊級
+ */
+ private Double z;
+
+ /**
+ * 浣嶇疆缂栫爜锛堝锛歅OS1, POS2锛�
+ */
+ private String positionCode;
+
+ /**
+ * 浣嶇疆鍊硷紙PLC涓殑浣嶇疆鍊硷級
+ */
+ private Integer positionValue;
+
+ public VehiclePosition() {
+ }
+
+ public VehiclePosition(Double x, Double y) {
+ this.x = x;
+ this.y = y;
+ }
+
+ public VehiclePosition(String positionCode, Integer positionValue) {
+ this.positionCode = positionCode;
+ this.positionValue = positionValue;
+ }
+
+ /**
+ * 璁$畻鍒扮洰鏍囦綅缃殑璺濈
+ */
+ public double distanceTo(VehiclePosition target) {
+ if (x == null || y == null || target.x == null || target.y == null) {
+ return 0.0;
+ }
+ double dx = target.x - x;
+ double dy = target.y - y;
+ return Math.sqrt(dx * dx + dy * dy);
+ }
+}
+
diff --git a/mes-processes/mes-plcSend/src/main/java/com/mes/interaction/vehicle/model/VehicleState.java b/mes-processes/mes-plcSend/src/main/java/com/mes/interaction/vehicle/model/VehicleState.java
new file mode 100644
index 0000000..cd6c724
--- /dev/null
+++ b/mes-processes/mes-plcSend/src/main/java/com/mes/interaction/vehicle/model/VehicleState.java
@@ -0,0 +1,30 @@
+package com.mes.interaction.vehicle.model;
+
+/**
+ * 杞﹁締鐘舵�佹灇涓�
+ *
+ * @author mes
+ * @since 2025-01-XX
+ */
+public enum VehicleState {
+ /**
+ * 绌洪棽 - 杞﹁締鍙敤锛屽彲浠ユ帴鍙楁柊浠诲姟
+ */
+ IDLE,
+
+ /**
+ * 鎵ц涓� - 杞﹁締姝e湪鎵ц浠诲姟锛屼笉鑳芥搷浣�
+ */
+ EXECUTING,
+
+ /**
+ * 绛夊緟 - 杞﹁締鍦ㄦ帓闃熺瓑寰呮墽琛�
+ */
+ WAITING,
+
+ /**
+ * 閿欒 - 杞﹁締鍑虹幇閿欒
+ */
+ ERROR
+}
+
diff --git a/mes-processes/mes-plcSend/src/main/java/com/mes/interaction/vehicle/model/VehicleStatus.java b/mes-processes/mes-plcSend/src/main/java/com/mes/interaction/vehicle/model/VehicleStatus.java
new file mode 100644
index 0000000..39d10c4
--- /dev/null
+++ b/mes-processes/mes-plcSend/src/main/java/com/mes/interaction/vehicle/model/VehicleStatus.java
@@ -0,0 +1,113 @@
+package com.mes.interaction.vehicle.model;
+
+import lombok.Data;
+import java.time.LocalDateTime;
+
+/**
+ * 杞﹁締杩愯鏃剁姸鎬�
+ * 姣忎釜璁惧瀹炰緥鏈夌嫭绔嬬殑鐘舵�佸璞�
+ *
+ * @author huang
+ * @since 2025-11-21
+ */
+@Data
+public class VehicleStatus {
+ /**
+ * 璁惧ID锛堝搴擠eviceConfig.deviceId锛�
+ */
+ private String deviceId;
+
+ /**
+ * 璁惧鍚嶇О
+ */
+ private String deviceName;
+
+ /**
+ * 褰撳墠鐘舵��
+ */
+ private VehicleState state;
+
+ /**
+ * 褰撳墠浣嶇疆
+ */
+ private VehiclePosition currentPosition;
+
+ /**
+ * 鐩爣浣嶇疆
+ */
+ private VehiclePosition targetPosition;
+
+ /**
+ * 褰撳墠閫熷害
+ */
+ private Double speed;
+
+ /**
+ * 浠诲姟寮�濮嬫椂闂�
+ */
+ private LocalDateTime taskStartTime;
+
+ /**
+ * 棰勮缁撴潫鏃堕棿
+ */
+ private LocalDateTime estimatedEndTime;
+
+ /**
+ * 褰撳墠浠诲姟
+ */
+ private VehicleTask currentTask;
+
+ /**
+ * 鏈�鍚庢洿鏂版椂闂�
+ */
+ private LocalDateTime lastUpdateTime;
+
+ public VehicleStatus() {
+ this.state = VehicleState.IDLE;
+ this.lastUpdateTime = LocalDateTime.now();
+ }
+
+ public VehicleStatus(String deviceId) {
+ this();
+ this.deviceId = deviceId;
+ }
+
+ public VehicleStatus(String deviceId, String deviceName) {
+ this(deviceId);
+ this.deviceName = deviceName;
+ }
+
+ /**
+ * 鏇存柊鐘舵��
+ */
+ public void setState(VehicleState newState) {
+ this.state = newState;
+ this.lastUpdateTime = LocalDateTime.now();
+
+ if (newState == VehicleState.EXECUTING && currentTask != null) {
+ this.taskStartTime = LocalDateTime.now();
+ currentTask.setStartTime(taskStartTime);
+ currentTask.calculateEstimatedEndTime();
+ this.estimatedEndTime = currentTask.getEstimatedEndTime();
+ } else if (newState == VehicleState.IDLE) {
+ this.taskStartTime = null;
+ this.estimatedEndTime = null;
+ this.currentTask = null;
+ }
+ }
+
+ /**
+ * 妫�鏌ユ槸鍚﹀彲鐢�
+ */
+ public boolean isAvailable() {
+ return state == VehicleState.IDLE;
+ }
+
+ /**
+ * 妫�鏌ユ槸鍚︽鍦ㄦ墽琛�
+ */
+ public boolean isExecuting() {
+ return state == VehicleState.EXECUTING;
+ }
+}
+
diff --git a/mes-processes/mes-plcSend/src/main/java/com/mes/interaction/vehicle/model/VehicleTask.java b/mes-processes/mes-plcSend/src/main/java/com/mes/interaction/vehicle/model/VehicleTask.java
new file mode 100644
index 0000000..f64da91
--- /dev/null
+++ b/mes-processes/mes-plcSend/src/main/java/com/mes/interaction/vehicle/model/VehicleTask.java
@@ -0,0 +1,81 @@
+package com.mes.interaction.vehicle.model;
+
+import lombok.Data;
+import java.time.LocalDateTime;
+
+/**
+ * 杞﹁締浠诲姟淇℃伅
+ *
+ * @author mes
+ * @since 2025-01-XX
+ */
+@Data
+public class VehicleTask {
+ /**
+ * 浠诲姟ID
+ */
+ private String taskId;
+
+ /**
+ * 浠诲姟鍚嶇О
+ */
+ private String taskName;
+
+ /**
+ * 鎿嶄綔绫诲瀷锛堝锛歠eedGlass, triggerRequest绛夛級
+ */
+ private String operation;
+
+ /**
+ * 璁″垝璺緞
+ */
+ private VehiclePath plannedPath;
+
+ /**
+ * 浠诲姟寮�濮嬫椂闂�
+ */
+ private LocalDateTime startTime;
+
+ /**
+ * 棰勮缁撴潫鏃堕棿
+ */
+ private LocalDateTime estimatedEndTime;
+
+ /**
+ * 杞﹁締閫熷害锛堢敤浜庤绠楅璁$粨鏉熸椂闂达級
+ */
+ private Double speed;
+
+ /**
+ * 浠诲姟鍙傛暟
+ */
+ private java.util.Map<String, Object> parameters;
+
+ public VehicleTask() {
+ this.parameters = new java.util.HashMap<>();
+ }
+
+ /**
+ * 璁$畻棰勮缁撴潫鏃堕棿
+ */
+ public void calculateEstimatedEndTime() {
+ if (plannedPath == null || speed == null || speed <= 0) {
+ return;
+ }
+
+ VehiclePosition start = plannedPath.getStartPosition();
+ VehiclePosition end = plannedPath.getEndPosition();
+
+ if (start != null && end != null) {
+ double distance = start.distanceTo(end);
+ if (distance > 0) {
+ // 鏃堕棿 = 璺濈 / 閫熷害锛堢锛�
+ long seconds = (long) (distance / speed);
+ estimatedEndTime = startTime != null ?
+ startTime.plusSeconds(seconds) :
+ LocalDateTime.now().plusSeconds(seconds);
+ }
+ }
+ }
+}
+
diff --git a/mes-processes/mes-plcSend/src/main/java/com/mes/interaction/workstation/base/WorkstationBaseHandler.java b/mes-processes/mes-plcSend/src/main/java/com/mes/interaction/workstation/base/WorkstationBaseHandler.java
new file mode 100644
index 0000000..c37bb7a
--- /dev/null
+++ b/mes-processes/mes-plcSend/src/main/java/com/mes/interaction/workstation/base/WorkstationBaseHandler.java
@@ -0,0 +1,80 @@
+package com.mes.interaction.workstation.base;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.mes.device.entity.DeviceConfig;
+import com.mes.device.service.DevicePlcOperationService;
+import com.mes.device.vo.DevicePlcVO;
+import com.mes.interaction.BaseDeviceLogicHandler;
+import com.mes.interaction.workstation.config.WorkstationLogicConfig;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import java.util.Map;
+
+/**
+ * 鍗ц浆绔嬬郴鍒楄澶囩殑閫氱敤澶勭悊鍣ㄥ熀绫�
+ * 璐熻矗瑙f瀽 workstation 閰嶇疆銆佹彁渚涘崰浣嶇殑鎵ц妯℃澘锛屽悗缁叿浣撹澶囧湪瀛愮被涓疄鐜般��
+ *
+ * @author mes
+ * @since 2025-11-24
+ */
+@Slf4j
+public abstract class WorkstationBaseHandler extends BaseDeviceLogicHandler {
+
+ protected WorkstationBaseHandler(DevicePlcOperationService devicePlcOperationService) {
+ super(devicePlcOperationService);
+ }
+
+ /**
+ * 瑙f瀽 workstation 鐩稿叧閰嶇疆
+ */
+ protected WorkstationLogicConfig parseWorkstationConfig(Map<String, Object> logicParams) {
+ WorkstationLogicConfig config = new WorkstationLogicConfig();
+ if (logicParams == null) {
+ return config;
+ }
+ config.setScanIntervalMs(getLogicParam(logicParams, "scanIntervalMs", 10_000));
+ config.setTransferDelayMs(getLogicParam(logicParams, "transferDelayMs", 30_000));
+ config.setVehicleCapacity(getLogicParam(logicParams, "vehicleCapacity", 6000));
+ config.setAutoAck(getLogicParam(logicParams, "autoAck", Boolean.TRUE));
+ return config;
+ }
+
+ /**
+ * 榛樿瀹炵幇锛氭彁绀哄皻鏈疄鐜板叿浣撻�昏緫
+ */
+ @Override
+ protected DevicePlcVO.OperationResult doExecute(com.mes.device.entity.DeviceConfig deviceConfig,
+ String operation,
+ java.util.Map<String, Object> params,
+ java.util.Map<String, Object> logicParams) {
+ log.warn("褰撳墠璁惧閫昏緫灏氭湭瀹炵幇: deviceType={}, operation={}", deviceConfig.getDeviceType(), operation);
+ return DevicePlcVO.OperationResult.builder()
+ .success(false)
+ .message("璁惧閫昏緫寰呭疄鐜�: " + deviceConfig.getDeviceType())
+ .build();
+ }
+
+ @Override
+ public String validateLogicParams(DeviceConfig deviceConfig) {
+ // 榛樿璁や负閰嶇疆鍚堟硶锛屽叿浣撴牎楠屼氦鐢卞瓙绫诲疄鐜�
+ return null;
+ }
+
+ @Override
+ public String getDefaultLogicParams() {
+ Map<String, Object> defaults = new HashMap<>();
+ defaults.put("scanIntervalMs", 10_000);
+ defaults.put("transferDelayMs", 30_000);
+ defaults.put("vehicleCapacity", 6_000);
+ defaults.put("autoAck", true);
+ try {
+ return objectMapper.writeValueAsString(defaults);
+ } catch (JsonProcessingException e) {
+ return "{\"scanIntervalMs\":10000,\"transferDelayMs\":30000,\"vehicleCapacity\":6000,\"autoAck\":true}";
+ }
+ }
+}
+
diff --git a/mes-processes/mes-plcSend/src/main/java/com/mes/interaction/workstation/config/WorkstationLogicConfig.java b/mes-processes/mes-plcSend/src/main/java/com/mes/interaction/workstation/config/WorkstationLogicConfig.java
new file mode 100644
index 0000000..a782412
--- /dev/null
+++ b/mes-processes/mes-plcSend/src/main/java/com/mes/interaction/workstation/config/WorkstationLogicConfig.java
@@ -0,0 +1,32 @@
+package com.mes.interaction.workstation.config;
+
+import lombok.Data;
+
+/**
+ * 鍗ц浆绔嬬浉鍏宠澶囩殑閫昏緫閰嶇疆
+ * 瀵瑰簲 extraParams.deviceLogic 涓殑瀛楁
+ */
+@Data
+public class WorkstationLogicConfig {
+
+ /**
+ * 鎵爜璁惧鍙戦�佽姹傜殑鏃堕棿闂撮殧锛堟绉掞級
+ */
+ private Integer scanIntervalMs = 10_000;
+
+ /**
+ * 鍗ц浆绔嬪埌澶ц溅鐨勮繍杈撴椂闂达紙姣锛�
+ */
+ private Integer transferDelayMs = 30_000;
+
+ /**
+ * 鍙杞界殑鏈�澶у搴︼紙mm锛�
+ */
+ private Integer vehicleCapacity = 6_000;
+
+ /**
+ * 鏄惁鑷姩纭 MES 鍙戦�佺殑鐜荤拑淇℃伅
+ */
+ private Boolean autoAck = Boolean.TRUE;
+}
+
diff --git a/mes-processes/mes-plcSend/src/main/java/com/mes/interaction/workstation/scanner/handler/HorizontalScannerLogicHandler.java b/mes-processes/mes-plcSend/src/main/java/com/mes/interaction/workstation/scanner/handler/HorizontalScannerLogicHandler.java
new file mode 100644
index 0000000..3b45e80
--- /dev/null
+++ b/mes-processes/mes-plcSend/src/main/java/com/mes/interaction/workstation/scanner/handler/HorizontalScannerLogicHandler.java
@@ -0,0 +1,161 @@
+package com.mes.interaction.workstation.scanner.handler;
+
+import com.mes.device.entity.DeviceConfig;
+import com.mes.device.entity.GlassInfo;
+import com.mes.device.service.DevicePlcOperationService;
+import com.mes.device.service.GlassInfoService;
+import com.mes.device.vo.DevicePlcVO;
+import com.mes.interaction.workstation.base.WorkstationBaseHandler;
+import com.mes.interaction.workstation.config.WorkstationLogicConfig;
+import com.mes.s7.enhanced.EnhancedS7Serializer;
+import com.mes.s7.provider.S7SerializerProvider;
+import com.mes.service.PlcDynamicDataService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+
+import java.time.LocalDateTime;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 鍗ц浆绔嬫壂鐮佽澶囬�昏緫澶勭悊鍣�
+ * 璐熻矗浠嶮ES鍐欏尯璇诲彇鐜荤拑灏哄锛屽苟钀藉簱 glass_info
+ */
+@Slf4j
+@Component
+public class HorizontalScannerLogicHandler extends WorkstationBaseHandler {
+
+ private static final List<String> MES_FIELDS = Arrays.asList("mesSend", "mesGlassId", "mesWidth", "mesHeight", "workLine");
+
+ private final PlcDynamicDataService plcDynamicDataService;
+ private final GlassInfoService glassInfoService;
+ private final S7SerializerProvider s7SerializerProvider;
+
+ public HorizontalScannerLogicHandler(DevicePlcOperationService devicePlcOperationService,
+ PlcDynamicDataService plcDynamicDataService,
+ GlassInfoService glassInfoService,
+ S7SerializerProvider s7SerializerProvider) {
+ super(devicePlcOperationService);
+ this.plcDynamicDataService = plcDynamicDataService;
+ this.glassInfoService = glassInfoService;
+ this.s7SerializerProvider = s7SerializerProvider;
+ }
+
+ @Override
+ public String getDeviceType() {
+ return DeviceConfig.DeviceType.WORKSTATION_SCANNER;
+ }
+
+ @Override
+ protected DevicePlcVO.OperationResult doExecute(DeviceConfig deviceConfig,
+ String operation,
+ Map<String, Object> params,
+ Map<String, Object> logicParams) {
+ WorkstationLogicConfig config = parseWorkstationConfig(logicParams);
+ EnhancedS7Serializer serializer = s7SerializerProvider.getSerializer(deviceConfig);
+ if (serializer == null) {
+ return buildResult(deviceConfig, operation, false, "鑾峰彇PLC搴忓垪鍖栧櫒澶辫触");
+ }
+
+ try {
+ log.debug("鍗ц浆绔嬫壂鐮佽鍙朚ES鍐欏尯: deviceId={}, scanInterval={}ms",
+ deviceConfig.getId(), config.getScanIntervalMs());
+ Map<String, Object> mesData = plcDynamicDataService.readPlcData(deviceConfig, MES_FIELDS, serializer);
+ if (mesData == null || mesData.isEmpty()) {
+ return buildResult(deviceConfig, operation, false, "璇诲彇MES鍐欏尯澶辫触");
+ }
+
+ Integer mesSend = parseInteger(mesData.get("mesSend"));
+ if (mesSend == null || mesSend == 0) {
+ return buildResult(deviceConfig, operation, true, "鏆傛棤寰呭鐞嗙殑鐜荤拑淇℃伅");
+ }
+
+ String glassId = parseString(mesData.get("mesGlassId"));
+ if (!StringUtils.hasText(glassId)) {
+ return buildResult(deviceConfig, operation, false, "MES鍐欏尯鏈彁渚涚幓鐠僆D");
+ }
+
+ Integer longSide = convertDimension(parseInteger(mesData.get("mesWidth")));
+ Integer shortSide = convertDimension(parseInteger(mesData.get("mesHeight")));
+ Integer workLine = parseInteger(mesData.get("workLine"));
+
+ GlassInfo glassInfo = buildGlassInfo(glassId, longSide, shortSide, workLine);
+ boolean saved = glassInfoService.saveOrUpdateGlassInfo(glassInfo);
+ if (!saved) {
+ return buildResult(deviceConfig, operation, false, "淇濆瓨鐜荤拑淇℃伅澶辫触: " + glassId);
+ }
+
+ // 璇诲彇鍒癕ES鏁版嵁鍚庯紝閲嶇疆mesSend锛岄伩鍏嶉噸澶嶆秷璐�
+ plcDynamicDataService.writePlcField(deviceConfig, "mesSend", 0, serializer);
+
+ String msg = String.format("鐜荤拑[%s] 灏哄[%s x %s] 宸叉帴鏀跺苟鍏ュ簱锛寃orkLine=%s",
+ glassId,
+ longSide != null ? longSide + "mm" : "-",
+ shortSide != null ? shortSide + "mm" : "-",
+ workLine != null ? workLine : "-");
+ return buildResult(deviceConfig, operation, true, msg);
+ } catch (Exception e) {
+ log.error("鍗ц浆绔嬫壂鐮佸鐞嗗紓甯�, deviceId={}", deviceConfig.getId(), e);
+ return buildResult(deviceConfig, operation, false, "澶勭悊寮傚父: " + e.getMessage());
+ }
+ }
+
+ private GlassInfo buildGlassInfo(String glassId, Integer longSide, Integer shortSide, Integer workLine) {
+ GlassInfo glassInfo = new GlassInfo();
+ glassInfo.setGlassId(glassId.trim());
+ if (longSide != null) {
+ glassInfo.setGlassLength(longSide);
+ }
+ if (shortSide != null) {
+ glassInfo.setGlassWidth(shortSide);
+ }
+ glassInfo.setStatus(GlassInfo.Status.ACTIVE);
+ if (workLine != null) {
+ glassInfo.setDescription("workLine=" + workLine);
+ }
+ return glassInfo;
+ }
+
+ private Integer parseInteger(Object value) {
+ if (value instanceof Number) {
+ return ((Number) value).intValue();
+ }
+ if (value == null) {
+ return null;
+ }
+ try {
+ return Integer.parseInt(String.valueOf(value));
+ } catch (NumberFormatException e) {
+ return null;
+ }
+ }
+
+ private String parseString(Object value) {
+ return value == null ? null : String.valueOf(value).trim();
+ }
+
+ private Integer convertDimension(Integer raw) {
+ if (raw == null) {
+ return null;
+ }
+ return raw / 10;
+ }
+
+ private DevicePlcVO.OperationResult buildResult(DeviceConfig deviceConfig,
+ String operation,
+ boolean success,
+ String message) {
+ return DevicePlcVO.OperationResult.builder()
+ .deviceId(deviceConfig.getId())
+ .deviceName(deviceConfig.getDeviceName())
+ .deviceCode(deviceConfig.getDeviceCode())
+ .projectId(deviceConfig.getProjectId() != null ? String.valueOf(deviceConfig.getProjectId()) : null)
+ .operation(operation)
+ .success(success)
+ .message(message)
+ .timestamp(LocalDateTime.now())
+ .build();
+ }
+}
diff --git a/mes-processes/mes-plcSend/src/main/java/com/mes/interaction/workstation/transfer/handler/HorizontalTransferLogicHandler.java b/mes-processes/mes-plcSend/src/main/java/com/mes/interaction/workstation/transfer/handler/HorizontalTransferLogicHandler.java
new file mode 100644
index 0000000..85b9dc3
--- /dev/null
+++ b/mes-processes/mes-plcSend/src/main/java/com/mes/interaction/workstation/transfer/handler/HorizontalTransferLogicHandler.java
@@ -0,0 +1,481 @@
+package com.mes.interaction.workstation.transfer.handler;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.mes.device.entity.DeviceConfig;
+import com.mes.device.entity.GlassInfo;
+import com.mes.device.mapper.DeviceGlassInfoMapper;
+import com.mes.device.service.DevicePlcOperationService;
+import com.mes.device.service.GlassInfoService;
+import com.mes.device.vo.DevicePlcVO;
+import com.mes.interaction.workstation.base.WorkstationBaseHandler;
+import com.mes.interaction.workstation.config.WorkstationLogicConfig;
+import com.mes.s7.enhanced.EnhancedS7Serializer;
+import com.mes.s7.provider.S7SerializerProvider;
+import com.mes.service.PlcDynamicDataService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+import javax.annotation.PreDestroy;
+
+import java.time.LocalDateTime;
+import java.util.*;
+import java.util.concurrent.*;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.stream.Collectors;
+
+/**
+ * 鍗ц浆绔嬩富浣撹澶囬�昏緫澶勭悊鍣�
+ * 璐熻矗鐜荤拑缂撳啿銆佸閲忔牎楠屻�佹壒娆$粍瑁呫�丳LC鍐欏叆绛夐�昏緫
+ */
+@Slf4j
+@Component
+public class HorizontalTransferLogicHandler extends WorkstationBaseHandler {
+
+ private final PlcDynamicDataService plcDynamicDataService;
+ private final GlassInfoService glassInfoService;
+ private final S7SerializerProvider s7SerializerProvider;
+
+ @Autowired(required = false)
+ private DeviceGlassInfoMapper glassInfoMapper;
+
+ // 鐜荤拑缂撳啿闃熷垪锛歞eviceId -> 鐜荤拑淇℃伅鍒楄〃
+ private final Map<String, List<GlassBufferItem>> glassBuffer = new ConcurrentHashMap<>();
+
+ // 鏈�鍚庢壂鐮佹椂闂达細deviceId -> 鏈�鍚庢壂鐮佹椂闂存埑
+ private final Map<String, AtomicLong> lastScanTime = new ConcurrentHashMap<>();
+
+ // 鐩戞帶浠诲姟锛歞eviceId -> 鐩戞帶浠诲姟
+ private final Map<String, ScheduledFuture<?>> monitorTasks = new ConcurrentHashMap<>();
+
+ // 鐩戞帶绾跨▼姹�
+ private final ScheduledExecutorService monitorExecutor = Executors.newScheduledThreadPool(5, r -> {
+ Thread t = new Thread(r, "HorizontalTransferMonitor");
+ t.setDaemon(true);
+ return t;
+ });
+
+ @Autowired
+ public HorizontalTransferLogicHandler(DevicePlcOperationService devicePlcOperationService,
+ PlcDynamicDataService plcDynamicDataService,
+ @Qualifier("deviceGlassInfoService") GlassInfoService glassInfoService,
+ S7SerializerProvider s7SerializerProvider) {
+ super(devicePlcOperationService);
+ this.plcDynamicDataService = plcDynamicDataService;
+ this.glassInfoService = glassInfoService;
+ this.s7SerializerProvider = s7SerializerProvider;
+ }
+
+ @Override
+ public String getDeviceType() {
+ return DeviceConfig.DeviceType.WORKSTATION_TRANSFER;
+ }
+
+ @Override
+ protected DevicePlcVO.OperationResult doExecute(DeviceConfig deviceConfig,
+ String operation,
+ Map<String, Object> params,
+ Map<String, Object> logicParams) {
+ WorkstationLogicConfig config = parseWorkstationConfig(logicParams);
+
+ try {
+ switch (operation) {
+ case "checkAndProcess":
+ case "process":
+ return handleCheckAndProcess(deviceConfig, config, logicParams);
+ case "startMonitor":
+ return handleStartMonitor(deviceConfig, config, logicParams);
+ case "stopMonitor":
+ return handleStopMonitor(deviceConfig);
+ case "clearBuffer":
+ return handleClearBuffer(deviceConfig);
+ default:
+ return buildResult(deviceConfig, operation, false,
+ "涓嶆敮鎸佺殑鎿嶄綔: " + operation);
+ }
+ } catch (Exception e) {
+ log.error("鍗ц浆绔嬩富浣撳鐞嗗紓甯�: deviceId={}, operation={}",
+ deviceConfig.getId(), operation, e);
+ return buildResult(deviceConfig, operation, false,
+ "澶勭悊寮傚父: " + e.getMessage());
+ }
+ }
+
+ /**
+ * 妫�鏌ュ苟澶勭悊鐜荤拑鎵规
+ * 浠庢暟鎹簱璇诲彇鏈�杩戞壂鐮佺殑鐜荤拑锛岃繘琛屽閲忓垽鏂紝缁勮鎵规锛屽啓鍏LC
+ */
+ private DevicePlcVO.OperationResult handleCheckAndProcess(
+ DeviceConfig deviceConfig,
+ WorkstationLogicConfig config,
+ Map<String, Object> logicParams) {
+
+ String deviceId = deviceConfig.getDeviceId();
+ EnhancedS7Serializer serializer = s7SerializerProvider.getSerializer(deviceConfig);
+ if (serializer == null) {
+ return buildResult(deviceConfig, "checkAndProcess", false,
+ "鑾峰彇PLC搴忓垪鍖栧櫒澶辫触");
+ }
+
+ try {
+ // 1. 浠庢暟鎹簱鏌ヨ鏈�杩戞壂鐮佺殑鐜荤拑淇℃伅锛堟渶杩�1鍒嗛挓鍐呯殑璁板綍锛�
+ List<GlassInfo> recentGlasses = queryRecentScannedGlasses(deviceConfig, logicParams);
+ if (recentGlasses.isEmpty()) {
+ return buildResult(deviceConfig, "checkAndProcess", true,
+ "鏆傛棤寰呭鐞嗙殑鐜荤拑淇℃伅");
+ }
+
+ log.info("鏌ヨ鍒版渶杩戞壂鐮佺殑鐜荤拑: deviceId={}, count={}",
+ deviceId, recentGlasses.size());
+
+ // 2. 鏇存柊缂撳啿闃熷垪鍜屾渶鍚庢壂鐮佹椂闂�
+ updateBuffer(deviceId, recentGlasses);
+ lastScanTime.put(deviceId, new AtomicLong(System.currentTimeMillis()));
+
+ // 3. 妫�鏌ユ槸鍚﹂渶瑕佺珛鍗冲鐞嗭紙瀹归噺宸叉弧鎴�30s鍐呮棤鏂扮幓鐠冿級
+ List<GlassBufferItem> buffer = glassBuffer.get(deviceId);
+ if (buffer == null || buffer.isEmpty()) {
+ return buildResult(deviceConfig, "checkAndProcess", true,
+ "缂撳啿闃熷垪涓虹┖");
+ }
+
+ // 4. 鍒ゆ柇鏄惁婊¤冻澶勭悊鏉′欢
+ boolean shouldProcess = shouldProcessBatch(deviceId, buffer, config);
+ if (!shouldProcess) {
+ return buildResult(deviceConfig, "checkAndProcess", true,
+ "绛夊緟鏇村鐜荤拑鎴�30s瓒呮椂");
+ }
+
+ // 5. 瀹归噺鍒ゆ柇鍜屾壒娆$粍瑁�
+ List<GlassInfo> batch = assembleBatch(buffer, config.getVehicleCapacity());
+ if (batch.isEmpty()) {
+ return buildResult(deviceConfig, "checkAndProcess", false,
+ "鏃犳硶缁勮鏈夋晥鎵规锛堝閲忎笉瓒筹級");
+ }
+
+ // 6. 鍐欏叆PLC
+ DevicePlcVO.OperationResult writeResult = writeBatchToPlc(
+ deviceConfig, batch, serializer, logicParams);
+
+ if (!Boolean.TRUE.equals(writeResult.getSuccess())) {
+ return writeResult;
+ }
+
+ // 7. 浠庣紦鍐查槦鍒椾腑绉婚櫎宸插鐞嗙殑鐜荤拑
+ removeProcessedGlasses(deviceId, batch);
+
+ String msg = String.format("鎵规宸插啓鍏LC: glassCount=%d, glassIds=%s",
+ batch.size(),
+ batch.stream().map(GlassInfo::getGlassId).collect(Collectors.joining(",")));
+ return buildResult(deviceConfig, "checkAndProcess", true, msg);
+
+ } catch (Exception e) {
+ log.error("妫�鏌ュ苟澶勭悊鐜荤拑鎵规寮傚父: deviceId={}", deviceId, e);
+ return buildResult(deviceConfig, "checkAndProcess", false,
+ "澶勭悊寮傚父: " + e.getMessage());
+ }
+ }
+
+ /**
+ * 鏌ヨ鏈�杩戞壂鐮佺殑鐜荤拑淇℃伅
+ */
+ private List<GlassInfo> queryRecentScannedGlasses(
+ DeviceConfig deviceConfig,
+ Map<String, Object> logicParams) {
+
+ if (glassInfoMapper == null) {
+ log.warn("GlassInfoMapper鏈敞鍏ワ紝鏃犳硶鏌ヨ鏈�杩戞壂鐮佺殑鐜荤拑");
+ return Collections.emptyList();
+ }
+
+ try {
+ // 浠庨厤缃腑鑾峰彇workLine锛岀敤浜庤繃婊�
+ String workLine = getLogicParam(logicParams, "workLine", null);
+
+ // 鏌ヨ鏈�杩�2鍒嗛挓鍐呯殑鐜荤拑璁板綍锛堟墿澶ф椂闂寸獥鍙o紝纭繚涓嶉仐婕忥級
+ Date twoMinutesAgo = new Date(System.currentTimeMillis() - 120000);
+
+ LambdaQueryWrapper<GlassInfo> wrapper = new LambdaQueryWrapper<>();
+ wrapper.eq(GlassInfo::getStatus, GlassInfo.Status.ACTIVE)
+ .ge(GlassInfo::getCreatedTime, twoMinutesAgo)
+ .orderByDesc(GlassInfo::getCreatedTime)
+ .last("LIMIT 20"); // 闄愬埗鏌ヨ鏁伴噺锛岄伩鍏嶈繃澶�
+
+ // 濡傛灉閰嶇疆浜唚orkLine锛屽垯杩囨护description
+ if (workLine != null && !workLine.isEmpty()) {
+ wrapper.like(GlassInfo::getDescription, "workLine=" + workLine);
+ }
+
+ List<GlassInfo> recentGlasses = glassInfoMapper.selectList(wrapper);
+
+ log.debug("鏌ヨ鍒版渶杩戞壂鐮佺殑鐜荤拑: deviceId={}, workLine={}, count={}",
+ deviceConfig.getId(), workLine, recentGlasses.size());
+
+ return recentGlasses;
+
+ } catch (Exception e) {
+ log.error("鏌ヨ鏈�杩戞壂鐮佺殑鐜荤拑淇℃伅寮傚父: deviceId={}",
+ deviceConfig.getId(), e);
+ return Collections.emptyList();
+ }
+ }
+
+ /**
+ * 鏇存柊缂撳啿闃熷垪
+ */
+ private void updateBuffer(String deviceId, List<GlassInfo> newGlasses) {
+ List<GlassBufferItem> buffer = glassBuffer.computeIfAbsent(
+ deviceId, k -> new CopyOnWriteArrayList<>());
+
+ Set<String> existingIds = buffer.stream()
+ .map(item -> item.glassInfo.getGlassId())
+ .collect(Collectors.toSet());
+
+ for (GlassInfo glass : newGlasses) {
+ if (!existingIds.contains(glass.getGlassId())) {
+ buffer.add(new GlassBufferItem(glass, System.currentTimeMillis()));
+ log.debug("娣诲姞鐜荤拑鍒扮紦鍐查槦鍒�: deviceId={}, glassId={}",
+ deviceId, glass.getGlassId());
+ }
+ }
+ }
+
+ /**
+ * 鍒ゆ柇鏄惁搴旇澶勭悊鎵规
+ */
+ private boolean shouldProcessBatch(String deviceId,
+ List<GlassBufferItem> buffer,
+ WorkstationLogicConfig config) {
+ // 鏉′欢1锛氱紦鍐查槦鍒楀凡婊★紙杈惧埌瀹归噺闄愬埗锛�
+ int totalLength = buffer.stream()
+ .mapToInt(item -> item.glassInfo.getGlassLength() != null ?
+ item.glassInfo.getGlassLength() : 0)
+ .sum();
+ if (totalLength >= config.getVehicleCapacity()) {
+ log.info("缂撳啿闃熷垪瀹归噺宸叉弧锛岃Е鍙戞壒娆″鐞�: deviceId={}, totalLength={}, capacity={}",
+ deviceId, totalLength, config.getVehicleCapacity());
+ return true;
+ }
+
+ // 鏉′欢2锛�30s鍐呮棤鏂扮幓鐠冩壂鐮�
+ AtomicLong lastTime = lastScanTime.get(deviceId);
+ if (lastTime != null) {
+ long elapsed = System.currentTimeMillis() - lastTime.get();
+ if (elapsed >= config.getTransferDelayMs()) {
+ log.info("30s鍐呮棤鏂扮幓鐠冩壂鐮侊紝瑙﹀彂鎵规澶勭悊: deviceId={}, elapsed={}ms",
+ deviceId, elapsed);
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * 缁勮鎵规锛堝閲忓垽鏂級
+ */
+ private List<GlassInfo> assembleBatch(List<GlassBufferItem> buffer,
+ int vehicleCapacity) {
+ List<GlassInfo> batch = new ArrayList<>();
+ int usedLength = 0;
+
+ for (GlassBufferItem item : buffer) {
+ GlassInfo glass = item.glassInfo;
+ int glassLength = glass.getGlassLength() != null ?
+ glass.getGlassLength() : 0;
+
+ if (usedLength + glassLength <= vehicleCapacity && batch.size() < 6) {
+ batch.add(glass);
+ usedLength += glassLength;
+ } else {
+ break;
+ }
+ }
+
+ return batch;
+ }
+
+ /**
+ * 鍐欏叆鎵规鍒癙LC
+ */
+ private DevicePlcVO.OperationResult writeBatchToPlc(
+ DeviceConfig deviceConfig,
+ List<GlassInfo> batch,
+ EnhancedS7Serializer serializer,
+ Map<String, Object> logicParams) {
+
+ Map<String, Object> payload = new HashMap<>();
+
+ // 鍐欏叆鐜荤拑ID锛堟渶澶�6涓級
+ int count = Math.min(batch.size(), 6);
+ for (int i = 0; i < count; i++) {
+ String fieldName = "plcGlassId" + (i + 1);
+ payload.put(fieldName, batch.get(i).getGlassId());
+ }
+
+ // 鍐欏叆鐜荤拑鏁伴噺
+ payload.put("plcGlassCount", count);
+
+ // 鍐欏叆浣嶇疆淇℃伅锛堝鏋滄湁閰嶇疆锛�
+ Integer inPosition = getLogicParam(logicParams, "inPosition", null);
+ if (inPosition != null) {
+ payload.put("inPosition", inPosition);
+ }
+
+ // 鍐欏叆璇锋眰瀛楋紙瑙﹀彂澶ц溅锛�
+ payload.put("plcRequest", 1);
+
+ try {
+ plcDynamicDataService.writePlcData(deviceConfig, payload, serializer);
+ log.info("鎵规宸插啓鍏LC: deviceId={}, glassCount={}",
+ deviceConfig.getId(), count);
+ return buildResult(deviceConfig, "writeBatchToPlc", true,
+ "鎵规鍐欏叆鎴愬姛");
+ } catch (Exception e) {
+ log.error("鍐欏叆鎵规鍒癙LC澶辫触: deviceId={}", deviceConfig.getId(), e);
+ return buildResult(deviceConfig, "writeBatchToPlc", false,
+ "鍐欏叆澶辫触: " + e.getMessage());
+ }
+ }
+
+ /**
+ * 浠庣紦鍐查槦鍒楃Щ闄ゅ凡澶勭悊鐨勭幓鐠�
+ */
+ private void removeProcessedGlasses(String deviceId, List<GlassInfo> processed) {
+ List<GlassBufferItem> buffer = glassBuffer.get(deviceId);
+ if (buffer == null) {
+ return;
+ }
+
+ Set<String> processedIds = processed.stream()
+ .map(GlassInfo::getGlassId)
+ .collect(Collectors.toSet());
+
+ buffer.removeIf(item -> processedIds.contains(item.glassInfo.getGlassId()));
+ }
+
+ /**
+ * 鍚姩鐩戞帶浠诲姟锛堝畾鏈熸鏌ュ苟澶勭悊锛�
+ */
+ private DevicePlcVO.OperationResult handleStartMonitor(
+ DeviceConfig deviceConfig,
+ WorkstationLogicConfig config,
+ Map<String, Object> logicParams) {
+
+ String deviceId = deviceConfig.getDeviceId();
+
+ // 鍋滄鏃х殑鐩戞帶浠诲姟
+ handleStopMonitor(deviceConfig);
+
+ // 鑾峰彇鐩戞帶闂撮殧
+ Integer monitorIntervalMs = getLogicParam(logicParams, "monitorIntervalMs",
+ config.getScanIntervalMs());
+
+ // 鍚姩鐩戞帶浠诲姟
+ ScheduledFuture<?> future = monitorExecutor.scheduleWithFixedDelay(() -> {
+ try {
+ handleCheckAndProcess(deviceConfig, config, logicParams);
+ } catch (Exception e) {
+ log.error("鐩戞帶浠诲姟鎵ц寮傚父: deviceId={}", deviceId, e);
+ }
+ }, monitorIntervalMs, monitorIntervalMs, TimeUnit.MILLISECONDS);
+
+ monitorTasks.put(deviceId, future);
+ log.info("宸插惎鍔ㄥ崸杞珛鐩戞帶浠诲姟: deviceId={}, interval={}ms",
+ deviceId, monitorIntervalMs);
+
+ return buildResult(deviceConfig, "startMonitor", true,
+ "鐩戞帶浠诲姟宸插惎鍔�");
+ }
+
+ /**
+ * 鍋滄鐩戞帶浠诲姟
+ */
+ private DevicePlcVO.OperationResult handleStopMonitor(DeviceConfig deviceConfig) {
+ String deviceId = deviceConfig.getDeviceId();
+ ScheduledFuture<?> future = monitorTasks.remove(deviceId);
+ if (future != null && !future.isCancelled()) {
+ future.cancel(false);
+ log.info("宸插仠姝㈠崸杞珛鐩戞帶浠诲姟: deviceId={}", deviceId);
+ }
+ return buildResult(deviceConfig, "stopMonitor", true, "鐩戞帶浠诲姟宸插仠姝�");
+ }
+
+ /**
+ * 娓呯┖缂撳啿闃熷垪
+ */
+ private DevicePlcVO.OperationResult handleClearBuffer(DeviceConfig deviceConfig) {
+ String deviceId = deviceConfig.getDeviceId();
+ glassBuffer.remove(deviceId);
+ lastScanTime.remove(deviceId);
+ log.info("宸叉竻绌虹紦鍐查槦鍒�: deviceId={}", deviceId);
+ return buildResult(deviceConfig, "clearBuffer", true, "缂撳啿闃熷垪宸叉竻绌�");
+ }
+
+ /**
+ * 鏋勫缓鎿嶄綔缁撴灉
+ */
+ private DevicePlcVO.OperationResult buildResult(DeviceConfig deviceConfig,
+ String operation,
+ boolean success,
+ String message) {
+ return DevicePlcVO.OperationResult.builder()
+ .deviceId(deviceConfig.getId())
+ .deviceName(deviceConfig.getDeviceName())
+ .deviceCode(deviceConfig.getDeviceCode())
+ .projectId(deviceConfig.getProjectId() != null ?
+ String.valueOf(deviceConfig.getProjectId()) : null)
+ .operation(operation)
+ .success(success)
+ .message(message)
+ .timestamp(LocalDateTime.now())
+ .build();
+ }
+
+ /**
+ * 搴旂敤鍏抽棴鏃舵竻鐞嗚祫婧�
+ */
+ @PreDestroy
+ public void destroy() {
+ log.info("姝e湪鍏抽棴鍗ц浆绔嬬洃鎺х嚎绋嬫睜...");
+
+ // 鍋滄鎵�鏈夌洃鎺т换鍔�
+ for (String deviceId : new ArrayList<>(monitorTasks.keySet())) {
+ ScheduledFuture<?> future = monitorTasks.remove(deviceId);
+ if (future != null && !future.isCancelled()) {
+ future.cancel(false);
+ }
+ }
+
+ // 鍏抽棴绾跨▼姹�
+ monitorExecutor.shutdown();
+ try {
+ if (!monitorExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
+ monitorExecutor.shutdownNow();
+ if (!monitorExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
+ log.warn("鍗ц浆绔嬬洃鎺х嚎绋嬫睜鏈兘姝e父鍏抽棴");
+ }
+ }
+ } catch (InterruptedException e) {
+ monitorExecutor.shutdownNow();
+ Thread.currentThread().interrupt();
+ }
+
+ log.info("鍗ц浆绔嬬洃鎺х嚎绋嬫睜宸插叧闂�");
+ }
+
+ /**
+ * 鐜荤拑缂撳啿椤�
+ */
+ private static class GlassBufferItem {
+ final GlassInfo glassInfo;
+ final long timestamp;
+
+ GlassBufferItem(GlassInfo glassInfo, long timestamp) {
+ this.glassInfo = glassInfo;
+ this.timestamp = timestamp;
+ }
+ }
+}
+
diff --git a/mes-processes/mes-plcSend/src/main/java/com/mes/s7/provider/S7SerializerProvider.java b/mes-processes/mes-plcSend/src/main/java/com/mes/s7/provider/S7SerializerProvider.java
new file mode 100644
index 0000000..9eead5a
--- /dev/null
+++ b/mes-processes/mes-plcSend/src/main/java/com/mes/s7/provider/S7SerializerProvider.java
@@ -0,0 +1,90 @@
+package com.mes.s7.provider;
+
+import com.github.xingshuangs.iot.protocol.s7.enums.EPlcType;
+import com.github.xingshuangs.iot.protocol.s7.service.S7PLC;
+import com.mes.device.entity.DeviceConfig;
+import com.mes.s7.enhanced.EnhancedS7Serializer;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+/**
+ * 鎻愪緵 S7 搴忓垪鍖栧櫒鐨勫叕鍏卞伐鍘傦紝閬垮厤鍚勫閲嶅鍒涘缓/缂撳瓨
+ * @author huang
+ */
+@Slf4j
+@Component
+public class S7SerializerProvider {
+
+ private final ConcurrentMap<String, EnhancedS7Serializer> serializerCache = new ConcurrentHashMap<>();
+
+ /**
+ * 鑾峰彇鎴栧垱寤轰笌璁惧缁戝畾鐨� S7 搴忓垪鍖栧櫒
+ */
+ public EnhancedS7Serializer getSerializer(DeviceConfig deviceConfig) {
+ if (deviceConfig == null) {
+ log.error("璁惧閰嶇疆涓虹┖锛屾棤娉曡幏鍙朣7搴忓垪鍖栧櫒");
+ return null;
+ }
+
+ String cacheKey = buildCacheKey(deviceConfig);
+ return serializerCache.computeIfAbsent(cacheKey, key -> createSerializer(deviceConfig));
+ }
+
+ /**
+ * 娓呴櫎鎸囧畾璁惧瀵瑰簲鐨勭紦瀛�
+ */
+ public void clear(DeviceConfig deviceConfig) {
+ if (deviceConfig == null) {
+ return;
+ }
+ serializerCache.remove(buildCacheKey(deviceConfig));
+ }
+
+ private String buildCacheKey(DeviceConfig deviceConfig) {
+ if (deviceConfig.getId() != null) {
+ return "device:" + deviceConfig.getId();
+ }
+ if (deviceConfig.getDeviceCode() != null) {
+ return "device:" + deviceConfig.getDeviceCode();
+ }
+ return "device:" + Objects.hash(deviceConfig);
+ }
+
+ private EnhancedS7Serializer createSerializer(DeviceConfig deviceConfig) {
+ try {
+ EPlcType plcType = parsePlcType(deviceConfig.getPlcType());
+ String plcIp = deviceConfig.getPlcIp();
+ if (plcIp == null || plcIp.isEmpty()) {
+ log.warn("璁惧鏈厤缃甈LC IP锛屼娇鐢ㄩ粯璁ゅ�� 192.168.10.21, deviceId={}", deviceConfig.getId());
+ plcIp = "192.168.10.21";
+ }
+ S7PLC s7Plc = new S7PLC(plcType, plcIp);
+ EnhancedS7Serializer serializer = EnhancedS7Serializer.newInstance(s7Plc);
+ if (serializer == null) {
+ log.error("鍒涘缓EnhancedS7Serializer澶辫触: deviceId={}, plcIp={}, plcType={}",
+ deviceConfig.getId(), plcIp, plcType);
+ }
+ return serializer;
+ } catch (Exception e) {
+ log.error("鍒涘缓S7搴忓垪鍖栧櫒寮傚父: deviceId={}", deviceConfig.getId(), e);
+ return null;
+ }
+ }
+
+ private EPlcType parsePlcType(String plcTypeValue) {
+ if (plcTypeValue == null || plcTypeValue.isEmpty()) {
+ return EPlcType.S1200;
+ }
+ try {
+ return EPlcType.valueOf(plcTypeValue);
+ } catch (IllegalArgumentException e) {
+ log.warn("鏈煡鐨凱LC绫诲瀷: {}锛屼娇鐢ㄩ粯璁ょ被鍨婼1200", plcTypeValue);
+ return EPlcType.S1200;
+ }
+ }
+}
+
diff --git a/mes-processes/mes-plcSend/src/main/java/com/mes/task/controller/TaskStatusNotificationController.java b/mes-processes/mes-plcSend/src/main/java/com/mes/task/controller/TaskStatusNotificationController.java
index b5247b8..cdaa096 100644
--- a/mes-processes/mes-plcSend/src/main/java/com/mes/task/controller/TaskStatusNotificationController.java
+++ b/mes-processes/mes-plcSend/src/main/java/com/mes/task/controller/TaskStatusNotificationController.java
@@ -16,15 +16,17 @@
* @since 2025-01-XX
*/
@RestController
-@RequestMapping("/api/plcSend/task/notification")
+@RequestMapping("task/notification")
@Api(tags = "浠诲姟鐘舵�侀�氱煡")
@RequiredArgsConstructor
+@CrossOrigin(origins = "*", maxAge = 3600)
public class TaskStatusNotificationController {
private final TaskStatusNotificationService notificationService;
@GetMapping(value = "/sse", produces = "text/event-stream")
@ApiOperation("鍒涘缓SSE杩炴帴锛岀洃鍚换鍔$姸鎬佸彉鍖�")
+ @CrossOrigin(origins = "*")
public SseEmitter createConnection(@RequestParam(required = false) String taskId) {
SseEmitter emitter = notificationService.createConnection(taskId);
if (emitter == null) {
@@ -33,8 +35,15 @@
return emitter;
}
+ @RequestMapping(value = "/sse", method = RequestMethod.OPTIONS)
+ @CrossOrigin(origins = "*")
+ public void options() {
+ // 澶勭悊 OPTIONS 棰勬璇锋眰
+ }
+
@GetMapping(value = "/sse/all", produces = "text/event-stream")
@ApiOperation("鍒涘缓SSE杩炴帴锛岀洃鍚墍鏈変换鍔$姸鎬佸彉鍖�")
+ @CrossOrigin(origins = "*")
public SseEmitter createConnectionForAllTasks() {
return createConnection(null);
}
diff --git a/mes-processes/mes-plcSend/src/main/java/com/mes/task/service/TaskExecutionEngine.java b/mes-processes/mes-plcSend/src/main/java/com/mes/task/service/TaskExecutionEngine.java
index 2919b0f..8f995c0 100644
--- a/mes-processes/mes-plcSend/src/main/java/com/mes/task/service/TaskExecutionEngine.java
+++ b/mes-processes/mes-plcSend/src/main/java/com/mes/task/service/TaskExecutionEngine.java
@@ -769,7 +769,7 @@
switch (device.getDeviceType()) {
case DeviceConfig.DeviceType.LOAD_VEHICLE:
context.setLoadedGlassIds(glassIds);
- // 鏁版嵁浼犻�掞細涓婂ぇ杞� -> 涓嬩竴涓澶�
+ // 鏁版嵁浼犻�掞細澶ц溅璁惧 -> 涓嬩竴涓澶�
if (!CollectionUtils.isEmpty(glassIds)) {
Map<String, Object> transferData = new HashMap<>();
transferData.put("glassIds", glassIds);
diff --git a/mes-processes/mes-plcSend/src/main/java/com/mes/task/service/impl/MultiDeviceTaskServiceImpl.java b/mes-processes/mes-plcSend/src/main/java/com/mes/task/service/impl/MultiDeviceTaskServiceImpl.java
index 6792e3a..2cdac06 100644
--- a/mes-processes/mes-plcSend/src/main/java/com/mes/task/service/impl/MultiDeviceTaskServiceImpl.java
+++ b/mes-processes/mes-plcSend/src/main/java/com/mes/task/service/impl/MultiDeviceTaskServiceImpl.java
@@ -22,6 +22,7 @@
import com.mes.task.service.TaskStatusNotificationService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
@@ -69,6 +70,7 @@
throw new IllegalArgumentException("鑷冲皯闇�瑕侀厤缃竴鏉$幓鐠僆D");
}
+ // 鍒涘缓浠诲姟璁板綍
MultiDeviceTask task = new MultiDeviceTask();
task.setTaskId(generateTaskId(groupConfig));
task.setGroupId(String.valueOf(groupConfig.getId()));
@@ -79,11 +81,39 @@
task.setStartTime(new Date());
save(task);
+ // 寮傛鎵ц浠诲姟锛岀珛鍗宠繑鍥炰换鍔D
+ executeTaskAsync(task, groupConfig, devices, parameters);
+
+ log.info("璁惧缁勪换鍔″凡鍚姩锛堝紓姝ユ墽琛岋級: taskId={}, groupId={}, groupName={}",
+ task.getTaskId(), groupConfig.getId(), groupConfig.getGroupName());
+
+ return task;
+ }
+
+ /**
+ * 寮傛鎵ц璁惧缁勪换鍔�
+ * 姣忎釜璁惧缁勪綔涓虹嫭绔嬬嚎绋嬫墽琛岋紝浜掍笉闃诲
+ */
+ @Async("deviceGroupTaskExecutor")
+ public void executeTaskAsync(MultiDeviceTask task,
+ DeviceGroupConfig groupConfig,
+ List<DeviceConfig> devices,
+ TaskParameters parameters) {
try {
+ log.info("寮�濮嬫墽琛岃澶囩粍浠诲姟: taskId={}, groupId={}, deviceCount={}",
+ task.getTaskId(), groupConfig.getId(), devices.size());
+
+ // 鏇存柊浠诲姟鐘舵�佷负杩愯涓�
+ task.setStatus(MultiDeviceTask.Status.RUNNING.name());
+ updateById(task);
+
// 閫氱煡浠诲姟寮�濮�
notificationService.notifyTaskStatus(task);
+ // 鎵ц浠诲姟
TaskExecutionResult result = taskExecutionEngine.execute(task, groupConfig, devices, parameters);
+
+ // 鏇存柊浠诲姟缁撴灉
task.setStatus(result.isSuccess() ? MultiDeviceTask.Status.COMPLETED.name() : MultiDeviceTask.Status.FAILED.name());
task.setErrorMessage(result.isSuccess() ? null : result.getMessage());
task.setEndTime(new Date());
@@ -93,14 +123,20 @@
// 閫氱煡浠诲姟瀹屾垚
notificationService.notifyTaskStatus(task);
- return task;
+ log.info("璁惧缁勪换鍔℃墽琛屽畬鎴�: taskId={}, success={}, message={}",
+ task.getTaskId(), result.isSuccess(), result.getMessage());
+
} catch (Exception ex) {
- log.error("澶氳澶囦换鍔℃墽琛屽紓甯�, taskId={}", task.getTaskId(), ex);
+ log.error("璁惧缁勪换鍔℃墽琛屽紓甯�: taskId={}, groupId={}", task.getTaskId(), groupConfig.getId(), ex);
+
+ // 鏇存柊浠诲姟鐘舵�佷负澶辫触
task.setStatus(MultiDeviceTask.Status.FAILED.name());
task.setErrorMessage(ex.getMessage());
task.setEndTime(new Date());
updateById(task);
- throw new RuntimeException("澶氳澶囦换鍔℃墽琛屽け璐�: " + ex.getMessage(), ex);
+
+ // 閫氱煡浠诲姟澶辫触
+ notificationService.notifyTaskStatus(task);
}
}
diff --git a/mes-web/src/utils/constants.js b/mes-web/src/utils/constants.js
index ad93a89..d33355f 100644
--- a/mes-web/src/utils/constants.js
+++ b/mes-web/src/utils/constants.js
@@ -1,6 +1,6 @@
// export const WebSocketHost = "10.153.19.150";
// export const WebSocketHost = "172.17.2.7";
-export const WebSocketHost = "10.153.19.49";//hxl
+export const WebSocketHost = "10.153.19.225";//hxl
// export const WebSocketHost = "10.153.19.2";//zt
//export const WebSocketHost = "10.153.19.20";//wsx
// export const WebSocketHost = "127.0.0.1";
diff --git a/mes-web/src/views/device/DeviceConfigList.vue b/mes-web/src/views/device/DeviceConfigList.vue
index 6050c82..07d95a6 100644
--- a/mes-web/src/views/device/DeviceConfigList.vue
+++ b/mes-web/src/views/device/DeviceConfigList.vue
@@ -190,8 +190,8 @@
pageNum: pagination.page,
pageSize: pagination.size,
deviceType: searchForm.deviceType || undefined,
- status: searchForm.deviceStatus || undefined,
- deviceCode: searchForm.keyword || undefined
+ deviceStatus: searchForm.deviceStatus || undefined,
+ keyword: searchForm.keyword?.trim() || undefined
}
const response = await deviceConfigApi.getList(params)
diff --git a/mes-web/src/views/device/DeviceEditDialog.vue b/mes-web/src/views/device/DeviceEditDialog.vue
index 888cc2b..494fa61 100644
--- a/mes-web/src/views/device/DeviceEditDialog.vue
+++ b/mes-web/src/views/device/DeviceEditDialog.vue
@@ -41,9 +41,10 @@
<el-form-item label="璁惧绫诲瀷" prop="deviceType">
<el-select v-model="deviceForm.deviceType" placeholder="閫夋嫨璁惧绫诲瀷" style="width: 100%;">
- <el-option label="涓婂ぇ杞�" value="涓婂ぇ杞�" />
- <el-option label="澶х悊鐗�" value="澶х悊鐗�" />
- <el-option label="鐜荤拑瀛樺偍" value="鐜荤拑瀛樺偍" />
+ <el-option label="澶ц溅璁惧" value="澶ц溅璁惧" />
+ <el-option label="澶х悊鐗囩" value="澶х悊鐗囩" />
+ <el-option label="鍗ц浆绔嬫壂鐮�" value="鍗ц浆绔嬫壂鐮�" />
+ <el-option label="鍗ц浆绔�" value="鍗ц浆绔�" />
</el-select>
</el-form-item>
@@ -51,10 +52,7 @@
<el-select v-model="deviceForm.plcType" placeholder="閫夋嫨PLC绫诲瀷" style="width: 100%;" clearable>
<el-option label="瑗块棬瀛� S7-1200" value="S1200" />
<el-option label="瑗块棬瀛� S7-1500" value="S1500" />
- <el-option label="瑗块棬瀛� S7-400" value="S400" />
- <el-option label="瑗块棬瀛� S7-300" value="S300" />
- <el-option label="瑗块棬瀛� S7-200" value="S200" />
- <el-option label="瑗块棬瀛� S7-200 SMART" value="S200_SMART" />
+ <el-option label="Modbus 鎺у埗鍣�" value="MODBUS" />
</el-select>
</el-form-item>
@@ -254,204 +252,19 @@
<span class="form-tip">鏍规嵁璁惧绫诲瀷閰嶇疆鐗瑰畾鐨勪笟鍔¢�昏緫鍙傛暟</span>
</template>
- <!-- 涓婂ぇ杞﹁澶囬�昏緫閰嶇疆 -->
- <div v-if="deviceForm.deviceType === '涓婂ぇ杞�'">
- <el-row :gutter="20">
- <el-col :span="12">
- <el-form-item label="杞﹁締瀹归噺">
- <el-input-number
- v-model="deviceLogicParams.vehicleCapacity"
- :min="1"
- :max="10000"
- :step="100"
- style="width: 100%;"
+ <!-- 浣跨敤鍔ㄦ�佺粍浠跺姞杞藉搴旇澶囩被鍨嬬殑閰嶇疆缁勪欢 -->
+ <component
+ :is="deviceConfigComponent"
+ v-if="deviceConfigComponent"
+ v-model="deviceLogicParams"
/>
- <span class="form-tip">杞﹁締鏈�澶у閲�</span>
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="鐜荤拑闂撮殧(ms)">
- <el-input-number
- v-model="deviceLogicParams.glassIntervalMs"
- :min="100"
- :max="10000"
- :step="100"
- style="width: 100%;"
- />
- <span class="form-tip">鐜荤拑涓婃枡闂撮殧鏃堕棿锛堟绉掞級</span>
- </el-form-item>
- </el-col>
- </el-row>
- <el-row :gutter="20">
- <el-col :span="12">
- <el-form-item label="榛樿鐜荤拑闀垮害(mm)">
- <el-input-number
- v-model="deviceLogicParams.defaultGlassLength"
- :min="100"
- :max="10000"
- :step="100"
- style="width: 100%;"
- />
- <span class="form-tip">褰撶幓鐠冩湭鎻愪緵闀垮害鏃朵娇鐢ㄧ殑榛樿鍊�</span>
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="鑷姩涓婃枡">
- <el-switch v-model="deviceLogicParams.autoFeed" />
- <span class="form-tip">鏄惁鑷姩瑙﹀彂涓婃枡璇锋眰</span>
- </el-form-item>
- </el-col>
- </el-row>
- <el-row :gutter="20">
- <el-col :span="12">
- <el-form-item label="鏈�澶ч噸璇曟鏁�">
- <el-input-number
- v-model="deviceLogicParams.maxRetryCount"
- :min="0"
- :max="10"
- :step="1"
- style="width: 100%;"
- />
- </el-form-item>
- </el-col>
- </el-row>
- <el-form-item label="浣嶇疆鏄犲皠">
- <div class="position-mapping">
- <div
- v-for="(value, key, index) in deviceLogicParams.positionMapping"
- :key="index"
- class="mapping-item"
- >
- <el-input
- v-model="mappingKeys[index]"
- placeholder="浣嶇疆浠g爜"
- size="small"
- style="width: 150px; margin-right: 10px;"
- @input="updatePositionMapping(index, $event, value)"
- />
- <el-input-number
- v-model="deviceLogicParams.positionMapping[mappingKeys[index] || key]"
- :min="0"
- :max="100"
- size="small"
- style="width: 120px; margin-right: 10px;"
- />
- <el-button
- type="danger"
- size="small"
- @click="removePositionMapping(key)"
- >
- 鍒犻櫎
- </el-button>
- </div>
- <el-button type="primary" size="small" @click="addPositionMapping">
- 娣诲姞浣嶇疆鏄犲皠
- </el-button>
- </div>
- </el-form-item>
- </div>
-
- <!-- 澶х悊鐗囪澶囬�昏緫閰嶇疆 -->
- <div v-if="deviceForm.deviceType === '澶х悊鐗�'">
- <el-row :gutter="20">
- <el-col :span="12">
- <el-form-item label="鐜荤拑灏哄">
- <el-input-number
- v-model="deviceLogicParams.glassSize"
- :min="100"
- :max="5000"
- :step="100"
- style="width: 100%;"
- />
- <span class="form-tip">鐜荤拑灏哄锛坢m锛�</span>
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="澶勭悊鏃堕棿(ms)">
- <el-input-number
- v-model="deviceLogicParams.processingTime"
- :min="1000"
- :max="60000"
- :step="1000"
- style="width: 100%;"
- />
- <span class="form-tip">鐜荤拑澶勭悊鏃堕棿锛堟绉掞級</span>
- </el-form-item>
- </el-col>
- </el-row>
- <el-row :gutter="20">
- <el-col :span="12">
- <el-form-item label="鑷姩澶勭悊">
- <el-switch v-model="deviceLogicParams.autoProcess" />
- <span class="form-tip">鏄惁鑷姩瑙﹀彂澶勭悊璇锋眰</span>
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="鏈�澶ч噸璇曟鏁�">
- <el-input-number
- v-model="deviceLogicParams.maxRetryCount"
- :min="0"
- :max="10"
- :step="1"
- style="width: 100%;"
- />
- </el-form-item>
- </el-col>
- </el-row>
- </div>
-
- <!-- 鐜荤拑瀛樺偍璁惧閫昏緫閰嶇疆 -->
- <div v-if="deviceForm.deviceType === '鐜荤拑瀛樺偍'">
- <el-row :gutter="20">
- <el-col :span="12">
- <el-form-item label="瀛樺偍瀹归噺">
- <el-input-number
- v-model="deviceLogicParams.storageCapacity"
- :min="1"
- :max="1000"
- :step="1"
- style="width: 100%;"
- />
- <span class="form-tip">鏈�澶у瓨鍌ㄦ暟閲�</span>
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="鍙栬揣妯″紡">
- <el-select v-model="deviceLogicParams.retrievalMode" style="width: 100%;">
- <el-option label="鍏堣繘鍏堝嚭 (FIFO)" value="FIFO" />
- <el-option label="鍚庤繘鍏堝嚭 (LIFO)" value="LIFO" />
- <el-option label="闅忔満 (RANDOM)" value="RANDOM" />
- </el-select>
- </el-form-item>
- </el-col>
- </el-row>
- <el-row :gutter="20">
- <el-col :span="12">
- <el-form-item label="鑷姩瀛樺偍">
- <el-switch v-model="deviceLogicParams.autoStore" />
- <span class="form-tip">鏄惁鑷姩瑙﹀彂瀛樺偍璇锋眰</span>
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="鑷姩鍙栬揣">
- <el-switch v-model="deviceLogicParams.autoRetrieve" />
- <span class="form-tip">鏄惁鑷姩瑙﹀彂鍙栬揣璇锋眰</span>
- </el-form-item>
- </el-col>
- </el-row>
- <el-row :gutter="20">
- <el-col :span="12">
- <el-form-item label="鏈�澶ч噸璇曟鏁�">
- <el-input-number
- v-model="deviceLogicParams.maxRetryCount"
- :min="0"
- :max="10"
- :step="1"
- style="width: 100%;"
- />
- </el-form-item>
- </el-col>
- </el-row>
+ <div v-else class="no-config-tip">
+ <el-alert
+ :title="`璁惧绫诲瀷銆�${deviceForm.deviceType}銆嶆殏鏃犻厤缃粍浠禶"
+ type="info"
+ :closable="false"
+ show-icon
+ />
</div>
</el-card>
@@ -510,6 +323,7 @@
import { ref, reactive, watch, computed } from 'vue'
import { ElMessage } from 'element-plus'
import { deviceConfigApi } from '@/api/device/deviceManagement'
+import { getDeviceConfigComponent } from './components/DeviceLogicConfig'
// Props瀹氫箟
const props = defineProps({
@@ -534,27 +348,18 @@
const testResult = ref(null)
// 璁惧閫昏緫鍙傛暟锛堟牴鎹澶囩被鍨嬪姩鎬佹樉绀猴級
-const deviceLogicParams = reactive({
- // 涓婂ぇ杞﹀弬鏁�
- vehicleCapacity: 6000,
- glassIntervalMs: 1000,
- defaultGlassLength: 2000,
- autoFeed: true,
- maxRetryCount: 5,
- positionMapping: {},
- // 澶х悊鐗囧弬鏁�
- glassSize: 2000,
- processingTime: 5000,
- autoProcess: true,
- // 鐜荤拑瀛樺偍鍙傛暟
- storageCapacity: 100,
- retrievalMode: 'FIFO',
- autoStore: true,
- autoRetrieve: true
-})
+const deviceLogicParams = reactive({})
-// 浣嶇疆鏄犲皠鐨勯敭鏁扮粍锛堢敤浜巚-for锛�
-const mappingKeys = ref([])
+const S7_PLC_TYPES = ['S1200', 'S1500']
+const MODBUS_PLC_TYPES = ['MODBUS']
+
+// 璁$畻灞炴�э細鏍规嵁璁惧绫诲瀷鑾峰彇瀵瑰簲鐨勯厤缃粍浠�
+const deviceConfigComponent = computed(() => {
+ if (!deviceForm.deviceType) {
+ return null
+ }
+ return getDeviceConfigComponent(deviceForm.deviceType)
+})
// 璁惧琛ㄥ崟鏁版嵁
const getDefaultForm = () => ({
@@ -660,22 +465,37 @@
// 鐩戝惉PLC绫诲瀷鍙樺寲锛岃嚜鍔ㄨ缃�氳鍗忚
watch(() => deviceForm.plcType, (newPlcType) => {
- // 濡傛灉閫夋嫨鐨勬槸S7绯诲垪PLC锛岃嚜鍔ㄨ缃�氳鍗忚涓篠7 Communication
- if (newPlcType && (newPlcType.startsWith('S') || newPlcType.includes('S7'))) {
- if (!deviceForm.protocolType || deviceForm.protocolType === '鍏朵粬') {
+ if (!newPlcType) {
+ return
+ }
+
+ if (S7_PLC_TYPES.includes(newPlcType)) {
+ if (!deviceForm.protocolType || deviceForm.protocolType === '鍏朵粬' || deviceForm.protocolType === 'Modbus TCP') {
deviceForm.protocolType = 'S7 Communication'
+ }
+ return
+ }
+
+ if (MODBUS_PLC_TYPES.includes(newPlcType)) {
+ if (!deviceForm.protocolType || deviceForm.protocolType === '鍏朵粬' || deviceForm.protocolType === 'S7 Communication') {
+ deviceForm.protocolType = 'Modbus TCP'
}
}
})
// 澶勭悊閫氳鍗忚鍙樺寲
const handleProtocolTypeChange = (value) => {
- // 濡傛灉閫夋嫨浜嗛潪S7鍗忚锛屼絾PLC绫诲瀷鏄疭7绯诲垪锛岀粰鍑烘彁绀�
- if (value && value !== 'S7 Communication' && deviceForm.plcType) {
- const s7Types = ['S1200', 'S1500', 'S400', 'S300', 'S200', 'S200_SMART']
- if (s7Types.includes(deviceForm.plcType)) {
- ElMessage.warning('S7绯诲垪PLC閫氬父浣跨敤S7 Communication鍗忚锛岃纭鍗忚閫夋嫨鏄惁姝g‘')
- }
+ if (!deviceForm.plcType || !value) {
+ return
+ }
+
+ if (value !== 'S7 Communication' && S7_PLC_TYPES.includes(deviceForm.plcType)) {
+ ElMessage.warning('S7绯诲垪PLC閫氬父浣跨敤S7 Communication鍗忚锛岃纭鍗忚閫夋嫨鏄惁姝g‘')
+ return
+ }
+
+ if (value !== 'Modbus TCP' && MODBUS_PLC_TYPES.includes(deviceForm.plcType)) {
+ ElMessage.warning('Modbus 绫诲瀷PLC閫氬父浣跨敤 Modbus TCP 鍗忚锛岃纭鍗忚閫夋嫨鏄惁姝g‘')
}
}
@@ -824,72 +644,26 @@
// 鍔犺浇璁惧閫昏緫鍙傛暟
const loadDeviceLogicParams = (deviceLogic, deviceType) => {
- if (deviceType === '涓婂ぇ杞�') {
- deviceLogicParams.vehicleCapacity = deviceLogic.vehicleCapacity ?? 6000
- deviceLogicParams.glassIntervalMs = deviceLogic.glassIntervalMs ?? 1000
- deviceLogicParams.defaultGlassLength = deviceLogic.defaultGlassLength ?? 2000
- deviceLogicParams.autoFeed = deviceLogic.autoFeed ?? true
- deviceLogicParams.maxRetryCount = deviceLogic.maxRetryCount ?? 5
- deviceLogicParams.positionMapping = deviceLogic.positionMapping || {}
- mappingKeys.value = Object.keys(deviceLogicParams.positionMapping)
- } else if (deviceType === '澶х悊鐗�') {
- deviceLogicParams.glassSize = deviceLogic.glassSize ?? 2000
- deviceLogicParams.processingTime = deviceLogic.processingTime ?? 5000
- deviceLogicParams.autoProcess = deviceLogic.autoProcess ?? true
- deviceLogicParams.maxRetryCount = deviceLogic.maxRetryCount ?? 3
- } else if (deviceType === '鐜荤拑瀛樺偍') {
- deviceLogicParams.storageCapacity = deviceLogic.storageCapacity ?? 100
- deviceLogicParams.retrievalMode = deviceLogic.retrievalMode || 'FIFO'
- deviceLogicParams.autoStore = deviceLogic.autoStore ?? true
- deviceLogicParams.autoRetrieve = deviceLogic.autoRetrieve ?? true
- deviceLogicParams.maxRetryCount = deviceLogic.maxRetryCount ?? 3
+ // 娓呯┖鐜版湁鍙傛暟
+ Object.keys(deviceLogicParams).forEach(key => {
+ delete deviceLogicParams[key]
+ })
+
+ // 鏍规嵁璁惧绫诲瀷鍔犺浇瀵瑰簲鐨勫弬鏁�
+ if (deviceLogic && Object.keys(deviceLogic).length > 0) {
+ Object.assign(deviceLogicParams, deviceLogic)
}
}
-// 浣嶇疆鏄犲皠鐩稿叧鏂规硶
-const addPositionMapping = () => {
- const newKey = `POS${Object.keys(deviceLogicParams.positionMapping).length + 1}`
- deviceLogicParams.positionMapping[newKey] = 1
- mappingKeys.value.push(newKey)
-}
-
-const removePositionMapping = (key) => {
- delete deviceLogicParams.positionMapping[key]
- mappingKeys.value = mappingKeys.value.filter(k => k !== key)
-}
-
-const updatePositionMapping = (index, newKey, oldValue) => {
- const oldKey = mappingKeys.value[index]
- if (oldKey && oldKey !== newKey) {
- delete deviceLogicParams.positionMapping[oldKey]
- }
- mappingKeys.value[index] = newKey
- if (newKey) {
- deviceLogicParams.positionMapping[newKey] = oldValue || 1
- }
-}
const resetForm = () => {
Object.assign(deviceForm, getDefaultForm())
deviceFormRef.value?.clearValidate()
// 閲嶇疆璁惧閫昏緫鍙傛暟
- deviceLogicParams.vehicleCapacity = 6000
- deviceLogicParams.glassIntervalMs = 1000
- deviceLogicParams.defaultGlassLength = 2000
- deviceLogicParams.autoFeed = true
- deviceLogicParams.maxRetryCount = 5
- deviceLogicParams.positionMapping = {}
- mappingKeys.value = []
-
- deviceLogicParams.glassSize = 2000
- deviceLogicParams.processingTime = 5000
- deviceLogicParams.autoProcess = true
-
- deviceLogicParams.storageCapacity = 100
- deviceLogicParams.retrievalMode = 'FIFO'
- deviceLogicParams.autoStore = true
- deviceLogicParams.autoRetrieve = true
+ Object.keys(deviceLogicParams).forEach(key => {
+ delete deviceLogicParams[key]
+ })
}
const addConfigParam = () => {
@@ -963,30 +737,9 @@
plcType: deviceForm.plcType
}
- // 淇濆瓨璁惧閫昏緫鍙傛暟
- const deviceLogic = {}
- if (deviceForm.deviceType === '涓婂ぇ杞�') {
- deviceLogic.vehicleCapacity = deviceLogicParams.vehicleCapacity
- deviceLogic.glassIntervalMs = deviceLogicParams.glassIntervalMs
- deviceLogic.defaultGlassLength = deviceLogicParams.defaultGlassLength
- deviceLogic.autoFeed = deviceLogicParams.autoFeed
- deviceLogic.maxRetryCount = deviceLogicParams.maxRetryCount
- deviceLogic.positionMapping = deviceLogicParams.positionMapping
- } else if (deviceForm.deviceType === '澶х悊鐗�') {
- deviceLogic.glassSize = deviceLogicParams.glassSize
- deviceLogic.processingTime = deviceLogicParams.processingTime
- deviceLogic.autoProcess = deviceLogicParams.autoProcess
- deviceLogic.maxRetryCount = deviceLogicParams.maxRetryCount
- } else if (deviceForm.deviceType === '鐜荤拑瀛樺偍') {
- deviceLogic.storageCapacity = deviceLogicParams.storageCapacity
- deviceLogic.retrievalMode = deviceLogicParams.retrievalMode
- deviceLogic.autoStore = deviceLogicParams.autoStore
- deviceLogic.autoRetrieve = deviceLogicParams.autoRetrieve
- deviceLogic.maxRetryCount = deviceLogicParams.maxRetryCount
- }
-
- if (Object.keys(deviceLogic).length > 0) {
- extraObj.deviceLogic = deviceLogic
+ // 淇濆瓨璁惧閫昏緫鍙傛暟锛堢洿鎺ヤ娇鐢╠eviceLogicParams锛岀敱鍚勪釜閰嶇疆缁勪欢绠$悊锛�
+ if (deviceLogicParams && Object.keys(deviceLogicParams).length > 0) {
+ extraObj.deviceLogic = { ...deviceLogicParams }
}
// 鏋勫缓 configJson锛氬皢 configParams 鏁扮粍杞崲涓� JSON 瀛楃涓�
@@ -1142,4 +895,8 @@
border-radius: 6px;
background-color: #fafafa;
}
+
+.no-config-tip {
+ padding: 20px;
+}
</style>
\ No newline at end of file
diff --git a/mes-web/src/views/device/components/DeviceLogicConfig/LargeGlassConfig.vue b/mes-web/src/views/device/components/DeviceLogicConfig/LargeGlassConfig.vue
new file mode 100644
index 0000000..3b7e68f
--- /dev/null
+++ b/mes-web/src/views/device/components/DeviceLogicConfig/LargeGlassConfig.vue
@@ -0,0 +1,177 @@
+<template>
+ <div class="large-glass-config">
+ <el-form-item label="鏍煎瓙鑼冨洿閰嶇疆">
+ <div class="grid-ranges">
+ <div
+ v-for="(range, index) in config.gridRanges"
+ :key="index"
+ class="grid-range-item"
+ >
+ <el-input-number
+ v-model="range.row"
+ :min="1"
+ :max="100"
+ :step="1"
+ style="width: 100px; margin-right: 10px;"
+ placeholder="琛屽彿"
+ />
+ <span>琛岋細</span>
+ <el-input-number
+ v-model="range.start"
+ :min="1"
+ :max="1000"
+ :step="1"
+ style="width: 120px; margin: 0 10px;"
+ placeholder="璧峰鏍煎瓙"
+ />
+ <span>~</span>
+ <el-input-number
+ v-model="range.end"
+ :min="1"
+ :max="1000"
+ :step="1"
+ style="width: 120px; margin-left: 10px;"
+ placeholder="缁撴潫鏍煎瓙"
+ />
+ <el-button
+ type="danger"
+ size="small"
+ style="margin-left: 10px;"
+ @click="removeGridRange(index)"
+ >
+ 鍒犻櫎
+ </el-button>
+ </div>
+ <el-button type="primary" size="small" @click="addGridRange">
+ 娣诲姞鏍煎瓙鑼冨洿
+ </el-button>
+ </div>
+ <span class="form-tip">閰嶇疆姣忚鐨勬牸瀛愯寖鍥达紝渚嬪锛氱涓�琛�1~52鏍硷紝绗簩琛�53~101鏍�</span>
+ </el-form-item>
+
+ <el-row :gutter="20">
+ <el-col :span="8">
+ <el-form-item label="姣忔牸闀垮害(mm)">
+ <el-input-number
+ v-model="config.gridLength"
+ :min="100"
+ :max="10000"
+ :step="100"
+ style="width: 100%;"
+ />
+ <span class="form-tip">姣忔牸闀垮害锛堟绫筹級</span>
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="姣忔牸瀹藉害(mm)">
+ <el-input-number
+ v-model="config.gridWidth"
+ :min="100"
+ :max="10000"
+ :step="100"
+ style="width: 100%;"
+ />
+ <span class="form-tip">姣忔牸瀹藉害锛堟绫筹級</span>
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="姣忔牸鍘氬害(mm)">
+ <el-input-number
+ v-model="config.gridThickness"
+ :min="1"
+ :max="100"
+ :step="1"
+ style="width: 100%;"
+ />
+ <span class="form-tip">姣忔牸鍘氬害锛堟绫筹級</span>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </div>
+</template>
+
+<script setup>
+import { ref, watch } from 'vue'
+
+const props = defineProps({
+ modelValue: {
+ type: Object,
+ default: () => ({})
+ }
+})
+
+const emit = defineEmits(['update:modelValue'])
+
+// 閰嶇疆鏁版嵁
+const config = ref({
+ gridRanges: [
+ { row: 1, start: 1, end: 52 },
+ { row: 2, start: 53, end: 101 }
+ ],
+ gridLength: 2000,
+ gridWidth: 1500,
+ gridThickness: 5
+})
+
+// 鐩戝惉props鍙樺寲
+watch(() => props.modelValue, (newVal) => {
+ if (newVal && Object.keys(newVal).length > 0) {
+ config.value = {
+ gridRanges: newVal.gridRanges || [
+ { row: 1, start: 1, end: 52 },
+ { row: 2, start: 53, end: 101 }
+ ],
+ gridLength: newVal.gridLength ?? 2000,
+ gridWidth: newVal.gridWidth ?? 1500,
+ gridThickness: newVal.gridThickness ?? 5
+ }
+ }
+}, { immediate: true, deep: true })
+
+// 鐩戝惉config鍙樺寲锛屽悓姝ュ埌鐖剁粍浠�
+watch(config, (newVal) => {
+ emit('update:modelValue', { ...newVal })
+}, { deep: true })
+
+// 鏍煎瓙鑼冨洿鐩稿叧鏂规硶
+const addGridRange = () => {
+ const maxRow = config.value.gridRanges.length > 0
+ ? Math.max(...config.value.gridRanges.map(r => r.row))
+ : 0
+ const lastEnd = config.value.gridRanges.length > 0
+ ? Math.max(...config.value.gridRanges.map(r => r.end))
+ : 0
+ config.value.gridRanges.push({
+ row: maxRow + 1,
+ start: lastEnd + 1,
+ end: lastEnd + 50
+ })
+}
+
+const removeGridRange = (index) => {
+ config.value.gridRanges.splice(index, 1)
+}
+</script>
+
+<style scoped>
+.form-tip {
+ margin-left: 10px;
+ font-size: 12px;
+ color: #909399;
+}
+
+.grid-ranges {
+ width: 100%;
+}
+
+.grid-range-item {
+ display: flex;
+ align-items: center;
+ margin-bottom: 12px;
+ padding: 12px;
+ border: 1px solid #ebeef5;
+ border-radius: 6px;
+ background-color: #fafafa;
+}
+</style>
+
diff --git a/mes-web/src/views/device/components/DeviceLogicConfig/LoadVehicleConfig.vue b/mes-web/src/views/device/components/DeviceLogicConfig/LoadVehicleConfig.vue
new file mode 100644
index 0000000..6f1bd5b
--- /dev/null
+++ b/mes-web/src/views/device/components/DeviceLogicConfig/LoadVehicleConfig.vue
@@ -0,0 +1,324 @@
+<template>
+ <div class="load-vehicle-config">
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="杞﹁締瀹归噺(mm)">
+ <el-input-number
+ v-model="config.vehicleCapacity"
+ :min="1"
+ :max="10000"
+ :step="100"
+ style="width: 100%;"
+ />
+ <span class="form-tip">杞﹁締鏈�澶у閲�</span>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="杞﹁締閫熷害(鏍�/绉�)">
+ <el-input-number
+ v-model="config.vehicleSpeed"
+ :min="0.1"
+ :max="10"
+ :step="0.1"
+ :precision="1"
+ style="width: 100%;"
+ />
+ <span class="form-tip">杞﹁締杩愬姩閫熷害锛岄粯璁�1鏍�/绉�</span>
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鐜荤拑闂撮殧(ms)">
+ <el-input-number
+ v-model="config.glassIntervalMs"
+ :min="100"
+ :max="10000"
+ :step="100"
+ style="width: 100%;"
+ />
+ <span class="form-tip">鐜荤拑涓婃枡闂撮殧鏃堕棿锛堟绉掞級</span>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="榛樿鐜荤拑闀垮害(mm)">
+ <el-input-number
+ v-model="config.defaultGlassLength"
+ :min="100"
+ :max="10000"
+ :step="100"
+ style="width: 100%;"
+ />
+ <span class="form-tip">褰撶幓鐠冩湭鎻愪緵闀垮害鏃朵娇鐢ㄧ殑榛樿鍊�</span>
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鍒濆浣嶇疆(鏍�)">
+ <el-input-number
+ v-model="config.homePosition"
+ :min="0"
+ :max="1000"
+ :step="1"
+ style="width: 100%;"
+ />
+ <span class="form-tip">杞﹁締鍒濆浣嶇疆锛堟牸瀛愶級</span>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="杩愬姩璺濈鑼冨洿">
+ <el-input-number
+ v-model="config.minRange"
+ :min="1"
+ :max="1000"
+ :step="1"
+ style="width: 48%;"
+ placeholder="鏈�灏�"
+ />
+ <span style="margin: 0 2%;">~</span>
+ <el-input-number
+ v-model="config.maxRange"
+ :min="1"
+ :max="1000"
+ :step="1"
+ style="width: 48%;"
+ placeholder="鏈�澶�"
+ />
+ <span class="form-tip">杩愬姩璺濈鑼冨洿锛堟牸瀛愶級</span>
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="绌洪棽鐩戞帶闂撮殧(ms)">
+ <el-input-number
+ v-model="config.idleMonitorIntervalMs"
+ :min="500"
+ :max="10000"
+ :step="100"
+ style="width: 100%;"
+ />
+ <span class="form-tip">绌洪棽鐘舵�佺洃鎺ч棿闅旓紝榛樿2000ms</span>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="浠诲姟鐩戞帶闂撮殧(ms)">
+ <el-input-number
+ v-model="config.taskMonitorIntervalMs"
+ :min="500"
+ :max="10000"
+ :step="100"
+ style="width: 100%;"
+ />
+ <span class="form-tip">浠诲姟鎵ц鐩戞帶闂撮殧锛岄粯璁�1000ms</span>
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="MES纭瓒呮椂(ms)">
+ <el-input-number
+ v-model="config.mesConfirmTimeoutMs"
+ :min="5000"
+ :max="300000"
+ :step="1000"
+ style="width: 100%;"
+ />
+ <span class="form-tip">绛夊緟MES纭鐨勮秴鏃舵椂闂达紝榛樿30000ms</span>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鑷姩涓婃枡">
+ <el-switch v-model="config.autoFeed" />
+ <span class="form-tip">鏄惁鑷姩瑙﹀彂涓婃枡璇锋眰</span>
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鏈�澶ч噸璇曟鏁�">
+ <el-input-number
+ v-model="config.maxRetryCount"
+ :min="0"
+ :max="10"
+ :step="1"
+ style="width: 100%;"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-form-item label="浣嶇疆鏄犲皠">
+ <div class="position-mapping">
+ <div
+ v-for="(value, key, index) in config.positionMapping"
+ :key="index"
+ class="mapping-item"
+ >
+ <el-input
+ v-model="mappingKeys[index]"
+ placeholder="浣嶇疆浠g爜锛堝900/901锛�"
+ size="small"
+ style="width: 150px; margin-right: 10px;"
+ @input="updatePositionMapping(index, $event, value)"
+ />
+ <el-input-number
+ v-model="config.positionMapping[mappingKeys[index] || key]"
+ :min="0"
+ :max="1000"
+ :step="1"
+ size="small"
+ style="width: 120px; margin-right: 10px;"
+ placeholder="浣嶇疆鍊硷紙鏍硷級"
+ />
+ <el-button
+ type="danger"
+ size="small"
+ @click="removePositionMapping(key)"
+ >
+ 鍒犻櫎
+ </el-button>
+ </div>
+ <el-button type="primary" size="small" @click="addPositionMapping">
+ 娣诲姞浣嶇疆鏄犲皠
+ </el-button>
+ </div>
+ <span class="form-tip">灏哅ES缂栧彿锛堝900/901锛夋槧灏勪负瀹為檯浣嶇疆鍊硷紙鏍煎瓙锛�</span>
+ </el-form-item>
+
+ <el-form-item label="鍑虹墖浠诲姟鏍煎瓙鑼冨洿">
+ <el-input-number
+ v-model="config.outboundSlotRanges[0]"
+ :min="1"
+ :max="1000"
+ :step="1"
+ style="width: 48%;"
+ placeholder="鏈�灏忔牸瀛愮紪鍙�"
+ />
+ <span style="margin: 0 2%;">~</span>
+ <el-input-number
+ v-model="config.outboundSlotRanges[1]"
+ :min="1"
+ :max="1000"
+ :step="1"
+ style="width: 48%;"
+ placeholder="鏈�澶ф牸瀛愮紪鍙�"
+ />
+ <span class="form-tip">鍑虹墖浠诲姟鐨剆tartSlot鑼冨洿锛屼緥濡俒1, 101]琛ㄧず鏍煎瓙1~101閮芥槸鍑虹墖浠诲姟</span>
+ </el-form-item>
+ </div>
+</template>
+
+<script setup>
+import { ref, watch } from 'vue'
+
+const props = defineProps({
+ modelValue: {
+ type: Object,
+ default: () => ({})
+ }
+})
+
+const emit = defineEmits(['update:modelValue'])
+
+// 閰嶇疆鏁版嵁
+const config = ref({
+ vehicleCapacity: 6000,
+ vehicleSpeed: 1.0,
+ glassIntervalMs: 1000,
+ defaultGlassLength: 2000,
+ homePosition: 0,
+ minRange: 1,
+ maxRange: 100,
+ idleMonitorIntervalMs: 2000,
+ taskMonitorIntervalMs: 1000,
+ mesConfirmTimeoutMs: 30000,
+ autoFeed: true,
+ maxRetryCount: 5,
+ positionMapping: {},
+ outboundSlotRanges: [1, 101]
+})
+
+// 浣嶇疆鏄犲皠鐨勯敭鏁扮粍
+const mappingKeys = ref([])
+
+// 鐩戝惉props鍙樺寲
+watch(() => props.modelValue, (newVal) => {
+ if (newVal && Object.keys(newVal).length > 0) {
+ config.value = {
+ vehicleCapacity: newVal.vehicleCapacity ?? 6000,
+ vehicleSpeed: newVal.vehicleSpeed ?? 1.0,
+ glassIntervalMs: newVal.glassIntervalMs ?? 1000,
+ defaultGlassLength: newVal.defaultGlassLength ?? 2000,
+ homePosition: newVal.homePosition ?? 0,
+ minRange: newVal.minRange ?? 1,
+ maxRange: newVal.maxRange ?? 100,
+ idleMonitorIntervalMs: newVal.idleMonitorIntervalMs ?? 2000,
+ taskMonitorIntervalMs: newVal.taskMonitorIntervalMs ?? 1000,
+ mesConfirmTimeoutMs: newVal.mesConfirmTimeoutMs ?? 30000,
+ autoFeed: newVal.autoFeed ?? true,
+ maxRetryCount: newVal.maxRetryCount ?? 5,
+ positionMapping: newVal.positionMapping || {},
+ outboundSlotRanges: newVal.outboundSlotRanges || [1, 101]
+ }
+ mappingKeys.value = Object.keys(config.value.positionMapping)
+ }
+}, { immediate: true, deep: true })
+
+// 鐩戝惉config鍙樺寲锛屽悓姝ュ埌鐖剁粍浠�
+watch(config, (newVal) => {
+ emit('update:modelValue', { ...newVal })
+}, { deep: true })
+
+// 浣嶇疆鏄犲皠鐩稿叧鏂规硶
+const addPositionMapping = () => {
+ const newKey = `POS${Object.keys(config.value.positionMapping).length + 1}`
+ config.value.positionMapping[newKey] = 1
+ mappingKeys.value.push(newKey)
+}
+
+const removePositionMapping = (key) => {
+ delete config.value.positionMapping[key]
+ mappingKeys.value = mappingKeys.value.filter(k => k !== key)
+}
+
+const updatePositionMapping = (index, newKey, oldValue) => {
+ const oldKey = mappingKeys.value[index]
+ if (oldKey && oldKey !== newKey) {
+ delete config.value.positionMapping[oldKey]
+ }
+ mappingKeys.value[index] = newKey
+ if (newKey) {
+ config.value.positionMapping[newKey] = oldValue || 1
+ }
+}
+</script>
+
+<style scoped>
+.form-tip {
+ margin-left: 10px;
+ font-size: 12px;
+ color: #909399;
+}
+
+.position-mapping {
+ width: 100%;
+}
+
+.mapping-item {
+ display: flex;
+ align-items: center;
+ margin-bottom: 12px;
+ padding: 12px;
+ border: 1px solid #ebeef5;
+ border-radius: 6px;
+ background-color: #fafafa;
+}
+</style>
+
diff --git a/mes-web/src/views/device/components/DeviceLogicConfig/WorkstationScannerConfig.vue b/mes-web/src/views/device/components/DeviceLogicConfig/WorkstationScannerConfig.vue
new file mode 100644
index 0000000..e1c86c6
--- /dev/null
+++ b/mes-web/src/views/device/components/DeviceLogicConfig/WorkstationScannerConfig.vue
@@ -0,0 +1,84 @@
+<template>
+ <div class="workstation-scanner-config">
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鎵爜闂撮殧(ms)">
+ <el-input-number
+ v-model="config.scanIntervalMs"
+ :min="1000"
+ :max="60000"
+ :step="1000"
+ style="width: 100%;"
+ />
+ <span class="form-tip">瀹氭椂鎵弿MES鍐欏尯鐨勬椂闂撮棿闅旓紝榛樿10000ms锛�10绉掞級</span>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="浜х嚎缂栧彿">
+ <el-input-number
+ v-model="config.workLine"
+ :min="1"
+ :max="100"
+ :step="1"
+ style="width: 100%;"
+ />
+ <span class="form-tip">浜х嚎缂栧彿锛岀敤浜庤繃婊ょ幓鐠冧俊鎭�</span>
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鑷姩纭">
+ <el-switch v-model="config.autoAck" />
+ <span class="form-tip">鏄惁鑷姩纭MES鍙戦�佺殑鐜荤拑淇℃伅锛堝洖鍐檓esSend=0锛�</span>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </div>
+</template>
+
+<script setup>
+import { ref, watch } from 'vue'
+
+const props = defineProps({
+ modelValue: {
+ type: Object,
+ default: () => ({})
+ }
+})
+
+const emit = defineEmits(['update:modelValue'])
+
+// 閰嶇疆鏁版嵁
+const config = ref({
+ scanIntervalMs: 10000,
+ workLine: null,
+ autoAck: true
+})
+
+// 鐩戝惉props鍙樺寲
+watch(() => props.modelValue, (newVal) => {
+ if (newVal && Object.keys(newVal).length > 0) {
+ config.value = {
+ scanIntervalMs: newVal.scanIntervalMs ?? 10000,
+ workLine: newVal.workLine ?? null,
+ autoAck: newVal.autoAck ?? true
+ }
+ }
+}, { immediate: true, deep: true })
+
+// 鐩戝惉config鍙樺寲锛屽悓姝ュ埌鐖剁粍浠�
+watch(config, (newVal) => {
+ emit('update:modelValue', { ...newVal })
+}, { deep: true })
+</script>
+
+<style scoped>
+.form-tip {
+ margin-left: 10px;
+ font-size: 12px;
+ color: #909399;
+}
+</style>
+
diff --git a/mes-web/src/views/device/components/DeviceLogicConfig/WorkstationTransferConfig.vue b/mes-web/src/views/device/components/DeviceLogicConfig/WorkstationTransferConfig.vue
new file mode 100644
index 0000000..6b1f9d8
--- /dev/null
+++ b/mes-web/src/views/device/components/DeviceLogicConfig/WorkstationTransferConfig.vue
@@ -0,0 +1,146 @@
+<template>
+ <div class="workstation-transfer-config">
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鎵爜闂撮殧(ms)">
+ <el-input-number
+ v-model="config.scanIntervalMs"
+ :min="1000"
+ :max="60000"
+ :step="1000"
+ style="width: 100%;"
+ />
+ <span class="form-tip">瀹氭椂鏌ヨ鏈�杩戞壂鐮佺幓鐠冪殑鏃堕棿闂撮殧锛岄粯璁�10000ms锛�10绉掞級</span>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="缂撳啿鍒ゅ畾鏃堕棿(ms)">
+ <el-input-number
+ v-model="config.transferDelayMs"
+ :min="5000"
+ :max="120000"
+ :step="1000"
+ style="width: 100%;"
+ />
+ <span class="form-tip">30绉掑唴鏃犳柊鐜荤拑鎵爜鍒欏垽瀹氫负鏈�鍚庝竴鐗囷紝榛樿30000ms锛�30绉掞級</span>
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="杞﹁締瀹归噺(mm)">
+ <el-input-number
+ v-model="config.vehicleCapacity"
+ :min="1000"
+ :max="20000"
+ :step="100"
+ style="width: 100%;"
+ />
+ <span class="form-tip">鍙杞界殑鏈�澶у搴︼紙姣背锛夛紝榛樿6000mm</span>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鐩戞帶闂撮殧(ms)">
+ <el-input-number
+ v-model="config.monitorIntervalMs"
+ :min="1000"
+ :max="60000"
+ :step="1000"
+ style="width: 100%;"
+ />
+ <span class="form-tip">鎵规澶勭悊鐩戞帶闂撮殧锛岄粯璁や娇鐢╯canIntervalMs</span>
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="浜х嚎缂栧彿">
+ <el-input-number
+ v-model="config.workLine"
+ :min="1"
+ :max="100"
+ :step="1"
+ style="width: 100%;"
+ />
+ <span class="form-tip">浜х嚎缂栧彿锛岀敤浜庤繃婊ょ幓鐠冧俊鎭�</span>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="浣嶇疆鍊�(鏍�)">
+ <el-input-number
+ v-model="config.inPosition"
+ :min="0"
+ :max="1000"
+ :step="1"
+ style="width: 100%;"
+ />
+ <span class="form-tip">鍐欏叆PLC鐨刬nPosition鍊硷紙鏍煎瓙锛�</span>
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鑷姩纭">
+ <el-switch v-model="config.autoAck" />
+ <span class="form-tip">鏄惁鑷姩纭MES鍙戦�佺殑鐜荤拑淇℃伅</span>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </div>
+</template>
+
+<script setup>
+import { ref, watch } from 'vue'
+
+const props = defineProps({
+ modelValue: {
+ type: Object,
+ default: () => ({})
+ }
+})
+
+const emit = defineEmits(['update:modelValue'])
+
+// 閰嶇疆鏁版嵁
+const config = ref({
+ scanIntervalMs: 10000,
+ transferDelayMs: 30000,
+ vehicleCapacity: 6000,
+ monitorIntervalMs: 10000,
+ workLine: null,
+ inPosition: null,
+ autoAck: true
+})
+
+// 鐩戝惉props鍙樺寲
+watch(() => props.modelValue, (newVal) => {
+ if (newVal && Object.keys(newVal).length > 0) {
+ config.value = {
+ scanIntervalMs: newVal.scanIntervalMs ?? 10000,
+ transferDelayMs: newVal.transferDelayMs ?? 30000,
+ vehicleCapacity: newVal.vehicleCapacity ?? 6000,
+ monitorIntervalMs: newVal.monitorIntervalMs ?? newVal.scanIntervalMs ?? 10000,
+ workLine: newVal.workLine ?? null,
+ inPosition: newVal.inPosition ?? null,
+ autoAck: newVal.autoAck ?? true
+ }
+ }
+}, { immediate: true, deep: true })
+
+// 鐩戝惉config鍙樺寲锛屽悓姝ュ埌鐖剁粍浠�
+watch(config, (newVal) => {
+ emit('update:modelValue', { ...newVal })
+}, { deep: true })
+</script>
+
+<style scoped>
+.form-tip {
+ margin-left: 10px;
+ font-size: 12px;
+ color: #909399;
+}
+</style>
+
diff --git a/mes-web/src/views/device/components/DeviceLogicConfig/index.js b/mes-web/src/views/device/components/DeviceLogicConfig/index.js
new file mode 100644
index 0000000..7456aa4
--- /dev/null
+++ b/mes-web/src/views/device/components/DeviceLogicConfig/index.js
@@ -0,0 +1,34 @@
+/**
+ * 璁惧閫昏緫閰嶇疆缁勪欢瀵煎嚭
+ * 缁熶竴绠$悊鎵�鏈夎澶囩被鍨嬬殑閰嶇疆缁勪欢
+ */
+
+import LoadVehicleConfig from './LoadVehicleConfig.vue'
+import LargeGlassConfig from './LargeGlassConfig.vue'
+import WorkstationScannerConfig from './WorkstationScannerConfig.vue'
+import WorkstationTransferConfig from './WorkstationTransferConfig.vue'
+
+// 璁惧绫诲瀷鍒扮粍浠剁殑鏄犲皠
+export const deviceTypeComponentMap = {
+ '澶ц溅璁惧': LoadVehicleConfig,
+ '澶х悊鐗囩': LargeGlassConfig,
+ '鍗ц浆绔嬫壂鐮�': WorkstationScannerConfig,
+ '鍗ц浆绔�': WorkstationTransferConfig,
+ // 鍏煎鏃у悕绉�
+ '涓婂ぇ杞�': LoadVehicleConfig,
+ '澶х悊鐗�': LargeGlassConfig
+}
+
+// 瀵煎嚭鎵�鏈夌粍浠�
+export {
+ LoadVehicleConfig,
+ LargeGlassConfig,
+ WorkstationScannerConfig,
+ WorkstationTransferConfig
+}
+
+// 鏍规嵁璁惧绫诲瀷鑾峰彇瀵瑰簲鐨勯厤缃粍浠�
+export function getDeviceConfigComponent(deviceType) {
+ return deviceTypeComponentMap[deviceType] || null
+}
+
diff --git a/mes-web/src/views/plcTest/MultiDeviceWorkbench.vue b/mes-web/src/views/plcTest/MultiDeviceWorkbench.vue
index 5c5c465..3472ea8 100644
--- a/mes-web/src/views/plcTest/MultiDeviceWorkbench.vue
+++ b/mes-web/src/views/plcTest/MultiDeviceWorkbench.vue
@@ -3,23 +3,56 @@
<div class="main-grid">
<div class="left-panel">
<GroupList @select="handleGroupSelect" />
+ <GroupTopology
+ v-if="selectedGroup"
+ :group="selectedGroup"
+ class="topology-panel"
+ />
</div>
<div class="right-panel">
- <TaskOrchestration :group="selectedGroup" @task-started="refreshMonitor" />
- <ExecutionMonitor ref="monitorRef" :group-id="selectedGroupId" class="monitor-panel" />
+ <el-tabs v-model="activeTab" type="card" class="workbench-tabs">
+ <el-tab-pane label="浠诲姟缂栨帓" name="orchestration">
+ <TaskOrchestration
+ :group="selectedGroup"
+ @task-started="handleTaskStarted"
+ />
+ </el-tab-pane>
+ <el-tab-pane label="鎵ц鐩戞帶" name="monitor">
+ <ExecutionMonitor
+ ref="monitorRef"
+ :group-id="selectedGroupId"
+ :task-id="selectedTaskId"
+ class="monitor-panel"
+ @task-selected="handleTaskSelected"
+ />
+ </el-tab-pane>
+ <el-tab-pane label="缁撴灉鍒嗘瀽" name="analysis">
+ <ResultAnalysis
+ ref="analysisRef"
+ :task="selectedTask"
+ class="analysis-panel"
+ />
+ </el-tab-pane>
+ </el-tabs>
</div>
</div>
</div>
</template>
<script setup>
-import { computed, ref } from 'vue'
+import { computed, ref, watch } from 'vue'
import GroupList from './components/DeviceGroup/GroupList.vue'
+import GroupTopology from './components/DeviceGroup/GroupTopology.vue'
import TaskOrchestration from './components/MultiDeviceTest/TaskOrchestration.vue'
import ExecutionMonitor from './components/MultiDeviceTest/ExecutionMonitor.vue'
+import ResultAnalysis from './components/MultiDeviceTest/ResultAnalysis.vue'
const selectedGroup = ref(null)
const monitorRef = ref(null)
+const analysisRef = ref(null)
+const activeTab = ref('orchestration')
+const selectedTaskId = ref(null)
+const selectedTask = ref(null)
const selectedGroupId = computed(() => {
if (!selectedGroup.value) return null
@@ -28,10 +61,39 @@
const handleGroupSelect = (group) => {
selectedGroup.value = group
+ selectedTask.value = null
+ selectedTaskId.value = null
+ // 鍒囨崲鍒扮紪鎺掓爣绛鹃〉
+ activeTab.value = 'orchestration'
}
-const refreshMonitor = () => {
- monitorRef.value?.fetchTasks?.()
+const handleTaskStarted = (task) => {
+ // 浠诲姟鍚姩鍚庯紝鍒囨崲鍒扮洃鎺ф爣绛鹃〉锛堝鏋滃綋鍓嶄笉鍦ㄧ洃鎺ч〉锛�
+ if (activeTab.value !== 'monitor') {
+ activeTab.value = 'monitor'
+ }
+ // 绔嬪嵆鍒锋柊鐩戞帶鍒楄〃锛屾樉绀烘柊鍚姩鐨勪换鍔�
+ setTimeout(() => {
+ monitorRef.value?.fetchTasks?.()
+ }, 300)
+
+ // 濡傛灉浼犲叆浜嗕换鍔′俊鎭紝鍙互鑷姩閫変腑
+ if (task && task.taskId) {
+ selectedTaskId.value = task.taskId
+ }
+}
+
+const handleTaskSelected = (task) => {
+ selectedTask.value = task
+ selectedTaskId.value = task?.taskId || null
+ // 濡傛灉浠诲姟宸插畬鎴愭垨澶辫触锛屽垏鎹㈠埌缁撴灉鍒嗘瀽鏍囩椤�
+ if (task && (task.status === 'COMPLETED' || task.status === 'FAILED')) {
+ activeTab.value = 'analysis'
+ // 鍒锋柊鍒嗘瀽鏁版嵁
+ setTimeout(() => {
+ analysisRef.value?.fetchSteps?.()
+ }, 100)
+ }
}
</script>
@@ -48,14 +110,43 @@
gap: 24px;
}
-.right-panel {
+.left-panel {
display: flex;
flex-direction: column;
gap: 24px;
}
-.monitor-panel {
+.topology-panel {
flex: 1;
+ min-height: 300px;
+}
+
+.right-panel {
+ display: flex;
+ flex-direction: column;
+ min-height: 0;
+}
+
+.workbench-tabs {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ min-height: 0;
+}
+
+.workbench-tabs :deep(.el-tabs__content) {
+ flex: 1;
+ overflow: auto;
+}
+
+.workbench-tabs :deep(.el-tab-pane) {
+ height: 100%;
+}
+
+.monitor-panel,
+.analysis-panel {
+ flex: 1;
+ min-height: 500px;
}
@media (max-width: 1200px) {
diff --git a/mes-web/src/views/plcTest/components/DeviceGroup/GroupTopology.vue b/mes-web/src/views/plcTest/components/DeviceGroup/GroupTopology.vue
new file mode 100644
index 0000000..e46b9c0
--- /dev/null
+++ b/mes-web/src/views/plcTest/components/DeviceGroup/GroupTopology.vue
@@ -0,0 +1,478 @@
+<template>
+ <div class="group-topology">
+ <div class="panel-header">
+ <div>
+ <h3>璁惧缁勬嫇鎵戝浘</h3>
+ <p v-if="group">{{ group.groupName }} - 璁惧鎵ц娴佺▼鍙鍖�</p>
+ <p v-else class="warning">璇峰厛閫夋嫨涓�涓澶囩粍</p>
+ </div>
+ <div class="action-buttons">
+ <el-button :loading="loading" @click="handleRefresh">
+ <el-icon><Refresh /></el-icon>
+ 鍒锋柊
+ </el-button>
+ <el-button @click="toggleLayout">
+ <el-icon><Grid /></el-icon>
+ {{ layoutMode === 'horizontal' ? '鍨傜洿甯冨眬' : '姘村钩甯冨眬' }}
+ </el-button>
+ </div>
+ </div>
+
+ <div v-if="!group" class="empty-state">
+ <el-empty description="璇烽�夋嫨璁惧缁勬煡鐪嬫嫇鎵戝浘" />
+ </div>
+
+ <div v-else class="topology-container" :class="`layout-${layoutMode}`">
+ <template v-for="(device, index) in devices" :key="device.id || device.deviceId">
+ <div
+ class="topology-node-wrapper"
+ :class="`layout-${layoutMode}`"
+ >
+ <div
+ class="topology-node"
+ :class="getDeviceTypeClass(device.deviceType)"
+ @click="handleNodeClick(device)"
+ :title="`鐐瑰嚮鏌ョ湅璁惧璇︽儏 | 鎵ц椤哄簭: ${index + 1}`"
+ >
+ <div class="node-content">
+ <div class="node-icon">
+ <el-icon :size="24">
+ <component :is="getDeviceIcon(device.deviceType)" />
+ </el-icon>
+ </div>
+ <div class="node-info">
+ <div class="node-name">{{ device.deviceName || device.deviceCode }}</div>
+ <div class="node-type">{{ getDeviceTypeLabel(device.deviceType) }}</div>
+ <div class="node-status">
+ <el-tag :type="getStatusType(device.status)" size="small">
+ {{ getStatusLabel(device.status) }}
+ </el-tag>
+ </div>
+ </div>
+ </div>
+ <!-- 鎵ц椤哄簭鏍囪瘑锛氬彸涓婅鐨勬暟瀛楀渾鍦� -->
+ <div class="node-order" :title="`鎵ц椤哄簭: 绗� ${index + 1} 姝">
+ {{ index + 1 }}
+ </div>
+ </div>
+ <!-- 娴佺▼鏂瑰悜绠ご锛氳〃绀鸿澶囨墽琛岄『搴忓拰鏁版嵁娴佸悜 -->
+ <div
+ v-if="index < devices.length - 1"
+ class="node-arrow"
+ :title="`鏁版嵁娴佸悜: ${device.deviceName || device.deviceCode} 鈫� ${devices[index + 1]?.deviceName || devices[index + 1]?.deviceCode}`"
+ >
+ <el-icon :size="20">
+ <ArrowRight v-if="layoutMode === 'horizontal'" />
+ <ArrowDown v-else />
+ </el-icon>
+ </div>
+ </div>
+ </template>
+ </div>
+
+ <!-- 璁惧璇︽儏鍗$墖 -->
+ <el-card v-if="selectedDevice" class="device-detail-card" shadow="never">
+ <template #header>
+ <div class="card-header">
+ <span>璁惧璇︽儏</span>
+ <el-button link @click="selectedDevice = null">
+ <el-icon><Close /></el-icon>
+ </el-button>
+ </div>
+ </template>
+ <el-descriptions :column="2" border>
+ <el-descriptions-item label="璁惧鍚嶇О">
+ {{ selectedDevice.deviceName }}
+ </el-descriptions-item>
+ <el-descriptions-item label="璁惧缂栫爜">
+ {{ selectedDevice.deviceCode }}
+ </el-descriptions-item>
+ <el-descriptions-item label="璁惧绫诲瀷">
+ {{ getDeviceTypeLabel(selectedDevice.deviceType) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鐘舵��">
+ <el-tag :type="getStatusType(selectedDevice.status)">
+ {{ getStatusLabel(selectedDevice.status) }}
+ </el-tag>
+ </el-descriptions-item>
+ <el-descriptions-item label="PLC IP" v-if="selectedDevice.plcIp">
+ {{ selectedDevice.plcIp }}
+ </el-descriptions-item>
+ <el-descriptions-item label="PLC绫诲瀷" v-if="selectedDevice.plcType">
+ {{ selectedDevice.plcType }}
+ </el-descriptions-item>
+ <el-descriptions-item label="妯″潡鍚嶇О" v-if="selectedDevice.moduleName">
+ {{ selectedDevice.moduleName }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鏄惁鍚敤">
+ <el-tag :type="selectedDevice.enabled ? 'success' : 'info'">
+ {{ selectedDevice.enabled ? '鍚敤' : '鍋滅敤' }}
+ </el-tag>
+ </el-descriptions-item>
+ </el-descriptions>
+ </el-card>
+ </div>
+</template>
+
+<script setup>
+import { computed, ref, watch } from 'vue'
+import { ElMessage } from 'element-plus'
+import {
+ Refresh,
+ Grid,
+ ArrowRight,
+ ArrowDown,
+ Close,
+ Files,
+ Box,
+ Folder
+} from '@element-plus/icons-vue'
+import { deviceGroupApi } from '@/api/device/deviceManagement'
+
+const props = defineProps({
+ group: {
+ type: Object,
+ default: null
+ }
+})
+
+const loading = ref(false)
+const devices = ref([])
+const layoutMode = ref('horizontal') // 'horizontal' | 'vertical'
+const selectedDevice = ref(null)
+
+const fetchDevices = async () => {
+ if (!props.group) {
+ devices.value = []
+ return
+ }
+ const groupId = props.group.id || props.group.groupId
+ if (!groupId) {
+ devices.value = []
+ return
+ }
+ try {
+ loading.value = true
+ const response = await deviceGroupApi.getGroupDevices(groupId)
+ const rawList = response?.data
+ const deviceList = Array.isArray(rawList)
+ ? rawList
+ : Array.isArray(rawList?.records)
+ ? rawList.records
+ : Array.isArray(rawList?.data)
+ ? rawList.data
+ : []
+ // 鎸夋墽琛岄『搴忔帓搴�
+ devices.value = deviceList.sort((a, b) => {
+ const orderA = a.executionOrder || a.order || 0
+ const orderB = b.executionOrder || b.order || 0
+ return orderA - orderB
+ })
+ } catch (error) {
+ ElMessage.error(error?.message || '鍔犺浇璁惧鍒楄〃澶辫触')
+ devices.value = []
+ } finally {
+ loading.value = false
+ }
+}
+
+const handleRefresh = () => {
+ fetchDevices()
+}
+
+const toggleLayout = () => {
+ layoutMode.value = layoutMode.value === 'horizontal' ? 'vertical' : 'horizontal'
+}
+
+const getDeviceTypeClass = (deviceType) => {
+ if (!deviceType) return 'type-unknown'
+ const type = deviceType.toUpperCase()
+ if (type.includes('VEHICLE') || type.includes('澶ц溅')) return 'type-vehicle'
+ if (type.includes('GLASS') || type.includes('澶х悊鐗�')) return 'type-glass'
+ if (type.includes('STORAGE') || type.includes('瀛樺偍')) return 'type-storage'
+ return 'type-unknown'
+}
+
+const getDeviceIcon = (deviceType) => {
+ if (!deviceType) return Box
+ const type = deviceType.toUpperCase()
+ if (type.includes('VEHICLE') || type.includes('澶ц溅')) return Files
+ if (type.includes('GLASS') || type.includes('澶х悊鐗�')) return Box
+ if (type.includes('STORAGE') || type.includes('瀛樺偍')) return Folder
+ return Box
+}
+
+const getDeviceTypeLabel = (deviceType) => {
+ if (!deviceType) return '鏈煡璁惧'
+ const type = deviceType.toUpperCase()
+ if (type.includes('VEHICLE') || type.includes('澶ц溅')) return '涓婂ぇ杞﹁澶�'
+ if (type.includes('GLASS') || type.includes('澶х悊鐗�')) return '澶х悊鐗囪澶�'
+ if (type.includes('STORAGE') || type.includes('瀛樺偍')) return '鐜荤拑瀛樺偍璁惧'
+ return deviceType
+}
+
+const getStatusType = (status) => {
+ if (!status) return 'info'
+ const s = String(status).toUpperCase()
+ if (s === '1' || s === '鍚敤' || s === 'ENABLED' || s === 'ONLINE') return 'success'
+ if (s === '0' || s === '鍋滅敤' || s === 'DISABLED' || s === 'OFFLINE') return 'danger'
+ if (s === '2' || s === '缁存姢' || s === 'MAINTENANCE') return 'warning'
+ return 'info'
+}
+
+const getStatusLabel = (status) => {
+ if (!status) return '鏈煡'
+ const s = String(status).toUpperCase()
+ if (s === '1' || s === '鍚敤' || s === 'ENABLED' || s === 'ONLINE') return '鍦ㄧ嚎'
+ if (s === '0' || s === '鍋滅敤' || s === 'DISABLED' || s === 'OFFLINE') return '绂荤嚎'
+ if (s === '2' || s === '缁存姢' || s === 'MAINTENANCE') return '缁存姢涓�'
+ return String(status)
+}
+
+watch(
+ () => props.group,
+ () => {
+ fetchDevices()
+ selectedDevice.value = null
+ },
+ { immediate: true }
+)
+
+// 鐐瑰嚮鑺傜偣閫夋嫨璁惧
+const handleNodeClick = (device) => {
+ selectedDevice.value = device
+}
+
+defineExpose({
+ fetchDevices
+})
+</script>
+
+<style scoped>
+.group-topology {
+ background: #fff;
+ border-radius: 12px;
+ padding: 20px;
+ box-shadow: 0 8px 32px rgba(15, 18, 63, 0.08);
+}
+
+.panel-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 20px;
+}
+
+.panel-header h3 {
+ margin: 0;
+}
+
+.panel-header p {
+ margin: 4px 0 0;
+ color: #909399;
+ font-size: 13px;
+}
+
+.panel-header .warning {
+ color: #f56c6c;
+}
+
+.action-buttons {
+ display: flex;
+ gap: 12px;
+}
+
+.empty-state {
+ padding: 60px 0;
+}
+
+.topology-container {
+ display: flex;
+ align-items: center;
+ padding: 20px 0;
+ min-height: 200px;
+ overflow-x: auto;
+ /* 骞虫粦婊氬姩 */
+ scroll-behavior: smooth;
+}
+
+.topology-container.layout-horizontal {
+ flex-direction: row;
+ justify-content: flex-start;
+ align-items: center;
+ /* 娣诲姞宸﹀彸鍐呰竟璺濓紝纭繚绗竴涓拰鏈�鍚庝竴涓妭鐐瑰畬鍏ㄥ彲瑙� */
+ padding-left: 20px;
+ padding-right: 20px;
+}
+
+.topology-container.layout-vertical {
+ flex-direction: column;
+ align-items: center;
+}
+
+/* 鑺傜偣鍖呰鍣細姘村钩甯冨眬鏃舵í鍚戞帓鍒楋紝鍨傜洿甯冨眬鏃剁旱鍚戞帓鍒� */
+.topology-node-wrapper {
+ display: flex;
+ align-items: center;
+}
+
+.topology-node-wrapper.layout-horizontal {
+ flex-direction: row;
+ align-items: center;
+}
+
+.topology-node-wrapper.layout-vertical {
+ flex-direction: column;
+ align-items: center;
+}
+
+.topology-node {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ cursor: pointer;
+ transition: transform 0.2s;
+}
+
+.topology-node:hover {
+ transform: translateY(-4px);
+}
+
+.node-content {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 20px;
+ background: #fff;
+ border-radius: 12px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+ min-width: 160px;
+ transition: all 0.3s;
+}
+
+.topology-node:hover .node-content {
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
+}
+
+.node-icon {
+ width: 56px;
+ height: 56px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-bottom: 12px;
+ color: #fff;
+}
+
+.type-vehicle .node-icon {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+}
+
+.type-glass .node-icon {
+ background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
+}
+
+.type-storage .node-icon {
+ background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
+}
+
+.type-unknown .node-icon {
+ background: linear-gradient(135deg, #909399 0%, #b1b3b8 100%);
+}
+
+.node-info {
+ text-align: center;
+ width: 100%;
+}
+
+.node-name {
+ font-size: 16px;
+ font-weight: 600;
+ color: #303133;
+ margin-bottom: 4px;
+ word-break: break-all;
+}
+
+.node-type {
+ font-size: 12px;
+ color: #909399;
+ margin-bottom: 8px;
+}
+
+.node-status {
+ display: flex;
+ justify-content: center;
+}
+
+.node-order {
+ position: absolute;
+ top: -8px;
+ right: -8px;
+ width: 24px;
+ height: 24px;
+ border-radius: 50%;
+ background: #409eff;
+ color: #fff;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 12px;
+ font-weight: 600;
+ box-shadow: 0 2px 8px rgba(64, 158, 255, 0.4);
+}
+
+.node-arrow {
+ color: #c0c4cc;
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+/* 姘村钩甯冨眬锛氱澶村湪鑺傜偣鍙充晶 */
+.topology-node-wrapper.layout-horizontal .node-arrow {
+ margin-left: 20px;
+ margin-right: 20px;
+}
+
+/* 鍨傜洿甯冨眬锛氱澶村湪鑺傜偣涓嬫柟 */
+.topology-node-wrapper.layout-vertical .node-arrow {
+ margin-top: 15px;
+ margin-bottom: 15px;
+}
+
+.device-detail-card {
+ margin-top: 20px;
+}
+
+.device-detail-card .card-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+@media (max-width: 768px) {
+ .panel-header {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 12px;
+ }
+
+ .action-buttons {
+ width: 100%;
+ flex-wrap: wrap;
+ }
+
+ .topology-container.layout-horizontal {
+ flex-direction: column;
+ gap: 30px;
+ }
+
+ .node-arrow {
+ transform: rotate(90deg);
+ }
+}
+</style>
+
diff --git a/mes-web/src/views/plcTest/components/MultiDeviceTest/ExecutionMonitor.vue b/mes-web/src/views/plcTest/components/MultiDeviceTest/ExecutionMonitor.vue
index 0569fd3..1502b50 100644
--- a/mes-web/src/views/plcTest/components/MultiDeviceTest/ExecutionMonitor.vue
+++ b/mes-web/src/views/plcTest/components/MultiDeviceTest/ExecutionMonitor.vue
@@ -4,11 +4,34 @@
<div>
<h3>浠诲姟鎵ц鐩戞帶</h3>
<p>瀹炴椂鏌ョ湅鏈�鏂扮殑澶氳澶囦换鍔�</p>
+ <p v-if="sseConnected" class="sse-status connected">
+ <el-icon><Connection /></el-icon>
+ 瀹炴椂鐩戞帶宸茶繛鎺�
+ </p>
+ <p v-else class="sse-status disconnected">
+ <el-icon><Close /></el-icon>
+ 瀹炴椂鐩戞帶鏈繛鎺�
+ </p>
</div>
- <el-button :loading="loading" @click="fetchTasks">
- <el-icon><Refresh /></el-icon>
- 鍒锋柊
- </el-button>
+ <div class="action-buttons">
+ <el-button :loading="loading" @click="fetchTasks">
+ <el-icon><Refresh /></el-icon>
+ 鍒锋柊
+ </el-button>
+ <el-button
+ v-if="!sseConnected"
+ type="success"
+ @click="connectSSE"
+ :loading="sseConnecting"
+ >
+ <el-icon><VideoPlay /></el-icon>
+ 寮�鍚疄鏃剁洃鎺�
+ </el-button>
+ <el-button v-else type="danger" @click="disconnectSSE">
+ <el-icon><VideoPause /></el-icon>
+ 鍏抽棴瀹炴椂鐩戞帶
+ </el-button>
+ </div>
</div>
<el-table
@@ -17,17 +40,29 @@
height="300"
stripe
@row-click="handleRowClick"
+ row-key="taskId"
>
<el-table-column prop="taskId" label="浠诲姟缂栧彿" min-width="160" />
<el-table-column prop="groupId" label="璁惧缁処D" width="120" />
<el-table-column prop="status" label="鐘舵��" width="120">
<template #default="{ row }">
- <el-tag :type="statusType(row.status)">{{ row.status }}</el-tag>
+ <el-tag :type="statusType(row.status)">
+ {{ getStatusLabel(row.status) }}
+ </el-tag>
</template>
</el-table-column>
- <el-table-column prop="currentStep" label="杩涘害" width="120">
+ <el-table-column prop="currentStep" label="杩涘害" width="140">
<template #default="{ row }">
- {{ row.currentStep || 0 }} / {{ row.totalSteps || 0 }}
+ <div class="progress-cell">
+ <span>{{ row.currentStep || 0 }} / {{ row.totalSteps || 0 }}</span>
+ <el-progress
+ :percentage="getProgressPercentage(row)"
+ :status="getProgressStatus(row.status)"
+ :stroke-width="6"
+ :show-text="false"
+ style="margin-top: 4px;"
+ />
+ </div>
</template>
</el-table-column>
<el-table-column label="寮�濮嬫椂闂�" min-width="160">
@@ -40,20 +75,61 @@
{{ formatDateTime(row.endTime) }}
</template>
</el-table-column>
+ <el-table-column label="鎿嶄綔" width="120" fixed="right">
+ <template #default="{ row }">
+ <el-button
+ link
+ type="primary"
+ size="small"
+ @click.stop="handleRowClick(row)"
+ >
+ 鏌ョ湅璇︽儏
+ </el-button>
+ <el-button
+ v-if="row.status === 'RUNNING'"
+ link
+ type="danger"
+ size="small"
+ @click.stop="handleCancelTask(row)"
+ >
+ 鍙栨秷
+ </el-button>
+ </template>
+ </el-table-column>
</el-table>
- <el-drawer v-model="drawerVisible" size="40%" title="浠诲姟姝ラ璇︽儏">
- <el-timeline v-loading="stepsLoading" :reverse="false">
+ <el-drawer v-model="drawerVisible" size="40%" :title="`浠诲姟姝ラ璇︽儏 - ${currentTaskId || ''}`">
+ <div class="drawer-header" v-if="currentTask">
+ <el-descriptions :column="2" border size="small">
+ <el-descriptions-item label="浠诲姟鐘舵��">
+ <el-tag :type="statusType(currentTask.status)">
+ {{ getStatusLabel(currentTask.status) }}
+ </el-tag>
+ </el-descriptions-item>
+ <el-descriptions-item label="杩涘害">
+ {{ currentTask.currentStep || 0 }} / {{ currentTask.totalSteps || 0 }}
+ </el-descriptions-item>
+ </el-descriptions>
+ </div>
+ <el-timeline v-loading="stepsLoading" :reverse="false" style="margin-top: 20px;">
<el-timeline-item
v-for="step in steps"
:key="step.id"
:timestamp="formatDateTime(step.startTime) || '-'"
- :type="step.status === 'COMPLETED' ? 'success' : step.status === 'FAILED' ? 'danger' : 'primary'"
+ :type="getStepTimelineType(step.status)"
>
<div class="step-title">{{ step.stepName }}</div>
- <div class="step-desc">鐘舵�侊細{{ step.status }}</div>
+ <div class="step-desc">
+ <el-tag :type="getStepStatusType(step.status)" size="small">
+ {{ getStepStatusLabel(step.status) }}
+ </el-tag>
+ </div>
<div class="step-desc">鑰楁椂锛歿{ formatDuration(step.durationMs) }}</div>
- <div class="step-desc" v-if="step.errorMessage">
+ <div class="step-desc" v-if="step.retryCount > 0">
+ 閲嶈瘯娆℃暟锛歿{ step.retryCount }}
+ </div>
+ <div class="step-desc error-message" v-if="step.errorMessage">
+ <el-icon><Warning /></el-icon>
閿欒锛歿{ step.errorMessage }}
</div>
</el-timeline-item>
@@ -63,14 +139,25 @@
</template>
<script setup>
-import { onMounted, ref, watch } from 'vue'
-import { ElMessage } from 'element-plus'
-import { Refresh } from '@element-plus/icons-vue'
+import { onMounted, onUnmounted, ref, watch } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import {
+ Refresh,
+ Connection,
+ Close,
+ VideoPlay,
+ VideoPause,
+ Warning
+} from '@element-plus/icons-vue'
import { multiDeviceTaskApi } from '@/api/device/multiDeviceTask'
const props = defineProps({
groupId: {
type: [String, Number],
+ default: null
+ },
+ taskId: {
+ type: String,
default: null
}
})
@@ -81,6 +168,13 @@
const stepsLoading = ref(false)
const steps = ref([])
const currentTaskId = ref(null)
+const currentTask = ref(null)
+
+// SSE鐩稿叧
+const sseConnected = ref(false)
+const sseConnecting = ref(false)
+let eventSource = null
+const baseURL = import.meta.env.VITE_API_BASE_URL || ''
const fetchTasks = async () => {
try {
@@ -98,8 +192,223 @@
}
}
+// SSE杩炴帴
+const connectSSE = () => {
+ if (eventSource) {
+ disconnectSSE()
+ }
+
+ sseConnecting.value = true
+ try {
+ // 鏋勫缓SSE URL - 鍚庣鍙敮鎸� taskId 鍙傛暟锛屼笉鏀寔 groupId
+ let url = `${baseURL}/api/plcSend/task/notification/sse`
+ // 濡傛灉娌℃湁鎸囧畾 taskId锛屽垯鐩戝惉鎵�鏈変换鍔★紙涓嶄紶鍙傛暟锛�
+ if (props.taskId) {
+ url += `?taskId=${encodeURIComponent(props.taskId)}`
+ }
+ // 娉ㄦ剰锛氬悗绔笉鏀寔 groupId 鍙傛暟锛屽鏋滈渶瑕佺洃鍚煇涓粍鐨勬墍鏈変换鍔★紝
+ // 闇�瑕佸湪鍓嶇鏍规嵁 groupId 鑾峰彇浠诲姟鍒楄〃锛岀劧鍚庝负姣忎釜浠诲姟鍒涘缓杩炴帴
+ // 鎴栬�呬娇鐢ㄤ笉浼犲弬鏁扮殑鏂瑰紡鐩戝惉鎵�鏈変换鍔★紝鐒跺悗鍦ㄥ墠绔繃婊�
+
+ eventSource = new EventSource(url)
+
+ eventSource.onopen = () => {
+ sseConnected.value = true
+ sseConnecting.value = false
+ ElMessage.success('瀹炴椂鐩戞帶宸茶繛鎺�')
+ }
+
+ eventSource.onerror = (error) => {
+ console.error('SSE杩炴帴閿欒:', error)
+ sseConnected.value = false
+ sseConnecting.value = false
+ if (eventSource?.readyState === EventSource.CLOSED) {
+ ElMessage.warning('瀹炴椂鐩戞帶杩炴帴宸叉柇寮�')
+ // 灏濊瘯閲嶈繛
+ setTimeout(() => {
+ if (!sseConnected.value) {
+ connectSSE()
+ }
+ }, 3000)
+ }
+ }
+
+ // 鐩戝惉杩炴帴鎴愬姛浜嬩欢
+ eventSource.addEventListener('connected', (event) => {
+ try {
+ const data = JSON.parse(event.data)
+ console.log('SSE杩炴帴鎴愬姛:', data)
+ } catch (error) {
+ console.error('瑙f瀽杩炴帴娑堟伅澶辫触:', error)
+ }
+ })
+
+ // 鐩戝惉浠诲姟鐘舵�佹洿鏂�
+ eventSource.addEventListener('taskStatus', (event) => {
+ try {
+ const data = JSON.parse(event.data)
+ // 濡傛灉鎸囧畾浜� groupId锛屽彧鏇存柊璇ョ粍鐨勪换鍔�
+ if (!props.groupId || !data.groupId || String(data.groupId) === String(props.groupId)) {
+ updateTaskFromSSE(data)
+ }
+ } catch (error) {
+ console.error('瑙f瀽浠诲姟鐘舵�佸け璐�:', error)
+ }
+ })
+
+ // 鐩戝惉姝ラ鏇存柊
+ eventSource.addEventListener('stepUpdate', (event) => {
+ try {
+ const data = JSON.parse(event.data)
+ // 濡傛灉鏁版嵁涓寘鍚� taskId锛屾鏌ユ槸鍚﹀尮閰嶅綋鍓嶆煡鐪嬬殑浠诲姟
+ if (data.taskId && data.taskId === currentTaskId.value) {
+ updateStepFromSSE(data)
+ } else if (!data.taskId) {
+ // 濡傛灉娌℃湁 taskId锛屽彲鑳芥槸姝ラ鏁版嵁鐩存帴浼犻��
+ updateStepFromSSE(data)
+ }
+ } catch (error) {
+ console.error('瑙f瀽姝ラ鏇存柊澶辫触:', error)
+ }
+ })
+
+ // 鐩戝惉姝ラ鍒楄〃鏇存柊
+ eventSource.addEventListener('stepsUpdate', (event) => {
+ try {
+ const data = JSON.parse(event.data)
+ if (data.taskId === currentTaskId.value && Array.isArray(data.steps)) {
+ steps.value = data.steps
+ }
+ } catch (error) {
+ console.error('瑙f瀽姝ラ鍒楄〃澶辫触:', error)
+ }
+ })
+ } catch (error) {
+ console.error('鍒涘缓SSE杩炴帴澶辫触:', error)
+ ElMessage.error('杩炴帴瀹炴椂鐩戞帶澶辫触: ' + error.message)
+ sseConnecting.value = false
+ }
+}
+
+const disconnectSSE = () => {
+ if (eventSource) {
+ eventSource.close()
+ eventSource = null
+ }
+ sseConnected.value = false
+ sseConnecting.value = false
+}
+
+// 浠嶴SE鏇存柊浠诲姟鐘舵��
+const updateTaskFromSSE = (data) => {
+ if (!data || !data.taskId) return
+
+ // 濡傛灉鎸囧畾浜� groupId锛屽彧澶勭悊璇ョ粍鐨勪换鍔�
+ if (props.groupId && data.groupId && String(data.groupId) !== String(props.groupId)) {
+ return
+ }
+
+ const taskIndex = tasks.value.findIndex(t => t.taskId === data.taskId)
+ if (taskIndex >= 0) {
+ // 鏇存柊浠诲姟 - 淇濈暀鍘熸湁瀛楁锛屽彧鏇存柊SSE浼犳潵鐨勫瓧娈�
+ const existingTask = tasks.value[taskIndex]
+ tasks.value[taskIndex] = {
+ ...existingTask,
+ status: data.status || existingTask.status,
+ currentStep: data.currentStep !== undefined ? data.currentStep : existingTask.currentStep,
+ totalSteps: data.totalSteps !== undefined ? data.totalSteps : existingTask.totalSteps,
+ startTime: data.startTime ? new Date(data.startTime) : existingTask.startTime,
+ endTime: data.endTime ? new Date(data.endTime) : existingTask.endTime,
+ errorMessage: data.errorMessage || existingTask.errorMessage
+ }
+ // 濡傛灉褰撳墠鏌ョ湅鐨勬槸杩欎釜浠诲姟锛屼篃鏇存柊
+ if (currentTaskId.value === data.taskId) {
+ currentTask.value = tasks.value[taskIndex]
+ }
+ } else {
+ // 鏂颁换鍔★紝娣诲姞鍒板垪琛紙闇�瑕佽浆鎹㈡椂闂存埑涓篋ate瀵硅薄锛�
+ const newTask = {
+ ...data,
+ startTime: data.startTime ? new Date(data.startTime) : null,
+ endTime: data.endTime ? new Date(data.endTime) : null
+ }
+ tasks.value.unshift(newTask)
+ }
+}
+
+// 浠嶴SE鏇存柊姝ラ
+const updateStepFromSSE = (data) => {
+ if (!data) return
+
+ // 濡傛灉鏁版嵁涓寘鍚� taskId锛屾鏌ユ槸鍚﹀尮閰嶅綋鍓嶆煡鐪嬬殑浠诲姟
+ if (data.taskId && data.taskId !== currentTaskId.value) {
+ return
+ }
+
+ // 濡傛灉褰撳墠娌℃湁鎵撳紑浠诲姟璇︽儏锛屼笉鏇存柊姝ラ
+ if (!currentTaskId.value) {
+ return
+ }
+
+ // 浣跨敤 id 鎴� stepOrder 鏉ユ煡鎵炬楠�
+ const stepIndex = data.id
+ ? steps.value.findIndex(s => s.id === data.id)
+ : data.stepOrder !== undefined
+ ? steps.value.findIndex(s => s.stepOrder === data.stepOrder)
+ : -1
+
+ if (stepIndex >= 0) {
+ // 鏇存柊姝ラ - 淇濈暀鍘熸湁瀛楁锛屽彧鏇存柊SSE浼犳潵鐨勫瓧娈�
+ const existingStep = steps.value[stepIndex]
+ steps.value[stepIndex] = {
+ ...existingStep,
+ status: data.status || existingStep.status,
+ startTime: data.startTime ? new Date(data.startTime) : existingStep.startTime,
+ endTime: data.endTime ? new Date(data.endTime) : existingStep.endTime,
+ durationMs: data.durationMs !== undefined ? data.durationMs : existingStep.durationMs,
+ retryCount: data.retryCount !== undefined ? data.retryCount : existingStep.retryCount,
+ errorMessage: data.errorMessage || existingStep.errorMessage
+ }
+ } else if (data.stepOrder !== undefined) {
+ // 鏂版楠わ紝娣诲姞鍒板垪琛紙闇�瑕佽浆鎹㈡椂闂存埑涓篋ate瀵硅薄锛�
+ const newStep = {
+ ...data,
+ startTime: data.startTime ? new Date(data.startTime) : null,
+ endTime: data.endTime ? new Date(data.endTime) : null
+ }
+ steps.value.push(newStep)
+ // 鎸� stepOrder 鎺掑簭
+ steps.value.sort((a, b) => (a.stepOrder || 0) - (b.stepOrder || 0))
+ }
+}
+
+const handleCancelTask = async (row) => {
+ try {
+ await ElMessageBox.confirm(
+ `纭畾瑕佸彇娑堜换鍔� ${row.taskId} 鍚楋紵`,
+ '纭鍙栨秷',
+ {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ }
+ )
+ await multiDeviceTaskApi.cancelTask(row.taskId)
+ ElMessage.success('浠诲姟宸插彇娑�')
+ fetchTasks()
+ } catch (error) {
+ if (error !== 'cancel') {
+ ElMessage.error(error?.message || '鍙栨秷浠诲姟澶辫触')
+ }
+ }
+}
+
+const emit = defineEmits(['task-selected'])
+
const handleRowClick = async (row) => {
currentTaskId.value = row.taskId
+ currentTask.value = row
+ emit('task-selected', row)
drawerVisible.value = true
stepsLoading.value = true
try {
@@ -120,9 +429,67 @@
return 'danger'
case 'RUNNING':
return 'warning'
+ case 'PENDING':
+ return 'info'
+ case 'CANCELLED':
+ return 'info'
default:
return 'info'
}
+}
+
+const getStatusLabel = (status) => {
+ const s = (status || '').toUpperCase()
+ const statusMap = {
+ 'COMPLETED': '宸插畬鎴�',
+ 'FAILED': '澶辫触',
+ 'RUNNING': '鎵ц涓�',
+ 'PENDING': '绛夊緟涓�',
+ 'CANCELLED': '宸插彇娑�'
+ }
+ return statusMap[s] || s || '鏈煡'
+}
+
+const getProgressPercentage = (row) => {
+ if (!row.totalSteps || row.totalSteps === 0) return 0
+ return Math.round(((row.currentStep || 0) / row.totalSteps) * 100)
+}
+
+const getProgressStatus = (status) => {
+ const s = (status || '').toUpperCase()
+ if (s === 'COMPLETED') return 'success'
+ if (s === 'FAILED') return 'exception'
+ if (s === 'RUNNING') return 'active'
+ return null
+}
+
+const getStepTimelineType = (status) => {
+ const s = (status || '').toUpperCase()
+ if (s === 'COMPLETED') return 'success'
+ if (s === 'FAILED') return 'danger'
+ if (s === 'RUNNING') return 'primary'
+ return 'info'
+}
+
+const getStepStatusType = (status) => {
+ const s = (status || '').toUpperCase()
+ if (s === 'COMPLETED') return 'success'
+ if (s === 'FAILED') return 'danger'
+ if (s === 'RUNNING') return 'warning'
+ if (s === 'PENDING') return 'info'
+ return 'default'
+}
+
+const getStepStatusLabel = (status) => {
+ const s = (status || '').toUpperCase()
+ const statusMap = {
+ 'COMPLETED': '宸插畬鎴�',
+ 'FAILED': '澶辫触',
+ 'RUNNING': '鎵ц涓�',
+ 'PENDING': '绛夊緟涓�',
+ 'SKIPPED': '宸茶烦杩�'
+ }
+ return statusMap[s] || s || '鏈煡'
}
const formatDuration = (ms) => {
@@ -157,14 +524,45 @@
() => props.groupId,
() => {
fetchTasks()
+ // 濡傛灉SSE宸茶繛鎺ワ紝閲嶆柊杩炴帴锛堝洜涓虹洃鍚墍鏈変换鍔★紝鍓嶇浼氳繃婊わ級
+ if (sseConnected.value) {
+ disconnectSSE()
+ // 寤惰繜閲嶈繛锛岄伩鍏嶉绻佽繛鎺�
+ setTimeout(() => {
+ connectSSE()
+ }, 500)
+ }
},
{ immediate: true }
)
-onMounted(fetchTasks)
+watch(
+ () => props.taskId,
+ () => {
+ // 濡傛灉鎸囧畾浜� taskId锛岄噸鏂拌繛鎺ヤ互鐩戝惉鐗瑰畾浠诲姟
+ if (sseConnected.value) {
+ disconnectSSE()
+ setTimeout(() => {
+ connectSSE()
+ }, 500)
+ }
+ }
+)
+
+onMounted(() => {
+ fetchTasks()
+ // 鑷姩杩炴帴SSE
+ connectSSE()
+})
+
+onUnmounted(() => {
+ disconnectSSE()
+})
defineExpose({
- fetchTasks
+ fetchTasks,
+ connectSSE,
+ disconnectSSE
})
</script>
@@ -193,6 +591,56 @@
font-size: 13px;
}
+.sse-status {
+ margin-top: 4px;
+ font-size: 12px;
+ display: flex;
+ align-items: center;
+ gap: 4px;
+}
+
+.sse-status.connected {
+ color: #67c23a;
+}
+
+.sse-status.disconnected {
+ color: #f56c6c;
+}
+
+.action-buttons {
+ display: flex;
+ gap: 12px;
+}
+
+.progress-cell {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.drawer-header {
+ margin-bottom: 20px;
+}
+
+.step-title {
+ font-weight: 600;
+ margin-bottom: 4px;
+}
+
+.step-desc {
+ font-size: 13px;
+ color: #606266;
+ margin-top: 4px;
+}
+
+.step-desc.error-message {
+ color: #f56c6c;
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ margin-top: 8px;
+}
+
.step-title {
font-weight: 600;
margin-bottom: 4px;
diff --git a/mes-web/src/views/plcTest/components/MultiDeviceTest/ResultAnalysis.vue b/mes-web/src/views/plcTest/components/MultiDeviceTest/ResultAnalysis.vue
new file mode 100644
index 0000000..47588e3
--- /dev/null
+++ b/mes-web/src/views/plcTest/components/MultiDeviceTest/ResultAnalysis.vue
@@ -0,0 +1,638 @@
+<template>
+ <div class="result-analysis">
+ <div class="panel-header">
+ <div>
+ <h3>娴嬭瘯缁撴灉鍒嗘瀽</h3>
+ <p v-if="task">浠诲姟缂栧彿锛歿{ task.taskId }}</p>
+ <p v-else class="warning">璇烽�夋嫨涓�涓换鍔℃煡鐪嬪垎鏋愮粨鏋�</p>
+ </div>
+ <div class="action-buttons">
+ <el-button :loading="loading" @click="handleRefresh">
+ <el-icon><Refresh /></el-icon>
+ 鍒锋柊
+ </el-button>
+ <el-button type="primary" :disabled="!task" @click="handleExport('json')">
+ <el-icon><Download /></el-icon>
+ 瀵煎嚭JSON
+ </el-button>
+ <el-button type="success" :disabled="!task" @click="handleExport('excel')">
+ <el-icon><Document /></el-icon>
+ 瀵煎嚭Excel
+ </el-button>
+ </div>
+ </div>
+
+ <div v-if="!task" class="empty-state">
+ <el-empty description="鏆傛棤浠诲姟鏁版嵁" />
+ </div>
+
+ <div v-else class="analysis-content">
+ <!-- 鎬讳綋缁撴灉 -->
+ <el-card class="overall-result-card" shadow="never">
+ <template #header>
+ <div class="card-header">
+ <span>鎬讳綋缁撴灉</span>
+ <el-tag :type="getOverallStatusType()" size="large">
+ {{ getOverallStatusLabel() }}
+ </el-tag>
+ </div>
+ </template>
+ <div class="result-stats">
+ <div class="stat-item">
+ <div class="stat-label">鎵ц鏃堕棿</div>
+ <div class="stat-value">{{ formatDuration(taskDuration) }}</div>
+ </div>
+ <div class="stat-item">
+ <div class="stat-label">鎬绘楠ゆ暟</div>
+ <div class="stat-value">{{ task.totalSteps || 0 }}</div>
+ </div>
+ <div class="stat-item">
+ <div class="stat-label">瀹屾垚姝ラ</div>
+ <div class="stat-value success">{{ completedSteps }}</div>
+ </div>
+ <div class="stat-item">
+ <div class="stat-label">澶辫触姝ラ</div>
+ <div class="stat-value danger">{{ failedSteps }}</div>
+ </div>
+ <div class="stat-item">
+ <div class="stat-label">鎴愬姛鐜�</div>
+ <div class="stat-value" :class="successRateClass">
+ {{ successRate }}%
+ </div>
+ </div>
+ </div>
+ </el-card>
+
+ <!-- 杩涘害鏉� -->
+ <el-card class="progress-card" shadow="never">
+ <template #header>
+ <span>鎵ц杩涘害</span>
+ </template>
+ <el-progress
+ :percentage="progressPercentage"
+ :status="progressStatus"
+ :stroke-width="20"
+ :format="() => `${completedSteps}/${task.totalSteps || 0}`"
+ />
+ </el-card>
+
+ <!-- 姝ラ璇︽儏 -->
+ <el-card class="steps-card" shadow="never">
+ <template #header>
+ <span>姝ラ鎵ц璇︽儏</span>
+ </template>
+ <el-table
+ v-loading="stepsLoading"
+ :data="steps"
+ stripe
+ style="width: 100%"
+ >
+ <el-table-column type="index" label="搴忓彿" width="60" />
+ <el-table-column prop="stepName" label="姝ラ鍚嶇О" min-width="150" />
+ <el-table-column prop="deviceId" label="璁惧ID" width="120" />
+ <el-table-column prop="status" label="鐘舵��" width="100">
+ <template #default="{ row }">
+ <el-tag :type="getStepStatusType(row.status)">
+ {{ getStepStatusLabel(row.status) }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="鑰楁椂" width="100">
+ <template #default="{ row }">
+ {{ formatDuration(row.durationMs) }}
+ </template>
+ </el-table-column>
+ <el-table-column label="閲嶈瘯娆℃暟" width="100">
+ <template #default="{ row }">
+ {{ row.retryCount || 0 }}
+ </template>
+ </el-table-column>
+ <el-table-column label="寮�濮嬫椂闂�" min-width="160">
+ <template #default="{ row }">
+ {{ formatDateTime(row.startTime) }}
+ </template>
+ </el-table-column>
+ <el-table-column label="缁撴潫鏃堕棿" min-width="160">
+ <template #default="{ row }">
+ {{ formatDateTime(row.endTime) }}
+ </template>
+ </el-table-column>
+ <el-table-column prop="errorMessage" label="閿欒淇℃伅" min-width="200" show-overflow-tooltip />
+ <el-table-column label="鎿嶄綔" width="120" fixed="right">
+ <template #default="{ row }">
+ <el-button
+ link
+ type="primary"
+ size="small"
+ @click="viewStepDetail(row)"
+ >
+ 鏌ョ湅璇︽儏
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-card>
+
+ <!-- 鏁版嵁缁熻鍥捐〃 -->
+ <el-card class="chart-card" shadow="never" v-if="steps.length > 0">
+ <template #header>
+ <span>鎵ц鏃堕棿鍒嗗竷</span>
+ </template>
+ <div class="chart-container">
+ <div class="chart-item" v-for="(step, index) in steps" :key="step.id">
+ <div class="chart-bar">
+ <div
+ class="bar-fill"
+ :class="getStepStatusClass(step.status)"
+ :style="{ width: getBarWidth(step.durationMs) + '%' }"
+ >
+ <span class="bar-label">{{ formatDuration(step.durationMs) }}</span>
+ </div>
+ </div>
+ <div class="chart-label">{{ step.stepName }}</div>
+ </div>
+ </div>
+ </el-card>
+ </div>
+
+ <!-- 姝ラ璇︽儏瀵硅瘽妗� -->
+ <el-dialog
+ v-model="detailDialogVisible"
+ :title="`姝ラ璇︽儏 - ${selectedStep?.stepName || ''}`"
+ width="60%"
+ >
+ <div v-if="selectedStep" class="step-detail-content">
+ <el-descriptions :column="2" border>
+ <el-descriptions-item label="姝ラ鍚嶇О">
+ {{ selectedStep.stepName }}
+ </el-descriptions-item>
+ <el-descriptions-item label="璁惧ID">
+ {{ selectedStep.deviceId }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鐘舵��">
+ <el-tag :type="getStepStatusType(selectedStep.status)">
+ {{ getStepStatusLabel(selectedStep.status) }}
+ </el-tag>
+ </el-descriptions-item>
+ <el-descriptions-item label="閲嶈瘯娆℃暟">
+ {{ selectedStep.retryCount || 0 }}
+ </el-descriptions-item>
+ <el-descriptions-item label="寮�濮嬫椂闂�">
+ {{ formatDateTime(selectedStep.startTime) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="缁撴潫鏃堕棿">
+ {{ formatDateTime(selectedStep.endTime) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鑰楁椂">
+ {{ formatDuration(selectedStep.durationMs) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="閿欒淇℃伅" v-if="selectedStep.errorMessage">
+ {{ selectedStep.errorMessage }}
+ </el-descriptions-item>
+ </el-descriptions>
+
+ <el-divider>杈撳叆鏁版嵁</el-divider>
+ <el-input
+ v-model="selectedStepInputData"
+ type="textarea"
+ :rows="6"
+ readonly
+ />
+
+ <el-divider>杈撳嚭鏁版嵁</el-divider>
+ <el-input
+ v-model="selectedStepOutputData"
+ type="textarea"
+ :rows="6"
+ readonly
+ />
+ </div>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import { computed, ref, watch } from 'vue'
+import { ElMessage } from 'element-plus'
+import { Refresh, Download, Document } from '@element-plus/icons-vue'
+import { multiDeviceTaskApi } from '@/api/device/multiDeviceTask'
+
+const props = defineProps({
+ task: {
+ type: Object,
+ default: null
+ }
+})
+
+const loading = ref(false)
+const stepsLoading = ref(false)
+const steps = ref([])
+const detailDialogVisible = ref(false)
+const selectedStep = ref(null)
+
+const selectedStepInputData = computed(() => {
+ if (!selectedStep.value?.inputData) return ''
+ try {
+ return JSON.stringify(JSON.parse(selectedStep.value.inputData), null, 2)
+ } catch {
+ return selectedStep.value.inputData
+ }
+})
+
+const selectedStepOutputData = computed(() => {
+ if (!selectedStep.value?.outputData) return ''
+ try {
+ return JSON.stringify(JSON.parse(selectedStep.value.outputData), null, 2)
+ } catch {
+ return selectedStep.value.outputData
+ }
+})
+
+const taskDuration = computed(() => {
+ if (!props.task?.startTime || !props.task?.endTime) return 0
+ try {
+ const start = new Date(props.task.startTime)
+ const end = new Date(props.task.endTime)
+ return end.getTime() - start.getTime()
+ } catch {
+ return 0
+ }
+})
+
+const completedSteps = computed(() => {
+ return steps.value.filter(s => s.status === 'COMPLETED').length
+})
+
+const failedSteps = computed(() => {
+ return steps.value.filter(s => s.status === 'FAILED').length
+})
+
+const successRate = computed(() => {
+ if (steps.value.length === 0) return 0
+ return Math.round((completedSteps.value / steps.value.length) * 100)
+})
+
+const successRateClass = computed(() => {
+ if (successRate.value >= 90) return 'success'
+ if (successRate.value >= 70) return 'warning'
+ return 'danger'
+})
+
+const progressPercentage = computed(() => {
+ if (!props.task?.totalSteps || props.task.totalSteps === 0) return 0
+ return Math.round((completedSteps.value / props.task.totalSteps) * 100)
+})
+
+const progressStatus = computed(() => {
+ if (props.task?.status === 'COMPLETED') return 'success'
+ if (props.task?.status === 'FAILED') return 'exception'
+ return 'active'
+})
+
+const fetchSteps = async () => {
+ if (!props.task?.taskId) {
+ steps.value = []
+ return
+ }
+ try {
+ stepsLoading.value = true
+ const { data } = await multiDeviceTaskApi.getTaskSteps(props.task.taskId)
+ steps.value = Array.isArray(data) ? data : (data?.data || [])
+ } catch (error) {
+ ElMessage.error(error?.message || '鍔犺浇姝ラ璇︽儏澶辫触')
+ steps.value = []
+ } finally {
+ stepsLoading.value = false
+ }
+}
+
+const handleRefresh = () => {
+ fetchSteps()
+}
+
+const viewStepDetail = (step) => {
+ selectedStep.value = step
+ detailDialogVisible.value = true
+}
+
+const handleExport = async (format) => {
+ if (!props.task) {
+ ElMessage.warning('璇峰厛閫夋嫨浠诲姟')
+ return
+ }
+ try {
+ loading.value = true
+ // 鏋勫缓瀵煎嚭鏁版嵁
+ const exportData = {
+ task: props.task,
+ steps: steps.value,
+ statistics: {
+ totalSteps: props.task.totalSteps || 0,
+ completedSteps: completedSteps.value,
+ failedSteps: failedSteps.value,
+ successRate: successRate.value,
+ duration: taskDuration.value
+ }
+ }
+
+ if (format === 'json') {
+ const blob = new Blob([JSON.stringify(exportData, null, 2)], {
+ type: 'application/json'
+ })
+ const url = URL.createObjectURL(blob)
+ const a = document.createElement('a')
+ a.href = url
+ a.download = `task_${props.task.taskId}_${Date.now()}.json`
+ a.click()
+ URL.revokeObjectURL(url)
+ ElMessage.success('瀵煎嚭鎴愬姛')
+ } else if (format === 'excel') {
+ // TODO: 瀹炵幇Excel瀵煎嚭
+ ElMessage.info('Excel瀵煎嚭鍔熻兘寮�鍙戜腑')
+ }
+ } catch (error) {
+ ElMessage.error('瀵煎嚭澶辫触: ' + error.message)
+ } finally {
+ loading.value = false
+ }
+}
+
+const getOverallStatusType = () => {
+ const status = props.task?.status?.toUpperCase()
+ if (status === 'COMPLETED') return 'success'
+ if (status === 'FAILED') return 'danger'
+ if (status === 'RUNNING') return 'warning'
+ return 'info'
+}
+
+const getOverallStatusLabel = () => {
+ const status = props.task?.status?.toUpperCase()
+ const statusMap = {
+ 'COMPLETED': '宸插畬鎴�',
+ 'FAILED': '澶辫触',
+ 'RUNNING': '鎵ц涓�',
+ 'PENDING': '绛夊緟涓�',
+ 'CANCELLED': '宸插彇娑�'
+ }
+ return statusMap[status] || status || '鏈煡'
+}
+
+const getStepStatusType = (status) => {
+ const s = (status || '').toUpperCase()
+ if (s === 'COMPLETED') return 'success'
+ if (s === 'FAILED') return 'danger'
+ if (s === 'RUNNING') return 'warning'
+ if (s === 'PENDING') return 'info'
+ return 'default'
+}
+
+const getStepStatusLabel = (status) => {
+ const s = (status || '').toUpperCase()
+ const statusMap = {
+ 'COMPLETED': '宸插畬鎴�',
+ 'FAILED': '澶辫触',
+ 'RUNNING': '鎵ц涓�',
+ 'PENDING': '绛夊緟涓�',
+ 'SKIPPED': '宸茶烦杩�'
+ }
+ return statusMap[s] || s || '鏈煡'
+}
+
+const getStepStatusClass = (status) => {
+ const s = (status || '').toUpperCase()
+ if (s === 'COMPLETED') return 'status-success'
+ if (s === 'FAILED') return 'status-danger'
+ if (s === 'RUNNING') return 'status-warning'
+ return 'status-info'
+}
+
+const getBarWidth = (durationMs) => {
+ if (!durationMs || steps.value.length === 0) return 0
+ const maxDuration = Math.max(...steps.value.map(s => s.durationMs || 0))
+ if (maxDuration === 0) return 0
+ return Math.min((durationMs / maxDuration) * 100, 100)
+}
+
+const formatDuration = (ms) => {
+ if (!ms) return '-'
+ if (ms < 1000) return `${ms} ms`
+ if (ms < 60000) return `${(ms / 1000).toFixed(1)} s`
+ const minutes = Math.floor(ms / 60000)
+ const seconds = ((ms % 60000) / 1000).toFixed(0)
+ return `${minutes}鍒�${seconds}绉抈
+}
+
+const formatDateTime = (dateTime) => {
+ if (!dateTime) return '-'
+ try {
+ const date = new Date(dateTime)
+ if (isNaN(date.getTime())) return dateTime
+ const year = date.getFullYear()
+ const month = String(date.getMonth() + 1).padStart(2, '0')
+ const day = String(date.getDate()).padStart(2, '0')
+ const hours = String(date.getHours()).padStart(2, '0')
+ const minutes = String(date.getMinutes()).padStart(2, '0')
+ const seconds = String(date.getSeconds()).padStart(2, '0')
+ return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
+ } catch {
+ return dateTime
+ }
+}
+
+watch(
+ () => props.task,
+ () => {
+ fetchSteps()
+ },
+ { immediate: true }
+)
+
+defineExpose({
+ fetchSteps
+})
+</script>
+
+<style scoped>
+.result-analysis {
+ background: #fff;
+ border-radius: 12px;
+ padding: 20px;
+ box-shadow: 0 8px 32px rgba(15, 18, 63, 0.08);
+}
+
+.panel-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 20px;
+}
+
+.panel-header h3 {
+ margin: 0;
+}
+
+.panel-header p {
+ margin: 4px 0 0;
+ color: #909399;
+ font-size: 13px;
+}
+
+.panel-header .warning {
+ color: #f56c6c;
+}
+
+.action-buttons {
+ display: flex;
+ gap: 12px;
+}
+
+.empty-state {
+ padding: 60px 0;
+}
+
+.analysis-content {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+}
+
+.overall-result-card .card-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.result-stats {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
+ gap: 20px;
+}
+
+.stat-item {
+ text-align: center;
+}
+
+.stat-label {
+ font-size: 13px;
+ color: #909399;
+ margin-bottom: 8px;
+}
+
+.stat-value {
+ font-size: 24px;
+ font-weight: 600;
+ color: #303133;
+}
+
+.stat-value.success {
+ color: #67c23a;
+}
+
+.stat-value.danger {
+ color: #f56c6c;
+}
+
+.stat-value.warning {
+ color: #e6a23c;
+}
+
+.progress-card {
+ margin-top: 0;
+}
+
+.steps-card {
+ margin-top: 0;
+}
+
+.chart-card {
+ margin-top: 0;
+}
+
+.chart-container {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ padding: 20px 0;
+}
+
+.chart-item {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.chart-bar {
+ flex: 1;
+ height: 32px;
+ background: #f0f2f5;
+ border-radius: 4px;
+ position: relative;
+ overflow: hidden;
+}
+
+.bar-fill {
+ height: 100%;
+ border-radius: 4px;
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ padding: 0 8px;
+ transition: width 0.3s;
+ min-width: 60px;
+}
+
+.bar-fill.status-success {
+ background: linear-gradient(90deg, #67c23a 0%, #85ce61 100%);
+}
+
+.bar-fill.status-danger {
+ background: linear-gradient(90deg, #f56c6c 0%, #f78989 100%);
+}
+
+.bar-fill.status-warning {
+ background: linear-gradient(90deg, #e6a23c 0%, #ebb563 100%);
+}
+
+.bar-fill.status-info {
+ background: linear-gradient(90deg, #909399 0%, #b1b3b8 100%);
+}
+
+.bar-label {
+ color: #fff;
+ font-size: 12px;
+ font-weight: 500;
+ white-space: nowrap;
+}
+
+.chart-label {
+ width: 150px;
+ font-size: 13px;
+ color: #606266;
+ text-align: right;
+ flex-shrink: 0;
+}
+
+.step-detail-content {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+}
+
+@media (max-width: 768px) {
+ .panel-header {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 12px;
+ }
+
+ .action-buttons {
+ width: 100%;
+ flex-wrap: wrap;
+ }
+
+ .result-stats {
+ grid-template-columns: repeat(2, 1fr);
+ }
+}
+</style>
+
diff --git a/mes-web/src/views/plcTest/components/MultiDeviceTest/TaskOrchestration.vue b/mes-web/src/views/plcTest/components/MultiDeviceTest/TaskOrchestration.vue
index f3490b4..8cf1feb 100644
--- a/mes-web/src/views/plcTest/components/MultiDeviceTest/TaskOrchestration.vue
+++ b/mes-web/src/views/plcTest/components/MultiDeviceTest/TaskOrchestration.vue
@@ -5,7 +5,7 @@
<h3>澶氳澶囨祴璇曠紪鎺�</h3>
<p v-if="group">褰撳墠璁惧缁勶細{{ group.groupName }}锛坽{ group.deviceCount || '-' }} 鍙拌澶囷級</p>
<p v-else class="warning">璇峰厛鍦ㄥ乏渚ч�夋嫨涓�涓澶囩粍</p>
- <p v-if="group && loadDeviceName" class="sub-info">涓婂ぇ杞﹁澶囷細{{ loadDeviceName }}</p>
+ <p v-if="group && loadDeviceName" class="sub-info">褰撳墠璁惧锛歿{ loadDeviceName }}</p>
</div>
<div class="action-buttons">
<el-button
@@ -25,23 +25,94 @@
</div>
</div>
- <el-form :model="form" label-width="120px">
- <el-form-item label="鐜荤拑ID鍒楄〃">
+ <el-form :model="form" label-width="120px" :rules="rules" ref="formRef">
+ <el-form-item label="鐜荤拑ID鍒楄〃" prop="glassIds" required>
<el-input
v-model="glassIdsInput"
type="textarea"
:rows="4"
- placeholder="璇疯緭鍏ョ幓鐠冩潯鐮侊紝鏀寔澶氳鎴栭�楀彿鍒嗛殧"
+ placeholder="璇疯緭鍏ョ幓鐠冩潯鐮侊紝鏀寔澶氳鎴栭�楀彿鍒嗛殧锛屾瘡琛屼竴涓垨閫楀彿鍒嗛殧"
+ show-word-limit
+ :maxlength="5000"
/>
+ <div class="form-tip">
+ 宸茶緭鍏� {{ glassIds.length }} 涓幓鐠僆D
+ </div>
</el-form-item>
+
+ <el-divider content-position="left">璁惧鐗瑰畾閰嶇疆</el-divider>
+
<el-form-item label="浣嶇疆缂栫爜">
- <el-input v-model="form.positionCode" placeholder="渚嬪锛歅OS1" />
+ <el-input
+ v-model="form.positionCode"
+ placeholder="渚嬪锛歅OS1"
+ clearable
+ />
+ <div class="form-tip">涓婂ぇ杞﹁澶囩殑浣嶇疆缂栫爜</div>
</el-form-item>
+
+ <el-form-item label="浣嶇疆鍊�">
+ <el-input-number
+ v-model="form.positionValue"
+ :min="0"
+ :max="9999"
+ placeholder="浣嶇疆鏁板��"
+ />
+ <div class="form-tip">涓婂ぇ杞﹁澶囩殑浣嶇疆鏁板��</div>
+ </el-form-item>
+
<el-form-item label="瀛樺偍浣嶇疆">
- <el-input-number v-model="form.storagePosition" :min="1" :max="200" />
+ <el-input-number
+ v-model="form.storagePosition"
+ :min="1"
+ :max="200"
+ placeholder="瀛樺偍浣嶇疆缂栧彿"
+ />
+ <div class="form-tip">鐜荤拑瀛樺偍璁惧鐨勫瓨鍌ㄤ綅缃�</div>
</el-form-item>
+
+ <el-form-item label="澶勭悊绫诲瀷">
+ <el-select v-model="form.processType" placeholder="閫夋嫨澶勭悊绫诲瀷" clearable>
+ <el-option label="鏍囧噯澶勭悊" :value="1" />
+ <el-option label="蹇�熷鐞�" :value="2" />
+ <el-option label="鎱㈤�熷鐞�" :value="3" />
+ </el-select>
+ <div class="form-tip">澶х悊鐗囪澶囩殑澶勭悊绫诲瀷</div>
+ </el-form-item>
+
+ <el-divider content-position="left">鎵ц閰嶇疆</el-divider>
+
<el-form-item label="鎵ц闂撮殧 (ms)">
- <el-input-number v-model="form.executionInterval" :min="100" :max="10000" />
+ <el-input-number
+ v-model="form.executionInterval"
+ :min="100"
+ :max="10000"
+ :step="100"
+ placeholder="璁惧鎿嶄綔闂撮殧鏃堕棿"
+ />
+ <div class="form-tip">姣忎釜璁惧鎿嶄綔涔嬮棿鐨勯棿闅旀椂闂达紙姣锛�</div>
+ </el-form-item>
+
+ <el-form-item label="瓒呮椂鏃堕棿 (鍒嗛挓)">
+ <el-input-number
+ v-model="form.timeoutMinutes"
+ :min="1"
+ :max="60"
+ :step="1"
+ placeholder="浠诲姟瓒呮椂鏃堕棿"
+ />
+ <div class="form-tip">浠诲姟鎵ц鐨勬渶澶ц秴鏃舵椂闂�</div>
+ </el-form-item>
+
+ <el-form-item label="閲嶈瘯娆℃暟">
+ <el-input-number
+ v-model="form.retryCount"
+ :min="0"
+ :max="10"
+ :step="1"
+ placeholder="澶辫触閲嶈瘯娆℃暟"
+ />
+ <div class="form-tip">璁惧鎿嶄綔澶辫触鏃剁殑鏈�澶ч噸璇曟鏁�</div>
</el-form-item>
</el-form>
</div>
@@ -65,9 +136,41 @@
const form = reactive({
positionCode: '',
+ positionValue: null,
storagePosition: null,
- executionInterval: 1000
+ processType: null,
+ executionInterval: 1000,
+ timeoutMinutes: 30,
+ retryCount: 3
})
+
+const formRef = ref(null)
+
+const rules = {
+ glassIds: [
+ {
+ validator: (rule, value, callback) => {
+ if (glassIds.value.length === 0) {
+ callback(new Error('璇疯嚦灏戣緭鍏ヤ竴涓幓鐠僆D'))
+ } else if (glassIds.value.length > 100) {
+ callback(new Error('鐜荤拑ID鏁伴噺涓嶈兘瓒呰繃100涓�'))
+ } else {
+ // 楠岃瘉鐜荤拑ID鏍煎紡
+ const invalidIds = glassIds.value.filter(id => {
+ // 绠�鍗曠殑鏍煎紡楠岃瘉锛氫笉鑳戒负绌猴紝闀垮害鍦�1-50涔嬮棿
+ return !id || id.length === 0 || id.length > 50
+ })
+ if (invalidIds.length > 0) {
+ callback(new Error(`瀛樺湪鏃犳晥鐨勭幓鐠僆D鏍煎紡锛岃妫�鏌))
+ } else {
+ callback()
+ }
+ }
+ },
+ trigger: 'blur'
+ }
+ ]
+}
const glassIdsInput = ref('')
const loading = ref(false)
@@ -133,23 +236,78 @@
ElMessage.warning('璇峰厛閫夋嫨璁惧缁�')
return
}
+
+ // 琛ㄥ崟楠岃瘉
+ if (!formRef.value) return
+ try {
+ await formRef.value.validate()
+ } catch (error) {
+ ElMessage.warning('璇锋鏌ヨ〃鍗曡緭鍏�')
+ return
+ }
+
if (glassIds.value.length === 0) {
ElMessage.warning('璇疯嚦灏戣緭鍏ヤ竴涓幓鐠僆D')
return
}
+
try {
loading.value = true
- await multiDeviceTaskApi.startTask({
+
+ // 鏋勫缓浠诲姟鍙傛暟
+ const parameters = {
+ glassIds: glassIds.value,
+ executionInterval: form.executionInterval || 1000
+ }
+
+ // 娣诲姞鍙�夊弬鏁�
+ if (form.positionCode) {
+ parameters.positionCode = form.positionCode
+ }
+ if (form.positionValue !== null) {
+ parameters.positionValue = form.positionValue
+ }
+ if (form.storagePosition !== null) {
+ parameters.storagePosition = form.storagePosition
+ }
+ if (form.processType !== null) {
+ parameters.processType = form.processType
+ }
+ if (form.timeoutMinutes) {
+ parameters.timeoutMinutes = form.timeoutMinutes
+ }
+ if (form.retryCount !== null) {
+ parameters.retryCount = form.retryCount
+ }
+
+ // 寮傛鍚姩浠诲姟锛岀珛鍗宠繑鍥烇紝涓嶉樆濉�
+ const response = await multiDeviceTaskApi.startTask({
groupId: props.group.id || props.group.groupId,
- parameters: {
- glassIds: glassIds.value,
- positionCode: form.positionCode || null,
- storagePosition: form.storagePosition,
- executionInterval: form.executionInterval
- }
+ parameters
})
- ElMessage.success('浠诲姟宸插惎鍔�')
- emit('task-started')
+
+ const task = response?.data
+ if (task && task.taskId) {
+ ElMessage.success(`浠诲姟宸插惎鍔紙寮傛鎵ц锛�: ${task.taskId}`)
+ emit('task-started', task)
+
+ // 绔嬪嵆鍒锋柊鐩戞帶鍒楄〃锛屾樉绀烘柊鍚姩鐨勪换鍔�
+ setTimeout(() => {
+ emit('task-started')
+ }, 500)
+
+ // 閲嶇疆琛ㄥ崟锛堜繚鐣欓儴鍒嗛厤缃級锛屾柟渚跨户缁惎鍔ㄥ叾浠栬澶囩粍
+ glassIdsInput.value = ''
+ form.positionCode = ''
+ form.positionValue = null
+ form.storagePosition = null
+ form.processType = null
+
+ // 鎻愮ず鐢ㄦ埛鍙互缁х画鍚姩鍏朵粬璁惧缁�
+ ElMessage.info('鍙互缁х画閫夋嫨鍏朵粬璁惧缁勫惎鍔ㄦ祴璇曪紝澶氫釜璁惧缁勫皢骞惰鎵ц')
+ } else {
+ ElMessage.warning('浠诲姟鍚姩鍝嶅簲寮傚父')
+ }
} catch (error) {
ElMessage.error(error?.message || '浠诲姟鍚姩澶辫触')
} finally {
@@ -234,5 +392,12 @@
gap: 12px;
align-items: center;
}
+
+.form-tip {
+ font-size: 12px;
+ color: #909399;
+ margin-top: 4px;
+ line-height: 1.4;
+}
</style>
--
Gitblit v1.8.0