[add] 新增无人机模块后端项目

[refactor] 重构后端项目
This commit is contained in:
lcj
2025-05-21 11:30:59 +08:00
parent 4c238435d8
commit dc1de34116
1699 changed files with 54361 additions and 325 deletions

92
drone/ruoyi-core/pom.xml Normal file
View File

@ -0,0 +1,92 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.ruoyi</groupId>
<artifactId>Ruoyi-Vue</artifactId>
<version>3.8.8</version>
</parent>
<artifactId>ruoyi-core</artifactId>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- 通用工具-->
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-system</artifactId>
</dependency>
<!-- MQTT -->
<dependency>
<groupId>org.eclipse.paho</groupId>
<artifactId>org.eclipse.paho.client.mqttv3</artifactId>
<version>1.2.5</version>
</dependency>
<!-- Spring Integration 依赖 -->
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-mqtt</artifactId>
</dependency>
<!-- JSON 解析依赖 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- WebSocket -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- MyBatisPlus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3.4</version>
</dependency>
<!-- MinIO -->
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.3.0</version>
</dependency>
<!-- OkHttp 依赖项 -->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.8.1</version>
</dependency>
<dependency>
<groupId>com.cronutils</groupId>
<artifactId>cron-utils</artifactId>
<version>9.1.6</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>9</source>
<target>9</target>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,89 @@
package com.ruoyi.dj.controller;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.dj.mqtt.domain.RespModel;
import com.ruoyi.dj.mqtt.utils.AESUtil;
import com.ruoyi.dj.mqtt.utils.StorageConfigUtil;
import com.ruoyi.dj.websocket.service.MessagingService;
import io.minio.*;
import io.minio.errors.ErrorResponseException;
import io.minio.http.Method;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
import javax.annotation.Resource;
import java.io.ByteArrayInputStream;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.*;
@Slf4j
@Api(tags = "AI相关")
@RestController
@RequestMapping("/business/ai")
public class AiController {
// 创建一个线程安全的 MinioClient 实例、避免重复创建
private static final MinioClient minioClient = StorageConfigUtil.getMinioClient();
@Resource
private MessagingService messagingService;
@PostMapping("/callback")
public AjaxResult ai(@RequestBody AiDataObject aiDataObject) {
System.out.println("收到AI数据" + aiDataObject.getCategory_counts());
String gateway = aiDataObject.getGateway();
System.out.println(aiDataObject.getCategory_counts());
try {
// 解码 imgData
byte[] decodedImg = Base64.decodeBase64(aiDataObject.getFrame_base64());
// 获取当前日期,构建目录结构
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd");
String datePath = dateFormat.format(new Date());
// 定义桶名称
String bucketName = "ai" + gateway.toLowerCase().replace("_", "");
// 检查桶是否存在、不存在则创建
synchronized (this) {
// 使用 synchronized 避免并发创建
if (!minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build())) {
try {
minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
log.info("已创建桶: {}", bucketName);
} catch (Exception e) {
if (e instanceof ErrorResponseException &&
((ErrorResponseException) e).errorResponse().code().equals("BucketAlreadyOwnedByYou")) {
log.warn("桶已存在: {}", bucketName);
} else {
throw e;
}
}
}
}
// 生成文件名
String fileName = UUID.randomUUID().toString() + ".jpg";
String objectName = "AI检测数据/" + datePath + "/" + fileName;
// 上传文件,设置并发上传,提高效率
ByteArrayInputStream inputStream = new ByteArrayInputStream(decodedImg);
minioClient.putObject(
PutObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.stream(inputStream, decodedImg.length, -1)
.contentType("image/jpeg")
.build()
);
log.info("文件上传成功: {}", objectName);
} catch (Exception e) {
log.error("保存图片时出现错误", e);
}
return AjaxResult.success();
}
}

View File

@ -0,0 +1,12 @@
package com.ruoyi.dj.controller;
import lombok.Data;
import java.util.Map;
@Data
class AiDataObject {
private String gateway;
private Map<String, Integer> category_counts;
private String frame_base64;
}

View File

