diff --git a/admin/src/main/java/com/multictrl/common/aspect/Q20ServiceLogAspect.java b/admin/src/main/java/com/multictrl/common/aspect/Q20ServiceLogAspect.java new file mode 100644 index 0000000..e69ec05 --- /dev/null +++ b/admin/src/main/java/com/multictrl/common/aspect/Q20ServiceLogAspect.java @@ -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; + } + } +} diff --git a/admin/src/main/java/com/multictrl/common/config/Jackson2ObjectMapperConfig.java b/admin/src/main/java/com/multictrl/common/config/Jackson2ObjectMapperConfig.java new file mode 100644 index 0000000..b245544 --- /dev/null +++ b/admin/src/main/java/com/multictrl/common/config/Jackson2ObjectMapperConfig.java @@ -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.ObjectMapper(Jackson 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; + } +} diff --git a/admin/src/main/java/com/multictrl/modules/business/dto/RouteDTO.java b/admin/src/main/java/com/multictrl/modules/business/dto/RouteDTO.java index 5aae36d..c5c1907 100644 --- a/admin/src/main/java/com/multictrl/modules/business/dto/RouteDTO.java +++ b/admin/src/main/java/com/multictrl/modules/business/dto/RouteDTO.java @@ -104,6 +104,10 @@ public class RouteDTO implements Serializable { @Schema(description = "kmz地址") private String kmzUrl; + @JsonProperty(access = JsonProperty.Access.READ_ONLY) + @Schema(description = "Q20航线ID(Q20设备上报的wayline ID)") + private String q20RouteId; + @JsonProperty(access = JsonProperty.Access.READ_ONLY) @Schema(description = "航点个数") private Integer waypointNum; diff --git a/admin/src/main/java/com/multictrl/modules/business/entity/RouteEntity.java b/admin/src/main/java/com/multictrl/modules/business/entity/RouteEntity.java index 59fd978..ba7774e 100644 --- a/admin/src/main/java/com/multictrl/modules/business/entity/RouteEntity.java +++ b/admin/src/main/java/com/multictrl/modules/business/entity/RouteEntity.java @@ -85,6 +85,10 @@ public class RouteEntity extends BaseEntity { * kmz地址 */ private String kmzUrl; + /** + * Q20航线ID(Q20设备上报的wayline ID) + */ + private String q20RouteId; /** * 航点个数 */ diff --git a/admin/src/main/java/com/multictrl/modules/business/handler/TopicDistributor.java b/admin/src/main/java/com/multictrl/modules/business/handler/TopicDistributor.java index 11dd069..1db28b6 100644 --- a/admin/src/main/java/com/multictrl/modules/business/handler/TopicDistributor.java +++ b/admin/src/main/java/com/multictrl/modules/business/handler/TopicDistributor.java @@ -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 handlerMap = new ConcurrentHashMap<>(); private final Map 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 map = topic.startsWith(Q20_TOPIC_PREFIX) ? q20HandlerMap : handlerMap; + boolean isQ20 = topic.startsWith(Q20_THING_PREFIX) || topic.startsWith(Q20_SYS_PREFIX); + Map map = isQ20 ? q20HandlerMap : handlerMap; MessageHandler handler = map.get(method); if (handler != null) { handler.handleMessage(topic, payload, gateway); diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/controller/Q20CommandController.java b/admin/src/main/java/com/multictrl/modules/business/q20/controller/Q20CommandController.java new file mode 100644 index 0000000..4f2134d --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/q20/controller/Q20CommandController.java @@ -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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 version(@PathVariable String deviceSn) { + return new Result<>().ok(q20CommandService.version(deviceSn)); + } + + // ==================== 日志 ==================== + + @GetMapping("/logCount/{deviceSn}") + @Operation(summary = "获取日志数量") + @RequiresPermissions("bus:q20:log") + public Result logCount(@PathVariable String deviceSn) { + return new Result<>().ok(q20CommandService.logCount(deviceSn)); + } + + @PostMapping("/logList/{deviceSn}") + @Operation(summary = "获取日志列表") + @RequiresPermissions("bus:q20:log") + public Result 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 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 otaUpgrade(@PathVariable String deviceSn, @RequestBody Q20OtaUpgradeDTO dto) { + return new Result<>().ok(q20CommandService.otaUpgrade(deviceSn, dto)); + } +} diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/controller/Q20Controller.java b/admin/src/main/java/com/multictrl/modules/business/q20/controller/Q20Controller.java new file mode 100644 index 0000000..04a7bbf --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/q20/controller/Q20Controller.java @@ -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 getOnlineStatus(@PathVariable String deviceSn) { + return new Result().ok(q20DeviceService.getOnlineStatus(deviceSn)); + } + + @PostMapping("/onlineBatch") + @Operation(summary = "批量查询飞机在线状态") + @RequiresPermissions("bus:q20:online") + public Result> getOnlineStatusBatch(@RequestBody List deviceSnList) { + return new Result>().ok(q20DeviceService.getOnlineStatusBatch(deviceSnList)); + } +} diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/controller/Q20MockController.java b/admin/src/main/java/com/multictrl/modules/business/q20/controller/Q20MockController.java new file mode 100644 index 0000000..8f3a65c --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/q20/controller/Q20MockController.java @@ -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 getPending(@PathVariable String deviceSn) { + return new Result<>().ok(djiBaseService.getPendingCmd(deviceSn)); + } + + @PostMapping("/reply/{deviceSn}") + @Operation(summary = "注入模拟MQTT回复(直接写入缓存,body为完整回复JSON)") + public Result 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 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); + }; + } +} diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/controller/Q20ParamController.java b/admin/src/main/java/com/multictrl/modules/business/q20/controller/Q20ParamController.java new file mode 100644 index 0000000..9f93b09 --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/q20/controller/Q20ParamController.java @@ -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 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 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 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 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 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 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 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 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 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 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 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 getBatteryLowThreshold(@PathVariable String deviceSn) { + return new Result<>().ok(q20ParamService.getBatteryLowThreshold(deviceSn)); + } + + // ==================== 严重低电阈值 ==================== + + @PostMapping("/setBatteryCriticalThreshold/{deviceSn}") + @LogOperation("设置严重低电阈值") + @Operation(summary = "设置严重低电阈值(比例)") + @RequiresPermissions("bus:q20:param") + public Result 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 getBatteryCriticalThreshold(@PathVariable String deviceSn) { + return new Result<>().ok(q20ParamService.getBatteryCriticalThreshold(deviceSn)); + } + + // ==================== 紧急低电阈值 ==================== + + @PostMapping("/setBatteryEmergencyThreshold/{deviceSn}") + @LogOperation("设置紧急低电阈值") + @Operation(summary = "设置紧急低电阈值(比例)") + @RequiresPermissions("bus:q20:param") + public Result 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 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 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 getLowBatteryAction(@PathVariable String deviceSn) { + return new Result<>().ok(q20ParamService.getLowBatteryAction(deviceSn)); + } + + // ==================== 精准降落 ==================== + + @PostMapping("/setAprilTagId/{deviceSn}") + @LogOperation("设置精准降落二维码标识") + @Operation(summary = "设置精准降落二维码标识") + @RequiresPermissions("bus:q20:param") + public Result 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 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 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 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 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 getObstacleAvoidanceEnable(@PathVariable String deviceSn) { + return new Result<>().ok(q20ParamService.getObstacleAvoidanceEnable(deviceSn)); + } + + @PostMapping("/setObstacleAvoidanceDistance/{deviceSn}") + @LogOperation("设置避障距离") + @Operation(summary = "设置避障距离(m)") + @RequiresPermissions("bus:q20:param") + public Result 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 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 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 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 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 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 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 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 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 getParachuteSafetyEnable(@PathVariable String deviceSn) { + return new Result<>().ok(q20ParamService.getParachuteSafetyEnable(deviceSn)); + } + + // ==================== 批量 ==================== + + @PostMapping("/setParams/{deviceSn}") + @LogOperation("批量设置参数") + @Operation(summary = "批量设置参数(null字段不下发)") + @RequiresPermissions("bus:q20:param") + public Result 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 getParams(@PathVariable String deviceSn) { + return new Result<>().ok(q20ParamService.getParams(deviceSn)); + } +} diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/controller/Q20RouteController.java b/admin/src/main/java/com/multictrl/modules/business/q20/controller/Q20RouteController.java new file mode 100644 index 0000000..2fea50f --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/q20/controller/Q20RouteController.java @@ -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 nextWayline(@RequestParam(defaultValue = "route") String type) { + return new Result().ok(q20RouteService.nextWayline(type)); + } + + // ==================== 上传航线 ==================== + + @PostMapping("/upload/{deviceSn}") + @LogOperation("上传航线") + @Operation(summary = "上传航线(route_upload)") + @RequiresPermissions("bus:q20:route") + public Result 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 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 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 breakLoopHover(@PathVariable String deviceSn) { + return new Result<>().ok(q20RouteService.breakLoopHover(deviceSn)); + } + + // ==================== 获取航线信息 ==================== + + @GetMapping("/info/{deviceSn}") + @Operation(summary = "获取航线信息(route_info)") + @RequiresPermissions("bus:q20:route") + public Result 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 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 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 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 routeFinish(@PathVariable String deviceSn) { + return new Result<>().ok(q20RouteService.routeFinish(deviceSn)); + } + + // ==================== 获取围栏信息 ==================== + + @GetMapping("/geofenceInfo/{deviceSn}") + @Operation(summary = "获取围栏信息(geofence_info)") + @RequiresPermissions("bus:q20:route") + public Result 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 geofenceUpload(@PathVariable String deviceSn, + @RequestBody Q20GeofenceUploadDTO dto) { + return new Result<>().ok(q20RouteService.geofenceUpload(deviceSn, dto)); + } +} diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20CameraDTO.java b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20CameraDTO.java new file mode 100644 index 0000000..296fe57 --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20CameraDTO.java @@ -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; +} diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20CameraFocalLengthDTO.java b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20CameraFocalLengthDTO.java new file mode 100644 index 0000000..1686b0d --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20CameraFocalLengthDTO.java @@ -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; +} diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20DockAlternatePointDTO.java b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20DockAlternatePointDTO.java new file mode 100644 index 0000000..50d2b12 --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20DockAlternatePointDTO.java @@ -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; +} diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20DockInfoItemDTO.java b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20DockInfoItemDTO.java new file mode 100644 index 0000000..0cf84bb --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20DockInfoItemDTO.java @@ -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; +} diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20DrcDTO.java b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20DrcDTO.java new file mode 100644 index 0000000..b7f9050 --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20DrcDTO.java @@ -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; +} diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20EmergencyStopDTO.java b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20EmergencyStopDTO.java new file mode 100644 index 0000000..605a15d --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20EmergencyStopDTO.java @@ -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; +} diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20GeofenceItemDTO.java b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20GeofenceItemDTO.java new file mode 100644 index 0000000..0df322f --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20GeofenceItemDTO.java @@ -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 vertexCoordinates; + + @Schema(description = "圆形围栏半径(m)") + @JsonProperty("circle_radius") + private Integer circleRadius; + + @Schema(description = "圆形围栏中心坐标,格式:\"经度,纬度\"") + @JsonProperty("circle_coordinate") + private String circleCoordinate; +} diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20GeofenceUploadDTO.java b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20GeofenceUploadDTO.java new file mode 100644 index 0000000..c32c51e --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20GeofenceUploadDTO.java @@ -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 geofenceInfo; +} diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20GimbalControlDTO.java b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20GimbalControlDTO.java new file mode 100644 index 0000000..e61f18a --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20GimbalControlDTO.java @@ -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; +} diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20GoHomeDTO.java b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20GoHomeDTO.java new file mode 100644 index 0000000..6dc2229 --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20GoHomeDTO.java @@ -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; + } +} diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20HalyardingDTO.java b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20HalyardingDTO.java new file mode 100644 index 0000000..30a5373 --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20HalyardingDTO.java @@ -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; +} diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20ImageSwitchDTO.java b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20ImageSwitchDTO.java new file mode 100644 index 0000000..5cfae67 --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20ImageSwitchDTO.java @@ -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; +} diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20LandDTO.java b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20LandDTO.java new file mode 100644 index 0000000..a794310 --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20LandDTO.java @@ -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; +} diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20LiveStartDTO.java b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20LiveStartDTO.java new file mode 100644 index 0000000..6f805ef --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20LiveStartDTO.java @@ -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; +} diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20LogDataDTO.java b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20LogDataDTO.java new file mode 100644 index 0000000..de9087f --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20LogDataDTO.java @@ -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; +} diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20LogListDTO.java b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20LogListDTO.java new file mode 100644 index 0000000..3d88a0f --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20LogListDTO.java @@ -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; +} diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20MountTdaDTO.java b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20MountTdaDTO.java new file mode 100644 index 0000000..fc1ed2c --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20MountTdaDTO.java @@ -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 tda; +} diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20NavigateDTO.java b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20NavigateDTO.java new file mode 100644 index 0000000..ac3433d --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20NavigateDTO.java @@ -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; +} diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20OpenParachuteDTO.java b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20OpenParachuteDTO.java new file mode 100644 index 0000000..bc865b3 --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20OpenParachuteDTO.java @@ -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; +} diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20OtaUpgradeDTO.java b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20OtaUpgradeDTO.java new file mode 100644 index 0000000..aad112c --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20OtaUpgradeDTO.java @@ -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; +} diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20ParamsDTO.java b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20ParamsDTO.java new file mode 100644 index 0000000..c3f38e0 --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20ParamsDTO.java @@ -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; +} diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20QueryStatusDTO.java b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20QueryStatusDTO.java new file mode 100644 index 0000000..5815ab1 --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20QueryStatusDTO.java @@ -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; +} diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20RouteActionDTO.java b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20RouteActionDTO.java new file mode 100644 index 0000000..0ca8973 --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20RouteActionDTO.java @@ -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 actionActuatorFuncParam; +} diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20RouteActionGroupDTO.java b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20RouteActionGroupDTO.java new file mode 100644 index 0000000..666f9ab --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20RouteActionGroupDTO.java @@ -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 action; +} diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20RouteAlternatePointDTO.java b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20RouteAlternatePointDTO.java new file mode 100644 index 0000000..7a8b27b --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20RouteAlternatePointDTO.java @@ -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; +} diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20RouteAutoDTO.java b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20RouteAutoDTO.java new file mode 100644 index 0000000..1d033ed --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20RouteAutoDTO.java @@ -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(route_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; +} diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20RouteDroneInfoDTO.java b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20RouteDroneInfoDTO.java new file mode 100644 index 0000000..8a6d79f --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20RouteDroneInfoDTO.java @@ -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; +} diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20RouteExecuteDTO.java b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20RouteExecuteDTO.java new file mode 100644 index 0000000..27499fc --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20RouteExecuteDTO.java @@ -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航线任务 - 执行航线DTO(route_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 = "航线ID,mode=2时有效") + @JsonProperty("route_id") + private String routeId; + + @Schema(description = "是否指定机库:0=未设置,1=已设置") + @JsonProperty("specific_dock") + private int specificDock; + + @Schema(description = "机库信息列表") + @JsonProperty("dock_info") + private List dockInfo; +} diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20RouteFolderDTO.java b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20RouteFolderDTO.java new file mode 100644 index 0000000..d3d56e7 --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20RouteFolderDTO.java @@ -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 placemark; + + @Schema(description = "备降点列表") + @JsonProperty("alternate_points") + private List alternatePoints; +} diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20RouteMissionConfigDTO.java b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20RouteMissionConfigDTO.java new file mode 100644 index 0000000..dfa13c9 --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20RouteMissionConfigDTO.java @@ -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; +} diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20RoutePayloadInfoDTO.java b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20RoutePayloadInfoDTO.java new file mode 100644 index 0000000..c295452 --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20RoutePayloadInfoDTO.java @@ -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; +} diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20RoutePlacemarkDTO.java b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20RoutePlacemarkDTO.java new file mode 100644 index 0000000..7048c13 --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20RoutePlacemarkDTO.java @@ -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; +} diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20RoutePointDTO.java b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20RoutePointDTO.java new file mode 100644 index 0000000..839ab5a --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20RoutePointDTO.java @@ -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; +} diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20RouteUploadDTO.java b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20RouteUploadDTO.java new file mode 100644 index 0000000..132633c --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20RouteUploadDTO.java @@ -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航线任务 - 上传航线顶层DTO(route_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; +} diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20RouteVehicleConfigDTO.java b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20RouteVehicleConfigDTO.java new file mode 100644 index 0000000..1928689 --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20RouteVehicleConfigDTO.java @@ -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; +} diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20RouteWaypointHeadingDTO.java b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20RouteWaypointHeadingDTO.java new file mode 100644 index 0000000..49f9902 --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20RouteWaypointHeadingDTO.java @@ -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; +} diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20RouteWaypointTurnDTO.java b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20RouteWaypointTurnDTO.java new file mode 100644 index 0000000..5ea2fd6 --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20RouteWaypointTurnDTO.java @@ -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; +} diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20SetHomeDTO.java b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20SetHomeDTO.java new file mode 100644 index 0000000..45a67d8 --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20SetHomeDTO.java @@ -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; +} diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20TakeoffDTO.java b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20TakeoffDTO.java new file mode 100644 index 0000000..4f65ecd --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20TakeoffDTO.java @@ -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; +} diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20ThrowingDTO.java b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20ThrowingDTO.java new file mode 100644 index 0000000..992a003 --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20ThrowingDTO.java @@ -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; +} diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/handler/Q20Constant.java b/admin/src/main/java/com/multictrl/modules/business/q20/handler/Q20Constant.java index c3a5602..a94f4a5 100644 --- a/admin/src/main/java/com/multictrl/modules/business/q20/handler/Q20Constant.java +++ b/admin/src/main/java/com/multictrl/modules/business/q20/handler/Q20Constant.java @@ -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; } diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/handler/Q20StatusHandler.java b/admin/src/main/java/com/multictrl/modules/business/q20/handler/Q20StatusHandler.java new file mode 100644 index 0000000..a8689bc --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/q20/handler/Q20StatusHandler.java @@ -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_reply(output为空对象) + */ + 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_reply(output含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); + } +} diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/service/Q20CommandService.java b/admin/src/main/java/com/multictrl/modules/business/q20/service/Q20CommandService.java new file mode 100644 index 0000000..861f585 --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/q20/service/Q20CommandService.java @@ -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); +} diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/service/Q20DeviceService.java b/admin/src/main/java/com/multictrl/modules/business/q20/service/Q20DeviceService.java new file mode 100644 index 0000000..e4ba9d7 --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/q20/service/Q20DeviceService.java @@ -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 getOnlineStatusBatch(List deviceSnList); +} diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/service/Q20ParamService.java b/admin/src/main/java/com/multictrl/modules/business/q20/service/Q20ParamService.java new file mode 100644 index 0000000..986603a --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/q20/service/Q20ParamService.java @@ -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); +} diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/service/Q20RouteService.java b/admin/src/main/java/com/multictrl/modules/business/q20/service/Q20RouteService.java new file mode 100644 index 0000000..d3a9793 --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/q20/service/Q20RouteService.java @@ -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); +} diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/service/impl/Q20CommandServiceImpl.java b/admin/src/main/java/com/multictrl/modules/business/q20/service/impl/Q20CommandServiceImpl.java new file mode 100644 index 0000000..ab4400d --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/q20/service/impl/Q20CommandServiceImpl.java @@ -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); + } +} diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/service/impl/Q20DeviceServiceImpl.java b/admin/src/main/java/com/multictrl/modules/business/q20/service/impl/Q20DeviceServiceImpl.java new file mode 100644 index 0000000..74d8a85 --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/q20/service/impl/Q20DeviceServiceImpl.java @@ -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 getOnlineStatusBatch(List deviceSnList) { + if (CollectionUtils.isEmpty(deviceSnList)) { + return Collections.emptyList(); + } + return deviceSnList.stream().map(this::getOnlineStatus).collect(Collectors.toList()); + } +} diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/service/impl/Q20ParamServiceImpl.java b/admin/src/main/java/com/multictrl/modules/business/q20/service/impl/Q20ParamServiceImpl.java new file mode 100644 index 0000000..67005bb --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/q20/service/impl/Q20ParamServiceImpl.java @@ -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"); + } +} diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/service/impl/Q20RouteServiceImpl.java b/admin/src/main/java/com/multictrl/modules/business/q20/service/impl/Q20RouteServiceImpl.java new file mode 100644 index 0000000..e302dc6 --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/q20/service/impl/Q20RouteServiceImpl.java @@ -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() + .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() + .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 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() + .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() +// .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() + .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().eq("route_id", oldId)); + waypointActionDao.delete(new QueryWrapper().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 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 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() + .eq("q20_route_id", waylineId) + .eq("template_type", TEMPLATE_TYPE_Q20)); + } + if (route == null) { + route = routeDao.selectOne(new QueryWrapper() + .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; + } +} diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/vo/Q20DeviceStatusVO.java b/admin/src/main/java/com/multictrl/modules/business/q20/vo/Q20DeviceStatusVO.java new file mode 100644 index 0000000..ffb10f2 --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/q20/vo/Q20DeviceStatusVO.java @@ -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; +} diff --git a/admin/src/main/java/com/multictrl/modules/business/service/DJIBaseService.java b/admin/src/main/java/com/multictrl/modules/business/service/DJIBaseService.java index 97fcc67..a62bbbd 100644 --- a/admin/src/main/java/com/multictrl/modules/business/service/DJIBaseService.java +++ b/admin/src/main/java/com/multictrl/modules/business/service/DJIBaseService.java @@ -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); } diff --git a/admin/src/main/java/com/multictrl/modules/business/service/impl/DJIBaseServiceImpl.java b/admin/src/main/java/com/multictrl/modules/business/service/impl/DJIBaseServiceImpl.java index 9fd3c19..580d46e 100644 --- a/admin/src/main/java/com/multictrl/modules/business/service/impl/DJIBaseServiceImpl.java +++ b/admin/src/main/java/com/multictrl/modules/business/service/impl/DJIBaseServiceImpl.java @@ -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 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 diff --git a/admin/src/main/java/com/multictrl/modules/security/config/ShiroConfig.java b/admin/src/main/java/com/multictrl/modules/security/config/ShiroConfig.java index 71ab1cf..b03fd4d 100644 --- a/admin/src/main/java/com/multictrl/modules/security/config/ShiroConfig.java +++ b/admin/src/main/java/com/multictrl/modules/security/config/ShiroConfig.java @@ -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"); diff --git a/admin/src/main/resources/application-docker.yml b/admin/src/main/resources/application-docker.yml index 33dd006..3d763ee 100644 --- a/admin/src/main/resources/application-docker.yml +++ b/admin/src/main/resources/application-docker.yml @@ -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 diff --git a/admin/src/main/resources/static/q20-ctrl.html b/admin/src/main/resources/static/q20-ctrl.html new file mode 100644 index 0000000..4f0a316 --- /dev/null +++ b/admin/src/main/resources/static/q20-ctrl.html @@ -0,0 +1,2891 @@ + + + + + +Q20 飞行控制台 + + + + + + + + +
+ + Q20 Control + +
+ + +
+ 请先填写设备 SN + +
+ + +
+
+ + +
+ + +
字段标记:* 必填项 选填 非必填项(依据 doc 协议文档)
+ +
+ + +
+
+ +
+
起飞
+
+
takeoff — 起飞到指定高度
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
降落
+
+
land — 降落
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
紧急悬停
+
+
stop — 立即悬停 (value 固定=1)
+

无需参数,发送后飞机立即悬停

+ + +
+
+ +
+
返航
+
+
goHome — 返回 Home 点
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+
+ + + +
+
+ +
+
设置返航点
+
+
setHome — 设置 Home 点坐标
+
+ + +
+ + + +
+
+ +
+
航点飞行
+
+
navigate — 飞往指定坐标
+
+
+
+
+
+
+ + +
+
+
+ + +
+
+ +
+
+ + +
+
+ +
+
云台控制
+
+
gimbalControl — 控制云台角度
+
+
+
+ + +
+
+
+
+
+ + +
+
+ +
+
拍照
+
+
cameraPhotoTake — 拍摄一张照片
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
录像控制
+
+
cameraRecordingStart / Stop
+
+
+
+ + +
+
+ + +
+
+
+ + +
+ +
+
+ +
+
焦距控制
+
+
cameraFocalLengthSet — 设置变焦
+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
图像切换
+
+
imageSwitch — 红外/可见光/激光
+
+
+
+ + +
+
+
+
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ +
+
开始直播推流
+
+
liveStartPush
+
+
+ + +
+
+ + +
+
+
+
+
+
+ + +
+
+ +
+
停止直播推流
+
+
liveStopPush
+
+
+
+
+ + +
+
+ +
+
+ + +
+
+ +
+
充电控制
+
+
deviceCharge
+
+
+
+
+ + +
+
+ +
+
重启 / 关机
+
+
deviceBoot
+
+
+
+ + +
+
+ +
+
安全开关
+
+
safetySwitch
+
+
+
+ + +
+
+ +
+
下视觉图像
+
+
stereoImage
+
+
+
+ + +
+
+ +
+
获取设备配置
+
+ + +
+
+ +
+
固件版本
+
+ + +
+
+ +
+
+ + +
+
+ +
+
投掷锁控制
+
+
throwingLock
+
+
+
+
+ + +
+
+ +
+
执行投掷
+
+
throwingExecution
+
+
+
+ + +
+
+ +
+
绞车控制
+
+
halyarding
+
+
+
+
+
+
+ + +
+
+ +
+
物流控制
+
+
logistics
+
+
+
+
+ + +
+
+ +
+
+ + +
+
+
+
+
虚拟摇杆 (DRC)
+
+

高频指令,不等回复。按住持续发送,松开停止。

+
+ +
+
平移 (vx / vy)
+
+
+ +
+ +
+ +
+ +
+ +
+
+
+ +
+
升降 / 偏航 (vz / vyaw)
+
+
+ +
+ +
+ +
+ +
+ +
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
手动输入 DRC 指令
+
+
+
+
+
+
+
+ +
+
+
+
+
+ + +
+
+ +
+
自动开伞开关
+
+
setAutoParachute
+
+
+
+ + +
+
+ +
+
紧急停止 (断电)
+
+
emergencyStop — 需先获取 OTP
+ + + + + + +
+
+ +
+
开伞
+
+
openParachute — 需先获取 OTP (99=强制)
+ + + + + + +
+
+ +
+
确认开伞安全
+
+
confirmParachuteSafety
+
+
+
+ + +
+
+ +
+
+ + +
+
+
+
查询飞行状态
+
+
queryStatus
+
+
+
+
+
+
+ + +
+
+
+
+ + +
+
+ +
+
日志数量
+
+ + +
+
+ +
+
日志列表
+
+
+
+
+
+ + +
+
+ +
+
上传日志
+
+
+
+
+
+ + +
+
+ +
+
+ + +
+
+
+
OTA 固件升级
+
+
otaUpgrade — 推送固件升级任务
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+ + +
+ + +
+
+
航线快捷控制
+
+
+ + + + + +
+ +
+
+
+ + +
+
上传航线 (route_upload)
+
+
+ +
+
基础配置
+
+
+
+
+ + +
+
+
+ + +
+
+ + +
+ +
+
+
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+
+
+
+ + +
+
+
+
+ +
+
+
航点列表 (placemark)
+
+ + + +
+
+
+ +
+
+
+
+ + +
+ +
+
+ +
+ + +
+
获取航线信息
+
+
routeInfo — 查询已上传的航线
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
执行航线
+
+
route_execute — 执行已上传的航线
+
+
+ + +
+
+ + +
+
+ + +
+
+ + + +
+
+ + +
+
围栏管理
+
+
geofenceInfo / geofenceUpload
+ + + + + +
+
+ +
+ + +
+
一键飞行 (route_auto = route_info + execute_info + vehicle_config)
+
+

一次性下发「航线信息 + 执行信息 + 无人机设置」,设备上传航线后立即执行。本表单与上方「上传航线」相互独立。

+
+ +
+
航线信息 (route_info)
+
+
+
+
+ + +
+
+
+ + +
+
+ + +
+ +
+
+
+
+ +
+
+ + +
+
+ + +
+
+
+
+
+
+
+
+ + +
+
+
+ +
无人机设置 (vehicle_config)
+
+
+ + +
+
+
+ + +
+
+ +
执行信息 (execute_info)
+
+
+ + +
一键飞行即上传并执行本次航线,固定为「执行最近上传」
+
+
+ + +
+ +
+
+ +
+
+
航点列表 (placemark)
+
+ + + +
+
+
+ +
+
+
+
+ + +
+ +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + +
+ + + diff --git a/admin/src/main/resources/static/q20-login.html b/admin/src/main/resources/static/q20-login.html new file mode 100644 index 0000000..b0f1315 --- /dev/null +++ b/admin/src/main/resources/static/q20-login.html @@ -0,0 +1,126 @@ + + + + + +Q20 登录 + + + + + + +
+
+ +
Q20 飞行控制台
+ Drone Control Panel +
+ +
+ + +
+
+ + +
+
+ + +
+
+ +
+ + captcha +
+
+ + + +
+ + + + diff --git a/doc/Q20参数指令.docx b/doc/Q20参数指令.docx new file mode 100644 index 0000000..b332363 Binary files /dev/null and b/doc/Q20参数指令.docx differ diff --git a/doc/Q20控制指令.docx b/doc/Q20控制指令.docx new file mode 100644 index 0000000..95239d8 Binary files /dev/null and b/doc/Q20控制指令.docx differ diff --git a/doc/Q20航线任务.docx b/doc/Q20航线任务.docx new file mode 100644 index 0000000..6f27500 Binary files /dev/null and b/doc/Q20航线任务.docx differ diff --git a/doc/Q20设备拓扑状态.docx b/doc/Q20设备拓扑状态.docx new file mode 100644 index 0000000..0aba473 Binary files /dev/null and b/doc/Q20设备拓扑状态.docx differ diff --git a/prj-deploy/file/pgsql/init.sql b/prj-deploy/file/pgsql/init.sql index d8704ff..95d5a53 100644 --- a/prj-deploy/file/pgsql/init.sql +++ b/prj-deploy/file/pgsql/init.sql @@ -2071,4 +2071,10 @@ ON COLUMN "public"."bus_geo_mark"."create_date" IS '创建时间'; COMMENT ON COLUMN "public"."bus_geo_mark"."creator" IS '创建人'; COMMENT -ON TABLE "public"."bus_geo_mark" IS '地图标注'; \ No newline at end of file +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'; \ No newline at end of file