ard-admin/src/main/java/com/ard/web/controller/route/RouteController.java
New file @@ -0,0 +1,65 @@ package com.ard.web.controller.route; import com.ard.common.core.domain.AjaxResult; import com.ard.work.config.RouteStateMachineService; import com.ard.work.domain.enums.RouteEvent; import com.ard.work.domain.enums.RouteState; import io.swagger.annotations.Api; import io.swagger.annotations.ApiImplicitParam; import io.swagger.annotations.ApiImplicitParams; import io.swagger.annotations.ApiOperation; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; @Api(tags = "航线状态机") @RestController @RequestMapping("/api/route") @RequiredArgsConstructor public class RouteController { @Autowired private RouteStateMachineService stateMachineService; /** * 发送航线事件 */ @ApiOperation("航线状态测试") @ApiImplicitParams({ @ApiImplicitParam(name = "boxSn", value = "设备编号", required = true, dataType = "String", paramType = "path"), @ApiImplicitParam(name = "event", value = "事件", required = true, dataType = "RouteEvent", paramType = "query") }) @PostMapping("/{boxSn}/event") public AjaxResult sendRouteEvent( @PathVariable String boxSn, @RequestParam RouteEvent event) { boolean success = stateMachineService.sendEvent(boxSn, event); if (success) { RouteState currentState = stateMachineService.getCurrentState(boxSn); return AjaxResult.success("事件执行成功,设备" + boxSn + "当前状态:" + currentState); } else { return AjaxResult.error("事件执行失败(状态流转不允许),设备" + boxSn + "当前状态:" + stateMachineService.getCurrentState(boxSn)); } } /** * 获取设备当前航线状态 */ @ApiOperation("获取设备当前航线状态") @GetMapping("/{boxSn}/state") public RouteState getRouteState(@PathVariable String boxSn) { return stateMachineService.getCurrentState(boxSn); } /** * 重置设备航线状态 */ @ApiOperation("重置设备航线状态") @PostMapping("/{boxSn}/reset") public String resetRouteState(@PathVariable String boxSn) { stateMachineService.resetStateMachine(boxSn); return "设备" + boxSn + "航线状态已重置为:" + RouteState.IDLE; } } ard-work/pom.xml
@@ -113,6 +113,11 @@ <groupId>com.ard</groupId> <artifactId>ard-oss</artifactId> </dependency> <dependency> <groupId>org.springframework.statemachine</groupId> <artifactId>spring-statemachine-starter</artifactId> <version>2.3.0</version> </dependency> </dependencies> <repositories> <repository> ard-work/src/main/java/com/ard/work/component/netty/command/notice/WayLineFinishedReplyCmd.java
File was renamed from ard-work/src/main/java/com/ard/work/component/netty/command/notice/FlyWayEndReplyCmd.java @@ -13,13 +13,13 @@ **/ @Data @EqualsAndHashCode(callSuper = false) public class FlyWayEndReplyCmd extends BaseCmd { public class WayLineFinishedReplyCmd extends BaseCmd { private final byte encryptFlag = 0x00; // 加密标志 private final byte actionCode = 0x2E; // 动作编号 private final byte result; // 执行结果 private final int errorCode; // 错误码 public FlyWayEndReplyCmd(String boxSn, byte result, int errorCode) { public WayLineFinishedReplyCmd(String boxSn, byte result, int errorCode) { setDataHeader((short) 0x6A77); // 设置帧头 setCmdCode((byte) 0x3D); // 设置指令编号 setDataLength((short) 23); // 数据长度 ard-work/src/main/java/com/ard/work/component/netty/handler/MessageProtocolDecoder.java
@@ -12,7 +12,7 @@ import com.ard.work.domain.dto.telemetry.TelemetryData; import com.ard.work.component.netty.command.BaseCmd; import com.ard.work.component.netty.command.notice.BoxPowerStatusReplyCmd; import com.ard.work.component.netty.command.notice.FlyWayEndReplyCmd; import com.ard.work.component.netty.command.notice.WayLineFinishedReplyCmd; import com.ard.work.component.netty.command.notice.UavReadyReplyCmd; import com.ard.work.component.netty.command.real.SignalInfoCmd; import com.ard.work.component.netty.command.real.UavStateCmd; @@ -185,7 +185,7 @@ case 0x25: return new RecordStopReplyCmd(boxSn, result, errorCode); case 0x2E: return new FlyWayEndReplyCmd(boxSn, result, errorCode); return new WayLineFinishedReplyCmd(boxSn, result, errorCode); // Special cases case 0x3D: case 0x40: ard-work/src/main/java/com/ard/work/config/RouteStateMachineConfig.java
New file @@ -0,0 +1,108 @@ package com.ard.work.config; import com.ard.work.domain.enums.RouteEvent; import com.ard.work.domain.enums.RouteState; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.statemachine.config.EnableStateMachineFactory; import org.springframework.statemachine.config.StateMachineConfigurerAdapter; import org.springframework.statemachine.config.builders.StateMachineStateConfigurer; import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer; import org.springframework.statemachine.guard.Guard; import org.springframework.statemachine.service.DefaultStateMachineService; import org.springframework.statemachine.service.StateMachineService; import java.util.EnumSet; @Configuration @EnableStateMachineFactory public class RouteStateMachineConfig extends StateMachineConfigurerAdapter<RouteState, RouteEvent> { /** * 配置状态机的状态集 */ @Override public void configure(StateMachineStateConfigurer<RouteState, RouteEvent> states) throws Exception { states .withStates() .initial(RouteState.IDLE) // 初始状态 .states(EnumSet.allOf(RouteState.class)); // 注册所有状态 } /** * 配置状态流转规则 */ @Override public void configure(StateMachineTransitionConfigurer<RouteState, RouteEvent> transitions) throws Exception { transitions // IDLE → UPLOADED .withExternal() .source(RouteState.IDLE) .target(RouteState.UPLOADED) .event(RouteEvent.UPLOAD_SUCCESS) .and() // UPLOADED → RUNNING .withExternal() .source(RouteState.UPLOADED) .target(RouteState.RUNNING) .event(RouteEvent.START_SUCCESS) .and() // RUNNING → PAUSED .withExternal() .source(RouteState.RUNNING) .target(RouteState.PAUSED) .event(RouteEvent.PAUSE_SUCCESS) .and() // PAUSED → RUNNING .withExternal() .source(RouteState.PAUSED) .target(RouteState.RUNNING) .event(RouteEvent.RESUME_SUCCESS) .and() // RUNNING / PAUSED → STOPPED .withExternal() .source(RouteState.RUNNING) .target(RouteState.STOPPED) .event(RouteEvent.STOP_SUCCESS) .and() .withExternal() .source(RouteState.PAUSED) .target(RouteState.STOPPED) .event(RouteEvent.STOP_SUCCESS) .and() // ⭐ STOPPED → IDLE(为下一条航线做准备) .withExternal() .source(RouteState.STOPPED) .target(RouteState.IDLE) .event(RouteEvent.RESET_ROUTE); } /** * 可选:守卫器 - 校验boxSn合法性(示例) */ @Bean public Guard<RouteState, RouteEvent> validBoxSnGuard() { return context -> { // 从扩展变量中获取boxSn并校验 String boxSn = (String) context.getMessageHeader("boxSn"); return boxSn != null && !boxSn.isEmpty(); }; } /** * 状态机服务(用于管理多实例) */ @Bean public StateMachineService<RouteState, RouteEvent> stateMachineService( org.springframework.statemachine.config.StateMachineFactory<RouteState, RouteEvent> factory) { return new DefaultStateMachineService<>(factory); } } ard-work/src/main/java/com/ard/work/config/RouteStateMachineListener.java
New file @@ -0,0 +1,35 @@ package com.ard.work.config; import com.ard.work.domain.enums.RouteEvent; import com.ard.work.domain.enums.RouteState; import lombok.extern.slf4j.Slf4j; import org.springframework.statemachine.StateContext; import org.springframework.statemachine.listener.StateMachineListenerAdapter; import org.springframework.stereotype.Component; @Component @Slf4j public class RouteStateMachineListener extends StateMachineListenerAdapter<RouteState, RouteEvent> { @Override public void stateContext(StateContext<RouteState, RouteEvent> context) { if (context.getStage() != StateContext.Stage.STATE_ENTRY) { return; // 避免多次触发 } RouteState target = context.getTarget() != null ? context.getTarget().getId() : null; String boxSn = (String) context.getMessageHeaders().get("boxSn"); if (boxSn == null || target == null) { return; } log.info("boxSn={} 状态变更为 {}", boxSn, target); } } ard-work/src/main/java/com/ard/work/config/RouteStateMachineService.java
New file @@ -0,0 +1,115 @@ package com.ard.work.config; import com.ard.work.domain.enums.RouteEvent; import com.ard.work.domain.enums.RouteState; import lombok.RequiredArgsConstructor; import org.springframework.messaging.Message; import org.springframework.messaging.support.MessageBuilder; import org.springframework.statemachine.StateMachine; import org.springframework.statemachine.service.StateMachineService; import org.springframework.statemachine.support.DefaultExtendedState; import org.springframework.statemachine.support.DefaultStateMachineContext; import org.springframework.stereotype.Service; import java.util.concurrent.ConcurrentHashMap; @Service @RequiredArgsConstructor public class RouteStateMachineService { private final StateMachineService<RouteState, RouteEvent> stateMachineService; private final RouteStateMachineListener listener; private final ConcurrentHashMap<String, Boolean> listenerRegisteredMap = new ConcurrentHashMap<>(); private StateMachine<RouteState, RouteEvent> getStateMachine(String boxSn) { StateMachine<RouteState, RouteEvent> stateMachine = stateMachineService.acquireStateMachine(boxSn); // 使用原子操作确保 listener 只添加一次 listenerRegisteredMap.computeIfAbsent(boxSn, k -> { stateMachine.addStateListener(listener); return true; }); stateMachine.start(); return stateMachine; } /** * 发送事件到指定boxSn的状态机 */ public boolean sendEvent(String boxSn, RouteEvent event) { try { // 获取/创建指定boxSn的状态机实例 StateMachine<RouteState, RouteEvent> stateMachine = getStateMachine(boxSn); // 构建事件消息(携带boxSn作为头信息) Message<RouteEvent> message = MessageBuilder .withPayload(event) .setHeader("boxSn", boxSn) .build(); // 发送事件并返回执行结果 return stateMachine.sendEvent(message); } catch (Exception e) { // 异常处理:日志记录、状态机重置等 throw new RuntimeException("发送航线状态事件失败,boxSn=" + boxSn + ", event=" + event, e); } } /** * 获取指定boxSn的当前状态 */ public RouteState getCurrentState(String boxSn) { try { StateMachine<RouteState, RouteEvent> stateMachine = stateMachineService.acquireStateMachine(boxSn); return stateMachine.getState().getId(); } catch (Exception e) { // 未找到状态机实例时返回初始状态 return RouteState.IDLE; } } /** * 释放指定boxSn的状态机实例(可选,节省资源) */ public void releaseStateMachine(String boxSn) { stateMachineService.releaseStateMachine(boxSn); } /** * 重置指定boxSn的状态机(恢复到初始状态) */ public void resetStateMachine(String boxSn) { try { StateMachine<RouteState, RouteEvent> stateMachine = stateMachineService.acquireStateMachine(boxSn); // 1️⃣ 停止状态机 stateMachine.stop(); // 2️⃣ 构造 ExtendedState(可选,但强烈推荐) DefaultExtendedState extendedState = new DefaultExtendedState(); extendedState.getVariables().put("boxSn", boxSn); // 3️⃣ 重置状态机到 IDLE stateMachine.getStateMachineAccessor() .doWithAllRegions(accessor -> { accessor.resetStateMachine( new DefaultStateMachineContext<>( RouteState.IDLE, // 初始状态 null, // event null, // event headers extendedState // 扩展状态 ) ); }); // 4️⃣ 重新启动 stateMachine.start(); } catch (Exception e) { throw new RuntimeException("重置航线状态机失败,boxSn=" + boxSn, e); } } } ard-work/src/main/java/com/ard/work/domain/enums/RouteEvent.java
New file @@ -0,0 +1,10 @@ package com.ard.work.domain.enums; public enum RouteEvent { UPLOAD_SUCCESS, START_SUCCESS, PAUSE_SUCCESS, RESUME_SUCCESS, STOP_SUCCESS, RESET_ROUTE } ard-work/src/main/java/com/ard/work/domain/enums/RouteState.java
New file @@ -0,0 +1,9 @@ package com.ard.work.domain.enums; public enum RouteState { IDLE, // 空闲(未上传) UPLOADED, // 航线上传完成 RUNNING, // 执行中 PAUSED, // 已暂停 STOPPED // 已停止 } ard-work/src/main/java/com/ard/work/service/impl/TyServiceImpl.java
@@ -14,7 +14,7 @@ import com.ard.work.component.netty.command.control.SpeedControlCmd; import com.ard.work.component.netty.command.manual.*; import com.ard.work.component.netty.command.notice.BoxPowerStatusReplyCmd; import com.ard.work.component.netty.command.notice.FlyWayEndReplyCmd; import com.ard.work.component.netty.command.notice.WayLineFinishedReplyCmd; import com.ard.work.component.netty.command.notice.UavReadyReplyCmd; import com.ard.work.component.netty.command.real.SignalInfoCmd; import com.ard.work.component.netty.command.real.UavStateCmd; @@ -26,10 +26,10 @@ import com.ard.work.component.netty.handler.ErrorCodeEnum; import com.ard.work.component.netty.protobuf.TelemetryDataPB; import com.ard.work.component.websocket.utils.WebSocketUtil; import com.ard.work.config.RouteStateMachineService; import com.ard.work.domain.cache.UavSignalInfoCache; import com.ard.work.domain.cache.UavStateCache; import com.ard.work.domain.cache.UavTelemetryDataCache; import com.ard.work.domain.cache.UavWaylineCache; import com.ard.work.domain.dto.BoxHistoryDTO; import com.ard.work.domain.dto.BoxPhotosDTO; import com.ard.work.domain.dto.BoxSettingsDTO; @@ -49,6 +49,7 @@ import com.ard.work.domain.dto.wayline.*; import com.ard.work.domain.entity.ArdDroneInfo; import com.ard.work.domain.entity.ArdServerInfo; import com.ard.work.domain.enums.RouteEvent; import com.ard.work.domain.message.ServerReplyMessage; import com.ard.work.domain.message.UavControlType; import com.ard.work.domain.receiver.*; @@ -56,6 +57,7 @@ import com.ard.work.service.IArdServerInfoService; import com.ard.work.service.ITyService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import javax.annotation.Resource; @@ -78,6 +80,8 @@ private IArdDroneInfoService ardDroneService; @Resource private TyClient tyClient; @Autowired private RouteStateMachineService stateMachineService; /** * 获取边缘服务器token信息 @@ -975,10 +979,6 @@ CameraModeReplyCmd cameraModeReplyCommand = (CameraModeReplyCmd) tyCommand; logPrint("无人机设置相机模式", cameraModeReplyCommand.getResult(), cameraModeReplyCommand.getErrorCode()); } if (tyCommand instanceof FlyWayEndReplyCmd) { FlyWayEndReplyCmd flyWayEndReplyCmd = (FlyWayEndReplyCmd) tyCommand; logPrint("无人机飞行结束", flyWayEndReplyCmd.getResult(), flyWayEndReplyCmd.getErrorCode()); } //格式化存储卡 if (tyCommand instanceof FormatSdCardReplyCmd) { FormatSdCardReplyCmd formatSdCardReplyCmd = (FormatSdCardReplyCmd) tyCommand; @@ -1044,23 +1044,53 @@ } //开始航线回复 if (tyCommand instanceof WayLineBeginReplyCmd) { WayLineBeginReplyCmd wayLineBeginReplyCmd = (WayLineBeginReplyCmd) tyCommand; logPrint("开始航线", wayLineBeginReplyCmd.getResult(), wayLineBeginReplyCmd.getErrorCode()); if (wayLineBeginReplyCmd.getResult() == 0) { byte[] boxSnBytes = tyCommand.getBoxSn(); String boxSn = new String(boxSnBytes); stateMachineService.sendEvent(boxSn, RouteEvent.START_SUCCESS); } } //暂停航线回复 if (tyCommand instanceof WayLinePauseReplyCmd) { WayLinePauseReplyCmd wayLinePauseReplyCmd = (WayLinePauseReplyCmd) tyCommand; logPrint("暂停航线", wayLinePauseReplyCmd.getResult(), wayLinePauseReplyCmd.getErrorCode()); if (wayLinePauseReplyCmd.getResult() == 0) { byte[] boxSnBytes = tyCommand.getBoxSn(); String boxSn = new String(boxSnBytes); stateMachineService.sendEvent(boxSn, RouteEvent.PAUSE_SUCCESS); } } //恢复航线回复 if (tyCommand instanceof WayLineResumeReplyCmd) { WayLineResumeReplyCmd wayLineResumeReplyCmd = (WayLineResumeReplyCmd) tyCommand; logPrint("恢复航线", wayLineResumeReplyCmd.getResult(), wayLineResumeReplyCmd.getErrorCode()); if (wayLineResumeReplyCmd.getResult() == 0) { byte[] boxSnBytes = tyCommand.getBoxSn(); String boxSn = new String(boxSnBytes); stateMachineService.sendEvent(boxSn, RouteEvent.RESUME_SUCCESS); } } //结束航线回复 if (tyCommand instanceof WayLineEndReplyCmd) { WayLineEndReplyCmd wayLineEndReplyCmd = (WayLineEndReplyCmd) tyCommand; logPrint("结束航线", wayLineEndReplyCmd.getResult(), wayLineEndReplyCmd.getErrorCode()); if (wayLineEndReplyCmd.getResult() == 0) { byte[] boxSnBytes = tyCommand.getBoxSn(); String boxSn = new String(boxSnBytes); stateMachineService.sendEvent(boxSn, RouteEvent.STOP_SUCCESS); } } if (tyCommand instanceof WayLineFinishedReplyCmd) { WayLineFinishedReplyCmd wayLineFinishedReplyCmd = (WayLineFinishedReplyCmd) tyCommand; logPrint("航线执行完成", wayLineFinishedReplyCmd.getResult(), wayLineFinishedReplyCmd.getErrorCode()); if (wayLineFinishedReplyCmd.getResult() == 0) { byte[] boxSnBytes = tyCommand.getBoxSn(); String boxSn = new String(boxSnBytes); stateMachineService.sendEvent(boxSn, RouteEvent.RESET_ROUTE); } } //设置返航高度回复 if (tyCommand instanceof ReturnHeightSettingReplyCmd) {