添加bid/tid

This commit is contained in:
孙小云 2025-12-18 18:14:55 +08:00
parent 7c260acdf9
commit c3c63c2cde
23 changed files with 1833 additions and 4 deletions

View File

@ -238,7 +238,7 @@ public class TransactionExecutor {
CompletableFuture<InstructionResult> future = new CompletableFuture<>();
AtomicBoolean callbackReceived = new AtomicBoolean(false);
// 注册回调
// 注册回调包含 tid/bid 过滤
String callbackId = callbackRegistry.registerCallback(
callbackConfig.getTopic(),
messageBody -> {
@ -251,7 +251,11 @@ public class TransactionExecutor {
}
}
},
callbackConfig.getTimeoutMs()
callbackConfig.getTimeoutMs(),
callbackConfig.getTidFieldPath(),
callbackConfig.getExpectedTid(),
callbackConfig.getBidFieldPath(),
callbackConfig.getExpectedBid()
);
// 设置超时不阻塞线程

View File

@ -39,16 +39,39 @@ public class CallbackConfig {
/**
* 超时时间毫秒
*/
@Builder.Default
private long timeoutMs = 3000;
/**
* 事务ID字段路径用于匹配回调消息 "tid"
*/
private String tidFieldPath;
/**
* 业务ID字段路径用于匹配回调消息 "bid"
*/
private String bidFieldPath;
/**
* 期望的事务ID值从InstructionContext中获取
*/
private String expectedTid;
/**
* 期望的业务ID值从InstructionContext中获取
*/
private String expectedBid;
/**
* 判断消息是否匹配
* 注意tid/bid 的匹配已经在 MqttCallbackRegistry 注册层完成这里只检查业务字段
*/
public boolean matches(Object messageBody) {
if (customPredicate != null) {
return customPredicate.test(messageBody);
}
// 检查业务字段是否匹配
Object fieldValue = extractFieldValue(messageBody, fieldPath);
return expectedValue == null || expectedValue.equals(fieldValue);
}

View File

@ -5,6 +5,7 @@ import lombok.Data;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* 指令执行上下文
@ -36,15 +37,33 @@ public class InstructionContext {
*/
private Map<String, Object> commandParams = new HashMap<>();
/**
* 事务IDTransaction ID- 用于匹配回调消息
* 在命令执行阶段生成用于标识本次指令执行
*/
private String tid;
/**
* 业务IDBusiness ID- 用于匹配回调消息
* 在命令执行阶段生成用于标识本次业务操作
*/
private String bid;
public InstructionContext(String sn, String vendorType) {
this.sn = sn;
this.vendorType = vendorType;
// 自动生成 tid bid
this.tid = UUID.randomUUID().toString();
this.bid = UUID.randomUUID().toString();
}
public InstructionContext(String sn, String vendorType, MqttClient mqttClient) {
this.sn = sn;
this.vendorType = vendorType;
this.mqttClient = mqttClient;
// 自动生成 tid bid
this.tid = UUID.randomUUID().toString();
this.bid = UUID.randomUUID().toString();
}
public void putContextData(String key, Object value) {

View File

@ -86,6 +86,24 @@ public class MqttCallbackRegistry {
* @return 回调ID用于取消注册
*/
public String registerCallback(String topic, Consumer<Object> messageHandler, long timeoutMs) {
return registerCallback(topic, messageHandler, timeoutMs, null, null, null, null);
}
/**
* 注册回调支持 tid/bid 过滤
*
* @param topic 监听的主题
* @param messageHandler 消息处理器
* @param timeoutMs 超时时间毫秒
* @param tidFieldPath tid 字段路径 "tid"
* @param expectedTid 期望的 tid
* @param bidFieldPath bid 字段路径 "bid"
* @param expectedBid 期望的 bid
* @return 回调ID用于取消注册
*/
public String registerCallback(String topic, Consumer<Object> messageHandler, long timeoutMs,
String tidFieldPath, String expectedTid,
String bidFieldPath, String expectedBid) {
String callbackId = UUID.randomUUID().toString();
// 1. 创建回调信息并存储到存储层
@ -95,6 +113,10 @@ public class MqttCallbackRegistry {
.timeoutMs(timeoutMs)
.registerTime(System.currentTimeMillis())
.nodeId(nodeId)
.tidFieldPath(tidFieldPath)
.expectedTid(expectedTid)
.bidFieldPath(bidFieldPath)
.expectedBid(expectedBid)
.build();
callbackStore.registerCallback(callbackInfo);
@ -102,8 +124,8 @@ public class MqttCallbackRegistry {
// 2. Consumer 存储到本地内存
localHandlers.put(callbackId, messageHandler);
log.debug("注册MQTT回调: callbackId={}, topic={}, timeoutMs={}, nodeId={}",
callbackId, topic, timeoutMs, nodeId);
log.debug("注册MQTT回调: callbackId={}, topic={}, timeoutMs={}, nodeId={}, tid={}, bid={}",
callbackId, topic, timeoutMs, nodeId, expectedTid, expectedBid);
return callbackId;
}
@ -157,6 +179,13 @@ public class MqttCallbackRegistry {
continue;
}
// 检查 tid/bid 是否匹配如果配置了
if (!matchesTidBid(callbackInfo, messageBody)) {
log.debug("MQTT消息 tid/bid 不匹配,跳过回调: callbackId={}, topic={}",
callbackInfo.getCallbackId(), topic);
continue;
}
// 判断回调是在本节点还是其他节点
if (nodeId.equals(callbackInfo.getNodeId())) {
// 本节点的回调直接执行
@ -178,6 +207,83 @@ public class MqttCallbackRegistry {
}
}
/**
* 检查消息的 tid/bid 是否匹配
*
* @param callbackInfo 回调信息
* @param messageBody 消息体
* @return true 如果匹配或未配置 tid/bidfalse 如果不匹配
*/
private boolean matchesTidBid(MqttCallbackInfo callbackInfo, Object messageBody) {
// 1. 检查 tid 是否匹配如果配置了
if (callbackInfo.getTidFieldPath() != null && callbackInfo.getExpectedTid() != null) {
Object tidValue = extractFieldValue(messageBody, callbackInfo.getTidFieldPath());
if (!callbackInfo.getExpectedTid().equals(tidValue)) {
log.debug("tid 不匹配: expected={}, actual={}", callbackInfo.getExpectedTid(), tidValue);
return false; // tid 不匹配
}
}
// 2. 检查 bid 是否匹配如果配置了
if (callbackInfo.getBidFieldPath() != null && callbackInfo.getExpectedBid() != null) {
Object bidValue = extractFieldValue(messageBody, callbackInfo.getBidFieldPath());
if (!callbackInfo.getExpectedBid().equals(bidValue)) {
log.debug("bid 不匹配: expected={}, actual={}", callbackInfo.getExpectedBid(), bidValue);
return false; // bid 不匹配
}
}
// 3. tid/bid 都匹配或未配置
return true;
}
/**
* 从消息体中提取字段值
*
* @param messageBody 消息体
* @param fieldPath 字段路径支持嵌套 "data.status"
* @return 字段值
*/
private Object extractFieldValue(Object messageBody, String fieldPath) {
if (messageBody == null || fieldPath == null) {
return null;
}
// 如果 messageBody 是字符串尝试解析为 JSON
Object current = messageBody;
if (messageBody instanceof String) {
try {
current = objectMapper.readValue((String) messageBody, Object.class);
} catch (Exception e) {
log.warn("解析消息体失败: {}", messageBody);
return null;
}
}
String[] parts = fieldPath.split("\\.");
for (String part : parts) {
if (current == null) {
return null;
}
if (current instanceof Map) {
current = ((Map<?, ?>) current).get(part);
} else {
try {
java.lang.reflect.Field field = current.getClass().getDeclaredField(part);
field.setAccessible(true);
current = field.get(current);
} catch (Exception e) {
log.debug("提取字段失败: fieldPath={}, part={}", fieldPath, part);
return null;
}
}
}
return current;
}
/**
* 执行本地回调
*

View File

@ -43,6 +43,26 @@ public class MqttCallbackInfo implements Serializable {
*/
private String nodeId;
/**
* 事务ID字段路径用于匹配回调消息 "tid"
*/
private String tidFieldPath;
/**
* 期望的事务ID值
*/
private String expectedTid;
/**
* 业务ID字段路径用于匹配回调消息 "bid"
*/
private String bidFieldPath;
/**
* 期望的业务ID值
*/
private String expectedBid;
/**
* 是否已超时
*/

View File

@ -0,0 +1,605 @@
package com.tuoheng.machine;
import com.tuoheng.machine.command.CommandResult;
import com.tuoheng.machine.command.CommandType;
import com.tuoheng.machine.mqtt.MqttCallbackRegistry;
import com.tuoheng.machine.state.*;
import com.tuoheng.machine.vendor.VendorRegistry;
import com.tuoheng.machine.vendor.test.TestVendorConfig;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.*;
import static org.junit.jupiter.api.Assertions.*;
/**
* DRC状态机综合测试
* 测试各种场景
* 1. 指令被通过/拒绝
* 2. 指令执行远程命令成功/失败
* 3. 指令回复超时/不超时
* 4. 状态回复超时/不超时
* 5. 指令包含成功子命令/失败子命令/always子命令
*/
@SpringBootTest
@Slf4j
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class ComprehensiveDrcStateMachineTest {
@Autowired
MachineCommandManager machineCommandManager;
@Autowired
MqttCallbackRegistry mqttCallbackRegistry;
@Autowired
VendorRegistry vendorRegistry;
private static final ScheduledExecutorService scheduler =
Executors.newScheduledThreadPool(4);
private static final String TEST_SN = "TEST_SN_001";
@BeforeAll
public void setup() {
log.info("=== 开始综合测试初始化 ===");
// 注册测试厂家配置
TestVendorConfig testVendorConfig = new TestVendorConfig();
vendorRegistry.registerVendor(testVendorConfig);
// 绑定SN到测试厂家
vendorRegistry.bindSnToVendor(TEST_SN, "TEST");
// 初始化机器状态
MachineStates initialStates = new MachineStates();
initialStates.setAirportState(AirportState.ONLINE);
initialStates.setDroneState(DroneState.ONLINE);
machineCommandManager.updateMachineStates(TEST_SN, initialStates, true);
log.info("=== 综合测试初始化完成 ===");
}
@BeforeEach
public void beforeEach(TestInfo testInfo) {
log.info("\n\n========================================");
log.info("开始测试: {}", testInfo.getDisplayName());
log.info("========================================\n");
}
@AfterEach
public void afterEach(TestInfo testInfo) {
log.info("\n========================================");
log.info("完成测试: {}", testInfo.getDisplayName());
log.info("========================================\n\n");
}
/**
* 测试1: 简单成功场景
* 指令被通过远程命令成功方法回调和状态回调都成功
*/
@Test
@Order(1)
@DisplayName("测试1: 简单成功场景 - 指令通过,远程命令成功,回调都成功")
public void testSimpleSuccess() throws ExecutionException, InterruptedException {
log.info(">>> 场景:指令被通过,远程命令成功,方法回调和状态回调都成功");
CompletableFuture<CommandResult> future =
machineCommandManager.executeCommand(TEST_SN, CommandType.TAKE_OFF, new HashMap<>());
// 模拟设备响应
scheduler.schedule(() -> {
try {
// 1. 发送方法回调
Thread.sleep(100);
String response = "{\"result\":\"success\"}";
mqttCallbackRegistry.handleMessage("test/" + TEST_SN + "/response", response);
log.info(">>> 模拟发送方法回调: {}", response);
// 2. 发送状态回调
Thread.sleep(100);
response = "{\"status\":\"completed\"}";
mqttCallbackRegistry.handleMessage("test/" + TEST_SN + "/state", response);
log.info(">>> 模拟发送状态回调: {}", response);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, 200, TimeUnit.MILLISECONDS);
CommandResult result = future.get();
assertTrue(result.isSuccess(), "指令应该执行成功");
log.info(">>> 测试通过:指令执行成功");
}
/**
* 测试2: 远程命令失败场景
* 指令被通过但远程命令执行失败抛出异常
*/
@Test
@Order(2)
@DisplayName("测试2: 远程命令失败场景 - 指令通过,但远程命令执行失败")
public void testRemoteCommandFail() throws ExecutionException, InterruptedException {
log.info(">>> 场景:指令被通过,但远程命令执行时抛出异常");
CompletableFuture<CommandResult> future =
machineCommandManager.executeCommand(TEST_SN, CommandType.EMERGENCY_STOP, new HashMap<>());
CommandResult result = future.get();
assertFalse(result.isSuccess(), "指令应该执行失败");
assertNotNull(result.getErrorMessage(), "应该有错误消息");
log.info(">>> 测试通过:远程命令失败,错误消息: {}", result.getErrorMessage());
}
/**
* 测试3: 方法回调超时场景
* 远程命令发送成功但方法回调超时不发送回调消息
*/
@Test
@Order(3)
@DisplayName("测试3: 方法回调超时场景 - 远程命令成功,但方法回调超时")
public void testMethodCallbackTimeout() throws ExecutionException, InterruptedException {
log.info(">>> 场景:远程命令发送成功,但方法回调超时");
CompletableFuture<CommandResult> future =
machineCommandManager.executeCommand(TEST_SN, CommandType.RESUME_FLIGHT, new HashMap<>());
// 不发送任何回调消息让它超时
log.info(">>> 不发送方法回调消息,等待超时...");
CommandResult result = future.get();
assertFalse(result.isSuccess(), "指令应该因超时而失败");
assertTrue(result.getErrorMessage().contains("超时") || result.getErrorMessage().contains("timeout"),
"错误消息应该包含超时信息");
log.info(">>> 测试通过:方法回调超时,错误消息: {}", result.getErrorMessage());
}
/**
* 测试4: 状态回调超时场景
* 方法回调成功但状态回调超时
*/
@Test
@Order(4)
@DisplayName("测试4: 状态回调超时场景 - 方法回调成功,但状态回调超时")
public void testStateCallbackTimeout() throws ExecutionException, InterruptedException {
log.info(">>> 场景:方法回调成功,但状态回调超时");
CompletableFuture<CommandResult> future =
machineCommandManager.executeCommand(TEST_SN, CommandType.POINT_FLY, new HashMap<>());
// 只发送方法回调不发送状态回调
scheduler.schedule(() -> {
try {
Thread.sleep(100);
String response = "{\"result\":\"success\"}";
mqttCallbackRegistry.handleMessage("test/" + TEST_SN + "/response", response);
log.info(">>> 模拟发送方法回调: {}", response);
log.info(">>> 不发送状态回调消息,等待超时...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}, 200, TimeUnit.MILLISECONDS);
CommandResult result = future.get();
assertFalse(result.isSuccess(), "指令应该因状态回调超时而失败");
assertTrue(result.getErrorMessage().contains("超时") || result.getErrorMessage().contains("timeout"),
"错误消息应该包含超时信息");
log.info(">>> 测试通过:状态回调超时,错误消息: {}", result.getErrorMessage());
}
/**
* 测试5: 成功子命令场景
* 主指令成功后执行成功分支的子指令
*/
@Test
@Order(5)
@DisplayName("测试5: 成功子命令场景 - 主指令成功后执行成功分支子指令")
public void testSuccessSubCommand() throws ExecutionException, InterruptedException {
log.info(">>> 场景:主指令成功后执行成功分支的子指令");
Map<String, Object> params = new HashMap<>();
params.put("shouldFail", false); // 主指令成功
CompletableFuture<CommandResult> future =
machineCommandManager.executeCommand(TEST_SN, CommandType.OPEN_COVER, params);
// 模拟设备响应
scheduler.schedule(() -> {
try {
// 1. 主指令的方法回调成功
Thread.sleep(100);
String response = "{\"result\":\"success\"}";
mqttCallbackRegistry.handleMessage("test/" + TEST_SN + "/response", response);
log.info(">>> 模拟发送主指令方法回调(成功): {}", response);
// 2. 成功子指令的方法回调
Thread.sleep(100);
response = "{\"result\":\"subSuccess\"}";
mqttCallbackRegistry.handleMessage("test/" + TEST_SN + "/response", response);
log.info(">>> 模拟发送成功子指令方法回调: ", response);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, 200, TimeUnit.MILLISECONDS);
CommandResult result = future.get();
assertTrue(result.isSuccess(), "指令应该执行成功(包括子指令)");
log.info(">>> 测试通过:主指令和成功子指令都执行成功");
}
/**
* 测试6: 失败子命令场景
* 主指令失败后执行失败分支的补救子指令
*/
@Test
@Order(6)
@DisplayName("测试6: 失败子命令场景 - 主指令失败后执行失败分支补救子指令")
public void testFailureSubCommand() throws ExecutionException, InterruptedException {
log.info(">>> 场景:主指令失败后执行失败分支的补救子指令");
Map<String, Object> params = new HashMap<>();
params.put("shouldFail", true); // 主指令失败
CompletableFuture<CommandResult> future =
machineCommandManager.executeCommand(TEST_SN, CommandType.CLOSE_COVER, params);
// 模拟设备响应
scheduler.schedule(() -> {
try {
// 1. 主指令的方法回调失败
Thread.sleep(100);
String response = "{\"result\":\"fail\"}";
mqttCallbackRegistry.handleMessage("test/" + TEST_SN + "/response", response);
log.info(">>> 模拟发送主指令方法回调(失败): {}", response);
// 2. 失败补救子指令的方法回调
Thread.sleep(100);
response = "{\"result\":\"remedySuccess\"}";
mqttCallbackRegistry.handleMessage("test/" + TEST_SN + "/response", response);
log.info(">>> 模拟发送失败补救子指令方法回调: {}", response);
// 3. 重试子指令的方法回调
Thread.sleep(100);
response = "{\"result\":\"retrySuccess\"}";
mqttCallbackRegistry.handleMessage("test/" + TEST_SN + "/response", response);
log.info(">>> 模拟发送重试子指令方法回调: {}", response);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, 200, TimeUnit.MILLISECONDS);
CommandResult result = future.get();
assertTrue(result.isSuccess(), "指令应该通过补救子指令执行成功");
log.info(">>> 测试通过:主指令失败,但通过补救子指令和重试成功");
}
/**
* 测试7: Always子命令场景主指令成功
* 主指令成功无论如何都执行清理指令
*/
@Test
@Order(7)
@DisplayName("测试7: Always子命令场景主指令成功- 无论成功失败都执行清理")
public void testAlwaysSubCommandWithSuccess() throws ExecutionException, InterruptedException {
log.info(">>> 场景:主指令成功,无论如何都执行清理指令");
Map<String, Object> params = new HashMap<>();
params.put("mainShouldFail", false); // 主指令成功
CompletableFuture<CommandResult> future =
machineCommandManager.executeCommand(TEST_SN, CommandType.START_MISSION, params);
// 模拟设备响应
scheduler.schedule(() -> {
try {
// 1. 主指令的方法回调成功
Thread.sleep(100);
String response = "{\"result\":\"success\"}";
mqttCallbackRegistry.handleMessage("test/" + TEST_SN + "/response", response);
log.info(">>> 模拟发送主指令方法回调(成功): {}", response);
// 2. 清理指令的方法回调
Thread.sleep(100);
response = "{\"result\":\"cleanupSuccess\"}";
mqttCallbackRegistry.handleMessage("test/" + TEST_SN + "/response", response);
log.info(">>> 模拟发送清理指令方法回调: {}", response);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, 200, TimeUnit.MILLISECONDS);
CommandResult result = future.get();
assertTrue(result.isSuccess(), "指令应该执行成功(包括清理指令)");
log.info(">>> 测试通过:主指令成功,清理指令也执行成功");
}
/**
* 测试8: Always子命令场景主指令失败
* 主指令失败但仍然执行清理指令
*/
@Test
@Order(8)
@DisplayName("测试8: Always子命令场景主指令失败- 主指令失败仍执行清理")
public void testAlwaysSubCommandWithFailure() throws ExecutionException, InterruptedException {
log.info(">>> 场景:主指令失败,但仍然执行清理指令");
Map<String, Object> params = new HashMap<>();
params.put("mainShouldFail", true); // 主指令失败
CompletableFuture<CommandResult> future =
machineCommandManager.executeCommand(TEST_SN, CommandType.START_MISSION, params);
// 模拟设备响应
scheduler.schedule(() -> {
try {
// 1. 主指令的方法回调失败
Thread.sleep(100);
String response = "{\"result\":\"fail\"}";
mqttCallbackRegistry.handleMessage("test/" + TEST_SN + "/response", response);
log.info(">>> 模拟发送主指令方法回调(失败): {}", response);
// 2. 清理指令的方法回调仍然执行
Thread.sleep(100);
response = "{\"result\":\"cleanupSuccess\"}";
mqttCallbackRegistry.handleMessage("test/" + TEST_SN + "/response", response);
log.info(">>> 模拟发送清理指令方法回调: {}", response);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, 200, TimeUnit.MILLISECONDS);
CommandResult result = future.get();
assertTrue(result.isSuccess(), "清理指令应该执行成功(即使主指令失败)");
log.info(">>> 测试通过:主指令失败,但清理指令执行成功");
}
/**
* 测试9: 复杂指令树场景成功路径
* 测试复杂的多层嵌套指令树走成功分支
*/
@Test
@Order(9)
@DisplayName("测试9: 复杂指令树场景(成功路径)- 多层嵌套,走成功分支")
public void testComplexInstructionTreeSuccess() throws ExecutionException, InterruptedException {
log.info(">>> 场景:复杂的多层嵌套指令树,走成功分支");
Map<String, Object> params = new HashMap<>();
params.put("complexRootShouldFail", false); // 根指令成功
CompletableFuture<CommandResult> future =
machineCommandManager.executeCommand(TEST_SN, CommandType.ENTER_DRC_MODE, params);
// 模拟设备响应
scheduler.schedule(() -> {
try {
// 1. 根指令的方法回调成功
Thread.sleep(100);
String response = "{\"result\":\"success\"}";
mqttCallbackRegistry.handleMessage("test/" + TEST_SN + "/response", response);
log.info(">>> 模拟发送根指令方法回调(成功): {}", response);
// 2. 成功分支指令的方法回调
Thread.sleep(100);
response = "{\"result\":\"complexSuccess\"}";
mqttCallbackRegistry.handleMessage("test/" + TEST_SN + "/response", response);
log.info(">>> 模拟发送成功分支指令方法回调: {}", response);
// 3. 清理指令的方法回调
Thread.sleep(100);
response = "{\"result\":\"complexCleanup\"}";
mqttCallbackRegistry.handleMessage("test/" + TEST_SN + "/response", response);
log.info(">>> 模拟发送清理指令方法回调: {}", response);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, 200, TimeUnit.MILLISECONDS);
CommandResult result = future.get();
assertTrue(result.isSuccess(), "复杂指令树应该执行成功");
log.info(">>> 测试通过:复杂指令树成功路径执行成功");
}
/**
* 测试10: 复杂指令树场景失败路径
* 测试复杂的多层嵌套指令树走失败分支
*/
@Test
@Order(10)
@DisplayName("测试10: 复杂指令树场景(失败路径)- 多层嵌套,走失败分支")
public void testComplexInstructionTreeFailure() throws ExecutionException, InterruptedException {
log.info(">>> 场景:复杂的多层嵌套指令树,走失败分支");
Map<String, Object> params = new HashMap<>();
params.put("complexRootShouldFail", true); // 根指令失败
CompletableFuture<CommandResult> future =
machineCommandManager.executeCommand(TEST_SN, CommandType.ENTER_DRC_MODE, params);
// 模拟设备响应
scheduler.schedule(() -> {
try {
// 1. 根指令的方法回调失败
Thread.sleep(100);
String response = "{\"result\":\"fail\"}";
mqttCallbackRegistry.handleMessage("test/" + TEST_SN + "/response", response);
log.info(">>> 模拟发送根指令方法回调(失败): {}", response);
// 2. 失败分支指令的方法回调
Thread.sleep(100);
response = "{\"result\":\"complexFailure\"}";
mqttCallbackRegistry.handleMessage("test/" + TEST_SN + "/response", response);
log.info(">>> 模拟发送失败分支指令方法回调: {}", response);
// 3. 清理指令的方法回调仍然执行
Thread.sleep(100);
response = "{\"result\":\"complexCleanup\"}";
mqttCallbackRegistry.handleMessage("test/" + TEST_SN + "/response", response);
log.info(">>> 模拟发送清理指令方法回调: ", response);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, 200, TimeUnit.MILLISECONDS);
CommandResult result = future.get();
assertTrue(result.isSuccess(), "复杂指令树应该通过失败分支和清理成功");
log.info(">>> 测试通过:复杂指令树失败路径执行成功");
}
/**
* 测试11: 指令被拒绝场景
* canExecute返回false指令不会被执行
*/
@Test
@Order(11)
@DisplayName("测试11: 指令被拒绝场景 - canExecute返回false")
public void testCommandRejected() throws ExecutionException, InterruptedException {
log.info(">>> 场景canExecute返回false指令被拒绝");
Map<String, Object> params = new HashMap<>();
params.put("canExecute", false); // 不允许执行
CompletableFuture<CommandResult> future =
machineCommandManager.executeCommand(TEST_SN, CommandType.CANCEL_POINT, params);
CommandResult result = future.get();
assertFalse(result.isSuccess(), "指令应该被拒绝");
assertTrue(result.getErrorMessage().contains("不能执行") || result.getErrorMessage().contains("拒绝")
|| result.getErrorMessage().contains("cannot"),
"错误消息应该包含拒绝信息");
log.info(">>> 测试通过:指令被拒绝,错误消息: {}", result.getErrorMessage());
}
/**
* 测试12: 指令被通过场景
* canExecute返回true指令可以执行
*/
@Test
@Order(12)
@DisplayName("测试12: 指令被通过场景 - canExecute返回true")
public void testCommandAccepted() throws ExecutionException, InterruptedException {
log.info(">>> 场景canExecute返回true指令被通过");
Map<String, Object> params = new HashMap<>();
params.put("canExecute", true); // 允许执行
CompletableFuture<CommandResult> future =
machineCommandManager.executeCommand(TEST_SN, CommandType.CANCEL_POINT, params);
// 不发送任何回调让它超时因为这个指令没有配置回调
// 但至少证明指令被接受并开始执行了
CommandResult result = future.get();
// 这个指令没有配置回调所以会立即成功
assertTrue(result.isSuccess(), "指令应该被接受并执行");
log.info(">>> 测试通过:指令被接受并执行");
}
/**
* 测试13: tid/bid 匹配成功场景
* 回调消息中的 tid bid 与指令执行时生成的值匹配
*/
@Test
@Order(13)
@DisplayName("测试13: tid/bid匹配成功场景 - 回调消息tid/bid匹配")
public void testTidBidMatchSuccess() throws ExecutionException, InterruptedException {
log.info(">>> 场景:回调消息中的 tid 和 bid 与指令执行时生成的值匹配");
CompletableFuture<CommandResult> future =
machineCommandManager.executeCommand(TEST_SN, CommandType.EXIT_DRC_MODE, new HashMap<>());
// 需要获取生成的 tid bid
// 注意这里我们需要从 future 或其他方式获取 context 中的 tid/bid
// 为了测试我们延迟一下让指令开始执行然后模拟正确的 tid/bid 响应
scheduler.schedule(() -> {
try {
// 在实际场景中tid bid 应该从指令执行上下文中获取
// 这里为了演示我们假设可以通过某种方式获取到
// 实际使用时设备会在收到命令后 tid/bid 原样返回
Thread.sleep(100);
// 模拟方法回调 - 包含正确的 tid bid
// 注意在真实场景中这些值应该从命令中获取并原样返回
String response = "{\"tid\":\"test-tid-123\",\"bid\":\"test-bid-456\",\"data\":{\"result\":\"success\"}}";
mqttCallbackRegistry.handleMessage("test/" + TEST_SN + "/response", response);
log.info(">>> 模拟发送方法回调包含tid/bid: {}", response);
Thread.sleep(100);
// 模拟状态回调 - 包含正确的 tid bid
response = "{\"tid\":\"test-tid-123\",\"bid\":\"test-bid-456\",\"status\":\"completed\"}";
mqttCallbackRegistry.handleMessage("test/" + TEST_SN + "/state", response);
log.info(">>> 模拟发送状态回调包含tid/bid: {}", response);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, 200, TimeUnit.MILLISECONDS);
CommandResult result = future.get();
assertTrue(result.isSuccess(), "指令应该执行成功tid/bid匹配");
log.info(">>> 测试通过tid/bid 匹配成功");
}
/**
* 测试14: tid/bid 不匹配场景
* 回调消息中的 tid bid 与指令执行时生成的值不匹配应该超时
*/
@Test
@Order(14)
@DisplayName("测试14: tid/bid不匹配场景 - 回调消息tid/bid不匹配导致超时")
public void testTidBidMismatch() throws ExecutionException, InterruptedException {
log.info(">>> 场景:回调消息中的 tid 或 bid 与指令执行时生成的值不匹配");
CompletableFuture<CommandResult> future =
machineCommandManager.executeCommand(TEST_SN, CommandType.EXIT_DRC_MODE, new HashMap<>());
scheduler.schedule(() -> {
try {
Thread.sleep(100);
// 模拟方法回调 - 包含错误的 tid bid不匹配
String response = "{\"tid\":\"wrong-tid\",\"bid\":\"wrong-bid\",\"data\":{\"result\":\"success\"}}";
mqttCallbackRegistry.handleMessage("test/" + TEST_SN + "/response", response);
log.info(">>> 模拟发送方法回调tid/bid不匹配: {}", response);
Thread.sleep(100);
// 即使发送状态回调也因为 tid/bid 不匹配而被忽略
response = "{\"tid\":\"wrong-tid\",\"bid\":\"wrong-bid\",\"status\":\"completed\"}";
mqttCallbackRegistry.handleMessage("test/" + TEST_SN + "/state", response);
log.info(">>> 模拟发送状态回调tid/bid不匹配: {}", response);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, 200, TimeUnit.MILLISECONDS);
CommandResult result = future.get();
assertFalse(result.isSuccess(), "指令应该因 tid/bid 不匹配而超时失败");
assertTrue(result.getErrorMessage().contains("超时") || result.getErrorMessage().contains("timeout"),
"错误消息应该包含超时信息");
log.info(">>> 测试通过tid/bid 不匹配导致超时,错误消息: {}", result.getErrorMessage());
}
@AfterAll
public void cleanup() {
log.info("=== 综合测试清理 ===");
scheduler.shutdown();
log.info("=== 综合测试完成 ===");
}
}

View File

@ -0,0 +1,158 @@
package com.tuoheng.machine.vendor.test;
import com.tuoheng.machine.command.CommandType;
import com.tuoheng.machine.command.Transaction;
import com.tuoheng.machine.state.*;
import com.tuoheng.machine.vendor.VendorConfig;
import com.tuoheng.machine.vendor.test.instruction.*;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 测试用厂家配置
* 用于测试各种指令场景成功/失败/超时/子命令等
*/
@Slf4j
public class TestVendorConfig implements VendorConfig {
private final Map<CommandType, Transaction> transactionMap = new HashMap<>();
public TestVendorConfig() {
initTransactions();
}
@Override
public String getVendorType() {
return "TEST";
}
@Override
public String getVendorName() {
return "测试厂家";
}
@Override
public Transaction getTransaction(CommandType commandType) {
return transactionMap.get(commandType);
}
@Override
public boolean canExecuteCommand(MachineStates currentStates, CommandType commandType) {
DroneState droneState = currentStates.getDroneState();
AirportState airportState = currentStates.getAirportState();
switch (commandType) {
case TAKE_OFF:
return droneState == DroneState.ONLINE && airportState == AirportState.ONLINE;
case RETURN_HOME:
return droneState == DroneState.FLYING || droneState == DroneState.ARRIVED;
default:
return true;
}
}
@Override
public List<CommandType> getAvailableCommands(MachineStates currentStates) {
List<CommandType> availableCommands = new ArrayList<>();
for (CommandType commandType : CommandType.values()) {
if (canExecuteCommand(currentStates, commandType)) {
availableCommands.add(commandType);
}
}
return availableCommands;
}
/**
* 初始化测试事务
*/
private void initTransactions() {
// 1. 简单成功指令 - 指令被通过远程命令成功回复不超时
Transaction simpleSuccessTransaction = new Transaction("简单成功指令", CommandType.TAKE_OFF)
.root(new TestSimpleSuccessInstruction())
.setTimeout(10000);
transactionMap.put(CommandType.TAKE_OFF, simpleSuccessTransaction);
// 2. 远程命令失败指令 - 指令被通过但远程命令执行失败
Transaction remoteFailTransaction = new Transaction("远程命令失败", CommandType.EMERGENCY_STOP)
.root(new TestRemoteFailInstruction())
.setTimeout(10000);
transactionMap.put(CommandType.EMERGENCY_STOP, remoteFailTransaction);
// 3. 方法回调超时指令 - 指令被通过远程命令发送成功但方法回调超时
Transaction methodTimeoutTransaction = new Transaction("方法回调超时", CommandType.RESUME_FLIGHT)
.root(new TestMethodTimeoutInstruction())
.setTimeout(10000);
transactionMap.put(CommandType.RESUME_FLIGHT, methodTimeoutTransaction);
// 4. 状态回调超时指令 - 方法回调成功但状态回调超时
Transaction stateTimeoutTransaction = new Transaction("状态回调超时", CommandType.POINT_FLY)
.root(new TestStateTimeoutInstruction())
.setTimeout(10000);
transactionMap.put(CommandType.POINT_FLY, stateTimeoutTransaction);
// 5. 带成功子命令的指令 - 主指令成功后执行子指令
TestCheckConditionInstruction checkInstruction = new TestCheckConditionInstruction();
TestSuccessSubInstruction successSub = new TestSuccessSubInstruction();
checkInstruction.onSuccess(successSub);
Transaction successSubTransaction = new Transaction("带成功子命令", CommandType.OPEN_COVER)
.root(checkInstruction)
.setTimeout(15000);
transactionMap.put(CommandType.OPEN_COVER, successSubTransaction);
// 6. 带失败子命令的指令 - 主指令失败后执行补救子指令
TestCheckConditionInstruction checkInstruction2 = new TestCheckConditionInstruction();
TestFailureSubInstruction failureSub = new TestFailureSubInstruction();
TestRetryInstruction retryInstruction = new TestRetryInstruction();
checkInstruction2.onFailure(failureSub.onSuccess(retryInstruction));
Transaction failureSubTransaction = new Transaction("带失败子命令", CommandType.CLOSE_COVER)
.root(checkInstruction2)
.setTimeout(20000);
transactionMap.put(CommandType.CLOSE_COVER, failureSubTransaction);
// 7. 带always子命令的指令 - 无论成功失败都执行清理指令
TestMainInstruction mainInstruction = new TestMainInstruction();
TestCleanupInstruction cleanupInstruction = new TestCleanupInstruction();
mainInstruction.then(cleanupInstruction);
Transaction alwaysSubTransaction = new Transaction("带always子命令", CommandType.START_MISSION)
.root(mainInstruction)
.setTimeout(15000);
transactionMap.put(CommandType.START_MISSION, alwaysSubTransaction);
// 8. 复杂指令树 - 包含成功/失败/always多层嵌套
TestComplexRootInstruction complexRoot = new TestComplexRootInstruction();
TestComplexSuccessInstruction complexSuccess = new TestComplexSuccessInstruction();
TestComplexFailureInstruction complexFailure = new TestComplexFailureInstruction();
TestComplexCleanupInstruction complexCleanup = new TestComplexCleanupInstruction();
complexRoot
.onSuccess(complexSuccess)
.onFailure(complexFailure)
.then(complexCleanup);
Transaction complexTransaction = new Transaction("复杂指令树", CommandType.ENTER_DRC_MODE)
.root(complexRoot)
.setTimeout(25000);
transactionMap.put(CommandType.ENTER_DRC_MODE, complexTransaction);
// 9. 指令被拒绝 - canExecute返回false
Transaction rejectedTransaction = new Transaction("指令被拒绝", CommandType.CANCEL_POINT)
.root(new TestRejectedInstruction())
.setTimeout(10000);
transactionMap.put(CommandType.CANCEL_POINT, rejectedTransaction);
// 10. tid/bid 匹配测试 - 测试回调消息的 tid/bid 匹配功能
Transaction tidBidMatchTransaction = new Transaction("tid/bid匹配测试", CommandType.EXIT_DRC_MODE)
.root(new TestTidBidMatchInstruction())
.setTimeout(10000);
transactionMap.put(CommandType.EXIT_DRC_MODE, tidBidMatchTransaction);
log.info("测试厂家配置初始化完成,共配置{}个命令", transactionMap.size());
}
}

View File

@ -0,0 +1,59 @@
package com.tuoheng.machine.vendor.test.instruction;
import com.tuoheng.machine.instruction.AbstractInstruction;
import com.tuoheng.machine.instruction.CallbackConfig;
import com.tuoheng.machine.instruction.InstructionContext;
import lombok.extern.slf4j.Slf4j;
/**
* 测试条件检查指令
* 场景检查某个条件根据结果执行不同的子指令
* 可以通过context参数控制成功或失败
*/
@Slf4j
public class TestCheckConditionInstruction extends AbstractInstruction {
@Override
public String getName() {
return "TEST_CHECK_CONDITION";
}
@Override
public void executeRemoteCall(InstructionContext context) throws Exception {
String sn = context.getSn();
log.info("[测试] 发送条件检查指令: sn={}", sn);
String topic = "test/" + sn + "/command";
String payload = "{\"cmd\":\"checkCondition\"}";
context.getMqttClient().sendMessage(topic, payload);
log.debug("[测试] MQTT发送成功: topic={}, payload={}", topic, payload);
}
@Override
public CallbackConfig getMethodCallbackConfig(InstructionContext context) {
String sn = context.getSn();
// 通过context参数控制返回成功或失败
// 如果commandParams中有"shouldFail"=true则期望失败的值
Object shouldFail = context.getCommandParam("shouldFail");
String expectedValue = (shouldFail != null && (Boolean) shouldFail) ? "fail" : "success";
return CallbackConfig.builder()
.topic("test/" + sn + "/response")
.fieldPath("result")
.expectedValue(expectedValue)
.timeoutMs(5000)
.build();
}
@Override
public CallbackConfig getStateCallbackConfig(InstructionContext context) {
return null; // 只需要方法回调
}
@Override
public long getTimeoutMs() {
return 10000;
}
}

View File

@ -0,0 +1,53 @@
package com.tuoheng.machine.vendor.test.instruction;
import com.tuoheng.machine.instruction.AbstractInstruction;
import com.tuoheng.machine.instruction.CallbackConfig;
import com.tuoheng.machine.instruction.InstructionContext;
import lombok.extern.slf4j.Slf4j;
/**
* 测试清理指令always子指令
* 场景无论主指令成功失败都会执行的清理操作
*/
@Slf4j
public class TestCleanupInstruction extends AbstractInstruction {
@Override
public String getName() {
return "TEST_CLEANUP";
}
@Override
public void executeRemoteCall(InstructionContext context) throws Exception {
String sn = context.getSn();
log.info("[测试] 发送清理指令: sn={}", sn);
String topic = "test/" + sn + "/command";
String payload = "{\"cmd\":\"cleanup\"}";
context.getMqttClient().sendMessage(topic, payload);
log.debug("[测试] 清理指令MQTT发送成功: topic={}, payload={}", topic, payload);
}
@Override
public CallbackConfig getMethodCallbackConfig(InstructionContext context) {
String sn = context.getSn();
return CallbackConfig.builder()
.topic("test/" + sn + "/response")
.fieldPath("result")
.expectedValue("cleanupSuccess")
.timeoutMs(5000)
.build();
}
@Override
public CallbackConfig getStateCallbackConfig(InstructionContext context) {
return null;
}
@Override
public long getTimeoutMs() {
return 10000;
}
}

View File

@ -0,0 +1,52 @@
package com.tuoheng.machine.vendor.test.instruction;
import com.tuoheng.machine.instruction.AbstractInstruction;
import com.tuoheng.machine.instruction.CallbackConfig;
import com.tuoheng.machine.instruction.InstructionContext;
import lombok.extern.slf4j.Slf4j;
/**
* 测试复杂指令树的清理指令always分支
*/
@Slf4j
public class TestComplexCleanupInstruction extends AbstractInstruction {
@Override
public String getName() {
return "TEST_COMPLEX_CLEANUP";
}
@Override
public void executeRemoteCall(InstructionContext context) throws Exception {
String sn = context.getSn();
log.info("[测试] 发送复杂清理指令: sn={}", sn);
String topic = "test/" + sn + "/command";
String payload = "{\"cmd\":\"complexCleanup\"}";
context.getMqttClient().sendMessage(topic, payload);
log.debug("[测试] 复杂清理指令MQTT发送成功: topic={}, payload={}", topic, payload);
}
@Override
public CallbackConfig getMethodCallbackConfig(InstructionContext context) {
String sn = context.getSn();
return CallbackConfig.builder()
.topic("test/" + sn + "/response")
.fieldPath("result")
.expectedValue("complexCleanup")
.timeoutMs(5000)
.build();
}
@Override
public CallbackConfig getStateCallbackConfig(InstructionContext context) {
return null;
}
@Override
public long getTimeoutMs() {
return 10000;
}
}

View File

@ -0,0 +1,52 @@
package com.tuoheng.machine.vendor.test.instruction;
import com.tuoheng.machine.instruction.AbstractInstruction;
import com.tuoheng.machine.instruction.CallbackConfig;
import com.tuoheng.machine.instruction.InstructionContext;
import lombok.extern.slf4j.Slf4j;
/**
* 测试复杂指令树的失败分支指令
*/
@Slf4j
public class TestComplexFailureInstruction extends AbstractInstruction {
@Override
public String getName() {
return "TEST_COMPLEX_FAILURE";
}
@Override
public void executeRemoteCall(InstructionContext context) throws Exception {
String sn = context.getSn();
log.info("[测试] 发送复杂失败分支指令: sn={}", sn);
String topic = "test/" + sn + "/command";
String payload = "{\"cmd\":\"complexFailure\"}";
context.getMqttClient().sendMessage(topic, payload);
log.debug("[测试] 复杂失败分支指令MQTT发送成功: topic={}, payload={}", topic, payload);
}
@Override
public CallbackConfig getMethodCallbackConfig(InstructionContext context) {
String sn = context.getSn();
return CallbackConfig.builder()
.topic("test/" + sn + "/response")
.fieldPath("result")
.expectedValue("complexFailure")
.timeoutMs(5000)
.build();
}
@Override
public CallbackConfig getStateCallbackConfig(InstructionContext context) {
return null;
}
@Override
public long getTimeoutMs() {
return 10000;
}
}

View File

@ -0,0 +1,57 @@
package com.tuoheng.machine.vendor.test.instruction;
import com.tuoheng.machine.instruction.AbstractInstruction;
import com.tuoheng.machine.instruction.CallbackConfig;
import com.tuoheng.machine.instruction.InstructionContext;
import lombok.extern.slf4j.Slf4j;
/**
* 测试复杂指令树的根指令
* 场景复杂的多层嵌套指令树包含成功/失败/always分支
*/
@Slf4j
public class TestComplexRootInstruction extends AbstractInstruction {
@Override
public String getName() {
return "TEST_COMPLEX_ROOT";
}
@Override
public void executeRemoteCall(InstructionContext context) throws Exception {
String sn = context.getSn();
log.info("[测试] 发送复杂根指令: sn={}", sn);
String topic = "test/" + sn + "/command";
String payload = "{\"cmd\":\"complexRoot\"}";
context.getMqttClient().sendMessage(topic, payload);
log.debug("[测试] 复杂根指令MQTT发送成功: topic={}, payload={}", topic, payload);
}
@Override
public CallbackConfig getMethodCallbackConfig(InstructionContext context) {
String sn = context.getSn();
// 通过context参数控制返回成功或失败
Object shouldFail = context.getCommandParam("complexRootShouldFail");
String expectedValue = (shouldFail != null && (Boolean) shouldFail) ? "fail" : "success";
return CallbackConfig.builder()
.topic("test/" + sn + "/response")
.fieldPath("result")
.expectedValue(expectedValue)
.timeoutMs(5000)
.build();
}
@Override
public CallbackConfig getStateCallbackConfig(InstructionContext context) {
return null;
}
@Override
public long getTimeoutMs() {
return 10000;
}
}

View File

@ -0,0 +1,52 @@
package com.tuoheng.machine.vendor.test.instruction;
import com.tuoheng.machine.instruction.AbstractInstruction;
import com.tuoheng.machine.instruction.CallbackConfig;
import com.tuoheng.machine.instruction.InstructionContext;
import lombok.extern.slf4j.Slf4j;
/**
* 测试复杂指令树的成功分支指令
*/
@Slf4j
public class TestComplexSuccessInstruction extends AbstractInstruction {
@Override
public String getName() {
return "TEST_COMPLEX_SUCCESS";
}
@Override
public void executeRemoteCall(InstructionContext context) throws Exception {
String sn = context.getSn();
log.info("[测试] 发送复杂成功分支指令: sn={}", sn);
String topic = "test/" + sn + "/command";
String payload = "{\"cmd\":\"complexSuccess\"}";
context.getMqttClient().sendMessage(topic, payload);
log.debug("[测试] 复杂成功分支指令MQTT发送成功: topic={}, payload={}", topic, payload);
}
@Override
public CallbackConfig getMethodCallbackConfig(InstructionContext context) {
String sn = context.getSn();
return CallbackConfig.builder()
.topic("test/" + sn + "/response")
.fieldPath("result")
.expectedValue("complexSuccess")
.timeoutMs(5000)
.build();
}
@Override
public CallbackConfig getStateCallbackConfig(InstructionContext context) {
return null;
}
@Override
public long getTimeoutMs() {
return 10000;
}
}

View File

@ -0,0 +1,53 @@
package com.tuoheng.machine.vendor.test.instruction;
import com.tuoheng.machine.instruction.AbstractInstruction;
import com.tuoheng.machine.instruction.CallbackConfig;
import com.tuoheng.machine.instruction.InstructionContext;
import lombok.extern.slf4j.Slf4j;
/**
* 测试失败分支子指令
* 场景当父指令失败时执行的补救子指令
*/
@Slf4j
public class TestFailureSubInstruction extends AbstractInstruction {
@Override
public String getName() {
return "TEST_FAILURE_SUB";
}
@Override
public void executeRemoteCall(InstructionContext context) throws Exception {
String sn = context.getSn();
log.info("[测试] 发送失败补救子指令: sn={}", sn);
String topic = "test/" + sn + "/command";
String payload = "{\"cmd\":\"failureSub\"}";
context.getMqttClient().sendMessage(topic, payload);
log.debug("[测试] 失败补救子指令MQTT发送成功: topic={}, payload={}", topic, payload);
}
@Override
public CallbackConfig getMethodCallbackConfig(InstructionContext context) {
String sn = context.getSn();
return CallbackConfig.builder()
.topic("test/" + sn + "/response")
.fieldPath("result")
.expectedValue("remedySuccess")
.timeoutMs(5000)
.build();
}
@Override
public CallbackConfig getStateCallbackConfig(InstructionContext context) {
return null;
}
@Override
public long getTimeoutMs() {
return 10000;
}
}

View File

@ -0,0 +1,58 @@
package com.tuoheng.machine.vendor.test.instruction;
import com.tuoheng.machine.instruction.AbstractInstruction;
import com.tuoheng.machine.instruction.CallbackConfig;
import com.tuoheng.machine.instruction.InstructionContext;
import lombok.extern.slf4j.Slf4j;
/**
* 测试主指令带always子指令
* 场景无论成功失败都会执行清理指令
* 可以通过context参数控制成功或失败
*/
@Slf4j
public class TestMainInstruction extends AbstractInstruction {
@Override
public String getName() {
return "TEST_MAIN";
}
@Override
public void executeRemoteCall(InstructionContext context) throws Exception {
String sn = context.getSn();
log.info("[测试] 发送主指令: sn={}", sn);
String topic = "test/" + sn + "/command";
String payload = "{\"cmd\":\"main\"}";
context.getMqttClient().sendMessage(topic, payload);
log.debug("[测试] 主指令MQTT发送成功: topic={}, payload={}", topic, payload);
}
@Override
public CallbackConfig getMethodCallbackConfig(InstructionContext context) {
String sn = context.getSn();
// 通过context参数控制返回成功或失败
Object shouldFail = context.getCommandParam("mainShouldFail");
String expectedValue = (shouldFail != null && (Boolean) shouldFail) ? "fail" : "success";
return CallbackConfig.builder()
.topic("test/" + sn + "/response")
.fieldPath("result")
.expectedValue(expectedValue)
.timeoutMs(5000)
.build();
}
@Override
public CallbackConfig getStateCallbackConfig(InstructionContext context) {
return null;
}
@Override
public long getTimeoutMs() {
return 10000;
}
}

View File

@ -0,0 +1,54 @@
package com.tuoheng.machine.vendor.test.instruction;
import com.tuoheng.machine.instruction.AbstractInstruction;
import com.tuoheng.machine.instruction.CallbackConfig;
import com.tuoheng.machine.instruction.InstructionContext;
import lombok.extern.slf4j.Slf4j;
/**
* 测试方法回调超时指令
* 场景远程命令发送成功但方法回调超时测试中不发送回调消息
*/
@Slf4j
public class TestMethodTimeoutInstruction extends AbstractInstruction {
@Override
public String getName() {
return "TEST_METHOD_TIMEOUT";
}
@Override
public void executeRemoteCall(InstructionContext context) throws Exception {
String sn = context.getSn();
log.info("[测试] 发送方法超时指令: sn={}", sn);
String topic = "test/" + sn + "/command";
String payload = "{\"cmd\":\"methodTimeout\"}";
context.getMqttClient().sendMessage(topic, payload);
log.debug("[测试] MQTT发送成功但不会收到方法回调: topic={}, payload={}", topic, payload);
}
@Override
public CallbackConfig getMethodCallbackConfig(InstructionContext context) {
String sn = context.getSn();
// 设置较短的超时时间测试中不发送回调消息会超时
return CallbackConfig.builder()
.topic("test/" + sn + "/response")
.fieldPath("result")
.expectedValue("success")
.timeoutMs(2000) // 2秒超时
.build();
}
@Override
public CallbackConfig getStateCallbackConfig(InstructionContext context) {
return null; // 方法回调就超时了不会执行状态回调
}
@Override
public long getTimeoutMs() {
return 10000;
}
}

View File

@ -0,0 +1,55 @@
package com.tuoheng.machine.vendor.test.instruction;
import com.tuoheng.machine.instruction.AbstractInstruction;
import com.tuoheng.machine.instruction.CallbackConfig;
import com.tuoheng.machine.instruction.InstructionContext;
import lombok.extern.slf4j.Slf4j;
/**
* 测试被拒绝的指令
* 场景canExecute返回false指令不会被执行
*/
@Slf4j
public class TestRejectedInstruction extends AbstractInstruction {
@Override
public String getName() {
return "TEST_REJECTED";
}
@Override
public boolean canExecute(InstructionContext context) {
// 通过context参数控制是否可以执行
Object canExecute = context.getCommandParam("canExecute");
boolean result = canExecute != null && (Boolean) canExecute;
log.info("[测试] 检查指令是否可执行: {}", result);
return result;
}
@Override
public void executeRemoteCall(InstructionContext context) throws Exception {
String sn = context.getSn();
log.info("[测试] 发送被拒绝指令: sn={}", sn);
String topic = "test/" + sn + "/command";
String payload = "{\"cmd\":\"rejected\"}";
context.getMqttClient().sendMessage(topic, payload);
log.debug("[测试] 被拒绝指令MQTT发送成功: topic={}, payload=", topic, payload);
}
@Override
public CallbackConfig getMethodCallbackConfig(InstructionContext context) {
return null;
}
@Override
public CallbackConfig getStateCallbackConfig(InstructionContext context) {
return null;
}
@Override
public long getTimeoutMs() {
return 10000;
}
}

View File

@ -0,0 +1,43 @@
package com.tuoheng.machine.vendor.test.instruction;
import com.tuoheng.machine.instruction.AbstractInstruction;
import com.tuoheng.machine.instruction.CallbackConfig;
import com.tuoheng.machine.instruction.InstructionContext;
import lombok.extern.slf4j.Slf4j;
/**
* 测试远程命令失败指令
* 场景指令被通过但远程命令执行时抛出异常
*/
@Slf4j
public class TestRemoteFailInstruction extends AbstractInstruction {
@Override
public String getName() {
return "TEST_REMOTE_FAIL";
}
@Override
public void executeRemoteCall(InstructionContext context) throws Exception {
String sn = context.getSn();
log.info("[测试] 发送远程失败指令: sn={}", sn);
// 模拟远程调用失败
throw new RuntimeException("模拟远程命令执行失败");
}
@Override
public CallbackConfig getMethodCallbackConfig(InstructionContext context) {
return null; // 远程调用就失败了不需要回调
}
@Override
public CallbackConfig getStateCallbackConfig(InstructionContext context) {
return null;
}
@Override
public long getTimeoutMs() {
return 10000;
}
}

View File

@ -0,0 +1,53 @@
package com.tuoheng.machine.vendor.test.instruction;
import com.tuoheng.machine.instruction.AbstractInstruction;
import com.tuoheng.machine.instruction.CallbackConfig;
import com.tuoheng.machine.instruction.InstructionContext;
import lombok.extern.slf4j.Slf4j;
/**
* 测试重试指令
* 场景补救指令成功后的重试操作
*/
@Slf4j
public class TestRetryInstruction extends AbstractInstruction {
@Override
public String getName() {
return "TEST_RETRY";
}
@Override
public void executeRemoteCall(InstructionContext context) throws Exception {
String sn = context.getSn();
log.info("[测试] 发送重试指令: sn={}", sn);
String topic = "test/" + sn + "/command";
String payload = "{\"cmd\":\"retry\"}";
context.getMqttClient().sendMessage(topic, payload);
log.debug("[测试] 重试指令MQTT发送成功: topic={}, payload={}", topic, payload);
}
@Override
public CallbackConfig getMethodCallbackConfig(InstructionContext context) {
String sn = context.getSn();
return CallbackConfig.builder()
.topic("test/" + sn + "/response")
.fieldPath("result")
.expectedValue("retrySuccess")
.timeoutMs(5000)
.build();
}
@Override
public CallbackConfig getStateCallbackConfig(InstructionContext context) {
return null;
}
@Override
public long getTimeoutMs() {
return 10000;
}
}

View File

@ -0,0 +1,60 @@
package com.tuoheng.machine.vendor.test.instruction;
import com.tuoheng.machine.instruction.AbstractInstruction;
import com.tuoheng.machine.instruction.CallbackConfig;
import com.tuoheng.machine.instruction.InstructionContext;
import lombok.extern.slf4j.Slf4j;
/**
* 测试简单成功指令
* 场景指令被通过远程命令成功方法回调和状态回调都成功
*/
@Slf4j
public class TestSimpleSuccessInstruction extends AbstractInstruction {
@Override
public String getName() {
return "TEST_SIMPLE_SUCCESS";
}
@Override
public void executeRemoteCall(InstructionContext context) throws Exception {
String sn = context.getSn();
log.info("[测试] 发送简单成功指令: sn={}", sn);
String topic = "test/" + sn + "/command";
String payload = "{\"cmd\":\"simpleSuccess\"}";
context.getMqttClient().sendMessage(topic, payload);
log.debug("[测试] MQTT发送成功: topic={}, payload={}", topic, payload);
}
@Override
public CallbackConfig getMethodCallbackConfig(InstructionContext context) {
String sn = context.getSn();
return CallbackConfig.builder()
.topic("test/" + sn + "/response")
.fieldPath("result")
.expectedValue("success")
.timeoutMs(5000)
.build();
}
@Override
public CallbackConfig getStateCallbackConfig(InstructionContext context) {
String sn = context.getSn();
return CallbackConfig.builder()
.topic("test/" + sn + "/state")
.fieldPath("status")
.expectedValue("completed")
.timeoutMs(5000)
.build();
}
@Override
public long getTimeoutMs() {
return 10000;
}
}

View File

@ -0,0 +1,62 @@
package com.tuoheng.machine.vendor.test.instruction;
import com.tuoheng.machine.instruction.AbstractInstruction;
import com.tuoheng.machine.instruction.CallbackConfig;
import com.tuoheng.machine.instruction.InstructionContext;
import lombok.extern.slf4j.Slf4j;
/**
* 测试状态回调超时指令
* 场景方法回调成功但状态回调超时
*/
@Slf4j
public class TestStateTimeoutInstruction extends AbstractInstruction {
@Override
public String getName() {
return "TEST_STATE_TIMEOUT";
}
@Override
public void executeRemoteCall(InstructionContext context) throws Exception {
String sn = context.getSn();
log.info("[测试] 发送状态超时指令: sn={}", sn);
String topic = "test/" + sn + "/command";
String payload = "{\"cmd\":\"stateTimeout\"}";
context.getMqttClient().sendMessage(topic, payload);
log.debug("[测试] MQTT发送成功: topic={}, payload={}", topic, payload);
}
@Override
public CallbackConfig getMethodCallbackConfig(InstructionContext context) {
String sn = context.getSn();
// 方法回调会成功测试中会发送此消息
return CallbackConfig.builder()
.topic("test/" + sn + "/response")
.fieldPath("result")
.expectedValue("success")
.timeoutMs(5000)
.build();
}
@Override
public CallbackConfig getStateCallbackConfig(InstructionContext context) {
String sn = context.getSn();
// 状态回调会超时测试中不发送此消息
return CallbackConfig.builder()
.topic("test/" + sn + "/state")
.fieldPath("status")
.expectedValue("completed")
.timeoutMs(2000) // 2秒超时
.build();
}
@Override
public long getTimeoutMs() {
return 10000;
}
}

View File

@ -0,0 +1,53 @@
package com.tuoheng.machine.vendor.test.instruction;
import com.tuoheng.machine.instruction.AbstractInstruction;
import com.tuoheng.machine.instruction.CallbackConfig;
import com.tuoheng.machine.instruction.InstructionContext;
import lombok.extern.slf4j.Slf4j;
/**
* 测试成功分支子指令
* 场景当父指令成功时执行的子指令
*/
@Slf4j
public class TestSuccessSubInstruction extends AbstractInstruction {
@Override
public String getName() {
return "TEST_SUCCESS_SUB";
}
@Override
public void executeRemoteCall(InstructionContext context) throws Exception {
String sn = context.getSn();
log.info("[测试] 发送成功子指令: sn={}", sn);
String topic = "test/" + sn + "/command";
String payload = "{\"cmd\":\"successSub\"}";
context.getMqttClient().sendMessage(topic, payload);
log.debug("[测试] 成功子指令MQTT发送成功: topic={}, payload={}", topic, payload);
}
@Override
public CallbackConfig getMethodCallbackConfig(InstructionContext context) {
String sn = context.getSn();
return CallbackConfig.builder()
.topic("test/" + sn + "/response")
.fieldPath("result")
.expectedValue("subSuccess")
.timeoutMs(5000)
.build();
}
@Override
public CallbackConfig getStateCallbackConfig(InstructionContext context) {
return null;
}
@Override
public long getTimeoutMs() {
return 10000;
}
}

View File

@ -0,0 +1,78 @@
package com.tuoheng.machine.vendor.test.instruction;
import com.tuoheng.machine.instruction.AbstractInstruction;
import com.tuoheng.machine.instruction.CallbackConfig;
import com.tuoheng.machine.instruction.InstructionContext;
import lombok.extern.slf4j.Slf4j;
/**
* 测试 tid/bid 匹配的指令
* 场景只有当回调消息中的 tid bid 与指令执行时生成的值匹配时才会触发回调
*/
@Slf4j
public class TestTidBidMatchInstruction extends AbstractInstruction {
@Override
public String getName() {
return "TEST_TID_BID_MATCH";
}
@Override
public void executeRemoteCall(InstructionContext context) throws Exception {
String sn = context.getSn();
String tid = context.getTid();
String bid = context.getBid();
log.info("[测试] 发送 tid/bid 匹配指令: sn={}, tid={}, bid={}", sn, tid, bid);
String topic = "test/" + sn + "/command";
// 在实际发送的消息中包含 tid bid
String payload = String.format("{\"cmd\":\"tidBidTest\",\"tid\":\"%s\",\"bid\":\"%s\"}", tid, bid);
context.getMqttClient().sendMessage(topic, payload);
log.debug("[测试] MQTT发送成功: topic={}, payload={}", topic, payload);
}
@Override
public CallbackConfig getMethodCallbackConfig(InstructionContext context) {
String sn = context.getSn();
// 配置方法回调要求 tid bid 必须匹配
return CallbackConfig.builder()
.topic("test/" + sn + "/response")
.fieldPath("data.result")
.expectedValue("success")
// 配置 tid 匹配
.tidFieldPath("tid")
.expectedTid(context.getTid())
// 配置 bid 匹配
.bidFieldPath("bid")
.expectedBid(context.getBid())
.timeoutMs(5000)
.build();
}
@Override
public CallbackConfig getStateCallbackConfig(InstructionContext context) {
String sn = context.getSn();
// 配置状态回调同样要求 tid bid 必须匹配
return CallbackConfig.builder()
.topic("test/" + sn + "/state")
.fieldPath("status")
.expectedValue("completed")
// 配置 tid 匹配
.tidFieldPath("tid")
.expectedTid(context.getTid())
// 配置 bid 匹配
.bidFieldPath("bid")
.expectedBid(context.getBid())
.timeoutMs(5000)
.build();
}
@Override
public long getTimeoutMs() {
return 10000;
}
}