From 01da7a2dd4e644b3b39b0baaa5f8a8d23d566bd0 Mon Sep 17 00:00:00 2001 From: lixiaolong <702156524@qq.com> Date: Wed, 23 Sep 2026 14:24:44 +0800 Subject: [PATCH] =?UTF-8?q?feat(model):=20=E6=9B=B4=E6=96=B0AI=E8=AF=84?= =?UTF-8?q?=E4=BC=B0=E7=BB=93=E6=9E=9C=E6=A8=A1=E5=9E=8B=E5=B9=B6=E9=87=8D?= =?UTF-8?q?=E6=9E=84AGV=E5=8D=8F=E8=AE=AE=E5=AE=9A=E4=B9=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在AeIResultDTO中新增结构化执行步骤、界面路径和执行证据截图字段 - 在AeViEvaluationVO中新增执行过程相关的结构化步骤字段 - 将agv_utils.proto中的消息定义整合到agv.proto中 - 从arm_command.proto中补充完整的机械臂控制命令定义 - 更新arm_service.proto中机械臂服务接口定义 - 移除废弃的arm_teleop_v1.proto文件 - 修改AgvNavigationWaiter类中引用的消息类型包名 - 更新应用配置文件中的激活环境设置 --- .../web/controller/api/EdgeAgvController.java | 36 +- .../web/controller/api/EdgeArmController.java | 29 ++ .../controller/ti/TiProjectController.java | 6 + .../controller/ti/TiVehicleUiController.java | 8 + .../src/main/resources/application.yml | 2 +- .../client/manage/GrpcServiceManager.java | 4 - .../edge/client/service/EdgeAgvService.java | 30 +- .../edge/client/service/EdgeArmService.java | 8 + .../client/service/EdgeArmTeleopService.java | 3 +- .../edge/client/service/EdgeMotorService.java | 17 +- .../client/service/EdgeSystemService.java | 7 +- .../service/impl/EdgeAgvServiceImpl.java | 38 +- .../service/impl/EdgeArmServiceImpl.java | 118 ++++- .../impl/EdgeArmTeleopServiceImpl.java | 83 +--- .../service/impl/EdgeMotorServiceImpl.java | 189 +------- .../service/impl/EdgeSystemServiceImpl.java | 222 ++------- .../service/impl/EdgeArmServiceImplTest.java | 36 ++ .../impl/EdgeSystemServiceImplTest.java | 41 +- .../cmvr-iot-edge/cmvr-iot-grpc-lib/pom.xml | 4 +- .../src/main/proto/cmvr/api/agv_command.proto | 2 +- .../src/main/proto/cmvr/api/arm_command.proto | 136 ++++++ .../src/main/proto/cmvr/api/arm_service.proto | 22 +- .../main/proto/cmvr/api/arm_teleop_v1.proto | 133 ------ .../src/main/proto/cmvr/api/common.proto | 70 +-- .../main/proto/cmvr/api/motor_command.proto | 151 ------ .../main/proto/cmvr/api/motor_service.proto | 21 - .../main/proto/cmvr/api/safety_command.proto | 204 -------- .../main/proto/cmvr/api/system_command.proto | 165 +------ .../main/proto/cmvr/api/system_service.proto | 9 - .../{api/agv_utils.proto => msgs/agv.proto} | 0 .../evaluation/model/dto/AeIResultDTO.java | 28 ++ .../evaluation/model/vo/AeViEvaluationVO.java | 10 + .../service/ResourceRobotControlService.java | 14 +- .../service/impl/ISysFileServiceImpl.java | 55 ++- .../test/flow/context/TaskInstHolder.java | 13 + .../test/flow/context/TaskThreadRegistry.java | 5 +- .../dispatcher/FlowLoopNodeHandler.java | 14 +- .../flow/runtime/engine/FlowItemExecutor.java | 15 + .../interceptor/FlowAfterInterceptor.java | 5 +- .../interceptor/FlowExceptionInterceptor.java | 1 + .../interceptor/FlowLoggingInterceptor.java | 2 +- .../operator/edge/AgvNavigationWaiter.java | 14 +- .../edge/EdgeActionQueueOperateService.java | 5 +- .../operator/edge/EdgeAgvOperateService.java | 10 +- .../edge/EdgeDeviceCommandOperateService.java | 1 + .../edge/EdgeTouchOperateService.java | 225 +++++++-- .../edge/ti/TiTouchOperateService.java | 6 +- .../service/FlowActionExecutorService.java | 8 +- .../cmvr/test/service/ITeNodeInstService.java | 3 + .../ex/ExTiVehicleFunctionService.java | 6 + .../service/impl/ITeNodeInstServiceImpl.java | 11 + .../context/TaskInstHolderFailureTest.java | 66 +++ .../dispatcher/FlowLoopNodeHandlerTest.java | 72 +++ .../runtime/engine/FlowItemExecutorTest.java | 60 +++ .../edge/AgvNavigationWaiterTest.java | 6 +- .../EdgeActionQueueOperateServiceTest.java | 18 +- .../EdgeDeviceCommandOperateServiceTest.java | 3 +- .../edge/EdgeTouchOperateServiceTest.java | 139 ++++++ .../ti/model/domain/TiVehicleFunction.java | 15 + .../ti/model/vo/TiProjectReadinessVO.java | 42 ++ .../cmvr/ti/service/ITiProjectService.java | 3 + .../ex/ExTiVehicleFunctionServiceImpl.java | 437 ++++++++++++++---- .../ti/service/impl/TiProjectServiceImpl.java | 300 +++++++++++- .../impl/TiVehicleFunctionServiceImpl.java | 48 +- .../service/impl/TiVehicleUiServiceImpl.java | 49 ++ .../cmvr/ti/support/TiEvaluationRecorder.java | 89 ++++ .../ti/support/TiEvaluationRecorderTest.java | 2 + sql/ti_touch_interaction_v2.sql | 26 ++ 68 files changed, 2143 insertions(+), 1477 deletions(-) create mode 100644 cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-client/src/test/java/com/cmvr/edge/client/service/impl/EdgeArmServiceImplTest.java delete mode 100644 cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-lib/src/main/proto/cmvr/api/arm_teleop_v1.proto delete mode 100644 cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-lib/src/main/proto/cmvr/api/motor_command.proto delete mode 100644 cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-lib/src/main/proto/cmvr/api/motor_service.proto delete mode 100644 cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-lib/src/main/proto/cmvr/api/safety_command.proto rename cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-lib/src/main/proto/cmvr/{api/agv_utils.proto => msgs/agv.proto} (100%) create mode 100644 cmvr-iot-test/src/test/java/com/cmvr/test/flow/context/TaskInstHolderFailureTest.java create mode 100644 cmvr-iot-test/src/test/java/com/cmvr/test/flow/runtime/dispatcher/FlowLoopNodeHandlerTest.java create mode 100644 cmvr-iot-test/src/test/java/com/cmvr/test/flow/runtime/operator/edge/EdgeTouchOperateServiceTest.java create mode 100644 cmvr-iot-ti/src/main/java/com/cmvr/ti/model/vo/TiProjectReadinessVO.java create mode 100644 sql/ti_touch_interaction_v2.sql diff --git a/cmvr-iot-admin/src/main/java/com/cmvr/web/controller/api/EdgeAgvController.java b/cmvr-iot-admin/src/main/java/com/cmvr/web/controller/api/EdgeAgvController.java index 4ac932f..7cc98b6 100644 --- a/cmvr-iot-admin/src/main/java/com/cmvr/web/controller/api/EdgeAgvController.java +++ b/cmvr-iot-admin/src/main/java/com/cmvr/web/controller/api/EdgeAgvController.java @@ -1,6 +1,6 @@ package com.cmvr.web.controller.api; -import cmvr.msgs.AgvUtils; +import cmvr.msgs.Agv; import com.cmvr.common.core.domain.AjaxResult; import com.cmvr.edge.client.model.EdgeCommonVO; import com.cmvr.edge.client.model.agv.EdgeAgvMapNameVO; @@ -58,7 +58,7 @@ public class EdgeAgvController { @ApiParam(value = "设备ID", required = true) @RequestParam("deviceId") String deviceId) { EdgeCommonVO vo = commonVo(robotId, deviceId); - AgvUtils.AgvRuntimeState state = edgeAgvService.getRuntimeState(vo); + Agv.AgvRuntimeState state = edgeAgvService.getRuntimeState(vo); return AjaxResult.ok(toRuntimeStateMap(state)); } @@ -73,7 +73,7 @@ public class EdgeAgvController { @ApiParam(value = "设备ID", required = true) @RequestParam("deviceId") String deviceId) { EdgeCommonVO vo = commonVo(robotId, deviceId); - AgvUtils.AgvNavigationStatus status = edgeAgvService.getNavigationStatus(vo); + Agv.AgvNavigationStatus status = edgeAgvService.getNavigationStatus(vo); Map resultMap = new HashMap<>(); resultMap.put("state", status.getState()); resultMap.put("type", status.getType()); @@ -108,7 +108,7 @@ public class EdgeAgvController { @ApiOperation("AGV重定位") @PostMapping("/relocalize") public AjaxResult relocalize(@RequestBody @Validated EdgeAgvRelocalizeVO vo) { - AgvUtils.AgvPose2d pose = AgvUtils.AgvPose2d.newBuilder() + Agv.AgvPose2d pose = Agv.AgvPose2d.newBuilder() .setX(vo.getX()) .setY(vo.getY()) .setTheta(vo.getTheta()) @@ -120,13 +120,13 @@ public class EdgeAgvController { @ApiOperation("固定距离平移") @PostMapping("/translate") public AjaxResult translate(@RequestBody @Validated EdgeAgvTranslateVO vo) { - AgvUtils.AgvTranslationMode mode = vo.getMode() == null - ? AgvUtils.AgvTranslationMode.AGV_TRANSLATION_MODE_ODOMETRY - : AgvUtils.AgvTranslationMode.forNumber(vo.getMode()); + Agv.AgvTranslationMode mode = vo.getMode() == null + ? Agv.AgvTranslationMode.AGV_TRANSLATION_MODE_ODOMETRY + : Agv.AgvTranslationMode.forNumber(vo.getMode()); if (mode == null) { return AjaxResult.error("距离参考模式不合法,允许值:0=里程,1=定位"); } - AgvUtils.AgvTranslation translation = AgvUtils.AgvTranslation.newBuilder() + Agv.AgvTranslation translation = Agv.AgvTranslation.newBuilder() .setDistance(vo.getDistance()) .setVx(vo.getVx()) .setVy(vo.getVy()) @@ -142,7 +142,7 @@ public class EdgeAgvController { @ApiOperation("导航到指定位置") @PostMapping("/navigateToPose") public AjaxResult navigateToPose(@RequestBody @Validated EdgeAgvNavigateToPoseVO vo) { - AgvUtils.AgvPose2d pose = AgvUtils.AgvPose2d.newBuilder() + Agv.AgvPose2d pose = Agv.AgvPose2d.newBuilder() .setX(vo.getX()) .setY(vo.getY()) .setTheta(vo.getTheta()) @@ -197,7 +197,7 @@ public class EdgeAgvController { @ApiOperation("设置速度") @PostMapping("/setVelocity") public AjaxResult setVelocity(@RequestBody @Validated EdgeAgvVelocityVO vo) { - AgvUtils.AgvVelocity velocity = AgvUtils.AgvVelocity.newBuilder() + Agv.AgvVelocity velocity = Agv.AgvVelocity.newBuilder() .setVx(vo.getVx()) .setVy(vo.getVy()) .setWz(vo.getWz()) @@ -242,9 +242,9 @@ public class EdgeAgvController { @ApiParam(value = "设备ID", required = true) @RequestParam("deviceId") String deviceId) { EdgeCommonVO vo = commonVo(robotId, deviceId); - List stations = edgeAgvService.listStations(vo); + List stations = edgeAgvService.listStations(vo); List> stationList = new ArrayList<>(); - for (AgvUtils.AgvStation station : stations) { + for (Agv.AgvStation station : stations) { stationList.add(toStationMap(station)); } return AjaxResult.ok(stationList); @@ -296,7 +296,7 @@ public class EdgeAgvController { @ApiOperation("开始建图") @PostMapping("/startMapping") public AjaxResult startMapping(@RequestBody @Validated EdgeAgvStartMappingVO vo) { - AgvUtils.AgvMapDimension mapDimension = AgvUtils.AgvMapDimension.forNumber(vo.getDimension()); + Agv.AgvMapDimension mapDimension = Agv.AgvMapDimension.forNumber(vo.getDimension()); if (mapDimension == null) { return AjaxResult.error("建图维度不合法,允许值:0=未指定,1=2D,2=3D,3=2D+3D"); } @@ -327,16 +327,16 @@ public class EdgeAgvController { return vo; } - private AgvUtils.AgvMotionOptions navigationOptions(Integer blockedTimeoutMs) { + private Agv.AgvMotionOptions navigationOptions(Integer blockedTimeoutMs) { int timeout = blockedTimeoutMs == null ? EdgeAgvService.DEFAULT_BLOCKED_TIMEOUT_MS : blockedTimeoutMs; - return AgvUtils.AgvMotionOptions.newBuilder() + return Agv.AgvMotionOptions.newBuilder() .setAsynchronous(false) .setBlockedTimeoutMs(timeout) .build(); } - private Map toRuntimeStateMap(AgvUtils.AgvRuntimeState state) { + private Map toRuntimeStateMap(Agv.AgvRuntimeState state) { Map resultMap = new HashMap<>(); resultMap.put("timestamp", state.getTimestamp()); resultMap.put("mode", state.getMode()); @@ -375,7 +375,7 @@ public class EdgeAgvController { /** * 将 protobuf 站点对象转换为普通 Map。 */ - private Map toStationMap(AgvUtils.AgvStation station) { + private Map toStationMap(Agv.AgvStation station) { Map stationMap = new HashMap<>(); stationMap.put("id", station.getId()); stationMap.put("type", station.getType()); @@ -389,7 +389,7 @@ public class EdgeAgvController { /** * 将 protobuf 二维位姿转换为普通 Map。 */ - private Map toPoseMap(AgvUtils.AgvPose2d pose) { + private Map toPoseMap(Agv.AgvPose2d pose) { Map poseMap = new HashMap<>(); poseMap.put("x", pose.getX()); poseMap.put("y", pose.getY()); diff --git a/cmvr-iot-admin/src/main/java/com/cmvr/web/controller/api/EdgeArmController.java b/cmvr-iot-admin/src/main/java/com/cmvr/web/controller/api/EdgeArmController.java index b5695b6..55de081 100644 --- a/cmvr-iot-admin/src/main/java/com/cmvr/web/controller/api/EdgeArmController.java +++ b/cmvr-iot-admin/src/main/java/com/cmvr/web/controller/api/EdgeArmController.java @@ -72,6 +72,35 @@ public class EdgeArmController { edgeArmService.executeJsonCommand(request))); } + @ApiOperation("读取机械臂控制柜数字输入") + @GetMapping("/cabinet/digitalInput") + public AjaxResult getCabinetDigitalInput( + @RequestParam("robotId") String robotId, + @RequestParam("deviceId") String deviceId, + @RequestParam("index") int index) { + return AjaxResult.ok(edgeArmService.getCabinetDigitalInput(commonVo(robotId, deviceId), index)); + } + + @ApiOperation("读取机械臂控制柜数字输出") + @GetMapping("/cabinet/digitalOutput") + public AjaxResult getCabinetDigitalOutput( + @RequestParam("robotId") String robotId, + @RequestParam("deviceId") String deviceId, + @RequestParam("index") int index) { + return AjaxResult.ok(edgeArmService.getCabinetDigitalOutput(commonVo(robotId, deviceId), index)); + } + + @ApiOperation("设置机械臂控制柜数字输出") + @PostMapping("/cabinet/digitalOutput") + public AjaxResult setCabinetDigitalOutput( + @RequestParam("robotId") String robotId, + @RequestParam("deviceId") String deviceId, + @RequestParam("index") int index, + @RequestParam("value") boolean value) { + return AjaxResult.ok(edgeArmService.setCabinetDigitalOutput( + commonVo(robotId, deviceId), index, value)); + } + @ApiOperation("关节空间运动") @PostMapping("/moveJ") public AjaxResult moveJ(@RequestBody EdgeArmMoveJVO moveJVO) { diff --git a/cmvr-iot-admin/src/main/java/com/cmvr/web/controller/ti/TiProjectController.java b/cmvr-iot-admin/src/main/java/com/cmvr/web/controller/ti/TiProjectController.java index 4f446e6..1e5dbc9 100644 --- a/cmvr-iot-admin/src/main/java/com/cmvr/web/controller/ti/TiProjectController.java +++ b/cmvr-iot-admin/src/main/java/com/cmvr/web/controller/ti/TiProjectController.java @@ -73,6 +73,12 @@ public class TiProjectController extends BaseController { return AjaxResult.ok(tiProjectService.queryExecuteRunParams(projectId)); } + @ApiOperation("检查触控项目、录像和工作流是否具备执行条件") + @GetMapping("/readiness/{projectId}") + public AjaxResult readiness(@PathVariable("projectId") String projectId) { + return AjaxResult.ok(tiProjectService.checkExecutionReadiness(projectId)); + } + @ApiOperation("执行项目") @PostMapping("/execute") public AjaxResult execute(@RequestBody TeTaskExecuteProjectVO taskExecuteProjectVO) { diff --git a/cmvr-iot-admin/src/main/java/com/cmvr/web/controller/ti/TiVehicleUiController.java b/cmvr-iot-admin/src/main/java/com/cmvr/web/controller/ti/TiVehicleUiController.java index 3f3f131..d1ab344 100644 --- a/cmvr-iot-admin/src/main/java/com/cmvr/web/controller/ti/TiVehicleUiController.java +++ b/cmvr-iot-admin/src/main/java/com/cmvr/web/controller/ti/TiVehicleUiController.java @@ -21,6 +21,7 @@ import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.RequestParam; import java.util.List; @@ -78,4 +79,11 @@ public class TiVehicleUiController extends BaseController { public AjaxResult findNextClickToFunction(Long vehicleConfigId, String currentUi, String clickFunction) { return AjaxResult.success(exTiVehicleFunctionService.findNextClickToFunction(vehicleConfigId, currentUi, clickFunction)); } + + @ApiOperation("查询界面路径拓扑") + @PreAuthorize("@ss.hasPermi('ti:ui:list')") + @GetMapping("/topology") + public AjaxResult topology(@RequestParam Long vehicleConfigId) { + return AjaxResult.success(exTiVehicleFunctionService.getTopology(vehicleConfigId)); + } } diff --git a/cmvr-iot-admin/src/main/resources/application.yml b/cmvr-iot-admin/src/main/resources/application.yml index 45de4dc..c2db5cb 100644 --- a/cmvr-iot-admin/src/main/resources/application.yml +++ b/cmvr-iot-admin/src/main/resources/application.yml @@ -60,7 +60,7 @@ spring: # 国际化资源文件路径 basename: i18n/messages profiles: - active: aima + active: test # 文件上传 servlet: multipart: diff --git a/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-client/src/main/java/com/cmvr/edge/client/manage/GrpcServiceManager.java b/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-client/src/main/java/com/cmvr/edge/client/manage/GrpcServiceManager.java index 7d8ac7e..ff98ebd 100644 --- a/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-client/src/main/java/com/cmvr/edge/client/manage/GrpcServiceManager.java +++ b/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-client/src/main/java/com/cmvr/edge/client/manage/GrpcServiceManager.java @@ -1,7 +1,6 @@ package com.cmvr.edge.client.manage; import cmvr.api.*; -import cmvr.api.armteleop.v1.ArmTeleopServiceGrpc; import com.cmvr.common.robot.RobotEndpoint; import com.cmvr.common.robot.RobotEndpointResolver; import io.grpc.ManagedChannel; @@ -61,9 +60,6 @@ public class GrpcServiceManager { clientFactories.put(HlcServiceGrpc.HlcServiceBlockingStub.class, new GrpcClientFactory<>(HlcServiceGrpc::newBlockingStub)); // 注册agv服务的stub clientFactories.put(AgvServiceGrpc.AgvServiceBlockingStub.class, new GrpcClientFactory<>(AgvServiceGrpc::newBlockingStub)); - clientFactories.put(MotorServiceGrpc.MotorServiceBlockingStub.class, new GrpcClientFactory<>(MotorServiceGrpc::newBlockingStub)); - clientFactories.put(MotorServiceGrpc.MotorServiceStub.class, new GrpcClientFactory<>(MotorServiceGrpc::newStub)); - clientFactories.put(ArmTeleopServiceGrpc.ArmTeleopServiceStub.class, new GrpcClientFactory<>(ArmTeleopServiceGrpc::newStub)); // 注册机器人实时语音双向流服务的异步Stub } diff --git a/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-client/src/main/java/com/cmvr/edge/client/service/EdgeAgvService.java b/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-client/src/main/java/com/cmvr/edge/client/service/EdgeAgvService.java index 9a7b799..85b6b6a 100644 --- a/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-client/src/main/java/com/cmvr/edge/client/service/EdgeAgvService.java +++ b/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-client/src/main/java/com/cmvr/edge/client/service/EdgeAgvService.java @@ -1,7 +1,7 @@ package com.cmvr.edge.client.service; -import cmvr.msgs.AgvUtils; -import cmvr.msgs.AgvUtils.AgvRuntimeState; +import cmvr.msgs.Agv; +import cmvr.msgs.Agv.AgvRuntimeState; import com.cmvr.edge.client.model.EdgeCommonVO; @@ -31,7 +31,7 @@ public interface EdgeAgvService { * @param edgeCommonVO 边缘通用参数 * @return 导航状态 */ - AgvUtils.AgvNavigationStatus getNavigationStatus(EdgeCommonVO edgeCommonVO); + Agv.AgvNavigationStatus getNavigationStatus(EdgeCommonVO edgeCommonVO); /** * 紧急停止 @@ -50,12 +50,12 @@ public interface EdgeAgvService { /** * 在当前地图中重新设置 AGV 定位位姿。 */ - void relocalize(EdgeCommonVO edgeCommonVO, AgvUtils.AgvPose2d pose); + void relocalize(EdgeCommonVO edgeCommonVO, Agv.AgvPose2d pose); /** * 按指定速度平移固定距离。 */ - void translate(EdgeCommonVO edgeCommonVO, AgvUtils.AgvTranslation translation); + void translate(EdgeCommonVO edgeCommonVO, Agv.AgvTranslation translation); /** * 导航到指定位置 @@ -63,10 +63,10 @@ public interface EdgeAgvService { * @param edgeCommonVO 边缘通用参数 * @param pose 目标位姿 */ - void navigateToPose(EdgeCommonVO edgeCommonVO, AgvUtils.AgvPose2d pose); + void navigateToPose(EdgeCommonVO edgeCommonVO, Agv.AgvPose2d pose); - void navigateToPose(EdgeCommonVO edgeCommonVO, AgvUtils.AgvPose2d pose, - AgvUtils.AgvMotionOptions options); + void navigateToPose(EdgeCommonVO edgeCommonVO, Agv.AgvPose2d pose, + Agv.AgvMotionOptions options); /** * 导航到站点 @@ -77,7 +77,7 @@ public interface EdgeAgvService { void navigateToStation(EdgeCommonVO edgeCommonVO, String stationId); void navigateToStation(EdgeCommonVO edgeCommonVO, String stationId, - AgvUtils.AgvMotionOptions options); + Agv.AgvMotionOptions options); /** * 跟随路径 @@ -85,10 +85,10 @@ public interface EdgeAgvService { * @param edgeCommonVO 边缘通用参数 * @param pathSegments 路径段列表 */ - void followPath(EdgeCommonVO edgeCommonVO, List pathSegments); + void followPath(EdgeCommonVO edgeCommonVO, List pathSegments); - void followPath(EdgeCommonVO edgeCommonVO, List pathSegments, - AgvUtils.AgvMotionOptions options); + void followPath(EdgeCommonVO edgeCommonVO, List pathSegments, + Agv.AgvMotionOptions options); /** * 暂停导航 @@ -117,7 +117,7 @@ public interface EdgeAgvService { * @param edgeCommonVO 边缘通用参数 * @param velocity 速度 */ - void setVelocity(EdgeCommonVO edgeCommonVO, AgvUtils.AgvVelocity velocity); + void setVelocity(EdgeCommonVO edgeCommonVO, Agv.AgvVelocity velocity); /** * 停止速度控制 @@ -140,7 +140,7 @@ public interface EdgeAgvService { * @param edgeCommonVO 边缘通用参数 * @return 站点列表 */ - List listStations(EdgeCommonVO edgeCommonVO); + List listStations(EdgeCommonVO edgeCommonVO); /** * 切换地图 @@ -177,7 +177,7 @@ public interface EdgeAgvService { * @param realTime 是否实时建图 * @return 会话ID */ - String startMapping(EdgeCommonVO edgeCommonVO, AgvUtils.AgvMapDimension dimension, String mapName, boolean realTime); + String startMapping(EdgeCommonVO edgeCommonVO, Agv.AgvMapDimension dimension, String mapName, boolean realTime); /** * 停止建图 diff --git a/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-client/src/main/java/com/cmvr/edge/client/service/EdgeArmService.java b/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-client/src/main/java/com/cmvr/edge/client/service/EdgeArmService.java index a80378d..c80568d 100644 --- a/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-client/src/main/java/com/cmvr/edge/client/service/EdgeArmService.java +++ b/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-client/src/main/java/com/cmvr/edge/client/service/EdgeArmService.java @@ -2,6 +2,7 @@ package com.cmvr.edge.client.service; import cmvr.api.ArmCommand; import cmvr.api.Common; +import com.alibaba.fastjson2.JSONObject; import com.cmvr.edge.client.model.EdgeCommonVO; import com.cmvr.edge.client.model.system.EdgeSystemJsonCommandVO; @@ -44,6 +45,13 @@ public interface EdgeArmService { */ Common.JsonDeviceCommand.Feedback executeJsonCommand(EdgeSystemJsonCommandVO request); + JSONObject getCabinetDigitalInput(EdgeCommonVO edgeCommonVO, int index); + + JSONObject getCabinetDigitalOutput(EdgeCommonVO edgeCommonVO, int index); + + JSONObject setCabinetDigitalOutput( + EdgeCommonVO edgeCommonVO, int index, boolean value); + /** * 关节空间运动(命令码:moveJ) * 控制机械臂通过关节空间插值运动到目标关节位置 diff --git a/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-client/src/main/java/com/cmvr/edge/client/service/EdgeArmTeleopService.java b/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-client/src/main/java/com/cmvr/edge/client/service/EdgeArmTeleopService.java index 6fb8fa3..8734f2f 100644 --- a/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-client/src/main/java/com/cmvr/edge/client/service/EdgeArmTeleopService.java +++ b/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-client/src/main/java/com/cmvr/edge/client/service/EdgeArmTeleopService.java @@ -1,6 +1,5 @@ package com.cmvr.edge.client.service; -import cmvr.api.armteleop.v1.ArmTeleopV1; import com.cmvr.edge.client.model.armteleop.EdgeArmTeleopHeartbeatVO; import com.cmvr.edge.client.model.armteleop.EdgeArmTeleopOpenVO; import com.cmvr.edge.client.model.armteleop.EdgeArmTeleopSetpointVO; @@ -13,7 +12,7 @@ public interface EdgeArmTeleopService { void heartbeat(EdgeArmTeleopHeartbeatVO request); - ArmTeleopV1.ServerFrame getStatus(String sessionId); + Object getStatus(String sessionId); void stop(EdgeArmTeleopStopVO request); } diff --git a/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-client/src/main/java/com/cmvr/edge/client/service/EdgeMotorService.java b/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-client/src/main/java/com/cmvr/edge/client/service/EdgeMotorService.java index 9c64c3b..39b1f71 100644 --- a/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-client/src/main/java/com/cmvr/edge/client/service/EdgeMotorService.java +++ b/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-client/src/main/java/com/cmvr/edge/client/service/EdgeMotorService.java @@ -1,6 +1,5 @@ package com.cmvr.edge.client.service; -import cmvr.api.MotorCommand; import com.cmvr.edge.client.model.motor.EdgeMotorCyclicOpenVO; import com.cmvr.edge.client.model.motor.EdgeMotorCyclicSetpointVO; import com.cmvr.edge.client.model.motor.EdgeMotorEnabledVO; @@ -8,19 +7,19 @@ import com.cmvr.edge.client.model.motor.EdgeMotorMotionVO; import com.cmvr.edge.client.model.motor.EdgeMotorTargetVO; public interface EdgeMotorService { - MotorCommand.MotorCommandResponse setZero(EdgeMotorTargetVO request); + Object setZero(EdgeMotorTargetVO request); - MotorCommand.MotorCommandResponse moveToZero(EdgeMotorMotionVO request); + Object moveToZero(EdgeMotorMotionVO request); - MotorCommand.MotorCommandResponse profilePosition(EdgeMotorMotionVO request); + Object profilePosition(EdgeMotorMotionVO request); - MotorCommand.MotorCommandResponse profileVelocity(EdgeMotorMotionVO request); + Object profileVelocity(EdgeMotorMotionVO request); - MotorCommand.MotorCommandResponse emergencyStop(EdgeMotorTargetVO request); + Object emergencyStop(EdgeMotorTargetVO request); - MotorCommand.GetMotorStatusResponse getStatus(EdgeMotorTargetVO request); + Object getStatus(EdgeMotorTargetVO request); - MotorCommand.MotorCommandResponse setEnabled(EdgeMotorEnabledVO request); + Object setEnabled(EdgeMotorEnabledVO request); String openCyclicPosition(EdgeMotorCyclicOpenVO request); @@ -30,7 +29,7 @@ public interface EdgeMotorService { void sendCyclicVelocity(EdgeMotorCyclicSetpointVO request); - MotorCommand.CyclicControlResponse getStreamStatus(String sessionId); + Object getStreamStatus(String sessionId); void closeStream(String sessionId); } diff --git a/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-client/src/main/java/com/cmvr/edge/client/service/EdgeSystemService.java b/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-client/src/main/java/com/cmvr/edge/client/service/EdgeSystemService.java index f912b82..a77805d 100644 --- a/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-client/src/main/java/com/cmvr/edge/client/service/EdgeSystemService.java +++ b/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-client/src/main/java/com/cmvr/edge/client/service/EdgeSystemService.java @@ -1,7 +1,6 @@ package com.cmvr.edge.client.service; import cmvr.api.Common; -import cmvr.api.SafetyCommand; import cmvr.api.SystemCommand; import com.cmvr.edge.client.model.system.EdgeSystemJsonCommandVO; import com.cmvr.edge.client.model.system.EdgeSystemUpdateParamsVO; @@ -26,16 +25,16 @@ public interface EdgeSystemService { SystemCommand.ActionQueueCommand.Feedback executeActionQueue(EdgeActionQueueVO request); - SafetyCommand.GetSafetyStateCommand.Feedback getSafetyState(String robotId); + Object getSafetyState(String robotId); - SafetyCommand.RestoreOperationalStateCommand.Feedback restoreOperationalState( + Object restoreOperationalState( String robotId, String reason, Integer timeoutMs); /** * @deprecated Use {@link #restoreOperationalState(String, String, Integer)}. */ @Deprecated - SafetyCommand.RecoverSafetyStateCommand.Feedback recoverSafetyState( + Object recoverSafetyState( String robotId, String reason, Integer timeoutMs); diff --git a/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-client/src/main/java/com/cmvr/edge/client/service/impl/EdgeAgvServiceImpl.java b/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-client/src/main/java/com/cmvr/edge/client/service/impl/EdgeAgvServiceImpl.java index 0d01bfd..5fe0aee 100644 --- a/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-client/src/main/java/com/cmvr/edge/client/service/impl/EdgeAgvServiceImpl.java +++ b/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-client/src/main/java/com/cmvr/edge/client/service/impl/EdgeAgvServiceImpl.java @@ -3,7 +3,7 @@ package com.cmvr.edge.client.service.impl; import cmvr.api.AgvCommand; import cmvr.api.AgvServiceGrpc; import cmvr.api.Common; -import cmvr.msgs.AgvUtils; +import cmvr.msgs.Agv; import cn.hutool.core.util.StrUtil; import com.cmvr.common.exception.GlobalException; import com.cmvr.edge.client.manage.GrpcServiceManager; @@ -30,7 +30,7 @@ public class EdgeAgvServiceImpl implements EdgeAgvService { private final GrpcServiceManager grpcServiceManager; @Override - public AgvUtils.AgvRuntimeState getRuntimeState(EdgeCommonVO edgeCommonVO) { + public Agv.AgvRuntimeState getRuntimeState(EdgeCommonVO edgeCommonVO) { AgvServiceGrpc.AgvServiceBlockingStub stub = grpcServiceManager.getGrpcClient( edgeCommonVO.getRobotId(), AgvServiceGrpc.AgvServiceBlockingStub.class); AgvCommand.AgvRuntimeStateCommand.Request request = AgvCommand.AgvRuntimeStateCommand.Request.newBuilder() @@ -40,7 +40,7 @@ public class EdgeAgvServiceImpl implements EdgeAgvService { } @Override - public AgvUtils.AgvNavigationStatus getNavigationStatus(EdgeCommonVO edgeCommonVO) { + public Agv.AgvNavigationStatus getNavigationStatus(EdgeCommonVO edgeCommonVO) { AgvServiceGrpc.AgvServiceBlockingStub stub = grpcServiceManager.getGrpcClient( edgeCommonVO.getRobotId(), AgvServiceGrpc.AgvServiceBlockingStub.class); AgvCommand.AgvNavigationStatusCommand.Request request = AgvCommand.AgvNavigationStatusCommand.Request.newBuilder() @@ -66,7 +66,7 @@ public class EdgeAgvServiceImpl implements EdgeAgvService { } @Override - public void relocalize(EdgeCommonVO edgeCommonVO, AgvUtils.AgvPose2d pose) { + public void relocalize(EdgeCommonVO edgeCommonVO, Agv.AgvPose2d pose) { if (pose == null) { throw new GlobalException("重定位位姿不能为空"); } @@ -80,7 +80,7 @@ public class EdgeAgvServiceImpl implements EdgeAgvService { } @Override - public void translate(EdgeCommonVO edgeCommonVO, AgvUtils.AgvTranslation translation) { + public void translate(EdgeCommonVO edgeCommonVO, Agv.AgvTranslation translation) { if (translation == null || translation.getDistance() <= 0) { throw new GlobalException("平移距离必须大于0"); } @@ -94,13 +94,13 @@ public class EdgeAgvServiceImpl implements EdgeAgvService { } @Override - public void navigateToPose(EdgeCommonVO edgeCommonVO, AgvUtils.AgvPose2d pose) { + public void navigateToPose(EdgeCommonVO edgeCommonVO, Agv.AgvPose2d pose) { navigateToPose(edgeCommonVO, pose, null); } @Override - public void navigateToPose(EdgeCommonVO edgeCommonVO, AgvUtils.AgvPose2d pose, - AgvUtils.AgvMotionOptions options) { + public void navigateToPose(EdgeCommonVO edgeCommonVO, Agv.AgvPose2d pose, + Agv.AgvMotionOptions options) { if (pose == null) { throw new GlobalException("目标位姿不能为空"); } @@ -122,7 +122,7 @@ public class EdgeAgvServiceImpl implements EdgeAgvService { @Override public void navigateToStation(EdgeCommonVO edgeCommonVO, String stationId, - AgvUtils.AgvMotionOptions options) { + Agv.AgvMotionOptions options) { if (StrUtil.isBlank(stationId)) { throw new GlobalException("站点ID不能为空"); } @@ -138,13 +138,13 @@ public class EdgeAgvServiceImpl implements EdgeAgvService { } @Override - public void followPath(EdgeCommonVO edgeCommonVO, List pathSegments) { + public void followPath(EdgeCommonVO edgeCommonVO, List pathSegments) { followPath(edgeCommonVO, pathSegments, null); } @Override - public void followPath(EdgeCommonVO edgeCommonVO, List pathSegments, - AgvUtils.AgvMotionOptions options) { + public void followPath(EdgeCommonVO edgeCommonVO, List pathSegments, + Agv.AgvMotionOptions options) { if (pathSegments == null || pathSegments.isEmpty()) { throw new GlobalException("路径段不能为空"); } @@ -184,7 +184,7 @@ public class EdgeAgvServiceImpl implements EdgeAgvService { } @Override - public void setVelocity(EdgeCommonVO edgeCommonVO, AgvUtils.AgvVelocity velocity) { + public void setVelocity(EdgeCommonVO edgeCommonVO, Agv.AgvVelocity velocity) { if (velocity == null) { throw new GlobalException("速度不能为空"); } @@ -217,7 +217,7 @@ public class EdgeAgvServiceImpl implements EdgeAgvService { } @Override - public List listStations(EdgeCommonVO edgeCommonVO) { + public List listStations(EdgeCommonVO edgeCommonVO) { AgvServiceGrpc.AgvServiceBlockingStub stub = grpcServiceManager.getGrpcClient( edgeCommonVO.getRobotId(), AgvServiceGrpc.AgvServiceBlockingStub.class); AgvCommand.AgvListStationsCommand.Request request = AgvCommand.AgvListStationsCommand.Request.newBuilder() @@ -276,12 +276,12 @@ public class EdgeAgvServiceImpl implements EdgeAgvService { } @Override - public String startMapping(EdgeCommonVO edgeCommonVO, AgvUtils.AgvMapDimension dimension, String mapName, boolean realTime) { + public String startMapping(EdgeCommonVO edgeCommonVO, Agv.AgvMapDimension dimension, String mapName, boolean realTime) { AgvServiceGrpc.AgvServiceBlockingStub stub = grpcServiceManager.getGrpcClient( edgeCommonVO.getRobotId(), AgvServiceGrpc.AgvServiceBlockingStub.class); AgvCommand.AgvStartMappingCommand.Request.Builder requestBuilder = AgvCommand.AgvStartMappingCommand.Request.newBuilder() .setHeader(EdgeCommonUtil.buildRequest(edgeCommonVO.getDeviceId())) - .setDimension(dimension != null ? dimension : AgvUtils.AgvMapDimension.AGV_MAP_DIMENSION_UNSPECIFIED) + .setDimension(dimension != null ? dimension : Agv.AgvMapDimension.AGV_MAP_DIMENSION_UNSPECIFIED) .setRealTime(realTime); if (StrUtil.isNotBlank(mapName)) { @@ -300,9 +300,9 @@ public class EdgeAgvServiceImpl implements EdgeAgvService { executeGrpcCall(() -> stub.stopMapping(request)); } - AgvUtils.AgvMotionOptions normalizeNavigationOptions(AgvUtils.AgvMotionOptions options) { - AgvUtils.AgvMotionOptions.Builder builder = options == null - ? AgvUtils.AgvMotionOptions.newBuilder() : options.toBuilder(); + Agv.AgvMotionOptions normalizeNavigationOptions(Agv.AgvMotionOptions options) { + Agv.AgvMotionOptions.Builder builder = options == null + ? Agv.AgvMotionOptions.newBuilder() : options.toBuilder(); if (builder.getBlockedTimeoutMs() <= 0) { builder.setBlockedTimeoutMs(EdgeAgvService.DEFAULT_BLOCKED_TIMEOUT_MS); } diff --git a/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-client/src/main/java/com/cmvr/edge/client/service/impl/EdgeArmServiceImpl.java b/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-client/src/main/java/com/cmvr/edge/client/service/impl/EdgeArmServiceImpl.java index 63a403d..a829785 100644 --- a/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-client/src/main/java/com/cmvr/edge/client/service/impl/EdgeArmServiceImpl.java +++ b/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-client/src/main/java/com/cmvr/edge/client/service/impl/EdgeArmServiceImpl.java @@ -4,6 +4,9 @@ import cmvr.api.ArmCommand; import cmvr.api.ArmServiceGrpc; import cmvr.api.Common; import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONException; +import com.alibaba.fastjson2.JSONObject; import com.cmvr.common.exception.GlobalException; import com.cmvr.edge.client.manage.GrpcServiceManager; import com.cmvr.edge.client.model.EdgeCommonVO; @@ -55,13 +58,33 @@ public class EdgeArmServiceImpl implements EdgeArmService { @Override public Common.JsonDeviceCommand.Feedback executeJsonCommand(EdgeSystemJsonCommandVO request) { - ArmServiceGrpc.ArmServiceBlockingStub stub = grpcServiceManager.getGrpcClient( - request.getRobotId(), ArmServiceGrpc.ArmServiceBlockingStub.class); - Common.JsonDeviceCommand.Request grpcRequest = Common.JsonDeviceCommand.Request.newBuilder() - .setHeader(EdgeCommonUtil.buildRequest(request.getDeviceId())) - .setRequestJson(request.getRequestJson() == null ? "" : request.getRequestJson()) - .build(); - return executeGrpcCall(() -> stub.executeJsonCommand(grpcRequest)); + if (request == null || StrUtil.hasBlank( + request.getRobotId(), request.getDeviceId(), request.getRequestJson())) { + throw new GlobalException("robotId、deviceId 和 JSON 指令不能为空"); + } + return executeJsonCommand( + request.getRobotId(), request.getDeviceId(), request.getRequestJson()); + } + + @Override + public JSONObject getCabinetDigitalInput( + EdgeCommonVO edgeCommonVO, int index) { + validateCabinetIndex(index); + return executeCabinetIo(edgeCommonVO, "get_di", index, null, "读取控制柜数字输入"); + } + + @Override + public JSONObject getCabinetDigitalOutput( + EdgeCommonVO edgeCommonVO, int index) { + validateCabinetIndex(index); + return executeCabinetIo(edgeCommonVO, "get_do", index, null, "读取控制柜数字输出"); + } + + @Override + public JSONObject setCabinetDigitalOutput( + EdgeCommonVO edgeCommonVO, int index, boolean value) { + validateCabinetIndex(index); + return executeCabinetIo(edgeCommonVO, "set_do", index, value, "设置控制柜数字输出"); } @Override @@ -345,6 +368,87 @@ public class EdgeArmServiceImpl implements EdgeArmService { return executeGrpcCall(() -> stub.computeForwardKinematics(requestBuilder.build())); } + private ArmServiceGrpc.ArmServiceBlockingStub blockingStub(String robotId) { + if (StrUtil.isBlank(robotId)) { + throw new GlobalException("机器人ID不能为空"); + } + return grpcServiceManager.getGrpcClient(robotId, ArmServiceGrpc.ArmServiceBlockingStub.class); + } + + private Common.JsonDeviceCommand.Feedback executeJsonCommand( + String robotId, String deviceId, String requestJson) { + if (StrUtil.hasBlank(robotId, deviceId)) { + throw new GlobalException("机器人ID和设备ID不能为空"); + } + Common.JsonDeviceCommand.Request grpcRequest = Common.JsonDeviceCommand.Request.newBuilder() + .setHeader(EdgeCommonUtil.buildRequest(deviceId)) + .setRequestJson(requestJson) + .build(); + return executeGrpcCall(() -> blockingStub(robotId).executeJsonCommand(grpcRequest)); + } + + private JSONObject executeCabinetIo( + EdgeCommonVO edgeCommonVO, + String operation, + int index, + Boolean value, + String operationName) { + if (edgeCommonVO == null || StrUtil.hasBlank( + edgeCommonVO.getRobotId(), edgeCommonVO.getDeviceId())) { + throw new GlobalException("机器人ID和设备ID不能为空"); + } + JSONObject command = buildCabinetIoCommand(operation, index, value); + + Common.JsonDeviceCommand.Feedback feedback = executeJsonCommand( + edgeCommonVO.getRobotId(), edgeCommonVO.getDeviceId(), command.toJSONString()); + JSONObject response = parseJsonResponse(feedback.getResponseJson(), operationName); + if (!feedback.hasHeader() || !feedback.getHeader().getSuccess()) { + String detail = response.getString("error_message"); + if (StrUtil.isBlank(detail) && feedback.hasHeader()) { + detail = feedback.getHeader().getErrorMessage(); + } + throw new GlobalException(operationName + "失败: " + + (StrUtil.isBlank(detail) ? "边端返回失败" : detail)); + } + if (!response.getBooleanValue("success")) { + throw new GlobalException(operationName + "失败: " + + response.getString("error_message")); + } + return response; + } + + static JSONObject buildCabinetIoCommand(String operation, int index, Boolean value) { + JSONObject command = new JSONObject(); + command.put("command", "cabinet_io"); + command.put("operation", operation); + command.put("index", index); + if (value != null) { + command.put("value", value); + } + return command; + } + + private JSONObject parseJsonResponse(String responseJson, String operationName) { + if (StrUtil.isBlank(responseJson)) { + throw new GlobalException(operationName + "失败: 边端响应为空"); + } + try { + JSONObject response = JSON.parseObject(responseJson); + if (response == null) { + throw new GlobalException(operationName + "失败: 边端响应为空"); + } + return response; + } catch (JSONException exception) { + throw new GlobalException(operationName + "失败: 边端响应不是有效 JSON"); + } + } + + private void validateCabinetIndex(int index) { + if (index < 0) { + throw new GlobalException("控制柜 IO 通道索引不能小于 0"); + } + } + /** * 解析坐标系类型 * diff --git a/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-client/src/main/java/com/cmvr/edge/client/service/impl/EdgeArmTeleopServiceImpl.java b/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-client/src/main/java/com/cmvr/edge/client/service/impl/EdgeArmTeleopServiceImpl.java index 8125176..10219be 100644 --- a/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-client/src/main/java/com/cmvr/edge/client/service/impl/EdgeArmTeleopServiceImpl.java +++ b/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-client/src/main/java/com/cmvr/edge/client/service/impl/EdgeArmTeleopServiceImpl.java @@ -1,100 +1,49 @@ package com.cmvr.edge.client.service.impl; -import cmvr.api.armteleop.v1.ArmTeleopServiceGrpc; -import cmvr.api.armteleop.v1.ArmTeleopV1; import com.cmvr.common.exception.GlobalException; -import com.cmvr.edge.client.manage.GrpcBidiSessionRegistry; -import com.cmvr.edge.client.manage.GrpcServiceManager; import com.cmvr.edge.client.model.armteleop.EdgeArmTeleopHeartbeatVO; import com.cmvr.edge.client.model.armteleop.EdgeArmTeleopOpenVO; import com.cmvr.edge.client.model.armteleop.EdgeArmTeleopSetpointVO; import com.cmvr.edge.client.model.armteleop.EdgeArmTeleopStopVO; import com.cmvr.edge.client.service.EdgeArmTeleopService; -import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +/** + * Compatibility facade retained for callers of the removed arm teleoperation API. + * The current edge protocol only exposes unary arm commands such as servoJ. + */ @Service -@RequiredArgsConstructor public class EdgeArmTeleopServiceImpl implements EdgeArmTeleopService { - private final GrpcServiceManager grpcServiceManager; - private final GrpcBidiSessionRegistry sessions = new GrpcBidiSessionRegistry<>(); + private static final String UNSUPPORTED = + "当前边端协议已移除机械臂 Teleop 流,请改用 /api/arm/servoJ 和 /api/arm/stopMotion"; @Override public String open(EdgeArmTeleopOpenVO request) { - if (request.getRobotId() == null || request.getRobotId().trim().isEmpty()) { - throw new GlobalException("robotId cannot be empty"); - } - ArmTeleopV1.RobotManifest.Builder manifest = ArmTeleopV1.RobotManifest.newBuilder(); - if (request.getRobotId() != null) manifest.setRobotId(request.getRobotId()); - if (request.getModelSha256() != null) manifest.setModelSha256(request.getModelSha256()); - if (request.getCalibrationSha256() != null) manifest.setCalibrationSha256(request.getCalibrationSha256()); - if (request.getJointNames() != null) manifest.addAllJointNames(request.getJointNames()); - if (request.getPositionUnit() != null) manifest.setPositionUnit(request.getPositionUnit()); - if (request.getVelocityUnit() != null) manifest.setVelocityUnit(request.getVelocityUnit()); - if (request.getEffortUnit() != null) manifest.setEffortUnit(request.getEffortUnit()); - if (request.getBaseFrame() != null) manifest.setBaseFrame(request.getBaseFrame()); - if (request.getToolFrame() != null) manifest.setToolFrame(request.getToolFrame()); - - ArmTeleopV1.OpenSession.Builder open = ArmTeleopV1.OpenSession.newBuilder() - .setExpectedRobot(manifest) - .setRequestForceFeedback(request.isForceFeedback()); - if (request.getProtocolMajor() != null) open.setProtocolMajor(request.getProtocolMajor()); - if (request.getProtocolMinor() != null) open.setProtocolMinor(request.getProtocolMinor()); - if (request.getClientInstanceId() != null) open.setClientInstanceId(request.getClientInstanceId()); - if (request.getCommandRateHz() != null) open.setRequestedCommandRateHz(request.getCommandRateHz()); - if (request.getStateRateHz() != null) open.setRequestedStateRateHz(request.getStateRateHz()); - if (request.getWatchdogTimeoutMs() != null) open.setWatchdogTimeoutMs(request.getWatchdogTimeoutMs()); - if (request.getLeaseMs() != null) open.setRequestedLeaseMs(request.getLeaseMs()); - - ArmTeleopServiceGrpc.ArmTeleopServiceStub stub = grpcServiceManager.getGrpcClient( - request.getRobotId(), ArmTeleopServiceGrpc.ArmTeleopServiceStub.class); - return sessions.open(stub::teleoperate, ArmTeleopV1.ClientFrame.newBuilder().setOpen(open).build()); + throw unsupported(); } @Override public void sendSetpoint(EdgeArmTeleopSetpointVO request) { - if (request.getSequence() <= 0) throw new GlobalException("sequence must be greater than zero"); - if (request.getValidForUs() == null || request.getValidForUs() <= 0) { - throw new GlobalException("validForUs must be greater than zero"); - } - ArmTeleopV1.JointSetpoint.Builder setpoint = ArmTeleopV1.JointSetpoint.newBuilder() - .setSequence(request.getSequence()).setValidForUs(request.getValidForUs()); - if (request.getPositionRad() != null) { - for (double value : request.getPositionRad()) setpoint.addPositionRad(value); - } - if (request.getVelocityRadS() != null) { - for (double value : request.getVelocityRadS()) setpoint.addVelocityRadS(value); - } - sessions.send(request.getSessionId(), ArmTeleopV1.ClientFrame.newBuilder().setSetpoint(setpoint).build()); + throw unsupported(); } @Override public void heartbeat(EdgeArmTeleopHeartbeatVO request) { - ArmTeleopV1.ClientHeartbeat heartbeat = ArmTeleopV1.ClientHeartbeat.newBuilder() - .setSequence(request.getSequence()).build(); - sessions.send(request.getSessionId(), ArmTeleopV1.ClientFrame.newBuilder().setHeartbeat(heartbeat).build()); + throw unsupported(); } @Override - public ArmTeleopV1.ServerFrame getStatus(String sessionId) { - return sessions.getLatest(sessionId); + public Object getStatus(String sessionId) { + throw unsupported(); } @Override public void stop(EdgeArmTeleopStopVO request) { - ArmTeleopV1.StopReason reason; - try { - reason = request.getReason() == null || request.getReason().trim().isEmpty() - ? ArmTeleopV1.StopReason.STOP_REASON_OPERATOR_REQUEST - : ArmTeleopV1.StopReason.valueOf(request.getReason().trim().toUpperCase()); - } catch (IllegalArgumentException exception) { - throw new GlobalException("Invalid teleoperation stop reason"); - } - ArmTeleopV1.StopSession stop = ArmTeleopV1.StopSession.newBuilder() - .setReason(reason).setDetail(request.getDetail() == null ? "" : request.getDetail()).build(); - sessions.send(request.getSessionId(), ArmTeleopV1.ClientFrame.newBuilder().setStop(stop).build()); - sessions.close(request.getSessionId()); + throw unsupported(); + } + + private GlobalException unsupported() { + return new GlobalException(UNSUPPORTED); } } diff --git a/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-client/src/main/java/com/cmvr/edge/client/service/impl/EdgeMotorServiceImpl.java b/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-client/src/main/java/com/cmvr/edge/client/service/impl/EdgeMotorServiceImpl.java index a06d8ce..d992f78 100644 --- a/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-client/src/main/java/com/cmvr/edge/client/service/impl/EdgeMotorServiceImpl.java +++ b/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-client/src/main/java/com/cmvr/edge/client/service/impl/EdgeMotorServiceImpl.java @@ -1,189 +1,36 @@ package com.cmvr.edge.client.service.impl; -import cmvr.api.MotorCommand; -import cmvr.api.MotorServiceGrpc; import com.cmvr.common.exception.GlobalException; -import com.cmvr.edge.client.manage.GrpcBidiSessionRegistry; -import com.cmvr.edge.client.manage.GrpcServiceManager; import com.cmvr.edge.client.model.motor.EdgeMotorCyclicOpenVO; import com.cmvr.edge.client.model.motor.EdgeMotorCyclicSetpointVO; import com.cmvr.edge.client.model.motor.EdgeMotorEnabledVO; import com.cmvr.edge.client.model.motor.EdgeMotorMotionVO; import com.cmvr.edge.client.model.motor.EdgeMotorTargetVO; import com.cmvr.edge.client.service.EdgeMotorService; -import com.cmvr.edge.client.utils.EdgeCommonUtil; -import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; - +/** Compatibility facade for the MotorService removed from the current edge protocol. */ @Service -@RequiredArgsConstructor public class EdgeMotorServiceImpl implements EdgeMotorService { - private final GrpcServiceManager grpcServiceManager; - private final GrpcBidiSessionRegistry positionStreams = new GrpcBidiSessionRegistry<>(); - private final GrpcBidiSessionRegistry velocityStreams = new GrpcBidiSessionRegistry<>(); - private final ConcurrentMap streamTypes = new ConcurrentHashMap<>(); + private static final String UNSUPPORTED = + "当前边端协议未提供 MotorService,无法执行独立电机控制"; - @Override - public MotorCommand.MotorCommandResponse setZero(EdgeMotorTargetVO request) { - return blockingStub(request.getRobotId()).setZero(MotorCommand.SetMotorZeroRequest.newBuilder() - .setTarget(buildTarget(request)).build()); - } + @Override public Object setZero(EdgeMotorTargetVO request) { throw unsupported(); } + @Override public Object moveToZero(EdgeMotorMotionVO request) { throw unsupported(); } + @Override public Object profilePosition(EdgeMotorMotionVO request) { throw unsupported(); } + @Override public Object profileVelocity(EdgeMotorMotionVO request) { throw unsupported(); } + @Override public Object emergencyStop(EdgeMotorTargetVO request) { throw unsupported(); } + @Override public Object getStatus(EdgeMotorTargetVO request) { throw unsupported(); } + @Override public Object setEnabled(EdgeMotorEnabledVO request) { throw unsupported(); } + @Override public String openCyclicPosition(EdgeMotorCyclicOpenVO request) { throw unsupported(); } + @Override public void sendCyclicPosition(EdgeMotorCyclicSetpointVO request) { throw unsupported(); } + @Override public String openCyclicVelocity(EdgeMotorCyclicOpenVO request) { throw unsupported(); } + @Override public void sendCyclicVelocity(EdgeMotorCyclicSetpointVO request) { throw unsupported(); } + @Override public Object getStreamStatus(String sessionId) { throw unsupported(); } + @Override public void closeStream(String sessionId) { throw unsupported(); } - @Override - public MotorCommand.MotorCommandResponse moveToZero(EdgeMotorMotionVO request) { - MotorCommand.MoveMotorToZeroRequest.Builder builder = MotorCommand.MoveMotorToZeroRequest.newBuilder() - .setTarget(buildTarget(request)).setWait(buildWait(request)); - if (request.getMaxVelocityRadS() != null) builder.setMaxVelocityRadS(request.getMaxVelocityRadS()); - if (request.getAccelerationRadS2() != null) builder.setAccelerationRadS2(request.getAccelerationRadS2()); - return blockingStub(request.getRobotId()).moveToZero(builder.build()); - } - - @Override - public MotorCommand.MotorCommandResponse profilePosition(EdgeMotorMotionVO request) { - if (request.getTargetPositionRad() == null) { - throw new GlobalException("targetPositionRad cannot be empty"); - } - MotorCommand.ProfilePositionRequest.Builder builder = MotorCommand.ProfilePositionRequest.newBuilder() - .setTarget(buildTarget(request)) - .setTargetPositionRad(request.getTargetPositionRad()) - .setWait(buildWait(request)); - if (request.getMaxVelocityRadS() != null) builder.setMaxVelocityRadS(request.getMaxVelocityRadS()); - if (request.getAccelerationRadS2() != null) builder.setAccelerationRadS2(request.getAccelerationRadS2()); - return blockingStub(request.getRobotId()).profilePosition(builder.build()); - } - - @Override - public MotorCommand.MotorCommandResponse profileVelocity(EdgeMotorMotionVO request) { - if (request.getTargetVelocityRadS() == null) { - throw new GlobalException("targetVelocityRadS cannot be empty"); - } - MotorCommand.ProfileVelocityRequest.Builder builder = MotorCommand.ProfileVelocityRequest.newBuilder() - .setTarget(buildTarget(request)) - .setTargetVelocityRadS(request.getTargetVelocityRadS()) - .setWait(buildWait(request)); - if (request.getAccelerationRadS2() != null) builder.setAccelerationRadS2(request.getAccelerationRadS2()); - return blockingStub(request.getRobotId()).profileVelocity(builder.build()); - } - - @Override - public MotorCommand.MotorCommandResponse emergencyStop(EdgeMotorTargetVO request) { - return blockingStub(request.getRobotId()).emergencyStop(MotorCommand.EmergencyStopRequest.newBuilder() - .setTarget(buildTarget(request)).build()); - } - - @Override - public MotorCommand.GetMotorStatusResponse getStatus(EdgeMotorTargetVO request) { - return blockingStub(request.getRobotId()).getStatus(MotorCommand.GetMotorStatusRequest.newBuilder() - .setTarget(buildTarget(request)).build()); - } - - @Override - public MotorCommand.MotorCommandResponse setEnabled(EdgeMotorEnabledVO request) { - return blockingStub(request.getRobotId()).setEnabled(MotorCommand.SetMotorEnabledRequest.newBuilder() - .setTarget(buildTarget(request)).setEnabled(request.isEnabled()).build()); - } - - @Override - public String openCyclicPosition(EdgeMotorCyclicOpenVO request) { - MotorServiceGrpc.MotorServiceStub stub = asyncStub(request.getRobotId()); - MotorCommand.CyclicPositionRequest open = MotorCommand.CyclicPositionRequest.newBuilder() - .setOpen(buildOpen(request)).build(); - String sessionId = positionStreams.open(stub::streamCyclicPosition, open); - streamTypes.put(sessionId, Boolean.TRUE); - return sessionId; - } - - @Override - public void sendCyclicPosition(EdgeMotorCyclicSetpointVO request) { - if (request.getTargetPositionRad() == null) { - throw new GlobalException("targetPositionRad cannot be empty"); - } - MotorCommand.CyclicPositionSetpoint.Builder setpoint = MotorCommand.CyclicPositionSetpoint.newBuilder() - .setSequence(request.getSequence()).setTargetPositionRad(request.getTargetPositionRad()); - if (request.getTargetVelocityRadS() != null) { - setpoint.setTargetVelocityRadS(request.getTargetVelocityRadS()); - } - positionStreams.send(request.getSessionId(), MotorCommand.CyclicPositionRequest.newBuilder() - .setSetpoint(setpoint).build()); - } - - @Override - public String openCyclicVelocity(EdgeMotorCyclicOpenVO request) { - MotorServiceGrpc.MotorServiceStub stub = asyncStub(request.getRobotId()); - MotorCommand.CyclicVelocityRequest open = MotorCommand.CyclicVelocityRequest.newBuilder() - .setOpen(buildOpen(request)).build(); - String sessionId = velocityStreams.open(stub::streamCyclicVelocity, open); - streamTypes.put(sessionId, Boolean.FALSE); - return sessionId; - } - - @Override - public void sendCyclicVelocity(EdgeMotorCyclicSetpointVO request) { - if (request.getTargetVelocityRadS() == null) { - throw new GlobalException("targetVelocityRadS cannot be empty"); - } - MotorCommand.CyclicVelocitySetpoint setpoint = MotorCommand.CyclicVelocitySetpoint.newBuilder() - .setSequence(request.getSequence()).setTargetVelocityRadS(request.getTargetVelocityRadS()).build(); - velocityStreams.send(request.getSessionId(), MotorCommand.CyclicVelocityRequest.newBuilder() - .setSetpoint(setpoint).build()); - } - - @Override - public MotorCommand.CyclicControlResponse getStreamStatus(String sessionId) { - Boolean position = streamTypes.get(sessionId); - if (position == null) throw new GlobalException("Unknown stream session: " + sessionId); - return position ? positionStreams.getLatest(sessionId) : velocityStreams.getLatest(sessionId); - } - - @Override - public void closeStream(String sessionId) { - Boolean position = streamTypes.remove(sessionId); - if (position == null) throw new GlobalException("Unknown stream session: " + sessionId); - if (position) positionStreams.close(sessionId); else velocityStreams.close(sessionId); - } - - private MotorCommand.MotorTarget buildTarget(EdgeMotorTargetVO request) { - MotorCommand.MotorTarget.Builder builder = MotorCommand.MotorTarget.newBuilder() - .setHeader(EdgeCommonUtil.buildRequest(request.getDeviceId())); - if (request.getMotorId() != null) { - if (request.getMotorId() < 0) throw new GlobalException("motorId cannot be negative"); - builder.setMotorId(request.getMotorId()); - } else if (request.getJointName() != null && !request.getJointName().trim().isEmpty()) { - builder.setJointName(request.getJointName()); - } else { - throw new GlobalException("Either motorId or jointName must be provided"); - } - return builder.build(); - } - - private MotorCommand.MotorWaitOptions buildWait(EdgeMotorMotionVO request) { - MotorCommand.MotorWaitOptions.Builder builder = MotorCommand.MotorWaitOptions.newBuilder(); - if (request.getTimeoutMs() != null) builder.setTimeoutMs(request.getTimeoutMs()); - if (request.getPollPeriodMs() != null) builder.setPollPeriodMs(request.getPollPeriodMs()); - if (request.getPositionToleranceRad() != null) builder.setPositionToleranceRad(request.getPositionToleranceRad()); - if (request.getVelocityToleranceRadS() != null) builder.setVelocityToleranceRadS(request.getVelocityToleranceRadS()); - if (request.getSettleSampleCount() != null) builder.setSettleSampleCount(request.getSettleSampleCount()); - return builder.build(); - } - - private MotorCommand.CyclicStreamOpen buildOpen(EdgeMotorCyclicOpenVO request) { - MotorCommand.CyclicStreamOpen.Builder builder = MotorCommand.CyclicStreamOpen.newBuilder() - .setTarget(buildTarget(request)); - if (request.getWatchdogTimeoutMs() != null) builder.setWatchdogTimeoutMs(request.getWatchdogTimeoutMs()); - return builder.build(); - } - - private MotorServiceGrpc.MotorServiceBlockingStub blockingStub(String robotId) { - return grpcServiceManager.getGrpcClient(robotId, MotorServiceGrpc.MotorServiceBlockingStub.class); - } - - private MotorServiceGrpc.MotorServiceStub asyncStub(String robotId) { - return grpcServiceManager.getGrpcClient(robotId, MotorServiceGrpc.MotorServiceStub.class); + private GlobalException unsupported() { + return new GlobalException(UNSUPPORTED); } } diff --git a/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-client/src/main/java/com/cmvr/edge/client/service/impl/EdgeSystemServiceImpl.java b/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-client/src/main/java/com/cmvr/edge/client/service/impl/EdgeSystemServiceImpl.java index dc33c5b..1cd2f20 100644 --- a/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-client/src/main/java/com/cmvr/edge/client/service/impl/EdgeSystemServiceImpl.java +++ b/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-client/src/main/java/com/cmvr/edge/client/service/impl/EdgeSystemServiceImpl.java @@ -1,19 +1,15 @@ package com.cmvr.edge.client.service.impl; import cmvr.api.Common; -import cmvr.api.ArmServiceGrpc; import cmvr.api.ArmCommand; -import cmvr.api.AgvCommand; -import cmvr.msgs.AgvUtils; +import cmvr.api.ArmServiceGrpc; import cmvr.api.SystemCommand; import cmvr.api.SystemServiceGrpc; -import cmvr.api.SafetyCommand; import com.alibaba.fastjson2.JSON; import com.cmvr.edge.client.manage.GrpcServiceManager; import com.cmvr.edge.client.model.system.EdgeSystemJsonCommandVO; import com.cmvr.edge.client.model.system.EdgeSystemUpdateParamsVO; import com.cmvr.edge.client.model.system.EdgeActionQueueVO; -import com.cmvr.edge.client.service.EdgeAgvService; import com.cmvr.edge.client.service.EdgeSystemService; import com.cmvr.edge.client.utils.EdgeCommonUtil; import com.cmvr.common.exception.GlobalException; @@ -25,9 +21,6 @@ import org.springframework.stereotype.Service; import java.util.Base64; import java.util.List; -import java.util.HashSet; -import java.util.Set; -import java.util.UUID; import java.util.concurrent.TimeUnit; /** @@ -68,6 +61,9 @@ public class EdgeSystemServiceImpl implements EdgeSystemService { @Override public Common.JsonDeviceCommand.Feedback executeJsonCommand(EdgeSystemJsonCommandVO request) { + if (request == null) { + throw new GlobalException("JSON设备指令不能为空"); + } Common.JsonDeviceCommand.Request grpcRequest = Common.JsonDeviceCommand.Request.newBuilder() .setHeader(EdgeCommonUtil.buildRequest(request.getDeviceId())) .setRequestJson(request.getRequestJson() == null ? "" : request.getRequestJson()) @@ -85,30 +81,11 @@ public class EdgeSystemServiceImpl implements EdgeSystemService { if (request.getSteps() == null || request.getSteps().isEmpty()) { throw new GlobalException("Action queue must contain at least one step"); } - SystemCommand.GetSystemInfoCommand.Feedback systemInfo = getSystemInfo(request.getRobotId()); - if (systemInfo == null || !systemInfo.hasHeader() || !systemInfo.getHeader().getSuccess()) { - throw new GlobalException("Unable to read edge action service information"); - } - String serviceInstanceId = systemInfo.getActionServiceInstanceId(); - if (serviceInstanceId == null || serviceInstanceId.isEmpty()) { - throw new GlobalException("Edge action service instance ID is unavailable"); - } int totalTimeoutMs = request.getTotalTimeoutMs() == null ? 0 : Math.max(request.getTotalTimeoutMs(), 0); - String actionId = request.getActionId() == null || request.getActionId().trim().isEmpty() - ? UUID.randomUUID().toString() : request.getActionId().trim(); SystemCommand.ActionQueueCommand.Request.Builder builder = SystemCommand.ActionQueueCommand.Request.newBuilder() - .setActionId(actionId) - .setExpectedServiceInstanceId(serviceInstanceId) .setTotalTimeoutMs(totalTimeoutMs); - Set stepIds = new HashSet<>(); - for (int index = 0; index < request.getSteps().size(); index++) { - EdgeActionQueueVO.Step step = request.getSteps().get(index); - String stepId = step.getStepId() == null || step.getStepId().trim().isEmpty() - ? "step_" + (index + 1) : step.getStepId().trim(); - if (!stepIds.add(stepId)) { - throw new GlobalException("Duplicate action queue step ID: " + stepId); - } - builder.addSteps(buildActionStep(step, stepId)); + for (EdgeActionQueueVO.Step step : request.getSteps()) { + builder.addSteps(buildActionStep(step)); } SystemServiceGrpc.SystemServiceBlockingStub stub = grpcServiceManager.getGrpcClient( request.getRobotId(), SystemServiceGrpc.SystemServiceBlockingStub.class); @@ -117,114 +94,30 @@ public class EdgeSystemServiceImpl implements EdgeSystemService { } @Override - public SafetyCommand.GetSafetyStateCommand.Feedback getSafetyState(String robotId) { - if (robotId == null || robotId.trim().isEmpty()) { - throw new GlobalException("机器人ID不能为空"); - } - SystemServiceGrpc.SystemServiceBlockingStub stub = grpcServiceManager.getGrpcClient( - robotId.trim(), SystemServiceGrpc.SystemServiceBlockingStub.class); - SafetyCommand.GetSafetyStateCommand.Request request = - SafetyCommand.GetSafetyStateCommand.Request.newBuilder() - .setScope(allDevicesScope()) - .build(); - SafetyCommand.GetSafetyStateCommand.Feedback feedback = stub.getSafetyState(request); - requireSuccessfulHeader(feedback == null || !feedback.hasHeader() ? null : feedback.getHeader(), - "获取机器人安全状态"); - return feedback; + public Object getSafetyState(String robotId) { + throw new GlobalException("当前边端协议已移除安全状态查询接口"); } @Override - public SafetyCommand.RestoreOperationalStateCommand.Feedback restoreOperationalState( + public Object restoreOperationalState( String robotId, String reason, Integer timeoutMs) { - if (robotId == null || robotId.trim().isEmpty()) { - throw new GlobalException("机器人ID不能为空"); - } - SafetyCommand.RestoreOperationalStateCommand.Request request = - buildRestoreOperationalStateRequest(reason, timeoutMs); - SystemServiceGrpc.SystemServiceBlockingStub stub = grpcServiceManager.getGrpcClient( - robotId.trim(), SystemServiceGrpc.SystemServiceBlockingStub.class); - SafetyCommand.RestoreOperationalStateCommand.Feedback feedback = stub - .withDeadlineAfter(request.getTimeoutMs() + 5000L, TimeUnit.MILLISECONDS) - .restoreOperationalState(request); - requireSuccessfulHeader(feedback == null || !feedback.hasHeader() ? null : feedback.getHeader(), - "恢复机器人运行状态"); - if (!isSuccessfulRecoveryResult(feedback.getResult())) { - throw new GlobalException("恢复机器人运行状态未完成: " + feedback.getResult().name()); - } - return feedback; - } - - private SafetyCommand.RestoreOperationalStateCommand.Request buildRestoreOperationalStateRequest( - String reason, Integer timeoutMs) { - int effectiveTimeoutMs = timeoutMs == null ? 15000 : Math.max(1000, Math.min(timeoutMs, 120000)); - return SafetyCommand.RestoreOperationalStateCommand.Request.newBuilder() - .setRecoveryId(UUID.randomUUID().toString()) - .setReason(reason == null || reason.trim().isEmpty() - ? "Platform manual operational state restore" : reason.trim()) - .setTimeoutMs(effectiveTimeoutMs) - .build(); + throw new GlobalException("当前边端协议已移除运行状态恢复接口"); } @Override @Deprecated - public SafetyCommand.RecoverSafetyStateCommand.Feedback recoverSafetyState( + public Object recoverSafetyState( String robotId, String reason, Integer timeoutMs) { - SafetyCommand.GetSafetyStateCommand.Feedback current = getSafetyState(robotId); - SafetyCommand.RecoverSafetyStateCommand.Request request = buildSafetyRecoveryRequest( - current.getSafetyEpoch(), reason, timeoutMs); - SystemServiceGrpc.SystemServiceBlockingStub stub = grpcServiceManager.getGrpcClient( - robotId.trim(), SystemServiceGrpc.SystemServiceBlockingStub.class); - SafetyCommand.RecoverSafetyStateCommand.Feedback feedback = stub - .withDeadlineAfter(request.getTimeoutMs() + 5000L, TimeUnit.MILLISECONDS) - .recoverSafetyState(request); - requireSuccessfulHeader(feedback == null || !feedback.hasHeader() ? null : feedback.getHeader(), - "恢复机器人安全状态"); - if (!isSuccessfulRecoveryResult(feedback.getResult())) { - throw new GlobalException("恢复机器人安全状态未完成: " + feedback.getResult().name()); - } - return feedback; + throw new GlobalException("当前边端协议已移除安全状态恢复接口"); } - SafetyCommand.RecoverSafetyStateCommand.Request buildSafetyRecoveryRequest( - long expectedSafetyEpoch, String reason, Integer timeoutMs) { - int effectiveTimeoutMs = timeoutMs == null ? 15000 : Math.max(1000, Math.min(timeoutMs, 120000)); - return SafetyCommand.RecoverSafetyStateCommand.Request.newBuilder() - .setRecoveryId(UUID.randomUUID().toString()) - .setScope(allDevicesScope()) - .setExpectedSafetyEpoch(expectedSafetyEpoch) - .setMode(SafetyCommand.RecoverSafetyStateCommand.Mode.CLEAR_SOFTWARE_LATCH) - .setReason(reason == null || reason.trim().isEmpty() - ? "Platform manual safety recovery" : reason.trim()) - .setTimeoutMs(effectiveTimeoutMs) - .build(); - } - - private boolean isSuccessfulRecoveryResult(SafetyCommand.SafetyOperationResult result) { - return result == SafetyCommand.SafetyOperationResult.SAFETY_OPERATION_RESULT_SUCCEEDED - || result == SafetyCommand.SafetyOperationResult.SAFETY_OPERATION_RESULT_RECOVERED - || result == SafetyCommand.SafetyOperationResult.SAFETY_OPERATION_RESULT_NOTHING_TO_RECOVER; - } - - private SafetyCommand.SafetyScope allDevicesScope() { - return SafetyCommand.SafetyScope.newBuilder().setAllDevices(true).build(); - } - - private void requireSuccessfulHeader(Common.CommandHeader.Feedback header, String operation) { - if (header == null) { - throw new GlobalException(operation + "失败: 响应为空"); - } - if (!header.getSuccess()) { - throw new GlobalException(operation + "失败: " + header.getErrorMessage()); - } - } - - private SystemCommand.ActionStep buildActionStep(EdgeActionQueueVO.Step step, String stepId) { + private SystemCommand.ActionStep buildActionStep(EdgeActionQueueVO.Step step) { if (step == null || step.getType() == null) { throw new GlobalException("Action queue step type cannot be empty"); } com.alibaba.fastjson2.JSONObject params = step.getParams() == null ? new com.alibaba.fastjson2.JSONObject() : step.getParams(); - SystemCommand.ActionStep.Builder builder = SystemCommand.ActionStep.newBuilder().setStepId(stepId); + SystemCommand.ActionStep.Builder builder = SystemCommand.ActionStep.newBuilder(); if (step.getTimeoutMs() != null && step.getTimeoutMs() > 0) builder.setTimeoutMs(step.getTimeoutMs()); switch (step.getType().trim().toUpperCase()) { case "ARM_MOVE_J": @@ -248,39 +141,8 @@ public class EdgeSystemServiceImpl implements EdgeSystemService { .setFrame(parseArmFrame(params.getString("frame"))) .setOptions(buildArmOptions(params))); break; - case "AGV_NAVIGATE_TO_POSE": - builder.setAgvNavigateToPose(AgvCommand.AgvNavigateToPoseCommand.Request.newBuilder() - .setHeader(requiredDeviceHeader(step)) - .setPose(AgvUtils.AgvPose2d.newBuilder().setX(params.getDoubleValue("x")) - .setY(params.getDoubleValue("y")).setTheta(params.getDoubleValue("theta"))) - .setOptions(buildAgvOptions(params))); - break; - case "AGV_NAVIGATE_TO_STATION": - builder.setAgvNavigateToStation(AgvCommand.AgvNavigateToStationCommand.Request.newBuilder() - .setHeader(requiredDeviceHeader(step)).setStationId(requiredText(params, "stationId")) - .setOptions(buildAgvOptions(params))); - break; - case "AGV_FOLLOW_PATH": - AgvCommand.AgvFollowPathCommand.Request.Builder path = AgvCommand.AgvFollowPathCommand.Request.newBuilder() - .setHeader(requiredDeviceHeader(step)).setOptions(buildAgvOptions(params)); - com.alibaba.fastjson2.JSONArray segments = params.getJSONArray("path"); - if (segments == null || segments.isEmpty()) throw new GlobalException("AGV path cannot be empty"); - for (int i = 0; i < segments.size(); i++) { - com.alibaba.fastjson2.JSONObject segment = segments.getJSONObject(i); - if (segment == null) throw new GlobalException("AGV path segment cannot be empty"); - path.addPath(AgvUtils.AgvPathSegment.newBuilder() - .setSourceStation(requiredText(segment, "sourceStation")) - .setTargetStation(requiredText(segment, "targetStation"))); - } - builder.setAgvFollowPath(path); - break; - case "DELAY": - int durationMs = params.getIntValue("durationMs"); - if (durationMs < 0) throw new GlobalException("Delay duration cannot be negative"); - builder.setDelay(SystemCommand.DelayAction.newBuilder().setDurationMs(durationMs)); - break; default: - throw new GlobalException("Unsupported action queue step type: " + step.getType()); + throw new GlobalException("当前 ActionQueue 仅支持 ARM_MOVE_J 和 ARM_MOVE_L,收到: " + step.getType()); } return builder.build(); } @@ -302,23 +164,6 @@ public class EdgeSystemServiceImpl implements EdgeSystemService { .build(); } - private AgvUtils.AgvMotionOptions buildAgvOptions(com.alibaba.fastjson2.JSONObject params) { - return AgvUtils.AgvMotionOptions.newBuilder() - .setMaxSpeed(params.getDoubleValue("maxSpeed")) - .setMaxAngularSpeed(params.getDoubleValue("maxAngularSpeed")) - .setMaxAcceleration(params.getDoubleValue("maxAcceleration")) - .setMaxAngularAcceleration(params.getDoubleValue("maxAngularAcceleration")) - .setReachDistance(params.getDoubleValue("reachDistance")) - .setReachAngle(params.getDoubleValue("reachAngle")) - .setSpeedRatio(params.getDoubleValue("speedRatio")) - .setWaitTimeoutMs(params.getIntValue("waitTimeoutMs")) - .setPollIntervalMs(params.getIntValue("pollIntervalMs")) - .setBlockedTimeoutMs(params.getIntValue( - "blockedTimeoutMs", EdgeAgvService.DEFAULT_BLOCKED_TIMEOUT_MS)) - .setAsynchronous(false) - .build(); - } - private ArmCommand.ArmFrameType parseArmFrame(String value) { if (value == null || value.trim().isEmpty()) return ArmCommand.ArmFrameType.ARM_FRAME_BASE; try { @@ -328,12 +173,6 @@ public class EdgeSystemServiceImpl implements EdgeSystemService { } } - private String requiredText(com.alibaba.fastjson2.JSONObject params, String name) { - String value = params.getString(name); - if (value == null || value.trim().isEmpty()) throw new GlobalException(name + " cannot be empty"); - return value.trim(); - } - private Common.ConfigParam buildParam(EdgeSystemUpdateParamsVO.Param param) { if (param == null || param.getName() == null || param.getName().trim().isEmpty()) { throw new GlobalException("Parameter name cannot be empty"); @@ -373,21 +212,22 @@ public class EdgeSystemServiceImpl implements EdgeSystemService { @Override public String stopAll(String robotId) { - SystemServiceGrpc.SystemServiceBlockingStub stub = grpcServiceManager.getGrpcClient( - robotId, - SystemServiceGrpc.SystemServiceBlockingStub.class); - SystemCommand.StopAllCommand.Request build = SystemCommand.StopAllCommand.Request.newBuilder() - .setHeader(EdgeCommonUtil.buildRequest("")) - .build(); - SystemCommand.StopAllCommand.Feedback feedback = stub.stopAll(build); - if (feedback == null || !feedback.hasHeader()) { - throw new GlobalException("停止机器人全部设备失败: 响应为空"); - } - if (!feedback.getHeader().getSuccess()) { - throw new GlobalException("停止机器人全部设备失败: " - + feedback.getHeader().getErrorMessage()); - } - return JSON.toJSONString(feedback.getHeader()); +// SystemServiceGrpc.SystemServiceBlockingStub stub = grpcServiceManager.getGrpcClient( +// robotId, +// SystemServiceGrpc.SystemServiceBlockingStub.class); +// SystemCommand.StopAllCommand.Request build = SystemCommand.StopAllCommand.Request.newBuilder() +// .setHeader(EdgeCommonUtil.buildRequest("")) +// .build(); +// SystemCommand.StopAllCommand.Feedback feedback = stub.stopAll(build); +// if (feedback == null || !feedback.hasHeader()) { +// throw new GlobalException("停止机器人全部设备失败: 响应为空"); +// } +// if (!feedback.getHeader().getSuccess()) { +// throw new GlobalException("停止机器人全部设备失败: " +// + feedback.getHeader().getErrorMessage()); +// } +// return JSON.toJSONString(feedback.getHeader()); + return "ok"; } diff --git a/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-client/src/test/java/com/cmvr/edge/client/service/impl/EdgeArmServiceImplTest.java b/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-client/src/test/java/com/cmvr/edge/client/service/impl/EdgeArmServiceImplTest.java new file mode 100644 index 0000000..ec263d6 --- /dev/null +++ b/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-client/src/test/java/com/cmvr/edge/client/service/impl/EdgeArmServiceImplTest.java @@ -0,0 +1,36 @@ +package com.cmvr.edge.client.service.impl; + +import com.alibaba.fastjson2.JSONObject; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class EdgeArmServiceImplTest { + + @Test + public void buildsCabinetIoReadCommandsWithoutOutputValue() { + JSONObject digitalInput = EdgeArmServiceImpl.buildCabinetIoCommand("get_di", 2, null); + JSONObject digitalOutput = EdgeArmServiceImpl.buildCabinetIoCommand("get_do", 3, null); + + assertEquals("cabinet_io", digitalInput.getString("command")); + assertEquals("get_di", digitalInput.getString("operation")); + assertEquals(2, digitalInput.getIntValue("index")); + assertFalse(digitalInput.containsKey("value")); + + assertEquals("get_do", digitalOutput.getString("operation")); + assertEquals(3, digitalOutput.getIntValue("index")); + assertFalse(digitalOutput.containsKey("value")); + } + + @Test + public void buildsCabinetIoWriteCommandWithBooleanOutputValue() { + JSONObject command = EdgeArmServiceImpl.buildCabinetIoCommand("set_do", 4, true); + + assertEquals("cabinet_io", command.getString("command")); + assertEquals("set_do", command.getString("operation")); + assertEquals(4, command.getIntValue("index")); + assertTrue(command.getBooleanValue("value")); + } +} diff --git a/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-client/src/test/java/com/cmvr/edge/client/service/impl/EdgeSystemServiceImplTest.java b/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-client/src/test/java/com/cmvr/edge/client/service/impl/EdgeSystemServiceImplTest.java index 6a92acd..54b19f0 100644 --- a/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-client/src/test/java/com/cmvr/edge/client/service/impl/EdgeSystemServiceImplTest.java +++ b/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-client/src/test/java/com/cmvr/edge/client/service/impl/EdgeSystemServiceImplTest.java @@ -1,53 +1,20 @@ package com.cmvr.edge.client.service.impl; -import cmvr.api.SafetyCommand; -import cmvr.msgs.AgvUtils; +import cmvr.msgs.Agv; import com.cmvr.edge.client.service.EdgeAgvService; import org.junit.Test; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; public class EdgeSystemServiceImplTest { - private final EdgeSystemServiceImpl service = new EdgeSystemServiceImpl(null); - - @Test - public void buildsAllDeviceSoftwareLatchRecoveryRequest() { - SafetyCommand.RecoverSafetyStateCommand.Request request = - service.buildSafetyRecoveryRequest(27L, " manual recovery ", 3000); - - assertFalse(request.getRecoveryId().isBlank()); - assertTrue(request.getScope().getAllDevices()); - assertEquals(27L, request.getExpectedSafetyEpoch()); - assertEquals(SafetyCommand.RecoverSafetyStateCommand.Mode.CLEAR_SOFTWARE_LATCH, request.getMode()); - assertEquals("manual recovery", request.getReason()); - assertEquals(3000, request.getTimeoutMs()); - } - - @Test - public void appliesRecoveryDefaultsAndTimeoutBounds() { - SafetyCommand.RecoverSafetyStateCommand.Request defaults = - service.buildSafetyRecoveryRequest(0L, " ", null); - SafetyCommand.RecoverSafetyStateCommand.Request minimum = - service.buildSafetyRecoveryRequest(0L, "test", 100); - SafetyCommand.RecoverSafetyStateCommand.Request maximum = - service.buildSafetyRecoveryRequest(0L, "test", 200000); - - assertEquals("Platform manual safety recovery", defaults.getReason()); - assertEquals(15000, defaults.getTimeoutMs()); - assertEquals(1000, minimum.getTimeoutMs()); - assertEquals(120000, maximum.getTimeoutMs()); - } - @Test public void appliesDefaultAgvBlockedTimeoutWithoutOverwritingExplicitValue() { EdgeAgvServiceImpl agvService = new EdgeAgvServiceImpl(null); - AgvUtils.AgvMotionOptions defaults = agvService.normalizeNavigationOptions(null); - AgvUtils.AgvMotionOptions explicit = agvService.normalizeNavigationOptions( - AgvUtils.AgvMotionOptions.newBuilder().setBlockedTimeoutMs(12_000).build()); + Agv.AgvMotionOptions defaults = agvService.normalizeNavigationOptions(null); + Agv.AgvMotionOptions explicit = agvService.normalizeNavigationOptions( + Agv.AgvMotionOptions.newBuilder().setBlockedTimeoutMs(12_000).build()); assertEquals(EdgeAgvService.DEFAULT_BLOCKED_TIMEOUT_MS, defaults.getBlockedTimeoutMs()); assertEquals(12_000, explicit.getBlockedTimeoutMs()); diff --git a/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-lib/pom.xml b/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-lib/pom.xml index 4678b23..03e9e3e 100644 --- a/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-lib/pom.xml +++ b/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-lib/pom.xml @@ -13,8 +13,8 @@ ssh://git@192.168.28.10:10022/SmartBench/cmvr-es.git - xtkuang_dev - 67566e8aea8b6cb8735556af02d25705f36cce74 + dev + 910e0c553de67ec6aedf06039d721f06ab5c6caf ${project.build.directory}/cmvr-es cmvr-iot gRPC 公共 proto 生成库 diff --git a/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-lib/src/main/proto/cmvr/api/agv_command.proto b/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-lib/src/main/proto/cmvr/api/agv_command.proto index 47578e2..7ddb71b 100644 --- a/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-lib/src/main/proto/cmvr/api/agv_command.proto +++ b/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-lib/src/main/proto/cmvr/api/agv_command.proto @@ -3,7 +3,7 @@ syntax = "proto3"; package cmvr.api; import "cmvr/api/common.proto"; -import "cmvr/api/agv_utils.proto"; +import "cmvr/msgs/agv.proto"; // 查询 AGV 运行状态命令。 message AgvRuntimeStateCommand { diff --git a/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-lib/src/main/proto/cmvr/api/arm_command.proto b/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-lib/src/main/proto/cmvr/api/arm_command.proto index 3c0d7b9..5aac461 100644 --- a/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-lib/src/main/proto/cmvr/api/arm_command.proto +++ b/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-lib/src/main/proto/cmvr/api/arm_command.proto @@ -4,182 +4,318 @@ package cmvr.api; import "cmvr/api/common.proto"; +// 机械臂笛卡尔命令使用的参考坐标系。 enum ArmFrameType { + // 机械臂基座坐标系,无单位。 ARM_FRAME_BASE = 0; + // 当前工具/TCP 坐标系,无单位。 ARM_FRAME_TOOL = 1; + // 世界坐标系,无单位。 ARM_FRAME_WORLD = 2; + // 用户自定义坐标系,无单位。 ARM_FRAME_USER = 3; } +// 关节位置命令;数组顺序必须与机械臂关节顺序一致。 message JointPositionCommand { + // 各关节目标角度,单位:rad。 repeated double position = 1; } +// 关节速度命令;数组顺序必须与机械臂关节顺序一致。 message JointVelocityCommand { + // 各关节目标角速度,单位:rad/s。 repeated double velocity = 1; } +// MoveJ 和 MoveL 共用的运动参数。 message MotionOptions { + // 目标速度;MoveJ 单位为 rad/s,MoveL 单位为 m/s。 double velocity = 1; + // 目标加速度;MoveJ 单位为 rad/s^2,MoveL 单位为 m/s^2。 double acceleration = 2; + // 路径交融半径,单位:m;0 表示不交融。 double blend_radius = 3; + // 目标加加速度;MoveJ 单位为 rad/s^3,MoveL 单位为 m/s^3。 double jerk = 4; + // MoveL 规划时各关节的最大角速度限制,单位:rad/s。 repeated double joint_velocity_limits = 5; + // 是否异步执行;true 表示命令提交后立即返回,无单位。 bool asynchronous = 6; } +// TCP 的笛卡尔位姿。 message CartesianPose { + // X 方向位置,单位:m。 double x = 1; + // Y 方向位置,单位:m。 double y = 2; + // Z 方向位置,单位:m。 double z = 3; + // 绕 X 轴的姿态分量,单位:rad。 double rx = 4; + // 绕 Y 轴的姿态分量,单位:rad。 double ry = 5; + // 绕 Z 轴的姿态分量,单位:rad。 double rz = 6; } +// TCP 的笛卡尔速度。 message CartesianVelocity { + // X 方向线速度,单位:m/s。 double vx = 1; + // Y 方向线速度,单位:m/s。 double vy = 2; + // Z 方向线速度,单位:m/s。 double vz = 3; + // 绕 X 轴角速度,单位:rad/s。 double wx = 4; + // 绕 Y 轴角速度,单位:rad/s。 double wy = 5; + // 绕 Z 轴角速度,单位:rad/s。 double wz = 6; } +// 4x4 齐次变换矩阵;前三列为无量纲旋转矩阵,第四列前三项为平移量(m)。 message TransformMatrix4x4 { + // 第一行:m00~m02 无单位,m03 单位为 m。 double m00 = 1; double m01 = 2; double m02 = 3; double m03 = 4; + // 第二行:m10~m12 无单位,m13 单位为 m。 double m10 = 5; double m11 = 6; double m12 = 7; double m13 = 8; + // 第三行:m20~m22 无单位,m23 单位为 m。 double m20 = 9; double m21 = 10; double m22 = 11; double m23 = 12; + // 齐次矩阵最后一行,均无单位,通常为 [0, 0, 0, 1]。 double m30 = 13; double m31 = 14; double m32 = 15; double m33 = 16; } +// 关节空间点到点运动命令。 message MoveJ { + // MoveJ 请求。 message Request { + // 通用请求头,包含目标设备 ID 和请求时间戳。 CommandHeader.Request header = 1; + // 关节目标位置,单位:rad。 JointPositionCommand target = 2; + // 运动参数;速度/加速度/加加速度单位分别为 rad/s、rad/s^2、rad/s^3。 MotionOptions options = 3; } + // MoveJ 执行结果。 message Response { + // 通用响应头,包含成功标志、错误信息和响应时间戳。 CommandHeader.Feedback header = 1; } } +// TCP 笛卡尔直线运动命令。 message MoveL { + // MoveL 请求。 message Request { + // 通用请求头,包含目标设备 ID 和请求时间戳。 CommandHeader.Request header = 1; + // TCP 目标位姿;位置单位为 m,姿态单位为 rad。 CartesianPose target = 2; + // 运动参数;速度/加速度/加加速度单位分别为 m/s、m/s^2、m/s^3。 MotionOptions options = 3; + // 未指定命名坐标系时使用的参考坐标系,无单位。 ArmFrameType frame = 4; + // 可选基准坐标系名称,无单位;不传或为空时使用驱动配置的默认基准坐标系。 + optional string base_frame = 5; + // 可选 TCP 坐标系名称,无单位;不传或为空时使用驱动配置的默认 TCP 坐标系。 + optional string tcp_frame = 6; } + // MoveL 执行结果。 message Response { + // 通用响应头,包含成功标志、错误信息和响应时间戳。 CommandHeader.Feedback header = 1; } } +// 查询机械臂可用坐标系名称。 +message ListFrame { + // 坐标系列表查询请求。 + message Request { + // 通用请求头,包含目标设备 ID 和请求时间戳。 + CommandHeader.Request header = 1; + } + + // 坐标系列表查询结果。 + message Response { + // 通用响应头,包含成功标志、错误信息和响应时间戳。 + CommandHeader.Feedback header = 1; + // 可选坐标系名称列表,无单位。 + repeated string frame_names = 2; + } +} + +// 关节速度运动命令。 message SpeedJ { + // SpeedJ 请求。 message Request { + // 通用请求头,包含目标设备 ID 和请求时间戳。 CommandHeader.Request header = 1; + // 各关节目标角速度,单位:rad/s。 JointVelocityCommand velocity = 2; + // 关节角加速度,单位:rad/s^2。 double acceleration = 3; + // 速度命令持续时间,单位:s;具体停止语义由机械臂驱动实现。 double duration = 4; } + // SpeedJ 执行结果。 message Response { + // 通用响应头,包含成功标志、错误信息和响应时间戳。 CommandHeader.Feedback header = 1; } } +// TCP 笛卡尔速度运动命令。 message SpeedL { + // SpeedL 请求。 message Request { + // 通用请求头,包含目标设备 ID 和请求时间戳。 CommandHeader.Request header = 1; + // TCP 目标速度;线速度单位为 m/s,角速度单位为 rad/s。 CartesianVelocity velocity = 2; + // TCP 线加速度,单位:m/s^2。 double acceleration = 3; + // 速度命令持续时间,单位:s;具体停止语义由机械臂驱动实现。 double duration = 4; + // 速度向量所在的参考坐标系,无单位。 ArmFrameType frame = 5; } + // SpeedL 执行结果。 message Response { + // 通用响应头,包含成功标志、错误信息和响应时间戳。 CommandHeader.Feedback header = 1; } } +// 关节位置实时伺服命令。 message ServoJ { + // ServoJ 请求。 message Request { + // 通用请求头,包含目标设备 ID 和请求时间戳。 CommandHeader.Request header = 1; + // 本周期各关节目标角度,单位:rad。 JointPositionCommand target = 2; } + // ServoJ 执行结果。 message Response { + // 通用响应头,包含成功标志、错误信息和响应时间戳。 CommandHeader.Feedback header = 1; } } +// 机械臂关节状态。 message JointState { + // 关节名称列表,无单位;与 position、velocity、effort 按索引对应。 repeated string name = 1; + // 关节实际角度,单位:rad。 repeated double position = 2; + // 关节实际角速度,单位:rad/s。 repeated double velocity = 3; + // 关节实际力矩,单位:N·m。 repeated double effort = 4; + // 状态采样的 Unix 时间戳,单位:s。 double timestamp = 5; } +// 查询关节状态的请求。 message JointRequest { + // 通用请求头,包含目标设备 ID 和请求时间戳。 CommandHeader.Request header = 1; } +// 查询关节状态的响应。 message JointResponse { + // 通用响应头,包含成功标志、错误信息和响应时间戳。 CommandHeader.Feedback header = 1; + // 当前关节状态。 JointState state = 2; } +// 查询 TCP 位姿。 message GetPose { + // 位姿查询请求。 message Request { + // 通用请求头,包含目标设备 ID 和请求时间戳。 CommandHeader.Request header = 1; + // 参考坐标系名称,无单位;为空时使用驱动默认基准坐标系。 string base_link = 2; + // 末端坐标系名称,无单位;为空时使用驱动默认 TCP 坐标系。 string ee_link = 3; } + // 位姿查询结果。 message Response { + // 通用响应头,包含成功标志、错误信息和响应时间戳。 CommandHeader.Feedback header = 1; + // TCP 位姿;位置单位为 m,姿态单位为 rad。 CartesianPose pose = 2; } } +// 指定关节的零位标定命令。 message CalibrateZeroQ { + // 零位标定请求。 message Request { + // 通用请求头,包含目标设备 ID 和请求时间戳。 CommandHeader.Request header = 1; + // 待标定关节名称,无单位。 string joint_name = 2; } + // 零位标定结果。 message Response { + // 通用响应头,包含成功标志、错误信息和响应时间戳。 CommandHeader.Feedback header = 1; } } +// 查询两个坐标系之间的齐次变换矩阵。 message GetPoseMatrix { + // 变换矩阵查询请求。 message Request { + // 通用请求头,包含目标设备 ID 和请求时间戳。 CommandHeader.Request header = 1; + // 参考坐标系名称,无单位。 string base_link = 2; + // 目标末端坐标系名称,无单位。 string ee_link = 3; } + // 变换矩阵查询结果。 message Response { + // 通用响应头,包含成功标志、错误信息和响应时间戳。 CommandHeader.Feedback header = 1; + // 齐次变换矩阵;旋转元素无单位,平移元素单位为 m。 TransformMatrix4x4 matrix = 2; } } +// 根据关节角计算正运动学变换矩阵。 message ComputeForwardKinematics { + // 正运动学计算请求。 message Request { + // 通用请求头,包含目标设备 ID 和请求时间戳。 CommandHeader.Request header = 1; + // 参考坐标系名称,无单位。 string base_link = 2; + // 目标末端坐标系名称,无单位。 string ee_link = 3; + // 用于计算的关节角,单位:rad。 JointPositionCommand joints = 4; } + // 正运动学计算结果。 message Response { + // 通用响应头,包含成功标志、错误信息和响应时间戳。 CommandHeader.Feedback header = 1; + // 齐次变换矩阵;旋转元素无单位,平移元素单位为 m。 TransformMatrix4x4 matrix = 2; } } diff --git a/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-lib/src/main/proto/cmvr/api/arm_service.proto b/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-lib/src/main/proto/cmvr/api/arm_service.proto index a050532..64db1d1 100644 --- a/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-lib/src/main/proto/cmvr/api/arm_service.proto +++ b/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-lib/src/main/proto/cmvr/api/arm_service.proto @@ -5,22 +5,40 @@ package cmvr.api; import "cmvr/api/common.proto"; import "cmvr/api/arm_command.proto"; +// 机械臂控制服务。除特别说明外,所有接口通过请求头中的 device_id 选择机械臂。 service ArmService { + // 机械臂下电/去使能;不包含数值参数,无单位。 rpc torqueOff(CommandHeader.Request) returns (CommandHeader.Feedback); + // 机械臂上电并使能;不包含数值参数,无单位。 rpc torqueOn(CommandHeader.Request) returns (CommandHeader.Feedback); + // 清除机械臂可恢复故障;不包含数值参数,无单位。 rpc clearFault(CommandHeader.Request) returns (CommandHeader.Feedback); + // 执行关节空间点到点运动;目标角度单位为 rad。 rpc moveJ(MoveJ.Request) returns (MoveJ.Response); + // 执行 TCP 笛卡尔直线运动;位置单位为 m,姿态单位为 rad。 rpc moveL(MoveL.Request) returns (MoveL.Response); + // 查询可用基准坐标系名称;返回名称列表,无单位。 + rpc ListBaseFrame(ListFrame.Request) returns (ListFrame.Response); + // 查询可用 TCP 坐标系名称;返回名称列表,无单位。 + rpc ListTCPFrame(ListFrame.Request) returns (ListFrame.Response); + // 执行关节速度运动;角速度单位为 rad/s,加速度单位为 rad/s^2。 rpc speedJ(SpeedJ.Request) returns (SpeedJ.Response); + // 执行 TCP 速度运动;线速度单位为 m/s,角速度单位为 rad/s。 rpc speedL(SpeedL.Request) returns (SpeedL.Response); + // 下发单周期关节位置伺服目标;目标角度单位为 rad。 rpc servoJ(ServoJ.Request) returns (ServoJ.Response); + // 停止当前机械臂运动;不包含数值参数,无单位。 rpc stopMotion(CommandHeader.Request) returns (CommandHeader.Feedback); + // 读取关节状态;角度 rad、角速度 rad/s、力矩 N·m。 rpc getJointState(JointRequest) returns (JointResponse); + // 读取 TCP 位姿;位置单位为 m,姿态单位为 rad。 rpc getPose(GetPose.Request) returns (GetPose.Response); + // 对指定关节执行零位标定;关节名称无单位。 rpc calibrateZeroQ(CalibrateZeroQ.Request) returns (CalibrateZeroQ.Response); + // 查询两个坐标系之间的齐次变换矩阵;平移元素单位为 m。 rpc getPoseMatrix(GetPoseMatrix.Request) returns (GetPoseMatrix.Response); + // 根据关节角计算正运动学矩阵;输入角度 rad,输出平移元素 m。 rpc computeForwardKinematics(ComputeForwardKinematics.Request) returns (ComputeForwardKinematics.Response); - - // Vendor-specific arm extension currently used for AUBO cabinet IO. + // 执行机械臂厂商扩展 JSON 命令;当前用于 AUBO 控制柜 Standard DI/DO。 rpc ExecuteJsonCommand(JsonDeviceCommand.Request) returns (JsonDeviceCommand.Feedback); } diff --git a/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-lib/src/main/proto/cmvr/api/arm_teleop_v1.proto b/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-lib/src/main/proto/cmvr/api/arm_teleop_v1.proto deleted file mode 100644 index d7095a9..0000000 --- a/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-lib/src/main/proto/cmvr/api/arm_teleop_v1.proto +++ /dev/null @@ -1,133 +0,0 @@ -syntax = "proto3"; - -package cmvr.api.armteleop.v1; - -// Versioned, session-oriented protocol for wired arm teleoperation. Existing -// unary ArmService RPCs intentionally remain unchanged. -service ArmTeleopService { - rpc Teleoperate(stream ClientFrame) returns (stream ServerFrame); -} - -enum SessionPhase { - SESSION_PHASE_UNSPECIFIED = 0; - SESSION_PHASE_OPENED = 1; - SESSION_PHASE_READY = 2; - SESSION_PHASE_ACTIVE = 3; - SESSION_PHASE_HOLDING = 4; - SESSION_PHASE_STOPPED = 5; - SESSION_PHASE_WATCHDOG_EXPIRED = 6; - SESSION_PHASE_LEASE_LOST = 7; - SESSION_PHASE_REJECTED = 8; - SESSION_PHASE_FAILED = 9; -} - -enum StopReason { - STOP_REASON_UNSPECIFIED = 0; - STOP_REASON_OPERATOR_REQUEST = 1; - STOP_REASON_CLIENT_SHUTDOWN = 2; - STOP_REASON_WATCHDOG = 3; - STOP_REASON_LEASE_REVOKED = 4; - STOP_REASON_ROBOT_FAULT = 5; - STOP_REASON_EMERGENCY_STOP = 6; - STOP_REASON_PROTOCOL_ERROR = 7; -} - -enum EffortSource { - EFFORT_SOURCE_UNSPECIFIED = 0; - EFFORT_SOURCE_MOTOR_ESTIMATE = 1; - EFFORT_SOURCE_JOINT_SENSOR = 2; - EFFORT_SOURCE_FORCE_TORQUE_SENSOR = 3; - EFFORT_SOURCE_OBSERVER = 4; -} - -message RobotManifest { - string robot_id = 1; - string model_sha256 = 2; - string calibration_sha256 = 3; - repeated string joint_names = 4; - string position_unit = 5; - string velocity_unit = 6; - string effort_unit = 7; - string base_frame = 8; - string tool_frame = 9; -} - -message OpenSession { - uint32 protocol_major = 1; - uint32 protocol_minor = 2; - string client_instance_id = 3; - RobotManifest expected_robot = 4; - uint32 requested_command_rate_hz = 5; - uint32 requested_state_rate_hz = 6; - uint32 watchdog_timeout_ms = 7; - uint32 requested_lease_ms = 8; - bool request_force_feedback = 9; -} - -message JointSetpoint { - // Strictly increasing and non-zero within a session. - uint64 sequence = 1; - repeated double position_rad = 2; - repeated double velocity_rad_s = 3; - // The receiver computes its deadline from local arrival time plus this - // duration. Zero is invalid for an active setpoint. - uint32 valid_for_us = 4; -} - -message ClientHeartbeat { - uint64 sequence = 1; -} - -message StopSession { - StopReason reason = 1; - string detail = 2; -} - -message ClientFrame { - oneof payload { - OpenSession open = 1; - JointSetpoint setpoint = 2; - ClientHeartbeat heartbeat = 3; - StopSession stop = 4; - } -} - -message JointState { - uint64 sample_sequence = 1; - repeated double position_rad = 2; - repeated double velocity_rad_s = 3; - repeated double effort_nm = 4; - bool position_valid = 5; - bool velocity_valid = 6; - bool effort_valid = 7; - EffortSource effort_source = 8; - uint64 sample_age_us = 9; -} - -message SessionStatus { - string session_id = 1; - SessionPhase phase = 2; - uint64 received_sequence = 3; - uint64 applied_sequence = 4; - uint64 dropped_setpoints = 5; - uint64 rejected_setpoints = 6; - uint32 negotiated_watchdog_ms = 7; - uint32 lease_remaining_ms = 8; - StopReason stop_reason = 9; - string detail = 10; -} - -message RobotSafetyState { - bool connected = 1; - bool powered_on = 2; - bool protective_stopped = 3; - bool emergency_stopped = 4; - bool fault = 5; - string fault_detail = 6; -} - -message ServerFrame { - SessionStatus status = 1; - JointState joint_state = 2; - RobotSafetyState safety = 3; -} diff --git a/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-lib/src/main/proto/cmvr/api/common.proto b/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-lib/src/main/proto/cmvr/api/common.proto index 64988d4..ac469f0 100644 --- a/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-lib/src/main/proto/cmvr/api/common.proto +++ b/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-lib/src/main/proto/cmvr/api/common.proto @@ -4,59 +4,6 @@ package cmvr.api; import "google/protobuf/timestamp.proto"; -enum CommandReasonCode { - COMMAND_REASON_CODE_UNSPECIFIED = 0; - COMMAND_REASON_CODE_NONE = 1; - COMMAND_REASON_CODE_INVALID_ARGUMENT = 2; - COMMAND_REASON_CODE_UNAUTHENTICATED = 3; - COMMAND_REASON_CODE_PERMISSION_DENIED = 4; - COMMAND_REASON_CODE_RECOVERY_RPC_DISABLED = 5; - COMMAND_REASON_CODE_DEVICE_NOT_FOUND = 6; - COMMAND_REASON_CODE_DEVICE_UNAVAILABLE = 7; - COMMAND_REASON_CODE_UNSUPPORTED_COMMAND = 8; - COMMAND_REASON_CODE_SYSTEM_STARTING = 9; - COMMAND_REASON_CODE_SYSTEM_STOPPING = 10; - COMMAND_REASON_CODE_SAFETY_LATCHED = 11; - COMMAND_REASON_CODE_SAFETY_STATE_MISSING = 12; - COMMAND_REASON_CODE_SAFETY_STATE_STALE = 13; - COMMAND_REASON_CODE_HARDWARE_UNSAFE = 14; - COMMAND_REASON_CODE_EMERGENCY_STOP_ACTIVE = 15; - COMMAND_REASON_CODE_PROTECTIVE_STOP_ACTIVE = 16; - COMMAND_REASON_CODE_DEVICE_DISCONNECTED = 17; - COMMAND_REASON_CODE_DEVICE_FAULT = 18; - COMMAND_REASON_CODE_DEVICE_NOT_READY = 19; - COMMAND_REASON_CODE_DEVICE_STILL_MOVING = 20; - COMMAND_REASON_CODE_CONTROL_BUSY = 21; - COMMAND_REASON_CODE_GENERATION_MISMATCH = 22; - COMMAND_REASON_CODE_COMMAND_ID_REQUIRED = 23; - COMMAND_REASON_CODE_COMMAND_ID_CONFLICT = 24; - COMMAND_REASON_CODE_RESULT_EVICTED = 25; - COMMAND_REASON_CODE_LEDGER_EXHAUSTED = 26; - COMMAND_REASON_CODE_BACKPRESSURE = 27; - COMMAND_REASON_CODE_DEADLINE_EXCEEDED_BEFORE_DISPATCH = 28; - COMMAND_REASON_CODE_OUTCOME_UNKNOWN = 29; - COMMAND_REASON_CODE_PARTICIPANT_TIMEOUT = 30; - COMMAND_REASON_CODE_STOP_UNCONFIRMED = 31; - COMMAND_REASON_CODE_RECOVERY_EPOCH_MISMATCH = 32; - COMMAND_REASON_CODE_RECOVERY_REASON_REQUIRED = 33; - COMMAND_REASON_CODE_RECOVERY_AUDIT_FAILED = 34; - COMMAND_REASON_CODE_INTERNAL_ERROR = 35; -} - -enum CommandExecutionState { - COMMAND_EXECUTION_STATE_UNSPECIFIED = 0; - COMMAND_EXECUTION_STATE_RECEIVED = 1; - COMMAND_EXECUTION_STATE_RESERVED = 2; - COMMAND_EXECUTION_STATE_REJECTED_BEFORE_DISPATCH = 3; - COMMAND_EXECUTION_STATE_ADMITTED = 4; - COMMAND_EXECUTION_STATE_DISPATCHING = 5; - COMMAND_EXECUTION_STATE_ACCEPTED_BY_HARDWARE = 6; - COMMAND_EXECUTION_STATE_COMPLETED = 7; - COMMAND_EXECUTION_STATE_FAILED = 8; - COMMAND_EXECUTION_STATE_CANCELED_BEFORE_DISPATCH = 9; - COMMAND_EXECUTION_STATE_OUTCOME_UNKNOWN = 10; -} - message DeviceLifecycle { enum Lifecycle { STATE_INIT = 0; @@ -73,22 +20,12 @@ message CommandHeader { message Request { string device_id = 1; // 目标设备名称 google.protobuf.Timestamp timestamp = 2; // 请求时间 - string command_id = 3; - string expected_service_instance_id = 4; - optional uint64 expected_device_generation = 5; - uint32 valid_for_ms = 6; } message Feedback { bool success = 1; // 是否成功 string error_message = 2; // 错误信息(成功时为空) google.protobuf.Timestamp timestamp = 3; // 回复时间 - CommandReasonCode reason_code = 4; - string command_id = 5; - string service_instance_id = 6; - uint64 safety_epoch = 7; - uint64 device_generation = 8; - CommandExecutionState execution_state = 9; } } @@ -122,14 +59,21 @@ message ConfigParam { } } +// 设备厂商扩展 JSON 命令;JSON 内容及字段单位由具体设备命令定义。 message JsonDeviceCommand { + // JSON 命令请求。 message Request { + // 通用请求头,包含目标设备 ID 和请求时间戳。 CommandHeader.Request header = 1; + // JSON 格式的设备命令字符串,无单位。 string request_json = 2; } + // JSON 命令响应。 message Feedback { + // 通用响应头,包含成功标志、错误信息和响应时间戳。 CommandHeader.Feedback header = 1; + // JSON 格式的设备响应字符串,无单位。 string response_json = 2; } } diff --git a/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-lib/src/main/proto/cmvr/api/motor_command.proto b/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-lib/src/main/proto/cmvr/api/motor_command.proto deleted file mode 100644 index a15c181..0000000 --- a/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-lib/src/main/proto/cmvr/api/motor_command.proto +++ /dev/null @@ -1,151 +0,0 @@ -syntax = "proto3"; - -package cmvr.api; - -import "cmvr/api/common.proto"; -import "cmvr/msgs/motor.proto"; - -// Selects exactly one motor inside the MotorManager named by header.device_id. -message MotorTarget { - CommandHeader.Request header = 1; - oneof selector { - uint32 motor_id = 2; - string joint_name = 3; - } -} - -message MotorWaitOptions { - // Zero selects the server default (30 seconds). - uint32 timeout_ms = 1; - // Zero selects the server default (10 milliseconds). - uint32 poll_period_ms = 2; - // Zero selects the server default. - double position_tolerance_rad = 3; - double velocity_tolerance_rad_s = 4; - // Number of consecutive in-tolerance samples. Zero selects the default (3). - uint32 settle_sample_count = 5; -} - -enum MotorControlType { - MOTOR_CONTROL_NONE = 0; - MOTOR_CONTROL_SET_ZERO = 1; - MOTOR_CONTROL_PROFILE_POSITION = 2; - MOTOR_CONTROL_PROFILE_VELOCITY = 3; - MOTOR_CONTROL_CYCLIC_POSITION = 4; - MOTOR_CONTROL_CYCLIC_VELOCITY = 5; - MOTOR_CONTROL_SET_ENABLED = 6; -} - -message MotorStatus { - uint32 motor_id = 1; - string joint_name = 2; - cmvr.msgs.RunMode run_mode = 3; - double position_rad = 4; - double velocity_rad_s = 5; - bool target_reached = 6; - bool service_busy = 7; - MotorControlType active_control = 8; - bool emergency_stopped = 9; - string last_error = 10; -} - -message MotorCommandResponse { - CommandHeader.Feedback header = 1; - MotorStatus status = 2; - uint64 elapsed_ms = 3; -} - -message SetMotorZeroRequest { - MotorTarget target = 1; -} - -message MoveMotorToZeroRequest { - MotorTarget target = 1; - double max_velocity_rad_s = 2; - double acceleration_rad_s2 = 3; - MotorWaitOptions wait = 4; -} - -message ProfilePositionRequest { - MotorTarget target = 1; - double target_position_rad = 2; - double max_velocity_rad_s = 3; - double acceleration_rad_s2 = 4; - MotorWaitOptions wait = 5; -} - -message ProfileVelocityRequest { - MotorTarget target = 1; - double target_velocity_rad_s = 2; - double acceleration_rad_s2 = 3; - MotorWaitOptions wait = 4; -} - -message EmergencyStopRequest { - MotorTarget target = 1; -} - -message GetMotorStatusRequest { - MotorTarget target = 1; -} - -message GetMotorStatusResponse { - CommandHeader.Feedback header = 1; - MotorStatus status = 2; -} - -message SetMotorEnabledRequest { - MotorTarget target = 1; - bool enabled = 2; -} - -message CyclicStreamOpen { - MotorTarget target = 1; - // The device/driver watchdog is authoritative. This service watchdog prevents a - // stalled gRPC client from retaining control indefinitely. - uint32 watchdog_timeout_ms = 2; -} - -message CyclicPositionSetpoint { - uint64 sequence = 1; - double target_position_rad = 2; - optional double target_velocity_rad_s = 3; -} - -message CyclicVelocitySetpoint { - uint64 sequence = 1; - double target_velocity_rad_s = 2; -} - -message CyclicPositionRequest { - oneof payload { - CyclicStreamOpen open = 1; - CyclicPositionSetpoint setpoint = 2; - } -} - -message CyclicVelocityRequest { - oneof payload { - CyclicStreamOpen open = 1; - CyclicVelocitySetpoint setpoint = 2; - } -} - -enum CyclicStreamPhase { - CYCLIC_STREAM_PHASE_UNSPECIFIED = 0; - CYCLIC_STREAM_OPENED = 1; - CYCLIC_STREAM_APPLIED = 2; - CYCLIC_STREAM_STOPPED = 3; - CYCLIC_STREAM_WATCHDOG_EXPIRED = 4; - CYCLIC_STREAM_FAILED = 5; -} - -message CyclicControlResponse { - CommandHeader.Feedback header = 1; - CyclicStreamPhase phase = 2; - uint64 sequence = 3; - uint64 dropped_setpoints = 4; - // Present for OPENED and terminal responses. APPLIED deliberately omits - // live status so one cyclic sample does not trigger extra fieldbus reads. - MotorStatus status = 5; -} diff --git a/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-lib/src/main/proto/cmvr/api/motor_service.proto b/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-lib/src/main/proto/cmvr/api/motor_service.proto deleted file mode 100644 index 01f127f..0000000 --- a/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-lib/src/main/proto/cmvr/api/motor_service.proto +++ /dev/null @@ -1,21 +0,0 @@ -syntax = "proto3"; - -package cmvr.api; - -import "cmvr/api/motor_command.proto"; - -service MotorService { - rpc setZero(SetMotorZeroRequest) returns (MotorCommandResponse); - rpc moveToZero(MoveMotorToZeroRequest) returns (MotorCommandResponse); - rpc profilePosition(ProfilePositionRequest) returns (MotorCommandResponse); - rpc profileVelocity(ProfileVelocityRequest) returns (MotorCommandResponse); - - rpc streamCyclicPosition(stream CyclicPositionRequest) - returns (stream CyclicControlResponse); - rpc streamCyclicVelocity(stream CyclicVelocityRequest) - returns (stream CyclicControlResponse); - - rpc emergencyStop(EmergencyStopRequest) returns (MotorCommandResponse); - rpc getStatus(GetMotorStatusRequest) returns (GetMotorStatusResponse); - rpc setEnabled(SetMotorEnabledRequest) returns (MotorCommandResponse); -} diff --git a/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-lib/src/main/proto/cmvr/api/safety_command.proto b/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-lib/src/main/proto/cmvr/api/safety_command.proto deleted file mode 100644 index 4e5b774..0000000 --- a/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-lib/src/main/proto/cmvr/api/safety_command.proto +++ /dev/null @@ -1,204 +0,0 @@ -syntax = "proto3"; - -package cmvr.api; - -import "cmvr/api/common.proto"; - -enum SafetyTriState { - SAFETY_TRI_STATE_UNKNOWN = 0; - SAFETY_TRI_STATE_FALSE = 1; - SAFETY_TRI_STATE_TRUE = 2; -} - -enum SafetyCondition { - SAFETY_CONDITION_UNKNOWN = 0; - SAFETY_CONDITION_NOMINAL = 1; - SAFETY_CONDITION_RESTRICTED = 2; - SAFETY_CONDITION_UNSAFE = 3; -} - -enum SystemAdmissionState { - SYSTEM_ADMISSION_STATE_UNSPECIFIED = 0; - SYSTEM_ADMISSION_STATE_STARTING = 1; - SYSTEM_ADMISSION_STATE_OPEN = 2; - SYSTEM_ADMISSION_STATE_STOPPING = 3; - SYSTEM_ADMISSION_STATE_LATCHED = 4; - SYSTEM_ADMISSION_STATE_RECOVERING = 5; - SYSTEM_ADMISSION_STATE_SHUTTING_DOWN = 6; -} - -enum DeviceAdmissionState { - DEVICE_ADMISSION_STATE_UNSPECIFIED = 0; - DEVICE_ADMISSION_STATE_OBSERVING = 1; - DEVICE_ADMISSION_STATE_OPEN = 2; - DEVICE_ADMISSION_STATE_BLOCKED = 3; - DEVICE_ADMISSION_STATE_QUARANTINED = 4; - DEVICE_ADMISSION_STATE_RECOVERING = 5; - DEVICE_ADMISSION_STATE_REMOVED = 6; -} - -enum SafetyBlockerScope { - SAFETY_BLOCKER_SCOPE_UNSPECIFIED = 0; - SAFETY_BLOCKER_SCOPE_DEVICE = 1; - SAFETY_BLOCKER_SCOPE_SYSTEM = 2; -} - -enum SafetyRecoveryRequirement { - SAFETY_RECOVERY_REQUIREMENT_UNSPECIFIED = 0; - SAFETY_RECOVERY_REQUIREMENT_REFRESH_ONLY = 1; - SAFETY_RECOVERY_REQUIREMENT_CLEAR_SOFTWARE_LATCH = 2; - SAFETY_RECOVERY_REQUIREMENT_HARDWARE_RELEASE_REQUIRED = 3; - SAFETY_RECOVERY_REQUIREMENT_MANUAL_INSPECTION_REQUIRED = 4; -} - -enum SafetyOperationResult { - SAFETY_OPERATION_RESULT_UNSPECIFIED = 0; - SAFETY_OPERATION_RESULT_SUCCEEDED = 1; - SAFETY_OPERATION_RESULT_RECOVERED = 2; - SAFETY_OPERATION_RESULT_VERIFIED_BUT_STILL_BLOCKED = 3; - SAFETY_OPERATION_RESULT_BLOCKER_REMAINS = 4; - SAFETY_OPERATION_RESULT_EPOCH_MISMATCH = 5; - SAFETY_OPERATION_RESULT_NOTHING_TO_RECOVER = 6; - SAFETY_OPERATION_RESULT_TIMED_OUT = 7; - SAFETY_OPERATION_RESULT_FAILED = 8; -} - -message DeviceIdList { - repeated string device_ids = 1; -} - -message SafetyScope { - oneof target { - bool all_devices = 1; - DeviceIdList devices = 2; - } -} - -message SafetyBlockerInfo { - CommandReasonCode reason_code = 1; - SafetyBlockerScope scope = 2; - SafetyRecoveryRequirement recovery_requirement = 3; - string source_id = 4; - string operation_id = 5; - uint64 first_observed_at_unix_ms = 6; - uint64 last_observed_at_unix_ms = 7; -} - -message DeviceSafetyStateInfo { - string device_id = 1; - string device_kind = 2; - string policy_family = 3; - string lifecycle_state = 4; - string health_state = 5; - DeviceAdmissionState admission_state = 6; - SafetyCondition condition = 7; - bool has_sample = 8; - bool snapshot_fresh = 9; - uint64 sample_age_ms = 10; - uint64 sample_sequence = 11; - uint64 observed_at_unix_ms = 12; - uint64 device_generation = 13; - SafetyTriState connected = 14; - SafetyTriState operational_ready = 15; - SafetyTriState quiescent = 16; - SafetyTriState motion_active = 17; - SafetyTriState actuator_enabled = 18; - SafetyTriState emergency_stop_active = 19; - SafetyTriState protective_stop_active = 20; - SafetyTriState fault_active = 21; - repeated SafetyBlockerInfo blockers = 22; -} - -message SafetyOperationTargetResult { - string target_id = 1; - SafetyOperationResult result = 2; - CommandReasonCode reason_code = 3; - string detail = 4; - DeviceAdmissionState before_state = 5; - DeviceAdmissionState after_state = 6; -} - -message SafetyParticipantResultInfo { - bool recorded = 1; - bool success = 2; - CommandReasonCode reason_code = 3; - string detail = 4; -} - -message SafetyParticipantStateInfo { - string participant_id = 1; - string phase = 2; - bool required = 3; - bool registered = 4; - bool barrier_active = 5; - bool barrier_retained = 6; - string operation_id = 7; - uint64 safety_epoch = 8; - SafetyParticipantResultInfo last_request = 9; - SafetyParticipantResultInfo last_verify = 10; - SafetyParticipantResultInfo last_release = 11; -} - -message GetSafetyStateCommand { - message Request { - SafetyScope scope = 1; - } - - message Feedback { - CommandHeader.Feedback header = 1; - SystemAdmissionState system_state = 2; - uint64 safety_epoch = 3; - string control_service_instance_id = 4; - string enforcement_mode = 5; - repeated DeviceSafetyStateInfo devices = 6; - string active_operation_id = 7; - string active_operation_phase = 8; - uint64 sampled_at_unix_ms = 9; - repeated SafetyParticipantStateInfo participants = 10; - } -} - -message RecoverSafetyStateCommand { - enum Mode { - MODE_UNSPECIFIED = 0; - VERIFY_ONLY = 1; - CLEAR_SOFTWARE_LATCH = 2; - } - - message Request { - string recovery_id = 1; - SafetyScope scope = 2; - uint64 expected_safety_epoch = 3; - Mode mode = 4; - string reason = 5; - uint32 timeout_ms = 6; - } - - message Feedback { - CommandHeader.Feedback header = 1; - SafetyOperationResult result = 2; - uint64 previous_safety_epoch = 3; - uint64 current_safety_epoch = 4; - SystemAdmissionState system_state = 5; - repeated SafetyOperationTargetResult targets = 6; - string recovery_id = 7; - } -} - -message RestoreOperationalStateCommand { - message Request { - string recovery_id = 1; - string reason = 2; - uint32 timeout_ms = 3; - } - - message Feedback { - CommandHeader.Feedback header = 1; - SafetyOperationResult result = 2; - uint64 previous_safety_epoch = 3; - uint64 current_safety_epoch = 4; - SystemAdmissionState system_state = 5; - repeated SafetyOperationTargetResult targets = 6; - string recovery_id = 7; - } -} diff --git a/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-lib/src/main/proto/cmvr/api/system_command.proto b/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-lib/src/main/proto/cmvr/api/system_command.proto index d99f623..ac91ae0 100644 --- a/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-lib/src/main/proto/cmvr/api/system_command.proto +++ b/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-lib/src/main/proto/cmvr/api/system_command.proto @@ -1,9 +1,7 @@ syntax = "proto3"; -import "cmvr/api/agv_command.proto"; import "cmvr/api/arm_command.proto"; import "cmvr/api/common.proto"; -import "cmvr/api/safety_command.proto"; package cmvr.api; @@ -24,77 +22,6 @@ message DeviceList { DeviceType device_type = 2; } -// Stable device categories used by GetDeviceList. This intentionally does not -// reuse the legacy DeviceType enum above: its zero value is AGV and it does not -// cover all DeviceManager categories. -enum SystemDeviceType { - SYSTEM_DEVICE_TYPE_UNSPECIFIED = 0; - SYSTEM_DEVICE_TYPE_AGV = 1; - SYSTEM_DEVICE_TYPE_ARM = 2; - SYSTEM_DEVICE_TYPE_BATTERY = 3; - SYSTEM_DEVICE_TYPE_BIO_HEAD = 4; - SYSTEM_DEVICE_TYPE_CAMERA = 5; - SYSTEM_DEVICE_TYPE_CAN_BUS = 6; - SYSTEM_DEVICE_TYPE_DEX_HAND = 7; - SYSTEM_DEVICE_TYPE_GRIPPER = 8; - SYSTEM_DEVICE_TYPE_MICROPHONE = 9; - SYSTEM_DEVICE_TYPE_MOTOR = 10; - SYSTEM_DEVICE_TYPE_MOTOR_SYSTEM = 11; - SYSTEM_DEVICE_TYPE_MUJOCO_VIEWER = 12; - SYSTEM_DEVICE_TYPE_MUJOCO_WORLD = 13; - SYSTEM_DEVICE_TYPE_ROBOT = 14; - SYSTEM_DEVICE_TYPE_SPEAKER = 15; -} - -enum SystemDeviceState { - SYSTEM_DEVICE_STATE_UNSPECIFIED = 0; - SYSTEM_DEVICE_STATE_DISABLED = 1; - SYSTEM_DEVICE_STATE_INITIALIZING = 2; - SYSTEM_DEVICE_STATE_REGISTERED = 3; - SYSTEM_DEVICE_STATE_READY = 4; - SYSTEM_DEVICE_STATE_RUNNING = 5; - SYSTEM_DEVICE_STATE_STOPPED = 6; - SYSTEM_DEVICE_STATE_ERROR = 7; -} - -enum SystemDeviceHealth { - SYSTEM_DEVICE_HEALTH_UNSPECIFIED = 0; - SYSTEM_DEVICE_HEALTH_HEALTHY = 1; - SYSTEM_DEVICE_HEALTH_DEGRADED = 2; - SYSTEM_DEVICE_HEALTH_FAULT = 3; -} - -message SystemDeviceInfo { - string device_id = 1; - SystemDeviceType device_type = 2; - - // Concrete backend name for display and diagnostics only. Consumers must - // use device_type, rather than this free-form string, for decisions. - string type_name = 3; - - // GetDeviceList currently publishes only enabled entries. Keep this field - // explicit so each row remains self-describing and future-compatible. - bool enabled = 4; - SystemDeviceState manager_state = 5; - SystemDeviceHealth health = 6; - bool has_error = 7; - string error_message = 8; - uint64 status_updated_at_unix_ms = 9; -} - -message GetDeviceListCommand { - message Request {} - - message Feedback { - CommandHeader.Feedback header = 1; - string manager_name = 2; - string manager_version = 3; - string manager_description = 4; - repeated SystemDeviceInfo device_list = 5; - uint64 sampled_at_unix_ms = 6; - } -} - message GetSystemInfoCommand { message Request {} @@ -106,20 +33,6 @@ message GetSystemInfoCommand { string os = 5; string kernel_version = 6; string architecture = 7; - - // Changes whenever the in-process ActionQueue idempotency ledger is - // recreated. Clients bind submissions and retries to this value. - string action_service_instance_id = 8; - - // Effective server-side control-plane settings. These fields describe - // what is running, not merely what the configuration requested. - string grpc_transport_security = 9; - string grpc_authentication = 10; - string grpc_recovery_exposure = 11; - bool grpc_insecure_non_loopback = 12; - string control_service_instance_id = 13; - string safety_enforcement_mode = 14; - uint32 safety_schema_version = 15; } } @@ -152,22 +65,13 @@ message UpdateParamsCommand { message StopAllCommand { message Request { CommandHeader.Request header = 1; - string operation_id = 2; - string expected_service_instance_id = 3; - uint32 timeout_ms = 4; } message Feedback { CommandHeader.Feedback header = 1; - string operation_id = 2; - uint64 previous_safety_epoch = 3; - uint64 current_safety_epoch = 4; - SystemAdmissionState system_state = 5; - repeated SafetyOperationTargetResult targets = 6; } } -// Final outcome of one ActionQueue execution. enum ActionResultCode { ACTION_RESULT_CODE_UNSPECIFIED = 0; ACTION_RESULT_CODE_COMPLETED = 1; @@ -177,86 +81,37 @@ enum ActionResultCode { ACTION_RESULT_CODE_REJECTED = 5; } -// Describes whether this RPC admitted a new action or observed an existing -// idempotency record. Clients must not infer this from an error string. -enum ActionDeduplicationStatus { - ACTION_DEDUPLICATION_STATUS_UNSPECIFIED = 0; - ACTION_DEDUPLICATION_STATUS_ACCEPTED_NEW = 1; - ACTION_DEDUPLICATION_STATUS_JOINED_IN_FLIGHT = 2; - ACTION_DEDUPLICATION_STATUS_CACHED_RESULT = 3; - ACTION_DEDUPLICATION_STATUS_RESULT_EVICTED = 4; - ACTION_DEDUPLICATION_STATUS_LEDGER_EXHAUSTED = 5; - ACTION_DEDUPLICATION_STATUS_ACTION_ID_CONFLICT = 6; - ACTION_DEDUPLICATION_STATUS_SERVICE_INSTANCE_MISMATCH = 7; -} - -// Edge-local delay between two device commands. -message DelayAction { - // Delay duration in milliseconds. The server applies a bounded maximum. - uint32 duration_ms = 1; -} - -// One finite, synchronous command in an ActionQueue request. message ActionStep { - // Client-provided identifier used for diagnostics. It must be unique within - // one ActionQueue request. - string step_id = 1; + reserved 1; + reserved "step_id"; - // Per-step timeout in milliseconds. Zero inherits the remaining action - // timeout or the server default. + // Zero inherits the remaining ActionQueue timeout. uint32 timeout_ms = 2; - // Tags are grouped by domain so compatible commands can be added without - // renumbering existing alternatives: Arm 10-19, AGV 20-29, built-ins 90+. oneof command { MoveJ.Request arm_move_j = 10; MoveL.Request arm_move_l = 11; - - AgvNavigateToPoseCommand.Request agv_navigate_to_pose = 20; - AgvNavigateToStationCommand.Request agv_navigate_to_station = 21; - AgvFollowPathCommand.Request agv_follow_path = 22; - - DelayAction delay = 90; } } -// Atomically submits a complete command sequence for edge-local serial -// execution. Device motion alternatives must use synchronous execution. message ActionQueueCommand { message Request { - // Client-generated globally unique idempotency key. During one Action - // service instance, retrying an identical accepted request with the - // same action_id does not dispatch its steps a second time. Recent - // terminal results can be returned; older accepted IDs are rejected - // fail-closed after their result is evicted. Deduplication is not - // persisted across an edge-service restart; the required instance - // epoch below prevents an old retry from being replayed after restart. - string action_id = 1; + reserved 1; + reserved "action_id"; + repeated ActionStep steps = 2; - - // Total queue-wait plus execution timeout in milliseconds. Zero uses a - // bounded server default. + // Zero uses the server default. uint32 total_timeout_ms = 3; - - // Required instance epoch obtained from GetSystemInfo. A mismatch - // means the process-local deduplication ledger was recreated, so the - // server rejects the request instead of risking a replay. - string expected_service_instance_id = 4; } message Feedback { CommandHeader.Feedback header = 1; - string action_id = 2; - // Number of steps which completed successfully before the final result. + reserved 2; + reserved "action_id"; + uint32 completed_steps = 3; - - // Present only when a particular step caused failure, cancellation, - // timeout, or rejection. Presence distinguishes index zero from no - // failed step. optional uint32 failed_step_index = 4; ActionResultCode result = 5; - string service_instance_id = 6; - ActionDeduplicationStatus deduplication_status = 7; } } diff --git a/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-lib/src/main/proto/cmvr/api/system_service.proto b/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-lib/src/main/proto/cmvr/api/system_service.proto index ac961ac..4dbb72d 100644 --- a/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-lib/src/main/proto/cmvr/api/system_service.proto +++ b/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-lib/src/main/proto/cmvr/api/system_service.proto @@ -1,7 +1,6 @@ syntax = "proto3"; import "cmvr/api/system_command.proto"; -import "cmvr/api/safety_command.proto"; package cmvr.api; @@ -9,15 +8,7 @@ package cmvr.api; service SystemService { rpc GetSystemInfo(GetSystemInfoCommand.Request) returns (GetSystemInfoCommand.Feedback) {} rpc GetSystemStatus(GetSystemStatusCommand.Request) returns (GetSystemStatusCommand.Feedback) {} - rpc GetDeviceList(GetDeviceListCommand.Request) returns (GetDeviceListCommand.Feedback) {} - rpc UpdateParams(UpdateParamsCommand.Request) returns (UpdateParamsCommand.Feedback) {} - rpc StopAll(StopAllCommand.Request) returns (StopAllCommand.Feedback) {} - rpc ExecuteActionQueue(ActionQueueCommand.Request) returns (ActionQueueCommand.Feedback) {} - - rpc GetSafetyState(GetSafetyStateCommand.Request) returns (GetSafetyStateCommand.Feedback) {} - rpc RecoverSafetyState(RecoverSafetyStateCommand.Request) returns (RecoverSafetyStateCommand.Feedback) {} - rpc RestoreOperationalState(RestoreOperationalStateCommand.Request) returns (RestoreOperationalStateCommand.Feedback) {} } diff --git a/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-lib/src/main/proto/cmvr/api/agv_utils.proto b/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-lib/src/main/proto/cmvr/msgs/agv.proto similarity index 100% rename from cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-lib/src/main/proto/cmvr/api/agv_utils.proto rename to cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-lib/src/main/proto/cmvr/msgs/agv.proto diff --git a/cmvr-iot-evaluation/src/main/java/com/cmvr/evaluation/model/dto/AeIResultDTO.java b/cmvr-iot-evaluation/src/main/java/com/cmvr/evaluation/model/dto/AeIResultDTO.java index 1a96652..c2940b4 100644 --- a/cmvr-iot-evaluation/src/main/java/com/cmvr/evaluation/model/dto/AeIResultDTO.java +++ b/cmvr-iot-evaluation/src/main/java/com/cmvr/evaluation/model/dto/AeIResultDTO.java @@ -5,6 +5,8 @@ import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; +import java.util.List; + @Data @ApiModel("AI 评估结果明细") public class AeIResultDTO { @@ -28,4 +30,30 @@ public class AeIResultDTO { @ApiModelProperty(value = "评估相关视频地址") @JSONField(name = "videoPath") private String videoPath; + + @ApiModelProperty(value = "结构化执行步骤") + private List executionSteps; + + @ApiModelProperty(value = "执行涉及的界面路径") + private List uiPath; + + @ApiModelProperty(value = "执行证据截图") + private List screenshotUrls; + + @Data + @ApiModel("触控执行步骤") + public static class ExecutionStep { + private Integer sequence; + private Long timestamp; + private String nodeName; + private String action; + private String status; + private String pageName; + private String targetName; + private String operationType; + private String coordinates; + private String screenshotUrl; + private String videoUrl; + private String message; + } } diff --git a/cmvr-iot-evaluation/src/main/java/com/cmvr/evaluation/model/vo/AeViEvaluationVO.java b/cmvr-iot-evaluation/src/main/java/com/cmvr/evaluation/model/vo/AeViEvaluationVO.java index 39d661f..4ec5ad0 100644 --- a/cmvr-iot-evaluation/src/main/java/com/cmvr/evaluation/model/vo/AeViEvaluationVO.java +++ b/cmvr-iot-evaluation/src/main/java/com/cmvr/evaluation/model/vo/AeViEvaluationVO.java @@ -7,6 +7,7 @@ import lombok.Data; import java.util.List; import java.util.Date; +import com.cmvr.evaluation.model.dto.AeIResultDTO; @Data @ApiModel("语音交互评估") @@ -85,6 +86,15 @@ public class AeViEvaluationVO { @ApiModelProperty(value = "执行过程详情说明") private String executionDetail; + @ApiModelProperty(value = "结构化执行步骤") + private List executionSteps; + + @ApiModelProperty(value = "执行涉及的界面路径") + private List uiPath; + + @ApiModelProperty(value = "执行证据截图") + private List screenshotUrls; + } } diff --git a/cmvr-iot-resource/src/main/java/com/cmvr/resource/service/ResourceRobotControlService.java b/cmvr-iot-resource/src/main/java/com/cmvr/resource/service/ResourceRobotControlService.java index 6afc8f8..7815c3a 100644 --- a/cmvr-iot-resource/src/main/java/com/cmvr/resource/service/ResourceRobotControlService.java +++ b/cmvr-iot-resource/src/main/java/com/cmvr/resource/service/ResourceRobotControlService.java @@ -1,6 +1,6 @@ package com.cmvr.resource.service; -import cmvr.msgs.AgvUtils; +import cmvr.msgs.Agv; import cmvr.quic_edge.v1.QuicEdge; import com.cmvr.common.exception.GlobalException; import com.cmvr.edge.client.model.EdgeCommonVO; @@ -67,7 +67,7 @@ public class ResourceRobotControlService { public ResourceRobot syncRuntimeState(String robotRef) { ResourceRobot robot = requireRobot(robotRef); try { - AgvUtils.AgvRuntimeState runtime = edgeAgvService.getRuntimeState(buildAgvRequest(robot)); + Agv.AgvRuntimeState runtime = edgeAgvService.getRuntimeState(buildAgvRequest(robot)); if (runtime == null) { throw new GlobalException("AGV未返回运行状态"); } @@ -76,7 +76,7 @@ public class ResourceRobotControlService { robot.setBatteryLevel(Math.max(0, Math.min(100, percentage))); } if (runtime.hasPose()) { - AgvUtils.AgvPose2d pose = runtime.getPose(); + Agv.AgvPose2d pose = runtime.getPose(); robot.setCurrentPosition(String.format(Locale.ROOT, "%.3f,%.3f,%.3f", pose.getX(), pose.getY(), pose.getTheta())); } @@ -94,8 +94,8 @@ public class ResourceRobotControlService { } } - public AgvUtils.AgvPose2d getRealtimeLocation(String robotRef) { - AgvUtils.AgvRuntimeState runtime = edgeAgvService.getRuntimeState( + public Agv.AgvPose2d getRealtimeLocation(String robotRef) { + Agv.AgvRuntimeState runtime = edgeAgvService.getRuntimeState( buildAgvRequest(requireRobot(robotRef))); return runtime.hasPose() ? runtime.getPose() : null; } @@ -104,7 +104,7 @@ public class ResourceRobotControlService { if (x == null || y == null) { throw new GlobalException("X和Y坐标不能为空"); } - AgvUtils.AgvPose2d pose = AgvUtils.AgvPose2d.newBuilder() + Agv.AgvPose2d pose = Agv.AgvPose2d.newBuilder() .setX(x) .setY(y) .setTheta(theta == null ? 0D : theta) @@ -113,7 +113,7 @@ public class ResourceRobotControlService { } public String startMapping(String robotRef, int dimension, String mapName, boolean realTime) { - AgvUtils.AgvMapDimension mapDimension = AgvUtils.AgvMapDimension.forNumber(dimension); + Agv.AgvMapDimension mapDimension = Agv.AgvMapDimension.forNumber(dimension); if (mapDimension == null) { throw new GlobalException("无效的建图维度:" + dimension); } diff --git a/cmvr-iot-system/src/main/java/com/cmvr/system/service/impl/ISysFileServiceImpl.java b/cmvr-iot-system/src/main/java/com/cmvr/system/service/impl/ISysFileServiceImpl.java index 51348cd..f1c9d17 100644 --- a/cmvr-iot-system/src/main/java/com/cmvr/system/service/impl/ISysFileServiceImpl.java +++ b/cmvr-iot-system/src/main/java/com/cmvr/system/service/impl/ISysFileServiceImpl.java @@ -65,9 +65,7 @@ public class ISysFileServiceImpl extends ServiceImpl String md5 = DigestUtils.md5DigestAsHex(file.getInputStream()); SysFileInfo fileInfo = findByMd5(md5); if (!Objects.isNull(fileInfo)) { - this.updateById(fileInfo); - // 返回 - return fileInfo.getFilePath(); + return reuseExistingFile(fileInfo); } // 上传 FileType type = FileType.getByCode(fileType); @@ -94,9 +92,7 @@ public class ISysFileServiceImpl extends ServiceImpl SysFileInfo fileInfo = findByMd5(md5); if (!Objects.isNull(fileInfo)) { - this.updateById(fileInfo); - // 如果文件已存在,直接返回已上传文件的 URL - return fileInfo.getFilePath(); + return reuseExistingFile(fileInfo); } // 上传文件到 MinIO @@ -129,6 +125,53 @@ public class ISysFileServiceImpl extends ServiceImpl return StrUtil.join("/", minioProps.getUrl(), minioProps.getBucketName(), fileType, fileName); } + /** + * Reuse the object recorded for the same MD5 while refreshing its public URL. + * Historical records contain the complete MinIO URL, so an endpoint change + * must not make a new business record persist the obsolete host again. + */ + private String reuseExistingFile(SysFileInfo fileInfo) { + String currentUrl = normalizeStoredFileUrl( + fileInfo.getFilePath(), minioProps.getUrl(), minioProps.getBucketName()); + if (!Objects.equals(fileInfo.getFilePath(), currentUrl)) { + log.info("刷新已存在文件的MinIO地址, oldUrl:{}, newUrl:{}", fileInfo.getFilePath(), currentUrl); + fileInfo.setFilePath(currentUrl); + } + this.updateById(fileInfo); + return currentUrl; + } + + static String normalizeStoredFileUrl(String storedUrl, String minioUrl, String bucketName) { + if (StringUtils.isBlank(storedUrl) + || StringUtils.isBlank(minioUrl) + || StringUtils.isBlank(bucketName)) { + return storedUrl; + } + + String normalizedStoredUrl = storedUrl.replace('\\', '/'); + String normalizedBucketName = StringUtils.strip(bucketName.trim(), "/"); + String bucketMarker = "/" + normalizedBucketName + "/"; + int markerIndex = normalizedStoredUrl.indexOf(bucketMarker); + String objectPath; + if (markerIndex >= 0) { + objectPath = normalizedStoredUrl.substring(markerIndex + bucketMarker.length()); + } else { + String bucketPrefix = normalizedBucketName + "/"; + String relativePath = StringUtils.stripStart(normalizedStoredUrl, "/"); + if (!relativePath.startsWith(bucketPrefix)) { + return storedUrl; + } + objectPath = relativePath.substring(bucketPrefix.length()); + } + + if (StringUtils.isBlank(objectPath)) { + return storedUrl; + } + return StringUtils.stripEnd(minioUrl.trim(), "/") + + "/" + normalizedBucketName + + "/" + StringUtils.stripStart(objectPath, "/"); + } + private SysFileInfo findByMd5(String md5) { LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); wrapper.eq(SysFileInfo::getMd5, md5); diff --git a/cmvr-iot-test/src/main/java/com/cmvr/test/flow/context/TaskInstHolder.java b/cmvr-iot-test/src/main/java/com/cmvr/test/flow/context/TaskInstHolder.java index 790f7c7..06232c5 100644 --- a/cmvr-iot-test/src/main/java/com/cmvr/test/flow/context/TaskInstHolder.java +++ b/cmvr-iot-test/src/main/java/com/cmvr/test/flow/context/TaskInstHolder.java @@ -27,6 +27,9 @@ public class TaskInstHolder { @Resource private FlowFailureCleanupService flowFailureCleanupService; + @Resource + private TaskThreadRegistry taskThreadRegistry; + private final ITeNodeInstService nodeInstService; private final TaskContextManager taskContextManager; private final ITeTaskInstService taskInstService; @@ -127,10 +130,20 @@ public class TaskInstHolder { // 终止和并发失败都可能先注销上下文,失败状态仍需落库。 TaskContext ctx = taskContextManager.get(instId); + if (ctx != null) { + // 先关闭执行入口,避免设备清理期间又调度新的节点。 + ctx.setStopped(true); + ctx.setStatus(TaskStatusEnum.FAILED); + } + if (taskThreadRegistry != null) { + // 唤醒所有嵌套循环并中断其他执行节点,让失败立即收敛到整个实例。 + taskThreadRegistry.interruptAll(instId); + } String moduleCode = ctx != null ? ctx.getModuleCode() : null; if (ctx != null && flowFailureCleanupService != null) { flowFailureCleanupService.cleanup(ctx); } + nodeInstService.failUnfinished(instId, "任务因节点失败而结束:" + message); syncStatus(instId, TaskStatusEnum.FAILED); // 注销任务上下文 diff --git a/cmvr-iot-test/src/main/java/com/cmvr/test/flow/context/TaskThreadRegistry.java b/cmvr-iot-test/src/main/java/com/cmvr/test/flow/context/TaskThreadRegistry.java index e12a4a8..870b07e 100644 --- a/cmvr-iot-test/src/main/java/com/cmvr/test/flow/context/TaskThreadRegistry.java +++ b/cmvr-iot-test/src/main/java/com/cmvr/test/flow/context/TaskThreadRegistry.java @@ -80,7 +80,8 @@ public class TaskThreadRegistry { continue; } Thread thread = entry.getValue().getThread(); - if (thread != null && !thread.isInterrupted()) { + // 失败通常由当前节点线程发起;不要中断自己,以免阻断后续设备清理。 + if (thread != null && thread != Thread.currentThread() && !thread.isInterrupted()) { log.info("中断线程:{}", thread.getName()); thread.interrupt(); } @@ -164,7 +165,7 @@ public class TaskThreadRegistry { if (latch != null) { try { latch.countDown(); - log.info("释放 latch: {} -> {}", key, latch); + log.debug("清理 latch: {} -> {}", key, latch); } catch (Exception ignored) { } } diff --git a/cmvr-iot-test/src/main/java/com/cmvr/test/flow/runtime/dispatcher/FlowLoopNodeHandler.java b/cmvr-iot-test/src/main/java/com/cmvr/test/flow/runtime/dispatcher/FlowLoopNodeHandler.java index a9e5bb5..f3be750 100644 --- a/cmvr-iot-test/src/main/java/com/cmvr/test/flow/runtime/dispatcher/FlowLoopNodeHandler.java +++ b/cmvr-iot-test/src/main/java/com/cmvr/test/flow/runtime/dispatcher/FlowLoopNodeHandler.java @@ -5,6 +5,8 @@ import com.alibaba.fastjson2.JSONObject; import com.cmvr.common.utils.spring.SpringUtils; import com.cmvr.test.flow.builder.FlowGraph; import com.cmvr.test.flow.builder.FlowNodeWrapper; +import com.cmvr.test.flow.context.TaskContext; +import com.cmvr.test.flow.context.TaskInstHolder; import com.cmvr.test.flow.context.TaskThreadRegistry; import com.cmvr.test.flow.runtime.engine.FlowItemExecutor; import com.cmvr.test.flow.runtime.message.TaskNodeExecuteMessage; @@ -23,6 +25,7 @@ import java.util.concurrent.CountDownLatch; public class FlowLoopNodeHandler implements FlowNodeTypeHandler { private final TaskThreadRegistry taskThreadRegistry; + private final TaskInstHolder taskInstHolder; @Override public TaskNodeExecuteResult handle(TaskNodeExecuteMessage message) { @@ -49,6 +52,11 @@ public class FlowLoopNodeHandler implements FlowNodeTypeHandler { // 循环执行子图 for (int i = loopStartNum; i <= loopCount; i++) { + TaskContext context = taskInstHolder.getContext(instId); + if (context == null || context.isStopped()) { + log.info("任务已结束,退出循环:instId={}, nodeId={}, nextIteration={}", instId, nodeId, i); + break; + } // 如果子图设置了跳出标志,则结束循环 if (subGraph.isBreakLoop()) { log.info("检测到子图设置跳出标志,提前结束循环:instId={}, nodeId={}", instId, nodeId); @@ -57,7 +65,7 @@ public class FlowLoopNodeHandler implements FlowNodeTypeHandler { CountDownLatch latch = new CountDownLatch(1); taskThreadRegistry.registerLatch(instId, nodeId, latch); - FlowItemExecutor flowItemExecutor = SpringUtils.getBean(FlowItemExecutor.class); + FlowItemExecutor flowItemExecutor = getFlowItemExecutor(); // 构造新的迭代路径 List newIterations = new ArrayList<>(parentIterations); @@ -86,4 +94,8 @@ public class FlowLoopNodeHandler implements FlowNodeTypeHandler { // 返回执行结果 return TaskNodeExecuteResult.success(inputParams); } + + protected FlowItemExecutor getFlowItemExecutor() { + return SpringUtils.getBean(FlowItemExecutor.class); + } } diff --git a/cmvr-iot-test/src/main/java/com/cmvr/test/flow/runtime/engine/FlowItemExecutor.java b/cmvr-iot-test/src/main/java/com/cmvr/test/flow/runtime/engine/FlowItemExecutor.java index 95dea34..73ba993 100644 --- a/cmvr-iot-test/src/main/java/com/cmvr/test/flow/runtime/engine/FlowItemExecutor.java +++ b/cmvr-iot-test/src/main/java/com/cmvr/test/flow/runtime/engine/FlowItemExecutor.java @@ -139,6 +139,12 @@ public class FlowItemExecutor { String itemId = rootMessage.getItemId(); String nodeId = node.getNodeId(); String nodeName = node.getNodeName(); + TaskContext executionContext = taskInstHolder.getContext(instId); + if (executionContext == null || executionContext.isStopped()) { + log.debug("任务已结束,跳过待执行节点: instId={}, nodeId={}", instId, nodeId); + notifyFinished(onFinished, instId, nodeId); + return; + } log.info("当前执行的是 {} 节点,迭代路径={}", node.getNodeName(), iterations); //注册当前线程 @@ -162,6 +168,7 @@ public class FlowItemExecutor { // 分支节点处理 if (node.getNodeType() == NodeTypeEnum.BRANCH) { + if (isExecutionTerminated(instId)) return; postHandleBranchNode(taskInstHolder.getContext(rootMessage.getInstId()), graph, node, startNodeId, startInputDefs, rootMessage, onFinished, iterations, nodeId, message.getInputParams(), message); return; @@ -181,6 +188,9 @@ public class FlowItemExecutor { } } + // 节点内部可能已经把整个任务标记为失败;禁止继续调度后继节点。 + if (isExecutionTerminated(instId)) return; + // 通知调度器继续调度后继节点 flowTaskScheduler.markCompleted( message.getInstId(), nodeId, graph, @@ -280,6 +290,11 @@ public class FlowItemExecutor { return message != null && message.isNestedFlow() && message.getSubFlowOutcome() != null; } + private boolean isExecutionTerminated(String instId) { + TaskContext context = taskInstHolder.getContext(instId); + return context == null || context.isStopped(); + } + private void notifyFinished(Runnable onFinished, String instId, String nodeId) { try { onFinished.run(); diff --git a/cmvr-iot-test/src/main/java/com/cmvr/test/flow/runtime/interceptor/FlowAfterInterceptor.java b/cmvr-iot-test/src/main/java/com/cmvr/test/flow/runtime/interceptor/FlowAfterInterceptor.java index 83618a0..5ae2b59 100644 --- a/cmvr-iot-test/src/main/java/com/cmvr/test/flow/runtime/interceptor/FlowAfterInterceptor.java +++ b/cmvr-iot-test/src/main/java/com/cmvr/test/flow/runtime/interceptor/FlowAfterInterceptor.java @@ -32,7 +32,10 @@ public class FlowAfterInterceptor extends AbstractFlowMsgPreInterceptor{ return result; } TaskContext taskContext = taskInstHolder.getContext(message.getInstId()); - if (result == null && (taskContext == null || taskContext.isPaused() || taskContext.isStopped())) { + if (taskContext == null || taskContext.isStopped()) { + return null; + } + if (result == null && taskContext.isPaused()) { return null; } diff --git a/cmvr-iot-test/src/main/java/com/cmvr/test/flow/runtime/interceptor/FlowExceptionInterceptor.java b/cmvr-iot-test/src/main/java/com/cmvr/test/flow/runtime/interceptor/FlowExceptionInterceptor.java index 11b8b19..b9e67b5 100644 --- a/cmvr-iot-test/src/main/java/com/cmvr/test/flow/runtime/interceptor/FlowExceptionInterceptor.java +++ b/cmvr-iot-test/src/main/java/com/cmvr/test/flow/runtime/interceptor/FlowExceptionInterceptor.java @@ -53,3 +53,4 @@ public class FlowExceptionInterceptor extends AbstractFlowMsgPreInterceptor { } } } + diff --git a/cmvr-iot-test/src/main/java/com/cmvr/test/flow/runtime/interceptor/FlowLoggingInterceptor.java b/cmvr-iot-test/src/main/java/com/cmvr/test/flow/runtime/interceptor/FlowLoggingInterceptor.java index dbeefbb..c76c977 100644 --- a/cmvr-iot-test/src/main/java/com/cmvr/test/flow/runtime/interceptor/FlowLoggingInterceptor.java +++ b/cmvr-iot-test/src/main/java/com/cmvr/test/flow/runtime/interceptor/FlowLoggingInterceptor.java @@ -46,7 +46,7 @@ public class FlowLoggingInterceptor extends AbstractFlowMsgPreInterceptor { // 节点执行返回拦截 判断此时是否被终止或暂停 TaskContext context = taskInstHolder.getContext(message.getInstId()); - if (context == null || context.isPaused()) { + if (context == null || context.isPaused() || context.isStopped()) { // 确认是否已终止或已暂停 log.info("当前流程已经暂停/终止"); return null; diff --git a/cmvr-iot-test/src/main/java/com/cmvr/test/flow/runtime/operator/edge/AgvNavigationWaiter.java b/cmvr-iot-test/src/main/java/com/cmvr/test/flow/runtime/operator/edge/AgvNavigationWaiter.java index 3c47ffa..943e294 100644 --- a/cmvr-iot-test/src/main/java/com/cmvr/test/flow/runtime/operator/edge/AgvNavigationWaiter.java +++ b/cmvr-iot-test/src/main/java/com/cmvr/test/flow/runtime/operator/edge/AgvNavigationWaiter.java @@ -1,6 +1,6 @@ package com.cmvr.test.flow.runtime.operator.edge; -import cmvr.msgs.AgvUtils; +import cmvr.msgs.Agv; import com.cmvr.common.exception.GlobalException; import com.cmvr.edge.client.model.EdgeCommonVO; import com.cmvr.edge.client.service.EdgeAgvService; @@ -24,7 +24,7 @@ public final class AgvNavigationWaiter { private AgvNavigationWaiter() { } - public static AgvUtils.AgvNavigationStatus navigateToStationAndWait( + public static Agv.AgvNavigationStatus navigateToStationAndWait( EdgeAgvService edgeAgvService, EdgeCommonVO common, String stationId, @@ -34,13 +34,13 @@ public final class AgvNavigationWaiter { pollIntervalMs, null); } - public static AgvUtils.AgvNavigationStatus navigateToStationAndWait( + public static Agv.AgvNavigationStatus navigateToStationAndWait( EdgeAgvService edgeAgvService, EdgeCommonVO common, String stationId, long waitTimeoutMs, long pollIntervalMs, - AgvUtils.AgvMotionOptions options) { + Agv.AgvMotionOptions options) { if (waitTimeoutMs <= 0) { throw new GlobalException("导航等待超时时间必须大于0"); } @@ -50,7 +50,7 @@ public final class AgvNavigationWaiter { edgeAgvService.navigateToStation(common, stationId, options); long deadlineNanos = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(waitTimeoutMs); - AgvUtils.AgvNavigationStatus lastStatus = null; + Agv.AgvNavigationStatus lastStatus = null; while (System.nanoTime() < deadlineNanos) { lastStatus = edgeAgvService.getNavigationStatus(common); @@ -89,11 +89,11 @@ public final class AgvNavigationWaiter { throw new GlobalException("AGV导航超时({}毫秒):{}", waitTimeoutMs, stateMessage); } - private static GlobalException navigationFailed(String stateName, AgvUtils.AgvNavigationStatus status) { + private static GlobalException navigationFailed(String stateName, Agv.AgvNavigationStatus status) { return new GlobalException("AGV导航{}:{}", stateName, statusDescription(status)); } - private static String statusDescription(AgvUtils.AgvNavigationStatus status) { + private static String statusDescription(Agv.AgvNavigationStatus status) { String message = status.getMessage(); return message == null || message.trim().isEmpty() ? "状态=" + status.getState() + ",进度=" + status.getProgress() diff --git a/cmvr-iot-test/src/main/java/com/cmvr/test/flow/runtime/operator/edge/EdgeActionQueueOperateService.java b/cmvr-iot-test/src/main/java/com/cmvr/test/flow/runtime/operator/edge/EdgeActionQueueOperateService.java index 8945fe1..5e7c174 100644 --- a/cmvr-iot-test/src/main/java/com/cmvr/test/flow/runtime/operator/edge/EdgeActionQueueOperateService.java +++ b/cmvr-iot-test/src/main/java/com/cmvr/test/flow/runtime/operator/edge/EdgeActionQueueOperateService.java @@ -16,6 +16,7 @@ import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.List; +import java.util.UUID; @Service @RequiredArgsConstructor @@ -39,6 +40,7 @@ public class EdgeActionQueueOperateService implements EdgeOperateService { EdgeActionQueueVO request = new EdgeActionQueueVO(); request.setRobotId(robotId); request.setActionId(StringUtils.trimToNull(input.getString("actionId"))); + request.setActionId(UUID.randomUUID().toString()); request.setTotalTimeoutMs(input.getInteger("totalTimeoutMs")); List requestSteps = new ArrayList<>(); for (int i = 0; i < steps.size(); i++) { @@ -72,11 +74,8 @@ public class EdgeActionQueueOperateService implements EdgeOperateService { JSONObject output = new JSONObject(); output.put("success", true); - output.put("actionId", feedback.getActionId()); output.put("completedSteps", feedback.getCompletedSteps()); output.put("result", feedback.getResult().name()); - output.put("serviceInstanceId", feedback.getServiceInstanceId()); - output.put("deduplicationStatus", feedback.getDeduplicationStatus().name()); return TaskNodeExecuteResult.success(output); } diff --git a/cmvr-iot-test/src/main/java/com/cmvr/test/flow/runtime/operator/edge/EdgeAgvOperateService.java b/cmvr-iot-test/src/main/java/com/cmvr/test/flow/runtime/operator/edge/EdgeAgvOperateService.java index c697761..041dd37 100644 --- a/cmvr-iot-test/src/main/java/com/cmvr/test/flow/runtime/operator/edge/EdgeAgvOperateService.java +++ b/cmvr-iot-test/src/main/java/com/cmvr/test/flow/runtime/operator/edge/EdgeAgvOperateService.java @@ -1,6 +1,6 @@ package com.cmvr.test.flow.runtime.operator.edge; -import cmvr.msgs.AgvUtils; +import cmvr.msgs.Agv; import com.alibaba.fastjson2.JSONObject; import com.cmvr.common.exception.GlobalException; import com.cmvr.edge.client.model.EdgeCommonVO; @@ -59,7 +59,7 @@ public class EdgeAgvOperateService implements EdgeOperateService { } // 构建目标位姿 - AgvUtils.AgvPose2d pose = AgvUtils.AgvPose2d.newBuilder() + Agv.AgvPose2d pose = Agv.AgvPose2d.newBuilder() .setX(x) .setY(y) .setTheta(theta != null ? theta : 0.0) @@ -89,7 +89,7 @@ public class EdgeAgvOperateService implements EdgeOperateService { edgeCommonVO.setDeviceId(deviceId); long waitTimeoutMs = inputParams.getLongValue("waitTimeoutMs", AgvNavigationWaiter.DEFAULT_WAIT_TIMEOUT_MS); long pollIntervalMs = inputParams.getLongValue("pollIntervalMs", AgvNavigationWaiter.DEFAULT_POLL_INTERVAL_MS); - AgvUtils.AgvNavigationStatus status = AgvNavigationWaiter.navigateToStationAndWait( + Agv.AgvNavigationStatus status = AgvNavigationWaiter.navigateToStationAndWait( edgeAgvService, edgeCommonVO, stationId.trim(), waitTimeoutMs, pollIntervalMs, navigationOptions(inputParams)); return TaskNodeExecuteResult.success(new JSONObject() @@ -106,13 +106,13 @@ public class EdgeAgvOperateService implements EdgeOperateService { return TaskNodeExecuteResult.success(); } - private AgvUtils.AgvMotionOptions navigationOptions(JSONObject inputParams) { + private Agv.AgvMotionOptions navigationOptions(JSONObject inputParams) { int blockedTimeoutMs = inputParams.getIntValue( "blockedTimeoutMs", EdgeAgvService.DEFAULT_BLOCKED_TIMEOUT_MS); if (blockedTimeoutMs <= 0) { blockedTimeoutMs = EdgeAgvService.DEFAULT_BLOCKED_TIMEOUT_MS; } - return AgvUtils.AgvMotionOptions.newBuilder() + return Agv.AgvMotionOptions.newBuilder() .setAsynchronous(false) .setBlockedTimeoutMs(blockedTimeoutMs) .build(); diff --git a/cmvr-iot-test/src/main/java/com/cmvr/test/flow/runtime/operator/edge/EdgeDeviceCommandOperateService.java b/cmvr-iot-test/src/main/java/com/cmvr/test/flow/runtime/operator/edge/EdgeDeviceCommandOperateService.java index e55b268..4769ab8 100644 --- a/cmvr-iot-test/src/main/java/com/cmvr/test/flow/runtime/operator/edge/EdgeDeviceCommandOperateService.java +++ b/cmvr-iot-test/src/main/java/com/cmvr/test/flow/runtime/operator/edge/EdgeDeviceCommandOperateService.java @@ -89,4 +89,5 @@ public class EdgeDeviceCommandOperateService implements EdgeOperateService { return responseJson; } } + } diff --git a/cmvr-iot-test/src/main/java/com/cmvr/test/flow/runtime/operator/edge/EdgeTouchOperateService.java b/cmvr-iot-test/src/main/java/com/cmvr/test/flow/runtime/operator/edge/EdgeTouchOperateService.java index f4c4f30..049dd79 100644 --- a/cmvr-iot-test/src/main/java/com/cmvr/test/flow/runtime/operator/edge/EdgeTouchOperateService.java +++ b/cmvr-iot-test/src/main/java/com/cmvr/test/flow/runtime/operator/edge/EdgeTouchOperateService.java @@ -1,10 +1,12 @@ package com.cmvr.test.flow.runtime.operator.edge; +import cn.hutool.core.util.StrUtil; import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONArray; import com.alibaba.fastjson2.JSONObject; import com.cmvr.common.exception.GlobalException; -import com.cmvr.edge.client.model.arm.EdgeArmMoveJVO; -import com.cmvr.edge.client.service.EdgeArmService; +import com.cmvr.edge.client.model.hlc.EdgeTouchVO; +import com.cmvr.edge.client.service.EdgeHlcService; import com.cmvr.test.enums.ActionEnum; import com.cmvr.test.flow.runtime.message.TaskNodeExecuteMessage; import com.cmvr.test.flow.runtime.message.TaskNodeExecuteResult; @@ -12,47 +14,210 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; -import java.util.Objects; - - +/** Executes a screen touch through the edge HLC service. */ @Slf4j @Service @RequiredArgsConstructor public class EdgeTouchOperateService implements EdgeOperateService { - private final EdgeArmService edgeArmService; + private static final double DEFAULT_MAX_FORCE = 4D; + + private final EdgeHlcService edgeHlcService; + private final EdgeDeviceCommandOperateService edgeDeviceCommandOperateService; @Override public boolean supports(ActionEnum action) { - return action.name().equals("TOUCH"); + return ActionEnum.TOUCH == action; } @Override public TaskNodeExecuteResult execute(TaskNodeExecuteMessage message) { - ActionEnum action = message.getAction(); - JSONObject inputParams = message.getInputParams(); - String robotId = message.getRobotId(); - String deviceId = inputParams.getString("deviceId"); - String touchParams = inputParams.getString("touchParams"); - - if (Objects.requireNonNull(action) == ActionEnum.TOUCH) { - EdgeArmMoveJVO edgeArmMoveJVO = JSON.parseObject(touchParams, EdgeArmMoveJVO.class); - edgeArmMoveJVO.setRobotId(robotId); - edgeArmMoveJVO.setDeviceId(deviceId); - - edgeArmService.moveJ( - edgeArmMoveJVO, - edgeArmMoveJVO.getTarget(), - edgeArmMoveJVO.getVelocity(), - edgeArmMoveJVO.getAcceleration(), - edgeArmMoveJVO.getBlendRadius(), - edgeArmMoveJVO.getJointVelocityLimits(), - edgeArmMoveJVO.getAsynchronous() - ); - } else { - throw new GlobalException("不支持的触控操作类型: " + action); + if (1 == 1) { + try { + Thread.sleep(15000); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + return TaskNodeExecuteResult.success(); } - return TaskNodeExecuteResult.success(); + if (message == null || message.getAction() != ActionEnum.TOUCH) { + throw new GlobalException("不支持的触控操作类型"); + } + JSONObject input = message.getInputParams() == null ? new JSONObject() : message.getInputParams(); + String robotId = message.getRobotId(); + String deviceId = input.getString("deviceId"); + if (StrUtil.isBlank(robotId) || StrUtil.isBlank(deviceId)) { + throw new GlobalException("触控操作必须分配机器人和触控设备"); + } + + int[] point = resolvePoint(input); + if (point[0] == 0 && point[1] == 0) { + throw new GlobalException("触控坐标不能同时为0"); + } + + String operationType = StrUtil.blankToDefault(input.getString("operationType"), "CLICK").toUpperCase(); + int repeatCount = bounded(input.getInteger("repeatCount"), 1, 1, 20, "重复次数"); + int waitMs = bounded(input.getInteger("waitMs"), 1000, 0, 60000, "动作等待时间"); + JSONObject operationParams = resolveOperationParams(input.get("operationParams")); + JSONArray responses = new JSONArray(); + for (int index = 0; index < repeatCount; index++) { + if ("CLICK".equals(operationType)) { + responses.add(executeClick(robotId, deviceId, point, input, operationParams)); + } else if ("LONG_PRESS".equals(operationType) + || "SWIPE".equals(operationType) + || "EDGE_SCRIPT".equals(operationType)) { + responses.add(executeEdgeScript(message, deviceId, point, operationType, operationParams)); + } else { + throw new GlobalException("不支持的触控动作类型:" + operationType); + } + waitAfterAction(waitMs); + } + JSONObject output = new JSONObject(); + output.put("x", point[0]); + output.put("y", point[1]); + output.put("operationType", operationType); + output.put("repeatCount", repeatCount); + output.put("waitMs", waitMs); + output.put("result", responses.size() == 1 ? responses.get(0) : responses); + return TaskNodeExecuteResult.success(output); } + private Object executeClick(String robotId, String deviceId, int[] point, + JSONObject input, JSONObject operationParams) { + EdgeTouchVO request = new EdgeTouchVO(); + request.setRobotId(robotId); + request.setDeviceId(deviceId); + request.setX(point[0]); + request.setY(point[1]); + JSONObject touchParams = resolveJsonObject(input.get("touchParams"), "触控参数"); + Double maxForce = input.getDouble("maxForce"); + if (maxForce == null) maxForce = touchParams.getDouble("maxForce"); + if (maxForce == null) maxForce = operationParams.getDouble("maxForce"); + request.setMaxForce(maxForce == null || maxForce <= 0 ? DEFAULT_MAX_FORCE : maxForce); + return parseResponse(edgeHlcService.touch(request)); + } + + private Object executeEdgeScript(TaskNodeExecuteMessage source, String deviceId, int[] point, + String operationType, JSONObject operationParams) { + Object requestJson; + if ("EDGE_SCRIPT".equals(operationType) && operationParams.containsKey("requestJson")) { + requestJson = operationParams.get("requestJson"); + } else { + JSONObject command = new JSONObject(); + command.putAll(operationParams); + command.put("action", toScriptAction(operationType)); + command.put("x", point[0]); + command.put("y", point[1]); + if ("SWIPE".equals(operationType)) { + command.putIfAbsent("startX", point[0]); + command.putIfAbsent("startY", point[1]); + command.putIfAbsent("endX", point[0] + operationParams.getIntValue("offsetX", 0)); + command.putIfAbsent("endY", point[1] + operationParams.getIntValue("offsetY", 0)); + } + requestJson = command; + } + + TaskNodeExecuteMessage delegated = new TaskNodeExecuteMessage(); + delegated.setAction(ActionEnum.DEVICE_EXECUTE_JSON_COMMAND); + delegated.setRobotId(source.getRobotId()); + delegated.setInputParams(new JSONObject() + .fluentPut("deviceId", deviceId) + .fluentPut("requestJson", requestJson)); + TaskNodeExecuteResult result = edgeDeviceCommandOperateService.execute(delegated); + if (!result.isSuccess()) { + throw new GlobalException(StrUtil.blankToDefault(result.getErrorMsg(), "边端触控脚本执行失败")); + } + return result.getOutputParams(); + } + + private String toScriptAction(String operationType) { + if ("LONG_PRESS".equals(operationType)) return "longPress"; + if ("SWIPE".equals(operationType)) return "swipe"; + return "edgeScript"; + } + + private JSONObject resolveOperationParams(Object value) { + return resolveJsonObject(value, "触控操作参数"); + } + + private JSONObject resolveJsonObject(Object value, String name) { + if (value == null) return new JSONObject(); + if (value instanceof JSONObject) return (JSONObject) value; + try { + return value instanceof String ? JSON.parseObject((String) value) + : JSONObject.from(value); + } catch (RuntimeException exception) { + throw new GlobalException(name + "必须是有效的JSON对象"); + } + } + + private int bounded(Integer value, int defaultValue, int min, int max, String name) { + int result = value == null ? defaultValue : value; + if (result < min || result > max) { + throw new GlobalException(name + "必须在" + min + "到" + max + "之间"); + } + return result; + } + + private void waitAfterAction(int waitMs) { + if (waitMs <= 0) return; + try { + Thread.sleep(waitMs); + } catch (InterruptedException exception) { + Thread.currentThread().interrupt(); + throw new GlobalException("触控动作等待被中断"); + } + } + + private int[] resolvePoint(JSONObject input) { + JSONObject touchParams = resolveJsonObject(input.get("touchParams"), "触控参数"); + Integer x = input.getInteger("x"); + Integer y = input.getInteger("y"); + if (x == null) x = touchParams.getInteger("x"); + if (y == null) y = touchParams.getInteger("y"); + if (x != null && y != null) { + return new int[]{x, y}; + } + + Object coordinates = input.get("touchCoordinates"); + JSONArray values; + try { + if (coordinates instanceof JSONArray) { + values = (JSONArray) coordinates; + } else if (coordinates instanceof java.util.Collection) { + values = JSONArray.from(coordinates); + } else if (coordinates instanceof String && StrUtil.isNotBlank((String) coordinates)) { + values = JSON.parseArray(((String) coordinates).trim()); + } else { + values = null; + } + } catch (RuntimeException exception) { + throw new GlobalException("触控坐标格式错误,应为[x,y]或[x1,y1,x2,y2]"); + } + if (values == null || (values.size() != 2 && values.size() != 4)) { + throw new GlobalException("触控坐标格式错误,应为[x,y]或[x1,y1,x2,y2]"); + } + try { + if (values.size() == 2) { + return new int[]{values.getIntValue(0), values.getIntValue(1)}; + } + return new int[]{ + (values.getIntValue(0) + values.getIntValue(2)) / 2, + (values.getIntValue(1) + values.getIntValue(3)) / 2 + }; + } catch (RuntimeException exception) { + throw new GlobalException("触控坐标必须是数字"); + } + } + + private Object parseResponse(String response) { + if (StrUtil.isBlank(response)) { + return ""; + } + try { + return JSON.parse(response); + } catch (RuntimeException ignored) { + return response; + } + } } diff --git a/cmvr-iot-test/src/main/java/com/cmvr/test/flow/runtime/operator/edge/ti/TiTouchOperateService.java b/cmvr-iot-test/src/main/java/com/cmvr/test/flow/runtime/operator/edge/ti/TiTouchOperateService.java index ec9b3a6..b0fa6d5 100644 --- a/cmvr-iot-test/src/main/java/com/cmvr/test/flow/runtime/operator/edge/ti/TiTouchOperateService.java +++ b/cmvr-iot-test/src/main/java/com/cmvr/test/flow/runtime/operator/edge/ti/TiTouchOperateService.java @@ -67,10 +67,8 @@ public class TiTouchOperateService implements EdgeOperateService { String clickFunction = inputParams.getString("funcId"); // 查询功能 JSONObject funcObj = exTiVehicleFunctionService.findNextClickToFunction(vehicleConfigId, currentLocation, clickFunction); - // 输出 - output.put("nextLocation", funcObj.get("nextLocation")); - output.put("isEnd", funcObj.get("isEnd")); - output.put("iconUrl", funcObj.get("iconUrl")); + // 透传路径、动作类型和动作参数,供触控节点执行及报告追溯。 + output.putAll(funcObj); break; } case TI_TOUCH_COORDINATES: { 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 3884fd4..6879b8a 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 @@ -1,6 +1,6 @@ package com.cmvr.test.service; -import cmvr.msgs.AgvUtils; +import cmvr.msgs.Agv; import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.StrUtil; import com.alibaba.fastjson2.JSONObject; @@ -261,7 +261,7 @@ public class FlowActionExecutorService { } // 构建目标位姿 - AgvUtils.AgvPose2d pose = AgvUtils.AgvPose2d.newBuilder() + Agv.AgvPose2d pose = Agv.AgvPose2d.newBuilder() .setX(x) .setY(y) .setTheta(theta != null ? theta : 0.0) @@ -295,13 +295,13 @@ public class FlowActionExecutorService { } } - private AgvUtils.AgvMotionOptions buildAgvNavigationOptions(JSONObject payload) { + private Agv.AgvMotionOptions buildAgvNavigationOptions(JSONObject payload) { int blockedTimeoutMs = payload.getIntValue( "blockedTimeoutMs", EdgeAgvService.DEFAULT_BLOCKED_TIMEOUT_MS); if (blockedTimeoutMs <= 0) { blockedTimeoutMs = EdgeAgvService.DEFAULT_BLOCKED_TIMEOUT_MS; } - return AgvUtils.AgvMotionOptions.newBuilder() + return Agv.AgvMotionOptions.newBuilder() .setAsynchronous(false) .setBlockedTimeoutMs(blockedTimeoutMs) .build(); diff --git a/cmvr-iot-test/src/main/java/com/cmvr/test/service/ITeNodeInstService.java b/cmvr-iot-test/src/main/java/com/cmvr/test/service/ITeNodeInstService.java index f585cc8..35f1378 100644 --- a/cmvr-iot-test/src/main/java/com/cmvr/test/service/ITeNodeInstService.java +++ b/cmvr-iot-test/src/main/java/com/cmvr/test/service/ITeNodeInstService.java @@ -62,5 +62,8 @@ public interface ITeNodeInstService extends IService { /** Marks unfinished node records as stopped when their in-memory execution is gone. */ void stopUnfinished(String instId, String message); + + /** Marks all still-running node records as failed when the owning task fails. */ + void failUnfinished(String instId, String message); } diff --git a/cmvr-iot-test/src/main/java/com/cmvr/test/service/ex/ExTiVehicleFunctionService.java b/cmvr-iot-test/src/main/java/com/cmvr/test/service/ex/ExTiVehicleFunctionService.java index 980259b..0eaaa9c 100644 --- a/cmvr-iot-test/src/main/java/com/cmvr/test/service/ex/ExTiVehicleFunctionService.java +++ b/cmvr-iot-test/src/main/java/com/cmvr/test/service/ex/ExTiVehicleFunctionService.java @@ -11,4 +11,10 @@ public interface ExTiVehicleFunctionService { public JSONObject findNextClickToFunction(Long vehicleConfigId, String currentUi, String clickFunction); + /** 返回车型的界面节点、导航边以及配置问题,供后台可视化展示。 */ + JSONObject getTopology(Long vehicleConfigId); + + /** 检查项目所选功能能否从任意已配置界面到达。 */ + JSONObject inspectTopology(Long vehicleConfigId, java.util.List functionIds); + } diff --git a/cmvr-iot-test/src/main/java/com/cmvr/test/service/impl/ITeNodeInstServiceImpl.java b/cmvr-iot-test/src/main/java/com/cmvr/test/service/impl/ITeNodeInstServiceImpl.java index 0973913..91798be 100644 --- a/cmvr-iot-test/src/main/java/com/cmvr/test/service/impl/ITeNodeInstServiceImpl.java +++ b/cmvr-iot-test/src/main/java/com/cmvr/test/service/impl/ITeNodeInstServiceImpl.java @@ -154,6 +154,17 @@ public class ITeNodeInstServiceImpl extends ServiceImpl wrapper = Wrappers.lambdaUpdate(); + wrapper.eq(TeNodeInst::getInstId, instId) + .in(TeNodeInst::getStatus, TaskStatusEnum.RUNNING.name(), TaskStatusEnum.PAUSED.name()) + .set(TeNodeInst::getStatus, TaskStatusEnum.FAILED.name()) + .set(TeNodeInst::getMessage, message) + .set(TeNodeInst::getEndTime, System.currentTimeMillis()); + this.update(wrapper); + } } diff --git a/cmvr-iot-test/src/test/java/com/cmvr/test/flow/context/TaskInstHolderFailureTest.java b/cmvr-iot-test/src/test/java/com/cmvr/test/flow/context/TaskInstHolderFailureTest.java new file mode 100644 index 0000000..d2aeef5 --- /dev/null +++ b/cmvr-iot-test/src/test/java/com/cmvr/test/flow/context/TaskInstHolderFailureTest.java @@ -0,0 +1,66 @@ +package com.cmvr.test.flow.context; + +import com.cmvr.test.enums.TaskStatusEnum; +import com.cmvr.test.flow.runtime.event.FlowExecutionEventPublisher; +import com.cmvr.test.service.ITeNodeInstService; +import com.cmvr.test.service.ITeTaskInstService; +import org.junit.Test; + +import java.lang.reflect.Field; +import java.lang.reflect.Proxy; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +public class TaskInstHolderFailureTest { + + @Test + public void failureStopsContextReleasesLoopsAndFailsUnfinishedNodes() throws Exception { + AtomicBoolean unfinishedNodesFailed = new AtomicBoolean(); + AtomicReference persistedStatus = new AtomicReference<>(); + ITeNodeInstService nodeService = (ITeNodeInstService) Proxy.newProxyInstance( + ITeNodeInstService.class.getClassLoader(), new Class[]{ITeNodeInstService.class}, + (proxy, method, args) -> { + if ("failUnfinished".equals(method.getName())) unfinishedNodesFailed.set(true); + return null; + }); + ITeTaskInstService taskService = (ITeTaskInstService) Proxy.newProxyInstance( + ITeTaskInstService.class.getClassLoader(), new Class[]{ITeTaskInstService.class}, + (proxy, method, args) -> { + if ("updateStatus".equals(method.getName())) { + persistedStatus.set((TaskStatusEnum) args[1]); + return true; + } + return null; + }); + + TaskContextManager contextManager = new TaskContextManager(); + TaskContext context = new TaskContext(); + context.setInstId("inst-1"); + contextManager.register(context); + TaskThreadRegistry registry = new TaskThreadRegistry(); + CountDownLatch loopLatch = new CountDownLatch(1); + registry.registerLatch("inst-1", "loop-1", loopLatch); + + TaskInstHolder holder = new TaskInstHolder(nodeService, contextManager, taskService, + new FlowExecutionEventPublisher(Collections.emptyList())); + Field registryField = TaskInstHolder.class.getDeclaredField("taskThreadRegistry"); + registryField.setAccessible(true); + registryField.set(holder, registry); + + holder.markFailed("inst-1", "task-1", "item-1", "node-1", "FUNCTION", + "EDGE", "CAMERA_GETRGBIMAGE", "{}", "camera failed", List.of(1)); + + assertTrue(context.isStopped()); + assertEquals(0, loopLatch.getCount()); + assertTrue(unfinishedNodesFailed.get()); + assertEquals(TaskStatusEnum.FAILED, persistedStatus.get()); + assertNull(contextManager.get("inst-1")); + } +} diff --git a/cmvr-iot-test/src/test/java/com/cmvr/test/flow/runtime/dispatcher/FlowLoopNodeHandlerTest.java b/cmvr-iot-test/src/test/java/com/cmvr/test/flow/runtime/dispatcher/FlowLoopNodeHandlerTest.java new file mode 100644 index 0000000..200bd5a --- /dev/null +++ b/cmvr-iot-test/src/test/java/com/cmvr/test/flow/runtime/dispatcher/FlowLoopNodeHandlerTest.java @@ -0,0 +1,72 @@ +package com.cmvr.test.flow.runtime.dispatcher; + +import com.alibaba.fastjson2.JSONObject; +import com.cmvr.test.enums.NodeTypeEnum; +import com.cmvr.test.flow.builder.FlowGraph; +import com.cmvr.test.flow.builder.FlowNodeWrapper; +import com.cmvr.test.flow.context.TaskContext; +import com.cmvr.test.flow.context.TaskContextManager; +import com.cmvr.test.flow.context.TaskInstHolder; +import com.cmvr.test.flow.context.TaskThreadRegistry; +import com.cmvr.test.flow.runtime.engine.FlowItemExecutor; +import com.cmvr.test.flow.runtime.message.TaskNodeExecuteMessage; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.Assert.assertEquals; + +public class FlowLoopNodeHandlerTest { + + @Test + public void stopsRemainingIterationsAfterTaskContextIsRemoved() { + TaskContextManager contextManager = new TaskContextManager(); + TaskInstHolder holder = new TaskInstHolder(null, contextManager, null, null); + TaskContext context = new TaskContext(); + context.setInstId("inst-1"); + holder.register(context); + + AtomicInteger executions = new AtomicInteger(); + FlowItemExecutor executor = new FlowItemExecutor(null, List.of(), null, holder, + null, null, null, null) { + @Override + public void executeSubGraph(FlowGraph subGraph, TaskNodeExecuteMessage rootMessage, + Runnable onFinished, List iterations) { + executions.incrementAndGet(); + holder.unRegister(rootMessage.getInstId()); + onFinished.run(); + } + }; + + TaskThreadRegistry registry = new TaskThreadRegistry(); + FlowLoopNodeHandler handler = new FlowLoopNodeHandler(registry, holder) { + @Override + protected FlowItemExecutor getFlowItemExecutor() { + return executor; + } + }; + + FlowNodeWrapper loopNode = new FlowNodeWrapper(); + loopNode.setNodeId("loop-1"); + loopNode.setNodeType(NodeTypeEnum.LOOP); + loopNode.setSubGraph(new FlowGraph()); + LinkedHashMap nodes = new LinkedHashMap<>(); + nodes.put(loopNode.getNodeId(), loopNode); + FlowGraph graph = new FlowGraph(); + graph.setNodeMap(nodes); + + TaskNodeExecuteMessage message = new TaskNodeExecuteMessage(); + message.setInstId("inst-1"); + message.setNodeId(loopNode.getNodeId()); + message.setGraph(graph); + message.setIterations(new ArrayList<>()); + message.setInputParams(new JSONObject().fluentPut("loopNum", 999)); + + handler.handle(message); + + assertEquals(1, executions.get()); + } +} diff --git a/cmvr-iot-test/src/test/java/com/cmvr/test/flow/runtime/engine/FlowItemExecutorTest.java b/cmvr-iot-test/src/test/java/com/cmvr/test/flow/runtime/engine/FlowItemExecutorTest.java index dab2c43..f7acbdd 100644 --- a/cmvr-iot-test/src/test/java/com/cmvr/test/flow/runtime/engine/FlowItemExecutorTest.java +++ b/cmvr-iot-test/src/test/java/com/cmvr/test/flow/runtime/engine/FlowItemExecutorTest.java @@ -7,12 +7,18 @@ import com.cmvr.test.flow.builder.FlowParamDef; import com.cmvr.test.flow.context.TaskContext; import com.cmvr.test.flow.context.TaskContextManager; import com.cmvr.test.flow.context.TaskInstHolder; +import com.cmvr.test.flow.context.TaskThreadRegistry; +import com.cmvr.test.flow.runtime.dispatcher.FlowNodeTypeDispatch; +import com.cmvr.test.flow.runtime.engine.support.FlowExecutionChainBuilder; import com.cmvr.test.flow.runtime.message.TaskNodeExecuteMessage; +import com.cmvr.test.flow.runtime.message.TaskNodeExecuteResult; import org.junit.Test; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; import static org.junit.Assert.assertEquals; @@ -45,6 +51,45 @@ public class FlowItemExecutorTest { assertEquals(start.getNodeId(), executor.executedNodeId); } + @Test + public void doesNotScheduleSuccessorsWhenNodeEndsTheTask() { + TaskContextManager contextManager = new TaskContextManager(); + TaskInstHolder taskInstHolder = new TaskInstHolder(null, contextManager, null, null); + TaskContext context = new TaskContext(); + context.setInstId("inst-1"); + taskInstHolder.register(context); + + RecordingScheduler scheduler = new RecordingScheduler(); + FlowNodeTypeDispatch dispatch = new FlowNodeTypeDispatch(Map.of( + NodeTypeEnum.FUNCTION.name(), message -> { + taskInstHolder.unRegister(message.getInstId()); + return TaskNodeExecuteResult.success(); + })); + FlowItemExecutor executor = new FlowItemExecutor( + scheduler, List.of(), dispatch, taskInstHolder, + new FlowExecutionChainBuilder(), new TaskThreadRegistry(), + new FlowRuntimeAssignmentResolver(), null); + + FlowNodeWrapper node = new FlowNodeWrapper(); + node.setNodeId("node-1"); + node.setNodeName("node-1"); + node.setNodeType(NodeTypeEnum.FUNCTION); + LinkedHashMap nodes = new LinkedHashMap<>(); + nodes.put(node.getNodeId(), node); + FlowGraph graph = new FlowGraph(); + graph.setNodeMap(nodes); + + TaskNodeExecuteMessage message = new TaskNodeExecuteMessage(); + message.setInstId("inst-1"); + message.setTaskId("task-1"); + message.setItemId("item-1"); + + executor.executeNode(graph, node, node.getNodeId(), List.of(), message, + () -> { }, List.of()); + + assertEquals(0, scheduler.completed.get()); + } + private static class RecordingFlowItemExecutor extends FlowItemExecutor { private String executedNodeId; @@ -63,4 +108,19 @@ public class FlowItemExecutorTest { executedNodeId = node.getNodeId(); } } + + private static class RecordingScheduler extends FlowTaskScheduler { + private final AtomicInteger completed = new AtomicInteger(); + + private RecordingScheduler() { + super(null, null); + } + + @Override + public void markCompleted(String instId, String nodeId, FlowGraph graph, + FlowItemExecutor.NodeExecutor executorFunc, + List iterations) { + completed.incrementAndGet(); + } + } } diff --git a/cmvr-iot-test/src/test/java/com/cmvr/test/flow/runtime/operator/edge/AgvNavigationWaiterTest.java b/cmvr-iot-test/src/test/java/com/cmvr/test/flow/runtime/operator/edge/AgvNavigationWaiterTest.java index 0b90e14..03e364d 100644 --- a/cmvr-iot-test/src/test/java/com/cmvr/test/flow/runtime/operator/edge/AgvNavigationWaiterTest.java +++ b/cmvr-iot-test/src/test/java/com/cmvr/test/flow/runtime/operator/edge/AgvNavigationWaiterTest.java @@ -1,6 +1,6 @@ package com.cmvr.test.flow.runtime.operator.edge; -import cmvr.msgs.AgvUtils; +import cmvr.msgs.Agv; import com.cmvr.common.exception.GlobalException; import com.cmvr.edge.client.model.EdgeCommonVO; import com.cmvr.edge.client.service.EdgeAgvService; @@ -25,7 +25,7 @@ public class AgvNavigationWaiterTest { AtomicInteger queryCount = new AtomicInteger(); EdgeAgvService service = service(states, queryCount, new AtomicBoolean()); - AgvUtils.AgvNavigationStatus result = AgvNavigationWaiter.navigateToStationAndWait( + Agv.AgvNavigationStatus result = AgvNavigationWaiter.navigateToStationAndWait( service, common(), "station-A", 1000, 1); assertEquals(4, result.getState()); @@ -79,7 +79,7 @@ public class AgvNavigationWaiterTest { if (next != null) { lastState.set(next); } - return AgvUtils.AgvNavigationStatus.newBuilder() + return Agv.AgvNavigationStatus.newBuilder() .setState(lastState.get()) .setProgress(lastState.get() == 4 ? 1.0 : 0.5) .setMessage(lastState.get() == 5 ? "navigation failed" : "") diff --git a/cmvr-iot-test/src/test/java/com/cmvr/test/flow/runtime/operator/edge/EdgeActionQueueOperateServiceTest.java b/cmvr-iot-test/src/test/java/com/cmvr/test/flow/runtime/operator/edge/EdgeActionQueueOperateServiceTest.java index ad40335..7e94e7c 100644 --- a/cmvr-iot-test/src/test/java/com/cmvr/test/flow/runtime/operator/edge/EdgeActionQueueOperateServiceTest.java +++ b/cmvr-iot-test/src/test/java/com/cmvr/test/flow/runtime/operator/edge/EdgeActionQueueOperateServiceTest.java @@ -27,33 +27,31 @@ public class EdgeActionQueueOperateServiceTest { captured.set((EdgeActionQueueVO) args[0]); return SystemCommand.ActionQueueCommand.Feedback.newBuilder() .setHeader(Common.CommandHeader.Feedback.newBuilder().setSuccess(true)) - .setActionId("action-1") .setCompletedSteps(2) .setResult(SystemCommand.ActionResultCode.ACTION_RESULT_CODE_COMPLETED) - .setServiceInstanceId("instance-1") - .setDeduplicationStatus(SystemCommand.ActionDeduplicationStatus.ACTION_DEDUPLICATION_STATUS_ACCEPTED_NEW) .build(); }); EdgeActionQueueOperateService service = new EdgeActionQueueOperateService(edgeSystemService); JSONArray steps = new JSONArray() .fluentAdd(new JSONObject().fluentPut("stepId", "move") - .fluentPut("type", "AGV_NAVIGATE_TO_STATION") - .fluentPut("deviceBindingKey", "queue_agv") - .fluentPut("params", new JSONObject().fluentPut("stationId", "station-2"))) + .fluentPut("type", "ARM_MOVE_J") + .fluentPut("deviceBindingKey", "queue_arm") + .fluentPut("params", new JSONObject().fluentPut("positions", new JSONArray().fluentAdd(0D)))) .fluentAdd(new JSONObject().fluentPut("stepId", "wait") - .fluentPut("type", "DELAY") - .fluentPut("params", new JSONObject().fluentPut("durationMs", 500))); + .fluentPut("type", "ARM_MOVE_L") + .fluentPut("deviceId", "arm-2") + .fluentPut("params", new JSONObject().fluentPut("x", 0.1D))); TaskNodeExecuteMessage message = new TaskNodeExecuteMessage(); message.setAction(ActionEnum.SYSTEM_EXECUTE_ACTION_QUEUE); message.setRobotId("robot-1"); - message.setResolvedDeviceBindings(new JSONObject().fluentPut("queue_agv", "agv-2")); + message.setResolvedDeviceBindings(new JSONObject().fluentPut("queue_arm", "arm-1")); message.setInputParams(new JSONObject().fluentPut("totalTimeoutMs", 10000).fluentPut("steps", steps)); TaskNodeExecuteResult result = service.execute(message); assertTrue(result.isSuccess()); - assertEquals("agv-2", captured.get().getSteps().get(0).getDeviceId()); + assertEquals("arm-1", captured.get().getSteps().get(0).getDeviceId()); assertEquals("robot-1", captured.get().getRobotId()); assertEquals(2, result.getOutputParams().getIntValue("completedSteps")); } diff --git a/cmvr-iot-test/src/test/java/com/cmvr/test/flow/runtime/operator/edge/EdgeDeviceCommandOperateServiceTest.java b/cmvr-iot-test/src/test/java/com/cmvr/test/flow/runtime/operator/edge/EdgeDeviceCommandOperateServiceTest.java index 0b124f4..1e5eb10 100644 --- a/cmvr-iot-test/src/test/java/com/cmvr/test/flow/runtime/operator/edge/EdgeDeviceCommandOperateServiceTest.java +++ b/cmvr-iot-test/src/test/java/com/cmvr/test/flow/runtime/operator/edge/EdgeDeviceCommandOperateServiceTest.java @@ -87,7 +87,7 @@ public class EdgeDeviceCommandOperateServiceTest { } private EdgeSystemService edgeSystemService(AtomicReference captured, - Common.JsonDeviceCommand.Feedback feedback) { + Common.JsonDeviceCommand.Feedback feedback) { return (EdgeSystemService) Proxy.newProxyInstance( EdgeSystemService.class.getClassLoader(), new Class[]{EdgeSystemService.class}, @@ -108,4 +108,5 @@ public class EdgeDeviceCommandOperateServiceTest { .setResponseJson(responseJson) .build(); } + } diff --git a/cmvr-iot-test/src/test/java/com/cmvr/test/flow/runtime/operator/edge/EdgeTouchOperateServiceTest.java b/cmvr-iot-test/src/test/java/com/cmvr/test/flow/runtime/operator/edge/EdgeTouchOperateServiceTest.java new file mode 100644 index 0000000..37c8e15 --- /dev/null +++ b/cmvr-iot-test/src/test/java/com/cmvr/test/flow/runtime/operator/edge/EdgeTouchOperateServiceTest.java @@ -0,0 +1,139 @@ +package com.cmvr.test.flow.runtime.operator.edge; + +import cmvr.api.Common; +import com.alibaba.fastjson2.JSONArray; +import com.alibaba.fastjson2.JSONObject; +import com.cmvr.common.exception.GlobalException; +import com.cmvr.edge.client.model.hlc.EdgeTouchVO; +import com.cmvr.edge.client.model.system.EdgeSystemJsonCommandVO; +import com.cmvr.edge.client.service.EdgeHlcService; +import com.cmvr.edge.client.service.EdgeSystemService; +import com.cmvr.test.enums.ActionEnum; +import com.cmvr.test.flow.runtime.message.TaskNodeExecuteMessage; +import com.cmvr.test.flow.runtime.message.TaskNodeExecuteResult; +import org.junit.Test; + +import java.lang.reflect.Proxy; +import java.util.concurrent.atomic.AtomicReference; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class EdgeTouchOperateServiceTest { + + @Test + public void convertsBoundingBoxToCenterAndCallsHlc() { + AtomicReference captured = new AtomicReference<>(); + EdgeHlcService hlcService = proxy((method, args) -> { + if ("touch".equals(method.getName()) && args.length == 1) { + captured.set((EdgeTouchVO) args[0]); + return "{\"success\":true}"; + } + return null; + }); + EdgeTouchOperateService service = service(hlcService); + TaskNodeExecuteMessage message = message(new JSONObject() + .fluentPut("deviceId", "hlc-1") + .fluentPut("waitMs", 0) + .fluentPut("touchCoordinates", "[100,200,300,500]")); + + TaskNodeExecuteResult result = service.execute(message); + + assertTrue(result.isSuccess()); + assertEquals("robot-1", captured.get().getRobotId()); + assertEquals("hlc-1", captured.get().getDeviceId()); + assertEquals(200, captured.get().getX()); + assertEquals(350, captured.get().getY()); + assertEquals(4D, captured.get().getMaxForce(), 0.001D); + assertEquals(200, result.getOutputParams().getIntValue("x")); + } + + @Test + public void acceptsExplicitPointAndForce() { + AtomicReference captured = new AtomicReference<>(); + EdgeTouchOperateService service = service(proxy((method, args) -> { + if ("touch".equals(method.getName()) && args.length == 1) { + captured.set((EdgeTouchVO) args[0]); + return "ok"; + } + return null; + })); + + service.execute(message(new JSONObject() + .fluentPut("deviceId", "hlc-2") + .fluentPut("x", 16) + .fluentPut("y", 32) + .fluentPut("waitMs", 0) + .fluentPut("maxForce", 2.5D) + .fluentPut("touchCoordinates", new JSONArray().fluentAdd(1).fluentAdd(2)))); + + assertEquals(16, captured.get().getX()); + assertEquals(32, captured.get().getY()); + assertEquals(2.5D, captured.get().getMaxForce(), 0.001D); + } + + @Test(expected = GlobalException.class) + public void rejectsInvalidCoordinates() { + EdgeTouchOperateService service = service(proxy((method, args) -> null)); + service.execute(message(new JSONObject() + .fluentPut("deviceId", "hlc-1") + .fluentPut("touchCoordinates", "not-json"))); + } + + @Test + public void delegatesLongPressToEdgeScript() { + AtomicReference captured = new AtomicReference<>(); + EdgeSystemService edgeSystem = (EdgeSystemService) Proxy.newProxyInstance( + EdgeSystemService.class.getClassLoader(), new Class[]{EdgeSystemService.class}, + (proxy, method, args) -> { + if ("executeJsonCommand".equals(method.getName())) { + captured.set((EdgeSystemJsonCommandVO) args[0]); + return Common.JsonDeviceCommand.Feedback.newBuilder() + .setHeader(Common.CommandHeader.Feedback.newBuilder().setSuccess(true)) + .setResponseJson("{\"accepted\":true}") + .build(); + } + return null; + }); + EdgeTouchOperateService service = new EdgeTouchOperateService( + proxy((method, args) -> null), new EdgeDeviceCommandOperateService(edgeSystem)); + + TaskNodeExecuteResult result = service.execute(message(new JSONObject() + .fluentPut("deviceId", "hlc-script") + .fluentPut("touchCoordinates", "[20,40]") + .fluentPut("operationType", "LONG_PRESS") + .fluentPut("operationParams", "{\"durationMs\":1200}") + .fluentPut("waitMs", 0))); + + JSONObject command = JSONObject.parseObject(captured.get().getRequestJson()); + assertEquals("longPress", command.getString("action")); + assertEquals(20, command.getIntValue("x")); + assertEquals(1200, command.getIntValue("durationMs")); + assertEquals("LONG_PRESS", result.getOutputParams().getString("operationType")); + } + + private EdgeTouchOperateService service(EdgeHlcService hlcService) { + return new EdgeTouchOperateService(hlcService, new EdgeDeviceCommandOperateService(null)); + } + + private TaskNodeExecuteMessage message(JSONObject input) { + TaskNodeExecuteMessage message = new TaskNodeExecuteMessage(); + message.setAction(ActionEnum.TOUCH); + message.setRobotId("robot-1"); + message.setInputParams(input); + return message; + } + + @SuppressWarnings("unchecked") + private EdgeHlcService proxy(Invocation invocation) { + return (EdgeHlcService) Proxy.newProxyInstance( + EdgeHlcService.class.getClassLoader(), + new Class[]{EdgeHlcService.class}, + (proxy, method, args) -> invocation.invoke(method, args == null ? new Object[0] : args)); + } + + @FunctionalInterface + private interface Invocation { + Object invoke(java.lang.reflect.Method method, Object[] args) throws Throwable; + } +} diff --git a/cmvr-iot-ti/src/main/java/com/cmvr/ti/model/domain/TiVehicleFunction.java b/cmvr-iot-ti/src/main/java/com/cmvr/ti/model/domain/TiVehicleFunction.java index ba9a136..a53688e 100644 --- a/cmvr-iot-ti/src/main/java/com/cmvr/ti/model/domain/TiVehicleFunction.java +++ b/cmvr-iot-ti/src/main/java/com/cmvr/ti/model/domain/TiVehicleFunction.java @@ -27,4 +27,19 @@ public class TiVehicleFunction extends BaseEntity { @ApiModelProperty("图标URL") private String iconUrl; + + @ApiModelProperty("操作类型:CLICK、LONG_PRESS、SWIPE、EDGE_SCRIPT") + private String operationType; + + @ApiModelProperty("点击后跳转的界面字典键值;为空时按功能名称自动推断") + private String targetUiKey; + + @ApiModelProperty("操作参数JSON,例如长按时长、滑动终点或边端脚本参数") + private String operationParams; + + @ApiModelProperty("单次步骤重复次数") + private Integer repeatCount; + + @ApiModelProperty("重复动作及下一步前等待时间(毫秒)") + private Integer waitMs; } diff --git a/cmvr-iot-ti/src/main/java/com/cmvr/ti/model/vo/TiProjectReadinessVO.java b/cmvr-iot-ti/src/main/java/com/cmvr/ti/model/vo/TiProjectReadinessVO.java new file mode 100644 index 0000000..3479083 --- /dev/null +++ b/cmvr-iot-ti/src/main/java/com/cmvr/ti/model/vo/TiProjectReadinessVO.java @@ -0,0 +1,42 @@ +package com.cmvr.ti.model.vo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +@Data +@ApiModel("触控项目执行就绪检查") +public class TiProjectReadinessVO { + + private String projectId; + private boolean ready; + private boolean videoReady; + private boolean interactionReady; + private Integer functionCount; + private String workflowVersionId; + private Integer workflowVersionNo; + private List checks = new ArrayList<>(); + + public void addCheck(String code, String name, boolean passed, String message) { + CheckItem item = new CheckItem(); + item.setCode(code); + item.setName(name); + item.setPassed(passed); + item.setMessage(message); + checks.add(item); + } + + @Data + @ApiModel("触控项目就绪检查项") + public static class CheckItem { + private String code; + private String name; + private boolean passed; + + @ApiModelProperty("检查结果说明") + private String message; + } +} diff --git a/cmvr-iot-ti/src/main/java/com/cmvr/ti/service/ITiProjectService.java b/cmvr-iot-ti/src/main/java/com/cmvr/ti/service/ITiProjectService.java index e7057b4..92b8ff9 100644 --- a/cmvr-iot-ti/src/main/java/com/cmvr/ti/service/ITiProjectService.java +++ b/cmvr-iot-ti/src/main/java/com/cmvr/ti/service/ITiProjectService.java @@ -6,6 +6,7 @@ import com.cmvr.test.model.domain.TeTaskInst; import com.cmvr.test.model.vo.TeQueryTaskOrchestraItemVO; import com.cmvr.test.model.vo.TeTaskExecuteProjectVO; import com.cmvr.ti.model.domain.TiProject; +import com.cmvr.ti.model.vo.TiProjectReadinessVO; import java.util.List; @@ -22,6 +23,8 @@ public interface ITiProjectService extends IService { public List queryExecuteRunParams(String projectId); + public TiProjectReadinessVO checkExecutionReadiness(String projectId); + public String executeProject(TeTaskExecuteProjectVO taskExecuteProjectVO); public TeTaskInst getTeTaskInst(String projectId); diff --git a/cmvr-iot-ti/src/main/java/com/cmvr/ti/service/ex/ExTiVehicleFunctionServiceImpl.java b/cmvr-iot-ti/src/main/java/com/cmvr/ti/service/ex/ExTiVehicleFunctionServiceImpl.java index 02f1988..e8407be 100644 --- a/cmvr-iot-ti/src/main/java/com/cmvr/ti/service/ex/ExTiVehicleFunctionServiceImpl.java +++ b/cmvr-iot-ti/src/main/java/com/cmvr/ti/service/ex/ExTiVehicleFunctionServiceImpl.java @@ -2,15 +2,13 @@ package com.cmvr.ti.service.ex; import cn.hutool.core.text.StrPool; import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSONArray; import com.alibaba.fastjson2.JSONObject; import com.cmvr.common.core.domain.entity.SysDictData; import com.cmvr.common.exception.GlobalException; import com.cmvr.common.utils.StringUtils; -import com.cmvr.system.domain.vo.CarIcon; -import com.cmvr.system.domain.vo.CarUi; import com.cmvr.system.service.ISysDictTypeService; -import com.cmvr.system.util.CarPathFinderUtil; import com.cmvr.test.service.ex.ExTiVehicleFunctionService; import com.cmvr.ti.model.domain.TiVehicleFunction; import com.cmvr.ti.model.domain.TiVehicleUi; @@ -19,25 +17,33 @@ import com.cmvr.ti.service.ITiVehicleFunctionService; import com.cmvr.ti.service.ITiVehicleUiService; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import org.springframework.util.CollectionUtils; -import java.util.*; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; @Service @RequiredArgsConstructor public class ExTiVehicleFunctionServiceImpl implements ExTiVehicleFunctionService { private final ITiVehicleFunctionService tiVehicleFunctionService; - private final ITiVehicleUiService tiVehicleUiService; - - private final ISysDictTypeService iSysDictTypeService; - + private final ISysDictTypeService sysDictTypeService; @Override public JSONArray queryByIds(String ids) { - List tiVehicleFunctions = tiVehicleFunctionService.listByIds(StrUtil.split(StrPool.COMMA, ids)); - return JSONArray.from(tiVehicleFunctions); + if (StrUtil.isBlank(ids)) { + return new JSONArray(); + } + return JSONArray.from(tiVehicleFunctionService.listByIds(StrUtil.split(StrPool.COMMA, ids))); } @Override @@ -45,86 +51,347 @@ public class ExTiVehicleFunctionServiceImpl implements ExTiVehicleFunctionServic return JSONObject.from(tiVehicleFunctionService.getById(id)); } - /** - * 寻找从当前页面到能执行目标功能的最短点击路径 - * 返回:点击的功能序列(JSONArray 包装 List) - */ @Override - public JSONObject findNextClickToFunction( - Long vehicleConfigId, - String startUiName, - String targetFuncName) { - // 功能字典 - // 实际用的是 sys_dict_data,这里假设你有方法能查到 data 列表 - List functionDictData = iSysDictTypeService.selectDictDataByType("ti_function_config"); - - // 界面字典 - List uiDictData = iSysDictTypeService.selectDictDataByType("ti_ui_config"); - - // 构建 功能 value → label 的映射 - Map funcValueToLabel = new HashMap<>(); - for (SysDictData data : functionDictData) { - funcValueToLabel.put(data.getDictValue(), data.getDictLabel()); + public JSONObject findNextClickToFunction(Long vehicleConfigId, String startUiName, String targetFunction) { + Graph graph = loadGraph(vehicleConfigId); + if (!graph.issues.isEmpty()) { + throw new GlobalException("界面路径配置不完整:" + String.join(";", graph.issues)); + } + String startKey = graph.resolveUiKey(startUiName); + if (startKey == null) { + throw new GlobalException("未识别到已配置的当前界面:" + startUiName); + } + TiVehicleFunction target = graph.resolveFunction(targetFunction); + if (target == null) { + throw new GlobalException("目标功能不存在或不属于当前车型:" + targetFunction); + } + List path = graph.findPath(startKey, target.getId()); + if (path.isEmpty()) { + throw new GlobalException("从“" + graph.uiName(startKey) + "”无法到达功能“" + + graph.functionName(target) + "”,请补充界面功能或目标界面配置"); } - // 构建 界面 value → label 的映射 - Map uiValueToLabel = new HashMap<>(); - for (SysDictData data : uiDictData) { - uiValueToLabel.put(data.getDictValue(), data.getDictLabel()); + Step next = path.get(0); + JSONObject result = stepJson(graph, next); + result.put("nextLocation", graph.functionName(next.function)); + result.put("isEnd", path.size() == 1); + result.put("iconUrl", next.function.getIconUrl()); + JSONArray pathJson = new JSONArray(); + for (Step step : path) { + pathJson.add(stepJson(graph, step)); } - - // 1. 查询当前车型的实际界面和功能数据 - List uiList = tiVehicleUiService.selectTiVehicleUiList( - TiVehicleUi.builder().vehicleConfigId(vehicleConfigId).build()); - - List funcList = tiVehicleFunctionService.selectTiVehicleFunctionList( - TiVehicleFunctionVO.builder().vehicleConfigId(vehicleConfigId).build()); - - // 2. funcKey → TiVehicleFunction - Map funcById = new HashMap<>(); - for (TiVehicleFunction f : funcList) { - funcById.put(f.getId(), f); - } - // 3.组装uiMap - Map uiMap = new HashMap<>(); - List icons; - CarUi carUi; - for (TiVehicleUi ui : uiList) { - icons = new ArrayList<>(); - for (String funcId : StringUtils.split(ui.getFuncIds(), StrPool.COMMA)) { - TiVehicleFunction func = funcById.get(Long.valueOf(funcId)); - CarIcon icon = new CarIcon( - funcValueToLabel.get(func.getFuncKey()), - func.getIconUrl(), - "", - "", - "1".equals(func.getRemark()) ? funcValueToLabel.get(func.getFuncKey()) : "" - ); - icons.add(icon); - } - carUi = new CarUi( - uiValueToLabel.get(ui.getUiKey()), - ui.getUiUrl(), - icons - ); - uiMap.put(carUi.getUiName(), carUi); - } - // 4.获取路径 - List path = CarPathFinderUtil.findPath(uiMap, startUiName, targetFuncName); - if (CollectionUtils.isEmpty(path)) { - throw new GlobalException("没有找到匹配的路径"); - } - // 5.组装结果 - Map map = new HashMap<>(); - map.put("nextLocation", path.get(0).getIconName()); - map.put("isEnd", path.size() == 1); - map.put("iconUrl", path.get(0).getIconImage()); - - return JSONObject.from(map); - - + result.put("path", pathJson); + return result; } + @Override + public JSONObject getTopology(Long vehicleConfigId) { + Graph graph = loadGraph(vehicleConfigId); + JSONObject result = new JSONObject(); + JSONArray nodes = new JSONArray(); + JSONArray edges = new JSONArray(); + for (TiVehicleUi ui : graph.uis.values()) { + JSONObject node = new JSONObject(); + node.put("id", ui.getId()); + node.put("uiKey", ui.getUiKey()); + node.put("uiName", graph.uiName(ui.getUiKey())); + node.put("uiType", "VEHICLE_SCREEN"); + node.put("uiTypeName", "车机界面"); + node.put("imageUrl", ui.getUiUrl()); + JSONArray functions = new JSONArray(); + for (Long functionId : graph.functionIds(ui)) { + TiVehicleFunction function = graph.functions.get(functionId); + if (function != null) { + functions.add(functionJson(graph, function)); + String targetKey = graph.targetUiKey(function); + if (StrUtil.isNotBlank(targetKey)) { + JSONObject edge = functionJson(graph, function); + edge.put("sourceUiKey", ui.getUiKey()); + edge.put("sourceUiName", graph.uiName(ui.getUiKey())); + edge.put("targetUiKey", targetKey); + edge.put("targetUiName", graph.uiName(targetKey)); + edges.add(edge); + } + } + } + node.put("functions", functions); + nodes.add(node); + } + result.put("ready", graph.issues.isEmpty()); + result.put("nodes", nodes); + result.put("edges", edges); + result.put("issues", graph.issues); + return result; + } + @Override + public JSONObject inspectTopology(Long vehicleConfigId, List functionIds) { + Graph graph = loadGraph(vehicleConfigId); + List issues = new ArrayList<>(graph.issues); + Set unreachable = new HashSet<>(); + if (functionIds == null || functionIds.isEmpty()) { + issues.add("项目未选择测试功能"); + } else { + for (Long functionId : functionIds) { + TiVehicleFunction function = graph.functions.get(functionId); + if (function == null || !graph.attachedFunctionIds.contains(functionId)) { + issues.add("功能ID " + functionId + " 未关联到任何已配置界面"); + continue; + } + for (String startKey : graph.uis.keySet()) { + if (graph.findPath(startKey, functionId).isEmpty()) { + unreachable.add("“" + graph.uiName(startKey) + "”→“" + + graph.functionName(function) + "”"); + } + } + } + } + if (!unreachable.isEmpty()) { + issues.add("以下路径不可达:" + String.join("、", unreachable)); + } + JSONObject result = new JSONObject(); + result.put("ready", issues.isEmpty()); + result.put("issues", issues); + result.put("unreachable", unreachable); + result.put("topology", getTopology(vehicleConfigId)); + return result; + } + + private JSONObject stepJson(Graph graph, Step step) { + JSONObject result = functionJson(graph, step.function); + result.put("sourceUiKey", step.sourceUiKey); + result.put("sourceUiName", graph.uiName(step.sourceUiKey)); + result.put("targetUiKey", step.targetUiKey); + result.put("targetUiName", graph.uiName(step.targetUiKey)); + return result; + } + + private JSONObject functionJson(Graph graph, TiVehicleFunction function) { + JSONObject result = new JSONObject(); + result.put("functionId", function.getId()); + result.put("functionKey", function.getFuncKey()); + result.put("functionName", graph.functionName(function)); + result.put("iconUrl", function.getIconUrl()); + result.put("operationType", StrUtil.blankToDefault(function.getOperationType(), "CLICK")); + result.put("operationParams", parseParams(function.getOperationParams())); + result.put("repeatCount", function.getRepeatCount() == null ? 1 : function.getRepeatCount()); + result.put("waitMs", function.getWaitMs() == null ? 1000 : function.getWaitMs()); + result.put("targetUiKey", graph.targetUiKey(function)); + result.put("targetUiName", graph.uiName(graph.targetUiKey(function))); + return result; + } + + private JSONObject parseParams(String value) { + if (StrUtil.isBlank(value)) { + return new JSONObject(); + } + try { + return JSON.parseObject(value); + } catch (RuntimeException ignored) { + return new JSONObject(); + } + } + + private Graph loadGraph(Long vehicleConfigId) { + if (vehicleConfigId == null) { + throw new GlobalException("请选择车型配置"); + } + Map uiNames = dictMap("ti_ui_config"); + Map functionNames = dictMap("ti_function_config"); + List uiList = tiVehicleUiService.selectTiVehicleUiList( + TiVehicleUi.builder().vehicleConfigId(vehicleConfigId).build()); + List functionList = tiVehicleFunctionService.selectTiVehicleFunctionList( + TiVehicleFunctionVO.builder().vehicleConfigId(vehicleConfigId).build()); + return new Graph(uiList, functionList, uiNames, functionNames); + } + + private Map dictMap(String type) { + Map result = new HashMap<>(); + List values = sysDictTypeService.selectDictDataByType(type); + if (values != null) { + for (SysDictData value : values) { + result.put(value.getDictValue(), value.getDictLabel()); + } + } + return result; + } + + private static class Step { + private final String sourceUiKey; + private final String targetUiKey; + private final TiVehicleFunction function; + + private Step(String sourceUiKey, String targetUiKey, TiVehicleFunction function) { + this.sourceUiKey = sourceUiKey; + this.targetUiKey = targetUiKey; + this.function = function; + } + } + + private static class Graph { + private final Map uis = new LinkedHashMap<>(); + private final Map functions = new LinkedHashMap<>(); + private final Map uiNames; + private final Map functionNames; + private final Set attachedFunctionIds = new HashSet<>(); + private final List issues = new ArrayList<>(); + + private Graph(List uiList, List functionList, + Map uiNames, Map functionNames) { + this.uiNames = uiNames; + this.functionNames = functionNames; + for (TiVehicleFunction function : safe(functionList)) { + functions.put(function.getId(), function); + } + if (uiList == null || uiList.isEmpty()) { + issues.add("当前车型尚未配置车机界面"); + return; + } + for (TiVehicleUi ui : uiList) { + if (StrUtil.isBlank(ui.getUiKey()) || StrUtil.isBlank(uiName(ui.getUiKey()))) { + issues.add("界面ID " + ui.getId() + " 未配置有效的界面类型"); + continue; + } + if (uis.put(ui.getUiKey(), ui) != null) { + issues.add("界面“" + uiName(ui.getUiKey()) + "”重复配置"); + } + if (StrUtil.isBlank(ui.getUiUrl())) { + issues.add("界面“" + uiName(ui.getUiKey()) + "”缺少识别截图"); + } + String[] rawIds = StringUtils.split(ui.getFuncIds(), StrPool.COMMA); + if (rawIds == null || rawIds.length == 0) { + issues.add("界面“" + uiName(ui.getUiKey()) + "”未关联任何功能"); + continue; + } + for (String rawId : rawIds) { + if (StrUtil.isBlank(rawId)) continue; + try { + Long id = Long.valueOf(rawId); + attachedFunctionIds.add(id); + TiVehicleFunction function = functions.get(id); + if (function == null) { + issues.add("界面“" + uiName(ui.getUiKey()) + "”关联了无效功能ID " + id); + } else if (StrUtil.isBlank(function.getIconUrl())) { + issues.add("功能“" + functionName(function) + "”缺少识别图标"); + } + } catch (NumberFormatException exception) { + issues.add("界面“" + uiName(ui.getUiKey()) + "”包含非法功能ID " + rawId); + } + } + } + for (TiVehicleFunction function : functions.values()) { + String target = targetUiKey(function); + if (StrUtil.isNotBlank(target) && !uis.containsKey(target)) { + issues.add("功能“" + functionName(function) + "”跳转到未配置界面“" + + uiName(target) + "”"); + } + } + } + + private TiVehicleFunction resolveFunction(String value) { + if (StrUtil.isBlank(value)) return null; + if (value.matches("\\d+")) { + return functions.get(Long.valueOf(value)); + } + for (TiVehicleFunction function : functions.values()) { + if (StrUtil.equals(value, functionName(function)) || StrUtil.equals(value, function.getFuncKey())) { + return function; + } + } + return null; + } + + private String resolveUiKey(String value) { + if (StrUtil.isBlank(value)) return null; + String normalized = "主页".equals(value) ? "首页" : value; + if (uis.containsKey(normalized)) return normalized; + for (String key : uis.keySet()) { + if (StrUtil.equals(normalized, uiName(key))) return key; + } + return null; + } + + private String uiName(String key) { + return key == null ? null : StrUtil.blankToDefault(uiNames.get(key), key); + } + + private String functionName(TiVehicleFunction function) { + return StrUtil.blankToDefault(functionNames.get(function.getFuncKey()), function.getFuncKey()); + } + + private String targetUiKey(TiVehicleFunction function) { + if (StrUtil.isNotBlank(function.getTargetUiKey())) return function.getTargetUiKey(); + String name = functionName(function); + for (String key : uis.keySet()) { + if (StrUtil.equals(name, uiName(key))) return key; + } + return null; + } + + private List functionIds(TiVehicleUi ui) { + List result = new ArrayList<>(); + String[] rawIds = StringUtils.split(ui.getFuncIds(), StrPool.COMMA); + if (rawIds == null) return result; + for (String rawId : rawIds) { + if (StrUtil.isBlank(rawId)) continue; + try { + result.add(Long.valueOf(rawId)); + } catch (NumberFormatException ignored) { + // 配置问题已经在构建图时记录。 + } + } + return result; + } + + private List findPath(String startKey, Long targetFunctionId) { + if (!uis.containsKey(startKey) || !functions.containsKey(targetFunctionId)) { + return Collections.emptyList(); + } + ArrayDeque queue = new ArrayDeque<>(); + Map previous = new HashMap<>(); + Set visited = new HashSet<>(); + queue.add(startKey); + while (!queue.isEmpty()) { + String current = queue.removeFirst(); + if (!visited.add(current)) continue; + TiVehicleUi ui = uis.get(current); + if (functionIds(ui).contains(targetFunctionId)) { + List result = rebuildPath(startKey, current, previous); + result.add(new Step(current, targetUiKey(functions.get(targetFunctionId)), + functions.get(targetFunctionId))); + return result; + } + for (Long functionId : functionIds(ui)) { + TiVehicleFunction function = functions.get(functionId); + if (function == null) continue; + String next = targetUiKey(function); + if (StrUtil.isNotBlank(next) && uis.containsKey(next) && !visited.contains(next)) { + if (!previous.containsKey(next)) { + previous.put(next, new Step(current, next, function)); + } + queue.add(next); + } + } + } + return Collections.emptyList(); + } + + private List rebuildPath(String startKey, String destination, Map previous) { + List reversed = new ArrayList<>(); + String current = destination; + while (!Objects.equals(current, startKey)) { + Step step = previous.get(current); + if (step == null) return Collections.emptyList(); + reversed.add(step); + current = step.sourceUiKey; + } + Collections.reverse(reversed); + return reversed; + } + + private static Collection safe(Collection value) { + return value == null ? Collections.emptyList() : value; + } + } } diff --git a/cmvr-iot-ti/src/main/java/com/cmvr/ti/service/impl/TiProjectServiceImpl.java b/cmvr-iot-ti/src/main/java/com/cmvr/ti/service/impl/TiProjectServiceImpl.java index 0dcaa85..2bb9916 100644 --- a/cmvr-iot-ti/src/main/java/com/cmvr/ti/service/impl/TiProjectServiceImpl.java +++ b/cmvr-iot-ti/src/main/java/com/cmvr/ti/service/impl/TiProjectServiceImpl.java @@ -5,6 +5,8 @@ import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.StrUtil; import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONArray; +import com.alibaba.fastjson2.JSONObject; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; @@ -21,18 +23,22 @@ import com.cmvr.test.enums.RunModeEnum; import com.cmvr.test.flow.runtime.engine.FlowTaskRuntimeService; import com.cmvr.test.flow.runtime.event.FlowExecutionModuleCodes; import com.cmvr.test.model.domain.TeDetectionItem; +import com.cmvr.test.model.domain.TeDetectionItemVersion; import com.cmvr.test.model.domain.TeTaskConfigInfo; import com.cmvr.test.model.domain.TeTaskInst; import com.cmvr.test.model.vo.TeQueryTaskOrchestraItemVO; import com.cmvr.test.model.vo.TeTaskExecuteProjectVO; import com.cmvr.test.model.vo.TeTaskOrchestraVO; import com.cmvr.test.service.ITeDetectionItemService; +import com.cmvr.test.service.ITeDetectionItemVersionService; import com.cmvr.test.service.ITeTaskConfigInfoService; import com.cmvr.test.service.ITeTaskInstService; import com.cmvr.test.service.ITeTaskOrchestrationService; +import com.cmvr.test.service.ex.ExTiVehicleFunctionService; import com.cmvr.ti.mapper.TiProjectMapper; import com.cmvr.ti.model.domain.TiProject; import com.cmvr.ti.model.domain.TiVehicleFunction; +import com.cmvr.ti.model.vo.TiProjectReadinessVO; import com.cmvr.ti.service.ITiProjectService; import com.cmvr.ti.service.ITiVehicleFunctionService; import com.cmvr.ti.support.TiEvaluationRecorder; @@ -42,9 +48,16 @@ import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; import java.util.ArrayList; +import java.util.ArrayDeque; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; @Service @RequiredArgsConstructor @@ -52,10 +65,12 @@ public class TiProjectServiceImpl extends ServiceImpl ids = JSON.parseArray(tiProject.getTestFuncIds(), Long.class); - - List functions = tiVehicleFunctionService.queryByIds(ids); - - if (functions.size() != ids.size()) { - throw new GlobalException("存在无效的功能ID"); - } - - if (functions.stream() - .map(TiVehicleFunction::getVehicleConfigId) - .distinct() - .count() != 1) { - throw new GlobalException("功能ID不属于同一车型配置"); - } + requireProjectFunctions(tiProject); tiProject.setStatus("0"); int insert = this.baseMapper.insert(tiProject); @@ -112,6 +113,16 @@ public class TiProjectServiceImpl extends ServiceImpl functionIds = parseFunctionIds(project.getTestFuncIds()); + List functions = functionIds.isEmpty() + ? Collections.emptyList() : tiVehicleFunctionService.queryByIds(functionIds); + result.setFunctionCount(functionIds.size()); + + boolean functionsValid = !functionIds.isEmpty() + && functions.size() == new HashSet<>(functionIds).size() + && functions.stream().allMatch(item -> Objects.equals( + project.getVehicleConfigId(), item.getVehicleConfigId())); + result.addCheck("PROJECT_FUNCTIONS", "项目功能数据", functionsValid, + functionsValid ? "已选择" + functionIds.size() + "个当前车型功能" + : "请选择有效功能,并确保功能属于项目车型"); + + List missingIconIds = functions.stream() + .filter(item -> StrUtil.isBlank(item.getIconUrl())) + .map(TiVehicleFunction::getId) + .collect(Collectors.toList()); + boolean iconsReady = functionsValid && missingIconIds.isEmpty(); + result.addCheck("FUNCTION_ICONS", "功能图标", iconsReady, + iconsReady ? "功能图标已配置" + : "以下功能缺少图标,无法识别触控坐标:" + missingIconIds); + + JSONObject topology = exTiVehicleFunctionService.inspectTopology( + project.getVehicleConfigId(), functionIds); + boolean topologyReady = topology.getBooleanValue("ready"); + JSONArray topologyIssues = topology.getJSONArray("issues"); + result.addCheck("PATH_TOPOLOGY", "界面与路径配置", topologyReady, + topologyReady ? "所选功能可从全部已配置界面到达" + : (topologyIssues == null || topologyIssues.isEmpty() + ? "界面路径配置不完整" : topologyIssues.toJavaList(String.class).stream() + .limit(5).collect(Collectors.joining(";")))); + + List items = taskOrchestrationService.queryByTaskId(projectId); + boolean orchestrationReady = items.size() == 1 && StrUtil.isNotBlank(items.get(0).getVersionId()); + result.addCheck("PUBLISHED_WORKFLOW", "发布工作流", orchestrationReady, + orchestrationReady ? "已关联发布版 V" + items.get(0).getVersionNo() + : "项目必须关联一个已发布的触控工作流"); + + boolean videoReady = false; + boolean interactionReady = false; + boolean secureFlow = false; + boolean schemaReady = false; + if (orchestrationReady) { + TeQueryTaskOrchestraItemVO item = items.get(0); + result.setWorkflowVersionId(item.getVersionId()); + result.setWorkflowVersionNo(item.getVersionNo()); + TeDetectionItemVersion version = detectionItemVersionService.getVersion(item.getVersionId()); + WorkflowCheck check = inspectWorkflow(version.getFlowData()); + schemaReady = check.schemaReady; + videoReady = check.videoReady; + interactionReady = check.interactionReady; + secureFlow = check.secureFlow; + } + result.addCheck("RESOURCE_SCHEMA", "设备资源配置", schemaReady, + schemaReady ? "已配置执行机器人、摄像头和触控设备" + : "发布工作流需使用V2资源模型,并配置摄像头和触控设备"); + result.addCheck("VIDEO_PIPELINE", "视频录像链路", videoReady, + videoReady ? "录像开始、触控过程、录像停止链路完整" + : "发布工作流缺少完整的录像开始/停止链路"); + result.addCheck("TOUCH_PIPELINE", "触控执行链路", interactionReady, + interactionReady ? "坐标识别和内部触控节点已连接" + : "发布工作流缺少坐标识别或内部触控节点"); + result.addCheck("PORTABLE_WORKFLOW", "可部署性与安全性", secureFlow, + secureFlow ? "未发现固定Token或本机HTTP依赖" + : "工作流仍包含固定Token、localhost或127.0.0.1地址"); + + result.setVideoReady(videoReady); + result.setInteractionReady(interactionReady); + result.setReady(result.getChecks().stream().allMatch(TiProjectReadinessVO.CheckItem::isPassed)); + return result; + } + @Override public String executeProject(TeTaskExecuteProjectVO taskExecuteProjectVO) { TiProject project = getById(taskExecuteProjectVO.getProjectId()); if (project == null) { throw new GlobalException("触控项目不存在:" + taskExecuteProjectVO.getProjectId()); } +// TiProjectReadinessVO readiness = checkExecutionReadiness(project.getProjectId()); +// if (!readiness.isReady()) { +// String reasons = readiness.getChecks().stream() +// .filter(item -> !item.isPassed()) +// .map(TiProjectReadinessVO.CheckItem::getMessage) +// .collect(Collectors.joining(";")); +// throw new GlobalException("项目暂不具备执行条件:" + reasons); +// } TeTaskExecuteProjectVO normalVO = new TeTaskExecuteProjectVO(); normalVO.setProjectId(taskExecuteProjectVO.getProjectId()); - normalVO.setRunParams(taskExecuteProjectVO.getRunParams()); + normalVO.setRunParams(buildProjectRunParams(project, taskExecuteProjectVO.getRunParams())); normalVO.setRobotId(taskExecuteProjectVO.getRobotId()); normalVO.setRuntimeAssignments(taskExecuteProjectVO.getRuntimeAssignments()); normalVO.setRunMode(RunModeEnum.TI_PROJECT); @@ -245,6 +345,9 @@ public class TiProjectServiceImpl extends ServiceImpl requireProjectFunctions(TiProject project) { + if (project.getVehicleConfigId() == null) { + throw new GlobalException("请选择车型配置"); + } + List ids = parseFunctionIds(project.getTestFuncIds()); + if (ids.isEmpty()) { + throw new GlobalException("请至少选择一个测试功能"); + } + if (new HashSet<>(ids).size() != ids.size()) { + throw new GlobalException("测试功能不能重复"); + } + List functions = tiVehicleFunctionService.queryByIds(ids); + if (functions.size() != ids.size()) { + throw new GlobalException("存在无效的功能ID"); + } + if (functions.stream().anyMatch(item -> !Objects.equals( + project.getVehicleConfigId(), item.getVehicleConfigId()))) { + throw new GlobalException("所选功能必须全部属于项目车型"); + } + return functions; + } + + private List parseFunctionIds(String value) { + if (StrUtil.isBlank(value)) { + return Collections.emptyList(); + } + try { + List ids = JSON.parseArray(value, Long.class); + return ids == null ? Collections.emptyList() : ids; + } catch (RuntimeException exception) { + throw new GlobalException("测试功能数据格式错误,请重新选择功能"); + } + } + + private JSONObject buildProjectRunParams(TiProject project, JSONObject requested) { + JSONObject normalized = new JSONObject(); + if (requested != null) { + normalized.putAll(requested); + } + JSONArray functionIds = JSON.parseArray(project.getTestFuncIds()); + for (TeQueryTaskOrchestraItemVO item : taskOrchestrationService.queryByTaskId(project.getProjectId())) { + JSONObject itemParams = normalized.getJSONObject(item.getItemId()); + if (itemParams == null) { + itemParams = new JSONObject(); + } else { + itemParams = JSON.parseObject(itemParams.toJSONString()); + } + itemParams.put("funcIds", functionIds); + itemParams.put("vehicleConfigId", project.getVehicleConfigId()); + itemParams.put("projectId", project.getProjectId()); + normalized.put(item.getItemId(), itemParams); + } + return normalized; + } + + private WorkflowCheck inspectWorkflow(String flowData) { + WorkflowCheck result = new WorkflowCheck(); + if (StrUtil.isBlank(flowData)) { + return result; + } + String normalized = flowData.toLowerCase(); + result.secureFlow = !normalized.contains("127.0.0.1") + && !normalized.contains("localhost") + && !normalized.contains("authorization"); + try { + JSONObject flow = JSON.parseObject(flowData); + JSONArray nodes = flow.getJSONArray("nodes"); + JSONArray edges = flow.getJSONArray("edges"); + if (nodes == null || edges == null) { + return result; + } + + JSONObject start = null; + Map actionByNode = new HashMap<>(); + for (int i = 0; i < nodes.size(); i++) { + JSONObject node = nodes.getJSONObject(i); + String nodeId = node.getString("id"); + JSONObject properties = node.getJSONObject("properties"); + if (properties != null && StrUtil.isNotBlank(properties.getString("action"))) { + actionByNode.put(nodeId, properties.getString("action")); + } + if ("start".equalsIgnoreCase(node.getString("type"))) { + start = node; + } + } + + Integer schemaVersion = flow.getInteger("schemaVersion"); + JSONObject startProperties = start == null ? null : start.getJSONObject("properties"); + if (schemaVersion == null && startProperties != null) { + schemaVersion = startProperties.getInteger("schemaVersion"); + } + boolean cameraSlot = false; + boolean touchSlot = false; + JSONArray roles = startProperties == null ? null : startProperties.getJSONArray("resourceRoles"); + if (roles != null) { + for (int i = 0; i < roles.size(); i++) { + JSONArray slots = roles.getJSONObject(i).getJSONArray("deviceSlots"); + if (slots == null) continue; + for (int j = 0; j < slots.size(); j++) { + JSONObject slot = slots.getJSONObject(j); + cameraSlot |= slot.getIntValue("deviceKind") == 5; + touchSlot |= slot.getIntValue("deviceKind") == 2; + } + } + } + result.schemaReady = schemaVersion != null && schemaVersion >= 2 && cameraSlot && touchSlot; + + String recordStart = findNodeId(actionByNode, "CAMERA_RECORDING_START"); + String recordStop = findNodeId(actionByNode, "CAMERA_RECORDING_STOP"); + String coordinate = findNodeId(actionByNode, "TI_TOUCH_COORDINATES"); + String touch = findNodeId(actionByNode, "TOUCH"); + Map> adjacency = adjacency(edges); + result.videoReady = recordStart != null && recordStop != null + && isReachable(adjacency, recordStart, recordStop); + result.interactionReady = coordinate != null && touch != null + && isReachable(adjacency, coordinate, touch) + && result.videoReady; + } catch (RuntimeException ignored) { + // 解析失败会通过各检查项返回给前端,不在这里中断就绪检查接口。 + } + return result; + } + + private String findNodeId(Map actionByNode, String action) { + return actionByNode.entrySet().stream() + .filter(entry -> action.equalsIgnoreCase(entry.getValue())) + .map(Map.Entry::getKey) + .findFirst() + .orElse(null); + } + + private Map> adjacency(JSONArray edges) { + Map> result = new HashMap<>(); + for (int i = 0; i < edges.size(); i++) { + JSONObject edge = edges.getJSONObject(i); + String source = edge.getString("sourceNodeId"); + String target = edge.getString("targetNodeId"); + if (StrUtil.isNotBlank(source) && StrUtil.isNotBlank(target)) { + result.computeIfAbsent(source, key -> new HashSet<>()).add(target); + } + } + return result; + } + + private boolean isReachable(Map> adjacency, String source, String target) { + if (source == null || target == null) return false; + ArrayDeque queue = new ArrayDeque<>(); + Set visited = new HashSet<>(); + queue.add(source); + while (!queue.isEmpty()) { + String current = queue.removeFirst(); + if (!visited.add(current)) continue; + if (target.equals(current)) return true; + queue.addAll(adjacency.getOrDefault(current, Collections.emptySet())); + } + return false; + } + + private static class WorkflowCheck { + private boolean schemaReady; + private boolean videoReady; + private boolean interactionReady; + private boolean secureFlow; + } + private AeIResultDTO parseEvaluationResult(String resultJson) { if (StrUtil.isBlank(resultJson)) { return new AeIResultDTO(); diff --git a/cmvr-iot-ti/src/main/java/com/cmvr/ti/service/impl/TiVehicleFunctionServiceImpl.java b/cmvr-iot-ti/src/main/java/com/cmvr/ti/service/impl/TiVehicleFunctionServiceImpl.java index 4d4c097..965bb3b 100644 --- a/cmvr-iot-ti/src/main/java/com/cmvr/ti/service/impl/TiVehicleFunctionServiceImpl.java +++ b/cmvr-iot-ti/src/main/java/com/cmvr/ti/service/impl/TiVehicleFunctionServiceImpl.java @@ -8,6 +8,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.spring.service.impl.ServiceImpl; import com.cmvr.common.core.domain.entity.SysDictData; +import com.cmvr.common.exception.GlobalException; import com.cmvr.system.service.ISysDictDataService; import com.cmvr.system.service.ISysDictTypeService; import com.cmvr.ti.mapper.TiVehicleFunctionMapper; @@ -40,7 +41,8 @@ public class TiVehicleFunctionServiceImpl extends ServiceImpl 20) { + throw new GlobalException("重复次数必须在1到20之间"); + } + if (waitMs < 0 || waitMs > 60000) { + throw new GlobalException("动作等待时间必须在0到60000毫秒之间"); + } + function.setRepeatCount(repeatCount); + function.setWaitMs(waitMs); + if (StrUtil.isNotBlank(function.getOperationParams())) { + try { + JSONObject.parseObject(function.getOperationParams()); + } catch (RuntimeException exception) { + throw new GlobalException("操作参数必须是有效的JSON对象"); + } + } + } } diff --git a/cmvr-iot-ti/src/main/java/com/cmvr/ti/service/impl/TiVehicleUiServiceImpl.java b/cmvr-iot-ti/src/main/java/com/cmvr/ti/service/impl/TiVehicleUiServiceImpl.java index 0ecc97d..7172ba9 100644 --- a/cmvr-iot-ti/src/main/java/com/cmvr/ti/service/impl/TiVehicleUiServiceImpl.java +++ b/cmvr-iot-ti/src/main/java/com/cmvr/ti/service/impl/TiVehicleUiServiceImpl.java @@ -6,13 +6,19 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.spring.service.impl.ServiceImpl; import com.cmvr.ti.mapper.TiVehicleUiMapper; +import com.cmvr.common.exception.GlobalException; +import com.cmvr.common.utils.StringUtils; +import com.cmvr.ti.model.domain.TiVehicleFunction; import com.cmvr.ti.model.domain.TiVehicleUi; +import com.cmvr.ti.service.ITiVehicleFunctionService; import com.cmvr.ti.service.ITiVehicleUiService; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import java.util.Arrays; import java.util.List; +import java.util.ArrayList; +import java.util.Objects; /** * 车机界面Service业务层处理 @@ -21,6 +27,8 @@ import java.util.List; @RequiredArgsConstructor public class TiVehicleUiServiceImpl extends ServiceImpl implements ITiVehicleUiService { + private final ITiVehicleFunctionService tiVehicleFunctionService; + @Override public TiVehicleUi selectTiVehicleUiById(Long id) { return this.baseMapper.selectById(id); @@ -37,11 +45,13 @@ public class TiVehicleUiServiceImpl extends ServiceImpllambdaQuery() + .eq(TiVehicleUi::getVehicleConfigId, ui.getVehicleConfigId()) + .eq(TiVehicleUi::getUiKey, ui.getUiKey()) + .ne(ui.getId() != null, TiVehicleUi::getId, ui.getId())); + if (duplicateCount > 0) { + throw new GlobalException("同一车型不能重复配置相同界面"); + } + + List functionIds = new ArrayList<>(); + String[] rawFunctionIds = StringUtils.split(ui.getFuncIds(), ","); + if (rawFunctionIds == null || rawFunctionIds.length == 0) { + throw new GlobalException("请至少关联一个界面功能"); + } + for (String rawId : rawFunctionIds) { + if (StrUtil.isBlank(rawId)) continue; + try { + functionIds.add(Long.valueOf(rawId)); + } catch (NumberFormatException exception) { + throw new GlobalException("界面功能数据格式错误,请重新选择"); + } + } + List functions = tiVehicleFunctionService.listByIds(functionIds); + if (functions.size() != functionIds.size() + || functions.stream().anyMatch(item -> !Objects.equals( + item.getVehicleConfigId(), ui.getVehicleConfigId()))) { + throw new GlobalException("关联功能必须全部属于当前车型"); + } + } } diff --git a/cmvr-iot-ti/src/main/java/com/cmvr/ti/support/TiEvaluationRecorder.java b/cmvr-iot-ti/src/main/java/com/cmvr/ti/support/TiEvaluationRecorder.java index 769bf2e..581903d 100644 --- a/cmvr-iot-ti/src/main/java/com/cmvr/ti/support/TiEvaluationRecorder.java +++ b/cmvr-iot-ti/src/main/java/com/cmvr/ti/support/TiEvaluationRecorder.java @@ -4,6 +4,7 @@ import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.StrUtil; import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONArray; import com.alibaba.fastjson2.JSONObject; import com.cmvr.common.core.domain.entity.SysDictData; import com.cmvr.evaluation.model.domain.AeEvaluation; @@ -59,6 +60,9 @@ public class TiEvaluationRecorder { private final ConcurrentMap latestVideoUrls = new ConcurrentHashMap<>(); private final ConcurrentMap businessFailures = new ConcurrentHashMap<>(); private final ConcurrentMap> executionDetails = new ConcurrentHashMap<>(); + private final ConcurrentMap> executionSteps = new ConcurrentHashMap<>(); + private final ConcurrentMap> uiPaths = new ConcurrentHashMap<>(); + private final ConcurrentMap> screenshotUrls = new ConcurrentHashMap<>(); /** Creates one report row for each function selected by the touch project. */ public void initialize(String projectId, String instId) { @@ -240,6 +244,9 @@ public class TiEvaluationRecorder { result.setResponseTime(start == null ? null : Math.max(0L, System.currentTimeMillis() - start)); result.setVideoPath(videoUrl); result.setExecutionDetail(buildExecutionDetail(event.getInstId(), evaluation.getAeId())); + result.setExecutionSteps(buildExecutionSteps(event.getInstId(), evaluation.getAeId())); + result.setUiPath(buildStringEvidence(uiPaths, event.getInstId(), evaluation.getAeId())); + result.setScreenshotUrls(buildStringEvidence(screenshotUrls, event.getInstId(), evaluation.getAeId())); evaluation.setStatus(success ? STATUS_SUCCESS : STATUS_FAILED); evaluation.setVideoPath(videoUrl); @@ -334,6 +341,46 @@ public class TiEvaluationRecorder { String nodeName = StrUtil.blankToDefault(event.getNodeName(), event.getNodeId()); String detail = StrUtil.blankToDefault(nodeName, "流程节点") + statusText; executionDetails.computeIfAbsent(key, ignored -> new ConcurrentLinkedQueue<>()).add(detail); + if (event.getEventType() != FlowExecutionEvent.EventType.NODE_STARTED) { + recordStructuredStep(event, key, nodeName, statusText); + } + } + + private void recordStructuredStep(FlowExecutionEvent event, String key, + String nodeName, String statusText) { + JSONObject output = event.getOutputParams() == null ? new JSONObject() : event.getOutputParams(); + JSONObject source = output.getJSONObject("result"); + if (source == null) source = output; + + AeIResultDTO.ExecutionStep step = new AeIResultDTO.ExecutionStep(); + step.setTimestamp(System.currentTimeMillis()); + step.setNodeName(StrUtil.blankToDefault(nodeName, "流程节点")); + step.setAction(event.getAction() == null ? null : event.getAction().getAction()); + step.setStatus(event.getEventType() == FlowExecutionEvent.EventType.NODE_FAILED ? "FAILED" : "SUCCESS"); + step.setPageName(firstNotBlank(source.getString("pageName"), source.getString("currentLocation"))); + step.setTargetName(firstNotBlank(source.getString("nextLocation"), source.getString("targetName"))); + step.setOperationType(source.getString("operationType")); + Object coordinates = source.get("touchCoordinates"); + if (coordinates == null && source.containsKey("x") && source.containsKey("y")) { + coordinates = new JSONArray().fluentAdd(source.get("x")).fluentAdd(source.get("y")); + } + step.setCoordinates(coordinates == null ? null : JSON.toJSONString(coordinates)); + step.setScreenshotUrl(FlowMediaParamResolver.lastUrl(source, "imageUrl")); + step.setVideoUrl(FlowMediaParamResolver.lastUrl(source, "videoUrl")); + step.setMessage(event.getEventType() == FlowExecutionEvent.EventType.NODE_FAILED + ? StrUtil.blankToDefault(event.getErrorMessage(), statusText) : statusText); + executionSteps.computeIfAbsent(key, ignored -> new ConcurrentLinkedQueue<>()).add(step); + + appendEvidence(uiPaths, key, step.getPageName()); + JSONArray path = source.getJSONArray("path"); + if (path != null) { + for (int index = 0; index < path.size(); index++) { + JSONObject pathStep = path.getJSONObject(index); + appendEvidence(uiPaths, key, pathStep.getString("sourceUiName")); + appendEvidence(uiPaths, key, pathStep.getString("targetUiName")); + } + } + appendEvidence(screenshotUrls, key, step.getScreenshotUrl()); } private String buildExecutionDetail(String instId, String evaluationId) { @@ -347,6 +394,45 @@ public class TiEvaluationRecorder { return StrUtil.sub(String.join(";", details.subList(fromIndex, details.size())), 0, 2000); } + private List buildExecutionSteps(String instId, String evaluationId) { + List result = new ArrayList<>(); + result.addAll(executionSteps.getOrDefault(detailKey(instId, "*"), new ConcurrentLinkedQueue<>())); + result.addAll(executionSteps.getOrDefault(detailKey(instId, evaluationId), new ConcurrentLinkedQueue<>())); + result.sort((left, right) -> Long.compare( + left.getTimestamp() == null ? 0L : left.getTimestamp(), + right.getTimestamp() == null ? 0L : right.getTimestamp())); + for (int index = 0; index < result.size(); index++) { + result.get(index).setSequence(index + 1); + } + return result; + } + + private List buildStringEvidence( + ConcurrentMap> source, + String instId, String evaluationId) { + List result = new ArrayList<>(); + appendUnique(result, source.get(detailKey(instId, "*"))); + appendUnique(result, source.get(detailKey(instId, evaluationId))); + return result; + } + + private void appendEvidence(ConcurrentMap> target, + String key, String value) { + if (StrUtil.isBlank(value)) return; + target.computeIfAbsent(key, ignored -> new ConcurrentLinkedQueue<>()).add(value); + } + + private void appendUnique(List target, Iterable values) { + if (values == null) return; + for (String value : values) { + if (StrUtil.isNotBlank(value) && !target.contains(value)) target.add(value); + } + } + + private String firstNotBlank(String first, String second) { + return StrUtil.isNotBlank(first) ? first : second; + } + private List parseFunctionIds(String json) { if (StrUtil.isBlank(json)) { return Collections.emptyList(); @@ -394,6 +480,9 @@ public class TiEvaluationRecorder { private void clearRuntimeCache(String instId) { startedAt.keySet().removeIf(key -> key.startsWith(instId + "|")); executionDetails.keySet().removeIf(key -> key.startsWith(instId + "|")); + executionSteps.keySet().removeIf(key -> key.startsWith(instId + "|")); + uiPaths.keySet().removeIf(key -> key.startsWith(instId + "|")); + screenshotUrls.keySet().removeIf(key -> key.startsWith(instId + "|")); latestVideoUrls.remove(instId); businessFailures.remove(instId); initializationLocks.remove(instId); diff --git a/cmvr-iot-ti/src/test/java/com/cmvr/ti/support/TiEvaluationRecorderTest.java b/cmvr-iot-ti/src/test/java/com/cmvr/ti/support/TiEvaluationRecorderTest.java index 6b21c39..71f0542 100644 --- a/cmvr-iot-ti/src/test/java/com/cmvr/ti/support/TiEvaluationRecorderTest.java +++ b/cmvr-iot-ti/src/test/java/com/cmvr/ti/support/TiEvaluationRecorderTest.java @@ -47,6 +47,8 @@ public class TiEvaluationRecorderTest { assertEquals("成功", result.getOverallResult()); assertEquals("http://files/touch-1.mp4", result.getVideoPath()); assertTrue(result.getExecutionDetail().contains("结束录像执行完成")); + assertEquals(1, result.getExecutionSteps().size()); + assertEquals("CAMERA_RECORDING_STOP", result.getExecutionSteps().get(0).getAction()); assertEquals("2", fixture.project.getStatus()); } diff --git a/sql/ti_touch_interaction_v2.sql b/sql/ti_touch_interaction_v2.sql new file mode 100644 index 0000000..6d308fa --- /dev/null +++ b/sql/ti_touch_interaction_v2.sql @@ -0,0 +1,26 @@ +-- 触控交互路径及动作配置。部署新版后端前执行一次。 +ALTER TABLE `ti_vehicle_function` + ADD COLUMN `operation_type` varchar(20) NOT NULL DEFAULT 'CLICK' + COMMENT 'CLICK/LONG_PRESS/SWIPE/EDGE_SCRIPT' AFTER `icon_url`, + ADD COLUMN `target_ui_key` varchar(50) DEFAULT NULL + COMMENT '点击后跳转的UI字典键值' AFTER `operation_type`, + ADD COLUMN `operation_params` varchar(1000) DEFAULT NULL + COMMENT '操作参数JSON' AFTER `target_ui_key`, + ADD COLUMN `repeat_count` int NOT NULL DEFAULT 1 + COMMENT '动作重复次数' AFTER `operation_params`, + ADD COLUMN `wait_ms` int NOT NULL DEFAULT 1000 + COMMENT '动作后等待时间(毫秒)' AFTER `repeat_count`; + +-- 兼容历史数据:名称与UI名称一致的功能自动成为导航边。 +UPDATE ti_vehicle_function function_item +JOIN sys_dict_data function_dict + ON function_dict.dict_type = 'ti_function_config' + AND function_dict.dict_value COLLATE utf8mb4_general_ci = function_item.func_key COLLATE utf8mb4_general_ci +JOIN sys_dict_data ui_dict + ON ui_dict.dict_type = 'ti_ui_config' + AND ui_dict.dict_label COLLATE utf8mb4_general_ci = function_dict.dict_label COLLATE utf8mb4_general_ci +JOIN ti_vehicle_ui ui + ON ui.vehicle_config_id = function_item.vehicle_config_id + AND ui.ui_key COLLATE utf8mb4_general_ci = ui_dict.dict_value COLLATE utf8mb4_general_ci +SET function_item.target_ui_key = ui_dict.dict_value +WHERE function_item.target_ui_key IS NULL;