feat(model): 更新AI评估结果模型并重构AGV协议定义

- 在AeIResultDTO中新增结构化执行步骤、界面路径和执行证据截图字段
- 在AeViEvaluationVO中新增执行过程相关的结构化步骤字段
- 将agv_utils.proto中的消息定义整合到agv.proto中
- 从arm_command.proto中补充完整的机械臂控制命令定义
- 更新arm_service.proto中机械臂服务接口定义
- 移除废弃的arm_teleop_v1.proto文件
- 修改AgvNavigationWaiter类中引用的消息类型包名
- 更新应用配置文件中的激活环境设置
This commit is contained in:
lixiaolong 2026-09-23 14:24:44 +08:00
parent 95c3030b91
commit 01da7a2dd4
68 changed files with 2143 additions and 1477 deletions

View File

@ -1,6 +1,6 @@
package com.cmvr.web.controller.api; package com.cmvr.web.controller.api;
import cmvr.msgs.AgvUtils; import cmvr.msgs.Agv;
import com.cmvr.common.core.domain.AjaxResult; import com.cmvr.common.core.domain.AjaxResult;
import com.cmvr.edge.client.model.EdgeCommonVO; import com.cmvr.edge.client.model.EdgeCommonVO;
import com.cmvr.edge.client.model.agv.EdgeAgvMapNameVO; import com.cmvr.edge.client.model.agv.EdgeAgvMapNameVO;
@ -58,7 +58,7 @@ public class EdgeAgvController {
@ApiParam(value = "设备ID", required = true) @ApiParam(value = "设备ID", required = true)
@RequestParam("deviceId") String deviceId) { @RequestParam("deviceId") String deviceId) {
EdgeCommonVO vo = commonVo(robotId, deviceId); EdgeCommonVO vo = commonVo(robotId, deviceId);
AgvUtils.AgvRuntimeState state = edgeAgvService.getRuntimeState(vo); Agv.AgvRuntimeState state = edgeAgvService.getRuntimeState(vo);
return AjaxResult.ok(toRuntimeStateMap(state)); return AjaxResult.ok(toRuntimeStateMap(state));
} }
@ -73,7 +73,7 @@ public class EdgeAgvController {
@ApiParam(value = "设备ID", required = true) @ApiParam(value = "设备ID", required = true)
@RequestParam("deviceId") String deviceId) { @RequestParam("deviceId") String deviceId) {
EdgeCommonVO vo = commonVo(robotId, deviceId); EdgeCommonVO vo = commonVo(robotId, deviceId);
AgvUtils.AgvNavigationStatus status = edgeAgvService.getNavigationStatus(vo); Agv.AgvNavigationStatus status = edgeAgvService.getNavigationStatus(vo);
Map<String, Object> resultMap = new HashMap<>(); Map<String, Object> resultMap = new HashMap<>();
resultMap.put("state", status.getState()); resultMap.put("state", status.getState());
resultMap.put("type", status.getType()); resultMap.put("type", status.getType());
@ -108,7 +108,7 @@ public class EdgeAgvController {
@ApiOperation("AGV重定位") @ApiOperation("AGV重定位")
@PostMapping("/relocalize") @PostMapping("/relocalize")
public AjaxResult relocalize(@RequestBody @Validated EdgeAgvRelocalizeVO vo) { public AjaxResult relocalize(@RequestBody @Validated EdgeAgvRelocalizeVO vo) {
AgvUtils.AgvPose2d pose = AgvUtils.AgvPose2d.newBuilder() Agv.AgvPose2d pose = Agv.AgvPose2d.newBuilder()
.setX(vo.getX()) .setX(vo.getX())
.setY(vo.getY()) .setY(vo.getY())
.setTheta(vo.getTheta()) .setTheta(vo.getTheta())
@ -120,13 +120,13 @@ public class EdgeAgvController {
@ApiOperation("固定距离平移") @ApiOperation("固定距离平移")
@PostMapping("/translate") @PostMapping("/translate")
public AjaxResult translate(@RequestBody @Validated EdgeAgvTranslateVO vo) { public AjaxResult translate(@RequestBody @Validated EdgeAgvTranslateVO vo) {
AgvUtils.AgvTranslationMode mode = vo.getMode() == null Agv.AgvTranslationMode mode = vo.getMode() == null
? AgvUtils.AgvTranslationMode.AGV_TRANSLATION_MODE_ODOMETRY ? Agv.AgvTranslationMode.AGV_TRANSLATION_MODE_ODOMETRY
: AgvUtils.AgvTranslationMode.forNumber(vo.getMode()); : Agv.AgvTranslationMode.forNumber(vo.getMode());
if (mode == null) { if (mode == null) {
return AjaxResult.error("距离参考模式不合法,允许值:0=里程,1=定位"); return AjaxResult.error("距离参考模式不合法,允许值:0=里程,1=定位");
} }
AgvUtils.AgvTranslation translation = AgvUtils.AgvTranslation.newBuilder() Agv.AgvTranslation translation = Agv.AgvTranslation.newBuilder()
.setDistance(vo.getDistance()) .setDistance(vo.getDistance())
.setVx(vo.getVx()) .setVx(vo.getVx())
.setVy(vo.getVy()) .setVy(vo.getVy())
@ -142,7 +142,7 @@ public class EdgeAgvController {
@ApiOperation("导航到指定位置") @ApiOperation("导航到指定位置")
@PostMapping("/navigateToPose") @PostMapping("/navigateToPose")
public AjaxResult navigateToPose(@RequestBody @Validated EdgeAgvNavigateToPoseVO vo) { public AjaxResult navigateToPose(@RequestBody @Validated EdgeAgvNavigateToPoseVO vo) {
AgvUtils.AgvPose2d pose = AgvUtils.AgvPose2d.newBuilder() Agv.AgvPose2d pose = Agv.AgvPose2d.newBuilder()
.setX(vo.getX()) .setX(vo.getX())
.setY(vo.getY()) .setY(vo.getY())
.setTheta(vo.getTheta()) .setTheta(vo.getTheta())
@ -197,7 +197,7 @@ public class EdgeAgvController {
@ApiOperation("设置速度") @ApiOperation("设置速度")
@PostMapping("/setVelocity") @PostMapping("/setVelocity")
public AjaxResult setVelocity(@RequestBody @Validated EdgeAgvVelocityVO vo) { public AjaxResult setVelocity(@RequestBody @Validated EdgeAgvVelocityVO vo) {
AgvUtils.AgvVelocity velocity = AgvUtils.AgvVelocity.newBuilder() Agv.AgvVelocity velocity = Agv.AgvVelocity.newBuilder()
.setVx(vo.getVx()) .setVx(vo.getVx())
.setVy(vo.getVy()) .setVy(vo.getVy())
.setWz(vo.getWz()) .setWz(vo.getWz())
@ -242,9 +242,9 @@ public class EdgeAgvController {
@ApiParam(value = "设备ID", required = true) @ApiParam(value = "设备ID", required = true)
@RequestParam("deviceId") String deviceId) { @RequestParam("deviceId") String deviceId) {
EdgeCommonVO vo = commonVo(robotId, deviceId); EdgeCommonVO vo = commonVo(robotId, deviceId);
List<AgvUtils.AgvStation> stations = edgeAgvService.listStations(vo); List<Agv.AgvStation> stations = edgeAgvService.listStations(vo);
List<Map<String, Object>> stationList = new ArrayList<>(); List<Map<String, Object>> stationList = new ArrayList<>();
for (AgvUtils.AgvStation station : stations) { for (Agv.AgvStation station : stations) {
stationList.add(toStationMap(station)); stationList.add(toStationMap(station));
} }
return AjaxResult.ok(stationList); return AjaxResult.ok(stationList);
@ -296,7 +296,7 @@ public class EdgeAgvController {
@ApiOperation("开始建图") @ApiOperation("开始建图")
@PostMapping("/startMapping") @PostMapping("/startMapping")
public AjaxResult startMapping(@RequestBody @Validated EdgeAgvStartMappingVO vo) { 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) { if (mapDimension == null) {
return AjaxResult.error("建图维度不合法,允许值:0=未指定,1=2D,2=3D,3=2D+3D"); return AjaxResult.error("建图维度不合法,允许值:0=未指定,1=2D,2=3D,3=2D+3D");
} }
@ -327,16 +327,16 @@ public class EdgeAgvController {
return vo; return vo;
} }
private AgvUtils.AgvMotionOptions navigationOptions(Integer blockedTimeoutMs) { private Agv.AgvMotionOptions navigationOptions(Integer blockedTimeoutMs) {
int timeout = blockedTimeoutMs == null int timeout = blockedTimeoutMs == null
? EdgeAgvService.DEFAULT_BLOCKED_TIMEOUT_MS : blockedTimeoutMs; ? EdgeAgvService.DEFAULT_BLOCKED_TIMEOUT_MS : blockedTimeoutMs;
return AgvUtils.AgvMotionOptions.newBuilder() return Agv.AgvMotionOptions.newBuilder()
.setAsynchronous(false) .setAsynchronous(false)
.setBlockedTimeoutMs(timeout) .setBlockedTimeoutMs(timeout)
.build(); .build();
} }
private Map<String, Object> toRuntimeStateMap(AgvUtils.AgvRuntimeState state) { private Map<String, Object> toRuntimeStateMap(Agv.AgvRuntimeState state) {
Map<String, Object> resultMap = new HashMap<>(); Map<String, Object> resultMap = new HashMap<>();
resultMap.put("timestamp", state.getTimestamp()); resultMap.put("timestamp", state.getTimestamp());
resultMap.put("mode", state.getMode()); resultMap.put("mode", state.getMode());
@ -375,7 +375,7 @@ public class EdgeAgvController {
/** /**
* 将 protobuf 站点对象转换为普通 Map。 * 将 protobuf 站点对象转换为普通 Map。
*/ */
private Map<String, Object> toStationMap(AgvUtils.AgvStation station) { private Map<String, Object> toStationMap(Agv.AgvStation station) {
Map<String, Object> stationMap = new HashMap<>(); Map<String, Object> stationMap = new HashMap<>();
stationMap.put("id", station.getId()); stationMap.put("id", station.getId());
stationMap.put("type", station.getType()); stationMap.put("type", station.getType());
@ -389,7 +389,7 @@ public class EdgeAgvController {
/** /**
* 将 protobuf 二维位姿转换为普通 Map。 * 将 protobuf 二维位姿转换为普通 Map。
*/ */
private Map<String, Object> toPoseMap(AgvUtils.AgvPose2d pose) { private Map<String, Object> toPoseMap(Agv.AgvPose2d pose) {
Map<String, Object> poseMap = new HashMap<>(); Map<String, Object> poseMap = new HashMap<>();
poseMap.put("x", pose.getX()); poseMap.put("x", pose.getX());
poseMap.put("y", pose.getY()); poseMap.put("y", pose.getY());

View File

@ -72,6 +72,35 @@ public class EdgeArmController {
edgeArmService.executeJsonCommand(request))); 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("关节空间运动") @ApiOperation("关节空间运动")
@PostMapping("/moveJ") @PostMapping("/moveJ")
public AjaxResult moveJ(@RequestBody EdgeArmMoveJVO moveJVO) { public AjaxResult moveJ(@RequestBody EdgeArmMoveJVO moveJVO) {

View File

@ -73,6 +73,12 @@ public class TiProjectController extends BaseController {
return AjaxResult.ok(tiProjectService.queryExecuteRunParams(projectId)); return AjaxResult.ok(tiProjectService.queryExecuteRunParams(projectId));
} }
@ApiOperation("检查触控项目、录像和工作流是否具备执行条件")
@GetMapping("/readiness/{projectId}")
public AjaxResult readiness(@PathVariable("projectId") String projectId) {
return AjaxResult.ok(tiProjectService.checkExecutionReadiness(projectId));
}
@ApiOperation("执行项目") @ApiOperation("执行项目")
@PostMapping("/execute") @PostMapping("/execute")
public AjaxResult execute(@RequestBody TeTaskExecuteProjectVO taskExecuteProjectVO) { public AjaxResult execute(@RequestBody TeTaskExecuteProjectVO taskExecuteProjectVO) {

View File

@ -21,6 +21,7 @@ import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.List; import java.util.List;
@ -78,4 +79,11 @@ public class TiVehicleUiController extends BaseController {
public AjaxResult findNextClickToFunction(Long vehicleConfigId, String currentUi, String clickFunction) { public AjaxResult findNextClickToFunction(Long vehicleConfigId, String currentUi, String clickFunction) {
return AjaxResult.success(exTiVehicleFunctionService.findNextClickToFunction(vehicleConfigId, currentUi, 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));
}
} }

View File

@ -60,7 +60,7 @@ spring:
# 国际化资源文件路径 # 国际化资源文件路径
basename: i18n/messages basename: i18n/messages
profiles: profiles:
active: aima active: test
# 文件上传 # 文件上传
servlet: servlet:
multipart: multipart:

View File

@ -1,7 +1,6 @@
package com.cmvr.edge.client.manage; package com.cmvr.edge.client.manage;
import cmvr.api.*; import cmvr.api.*;
import cmvr.api.armteleop.v1.ArmTeleopServiceGrpc;
import com.cmvr.common.robot.RobotEndpoint; import com.cmvr.common.robot.RobotEndpoint;
import com.cmvr.common.robot.RobotEndpointResolver; import com.cmvr.common.robot.RobotEndpointResolver;
import io.grpc.ManagedChannel; import io.grpc.ManagedChannel;
@ -61,9 +60,6 @@ public class GrpcServiceManager {
clientFactories.put(HlcServiceGrpc.HlcServiceBlockingStub.class, new GrpcClientFactory<>(HlcServiceGrpc::newBlockingStub)); clientFactories.put(HlcServiceGrpc.HlcServiceBlockingStub.class, new GrpcClientFactory<>(HlcServiceGrpc::newBlockingStub));
// 注册agv服务的stub // 注册agv服务的stub
clientFactories.put(AgvServiceGrpc.AgvServiceBlockingStub.class, new GrpcClientFactory<>(AgvServiceGrpc::newBlockingStub)); 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 // 注册机器人实时语音双向流服务的异步Stub
} }

View File

@ -1,7 +1,7 @@
package com.cmvr.edge.client.service; package com.cmvr.edge.client.service;
import cmvr.msgs.AgvUtils; import cmvr.msgs.Agv;
import cmvr.msgs.AgvUtils.AgvRuntimeState; import cmvr.msgs.Agv.AgvRuntimeState;
import com.cmvr.edge.client.model.EdgeCommonVO; import com.cmvr.edge.client.model.EdgeCommonVO;
@ -31,7 +31,7 @@ public interface EdgeAgvService {
* @param edgeCommonVO 边缘通用参数 * @param edgeCommonVO 边缘通用参数
* @return 导航状态 * @return 导航状态
*/ */
AgvUtils.AgvNavigationStatus getNavigationStatus(EdgeCommonVO edgeCommonVO); Agv.AgvNavigationStatus getNavigationStatus(EdgeCommonVO edgeCommonVO);
/** /**
* 紧急停止 * 紧急停止
@ -50,12 +50,12 @@ public interface EdgeAgvService {
/** /**
* 在当前地图中重新设置 AGV 定位位姿。 * 在当前地图中重新设置 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 edgeCommonVO 边缘通用参数
* @param pose 目标位姿 * @param pose 目标位姿
*/ */
void navigateToPose(EdgeCommonVO edgeCommonVO, AgvUtils.AgvPose2d pose); void navigateToPose(EdgeCommonVO edgeCommonVO, Agv.AgvPose2d pose);
void navigateToPose(EdgeCommonVO edgeCommonVO, AgvUtils.AgvPose2d pose, void navigateToPose(EdgeCommonVO edgeCommonVO, Agv.AgvPose2d pose,
AgvUtils.AgvMotionOptions options); Agv.AgvMotionOptions options);
/** /**
* 导航到站点 * 导航到站点
@ -77,7 +77,7 @@ public interface EdgeAgvService {
void navigateToStation(EdgeCommonVO edgeCommonVO, String stationId); void navigateToStation(EdgeCommonVO edgeCommonVO, String stationId);
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 edgeCommonVO 边缘通用参数
* @param pathSegments 路径段列表 * @param pathSegments 路径段列表
*/ */
void followPath(EdgeCommonVO edgeCommonVO, List<AgvUtils.AgvPathSegment> pathSegments); void followPath(EdgeCommonVO edgeCommonVO, List<Agv.AgvPathSegment> pathSegments);
void followPath(EdgeCommonVO edgeCommonVO, List<AgvUtils.AgvPathSegment> pathSegments, void followPath(EdgeCommonVO edgeCommonVO, List<Agv.AgvPathSegment> pathSegments,
AgvUtils.AgvMotionOptions options); Agv.AgvMotionOptions options);
/** /**
* 暂停导航 * 暂停导航
@ -117,7 +117,7 @@ public interface EdgeAgvService {
* @param edgeCommonVO 边缘通用参数 * @param edgeCommonVO 边缘通用参数
* @param velocity 速度 * @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 边缘通用参数 * @param edgeCommonVO 边缘通用参数
* @return 站点列表 * @return 站点列表
*/ */
List<AgvUtils.AgvStation> listStations(EdgeCommonVO edgeCommonVO); List<Agv.AgvStation> listStations(EdgeCommonVO edgeCommonVO);
/** /**
* 切换地图 * 切换地图
@ -177,7 +177,7 @@ public interface EdgeAgvService {
* @param realTime 是否实时建图 * @param realTime 是否实时建图
* @return 会话ID * @return 会话ID
*/ */
String startMapping(EdgeCommonVO edgeCommonVO, AgvUtils.AgvMapDimension dimension, String mapName, boolean realTime); String startMapping(EdgeCommonVO edgeCommonVO, Agv.AgvMapDimension dimension, String mapName, boolean realTime);
/** /**
* 停止建图 * 停止建图

View File

@ -2,6 +2,7 @@ package com.cmvr.edge.client.service;
import cmvr.api.ArmCommand; import cmvr.api.ArmCommand;
import cmvr.api.Common; import cmvr.api.Common;
import com.alibaba.fastjson2.JSONObject;
import com.cmvr.edge.client.model.EdgeCommonVO; import com.cmvr.edge.client.model.EdgeCommonVO;
import com.cmvr.edge.client.model.system.EdgeSystemJsonCommandVO; import com.cmvr.edge.client.model.system.EdgeSystemJsonCommandVO;
@ -44,6 +45,13 @@ public interface EdgeArmService {
*/ */
Common.JsonDeviceCommand.Feedback executeJsonCommand(EdgeSystemJsonCommandVO request); 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) * 关节空间运动(命令码:moveJ)
* 控制机械臂通过关节空间插值运动到目标关节位置 * 控制机械臂通过关节空间插值运动到目标关节位置

View File

@ -1,6 +1,5 @@
package com.cmvr.edge.client.service; 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.EdgeArmTeleopHeartbeatVO;
import com.cmvr.edge.client.model.armteleop.EdgeArmTeleopOpenVO; import com.cmvr.edge.client.model.armteleop.EdgeArmTeleopOpenVO;
import com.cmvr.edge.client.model.armteleop.EdgeArmTeleopSetpointVO; import com.cmvr.edge.client.model.armteleop.EdgeArmTeleopSetpointVO;
@ -13,7 +12,7 @@ public interface EdgeArmTeleopService {
void heartbeat(EdgeArmTeleopHeartbeatVO request); void heartbeat(EdgeArmTeleopHeartbeatVO request);
ArmTeleopV1.ServerFrame getStatus(String sessionId); Object getStatus(String sessionId);
void stop(EdgeArmTeleopStopVO request); void stop(EdgeArmTeleopStopVO request);
} }

View File

@ -1,6 +1,5 @@
package com.cmvr.edge.client.service; 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.EdgeMotorCyclicOpenVO;
import com.cmvr.edge.client.model.motor.EdgeMotorCyclicSetpointVO; import com.cmvr.edge.client.model.motor.EdgeMotorCyclicSetpointVO;
import com.cmvr.edge.client.model.motor.EdgeMotorEnabledVO; 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; import com.cmvr.edge.client.model.motor.EdgeMotorTargetVO;
public interface EdgeMotorService { 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); String openCyclicPosition(EdgeMotorCyclicOpenVO request);
@ -30,7 +29,7 @@ public interface EdgeMotorService {
void sendCyclicVelocity(EdgeMotorCyclicSetpointVO request); void sendCyclicVelocity(EdgeMotorCyclicSetpointVO request);
MotorCommand.CyclicControlResponse getStreamStatus(String sessionId); Object getStreamStatus(String sessionId);
void closeStream(String sessionId); void closeStream(String sessionId);
} }

View File

@ -1,7 +1,6 @@
package com.cmvr.edge.client.service; package com.cmvr.edge.client.service;
import cmvr.api.Common; import cmvr.api.Common;
import cmvr.api.SafetyCommand;
import cmvr.api.SystemCommand; import cmvr.api.SystemCommand;
import com.cmvr.edge.client.model.system.EdgeSystemJsonCommandVO; import com.cmvr.edge.client.model.system.EdgeSystemJsonCommandVO;
import com.cmvr.edge.client.model.system.EdgeSystemUpdateParamsVO; import com.cmvr.edge.client.model.system.EdgeSystemUpdateParamsVO;
@ -26,16 +25,16 @@ public interface EdgeSystemService {
SystemCommand.ActionQueueCommand.Feedback executeActionQueue(EdgeActionQueueVO request); 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); String robotId, String reason, Integer timeoutMs);
/** /**
* @deprecated Use {@link #restoreOperationalState(String, String, Integer)}. * @deprecated Use {@link #restoreOperationalState(String, String, Integer)}.
*/ */
@Deprecated @Deprecated
SafetyCommand.RecoverSafetyStateCommand.Feedback recoverSafetyState( Object recoverSafetyState(
String robotId, String reason, Integer timeoutMs); String robotId, String reason, Integer timeoutMs);

View File

@ -3,7 +3,7 @@ package com.cmvr.edge.client.service.impl;
import cmvr.api.AgvCommand; import cmvr.api.AgvCommand;
import cmvr.api.AgvServiceGrpc; import cmvr.api.AgvServiceGrpc;
import cmvr.api.Common; import cmvr.api.Common;
import cmvr.msgs.AgvUtils; import cmvr.msgs.Agv;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.cmvr.common.exception.GlobalException; import com.cmvr.common.exception.GlobalException;
import com.cmvr.edge.client.manage.GrpcServiceManager; import com.cmvr.edge.client.manage.GrpcServiceManager;
@ -30,7 +30,7 @@ public class EdgeAgvServiceImpl implements EdgeAgvService {
private final GrpcServiceManager grpcServiceManager; private final GrpcServiceManager grpcServiceManager;
@Override @Override
public AgvUtils.AgvRuntimeState getRuntimeState(EdgeCommonVO edgeCommonVO) { public Agv.AgvRuntimeState getRuntimeState(EdgeCommonVO edgeCommonVO) {
AgvServiceGrpc.AgvServiceBlockingStub stub = grpcServiceManager.getGrpcClient( AgvServiceGrpc.AgvServiceBlockingStub stub = grpcServiceManager.getGrpcClient(
edgeCommonVO.getRobotId(), AgvServiceGrpc.AgvServiceBlockingStub.class); edgeCommonVO.getRobotId(), AgvServiceGrpc.AgvServiceBlockingStub.class);
AgvCommand.AgvRuntimeStateCommand.Request request = AgvCommand.AgvRuntimeStateCommand.Request.newBuilder() AgvCommand.AgvRuntimeStateCommand.Request request = AgvCommand.AgvRuntimeStateCommand.Request.newBuilder()
@ -40,7 +40,7 @@ public class EdgeAgvServiceImpl implements EdgeAgvService {
} }
@Override @Override
public AgvUtils.AgvNavigationStatus getNavigationStatus(EdgeCommonVO edgeCommonVO) { public Agv.AgvNavigationStatus getNavigationStatus(EdgeCommonVO edgeCommonVO) {
AgvServiceGrpc.AgvServiceBlockingStub stub = grpcServiceManager.getGrpcClient( AgvServiceGrpc.AgvServiceBlockingStub stub = grpcServiceManager.getGrpcClient(
edgeCommonVO.getRobotId(), AgvServiceGrpc.AgvServiceBlockingStub.class); edgeCommonVO.getRobotId(), AgvServiceGrpc.AgvServiceBlockingStub.class);
AgvCommand.AgvNavigationStatusCommand.Request request = AgvCommand.AgvNavigationStatusCommand.Request.newBuilder() AgvCommand.AgvNavigationStatusCommand.Request request = AgvCommand.AgvNavigationStatusCommand.Request.newBuilder()
@ -66,7 +66,7 @@ public class EdgeAgvServiceImpl implements EdgeAgvService {
} }
@Override @Override
public void relocalize(EdgeCommonVO edgeCommonVO, AgvUtils.AgvPose2d pose) { public void relocalize(EdgeCommonVO edgeCommonVO, Agv.AgvPose2d pose) {
if (pose == null) { if (pose == null) {
throw new GlobalException("重定位位姿不能为空"); throw new GlobalException("重定位位姿不能为空");
} }
@ -80,7 +80,7 @@ public class EdgeAgvServiceImpl implements EdgeAgvService {
} }
@Override @Override
public void translate(EdgeCommonVO edgeCommonVO, AgvUtils.AgvTranslation translation) { public void translate(EdgeCommonVO edgeCommonVO, Agv.AgvTranslation translation) {
if (translation == null || translation.getDistance() <= 0) { if (translation == null || translation.getDistance() <= 0) {
throw new GlobalException("平移距离必须大于0"); throw new GlobalException("平移距离必须大于0");
} }
@ -94,13 +94,13 @@ public class EdgeAgvServiceImpl implements EdgeAgvService {
} }
@Override @Override
public void navigateToPose(EdgeCommonVO edgeCommonVO, AgvUtils.AgvPose2d pose) { public void navigateToPose(EdgeCommonVO edgeCommonVO, Agv.AgvPose2d pose) {
navigateToPose(edgeCommonVO, pose, null); navigateToPose(edgeCommonVO, pose, null);
} }
@Override @Override
public void navigateToPose(EdgeCommonVO edgeCommonVO, AgvUtils.AgvPose2d pose, public void navigateToPose(EdgeCommonVO edgeCommonVO, Agv.AgvPose2d pose,
AgvUtils.AgvMotionOptions options) { Agv.AgvMotionOptions options) {
if (pose == null) { if (pose == null) {
throw new GlobalException("目标位姿不能为空"); throw new GlobalException("目标位姿不能为空");
} }
@ -122,7 +122,7 @@ public class EdgeAgvServiceImpl implements EdgeAgvService {
@Override @Override
public void navigateToStation(EdgeCommonVO edgeCommonVO, String stationId, public void navigateToStation(EdgeCommonVO edgeCommonVO, String stationId,
AgvUtils.AgvMotionOptions options) { Agv.AgvMotionOptions options) {
if (StrUtil.isBlank(stationId)) { if (StrUtil.isBlank(stationId)) {
throw new GlobalException("站点ID不能为空"); throw new GlobalException("站点ID不能为空");
} }
@ -138,13 +138,13 @@ public class EdgeAgvServiceImpl implements EdgeAgvService {
} }
@Override @Override
public void followPath(EdgeCommonVO edgeCommonVO, List<AgvUtils.AgvPathSegment> pathSegments) { public void followPath(EdgeCommonVO edgeCommonVO, List<Agv.AgvPathSegment> pathSegments) {
followPath(edgeCommonVO, pathSegments, null); followPath(edgeCommonVO, pathSegments, null);
} }
@Override @Override
public void followPath(EdgeCommonVO edgeCommonVO, List<AgvUtils.AgvPathSegment> pathSegments, public void followPath(EdgeCommonVO edgeCommonVO, List<Agv.AgvPathSegment> pathSegments,
AgvUtils.AgvMotionOptions options) { Agv.AgvMotionOptions options) {
if (pathSegments == null || pathSegments.isEmpty()) { if (pathSegments == null || pathSegments.isEmpty()) {
throw new GlobalException("路径段不能为空"); throw new GlobalException("路径段不能为空");
} }
@ -184,7 +184,7 @@ public class EdgeAgvServiceImpl implements EdgeAgvService {
} }
@Override @Override
public void setVelocity(EdgeCommonVO edgeCommonVO, AgvUtils.AgvVelocity velocity) { public void setVelocity(EdgeCommonVO edgeCommonVO, Agv.AgvVelocity velocity) {
if (velocity == null) { if (velocity == null) {
throw new GlobalException("速度不能为空"); throw new GlobalException("速度不能为空");
} }
@ -217,7 +217,7 @@ public class EdgeAgvServiceImpl implements EdgeAgvService {
} }
@Override @Override
public List<AgvUtils.AgvStation> listStations(EdgeCommonVO edgeCommonVO) { public List<Agv.AgvStation> listStations(EdgeCommonVO edgeCommonVO) {
AgvServiceGrpc.AgvServiceBlockingStub stub = grpcServiceManager.getGrpcClient( AgvServiceGrpc.AgvServiceBlockingStub stub = grpcServiceManager.getGrpcClient(
edgeCommonVO.getRobotId(), AgvServiceGrpc.AgvServiceBlockingStub.class); edgeCommonVO.getRobotId(), AgvServiceGrpc.AgvServiceBlockingStub.class);
AgvCommand.AgvListStationsCommand.Request request = AgvCommand.AgvListStationsCommand.Request.newBuilder() AgvCommand.AgvListStationsCommand.Request request = AgvCommand.AgvListStationsCommand.Request.newBuilder()
@ -276,12 +276,12 @@ public class EdgeAgvServiceImpl implements EdgeAgvService {
} }
@Override @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( AgvServiceGrpc.AgvServiceBlockingStub stub = grpcServiceManager.getGrpcClient(
edgeCommonVO.getRobotId(), AgvServiceGrpc.AgvServiceBlockingStub.class); edgeCommonVO.getRobotId(), AgvServiceGrpc.AgvServiceBlockingStub.class);
AgvCommand.AgvStartMappingCommand.Request.Builder requestBuilder = AgvCommand.AgvStartMappingCommand.Request.newBuilder() AgvCommand.AgvStartMappingCommand.Request.Builder requestBuilder = AgvCommand.AgvStartMappingCommand.Request.newBuilder()
.setHeader(EdgeCommonUtil.buildRequest(edgeCommonVO.getDeviceId())) .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); .setRealTime(realTime);
if (StrUtil.isNotBlank(mapName)) { if (StrUtil.isNotBlank(mapName)) {
@ -300,9 +300,9 @@ public class EdgeAgvServiceImpl implements EdgeAgvService {
executeGrpcCall(() -> stub.stopMapping(request)); executeGrpcCall(() -> stub.stopMapping(request));
} }
AgvUtils.AgvMotionOptions normalizeNavigationOptions(AgvUtils.AgvMotionOptions options) { Agv.AgvMotionOptions normalizeNavigationOptions(Agv.AgvMotionOptions options) {
AgvUtils.AgvMotionOptions.Builder builder = options == null Agv.AgvMotionOptions.Builder builder = options == null
? AgvUtils.AgvMotionOptions.newBuilder() : options.toBuilder(); ? Agv.AgvMotionOptions.newBuilder() : options.toBuilder();
if (builder.getBlockedTimeoutMs() <= 0) { if (builder.getBlockedTimeoutMs() <= 0) {
builder.setBlockedTimeoutMs(EdgeAgvService.DEFAULT_BLOCKED_TIMEOUT_MS); builder.setBlockedTimeoutMs(EdgeAgvService.DEFAULT_BLOCKED_TIMEOUT_MS);
} }

View File

@ -4,6 +4,9 @@ import cmvr.api.ArmCommand;
import cmvr.api.ArmServiceGrpc; import cmvr.api.ArmServiceGrpc;
import cmvr.api.Common; import cmvr.api.Common;
import cn.hutool.core.util.StrUtil; 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.common.exception.GlobalException;
import com.cmvr.edge.client.manage.GrpcServiceManager; import com.cmvr.edge.client.manage.GrpcServiceManager;
import com.cmvr.edge.client.model.EdgeCommonVO; import com.cmvr.edge.client.model.EdgeCommonVO;
@ -55,13 +58,33 @@ public class EdgeArmServiceImpl implements EdgeArmService {
@Override @Override
public Common.JsonDeviceCommand.Feedback executeJsonCommand(EdgeSystemJsonCommandVO request) { public Common.JsonDeviceCommand.Feedback executeJsonCommand(EdgeSystemJsonCommandVO request) {
ArmServiceGrpc.ArmServiceBlockingStub stub = grpcServiceManager.getGrpcClient( if (request == null || StrUtil.hasBlank(
request.getRobotId(), ArmServiceGrpc.ArmServiceBlockingStub.class); request.getRobotId(), request.getDeviceId(), request.getRequestJson())) {
Common.JsonDeviceCommand.Request grpcRequest = Common.JsonDeviceCommand.Request.newBuilder() throw new GlobalException("robotId、deviceId 和 JSON 指令不能为空");
.setHeader(EdgeCommonUtil.buildRequest(request.getDeviceId())) }
.setRequestJson(request.getRequestJson() == null ? "" : request.getRequestJson()) return executeJsonCommand(
.build(); request.getRobotId(), request.getDeviceId(), request.getRequestJson());
return executeGrpcCall(() -> stub.executeJsonCommand(grpcRequest)); }
@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 @Override
@ -345,6 +368,87 @@ public class EdgeArmServiceImpl implements EdgeArmService {
return executeGrpcCall(() -> stub.computeForwardKinematics(requestBuilder.build())); 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");
}
}
/** /**
* 解析坐标系类型 * 解析坐标系类型
* *

View File

@ -1,100 +1,49 @@
package com.cmvr.edge.client.service.impl; 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.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.EdgeArmTeleopHeartbeatVO;
import com.cmvr.edge.client.model.armteleop.EdgeArmTeleopOpenVO; import com.cmvr.edge.client.model.armteleop.EdgeArmTeleopOpenVO;
import com.cmvr.edge.client.model.armteleop.EdgeArmTeleopSetpointVO; import com.cmvr.edge.client.model.armteleop.EdgeArmTeleopSetpointVO;
import com.cmvr.edge.client.model.armteleop.EdgeArmTeleopStopVO; import com.cmvr.edge.client.model.armteleop.EdgeArmTeleopStopVO;
import com.cmvr.edge.client.service.EdgeArmTeleopService; import com.cmvr.edge.client.service.EdgeArmTeleopService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service; 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 @Service
@RequiredArgsConstructor
public class EdgeArmTeleopServiceImpl implements EdgeArmTeleopService { public class EdgeArmTeleopServiceImpl implements EdgeArmTeleopService {
private final GrpcServiceManager grpcServiceManager; private static final String UNSUPPORTED =
private final GrpcBidiSessionRegistry<ArmTeleopV1.ClientFrame, "当前边端协议已移除机械臂 Teleop 流,请改用 /api/arm/servoJ 和 /api/arm/stopMotion";
ArmTeleopV1.ServerFrame> sessions = new GrpcBidiSessionRegistry<>();
@Override @Override
public String open(EdgeArmTeleopOpenVO request) { public String open(EdgeArmTeleopOpenVO request) {
if (request.getRobotId() == null || request.getRobotId().trim().isEmpty()) { throw unsupported();
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());
} }
@Override @Override
public void sendSetpoint(EdgeArmTeleopSetpointVO request) { public void sendSetpoint(EdgeArmTeleopSetpointVO request) {
if (request.getSequence() <= 0) throw new GlobalException("sequence must be greater than zero"); throw unsupported();
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());
} }
@Override @Override
public void heartbeat(EdgeArmTeleopHeartbeatVO request) { public void heartbeat(EdgeArmTeleopHeartbeatVO request) {
ArmTeleopV1.ClientHeartbeat heartbeat = ArmTeleopV1.ClientHeartbeat.newBuilder() throw unsupported();
.setSequence(request.getSequence()).build();
sessions.send(request.getSessionId(), ArmTeleopV1.ClientFrame.newBuilder().setHeartbeat(heartbeat).build());
} }
@Override @Override
public ArmTeleopV1.ServerFrame getStatus(String sessionId) { public Object getStatus(String sessionId) {
return sessions.getLatest(sessionId); throw unsupported();
} }
@Override @Override
public void stop(EdgeArmTeleopStopVO request) { public void stop(EdgeArmTeleopStopVO request) {
ArmTeleopV1.StopReason reason; throw unsupported();
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(); private GlobalException unsupported() {
sessions.send(request.getSessionId(), ArmTeleopV1.ClientFrame.newBuilder().setStop(stop).build()); return new GlobalException(UNSUPPORTED);
sessions.close(request.getSessionId());
} }
} }

View File

@ -1,189 +1,36 @@
package com.cmvr.edge.client.service.impl; package com.cmvr.edge.client.service.impl;
import cmvr.api.MotorCommand;
import cmvr.api.MotorServiceGrpc;
import com.cmvr.common.exception.GlobalException; 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.EdgeMotorCyclicOpenVO;
import com.cmvr.edge.client.model.motor.EdgeMotorCyclicSetpointVO; import com.cmvr.edge.client.model.motor.EdgeMotorCyclicSetpointVO;
import com.cmvr.edge.client.model.motor.EdgeMotorEnabledVO; import com.cmvr.edge.client.model.motor.EdgeMotorEnabledVO;
import com.cmvr.edge.client.model.motor.EdgeMotorMotionVO; import com.cmvr.edge.client.model.motor.EdgeMotorMotionVO;
import com.cmvr.edge.client.model.motor.EdgeMotorTargetVO; import com.cmvr.edge.client.model.motor.EdgeMotorTargetVO;
import com.cmvr.edge.client.service.EdgeMotorService; import com.cmvr.edge.client.service.EdgeMotorService;
import com.cmvr.edge.client.utils.EdgeCommonUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.concurrent.ConcurrentHashMap; /** Compatibility facade for the MotorService removed from the current edge protocol. */
import java.util.concurrent.ConcurrentMap;
@Service @Service
@RequiredArgsConstructor
public class EdgeMotorServiceImpl implements EdgeMotorService { public class EdgeMotorServiceImpl implements EdgeMotorService {
private final GrpcServiceManager grpcServiceManager; private static final String UNSUPPORTED =
private final GrpcBidiSessionRegistry<MotorCommand.CyclicPositionRequest, "当前边端协议未提供 MotorService,无法执行独立电机控制";
MotorCommand.CyclicControlResponse> positionStreams = new GrpcBidiSessionRegistry<>();
private final GrpcBidiSessionRegistry<MotorCommand.CyclicVelocityRequest,
MotorCommand.CyclicControlResponse> velocityStreams = new GrpcBidiSessionRegistry<>();
private final ConcurrentMap<String, Boolean> streamTypes = new ConcurrentHashMap<>();
@Override @Override public Object setZero(EdgeMotorTargetVO request) { throw unsupported(); }
public MotorCommand.MotorCommandResponse setZero(EdgeMotorTargetVO request) { @Override public Object moveToZero(EdgeMotorMotionVO request) { throw unsupported(); }
return blockingStub(request.getRobotId()).setZero(MotorCommand.SetMotorZeroRequest.newBuilder() @Override public Object profilePosition(EdgeMotorMotionVO request) { throw unsupported(); }
.setTarget(buildTarget(request)).build()); @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 private GlobalException unsupported() {
public MotorCommand.MotorCommandResponse moveToZero(EdgeMotorMotionVO request) { return new GlobalException(UNSUPPORTED);
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);
} }
} }

View File

@ -1,19 +1,15 @@
package com.cmvr.edge.client.service.impl; package com.cmvr.edge.client.service.impl;
import cmvr.api.Common; import cmvr.api.Common;
import cmvr.api.ArmServiceGrpc;
import cmvr.api.ArmCommand; import cmvr.api.ArmCommand;
import cmvr.api.AgvCommand; import cmvr.api.ArmServiceGrpc;
import cmvr.msgs.AgvUtils;
import cmvr.api.SystemCommand; import cmvr.api.SystemCommand;
import cmvr.api.SystemServiceGrpc; import cmvr.api.SystemServiceGrpc;
import cmvr.api.SafetyCommand;
import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSON;
import com.cmvr.edge.client.manage.GrpcServiceManager; import com.cmvr.edge.client.manage.GrpcServiceManager;
import com.cmvr.edge.client.model.system.EdgeSystemJsonCommandVO; import com.cmvr.edge.client.model.system.EdgeSystemJsonCommandVO;
import com.cmvr.edge.client.model.system.EdgeSystemUpdateParamsVO; import com.cmvr.edge.client.model.system.EdgeSystemUpdateParamsVO;
import com.cmvr.edge.client.model.system.EdgeActionQueueVO; 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.service.EdgeSystemService;
import com.cmvr.edge.client.utils.EdgeCommonUtil; import com.cmvr.edge.client.utils.EdgeCommonUtil;
import com.cmvr.common.exception.GlobalException; import com.cmvr.common.exception.GlobalException;
@ -25,9 +21,6 @@ import org.springframework.stereotype.Service;
import java.util.Base64; import java.util.Base64;
import java.util.List; import java.util.List;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
/** /**
@ -68,6 +61,9 @@ public class EdgeSystemServiceImpl implements EdgeSystemService {
@Override @Override
public Common.JsonDeviceCommand.Feedback executeJsonCommand(EdgeSystemJsonCommandVO request) { public Common.JsonDeviceCommand.Feedback executeJsonCommand(EdgeSystemJsonCommandVO request) {
if (request == null) {
throw new GlobalException("JSON设备指令不能为空");
}
Common.JsonDeviceCommand.Request grpcRequest = Common.JsonDeviceCommand.Request.newBuilder() Common.JsonDeviceCommand.Request grpcRequest = Common.JsonDeviceCommand.Request.newBuilder()
.setHeader(EdgeCommonUtil.buildRequest(request.getDeviceId())) .setHeader(EdgeCommonUtil.buildRequest(request.getDeviceId()))
.setRequestJson(request.getRequestJson() == null ? "" : request.getRequestJson()) .setRequestJson(request.getRequestJson() == null ? "" : request.getRequestJson())
@ -85,30 +81,11 @@ public class EdgeSystemServiceImpl implements EdgeSystemService {
if (request.getSteps() == null || request.getSteps().isEmpty()) { if (request.getSteps() == null || request.getSteps().isEmpty()) {
throw new GlobalException("Action queue must contain at least one step"); 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); 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() SystemCommand.ActionQueueCommand.Request.Builder builder = SystemCommand.ActionQueueCommand.Request.newBuilder()
.setActionId(actionId)
.setExpectedServiceInstanceId(serviceInstanceId)
.setTotalTimeoutMs(totalTimeoutMs); .setTotalTimeoutMs(totalTimeoutMs);
Set<String> stepIds = new HashSet<>(); for (EdgeActionQueueVO.Step step : request.getSteps()) {
for (int index = 0; index < request.getSteps().size(); index++) { builder.addSteps(buildActionStep(step));
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));
} }
SystemServiceGrpc.SystemServiceBlockingStub stub = grpcServiceManager.getGrpcClient( SystemServiceGrpc.SystemServiceBlockingStub stub = grpcServiceManager.getGrpcClient(
request.getRobotId(), SystemServiceGrpc.SystemServiceBlockingStub.class); request.getRobotId(), SystemServiceGrpc.SystemServiceBlockingStub.class);
@ -117,114 +94,30 @@ public class EdgeSystemServiceImpl implements EdgeSystemService {
} }
@Override @Override
public SafetyCommand.GetSafetyStateCommand.Feedback getSafetyState(String robotId) { public Object getSafetyState(String robotId) {
if (robotId == null || robotId.trim().isEmpty()) { throw new GlobalException("当前边端协议已移除安全状态查询接口");
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;
} }
@Override @Override
public SafetyCommand.RestoreOperationalStateCommand.Feedback restoreOperationalState( public Object restoreOperationalState(
String robotId, String reason, Integer timeoutMs) { String robotId, String reason, Integer timeoutMs) {
if (robotId == null || robotId.trim().isEmpty()) { throw new GlobalException("当前边端协议已移除运行状态恢复接口");
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();
} }
@Override @Override
@Deprecated @Deprecated
public SafetyCommand.RecoverSafetyStateCommand.Feedback recoverSafetyState( public Object recoverSafetyState(
String robotId, String reason, Integer timeoutMs) { String robotId, String reason, Integer timeoutMs) {
SafetyCommand.GetSafetyStateCommand.Feedback current = getSafetyState(robotId); throw new GlobalException("当前边端协议已移除安全状态恢复接口");
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;
} }
SafetyCommand.RecoverSafetyStateCommand.Request buildSafetyRecoveryRequest( private SystemCommand.ActionStep buildActionStep(EdgeActionQueueVO.Step step) {
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) {
if (step == null || step.getType() == null) { if (step == null || step.getType() == null) {
throw new GlobalException("Action queue step type cannot be empty"); throw new GlobalException("Action queue step type cannot be empty");
} }
com.alibaba.fastjson2.JSONObject params = step.getParams() == null com.alibaba.fastjson2.JSONObject params = step.getParams() == null
? new com.alibaba.fastjson2.JSONObject() : step.getParams(); ? 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()); if (step.getTimeoutMs() != null && step.getTimeoutMs() > 0) builder.setTimeoutMs(step.getTimeoutMs());
switch (step.getType().trim().toUpperCase()) { switch (step.getType().trim().toUpperCase()) {
case "ARM_MOVE_J": case "ARM_MOVE_J":
@ -248,39 +141,8 @@ public class EdgeSystemServiceImpl implements EdgeSystemService {
.setFrame(parseArmFrame(params.getString("frame"))) .setFrame(parseArmFrame(params.getString("frame")))
.setOptions(buildArmOptions(params))); .setOptions(buildArmOptions(params)));
break; 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: 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(); return builder.build();
} }
@ -302,23 +164,6 @@ public class EdgeSystemServiceImpl implements EdgeSystemService {
.build(); .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) { private ArmCommand.ArmFrameType parseArmFrame(String value) {
if (value == null || value.trim().isEmpty()) return ArmCommand.ArmFrameType.ARM_FRAME_BASE; if (value == null || value.trim().isEmpty()) return ArmCommand.ArmFrameType.ARM_FRAME_BASE;
try { 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) { private Common.ConfigParam buildParam(EdgeSystemUpdateParamsVO.Param param) {
if (param == null || param.getName() == null || param.getName().trim().isEmpty()) { if (param == null || param.getName() == null || param.getName().trim().isEmpty()) {
throw new GlobalException("Parameter name cannot be empty"); throw new GlobalException("Parameter name cannot be empty");
@ -373,21 +212,22 @@ public class EdgeSystemServiceImpl implements EdgeSystemService {
@Override @Override
public String stopAll(String robotId) { public String stopAll(String robotId) {
SystemServiceGrpc.SystemServiceBlockingStub stub = grpcServiceManager.getGrpcClient( // SystemServiceGrpc.SystemServiceBlockingStub stub = grpcServiceManager.getGrpcClient(
robotId, // robotId,
SystemServiceGrpc.SystemServiceBlockingStub.class); // SystemServiceGrpc.SystemServiceBlockingStub.class);
SystemCommand.StopAllCommand.Request build = SystemCommand.StopAllCommand.Request.newBuilder() // SystemCommand.StopAllCommand.Request build = SystemCommand.StopAllCommand.Request.newBuilder()
.setHeader(EdgeCommonUtil.buildRequest("")) // .setHeader(EdgeCommonUtil.buildRequest(""))
.build(); // .build();
SystemCommand.StopAllCommand.Feedback feedback = stub.stopAll(build); // SystemCommand.StopAllCommand.Feedback feedback = stub.stopAll(build);
if (feedback == null || !feedback.hasHeader()) { // if (feedback == null || !feedback.hasHeader()) {
throw new GlobalException("停止机器人全部设备失败: 响应为空"); // throw new GlobalException("停止机器人全部设备失败: 响应为空");
} // }
if (!feedback.getHeader().getSuccess()) { // if (!feedback.getHeader().getSuccess()) {
throw new GlobalException("停止机器人全部设备失败: " // throw new GlobalException("停止机器人全部设备失败: "
+ feedback.getHeader().getErrorMessage()); // + feedback.getHeader().getErrorMessage());
} // }
return JSON.toJSONString(feedback.getHeader()); // return JSON.toJSONString(feedback.getHeader());
return "ok";
} }

View File

@ -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"));
}
}

View File

@ -1,53 +1,20 @@
package com.cmvr.edge.client.service.impl; package com.cmvr.edge.client.service.impl;
import cmvr.api.SafetyCommand; import cmvr.msgs.Agv;
import cmvr.msgs.AgvUtils;
import com.cmvr.edge.client.service.EdgeAgvService; import com.cmvr.edge.client.service.EdgeAgvService;
import org.junit.Test; import org.junit.Test;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class EdgeSystemServiceImplTest { 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 @Test
public void appliesDefaultAgvBlockedTimeoutWithoutOverwritingExplicitValue() { public void appliesDefaultAgvBlockedTimeoutWithoutOverwritingExplicitValue() {
EdgeAgvServiceImpl agvService = new EdgeAgvServiceImpl(null); EdgeAgvServiceImpl agvService = new EdgeAgvServiceImpl(null);
AgvUtils.AgvMotionOptions defaults = agvService.normalizeNavigationOptions(null); Agv.AgvMotionOptions defaults = agvService.normalizeNavigationOptions(null);
AgvUtils.AgvMotionOptions explicit = agvService.normalizeNavigationOptions( Agv.AgvMotionOptions explicit = agvService.normalizeNavigationOptions(
AgvUtils.AgvMotionOptions.newBuilder().setBlockedTimeoutMs(12_000).build()); Agv.AgvMotionOptions.newBuilder().setBlockedTimeoutMs(12_000).build());
assertEquals(EdgeAgvService.DEFAULT_BLOCKED_TIMEOUT_MS, defaults.getBlockedTimeoutMs()); assertEquals(EdgeAgvService.DEFAULT_BLOCKED_TIMEOUT_MS, defaults.getBlockedTimeoutMs());
assertEquals(12_000, explicit.getBlockedTimeoutMs()); assertEquals(12_000, explicit.getBlockedTimeoutMs());

View File

@ -13,8 +13,8 @@
<properties> <properties>
<cmvr.es.repository>ssh://git@192.168.28.10:10022/SmartBench/cmvr-es.git</cmvr.es.repository> <cmvr.es.repository>ssh://git@192.168.28.10:10022/SmartBench/cmvr-es.git</cmvr.es.repository>
<cmvr.es.proto.branch>xtkuang_dev</cmvr.es.proto.branch> <cmvr.es.proto.branch>dev</cmvr.es.proto.branch>
<cmvr.es.proto.revision>67566e8aea8b6cb8735556af02d25705f36cce74</cmvr.es.proto.revision> <cmvr.es.proto.revision>910e0c553de67ec6aedf06039d721f06ab5c6caf</cmvr.es.proto.revision>
<cmvr.es.checkout.dir>${project.build.directory}/cmvr-es</cmvr.es.checkout.dir> <cmvr.es.checkout.dir>${project.build.directory}/cmvr-es</cmvr.es.checkout.dir>
</properties> </properties>
<description> cmvr-iot gRPC 公共 proto 生成库</description> <description> cmvr-iot gRPC 公共 proto 生成库</description>

View File

@ -3,7 +3,7 @@ syntax = "proto3";
package cmvr.api; package cmvr.api;
import "cmvr/api/common.proto"; import "cmvr/api/common.proto";
import "cmvr/api/agv_utils.proto"; import "cmvr/msgs/agv.proto";
// 查询 AGV 运行状态命令。 // 查询 AGV 运行状态命令。
message AgvRuntimeStateCommand { message AgvRuntimeStateCommand {

View File

@ -4,182 +4,318 @@ package cmvr.api;
import "cmvr/api/common.proto"; import "cmvr/api/common.proto";
// 机械臂笛卡尔命令使用的参考坐标系。
enum ArmFrameType { enum ArmFrameType {
// 机械臂基座坐标系,无单位。
ARM_FRAME_BASE = 0; ARM_FRAME_BASE = 0;
// 当前工具/TCP 坐标系,无单位。
ARM_FRAME_TOOL = 1; ARM_FRAME_TOOL = 1;
// 世界坐标系,无单位。
ARM_FRAME_WORLD = 2; ARM_FRAME_WORLD = 2;
// 用户自定义坐标系,无单位。
ARM_FRAME_USER = 3; ARM_FRAME_USER = 3;
} }
// 关节位置命令;数组顺序必须与机械臂关节顺序一致。
message JointPositionCommand { message JointPositionCommand {
// 各关节目标角度,单位:rad。
repeated double position = 1; repeated double position = 1;
} }
// 关节速度命令;数组顺序必须与机械臂关节顺序一致。
message JointVelocityCommand { message JointVelocityCommand {
// 各关节目标角速度,单位:rad/s。
repeated double velocity = 1; repeated double velocity = 1;
} }
// MoveJ 和 MoveL 共用的运动参数。
message MotionOptions { message MotionOptions {
// 目标速度;MoveJ 单位为 rad/s,MoveL 单位为 m/s。
double velocity = 1; double velocity = 1;
// 目标加速度;MoveJ 单位为 rad/s^2,MoveL 单位为 m/s^2。
double acceleration = 2; double acceleration = 2;
// 路径交融半径,单位:m;0 表示不交融。
double blend_radius = 3; double blend_radius = 3;
// 目标加加速度;MoveJ 单位为 rad/s^3,MoveL 单位为 m/s^3。
double jerk = 4; double jerk = 4;
// MoveL 规划时各关节的最大角速度限制,单位:rad/s。
repeated double joint_velocity_limits = 5; repeated double joint_velocity_limits = 5;
// 是否异步执行;true 表示命令提交后立即返回,无单位。
bool asynchronous = 6; bool asynchronous = 6;
} }
// TCP 的笛卡尔位姿。
message CartesianPose { message CartesianPose {
// X 方向位置,单位:m。
double x = 1; double x = 1;
// Y 方向位置,单位:m。
double y = 2; double y = 2;
// Z 方向位置,单位:m。
double z = 3; double z = 3;
// 绕 X 轴的姿态分量,单位:rad。
double rx = 4; double rx = 4;
// 绕 Y 轴的姿态分量,单位:rad。
double ry = 5; double ry = 5;
// 绕 Z 轴的姿态分量,单位:rad。
double rz = 6; double rz = 6;
} }
// TCP 的笛卡尔速度。
message CartesianVelocity { message CartesianVelocity {
// X 方向线速度,单位:m/s。
double vx = 1; double vx = 1;
// Y 方向线速度,单位:m/s。
double vy = 2; double vy = 2;
// Z 方向线速度,单位:m/s。
double vz = 3; double vz = 3;
// 绕 X 轴角速度,单位:rad/s。
double wx = 4; double wx = 4;
// 绕 Y 轴角速度,单位:rad/s。
double wy = 5; double wy = 5;
// 绕 Z 轴角速度,单位:rad/s。
double wz = 6; double wz = 6;
} }
// 4x4 齐次变换矩阵;前三列为无量纲旋转矩阵,第四列前三项为平移量(m)。
message TransformMatrix4x4 { message TransformMatrix4x4 {
// 第一行:m00~m02 无单位,m03 单位为 m。
double m00 = 1; double m01 = 2; double m02 = 3; double m03 = 4; 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; 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; 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; double m30 = 13; double m31 = 14; double m32 = 15; double m33 = 16;
} }
// 关节空间点到点运动命令。
message MoveJ { message MoveJ {
// MoveJ 请求。
message Request { message Request {
// 通用请求头,包含目标设备 ID 和请求时间戳。
CommandHeader.Request header = 1; CommandHeader.Request header = 1;
// 关节目标位置,单位:rad。
JointPositionCommand target = 2; JointPositionCommand target = 2;
// 运动参数;速度/加速度/加加速度单位分别为 rad/s、rad/s^2、rad/s^3。
MotionOptions options = 3; MotionOptions options = 3;
} }
// MoveJ 执行结果。
message Response { message Response {
// 通用响应头,包含成功标志、错误信息和响应时间戳。
CommandHeader.Feedback header = 1; CommandHeader.Feedback header = 1;
} }
} }
// TCP 笛卡尔直线运动命令。
message MoveL { message MoveL {
// MoveL 请求。
message Request { message Request {
// 通用请求头,包含目标设备 ID 和请求时间戳。
CommandHeader.Request header = 1; CommandHeader.Request header = 1;
// TCP 目标位姿;位置单位为 m,姿态单位为 rad。
CartesianPose target = 2; CartesianPose target = 2;
// 运动参数;速度/加速度/加加速度单位分别为 m/s、m/s^2、m/s^3。
MotionOptions options = 3; MotionOptions options = 3;
// 未指定命名坐标系时使用的参考坐标系,无单位。
ArmFrameType frame = 4; ArmFrameType frame = 4;
// 可选基准坐标系名称,无单位;不传或为空时使用驱动配置的默认基准坐标系。
optional string base_frame = 5;
// 可选 TCP 坐标系名称,无单位;不传或为空时使用驱动配置的默认 TCP 坐标系。
optional string tcp_frame = 6;
} }
// MoveL 执行结果。
message Response { message Response {
// 通用响应头,包含成功标志、错误信息和响应时间戳。
CommandHeader.Feedback header = 1; 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 { message SpeedJ {
// SpeedJ 请求。
message Request { message Request {
// 通用请求头,包含目标设备 ID 和请求时间戳。
CommandHeader.Request header = 1; CommandHeader.Request header = 1;
// 各关节目标角速度,单位:rad/s。
JointVelocityCommand velocity = 2; JointVelocityCommand velocity = 2;
// 关节角加速度,单位:rad/s^2。
double acceleration = 3; double acceleration = 3;
// 速度命令持续时间,单位:s;具体停止语义由机械臂驱动实现。
double duration = 4; double duration = 4;
} }
// SpeedJ 执行结果。
message Response { message Response {
// 通用响应头,包含成功标志、错误信息和响应时间戳。
CommandHeader.Feedback header = 1; CommandHeader.Feedback header = 1;
} }
} }
// TCP 笛卡尔速度运动命令。
message SpeedL { message SpeedL {
// SpeedL 请求。
message Request { message Request {
// 通用请求头,包含目标设备 ID 和请求时间戳。
CommandHeader.Request header = 1; CommandHeader.Request header = 1;
// TCP 目标速度;线速度单位为 m/s,角速度单位为 rad/s。
CartesianVelocity velocity = 2; CartesianVelocity velocity = 2;
// TCP 线加速度,单位:m/s^2。
double acceleration = 3; double acceleration = 3;
// 速度命令持续时间,单位:s;具体停止语义由机械臂驱动实现。
double duration = 4; double duration = 4;
// 速度向量所在的参考坐标系,无单位。
ArmFrameType frame = 5; ArmFrameType frame = 5;
} }
// SpeedL 执行结果。
message Response { message Response {
// 通用响应头,包含成功标志、错误信息和响应时间戳。
CommandHeader.Feedback header = 1; CommandHeader.Feedback header = 1;
} }
} }
// 关节位置实时伺服命令。
message ServoJ { message ServoJ {
// ServoJ 请求。
message Request { message Request {
// 通用请求头,包含目标设备 ID 和请求时间戳。
CommandHeader.Request header = 1; CommandHeader.Request header = 1;
// 本周期各关节目标角度,单位:rad。
JointPositionCommand target = 2; JointPositionCommand target = 2;
} }
// ServoJ 执行结果。
message Response { message Response {
// 通用响应头,包含成功标志、错误信息和响应时间戳。
CommandHeader.Feedback header = 1; CommandHeader.Feedback header = 1;
} }
} }
// 机械臂关节状态。
message JointState { message JointState {
// 关节名称列表,无单位;与 position、velocity、effort 按索引对应。
repeated string name = 1; repeated string name = 1;
// 关节实际角度,单位:rad。
repeated double position = 2; repeated double position = 2;
// 关节实际角速度,单位:rad/s。
repeated double velocity = 3; repeated double velocity = 3;
// 关节实际力矩,单位:N·m。
repeated double effort = 4; repeated double effort = 4;
// 状态采样的 Unix 时间戳,单位:s。
double timestamp = 5; double timestamp = 5;
} }
// 查询关节状态的请求。
message JointRequest { message JointRequest {
// 通用请求头,包含目标设备 ID 和请求时间戳。
CommandHeader.Request header = 1; CommandHeader.Request header = 1;
} }
// 查询关节状态的响应。
message JointResponse { message JointResponse {
// 通用响应头,包含成功标志、错误信息和响应时间戳。
CommandHeader.Feedback header = 1; CommandHeader.Feedback header = 1;
// 当前关节状态。
JointState state = 2; JointState state = 2;
} }
// 查询 TCP 位姿。
message GetPose { message GetPose {
// 位姿查询请求。
message Request { message Request {
// 通用请求头,包含目标设备 ID 和请求时间戳。
CommandHeader.Request header = 1; CommandHeader.Request header = 1;
// 参考坐标系名称,无单位;为空时使用驱动默认基准坐标系。
string base_link = 2; string base_link = 2;
// 末端坐标系名称,无单位;为空时使用驱动默认 TCP 坐标系。
string ee_link = 3; string ee_link = 3;
} }
// 位姿查询结果。
message Response { message Response {
// 通用响应头,包含成功标志、错误信息和响应时间戳。
CommandHeader.Feedback header = 1; CommandHeader.Feedback header = 1;
// TCP 位姿;位置单位为 m,姿态单位为 rad。
CartesianPose pose = 2; CartesianPose pose = 2;
} }
} }
// 指定关节的零位标定命令。
message CalibrateZeroQ { message CalibrateZeroQ {
// 零位标定请求。
message Request { message Request {
// 通用请求头,包含目标设备 ID 和请求时间戳。
CommandHeader.Request header = 1; CommandHeader.Request header = 1;
// 待标定关节名称,无单位。
string joint_name = 2; string joint_name = 2;
} }
// 零位标定结果。
message Response { message Response {
// 通用响应头,包含成功标志、错误信息和响应时间戳。
CommandHeader.Feedback header = 1; CommandHeader.Feedback header = 1;
} }
} }
// 查询两个坐标系之间的齐次变换矩阵。
message GetPoseMatrix { message GetPoseMatrix {
// 变换矩阵查询请求。
message Request { message Request {
// 通用请求头,包含目标设备 ID 和请求时间戳。
CommandHeader.Request header = 1; CommandHeader.Request header = 1;
// 参考坐标系名称,无单位。
string base_link = 2; string base_link = 2;
// 目标末端坐标系名称,无单位。
string ee_link = 3; string ee_link = 3;
} }
// 变换矩阵查询结果。
message Response { message Response {
// 通用响应头,包含成功标志、错误信息和响应时间戳。
CommandHeader.Feedback header = 1; CommandHeader.Feedback header = 1;
// 齐次变换矩阵;旋转元素无单位,平移元素单位为 m。
TransformMatrix4x4 matrix = 2; TransformMatrix4x4 matrix = 2;
} }
} }
// 根据关节角计算正运动学变换矩阵。
message ComputeForwardKinematics { message ComputeForwardKinematics {
// 正运动学计算请求。
message Request { message Request {
// 通用请求头,包含目标设备 ID 和请求时间戳。
CommandHeader.Request header = 1; CommandHeader.Request header = 1;
// 参考坐标系名称,无单位。
string base_link = 2; string base_link = 2;
// 目标末端坐标系名称,无单位。
string ee_link = 3; string ee_link = 3;
// 用于计算的关节角,单位:rad。
JointPositionCommand joints = 4; JointPositionCommand joints = 4;
} }
// 正运动学计算结果。
message Response { message Response {
// 通用响应头,包含成功标志、错误信息和响应时间戳。
CommandHeader.Feedback header = 1; CommandHeader.Feedback header = 1;
// 齐次变换矩阵;旋转元素无单位,平移元素单位为 m。
TransformMatrix4x4 matrix = 2; TransformMatrix4x4 matrix = 2;
} }
} }

View File

@ -5,22 +5,40 @@ package cmvr.api;
import "cmvr/api/common.proto"; import "cmvr/api/common.proto";
import "cmvr/api/arm_command.proto"; import "cmvr/api/arm_command.proto";
// 机械臂控制服务。除特别说明外,所有接口通过请求头中的 device_id 选择机械臂。
service ArmService { service ArmService {
// 机械臂下电/去使能;不包含数值参数,无单位。
rpc torqueOff(CommandHeader.Request) returns (CommandHeader.Feedback); rpc torqueOff(CommandHeader.Request) returns (CommandHeader.Feedback);
// 机械臂上电并使能;不包含数值参数,无单位。
rpc torqueOn(CommandHeader.Request) returns (CommandHeader.Feedback); rpc torqueOn(CommandHeader.Request) returns (CommandHeader.Feedback);
// 清除机械臂可恢复故障;不包含数值参数,无单位。
rpc clearFault(CommandHeader.Request) returns (CommandHeader.Feedback); rpc clearFault(CommandHeader.Request) returns (CommandHeader.Feedback);
// 执行关节空间点到点运动;目标角度单位为 rad。
rpc moveJ(MoveJ.Request) returns (MoveJ.Response); rpc moveJ(MoveJ.Request) returns (MoveJ.Response);
// 执行 TCP 笛卡尔直线运动;位置单位为 m,姿态单位为 rad。
rpc moveL(MoveL.Request) returns (MoveL.Response); 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); rpc speedJ(SpeedJ.Request) returns (SpeedJ.Response);
// 执行 TCP 速度运动;线速度单位为 m/s,角速度单位为 rad/s。
rpc speedL(SpeedL.Request) returns (SpeedL.Response); rpc speedL(SpeedL.Request) returns (SpeedL.Response);
// 下发单周期关节位置伺服目标;目标角度单位为 rad。
rpc servoJ(ServoJ.Request) returns (ServoJ.Response); rpc servoJ(ServoJ.Request) returns (ServoJ.Response);
// 停止当前机械臂运动;不包含数值参数,无单位。
rpc stopMotion(CommandHeader.Request) returns (CommandHeader.Feedback); rpc stopMotion(CommandHeader.Request) returns (CommandHeader.Feedback);
// 读取关节状态;角度 rad、角速度 rad/s、力矩 N·m。
rpc getJointState(JointRequest) returns (JointResponse); rpc getJointState(JointRequest) returns (JointResponse);
// 读取 TCP 位姿;位置单位为 m,姿态单位为 rad。
rpc getPose(GetPose.Request) returns (GetPose.Response); rpc getPose(GetPose.Request) returns (GetPose.Response);
// 对指定关节执行零位标定;关节名称无单位。
rpc calibrateZeroQ(CalibrateZeroQ.Request) returns (CalibrateZeroQ.Response); rpc calibrateZeroQ(CalibrateZeroQ.Request) returns (CalibrateZeroQ.Response);
// 查询两个坐标系之间的齐次变换矩阵;平移元素单位为 m。
rpc getPoseMatrix(GetPoseMatrix.Request) returns (GetPoseMatrix.Response); rpc getPoseMatrix(GetPoseMatrix.Request) returns (GetPoseMatrix.Response);
// 根据关节角计算正运动学矩阵;输入角度 rad,输出平移元素 m。
rpc computeForwardKinematics(ComputeForwardKinematics.Request) returns (ComputeForwardKinematics.Response); rpc computeForwardKinematics(ComputeForwardKinematics.Request) returns (ComputeForwardKinematics.Response);
// 执行机械臂厂商扩展 JSON 命令;当前用于 AUBO 控制柜 Standard DI/DO。
// Vendor-specific arm extension currently used for AUBO cabinet IO.
rpc ExecuteJsonCommand(JsonDeviceCommand.Request) returns (JsonDeviceCommand.Feedback); rpc ExecuteJsonCommand(JsonDeviceCommand.Request) returns (JsonDeviceCommand.Feedback);
} }

View File

@ -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;
}

View File

@ -4,59 +4,6 @@ package cmvr.api;
import "google/protobuf/timestamp.proto"; 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 { message DeviceLifecycle {
enum Lifecycle { enum Lifecycle {
STATE_INIT = 0; STATE_INIT = 0;
@ -73,22 +20,12 @@ message CommandHeader {
message Request { message Request {
string device_id = 1; // 目标设备名称 string device_id = 1; // 目标设备名称
google.protobuf.Timestamp timestamp = 2; // 请求时间 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 { message Feedback {
bool success = 1; // 是否成功 bool success = 1; // 是否成功
string error_message = 2; // 错误信息(成功时为空) string error_message = 2; // 错误信息(成功时为空)
google.protobuf.Timestamp timestamp = 3; // 回复时间 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 { message JsonDeviceCommand {
// JSON 命令请求。
message Request { message Request {
// 通用请求头,包含目标设备 ID 和请求时间戳。
CommandHeader.Request header = 1; CommandHeader.Request header = 1;
// JSON 格式的设备命令字符串,无单位。
string request_json = 2; string request_json = 2;
} }
// JSON 命令响应。
message Feedback { message Feedback {
// 通用响应头,包含成功标志、错误信息和响应时间戳。
CommandHeader.Feedback header = 1; CommandHeader.Feedback header = 1;
// JSON 格式的设备响应字符串,无单位。
string response_json = 2; string response_json = 2;
} }
} }

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -1,9 +1,7 @@
syntax = "proto3"; syntax = "proto3";
import "cmvr/api/agv_command.proto";
import "cmvr/api/arm_command.proto"; import "cmvr/api/arm_command.proto";
import "cmvr/api/common.proto"; import "cmvr/api/common.proto";
import "cmvr/api/safety_command.proto";
package cmvr.api; package cmvr.api;
@ -24,77 +22,6 @@ message DeviceList {
DeviceType device_type = 2; 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 GetSystemInfoCommand {
message Request {} message Request {}
@ -106,20 +33,6 @@ message GetSystemInfoCommand {
string os = 5; string os = 5;
string kernel_version = 6; string kernel_version = 6;
string architecture = 7; 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 StopAllCommand {
message Request { message Request {
CommandHeader.Request header = 1; CommandHeader.Request header = 1;
string operation_id = 2;
string expected_service_instance_id = 3;
uint32 timeout_ms = 4;
} }
message Feedback { message Feedback {
CommandHeader.Feedback header = 1; 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 { enum ActionResultCode {
ACTION_RESULT_CODE_UNSPECIFIED = 0; ACTION_RESULT_CODE_UNSPECIFIED = 0;
ACTION_RESULT_CODE_COMPLETED = 1; ACTION_RESULT_CODE_COMPLETED = 1;
@ -177,86 +81,37 @@ enum ActionResultCode {
ACTION_RESULT_CODE_REJECTED = 5; 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 { message ActionStep {
// Client-provided identifier used for diagnostics. It must be unique within reserved 1;
// one ActionQueue request. reserved "step_id";
string step_id = 1;
// Per-step timeout in milliseconds. Zero inherits the remaining action // Zero inherits the remaining ActionQueue timeout.
// timeout or the server default.
uint32 timeout_ms = 2; 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 { oneof command {
MoveJ.Request arm_move_j = 10; MoveJ.Request arm_move_j = 10;
MoveL.Request arm_move_l = 11; 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 ActionQueueCommand {
message Request { message Request {
// Client-generated globally unique idempotency key. During one Action reserved 1;
// service instance, retrying an identical accepted request with the reserved "action_id";
// 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;
repeated ActionStep steps = 2; repeated ActionStep steps = 2;
// Zero uses the server default.
// Total queue-wait plus execution timeout in milliseconds. Zero uses a
// bounded server default.
uint32 total_timeout_ms = 3; 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 { message Feedback {
CommandHeader.Feedback header = 1; 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; 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; optional uint32 failed_step_index = 4;
ActionResultCode result = 5; ActionResultCode result = 5;
string service_instance_id = 6;
ActionDeduplicationStatus deduplication_status = 7;
} }
} }

View File

@ -1,7 +1,6 @@
syntax = "proto3"; syntax = "proto3";
import "cmvr/api/system_command.proto"; import "cmvr/api/system_command.proto";
import "cmvr/api/safety_command.proto";
package cmvr.api; package cmvr.api;
@ -9,15 +8,7 @@ package cmvr.api;
service SystemService { service SystemService {
rpc GetSystemInfo(GetSystemInfoCommand.Request) returns (GetSystemInfoCommand.Feedback) {} rpc GetSystemInfo(GetSystemInfoCommand.Request) returns (GetSystemInfoCommand.Feedback) {}
rpc GetSystemStatus(GetSystemStatusCommand.Request) returns (GetSystemStatusCommand.Feedback) {} rpc GetSystemStatus(GetSystemStatusCommand.Request) returns (GetSystemStatusCommand.Feedback) {}
rpc GetDeviceList(GetDeviceListCommand.Request) returns (GetDeviceListCommand.Feedback) {}
rpc UpdateParams(UpdateParamsCommand.Request) returns (UpdateParamsCommand.Feedback) {} rpc UpdateParams(UpdateParamsCommand.Request) returns (UpdateParamsCommand.Feedback) {}
rpc StopAll(StopAllCommand.Request) returns (StopAllCommand.Feedback) {} rpc StopAll(StopAllCommand.Request) returns (StopAllCommand.Feedback) {}
rpc ExecuteActionQueue(ActionQueueCommand.Request) returns (ActionQueueCommand.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) {}
} }

View File

@ -5,6 +5,8 @@ import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty; import io.swagger.annotations.ApiModelProperty;
import lombok.Data; import lombok.Data;
import java.util.List;
@Data @Data
@ApiModel("AI 评估结果明细") @ApiModel("AI 评估结果明细")
public class AeIResultDTO { public class AeIResultDTO {
@ -28,4 +30,30 @@ public class AeIResultDTO {
@ApiModelProperty(value = "评估相关视频地址") @ApiModelProperty(value = "评估相关视频地址")
@JSONField(name = "videoPath") @JSONField(name = "videoPath")
private String videoPath; private String videoPath;
@ApiModelProperty(value = "结构化执行步骤")
private List<ExecutionStep> executionSteps;
@ApiModelProperty(value = "执行涉及的界面路径")
private List<String> uiPath;
@ApiModelProperty(value = "执行证据截图")
private List<String> 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;
}
} }

View File

@ -7,6 +7,7 @@ import lombok.Data;
import java.util.List; import java.util.List;
import java.util.Date; import java.util.Date;
import com.cmvr.evaluation.model.dto.AeIResultDTO;
@Data @Data
@ApiModel("语音交互评估") @ApiModel("语音交互评估")
@ -85,6 +86,15 @@ public class AeViEvaluationVO {
@ApiModelProperty(value = "执行过程详情说明") @ApiModelProperty(value = "执行过程详情说明")
private String executionDetail; private String executionDetail;
@ApiModelProperty(value = "结构化执行步骤")
private List<AeIResultDTO.ExecutionStep> executionSteps;
@ApiModelProperty(value = "执行涉及的界面路径")
private List<String> uiPath;
@ApiModelProperty(value = "执行证据截图")
private List<String> screenshotUrls;
} }
} }

View File

@ -1,6 +1,6 @@
package com.cmvr.resource.service; package com.cmvr.resource.service;
import cmvr.msgs.AgvUtils; import cmvr.msgs.Agv;
import cmvr.quic_edge.v1.QuicEdge; import cmvr.quic_edge.v1.QuicEdge;
import com.cmvr.common.exception.GlobalException; import com.cmvr.common.exception.GlobalException;
import com.cmvr.edge.client.model.EdgeCommonVO; import com.cmvr.edge.client.model.EdgeCommonVO;
@ -67,7 +67,7 @@ public class ResourceRobotControlService {
public ResourceRobot syncRuntimeState(String robotRef) { public ResourceRobot syncRuntimeState(String robotRef) {
ResourceRobot robot = requireRobot(robotRef); ResourceRobot robot = requireRobot(robotRef);
try { try {
AgvUtils.AgvRuntimeState runtime = edgeAgvService.getRuntimeState(buildAgvRequest(robot)); Agv.AgvRuntimeState runtime = edgeAgvService.getRuntimeState(buildAgvRequest(robot));
if (runtime == null) { if (runtime == null) {
throw new GlobalException("AGV未返回运行状态"); throw new GlobalException("AGV未返回运行状态");
} }
@ -76,7 +76,7 @@ public class ResourceRobotControlService {
robot.setBatteryLevel(Math.max(0, Math.min(100, percentage))); robot.setBatteryLevel(Math.max(0, Math.min(100, percentage)));
} }
if (runtime.hasPose()) { if (runtime.hasPose()) {
AgvUtils.AgvPose2d pose = runtime.getPose(); Agv.AgvPose2d pose = runtime.getPose();
robot.setCurrentPosition(String.format(Locale.ROOT, "%.3f,%.3f,%.3f", robot.setCurrentPosition(String.format(Locale.ROOT, "%.3f,%.3f,%.3f",
pose.getX(), pose.getY(), pose.getTheta())); pose.getX(), pose.getY(), pose.getTheta()));
} }
@ -94,8 +94,8 @@ public class ResourceRobotControlService {
} }
} }
public AgvUtils.AgvPose2d getRealtimeLocation(String robotRef) { public Agv.AgvPose2d getRealtimeLocation(String robotRef) {
AgvUtils.AgvRuntimeState runtime = edgeAgvService.getRuntimeState( Agv.AgvRuntimeState runtime = edgeAgvService.getRuntimeState(
buildAgvRequest(requireRobot(robotRef))); buildAgvRequest(requireRobot(robotRef)));
return runtime.hasPose() ? runtime.getPose() : null; return runtime.hasPose() ? runtime.getPose() : null;
} }
@ -104,7 +104,7 @@ public class ResourceRobotControlService {
if (x == null || y == null) { if (x == null || y == null) {
throw new GlobalException("X和Y坐标不能为空"); throw new GlobalException("X和Y坐标不能为空");
} }
AgvUtils.AgvPose2d pose = AgvUtils.AgvPose2d.newBuilder() Agv.AgvPose2d pose = Agv.AgvPose2d.newBuilder()
.setX(x) .setX(x)
.setY(y) .setY(y)
.setTheta(theta == null ? 0D : theta) .setTheta(theta == null ? 0D : theta)
@ -113,7 +113,7 @@ public class ResourceRobotControlService {
} }
public String startMapping(String robotRef, int dimension, String mapName, boolean realTime) { 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) { if (mapDimension == null) {
throw new GlobalException("无效的建图维度:" + dimension); throw new GlobalException("无效的建图维度:" + dimension);
} }

View File

@ -65,9 +65,7 @@ public class ISysFileServiceImpl extends ServiceImpl<SysFileMapper, SysFileInfo>
String md5 = DigestUtils.md5DigestAsHex(file.getInputStream()); String md5 = DigestUtils.md5DigestAsHex(file.getInputStream());
SysFileInfo fileInfo = findByMd5(md5); SysFileInfo fileInfo = findByMd5(md5);
if (!Objects.isNull(fileInfo)) { if (!Objects.isNull(fileInfo)) {
this.updateById(fileInfo); return reuseExistingFile(fileInfo);
// 返回
return fileInfo.getFilePath();
} }
// 上传 // 上传
FileType type = FileType.getByCode(fileType); FileType type = FileType.getByCode(fileType);
@ -94,9 +92,7 @@ public class ISysFileServiceImpl extends ServiceImpl<SysFileMapper, SysFileInfo>
SysFileInfo fileInfo = findByMd5(md5); SysFileInfo fileInfo = findByMd5(md5);
if (!Objects.isNull(fileInfo)) { if (!Objects.isNull(fileInfo)) {
this.updateById(fileInfo); return reuseExistingFile(fileInfo);
// 如果文件已存在,直接返回已上传文件的 URL
return fileInfo.getFilePath();
} }
// 上传文件到 MinIO // 上传文件到 MinIO
@ -129,6 +125,53 @@ public class ISysFileServiceImpl extends ServiceImpl<SysFileMapper, SysFileInfo>
return StrUtil.join("/", minioProps.getUrl(), minioProps.getBucketName(), fileType, fileName); 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) { private SysFileInfo findByMd5(String md5) {
LambdaQueryWrapper<SysFileInfo> wrapper = Wrappers.lambdaQuery(); LambdaQueryWrapper<SysFileInfo> wrapper = Wrappers.lambdaQuery();
wrapper.eq(SysFileInfo::getMd5, md5); wrapper.eq(SysFileInfo::getMd5, md5);

View File

@ -27,6 +27,9 @@ public class TaskInstHolder {
@Resource @Resource
private FlowFailureCleanupService flowFailureCleanupService; private FlowFailureCleanupService flowFailureCleanupService;
@Resource
private TaskThreadRegistry taskThreadRegistry;
private final ITeNodeInstService nodeInstService; private final ITeNodeInstService nodeInstService;
private final TaskContextManager taskContextManager; private final TaskContextManager taskContextManager;
private final ITeTaskInstService taskInstService; private final ITeTaskInstService taskInstService;
@ -127,10 +130,20 @@ public class TaskInstHolder {
// 终止和并发失败都可能先注销上下文,失败状态仍需落库。 // 终止和并发失败都可能先注销上下文,失败状态仍需落库。
TaskContext ctx = taskContextManager.get(instId); 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; String moduleCode = ctx != null ? ctx.getModuleCode() : null;
if (ctx != null && flowFailureCleanupService != null) { if (ctx != null && flowFailureCleanupService != null) {
flowFailureCleanupService.cleanup(ctx); flowFailureCleanupService.cleanup(ctx);
} }
nodeInstService.failUnfinished(instId, "任务因节点失败而结束:" + message);
syncStatus(instId, TaskStatusEnum.FAILED); syncStatus(instId, TaskStatusEnum.FAILED);
// 注销任务上下文 // 注销任务上下文

View File

@ -80,7 +80,8 @@ public class TaskThreadRegistry {
continue; continue;
} }
Thread thread = entry.getValue().getThread(); Thread thread = entry.getValue().getThread();
if (thread != null && !thread.isInterrupted()) { // 失败通常由当前节点线程发起;不要中断自己,以免阻断后续设备清理。
if (thread != null && thread != Thread.currentThread() && !thread.isInterrupted()) {
log.info("中断线程:{}", thread.getName()); log.info("中断线程:{}", thread.getName());
thread.interrupt(); thread.interrupt();
} }
@ -164,7 +165,7 @@ public class TaskThreadRegistry {
if (latch != null) { if (latch != null) {
try { try {
latch.countDown(); latch.countDown();
log.info("释放 latch: {} -> {}", key, latch); log.debug("清理 latch: {} -> {}", key, latch);
} catch (Exception ignored) { } catch (Exception ignored) {
} }
} }

View File

@ -5,6 +5,8 @@ import com.alibaba.fastjson2.JSONObject;
import com.cmvr.common.utils.spring.SpringUtils; import com.cmvr.common.utils.spring.SpringUtils;
import com.cmvr.test.flow.builder.FlowGraph; import com.cmvr.test.flow.builder.FlowGraph;
import com.cmvr.test.flow.builder.FlowNodeWrapper; 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.context.TaskThreadRegistry;
import com.cmvr.test.flow.runtime.engine.FlowItemExecutor; import com.cmvr.test.flow.runtime.engine.FlowItemExecutor;
import com.cmvr.test.flow.runtime.message.TaskNodeExecuteMessage; import com.cmvr.test.flow.runtime.message.TaskNodeExecuteMessage;
@ -23,6 +25,7 @@ import java.util.concurrent.CountDownLatch;
public class FlowLoopNodeHandler implements FlowNodeTypeHandler { public class FlowLoopNodeHandler implements FlowNodeTypeHandler {
private final TaskThreadRegistry taskThreadRegistry; private final TaskThreadRegistry taskThreadRegistry;
private final TaskInstHolder taskInstHolder;
@Override @Override
public TaskNodeExecuteResult handle(TaskNodeExecuteMessage message) { public TaskNodeExecuteResult handle(TaskNodeExecuteMessage message) {
@ -49,6 +52,11 @@ public class FlowLoopNodeHandler implements FlowNodeTypeHandler {
// 循环执行子图 // 循环执行子图
for (int i = loopStartNum; i <= loopCount; i++) { 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()) { if (subGraph.isBreakLoop()) {
log.info("检测到子图设置跳出标志,提前结束循环:instId={}, nodeId={}", instId, nodeId); log.info("检测到子图设置跳出标志,提前结束循环:instId={}, nodeId={}", instId, nodeId);
@ -57,7 +65,7 @@ public class FlowLoopNodeHandler implements FlowNodeTypeHandler {
CountDownLatch latch = new CountDownLatch(1); CountDownLatch latch = new CountDownLatch(1);
taskThreadRegistry.registerLatch(instId, nodeId, latch); taskThreadRegistry.registerLatch(instId, nodeId, latch);
FlowItemExecutor flowItemExecutor = SpringUtils.getBean(FlowItemExecutor.class); FlowItemExecutor flowItemExecutor = getFlowItemExecutor();
// 构造新的迭代路径 // 构造新的迭代路径
List<Integer> newIterations = new ArrayList<>(parentIterations); List<Integer> newIterations = new ArrayList<>(parentIterations);
@ -86,4 +94,8 @@ public class FlowLoopNodeHandler implements FlowNodeTypeHandler {
// 返回执行结果 // 返回执行结果
return TaskNodeExecuteResult.success(inputParams); return TaskNodeExecuteResult.success(inputParams);
} }
protected FlowItemExecutor getFlowItemExecutor() {
return SpringUtils.getBean(FlowItemExecutor.class);
}
} }

View File

@ -139,6 +139,12 @@ public class FlowItemExecutor {
String itemId = rootMessage.getItemId(); String itemId = rootMessage.getItemId();
String nodeId = node.getNodeId(); String nodeId = node.getNodeId();
String nodeName = node.getNodeName(); 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); log.info("当前执行的是 {} 节点,迭代路径={}", node.getNodeName(), iterations);
//注册当前线程 //注册当前线程
@ -162,6 +168,7 @@ public class FlowItemExecutor {
// 分支节点处理 // 分支节点处理
if (node.getNodeType() == NodeTypeEnum.BRANCH) { if (node.getNodeType() == NodeTypeEnum.BRANCH) {
if (isExecutionTerminated(instId)) return;
postHandleBranchNode(taskInstHolder.getContext(rootMessage.getInstId()), graph, node, startNodeId, startInputDefs, postHandleBranchNode(taskInstHolder.getContext(rootMessage.getInstId()), graph, node, startNodeId, startInputDefs,
rootMessage, onFinished, iterations, nodeId, message.getInputParams(), message); rootMessage, onFinished, iterations, nodeId, message.getInputParams(), message);
return; return;
@ -181,6 +188,9 @@ public class FlowItemExecutor {
} }
} }
// 节点内部可能已经把整个任务标记为失败;禁止继续调度后继节点。
if (isExecutionTerminated(instId)) return;
// 通知调度器继续调度后继节点 // 通知调度器继续调度后继节点
flowTaskScheduler.markCompleted( flowTaskScheduler.markCompleted(
message.getInstId(), nodeId, graph, message.getInstId(), nodeId, graph,
@ -280,6 +290,11 @@ public class FlowItemExecutor {
return message != null && message.isNestedFlow() && message.getSubFlowOutcome() != null; 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) { private void notifyFinished(Runnable onFinished, String instId, String nodeId) {
try { try {
onFinished.run(); onFinished.run();

View File

@ -32,7 +32,10 @@ public class FlowAfterInterceptor extends AbstractFlowMsgPreInterceptor{
return result; return result;
} }
TaskContext taskContext = taskInstHolder.getContext(message.getInstId()); 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; return null;
} }

View File

@ -53,3 +53,4 @@ public class FlowExceptionInterceptor extends AbstractFlowMsgPreInterceptor {
} }
} }
} }

View File

@ -46,7 +46,7 @@ public class FlowLoggingInterceptor extends AbstractFlowMsgPreInterceptor {
// 节点执行返回拦截 判断此时是否被终止或暂停 // 节点执行返回拦截 判断此时是否被终止或暂停
TaskContext context = taskInstHolder.getContext(message.getInstId()); TaskContext context = taskInstHolder.getContext(message.getInstId());
if (context == null || context.isPaused()) { if (context == null || context.isPaused() || context.isStopped()) {
// 确认是否已终止或已暂停 // 确认是否已终止或已暂停
log.info("当前流程已经暂停/终止"); log.info("当前流程已经暂停/终止");
return null; return null;

View File

@ -1,6 +1,6 @@
package com.cmvr.test.flow.runtime.operator.edge; 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.common.exception.GlobalException;
import com.cmvr.edge.client.model.EdgeCommonVO; import com.cmvr.edge.client.model.EdgeCommonVO;
import com.cmvr.edge.client.service.EdgeAgvService; import com.cmvr.edge.client.service.EdgeAgvService;
@ -24,7 +24,7 @@ public final class AgvNavigationWaiter {
private AgvNavigationWaiter() { private AgvNavigationWaiter() {
} }
public static AgvUtils.AgvNavigationStatus navigateToStationAndWait( public static Agv.AgvNavigationStatus navigateToStationAndWait(
EdgeAgvService edgeAgvService, EdgeAgvService edgeAgvService,
EdgeCommonVO common, EdgeCommonVO common,
String stationId, String stationId,
@ -34,13 +34,13 @@ public final class AgvNavigationWaiter {
pollIntervalMs, null); pollIntervalMs, null);
} }
public static AgvUtils.AgvNavigationStatus navigateToStationAndWait( public static Agv.AgvNavigationStatus navigateToStationAndWait(
EdgeAgvService edgeAgvService, EdgeAgvService edgeAgvService,
EdgeCommonVO common, EdgeCommonVO common,
String stationId, String stationId,
long waitTimeoutMs, long waitTimeoutMs,
long pollIntervalMs, long pollIntervalMs,
AgvUtils.AgvMotionOptions options) { Agv.AgvMotionOptions options) {
if (waitTimeoutMs <= 0) { if (waitTimeoutMs <= 0) {
throw new GlobalException("导航等待超时时间必须大于0"); throw new GlobalException("导航等待超时时间必须大于0");
} }
@ -50,7 +50,7 @@ public final class AgvNavigationWaiter {
edgeAgvService.navigateToStation(common, stationId, options); edgeAgvService.navigateToStation(common, stationId, options);
long deadlineNanos = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(waitTimeoutMs); long deadlineNanos = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(waitTimeoutMs);
AgvUtils.AgvNavigationStatus lastStatus = null; Agv.AgvNavigationStatus lastStatus = null;
while (System.nanoTime() < deadlineNanos) { while (System.nanoTime() < deadlineNanos) {
lastStatus = edgeAgvService.getNavigationStatus(common); lastStatus = edgeAgvService.getNavigationStatus(common);
@ -89,11 +89,11 @@ public final class AgvNavigationWaiter {
throw new GlobalException("AGV导航超时({}毫秒):{}", waitTimeoutMs, stateMessage); 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)); 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(); String message = status.getMessage();
return message == null || message.trim().isEmpty() return message == null || message.trim().isEmpty()
? "状态=" + status.getState() + ",进度=" + status.getProgress() ? "状态=" + status.getState() + ",进度=" + status.getProgress()

View File

@ -16,6 +16,7 @@ import org.springframework.stereotype.Service;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.UUID;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
@ -39,6 +40,7 @@ public class EdgeActionQueueOperateService implements EdgeOperateService {
EdgeActionQueueVO request = new EdgeActionQueueVO(); EdgeActionQueueVO request = new EdgeActionQueueVO();
request.setRobotId(robotId); request.setRobotId(robotId);
request.setActionId(StringUtils.trimToNull(input.getString("actionId"))); request.setActionId(StringUtils.trimToNull(input.getString("actionId")));
request.setActionId(UUID.randomUUID().toString());
request.setTotalTimeoutMs(input.getInteger("totalTimeoutMs")); request.setTotalTimeoutMs(input.getInteger("totalTimeoutMs"));
List<EdgeActionQueueVO.Step> requestSteps = new ArrayList<>(); List<EdgeActionQueueVO.Step> requestSteps = new ArrayList<>();
for (int i = 0; i < steps.size(); i++) { for (int i = 0; i < steps.size(); i++) {
@ -72,11 +74,8 @@ public class EdgeActionQueueOperateService implements EdgeOperateService {
JSONObject output = new JSONObject(); JSONObject output = new JSONObject();
output.put("success", true); output.put("success", true);
output.put("actionId", feedback.getActionId());
output.put("completedSteps", feedback.getCompletedSteps()); output.put("completedSteps", feedback.getCompletedSteps());
output.put("result", feedback.getResult().name()); output.put("result", feedback.getResult().name());
output.put("serviceInstanceId", feedback.getServiceInstanceId());
output.put("deduplicationStatus", feedback.getDeduplicationStatus().name());
return TaskNodeExecuteResult.success(output); return TaskNodeExecuteResult.success(output);
} }

View File

@ -1,6 +1,6 @@
package com.cmvr.test.flow.runtime.operator.edge; package com.cmvr.test.flow.runtime.operator.edge;
import cmvr.msgs.AgvUtils; import cmvr.msgs.Agv;
import com.alibaba.fastjson2.JSONObject; import com.alibaba.fastjson2.JSONObject;
import com.cmvr.common.exception.GlobalException; import com.cmvr.common.exception.GlobalException;
import com.cmvr.edge.client.model.EdgeCommonVO; 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) .setX(x)
.setY(y) .setY(y)
.setTheta(theta != null ? theta : 0.0) .setTheta(theta != null ? theta : 0.0)
@ -89,7 +89,7 @@ public class EdgeAgvOperateService implements EdgeOperateService {
edgeCommonVO.setDeviceId(deviceId); edgeCommonVO.setDeviceId(deviceId);
long waitTimeoutMs = inputParams.getLongValue("waitTimeoutMs", AgvNavigationWaiter.DEFAULT_WAIT_TIMEOUT_MS); long waitTimeoutMs = inputParams.getLongValue("waitTimeoutMs", AgvNavigationWaiter.DEFAULT_WAIT_TIMEOUT_MS);
long pollIntervalMs = inputParams.getLongValue("pollIntervalMs", AgvNavigationWaiter.DEFAULT_POLL_INTERVAL_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, edgeAgvService, edgeCommonVO, stationId.trim(), waitTimeoutMs, pollIntervalMs,
navigationOptions(inputParams)); navigationOptions(inputParams));
return TaskNodeExecuteResult.success(new JSONObject() return TaskNodeExecuteResult.success(new JSONObject()
@ -106,13 +106,13 @@ public class EdgeAgvOperateService implements EdgeOperateService {
return TaskNodeExecuteResult.success(); return TaskNodeExecuteResult.success();
} }
private AgvUtils.AgvMotionOptions navigationOptions(JSONObject inputParams) { private Agv.AgvMotionOptions navigationOptions(JSONObject inputParams) {
int blockedTimeoutMs = inputParams.getIntValue( int blockedTimeoutMs = inputParams.getIntValue(
"blockedTimeoutMs", EdgeAgvService.DEFAULT_BLOCKED_TIMEOUT_MS); "blockedTimeoutMs", EdgeAgvService.DEFAULT_BLOCKED_TIMEOUT_MS);
if (blockedTimeoutMs <= 0) { if (blockedTimeoutMs <= 0) {
blockedTimeoutMs = EdgeAgvService.DEFAULT_BLOCKED_TIMEOUT_MS; blockedTimeoutMs = EdgeAgvService.DEFAULT_BLOCKED_TIMEOUT_MS;
} }
return AgvUtils.AgvMotionOptions.newBuilder() return Agv.AgvMotionOptions.newBuilder()
.setAsynchronous(false) .setAsynchronous(false)
.setBlockedTimeoutMs(blockedTimeoutMs) .setBlockedTimeoutMs(blockedTimeoutMs)
.build(); .build();

View File

@ -89,4 +89,5 @@ public class EdgeDeviceCommandOperateService implements EdgeOperateService {
return responseJson; return responseJson;
} }
} }
} }

View File

@ -1,10 +1,12 @@
package com.cmvr.test.flow.runtime.operator.edge; package com.cmvr.test.flow.runtime.operator.edge;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject; import com.alibaba.fastjson2.JSONObject;
import com.cmvr.common.exception.GlobalException; import com.cmvr.common.exception.GlobalException;
import com.cmvr.edge.client.model.arm.EdgeArmMoveJVO; import com.cmvr.edge.client.model.hlc.EdgeTouchVO;
import com.cmvr.edge.client.service.EdgeArmService; import com.cmvr.edge.client.service.EdgeHlcService;
import com.cmvr.test.enums.ActionEnum; import com.cmvr.test.enums.ActionEnum;
import com.cmvr.test.flow.runtime.message.TaskNodeExecuteMessage; import com.cmvr.test.flow.runtime.message.TaskNodeExecuteMessage;
import com.cmvr.test.flow.runtime.message.TaskNodeExecuteResult; import com.cmvr.test.flow.runtime.message.TaskNodeExecuteResult;
@ -12,47 +14,210 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.Objects; /** Executes a screen touch through the edge HLC service. */
@Slf4j @Slf4j
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class EdgeTouchOperateService implements EdgeOperateService { 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 @Override
public boolean supports(ActionEnum action) { public boolean supports(ActionEnum action) {
return action.name().equals("TOUCH"); return ActionEnum.TOUCH == action;
} }
@Override @Override
public TaskNodeExecuteResult execute(TaskNodeExecuteMessage message) { public TaskNodeExecuteResult execute(TaskNodeExecuteMessage message) {
ActionEnum action = message.getAction(); if (1 == 1) {
JSONObject inputParams = message.getInputParams(); try {
String robotId = message.getRobotId(); Thread.sleep(15000);
String deviceId = inputParams.getString("deviceId"); } catch (InterruptedException e) {
String touchParams = inputParams.getString("touchParams"); throw new RuntimeException(e);
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);
} }
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;
}
}
} }

View File

@ -67,10 +67,8 @@ public class TiTouchOperateService implements EdgeOperateService {
String clickFunction = inputParams.getString("funcId"); String clickFunction = inputParams.getString("funcId");
// 查询功能 // 查询功能
JSONObject funcObj = exTiVehicleFunctionService.findNextClickToFunction(vehicleConfigId, currentLocation, clickFunction); JSONObject funcObj = exTiVehicleFunctionService.findNextClickToFunction(vehicleConfigId, currentLocation, clickFunction);
// 输出 // 透传路径、动作类型和动作参数,供触控节点执行及报告追溯。
output.put("nextLocation", funcObj.get("nextLocation")); output.putAll(funcObj);
output.put("isEnd", funcObj.get("isEnd"));
output.put("iconUrl", funcObj.get("iconUrl"));
break; break;
} }
case TI_TOUCH_COORDINATES: { case TI_TOUCH_COORDINATES: {

View File

@ -1,6 +1,6 @@
package com.cmvr.test.service; package com.cmvr.test.service;
import cmvr.msgs.AgvUtils; import cmvr.msgs.Agv;
import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSONObject; 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) .setX(x)
.setY(y) .setY(y)
.setTheta(theta != null ? theta : 0.0) .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( int blockedTimeoutMs = payload.getIntValue(
"blockedTimeoutMs", EdgeAgvService.DEFAULT_BLOCKED_TIMEOUT_MS); "blockedTimeoutMs", EdgeAgvService.DEFAULT_BLOCKED_TIMEOUT_MS);
if (blockedTimeoutMs <= 0) { if (blockedTimeoutMs <= 0) {
blockedTimeoutMs = EdgeAgvService.DEFAULT_BLOCKED_TIMEOUT_MS; blockedTimeoutMs = EdgeAgvService.DEFAULT_BLOCKED_TIMEOUT_MS;
} }
return AgvUtils.AgvMotionOptions.newBuilder() return Agv.AgvMotionOptions.newBuilder()
.setAsynchronous(false) .setAsynchronous(false)
.setBlockedTimeoutMs(blockedTimeoutMs) .setBlockedTimeoutMs(blockedTimeoutMs)
.build(); .build();

View File

@ -62,5 +62,8 @@ public interface ITeNodeInstService extends IService<TeNodeInst> {
/** Marks unfinished node records as stopped when their in-memory execution is gone. */ /** Marks unfinished node records as stopped when their in-memory execution is gone. */
void stopUnfinished(String instId, String message); 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);
} }

View File

@ -11,4 +11,10 @@ public interface ExTiVehicleFunctionService {
public JSONObject findNextClickToFunction(Long vehicleConfigId, String currentUi, String clickFunction); public JSONObject findNextClickToFunction(Long vehicleConfigId, String currentUi, String clickFunction);
/** 返回车型的界面节点、导航边以及配置问题,供后台可视化展示。 */
JSONObject getTopology(Long vehicleConfigId);
/** 检查项目所选功能能否从任意已配置界面到达。 */
JSONObject inspectTopology(Long vehicleConfigId, java.util.List<Long> functionIds);
} }

View File

@ -154,6 +154,17 @@ public class ITeNodeInstServiceImpl extends ServiceImpl<TeNodeInstMapper, TeNode
.set(TeNodeInst::getEndTime, System.currentTimeMillis()); .set(TeNodeInst::getEndTime, System.currentTimeMillis());
this.update(wrapper); this.update(wrapper);
} }
@Override
public void failUnfinished(String instId, String message) {
LambdaUpdateWrapper<TeNodeInst> 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);
}
} }

View File

@ -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<TaskStatusEnum> 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"));
}
}

View File

@ -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<Integer> 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<String, FlowNodeWrapper> 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());
}
}

View File

@ -7,12 +7,18 @@ import com.cmvr.test.flow.builder.FlowParamDef;
import com.cmvr.test.flow.context.TaskContext; import com.cmvr.test.flow.context.TaskContext;
import com.cmvr.test.flow.context.TaskContextManager; import com.cmvr.test.flow.context.TaskContextManager;
import com.cmvr.test.flow.context.TaskInstHolder; 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.TaskNodeExecuteMessage;
import com.cmvr.test.flow.runtime.message.TaskNodeExecuteResult;
import org.junit.Test; import org.junit.Test;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
@ -45,6 +51,45 @@ public class FlowItemExecutorTest {
assertEquals(start.getNodeId(), executor.executedNodeId); 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<String, FlowNodeWrapper> 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 static class RecordingFlowItemExecutor extends FlowItemExecutor {
private String executedNodeId; private String executedNodeId;
@ -63,4 +108,19 @@ public class FlowItemExecutorTest {
executedNodeId = node.getNodeId(); 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<Integer> iterations) {
completed.incrementAndGet();
}
}
} }

View File

@ -1,6 +1,6 @@
package com.cmvr.test.flow.runtime.operator.edge; 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.common.exception.GlobalException;
import com.cmvr.edge.client.model.EdgeCommonVO; import com.cmvr.edge.client.model.EdgeCommonVO;
import com.cmvr.edge.client.service.EdgeAgvService; import com.cmvr.edge.client.service.EdgeAgvService;
@ -25,7 +25,7 @@ public class AgvNavigationWaiterTest {
AtomicInteger queryCount = new AtomicInteger(); AtomicInteger queryCount = new AtomicInteger();
EdgeAgvService service = service(states, queryCount, new AtomicBoolean()); EdgeAgvService service = service(states, queryCount, new AtomicBoolean());
AgvUtils.AgvNavigationStatus result = AgvNavigationWaiter.navigateToStationAndWait( Agv.AgvNavigationStatus result = AgvNavigationWaiter.navigateToStationAndWait(
service, common(), "station-A", 1000, 1); service, common(), "station-A", 1000, 1);
assertEquals(4, result.getState()); assertEquals(4, result.getState());
@ -79,7 +79,7 @@ public class AgvNavigationWaiterTest {
if (next != null) { if (next != null) {
lastState.set(next); lastState.set(next);
} }
return AgvUtils.AgvNavigationStatus.newBuilder() return Agv.AgvNavigationStatus.newBuilder()
.setState(lastState.get()) .setState(lastState.get())
.setProgress(lastState.get() == 4 ? 1.0 : 0.5) .setProgress(lastState.get() == 4 ? 1.0 : 0.5)
.setMessage(lastState.get() == 5 ? "navigation failed" : "") .setMessage(lastState.get() == 5 ? "navigation failed" : "")

View File

@ -27,33 +27,31 @@ public class EdgeActionQueueOperateServiceTest {
captured.set((EdgeActionQueueVO) args[0]); captured.set((EdgeActionQueueVO) args[0]);
return SystemCommand.ActionQueueCommand.Feedback.newBuilder() return SystemCommand.ActionQueueCommand.Feedback.newBuilder()
.setHeader(Common.CommandHeader.Feedback.newBuilder().setSuccess(true)) .setHeader(Common.CommandHeader.Feedback.newBuilder().setSuccess(true))
.setActionId("action-1")
.setCompletedSteps(2) .setCompletedSteps(2)
.setResult(SystemCommand.ActionResultCode.ACTION_RESULT_CODE_COMPLETED) .setResult(SystemCommand.ActionResultCode.ACTION_RESULT_CODE_COMPLETED)
.setServiceInstanceId("instance-1")
.setDeduplicationStatus(SystemCommand.ActionDeduplicationStatus.ACTION_DEDUPLICATION_STATUS_ACCEPTED_NEW)
.build(); .build();
}); });
EdgeActionQueueOperateService service = new EdgeActionQueueOperateService(edgeSystemService); EdgeActionQueueOperateService service = new EdgeActionQueueOperateService(edgeSystemService);
JSONArray steps = new JSONArray() JSONArray steps = new JSONArray()
.fluentAdd(new JSONObject().fluentPut("stepId", "move") .fluentAdd(new JSONObject().fluentPut("stepId", "move")
.fluentPut("type", "AGV_NAVIGATE_TO_STATION") .fluentPut("type", "ARM_MOVE_J")
.fluentPut("deviceBindingKey", "queue_agv") .fluentPut("deviceBindingKey", "queue_arm")
.fluentPut("params", new JSONObject().fluentPut("stationId", "station-2"))) .fluentPut("params", new JSONObject().fluentPut("positions", new JSONArray().fluentAdd(0D))))
.fluentAdd(new JSONObject().fluentPut("stepId", "wait") .fluentAdd(new JSONObject().fluentPut("stepId", "wait")
.fluentPut("type", "DELAY") .fluentPut("type", "ARM_MOVE_L")
.fluentPut("params", new JSONObject().fluentPut("durationMs", 500))); .fluentPut("deviceId", "arm-2")
.fluentPut("params", new JSONObject().fluentPut("x", 0.1D)));
TaskNodeExecuteMessage message = new TaskNodeExecuteMessage(); TaskNodeExecuteMessage message = new TaskNodeExecuteMessage();
message.setAction(ActionEnum.SYSTEM_EXECUTE_ACTION_QUEUE); message.setAction(ActionEnum.SYSTEM_EXECUTE_ACTION_QUEUE);
message.setRobotId("robot-1"); 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)); message.setInputParams(new JSONObject().fluentPut("totalTimeoutMs", 10000).fluentPut("steps", steps));
TaskNodeExecuteResult result = service.execute(message); TaskNodeExecuteResult result = service.execute(message);
assertTrue(result.isSuccess()); 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("robot-1", captured.get().getRobotId());
assertEquals(2, result.getOutputParams().getIntValue("completedSteps")); assertEquals(2, result.getOutputParams().getIntValue("completedSteps"));
} }

View File

@ -108,4 +108,5 @@ public class EdgeDeviceCommandOperateServiceTest {
.setResponseJson(responseJson) .setResponseJson(responseJson)
.build(); .build();
} }
} }

View File

@ -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<EdgeTouchVO> 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<EdgeTouchVO> 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<EdgeSystemJsonCommandVO> 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;
}
}

View File

@ -27,4 +27,19 @@ public class TiVehicleFunction extends BaseEntity {
@ApiModelProperty("图标URL") @ApiModelProperty("图标URL")
private String iconUrl; 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;
} }

View File

@ -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<CheckItem> 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;
}
}

View File

@ -6,6 +6,7 @@ import com.cmvr.test.model.domain.TeTaskInst;
import com.cmvr.test.model.vo.TeQueryTaskOrchestraItemVO; import com.cmvr.test.model.vo.TeQueryTaskOrchestraItemVO;
import com.cmvr.test.model.vo.TeTaskExecuteProjectVO; import com.cmvr.test.model.vo.TeTaskExecuteProjectVO;
import com.cmvr.ti.model.domain.TiProject; import com.cmvr.ti.model.domain.TiProject;
import com.cmvr.ti.model.vo.TiProjectReadinessVO;
import java.util.List; import java.util.List;
@ -22,6 +23,8 @@ public interface ITiProjectService extends IService<TiProject> {
public List<TeQueryTaskOrchestraItemVO> queryExecuteRunParams(String projectId); public List<TeQueryTaskOrchestraItemVO> queryExecuteRunParams(String projectId);
public TiProjectReadinessVO checkExecutionReadiness(String projectId);
public String executeProject(TeTaskExecuteProjectVO taskExecuteProjectVO); public String executeProject(TeTaskExecuteProjectVO taskExecuteProjectVO);
public TeTaskInst getTeTaskInst(String projectId); public TeTaskInst getTeTaskInst(String projectId);

View File

@ -2,15 +2,13 @@ package com.cmvr.ti.service.ex;
import cn.hutool.core.text.StrPool; import cn.hutool.core.text.StrPool;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray; import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject; import com.alibaba.fastjson2.JSONObject;
import com.cmvr.common.core.domain.entity.SysDictData; import com.cmvr.common.core.domain.entity.SysDictData;
import com.cmvr.common.exception.GlobalException; import com.cmvr.common.exception.GlobalException;
import com.cmvr.common.utils.StringUtils; 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.service.ISysDictTypeService;
import com.cmvr.system.util.CarPathFinderUtil;
import com.cmvr.test.service.ex.ExTiVehicleFunctionService; import com.cmvr.test.service.ex.ExTiVehicleFunctionService;
import com.cmvr.ti.model.domain.TiVehicleFunction; import com.cmvr.ti.model.domain.TiVehicleFunction;
import com.cmvr.ti.model.domain.TiVehicleUi; import com.cmvr.ti.model.domain.TiVehicleUi;
@ -19,25 +17,33 @@ import com.cmvr.ti.service.ITiVehicleFunctionService;
import com.cmvr.ti.service.ITiVehicleUiService; import com.cmvr.ti.service.ITiVehicleUiService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service; 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 @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class ExTiVehicleFunctionServiceImpl implements ExTiVehicleFunctionService { public class ExTiVehicleFunctionServiceImpl implements ExTiVehicleFunctionService {
private final ITiVehicleFunctionService tiVehicleFunctionService; private final ITiVehicleFunctionService tiVehicleFunctionService;
private final ITiVehicleUiService tiVehicleUiService; private final ITiVehicleUiService tiVehicleUiService;
private final ISysDictTypeService sysDictTypeService;
private final ISysDictTypeService iSysDictTypeService;
@Override @Override
public JSONArray queryByIds(String ids) { public JSONArray queryByIds(String ids) {
List<TiVehicleFunction> tiVehicleFunctions = tiVehicleFunctionService.listByIds(StrUtil.split(StrPool.COMMA, ids)); if (StrUtil.isBlank(ids)) {
return JSONArray.from(tiVehicleFunctions); return new JSONArray();
}
return JSONArray.from(tiVehicleFunctionService.listByIds(StrUtil.split(StrPool.COMMA, ids)));
} }
@Override @Override
@ -45,86 +51,347 @@ public class ExTiVehicleFunctionServiceImpl implements ExTiVehicleFunctionServic
return JSONObject.from(tiVehicleFunctionService.getById(id)); return JSONObject.from(tiVehicleFunctionService.getById(id));
} }
/**
* 寻找从当前页面到能执行目标功能的最短点击路径
* 返回:点击的功能序列(JSONArray 包装 List<TiVehicleFunction>)
*/
@Override @Override
public JSONObject findNextClickToFunction( public JSONObject findNextClickToFunction(Long vehicleConfigId, String startUiName, String targetFunction) {
Long vehicleConfigId, Graph graph = loadGraph(vehicleConfigId);
String startUiName, if (!graph.issues.isEmpty()) {
String targetFuncName) { throw new GlobalException("界面路径配置不完整:" + String.join(";", graph.issues));
// 功能字典 }
// 实际用的是 sys_dict_data,这里假设你有方法能查到 data 列表 String startKey = graph.resolveUiKey(startUiName);
List<SysDictData> functionDictData = iSysDictTypeService.selectDictDataByType("ti_function_config"); if (startKey == null) {
throw new GlobalException("未识别到已配置的当前界面:" + startUiName);
// 界面字典 }
List<SysDictData> uiDictData = iSysDictTypeService.selectDictDataByType("ti_ui_config"); TiVehicleFunction target = graph.resolveFunction(targetFunction);
if (target == null) {
// 构建 功能 value → label 的映射 throw new GlobalException("目标功能不存在或不属于当前车型:" + targetFunction);
Map<String, String> funcValueToLabel = new HashMap<>(); }
for (SysDictData data : functionDictData) { List<Step> path = graph.findPath(startKey, target.getId());
funcValueToLabel.put(data.getDictValue(), data.getDictLabel()); if (path.isEmpty()) {
throw new GlobalException("从“" + graph.uiName(startKey) + "”无法到达功能“"
+ graph.functionName(target) + "”,请补充界面功能或目标界面配置");
} }
// 构建 界面 value → label 的映射 Step next = path.get(0);
Map<String, String> uiValueToLabel = new HashMap<>(); JSONObject result = stepJson(graph, next);
for (SysDictData data : uiDictData) { result.put("nextLocation", graph.functionName(next.function));
uiValueToLabel.put(data.getDictValue(), data.getDictLabel()); 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));
}
result.put("path", pathJson);
return result;
} }
// 1. 查询当前车型的实际界面和功能数据 @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<Long> functionIds) {
Graph graph = loadGraph(vehicleConfigId);
List<String> issues = new ArrayList<>(graph.issues);
Set<String> 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<String, String> uiNames = dictMap("ti_ui_config");
Map<String, String> functionNames = dictMap("ti_function_config");
List<TiVehicleUi> uiList = tiVehicleUiService.selectTiVehicleUiList( List<TiVehicleUi> uiList = tiVehicleUiService.selectTiVehicleUiList(
TiVehicleUi.builder().vehicleConfigId(vehicleConfigId).build()); TiVehicleUi.builder().vehicleConfigId(vehicleConfigId).build());
List<TiVehicleFunction> functionList = tiVehicleFunctionService.selectTiVehicleFunctionList(
List<TiVehicleFunction> funcList = tiVehicleFunctionService.selectTiVehicleFunctionList(
TiVehicleFunctionVO.builder().vehicleConfigId(vehicleConfigId).build()); TiVehicleFunctionVO.builder().vehicleConfigId(vehicleConfigId).build());
return new Graph(uiList, functionList, uiNames, functionNames);
// 2. funcKey → TiVehicleFunction }
Map<Long, TiVehicleFunction> funcById = new HashMap<>();
for (TiVehicleFunction f : funcList) { private Map<String, String> dictMap(String type) {
funcById.put(f.getId(), f); Map<String, String> result = new HashMap<>();
List<SysDictData> 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<String, TiVehicleUi> uis = new LinkedHashMap<>();
private final Map<Long, TiVehicleFunction> functions = new LinkedHashMap<>();
private final Map<String, String> uiNames;
private final Map<String, String> functionNames;
private final Set<Long> attachedFunctionIds = new HashSet<>();
private final List<String> issues = new ArrayList<>();
private Graph(List<TiVehicleUi> uiList, List<TiVehicleFunction> functionList,
Map<String, String> uiNames, Map<String, String> 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;
} }
// 3.组装uiMap
Map<String, CarUi> uiMap = new HashMap<>();
List<CarIcon> icons;
CarUi carUi;
for (TiVehicleUi ui : uiList) { for (TiVehicleUi ui : uiList) {
icons = new ArrayList<>(); if (StrUtil.isBlank(ui.getUiKey()) || StrUtil.isBlank(uiName(ui.getUiKey()))) {
for (String funcId : StringUtils.split(ui.getFuncIds(), StrPool.COMMA)) { issues.add("界面ID " + ui.getId() + " 未配置有效的界面类型");
TiVehicleFunction func = funcById.get(Long.valueOf(funcId)); continue;
CarIcon icon = new CarIcon(
funcValueToLabel.get(func.getFuncKey()),
func.getIconUrl(),
"",
"",
"1".equals(func.getRemark()) ? funcValueToLabel.get(func.getFuncKey()) : ""
);
icons.add(icon);
} }
carUi = new CarUi( if (uis.put(ui.getUiKey(), ui) != null) {
uiValueToLabel.get(ui.getUiKey()), issues.add("界面“" + uiName(ui.getUiKey()) + "”重复配置");
ui.getUiUrl(), }
icons if (StrUtil.isBlank(ui.getUiUrl())) {
); issues.add("界面“" + uiName(ui.getUiKey()) + "”缺少识别截图");
uiMap.put(carUi.getUiName(), carUi); }
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) + "”");
} }
// 4.获取路径
List<CarIcon> path = CarPathFinderUtil.findPath(uiMap, startUiName, targetFuncName);
if (CollectionUtils.isEmpty(path)) {
throw new GlobalException("没有找到匹配的路径");
} }
// 5.组装结果
Map<String, Object> 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);
} }
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<Long> functionIds(TiVehicleUi ui) {
List<Long> 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<Step> findPath(String startKey, Long targetFunctionId) {
if (!uis.containsKey(startKey) || !functions.containsKey(targetFunctionId)) {
return Collections.emptyList();
}
ArrayDeque<String> queue = new ArrayDeque<>();
Map<String, Step> previous = new HashMap<>();
Set<String> 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<Step> 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<Step> rebuildPath(String startKey, String destination, Map<String, Step> previous) {
List<Step> 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 <T> Collection<T> safe(Collection<T> value) {
return value == null ? Collections.emptyList() : value;
}
}
} }

View File

@ -5,6 +5,8 @@ import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSON; 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.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers; 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.engine.FlowTaskRuntimeService;
import com.cmvr.test.flow.runtime.event.FlowExecutionModuleCodes; import com.cmvr.test.flow.runtime.event.FlowExecutionModuleCodes;
import com.cmvr.test.model.domain.TeDetectionItem; 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.TeTaskConfigInfo;
import com.cmvr.test.model.domain.TeTaskInst; import com.cmvr.test.model.domain.TeTaskInst;
import com.cmvr.test.model.vo.TeQueryTaskOrchestraItemVO; import com.cmvr.test.model.vo.TeQueryTaskOrchestraItemVO;
import com.cmvr.test.model.vo.TeTaskExecuteProjectVO; import com.cmvr.test.model.vo.TeTaskExecuteProjectVO;
import com.cmvr.test.model.vo.TeTaskOrchestraVO; import com.cmvr.test.model.vo.TeTaskOrchestraVO;
import com.cmvr.test.service.ITeDetectionItemService; import com.cmvr.test.service.ITeDetectionItemService;
import com.cmvr.test.service.ITeDetectionItemVersionService;
import com.cmvr.test.service.ITeTaskConfigInfoService; import com.cmvr.test.service.ITeTaskConfigInfoService;
import com.cmvr.test.service.ITeTaskInstService; import com.cmvr.test.service.ITeTaskInstService;
import com.cmvr.test.service.ITeTaskOrchestrationService; import com.cmvr.test.service.ITeTaskOrchestrationService;
import com.cmvr.test.service.ex.ExTiVehicleFunctionService;
import com.cmvr.ti.mapper.TiProjectMapper; import com.cmvr.ti.mapper.TiProjectMapper;
import com.cmvr.ti.model.domain.TiProject; import com.cmvr.ti.model.domain.TiProject;
import com.cmvr.ti.model.domain.TiVehicleFunction; 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.ITiProjectService;
import com.cmvr.ti.service.ITiVehicleFunctionService; import com.cmvr.ti.service.ITiVehicleFunctionService;
import com.cmvr.ti.support.TiEvaluationRecorder; import com.cmvr.ti.support.TiEvaluationRecorder;
@ -42,9 +48,16 @@ import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.ArrayDeque;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
@ -52,10 +65,12 @@ public class TiProjectServiceImpl extends ServiceImpl<TiProjectMapper, TiProject
private final ITeTaskOrchestrationService taskOrchestrationService; private final ITeTaskOrchestrationService taskOrchestrationService;
private final ITeDetectionItemService detectionItemService; private final ITeDetectionItemService detectionItemService;
private final ITeDetectionItemVersionService detectionItemVersionService;
private final ITeTaskConfigInfoService taskConfigInfoService; private final ITeTaskConfigInfoService taskConfigInfoService;
private final ITeTaskInstService taskInstService; private final ITeTaskInstService taskInstService;
private final FlowTaskRuntimeService flowTaskRuntimeService; private final FlowTaskRuntimeService flowTaskRuntimeService;
private final ITiVehicleFunctionService tiVehicleFunctionService; private final ITiVehicleFunctionService tiVehicleFunctionService;
private final ExTiVehicleFunctionService exTiVehicleFunctionService;
private final IAeEvaluationService aeEvaluationService; private final IAeEvaluationService aeEvaluationService;
private final AeIndicatorService aeIndicatorService; private final AeIndicatorService aeIndicatorService;
private final TiEvaluationRecorder evaluationRecorder; private final TiEvaluationRecorder evaluationRecorder;
@ -76,21 +91,7 @@ public class TiProjectServiceImpl extends ServiceImpl<TiProjectMapper, TiProject
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public int insertTiProject(TiProject tiProject) { public int insertTiProject(TiProject tiProject) {
// 校验功能ID是否属于同一车型配置 requireProjectFunctions(tiProject);
List<Long> ids = JSON.parseArray(tiProject.getTestFuncIds(), Long.class);
List<TiVehicleFunction> 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不属于同一车型配置");
}
tiProject.setStatus("0"); tiProject.setStatus("0");
int insert = this.baseMapper.insert(tiProject); int insert = this.baseMapper.insert(tiProject);
@ -112,6 +113,16 @@ public class TiProjectServiceImpl extends ServiceImpl<TiProjectMapper, TiProject
@Override @Override
public int updateTiProject(TiProject tiProject) { public int updateTiProject(TiProject tiProject) {
TiProject current = getById(tiProject.getProjectId());
if (current == null) {
throw new GlobalException("触控项目不存在:" + tiProject.getProjectId());
}
TiProject candidate = new TiProject();
candidate.setVehicleConfigId(tiProject.getVehicleConfigId() == null
? current.getVehicleConfigId() : tiProject.getVehicleConfigId());
candidate.setTestFuncIds(StrUtil.isBlank(tiProject.getTestFuncIds())
? current.getTestFuncIds() : tiProject.getTestFuncIds());
requireProjectFunctions(candidate);
return this.baseMapper.updateById(tiProject); return this.baseMapper.updateById(tiProject);
} }
@ -126,15 +137,104 @@ public class TiProjectServiceImpl extends ServiceImpl<TiProjectMapper, TiProject
return taskOrchestrationService.queryByTaskId(projectId); return taskOrchestrationService.queryByTaskId(projectId);
} }
@Override
public TiProjectReadinessVO checkExecutionReadiness(String projectId) {
TiProject project = getById(projectId);
if (project == null) {
throw new GlobalException("触控项目不存在:" + projectId);
}
TiProjectReadinessVO result = new TiProjectReadinessVO();
result.setProjectId(projectId);
List<Long> functionIds = parseFunctionIds(project.getTestFuncIds());
List<TiVehicleFunction> 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<Long> 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<TeQueryTaskOrchestraItemVO> 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 @Override
public String executeProject(TeTaskExecuteProjectVO taskExecuteProjectVO) { public String executeProject(TeTaskExecuteProjectVO taskExecuteProjectVO) {
TiProject project = getById(taskExecuteProjectVO.getProjectId()); TiProject project = getById(taskExecuteProjectVO.getProjectId());
if (project == null) { if (project == null) {
throw new GlobalException("触控项目不存在:" + taskExecuteProjectVO.getProjectId()); 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(); TeTaskExecuteProjectVO normalVO = new TeTaskExecuteProjectVO();
normalVO.setProjectId(taskExecuteProjectVO.getProjectId()); normalVO.setProjectId(taskExecuteProjectVO.getProjectId());
normalVO.setRunParams(taskExecuteProjectVO.getRunParams()); normalVO.setRunParams(buildProjectRunParams(project, taskExecuteProjectVO.getRunParams()));
normalVO.setRobotId(taskExecuteProjectVO.getRobotId()); normalVO.setRobotId(taskExecuteProjectVO.getRobotId());
normalVO.setRuntimeAssignments(taskExecuteProjectVO.getRuntimeAssignments()); normalVO.setRuntimeAssignments(taskExecuteProjectVO.getRuntimeAssignments());
normalVO.setRunMode(RunModeEnum.TI_PROJECT); normalVO.setRunMode(RunModeEnum.TI_PROJECT);
@ -245,6 +345,9 @@ public class TiProjectServiceImpl extends ServiceImpl<TiProjectMapper, TiProject
itemDto.setFailureReason(dto.getFailureReason()); itemDto.setFailureReason(dto.getFailureReason());
itemDto.setVideoPath(StrUtil.blankToDefault(dto.getVideoPath(), evaluation.getVideoPath())); itemDto.setVideoPath(StrUtil.blankToDefault(dto.getVideoPath(), evaluation.getVideoPath()));
itemDto.setExecutionDetail(dto.getExecutionDetail()); itemDto.setExecutionDetail(dto.getExecutionDetail());
itemDto.setExecutionSteps(dto.getExecutionSteps());
itemDto.setUiPath(dto.getUiPath());
itemDto.setScreenshotUrls(dto.getScreenshotUrls());
itemDetails.add(itemDto); itemDetails.add(itemDto);
} }
@ -269,6 +372,171 @@ public class TiProjectServiceImpl extends ServiceImpl<TiProjectMapper, TiProject
return vo; return vo;
} }
private List<TiVehicleFunction> requireProjectFunctions(TiProject project) {
if (project.getVehicleConfigId() == null) {
throw new GlobalException("请选择车型配置");
}
List<Long> ids = parseFunctionIds(project.getTestFuncIds());
if (ids.isEmpty()) {
throw new GlobalException("请至少选择一个测试功能");
}
if (new HashSet<>(ids).size() != ids.size()) {
throw new GlobalException("测试功能不能重复");
}
List<TiVehicleFunction> 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<Long> parseFunctionIds(String value) {
if (StrUtil.isBlank(value)) {
return Collections.emptyList();
}
try {
List<Long> 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<String, String> 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<String, Set<String>> 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<String, String> actionByNode, String action) {
return actionByNode.entrySet().stream()
.filter(entry -> action.equalsIgnoreCase(entry.getValue()))
.map(Map.Entry::getKey)
.findFirst()
.orElse(null);
}
private Map<String, Set<String>> adjacency(JSONArray edges) {
Map<String, Set<String>> 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<String, Set<String>> adjacency, String source, String target) {
if (source == null || target == null) return false;
ArrayDeque<String> queue = new ArrayDeque<>();
Set<String> 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) { private AeIResultDTO parseEvaluationResult(String resultJson) {
if (StrUtil.isBlank(resultJson)) { if (StrUtil.isBlank(resultJson)) {
return new AeIResultDTO(); return new AeIResultDTO();

View File

@ -8,6 +8,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.spring.service.impl.ServiceImpl; import com.baomidou.mybatisplus.spring.service.impl.ServiceImpl;
import com.cmvr.common.core.domain.entity.SysDictData; 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.ISysDictDataService;
import com.cmvr.system.service.ISysDictTypeService; import com.cmvr.system.service.ISysDictTypeService;
import com.cmvr.ti.mapper.TiVehicleFunctionMapper; import com.cmvr.ti.mapper.TiVehicleFunctionMapper;
@ -40,7 +41,8 @@ public class TiVehicleFunctionServiceImpl extends ServiceImpl<TiVehicleFunctionM
@Override @Override
public JSONObject selectTiVehicleFunctionDetailById(Long id) { public JSONObject selectTiVehicleFunctionDetailById(Long id) {
JSONObject jsonObject = JSONObject.from(this.getById(id)); JSONObject jsonObject = JSONObject.from(this.getById(id));
jsonObject.put("funcName", sysDictDataService.selectDictLabel("ti_function_config", jsonObject.getString("funcKey"))); String name = sysDictDataService.selectDictLabel("ti_function_config", jsonObject.getString("funcKey"));
jsonObject.put("funcName", StrUtil.blankToDefault(name, jsonObject.getString("funcKey")));
return jsonObject; return jsonObject;
} }
@ -60,17 +62,53 @@ public class TiVehicleFunctionServiceImpl extends ServiceImpl<TiVehicleFunctionM
} }
@Override @Override
public int insertTiVehicleFunction(TiVehicleFunction TiVehicleFunction) { public int insertTiVehicleFunction(TiVehicleFunction tiVehicleFunction) {
return this.baseMapper.insert(TiVehicleFunction); normalizeAndValidate(tiVehicleFunction);
return this.baseMapper.insert(tiVehicleFunction);
} }
@Override @Override
public int updateTiVehicleFunction(TiVehicleFunction TiVehicleFunction) { public int updateTiVehicleFunction(TiVehicleFunction tiVehicleFunction) {
return this.baseMapper.updateById(TiVehicleFunction); normalizeAndValidate(tiVehicleFunction);
return this.baseMapper.updateById(tiVehicleFunction);
} }
@Override @Override
public int deleteTiVehicleFunctionByIds(Long[] ids) { public int deleteTiVehicleFunctionByIds(Long[] ids) {
return this.baseMapper.deleteBatchIds(Arrays.asList(ids)); return this.baseMapper.deleteBatchIds(Arrays.asList(ids));
} }
private void normalizeAndValidate(TiVehicleFunction function) {
if (function.getVehicleConfigId() == null) {
throw new GlobalException("请选择车型配置");
}
if (StrUtil.isBlank(function.getFuncKey())) {
throw new GlobalException("请选择或填写功能名称");
}
if (StrUtil.isBlank(function.getIconUrl())) {
throw new GlobalException("请上传功能图标,用于识别触控坐标");
}
String operationType = StrUtil.blankToDefault(function.getOperationType(), "CLICK").toUpperCase();
if (!Arrays.asList("CLICK", "LONG_PRESS", "SWIPE", "EDGE_SCRIPT").contains(operationType)) {
throw new GlobalException("不支持的操作类型:" + operationType);
}
function.setOperationType(operationType);
int repeatCount = function.getRepeatCount() == null ? 1 : function.getRepeatCount();
int waitMs = function.getWaitMs() == null ? 1000 : function.getWaitMs();
if (repeatCount < 1 || repeatCount > 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对象");
}
}
}
} }

View File

@ -6,13 +6,19 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.spring.service.impl.ServiceImpl; import com.baomidou.mybatisplus.spring.service.impl.ServiceImpl;
import com.cmvr.ti.mapper.TiVehicleUiMapper; 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.model.domain.TiVehicleUi;
import com.cmvr.ti.service.ITiVehicleFunctionService;
import com.cmvr.ti.service.ITiVehicleUiService; import com.cmvr.ti.service.ITiVehicleUiService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.ArrayList;
import java.util.Objects;
/** /**
* 车机界面Service业务层处理 * 车机界面Service业务层处理
@ -21,6 +27,8 @@ import java.util.List;
@RequiredArgsConstructor @RequiredArgsConstructor
public class TiVehicleUiServiceImpl extends ServiceImpl<TiVehicleUiMapper, TiVehicleUi> implements ITiVehicleUiService { public class TiVehicleUiServiceImpl extends ServiceImpl<TiVehicleUiMapper, TiVehicleUi> implements ITiVehicleUiService {
private final ITiVehicleFunctionService tiVehicleFunctionService;
@Override @Override
public TiVehicleUi selectTiVehicleUiById(Long id) { public TiVehicleUi selectTiVehicleUiById(Long id) {
return this.baseMapper.selectById(id); return this.baseMapper.selectById(id);
@ -37,11 +45,13 @@ public class TiVehicleUiServiceImpl extends ServiceImpl<TiVehicleUiMapper, TiVeh
@Override @Override
public int insertTiVehicleUi(TiVehicleUi tiVehicleUi) { public int insertTiVehicleUi(TiVehicleUi tiVehicleUi) {
validate(tiVehicleUi);
return this.baseMapper.insert(tiVehicleUi); return this.baseMapper.insert(tiVehicleUi);
} }
@Override @Override
public int updateTiVehicleUi(TiVehicleUi tiVehicleUi) { public int updateTiVehicleUi(TiVehicleUi tiVehicleUi) {
validate(tiVehicleUi);
return this.baseMapper.updateById(tiVehicleUi); return this.baseMapper.updateById(tiVehicleUi);
} }
@ -49,4 +59,43 @@ public class TiVehicleUiServiceImpl extends ServiceImpl<TiVehicleUiMapper, TiVeh
public int deleteTiVehicleUiByIds(Long[] ids) { public int deleteTiVehicleUiByIds(Long[] ids) {
return this.baseMapper.deleteBatchIds(Arrays.asList(ids)); return this.baseMapper.deleteBatchIds(Arrays.asList(ids));
} }
private void validate(TiVehicleUi ui) {
if (ui.getVehicleConfigId() == null) {
throw new GlobalException("请选择车型配置");
}
if (StrUtil.isBlank(ui.getUiKey())) {
throw new GlobalException("请选择界面类型");
}
if (StrUtil.isBlank(ui.getUiUrl())) {
throw new GlobalException("请上传界面截图,用于页面识别");
}
long duplicateCount = this.count(Wrappers.<TiVehicleUi>lambdaQuery()
.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<Long> 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<TiVehicleFunction> functions = tiVehicleFunctionService.listByIds(functionIds);
if (functions.size() != functionIds.size()
|| functions.stream().anyMatch(item -> !Objects.equals(
item.getVehicleConfigId(), ui.getVehicleConfigId()))) {
throw new GlobalException("关联功能必须全部属于当前车型");
}
}
} }

View File

@ -4,6 +4,7 @@ import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject; import com.alibaba.fastjson2.JSONObject;
import com.cmvr.common.core.domain.entity.SysDictData; import com.cmvr.common.core.domain.entity.SysDictData;
import com.cmvr.evaluation.model.domain.AeEvaluation; import com.cmvr.evaluation.model.domain.AeEvaluation;
@ -59,6 +60,9 @@ public class TiEvaluationRecorder {
private final ConcurrentMap<String, String> latestVideoUrls = new ConcurrentHashMap<>(); private final ConcurrentMap<String, String> latestVideoUrls = new ConcurrentHashMap<>();
private final ConcurrentMap<String, String> businessFailures = new ConcurrentHashMap<>(); private final ConcurrentMap<String, String> businessFailures = new ConcurrentHashMap<>();
private final ConcurrentMap<String, ConcurrentLinkedQueue<String>> executionDetails = new ConcurrentHashMap<>(); private final ConcurrentMap<String, ConcurrentLinkedQueue<String>> executionDetails = new ConcurrentHashMap<>();
private final ConcurrentMap<String, ConcurrentLinkedQueue<AeIResultDTO.ExecutionStep>> executionSteps = new ConcurrentHashMap<>();
private final ConcurrentMap<String, ConcurrentLinkedQueue<String>> uiPaths = new ConcurrentHashMap<>();
private final ConcurrentMap<String, ConcurrentLinkedQueue<String>> screenshotUrls = new ConcurrentHashMap<>();
/** Creates one report row for each function selected by the touch project. */ /** Creates one report row for each function selected by the touch project. */
public void initialize(String projectId, String instId) { 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.setResponseTime(start == null ? null : Math.max(0L, System.currentTimeMillis() - start));
result.setVideoPath(videoUrl); result.setVideoPath(videoUrl);
result.setExecutionDetail(buildExecutionDetail(event.getInstId(), evaluation.getAeId())); 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.setStatus(success ? STATUS_SUCCESS : STATUS_FAILED);
evaluation.setVideoPath(videoUrl); evaluation.setVideoPath(videoUrl);
@ -334,6 +341,46 @@ public class TiEvaluationRecorder {
String nodeName = StrUtil.blankToDefault(event.getNodeName(), event.getNodeId()); String nodeName = StrUtil.blankToDefault(event.getNodeName(), event.getNodeId());
String detail = StrUtil.blankToDefault(nodeName, "流程节点") + statusText; String detail = StrUtil.blankToDefault(nodeName, "流程节点") + statusText;
executionDetails.computeIfAbsent(key, ignored -> new ConcurrentLinkedQueue<>()).add(detail); 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) { 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); return StrUtil.sub(String.join(";", details.subList(fromIndex, details.size())), 0, 2000);
} }
private List<AeIResultDTO.ExecutionStep> buildExecutionSteps(String instId, String evaluationId) {
List<AeIResultDTO.ExecutionStep> 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<String> buildStringEvidence(
ConcurrentMap<String, ConcurrentLinkedQueue<String>> source,
String instId, String evaluationId) {
List<String> result = new ArrayList<>();
appendUnique(result, source.get(detailKey(instId, "*")));
appendUnique(result, source.get(detailKey(instId, evaluationId)));
return result;
}
private void appendEvidence(ConcurrentMap<String, ConcurrentLinkedQueue<String>> target,
String key, String value) {
if (StrUtil.isBlank(value)) return;
target.computeIfAbsent(key, ignored -> new ConcurrentLinkedQueue<>()).add(value);
}
private void appendUnique(List<String> target, Iterable<String> 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<Long> parseFunctionIds(String json) { private List<Long> parseFunctionIds(String json) {
if (StrUtil.isBlank(json)) { if (StrUtil.isBlank(json)) {
return Collections.emptyList(); return Collections.emptyList();
@ -394,6 +480,9 @@ public class TiEvaluationRecorder {
private void clearRuntimeCache(String instId) { private void clearRuntimeCache(String instId) {
startedAt.keySet().removeIf(key -> key.startsWith(instId + "|")); startedAt.keySet().removeIf(key -> key.startsWith(instId + "|"));
executionDetails.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); latestVideoUrls.remove(instId);
businessFailures.remove(instId); businessFailures.remove(instId);
initializationLocks.remove(instId); initializationLocks.remove(instId);

View File

@ -47,6 +47,8 @@ public class TiEvaluationRecorderTest {
assertEquals("成功", result.getOverallResult()); assertEquals("成功", result.getOverallResult());
assertEquals("http://files/touch-1.mp4", result.getVideoPath()); assertEquals("http://files/touch-1.mp4", result.getVideoPath());
assertTrue(result.getExecutionDetail().contains("结束录像执行完成")); assertTrue(result.getExecutionDetail().contains("结束录像执行完成"));
assertEquals(1, result.getExecutionSteps().size());
assertEquals("CAMERA_RECORDING_STOP", result.getExecutionSteps().get(0).getAction());
assertEquals("2", fixture.project.getStatus()); assertEquals("2", fixture.project.getStatus());
} }

View File

@ -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;