1.新增Q20飞机参数指令11个接口

2.新增Q20飞机控制指令12个接口
3.新增Q20飞机航线任务13个接口
4.新增Q20飞机设备拓扑状态2个接口
5.新增Q20飞机切面日志
This commit is contained in:
938693313@qq.com 2026-05-27 13:56:39 +08:00
parent 0aa306b8a9
commit e23e95c511
72 changed files with 7082 additions and 12 deletions

View File

@ -0,0 +1,51 @@
package com.multictrl.common.aspect;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
/**
* Q20 service 调用日志切面
* 在每个 Q20 service 方法执行前后打印开始/结束及耗时异常时打印错误信息
*
* @author 938693313@qq.com
* @since 1.0.0 2026/5/20
*/
@Slf4j
@Aspect
@Component
public class Q20ServiceLogAspect {
@Pointcut("execution(public * com.multictrl.modules.business.q20.service.impl.*.*(..))")
public void q20ServicePointcut() {
}
@Around("q20ServicePointcut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
MethodSignature sig = (MethodSignature) point.getSignature();
String className = sig.getDeclaringType().getSimpleName();
String methodName = sig.getName();
String tag = className + "." + methodName;
// 第一个参数通常是 deviceSn
Object[] args = point.getArgs();
String deviceSn = (args != null && args.length > 0 && args[0] instanceof String)
? (String) args[0] : "-";
log.info("Q20 [{}] 开始 deviceSn={}", tag, deviceSn);
long start = System.currentTimeMillis();
try {
Object result = point.proceed();
log.info("Q20 [{}] 结束 deviceSn={} 耗时={}ms", tag, deviceSn, System.currentTimeMillis() - start);
return result;
} catch (Exception e) {
log.error("Q20 [{}] 异常 deviceSn={} 耗时={}ms error={}",
tag, deviceSn, System.currentTimeMillis() - start, e.getMessage());
throw e;
}
}
}

View File

@ -0,0 +1,24 @@
package com.multictrl.common.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 注册 Jackson 2.x ObjectMapper bean
* Spring Boot 4.x 默认只注册 tools.jackson.databind.ObjectMapperJackson 3.x
* 项目中使用 com.fasterxml.jackson.annotation.JsonProperty 的代码需要此 bean
*/
@Configuration
public class Jackson2ObjectMapperConfig {
@Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
return mapper;
}
}

View File

