liusuyi
2025-12-18 073239cedaab68465e109b1957144f5fca17958d
增加航线状态机
6 files added
3 files modified
1 files renamed
397 ■■■■■ changed files
ard-admin/src/main/java/com/ard/web/controller/route/RouteController.java 65 ●●●●● patch | view | raw | blame | history
ard-work/pom.xml 5 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/work/component/netty/command/notice/WayLineFinishedReplyCmd.java 4 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/work/component/netty/handler/MessageProtocolDecoder.java 4 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/work/config/RouteStateMachineConfig.java 108 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/work/config/RouteStateMachineListener.java 35 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/work/config/RouteStateMachineService.java 115 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/work/domain/enums/RouteEvent.java 10 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/work/domain/enums/RouteState.java 9 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/work/service/impl/TyServiceImpl.java 42 ●●●●● patch | view | raw | blame | history
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) {