Merge remote-tracking branch 'origin/main'
This commit is contained in:
commit
b2683cd7fe
13
pom.xml
13
pom.xml
|
|
@ -105,6 +105,19 @@
|
|||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Integration MQTT -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.integration</groupId>
|
||||
<artifactId>spring-integration-mqtt</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Eclipse Paho MQTT v5 Client (支持MQTT 5.0) -->
|
||||
<dependency>
|
||||
<groupId>org.eclipse.paho</groupId>
|
||||
<artifactId>org.eclipse.paho.mqttv5.client</artifactId>
|
||||
<version>1.2.5</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,202 @@
|
|||
# DJI MQTT 模块使用说明(支持多客户端)
|
||||
|
||||
## 概述
|
||||
|
||||
本模块实现了大疆MQTT消息的接收和处理功能,支持动态创建多个MQTT客户端,每个客户端可以连接到不同的服务器。
|
||||
|
||||
## 核心特性
|
||||
|
||||
✅ **多客户端支持** - 可以同时创建多个MQTT客户端
|
||||
✅ **动态配置** - 每个客户端可以独立配置IP、端口、用户名等
|
||||
✅ **独立消息处理** - 每个客户端有独立的消息处理器
|
||||
✅ **共享订阅可选** - 支持开启/关闭共享订阅
|
||||
✅ **完整数据模型** - 包含所有无人机和机场字段
|
||||
✅ **自动区分设备** - 自动识别无人机和机场数据
|
||||
|
||||
## 架构设计
|
||||
|
||||
```
|
||||
DjiMqttClientManager (管理器)
|
||||
↓
|
||||
DjiMqttClientService (客户端实例)
|
||||
↓
|
||||
DjiMqttMessageHandler (消息处理器)
|
||||
↓
|
||||
IDroneDataCallback / IDockDataCallback (回调接口)
|
||||
```
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 1. 注入管理器
|
||||
|
||||
```java
|
||||
@Autowired
|
||||
private DjiMqttClientManager clientManager;
|
||||
```
|
||||
|
||||
### 2. 创建客户端
|
||||
|
||||
```java
|
||||
// 构建配置
|
||||
DjiMqttClientConfig config = DjiMqttClientConfig.builder()
|
||||
.host("mqtt.t-aaron.com") // MQTT服务器地址
|
||||
.port(10883) // 端口
|
||||
.clientId("my_client_1") // 客户端ID(必须唯一)
|
||||
.username("admin") // 用户名
|
||||
.password("admin") // 密码
|
||||
.useSharedSubscription(true) // 是否使用共享订阅
|
||||
.sharedGroupName("dji-group") // 共享订阅组名
|
||||
.build();
|
||||
|
||||
// 创建并连接客户端
|
||||
String clientId = clientManager.createClient(config);
|
||||
```
|
||||
|
||||
### 3. 注册回调
|
||||
|
||||
```java
|
||||
// 获取消息处理器
|
||||
DjiMqttMessageHandler handler = clientManager.getHandler(clientId);
|
||||
|
||||
// 注册无人机数据回调
|
||||
handler.registerDroneDataCallback(droneData -> {
|
||||
System.out.println("无人机SN: " + droneData.getDeviceSn());
|
||||
System.out.println("位置: " + droneData.getLatitude() + ", " + droneData.getLongitude());
|
||||
});
|
||||
|
||||
// 注册机场数据回调
|
||||
handler.registerDockDataCallback(dockData -> {
|
||||
System.out.println("机场SN: " + dockData.getDeviceSn());
|
||||
System.out.println("温度: " + dockData.getTemperature());
|
||||
});
|
||||
```
|
||||
|
||||
## 多客户端示例
|
||||
|
||||
### 场景:同时连接多个MQTT服务器
|
||||
|
||||
```java
|
||||
@Component
|
||||
public class MyMqttService {
|
||||
|
||||
@Autowired
|
||||
private DjiMqttClientManager clientManager;
|
||||
|
||||
public void init() {
|
||||
// 客户端1:连接到服务器A
|
||||
DjiMqttClientConfig config1 = DjiMqttClientConfig.builder()
|
||||
.host("mqtt.server-a.com")
|
||||
.port(10883)
|
||||
.clientId("client_a")
|
||||
.username("admin")
|
||||
.password("admin")
|
||||
.useSharedSubscription(true)
|
||||
.build();
|
||||
|
||||
String clientId1 = clientManager.createClient(config1);
|
||||
DjiMqttMessageHandler handler1 = clientManager.getHandler(clientId1);
|
||||
handler1.registerDroneDataCallback(data -> processServerA(data));
|
||||
|
||||
// 客户端2:连接到服务器B
|
||||
DjiMqttClientConfig config2 = DjiMqttClientConfig.builder()
|
||||
.host("mqtt.server-b.com")
|
||||
.port(1883)
|
||||
.clientId("client_b")
|
||||
.username("user2")
|
||||
.password("pass2")
|
||||
.useSharedSubscription(false)
|
||||
.build();
|
||||
|
||||
String clientId2 = clientManager.createClient(config2);
|
||||
DjiMqttMessageHandler handler2 = clientManager.getHandler(clientId2);
|
||||
handler2.registerDroneDataCallback(data -> processServerB(data));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 管理器API
|
||||
|
||||
### 创建客户端
|
||||
```java
|
||||
String clientId = clientManager.createClient(config);
|
||||
```
|
||||
|
||||
### 获取消息处理器
|
||||
```java
|
||||
DjiMqttMessageHandler handler = clientManager.getHandler(clientId);
|
||||
```
|
||||
|
||||
### 获取客户端
|
||||
```java
|
||||
DjiMqttClientService client = clientManager.getClient(clientId);
|
||||
boolean isConnected = client.isConnected();
|
||||
```
|
||||
|
||||
### 移除客户端
|
||||
```java
|
||||
clientManager.removeClient(clientId);
|
||||
```
|
||||
|
||||
### 断开所有客户端
|
||||
```java
|
||||
clientManager.disconnectAll();
|
||||
```
|
||||
|
||||
### 获取所有客户端ID
|
||||
```java
|
||||
Set<String> clientIds = clientManager.getAllClientIds();
|
||||
```
|
||||
|
||||
## 配置参数说明
|
||||
|
||||
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|
||||
|------|------|------|--------|------|
|
||||
| host | String | 是 | - | MQTT服务器地址 |
|
||||
| port | Integer | 是 | - | MQTT服务器端口 |
|
||||
| clientId | String | 是 | - | 客户端ID(必须唯一) |
|
||||
| username | String | 是 | - | 用户名 |
|
||||
| password | String | 是 | - | 密码 |
|
||||
| connectionTimeout | Integer | 否 | 30 | 连接超时时间(秒) |
|
||||
| keepAliveInterval | Integer | 否 | 60 | 保持连接时间(秒) |
|
||||
| autoReconnect | Boolean | 否 | true | 自动重连 |
|
||||
| cleanSession | Boolean | 否 | false | 清除会话 |
|
||||
| useSharedSubscription | Boolean | 否 | true | 是否使用共享订阅 |
|
||||
| sharedGroupName | String | 否 | dji-group | 共享订阅组名 |
|
||||
|
||||
## 数据模型
|
||||
|
||||
### DroneData(无人机数据)
|
||||
|
||||
包含100+字段,主要分类:
|
||||
- **基础信息**:固件版本、飞行器状态、档位等
|
||||
- **位置信息**:经纬度、高度、Home点等
|
||||
- **姿态信息**:偏航角、横滚角、俯仰角
|
||||
- **速度信息**:水平速度、垂直速度、风速
|
||||
- **电池信息**:电量、剩余飞行时间、电池详情
|
||||
- **相机信息**:拍照录像状态、变焦、红外测温等
|
||||
- **避障信息**:水平/上视/下视避障状态
|
||||
- **保养信息**:保养状态、累计飞行时间/架次
|
||||
|
||||
### DockData(机场数据)
|
||||
|
||||
包含60+字段,主要分类:
|
||||
- **基础信息**:固件版本、机场状态、任务状态
|
||||
- **位置信息**:经纬度、高度、朝向角
|
||||
- **环境信息**:温度、湿度、风速、降雨量
|
||||
- **设备状态**:舱盖状态、飞行器在舱状态、补光灯等
|
||||
- **电池信息**:充电状态、备用电池、电池保养
|
||||
- **网络信息**:网络类型、质量、速率
|
||||
- **图传信息**:4G/SDR链路状态、信号质量
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **clientId必须唯一**:每个MQTT客户端的clientId必须全局唯一
|
||||
2. **部分字段推送**:每次MQTT消息可能只包含部分字段,使用时需要判空
|
||||
3. **原始数据访问**:所有字段都保存在`rawData`中,可以通过Map访问
|
||||
4. **共享订阅**:多实例部署时建议开启共享订阅,避免重复消费
|
||||
5. **独立处理器**:每个客户端有独立的消息处理器,互不影响
|
||||
6. **自动重连**:连接断开后会自动重连(可配置)
|
||||
|
||||
## 完整示例
|
||||
|
||||
参考 `DjiMqttUsageExample.java` 获取完整示例代码。
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.ruoyi.device.domain.impl.djimqtt.callback;
|
||||
|
||||
import com.ruoyi.device.domain.impl.djimqtt.model.DockData;
|
||||
|
||||
/**
|
||||
* 机场数据回调接口
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
public interface IDockDataCallback {
|
||||
|
||||
/**
|
||||
* 处理机场数据
|
||||
*
|
||||
* @param dockData 机场数据
|
||||
*/
|
||||
void onDockData(DockData dockData);
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.ruoyi.device.domain.impl.djimqtt.callback;
|
||||
|
||||
import com.ruoyi.device.domain.impl.djimqtt.model.DroneData;
|
||||
|
||||
/**
|
||||
* 无人机数据回调接口
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
public interface IDroneDataCallback {
|
||||
|
||||
/**
|
||||
* 处理无人机数据
|
||||
*
|
||||
* @param droneData 无人机数据
|
||||
*/
|
||||
void onDroneData(DroneData droneData);
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
package com.ruoyi.device.domain.impl.djimqtt.config;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* DJI MQTT客户端配置
|
||||
* 用于动态创建MQTT客户端
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
public class DjiMqttClientConfig {
|
||||
|
||||
/**
|
||||
* MQTT服务器地址
|
||||
*/
|
||||
private String host;
|
||||
|
||||
/**
|
||||
* MQTT服务器端口
|
||||
*/
|
||||
private Integer port;
|
||||
|
||||
/**
|
||||
* 客户端ID(必须唯一)
|
||||
*/
|
||||
private String clientId;
|
||||
|
||||
/**
|
||||
* 用户名
|
||||
*/
|
||||
private String username;
|
||||
|
||||
/**
|
||||
* 密码
|
||||
*/
|
||||
private String password;
|
||||
|
||||
/**
|
||||
* 连接超时时间(秒)
|
||||
*/
|
||||
@Builder.Default
|
||||
private Integer connectionTimeout = 30;
|
||||
|
||||
/**
|
||||
* 保持连接时间(秒)
|
||||
*/
|
||||
@Builder.Default
|
||||
private Integer keepAliveInterval = 60;
|
||||
|
||||
/**
|
||||
* 自动重连
|
||||
*/
|
||||
@Builder.Default
|
||||
private Boolean autoReconnect = true;
|
||||
|
||||
/**
|
||||
* 清除会话
|
||||
*/
|
||||
@Builder.Default
|
||||
private Boolean cleanSession = false;
|
||||
|
||||
/**
|
||||
* 是否使用共享订阅
|
||||
*/
|
||||
@Builder.Default
|
||||
private Boolean useSharedSubscription = true;
|
||||
|
||||
/**
|
||||
* 共享订阅组名
|
||||
*/
|
||||
@Builder.Default
|
||||
private String sharedGroupName = "dji-group";
|
||||
}
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
package com.ruoyi.device.domain.impl.djimqtt.example;
|
||||
|
||||
import com.ruoyi.device.domain.impl.djimqtt.callback.IDockDataCallback;
|
||||
import com.ruoyi.device.domain.impl.djimqtt.callback.IDroneDataCallback;
|
||||
import com.ruoyi.device.domain.impl.djimqtt.config.DjiMqttClientConfig;
|
||||
import com.ruoyi.device.domain.impl.djimqtt.handler.DjiMqttMessageHandler;
|
||||
import com.ruoyi.device.domain.impl.djimqtt.manager.DjiMqttClientManager;
|
||||
import com.ruoyi.device.domain.impl.djimqtt.model.DockData;
|
||||
import com.ruoyi.device.domain.impl.djimqtt.model.DroneData;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* DJI MQTT使用示例(支持多客户端)
|
||||
*
|
||||
* 使用说明:
|
||||
* 1. 注入 DjiMqttClientManager
|
||||
* 2. 使用 DjiMqttClientConfig.builder() 创建配置
|
||||
* 3. 调用 manager.createClient(config) 创建客户端
|
||||
* 4. 通过 manager.getHandler(clientId) 获取消息处理器
|
||||
* 5. 注册回调处理数据
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class DjiMqttUsageExample {
|
||||
|
||||
// @Autowired
|
||||
// private DjiMqttClientManager clientManager;
|
||||
//
|
||||
// /**
|
||||
// * 应用启动后创建MQTT客户端
|
||||
// */
|
||||
// @EventListener(ApplicationReadyEvent.class)
|
||||
// public void onApplicationReady() {
|
||||
// // 示例1:创建第一个MQTT客户端
|
||||
// createClient1();
|
||||
//
|
||||
// // 示例2:创建第二个MQTT客户端(不同的服务器)
|
||||
// createClient2();
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 创建第一个MQTT客户端
|
||||
// */
|
||||
// private void createClient1() {
|
||||
// // 构建配置
|
||||
// DjiMqttClientConfig config = DjiMqttClientConfig.builder()
|
||||
// .host("mqtt.t-aaron.com")
|
||||
// .port(10883)
|
||||
// .clientId("client_1")
|
||||
// .username("admin")
|
||||
// .password("admin")
|
||||
// .useSharedSubscription(true)
|
||||
// .sharedGroupName("dji-group-1")
|
||||
// .build();
|
||||
//
|
||||
// // 创建客户端
|
||||
// String clientId = clientManager.createClient(config);
|
||||
//
|
||||
// // 获取消息处理器
|
||||
// DjiMqttMessageHandler handler = clientManager.getHandler(clientId);
|
||||
//
|
||||
// // 注册无人机数据回调
|
||||
// handler.registerDroneDataCallback(new IDroneDataCallback() {
|
||||
// @Override
|
||||
// public void onDroneData(DroneData droneData) {
|
||||
// handleDroneDataForClient1(droneData);
|
||||
// }
|
||||
// });
|
||||
//
|
||||
// // 注册机场数据回调
|
||||
// handler.registerDockDataCallback(new IDockDataCallback() {
|
||||
// @Override
|
||||
// public void onDockData(DockData dockData) {
|
||||
// handleDockDataForClient1(dockData);
|
||||
// }
|
||||
// });
|
||||
//
|
||||
// log.info("客户端1已创建并注册回调");
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 创建第二个MQTT客户端(连接到不同的服务器)
|
||||
// */
|
||||
// private void createClient2() {
|
||||
// // 构建配置
|
||||
// DjiMqttClientConfig config = DjiMqttClientConfig.builder()
|
||||
// .host("mqtt.another-server.com")
|
||||
// .port(1883)
|
||||
// .clientId("client_2")
|
||||
// .username("user2")
|
||||
// .password("pass2")
|
||||
// .useSharedSubscription(false) // 不使用共享订阅
|
||||
// .build();
|
||||
//
|
||||
// // 创建客户端
|
||||
// String clientId = clientManager.createClient(config);
|
||||
//
|
||||
// // 获取消息处理器
|
||||
// DjiMqttMessageHandler handler = clientManager.getHandler(clientId);
|
||||
//
|
||||
// // 注册回调
|
||||
// handler.registerDroneDataCallback(droneData -> handleDroneDataForClient2(droneData));
|
||||
// handler.registerDockDataCallback(dockData -> handleDockDataForClient2(dockData));
|
||||
//
|
||||
// log.info("客户端2已创建并注册回调");
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 处理客户端1的无人机数据
|
||||
// */
|
||||
// private void handleDroneDataForClient1(DroneData droneData) {
|
||||
// log.info("[客户端1] 收到无人机数据 - SN: {}, Type: {}",
|
||||
// droneData.getDeviceSn(),
|
||||
// droneData.getMessageType());
|
||||
//
|
||||
// // 处理位置信息
|
||||
// if (droneData.getLatitude() != null && droneData.getLongitude() != null) {
|
||||
// log.info("[客户端1] 无人机位置 - 纬度: {}, 经度: {}, 高度: {}",
|
||||
// droneData.getLatitude(),
|
||||
// droneData.getLongitude(),
|
||||
// droneData.getElevation());
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 处理客户端1的机场数据
|
||||
// */
|
||||
// private void handleDockDataForClient1(DockData dockData) {
|
||||
// log.info("[客户端1] 收到机场数据 - SN: {}, Type: {}",
|
||||
// dockData.getDeviceSn(),
|
||||
// dockData.getMessageType());
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 处理客户端2的无人机数据
|
||||
// */
|
||||
// private void handleDroneDataForClient2(DroneData droneData) {
|
||||
// log.info("[客户端2] 收到无人机数据 - SN: {}", droneData.getDeviceSn());
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 处理客户端2的机场数据
|
||||
// */
|
||||
// private void handleDockDataForClient2(DockData dockData) {
|
||||
// log.info("[客户端2] 收到机场数据 - SN: {}", dockData.getDeviceSn());
|
||||
// }
|
||||
}
|
||||
|
|
@ -0,0 +1,216 @@
|
|||
package com.ruoyi.device.domain.impl.djimqtt.handler;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.ruoyi.device.domain.impl.djimqtt.callback.IDockDataCallback;
|
||||
import com.ruoyi.device.domain.impl.djimqtt.callback.IDroneDataCallback;
|
||||
import com.ruoyi.device.domain.impl.djimqtt.model.DjiMqttMessage;
|
||||
import com.ruoyi.device.domain.impl.djimqtt.model.DockData;
|
||||
import com.ruoyi.device.domain.impl.djimqtt.model.DroneData;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* DJI MQTT消息处理器
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class DjiMqttMessageHandler {
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
/**
|
||||
* 无人机数据回调列表
|
||||
*/
|
||||
private final List<IDroneDataCallback> droneDataCallbacks = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* 机场数据回调列表
|
||||
*/
|
||||
private final List<IDockDataCallback> dockDataCallbacks = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* 无人机SN正则表达式(根据文档示例:1581F6Q8X251C00G04H8)
|
||||
*/
|
||||
private static final Pattern DRONE_SN_PATTERN = Pattern.compile("^[0-9A-Z]{20}$");
|
||||
|
||||
/**
|
||||
* 机场SN正则表达式(根据文档示例:7CTXN5K00B0AXM)
|
||||
*/
|
||||
private static final Pattern DOCK_SN_PATTERN = Pattern.compile("^[0-9A-Z]{14}$");
|
||||
|
||||
/**
|
||||
* 注册无人机数据回调
|
||||
*
|
||||
* @param callback 回调接口
|
||||
*/
|
||||
public void registerDroneDataCallback(IDroneDataCallback callback) {
|
||||
if (callback != null && !droneDataCallbacks.contains(callback)) {
|
||||
droneDataCallbacks.add(callback);
|
||||
log.info("注册无人机数据回调: {}", callback.getClass().getSimpleName());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册机场数据回调
|
||||
*
|
||||
* @param callback 回调接口
|
||||
*/
|
||||
public void registerDockDataCallback(IDockDataCallback callback) {
|
||||
if (callback != null && !dockDataCallbacks.contains(callback)) {
|
||||
dockDataCallbacks.add(callback);
|
||||
log.info("注册机场数据回调: {}", callback.getClass().getSimpleName());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理MQTT消息
|
||||
*
|
||||
* @param topic 主题
|
||||
* @param payload 消息内容
|
||||
*/
|
||||
public void handleMessage(String topic, String payload) {
|
||||
try {
|
||||
log.debug("收到MQTT消息 - Topic: {}, Payload: {}", topic, payload);
|
||||
|
||||
// 解析设备SN和消息类型
|
||||
String deviceSn = extractDeviceSnFromTopic(topic);
|
||||
String messageType = extractMessageTypeFromTopic(topic);
|
||||
|
||||
if (deviceSn == null || messageType == null) {
|
||||
log.warn("无法从Topic解析设备SN或消息类型: {}", topic);
|
||||
return;
|
||||
}
|
||||
|
||||
// 解析JSON消息
|
||||
@SuppressWarnings("unchecked")
|
||||
DjiMqttMessage<Map<String, Object>> message = objectMapper.readValue(
|
||||
payload,
|
||||
objectMapper.getTypeFactory().constructParametricType(
|
||||
DjiMqttMessage.class,
|
||||
Map.class
|
||||
)
|
||||
);
|
||||
|
||||
// 判断是无人机还是机场
|
||||
if (isDroneSn(deviceSn)) {
|
||||
handleDroneMessage(deviceSn, messageType, message);
|
||||
} else if (isDockSn(deviceSn)) {
|
||||
handleDockMessage(deviceSn, messageType, message);
|
||||
} else {
|
||||
log.warn("未知的设备SN格式: {}", deviceSn);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理MQTT消息失败 - Topic: {}, Error: {}", topic, e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理无人机消息
|
||||
*/
|
||||
private void handleDroneMessage(String deviceSn, String messageType, DjiMqttMessage<Map<String, Object>> message) {
|
||||
try {
|
||||
DroneData droneData = objectMapper.convertValue(message.getData(), DroneData.class);
|
||||
droneData.setDeviceSn(deviceSn);
|
||||
droneData.setMessageType(messageType);
|
||||
droneData.setRawData(message.getData());
|
||||
|
||||
log.debug("处理无人机数据 - SN: {}, Type: {}", deviceSn, messageType);
|
||||
|
||||
// 通知所有回调
|
||||
for (IDroneDataCallback callback : droneDataCallbacks) {
|
||||
try {
|
||||
callback.onDroneData(droneData);
|
||||
} catch (Exception e) {
|
||||
log.error("无人机数据回调执行失败: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("处理无人机消息失败 - SN: {}, Error: {}", deviceSn, e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理机场消息
|
||||
*/
|
||||
private void handleDockMessage(String deviceSn, String messageType, DjiMqttMessage<Map<String, Object>> message) {
|
||||
try {
|
||||
DockData dockData = objectMapper.convertValue(message.getData(), DockData.class);
|
||||
dockData.setDeviceSn(deviceSn);
|
||||
dockData.setMessageType(messageType);
|
||||
dockData.setRawData(message.getData());
|
||||
|
||||
log.debug("处理机场数据 - SN: {}, Type: {}", deviceSn, messageType);
|
||||
|
||||
// 通知所有回调
|
||||
for (IDockDataCallback callback : dockDataCallbacks) {
|
||||
try {
|
||||
callback.onDockData(dockData);
|
||||
} catch (Exception e) {
|
||||
log.error("机场数据回调执行失败: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("处理机场消息失败 - SN: {}, Error: {}", deviceSn, e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从Topic中提取设备SN
|
||||
* Topic格式: thing/product/{deviceSn}/osd 或 thing/product/{deviceSn}/state
|
||||
*/
|
||||
private String extractDeviceSnFromTopic(String topic) {
|
||||
if (topic == null) {
|
||||
return null;
|
||||
}
|
||||
String[] parts = topic.split("/");
|
||||
if (parts.length >= 3) {
|
||||
return parts[2];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从Topic中提取消息类型
|
||||
*/
|
||||
private String extractMessageTypeFromTopic(String topic) {
|
||||
if (topic == null) {
|
||||
return null;
|
||||
}
|
||||
String[] parts = topic.split("/");
|
||||
if (parts.length >= 4) {
|
||||
return parts[3]; // osd 或 state
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为无人机SN
|
||||
*/
|
||||
private boolean isDroneSn(String sn) {
|
||||
if (sn == null) {
|
||||
return false;
|
||||
}
|
||||
Matcher matcher = DRONE_SN_PATTERN.matcher(sn);
|
||||
return matcher.matches();
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为机场SN
|
||||
*/
|
||||
private boolean isDockSn(String sn) {
|
||||
if (sn == null) {
|
||||
return false;
|
||||
}
|
||||
Matcher matcher = DOCK_SN_PATTERN.matcher(sn);
|
||||
return matcher.matches();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
package com.ruoyi.device.domain.impl.djimqtt.manager;
|
||||
|
||||
import com.ruoyi.device.domain.impl.djimqtt.config.DjiMqttClientConfig;
|
||||
import com.ruoyi.device.domain.impl.djimqtt.handler.DjiMqttMessageHandler;
|
||||
import com.ruoyi.device.domain.impl.djimqtt.service.DjiMqttClientService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* DJI MQTT客户端管理器
|
||||
* 管理多个MQTT客户端实例
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class DjiMqttClientManager {
|
||||
|
||||
/**
|
||||
* 客户端映射表 clientId -> DjiMqttClientService
|
||||
*/
|
||||
private final Map<String, DjiMqttClientService> clients = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 消息处理器映射表 clientId -> DjiMqttMessageHandler
|
||||
*/
|
||||
private final Map<String, DjiMqttMessageHandler> handlers = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 创建并连接MQTT客户端
|
||||
*
|
||||
* @param config 客户端配置
|
||||
* @return 客户端ID
|
||||
*/
|
||||
public String createClient(DjiMqttClientConfig config) {
|
||||
String clientId = config.getClientId();
|
||||
|
||||
if (clients.containsKey(clientId)) {
|
||||
log.warn("MQTT客户端[{}]已存在", clientId);
|
||||
return clientId;
|
||||
}
|
||||
|
||||
// 为每个客户端创建独立的消息处理器
|
||||
DjiMqttMessageHandler handler = new DjiMqttMessageHandler();
|
||||
handlers.put(clientId, handler);
|
||||
|
||||
// 创建客户端
|
||||
DjiMqttClientService client = new DjiMqttClientService(config, handler);
|
||||
clients.put(clientId, client);
|
||||
|
||||
// 连接
|
||||
client.connect();
|
||||
|
||||
log.info("成功创建MQTT客户端[{}]", clientId);
|
||||
return clientId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取消息处理器
|
||||
*
|
||||
* @param clientId 客户端ID
|
||||
* @return 消息处理器
|
||||
*/
|
||||
public DjiMqttMessageHandler getHandler(String clientId) {
|
||||
return handlers.get(clientId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取客户端
|
||||
*
|
||||
* @param clientId 客户端ID
|
||||
* @return 客户端服务
|
||||
*/
|
||||
public DjiMqttClientService getClient(String clientId) {
|
||||
return clients.get(clientId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 断开并移除客户端
|
||||
*
|
||||
* @param clientId 客户端ID
|
||||
*/
|
||||
public void removeClient(String clientId) {
|
||||
DjiMqttClientService client = clients.remove(clientId);
|
||||
if (client != null) {
|
||||
client.disconnect();
|
||||
log.info("已移除MQTT客户端[{}]", clientId);
|
||||
}
|
||||
|
||||
handlers.remove(clientId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 断开所有客户端
|
||||
*/
|
||||
public void disconnectAll() {
|
||||
clients.forEach((clientId, client) -> {
|
||||
try {
|
||||
client.disconnect();
|
||||
log.info("已断开MQTT客户端[{}]", clientId);
|
||||
} catch (Exception e) {
|
||||
log.error("断开MQTT客户端[{}]失败: {}", clientId, e.getMessage(), e);
|
||||
}
|
||||
});
|
||||
clients.clear();
|
||||
handlers.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有客户端ID
|
||||
*/
|
||||
public java.util.Set<String> getAllClientIds() {
|
||||
return clients.keySet();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查客户端是否存在
|
||||
*/
|
||||
public boolean hasClient(String clientId) {
|
||||
return clients.containsKey(clientId);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
package com.ruoyi.device.domain.impl.djimqtt.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* DJI MQTT消息基础结构
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Data
|
||||
public class DjiMqttMessage<T> {
|
||||
|
||||
/**
|
||||
* 事务ID
|
||||
*/
|
||||
@JsonProperty("tid")
|
||||
private String tid;
|
||||
|
||||
/**
|
||||
* 业务ID
|
||||
*/
|
||||
@JsonProperty("bid")
|
||||
private String bid;
|
||||
|
||||
/**
|
||||
* 时间戳
|
||||
*/
|
||||
@JsonProperty("timestamp")
|
||||
private Long timestamp;
|
||||
|
||||
/**
|
||||
* 数据内容
|
||||
*/
|
||||
@JsonProperty("data")
|
||||
private T data;
|
||||
|
||||
/**
|
||||
* 网关设备SN
|
||||
*/
|
||||
@JsonProperty("gateway")
|
||||
private String gateway;
|
||||
|
||||
/**
|
||||
* 消息来源
|
||||
*/
|
||||
@JsonProperty("source")
|
||||
private String source;
|
||||
|
||||
/**
|
||||
* 是否需要回复
|
||||
*/
|
||||
@JsonProperty("need_reply")
|
||||
private Integer needReply;
|
||||
}
|
||||
|
|
@ -0,0 +1,291 @@
|
|||
package com.ruoyi.device.domain.impl.djimqtt.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.ruoyi.device.domain.impl.djimqtt.model.dock.*;
|
||||
import com.ruoyi.device.domain.impl.djimqtt.model.drone.MaintainStatus;
|
||||
import com.ruoyi.device.domain.impl.djimqtt.model.drone.PositionState;
|
||||
import com.ruoyi.device.domain.impl.djimqtt.model.drone.StorageInfo;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 机场完整数据(从osd和state主题接收)
|
||||
* 注意:每次推送可能只包含部分字段
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class DockData {
|
||||
|
||||
// ========== 元数据 ==========
|
||||
|
||||
/**
|
||||
* 设备SN(从topic中解析)
|
||||
*/
|
||||
private String deviceSn;
|
||||
|
||||
/**
|
||||
* 消息类型(osd或state)
|
||||
*/
|
||||
private String messageType;
|
||||
|
||||
/**
|
||||
* 原始数据(用于存储所有字段)
|
||||
*/
|
||||
private Map<String, Object> rawData;
|
||||
|
||||
// ========== 基础信息 ==========
|
||||
|
||||
/**
|
||||
* Home 点有效性:0-无效,1-有效
|
||||
*/
|
||||
@JsonProperty("home_position_is_valid")
|
||||
private Integer homePositionIsValid;
|
||||
|
||||
/**
|
||||
* 机场朝向角
|
||||
*/
|
||||
@JsonProperty("heading")
|
||||
private Double heading;
|
||||
|
||||
/**
|
||||
* 固件版本
|
||||
*/
|
||||
@JsonProperty("firmware_version")
|
||||
private String firmwareVersion;
|
||||
|
||||
/**
|
||||
* 机场状态
|
||||
*/
|
||||
@JsonProperty("mode_code")
|
||||
private Integer modeCode;
|
||||
|
||||
/**
|
||||
* 机场任务状态
|
||||
*/
|
||||
@JsonProperty("flighttask_step_code")
|
||||
private Integer flighttaskStepCode;
|
||||
|
||||
// ========== 位置信息 ==========
|
||||
|
||||
/**
|
||||
* 纬度
|
||||
*/
|
||||
@JsonProperty("latitude")
|
||||
private Double latitude;
|
||||
|
||||
/**
|
||||
* 经度
|
||||
*/
|
||||
@JsonProperty("longitude")
|
||||
private Double longitude;
|
||||
|
||||
/**
|
||||
* 椭球高度(米)
|
||||
*/
|
||||
@JsonProperty("height")
|
||||
private Double height;
|
||||
|
||||
// ========== 环境信息 ==========
|
||||
|
||||
/**
|
||||
* 舱内温度(摄氏度)
|
||||
*/
|
||||
@JsonProperty("temperature")
|
||||
private Float temperature;
|
||||
|
||||
/**
|
||||
* 舱内湿度(%RH)
|
||||
*/
|
||||
@JsonProperty("humidity")
|
||||
private Float humidity;
|
||||
|
||||
/**
|
||||
* 环境温度(摄氏度)
|
||||
*/
|
||||
@JsonProperty("environment_temperature")
|
||||
private Float environmentTemperature;
|
||||
|
||||
/**
|
||||
* 风速(米/秒)
|
||||
*/
|
||||
@JsonProperty("wind_speed")
|
||||
private Float windSpeed;
|
||||
|
||||
/**
|
||||
* 降雨量:0-无雨,1-小雨,2-中雨,3-大雨
|
||||
*/
|
||||
@JsonProperty("rainfall")
|
||||
private Integer rainfall;
|
||||
|
||||
// ========== 设备状态 ==========
|
||||
|
||||
/**
|
||||
* 舱盖状态:0-关闭,1-打开,2-半开,3-舱盖状态异常
|
||||
*/
|
||||
@JsonProperty("cover_state")
|
||||
private Integer coverState;
|
||||
|
||||
/**
|
||||
* 飞行器是否在舱:0-舱外,1-舱内
|
||||
*/
|
||||
@JsonProperty("drone_in_dock")
|
||||
private Integer droneInDock;
|
||||
|
||||
/**
|
||||
* DRC链路状态:0-未连接,1-连接中,2-已连接
|
||||
*/
|
||||
@JsonProperty("drc_state")
|
||||
private Integer drcState;
|
||||
|
||||
/**
|
||||
* 紧急停止按钮状态:0-关闭,1-开启
|
||||
*/
|
||||
@JsonProperty("emergency_stop_state")
|
||||
private Integer emergencyStopState;
|
||||
|
||||
/**
|
||||
* 补光灯状态:0-关闭,1-打开
|
||||
*/
|
||||
@JsonProperty("supplement_light_state")
|
||||
private Integer supplementLightState;
|
||||
|
||||
/**
|
||||
* 机场声光报警状态:0-关闭,1-开启
|
||||
*/
|
||||
@JsonProperty("alarm_state")
|
||||
private Integer alarmState;
|
||||
|
||||
// ========== 电池和电源 ==========
|
||||
|
||||
/**
|
||||
* 飞行器电池保养信息
|
||||
*/
|
||||
@JsonProperty("drone_battery_maintenance_info")
|
||||
private DroneBatteryMaintenanceInfo droneBatteryMaintenanceInfo;
|
||||
|
||||
/**
|
||||
* 飞行器充电状态
|
||||
*/
|
||||
@JsonProperty("drone_charge_state")
|
||||
private DroneChargeState droneChargeState;
|
||||
|
||||
/**
|
||||
* 机场备用电池信息
|
||||
*/
|
||||
@JsonProperty("backup_battery")
|
||||
private BackupBattery backupBattery;
|
||||
|
||||
/**
|
||||
* 电池运行模式:1-计划模式,2-待命模式
|
||||
*/
|
||||
@JsonProperty("battery_store_mode")
|
||||
private Integer batteryStoreMode;
|
||||
|
||||
/**
|
||||
* 工作电流(毫安)
|
||||
*/
|
||||
@JsonProperty("working_current")
|
||||
private Float workingCurrent;
|
||||
|
||||
/**
|
||||
* 工作电压(毫伏)
|
||||
*/
|
||||
@JsonProperty("working_voltage")
|
||||
private Integer workingVoltage;
|
||||
|
||||
// ========== 网络和存储 ==========
|
||||
|
||||
/**
|
||||
* 网络状态
|
||||
*/
|
||||
@JsonProperty("network_state")
|
||||
private NetworkState networkState;
|
||||
|
||||
/**
|
||||
* 图传链路
|
||||
*/
|
||||
@JsonProperty("wireless_link")
|
||||
private WirelessLink wirelessLink;
|
||||
|
||||
/**
|
||||
* 存储容量
|
||||
*/
|
||||
@JsonProperty("storage")
|
||||
private StorageInfo storage;
|
||||
|
||||
// ========== 空调和配置 ==========
|
||||
|
||||
/**
|
||||
* 机场空调工作状态信息
|
||||
*/
|
||||
@JsonProperty("air_conditioner")
|
||||
private AirConditioner airConditioner;
|
||||
|
||||
/**
|
||||
* 空中回传:false-关闭,true-开启
|
||||
*/
|
||||
@JsonProperty("air_transfer_enable")
|
||||
private Boolean airTransferEnable;
|
||||
|
||||
/**
|
||||
* 机场静音模式:0-非静音模式,1-静音模式
|
||||
*/
|
||||
@JsonProperty("silent_mode")
|
||||
private Integer silentMode;
|
||||
|
||||
// ========== 保养和固件 ==========
|
||||
|
||||
/**
|
||||
* 保养信息
|
||||
*/
|
||||
@JsonProperty("maintain_status")
|
||||
private MaintainStatus maintainStatus;
|
||||
|
||||
/**
|
||||
* 搜星状态
|
||||
*/
|
||||
@JsonProperty("position_state")
|
||||
private PositionState positionState;
|
||||
|
||||
/**
|
||||
* 机场激活时间(unix 时间戳)
|
||||
*/
|
||||
@JsonProperty("activation_time")
|
||||
private Long activationTime;
|
||||
|
||||
/**
|
||||
* 固件升级状态:0-未升级,1-升级中
|
||||
*/
|
||||
@JsonProperty("firmware_upgrade_status")
|
||||
private Integer firmwareUpgradeStatus;
|
||||
|
||||
/**
|
||||
* 固件一致性:0-不需要一致性升级,1-需要一致性升级
|
||||
*/
|
||||
@JsonProperty("compatible_status")
|
||||
private Integer compatibleStatus;
|
||||
|
||||
// ========== 统计信息 ==========
|
||||
|
||||
/**
|
||||
* 机场累计运行时长(秒)
|
||||
*/
|
||||
@JsonProperty("acc_time")
|
||||
private Integer accTime;
|
||||
|
||||
/**
|
||||
* 机场累计作业次数
|
||||
*/
|
||||
@JsonProperty("job_number")
|
||||
private Integer jobNumber;
|
||||
|
||||
/**
|
||||
* 首次上电时间(毫秒)
|
||||
*/
|
||||
@JsonProperty("first_power_on")
|
||||
private Long firstPowerOn;
|
||||
}
|
||||
|
|
@ -0,0 +1,329 @@
|
|||
package com.ruoyi.device.domain.impl.djimqtt.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.ruoyi.device.domain.impl.djimqtt.model.drone.*;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 无人机完整数据(从osd和state主题接收)
|
||||
* 注意:每次推送可能只包含部分字段
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class DroneData {
|
||||
|
||||
// ========== 元数据 ==========
|
||||
|
||||
/**
|
||||
* 设备SN(从topic中解析)
|
||||
*/
|
||||
private String deviceSn;
|
||||
|
||||
/**
|
||||
* 消息类型(osd或state)
|
||||
*/
|
||||
private String messageType;
|
||||
|
||||
/**
|
||||
* 原始数据(用于存储所有字段)
|
||||
*/
|
||||
private Map<String, Object> rawData;
|
||||
|
||||
// ========== 基础信息 ==========
|
||||
|
||||
/**
|
||||
* 飞行器图传连接质量最好的网关SN
|
||||
*/
|
||||
@JsonProperty("best_link_gateway")
|
||||
private String bestLinkGateway;
|
||||
|
||||
/**
|
||||
* 图传连接拓扑
|
||||
*/
|
||||
@JsonProperty("wireless_link_topo")
|
||||
private WirelessLinkTopo wirelessLinkTopo;
|
||||
|
||||
/**
|
||||
* 飞行器相机信息
|
||||
*/
|
||||
@JsonProperty("cameras")
|
||||
private List<CameraInfo> cameras;
|
||||
|
||||
/**
|
||||
* 飞行安全数据库版本
|
||||
*/
|
||||
@JsonProperty("flysafe_database_version")
|
||||
private String flysafeDatabaseVersion;
|
||||
|
||||
/**
|
||||
* 离线地图开关:false-关闭,true-开启
|
||||
*/
|
||||
@JsonProperty("offline_map_enable")
|
||||
private Boolean offlineMapEnable;
|
||||
|
||||
// ========== 返航和限制 ==========
|
||||
|
||||
/**
|
||||
* 返航高度模式当前值:0-智能高度,1-设定高度
|
||||
*/
|
||||
@JsonProperty("current_rth_mode")
|
||||
private Integer currentRthMode;
|
||||
|
||||
/**
|
||||
* 返航高度模式设置值:0-智能高度,1-设定高度
|
||||
*/
|
||||
@JsonProperty("rth_mode")
|
||||
private Integer rthMode;
|
||||
|
||||
/**
|
||||
* 飞行器避障状态
|
||||
*/
|
||||
@JsonProperty("obstacle_avoidance")
|
||||
private ObstacleAvoidance obstacleAvoidance;
|
||||
|
||||
/**
|
||||
* 是否接近限飞区:0-未达到,1-接近
|
||||
*/
|
||||
@JsonProperty("is_near_area_limit")
|
||||
private Integer isNearAreaLimit;
|
||||
|
||||
/**
|
||||
* 是否接近设定的限制高度:0-未达到,1-接近
|
||||
*/
|
||||
@JsonProperty("is_near_height_limit")
|
||||
private Integer isNearHeightLimit;
|
||||
|
||||
/**
|
||||
* 飞行器限高(米)
|
||||
*/
|
||||
@JsonProperty("height_limit")
|
||||
private Integer heightLimit;
|
||||
|
||||
/**
|
||||
* 飞行器限远状态
|
||||
*/
|
||||
@JsonProperty("distance_limit_status")
|
||||
private DistanceLimitStatus distanceLimitStatus;
|
||||
|
||||
// ========== 设备状态 ==========
|
||||
|
||||
/**
|
||||
* 飞行器夜航灯状态:0-关闭,1-打开
|
||||
*/
|
||||
@JsonProperty("night_lights_state")
|
||||
private Integer nightLightsState;
|
||||
|
||||
/**
|
||||
* 飞行器激活时间(unix 时间戳)
|
||||
*/
|
||||
@JsonProperty("activation_time")
|
||||
private Long activationTime;
|
||||
|
||||
/**
|
||||
* 保养信息
|
||||
*/
|
||||
@JsonProperty("maintain_status")
|
||||
private MaintainStatus maintainStatus;
|
||||
|
||||
/**
|
||||
* 飞行器累计飞行总架次
|
||||
*/
|
||||
@JsonProperty("total_flight_sorties")
|
||||
private Integer totalFlightSorties;
|
||||
|
||||
/**
|
||||
* 飞行器累计飞行总里程(米)
|
||||
*/
|
||||
@JsonProperty("total_flight_distance")
|
||||
private Float totalFlightDistance;
|
||||
|
||||
/**
|
||||
* 飞行器累计飞行航时(秒)
|
||||
*/
|
||||
@JsonProperty("total_flight_time")
|
||||
private Float totalFlightTime;
|
||||
|
||||
// ========== 位置信息 ==========
|
||||
|
||||
/**
|
||||
* 搜星状态
|
||||
*/
|
||||
@JsonProperty("position_state")
|
||||
private PositionState positionState;
|
||||
|
||||
/**
|
||||
* 存储容量
|
||||
*/
|
||||
@JsonProperty("storage")
|
||||
private StorageInfo storage;
|
||||
|
||||
/**
|
||||
* 当前位置纬度
|
||||
*/
|
||||
@JsonProperty("latitude")
|
||||
private Double latitude;
|
||||
|
||||
/**
|
||||
* 当前位置经度
|
||||
*/
|
||||
@JsonProperty("longitude")
|
||||
private Double longitude;
|
||||
|
||||
/**
|
||||
* 相对起飞点高度(米)
|
||||
*/
|
||||
@JsonProperty("elevation")
|
||||
private Float elevation;
|
||||
|
||||
/**
|
||||
* 绝对高度(米)
|
||||
*/
|
||||
@JsonProperty("height")
|
||||
private Double height;
|
||||
|
||||
/**
|
||||
* Home点纬度
|
||||
*/
|
||||
@JsonProperty("home_latitude")
|
||||
private Double homeLatitude;
|
||||
|
||||
/**
|
||||
* Home点经度
|
||||
*/
|
||||
@JsonProperty("home_longitude")
|
||||
private Double homeLongitude;
|
||||
|
||||
/**
|
||||
* 距离Home点的距离(米)
|
||||
*/
|
||||
@JsonProperty("home_distance")
|
||||
private Float homeDistance;
|
||||
|
||||
// ========== 姿态信息 ==========
|
||||
|
||||
/**
|
||||
* 偏航轴角度
|
||||
*/
|
||||
@JsonProperty("attitude_head")
|
||||
private Integer attitudeHead;
|
||||
|
||||
/**
|
||||
* 横滚轴角度
|
||||
*/
|
||||
@JsonProperty("attitude_roll")
|
||||
private Float attitudeRoll;
|
||||
|
||||
/**
|
||||
* 俯仰轴角度
|
||||
*/
|
||||
@JsonProperty("attitude_pitch")
|
||||
private Float attitudePitch;
|
||||
|
||||
// ========== 速度和风速 ==========
|
||||
|
||||
/**
|
||||
* 水平速度(米/秒)
|
||||
*/
|
||||
@JsonProperty("horizontal_speed")
|
||||
private Float horizontalSpeed;
|
||||
|
||||
/**
|
||||
* 垂直速度(米/秒)
|
||||
*/
|
||||
@JsonProperty("vertical_speed")
|
||||
private Float verticalSpeed;
|
||||
|
||||
/**
|
||||
* 当前风向
|
||||
*/
|
||||
@JsonProperty("wind_direction")
|
||||
private Integer windDirection;
|
||||
|
||||
/**
|
||||
* 风速(米/秒)
|
||||
*/
|
||||
@JsonProperty("wind_speed")
|
||||
private Float windSpeed;
|
||||
|
||||
// ========== 电池信息 ==========
|
||||
|
||||
/**
|
||||
* 飞行器电池信息
|
||||
*/
|
||||
@JsonProperty("battery")
|
||||
private DroneBatteryInfo battery;
|
||||
|
||||
/**
|
||||
* 严重低电量告警百分比
|
||||
*/
|
||||
@JsonProperty("serious_low_battery_warning_threshold")
|
||||
private Integer seriousLowBatteryWarningThreshold;
|
||||
|
||||
/**
|
||||
* 低电量告警百分比
|
||||
*/
|
||||
@JsonProperty("low_battery_warning_threshold")
|
||||
private Integer lowBatteryWarningThreshold;
|
||||
|
||||
/**
|
||||
* 返航预留电量百分比
|
||||
*/
|
||||
@JsonProperty("remaining_power_for_return_home")
|
||||
private Integer remainingPowerForReturnHome;
|
||||
|
||||
// ========== 控制和状态 ==========
|
||||
|
||||
/**
|
||||
* 当前控制源
|
||||
*/
|
||||
@JsonProperty("control_source")
|
||||
private String controlSource;
|
||||
|
||||
/**
|
||||
* 固件升级状态:0-未升级,1-升级中
|
||||
*/
|
||||
@JsonProperty("firmware_upgrade_status")
|
||||
private Integer firmwareUpgradeStatus;
|
||||
|
||||
/**
|
||||
* 固件一致性:0-不需要一致性升级,1-需要一致性升级
|
||||
*/
|
||||
@JsonProperty("compatible_status")
|
||||
private Integer compatibleStatus;
|
||||
|
||||
/**
|
||||
* 固件版本
|
||||
*/
|
||||
@JsonProperty("firmware_version")
|
||||
private String firmwareVersion;
|
||||
|
||||
/**
|
||||
* 档位
|
||||
*/
|
||||
@JsonProperty("gear")
|
||||
private Integer gear;
|
||||
|
||||
/**
|
||||
* 飞行器状态
|
||||
*/
|
||||
@JsonProperty("mode_code")
|
||||
private Integer modeCode;
|
||||
|
||||
/**
|
||||
* 飞行器进入当前状态的原因
|
||||
*/
|
||||
@JsonProperty("mode_code_reason")
|
||||
private Integer modeCodeReason;
|
||||
|
||||
/**
|
||||
* 航迹ID
|
||||
*/
|
||||
@JsonProperty("track_id")
|
||||
private String trackId;
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.ruoyi.device.domain.impl.djimqtt.model.dock;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 机场空调工作状态信息
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class AirConditioner {
|
||||
|
||||
/**
|
||||
* 机场空调状态
|
||||
*/
|
||||
@JsonProperty("air_conditioner_state")
|
||||
private Integer airConditionerState;
|
||||
|
||||
/**
|
||||
* 剩余等待可切换时间(秒)
|
||||
*/
|
||||
@JsonProperty("switch_time")
|
||||
private Integer switchTime;
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.ruoyi.device.domain.impl.djimqtt.model.dock;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 机场备用电池信息
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class BackupBattery {
|
||||
|
||||
/**
|
||||
* 备用电池开关:0-关闭,1-开启
|
||||
*/
|
||||
@JsonProperty("switch")
|
||||
private Integer switchState;
|
||||
|
||||
/**
|
||||
* 备用电池电压(毫伏)
|
||||
*/
|
||||
@JsonProperty("voltage")
|
||||
private Integer voltage;
|
||||
|
||||
/**
|
||||
* 备用电池温度(摄氏度)
|
||||
*/
|
||||
@JsonProperty("temperature")
|
||||
private Float temperature;
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package com.ruoyi.device.domain.impl.djimqtt.model.dock;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 机场电池详细信息
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class DockBatteryDetail {
|
||||
|
||||
/**
|
||||
* 电池剩余电量
|
||||
*/
|
||||
@JsonProperty("capacity_percent")
|
||||
private Integer capacityPercent;
|
||||
|
||||
/**
|
||||
* 电池序号:0-左电池,1-右电池
|
||||
*/
|
||||
@JsonProperty("index")
|
||||
private Integer index;
|
||||
|
||||
/**
|
||||
* 电压(毫伏)
|
||||
*/
|
||||
@JsonProperty("voltage")
|
||||
private Integer voltage;
|
||||
|
||||
/**
|
||||
* 温度(摄氏度)
|
||||
*/
|
||||
@JsonProperty("temperature")
|
||||
private Float temperature;
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
package com.ruoyi.device.domain.impl.djimqtt.model.dock;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 飞行器电池保养信息
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class DroneBatteryMaintenanceInfo {
|
||||
|
||||
/**
|
||||
* 保养状态:0-无需保养,1-待保养,2-正在保养
|
||||
*/
|
||||
@JsonProperty("maintenance_state")
|
||||
private Integer maintenanceState;
|
||||
|
||||
/**
|
||||
* 电池保养剩余时间(小时)
|
||||
*/
|
||||
@JsonProperty("maintenance_time_left")
|
||||
private Integer maintenanceTimeLeft;
|
||||
|
||||
/**
|
||||
* 电池加热保温状态:0-未开启,1-加热中,2-保温中
|
||||
*/
|
||||
@JsonProperty("heat_state")
|
||||
private Integer heatState;
|
||||
|
||||
/**
|
||||
* 电池详细信息
|
||||
*/
|
||||
@JsonProperty("batteries")
|
||||
private List<DockBatteryDetail> batteries;
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.ruoyi.device.domain.impl.djimqtt.model.dock;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 飞行器充电状态
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class DroneChargeState {
|
||||
|
||||
/**
|
||||
* 电量百分比
|
||||
*/
|
||||
@JsonProperty("capacity_percent")
|
||||
private Integer capacityPercent;
|
||||
|
||||
/**
|
||||
* 充电状态:0-空闲,1-充电中
|
||||
*/
|
||||
@JsonProperty("state")
|
||||
private Integer state;
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.ruoyi.device.domain.impl.djimqtt.model.dock;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 网络状态
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class NetworkState {
|
||||
|
||||
/**
|
||||
* 网络类型:1-4G,2-以太网
|
||||
*/
|
||||
@JsonProperty("type")
|
||||
private Integer type;
|
||||
|
||||
/**
|
||||
* 网络质量:0-无信号,1-差,2-较差,3-一般,4-较好,5-好
|
||||
*/
|
||||
@JsonProperty("quality")
|
||||
private Integer quality;
|
||||
|
||||
/**
|
||||
* 网络速率(KB/s)
|
||||
*/
|
||||
@JsonProperty("rate")
|
||||
private Float rate;
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
package com.ruoyi.device.domain.impl.djimqtt.model.dock;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 图传链路
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class WirelessLink {
|
||||
|
||||
/**
|
||||
* 飞行器上 Dongle 数量
|
||||
*/
|
||||
@JsonProperty("dongle_number")
|
||||
private Integer dongleNumber;
|
||||
|
||||
/**
|
||||
* 4G 链路连接状态:0-断开,1-连接
|
||||
*/
|
||||
@JsonProperty("4g_link_state")
|
||||
private Integer fourGLinkState;
|
||||
|
||||
/**
|
||||
* SDR 链路连接状态:0-断开,1-连接
|
||||
*/
|
||||
@JsonProperty("sdr_link_state")
|
||||
private Integer sdrLinkState;
|
||||
|
||||
/**
|
||||
* 机场的图传链路模式:0-SDR 模式,1-4G 融合模式
|
||||
*/
|
||||
@JsonProperty("link_workmode")
|
||||
private Integer linkWorkmode;
|
||||
|
||||
/**
|
||||
* SDR 信号质量(0-5)
|
||||
*/
|
||||
@JsonProperty("sdr_quality")
|
||||
private Integer sdrQuality;
|
||||
|
||||
/**
|
||||
* 总体 4G 信号质量(0-5)
|
||||
*/
|
||||
@JsonProperty("4g_quality")
|
||||
private Integer fourGQuality;
|
||||
|
||||
/**
|
||||
* 天端 4G 信号质量(0-5)
|
||||
*/
|
||||
@JsonProperty("4g_uav_quality")
|
||||
private Integer fourGUavQuality;
|
||||
|
||||
/**
|
||||
* 地端 4G 信号质量(0-5)
|
||||
*/
|
||||
@JsonProperty("4g_gnd_quality")
|
||||
private Integer fourGGndQuality;
|
||||
|
||||
/**
|
||||
* SDR 频段
|
||||
*/
|
||||
@JsonProperty("sdr_freq_band")
|
||||
private Float sdrFreqBand;
|
||||
|
||||
/**
|
||||
* 4G 频段
|
||||
*/
|
||||
@JsonProperty("4g_freq_band")
|
||||
private Float fourGFreqBand;
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
package com.ruoyi.device.domain.impl.djimqtt.model.drone;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 电池详细信息
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class BatteryDetail {
|
||||
|
||||
/**
|
||||
* 电池剩余电量
|
||||
*/
|
||||
@JsonProperty("capacity_percent")
|
||||
private Integer capacityPercent;
|
||||
|
||||
/**
|
||||
* 电池序号
|
||||
*/
|
||||
@JsonProperty("index")
|
||||
private Integer index;
|
||||
|
||||
/**
|
||||
* 电池序列号(SN)
|
||||
*/
|
||||
@JsonProperty("sn")
|
||||
private String sn;
|
||||
|
||||
/**
|
||||
* 电池类型
|
||||
*/
|
||||
@JsonProperty("type")
|
||||
private Integer type;
|
||||
|
||||
/**
|
||||
* 电池子类型
|
||||
*/
|
||||
@JsonProperty("sub_type")
|
||||
private Integer subType;
|
||||
|
||||
/**
|
||||
* 固件版本
|
||||
*/
|
||||
@JsonProperty("firmware_version")
|
||||
private String firmwareVersion;
|
||||
|
||||
/**
|
||||
* 电池循环次数
|
||||
*/
|
||||
@JsonProperty("loop_times")
|
||||
private Integer loopTimes;
|
||||
|
||||
/**
|
||||
* 电压(毫伏)
|
||||
*/
|
||||
@JsonProperty("voltage")
|
||||
private Integer voltage;
|
||||
|
||||
/**
|
||||
* 温度(摄氏度)
|
||||
*/
|
||||
@JsonProperty("temperature")
|
||||
private Float temperature;
|
||||
|
||||
/**
|
||||
* 高电压存储天数
|
||||
*/
|
||||
@JsonProperty("high_voltage_storage_days")
|
||||
private Integer highVoltageStorageDays;
|
||||
}
|
||||
|
|
@ -0,0 +1,167 @@
|
|||
package com.ruoyi.device.domain.impl.djimqtt.model.drone;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 飞行器相机信息
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class CameraInfo {
|
||||
|
||||
/**
|
||||
* 剩余拍照张数
|
||||
*/
|
||||
@JsonProperty("remain_photo_num")
|
||||
private Integer remainPhotoNum;
|
||||
|
||||
/**
|
||||
* 剩余录像时间(秒)
|
||||
*/
|
||||
@JsonProperty("remain_record_duration")
|
||||
private Integer remainRecordDuration;
|
||||
|
||||
/**
|
||||
* 视频录制时长(秒)
|
||||
*/
|
||||
@JsonProperty("record_time")
|
||||
private Integer recordTime;
|
||||
|
||||
/**
|
||||
* 负载编号
|
||||
*/
|
||||
@JsonProperty("payload_index")
|
||||
private String payloadIndex;
|
||||
|
||||
/**
|
||||
* 相机模式:0-拍照,1-录像,2-智能低光,3-全景拍照
|
||||
*/
|
||||
@JsonProperty("camera_mode")
|
||||
private Integer cameraMode;
|
||||
|
||||
/**
|
||||
* 拍照状态:0-空闲,1-拍照中
|
||||
*/
|
||||
@JsonProperty("photo_state")
|
||||
private Integer photoState;
|
||||
|
||||
/**
|
||||
* 录像状态:0-空闲,1-录像中
|
||||
*/
|
||||
@JsonProperty("recording_state")
|
||||
private Integer recordingState;
|
||||
|
||||
/**
|
||||
* 变焦倍数
|
||||
*/
|
||||
@JsonProperty("zoom_factor")
|
||||
private Float zoomFactor;
|
||||
|
||||
/**
|
||||
* 红外变焦倍数
|
||||
*/
|
||||
@JsonProperty("ir_zoom_factor")
|
||||
private Float irZoomFactor;
|
||||
|
||||
/**
|
||||
* 视场角(FOV)在 liveview 中的区域
|
||||
*/
|
||||
@JsonProperty("liveview_world_region")
|
||||
private LiveviewWorldRegion liveviewWorldRegion;
|
||||
|
||||
/**
|
||||
* 照片存储设置集合
|
||||
*/
|
||||
@JsonProperty("photo_storage_settings")
|
||||
private List<String> photoStorageSettings;
|
||||
|
||||
/**
|
||||
* 视频存储设置集合
|
||||
*/
|
||||
@JsonProperty("video_storage_settings")
|
||||
private List<String> videoStorageSettings;
|
||||
|
||||
/**
|
||||
* 广角镜头曝光模式
|
||||
*/
|
||||
@JsonProperty("wide_exposure_mode")
|
||||
private Integer wideExposureMode;
|
||||
|
||||
/**
|
||||
* 广角镜头感光度
|
||||
*/
|
||||
@JsonProperty("wide_iso")
|
||||
private Integer wideIso;
|
||||
|
||||
/**
|
||||
* 广角镜头快门速度
|
||||
*/
|
||||
@JsonProperty("wide_shutter_speed")
|
||||
private Integer wideShutterSpeed;
|
||||
|
||||
/**
|
||||
* 广角镜头曝光值
|
||||
*/
|
||||
@JsonProperty("wide_exposure_value")
|
||||
private Integer wideExposureValue;
|
||||
|
||||
/**
|
||||
* 变焦镜头曝光模式
|
||||
*/
|
||||
@JsonProperty("zoom_exposure_mode")
|
||||
private Integer zoomExposureMode;
|
||||
|
||||
/**
|
||||
* 变焦镜头感光度
|
||||
*/
|
||||
@JsonProperty("zoom_iso")
|
||||
private Integer zoomIso;
|
||||
|
||||
/**
|
||||
* 变焦镜头快门速度
|
||||
*/
|
||||
@JsonProperty("zoom_shutter_speed")
|
||||
private Integer zoomShutterSpeed;
|
||||
|
||||
/**
|
||||
* 变焦镜头曝光值
|
||||
*/
|
||||
@JsonProperty("zoom_exposure_value")
|
||||
private Integer zoomExposureValue;
|
||||
|
||||
/**
|
||||
* 变焦镜头对焦模式
|
||||
*/
|
||||
@JsonProperty("zoom_focus_mode")
|
||||
private Integer zoomFocusMode;
|
||||
|
||||
/**
|
||||
* 变焦镜头对焦值
|
||||
*/
|
||||
@JsonProperty("zoom_focus_value")
|
||||
private Integer zoomFocusValue;
|
||||
|
||||
/**
|
||||
* 红外测温模式
|
||||
*/
|
||||
@JsonProperty("ir_metering_mode")
|
||||
private Integer irMeteringMode;
|
||||
|
||||
/**
|
||||
* 红外测温点
|
||||
*/
|
||||
@JsonProperty("ir_metering_point")
|
||||
private IrMeteringPoint irMeteringPoint;
|
||||
|
||||
/**
|
||||
* 红外测温区域
|
||||
*/
|
||||
@JsonProperty("ir_metering_area")
|
||||
private IrMeteringArea irMeteringArea;
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.ruoyi.device.domain.impl.djimqtt.model.drone;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 飞行器对频信息(中心节点)
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class CenterNode {
|
||||
|
||||
/**
|
||||
* 扰码信息
|
||||
*/
|
||||
@JsonProperty("sdr_id")
|
||||
private Integer sdrId;
|
||||
|
||||
/**
|
||||
* 设备sn
|
||||
*/
|
||||
@JsonProperty("sn")
|
||||
private String sn;
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.ruoyi.device.domain.impl.djimqtt.model.drone;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 飞行器限远状态
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class DistanceLimitStatus {
|
||||
|
||||
/**
|
||||
* 是否开启限远:0-未设置,1-已设置
|
||||
*/
|
||||
@JsonProperty("state")
|
||||
private Integer state;
|
||||
|
||||
/**
|
||||
* 限远距离(米)
|
||||
*/
|
||||
@JsonProperty("distance_limit")
|
||||
private Integer distanceLimit;
|
||||
|
||||
/**
|
||||
* 是否接近设定的限制距离:0-未达到,1-接近
|
||||
*/
|
||||
@JsonProperty("is_near_distance_limit")
|
||||
private Integer isNearDistanceLimit;
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
package com.ruoyi.device.domain.impl.djimqtt.model.drone;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 飞行器电池信息
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class DroneBatteryInfo {
|
||||
|
||||
/**
|
||||
* 电池的总剩余电量
|
||||
*/
|
||||
@JsonProperty("capacity_percent")
|
||||
private Integer capacityPercent;
|
||||
|
||||
/**
|
||||
* 剩余飞行时间(秒)
|
||||
*/
|
||||
@JsonProperty("remain_flight_time")
|
||||
private Integer remainFlightTime;
|
||||
|
||||
/**
|
||||
* 返航所需电量百分比
|
||||
*/
|
||||
@JsonProperty("return_home_power")
|
||||
private Integer returnHomePower;
|
||||
|
||||
/**
|
||||
* 强制降落电量百分比
|
||||
*/
|
||||
@JsonProperty("landing_power")
|
||||
private Integer landingPower;
|
||||
|
||||
/**
|
||||
* 电池详细信息
|
||||
*/
|
||||
@JsonProperty("batteries")
|
||||
private List<BatteryDetail> batteries;
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
package com.ruoyi.device.domain.impl.djimqtt.model.drone;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 红外测温区域
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class IrMeteringArea {
|
||||
|
||||
/**
|
||||
* 测温区域左上角点坐标 x
|
||||
*/
|
||||
@JsonProperty("x")
|
||||
private Double x;
|
||||
|
||||
/**
|
||||
* 测温区域左上角点坐标 y
|
||||
*/
|
||||
@JsonProperty("y")
|
||||
private Double y;
|
||||
|
||||
/**
|
||||
* 测温区域宽度
|
||||
*/
|
||||
@JsonProperty("width")
|
||||
private Double width;
|
||||
|
||||
/**
|
||||
* 测温区域高度
|
||||
*/
|
||||
@JsonProperty("height")
|
||||
private Double height;
|
||||
|
||||
/**
|
||||
* 测温区域平均温度
|
||||
*/
|
||||
@JsonProperty("aver_temperature")
|
||||
private Double averTemperature;
|
||||
|
||||
/**
|
||||
* 测温区域最低温度点
|
||||
*/
|
||||
@JsonProperty("min_temperature_point")
|
||||
private TemperaturePoint minTemperaturePoint;
|
||||
|
||||
/**
|
||||
* 测温区域最高温度点
|
||||
*/
|
||||
@JsonProperty("max_temperature_point")
|
||||
private TemperaturePoint maxTemperaturePoint;
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.ruoyi.device.domain.impl.djimqtt.model.drone;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 红外测温点
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class IrMeteringPoint {
|
||||
|
||||
/**
|
||||
* 测温点坐标 x
|
||||
*/
|
||||
@JsonProperty("x")
|
||||
private Double x;
|
||||
|
||||
/**
|
||||
* 测温点坐标 y
|
||||
*/
|
||||
@JsonProperty("y")
|
||||
private Double y;
|
||||
|
||||
/**
|
||||
* 测温点的温度
|
||||
*/
|
||||
@JsonProperty("temperature")
|
||||
private Double temperature;
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.ruoyi.device.domain.impl.djimqtt.model.drone;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 机场或遥控器对频信息(叶子节点)
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class LeafNode {
|
||||
|
||||
/**
|
||||
* 扰码信息
|
||||
*/
|
||||
@JsonProperty("sdr_id")
|
||||
private Integer sdrId;
|
||||
|
||||
/**
|
||||
* 设备sn
|
||||
*/
|
||||
@JsonProperty("sn")
|
||||
private String sn;
|
||||
|
||||
/**
|
||||
* 控制源序号
|
||||
*/
|
||||
@JsonProperty("control_source_index")
|
||||
private Integer controlSourceIndex;
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package com.ruoyi.device.domain.impl.djimqtt.model.drone;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 视场角(FOV)在 liveview 中的区域
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class LiveviewWorldRegion {
|
||||
|
||||
/**
|
||||
* 左上角的 x 轴起始点
|
||||
*/
|
||||
@JsonProperty("left")
|
||||
private Float left;
|
||||
|
||||
/**
|
||||
* 左上角的 y 轴起始点
|
||||
*/
|
||||
@JsonProperty("top")
|
||||
private Float top;
|
||||
|
||||
/**
|
||||
* 右下角的 x 轴起始点
|
||||
*/
|
||||
@JsonProperty("right")
|
||||
private Float right;
|
||||
|
||||
/**
|
||||
* 右下角的 y 轴起始点
|
||||
*/
|
||||
@JsonProperty("bottom")
|
||||
private Float bottom;
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package com.ruoyi.device.domain.impl.djimqtt.model.drone;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 保养信息
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class MaintainStatus {
|
||||
|
||||
/**
|
||||
* 保养信息数组
|
||||
*/
|
||||
@JsonProperty("maintain_status_array")
|
||||
private List<MaintainStatusItem> maintainStatusArray;
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
package com.ruoyi.device.domain.impl.djimqtt.model.drone;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 保养信息项
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class MaintainStatusItem {
|
||||
|
||||
/**
|
||||
* 保养状态:0-无保养,1-有保养
|
||||
*/
|
||||
@JsonProperty("state")
|
||||
private Integer state;
|
||||
|
||||
/**
|
||||
* 上一次保养类型:1-飞行器基础保养,2-飞行器常规保养,3-飞行器深度保养
|
||||
*/
|
||||
@JsonProperty("last_maintain_type")
|
||||
private Integer lastMaintainType;
|
||||
|
||||
/**
|
||||
* 上一次保养时间
|
||||
*/
|
||||
@JsonProperty("last_maintain_time")
|
||||
private Long lastMaintainTime;
|
||||
|
||||
/**
|
||||
* 上一次保养时飞行航时(小时)
|
||||
*/
|
||||
@JsonProperty("last_maintain_flight_time")
|
||||
private Integer lastMaintainFlightTime;
|
||||
|
||||
/**
|
||||
* 上一次保养时飞行架次
|
||||
*/
|
||||
@JsonProperty("last_maintain_flight_sorties")
|
||||
private Integer lastMaintainFlightSorties;
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.ruoyi.device.domain.impl.djimqtt.model.drone;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 飞行器避障状态
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class ObstacleAvoidance {
|
||||
|
||||
/**
|
||||
* 水平避障状态:0-关闭,1-开启
|
||||
*/
|
||||
@JsonProperty("horizon")
|
||||
private Integer horizon;
|
||||
|
||||
/**
|
||||
* 上视避障状态:0-关闭,1-开启
|
||||
*/
|
||||
@JsonProperty("upside")
|
||||
private Integer upside;
|
||||
|
||||
/**
|
||||
* 下视避障状态:0-关闭,1-开启
|
||||
*/
|
||||
@JsonProperty("downside")
|
||||
private Integer downside;
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package com.ruoyi.device.domain.impl.djimqtt.model.drone;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 搜星状态
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class PositionState {
|
||||
|
||||
/**
|
||||
* 是否收敛:0-未开始,1-收敛中,2-收敛成功,3-收敛失败
|
||||
*/
|
||||
@JsonProperty("is_fixed")
|
||||
private Integer isFixed;
|
||||
|
||||
/**
|
||||
* 搜星档位:1-1档,2-2档,3-3档,4-4档,5-5档,10-RTK fixed
|
||||
*/
|
||||
@JsonProperty("quality")
|
||||
private Integer quality;
|
||||
|
||||
/**
|
||||
* GPS 搜星数量
|
||||
*/
|
||||
@JsonProperty("gps_number")
|
||||
private Integer gpsNumber;
|
||||
|
||||
/**
|
||||
* RTK 搜星数量
|
||||
*/
|
||||
@JsonProperty("rtk_number")
|
||||
private Integer rtkNumber;
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.ruoyi.device.domain.impl.djimqtt.model.drone;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 存储容量
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class StorageInfo {
|
||||
|
||||
/**
|
||||
* 总容量(KB)
|
||||
*/
|
||||
@JsonProperty("total")
|
||||
private Integer total;
|
||||
|
||||
/**
|
||||
* 已使用容量(KB)
|
||||
*/
|
||||
@JsonProperty("used")
|
||||
private Integer used;
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.ruoyi.device.domain.impl.djimqtt.model.drone;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 温度点
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class TemperaturePoint {
|
||||
|
||||
/**
|
||||
* 温度点坐标 x
|
||||
*/
|
||||
@JsonProperty("x")
|
||||
private Double x;
|
||||
|
||||
/**
|
||||
* 温度点坐标 y
|
||||
*/
|
||||
@JsonProperty("y")
|
||||
private Double y;
|
||||
|
||||
/**
|
||||
* 温度点的温度
|
||||
*/
|
||||
@JsonProperty("temperature")
|
||||
private Double temperature;
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package com.ruoyi.device.domain.impl.djimqtt.model.drone;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 图传连接拓扑
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class WirelessLinkTopo {
|
||||
|
||||
/**
|
||||
* 加密编码
|
||||
*/
|
||||
@JsonProperty("secret_code")
|
||||
private List<Integer> secretCode;
|
||||
|
||||
/**
|
||||
* 飞行器对频信息
|
||||
*/
|
||||
@JsonProperty("center_node")
|
||||
private CenterNode centerNode;
|
||||
|
||||
/**
|
||||
* 当前连接的机场或遥控器对频信息
|
||||
*/
|
||||
@JsonProperty("leaf_nodes")
|
||||
private List<LeafNode> leafNodes;
|
||||
}
|
||||
|
|
@ -0,0 +1,190 @@
|
|||
package com.ruoyi.device.domain.impl.djimqtt.service;
|
||||
|
||||
import com.ruoyi.device.domain.impl.djimqtt.config.DjiMqttClientConfig;
|
||||
import com.ruoyi.device.domain.impl.djimqtt.handler.DjiMqttMessageHandler;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.paho.mqttv5.client.IMqttToken;
|
||||
import org.eclipse.paho.mqttv5.client.MqttCallback;
|
||||
import org.eclipse.paho.mqttv5.client.MqttClient;
|
||||
import org.eclipse.paho.mqttv5.client.MqttConnectionOptions;
|
||||
import org.eclipse.paho.mqttv5.client.MqttDisconnectResponse;
|
||||
import org.eclipse.paho.mqttv5.client.persist.MemoryPersistence;
|
||||
import org.eclipse.paho.mqttv5.common.MqttException;
|
||||
import org.eclipse.paho.mqttv5.common.MqttMessage;
|
||||
import org.eclipse.paho.mqttv5.common.packet.MqttProperties;
|
||||
|
||||
/**
|
||||
* DJI MQTT客户端服务(基于Eclipse Paho MQTT v5)
|
||||
* 支持MQTT 5.0协议,动态创建多个客户端
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Slf4j
|
||||
public class DjiMqttClientService {
|
||||
|
||||
private final DjiMqttClientConfig config;
|
||||
private final DjiMqttMessageHandler messageHandler;
|
||||
private MqttClient mqttClient;
|
||||
|
||||
/**
|
||||
* 无人机OSD主题
|
||||
*/
|
||||
private static final String DRONE_OSD_TOPIC = "thing/product/+/osd";
|
||||
|
||||
/**
|
||||
* 无人机State主题
|
||||
*/
|
||||
private static final String DRONE_STATE_TOPIC = "thing/product/+/state";
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param config 客户端配置
|
||||
* @param messageHandler 消息处理器
|
||||
*/
|
||||
public DjiMqttClientService(DjiMqttClientConfig config, DjiMqttMessageHandler messageHandler) {
|
||||
this.config = config;
|
||||
this.messageHandler = messageHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接到MQTT服务器
|
||||
*/
|
||||
public void connect() {
|
||||
try {
|
||||
if (mqttClient != null && mqttClient.isConnected()) {
|
||||
log.info("MQTT客户端[{}]已连接,无需重复连接", config.getClientId());
|
||||
return;
|
||||
}
|
||||
|
||||
String broker = String.format("tcp://%s:%d", config.getHost(), config.getPort());
|
||||
log.info("开始连接DJI MQTT服务器[{}]: {}", config.getClientId(), broker);
|
||||
|
||||
mqttClient = new MqttClient(broker, config.getClientId(), new MemoryPersistence());
|
||||
|
||||
MqttConnectionOptions options = new MqttConnectionOptions();
|
||||
options.setUserName(config.getUsername());
|
||||
options.setPassword(config.getPassword().getBytes());
|
||||
options.setConnectionTimeout(config.getConnectionTimeout());
|
||||
options.setKeepAliveInterval(config.getKeepAliveInterval());
|
||||
options.setAutomaticReconnect(config.getAutoReconnect());
|
||||
options.setCleanStart(config.getCleanSession());
|
||||
|
||||
mqttClient.setCallback(new MqttCallback() {
|
||||
@Override
|
||||
public void disconnected(MqttDisconnectResponse disconnectResponse) {
|
||||
log.error("MQTT客户端[{}]连接丢失: {}", config.getClientId(),
|
||||
disconnectResponse.getReasonString());
|
||||
|
||||
if (config.getAutoReconnect()) {
|
||||
log.info("MQTT客户端[{}]将自动重连...", config.getClientId());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mqttErrorOccurred(MqttException exception) {
|
||||
log.error("MQTT客户端[{}]发生错误: {}", config.getClientId(),
|
||||
exception.getMessage(), exception);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void messageArrived(String topic, MqttMessage message) {
|
||||
try {
|
||||
String payload = new String(message.getPayload());
|
||||
messageHandler.handleMessage(topic, payload);
|
||||
} catch (Exception e) {
|
||||
log.error("MQTT客户端[{}]处理消息失败: {}", config.getClientId(),
|
||||
e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deliveryComplete(IMqttToken token) {
|
||||
// 不需要处理
|
||||
}
|
||||
|
||||
@Override
|
||||
public void connectComplete(boolean reconnect, String serverURI) {
|
||||
if (reconnect) {
|
||||
log.info("MQTT客户端[{}]重连成功: {}", config.getClientId(), serverURI);
|
||||
// 重连后重新订阅
|
||||
subscribe();
|
||||
} else {
|
||||
log.info("MQTT客户端[{}]首次连接成功: {}", config.getClientId(), serverURI);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void authPacketArrived(int reasonCode, MqttProperties properties) {
|
||||
// 不需要处理
|
||||
}
|
||||
});
|
||||
|
||||
mqttClient.connect(options);
|
||||
log.info("MQTT客户端[{}]成功连接到服务器", config.getClientId());
|
||||
|
||||
subscribe();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("MQTT客户端[{}]连接失败: {}", config.getClientId(), e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 订阅主题
|
||||
*/
|
||||
private void subscribe() {
|
||||
try {
|
||||
if (mqttClient == null || !mqttClient.isConnected()) {
|
||||
log.warn("MQTT客户端[{}]未连接,无法订阅主题", config.getClientId());
|
||||
return;
|
||||
}
|
||||
|
||||
String osdTopic = config.getUseSharedSubscription()
|
||||
? String.format("$share/%s/%s", config.getSharedGroupName(), DRONE_OSD_TOPIC)
|
||||
: DRONE_OSD_TOPIC;
|
||||
|
||||
String stateTopic = config.getUseSharedSubscription()
|
||||
? String.format("$share/%s/%s", config.getSharedGroupName(), DRONE_STATE_TOPIC)
|
||||
: DRONE_STATE_TOPIC;
|
||||
|
||||
mqttClient.subscribe(osdTopic, 1);
|
||||
log.info("MQTT客户端[{}]成功订阅主题: {}", config.getClientId(), osdTopic);
|
||||
|
||||
mqttClient.subscribe(stateTopic, 1);
|
||||
log.info("MQTT客户端[{}]成功订阅主题: {}", config.getClientId(), stateTopic);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("MQTT客户端[{}]订阅主题失败: {}", config.getClientId(), e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 断开连接
|
||||
*/
|
||||
public void disconnect() {
|
||||
try {
|
||||
if (mqttClient != null && mqttClient.isConnected()) {
|
||||
mqttClient.disconnect();
|
||||
mqttClient.close();
|
||||
log.info("MQTT客户端[{}]已断开连接", config.getClientId());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("MQTT客户端[{}]断开连接失败: {}", config.getClientId(), e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查连接状态
|
||||
*/
|
||||
public boolean isConnected() {
|
||||
return mqttClient != null && mqttClient.isConnected();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取客户端ID
|
||||
*/
|
||||
public String getClientId() {
|
||||
return config.getClientId();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,272 @@
|
|||
package com.ruoyi.device.domain.impl.machine;
|
||||
|
||||
|
||||
import com.ruoyi.device.domain.impl.machine.command.*;
|
||||
import com.ruoyi.device.domain.impl.machine.instruction.InstructionContext;
|
||||
import com.ruoyi.device.domain.impl.machine.mqtt.MqttClient;
|
||||
import com.ruoyi.device.domain.impl.machine.state.MachineStates;
|
||||
import com.ruoyi.device.domain.impl.machine.statemachine.MachineStateManager;
|
||||
import com.ruoyi.device.domain.impl.machine.statemachine.StateChangeListener;
|
||||
import com.ruoyi.device.domain.impl.machine.vendor.VendorConfig;
|
||||
import com.ruoyi.device.domain.impl.machine.vendor.VendorRegistry;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 设备命令管理器(框架使用者的主要入口)
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class MachineCommandManager {
|
||||
|
||||
private final VendorRegistry vendorRegistry;
|
||||
private final MachineStateManager stateManager;
|
||||
private final TransactionExecutor transactionExecutor;
|
||||
private final MqttClient mqttClient;
|
||||
|
||||
/**
|
||||
* SN -> 当前正在执行的命令
|
||||
*/
|
||||
private final Map<String, CommandExecution> executingCommands = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 命令执行监听器
|
||||
*/
|
||||
private final Map<String, CommandExecutionListener> commandListeners = new ConcurrentHashMap<>();
|
||||
|
||||
public MachineCommandManager(VendorRegistry vendorRegistry,
|
||||
MachineStateManager stateManager,
|
||||
TransactionExecutor transactionExecutor,
|
||||
MqttClient mqttClient) {
|
||||
this.vendorRegistry = vendorRegistry;
|
||||
this.stateManager = stateManager;
|
||||
this.transactionExecutor = transactionExecutor;
|
||||
this.mqttClient = mqttClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取设备当前状态
|
||||
*
|
||||
* @param sn 设备SN号
|
||||
* @return 设备状态
|
||||
*/
|
||||
public MachineStates getMachineStates(String sn) {
|
||||
return stateManager.getStates(sn);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新设备状态(通常在心跳中调用)
|
||||
*
|
||||
* @param sn 设备SN号
|
||||
* @param newStates 新状态
|
||||
*/
|
||||
public void updateMachineStates(String sn, MachineStates newStates,Boolean force) {
|
||||
stateManager.updateStates(sn, newStates,force);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断设备是否正在执行命令
|
||||
*
|
||||
* @param sn 设备SN号
|
||||
* @return 是否正在执行命令
|
||||
*/
|
||||
public boolean isExecutingCommand(String sn) {
|
||||
CommandExecution execution = executingCommands.get(sn);
|
||||
return execution != null && !execution.getFuture().isDone();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取设备当前正在执行的命令类型
|
||||
*
|
||||
* @param sn 设备SN号
|
||||
* @return 命令类型,如果没有正在执行的命令则返回null
|
||||
*/
|
||||
public CommandType getExecutingCommandType(String sn) {
|
||||
CommandExecution execution = executingCommands.get(sn);
|
||||
if (execution != null && !execution.getFuture().isDone()) {
|
||||
return execution.getCommandType();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取设备在当前状态下可以执行的命令列表
|
||||
*
|
||||
* @param sn 设备SN号
|
||||
* @return 可执行的命令列表
|
||||
*/
|
||||
public List<CommandType> getAvailableCommands(String sn) {
|
||||
VendorConfig vendorConfig = vendorRegistry.getVendorConfig(sn);
|
||||
if (vendorConfig == null) {
|
||||
log.warn("设备未绑定厂家: sn={}", sn);
|
||||
return List.of();
|
||||
}
|
||||
|
||||
MachineStates currentStates = stateManager.getStates(sn);
|
||||
return vendorConfig.getAvailableCommands(currentStates);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行命令
|
||||
*
|
||||
* @param sn 设备SN号
|
||||
* @param commandType 命令类型
|
||||
* @return 命令执行结果的Future
|
||||
*/
|
||||
public CompletableFuture<CommandResult> executeCommand(String sn, CommandType commandType) {
|
||||
return executeCommand(sn, commandType, Map.of());
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行命令(带参数)
|
||||
*
|
||||
* @param sn 设备SN号
|
||||
* @param commandType 命令类型
|
||||
* @param params 命令参数
|
||||
* @return 命令执行结果的Future
|
||||
*/
|
||||
public CompletableFuture<CommandResult> executeCommand(String sn, CommandType commandType, Map<String, Object> params) {
|
||||
log.info("收到命令执行请求: sn={}, commandType={}, params={}", sn, commandType, params);
|
||||
|
||||
// 1. 检查设备是否已绑定厂家
|
||||
VendorConfig vendorConfig = vendorRegistry.getVendorConfig(sn);
|
||||
if (vendorConfig == null) {
|
||||
String error = "设备未绑定厂家";
|
||||
log.error("{}: sn={}", error, sn);
|
||||
return CompletableFuture.completedFuture(CommandResult.failure(commandType, error));
|
||||
}
|
||||
|
||||
// 2. 检查是否正在执行其他命令
|
||||
if (isExecutingCommand(sn)) {
|
||||
String error = "设备正在执行其他命令: " + getExecutingCommandType(sn);
|
||||
log.warn("{}: sn={}", error, sn);
|
||||
return CompletableFuture.completedFuture(CommandResult.failure(commandType, error));
|
||||
}
|
||||
|
||||
// 3. 检查当前状态是否可以执行该命令
|
||||
MachineStates currentStates = stateManager.getStates(sn);
|
||||
if (!vendorConfig.canExecuteCommand(currentStates, commandType)) {
|
||||
String error = "当前状态不允许执行该命令";
|
||||
log.warn("{}: sn={}, commandType={}, currentStates={}", error, sn, commandType, currentStates);
|
||||
return CompletableFuture.completedFuture(CommandResult.failure(commandType, error));
|
||||
}
|
||||
|
||||
// 4. 获取事务定义
|
||||
Transaction transaction = vendorConfig.getTransaction(commandType);
|
||||
if (transaction == null) {
|
||||
String error = "厂家不支持该命令";
|
||||
log.error("{}: sn={}, commandType={}, vendorType={}", error, sn, commandType, vendorConfig.getVendorType());
|
||||
return CompletableFuture.completedFuture(CommandResult.failure(commandType, error));
|
||||
}
|
||||
|
||||
// 5. 创建指令上下文
|
||||
InstructionContext context = new InstructionContext(sn, vendorConfig.getVendorType(), mqttClient);
|
||||
params.forEach(context::putCommandParam);
|
||||
|
||||
// 6. 执行事务
|
||||
CompletableFuture<CommandResult> future = transactionExecutor.executeTransaction(transaction, context);
|
||||
|
||||
// 7. 记录正在执行的命令
|
||||
executingCommands.put(sn, new CommandExecution(commandType, future, System.currentTimeMillis()));
|
||||
|
||||
// 8. 添加完成回调
|
||||
future.whenComplete((result, throwable) -> {
|
||||
executingCommands.remove(sn);
|
||||
|
||||
if (throwable != null) {
|
||||
log.error("命令执行异常: sn={}, commandType={}", sn, commandType, throwable);
|
||||
notifyCommandComplete(sn, CommandResult.failure(commandType, "命令执行异常: " + throwable.getMessage()));
|
||||
} else {
|
||||
log.info("命令执行完成: sn={}, commandType={}, success={}", sn, commandType, result.isSuccess());
|
||||
notifyCommandComplete(sn, result);
|
||||
}
|
||||
});
|
||||
|
||||
return future;
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册命令执行监听器
|
||||
*
|
||||
* @param listenerId 监听器ID
|
||||
* @param listener 监听器
|
||||
*/
|
||||
public void registerCommandListener(String listenerId, CommandExecutionListener listener) {
|
||||
commandListeners.put(listenerId, listener);
|
||||
log.debug("注册命令执行监听器: listenerId={}", listenerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消注册命令执行监听器
|
||||
*
|
||||
* @param listenerId 监听器ID
|
||||
*/
|
||||
public void unregisterCommandListener(String listenerId) {
|
||||
commandListeners.remove(listenerId);
|
||||
log.debug("取消注册命令执行监听器: listenerId={}", listenerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册状态变化监听器
|
||||
*
|
||||
* @param listenerId 监听器ID
|
||||
* @param listener 监听器
|
||||
*/
|
||||
public void registerStateChangeListener(String listenerId, StateChangeListener listener) {
|
||||
stateManager.registerStateChangeListener(listenerId, listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消注册状态变化监听器
|
||||
*
|
||||
* @param listenerId 监听器ID
|
||||
*/
|
||||
public void unregisterStateChangeListener(String listenerId) {
|
||||
stateManager.unregisterStateChangeListener(listenerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知命令执行完成
|
||||
*/
|
||||
private void notifyCommandComplete(String sn, CommandResult result) {
|
||||
for (CommandExecutionListener listener : commandListeners.values()) {
|
||||
try {
|
||||
listener.onCommandComplete(sn, result);
|
||||
} catch (Exception e) {
|
||||
log.error("命令执行监听器执行失败: sn={}, commandType={}", sn, result.getCommandType(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 命令执行信息
|
||||
*/
|
||||
private static class CommandExecution {
|
||||
private final CommandType commandType;
|
||||
private final CompletableFuture<CommandResult> future;
|
||||
private final long startTime;
|
||||
|
||||
public CommandExecution(CommandType commandType, CompletableFuture<CommandResult> future, long startTime) {
|
||||
this.commandType = commandType;
|
||||
this.future = future;
|
||||
this.startTime = startTime;
|
||||
}
|
||||
|
||||
public CommandType getCommandType() {
|
||||
return commandType;
|
||||
}
|
||||
|
||||
public CompletableFuture<CommandResult> getFuture() {
|
||||
return future;
|
||||
}
|
||||
|
||||
public long getStartTime() {
|
||||
return startTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.ruoyi.device.domain.impl.machine.command;
|
||||
|
||||
/**
|
||||
* 命令执行监听器
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface CommandExecutionListener {
|
||||
/**
|
||||
* 命令执行完成回调
|
||||
*
|
||||
* @param sn 设备SN号
|
||||
* @param result 命令执行结果
|
||||
*/
|
||||
void onCommandComplete(String sn, CommandResult result);
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
package com.ruoyi.device.domain.impl.machine.command;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 命令执行结果
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class CommandResult {
|
||||
/**
|
||||
* 命令类型
|
||||
*/
|
||||
private CommandType commandType;
|
||||
|
||||
/**
|
||||
* 是否成功
|
||||
*/
|
||||
private boolean success;
|
||||
|
||||
/**
|
||||
* 错误信息
|
||||
*/
|
||||
private String errorMessage;
|
||||
|
||||
/**
|
||||
* 失败的指令名称
|
||||
*/
|
||||
private String failedInstructionName;
|
||||
|
||||
/**
|
||||
* 结果数据
|
||||
*/
|
||||
private Object data;
|
||||
|
||||
public static CommandResult success(CommandType commandType) {
|
||||
return new CommandResult(commandType, true, null, null, null);
|
||||
}
|
||||
|
||||
public static CommandResult success(CommandType commandType, Object data) {
|
||||
return new CommandResult(commandType, true, null, null, data);
|
||||
}
|
||||
|
||||
public static CommandResult failure(CommandType commandType, String errorMessage) {
|
||||
return new CommandResult(commandType, false, errorMessage, null, null);
|
||||
}
|
||||
|
||||
public static CommandResult failure(CommandType commandType, String errorMessage, String failedInstructionName) {
|
||||
return new CommandResult(commandType, false, errorMessage, failedInstructionName, null);
|
||||
}
|
||||
|
||||
public static CommandResult timeout(CommandType commandType) {
|
||||
return new CommandResult(commandType, false, "命令执行超时", null, null);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
package com.ruoyi.device.domain.impl.machine.command;
|
||||
|
||||
/**
|
||||
* 命令类型枚举
|
||||
*/
|
||||
public enum CommandType {
|
||||
/**
|
||||
* 起飞
|
||||
*/
|
||||
TAKE_OFF,
|
||||
|
||||
/**
|
||||
* 返航
|
||||
*/
|
||||
RETURN_HOME,
|
||||
|
||||
/**
|
||||
* 急停
|
||||
*/
|
||||
EMERGENCY_STOP,
|
||||
|
||||
/**
|
||||
* 继续飞行
|
||||
*/
|
||||
RESUME_FLIGHT,
|
||||
|
||||
/**
|
||||
* 指点飞行
|
||||
*/
|
||||
POINT_FLY,
|
||||
|
||||
/**
|
||||
* 取消指点
|
||||
*/
|
||||
CANCEL_POINT,
|
||||
|
||||
/**
|
||||
* 开始航线任务
|
||||
*/
|
||||
START_MISSION,
|
||||
|
||||
/**
|
||||
* 暂停航线任务
|
||||
*/
|
||||
PAUSE_MISSION,
|
||||
|
||||
/**
|
||||
* 恢复航线任务
|
||||
*/
|
||||
RESUME_MISSION,
|
||||
|
||||
/**
|
||||
* 打开舱门
|
||||
*/
|
||||
OPEN_COVER,
|
||||
|
||||
/**
|
||||
* 关闭舱门
|
||||
*/
|
||||
CLOSE_COVER,
|
||||
|
||||
/**
|
||||
* 进入调试模式
|
||||
*/
|
||||
ENTER_DEBUG_MODE,
|
||||
|
||||
/**
|
||||
* 退出调试模式
|
||||
*/
|
||||
EXIT_DEBUG_MODE,
|
||||
|
||||
/**
|
||||
* 进入DRC模式
|
||||
*/
|
||||
ENTER_DRC_MODE,
|
||||
|
||||
/**
|
||||
* 退出DRC模式
|
||||
*/
|
||||
EXIT_DRC_MODE,
|
||||
|
||||
/**
|
||||
* 重启机巢
|
||||
*/
|
||||
REBOOT_AIRPORT
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
package com.ruoyi.device.domain.impl.machine.command;
|
||||
|
||||
|
||||
import com.ruoyi.device.domain.impl.machine.instruction.Instruction;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 事务(由多个指令组成的树状结构,支持条件分支)
|
||||
*/
|
||||
@Data
|
||||
public class Transaction {
|
||||
/**
|
||||
* 事务名称
|
||||
*/
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 命令类型
|
||||
*/
|
||||
private CommandType commandType;
|
||||
|
||||
/**
|
||||
* 根指令(事务的起始指令)
|
||||
*/
|
||||
private Instruction rootInstruction;
|
||||
|
||||
/**
|
||||
* 事务超时时间(毫秒)
|
||||
*/
|
||||
private long timeoutMs = 20000; // 默认10秒
|
||||
|
||||
public Transaction(String name, CommandType commandType) {
|
||||
this.name = name;
|
||||
this.commandType = commandType;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置根指令
|
||||
*
|
||||
* @param instruction 根指令
|
||||
* @return Transaction 支持链式调用
|
||||
*/
|
||||
public Transaction root(Instruction instruction) {
|
||||
this.rootInstruction = instruction;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取根指令
|
||||
*/
|
||||
public Instruction getRootInstruction() {
|
||||
return rootInstruction;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置超时时间
|
||||
*/
|
||||
public Transaction setTimeout(long timeoutMs) {
|
||||
this.timeoutMs = timeoutMs;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,313 @@
|
|||
package com.ruoyi.device.domain.impl.machine.command;
|
||||
|
||||
|
||||
import com.ruoyi.device.domain.impl.machine.instruction.CallbackConfig;
|
||||
import com.ruoyi.device.domain.impl.machine.instruction.Instruction;
|
||||
import com.ruoyi.device.domain.impl.machine.instruction.InstructionContext;
|
||||
import com.ruoyi.device.domain.impl.machine.instruction.InstructionResult;
|
||||
import com.ruoyi.device.domain.impl.machine.mqtt.MqttCallbackRegistry;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* 事务执行器(完全异步化版本)
|
||||
*
|
||||
* 设计说明:
|
||||
* 1. 完全异步:不阻塞任何线程,所有操作都通过 CompletableFuture 链式调用
|
||||
* 2. 高并发:可以同时处理数万个命令,不会创建大量线程
|
||||
* 3. 资源高效:线程只在真正需要执行任务时才使用,不会浪费在等待上
|
||||
*
|
||||
* 性能优势:
|
||||
* - 传统方式:10万个命令 = 10万个阻塞线程 = 系统崩溃
|
||||
* - 异步方式:10万个命令 = 200个工作线程 + 10万个 CompletableFuture = 正常运行
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class TransactionExecutor {
|
||||
|
||||
private final MqttCallbackRegistry callbackRegistry;
|
||||
private final Executor commandExecutor;
|
||||
private final ScheduledExecutorService timeoutScheduler;
|
||||
|
||||
public TransactionExecutor(
|
||||
MqttCallbackRegistry callbackRegistry,
|
||||
@Qualifier("commandExecutor") Executor commandExecutor) {
|
||||
this.callbackRegistry = callbackRegistry;
|
||||
this.commandExecutor = commandExecutor;
|
||||
|
||||
// 创建一个专门用于超时检查的调度器(核心线程数较小)
|
||||
this.timeoutScheduler = new ScheduledThreadPoolExecutor(
|
||||
2,
|
||||
r -> {
|
||||
Thread t = new Thread(r, "timeout-scheduler");
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
}
|
||||
);
|
||||
|
||||
log.info("事务执行器初始化完成(完全异步模式)");
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行事务(完全异步)
|
||||
*
|
||||
* @param transaction 事务定义
|
||||
* @param context 执行上下文
|
||||
* @return CompletableFuture,不会阻塞调用线程
|
||||
*/
|
||||
public CompletableFuture<CommandResult> executeTransaction(Transaction transaction, InstructionContext context) {
|
||||
log.info("开始执行事务: transaction={}, sn={}", transaction.getName(), context.getSn());
|
||||
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
// 直接返回异步执行的结果,不创建新线程
|
||||
return executeInstructionTreeAsync(transaction, context, startTime, transaction.getRootInstruction());
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步执行指令树
|
||||
*
|
||||
* @param transaction 事务定义
|
||||
* @param context 执行上下文
|
||||
* @param startTime 事务开始时间
|
||||
* @param currentInstruction 当前要执行的指令
|
||||
* @return CompletableFuture
|
||||
*/
|
||||
private CompletableFuture<CommandResult> executeInstructionTreeAsync(
|
||||
Transaction transaction,
|
||||
InstructionContext context,
|
||||
long startTime,
|
||||
Instruction currentInstruction) {
|
||||
|
||||
// 检查根指令
|
||||
if (currentInstruction == null) {
|
||||
log.error("事务没有根指令: transaction={}", transaction.getName());
|
||||
return CompletableFuture.completedFuture(
|
||||
CommandResult.failure(transaction.getCommandType(), "事务没有根指令")
|
||||
);
|
||||
}
|
||||
|
||||
// 检查事务是否超时
|
||||
if (System.currentTimeMillis() - startTime > transaction.getTimeoutMs()) {
|
||||
log.warn("事务执行超时: transaction={}, sn={}", transaction.getName(), context.getSn());
|
||||
return CompletableFuture.completedFuture(
|
||||
CommandResult.timeout(transaction.getCommandType())
|
||||
);
|
||||
}
|
||||
|
||||
log.debug("执行指令: instruction={}", currentInstruction.getName());
|
||||
|
||||
// 异步执行当前指令
|
||||
Instruction finalCurrentInstruction = currentInstruction;
|
||||
return executeInstructionAsync(currentInstruction, context)
|
||||
.thenCompose(result -> {
|
||||
// 根据执行结果获取下游指令
|
||||
Instruction nextInstruction = finalCurrentInstruction.getNextInstruction(result.isSuccess());
|
||||
|
||||
if (nextInstruction != null) {
|
||||
// 有下游指令,递归执行
|
||||
log.debug("根据执行结果选择下游指令: success={}, nextInstruction={}",
|
||||
result.isSuccess(), nextInstruction.getName());
|
||||
return executeInstructionTreeAsync(transaction, context, startTime, nextInstruction);
|
||||
} else {
|
||||
// 没有下游指令,当前指令的结果就是事务的结果
|
||||
if (!result.isSuccess()) {
|
||||
log.error("指令执行失败(无下游指令): instruction={}, error={}",
|
||||
finalCurrentInstruction.getName(), result.getErrorMessage());
|
||||
return CompletableFuture.completedFuture(
|
||||
CommandResult.failure(
|
||||
transaction.getCommandType(),
|
||||
result.getErrorMessage(),
|
||||
finalCurrentInstruction.getName()
|
||||
)
|
||||
);
|
||||
} else {
|
||||
log.info("指令执行成功(无下游指令),事务完成: instruction={}, sn={}",
|
||||
finalCurrentInstruction.getName(), context.getSn());
|
||||
return CompletableFuture.completedFuture(
|
||||
CommandResult.success(transaction.getCommandType())
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步执行单个指令
|
||||
*
|
||||
* @param instruction 指令
|
||||
* @param context 执行上下文
|
||||
* @return CompletableFuture<InstructionResult>
|
||||
*/
|
||||
private CompletableFuture<InstructionResult> executeInstructionAsync(
|
||||
Instruction instruction,
|
||||
InstructionContext context) {
|
||||
|
||||
log.debug("开始执行指令: instruction={}, sn={}", instruction.getName(), context.getSn());
|
||||
|
||||
// a. 判断是否可以执行
|
||||
if (!instruction.canExecute(context)) {
|
||||
String error = "指令被拒绝";
|
||||
log.warn("指令不满足执行条件: instruction={}, sn={}", instruction.getName(), context.getSn());
|
||||
InstructionResult result = InstructionResult.failure(error);
|
||||
instruction.onComplete(context, result);
|
||||
return CompletableFuture.completedFuture(result);
|
||||
}
|
||||
|
||||
// b. 在线程池中执行远程调用(避免阻塞当前线程)
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try {
|
||||
instruction.executeRemoteCall(context);
|
||||
log.debug("远程调用已发送: instruction={}", instruction.getName());
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.error("远程调用失败: instruction={}, sn={}", instruction.getName(), context.getSn(), e);
|
||||
return false;
|
||||
}
|
||||
}, commandExecutor).thenCompose(remoteCallSuccess -> {
|
||||
if (!remoteCallSuccess) {
|
||||
InstructionResult result = InstructionResult.failure("远程调用失败");
|
||||
instruction.onComplete(context, result);
|
||||
return CompletableFuture.completedFuture(result);
|
||||
}
|
||||
|
||||
// c. 等待方法回调(异步)
|
||||
CallbackConfig methodCallback = instruction.getMethodCallbackConfig(context);
|
||||
if (methodCallback != null) {
|
||||
// 自动设置为方法回调类型
|
||||
methodCallback.setCallbackType(CallbackConfig.CallbackType.METHOD);
|
||||
|
||||
return waitForCallbackAsync(methodCallback, context)
|
||||
.thenCompose(methodResult -> {
|
||||
if (!methodResult.isSuccess()) {
|
||||
instruction.onComplete(context, methodResult);
|
||||
return CompletableFuture.completedFuture(methodResult);
|
||||
}
|
||||
|
||||
// d. 等待状态回调(异步)
|
||||
CallbackConfig stateCallback = instruction.getStateCallbackConfig(context);
|
||||
if (stateCallback != null) {
|
||||
// 自动设置为状态回调类型
|
||||
stateCallback.setCallbackType(CallbackConfig.CallbackType.STATE);
|
||||
|
||||
return waitForCallbackAsync(stateCallback, context)
|
||||
.thenApply(stateResult -> {
|
||||
instruction.onComplete(context, stateResult);
|
||||
return stateResult;
|
||||
});
|
||||
}
|
||||
|
||||
// 没有状态回调,直接成功
|
||||
InstructionResult result = InstructionResult.success();
|
||||
instruction.onComplete(context, result);
|
||||
return CompletableFuture.completedFuture(result);
|
||||
});
|
||||
}
|
||||
|
||||
// 没有方法回调,检查是否有状态回调
|
||||
CallbackConfig stateCallback = instruction.getStateCallbackConfig(context);
|
||||
if (stateCallback != null) {
|
||||
// 自动设置为状态回调类型
|
||||
stateCallback.setCallbackType(CallbackConfig.CallbackType.STATE);
|
||||
|
||||
return waitForCallbackAsync(stateCallback, context)
|
||||
.thenApply(stateResult -> {
|
||||
instruction.onComplete(context, stateResult);
|
||||
return stateResult;
|
||||
});
|
||||
}
|
||||
|
||||
// 没有任何回调,直接成功
|
||||
InstructionResult result = InstructionResult.success();
|
||||
instruction.onComplete(context, result);
|
||||
return CompletableFuture.completedFuture(result);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步等待回调(不阻塞线程)
|
||||
*
|
||||
* 关键改进:
|
||||
* 1. 不使用 future.get() 阻塞线程
|
||||
* 2. 使用 ScheduledExecutorService 实现超时
|
||||
* 3. 完全基于回调机制
|
||||
*
|
||||
* @param callbackConfig 回调配置
|
||||
* @param context 执行上下文
|
||||
* @return CompletableFuture<InstructionResult>
|
||||
*/
|
||||
private CompletableFuture<InstructionResult> waitForCallbackAsync(
|
||||
CallbackConfig callbackConfig,
|
||||
InstructionContext context) {
|
||||
|
||||
CompletableFuture<InstructionResult> future = new CompletableFuture<>();
|
||||
AtomicBoolean callbackReceived = new AtomicBoolean(false);
|
||||
|
||||
// 注册回调(包含 tid/bid 过滤)
|
||||
String callbackId = callbackRegistry.registerCallback(
|
||||
callbackConfig.getTopic(),
|
||||
messageBody -> {
|
||||
// 判断消息是否匹配
|
||||
boolean matches = callbackConfig.matches(messageBody);
|
||||
if (matches) {
|
||||
// 匹配成功
|
||||
if (callbackReceived.compareAndSet(false, true)) {
|
||||
future.complete(InstructionResult.success(messageBody));
|
||||
log.debug("收到匹配的回调消息: topic={}, type={}",
|
||||
callbackConfig.getTopic(), callbackConfig.getCallbackType());
|
||||
}
|
||||
} else {
|
||||
// 不匹配:根据回调类型决定行为
|
||||
if (callbackConfig.getCallbackType() == CallbackConfig.CallbackType.METHOD) {
|
||||
// 方法回调:不匹配就失败
|
||||
if (callbackReceived.compareAndSet(false, true)) {
|
||||
future.complete(InstructionResult.failure("方法回调不匹配"));
|
||||
log.warn("方法回调不匹配,指令失败: topic={}, expected={}, actual={}",
|
||||
callbackConfig.getTopic(),
|
||||
callbackConfig.getExpectedValue(),
|
||||
messageBody);
|
||||
}
|
||||
} else {
|
||||
// 状态回调:不匹配继续等待
|
||||
// 使用 CAS 确保只处理一次,然后重置状态
|
||||
if (callbackReceived.compareAndSet(false, true)) {
|
||||
callbackReceived.set(false); // 重置状态,继续等待下一条消息
|
||||
log.debug("状态回调不匹配,继续等待: topic={}, expected={}, actual={}",
|
||||
callbackConfig.getTopic(),
|
||||
callbackConfig.getExpectedValue(),
|
||||
messageBody);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
callbackConfig.getTimeoutMs(),
|
||||
callbackConfig.getTidFieldPath(),
|
||||
callbackConfig.getExpectedTid(),
|
||||
callbackConfig.getBidFieldPath(),
|
||||
callbackConfig.getExpectedBid()
|
||||
);
|
||||
|
||||
// 设置超时(不阻塞线程)
|
||||
timeoutScheduler.schedule(() -> {
|
||||
// 使用 CAS 确保只处理一次
|
||||
if (callbackReceived.compareAndSet(false, true)) {
|
||||
future.complete(InstructionResult.timeout());
|
||||
log.warn("等待回调超时: topic={}, timeout={}ms",
|
||||
callbackConfig.getTopic(), callbackConfig.getTimeoutMs());
|
||||
}
|
||||
}, callbackConfig.getTimeoutMs(), TimeUnit.MILLISECONDS);
|
||||
|
||||
// 清理回调(无论成功还是超时)
|
||||
return future.whenComplete((result, throwable) -> {
|
||||
callbackRegistry.unregisterCallback(callbackId);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
package com.ruoyi.device.domain.impl.machine.config;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
|
||||
/**
|
||||
* 线程池配置
|
||||
* 用于命令执行的异步任务
|
||||
*/
|
||||
@Slf4j
|
||||
@Configuration
|
||||
public class ExecutorConfig {
|
||||
|
||||
/**
|
||||
* 命令执行线程池
|
||||
*
|
||||
* 设计说明:
|
||||
* 1. 核心线程数:CPU 核心数 * 2(适合 I/O 密集型任务)
|
||||
* 2. 最大线程数:200(控制最大并发,防止线程爆炸)
|
||||
* 3. 队列容量:10000(缓冲等待执行的任务)
|
||||
* 4. 拒绝策略:CallerRunsPolicy(背压机制,让调用者线程执行)
|
||||
*
|
||||
* 性能预估:
|
||||
* - 假设每个命令平均执行 10 秒
|
||||
* - 200 个线程可以同时处理 200 个命令
|
||||
* - 队列可以缓冲 10000 个命令
|
||||
* - 总容量:10200 个并发命令
|
||||
* - 吞吐量:200 / 10 = 20 个命令/秒
|
||||
*/
|
||||
@Bean(name = "commandExecutor")
|
||||
public Executor commandExecutor() {
|
||||
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||
|
||||
// 核心线程数:根据 CPU 核心数设置
|
||||
int corePoolSize = Runtime.getRuntime().availableProcessors() * 2;
|
||||
executor.setCorePoolSize(corePoolSize);
|
||||
log.info("命令执行线程池核心线程数: {}", corePoolSize);
|
||||
|
||||
// 最大线程数:限制最大并发,防止线程爆炸
|
||||
executor.setMaxPoolSize(200);
|
||||
|
||||
// 队列容量:缓冲等待执行的任务
|
||||
executor.setQueueCapacity(10000);
|
||||
|
||||
// 拒绝策略:队列满时,调用者线程执行(背压机制)
|
||||
// 这样可以防止任务丢失,同时给系统施加背压
|
||||
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
|
||||
|
||||
// 线程名称前缀(方便日志追踪)
|
||||
executor.setThreadNamePrefix("cmd-exec-");
|
||||
|
||||
// 等待任务完成后再关闭(优雅关闭)
|
||||
executor.setWaitForTasksToCompleteOnShutdown(true);
|
||||
executor.setAwaitTerminationSeconds(60);
|
||||
|
||||
// 允许核心线程超时(节省资源)
|
||||
executor.setAllowCoreThreadTimeOut(true);
|
||||
executor.setKeepAliveSeconds(60);
|
||||
|
||||
executor.initialize();
|
||||
|
||||
log.info("命令执行线程池初始化完成: corePoolSize={}, maxPoolSize={}, queueCapacity={}",
|
||||
corePoolSize, 200, 10000);
|
||||
|
||||
return executor;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package com.ruoyi.device.domain.impl.machine.config;
|
||||
|
||||
|
||||
import com.ruoyi.device.domain.impl.machine.vendor.VendorRegistry;
|
||||
import com.ruoyi.device.domain.impl.machine.vendor.dji.DjiVendorConfig;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* 设备框架配置类
|
||||
*/
|
||||
@Slf4j
|
||||
@Configuration
|
||||
public class MachineFrameworkConfig {
|
||||
|
||||
/**
|
||||
* 自动注册所有厂家配置
|
||||
*/
|
||||
@Bean
|
||||
public CommandLineRunner registerVendors(VendorRegistry vendorRegistry, DjiVendorConfig djiVendorConfig) {
|
||||
return args -> {
|
||||
// 注册大疆厂家配置
|
||||
vendorRegistry.registerVendor(djiVendorConfig);
|
||||
log.info("设备框架初始化完成,已注册厂家: {}", vendorRegistry.getAllVendorTypes());
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package com.ruoyi.device.domain.impl.machine.config;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
|
||||
|
||||
/**
|
||||
* Redis 配置类
|
||||
* 用于配置 Redis Pub/Sub 相关的 Bean
|
||||
*/
|
||||
@Configuration
|
||||
@ConditionalOnProperty(name = "machine.state.store.type", havingValue = "redis")
|
||||
public class RedisConfig {
|
||||
|
||||
/**
|
||||
* 创建 Redis 消息监听容器(专用于机器框架的 MQTT 回调)
|
||||
* 用于 Redis Pub/Sub 功能
|
||||
*
|
||||
* 注意:使用特定的 Bean 名称避免与其他模块冲突
|
||||
*/
|
||||
@Bean(name = "machineFrameworkRedisMessageListenerContainer")
|
||||
public RedisMessageListenerContainer machineFrameworkRedisMessageListenerContainer(
|
||||
RedisConnectionFactory connectionFactory) {
|
||||
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
|
||||
container.setConnectionFactory(connectionFactory);
|
||||
return container;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
package com.ruoyi.device.domain.impl.machine.instruction;
|
||||
|
||||
/**
|
||||
* 抽象指令基类,提供默认实现和下游节点管理
|
||||
*/
|
||||
public abstract class AbstractInstruction implements Instruction {
|
||||
|
||||
/**
|
||||
* 成功后执行的下一个指令
|
||||
*/
|
||||
private Instruction onSuccessInstruction;
|
||||
|
||||
/**
|
||||
* 失败后执行的下一个指令
|
||||
*/
|
||||
private Instruction onFailureInstruction;
|
||||
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public Instruction getOnSuccessInstruction() {
|
||||
return onSuccessInstruction;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Instruction getOnFailureInstruction() {
|
||||
return onFailureInstruction;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 设置成功后执行的指令(支持链式调用)
|
||||
*/
|
||||
public <T extends AbstractInstruction> T onSuccess(Instruction instruction) {
|
||||
this.onSuccessInstruction = instruction;
|
||||
return (T) this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置失败后执行的指令(支持链式调用)
|
||||
*/
|
||||
public <T extends AbstractInstruction> T onFailure(Instruction instruction) {
|
||||
this.onFailureInstruction = instruction;
|
||||
return (T) this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置无论成功失败都执行的指令(支持链式调用)
|
||||
*/
|
||||
public <T extends AbstractInstruction> T then(Instruction instruction) {
|
||||
this.onFailureInstruction = instruction;
|
||||
this.onSuccessInstruction = instruction;
|
||||
return (T) this;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
package com.ruoyi.device.domain.impl.machine.instruction;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.function.Predicate;
|
||||
|
||||
/**
|
||||
* 回调配置(用于方法回调和状态回调)
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class CallbackConfig {
|
||||
|
||||
/**
|
||||
* 回调类型枚举
|
||||
*/
|
||||
public enum CallbackType {
|
||||
/**
|
||||
* 方法回调:设备对指令的直接响应
|
||||
* - 收到匹配的响应 → 成功
|
||||
* - 收到不匹配的响应 → 失败(立即)
|
||||
* - 超时 → 失败
|
||||
*/
|
||||
METHOD,
|
||||
|
||||
/**
|
||||
* 状态回调:等待设备状态变化
|
||||
* - 收到匹配的状态 → 成功
|
||||
* - 收到不匹配的状态 → 继续等待
|
||||
* - 超时 → 失败
|
||||
*/
|
||||
STATE
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听的MQTT主题
|
||||
*/
|
||||
private String topic;
|
||||
|
||||
/**
|
||||
* 字段路径(支持嵌套,如 "data.status")
|
||||
*/
|
||||
private String fieldPath;
|
||||
|
||||
/**
|
||||
* 期望的字段值
|
||||
*/
|
||||
private Object expectedValue;
|
||||
|
||||
/**
|
||||
* 自定义判断逻辑(如果设置,则优先使用此逻辑)
|
||||
*/
|
||||
private Predicate<Object> customPredicate;
|
||||
|
||||
/**
|
||||
* 超时时间(毫秒)
|
||||
*/
|
||||
@Builder.Default
|
||||
private long timeoutMs = 10000;
|
||||
|
||||
/**
|
||||
* 事务ID字段路径(用于匹配回调消息,如 "tid")
|
||||
*/
|
||||
private String tidFieldPath;
|
||||
|
||||
/**
|
||||
* 业务ID字段路径(用于匹配回调消息,如 "bid")
|
||||
*/
|
||||
private String bidFieldPath;
|
||||
|
||||
/**
|
||||
* 期望的事务ID值(从InstructionContext中获取)
|
||||
*/
|
||||
private String expectedTid;
|
||||
|
||||
/**
|
||||
* 期望的业务ID值(从InstructionContext中获取)
|
||||
*/
|
||||
private String expectedBid;
|
||||
|
||||
/**
|
||||
* 回调类型(由框架自动设置,不需要手动指定)
|
||||
* - getMethodCallbackConfig() 返回的配置会被设置为 METHOD
|
||||
* - getStateCallbackConfig() 返回的配置会被设置为 STATE
|
||||
*/
|
||||
private CallbackType callbackType;
|
||||
|
||||
/**
|
||||
* 判断消息是否匹配
|
||||
* 注意: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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从消息体中提取字段值
|
||||
*/
|
||||
private Object extractFieldValue(Object messageBody, String path) {
|
||||
if (messageBody == null || path == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 如果 messageBody 是字符串,尝试解析为 JSON
|
||||
Object current = messageBody;
|
||||
if (messageBody instanceof String) {
|
||||
try {
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
current = objectMapper.readValue((String) messageBody, Object.class);
|
||||
} catch (Exception e) {
|
||||
// 解析失败,返回 null
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
String[] parts = path.split("\\.");
|
||||
|
||||
for (String part : parts) {
|
||||
if (current == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (current instanceof java.util.Map) {
|
||||
current = ((java.util.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) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
package com.ruoyi.device.domain.impl.machine.instruction;
|
||||
|
||||
/**
|
||||
* 指令接口
|
||||
* 一个指令包含四个部分:
|
||||
* a. 判断是否可以执行该指令
|
||||
* b. 执行远程调用(如MQTT发送)
|
||||
* c. 等待方法回调,并判断该方法是否成功执行
|
||||
* d. 等待状态回调,并判断结果是否OK
|
||||
*/
|
||||
public interface Instruction {
|
||||
/**
|
||||
* 获取指令名称
|
||||
*/
|
||||
String getName();
|
||||
|
||||
/**
|
||||
* a. 判断是否可以执行该指令
|
||||
*/
|
||||
default boolean canExecute(InstructionContext context){
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* b. 执行远程调用(如MQTT发送)
|
||||
*/
|
||||
default void executeRemoteCall(InstructionContext context) throws Exception{
|
||||
}
|
||||
|
||||
/**
|
||||
* c. 获取方法回调配置(可选)
|
||||
* 返回null表示不需要方法回调
|
||||
*/
|
||||
CallbackConfig getMethodCallbackConfig(InstructionContext context);
|
||||
|
||||
/**
|
||||
* d. 获取状态回调配置(可选)
|
||||
* 返回null表示不需要状态回调
|
||||
*/
|
||||
CallbackConfig getStateCallbackConfig(InstructionContext context);
|
||||
|
||||
/**
|
||||
* 获取指令超时时间(毫秒)
|
||||
*/
|
||||
default long getTimeoutMs() {
|
||||
return 1000; // 默认10秒
|
||||
}
|
||||
|
||||
/**
|
||||
* 指令执行完成回调(无论成功失败都会调用)
|
||||
*/
|
||||
default void onComplete(InstructionContext context, InstructionResult result) {
|
||||
// 默认空实现
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取成功后执行的下一个指令
|
||||
*/
|
||||
default Instruction getOnSuccessInstruction() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取失败后执行的下一个指令
|
||||
*/
|
||||
default Instruction getOnFailureInstruction() {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 根据执行结果获取下一个指令
|
||||
*
|
||||
* @param success 是否成功
|
||||
* @return 下一个指令,如果没有则返回null
|
||||
*/
|
||||
default Instruction getNextInstruction(boolean success) {
|
||||
|
||||
// 根据成功失败返回对应的指令
|
||||
if (success) {
|
||||
return getOnSuccessInstruction();
|
||||
} else {
|
||||
return getOnFailureInstruction();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
package com.ruoyi.device.domain.impl.machine.instruction;
|
||||
|
||||
|
||||
import com.ruoyi.device.domain.impl.machine.mqtt.MqttClient;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 指令执行上下文
|
||||
*/
|
||||
@Data
|
||||
public class InstructionContext {
|
||||
/**
|
||||
* 设备SN号
|
||||
*/
|
||||
private String sn;
|
||||
|
||||
/**
|
||||
* 厂家类型
|
||||
*/
|
||||
private String vendorType;
|
||||
|
||||
/**
|
||||
* MQTT客户端(用于发送MQTT消息)
|
||||
*/
|
||||
private MqttClient mqttClient;
|
||||
|
||||
/**
|
||||
* 上下文数据(用于在指令间传递数据)
|
||||
*/
|
||||
private Map<String, Object> contextData = new HashMap<>();
|
||||
|
||||
/**
|
||||
* 命令参数
|
||||
*/
|
||||
private Map<String, Object> commandParams = new HashMap<>();
|
||||
|
||||
/**
|
||||
* 事务ID(Transaction ID)- 用于匹配回调消息
|
||||
* 在命令执行阶段生成,用于标识本次指令执行
|
||||
*/
|
||||
private String tid;
|
||||
|
||||
/**
|
||||
* 业务ID(Business 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) {
|
||||
contextData.put(key, value);
|
||||
}
|
||||
|
||||
public Object getContextData(String key) {
|
||||
return contextData.get(key);
|
||||
}
|
||||
|
||||
public void putCommandParam(String key, Object value) {
|
||||
commandParams.put(key, value);
|
||||
}
|
||||
|
||||
public Object getCommandParam(String key) {
|
||||
return commandParams.get(key);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
package com.ruoyi.device.domain.impl.machine.instruction;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 指令执行结果
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class InstructionResult {
|
||||
/**
|
||||
* 是否成功
|
||||
*/
|
||||
private boolean success;
|
||||
|
||||
/**
|
||||
* 错误信息
|
||||
*/
|
||||
private String errorMessage;
|
||||
|
||||
/**
|
||||
* 结果数据
|
||||
*/
|
||||
private Object data;
|
||||
|
||||
public static InstructionResult success() {
|
||||
return new InstructionResult(true, null, null);
|
||||
}
|
||||
|
||||
public static InstructionResult success(Object data) {
|
||||
return new InstructionResult(true, null, data);
|
||||
}
|
||||
|
||||
public static InstructionResult failure(String errorMessage) {
|
||||
return new InstructionResult(false, errorMessage, null);
|
||||
}
|
||||
|
||||
public static InstructionResult timeout() {
|
||||
return new InstructionResult(false, "指令执行超时", null);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
package com.ruoyi.device.domain.impl.machine.mqtt;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* MQTT回调处理器
|
||||
*/
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
public class MqttCallbackHandler {
|
||||
/**
|
||||
* 回调ID(用于取消注册)
|
||||
*/
|
||||
private String callbackId;
|
||||
|
||||
/**
|
||||
* 监听的主题
|
||||
*/
|
||||
private String topic;
|
||||
|
||||
/**
|
||||
* 消息处理器
|
||||
*/
|
||||
private Consumer<Object> messageHandler;
|
||||
|
||||
/**
|
||||
* 超时时间(毫秒)
|
||||
*/
|
||||
private long timeoutMs;
|
||||
|
||||
/**
|
||||
* 注册时间
|
||||
*/
|
||||
private long registerTime;
|
||||
|
||||
/**
|
||||
* 是否已超时
|
||||
*/
|
||||
public boolean isTimeout() {
|
||||
return System.currentTimeMillis() - registerTime > timeoutMs;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,361 @@
|
|||
package com.ruoyi.device.domain.impl.machine.mqtt;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import com.ruoyi.device.domain.impl.machine.mqtt.store.MqttCallbackInfo;
|
||||
import com.ruoyi.device.domain.impl.machine.mqtt.store.MqttCallbackStore;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* MQTT回调注册中心
|
||||
* 用于注册和管理MQTT消息的回调处理器,他的 handleMessage 需要被真实的MQTT回调去调用
|
||||
*
|
||||
* 架构说明:
|
||||
* - 回调元数据存储在 MqttCallbackStore 中(支持内存/Redis)
|
||||
* - Consumer<Object> 回调函数存储在本地内存中(无法序列化)
|
||||
* - 多节点部署时,通过 Redis Pub/Sub 在节点间传递消息
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class MqttCallbackRegistry {
|
||||
|
||||
/**
|
||||
* 回调存储层(支持内存、Redis等多种实现)
|
||||
*/
|
||||
private final MqttCallbackStore callbackStore;
|
||||
|
||||
/**
|
||||
* 回调ID -> 本地消息处理器(Consumer 无法序列化,只能存储在本地)
|
||||
*/
|
||||
private final Map<String, Consumer<Object>> localHandlers = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 当前节点ID(用于 Redis Pub/Sub 路由)
|
||||
*/
|
||||
private String nodeId;
|
||||
|
||||
/**
|
||||
* ObjectMapper 用于序列化消息
|
||||
*/
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
@Value("${machine.node.id:#{null}}")
|
||||
private String configuredNodeId;
|
||||
|
||||
public MqttCallbackRegistry(MqttCallbackStore callbackStore) {
|
||||
this.callbackStore = callbackStore;
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
// 初始化节点ID
|
||||
if (configuredNodeId != null && !configuredNodeId.isEmpty()) {
|
||||
nodeId = configuredNodeId;
|
||||
} else {
|
||||
// 自动生成节点ID:主机名 + UUID
|
||||
try {
|
||||
String hostname = InetAddress.getLocalHost().getHostName();
|
||||
nodeId = hostname + "-" + UUID.randomUUID().toString().substring(0, 8);
|
||||
} catch (Exception e) {
|
||||
nodeId = "node-" + UUID.randomUUID().toString().substring(0, 8);
|
||||
}
|
||||
}
|
||||
|
||||
// 订阅当前节点的消息(用于 Redis Pub/Sub)
|
||||
callbackStore.subscribeNodeMessages(nodeId, this::handleNodeMessage);
|
||||
|
||||
log.info("MQTT回调注册中心初始化完成,节点ID: {}, 存储实现: {}",
|
||||
nodeId, callbackStore.getClass().getSimpleName());
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册回调
|
||||
*
|
||||
* @param topic 监听的主题
|
||||
* @param messageHandler 消息处理器
|
||||
* @param timeoutMs 超时时间(毫秒)
|
||||
* @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. 创建回调信息并存储到存储层
|
||||
MqttCallbackInfo callbackInfo = MqttCallbackInfo.builder()
|
||||
.callbackId(callbackId)
|
||||
.topic(topic)
|
||||
.timeoutMs(timeoutMs)
|
||||
.registerTime(System.currentTimeMillis())
|
||||
.nodeId(nodeId)
|
||||
.tidFieldPath(tidFieldPath)
|
||||
.expectedTid(expectedTid)
|
||||
.bidFieldPath(bidFieldPath)
|
||||
.expectedBid(expectedBid)
|
||||
.build();
|
||||
|
||||
callbackStore.registerCallback(callbackInfo);
|
||||
|
||||
// 2. 将 Consumer 存储到本地内存
|
||||
localHandlers.put(callbackId, messageHandler);
|
||||
|
||||
log.debug("注册MQTT回调: callbackId={}, topic={}, timeoutMs={}, nodeId={}, tid={}, bid={}",
|
||||
callbackId, topic, timeoutMs, nodeId, expectedTid, expectedBid);
|
||||
return callbackId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消注册回调
|
||||
*
|
||||
* @param callbackId 回调ID
|
||||
*/
|
||||
public void unregisterCallback(String callbackId) {
|
||||
// 1. 从存储层删除回调信息
|
||||
callbackStore.unregisterCallback(callbackId);
|
||||
|
||||
// 2. 从本地内存删除 Consumer
|
||||
localHandlers.remove(callbackId);
|
||||
|
||||
log.debug("取消注册MQTT回调: callbackId={}", callbackId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理接收到的MQTT消息(由真实的 MQTT 客户端调用)
|
||||
*
|
||||
* @param topic 主题
|
||||
* @param messageBody 消息体
|
||||
*/
|
||||
public void handleMessage(String topic, Object messageBody) {
|
||||
// 1. 从存储层获取所有等待该 topic 的回调信息
|
||||
List<MqttCallbackInfo> callbacks = callbackStore.getCallbacksByTopic(topic);
|
||||
if (callbacks.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
log.debug("处理MQTT消息: topic={}, callbackCount={}", topic, callbacks.size());
|
||||
|
||||
// 2. 序列化消息体(用于跨节点传递)
|
||||
String messageBodyJson;
|
||||
try {
|
||||
messageBodyJson = objectMapper.writeValueAsString(messageBody);
|
||||
} catch (Exception e) {
|
||||
log.error("序列化消息体失败: topic={}", topic, e);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 处理每个回调
|
||||
for (MqttCallbackInfo callbackInfo : callbacks) {
|
||||
try {
|
||||
// 检查是否超时
|
||||
if (callbackInfo.isTimeout()) {
|
||||
log.warn("MQTT回调已超时: callbackId={}, topic={}",
|
||||
callbackInfo.getCallbackId(), topic);
|
||||
unregisterCallback(callbackInfo.getCallbackId());
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查 tid/bid 是否匹配(如果配置了)
|
||||
if (!matchesTidBid(callbackInfo, messageBody)) {
|
||||
log.debug("MQTT消息 tid/bid 不匹配,跳过回调: callbackId={}, topic={}",
|
||||
callbackInfo.getCallbackId(), topic);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 判断回调是在本节点还是其他节点
|
||||
if (nodeId.equals(callbackInfo.getNodeId())) {
|
||||
// 本节点的回调,直接执行
|
||||
executeLocalCallback(callbackInfo.getCallbackId(), messageBody);
|
||||
} else {
|
||||
// 其他节点的回调,通过 Redis Pub/Sub 转发
|
||||
callbackStore.publishMessageToNode(
|
||||
callbackInfo.getNodeId(),
|
||||
callbackInfo.getCallbackId(),
|
||||
messageBodyJson
|
||||
);
|
||||
log.debug("转发消息到节点: nodeId={}, callbackId={}",
|
||||
callbackInfo.getNodeId(), callbackInfo.getCallbackId());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("处理MQTT回调失败: callbackId={}, topic={}",
|
||||
callbackInfo.getCallbackId(), topic, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查消息的 tid/bid 是否匹配
|
||||
*
|
||||
* @param callbackInfo 回调信息
|
||||
* @param messageBody 消息体
|
||||
* @return true 如果匹配或未配置 tid/bid,false 如果不匹配
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行本地回调
|
||||
*
|
||||
* @param callbackId 回调ID
|
||||
* @param messageBody 消息体
|
||||
*/
|
||||
private void executeLocalCallback(String callbackId, Object messageBody) {
|
||||
Consumer<Object> handler = localHandlers.get(callbackId);
|
||||
if (handler != null) {
|
||||
try {
|
||||
handler.accept(messageBody);
|
||||
log.debug("执行本地回调成功: callbackId={}", callbackId);
|
||||
} catch (Exception e) {
|
||||
log.error("执行本地回调失败: callbackId={}", callbackId, e);
|
||||
}
|
||||
} else {
|
||||
log.warn("本地回调处理器不存在: callbackId={}", callbackId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理从 Redis Pub/Sub 接收到的节点消息
|
||||
*
|
||||
* @param callbackId 回调ID
|
||||
* @param messageBodyJson 消息体(JSON 字符串)
|
||||
*/
|
||||
private void handleNodeMessage(String callbackId, String messageBodyJson) {
|
||||
try {
|
||||
// 反序列化消息体
|
||||
Object messageBody = objectMapper.readValue(messageBodyJson, Object.class);
|
||||
|
||||
// 执行本地回调
|
||||
executeLocalCallback(callbackId, messageBody);
|
||||
} catch (Exception e) {
|
||||
log.error("处理节点消息失败: callbackId={}", callbackId, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理超时的回调
|
||||
*/
|
||||
public void cleanupTimeoutCallbacks() {
|
||||
List<MqttCallbackInfo> allCallbacks = callbackStore.getAllCallbacks();
|
||||
for (MqttCallbackInfo callbackInfo : allCallbacks) {
|
||||
if (callbackInfo.isTimeout()) {
|
||||
log.warn("清理超时的MQTT回调: callbackId={}, topic={}",
|
||||
callbackInfo.getCallbackId(), callbackInfo.getTopic());
|
||||
unregisterCallback(callbackInfo.getCallbackId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前注册的回调数量
|
||||
*/
|
||||
public int getCallbackCount() {
|
||||
return localHandlers.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理所有回调(仅用于测试环境)
|
||||
* 警告:此方法会清理所有回调,包括未超时的,仅应在测试环境中使用
|
||||
*/
|
||||
public void cleanupAllCallbacks() {
|
||||
List<MqttCallbackInfo> allCallbacks = callbackStore.getAllCallbacks();
|
||||
for (MqttCallbackInfo callbackInfo : allCallbacks) {
|
||||
log.debug("清理MQTT回调: callbackId={}, topic={}",
|
||||
callbackInfo.getCallbackId(), callbackInfo.getTopic());
|
||||
unregisterCallback(callbackInfo.getCallbackId());
|
||||
}
|
||||
log.info("已清理所有MQTT回调,共{}个", allCallbacks.size());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.ruoyi.device.domain.impl.machine.mqtt;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* MQTT客户端
|
||||
*/
|
||||
@Component
|
||||
public class MqttClient {
|
||||
|
||||
public void sendMessage(String topic, String message) {
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
package com.ruoyi.device.domain.impl.machine.mqtt.store;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 基于内存的 MQTT 回调存储实现
|
||||
* 适用于单节点部署或开发测试环境
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@ConditionalOnProperty(name = "machine.state.store.type", havingValue = "memory", matchIfMissing = true)
|
||||
public class InMemoryMqttCallbackStore implements MqttCallbackStore {
|
||||
|
||||
/**
|
||||
* 主题 -> 回调信息列表
|
||||
*/
|
||||
private final Map<String, CopyOnWriteArrayList<MqttCallbackInfo>> topicCallbacks = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 回调ID -> 回调信息
|
||||
*/
|
||||
private final Map<String, MqttCallbackInfo> callbackMap = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
public void registerCallback(MqttCallbackInfo callbackInfo) {
|
||||
topicCallbacks.computeIfAbsent(callbackInfo.getTopic(), k -> new CopyOnWriteArrayList<>())
|
||||
.add(callbackInfo);
|
||||
callbackMap.put(callbackInfo.getCallbackId(), callbackInfo);
|
||||
log.debug("注册MQTT回调到内存: callbackId={}, topic={}",
|
||||
callbackInfo.getCallbackId(), callbackInfo.getTopic());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregisterCallback(String callbackId) {
|
||||
MqttCallbackInfo callbackInfo = callbackMap.remove(callbackId);
|
||||
if (callbackInfo != null) {
|
||||
CopyOnWriteArrayList<MqttCallbackInfo> callbacks = topicCallbacks.get(callbackInfo.getTopic());
|
||||
if (callbacks != null) {
|
||||
callbacks.remove(callbackInfo);
|
||||
if (callbacks.isEmpty()) {
|
||||
topicCallbacks.remove(callbackInfo.getTopic());
|
||||
}
|
||||
}
|
||||
log.debug("从内存中取消注册MQTT回调: callbackId={}, topic={}",
|
||||
callbackId, callbackInfo.getTopic());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MqttCallbackInfo> getCallbacksByTopic(String topic) {
|
||||
CopyOnWriteArrayList<MqttCallbackInfo> callbacks = topicCallbacks.get(topic);
|
||||
return callbacks != null ? new ArrayList<>(callbacks) : new ArrayList<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MqttCallbackInfo getCallbackById(String callbackId) {
|
||||
return callbackMap.get(callbackId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MqttCallbackInfo> getAllCallbacks() {
|
||||
return new ArrayList<>(callbackMap.values());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void publishMessageToNode(String nodeId, String callbackId, String messageBody) {
|
||||
// 内存实现中,不需要跨节点通信,此方法为空操作
|
||||
log.trace("内存实现不需要发布消息到节点: nodeId={}, callbackId={}", nodeId, callbackId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void subscribeNodeMessages(String nodeId, NodeMessageListener messageListener) {
|
||||
// 内存实现中,不需要订阅节点消息,此方法为空操作
|
||||
log.trace("内存实现不需要订阅节点消息: nodeId={}", nodeId);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
package com.ruoyi.device.domain.impl.machine.mqtt.store;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* MQTT 回调信息(可序列化到 Redis)
|
||||
* 不包含 Consumer,只包含回调的元数据
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class MqttCallbackInfo implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 回调ID(用于取消注册)
|
||||
*/
|
||||
private String callbackId;
|
||||
|
||||
/**
|
||||
* 监听的主题
|
||||
*/
|
||||
private String topic;
|
||||
|
||||
/**
|
||||
* 超时时间(毫秒)
|
||||
*/
|
||||
private long timeoutMs;
|
||||
|
||||
/**
|
||||
* 注册时间
|
||||
*/
|
||||
private long registerTime;
|
||||
|
||||
/**
|
||||
* 注册该回调的节点ID(用于 Redis Pub/Sub 路由)
|
||||
*/
|
||||
private String nodeId;
|
||||
|
||||
/**
|
||||
* 事务ID字段路径(用于匹配回调消息,如 "tid")
|
||||
*/
|
||||
private String tidFieldPath;
|
||||
|
||||
/**
|
||||
* 期望的事务ID值
|
||||
*/
|
||||
private String expectedTid;
|
||||
|
||||
/**
|
||||
* 业务ID字段路径(用于匹配回调消息,如 "bid")
|
||||
*/
|
||||
private String bidFieldPath;
|
||||
|
||||
/**
|
||||
* 期望的业务ID值
|
||||
*/
|
||||
private String expectedBid;
|
||||
|
||||
/**
|
||||
* 是否已超时
|
||||
*/
|
||||
public boolean isTimeout() {
|
||||
return System.currentTimeMillis() - registerTime > timeoutMs;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
package com.ruoyi.device.domain.impl.machine.mqtt.store;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* MQTT 回调存储接口
|
||||
* 提供回调信息的存储和获取抽象,支持多种实现(内存、Redis等)
|
||||
*/
|
||||
public interface MqttCallbackStore {
|
||||
|
||||
/**
|
||||
* 注册回调信息
|
||||
*
|
||||
* @param callbackInfo 回调信息
|
||||
*/
|
||||
void registerCallback(MqttCallbackInfo callbackInfo);
|
||||
|
||||
/**
|
||||
* 取消注册回调
|
||||
*
|
||||
* @param callbackId 回调ID
|
||||
*/
|
||||
void unregisterCallback(String callbackId);
|
||||
|
||||
/**
|
||||
* 根据 topic 获取所有等待该 topic 的回调信息
|
||||
*
|
||||
* @param topic MQTT 主题
|
||||
* @return 回调信息列表
|
||||
*/
|
||||
List<MqttCallbackInfo> getCallbacksByTopic(String topic);
|
||||
|
||||
/**
|
||||
* 根据 callbackId 获取回调信息
|
||||
*
|
||||
* @param callbackId 回调ID
|
||||
* @return 回调信息,如果不存在返回 null
|
||||
*/
|
||||
MqttCallbackInfo getCallbackById(String callbackId);
|
||||
|
||||
/**
|
||||
* 获取所有回调信息(用于清理超时回调)
|
||||
*
|
||||
* @return 所有回调信息列表
|
||||
*/
|
||||
List<MqttCallbackInfo> getAllCallbacks();
|
||||
|
||||
/**
|
||||
* 发布消息到指定节点(用于 Redis Pub/Sub)
|
||||
* 在内存实现中,此方法为空操作
|
||||
*
|
||||
* @param nodeId 节点ID
|
||||
* @param callbackId 回调ID
|
||||
* @param messageBody 消息体(JSON 字符串)
|
||||
*/
|
||||
void publishMessageToNode(String nodeId, String callbackId, String messageBody);
|
||||
|
||||
/**
|
||||
* 订阅当前节点的消息(用于 Redis Pub/Sub)
|
||||
* 在内存实现中,此方法为空操作
|
||||
*
|
||||
* @param nodeId 当前节点ID
|
||||
* @param messageListener 消息监听器
|
||||
*/
|
||||
void subscribeNodeMessages(String nodeId, NodeMessageListener messageListener);
|
||||
|
||||
/**
|
||||
* 节点消息监听器
|
||||
*/
|
||||
interface NodeMessageListener {
|
||||
/**
|
||||
* 处理接收到的消息
|
||||
*
|
||||
* @param callbackId 回调ID
|
||||
* @param messageBody 消息体(JSON 字符串)
|
||||
*/
|
||||
void onMessage(String callbackId, String messageBody);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,267 @@
|
|||
package com.ruoyi.device.domain.impl.machine.mqtt.store;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.data.redis.connection.MessageListener;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.data.redis.core.script.DefaultRedisScript;
|
||||
import org.springframework.data.redis.listener.ChannelTopic;
|
||||
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 基于 Redis 的 MQTT 回调存储实现
|
||||
* 适用于多节点部署的生产环境
|
||||
*
|
||||
* 架构说明:
|
||||
* 1. 回调信息存储在 Redis Hash 中:mqtt:callback:{callbackId} -> MqttCallbackInfo (JSON)
|
||||
* 2. Topic 索引存储在 Redis Set 中:mqtt:topic:{topic} -> Set<callbackId>
|
||||
* 3. 使用 Redis Pub/Sub 在节点间传递 MQTT 消息:mqtt:node:{nodeId} -> {callbackId, messageBody}
|
||||
*
|
||||
* 工作流程:
|
||||
* - 节点A 注册回调 -> 存储到 Redis
|
||||
* - 节点B 收到 MQTT 消息 -> 从 Redis 查询等待该 topic 的回调 -> 通过 Pub/Sub 发送到对应节点
|
||||
* - 节点A 收到 Pub/Sub 消息 -> 执行本地的 Consumer 回调
|
||||
*
|
||||
* 使用方式:
|
||||
* 1. 在 application.properties 中配置:machine.state.store.type=redis
|
||||
* 2. 配置 Redis 连接信息
|
||||
* 3. 实现 Redis 相关的序列化和 Pub/Sub 逻辑
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@ConditionalOnProperty(name = "machine.state.store.type", havingValue = "redis")
|
||||
public class RedisMqttCallbackStore implements MqttCallbackStore {
|
||||
|
||||
private final StringRedisTemplate stringRedisTemplate;
|
||||
private final RedisMessageListenerContainer redisMessageListenerContainer;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
// Redis key 前缀
|
||||
private static final String CALLBACK_KEY_PREFIX = "mqtt:callback:";
|
||||
private static final String TOPIC_INDEX_PREFIX = "mqtt:topic:";
|
||||
private static final String NODE_CHANNEL_PREFIX = "mqtt:node:";
|
||||
|
||||
// 配置回调信息的过期时间
|
||||
private static final long EXPIRE_SECONDS = 3600; // 1小时
|
||||
|
||||
/**
|
||||
* Lua 脚本:注册 MQTT 回调
|
||||
* 使用 Lua 脚本保证原子性,避免竞态条件
|
||||
*
|
||||
* KEYS[1]: Topic 索引 key (mqtt:topic:{topic})
|
||||
* KEYS[2]: 回调信息 key (mqtt:callback:{callbackId})
|
||||
* ARGV[1]: callbackId
|
||||
* ARGV[2]: 过期时间(秒)
|
||||
* ARGV[3]: 回调信息 JSON
|
||||
*
|
||||
* 返回值: 1 表示成功
|
||||
*/
|
||||
private static final String REGISTER_CALLBACK_SCRIPT =
|
||||
"redis.call('SADD', KEYS[1], ARGV[1]) " +
|
||||
"redis.call('EXPIRE', KEYS[1], ARGV[2]) " +
|
||||
"redis.call('SETEX', KEYS[2], ARGV[2], ARGV[3]) " +
|
||||
"return 1";
|
||||
|
||||
/**
|
||||
* Lua 脚本:取消注册 MQTT 回调
|
||||
* 使用 Lua 脚本保证原子性
|
||||
*
|
||||
* KEYS[1]: Topic 索引 key (mqtt:topic:{topic})
|
||||
* KEYS[2]: 回调信息 key (mqtt:callback:{callbackId})
|
||||
* ARGV[1]: callbackId
|
||||
*
|
||||
* 返回值: 1 表示成功
|
||||
*/
|
||||
private static final String UNREGISTER_CALLBACK_SCRIPT =
|
||||
"redis.call('SREM', KEYS[1], ARGV[1]) " +
|
||||
"redis.call('DEL', KEYS[2]) " +
|
||||
"return 1";
|
||||
|
||||
public RedisMqttCallbackStore(
|
||||
StringRedisTemplate stringRedisTemplate,
|
||||
@Qualifier("machineFrameworkRedisMessageListenerContainer") RedisMessageListenerContainer redisMessageListenerContainer,
|
||||
ObjectMapper objectMapper) {
|
||||
this.stringRedisTemplate = stringRedisTemplate;
|
||||
this.redisMessageListenerContainer = redisMessageListenerContainer;
|
||||
this.objectMapper = objectMapper;
|
||||
log.info("使用 Redis MQTT 回调存储实现");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerCallback(MqttCallbackInfo callbackInfo) {
|
||||
try {
|
||||
// 1. 序列化回调信息为 JSON
|
||||
String json = objectMapper.writeValueAsString(callbackInfo);
|
||||
|
||||
// 2. 准备 Redis key
|
||||
String topicKey = TOPIC_INDEX_PREFIX + callbackInfo.getTopic();
|
||||
String callbackKey = CALLBACK_KEY_PREFIX + callbackInfo.getCallbackId();
|
||||
|
||||
// 3. 使用 Lua 脚本原子性地注册回调
|
||||
// 先添加到 Topic 索引,再存储回调信息,避免竞态条件
|
||||
stringRedisTemplate.execute(
|
||||
new DefaultRedisScript<>(REGISTER_CALLBACK_SCRIPT, Long.class),
|
||||
Arrays.asList(topicKey, callbackKey),
|
||||
callbackInfo.getCallbackId(),
|
||||
String.valueOf(EXPIRE_SECONDS),
|
||||
json
|
||||
);
|
||||
|
||||
log.debug("注册MQTT回调到Redis: callbackId={}, topic={}",
|
||||
callbackInfo.getCallbackId(), callbackInfo.getTopic());
|
||||
|
||||
} catch (JsonProcessingException e) {
|
||||
log.error("序列化回调信息失败: callbackId={}, topic={}",
|
||||
callbackInfo.getCallbackId(), callbackInfo.getTopic(), e);
|
||||
throw new RuntimeException("注册MQTT回调失败: 序列化错误", e);
|
||||
} catch (Exception e) {
|
||||
log.error("注册MQTT回调到Redis失败: callbackId={}, topic={}",
|
||||
callbackInfo.getCallbackId(), callbackInfo.getTopic(), e);
|
||||
// 不抛出异常,让上层通过超时机制处理
|
||||
// 这样可以避免因为 Redis 临时故障导致整个命令执行失败
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregisterCallback(String callbackId) {
|
||||
try {
|
||||
// 1. 获取回调信息(需要知道 topic 才能删除索引)
|
||||
MqttCallbackInfo callbackInfo = getCallbackById(callbackId);
|
||||
if (callbackInfo == null) {
|
||||
log.debug("回调信息不存在,无需取消注册: callbackId={}", callbackId);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 准备 Redis key
|
||||
String topicKey = TOPIC_INDEX_PREFIX + callbackInfo.getTopic();
|
||||
String callbackKey = CALLBACK_KEY_PREFIX + callbackId;
|
||||
|
||||
// 3. 使用 Lua 脚本原子性地取消注册回调
|
||||
stringRedisTemplate.execute(
|
||||
new DefaultRedisScript<>(UNREGISTER_CALLBACK_SCRIPT, Long.class),
|
||||
Arrays.asList(topicKey, callbackKey),
|
||||
callbackId
|
||||
);
|
||||
|
||||
log.debug("从Redis中取消注册MQTT回调: callbackId={}, topic={}",
|
||||
callbackId, callbackInfo.getTopic());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("从Redis中取消注册MQTT回调失败: callbackId={}", callbackId, e);
|
||||
// 不抛出异常,取消注册失败不影响主流程
|
||||
// 回调会因为 TTL 自动过期
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MqttCallbackInfo> getCallbacksByTopic(String topic) {
|
||||
// 1. 从 Topic 索引获取所有 callbackId
|
||||
String topicKey = TOPIC_INDEX_PREFIX + topic;
|
||||
Set<String> callbackIds = stringRedisTemplate.opsForSet().members(topicKey);
|
||||
if (callbackIds == null || callbackIds.isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
// 2. 批量获取回调信息
|
||||
List<MqttCallbackInfo> callbacks = new ArrayList<>();
|
||||
for (String callbackId : callbackIds) {
|
||||
MqttCallbackInfo callbackInfo = getCallbackById(callbackId);
|
||||
if (callbackInfo != null) {
|
||||
callbacks.add(callbackInfo);
|
||||
}
|
||||
}
|
||||
return callbacks;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MqttCallbackInfo getCallbackById(String callbackId) {
|
||||
String callbackKey = CALLBACK_KEY_PREFIX + callbackId;
|
||||
String json = stringRedisTemplate.opsForValue().get(callbackKey);
|
||||
if (json == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return objectMapper.readValue(json, MqttCallbackInfo.class);
|
||||
} catch (JsonProcessingException e) {
|
||||
log.error("反序列化回调信息失败: callbackId={}", callbackId, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MqttCallbackInfo> getAllCallbacks() {
|
||||
// 1. 扫描所有 mqtt:callback:* 的 key
|
||||
Set<String> keys = stringRedisTemplate.keys(CALLBACK_KEY_PREFIX + "*");
|
||||
if (keys == null || keys.isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
// 2. 批量获取回调信息
|
||||
List<MqttCallbackInfo> callbacks = new ArrayList<>();
|
||||
for (String key : keys) {
|
||||
String callbackId = key.substring(CALLBACK_KEY_PREFIX.length());
|
||||
MqttCallbackInfo callbackInfo = getCallbackById(callbackId);
|
||||
if (callbackInfo != null) {
|
||||
callbacks.add(callbackInfo);
|
||||
}
|
||||
}
|
||||
return callbacks;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void publishMessageToNode(String nodeId, String callbackId, String messageBody) {
|
||||
try {
|
||||
// 1. 构造消息体
|
||||
Map<String, String> message = new HashMap<>();
|
||||
message.put("callbackId", callbackId);
|
||||
message.put("messageBody", messageBody);
|
||||
|
||||
// 2. 序列化消息
|
||||
String json = objectMapper.writeValueAsString(message);
|
||||
|
||||
// 3. 发布到节点频道
|
||||
String channel = NODE_CHANNEL_PREFIX + nodeId;
|
||||
stringRedisTemplate.convertAndSend(channel, json);
|
||||
|
||||
log.debug("发布消息到节点: nodeId={}, callbackId={}, channel={}",
|
||||
nodeId, callbackId, channel);
|
||||
} catch (JsonProcessingException e) {
|
||||
log.error("序列化节点消息失败: nodeId={}, callbackId={}", nodeId, callbackId, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void subscribeNodeMessages(String nodeId, NodeMessageListener messageListener) {
|
||||
// 1. 创建消息监听器
|
||||
MessageListener redisMessageListener = (message, pattern) -> {
|
||||
try {
|
||||
String json = new String(message.getBody());
|
||||
Map<String, String> data = objectMapper.readValue(json,
|
||||
new TypeReference<Map<String, String>>() {});
|
||||
|
||||
String callbackId = data.get("callbackId");
|
||||
String messageBody = data.get("messageBody");
|
||||
|
||||
messageListener.onMessage(callbackId, messageBody);
|
||||
} catch (Exception e) {
|
||||
log.error("处理Redis Pub/Sub消息失败", e);
|
||||
}
|
||||
};
|
||||
|
||||
// 2. 订阅节点频道
|
||||
String channel = NODE_CHANNEL_PREFIX + nodeId;
|
||||
redisMessageListenerContainer.addMessageListener(redisMessageListener, new ChannelTopic(channel));
|
||||
|
||||
log.info("订阅节点消息: nodeId={}, channel={}", nodeId, channel);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,218 @@
|
|||
生产需要实现
|
||||
MysqlSnVendorMappingRepository 这个类
|
||||
MQTT回调的地方需要转发到:MqttCallbackRegistry 这个类
|
||||
需要实现 MqttClient 这边消息发送的逻辑
|
||||
需要配置 sn.repository.type=mysql
|
||||
|
||||
|
||||
单节点部署(默认)
|
||||
|
||||
# 使用内存存储(默认配置)
|
||||
machine.state.store.type=memory
|
||||
|
||||
# 以下配置生产需要修改为mysql,同时实现 MysqlSnVendorMappingRepository 这个类
|
||||
sn.repository.type=memory
|
||||
|
||||
|
||||
多节点部署
|
||||
# 切换到 Redis 存储
|
||||
machine.state.store.type=redis
|
||||
# 配置节点ID(可选,不配置会自动生成)
|
||||
machine.node.id=node-1
|
||||
#本地启动redis
|
||||
#docker run --name some-redis -d -p 6379:6379 redi
|
||||
# Redis 配置
|
||||
spring.redis.host=localhost
|
||||
spring.redis.port=6379
|
||||
spring.redis.password=your-password
|
||||
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 步骤1: 节点A 执行命令并注册回调 │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
节点A: executeCommand(TAKE_OFF)
|
||||
↓
|
||||
节点A: registerCallback(topic="dji/SN9527/response", ...)
|
||||
↓
|
||||
节点A: MqttCallbackStore.registerCallback()
|
||||
↓
|
||||
Redis: 存储回调信息(使用两个 Key 的原因:性能优化)
|
||||
|
||||
【Key 1】回调详细信息(Hash 结构)
|
||||
- Key: mqtt:callback:{callbackId}
|
||||
- Value: {callbackId, topic, nodeId="nodeA", timeoutMs, registerTime, ...}
|
||||
- 作用: 存储单个回调的完整信息
|
||||
- 查询: O(1) 时间复杂度,通过 callbackId 直接获取
|
||||
|
||||
【Key 2】Topic 索引(Set 结构)
|
||||
- Key: mqtt:topic:dji/SN9527/response
|
||||
- Value: Set<callbackId> // 例如: ["abc-123", "def-456", "ghi-789"]
|
||||
- 作用: 快速查询等待某个 topic 的所有回调
|
||||
- 查询: O(1) 时间复杂度,直接获取 callbackId 列表
|
||||
|
||||
【为什么需要两个 Key?】
|
||||
如果只用一个 Key 存储所有回调,查询时需要遍历所有回调并过滤 topic,
|
||||
时间复杂度为 O(n)。使用 Topic 索引后,可以直接获取目标回调列表,
|
||||
时间复杂度降为 O(1),大幅提升性能。
|
||||
|
||||
【示例】
|
||||
假设有 3 个回调:
|
||||
- callbackId="abc-123", topic="dji/SN9527/response", nodeId="nodeA"
|
||||
- callbackId="def-456", topic="dji/SN9527/state", nodeId="nodeB"
|
||||
- callbackId="ghi-789", topic="dji/SN9527/response", nodeId="nodeA"
|
||||
|
||||
Redis 存储结构:
|
||||
mqtt:callback:abc-123 → {callbackId:"abc-123", topic:"dji/SN9527/response", nodeId:"nodeA"}
|
||||
mqtt:callback:def-456 → {callbackId:"def-456", topic:"dji/SN9527/state", nodeId:"nodeB"}
|
||||
mqtt:callback:ghi-789 → {callbackId:"ghi-789", topic:"dji/SN9527/response", nodeId:"nodeA"}
|
||||
mqtt:topic:dji/SN9527/response → ["abc-123", "ghi-789"]
|
||||
mqtt:topic:dji/SN9527/state → ["def-456"]
|
||||
|
||||
查询 topic="dji/SN9527/response" 的回调:
|
||||
1. 从索引获取: SMEMBERS mqtt:topic:dji/SN9527/response → ["abc-123", "ghi-789"]
|
||||
2. 批量获取详情: MGET mqtt:callback:abc-123 mqtt:callback:ghi-789
|
||||
3. 总耗时: O(1) + O(k),k 是该 topic 的回调数量(通常很小)
|
||||
|
||||
【Redis 数据清理时机】
|
||||
Redis 中的回调数据有两种清理机制:
|
||||
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 1️⃣ 主动清理(业务逻辑触发) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
触发时机:
|
||||
✅ 回调成功执行后(TransactionExecutor 的 finally 块)
|
||||
✅ 回调超时后(TransactionExecutor 的 finally 块)
|
||||
✅ handleMessage 检测到超时(转发前检查)
|
||||
|
||||
清理操作:
|
||||
unregisterCallback(callbackId)
|
||||
↓
|
||||
1. 获取回调信息: GET mqtt:callback:{callbackId}
|
||||
2. 删除回调信息: DEL mqtt:callback:{callbackId}
|
||||
3. 从索引中移除: SREM mqtt:topic:{topic} {callbackId}
|
||||
|
||||
示例:
|
||||
T0: 注册回调,超时时间 10 秒
|
||||
T5: 收到 MQTT 响应,回调执行成功
|
||||
T5: 立即清理 Redis 数据 ✅
|
||||
- DEL mqtt:callback:abc-123
|
||||
- SREM mqtt:topic:dji/SN9527/response abc-123
|
||||
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 2️⃣ 被动清理(Redis TTL 自动过期) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
作用:兜底机制,防止异常情况下的数据残留
|
||||
|
||||
设置方式:
|
||||
// 注册回调时设置 TTL
|
||||
SET mqtt:callback:{callbackId} {json} EX 3600 // 1小时后自动过期
|
||||
EXPIRE mqtt:topic:{topic} 3600 // 1小时后自动过期
|
||||
|
||||
触发时机:
|
||||
⚠️ 应用异常崩溃,主动清理未执行
|
||||
⚠️ 网络分区,无法删除 Redis 数据
|
||||
⚠️ 代码 Bug,主动清理失败
|
||||
|
||||
示例:
|
||||
T0: 注册回调,TTL=3600秒(1小时)
|
||||
T5: 应用崩溃,主动清理未执行 ❌
|
||||
T3600: Redis 自动删除过期数据 ✅
|
||||
- mqtt:callback:abc-123 自动过期删除
|
||||
- mqtt:topic:dji/SN9527/response 自动过期删除
|
||||
|
||||
【推荐配置】
|
||||
TTL 应该设置为回调超时时间的 2-3 倍,例如:
|
||||
- 回调超时: 10 秒
|
||||
- Redis TTL: 30 秒(10秒 × 3)
|
||||
|
||||
这样可以确保:
|
||||
✅ 正常情况下,主动清理会在 10 秒内完成
|
||||
✅ 异常情况下,Redis 会在 30 秒后自动清理
|
||||
✅ 避免设置过长的 TTL 导致内存浪费
|
||||
|
||||
【注意事项】
|
||||
⚠️ Topic 索引的 TTL 问题:
|
||||
如果同一个 topic 有多个回调,每次添加新回调时都会刷新 TTL。
|
||||
这可能导致索引的 TTL 比单个回调的 TTL 更长。
|
||||
|
||||
解决方案:
|
||||
方案1: 不为 Topic 索引设置 TTL,只在删除最后一个 callbackId 时删除索引
|
||||
方案2: 每次查询时过滤掉已过期的 callbackId(推荐)
|
||||
↓
|
||||
节点A: 本地内存存储 Consumer<Object>
|
||||
- localHandlers.put(callbackId, consumer)
|
||||
↓
|
||||
节点A: 订阅 Redis Pub/Sub 频道
|
||||
- Channel: mqtt:node:nodeA
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 步骤2: MQTT Broker 将响应路由到节点B(不是节点A) │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
MQTT Broker: 收到设备响应
|
||||
↓
|
||||
MQTT Broker: 将消息路由到节点B(随机/轮询)
|
||||
↓
|
||||
节点B: MqttCallbackRegistry.handleMessage(topic, messageBody)
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 步骤3: 节点B 从 Redis 查询等待该 topic 的回调 │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
节点B: callbackStore.getCallbacksByTopic("dji/SN9527/response")
|
||||
↓
|
||||
Redis: 查询 mqtt:topic:dji/SN9527/response
|
||||
↓
|
||||
Redis: 返回 Set<callbackId>
|
||||
↓
|
||||
Redis: 批量获取回调信息
|
||||
- mqtt:callback:{callbackId1} → {nodeId="nodeA", ...}
|
||||
- mqtt:callback:{callbackId2} → {nodeId="nodeA", ...}
|
||||
↓
|
||||
节点B: 获得回调列表 List<MqttCallbackInfo>
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 步骤4: 节点B 判断回调属于哪个节点 │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
节点B: for (MqttCallbackInfo callback : callbacks) {
|
||||
if (nodeId.equals(callback.getNodeId())) {
|
||||
// 本节点的回调,直接执行
|
||||
executeLocalCallback(...)
|
||||
} else {
|
||||
// 其他节点的回调,转发到目标节点
|
||||
callbackStore.publishMessageToNode(...)
|
||||
}
|
||||
}
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 步骤5: 节点B 通过 Redis Pub/Sub 转发消息到节点A │
|
||||
└─────────────────────────────────┘
|
||||
节点B: callbackStore.publishMessageToNode(
|
||||
nodeId="nodeA",
|
||||
callbackId="xxx",
|
||||
messageBody="{...}" // JSON 字符串
|
||||
)
|
||||
↓
|
||||
Redis Pub/Sub: PUBLISH mqtt:node:nodeA
|
||||
{
|
||||
"callbackId": "xxx",
|
||||
"messageBody": "{...}"
|
||||
}
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 步骤6: 节点A 收到 Redis Pub/Sub 消息 │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
节点A: Redis Pub/Sub Listener 收到消息
|
||||
↓
|
||||
节点A: handleNodeMessage(callbackId, messageBodyJson)
|
||||
↓
|
||||
节点A: 反序列化消息体
|
||||
- Object messageBody = objectMapper.readValue(messageBodyJson)
|
||||
↓
|
||||
节点A: executeLocalCallback(callbackId, messageBody)
|
||||
↓
|
||||
节点A: 从本地内存获取 Consumer
|
||||
- Consumer<Object> handler = localHandlers.get(callbackId)
|
||||
↓
|
||||
节点A: 执行回调
|
||||
- handler.accept(messageBody)
|
||||
↓
|
||||
✅ 命令执行成功!
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.ruoyi.device.domain.impl.machine.state;
|
||||
/**
|
||||
* 机巢状态枚举
|
||||
*/
|
||||
public enum AirportState {
|
||||
/**
|
||||
* 未知状态(服务器重启后的初始状态,等待第一次心跳同步),同时也是离线状态
|
||||
*/
|
||||
UNKNOWN,
|
||||
|
||||
/**
|
||||
* 在线
|
||||
*/
|
||||
ONLINE
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.ruoyi.device.domain.impl.machine.state;
|
||||
|
||||
/**
|
||||
* 舱门状态枚举
|
||||
*/
|
||||
public enum CoverState {
|
||||
/**
|
||||
* 未知状态(服务器重启后的初始状态,等待第一次心跳同步)
|
||||
*/
|
||||
UNKNOWN,
|
||||
|
||||
/**
|
||||
* 舱门已关闭
|
||||
*/
|
||||
CLOSED,
|
||||
|
||||
/**
|
||||
* 舱门已打开
|
||||
*/
|
||||
OPENED,
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.ruoyi.device.domain.impl.machine.state;
|
||||
|
||||
/**
|
||||
* 调试模式状态枚举
|
||||
*/
|
||||
public enum DebugModeState {
|
||||
/**
|
||||
* 未知状态
|
||||
*/
|
||||
UNKNOWN,
|
||||
|
||||
/**
|
||||
* 调试模式
|
||||
*/
|
||||
ENTERED,
|
||||
|
||||
/**
|
||||
* 退出调试模式
|
||||
*/
|
||||
EXITED
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package com.ruoyi.device.domain.impl.machine.state;
|
||||
|
||||
/**
|
||||
* 飞行控制模式(DRC)状态枚举
|
||||
*/
|
||||
public enum DrcState {
|
||||
/**
|
||||
* 未知状态(服务器重启后的初始状态,等待第一次心跳同步)
|
||||
*/
|
||||
UNKNOWN,
|
||||
|
||||
/**
|
||||
* 退出状态(DRC模式已退出)
|
||||
*/
|
||||
EXITED,
|
||||
|
||||
/**
|
||||
* 进入状态(已进入DRC模式)
|
||||
*/
|
||||
ENTERED,
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.ruoyi.device.domain.impl.machine.state;
|
||||
|
||||
/**
|
||||
* 无人机状态枚举
|
||||
* 分为:准备中 -> 飞行中 -> 返航 三个大状态
|
||||
*/
|
||||
public enum DroneState {
|
||||
/**
|
||||
* 未知状态(服务器重启后的初始状态,等待第一次心跳同步),同时也是离线状态
|
||||
*/
|
||||
UNKNOWN,
|
||||
|
||||
/**
|
||||
* 在线
|
||||
*/
|
||||
ONLINE,
|
||||
|
||||
/**
|
||||
* 飞行中
|
||||
*/
|
||||
FLYING,
|
||||
|
||||
/**
|
||||
* 到达目的地
|
||||
*/
|
||||
ARRIVED,
|
||||
|
||||
/**
|
||||
* 返航中
|
||||
*/
|
||||
RETURNING,
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
package com.ruoyi.device.domain.impl.machine.state;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 设备的六套大状态
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class MachineStates {
|
||||
/**
|
||||
* 无人机状态
|
||||
*/
|
||||
private DroneState droneState = DroneState.UNKNOWN;
|
||||
|
||||
/**
|
||||
* 机巢状态
|
||||
*/
|
||||
private AirportState airportState = AirportState.UNKNOWN;
|
||||
|
||||
/**
|
||||
* 舱门状态
|
||||
*/
|
||||
private CoverState coverState = CoverState.UNKNOWN;
|
||||
|
||||
/**
|
||||
* DRC状态
|
||||
*/
|
||||
private DrcState drcState = DrcState.UNKNOWN;
|
||||
|
||||
/**
|
||||
* 调试模式状态
|
||||
*/
|
||||
private DebugModeState debugModeState = DebugModeState.UNKNOWN;
|
||||
|
||||
/**
|
||||
* 急停状态
|
||||
*/
|
||||
private StopState stopState = StopState.UNKNOWN;
|
||||
|
||||
/**
|
||||
* 复制当前状态
|
||||
*/
|
||||
public MachineStates copy() {
|
||||
return new MachineStates(droneState, airportState, coverState, drcState, debugModeState, stopState);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.ruoyi.device.domain.impl.machine.state;
|
||||
|
||||
/**
|
||||
* 急停状态
|
||||
*/
|
||||
public enum StopState {
|
||||
/**
|
||||
* 未知状态(服务器重启后的初始状态,等待第一次心跳同步)
|
||||
*/
|
||||
UNKNOWN,
|
||||
|
||||
/**
|
||||
* 退出状态
|
||||
*/
|
||||
EXITED,
|
||||
|
||||
/**
|
||||
* 进入状态
|
||||
*/
|
||||
ENTERED,
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,195 @@
|
|||
package com.ruoyi.device.domain.impl.machine.statemachine;
|
||||
|
||||
|
||||
import com.ruoyi.device.domain.impl.machine.state.*;
|
||||
import com.ruoyi.device.domain.impl.machine.statemachine.store.MachineStateStore;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 设备状态管理器
|
||||
* 负责状态的业务逻辑处理和变化通知,底层存储由 MachineStateStore 实现
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class MachineStateManager {
|
||||
|
||||
/**
|
||||
* 状态存储层(支持内存、Redis等多种实现)
|
||||
*/
|
||||
private final MachineStateStore stateStore;
|
||||
|
||||
/**
|
||||
* 状态变化监听器
|
||||
*/
|
||||
private final Map<String, StateChangeListener> stateChangeListeners = new ConcurrentHashMap<>();
|
||||
public MachineStateManager(MachineStateStore stateStore) {
|
||||
this.stateStore = stateStore;
|
||||
log.info("设备状态管理器初始化完成,使用存储实现: {}", stateStore.getClass().getSimpleName());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取设备状态
|
||||
*/
|
||||
public MachineStates getStates(String sn) {
|
||||
return stateStore.getStates(sn);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置无人机状态
|
||||
*/
|
||||
public void setDroneState(String sn, DroneState newState) {
|
||||
MachineStates states = getStates(sn);
|
||||
DroneState oldState = states.getDroneState();
|
||||
|
||||
if (oldState != newState) {
|
||||
states.setDroneState(newState);
|
||||
stateStore.saveStates(sn, states); // 保存到存储层
|
||||
log.info("无人机状态变化: sn={}, {} -> {}", sn, oldState, newState);
|
||||
notifyStateChange(sn, states.copy());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置机巢状态
|
||||
*/
|
||||
public void setAirportState(String sn, AirportState newState) {
|
||||
MachineStates states = getStates(sn);
|
||||
AirportState oldState = states.getAirportState();
|
||||
|
||||
if (oldState != newState) {
|
||||
states.setAirportState(newState);
|
||||
stateStore.saveStates(sn, states); // 保存到存储层
|
||||
log.info("机巢状态变化: sn={}, {} -> {}", sn, oldState, newState);
|
||||
notifyStateChange(sn, states.copy());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置舱门状态
|
||||
*/
|
||||
public void setCoverState(String sn, CoverState newState) {
|
||||
MachineStates states = getStates(sn);
|
||||
CoverState oldState = states.getCoverState();
|
||||
|
||||
if (oldState != newState) {
|
||||
states.setCoverState(newState);
|
||||
stateStore.saveStates(sn, states); // 保存到存储层
|
||||
log.info("舱门状态变化: sn={}, {} -> {}", sn, oldState, newState);
|
||||
notifyStateChange(sn, states.copy());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置DRC状态
|
||||
*/
|
||||
public void setDrcState(String sn, DrcState newState) {
|
||||
MachineStates states = getStates(sn);
|
||||
DrcState oldState = states.getDrcState();
|
||||
|
||||
if (oldState != newState) {
|
||||
states.setDrcState(newState);
|
||||
stateStore.saveStates(sn, states); // 保存到存储层
|
||||
log.info("DRC状态变化: sn={}, {} -> {}", sn, oldState, newState);
|
||||
notifyStateChange(sn, states.copy());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量更新状态(用于心跳同步)
|
||||
* 只更新非UNKNOWN的状态,避免覆盖已有状态
|
||||
*/
|
||||
public void updateStates(String sn, MachineStates newStates) {
|
||||
updateStates(sn, newStates, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量更新状态(用于心跳同步)
|
||||
*
|
||||
* @param sn 设备SN号
|
||||
* @param newStates 新状态
|
||||
* @param forceUpdate 是否强制更新所有状态(包括UNKNOWN)
|
||||
*/
|
||||
public void updateStates(String sn, MachineStates newStates, boolean forceUpdate) {
|
||||
MachineStates currentStates = getStates(sn);
|
||||
boolean changed = false;
|
||||
|
||||
// 更新无人机状态(如果不是UNKNOWN或强制更新)
|
||||
if (forceUpdate || newStates.getDroneState() != DroneState.UNKNOWN) {
|
||||
if (currentStates.getDroneState() != newStates.getDroneState()) {
|
||||
currentStates.setDroneState(newStates.getDroneState());
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新机巢状态(如果不是UNKNOWN或强制更新)
|
||||
if (forceUpdate || newStates.getAirportState() != AirportState.UNKNOWN) {
|
||||
if (currentStates.getAirportState() != newStates.getAirportState()) {
|
||||
currentStates.setAirportState(newStates.getAirportState());
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新舱门状态(如果不是UNKNOWN或强制更新)
|
||||
if (forceUpdate || newStates.getCoverState() != CoverState.UNKNOWN) {
|
||||
if (currentStates.getCoverState() != newStates.getCoverState()) {
|
||||
currentStates.setCoverState(newStates.getCoverState());
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新DRC状态(如果不是UNKNOWN或强制更新)
|
||||
if (forceUpdate || newStates.getDrcState() != DrcState.UNKNOWN) {
|
||||
if (currentStates.getDrcState() != newStates.getDrcState()) {
|
||||
currentStates.setDrcState(newStates.getDrcState());
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
stateStore.saveStates(sn, currentStates); // 保存到存储层
|
||||
log.info("设备状态批量更新: sn={}, states={}", sn, currentStates);
|
||||
notifyStateChange(sn, currentStates.copy());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册状态变化监听器
|
||||
*/
|
||||
public void registerStateChangeListener(String listenerId, StateChangeListener listener) {
|
||||
stateChangeListeners.put(listenerId, listener);
|
||||
log.debug("注册状态变化监听器: listenerId={}", listenerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消注册状态变化监听器
|
||||
*/
|
||||
public void unregisterStateChangeListener(String listenerId) {
|
||||
stateChangeListeners.remove(listenerId);
|
||||
log.debug("取消注册状态变化监听器: listenerId={}", listenerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知状态变化
|
||||
*/
|
||||
private void notifyStateChange(String sn, MachineStates newStates) {
|
||||
for (StateChangeListener listener : stateChangeListeners.values()) {
|
||||
try {
|
||||
listener.onStateChange(sn, newStates);
|
||||
} catch (Exception e) {
|
||||
log.error("状态变化监听器执行失败: sn={}", sn, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除设备状态(设备下线时调用)
|
||||
*/
|
||||
public void removeStates(String sn) {
|
||||
stateStore.removeStates(sn);
|
||||
log.info("移除设备状态: sn={}", sn);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.ruoyi.device.domain.impl.machine.statemachine;
|
||||
|
||||
|
||||
import com.ruoyi.device.domain.impl.machine.state.MachineStates;
|
||||
|
||||
/**
|
||||
* 状态变化监听器
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface StateChangeListener {
|
||||
/**
|
||||
* 状态变化回调
|
||||
*
|
||||
* @param sn 设备SN号
|
||||
* @param newStates 新状态
|
||||
*/
|
||||
void onStateChange(String sn, MachineStates newStates);
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
package com.ruoyi.device.domain.impl.machine.statemachine.store;
|
||||
|
||||
|
||||
import com.ruoyi.device.domain.impl.machine.state.MachineStates;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 基于内存的设备状态存储实现
|
||||
* 适用于单节点部署或开发测试环境
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@ConditionalOnProperty(name = "machine.state.store.type", havingValue = "memory", matchIfMissing = true)
|
||||
public class InMemoryMachineStateStore implements MachineStateStore {
|
||||
|
||||
/**
|
||||
* SN -> 设备状态
|
||||
*/
|
||||
private final Map<String, MachineStates> stateMap = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
public MachineStates getStates(String sn) {
|
||||
return stateMap.computeIfAbsent(sn, k -> {
|
||||
log.debug("创建新的设备状态: sn={}", sn);
|
||||
return new MachineStates();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveStates(String sn, MachineStates states) {
|
||||
stateMap.put(sn, states);
|
||||
log.debug("保存设备状态到内存: sn={}", sn);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeStates(String sn) {
|
||||
stateMap.remove(sn);
|
||||
log.debug("从内存中移除设备状态: sn={}", sn);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exists(String sn) {
|
||||
return stateMap.containsKey(sn);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
package com.ruoyi.device.domain.impl.machine.statemachine.store;
|
||||
|
||||
|
||||
import com.ruoyi.device.domain.impl.machine.state.MachineStates;
|
||||
|
||||
/**
|
||||
* 设备状态存储接口
|
||||
* 提供状态的存储和获取抽象,支持多种实现(内存、Redis等)
|
||||
*/
|
||||
public interface MachineStateStore {
|
||||
|
||||
/**
|
||||
* 获取设备状态
|
||||
* 如果设备状态不存在,返回一个新的默认状态对象
|
||||
*
|
||||
* @param sn 设备SN号
|
||||
* @return 设备状态
|
||||
*/
|
||||
MachineStates getStates(String sn);
|
||||
|
||||
/**
|
||||
* 保存设备状态
|
||||
*
|
||||
* @param sn 设备SN号
|
||||
* @param states 设备状态
|
||||
*/
|
||||
void saveStates(String sn, MachineStates states);
|
||||
|
||||
/**
|
||||
* 移除设备状态
|
||||
*
|
||||
* @param sn 设备SN号
|
||||
*/
|
||||
void removeStates(String sn);
|
||||
|
||||
/**
|
||||
* 检查设备状态是否存在
|
||||
*
|
||||
* @param sn 设备SN号
|
||||
* @return 是否存在
|
||||
*/
|
||||
boolean exists(String sn);
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
package com.ruoyi.device.domain.impl.machine.statemachine.store;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import com.ruoyi.device.domain.impl.machine.state.MachineStates;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 基于 Redis 的设备状态存储实现
|
||||
* 适用于多节点部署的生产环境
|
||||
*
|
||||
* Redis 数据结构:
|
||||
* - Key: machine:state:{sn}
|
||||
* - Value: MachineStates (JSON)
|
||||
* - TTL: 86400 秒(24小时)
|
||||
*
|
||||
* 使用方式:
|
||||
* 1. 在 application.properties 中配置:machine.state.store.type=redis
|
||||
* 2. 配置 Redis 连接信息
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@ConditionalOnProperty(name = "machine.state.store.type", havingValue = "redis")
|
||||
public class RedisMachineStateStore implements MachineStateStore {
|
||||
|
||||
private final StringRedisTemplate redisTemplate;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
// Redis key 前缀
|
||||
private static final String KEY_PREFIX = "machine:state:";
|
||||
|
||||
// 配置状态的过期时间
|
||||
private static final long EXPIRE_SECONDS = 86400; // 24小时
|
||||
|
||||
public RedisMachineStateStore(StringRedisTemplate redisTemplate, ObjectMapper objectMapper) {
|
||||
this.redisTemplate = redisTemplate;
|
||||
this.objectMapper = objectMapper;
|
||||
log.info("使用 Redis 设备状态存储实现");
|
||||
}
|
||||
|
||||
@Override
|
||||
public MachineStates getStates(String sn) {
|
||||
String key = KEY_PREFIX + sn;
|
||||
String json = redisTemplate.opsForValue().get(key);
|
||||
|
||||
if (json == null) {
|
||||
log.debug("Redis 中不存在设备状态,返回默认状态: sn={}", sn);
|
||||
MachineStates states = new MachineStates();
|
||||
saveStates(sn, states);
|
||||
return states;
|
||||
}
|
||||
|
||||
try {
|
||||
MachineStates states = objectMapper.readValue(json, MachineStates.class);
|
||||
log.debug("从 Redis 获取设备状态: sn={}", sn);
|
||||
return states;
|
||||
} catch (JsonProcessingException e) {
|
||||
log.error("反序列化设备状态失败: sn={}", sn, e);
|
||||
return new MachineStates();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveStates(String sn, MachineStates states) {
|
||||
try {
|
||||
String key = KEY_PREFIX + sn;
|
||||
String json = objectMapper.writeValueAsString(states);
|
||||
redisTemplate.opsForValue().set(key, json, EXPIRE_SECONDS, TimeUnit.SECONDS);
|
||||
log.debug("保存设备状态到 Redis: sn={}", sn);
|
||||
} catch (JsonProcessingException e) {
|
||||
log.error("序列化设备状态失败: sn={}", sn, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeStates(String sn) {
|
||||
String key = KEY_PREFIX + sn;
|
||||
redisTemplate.delete(key);
|
||||
log.debug("从 Redis 中移除设备状态: sn={}", sn);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exists(String sn) {
|
||||
String key = KEY_PREFIX + sn;
|
||||
return Boolean.TRUE.equals(redisTemplate.hasKey(key));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
package com.ruoyi.device.domain.impl.machine.vendor;
|
||||
|
||||
import com.ruoyi.device.domain.impl.machine.command.CommandType;
|
||||
import com.ruoyi.device.domain.impl.machine.command.Transaction;
|
||||
import com.ruoyi.device.domain.impl.machine.state.MachineStates;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 厂家配置接口
|
||||
* 每个厂家需要实现此接口,定义其支持的命令和状态转换规则
|
||||
*/
|
||||
public interface VendorConfig {
|
||||
/**
|
||||
* 获取厂家类型
|
||||
*/
|
||||
String getVendorType();
|
||||
|
||||
/**
|
||||
* 获取厂家名称
|
||||
*/
|
||||
String getVendorName();
|
||||
|
||||
/**
|
||||
* 获取指定命令的事务定义
|
||||
*
|
||||
* @param commandType 命令类型
|
||||
* @return 事务定义,如果不支持该命令则返回null
|
||||
*/
|
||||
Transaction getTransaction(CommandType commandType);
|
||||
|
||||
/**
|
||||
* 判断在当前状态下是否可以执行指定命令
|
||||
*
|
||||
* @param currentStates 当前状态
|
||||
* @param commandType 命令类型
|
||||
* @return 是否可以执行
|
||||
*/
|
||||
boolean canExecuteCommand(MachineStates currentStates, CommandType commandType);
|
||||
|
||||
/**
|
||||
* 获取在当前状态下可以执行的命令列表
|
||||
*
|
||||
* @param currentStates 当前状态
|
||||
* @return 可执行的命令列表
|
||||
*/
|
||||
List<CommandType> getAvailableCommands(MachineStates currentStates);
|
||||
}
|
||||
119
src/main/java/com/ruoyi/device/domain/impl/machine/vendor/VendorRegistry.java
vendored
Normal file
119
src/main/java/com/ruoyi/device/domain/impl/machine/vendor/VendorRegistry.java
vendored
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
package com.ruoyi.device.domain.impl.machine.vendor;
|
||||
|
||||
|
||||
import com.ruoyi.device.domain.impl.machine.vendor.store.SnVendorMappingStore;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 厂家注册中心
|
||||
* 管理厂家配置和 SN 到厂家的映射关系
|
||||
*
|
||||
* 架构说明:
|
||||
* - 厂家配置存储在本地内存(vendorConfigs)
|
||||
* - SN 到厂家类型的映射通过 SnVendorMappingStore 存储(支持内存/Redis+MySQL)
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class VendorRegistry {
|
||||
|
||||
/**
|
||||
* 厂家类型 -> 厂家配置(存储在本地内存)
|
||||
*/
|
||||
private final Map<String, VendorConfig> vendorConfigs = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* SN 到厂家类型的映射存储层(支持内存、Redis+MySQL等多种实现)
|
||||
*/
|
||||
private final SnVendorMappingStore mappingStore;
|
||||
|
||||
public VendorRegistry(SnVendorMappingStore mappingStore) {
|
||||
this.mappingStore = mappingStore;
|
||||
log.info("厂家注册中心初始化完成,使用映射存储实现: {}", mappingStore.getClass().getSimpleName());
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册厂家配置
|
||||
*/
|
||||
public void registerVendor(VendorConfig vendorConfig) {
|
||||
vendorConfigs.put(vendorConfig.getVendorType(), vendorConfig);
|
||||
log.info("注册厂家配置: vendorType={}, vendorName={}",
|
||||
vendorConfig.getVendorType(), vendorConfig.getVendorName());
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定SN到厂家
|
||||
*
|
||||
* @deprecated 不建议直接调用此方法,应该通过数据库或配置文件管理 SN 映射关系
|
||||
*/
|
||||
@Deprecated
|
||||
public void bindSnToVendor(String sn, String vendorType) {
|
||||
if (!vendorConfigs.containsKey(vendorType)) {
|
||||
throw new IllegalArgumentException("未注册的厂家类型: " + vendorType);
|
||||
}
|
||||
mappingStore.saveMapping(sn, vendorType);
|
||||
log.debug("绑定SN到厂家: sn={}, vendorType={}", sn, vendorType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解绑SN
|
||||
*
|
||||
* @deprecated 不建议直接调用此方法,应该通过数据库或配置文件管理 SN 映射关系
|
||||
*/
|
||||
@Deprecated
|
||||
public void unbindSn(String sn) {
|
||||
mappingStore.removeMapping(sn);
|
||||
log.debug("解绑SN: sn={}", sn);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取SN对应的厂家类型
|
||||
*
|
||||
* 查询顺序(Redis+MySQL 模式):
|
||||
* 1. 先从 Redis 缓存获取
|
||||
* 2. Redis 没有则从 MySQL 数据库获取
|
||||
* 3. 获取到后存入 Redis 缓存
|
||||
*/
|
||||
public String getVendorType(String sn) {
|
||||
return mappingStore.getVendorType(sn);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取SN对应的厂家配置
|
||||
*
|
||||
* 查询顺序(Redis+MySQL 模式):
|
||||
* 1. 通过 mappingStore 获取厂家类型(Redis → MySQL → 缓存)
|
||||
* 2. 根据厂家类型获取厂家配置
|
||||
*/
|
||||
public VendorConfig getVendorConfig(String sn) {
|
||||
String vendorType = getVendorType(sn);
|
||||
if (vendorType == null) {
|
||||
return null;
|
||||
}
|
||||
return vendorConfigs.get(vendorType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据厂家类型获取厂家配置
|
||||
*/
|
||||
public VendorConfig getVendorConfigByType(String vendorType) {
|
||||
return vendorConfigs.get(vendorType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断SN是否已绑定厂家
|
||||
*/
|
||||
public boolean isSnBound(String sn) {
|
||||
return mappingStore.exists(sn);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有已注册的厂家类型
|
||||
*/
|
||||
public java.util.Set<String> getAllVendorTypes() {
|
||||
return vendorConfigs.keySet();
|
||||
}
|
||||
}
|
||||
121
src/main/java/com/ruoyi/device/domain/impl/machine/vendor/dji/DjiVendorConfig.java
vendored
Normal file
121
src/main/java/com/ruoyi/device/domain/impl/machine/vendor/dji/DjiVendorConfig.java
vendored
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
package com.ruoyi.device.domain.impl.machine.vendor.dji;
|
||||
|
||||
import com.ruoyi.device.domain.impl.machine.command.CommandType;
|
||||
import com.ruoyi.device.domain.impl.machine.command.Transaction;
|
||||
import com.ruoyi.device.domain.impl.machine.state.*;
|
||||
import com.ruoyi.device.domain.impl.machine.vendor.VendorConfig;
|
||||
import com.ruoyi.device.domain.impl.machine.vendor.dji.instruction.*;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 大疆无人机厂家配置
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class DjiVendorConfig implements VendorConfig {
|
||||
|
||||
private final Map<CommandType, Transaction> transactionMap = new HashMap<>();
|
||||
|
||||
public DjiVendorConfig() {
|
||||
initTransactions();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getVendorType() {
|
||||
return "DJI";
|
||||
}
|
||||
|
||||
@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();
|
||||
CoverState coverState = currentStates.getCoverState();
|
||||
DebugModeState debugModeState = currentStates.getDebugModeState();
|
||||
StopState stopState = currentStates.getStopState();
|
||||
|
||||
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() {
|
||||
// 起飞命令
|
||||
Transaction takeOffTransaction = new Transaction("起飞", CommandType.TAKE_OFF)
|
||||
.root(new DjiTakeOffInstruction())
|
||||
.setTimeout(10000);
|
||||
transactionMap.put(takeOffTransaction.getCommandType(), takeOffTransaction);
|
||||
|
||||
/**
|
||||
* 开仓命令 Transaction
|
||||
* 流程说明:
|
||||
* 1. root: 检查是否已在调试模式
|
||||
* - 成功:直接执行开仓命令
|
||||
* - 失败:先开启调试模式,再执行开仓命令
|
||||
*/
|
||||
// 创建检查调试模式的指令(root)
|
||||
DjiCheckDebugModeInstruction checkDebugMode = new DjiCheckDebugModeInstruction();
|
||||
|
||||
// 创建开仓指令(成功分支)
|
||||
DjiOpenCoverInstruction openCoverAfterCheck = new DjiOpenCoverInstruction();
|
||||
|
||||
// 创建开启调试模式的指令(失败分支)
|
||||
DjiEnableDebugModeInstruction enableDebugMode = new DjiEnableDebugModeInstruction();
|
||||
|
||||
// 创建开仓指令(失败分支的子指令)
|
||||
DjiOpenCoverInstruction openCoverAfterEnable = new DjiOpenCoverInstruction();
|
||||
|
||||
// 构建指令树
|
||||
checkDebugMode
|
||||
.onSuccess(openCoverAfterCheck) // 如果已在调试模式,直接开仓
|
||||
.onFailure(enableDebugMode // 如果不在调试模式,先开启调试模式
|
||||
.onSuccess(openCoverAfterEnable)); // 开启调试模式成功后,再开仓
|
||||
|
||||
Transaction openCoverTransaction = new Transaction("开仓", CommandType.OPEN_COVER)
|
||||
.root(checkDebugMode)
|
||||
.setTimeout(80000); // 总超时时间80秒
|
||||
transactionMap.put(openCoverTransaction.getCommandType(), openCoverTransaction);
|
||||
|
||||
log.info("大疆厂家配置初始化完成,共配置{}个命令", transactionMap.size());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
package com.ruoyi.device.domain.impl.machine.vendor.dji.instruction;
|
||||
|
||||
import com.ruoyi.device.domain.impl.machine.instruction.AbstractInstruction;
|
||||
import com.ruoyi.device.domain.impl.machine.instruction.CallbackConfig;
|
||||
import com.ruoyi.device.domain.impl.machine.instruction.InstructionContext;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* 大疆取消指点指令
|
||||
*/
|
||||
@Slf4j
|
||||
public class DjiCancelPointInstruction extends AbstractInstruction {
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "DJI_CANCEL_POINT";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void executeRemoteCall(InstructionContext context) throws Exception {
|
||||
String sn = context.getSn();
|
||||
log.info("发送大疆取消指点指令: sn={}", sn);
|
||||
|
||||
String topic = "dji/" + sn + "/command";
|
||||
String payload = "{\"cmd\":\"cancelPoint\"}";
|
||||
log.debug("MQTT发送: topic={}, payload={}", topic, payload);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CallbackConfig getMethodCallbackConfig(InstructionContext context) {
|
||||
String sn = context.getSn();
|
||||
|
||||
return CallbackConfig.builder()
|
||||
.topic("dji/" + sn + "/response")
|
||||
.fieldPath("cmd")
|
||||
.expectedValue("cancelPoint")
|
||||
.timeoutMs(10000)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CallbackConfig getStateCallbackConfig(InstructionContext context) {
|
||||
String sn = context.getSn();
|
||||
|
||||
return CallbackConfig.builder()
|
||||
.topic("dji/" + sn + "/state")
|
||||
.fieldPath("droneState")
|
||||
.expectedValue("POINT_CANCELLED")
|
||||
.timeoutMs(30000)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getTimeoutMs() {
|
||||
return 30000;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
package com.ruoyi.device.domain.impl.machine.vendor.dji.instruction;
|
||||
|
||||
|
||||
import com.ruoyi.device.domain.impl.machine.instruction.AbstractInstruction;
|
||||
import com.ruoyi.device.domain.impl.machine.instruction.CallbackConfig;
|
||||
import com.ruoyi.device.domain.impl.machine.instruction.InstructionContext;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* 大疆检查调试模式指令
|
||||
* 用于判断设备是否已经进入调试模式
|
||||
*/
|
||||
@Slf4j
|
||||
public class DjiCheckDebugModeInstruction extends AbstractInstruction {
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "DJI_CHECK_DEBUG_MODE";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void executeRemoteCall(InstructionContext context) throws Exception {
|
||||
String sn = context.getSn();
|
||||
log.info("检查大疆设备调试模式状态: sn={}", sn);
|
||||
// 不需要发送命令,只需要等待状态回调
|
||||
}
|
||||
|
||||
@Override
|
||||
public CallbackConfig getMethodCallbackConfig(InstructionContext context) {
|
||||
// 不需要方法回调
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CallbackConfig getStateCallbackConfig(InstructionContext context) {
|
||||
String sn = context.getSn();
|
||||
|
||||
// 等待设备状态回调,判断是否在调试模式
|
||||
return CallbackConfig.builder()
|
||||
.topic("dji/" + sn + "/state")
|
||||
.fieldPath("debugMode")
|
||||
.expectedValue("ENABLED")
|
||||
.timeoutMs(3000) // 3秒超时,如果没有收到说明不在调试模式
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getTimeoutMs() {
|
||||
return 3000;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
package com.ruoyi.device.domain.impl.machine.vendor.dji.instruction;
|
||||
|
||||
|
||||
import com.ruoyi.device.domain.impl.machine.instruction.AbstractInstruction;
|
||||
import com.ruoyi.device.domain.impl.machine.instruction.CallbackConfig;
|
||||
import com.ruoyi.device.domain.impl.machine.instruction.InstructionContext;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* 大疆关闭舱门指令
|
||||
*/
|
||||
@Slf4j
|
||||
public class DjiCloseCoverInstruction extends AbstractInstruction {
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "DJI_CLOSE_COVER";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void executeRemoteCall(InstructionContext context) throws Exception {
|
||||
String sn = context.getSn();
|
||||
log.info("发送大疆关闭舱门指令: sn={}", sn);
|
||||
|
||||
String topic = "dji/" + sn + "/command";
|
||||
String payload = "{\"cmd\":\"closeCover\"}";
|
||||
log.debug("MQTT发送: topic={}, payload={}", topic, payload);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CallbackConfig getMethodCallbackConfig(InstructionContext context) {
|
||||
String sn = context.getSn();
|
||||
|
||||
return CallbackConfig.builder()
|
||||
.topic("dji/" + sn + "/response")
|
||||
.fieldPath("cmd")
|
||||
.expectedValue("closeCover")
|
||||
.timeoutMs(10000)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CallbackConfig getStateCallbackConfig(InstructionContext context) {
|
||||
String sn = context.getSn();
|
||||
|
||||
return CallbackConfig.builder()
|
||||
.topic("dji/" + sn + "/state")
|
||||
.fieldPath("coverState")
|
||||
.expectedValue("CLOSED")
|
||||
.timeoutMs(60000)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getTimeoutMs() {
|
||||
return 60000;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
package com.ruoyi.device.domain.impl.machine.vendor.dji.instruction;
|
||||
|
||||
|
||||
import com.ruoyi.device.domain.impl.machine.instruction.AbstractInstruction;
|
||||
import com.ruoyi.device.domain.impl.machine.instruction.CallbackConfig;
|
||||
import com.ruoyi.device.domain.impl.machine.instruction.InstructionContext;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* 大疆急停指令
|
||||
*/
|
||||
@Slf4j
|
||||
public class DjiEmergencyStopInstruction extends AbstractInstruction {
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "DJI_EMERGENCY_STOP";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void executeRemoteCall(InstructionContext context) throws Exception {
|
||||
String sn = context.getSn();
|
||||
log.info("发送大疆急停指令: sn={}", sn);
|
||||
|
||||
String topic = "dji/" + sn + "/command";
|
||||
String payload = "{\"cmd\":\"emergencyStop\"}";
|
||||
log.debug("MQTT发送: topic={}, payload={}", topic, payload);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CallbackConfig getMethodCallbackConfig(InstructionContext context) {
|
||||
String sn = context.getSn();
|
||||
|
||||
return CallbackConfig.builder()
|
||||
.topic("dji/" + sn + "/response")
|
||||
.fieldPath("cmd")
|
||||
.expectedValue("emergencyStop")
|
||||
.timeoutMs(5000)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CallbackConfig getStateCallbackConfig(InstructionContext context) {
|
||||
String sn = context.getSn();
|
||||
|
||||
return CallbackConfig.builder()
|
||||
.topic("dji/" + sn + "/state")
|
||||
.fieldPath("droneState")
|
||||
.customPredicate(state -> {
|
||||
// 急停状态可能是 EMERGENCY_STOP 或 RETURN_EMERGENCY_STOP
|
||||
return "EMERGENCY_STOP".equals(state) || "RETURN_EMERGENCY_STOP".equals(state);
|
||||
})
|
||||
.timeoutMs(30000)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getTimeoutMs() {
|
||||
return 30000;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
package com.ruoyi.device.domain.impl.machine.vendor.dji.instruction;
|
||||
|
||||
|
||||
import com.ruoyi.device.domain.impl.machine.instruction.AbstractInstruction;
|
||||
import com.ruoyi.device.domain.impl.machine.instruction.CallbackConfig;
|
||||
import com.ruoyi.device.domain.impl.machine.instruction.InstructionContext;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* 大疆开启调试模式指令
|
||||
*/
|
||||
@Slf4j
|
||||
public class DjiEnableDebugModeInstruction extends AbstractInstruction {
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "DJI_ENABLE_DEBUG_MODE";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void executeRemoteCall(InstructionContext context) throws Exception {
|
||||
String sn = context.getSn();
|
||||
log.info("发送大疆开启调试模式指令: sn={}", sn);
|
||||
|
||||
String topic = "dji/" + sn + "/command";
|
||||
String payload = "{\"cmd\":\"enableDebugMode\"}";
|
||||
|
||||
context.getMqttClient().sendMessage(topic, payload);
|
||||
log.debug("MQTT发送成功: topic={}, payload={}", topic, payload);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CallbackConfig getMethodCallbackConfig(InstructionContext context) {
|
||||
String sn = context.getSn();
|
||||
|
||||
// 等待开启调试模式命令的ACK响应
|
||||
return CallbackConfig.builder()
|
||||
.topic("dji/" + sn + "/response")
|
||||
.fieldPath("cmd")
|
||||
.expectedValue("enableDebugMode")
|
||||
.timeoutMs(10000)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CallbackConfig getStateCallbackConfig(InstructionContext context) {
|
||||
// 不需要状态回调,只要收到ACK就认为命令发送成功
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getTimeoutMs() {
|
||||
return 10000;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
package com.ruoyi.device.domain.impl.machine.vendor.dji.instruction;
|
||||
|
||||
|
||||
import com.ruoyi.device.domain.impl.machine.instruction.AbstractInstruction;
|
||||
import com.ruoyi.device.domain.impl.machine.instruction.CallbackConfig;
|
||||
import com.ruoyi.device.domain.impl.machine.instruction.InstructionContext;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* 大疆降落指令
|
||||
*/
|
||||
@Slf4j
|
||||
public class DjiLandInstruction extends AbstractInstruction {
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "DJI_LAND";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void executeRemoteCall(InstructionContext context) throws Exception {
|
||||
String sn = context.getSn();
|
||||
log.info("发送大疆降落指令: sn={}", sn);
|
||||
|
||||
// TODO: 实际的MQTT发送逻辑
|
||||
String topic = "dji/" + sn + "/command";
|
||||
String payload = "{\"cmd\":\"land\"}";
|
||||
log.debug("MQTT发送: topic={}, payload={}", topic, payload);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CallbackConfig getMethodCallbackConfig(InstructionContext context) {
|
||||
String sn = context.getSn();
|
||||
|
||||
return CallbackConfig.builder()
|
||||
.topic("dji/" + sn + "/response")
|
||||
.fieldPath("cmd")
|
||||
.expectedValue("land")
|
||||
.timeoutMs(10000)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CallbackConfig getStateCallbackConfig(InstructionContext context) {
|
||||
String sn = context.getSn();
|
||||
|
||||
return CallbackConfig.builder()
|
||||
.topic("dji/" + sn + "/state")
|
||||
.fieldPath("droneState")
|
||||
.expectedValue("PREPARING")
|
||||
.timeoutMs(60000)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getTimeoutMs() {
|
||||
return 90000;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
package com.ruoyi.device.domain.impl.machine.vendor.dji.instruction;
|
||||
|
||||
|
||||
import com.ruoyi.device.domain.impl.machine.instruction.AbstractInstruction;
|
||||
import com.ruoyi.device.domain.impl.machine.instruction.CallbackConfig;
|
||||
import com.ruoyi.device.domain.impl.machine.instruction.InstructionContext;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* 大疆打开舱门指令
|
||||
*/
|
||||
@Slf4j
|
||||
public class DjiOpenCoverInstruction extends AbstractInstruction {
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "DJI_OPEN_COVER";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void executeRemoteCall(InstructionContext context) throws Exception {
|
||||
String sn = context.getSn();
|
||||
log.info("发送大疆打开舱门指令: sn={}", sn);
|
||||
|
||||
String topic = "dji/" + sn + "/command";
|
||||
String payload = "{\"cmd\":\"openCover\"}";
|
||||
|
||||
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("dji/" + sn + "/response")
|
||||
.fieldPath("cmd")
|
||||
.expectedValue("openCover")
|
||||
.timeoutMs(10000)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CallbackConfig getStateCallbackConfig(InstructionContext context) {
|
||||
String sn = context.getSn();
|
||||
|
||||
return CallbackConfig.builder()
|
||||
.topic("dji/" + sn + "/state")
|
||||
.fieldPath("coverState")
|
||||
.expectedValue("OPENED")
|
||||
.timeoutMs(60000)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getTimeoutMs() {
|
||||
return 60000;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
package com.ruoyi.device.domain.impl.machine.vendor.dji.instruction;
|
||||
|
||||
|
||||
import com.ruoyi.device.domain.impl.machine.instruction.AbstractInstruction;
|
||||
import com.ruoyi.device.domain.impl.machine.instruction.CallbackConfig;
|
||||
import com.ruoyi.device.domain.impl.machine.instruction.InstructionContext;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* 大疆指点飞行指令
|
||||
*/
|
||||
@Slf4j
|
||||
public class DjiPointFlyInstruction extends AbstractInstruction {
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "DJI_POINT_FLY";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void executeRemoteCall(InstructionContext context) throws Exception {
|
||||
String sn = context.getSn();
|
||||
Object latitude = context.getCommandParam("latitude");
|
||||
Object longitude = context.getCommandParam("longitude");
|
||||
Object altitude = context.getCommandParam("altitude");
|
||||
|
||||
log.info("发送大疆指点飞行指令: sn={}, lat={}, lon={}, alt={}", sn, latitude, longitude, altitude);
|
||||
|
||||
String topic = "dji/" + sn + "/command";
|
||||
String payload = String.format("{\"cmd\":\"pointFly\",\"latitude\":%s,\"longitude\":%s,\"altitude\":%s}",
|
||||
latitude, longitude, altitude);
|
||||
log.debug("MQTT发送: topic={}, payload={}", topic, payload);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CallbackConfig getMethodCallbackConfig(InstructionContext context) {
|
||||
String sn = context.getSn();
|
||||
|
||||
return CallbackConfig.builder()
|
||||
.topic("dji/" + sn + "/response")
|
||||
.fieldPath("cmd")
|
||||
.expectedValue("pointFly")
|
||||
.timeoutMs(10000)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CallbackConfig getStateCallbackConfig(InstructionContext context) {
|
||||
String sn = context.getSn();
|
||||
|
||||
return CallbackConfig.builder()
|
||||
.topic("dji/" + sn + "/state")
|
||||
.fieldPath("droneState")
|
||||
.expectedValue("POINT_FLYING")
|
||||
.timeoutMs(30000)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getTimeoutMs() {
|
||||
return 90000;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
package com.ruoyi.device.domain.impl.machine.vendor.dji.instruction;
|
||||
|
||||
|
||||
import com.ruoyi.device.domain.impl.machine.instruction.AbstractInstruction;
|
||||
import com.ruoyi.device.domain.impl.machine.instruction.CallbackConfig;
|
||||
import com.ruoyi.device.domain.impl.machine.instruction.InstructionContext;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* 大疆继续飞行指令
|
||||
*/
|
||||
@Slf4j
|
||||
public class DjiResumeFlightInstruction extends AbstractInstruction {
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "DJI_RESUME_FLIGHT";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void executeRemoteCall(InstructionContext context) throws Exception {
|
||||
String sn = context.getSn();
|
||||
log.info("发送大疆继续飞行指令: sn={}", sn);
|
||||
|
||||
String topic = "dji/" + sn + "/command";
|
||||
String payload = "{\"cmd\":\"resumeFlight\"}";
|
||||
log.debug("MQTT发送: topic={}, payload={}", topic, payload);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CallbackConfig getMethodCallbackConfig(InstructionContext context) {
|
||||
String sn = context.getSn();
|
||||
|
||||
return CallbackConfig.builder()
|
||||
.topic("dji/" + sn + "/response")
|
||||
.fieldPath("cmd")
|
||||
.expectedValue("resumeFlight")
|
||||
.timeoutMs(10000)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CallbackConfig getStateCallbackConfig(InstructionContext context) {
|
||||
String sn = context.getSn();
|
||||
|
||||
return CallbackConfig.builder()
|
||||
.topic("dji/" + sn + "/state")
|
||||
.fieldPath("droneState")
|
||||
.customPredicate(state -> {
|
||||
// 继续飞行后可能变为 FLYING 或 RETURNING
|
||||
return "FLYING".equals(state) || "RETURNING".equals(state);
|
||||
})
|
||||
.timeoutMs(30000)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getTimeoutMs() {
|
||||
return 60000;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
package com.ruoyi.device.domain.impl.machine.vendor.dji.instruction;
|
||||
|
||||
|
||||
import com.ruoyi.device.domain.impl.machine.instruction.AbstractInstruction;
|
||||
import com.ruoyi.device.domain.impl.machine.instruction.CallbackConfig;
|
||||
import com.ruoyi.device.domain.impl.machine.instruction.InstructionContext;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* 大疆返航指令
|
||||
*/
|
||||
@Slf4j
|
||||
public class DjiReturnHomeInstruction extends AbstractInstruction {
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "DJI_RETURN_HOME";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void executeRemoteCall(InstructionContext context) throws Exception {
|
||||
String sn = context.getSn();
|
||||
log.info("发送大疆返航指令: sn={}", sn);
|
||||
|
||||
String topic = "dji/" + sn + "/command";
|
||||
String payload = "{\"cmd\":\"returnHome\"}";
|
||||
log.debug("MQTT发送: topic={}, payload={}", topic, payload);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CallbackConfig getMethodCallbackConfig(InstructionContext context) {
|
||||
String sn = context.getSn();
|
||||
|
||||
return CallbackConfig.builder()
|
||||
.topic("dji/" + sn + "/response")
|
||||
.fieldPath("cmd")
|
||||
.expectedValue("returnHome")
|
||||
.timeoutMs(10000)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CallbackConfig getStateCallbackConfig(InstructionContext context) {
|
||||
String sn = context.getSn();
|
||||
|
||||
return CallbackConfig.builder()
|
||||
.topic("dji/" + sn + "/state")
|
||||
.fieldPath("droneState")
|
||||
.expectedValue("RETURNING")
|
||||
.timeoutMs(60000)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getTimeoutMs() {
|
||||
return 120000;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
package com.ruoyi.device.domain.impl.machine.vendor.dji.instruction;
|
||||
|
||||
|
||||
import com.ruoyi.device.domain.impl.machine.instruction.AbstractInstruction;
|
||||
import com.ruoyi.device.domain.impl.machine.instruction.CallbackConfig;
|
||||
import com.ruoyi.device.domain.impl.machine.instruction.InstructionContext;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* 大疆开始航线任务指令
|
||||
*/
|
||||
@Slf4j
|
||||
public class DjiStartMissionInstruction extends AbstractInstruction {
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "DJI_START_MISSION";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void executeRemoteCall(InstructionContext context) throws Exception {
|
||||
String sn = context.getSn();
|
||||
Object missionId = context.getCommandParam("missionId");
|
||||
|
||||
log.info("发送大疆开始航线任务指令: sn={}, missionId={}", sn, missionId);
|
||||
|
||||
String topic = "dji/" + sn + "/command";
|
||||
String payload = String.format("{\"cmd\":\"startMission\",\"missionId\":\"%s\"}", missionId);
|
||||
log.debug("MQTT发送: topic={}, payload={}", topic, payload);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CallbackConfig getMethodCallbackConfig(InstructionContext context) {
|
||||
String sn = context.getSn();
|
||||
|
||||
return CallbackConfig.builder()
|
||||
.topic("dji/" + sn + "/response")
|
||||
.fieldPath("cmd")
|
||||
.expectedValue("startMission")
|
||||
.timeoutMs(10000)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CallbackConfig getStateCallbackConfig(InstructionContext context) {
|
||||
String sn = context.getSn();
|
||||
|
||||
return CallbackConfig.builder()
|
||||
.topic("dji/" + sn + "/state")
|
||||
.fieldPath("droneState")
|
||||
.expectedValue("FLYING")
|
||||
.timeoutMs(60000)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getTimeoutMs() {
|
||||
return 120000;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
package com.ruoyi.device.domain.impl.machine.vendor.dji.instruction;
|
||||
|
||||
|
||||
import com.ruoyi.device.domain.impl.machine.instruction.AbstractInstruction;
|
||||
import com.ruoyi.device.domain.impl.machine.instruction.CallbackConfig;
|
||||
import com.ruoyi.device.domain.impl.machine.instruction.InstructionContext;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* 大疆起飞指令
|
||||
*/
|
||||
@Slf4j
|
||||
public class DjiTakeOffInstruction extends AbstractInstruction {
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "DJI_TAKE_OFF";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void executeRemoteCall(InstructionContext context) throws Exception {
|
||||
String sn = context.getSn();
|
||||
log.info("发送大疆起飞指令: sn={}", sn);
|
||||
|
||||
// 通过 context 获取 MqttClient 并发送消息
|
||||
String topic = "dji/" + sn + "/command";
|
||||
String payload = "{\"cmd\":\"takeoff\"}";
|
||||
|
||||
context.getMqttClient().sendMessage(topic, payload);
|
||||
log.debug("MQTT发送成功: topic={}, payload={}", topic, payload);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CallbackConfig getMethodCallbackConfig(InstructionContext context) {
|
||||
// return null;
|
||||
String sn = context.getSn();
|
||||
|
||||
// 方法回调:等待起飞指令的ACK响应
|
||||
return CallbackConfig.builder()
|
||||
.topic("dji/" + sn + "/response")
|
||||
.fieldPath("data.result")
|
||||
.expectedValue("takeoff")
|
||||
.timeoutMs(10000) // 10秒超时
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CallbackConfig getStateCallbackConfig(InstructionContext context) {
|
||||
// return null;
|
||||
String sn = context.getSn();
|
||||
|
||||
// 状态回调:等待无人机状态变为飞行中
|
||||
return CallbackConfig.builder()
|
||||
.topic("dji/" + sn + "/state")
|
||||
.fieldPath("droneState")
|
||||
.expectedValue("FLYING")
|
||||
.timeoutMs(10000) // 10秒超时
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getTimeoutMs() {
|
||||
return 10000; // 10秒总超时
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
package com.ruoyi.device.domain.impl.machine.vendor.repository;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 基于内存的 SN 到厂家类型映射持久化存储实现
|
||||
* 适用于单节点部署或开发测试环境
|
||||
*
|
||||
* 注意:这是默认实现,当没有配置数据库时使用
|
||||
*
|
||||
* 使用方式:
|
||||
* 1. 不配置 machine.sn.repository.type(默认使用内存实现)
|
||||
* 2. 或在 application.properties 中配置:machine.sn.repository.type=memory
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@ConditionalOnProperty(name = "machine.sn.repository.type", havingValue = "memory", matchIfMissing = true)
|
||||
public class InMemorySnVendorMappingRepository implements SnVendorMappingRepository {
|
||||
|
||||
/**
|
||||
* SN -> 厂家类型(模拟数据库存储)
|
||||
*/
|
||||
private final Map<String, String> dataStore = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
public String findVendorTypeBySn(String sn) {
|
||||
String vendorType = dataStore.get(sn);
|
||||
log.debug("从内存数据库查询 SN 映射: sn={}, vendorType={}", sn, vendorType);
|
||||
return vendorType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(String sn, String vendorType) {
|
||||
dataStore.put(sn, vendorType);
|
||||
log.debug("保存 SN 映射到内存数据库: sn={}, vendorType={}", sn, vendorType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String sn) {
|
||||
dataStore.remove(sn);
|
||||
log.debug("从内存数据库删除 SN 映射: sn={}", sn);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exists(String sn) {
|
||||
return dataStore.containsKey(sn);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
package com.ruoyi.device.domain.impl.machine.vendor.repository;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 基于 MySQL 的 SN 到厂家类型映射持久化存储实现
|
||||
* 适用于生产环境
|
||||
*
|
||||
* MySQL 表结构(示例):
|
||||
* CREATE TABLE sn_vendor_mapping (
|
||||
* sn VARCHAR(64) PRIMARY KEY COMMENT '设备SN号',
|
||||
* vendor_type VARCHAR(32) NOT NULL COMMENT '厂家类型',
|
||||
* created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
* updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
* INDEX idx_vendor_type (vendor_type)
|
||||
* ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='SN到厂家类型映射表';
|
||||
*
|
||||
* 使用方式:
|
||||
* 1. 在 application.properties 中配置:machine.sn.repository.type=mysql
|
||||
* 2. 配置 MySQL 数据源
|
||||
* 3. 创建上述表结构
|
||||
* 4. 实现下面的 CRUD 方法
|
||||
*
|
||||
* 注意:当前为空实现,需要在连接数据库后完善
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@ConditionalOnProperty(name = "machine.sn.repository.type", havingValue = "mysql")
|
||||
public class MysqlSnVendorMappingRepository implements SnVendorMappingRepository {
|
||||
|
||||
// TODO: 注入 JdbcTemplate 或 MyBatis Mapper
|
||||
// private final JdbcTemplate jdbcTemplate;
|
||||
//
|
||||
// public MysqlSnVendorMappingRepository(JdbcTemplate jdbcTemplate) {
|
||||
// this.jdbcTemplate = jdbcTemplate;
|
||||
// }
|
||||
|
||||
public MysqlSnVendorMappingRepository() {
|
||||
log.warn("使用 MySQL SN 映射持久化存储实现,但当前为空实现,请在连接数据库后完善");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String findVendorTypeBySn(String sn) {
|
||||
// TODO: 实现从 MySQL 查询
|
||||
// try {
|
||||
// return jdbcTemplate.queryForObject(
|
||||
// "SELECT vendor_type FROM sn_vendor_mapping WHERE sn = ?",
|
||||
// String.class,
|
||||
// sn
|
||||
// );
|
||||
// } catch (EmptyResultDataAccessException e) {
|
||||
// log.debug("MySQL 中不存在 SN 映射: sn={}", sn);
|
||||
// return null;
|
||||
// }
|
||||
|
||||
log.warn("MySQL SN 映射持久化存储未实现,返回 null: sn={}", sn);
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(String sn, String vendorType) {
|
||||
// TODO: 实现保存到 MySQL
|
||||
// jdbcTemplate.update(
|
||||
// "INSERT INTO sn_vendor_mapping (sn, vendor_type) VALUES (?, ?) " +
|
||||
// "ON DUPLICATE KEY UPDATE vendor_type = ?, updated_at = CURRENT_TIMESTAMP",
|
||||
// sn, vendorType, vendorType
|
||||
// );
|
||||
// log.debug("保存 SN 映射到 MySQL: sn={}, vendorType={}", sn, vendorType);
|
||||
|
||||
log.warn("MySQL SN 映射持久化存储未实现,跳过保存: sn={}, vendorType={}", sn, vendorType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String sn) {
|
||||
// TODO: 实现从 MySQL 删除
|
||||
// jdbcTemplate.update("DELETE FROM sn_vendor_mapping WHERE sn = ?", sn);
|
||||
// log.debug("从 MySQL 删除 SN 映射: sn={}", sn);
|
||||
|
||||
log.warn("MySQL SN 映射持久化存储未实现,跳过删除: sn={}", sn);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exists(String sn) {
|
||||
// TODO: 实现检查 MySQL 中是否存在
|
||||
// Integer count = jdbcTemplate.queryForObject(
|
||||
// "SELECT COUNT(*) FROM sn_vendor_mapping WHERE sn = ?",
|
||||
// Integer.class,
|
||||
// sn
|
||||
// );
|
||||
// return count != null && count > 0;
|
||||
|
||||
log.warn("MySQL SN 映射持久化存储未实现,返回 false: sn={}", sn);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
package com.ruoyi.device.domain.impl.machine.vendor.repository;
|
||||
|
||||
/**
|
||||
* SN 到厂家类型映射的持久化存储接口
|
||||
* 提供数据库层的 CRUD 操作抽象,支持多种实现(内存、MySQL等)
|
||||
*
|
||||
* 职责说明:
|
||||
* - 这是持久化层(数据库层)的抽象
|
||||
* - 与 SnVendorMappingStore 的区别:
|
||||
* - SnVendorMappingStore:包含缓存逻辑(Redis + Repository)
|
||||
* - SnVendorMappingRepository:纯粹的持久化存储(数据库)
|
||||
*/
|
||||
public interface SnVendorMappingRepository {
|
||||
|
||||
/**
|
||||
* 从持久化存储中查询 SN 对应的厂家类型
|
||||
*
|
||||
* @param sn 设备SN号
|
||||
* @return 厂家类型,如果不存在返回 null
|
||||
*/
|
||||
String findVendorTypeBySn(String sn);
|
||||
|
||||
/**
|
||||
* 保存 SN 到厂家类型的映射到持久化存储
|
||||
* 如果已存在则更新
|
||||
*
|
||||
* @param sn 设备SN号
|
||||
* @param vendorType 厂家类型
|
||||
*/
|
||||
void save(String sn, String vendorType);
|
||||
|
||||
/**
|
||||
* 从持久化存储中删除 SN 的映射关系
|
||||
*
|
||||
* @param sn 设备SN号
|
||||
*/
|
||||
void delete(String sn);
|
||||
|
||||
/**
|
||||
* 检查持久化存储中是否存在 SN 的映射关系
|
||||
*
|
||||
* @param sn 设备SN号
|
||||
* @return 是否存在
|
||||
*/
|
||||
boolean exists(String sn);
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
package com.ruoyi.device.domain.impl.machine.vendor.store;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 基于内存的 SN 到厂家类型映射存储实现
|
||||
* 适用于单节点部署或开发测试环境
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@ConditionalOnProperty(name = "machine.state.store.type", havingValue = "memory", matchIfMissing = true)
|
||||
public class InMemorySnVendorMappingStore implements SnVendorMappingStore {
|
||||
|
||||
/**
|
||||
* SN -> 厂家类型
|
||||
*/
|
||||
private final Map<String, String> snToVendorMap = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
public String getVendorType(String sn) {
|
||||
String vendorType = snToVendorMap.get(sn);
|
||||
log.debug("从内存获取 SN 映射: sn={}, vendorType={}", sn, vendorType);
|
||||
return vendorType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveMapping(String sn, String vendorType) {
|
||||
snToVendorMap.put(sn, vendorType);
|
||||
log.debug("保存 SN 映射到内存: sn={}, vendorType={}", sn, vendorType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeMapping(String sn) {
|
||||
snToVendorMap.remove(sn);
|
||||
log.debug("从内存中移除 SN 映射: sn={}", sn);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exists(String sn) {
|
||||
return snToVendorMap.containsKey(sn);
|
||||
}
|
||||
}
|
||||
117
src/main/java/com/ruoyi/device/domain/impl/machine/vendor/store/RedisSnVendorMappingStore.java
vendored
Normal file
117
src/main/java/com/ruoyi/device/domain/impl/machine/vendor/store/RedisSnVendorMappingStore.java
vendored
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
package com.ruoyi.device.domain.impl.machine.vendor.store;
|
||||
|
||||
|
||||
import com.ruoyi.device.domain.impl.machine.vendor.repository.SnVendorMappingRepository;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 基于 Redis + Repository 的 SN 到厂家类型映射存储实现
|
||||
* 适用于多节点部署的生产环境
|
||||
*
|
||||
* 架构说明:
|
||||
* 1. Redis 作为缓存层,提供快速查询
|
||||
* 2. Repository 作为持久化层(支持内存/MySQL等实现)
|
||||
* 3. 查询流程:Redis → Repository → 回写 Redis
|
||||
*
|
||||
* Redis 数据结构:
|
||||
* - Key: machine:sn:vendor:{sn}
|
||||
* - Value: {vendorType}
|
||||
* - TTL: 86400 秒(24小时)
|
||||
*
|
||||
* 使用方式:
|
||||
* 1. 在 application.properties 中配置:machine.state.store.type=redis
|
||||
* 2. 配置 Redis 连接信息
|
||||
* 3. Repository 会根据配置自动选择实现(内存/MySQL)
|
||||
* 4. 实现 Redis 的缓存逻辑
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@ConditionalOnProperty(name = "machine.state.store.type", havingValue = "redis")
|
||||
public class RedisSnVendorMappingStore implements SnVendorMappingStore {
|
||||
|
||||
private final StringRedisTemplate redisTemplate;
|
||||
|
||||
/**
|
||||
* 持久化存储层(支持内存/MySQL等实现)
|
||||
*/
|
||||
private final SnVendorMappingRepository repository;
|
||||
|
||||
// Redis key 前缀
|
||||
private static final String KEY_PREFIX = "machine:sn:vendor:";
|
||||
|
||||
// 配置缓存过期时间
|
||||
private static final long CACHE_EXPIRE_SECONDS = 86400; // 24小时
|
||||
|
||||
public RedisSnVendorMappingStore(StringRedisTemplate redisTemplate,
|
||||
SnVendorMappingRepository repository) {
|
||||
this.redisTemplate = redisTemplate;
|
||||
this.repository = repository;
|
||||
log.info("使用 Redis+Repository SN 映射存储实现");
|
||||
log.info("持久化层实现: {}", repository.getClass().getSimpleName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getVendorType(String sn) {
|
||||
// 1. 先从 Redis 缓存获取
|
||||
String key = KEY_PREFIX + sn;
|
||||
String vendorType = redisTemplate.opsForValue().get(key);
|
||||
if (vendorType != null) {
|
||||
log.debug("从 Redis 缓存获取 SN 映射: sn={}, vendorType={}", sn, vendorType);
|
||||
return vendorType;
|
||||
}
|
||||
|
||||
// 2. Redis 没有,从 Repository 持久化层获取
|
||||
vendorType = repository.findVendorTypeBySn(sn);
|
||||
|
||||
if (vendorType != null) {
|
||||
// 3. 获取到后存入 Redis 缓存
|
||||
redisTemplate.opsForValue().set(key, vendorType, CACHE_EXPIRE_SECONDS, TimeUnit.SECONDS);
|
||||
log.debug("从 Repository 获取 SN 映射并缓存到 Redis: sn={}, vendorType={}", sn, vendorType);
|
||||
return vendorType;
|
||||
}
|
||||
|
||||
log.debug("Repository 中不存在 SN 映射: sn={}", sn);
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveMapping(String sn, String vendorType) {
|
||||
// 1. 保存到 Repository 持久化层
|
||||
repository.save(sn, vendorType);
|
||||
|
||||
// 2. 保存到 Redis 缓存
|
||||
String key = KEY_PREFIX + sn;
|
||||
redisTemplate.opsForValue().set(key, vendorType, CACHE_EXPIRE_SECONDS, TimeUnit.SECONDS);
|
||||
|
||||
log.debug("保存 SN 映射到 Redis+Repository: sn={}, vendorType={}", sn, vendorType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeMapping(String sn) {
|
||||
// 1. 从 Repository 删除
|
||||
repository.delete(sn);
|
||||
|
||||
// 2. 从 Redis 删除
|
||||
String key = KEY_PREFIX + sn;
|
||||
redisTemplate.delete(key);
|
||||
|
||||
log.debug("从 Redis+Repository 中移除 SN 映射: sn={}", sn);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exists(String sn) {
|
||||
// 1. 先检查 Redis
|
||||
String key = KEY_PREFIX + sn;
|
||||
if (Boolean.TRUE.equals(redisTemplate.hasKey(key))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 2. 再检查 Repository
|
||||
return repository.exists(sn);
|
||||
}
|
||||
}
|
||||
49
src/main/java/com/ruoyi/device/domain/impl/machine/vendor/store/SnVendorMappingStore.java
vendored
Normal file
49
src/main/java/com/ruoyi/device/domain/impl/machine/vendor/store/SnVendorMappingStore.java
vendored
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
package com.ruoyi.device.domain.impl.machine.vendor.store;
|
||||
/**
|
||||
* SN 到厂家类型映射的存储接口
|
||||
* 提供 SN 与厂家类型映射关系的存储和获取抽象,支持多种实现(内存、Redis+MySQL等)
|
||||
*
|
||||
* 数据流:
|
||||
* 1. 先从 Redis 缓存获取
|
||||
* 2. Redis 没有则从 MySQL 数据库获取
|
||||
* 3. 获取到后存入 Redis 缓存
|
||||
* 4. 都没有则返回 null
|
||||
*/
|
||||
public interface SnVendorMappingStore {
|
||||
|
||||
/**
|
||||
* 获取 SN 对应的厂家类型
|
||||
*
|
||||
* 查询顺序:
|
||||
* 1. 先从 Redis 缓存获取
|
||||
* 2. Redis 没有则从 MySQL 数据库获取
|
||||
* 3. 获取到后存入 Redis 缓存
|
||||
*
|
||||
* @param sn 设备SN号
|
||||
* @return 厂家类型,如果不存在返回 null
|
||||
*/
|
||||
String getVendorType(String sn);
|
||||
|
||||
/**
|
||||
* 保存 SN 到厂家类型的映射
|
||||
*
|
||||
* @param sn 设备SN号
|
||||
* @param vendorType 厂家类型
|
||||
*/
|
||||
void saveMapping(String sn, String vendorType);
|
||||
|
||||
/**
|
||||
* 删除 SN 的映射关系
|
||||
*
|
||||
* @param sn 设备SN号
|
||||
*/
|
||||
void removeMapping(String sn);
|
||||
|
||||
/**
|
||||
* 检查 SN 是否已有映射关系
|
||||
*
|
||||
* @param sn 设备SN号
|
||||
* @return 是否存在映射
|
||||
*/
|
||||
boolean exists(String sn);
|
||||
}
|
||||
|
|
@ -41,6 +41,9 @@ public class Dock implements Serializable
|
|||
/** 备注 */
|
||||
private String remark;
|
||||
|
||||
/** 最后活跃时间 */
|
||||
private Date lastActiveTime;
|
||||
|
||||
public Long getDockId()
|
||||
{
|
||||
return dockId;
|
||||
|
|
@ -130,4 +133,14 @@ public class Dock implements Serializable
|
|||
{
|
||||
this.remark = remark;
|
||||
}
|
||||
|
||||
public Date getLastActiveTime()
|
||||
{
|
||||
return lastActiveTime;
|
||||
}
|
||||
|
||||
public void setLastActiveTime(Date lastActiveTime)
|
||||
{
|
||||
this.lastActiveTime = lastActiveTime;
|
||||
}
|
||||
}
|
||||
|
|
@ -25,6 +25,9 @@ public class BatteryData {
|
|||
@JsonProperty("return_home_power")
|
||||
private Integer returnHomePower;
|
||||
|
||||
@JsonProperty("remain_job_time")
|
||||
private Integer remainJobTime;
|
||||
|
||||
// 构造方法
|
||||
public BatteryData() {
|
||||
}
|
||||
|
|
@ -70,6 +73,14 @@ public class BatteryData {
|
|||
this.returnHomePower = returnHomePower;
|
||||
}
|
||||
|
||||
public Integer getRemainJobTime() {
|
||||
return remainJobTime;
|
||||
}
|
||||
|
||||
public void setRemainJobTime(Integer remainJobTime) {
|
||||
this.remainJobTime = remainJobTime;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "BatteryData{" +
|
||||
|
|
@ -78,6 +89,7 @@ public class BatteryData {
|
|||
", landingPower=" + landingPower +
|
||||
", remainFlightTime=" + remainFlightTime +
|
||||
", returnHomePower=" + returnHomePower +
|
||||
", remainJobTime=" + remainJobTime +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
|
@ -240,6 +240,20 @@ public class DeviceTelemetry {
|
|||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* 电量百分比
|
||||
*/
|
||||
public static final TelemetryKey<Integer> Drone_Charge_State_Capacity_Percent = TelemetryKey.of(
|
||||
"drone_charge_state.capacity_percent",
|
||||
Integer.class,
|
||||
value -> {
|
||||
if (value == null) return null;
|
||||
if (value instanceof Number) {
|
||||
return ((Number) value).intValue();
|
||||
}
|
||||
return Integer.parseInt(value.toString());
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
*
|
||||
|
|
@ -273,6 +287,22 @@ public class DeviceTelemetry {
|
|||
}
|
||||
);
|
||||
|
||||
|
||||
/**
|
||||
* 无人机 position_state.gps_number GPS 搜星数量
|
||||
*/
|
||||
|
||||
public static final TelemetryKey<Integer> Position_State_GPS_Number = TelemetryKey.of(
|
||||
"position_state.gps_number",
|
||||
Integer.class,
|
||||
value -> {
|
||||
if (value == null) return null;
|
||||
if (value instanceof Number) {
|
||||
return ((Number) value).intValue();
|
||||
}
|
||||
return Integer.parseInt(value.toString());
|
||||
}
|
||||
);
|
||||
/**
|
||||
* 风速
|
||||
*/
|
||||
|
|
@ -499,7 +529,9 @@ public class DeviceTelemetry {
|
|||
Alternate_land_point_Longitude,
|
||||
Environment_Temperature,
|
||||
Network_State_Rate,
|
||||
Cover_State
|
||||
Cover_State,
|
||||
Drone_Charge_State_Capacity_Percent,
|
||||
Position_State_GPS_Number
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ package com.ruoyi.device.mapper.entity;
|
|||
|
||||
import com.ruoyi.common.core.web.domain.BaseEntity;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 机场表实体对象 device_dock
|
||||
* Mapper 层实体,对应数据库表
|
||||
|
|
@ -25,6 +27,9 @@ public class DockEntity extends BaseEntity
|
|||
/** 设备表主键 */
|
||||
private Long deviceId;
|
||||
|
||||
/** 最后活跃时间 */
|
||||
private Date lastActiveTime;
|
||||
|
||||
public Long getDockId()
|
||||
{
|
||||
return dockId;
|
||||
|
|
@ -65,6 +70,16 @@ public class DockEntity extends BaseEntity
|
|||
this.deviceId = deviceId;
|
||||
}
|
||||
|
||||
public Date getLastActiveTime()
|
||||
{
|
||||
return lastActiveTime;
|
||||
}
|
||||
|
||||
public void setLastActiveTime(Date lastActiveTime)
|
||||
{
|
||||
this.lastActiveTime = lastActiveTime;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
|
|
@ -73,6 +88,7 @@ public class DockEntity extends BaseEntity
|
|||
", dockName='" + dockName + '\'' +
|
||||
", dockLocation='" + dockLocation + '\'' +
|
||||
", deviceId=" + deviceId +
|
||||
", lastActiveTime=" + lastActiveTime +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
package com.ruoyi.device.service.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* DJI MQTT配置属性
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Data
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "dji.mqtt")
|
||||
public class DjiMqttProperties {
|
||||
|
||||
/**
|
||||
* MQTT服务器地址
|
||||
*/
|
||||
private String host = "mqtt.t-aaron.com";
|
||||
|
||||
/**
|
||||
* MQTT服务器端口
|
||||
*/
|
||||
private Integer port = 10883;
|
||||
|
||||
/**
|
||||
* MQTT协议版本
|
||||
*/
|
||||
private Integer version = 5;
|
||||
|
||||
/**
|
||||
* 客户端ID
|
||||
*/
|
||||
private String clientId = "ThingsBoard_gateway";
|
||||
|
||||
/**
|
||||
* 用户名
|
||||
*/
|
||||
private String username = "admin";
|
||||
|
||||
/**
|
||||
* 密码
|
||||
*/
|
||||
private String password = "admin";
|
||||
|
||||
/**
|
||||
* 连接超时时间(秒)
|
||||
*/
|
||||
private Integer connectionTimeout = 30;
|
||||
|
||||
/**
|
||||
* 保持连接时间(秒)
|
||||
*/
|
||||
private Integer keepAliveInterval = 60;
|
||||
|
||||
/**
|
||||
* 自动重连
|
||||
*/
|
||||
private Boolean autoReconnect = true;
|
||||
|
||||
/**
|
||||
* 清除会话
|
||||
*/
|
||||
private Boolean cleanSession = false;
|
||||
}
|
||||
|
|
@ -70,6 +70,9 @@ public class AircraftDetailDTO implements Serializable
|
|||
/** RTK信号 */
|
||||
private Integer rtkSignal;
|
||||
|
||||
/** GPS信号 */
|
||||
private Integer gpsSignal;
|
||||
|
||||
/** 限高 */
|
||||
private Integer maxAltitude;
|
||||
|
||||
|
|
|
|||
|
|
@ -44,5 +44,8 @@ public class DockDTO implements Serializable
|
|||
/** 备注 */
|
||||
private String remark;
|
||||
|
||||
/** 最后活跃时间 */
|
||||
private Date lastActiveTime;
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import com.ruoyi.device.api.domain.PayloadVO;
|
|||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
|
|
@ -125,6 +126,11 @@ public class DockDetailDTO implements Serializable
|
|||
/** 充放电状态 */
|
||||
private String chargingStatus;
|
||||
|
||||
/**
|
||||
* 电量百分比
|
||||
*/
|
||||
private Integer capacity_percent;
|
||||
|
||||
/** 舱内温度 */
|
||||
private Double cabinTemperature;
|
||||
|
||||
|
|
@ -149,5 +155,8 @@ public class DockDetailDTO implements Serializable
|
|||
*/
|
||||
private Double longitude;
|
||||
|
||||
/** 最后活跃时间 */
|
||||
private Date lastActiveTime;
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -340,6 +340,23 @@ public class BufferDeviceImpl implements IBufferDeviceService
|
|||
() -> log.warn("未获取到经度数据,dockerDeviceIotId: {}", dockerDeviceIotId)
|
||||
);
|
||||
|
||||
// 无人机充电状态(枚举值:0-空闲,1-充电中)
|
||||
telemetryMap.get(DeviceTelemetry.Drone_Charge_State_State)
|
||||
.ifPresent(telemetryValue -> {
|
||||
Integer chargeState = telemetryValue.getValue();
|
||||
if (chargeState != null) {
|
||||
String chargingStatus = mapChargeStateToStatus(chargeState);
|
||||
dto.setChargingStatus(chargingStatus);
|
||||
}
|
||||
});
|
||||
|
||||
// 电量百分比
|
||||
telemetryMap.get(DeviceTelemetry.Drone_Charge_State_Capacity_Percent)
|
||||
.ifPresent(telemetryValue -> {
|
||||
Integer capacityPercent = telemetryValue.getValue();
|
||||
dto.setCapacity_percent(capacityPercent);
|
||||
});
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
|
|
@ -385,6 +402,26 @@ public class BufferDeviceImpl implements IBufferDeviceService
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将充电状态代码映射到充电状态字符串
|
||||
* @param chargeState 充电状态代码(0-空闲,1-充电中)
|
||||
* @return 充电状态字符串
|
||||
*/
|
||||
private String mapChargeStateToStatus(Integer chargeState) {
|
||||
if (chargeState == null) {
|
||||
return "未知";
|
||||
}
|
||||
|
||||
switch (chargeState) {
|
||||
case 0:
|
||||
return "FREE";
|
||||
case 1:
|
||||
return "CHARGING";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将空调状态代码映射到 AirConditionerStatusEnum
|
||||
* @param stateCode 空调状态代码
|
||||
|
|
@ -465,6 +502,14 @@ public class BufferDeviceImpl implements IBufferDeviceService
|
|||
}
|
||||
});
|
||||
|
||||
telemetryMap.get(DeviceTelemetry.Position_State_GPS_Number)
|
||||
.ifPresent(telemetryValue -> {
|
||||
Integer gpsNumber = telemetryValue.getValue();
|
||||
if (gpsNumber != null) {
|
||||
dto.setGpsSignal(gpsNumber);
|
||||
}
|
||||
});
|
||||
|
||||
// 限高
|
||||
telemetryMap.get(DeviceTelemetry.Height_Limit)
|
||||
.ifPresent(telemetryValue -> dto.setMaxAltitude(telemetryValue.getValue()));
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
package com.ruoyi.device.service.impl;
|
||||
|
||||
import com.ruoyi.device.domain.impl.djimqtt.callback.IDockDataCallback;
|
||||
import com.ruoyi.device.domain.impl.djimqtt.callback.IDroneDataCallback;
|
||||
import com.ruoyi.device.domain.impl.djimqtt.config.DjiMqttClientConfig;
|
||||
import com.ruoyi.device.service.config.DjiMqttProperties;
|
||||
import com.ruoyi.device.domain.impl.djimqtt.handler.DjiMqttMessageHandler;
|
||||
import com.ruoyi.device.domain.impl.djimqtt.manager.DjiMqttClientManager;
|
||||
import com.ruoyi.device.domain.impl.djimqtt.model.DockData;
|
||||
import com.ruoyi.device.domain.impl.djimqtt.model.DroneData;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
public class DjiService {
|
||||
|
||||
@Autowired
|
||||
private DjiMqttClientManager clientManager;
|
||||
|
||||
@Autowired
|
||||
private DjiMqttProperties mqttProperties;
|
||||
|
||||
@EventListener(ApplicationReadyEvent.class)
|
||||
public void onApplicationReady() {
|
||||
|
||||
// 从配置文件读取配置
|
||||
DjiMqttClientConfig config = DjiMqttClientConfig.builder()
|
||||
.host(mqttProperties.getHost())
|
||||
.port(mqttProperties.getPort())
|
||||
.clientId(mqttProperties.getClientId())
|
||||
.username(mqttProperties.getUsername())
|
||||
.password(mqttProperties.getPassword())
|
||||
.connectionTimeout(mqttProperties.getConnectionTimeout())
|
||||
.keepAliveInterval(mqttProperties.getKeepAliveInterval())
|
||||
.autoReconnect(mqttProperties.getAutoReconnect())
|
||||
.cleanSession(mqttProperties.getCleanSession())
|
||||
.useSharedSubscription(true)
|
||||
.sharedGroupName("dji-group")
|
||||
.build();
|
||||
|
||||
// 创建客户端
|
||||
String clientId = clientManager.createClient(config);
|
||||
|
||||
// 获取消息处理器
|
||||
DjiMqttMessageHandler handler = clientManager.getHandler(clientId);
|
||||
|
||||
// 注册无人机数据回调
|
||||
handler.registerDroneDataCallback(new IDroneDataCallback() {
|
||||
@Override
|
||||
public void onDroneData(DroneData droneData) {
|
||||
// log.info("droneData:{}", droneData);
|
||||
}
|
||||
});
|
||||
|
||||
// 注册机场数据回调
|
||||
handler.registerDockDataCallback(new IDockDataCallback() {
|
||||
@Override
|
||||
public void onDockData(DockData dockData) {
|
||||
// log.info("droneData:{}", dockData);
|
||||
}
|
||||
});
|
||||
|
||||
log.info("客户端已创建并注册回调");
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue