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:
parent
95c3030b91
commit
01da7a2dd4
@ -1,6 +1,6 @@
|
||||
package com.cmvr.web.controller.api;
|
||||
|
||||
import cmvr.msgs.AgvUtils;
|
||||
import cmvr.msgs.Agv;
|
||||
import com.cmvr.common.core.domain.AjaxResult;
|
||||
import com.cmvr.edge.client.model.EdgeCommonVO;
|
||||
import com.cmvr.edge.client.model.agv.EdgeAgvMapNameVO;
|
||||
@ -58,7 +58,7 @@ public class EdgeAgvController {
|
||||
@ApiParam(value = "设备ID", required = true)
|
||||
@RequestParam("deviceId") String deviceId) {
|
||||
EdgeCommonVO vo = commonVo(robotId, deviceId);
|
||||
AgvUtils.AgvRuntimeState state = edgeAgvService.getRuntimeState(vo);
|
||||
Agv.AgvRuntimeState state = edgeAgvService.getRuntimeState(vo);
|
||||
return AjaxResult.ok(toRuntimeStateMap(state));
|
||||
}
|
||||
|
||||
@ -73,7 +73,7 @@ public class EdgeAgvController {
|
||||
@ApiParam(value = "设备ID", required = true)
|
||||
@RequestParam("deviceId") String deviceId) {
|
||||
EdgeCommonVO vo = commonVo(robotId, deviceId);
|
||||
AgvUtils.AgvNavigationStatus status = edgeAgvService.getNavigationStatus(vo);
|
||||
Agv.AgvNavigationStatus status = edgeAgvService.getNavigationStatus(vo);
|
||||
Map<String, Object> resultMap = new HashMap<>();
|
||||
resultMap.put("state", status.getState());
|
||||
resultMap.put("type", status.getType());
|
||||
@ -108,7 +108,7 @@ public class EdgeAgvController {
|
||||
@ApiOperation("AGV重定位")
|
||||
@PostMapping("/relocalize")
|
||||
public AjaxResult relocalize(@RequestBody @Validated EdgeAgvRelocalizeVO vo) {
|
||||
AgvUtils.AgvPose2d pose = AgvUtils.AgvPose2d.newBuilder()
|
||||
Agv.AgvPose2d pose = Agv.AgvPose2d.newBuilder()
|
||||
.setX(vo.getX())
|
||||
.setY(vo.getY())
|
||||
.setTheta(vo.getTheta())
|
||||
@ -120,13 +120,13 @@ public class EdgeAgvController {
|
||||
@ApiOperation("固定距离平移")
|
||||
@PostMapping("/translate")
|
||||
public AjaxResult translate(@RequestBody @Validated EdgeAgvTranslateVO vo) {
|
||||
AgvUtils.AgvTranslationMode mode = vo.getMode() == null
|
||||
? AgvUtils.AgvTranslationMode.AGV_TRANSLATION_MODE_ODOMETRY
|
||||
: AgvUtils.AgvTranslationMode.forNumber(vo.getMode());
|
||||
Agv.AgvTranslationMode mode = vo.getMode() == null
|
||||
? Agv.AgvTranslationMode.AGV_TRANSLATION_MODE_ODOMETRY
|
||||
: Agv.AgvTranslationMode.forNumber(vo.getMode());
|
||||
if (mode == null) {
|
||||
return AjaxResult.error("距离参考模式不合法,允许值:0=里程,1=定位");
|
||||
}
|
||||
AgvUtils.AgvTranslation translation = AgvUtils.AgvTranslation.newBuilder()
|
||||
Agv.AgvTranslation translation = Agv.AgvTranslation.newBuilder()
|
||||
.setDistance(vo.getDistance())
|
||||
.setVx(vo.getVx())
|
||||
.setVy(vo.getVy())
|
||||
@ -142,7 +142,7 @@ public class EdgeAgvController {
|
||||
@ApiOperation("导航到指定位置")
|
||||
@PostMapping("/navigateToPose")
|
||||
public AjaxResult navigateToPose(@RequestBody @Validated EdgeAgvNavigateToPoseVO vo) {
|
||||
AgvUtils.AgvPose2d pose = AgvUtils.AgvPose2d.newBuilder()
|
||||
Agv.AgvPose2d pose = Agv.AgvPose2d.newBuilder()
|
||||
.setX(vo.getX())
|
||||
.setY(vo.getY())
|
||||
.setTheta(vo.getTheta())
|
||||
@ -197,7 +197,7 @@ public class EdgeAgvController {
|
||||
@ApiOperation("设置速度")
|
||||
@PostMapping("/setVelocity")
|
||||
public AjaxResult setVelocity(@RequestBody @Validated EdgeAgvVelocityVO vo) {
|
||||
AgvUtils.AgvVelocity velocity = AgvUtils.AgvVelocity.newBuilder()
|
||||
Agv.AgvVelocity velocity = Agv.AgvVelocity.newBuilder()
|
||||
.setVx(vo.getVx())
|
||||
.setVy(vo.getVy())
|
||||
.setWz(vo.getWz())
|
||||
@ -242,9 +242,9 @@ public class EdgeAgvController {
|
||||
@ApiParam(value = "设备ID", required = true)
|
||||
@RequestParam("deviceId") String deviceId) {
|
||||
EdgeCommonVO vo = commonVo(robotId, deviceId);
|
||||
List<AgvUtils.AgvStation> stations = edgeAgvService.listStations(vo);
|
||||
List<Agv.AgvStation> stations = edgeAgvService.listStations(vo);
|
||||
List<Map<String, Object>> stationList = new ArrayList<>();
|
||||
for (AgvUtils.AgvStation station : stations) {
|
||||
for (Agv.AgvStation station : stations) {
|
||||
stationList.add(toStationMap(station));
|
||||
}
|
||||
return AjaxResult.ok(stationList);
|
||||
@ -296,7 +296,7 @@ public class EdgeAgvController {
|
||||
@ApiOperation("开始建图")
|
||||
@PostMapping("/startMapping")
|
||||
public AjaxResult startMapping(@RequestBody @Validated EdgeAgvStartMappingVO vo) {
|
||||
AgvUtils.AgvMapDimension mapDimension = AgvUtils.AgvMapDimension.forNumber(vo.getDimension());
|
||||
Agv.AgvMapDimension mapDimension = Agv.AgvMapDimension.forNumber(vo.getDimension());
|
||||
if (mapDimension == null) {
|
||||
return AjaxResult.error("建图维度不合法,允许值:0=未指定,1=2D,2=3D,3=2D+3D");
|
||||
}
|
||||
@ -327,16 +327,16 @@ public class EdgeAgvController {
|
||||
return vo;
|
||||
}
|
||||
|
||||
private AgvUtils.AgvMotionOptions navigationOptions(Integer blockedTimeoutMs) {
|
||||
private Agv.AgvMotionOptions navigationOptions(Integer blockedTimeoutMs) {
|
||||
int timeout = blockedTimeoutMs == null
|
||||
? EdgeAgvService.DEFAULT_BLOCKED_TIMEOUT_MS : blockedTimeoutMs;
|
||||
return AgvUtils.AgvMotionOptions.newBuilder()
|
||||
return Agv.AgvMotionOptions.newBuilder()
|
||||
.setAsynchronous(false)
|
||||
.setBlockedTimeoutMs(timeout)
|
||||
.build();
|
||||
}
|
||||
|
||||
private Map<String, Object> toRuntimeStateMap(AgvUtils.AgvRuntimeState state) {
|
||||
private Map<String, Object> toRuntimeStateMap(Agv.AgvRuntimeState state) {
|
||||
Map<String, Object> resultMap = new HashMap<>();
|
||||
resultMap.put("timestamp", state.getTimestamp());
|
||||
resultMap.put("mode", state.getMode());
|
||||
@ -375,7 +375,7 @@ public class EdgeAgvController {
|
||||
/**
|
||||
* 将 protobuf 站点对象转换为普通 Map。
|
||||
*/
|
||||
private Map<String, Object> toStationMap(AgvUtils.AgvStation station) {
|
||||
private Map<String, Object> toStationMap(Agv.AgvStation station) {
|
||||
Map<String, Object> stationMap = new HashMap<>();
|
||||
stationMap.put("id", station.getId());
|
||||
stationMap.put("type", station.getType());
|
||||
@ -389,7 +389,7 @@ public class EdgeAgvController {
|
||||
/**
|
||||
* 将 protobuf 二维位姿转换为普通 Map。
|
||||
*/
|
||||
private Map<String, Object> toPoseMap(AgvUtils.AgvPose2d pose) {
|
||||
private Map<String, Object> toPoseMap(Agv.AgvPose2d pose) {
|
||||
Map<String, Object> poseMap = new HashMap<>();
|
||||
poseMap.put("x", pose.getX());
|
||||
poseMap.put("y", pose.getY());
|
||||
|
||||
@ -72,6 +72,35 @@ public class EdgeArmController {
|
||||
edgeArmService.executeJsonCommand(request)));
|
||||
}
|
||||
|
||||
@ApiOperation("读取机械臂控制柜数字输入")
|
||||
@GetMapping("/cabinet/digitalInput")
|
||||
public AjaxResult getCabinetDigitalInput(
|
||||
@RequestParam("robotId") String robotId,
|
||||
@RequestParam("deviceId") String deviceId,
|
||||
@RequestParam("index") int index) {
|
||||
return AjaxResult.ok(edgeArmService.getCabinetDigitalInput(commonVo(robotId, deviceId), index));
|
||||
}
|
||||
|
||||
@ApiOperation("读取机械臂控制柜数字输出")
|
||||
@GetMapping("/cabinet/digitalOutput")
|
||||
public AjaxResult getCabinetDigitalOutput(
|
||||
@RequestParam("robotId") String robotId,
|
||||
@RequestParam("deviceId") String deviceId,
|
||||
@RequestParam("index") int index) {
|
||||
return AjaxResult.ok(edgeArmService.getCabinetDigitalOutput(commonVo(robotId, deviceId), index));
|
||||
}
|
||||
|
||||
@ApiOperation("设置机械臂控制柜数字输出")
|
||||
@PostMapping("/cabinet/digitalOutput")
|
||||
public AjaxResult setCabinetDigitalOutput(
|
||||
@RequestParam("robotId") String robotId,
|
||||
@RequestParam("deviceId") String deviceId,
|
||||
@RequestParam("index") int index,
|
||||
@RequestParam("value") boolean value) {
|
||||
return AjaxResult.ok(edgeArmService.setCabinetDigitalOutput(
|
||||
commonVo(robotId, deviceId), index, value));
|
||||
}
|
||||
|
||||
@ApiOperation("关节空间运动")
|
||||
@PostMapping("/moveJ")
|
||||
public AjaxResult moveJ(@RequestBody EdgeArmMoveJVO moveJVO) {
|
||||
|
||||
@ -73,6 +73,12 @@ public class TiProjectController extends BaseController {
|
||||
return AjaxResult.ok(tiProjectService.queryExecuteRunParams(projectId));
|
||||
}
|
||||
|
||||
@ApiOperation("检查触控项目、录像和工作流是否具备执行条件")
|
||||
@GetMapping("/readiness/{projectId}")
|
||||
public AjaxResult readiness(@PathVariable("projectId") String projectId) {
|
||||
return AjaxResult.ok(tiProjectService.checkExecutionReadiness(projectId));
|
||||
}
|
||||
|
||||
@ApiOperation("执行项目")
|
||||
@PostMapping("/execute")
|
||||
public AjaxResult execute(@RequestBody TeTaskExecuteProjectVO taskExecuteProjectVO) {
|
||||
|
||||
@ -21,6 +21,7 @@ import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@ -78,4 +79,11 @@ public class TiVehicleUiController extends BaseController {
|
||||
public AjaxResult findNextClickToFunction(Long vehicleConfigId, String currentUi, String clickFunction) {
|
||||
return AjaxResult.success(exTiVehicleFunctionService.findNextClickToFunction(vehicleConfigId, currentUi, clickFunction));
|
||||
}
|
||||
|
||||
@ApiOperation("查询界面路径拓扑")
|
||||
@PreAuthorize("@ss.hasPermi('ti:ui:list')")
|
||||
@GetMapping("/topology")
|
||||
public AjaxResult topology(@RequestParam Long vehicleConfigId) {
|
||||
return AjaxResult.success(exTiVehicleFunctionService.getTopology(vehicleConfigId));
|
||||
}
|
||||
}
|
||||
|
||||
@ -60,7 +60,7 @@ spring:
|
||||
# 国际化资源文件路径
|
||||
basename: i18n/messages
|
||||
profiles:
|
||||
active: aima
|
||||
active: test
|
||||
# 文件上传
|
||||
servlet:
|
||||
multipart:
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
package com.cmvr.edge.client.manage;
|
||||
|
||||
import cmvr.api.*;
|
||||
import cmvr.api.armteleop.v1.ArmTeleopServiceGrpc;
|
||||
import com.cmvr.common.robot.RobotEndpoint;
|
||||
import com.cmvr.common.robot.RobotEndpointResolver;
|
||||
import io.grpc.ManagedChannel;
|
||||
@ -61,9 +60,6 @@ public class GrpcServiceManager {
|
||||
clientFactories.put(HlcServiceGrpc.HlcServiceBlockingStub.class, new GrpcClientFactory<>(HlcServiceGrpc::newBlockingStub));
|
||||
// 注册agv服务的stub
|
||||
clientFactories.put(AgvServiceGrpc.AgvServiceBlockingStub.class, new GrpcClientFactory<>(AgvServiceGrpc::newBlockingStub));
|
||||
clientFactories.put(MotorServiceGrpc.MotorServiceBlockingStub.class, new GrpcClientFactory<>(MotorServiceGrpc::newBlockingStub));
|
||||
clientFactories.put(MotorServiceGrpc.MotorServiceStub.class, new GrpcClientFactory<>(MotorServiceGrpc::newStub));
|
||||
clientFactories.put(ArmTeleopServiceGrpc.ArmTeleopServiceStub.class, new GrpcClientFactory<>(ArmTeleopServiceGrpc::newStub));
|
||||
// 注册机器人实时语音双向流服务的异步Stub
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
package com.cmvr.edge.client.service;
|
||||
|
||||
import cmvr.msgs.AgvUtils;
|
||||
import cmvr.msgs.AgvUtils.AgvRuntimeState;
|
||||
import cmvr.msgs.Agv;
|
||||
import cmvr.msgs.Agv.AgvRuntimeState;
|
||||
|
||||
import com.cmvr.edge.client.model.EdgeCommonVO;
|
||||
|
||||
@ -31,7 +31,7 @@ public interface EdgeAgvService {
|
||||
* @param edgeCommonVO 边缘通用参数
|
||||
* @return 导航状态
|
||||
*/
|
||||
AgvUtils.AgvNavigationStatus getNavigationStatus(EdgeCommonVO edgeCommonVO);
|
||||
Agv.AgvNavigationStatus getNavigationStatus(EdgeCommonVO edgeCommonVO);
|
||||
|
||||
/**
|
||||
* 紧急停止
|
||||
@ -50,12 +50,12 @@ public interface EdgeAgvService {
|
||||
/**
|
||||
* 在当前地图中重新设置 AGV 定位位姿。
|
||||
*/
|
||||
void relocalize(EdgeCommonVO edgeCommonVO, AgvUtils.AgvPose2d pose);
|
||||
void relocalize(EdgeCommonVO edgeCommonVO, Agv.AgvPose2d pose);
|
||||
|
||||
/**
|
||||
* 按指定速度平移固定距离。
|
||||
*/
|
||||
void translate(EdgeCommonVO edgeCommonVO, AgvUtils.AgvTranslation translation);
|
||||
void translate(EdgeCommonVO edgeCommonVO, Agv.AgvTranslation translation);
|
||||
|
||||
/**
|
||||
* 导航到指定位置
|
||||
@ -63,10 +63,10 @@ public interface EdgeAgvService {
|
||||
* @param edgeCommonVO 边缘通用参数
|
||||
* @param pose 目标位姿
|
||||
*/
|
||||
void navigateToPose(EdgeCommonVO edgeCommonVO, AgvUtils.AgvPose2d pose);
|
||||
void navigateToPose(EdgeCommonVO edgeCommonVO, Agv.AgvPose2d pose);
|
||||
|
||||
void navigateToPose(EdgeCommonVO edgeCommonVO, AgvUtils.AgvPose2d pose,
|
||||
AgvUtils.AgvMotionOptions options);
|
||||
void navigateToPose(EdgeCommonVO edgeCommonVO, Agv.AgvPose2d pose,
|
||||
Agv.AgvMotionOptions options);
|
||||
|
||||
/**
|
||||
* 导航到站点
|
||||
@ -77,7 +77,7 @@ public interface EdgeAgvService {
|
||||
void navigateToStation(EdgeCommonVO edgeCommonVO, String stationId);
|
||||
|
||||
void navigateToStation(EdgeCommonVO edgeCommonVO, String stationId,
|
||||
AgvUtils.AgvMotionOptions options);
|
||||
Agv.AgvMotionOptions options);
|
||||
|
||||
/**
|
||||
* 跟随路径
|
||||
@ -85,10 +85,10 @@ public interface EdgeAgvService {
|
||||
* @param edgeCommonVO 边缘通用参数
|
||||
* @param pathSegments 路径段列表
|
||||
*/
|
||||
void followPath(EdgeCommonVO edgeCommonVO, List<AgvUtils.AgvPathSegment> pathSegments);
|
||||
void followPath(EdgeCommonVO edgeCommonVO, List<Agv.AgvPathSegment> pathSegments);
|
||||
|
||||
void followPath(EdgeCommonVO edgeCommonVO, List<AgvUtils.AgvPathSegment> pathSegments,
|
||||
AgvUtils.AgvMotionOptions options);
|
||||
void followPath(EdgeCommonVO edgeCommonVO, List<Agv.AgvPathSegment> pathSegments,
|
||||
Agv.AgvMotionOptions options);
|
||||
|
||||
/**
|
||||
* 暂停导航
|
||||
@ -117,7 +117,7 @@ public interface EdgeAgvService {
|
||||
* @param edgeCommonVO 边缘通用参数
|
||||
* @param velocity 速度
|
||||
*/
|
||||
void setVelocity(EdgeCommonVO edgeCommonVO, AgvUtils.AgvVelocity velocity);
|
||||
void setVelocity(EdgeCommonVO edgeCommonVO, Agv.AgvVelocity velocity);
|
||||
|
||||
/**
|
||||
* 停止速度控制
|
||||
@ -140,7 +140,7 @@ public interface EdgeAgvService {
|
||||
* @param edgeCommonVO 边缘通用参数
|
||||
* @return 站点列表
|
||||
*/
|
||||
List<AgvUtils.AgvStation> listStations(EdgeCommonVO edgeCommonVO);
|
||||
List<Agv.AgvStation> listStations(EdgeCommonVO edgeCommonVO);
|
||||
|
||||
/**
|
||||
* 切换地图
|
||||
@ -177,7 +177,7 @@ public interface EdgeAgvService {
|
||||
* @param realTime 是否实时建图
|
||||
* @return 会话ID
|
||||
*/
|
||||
String startMapping(EdgeCommonVO edgeCommonVO, AgvUtils.AgvMapDimension dimension, String mapName, boolean realTime);
|
||||
String startMapping(EdgeCommonVO edgeCommonVO, Agv.AgvMapDimension dimension, String mapName, boolean realTime);
|
||||
|
||||
/**
|
||||
* 停止建图
|
||||
|
||||
@ -2,6 +2,7 @@ package com.cmvr.edge.client.service;
|
||||
|
||||
import cmvr.api.ArmCommand;
|
||||
import cmvr.api.Common;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.cmvr.edge.client.model.EdgeCommonVO;
|
||||
import com.cmvr.edge.client.model.system.EdgeSystemJsonCommandVO;
|
||||
|
||||
@ -44,6 +45,13 @@ public interface EdgeArmService {
|
||||
*/
|
||||
Common.JsonDeviceCommand.Feedback executeJsonCommand(EdgeSystemJsonCommandVO request);
|
||||
|
||||
JSONObject getCabinetDigitalInput(EdgeCommonVO edgeCommonVO, int index);
|
||||
|
||||
JSONObject getCabinetDigitalOutput(EdgeCommonVO edgeCommonVO, int index);
|
||||
|
||||
JSONObject setCabinetDigitalOutput(
|
||||
EdgeCommonVO edgeCommonVO, int index, boolean value);
|
||||
|
||||
/**
|
||||
* 关节空间运动(命令码:moveJ)
|
||||
* 控制机械臂通过关节空间插值运动到目标关节位置
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
package com.cmvr.edge.client.service;
|
||||
|
||||
import cmvr.api.armteleop.v1.ArmTeleopV1;
|
||||
import com.cmvr.edge.client.model.armteleop.EdgeArmTeleopHeartbeatVO;
|
||||
import com.cmvr.edge.client.model.armteleop.EdgeArmTeleopOpenVO;
|
||||
import com.cmvr.edge.client.model.armteleop.EdgeArmTeleopSetpointVO;
|
||||
@ -13,7 +12,7 @@ public interface EdgeArmTeleopService {
|
||||
|
||||
void heartbeat(EdgeArmTeleopHeartbeatVO request);
|
||||
|
||||
ArmTeleopV1.ServerFrame getStatus(String sessionId);
|
||||
Object getStatus(String sessionId);
|
||||
|
||||
void stop(EdgeArmTeleopStopVO request);
|
||||
}
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
package com.cmvr.edge.client.service;
|
||||
|
||||
import cmvr.api.MotorCommand;
|
||||
import com.cmvr.edge.client.model.motor.EdgeMotorCyclicOpenVO;
|
||||
import com.cmvr.edge.client.model.motor.EdgeMotorCyclicSetpointVO;
|
||||
import com.cmvr.edge.client.model.motor.EdgeMotorEnabledVO;
|
||||
@ -8,19 +7,19 @@ import com.cmvr.edge.client.model.motor.EdgeMotorMotionVO;
|
||||
import com.cmvr.edge.client.model.motor.EdgeMotorTargetVO;
|
||||
|
||||
public interface EdgeMotorService {
|
||||
MotorCommand.MotorCommandResponse setZero(EdgeMotorTargetVO request);
|
||||
Object setZero(EdgeMotorTargetVO request);
|
||||
|
||||
MotorCommand.MotorCommandResponse moveToZero(EdgeMotorMotionVO request);
|
||||
Object moveToZero(EdgeMotorMotionVO request);
|
||||
|
||||
MotorCommand.MotorCommandResponse profilePosition(EdgeMotorMotionVO request);
|
||||
Object profilePosition(EdgeMotorMotionVO request);
|
||||
|
||||
MotorCommand.MotorCommandResponse profileVelocity(EdgeMotorMotionVO request);
|
||||
Object profileVelocity(EdgeMotorMotionVO request);
|
||||
|
||||
MotorCommand.MotorCommandResponse emergencyStop(EdgeMotorTargetVO request);
|
||||
Object emergencyStop(EdgeMotorTargetVO request);
|
||||
|
||||
MotorCommand.GetMotorStatusResponse getStatus(EdgeMotorTargetVO request);
|
||||
Object getStatus(EdgeMotorTargetVO request);
|
||||
|
||||
MotorCommand.MotorCommandResponse setEnabled(EdgeMotorEnabledVO request);
|
||||
Object setEnabled(EdgeMotorEnabledVO request);
|
||||
|
||||
String openCyclicPosition(EdgeMotorCyclicOpenVO request);
|
||||
|
||||
@ -30,7 +29,7 @@ public interface EdgeMotorService {
|
||||
|
||||
void sendCyclicVelocity(EdgeMotorCyclicSetpointVO request);
|
||||
|
||||
MotorCommand.CyclicControlResponse getStreamStatus(String sessionId);
|
||||
Object getStreamStatus(String sessionId);
|
||||
|
||||
void closeStream(String sessionId);
|
||||
}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
package com.cmvr.edge.client.service;
|
||||
|
||||
import cmvr.api.Common;
|
||||
import cmvr.api.SafetyCommand;
|
||||
import cmvr.api.SystemCommand;
|
||||
import com.cmvr.edge.client.model.system.EdgeSystemJsonCommandVO;
|
||||
import com.cmvr.edge.client.model.system.EdgeSystemUpdateParamsVO;
|
||||
@ -26,16 +25,16 @@ public interface EdgeSystemService {
|
||||
|
||||
SystemCommand.ActionQueueCommand.Feedback executeActionQueue(EdgeActionQueueVO request);
|
||||
|
||||
SafetyCommand.GetSafetyStateCommand.Feedback getSafetyState(String robotId);
|
||||
Object getSafetyState(String robotId);
|
||||
|
||||
SafetyCommand.RestoreOperationalStateCommand.Feedback restoreOperationalState(
|
||||
Object restoreOperationalState(
|
||||
String robotId, String reason, Integer timeoutMs);
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link #restoreOperationalState(String, String, Integer)}.
|
||||
*/
|
||||
@Deprecated
|
||||
SafetyCommand.RecoverSafetyStateCommand.Feedback recoverSafetyState(
|
||||
Object recoverSafetyState(
|
||||
String robotId, String reason, Integer timeoutMs);
|
||||
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@ package com.cmvr.edge.client.service.impl;
|
||||
import cmvr.api.AgvCommand;
|
||||
import cmvr.api.AgvServiceGrpc;
|
||||
import cmvr.api.Common;
|
||||
import cmvr.msgs.AgvUtils;
|
||||
import cmvr.msgs.Agv;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.cmvr.common.exception.GlobalException;
|
||||
import com.cmvr.edge.client.manage.GrpcServiceManager;
|
||||
@ -30,7 +30,7 @@ public class EdgeAgvServiceImpl implements EdgeAgvService {
|
||||
private final GrpcServiceManager grpcServiceManager;
|
||||
|
||||
@Override
|
||||
public AgvUtils.AgvRuntimeState getRuntimeState(EdgeCommonVO edgeCommonVO) {
|
||||
public Agv.AgvRuntimeState getRuntimeState(EdgeCommonVO edgeCommonVO) {
|
||||
AgvServiceGrpc.AgvServiceBlockingStub stub = grpcServiceManager.getGrpcClient(
|
||||
edgeCommonVO.getRobotId(), AgvServiceGrpc.AgvServiceBlockingStub.class);
|
||||
AgvCommand.AgvRuntimeStateCommand.Request request = AgvCommand.AgvRuntimeStateCommand.Request.newBuilder()
|
||||
@ -40,7 +40,7 @@ public class EdgeAgvServiceImpl implements EdgeAgvService {
|
||||
}
|
||||
|
||||
@Override
|
||||
public AgvUtils.AgvNavigationStatus getNavigationStatus(EdgeCommonVO edgeCommonVO) {
|
||||
public Agv.AgvNavigationStatus getNavigationStatus(EdgeCommonVO edgeCommonVO) {
|
||||
AgvServiceGrpc.AgvServiceBlockingStub stub = grpcServiceManager.getGrpcClient(
|
||||
edgeCommonVO.getRobotId(), AgvServiceGrpc.AgvServiceBlockingStub.class);
|
||||
AgvCommand.AgvNavigationStatusCommand.Request request = AgvCommand.AgvNavigationStatusCommand.Request.newBuilder()
|
||||
@ -66,7 +66,7 @@ public class EdgeAgvServiceImpl implements EdgeAgvService {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void relocalize(EdgeCommonVO edgeCommonVO, AgvUtils.AgvPose2d pose) {
|
||||
public void relocalize(EdgeCommonVO edgeCommonVO, Agv.AgvPose2d pose) {
|
||||
if (pose == null) {
|
||||
throw new GlobalException("重定位位姿不能为空");
|
||||
}
|
||||
@ -80,7 +80,7 @@ public class EdgeAgvServiceImpl implements EdgeAgvService {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void translate(EdgeCommonVO edgeCommonVO, AgvUtils.AgvTranslation translation) {
|
||||
public void translate(EdgeCommonVO edgeCommonVO, Agv.AgvTranslation translation) {
|
||||
if (translation == null || translation.getDistance() <= 0) {
|
||||
throw new GlobalException("平移距离必须大于0");
|
||||
}
|
||||
@ -94,13 +94,13 @@ public class EdgeAgvServiceImpl implements EdgeAgvService {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void navigateToPose(EdgeCommonVO edgeCommonVO, AgvUtils.AgvPose2d pose) {
|
||||
public void navigateToPose(EdgeCommonVO edgeCommonVO, Agv.AgvPose2d pose) {
|
||||
navigateToPose(edgeCommonVO, pose, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void navigateToPose(EdgeCommonVO edgeCommonVO, AgvUtils.AgvPose2d pose,
|
||||
AgvUtils.AgvMotionOptions options) {
|
||||
public void navigateToPose(EdgeCommonVO edgeCommonVO, Agv.AgvPose2d pose,
|
||||
Agv.AgvMotionOptions options) {
|
||||
if (pose == null) {
|
||||
throw new GlobalException("目标位姿不能为空");
|
||||
}
|
||||
@ -122,7 +122,7 @@ public class EdgeAgvServiceImpl implements EdgeAgvService {
|
||||
|
||||
@Override
|
||||
public void navigateToStation(EdgeCommonVO edgeCommonVO, String stationId,
|
||||
AgvUtils.AgvMotionOptions options) {
|
||||
Agv.AgvMotionOptions options) {
|
||||
if (StrUtil.isBlank(stationId)) {
|
||||
throw new GlobalException("站点ID不能为空");
|
||||
}
|
||||
@ -138,13 +138,13 @@ public class EdgeAgvServiceImpl implements EdgeAgvService {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void followPath(EdgeCommonVO edgeCommonVO, List<AgvUtils.AgvPathSegment> pathSegments) {
|
||||
public void followPath(EdgeCommonVO edgeCommonVO, List<Agv.AgvPathSegment> pathSegments) {
|
||||
followPath(edgeCommonVO, pathSegments, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void followPath(EdgeCommonVO edgeCommonVO, List<AgvUtils.AgvPathSegment> pathSegments,
|
||||
AgvUtils.AgvMotionOptions options) {
|
||||
public void followPath(EdgeCommonVO edgeCommonVO, List<Agv.AgvPathSegment> pathSegments,
|
||||
Agv.AgvMotionOptions options) {
|
||||
if (pathSegments == null || pathSegments.isEmpty()) {
|
||||
throw new GlobalException("路径段不能为空");
|
||||
}
|
||||
@ -184,7 +184,7 @@ public class EdgeAgvServiceImpl implements EdgeAgvService {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setVelocity(EdgeCommonVO edgeCommonVO, AgvUtils.AgvVelocity velocity) {
|
||||
public void setVelocity(EdgeCommonVO edgeCommonVO, Agv.AgvVelocity velocity) {
|
||||
if (velocity == null) {
|
||||
throw new GlobalException("速度不能为空");
|
||||
}
|
||||
@ -217,7 +217,7 @@ public class EdgeAgvServiceImpl implements EdgeAgvService {
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AgvUtils.AgvStation> listStations(EdgeCommonVO edgeCommonVO) {
|
||||
public List<Agv.AgvStation> listStations(EdgeCommonVO edgeCommonVO) {
|
||||
AgvServiceGrpc.AgvServiceBlockingStub stub = grpcServiceManager.getGrpcClient(
|
||||
edgeCommonVO.getRobotId(), AgvServiceGrpc.AgvServiceBlockingStub.class);
|
||||
AgvCommand.AgvListStationsCommand.Request request = AgvCommand.AgvListStationsCommand.Request.newBuilder()
|
||||
@ -276,12 +276,12 @@ public class EdgeAgvServiceImpl implements EdgeAgvService {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String startMapping(EdgeCommonVO edgeCommonVO, AgvUtils.AgvMapDimension dimension, String mapName, boolean realTime) {
|
||||
public String startMapping(EdgeCommonVO edgeCommonVO, Agv.AgvMapDimension dimension, String mapName, boolean realTime) {
|
||||
AgvServiceGrpc.AgvServiceBlockingStub stub = grpcServiceManager.getGrpcClient(
|
||||
edgeCommonVO.getRobotId(), AgvServiceGrpc.AgvServiceBlockingStub.class);
|
||||
AgvCommand.AgvStartMappingCommand.Request.Builder requestBuilder = AgvCommand.AgvStartMappingCommand.Request.newBuilder()
|
||||
.setHeader(EdgeCommonUtil.buildRequest(edgeCommonVO.getDeviceId()))
|
||||
.setDimension(dimension != null ? dimension : AgvUtils.AgvMapDimension.AGV_MAP_DIMENSION_UNSPECIFIED)
|
||||
.setDimension(dimension != null ? dimension : Agv.AgvMapDimension.AGV_MAP_DIMENSION_UNSPECIFIED)
|
||||
.setRealTime(realTime);
|
||||
|
||||
if (StrUtil.isNotBlank(mapName)) {
|
||||
@ -300,9 +300,9 @@ public class EdgeAgvServiceImpl implements EdgeAgvService {
|
||||
executeGrpcCall(() -> stub.stopMapping(request));
|
||||
}
|
||||
|
||||
AgvUtils.AgvMotionOptions normalizeNavigationOptions(AgvUtils.AgvMotionOptions options) {
|
||||
AgvUtils.AgvMotionOptions.Builder builder = options == null
|
||||
? AgvUtils.AgvMotionOptions.newBuilder() : options.toBuilder();
|
||||
Agv.AgvMotionOptions normalizeNavigationOptions(Agv.AgvMotionOptions options) {
|
||||
Agv.AgvMotionOptions.Builder builder = options == null
|
||||
? Agv.AgvMotionOptions.newBuilder() : options.toBuilder();
|
||||
if (builder.getBlockedTimeoutMs() <= 0) {
|
||||
builder.setBlockedTimeoutMs(EdgeAgvService.DEFAULT_BLOCKED_TIMEOUT_MS);
|
||||
}
|
||||
|
||||
@ -4,6 +4,9 @@ import cmvr.api.ArmCommand;
|
||||
import cmvr.api.ArmServiceGrpc;
|
||||
import cmvr.api.Common;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONException;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.cmvr.common.exception.GlobalException;
|
||||
import com.cmvr.edge.client.manage.GrpcServiceManager;
|
||||
import com.cmvr.edge.client.model.EdgeCommonVO;
|
||||
@ -55,13 +58,33 @@ public class EdgeArmServiceImpl implements EdgeArmService {
|
||||
|
||||
@Override
|
||||
public Common.JsonDeviceCommand.Feedback executeJsonCommand(EdgeSystemJsonCommandVO request) {
|
||||
ArmServiceGrpc.ArmServiceBlockingStub stub = grpcServiceManager.getGrpcClient(
|
||||
request.getRobotId(), ArmServiceGrpc.ArmServiceBlockingStub.class);
|
||||
Common.JsonDeviceCommand.Request grpcRequest = Common.JsonDeviceCommand.Request.newBuilder()
|
||||
.setHeader(EdgeCommonUtil.buildRequest(request.getDeviceId()))
|
||||
.setRequestJson(request.getRequestJson() == null ? "" : request.getRequestJson())
|
||||
.build();
|
||||
return executeGrpcCall(() -> stub.executeJsonCommand(grpcRequest));
|
||||
if (request == null || StrUtil.hasBlank(
|
||||
request.getRobotId(), request.getDeviceId(), request.getRequestJson())) {
|
||||
throw new GlobalException("robotId、deviceId 和 JSON 指令不能为空");
|
||||
}
|
||||
return executeJsonCommand(
|
||||
request.getRobotId(), request.getDeviceId(), request.getRequestJson());
|
||||
}
|
||||
|
||||
@Override
|
||||
public JSONObject getCabinetDigitalInput(
|
||||
EdgeCommonVO edgeCommonVO, int index) {
|
||||
validateCabinetIndex(index);
|
||||
return executeCabinetIo(edgeCommonVO, "get_di", index, null, "读取控制柜数字输入");
|
||||
}
|
||||
|
||||
@Override
|
||||
public JSONObject getCabinetDigitalOutput(
|
||||
EdgeCommonVO edgeCommonVO, int index) {
|
||||
validateCabinetIndex(index);
|
||||
return executeCabinetIo(edgeCommonVO, "get_do", index, null, "读取控制柜数字输出");
|
||||
}
|
||||
|
||||
@Override
|
||||
public JSONObject setCabinetDigitalOutput(
|
||||
EdgeCommonVO edgeCommonVO, int index, boolean value) {
|
||||
validateCabinetIndex(index);
|
||||
return executeCabinetIo(edgeCommonVO, "set_do", index, value, "设置控制柜数字输出");
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -345,6 +368,87 @@ public class EdgeArmServiceImpl implements EdgeArmService {
|
||||
return executeGrpcCall(() -> stub.computeForwardKinematics(requestBuilder.build()));
|
||||
}
|
||||
|
||||
private ArmServiceGrpc.ArmServiceBlockingStub blockingStub(String robotId) {
|
||||
if (StrUtil.isBlank(robotId)) {
|
||||
throw new GlobalException("机器人ID不能为空");
|
||||
}
|
||||
return grpcServiceManager.getGrpcClient(robotId, ArmServiceGrpc.ArmServiceBlockingStub.class);
|
||||
}
|
||||
|
||||
private Common.JsonDeviceCommand.Feedback executeJsonCommand(
|
||||
String robotId, String deviceId, String requestJson) {
|
||||
if (StrUtil.hasBlank(robotId, deviceId)) {
|
||||
throw new GlobalException("机器人ID和设备ID不能为空");
|
||||
}
|
||||
Common.JsonDeviceCommand.Request grpcRequest = Common.JsonDeviceCommand.Request.newBuilder()
|
||||
.setHeader(EdgeCommonUtil.buildRequest(deviceId))
|
||||
.setRequestJson(requestJson)
|
||||
.build();
|
||||
return executeGrpcCall(() -> blockingStub(robotId).executeJsonCommand(grpcRequest));
|
||||
}
|
||||
|
||||
private JSONObject executeCabinetIo(
|
||||
EdgeCommonVO edgeCommonVO,
|
||||
String operation,
|
||||
int index,
|
||||
Boolean value,
|
||||
String operationName) {
|
||||
if (edgeCommonVO == null || StrUtil.hasBlank(
|
||||
edgeCommonVO.getRobotId(), edgeCommonVO.getDeviceId())) {
|
||||
throw new GlobalException("机器人ID和设备ID不能为空");
|
||||
}
|
||||
JSONObject command = buildCabinetIoCommand(operation, index, value);
|
||||
|
||||
Common.JsonDeviceCommand.Feedback feedback = executeJsonCommand(
|
||||
edgeCommonVO.getRobotId(), edgeCommonVO.getDeviceId(), command.toJSONString());
|
||||
JSONObject response = parseJsonResponse(feedback.getResponseJson(), operationName);
|
||||
if (!feedback.hasHeader() || !feedback.getHeader().getSuccess()) {
|
||||
String detail = response.getString("error_message");
|
||||
if (StrUtil.isBlank(detail) && feedback.hasHeader()) {
|
||||
detail = feedback.getHeader().getErrorMessage();
|
||||
}
|
||||
throw new GlobalException(operationName + "失败: "
|
||||
+ (StrUtil.isBlank(detail) ? "边端返回失败" : detail));
|
||||
}
|
||||
if (!response.getBooleanValue("success")) {
|
||||
throw new GlobalException(operationName + "失败: "
|
||||
+ response.getString("error_message"));
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
static JSONObject buildCabinetIoCommand(String operation, int index, Boolean value) {
|
||||
JSONObject command = new JSONObject();
|
||||
command.put("command", "cabinet_io");
|
||||
command.put("operation", operation);
|
||||
command.put("index", index);
|
||||
if (value != null) {
|
||||
command.put("value", value);
|
||||
}
|
||||
return command;
|
||||
}
|
||||
|
||||
private JSONObject parseJsonResponse(String responseJson, String operationName) {
|
||||
if (StrUtil.isBlank(responseJson)) {
|
||||
throw new GlobalException(operationName + "失败: 边端响应为空");
|
||||
}
|
||||
try {
|
||||
JSONObject response = JSON.parseObject(responseJson);
|
||||
if (response == null) {
|
||||
throw new GlobalException(operationName + "失败: 边端响应为空");
|
||||
}
|
||||
return response;
|
||||
} catch (JSONException exception) {
|
||||
throw new GlobalException(operationName + "失败: 边端响应不是有效 JSON");
|
||||
}
|
||||
}
|
||||
|
||||
private void validateCabinetIndex(int index) {
|
||||
if (index < 0) {
|
||||
throw new GlobalException("控制柜 IO 通道索引不能小于 0");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析坐标系类型
|
||||
*
|
||||
|
||||
@ -1,100 +1,49 @@
|
||||
package com.cmvr.edge.client.service.impl;
|
||||
|
||||
import cmvr.api.armteleop.v1.ArmTeleopServiceGrpc;
|
||||
import cmvr.api.armteleop.v1.ArmTeleopV1;
|
||||
import com.cmvr.common.exception.GlobalException;
|
||||
import com.cmvr.edge.client.manage.GrpcBidiSessionRegistry;
|
||||
import com.cmvr.edge.client.manage.GrpcServiceManager;
|
||||
import com.cmvr.edge.client.model.armteleop.EdgeArmTeleopHeartbeatVO;
|
||||
import com.cmvr.edge.client.model.armteleop.EdgeArmTeleopOpenVO;
|
||||
import com.cmvr.edge.client.model.armteleop.EdgeArmTeleopSetpointVO;
|
||||
import com.cmvr.edge.client.model.armteleop.EdgeArmTeleopStopVO;
|
||||
import com.cmvr.edge.client.service.EdgeArmTeleopService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* Compatibility facade retained for callers of the removed arm teleoperation API.
|
||||
* The current edge protocol only exposes unary arm commands such as servoJ.
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class EdgeArmTeleopServiceImpl implements EdgeArmTeleopService {
|
||||
|
||||
private final GrpcServiceManager grpcServiceManager;
|
||||
private final GrpcBidiSessionRegistry<ArmTeleopV1.ClientFrame,
|
||||
ArmTeleopV1.ServerFrame> sessions = new GrpcBidiSessionRegistry<>();
|
||||
private static final String UNSUPPORTED =
|
||||
"当前边端协议已移除机械臂 Teleop 流,请改用 /api/arm/servoJ 和 /api/arm/stopMotion";
|
||||
|
||||
@Override
|
||||
public String open(EdgeArmTeleopOpenVO request) {
|
||||
if (request.getRobotId() == null || request.getRobotId().trim().isEmpty()) {
|
||||
throw new GlobalException("robotId cannot be empty");
|
||||
}
|
||||
ArmTeleopV1.RobotManifest.Builder manifest = ArmTeleopV1.RobotManifest.newBuilder();
|
||||
if (request.getRobotId() != null) manifest.setRobotId(request.getRobotId());
|
||||
if (request.getModelSha256() != null) manifest.setModelSha256(request.getModelSha256());
|
||||
if (request.getCalibrationSha256() != null) manifest.setCalibrationSha256(request.getCalibrationSha256());
|
||||
if (request.getJointNames() != null) manifest.addAllJointNames(request.getJointNames());
|
||||
if (request.getPositionUnit() != null) manifest.setPositionUnit(request.getPositionUnit());
|
||||
if (request.getVelocityUnit() != null) manifest.setVelocityUnit(request.getVelocityUnit());
|
||||
if (request.getEffortUnit() != null) manifest.setEffortUnit(request.getEffortUnit());
|
||||
if (request.getBaseFrame() != null) manifest.setBaseFrame(request.getBaseFrame());
|
||||
if (request.getToolFrame() != null) manifest.setToolFrame(request.getToolFrame());
|
||||
|
||||
ArmTeleopV1.OpenSession.Builder open = ArmTeleopV1.OpenSession.newBuilder()
|
||||
.setExpectedRobot(manifest)
|
||||
.setRequestForceFeedback(request.isForceFeedback());
|
||||
if (request.getProtocolMajor() != null) open.setProtocolMajor(request.getProtocolMajor());
|
||||
if (request.getProtocolMinor() != null) open.setProtocolMinor(request.getProtocolMinor());
|
||||
if (request.getClientInstanceId() != null) open.setClientInstanceId(request.getClientInstanceId());
|
||||
if (request.getCommandRateHz() != null) open.setRequestedCommandRateHz(request.getCommandRateHz());
|
||||
if (request.getStateRateHz() != null) open.setRequestedStateRateHz(request.getStateRateHz());
|
||||
if (request.getWatchdogTimeoutMs() != null) open.setWatchdogTimeoutMs(request.getWatchdogTimeoutMs());
|
||||
if (request.getLeaseMs() != null) open.setRequestedLeaseMs(request.getLeaseMs());
|
||||
|
||||
ArmTeleopServiceGrpc.ArmTeleopServiceStub stub = grpcServiceManager.getGrpcClient(
|
||||
request.getRobotId(), ArmTeleopServiceGrpc.ArmTeleopServiceStub.class);
|
||||
return sessions.open(stub::teleoperate, ArmTeleopV1.ClientFrame.newBuilder().setOpen(open).build());
|
||||
throw unsupported();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendSetpoint(EdgeArmTeleopSetpointVO request) {
|
||||
if (request.getSequence() <= 0) throw new GlobalException("sequence must be greater than zero");
|
||||
if (request.getValidForUs() == null || request.getValidForUs() <= 0) {
|
||||
throw new GlobalException("validForUs must be greater than zero");
|
||||
}
|
||||
ArmTeleopV1.JointSetpoint.Builder setpoint = ArmTeleopV1.JointSetpoint.newBuilder()
|
||||
.setSequence(request.getSequence()).setValidForUs(request.getValidForUs());
|
||||
if (request.getPositionRad() != null) {
|
||||
for (double value : request.getPositionRad()) setpoint.addPositionRad(value);
|
||||
}
|
||||
if (request.getVelocityRadS() != null) {
|
||||
for (double value : request.getVelocityRadS()) setpoint.addVelocityRadS(value);
|
||||
}
|
||||
sessions.send(request.getSessionId(), ArmTeleopV1.ClientFrame.newBuilder().setSetpoint(setpoint).build());
|
||||
throw unsupported();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void heartbeat(EdgeArmTeleopHeartbeatVO request) {
|
||||
ArmTeleopV1.ClientHeartbeat heartbeat = ArmTeleopV1.ClientHeartbeat.newBuilder()
|
||||
.setSequence(request.getSequence()).build();
|
||||
sessions.send(request.getSessionId(), ArmTeleopV1.ClientFrame.newBuilder().setHeartbeat(heartbeat).build());
|
||||
throw unsupported();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ArmTeleopV1.ServerFrame getStatus(String sessionId) {
|
||||
return sessions.getLatest(sessionId);
|
||||
public Object getStatus(String sessionId) {
|
||||
throw unsupported();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop(EdgeArmTeleopStopVO request) {
|
||||
ArmTeleopV1.StopReason reason;
|
||||
try {
|
||||
reason = request.getReason() == null || request.getReason().trim().isEmpty()
|
||||
? ArmTeleopV1.StopReason.STOP_REASON_OPERATOR_REQUEST
|
||||
: ArmTeleopV1.StopReason.valueOf(request.getReason().trim().toUpperCase());
|
||||
} catch (IllegalArgumentException exception) {
|
||||
throw new GlobalException("Invalid teleoperation stop reason");
|
||||
throw unsupported();
|
||||
}
|
||||
ArmTeleopV1.StopSession stop = ArmTeleopV1.StopSession.newBuilder()
|
||||
.setReason(reason).setDetail(request.getDetail() == null ? "" : request.getDetail()).build();
|
||||
sessions.send(request.getSessionId(), ArmTeleopV1.ClientFrame.newBuilder().setStop(stop).build());
|
||||
sessions.close(request.getSessionId());
|
||||
|
||||
private GlobalException unsupported() {
|
||||
return new GlobalException(UNSUPPORTED);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,189 +1,36 @@
|
||||
package com.cmvr.edge.client.service.impl;
|
||||
|
||||
import cmvr.api.MotorCommand;
|
||||
import cmvr.api.MotorServiceGrpc;
|
||||
import com.cmvr.common.exception.GlobalException;
|
||||
import com.cmvr.edge.client.manage.GrpcBidiSessionRegistry;
|
||||
import com.cmvr.edge.client.manage.GrpcServiceManager;
|
||||
import com.cmvr.edge.client.model.motor.EdgeMotorCyclicOpenVO;
|
||||
import com.cmvr.edge.client.model.motor.EdgeMotorCyclicSetpointVO;
|
||||
import com.cmvr.edge.client.model.motor.EdgeMotorEnabledVO;
|
||||
import com.cmvr.edge.client.model.motor.EdgeMotorMotionVO;
|
||||
import com.cmvr.edge.client.model.motor.EdgeMotorTargetVO;
|
||||
import com.cmvr.edge.client.service.EdgeMotorService;
|
||||
import com.cmvr.edge.client.utils.EdgeCommonUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
|
||||
/** Compatibility facade for the MotorService removed from the current edge protocol. */
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class EdgeMotorServiceImpl implements EdgeMotorService {
|
||||
|
||||
private final GrpcServiceManager grpcServiceManager;
|
||||
private final GrpcBidiSessionRegistry<MotorCommand.CyclicPositionRequest,
|
||||
MotorCommand.CyclicControlResponse> positionStreams = new GrpcBidiSessionRegistry<>();
|
||||
private final GrpcBidiSessionRegistry<MotorCommand.CyclicVelocityRequest,
|
||||
MotorCommand.CyclicControlResponse> velocityStreams = new GrpcBidiSessionRegistry<>();
|
||||
private final ConcurrentMap<String, Boolean> streamTypes = new ConcurrentHashMap<>();
|
||||
private static final String UNSUPPORTED =
|
||||
"当前边端协议未提供 MotorService,无法执行独立电机控制";
|
||||
|
||||
@Override
|
||||
public MotorCommand.MotorCommandResponse setZero(EdgeMotorTargetVO request) {
|
||||
return blockingStub(request.getRobotId()).setZero(MotorCommand.SetMotorZeroRequest.newBuilder()
|
||||
.setTarget(buildTarget(request)).build());
|
||||
}
|
||||
@Override public Object setZero(EdgeMotorTargetVO request) { throw unsupported(); }
|
||||
@Override public Object moveToZero(EdgeMotorMotionVO request) { throw unsupported(); }
|
||||
@Override public Object profilePosition(EdgeMotorMotionVO request) { throw unsupported(); }
|
||||
@Override public Object profileVelocity(EdgeMotorMotionVO request) { throw unsupported(); }
|
||||
@Override public Object emergencyStop(EdgeMotorTargetVO request) { throw unsupported(); }
|
||||
@Override public Object getStatus(EdgeMotorTargetVO request) { throw unsupported(); }
|
||||
@Override public Object setEnabled(EdgeMotorEnabledVO request) { throw unsupported(); }
|
||||
@Override public String openCyclicPosition(EdgeMotorCyclicOpenVO request) { throw unsupported(); }
|
||||
@Override public void sendCyclicPosition(EdgeMotorCyclicSetpointVO request) { throw unsupported(); }
|
||||
@Override public String openCyclicVelocity(EdgeMotorCyclicOpenVO request) { throw unsupported(); }
|
||||
@Override public void sendCyclicVelocity(EdgeMotorCyclicSetpointVO request) { throw unsupported(); }
|
||||
@Override public Object getStreamStatus(String sessionId) { throw unsupported(); }
|
||||
@Override public void closeStream(String sessionId) { throw unsupported(); }
|
||||
|
||||
@Override
|
||||
public MotorCommand.MotorCommandResponse moveToZero(EdgeMotorMotionVO request) {
|
||||
MotorCommand.MoveMotorToZeroRequest.Builder builder = MotorCommand.MoveMotorToZeroRequest.newBuilder()
|
||||
.setTarget(buildTarget(request)).setWait(buildWait(request));
|
||||
if (request.getMaxVelocityRadS() != null) builder.setMaxVelocityRadS(request.getMaxVelocityRadS());
|
||||
if (request.getAccelerationRadS2() != null) builder.setAccelerationRadS2(request.getAccelerationRadS2());
|
||||
return blockingStub(request.getRobotId()).moveToZero(builder.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public MotorCommand.MotorCommandResponse profilePosition(EdgeMotorMotionVO request) {
|
||||
if (request.getTargetPositionRad() == null) {
|
||||
throw new GlobalException("targetPositionRad cannot be empty");
|
||||
}
|
||||
MotorCommand.ProfilePositionRequest.Builder builder = MotorCommand.ProfilePositionRequest.newBuilder()
|
||||
.setTarget(buildTarget(request))
|
||||
.setTargetPositionRad(request.getTargetPositionRad())
|
||||
.setWait(buildWait(request));
|
||||
if (request.getMaxVelocityRadS() != null) builder.setMaxVelocityRadS(request.getMaxVelocityRadS());
|
||||
if (request.getAccelerationRadS2() != null) builder.setAccelerationRadS2(request.getAccelerationRadS2());
|
||||
return blockingStub(request.getRobotId()).profilePosition(builder.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public MotorCommand.MotorCommandResponse profileVelocity(EdgeMotorMotionVO request) {
|
||||
if (request.getTargetVelocityRadS() == null) {
|
||||
throw new GlobalException("targetVelocityRadS cannot be empty");
|
||||
}
|
||||
MotorCommand.ProfileVelocityRequest.Builder builder = MotorCommand.ProfileVelocityRequest.newBuilder()
|
||||
.setTarget(buildTarget(request))
|
||||
.setTargetVelocityRadS(request.getTargetVelocityRadS())
|
||||
.setWait(buildWait(request));
|
||||
if (request.getAccelerationRadS2() != null) builder.setAccelerationRadS2(request.getAccelerationRadS2());
|
||||
return blockingStub(request.getRobotId()).profileVelocity(builder.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public MotorCommand.MotorCommandResponse emergencyStop(EdgeMotorTargetVO request) {
|
||||
return blockingStub(request.getRobotId()).emergencyStop(MotorCommand.EmergencyStopRequest.newBuilder()
|
||||
.setTarget(buildTarget(request)).build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public MotorCommand.GetMotorStatusResponse getStatus(EdgeMotorTargetVO request) {
|
||||
return blockingStub(request.getRobotId()).getStatus(MotorCommand.GetMotorStatusRequest.newBuilder()
|
||||
.setTarget(buildTarget(request)).build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public MotorCommand.MotorCommandResponse setEnabled(EdgeMotorEnabledVO request) {
|
||||
return blockingStub(request.getRobotId()).setEnabled(MotorCommand.SetMotorEnabledRequest.newBuilder()
|
||||
.setTarget(buildTarget(request)).setEnabled(request.isEnabled()).build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String openCyclicPosition(EdgeMotorCyclicOpenVO request) {
|
||||
MotorServiceGrpc.MotorServiceStub stub = asyncStub(request.getRobotId());
|
||||
MotorCommand.CyclicPositionRequest open = MotorCommand.CyclicPositionRequest.newBuilder()
|
||||
.setOpen(buildOpen(request)).build();
|
||||
String sessionId = positionStreams.open(stub::streamCyclicPosition, open);
|
||||
streamTypes.put(sessionId, Boolean.TRUE);
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendCyclicPosition(EdgeMotorCyclicSetpointVO request) {
|
||||
if (request.getTargetPositionRad() == null) {
|
||||
throw new GlobalException("targetPositionRad cannot be empty");
|
||||
}
|
||||
MotorCommand.CyclicPositionSetpoint.Builder setpoint = MotorCommand.CyclicPositionSetpoint.newBuilder()
|
||||
.setSequence(request.getSequence()).setTargetPositionRad(request.getTargetPositionRad());
|
||||
if (request.getTargetVelocityRadS() != null) {
|
||||
setpoint.setTargetVelocityRadS(request.getTargetVelocityRadS());
|
||||
}
|
||||
positionStreams.send(request.getSessionId(), MotorCommand.CyclicPositionRequest.newBuilder()
|
||||
.setSetpoint(setpoint).build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String openCyclicVelocity(EdgeMotorCyclicOpenVO request) {
|
||||
MotorServiceGrpc.MotorServiceStub stub = asyncStub(request.getRobotId());
|
||||
MotorCommand.CyclicVelocityRequest open = MotorCommand.CyclicVelocityRequest.newBuilder()
|
||||
.setOpen(buildOpen(request)).build();
|
||||
String sessionId = velocityStreams.open(stub::streamCyclicVelocity, open);
|
||||
streamTypes.put(sessionId, Boolean.FALSE);
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendCyclicVelocity(EdgeMotorCyclicSetpointVO request) {
|
||||
if (request.getTargetVelocityRadS() == null) {
|
||||
throw new GlobalException("targetVelocityRadS cannot be empty");
|
||||
}
|
||||
MotorCommand.CyclicVelocitySetpoint setpoint = MotorCommand.CyclicVelocitySetpoint.newBuilder()
|
||||
.setSequence(request.getSequence()).setTargetVelocityRadS(request.getTargetVelocityRadS()).build();
|
||||
velocityStreams.send(request.getSessionId(), MotorCommand.CyclicVelocityRequest.newBuilder()
|
||||
.setSetpoint(setpoint).build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public MotorCommand.CyclicControlResponse getStreamStatus(String sessionId) {
|
||||
Boolean position = streamTypes.get(sessionId);
|
||||
if (position == null) throw new GlobalException("Unknown stream session: " + sessionId);
|
||||
return position ? positionStreams.getLatest(sessionId) : velocityStreams.getLatest(sessionId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void closeStream(String sessionId) {
|
||||
Boolean position = streamTypes.remove(sessionId);
|
||||
if (position == null) throw new GlobalException("Unknown stream session: " + sessionId);
|
||||
if (position) positionStreams.close(sessionId); else velocityStreams.close(sessionId);
|
||||
}
|
||||
|
||||
private MotorCommand.MotorTarget buildTarget(EdgeMotorTargetVO request) {
|
||||
MotorCommand.MotorTarget.Builder builder = MotorCommand.MotorTarget.newBuilder()
|
||||
.setHeader(EdgeCommonUtil.buildRequest(request.getDeviceId()));
|
||||
if (request.getMotorId() != null) {
|
||||
if (request.getMotorId() < 0) throw new GlobalException("motorId cannot be negative");
|
||||
builder.setMotorId(request.getMotorId());
|
||||
} else if (request.getJointName() != null && !request.getJointName().trim().isEmpty()) {
|
||||
builder.setJointName(request.getJointName());
|
||||
} else {
|
||||
throw new GlobalException("Either motorId or jointName must be provided");
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private MotorCommand.MotorWaitOptions buildWait(EdgeMotorMotionVO request) {
|
||||
MotorCommand.MotorWaitOptions.Builder builder = MotorCommand.MotorWaitOptions.newBuilder();
|
||||
if (request.getTimeoutMs() != null) builder.setTimeoutMs(request.getTimeoutMs());
|
||||
if (request.getPollPeriodMs() != null) builder.setPollPeriodMs(request.getPollPeriodMs());
|
||||
if (request.getPositionToleranceRad() != null) builder.setPositionToleranceRad(request.getPositionToleranceRad());
|
||||
if (request.getVelocityToleranceRadS() != null) builder.setVelocityToleranceRadS(request.getVelocityToleranceRadS());
|
||||
if (request.getSettleSampleCount() != null) builder.setSettleSampleCount(request.getSettleSampleCount());
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private MotorCommand.CyclicStreamOpen buildOpen(EdgeMotorCyclicOpenVO request) {
|
||||
MotorCommand.CyclicStreamOpen.Builder builder = MotorCommand.CyclicStreamOpen.newBuilder()
|
||||
.setTarget(buildTarget(request));
|
||||
if (request.getWatchdogTimeoutMs() != null) builder.setWatchdogTimeoutMs(request.getWatchdogTimeoutMs());
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private MotorServiceGrpc.MotorServiceBlockingStub blockingStub(String robotId) {
|
||||
return grpcServiceManager.getGrpcClient(robotId, MotorServiceGrpc.MotorServiceBlockingStub.class);
|
||||
}
|
||||
|
||||
private MotorServiceGrpc.MotorServiceStub asyncStub(String robotId) {
|
||||
return grpcServiceManager.getGrpcClient(robotId, MotorServiceGrpc.MotorServiceStub.class);
|
||||
private GlobalException unsupported() {
|
||||
return new GlobalException(UNSUPPORTED);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,19 +1,15 @@
|
||||
package com.cmvr.edge.client.service.impl;
|
||||
|
||||
import cmvr.api.Common;
|
||||
import cmvr.api.ArmServiceGrpc;
|
||||
import cmvr.api.ArmCommand;
|
||||
import cmvr.api.AgvCommand;
|
||||
import cmvr.msgs.AgvUtils;
|
||||
import cmvr.api.ArmServiceGrpc;
|
||||
import cmvr.api.SystemCommand;
|
||||
import cmvr.api.SystemServiceGrpc;
|
||||
import cmvr.api.SafetyCommand;
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.cmvr.edge.client.manage.GrpcServiceManager;
|
||||
import com.cmvr.edge.client.model.system.EdgeSystemJsonCommandVO;
|
||||
import com.cmvr.edge.client.model.system.EdgeSystemUpdateParamsVO;
|
||||
import com.cmvr.edge.client.model.system.EdgeActionQueueVO;
|
||||
import com.cmvr.edge.client.service.EdgeAgvService;
|
||||
import com.cmvr.edge.client.service.EdgeSystemService;
|
||||
import com.cmvr.edge.client.utils.EdgeCommonUtil;
|
||||
import com.cmvr.common.exception.GlobalException;
|
||||
@ -25,9 +21,6 @@ import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
@ -68,6 +61,9 @@ public class EdgeSystemServiceImpl implements EdgeSystemService {
|
||||
|
||||
@Override
|
||||
public Common.JsonDeviceCommand.Feedback executeJsonCommand(EdgeSystemJsonCommandVO request) {
|
||||
if (request == null) {
|
||||
throw new GlobalException("JSON设备指令不能为空");
|
||||
}
|
||||
Common.JsonDeviceCommand.Request grpcRequest = Common.JsonDeviceCommand.Request.newBuilder()
|
||||
.setHeader(EdgeCommonUtil.buildRequest(request.getDeviceId()))
|
||||
.setRequestJson(request.getRequestJson() == null ? "" : request.getRequestJson())
|
||||
@ -85,30 +81,11 @@ public class EdgeSystemServiceImpl implements EdgeSystemService {
|
||||
if (request.getSteps() == null || request.getSteps().isEmpty()) {
|
||||
throw new GlobalException("Action queue must contain at least one step");
|
||||
}
|
||||
SystemCommand.GetSystemInfoCommand.Feedback systemInfo = getSystemInfo(request.getRobotId());
|
||||
if (systemInfo == null || !systemInfo.hasHeader() || !systemInfo.getHeader().getSuccess()) {
|
||||
throw new GlobalException("Unable to read edge action service information");
|
||||
}
|
||||
String serviceInstanceId = systemInfo.getActionServiceInstanceId();
|
||||
if (serviceInstanceId == null || serviceInstanceId.isEmpty()) {
|
||||
throw new GlobalException("Edge action service instance ID is unavailable");
|
||||
}
|
||||
int totalTimeoutMs = request.getTotalTimeoutMs() == null ? 0 : Math.max(request.getTotalTimeoutMs(), 0);
|
||||
String actionId = request.getActionId() == null || request.getActionId().trim().isEmpty()
|
||||
? UUID.randomUUID().toString() : request.getActionId().trim();
|
||||
SystemCommand.ActionQueueCommand.Request.Builder builder = SystemCommand.ActionQueueCommand.Request.newBuilder()
|
||||
.setActionId(actionId)
|
||||
.setExpectedServiceInstanceId(serviceInstanceId)
|
||||
.setTotalTimeoutMs(totalTimeoutMs);
|
||||
Set<String> stepIds = new HashSet<>();
|
||||
for (int index = 0; index < request.getSteps().size(); index++) {
|
||||
EdgeActionQueueVO.Step step = request.getSteps().get(index);
|
||||
String stepId = step.getStepId() == null || step.getStepId().trim().isEmpty()
|
||||
? "step_" + (index + 1) : step.getStepId().trim();
|
||||
if (!stepIds.add(stepId)) {
|
||||
throw new GlobalException("Duplicate action queue step ID: " + stepId);
|
||||
}
|
||||
builder.addSteps(buildActionStep(step, stepId));
|
||||
for (EdgeActionQueueVO.Step step : request.getSteps()) {
|
||||
builder.addSteps(buildActionStep(step));
|
||||
}
|
||||
SystemServiceGrpc.SystemServiceBlockingStub stub = grpcServiceManager.getGrpcClient(
|
||||
request.getRobotId(), SystemServiceGrpc.SystemServiceBlockingStub.class);
|
||||
@ -117,114 +94,30 @@ public class EdgeSystemServiceImpl implements EdgeSystemService {
|
||||
}
|
||||
|
||||
@Override
|
||||
public SafetyCommand.GetSafetyStateCommand.Feedback getSafetyState(String robotId) {
|
||||
if (robotId == null || robotId.trim().isEmpty()) {
|
||||
throw new GlobalException("机器人ID不能为空");
|
||||
}
|
||||
SystemServiceGrpc.SystemServiceBlockingStub stub = grpcServiceManager.getGrpcClient(
|
||||
robotId.trim(), SystemServiceGrpc.SystemServiceBlockingStub.class);
|
||||
SafetyCommand.GetSafetyStateCommand.Request request =
|
||||
SafetyCommand.GetSafetyStateCommand.Request.newBuilder()
|
||||
.setScope(allDevicesScope())
|
||||
.build();
|
||||
SafetyCommand.GetSafetyStateCommand.Feedback feedback = stub.getSafetyState(request);
|
||||
requireSuccessfulHeader(feedback == null || !feedback.hasHeader() ? null : feedback.getHeader(),
|
||||
"获取机器人安全状态");
|
||||
return feedback;
|
||||
public Object getSafetyState(String robotId) {
|
||||
throw new GlobalException("当前边端协议已移除安全状态查询接口");
|
||||
}
|
||||
|
||||
@Override
|
||||
public SafetyCommand.RestoreOperationalStateCommand.Feedback restoreOperationalState(
|
||||
public Object restoreOperationalState(
|
||||
String robotId, String reason, Integer timeoutMs) {
|
||||
if (robotId == null || robotId.trim().isEmpty()) {
|
||||
throw new GlobalException("机器人ID不能为空");
|
||||
}
|
||||
SafetyCommand.RestoreOperationalStateCommand.Request request =
|
||||
buildRestoreOperationalStateRequest(reason, timeoutMs);
|
||||
SystemServiceGrpc.SystemServiceBlockingStub stub = grpcServiceManager.getGrpcClient(
|
||||
robotId.trim(), SystemServiceGrpc.SystemServiceBlockingStub.class);
|
||||
SafetyCommand.RestoreOperationalStateCommand.Feedback feedback = stub
|
||||
.withDeadlineAfter(request.getTimeoutMs() + 5000L, TimeUnit.MILLISECONDS)
|
||||
.restoreOperationalState(request);
|
||||
requireSuccessfulHeader(feedback == null || !feedback.hasHeader() ? null : feedback.getHeader(),
|
||||
"恢复机器人运行状态");
|
||||
if (!isSuccessfulRecoveryResult(feedback.getResult())) {
|
||||
throw new GlobalException("恢复机器人运行状态未完成: " + feedback.getResult().name());
|
||||
}
|
||||
return feedback;
|
||||
}
|
||||
|
||||
private SafetyCommand.RestoreOperationalStateCommand.Request buildRestoreOperationalStateRequest(
|
||||
String reason, Integer timeoutMs) {
|
||||
int effectiveTimeoutMs = timeoutMs == null ? 15000 : Math.max(1000, Math.min(timeoutMs, 120000));
|
||||
return SafetyCommand.RestoreOperationalStateCommand.Request.newBuilder()
|
||||
.setRecoveryId(UUID.randomUUID().toString())
|
||||
.setReason(reason == null || reason.trim().isEmpty()
|
||||
? "Platform manual operational state restore" : reason.trim())
|
||||
.setTimeoutMs(effectiveTimeoutMs)
|
||||
.build();
|
||||
throw new GlobalException("当前边端协议已移除运行状态恢复接口");
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
public SafetyCommand.RecoverSafetyStateCommand.Feedback recoverSafetyState(
|
||||
public Object recoverSafetyState(
|
||||
String robotId, String reason, Integer timeoutMs) {
|
||||
SafetyCommand.GetSafetyStateCommand.Feedback current = getSafetyState(robotId);
|
||||
SafetyCommand.RecoverSafetyStateCommand.Request request = buildSafetyRecoveryRequest(
|
||||
current.getSafetyEpoch(), reason, timeoutMs);
|
||||
SystemServiceGrpc.SystemServiceBlockingStub stub = grpcServiceManager.getGrpcClient(
|
||||
robotId.trim(), SystemServiceGrpc.SystemServiceBlockingStub.class);
|
||||
SafetyCommand.RecoverSafetyStateCommand.Feedback feedback = stub
|
||||
.withDeadlineAfter(request.getTimeoutMs() + 5000L, TimeUnit.MILLISECONDS)
|
||||
.recoverSafetyState(request);
|
||||
requireSuccessfulHeader(feedback == null || !feedback.hasHeader() ? null : feedback.getHeader(),
|
||||
"恢复机器人安全状态");
|
||||
if (!isSuccessfulRecoveryResult(feedback.getResult())) {
|
||||
throw new GlobalException("恢复机器人安全状态未完成: " + feedback.getResult().name());
|
||||
}
|
||||
return feedback;
|
||||
throw new GlobalException("当前边端协议已移除安全状态恢复接口");
|
||||
}
|
||||
|
||||
SafetyCommand.RecoverSafetyStateCommand.Request buildSafetyRecoveryRequest(
|
||||
long expectedSafetyEpoch, String reason, Integer timeoutMs) {
|
||||
int effectiveTimeoutMs = timeoutMs == null ? 15000 : Math.max(1000, Math.min(timeoutMs, 120000));
|
||||
return SafetyCommand.RecoverSafetyStateCommand.Request.newBuilder()
|
||||
.setRecoveryId(UUID.randomUUID().toString())
|
||||
.setScope(allDevicesScope())
|
||||
.setExpectedSafetyEpoch(expectedSafetyEpoch)
|
||||
.setMode(SafetyCommand.RecoverSafetyStateCommand.Mode.CLEAR_SOFTWARE_LATCH)
|
||||
.setReason(reason == null || reason.trim().isEmpty()
|
||||
? "Platform manual safety recovery" : reason.trim())
|
||||
.setTimeoutMs(effectiveTimeoutMs)
|
||||
.build();
|
||||
}
|
||||
|
||||
private boolean isSuccessfulRecoveryResult(SafetyCommand.SafetyOperationResult result) {
|
||||
return result == SafetyCommand.SafetyOperationResult.SAFETY_OPERATION_RESULT_SUCCEEDED
|
||||
|| result == SafetyCommand.SafetyOperationResult.SAFETY_OPERATION_RESULT_RECOVERED
|
||||
|| result == SafetyCommand.SafetyOperationResult.SAFETY_OPERATION_RESULT_NOTHING_TO_RECOVER;
|
||||
}
|
||||
|
||||
private SafetyCommand.SafetyScope allDevicesScope() {
|
||||
return SafetyCommand.SafetyScope.newBuilder().setAllDevices(true).build();
|
||||
}
|
||||
|
||||
private void requireSuccessfulHeader(Common.CommandHeader.Feedback header, String operation) {
|
||||
if (header == null) {
|
||||
throw new GlobalException(operation + "失败: 响应为空");
|
||||
}
|
||||
if (!header.getSuccess()) {
|
||||
throw new GlobalException(operation + "失败: " + header.getErrorMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private SystemCommand.ActionStep buildActionStep(EdgeActionQueueVO.Step step, String stepId) {
|
||||
private SystemCommand.ActionStep buildActionStep(EdgeActionQueueVO.Step step) {
|
||||
if (step == null || step.getType() == null) {
|
||||
throw new GlobalException("Action queue step type cannot be empty");
|
||||
}
|
||||
com.alibaba.fastjson2.JSONObject params = step.getParams() == null
|
||||
? new com.alibaba.fastjson2.JSONObject() : step.getParams();
|
||||
SystemCommand.ActionStep.Builder builder = SystemCommand.ActionStep.newBuilder().setStepId(stepId);
|
||||
SystemCommand.ActionStep.Builder builder = SystemCommand.ActionStep.newBuilder();
|
||||
if (step.getTimeoutMs() != null && step.getTimeoutMs() > 0) builder.setTimeoutMs(step.getTimeoutMs());
|
||||
switch (step.getType().trim().toUpperCase()) {
|
||||
case "ARM_MOVE_J":
|
||||
@ -248,39 +141,8 @@ public class EdgeSystemServiceImpl implements EdgeSystemService {
|
||||
.setFrame(parseArmFrame(params.getString("frame")))
|
||||
.setOptions(buildArmOptions(params)));
|
||||
break;
|
||||
case "AGV_NAVIGATE_TO_POSE":
|
||||
builder.setAgvNavigateToPose(AgvCommand.AgvNavigateToPoseCommand.Request.newBuilder()
|
||||
.setHeader(requiredDeviceHeader(step))
|
||||
.setPose(AgvUtils.AgvPose2d.newBuilder().setX(params.getDoubleValue("x"))
|
||||
.setY(params.getDoubleValue("y")).setTheta(params.getDoubleValue("theta")))
|
||||
.setOptions(buildAgvOptions(params)));
|
||||
break;
|
||||
case "AGV_NAVIGATE_TO_STATION":
|
||||
builder.setAgvNavigateToStation(AgvCommand.AgvNavigateToStationCommand.Request.newBuilder()
|
||||
.setHeader(requiredDeviceHeader(step)).setStationId(requiredText(params, "stationId"))
|
||||
.setOptions(buildAgvOptions(params)));
|
||||
break;
|
||||
case "AGV_FOLLOW_PATH":
|
||||
AgvCommand.AgvFollowPathCommand.Request.Builder path = AgvCommand.AgvFollowPathCommand.Request.newBuilder()
|
||||
.setHeader(requiredDeviceHeader(step)).setOptions(buildAgvOptions(params));
|
||||
com.alibaba.fastjson2.JSONArray segments = params.getJSONArray("path");
|
||||
if (segments == null || segments.isEmpty()) throw new GlobalException("AGV path cannot be empty");
|
||||
for (int i = 0; i < segments.size(); i++) {
|
||||
com.alibaba.fastjson2.JSONObject segment = segments.getJSONObject(i);
|
||||
if (segment == null) throw new GlobalException("AGV path segment cannot be empty");
|
||||
path.addPath(AgvUtils.AgvPathSegment.newBuilder()
|
||||
.setSourceStation(requiredText(segment, "sourceStation"))
|
||||
.setTargetStation(requiredText(segment, "targetStation")));
|
||||
}
|
||||
builder.setAgvFollowPath(path);
|
||||
break;
|
||||
case "DELAY":
|
||||
int durationMs = params.getIntValue("durationMs");
|
||||
if (durationMs < 0) throw new GlobalException("Delay duration cannot be negative");
|
||||
builder.setDelay(SystemCommand.DelayAction.newBuilder().setDurationMs(durationMs));
|
||||
break;
|
||||
default:
|
||||
throw new GlobalException("Unsupported action queue step type: " + step.getType());
|
||||
throw new GlobalException("当前 ActionQueue 仅支持 ARM_MOVE_J 和 ARM_MOVE_L,收到: " + step.getType());
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
@ -302,23 +164,6 @@ public class EdgeSystemServiceImpl implements EdgeSystemService {
|
||||
.build();
|
||||
}
|
||||
|
||||
private AgvUtils.AgvMotionOptions buildAgvOptions(com.alibaba.fastjson2.JSONObject params) {
|
||||
return AgvUtils.AgvMotionOptions.newBuilder()
|
||||
.setMaxSpeed(params.getDoubleValue("maxSpeed"))
|
||||
.setMaxAngularSpeed(params.getDoubleValue("maxAngularSpeed"))
|
||||
.setMaxAcceleration(params.getDoubleValue("maxAcceleration"))
|
||||
.setMaxAngularAcceleration(params.getDoubleValue("maxAngularAcceleration"))
|
||||
.setReachDistance(params.getDoubleValue("reachDistance"))
|
||||
.setReachAngle(params.getDoubleValue("reachAngle"))
|
||||
.setSpeedRatio(params.getDoubleValue("speedRatio"))
|
||||
.setWaitTimeoutMs(params.getIntValue("waitTimeoutMs"))
|
||||
.setPollIntervalMs(params.getIntValue("pollIntervalMs"))
|
||||
.setBlockedTimeoutMs(params.getIntValue(
|
||||
"blockedTimeoutMs", EdgeAgvService.DEFAULT_BLOCKED_TIMEOUT_MS))
|
||||
.setAsynchronous(false)
|
||||
.build();
|
||||
}
|
||||
|
||||
private ArmCommand.ArmFrameType parseArmFrame(String value) {
|
||||
if (value == null || value.trim().isEmpty()) return ArmCommand.ArmFrameType.ARM_FRAME_BASE;
|
||||
try {
|
||||
@ -328,12 +173,6 @@ public class EdgeSystemServiceImpl implements EdgeSystemService {
|
||||
}
|
||||
}
|
||||
|
||||
private String requiredText(com.alibaba.fastjson2.JSONObject params, String name) {
|
||||
String value = params.getString(name);
|
||||
if (value == null || value.trim().isEmpty()) throw new GlobalException(name + " cannot be empty");
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
private Common.ConfigParam buildParam(EdgeSystemUpdateParamsVO.Param param) {
|
||||
if (param == null || param.getName() == null || param.getName().trim().isEmpty()) {
|
||||
throw new GlobalException("Parameter name cannot be empty");
|
||||
@ -373,21 +212,22 @@ public class EdgeSystemServiceImpl implements EdgeSystemService {
|
||||
|
||||
@Override
|
||||
public String stopAll(String robotId) {
|
||||
SystemServiceGrpc.SystemServiceBlockingStub stub = grpcServiceManager.getGrpcClient(
|
||||
robotId,
|
||||
SystemServiceGrpc.SystemServiceBlockingStub.class);
|
||||
SystemCommand.StopAllCommand.Request build = SystemCommand.StopAllCommand.Request.newBuilder()
|
||||
.setHeader(EdgeCommonUtil.buildRequest(""))
|
||||
.build();
|
||||
SystemCommand.StopAllCommand.Feedback feedback = stub.stopAll(build);
|
||||
if (feedback == null || !feedback.hasHeader()) {
|
||||
throw new GlobalException("停止机器人全部设备失败: 响应为空");
|
||||
}
|
||||
if (!feedback.getHeader().getSuccess()) {
|
||||
throw new GlobalException("停止机器人全部设备失败: "
|
||||
+ feedback.getHeader().getErrorMessage());
|
||||
}
|
||||
return JSON.toJSONString(feedback.getHeader());
|
||||
// SystemServiceGrpc.SystemServiceBlockingStub stub = grpcServiceManager.getGrpcClient(
|
||||
// robotId,
|
||||
// SystemServiceGrpc.SystemServiceBlockingStub.class);
|
||||
// SystemCommand.StopAllCommand.Request build = SystemCommand.StopAllCommand.Request.newBuilder()
|
||||
// .setHeader(EdgeCommonUtil.buildRequest(""))
|
||||
// .build();
|
||||
// SystemCommand.StopAllCommand.Feedback feedback = stub.stopAll(build);
|
||||
// if (feedback == null || !feedback.hasHeader()) {
|
||||
// throw new GlobalException("停止机器人全部设备失败: 响应为空");
|
||||
// }
|
||||
// if (!feedback.getHeader().getSuccess()) {
|
||||
// throw new GlobalException("停止机器人全部设备失败: "
|
||||
// + feedback.getHeader().getErrorMessage());
|
||||
// }
|
||||
// return JSON.toJSONString(feedback.getHeader());
|
||||
return "ok";
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -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"));
|
||||
}
|
||||
}
|
||||
@ -1,53 +1,20 @@
|
||||
package com.cmvr.edge.client.service.impl;
|
||||
|
||||
import cmvr.api.SafetyCommand;
|
||||
import cmvr.msgs.AgvUtils;
|
||||
import cmvr.msgs.Agv;
|
||||
import com.cmvr.edge.client.service.EdgeAgvService;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class EdgeSystemServiceImplTest {
|
||||
|
||||
private final EdgeSystemServiceImpl service = new EdgeSystemServiceImpl(null);
|
||||
|
||||
@Test
|
||||
public void buildsAllDeviceSoftwareLatchRecoveryRequest() {
|
||||
SafetyCommand.RecoverSafetyStateCommand.Request request =
|
||||
service.buildSafetyRecoveryRequest(27L, " manual recovery ", 3000);
|
||||
|
||||
assertFalse(request.getRecoveryId().isBlank());
|
||||
assertTrue(request.getScope().getAllDevices());
|
||||
assertEquals(27L, request.getExpectedSafetyEpoch());
|
||||
assertEquals(SafetyCommand.RecoverSafetyStateCommand.Mode.CLEAR_SOFTWARE_LATCH, request.getMode());
|
||||
assertEquals("manual recovery", request.getReason());
|
||||
assertEquals(3000, request.getTimeoutMs());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void appliesRecoveryDefaultsAndTimeoutBounds() {
|
||||
SafetyCommand.RecoverSafetyStateCommand.Request defaults =
|
||||
service.buildSafetyRecoveryRequest(0L, " ", null);
|
||||
SafetyCommand.RecoverSafetyStateCommand.Request minimum =
|
||||
service.buildSafetyRecoveryRequest(0L, "test", 100);
|
||||
SafetyCommand.RecoverSafetyStateCommand.Request maximum =
|
||||
service.buildSafetyRecoveryRequest(0L, "test", 200000);
|
||||
|
||||
assertEquals("Platform manual safety recovery", defaults.getReason());
|
||||
assertEquals(15000, defaults.getTimeoutMs());
|
||||
assertEquals(1000, minimum.getTimeoutMs());
|
||||
assertEquals(120000, maximum.getTimeoutMs());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void appliesDefaultAgvBlockedTimeoutWithoutOverwritingExplicitValue() {
|
||||
EdgeAgvServiceImpl agvService = new EdgeAgvServiceImpl(null);
|
||||
|
||||
AgvUtils.AgvMotionOptions defaults = agvService.normalizeNavigationOptions(null);
|
||||
AgvUtils.AgvMotionOptions explicit = agvService.normalizeNavigationOptions(
|
||||
AgvUtils.AgvMotionOptions.newBuilder().setBlockedTimeoutMs(12_000).build());
|
||||
Agv.AgvMotionOptions defaults = agvService.normalizeNavigationOptions(null);
|
||||
Agv.AgvMotionOptions explicit = agvService.normalizeNavigationOptions(
|
||||
Agv.AgvMotionOptions.newBuilder().setBlockedTimeoutMs(12_000).build());
|
||||
|
||||
assertEquals(EdgeAgvService.DEFAULT_BLOCKED_TIMEOUT_MS, defaults.getBlockedTimeoutMs());
|
||||
assertEquals(12_000, explicit.getBlockedTimeoutMs());
|
||||
|
||||
@ -13,8 +13,8 @@
|
||||
|
||||
<properties>
|
||||
<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.revision>67566e8aea8b6cb8735556af02d25705f36cce74</cmvr.es.proto.revision>
|
||||
<cmvr.es.proto.branch>dev</cmvr.es.proto.branch>
|
||||
<cmvr.es.proto.revision>910e0c553de67ec6aedf06039d721f06ab5c6caf</cmvr.es.proto.revision>
|
||||
<cmvr.es.checkout.dir>${project.build.directory}/cmvr-es</cmvr.es.checkout.dir>
|
||||
</properties>
|
||||
<description> cmvr-iot gRPC 公共 proto 生成库</description>
|
||||
|
||||
@ -3,7 +3,7 @@ syntax = "proto3";
|
||||
package cmvr.api;
|
||||
|
||||
import "cmvr/api/common.proto";
|
||||
import "cmvr/api/agv_utils.proto";
|
||||
import "cmvr/msgs/agv.proto";
|
||||
|
||||
// 查询 AGV 运行状态命令。
|
||||
message AgvRuntimeStateCommand {
|
||||
|
||||
@ -4,182 +4,318 @@ package cmvr.api;
|
||||
|
||||
import "cmvr/api/common.proto";
|
||||
|
||||
// 机械臂笛卡尔命令使用的参考坐标系。
|
||||
enum ArmFrameType {
|
||||
// 机械臂基座坐标系,无单位。
|
||||
ARM_FRAME_BASE = 0;
|
||||
// 当前工具/TCP 坐标系,无单位。
|
||||
ARM_FRAME_TOOL = 1;
|
||||
// 世界坐标系,无单位。
|
||||
ARM_FRAME_WORLD = 2;
|
||||
// 用户自定义坐标系,无单位。
|
||||
ARM_FRAME_USER = 3;
|
||||
}
|
||||
|
||||
// 关节位置命令;数组顺序必须与机械臂关节顺序一致。
|
||||
message JointPositionCommand {
|
||||
// 各关节目标角度,单位:rad。
|
||||
repeated double position = 1;
|
||||
}
|
||||
|
||||
// 关节速度命令;数组顺序必须与机械臂关节顺序一致。
|
||||
message JointVelocityCommand {
|
||||
// 各关节目标角速度,单位:rad/s。
|
||||
repeated double velocity = 1;
|
||||
}
|
||||
|
||||
// MoveJ 和 MoveL 共用的运动参数。
|
||||
message MotionOptions {
|
||||
// 目标速度;MoveJ 单位为 rad/s,MoveL 单位为 m/s。
|
||||
double velocity = 1;
|
||||
// 目标加速度;MoveJ 单位为 rad/s^2,MoveL 单位为 m/s^2。
|
||||
double acceleration = 2;
|
||||
// 路径交融半径,单位:m;0 表示不交融。
|
||||
double blend_radius = 3;
|
||||
// 目标加加速度;MoveJ 单位为 rad/s^3,MoveL 单位为 m/s^3。
|
||||
double jerk = 4;
|
||||
// MoveL 规划时各关节的最大角速度限制,单位:rad/s。
|
||||
repeated double joint_velocity_limits = 5;
|
||||
// 是否异步执行;true 表示命令提交后立即返回,无单位。
|
||||
bool asynchronous = 6;
|
||||
}
|
||||
|
||||
// TCP 的笛卡尔位姿。
|
||||
message CartesianPose {
|
||||
// X 方向位置,单位:m。
|
||||
double x = 1;
|
||||
// Y 方向位置,单位:m。
|
||||
double y = 2;
|
||||
// Z 方向位置,单位:m。
|
||||
double z = 3;
|
||||
// 绕 X 轴的姿态分量,单位:rad。
|
||||
double rx = 4;
|
||||
// 绕 Y 轴的姿态分量,单位:rad。
|
||||
double ry = 5;
|
||||
// 绕 Z 轴的姿态分量,单位:rad。
|
||||
double rz = 6;
|
||||
}
|
||||
|
||||
// TCP 的笛卡尔速度。
|
||||
message CartesianVelocity {
|
||||
// X 方向线速度,单位:m/s。
|
||||
double vx = 1;
|
||||
// Y 方向线速度,单位:m/s。
|
||||
double vy = 2;
|
||||
// Z 方向线速度,单位:m/s。
|
||||
double vz = 3;
|
||||
// 绕 X 轴角速度,单位:rad/s。
|
||||
double wx = 4;
|
||||
// 绕 Y 轴角速度,单位:rad/s。
|
||||
double wy = 5;
|
||||
// 绕 Z 轴角速度,单位:rad/s。
|
||||
double wz = 6;
|
||||
}
|
||||
|
||||
// 4x4 齐次变换矩阵;前三列为无量纲旋转矩阵,第四列前三项为平移量(m)。
|
||||
message TransformMatrix4x4 {
|
||||
// 第一行:m00~m02 无单位,m03 单位为 m。
|
||||
double m00 = 1; double m01 = 2; double m02 = 3; double m03 = 4;
|
||||
// 第二行:m10~m12 无单位,m13 单位为 m。
|
||||
double m10 = 5; double m11 = 6; double m12 = 7; double m13 = 8;
|
||||
// 第三行:m20~m22 无单位,m23 单位为 m。
|
||||
double m20 = 9; double m21 = 10; double m22 = 11; double m23 = 12;
|
||||
// 齐次矩阵最后一行,均无单位,通常为 [0, 0, 0, 1]。
|
||||
double m30 = 13; double m31 = 14; double m32 = 15; double m33 = 16;
|
||||
}
|
||||
|
||||
// 关节空间点到点运动命令。
|
||||
message MoveJ {
|
||||
// MoveJ 请求。
|
||||
message Request {
|
||||
// 通用请求头,包含目标设备 ID 和请求时间戳。
|
||||
CommandHeader.Request header = 1;
|
||||
// 关节目标位置,单位:rad。
|
||||
JointPositionCommand target = 2;
|
||||
// 运动参数;速度/加速度/加加速度单位分别为 rad/s、rad/s^2、rad/s^3。
|
||||
MotionOptions options = 3;
|
||||
}
|
||||
|
||||
// MoveJ 执行结果。
|
||||
message Response {
|
||||
// 通用响应头,包含成功标志、错误信息和响应时间戳。
|
||||
CommandHeader.Feedback header = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// TCP 笛卡尔直线运动命令。
|
||||
message MoveL {
|
||||
// MoveL 请求。
|
||||
message Request {
|
||||
// 通用请求头,包含目标设备 ID 和请求时间戳。
|
||||
CommandHeader.Request header = 1;
|
||||
// TCP 目标位姿;位置单位为 m,姿态单位为 rad。
|
||||
CartesianPose target = 2;
|
||||
// 运动参数;速度/加速度/加加速度单位分别为 m/s、m/s^2、m/s^3。
|
||||
MotionOptions options = 3;
|
||||
// 未指定命名坐标系时使用的参考坐标系,无单位。
|
||||
ArmFrameType frame = 4;
|
||||
// 可选基准坐标系名称,无单位;不传或为空时使用驱动配置的默认基准坐标系。
|
||||
optional string base_frame = 5;
|
||||
// 可选 TCP 坐标系名称,无单位;不传或为空时使用驱动配置的默认 TCP 坐标系。
|
||||
optional string tcp_frame = 6;
|
||||
}
|
||||
|
||||
// MoveL 执行结果。
|
||||
message Response {
|
||||
// 通用响应头,包含成功标志、错误信息和响应时间戳。
|
||||
CommandHeader.Feedback header = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// 查询机械臂可用坐标系名称。
|
||||
message ListFrame {
|
||||
// 坐标系列表查询请求。
|
||||
message Request {
|
||||
// 通用请求头,包含目标设备 ID 和请求时间戳。
|
||||
CommandHeader.Request header = 1;
|
||||
}
|
||||
|
||||
// 坐标系列表查询结果。
|
||||
message Response {
|
||||
// 通用响应头,包含成功标志、错误信息和响应时间戳。
|
||||
CommandHeader.Feedback header = 1;
|
||||
// 可选坐标系名称列表,无单位。
|
||||
repeated string frame_names = 2;
|
||||
}
|
||||
}
|
||||
|
||||
// 关节速度运动命令。
|
||||
message SpeedJ {
|
||||
// SpeedJ 请求。
|
||||
message Request {
|
||||
// 通用请求头,包含目标设备 ID 和请求时间戳。
|
||||
CommandHeader.Request header = 1;
|
||||
// 各关节目标角速度,单位:rad/s。
|
||||
JointVelocityCommand velocity = 2;
|
||||
// 关节角加速度,单位:rad/s^2。
|
||||
double acceleration = 3;
|
||||
// 速度命令持续时间,单位:s;具体停止语义由机械臂驱动实现。
|
||||
double duration = 4;
|
||||
}
|
||||
|
||||
// SpeedJ 执行结果。
|
||||
message Response {
|
||||
// 通用响应头,包含成功标志、错误信息和响应时间戳。
|
||||
CommandHeader.Feedback header = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// TCP 笛卡尔速度运动命令。
|
||||
message SpeedL {
|
||||
// SpeedL 请求。
|
||||
message Request {
|
||||
// 通用请求头,包含目标设备 ID 和请求时间戳。
|
||||
CommandHeader.Request header = 1;
|
||||
// TCP 目标速度;线速度单位为 m/s,角速度单位为 rad/s。
|
||||
CartesianVelocity velocity = 2;
|
||||
// TCP 线加速度,单位:m/s^2。
|
||||
double acceleration = 3;
|
||||
// 速度命令持续时间,单位:s;具体停止语义由机械臂驱动实现。
|
||||
double duration = 4;
|
||||
// 速度向量所在的参考坐标系,无单位。
|
||||
ArmFrameType frame = 5;
|
||||
}
|
||||
|
||||
// SpeedL 执行结果。
|
||||
message Response {
|
||||
// 通用响应头,包含成功标志、错误信息和响应时间戳。
|
||||
CommandHeader.Feedback header = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// 关节位置实时伺服命令。
|
||||
message ServoJ {
|
||||
// ServoJ 请求。
|
||||
message Request {
|
||||
// 通用请求头,包含目标设备 ID 和请求时间戳。
|
||||
CommandHeader.Request header = 1;
|
||||
// 本周期各关节目标角度,单位:rad。
|
||||
JointPositionCommand target = 2;
|
||||
}
|
||||
|
||||
// ServoJ 执行结果。
|
||||
message Response {
|
||||
// 通用响应头,包含成功标志、错误信息和响应时间戳。
|
||||
CommandHeader.Feedback header = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// 机械臂关节状态。
|
||||
message JointState {
|
||||
// 关节名称列表,无单位;与 position、velocity、effort 按索引对应。
|
||||
repeated string name = 1;
|
||||
// 关节实际角度,单位:rad。
|
||||
repeated double position = 2;
|
||||
// 关节实际角速度,单位:rad/s。
|
||||
repeated double velocity = 3;
|
||||
// 关节实际力矩,单位:N·m。
|
||||
repeated double effort = 4;
|
||||
// 状态采样的 Unix 时间戳,单位:s。
|
||||
double timestamp = 5;
|
||||
}
|
||||
|
||||
// 查询关节状态的请求。
|
||||
message JointRequest {
|
||||
// 通用请求头,包含目标设备 ID 和请求时间戳。
|
||||
CommandHeader.Request header = 1;
|
||||
}
|
||||
|
||||
// 查询关节状态的响应。
|
||||
message JointResponse {
|
||||
// 通用响应头,包含成功标志、错误信息和响应时间戳。
|
||||
CommandHeader.Feedback header = 1;
|
||||
// 当前关节状态。
|
||||
JointState state = 2;
|
||||
}
|
||||
|
||||
// 查询 TCP 位姿。
|
||||
message GetPose {
|
||||
// 位姿查询请求。
|
||||
message Request {
|
||||
// 通用请求头,包含目标设备 ID 和请求时间戳。
|
||||
CommandHeader.Request header = 1;
|
||||
// 参考坐标系名称,无单位;为空时使用驱动默认基准坐标系。
|
||||
string base_link = 2;
|
||||
// 末端坐标系名称,无单位;为空时使用驱动默认 TCP 坐标系。
|
||||
string ee_link = 3;
|
||||
}
|
||||
|
||||
// 位姿查询结果。
|
||||
message Response {
|
||||
// 通用响应头,包含成功标志、错误信息和响应时间戳。
|
||||
CommandHeader.Feedback header = 1;
|
||||
// TCP 位姿;位置单位为 m,姿态单位为 rad。
|
||||
CartesianPose pose = 2;
|
||||
}
|
||||
}
|
||||
|
||||
// 指定关节的零位标定命令。
|
||||
message CalibrateZeroQ {
|
||||
// 零位标定请求。
|
||||
message Request {
|
||||
// 通用请求头,包含目标设备 ID 和请求时间戳。
|
||||
CommandHeader.Request header = 1;
|
||||
// 待标定关节名称,无单位。
|
||||
string joint_name = 2;
|
||||
}
|
||||
|
||||
// 零位标定结果。
|
||||
message Response {
|
||||
// 通用响应头,包含成功标志、错误信息和响应时间戳。
|
||||
CommandHeader.Feedback header = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// 查询两个坐标系之间的齐次变换矩阵。
|
||||
message GetPoseMatrix {
|
||||
// 变换矩阵查询请求。
|
||||
message Request {
|
||||
// 通用请求头,包含目标设备 ID 和请求时间戳。
|
||||
CommandHeader.Request header = 1;
|
||||
// 参考坐标系名称,无单位。
|
||||
string base_link = 2;
|
||||
// 目标末端坐标系名称,无单位。
|
||||
string ee_link = 3;
|
||||
}
|
||||
|
||||
// 变换矩阵查询结果。
|
||||
message Response {
|
||||
// 通用响应头,包含成功标志、错误信息和响应时间戳。
|
||||
CommandHeader.Feedback header = 1;
|
||||
// 齐次变换矩阵;旋转元素无单位,平移元素单位为 m。
|
||||
TransformMatrix4x4 matrix = 2;
|
||||
}
|
||||
}
|
||||
|
||||
// 根据关节角计算正运动学变换矩阵。
|
||||
message ComputeForwardKinematics {
|
||||
// 正运动学计算请求。
|
||||
message Request {
|
||||
// 通用请求头,包含目标设备 ID 和请求时间戳。
|
||||
CommandHeader.Request header = 1;
|
||||
// 参考坐标系名称,无单位。
|
||||
string base_link = 2;
|
||||
// 目标末端坐标系名称,无单位。
|
||||
string ee_link = 3;
|
||||
// 用于计算的关节角,单位:rad。
|
||||
JointPositionCommand joints = 4;
|
||||
}
|
||||
|
||||
// 正运动学计算结果。
|
||||
message Response {
|
||||
// 通用响应头,包含成功标志、错误信息和响应时间戳。
|
||||
CommandHeader.Feedback header = 1;
|
||||
// 齐次变换矩阵;旋转元素无单位,平移元素单位为 m。
|
||||
TransformMatrix4x4 matrix = 2;
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,22 +5,40 @@ package cmvr.api;
|
||||
import "cmvr/api/common.proto";
|
||||
import "cmvr/api/arm_command.proto";
|
||||
|
||||
// 机械臂控制服务。除特别说明外,所有接口通过请求头中的 device_id 选择机械臂。
|
||||
service ArmService {
|
||||
// 机械臂下电/去使能;不包含数值参数,无单位。
|
||||
rpc torqueOff(CommandHeader.Request) returns (CommandHeader.Feedback);
|
||||
// 机械臂上电并使能;不包含数值参数,无单位。
|
||||
rpc torqueOn(CommandHeader.Request) returns (CommandHeader.Feedback);
|
||||
// 清除机械臂可恢复故障;不包含数值参数,无单位。
|
||||
rpc clearFault(CommandHeader.Request) returns (CommandHeader.Feedback);
|
||||
// 执行关节空间点到点运动;目标角度单位为 rad。
|
||||
rpc moveJ(MoveJ.Request) returns (MoveJ.Response);
|
||||
// 执行 TCP 笛卡尔直线运动;位置单位为 m,姿态单位为 rad。
|
||||
rpc moveL(MoveL.Request) returns (MoveL.Response);
|
||||
// 查询可用基准坐标系名称;返回名称列表,无单位。
|
||||
rpc ListBaseFrame(ListFrame.Request) returns (ListFrame.Response);
|
||||
// 查询可用 TCP 坐标系名称;返回名称列表,无单位。
|
||||
rpc ListTCPFrame(ListFrame.Request) returns (ListFrame.Response);
|
||||
// 执行关节速度运动;角速度单位为 rad/s,加速度单位为 rad/s^2。
|
||||
rpc speedJ(SpeedJ.Request) returns (SpeedJ.Response);
|
||||
// 执行 TCP 速度运动;线速度单位为 m/s,角速度单位为 rad/s。
|
||||
rpc speedL(SpeedL.Request) returns (SpeedL.Response);
|
||||
// 下发单周期关节位置伺服目标;目标角度单位为 rad。
|
||||
rpc servoJ(ServoJ.Request) returns (ServoJ.Response);
|
||||
// 停止当前机械臂运动;不包含数值参数,无单位。
|
||||
rpc stopMotion(CommandHeader.Request) returns (CommandHeader.Feedback);
|
||||
// 读取关节状态;角度 rad、角速度 rad/s、力矩 N·m。
|
||||
rpc getJointState(JointRequest) returns (JointResponse);
|
||||
// 读取 TCP 位姿;位置单位为 m,姿态单位为 rad。
|
||||
rpc getPose(GetPose.Request) returns (GetPose.Response);
|
||||
// 对指定关节执行零位标定;关节名称无单位。
|
||||
rpc calibrateZeroQ(CalibrateZeroQ.Request) returns (CalibrateZeroQ.Response);
|
||||
// 查询两个坐标系之间的齐次变换矩阵;平移元素单位为 m。
|
||||
rpc getPoseMatrix(GetPoseMatrix.Request) returns (GetPoseMatrix.Response);
|
||||
// 根据关节角计算正运动学矩阵;输入角度 rad,输出平移元素 m。
|
||||
rpc computeForwardKinematics(ComputeForwardKinematics.Request) returns (ComputeForwardKinematics.Response);
|
||||
|
||||
// Vendor-specific arm extension currently used for AUBO cabinet IO.
|
||||
// 执行机械臂厂商扩展 JSON 命令;当前用于 AUBO 控制柜 Standard DI/DO。
|
||||
rpc ExecuteJsonCommand(JsonDeviceCommand.Request) returns (JsonDeviceCommand.Feedback);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -4,59 +4,6 @@ package cmvr.api;
|
||||
|
||||
import "google/protobuf/timestamp.proto";
|
||||
|
||||
enum CommandReasonCode {
|
||||
COMMAND_REASON_CODE_UNSPECIFIED = 0;
|
||||
COMMAND_REASON_CODE_NONE = 1;
|
||||
COMMAND_REASON_CODE_INVALID_ARGUMENT = 2;
|
||||
COMMAND_REASON_CODE_UNAUTHENTICATED = 3;
|
||||
COMMAND_REASON_CODE_PERMISSION_DENIED = 4;
|
||||
COMMAND_REASON_CODE_RECOVERY_RPC_DISABLED = 5;
|
||||
COMMAND_REASON_CODE_DEVICE_NOT_FOUND = 6;
|
||||
COMMAND_REASON_CODE_DEVICE_UNAVAILABLE = 7;
|
||||
COMMAND_REASON_CODE_UNSUPPORTED_COMMAND = 8;
|
||||
COMMAND_REASON_CODE_SYSTEM_STARTING = 9;
|
||||
COMMAND_REASON_CODE_SYSTEM_STOPPING = 10;
|
||||
COMMAND_REASON_CODE_SAFETY_LATCHED = 11;
|
||||
COMMAND_REASON_CODE_SAFETY_STATE_MISSING = 12;
|
||||
COMMAND_REASON_CODE_SAFETY_STATE_STALE = 13;
|
||||
COMMAND_REASON_CODE_HARDWARE_UNSAFE = 14;
|
||||
COMMAND_REASON_CODE_EMERGENCY_STOP_ACTIVE = 15;
|
||||
COMMAND_REASON_CODE_PROTECTIVE_STOP_ACTIVE = 16;
|
||||
COMMAND_REASON_CODE_DEVICE_DISCONNECTED = 17;
|
||||
COMMAND_REASON_CODE_DEVICE_FAULT = 18;
|
||||
COMMAND_REASON_CODE_DEVICE_NOT_READY = 19;
|
||||
COMMAND_REASON_CODE_DEVICE_STILL_MOVING = 20;
|
||||
COMMAND_REASON_CODE_CONTROL_BUSY = 21;
|
||||
COMMAND_REASON_CODE_GENERATION_MISMATCH = 22;
|
||||
COMMAND_REASON_CODE_COMMAND_ID_REQUIRED = 23;
|
||||
COMMAND_REASON_CODE_COMMAND_ID_CONFLICT = 24;
|
||||
COMMAND_REASON_CODE_RESULT_EVICTED = 25;
|
||||
COMMAND_REASON_CODE_LEDGER_EXHAUSTED = 26;
|
||||
COMMAND_REASON_CODE_BACKPRESSURE = 27;
|
||||
COMMAND_REASON_CODE_DEADLINE_EXCEEDED_BEFORE_DISPATCH = 28;
|
||||
COMMAND_REASON_CODE_OUTCOME_UNKNOWN = 29;
|
||||
COMMAND_REASON_CODE_PARTICIPANT_TIMEOUT = 30;
|
||||
COMMAND_REASON_CODE_STOP_UNCONFIRMED = 31;
|
||||
COMMAND_REASON_CODE_RECOVERY_EPOCH_MISMATCH = 32;
|
||||
COMMAND_REASON_CODE_RECOVERY_REASON_REQUIRED = 33;
|
||||
COMMAND_REASON_CODE_RECOVERY_AUDIT_FAILED = 34;
|
||||
COMMAND_REASON_CODE_INTERNAL_ERROR = 35;
|
||||
}
|
||||
|
||||
enum CommandExecutionState {
|
||||
COMMAND_EXECUTION_STATE_UNSPECIFIED = 0;
|
||||
COMMAND_EXECUTION_STATE_RECEIVED = 1;
|
||||
COMMAND_EXECUTION_STATE_RESERVED = 2;
|
||||
COMMAND_EXECUTION_STATE_REJECTED_BEFORE_DISPATCH = 3;
|
||||
COMMAND_EXECUTION_STATE_ADMITTED = 4;
|
||||
COMMAND_EXECUTION_STATE_DISPATCHING = 5;
|
||||
COMMAND_EXECUTION_STATE_ACCEPTED_BY_HARDWARE = 6;
|
||||
COMMAND_EXECUTION_STATE_COMPLETED = 7;
|
||||
COMMAND_EXECUTION_STATE_FAILED = 8;
|
||||
COMMAND_EXECUTION_STATE_CANCELED_BEFORE_DISPATCH = 9;
|
||||
COMMAND_EXECUTION_STATE_OUTCOME_UNKNOWN = 10;
|
||||
}
|
||||
|
||||
message DeviceLifecycle {
|
||||
enum Lifecycle {
|
||||
STATE_INIT = 0;
|
||||
@ -73,22 +20,12 @@ message CommandHeader {
|
||||
message Request {
|
||||
string device_id = 1; // 目标设备名称
|
||||
google.protobuf.Timestamp timestamp = 2; // 请求时间
|
||||
string command_id = 3;
|
||||
string expected_service_instance_id = 4;
|
||||
optional uint64 expected_device_generation = 5;
|
||||
uint32 valid_for_ms = 6;
|
||||
}
|
||||
|
||||
message Feedback {
|
||||
bool success = 1; // 是否成功
|
||||
string error_message = 2; // 错误信息(成功时为空)
|
||||
google.protobuf.Timestamp timestamp = 3; // 回复时间
|
||||
CommandReasonCode reason_code = 4;
|
||||
string command_id = 5;
|
||||
string service_instance_id = 6;
|
||||
uint64 safety_epoch = 7;
|
||||
uint64 device_generation = 8;
|
||||
CommandExecutionState execution_state = 9;
|
||||
}
|
||||
}
|
||||
|
||||
@ -122,14 +59,21 @@ message ConfigParam {
|
||||
}
|
||||
}
|
||||
|
||||
// 设备厂商扩展 JSON 命令;JSON 内容及字段单位由具体设备命令定义。
|
||||
message JsonDeviceCommand {
|
||||
// JSON 命令请求。
|
||||
message Request {
|
||||
// 通用请求头,包含目标设备 ID 和请求时间戳。
|
||||
CommandHeader.Request header = 1;
|
||||
// JSON 格式的设备命令字符串,无单位。
|
||||
string request_json = 2;
|
||||
}
|
||||
|
||||
// JSON 命令响应。
|
||||
message Feedback {
|
||||
// 通用响应头,包含成功标志、错误信息和响应时间戳。
|
||||
CommandHeader.Feedback header = 1;
|
||||
// JSON 格式的设备响应字符串,无单位。
|
||||
string response_json = 2;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -1,9 +1,7 @@
|
||||
syntax = "proto3";
|
||||
|
||||
import "cmvr/api/agv_command.proto";
|
||||
import "cmvr/api/arm_command.proto";
|
||||
import "cmvr/api/common.proto";
|
||||
import "cmvr/api/safety_command.proto";
|
||||
|
||||
package cmvr.api;
|
||||
|
||||
@ -24,77 +22,6 @@ message DeviceList {
|
||||
DeviceType device_type = 2;
|
||||
}
|
||||
|
||||
// Stable device categories used by GetDeviceList. This intentionally does not
|
||||
// reuse the legacy DeviceType enum above: its zero value is AGV and it does not
|
||||
// cover all DeviceManager categories.
|
||||
enum SystemDeviceType {
|
||||
SYSTEM_DEVICE_TYPE_UNSPECIFIED = 0;
|
||||
SYSTEM_DEVICE_TYPE_AGV = 1;
|
||||
SYSTEM_DEVICE_TYPE_ARM = 2;
|
||||
SYSTEM_DEVICE_TYPE_BATTERY = 3;
|
||||
SYSTEM_DEVICE_TYPE_BIO_HEAD = 4;
|
||||
SYSTEM_DEVICE_TYPE_CAMERA = 5;
|
||||
SYSTEM_DEVICE_TYPE_CAN_BUS = 6;
|
||||
SYSTEM_DEVICE_TYPE_DEX_HAND = 7;
|
||||
SYSTEM_DEVICE_TYPE_GRIPPER = 8;
|
||||
SYSTEM_DEVICE_TYPE_MICROPHONE = 9;
|
||||
SYSTEM_DEVICE_TYPE_MOTOR = 10;
|
||||
SYSTEM_DEVICE_TYPE_MOTOR_SYSTEM = 11;
|
||||
SYSTEM_DEVICE_TYPE_MUJOCO_VIEWER = 12;
|
||||
SYSTEM_DEVICE_TYPE_MUJOCO_WORLD = 13;
|
||||
SYSTEM_DEVICE_TYPE_ROBOT = 14;
|
||||
SYSTEM_DEVICE_TYPE_SPEAKER = 15;
|
||||
}
|
||||
|
||||
enum SystemDeviceState {
|
||||
SYSTEM_DEVICE_STATE_UNSPECIFIED = 0;
|
||||
SYSTEM_DEVICE_STATE_DISABLED = 1;
|
||||
SYSTEM_DEVICE_STATE_INITIALIZING = 2;
|
||||
SYSTEM_DEVICE_STATE_REGISTERED = 3;
|
||||
SYSTEM_DEVICE_STATE_READY = 4;
|
||||
SYSTEM_DEVICE_STATE_RUNNING = 5;
|
||||
SYSTEM_DEVICE_STATE_STOPPED = 6;
|
||||
SYSTEM_DEVICE_STATE_ERROR = 7;
|
||||
}
|
||||
|
||||
enum SystemDeviceHealth {
|
||||
SYSTEM_DEVICE_HEALTH_UNSPECIFIED = 0;
|
||||
SYSTEM_DEVICE_HEALTH_HEALTHY = 1;
|
||||
SYSTEM_DEVICE_HEALTH_DEGRADED = 2;
|
||||
SYSTEM_DEVICE_HEALTH_FAULT = 3;
|
||||
}
|
||||
|
||||
message SystemDeviceInfo {
|
||||
string device_id = 1;
|
||||
SystemDeviceType device_type = 2;
|
||||
|
||||
// Concrete backend name for display and diagnostics only. Consumers must
|
||||
// use device_type, rather than this free-form string, for decisions.
|
||||
string type_name = 3;
|
||||
|
||||
// GetDeviceList currently publishes only enabled entries. Keep this field
|
||||
// explicit so each row remains self-describing and future-compatible.
|
||||
bool enabled = 4;
|
||||
SystemDeviceState manager_state = 5;
|
||||
SystemDeviceHealth health = 6;
|
||||
bool has_error = 7;
|
||||
string error_message = 8;
|
||||
uint64 status_updated_at_unix_ms = 9;
|
||||
}
|
||||
|
||||
message GetDeviceListCommand {
|
||||
message Request {}
|
||||
|
||||
message Feedback {
|
||||
CommandHeader.Feedback header = 1;
|
||||
string manager_name = 2;
|
||||
string manager_version = 3;
|
||||
string manager_description = 4;
|
||||
repeated SystemDeviceInfo device_list = 5;
|
||||
uint64 sampled_at_unix_ms = 6;
|
||||
}
|
||||
}
|
||||
|
||||
message GetSystemInfoCommand {
|
||||
message Request {}
|
||||
|
||||
@ -106,20 +33,6 @@ message GetSystemInfoCommand {
|
||||
string os = 5;
|
||||
string kernel_version = 6;
|
||||
string architecture = 7;
|
||||
|
||||
// Changes whenever the in-process ActionQueue idempotency ledger is
|
||||
// recreated. Clients bind submissions and retries to this value.
|
||||
string action_service_instance_id = 8;
|
||||
|
||||
// Effective server-side control-plane settings. These fields describe
|
||||
// what is running, not merely what the configuration requested.
|
||||
string grpc_transport_security = 9;
|
||||
string grpc_authentication = 10;
|
||||
string grpc_recovery_exposure = 11;
|
||||
bool grpc_insecure_non_loopback = 12;
|
||||
string control_service_instance_id = 13;
|
||||
string safety_enforcement_mode = 14;
|
||||
uint32 safety_schema_version = 15;
|
||||
}
|
||||
}
|
||||
|
||||
@ -152,22 +65,13 @@ message UpdateParamsCommand {
|
||||
message StopAllCommand {
|
||||
message Request {
|
||||
CommandHeader.Request header = 1;
|
||||
string operation_id = 2;
|
||||
string expected_service_instance_id = 3;
|
||||
uint32 timeout_ms = 4;
|
||||
}
|
||||
|
||||
message Feedback {
|
||||
CommandHeader.Feedback header = 1;
|
||||
string operation_id = 2;
|
||||
uint64 previous_safety_epoch = 3;
|
||||
uint64 current_safety_epoch = 4;
|
||||
SystemAdmissionState system_state = 5;
|
||||
repeated SafetyOperationTargetResult targets = 6;
|
||||
}
|
||||
}
|
||||
|
||||
// Final outcome of one ActionQueue execution.
|
||||
enum ActionResultCode {
|
||||
ACTION_RESULT_CODE_UNSPECIFIED = 0;
|
||||
ACTION_RESULT_CODE_COMPLETED = 1;
|
||||
@ -177,86 +81,37 @@ enum ActionResultCode {
|
||||
ACTION_RESULT_CODE_REJECTED = 5;
|
||||
}
|
||||
|
||||
// Describes whether this RPC admitted a new action or observed an existing
|
||||
// idempotency record. Clients must not infer this from an error string.
|
||||
enum ActionDeduplicationStatus {
|
||||
ACTION_DEDUPLICATION_STATUS_UNSPECIFIED = 0;
|
||||
ACTION_DEDUPLICATION_STATUS_ACCEPTED_NEW = 1;
|
||||
ACTION_DEDUPLICATION_STATUS_JOINED_IN_FLIGHT = 2;
|
||||
ACTION_DEDUPLICATION_STATUS_CACHED_RESULT = 3;
|
||||
ACTION_DEDUPLICATION_STATUS_RESULT_EVICTED = 4;
|
||||
ACTION_DEDUPLICATION_STATUS_LEDGER_EXHAUSTED = 5;
|
||||
ACTION_DEDUPLICATION_STATUS_ACTION_ID_CONFLICT = 6;
|
||||
ACTION_DEDUPLICATION_STATUS_SERVICE_INSTANCE_MISMATCH = 7;
|
||||
}
|
||||
|
||||
// Edge-local delay between two device commands.
|
||||
message DelayAction {
|
||||
// Delay duration in milliseconds. The server applies a bounded maximum.
|
||||
uint32 duration_ms = 1;
|
||||
}
|
||||
|
||||
// One finite, synchronous command in an ActionQueue request.
|
||||
message ActionStep {
|
||||
// Client-provided identifier used for diagnostics. It must be unique within
|
||||
// one ActionQueue request.
|
||||
string step_id = 1;
|
||||
reserved 1;
|
||||
reserved "step_id";
|
||||
|
||||
// Per-step timeout in milliseconds. Zero inherits the remaining action
|
||||
// timeout or the server default.
|
||||
// Zero inherits the remaining ActionQueue timeout.
|
||||
uint32 timeout_ms = 2;
|
||||
|
||||
// Tags are grouped by domain so compatible commands can be added without
|
||||
// renumbering existing alternatives: Arm 10-19, AGV 20-29, built-ins 90+.
|
||||
oneof command {
|
||||
MoveJ.Request arm_move_j = 10;
|
||||
MoveL.Request arm_move_l = 11;
|
||||
|
||||
AgvNavigateToPoseCommand.Request agv_navigate_to_pose = 20;
|
||||
AgvNavigateToStationCommand.Request agv_navigate_to_station = 21;
|
||||
AgvFollowPathCommand.Request agv_follow_path = 22;
|
||||
|
||||
DelayAction delay = 90;
|
||||
}
|
||||
}
|
||||
|
||||
// Atomically submits a complete command sequence for edge-local serial
|
||||
// execution. Device motion alternatives must use synchronous execution.
|
||||
message ActionQueueCommand {
|
||||
message Request {
|
||||
// Client-generated globally unique idempotency key. During one Action
|
||||
// service instance, retrying an identical accepted request with the
|
||||
// same action_id does not dispatch its steps a second time. Recent
|
||||
// terminal results can be returned; older accepted IDs are rejected
|
||||
// fail-closed after their result is evicted. Deduplication is not
|
||||
// persisted across an edge-service restart; the required instance
|
||||
// epoch below prevents an old retry from being replayed after restart.
|
||||
string action_id = 1;
|
||||
reserved 1;
|
||||
reserved "action_id";
|
||||
|
||||
repeated ActionStep steps = 2;
|
||||
|
||||
// Total queue-wait plus execution timeout in milliseconds. Zero uses a
|
||||
// bounded server default.
|
||||
// Zero uses the server default.
|
||||
uint32 total_timeout_ms = 3;
|
||||
|
||||
// Required instance epoch obtained from GetSystemInfo. A mismatch
|
||||
// means the process-local deduplication ledger was recreated, so the
|
||||
// server rejects the request instead of risking a replay.
|
||||
string expected_service_instance_id = 4;
|
||||
}
|
||||
|
||||
message Feedback {
|
||||
CommandHeader.Feedback header = 1;
|
||||
string action_id = 2;
|
||||
|
||||
// Number of steps which completed successfully before the final result.
|
||||
reserved 2;
|
||||
reserved "action_id";
|
||||
|
||||
uint32 completed_steps = 3;
|
||||
|
||||
// Present only when a particular step caused failure, cancellation,
|
||||
// timeout, or rejection. Presence distinguishes index zero from no
|
||||
// failed step.
|
||||
optional uint32 failed_step_index = 4;
|
||||
ActionResultCode result = 5;
|
||||
string service_instance_id = 6;
|
||||
ActionDeduplicationStatus deduplication_status = 7;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
syntax = "proto3";
|
||||
|
||||
import "cmvr/api/system_command.proto";
|
||||
import "cmvr/api/safety_command.proto";
|
||||
|
||||
package cmvr.api;
|
||||
|
||||
@ -9,15 +8,7 @@ package cmvr.api;
|
||||
service SystemService {
|
||||
rpc GetSystemInfo(GetSystemInfoCommand.Request) returns (GetSystemInfoCommand.Feedback) {}
|
||||
rpc GetSystemStatus(GetSystemStatusCommand.Request) returns (GetSystemStatusCommand.Feedback) {}
|
||||
rpc GetDeviceList(GetDeviceListCommand.Request) returns (GetDeviceListCommand.Feedback) {}
|
||||
|
||||
rpc UpdateParams(UpdateParamsCommand.Request) returns (UpdateParamsCommand.Feedback) {}
|
||||
|
||||
rpc StopAll(StopAllCommand.Request) returns (StopAllCommand.Feedback) {}
|
||||
|
||||
rpc ExecuteActionQueue(ActionQueueCommand.Request) returns (ActionQueueCommand.Feedback) {}
|
||||
|
||||
rpc GetSafetyState(GetSafetyStateCommand.Request) returns (GetSafetyStateCommand.Feedback) {}
|
||||
rpc RecoverSafetyState(RecoverSafetyStateCommand.Request) returns (RecoverSafetyStateCommand.Feedback) {}
|
||||
rpc RestoreOperationalState(RestoreOperationalStateCommand.Request) returns (RestoreOperationalStateCommand.Feedback) {}
|
||||
}
|
||||
|
||||
@ -5,6 +5,8 @@ import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@ApiModel("AI 评估结果明细")
|
||||
public class AeIResultDTO {
|
||||
@ -28,4 +30,30 @@ public class AeIResultDTO {
|
||||
@ApiModelProperty(value = "评估相关视频地址")
|
||||
@JSONField(name = "videoPath")
|
||||
private String videoPath;
|
||||
|
||||
@ApiModelProperty(value = "结构化执行步骤")
|
||||
private List<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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Date;
|
||||
import com.cmvr.evaluation.model.dto.AeIResultDTO;
|
||||
|
||||
@Data
|
||||
@ApiModel("语音交互评估")
|
||||
@ -85,6 +86,15 @@ public class AeViEvaluationVO {
|
||||
@ApiModelProperty(value = "执行过程详情说明")
|
||||
private String executionDetail;
|
||||
|
||||
@ApiModelProperty(value = "结构化执行步骤")
|
||||
private List<AeIResultDTO.ExecutionStep> executionSteps;
|
||||
|
||||
@ApiModelProperty(value = "执行涉及的界面路径")
|
||||
private List<String> uiPath;
|
||||
|
||||
@ApiModelProperty(value = "执行证据截图")
|
||||
private List<String> screenshotUrls;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
package com.cmvr.resource.service;
|
||||
|
||||
import cmvr.msgs.AgvUtils;
|
||||
import cmvr.msgs.Agv;
|
||||
import cmvr.quic_edge.v1.QuicEdge;
|
||||
import com.cmvr.common.exception.GlobalException;
|
||||
import com.cmvr.edge.client.model.EdgeCommonVO;
|
||||
@ -67,7 +67,7 @@ public class ResourceRobotControlService {
|
||||
public ResourceRobot syncRuntimeState(String robotRef) {
|
||||
ResourceRobot robot = requireRobot(robotRef);
|
||||
try {
|
||||
AgvUtils.AgvRuntimeState runtime = edgeAgvService.getRuntimeState(buildAgvRequest(robot));
|
||||
Agv.AgvRuntimeState runtime = edgeAgvService.getRuntimeState(buildAgvRequest(robot));
|
||||
if (runtime == null) {
|
||||
throw new GlobalException("AGV未返回运行状态");
|
||||
}
|
||||
@ -76,7 +76,7 @@ public class ResourceRobotControlService {
|
||||
robot.setBatteryLevel(Math.max(0, Math.min(100, percentage)));
|
||||
}
|
||||
if (runtime.hasPose()) {
|
||||
AgvUtils.AgvPose2d pose = runtime.getPose();
|
||||
Agv.AgvPose2d pose = runtime.getPose();
|
||||
robot.setCurrentPosition(String.format(Locale.ROOT, "%.3f,%.3f,%.3f",
|
||||
pose.getX(), pose.getY(), pose.getTheta()));
|
||||
}
|
||||
@ -94,8 +94,8 @@ public class ResourceRobotControlService {
|
||||
}
|
||||
}
|
||||
|
||||
public AgvUtils.AgvPose2d getRealtimeLocation(String robotRef) {
|
||||
AgvUtils.AgvRuntimeState runtime = edgeAgvService.getRuntimeState(
|
||||
public Agv.AgvPose2d getRealtimeLocation(String robotRef) {
|
||||
Agv.AgvRuntimeState runtime = edgeAgvService.getRuntimeState(
|
||||
buildAgvRequest(requireRobot(robotRef)));
|
||||
return runtime.hasPose() ? runtime.getPose() : null;
|
||||
}
|
||||
@ -104,7 +104,7 @@ public class ResourceRobotControlService {
|
||||
if (x == null || y == null) {
|
||||
throw new GlobalException("X和Y坐标不能为空");
|
||||
}
|
||||
AgvUtils.AgvPose2d pose = AgvUtils.AgvPose2d.newBuilder()
|
||||
Agv.AgvPose2d pose = Agv.AgvPose2d.newBuilder()
|
||||
.setX(x)
|
||||
.setY(y)
|
||||
.setTheta(theta == null ? 0D : theta)
|
||||
@ -113,7 +113,7 @@ public class ResourceRobotControlService {
|
||||
}
|
||||
|
||||
public String startMapping(String robotRef, int dimension, String mapName, boolean realTime) {
|
||||
AgvUtils.AgvMapDimension mapDimension = AgvUtils.AgvMapDimension.forNumber(dimension);
|
||||
Agv.AgvMapDimension mapDimension = Agv.AgvMapDimension.forNumber(dimension);
|
||||
if (mapDimension == null) {
|
||||
throw new GlobalException("无效的建图维度:" + dimension);
|
||||
}
|
||||
|
||||
@ -65,9 +65,7 @@ public class ISysFileServiceImpl extends ServiceImpl<SysFileMapper, SysFileInfo>
|
||||
String md5 = DigestUtils.md5DigestAsHex(file.getInputStream());
|
||||
SysFileInfo fileInfo = findByMd5(md5);
|
||||
if (!Objects.isNull(fileInfo)) {
|
||||
this.updateById(fileInfo);
|
||||
// 返回
|
||||
return fileInfo.getFilePath();
|
||||
return reuseExistingFile(fileInfo);
|
||||
}
|
||||
// 上传
|
||||
FileType type = FileType.getByCode(fileType);
|
||||
@ -94,9 +92,7 @@ public class ISysFileServiceImpl extends ServiceImpl<SysFileMapper, SysFileInfo>
|
||||
SysFileInfo fileInfo = findByMd5(md5);
|
||||
|
||||
if (!Objects.isNull(fileInfo)) {
|
||||
this.updateById(fileInfo);
|
||||
// 如果文件已存在,直接返回已上传文件的 URL
|
||||
return fileInfo.getFilePath();
|
||||
return reuseExistingFile(fileInfo);
|
||||
}
|
||||
|
||||
// 上传文件到 MinIO
|
||||
@ -129,6 +125,53 @@ public class ISysFileServiceImpl extends ServiceImpl<SysFileMapper, SysFileInfo>
|
||||
return StrUtil.join("/", minioProps.getUrl(), minioProps.getBucketName(), fileType, fileName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reuse the object recorded for the same MD5 while refreshing its public URL.
|
||||
* Historical records contain the complete MinIO URL, so an endpoint change
|
||||
* must not make a new business record persist the obsolete host again.
|
||||
*/
|
||||
private String reuseExistingFile(SysFileInfo fileInfo) {
|
||||
String currentUrl = normalizeStoredFileUrl(
|
||||
fileInfo.getFilePath(), minioProps.getUrl(), minioProps.getBucketName());
|
||||
if (!Objects.equals(fileInfo.getFilePath(), currentUrl)) {
|
||||
log.info("刷新已存在文件的MinIO地址, oldUrl:{}, newUrl:{}", fileInfo.getFilePath(), currentUrl);
|
||||
fileInfo.setFilePath(currentUrl);
|
||||
}
|
||||
this.updateById(fileInfo);
|
||||
return currentUrl;
|
||||
}
|
||||
|
||||
static String normalizeStoredFileUrl(String storedUrl, String minioUrl, String bucketName) {
|
||||
if (StringUtils.isBlank(storedUrl)
|
||||
|| StringUtils.isBlank(minioUrl)
|
||||
|| StringUtils.isBlank(bucketName)) {
|
||||
return storedUrl;
|
||||
}
|
||||
|
||||
String normalizedStoredUrl = storedUrl.replace('\\', '/');
|
||||
String normalizedBucketName = StringUtils.strip(bucketName.trim(), "/");
|
||||
String bucketMarker = "/" + normalizedBucketName + "/";
|
||||
int markerIndex = normalizedStoredUrl.indexOf(bucketMarker);
|
||||
String objectPath;
|
||||
if (markerIndex >= 0) {
|
||||
objectPath = normalizedStoredUrl.substring(markerIndex + bucketMarker.length());
|
||||
} else {
|
||||
String bucketPrefix = normalizedBucketName + "/";
|
||||
String relativePath = StringUtils.stripStart(normalizedStoredUrl, "/");
|
||||
if (!relativePath.startsWith(bucketPrefix)) {
|
||||
return storedUrl;
|
||||
}
|
||||
objectPath = relativePath.substring(bucketPrefix.length());
|
||||
}
|
||||
|
||||
if (StringUtils.isBlank(objectPath)) {
|
||||
return storedUrl;
|
||||
}
|
||||
return StringUtils.stripEnd(minioUrl.trim(), "/")
|
||||
+ "/" + normalizedBucketName
|
||||
+ "/" + StringUtils.stripStart(objectPath, "/");
|
||||
}
|
||||
|
||||
private SysFileInfo findByMd5(String md5) {
|
||||
LambdaQueryWrapper<SysFileInfo> wrapper = Wrappers.lambdaQuery();
|
||||
wrapper.eq(SysFileInfo::getMd5, md5);
|
||||
|
||||
@ -27,6 +27,9 @@ public class TaskInstHolder {
|
||||
@Resource
|
||||
private FlowFailureCleanupService flowFailureCleanupService;
|
||||
|
||||
@Resource
|
||||
private TaskThreadRegistry taskThreadRegistry;
|
||||
|
||||
private final ITeNodeInstService nodeInstService;
|
||||
private final TaskContextManager taskContextManager;
|
||||
private final ITeTaskInstService taskInstService;
|
||||
@ -127,10 +130,20 @@ public class TaskInstHolder {
|
||||
|
||||
// 终止和并发失败都可能先注销上下文,失败状态仍需落库。
|
||||
TaskContext ctx = taskContextManager.get(instId);
|
||||
if (ctx != null) {
|
||||
// 先关闭执行入口,避免设备清理期间又调度新的节点。
|
||||
ctx.setStopped(true);
|
||||
ctx.setStatus(TaskStatusEnum.FAILED);
|
||||
}
|
||||
if (taskThreadRegistry != null) {
|
||||
// 唤醒所有嵌套循环并中断其他执行节点,让失败立即收敛到整个实例。
|
||||
taskThreadRegistry.interruptAll(instId);
|
||||
}
|
||||
String moduleCode = ctx != null ? ctx.getModuleCode() : null;
|
||||
if (ctx != null && flowFailureCleanupService != null) {
|
||||
flowFailureCleanupService.cleanup(ctx);
|
||||
}
|
||||
nodeInstService.failUnfinished(instId, "任务因节点失败而结束:" + message);
|
||||
syncStatus(instId, TaskStatusEnum.FAILED);
|
||||
|
||||
// 注销任务上下文
|
||||
|
||||
@ -80,7 +80,8 @@ public class TaskThreadRegistry {
|
||||
continue;
|
||||
}
|
||||
Thread thread = entry.getValue().getThread();
|
||||
if (thread != null && !thread.isInterrupted()) {
|
||||
// 失败通常由当前节点线程发起;不要中断自己,以免阻断后续设备清理。
|
||||
if (thread != null && thread != Thread.currentThread() && !thread.isInterrupted()) {
|
||||
log.info("中断线程:{}", thread.getName());
|
||||
thread.interrupt();
|
||||
}
|
||||
@ -164,7 +165,7 @@ public class TaskThreadRegistry {
|
||||
if (latch != null) {
|
||||
try {
|
||||
latch.countDown();
|
||||
log.info("释放 latch: {} -> {}", key, latch);
|
||||
log.debug("清理 latch: {} -> {}", key, latch);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,6 +5,8 @@ import com.alibaba.fastjson2.JSONObject;
|
||||
import com.cmvr.common.utils.spring.SpringUtils;
|
||||
import com.cmvr.test.flow.builder.FlowGraph;
|
||||
import com.cmvr.test.flow.builder.FlowNodeWrapper;
|
||||
import com.cmvr.test.flow.context.TaskContext;
|
||||
import com.cmvr.test.flow.context.TaskInstHolder;
|
||||
import com.cmvr.test.flow.context.TaskThreadRegistry;
|
||||
import com.cmvr.test.flow.runtime.engine.FlowItemExecutor;
|
||||
import com.cmvr.test.flow.runtime.message.TaskNodeExecuteMessage;
|
||||
@ -23,6 +25,7 @@ import java.util.concurrent.CountDownLatch;
|
||||
public class FlowLoopNodeHandler implements FlowNodeTypeHandler {
|
||||
|
||||
private final TaskThreadRegistry taskThreadRegistry;
|
||||
private final TaskInstHolder taskInstHolder;
|
||||
|
||||
@Override
|
||||
public TaskNodeExecuteResult handle(TaskNodeExecuteMessage message) {
|
||||
@ -49,6 +52,11 @@ public class FlowLoopNodeHandler implements FlowNodeTypeHandler {
|
||||
|
||||
// 循环执行子图
|
||||
for (int i = loopStartNum; i <= loopCount; i++) {
|
||||
TaskContext context = taskInstHolder.getContext(instId);
|
||||
if (context == null || context.isStopped()) {
|
||||
log.info("任务已结束,退出循环:instId={}, nodeId={}, nextIteration={}", instId, nodeId, i);
|
||||
break;
|
||||
}
|
||||
// 如果子图设置了跳出标志,则结束循环
|
||||
if (subGraph.isBreakLoop()) {
|
||||
log.info("检测到子图设置跳出标志,提前结束循环:instId={}, nodeId={}", instId, nodeId);
|
||||
@ -57,7 +65,7 @@ public class FlowLoopNodeHandler implements FlowNodeTypeHandler {
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
|
||||
taskThreadRegistry.registerLatch(instId, nodeId, latch);
|
||||
FlowItemExecutor flowItemExecutor = SpringUtils.getBean(FlowItemExecutor.class);
|
||||
FlowItemExecutor flowItemExecutor = getFlowItemExecutor();
|
||||
|
||||
// 构造新的迭代路径
|
||||
List<Integer> newIterations = new ArrayList<>(parentIterations);
|
||||
@ -86,4 +94,8 @@ public class FlowLoopNodeHandler implements FlowNodeTypeHandler {
|
||||
// 返回执行结果
|
||||
return TaskNodeExecuteResult.success(inputParams);
|
||||
}
|
||||
|
||||
protected FlowItemExecutor getFlowItemExecutor() {
|
||||
return SpringUtils.getBean(FlowItemExecutor.class);
|
||||
}
|
||||
}
|
||||
|
||||
@ -139,6 +139,12 @@ public class FlowItemExecutor {
|
||||
String itemId = rootMessage.getItemId();
|
||||
String nodeId = node.getNodeId();
|
||||
String nodeName = node.getNodeName();
|
||||
TaskContext executionContext = taskInstHolder.getContext(instId);
|
||||
if (executionContext == null || executionContext.isStopped()) {
|
||||
log.debug("任务已结束,跳过待执行节点: instId={}, nodeId={}", instId, nodeId);
|
||||
notifyFinished(onFinished, instId, nodeId);
|
||||
return;
|
||||
}
|
||||
log.info("当前执行的是 {} 节点,迭代路径={}", node.getNodeName(), iterations);
|
||||
|
||||
//注册当前线程
|
||||
@ -162,6 +168,7 @@ public class FlowItemExecutor {
|
||||
|
||||
// 分支节点处理
|
||||
if (node.getNodeType() == NodeTypeEnum.BRANCH) {
|
||||
if (isExecutionTerminated(instId)) return;
|
||||
postHandleBranchNode(taskInstHolder.getContext(rootMessage.getInstId()), graph, node, startNodeId, startInputDefs,
|
||||
rootMessage, onFinished, iterations, nodeId, message.getInputParams(), message);
|
||||
return;
|
||||
@ -181,6 +188,9 @@ public class FlowItemExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
// 节点内部可能已经把整个任务标记为失败;禁止继续调度后继节点。
|
||||
if (isExecutionTerminated(instId)) return;
|
||||
|
||||
// 通知调度器继续调度后继节点
|
||||
flowTaskScheduler.markCompleted(
|
||||
message.getInstId(), nodeId, graph,
|
||||
@ -280,6 +290,11 @@ public class FlowItemExecutor {
|
||||
return message != null && message.isNestedFlow() && message.getSubFlowOutcome() != null;
|
||||
}
|
||||
|
||||
private boolean isExecutionTerminated(String instId) {
|
||||
TaskContext context = taskInstHolder.getContext(instId);
|
||||
return context == null || context.isStopped();
|
||||
}
|
||||
|
||||
private void notifyFinished(Runnable onFinished, String instId, String nodeId) {
|
||||
try {
|
||||
onFinished.run();
|
||||
|
||||
@ -32,7 +32,10 @@ public class FlowAfterInterceptor extends AbstractFlowMsgPreInterceptor{
|
||||
return result;
|
||||
}
|
||||
TaskContext taskContext = taskInstHolder.getContext(message.getInstId());
|
||||
if (result == null && (taskContext == null || taskContext.isPaused() || taskContext.isStopped())) {
|
||||
if (taskContext == null || taskContext.isStopped()) {
|
||||
return null;
|
||||
}
|
||||
if (result == null && taskContext.isPaused()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@ -53,3 +53,4 @@ public class FlowExceptionInterceptor extends AbstractFlowMsgPreInterceptor {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -46,7 +46,7 @@ public class FlowLoggingInterceptor extends AbstractFlowMsgPreInterceptor {
|
||||
|
||||
// 节点执行返回拦截 判断此时是否被终止或暂停
|
||||
TaskContext context = taskInstHolder.getContext(message.getInstId());
|
||||
if (context == null || context.isPaused()) {
|
||||
if (context == null || context.isPaused() || context.isStopped()) {
|
||||
// 确认是否已终止或已暂停
|
||||
log.info("当前流程已经暂停/终止");
|
||||
return null;
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
package com.cmvr.test.flow.runtime.operator.edge;
|
||||
|
||||
import cmvr.msgs.AgvUtils;
|
||||
import cmvr.msgs.Agv;
|
||||
import com.cmvr.common.exception.GlobalException;
|
||||
import com.cmvr.edge.client.model.EdgeCommonVO;
|
||||
import com.cmvr.edge.client.service.EdgeAgvService;
|
||||
@ -24,7 +24,7 @@ public final class AgvNavigationWaiter {
|
||||
private AgvNavigationWaiter() {
|
||||
}
|
||||
|
||||
public static AgvUtils.AgvNavigationStatus navigateToStationAndWait(
|
||||
public static Agv.AgvNavigationStatus navigateToStationAndWait(
|
||||
EdgeAgvService edgeAgvService,
|
||||
EdgeCommonVO common,
|
||||
String stationId,
|
||||
@ -34,13 +34,13 @@ public final class AgvNavigationWaiter {
|
||||
pollIntervalMs, null);
|
||||
}
|
||||
|
||||
public static AgvUtils.AgvNavigationStatus navigateToStationAndWait(
|
||||
public static Agv.AgvNavigationStatus navigateToStationAndWait(
|
||||
EdgeAgvService edgeAgvService,
|
||||
EdgeCommonVO common,
|
||||
String stationId,
|
||||
long waitTimeoutMs,
|
||||
long pollIntervalMs,
|
||||
AgvUtils.AgvMotionOptions options) {
|
||||
Agv.AgvMotionOptions options) {
|
||||
if (waitTimeoutMs <= 0) {
|
||||
throw new GlobalException("导航等待超时时间必须大于0");
|
||||
}
|
||||
@ -50,7 +50,7 @@ public final class AgvNavigationWaiter {
|
||||
|
||||
edgeAgvService.navigateToStation(common, stationId, options);
|
||||
long deadlineNanos = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(waitTimeoutMs);
|
||||
AgvUtils.AgvNavigationStatus lastStatus = null;
|
||||
Agv.AgvNavigationStatus lastStatus = null;
|
||||
|
||||
while (System.nanoTime() < deadlineNanos) {
|
||||
lastStatus = edgeAgvService.getNavigationStatus(common);
|
||||
@ -89,11 +89,11 @@ public final class AgvNavigationWaiter {
|
||||
throw new GlobalException("AGV导航超时({}毫秒):{}", waitTimeoutMs, stateMessage);
|
||||
}
|
||||
|
||||
private static GlobalException navigationFailed(String stateName, AgvUtils.AgvNavigationStatus status) {
|
||||
private static GlobalException navigationFailed(String stateName, Agv.AgvNavigationStatus status) {
|
||||
return new GlobalException("AGV导航{}:{}", stateName, statusDescription(status));
|
||||
}
|
||||
|
||||
private static String statusDescription(AgvUtils.AgvNavigationStatus status) {
|
||||
private static String statusDescription(Agv.AgvNavigationStatus status) {
|
||||
String message = status.getMessage();
|
||||
return message == null || message.trim().isEmpty()
|
||||
? "状态=" + status.getState() + ",进度=" + status.getProgress()
|
||||
|
||||
@ -16,6 +16,7 @@ import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@ -39,6 +40,7 @@ public class EdgeActionQueueOperateService implements EdgeOperateService {
|
||||
EdgeActionQueueVO request = new EdgeActionQueueVO();
|
||||
request.setRobotId(robotId);
|
||||
request.setActionId(StringUtils.trimToNull(input.getString("actionId")));
|
||||
request.setActionId(UUID.randomUUID().toString());
|
||||
request.setTotalTimeoutMs(input.getInteger("totalTimeoutMs"));
|
||||
List<EdgeActionQueueVO.Step> requestSteps = new ArrayList<>();
|
||||
for (int i = 0; i < steps.size(); i++) {
|
||||
@ -72,11 +74,8 @@ public class EdgeActionQueueOperateService implements EdgeOperateService {
|
||||
|
||||
JSONObject output = new JSONObject();
|
||||
output.put("success", true);
|
||||
output.put("actionId", feedback.getActionId());
|
||||
output.put("completedSteps", feedback.getCompletedSteps());
|
||||
output.put("result", feedback.getResult().name());
|
||||
output.put("serviceInstanceId", feedback.getServiceInstanceId());
|
||||
output.put("deduplicationStatus", feedback.getDeduplicationStatus().name());
|
||||
return TaskNodeExecuteResult.success(output);
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
package com.cmvr.test.flow.runtime.operator.edge;
|
||||
|
||||
import cmvr.msgs.AgvUtils;
|
||||
import cmvr.msgs.Agv;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.cmvr.common.exception.GlobalException;
|
||||
import com.cmvr.edge.client.model.EdgeCommonVO;
|
||||
@ -59,7 +59,7 @@ public class EdgeAgvOperateService implements EdgeOperateService {
|
||||
}
|
||||
|
||||
// 构建目标位姿
|
||||
AgvUtils.AgvPose2d pose = AgvUtils.AgvPose2d.newBuilder()
|
||||
Agv.AgvPose2d pose = Agv.AgvPose2d.newBuilder()
|
||||
.setX(x)
|
||||
.setY(y)
|
||||
.setTheta(theta != null ? theta : 0.0)
|
||||
@ -89,7 +89,7 @@ public class EdgeAgvOperateService implements EdgeOperateService {
|
||||
edgeCommonVO.setDeviceId(deviceId);
|
||||
long waitTimeoutMs = inputParams.getLongValue("waitTimeoutMs", AgvNavigationWaiter.DEFAULT_WAIT_TIMEOUT_MS);
|
||||
long pollIntervalMs = inputParams.getLongValue("pollIntervalMs", AgvNavigationWaiter.DEFAULT_POLL_INTERVAL_MS);
|
||||
AgvUtils.AgvNavigationStatus status = AgvNavigationWaiter.navigateToStationAndWait(
|
||||
Agv.AgvNavigationStatus status = AgvNavigationWaiter.navigateToStationAndWait(
|
||||
edgeAgvService, edgeCommonVO, stationId.trim(), waitTimeoutMs, pollIntervalMs,
|
||||
navigationOptions(inputParams));
|
||||
return TaskNodeExecuteResult.success(new JSONObject()
|
||||
@ -106,13 +106,13 @@ public class EdgeAgvOperateService implements EdgeOperateService {
|
||||
return TaskNodeExecuteResult.success();
|
||||
}
|
||||
|
||||
private AgvUtils.AgvMotionOptions navigationOptions(JSONObject inputParams) {
|
||||
private Agv.AgvMotionOptions navigationOptions(JSONObject inputParams) {
|
||||
int blockedTimeoutMs = inputParams.getIntValue(
|
||||
"blockedTimeoutMs", EdgeAgvService.DEFAULT_BLOCKED_TIMEOUT_MS);
|
||||
if (blockedTimeoutMs <= 0) {
|
||||
blockedTimeoutMs = EdgeAgvService.DEFAULT_BLOCKED_TIMEOUT_MS;
|
||||
}
|
||||
return AgvUtils.AgvMotionOptions.newBuilder()
|
||||
return Agv.AgvMotionOptions.newBuilder()
|
||||
.setAsynchronous(false)
|
||||
.setBlockedTimeoutMs(blockedTimeoutMs)
|
||||
.build();
|
||||
|
||||
@ -89,4 +89,5 @@ public class EdgeDeviceCommandOperateService implements EdgeOperateService {
|
||||
return responseJson;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
package com.cmvr.test.flow.runtime.operator.edge;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.cmvr.common.exception.GlobalException;
|
||||
import com.cmvr.edge.client.model.arm.EdgeArmMoveJVO;
|
||||
import com.cmvr.edge.client.service.EdgeArmService;
|
||||
import com.cmvr.edge.client.model.hlc.EdgeTouchVO;
|
||||
import com.cmvr.edge.client.service.EdgeHlcService;
|
||||
import com.cmvr.test.enums.ActionEnum;
|
||||
import com.cmvr.test.flow.runtime.message.TaskNodeExecuteMessage;
|
||||
import com.cmvr.test.flow.runtime.message.TaskNodeExecuteResult;
|
||||
@ -12,47 +14,210 @@ import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
|
||||
/** Executes a screen touch through the edge HLC service. */
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class EdgeTouchOperateService implements EdgeOperateService {
|
||||
|
||||
private final EdgeArmService edgeArmService;
|
||||
private static final double DEFAULT_MAX_FORCE = 4D;
|
||||
|
||||
private final EdgeHlcService edgeHlcService;
|
||||
private final EdgeDeviceCommandOperateService edgeDeviceCommandOperateService;
|
||||
|
||||
@Override
|
||||
public boolean supports(ActionEnum action) {
|
||||
return action.name().equals("TOUCH");
|
||||
return ActionEnum.TOUCH == action;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TaskNodeExecuteResult execute(TaskNodeExecuteMessage message) {
|
||||
ActionEnum action = message.getAction();
|
||||
JSONObject inputParams = message.getInputParams();
|
||||
String robotId = message.getRobotId();
|
||||
String deviceId = inputParams.getString("deviceId");
|
||||
String touchParams = inputParams.getString("touchParams");
|
||||
|
||||
if (Objects.requireNonNull(action) == ActionEnum.TOUCH) {
|
||||
EdgeArmMoveJVO edgeArmMoveJVO = JSON.parseObject(touchParams, EdgeArmMoveJVO.class);
|
||||
edgeArmMoveJVO.setRobotId(robotId);
|
||||
edgeArmMoveJVO.setDeviceId(deviceId);
|
||||
|
||||
edgeArmService.moveJ(
|
||||
edgeArmMoveJVO,
|
||||
edgeArmMoveJVO.getTarget(),
|
||||
edgeArmMoveJVO.getVelocity(),
|
||||
edgeArmMoveJVO.getAcceleration(),
|
||||
edgeArmMoveJVO.getBlendRadius(),
|
||||
edgeArmMoveJVO.getJointVelocityLimits(),
|
||||
edgeArmMoveJVO.getAsynchronous()
|
||||
);
|
||||
} else {
|
||||
throw new GlobalException("不支持的触控操作类型: " + action);
|
||||
if (1 == 1) {
|
||||
try {
|
||||
Thread.sleep(15000);
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return TaskNodeExecuteResult.success();
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -67,10 +67,8 @@ public class TiTouchOperateService implements EdgeOperateService {
|
||||
String clickFunction = inputParams.getString("funcId");
|
||||
// 查询功能
|
||||
JSONObject funcObj = exTiVehicleFunctionService.findNextClickToFunction(vehicleConfigId, currentLocation, clickFunction);
|
||||
// 输出
|
||||
output.put("nextLocation", funcObj.get("nextLocation"));
|
||||
output.put("isEnd", funcObj.get("isEnd"));
|
||||
output.put("iconUrl", funcObj.get("iconUrl"));
|
||||
// 透传路径、动作类型和动作参数,供触控节点执行及报告追溯。
|
||||
output.putAll(funcObj);
|
||||
break;
|
||||
}
|
||||
case TI_TOUCH_COORDINATES: {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
package com.cmvr.test.service;
|
||||
|
||||
import cmvr.msgs.AgvUtils;
|
||||
import cmvr.msgs.Agv;
|
||||
import cn.hutool.core.util.ObjUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
@ -261,7 +261,7 @@ public class FlowActionExecutorService {
|
||||
}
|
||||
|
||||
// 构建目标位姿
|
||||
AgvUtils.AgvPose2d pose = AgvUtils.AgvPose2d.newBuilder()
|
||||
Agv.AgvPose2d pose = Agv.AgvPose2d.newBuilder()
|
||||
.setX(x)
|
||||
.setY(y)
|
||||
.setTheta(theta != null ? theta : 0.0)
|
||||
@ -295,13 +295,13 @@ public class FlowActionExecutorService {
|
||||
}
|
||||
}
|
||||
|
||||
private AgvUtils.AgvMotionOptions buildAgvNavigationOptions(JSONObject payload) {
|
||||
private Agv.AgvMotionOptions buildAgvNavigationOptions(JSONObject payload) {
|
||||
int blockedTimeoutMs = payload.getIntValue(
|
||||
"blockedTimeoutMs", EdgeAgvService.DEFAULT_BLOCKED_TIMEOUT_MS);
|
||||
if (blockedTimeoutMs <= 0) {
|
||||
blockedTimeoutMs = EdgeAgvService.DEFAULT_BLOCKED_TIMEOUT_MS;
|
||||
}
|
||||
return AgvUtils.AgvMotionOptions.newBuilder()
|
||||
return Agv.AgvMotionOptions.newBuilder()
|
||||
.setAsynchronous(false)
|
||||
.setBlockedTimeoutMs(blockedTimeoutMs)
|
||||
.build();
|
||||
|
||||
@ -62,5 +62,8 @@ public interface ITeNodeInstService extends IService<TeNodeInst> {
|
||||
|
||||
/** Marks unfinished node records as stopped when their in-memory execution is gone. */
|
||||
void stopUnfinished(String instId, String message);
|
||||
|
||||
/** Marks all still-running node records as failed when the owning task fails. */
|
||||
void failUnfinished(String instId, String message);
|
||||
}
|
||||
|
||||
|
||||
@ -11,4 +11,10 @@ public interface ExTiVehicleFunctionService {
|
||||
|
||||
public JSONObject findNextClickToFunction(Long vehicleConfigId, String currentUi, String clickFunction);
|
||||
|
||||
/** 返回车型的界面节点、导航边以及配置问题,供后台可视化展示。 */
|
||||
JSONObject getTopology(Long vehicleConfigId);
|
||||
|
||||
/** 检查项目所选功能能否从任意已配置界面到达。 */
|
||||
JSONObject inspectTopology(Long vehicleConfigId, java.util.List<Long> functionIds);
|
||||
|
||||
}
|
||||
|
||||
@ -154,6 +154,17 @@ public class ITeNodeInstServiceImpl extends ServiceImpl<TeNodeInstMapper, TeNode
|
||||
.set(TeNodeInst::getEndTime, System.currentTimeMillis());
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -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"));
|
||||
}
|
||||
}
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
@ -7,12 +7,18 @@ import com.cmvr.test.flow.builder.FlowParamDef;
|
||||
import com.cmvr.test.flow.context.TaskContext;
|
||||
import com.cmvr.test.flow.context.TaskContextManager;
|
||||
import com.cmvr.test.flow.context.TaskInstHolder;
|
||||
import com.cmvr.test.flow.context.TaskThreadRegistry;
|
||||
import com.cmvr.test.flow.runtime.dispatcher.FlowNodeTypeDispatch;
|
||||
import com.cmvr.test.flow.runtime.engine.support.FlowExecutionChainBuilder;
|
||||
import com.cmvr.test.flow.runtime.message.TaskNodeExecuteMessage;
|
||||
import com.cmvr.test.flow.runtime.message.TaskNodeExecuteResult;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
@ -45,6 +51,45 @@ public class FlowItemExecutorTest {
|
||||
assertEquals(start.getNodeId(), executor.executedNodeId);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doesNotScheduleSuccessorsWhenNodeEndsTheTask() {
|
||||
TaskContextManager contextManager = new TaskContextManager();
|
||||
TaskInstHolder taskInstHolder = new TaskInstHolder(null, contextManager, null, null);
|
||||
TaskContext context = new TaskContext();
|
||||
context.setInstId("inst-1");
|
||||
taskInstHolder.register(context);
|
||||
|
||||
RecordingScheduler scheduler = new RecordingScheduler();
|
||||
FlowNodeTypeDispatch dispatch = new FlowNodeTypeDispatch(Map.of(
|
||||
NodeTypeEnum.FUNCTION.name(), message -> {
|
||||
taskInstHolder.unRegister(message.getInstId());
|
||||
return TaskNodeExecuteResult.success();
|
||||
}));
|
||||
FlowItemExecutor executor = new FlowItemExecutor(
|
||||
scheduler, List.of(), dispatch, taskInstHolder,
|
||||
new FlowExecutionChainBuilder(), new TaskThreadRegistry(),
|
||||
new FlowRuntimeAssignmentResolver(), null);
|
||||
|
||||
FlowNodeWrapper node = new FlowNodeWrapper();
|
||||
node.setNodeId("node-1");
|
||||
node.setNodeName("node-1");
|
||||
node.setNodeType(NodeTypeEnum.FUNCTION);
|
||||
LinkedHashMap<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 String executedNodeId;
|
||||
|
||||
@ -63,4 +108,19 @@ public class FlowItemExecutorTest {
|
||||
executedNodeId = node.getNodeId();
|
||||
}
|
||||
}
|
||||
|
||||
private static class RecordingScheduler extends FlowTaskScheduler {
|
||||
private final AtomicInteger completed = new AtomicInteger();
|
||||
|
||||
private RecordingScheduler() {
|
||||
super(null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void markCompleted(String instId, String nodeId, FlowGraph graph,
|
||||
FlowItemExecutor.NodeExecutor executorFunc,
|
||||
List<Integer> iterations) {
|
||||
completed.incrementAndGet();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
package com.cmvr.test.flow.runtime.operator.edge;
|
||||
|
||||
import cmvr.msgs.AgvUtils;
|
||||
import cmvr.msgs.Agv;
|
||||
import com.cmvr.common.exception.GlobalException;
|
||||
import com.cmvr.edge.client.model.EdgeCommonVO;
|
||||
import com.cmvr.edge.client.service.EdgeAgvService;
|
||||
@ -25,7 +25,7 @@ public class AgvNavigationWaiterTest {
|
||||
AtomicInteger queryCount = new AtomicInteger();
|
||||
EdgeAgvService service = service(states, queryCount, new AtomicBoolean());
|
||||
|
||||
AgvUtils.AgvNavigationStatus result = AgvNavigationWaiter.navigateToStationAndWait(
|
||||
Agv.AgvNavigationStatus result = AgvNavigationWaiter.navigateToStationAndWait(
|
||||
service, common(), "station-A", 1000, 1);
|
||||
|
||||
assertEquals(4, result.getState());
|
||||
@ -79,7 +79,7 @@ public class AgvNavigationWaiterTest {
|
||||
if (next != null) {
|
||||
lastState.set(next);
|
||||
}
|
||||
return AgvUtils.AgvNavigationStatus.newBuilder()
|
||||
return Agv.AgvNavigationStatus.newBuilder()
|
||||
.setState(lastState.get())
|
||||
.setProgress(lastState.get() == 4 ? 1.0 : 0.5)
|
||||
.setMessage(lastState.get() == 5 ? "navigation failed" : "")
|
||||
|
||||
@ -27,33 +27,31 @@ public class EdgeActionQueueOperateServiceTest {
|
||||
captured.set((EdgeActionQueueVO) args[0]);
|
||||
return SystemCommand.ActionQueueCommand.Feedback.newBuilder()
|
||||
.setHeader(Common.CommandHeader.Feedback.newBuilder().setSuccess(true))
|
||||
.setActionId("action-1")
|
||||
.setCompletedSteps(2)
|
||||
.setResult(SystemCommand.ActionResultCode.ACTION_RESULT_CODE_COMPLETED)
|
||||
.setServiceInstanceId("instance-1")
|
||||
.setDeduplicationStatus(SystemCommand.ActionDeduplicationStatus.ACTION_DEDUPLICATION_STATUS_ACCEPTED_NEW)
|
||||
.build();
|
||||
});
|
||||
EdgeActionQueueOperateService service = new EdgeActionQueueOperateService(edgeSystemService);
|
||||
|
||||
JSONArray steps = new JSONArray()
|
||||
.fluentAdd(new JSONObject().fluentPut("stepId", "move")
|
||||
.fluentPut("type", "AGV_NAVIGATE_TO_STATION")
|
||||
.fluentPut("deviceBindingKey", "queue_agv")
|
||||
.fluentPut("params", new JSONObject().fluentPut("stationId", "station-2")))
|
||||
.fluentPut("type", "ARM_MOVE_J")
|
||||
.fluentPut("deviceBindingKey", "queue_arm")
|
||||
.fluentPut("params", new JSONObject().fluentPut("positions", new JSONArray().fluentAdd(0D))))
|
||||
.fluentAdd(new JSONObject().fluentPut("stepId", "wait")
|
||||
.fluentPut("type", "DELAY")
|
||||
.fluentPut("params", new JSONObject().fluentPut("durationMs", 500)));
|
||||
.fluentPut("type", "ARM_MOVE_L")
|
||||
.fluentPut("deviceId", "arm-2")
|
||||
.fluentPut("params", new JSONObject().fluentPut("x", 0.1D)));
|
||||
TaskNodeExecuteMessage message = new TaskNodeExecuteMessage();
|
||||
message.setAction(ActionEnum.SYSTEM_EXECUTE_ACTION_QUEUE);
|
||||
message.setRobotId("robot-1");
|
||||
message.setResolvedDeviceBindings(new JSONObject().fluentPut("queue_agv", "agv-2"));
|
||||
message.setResolvedDeviceBindings(new JSONObject().fluentPut("queue_arm", "arm-1"));
|
||||
message.setInputParams(new JSONObject().fluentPut("totalTimeoutMs", 10000).fluentPut("steps", steps));
|
||||
|
||||
TaskNodeExecuteResult result = service.execute(message);
|
||||
|
||||
assertTrue(result.isSuccess());
|
||||
assertEquals("agv-2", captured.get().getSteps().get(0).getDeviceId());
|
||||
assertEquals("arm-1", captured.get().getSteps().get(0).getDeviceId());
|
||||
assertEquals("robot-1", captured.get().getRobotId());
|
||||
assertEquals(2, result.getOutputParams().getIntValue("completedSteps"));
|
||||
}
|
||||
|
||||
@ -108,4 +108,5 @@ public class EdgeDeviceCommandOperateServiceTest {
|
||||
.setResponseJson(responseJson)
|
||||
.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -27,4 +27,19 @@ public class TiVehicleFunction extends BaseEntity {
|
||||
|
||||
@ApiModelProperty("图标URL")
|
||||
private String iconUrl;
|
||||
|
||||
@ApiModelProperty("操作类型:CLICK、LONG_PRESS、SWIPE、EDGE_SCRIPT")
|
||||
private String operationType;
|
||||
|
||||
@ApiModelProperty("点击后跳转的界面字典键值;为空时按功能名称自动推断")
|
||||
private String targetUiKey;
|
||||
|
||||
@ApiModelProperty("操作参数JSON,例如长按时长、滑动终点或边端脚本参数")
|
||||
private String operationParams;
|
||||
|
||||
@ApiModelProperty("单次步骤重复次数")
|
||||
private Integer repeatCount;
|
||||
|
||||
@ApiModelProperty("重复动作及下一步前等待时间(毫秒)")
|
||||
private Integer waitMs;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -6,6 +6,7 @@ import com.cmvr.test.model.domain.TeTaskInst;
|
||||
import com.cmvr.test.model.vo.TeQueryTaskOrchestraItemVO;
|
||||
import com.cmvr.test.model.vo.TeTaskExecuteProjectVO;
|
||||
import com.cmvr.ti.model.domain.TiProject;
|
||||
import com.cmvr.ti.model.vo.TiProjectReadinessVO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@ -22,6 +23,8 @@ public interface ITiProjectService extends IService<TiProject> {
|
||||
|
||||
public List<TeQueryTaskOrchestraItemVO> queryExecuteRunParams(String projectId);
|
||||
|
||||
public TiProjectReadinessVO checkExecutionReadiness(String projectId);
|
||||
|
||||
public String executeProject(TeTaskExecuteProjectVO taskExecuteProjectVO);
|
||||
|
||||
public TeTaskInst getTeTaskInst(String projectId);
|
||||
|
||||
@ -2,15 +2,13 @@ package com.cmvr.ti.service.ex;
|
||||
|
||||
import cn.hutool.core.text.StrPool;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.cmvr.common.core.domain.entity.SysDictData;
|
||||
import com.cmvr.common.exception.GlobalException;
|
||||
import com.cmvr.common.utils.StringUtils;
|
||||
import com.cmvr.system.domain.vo.CarIcon;
|
||||
import com.cmvr.system.domain.vo.CarUi;
|
||||
import com.cmvr.system.service.ISysDictTypeService;
|
||||
import com.cmvr.system.util.CarPathFinderUtil;
|
||||
import com.cmvr.test.service.ex.ExTiVehicleFunctionService;
|
||||
import com.cmvr.ti.model.domain.TiVehicleFunction;
|
||||
import com.cmvr.ti.model.domain.TiVehicleUi;
|
||||
@ -19,25 +17,33 @@ import com.cmvr.ti.service.ITiVehicleFunctionService;
|
||||
import com.cmvr.ti.service.ITiVehicleUiService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ExTiVehicleFunctionServiceImpl implements ExTiVehicleFunctionService {
|
||||
|
||||
private final ITiVehicleFunctionService tiVehicleFunctionService;
|
||||
|
||||
private final ITiVehicleUiService tiVehicleUiService;
|
||||
|
||||
private final ISysDictTypeService iSysDictTypeService;
|
||||
|
||||
private final ISysDictTypeService sysDictTypeService;
|
||||
|
||||
@Override
|
||||
public JSONArray queryByIds(String ids) {
|
||||
List<TiVehicleFunction> tiVehicleFunctions = tiVehicleFunctionService.listByIds(StrUtil.split(StrPool.COMMA, ids));
|
||||
return JSONArray.from(tiVehicleFunctions);
|
||||
if (StrUtil.isBlank(ids)) {
|
||||
return new JSONArray();
|
||||
}
|
||||
return JSONArray.from(tiVehicleFunctionService.listByIds(StrUtil.split(StrPool.COMMA, ids)));
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -45,86 +51,347 @@ public class ExTiVehicleFunctionServiceImpl implements ExTiVehicleFunctionServic
|
||||
return JSONObject.from(tiVehicleFunctionService.getById(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 寻找从当前页面到能执行目标功能的最短点击路径
|
||||
* 返回:点击的功能序列(JSONArray 包装 List<TiVehicleFunction>)
|
||||
*/
|
||||
@Override
|
||||
public JSONObject findNextClickToFunction(
|
||||
Long vehicleConfigId,
|
||||
String startUiName,
|
||||
String targetFuncName) {
|
||||
// 功能字典
|
||||
// 实际用的是 sys_dict_data,这里假设你有方法能查到 data 列表
|
||||
List<SysDictData> functionDictData = iSysDictTypeService.selectDictDataByType("ti_function_config");
|
||||
|
||||
// 界面字典
|
||||
List<SysDictData> uiDictData = iSysDictTypeService.selectDictDataByType("ti_ui_config");
|
||||
|
||||
// 构建 功能 value → label 的映射
|
||||
Map<String, String> funcValueToLabel = new HashMap<>();
|
||||
for (SysDictData data : functionDictData) {
|
||||
funcValueToLabel.put(data.getDictValue(), data.getDictLabel());
|
||||
public JSONObject findNextClickToFunction(Long vehicleConfigId, String startUiName, String targetFunction) {
|
||||
Graph graph = loadGraph(vehicleConfigId);
|
||||
if (!graph.issues.isEmpty()) {
|
||||
throw new GlobalException("界面路径配置不完整:" + String.join(";", graph.issues));
|
||||
}
|
||||
String startKey = graph.resolveUiKey(startUiName);
|
||||
if (startKey == null) {
|
||||
throw new GlobalException("未识别到已配置的当前界面:" + startUiName);
|
||||
}
|
||||
TiVehicleFunction target = graph.resolveFunction(targetFunction);
|
||||
if (target == null) {
|
||||
throw new GlobalException("目标功能不存在或不属于当前车型:" + targetFunction);
|
||||
}
|
||||
List<Step> path = graph.findPath(startKey, target.getId());
|
||||
if (path.isEmpty()) {
|
||||
throw new GlobalException("从“" + graph.uiName(startKey) + "”无法到达功能“"
|
||||
+ graph.functionName(target) + "”,请补充界面功能或目标界面配置");
|
||||
}
|
||||
|
||||
// 构建 界面 value → label 的映射
|
||||
Map<String, String> uiValueToLabel = new HashMap<>();
|
||||
for (SysDictData data : uiDictData) {
|
||||
uiValueToLabel.put(data.getDictValue(), data.getDictLabel());
|
||||
Step next = path.get(0);
|
||||
JSONObject result = stepJson(graph, next);
|
||||
result.put("nextLocation", graph.functionName(next.function));
|
||||
result.put("isEnd", path.size() == 1);
|
||||
result.put("iconUrl", next.function.getIconUrl());
|
||||
JSONArray pathJson = new JSONArray();
|
||||
for (Step step : path) {
|
||||
pathJson.add(stepJson(graph, step));
|
||||
}
|
||||
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(
|
||||
TiVehicleUi.builder().vehicleConfigId(vehicleConfigId).build());
|
||||
|
||||
List<TiVehicleFunction> funcList = tiVehicleFunctionService.selectTiVehicleFunctionList(
|
||||
List<TiVehicleFunction> functionList = tiVehicleFunctionService.selectTiVehicleFunctionList(
|
||||
TiVehicleFunctionVO.builder().vehicleConfigId(vehicleConfigId).build());
|
||||
|
||||
// 2. funcKey → TiVehicleFunction
|
||||
Map<Long, TiVehicleFunction> funcById = new HashMap<>();
|
||||
for (TiVehicleFunction f : funcList) {
|
||||
funcById.put(f.getId(), f);
|
||||
return new Graph(uiList, functionList, uiNames, functionNames);
|
||||
}
|
||||
|
||||
private Map<String, String> dictMap(String type) {
|
||||
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) {
|
||||
icons = new ArrayList<>();
|
||||
for (String funcId : StringUtils.split(ui.getFuncIds(), StrPool.COMMA)) {
|
||||
TiVehicleFunction func = funcById.get(Long.valueOf(funcId));
|
||||
CarIcon icon = new CarIcon(
|
||||
funcValueToLabel.get(func.getFuncKey()),
|
||||
func.getIconUrl(),
|
||||
"",
|
||||
"",
|
||||
"1".equals(func.getRemark()) ? funcValueToLabel.get(func.getFuncKey()) : ""
|
||||
);
|
||||
icons.add(icon);
|
||||
if (StrUtil.isBlank(ui.getUiKey()) || StrUtil.isBlank(uiName(ui.getUiKey()))) {
|
||||
issues.add("界面ID " + ui.getId() + " 未配置有效的界面类型");
|
||||
continue;
|
||||
}
|
||||
carUi = new CarUi(
|
||||
uiValueToLabel.get(ui.getUiKey()),
|
||||
ui.getUiUrl(),
|
||||
icons
|
||||
);
|
||||
uiMap.put(carUi.getUiName(), carUi);
|
||||
if (uis.put(ui.getUiKey(), ui) != null) {
|
||||
issues.add("界面“" + uiName(ui.getUiKey()) + "”重复配置");
|
||||
}
|
||||
if (StrUtil.isBlank(ui.getUiUrl())) {
|
||||
issues.add("界面“" + uiName(ui.getUiKey()) + "”缺少识别截图");
|
||||
}
|
||||
String[] rawIds = StringUtils.split(ui.getFuncIds(), StrPool.COMMA);
|
||||
if (rawIds == null || rawIds.length == 0) {
|
||||
issues.add("界面“" + uiName(ui.getUiKey()) + "”未关联任何功能");
|
||||
continue;
|
||||
}
|
||||
for (String rawId : rawIds) {
|
||||
if (StrUtil.isBlank(rawId)) continue;
|
||||
try {
|
||||
Long id = Long.valueOf(rawId);
|
||||
attachedFunctionIds.add(id);
|
||||
TiVehicleFunction function = functions.get(id);
|
||||
if (function == null) {
|
||||
issues.add("界面“" + uiName(ui.getUiKey()) + "”关联了无效功能ID " + id);
|
||||
} else if (StrUtil.isBlank(function.getIconUrl())) {
|
||||
issues.add("功能“" + functionName(function) + "”缺少识别图标");
|
||||
}
|
||||
} catch (NumberFormatException exception) {
|
||||
issues.add("界面“" + uiName(ui.getUiKey()) + "”包含非法功能ID " + rawId);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (TiVehicleFunction function : functions.values()) {
|
||||
String target = targetUiKey(function);
|
||||
if (StrUtil.isNotBlank(target) && !uis.containsKey(target)) {
|
||||
issues.add("功能“" + functionName(function) + "”跳转到未配置界面“"
|
||||
+ uiName(target) + "”");
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,6 +5,8 @@ import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.ObjUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
@ -21,18 +23,22 @@ import com.cmvr.test.enums.RunModeEnum;
|
||||
import com.cmvr.test.flow.runtime.engine.FlowTaskRuntimeService;
|
||||
import com.cmvr.test.flow.runtime.event.FlowExecutionModuleCodes;
|
||||
import com.cmvr.test.model.domain.TeDetectionItem;
|
||||
import com.cmvr.test.model.domain.TeDetectionItemVersion;
|
||||
import com.cmvr.test.model.domain.TeTaskConfigInfo;
|
||||
import com.cmvr.test.model.domain.TeTaskInst;
|
||||
import com.cmvr.test.model.vo.TeQueryTaskOrchestraItemVO;
|
||||
import com.cmvr.test.model.vo.TeTaskExecuteProjectVO;
|
||||
import com.cmvr.test.model.vo.TeTaskOrchestraVO;
|
||||
import com.cmvr.test.service.ITeDetectionItemService;
|
||||
import com.cmvr.test.service.ITeDetectionItemVersionService;
|
||||
import com.cmvr.test.service.ITeTaskConfigInfoService;
|
||||
import com.cmvr.test.service.ITeTaskInstService;
|
||||
import com.cmvr.test.service.ITeTaskOrchestrationService;
|
||||
import com.cmvr.test.service.ex.ExTiVehicleFunctionService;
|
||||
import com.cmvr.ti.mapper.TiProjectMapper;
|
||||
import com.cmvr.ti.model.domain.TiProject;
|
||||
import com.cmvr.ti.model.domain.TiVehicleFunction;
|
||||
import com.cmvr.ti.model.vo.TiProjectReadinessVO;
|
||||
import com.cmvr.ti.service.ITiProjectService;
|
||||
import com.cmvr.ti.service.ITiVehicleFunctionService;
|
||||
import com.cmvr.ti.support.TiEvaluationRecorder;
|
||||
@ -42,9 +48,16 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@ -52,10 +65,12 @@ public class TiProjectServiceImpl extends ServiceImpl<TiProjectMapper, TiProject
|
||||
|
||||
private final ITeTaskOrchestrationService taskOrchestrationService;
|
||||
private final ITeDetectionItemService detectionItemService;
|
||||
private final ITeDetectionItemVersionService detectionItemVersionService;
|
||||
private final ITeTaskConfigInfoService taskConfigInfoService;
|
||||
private final ITeTaskInstService taskInstService;
|
||||
private final FlowTaskRuntimeService flowTaskRuntimeService;
|
||||
private final ITiVehicleFunctionService tiVehicleFunctionService;
|
||||
private final ExTiVehicleFunctionService exTiVehicleFunctionService;
|
||||
private final IAeEvaluationService aeEvaluationService;
|
||||
private final AeIndicatorService aeIndicatorService;
|
||||
private final TiEvaluationRecorder evaluationRecorder;
|
||||
@ -76,21 +91,7 @@ public class TiProjectServiceImpl extends ServiceImpl<TiProjectMapper, TiProject
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int insertTiProject(TiProject tiProject) {
|
||||
// 校验功能ID是否属于同一车型配置
|
||||
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不属于同一车型配置");
|
||||
}
|
||||
requireProjectFunctions(tiProject);
|
||||
|
||||
tiProject.setStatus("0");
|
||||
int insert = this.baseMapper.insert(tiProject);
|
||||
@ -112,6 +113,16 @@ public class TiProjectServiceImpl extends ServiceImpl<TiProjectMapper, TiProject
|
||||
|
||||
@Override
|
||||
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);
|
||||
}
|
||||
|
||||
@ -126,15 +137,104 @@ public class TiProjectServiceImpl extends ServiceImpl<TiProjectMapper, TiProject
|
||||
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
|
||||
public String executeProject(TeTaskExecuteProjectVO taskExecuteProjectVO) {
|
||||
TiProject project = getById(taskExecuteProjectVO.getProjectId());
|
||||
if (project == null) {
|
||||
throw new GlobalException("触控项目不存在:" + taskExecuteProjectVO.getProjectId());
|
||||
}
|
||||
// TiProjectReadinessVO readiness = checkExecutionReadiness(project.getProjectId());
|
||||
// if (!readiness.isReady()) {
|
||||
// String reasons = readiness.getChecks().stream()
|
||||
// .filter(item -> !item.isPassed())
|
||||
// .map(TiProjectReadinessVO.CheckItem::getMessage)
|
||||
// .collect(Collectors.joining(";"));
|
||||
// throw new GlobalException("项目暂不具备执行条件:" + reasons);
|
||||
// }
|
||||
TeTaskExecuteProjectVO normalVO = new TeTaskExecuteProjectVO();
|
||||
normalVO.setProjectId(taskExecuteProjectVO.getProjectId());
|
||||
normalVO.setRunParams(taskExecuteProjectVO.getRunParams());
|
||||
normalVO.setRunParams(buildProjectRunParams(project, taskExecuteProjectVO.getRunParams()));
|
||||
normalVO.setRobotId(taskExecuteProjectVO.getRobotId());
|
||||
normalVO.setRuntimeAssignments(taskExecuteProjectVO.getRuntimeAssignments());
|
||||
normalVO.setRunMode(RunModeEnum.TI_PROJECT);
|
||||
@ -245,6 +345,9 @@ public class TiProjectServiceImpl extends ServiceImpl<TiProjectMapper, TiProject
|
||||
itemDto.setFailureReason(dto.getFailureReason());
|
||||
itemDto.setVideoPath(StrUtil.blankToDefault(dto.getVideoPath(), evaluation.getVideoPath()));
|
||||
itemDto.setExecutionDetail(dto.getExecutionDetail());
|
||||
itemDto.setExecutionSteps(dto.getExecutionSteps());
|
||||
itemDto.setUiPath(dto.getUiPath());
|
||||
itemDto.setScreenshotUrls(dto.getScreenshotUrls());
|
||||
itemDetails.add(itemDto);
|
||||
}
|
||||
|
||||
@ -269,6 +372,171 @@ public class TiProjectServiceImpl extends ServiceImpl<TiProjectMapper, TiProject
|
||||
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) {
|
||||
if (StrUtil.isBlank(resultJson)) {
|
||||
return new AeIResultDTO();
|
||||
|
||||
@ -8,6 +8,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.spring.service.impl.ServiceImpl;
|
||||
import com.cmvr.common.core.domain.entity.SysDictData;
|
||||
import com.cmvr.common.exception.GlobalException;
|
||||
import com.cmvr.system.service.ISysDictDataService;
|
||||
import com.cmvr.system.service.ISysDictTypeService;
|
||||
import com.cmvr.ti.mapper.TiVehicleFunctionMapper;
|
||||
@ -40,7 +41,8 @@ public class TiVehicleFunctionServiceImpl extends ServiceImpl<TiVehicleFunctionM
|
||||
@Override
|
||||
public JSONObject selectTiVehicleFunctionDetailById(Long 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;
|
||||
}
|
||||
|
||||
@ -60,17 +62,53 @@ public class TiVehicleFunctionServiceImpl extends ServiceImpl<TiVehicleFunctionM
|
||||
}
|
||||
|
||||
@Override
|
||||
public int insertTiVehicleFunction(TiVehicleFunction TiVehicleFunction) {
|
||||
return this.baseMapper.insert(TiVehicleFunction);
|
||||
public int insertTiVehicleFunction(TiVehicleFunction tiVehicleFunction) {
|
||||
normalizeAndValidate(tiVehicleFunction);
|
||||
return this.baseMapper.insert(tiVehicleFunction);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int updateTiVehicleFunction(TiVehicleFunction TiVehicleFunction) {
|
||||
return this.baseMapper.updateById(TiVehicleFunction);
|
||||
public int updateTiVehicleFunction(TiVehicleFunction tiVehicleFunction) {
|
||||
normalizeAndValidate(tiVehicleFunction);
|
||||
return this.baseMapper.updateById(tiVehicleFunction);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int deleteTiVehicleFunctionByIds(Long[] 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对象");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,13 +6,19 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.spring.service.impl.ServiceImpl;
|
||||
import com.cmvr.ti.mapper.TiVehicleUiMapper;
|
||||
import com.cmvr.common.exception.GlobalException;
|
||||
import com.cmvr.common.utils.StringUtils;
|
||||
import com.cmvr.ti.model.domain.TiVehicleFunction;
|
||||
import com.cmvr.ti.model.domain.TiVehicleUi;
|
||||
import com.cmvr.ti.service.ITiVehicleFunctionService;
|
||||
import com.cmvr.ti.service.ITiVehicleUiService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 车机界面Service业务层处理
|
||||
@ -21,6 +27,8 @@ import java.util.List;
|
||||
@RequiredArgsConstructor
|
||||
public class TiVehicleUiServiceImpl extends ServiceImpl<TiVehicleUiMapper, TiVehicleUi> implements ITiVehicleUiService {
|
||||
|
||||
private final ITiVehicleFunctionService tiVehicleFunctionService;
|
||||
|
||||
@Override
|
||||
public TiVehicleUi selectTiVehicleUiById(Long id) {
|
||||
return this.baseMapper.selectById(id);
|
||||
@ -37,11 +45,13 @@ public class TiVehicleUiServiceImpl extends ServiceImpl<TiVehicleUiMapper, TiVeh
|
||||
|
||||
@Override
|
||||
public int insertTiVehicleUi(TiVehicleUi tiVehicleUi) {
|
||||
validate(tiVehicleUi);
|
||||
return this.baseMapper.insert(tiVehicleUi);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int updateTiVehicleUi(TiVehicleUi tiVehicleUi) {
|
||||
validate(tiVehicleUi);
|
||||
return this.baseMapper.updateById(tiVehicleUi);
|
||||
}
|
||||
|
||||
@ -49,4 +59,43 @@ public class TiVehicleUiServiceImpl extends ServiceImpl<TiVehicleUiMapper, TiVeh
|
||||
public int deleteTiVehicleUiByIds(Long[] 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("关联功能必须全部属于当前车型");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.cmvr.common.core.domain.entity.SysDictData;
|
||||
import com.cmvr.evaluation.model.domain.AeEvaluation;
|
||||
@ -59,6 +60,9 @@ public class TiEvaluationRecorder {
|
||||
private final ConcurrentMap<String, String> latestVideoUrls = new ConcurrentHashMap<>();
|
||||
private final ConcurrentMap<String, String> businessFailures = 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. */
|
||||
public void initialize(String projectId, String instId) {
|
||||
@ -240,6 +244,9 @@ public class TiEvaluationRecorder {
|
||||
result.setResponseTime(start == null ? null : Math.max(0L, System.currentTimeMillis() - start));
|
||||
result.setVideoPath(videoUrl);
|
||||
result.setExecutionDetail(buildExecutionDetail(event.getInstId(), evaluation.getAeId()));
|
||||
result.setExecutionSteps(buildExecutionSteps(event.getInstId(), evaluation.getAeId()));
|
||||
result.setUiPath(buildStringEvidence(uiPaths, event.getInstId(), evaluation.getAeId()));
|
||||
result.setScreenshotUrls(buildStringEvidence(screenshotUrls, event.getInstId(), evaluation.getAeId()));
|
||||
|
||||
evaluation.setStatus(success ? STATUS_SUCCESS : STATUS_FAILED);
|
||||
evaluation.setVideoPath(videoUrl);
|
||||
@ -334,6 +341,46 @@ public class TiEvaluationRecorder {
|
||||
String nodeName = StrUtil.blankToDefault(event.getNodeName(), event.getNodeId());
|
||||
String detail = StrUtil.blankToDefault(nodeName, "流程节点") + statusText;
|
||||
executionDetails.computeIfAbsent(key, ignored -> new ConcurrentLinkedQueue<>()).add(detail);
|
||||
if (event.getEventType() != FlowExecutionEvent.EventType.NODE_STARTED) {
|
||||
recordStructuredStep(event, key, nodeName, statusText);
|
||||
}
|
||||
}
|
||||
|
||||
private void recordStructuredStep(FlowExecutionEvent event, String key,
|
||||
String nodeName, String statusText) {
|
||||
JSONObject output = event.getOutputParams() == null ? new JSONObject() : event.getOutputParams();
|
||||
JSONObject source = output.getJSONObject("result");
|
||||
if (source == null) source = output;
|
||||
|
||||
AeIResultDTO.ExecutionStep step = new AeIResultDTO.ExecutionStep();
|
||||
step.setTimestamp(System.currentTimeMillis());
|
||||
step.setNodeName(StrUtil.blankToDefault(nodeName, "流程节点"));
|
||||
step.setAction(event.getAction() == null ? null : event.getAction().getAction());
|
||||
step.setStatus(event.getEventType() == FlowExecutionEvent.EventType.NODE_FAILED ? "FAILED" : "SUCCESS");
|
||||
step.setPageName(firstNotBlank(source.getString("pageName"), source.getString("currentLocation")));
|
||||
step.setTargetName(firstNotBlank(source.getString("nextLocation"), source.getString("targetName")));
|
||||
step.setOperationType(source.getString("operationType"));
|
||||
Object coordinates = source.get("touchCoordinates");
|
||||
if (coordinates == null && source.containsKey("x") && source.containsKey("y")) {
|
||||
coordinates = new JSONArray().fluentAdd(source.get("x")).fluentAdd(source.get("y"));
|
||||
}
|
||||
step.setCoordinates(coordinates == null ? null : JSON.toJSONString(coordinates));
|
||||
step.setScreenshotUrl(FlowMediaParamResolver.lastUrl(source, "imageUrl"));
|
||||
step.setVideoUrl(FlowMediaParamResolver.lastUrl(source, "videoUrl"));
|
||||
step.setMessage(event.getEventType() == FlowExecutionEvent.EventType.NODE_FAILED
|
||||
? StrUtil.blankToDefault(event.getErrorMessage(), statusText) : statusText);
|
||||
executionSteps.computeIfAbsent(key, ignored -> new ConcurrentLinkedQueue<>()).add(step);
|
||||
|
||||
appendEvidence(uiPaths, key, step.getPageName());
|
||||
JSONArray path = source.getJSONArray("path");
|
||||
if (path != null) {
|
||||
for (int index = 0; index < path.size(); index++) {
|
||||
JSONObject pathStep = path.getJSONObject(index);
|
||||
appendEvidence(uiPaths, key, pathStep.getString("sourceUiName"));
|
||||
appendEvidence(uiPaths, key, pathStep.getString("targetUiName"));
|
||||
}
|
||||
}
|
||||
appendEvidence(screenshotUrls, key, step.getScreenshotUrl());
|
||||
}
|
||||
|
||||
private String buildExecutionDetail(String instId, String evaluationId) {
|
||||
@ -347,6 +394,45 @@ public class TiEvaluationRecorder {
|
||||
return StrUtil.sub(String.join(";", details.subList(fromIndex, details.size())), 0, 2000);
|
||||
}
|
||||
|
||||
private List<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) {
|
||||
if (StrUtil.isBlank(json)) {
|
||||
return Collections.emptyList();
|
||||
@ -394,6 +480,9 @@ public class TiEvaluationRecorder {
|
||||
private void clearRuntimeCache(String instId) {
|
||||
startedAt.keySet().removeIf(key -> key.startsWith(instId + "|"));
|
||||
executionDetails.keySet().removeIf(key -> key.startsWith(instId + "|"));
|
||||
executionSteps.keySet().removeIf(key -> key.startsWith(instId + "|"));
|
||||
uiPaths.keySet().removeIf(key -> key.startsWith(instId + "|"));
|
||||
screenshotUrls.keySet().removeIf(key -> key.startsWith(instId + "|"));
|
||||
latestVideoUrls.remove(instId);
|
||||
businessFailures.remove(instId);
|
||||
initializationLocks.remove(instId);
|
||||
|
||||
@ -47,6 +47,8 @@ public class TiEvaluationRecorderTest {
|
||||
assertEquals("成功", result.getOverallResult());
|
||||
assertEquals("http://files/touch-1.mp4", result.getVideoPath());
|
||||
assertTrue(result.getExecutionDetail().contains("结束录像执行完成"));
|
||||
assertEquals(1, result.getExecutionSteps().size());
|
||||
assertEquals("CAMERA_RECORDING_STOP", result.getExecutionSteps().get(0).getAction());
|
||||
assertEquals("2", fixture.project.getStatus());
|
||||
}
|
||||
|
||||
|
||||
26
sql/ti_touch_interaction_v2.sql
Normal file
26
sql/ti_touch_interaction_v2.sql
Normal 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;
|
||||
Loading…
Reference in New Issue
Block a user