diff --git a/cmvr-iot-admin/src/main/resources/application-dev.yml b/cmvr-iot-admin/src/main/resources/application-dev.yml index a48f8d4..83386b1 100644 --- a/cmvr-iot-admin/src/main/resources/application-dev.yml +++ b/cmvr-iot-admin/src/main/resources/application-dev.yml @@ -132,8 +132,3 @@ flowise: abort: http://192.168.0.108:3000/api/v1/chatmessage/abort/f99329e9-b33d-437d-90ed-69eaa8a05418/ query: http://192.168.0.108:3000/api/v1/executions/ api-key: pg61JW6W_GXyqmqSoURJ4mlSRrCMRzGLBJli_w3BFkg - -# TTS外部接口配置 -tts: - external-api: - url: http://192.168.0.102:9003/generate_advanced_audio diff --git a/cmvr-iot-admin/src/main/resources/application.yml b/cmvr-iot-admin/src/main/resources/application.yml index 431a92d..0dd4850 100644 --- a/cmvr-iot-admin/src/main/resources/application.yml +++ b/cmvr-iot-admin/src/main/resources/application.yml @@ -124,6 +124,11 @@ xss: # Integrated QUIC gateway and in-process node synchronization. cmvr: + inspection: + robot-state: + enabled: true + initial-delay-ms: 3000 + refresh-ms: 1000 quic: enabled: true bind-host: 0.0.0.0 diff --git a/cmvr-iot-inspection/src/main/java/com/cmvr/inspection/domain/InspectionRobot.java b/cmvr-iot-inspection/src/main/java/com/cmvr/inspection/domain/InspectionRobot.java index 29bbb18..30b95a2 100644 --- a/cmvr-iot-inspection/src/main/java/com/cmvr/inspection/domain/InspectionRobot.java +++ b/cmvr-iot-inspection/src/main/java/com/cmvr/inspection/domain/InspectionRobot.java @@ -38,9 +38,9 @@ public class InspectionRobot extends BaseEntity @ApiModelProperty("机器人永久唯一ID") private String robotId; - /** 机器人编码 */ - @Excel(name = "机器人编码") - @ApiModelProperty("机器人编码") + /** Business display code. It is not an edge device ID. */ + @Excel(name = "机器人业务编号") + @ApiModelProperty("机器人业务编号,仅用于业务展示,不作为设备ID") private String robotCode; /** 机器人名称 */ @@ -75,7 +75,7 @@ public class InspectionRobot extends BaseEntity /** 当前状态 */ @Excel(name = "当前状态") - @ApiModelProperty("当前状态(0在线 1离线 2充电中 3巡检中)") + @ApiModelProperty("AGV运行模式(0未知 1断开 2空闲 3手动 4自动 5充电 6暂停 7停止 8故障 9急停)") private String status; /** 电量百分比 */ diff --git a/cmvr-iot-inspection/src/main/java/com/cmvr/inspection/domain/vo/InspectionRobotVo.java b/cmvr-iot-inspection/src/main/java/com/cmvr/inspection/domain/vo/InspectionRobotVo.java index 6af1ec4..d1d42ed 100644 --- a/cmvr-iot-inspection/src/main/java/com/cmvr/inspection/domain/vo/InspectionRobotVo.java +++ b/cmvr-iot-inspection/src/main/java/com/cmvr/inspection/domain/vo/InspectionRobotVo.java @@ -34,9 +34,9 @@ public class InspectionRobotVo extends BaseEntity @ApiModelProperty("机器人永久唯一ID") private String robotId; - /** 机器人编码 */ - @Excel(name = "机器人编码") - @ApiModelProperty("机器人编码") + /** Business display code. It is not an edge device ID. */ + @Excel(name = "机器人业务编号") + @ApiModelProperty("机器人业务编号,仅用于业务展示,不作为设备ID") private String robotCode; /** 机器人名称 */ @@ -81,7 +81,7 @@ public class InspectionRobotVo extends BaseEntity /** 当前状态 */ @Excel(name = "当前状态") - @ApiModelProperty("当前状态(0在线 1离线 2充电中 3巡检中)") + @ApiModelProperty("AGV运行模式(0未知 1断开 2空闲 3手动 4自动 5充电 6暂停 7停止 8故障 9急停)") private String status; /** 电量百分比 */ diff --git a/cmvr-iot-inspection/src/main/java/com/cmvr/inspection/mapper/InspectionRobotMapper.java b/cmvr-iot-inspection/src/main/java/com/cmvr/inspection/mapper/InspectionRobotMapper.java index f284fd3..a17545b 100644 --- a/cmvr-iot-inspection/src/main/java/com/cmvr/inspection/mapper/InspectionRobotMapper.java +++ b/cmvr-iot-inspection/src/main/java/com/cmvr/inspection/mapper/InspectionRobotMapper.java @@ -27,6 +27,16 @@ public interface InspectionRobotMapper extends MPJBaseMapper int updateQuicRuntimeState(InspectionRobot robot); + /** + * Update the AGV runtime snapshot without changing business audit fields. + */ + int updateRuntimeStatus(InspectionRobot robot); + + /** + * Select connected robots that currently report one online AGV device. + */ + List selectRuntimeSyncCandidates(); + int archiveInspectionRobotByIds(@Param("ids") String[] ids, @Param("updateBy") String updateBy); /** diff --git a/cmvr-iot-inspection/src/main/java/com/cmvr/inspection/service/RobotQuicStateService.java b/cmvr-iot-inspection/src/main/java/com/cmvr/inspection/service/RobotQuicStateService.java index 11c15a9..1276543 100644 --- a/cmvr-iot-inspection/src/main/java/com/cmvr/inspection/service/RobotQuicStateService.java +++ b/cmvr-iot-inspection/src/main/java/com/cmvr/inspection/service/RobotQuicStateService.java @@ -28,6 +28,7 @@ import java.util.Objects; public class RobotQuicStateService { private static final Logger log = LoggerFactory.getLogger(RobotQuicStateService.class); + private static final int AGV_DEVICE_KIND = 1; private final InspectionRobotMapper robotMapper; private final InspectionRobotDeviceMapper deviceMapper; @@ -90,6 +91,9 @@ public class RobotQuicStateService { robot.setSoftwareVersion(node.getSoftwareVersion()); robot.setLastHeartbeatTime(eventTime); robot.setUpdateTime(DateUtils.getNowDate()); + if (offline || !hasEnabledAgv(node)) { + robot.setStatus("1"); + } robotMapper.updateQuicRuntimeState(robot); if (offline) { @@ -162,6 +166,11 @@ public class RobotQuicStateService { return device; } + private boolean hasEnabledAgv(NodeSnapshot node) { + return node.getDeviceManager().getDevicesList().stream() + .anyMatch(device -> device.getKind() == AGV_DEVICE_KIND && device.getEnabled()); + } + private String resolveGrpcHost(NodeSnapshot node) { String advertised = StringUtils.trimToNull(node.getGrpcEndpoint().getHost()); String observed = StringUtils.trimToNull(node.getObservedSourceIp()); diff --git a/cmvr-iot-inspection/src/main/java/com/cmvr/inspection/service/impl/InspectionRobotServiceImpl.java b/cmvr-iot-inspection/src/main/java/com/cmvr/inspection/service/impl/InspectionRobotServiceImpl.java index 9830ea1..b557191 100644 --- a/cmvr-iot-inspection/src/main/java/com/cmvr/inspection/service/impl/InspectionRobotServiceImpl.java +++ b/cmvr-iot-inspection/src/main/java/com/cmvr/inspection/service/impl/InspectionRobotServiceImpl.java @@ -2,9 +2,11 @@ package com.cmvr.inspection.service.impl; import java.util.List; +import java.util.Locale; import java.util.UUID; import cmvr.msgs.AgvUtils; +import cmvr.quic_edge.v1.QuicEdge; import com.baomidou.mybatisplus.spring.service.impl.ServiceImpl; import com.cmvr.common.utils.SecurityUtils; import com.cmvr.edge.client.model.EdgeCommonVO; @@ -167,6 +169,50 @@ public class InspectionRobotServiceImpl extends ServiceImpl agvDevices = inspectionRobotDeviceMapper.selectByRobotIdAndKind( + robot.getRobotId(), QuicEdge.DeviceKind.DEVICE_KIND_AGV_VALUE, true); + if (agvDevices.isEmpty()) { + List onlineDevices = inspectionRobotDeviceMapper.selectByRobotIdAndKind( + robot.getRobotId(), null, true); + if (onlineDevices.isEmpty()) { + throw new GlobalException("机器人当前没有在线设备:" + robot.getRobotId()); + } + String deviceSummary = onlineDevices.stream() + .map(device -> device.getDeviceId() + "(" + deviceKindName(device.getDeviceKind()) + ")") + .collect(java.util.stream.Collectors.joining("、")); + throw new GlobalException("机器人在线设备中没有AGV,当前设备:" + deviceSummary); + } + if (agvDevices.size() > 1) { + String deviceIds = agvDevices.stream() + .map(InspectionRobotDevice::getDeviceId) + .collect(java.util.stream.Collectors.joining("、")); + throw new GlobalException("机器人存在多个在线AGV设备,无法确定默认设备:" + deviceIds); + } + + EdgeCommonVO request = new EdgeCommonVO(); + request.setRobotId(robot.getRobotId()); + request.setDeviceId(agvDevices.get(0).getDeviceId()); + return request; + } + + private String deviceKindName(Integer deviceKind) + { + if (deviceKind == null) { + return "未知类型"; + } + QuicEdge.DeviceKind kind = QuicEdge.DeviceKind.forNumber(deviceKind); + return kind == null ? "未知类型" + deviceKind : kind.name().replace("DEVICE_KIND_", ""); + } + /** * 获取机器人地图列表(从AGV获取) * @@ -180,15 +226,7 @@ public class InspectionRobotServiceImpl extends ServiceImpl syncingRobotIds = ConcurrentHashMap.newKeySet(); + + public RobotRuntimeStateRefreshTask(InspectionRobotMapper inspectionRobotMapper, + IInspectionRobotService inspectionRobotService, + @Qualifier("threadPoolTaskExecutor") TaskExecutor taskExecutor) + { + this.inspectionRobotMapper = inspectionRobotMapper; + this.inspectionRobotService = inspectionRobotService; + this.taskExecutor = taskExecutor; + } + + @Scheduled(initialDelayString = "${cmvr.inspection.robot-state.initial-delay-ms:3000}", + fixedDelayString = "${cmvr.inspection.robot-state.refresh-ms:5000}") + public void refreshConnectedRobots() + { + for (InspectionRobot robot : inspectionRobotMapper.selectRuntimeSyncCandidates()) { + if (robot.getId() == null || !syncingRobotIds.add(robot.getId())) { + continue; + } + taskExecutor.execute(() -> refreshRobot(robot)); + } + } + + private void refreshRobot(InspectionRobot robot) + { + try { + inspectionRobotService.syncRobotStatus(robot.getId()); + } catch (Exception e) { + log.debug("Failed to refresh AGV runtime state, robotId={}, reason={}", + robot.getRobotId(), e.getMessage()); + } finally { + syncingRobotIds.remove(robot.getId()); + } + } +} diff --git a/cmvr-iot-inspection/src/main/resources/mapper/inspection/InspectionRobotMapper.xml b/cmvr-iot-inspection/src/main/resources/mapper/inspection/InspectionRobotMapper.xml index 904f637..d7d27fb 100644 --- a/cmvr-iot-inspection/src/main/resources/mapper/inspection/InspectionRobotMapper.xml +++ b/cmvr-iot-inspection/src/main/resources/mapper/inspection/InspectionRobotMapper.xml @@ -169,6 +169,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" update inspection_robot set ip_address = #{ipAddress}, port = #{port}, connect_status = #{connectStatus}, + status = coalesce(#{status}, status), quic_node_id = #{quicNodeId}, quic_session_id = #{quicSessionId}, quic_boot_id = #{quicBootId}, observed_ip = #{observedIp}, software_version = #{softwareVersion}, last_heartbeat_time = #{lastHeartbeatTime}, @@ -176,6 +177,29 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" where id = #{id} + + update inspection_robot + set status = coalesce(#{status}, status), + battery_level = coalesce(#{batteryLevel}, battery_level), + current_position = coalesce(#{currentPosition}, current_position) + where id = #{id} + + + + update inspection_robot set archived_status = '1', connect_status = '0', update_time = sysdate(), update_by = #{updateBy} diff --git a/cmvr-iot-test/src/main/java/com/cmvr/test/enums/ActionEnum.java b/cmvr-iot-test/src/main/java/com/cmvr/test/enums/ActionEnum.java index ff98483..0cebfdf 100644 --- a/cmvr-iot-test/src/main/java/com/cmvr/test/enums/ActionEnum.java +++ b/cmvr-iot-test/src/main/java/com/cmvr/test/enums/ActionEnum.java @@ -11,6 +11,8 @@ import java.util.Map; @Getter @AllArgsConstructor public enum ActionEnum { + SPEAKER_PLAYAUDIO("EDGE", "SPEAKER_PLAYAUDIO", "Play audio"), + // 平台控制类 STOPPED("NONE", "STOP", "强制终止"), PAUSED("NONE", "PAUSED", "暂停"), diff --git a/cmvr-iot-test/src/main/java/com/cmvr/test/flow/runtime/operator/edge/EdgeSpeakerOperateService.java b/cmvr-iot-test/src/main/java/com/cmvr/test/flow/runtime/operator/edge/EdgeSpeakerOperateService.java index 9017130..7b98fa7 100644 --- a/cmvr-iot-test/src/main/java/com/cmvr/test/flow/runtime/operator/edge/EdgeSpeakerOperateService.java +++ b/cmvr-iot-test/src/main/java/com/cmvr/test/flow/runtime/operator/edge/EdgeSpeakerOperateService.java @@ -1,5 +1,6 @@ package com.cmvr.test.flow.runtime.operator.edge; +import cn.hutool.core.util.StrUtil; import com.alibaba.fastjson2.JSONObject; import com.cmvr.common.exception.GlobalException; import com.cmvr.edge.client.service.EdgeSpeakerService; @@ -24,6 +25,17 @@ public class EdgeSpeakerOperateService implements EdgeOperateService { @Override public TaskNodeExecuteResult execute(TaskNodeExecuteMessage message) { + JSONObject inputParams = message.getInputParams(); + String deviceId = inputParams.getString("deviceId"); + String audioPath = inputParams.getString("audioPath"); + String robotId = message.getRobotId(); + if (StrUtil.hasBlank(robotId, deviceId, audioPath)) { + throw new GlobalException("robotId, deviceId and audioPath are required"); + } + String result = edgeSpeakerService.playAudio(robotId, deviceId, audioPath); + if (result != null) { + return TaskNodeExecuteResult.success(JSONObject.of("result", result)); + } // ActionEnum action = message.getAction(); // JSONObject inputParams = message.getInputParams(); // String deviceId = inputParams.getString("deviceId"); @@ -64,6 +76,6 @@ public class EdgeSpeakerOperateService implements EdgeOperateService { // throw new GlobalException("不支持的扬声器操作类型: " + action); // } // return TaskNodeExecuteResult.success(); - return null; + return TaskNodeExecuteResult.success(); } } diff --git a/cmvr-iot-test/src/main/java/com/cmvr/test/service/FlowActionExecutorService.java b/cmvr-iot-test/src/main/java/com/cmvr/test/service/FlowActionExecutorService.java index 0d8254d..b846cc2 100644 --- a/cmvr-iot-test/src/main/java/com/cmvr/test/service/FlowActionExecutorService.java +++ b/cmvr-iot-test/src/main/java/com/cmvr/test/service/FlowActionExecutorService.java @@ -17,16 +17,25 @@ import com.cmvr.edge.client.service.EdgeAgvService; import com.cmvr.device.service.InspectionAlertListenService; import com.cmvr.llm.service.LLMAiAgentPlatformService; import com.cmvr.test.enums.ActionEnum; +import com.cmvr.test.flow.runtime.dispatcher.FlowCodeNodeHandler; +import com.cmvr.test.flow.runtime.dispatcher.FlowHttpNodeHandler; +import com.cmvr.test.flow.runtime.dispatcher.FlowSleepNodeHandler; import com.cmvr.test.flow.runtime.message.TaskNodeExecuteMessage; +import com.cmvr.test.flow.runtime.message.TaskNodeExecuteResult; +import com.cmvr.test.flow.runtime.operator.ae.AEOperateService; import com.cmvr.test.flow.runtime.operator.edge.EdgeManualInspectionOperateService; import com.cmvr.test.flow.runtime.operator.edge.EdgeDeviceCommandOperateService; import com.cmvr.test.flow.runtime.operator.edge.EdgeArmOperateService; +import com.cmvr.test.flow.runtime.operator.edge.EdgeOperateService; import com.cmvr.test.flow.runtime.operator.llm.InspectionMeterRecognizeOperateService; +import com.cmvr.test.flow.runtime.operator.llm.LLMOperateService; import com.cmvr.test.model.vo.FlowActionRequestVO; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import java.util.List; + @Slf4j @Service @RequiredArgsConstructor @@ -43,6 +52,12 @@ public class FlowActionExecutorService { private final EdgeManualInspectionOperateService edgeManualInspectionOperateService; private final EdgeDeviceCommandOperateService edgeDeviceCommandOperateService; private final InspectionMeterRecognizeOperateService inspectionMeterRecognizeOperateService; + private final List edgeOperateServices; + private final List llmOperateServices; + private final List aeOperateServices; + private final FlowSleepNodeHandler flowSleepNodeHandler; + private final FlowHttpNodeHandler flowHttpNodeHandler; + private final FlowCodeNodeHandler flowCodeNodeHandler; public String actionExecute(FlowActionRequestVO req) { @@ -54,16 +69,77 @@ public class FlowActionExecutorService { switch (action.getOperate()) { case "EDGE": - return executeEdgeAction(action, req); + return executeEdgeOperateAction(action, req); case "LLM": - return executeLLMAction(action, req); + return executeLLMOperateAction(action, req); + + case "AE": + return executeAEOperateAction(action, req); + + case "NONE": + return executeStandaloneAction(action, req); default: throw new GlobalException("仅设备和大模型 Action 可调用!"); } } + private String executeEdgeOperateAction(ActionEnum action, FlowActionRequestVO req) { + TaskNodeExecuteMessage message = buildSingleNodeMessage(action, req.getPayload()); + for (EdgeOperateService service : edgeOperateServices) { + if (service.supports(action)) { + return resultToString(service.execute(message)); + } + } + throw new GlobalException("Unsupported EDGE action: " + action.getAction()); + } + + private String executeLLMOperateAction(ActionEnum action, FlowActionRequestVO req) { + TaskNodeExecuteMessage message = buildSingleNodeMessage(action, req.getPayload()); + for (LLMOperateService service : llmOperateServices) { + if (service.supports(action)) { + return resultToString(service.execute(message)); + } + } + throw new GlobalException("Unsupported LLM action: " + action.getAction()); + } + + private String executeAEOperateAction(ActionEnum action, FlowActionRequestVO req) { + TaskNodeExecuteMessage message = buildSingleNodeMessage(action, req.getPayload()); + for (AEOperateService service : aeOperateServices) { + if (service.supports(action)) { + return resultToString(service.execute(message)); + } + } + throw new GlobalException("Unsupported AE action: " + action.getAction()); + } + + private String executeStandaloneAction(ActionEnum action, FlowActionRequestVO req) { + TaskNodeExecuteMessage message = buildSingleNodeMessage(action, req.getPayload()); + switch (action) { + case SLEEP: + return resultToString(flowSleepNodeHandler.handle(message)); + case HTTP: + return resultToString(flowHttpNodeHandler.handle(message)); + case CODE: + return resultToString(flowCodeNodeHandler.handle(message)); + default: + throw new GlobalException("This action requires flow context: " + action.getAction()); + } + } + + private String resultToString(TaskNodeExecuteResult result) { + if (result == null) { + throw new GlobalException("Single-node execution returned no result"); + } + if (!result.isSuccess()) { + throw new GlobalException(StrUtil.isBlank(result.getErrorMsg()) + ? "Single-node execution failed" : result.getErrorMsg()); + } + return result.getOutputParams() == null ? "{}" : result.getOutputParams().toJSONString(); + } + private String executeEdgeAction(ActionEnum action, FlowActionRequestVO req) { JSONObject payload = req.getPayload() == null ? new JSONObject() : req.getPayload(); String robotId = payload.getString("robotId"); @@ -253,9 +329,15 @@ public class FlowActionExecutorService { } private TaskNodeExecuteMessage buildSingleNodeMessage(ActionEnum action, JSONObject payload) { + JSONObject inputParams = payload == null ? new JSONObject() : payload; TaskNodeExecuteMessage message = new TaskNodeExecuteMessage(); message.setAction(action); - message.setInputParams(payload == null ? new JSONObject() : payload); + message.setNodeId("single-node"); + message.setRobotId(inputParams.getString("robotId")); + message.setIterations(java.util.Arrays.asList(1, 1)); + message.setLoopNum(1); + message.setLoopArray(inputParams.get("array")); + message.setInputParams(inputParams); return message; } } diff --git a/sql/inspection_module.sql b/sql/inspection_module.sql index 193af17..c5b8e5e 100644 --- a/sql/inspection_module.sql +++ b/sql/inspection_module.sql @@ -19,7 +19,7 @@ CREATE TABLE `inspection_robot` ( `ip_address` varchar(50) DEFAULT NULL COMMENT 'IP地址', `port` int(11) DEFAULT NULL COMMENT '端口号', `current_map_id` varchar(64) DEFAULT NULL COMMENT '当前地图ID', - `status` char(1) DEFAULT '0' COMMENT '状态(0在线 1离线 2充电中 3巡检中)', + `status` char(1) DEFAULT '0' COMMENT 'AGV运行模式(0未知 1断开 2空闲 3手动 4自动 5充电 6暂停 7停止 8故障 9急停)', `battery_level` int(3) DEFAULT NULL COMMENT '电量百分比', `current_position` varchar(200) DEFAULT NULL COMMENT '当前位置', `connect_status` char(1) NOT NULL DEFAULT '0' COMMENT '连接状态(0离线 1在线)',