From 6501986e3d92db26b71011242e516788d630bdb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AD=99=E5=B0=8F=E4=BA=91?= Date: Mon, 15 Dec 2025 16:27:55 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E7=8A=B6=E6=80=81=E6=9C=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 30 + .../airport/action/airport/OfflineAction.java | 20 + .../airport/action/airport/OnlineAction.java | 18 + .../action/cover/CloseCoverAction.java | 20 + .../action/cover/CoverClosedAction.java | 21 + .../action/cover/CoverOpenedAction.java | 21 + .../airport/action/cover/OpenCoverAction.java | 20 + .../action/debug/CloseDebugModeAction.java | 20 + .../action/debug/OpenDebugModeAction.java | 20 + .../airport/action/reboot/RebootAction.java | 20 + .../action/reboot/RebootCompletedAction.java | 21 + .../airport/config/AirportMachineConfig.java | 187 +++++ .../airport/config/CoverMachineConfig.java | 113 +++ .../airport/demo/AirportSystemDemo.java | 128 +++ .../status/airport/events/AirportEvent.java | 47 ++ .../status/airport/events/CoverEvent.java | 36 + .../guard/airport/CanOfflineGuard.java | 21 + .../airport/guard/airport/CanOnlineGuard.java | 21 + .../guard/airport/IsAirportOnlineGuard.java | 21 + .../guard/cover/CanCloseCoverGuard.java | 22 + .../guard/cover/CanOpenCoverGuard.java | 22 + .../guard/cover/IsCoverClosedGuard.java | 22 + .../guard/cover/IsCoverOpenedGuard.java | 22 + .../guard/debug/CanCloseDebugModeGuard.java | 21 + .../airport/guard/debug/IsDebugModeGuard.java | 21 + .../guard/debug/IsNotDebugModeGuard.java | 21 + .../guard/reboot/IsRebootCompletedGuard.java | 21 + .../listener/AirPortStatusListener.java | 77 ++ .../airport/manager/AirportSystemManager.java | 154 ++++ .../service/AirportMachineService.java | 182 +++++ .../airport/service/CoverMachineService.java | 71 ++ .../status/airport/status/AirportState.java | 31 + .../status/airport/status/CoverState.java | 36 + .../status/airport/status/DroneState.java | 4 + .../com/tuoheng/status/statemachine/README.md | 506 ++++++++++++ .../statemachine/action/TaskAction.java | 142 ++++ .../config/TaskStateMachineConfig.java | 182 +++++ .../statemachine/demo/StateMachineDemo.java | 94 +++ .../demo/StateMachineManagerDemo.java | 161 ++++ .../status/statemachine/events/Event.java | 15 + .../status/statemachine/guard/TaskGuard.java | 112 +++ .../listener/TaskStateMachineListener.java | 157 ++++ .../manager/StateMachineManager.java | 262 ++++++ .../service/StateMachineManagerService.java | 128 +++ .../service/TaskStateMachineService.java | 194 +++++ .../status/statemachine/status/Status.java | 19 + .../无人机控制状态机设计文档.md | 765 ++++++++++++++++++ .../statemachine/StateMachineManagerTest.java | 209 +++++ .../statemachine/TaskStateMachineTest.java | 65 ++ 49 files changed, 4543 insertions(+) create mode 100644 src/main/java/com/tuoheng/status/airport/action/airport/OfflineAction.java create mode 100644 src/main/java/com/tuoheng/status/airport/action/airport/OnlineAction.java create mode 100644 src/main/java/com/tuoheng/status/airport/action/cover/CloseCoverAction.java create mode 100644 src/main/java/com/tuoheng/status/airport/action/cover/CoverClosedAction.java create mode 100644 src/main/java/com/tuoheng/status/airport/action/cover/CoverOpenedAction.java create mode 100644 src/main/java/com/tuoheng/status/airport/action/cover/OpenCoverAction.java create mode 100644 src/main/java/com/tuoheng/status/airport/action/debug/CloseDebugModeAction.java create mode 100644 src/main/java/com/tuoheng/status/airport/action/debug/OpenDebugModeAction.java create mode 100644 src/main/java/com/tuoheng/status/airport/action/reboot/RebootAction.java create mode 100644 src/main/java/com/tuoheng/status/airport/action/reboot/RebootCompletedAction.java create mode 100644 src/main/java/com/tuoheng/status/airport/config/AirportMachineConfig.java create mode 100644 src/main/java/com/tuoheng/status/airport/config/CoverMachineConfig.java create mode 100644 src/main/java/com/tuoheng/status/airport/demo/AirportSystemDemo.java create mode 100644 src/main/java/com/tuoheng/status/airport/events/AirportEvent.java create mode 100644 src/main/java/com/tuoheng/status/airport/events/CoverEvent.java create mode 100644 src/main/java/com/tuoheng/status/airport/guard/airport/CanOfflineGuard.java create mode 100644 src/main/java/com/tuoheng/status/airport/guard/airport/CanOnlineGuard.java create mode 100644 src/main/java/com/tuoheng/status/airport/guard/airport/IsAirportOnlineGuard.java create mode 100644 src/main/java/com/tuoheng/status/airport/guard/cover/CanCloseCoverGuard.java create mode 100644 src/main/java/com/tuoheng/status/airport/guard/cover/CanOpenCoverGuard.java create mode 100644 src/main/java/com/tuoheng/status/airport/guard/cover/IsCoverClosedGuard.java create mode 100644 src/main/java/com/tuoheng/status/airport/guard/cover/IsCoverOpenedGuard.java create mode 100644 src/main/java/com/tuoheng/status/airport/guard/debug/CanCloseDebugModeGuard.java create mode 100644 src/main/java/com/tuoheng/status/airport/guard/debug/IsDebugModeGuard.java create mode 100644 src/main/java/com/tuoheng/status/airport/guard/debug/IsNotDebugModeGuard.java create mode 100644 src/main/java/com/tuoheng/status/airport/guard/reboot/IsRebootCompletedGuard.java create mode 100644 src/main/java/com/tuoheng/status/airport/listener/AirPortStatusListener.java create mode 100644 src/main/java/com/tuoheng/status/airport/manager/AirportSystemManager.java create mode 100644 src/main/java/com/tuoheng/status/airport/service/AirportMachineService.java create mode 100644 src/main/java/com/tuoheng/status/airport/service/CoverMachineService.java create mode 100644 src/main/java/com/tuoheng/status/airport/status/AirportState.java create mode 100644 src/main/java/com/tuoheng/status/airport/status/CoverState.java create mode 100644 src/main/java/com/tuoheng/status/airport/status/DroneState.java create mode 100644 src/main/java/com/tuoheng/status/statemachine/README.md create mode 100644 src/main/java/com/tuoheng/status/statemachine/action/TaskAction.java create mode 100644 src/main/java/com/tuoheng/status/statemachine/config/TaskStateMachineConfig.java create mode 100644 src/main/java/com/tuoheng/status/statemachine/demo/StateMachineDemo.java create mode 100644 src/main/java/com/tuoheng/status/statemachine/demo/StateMachineManagerDemo.java create mode 100644 src/main/java/com/tuoheng/status/statemachine/events/Event.java create mode 100644 src/main/java/com/tuoheng/status/statemachine/guard/TaskGuard.java create mode 100644 src/main/java/com/tuoheng/status/statemachine/listener/TaskStateMachineListener.java create mode 100644 src/main/java/com/tuoheng/status/statemachine/manager/StateMachineManager.java create mode 100644 src/main/java/com/tuoheng/status/statemachine/service/StateMachineManagerService.java create mode 100644 src/main/java/com/tuoheng/status/statemachine/service/TaskStateMachineService.java create mode 100644 src/main/java/com/tuoheng/status/statemachine/status/Status.java create mode 100644 src/main/java/com/tuoheng/status/statemachine/无人机控制状态机设计文档.md create mode 100644 src/test/java/com/tuoheng/status/statemachine/StateMachineManagerTest.java create mode 100644 src/test/java/com/tuoheng/status/statemachine/TaskStateMachineTest.java diff --git a/pom.xml b/pom.xml index d3f7602..74311d5 100644 --- a/pom.xml +++ b/pom.xml @@ -32,6 +32,18 @@ 4.2.1 + + org.springframework.statemachine + spring-statemachine-core + 3.2.0 + + + + + org.springframework + spring-context + + org.apache.commons @@ -45,6 +57,24 @@ 3.8.1 test + + org.testng + testng + 7.4.0 + compile + + + org.testng + testng + 7.4.0 + compile + + + org.testng + testng + 7.4.0 + compile + diff --git a/src/main/java/com/tuoheng/status/airport/action/airport/OfflineAction.java b/src/main/java/com/tuoheng/status/airport/action/airport/OfflineAction.java new file mode 100644 index 0000000..21be67f --- /dev/null +++ b/src/main/java/com/tuoheng/status/airport/action/airport/OfflineAction.java @@ -0,0 +1,20 @@ +package com.tuoheng.status.airport.action.airport; + +import com.tuoheng.status.airport.events.AirportEvent; +import com.tuoheng.status.airport.status.AirportState; +import org.springframework.statemachine.StateContext; +import org.springframework.statemachine.action.Action; +import org.springframework.stereotype.Component; + +/** + * 机巢离线动作 + */ +@Component +public class OfflineAction implements Action { + + @Override + public void execute(StateContext stateContext) { + System.out.println("执行机巢离线操作"); + // TODO: 实现机巢离线的具体逻辑 + } +} diff --git a/src/main/java/com/tuoheng/status/airport/action/airport/OnlineAction.java b/src/main/java/com/tuoheng/status/airport/action/airport/OnlineAction.java new file mode 100644 index 0000000..2348773 --- /dev/null +++ b/src/main/java/com/tuoheng/status/airport/action/airport/OnlineAction.java @@ -0,0 +1,18 @@ +package com.tuoheng.status.airport.action.airport; + +import com.tuoheng.status.airport.events.AirportEvent; +import com.tuoheng.status.airport.status.AirportState; +import org.springframework.statemachine.StateContext; +import org.springframework.statemachine.action.Action; +import org.springframework.statemachine.guard.Guard; +import org.springframework.stereotype.Component; + +@Component +public class OnlineAction implements Action { + + @Override + public void execute(StateContext stateContext) { + + } + +} diff --git a/src/main/java/com/tuoheng/status/airport/action/cover/CloseCoverAction.java b/src/main/java/com/tuoheng/status/airport/action/cover/CloseCoverAction.java new file mode 100644 index 0000000..9d3a0c7 --- /dev/null +++ b/src/main/java/com/tuoheng/status/airport/action/cover/CloseCoverAction.java @@ -0,0 +1,20 @@ +package com.tuoheng.status.airport.action.cover; + +import com.tuoheng.status.airport.events.AirportEvent; +import com.tuoheng.status.airport.status.AirportState; +import org.springframework.statemachine.StateContext; +import org.springframework.statemachine.action.Action; +import org.springframework.stereotype.Component; + +/** + * 关舱动作 + */ +@Component +public class CloseCoverAction implements Action { + + @Override + public void execute(StateContext stateContext) { + System.out.println("执行关舱操作"); + // TODO: 发送 cover_close 指令 + } +} diff --git a/src/main/java/com/tuoheng/status/airport/action/cover/CoverClosedAction.java b/src/main/java/com/tuoheng/status/airport/action/cover/CoverClosedAction.java new file mode 100644 index 0000000..fb9d647 --- /dev/null +++ b/src/main/java/com/tuoheng/status/airport/action/cover/CoverClosedAction.java @@ -0,0 +1,21 @@ +package com.tuoheng.status.airport.action.cover; + +import com.tuoheng.status.airport.events.AirportEvent; +import com.tuoheng.status.airport.status.AirportState; +import org.springframework.statemachine.StateContext; +import org.springframework.statemachine.action.Action; +import org.springframework.stereotype.Component; + +/** + * 舱门关闭完成动作 + * 处理舱门完全关闭后的逻辑 + */ +@Component +public class CoverClosedAction implements Action { + + @Override + public void execute(StateContext stateContext) { + System.out.println("舱门已完全关闭,执行后续处理"); + // TODO: 舱门关闭完成后的处理逻辑(例如:记录日志、自动关闭调试模式等) + } +} diff --git a/src/main/java/com/tuoheng/status/airport/action/cover/CoverOpenedAction.java b/src/main/java/com/tuoheng/status/airport/action/cover/CoverOpenedAction.java new file mode 100644 index 0000000..7b948a9 --- /dev/null +++ b/src/main/java/com/tuoheng/status/airport/action/cover/CoverOpenedAction.java @@ -0,0 +1,21 @@ +package com.tuoheng.status.airport.action.cover; + +import com.tuoheng.status.airport.events.AirportEvent; +import com.tuoheng.status.airport.status.AirportState; +import org.springframework.statemachine.StateContext; +import org.springframework.statemachine.action.Action; +import org.springframework.stereotype.Component; + +/** + * 舱门打开完成动作 + * 处理舱门完全打开后的逻辑 + */ +@Component +public class CoverOpenedAction implements Action { + + @Override + public void execute(StateContext stateContext) { + System.out.println("舱门已完全打开,执行后续处理"); + // TODO: 舱门打开完成后的处理逻辑(例如:记录日志、通知相关模块等) + } +} diff --git a/src/main/java/com/tuoheng/status/airport/action/cover/OpenCoverAction.java b/src/main/java/com/tuoheng/status/airport/action/cover/OpenCoverAction.java new file mode 100644 index 0000000..77370f7 --- /dev/null +++ b/src/main/java/com/tuoheng/status/airport/action/cover/OpenCoverAction.java @@ -0,0 +1,20 @@ +package com.tuoheng.status.airport.action.cover; + +import com.tuoheng.status.airport.events.AirportEvent; +import com.tuoheng.status.airport.status.AirportState; +import org.springframework.statemachine.StateContext; +import org.springframework.statemachine.action.Action; +import org.springframework.stereotype.Component; + +/** + * 开舱动作 + */ +@Component +public class OpenCoverAction implements Action { + + @Override + public void execute(StateContext stateContext) { + System.out.println("执行开舱操作"); + // TODO: 发送 cover_open 指令 + } +} diff --git a/src/main/java/com/tuoheng/status/airport/action/debug/CloseDebugModeAction.java b/src/main/java/com/tuoheng/status/airport/action/debug/CloseDebugModeAction.java new file mode 100644 index 0000000..ab7ecf4 --- /dev/null +++ b/src/main/java/com/tuoheng/status/airport/action/debug/CloseDebugModeAction.java @@ -0,0 +1,20 @@ +package com.tuoheng.status.airport.action.debug; + +import com.tuoheng.status.airport.events.AirportEvent; +import com.tuoheng.status.airport.status.AirportState; +import org.springframework.statemachine.StateContext; +import org.springframework.statemachine.action.Action; +import org.springframework.stereotype.Component; + +/** + * 关闭调试模式动作 + */ +@Component +public class CloseDebugModeAction implements Action { + + @Override + public void execute(StateContext stateContext) { + System.out.println("执行关闭调试模式操作"); + // TODO: 发送 debug_mode_close 指令 + } +} diff --git a/src/main/java/com/tuoheng/status/airport/action/debug/OpenDebugModeAction.java b/src/main/java/com/tuoheng/status/airport/action/debug/OpenDebugModeAction.java new file mode 100644 index 0000000..5a6fa70 --- /dev/null +++ b/src/main/java/com/tuoheng/status/airport/action/debug/OpenDebugModeAction.java @@ -0,0 +1,20 @@ +package com.tuoheng.status.airport.action.debug; + +import com.tuoheng.status.airport.events.AirportEvent; +import com.tuoheng.status.airport.status.AirportState; +import org.springframework.statemachine.StateContext; +import org.springframework.statemachine.action.Action; +import org.springframework.stereotype.Component; + +/** + * 开启调试模式动作 + */ +@Component +public class OpenDebugModeAction implements Action { + + @Override + public void execute(StateContext stateContext) { + System.out.println("执行开启调试模式操作"); + // TODO: 发送 debug_mode_open 指令 + } +} diff --git a/src/main/java/com/tuoheng/status/airport/action/reboot/RebootAction.java b/src/main/java/com/tuoheng/status/airport/action/reboot/RebootAction.java new file mode 100644 index 0000000..8a41802 --- /dev/null +++ b/src/main/java/com/tuoheng/status/airport/action/reboot/RebootAction.java @@ -0,0 +1,20 @@ +package com.tuoheng.status.airport.action.reboot; + +import com.tuoheng.status.airport.events.AirportEvent; +import com.tuoheng.status.airport.status.AirportState; +import org.springframework.statemachine.StateContext; +import org.springframework.statemachine.action.Action; +import org.springframework.stereotype.Component; + +/** + * 机巢重启动作 + */ +@Component +public class RebootAction implements Action { + + @Override + public void execute(StateContext stateContext) { + System.out.println("执行机巢重启操作"); + // TODO: 发送 device_reboot 指令 + } +} diff --git a/src/main/java/com/tuoheng/status/airport/action/reboot/RebootCompletedAction.java b/src/main/java/com/tuoheng/status/airport/action/reboot/RebootCompletedAction.java new file mode 100644 index 0000000..67553a1 --- /dev/null +++ b/src/main/java/com/tuoheng/status/airport/action/reboot/RebootCompletedAction.java @@ -0,0 +1,21 @@ +package com.tuoheng.status.airport.action.reboot; + +import com.tuoheng.status.airport.events.AirportEvent; +import com.tuoheng.status.airport.status.AirportState; +import org.springframework.statemachine.StateContext; +import org.springframework.statemachine.action.Action; +import org.springframework.stereotype.Component; + +/** + * 机巢重启完成动作 + * 处理机巢重启完成后的逻辑 + */ +@Component +public class RebootCompletedAction implements Action { + + @Override + public void execute(StateContext stateContext) { + System.out.println("机巢重启完成,执行初始化处理"); + // TODO: 机巢重启完成后的处理逻辑(例如:重新初始化、恢复状态等) + } +} diff --git a/src/main/java/com/tuoheng/status/airport/config/AirportMachineConfig.java b/src/main/java/com/tuoheng/status/airport/config/AirportMachineConfig.java new file mode 100644 index 0000000..e18070a --- /dev/null +++ b/src/main/java/com/tuoheng/status/airport/config/AirportMachineConfig.java @@ -0,0 +1,187 @@ +package com.tuoheng.status.airport.config; + +import com.tuoheng.status.airport.action.airport.OfflineAction; +import com.tuoheng.status.airport.action.airport.OnlineAction; +import com.tuoheng.status.airport.action.debug.CloseDebugModeAction; +import com.tuoheng.status.airport.action.debug.OpenDebugModeAction; +import com.tuoheng.status.airport.action.reboot.RebootAction; +import com.tuoheng.status.airport.action.reboot.RebootCompletedAction; +import com.tuoheng.status.airport.events.AirportEvent; +import com.tuoheng.status.airport.guard.airport.CanOfflineGuard; +import com.tuoheng.status.airport.guard.airport.CanOnlineGuard; +import com.tuoheng.status.airport.guard.debug.CanCloseDebugModeGuard; +import com.tuoheng.status.airport.guard.debug.IsDebugModeGuard; +import com.tuoheng.status.airport.guard.debug.IsNotDebugModeGuard; +import com.tuoheng.status.airport.guard.reboot.IsRebootCompletedGuard; +import com.tuoheng.status.airport.listener.AirPortStatusListener; +import com.tuoheng.status.airport.status.AirportState; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.statemachine.StateMachine; +import org.springframework.statemachine.config.StateMachineBuilder; +import org.springframework.statemachine.config.StateMachineFactory; + +import java.util.EnumSet; +import java.util.UUID; + +/** + * 机巢状态机配置(简化版 - 舱门状态已分离) + */ +@Configuration +public class AirportMachineConfig { + + @Autowired + AirPortStatusListener airPortStatusListener; + + // ==================== Actions ==================== + @Autowired + OnlineAction onlineAction; + + @Autowired + OfflineAction offlineAction; + + @Autowired + OpenDebugModeAction openDebugModeAction; + + @Autowired + CloseDebugModeAction closeDebugModeAction; + + @Autowired + RebootAction rebootAction; + + @Autowired + RebootCompletedAction rebootCompletedAction; + + // ==================== Guards ==================== + @Autowired + CanOnlineGuard canOnlineGuard; + + @Autowired + CanOfflineGuard canOfflineGuard; + + @Autowired + IsDebugModeGuard isDebugModeGuard; + + @Autowired + IsNotDebugModeGuard isNotDebugModeGuard; + + @Autowired + CanCloseDebugModeGuard canCloseDebugModeGuard; + + @Autowired + IsRebootCompletedGuard isRebootCompletedGuard; + + @Bean(name = "airportStateMachineFactory") + public StateMachineFactory stateMachineFactory() throws Exception { + return new StateMachineFactory() { + @Override + public StateMachine getStateMachine() { + return null; + } + + @Override + public StateMachine getStateMachine(String machineId) { + try { + StateMachineBuilder.Builder builder = StateMachineBuilder.builder(); + configureStateMachine(builder); + configureStates(builder); + configureTransitions(builder); + + StateMachine stateMachine = builder.build(); + stateMachine.getExtendedState().getVariables().put("machineId", machineId); + return stateMachine; + } catch (Exception e) { + throw new RuntimeException("Failed to create state machine", e); + } + } + + @Override + public StateMachine getStateMachine(UUID uuid) { + return null; + } + }; + } + + private void configureStateMachine(StateMachineBuilder.Builder builder) throws Exception { + builder.configureConfiguration() + .withConfiguration() + .autoStartup(true) + .listener(airPortStatusListener); + } + + private void configureStates(StateMachineBuilder.Builder builder) throws Exception { + builder.configureStates() + .withStates() + .initial(AirportState.OFFLINE) + .states(EnumSet.of( + AirportState.OFFLINE, + AirportState.ONLINE, + AirportState.REBOOTING + )) + .and() + .withStates() + .parent(AirportState.ONLINE) + .initial(AirportState.STANDBY) + .states(EnumSet.of( + AirportState.STANDBY, + AirportState.DEBUG_MODE + )); + } + + private void configureTransitions(StateMachineBuilder.Builder builder) throws Exception { + builder.configureTransitions() + // OFFLINE -> ONLINE(STANDBY) + .withExternal() + .source(AirportState.OFFLINE) + .target(AirportState.ONLINE) + .event(AirportEvent.AIRPORT_ONLINE) + .action(onlineAction) + .guard(canOnlineGuard) + .and() + + // ONLINE -> OFFLINE + .withExternal() + .source(AirportState.ONLINE) + .target(AirportState.OFFLINE) + .event(AirportEvent.AIRPORT_OFFLINE) + .action(offlineAction) + .guard(canOfflineGuard) + .and() + + // STANDBY -> DEBUG_MODE + .withExternal() + .source(AirportState.STANDBY) + .target(AirportState.DEBUG_MODE) + .event(AirportEvent.DEBUG_MODE_OPEN) + .action(openDebugModeAction) + .guard(isNotDebugModeGuard) + .and() + + // DEBUG_MODE -> STANDBY + .withExternal() + .source(AirportState.DEBUG_MODE) + .target(AirportState.STANDBY) + .event(AirportEvent.DEBUG_MODE_CLOSE) + .action(closeDebugModeAction) + .guard(canCloseDebugModeGuard) + .and() + + // DEBUG_MODE -> REBOOTING + .withExternal() + .source(AirportState.DEBUG_MODE) + .target(AirportState.REBOOTING) + .event(AirportEvent.AIRPORT_REBOOT) + .action(rebootAction) + .guard(isDebugModeGuard) + .and() + + // REBOOTING -> ONLINE(STANDBY) + .withExternal() + .source(AirportState.REBOOTING) + .target(AirportState.ONLINE) + .event(AirportEvent.REBOOT_COMPLETED) + .action(rebootCompletedAction) + .guard(isRebootCompletedGuard); + } +} diff --git a/src/main/java/com/tuoheng/status/airport/config/CoverMachineConfig.java b/src/main/java/com/tuoheng/status/airport/config/CoverMachineConfig.java new file mode 100644 index 0000000..b0d86a1 --- /dev/null +++ b/src/main/java/com/tuoheng/status/airport/config/CoverMachineConfig.java @@ -0,0 +1,113 @@ +package com.tuoheng.status.airport.config; + +import com.tuoheng.status.airport.events.CoverEvent; +import com.tuoheng.status.airport.status.CoverState; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.statemachine.StateMachine; +import org.springframework.statemachine.config.StateMachineBuilder; +import org.springframework.statemachine.config.StateMachineFactory; + +import java.util.EnumSet; +import java.util.UUID; + +/** + * 舱门状态机配置 + */ +@Configuration +public class CoverMachineConfig { + + @Bean + public StateMachineFactory coverStateMachineFactory() throws Exception { + return new StateMachineFactory() { + @Override + public StateMachine getStateMachine() { + return null; + } + + @Override + public StateMachine getStateMachine(String machineId) { + try { + StateMachineBuilder.Builder builder = StateMachineBuilder.builder(); + configureCoverStateMachine(builder); + configureCoverStates(builder); + configureCoverTransitions(builder); + + StateMachine stateMachine = builder.build(); + stateMachine.getExtendedState().getVariables().put("machineId", machineId); + return stateMachine; + } catch (Exception e) { + throw new RuntimeException("Failed to create cover state machine", e); + } + } + + @Override + public StateMachine getStateMachine(UUID uuid) { + return null; + } + }; + } + + private void configureCoverStateMachine(StateMachineBuilder.Builder builder) throws Exception { + builder.configureConfiguration() + .withConfiguration() + .autoStartup(true); + } + + private void configureCoverStates(StateMachineBuilder.Builder builder) throws Exception { + builder.configureStates() + .withStates() + .initial(CoverState.CLOSED) + .states(EnumSet.allOf(CoverState.class)); + } + + private void configureCoverTransitions(StateMachineBuilder.Builder builder) throws Exception { + builder.configureTransitions() + // CLOSED -> OPENING + .withExternal() + .source(CoverState.CLOSED) + .target(CoverState.OPENING) + .event(CoverEvent.OPEN) + .and() + + // OPENING -> OPENED + .withExternal() + .source(CoverState.OPENING) + .target(CoverState.OPENED) + .event(CoverEvent.OPENED) + .and() + + // OPENED -> CLOSING + .withExternal() + .source(CoverState.OPENED) + .target(CoverState.CLOSING) + .event(CoverEvent.CLOSE) + .and() + + // CLOSING -> CLOSED + .withExternal() + .source(CoverState.CLOSING) + .target(CoverState.CLOSED) + .event(CoverEvent.CLOSED) + .and() + + // ERROR handling + .withExternal() + .source(CoverState.OPENING) + .target(CoverState.ERROR) + .event(CoverEvent.ERROR) + .and() + + .withExternal() + .source(CoverState.CLOSING) + .target(CoverState.ERROR) + .event(CoverEvent.ERROR) + .and() + + // RESET from ERROR + .withExternal() + .source(CoverState.ERROR) + .target(CoverState.CLOSED) + .event(CoverEvent.RESET); + } +} diff --git a/src/main/java/com/tuoheng/status/airport/demo/AirportSystemDemo.java b/src/main/java/com/tuoheng/status/airport/demo/AirportSystemDemo.java new file mode 100644 index 0000000..09d0a58 --- /dev/null +++ b/src/main/java/com/tuoheng/status/airport/demo/AirportSystemDemo.java @@ -0,0 +1,128 @@ +package com.tuoheng.status.airport.demo; + +import com.tuoheng.status.airport.manager.AirportSystemManager; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; + +/** + * 机巢系统演示(组合状态机方案) + */ +public class AirportSystemDemo { + + public static void main(String[] args) throws InterruptedException { + + // 创建 Spring 上下文 + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + + // 扫描配置类、服务类和监听器类 + context.scan("com.tuoheng.status.airport"); + context.refresh(); + + // 获取 AirportSystemManager + AirportSystemManager systemManager = context.getBean(AirportSystemManager.class); + + System.out.println("\n========== 机巢系统演示开始(组合状态机方案) ==========\n"); + + String airportSn = "airport-001"; + + // ==================== 场景1: 机巢上线 ==================== + System.out.println("【场景1】机巢上线"); + System.out.println("----------------------------------------"); + systemManager.airportOnline(airportSn); + System.out.println(" " + systemManager.getFullStatus(airportSn)); + System.out.println(); + Thread.sleep(500); + + // ==================== 场景2: 开舱流程 ==================== + System.out.println("【场景2】开舱流程"); + System.out.println("----------------------------------------"); + + System.out.println("1. 开启调试模式"); + systemManager.openDebugMode(airportSn); + System.out.println(" " + systemManager.getFullStatus(airportSn)); + System.out.println(); + Thread.sleep(500); + + System.out.println("2. 开舱"); + systemManager.openCover(airportSn); + System.out.println(" " + systemManager.getFullStatus(airportSn)); + System.out.println(); + Thread.sleep(500); + + System.out.println("3. 舱门打开完成"); + systemManager.coverOpened(airportSn); + System.out.println(" " + systemManager.getFullStatus(airportSn)); + System.out.println(); + Thread.sleep(500); + + // ==================== 场景3: 关舱流程 ==================== + System.out.println("【场景3】关舱流程"); + System.out.println("----------------------------------------"); + + System.out.println("4. 关舱"); + systemManager.closeCover(airportSn); + System.out.println(" " + systemManager.getFullStatus(airportSn)); + System.out.println(); + Thread.sleep(500); + + System.out.println("5. 舱门关闭完成"); + systemManager.coverClosed(airportSn); + System.out.println(" " + systemManager.getFullStatus(airportSn)); + System.out.println(); + Thread.sleep(500); + + System.out.println("6. 关闭调试模式"); + systemManager.closeDebugMode(airportSn); + System.out.println(" " + systemManager.getFullStatus(airportSn)); + System.out.println(); + Thread.sleep(500); + + // ==================== 场景4: 状态查询 ==================== + System.out.println("【场景4】状态查询"); + System.out.println("----------------------------------------"); + System.out.println("机巢状态: " + systemManager.getAirportState(airportSn)); + System.out.println("舱门状态: " + systemManager.getCoverState(airportSn)); + System.out.println("完整状态: " + systemManager.getFullStatus(airportSn)); + System.out.println(); + + // ==================== 场景5: 机巢重启 ==================== + System.out.println("【场景5】机巢重启"); + System.out.println("----------------------------------------"); + + System.out.println("7. 开启调试模式"); + systemManager.openDebugMode(airportSn); + System.out.println(" " + systemManager.getFullStatus(airportSn)); + System.out.println(); + Thread.sleep(500); + + System.out.println("8. 机巢重启"); + systemManager.rebootAirport(airportSn); + System.out.println(" " + systemManager.getFullStatus(airportSn)); + System.out.println(); + Thread.sleep(500); + + System.out.println("9. 重启完成"); + systemManager.rebootCompleted(airportSn); + System.out.println(" " + systemManager.getFullStatus(airportSn)); + System.out.println(); + + // ==================== 场景6: 错误处理 ==================== + System.out.println("【场景6】错误处理(Guard拦截)"); + System.out.println("----------------------------------------"); + + System.out.println("10. 尝试在待机状态下开舱(应该失败)"); + boolean result = systemManager.openCover(airportSn); + System.out.println(" 操作结果: " + (result ? "成功" : "失败")); + System.out.println(" " + systemManager.getFullStatus(airportSn)); + System.out.println(); + + System.out.println("\n========== 机巢系统演示结束 ==========\n"); + + System.out.println("✅ 组合状态机方案优势:"); + System.out.println(" 1. 机巢状态和舱门状态独立管理"); + System.out.println(" 2. 状态层级扁平,易于理解"); + System.out.println(" 3. 易于扩展(可添加推杆、无人机等状态机)"); + System.out.println(" 4. 职责清晰,维护方便"); + + context.close(); + } +} diff --git a/src/main/java/com/tuoheng/status/airport/events/AirportEvent.java b/src/main/java/com/tuoheng/status/airport/events/AirportEvent.java new file mode 100644 index 0000000..77108f0 --- /dev/null +++ b/src/main/java/com/tuoheng/status/airport/events/AirportEvent.java @@ -0,0 +1,47 @@ +package com.tuoheng.status.airport.events; + +/** + * 机巢事件枚举 + * 定义机巢状态机中的所有事件 + */ +public enum AirportEvent { + + // ==================== 机巢基本事件 ==================== + /** + * 机巢上线事件 + * 触发源: OSD心跳 + */ + AIRPORT_ONLINE, + + /** + * 机巢离线事件 + * 触发源: OSD心跳 + */ + AIRPORT_OFFLINE, + + // ==================== 调试模式事件 ==================== + /** + * 开启调试模式 + * 触发源: 用户指令 + */ + DEBUG_MODE_OPEN, + + /** + * 关闭调试模式 + * 触发源: 用户指令/自动 + */ + DEBUG_MODE_CLOSE, + + // ==================== 机巢重启事件 ==================== + /** + * 机巢重启指令 + * 触发源: 用户指令 + */ + AIRPORT_REBOOT, + + /** + * 重启完成 + * 触发源: Events事件 + */ + REBOOT_COMPLETED +} diff --git a/src/main/java/com/tuoheng/status/airport/events/CoverEvent.java b/src/main/java/com/tuoheng/status/airport/events/CoverEvent.java new file mode 100644 index 0000000..f06ae13 --- /dev/null +++ b/src/main/java/com/tuoheng/status/airport/events/CoverEvent.java @@ -0,0 +1,36 @@ +package com.tuoheng.status.airport.events; + +/** + * 舱门事件枚举 + */ +public enum CoverEvent { + /** + * 开舱指令 + */ + OPEN, + + /** + * 开舱完成 + */ + OPENED, + + /** + * 关舱指令 + */ + CLOSE, + + /** + * 关舱完成 + */ + CLOSED, + + /** + * 舱门异常 + */ + ERROR, + + /** + * 重置舱门状态 + */ + RESET +} diff --git a/src/main/java/com/tuoheng/status/airport/guard/airport/CanOfflineGuard.java b/src/main/java/com/tuoheng/status/airport/guard/airport/CanOfflineGuard.java new file mode 100644 index 0000000..bbb0ef8 --- /dev/null +++ b/src/main/java/com/tuoheng/status/airport/guard/airport/CanOfflineGuard.java @@ -0,0 +1,21 @@ +package com.tuoheng.status.airport.guard.airport; + +import com.tuoheng.status.airport.events.AirportEvent; +import com.tuoheng.status.airport.status.AirportState; +import org.springframework.statemachine.StateContext; +import org.springframework.statemachine.guard.Guard; +import org.springframework.stereotype.Component; + +/** + * 检查机巢是否可以离线 + */ +@Component +public class CanOfflineGuard implements Guard { + + @Override + public boolean evaluate(StateContext stateContext) { + // TODO: 检查机巢是否可以安全离线(例如:无人机已降落、舱门已关闭等) + System.out.println("检查机巢是否可以离线"); + return true; + } +} diff --git a/src/main/java/com/tuoheng/status/airport/guard/airport/CanOnlineGuard.java b/src/main/java/com/tuoheng/status/airport/guard/airport/CanOnlineGuard.java new file mode 100644 index 0000000..2c35ac9 --- /dev/null +++ b/src/main/java/com/tuoheng/status/airport/guard/airport/CanOnlineGuard.java @@ -0,0 +1,21 @@ +package com.tuoheng.status.airport.guard.airport; + +import com.tuoheng.status.airport.events.AirportEvent; +import com.tuoheng.status.airport.status.AirportState; +import org.springframework.statemachine.StateContext; +import org.springframework.statemachine.guard.Guard; +import org.springframework.stereotype.Component; + +/** + * 检查机巢是否可以上线 + */ +@Component +public class CanOnlineGuard implements Guard { + + @Override + public boolean evaluate(StateContext stateContext) { + // TODO: 检查机巢是否可以上线(例如:设备状态正常、网络连接正常等) + System.out.println("检查机巢是否可以上线"); + return true; + } +} diff --git a/src/main/java/com/tuoheng/status/airport/guard/airport/IsAirportOnlineGuard.java b/src/main/java/com/tuoheng/status/airport/guard/airport/IsAirportOnlineGuard.java new file mode 100644 index 0000000..fd260f5 --- /dev/null +++ b/src/main/java/com/tuoheng/status/airport/guard/airport/IsAirportOnlineGuard.java @@ -0,0 +1,21 @@ +package com.tuoheng.status.airport.guard.airport; + +import com.tuoheng.status.airport.events.AirportEvent; +import com.tuoheng.status.airport.status.AirportState; +import org.springframework.statemachine.StateContext; +import org.springframework.statemachine.guard.Guard; +import org.springframework.stereotype.Component; + +/** + * 检查机巢是否在线 + */ +@Component +public class IsAirportOnlineGuard implements Guard { + + @Override + public boolean evaluate(StateContext stateContext) { + // TODO: 检查机巢是否在线 + System.out.println("检查机巢是否在线"); + return true; + } +} diff --git a/src/main/java/com/tuoheng/status/airport/guard/cover/CanCloseCoverGuard.java b/src/main/java/com/tuoheng/status/airport/guard/cover/CanCloseCoverGuard.java new file mode 100644 index 0000000..43d200b --- /dev/null +++ b/src/main/java/com/tuoheng/status/airport/guard/cover/CanCloseCoverGuard.java @@ -0,0 +1,22 @@ +package com.tuoheng.status.airport.guard.cover; + +import com.tuoheng.status.airport.events.AirportEvent; +import com.tuoheng.status.airport.status.AirportState; +import org.springframework.statemachine.StateContext; +import org.springframework.statemachine.guard.Guard; +import org.springframework.stereotype.Component; + +/** + * 检查是否可以关舱 + * 条件:舱门状态不是已关闭 + */ +@Component +public class CanCloseCoverGuard implements Guard { + + @Override + public boolean evaluate(StateContext stateContext) { + // TODO: 检查当前舱门状态不是已关闭 + System.out.println("检查是否可以关舱"); + return true; + } +} diff --git a/src/main/java/com/tuoheng/status/airport/guard/cover/CanOpenCoverGuard.java b/src/main/java/com/tuoheng/status/airport/guard/cover/CanOpenCoverGuard.java new file mode 100644 index 0000000..0245630 --- /dev/null +++ b/src/main/java/com/tuoheng/status/airport/guard/cover/CanOpenCoverGuard.java @@ -0,0 +1,22 @@ +package com.tuoheng.status.airport.guard.cover; + +import com.tuoheng.status.airport.events.AirportEvent; +import com.tuoheng.status.airport.status.AirportState; +import org.springframework.statemachine.StateContext; +import org.springframework.statemachine.guard.Guard; +import org.springframework.stereotype.Component; + +/** + * 检查是否可以开舱 + * 条件:舱门状态不是已打开 + */ +@Component +public class CanOpenCoverGuard implements Guard { + + @Override + public boolean evaluate(StateContext stateContext) { + // TODO: 检查当前舱门状态不是已打开 + System.out.println("检查是否可以开舱"); + return true; + } +} diff --git a/src/main/java/com/tuoheng/status/airport/guard/cover/IsCoverClosedGuard.java b/src/main/java/com/tuoheng/status/airport/guard/cover/IsCoverClosedGuard.java new file mode 100644 index 0000000..4d32dbb --- /dev/null +++ b/src/main/java/com/tuoheng/status/airport/guard/cover/IsCoverClosedGuard.java @@ -0,0 +1,22 @@ +package com.tuoheng.status.airport.guard.cover; + +import com.tuoheng.status.airport.events.AirportEvent; +import com.tuoheng.status.airport.status.AirportState; +import org.springframework.statemachine.StateContext; +import org.springframework.statemachine.guard.Guard; +import org.springframework.stereotype.Component; + +/** + * 检查舱门是否已完全关闭 + * 用于验证 COVER_CLOSED 事件的有效性 + */ +@Component +public class IsCoverClosedGuard implements Guard { + + @Override + public boolean evaluate(StateContext stateContext) { + // TODO: 检查舱门是否已完全关闭(通过OSD状态确认) + System.out.println("检查舱门是否已完全关闭"); + return true; + } +} diff --git a/src/main/java/com/tuoheng/status/airport/guard/cover/IsCoverOpenedGuard.java b/src/main/java/com/tuoheng/status/airport/guard/cover/IsCoverOpenedGuard.java new file mode 100644 index 0000000..ddac6f2 --- /dev/null +++ b/src/main/java/com/tuoheng/status/airport/guard/cover/IsCoverOpenedGuard.java @@ -0,0 +1,22 @@ +package com.tuoheng.status.airport.guard.cover; + +import com.tuoheng.status.airport.events.AirportEvent; +import com.tuoheng.status.airport.status.AirportState; +import org.springframework.statemachine.StateContext; +import org.springframework.statemachine.guard.Guard; +import org.springframework.stereotype.Component; + +/** + * 检查舱门是否已完全打开 + * 用于验证 COVER_OPENED 事件的有效性 + */ +@Component +public class IsCoverOpenedGuard implements Guard { + + @Override + public boolean evaluate(StateContext stateContext) { + // TODO: 检查舱门是否已完全打开(通过OSD状态确认) + System.out.println("检查舱门是否已完全打开"); + return true; + } +} diff --git a/src/main/java/com/tuoheng/status/airport/guard/debug/CanCloseDebugModeGuard.java b/src/main/java/com/tuoheng/status/airport/guard/debug/CanCloseDebugModeGuard.java new file mode 100644 index 0000000..ff0faa2 --- /dev/null +++ b/src/main/java/com/tuoheng/status/airport/guard/debug/CanCloseDebugModeGuard.java @@ -0,0 +1,21 @@ +package com.tuoheng.status.airport.guard.debug; + +import com.tuoheng.status.airport.events.AirportEvent; +import com.tuoheng.status.airport.status.AirportState; +import org.springframework.statemachine.StateContext; +import org.springframework.statemachine.guard.Guard; +import org.springframework.stereotype.Component; + +/** + * 检查是否可以关闭调试模式 + */ +@Component +public class CanCloseDebugModeGuard implements Guard { + + @Override + public boolean evaluate(StateContext stateContext) { + // TODO: 检查是否可以关闭调试模式(例如:没有正在进行的操作) + System.out.println("检查是否可以关闭调试模式"); + return true; + } +} diff --git a/src/main/java/com/tuoheng/status/airport/guard/debug/IsDebugModeGuard.java b/src/main/java/com/tuoheng/status/airport/guard/debug/IsDebugModeGuard.java new file mode 100644 index 0000000..7b897c3 --- /dev/null +++ b/src/main/java/com/tuoheng/status/airport/guard/debug/IsDebugModeGuard.java @@ -0,0 +1,21 @@ +package com.tuoheng.status.airport.guard.debug; + +import com.tuoheng.status.airport.events.AirportEvent; +import com.tuoheng.status.airport.status.AirportState; +import org.springframework.statemachine.StateContext; +import org.springframework.statemachine.guard.Guard; +import org.springframework.stereotype.Component; + +/** + * 检查是否处于调试模式 + */ +@Component +public class IsDebugModeGuard implements Guard { + + @Override + public boolean evaluate(StateContext stateContext) { + // TODO: 检查当前是否处于调试模式 + System.out.println("检查是否处于调试模式"); + return true; + } +} diff --git a/src/main/java/com/tuoheng/status/airport/guard/debug/IsNotDebugModeGuard.java b/src/main/java/com/tuoheng/status/airport/guard/debug/IsNotDebugModeGuard.java new file mode 100644 index 0000000..74426c7 --- /dev/null +++ b/src/main/java/com/tuoheng/status/airport/guard/debug/IsNotDebugModeGuard.java @@ -0,0 +1,21 @@ +package com.tuoheng.status.airport.guard.debug; + +import com.tuoheng.status.airport.events.AirportEvent; +import com.tuoheng.status.airport.status.AirportState; +import org.springframework.statemachine.StateContext; +import org.springframework.statemachine.guard.Guard; +import org.springframework.stereotype.Component; + +/** + * 检查是否不处于调试模式 + */ +@Component +public class IsNotDebugModeGuard implements Guard { + + @Override + public boolean evaluate(StateContext stateContext) { + // TODO: 检查当前不处于调试模式 + System.out.println("检查是否不处于调试模式"); + return true; + } +} diff --git a/src/main/java/com/tuoheng/status/airport/guard/reboot/IsRebootCompletedGuard.java b/src/main/java/com/tuoheng/status/airport/guard/reboot/IsRebootCompletedGuard.java new file mode 100644 index 0000000..2e5c414 --- /dev/null +++ b/src/main/java/com/tuoheng/status/airport/guard/reboot/IsRebootCompletedGuard.java @@ -0,0 +1,21 @@ +package com.tuoheng.status.airport.guard.reboot; + +import com.tuoheng.status.airport.events.AirportEvent; +import com.tuoheng.status.airport.status.AirportState; +import org.springframework.statemachine.StateContext; +import org.springframework.statemachine.guard.Guard; +import org.springframework.stereotype.Component; + +/** + * 检查机巢重启是否完成 + */ +@Component +public class IsRebootCompletedGuard implements Guard { + + @Override + public boolean evaluate(StateContext stateContext) { + // TODO: 检查机巢重启是否完成(通过Events事件确认) + System.out.println("检查机巢重启是否完成"); + return true; + } +} diff --git a/src/main/java/com/tuoheng/status/airport/listener/AirPortStatusListener.java b/src/main/java/com/tuoheng/status/airport/listener/AirPortStatusListener.java new file mode 100644 index 0000000..4cff578 --- /dev/null +++ b/src/main/java/com/tuoheng/status/airport/listener/AirPortStatusListener.java @@ -0,0 +1,77 @@ +package com.tuoheng.status.airport.listener; + +import com.tuoheng.status.airport.events.AirportEvent; +import com.tuoheng.status.airport.status.AirportState; +import com.tuoheng.status.statemachine.events.Event; +import com.tuoheng.status.statemachine.status.Status; +import org.springframework.messaging.Message; +import org.springframework.statemachine.StateContext; +import org.springframework.statemachine.StateMachine; +import org.springframework.statemachine.listener.StateMachineListener; +import org.springframework.statemachine.state.State; +import org.springframework.statemachine.transition.Transition; +import org.springframework.stereotype.Component; + +@Component +public class AirPortStatusListener implements StateMachineListener { + + @Override + public void stateChanged(State state, State state1) { + + } + + @Override + public void stateEntered(State state) { + + } + + @Override + public void stateExited(State state) { + + } + + @Override + public void eventNotAccepted(Message message) { + + } + + @Override + public void transition(Transition transition) { + + } + + @Override + public void transitionStarted(Transition transition) { + + } + + @Override + public void transitionEnded(Transition transition) { + + } + + @Override + public void stateMachineStarted(StateMachine stateMachine) { + + } + + @Override + public void stateMachineStopped(StateMachine stateMachine) { + + } + + @Override + public void stateMachineError(StateMachine stateMachine, Exception e) { + + } + + @Override + public void extendedStateChanged(Object o, Object o1) { + + } + + @Override + public void stateContext(StateContext stateContext) { + + } +} diff --git a/src/main/java/com/tuoheng/status/airport/manager/AirportSystemManager.java b/src/main/java/com/tuoheng/status/airport/manager/AirportSystemManager.java new file mode 100644 index 0000000..07cd1a1 --- /dev/null +++ b/src/main/java/com/tuoheng/status/airport/manager/AirportSystemManager.java @@ -0,0 +1,154 @@ +package com.tuoheng.status.airport.manager; + +import com.tuoheng.status.airport.events.AirportEvent; +import com.tuoheng.status.airport.events.CoverEvent; +import com.tuoheng.status.airport.service.AirportMachineService; +import com.tuoheng.status.airport.service.CoverMachineService; +import com.tuoheng.status.airport.status.AirportState; +import com.tuoheng.status.airport.status.CoverState; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * 机巢系统管理器 + * 协调机巢状态机和舱门状态机 + */ +@Component +public class AirportSystemManager { + + @Autowired + private AirportMachineService airportManager; + + @Autowired + private CoverMachineService coverManager; + + /** + * 机巢上线 + */ + public boolean airportOnline(String airportSn) { + return airportManager.sendEvent(airportSn, AirportEvent.AIRPORT_ONLINE); + } + + /** + * 机巢离线 + */ + public boolean airportOffline(String airportSn) { + return airportManager.sendEvent(airportSn, AirportEvent.AIRPORT_OFFLINE); + } + + /** + * 开启调试模式 + */ + public boolean openDebugMode(String airportSn) { + if (!airportManager.isInState(airportSn, AirportState.ONLINE)) { + System.out.println("机巢未在线,无法开启调试模式"); + return false; + } + return airportManager.sendEvent(airportSn, AirportEvent.DEBUG_MODE_OPEN); + } + + /** + * 关闭调试模式 + */ + public boolean closeDebugMode(String airportSn) { + return airportManager.sendEvent(airportSn, AirportEvent.DEBUG_MODE_CLOSE); + } + + /** + * 开舱 + */ + public boolean openCover(String airportSn) { + // 检查机巢是否在调试模式 + if (!airportManager.isInState(airportSn, AirportState.DEBUG_MODE)) { + System.out.println("必须在调试模式下才能开舱"); + return false; + } + + // 检查舱门是否已经打开 + if (coverManager.isInState(airportSn, CoverState.OPENED)) { + System.out.println("舱门已经打开"); + return false; + } + + // 发送开舱指令到舱门状态机 + return coverManager.sendEvent(airportSn, CoverEvent.OPEN); + } + + /** + * 舱门打开完成 + */ + public boolean coverOpened(String airportSn) { + return coverManager.sendEvent(airportSn, CoverEvent.OPENED); + } + + /** + * 关舱 + */ + public boolean closeCover(String airportSn) { + // 检查机巢是否在调试模式 + if (!airportManager.isInState(airportSn, AirportState.DEBUG_MODE)) { + System.out.println("必须在调试模式下才能关舱"); + return false; + } + + // 检查舱门是否已经关闭 + if (coverManager.isInState(airportSn, CoverState.CLOSED)) { + System.out.println("舱门已经关闭"); + return false; + } + + // 发送关舱指令到舱门状态机 + return coverManager.sendEvent(airportSn, CoverEvent.CLOSE); + } + + /** + * 舱门关闭完成 + */ + public boolean coverClosed(String airportSn) { + return coverManager.sendEvent(airportSn, CoverEvent.CLOSED); + } + + /** + * 机巢重启 + */ + public boolean rebootAirport(String airportSn) { + if (!airportManager.isInState(airportSn, AirportState.DEBUG_MODE)) { + System.out.println("必须在调试模式下才能重启"); + return false; + } + return airportManager.sendEvent(airportSn, AirportEvent.AIRPORT_REBOOT); + } + + /** + * 重启完成 + */ + public boolean rebootCompleted(String airportSn) { + return airportManager.sendEvent(airportSn, AirportEvent.REBOOT_COMPLETED); + } + + /** + * 获取机巢状态 + */ + public AirportState getAirportState(String airportSn) { + return airportManager.getCurrentState(airportSn); + } + + /** + * 获取舱门状态 + */ + public CoverState getCoverState(String airportSn) { + return coverManager.getCurrentState(airportSn); + } + + /** + * 获取完整状态信息 + */ + public String getFullStatus(String airportSn) { + AirportState airportState = getAirportState(airportSn); + CoverState coverState = getCoverState(airportSn); + + return String.format("机巢状态: %s, 舱门状态: %s", + airportState != null ? airportState : "未知", + coverState != null ? coverState : "未知"); + } +} diff --git a/src/main/java/com/tuoheng/status/airport/service/AirportMachineService.java b/src/main/java/com/tuoheng/status/airport/service/AirportMachineService.java new file mode 100644 index 0000000..68bc908 --- /dev/null +++ b/src/main/java/com/tuoheng/status/airport/service/AirportMachineService.java @@ -0,0 +1,182 @@ +package com.tuoheng.status.airport.service; + +import com.tuoheng.status.airport.events.AirportEvent; +import com.tuoheng.status.airport.status.AirportState; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.statemachine.StateMachine; +import org.springframework.statemachine.config.StateMachineFactory; +import org.springframework.stereotype.Component; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 机巢状态机管理器 + * 负责管理多个机巢的状态机实例 + */ +@Component +public class AirportMachineService { + + @Autowired + StateMachineFactory stateMachineFactory; + + /** + * 存储所有机巢的状态机实例 + * Key: 机巢ID (airportSn) + * Value: 状态机实例 + */ + private final Map> stateMachineMap = new ConcurrentHashMap<>(); + + /** + * 获取或创建状态机 + * 如果状态机不存在,则创建新的状态机实例 + * + * @param airportSn 机巢序列号 + * @return 状态机实例 + */ + public StateMachine getOrCreateStateMachine(String airportSn) { + return stateMachineMap.computeIfAbsent(airportSn, id -> { + StateMachine stateMachine = stateMachineFactory.getStateMachine(id); + stateMachine.start(); + System.out.println("创建并启动状态机: " + id); + return stateMachine; + }); + } + + /** + * 获取状态机(不创建) + * + * @param airportSn 机巢序列号 + * @return 状态机实例,如果不存在返回null + */ + public StateMachine getStateMachine(String airportSn) { + return stateMachineMap.get(airportSn); + } + + /** + * 获取状态机的当前状态 + * + * @param airportSn 机巢序列号 + * @return 当前状态,如果状态机不存在返回null + */ + public AirportState getCurrentState(String airportSn) { + StateMachine stateMachine = stateMachineMap.get(airportSn); + if (stateMachine == null) { + System.out.println("状态机不存在: " + airportSn); + return null; + } + return stateMachine.getState().getId(); + } + + /** + * 获取状态机的所有当前状态(包括子状态) + * + * @param airportSn 机巢序列号 + * @return 当前状态集合的字符串表示 + */ + public String getCurrentStates(String airportSn) { + StateMachine stateMachine = stateMachineMap.get(airportSn); + if (stateMachine == null) { + return "状态机不存在"; + } + + StringBuilder states = new StringBuilder(); + stateMachine.getState().getIds().forEach(state -> { + if (states.length() > 0) { + states.append(" -> "); + } + states.append(state); + }); + + return states.toString(); + } + + /** + * 发送事件到状态机 + * + * @param airportSn 机巢序列号 + * @param event 事件 + * @return 是否发送成功 + */ + public boolean sendEvent(String airportSn, AirportEvent event) { + StateMachine stateMachine = getOrCreateStateMachine(airportSn); + boolean result = stateMachine.sendEvent(event); + + if (result) { + System.out.println(String.format("事件发送成功 - 机巢: %s, 事件: %s, 当前状态: %s", + airportSn, event, getCurrentStates(airportSn))); + } else { + System.out.println(String.format("事件发送失败 - 机巢: %s, 事件: %s, 当前状态: %s", + airportSn, event, getCurrentStates(airportSn))); + } + + return result; + } + + /** + * 移除状态机 + * + * @param airportSn 机巢序列号 + */ + public void removeStateMachine(String airportSn) { + StateMachine stateMachine = stateMachineMap.remove(airportSn); + if (stateMachine != null) { + stateMachine.stop(); + System.out.println("停止并移除状态机: " + airportSn); + } + } + + /** + * 检查状态机是否存在 + * + * @param airportSn 机巢序列号 + * @return 是否存在 + */ + public boolean hasStateMachine(String airportSn) { + return stateMachineMap.containsKey(airportSn); + } + + /** + * 获取所有状态机的数量 + * + * @return 状态机数量 + */ + public int getStateMachineCount() { + return stateMachineMap.size(); + } + + /** + * 获取所有机巢ID + * + * @return 机巢ID集合 + */ + public java.util.Set getAllAirportIds() { + return stateMachineMap.keySet(); + } + + /** + * 检查状态机是否处于指定状态 + * + * @param airportSn 机巢序列号 + * @param state 要检查的状态 + * @return 是否处于指定状态 + */ + public boolean isInState(String airportSn, AirportState state) { + StateMachine stateMachine = stateMachineMap.get(airportSn); + if (stateMachine == null) { + return false; + } + return stateMachine.getState().getIds().contains(state); + } + + /** + * 重启状态机 + * + * @param airportSn 机巢序列号 + */ + public void restartStateMachine(String airportSn) { + removeStateMachine(airportSn); + getOrCreateStateMachine(airportSn); + System.out.println("重启状态机: " + airportSn); + } +} diff --git a/src/main/java/com/tuoheng/status/airport/service/CoverMachineService.java b/src/main/java/com/tuoheng/status/airport/service/CoverMachineService.java new file mode 100644 index 0000000..bc11166 --- /dev/null +++ b/src/main/java/com/tuoheng/status/airport/service/CoverMachineService.java @@ -0,0 +1,71 @@ +package com.tuoheng.status.airport.service; + +import com.tuoheng.status.airport.events.CoverEvent; +import com.tuoheng.status.airport.status.CoverState; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.statemachine.StateMachine; +import org.springframework.statemachine.config.StateMachineFactory; +import org.springframework.stereotype.Component; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 舱门状态机管理器 + */ +@Component +public class CoverMachineService { + + @Autowired + StateMachineFactory coverStateMachineFactory; + + private final Map> stateMachineMap = new ConcurrentHashMap<>(); + + public StateMachine getOrCreateStateMachine(String airportSn) { + return stateMachineMap.computeIfAbsent(airportSn, id -> { + StateMachine stateMachine = coverStateMachineFactory.getStateMachine(id); + stateMachine.start(); + System.out.println("创建并启动舱门状态机: " + id); + return stateMachine; + }); + } + + public CoverState getCurrentState(String airportSn) { + StateMachine stateMachine = stateMachineMap.get(airportSn); + if (stateMachine == null) { + return null; + } + return stateMachine.getState().getId(); + } + + public boolean sendEvent(String airportSn, CoverEvent event) { + StateMachine stateMachine = getOrCreateStateMachine(airportSn); + boolean result = stateMachine.sendEvent(event); + + if (result) { + System.out.println(String.format("舱门事件发送成功 - 机巢: %s, 事件: %s, 当前状态: %s", + airportSn, event, getCurrentState(airportSn))); + } else { + System.out.println(String.format("舱门事件发送失败 - 机巢: %s, 事件: %s, 当前状态: %s", + airportSn, event, getCurrentState(airportSn))); + } + + return result; + } + + public boolean isInState(String airportSn, CoverState state) { + StateMachine stateMachine = stateMachineMap.get(airportSn); + if (stateMachine == null) { + return false; + } + return stateMachine.getState().getId() == state; + } + + public void removeStateMachine(String airportSn) { + StateMachine stateMachine = stateMachineMap.remove(airportSn); + if (stateMachine != null) { + stateMachine.stop(); + System.out.println("停止并移除舱门状态机: " + airportSn); + } + } +} diff --git a/src/main/java/com/tuoheng/status/airport/status/AirportState.java b/src/main/java/com/tuoheng/status/airport/status/AirportState.java new file mode 100644 index 0000000..4b5b868 --- /dev/null +++ b/src/main/java/com/tuoheng/status/airport/status/AirportState.java @@ -0,0 +1,31 @@ +package com.tuoheng.status.airport.status; + +/** + * 机巢状态枚举(简化版 - 舱门状态已分离) + */ +public enum AirportState { + /** + * 离线 + */ + OFFLINE, + + /** + * 在线(父状态) + */ + ONLINE, + + /** + * 待机 + */ + STANDBY, + + /** + * 调试模式 + */ + DEBUG_MODE, + + /** + * 重启中 + */ + REBOOTING +} diff --git a/src/main/java/com/tuoheng/status/airport/status/CoverState.java b/src/main/java/com/tuoheng/status/airport/status/CoverState.java new file mode 100644 index 0000000..1050513 --- /dev/null +++ b/src/main/java/com/tuoheng/status/airport/status/CoverState.java @@ -0,0 +1,36 @@ +package com.tuoheng.status.airport.status; + +/** + * 舱门状态枚举 + */ +public enum CoverState { + /** + * 舱门已关闭 + */ + CLOSED, + + /** + * 舱门打开中 + */ + OPENING, + + /** + * 舱门已打开 + */ + OPENED, + + /** + * 舱门关闭中 + */ + CLOSING, + + /** + * 舱门半开 + */ + HALF_OPEN, + + /** + * 舱门状态异常 + */ + ERROR +} diff --git a/src/main/java/com/tuoheng/status/airport/status/DroneState.java b/src/main/java/com/tuoheng/status/airport/status/DroneState.java new file mode 100644 index 0000000..a357e9d --- /dev/null +++ b/src/main/java/com/tuoheng/status/airport/status/DroneState.java @@ -0,0 +1,4 @@ +package com.tuoheng.status.airport.status; + +public enum DroneState { +} diff --git a/src/main/java/com/tuoheng/status/statemachine/README.md b/src/main/java/com/tuoheng/status/statemachine/README.md new file mode 100644 index 0000000..1f8838a --- /dev/null +++ b/src/main/java/com/tuoheng/status/statemachine/README.md @@ -0,0 +1,506 @@ +# Spring StateMachine 使用文档 + +本文档详细介绍了如何在项目中使用 Spring StateMachine Core 来构建状态机,包括子状态、Action、Guard、事件监听器等核心功能。 + +## 目录 + +- [项目结构](#项目结构) +- [核心概念](#核心概念) +- [状态机配置](#状态机配置) +- [使用示例](#使用示例) +- [运行演示](#运行演示) +- [常见问题](#常见问题) + +## 项目结构 + +``` +com.tuoheng.status.statemachine/ +├── status/ +│ └── Status.java # 状态枚举(包含主状态和子状态) +├── events/ +│ └── Event.java # 事件枚举 +├── action/ +│ └── TaskAction.java # Action 实现类 +├── guard/ +│ └── TaskGuard.java # Guard 实现类 +├── listener/ +│ └── TaskStateMachineListener.java # 状态机事件监听器 +├── config/ +│ └── TaskStateMachineConfig.java # 状态机配置类 +├── service/ +│ └── TaskStateMachineService.java # 服务类(使用示例) +└── demo/ + └── StateMachineDemo.java # 演示主类 +``` + +## 核心概念 + +### 1. 状态(State) + +状态定义了状态机可能处于的各种情况。本示例包含: + +**主状态:** +- `IDLE` - 空闲状态(初始状态) +- `PROCESSING` - 处理中(父状态,包含子状态) +- `COMPLETED` - 已完成 +- `FAILED` - 失败 + +**子状态(PROCESSING 的子状态):** +- `PREPARING` - 准备中 +- `EXECUTING` - 执行中 +- `VALIDATING` - 验证中 + +### 2. 事件(Event) + +事件是触发状态转换的触发器: + +- `START` - 开始处理 +- `PREPARE` - 准备就绪 +- `EXECUTE` - 执行 +- `VALIDATE` - 验证 +- `COMPLETE` - 完成 +- `FAIL` - 失败 +- `RETRY` - 重试 +- `RESET` - 重置 + +### 3. Action(动作) + +Action 在状态转换时执行,用于执行业务逻辑。Action 在转换**过程中**执行。 + +**示例:** +```java +public static class StartAction implements Action { + @Override + public void execute(StateContext context) { + // 执行业务逻辑 + logger.info("开始处理任务"); + Object taskId = context.getExtendedState().getVariables().get("taskId"); + // ... + } +} +``` + +### 4. Guard(守卫) + +Guard 用于条件判断,决定是否允许状态转换。Guard 在转换**之前**执行,如果返回 `false`,转换将被阻止。 + +**示例:** +```java +public static class CanStartGuard implements Guard { + @Override + public boolean evaluate(StateContext context) { + // 检查条件 + Object taskId = context.getExtendedState().getVariables().get("taskId"); + return taskId != null; // 只有 taskId 存在时才允许转换 + } +} +``` + +### 5. 事件监听器(Listener) + +监听器用于监听状态机的各种事件,如状态变化、转换等。 + +**监听的事件类型:** +- 状态机启动/停止 +- 状态进入/退出 +- 状态改变 +- 转换开始/结束 +- 事件未接受 +- 扩展状态改变 + +## 状态机配置 + +### 状态转换流程 + +``` +IDLE + └─[START]─> PREPARING (子状态) + └─[PREPARE]─> EXECUTING (子状态) + └─[EXECUTE]─> VALIDATING (子状态) + └─[VALIDATE]─> COMPLETED + └─[RESET]─> IDLE + +PREPARING/EXECUTING/VALIDATING + └─[FAIL]─> FAILED + └─[RETRY]─> PREPARING (重试) + └─[RESET]─> IDLE (重置) +``` + +### 配置类说明 + +`TaskStateMachineConfig` 类负责配置状态机: + +1. **配置状态和子状态:** +```java +builder.configureStates() + .withStates() + .initial(Status.IDLE) // 初始状态 + .states(EnumSet.of(Status.IDLE, Status.COMPLETED, Status.FAILED)) + .and() + .withStates() + .parent(Status.PROCESSING) // 设置父状态 + .initial(Status.PREPARING) // 子状态的初始状态 + .states(EnumSet.of(Status.PREPARING, Status.EXECUTING, Status.VALIDATING)); +``` + +2. **配置转换:** +```java +builder.configureTransitions() + .withExternal() + .source(Status.IDLE) + .target(Status.PREPARING) + .event(Event.START) + .action(new TaskAction.StartAction()) // 添加 Action + .guard(new TaskGuard.CanStartGuard()) // 添加 Guard + .and() + // ... 更多转换配置 +``` + +## 状态机管理器 + +当需要管理多个状态机实例时,可以使用 `StateMachineManager` 来统一管理。 + +### 核心功能 + +- **通过ID获取状态机**:`getStateMachine(machineId)` +- **查询当前状态**:`getCurrentStatus(machineId)` +- **获取详细信息**:`getStateMachineInfo(machineId)` +- **创建/获取状态机**:`getOrCreateStateMachine(machineId)` +- **移除状态机**:`removeStateMachine(machineId)` + +### 使用示例 + +```java +@Autowired +private StateMachineManager stateMachineManager; + +// 1. 创建或获取状态机 +StateMachine stateMachine = + stateMachineManager.getOrCreateStateMachine("machine-001"); + +// 2. 通过ID获取状态机 +StateMachine sm = + stateMachineManager.getStateMachine("machine-001"); + +// 3. 查询当前状态 +Status currentStatus = stateMachineManager.getCurrentStatus("machine-001"); +System.out.println("当前状态: " + currentStatus); + +// 4. 获取详细信息 +StateMachineManager.StateMachineInfo info = + stateMachineManager.getStateMachineInfo("machine-001"); +System.out.println("状态机ID: " + info.getMachineId()); +System.out.println("当前状态: " + info.getCurrentStatus()); +System.out.println("是否运行中: " + info.isRunning()); +System.out.println("扩展状态: " + info.getExtendedState()); + +// 5. 发送事件 +stateMachine.sendEvent(Event.START); + +// 6. 移除状态机 +stateMachineManager.removeStateMachine("machine-001"); +``` + +### 使用服务类(推荐) + +`StateMachineManagerService` 提供了更便捷的方法: + +```java +@Autowired +private StateMachineManagerService stateMachineManagerService; + +// 启动任务(自动创建状态机) +stateMachineManagerService.startTask("machine-001", "task-001"); + +// 发送事件 +stateMachineManagerService.sendEvent("machine-001", Event.PREPARE); + +// 查询状态 +Status status = stateMachineManagerService.getCurrentStatus("machine-001"); + +// 获取详细信息 +StateMachineManager.StateMachineInfo info = + stateMachineManagerService.getStateMachineInfo("machine-001"); + +// 打印状态机信息(调试用) +stateMachineManagerService.printStateMachineInfo("machine-001"); + +// 获取所有状态机ID +Set allIds = stateMachineManagerService.getAllMachineIds(); +``` + +### 管理多个状态机 + +```java +// 创建多个状态机 +stateMachineManager.getOrCreateStateMachine("machine-001"); +stateMachineManager.getOrCreateStateMachine("machine-002"); +stateMachineManager.getOrCreateStateMachine("machine-003"); + +// 获取所有状态机ID +Set allIds = stateMachineManager.getAllMachineIds(); +System.out.println("管理的状态机数量: " + allIds.size()); + +// 为不同状态机设置不同状态 +stateMachineManager.getStateMachine("machine-001") + .getExtendedState().getVariables().put("taskId", "task-001"); +stateMachineManagerService.sendEvent("machine-001", Event.START); + +// 查询不同状态机的状态 +Status status1 = stateMachineManager.getCurrentStatus("machine-001"); +Status status2 = stateMachineManager.getCurrentStatus("machine-002"); +``` + +## 使用示例 + +### 1. 基本使用 + +```java +@Autowired +private StateMachineFactory stateMachineFactory; + +public void processTask(String taskId) { + // 创建状态机实例 + StateMachine stateMachine = stateMachineFactory.getStateMachine(); + + try { + // 设置扩展状态变量 + stateMachine.getExtendedState().getVariables().put("taskId", taskId); + + // 启动状态机 + stateMachine.start(); + + // 发送事件触发转换 + stateMachine.sendEvent(Event.START); + + // 继续处理... + stateMachine.getExtendedState().getVariables().put("prepared", true); + stateMachine.sendEvent(Event.PREPARE); + + } finally { + // 停止状态机 + stateMachine.stop(); + } +} +``` + +### 2. 使用 Guard 控制转换 + +```java +// Guard 会在转换前检查条件 +public static class CanStartGuard implements Guard { + @Override + public boolean evaluate(StateContext context) { + Object taskId = context.getExtendedState().getVariables().get("taskId"); + return taskId != null; // 只有满足条件才允许转换 + } +} + +// 使用:如果 taskId 不存在,START 事件将被拒绝 +stateMachine.sendEvent(Event.START); // Guard 检查失败,转换不会发生 +``` + +### 3. 使用 Action 执行业务逻辑 + +```java +// Action 在转换时执行 +public static class ExecuteAction implements Action { + @Override + public void execute(StateContext context) { + // 执行业务逻辑 + logger.info("执行任务"); + // 可以访问和修改扩展状态 + context.getExtendedState().getVariables().put("executionResult", "success"); + } +} +``` + +### 4. 监听状态机事件 + +监听器会自动监听所有状态机事件: + +```java +// 监听器会自动记录: +// - 状态机启动/停止 +// - 状态进入/退出 +// - 状态改变 +// - 转换开始/结束 +// - 事件未接受 +``` + +## 运行演示 + +### 方式一:运行演示主类 + +直接运行 `StateMachineDemo` 类的 `main` 方法: + +```bash +# 在 IDE 中运行 +com.tuoheng.status.statemachine.demo.StateMachineDemo + +# 或使用 Maven +mvn exec:java -Dexec.mainClass="com.tuoheng.status.statemachine.demo.StateMachineDemo" +``` + +演示内容包括: +1. **正常流程**:IDLE → PREPARING → EXECUTING → VALIDATING → COMPLETED +2. **失败和重试**:演示失败后的重试机制 +3. **Guard 条件不满足**:演示 Guard 如何阻止转换 +4. **重置流程**:演示如何重置状态机 + +### 方式二:运行测试类 + +运行 `TaskStateMachineTest` 测试类: + +```bash +mvn test -Dtest=TaskStateMachineTest +``` + +### 方式三:在服务中使用 + +```java +@Service +public class YourService { + @Autowired + private TaskStateMachineService taskStateMachineService; + + public void handleTask(String taskId) { + // 使用预定义的服务方法 + taskStateMachineService.startTask(taskId); + } +} +``` + +## 扩展状态(Extended State) + +扩展状态用于存储状态机相关的变量,可以在 Action、Guard 和监听器中访问: + +```java +// 设置变量 +stateMachine.getExtendedState().getVariables().put("taskId", "task-001"); +stateMachine.getExtendedState().getVariables().put("retryCount", 0); + +// 在 Action 或 Guard 中访问 +Object taskId = context.getExtendedState().getVariables().get("taskId"); +Integer retryCount = (Integer) context.getExtendedState().getVariables().getOrDefault("retryCount", 0); +``` + +## 常见问题 + +### 1. Guard 返回 false,转换不执行 + +**原因:** Guard 在转换前检查条件,如果返回 `false`,转换将被阻止。 + +**解决:** 确保在发送事件前,扩展状态中的变量已正确设置。 + +```java +// 错误示例:Guard 检查失败 +stateMachine.sendEvent(Event.START); // taskId 未设置,Guard 返回 false + +// 正确示例:先设置变量 +stateMachine.getExtendedState().getVariables().put("taskId", "task-001"); +stateMachine.sendEvent(Event.START); // Guard 检查通过 +``` + +### 2. 事件未接受 + +**原因:** 当前状态不支持该事件,或没有匹配的转换配置。 + +**解决:** +- 检查状态机当前状态 +- 确认配置中是否有对应的转换 +- 检查 Guard 条件是否满足 + +```java +// 检查事件是否被接受 +boolean accepted = stateMachine.sendEvent(Event.START); +if (!accepted) { + logger.warning("事件未被接受,当前状态: " + stateMachine.getState().getId()); +} +``` + +### 3. 子状态的使用 + +子状态是父状态的内部状态,当进入父状态时,会自动进入子状态的初始状态。 + +```java +// 从 IDLE 转换到 PREPARING(PROCESSING 的子状态) +stateMachine.sendEvent(Event.START); +// 状态机现在处于 PROCESSING 父状态,具体是 PREPARING 子状态 +``` + +### 4. 状态机实例管理 + +每次调用 `getStateMachine()` 都会创建新的状态机实例。如果需要复用实例,需要自己管理: + +```java +// 方式一:每次创建新实例(推荐用于独立任务) +StateMachine sm1 = stateMachineFactory.getStateMachine(); +StateMachine sm2 = stateMachineFactory.getStateMachine(); + +// 方式二:复用实例(需要自己管理生命周期) +StateMachine sm = stateMachineFactory.getStateMachine(); +// ... 使用 sm +sm.stop(); // 使用完后停止 +``` + +### 5. 监听器中的空指针异常 + +某些转换(如初始转换)可能没有 trigger,需要检查 null: + +```java +@Override +public void transitionStarted(Transition transition) { + if (transition.getTrigger() != null) { + logger.info("事件: " + transition.getTrigger().getEvent()); + } else { + logger.info("事件: null (可能是初始转换)"); + } +} +``` + +### 6. 通过ID获取状态机 + +使用 `StateMachineManager` 可以方便地管理多个状态机: + +```java +@Autowired +private StateMachineManager stateMachineManager; + +// 获取状态机 +StateMachine sm = stateMachineManager.getStateMachine("machine-id"); + +// 如果不存在,创建新实例 +StateMachine sm = stateMachineManager.getOrCreateStateMachine("machine-id"); + +// 查询当前状态 +Status status = stateMachineManager.getCurrentStatus("machine-id"); + +// 获取详细信息 +StateMachineManager.StateMachineInfo info = + stateMachineManager.getStateMachineInfo("machine-id"); +``` + +## 最佳实践 + +1. **使用 Guard 进行条件检查**:在转换前验证条件,避免无效转换 +2. **使用 Action 执行业务逻辑**:将业务逻辑放在 Action 中,保持代码清晰 +3. **合理使用扩展状态**:存储任务相关的变量,便于在 Action 和 Guard 中使用 +4. **添加监听器**:监听状态机事件,便于调试和监控 +5. **管理状态机生命周期**:及时启动和停止状态机,避免资源泄漏 +6. **处理异常情况**:在 Guard 中检查条件,在 Action 中处理异常 + +## 参考资源 + +- [Spring StateMachine 官方文档](https://docs.spring.io/spring-statemachine/docs/current/reference/) +- [Spring StateMachine API 文档](https://docs.spring.io/spring-statemachine/docs/current/api/) + +## 示例代码位置 + +- **配置类**:`com.tuoheng.status.statemachine.config.TaskStateMachineConfig` +- **状态机管理器**:`com.tuoheng.status.statemachine.manager.StateMachineManager` +- **管理器服务类**:`com.tuoheng.status.statemachine.service.StateMachineManagerService` +- **服务类**:`com.tuoheng.status.statemachine.service.TaskStateMachineService` +- **演示类**:`com.tuoheng.status.statemachine.demo.StateMachineDemo` +- **测试类**:`com.tuoheng.status.statemachine.TaskStateMachineTest` +- **管理器测试类**:`com.tuoheng.status.statemachine.StateMachineManagerTest` diff --git a/src/main/java/com/tuoheng/status/statemachine/action/TaskAction.java b/src/main/java/com/tuoheng/status/statemachine/action/TaskAction.java new file mode 100644 index 0000000..15e21ed --- /dev/null +++ b/src/main/java/com/tuoheng/status/statemachine/action/TaskAction.java @@ -0,0 +1,142 @@ +package com.tuoheng.status.statemachine.action; + +import com.tuoheng.status.statemachine.events.Event; +import com.tuoheng.status.statemachine.status.Status; +import org.springframework.statemachine.StateContext; +import org.springframework.statemachine.action.Action; + +import java.util.logging.Logger; + +/** + * 任务 Action 示例 + * Action 在状态转换时执行,用于执行业务逻辑 + */ +public class TaskAction { + + private static final Logger logger = Logger.getLogger(TaskAction.class.getName()); + + /** + * 开始处理 Action + */ + public static class StartAction implements Action { + @Override + public void execute(StateContext context) { + logger.info("=== Action: 开始处理任务 ==="); + logger.info("从状态: " + context.getSource().getId()); + logger.info("到状态: " + context.getTarget().getId()); + logger.info("事件: " + context.getEvent()); + + // 可以在这里执行业务逻辑,比如初始化任务数据 + Object taskId = context.getExtendedState().getVariables().get("taskId"); + logger.info("任务ID: " + taskId); + } + } + + /** + * 准备 Action + */ + public static class PrepareAction implements Action { + @Override + public void execute(StateContext context) { + logger.info("=== Action: 准备任务资源 ==="); + logger.info("当前状态: " + context.getStateMachine().getState().getId()); + + // 模拟准备资源 + try { + Thread.sleep(100); // 模拟耗时操作 + logger.info("资源准备完成"); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } + + /** + * 执行 Action + */ + public static class ExecuteAction implements Action { + @Override + public void execute(StateContext context) { + logger.info("=== Action: 执行任务 ==="); + logger.info("当前状态: " + context.getStateMachine().getState().getId()); + + // 模拟执行任务 + try { + Thread.sleep(200); // 模拟耗时操作 + logger.info("任务执行完成"); + + // 可以在扩展状态中存储执行结果 + context.getExtendedState().getVariables().put("executionResult", "success"); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } + + /** + * 验证 Action + */ + public static class ValidateAction implements Action { + @Override + public void execute(StateContext context) { + logger.info("=== Action: 验证任务结果 ==="); + logger.info("当前状态: " + context.getStateMachine().getState().getId()); + + // 获取执行结果 + Object result = context.getExtendedState().getVariables().get("executionResult"); + logger.info("执行结果: " + result); + + // 模拟验证 + boolean isValid = result != null && "success".equals(result); + context.getExtendedState().getVariables().put("validationResult", isValid); + logger.info("验证结果: " + isValid); + } + } + + /** + * 完成 Action + */ + public static class CompleteAction implements Action { + @Override + public void execute(StateContext context) { + logger.info("=== Action: 任务完成 ==="); + logger.info("从状态: " + context.getSource().getId()); + logger.info("到状态: " + context.getTarget().getId()); + + // 清理资源或执行完成后的操作 + logger.info("清理资源..."); + } + } + + /** + * 失败 Action + */ + public static class FailAction implements Action { + @Override + public void execute(StateContext context) { + logger.warning("=== Action: 任务失败 ==="); + logger.warning("从状态: " + context.getSource().getId()); + logger.warning("到状态: " + context.getTarget().getId()); + + // 记录失败信息 + Exception exception = context.getException(); + if (exception != null) { + logger.warning("失败原因: " + exception.getMessage()); + } + } + } + + /** + * 重置 Action + */ + public static class ResetAction implements Action { + @Override + public void execute(StateContext context) { + logger.info("=== Action: 重置状态机 ==="); + + // 清理扩展状态中的变量 + context.getExtendedState().getVariables().clear(); + logger.info("状态机已重置"); + } + } +} diff --git a/src/main/java/com/tuoheng/status/statemachine/config/TaskStateMachineConfig.java b/src/main/java/com/tuoheng/status/statemachine/config/TaskStateMachineConfig.java new file mode 100644 index 0000000..7e63342 --- /dev/null +++ b/src/main/java/com/tuoheng/status/statemachine/config/TaskStateMachineConfig.java @@ -0,0 +1,182 @@ +package com.tuoheng.status.statemachine.config; + +import com.tuoheng.status.statemachine.action.TaskAction; +import com.tuoheng.status.statemachine.events.Event; +import com.tuoheng.status.statemachine.guard.TaskGuard; +import com.tuoheng.status.statemachine.listener.TaskStateMachineListener; +import com.tuoheng.status.statemachine.status.Status; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.statemachine.StateMachine; +import org.springframework.statemachine.config.StateMachineBuilder; +import org.springframework.statemachine.config.StateMachineFactory; +import org.springframework.statemachine.config.builders.StateMachineConfigurationConfigurer; +import org.springframework.statemachine.config.builders.StateMachineStateConfigurer; +import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer; + +import java.util.EnumSet; +import java.util.UUID; +import java.util.logging.Logger; + +/** + * 状态机配置类 + * 配置状态、子状态、转换、Action、Guard 等 + */ +@Configuration +public class TaskStateMachineConfig { + + private static final Logger logger = Logger.getLogger(TaskStateMachineConfig.class.getName()); + + /** + * 构建状态机工厂 + * 使用工厂模式可以为每个任务创建独立的状态机实例 + */ + @Bean + public StateMachineFactory stateMachineFactory() throws Exception { + + // 返回一个简单的 StateMachineFactory 实现 + return new StateMachineFactory() { + @Override + public StateMachine getStateMachine() { + return getStateMachine(UUID.randomUUID().toString()); + } + + @Override + public StateMachine getStateMachine(String machineId) { + try { + // 为每个请求创建新的状态机实例 + StateMachineBuilder.Builder newBuilder = StateMachineBuilder.builder(); + configureStateMachine(newBuilder); + configureStates(newBuilder); + configureTransitions(newBuilder); + StateMachine stateMachine = newBuilder.build(); + stateMachine.getExtendedState().getVariables().put("machineId", machineId); + return stateMachine; + } catch (Exception e) { + throw new RuntimeException("Failed to create state machine", e); + } + } + + @Override + public StateMachine getStateMachine(UUID uuid) { + return getStateMachine(uuid.toString()); + } + }; + } + + /** + * 配置状态机基本设置 + */ + private void configureStateMachine(StateMachineBuilder.Builder builder) throws Exception { + builder.configureConfiguration() + .withConfiguration() + .autoStartup(true) // 自动启动 + .listener(new TaskStateMachineListener()); // 添加监听器 + } + + /** + * 配置状态和子状态 + */ + private void configureStates(StateMachineBuilder.Builder builder) throws Exception { + builder.configureStates() + .withStates() + // 初始状态 + .initial(Status.IDLE) + + // 定义主状态 + .states(EnumSet.of(Status.IDLE, Status.COMPLETED, Status.FAILED)) + + // PROCESSING 是父状态,包含子状态 + .and() + .withStates() + .parent(Status.PROCESSING) // 设置 PROCESSING 为父状态 + .initial(Status.PREPARING) // 子状态的初始状态是 PREPARING + .states(EnumSet.of(Status.PREPARING, Status.EXECUTING, Status.VALIDATING)); + } + + /** + * 配置状态转换 + */ + private void configureTransitions(StateMachineBuilder.Builder builder) throws Exception { + builder.configureTransitions() + // IDLE -> PROCESSING (PREPARING) + .withExternal() + .source(Status.IDLE) + .target(Status.PREPARING) + .event(Event.START) + .action(new TaskAction.StartAction()) + .guard(new TaskGuard.CanStartGuard()) + .and() + + // PREPARING -> EXECUTING + .withExternal() + .source(Status.PREPARING) + .target(Status.EXECUTING) + .event(Event.PREPARE) + .action(new TaskAction.PrepareAction()) + .guard(new TaskGuard.PrepareCompleteGuard()) + .and() + + // EXECUTING -> VALIDATING + .withExternal() + .source(Status.EXECUTING) + .target(Status.VALIDATING) + .event(Event.EXECUTE) + .action(new TaskAction.ExecuteAction()) + .guard(new TaskGuard.ExecutionSuccessGuard()) + .and() + + // VALIDATING -> COMPLETED (从子状态退出到主状态) + .withExternal() + .source(Status.VALIDATING) + .target(Status.COMPLETED) + .event(Event.VALIDATE) + .action(new TaskAction.ValidateAction()) + .guard(new TaskGuard.ValidationPassGuard()) + .and() + + // 任何 PROCESSING 子状态 -> FAILED + .withExternal() + .source(Status.PREPARING) + .target(Status.FAILED) + .event(Event.FAIL) + .action(new TaskAction.FailAction()) + .and() + .withExternal() + .source(Status.EXECUTING) + .target(Status.FAILED) + .event(Event.FAIL) + .action(new TaskAction.FailAction()) + .and() + .withExternal() + .source(Status.VALIDATING) + .target(Status.FAILED) + .event(Event.FAIL) + .action(new TaskAction.FailAction()) + .and() + + // FAILED -> PREPARING (重试) + .withExternal() + .source(Status.FAILED) + .target(Status.PREPARING) + .event(Event.RETRY) + .action(new TaskAction.StartAction()) + .guard(new TaskGuard.CanRetryGuard()) + .and() + + // COMPLETED -> IDLE (重置) + .withExternal() + .source(Status.COMPLETED) + .target(Status.IDLE) + .event(Event.RESET) + .action(new TaskAction.ResetAction()) + .and() + + // FAILED -> IDLE (重置) + .withExternal() + .source(Status.FAILED) + .target(Status.IDLE) + .event(Event.RESET) + .action(new TaskAction.ResetAction()); + } +} diff --git a/src/main/java/com/tuoheng/status/statemachine/demo/StateMachineDemo.java b/src/main/java/com/tuoheng/status/statemachine/demo/StateMachineDemo.java new file mode 100644 index 0000000..c5ebc39 --- /dev/null +++ b/src/main/java/com/tuoheng/status/statemachine/demo/StateMachineDemo.java @@ -0,0 +1,94 @@ +package com.tuoheng.status.statemachine.demo; + +import com.tuoheng.status.statemachine.service.TaskStateMachineService; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; + +/** + * 状态机演示主类 + * 可以直接运行此类的 main 方法来查看状态机的运行效果 + */ +public class StateMachineDemo { + + public static void main(String[] args) { + // 创建 Spring 上下文 + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + + // 扫描配置类和服务类 + context.scan( + "com.tuoheng.status.statemachine.config", + "com.tuoheng.status.statemachine.service" + ); + context.refresh(); + + // 获取服务实例 + TaskStateMachineService service = context.getBean(TaskStateMachineService.class); + + System.out.println("\n\n"); + System.out.println("╔════════════════════════════════════════════════════════════╗"); + System.out.println("║ Spring StateMachine 完整示例演示 ║"); + System.out.println("║ 包含:子状态、Action、Guard、事件监听器 ║"); + System.out.println("╚════════════════════════════════════════════════════════════╝"); + System.out.println("\n"); + + // 演示1: 正常流程 + System.out.println("═══════════════════════════════════════════════════════════════"); + System.out.println("演示1: 正常流程"); + System.out.println("流程: IDLE -> PREPARING -> EXECUTING -> VALIDATING -> COMPLETED"); + System.out.println("═══════════════════════════════════════════════════════════════"); + service.startTask("demo-task-001"); + + // 等待一下 + try { + Thread.sleep(500); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + // 演示2: 失败和重试 + System.out.println("\n"); + System.out.println("═══════════════════════════════════════════════════════════════"); + System.out.println("演示2: 失败和重试流程"); + System.out.println("流程: IDLE -> PREPARING -> FAILED -> PREPARING (重试)"); + System.out.println("═══════════════════════════════════════════════════════════════"); + service.demonstrateFailureAndRetry("demo-task-002"); + + // 等待一下 + try { + Thread.sleep(500); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + // 演示3: Guard 条件不满足 + System.out.println("\n"); + System.out.println("═══════════════════════════════════════════════════════════════"); + System.out.println("演示3: Guard 条件不满足"); + System.out.println("说明: 不设置 taskId,Guard 会阻止状态转换"); + System.out.println("═══════════════════════════════════════════════════════════════"); + service.demonstrateGuardFailure("demo-task-003"); + + // 等待一下 + try { + Thread.sleep(500); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + // 演示4: 重置流程 + System.out.println("\n"); + System.out.println("═══════════════════════════════════════════════════════════════"); + System.out.println("演示4: 重置流程"); + System.out.println("流程: COMPLETED -> IDLE (重置)"); + System.out.println("═══════════════════════════════════════════════════════════════"); + service.demonstrateReset("demo-task-004"); + + System.out.println("\n\n"); + System.out.println("╔════════════════════════════════════════════════════════════╗"); + System.out.println("║ 所有演示完成! ║"); + System.out.println("╚════════════════════════════════════════════════════════════╝"); + System.out.println("\n"); + + // 关闭上下文 + context.close(); + } +} diff --git a/src/main/java/com/tuoheng/status/statemachine/demo/StateMachineManagerDemo.java b/src/main/java/com/tuoheng/status/statemachine/demo/StateMachineManagerDemo.java new file mode 100644 index 0000000..619d4f9 --- /dev/null +++ b/src/main/java/com/tuoheng/status/statemachine/demo/StateMachineManagerDemo.java @@ -0,0 +1,161 @@ +package com.tuoheng.status.statemachine.demo; + +import com.tuoheng.status.statemachine.events.Event; +import com.tuoheng.status.statemachine.manager.StateMachineManager; +import com.tuoheng.status.statemachine.service.StateMachineManagerService; +import com.tuoheng.status.statemachine.status.Status; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; + +import java.util.Set; + +/** + * 状态机管理器演示类 + * 演示如何通过ID管理多个状态机实例 + */ +public class StateMachineManagerDemo { + + public static void main(String[] args) { + // 创建 Spring 上下文 + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.scan( + "com.tuoheng.status.statemachine.config", + "com.tuoheng.status.statemachine.manager", + "com.tuoheng.status.statemachine.service" + ); + context.refresh(); + + // 获取服务实例 + StateMachineManagerService service = context.getBean(StateMachineManagerService.class); + StateMachineManager manager = context.getBean(StateMachineManager.class); + + System.out.println("\n\n"); + System.out.println("╔════════════════════════════════════════════════════════════╗"); + System.out.println("║ Spring StateMachine 管理器演示 ║"); + System.out.println("║ 演示如何通过ID管理多个状态机实例 ║"); + System.out.println("╚════════════════════════════════════════════════════════════╝"); + System.out.println("\n"); + + // 演示1: 创建多个状态机 + System.out.println("═══════════════════════════════════════════════════════════════"); + System.out.println("演示1: 创建多个状态机实例"); + System.out.println("═══════════════════════════════════════════════════════════════"); + + String machineId1 = "machine-001"; + String machineId2 = "machine-002"; + String machineId3 = "machine-003"; + + // 创建状态机 + service.startTask(machineId1, "task-001"); + service.startTask(machineId2, "task-002"); + service.startTask(machineId3, "task-003"); + + System.out.println("创建了3个状态机:"); + System.out.println(" - " + machineId1); + System.out.println(" - " + machineId2); + System.out.println(" - " + machineId3); + System.out.println("管理的状态机数量: " + manager.getStateMachineCount()); + + // 等待状态转换 + try { + Thread.sleep(200); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + // 演示2: 查询不同状态机的状态 + System.out.println("\n"); + System.out.println("═══════════════════════════════════════════════════════════════"); + System.out.println("演示2: 查询不同状态机的当前状态"); + System.out.println("═══════════════════════════════════════════════════════════════"); + + Status status1 = service.getCurrentStatus(machineId1); + Status status2 = service.getCurrentStatus(machineId2); + Status status3 = service.getCurrentStatus(machineId3); + + System.out.println("状态机 " + machineId1 + " 当前状态: " + status1); + System.out.println("状态机 " + machineId2 + " 当前状态: " + status2); + System.out.println("状态机 " + machineId3 + " 当前状态: " + status3); + + // 演示3: 获取状态机详细信息 + System.out.println("\n"); + System.out.println("═══════════════════════════════════════════════════════════════"); + System.out.println("演示3: 获取状态机详细信息"); + System.out.println("═══════════════════════════════════════════════════════════════"); + + StateMachineManager.StateMachineInfo info1 = service.getStateMachineInfo(machineId1); + System.out.println("状态机1详细信息:"); + System.out.println(" ID: " + info1.getMachineId()); + System.out.println(" 当前状态: " + info1.getCurrentStatus()); + System.out.println(" 是否子状态: " + info1.isSubState()); + System.out.println(" 是否运行中: " + info1.isRunning()); + System.out.println(" 扩展状态: " + info1.getExtendedState()); + + // 演示4: 为不同状态机发送不同事件 + System.out.println("\n"); + System.out.println("═══════════════════════════════════════════════════════════════"); + System.out.println("演示4: 为不同状态机发送不同事件"); + System.out.println("═══════════════════════════════════════════════════════════════"); + + // 为 machine-001 继续处理 + manager.getStateMachine(machineId1) + .getExtendedState().getVariables().put("prepared", true); + service.sendEvent(machineId1, Event.PREPARE); + + // 为 machine-002 发送失败事件 + service.sendEvent(machineId2, Event.FAIL); + + try { + Thread.sleep(200); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + System.out.println("machine-001 发送 PREPARE 事件后状态: " + service.getCurrentStatus(machineId1)); + System.out.println("machine-002 发送 FAIL 事件后状态: " + service.getCurrentStatus(machineId2)); + System.out.println("machine-003 状态保持不变: " + service.getCurrentStatus(machineId3)); + + // 演示5: 获取所有状态机ID + System.out.println("\n"); + System.out.println("═══════════════════════════════════════════════════════════════"); + System.out.println("演示5: 获取所有状态机ID"); + System.out.println("═══════════════════════════════════════════════════════════════"); + + Set allIds = service.getAllMachineIds(); + System.out.println("所有状态机ID: " + allIds); + System.out.println("状态机数量: " + allIds.size()); + + // 演示6: 通过ID直接获取状态机并操作 + System.out.println("\n"); + System.out.println("═══════════════════════════════════════════════════════════════"); + System.out.println("演示6: 通过ID直接获取状态机并操作"); + System.out.println("═══════════════════════════════════════════════════════════════"); + + var stateMachine = manager.getStateMachine(machineId1); + if (stateMachine != null) { + System.out.println("成功获取状态机: " + machineId1); + System.out.println("当前状态: " + stateMachine.getState().getId()); + System.out.println("扩展状态中的 taskId: " + + stateMachine.getExtendedState().getVariables().get("taskId")); + } + + // 演示7: 移除状态机 + System.out.println("\n"); + System.out.println("═══════════════════════════════════════════════════════════════"); + System.out.println("演示7: 移除状态机"); + System.out.println("═══════════════════════════════════════════════════════════════"); + + boolean removed = service.removeStateMachine(machineId3); + System.out.println("移除状态机 " + machineId3 + ": " + (removed ? "成功" : "失败")); + System.out.println("移除后管理的状态机数量: " + manager.getStateMachineCount()); + System.out.println("状态机 " + machineId3 + " 是否存在: " + service.exists(machineId3)); + + System.out.println("\n\n"); + System.out.println("╔════════════════════════════════════════════════════════════╗"); + System.out.println("║ 所有演示完成! ║"); + System.out.println("╚════════════════════════════════════════════════════════════╝"); + System.out.println("\n"); + + // 关闭上下文 + context.close(); + } +} diff --git a/src/main/java/com/tuoheng/status/statemachine/events/Event.java b/src/main/java/com/tuoheng/status/statemachine/events/Event.java new file mode 100644 index 0000000..80a4925 --- /dev/null +++ b/src/main/java/com/tuoheng/status/statemachine/events/Event.java @@ -0,0 +1,15 @@ +package com.tuoheng.status.statemachine.events; + +/** + * 事件枚举 + */ +public enum Event { + START, // 开始处理 + PREPARE, // 准备就绪 + EXECUTE, // 执行 + VALIDATE, // 验证 + COMPLETE, // 完成 + FAIL, // 失败 + RETRY, // 重试 + RESET // 重置 +} diff --git a/src/main/java/com/tuoheng/status/statemachine/guard/TaskGuard.java b/src/main/java/com/tuoheng/status/statemachine/guard/TaskGuard.java new file mode 100644 index 0000000..b0bb45c --- /dev/null +++ b/src/main/java/com/tuoheng/status/statemachine/guard/TaskGuard.java @@ -0,0 +1,112 @@ +package com.tuoheng.status.statemachine.guard; + +import com.tuoheng.status.statemachine.events.Event; +import com.tuoheng.status.statemachine.status.Status; +import org.springframework.statemachine.StateContext; +import org.springframework.statemachine.guard.Guard; + +import java.util.logging.Logger; + +/** + * 任务 Guard 示例 + * Guard 用于条件判断,决定是否允许状态转换 + */ +public class TaskGuard { + + private static final Logger logger = Logger.getLogger(TaskGuard.class.getName()); + + /** + * 检查是否可以开始处理 + */ + public static class CanStartGuard implements Guard { + @Override + public boolean evaluate(StateContext context) { + logger.info("=== Guard: 检查是否可以开始处理 ==="); + + // 检查是否有任务ID + Object taskId = context.getExtendedState().getVariables().get("taskId"); + boolean canStart = taskId != null; + + logger.info("任务ID存在: " + canStart); + return canStart; + } + } + + /** + * 检查准备是否完成 + */ + public static class PrepareCompleteGuard implements Guard { + @Override + public boolean evaluate(StateContext context) { + logger.info("=== Guard: 检查准备是否完成 ==="); + + // 检查准备状态 + Object prepared = context.getExtendedState().getVariables().get("prepared"); + boolean isPrepared = prepared != null && Boolean.TRUE.equals(prepared); + + logger.info("准备完成: " + isPrepared); + return isPrepared; + } + } + + /** + * 检查执行是否成功 + */ + public static class ExecutionSuccessGuard implements Guard { + @Override + public boolean evaluate(StateContext context) { + logger.info("=== Guard: 检查执行是否成功 ==="); + + // 检查执行结果 + Object result = context.getExtendedState().getVariables().get("executionResult"); + boolean isSuccess = result != null && "success".equals(result); + + logger.info("执行成功: " + isSuccess); + return isSuccess; + } + } + + /** + * 检查验证是否通过 + */ + public static class ValidationPassGuard implements Guard { + @Override + public boolean evaluate(StateContext context) { + logger.info("=== Guard: 检查验证是否通过 ==="); + + // 检查验证结果 + Object validationResult = context.getExtendedState().getVariables().get("validationResult"); + boolean isValid = validationResult != null && Boolean.TRUE.equals(validationResult); + + logger.info("验证通过: " + isValid); + return isValid; + } + } + + /** + * 检查是否可以重试 + */ + public static class CanRetryGuard implements Guard { + @Override + public boolean evaluate(StateContext context) { + logger.info("=== Guard: 检查是否可以重试 ==="); + + // 检查重试次数 + Integer retryCount = (Integer) context.getExtendedState().getVariables().getOrDefault("retryCount", 0); + boolean canRetry = retryCount < 3; // 最多重试3次 + + logger.info("当前重试次数: " + retryCount + ", 可以重试: " + canRetry); + return canRetry; + } + } + + /** + * 总是返回 true 的 Guard(用于无条件转换) + */ + public static class AlwaysTrueGuard implements Guard { + @Override + public boolean evaluate(StateContext context) { + return true; + } + } +} diff --git a/src/main/java/com/tuoheng/status/statemachine/listener/TaskStateMachineListener.java b/src/main/java/com/tuoheng/status/statemachine/listener/TaskStateMachineListener.java new file mode 100644 index 0000000..a097901 --- /dev/null +++ b/src/main/java/com/tuoheng/status/statemachine/listener/TaskStateMachineListener.java @@ -0,0 +1,157 @@ +package com.tuoheng.status.statemachine.listener; + +import com.tuoheng.status.statemachine.events.Event; +import com.tuoheng.status.statemachine.status.Status; +import org.springframework.statemachine.StateContext; +import org.springframework.statemachine.StateMachine; +import org.springframework.statemachine.listener.StateMachineListener; +import org.springframework.statemachine.state.State; +import org.springframework.statemachine.transition.Transition; + +import java.util.logging.Logger; + +/** + * 状态机事件监听器 + * 监听状态机的各种事件,如状态变化、转换等 + */ +public class TaskStateMachineListener implements StateMachineListener { + + private static final Logger logger = Logger.getLogger(TaskStateMachineListener.class.getName()); + + /** + * 状态机启动时调用 + */ + @Override + public void stateMachineStarted(StateMachine stateMachine) { + logger.info("========== 状态机启动 =========="); + if (stateMachine.getInitialState() != null) { + logger.info("初始状态: " + stateMachine.getInitialState().getId()); + } else { + logger.info("初始状态: null"); + } + } + + /** + * 状态机停止时调用 + */ + @Override + public void stateMachineStopped(StateMachine stateMachine) { + logger.info("========== 状态机停止 =========="); + if (stateMachine.getState() != null) { + logger.info("最终状态: " + stateMachine.getState().getId()); + } else { + logger.info("最终状态: null"); + } + } + + /** + * 状态机错误时调用 + */ + @Override + public void stateMachineError(StateMachine stateMachine, Exception exception) { + logger.severe("========== 状态机错误 =========="); + logger.severe("错误信息: " + exception.getMessage()); + exception.printStackTrace(); + } + + /** + * 状态进入时调用 + */ + @Override + public void stateEntered(State state) { + logger.info("========== 进入状态: " + state.getId() + " =========="); + } + + /** + * 状态退出时调用 + */ + @Override + public void stateExited(State state) { + logger.info("========== 退出状态: " + state.getId() + " =========="); + } + + /** + * 状态改变时调用 + */ + @Override + public void stateChanged(State from, State to) { + logger.info("========== 状态改变 =========="); + logger.info("从: " + (from != null ? from.getId() : "null")); + logger.info("到: " + (to != null ? to.getId() : "null")); + } + + /** + * 转换开始时调用 + */ + @Override + public void transitionStarted(Transition transition) { + logger.info("========== 转换开始 =========="); + logger.info("从: " + (transition.getSource() != null ? transition.getSource().getId() : "null")); + logger.info("到: " + (transition.getTarget() != null ? transition.getTarget().getId() : "null")); + // 检查 trigger 是否为 null(某些转换可能没有 trigger,比如初始转换) + if (transition.getTrigger() != null) { + logger.info("事件: " + transition.getTrigger().getEvent()); + } else { + logger.info("事件: null (可能是初始转换或内部转换)"); + } + } + + /** + * 转换结束时调用 + */ + @Override + public void transitionEnded(Transition transition) { + logger.info("========== 转换结束 =========="); + logger.info("从: " + (transition.getSource() != null ? transition.getSource().getId() : "null")); + logger.info("到: " + (transition.getTarget() != null ? transition.getTarget().getId() : "null")); + // 检查 trigger 是否为 null + if (transition.getTrigger() != null) { + logger.info("事件: " + transition.getTrigger().getEvent()); + } else { + logger.info("事件: null (可能是初始转换或内部转换)"); + } + } + + /** + * 转换选择时调用 + */ + @Override + public void transition(Transition transition) { + logger.info("========== 转换选择 =========="); + logger.info("从: " + (transition.getSource() != null ? transition.getSource().getId() : "null")); + logger.info("到: " + (transition.getTarget() != null ? transition.getTarget().getId() : "null")); + // 检查 trigger 是否为 null + if (transition.getTrigger() != null) { + logger.info("事件: " + transition.getTrigger().getEvent()); + } else { + logger.info("事件: null (可能是初始转换或内部转换)"); + } + } + + /** + * 事件未接受时调用(没有匹配的转换) + */ + @Override + public void eventNotAccepted(org.springframework.messaging.Message event) { + logger.warning("========== 事件未接受 =========="); + logger.warning("事件: " + event.getPayload()); + logger.warning("当前状态可能不支持此事件"); + } + + /** + * 扩展状态改变时调用 + */ + @Override + public void extendedStateChanged(Object key, Object value) { + logger.info("========== 扩展状态改变 =========="); + logger.info("键: " + key + ", 值: " + value); + } + + /** + * 状态上下文入口时调用 + */ + @Override + public void stateContext(StateContext stateContext) { + // 可以在这里记录状态上下文信息 + } +} diff --git a/src/main/java/com/tuoheng/status/statemachine/manager/StateMachineManager.java b/src/main/java/com/tuoheng/status/statemachine/manager/StateMachineManager.java new file mode 100644 index 0000000..641bdd9 --- /dev/null +++ b/src/main/java/com/tuoheng/status/statemachine/manager/StateMachineManager.java @@ -0,0 +1,262 @@ +package com.tuoheng.status.statemachine.manager; + +import com.tuoheng.status.statemachine.events.Event; +import com.tuoheng.status.statemachine.status.Status; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.statemachine.StateMachine; +import org.springframework.statemachine.config.StateMachineFactory; +import org.springframework.statemachine.state.State; +import org.springframework.stereotype.Component; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Logger; + +/** + * 状态机管理器 + * 用于管理多个状态机实例,通过ID获取和查询状态机 + */ +@Component +public class StateMachineManager { + + private static final Logger logger = Logger.getLogger(StateMachineManager.class.getName()); + + @Autowired + private StateMachineFactory stateMachineFactory; + + // 存储状态机实例的Map,key为状态机ID + private final Map> stateMachineMap = new ConcurrentHashMap<>(); + + /** + * 创建或获取状态机实例 + * 如果ID对应的状态机不存在,则创建新实例;如果存在,则返回现有实例 + * + * @param machineId 状态机ID + * @return 状态机实例 + */ + public StateMachine getOrCreateStateMachine(String machineId) { + return stateMachineMap.computeIfAbsent(machineId, id -> { + logger.info("创建新状态机实例,ID: " + id); + StateMachine stateMachine = stateMachineFactory.getStateMachine(id); + // 将machineId存储到扩展状态中 + stateMachine.getExtendedState().getVariables().put("machineId", id); + // 启动状态机 + stateMachine.start(); + return stateMachine; + }); + } + + /** + * 获取状态机实例(如果不存在则返回null) + * + * @param machineId 状态机ID + * @return 状态机实例,如果不存在则返回null + */ + public StateMachine getStateMachine(String machineId) { + return stateMachineMap.get(machineId); + } + + /** + * 创建新的状态机实例(如果已存在则先停止并移除旧的) + * + * @param machineId 状态机ID + * @return 新创建的状态机实例 + */ + public StateMachine createStateMachine(String machineId) { + // 如果已存在,先停止并移除 + StateMachine existing = stateMachineMap.remove(machineId); + if (existing != null) { + logger.info("停止并移除已存在的状态机,ID: " + machineId); + try { + existing.stop(); + } catch (Exception e) { + logger.warning("停止状态机时发生错误: " + e.getMessage()); + } + } + + // 创建新实例 + logger.info("创建新状态机实例,ID: " + machineId); + StateMachine stateMachine = stateMachineFactory.getStateMachine(machineId); + stateMachine.getExtendedState().getVariables().put("machineId", machineId); + stateMachine.start(); + stateMachineMap.put(machineId, stateMachine); + return stateMachine; + } + + /** + * 获取状态机的当前状态 + * + * @param machineId 状态机ID + * @return 当前状态,如果状态机不存在则返回null + */ + public Status getCurrentStatus(String machineId) { + StateMachine stateMachine = stateMachineMap.get(machineId); + if (stateMachine == null) { + logger.warning("状态机不存在,ID: " + machineId); + return null; + } + + State state = stateMachine.getState(); + if (state == null) { + return null; + } + + return state.getId(); + } + + /** + * 获取状态机的详细信息(包括当前状态和扩展状态) + * + * @param machineId 状态机ID + * @return 状态机信息,如果状态机不存在则返回null + */ + public StateMachineInfo getStateMachineInfo(String machineId) { + StateMachine stateMachine = stateMachineMap.get(machineId); + if (stateMachine == null) { + return null; + } + + StateMachineInfo info = new StateMachineInfo(); + info.setMachineId(machineId); + + State state = stateMachine.getState(); + if (state != null) { + info.setCurrentStatus(state.getId()); + info.setIsSubState(state.isSubmachineState()); + } + + info.setExtendedState(stateMachine.getExtendedState().getVariables()); + // 通过检查状态机是否有状态来判断是否在运行 + // Spring StateMachine 3.2.0 没有 isRunning() 方法,使用状态判断 + info.setIsRunning(state != null); + + return info; + } + + /** + * 移除状态机实例 + * + * @param machineId 状态机ID + * @return 是否成功移除 + */ + public boolean removeStateMachine(String machineId) { + StateMachine stateMachine = stateMachineMap.remove(machineId); + if (stateMachine != null) { + logger.info("移除状态机实例,ID: " + machineId); + try { + stateMachine.stop(); + return true; + } catch (Exception e) { + logger.warning("停止状态机时发生错误: " + e.getMessage()); + return false; + } + } + return false; + } + + /** + * 检查状态机是否存在 + * + * @param machineId 状态机ID + * @return 是否存在 + */ + public boolean exists(String machineId) { + return stateMachineMap.containsKey(machineId); + } + + /** + * 获取所有状态机的ID列表 + * + * @return 状态机ID集合 + */ + public java.util.Set getAllMachineIds() { + return stateMachineMap.keySet(); + } + + /** + * 获取当前管理的状态机数量 + * + * @return 状态机数量 + */ + public int getStateMachineCount() { + return stateMachineMap.size(); + } + + /** + * 清空所有状态机实例 + */ + public void clearAll() { + logger.info("清空所有状态机实例,数量: " + stateMachineMap.size()); + for (Map.Entry> entry : stateMachineMap.entrySet()) { + try { + entry.getValue().stop(); + } catch (Exception e) { + logger.warning("停止状态机时发生错误,ID: " + entry.getKey() + ", 错误: " + e.getMessage()); + } + } + stateMachineMap.clear(); + } + + /** + * 状态机信息类 + */ + public static class StateMachineInfo { + private String machineId; + private Status currentStatus; + private boolean isSubState; + private boolean isRunning; + private Map extendedState; + + // Getters and Setters + public String getMachineId() { + return machineId; + } + + public void setMachineId(String machineId) { + this.machineId = machineId; + } + + public Status getCurrentStatus() { + return currentStatus; + } + + public void setCurrentStatus(Status currentStatus) { + this.currentStatus = currentStatus; + } + + public boolean isSubState() { + return isSubState; + } + + public void setIsSubState(boolean subState) { + isSubState = subState; + } + + public boolean isRunning() { + return isRunning; + } + + public void setIsRunning(boolean running) { + isRunning = running; + } + + public Map getExtendedState() { + return extendedState; + } + + public void setExtendedState(Map extendedState) { + this.extendedState = extendedState; + } + + @Override + public String toString() { + return "StateMachineInfo{" + + "machineId='" + machineId + '\'' + + ", currentStatus=" + currentStatus + + ", isSubState=" + isSubState + + ", isRunning=" + isRunning + + ", extendedState=" + extendedState + + '}'; + } + } +} diff --git a/src/main/java/com/tuoheng/status/statemachine/service/StateMachineManagerService.java b/src/main/java/com/tuoheng/status/statemachine/service/StateMachineManagerService.java new file mode 100644 index 0000000..717d863 --- /dev/null +++ b/src/main/java/com/tuoheng/status/statemachine/service/StateMachineManagerService.java @@ -0,0 +1,128 @@ +package com.tuoheng.status.statemachine.service; + +import com.tuoheng.status.statemachine.events.Event; +import com.tuoheng.status.statemachine.manager.StateMachineManager; +import com.tuoheng.status.statemachine.status.Status; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.statemachine.StateMachine; +import org.springframework.stereotype.Service; + +import java.util.Set; +import java.util.logging.Logger; + +/** + * 状态机管理器服务类 + * 提供便捷的方法来使用状态机管理器 + */ +@Service +public class StateMachineManagerService { + + private static final Logger logger = Logger.getLogger(StateMachineManagerService.class.getName()); + + @Autowired + private StateMachineManager stateMachineManager; + + /** + * 通过ID获取状态机并发送事件 + * + * @param machineId 状态机ID + * @param event 事件 + * @return 事件是否被接受 + */ + public boolean sendEvent(String machineId, Event event) { + StateMachine stateMachine = stateMachineManager.getStateMachine(machineId); + if (stateMachine == null) { + logger.warning("状态机不存在,ID: " + machineId); + return false; + } + return stateMachine.sendEvent(event); + } + + /** + * 通过ID获取状态机的当前状态 + * + * @param machineId 状态机ID + * @return 当前状态 + */ + public Status getCurrentStatus(String machineId) { + return stateMachineManager.getCurrentStatus(machineId); + } + + /** + * 通过ID获取状态机的详细信息 + * + * @param machineId 状态机ID + * @return 状态机信息 + */ + public StateMachineManager.StateMachineInfo getStateMachineInfo(String machineId) { + return stateMachineManager.getStateMachineInfo(machineId); + } + + /** + * 创建或获取状态机,并发送START事件 + * + * @param machineId 状态机ID + * @param taskId 任务ID(可选) + * @return 是否成功 + */ + public boolean startTask(String machineId, String taskId) { + StateMachine stateMachine = stateMachineManager.getOrCreateStateMachine(machineId); + + // 设置任务ID到扩展状态 + if (taskId != null) { + stateMachine.getExtendedState().getVariables().put("taskId", taskId); + } + + return stateMachine.sendEvent(Event.START); + } + + /** + * 获取所有状态机的ID列表 + * + * @return 状态机ID集合 + */ + public Set getAllMachineIds() { + return stateMachineManager.getAllMachineIds(); + } + + /** + * 移除状态机 + * + * @param machineId 状态机ID + * @return 是否成功移除 + */ + public boolean removeStateMachine(String machineId) { + return stateMachineManager.removeStateMachine(machineId); + } + + /** + * 检查状态机是否存在 + * + * @param machineId 状态机ID + * @return 是否存在 + */ + public boolean exists(String machineId) { + return stateMachineManager.exists(machineId); + } + + /** + * 打印状态机信息(用于调试) + * + * @param machineId 状态机ID + */ + public void printStateMachineInfo(String machineId) { + StateMachineManager.StateMachineInfo info = stateMachineManager.getStateMachineInfo(machineId); + if (info == null) { + logger.warning("状态机不存在,ID: " + machineId); + return; + } + + logger.info("========== 状态机信息 =========="); + logger.info("ID: " + info.getMachineId()); + logger.info("当前状态: " + info.getCurrentStatus()); + logger.info("是否子状态: " + info.isSubState()); + logger.info("是否运行中: " + info.isRunning()); + logger.info("扩展状态: " + info.getExtendedState()); + logger.info("================================"); + } +} diff --git a/src/main/java/com/tuoheng/status/statemachine/service/TaskStateMachineService.java b/src/main/java/com/tuoheng/status/statemachine/service/TaskStateMachineService.java new file mode 100644 index 0000000..beba3ef --- /dev/null +++ b/src/main/java/com/tuoheng/status/statemachine/service/TaskStateMachineService.java @@ -0,0 +1,194 @@ +package com.tuoheng.status.statemachine.service; + +import com.tuoheng.status.statemachine.events.Event; +import com.tuoheng.status.statemachine.status.Status; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.statemachine.StateMachine; +import org.springframework.statemachine.config.StateMachineFactory; +import org.springframework.stereotype.Service; + +import java.util.logging.Logger; + +/** + * 任务状态机服务示例 + * 演示如何使用状态机 + */ +@Service +public class TaskStateMachineService { + + private static final Logger logger = Logger.getLogger(TaskStateMachineService.class.getName()); + + @Autowired + private StateMachineFactory stateMachineFactory; + + /** + * 启动任务处理流程 + */ + public void startTask(String taskId) { + logger.info("\n========== 开始任务处理流程 =========="); + + StateMachine stateMachine = stateMachineFactory.getStateMachine(); + + try { + // 设置任务ID到扩展状态 + stateMachine.getExtendedState().getVariables().put("taskId", taskId); + stateMachine.getExtendedState().getVariables().put("retryCount", 0); + + // 启动状态机 + stateMachine.start(); + logger.info("状态机已启动,当前状态: " + stateMachine.getState().getId()); + + // 发送 START 事件 + logger.info("\n--- 发送 START 事件 ---"); + stateMachine.sendEvent(Event.START); + logger.info("当前状态: " + stateMachine.getState().getId()); + + // 模拟准备完成 + Thread.sleep(100); + stateMachine.getExtendedState().getVariables().put("prepared", true); + logger.info("\n--- 发送 PREPARE 事件 ---"); + stateMachine.sendEvent(Event.PREPARE); + logger.info("当前状态: " + stateMachine.getState().getId()); + + // 模拟执行完成(在发送事件之前设置结果,以便 Guard 可以检查) + Thread.sleep(200); + stateMachine.getExtendedState().getVariables().put("executionResult", "success"); + logger.info("\n--- 发送 EXECUTE 事件 ---"); + stateMachine.sendEvent(Event.EXECUTE); + logger.info("当前状态: " + stateMachine.getState().getId()); + + // 模拟验证完成(在发送事件之前设置验证结果,以便 Guard 可以检查) + Thread.sleep(100); + stateMachine.getExtendedState().getVariables().put("validationResult", true); + logger.info("\n--- 发送 VALIDATE 事件 ---"); + stateMachine.sendEvent(Event.VALIDATE); + logger.info("当前状态: " + stateMachine.getState().getId()); + + logger.info("\n========== 任务处理流程完成 ==========\n"); + + } catch (Exception e) { + logger.severe("处理任务时发生错误: " + e.getMessage()); + e.printStackTrace(); + } finally { + // 停止状态机 + stateMachine.stop(); + } + } + + /** + * 演示失败和重试流程 + */ + public void demonstrateFailureAndRetry(String taskId) { + logger.info("\n========== 演示失败和重试流程 =========="); + + StateMachine stateMachine = stateMachineFactory.getStateMachine(); + + try { + // 设置任务ID + stateMachine.getExtendedState().getVariables().put("taskId", taskId); + stateMachine.getExtendedState().getVariables().put("retryCount", 0); + + // 启动状态机 + stateMachine.start(); + logger.info("状态机已启动,当前状态: " + stateMachine.getState().getId()); + + // 发送 START 事件 + logger.info("\n--- 发送 START 事件 ---"); + stateMachine.sendEvent(Event.START); + logger.info("当前状态: " + stateMachine.getState().getId()); + + // 模拟失败 + logger.info("\n--- 发送 FAIL 事件(模拟失败)---"); + stateMachine.sendEvent(Event.FAIL); + logger.info("当前状态: " + stateMachine.getState().getId()); + + // 增加重试次数 + Integer retryCount = (Integer) stateMachine.getExtendedState().getVariables().getOrDefault("retryCount", 0); + stateMachine.getExtendedState().getVariables().put("retryCount", retryCount + 1); + + // 重试 + logger.info("\n--- 发送 RETRY 事件 ---"); + stateMachine.sendEvent(Event.RETRY); + logger.info("当前状态: " + stateMachine.getState().getId()); + + logger.info("\n========== 失败和重试流程演示完成 ==========\n"); + + } catch (Exception e) { + logger.severe("演示失败和重试流程时发生错误: " + e.getMessage()); + e.printStackTrace(); + } finally { + stateMachine.stop(); + } + } + + /** + * 演示 Guard 条件不满足的情况 + */ + public void demonstrateGuardFailure(String taskId) { + logger.info("\n========== 演示 Guard 条件不满足 =========="); + + StateMachine stateMachine = stateMachineFactory.getStateMachine(); + + try { + // 不设置 taskId,导致 CanStartGuard 失败 + // stateMachine.getExtendedState().getVariables().put("taskId", taskId); + + // 启动状态机 + stateMachine.start(); + logger.info("状态机已启动,当前状态: " + stateMachine.getState().getId()); + + // 发送 START 事件(应该失败,因为 Guard 条件不满足) + logger.info("\n--- 发送 START 事件(没有 taskId,Guard 应该失败)---"); + boolean accepted = stateMachine.sendEvent(Event.START); + logger.info("事件是否被接受: " + accepted); + logger.info("当前状态: " + stateMachine.getState().getId()); + + logger.info("\n========== Guard 条件不满足演示完成 ==========\n"); + + } catch (Exception e) { + logger.severe("演示 Guard 失败时发生错误: " + e.getMessage()); + e.printStackTrace(); + } finally { + stateMachine.stop(); + } + } + + /** + * 演示重置流程 + */ + public void demonstrateReset(String taskId) { + logger.info("\n========== 演示重置流程 =========="); + + StateMachine stateMachine = stateMachineFactory.getStateMachine(); + + try { + // 设置任务ID + stateMachine.getExtendedState().getVariables().put("taskId", taskId); + + // 启动状态机并完成流程 + stateMachine.start(); + stateMachine.sendEvent(Event.START); + stateMachine.getExtendedState().getVariables().put("prepared", true); + stateMachine.sendEvent(Event.PREPARE); + stateMachine.getExtendedState().getVariables().put("executionResult", "success"); + stateMachine.sendEvent(Event.EXECUTE); + stateMachine.getExtendedState().getVariables().put("validationResult", true); + stateMachine.sendEvent(Event.VALIDATE); + + logger.info("当前状态: " + stateMachine.getState().getId()); + + // 重置 + logger.info("\n--- 发送 RESET 事件 ---"); + stateMachine.sendEvent(Event.RESET); + logger.info("当前状态: " + stateMachine.getState().getId()); + + logger.info("\n========== 重置流程演示完成 ==========\n"); + + } catch (Exception e) { + logger.severe("演示重置流程时发生错误: " + e.getMessage()); + e.printStackTrace(); + } finally { + stateMachine.stop(); + } + } +} diff --git a/src/main/java/com/tuoheng/status/statemachine/status/Status.java b/src/main/java/com/tuoheng/status/statemachine/status/Status.java new file mode 100644 index 0000000..3f86a48 --- /dev/null +++ b/src/main/java/com/tuoheng/status/statemachine/status/Status.java @@ -0,0 +1,19 @@ +package com.tuoheng.status.statemachine.status; + +/** + * 状态枚举 + * 主状态:IDLE, PROCESSING, COMPLETED, FAILED + * PROCESSING 包含子状态:PREPARING, EXECUTING, VALIDATING + */ +public enum Status { + // 主状态 + IDLE, // 空闲状态 + PROCESSING, // 处理中(父状态,包含子状态) + COMPLETED, // 已完成 + FAILED, // 失败 + + // PROCESSING 的子状态 + PREPARING, // 准备中(PROCESSING 的子状态) + EXECUTING, // 执行中(PROCESSING 的子状态) + VALIDATING // 验证中(PROCESSING 的子状态) +} diff --git a/src/main/java/com/tuoheng/status/statemachine/无人机控制状态机设计文档.md b/src/main/java/com/tuoheng/status/statemachine/无人机控制状态机设计文档.md new file mode 100644 index 0000000..6c83ec0 --- /dev/null +++ b/src/main/java/com/tuoheng/status/statemachine/无人机控制状态机设计文档.md @@ -0,0 +1,765 @@ +# 大疆无人机巢状态机设计文档 + +## 一、概述 + +本文档基于大疆无人机巢功能统计清单,设计了一套完整的状态机模型,用于管理无人机和机巢的各种状态转换,确保操作的安全性和正确性。 + +### 1.1 设计目标 + +- **状态管理**:清晰定义无人机和机巢的所有可能状态 +- **状态转换**:明确状态之间的转换条件和规则 +- **业务规则**:将业务限制条件转化为状态机 Guard 和 Action +- **安全控制**:通过状态机确保操作的安全执行顺序 + +### 1.2 状态机层次结构 + +状态机采用**分层设计**,包含以下层次: + +1. **顶层状态机**:机巢整体状态 +2. **机巢子状态机**:机巢设备状态(舱门、电源等) +3. **无人机子状态机**:无人机状态(电源、飞行、任务等) +4. **模式状态机**:DRC模式、调试模式等 + +## 二、状态定义 + +### 2.1 机巢状态(AirportState) + +#### 主状态 + +| 状态 | 枚举值 | 说明 | +|------|--------|------| +| 离线 | `OFFLINE` | 机巢设备离线,无法通信 | +| 在线 | `ONLINE` | 机巢设备在线,可接收指令 | +| 重启中 | `REBOOTING` | 机巢正在重启 | + +#### 子状态(ONLINE 的子状态) + +| 状态 | 枚举值 | 说明 | +|------|--------|------| +| 待机 | `STANDBY` | 机巢待机状态(初始子状态) | +| 调试模式 | `DEBUG_MODE` | 调试模式开启中 | +| 操作中 | `OPERATING` | 正在执行操作(开舱、关舱等) | + +#### 舱门状态(OPERATING 的子状态) + +| 状态 | 枚举值 | 说明 | +|------|--------|------| +| 已关闭 | `COVER_CLOSED` | 舱门已关闭 | +| 已打开 | `COVER_OPENED` | 舱门已打开 | +| 半开 | `COVER_HALF_OPEN` | 舱门半开 | +| 状态异常 | `COVER_ERROR` | 舱门状态异常 | +| 开舱中 | `COVER_OPENING` | 正在打开舱门 | +| 关舱中 | `COVER_CLOSING` | 正在关闭舱门 | + +### 2.2 无人机状态(DroneState) + +#### 主状态 + +| 状态 | 枚举值 | 说明 | +|------|--------|------| +| 关机 | `POWER_OFF` | 无人机电源关闭(初始状态) | +| 开机 | `POWER_ON` | 无人机电源已开启 | +| 飞行中 | `FLYING` | 无人机正在飞行(父状态) | +| 返航中 | `RETURNING_HOME` | 无人机正在返航 | +| 降落中 | `LANDING` | 无人机正在降落 | +| 已降落 | `LANDED` | 无人机已降落 | + +#### 飞行子状态(FLYING 的子状态) + +| 状态 | 枚举值 | 说明 | +|------|--------|------| +| 待飞 | `READY_TO_FLY` | 准备起飞(初始子状态) | +| 一键起飞模式 | `ONE_KEY_TAKEOFF_MODE` | 一键起飞飞行模式(父状态) | +| 航线飞行模式 | `WAYLINE_FLIGHT_MODE` | 航线飞行模式(父状态) | + +#### 一键起飞子状态(ONE_KEY_TAKEOFF_MODE 的子状态) + +| 状态 | 枚举值 | 说明 | +|------|--------|------| +| 起飞准备 | `TAKEOFF_PREPARING` | 一键起飞准备中(初始子状态) | +| 起飞飞行中 | `TAKEOFF_FLYING` | 正常起飞飞行中 | +| 起飞指点飞行 | `TAKEOFF_POINT_FLYING` | 起飞过程中的指点飞行(子状态) | +| 起飞急停 | `TAKEOFF_EMERGENCY_STOP` | 起飞过程中的急停(子状态) | +| 起飞完成 | `TAKEOFF_COMPLETED` | 一键起飞完成 | +| 起飞失败 | `TAKEOFF_FAILED` | 一键起飞失败 | + +#### 航线飞行子状态(WAYLINE_FLIGHT_MODE 的子状态) + +| 状态 | 枚举值 | 说明 | +|------|--------|------| +| 航线准备 | `WAYLINE_PREPARING` | 航线任务准备中(初始子状态) | +| 航线飞行中 | `WAYLINE_FLYING` | 正常航线飞行中 | +| 航线指点飞行 | `WAYLINE_POINT_FLYING` | 航线过程中的指点飞行(子状态) | +| 航线急停 | `WAYLINE_EMERGENCY_STOP` | 航线过程中的急停(子状态) | +| 航线悬停 | `WAYLINE_HOVER` | 航线飞行中的悬停状态 | +| 航线完成 | `WAYLINE_COMPLETED` | 航线任务完成 | +| 航线失败 | `WAYLINE_FAILED` | 航线任务失败 | + +##### 指点飞行子状态(WAYLINE_POINT_FLYING 的子状态) + +| 状态 | 枚举值 | 说明 | +|------|--------|------| +| 指点飞行中 | `POINT_FLYING` | 正在飞向指定目标点(初始子状态) | +| 到达目的地 | `POINT_ARRIVED` | 已到达指定目标点 | +| 指点急停 | `POINT_EMERGENCY_STOP` | 指点飞行过程中的急停 | + +> 说明: +> - **一键起飞**和**航线飞行**是两种不同的飞行模式,作为FLYING的并列子状态 +> - **指点飞行**在两种模式中都作为子状态存在: +> - 起飞指点飞行(TAKEOFF_POINT_FLYING):在一键起飞过程中插入的指点飞行 +> - 航线指点飞行(WAYLINE_POINT_FLYING):在航线飞行过程中插入的指点飞行 +> - **悬停状态**(WAYLINE_HOVER): +> - 仅存在于航线飞行模式中 +> - 急停或暂停后进入悬停状态 +> - 悬停后可以恢复继续执行航线 +> - **急停**有三个层级: +> - FLYING层级的急停(FLYING_EMERGENCY_STOP):可以在任何飞行状态下触发 +> - 起飞急停(TAKEOFF_EMERGENCY_STOP):一键起飞模式内的急停子状态 +> - 航线急停(WAYLINE_EMERGENCY_STOP):航线飞行模式内的急停子状态,急停后进入WAYLINE_HOVER +> - **航线暂停**(WAYLINE_PAUSED)与急停不同,暂停后也会进入WAYLINE_HOVER悬停状态 + +### 2.3 模式状态(ModeState) + +#### DRC模式状态 + +| 状态 | 枚举值 | 说明 | +|------|--------|------| +| DRC未激活 | `DRC_INACTIVE` | DRC模式未激活(初始状态) | +| DRC激活中 | `DRC_ENTERING` | 正在进入DRC模式 | +| DRC已激活 | `DRC_ACTIVE` | DRC模式已激活 | +| DRC退出中 | `DRC_EXITING` | 正在退出DRC模式 | + +#### 调试模式状态 + +| 状态 | 枚举值 | 说明 | +|------|--------|------| +| 调试模式关闭 | `DEBUG_OFF` | 调试模式关闭(初始状态) | +| 调试模式开启中 | `DEBUG_OPENING` | 正在开启调试模式 | +| 调试模式已开启 | `DEBUG_ON` | 调试模式已开启 | +| 调试模式关闭中 | `DEBUG_CLOSING` | 正在关闭调试模式 | + +#### 控制权状态 + +| 状态 | 枚举值 | 说明 | +|------|--------|------| +| 未获取 | `AUTHORITY_NONE` | 未获取控制权(初始状态) | +| 飞行控制权已获取 | `FLIGHT_AUTHORITY` | 已获取飞行控制权 | +| 负载控制权已获取 | `PAYLOAD_AUTHORITY` | 已获取负载控制权 | +| 全部控制权已获取 | `ALL_AUTHORITY` | 已获取飞行和负载控制权 | + +## 三、事件定义 + +### 3.1 机巢事件(AirportEvent) + +| 事件 | 枚举值 | 说明 | 触发源 | +|------|--------|------|---------| +| 机巢上线 | `AIRPORT_ONLINE` | 机巢设备上线 | OSD心跳 | +| 机巢离线 | `AIRPORT_OFFLINE` | 机巢设备离线 | OSD心跳 | +| 开启调试模式 | `DEBUG_MODE_OPEN` | 开启调试模式 | 用户指令 | +| 关闭调试模式 | `DEBUG_MODE_CLOSE` | 关闭调试模式 | 用户指令/自动 | +| 开舱指令 | `COVER_OPEN` | 开舱指令 | 用户指令 | +| 关舱指令 | `COVER_CLOSE` | 关舱指令 | 用户指令 | +| 开舱完成 | `COVER_OPENED` | 舱门已打开 | OSD状态 | +| 关舱完成 | `COVER_CLOSED` | 舱门已关闭 | OSD状态 | +| 机巢重启 | `AIRPORT_REBOOT` | 机巢重启指令 | 用户指令 | +| 重启完成 | `REBOOT_COMPLETED` | 重启完成 | Events事件 | + +### 3.2 无人机事件(DroneEvent) + +| 事件 | 枚举值 | 说明 | 触发源 | +|------|--------|------|---------| +| 无人机开机 | `DRONE_POWER_ON` | 无人机开机指令 | 用户指令 | +| 无人机关机 | `DRONE_POWER_OFF` | 无人机关机指令 | 用户指令 | +| 开机完成 | `POWER_ON_COMPLETED` | 开机完成 | OSD状态 | +| 关机完成 | `POWER_OFF_COMPLETED` | 关机完成 | OSD状态 | +| 获取飞行控制权 | `GRAB_FLIGHT_AUTHORITY` | 获取飞行控制权 | 自动/用户指令 | +| 获取负载控制权 | `GRAB_PAYLOAD_AUTHORITY` | 获取负载控制权 | 自动/用户指令 | +| 控制权获取完成 | `AUTHORITY_GRABBED` | 控制权获取完成 | Services回复 | + +### 3.3 飞行事件(FlightEvent) + +| 事件 | 枚举值 | 说明 | 触发源 | +|------|--------|------|---------| +| 准备起飞 | `READY_TO_TAKEOFF` | 准备起飞 | 任务准备完成 | +| 开始飞行 | `START_FLIGHT` | 开始飞行 | 用户指令/自动 | +| 进入手动模式 | `ENTER_MANUAL_MODE` | 进入手动飞行模式 | DRC模式激活 | +| 进入自动模式 | `ENTER_AUTO_MODE` | 进入自动飞行模式 | 任务开始 | +| 急停指令 | `EMERGENCY_STOP` | 急停指令 | 用户指令 | +| 取消急停 | `CANCEL_EMERGENCY_STOP` | 取消急停 | 用户指令/自动 | +| 返航指令 | `RETURN_HOME` | 返航指令 | 用户指令/自动 | +| 返航完成 | `RETURN_HOME_COMPLETED` | 返航完成 | OSD状态 | +| 开始降落 | `START_LANDING` | 开始降落 | 自动/用户指令 | +| 降落完成 | `LANDING_COMPLETED` | 降落完成 | OSD状态 | + +### 3.4 任务事件(TaskEvent) + +| 事件 | 枚举值 | 说明 | 触发源 | +|------|--------|------|---------| +| 准备航线任务 | `PREPARE_WAYLINE` | 准备航线任务 | 用户指令 | +| 执行航线任务 | `EXECUTE_WAYLINE` | 执行航线任务 | 用户指令 | +| 准备完成 | `PREPARE_COMPLETED` | 任务准备完成 | Services回复 | +| 任务开始 | `TASK_STARTED` | 任务开始执行 | Events事件 | +| 任务暂停 | `TASK_PAUSE` | 暂停任务 | 用户指令 | +| 任务恢复 | `TASK_RECOVERY` | 恢复任务 | 用户指令 | +| 任务完成 | `TASK_COMPLETED` | 任务完成 | Events事件 | +| 任务失败 | `TASK_FAILED` | 任务失败 | Events事件 | +| 指点飞行 | `FLY_TO_POINT` | 指点飞行指令 | 用户指令 | +| 更新指点目标 | `UPDATE_FLY_TO_POINT` | 更新指点飞行目标 | 用户指令 | +| 一键起飞 | `TAKEOFF_TO_POINT` | 一键起飞指令 | 用户指令 | + +### 3.5 DRC模式事件(DRCEvent) + +| 事件 | 枚举值 | 说明 | 触发源 | +|------|--------|------|---------| +| 进入DRC模式 | `ENTER_DRC_MODE` | 进入DRC模式指令 | 用户指令/自动 | +| 退出DRC模式 | `EXIT_DRC_MODE` | 退出DRC模式指令 | 用户指令/自动 | +| DRC模式已激活 | `DRC_ACTIVATED` | DRC模式已激活 | Services回复 | +| DRC模式已退出 | `DRC_DEACTIVATED` | DRC模式已退出 | Services回复 | + +## 四、状态转换规则 + +### 4.1 机巢状态转换 + +``` +OFFLINE + └─[AIRPORT_ONLINE]─> ONLINE(STANDBY) + └─[DEBUG_MODE_OPEN]─> ONLINE(DEBUG_MODE) + └─[COVER_OPEN]─> ONLINE(OPERATING(COVER_OPENING)) + └─[COVER_OPENED]─> ONLINE(OPERATING(COVER_OPENED)) + └─[DEBUG_MODE_CLOSE]─> ONLINE(STANDBY) + + └─[AIRPORT_REBOOT]─> REBOOTING + └─[REBOOT_COMPLETED]─> ONLINE(STANDBY) + +ONLINE + └─[AIRPORT_OFFLINE]─> OFFLINE +``` + +**Guard 规则:** +- `COVER_OPEN`:检查当前舱门状态不是已打开 +- `COVER_CLOSE`:检查当前舱门状态不是已关闭 +- `DEBUG_MODE_OPEN`:检查当前不是调试模式 +- `AIRPORT_REBOOT`:检查当前是调试模式 + +### 4.2 无人机电源状态转换 + +``` +POWER_OFF (初始状态) + └─[DRONE_POWER_ON]─> POWER_ON + └─[POWER_ON_COMPLETED]─> POWER_ON(READY_TO_FLY) + +POWER_ON + └─[DRONE_POWER_OFF]─> POWER_OFF + └─[POWER_OFF_COMPLETED]─> POWER_OFF +``` + +**Guard 规则:** +- `DRONE_POWER_ON`:检查机巢在线且处于调试模式 +- `DRONE_POWER_OFF`:检查机巢在线且处于调试模式 + +### 4.3 飞行状态转换 + +``` +READY_TO_FLY + └─[START_FLIGHT]─> FLYING + ├─[ENTER_MANUAL_MODE]─> FLYING(MANUAL_FLIGHT) + │ └─[EMERGENCY_STOP]─> FLYING(EMERGENCY_STOP) + │ └─[CANCEL_EMERGENCY_STOP]─> FLYING(MANUAL_FLIGHT) + │ + └─[ENTER_AUTO_MODE]─> FLYING(AUTO_FLIGHT(TASK_PREPARING)) + └─[TASK_STARTED]─> FLYING(AUTO_FLIGHT(TASK_EXECUTING)) + ├─[TASK_PAUSE]─> FLYING(AUTO_FLIGHT(TASK_PAUSED)) + │ └─[TASK_RECOVERY]─> FLYING(AUTO_FLIGHT(TASK_EXECUTING)) + ├─[TASK_COMPLETED]─> FLYING(AUTO_FLIGHT(TASK_COMPLETED)) + └─[TASK_FAILED]─> FLYING(AUTO_FLIGHT(TASK_FAILED)) + +FLYING + └─[RETURN_HOME]─> RETURNING_HOME + └─[RETURN_HOME_COMPLETED]─> RETURNING_HOME + └─[START_LANDING]─> LANDING + └─[LANDING_COMPLETED]─> LANDED + └─[DRONE_POWER_OFF]─> POWER_OFF +``` + +**Guard 规则:** +- `START_FLIGHT`:检查无人机已开机且机巢在线 +- `ENTER_MANUAL_MODE`:检查DRC模式已激活 +- `ENTER_AUTO_MODE`:检查任务准备完成 +- `RETURN_HOME`:检查不在指点飞行中(如果是指点飞行,需要先停止) +- `EMERGENCY_STOP`:检查DRC模式已激活且电量充足 + +### 4.4 DRC模式状态转换 + +``` +DRC_INACTIVE (初始状态) + └─[ENTER_DRC_MODE]─> DRC_ENTERING + └─[DRC_ACTIVATED]─> DRC_ACTIVE + └─[EXIT_DRC_MODE]─> DRC_EXITING + └─[DRC_DEACTIVATED]─> DRC_INACTIVE +``` + +**自动转换规则(Action):** +- 指点飞行中:自动退出DRC模式 +- 一键起飞中:自动退出DRC模式 +- 返航中:自动退出DRC模式 +- 降落中:自动退出DRC模式 +- 指点飞行停止后:自动进入DRC模式 +- 一键起飞停止后:自动进入DRC模式 +- 急停操作:自动进入DRC模式 +- 摇杆控制:自动进入DRC模式 + +### 4.5 调试模式状态转换 + +``` +DEBUG_OFF (初始状态) + └─[DEBUG_MODE_OPEN]─> DEBUG_OPENING + └─[DEBUG_OPENED]─> DEBUG_ON + └─[DEBUG_MODE_CLOSE]─> DEBUG_CLOSING + └─[DEBUG_CLOSED]─> DEBUG_OFF +``` + +**自动转换规则(Action):** +- 开舱/关舱操作:自动开启调试模式 → 执行操作 → 2秒后自动关闭 +- 无人机开机/关机:自动开启调试模式 → 执行操作 → 2秒后自动关闭 +- 机巢重启:自动开启调试模式 → 执行操作 → 2秒后自动关闭 + +### 4.6 控制权状态转换 + +``` +AUTHORITY_NONE (初始状态) + └─[GRAB_FLIGHT_AUTHORITY]─> FLIGHT_AUTHORITY + └─[GRAB_PAYLOAD_AUTHORITY]─> ALL_AUTHORITY +``` + +**自动转换规则(Action):** +- 无人机起飞准备时:自动获取飞行控制权和负载控制权 + +## 五、Guard(守卫)设计 + +### 5.1 机巢操作 Guard + +| Guard名称 | 检查条件 | 失败原因 | +|-----------|----------|----------| +| `CanOpenCover` | 舱门状态不是已打开 | 舱门已经打开 | +| `CanCloseCover` | 舱门状态不是已关闭 | 舱门已经关闭 | +| `IsDebugMode` | 当前处于调试模式 | 未开启调试模式 | +| `IsNotDebugMode` | 当前不处于调试模式 | 处于调试模式中 | +| `IsAirportOnline` | 机巢在线 | 机巢离线 | + +### 5.2 无人机操作 Guard + +| Guard名称 | 检查条件 | 失败原因 | +|-----------|----------|----------| +| `IsDronePowerOn` | 无人机已开机 | 无人机未开机 | +| `IsDronePowerOff` | 无人机已关机 | 无人机未关机 | +| `IsNotFlying` | 无人机不在飞行中 | 无人机正在飞行 | +| `IsNotReturningHome` | 无人机不在返航中 | 无人机正在返航 | +| `IsNotLanding` | 无人机不在降落中 | 无人机正在降落 | +| `CanFlyToPoint` | 不在返航且不在降落 | 返航中或降落中 | +| `HasEnoughBattery` | 电量充足(不低于返航电量) | 电量不足 | +| `IsDRCActive` | DRC模式已激活 | DRC模式未激活 | +| `IsNotDRCActive` | DRC模式未激活 | DRC模式已激活 | + +### 5.3 任务操作 Guard + +| Guard名称 | 检查条件 | 失败原因 | +|-----------|----------|----------| +| `IsValidReturnHeight` | 返航高度≥30米 | 返航高度不足30米 | +| `IsTaskPrepared` | 任务已准备完成 | 任务未准备 | +| `IsTaskExecuting` | 任务执行中 | 任务未执行 | +| `IsTaskPaused` | 任务已暂停 | 任务未暂停 | +| `HasFlightAuthority` | 已获取飞行控制权 | 未获取飞行控制权 | +| `HasPayloadAuthority` | 已获取负载控制权 | 未获取负载控制权 | + +## 六、Action(动作)设计 + +### 6.1 机巢操作 Action + +| Action名称 | 执行内容 | 说明 | +|------------|----------|------| +| `OpenDebugModeAction` | 发送 `debug_mode_open` 指令 | 开启调试模式 | +| `CloseDebugModeAction` | 发送 `debug_mode_close` 指令 | 关闭调试模式 | +| `OpenCoverAction` | 发送 `cover_open` 指令 | 开舱操作 | +| `CloseCoverAction` | 发送 `cover_close` 指令 | 关舱操作 | +| `RebootAirportAction` | 发送 `device_reboot` 指令 | 重启机巢 | +| `AutoCloseDebugModeAction` | 等待2秒后关闭调试模式 | 自动关闭调试模式 | + +### 6.2 无人机操作 Action + +| Action名称 | 执行内容 | 说明 | +|------------|----------|------| +| `PowerOnDroneAction` | 发送 `drone_open` 指令 | 无人机开机 | +| `PowerOffDroneAction` | 发送 `drone_close` 指令 | 无人机关机 | +| `GrabFlightAuthorityAction` | 发送 `flight_authority_grab` 指令 | 获取飞行控制权 | +| `GrabPayloadAuthorityAction` | 发送 `payload_authority_grab` 指令 | 获取负载控制权 | +| `ClearLiveStreamCacheAction` | 清除直播相关缓存 | 清除缓存 | + +### 6.3 飞行操作 Action + +| Action名称 | 执行内容 | 说明 | +|------------|----------|------| +| `EnterDRCModeAction` | 发送 `drc_mode_enter` 指令 | 进入DRC模式 | +| `ExitDRCModeAction` | 发送 `drc_mode_exit` 指令 | 退出DRC模式 | +| `EmergencyStopAction` | 发送 `drone_emergency_stop` 属性设置 | 急停操作 | +| `ReturnHomeAction` | 发送 `return_home` 指令 | 返航操作 | +| `StartLiveStreamAction` | 发送 `live_start_push` 指令 | 开启直播推流 | +| `StopLiveStreamAction` | 发送 `live_stop_push` 指令 | 关闭直播推流 | + +### 6.4 任务操作 Action + +| Action名称 | 执行内容 | 说明 | +|------------|----------|------| +| `PrepareWaylineTaskAction` | 发送 `flighttask_prepare` 指令 | 准备航线任务 | +| `ExecuteWaylineTaskAction` | 等待1秒 → 开启机巢推流 → 发送 `flighttask_execute` 指令 | 执行航线任务 | +| `PauseTaskAction` | 发送 `flighttask_pause` 指令 | 暂停任务 | +| `RecoveryTaskAction` | 发送 `flighttask_recovery` 指令 | 恢复任务 | +| `FlyToPointAction` | 检查是否在指点飞行中 → 发送 `fly_to_point` 或 `fly_to_point_update` 指令 | 指点飞行 | +| `TakeoffToPointAction` | 发送 `takeoff_to_point` 指令 | 一键起飞 | +| `AdjustReturnHeightAction` | 检查返航高度,<100米调整为100米 | 调整返航高度 | + +### 6.5 自动管理 Action + +| Action名称 | 执行内容 | 说明 | +|------------|----------|------| +| `AutoEnterDRCModeAction` | 自动进入DRC模式 | 指点飞行停止后、一键起飞停止后 | +| `AutoExitDRCModeAction` | 自动退出DRC模式 | 指点飞行中、一键起飞中、返航中、降落中 | +| `AutoGrabAuthorityAction` | 自动获取控制权 | 无人机起飞准备时 | +| `UpdateFlightStatusAction` | 更新飞行状态 | 根据OSD数据更新状态(急停状态下不更新) | + +## 七、状态机监听器设计 + +### 7.1 OSD心跳监听器 + +监听 `thing/product/{airportSn}/osd` Topic,处理: + +- **机巢状态更新**:`device_online_status` → 触发 `AIRPORT_ONLINE`/`AIRPORT_OFFLINE` +- **舱门状态更新**:`cover_state` → 触发 `COVER_OPENED`/`COVER_CLOSED` +- **无人机电源状态**:`device_online_status` → 触发 `POWER_ON_COMPLETED`/`POWER_OFF_COMPLETED` +- **飞行模式**:`drone_mode_code` → 更新飞行状态 +- **返航状态**:`mode_code = "9"` → 触发 `RETURN_HOME` +- **降落状态**:`mode_code = "LAND"` → 触发 `START_LANDING` + +### 7.2 Events事件监听器 + +监听 `thing/product/{airportSn}/events` Topic,处理: + +- **任务进度**:`flighttask_progress` → 触发任务状态事件 +- **指点飞行进度**:`fly_to_point_progress` → 触发指点飞行状态事件 +- **一键起飞进度**:`takeoff_to_point_progress` → 触发一键起飞状态事件 +- **设备重启**:`device_reboot` → 触发重启状态事件 +- **文件上传**:`file_upload_callback` → 处理拍照文件上传 + +### 7.3 Services回复监听器 + +监听 `thing/product/{airportSn}/services_reply` Topic,处理: + +- **指令接受确认**:`result=0` → 确认指令已接受 +- **DRC模式状态**:`drc_mode_enter`/`drc_mode_exit` 回复 → 触发DRC模式状态事件 +- **控制权获取**:`flight_authority_grab`/`payload_authority_grab` 回复 → 触发控制权状态事件 + +### 7.4 Set回复监听器 + +监听 `thing/product/{airportSn}/set_reply` Topic,处理: + +- **属性设置确认**:急停、调色盘等属性设置的确认 + +## 八、业务规则实现 + +### 8.1 调试模式规则 + +**规则:** 开舱、关舱、无人机开机、无人机关机、机巢重启需要先开启调试模式 + +**实现:** +``` +Action: OpenDebugModeAction (Guard: IsNotDebugMode) + → 执行操作 (Guard: IsDebugMode) + → AutoCloseDebugModeAction (等待2秒后关闭) +``` + +### 8.2 飞行状态限制规则 + +**规则:** +- 返航中或降落中不可执行指点飞行 +- 一键起飞与航线飞行过程中允许插入指点飞行(实时更新目标) +- 航线/一键起飞的“非悬停”状态下均可急停,急停后进入悬停(Hover) + +**实现:** +``` +Guard: CanFlyToPoint + - 检查状态不是 RETURNING_HOME + - 检查状态不是 LANDING + - 允许在 TAKEOFF_TO_POINT / WAYLINE_TASK 中触发 FLY_TO_POINT 或 UPDATE_FLY_TO_POINT + +Guard: CanEmergencyStop + - 当前飞行状态非悬停即可触发急停 +Action: EmergencyStopAction + - 发送急停指令,进入 EMERGENCY_STOP→HOVER +``` + +### 8.3 航线任务规则 + +**规则:** 返航高度必须≥30米,<100米时自动调整为100米 + +**实现:** +``` +Guard: IsValidReturnHeight (检查≥30米) +Action: AdjustReturnHeightAction (检查<100米时调整为100米) +``` + +### 8.4 DRC模式规则 + +**规则:** 急停、摇杆控制依赖 DRC 模式;指点飞行中、一键起飞中、返航中、降落中自动退出 DRC 模式 + +**实现:** +``` +自动退出DRC模式: + - 指点飞行中:Action监听任务状态,自动发送 EXIT_DRC_MODE + - 一键起飞中:Action监听任务状态,自动发送 EXIT_DRC_MODE + - 返航中:Action监听返航状态,自动发送 EXIT_DRC_MODE + - 降落中:Action监听降落状态,自动发送 EXIT_DRC_MODE + +自动进入DRC模式: + - 指点飞行停止后:Action监听任务完成,自动发送 ENTER_DRC_MODE + - 一键起飞停止后:Action监听任务完成,自动发送 ENTER_DRC_MODE + - 急停操作:Action自动发送 ENTER_DRC_MODE(急停前确保 DRC 已激活) + - 摇杆控制:Action自动发送 ENTER_DRC_MODE +``` + +### 8.5 控制权规则 + +**规则:** 无人机起飞准备时自动获取飞行控制权和负载控制权 + +**实现:** +``` +Action: AutoGrabAuthorityAction + - 监听任务准备完成事件 + - 自动发送 GRAB_FLIGHT_AUTHORITY + - 自动发送 GRAB_PAYLOAD_AUTHORITY +``` + +### 8.6 急停规则 + +**规则:** +- 急停依赖 DRC 模式;飞行中(非悬停)都可急停,急停后进入悬停 +- 航线飞行的“非指点、非悬停”阶段可暂停,暂停后进入悬停(与急停后的悬停一致,但语义不同) +- 急停状态下不更新飞行状态,保持急停/悬停 + +**实现:** +``` +Guard: CanEmergencyStop + - 检查 DRC_ACTIVE + - 检查当前飞行状态非悬停即可触发 +Action: EmergencyStopAction + - 发送 drone_emergency_stop → 状态切换至 EMERGENCY_STOP → HOVER + +Guard: CanPauseWayline + - 当前在 WAYLINE_TASK 且非指点、非悬停 +Action: PauseTaskAction + - 发送 flighttask_pause → 状态切换至 TASK_PAUSED → HOVER + +Guard: UpdateFlightStatusGuard + - 检查当前状态不是 EMERGENCY_STOP + - 如果是急停/悬停,阻止飞行状态更新 +``` + +## 九、状态机配置示例 + +### 9.1 状态定义示例 + +```java +public enum AirportState { + // 主状态 + OFFLINE, + ONLINE, // 父状态 + REBOOTING, + + // ONLINE 的子状态 + STANDBY, // 初始子状态 + DEBUG_MODE, + OPERATING, // 父状态 + + // OPERATING 的子状态(舱门状态) + COVER_CLOSED, + COVER_OPENED, + COVER_HALF_OPEN, + COVER_ERROR, + COVER_OPENING, + COVER_CLOSING +} + +public enum DroneState { + // 主状态 + POWER_OFF, // 初始状态 + POWER_ON, + FLYING, // 父状态 + RETURNING_HOME, + LANDING, + LANDED, + + // FLYING 的子状态 + READY_TO_FLY, // 初始子状态 + MANUAL_FLIGHT, + AUTO_FLIGHT, // 父状态 + EMERGENCY_STOP, + + // AUTO_FLIGHT 的子状态 + TASK_PREPARING, // 初始子状态 + TASK_EXECUTING, // 父状态 + TASK_PAUSED, + TASK_COMPLETED, + TASK_FAILED, + + // TASK_EXECUTING 的子状态 + WAYLINE_TASK, + FLY_TO_POINT, + TAKEOFF_TO_POINT +} +``` + +### 9.2 转换配置示例 + +```java +// 机巢状态转换 +.withExternal() + .source(AirportState.OFFLINE) + .target(AirportState.STANDBY) + .event(AirportEvent.AIRPORT_ONLINE) + .guard(new IsAirportOnlineGuard()) + .and() + +// 开舱操作(需要调试模式) +.withExternal() + .source(AirportState.STANDBY) + .target(AirportState.DEBUG_MODE) + .event(AirportEvent.DEBUG_MODE_OPEN) + .action(new OpenDebugModeAction()) + .guard(new IsNotDebugModeGuard()) + .and() +.withExternal() + .source(AirportState.DEBUG_MODE) + .target(AirportState.COVER_OPENING) + .event(AirportEvent.COVER_OPEN) + .action(new OpenCoverAction()) + .guard(new CanOpenCoverGuard()) + .and() + +// 无人机开机(需要调试模式) +.withExternal() + .source(DroneState.POWER_OFF) + .target(DroneState.POWER_ON) + .event(DroneEvent.DRONE_POWER_ON) + .action(new PowerOnDroneAction()) + .guard(new IsDebugModeGuard()) + .and() + +// 进入DRC模式 +.withExternal() + .source(ModeState.DRC_INACTIVE) + .target(ModeState.DRC_ENTERING) + .event(DRCEvent.ENTER_DRC_MODE) + .action(new EnterDRCModeAction()) + .and() +``` + +## 十、使用场景示例 + +### 10.1 开舱场景 + +``` +1. 用户点击开舱 +2. 检查当前舱门状态(Guard: CanOpenCover) +3. 检查是否在调试模式(Guard: IsNotDebugMode) +4. 开启调试模式(Action: OpenDebugModeAction) +5. 发送开舱指令(Action: OpenCoverAction) +6. 监听OSD状态,等待舱门打开 +7. 2秒后自动关闭调试模式(Action: AutoCloseDebugModeAction) +``` + +### 10.2 航线任务场景 + +``` +1. 用户下发航线任务 +2. 检查返航高度≥30米(Guard: IsValidReturnHeight) +3. 调整返航高度<100米为100米(Action: AdjustReturnHeightAction) +4. 准备任务(Action: PrepareWaylineTaskAction) +5. 等待准备完成(监听services_reply) +6. 等待1秒 → 开启机巢推流 → 执行任务(Action: ExecuteWaylineTaskAction) +7. 监听任务进度(events: flighttask_progress) +8. 任务完成/失败后更新状态 +``` + +### 10.3 指点飞行场景 + +``` +1. 用户点击指点飞行 +2. 检查不在返航中且不在降落中(Guard: CanFlyToPoint) +3. 检查是否已在指点飞行中(判断events中的status) +4. 如果在指点飞行中:发送更新指令(fly_to_point_update) +5. 如果不在指点飞行中:发送新指令(fly_to_point) +6. 自动退出DRC模式(Action: AutoExitDRCModeAction) +7. 监听飞行进度(events: fly_to_point_progress) +8. 飞行停止后自动进入DRC模式(Action: AutoEnterDRCModeAction) +``` + +### 10.4 急停场景 + +``` +1. 用户点击急停 +2. 检查无人机已开机(Guard: IsDronePowerOn) +3. 检查电量充足(Guard: HasEnoughBattery) +4. 检查是否在DRC模式(Guard: IsNotDRCActive) +5. 如果不在DRC模式:先进入DRC模式(Action: EnterDRCModeAction) +6. 等待DRC模式激活(监听services_reply) +7. 发送急停指令(Action: EmergencyStopAction) +8. 进入急停状态,阻止状态更新(Guard: UpdateFlightStatusGuard) +``` + +## 十一、状态机管理 + +### 11.1 多状态机实例管理 + +每个机巢(airportSn)对应一个状态机实例: + +```java +StateMachineManager manager = new StateMachineManager(); + +// 获取或创建状态机 +StateMachine stateMachine = manager.getOrCreateStateMachine(airportSn); + +// 发送事件 +stateMachine.sendEvent(AirportEvent.COVER_OPEN); + +// 查询状态 +AirportState currentState = manager.getCurrentState(airportSn); +``` + +### 11.2 状态持久化 + +状态机状态可以持久化到数据库或Redis: + +- **状态快照**:定期保存状态机状态 +- **事件日志**:记录所有状态转换事件 +- **恢复机制**:系统重启后恢复状态机状态 + +## 十二、总结 + +### 12.1 设计特点 + +1. **分层设计**:主状态、子状态、子子状态,清晰表达复杂状态关系 +2. **业务规则**:通过Guard和Action实现业务限制条件 +3. **自动管理**:DRC模式、控制权等自动管理,减少人工干预 +4. **安全控制**:通过状态机确保操作的安全执行顺序 +5. **事件驱动**:基于MQTT事件驱动状态转换 + +### 12.2 扩展性 + +- **新增功能**:通过添加新状态、事件、转换即可扩展 +- **业务规则变更**:修改Guard和Action即可适应规则变化 +- **多设备支持**:每个机巢独立的状态机实例 + +### 12.3 注意事项 + +1. **状态同步**:确保MQTT事件与状态机状态同步 +2. **异常处理**:处理网络异常、设备异常等情况 +3. **超时处理**:操作超时后的状态恢复机制 +4. **并发控制**:同一状态机的并发操作控制 diff --git a/src/test/java/com/tuoheng/status/statemachine/StateMachineManagerTest.java b/src/test/java/com/tuoheng/status/statemachine/StateMachineManagerTest.java new file mode 100644 index 0000000..e5f37fd --- /dev/null +++ b/src/test/java/com/tuoheng/status/statemachine/StateMachineManagerTest.java @@ -0,0 +1,209 @@ +package com.tuoheng.status.statemachine; + +import com.tuoheng.status.statemachine.events.Event; +import com.tuoheng.status.statemachine.manager.StateMachineManager; +import com.tuoheng.status.statemachine.service.StateMachineManagerService; +import com.tuoheng.status.statemachine.status.Status; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.testng.AbstractTestNGSpringContextTests; +import org.testng.annotations.Test; + +import static org.testng.Assert.*; + +/** + * 状态机管理器测试类 + */ +@ContextConfiguration(classes = { + com.tuoheng.status.statemachine.config.TaskStateMachineConfig.class, + com.tuoheng.status.statemachine.manager.StateMachineManager.class, + StateMachineManagerService.class +}) +public class StateMachineManagerTest extends AbstractTestNGSpringContextTests { + + @Autowired + private StateMachineManager stateMachineManager; + + @Autowired + private StateMachineManagerService stateMachineManagerService; + + @Test + public void testCreateAndGetStateMachine() { + System.out.println("\n\n========================================"); + System.out.println("测试1: 创建和获取状态机"); + System.out.println("========================================\n"); + + String machineId = "test-machine-001"; + + // 创建状态机 + assertFalse(stateMachineManager.exists(machineId), "状态机应该不存在"); + + Status status = stateMachineManager.getCurrentStatus(machineId); + assertNull(status, "不存在的状态机应该返回null"); + + // 创建状态机 + stateMachineManager.getOrCreateStateMachine(machineId); + assertTrue(stateMachineManager.exists(machineId), "状态机应该存在"); + + // 获取状态 + status = stateMachineManager.getCurrentStatus(machineId); + assertNotNull(status, "状态机状态不应该为null"); + assertEquals(status, Status.IDLE, "初始状态应该是IDLE"); + + System.out.println("状态机ID: " + machineId); + System.out.println("当前状态: " + status); + } + + @Test + public void testGetStateMachineInfo() { + System.out.println("\n\n========================================"); + System.out.println("测试2: 获取状态机详细信息"); + System.out.println("========================================\n"); + + String machineId = "test-machine-002"; + stateMachineManager.getOrCreateStateMachine(machineId); + + StateMachineManager.StateMachineInfo info = stateMachineManager.getStateMachineInfo(machineId); + assertNotNull(info, "状态机信息不应该为null"); + assertEquals(info.getMachineId(), machineId); + assertEquals(info.getCurrentStatus(), Status.IDLE); + assertTrue(info.isRunning(), "状态机应该正在运行"); + + System.out.println("状态机信息: " + info); + } + + @Test + public void testSendEventAndCheckStatus() { + System.out.println("\n\n========================================"); + System.out.println("测试3: 发送事件并检查状态变化"); + System.out.println("========================================\n"); + + String machineId = "test-machine-003"; + + // 创建状态机并设置taskId + stateMachineManager.getOrCreateStateMachine(machineId); + stateMachineManager.getStateMachine(machineId) + .getExtendedState().getVariables().put("taskId", "task-001"); + + // 初始状态应该是IDLE + Status status = stateMachineManager.getCurrentStatus(machineId); + assertEquals(status, Status.IDLE); + + // 发送START事件 + boolean accepted = stateMachineManagerService.sendEvent(machineId, Event.START); + assertTrue(accepted, "START事件应该被接受"); + + // 等待状态转换 + try { + Thread.sleep(100); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + // 检查状态应该变为PREPARING + status = stateMachineManager.getCurrentStatus(machineId); + assertEquals(status, Status.PREPARING, "状态应该变为PREPARING"); + + System.out.println("状态机ID: " + machineId); + System.out.println("发送事件: START"); + System.out.println("当前状态: " + status); + } + + @Test + public void testMultipleStateMachines() { + System.out.println("\n\n========================================"); + System.out.println("测试4: 管理多个状态机"); + System.out.println("========================================\n"); + + String machineId1 = "test-machine-004-1"; + String machineId2 = "test-machine-004-2"; + String machineId3 = "test-machine-004-3"; + + // 创建多个状态机 + stateMachineManager.getOrCreateStateMachine(machineId1); + stateMachineManager.getOrCreateStateMachine(machineId2); + stateMachineManager.getOrCreateStateMachine(machineId3); + + // 验证所有状态机都存在 + assertTrue(stateMachineManager.exists(machineId1)); + assertTrue(stateMachineManager.exists(machineId2)); + assertTrue(stateMachineManager.exists(machineId3)); + + // 获取所有状态机ID + java.util.Set allIds = stateMachineManager.getAllMachineIds(); + assertTrue(allIds.contains(machineId1)); + assertTrue(allIds.contains(machineId2)); + assertTrue(allIds.contains(machineId3)); + assertEquals(stateMachineManager.getStateMachineCount(), 3); + + System.out.println("管理的状态机数量: " + stateMachineManager.getStateMachineCount()); + System.out.println("所有状态机ID: " + allIds); + + // 为不同的状态机设置不同的状态 + stateMachineManager.getStateMachine(machineId1) + .getExtendedState().getVariables().put("taskId", "task-001"); + stateMachineManagerService.sendEvent(machineId1, Event.START); + + try { + Thread.sleep(100); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + Status status1 = stateMachineManager.getCurrentStatus(machineId1); + Status status2 = stateMachineManager.getCurrentStatus(machineId2); + Status status3 = stateMachineManager.getCurrentStatus(machineId3); + + System.out.println("状态机1 (" + machineId1 + ") 状态: " + status1); + System.out.println("状态机2 (" + machineId2 + ") 状态: " + status2); + System.out.println("状态机3 (" + machineId3 + ") 状态: " + status3); + + assertNotEquals(status1, status2, "不同状态机应该有不同状态"); + } + + @Test + public void testRemoveStateMachine() { + System.out.println("\n\n========================================"); + System.out.println("测试5: 移除状态机"); + System.out.println("========================================\n"); + + String machineId = "test-machine-005"; + + // 创建状态机 + stateMachineManager.getOrCreateStateMachine(machineId); + assertTrue(stateMachineManager.exists(machineId)); + + // 移除状态机 + boolean removed = stateMachineManager.removeStateMachine(machineId); + assertTrue(removed, "应该成功移除状态机"); + assertFalse(stateMachineManager.exists(machineId), "状态机应该不存在了"); + + System.out.println("状态机已移除: " + machineId); + } + + @Test + public void testStateMachineService() { + System.out.println("\n\n========================================"); + System.out.println("测试6: 使用StateMachineManagerService"); + System.out.println("========================================\n"); + + String machineId = "test-machine-006"; + String taskId = "task-006"; + + // 使用服务类启动任务 + boolean started = stateMachineManagerService.startTask(machineId, taskId); + assertTrue(started, "任务应该成功启动"); + + // 检查状态 + Status status = stateMachineManagerService.getCurrentStatus(machineId); + assertEquals(status, Status.PREPARING, "状态应该是PREPARING"); + + // 获取详细信息 + StateMachineManager.StateMachineInfo info = stateMachineManagerService.getStateMachineInfo(machineId); + assertNotNull(info); + assertEquals(info.getCurrentStatus(), Status.PREPARING); + + // 打印信息 + stateMachineManagerService.printStateMachineInfo(machineId); + } +} diff --git a/src/test/java/com/tuoheng/status/statemachine/TaskStateMachineTest.java b/src/test/java/com/tuoheng/status/statemachine/TaskStateMachineTest.java new file mode 100644 index 0000000..caa0676 --- /dev/null +++ b/src/test/java/com/tuoheng/status/statemachine/TaskStateMachineTest.java @@ -0,0 +1,65 @@ +package com.tuoheng.status.statemachine; + +import com.tuoheng.status.statemachine.service.TaskStateMachineService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.testng.AbstractTestNGSpringContextTests; +import org.testng.annotations.Test; + +/** + * 状态机测试类 + * 演示 spring-statemachine-core 的各种功能 + */ +@ContextConfiguration(classes = { + com.tuoheng.status.statemachine.config.TaskStateMachineConfig.class, + TaskStateMachineService.class +}) +public class TaskStateMachineTest extends AbstractTestNGSpringContextTests { + + @Autowired + private TaskStateMachineService taskStateMachineService; + + /** + * 测试正常流程:IDLE -> PREPARING -> EXECUTING -> VALIDATING -> COMPLETED + */ + @Test + public void testNormalFlow() { + System.out.println("\n\n========================================"); + System.out.println("测试1: 正常流程"); + System.out.println("========================================\n"); + taskStateMachineService.startTask("task-001"); + } + + /** + * 测试失败和重试流程 + */ + @Test + public void testFailureAndRetry() { + System.out.println("\n\n========================================"); + System.out.println("测试2: 失败和重试流程"); + System.out.println("========================================\n"); + taskStateMachineService.demonstrateFailureAndRetry("task-002"); + } + + /** + * 测试 Guard 条件不满足的情况 + */ + @Test + public void testGuardFailure() { + System.out.println("\n\n========================================"); + System.out.println("测试3: Guard 条件不满足"); + System.out.println("========================================\n"); + taskStateMachineService.demonstrateGuardFailure("task-003"); + } + + /** + * 测试重置流程 + */ + @Test + public void testReset() { + System.out.println("\n\n========================================"); + System.out.println("测试4: 重置流程"); + System.out.println("========================================\n"); + taskStateMachineService.demonstrateReset("task-004"); + } +}