@ -104,6 +104,10 @@ public class RouteDTO implements Serializable {
@Schema(description = "kmz地址")
private String kmzUrl;
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
@Schema(description = "Q20航线IDQ20设备上报的wayline ID")
private String q20RouteId;
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
@Schema(description = "航点个数")
private Integer waypointNum;

View File

@ -85,6 +85,10 @@ public class RouteEntity extends BaseEntity {
* kmz地址
*/
private String kmzUrl;
/**
* Q20航线IDQ20设备上报的wayline ID
*/
private String q20RouteId;
/**
* 航点个数
*/

View File

@ -5,6 +5,7 @@ import com.multictrl.common.constant.BusinessConstant;
import com.multictrl.modules.business.q20.handler.Q20EventsHandler;
import com.multictrl.modules.business.q20.handler.Q20OsdTopicHandler;
import com.multictrl.modules.business.q20.handler.Q20StateTopicHandler;
import com.multictrl.modules.business.q20.handler.Q20StatusHandler;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@ -32,10 +33,12 @@ public class TopicDistributor {
private final Q20OsdTopicHandler q20OsdTopicHandler;
private final Q20EventsHandler q20EventsHandler;
private final Q20StateTopicHandler q20StateTopicHandler;
private final Q20StatusHandler q20StatusHandler;
private final Map<String, MessageHandler> handlerMap = new ConcurrentHashMap<>();
private final Map<String, MessageHandler> q20HandlerMap = new ConcurrentHashMap<>();
private static final String Q20_TOPIC_PREFIX = "thing/device/";
private static final String Q20_THING_PREFIX = "thing/device/";
private static final String Q20_SYS_PREFIX = "sys/device/";
@PostConstruct
public void init() {
@ -49,13 +52,16 @@ public class TopicDistributor {
q20HandlerMap.put(BusinessConstant.OSD, q20OsdTopicHandler);
q20HandlerMap.put(BusinessConstant.EVENTS, q20EventsHandler);
q20HandlerMap.put(BusinessConstant.STATE, q20StateTopicHandler);
q20HandlerMap.put(BusinessConstant.STATUS, q20StatusHandler);
q20HandlerMap.put(BusinessConstant.SERVICES_REPLY, servicesReplyHandler);
}
public void route(String topic, String payload) {
String method = StrUtil.subAfter(topic, "/", true);
String gateway = StrUtil.subAfter(StrUtil.subBefore(topic, "/", true),
"/", true);
Map<String, MessageHandler> map = topic.startsWith(Q20_TOPIC_PREFIX) ? q20HandlerMap : handlerMap;
boolean isQ20 = topic.startsWith(Q20_THING_PREFIX) || topic.startsWith(Q20_SYS_PREFIX);
Map<String, MessageHandler> map = isQ20 ? q20HandlerMap : handlerMap;
MessageHandler handler = map.get(method);
if (handler != null) {
handler.handleMessage(topic, payload, gateway);

View File

@ -0,0 +1,344 @@
package com.multictrl.modules.business.q20.controller;
import com.multictrl.common.annotation.ApiOrder;
import com.multictrl.common.annotation.LogOperation;
import com.multictrl.common.utils.Result;
import com.multictrl.modules.business.q20.dto.*;
import com.multictrl.modules.business.q20.service.Q20CommandService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.web.bind.annotation.*;
/**
* Q20控制指令接口
* topic: thing/device/{device_sn}/services (下行)
* 回复: thing/device/{device_sn}/services_reply (上行)
*
* @author 938693313@qq.com
* @since 1.0.0 2026/5/20
*/
@RestController
@RequestMapping("business/q20/cmd")
@Tag(name = "Q20控制指令")
@ApiOrder(12)
@RequiredArgsConstructor
public class Q20CommandController {
private final Q20CommandService q20CommandService;
// ==================== 飞行控制 ====================
@PostMapping("/takeoff/{deviceSn}")
@LogOperation("Q20起飞")
@Operation(summary = "起飞: mode=0绝对高度/1相对高度")
@RequiresPermissions("bus:q20:cmd")
public Result<Object> takeoff(@PathVariable String deviceSn, @RequestBody Q20TakeoffDTO dto) {
return new Result<>().ok(q20CommandService.takeoff(deviceSn, dto));
}
@PostMapping("/land/{deviceSn}")
@LogOperation("Q20降落")
@Operation(summary = "降落: mode=0直接降落/1精准降落")
@RequiresPermissions("bus:q20:cmd")
public Result<Object> land(@PathVariable String deviceSn, @RequestBody Q20LandDTO dto) {
return new Result<>().ok(q20CommandService.land(deviceSn, dto));
}
@PostMapping("/stop/{deviceSn}")
@LogOperation("Q20紧急悬停")
@Operation(summary = "紧急悬停(value固定为1)")
@RequiresPermissions("bus:q20:cmd")
public Result<Object> stop(@PathVariable String deviceSn) {
return new Result<>().ok(q20CommandService.stop(deviceSn));
}
@PostMapping("/goHome/{deviceSn}")
@LogOperation("Q20返航")
@Operation(summary = "返航: mode=1安全高度/2直线飞行/3原路返回")
@RequiresPermissions("bus:q20:cmd")
public Result<Object> goHome(@PathVariable String deviceSn, @RequestBody Q20GoHomeDTO dto) {
return new Result<>().ok(q20CommandService.goHome(deviceSn, dto));
}
@PostMapping("/setHome/{deviceSn}")
@LogOperation("Q20设置返航点")
@Operation(summary = "设置返航点坐标")
@RequiresPermissions("bus:q20:cmd")
public Result<Object> setHome(@PathVariable String deviceSn, @RequestBody Q20SetHomeDTO dto) {
return new Result<>().ok(q20CommandService.setHome(deviceSn, dto));
}
@PostMapping("/navigate/{deviceSn}")
@LogOperation("Q20航点飞行")
@Operation(summary = "飞往指定坐标: action=noAction/land/precland")
@RequiresPermissions("bus:q20:cmd")
public Result<Object> navigate(@PathVariable String deviceSn, @RequestBody Q20NavigateDTO dto) {
return new Result<>().ok(q20CommandService.navigate(deviceSn, dto));
}
// ==================== 云台相机 ====================
@PostMapping("/gimbalControl/{deviceSn}")
@LogOperation("Q20云台控制")
@Operation(summary = "云台控制: mode=0速度模式/1绝对值模式")
@RequiresPermissions("bus:q20:cmd")
public Result<Object> gimbalControl(@PathVariable String deviceSn, @RequestBody Q20GimbalControlDTO dto) {
return new Result<>().ok(q20CommandService.gimbalControl(deviceSn, dto));
}
@PostMapping("/mountTda/{deviceSn}")
@LogOperation("Q20挂载TDA数据")
@Operation(summary = "挂载TDA数据(仅支持deviceID=68的负载)")
@RequiresPermissions("bus:q20:cmd")
public Result<Object> mountTda(@PathVariable String deviceSn, @RequestBody Q20MountTdaDTO dto) {
return new Result<>().ok(q20CommandService.mountTda(deviceSn, dto));
}
@PostMapping("/cameraPhotoTake/{deviceSn}")
@LogOperation("Q20拍照")
@Operation(summary = "相机拍照")
@RequiresPermissions("bus:q20:cmd")
public Result<Object> cameraPhotoTake(@PathVariable String deviceSn, @RequestBody Q20CameraDTO dto) {
return new Result<>().ok(q20CommandService.cameraPhotoTake(deviceSn, dto));
}
@PostMapping("/cameraRecordingStart/{deviceSn}")
@LogOperation("Q20开始录像")
@Operation(summary = "开始录像")
@RequiresPermissions("bus:q20:cmd")
public Result<Object> cameraRecordingStart(@PathVariable String deviceSn, @RequestBody Q20CameraDTO dto) {
return new Result<>().ok(q20CommandService.cameraRecordingStart(deviceSn, dto));
}
@PostMapping("/cameraRecordingStop/{deviceSn}")
@LogOperation("Q20停止录像")
@Operation(summary = "停止录像")
@RequiresPermissions("bus:q20:cmd")
public Result<Object> cameraRecordingStop(@PathVariable String deviceSn, @RequestBody Q20CameraDTO dto) {
return new Result<>().ok(q20CommandService.cameraRecordingStop(deviceSn, dto));
}
@PostMapping("/cameraFocalLengthSet/{deviceSn}")
@LogOperation("Q20设置焦距")
@Operation(summary = "设置相机焦距: zoom_type=0绝对值/1连续, zoom_mode=0数字/1光学")
@RequiresPermissions("bus:q20:cmd")
public Result<Object> cameraFocalLengthSet(@PathVariable String deviceSn, @RequestBody Q20CameraFocalLengthDTO dto) {
return new Result<>().ok(q20CommandService.cameraFocalLengthSet(deviceSn, dto));
}
// ==================== 直播 ====================
@PostMapping("/liveStartPush/{deviceSn}")
@LogOperation("Q20开始直播")
@Operation(summary = "开始直播推流: url_type=0Agora/1RTMP/2RTSP/3GB28181/4WebRTC")
@RequiresPermissions("bus:q20:cmd")
public Result<Object> liveStartPush(@PathVariable String deviceSn, @RequestBody Q20LiveStartDTO dto) {
return new Result<>().ok(q20CommandService.liveStartPush(deviceSn, dto));
}
@PostMapping("/liveStopPush/{deviceSn}")
@LogOperation("Q20停止直播")
@Operation(summary = "停止直播推流")
@RequiresPermissions("bus:q20:cmd")
public Result<Object> liveStopPush(@PathVariable String deviceSn,
@RequestParam int payloadIndex,
@RequestParam int video) {
return new Result<>().ok(q20CommandService.liveStopPush(deviceSn, payloadIndex, video));
}
@PostMapping("/imageSwitch/{deviceSn}")
@LogOperation("Q20图像切换")
@Operation(summary = "图像切换(红外/可见光/激光)")
@RequiresPermissions("bus:q20:cmd")
public Result<Object> imageSwitch(@PathVariable String deviceSn, @RequestBody Q20ImageSwitchDTO dto) {
return new Result<>().ok(q20CommandService.imageSwitch(deviceSn, dto));
}
// ==================== 设备控制 ====================
@PostMapping("/deviceCharge/{deviceSn}")
@LogOperation("Q20充电控制")
@Operation(summary = "充电控制: version=1(1.0版本)/2(2.0版本)")
@RequiresPermissions("bus:q20:cmd")
public Result<Object> deviceCharge(@PathVariable String deviceSn,
@RequestParam int value,
@RequestParam int version) {
return new Result<>().ok(q20CommandService.deviceCharge(deviceSn, value, version));
}
@PostMapping("/deviceBoot/{deviceSn}")
@LogOperation("Q20重启/关机")
@Operation(summary = "设备控制: value=0关机/1重启/2硬重启/3锁定")
@RequiresPermissions("bus:q20:cmd")
public Result<Object> deviceBoot(@PathVariable String deviceSn, @RequestParam int value) {
return new Result<>().ok(q20CommandService.deviceBoot(deviceSn, value));
}
@PostMapping("/safetySwitch/{deviceSn}")
@LogOperation("Q20安全开关")
@Operation(summary = "安全开关控制")
@RequiresPermissions("bus:q20:cmd")
public Result<Object> safetySwitch(@PathVariable String deviceSn, @RequestParam int value) {
return new Result<>().ok(q20CommandService.safetySwitch(deviceSn, value));
}
// ==================== 负载 ====================
@PostMapping("/throwingLock/{deviceSn}")
@LogOperation("Q20投掷锁控制")
@Operation(summary = "投掷锁/解锁: value=0解锁/1锁定")
@RequiresPermissions("bus:q20:cmd")
public Result<Object> throwingLock(@PathVariable String deviceSn, @RequestBody Q20ThrowingDTO dto) {
return new Result<>().ok(q20CommandService.throwingLock(deviceSn, dto));
}
@PostMapping("/throwingExecution/{deviceSn}")
@LogOperation("Q20执行投掷")
@Operation(summary = "执行投掷动作")
@RequiresPermissions("bus:q20:cmd")
public Result<Object> throwingExecution(@PathVariable String deviceSn, @RequestParam long deviceId) {
return new Result<>().ok(q20CommandService.throwingExecution(deviceSn, deviceId));
}
@PostMapping("/halyarding/{deviceSn}")
@LogOperation("Q20绞车控制")
@Operation(summary = "绞车控制")
@RequiresPermissions("bus:q20:cmd")
public Result<Object> halyarding(@PathVariable String deviceSn, @RequestBody Q20HalyardingDTO dto) {
return new Result<>().ok(q20CommandService.halyarding(deviceSn, dto));
}
@PostMapping("/logistics/{deviceSn}")
@LogOperation("Q20物流控制")
@Operation(summary = "物流控制")
@RequiresPermissions("bus:q20:cmd")
public Result<Object> logistics(@PathVariable String deviceSn,
@RequestParam int value,
@RequestParam boolean force) {
return new Result<>().ok(q20CommandService.logistics(deviceSn, value, force));
}
// ==================== 虚拟摇杆 ====================
@PostMapping("/drc/{deviceSn}")
@Operation(summary = "虚拟摇杆控制(高频指令,不等回复)")
@RequiresPermissions("bus:q20:cmd")
public void drc(@PathVariable String deviceSn, @RequestBody Q20DrcDTO dto) {
q20CommandService.drc(deviceSn, dto);
}
// ==================== 安全/降落伞 ====================
@PostMapping("/setAutoParachute/{deviceSn}")
@LogOperation("Q20设置自动开伞")
@Operation(summary = "设置自动开伞开关")
@RequiresPermissions("bus:q20:parachute")
public Result<Object> setAutoParachute(@PathVariable String deviceSn, @RequestParam int value) {
return new Result<>().ok(q20CommandService.setAutoParachute(deviceSn, value));
}
@GetMapping("/getEmergencyStopOtp/{deviceSn}")
@Operation(summary = "获取紧急停止OTP验证码")
@RequiresPermissions("bus:q20:cmd")
public Result<Object> getEmergencyStopOtp(@PathVariable String deviceSn) {
return new Result<>().ok(q20CommandService.getEmergencyStopOtp(deviceSn));
}
@PostMapping("/emergencyStop/{deviceSn}")
@LogOperation("Q20紧急停止")
@Operation(summary = "紧急停止(需先获取OTP)")
@RequiresPermissions("bus:q20:cmd")
public Result<Object> emergencyStop(@PathVariable String deviceSn, @RequestBody Q20EmergencyStopDTO dto) {
return new Result<>().ok(q20CommandService.emergencyStop(deviceSn, dto));
}
@GetMapping("/getParachuteOtp/{deviceSn}")
@Operation(summary = "获取开伞OTP验证码")
@RequiresPermissions("bus:q20:parachute")
public Result<Object> getParachuteOtp(@PathVariable String deviceSn) {
return new Result<>().ok(q20CommandService.getParachuteOtp(deviceSn));
}
@PostMapping("/openParachute/{deviceSn}")
@LogOperation("Q20开伞")
@Operation(summary = "开伞(otp=99时为强制开伞)")
@RequiresPermissions("bus:q20:parachute")
public Result<Object> openParachute(@PathVariable String deviceSn, @RequestBody Q20OpenParachuteDTO dto) {
return new Result<>().ok(q20CommandService.openParachute(deviceSn, dto));
}
@PostMapping("/confirmParachuteSafety/{deviceSn}")
@LogOperation("Q20确认开伞安全")
@Operation(summary = "确认开伞安全")
@RequiresPermissions("bus:q20:parachute")
public Result<Object> confirmParachuteSafety(@PathVariable String deviceSn, @RequestParam int value) {
return new Result<>().ok(q20CommandService.confirmParachuteSafety(deviceSn, value));
}
// ==================== 状态/信息 ====================
@GetMapping("/queryStatus/{deviceSn}")
@Operation(summary = "查询飞行状态: query_value=liftoff/arrival等")
@RequiresPermissions("bus:q20:cmd")
public Result<Object> queryStatus(@PathVariable String deviceSn, @RequestBody Q20QueryStatusDTO dto) {
return new Result<>().ok(q20CommandService.queryStatus(deviceSn, dto));
}
@GetMapping("/config/{deviceSn}")
@Operation(summary = "获取设备配置")
@RequiresPermissions("bus:q20:cmd")
public Result<Object> getConfig(@PathVariable String deviceSn) {
return new Result<>().ok(q20CommandService.getConfig(deviceSn));
}
@PostMapping("/stereoImage/{deviceSn}")
@LogOperation("Q20立体图像开关")
@Operation(summary = "立体图像开关: 0=开启/1=关闭")
@RequiresPermissions("bus:q20:cmd")
public Result<Object> stereoImage(@PathVariable String deviceSn, @RequestParam int value) {
return new Result<>().ok(q20CommandService.stereoImage(deviceSn, value));
}
@GetMapping("/version/{deviceSn}")
@Operation(summary = "获取设备固件版本")
@RequiresPermissions("bus:q20:cmd")
public Result<Object> version(@PathVariable String deviceSn) {
return new Result<>().ok(q20CommandService.version(deviceSn));
}
// ==================== 日志 ====================
@GetMapping("/logCount/{deviceSn}")
@Operation(summary = "获取日志数量")
@RequiresPermissions("bus:q20:log")
public Result<Object> logCount(@PathVariable String deviceSn) {
return new Result<>().ok(q20CommandService.logCount(deviceSn));
}
@PostMapping("/logList/{deviceSn}")
@Operation(summary = "获取日志列表")
@RequiresPermissions("bus:q20:log")
public Result<Object> logList(@PathVariable String deviceSn, @RequestBody Q20LogListDTO dto) {
return new Result<>().ok(q20CommandService.logList(deviceSn, dto));
}
@PostMapping("/logData/{deviceSn}")
@LogOperation("Q20上传日志")
@Operation(summary = "上传日志到OSS")
@RequiresPermissions("bus:q20:log")
public Result<Object> logData(@PathVariable String deviceSn, @RequestBody Q20LogDataDTO dto) {
return new Result<>().ok(q20CommandService.logData(deviceSn, dto));
}
// ==================== OTA ====================
@PostMapping("/otaUpgrade/{deviceSn}")
@LogOperation("Q20 OTA升级")
@Operation(summary = "固件OTA升级")
@RequiresPermissions("bus:q20:ota")
public Result<Object> otaUpgrade(@PathVariable String deviceSn, @RequestBody Q20OtaUpgradeDTO dto) {
return new Result<>().ok(q20CommandService.otaUpgrade(deviceSn, dto));
}
}

View File

@ -0,0 +1,43 @@
package com.multictrl.modules.business.q20.controller;
import com.multictrl.common.annotation.ApiOrder;
import com.multictrl.common.utils.Result;
import com.multictrl.modules.business.q20.service.Q20DeviceService;
import com.multictrl.modules.business.q20.vo.Q20DeviceStatusVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* Q20飞机管理接口
*
* @author 938693313@qq.com
* @since 1.0.0 2026/5/20
*/
@RestController
@RequestMapping("business/q20")
@Tag(name = "Q20飞机管理")
@ApiOrder(10)
@RequiredArgsConstructor
public class Q20Controller {
private final Q20DeviceService q20DeviceService;
@GetMapping("/online/{deviceSn}")
@Operation(summary = "查询飞机在线状态在线时同时返回最新OSD数据")
@RequiresPermissions("bus:q20:online")
public Result<Q20DeviceStatusVO> getOnlineStatus(@PathVariable String deviceSn) {
return new Result<Q20DeviceStatusVO>().ok(q20DeviceService.getOnlineStatus(deviceSn));
}
@PostMapping("/onlineBatch")
@Operation(summary = "批量查询飞机在线状态")
@RequiresPermissions("bus:q20:online")
public Result<List<Q20DeviceStatusVO>> getOnlineStatusBatch(@RequestBody List<String> deviceSnList) {
return new Result<List<Q20DeviceStatusVO>>().ok(q20DeviceService.getOnlineStatusBatch(deviceSnList));
}
}

View File

@ -0,0 +1,77 @@
package com.multictrl.modules.business.q20.controller;
import cn.hutool.core.util.IdUtil;
import cn.hutool.json.JSONObject;
import com.multictrl.common.exception.RenException;
import com.multictrl.common.utils.Result;
import com.multictrl.modules.business.service.DJIBaseService;
import com.multictrl.modules.business.service.MqttPushService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
/**
* Q20 MQTT模拟接口测试/调试用
* - /reply : 模拟设备对 services 指令的回复注入 CacheUtils
* - /aircraft: 模拟飞机主动上报消息发布到对应 MQTT topic经过正常 handler 处理
*/
@RestController
@RequestMapping("business/q20/mock")
@Tag(name = "Q20 MQTT模拟(测试)")
@RequiredArgsConstructor
public class Q20MockController {
private final DJIBaseService djiBaseService;
private final MqttPushService mqttPushService;
// 指令模拟回复
@GetMapping("/pending/{deviceSn}")
@Operation(summary = "获取当前待回复命令信息")
public Result<Object> getPending(@PathVariable String deviceSn) {
return new Result<>().ok(djiBaseService.getPendingCmd(deviceSn));
}
@PostMapping("/reply/{deviceSn}")
@Operation(summary = "注入模拟MQTT回复(直接写入缓存body为完整回复JSON)")
public Result<Object> injectReply(
@PathVariable String deviceSn,
@RequestBody JSONObject replyData) {
djiBaseService.injectMockReply(deviceSn, replyData);
return new Result<>().ok("模拟回复已注入");
}
// 飞机主动上报模拟
/**
* 模拟飞机发布 MQTT 消息到指定 topic
* topicType: status | osd | events | services_reply
* 消息由前端构造完整 payload后端直接转发到对应 topic
*/
@PostMapping("/aircraft/{deviceSn}")
@Operation(summary = "模拟飞机上报消息(发布到MQTT topic)")
public Result<Object> aircraftPublish(
@PathVariable String deviceSn,
@RequestParam String topicType,
@RequestBody JSONObject payload) {
String topic = buildTopic(deviceSn, topicType);
// 补充缺失的公共字段
if (!payload.containsKey("tid")) payload.set("tid", IdUtil.fastUUID());
if (!payload.containsKey("bid")) payload.set("bid", IdUtil.fastUUID());
if (!payload.containsKey("timestamp")) payload.set("timestamp", System.currentTimeMillis());
if (!payload.containsKey("gateway")) payload.set("gateway", deviceSn);
mqttPushService.pushMessageByClient1(topic, payload.toString());
return new Result<>().ok("已发布 → " + topic);
}
private String buildTopic(String deviceSn, String topicType) {
return switch (topicType) {
case "status" -> "sys/device/" + deviceSn + "/status";
case "osd" -> "thing/device/" + deviceSn + "/osd";
case "events" -> "thing/device/" + deviceSn + "/events";
case "services_reply" -> "thing/device/" + deviceSn + "/services_reply";
default -> throw new RenException("未知 topicType: " + topicType);
};
}
}

View File

@ -0,0 +1,332 @@
package com.multictrl.modules.business.q20.controller;
import com.multictrl.common.annotation.ApiOrder;
import com.multictrl.common.annotation.LogOperation;
import com.multictrl.common.utils.Result;
import com.multictrl.modules.business.q20.dto.Q20ParamsDTO;
import com.multictrl.modules.business.q20.service.Q20ParamService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.web.bind.annotation.*;
/**
* Q20参数指令接口
* topic: thing/device/{device_sn}/services (下行)
* 回复: thing/device/{device_sn}/services_reply (上行)
*
* @author 938693313@qq.com
* @since 1.0.0 2026/5/20
*/
@RestController
@RequestMapping("business/q20/param")
@Tag(name = "Q20参数指令")
@ApiOrder(11)
@RequiredArgsConstructor
public class Q20ParamController {
private final Q20ParamService q20ParamService;
// ==================== 高度限制 ====================
@PostMapping("/setHeightLimit/{deviceSn}")
@LogOperation("设置高度限制")
@Operation(summary = "设置高度限制(m)0=无限制")
@RequiresPermissions("bus:q20:param")
public Result<Object> setHeightLimit(@PathVariable String deviceSn, @RequestParam float value) {
return new Result<>().ok(q20ParamService.setHeightLimit(deviceSn, value));
}
@GetMapping("/getHeightLimit/{deviceSn}")
@Operation(summary = "获取当前高度限制(m)")
@RequiresPermissions("bus:q20:param")
public Result<Object> getHeightLimit(@PathVariable String deviceSn) {
return new Result<>().ok(q20ParamService.getHeightLimit(deviceSn));
}
// ==================== 距离限制 ====================
@PostMapping("/setDistanceLimit/{deviceSn}")
@LogOperation("设置距离限制")
@Operation(summary = "设置距离限制(m)0=无限制")
@RequiresPermissions("bus:q20:param")
public Result<Object> setDistanceLimit(@PathVariable String deviceSn, @RequestParam float value) {
return new Result<>().ok(q20ParamService.setDistanceLimit(deviceSn, value));
}
@GetMapping("/getDistanceLimit/{deviceSn}")
@Operation(summary = "获取当前距离限制(m)")
@RequiresPermissions("bus:q20:param")
public Result<Object> getDistanceLimit(@PathVariable String deviceSn) {
return new Result<>().ok(q20ParamService.getDistanceLimit(deviceSn));
}
// ==================== 围栏动作 ====================
@PostMapping("/setGeofenceAction/{deviceSn}")
@LogOperation("设置围栏动作")
@Operation(summary = "设置围栏动作: goContinue/warning/hover/goBack/terminate/landing")
@RequiresPermissions("bus:q20:param")
public Result<Object> setGeofenceAction(@PathVariable String deviceSn, @RequestParam String value) {
return new Result<>().ok(q20ParamService.setGeofenceAction(deviceSn, value));
}
@GetMapping("/getGeofenceAction/{deviceSn}")
@Operation(summary = "获取当前围栏动作")
@RequiresPermissions("bus:q20:param")
public Result<Object> getGeofenceAction(@PathVariable String deviceSn) {
return new Result<>().ok(q20ParamService.getGeofenceAction(deviceSn));
}
// ==================== 返航高度 ====================
@PostMapping("/setReturnHeight/{deviceSn}")
@LogOperation("设置返航高度")
@Operation(summary = "设置返航高度(m)相对home点高度")
@RequiresPermissions("bus:q20:param")
public Result<Object> setReturnHeight(@PathVariable String deviceSn, @RequestParam float value) {
return new Result<>().ok(q20ParamService.setReturnHeight(deviceSn, value));
}
@GetMapping("/getReturnHeight/{deviceSn}")
@Operation(summary = "获取当前返航高度(m)")
@RequiresPermissions("bus:q20:param")
public Result<Object> getReturnHeight(@PathVariable String deviceSn) {
return new Result<>().ok(q20ParamService.getReturnHeight(deviceSn));
}
// ==================== 返航类型 ====================
@PostMapping("/setReturnType/{deviceSn}")
@LogOperation("设置返航类型")
@Operation(summary = "设置返航类型: directReturn(直接返航) / originalReturn(原路返航)")
@RequiresPermissions("bus:q20:param")
public Result<Object> setReturnType(@PathVariable String deviceSn, @RequestParam String value) {
return new Result<>().ok(q20ParamService.setReturnType(deviceSn, value));
}
@GetMapping("/getReturnType/{deviceSn}")
@Operation(summary = "获取当前返航类型")
@RequiresPermissions("bus:q20:param")
public Result<Object> getReturnType(@PathVariable String deviceSn) {
return new Result<>().ok(q20ParamService.getReturnType(deviceSn));
}
// ==================== 电池低电阈值 ====================
@PostMapping("/setBatteryLowThreshold/{deviceSn}")
@LogOperation("设置低电阈值")
@Operation(summary = "设置低电阈值比例如0.3=30%")
@RequiresPermissions("bus:q20:param")
public Result<Object> setBatteryLowThreshold(@PathVariable String deviceSn, @RequestParam float value) {
return new Result<>().ok(q20ParamService.setBatteryLowThreshold(deviceSn, value));
}
@GetMapping("/getBatteryLowThreshold/{deviceSn}")
@Operation(summary = "获取当前低电阈值")
@RequiresPermissions("bus:q20:param")
public Result<Object> getBatteryLowThreshold(@PathVariable String deviceSn) {
return new Result<>().ok(q20ParamService.getBatteryLowThreshold(deviceSn));
}
// ==================== 严重低电阈值 ====================
@PostMapping("/setBatteryCriticalThreshold/{deviceSn}")
@LogOperation("设置严重低电阈值")
@Operation(summary = "设置严重低电阈值(比例)")
@RequiresPermissions("bus:q20:param")
public Result<Object> setBatteryCriticalThreshold(@PathVariable String deviceSn, @RequestParam float value) {
return new Result<>().ok(q20ParamService.setBatteryCriticalThreshold(deviceSn, value));
}
@GetMapping("/getBatteryCriticalThreshold/{deviceSn}")
@Operation(summary = "获取严重低电阈值")
@RequiresPermissions("bus:q20:param")
public Result<Object> getBatteryCriticalThreshold(@PathVariable String deviceSn) {
return new Result<>().ok(q20ParamService.getBatteryCriticalThreshold(deviceSn));
}
// ==================== 紧急低电阈值 ====================
@PostMapping("/setBatteryEmergencyThreshold/{deviceSn}")
@LogOperation("设置紧急低电阈值")
@Operation(summary = "设置紧急低电阈值(比例)")
@RequiresPermissions("bus:q20:param")
public Result<Object> setBatteryEmergencyThreshold(@PathVariable String deviceSn, @RequestParam float value) {
return new Result<>().ok(q20ParamService.setBatteryEmergencyThreshold(deviceSn, value));
}
@GetMapping("/getBatteryEmergencyThreshold/{deviceSn}")
@Operation(summary = "获取紧急低电阈值")
@RequiresPermissions("bus:q20:param")
public Result<Object> getBatteryEmergencyThreshold(@PathVariable String deviceSn) {
return new Result<>().ok(q20ParamService.getBatteryEmergencyThreshold(deviceSn));
}
// ==================== 低电动作 ====================
@PostMapping("/setLowBatteryAction/{deviceSn}")
@LogOperation("设置低电动作")
@Operation(summary = "设置低电动作: warning(警告) / landing(降落) / goBackCritAndLandEmerg(低电返航·紧急降落)")
@RequiresPermissions("bus:q20:param")
public Result<Object> setLowBatteryAction(@PathVariable String deviceSn, @RequestParam String value) {
return new Result<>().ok(q20ParamService.setLowBatteryAction(deviceSn, value));
}
@GetMapping("/getLowBatteryAction/{deviceSn}")
@Operation(summary = "获取当前低电动作")
@RequiresPermissions("bus:q20:param")
public Result<Object> getLowBatteryAction(@PathVariable String deviceSn) {
return new Result<>().ok(q20ParamService.getLowBatteryAction(deviceSn));
}
// ==================== 精准降落 ====================
@PostMapping("/setAprilTagId/{deviceSn}")
@LogOperation("设置精准降落二维码标识")
@Operation(summary = "设置精准降落二维码标识")
@RequiresPermissions("bus:q20:param")
public Result<Object> setAprilTagId(@PathVariable String deviceSn, @RequestParam int value) {
return new Result<>().ok(q20ParamService.setAprilTagId(deviceSn, value));
}
@GetMapping("/getAprilTagId/{deviceSn}")
@Operation(summary = "获取精准降落二维码标识")
@RequiresPermissions("bus:q20:param")
public Result<Object> getAprilTagId(@PathVariable String deviceSn) {
return new Result<>().ok(q20ParamService.getAprilTagId(deviceSn));
}
@PostMapping("/setPrecisionLandMode/{deviceSn}")
@LogOperation("设置精准降落模式")
@Operation(summary = "设置精准降落模式: noPrecisionLanding / opportunisticPrecisionLanding / requiredPrecisionLanding")
@RequiresPermissions("bus:q20:param")
public Result<Object> setPrecisionLandMode(@PathVariable String deviceSn, @RequestParam String value) {
return new Result<>().ok(q20ParamService.setPrecisionLandMode(deviceSn, value));
}
@GetMapping("/getPrecisionLandMode/{deviceSn}")
@Operation(summary = "获取精准降落模式")
@RequiresPermissions("bus:q20:param")
public Result<Object> getPrecisionLandMode(@PathVariable String deviceSn) {
return new Result<>().ok(q20ParamService.getPrecisionLandMode(deviceSn));
}
// ==================== 避障 ====================
@PostMapping("/setObstacleAvoidanceEnable/{deviceSn}")
@LogOperation("设置避障开关")
@Operation(summary = "设置避障开关: 0=关闭 1=开启")
@RequiresPermissions("bus:q20:param")
public Result<Object> setObstacleAvoidanceEnable(@PathVariable String deviceSn, @RequestParam int value) {
return new Result<>().ok(q20ParamService.setObstacleAvoidanceEnable(deviceSn, value));
}
@GetMapping("/getObstacleAvoidanceEnable/{deviceSn}")
@Operation(summary = "获取避障开关状态")
@RequiresPermissions("bus:q20:param")
public Result<Object> getObstacleAvoidanceEnable(@PathVariable String deviceSn) {
return new Result<>().ok(q20ParamService.getObstacleAvoidanceEnable(deviceSn));
}
@PostMapping("/setObstacleAvoidanceDistance/{deviceSn}")
@LogOperation("设置避障距离")
@Operation(summary = "设置避障距离(m)")
@RequiresPermissions("bus:q20:param")
public Result<Object> setObstacleAvoidanceDistance(@PathVariable String deviceSn, @RequestParam float value) {
return new Result<>().ok(q20ParamService.setObstacleAvoidanceDistance(deviceSn, value));
}
@GetMapping("/getObstacleAvoidanceDistance/{deviceSn}")
@Operation(summary = "获取避障距离(m)")
@RequiresPermissions("bus:q20:param")
public Result<Object> getObstacleAvoidanceDistance(@PathVariable String deviceSn) {
return new Result<>().ok(q20ParamService.getObstacleAvoidanceDistance(deviceSn));
}
// ==================== 视觉传感器 ====================
@PostMapping("/setVisualSensorEnable/{deviceSn}")
@LogOperation("设置视觉传感器开关")
@Operation(summary = "设置视觉传感器开关: 0=关闭 1=开启")
@RequiresPermissions("bus:q20:param")
public Result<Object> setVisualSensorEnable(@PathVariable String deviceSn, @RequestParam int value) {
return new Result<>().ok(q20ParamService.setVisualSensorEnable(deviceSn, value));
}
@GetMapping("/getVisualSensorEnable/{deviceSn}")
@Operation(summary = "获取视觉传感器开关状态")
@RequiresPermissions("bus:q20:param")
public Result<Object> getVisualSensorEnable(@PathVariable String deviceSn) {
return new Result<>().ok(q20ParamService.getVisualSensorEnable(deviceSn));
}
// ==================== RTK ====================
@PostMapping("/setRtkEnable/{deviceSn}")
@LogOperation("设置RTK开关")
@Operation(summary = "设置RTK开关: 0=关闭 1=开启(重启后生效)")
@RequiresPermissions("bus:q20:param")
public Result<Object> setRtkEnable(@PathVariable String deviceSn, @RequestParam int value) {
return new Result<>().ok(q20ParamService.setRtkEnable(deviceSn, value));
}
@GetMapping("/getRtkEnable/{deviceSn}")
@Operation(summary = "获取RTK开关状态")
@RequiresPermissions("bus:q20:param")
public Result<Object> getRtkEnable(@PathVariable String deviceSn) {
return new Result<>().ok(q20ParamService.getRtkEnable(deviceSn));
}
// ==================== 多载控制 ====================
@PostMapping("/setUatMultCtrlEnable/{deviceSn}")
@LogOperation("设置多载控制开关")
@Operation(summary = "设置多载控制开关: 0=关闭 1=开启(重启后生效)")
@RequiresPermissions("bus:q20:param")
public Result<Object> setUatMultCtrlEnable(@PathVariable String deviceSn, @RequestParam int value) {
return new Result<>().ok(q20ParamService.setUatMultCtrlEnable(deviceSn, value));
}
@GetMapping("/getUatMultCtrlEnable/{deviceSn}")
@Operation(summary = "获取多载控制开关状态")
@RequiresPermissions("bus:q20:param")
public Result<Object> getUatMultCtrlEnable(@PathVariable String deviceSn) {
return new Result<>().ok(q20ParamService.getUatMultCtrlEnable(deviceSn));
}
// ==================== 降落伞 ====================
@PostMapping("/setParachuteSafetyEnable/{deviceSn}")
@LogOperation("设置降落伞安全开关")
@Operation(summary = "设置降落伞安全开关: 0=关闭 1=开启")
@RequiresPermissions("bus:q20:param")
public Result<Object> setParachuteSafetyEnable(@PathVariable String deviceSn, @RequestParam int value) {
return new Result<>().ok(q20ParamService.setParachuteSafetyEnable(deviceSn, value));
}
@GetMapping("/getParachuteSafetyEnable/{deviceSn}")
@Operation(summary = "获取降落伞安全开关状态")
@RequiresPermissions("bus:q20:param")
public Result<Object> getParachuteSafetyEnable(@PathVariable String deviceSn) {
return new Result<>().ok(q20ParamService.getParachuteSafetyEnable(deviceSn));
}
// ==================== 批量 ====================
@PostMapping("/setParams/{deviceSn}")
@LogOperation("批量设置参数")
@Operation(summary = "批量设置参数null字段不下发")
@RequiresPermissions("bus:q20:param")
public Result<Object> setParams(@PathVariable String deviceSn, @RequestBody Q20ParamsDTO dto) {
return new Result<>().ok(q20ParamService.setParams(deviceSn, dto));
}
@GetMapping("/getParams/{deviceSn}")
@Operation(summary = "批量获取所有参数")
@RequiresPermissions("bus:q20:param")
public Result<Object> getParams(@PathVariable String deviceSn) {
return new Result<>().ok(q20ParamService.getParams(deviceSn));
}
}

View File

@ -0,0 +1,152 @@
package com.multictrl.modules.business.q20.controller;
import com.multictrl.common.annotation.ApiOrder;
import com.multictrl.common.annotation.LogOperation;
import com.multictrl.common.utils.Result;
import com.multictrl.modules.business.q20.dto.*;
import com.multictrl.modules.business.q20.service.Q20RouteService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.web.bind.annotation.*;
/**
* Q20航线任务接口
* topic: thing/device/{device_sn}/services (下行)
* 回复: thing/device/{device_sn}/services_reply (上行)
*
* @author 938693313@qq.com
* @since 1.0.0 2026/5/20
*/
@RestController
@RequestMapping("business/q20/route")
@Tag(name = "Q20航线任务")
@ApiOrder(13)
@RequiredArgsConstructor
public class Q20RouteController {
private final Q20RouteService q20RouteService;
// ==================== 下一个航线ID ====================
@GetMapping("/nextWayline")
@Operation(summary = "获取下一个航线ID根据库中最新航线ID自动递增")
@RequiresPermissions("bus:q20:route")
public Result<String> nextWayline(@RequestParam(defaultValue = "route") String type) {
return new Result<String>().ok(q20RouteService.nextWayline(type));
}
// ==================== 上传航线 ====================
@PostMapping("/upload/{deviceSn}")
@LogOperation("上传航线")
@Operation(summary = "上传航线route_upload")
@RequiresPermissions("bus:q20:route")
public Result<Object> routeUpload(@PathVariable String deviceSn,
@RequestBody Q20RouteUploadDTO dto) {
return new Result<>().ok(q20RouteService.routeUpload(deviceSn, dto));
}
// ==================== 执行航线 ====================
@PostMapping("/execute/{deviceSn}")
@LogOperation("执行航线")
@Operation(summary = "执行航线route_execute")
@RequiresPermissions("bus:q20:route")
public Result<Object> routeExecute(@PathVariable String deviceSn,
@RequestBody Q20RouteExecuteDTO dto) {
return new Result<>().ok(q20RouteService.routeExecute(deviceSn, dto));
}
// ==================== 一键飞行 ====================
@PostMapping("/auto/{deviceSn}")
@LogOperation("一键飞行(上传+执行)")
@Operation(summary = "一键飞行上传并执行航线route_auto")
@RequiresPermissions("bus:q20:route")
public Result<Object> routeAuto(@PathVariable String deviceSn,
@RequestBody Q20RouteAutoDTO dto) {
return new Result<>().ok(q20RouteService.routeAuto(deviceSn, dto));
}
// ==================== 取消悬停 ====================
@PostMapping("/breakHover/{deviceSn}")
@LogOperation("取消悬停")
@Operation(summary = "取消悬停break_loop_hover")
@RequiresPermissions("bus:q20:route")
public Result<Object> breakLoopHover(@PathVariable String deviceSn) {
return new Result<>().ok(q20RouteService.breakLoopHover(deviceSn));
}
// ==================== 获取航线信息 ====================
@GetMapping("/info/{deviceSn}")
@Operation(summary = "获取航线信息route_info")
@RequiresPermissions("bus:q20:route")
public Result<Object> routeInfo(@PathVariable String deviceSn,
@RequestParam int mode,
@RequestParam(required = false) String value) {
return new Result<>().ok(q20RouteService.routeInfo(deviceSn, mode, value));
}
// ==================== 获取执行进度 ====================
@GetMapping("/progress/{deviceSn}")
@Operation(summary = "获取航线执行进度route_progress")
@RequiresPermissions("bus:q20:route")
public Result<Object> routeProgress(@PathVariable String deviceSn) {
return new Result<>().ok(q20RouteService.routeProgress(deviceSn));
}
// ==================== 暂停航线 ====================
@PostMapping("/pause/{deviceSn}")
@LogOperation("暂停航线")
@Operation(summary = "暂停航线route_pause")
@RequiresPermissions("bus:q20:route")
public Result<Object> routePause(@PathVariable String deviceSn) {
return new Result<>().ok(q20RouteService.routePause(deviceSn));
}
// ==================== 继续航线 ====================
@PostMapping("/resume/{deviceSn}")
@LogOperation("继续航线")
@Operation(summary = "继续航线route_resume")
@RequiresPermissions("bus:q20:route")
public Result<Object> routeResume(@PathVariable String deviceSn) {
return new Result<>().ok(q20RouteService.routeResume(deviceSn));
}
// ==================== 退出航线 ====================
@PostMapping("/finish/{deviceSn}")
@LogOperation("退出航线")
@Operation(summary = "退出航线route_finish")
@RequiresPermissions("bus:q20:route")
public Result<Object> routeFinish(@PathVariable String deviceSn) {
return new Result<>().ok(q20RouteService.routeFinish(deviceSn));
}
// ==================== 获取围栏信息 ====================
@GetMapping("/geofenceInfo/{deviceSn}")
@Operation(summary = "获取围栏信息geofence_info")
@RequiresPermissions("bus:q20:route")
public Result<Object> geofenceInfo(@PathVariable String deviceSn) {
return new Result<>().ok(q20RouteService.geofenceInfo(deviceSn));
}
// ==================== 上传围栏 ====================
@PostMapping("/geofenceUpload/{deviceSn}")
@LogOperation("上传围栏")
@Operation(summary = "上传围栏geofence_upload")
@RequiresPermissions("bus:q20:route")
public Result<Object> geofenceUpload(@PathVariable String deviceSn,
@RequestBody Q20GeofenceUploadDTO dto) {
return new Result<>().ok(q20RouteService.geofenceUpload(deviceSn, dto));
}
}

View File

@ -0,0 +1,24 @@
package com.multictrl.modules.business.q20.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* Q20相机操作DTO拍照/录像开始/录像停止共用
*
* @author 938693313@qq.com
* @since 1.0.0 2026/5/20
*/
@Data
@Schema(name = "Q20相机操作参数")
public class Q20CameraDTO {
@Schema(description = "设备id, JSON key: device_id")
private Long deviceId;
@Schema(description = "负载索引: 0=前向, 1=前向, 2=下向, JSON key: payload_index")
private int payloadIndex;
@Schema(description = "相机类型: ir=红外, wide=广角, zoom=变焦, JSON key: camera_type")
private String cameraType;
}

View File

@ -0,0 +1,33 @@
package com.multictrl.modules.business.q20.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* Q20相机焦距设置DTO
*
* @author 938693313@qq.com
* @since 1.0.0 2026/5/20
*/
@Data
@Schema(name = "Q20相机焦距参数")
public class Q20CameraFocalLengthDTO {
@Schema(description = "负载索引, JSON key: payload_index")
private int payloadIndex;
@Schema(description = "设备id, JSON key: device_id")
private long deviceId;
@Schema(description = "相机类型: ir/wide/zoom/visible, JSON key: camera_type")
private String cameraType;
@Schema(description = "变焦倍数: 可见光2-200, 红外2-20; 连续变焦时范围-10~10, JSON key: zoom_factor")
private Double zoomFactor;
@Schema(description = "变焦类型: 0=绝对值, 1=连续, JSON key: zoom_type")
private int zoomType;
@Schema(description = "变焦模式: 0=数字(默认), 1=光学, JSON key: zoom_mode")
private int zoomMode;
}

View File

@ -0,0 +1,29 @@
package com.multictrl.modules.business.q20.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* Q20航线任务 - 机库备降点DTO
*
* @author 938693313@qq.com
* @since 1.0.0 2026/5/20
*/
@Data
@Schema(description = "机库备降点信息")
public class Q20DockAlternatePointDTO {
@Schema(description = "经度")
private float longitude;
@Schema(description = "纬度")
private float latitude;
@Schema(description = "高度(m)")
private float altitude;
@Schema(description = "安全转移高度(m)")
@JsonProperty("safe_height")
private float safeHeight;
}

View File

@ -0,0 +1,48 @@
package com.multictrl.modules.business.q20.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* Q20航线任务 - 机库信息条目DTO用于route_execute
*
* @author 938693313@qq.com
* @since 1.0.0 2026/5/20
*/
@Data
@Schema(description = "机库信息条目")
public class Q20DockInfoItemDTO {
@Schema(description = "机库序号")
private int index;
@Schema(description = "机库类型takeoff=起飞点landing=降落点")
private String type;
@Schema(description = "机库/客户端设备序列号")
private String sn;
@Schema(description = "降落平台识别码")
private String code;
@Schema(description = "经度精确到7位小数")
private float longitude;
@Schema(description = "纬度精确到7位小数")
private float latitude;
@Schema(description = "椭球高度(EGM96)(m)")
private float altitude;
@Schema(description = "朝向角(deg)")
private float heading;
@Schema(description = "是否已设置备降点0=未设置1=已设置")
@JsonProperty("alternate_set")
private int alternateSet;
@Schema(description = "备降点信息")
@JsonProperty("alternate_point")
private Q20DockAlternatePointDTO alternatePoint;
}

View File

@ -0,0 +1,27 @@
package com.multictrl.modules.business.q20.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* Q20虚拟摇杆指令DTO高频指令不等回复
*
* @author 938693313@qq.com
* @since 1.0.0 2026/5/20
*/
@Data
@Schema(name = "Q20虚拟摇杆参数")
public class Q20DrcDTO {
@Schema(description = "X轴速度(m/s)")
private float vx;
@Schema(description = "Y轴速度(m/s)")
private float vy;
@Schema(description = "Z轴速度(m/s)")
private float vz;
@Schema(description = "偏航角速度(deg/s)")
private float vyaw;
}

View File

@ -0,0 +1,18 @@
package com.multictrl.modules.business.q20.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* Q20紧急停止DTO
*
* @author 938693313@qq.com
* @since 1.0.0 2026/5/20
*/
@Data
@Schema(name = "Q20紧急停止参数")
public class Q20EmergencyStopDTO {
@Schema(description = "OTP验证码执行紧急停止前通过接口获取")
private String otp;
}

View File

@ -0,0 +1,37 @@
package com.multictrl.modules.business.q20.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
/**
* Q20航线任务 - 围栏条目DTO
*
* @author 938693313@qq.com
* @since 1.0.0 2026/5/20
*/
@Data
@Schema(description = "围栏条目信息")
public class Q20GeofenceItemDTO {
@Schema(description = "围栏类型polygon=多边形围栏circle=圆形围栏")
@JsonProperty("geofence_type")
private String geofenceType;
@Schema(description = "围栏方向0=围栏内1=围栏外")
private int inclusion;
@Schema(description = "多边形围栏顶点经纬度列表,每项格式:\"经度,纬度\"")
@JsonProperty("vertex_coordinates")
private List<String> vertexCoordinates;
@Schema(description = "圆形围栏半径(m)")
@JsonProperty("circle_radius")
private Integer circleRadius;
@Schema(description = "圆形围栏中心坐标,格式:\"经度,纬度\"")
@JsonProperty("circle_coordinate")
private String circleCoordinate;
}

View File

@ -0,0 +1,22 @@
package com.multictrl.modules.business.q20.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
/**
* Q20航线任务 - 上传围栏DTO
*
* @author 938693313@qq.com
* @since 1.0.0 2026/5/20
*/
@Data
@Schema(description = "上传围栏请求体")
public class Q20GeofenceUploadDTO {
@Schema(description = "围栏信息列表")
@JsonProperty("geofence_info")
private List<Q20GeofenceItemDTO> geofenceInfo;
}

View File

@ -0,0 +1,30 @@
package com.multictrl.modules.business.q20.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* Q20云台控制DTO
*
* @author 938693313@qq.com
* @since 1.0.0 2026/5/20
*/
@Data
@Schema(name = "Q20云台控制参数")
public class Q20GimbalControlDTO {
@Schema(description = "设备id, JSON key: device_id")
private long deviceId;
@Schema(description = "控制模式: 0=速度模式, 1=绝对值模式")
private int mode;
@Schema(description = "横滚角(deg)")
private float roll;
@Schema(description = "偏航角(deg), 默认0, 顺时针为正")
private float yaw;
@Schema(description = "俯仰角(deg), 默认0, 向上为正向下为负")
private float pitch;
}

View File

@ -0,0 +1,47 @@
package com.multictrl.modules.business.q20.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* Q20返航指令DTO
*
* @author 938693313@qq.com
* @since 1.0.0 2026/5/20
*/
@Data
@Schema(name = "Q20返航参数")
public class Q20GoHomeDTO {
@Schema(description = "返航模式: 1=安全高度返回, 2=直线飞行, 3=原路返回")
private int mode;
@Schema(description = "安全返航高度(m), mode=1时有效, JSON key: safe_altitude")
private Float safeAltitude;
@Schema(description = "返航速度(m/s), mode=1时有效")
private Float speed;
@Schema(description = "是否指定返回点: 1=是, 0=否, JSON key: specific_home")
private Integer specificHome;
@Schema(description = "指定返回点坐标, specific_home=1时有效, JSON key: home_point")
private HomePoint homePoint;
@Data
@Schema(name = "返航目标点")
public static class HomePoint {
@Schema(description = "经度(7位小数)")
private float longitude;
@Schema(description = "纬度(7位小数)")
private float latitude;
@Schema(description = "椭球高度EGM96(m)")
private float altitude;
@Schema(description = "航向角(deg)")
private float heading;
}
}

View File

@ -0,0 +1,27 @@
package com.multictrl.modules.business.q20.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* Q20绞车控制DTO
*
* @author 938693313@qq.com
* @since 1.0.0 2026/5/20
*/
@Data
@Schema(name = "Q20绞车控制参数")
public class Q20HalyardingDTO {
@Schema(description = "设备id, JSON key: device_id")
private long deviceId;
@Schema(description = "绞车动作")
private int action;
@Schema(description = "绳子位置, JSON key: rope_position")
private int ropePosition;
@Schema(description = "绳子状态, JSON key: rope_status")
private int ropeStatus;
}

View File

@ -0,0 +1,33 @@
package com.multictrl.modules.business.q20.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* Q20图像切换DTO
*
* @author 938693313@qq.com
* @since 1.0.0 2026/5/20
*/
@Data
@Schema(name = "Q20图像切换参数")
public class Q20ImageSwitchDTO {
@Schema(description = "设备id, JSON key: device_id")
private long deviceId;
@Schema(description = "激光开关: 0=关, 1=开, JSON key: laser_enable")
private int laserEnable;
@Schema(description = "可见光开关: 0=关, 1=开, JSON key: visible_enable")
private int visibleEnable;
@Schema(description = "红外开关: 0=关, 1=开, JSON key: ir_enable")
private int irEnable;
@Schema(description = "红外类型: 0=白热, 1=黑热, 2=彩色, 3=辐射野, 4=辉煌红, JSON key: ir_type")
private int irType;
@Schema(description = "流类型: 0=VISIBLE_ONLY, 1=IR_ONLY, 2=IR_IN_VISIBLE, 3=VISIBLE_IN_IR, 4=VISIBLE_WITH_IR, 5=NIGHT_ONLY, JSON key: stream_type")
private int streamType;
}

View File

@ -0,0 +1,21 @@
package com.multictrl.modules.business.q20.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* Q20降落指令DTO
*
* @author 938693313@qq.com
* @since 1.0.0 2026/5/20
*/
@Data
@Schema(name = "Q20降落参数")
public class Q20LandDTO {
@Schema(description = "降落模式: 0=直接降落, 1=精准降落")
private int mode;
@Schema(description = "精准降落二维码号(mode=1时有效), JSON key: qr_code")
private int qrCode;
}

View File

@ -0,0 +1,33 @@
package com.multictrl.modules.business.q20.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* Q20直播开始DTO
*
* @author 938693313@qq.com
* @since 1.0.0 2026/5/20
*/
@Data
@Schema(name = "Q20直播开始参数")
public class Q20LiveStartDTO {
@Schema(description = "负载索引: fpv=-1, 前向=0, 前向=1, 下向=2, JSON key: payload_index")
private int payloadIndex;
@Schema(description = "推流协议类型: 0=Agora, 1=RTMP, 2=RTSP, 3=GB28181, 4=WebRTC, JSON key: url_type")
private int urlType;
@Schema(description = "直播URL地址")
private String url;
@Schema(description = "码流类型: 0=主码流, 1=辅码流")
private int video;
@Schema(description = "视频ID, 格式: {sn}/{camera_index}/{video_index}, JSON key: video_id")
private String videoId;
@Schema(description = "视频类型: ir/wide/zoom, JSON key: video_type")
private String videoType;
}

View File

@ -0,0 +1,21 @@
package com.multictrl.modules.business.q20.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* Q20日志数据上传DTO
*
* @author 938693313@qq.com
* @since 1.0.0 2026/5/20
*/
@Data
@Schema(name = "Q20日志数据参数")
public class Q20LogDataDTO {
@Schema(description = "日志ID")
private int id;
@Schema(description = "OSS上传URL")
private String url;
}

View File

@ -0,0 +1,21 @@
package com.multictrl.modules.business.q20.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* Q20日志列表查询DTO
*
* @author 938693313@qq.com
* @since 1.0.0 2026/5/20
*/
@Data
@Schema(name = "Q20日志列表参数")
public class Q20LogListDTO {
@Schema(description = "日志起始位置")
private int start;
@Schema(description = "日志结束位置")
private int end;
}

View File

@ -0,0 +1,26 @@
package com.multictrl.modules.business.q20.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
/**
* Q20挂载TDA数据DTO
*
* @author 938693313@qq.com
* @since 1.0.0 2026/5/20
*/
@Data
@Schema(name = "Q20挂载TDA参数")
public class Q20MountTdaDTO {
@Schema(description = "负载索引(仅支持deviceID为68的负载), JSON key: payload_index")
private int payloadIndex;
@Schema(description = "TDA数据长度, JSON key: tda_length")
private int tdaLength;
@Schema(description = "TDA数据内容(int8[])")
private List<Integer> tda;
}

View File

@ -0,0 +1,33 @@
package com.multictrl.modules.business.q20.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* Q20航点飞行DTO
*
* @author 938693313@qq.com
* @since 1.0.0 2026/5/20
*/
@Data
@Schema(name = "Q20航点飞行参数")
public class Q20NavigateDTO {
@Schema(description = "目标经度")
private float longitude;
@Schema(description = "目标纬度")
private float latitude;
@Schema(description = "目标高度(m)")
private float altitude;
@Schema(description = "飞行速度(m/s)")
private float speed;
@Schema(description = "到达后动作: noAction(悬停,默认)/land(降落)/precland(精准降落)")
private String action;
@Schema(description = "精准降落二维码号, JSON key: qr_code")
private Integer qrCode;
}

View File

@ -0,0 +1,18 @@
package com.multictrl.modules.business.q20.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* Q20开伞DTO
*
* @author 938693313@qq.com
* @since 1.0.0 2026/5/20
*/
@Data
@Schema(name = "Q20开伞参数")
public class Q20OpenParachuteDTO {
@Schema(description = "OTP验证码\"99\"时系统视为强制开伞")
private String otp;
}

View File

@ -0,0 +1,36 @@
package com.multictrl.modules.business.q20.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* Q20 OTA升级DTO
*
* @author 938693313@qq.com
* @since 1.0.0 2026/5/20
*/
@Data
@Schema(name = "Q20 OTA升级参数")
public class Q20OtaUpgradeDTO {
@Schema(description = "设备序列号")
private String sn;
@Schema(description = "设备型号, 如 Q20")
private String model;
@Schema(description = "固件包URL")
private String url;
@Schema(description = "固件包MD5")
private String md5;
@Schema(description = "固件包名称, JSON key: package_name")
private String packageName;
@Schema(description = "固件版本号")
private String version;
@Schema(description = "版本说明")
private String amend;
}

View File

@ -0,0 +1,70 @@
package com.multictrl.modules.business.q20.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* Q20批量参数设置set_params
* 所有字段均可选null字段不发送给设备
*
* @author 938693313@qq.com
* @since 1.0.0 2026/5/20
*/
@Data
@Schema(name = "Q20批量参数")
public class Q20ParamsDTO {
@Schema(description = "低电阈值比例如0.3=30%")
private Float batteryLowThreshold;
@Schema(description = "严重低电阈值(比例)")
private Float batteryCriticalThreshold;
@Schema(description = "紧急低电阈值(比例)")
private Float batteryEmergencyThreshold;
@Schema(description = "低电动作: warning(警告) / landing(降落) / goBackCritAndLandEmerg(低电返航·紧急降落)")
private String lowBatteryAction;
@Schema(description = "高度限制(m)0=无限制")
private Float heightLimit;
@Schema(description = "距离限制(m)0=无限制")
private Float distanceLimit;
@Schema(description = "围栏动作: goContinue/warning/hover/goBack/terminate/landing")
private String geofenceAction;
@Schema(description = "返航高度(m)相对home点高度")
private Float returnHeight;
@Schema(description = "返航速度(m/s)")
private Float returnSpeed;
@Schema(description = "返航类型: directReturn(直接返航) / originalReturn(原路返航)")
private String returnType;
@Schema(description = "避障开关: 0=关闭 1=开启")
private Integer obstacleAvoidanceEnable;
@Schema(description = "避障距离(m)")
private Float obstacleAvoidanceDistance;
@Schema(description = "视觉传感器开关: 0=关闭 1=开启")
private Integer visualSensorEnable;
@Schema(description = "精准降落二维码标识")
private Integer aprilTagId;
@Schema(description = "精准降落模式: noPrecisionLanding / opportunisticPrecisionLanding / requiredPrecisionLanding")
private String precisionLandMode;
@Schema(description = "数据链路丢失动作: goContinue/hover/goBack/landing/terminate/disarm")
private String dataLinkLossAction;
@Schema(description = "RC信号丢失动作: goContinue/hover/goBack/landing/terminate/disarm")
private String rcLossAction;
@Schema(description = "降落伞安全开关: 0=关闭 1=开启")
private Integer parachuteSafetyEnable;
}

View File

@ -0,0 +1,30 @@
package com.multictrl.modules.business.q20.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* Q20查询状态DTO
*
* @author 938693313@qq.com
* @since 1.0.0 2026/5/20
*/
@Data
@Schema(name = "Q20查询状态参数")
public class Q20QueryStatusDTO {
@Schema(description = "查询类型: liftoff/arrival等, JSON key: query_value")
private String queryValue;
@Schema(description = "业务ID")
private String bid;
@Schema(description = "类型: waypoint/command等")
private String type;
@Schema(description = "航线标识")
private String wayline;
@Schema(description = "状态码")
private String code;
}

View File

@ -0,0 +1,30 @@
package com.multictrl.modules.business.q20.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.Map;
/**
* Q20航线任务 - 动作DTO
*
* @author 938693313@qq.com
* @since 1.0.0 2026/5/20
*/
@Data
@Schema(description = "航点动作")
public class Q20RouteActionDTO {
@Schema(description = "动作ID范围[0,65535]")
@JsonProperty("action_id")
private int actionId;
@Schema(description = "动作执行函数takePhoto/startRecord/stopRecord/focus/zoom/customDirName/gimbalRotate/rotateYaw/hover/gimbalEvenlyRotate/orientedShoot/panoShot/recordPointCloud")
@JsonProperty("action_actuator_func")
private String actionActuatorFunc;
@Schema(description = "动作执行函数参数,参数结构因动作类型而异")
@JsonProperty("action_actuator_func_param")
private Map<String, Object> actionActuatorFuncParam;
}

View File

@ -0,0 +1,37 @@
package com.multictrl.modules.business.q20.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
/**
* Q20航线任务 - 动作组DTO
*
* @author 938693313@qq.com
* @since 1.0.0 2026/5/20
*/
@Data
@Schema(description = "航点动作组")
public class Q20RouteActionGroupDTO {
@Schema(description = "动作组ID范围[0,65535]")
@JsonProperty("action_group_id")
private int actionGroupId;
@Schema(description = "动作组起始航点索引")
@JsonProperty("action_group_start_index")
private Integer actionGroupStartIndex;
@Schema(description = "动作组结束航点索引")
@JsonProperty("action_group_end_index")
private Integer actionGroupEndIndex;
@Schema(description = "动作组执行模式")
@JsonProperty("action_group_mode")
private String actionGroupMode;
@Schema(description = "动作列表")
private List<Q20RouteActionDTO> action;
}

View File

@ -0,0 +1,23 @@
package com.multictrl.modules.business.q20.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* Q20航线任务 - 备降点DTO
*
* @author 938693313@qq.com
* @since 1.0.0 2026/5/20
*/
@Data
@Schema(description = "备降点信息")
public class Q20RouteAlternatePointDTO {
@Schema(description = "备降点坐标")
private Q20RoutePointDTO point;
@Schema(description = "备降点高度(m)")
@JsonProperty("alternate_height")
private Float alternateHeight;
}

View File

@ -0,0 +1,28 @@
package com.multictrl.modules.business.q20.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* Q20航线任务 - 一键飞行DTOroute_auto = 上传 + 执行
*
* @author 938693313@qq.com
* @since 1.0.0 2026/5/20
*/
@Data
@Schema(description = "一键飞行请求体route_auto = 上传 + 执行)")
public class Q20RouteAutoDTO {
@Schema(description = "航线信息(上传参数)")
@JsonProperty("route_info")
private Q20RouteUploadDTO routeInfo;
@Schema(description = "执行信息(执行参数)")
@JsonProperty("execute_info")
private Q20RouteExecuteDTO executeInfo;
@Schema(description = "飞机配置信息")
@JsonProperty("vehicle_config")
private Q20RouteVehicleConfigDTO vehicleConfig;
}

View File

@ -0,0 +1,24 @@
package com.multictrl.modules.business.q20.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* Q20航线任务 - 无人机信息DTO
*
* @author 938693313@qq.com
* @since 1.0.0 2026/5/20
*/
@Data
@Schema(description = "无人机信息")
public class Q20RouteDroneInfoDTO {
@Schema(description = "无人机枚举值20=Q20")
@JsonProperty("drone_enum_value")
private int droneEnumValue;
@Schema(description = "无人机子枚举值0=Q20, 1=Q20Y, 2=Q20N")
@JsonProperty("drone_sub_enum_value")
private int droneSubEnumValue;
}

View File

@ -0,0 +1,33 @@
package com.multictrl.modules.business.q20.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
/**
* Q20航线任务 - 执行航线DTOroute_execute
*
* @author 938693313@qq.com
* @since 1.0.0 2026/5/20
*/
@Data
@Schema(description = "执行航线请求体route_execute")
public class Q20RouteExecuteDTO {
@Schema(description = "执行模式1=执行最近一次上传航线2=执行指定route_id的航线")
private int mode;
@Schema(description = "航线IDmode=2时有效")
@JsonProperty("route_id")
private String routeId;
@Schema(description = "是否指定机库0=未设置1=已设置")
@JsonProperty("specific_dock")
private int specificDock;
@Schema(description = "机库信息列表")
@JsonProperty("dock_info")
private List<Q20DockInfoItemDTO> dockInfo;
}

View File

@ -0,0 +1,41 @@
package com.multictrl.modules.business.q20.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
/**
* Q20航线任务 - 航线文件夹DTO包含所有航点
*
* @author 938693313@qq.com
* @since 1.0.0 2026/5/20
*/
@Data
@Schema(description = "航线文件夹,包含所有航点信息")
public class Q20RouteFolderDTO {
@Schema(description = "模板ID默认0")
@JsonProperty("template_id")
private Integer templateId;
@Schema(description = "航线ID默认0")
@JsonProperty("wayline_id")
private Integer waylineId;
@Schema(description = "全局飞行速度(m/s),范围[1,15]")
@JsonProperty("auto_flight_speed")
private float autoFlightSpeed;
@Schema(description = "执行高度模式WGS84 / relativeToStartPoint / realTimeFollowSurface")
@JsonProperty("execute_height_mode")
private String executeHeightMode;
@Schema(description = "航点列表")
private List<Q20RoutePlacemarkDTO> placemark;
@Schema(description = "备降点列表")
@JsonProperty("alternate_points")
private List<Q20RouteAlternatePointDTO> alternatePoints;
}

View File

@ -0,0 +1,64 @@
package com.multictrl.modules.business.q20.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* Q20航线任务 - 任务配置DTO
*
* @author 938693313@qq.com
* @since 1.0.0 2026/5/20
*/
@Data
@Schema(description = "任务配置信息")
public class Q20RouteMissionConfigDTO {
@Schema(description = "飞向航线起点模式safely=安全模式 / pointToPoint=斜线直达模式")
@JsonProperty("fly_to_wayline_mode")
private String flyToWaylineMode;
@Schema(description = "完成动作goHome / noAction / autoLand / preLand / gotoFirstWaypoint")
@JsonProperty("finish_action")
private String finishAction;
@Schema(description = "完成动作附加值finishAction=preLand时为精准降落标识格式{hangar_sn}-{dock_code}")
@JsonProperty("action_value")
private String actionValue;
@Schema(description = "返航类型directReturn / originalReturn")
@JsonProperty("go_home_type")
private String goHomeType;
@Schema(description = "失联时退出航线goContinue / executeLostAction")
@JsonProperty("exit_on_rc_lost")
private String exitOnRcLost;
@Schema(description = "失联执行动作goBack / landing / hover")
@JsonProperty("execute_rc_lost_action")
private String executeRcLostAction;
@Schema(description = "安全起飞高度(m),范围[2,1500]")
@JsonProperty("takeoff_security_height")
private Float takeoffSecurityHeight;
@Schema(description = "全局过渡速度(m/s)")
@JsonProperty("global_transitional_speed")
private Float globalTransitionalSpeed;
@Schema(description = "全局返航高度(m)")
@JsonProperty("global_RTH_height")
private Float globalRthHeight;
@Schema(description = "降落点海拔高度(m)")
@JsonProperty("land_height")
private Float landHeight;
@Schema(description = "无人机信息")
@JsonProperty("drone_info")
private Q20RouteDroneInfoDTO droneInfo;
@Schema(description = "挂载信息")
@JsonProperty("payload_info")
private Q20RoutePayloadInfoDTO payloadInfo;
}

View File

@ -0,0 +1,24 @@
package com.multictrl.modules.business.q20.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* Q20航线任务 - 挂载信息DTO
*
* @author 938693313@qq.com
* @since 1.0.0 2026/5/20
*/
@Data
@Schema(description = "挂载信息")
public class Q20RoutePayloadInfoDTO {
@Schema(description = "挂载枚举值30=ZT30, 31=ZR30, 40=ZT40")
@JsonProperty("payload_enum_value")
private int payloadEnumValue;
@Schema(description = "挂载位置索引0=1号挂载位置")
@JsonProperty("payload_position_index")
private int payloadPositionIndex;
}

View File

@ -0,0 +1,54 @@
package com.multictrl.modules.business.q20.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* Q20航线任务 - 航点DTO
*
* @author 938693313@qq.com
* @since 1.0.0 2026/5/20
*/
@Data
@Schema(description = "单个航点信息")
public class Q20RoutePlacemarkDTO {
@Schema(description = "航点序号从0开始")
private int index;
@Schema(description = "是否危险点0=安全1=危险")
@JsonProperty("is_risky")
private Integer isRisky;
@Schema(description = "航点坐标")
private Q20RoutePointDTO point;
@Schema(description = "执行高度(m)")
@JsonProperty("execute_height")
private float executeHeight;
@Schema(description = "是否使用全局速度0=不使用全局速度1=使用全局速度")
@JsonProperty("use_global_speed")
private Integer useGlobalSpeed;
@Schema(description = "航点飞行速度(m/s)useGlobalSpeed=0时有效")
@JsonProperty("waypoint_speed")
private Float waypointSpeed;
@Schema(description = "偏航角参数")
@JsonProperty("waypoint_heading_param")
private Q20RouteWaypointHeadingDTO waypointHeadingParam;
@Schema(description = "转弯参数")
@JsonProperty("waypoint_turn_param")
private Q20RouteWaypointTurnDTO waypointTurnParam;
@Schema(description = "是否飞直线0=曲线1=直线")
@JsonProperty("use_straight_line")
private Integer useStraightLine;
@Schema(description = "动作组")
@JsonProperty("action_group")
private Q20RouteActionGroupDTO actionGroup;
}

View File

@ -0,0 +1,18 @@
package com.multictrl.modules.business.q20.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* Q20航线任务 - 坐标点DTO
*
* @author 938693313@qq.com
* @since 1.0.0 2026/5/20
*/
@Data
@Schema(description = "坐标点,格式:经度,纬度")
public class Q20RoutePointDTO {
@Schema(description = "坐标,格式:\"经度,纬度\",如 \"113.123,23.456\"")
private String coordinates;
}

View File

@ -0,0 +1,45 @@
package com.multictrl.modules.business.q20.dto;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import static com.fasterxml.jackson.annotation.JsonProperty.Access.WRITE_ONLY;
/**
* Q20航线任务 - 上传航线顶层DTOroute_upload
*
* @author 938693313@qq.com
* @since 1.0.0 2026/5/20
*/
@Data
@Schema(description = "上传航线请求体route_upload")
public class Q20RouteUploadDTO {
@Schema(description = "航线名称(仅本地存储用,不下发给设备)")
@JsonIgnore
private String routeName;
@Schema(description = "全局飞行高度(m),仅本地存储用,不下发给设备")
@JsonProperty(value = "flight_height", access = WRITE_ONLY)
private Float flightHeight;
@Schema(description = "是否精准导入,仅本地存储用,不下发给设备")
@JsonProperty(value = "accurate_import", access = WRITE_ONLY)
private Boolean accurateImport;
@Schema(description = "航线ID存储用")
private String wayline;
@Schema(description = "任务配置信息")
@JsonProperty("mission_config")
private Q20RouteMissionConfigDTO missionConfig;
@Schema(description = "航线文件夹,包含所有航点信息")
private Q20RouteFolderDTO folder;
@Schema(description = "飞机配置信息")
@JsonProperty("vehicle_config")
private Q20RouteVehicleConfigDTO vehicleConfig;
}

View File

@ -0,0 +1,28 @@
package com.multictrl.modules.business.q20.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* Q20航线任务 - 飞机配置DTO
*
* @author 938693313@qq.com
* @since 1.0.0 2026/5/20
*/
@Data
@Schema(description = "飞机配置信息")
public class Q20RouteVehicleConfigDTO {
@Schema(description = "返航类型directReturn / originalReturn")
@JsonProperty("go_home_type")
private String goHomeType;
@Schema(description = "全局返航高度(m)")
@JsonProperty("global_RTH_height")
private Float globalRthHeight;
@Schema(description = "低电动作warning / landing / goBackCritAndLandEmerg")
@JsonProperty("low_battery_action")
private String lowBatteryAction;
}

View File

@ -0,0 +1,32 @@
package com.multictrl.modules.business.q20.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* Q20航线任务 - 航点偏航角参数DTO
*
* @author 938693313@qq.com
* @since 1.0.0 2026/5/20
*/
@Data
@Schema(description = "航点偏航角参数")
public class Q20RouteWaypointHeadingDTO {
@Schema(description = "偏航角模式followWayline/manually/fixed/smoothTransition/towardPOI")
@JsonProperty("waypoint_heading_mode")
private String waypointHeadingMode;
@Schema(description = "偏航角(deg)")
@JsonProperty("waypoint_heading_angle")
private Float waypointHeadingAngle;
@Schema(description = "POI点坐标 \"[lng,lat,alt]\"towardPOI模式时有效")
@JsonProperty("waypoint_poi_point")
private String waypointPoiPoint;
@Schema(description = "偏航角路径模式clockwise顺时针/ counterClockwise逆时针")
@JsonProperty("waypoint_heading_path_mode")
private String waypointHeadingPathMode;
}

View File

@ -0,0 +1,24 @@
package com.multictrl.modules.business.q20.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* Q20航线任务 - 航点转弯参数DTO
*
* @author 938693313@qq.com
* @since 1.0.0 2026/5/20
*/
@Data
@Schema(description = "航点转弯参数")
public class Q20RouteWaypointTurnDTO {
@Schema(description = "转弯模式coordinateTurn / toPointAndStopWithDiscontinuityCurvature / toPointAndStopWithContinuityCurvature / toPointAndPassWithContinuityCurvature")
@JsonProperty("waypoint_turn_mode")
private String waypointTurnMode;
@Schema(description = "转弯截距(m)")
@JsonProperty("waypoint_turn_damping_dist")
private Float waypointTurnDampingDist;
}

View File

@ -0,0 +1,30 @@
package com.multictrl.modules.business.q20.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* Q20设置返航点DTO
*
* @author 938693313@qq.com
* @since 1.0.0 2026/5/20
*/
@Data
@Schema(name = "Q20设置返航点参数")
public class Q20SetHomeDTO {
@Schema(description = "经度")
private float longitude;
@Schema(description = "纬度")
private float latitude;
@Schema(description = "椭球高度(m)")
private float altitude;
@Schema(description = "航向角(deg)")
private float heading;
@Schema(description = "true=使用当前GPS位置为home, JSON key: current_gps")
private Boolean currentGps;
}

View File

@ -0,0 +1,21 @@
package com.multictrl.modules.business.q20.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* Q20起飞指令DTO
*
* @author 938693313@qq.com
* @since 1.0.0 2026/5/20
*/
@Data
@Schema(name = "Q20起飞参数")
public class Q20TakeoffDTO {
@Schema(description = "高度模式: 0=绝对高度, 1=相对高度(默认)")
private int mode;
@Schema(description = "起飞高度(m)")
private float altitude;
}

View File

@ -0,0 +1,21 @@
package com.multictrl.modules.business.q20.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* Q20投掷锁/解锁DTO
*
* @author 938693313@qq.com
* @since 1.0.0 2026/5/20
*/
@Data
@Schema(name = "Q20投掷控制参数")
public class Q20ThrowingDTO {
@Schema(description = "设备id, JSON key: device_id")
private long deviceId;
@Schema(description = "操作值: 0=解锁, 1=锁定")
private int value;
}

View File

@ -15,6 +15,9 @@ public interface Q20Constant {
String METHOD_MOUNT = "mount";
String METHOD_BATTERY = "battery";
String METHOD_OBS = "obs";
// Q20 status topic method字段值sys/device/{sn}/status
String METHOD_ONLINE = "online";
String METHOD_HEARTBEAT = "heartbeat";
// Q20 state topic method字段值
String METHOD_VERSION = "version";
String METHOD_NOTIFY = "notify";
@ -44,4 +47,10 @@ public interface Q20Constant {
String Q20_ARRIVAL = "q20_arrival_";
String Q20_ROUTE_EXECUTE = "q20_route_execute_";
String Q20_ROUTE_AUTO = "q20_route_auto_";
String Q20_ONLINE = "q20_online_";
// sys/device/ status topic前缀
String Q20_SYS_TOPIC_PREFIX = "sys/device/";
String STATUS_REPLY_SUFFIX = "_reply";
long ONLINE_TTL_MS = 35_000L;
}

View File

@ -0,0 +1,97 @@
package com.multictrl.modules.business.q20.handler;
import cn.hutool.json.JSONObject;
import com.multictrl.common.constant.BusinessConstant;
import com.multictrl.common.utils.CacheUtils;
import com.multictrl.common.utils.JsonUtils;
import com.multictrl.modules.business.handler.MessageHandler;
import com.multictrl.modules.business.service.MqttPushService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
* Q20设备拓扑状态处理topic: sys/device/{device_sn}/status
* method: online(设备上线) | heartbeat(心跳维持在线)
* 两种消息均需回复 sys/device/{device_sn}/status_reply
* 35s内无消息则缓存过期视为设备离线
*
* @author 938693313@qq.com
* @since 1.0.0 2026/5/20
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class Q20StatusHandler implements MessageHandler {
private final MqttPushService mqttPushService;
@Override
public void handleMessage(String topic, String payload, String gateway) {
JSONObject message = JsonUtils.parseObject(payload, JSONObject.class);
if (message == null) {
log.debug("Q20 status --> payload解析失败解析后为null");
return;
}
String deviceSn = message.getStr(BusinessConstant.GATEWAY);
String method = message.getStr(BusinessConstant.METHOD);
if (method == null) {
log.debug("Q20 status --> method字段缺失, topic: {}", topic);
return;
}
switch (method) {
case Q20Constant.METHOD_ONLINE:
handleOnline(topic, message, deviceSn);
break;
case Q20Constant.METHOD_HEARTBEAT:
handleHeartbeat(topic, message, deviceSn);
break;
default:
log.debug("Q20 status --> 未知method: {}, deviceSn: {}", method, deviceSn);
}
}
/**
* 设备上线刷新在线缓存回复 status_replyoutput为空对象
*/
private void handleOnline(String topic, JSONObject message, String deviceSn) {
CacheUtils.set(Q20Constant.Q20_ONLINE + deviceSn, true, Q20Constant.ONLINE_TTL_MS);
log.info("Q20 status --> 设备上线, deviceSn: {}", deviceSn);
Integer needReply = message.getInt(BusinessConstant.NEED_REPLY);
if (needReply != null && needReply == 1) {
sendReply(topic, message, new JSONObject());
}
}
/**
* 心跳刷新在线缓存TTL回复 status_replyoutput含uplink上行延迟ms
*/
private void handleHeartbeat(String topic, JSONObject message, String deviceSn) {
CacheUtils.set(Q20Constant.Q20_ONLINE + deviceSn, true, Q20Constant.ONLINE_TTL_MS);
log.debug("Q20 status --> 心跳, deviceSn: {}", deviceSn);
Integer needReply = message.getInt(BusinessConstant.NEED_REPLY);
if (needReply != null && needReply == 1) {
long uplink = 0;
Long timestamp = message.getLong("timestamp");
if (timestamp != null && timestamp > 0) {
uplink = Math.max(0, System.currentTimeMillis() - timestamp);
}
JSONObject output = new JSONObject();
output.set("uplink", uplink);
sendReply(topic, message, output);
}
}
private void sendReply(String topic, JSONObject message, JSONObject output) {
JSONObject data = new JSONObject();
data.set("result", 0);
data.set("output", output);
message.set(BusinessConstant.DATA, data);
message.set("timestamp", System.currentTimeMillis());
message.remove(BusinessConstant.NEED_REPLY);
String replyTopic = topic + Q20Constant.STATUS_REPLY_SUFFIX;
mqttPushService.pushMessageByClient1(replyTopic, message.toString());
log.debug("Q20 status --> 已回复, topic: {}", replyTopic);
}
}

View File

@ -0,0 +1,107 @@
package com.multictrl.modules.business.q20.service;
import cn.hutool.json.JSONObject;
import com.multictrl.modules.business.q20.dto.*;
/**
* Q20控制指令服务接口
*
* @author 938693313@qq.com
* @since 1.0.0 2026/5/20
*/
public interface Q20CommandService {
// ==================== 飞行控制 ====================
String takeoff(String deviceSn, Q20TakeoffDTO dto);
String land(String deviceSn, Q20LandDTO dto);
String stop(String deviceSn);
String goHome(String deviceSn, Q20GoHomeDTO dto);
String setHome(String deviceSn, Q20SetHomeDTO dto);
String navigate(String deviceSn, Q20NavigateDTO dto);
// ==================== 云台相机 ====================
String gimbalControl(String deviceSn, Q20GimbalControlDTO dto);
String mountTda(String deviceSn, Q20MountTdaDTO dto);
String cameraPhotoTake(String deviceSn, Q20CameraDTO dto);
String cameraRecordingStart(String deviceSn, Q20CameraDTO dto);
String cameraRecordingStop(String deviceSn, Q20CameraDTO dto);
String cameraFocalLengthSet(String deviceSn, Q20CameraFocalLengthDTO dto);
// ==================== 直播 ====================
String liveStartPush(String deviceSn, Q20LiveStartDTO dto);
String liveStopPush(String deviceSn, int payloadIndex, int video);
String imageSwitch(String deviceSn, Q20ImageSwitchDTO dto);
// ==================== 设备控制 ====================
String deviceCharge(String deviceSn, int value, int version);
String deviceBoot(String deviceSn, int value);
String safetySwitch(String deviceSn, int value);
// ==================== 负载 ====================
String throwingLock(String deviceSn, Q20ThrowingDTO dto);
String throwingExecution(String deviceSn, long deviceId);
String halyarding(String deviceSn, Q20HalyardingDTO dto);
String logistics(String deviceSn, int value, boolean force);
// ==================== 虚拟摇杆高频不等回复 ====================
void drc(String deviceSn, Q20DrcDTO dto);
// ==================== 安全/降落伞 ====================
String setAutoParachute(String deviceSn, int value);
JSONObject getEmergencyStopOtp(String deviceSn);
String emergencyStop(String deviceSn, Q20EmergencyStopDTO dto);
JSONObject getParachuteOtp(String deviceSn);
String openParachute(String deviceSn, Q20OpenParachuteDTO dto);
String confirmParachuteSafety(String deviceSn, int value);
// ==================== 状态/信息 ====================
JSONObject queryStatus(String deviceSn, Q20QueryStatusDTO dto);
JSONObject getConfig(String deviceSn);
String stereoImage(String deviceSn, int value);
JSONObject version(String deviceSn);
// ==================== 日志 ====================
JSONObject logCount(String deviceSn);
JSONObject logList(String deviceSn, Q20LogListDTO dto);
String logData(String deviceSn, Q20LogDataDTO dto);
// ==================== OTA ====================
String otaUpgrade(String deviceSn, Q20OtaUpgradeDTO dto);
}

View File

@ -0,0 +1,18 @@
package com.multictrl.modules.business.q20.service;
import com.multictrl.modules.business.q20.vo.Q20DeviceStatusVO;
import java.util.List;
/**
* Q20飞机设备服务
*
* @author 938693313@qq.com
* @since 1.0.0 2026/5/20
*/
public interface Q20DeviceService {
Q20DeviceStatusVO getOnlineStatus(String deviceSn);
List<Q20DeviceStatusVO> getOnlineStatusBatch(List<String> deviceSnList);
}

View File

@ -0,0 +1,84 @@
package com.multictrl.modules.business.q20.service;
import cn.hutool.json.JSONObject;
import com.multictrl.modules.business.q20.dto.Q20ParamsDTO;
/**
* Q20参数指令服务
* 通过 thing/device/{sn}/services 主题下发设备回复 services_reply
*
* @author 938693313@qq.com
* @since 1.0.0 2026/5/20
*/
public interface Q20ParamService {
// ==================== 高度/距离限制 ====================
String setHeightLimit(String deviceSn, float value);
Object getHeightLimit(String deviceSn);
String setDistanceLimit(String deviceSn, float value);
Object getDistanceLimit(String deviceSn);
// ==================== 围栏 ====================
String setGeofenceAction(String deviceSn, String value);
Object getGeofenceAction(String deviceSn);
// ==================== 返航 ====================
String setReturnHeight(String deviceSn, float value);
Object getReturnHeight(String deviceSn);
String setReturnType(String deviceSn, String value);
Object getReturnType(String deviceSn);
// ==================== 电池阈值 ====================
String setBatteryLowThreshold(String deviceSn, float value);
Object getBatteryLowThreshold(String deviceSn);
String setBatteryCriticalThreshold(String deviceSn, float value);
Object getBatteryCriticalThreshold(String deviceSn);
String setBatteryEmergencyThreshold(String deviceSn, float value);
Object getBatteryEmergencyThreshold(String deviceSn);
String setLowBatteryAction(String deviceSn, String value);
Object getLowBatteryAction(String deviceSn);
// ==================== 降落 ====================
String setAprilTagId(String deviceSn, int value);
Object getAprilTagId(String deviceSn);
String setPrecisionLandMode(String deviceSn, String value);
Object getPrecisionLandMode(String deviceSn);
// ==================== 避障/传感器 ====================
String setObstacleAvoidanceEnable(String deviceSn, int value);
Object getObstacleAvoidanceEnable(String deviceSn);
String setObstacleAvoidanceDistance(String deviceSn, float value);
Object getObstacleAvoidanceDistance(String deviceSn);
String setVisualSensorEnable(String deviceSn, int value);
Object getVisualSensorEnable(String deviceSn);
// ==================== 其他开关 ====================
String setRtkEnable(String deviceSn, int value);
Object getRtkEnable(String deviceSn);
String setUatMultCtrlEnable(String deviceSn, int value);
Object getUatMultCtrlEnable(String deviceSn);
String setParachuteSafetyEnable(String deviceSn, int value);
Object getParachuteSafetyEnable(String deviceSn);
// ==================== 批量 ====================
String setParams(String deviceSn, Q20ParamsDTO dto);
JSONObject getParams(String deviceSn);
}

View File

@ -0,0 +1,117 @@
package com.multictrl.modules.business.q20.service;
import cn.hutool.json.JSONObject;
import com.multictrl.modules.business.q20.dto.*;
/**
* Q20航线任务服务接口
* topic: thing/device/{device_sn}/services (下行)
* 回复: thing/device/{device_sn}/services_reply (上行)
*
* @author 938693313@qq.com
* @since 1.0.0 2026/5/20
*/
public interface Q20RouteService {
/**
* 获取下一个航线ID根据库中最新Q20航线ID自动递增无记录则返回默认ID
*
* @param type 航线类型route=上传航线(q20_route_)auto=一键航线(q20_auto_)
* @return 下一个可用的航线ID
*/
String nextWayline(String type);
/**
* 上传航线route_upload
*
* @param deviceSn 设备序列号
* @param dto 上传航线数据
* @return 执行结果描述
*/
String routeUpload(String deviceSn, Q20RouteUploadDTO dto);
/**
* 执行航线route_execute
*
* @param deviceSn 设备序列号
* @param dto 执行航线数据
* @return 执行结果描述
*/
String routeExecute(String deviceSn, Q20RouteExecuteDTO dto);
/**
* 一键飞行上传并执行航线route_auto
*
* @param deviceSn 设备序列号
* @param dto 一键飞行数据
* @return 执行结果描述
*/
String routeAuto(String deviceSn, Q20RouteAutoDTO dto);
/**
* 取消悬停break_loop_hover
*
* @param deviceSn 设备序列号
* @return 执行结果描述
*/
String breakLoopHover(String deviceSn);
/**
* 获取航线信息route_info
*
* @param deviceSn 设备序列号
* @param mode 模式1=最近一次2=指定route_id
* @param value mode=2时为route_id否则可传null
* @return 设备返回的航线信息
*/
JSONObject routeInfo(String deviceSn, int mode, String value);
/**
* 获取航线执行进度route_progress
*
* @param deviceSn 设备序列号
* @return 设备返回的执行进度信息
*/
JSONObject routeProgress(String deviceSn);
/**
* 暂停航线route_pause
*
* @param deviceSn 设备序列号
* @return 执行结果描述
*/
String routePause(String deviceSn);
/**
* 继续航线route_resume
*
* @param deviceSn 设备序列号
* @return 执行结果描述
*/
String routeResume(String deviceSn);
/**
* 退出航线route_finish
*
* @param deviceSn 设备序列号
* @return 执行结果描述
*/
String routeFinish(String deviceSn);
/**
* 获取围栏信息geofence_info
*
* @param deviceSn 设备序列号
* @return 设备返回的围栏信息
*/
JSONObject geofenceInfo(String deviceSn);
/**
* 上传围栏geofence_upload
*
* @param deviceSn 设备序列号
* @param dto 围栏上传数据
* @return 执行结果描述
*/
String geofenceUpload(String deviceSn, Q20GeofenceUploadDTO dto);
}

View File

@ -0,0 +1,420 @@
package com.multictrl.modules.business.q20.service.impl;
import cn.hutool.json.JSONObject;
import com.multictrl.common.exception.RenException;
import com.multictrl.modules.business.q20.dto.*;
import com.multictrl.modules.business.q20.service.Q20CommandService;
import com.multictrl.modules.business.service.DJIBaseService;
import com.multictrl.modules.business.service.MqttPushService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/**
* Q20控制指令服务实现
*
* @author 938693313@qq.com
* @since 1.0.0 2026/5/20
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class Q20CommandServiceImpl implements Q20CommandService {
private final DJIBaseService djiBaseService;
private final MqttPushService mqttPushService;
private static final String SERVICES_TOPIC = "thing/device/%s/services";
private String execCmd(String deviceSn, String method, JSONObject data) {
String topic = String.format(SERVICES_TOPIC, deviceSn);
JSONObject payload = djiBaseService.getPayload(method, data);
payload.set("gateway", deviceSn);
return djiBaseService.sendWaitReplyJudgeResult(topic, payload);
}
private JSONObject execCmdGetReply(String deviceSn, String method, JSONObject data) {
String topic = String.format(SERVICES_TOPIC, deviceSn);
JSONObject payload = djiBaseService.getPayload(method, data);
payload.set("gateway", deviceSn);
JSONObject reply = djiBaseService.sendWaitReply(topic, payload);
if (reply == null) {
throw new RenException("设备未响应,请检查设备是否在线");
}
return reply;
}
// ==================== 飞行控制 ====================
@Override
public String takeoff(String deviceSn, Q20TakeoffDTO dto) {
JSONObject data = new JSONObject();
data.set("mode", dto.getMode());
data.set("altitude", dto.getAltitude());
return execCmd(deviceSn, "takeoff", data);
}
@Override
public String land(String deviceSn, Q20LandDTO dto) {
JSONObject data = new JSONObject();
data.set("mode", dto.getMode());
data.set("qr_code", dto.getQrCode());
return execCmd(deviceSn, "land", data);
}
@Override
public String stop(String deviceSn) {
JSONObject data = new JSONObject();
data.set("value", 1);
return execCmd(deviceSn, "stop", data);
}
@Override
public String goHome(String deviceSn, Q20GoHomeDTO dto) {
JSONObject data = new JSONObject();
data.set("mode", dto.getMode());
if (dto.getSafeAltitude() != null) {
data.set("safe_altitude", dto.getSafeAltitude());
}
if (dto.getSpeed() != null) {
data.set("speed", dto.getSpeed());
}
if (dto.getSpecificHome() != null) {
data.set("specific_home", dto.getSpecificHome());
}
if (dto.getHomePoint() != null) {
Q20GoHomeDTO.HomePoint hp = dto.getHomePoint();
JSONObject homePoint = new JSONObject();
homePoint.set("longitude", hp.getLongitude());
homePoint.set("latitude", hp.getLatitude());
homePoint.set("altitude", hp.getAltitude());
homePoint.set("heading", hp.getHeading());
data.set("home_point", homePoint);
}
return execCmd(deviceSn, "go_home", data);
}
@Override
public String setHome(String deviceSn, Q20SetHomeDTO dto) {
JSONObject data = new JSONObject();
data.set("longitude", dto.getLongitude());
data.set("latitude", dto.getLatitude());
data.set("altitude", dto.getAltitude());
data.set("heading", dto.getHeading());
if (dto.getCurrentGps() != null) {
data.set("current_gps", dto.getCurrentGps());
}
return execCmd(deviceSn, "set_home", data);
}
@Override
public String navigate(String deviceSn, Q20NavigateDTO dto) {
JSONObject data = new JSONObject();
data.set("longitude", dto.getLongitude());
data.set("latitude", dto.getLatitude());
data.set("altitude", dto.getAltitude());
data.set("speed", dto.getSpeed());
data.set("action", dto.getAction());
if (dto.getQrCode() != null) {
data.set("qr_code", dto.getQrCode());
}
return execCmd(deviceSn, "navigate", data);
}
// ==================== 云台相机 ====================
@Override
public String gimbalControl(String deviceSn, Q20GimbalControlDTO dto) {
JSONObject data = new JSONObject();
data.set("device_id", dto.getDeviceId());
data.set("mode", dto.getMode());
data.set("roll", dto.getRoll());
data.set("yaw", dto.getYaw());
data.set("pitch", dto.getPitch());
return execCmd(deviceSn, "gimbal_control", data);
}
@Override
public String mountTda(String deviceSn, Q20MountTdaDTO dto) {
JSONObject data = new JSONObject();
data.set("payload_index", dto.getPayloadIndex());
data.set("tda_length", dto.getTdaLength());
data.set("tda", dto.getTda());
return execCmd(deviceSn, "mount_tda", data);
}
@Override
public String cameraPhotoTake(String deviceSn, Q20CameraDTO dto) {
JSONObject data = buildCameraData(dto);
return execCmd(deviceSn, "camera_photo_take", data);
}
@Override
public String cameraRecordingStart(String deviceSn, Q20CameraDTO dto) {
JSONObject data = buildCameraData(dto);
return execCmd(deviceSn, "camera_recording_start", data);
}
@Override
public String cameraRecordingStop(String deviceSn, Q20CameraDTO dto) {
JSONObject data = buildCameraData(dto);
return execCmd(deviceSn, "camera_recording_stop", data);
}
@Override
public String cameraFocalLengthSet(String deviceSn, Q20CameraFocalLengthDTO dto) {
JSONObject data = new JSONObject();
data.set("payload_index", dto.getPayloadIndex());
data.set("device_id", dto.getDeviceId());
data.set("camera_type", dto.getCameraType());
data.set("zoom_factor", dto.getZoomFactor());
data.set("zoom_type", dto.getZoomType());
data.set("zoom_mode", dto.getZoomMode());
return execCmd(deviceSn, "camera_focal_length_set", data);
}
private JSONObject buildCameraData(Q20CameraDTO dto) {
JSONObject data = new JSONObject();
if (dto.getDeviceId() != null) {
data.set("device_id", dto.getDeviceId());
}
data.set("payload_index", dto.getPayloadIndex());
data.set("camera_type", dto.getCameraType());
return data;
}
// ==================== 直播 ====================
@Override
public String liveStartPush(String deviceSn, Q20LiveStartDTO dto) {
JSONObject data = new JSONObject();
data.set("payload_index", dto.getPayloadIndex());
data.set("url_type", dto.getUrlType());
data.set("url", dto.getUrl());
data.set("video", dto.getVideo());
data.set("video_id", dto.getVideoId());
data.set("video_type", dto.getVideoType());
return execCmd(deviceSn, "live_start_push", data);
}
@Override
public String liveStopPush(String deviceSn, int payloadIndex, int video) {
JSONObject data = new JSONObject();
data.set("payload_index", payloadIndex);
data.set("video", video);
return execCmd(deviceSn, "live_stop_push", data);
}
@Override
public String imageSwitch(String deviceSn, Q20ImageSwitchDTO dto) {
JSONObject data = new JSONObject();
data.set("device_id", dto.getDeviceId());
data.set("laser_enable", dto.getLaserEnable());
data.set("visible_enable", dto.getVisibleEnable());
data.set("ir_enable", dto.getIrEnable());
data.set("ir_type", dto.getIrType());
data.set("stream_type", dto.getStreamType());
return execCmd(deviceSn, "image_switch", data);
}
// ==================== 设备控制 ====================
@Override
public String deviceCharge(String deviceSn, int value, int version) {
JSONObject data = new JSONObject();
data.set("value", value);
data.set("version", version);
return execCmd(deviceSn, "device_charge", data);
}
@Override
public String deviceBoot(String deviceSn, int value) {
JSONObject data = new JSONObject();
data.set("value", value);
return execCmd(deviceSn, "device_boot", data);
}
@Override
public String safetySwitch(String deviceSn, int value) {
JSONObject data = new JSONObject();
data.set("value", value);
return execCmd(deviceSn, "safety_switch", data);
}
// ==================== 负载 ====================
@Override
public String throwingLock(String deviceSn, Q20ThrowingDTO dto) {
JSONObject data = new JSONObject();
data.set("device_id", dto.getDeviceId());
data.set("value", dto.getValue());
return execCmd(deviceSn, "throwing_lock", data);
}
@Override
public String throwingExecution(String deviceSn, long deviceId) {
JSONObject data = new JSONObject();
data.set("device_id", deviceId);
return execCmd(deviceSn, "throwing_execution", data);
}
@Override
public String halyarding(String deviceSn, Q20HalyardingDTO dto) {
JSONObject data = new JSONObject();
data.set("device_id", dto.getDeviceId());
data.set("action", dto.getAction());
data.set("rope_position", dto.getRopePosition());
data.set("rope_status", dto.getRopeStatus());
return execCmd(deviceSn, "halyarding", data);
}
@Override
public String logistics(String deviceSn, int value, boolean force) {
JSONObject data = new JSONObject();
data.set("value", value);
data.set("force", force);
return execCmd(deviceSn, "logistics", data);
}
// ==================== 虚拟摇杆高频不等回复 ====================
@Override
public void drc(String deviceSn, Q20DrcDTO dto) {
JSONObject data = new JSONObject();
data.set("vx", dto.getVx());
data.set("vy", dto.getVy());
data.set("vz", dto.getVz());
data.set("vyaw", dto.getVyaw());
String topic = String.format(SERVICES_TOPIC, deviceSn);
JSONObject payload = djiBaseService.getPayload("drc", data);
payload.set("gateway", deviceSn);
mqttPushService.pushMessageByClient1(topic, payload.toString());
}
// ==================== 安全/降落伞 ====================
@Override
public String setAutoParachute(String deviceSn, int value) {
JSONObject data = new JSONObject();
data.set("value", value);
return execCmd(deviceSn, "set_auto_parachute", data);
}
@Override
public JSONObject getEmergencyStopOtp(String deviceSn) {
JSONObject data = new JSONObject();
data.set("value", 0);
return execCmdGetReply(deviceSn, "get_emergency_stop_otp", data);
}
@Override
public String emergencyStop(String deviceSn, Q20EmergencyStopDTO dto) {
JSONObject data = new JSONObject();
data.set("otp", dto.getOtp());
return execCmd(deviceSn, "emergency_stop", data);
}
@Override
public JSONObject getParachuteOtp(String deviceSn) {
JSONObject data = new JSONObject();
data.set("value", 0);
return execCmdGetReply(deviceSn, "get_parachute_otp", data);
}
@Override
public String openParachute(String deviceSn, Q20OpenParachuteDTO dto) {
JSONObject data = new JSONObject();
data.set("otp", dto.getOtp());
return execCmd(deviceSn, "open_parachute", data);
}
@Override
public String confirmParachuteSafety(String deviceSn, int value) {
JSONObject data = new JSONObject();
data.set("value", value);
return execCmd(deviceSn, "confirm_parachute_safety", data);
}
// ==================== 状态/信息 ====================
@Override
public JSONObject queryStatus(String deviceSn, Q20QueryStatusDTO dto) {
JSONObject data = new JSONObject();
data.set("query_value", dto.getQueryValue());
if (dto.getBid() != null) {
data.set("bid", dto.getBid());
}
if (dto.getType() != null) {
data.set("type", dto.getType());
}
if (dto.getWayline() != null) {
data.set("wayline", dto.getWayline());
}
if (dto.getCode() != null) {
data.set("code", dto.getCode());
}
return execCmdGetReply(deviceSn, "query_status", data);
}
@Override
public JSONObject getConfig(String deviceSn) {
JSONObject data = new JSONObject();
data.set("value", 1);
return execCmdGetReply(deviceSn, "get_config", data);
}
@Override
public String stereoImage(String deviceSn, int value) {
JSONObject data = new JSONObject();
data.set("value", value);
return execCmd(deviceSn, "stereo_image", data);
}
@Override
public JSONObject version(String deviceSn) {
JSONObject data = new JSONObject();
data.set("value", 0);
return execCmdGetReply(deviceSn, "version", data);
}
// ==================== 日志 ====================
@Override
public JSONObject logCount(String deviceSn) {
JSONObject data = new JSONObject();
data.set("value", 0);
return execCmdGetReply(deviceSn, "log_count", data);
}
@Override
public JSONObject logList(String deviceSn, Q20LogListDTO dto) {
JSONObject data = new JSONObject();
data.set("start", dto.getStart());
data.set("end", dto.getEnd());
return execCmdGetReply(deviceSn, "log_list", data);
}
@Override
public String logData(String deviceSn, Q20LogDataDTO dto) {
JSONObject data = new JSONObject();
data.set("id", dto.getId());
data.set("url", dto.getUrl());
return execCmd(deviceSn, "log_data", data);
}
// ==================== OTA ====================
@Override
public String otaUpgrade(String deviceSn, Q20OtaUpgradeDTO dto) {
JSONObject data = new JSONObject();
data.set("sn", dto.getSn());
data.set("model", dto.getModel());
data.set("url", dto.getUrl());
data.set("md5", dto.getMd5());
data.set("package_name", dto.getPackageName());
data.set("version", dto.getVersion());
data.set("amend", dto.getAmend());
return execCmd(deviceSn, "ota_upgrade", data);
}
}

View File

@ -0,0 +1,44 @@
package com.multictrl.modules.business.q20.service.impl;
import com.multictrl.common.utils.CacheUtils;
import com.multictrl.modules.business.q20.handler.Q20Constant;
import com.multictrl.modules.business.q20.service.Q20DeviceService;
import com.multictrl.modules.business.q20.vo.Q20DeviceStatusVO;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
/**
* Q20飞机设备服务实现
*
* @author 938693313@qq.com
* @since 1.0.0 2026/5/20
*/
@Service
@RequiredArgsConstructor
public class Q20DeviceServiceImpl implements Q20DeviceService {
@Override
public Q20DeviceStatusVO getOnlineStatus(String deviceSn) {
Q20DeviceStatusVO vo = new Q20DeviceStatusVO();
vo.setDeviceSn(deviceSn);
boolean online = CacheUtils.get(Q20Constant.Q20_ONLINE + deviceSn) != null;
vo.setOnline(online);
if (online) {
vo.setOsd(CacheUtils.get(Q20Constant.Q20_OSD + deviceSn));
}
return vo;
}
@Override
public List<Q20DeviceStatusVO> getOnlineStatusBatch(List<String> deviceSnList) {
if (CollectionUtils.isEmpty(deviceSnList)) {
return Collections.emptyList();
}
return deviceSnList.stream().map(this::getOnlineStatus).collect(Collectors.toList());
}
}

View File

@ -0,0 +1,311 @@
package com.multictrl.modules.business.q20.service.impl;
import cn.hutool.json.JSONObject;
import com.multictrl.common.exception.RenException;
import com.multictrl.modules.business.q20.dto.Q20ParamsDTO;
import com.multictrl.modules.business.q20.service.Q20ParamService;
import com.multictrl.modules.business.service.DJIBaseService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/**
* Q20参数指令服务实现
* 复用 DJIBaseService send/wait 机制仅替换 topic thing/device/ 前缀
*
* @author 938693313@qq.com
* @since 1.0.0 2026/5/20
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class Q20ParamServiceImpl implements Q20ParamService {
private final DJIBaseService djiBaseService;
private static final String SERVICES_TOPIC = "thing/device/%s/services";
//
// 内部通用方法
//
/** set类指令发送并等待回复返回结果描述 */
private String execSet(String deviceSn, String method, JSONObject data) {
String topic = String.format(SERVICES_TOPIC, deviceSn);
return djiBaseService.sendWaitReplyJudgeResult(topic, djiBaseService.getPayload(method, data));
}
/** get类指令发送并等待回复返回 param_value 字段值 */
private Object execGet(String deviceSn, String method) {
String topic = String.format(SERVICES_TOPIC, deviceSn);
JSONObject data = new JSONObject();
data.set("value", 0);
JSONObject reply = djiBaseService.sendWaitReply(topic, djiBaseService.getPayload(method, data));
if (reply == null) {
throw new RenException("设备未响应,请检查设备是否在线");
}
if (reply.getInt("result", -1) != 0) {
throw new RenException("设备返回错误: " + reply.getStr("message", "未知错误"));
}
return reply.get("param_value");
}
private JSONObject floatData(float value) {
JSONObject d = new JSONObject();
d.set("value", value);
return d;
}
private JSONObject intData(int value) {
JSONObject d = new JSONObject();
d.set("value", value);
return d;
}
private JSONObject strData(String value) {
JSONObject d = new JSONObject();
d.set("value", value);
return d;
}
//
// 高度/距离限制
//
@Override
public String setHeightLimit(String deviceSn, float value) {
return execSet(deviceSn, "set_height_limit", floatData(value));
}
@Override
public Object getHeightLimit(String deviceSn) {
return execGet(deviceSn, "get_height_limit");
}
@Override
public String setDistanceLimit(String deviceSn, float value) {
return execSet(deviceSn, "set_distance_limit", floatData(value));
}
@Override
public Object getDistanceLimit(String deviceSn) {
return execGet(deviceSn, "get_distance_limit");
}
//
// 围栏
//
@Override
public String setGeofenceAction(String deviceSn, String value) {
return execSet(deviceSn, "set_geofence_action", strData(value));
}
@Override
public Object getGeofenceAction(String deviceSn) {
return execGet(deviceSn, "get_geofence_action");
}
//
// 返航
//
@Override
public String setReturnHeight(String deviceSn, float value) {
return execSet(deviceSn, "set_return_height", floatData(value));
}
@Override
public Object getReturnHeight(String deviceSn) {
return execGet(deviceSn, "get_return_height");
}
@Override
public String setReturnType(String deviceSn, String value) {
return execSet(deviceSn, "set_return_type", strData(value));
}
@Override
public Object getReturnType(String deviceSn) {
return execGet(deviceSn, "get_return_type");
}
//
// 电池阈值
//
@Override
public String setBatteryLowThreshold(String deviceSn, float value) {
return execSet(deviceSn, "set_battery_low_threshold", floatData(value));
}
@Override
public Object getBatteryLowThreshold(String deviceSn) {
return execGet(deviceSn, "get_battery_low_threshold");
}
@Override
public String setBatteryCriticalThreshold(String deviceSn, float value) {
return execSet(deviceSn, "set_battery_critical_threshold", floatData(value));
}
@Override
public Object getBatteryCriticalThreshold(String deviceSn) {
return execGet(deviceSn, "get_battery_critical_threshold");
}
@Override
public String setBatteryEmergencyThreshold(String deviceSn, float value) {
return execSet(deviceSn, "set_battery_emergency_threshold", floatData(value));
}
@Override
public Object getBatteryEmergencyThreshold(String deviceSn) {
return execGet(deviceSn, "get_battery_emergency_threshold");
}
@Override
public String setLowBatteryAction(String deviceSn, String value) {
return execSet(deviceSn, "set_low_battery_action", strData(value));
}
@Override
public Object getLowBatteryAction(String deviceSn) {
return execGet(deviceSn, "get_low_battery_action");
}
//
// 降落
//
@Override
public String setAprilTagId(String deviceSn, int value) {
return execSet(deviceSn, "set_april_tag_id", intData(value));
}
@Override
public Object getAprilTagId(String deviceSn) {
return execGet(deviceSn, "get_april_tag_id");
}
@Override
public String setPrecisionLandMode(String deviceSn, String value) {
return execSet(deviceSn, "set_precision_land_mode", strData(value));
}
@Override
public Object getPrecisionLandMode(String deviceSn) {
return execGet(deviceSn, "get_precision_land_mode");
}
//
// 避障/传感器
//
@Override
public String setObstacleAvoidanceEnable(String deviceSn, int value) {
return execSet(deviceSn, "set_obstacle_avoidance_enable", intData(value));
}
@Override
public Object getObstacleAvoidanceEnable(String deviceSn) {
return execGet(deviceSn, "get_obstacle_avoidance_enable");
}
@Override
public String setObstacleAvoidanceDistance(String deviceSn, float value) {
return execSet(deviceSn, "set_obstacle_avoidance_distance", floatData(value));
}
@Override
public Object getObstacleAvoidanceDistance(String deviceSn) {
return execGet(deviceSn, "get_obstacle_avoidance_distance");
}
@Override
public String setVisualSensorEnable(String deviceSn, int value) {
return execSet(deviceSn, "set_visual_sensor_enable", intData(value));
}
@Override
public Object getVisualSensorEnable(String deviceSn) {
return execGet(deviceSn, "get_visual_sensor_enable");
}
//
// 其他开关
//
@Override
public String setRtkEnable(String deviceSn, int value) {
return execSet(deviceSn, "set_rtk_enable", intData(value));
}
@Override
public Object getRtkEnable(String deviceSn) {
return execGet(deviceSn, "get_rtk_enable");
}
@Override
public String setUatMultCtrlEnable(String deviceSn, int value) {
return execSet(deviceSn, "set_uat_mult_ctrl_enable", intData(value));
}
@Override
public Object getUatMultCtrlEnable(String deviceSn) {
return execGet(deviceSn, "get_uat_mult_ctrl_enable");
}
@Override
public String setParachuteSafetyEnable(String deviceSn, int value) {
return execSet(deviceSn, "set_parachute_safety_enable", intData(value));
}
@Override
public Object getParachuteSafetyEnable(String deviceSn) {
return execGet(deviceSn, "get_parachute_safety_enable");
}
//
// 批量
//
@Override
public String setParams(String deviceSn, Q20ParamsDTO dto) {
JSONObject data = new JSONObject();
if (dto.getBatteryLowThreshold() != null) data.set("battery_low_threshold", dto.getBatteryLowThreshold());
if (dto.getBatteryCriticalThreshold() != null) data.set("battery_critical_threshold", dto.getBatteryCriticalThreshold());
if (dto.getBatteryEmergencyThreshold() != null) data.set("battery_emergency_threshold", dto.getBatteryEmergencyThreshold());
if (dto.getLowBatteryAction() != null) data.set("low_battery_action", dto.getLowBatteryAction());
if (dto.getHeightLimit() != null) data.set("height_limit", dto.getHeightLimit());
if (dto.getDistanceLimit() != null) data.set("distance_limit", dto.getDistanceLimit());
if (dto.getGeofenceAction() != null) data.set("geofence_action", dto.getGeofenceAction());
if (dto.getReturnHeight() != null) data.set("return_height", dto.getReturnHeight());
if (dto.getReturnSpeed() != null) data.set("return_speed", dto.getReturnSpeed());
if (dto.getReturnType() != null) data.set("return_type", dto.getReturnType());
if (dto.getObstacleAvoidanceEnable() != null) data.set("obstacle_avoidance_enable", dto.getObstacleAvoidanceEnable());
if (dto.getObstacleAvoidanceDistance() != null) data.set("obstacle_avoidance_distance", dto.getObstacleAvoidanceDistance());
if (dto.getVisualSensorEnable() != null) data.set("visual_sensor_enable", dto.getVisualSensorEnable());
if (dto.getAprilTagId() != null) data.set("april_tag_id", dto.getAprilTagId());
if (dto.getPrecisionLandMode() != null) data.set("precision_land_mode", dto.getPrecisionLandMode());
if (dto.getDataLinkLossAction() != null) data.set("data_link_loss_action", dto.getDataLinkLossAction());
if (dto.getRcLossAction() != null) data.set("rc_loss_action", dto.getRcLossAction());
if (dto.getParachuteSafetyEnable() != null) data.set("parachute_safety_enable", dto.getParachuteSafetyEnable());
return execSet(deviceSn, "set_params", data);
}
@Override
public JSONObject getParams(String deviceSn) {
String topic = String.format(SERVICES_TOPIC, deviceSn);
JSONObject data = new JSONObject();
data.set("value", 0);
JSONObject reply = djiBaseService.sendWaitReply(topic, djiBaseService.getPayload("get_params", data));
if (reply == null) {
throw new RenException("设备未响应,请检查设备是否在线");
}
if (reply.getInt("result", -1) != 0) {
throw new RenException("设备返回错误: " + reply.getStr("message", "未知错误"));
}
return reply.getJSONObject("params");
}
}

View File

@ -0,0 +1,465 @@
package com.multictrl.modules.business.q20.service.impl;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.multictrl.common.exception.RenException;
import com.multictrl.modules.business.dao.RouteDao;
import com.multictrl.modules.business.dao.RouteWaypointDao;
import com.multictrl.modules.business.dao.WaypointActionDao;
import com.multictrl.modules.business.entity.RouteEntity;
import com.multictrl.modules.business.entity.RouteWaypointEntity;
import com.multictrl.modules.business.entity.WaypointActionEntity;
import com.multictrl.modules.business.q20.dto.*;
import com.multictrl.modules.business.q20.service.Q20RouteService;
import com.multictrl.modules.business.service.DJIBaseService;
import com.multictrl.modules.business.service.FlightTaskService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigInteger;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Q20航线任务服务实现
* 复用 DJIBaseService send/wait 机制topic thing/device/{sn}/services
*
* @author 938693313@qq.com
* @since 1.0.0 2026/5/20
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class Q20RouteServiceImpl implements Q20RouteService {
private final DJIBaseService djiBaseService;
private final FlightTaskService flightTaskService;
private final ObjectMapper objectMapper;
private final RouteDao routeDao;
private final RouteWaypointDao routeWaypointDao;
private final WaypointActionDao waypointActionDao;
private static final String SERVICES_TOPIC = "thing/device/%s/services";
private static final String TEMPLATE_TYPE_Q20 = "q20";
/** 上传航线的航线ID前缀 */
private static final String ROUTE_WAYLINE_PREFIX = "q20_route_";
/** 一键航线的航线ID前缀 */
private static final String AUTO_WAYLINE_PREFIX = "q20_auto_";
/** 拆分航线ID的「前缀 + 数字后缀」 */
private static final Pattern WAYLINE_TAIL = Pattern.compile("^(.*?)(\\d+)$");
//
// 内部通用方法
//
/** 发送指令并等待回复,返回结果描述字符串 */
private String execCmd(String deviceSn, String method, JSONObject data) {
String topic = String.format(SERVICES_TOPIC, deviceSn);
return djiBaseService.sendWaitReplyJudgeResult(topic, djiBaseService.getPayload(method, data));
}
/** 发送指令并等待回复返回完整回复JSONObject超时抛出RenException */
private JSONObject execCmdGetReply(String deviceSn, String method, JSONObject data) {
String topic = String.format(SERVICES_TOPIC, deviceSn);
JSONObject reply = djiBaseService.sendWaitReply(topic, djiBaseService.getPayload(method, data));
if (reply == null) {
throw new RenException("设备未响应,请检查设备是否在线");
}
return reply;
}
/** 利用Jackson @JsonProperty注解将DTO序列化为snake_case键的JSONObject */
private JSONObject dtoToJson(Object dto) {
try {
String json = objectMapper.writeValueAsString(dto);
return JSONUtil.parseObj(json);
} catch (Exception e) {
throw new RenException("参数序列化失败: " + e.getMessage());
}
}
//
// 接口实现
//
@Override
public String nextWayline(String type) {
// 按类型区分前缀一键航线用 q20_auto_上传航线用 q20_route_
String prefix = AUTO_WAYLINE_PREFIX.equals(type) || "auto".equalsIgnoreCase(type)
? AUTO_WAYLINE_PREFIX : ROUTE_WAYLINE_PREFIX;
String defaultId = prefix + "001";
// 取库中该前缀下最新一条Q20航线的航线ID按其数字后缀递增
RouteEntity latest = routeDao.selectOne(new QueryWrapper<RouteEntity>()
.eq("template_type", TEMPLATE_TYPE_Q20)
.likeRight("q20_route_id", prefix)
.orderByDesc("create_date")
.last("LIMIT 1"));
String next = latest != null ? incrementWayline(latest.getQ20RouteId()) : defaultId;
// 兜底防止递增结果与库中已有ID冲突
int guard = 0;
while (waylineExists(next) && guard++ < 1000) {
next = incrementWayline(next);
}
return next;
}
/** 航线ID递增保留前缀与位宽对数字后缀+1无数字后缀时追加_001 */
private String incrementWayline(String base) {
if (StrUtil.isBlank(base)) {
return ROUTE_WAYLINE_PREFIX + "001";
}
Matcher m = WAYLINE_TAIL.matcher(base.trim());
if (!m.matches()) {
return base.trim() + "_001";
}
String prefix = m.group(1);
String num = m.group(2);
String inc = new BigInteger(num).add(BigInteger.ONE).toString();
if (inc.length() < num.length()) {
inc = StrUtil.padPre(inc, num.length(), '0');
}
return prefix + inc;
}
/** 判断航线ID是否已存在于Q20航线库 */
private boolean waylineExists(String wayline) {
if (StrUtil.isBlank(wayline)) {
return false;
}
Long cnt = routeDao.selectCount(new QueryWrapper<RouteEntity>()
.eq("q20_route_id", wayline)
.eq("template_type", TEMPLATE_TYPE_Q20));
return cnt != null && cnt > 0;
}
@Override
@Transactional(rollbackFor = Exception.class)
public String routeUpload(String deviceSn, Q20RouteUploadDTO dto) {
validateWaypoints(dto);
checkRouteNotExists(dto.getWayline(), dto.getRouteName());
JSONObject data = dtoToJson(dto);
String result = execCmd(deviceSn, "route_upload", data);
saveRouteToLocal(deviceSn, dto);
return result;
}
/** 航点校验:航线至少需要两个有效航点(含经纬度坐标) */
private void validateWaypoints(Q20RouteUploadDTO dto) {
List<Q20RoutePlacemarkDTO> placemarks = (dto != null && dto.getFolder() != null)
? dto.getFolder().getPlacemark() : null;
long valid = placemarks == null ? 0 : placemarks.stream()
.filter(p -> p != null && p.getPoint() != null
&& StrUtil.isNotBlank(p.getPoint().getCoordinates())
&& p.getPoint().getCoordinates().split(",").length >= 2)
.count();
if (valid < 2) {
throw new RenException("航线至少需要两个有效航点(含经纬度)");
}
}
/** 上传前校验航线ID和航线名称均不能与库中已有记录重复 */
private void checkRouteNotExists(String wayline, String routeName) {
if (StrUtil.isNotBlank(wayline)) {
Long cnt = routeDao.selectCount(new QueryWrapper<RouteEntity>()
.eq("q20_route_id", wayline)
.eq("template_type", TEMPLATE_TYPE_Q20));
if (cnt != null && cnt > 0) {
throw new RenException("航线ID「" + wayline + "」已存在,请勿重复上传");
}
}
// if (StrUtil.isNotBlank(routeName)) {
// Long cnt = routeDao.selectCount(new QueryWrapper<RouteEntity>()
// .eq("route_name", routeName)
// .eq("template_type", TEMPLATE_TYPE_Q20));
// if (cnt != null && cnt > 0) {
// throw new RenException("航线名称「" + routeName + "」已存在,请勿重复上传");
// }
// }
}
@Override
public String routeExecute(String deviceSn, Q20RouteExecuteDTO dto) {
JSONObject data = dtoToJson(dto);
String result = execCmd(deviceSn, "route_execute", data);
saveFlightTask(deviceSn, dto.getRouteId());
return result;
}
@Override
@Transactional(rollbackFor = Exception.class)
public String routeAuto(String deviceSn, Q20RouteAutoDTO dto) {
validateWaypoints(dto.getRouteInfo());
Q20RouteUploadDTO routeInfo = dto.getRouteInfo();
checkRouteNotExists(routeInfo.getWayline(), routeInfo.getRouteName());
JSONObject data = new JSONObject();
data.set("route_info", dtoToJson(dto.getRouteInfo()));
data.set("execute_info", dtoToJson(dto.getExecuteInfo()));
data.set("vehicle_config", dtoToJson(dto.getVehicleConfig()));
String result = execCmd(deviceSn, "route_auto", data);
if (dto.getRouteInfo() != null) {
saveRouteToLocal(deviceSn, dto.getRouteInfo());
}
String waylineId = dto.getRouteInfo() != null ? dto.getRouteInfo().getWayline() : null;
saveFlightTask(deviceSn, waylineId);
return result;
}
@Override
public String breakLoopHover(String deviceSn) {
JSONObject data = new JSONObject();
data.set("value", 1);
return execCmd(deviceSn, "break_loop_hover", data);
}
@Override
public JSONObject routeInfo(String deviceSn, int mode, String value) {
JSONObject data = new JSONObject();
data.set("mode", mode);
data.set("value", value);
return execCmdGetReply(deviceSn, "route_info", data);
}
@Override
public JSONObject routeProgress(String deviceSn) {
JSONObject data = new JSONObject();
data.set("value", 1);
return execCmdGetReply(deviceSn, "route_progress", data);
}
@Override
public String routePause(String deviceSn) {
JSONObject data = new JSONObject();
data.set("value", 1);
return execCmd(deviceSn, "route_pause", data);
}
@Override
public String routeResume(String deviceSn) {
JSONObject data = new JSONObject();
data.set("value", 1);
return execCmd(deviceSn, "route_resume", data);
}
@Override
public String routeFinish(String deviceSn) {
JSONObject data = new JSONObject();
data.set("value", 1);
return execCmd(deviceSn, "route_finish", data);
}
@Override
public JSONObject geofenceInfo(String deviceSn) {
JSONObject data = new JSONObject();
data.set("value", 1);
return execCmdGetReply(deviceSn, "geofence_info", data);
}
@Override
public String geofenceUpload(String deviceSn, Q20GeofenceUploadDTO dto) {
JSONObject data = new JSONObject();
data.set("geofence", dtoToJson(dto));
return execCmd(deviceSn, "geofence_upload", data);
}
//
// 本地航线库存储
//
/**
* 上传成功后将航线数据持久化到本地航线库
* 同设备同 wayline ID 已存在时覆盖旧记录删除旧航点/动作后重新插入
*/
private void saveRouteToLocal(String deviceSn, Q20RouteUploadDTO dto) {
String wayline = dto.getWayline();
String routeName = StrUtil.isNotBlank(dto.getRouteName())
? dto.getRouteName()
: (StrUtil.isNotBlank(wayline) ? wayline : deviceSn + "_" + System.currentTimeMillis());
// 同设备相同 wayline 已存在则清除旧记录
RouteEntity existing = routeDao.selectOne(new QueryWrapper<RouteEntity>()
.eq("dock_sn", deviceSn)
.eq(StrUtil.isNotBlank(wayline), "q20_route_id", wayline)
.eq("template_type", TEMPLATE_TYPE_Q20));
if (existing != null) {
Long oldId = existing.getId();
routeWaypointDao.delete(new QueryWrapper<RouteWaypointEntity>().eq("route_id", oldId));
waypointActionDao.delete(new QueryWrapper<WaypointActionEntity>().eq("route_id", oldId));
routeDao.deleteById(oldId);
log.debug("Q20 routeUpload -> 覆盖旧航线记录 id={}, wayline={}", oldId, wayline);
}
// 构建 RouteEntity
RouteEntity route = new RouteEntity();
route.setRouteName(routeName);
route.setDockSn(deviceSn);
route.setTemplateType(TEMPLATE_TYPE_Q20);
route.setQ20RouteId(wayline);
Q20RouteMissionConfigDTO mc = dto.getMissionConfig();
if (mc != null) {
route.setFlyToWaylineMode(mc.getFlyToWaylineMode());
route.setFinishAction(mc.getFinishAction());
route.setExitOnRcLost(mc.getExitOnRcLost());
route.setExecuteRcLostAction(mc.getExecuteRcLostAction());
if (mc.getGlobalTransitionalSpeed() != null) {
route.setGlobalTransitionalSpeed(mc.getGlobalTransitionalSpeed().doubleValue());
}
if (mc.getGlobalRthHeight() != null) {
route.setGlobalRthHeight(mc.getGlobalRthHeight().doubleValue());
}
if (mc.getTakeoffSecurityHeight() != null) {
route.setTakeoffSecurityHeight(mc.getTakeoffSecurityHeight().doubleValue());
}
}
Q20RouteFolderDTO folder = dto.getFolder();
if (folder != null) {
route.setHeightModel(folder.getExecuteHeightMode() != null ? folder.getExecuteHeightMode() : "relativeToStartPoint");
route.setFlightSpeed((double) folder.getAutoFlightSpeed());
List<Q20RoutePlacemarkDTO> placemarks = folder.getPlacemark();
route.setWaypointNum(placemarks != null ? placemarks.size() : 0);
}
// vehicleConfig 中的 globalRthHeight 优先级低于 missionConfig
Q20RouteVehicleConfigDTO vc = dto.getVehicleConfig();
if (vc != null && route.getGlobalRthHeight() == null && vc.getGlobalRthHeight() != null) {
route.setGlobalRthHeight(vc.getGlobalRthHeight().doubleValue());
}
// 前端传入的可选覆盖字段
if (dto.getFlightHeight() != null) route.setFlightHeight(dto.getFlightHeight().doubleValue());
if (dto.getAccurateImport() != null) route.setIsAccurateImport(dto.getAccurateImport());
// 补全 NOT NULL 字段默认值
if (route.getFlightHeight() == null) route.setFlightHeight(100.0);
if (route.getGlobalRthHeight() == null) route.setGlobalRthHeight(100.0);
if (route.getTakeoffSecurityHeight() == null) route.setTakeoffSecurityHeight(100.0);
if (route.getGlobalTransitionalSpeed() == null) route.setGlobalTransitionalSpeed(5.0);
if (route.getFinishAction() == null) route.setFinishAction("goHome");
if (route.getExitOnRcLost() == null) route.setExitOnRcLost("executeLostAction");
if (route.getFlyToWaylineMode() == null) route.setFlyToWaylineMode("safely");
if (route.getTotalDistance() == null) route.setTotalDistance(0.0);
if (route.getExpectFlightTime() == null) route.setExpectFlightTime("0");
if (route.getIsAccurateImport() == null) route.setIsAccurateImport(false);
routeDao.insert(route);
Long routeId = route.getId();
// 保存航点及动作
if (folder != null && folder.getPlacemark() != null) {
for (Q20RoutePlacemarkDTO p : folder.getPlacemark()) {
RouteWaypointEntity wp = buildWaypoint(routeId, p, folder.getAutoFlightSpeed());
routeWaypointDao.insert(wp);
if (p.getActionGroup() != null && p.getActionGroup().getAction() != null) {
List<Q20RouteActionDTO> actions = p.getActionGroup().getAction();
for (int i = 0; i < actions.size(); i++) {
Q20RouteActionDTO action = actions.get(i);
WaypointActionEntity wa = new WaypointActionEntity();
wa.setRouteId(routeId);
wa.setWaypointId(wp.getId());
wa.setActionType(action.getActionActuatorFunc());
wa.setActionSort(i);
if (action.getActionActuatorFuncParam() != null) {
try {
wa.setActionValue(objectMapper.writeValueAsString(action.getActionActuatorFuncParam()));
} catch (Exception e) {
log.warn("Q20 routeUpload -> 动作参数序列化失败: {}", e.getMessage());
}
}
waypointActionDao.insert(wa);
}
}
}
}
log.info("Q20 routeUpload -> 航线已入库 routeId={}, routeName={}, wayline={}, deviceSn={}",
routeId, routeName, wayline, deviceSn);
}
/**
* 执行航线后创建飞行任务记录
* 优先按 waylineId 匹配找不到则取该设备最近一条 Q20 航线
*/
private void saveFlightTask(String deviceSn, String waylineId) {
RouteEntity route = null;
if (StrUtil.isNotBlank(waylineId)) {
route = routeDao.selectOne(new QueryWrapper<RouteEntity>()
.eq("q20_route_id", waylineId)
.eq("template_type", TEMPLATE_TYPE_Q20));
}
if (route == null) {
route = routeDao.selectOne(new QueryWrapper<RouteEntity>()
.eq("dock_sn", deviceSn)
.eq("template_type", TEMPLATE_TYPE_Q20)
.orderByDesc("create_date")
.last("LIMIT 1"));
}
if (route == null) {
log.warn("Q20 saveFlightTask -> 未找到航线记录,跳过任务创建 deviceSn={}, waylineId={}", deviceSn, waylineId);
return;
}
String taskId = IdUtil.fastSimpleUUID();
flightTaskService.addRouteTask(taskId, deviceSn, route.getId());
log.info("Q20 saveFlightTask -> 任务已创建 taskId={}, routeId={}, deviceSn={}", taskId, route.getId(), deviceSn);
}
private RouteWaypointEntity buildWaypoint(Long routeId, Q20RoutePlacemarkDTO p, float globalSpeed) {
RouteWaypointEntity wp = new RouteWaypointEntity();
wp.setRouteId(routeId);
wp.setWaypointSort(p.getIndex());
wp.setFlightHeight((double) p.getExecuteHeight());
// 解析坐标 "经度,纬度"
if (p.getPoint() != null && StrUtil.isNotBlank(p.getPoint().getCoordinates())) {
String[] parts = p.getPoint().getCoordinates().split(",");
if (parts.length >= 2) {
try {
wp.setLongitude(Double.parseDouble(parts[0].trim()));
wp.setLatitude(Double.parseDouble(parts[1].trim()));
} catch (NumberFormatException e) {
log.warn("Q20 routeUpload -> 坐标解析失败: {}", p.getPoint().getCoordinates());
}
}
}
boolean useGlobal = p.getUseGlobalSpeed() == null || p.getUseGlobalSpeed() == 1;
wp.setFollowRouteSpeed(useGlobal);
wp.setFollowRouteHeight(false);
wp.setFlightSpeed(useGlobal
? (double) globalSpeed
: (p.getWaypointSpeed() != null ? p.getWaypointSpeed().doubleValue() : (double) globalSpeed));
Q20RouteWaypointHeadingDTO heading = p.getWaypointHeadingParam();
if (heading != null) {
wp.setWaypointHeadingMode(heading.getWaypointHeadingMode());
if (heading.getWaypointHeadingAngle() != null) {
wp.setWaypointHeadingAngle(String.valueOf(heading.getWaypointHeadingAngle()));
}
wp.setWaypointHeadingPathMode(heading.getWaypointHeadingPathMode());
wp.setWaypointPoiPoint(heading.getWaypointPoiPoint());
}
if (wp.getWaypointHeadingMode() == null) wp.setWaypointHeadingMode("followWayline");
if (wp.getWaypointHeadingPathMode() == null) wp.setWaypointHeadingPathMode("followBadArc");
Q20RouteWaypointTurnDTO turn = p.getWaypointTurnParam();
if (turn != null) {
wp.setWaypointTruningMode(turn.getWaypointTurnMode());
if (turn.getWaypointTurnDampingDist() != null) {
wp.setWaypointTurnDampingDist(String.valueOf(turn.getWaypointTurnDampingDist()));
}
}
if (wp.getWaypointTruningMode() == null) wp.setWaypointTruningMode("toPointAndStopWithDiscontinuityCurvature");
return wp;
}
}

View File

@ -0,0 +1,24 @@
package com.multictrl.modules.business.q20.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* Q20飞机在线状态响应
*
* @author 938693313@qq.com
* @since 1.0.0 2026/5/20
*/
@Data
@Schema(name = "Q20飞机在线状态")
public class Q20DeviceStatusVO {
@Schema(description = "设备序列号")
private String deviceSn;
@Schema(description = "是否在线35s内有心跳则为true")
private boolean online;
@Schema(description = "最新OSD遥测数据仅在线时有值")
private Object osd;
}

View File

@ -49,4 +49,10 @@ public interface DJIBaseService {
//机场是否在线
Boolean isDockOnline(String dockSn);
// 获取当前待回复命令信息测试/模拟用
JSONObject getPendingCmd(String deviceSn);
// 注入模拟MQTT回复测试/模拟用replyData 为完整回复 JSON result/output 等字段
void injectMockReply(String deviceSn, JSONObject replyData);
}

View File

@ -15,6 +15,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
/**
* 大疆基础服务
@ -28,6 +29,9 @@ import java.util.Optional;
public class DJIBaseServiceImpl implements DJIBaseService {
private final MqttPushService mqttPushService;
/** 设备SN → 当前正在等待回复的命令信息(供模拟回复使用) */
private final ConcurrentHashMap<String, JSONObject> pendingCmds = new ConcurrentHashMap<>();
@Override
public JSONObject getPayload(String method, JSONObject data) {
JSONObject jsonObject = new JSONObject();
@ -46,16 +50,57 @@ public class DJIBaseServiceImpl implements DJIBaseService {
mqttPushService.pushMessageByClient1(topic, payload.toString());
String bid = payload.getStr("bid");
String tid = payload.getStr("tid");
int tryCount = 100;
while (tryCount-- > 0) {
Object object = CacheUtils.get(bid + "_" + tid);
if (object != null) {
return (JSONObject) object;
}
Utils.sleep(100);
String cacheKey = bid + "_" + tid;
// 记录当前等待回复的命令供前端模拟回复使用
String deviceSn = extractDeviceSn(topic);
if (deviceSn != null) {
JSONObject pending = new JSONObject();
pending.set("bid", bid);
pending.set("tid", tid);
pending.set("method", payload.getStr("method"));
pending.set("cacheKey", cacheKey);
pending.set("timestamp", System.currentTimeMillis());
pendingCmds.put(deviceSn, pending);
}
return null;
try {
int tryCount = 100;
while (tryCount-- > 0) {
Object object = CacheUtils.get(cacheKey);
if (object != null) {
return (JSONObject) object;
}
Utils.sleep(100);
}
return null;
} finally {
if (deviceSn != null) pendingCmds.remove(deviceSn);
}
}
private String extractDeviceSn(String topic) {
// topic格式: thing/device/{sn}/services thing/product/{sn}/services
if (topic == null) return null;
String[] parts = topic.split("/");
return parts.length >= 3 ? parts[2] : null;
}
@Override
public JSONObject getPendingCmd(String deviceSn) {
return pendingCmds.get(deviceSn);
}
@Override
public void injectMockReply(String deviceSn, JSONObject replyData) {
JSONObject pending = pendingCmds.get(deviceSn);
if (pending == null) {
throw new RenException("当前设备没有待回复的命令,可能已超时或尚未发送");
}
String cacheKey = pending.getStr("cacheKey");
JSONObject reply = replyData != null ? replyData : new JSONObject();
CacheUtils.set(cacheKey, reply);
log.debug("injectMockReply -> deviceSn={}, method={}, replyData={}", deviceSn, pending.getStr("method"), reply);
}
@Override

View File

@ -72,6 +72,8 @@ public class ShiroConfig {
filterMap.put("/swagger-resources/**", "anon");
filterMap.put("/captcha", "anon");
filterMap.put("/", "anon");
filterMap.put("/q20-login.html", "anon");
filterMap.put("/q20-ctrl.html", "anon");
filterMap.put("/srs/**", "anon");
filterMap.put("/mqtt/auth", "anon");
filterMap.put("/**", "oauth2");

View File

@ -45,7 +45,7 @@ mqtt:
username: dock
password: Dock@2023
subClientId: dj-one-sub${random.int(10)}
subTopic: thing/product/#,sys/product/#,thing/device/#
subTopic: thing/product/#,sys/product/#,thing/device/#,sys/device/#
pubClientId: dj-one-pub${random.int(10)}
client2:
url: tcp://${host.ip}:61637

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,126 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Q20 登录</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet">
<style>
:root {
--bs-body-bg: #0d1117;
--bs-body-color: #c9d1d9;
--card-bg: #161b22;
--border-color: #30363d;
--accent: #388bfd;
--danger: #f85149;
--muted: #8b949e;
}
body { background: var(--bs-body-bg); color: var(--bs-body-color); font-size: 13px; }
.card { background: var(--card-bg); border: 1px solid var(--border-color); border-radius: 8px; max-width: 400px; width: 100%; }
.form-control { background: #0d1117; border-color: var(--border-color); color: var(--bs-body-color); font-size: 12px; }
.form-control:focus { background: #0d1117; border-color: var(--accent); color: var(--bs-body-color); box-shadow: 0 0 0 2px rgba(56,139,253,.25); }
.form-label { font-size: 11px; color: var(--muted); margin-bottom: 2px; }
.btn-primary { background: var(--accent); border-color: var(--accent); font-size: 12px; }
#captchaImg { cursor: pointer; border: 1px solid var(--border-color); border-radius: 4px; height: 38px; }
</style>
</head>
<body class="d-flex justify-content-center align-items-center" style="min-height:100vh">
<div class="card p-4">
<div class="text-center mb-4">
<i class="bi bi-send-fill" style="font-size:2rem;color:var(--accent)"></i>
<h5 class="mt-2 mb-0" style="color:#e6edf3">Q20 飞行控制台</h5>
<small class="text-muted">Drone Control Panel</small>
</div>
<div class="mb-2">
<label class="form-label">服务地址</label>
<input id="apiBase" class="form-control" value="/multictrl" placeholder="如 http://192.168.1.1:61620/api">
</div>
<div class="mb-2">
<label class="form-label">用户名</label>
<input id="loginUser" class="form-control" placeholder="admin">
</div>
<div class="mb-2">
<label class="form-label">密码</label>
<input id="loginPass" class="form-control" type="password" placeholder="••••••••">
</div>
<div class="mb-3">
<label class="form-label">验证码</label>
<div class="d-flex gap-2">
<input id="loginCaptcha" class="form-control" placeholder="点击图片刷新" onkeydown="if(event.key==='Enter')doLogin()">
<img id="captchaImg" src="" alt="captcha" onclick="refreshCaptcha()" title="点击刷新">
</div>
</div>
<button class="btn btn-primary w-100" onclick="doLogin()">
<i class="bi bi-box-arrow-in-right me-1"></i>登 录
</button>
<div id="loginErr" class="mt-2 text-danger small" style="display:none"></div>
</div>
<script>
let loginUuid = '';
function genUuid() {
return crypto.randomUUID ? crypto.randomUUID()
: Math.random().toString(36).slice(2) + Date.now().toString(36);
}
function baseUrl() {
return (document.getElementById('apiBase').value || '/multictrl').replace(/\/$/, '');
}
function refreshCaptcha() {
loginUuid = genUuid();
document.getElementById('captchaImg').src =
baseUrl() + '/captcha?uuid=' + loginUuid + '&t=' + Date.now();
}
async function doLogin() {
const err = document.getElementById('loginErr');
err.style.display = 'none';
const username = document.getElementById('loginUser').value.trim();
const password = document.getElementById('loginPass').value;
const captcha = document.getElementById('loginCaptcha').value.trim();
if (!username || !password || !captcha) {
err.textContent = '请填写完整信息';
err.style.display = '';
return;
}
try {
const res = await fetch(baseUrl() + '/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password, captcha, uuid: loginUuid })
});
const json = await res.json();
if (json.code !== 0) {
err.textContent = json.msg || '登录失败';
err.style.display = '';
refreshCaptcha();
document.getElementById('loginCaptcha').value = '';
return;
}
sessionStorage.setItem('q20_token', json.data.token);
sessionStorage.setItem('q20_api_base', baseUrl());
sessionStorage.setItem('q20_username', username);
window.location.href = 'q20-ctrl.html';
} catch (e) {
err.textContent = '网络错误: ' + e.message;
err.style.display = '';
}
}
document.addEventListener('DOMContentLoaded', () => {
// 已登录则直接跳控制台
if (sessionStorage.getItem('q20_token')) {
window.location.href = 'q20-ctrl.html';
return;
}
refreshCaptcha();
});
</script>
</body>
</html>

BIN
doc/Q20参数指令.docx Normal file

Binary file not shown.

BIN
doc/Q20控制指令.docx Normal file

Binary file not shown.

BIN
doc/Q20航线任务.docx Normal file

Binary file not shown.

Binary file not shown.

View File

@ -2072,3 +2072,9 @@ COMMENT
ON COLUMN "public"."bus_geo_mark"."creator" IS '创建人';
COMMENT
ON TABLE "public"."bus_geo_mark" IS '地图标注';
-- 20260526 航线表新增飞机类型和航线id字段
ALTER TABLE public.bus_route
ADD COLUMN q20_route_id varchar(50);
COMMENT
ON COLUMN "public"."bus_route"."q20_route_id" IS 'q20航线id';