@ -0,0 +1,367 @@
package com.ruoyi.dj.controller;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.dj.mqtt.client.DJICloudMQTTClient;
import com.ruoyi.dj.mqtt.cmd.CmdConstant;
import com.ruoyi.dj.mqtt.common.CommonFieldsVo;
import com.ruoyi.dj.mqtt.config.DJIMqttConfig;
import com.ruoyi.dj.mqtt.domain.*;
import com.ruoyi.dj.mqtt.dto.DroneControlDto;
import com.ruoyi.dj.mqtt.dto.DroneEmergencyStop;
import com.ruoyi.dj.mqtt.utils.*;
import com.ruoyi.system.domain.GatewayOperationLog;
import com.ruoyi.system.domain.OneClickTakeoff;
import com.ruoyi.system.domain.VideoInfo;
import com.ruoyi.system.service.IGatewayOperationLogService;
import com.ruoyi.system.service.IOneClickTakeoffService;
import com.ruoyi.system.service.IVideoInfoService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.*;
import java.util.concurrent.CompletableFuture;
/**
* @auther 周志雄
* @date 2024/7/25 9:51
*/
@Slf4j
@Api(tags = "指令飞行")
@RestController
@RequestMapping("/dj/cmdFly")
public class CmdFlyController {
@Autowired
private StringRedisTemplate redisTemplate;
private static final String SERVICES_TOPIC = "thing/product/?/services";
private static final String DRC_SERVICES_TOPIC = "thing/product/?/drc/down";
@Resource
private DJICloudMQTTClient djiCloudMQTTClient;
@Autowired
private DJIMqttConfig djiMqttConfig;
@Autowired
private Environment environment;
@Resource
private IOneClickTakeoffService oneClickTakeoffService;
@Resource
private IVideoInfoService videoInfoService;
@Resource
private IGatewayOperationLogService gatewayOperationLogService;
Map<String, String> map = new HashMap<>();
@PostConstruct
public void init() {
map.put("flight_authority_grab", "飞行控制权抢夺");
map.put("payload_authority_grab", "负载控制权抢夺");
map.put("drc_mode_enter", "进入指令飞行控制模式");
map.put("drc_mode_close", "退出指令飞行控制模式");
map.put("takeoff_to_point", "一键起飞");
map.put("fly_to_point", "飞向目标点");
map.put("fly_to_point", "飞向目标点");
map.put("camera_mode_switch", "切换相机模式成功");
map.put("camera_photo_take", "开始拍照成功");
map.put("camera_photo_stop", "停止拍照成功");
map.put("camera_recording_start", "开始录像成功");
map.put("camera_recording_stop", "结束录像成功");
map.put("camera_screen_drag", "画面拖动控制成功");
map.put("camera_aim", "居中成功");
map.put("camera_focal_length_set", "变焦成功");
}
// 定义一个标志变量,用来控制心跳线程是否继续执行
private volatile boolean keepHeartBeatRunning = true;
/**
* 删除 Redis 中所有以 drc: 开头的键
*/
public void deleteAllWithPrefix(String prefix) {
Set<String> keys = redisTemplate.keys(prefix + "*");
if (keys != null && !keys.isEmpty()) {
redisTemplate.delete(keys);
}
}
/**
* 无参数的指令飞行
*
* @param cmdFlyServiceDto
* @return
*/
@ApiOperation("无参数的指令飞行")
@PostMapping("/noDataFlight")
public AjaxResult noParameterFlight(@RequestBody CmdFlyServiceDto cmdFlyServiceDto) {
String msg = map.get(cmdFlyServiceDto.getMethod());
// 退出指令模式
if ("drc_mode_exit".equals(cmdFlyServiceDto.getMethod())) {
// 更新标志位,通知心跳线程停止
keepHeartBeatRunning = false;
// 删除和这个网关有关的任何内容
String prefix = "drc:" + cmdFlyServiceDto.getGateway();
deleteAllWithPrefix(prefix);
}
String gateway = cmdFlyServiceDto.getGateway();
String topic = SERVICES_TOPIC.replace("?", cmdFlyServiceDto.getGateway());
CommonFieldsVo commonFieldVo = CommonFieldsVo.createCommonFieldVo(cmdFlyServiceDto.getMethod(), cmdFlyServiceDto.getGateway());
String payload = PayloadUtil.generatePayload(commonFieldVo, null);
log.info("无参数的指令发送数据:" + payload);
String response = djiCloudMQTTClient.publishMessage(topic, payload, commonFieldVo);
AjaxResult ajaxResult = HandleResultUtil.handleResult(response, null, msg);
// 记录日志
GatewayOperationLogUtil.generateLog(gateway, commonFieldVo.getMethod(), payload, response);
return ajaxResult;
}
/**
* 有参数的指令飞行
*
* @param cmdFlyDataServiceDto
* @return
*/
@ApiOperation("有参数的指令飞行")
@PostMapping("/hasDataFlight")
public AjaxResult payloadAuthorityGrab(@RequestBody CmdFlyDataServiceDto cmdFlyDataServiceDto) {
String msg = map.get(cmdFlyDataServiceDto.getMethod());
// 如果是一键起飞
if (cmdFlyDataServiceDto.getMethod().equals("takeoff_to_point")) {
Map<String, Object> params = cmdFlyDataServiceDto.getParams();
// 生成随机的七位字符串
String flightId = String.valueOf(new Random().nextInt(8999999) + 1000000);
params.put("flight_id", flightId);
// 查询飞机的序列号
VideoInfo videoInfo = videoInfoService.selectVideoByCondition(cmdFlyDataServiceDto.getGateway(), "飞机");
// 生成数据到数据库一键起飞记录中
OneClickTakeoff oneClickTakeoff = new OneClickTakeoff();
oneClickTakeoff.setFlightId(Long.valueOf(flightId));
oneClickTakeoff.setDroneSerialNumber(videoInfo.getSn());
oneClickTakeoff.setTakeoffTime(new Date());
oneClickTakeoffService.insertOneClickTakeoff(oneClickTakeoff);
}
String gateway = cmdFlyDataServiceDto.getGateway();
String topic = SERVICES_TOPIC.replace("?", cmdFlyDataServiceDto.getGateway());
CommonFieldsVo commonFieldVo = CommonFieldsVo.createCommonFieldVo(cmdFlyDataServiceDto.getMethod(), cmdFlyDataServiceDto.getGateway());
Map<String, Object> params = cmdFlyDataServiceDto.getParams();
String payload = PayloadUtil.generatePayload(commonFieldVo, params);
log.info("参数:{}", payload);
String response = djiCloudMQTTClient.publishMessage(topic, payload, commonFieldVo);
AjaxResult ajaxResult = HandleResultUtil.handleResult(response, null, msg);
// 记录日志
GatewayOperationLogUtil.generateLog(gateway, commonFieldVo.getMethod(), payload, response);
return ajaxResult;
}
/**
* 进入指令飞行控制模式
*
* @param drcModeEnter
* @return
*/
@ApiOperation("进入指令飞行控制模式")
@PostMapping("/drcModeEnter")
public AjaxResult drcModeEnter(@RequestBody DrcModeEnter drcModeEnter) {
String gateway = drcModeEnter.getGateway();
String topic = SERVICES_TOPIC.replace("?", drcModeEnter.getGateway());
CommonFieldsVo commonFieldVo = CommonFieldsVo.createCommonFieldVo(CmdConstant.DRC_MODE_ENTER, drcModeEnter.getGateway());
Map<String, Object> mqttMap = new HashMap<>();
mqttMap.put("address", environment.getProperty("cmd.host"));
// 设置 clientId
mqttMap.put("client_id", "cmd" + drcModeEnter.getGateway());
mqttMap.put("enable_tls", false);
mqttMap.put("expire_time", 7200);
mqttMap.put("password", environment.getProperty("cmd.password"));
mqttMap.put("username", environment.getProperty("cmd.username"));
Map<String, Object> params = new HashMap<>();
params.put("hsi_frequency", 1);
params.put("mqtt_broker", mqttMap);
params.put("osd_frequency", 10);
String payload = PayloadUtil.generatePayload(commonFieldVo, params);
String response = djiCloudMQTTClient.publishMessage(topic, payload, commonFieldVo);
AjaxResult ajaxResult = HandleResultUtil.handleResult(response, null, "进入指令飞行控制模式");
log.error("进入指令控制模式:{}", response);
GatewayOperationLogUtil.generateLog(gateway, commonFieldVo.getMethod(), payload, response);
// 启动心跳线程
CompletableFuture.runAsync(() -> {
while (keepHeartBeatRunning) { // 循环发送心跳直到标志位为false
HeartBeatVo heartBeatVo = new HeartBeatVo();
heartBeatVo.setGateway(gateway);
AjaxResult heart = heart(heartBeatVo);
if (heart.isSuccess()) {
try {
Thread.sleep(2000); // 休眠2秒
} catch (InterruptedException e) {
e.printStackTrace();
Thread.currentThread().interrupt(); // 中断线程
}
}
}
});
return ajaxResult;
}
/**
* DRC-飞行控制
*
* @param drcModeEnter
* @return
*/
/**
* DRC-飞行控制
*
* @param droneControlDto
* @return
*/
@ApiOperation("DRC-飞行控制")
@PostMapping("/droneControl")
public AjaxResult droneControl(@RequestBody DroneControlDto droneControlDto) {
long seq;
// 获取请求参数的字符串标识
String droneControlDtoString = droneControlDto.toString();
// 将参数的字符串标识 MD5 作为 Redis 的 Key
String md5 = Md5Util.getMD5(droneControlDtoString);
log.error(md5);
String key = "drc:" + droneControlDto.getGateway() + ":" + md5;
// 检查 Redis 中是否已存在该 key如果不存在则初始化为 0
Boolean keyExists = redisTemplate.hasKey(key);
if (!keyExists) {
// 如果 Redis 中没有该 key手动设置为 0
redisTemplate.opsForValue().set(key, "0");
seq = 0;
} else {
// 使用 Redis 原子性操作 INCRBY 增加值,如果键不存在则初始化为 0存在则加 1
seq = redisTemplate.opsForValue().increment(key, 1);
}
// 设置 key 为 seq
redisTemplate.opsForValue().set(key, String.valueOf(seq));
// 生成需要发布的消息
String gateway = droneControlDto.getGateway();
String topic = DRC_SERVICES_TOPIC.replace("?", gateway);
CommonFieldsVo commonFieldVo = CommonFieldsVo.createCommonFieldVo(CmdConstant.DRONE_CONTROL, gateway);
// 构建参数
Map<String, Object> params = new HashMap<>();
params.put("seq", seq);
params.put("x", droneControlDto.getX());
params.put("y", droneControlDto.getY());
params.put("h", droneControlDto.getH());
params.put("w", droneControlDto.getW());
// 生成消息内容
String payload = PayloadUtil.generatePayload(commonFieldVo, params);
List<String> list = new ArrayList<>();
list.add("gateway");
list.add("tid");
list.add("bid");
try {
payload = JsonKeyRemover.removeKeysFromJson(list, payload);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
GatewayOperationLog gatewayOperationLog = new GatewayOperationLog();
gatewayOperationLog.setGatewayId(gateway);
gatewayOperationLog.setSendParams(payload);
gatewayOperationLog.setOperationTime(new Date());
gatewayOperationLogService.insertGatewayOperationLog(gatewayOperationLog);
log.info("操作飞行参数:{}", payload);
// 异步发布 MQTT 消息、接口直接先返回
final String finalTopic = topic; // 声明为 final
final String finalPayload = payload; // 声明为 final
CompletableFuture.runAsync(() -> {
djiCloudMQTTClient.publishMessageWithDrc(finalTopic, finalPayload, commonFieldVo);
});
return AjaxResult.success();
}
/**
* DRC-飞行器急停
*
* @param droneEmergencyStop
* @return
*/
@ApiOperation("DRC-飞行器急停")
@PostMapping("/droneEmergencyStop")
public AjaxResult droneEmergencyStop(@RequestBody DroneEmergencyStop droneEmergencyStop) {
String gateway = droneEmergencyStop.getGateway();
String topic = DRC_SERVICES_TOPIC.replace("?", droneEmergencyStop.getGateway());
CommonFieldsVo commonFieldVo = CommonFieldsVo.createCommonFieldVo(CmdConstant.DRONE_EMERGENCY_STOP, droneEmergencyStop.getGateway());
String payload = PayloadUtil.generatePayload(commonFieldVo, null);
List<String> list = new ArrayList<>();
list.add("gateway");
list.add("tid");
list.add("bid");
try {
payload = JsonKeyRemover.removeKeysFromJson(list, payload);
log.error("急停发送数据:{}", payload);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
String response = djiCloudMQTTClient.publishMessageWithDrc(topic, payload, commonFieldVo);
AjaxResult ajaxResult = HandleResultUtil.handleResult(response, null, "飞行器急停成功");
// 记录日志
GatewayOperationLogUtil.generateLog(gateway, commonFieldVo.getMethod(), payload, response);
return ajaxResult;
}
@NotNull
private AjaxResult heart(HeartBeatVo heartBeatVo) {
String key = "drc:" + heartBeatVo.getGateway() + ":" + "heart";
// 使用 Redis 的 INCR 命令进行原子性递增
Long seq = redisTemplate.opsForValue().increment(key, 1); // 如果没有该 key会默认设置为 0 然后加 1
// 生成 topic 和 commonFieldVo
String gateway = heartBeatVo.getGateway();
String topic = DRC_SERVICES_TOPIC.replace("?", heartBeatVo.getGateway());
CommonFieldsVo commonFieldVo = CommonFieldsVo.createCommonFieldVo(CmdConstant.HEART_BEAT, heartBeatVo.getGateway());
// 构建请求参数
Map<String, Object> params = new HashMap<>();
params.put("timestamp", System.currentTimeMillis());
String payload = PayloadUtil.generatePayload(commonFieldVo, params);
List<String> keysToRemove = new ArrayList<>();
keysToRemove.add("gateway");
keysToRemove.add("tid");
keysToRemove.add("bid");
try {
// 移除不必要的键并添加 seq 字段
payload = JsonKeyRemover.removeKeysFromJson(keysToRemove, payload);
payload = JsonKeyRemover.addKeyToRoot(payload, "seq", seq.intValue());
log.error("心跳发送内容: {}", payload);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
// 发布消息到 MQTT
DrcVo drcVo = new DrcVo();
drcVo.setMethod(CmdConstant.HEART_BEAT);
String response = djiCloudMQTTClient.publishMessageWithDrc(topic, payload, commonFieldVo);
// 记录日志
GatewayOperationLogUtil.generateLog(gateway, commonFieldVo.getMethod(), payload, response);
// 返回响应
return AjaxResult.success("心跳发送成功", response);
}
}

View File

@ -0,0 +1,402 @@
package com.ruoyi.dj.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.dj.mqtt.client.DJICloudMQTTClient;
import com.ruoyi.dj.mqtt.cmd.CmdConstant;
import com.ruoyi.dj.mqtt.common.CommonFieldsVo;
import com.ruoyi.dj.mqtt.dto.*;
import com.ruoyi.dj.mqtt.service.TaskSchedulingService;
import com.ruoyi.dj.mqtt.utils.GatewayOperationLogUtil;
import com.ruoyi.dj.mqtt.utils.HandleResultUtil;
import com.ruoyi.dj.mqtt.utils.PayloadUtil;
import com.ruoyi.dj.mqtt.utils.StorageConfigUtil;
import com.ruoyi.system.domain.*;
import com.ruoyi.system.service.*;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @auther 周志雄
* @date 2024/7/15 14:31
*/
@Slf4j
@RestController
@Api(tags = "航线执行相关")
@RequestMapping("/dj")
public class FlightController {
private static final String SERVICES_TOPIC = "thing/product/?/services";
@Resource
private DJICloudMQTTClient djiCloudMQTTClient;
@Resource
private IGatewayOperationLogService gatewayOperationLogService;
@Resource
private IDroneMissionsService droneMissionsService;
@Resource
private IFlightTasksService flightTasksService;
@Resource
private IVideoInfoService videoInfoService;
@Resource
private IScheduleTaskService scheduleTaskService;
@Resource
private TaskSchedulingService taskSchedulingService;
@Resource
private IMinioFileService minioFileService;
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 下发任务
*
* @return
*/
@ApiOperation("下发任务")
@PostMapping("/flightTaskPrepare")
public AjaxResult flighttaskPrepare(@RequestBody FlightTaskDto flightTaskDto) {
// 查看任务的类型
Integer type = flightTaskDto.getType();
// 立即执行的任务则直接下发
if (type.equals(0)) {
// 首先下发任务、获取最新的ID和对应的flightId
Map<String, String> map = flightTaskPrepare(flightTaskDto);
String flightId = map.get("flightId");
String taskId = map.get("taskId");
// 然后立即执行任务
FlightTaskExecuteDto flightTaskExecuteDto = new FlightTaskExecuteDto();
flightTaskExecuteDto.setFlightId(flightId);
flightTaskExecuteDto.setGateway(flightTaskDto.getGateway());
flightTaskExecuteDto.setTaskId(Long.valueOf(taskId));
return executeTask(flightTaskExecuteDto);
} else {
// 定时任务、首先根据网关查询飞机的SN
VideoInfo videoInfo = videoInfoService.selectVideoByCondition(flightTaskDto.getGateway(), "飞机");
String sn = videoInfo.getSn();
// 定时任务则先存储数据到数据库
ScheduleTask scheduleTask = new ScheduleTask();
scheduleTask.setTaskId(String.valueOf(flightTaskDto.getMissionId()));
scheduleTask.setSn(sn);
scheduleTask.setCreatedAt(new Date());
scheduleTask.setExecMonth(flightTaskDto.getExecMonth());
scheduleTask.setExecDay(flightTaskDto.getExecDay());
scheduleTask.setExecHour(flightTaskDto.getExecHour());
scheduleTask.setExecMinute(flightTaskDto.getExecMinute());
scheduleTask.setExecSecond(flightTaskDto.getExecSecond());
// 新增定时任务成功
scheduleTaskService.insertScheduleTask(scheduleTask);
return AjaxResult.success("任务下发成功").put("data", "任务下发成功");
}
}
/**
* 执行任务
*
* @return
*/
@ApiOperation("执行任务")
@PostMapping("/flightTaskExecute")
public AjaxResult flightTaskExecute(@RequestBody FlightTaskExecuteDto flightTaskExecuteDto) {
return executeTask(flightTaskExecuteDto);
}
/**
* 取消任务
*
* @return
*/
@ApiOperation("取消任务")
@PostMapping("/flightTaskUndo")
public AjaxResult flightTaskUndo(@RequestBody FlightTaskUndoDto flightTaskUndoDto) {
String topic = SERVICES_TOPIC.replace("?", flightTaskUndoDto.getGateway());
CommonFieldsVo commonFieldVo = CommonFieldsVo.createCommonFieldVo(CmdConstant.FLIGHTTASK_UNDO, flightTaskUndoDto.getGateway());
// 构造参数
Map<String, Object> map = new HashMap<>();
map.put("flight_ids", flightTaskUndoDto.getFlightIds());
String payload = PayloadUtil.generatePayload(commonFieldVo, map);
log.info("取消航线请求数据:{}", payload);
String response = djiCloudMQTTClient.publishMessage(topic, payload, commonFieldVo);
// 记录操作日志
GatewayOperationLogUtil.generateLog(flightTaskUndoDto.getGateway(), CmdConstant.FLIGHTTASK_UNDO, payload, response);
return HandleResultUtil.handleResult(response, null,"取消任务成功");
}
/**
* 暂停任务
*
* @return
*/
@ApiOperation("暂停任务")
@PostMapping("/flightTaskPause")
public AjaxResult flightTaskPause(@RequestBody FlightTaskPauseDto flightTaskPauseDto) {
String topic = SERVICES_TOPIC.replace("?", flightTaskPauseDto.getGateway());
CommonFieldsVo commonFieldVo = CommonFieldsVo.createCommonFieldVo(CmdConstant.FLIGHTTASK_PAUSE, flightTaskPauseDto.getGateway());
String payload = PayloadUtil.generatePayload(commonFieldVo, null);
log.info("暂停航线请求数据:{}", payload);
String response = djiCloudMQTTClient.publishMessage(topic, payload, commonFieldVo);
// 记录操作日志
GatewayOperationLogUtil.generateLog(flightTaskPauseDto.getGateway(), CmdConstant.FLIGHTTASK_PAUSE, payload, response);
return HandleResultUtil.handleResult(response, null,"暂停任务成功");
}
/**
* 航线恢复
*
* @return
*/
@ApiOperation("航线恢复")
@PostMapping("/flightTaskRecovery")
public AjaxResult flightTaskRecovery(@RequestBody FlightTaskRecoveryDto flightTaskRecoveryDto) {
String topic = SERVICES_TOPIC.replace("?", flightTaskRecoveryDto.getGateway());
CommonFieldsVo commonFieldVo = CommonFieldsVo.createCommonFieldVo(CmdConstant.FLIGHTTASK_RECOVERY, flightTaskRecoveryDto.getGateway());
String payload = PayloadUtil.generatePayload(commonFieldVo, null);
log.info("恢复航线请求数据:{}", payload);
String response = djiCloudMQTTClient.publishMessage(topic, payload, commonFieldVo);
// 记录操作日志
GatewayOperationLogUtil.generateLog(flightTaskRecoveryDto.getGateway(), CmdConstant.FLIGHTTASK_RECOVERY, payload, response);
return HandleResultUtil.handleResult(response, null,"航线恢复成功");
}
/**
* 一键返航
*
* @return
*/
@ApiOperation("一键返航")
@PostMapping("/returnHome")
public AjaxResult returnHome(@RequestBody ReturnHomeDto returnHomeDto) {
String topic = SERVICES_TOPIC.replace("?", returnHomeDto.getGateway());
CommonFieldsVo commonFieldVo = CommonFieldsVo.createCommonFieldVo(CmdConstant.RETURN_HOME, returnHomeDto.getGateway());
String payload = PayloadUtil.generatePayload(commonFieldVo, null);
log.info("一键返航请求数据:{}", payload);
String response = djiCloudMQTTClient.publishMessage(topic, payload, commonFieldVo);
// 记录操作日志
GatewayOperationLogUtil.generateLog(returnHomeDto.getGateway(), CmdConstant.RETURN_HOME, payload, response);
return HandleResultUtil.handleResult(response, null,"一键返航成功");
}
/**
* 取消返航
*
* @return
*/
@ApiOperation("取消返航")
@PostMapping("/returnHomeCancel")
public AjaxResult returnHomeCancel(@RequestBody ReturnHomeCancelDto returnHomeCancelDto) {
String topic = SERVICES_TOPIC.replace("?", returnHomeCancelDto.getGateway());
CommonFieldsVo commonFieldVo = CommonFieldsVo.createCommonFieldVo(CmdConstant.RETURN_HOME_CANCEL, returnHomeCancelDto.getGateway());
String payload = PayloadUtil.generatePayload(commonFieldVo, null);
log.info("取消返航请求数据:{}", payload);
String response = djiCloudMQTTClient.publishMessage(topic, payload, commonFieldVo);
// 记录操作日志
GatewayOperationLogUtil.generateLog(returnHomeCancelDto.getGateway(), CmdConstant.RETURN_HOME_CANCEL, payload, response);
return HandleResultUtil.handleResult(response, null,"取消返航成功");
}
private Map<String, String> flightTaskPrepare(FlightTaskDto flightTaskDto) {
HashMap<String, String> hashMap = new HashMap<>();
String topic = SERVICES_TOPIC.replace("?", flightTaskDto.getGateway());
CommonFieldsVo commonFieldVo = CommonFieldsVo.createCommonFieldVo(CmdConstant.FLIGHTTASK_PREPARE, flightTaskDto.getGateway());
String gateway = flightTaskDto.getGateway();
// 先生成一个随机的数字字符串作为任务ID
String flightId = String.valueOf((int) (Math.random() * 900000) + 100000);
// 根据任务ID去查询是否存在任务信息
DroneMissions droneMissions = droneMissionsService.selectDroneMissionsById(flightTaskDto.getMissionId());
if (droneMissions != null) {
log.info("{}", droneMissions);
// 把任务对象转化为 FlightTaskParam 参数、便于构建下发任务的 JSON 参数
FlightTaskParam flightTaskParam = getFlightTaskParam(droneMissions, flightId);
flightTaskParam.setTaskType(Long.valueOf(flightTaskDto.getType()));
flightTaskParam.setExecuteTime(flightTaskDto.getTimeStamp());
Map<String, Object> map = convertDtoToMap(flightTaskParam);
String payload = PayloadUtil.generatePayload(commonFieldVo, map);
String response = djiCloudMQTTClient.publishMessage(topic, payload, commonFieldVo);
// 记录操作日志
GatewayOperationLogUtil.generateLog(flightTaskDto.getGateway(), CmdConstant.FLIGHTTASK_PREPARE, payload, response);
ObjectMapper objectMapper = new ObjectMapper();
// 将字符串解析为 JSON 对象
JsonNode resultNode = null;
try {
ObjectNode jsonNode = null;
jsonNode = (ObjectNode) objectMapper.readTree(response);
resultNode = jsonNode.path("data").path("result");
Integer result = Integer.valueOf(resultNode.asText());
if (result == 0) {
// 执行指令没有出错则存储下发任务数据到数据库
FlightTasks flightTasks = new FlightTasks();
// 根据网关查询飞机的SN
VideoInfo videoInfo = videoInfoService.selectVideoByCondition(gateway, "飞机");
String sn = videoInfo.getSn();
flightTasks.setSn(sn);
flightTasks.setIsExecuted(0);
flightTasks.setStatus("下发成功待执行");
flightTasks.setFlightId(flightId);
flightTasks.setCreatedAt(new Date());
flightTasks.setMessionId(flightTaskDto.getMissionId());
flightTasksService.insertFlightTasks(flightTasks);
// 返回任务ID
hashMap.put("flightId", flightId);
hashMap.put("taskId", flightTasks.getId().toString());
}
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
return hashMap;
}
private AjaxResult executeTask(FlightTaskExecuteDto flightTaskExecuteDto) {
String topic = SERVICES_TOPIC.replace("?", flightTaskExecuteDto.getGateway());
CommonFieldsVo commonFieldVo = CommonFieldsVo.createCommonFieldVo(CmdConstant.FLIGHTTASK_EXECUTE, flightTaskExecuteDto.getGateway());
// 构造参数
Map<String, Object> map = new HashMap<>();
map.put("flight_id", flightTaskExecuteDto.getFlightId());
String payload = PayloadUtil.generatePayload(commonFieldVo, map);
log.info("执行航线请求数据:{}", payload);
String response = djiCloudMQTTClient.publishMessage(topic, payload, commonFieldVo);
// 记录操作日志
GatewayOperationLogUtil.generateLog(flightTaskExecuteDto.getGateway(), CmdConstant.FLIGHTTASK_EXECUTE, payload, response);
return HandleResultUtil.handleResult(response, null,"下发航线成功");
}
/**
* 根据droneMissions获取FlightTaskParam
*
* @param droneMissions
* @return
*/
private static FlightTaskParam getFlightTaskParam(DroneMissions droneMissions, String flightId) {
FlightTaskParam flightTaskParam = new FlightTaskParam();
// 设置计划ID
flightTaskParam.setFlightId(flightId.toString());
// 创建航线文件对象
FlightTaskParam.File file = new FlightTaskParam.File();
// 设置航线文件MD5
file.setFingerprint(droneMissions.getFlightPathMd5());
// 设置航线文件URL
file.setUrl(droneMissions.getFlightPathUrl());
// 设置文件信息
flightTaskParam.setFile(file);
// 设置返航高度
flightTaskParam.setRthAltitude(Integer.valueOf(droneMissions.getReturnAltitude().toString()));
// 设置返航高度模式
flightTaskParam.setRthMode(Integer.valueOf(droneMissions.getReturnAltitudeMode().toString()));
// 设置遥控器失控类型
flightTaskParam.setOutOfControlAction(Integer.valueOf(droneMissions.getOutOfControlAction().toString()));
// 设置航线失控动作
flightTaskParam.setExitWaylineWhenRcLost(Integer.valueOf(droneMissions.getFlightControlAction().toString()));
// 设置航线精度类型
flightTaskParam.setWaylinePrecisionType(Integer.valueOf(droneMissions.getWaylinePrecisionType().toString()));
log.info("构造航线下发的参数:{}", flightTaskParam);
return flightTaskParam;
}
/**
* 构造航线下发的参数
*
* @param flightTaskParam
* @return
*/
private Map<String, Object> convertDtoToMap(FlightTaskParam flightTaskParam) {
Map<String, Object> dataMap = new HashMap<>();
// 如果断点信息不为空,则加入到 Map
if (flightTaskParam.getBreakPoint() != null) {
dataMap.put("break_point", flightTaskParam.getBreakPoint());
}
// 如果执行条件不为空,则加入到 Map
if (flightTaskParam.getExecutableConditions() != null) {
dataMap.put("executable_conditions", flightTaskParam.getExecutableConditions());
}
// 如果有指定任务执行时间,则加入到 Map
if (flightTaskParam.getExecuteTime() != null) {
dataMap.put("execute_time", flightTaskParam.getExecuteTime());
}
// 如果文件信息不为空,则加入到 Map
if (flightTaskParam.getFile() != null) {
dataMap.put("file", flightTaskParam.getFile());
}
// 如果有计划 ID则加入到 Map
if (flightTaskParam.getFlightId() != null) {
dataMap.put("flight_id", flightTaskParam.getFlightId());
}
// 如果有定义遥控器失控动作,则加入到 Map
if (flightTaskParam.getOutOfControlAction() != null) {
dataMap.put("out_of_control_action", flightTaskParam.getOutOfControlAction());
}
// 如果有就绪条件,则加入到 Map
if (flightTaskParam.getReadyConditions() != null) {
dataMap.put("ready_conditions", flightTaskParam.getReadyConditions());
}
// 如果模拟任务不为空,则加入到 Map
if (flightTaskParam.getSimulateMission() != null) {
dataMap.put("simulate_mission", flightTaskParam.getSimulateMission());
}
// 如果有返航高度,则加入到 Map
if (flightTaskParam.getRthAltitude() != null) {
dataMap.put("rth_altitude", flightTaskParam.getRthAltitude());
}
// 如果有返航高度模式,则加入到 Map
if (flightTaskParam.getRthMode() != null) {
dataMap.put("rth_mode", flightTaskParam.getRthMode());
}
// 如果有航线失控动作,则加入到 Map
if (flightTaskParam.getExitWaylineWhenRcLost() != null) {
dataMap.put("exit_wayline_when_rc_lost", flightTaskParam.getExitWaylineWhenRcLost());
}
// 如果有航线类型,则加入到 Map
if (flightTaskParam.getTaskType() != null) {
dataMap.put("task_type", flightTaskParam.getTaskType());
}
// 任务类型和航线精度类型始终加入到 Map因为它们为基础属性
dataMap.put("task_type", flightTaskParam.getTaskType());
dataMap.put("wayline_precision_type", flightTaskParam.getWaylinePrecisionType());
return dataMap;
}
@ApiOperation("获取任务执行文件结果")
@PostMapping("/getFileByflightId")
public AjaxResult getFileByflightId(@RequestBody GetFileDto getFileDto) {
VideoInfo videoInfo = videoInfoService.selectGatewayByCondition(getFileDto.getSn());
if (videoInfo == null) {
log.info("未找到对应的视频设备");
return AjaxResult.success().put("data", null);
}
List<Map<String, String>> files = StorageConfigUtil.getFiles(videoInfo.getGateway(), getFileDto.getFlightId());
log.info("获取到的文件信息:{}", files);
// 循环遍历数据
for (Map<String, String> file : files) {
String fileName = file.get("filename");
String fileUrl = file.get("url");
// 存到数据库之前需要先查看、不存在再存入
MinioFile exist = minioFileService.selectMinioFileName(fileName);
if (exist == null) {
MinioFile minioFile = new MinioFile();
minioFile.setFilename(fileName);
minioFile.setUrl(fileUrl);
minioFile.setFlightId(Long.valueOf(getFileDto.getFlightId()));
minioFile.setCreatedAt(new Date());
minioFileService.insertMinioFile(minioFile);
}
}
return AjaxResult.success().put("data", files);
}
}

View File

@ -0,0 +1,201 @@
package com.ruoyi.dj.controller;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.dj.mqtt.client.DJICloudMQTTClient;
import com.ruoyi.dj.mqtt.cmd.CmdConstant;
import com.ruoyi.dj.mqtt.common.CommonFieldsVo;
import com.ruoyi.dj.mqtt.dto.VideoInfoChangeVo;
import com.ruoyi.dj.mqtt.dto.VideoInfoQualityVo;
import com.ruoyi.dj.mqtt.dto.liveCameraChangeDto;
import com.ruoyi.dj.mqtt.utils.*;
import com.ruoyi.system.domain.vo.VideoInfoVo;
import com.ruoyi.system.service.IGatewayOperationLogService;
import com.ruoyi.system.service.IVideoInfoService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;
/**
* @auther 周志雄
* @date 2024/7/9 9:48
*/
@Slf4j
@RestController
@Api(tags = "直播相关")
@RequestMapping("/dj")
public class LiveController {
private static final String SERVICES_TOPIC = "thing/product/?/services";
@Resource
private DJICloudMQTTClient djiCloudMQTTClient;
@Autowired
private StringRedisTemplate redisTemplate;
@Resource
private IVideoInfoService videoInfoService;
@Resource
private IGatewayOperationLogService gatewayOperationLogService;
/**
* 开启直播
*
* @param videoInfoVo
* @return
*/
@ApiOperation(value = "开启直播")
@PostMapping("/live/start")
public AjaxResult startLive(@RequestBody VideoInfoVo videoInfoVo) {
String topic = SERVICES_TOPIC.replace("?", videoInfoVo.getGateway());
CommonFieldsVo commonFieldVo = CommonFieldsVo.createCommonFieldVo(CmdConstant.LIVE_START_PUSH, videoInfoVo.getGateway());
// 把数据进行MD5
String key = videoInfoVo.getGateway() + "," + videoInfoVo.getSn() + "," + videoInfoVo.getCameraIndex() + "," + videoInfoVo.getVideoIndex();
String encrypt = AESUtil.encrypt(key);
// 准备参数
Map<String, Object> data = new HashMap<>();
data.put("url_type", 1); // 设置格式为 RTMP
String rtmpUrl = "rtmp://" + videoInfoVo.getHost() + ":" + videoInfoVo.getRtmpPort() + "/live/" + key.replaceAll(",", "");
data.put("url", rtmpUrl);
data.put("video_id", videoInfoVo.getSn() + "/" + videoInfoVo.getCameraIndex() + "/" + videoInfoVo.getVideoIndex());
data.put("video_quality", videoInfoVo.getDefinition()); // 设置直播质量
String payload = PayloadUtil.generatePayload(commonFieldVo, data);
String response = djiCloudMQTTClient.publishMessage(topic, payload, commonFieldVo);
// 记录操作日志
GatewayOperationLogUtil.generateLog(videoInfoVo.getGateway(), CmdConstant.LIVE_START_PUSH, payload, response);
Map<String, Object> resultData = new HashMap<>();
String webrtcUrl = "http://" + videoInfoVo.getHost() + ":" + videoInfoVo.getRtcPort() + "/rtc/v1/whep/?app=live&stream=" + key.replaceAll(",", "");
resultData.put("url", webrtcUrl);
try {
ObjectMapper objectMapper = new ObjectMapper();
// 将字符串解析为 JSON 对象
ObjectNode jsonNode = null;
jsonNode = (ObjectNode) objectMapper.readTree(response);
JsonNode resultNode = jsonNode.path("data").path("result");
Integer result = Integer.valueOf(resultNode.asText());
// 根据 result 值判断是否成功
if (result == 0 || (result != 0 && result == 513003)) {
return AjaxResult.success("开启直播成功").put("data", resultData);
} else {
return HandleResultUtil.handleResult(response, resultData, "开启直播失败");
}
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
/**
* 关闭直播
*/
@ApiOperation(value = "关闭直播")
@PostMapping("/live/stop")
public AjaxResult stopLive(@RequestBody VideoInfoVo videoInfoVo) {
String topic = SERVICES_TOPIC.replace("?", videoInfoVo.getGateway());
CommonFieldsVo commonFieldVo = CommonFieldsVo.createCommonFieldVo(CmdConstant.LIVE_STOP_PUSH, videoInfoVo.getGateway());
// 准备参数
Map<String, Object> data = new HashMap<>();
data.put("video_id", videoInfoVo.getSn() + "/" + videoInfoVo.getCameraIndex() + "/" + videoInfoVo.getVideoIndex());
String payload = PayloadUtil.generatePayload(commonFieldVo, data);
String response = djiCloudMQTTClient.publishMessage(topic, PayloadUtil.generatePayload(commonFieldVo, data), commonFieldVo);
// 记录操作日志
GatewayOperationLogUtil.generateLog(videoInfoVo.getGateway(), CmdConstant.LIVE_STOP_PUSH, payload, response);
AjaxResult ajaxResult = HandleResultUtil.handleResult(response, null, "关闭直播成功");
return ajaxResult;
}
/**
* 镜头切换
*/
@ApiOperation(value = "镜头切换")
@PostMapping("/live/change")
public AjaxResult liveLensChange(@RequestBody VideoInfoChangeVo videoInfoChangeVo) {
String topic = SERVICES_TOPIC.replace("?", videoInfoChangeVo.getGateway());
CommonFieldsVo commonFieldVo = CommonFieldsVo.createCommonFieldVo(CmdConstant.LIVE_LENS_CHANGE, videoInfoChangeVo.getGateway());
// 准备参数
Map<String, Object> data = new HashMap<>();
data.put("video_id", videoInfoChangeVo.getSn() + "/" + videoInfoChangeVo.getCameraIndex() + "/" + videoInfoChangeVo.getVideoIndex());
data.put("video_type", videoInfoChangeVo.getVideoType());
String payload = PayloadUtil.generatePayload(commonFieldVo, data);
String response = djiCloudMQTTClient.publishMessage(topic, PayloadUtil.generatePayload(commonFieldVo, data), commonFieldVo);
// 记录操作日志
GatewayOperationLogUtil.generateLog(videoInfoChangeVo.getGateway(), CmdConstant.LIVE_LENS_CHANGE, payload, response);
AjaxResult ajaxResult = HandleResultUtil.handleResult(response, null, "镜头切换成功");
return ajaxResult;
}
/**
* 设置直播质量
*/
@ApiOperation(value = "设置直播质量")
@PostMapping("/live/quality")
public AjaxResult liveSetQuality(@RequestBody VideoInfoQualityVo videoInfoQualityVo) {
String topic = SERVICES_TOPIC.replace("?", videoInfoQualityVo.getGateway());
CommonFieldsVo commonFieldVo = CommonFieldsVo.createCommonFieldVo(CmdConstant.LIVE_SET_QUALITY, videoInfoQualityVo.getGateway());
// 准备参数
Map<String, Object> data = new HashMap<>();
data.put("video_id", videoInfoQualityVo.getSn() + "/" + videoInfoQualityVo.getCameraIndex() + "/" + videoInfoQualityVo.getVideoIndex());
data.put("video_quality", videoInfoQualityVo.getVideoQuality());
String payload = PayloadUtil.generatePayload(commonFieldVo, data);
String response = djiCloudMQTTClient.publishMessage(topic, PayloadUtil.generatePayload(commonFieldVo, data), commonFieldVo);
// 记录操作日志
GatewayOperationLogUtil.generateLog(videoInfoQualityVo.getGateway(), CmdConstant.LIVE_SET_QUALITY, payload, response);
String videoQuality = null;
switch (videoInfoQualityVo.getVideoQuality()) {
case 0:
videoQuality = "自适应";
break;
case 1:
videoQuality = "流畅";
break;
case 2:
videoQuality = "标清";
break;
case 3:
videoQuality = "高清";
break;
default:
videoQuality = "超清";
}
String message = "已为您切换" + videoQuality + "摄像头";
AjaxResult ajaxResult = HandleResultUtil.handleResult(response, null, message);
return ajaxResult;
}
/**
* 直播相机切换
*/
@ApiOperation(value = "直播相机切换")
@PostMapping("/live/camera/change")
public AjaxResult liveCameraChange(@RequestBody liveCameraChangeDto linkedCameraChangeDto) {
String topic = SERVICES_TOPIC.replace("?", linkedCameraChangeDto.getGateway());
CommonFieldsVo commonFieldVo = CommonFieldsVo.createCommonFieldVo(CmdConstant.LIVE_CAMERA_CHANGE, linkedCameraChangeDto.getGateway());
// 准备参数
Map<String, Object> data = new HashMap<>();
data.put("video_id", linkedCameraChangeDto.getSn() + "/" + linkedCameraChangeDto.getCameraIndex() + "/" + linkedCameraChangeDto.getVideoIndex());
data.put("camera_position", linkedCameraChangeDto.getCameraPosition());
String payload = PayloadUtil.generatePayload(commonFieldVo, data);
log.info("直播相机切换请求参数:" + payload);
String response = djiCloudMQTTClient.publishMessage(topic, PayloadUtil.generatePayload(commonFieldVo, data), commonFieldVo);
// 记录操作日志
GatewayOperationLogUtil.generateLog(linkedCameraChangeDto.getGateway(), CmdConstant.LIVE_CAMERA_CHANGE, payload, response);
String value = linkedCameraChangeDto.getCameraPosition() == 0 ? "舱内" : "舱外";
String msg = "已为您切换" + value + "摄像头";
AjaxResult ajaxResult = HandleResultUtil.handleResult(response, null, msg);
return ajaxResult;
}
}

View File

@ -0,0 +1,286 @@
package com.ruoyi.dj.controller;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.dj.mqtt.domain.DeleteBatchRequest;
import com.ruoyi.dj.mqtt.utils.StorageConfigUtil;
import com.ruoyi.system.domain.Missions;
import com.ruoyi.system.service.IMissionsService;
import io.minio.BucketExistsArgs;
import io.minio.MakeBucketArgs;
import io.minio.MinioClient;
import io.minio.errors.ErrorResponseException;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.text.SimpleDateFormat;
import java.util.*;
/**
* @auther 周志雄
* @date 2024/9/2 15:17
*/
@Slf4j
@RestController
@Api(tags = "Minio文件管理")
@RequestMapping("/minio")
public class MinioController {
@Resource
private IMissionsService missionsService;
// 创建一个线程安全的 MinioClient 实例,避免重复创建
private static final MinioClient minioClient = StorageConfigUtil.getMinioClient();
@GetMapping("/file")
@ApiOperation("获取文件列表")
public String getFile(String gateway, String itemPath) throws Exception {
// 构建存储桶名称
String bucketName = "drone" + gateway.toLowerCase().replace("_", "").replace("-", "");
// 检查桶是否存在,不存在则创建
synchronized (this) { // 使用 synchronized 避免并发创建
if (!minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build())) {
try {
minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
log.info("已创建桶: {}", bucketName);
} catch (Exception e) {
if (e instanceof ErrorResponseException &&
((ErrorResponseException) e).errorResponse().code().equals("BucketAlreadyOwnedByYou")) {
log.warn("桶已存在: {}", bucketName);
} else {
throw e; // 如果是其他异常,继续抛出
}
}
}
}
String listDirectoryContents = StorageConfigUtil.listDirectoryContents(bucketName, itemPath);
log.info("文件列表: {}", listDirectoryContents);
if (listDirectoryContents == null) {
return null;
}
if (itemPath == null || itemPath.equals("")) {
ObjectMapper mapper = new ObjectMapper();
JsonNode rootNode = mapper.readTree(listDirectoryContents);
// 遍历所有的键
Iterator<String> fieldNames = rootNode.fieldNames();
while (fieldNames.hasNext()) {
String key = fieldNames.next();
String idStr = key.split("/")[0]; // 提取 ID
Long id;
try {
id = Long.valueOf(idStr);
} catch (NumberFormatException e) {
continue;
}
// 获取任务名称
Missions missions = new Missions();
missions.setFlightId(id);
List<Missions> missionsList = missionsService.selectMissionsList(missions);
String missionName = idStr; // 默认使用 ID
if (missionsList != null && !missionsList.isEmpty()) {
Date createdAt = missionsList.get(0).getCreatedAt();
missionName = missionsList.get(0).getMissionName() + "(" + new SimpleDateFormat("yyyyMMdd").format(createdAt) + ")";
// 在对象中添加 missionName 字段
JsonNode valueNode = rootNode.get(key);
if (valueNode instanceof ObjectNode) {
((ObjectNode) valueNode).put("missionName", missionName);
}
}
}
// 将修改后的节点转换为 JSON 字符串并返回
return mapper.writeValueAsString(rootNode);
}
return listDirectoryContents;
}
@PostMapping("/urlList")
@ApiOperation("获取某个目录下面的所有文件的URL列表")
public List<String> urlList(String gateway, String itemPath) throws Exception {
// 构建存储桶名称
String bucketName = "drone" + gateway.toLowerCase().replace("_", "").replace("-", "");
// 检查桶是否存在,不存在则创建
synchronized (this) { // 使用 synchronized 避免并发创建
if (!minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build())) {
try {
minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
log.info("已创建桶: {}", bucketName);
} catch (Exception e) {
if (e instanceof ErrorResponseException &&
((ErrorResponseException) e).errorResponse().code().equals("BucketAlreadyOwnedByYou")) {
log.warn("桶已存在: {}", bucketName);
} else {
throw e; // 如果是其他异常,继续抛出
}
}
}
}
List<String> urlList = StorageConfigUtil.getUrlList(bucketName, itemPath);
return urlList;
}
@GetMapping("/info")
@ApiOperation("获取文件统计信息")
public AjaxResult info(@RequestParam String gateway) throws Exception {
// 构建存储桶名称
String bucketName = "drone" + gateway.toLowerCase().replace("_", "").replace("-", "");
// 检查桶是否存在,不存在则创建
synchronized (this) { // 使用 synchronized 避免并发创建
if (!minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build())) {
try {
minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
log.info("已创建桶: {}", bucketName);
} catch (Exception e) {
if (e instanceof ErrorResponseException &&
((ErrorResponseException) e).errorResponse().code().equals("BucketAlreadyOwnedByYou")) {
log.warn("桶已存在: {}", bucketName);
} else {
throw e; // 如果是其他异常,继续抛出
}
}
}
}
Map<String, Object> statistics = StorageConfigUtil.getMediaStatistics(bucketName);
return AjaxResult.success(statistics);
}
@DeleteMapping("/deleteBatch")
@ApiOperation("批量删除文件")
public AjaxResult deleteBatch(@RequestBody DeleteBatchRequest deleteBatchRequest) throws Exception {
// 调用批量删除方法
StorageConfigUtil.deleteFilesOrDirectories(deleteBatchRequest.getItemPaths(), deleteBatchRequest.getGateway());
return AjaxResult.success();
}
@GetMapping("/ai/file")
@ApiOperation("获取AI文件列表")
public String getAiFile(String gateway, String itemPath) throws Exception {
// 构建存储桶名称
String bucketName = "ai" + gateway.toLowerCase().replace("_", "").replace("-", "");
// 检查桶是否存在,不存在则创建
synchronized (this) { // 使用 synchronized 避免并发创建
if (!minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build())) {
try {
minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
log.info("已创建桶: {}", bucketName);
} catch (Exception e) {
if (e instanceof ErrorResponseException &&
((ErrorResponseException) e).errorResponse().code().equals("BucketAlreadyOwnedByYou")) {
log.warn("桶已存在: {}", bucketName);
} else {
throw e; // 如果是其他异常,继续抛出
}
}
}
}
String listDirectoryContents = StorageConfigUtil.listDirectoryContents(bucketName, itemPath);
if (listDirectoryContents == null) {
return null;
}
if (itemPath == null || itemPath.equals("")) {
ObjectMapper mapper = new ObjectMapper();
JsonNode rootNode = mapper.readTree(listDirectoryContents);
// 遍历所有的键
Iterator<String> fieldNames = rootNode.fieldNames();
while (fieldNames.hasNext()) {
String key = fieldNames.next(); // 例如 "881665/"
String idStr = key.split("/")[0]; // 提取 ID
Long id;
try {
id = Long.valueOf(idStr);
} catch (NumberFormatException e) {
continue;
}
// 获取任务名称
Missions missions = new Missions();
missions.setFlightId(id);
List<Missions> missionsList = missionsService.selectMissionsList(missions);
String missionName = idStr; // 默认使用 ID
if (missionsList != null && !missionsList.isEmpty()) {
Date createdAt = missionsList.get(0).getCreatedAt();
missionName = missionsList.get(0).getMissionName() + "(" + new SimpleDateFormat("yyyyMMdd").format(createdAt) + ")";
// 在对象中添加 missionName 字段
JsonNode valueNode = rootNode.get(key);
if (valueNode instanceof ObjectNode) {
((ObjectNode) valueNode).put("missionName", missionName);
}
}
}
// 将修改后的节点转换为 JSON 字符串并返回
return mapper.writeValueAsString(rootNode);
}
return listDirectoryContents;
}
@PostMapping("/ai/urlList")
@ApiOperation("获取AI检测某个目录下面的所有文件的URL列表")
public List<String> aiUrlList(String gateway, String itemPath) throws Exception {
// 构建存储桶名称
String bucketName = "ai" + gateway.toLowerCase().replace("_", "").replace("-", "");
// 检查桶是否存在,不存在则创建
synchronized (this) { // 使用 synchronized 避免并发创建
if (!minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build())) {
try {
minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
log.info("已创建桶: {}", bucketName);
} catch (Exception e) {
if (e instanceof ErrorResponseException &&
((ErrorResponseException) e).errorResponse().code().equals("BucketAlreadyOwnedByYou")) {
log.warn("桶已存在: {}", bucketName);
} else {
throw e; // 如果是其他异常,继续抛出
}
}
}
}
// 调用批量下载方法
List<String> urlList = StorageConfigUtil.getUrlList(bucketName, itemPath);
return urlList;
}
@GetMapping("/ai/info")
@ApiOperation("获取AI文件统计信息")
public AjaxResult aiInfo(@RequestParam String gateway) throws Exception {
// 构建存储桶名称
String bucketName = "ai" + gateway.toLowerCase().replace("_", "").replace("-", "");
// 检查桶是否存在,不存在则创建
synchronized (this) { // 使用 synchronized 避免并发创建
if (!minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build())) {
try {
minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
log.info("已创建桶: {}", bucketName);
} catch (Exception e) {
if (e instanceof ErrorResponseException &&
((ErrorResponseException) e).errorResponse().code().equals("BucketAlreadyOwnedByYou")) {
log.warn("桶已存在: {}", bucketName);
} else {
throw e; // 如果是其他异常,继续抛出
}
}
}
}
Map<String, Object> statistics = StorageConfigUtil.getMediaStatistics(bucketName);
return AjaxResult.success(statistics);
}
@DeleteMapping("/ai/deleteBatch")
@ApiOperation("批量删除文件")
public AjaxResult aiDeleteBatch(@RequestBody DeleteBatchRequest deleteBatchRequest) throws Exception {
// 调用批量删除方法
StorageConfigUtil.deleteAiFilesOrDirectories(deleteBatchRequest.getItemPaths(), deleteBatchRequest.getGateway());
return AjaxResult.success();
}
}

View File

@ -0,0 +1,108 @@
package com.ruoyi.dj.controller;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.dj.mqtt.client.DJICloudMQTTClient;
import com.ruoyi.dj.mqtt.cmd.CmdConstant;
import com.ruoyi.dj.mqtt.common.CommonFieldsVo;
import com.ruoyi.dj.mqtt.dto.DataParamDto;
import com.ruoyi.dj.mqtt.dto.DataParamDto01;
import com.ruoyi.dj.mqtt.dto.RemoteServiceDto;
import com.ruoyi.dj.mqtt.utils.GatewayOperationLogUtil;
import com.ruoyi.dj.mqtt.utils.HandleResultUtil;
import com.ruoyi.dj.mqtt.utils.PayloadUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@RestController
@Api(tags = "远程调试相关")
@RequestMapping("/dj")
public class MqttController {
@Resource
private DJICloudMQTTClient djiCloudMQTTClient;
private static final String SERVICES_TOPIC = "thing/product/?/services";
/**
* 发送和Service相关的无数据命令并等待其回复
*
* @return
*/
@ApiOperation("发送无数据命令")
@PostMapping("/remote/debug/service")
public AjaxResult debugModeOpen(@RequestBody RemoteServiceDto remoteServiceDto) {
String topic = SERVICES_TOPIC.replace("?", remoteServiceDto.getGateway());
CommonFieldsVo commonFieldVo = CommonFieldsVo.createCommonFieldVo(remoteServiceDto.getMethod(), remoteServiceDto.getGateway());
String payload = PayloadUtil.generatePayload(commonFieldVo, null);
log.info("远程调试相关发送数据:" + payload);
String response = djiCloudMQTTClient.publishMessage(topic, payload, commonFieldVo);
log.info("远程调试相关返回数据:" + response);
AjaxResult ajaxResult = HandleResultUtil.handleResult(response, null,"操作成功");
// 记录日志
GatewayOperationLogUtil.generateLog(remoteServiceDto.getGateway(), remoteServiceDto.getMethod(), payload, response);
return ajaxResult;
}
/**
* 电池维护开关切换
*
* @return
*/
@ApiOperation("电池维护开关切换")
@PostMapping("/remote/debug/batteryMaintenanceSwitch")
public AjaxResult batteryMaintenanceSwitch(@RequestBody DataParamDto dataParam) {
Map map = new HashMap();
if (dataParam.getAction().equals("supplement_light_open")) {
map.put("msg", "补光灯已打开");
} else if (dataParam.getAction().equals("supplement_light_close")) {
map.put("msg", "补光灯已关闭");
}
String topic = SERVICES_TOPIC.replace("?", dataParam.getGateway());
CommonFieldsVo commonFieldVo = CommonFieldsVo.createCommonFieldVo(CmdConstant.BATTERY_MAINTENANCE_SWITCH, dataParam.getGateway());
String payload = PayloadUtil.generatePayload(commonFieldVo, dataParam.getAction());
String response = djiCloudMQTTClient.publishMessage(topic, payload, commonFieldVo);
return HandleResultUtil.handleResult(response, map,"电池维护开关切换切换成功");
}
/**
* 空调模式开关切换
*
* @return
*/
@ApiOperation("空调模式开关切换")
@PostMapping("/remote/debug/airConditionerModeSwitch")
public AjaxResult airConditionerModeSwitch(@RequestBody DataParamDto01 dataParam) {
String topic = SERVICES_TOPIC.replace("?", dataParam.getGateway());
CommonFieldsVo commonFieldVo = CommonFieldsVo.createCommonFieldVo(CmdConstant.AIR_CONDITIONER_MODE_SWITCH, dataParam.getGateway());
HashMap<Object, Object> map = new HashMap<>();
map.put("action", dataParam.getAction());
String payload = PayloadUtil.generatePayload(commonFieldVo, map);
String response = djiCloudMQTTClient.publishMessage(topic, payload, commonFieldVo);
return HandleResultUtil.handleResult(response, null,"空调模式开关切换成功");
}
/**
* 增强图传开关
*
* @return
*/
@ApiOperation("增强图传开关")
@PostMapping("/remote/debug/sdrWorkmodeSwitch")
public AjaxResult sdrWorkmodeSwitch(@RequestBody DataParamDto dataParam) {
String topic = SERVICES_TOPIC.replace("?", dataParam.getGateway());
CommonFieldsVo commonFieldVo = CommonFieldsVo.createCommonFieldVo(CmdConstant.SDR_WORKMODE_SWITCH, dataParam.getGateway());
Map<String, Object> map = new HashMap<>();
map.put("link_workmode", dataParam.getAction());
String payload = PayloadUtil.generatePayload(commonFieldVo, map);
String response = djiCloudMQTTClient.publishMessage(topic, payload, commonFieldVo);
return HandleResultUtil.handleResult(response, null,"增强图传开关成功");
}
}

View File

@ -0,0 +1,372 @@
package com.ruoyi.dj.controller;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.dj.mqtt.client.DJICloudMQTTClient;
import com.ruoyi.dj.mqtt.cmd.CmdConstant;
import com.ruoyi.dj.mqtt.common.CommonFieldsVo;
import com.ruoyi.dj.mqtt.dto.*;
import com.ruoyi.dj.mqtt.utils.GatewayOperationLogUtil;
import com.ruoyi.dj.mqtt.utils.HandleResultUtil;
import com.ruoyi.dj.mqtt.utils.PayloadUtil;
import com.ruoyi.system.domain.*;
import com.ruoyi.system.dto.MissionsDto;
import com.ruoyi.system.dto.MissionsListDto;
import com.ruoyi.system.service.IFlightPathsService;
import com.ruoyi.system.service.IMissionsService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.models.auth.In;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.*;
/**
* @auther 周志雄
* @date 2024/9/14 14:48
*/
@Slf4j
@RestController
@Api(tags = "新航线任务相关")
@RequestMapping("/dj/task")
public class NewTaskController extends BaseController {
private static final String SERVICES_TOPIC = "thing/product/?/services";
@Resource
private DJICloudMQTTClient djiCloudMQTTClient;
@Resource
private IMissionsService missionsService;
@Resource
private IFlightPathsService flightPathsService;
@PostMapping("/add")
@ApiOperation("添加任务")
public AjaxResult add(@RequestBody Missions missions) {
// 获取文件ID
String fileId = missions.getFileId();
// 查询数据
FlightPaths flightPaths = flightPathsService.selectFlightPathsById(Long.valueOf(fileId));
// 如果没有文件、则返回错误
if (flightPaths != null && flightPaths.getFileUrl() == null) {
return AjaxResult.error("请先在航线绘制内容");
}
if (missions.getType() == 1) {
missions.setState("定时待执行");
}
// 任务有效
missions.setFlag(1);
// 新增数据到数据库
missions.setCreatedAt(new Date());
missionsService.insertMissions(missions);
// 处理业务数据、如果是立即任务则下发并执行
if (missions.getType() == 0) {
// TODO 下发任务
Map<String, String> map = flightTaskPrepare(missions);
String flightId = map.get("flightId");
String taskId = map.get("taskId");
// TODO 执行任务
FlightTaskExecuteDto flightTaskExecuteDto = new FlightTaskExecuteDto();
flightTaskExecuteDto.setFlightId(flightId);
flightTaskExecuteDto.setGateway(missions.getGateway());
flightTaskExecuteDto.setTaskId(Long.valueOf(taskId));
return executeTask(flightTaskExecuteDto);
}
return AjaxResult.success();
}
@PostMapping("/cancel")
@ApiOperation("挂起任务")
public AjaxResult cancel(String missionId) {
Missions missions = new Missions();
missions.setId(Long.valueOf(missionId));
missions.setFlag(0);
missionsService.updateMissions(missions);
return AjaxResult.success();
}
@GetMapping("/list")
@ApiOperation("任务列表")
public TableDataInfo list(MissionsListDto missionListDto) {
startPage();
// 获取所有符合条件的任务
List<MissionsDto> missionsList = missionsService.list(missionListDto);
return getDataTable(missionsList);
}
@GetMapping("/getLatest")
@ApiOperation("获取最新任务信息")
public AjaxResult getLatest(String gateway) {
// 获取所有符合条件的任务
MissionsDto missionsDto = missionsService.getLatest(gateway);
return AjaxResult.success(missionsDto);
}
/**
* 取消任务
*
* @return
*/
@ApiOperation("取消任务")
@PostMapping("/flightTaskUndo")
public AjaxResult flightTaskUndo(@RequestBody FlightTaskUndoDto flightTaskUndoDto) {
String topic = SERVICES_TOPIC.replace("?", flightTaskUndoDto.getGateway());
CommonFieldsVo commonFieldVo = CommonFieldsVo.createCommonFieldVo(CmdConstant.FLIGHTTASK_UNDO, flightTaskUndoDto.getGateway());
// 构造参数
Map<String, Object> map = new HashMap<>();
map.put("flight_ids", flightTaskUndoDto.getFlightIds());
String payload = PayloadUtil.generatePayload(commonFieldVo, map);
log.info("取消航线请求数据:{}", payload);
String response = djiCloudMQTTClient.publishMessage(topic, payload, commonFieldVo);
// 记录操作日志
GatewayOperationLogUtil.generateLog(flightTaskUndoDto.getGateway(), CmdConstant.FLIGHTTASK_UNDO, payload, response);
return HandleResultUtil.handleResult(response, null, "取消任务成功");
}
/**
* 暂停任务
*
* @return
*/
@ApiOperation("暂停任务")
@PostMapping("/flightTaskPause")
public AjaxResult flightTaskPause(@RequestBody FlightTaskPauseDto flightTaskPauseDto) {
String topic = SERVICES_TOPIC.replace("?", flightTaskPauseDto.getGateway());
CommonFieldsVo commonFieldVo = CommonFieldsVo.createCommonFieldVo(CmdConstant.FLIGHTTASK_PAUSE, flightTaskPauseDto.getGateway());
String payload = PayloadUtil.generatePayload(commonFieldVo, null);
log.info("暂停航线请求数据:{}", payload);
String response = djiCloudMQTTClient.publishMessage(topic, payload, commonFieldVo);
// 记录操作日志
GatewayOperationLogUtil.generateLog(flightTaskPauseDto.getGateway(), CmdConstant.FLIGHTTASK_PAUSE, payload, response);
return HandleResultUtil.handleResult(response, null, "暂停任务成功");
}
/**
* 航线恢复
*
* @return
*/
@ApiOperation("航线恢复")
@PostMapping("/flightTaskRecovery")
public AjaxResult flightTaskRecovery(@RequestBody FlightTaskRecoveryDto flightTaskRecoveryDto) {
String topic = SERVICES_TOPIC.replace("?", flightTaskRecoveryDto.getGateway());
CommonFieldsVo commonFieldVo = CommonFieldsVo.createCommonFieldVo(CmdConstant.FLIGHTTASK_RECOVERY, flightTaskRecoveryDto.getGateway());
String payload = PayloadUtil.generatePayload(commonFieldVo, null);
log.info("恢复航线请求数据:{}", payload);
String response = djiCloudMQTTClient.publishMessage(topic, payload, commonFieldVo);
// 记录操作日志
GatewayOperationLogUtil.generateLog(flightTaskRecoveryDto.getGateway(), CmdConstant.FLIGHTTASK_RECOVERY, payload, response);
return HandleResultUtil.handleResult(response, null, "航线恢复成功");
}
/**
* 一键返航
*
* @return
*/
@ApiOperation("一键返航")
@PostMapping("/returnHome")
public AjaxResult returnHome(@RequestBody ReturnHomeDto returnHomeDto) {
String topic = SERVICES_TOPIC.replace("?", returnHomeDto.getGateway());
CommonFieldsVo commonFieldVo = CommonFieldsVo.createCommonFieldVo(CmdConstant.RETURN_HOME, returnHomeDto.getGateway());
String payload = PayloadUtil.generatePayload(commonFieldVo, null);
log.info("一键返航请求数据:{}", payload);
String response = djiCloudMQTTClient.publishMessage(topic, payload, commonFieldVo);
// 记录操作日志
GatewayOperationLogUtil.generateLog(returnHomeDto.getGateway(), CmdConstant.RETURN_HOME, payload, response);
return HandleResultUtil.handleResult(response, null, "一键返航成功");
}
/**
* 下发任务
*
* @param missions
* @return
*/
public Map<String, String> flightTaskPrepare(Missions missions) {
HashMap<String, String> hashMap = new HashMap<>();
String topic = SERVICES_TOPIC.replace("?", missions.getGateway());
CommonFieldsVo commonFieldVo = CommonFieldsVo.createCommonFieldVo(CmdConstant.FLIGHTTASK_PREPARE, missions.getGateway());
// 先生成一个随机的数字字符串作为任务ID
String flightId = String.valueOf((int) (Math.random() * 900000) + 100000);
// 把任务对象转化为 FlightTaskParam 参数、便于构建下发任务的 JSON 参数
String fileId = missions.getFileId();
FlightPaths flightPaths = flightPathsService.selectFlightPathsById(Long.valueOf(fileId));
FlightTaskParam flightTaskParam = getFlightTaskParam(missions, flightId, flightPaths);
flightTaskParam.setTaskType(missions.getType());
// 设置执行时间为当前时间戳
flightTaskParam.setExecuteTime(missions.getTimeStamp());
Map<String, Object> map = convertDtoToMap(flightTaskParam);
String payload = PayloadUtil.generatePayload(commonFieldVo, map);
String response = djiCloudMQTTClient.publishMessage(topic, payload, commonFieldVo);
// 记录操作日志
GatewayOperationLogUtil.generateLog(missions.getGateway(), CmdConstant.FLIGHTTASK_PREPARE, payload, response);
ObjectMapper objectMapper = new ObjectMapper();
// 将字符串解析为 JSON 对象
JsonNode resultNode = null;
try {
ObjectNode jsonNode = null;
jsonNode = (ObjectNode) objectMapper.readTree(response);
resultNode = jsonNode.path("data").path("result");
Integer result = Integer.valueOf(resultNode.asText());
if (result == 0) {
// 执行指令没有出错则更新下发任务数据到数据库
missions.setFlightId(Long.valueOf(flightId));
missions.setState("待执行");
missionsService.updateMissions(missions);
}
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
// 返回任务ID
hashMap.put("flightId", flightId);
hashMap.put("taskId", missions.getId().toString());
return hashMap;
}
/**
* 根据droneMissions获取FlightTaskParam
*
* @param droneMissions
* @return
*/
private static FlightTaskParam getFlightTaskParam(Missions droneMissions, String flightId, FlightPaths flightPaths) {
FlightTaskParam flightTaskParam = new FlightTaskParam();
// 设置计划ID
flightTaskParam.setFlightId(flightId.toString());
// 创建航线文件对象
FlightTaskParam.File file = new FlightTaskParam.File();
// 设置航线文件MD5
file.setFingerprint(flightPaths.getFileMd5());
// 设置航线文件URL
file.setUrl(flightPaths.getFileUrl());
// 设置文件信息
flightTaskParam.setFile(file);
// 设置返航高度
flightTaskParam.setRthAltitude(Integer.valueOf(droneMissions.getReturnAltitude().toString()));
// 设置返航高度模式
flightTaskParam.setRthMode(Integer.valueOf(droneMissions.getReturnAltitudeMode().toString()));
// 设置遥控器失控类型
flightTaskParam.setOutOfControlAction(Integer.valueOf(droneMissions.getOutOfControlAction().toString()));
// 设置航线失控动作
flightTaskParam.setExitWaylineWhenRcLost(Integer.valueOf(droneMissions.getFlightControlAction().toString()));
// 设置航线精度类型
flightTaskParam.setWaylinePrecisionType(Integer.valueOf(droneMissions.getWaylinePrecisionType().toString()));
log.info("构造航线下发的参数:{}", flightTaskParam);
return flightTaskParam;
}
/**
* 构造航线下发的参数
*
* @param flightTaskParam
* @return
*/
private Map<String, Object> convertDtoToMap(FlightTaskParam flightTaskParam) {
Map<String, Object> dataMap = new HashMap<>();
// 如果断点信息不为空,则加入到 Map
if (flightTaskParam.getBreakPoint() != null) {
dataMap.put("break_point", flightTaskParam.getBreakPoint());
}
// 如果执行条件不为空,则加入到 Map
if (flightTaskParam.getExecutableConditions() != null) {
dataMap.put("executable_conditions", flightTaskParam.getExecutableConditions());
}
// 如果有指定任务执行时间,则加入到 Map
if (flightTaskParam.getExecuteTime() != null) {
dataMap.put("execute_time", flightTaskParam.getExecuteTime());
}
// 如果文件信息不为空,则加入到 Map
if (flightTaskParam.getFile() != null) {
dataMap.put("file", flightTaskParam.getFile());
}
// 如果有计划 ID则加入到 Map
if (flightTaskParam.getFlightId() != null) {
dataMap.put("flight_id", flightTaskParam.getFlightId());
}
// 如果有定义遥控器失控动作,则加入到 Map
if (flightTaskParam.getOutOfControlAction() != null) {
dataMap.put("out_of_control_action", flightTaskParam.getOutOfControlAction());
}
// 如果有就绪条件,则加入到 Map
if (flightTaskParam.getReadyConditions() != null) {
dataMap.put("ready_conditions", flightTaskParam.getReadyConditions());
}
// 如果模拟任务不为空,则加入到 Map
if (flightTaskParam.getSimulateMission() != null) {
dataMap.put("simulate_mission", flightTaskParam.getSimulateMission());
}
// 如果有返航高度,则加入到 Map
if (flightTaskParam.getRthAltitude() != null) {
dataMap.put("rth_altitude", flightTaskParam.getRthAltitude());
}
// 如果有返航高度模式,则加入到 Map
if (flightTaskParam.getRthMode() != null) {
dataMap.put("rth_mode", flightTaskParam.getRthMode());
}
// 如果有航线失控动作,则加入到 Map
if (flightTaskParam.getExitWaylineWhenRcLost() != null) {
dataMap.put("exit_wayline_when_rc_lost", flightTaskParam.getExitWaylineWhenRcLost());
}
// 如果有航线类型,则加入到 Map
if (flightTaskParam.getTaskType() != null) {
dataMap.put("task_type", flightTaskParam.getTaskType());
}
// 任务类型和航线精度类型始终加入到 Map因为它们为基础属性
dataMap.put("task_type", flightTaskParam.getTaskType());
dataMap.put("wayline_precision_type", flightTaskParam.getWaylinePrecisionType());
return dataMap;
}
/**
* 执行任务
*
* @param flightTaskExecuteDto
* @return
*/
public AjaxResult executeTask(FlightTaskExecuteDto flightTaskExecuteDto) {
String topic = SERVICES_TOPIC.replace("?", flightTaskExecuteDto.getGateway());
CommonFieldsVo commonFieldVo = CommonFieldsVo.createCommonFieldVo(CmdConstant.FLIGHTTASK_EXECUTE, flightTaskExecuteDto.getGateway());
// 构造参数
Map<String, Object> map = new HashMap<>();
map.put("flight_id", flightTaskExecuteDto.getFlightId());
String payload = PayloadUtil.generatePayload(commonFieldVo, map);
log.info("执行航线请求数据:{}", payload);
String response = djiCloudMQTTClient.publishMessage(topic, payload, commonFieldVo);
// 记录操作日志
GatewayOperationLogUtil.generateLog(flightTaskExecuteDto.getGateway(), CmdConstant.FLIGHTTASK_EXECUTE, payload, response);
ObjectMapper objectMapper = new ObjectMapper();
// 将字符串解析为 JSON 对象
JsonNode resultNode = null;
try {
ObjectNode jsonNode = null;
jsonNode = (ObjectNode) objectMapper.readTree(response);
resultNode = jsonNode.path("data").path("result");
Integer result = Integer.valueOf(resultNode.asText());
if (result == 0) {
Missions missions = new Missions();
missions.setFlightId(Long.valueOf(flightTaskExecuteDto.getFlightId()));
Missions mission = missionsService.selectMissionsList(missions).get(0);
mission.setState("执行中");
mission.setExecTime(new Date());
missionsService.updateMissions(mission);
}
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
return HandleResultUtil.handleResult(response, null, "执行任务成功");
}
}

View File

@ -0,0 +1,58 @@
package com.ruoyi.dj.controller;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.dj.mqtt.client.DJICloudMQTTClient;
import com.ruoyi.dj.mqtt.common.CommonFieldsVo;
import com.ruoyi.dj.mqtt.dto.PropertySetDto;
import com.ruoyi.dj.mqtt.utils.PayloadUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.concurrent.CompletableFuture;
/**
* @auther 周志雄
* @date 2024/9/10 10:44
*/
@Slf4j
@Api(tags = "属性相关")
@RestController
@RequestMapping("/property")
public class PropertyController {
@Resource
private DJICloudMQTTClient djiCloudMQTTClient;
private static final String PROPERTY_TOPIC = "thing/product/?/property/set";
/**
* 属性设置
*
* @param propertySetDto
* @return
*/
@ApiOperation("属性设置")
@PostMapping("/set")
public AjaxResult set(@RequestBody PropertySetDto propertySetDto) {
String gateway = propertySetDto.getGateway();
String topic = PROPERTY_TOPIC.replace("?", propertySetDto.getGateway());
CommonFieldsVo commonFieldVo = CommonFieldsVo.createCommonFieldVo(null, propertySetDto.getGateway());
String payload = PayloadUtil.generatePayload(commonFieldVo, propertySetDto.getParams());
log.info("设置属性payload:{}", payload);
// 发送消息之后直接成功、不需要等待回复
CompletableFuture.runAsync(() -> {
djiCloudMQTTClient.publishMessage(topic, payload, commonFieldVo);
});
try {
Thread.sleep(300);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return AjaxResult.success("操作成功");
}
}

View File

@ -0,0 +1,81 @@
package com.ruoyi.dj.controller;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.dj.mqtt.utils.StorageConfigUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@Slf4j
@RestController
@RequestMapping("/dj/video/record")
public class VideoRecordController {
@PostMapping("/upload")
public AjaxResult uploadFile(@RequestParam("file") MultipartFile file, @RequestParam("gateway") String gateway) {
// 保存文件到临时目录
String tempFilePath = saveTempFile(file);
if (tempFilePath == null) {
return AjaxResult.error("文件保存失败");
}
// 立即返回成功响应,前端会收到响应
new Thread(() -> uploadFileAsync(gateway, tempFilePath)).start();
// 直接返回成功响应,前端会收到响应
return AjaxResult.success();
}
private String saveTempFile(MultipartFile file) {
log.info("文件大小:");
try {
// 获取系统临时目录
Path tempDir = Paths.get(System.getProperty("java.io.tmpdir"));
Path tempFilePath = tempDir.resolve(file.getOriginalFilename());
// 将文件保存到临时目录
file.transferTo(tempFilePath.toFile());
log.info("文件已保存到临时目录: {}", tempFilePath.toString());
return tempFilePath.toString();
} catch (IOException e) {
log.error("保存文件到临时目录失败: ", e);
return null;
}
}
@Async
public void uploadFileAsync(String gateway, String tempFilePath) {
try {
// 从临时目录读取文件
File tempFile = new File(tempFilePath);
if (!tempFile.exists()) {
log.error("临时文件不存在: {}", tempFilePath);
return;
}
// 读取文件并调用 StorageConfigUtil 上传
StorageConfigUtil.uploadFile(gateway, "视频录制", tempFile);
// 上传完成后,删除临时文件
if (tempFile.delete()) {
log.info("临时文件已删除: {}", tempFilePath);
} else {
log.warn("删除临时文件失败: {}", tempFilePath);
}
} catch (Exception e) {
log.error("文件上传失败: ", e);
}
}
}

View File

@ -0,0 +1,265 @@
package com.ruoyi.dj.mqtt.client;
import com.ruoyi.dj.mqtt.common.CommonFieldsVo;
import com.ruoyi.dj.mqtt.config.DJIMqttConfig;
import com.ruoyi.dj.mqtt.domain.DrcOperVo;
import com.ruoyi.dj.mqtt.domain.DrcVo;
import com.ruoyi.dj.mqtt.service.MqttMessageService;
import com.ruoyi.dj.mqtt.utils.PayloadUtil;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.paho.client.mqttv3.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
/**
* MQTT客户端实现类负责与DJI云进行MQTT通信
*/
@Slf4j
@Component
public class DJICloudMQTTClient implements MqttCallback {
@Autowired
private DJIMqttConfig djiMqttConfig;
@Autowired
private MqttMessageService mqttMessageService;
private MqttClient mqttClient;
/**
* 用于存储响应的Future键为请求的公共字段值为对应的Future
*/
private final Map<CommonFieldsVo, CompletableFuture<String>> responseFutures = new ConcurrentHashMap<>();
private final Map<DrcVo, CompletableFuture<String>> drcResponseFutures = new ConcurrentHashMap<>();
private final Map<DrcOperVo, CompletableFuture<String>> drcOperVoCompletableFutureConcurrentHashMap = new ConcurrentHashMap<>();
/**
* 初始化客户端并连接到MQTT服务器
*
* @throws MqttException 如果连接失败
*/
@PostConstruct
public void connect() throws MqttException {
// 创建MQTT客户端并设置回调
// String uniqueClientId = UUID.randomUUID().toString();
mqttClient = new MqttClient(djiMqttConfig.getBroker(), "4363452132146456346");
// mqttClient = new MqttClient(djiMqttConfig.getBroker(), uniqueClientId);
mqttClient.setCallback(this);
// 设置连接选项,包括清空会话、用户名和密码
MqttConnectOptions connOpts = new MqttConnectOptions();
connOpts.setCleanSession(false);
connOpts.setUserName(djiMqttConfig.getUsername());
connOpts.setPassword(djiMqttConfig.getPassword().toCharArray());
connOpts.setAutomaticReconnect(true); // 启用自动重连
// 连接到MQTT服务器并订阅指定的主题
log.info("连接到服务器: " + djiMqttConfig.getBroker());
mqttClient.connect(connOpts);
for (String topic : djiMqttConfig.getTopics()) {
mqttClient.subscribe(topic);
log.info("订阅主题: " + topic);
}
}
/**
* 向指定主题发布消息,并等待响应
*
* @param topic 消息发布的主题
* @param payload 消息的内容
* @param commonFieldsVo 请求的公共字段,用于匹配响应
* @return 响应的消息内容
* @throws Exception 如果发布消息或等待响应失败
*/
public String publishMessage(String topic, String payload, CommonFieldsVo commonFieldsVo) {
try {
CompletableFuture<String> future = new CompletableFuture<>();
// 把网关置空、因为有的响应是不带网关这个字段的所以都不需要这个字段来做判断,否则代码会一直阻塞等待
commonFieldsVo.setGateway(null);
responseFutures.put(commonFieldsVo, future);
// 设置消息质量等级为2并发布消息
MqttMessage message = new MqttMessage(payload.getBytes());
message.setQos(2);
mqttClient.publish(topic, message);
log.info("消息已发送到主题: " + topic);
// 等待响应
return future.get();
} catch (MqttException e) {
throw new RuntimeException(e);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
}
public String publishMessageWithDrc(String topic, String payload, CommonFieldsVo commonFieldsVo) {
try {
DrcVo drcVo = new DrcVo();
drcVo.setMethod(commonFieldsVo.getMethod());
CompletableFuture<String> future = new CompletableFuture<>();
drcResponseFutures.put(drcVo, future);
// 设置消息质量等级为2并发布消息
MqttMessage message = new MqttMessage(payload.getBytes());
message.setQos(2);
mqttClient.publish(topic, message);
log.info("消息已发送到主题: " + topic);
// 等待响应
return future.get();
} catch (MqttException e) {
throw new RuntimeException(e);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
}
public String publishMessageWithDrcOper(String topic, String payload, CommonFieldsVo commonFieldsVo) {
try {
DrcOperVo drcOperVo = new DrcOperVo();
drcOperVo.setMethod(commonFieldsVo.getMethod());
// 从 payload 的 json 字符串内截取 data 下的 seq 字段、用于匹配响应
Long seqFromJson = PayloadUtil.getSeqFromJson(payload);
drcOperVo.setSeq(seqFromJson);
CompletableFuture<String> future = new CompletableFuture<>();
drcOperVoCompletableFutureConcurrentHashMap.put(drcOperVo, future);
// 设置消息质量等级为2并发布消息
MqttMessage message = new MqttMessage(payload.getBytes());
message.setQos(2);
mqttClient.publish(topic, message);
log.info("消息已发送到主题: " + topic);
// 等待响应
return future.get();
} catch (MqttException e) {
throw new RuntimeException(e);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
}
public void publishMessage(String topic, String payload) {
try {
log.info("发送消息到主题: " + topic);
log.info("发生消息的内容: " + payload);
// 设置消息质量等级为2并发布消息
MqttMessage message = new MqttMessage(payload.getBytes());
message.setQos(2);
mqttClient.publish(topic, message);
log.info("消息已发送到主题: " + topic);
} catch (MqttException e) {
throw new RuntimeException(e);
}
}
/**
* 当MQTT连接丢失时调用
*
* @param cause 连接丢失的原因
*/
@Override
public void connectionLost(Throwable cause) {
log.info("连接丢失! 原因: " + cause.getMessage());
}
/**
* 当收到消息时调用
*
* @param topic 收到消息的主题
* @param message 收到的消息
* @throws Exception 如果处理消息失败
*/
@Override
public void messageArrived(String topic, MqttMessage message) throws Exception {
String payload = new String(message.getPayload());
mqttMessageService.handleMessage(topic, payload);
}
/**
* 当消息成功交付给服务器时调用
*
* @param token 消息交付的令牌
*/
@Override
public void deliveryComplete(IMqttDeliveryToken token) {
log.info("消息传递完成.");
}
/**
* 根据公共字段获取对应的响应Future
*
* @param commonFieldsVo 公共字段
* @return 响应的Future
*/
public CompletableFuture<String> getResponseFuture(CommonFieldsVo commonFieldsVo) {
return responseFutures.get(commonFieldsVo);
}
public CompletableFuture<String> getDrcResponseFutures(DrcVo drcVo) {
return drcResponseFutures.get(drcVo);
}
/**
* 完成指定公共字段的响应Future并从map中移除
*
* @param commonFieldsVo 公共字段
* @param response 响应内容
*/
public void completeResponseFuture(CommonFieldsVo commonFieldsVo, String response) {
CompletableFuture<String> future = responseFutures.get(commonFieldsVo);
if (future != null) {
future.complete(response);
responseFutures.remove(commonFieldsVo);
}
}
public void completeDrcResponseFuture(DrcVo drcVo, String response) {
CompletableFuture<String> future = drcResponseFutures.get(drcVo);
if (future != null) {
future.complete(response);
drcResponseFutures.remove(drcVo);
}
}
/**
* 根据 DrcOperVo 获取对应的响应 Future
*
* @param drcOperVo DrcOperVo 对象
* @return 响应的 Future
*/
public CompletableFuture<String> getDrcOperVoResponseFuture(DrcOperVo drcOperVo) {
return drcOperVoCompletableFutureConcurrentHashMap.get(drcOperVo);
}
/**
* 完成指定 DrcOperVo 的响应 Future并从 map 中移除
*
* @param drcOperVo DrcOperVo 对象
* @param response 响应内容
*/
public void completeDrcOperVoResponseFuture(DrcOperVo drcOperVo, String response) {
CompletableFuture<String> future = drcOperVoCompletableFutureConcurrentHashMap.get(drcOperVo);
if (future != null) {
future.complete(response);
drcOperVoCompletableFutureConcurrentHashMap.remove(drcOperVo);
}
}
}

View File

@ -0,0 +1,27 @@
package com.ruoyi.dj.mqtt.cmd;
/**
* @auther 周志雄
* @date 2024/7/5 17:24
*/
public class CmdConstant {
public static final String BATTERY_MAINTENANCE_SWITCH = "battery_maintenance_switch";
public static final String AIR_CONDITIONER_MODE_SWITCH = "air_conditioner_mode_switch";
public static final String LIVE_START_PUSH= "live_start_push";
public static final String LIVE_STOP_PUSH= "live_stop_push";
public static final String LIVE_LENS_CHANGE= "live_lens_change";
public static final String LIVE_SET_QUALITY= "live_set_quality";
public static final String LIVE_CAMERA_CHANGE= "live_camera_change";
public static final String SDR_WORKMODE_SWITCH= "sdr_workmode_switch";
public static final String FLIGHTTASK_PREPARE= "flighttask_prepare";
public static final String FLIGHTTASK_EXECUTE= "flighttask_execute";
public static final String FLIGHTTASK_UNDO= "flighttask_undo";
public static final String FLIGHTTASK_PAUSE= "flighttask_pause";
public static final String FLIGHTTASK_RECOVERY= "flighttask_recovery";
public static final String RETURN_HOME= "return_home";
public static final String RETURN_HOME_CANCEL= "return_home_cancel";
public static final String DRC_MODE_ENTER= "drc_mode_enter";
public static final String DRONE_CONTROL= "drone_control";
public static final String DRONE_EMERGENCY_STOP= "drone_emergency_stop";
public static final String HEART_BEAT= "heart_beat";
}

View File

@ -0,0 +1,12 @@
package com.ruoyi.dj.mqtt.common;
import lombok.Data;
@Data
public class CommonFields {
private String tid;
private String bid;
private long timestamp;
private String method;
private String gateway;
}

View File

@ -0,0 +1,49 @@
package com.ruoyi.dj.mqtt.common;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Objects;
import java.util.UUID;
@Data
@NoArgsConstructor
public class CommonFieldsVo {
private String tid;
private String bid;
private String method;
private String gateway;
public CommonFieldsVo(String bid, String tid, String method, String gateway) {
this.tid = tid;
this.bid = bid;
this.method = method;
this.gateway = gateway;
}
public CommonFieldsVo(String bid, String tid, String method) {
this.tid = tid;
this.bid = bid;
this.method = method;
}
public static CommonFieldsVo createCommonFieldVo(String method, String gateway) {
String bid = UUID.randomUUID().toString();
String tid = UUID.randomUUID().toString();
return new CommonFieldsVo(bid, tid, method, gateway);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CommonFieldsVo that = (CommonFieldsVo) o;
return Objects.equals(tid, that.tid) && Objects.equals(bid, that.bid) && Objects.equals(method, that.method) && Objects.equals(gateway, that.gateway);
}
@Override
public int hashCode() {
return Objects.hash(tid, bid, method);
}
}

View File

@ -0,0 +1,13 @@
package com.ruoyi.dj.mqtt.common;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class CommonResponse {
private String tid;
private String bid;
private long timestamp;
private String method;
}

View File

@ -0,0 +1,12 @@
package com.ruoyi.dj.mqtt.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Data
@Configuration
@ConfigurationProperties(prefix = "ai")
public class AiConfig {
private String url;
}

View File

@ -0,0 +1,24 @@
package com.ruoyi.dj.mqtt.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
import java.util.List;
@Data
@Configuration
@ConfigurationProperties(prefix = "mqtt")
public class DJIMqttConfig {
private String broker;
private String username;
private String password;
private List<String> topics;
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}

View File

@ -0,0 +1,28 @@
package com.ruoyi.dj.mqtt.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.web.servlet.MultipartConfigFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.unit.DataSize;
import javax.servlet.MultipartConfigElement;
import java.io.File;
@Slf4j
@Configuration
public class FileUploadConfig {
@Bean
public MultipartConfigElement multipartConfigElement() {
MultipartConfigFactory factory = new MultipartConfigFactory();
// 设置最大文件大小为 10GB
factory.setMaxFileSize(DataSize.ofBytes(1024L * 1024L * 1024L * 1024L)); // 10GB
// 设置请求体大小为 10GB
factory.setMaxRequestSize(DataSize.ofBytes(1024L * 1024L * 1024L * 1024L)); // 10GB
return factory.createMultipartConfig();
}
}

View File

@ -0,0 +1,26 @@
package com.ruoyi.dj.mqtt.config;
import lombok.Data;
@Data
//public class MinioConfig {
// // MinIO 配置常量
// public static final String ENDPOINT = "http://zmkg.cqet.top:9999"; // STS 服务端点
// public static final String ACCESS_KEY = "admin"; // 访问密钥
// public static final String SECRET_KEY = "12345678"; // 秘密访问密钥
// public static final String ROLE_ARN = "arn:aws:s3:::drone-?/*"; // 角色 ARN
// public static final String ROLE_SESSION_NAME = "anysession"; // 角色会话名称
// public static final String REGION = "cn-chengdu"; // 地区
// public static final int DURATION_SECONDS = 3600; // 凭证持续时间(秒)
//}
public class MinioConfig {
// MinIO 配置常量
public static final String ENDPOINT = "http://192.168.110.65:9000"; // STS 服务端点
public static final String ACCESS_KEY = "minioadmin"; // 访问密钥
public static final String SECRET_KEY = "minioadmin"; // 秘密访问密钥
public static final String ROLE_ARN = "arn:aws:s3:::drone-?/*"; // 角色 ARN
public static final String ROLE_SESSION_NAME = "anysession"; // 角色会话名称
public static final String REGION = "cn-chengdu"; // 地区
public static final int DURATION_SECONDS = 3600; // 凭证持续时间(秒)
}

View File

@ -0,0 +1,27 @@
package com.ruoyi.dj.mqtt.domain;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
/**
* @auther 周志雄
* @date 2024/7/29 15:59
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class AppConfig {
private String bid;
private DataSection data;
private String gateway;
private String tid;
private long timestamp;
private String method;
@Data
public static class DataSection {
private int app_id;
private String app_key;
private String app_license;
private String ntp_server_host;
private int ntp_server_port;
}
}

View File

@ -0,0 +1,52 @@
package com.ruoyi.dj.mqtt.domain;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import java.util.List;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class CallbackRequest {
private String machineCode; // 设备机器码
private String alarmPicData; // 报警图片base64
private String alarmPicName; // 报警图片名称
private String cameraUrl; // 摄像头对应拉流地址
private int imageHeight; // 图片高度
private int imageWidth; // 图片宽度
private int taskId; // 任务ID
private long timestamp; // 时间戳
private String cameraId; // 摄像头唯一id
private String cameraName; // 摄像头别名
private String srcPicData; // 报警原图base64
private String srcPicName; // 报警原图名称
private int in; // 进入人数
private int out; // 出去人数
private List<Alarm> alarmArray; // 报警数组
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@ToString
public static class Alarm {
private int classid; // 算法ID
private String enName; // 英文名称
private String name; // 中文名称
private int height; // 框的高度
private int score; // 算法得分
private int width; // 框的宽度
private int x; // 框的x坐标
private int y; // 框的y坐标
private int count; // 人数
private String userid; // 用户ID
private String groupid; // 分组ID
}
}

View File

@ -0,0 +1,22 @@
package com.ruoyi.dj.mqtt.domain;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.Map;
/**
* @auther 周志雄
* @date 2024/7/25 9:53
*/
@Data
@ApiModel("有参数的指令飞行")
public class CmdFlyDataServiceDto {
@ApiModelProperty(value = "网关")
private String gateway;
@ApiModelProperty(value = "方法")
private String method;
@ApiModelProperty(value = "参数")
private Map<String, Object> params;
}

View File

@ -0,0 +1,19 @@
package com.ruoyi.dj.mqtt.domain;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* @auther 周志雄
* @date 2024/7/25 9:53
*/
@Data
@ApiModel("无参数的指令飞行")
public class CmdFlyServiceDto {
@ApiModelProperty(value = "网关")
private String gateway;
@ApiModelProperty(value = "指令")
private String method;
}

View File

@ -0,0 +1,15 @@
package com.ruoyi.dj.mqtt.domain;
import lombok.Data;
import java.util.List;
/**
* @auther 周志雄
* @date 2024/9/20 19:27
*/
@Data
public class DeleteBatchRequest {
private String gateway; // 网关名称
private List<String> itemPaths; // 要删除的文件路径列表
}

View File

@ -0,0 +1,16 @@
package com.ruoyi.dj.mqtt.domain;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* @auther 周志雄
* @date 2024/7/25 10:31
*/
@Data
@ApiModel("进入指令飞行控制模式")
public class DrcModeEnter {
@ApiModelProperty(value = "网关")
private String gateway;
}

View File

@ -0,0 +1,9 @@
package com.ruoyi.dj.mqtt.domain;
import lombok.Data;
@Data
public class DrcOperVo {
private String method;
private Long seq;
}

View File

@ -0,0 +1,8 @@
package com.ruoyi.dj.mqtt.domain;
import lombok.Data;
@Data
public class DrcVo {
private String method;
}

View File

@ -0,0 +1,33 @@
package com.ruoyi.dj.mqtt.domain;
import lombok.Data;
/**
* @auther 周志雄
* @date 2024/7/17 17:29
*/
@Data
public class DroneDataCredential {
private int result; // 返回码 0 表示成功
private Output output; // 输出对象
// 内部静态类用于封装输出数据
@Data
public static class Output {
private String bucket; // 对象存储桶名称
private Credentials credentials; // 凭证信息对象
private String endpoint; // 对外服务的访问域名
private String provider; // 云厂商枚举值
private String region; // 数据中心所在的地域
private String object_key_prefix; // 对象存储桶的 Key 的前缀
}
// 内部静态类,用于封装凭证信息
@Data
public static class Credentials {
private String access_key_id; // 访问密钥 ID
private String access_key_secret; // 秘密访问密钥
private int expire; // 访问密钥过期时间(秒)
private String security_token; // 会话凭证
}
}

View File

@ -0,0 +1,9 @@
package com.ruoyi.dj.mqtt.domain;
import lombok.Data;
@Data
public class HeartBeatVo {
private String gateway;
}

View File

@ -0,0 +1,69 @@
package com.ruoyi.dj.mqtt.domain;
import com.fasterxml.jackson.annotation.JsonAlias;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
import java.util.List;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class OSDMessage1 {
private String tid;
private String bid;
private long timestamp;
private DataField data;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public static class DataField {
private int job_number; // 作业次数
private int acc_time; // 累计运行时长
private int activation_time; // 激活时间
private MaintainStatus maintain_status; // 保养状态
private int electric_supply_voltage; // 市电电压
private int working_voltage; // 工作电压
private int working_current; // 工作电流
private BackupBattery backup_battery; // 备用电池信息
private DroneBatteryMaintenanceInfo drone_battery_maintenance_info; // 飞行器电池保养信息
@Data
public static class MaintainStatus {
private List<MaintainStatusArray> maintain_status_array; // 保养状态数组
@Data
public static class MaintainStatusArray {
private int state; // 保养状态
private int last_maintain_type; // 上一次保养类型
private long last_maintain_time; // 上一次保养时间
private int last_maintain_work_sorties; // 上一次保养时作业架次
}
}
@Data
public static class BackupBattery {
private int voltage; // 备用电池电压
private float temperature; // 备用电池温度
@JsonAlias("switch")
private int switchState; // 备用电池开关状态
}
@Data
public static class DroneBatteryMaintenanceInfo {
private int maintenance_state; // 电池保养状态
private int maintenance_time_left; // 电池保养剩余时间
private int heat_state; // 电池加热状态
private List<BatteryDetail> batteries; // 电池详细信息
@Data
public static class BatteryDetail {
private int index; // 电池序号
private int capacity_percent; // 电池剩余电量
private int voltage; // 电池电压
private float temperature; // 电池温度
}
}
}
}

View File

@ -0,0 +1,58 @@
package com.ruoyi.dj.mqtt.domain;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class OSDMessage2 {
private String tid;
private String bid;
private long timestamp;
private DataField data;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public static class DataField {
private int drc_state; // DRC 状态
private int flighttask_prepare_capacity; // 飞行任务准备容量
private int flighttask_step_code; // 飞行任务步骤代码
private MediaFileDetail media_file_detail; // 媒体文件上传细节
private SDR sdr; // SDR 信息
private int user_experience_improvement; // 用户体验改善计划
private WirelessLink wireless_link; // 无线链路
@Data
public static class MediaFileDetail {
private int remain_upload; // 待上传数量
}
@Data
public static class SDR {
private int down_quality; // 下行质量
private float frequency_band; // 频段
private int up_quality; // 上行质量
}
@Data
public static class WirelessLink {
private float sdr_freq_band; // SDR 频段
@JsonProperty("4g_freq_band")
private float _4g_freq_band; // 4G 频段
@JsonProperty("4g_gnd_quality")
private int _4g_gnd_quality; // 地端 4G 信号质量
@JsonProperty("4g_link_state")
private int _4g_link_state; // 4G 链路连接状态
@JsonProperty("4g_quality")
private int _4g_quality; // 总体 4G 信号质量
@JsonProperty("4g_uav_quality")
private int _4g_uav_quality; // 天端 4G 信号质量
private int dongle_number; // 飞行器上 Dongle 数量
private int link_workmode; // 图传链路模式
private int sdr_link_state; // SDR 链路连接状态
private int sdr_quality; // SDR 信号质量
}
}
}

View File

@ -0,0 +1,97 @@
package com.ruoyi.dj.mqtt.domain;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class OSDMessage3 {
private String tid;
private String bid;
private long timestamp;
private DataField data;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public static class DataField {
private NetworkState network_state; // 网络状态
private DroneChargeState drone_charge_state; // 飞行器充电状态
private int drone_in_dock; // 飞行器是否在舱
private int rainfall; // 降雨量
private float wind_speed; // 风速
private float environment_temperature; // 环境温度
private float temperature; // 舱内温度
private int humidity; // 舱内湿度
private float heading; // 航向
private int home_position_is_valid; // Home 位置是否有效
private double latitude; // 纬度
private double longitude; // 经度
private float height; // 椭球高度
private AlternateLandPoint alternate_land_point; // 备降点
private long first_power_on; // 首次上电时间
private PositionState position_state; // 搜星状态
private Storage storage; // 存储容量
private int mode_code; // 机场状态
private int cover_state; // 舱盖状态
private int silent_mode; // 静音模式
private int supplement_light_state; // 补光灯状态
private int emergency_stop_state; // 紧急停止按钮状态
private AirConditioner air_conditioner; // 空调信息
private int battery_store_mode; // 电池运行模式
private int alarm_state; // 机场声光报警状态
private int putter_state; // 推杆状态
private SubDevice sub_device; // 子设备状态
@Data
public static class NetworkState {
private int type; // 网络类型
private int quality; // 网络质量
private float rate; // 网络速率
}
@Data
public static class DroneChargeState {
private int state; // 充电状态
private int capacity_percent; // 电量百分比
}
@Data
public static class AlternateLandPoint {
private double latitude; // 备降点纬度
private double longitude; // 备降点经度
private float height; // 备降点高度
private float safe_land_height; // 安全高度
private int is_configured; // 是否设置备降点
}
@Data
public static class PositionState {
private int is_calibration; // 是否标定
private int is_fixed; // 是否收敛
private int quality; // 搜星档位
private int gps_number; // GPS 搜星数量
private int rtk_number; // RTK 搜星数量
}
@Data
public static class Storage {
private int total; // 总容量
private int used; // 已使用容量
}
@Data
public static class AirConditioner {
private int air_conditioner_state; // 空调状态
private long switch_time; // 空调开关时间
}
@Data
public static class SubDevice {
private String device_sn; // 子设备序列号
private String device_model_key; // 子设备型号键
private int device_online_status; // 子设备在线状态
private int device_paired; // 子设备对频状态
}
}
}

View File

@ -0,0 +1,205 @@
package com.ruoyi.dj.mqtt.domain;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
import java.util.List;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class OSDMessage4 {
private String tid;
private String bid;
private long timestamp;
private DataDetail data;
private String gateway;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public static class DataDetail {
private Gimbal gimbal; // 云台信息
private long activation_time; // 激活时间
private double attitude_head; // 姿态航向
private double attitude_pitch; // 姿态俯仰角
private double attitude_roll; // 姿态横滚角
private Battery battery; // 电池信息
private String best_link_gateway; // 最佳链路网关
private List<Camera> cameras; // 相机信息数组
private String country; // 国家
private DistanceLimitStatus distance_limit_status; // 距离限制状态
private double elevation; // 海拔高度
private int gear; // 档位
private double height; // 高度
private int height_limit; // 高度限制
private double home_distance; // 距离起点的距离
private double horizontal_speed; // 水平速度
private int is_near_area_limit; // 是否接近区域限制
private int is_near_height_limit; // 是否接近高度限制
private double latitude; // 纬度
private double longitude; // 经度
private MaintainStatus maintain_status; // 保养状态
private int mode_code; // 模式代码
private int night_lights_state; // 夜灯状态
private ObstacleAvoidance obstacle_avoidance; // 障碍物回避信息
private PositionState position_state; // 位置状态
private int rc_lost_action; // 遥控信号丢失后的动作
private boolean rid_state; // RID状态
private int rth_altitude; // 返回高度
private Storage storage; // 存储信息
private double total_flight_distance; // 总飞行距离
private int total_flight_sorties; // 总飞行架次
private double total_flight_time; // 总飞行时间
private String track_id; // 轨迹ID
private double vertical_speed; // 垂直速度
private int wind_direction; // 风向
private double wind_speed; // 风速
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public static class Gimbal {
private double gimbal_pitch; // 云台俯仰角
private double gimbal_roll; // 云台横滚角
private double gimbal_yaw; // 云台航向角
private String payload_index; // 载荷索引
private int thermal_current_palette_style; // 热成像当前调色板样式
private int thermal_gain_mode; // 热成像增益模式
private double thermal_global_temperature_max; // 热成像全球最高温度
private double thermal_global_temperature_min; // 热成像全球最低温度
private int thermal_isotherm_lower_limit; // 热成像等温线下限
private int thermal_isotherm_state; // 热成像等温线状态
private int thermal_isotherm_upper_limit; // 热成像等温线上限
private double zoom_factor; // 变焦因子
}
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public static class Battery {
private List<BatteryDetail> batteries; // 电池详细信息数组
private int capacity_percent; // 电池剩余容量百分比
private int landing_power; // 着陆功率
private int remain_flight_time; // 剩余飞行时间
private int return_home_power; // 返回家功率
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public static class BatteryDetail {
private int capacity_percent; // 电池剩余容量百分比
private String firmware_version; // 固件版本
private int high_voltage_storage_days; // 高电压存储天数
private int index; // 电池索引
private int loop_times; // 循环次数
private String sn; // 序列号
private int sub_type; // 子类型
private double temperature; // 温度
private int type; // 类型
private int voltage; // 电压
}
}
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public static class Camera {
private int camera_mode; // 相机模式
private int ir_metering_mode; // 红外测光模式
private IrMeteringPoint ir_metering_point; // 红外测光点
private double ir_zoom_factor; // 红外变焦因子
private LiveviewWorldRegion liveview_world_region; // 实时视图世界区域
private String payload_index; // 载荷索引
private int photo_state; // 照片状态
private String[] photo_storage_settings; // 照片存储设置
private int record_time; // 记录时间
private int recording_state; // 记录状态
private int remain_photo_num; // 剩余照片数量
private int remain_record_duration; // 剩余记录时长
private boolean screen_split_enable; // 是否启用分屏
private int wide_calibrate_farthest_focus_value; // 宽视角校准最远焦点值
private int wide_calibrate_nearest_focus_value; // 宽视角校准最近焦点值
private int wide_exposure_mode; // 宽视角曝光模式
private int wide_exposure_value; // 宽视角曝光值
private int wide_focus_mode; // 宽视角对焦模式
private int wide_focus_state; // 宽视角对焦状态
private int wide_focus_value; // 宽视角对焦值
private int wide_iso; // 宽视角ISO
private int wide_max_focus_value; // 宽视角最大对焦值
private int wide_min_focus_value; // 宽视角最小对焦值
private int wide_shutter_speed; // 宽视角快门速度
private int zoom_calibrate_farthest_focus_value; // 变焦校准最远焦点值
private int zoom_calibrate_nearest_focus_value; // 变焦校准最近焦点值
private int zoom_exposure_mode; // 变焦曝光模式
private int zoom_exposure_value; // 变焦曝光值
private double zoom_factor; // 变焦因子
private int zoom_focus_mode; // 变焦对焦模式
private int zoom_focus_state; // 变焦对焦状态
private int zoom_focus_value; // 变焦对焦值
private int zoom_iso; // 变焦ISO
private int zoom_max_focus_value; // 变焦最大对焦值
private int zoom_min_focus_value; // 变焦最小对焦值
private int zoom_shutter_speed; // 变焦快门速度
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public static class IrMeteringPoint {
private double temperature; // 温度
private double x; // X坐标
private double y; // Y坐标
}
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public static class LiveviewWorldRegion {
private double bottom; // 底部
private double left; // 左侧
private double right; // 右侧
private double top; // 顶部
}
}
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public static class DistanceLimitStatus {
private int distance_limit; // 距离限制
private int is_near_distance_limit; // 是否接近距离限制
private int state; // 状态
}
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public static class MaintainStatus {
private List<MaintainStatusArray> maintain_status_array; // 保养状态数组
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public static class MaintainStatusArray {
private int state; // 保养状态
private int last_maintain_type; // 上一次保养类型
private long last_maintain_time; // 上一次保养时间
private int last_maintain_flight_sorties; // 上一次保养时飞行架次
private double last_maintain_flight_time; // 上一次保养时飞行时间
}
}
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public static class ObstacleAvoidance {
private int downside; // 下侧
private int horizon; // 水平
private int upside; // 上侧
}
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public static class PositionState {
private int gps_number; // GPS数量
private int is_fixed; // 是否固定
private int quality; // 质量
private int rtk_number; // RTK数量
}
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public static class Storage {
private long total; // 总容量
private long used; // 已使用容量
}
}
}

View File

@ -0,0 +1,12 @@
package com.ruoyi.dj.mqtt.domain;
import lombok.Data;
import java.util.List;
@Data
public class RespModel {
private String url;
private String imgData;
private List<TypeModel> types;
}

View File

@ -0,0 +1,24 @@
package com.ruoyi.dj.mqtt.domain;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
/**
* @auther 周志雄
* @date 2024/7/19 10:32
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class StorageConfig {
private String bid;
private DataSection data;
private String tid;
private long timestamp;
private int need_reply;
private String method;
@Data
public static class DataSection {
private int module;
}
}

View File

@ -0,0 +1,13 @@
package com.ruoyi.dj.mqtt.domain;
import lombok.Data;
@Data
public class TypeModel {
private Double x;
private Double y;
private Double w;
private Double h;
private String type;
private Double percent;
}

View File

@ -0,0 +1,38 @@
package com.ruoyi.dj.mqtt.domain;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
import java.util.List;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class UpdateTopoRequest {
private String tid; // 事务ID
private String bid; // 业务ID
private String method; // 方法名
private Long timestamp; // 时间戳
private DeviceData data; // 数据对象
@Data
public static class DeviceData {
private String domain; // 网关设备的命名空间
private int type; // 网关设备的产品类型
private int sub_type; // 网关子设备的产品子类型
private String device_secret; // 网关设备的密钥
private String nonce; // nonce
private String thing_version; // 网关设备的物模型版本
private List<SubDevice> sub_devices; // 子设备列表
@Data
public static class SubDevice {
private String sn; // 子设备序列号SN
private String domain; // 子设备的命名空间
private int type; // 子设备的产品类型
private int sub_type; // 子设备的产品子类型
private String index; // 连接网关设备的通道索引
private String device_secret; // 子设备的密钥
private String nonce; // nonce
private String thing_version; // 子设备的物模型版本
}
}
}

View File

@ -0,0 +1,54 @@
package com.ruoyi.dj.mqtt.domain;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Data;
import java.io.IOException;
import java.util.List;
@Data
public class VideoRoot {
private String tid;
private String bid;
private long timestamp;
private DataContent data;
private String gateway;
@Data
public static class DataContent {
private LiveCapacity live_capacity;
@Data
public static class LiveCapacity {
private int available_video_number;
private int coexist_video_number_max;
private List<Device> device_list;
@Data
public static class Device {
private String sn;
private int available_video_number;
private int coexist_video_number_max;
private List<Camera> camera_list;
@Data
public static class Camera {
private String camera_index;
private int available_video_number;
private int coexist_video_number_max;
private List<Video> video_list;
private List<Integer> availabe_camera_positions;
private int camera_position;
@Data
public static class Video {
private String video_index;
private String video_type;
private List<String> switchable_video_types;
}
}
}
}
}
}

View File

@ -0,0 +1,13 @@
package com.ruoyi.dj.mqtt.dto;
import lombok.Data;
/**
* @auther 周志雄
* @date 2024/7/9 15:47
*/
@Data
public class DataParamDto {
private String gateway;
private String action;
}

View File

@ -0,0 +1,13 @@
package com.ruoyi.dj.mqtt.dto;
import lombok.Data;
/**
* @auther 周志雄
* @date 2024/7/19 16:14
*/
@Data
public class DataParamDto01 {
private String gateway;
private Integer action;
}

View File

@ -0,0 +1,12 @@
package com.ruoyi.dj.mqtt.dto;
import lombok.Data;
@Data
public class DroneControlDto {
private String gateway;
private Double x;
private Double y;
private Double h;
private Double w;
}

View File

@ -0,0 +1,8 @@
package com.ruoyi.dj.mqtt.dto;
import lombok.Data;
@Data
public class DroneEmergencyStop {
private String gateway;
}

View File

@ -0,0 +1,15 @@
package com.ruoyi.dj.mqtt.dto;
import lombok.Data;
import java.util.List;
/**
* @auther 周志雄
* @date 2024/7/17 15:07
*/
@Data
public class FileUploadListDto {
private String gateway;
private List<String> moduleList;
}

View File

@ -0,0 +1,21 @@
package com.ruoyi.dj.mqtt.dto;
import lombok.Data;
/**
* @auther 周志雄
* @date 2024/7/15 14:39
*/
@Data
public class FlightTaskDto {
private Long missionId; // 任务ID
private String gateway; // 网关
private Long timeStamp; // 立即执行的时间戳
private String execMonth; // 定时任务执行的月
private String execDay; // 定时任务执行的日
private String execHour; // 定时任务执行的时
private String execMinute; // 定时任务执行的分
private String execSecond; // 定时任务执行的秒
private Integer type;// 任务类型 0立即执行 1定时执行
}

View File

@ -0,0 +1,14 @@
package com.ruoyi.dj.mqtt.dto;
import lombok.Data;
/**
* @auther 周志雄
* @date 2024/7/16 10:24
*/
@Data
public class FlightTaskExecuteDto {
private String gateway;
private String flightId;
private Long taskId;
}

View File

@ -0,0 +1,58 @@
package com.ruoyi.dj.mqtt.dto;
import lombok.Data;
/**
* @auther 周志雄
* @date 2024/7/15 14:39
*/
@Data
public class FlightTaskParam {
private String flightId; // 计划 ID
private Long executeTime; // 任务开始执行时间毫秒时间戳。可选字段。当 task_type 为 0 或 1 时必填,为 2 时非必填。
private Long taskType; // 任务类型0: 立即任务1: 定时任务2: 条件任务
private File file; // 航线文件对象
private ReadyConditions readyConditions; // 任务就绪条件【暂不需要】
private ExecutableConditions executableConditions; // 任务执行条件【暂不需要】
private BreakPoint breakPoint; // 航线断点信息【暂不需要】
private Integer rthAltitude; // 返航高度
private Integer rthMode; // 返航高度模式
private Integer outOfControlAction; // 遥控器失控动作、文档说默认值是0
private Integer exitWaylineWhenRcLost; // 航线失控动作
private Integer waylinePrecisionType; // 航线精度类型
private SimulateMission simulateMission; // 是否在模拟器中执行任务【暂不需要】
@Data
public static class File {
private String url; // 文件 URL
private String fingerprint; // 文件 MD5 签名
}
@Data
public static class ReadyConditions {
private Integer batteryCapacity; // 电池容量百分比阈值
private Long beginTime; // 任务可执行时段的开始时间毫秒时间戳
private Long endTime; // 任务可执行时段的结束时间毫秒时间戳
}
@Data
public static class ExecutableConditions {
private Integer storageCapacity; // 存储容量
}
@Data
public static class BreakPoint {
private Integer index; // 断点序号
private Integer state; // 断点状态0: 在航段上1: 在航点上
private Float progress; // 当前航段进度
private Integer waylineId; // 航线 ID
}
@Data
public static class SimulateMission {
private Integer isEnable; // 是否开启模拟器任务
private Double latitude; // 纬度
private Double longitude; // 经度
}
}

View File

@ -0,0 +1,13 @@
package com.ruoyi.dj.mqtt.dto;
import lombok.Data;
/**
* @auther 周志雄
* @date 2024/7/16 10:24
*/
@Data
public class FlightTaskPauseDto {
private String gateway;
private Long taskId;
}

View File

@ -0,0 +1,13 @@
package com.ruoyi.dj.mqtt.dto;
import lombok.Data;
/**
* @auther 周志雄
* @date 2024/7/16 10:48
*/
@Data
public class FlightTaskRecoveryDto {
private String gateway;
private Long taskId;
}

View File

@ -0,0 +1,16 @@
package com.ruoyi.dj.mqtt.dto;
import lombok.Data;
import java.util.List;
/**
* @auther 周志雄
* @date 2024/7/16 10:24
*/
@Data
public class FlightTaskUndoDto {
private String gateway;
private List<String> flightIds;
private Long taskId;
}

View File

@ -0,0 +1,13 @@
package com.ruoyi.dj.mqtt.dto;
import lombok.Data;
/**
* @auther 周志雄
* @date 2024/7/30 18:35
*/
@Data
public class GetFileDto {
private String sn;
private String flightId;
}

View File

@ -0,0 +1,20 @@
package com.ruoyi.dj.mqtt.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.Map;
/**
* @auther 周志雄
* @date 2024/9/10 10:47
*/
@Data
@ApiModel(value = "属性设置")
public class PropertySetDto {
@ApiModelProperty(value = "网关")
private String gateway;
@ApiModelProperty(value = "参数")
private Map<String, Object> params;
}

View File

@ -0,0 +1,13 @@
package com.ruoyi.dj.mqtt.dto;
import lombok.Data;
/**
* @auther 周志雄
* @date 2024/7/9 9:46
*/
@Data
public class RemoteServiceDto {
private String gateway;
private String method;
}

View File

@ -0,0 +1,8 @@
package com.ruoyi.dj.mqtt.dto;
import lombok.Data;
@Data
public class ReturnHomeCancelDto {
private String gateway;
}

View File

@ -0,0 +1,13 @@
package com.ruoyi.dj.mqtt.dto;
import lombok.Data;
/**
* @auther 周志雄
* @date 2024/7/16 10:56
*/
@Data
public class ReturnHomeDto {
private String gateway;
private Long taskId;
}

View File

@ -0,0 +1,29 @@
package com.ruoyi.dj.mqtt.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.common.annotation.Excel;
import lombok.Data;
import java.util.Date;
/**
* @auther 周志雄
* @date 2024/7/17 18:14
*/
@Data
public class TaskCreateDto {
/** 任务名称 */
private String missionName;
/** 航线文件ID */
private String fileId;
/** 返航高度 */
private Long returnAltitude;
/** 返航高度模式 */
private Long returnAltitudeMode;
/** 遥控器失控类型 */
private Long outOfControlAction;
/** 航线失控动作 */
private Long flightControlAction;
/** 航线精度类型 */
private Long waylinePrecisionType;
}

View File

@ -0,0 +1,40 @@
package com.ruoyi.dj.mqtt.dto;
import com.ruoyi.common.annotation.Excel;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* @auther 周志雄
* @date 2024/8/16 17:38
*/
@Data
@ApiModel(value = "镜头切换参数")
public class VideoInfoChangeVo {
private static final long serialVersionUID = 1L;
/** 网关 */
@ApiModelProperty("网关")
private String gateway;
/** 设备序列号 */
@ApiModelProperty("设备序列号")
@Excel(name = "设备序列号")
private String sn;
/** 摄像头索引 */
@ApiModelProperty("摄像头索引")
@Excel(name = "摄像头索引")
private String cameraIndex;
/** 视频索引 */
@ApiModelProperty("视频索引")
@Excel(name = "视频索引")
private String videoIndex;
/** VideoType */
@ApiModelProperty("VideoType")
@Excel(name = "VideoType")
private String videoType;
}

View File

@ -0,0 +1,39 @@
package com.ruoyi.dj.mqtt.dto;
import com.ruoyi.common.annotation.Excel;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* @auther 周志雄
* @date 2024/8/16 18:33
*/
@Data
@ApiModel(value = "设置直播质量参数")
public class VideoInfoQualityVo {
private static final long serialVersionUID = 1L;
/** 网关 */
@ApiModelProperty("网关")
private String gateway;
/** 设备序列号 */
@ApiModelProperty("设备序列号")
@Excel(name = "设备序列号")
private String sn;
/** 摄像头索引 */
@ApiModelProperty("摄像头索引")
@Excel(name = "摄像头索引")
private String cameraIndex;
/** 视频索引 */
@ApiModelProperty("视频索引")
@Excel(name = "视频索引")
private String videoIndex;
/** VideoType */
@ApiModelProperty("videoQuality")
@Excel(name = "videoQuality")
private Integer videoQuality;
}

View File

@ -0,0 +1,36 @@
package com.ruoyi.dj.mqtt.dto;
import com.ruoyi.common.annotation.Excel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* @auther 周志雄
* @date 2024/9/11 14:10
*/
@Data
public class liveCameraChangeDto {
/** 网关 */
@ApiModelProperty("网关")
private String gateway;
/** 设备序列号 */
@ApiModelProperty("设备序列号")
@Excel(name = "设备序列号")
private String sn;
/** 摄像头索引 */
@ApiModelProperty("摄像头索引")
@Excel(name = "摄像头索引")
private String cameraIndex;
/** 视频索引 */
@ApiModelProperty("视频索引")
@Excel(name = "视频索引")
private String videoIndex;
/** FPV 位置 */
@ApiModelProperty("FPV 位置")
@Excel(name = "FPV 位置")
private Integer cameraPosition;
}

View File

@ -0,0 +1,451 @@
package com.ruoyi.dj.mqtt.error;
import java.util.HashMap;
import java.util.Map;
/**
* @auther 周志雄
* @date 2024/7/8 14:33
*/
public class ErrorCodeMap {
public static final Map<Integer, String> errorCodeMap = new HashMap<>();
static {
errorCodeMap.put(312014, "设备升级中,请勿重复操作");
errorCodeMap.put(312015, "机场:{dock_org_name} 业务繁忙无法进行设备升级,请等待机场处于空闲中后再试");
errorCodeMap.put(312016, "升级失败,机场和飞行器图传链路异常,请重启机场和飞行器后重试");
errorCodeMap.put(312022, "飞行器开机失败或未连接,请检查飞行器是否在舱内,是否安装电池,机场和飞行器是否已对频");
errorCodeMap.put(312023, "推杆闭合失败无法升级飞行器,请检查急停按钮是否被按下,推杆是否有异物卡住");
errorCodeMap.put(312027, "升级失败,机场未检测到飞行器");
errorCodeMap.put(312028, "升级失败,设备升级过程中设备被重启");
errorCodeMap.put(312029, "设备重启中无法进行设备升级,请等待设备重启完成后重试");
errorCodeMap.put(312030, "升级失败,飞行器增强图传开启后无法升级,请关闭飞行器增强图传后重试");
errorCodeMap.put(312704, "飞行器电量过低请充电至20%以上后重试");
errorCodeMap.put(314000, "设备当前无法支持该操作,建议检查设备当前工作状态");
errorCodeMap.put(314001, "飞行任务下发失败,请稍后重试");
errorCodeMap.put(314002, "飞行任务下发失败,请稍后重试");
errorCodeMap.put(314003, "航线文件格式不兼容,请检查航线文件是否正确");
errorCodeMap.put(314004, "飞行任务下发失败,请稍后重试或重启机场后重试");
errorCodeMap.put(314005, "飞行任务下发失败,请稍后重试或重启机场后重试");
errorCodeMap.put(314006, "飞行器初始化失败,请重启机场后重试");
errorCodeMap.put(314007, "您没有权限进行该操作");
errorCodeMap.put(314008, "飞行器起飞前准备超时,请重启机场后重试");
errorCodeMap.put(314009, "飞行器初始化失败,请重启机场后重试");
errorCodeMap.put(314010, "航线执行失败,请重启机场后重试");
errorCodeMap.put(314011, "机场系统异常,无法获取飞行任务执行结果");
errorCodeMap.put(314012, "飞行器起飞前准备失败,无法执行飞行任务,请重启机场后重试");
errorCodeMap.put(314013, "飞行任务下发失败,机场无法获取到本次飞行任务的航线,无法执行飞行任务,请稍后重试");
errorCodeMap.put(314014, "机场系统异常,飞行任务执行失败,请稍后重试");
errorCodeMap.put(314015, "机场传输精准复拍航线至飞行器失败,无法执行飞行任务,请稍后重试或重启机场后重试");
errorCodeMap.put(314016, "航线文件解析失败,无法执行飞行任务,请检查航线文件");
errorCodeMap.put(314017, "机场系统异常,飞行任务执行失败,请稍后重试");
errorCodeMap.put(314018, "飞行器 RTK 定位异常,无法执行飞行任务,请稍后重试或重启机场后重试");
errorCodeMap.put(314019, "飞行器 RTK 收敛失败,无法执行飞行任务,请稍后重试或重启机场后重试");
errorCodeMap.put(314020, "飞行器不在停机坪正中间或飞行器朝向不正确,无法执行飞行任务,请检查飞行器位置和朝向");
errorCodeMap.put(314021, "飞行器 RTK 定位异常,无法执行飞行任务,请稍后重试或重启机场后重试");
errorCodeMap.put(315000, "机场通信异常,请重启机场后重试");
errorCodeMap.put(315001, "机场通信异常,请远程开启飞机并等待 1min 后,再次下发任务重试");
errorCodeMap.put(315002, "机场通信异常,请重启机场后重试");
errorCodeMap.put(315003, "机场通信异常,请重启机场后重试");
errorCodeMap.put(315004, "任务失败,请等待两个机场都空闲后,再次下发任务重试");
errorCodeMap.put(315005, "机场通信异常,请重启机场后重试");
errorCodeMap.put(315006, "机场通信异常,请重启机场后重试");
errorCodeMap.put(315007, "机场通信异常,请将机场升级到最新版本或重启机场后重试");
errorCodeMap.put(315008, "降落机场和起飞机场标定信息不一致,请确认两个机场均链路通畅且使用了相同的网络信息标定");
errorCodeMap.put(315009, "机场通信异常,请重启机场后重试");
errorCodeMap.put(315010, "无法停止飞行任务,请稍后重试,如果仍报错请联系大疆售后");
errorCodeMap.put(315011, "无法停止飞行任务,请稍后重试,如果仍报错请联系大疆售后");
errorCodeMap.put(315012, "无法停止飞行任务,请稍后重试,如果仍报错请联系大疆售后");
errorCodeMap.put(315013, "飞行任务下发失败,请稍后重试,如果仍报错请联系大疆售后");
errorCodeMap.put(315014, "当前任务类型不支持设置返航点");
errorCodeMap.put(315015, "返航点设置失败,请稍后重试,如果仍报错请联系大疆售后");
errorCodeMap.put(315016, "飞行任务下发失败,请稍后重试,如果仍报错请联系大疆售后");
errorCodeMap.put(315017, "飞行任务下发失败,请稍后重试,如果仍报错请联系大疆售后");
errorCodeMap.put(315018, "任务失败,请等待两个机场都空闲后,再次下发任务重试");
errorCodeMap.put(315050, "机场系统异常,请重启机场后重试");
errorCodeMap.put(315051, "任务失败,请重启机场并再次下发任务后重试,如果仍报错请联系大疆售后");
errorCodeMap.put(315052, "机场位置未收敛,请等待一段时间后重试");
errorCodeMap.put(315053, "任务失败,请重启机场并再次下发任务后重试,如果仍报错请联系大疆售后");
errorCodeMap.put(315054, "任务失败,请重启机场并再次下发任务后重试,如果仍报错请联系大疆售后");
errorCodeMap.put(315055, "任务失败,请重启机场并再次下发任务后重试,如果仍报错请联系大疆售后");
errorCodeMap.put(315056, "任务失败,请重启机场并再次下发任务后重试,如果仍报错请联系大疆售后");
errorCodeMap.put(315057, "任务失败,请重启机场并再次下发任务后重试,如果仍报错请联系大疆售后");
errorCodeMap.put(315058, "任务失败,请重启机场并再次下发任务后重试,如果仍报错请联系大疆售后");
errorCodeMap.put(315059, "任务失败,请重启机场并再次下发任务后重试,如果仍报错请联系大疆售后");
errorCodeMap.put(315060, "任务失败,请重启机场并再次下发任务后重试,如果仍报错请联系大疆售后");
errorCodeMap.put(315061, "任务失败,请重启机场并再次下发任务后重试,如果仍报错请联系大疆售后");
errorCodeMap.put(315062, "任务失败,请重启机场并再次下发任务后重试,如果仍报错请联系大疆售后");
errorCodeMap.put(315063, "任务失败,请重启机场并再次下发任务后重试,如果仍报错请联系大疆售后");
errorCodeMap.put(315064, "任务失败,请重启机场并再次下发任务后重试,如果仍报错请联系大疆售后");
errorCodeMap.put(315065, "任务失败,请重启机场并再次下发任务后重试,如果仍报错请联系大疆售后");
errorCodeMap.put(316001, "飞行器参数配置失败,请重启机场后重试");
errorCodeMap.put(316002, "飞行器参数配置失败,请重启机场后重试");
errorCodeMap.put(316003, "飞行器参数配置失败,请重启机场后重试");
errorCodeMap.put(316004, "飞行器参数配置失败,请重启机场后重试");
errorCodeMap.put(316005, "飞行器 RTK 收敛失败,无法执行飞行任务,请重启机场后重试");
errorCodeMap.put(316006, "任务超时,飞行器已丢失或降落时机场未开启舱盖或展开推杆,飞行器无法降落回机场,请尽快至机场部署现场检查飞行器状况");
errorCodeMap.put(316007, "飞行器初始化失败,请重启机场后重试");
errorCodeMap.put(316008, "机场获取飞行器控制权失败,无法执行飞行任务,请确认遥控器未锁定控制权");
errorCodeMap.put(316009, "飞行器电量低于30%无法执行飞行任务请充电后重试建议电量≥50%");
errorCodeMap.put(316010, "机场未检测到飞行器,无法执行飞行任务,请检查舱内是否有飞行器,机场与飞行器是否已对频,或重启机场后重试");
errorCodeMap.put(316011, "飞行器降落位置偏移过大,请检查飞行器是否需要现场摆正");
errorCodeMap.put(316012, "飞行器起飞前准备失败,无法执行飞行任务,请重启机场后重试");
errorCodeMap.put(316013, "飞行器起飞前准备失败,无法执行飞行任务,请重启机场后重试");
errorCodeMap.put(316014, "飞行器起飞前准备失败,无法执行飞行任务,请重启机场后重试");
errorCodeMap.put(316015, "飞行器 RTK 收敛位置距离机场过远,无法执行飞行任务,请重启机场后重试");
errorCodeMap.put(316016, "飞行器降落至机场超时,可能是机场与飞行器断连导致,请通过直播查看飞行器是否降落至舱内");
errorCodeMap.put(316017, "获取飞行器媒体数量超时,可能是机场与飞行器断连导致,请通过直播查看飞行器是否降落至舱内");
errorCodeMap.put(316018, "飞行任务执行超时,可能是机场与飞行器断连导致,请通过直播查看飞行器是否降落至舱内");
errorCodeMap.put(316019, "机场系统错误,无法执行飞行任务,请稍后重试");
errorCodeMap.put(316020, "飞行器使用的 RTK 信号源错误,请稍后重试");
errorCodeMap.put(316021, "飞行器 RTK 信号源检查超时,请稍后重试");
errorCodeMap.put(316022, "飞行器无法执行返航指令,请检查飞行器是否已开机,机场与飞行器是否已断连,请确认无以上问题后重试");
errorCodeMap.put(316023, "飞行器无法执行返航指令,飞行器已被 B 控接管,请在 B 控操控飞行器,或关闭 B 控后重试");
errorCodeMap.put(316024, "飞行器执行返航指令失败,请检查飞行器是否已起飞,确认飞行器已起飞后请重试");
errorCodeMap.put(316025, "飞行器参数配置失败,请稍后重试或重启机场后重试");
errorCodeMap.put(316026, "机场急停按钮被按下,无法执行飞行任务,请释放急停按钮后重试");
errorCodeMap.put(316027, "飞行器参数配置超时,请稍后重试或重启机场后重试");
errorCodeMap.put(316029, "机场急停按钮被按下,飞行器将飞往备降点降落,请立即检查飞行器是否已安全降落并将飞行器放回至机场");
errorCodeMap.put(316032, "获取电池数据超时,请稍后重试或重启飞行器后重试");
errorCodeMap.put(316033, "飞行器电池循环次数过高,为保证飞行安全,已自动终止任务,建议更换该电池");
errorCodeMap.put(316034, "无法起飞,飞行器固件版本与机场固件版本不匹配,为保证飞行安全请升级固件后再试");
errorCodeMap.put(316050, "飞行器因电量过低在舱外降落,请立即检查飞行器是否已安全降落并将飞行器放回至机场");
errorCodeMap.put(316051, "飞行任务异常,飞行器在舱外降落,请立即检查飞行器是否已安全降落并将飞行器放回至机场");
errorCodeMap.put(316052, "飞行任务异常,飞行器将飞往备降点降落,请立即检查飞行器是否已安全降落并将飞行器放回至机场");
errorCodeMap.put(316053, "用户已操控飞行器降落,请立即检查飞行器是否已安全降落并将飞行器放回至机场");
errorCodeMap.put(316100, "获取相机概要信息失败,请重试");
errorCodeMap.put(316101, "设置相机为单拍模式失败,请重试");
errorCodeMap.put(316102, "关闭相机水印失败,请重试");
errorCodeMap.put(316103, "设置测光模式到平均测光失败,请重试");
errorCodeMap.put(316104, "切换镜头到广角镜头失败,请重试");
errorCodeMap.put(316105, "设置相机存储照片失败,请重试");
errorCodeMap.put(316106, "红外变焦倍数设置失败,请重试");
errorCodeMap.put(316107, "照片尺寸设置4k失败请重试");
errorCodeMap.put(316108, "设置照片存储格式为jpeg格式失败请重试");
errorCodeMap.put(316109, "关闭相机畸变矫正失败,请重试");
errorCodeMap.put(316110, "打开相机机械快门失败,请重试");
errorCodeMap.put(316111, "设置对焦模式失败,请重试");
errorCodeMap.put(317001, "获取飞行器媒体文件数量失败,请重启机场后重试");
errorCodeMap.put(317002, "飞行器存储格式化失败,飞行器未开机、未连接或未检测到相机,请确认无以上问题后重试,或重启飞行器后重试");
errorCodeMap.put(317003, "飞行器存储格式化失败,请重启飞行器后重试");
errorCodeMap.put(317004, "机场媒体文件格式化失败,请稍后重试或重启机场后重试");
errorCodeMap.put(317005, "飞行器结束录像失败,本次飞行任务的媒体文件可能无法上传");
errorCodeMap.put(317006, "无法格式化,请等待当前飞行器媒体文件下载完成后再试");
errorCodeMap.put(317007, "无法格式化,请等待当前飞行器媒体文件下载完成后再试");
errorCodeMap.put(319001, "机场作业中或设备异常反馈上传日志中,无法执行飞行任务,请等待当前飞行任务或操作执行完成后重试");
errorCodeMap.put(319002, "机场系统运行异常,请重启机场后重试");
errorCodeMap.put(319003, "机场系统运行异常,请重新下发任务");
errorCodeMap.put(319004, "飞行任务执行超时,已自动终止本次飞行任务");
errorCodeMap.put(319005, "云端与机场通信异常,无法执行飞行任务");
errorCodeMap.put(319006, "取消飞行任务失败,飞行任务已经在执行中");
errorCodeMap.put(319007, "修改飞行任务失败,飞行任务已经在执行中");
errorCodeMap.put(319008, "机场时间与云端时间不同步,机场无法执行飞行任务");
errorCodeMap.put(319009, "飞行任务下发失败,请稍后重试或重启机场后重试");
errorCodeMap.put(319010, "机场固件版本过低,无法执行飞行任务,请升级机场固件为最新版本后重试");
errorCodeMap.put(319015, "机场正在初始化中,无法执行飞行任务,请等待机场初始化完成后重试");
errorCodeMap.put(319016, "机场正在执行其他飞行任务,无法执行本次飞行任务");
errorCodeMap.put(319017, "机场正在处理上次飞行任务媒体文件,无法执行本次飞行任务,请稍后重试");
errorCodeMap.put(319018, "机场正在自动导出日志中(设备异常反馈),无法执行飞行任务,请稍后重试");
errorCodeMap.put(319019, "机场正在拉取日志中(设备异常反馈),无法执行飞行任务,请稍后重试");
errorCodeMap.put(319020, "航线中断失败,请稍后重试");
errorCodeMap.put(319021, "退出远程控制失败,请稍后重试");
errorCodeMap.put(319022, "指点飞行失败,请稍后重试");
errorCodeMap.put(319023, "指点飞行停止失败,请稍后重试");
errorCodeMap.put(319024, "一键起飞失败,请稍后重试");
errorCodeMap.put(319025, "机场未准备完成,无法执行云端下发的飞行任务,请稍后重试");
errorCodeMap.put(319026, "飞行器电池电量低于用户设置的任务开始执行的电量,请等待充电完成后再执行飞行任务");
errorCodeMap.put(319027, "机场或飞行器剩余存储容量过低,无法执行飞行任务,请等待媒体文件上传,机场和飞行器存储容量释放后再执行飞行任务");
errorCodeMap.put(319028, "正在更新自定义飞行区");
errorCodeMap.put(319029, "正在更新离线地图");
errorCodeMap.put(319030, "操作失败,无飞行器控制权");
errorCodeMap.put(319031, "控制权异常,请刷新重试");
errorCodeMap.put(319032, "指点飞行失败,请稍后重试");
errorCodeMap.put(319033, "虚拟摇杆操作失败,请稍后重试");
errorCodeMap.put(319034, "虚拟摇杆操作失败,请稍后重试");
errorCodeMap.put(319035, "急停失败,请稍后重试");
errorCodeMap.put(319036, "设备远程调试中,请稍后重试");
errorCodeMap.put(319037, "设备本地调试中,请稍后重试");
errorCodeMap.put(319038, "设备正在升级,请稍后重试");
errorCodeMap.put(319042, "航线恢复失败,请稍后重试");
errorCodeMap.put(319043, "取消返航失败,请稍后重试");
errorCodeMap.put(319044, "航线任务已结束,无法恢复");
errorCodeMap.put(319045, "急停成功,请重新按键操作");
errorCodeMap.put(319046, "无法暂停航线,飞行器尚未进入航线或已退出航线");
errorCodeMap.put(319999, "机场系统运行异常,请重启机场后重试");
errorCodeMap.put(321000, "航线执行异常,请稍后重试或重启机场后重试");
errorCodeMap.put(321004, "航线文件解析失败,无法执行飞行任务,请检查航线文件");
errorCodeMap.put(321005, "航线缺少断点信息,机场无法执行飞行任务");
errorCodeMap.put(321257, "飞行任务已在执行中,请勿重复执行");
errorCodeMap.put(321258, "飞行任务无法终止,请检查飞行器状态");
errorCodeMap.put(321259, "飞行任务未开始执行,无法终止飞行任务");
errorCodeMap.put(321260, "飞行任务未开始执行,无法中断飞行任务");
errorCodeMap.put(321513, "航线规划高度已超过飞行器限高,机场无法执行飞行任务");
errorCodeMap.put(321514, "任务失败,起点或终点位于限远区域的缓冲区内或超过了限远距离");
errorCodeMap.put(321515, "航线穿过限飞区,机场无法执行飞行任务");
errorCodeMap.put(321516, "飞行器飞行高度过低,飞行任务执行被终止");
errorCodeMap.put(321517, "飞行器触发避障,飞行任务执行被终止。为保证飞行安全,请勿用当前航线执行断点续飞任务");
errorCodeMap.put(321519, "飞行器接近限飞区或限远距离自动返航,无法完成航线飞行");
errorCodeMap.put(321523, "飞行器起飞失败,请稍后重试,如果仍报错请联系大疆售后。");
errorCodeMap.put(321524, "飞行器起飞前准备失败,可能是飞行器无法定位或档位错误导致,请检查飞行器状态");
errorCodeMap.put(321528, "触碰自定义飞行区边界,航线任务已暂停");
errorCodeMap.put(321529, "目标点位于禁飞区域或者障碍物内,无法到达,航线任务已暂停,请重新规划后再试。");
errorCodeMap.put(321530, "飞行航线过程中轨迹规划失败,航线任务已暂停");
errorCodeMap.put(321769, "飞行器卫星定位信号差,无法执行飞行任务,请重启机场后重试");
errorCodeMap.put(321770, "飞行器挡位错误,无法执行飞行任务,请重启机场后重试");
errorCodeMap.put(321771, "飞行器返航点未设置,无法执行飞行任务,请重启机场后重试");
errorCodeMap.put(321772, "飞行器电量低于30%无法执行飞行任务请充电后重试建议电量≥50%");
errorCodeMap.put(321773, "飞行器执行飞行任务过程中低电量返航,无法完成航线飞行");
errorCodeMap.put(321775, "飞行器航线飞行过程中失联,无法完成航线飞行");
errorCodeMap.put(321776, "飞行器 RTK 收敛失败,无法执行飞行任务,请重启机场后重试");
errorCodeMap.put(321777, "飞行器未悬停,无法开始执行飞行任务");
errorCodeMap.put(321778, "用户使用 B 控操控飞行器起桨,机场无法执行飞行任务");
errorCodeMap.put(321784, "任务过程中遇到大风紧急返航");
errorCodeMap.put(322282, "机场执行飞行任务过程中被中断,飞行器被云端用户或遥控器接管");
errorCodeMap.put(322283, "机场执行飞行任务过程中被用户触发返航,无法完成航线飞行");
errorCodeMap.put(322539, "航线的断点信息错误,机场无法执行飞行任务");
errorCodeMap.put(322563, "航线轨迹生成失败,请检查飞机视觉镜头是否存在脏污或重启飞机后再试,如果仍报错请联系大疆售后。");
errorCodeMap.put(324012, "日志压缩过程超时,所选日志过多,请减少选择的日志后重试");
errorCodeMap.put(324013, "设备日志列表获取失败,请稍后重试");
errorCodeMap.put(324014, "设备日志列表为空,请刷新页面或重启机场后重试");
errorCodeMap.put(324015, "飞行器已关机或未连接,无法获取日志列表,请确认飞行器在舱内,通过远程调试将飞行器开机后重试");
errorCodeMap.put(324016, "机场存储空间不足,日志压缩失败,请清理机场存储空间或稍后重试");
errorCodeMap.put(324017, "日志压缩失败,无法获取所选飞行器日志,请刷新页面或重启机场后重试");
errorCodeMap.put(324018, "日志文件拉取失败,导致本次设备异常反馈上传失败,请稍后重试或重启机场后重试");
errorCodeMap.put(324019, "因机场网络异常,日志上传失败,请稍后重试。如果连续多次出现该问题,请联系代理商或大疆售后进行网络排障");
errorCodeMap.put(324021, "因机场重启中断日志导出,日志导出失败,请稍后重试");
errorCodeMap.put(324030, "因机场网络异常、飞行器图传链路异常等原因,媒体文件暂时无法上传或文件已上传但云端读取失败");
errorCodeMap.put(325001, "云端下发给机场的命令不符合格式要求,机场无法执行");
errorCodeMap.put(325003, "设备端命令响应错误,请重试");
errorCodeMap.put(325004, "设备端命令请求已超时,请重试");
errorCodeMap.put(325005, "当前机场无法响应任务,请稍后重试");
errorCodeMap.put(325006, "当前机场启动检查中,请稍后重试");
errorCodeMap.put(325007, "当前机场执行作业任务中,请稍后重试");
errorCodeMap.put(325008, "当前机场处理作业任务结果中,请稍后重试");
errorCodeMap.put(325009, "当前机场执行远程日志导出中,请稍后重试");
errorCodeMap.put(325010, "当前机场更新自定义飞行区中,请稍后重试");
errorCodeMap.put(325011, "当前机场更新离线地图中,请稍后重试");
errorCodeMap.put(325012, "当前飞机未连接,请稍后重试");
errorCodeMap.put(326002, "飞行器未安装 DJI Cellualr 模块");
errorCodeMap.put(326003, "飞行器 DJI Cellualr 模块中未安装 SIM 卡");
errorCodeMap.put(326004, "飞行器 DJI Cellualr 模块需要强制升级,否则无法使用");
errorCodeMap.put(326005, "操作失败,增强图传无法建立连接,请检查 4G 信号强度,或咨询运营商查询套餐流量和 APN 设置");
errorCodeMap.put(326006, "增强图传开关切换失败,请稍后重试");
errorCodeMap.put(326008, "机场未安装 DJI Cellualr 模块");
errorCodeMap.put(326009, "机场 DJI Cellualr 模块中未安装 SIM 卡");
errorCodeMap.put(326010, "机场 DJI Cellualr 模块需要强制升级,否则无法使用");
errorCodeMap.put(326103, "当前 eSIM 正在激活中,请稍后再试");
errorCodeMap.put(326104, "当前 eSIM 正在切换运营商中,请稍后再试");
errorCodeMap.put(326105, "DJI 增强图传模块正在切换模式中,请稍后再试");
errorCodeMap.put(326106, "DJI 增强图传模块异常,请重启设备后再试,如果仍报错请联系大疆售后");
errorCodeMap.put(326107, "请在设备管理 > 机场 > 设备运维中激活DJI增强图传模块的 eSIM 或插入 SIM 卡后再试");
errorCodeMap.put(327000, "参数设置失败,请稍后重试");
errorCodeMap.put(327001, "参数设置失败,请稍后重试");
errorCodeMap.put(327002, "获取控制权失败,请稍后重试");
errorCodeMap.put(327003, "获取控制权失败,请稍后重试");
errorCodeMap.put(327004, "画面拖动失败,请重试");
errorCodeMap.put(327005, "双击画面归中失败");
errorCodeMap.put(327006, "拍照失败");
errorCodeMap.put(327007, "开始录像失败");
errorCodeMap.put(327008, "停止录像失败");
errorCodeMap.put(327009, "切换相机模式失败");
errorCodeMap.put(327010, "ZOOM相机变焦失败");
errorCodeMap.put(327011, "IR相机变焦失败");
errorCodeMap.put(327012, "获取控制权失败,请稍后重试");
errorCodeMap.put(327013, "参数设置失败,请稍后重试");
errorCodeMap.put(327014, "云台已达限位");
errorCodeMap.put(327015, "直播启动失败,建议刷新直播或重新打开设备小窗");
errorCodeMap.put(327016, "失联动作设置失败,请重试");
errorCodeMap.put(327017, "指点飞行高度设置失败,请重试");
errorCodeMap.put(327018, "指点飞行模式切换失败,请重试");
errorCodeMap.put(327019, "当前状态无法看向标注点");
errorCodeMap.put(327020, "全景拍照停止命令超时");
errorCodeMap.put(327050, "当前设备状态不支持播放音频");
errorCodeMap.put(327051, "下载音频文件失败");
errorCodeMap.put(327052, "喊话器处理模式切换失败");
errorCodeMap.put(327053, "上传音频文件失败");
errorCodeMap.put(327054, "播放音频失败");
errorCodeMap.put(327055, "设置工作模式失败");
errorCodeMap.put(327056, "上传文本失败");
errorCodeMap.put(327057, "停止播放失败");
errorCodeMap.put(327058, "设置播放模式失败");
errorCodeMap.put(327059, "设置音量失败");
errorCodeMap.put(327060, "设置控件值失败");
errorCodeMap.put(327061, "发送文本值失败");
errorCodeMap.put(327062, "切换系统语言失败");
errorCodeMap.put(327063, "获取设备功能列表失败");
errorCodeMap.put(327064, "获取设备配置文件失败");
errorCodeMap.put(327065, "获取设备图片文件失败");
errorCodeMap.put(327066, "设备文件压缩失败");
errorCodeMap.put(327067, "设备文件上传失败");
errorCodeMap.put(327068, "上传音频文件失败md5校验失败");
errorCodeMap.put(327069, "上传音频文件失败");
errorCodeMap.put(327070, "上传音频文件失败,异常终止");
errorCodeMap.put(327071, "上传TTS文本失败md5校验失败");
errorCodeMap.put(327072, "上传TTS文本失败");
errorCodeMap.put(327073, "上传TTS文本失败异常终止");
errorCodeMap.put(327074, "喊话器重播失败");
errorCodeMap.put(327075, "喊话器编码失败");
errorCodeMap.put(327201, "全景拍照失败");
errorCodeMap.put(327202, "全景拍摄终止");
errorCodeMap.put(327203, "当前设备不支持全景拍照");
errorCodeMap.put(327204, "系统繁忙,无法全景拍照");
errorCodeMap.put(327205, "请求失败,无法全景拍照");
errorCodeMap.put(327206, "飞机未起飞,无法开始全景拍摄");
errorCodeMap.put(327207, "控制权获取失败,全景拍摄终止");
errorCodeMap.put(327208, "未知相机错误,无法开始全景拍摄");
errorCodeMap.put(327209, "相机超时,全景拍摄终止");
errorCodeMap.put(327210, "无法全景拍照");
errorCodeMap.put(327211, "存储空间不足,全景拍摄终止");
errorCodeMap.put(327212, "飞机运动中,无法开始全景拍摄");
errorCodeMap.put(327213, "云台运动中,无法开始全景拍摄");
errorCodeMap.put(327214, "用户操作摇杆,全景拍摄终止");
errorCodeMap.put(327215, "碰到限飞区,全景拍摄终止");
errorCodeMap.put(327216, "触发距离限制,全景拍摄终止");
errorCodeMap.put(327217, "云台受阻,全景拍摄终止");
errorCodeMap.put(327218, "拍照失败,全景拍摄终止");
errorCodeMap.put(327219, "全景图片拼接失败");
errorCodeMap.put(327220, "加载标定参数失败,全景拍摄终止");
errorCodeMap.put(327221, "调整相机参数失败,全景拍摄终止");
errorCodeMap.put(327500, "飞行器镜头除雾失败,请稍后重试");
errorCodeMap.put(328051, "飞机未完成实名登记,请连接遥控器,按照指引完成、实名登记后飞行");
errorCodeMap.put(328052, "飞机实名登记状态已注销,请连接遥控器,按照指引完成实名登记后飞行");
errorCodeMap.put(336000, "指点飞行命令发送失败,请重试");
errorCodeMap.put(336001, "飞行器数据异常,无法响应指令");
errorCodeMap.put(336002, "飞行器GPS信号差");
errorCodeMap.put(336003, "飞行器定位失效,无法响应指令");
errorCodeMap.put(336004, "指点飞行自主规划失败");
errorCodeMap.put(336005, "飞行器返航点未更新");
errorCodeMap.put(336006, "飞行器已失联,已退出指点飞行");
errorCodeMap.put(336017, "飞行器电量不足以完成当前任务");
errorCodeMap.put(336018, "已切换飞行器规划模式");
errorCodeMap.put(336019, "指点飞行因限高自动调整飞行高度");
errorCodeMap.put(336513, "目标点在禁飞区内");
errorCodeMap.put(336514, "目标点超出飞行器限远");
errorCodeMap.put(336515, "目标点在禁飞区内");
errorCodeMap.put(336516, "目标点超出飞行器限高");
errorCodeMap.put(336517, "目标点超出飞行器限低");
errorCodeMap.put(337025, "飞行器无法起飞");
errorCodeMap.put(337026, "目标点异常,请重试");
errorCodeMap.put(337027, "飞行器速度设置异常,请重试");
errorCodeMap.put(337028, "飞行器版本异常,请检查飞行器版本");
errorCodeMap.put(337029, "飞行器无法响应当前任务,请稍后重试");
errorCodeMap.put(337030, "指令飞行安全离场高过低");
errorCodeMap.put(337537, "已触碰禁飞区");
errorCodeMap.put(337538, "已触碰飞行器限远");
errorCodeMap.put(337539, "已触碰禁飞区");
errorCodeMap.put(337540, "已触碰飞行器限高或限高区");
errorCodeMap.put(337541, "已触碰飞行器限低");
errorCodeMap.put(337542, "飞行器起飞失败,请重试");
errorCodeMap.put(337543, "目标点可能在障碍物内,请检查周边环境");
errorCodeMap.put(337544, "检测到障碍物,请检查周边环境");
errorCodeMap.put(337545, "飞行器规划异常,请重试");
errorCodeMap.put(337546, "已触碰自定义飞行区边界");
errorCodeMap.put(338001, "飞行器通信异常,无法执行飞行任务,请重启飞行器与机场后重试");
errorCodeMap.put(338002, "飞行器通信异常,无法执行飞行任务,请重启飞行器与机场后重试");
errorCodeMap.put(338003, "飞行器通信异常,无法执行飞行任务,请重启飞行器与机场后重试");
errorCodeMap.put(338004, "飞行器通信异常,无法执行飞行任务,请重启飞行器与机场后重试");
errorCodeMap.put(338005, "起飞机场与降落机场部署距离超出限制,无法执行飞行任务,请选择两个部署距离不超过 15km 的机场执行任务");
errorCodeMap.put(338006, "无法执行飞行任务,请检查降落机场是否已申请解禁证书、是否位于自定义禁飞区或是否位于自定义飞行区外");
errorCodeMap.put(338007, "目标降落机场部署突破限高区限高,无法执行任务,请申请解禁证书后再试");
errorCodeMap.put(338008, "目标降落机场部署突破飞行器设置的限高,无法执行任务,请调整限高后重试");
errorCodeMap.put(338009, "飞行器 GPS 定位信号差,无法执行任务,请重启飞行器后重试");
errorCodeMap.put(338010, "飞行器定位失效,无法执行任务,请重启飞行器后重试");
errorCodeMap.put(338017, "飞行器数据更新失败,无法执行任务,请重启飞行器后重试");
errorCodeMap.put(338018, "飞行器数据更新失败,无法执行任务,请重启飞行器后重试");
errorCodeMap.put(338019, "飞行器到目标机场的返航路线正在规划中,无法执行任务,请重启飞行器后重试");
errorCodeMap.put(338020, "飞行器无法根据规划的路径到达目标降落机场,无法执行任务,请重新选择降落机场后再试");
errorCodeMap.put(338021, "飞行器当前电量不足以到达目标降落机场,无法执行任务,请给飞行器充电后重试");
errorCodeMap.put(338049, "响应遥控器杆量,已退出指点飞行");
errorCodeMap.put(338050, "响应终止指令,已退出指点飞行");
errorCodeMap.put(338051, "飞行器低电量返航,已退出指点飞行");
errorCodeMap.put(338052, "飞行器低电量降落,已退出指点飞行");
errorCodeMap.put(338053, "附近有载人机,已退出指点飞行");
errorCodeMap.put(338054, "响应其他高优先级任务,已退出指点飞行");
errorCodeMap.put(338255, "飞行器通信异常,无法执行飞行任务,请重启飞行器与机场后重试");
errorCodeMap.put(386535, "航线执行异常,请稍后重试或重启机场后重试");
errorCodeMap.put(513001, "直播失败,飞行器不存在或飞行器类型错误");
errorCodeMap.put(513002, "直播失败,相机不存在或相机类型错误");
errorCodeMap.put(513003, "相机已经在直播中,请勿重复开启直播");
errorCodeMap.put(513005, "直播失败,直播参数(清晰度)设置错误");
errorCodeMap.put(513006, "直播启动失败,请刷新重试");
errorCodeMap.put(513008, "直播失败,设备端图传数据异常");
errorCodeMap.put(513010, "直播失败,设备无法联网");
errorCodeMap.put(513011, "操作失败,设备未开启直播");
errorCodeMap.put(513012, "操作失败,设备已在直播中,不支持切换镜头");
errorCodeMap.put(513013, "直播失败,直播使用的视频传输协议不支持");
errorCodeMap.put(513014, "直播失败,直播参数错误或者不完整");
errorCodeMap.put(513015, "直播异常,网络卡顿, 请刷新后重试");
errorCodeMap.put(513016, "直播异常,视频解码失败");
errorCodeMap.put(513017, "直播已暂停,请等待当前飞行器媒体文件下载完成后再试");
errorCodeMap.put(513099, "直播失败,请稍后重试");
errorCodeMap.put(514100, "机场运行异常,请重启机场后重试");
errorCodeMap.put(514101, "推杆闭合失败,请检查停机坪上是否存在异物,飞行器方向是否放反,或重启机场后重试");
errorCodeMap.put(514102, "推杆展开失败,请检查停机坪上是否存在异物,或重启机场后重试");
errorCodeMap.put(514103, "飞行器电量低于30%无法执行飞行任务请充电后重试建议电量≥50%");
errorCodeMap.put(514104, "飞行器电池开始充电失败,请重启机场后重试");
errorCodeMap.put(514105, "飞行器电池停止充电失败,请重启机场后重试");
errorCodeMap.put(514106, "飞行器电源控制异常,请重启机场后重试");
errorCodeMap.put(514107, "舱盖开启失败,请检查舱盖周围是否存在异物,或重启机场后重试");
errorCodeMap.put(514108, "舱盖关闭失败,请检查舱盖周围是否存在异物,或重启机场后重试");
errorCodeMap.put(514109, "飞行器开机失败,请检查飞行器是否在舱和飞机电量是否正常,或重启机场后重试");
errorCodeMap.put(514110, "飞行器关机失败,请重启机场后重试");
errorCodeMap.put(514111, "飞行器慢转收桨控制异常,请重启机场后重试");
errorCodeMap.put(514112, "飞行器慢转收桨控制异常,请重启机场后重试");
errorCodeMap.put(514113, "机场推杆与飞行器无法连接,请检查飞行器是否在舱内,推杆闭合时是否被卡住,充电连接器是否脏污或损坏");
errorCodeMap.put(514114, "获取飞行器电源状态失败,请重启机场后重试");
errorCodeMap.put(514116, "无法执行当前操作,机场正在执行其他控制指令,请稍后重试");
errorCodeMap.put(514117, "舱盖开启或关闭未到位,请重启机场后重试");
errorCodeMap.put(514118, "推杆展开或闭合未到位,请重启机场后重试");
errorCodeMap.put(514120, "机场与飞行器断连,请重启机场后重试或重新对频");
errorCodeMap.put(514121, "机场急停按钮被按下,请释放急停按钮");
errorCodeMap.put(514122, "获取飞行器充电状态失败,请重启机场后重试");
errorCodeMap.put(514123, "飞行器电池电量过低无法开机");
errorCodeMap.put(514124, "获取飞行器电池信息失败,无法执行飞行任务,请重启机场后重试");
errorCodeMap.put(514125, "飞行器电池电量已接近满电状态无法开始充电请使用至95%以下再进行充电");
errorCodeMap.put(514134, "雨量过大,机场无法执行飞行任务,请稍后重试");
errorCodeMap.put(514135, "风速大于12m/s26 mph机场无法执行飞行任务请稍后重试");
errorCodeMap.put(514136, "机场供电断开,机场无法执行飞行任务,请恢复机场供电后重试");
errorCodeMap.put(514137, "环境温度过低于-20℃ (-4°F),机场无法执行飞行任务,请稍后重试");
errorCodeMap.put(514138, "飞行器电池正在保养中,机场无法执行飞行任务,请等待保养结束后重试");
errorCodeMap.put(514139, "飞行器电池无法执行保养指令,飞行器电池无需保养");
errorCodeMap.put(514140, "飞行器电池无法执行保养指令,飞行器电池无需保养");
errorCodeMap.put(514141, "机场系统运行异常,请重启机场后重试");
errorCodeMap.put(514142, "飞行器起飞前,机场推杆与飞行器无法连接,请检查飞行器是否在舱内,推杆闭合时是否被卡住,充电连接器是否脏污或损坏");
errorCodeMap.put(514143, "推杆未闭合或闭合不到位,请稍后重试或重启机场后重试");
errorCodeMap.put(514144, "舱盖未关闭或关闭不到位,请稍后重试或重启机场后重试");
errorCodeMap.put(514145, "机场处于现场调试中,无法执行当前操作或执行飞行任务,请断开遥控器和机场的数据线连接后重试");
errorCodeMap.put(514146, "机场处于远程调试中,无法执行飞行任务,请退出远程调试后重试");
errorCodeMap.put(514147, "设备升级中,无法进行远程调试或执行飞行任务,请等待升级完成后重试");
errorCodeMap.put(514148, "机场已经在作业中,无法进行远程调试或再次执行飞行任务,请等待当前任务执行完成后重试");
errorCodeMap.put(514149, "机场系统运行异常,无法执行飞行任务,请重启机场后重试");
errorCodeMap.put(514150, "设备重启中,无法执行飞行任务,请等待重启完成后重试");
errorCodeMap.put(514151, "设备升级中,无法执行设备重启指令,请等待升级完成后重试");
errorCodeMap.put(514153, "机场已退出远程调试模式,无法执行当前操作");
errorCodeMap.put(514154, "获取内循环出风口温度失败,请稍后再试");
errorCodeMap.put(514156, "飞机不在舱内,请立即检查飞行器是否已安全降落并将飞行器放回至机场");
errorCodeMap.put(514157, "执行开机失败,无线充电线圈业务繁忙,请重启机场后再试复");
errorCodeMap.put(514158, "无法起飞,机场 RTK 业务异常,请重启机场后再试");
errorCodeMap.put(514159, "任务失败,降落机场检测到飞行器,请确保降落机场没有飞行器后再试");
errorCodeMap.put(514162, "飞机和机场连接失败,请关闭机场舱盖或重启机场后再试");
errorCodeMap.put(514163, "请确保飞机电池插入到位或重启飞机后再试");
errorCodeMap.put(514164, "设备重启失败,请稍后重试,如果仍报错请联系大疆售后。");
errorCodeMap.put(514165, "设备重启失败,请稍后重试,如果仍报错请联系大疆售后。");
errorCodeMap.put(514170, "机场系统初始化中,无法执行当前操作或指令,请等待机场系统初始化完成后重试");
errorCodeMap.put(514171, "云端下发给机场的命令不符合格式要求,机场无法执行");
errorCodeMap.put(514172, "飞行器无法关机,蓝牙连接状态为未连接,请尝试重启飞行器和机场,或去现场重新对频飞行器与机场后再试");
errorCodeMap.put(514173, "由于天气原因环境温度低于5度并且降雨大于等于中雨可能导致桨叶结冰影响作业安全暂无法执行任务");
errorCodeMap.put(514174, "飞行器充电失败,机场舱盖开启或关闭未到位,请关闭舱盖后再试");
errorCodeMap.put(514180, "停止空调制冷或停止空调制热失败,请稍后重试");
errorCodeMap.put(514181, "开启空调制冷失败,请稍后重试");
errorCodeMap.put(514182, "开启空调制热失败,请稍后重试");
errorCodeMap.put(514183, "开启空调除湿失败,请稍后重试");
errorCodeMap.put(514184, "当前温度低于 0 ℃32°F无法开启空调制冷");
errorCodeMap.put(514185, "当前温度高于 45 ℃115°F无法开启空调制热");
errorCodeMap.put(514300, "网关异常");
errorCodeMap.put(514301, "请求超时,连接断开");
errorCodeMap.put(514302, "网络证书异常,连接失败");
errorCodeMap.put(514303, "网络异常,连接断开");
errorCodeMap.put(514304, "机场请求被拒,连接失败");
errorCodeMap.put(326007, "开启失败,请检查图传增强模块");
}
}

View File

@ -0,0 +1,37 @@
package com.ruoyi.dj.mqtt.handler;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ruoyi.dj.mqtt.client.DJICloudMQTTClient;
import com.ruoyi.dj.mqtt.common.CommonFieldsVo;
import com.ruoyi.dj.mqtt.common.CommonResponse;
import com.ruoyi.dj.mqtt.domain.DrcVo;
import com.ruoyi.dj.mqtt.utils.CommonFieldsVoUtil;
import com.ruoyi.dj.mqtt.utils.ParseMethodUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.concurrent.CompletableFuture;
@Slf4j
@Component
public class DrcUpHandler {
@Autowired
private DJICloudMQTTClient djiCloudMQTTClient;
public void handle(String matchedPart, String message) {
log.info("drc up handler:{}", message);
// 提取方法名
DrcVo drcVo = CommonFieldsVoUtil.extractDrcVo(message);
log.info("drc method:{}", drcVo.getMethod());
if (drcVo.getMethod().equals("drone_control")) {
log.info("drone_control");
}
CompletableFuture<String> responseFuture = djiCloudMQTTClient.getDrcResponseFutures(drcVo);
if (responseFuture != null) {
responseFuture.complete(message);
djiCloudMQTTClient.completeDrcResponseFuture(drcVo, message);
}
}
}

View File

@ -0,0 +1,132 @@
package com.ruoyi.dj.mqtt.handler;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ruoyi.dj.mqtt.common.CommonResponse;
import com.ruoyi.dj.mqtt.utils.ParseMethodUtil;
import com.ruoyi.dj.websocket.service.MessagingService;
import com.ruoyi.system.domain.FlightTasks;
import com.ruoyi.system.domain.MinioFile;
import com.ruoyi.system.domain.Missions;
import com.ruoyi.system.domain.VideoInfo;
import com.ruoyi.system.mapper.FlightTasksMapper;
import com.ruoyi.system.service.IFlightTasksService;
import com.ruoyi.system.service.IMinioFileService;
import com.ruoyi.system.service.IMissionsService;
import com.ruoyi.system.service.IVideoInfoService;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Slf4j
@Component
public class EventsHandler {
@Resource
private MessagingService messagingService;
@Resource
private IFlightTasksService flightTasksService;
@Resource
private IVideoInfoService videoInfoService;
@Resource
private IMissionsService missionsService;
private ObjectMapper objectMapper = new ObjectMapper();
public void handle(String matchedPart, String message) {
log.info("eventHandler 中的 message:{}", message);
CommonResponse commonResponse = ParseMethodUtil.parseMethod(message);
if (commonResponse != null) {
if (commonResponse.getMethod().equals("putter_open")) {
messagingService.sendMessage("putter_open", matchedPart, message);
}
if (commonResponse.getMethod().equals("flighttask_ready")) {
log.info("下发任务后的任务就绪通知:{}", message);
}
// 打开充电进度
if (commonResponse.getMethod().equals("charge_open")) {
messagingService.sendMessage("charge_open", matchedPart, message);
}
// DRC 链路状态通知
if (commonResponse.getMethod().equals("drc_status_notify")) {
log.info("eventHandler 中的 drc_status_notify,{}", message);
messagingService.sendMessage("drc_status_notify", matchedPart, message);
}
// 关闭充电进度
if (commonResponse.getMethod().equals("charge_close")) {
messagingService.sendMessage("charge_close", matchedPart, message);
}
// 拍照进度
if (commonResponse.getMethod().equals("camera_photo_take_progress")) {
log.info("eventHandler 中的 camera_photo_take_progress,{}", message);
messagingService.sendMessage("camera_photo_take_progress", matchedPart, message);
}
// 航线任务的执行状态
if (commonResponse.getMethod().equals("flighttask_progress")) {
log.info("eventHandler 中的 flighttask_progress,{}", message);
messagingService.sendMessage("flighttask_progress", matchedPart, message);
JsonNode rootNode = null;
try {
rootNode = objectMapper.readTree(message);
// 获取flightId
JsonNode flightIdNode = rootNode.path("data").path("output").path("ext").path("flight_id");
// 获取状态
JsonNode statusNode = rootNode.path("data").path("output").path("status");
String flightId = flightIdNode.asText();
String status = statusNode.asText();
// 创建状态与汉字的映射表
Map<String, String> statusMap = new HashMap<>();
statusMap.put("canceled", "取消或终止");
statusMap.put("failed", "失败");
statusMap.put("in_progress", "执行中");
statusMap.put("ok", "执行成功");
statusMap.put("partially_done", "部分完成");
statusMap.put("paused", "暂停");
statusMap.put("rejected", "拒绝");
statusMap.put("sent", "已下发");
statusMap.put("timeout", "超时");
// 获取对应的中文状态
String chineseStatus = statusMap.getOrDefault(status, "未知状态");
// 根据flightId更新任务状态
Date date = new Date();
Missions missions = new Missions();
missions.setFlightId(Long.valueOf(flightId));
missions.setState(chineseStatus); // 设置汉字状态
missions.setLastTime(date);
missionsService.updateMissionsByFlightId(missions);
flightTasksService.updateFlightTasksStatus(flightId, chineseStatus, date);
log.info("任务状态更新为: flightId={}, state={}, lastTime={}", flightId, chineseStatus, date);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
if (commonResponse.getMethod().equals("flighttask_ready")) {
log.info("eventHandler 中的 flighttask_ready,{}", message);
}
}
try {
ObjectMapper mapper = new ObjectMapper();
JsonNode rootNode = mapper.readTree(message);
JsonNode methodNode = rootNode.get("method");
if (methodNode != null) {
String method = methodNode.asText();
messagingService.sendMessage(method, matchedPart, message);
}
JsonNode dataNode = rootNode.get("data");
if (dataNode != null && dataNode.has("reason")) {
log.info("Reason found in data: " + message);
}
} catch (Exception e) {
log.error("Error processing JSON message", e);
}
}
}

View File

@ -0,0 +1,93 @@
package com.ruoyi.dj.mqtt.handler;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ruoyi.dj.mqtt.domain.*;
import com.ruoyi.dj.websocket.service.MessagingService;
import com.ruoyi.system.domain.Drone;
import com.ruoyi.system.domain.VideoInfo;
import com.ruoyi.system.service.IDroneService;
import com.ruoyi.system.service.IVideoInfoService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
@Slf4j
@Component
public class OSDHandler {
@Autowired
private StringRedisTemplate redisTemplate;
@Resource
private MessagingService messagingService;
private static final ObjectMapper objectMapper = new ObjectMapper();
@Resource
private IDroneService droneService;
@Resource
private IVideoInfoService videoInfoService;
public void handle(String matchedPart, String message) {
System.out.println("收到消息:"+message);
Object object = null;
try {
object = convertJsonToOSDMessage(matchedPart, message);
if (object instanceof OSDMessage3) {
// 将 object 转化为 OSDMessage3
OSDMessage3 osdMessage3 = (OSDMessage3) object;
String deviceSn = osdMessage3.getData().getSub_device().getDevice_sn();
if (deviceSn != null) {
Drone drone = droneService.selectDroneBySn(deviceSn);
if (drone != null) {
// 设置网关在线状态【实际上是飞机的在线状态】
redisTemplate.opsForValue().set(matchedPart, String.valueOf(osdMessage3.getData().getSub_device().getDevice_online_status()));
// 更新 drone 的 isOnline
drone.setIsOnline(osdMessage3.getData().getSub_device().getDevice_online_status() == 0 ? "离线" : "在线");
droneService.updateDrone(drone);
} else {
// 新增设备数据
drone = new Drone();
drone.setSn(deviceSn);
drone.setGateway(matchedPart);
drone.setIsOnline(osdMessage3.getData().getSub_device().getDevice_online_status() == 0 ? "离线" : "在线");
droneService.insertDrone(drone);
}
}
}
} catch (Exception e) {
log.error("解析 OSD 消息失败: " + e.toString());
throw new RuntimeException(e);
}
}
public Object convertJsonToOSDMessage(String gateway, String json) throws Exception {
JsonNode rootNode = objectMapper.readTree(json);
if (rootNode.has("data") && rootNode.get("data").has("job_number")) {
// 大疆文档中的 thing/product/{dock_sn}/osd 的第一个 OSD 消息
messagingService.sendMessage("osd1", gateway, json);
return objectMapper.readValue(json, OSDMessage1.class);
} else if (rootNode.has("data") && rootNode.get("data").has("drc_state")) {
messagingService.sendMessage("osd2", gateway, json);
// 大疆文档中的 thing/product/{dock_sn}/osd 的第二个 OSD 消息
return objectMapper.readValue(json, OSDMessage2.class);
} else if (rootNode.has("data") && rootNode.get("data").has("network_state")) {
messagingService.sendMessage("osd3", gateway, json);
// 大疆文档中的 thing/product/{dock_sn}/osd 的第三个 OSD 消息
return objectMapper.readValue(json, OSDMessage3.class);
} else {
// 大疆文档中的 thing/product/{dock_sn}/osd 的第四个 OSD 消息
VideoInfo videoInfo = videoInfoService.selectGatewayByCondition(gateway);
if (videoInfo != null) {
messagingService.sendMessage("osd4", videoInfo.getGateway(), json);
}
return objectMapper.readValue(json, OSDMessage4.class);
}
}
}

View File

@ -0,0 +1,13 @@
package com.ruoyi.dj.mqtt.handler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class PropertySetReplyHandler {
public void handle(String matchedPart, String message) {
log.info("处理 Property Set Reply 消息: " + message);
}
}

View File

@ -0,0 +1,177 @@
package com.ruoyi.dj.mqtt.handler;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ruoyi.dj.mqtt.client.DJICloudMQTTClient;
import com.ruoyi.dj.mqtt.cmd.CmdConstant;
import com.ruoyi.dj.mqtt.common.CommonFields;
import com.ruoyi.dj.mqtt.common.CommonFieldsVo;
import com.ruoyi.dj.mqtt.common.CommonResponse;
import com.ruoyi.dj.mqtt.domain.AppConfig;
import com.ruoyi.dj.mqtt.domain.StorageConfig;
import com.ruoyi.dj.mqtt.utils.ParseMethodUtil;
import com.ruoyi.dj.mqtt.utils.PayloadUtil;
import com.ruoyi.dj.mqtt.utils.StorageConfigUtil;
import com.ruoyi.dj.websocket.service.MessagingService;
import com.ruoyi.system.domain.DroneMissions;
import com.ruoyi.system.domain.FlightPaths;
import com.ruoyi.system.domain.Missions;
import com.ruoyi.system.service.IDroneMissionsService;
import com.ruoyi.system.service.IFlightPathsService;
import com.ruoyi.system.service.IMissionsService;
import io.minio.credentials.Credentials;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
@Slf4j
@Component
public class RequestsHandler {
private static final String REQUEST_REPLY_TOPIC = "thing/product/?/requests_reply";
@Resource
private DJICloudMQTTClient djiCloudMQTTClient;
@Resource
private IDroneMissionsService droneMissionsService;
@Autowired
private Environment environment;
private ObjectMapper objectMapper = new ObjectMapper();
@Resource
private IMissionsService missionsService;
@Resource
private IFlightPathsService flightPathsService;
public void handle(String matchedPart, String message) {
CommonResponse commonResponse = ParseMethodUtil.parseMethod(message);
// 新任务资源获取
if (commonResponse.getMethod().equals("flighttask_resource_get")) {
log.info("request 中的 flighttask_resource_get:{}", message);
// 提取 message 中的 flight_id 字段
try {
JsonNode rootNode = objectMapper.readTree(message);
JsonNode dataNode = rootNode.path("data");
String flightId = dataNode.path("flight_id").asText();
Missions missions = new Missions();
missions.setFlightId(Long.valueOf(flightId));
Missions missions1 = missionsService.selectMissionsList(missions).get(0);
// 查询航线文件URL和签名
String fileId = missions1.getFileId();
FlightPaths flightPaths = flightPathsService.selectFlightPathsById(Long.valueOf(fileId));
CommonFieldsVo commonFieldsVo = new CommonFieldsVo();
commonFieldsVo.setGateway(matchedPart);
commonFieldsVo.setMethod(commonResponse.getMethod());
commonFieldsVo.setTid(commonResponse.getTid());
commonFieldsVo.setBid(commonResponse.getBid());
Map<String, Object> fileMap = new HashMap<>();
fileMap.put("fingerprint", flightPaths.getFileMd5());
fileMap.put("url", flightPaths.getFileUrl());
Map<String, Object> outputMap = new HashMap<>();
outputMap.put("file", fileMap);
Map<String, Object> dataMap = new HashMap<>();
dataMap.put("output", outputMap);
dataMap.put("result", 0);
String payload = PayloadUtil.generatePayload(commonFieldsVo, dataMap);
log.info("Generated Payload: " + payload);
djiCloudMQTTClient.publishMessage(REQUEST_REPLY_TOPIC.replace("?", matchedPart), payload);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
// 获取上传临时凭证
if (commonResponse.getMethod().equals("storage_config_get")) {
try {
// 将消息字符串转换为 JsonNode
JsonNode jsonNode = objectMapper.readTree(message);
log.info("请求凭证的原始消息: " + message);
// 检查 data 属性是否存在并包含 module 属性
if (jsonNode.has("data") && jsonNode.get("data").has("module")) {
// 转换为 StorageConfigRequest 对象
StorageConfig storageConfigRequest = objectMapper.treeToValue(jsonNode, StorageConfig.class);
// 获取 json 字符串中的 gateway、bid、tid、timestamp、method
String gateway = jsonNode.has("gateway") ? jsonNode.get("gateway").asText() : null;
String bid = jsonNode.has("bid") ? jsonNode.get("bid").asText() : null;
String tid = jsonNode.has("tid") ? jsonNode.get("tid").asText() : null;
String topic = REQUEST_REPLY_TOPIC.replace("?", gateway);
CommonFields commonFields = new CommonFields();
commonFields.setBid(bid);
commonFields.setGateway(gateway);
commonFields.setMethod(commonResponse.getMethod());
commonFields.setTid(tid);
commonFields.setTimestamp(System.currentTimeMillis());
// 异步发送消息、避免minio阻塞
CompletableFuture.runAsync(() -> {
try {
Map<String, Object> map = StorageConfigUtil.createStorageConfig(matchedPart);
if (map == null) {
log.error("MinIO 服务异常,配置返回为空");
String payload = PayloadUtil.generatePayload(commonFields, map);
log.info("下发凭证信息: " + payload);
// 发送消息给无人机
djiCloudMQTTClient.publishMessage(topic, payload);
} else {
String payload = PayloadUtil.generatePayload(commonFields, map);
djiCloudMQTTClient.publishMessage(topic, payload);
}
} catch (Exception e) {
log.error("MinIO 配置生成异常", e);
}
});
}
} catch (JsonProcessingException e) {
log.error("JSON 处理错误: ", e);
}
}
// 获取配置的方法【本地内网部署需要同步时钟周期】
if (commonResponse.getMethod().equals("config")) {
try {
// 将消息字符串转换为 JsonNode
JsonNode jsonNode = objectMapper.readTree(message);
log.info("获取配置的原始消息: " + message);
// 检查 data 属性是否存在并包含 app_id 属性
if (jsonNode.has("data") && jsonNode.get("data").has("app_id")) {
// 转换为 AppConfig 对象
AppConfig appConfig = objectMapper.treeToValue(jsonNode, AppConfig.class);
// 获取 json 字符串中的 gateway、bid、tid、timestamp、method
String gateway = jsonNode.has("gateway") ? jsonNode.get("gateway").asText() : null;
String bid = jsonNode.has("bid") ? jsonNode.get("bid").asText() : null;
String tid = jsonNode.has("tid") ? jsonNode.get("tid").asText() : null;
String topic = REQUEST_REPLY_TOPIC.replace("?", gateway);
CommonFields commonFields = new CommonFields();
commonFields.setBid(bid);
commonFields.setGateway(gateway);
commonFields.setMethod(commonResponse.getMethod());
commonFields.setTid(tid);
commonFields.setTimestamp(System.currentTimeMillis());
Map<String, Object> map = new HashMap<>();
map.put("app_id", environment.getProperty("deploy.appid"));
map.put("app_key", environment.getProperty("deploy.appKey"));
map.put("app_license", environment.getProperty("deploy.appLicense"));
map.put("ntp_server_host", environment.getProperty("deploy.ntpServerHost"));
map.put("ntp_server_port", environment.getProperty("deploy.ntpServerPort"));
String payload = PayloadUtil.generatePayload(commonFields, map);
log.info("下发配置更新的信息: " + payload);
// 发送消息给无人机
djiCloudMQTTClient.publishMessage(topic, payload);
}
} catch (JsonProcessingException e) {
log.error("JSON 处理错误: ", e);
}
}
}
}

View File

@ -0,0 +1,47 @@
package com.ruoyi.dj.mqtt.handler;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ruoyi.dj.mqtt.client.DJICloudMQTTClient;
import com.ruoyi.dj.mqtt.common.CommonFields;
import com.ruoyi.dj.mqtt.common.CommonFieldsVo;
import com.ruoyi.dj.mqtt.common.CommonResponse;
import com.ruoyi.dj.mqtt.utils.CommonFieldsVoUtil;
import com.ruoyi.dj.mqtt.utils.GatewayOperationLogUtil;
import com.ruoyi.dj.mqtt.utils.ParseMethodUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.concurrent.CompletableFuture;
@Slf4j
@Component
public class ServicesReplyHandler {
@Autowired
private DJICloudMQTTClient djiCloudMQTTClient;
ObjectMapper objectMapper = new ObjectMapper();
public void handle(String matchedPart, String message) {
CommonFieldsVo commonFieldsVo = CommonFieldsVoUtil.extractCommonFields(message);
if (commonFieldsVo != null) {
CommonResponse commonResponse = ParseMethodUtil.parseMethod(message);
if (commonResponse.getMethod().equals("flighttask_prepare")) {
log.info("下发任务的回复:{}", message);
}
if (commonResponse.getMethod().equals("flighttask_execute")) {
log.info("service_reply 的 flighttask_execute,{}", message);
}
CompletableFuture<String> responseFuture = djiCloudMQTTClient.getResponseFuture(commonFieldsVo);
if (responseFuture != null) {
responseFuture.complete(message);
djiCloudMQTTClient.completeResponseFuture(commonFieldsVo, message);
} else {
log.warn("未找到匹配 Services Reply 的响应处理器tid: {}, method: {}", commonFieldsVo.getTid(), commonFieldsVo.getMethod());
}
} else {
log.error("提取 CommonFields 失败,消息: " + message);
}
}
}

View File

@ -0,0 +1,88 @@
package com.ruoyi.dj.mqtt.handler;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ruoyi.dj.mqtt.common.CommonResponse;
import com.ruoyi.dj.mqtt.domain.VideoRoot;
import com.ruoyi.dj.mqtt.utils.ParseMethodUtil;
import com.ruoyi.system.domain.VideoInfo;
import com.ruoyi.system.service.IVideoInfoService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
@Slf4j
@Component
public class StateHandler {
private static final ObjectMapper objectMapper = new ObjectMapper();
@Resource
private IVideoInfoService videoInfoService;
public void handle(String matchedPart, String message) {
try {
JsonNode rootNode = objectMapper.readTree(message);
JsonNode dataNode = rootNode.path("data");
String gateway = rootNode.path("gateway").asText();
if (dataNode.has("live_capacity")) {
System.out.println(message);
log.info("是直播设备的相关属性状态、开始处理");
try {
VideoRoot videoRoot = objectMapper.readValue(message, VideoRoot.class);
for (VideoRoot.DataContent.LiveCapacity.Device device : videoRoot.getData().getLive_capacity().getDevice_list()) {
String sn = device.getSn();
for (VideoRoot.DataContent.LiveCapacity.Device.Camera camera : device.getCamera_list()) {
String cameraIndex = camera.getCamera_index();
for (VideoRoot.DataContent.LiveCapacity.Device.Camera.Video video : camera.getVideo_list()) {
String videoIndex = video.getVideo_index();
if (gateway != null && !gateway.isEmpty() && sn != null && !sn.isEmpty() && cameraIndex != null && !cameraIndex.isEmpty() && videoIndex != null && !videoIndex.isEmpty()) {
VideoInfo videoInfo = new VideoInfo();
videoInfo.setGateway(gateway);
videoInfo.setSn(sn);
videoInfo.setVideoIndex(videoIndex);
videoInfo.setType(sn.equals(gateway) ? "机场" : "飞机");
// 查询数据库中是否存在这条记录
VideoInfo existingVideoInfo = videoInfoService.findVideoInfo(videoInfo);
if (existingVideoInfo == null) {
// 数据不存在,新增
videoInfo.setCameraIndex(cameraIndex);
videoInfoService.insertVideoInfo(videoInfo);
log.info("新增数据: " + videoInfo);
} else {
// 数据已存在
String cameraIndexList = existingVideoInfo.getCameraIndex();
List<String> cameraIndexSet = Arrays.stream(cameraIndexList.split(",")).collect(Collectors.toList());
if (!cameraIndexSet.contains(cameraIndex)) {
// 如果 cameraIndex 不存在于现有记录中,追加并更新
existingVideoInfo.setCameraIndex(cameraIndexList + "," + cameraIndex);
videoInfoService.updateVideoInfo(existingVideoInfo);
log.info("更新后的数据: " + existingVideoInfo);
} else {
// cameraIndex 已存在,无需处理
log.info("cameraIndex 已存在,无需更新: " + existingVideoInfo);
}
}
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
} else {
log.info("不是直播设备的相关属性状态、不处理");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

View File

@ -0,0 +1,135 @@
package com.ruoyi.dj.mqtt.handler;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ruoyi.dj.mqtt.client.DJICloudMQTTClient;
import com.ruoyi.dj.mqtt.common.CommonFields;
import com.ruoyi.dj.mqtt.common.CommonResponse;
import com.ruoyi.dj.mqtt.domain.StorageConfig;
import com.ruoyi.dj.mqtt.domain.UpdateTopoRequest;
import com.ruoyi.dj.mqtt.utils.JsonKeyRemover;
import com.ruoyi.dj.mqtt.utils.ParseMethodUtil;
import com.ruoyi.dj.mqtt.utils.PayloadUtil;
import com.ruoyi.system.domain.VideoInfo;
import com.ruoyi.system.service.IDroneService;
import com.ruoyi.system.service.IVideoInfoService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
import java.util.stream.Collectors;
@Slf4j
@Component
public class StatusHandler {
@Resource
private IVideoInfoService videoInfoService;
private final ObjectMapper objectMapper;
@Resource
private DJICloudMQTTClient djiCloudMQTTClient;
private static final String STATUS_REPLY = "sys/product/?/status_reply";
public StatusHandler() {
this.objectMapper = new ObjectMapper();
}
public void handle(String matchedPart, String message) {
CommonResponse commonResponse = ParseMethodUtil.parseMethod(message);
// 检查方法是否为 "update_topo"
if (!"update_topo".equals(commonResponse.getMethod())) {
return;
}
// 处理消息回复
try {
String topic = STATUS_REPLY.replace("?", matchedPart);
JsonNode jsonNode = objectMapper.readTree(message);
// 提取必要的字段
CommonFields commonFields = extractCommonFields(jsonNode, commonResponse.getMethod());
String payload = generatePayloadWithRemovedKeys(commonFields);
// 异步发送消息以防止阻塞
sendAsyncMessage(topic, payload);
} catch (JsonProcessingException e) {
log.error("解析或生成 JSON 失败", e);
}
// 处理更新拓扑请求
try {
UpdateTopoRequest request = objectMapper.readValue(message, UpdateTopoRequest.class);
updateVideoInfoByGateway(matchedPart, request);
} catch (JsonProcessingException e) {
log.error("JSON消息转换错误", e);
}
}
/**
* 提取公共字段
*/
private CommonFields extractCommonFields(JsonNode jsonNode, String method) {
String bid = jsonNode.has("bid") ? jsonNode.get("bid").asText() : null;
String tid = jsonNode.has("tid") ? jsonNode.get("tid").asText() : null;
CommonFields commonFields = new CommonFields();
commonFields.setBid(bid);
commonFields.setTid(tid);
commonFields.setMethod(method);
commonFields.setTimestamp(System.currentTimeMillis());
return commonFields;
}
/**
* 生成去除特定键的负载数据
*/
private String generatePayloadWithRemovedKeys(CommonFields commonFields) throws JsonProcessingException {
Map<String, Object> map = new HashMap<>();
map.put("result", 0);
String payload = PayloadUtil.generatePayload(commonFields, map);
List<String> keysToRemove = Collections.singletonList("gateway");
return JsonKeyRemover.removeKeysFromJson(keysToRemove, payload);
}
/**
* 异步发送消息
*/
private void sendAsyncMessage(String topic, String payload) {
CompletableFuture.runAsync(() -> {
djiCloudMQTTClient.publishMessage(topic, payload);
});
}
/**
* 更新网关的设备信息
*/
private void updateVideoInfoByGateway(String matchedPart, UpdateTopoRequest request) {
List<VideoInfo> videoInfoByGateway = videoInfoService.findVideoInfoByGateway(matchedPart);
VideoInfo videoInfoGateway = videoInfoService.selectVideoByCondition(matchedPart, "机场");
UpdateTopoRequest.DeviceData data = request.getData();
if (data != null && videoInfoGateway != null) {
videoInfoGateway.setDeviceType(data.getDomain() + "-" + data.getType() + "-" + data.getSub_type());
videoInfoService.updateVideoInfo(videoInfoGateway);
}
VideoInfo videoInfoDrone = videoInfoService.selectVideoByCondition(matchedPart, "飞机");
// 检查子设备并更新设备信息
if (data != null && data.getSub_devices() != null && !data.getSub_devices().isEmpty()) {
UpdateTopoRequest.DeviceData.SubDevice subDevice = data.getSub_devices().get(0);
if (subDevice != null && videoInfoDrone != null) {
videoInfoDrone.setDeviceType(subDevice.getDomain() + "-" + subDevice.getType() + "-" + subDevice.getSub_type());
videoInfoDrone.setFlyIndex("0");
videoInfoService.updateVideoInfo(videoInfoDrone);
}
}
}
}

View File

@ -0,0 +1,28 @@
package com.ruoyi.dj.mqtt.runner;
import com.ruoyi.dj.mqtt.service.TaskSchedulingService;
import com.ruoyi.system.domain.ScheduleTask;
import com.ruoyi.system.service.IScheduleTaskService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* @auther 周志雄
* @date 2024/7/24 9:39
*/
@Slf4j
@Component
public class TaskInitializer implements CommandLineRunner {
@Autowired
private TaskSchedulingService taskSchedulingService;
@Override
public void run(String... args) throws Exception {
log.info("初始化所有的定时任务并启动周期性任务更新");
taskSchedulingService.scheduleUpdateTasks();
}
}

View File

@ -0,0 +1,86 @@
package com.ruoyi.dj.mqtt.service;
import com.ruoyi.dj.mqtt.handler.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.HashMap;
import java.util.Map;
import java.util.function.BiConsumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Slf4j
@Component
public class MqttMessageService {
private final Map<String, BiConsumer<String, String>> messageHandlers = new HashMap<>();
@Autowired
private OSDHandler osdHandler;
@Autowired
private StateHandler stateHandler;
@Autowired
private ServicesReplyHandler servicesReplyHandler;
@Autowired
private EventsHandler eventsHandler;
@Autowired
private RequestsHandler requestsHandler;
@Autowired
private StatusHandler statusHandler;
@Autowired
private PropertySetReplyHandler propertySetReplyHandler;
@Autowired
private DrcUpHandler drcUpHandler;
@PostConstruct
public void init() {
initMessageHandlers();
}
/**
* 初始化消息接收的处理函数
*/
private void initMessageHandlers() {
messageHandlers.put("thing/product/+/osd", osdHandler::handle);
messageHandlers.put("thing/product/+/state", stateHandler::handle);
messageHandlers.put("thing/product/+/services_reply", servicesReplyHandler::handle);
messageHandlers.put("thing/product/+/events", eventsHandler::handle);
messageHandlers.put("thing/product/+/requests", requestsHandler::handle);
messageHandlers.put("sys/product/+/status", statusHandler::handle);
messageHandlers.put("thing/product/+/property/set_reply", propertySetReplyHandler::handle);
messageHandlers.put("thing/product/+/drc/up", drcUpHandler::handle);
}
/**
* 调度到具体的处理器
*
* @param topic
* @param message
*/
public void handleMessage(String topic, String message) {
for (Map.Entry<String, BiConsumer<String, String>> entry : messageHandlers.entrySet()) {
String pattern = entry.getKey().replace("+", "([^/]+)");
Pattern regex = Pattern.compile(pattern);
Matcher matcher = regex.matcher(topic);
if (matcher.matches()) {
String matchedPart = matcher.group(1); // 获取匹配 + 的部分
try {
entry.getValue().accept(matchedPart, message);
} catch (Exception e) {
log.error("处理消息时发生错误, 主题: " + topic + ", 消息: " + message, e);
}
return;
}
}
}
}

View File

@ -0,0 +1,137 @@
package com.ruoyi.dj.mqtt.service;
import com.ruoyi.dj.controller.FlightController;
import com.ruoyi.dj.controller.NewTaskController;
import com.ruoyi.dj.mqtt.dto.FlightTaskDto;
import com.ruoyi.dj.mqtt.dto.FlightTaskExecuteDto;
import com.ruoyi.system.domain.Missions;
import com.ruoyi.system.domain.ScheduleTask;
import com.ruoyi.system.domain.VideoInfo;
import com.ruoyi.system.service.IMissionsService;
import com.ruoyi.system.service.IScheduleTaskService;
import com.ruoyi.system.service.IVideoInfoService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.support.CronTrigger;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture;
@Slf4j
@Service
public class TaskSchedulingService {
private ConcurrentHashMap<Long, ScheduledFuture<?>> jobsMap = new ConcurrentHashMap<>();
@Autowired
private TaskScheduler taskScheduler;
@Autowired
private IScheduleTaskService scheduleTaskService;
@Resource
private IVideoInfoService videoInfoService;
@Resource
private FlightController flightController;
@Resource
private IMissionsService missionsService;
@Resource
private NewTaskController newTaskController;
public void scheduleUpdateTasks() {
taskScheduler.scheduleAtFixedRate(this::updateNewTasks, 1000 * 30); // 每10秒执行一次更新任务
}
private void updateNewTasks() {
log.info("更新前的定时任务总数: {}", jobsMap.size());
Missions missions = new Missions();
missions.setType(1L);
List<Missions> missionsList = missionsService.selectMissionsList(missions);
log.info("取消所有现有任务。");
jobsMap.forEach((id, future) -> future.cancel(false));
jobsMap.clear();
log.info("根据最新的数据库条目添加新任务。");
missionsList.forEach(task -> addTaskToScheduler(task.getId(), task));
log.info("更新后的定时任务总数: {}", jobsMap.size());
}
public void addTaskToScheduler(Long taskId, Missions task) {
ScheduledFuture<?> scheduledTask = taskScheduler.schedule(() -> runTask(task), new CronTrigger(buildCronExpression(task)));
jobsMap.put(taskId, scheduledTask);
log.info("已添加任务: {}, 当前任务总数: {}", taskId, jobsMap.size());
}
private String buildCronExpression(Missions task) {
String timer = task.getTimer(); // 获取时间字符串
// 定义时间格式,假设输入格式为 "yyyy-MM-dd HH:mm:ss"
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date parsedDate;
try {
// 解析时间字符串为 Date 对象
parsedDate = dateFormat.parse(timer);
} catch (ParseException e) {
log.error("时间解析错误: {}", timer, e);
return null; // 或者抛出异常,取决于你的业务逻辑
}
// 使用 Calendar 来从 Date 中提取时间字段
Calendar calendar = Calendar.getInstance();
calendar.setTime(parsedDate);
int second = calendar.get(Calendar.SECOND);
int minute = calendar.get(Calendar.MINUTE);
int hour = calendar.get(Calendar.HOUR_OF_DAY);
int day = calendar.get(Calendar.DAY_OF_MONTH);
int month = calendar.get(Calendar.MONTH) + 1; // Calendar.MONTH 返回值从0开始所以+1
int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK) - 1; // Cron 表达式中的星期, 0=周日
// 构建 Cron 表达式
String cronExpression = String.format("%d %d %d %d %d ?",
second, // 秒
minute, // 分
hour, // 时
day, // 日
month // 月
);
log.info("构建的 Cron 表达式: {}", cronExpression);
return cronExpression;
}
private void runTask(Missions task) {
log.info("尝试执行任务: {}", task.getId());
try {
// // TODO 下发定时
task.setTimeStamp(System.currentTimeMillis());
task.setType(1L);
Map<String, String> map = newTaskController.flightTaskPrepare(task);
String flightId = map.get("flightId");
String taskId = map.get("taskId");
//
// // TODO 执行任务
FlightTaskExecuteDto flightTaskExecuteDto = new FlightTaskExecuteDto();
flightTaskExecuteDto.setFlightId(flightId);
flightTaskExecuteDto.setGateway(task.getGateway());
flightTaskExecuteDto.setTaskId(Long.valueOf(taskId));
//
// // 执行定时
newTaskController.executeTask(flightTaskExecuteDto);
log.info("定时飞行任务已执行: {}", task.getId());
} catch (Exception e) {
log.error("执行任务时出错: {}", task.getId(), e);
}
}
}

View File

@ -0,0 +1,62 @@
package com.ruoyi.dj.mqtt.utils;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
public class AESUtil {
private static final String KEY = "isjyauh7ya6di98d";
// 加密
public static String encrypt(String data) {
try {
SecretKeySpec secretKey = new SecretKeySpec(KEY.getBytes("UTF-8"), "AES");
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); // 使用AES/ECB/PKCS5Padding模式
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
byte[] encrypted = cipher.doFinal(data.getBytes("UTF-8"));
return Base64.getUrlEncoder().encodeToString(encrypted); // 使用URL安全的Base64编码
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
} catch (NoSuchPaddingException e) {
throw new RuntimeException(e);
} catch (InvalidKeyException e) {
throw new RuntimeException(e);
} catch (IllegalBlockSizeException e) {
throw new RuntimeException(e);
} catch (BadPaddingException e) {
throw new RuntimeException(e);
}
}
// 解密
public static String decrypt(String encryptedData) {
try {
SecretKeySpec secretKey = new SecretKeySpec(KEY.getBytes("UTF-8"), "AES");
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); // 使用AES/ECB/PKCS5Padding模式
cipher.init(Cipher.DECRYPT_MODE, secretKey);
byte[] decoded = Base64.getUrlDecoder().decode(encryptedData); // 使用URL安全的Base64解码
byte[] original = cipher.doFinal(decoded);
return new String(original, "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
} catch (NoSuchPaddingException e) {
throw new RuntimeException(e);
} catch (InvalidKeyException e) {
throw new RuntimeException(e);
} catch (IllegalBlockSizeException e) {
throw new RuntimeException(e);
} catch (BadPaddingException e) {
throw new RuntimeException(e);
}
}
}

View File

@ -0,0 +1,44 @@
package com.ruoyi.dj.mqtt.utils;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ruoyi.dj.mqtt.common.CommonFields;
import com.ruoyi.dj.mqtt.common.CommonFieldsVo;
import com.ruoyi.dj.mqtt.domain.DrcVo;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class CommonFieldsVoUtil {
private static final ObjectMapper objectMapper = new ObjectMapper();
/**
* 从 JSON 消息中提取 CommonFields 对象
* @param message JSON 消息
* @return 提取出的 CommonFields 对象
*/
public static CommonFieldsVo extractCommonFields(String message) {
try {
JsonNode jsonNode = objectMapper.readTree(message);
String tid = jsonNode.get("tid").asText();
String bid = jsonNode.get("bid").asText();
String method = jsonNode.get("method").asText();
return new CommonFieldsVo(bid, tid, method);
} catch (Exception e) {
log.error("提取 CommonFieldsVo 时发生错误: ", e);
return null;
}
}
public static DrcVo extractDrcVo(String message) {
try {
JsonNode jsonNode = objectMapper.readTree(message);
DrcVo drcVo = new DrcVo();
drcVo.setMethod(jsonNode.get("method").asText());
return drcVo;
} catch (Exception e) {
log.error("提取 CommonFieldsVo 时发生错误: ", e);
return null;
}
}
}

View File

@ -0,0 +1,55 @@
package com.ruoyi.dj.mqtt.utils;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.dj.mqtt.cmd.CmdConstant;
import com.ruoyi.system.domain.GatewayOperationLog;
import com.ruoyi.system.service.IGatewayOperationLogService;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.Date;
/**
* @auther 周志雄
* @date 2024/7/10 17:41
*/
@Component
public class GatewayOperationLogUtil {
@Resource
private IGatewayOperationLogService gatewayOperationLogService;
private static IGatewayOperationLogService staticGatewayOperationLogService;
@PostConstruct
public void init() {
staticGatewayOperationLogService = this.gatewayOperationLogService;
}
public static void generateLog(String gateway, String method, String message, String response) {
ObjectMapper objectMapper = new ObjectMapper();
String resultMsg = "成功";
try {
JsonNode rootNode = objectMapper.readTree(response);
int result = rootNode.path("data").path("result").asInt();
if (result != 0) {
resultMsg = "失败";
}
} catch (Exception e) {
e.printStackTrace();
}
GatewayOperationLog gatewayOperationLog = new GatewayOperationLog();
gatewayOperationLog.setGatewayId(gateway);
gatewayOperationLog.setOperationCommand(method);
gatewayOperationLog.setSendParams(message);
gatewayOperationLog.setExecutionResponse(response);
gatewayOperationLog.setExecutionResult(resultMsg);
gatewayOperationLog.setOperationTime(new Date());
staticGatewayOperationLogService.insertGatewayOperationLog(gatewayOperationLog);
}
}

View File

@ -0,0 +1,40 @@
package com.ruoyi.dj.mqtt.utils;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.dj.mqtt.error.ErrorCodeMap;
import java.util.Map;
/**
* @auther 周志雄
* @date 2024/7/9 16:01
*/
public class HandleResultUtil {
public static AjaxResult handleResult(String json, Map<String, Object> data, String message) {
try {
ObjectMapper objectMapper = new ObjectMapper();
// 将字符串解析为 JSON 对象
ObjectNode jsonNode = null;
jsonNode = (ObjectNode) objectMapper.readTree(json);
JsonNode resultNode = jsonNode.path("data").path("result");
Integer result = Integer.valueOf(resultNode.asText());
// 根据 result 值判断是否成功
if (result != 0) {
// 官方的 Bug、网络异常连接断开、其实是正常的
if (result == 514303) {
return AjaxResult.success().put("data", data);
}
String errorMessage = ErrorCodeMap.errorCodeMap.getOrDefault(result, "未知错误");
return AjaxResult.error(errorMessage);
} else {
return AjaxResult.success(message).put("data", data);
}
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
}

View File

@ -0,0 +1,71 @@
package com.ruoyi.dj.mqtt.utils;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
public class JsonKeyRemover {
/**
* 从 JSON 字符串中移除指定的键(包括嵌套路径键)
*/
public static String removeKeysFromJson(List<String> keysToRemove, String jsonString) throws JsonProcessingException {
ObjectMapper mapper = new ObjectMapper();
JsonNode rootNode = mapper.readTree(jsonString);
// 递归移除指定的键
removeKeysRecursive(rootNode, keysToRemove);
return mapper.writeValueAsString(rootNode);
}
/**
* 递归地从 JsonNode 中移除指定的键
*/
private static void removeKeysRecursive(JsonNode node, List<String> keysToRemove) {
if (node.isObject()) {
ObjectNode objectNode = (ObjectNode) node;
// 在当前级别移除键
for (String key : keysToRemove) {
if (objectNode.has(key)) {
objectNode.remove(key);
}
}
// 遍历剩余的字段
Iterator<Map.Entry<String, JsonNode>> fields = objectNode.fields();
while (fields.hasNext()) {
Map.Entry<String, JsonNode> entry = fields.next();
// 递归处理子节点
removeKeysRecursive(entry.getValue(), keysToRemove);
}
} else if (node.isArray()) {
// 如果节点是数组,处理每个元素
for (JsonNode arrayElement : node) {
removeKeysRecursive(arrayElement, keysToRemove);
}
}
}
/**
* 为 JSON 字符串的最外层添加一个新的字段
*/
public static String addKeyToRoot(String jsonString, String keyToAdd, Object valueToAdd) throws JsonProcessingException {
ObjectMapper mapper = new ObjectMapper();
JsonNode rootNode = mapper.readTree(jsonString);
// 将传入的 Object 转换为 JsonNode
JsonNode valueNode = mapper.valueToTree(valueToAdd);
if (rootNode.isObject()) {
((ObjectNode) rootNode).set(keyToAdd, valueNode);
}
return mapper.writeValueAsString(rootNode);
}
}

View File

@ -0,0 +1,22 @@
package com.ruoyi.dj.mqtt.utils;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class Md5Util {
public static String getMD5(String input) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] digest = md.digest(input.getBytes());
StringBuilder sb = new StringBuilder();
for (byte b : digest) {
sb.append(String.format("%02x", b & 0xff));
}
return sb.toString();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return null;
}
}

View File

@ -0,0 +1,27 @@
package com.ruoyi.dj.mqtt.utils;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ruoyi.dj.mqtt.common.CommonResponse;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.util.Map;
/**
* @auther 周志雄
* @date 2024/7/22 10:08
*/
@Slf4j
public class ParseMethodUtil {
private static final ObjectMapper objectMapper = new ObjectMapper();
public static CommonResponse parseMethod(String json) {
try {
CommonResponse response = objectMapper.readValue(json, CommonResponse.class);
return response;
} catch (Exception e) {
log.error("Failed to parse JSON", e);
return null;
}
}
}

View File

@ -0,0 +1,64 @@
package com.ruoyi.dj.mqtt.utils;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ruoyi.dj.mqtt.common.CommonFields;
import com.ruoyi.dj.mqtt.common.CommonFieldsVo;
import java.util.HashMap;
import java.util.Map;
public class PayloadUtil {
private static final ObjectMapper objectMapper = new ObjectMapper();
public static String generatePayload(CommonFieldsVo commonFieldsVo, Object data) {
Map<String, Object> payloadMap = new HashMap<>();
payloadMap.put("tid", commonFieldsVo.getTid());
payloadMap.put("bid", commonFieldsVo.getBid());
if(commonFieldsVo.getMethod()!=null) {
payloadMap.put("method", commonFieldsVo.getMethod());
}
payloadMap.put("gateway", commonFieldsVo.getGateway());
payloadMap.put("data", data);
try {
return objectMapper.writeValueAsString(payloadMap);
} catch (JsonProcessingException e) {
e.printStackTrace();
return null;
}
}
// 根据 json 数据 message 解析 data 中的 seq 字段
public static Long getSeqFromJson(String json) {
try {
// 将 JSON 字符串转换为 Map 对象
Map<String, Object> jsonMap = objectMapper.readValue(json, Map.class);
// 获取 data 字段的值
Map<String, Object> dataMap = (Map<String, Object>) jsonMap.get("data");
// 获取 seq 字段的值
return (Long) dataMap.get("seq");
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public static String generatePayload(CommonFields commonFields, Object data) {
Map<String, Object> payloadMap = new HashMap<>();
payloadMap.put("tid", commonFields.getTid());
payloadMap.put("bid", commonFields.getBid());
payloadMap.put("method", commonFields.getMethod());
payloadMap.put("gateway", commonFields.getGateway());
payloadMap.put("timestamp", commonFields.getTimestamp());
payloadMap.put("data", data);
try {
return objectMapper.writeValueAsString(payloadMap);
} catch (JsonProcessingException e) {
e.printStackTrace();
return null;
}
}
}

View File

@ -0,0 +1,656 @@
package com.ruoyi.dj.mqtt.utils;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ruoyi.dj.mqtt.config.MinioConfig;
import io.minio.*;
import io.minio.credentials.AssumeRoleProvider;
import io.minio.credentials.Credentials;
import io.minio.credentials.StaticProvider;
import io.minio.http.Method;
import io.minio.messages.Item;
import lombok.extern.slf4j.Slf4j;
import okhttp3.OkHttpClient;
import org.apache.commons.codec.digest.DigestUtils;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.time.ZonedDateTime;
import java.util.*;
@Slf4j
public class StorageConfigUtil {
/**
* 获取临时凭证
*
* @param sn 设备序列号
* @return 临时凭证
* @throws Exception 异常信息
*/
// private static Credentials getTemporaryCredentials(String sn) throws Exception {
// OkHttpClient customHttpClient = new OkHttpClient();
// AssumeRoleProvider assumeRoleProvider = new AssumeRoleProvider(
// "http://zmkg.cqet.top:9999",
// "admin",
// "12345678",
// 3600,
// null,
// "cn-chengdu",
// "arn:aws:s3:::drone-?/*".replace("?", sn),
// "anysession",
// null,
// customHttpClient
// );
// return assumeRoleProvider.fetch();
// }
private static Credentials getTemporaryCredentials(String sn) throws Exception {
OkHttpClient customHttpClient = new OkHttpClient();
AssumeRoleProvider assumeRoleProvider = new AssumeRoleProvider(
"http://192.168.110.65:9000",
"minioadmin",
"minioadmin",
3600,
null,
"cn-chengdu",
"arn:aws:s3:::drone-?/*".replace("?", sn),
"anysession",
null,
customHttpClient
);
return assumeRoleProvider.fetch();
}
/**
* 创建使用临时凭证的 MinioClient
*
* @param credentials 临时凭证
* @return MinioClient 实例
*/
private static MinioClient getMinioClientWithTemporaryCredentials(Credentials credentials) {
return MinioClient.builder()
.endpoint(MinioConfig.ENDPOINT)
.credentialsProvider(new StaticProvider(
credentials.accessKey(),
credentials.secretKey(),
credentials.sessionToken()))
.build();
}
/**
* 上传文件到 Minio 指定桶和文件夹
*/
public static void uploadFile(String sn, String folderName, File file) {
String bucketName = "drone" + sn.toLowerCase().replace("_", "").replace("-", "");
InputStream fileInputStream = null;
try {
// 获取临时凭证
Credentials credentials = getTemporaryCredentials(sn);
log.info("临时凭证创建成功: AccessKeyID: {}", credentials.accessKey());
// 创建使用临时凭证的 MinioClient
MinioClient minioClient = getMinioClientWithTemporaryCredentials(credentials);
// 检查桶是否存在,如果不存在则创建
if (!minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build())) {
minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
log.info("已创建桶: {}", bucketName);
} else {
log.info("桶已存在: {}", bucketName);
}
// 获取文件输入流
fileInputStream = new FileInputStream(file);
// 构建文件路径 (对象键,文件夹 + 文件名)
String objectName = folderName + "/" + file.getName();
// 上传文件到 Minio
minioClient.putObject(PutObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.stream(fileInputStream, file.length(), -1)
.contentType("application/octet-stream") // 你可以根据需要修改文件类型
.build());
log.info("文件上传成功,文件名: {}", file.getName());
} catch (Exception e) {
log.error("文件上传失败: ", e);
} finally {
try {
// 确保关闭文件流
if (fileInputStream != null) {
fileInputStream.close();
}
} catch (IOException e) {
log.error("关闭文件输入流失败: ", e);
}
}
}
/**
* 创建存储配置
*
* @param sn 设备序列号
* @return 配置 Map
*/
public static Map<String, Object> createStorageConfig(String sn) {
String bucketName = "drone" + sn.toLowerCase().replace("_", "").replace("-", "");
sn = sn.toLowerCase();
try {
// 获取临时凭证
Credentials credentials = getTemporaryCredentials(sn);
log.info("临时凭证创建成功: AccessKeyID: {}", credentials.accessKey());
// 创建使用临时凭证的 MinioClient
MinioClient minioClient = getMinioClientWithTemporaryCredentials(credentials);
// 检查桶是否存在,如果不存在则创建
if (!minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build())) {
minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
log.info("已创建桶: {}", bucketName);
} else {
log.info("桶已存在: {}", bucketName);
}
// 返回配置 Map
return createStorageConfigMap(bucketName, credentials, MinioConfig.ENDPOINT, "minio", MinioConfig.REGION, "/", MinioConfig.DURATION_SECONDS);
} catch (Exception e) {
log.error("创建临时凭证出错: ", e);
}
return null;
}
/**
* 创建存储配置映射
*
* @param bucketName 桶名称
* @param credentials 凭证
* @param endpoint 端点
* @param provider 提供商
* @param region 地区
* @param objectKeyPrefix 对象键前缀
* @param durationSeconds 有效期(秒)
* @return 配置映射
*/
private static Map<String, Object> createStorageConfigMap(String bucketName, Credentials credentials, String endpoint, String provider, String region, String objectKeyPrefix, int durationSeconds) {
try {
Map<String, Object> credentialsMap = new HashMap<>();
credentialsMap.put("access_key_id", credentials.accessKey());
credentialsMap.put("access_key_secret", credentials.secretKey());
credentialsMap.put("expire", durationSeconds);
credentialsMap.put("security_token", credentials.sessionToken());
Map<String, Object> outputMap = new HashMap<>();
outputMap.put("bucket", bucketName);
outputMap.put("credentials", credentialsMap);
outputMap.put("endpoint", endpoint);
outputMap.put("object_key_prefix", objectKeyPrefix);
outputMap.put("provider", provider);
outputMap.put("region", region);
Map<String, Object> responseMap = new HashMap<>();
responseMap.put("output", outputMap);
responseMap.put("result", 0);
return responseMap;
} catch (Exception e) {
log.error("创建存储配置映射出错: ", e);
}
return null;
}
/**
* 获取 MinioClient 实例
*
* @return MinioClient
*/
public static MinioClient getMinioClient() {
return MinioClient.builder()
.endpoint(MinioConfig.ENDPOINT)
.credentials(MinioConfig.ACCESS_KEY, MinioConfig.SECRET_KEY)
.build();
}
/**
* 获取指定目录下的所有文件信息
*
* @param sn 设备序列号
* @param taskId 任务ID
* @return 文件信息列表包括URL和文件名
*/
public static List<Map<String, String>> getFiles(String sn, String taskId) {
sn = sn.toLowerCase();
String bucketName = "drone" + sn.replace("_", "").replace("-", "");
String objectKeyPrefix = DigestUtils.md5Hex(sn) + "/" + taskId + "/";
try {
// 获取临时凭证
Credentials credentials = getTemporaryCredentials(sn);
// 创建使用临时凭证的 MinioClient
MinioClient minioClient = getMinioClientWithTemporaryCredentials(credentials);
// 列出指定前缀的对象
Iterable<Result<Item>> results = minioClient.listObjects(
ListObjectsArgs.builder().bucket(bucketName).prefix(objectKeyPrefix).recursive(true).build());
List<Map<String, String>> fileList = new ArrayList<>();
for (Result<Item> result : results) {
Item item = result.get();
if (!item.isDir()) {
String objectName = item.objectName();
String fileUrl = minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
.method(Method.GET)
.bucket(bucketName)
.object(objectName)
.build());
Map<String, String> fileInfo = new HashMap<>();
fileInfo.put("filename", objectName);
fileInfo.put("url", fileUrl);
fileList.add(fileInfo);
}
}
return fileList;
} catch (Exception e) {
log.error("获取文件列表出错: ", e);
}
return null;
}
public static String listDirectoryContents(String bucketName, String directoryName) throws Exception {
MinioClient minioClient = getMinioClient();
Map<String, Object> directoryStructure = new HashMap<>();
// 确保目录名以 "/" 结尾
if (directoryName == null || directoryName.isEmpty()) {
directoryName = "";
} else if (!directoryName.endsWith("/")) {
directoryName += "/";
}
// 获取指定目录下的直接子项(不递归)
Iterable<Result<Item>> immediateItems = minioClient.listObjects(
ListObjectsArgs.builder().bucket(bucketName).prefix(directoryName).recursive(false).build());
// 日期和大小格式化器
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
DecimalFormat sizeFormat = new DecimalFormat("#.##");
// 遍历直接子项
for (Result<Item> result : immediateItems) {
Item item = result.get();
String objectName = item.objectName();
Map<String, Object> details = new HashMap<>();
try {
if (!item.isDir()) {
// 对于文件,直接使用 Item 中的信息
ZonedDateTime lastModified = item.lastModified();
Date lastModifiedDate = Date.from(lastModified.toInstant());
details.put("creation_time", dateFormat.format(lastModifiedDate));
long sizeInBytes = item.size();
double sizeInMB = sizeInBytes / (1024.0 * 1024.0);
details.put("size", sizeFormat.format(sizeInMB) + " MB");
details.put("length", sizeInBytes);
String downloadUrl = minioClient.getPresignedObjectUrl(
GetPresignedObjectUrlArgs.builder()
.method(Method.GET)
.bucket(bucketName)
.object(objectName)
.expiry(24 * 60 * 60)
.build()
);
details.put("download_url", downloadUrl);
} else {
// 对于文件夹size 设置为 "N/A"
details.put("size", "N/A");
// 可以根据需要计算文件夹的总大小和长度
long totalSizeInBytes = calculateDirectorySize(minioClient, bucketName, objectName);
details.put("length", totalSizeInBytes);
// 获取文件夹的最早创建时间
Date directoryCreationTime = getDirectoryCreationTime(minioClient, bucketName, objectName);
details.put("creation_time", dateFormat.format(directoryCreationTime));
// 文件夹没有下载链接
details.put("download_url", "N/A");
}
} catch (Exception e) {
details.put("creation_time", "N/A");
details.put("size", "N/A");
details.put("length", 0);
details.put("download_url", "N/A");
}
directoryStructure.put(objectName, details);
}
directoryStructure = sortMap(directoryStructure);
ObjectMapper objectMapper = new ObjectMapper();
return objectMapper.writeValueAsString(directoryStructure);
}
// 确保 "视频录制/" 在最前面
public static Map<String, Object> sortMap(Map<String, Object> data) {
// 创建一个新的 LinkedHashMap 来保持排序
List<Map.Entry<String, Object>> entries = new ArrayList<>(data.entrySet());
// 排序:将 "视频录制/" 移到最前面
entries.sort((entryA, entryB) -> {
if ("视频录制/".equals(entryA.getKey())) return -1; // "视频录制/" 排到最前面
if ("视频录制/".equals(entryB.getKey())) return 1;
return 0; // 其他项保持原有顺序
});
// 将排序后的 entries 放入 LinkedHashMap 中,保持顺序
Map<String, Object> sortedMap = new LinkedHashMap<>();
for (Map.Entry<String, Object> entry : entries) {
sortedMap.put(entry.getKey(), entry.getValue());
}
return sortedMap;
}
/**
* 获取目录的创建时间
*
* @param minioClient Minio 客户端
* @param bucketName 桶名称
* @param objectName 对象名称
* @return 目录创建时间
* @throws Exception 异常信息
*/
private static Date getDirectoryCreationTime(MinioClient minioClient, String bucketName, String objectName) throws Exception {
Iterable<Result<Item>> subResults = minioClient.listObjects(
ListObjectsArgs.builder().bucket(bucketName).prefix(objectName).recursive(true).build());
Date earliestDate = null;
for (Result<Item> subResult : subResults) {
Item subItem = subResult.get();
if (!subItem.isDir()) {
ZonedDateTime lastModified = subItem.lastModified();
Date itemDate = Date.from(lastModified.toInstant());
if (earliestDate == null || itemDate.before(earliestDate)) {
earliestDate = itemDate;
}
}
}
// 如果没有找到文件,则使用当前时间
return earliestDate != null ? earliestDate : new Date();
}
/**
* 递归计算文件夹的总大小
*
* @param minioClient Minio 客户端
* @param bucketName 桶名称
* @param directoryPath 目录路径
* @return 总大小(字节)
* @throws Exception 异常信息
*/
private static long calculateDirectorySize(MinioClient minioClient, String bucketName, String directoryPath) throws Exception {
long totalSize = 0;
Iterable<Result<Item>> results = minioClient.listObjects(
ListObjectsArgs.builder().bucket(bucketName).prefix(directoryPath).recursive(true).build());
for (Result<Item> result : results) {
Item item = result.get();
if (!item.isDir()) {
totalSize += item.size();
}
}
return totalSize;
}
/**
* 获取桶下所有图片和视频的统计信息
*
* @param bucketName 桶名称
* @return 统计结果
* @throws Exception 异常信息
*/
public static Map<String, Object> getMediaStatistics(String bucketName) throws Exception {
MinioClient minioClient = getMinioClient();
Map<String, Object> statistics = new HashMap<>();
long totalImageSize = 0;
int totalImageCount = 0;
long totalVideoSize = 0;
int totalVideoCount = 0;
Iterable<Result<Item>> results = minioClient.listObjects(
ListObjectsArgs.builder().bucket(bucketName).recursive(true).build());
for (Result<Item> result : results) {
Item item = result.get();
String objectName = item.objectName();
if (!item.isDir() && isMediaFile(objectName)) {
try {
// 直接使用 Item 对象的 size() 方法获取文件大小,避免调用 statObject
long fileSize = item.size();
if (isImageFile(objectName)) {
totalImageSize += fileSize;
totalImageCount++;
} else if (isVideoFile(objectName)) {
totalVideoSize += fileSize;
totalVideoCount++;
}
} catch (Exception e) {
log.error("统计媒体文件出错: ", e);
}
}
}
DecimalFormat df = new DecimalFormat("#.##");
statistics.put("totalImageSize", df.format(totalImageSize / (1024.0 * 1024.0)) + "MB");
statistics.put("totalImageCount", totalImageCount);
statistics.put("totalVideoSize", df.format(totalVideoSize / (1024.0 * 1024.0)) + "MB");
statistics.put("totalVideoCount", totalVideoCount);
return statistics;
}
/**
* 判断文件是否是图片
*
* @param fileName 文件名
* @return 是否是图片
*/
private static boolean isImageFile(String fileName) {
String[] imageExtensions = {".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tiff"};
for (String ext : imageExtensions) {
if (fileName.toLowerCase().endsWith(ext)) {
return true;
}
}
return false;
}
/**
* 判断文件是否是视频
*
* @param fileName 文件名
* @return 是否是视频
*/
private static boolean isVideoFile(String fileName) {
String[] videoExtensions = {".mp4", ".avi", ".mov", ".mkv", ".flv"};
for (String ext : videoExtensions) {
if (fileName.toLowerCase().endsWith(ext)) {
return true;
}
}
return false;
}
/**
* 判断文件是否是媒体文件(图片或视频)
*
* @param fileName 文件名
* @return 是否是媒体文件
*/
private static boolean isMediaFile(String fileName) {
return isImageFile(fileName) || isVideoFile(fileName);
}
/**
* 获取指定路径下的文件 URL 列表
*
* @param bucketName 桶名称
* @param itemPath 路径
* @return URL 列表
*/
public static List<String> getUrlList(String bucketName, String itemPath) {
List<String> urlList = new ArrayList<>();
try {
MinioClient minioClient = getMinioClient();
if (itemPath == null || itemPath.isEmpty()) {
itemPath = "";
} else if (!itemPath.endsWith("/")) {
itemPath += "/";
}
Iterable<Result<Item>> results = minioClient.listObjects(
ListObjectsArgs.builder().bucket(bucketName).prefix(itemPath).recursive(true).build());
for (Result<Item> result : results) {
Item item = result.get();
if (!item.isDir()) {
String objectName = item.objectName();
String url = minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
.method(Method.GET)
.bucket(bucketName)
.object(objectName)
.expiry(24 * 60 * 60)
.build());
urlList.add(url);
}
}
} catch (Exception e) {
log.error("获取 URL 列表出错: ", e);
}
return urlList;
}
// 删除指定的文件或目录
public static void deleteFilesOrDirectories(List<String> itemPaths, String gateway) {
MinioClient minioClient = getMinioClient();
for (String itemPath : itemPaths) {
try {
// 判断是文件还是目录
if (itemPath.endsWith("/")) {
// 目录,递归删除其中的所有文件
deleteDirectoryContents(minioClient, gateway, itemPath);
} else {
// 文件,直接删除
deleteFile(minioClient, gateway, itemPath);
}
} catch (Exception e) {
log.error("删除文件或目录失败: ", e);
}
}
}
// 删除单个文件
private static void deleteFile(MinioClient minioClient, String gateway, String itemPath) throws Exception {
String bucketName = "drone" + gateway.toLowerCase().replace("_", "").replace("-", "");
minioClient.removeObject(RemoveObjectArgs.builder().bucket(bucketName).object(itemPath).build());
log.info("文件删除成功: {}", itemPath);
}
// 删除目录中的所有文件
private static void deleteDirectoryContents(MinioClient minioClient, String gateway, String directoryPath) throws Exception {
String bucketName = "drone" + gateway.toLowerCase().replace("_", "").replace("-", "");
// 获取指定目录下的所有文件
Iterable<Result<Item>> results = minioClient.listObjects(
ListObjectsArgs.builder().bucket(bucketName).prefix(directoryPath).recursive(true).build());
// 遍历并删除文件
for (Result<Item> result : results) {
Item item = result.get();
if (!item.isDir()) {
String objectName = item.objectName();
minioClient.removeObject(RemoveObjectArgs.builder().bucket(bucketName).object(objectName).build());
log.info("删除文件: {}", objectName);
}
}
// 删除目录本身
minioClient.removeObject(RemoveObjectArgs.builder().bucket(bucketName).object(directoryPath).build());
log.info("目录删除成功: {}", directoryPath);
}
// 删除指定的文件或目录
public static void deleteAiFilesOrDirectories(List<String> itemPaths, String gateway) {
MinioClient minioClient = getMinioClient();
for (String itemPath : itemPaths) {
try {
// 判断是文件还是目录
if (itemPath.endsWith("/")) {
// 目录,递归删除其中的所有文件
deleteAiDirectoryContents(minioClient, gateway, itemPath);
} else {
// 文件,直接删除
deleteAiFile(minioClient, gateway, itemPath);
}
} catch (Exception e) {
log.error("删除文件或目录失败: ", e);
}
}
}
// 删除单个文件
private static void deleteAiFile(MinioClient minioClient, String gateway, String itemPath) throws Exception {
String bucketName = "ai" + gateway.toLowerCase().replace("_", "").replace("-", "");
minioClient.removeObject(RemoveObjectArgs.builder().bucket(bucketName).object(itemPath).build());
log.info("文件删除成功: {}", itemPath);
}
// 删除目录中的所有文件
private static void deleteAiDirectoryContents(MinioClient minioClient, String gateway, String directoryPath) throws Exception {
String bucketName = "ai" + gateway.toLowerCase().replace("_", "").replace("-", "");
// 获取指定目录下的所有文件
Iterable<Result<Item>> results = minioClient.listObjects(
ListObjectsArgs.builder().bucket(bucketName).prefix(directoryPath).recursive(true).build());
// 遍历并删除文件
for (Result<Item> result : results) {
Item item = result.get();
if (!item.isDir()) {
String objectName = item.objectName();
minioClient.removeObject(RemoveObjectArgs.builder().bucket(bucketName).object(objectName).build());
log.info("删除文件: {}", objectName);
}
}
// 删除目录本身
minioClient.removeObject(RemoveObjectArgs.builder().bucket(bucketName).object(directoryPath).build());
log.info("目录删除成功: {}", directoryPath);
}
}

View File

@ -0,0 +1,123 @@
package com.ruoyi.dj.websocket.config;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import java.util.concurrent.ConcurrentHashMap;
import java.util.Map;
/**
* 自定义WebSocket处理类
*/
@Slf4j
@Component
public class CustomWebSocketHandler extends TextWebSocketHandler {
/**
* 存储所有WebSocket会话以确保线程安全
*/
private Map<String, WebSocketSession> sessions = new ConcurrentHashMap<>();
/**
* Redis中存储会话ID与客户端选择的gateway的映射关系的前缀
*/
private static final String SESSION_GATEWAY_PREFIX = "websocket:session:gateway:";
private ObjectMapper objectMapper = new ObjectMapper();
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 在连接建立后调用,将会话添加到会话映射中
*/
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
sessions.put(session.getId(), session);
log.info("会话已建立sessionId={}", session.getId());
}
/**
* 处理接收到客户端的文本消息
*/
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
String payload = message.getPayload();
JsonNode jsonNode = objectMapper.readTree(payload);
String clientID = jsonNode.has("clientID") ? jsonNode.get("clientID").asText() : null;
String gateway = jsonNode.has("gateway") ? jsonNode.get("gateway").asText() : null;
if (clientID != null && gateway != null) {
// 使用Redis存储会话ID与gateway的映射关系
String key = SESSION_GATEWAY_PREFIX + session.getId();
redisTemplate.opsForValue().set(key, gateway);
log.info("会话ID={} 已选择网关={}", session.getId(), gateway);
}
}
/**
* 向所有选择了特定 Gateway 的客户端发送消息
*/
public void sendMessageToAll(String message) {
try {
JsonNode jsonNode = objectMapper.readTree(message);
String gateway = jsonNode.has("gateway") ? jsonNode.get("gateway").asText() : null;
if (gateway == null) {
log.warn("消息中缺少gateway字段无法发送");
return;
}
// 创建文本消息对象
TextMessage textMessage = new TextMessage(message);
// 遍历所有会话尝试发送消息给选择了对应gateway的会话
sessions.values().forEach(session -> {
try {
if (session.isOpen()) {
String key = SESSION_GATEWAY_PREFIX + session.getId();
String sessionGateway = redisTemplate.opsForValue().get(key);
if (gateway.equals(sessionGateway)) {
session.sendMessage(textMessage);
}
}
} catch (Exception e) {
log.error("发送消息给sessionId={}时发生异常", session.getId(), e);
}
});
} catch (Exception e) {
log.error("解析消息时发生异常", e);
}
}
/**
* 在连接关闭后调用,移除会话及其映射关系
*/
@Override
public void afterConnectionClosed(WebSocketSession session, org.springframework.web.socket.CloseStatus status) throws Exception {
sessions.remove(session.getId());
// 从Redis中删除会话与gateway的映射
String key = SESSION_GATEWAY_PREFIX + session.getId();
redisTemplate.delete(key);
log.info("会话已关闭sessionId={}", session.getId());
}
/**
* 处理传输错误
*/
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
sessions.remove(session.getId());
// 从Redis中删除会话与gateway的映射
String key = SESSION_GATEWAY_PREFIX + session.getId();
redisTemplate.delete(key);
log.error("会话传输错误sessionId={}", session.getId(), exception);
}
}

View File

@ -0,0 +1,28 @@
package com.ruoyi.dj.websocket.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Slf4j
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
private static final Logger logger = LoggerFactory.getLogger(WebSocketConfig.class);
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(webSocketHandler(), "/websocket").setAllowedOrigins("*");
logger.info("WebSocket 服务器启动");
}
@Bean
public CustomWebSocketHandler webSocketHandler() {
return new CustomWebSocketHandler();
}
}

View File

@ -0,0 +1,14 @@
package com.ruoyi.dj.websocket.controller;
import org.springframework.web.bind.annotation.*;
/**
* @auther 周志雄
* @date 2024/8/7 15:36
*/
@RestController
@RequestMapping("/dj/gateways")
public class GatewayController {
}

View File

@ -0,0 +1,6 @@
package com.ruoyi.dj.websocket.core;
public interface MessageHandler {
void handleMessage(String message);
String getType();
}

View File

@ -0,0 +1,27 @@
package com.ruoyi.dj.websocket.handler;
import com.ruoyi.dj.websocket.config.CustomWebSocketHandler;
import com.ruoyi.dj.websocket.core.MessageHandler;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* @auther 周志雄
* @date 2024/9/25 14:13
*/
@Component
public class CameraPhotoTakeProgressHandler implements MessageHandler {
@Resource
private CustomWebSocketHandler webSocketHandler;
@Override
public void handleMessage(String message) {
webSocketHandler.sendMessageToAll(message);
}
@Override
public String getType() {
return "camera_photo_take_progress";
}
}

View File

@ -0,0 +1,26 @@
package com.ruoyi.dj.websocket.handler;
import com.ruoyi.dj.websocket.config.CustomWebSocketHandler;
import com.ruoyi.dj.websocket.core.MessageHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @auther 周志雄
* @date 2024/7/5 17:13
*/
@Component
public class ChargeCloseHandler implements MessageHandler {
@Autowired
private CustomWebSocketHandler webSocketHandler;
@Override
public void handleMessage(String message) {
webSocketHandler.sendMessageToAll(message);
}
@Override
public String getType() {
return "charge_close";
}
}

View File

@ -0,0 +1,26 @@
package com.ruoyi.dj.websocket.handler;
import com.ruoyi.dj.websocket.config.CustomWebSocketHandler;
import com.ruoyi.dj.websocket.core.MessageHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @auther 周志雄
* @date 2024/7/5 17:13
*/
@Component
public class ChargeOpenHandler implements MessageHandler {
@Autowired
private CustomWebSocketHandler webSocketHandler;
@Override
public void handleMessage(String message) {
webSocketHandler.sendMessageToAll(message);
}
@Override
public String getType() {
return "charge_open";
}
}

View File

@ -0,0 +1,26 @@
package com.ruoyi.dj.websocket.handler;
import com.ruoyi.dj.websocket.config.CustomWebSocketHandler;
import com.ruoyi.dj.websocket.core.MessageHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @auther 周志雄
* @date 2024/7/5 17:13
*/
@Component
public class CoverCloseHandler implements MessageHandler {
@Autowired
private CustomWebSocketHandler webSocketHandler;
@Override
public void handleMessage(String message) {
webSocketHandler.sendMessageToAll(message);
}
@Override
public String getType() {
return "cover_close";
}
}

View File

@ -0,0 +1,26 @@
package com.ruoyi.dj.websocket.handler;
import com.ruoyi.dj.websocket.config.CustomWebSocketHandler;
import com.ruoyi.dj.websocket.core.MessageHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @auther 周志雄
* @date 2024/7/5 17:13
*/
@Component
public class CoverOpenHandler implements MessageHandler {
@Autowired
private CustomWebSocketHandler webSocketHandler;
@Override
public void handleMessage(String message) {
webSocketHandler.sendMessageToAll(message);
}
@Override
public String getType() {
return "cover_open";
}
}

View File

@ -0,0 +1,26 @@
package com.ruoyi.dj.websocket.handler;
import com.ruoyi.dj.websocket.config.CustomWebSocketHandler;
import com.ruoyi.dj.websocket.core.MessageHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @auther 周志雄
* @date 2024/7/5 17:13
*/
@Component
public class DeviceFormatHandler implements MessageHandler {
@Autowired
private CustomWebSocketHandler webSocketHandler;
@Override
public void handleMessage(String message) {
webSocketHandler.sendMessageToAll(message);
}
@Override
public String getType() {
return "device_format";
}
}

View File

@ -0,0 +1,27 @@
package com.ruoyi.dj.websocket.handler;
import com.ruoyi.dj.websocket.config.CustomWebSocketHandler;
import com.ruoyi.dj.websocket.core.MessageHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @auther 周志雄
* @date 2024/7/5 17:11
*/
@Component
public class DeviceRebootHandler implements MessageHandler {
@Autowired
private CustomWebSocketHandler webSocketHandler;
@Override
public void handleMessage(String message) {
webSocketHandler.sendMessageToAll(message);
}
@Override
public String getType() {
return "device_reboot";
}
}

View File

@ -0,0 +1,26 @@
package com.ruoyi.dj.websocket.handler;
import com.ruoyi.dj.websocket.config.CustomWebSocketHandler;
import com.ruoyi.dj.websocket.core.MessageHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @auther 周志雄
* @date 2024/7/5 16:55
*/
@Component
public class DroneCloseHandler implements MessageHandler {
@Autowired
private CustomWebSocketHandler webSocketHandler;
@Override
public void handleMessage(String message) {
webSocketHandler.sendMessageToAll(message);
}
@Override
public String getType() {
return "drone_close";
}
}

View File

@ -0,0 +1,26 @@
package com.ruoyi.dj.websocket.handler;
import com.ruoyi.dj.websocket.config.CustomWebSocketHandler;
import com.ruoyi.dj.websocket.core.MessageHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @auther 周志雄
* @date 2024/7/5 17:13
*/
@Component
public class DroneFormatHandler implements MessageHandler {
@Autowired
private CustomWebSocketHandler webSocketHandler;
@Override
public void handleMessage(String message) {
webSocketHandler.sendMessageToAll(message);
}
@Override
public String getType() {
return "drone_format";
}
}

View File

@ -0,0 +1,26 @@
package com.ruoyi.dj.websocket.handler;
import com.ruoyi.dj.websocket.config.CustomWebSocketHandler;
import com.ruoyi.dj.websocket.core.MessageHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @auther 周志雄
* @date 2024/7/5 16:55
*/
@Component
public class DroneOpenHandler implements MessageHandler {
@Autowired
private CustomWebSocketHandler webSocketHandler;
@Override
public void handleMessage(String message) {
webSocketHandler.sendMessageToAll(message);
}
@Override
public String getType() {
return "drone_open";
}
}

View File

@ -0,0 +1,26 @@
package com.ruoyi.dj.websocket.handler;
import com.ruoyi.dj.websocket.config.CustomWebSocketHandler;
import com.ruoyi.dj.websocket.core.MessageHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @auther 周志雄
* @date 2024/7/5 17:13
*/
@Component
public class EsimActivateHandler implements MessageHandler {
@Autowired
private CustomWebSocketHandler webSocketHandler;
@Override
public void handleMessage(String message) {
webSocketHandler.sendMessageToAll(message);
}
@Override
public String getType() {
return "esim_activate";
}
}

View File

@ -0,0 +1,26 @@
package com.ruoyi.dj.websocket.handler;
import com.ruoyi.dj.websocket.config.CustomWebSocketHandler;
import com.ruoyi.dj.websocket.core.MessageHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @auther 周志雄
* @date 2024/7/17 14:06
*/
@Component
public class FlightTaskProgressHandler implements MessageHandler {
@Autowired
private CustomWebSocketHandler webSocketHandler;
@Override
public void handleMessage(String message) {
webSocketHandler.sendMessageToAll(message);
}
@Override
public String getType() {
return "flighttask_progress";
}
}

View File

@ -0,0 +1,26 @@
package com.ruoyi.dj.websocket.handler;
import com.ruoyi.dj.websocket.core.MessageHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.ruoyi.dj.websocket.config.CustomWebSocketHandler;
/**
* @auther 周志雄
* @date 2024/7/8 18:24
*/
@Component
public class OSD1Handler implements MessageHandler {
@Autowired
private CustomWebSocketHandler webSocketHandler;
@Override
public void handleMessage(String message) {
webSocketHandler.sendMessageToAll(message);
}
@Override
public String getType() {
return "osd1";
}
}

View File

@ -0,0 +1,31 @@
package com.ruoyi.dj.websocket.handler;
/**
* @auther 周志雄
* @date 2024/7/8 18:24
*/
import com.ruoyi.dj.websocket.core.MessageHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.ruoyi.dj.websocket.config.CustomWebSocketHandler;
/**
* @auther 周志雄
* @date 2024/7/8 18:24
*/
@Component
public class OSD2Handler implements MessageHandler {
@Autowired
private CustomWebSocketHandler webSocketHandler;
@Override
public void handleMessage(String message) {
webSocketHandler.sendMessageToAll(message);
}
@Override
public String getType() {
return "osd2";
}
}

View File

@ -0,0 +1,31 @@
package com.ruoyi.dj.websocket.handler;
/**
* @auther 周志雄
* @date 2024/7/8 18:24
*/
import com.ruoyi.dj.websocket.core.MessageHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.ruoyi.dj.websocket.config.CustomWebSocketHandler;
/**
* @auther 周志雄
* @date 2024/7/8 18:24
*/
@Component
public class OSD3Handler implements MessageHandler {
@Autowired
private CustomWebSocketHandler webSocketHandler;
@Override
public void handleMessage(String message) {
webSocketHandler.sendMessageToAll(message);
}
@Override
public String getType() {
return "osd3";
}
}

View File

@ -0,0 +1,26 @@
package com.ruoyi.dj.websocket.handler;
import com.ruoyi.dj.websocket.config.CustomWebSocketHandler;
import com.ruoyi.dj.websocket.core.MessageHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @auther 周志雄
* @date 2024/7/8 18:24
*/
@Component
public class OSD4Handler implements MessageHandler {
@Autowired
private CustomWebSocketHandler webSocketHandler;
@Override
public void handleMessage(String message) {
webSocketHandler.sendMessageToAll(message);
}
@Override
public String getType() {
return "osd4";
}
}

Some files were not shown because too many files have changed in this diff Show More