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