From e23e95c511761f621a320b9cf4d9d3b7a8443e3e Mon Sep 17 00:00:00 2001 From: "938693313@qq.com" <938693313@qq.com> Date: Wed, 27 May 2026 13:56:39 +0800 Subject: [PATCH] =?UTF-8?q?1.=E6=96=B0=E5=A2=9EQ20=E9=A3=9E=E6=9C=BA?= =?UTF-8?q?=E5=8F=82=E6=95=B0=E6=8C=87=E4=BB=A411=E4=B8=AA=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=202.=E6=96=B0=E5=A2=9EQ20=E9=A3=9E=E6=9C=BA=E6=8E=A7?= =?UTF-8?q?=E5=88=B6=E6=8C=87=E4=BB=A412=E4=B8=AA=E6=8E=A5=E5=8F=A3=203.?= =?UTF-8?q?=E6=96=B0=E5=A2=9EQ20=E9=A3=9E=E6=9C=BA=E8=88=AA=E7=BA=BF?= =?UTF-8?q?=E4=BB=BB=E5=8A=A113=E4=B8=AA=E6=8E=A5=E5=8F=A3=204.=E6=96=B0?= =?UTF-8?q?=E5=A2=9EQ20=E9=A3=9E=E6=9C=BA=E8=AE=BE=E5=A4=87=E6=8B=93?= =?UTF-8?q?=E6=89=91=E7=8A=B6=E6=80=812=E4=B8=AA=E6=8E=A5=E5=8F=A3=205.?= =?UTF-8?q?=E6=96=B0=E5=A2=9EQ20=E9=A3=9E=E6=9C=BA=E5=88=87=E9=9D=A2?= =?UTF-8?q?=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/aspect/Q20ServiceLogAspect.java | 51 + .../config/Jackson2ObjectMapperConfig.java | 24 + .../modules/business/dto/RouteDTO.java | 4 + .../modules/business/entity/RouteEntity.java | 4 + .../business/handler/TopicDistributor.java | 10 +- .../q20/controller/Q20CommandController.java | 344 ++ .../q20/controller/Q20Controller.java | 43 + .../q20/controller/Q20MockController.java | 77 + .../q20/controller/Q20ParamController.java | 332 ++ .../q20/controller/Q20RouteController.java | 152 + .../business/q20/dto/Q20CameraDTO.java | 24 + .../q20/dto/Q20CameraFocalLengthDTO.java | 33 + .../q20/dto/Q20DockAlternatePointDTO.java | 29 + .../business/q20/dto/Q20DockInfoItemDTO.java | 48 + .../modules/business/q20/dto/Q20DrcDTO.java | 27 + .../business/q20/dto/Q20EmergencyStopDTO.java | 18 + .../business/q20/dto/Q20GeofenceItemDTO.java | 37 + .../q20/dto/Q20GeofenceUploadDTO.java | 22 + .../business/q20/dto/Q20GimbalControlDTO.java | 30 + .../business/q20/dto/Q20GoHomeDTO.java | 47 + .../business/q20/dto/Q20HalyardingDTO.java | 27 + .../business/q20/dto/Q20ImageSwitchDTO.java | 33 + .../modules/business/q20/dto/Q20LandDTO.java | 21 + .../business/q20/dto/Q20LiveStartDTO.java | 33 + .../business/q20/dto/Q20LogDataDTO.java | 21 + .../business/q20/dto/Q20LogListDTO.java | 21 + .../business/q20/dto/Q20MountTdaDTO.java | 26 + .../business/q20/dto/Q20NavigateDTO.java | 33 + .../business/q20/dto/Q20OpenParachuteDTO.java | 18 + .../business/q20/dto/Q20OtaUpgradeDTO.java | 36 + .../business/q20/dto/Q20ParamsDTO.java | 70 + .../business/q20/dto/Q20QueryStatusDTO.java | 30 + .../business/q20/dto/Q20RouteActionDTO.java | 30 + .../q20/dto/Q20RouteActionGroupDTO.java | 37 + .../q20/dto/Q20RouteAlternatePointDTO.java | 23 + .../business/q20/dto/Q20RouteAutoDTO.java | 28 + .../q20/dto/Q20RouteDroneInfoDTO.java | 24 + .../business/q20/dto/Q20RouteExecuteDTO.java | 33 + .../business/q20/dto/Q20RouteFolderDTO.java | 41 + .../q20/dto/Q20RouteMissionConfigDTO.java | 64 + .../q20/dto/Q20RoutePayloadInfoDTO.java | 24 + .../q20/dto/Q20RoutePlacemarkDTO.java | 54 + .../business/q20/dto/Q20RoutePointDTO.java | 18 + .../business/q20/dto/Q20RouteUploadDTO.java | 45 + .../q20/dto/Q20RouteVehicleConfigDTO.java | 28 + .../q20/dto/Q20RouteWaypointHeadingDTO.java | 32 + .../q20/dto/Q20RouteWaypointTurnDTO.java | 24 + .../business/q20/dto/Q20SetHomeDTO.java | 30 + .../business/q20/dto/Q20TakeoffDTO.java | 21 + .../business/q20/dto/Q20ThrowingDTO.java | 21 + .../business/q20/handler/Q20Constant.java | 9 + .../q20/handler/Q20StatusHandler.java | 97 + .../q20/service/Q20CommandService.java | 107 + .../q20/service/Q20DeviceService.java | 18 + .../business/q20/service/Q20ParamService.java | 84 + .../business/q20/service/Q20RouteService.java | 117 + .../service/impl/Q20CommandServiceImpl.java | 420 +++ .../service/impl/Q20DeviceServiceImpl.java | 44 + .../q20/service/impl/Q20ParamServiceImpl.java | 311 ++ .../q20/service/impl/Q20RouteServiceImpl.java | 465 +++ .../business/q20/vo/Q20DeviceStatusVO.java | 24 + .../business/service/DJIBaseService.java | 6 + .../service/impl/DJIBaseServiceImpl.java | 61 +- .../modules/security/config/ShiroConfig.java | 2 + .../src/main/resources/application-docker.yml | 2 +- admin/src/main/resources/static/q20-ctrl.html | 2891 +++++++++++++++++ .../src/main/resources/static/q20-login.html | 126 + doc/Q20参数指令.docx | Bin 0 -> 25452 bytes doc/Q20控制指令.docx | Bin 0 -> 42102 bytes doc/Q20航线任务.docx | Bin 0 -> 42811 bytes doc/Q20设备拓扑状态.docx | Bin 0 -> 5502 bytes prj-deploy/file/pgsql/init.sql | 8 +- 72 files changed, 7082 insertions(+), 12 deletions(-) create mode 100644 admin/src/main/java/com/multictrl/common/aspect/Q20ServiceLogAspect.java create mode 100644 admin/src/main/java/com/multictrl/common/config/Jackson2ObjectMapperConfig.java create mode 100644 admin/src/main/java/com/multictrl/modules/business/q20/controller/Q20CommandController.java create mode 100644 admin/src/main/java/com/multictrl/modules/business/q20/controller/Q20Controller.java create mode 100644 admin/src/main/java/com/multictrl/modules/business/q20/controller/Q20MockController.java create mode 100644 admin/src/main/java/com/multictrl/modules/business/q20/controller/Q20ParamController.java create mode 100644 admin/src/main/java/com/multictrl/modules/business/q20/controller/Q20RouteController.java create mode 100644 admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20CameraDTO.java create mode 100644 admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20CameraFocalLengthDTO.java create mode 100644 admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20DockAlternatePointDTO.java create mode 100644 admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20DockInfoItemDTO.java create mode 100644 admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20DrcDTO.java create mode 100644 admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20EmergencyStopDTO.java create mode 100644 admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20GeofenceItemDTO.java create mode 100644 admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20GeofenceUploadDTO.java create mode 100644 admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20GimbalControlDTO.java create mode 100644 admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20GoHomeDTO.java create mode 100644 admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20HalyardingDTO.java create mode 100644 admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20ImageSwitchDTO.java create mode 100644 admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20LandDTO.java create mode 100644 admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20LiveStartDTO.java create mode 100644 admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20LogDataDTO.java create mode 100644 admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20LogListDTO.java create mode 100644 admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20MountTdaDTO.java create mode 100644 admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20NavigateDTO.java create mode 100644 admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20OpenParachuteDTO.java create mode 100644 admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20OtaUpgradeDTO.java create mode 100644 admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20ParamsDTO.java create mode 100644 admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20QueryStatusDTO.java create mode 100644 admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20RouteActionDTO.java create mode 100644 admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20RouteActionGroupDTO.java create mode 100644 admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20RouteAlternatePointDTO.java create mode 100644 admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20RouteAutoDTO.java create mode 100644 admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20RouteDroneInfoDTO.java create mode 100644 admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20RouteExecuteDTO.java create mode 100644 admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20RouteFolderDTO.java create mode 100644 admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20RouteMissionConfigDTO.java create mode 100644 admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20RoutePayloadInfoDTO.java create mode 100644 admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20RoutePlacemarkDTO.java create mode 100644 admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20RoutePointDTO.java create mode 100644 admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20RouteUploadDTO.java create mode 100644 admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20RouteVehicleConfigDTO.java create mode 100644 admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20RouteWaypointHeadingDTO.java create mode 100644 admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20RouteWaypointTurnDTO.java create mode 100644 admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20SetHomeDTO.java create mode 100644 admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20TakeoffDTO.java create mode 100644 admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20ThrowingDTO.java create mode 100644 admin/src/main/java/com/multictrl/modules/business/q20/handler/Q20StatusHandler.java create mode 100644 admin/src/main/java/com/multictrl/modules/business/q20/service/Q20CommandService.java create mode 100644 admin/src/main/java/com/multictrl/modules/business/q20/service/Q20DeviceService.java create mode 100644 admin/src/main/java/com/multictrl/modules/business/q20/service/Q20ParamService.java create mode 100644 admin/src/main/java/com/multictrl/modules/business/q20/service/Q20RouteService.java create mode 100644 admin/src/main/java/com/multictrl/modules/business/q20/service/impl/Q20CommandServiceImpl.java create mode 100644 admin/src/main/java/com/multictrl/modules/business/q20/service/impl/Q20DeviceServiceImpl.java create mode 100644 admin/src/main/java/com/multictrl/modules/business/q20/service/impl/Q20ParamServiceImpl.java create mode 100644 admin/src/main/java/com/multictrl/modules/business/q20/service/impl/Q20RouteServiceImpl.java create mode 100644 admin/src/main/java/com/multictrl/modules/business/q20/vo/Q20DeviceStatusVO.java create mode 100644 admin/src/main/resources/static/q20-ctrl.html create mode 100644 admin/src/main/resources/static/q20-login.html create mode 100644 doc/Q20参数指令.docx create mode 100644 doc/Q20控制指令.docx create mode 100644 doc/Q20航线任务.docx create mode 100644 doc/Q20设备拓扑状态.docx 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 0000000000000000000000000000000000000000..b3323639b0c25e70873affea439957c402003c7d GIT binary patch literal 25452 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 literal 0 HcmV?d00001 diff --git a/doc/Q20控制指令.docx b/doc/Q20控制指令.docx new file mode 100644 index 0000000000000000000000000000000000000000..95239d814e42ab76239dd449e698bb06bc289e1c GIT binary patch literal 42102 zcmagF18}5GzdjspY}>YN+qOBeZQD*Z+<0SaV{Ghfw6X0?@}GU)gZET@=X|M}o~dN+ z)ZKk|Uwz@%%5vZk=pfM0&>$`T&H5n!_JaoetS9E^;AY|AW~A=zWZ`PS=moHEN&D&W zgBdx@h~la**cbbj*dUx4A9@Kf^uki{%^)@H zFzAAGTJnmh%*qUv6Gx5E?m~IOQpAFOHARDntQo^1)w7Joqg{V216T|*yB5-s6g)>o z(K~T9gxINI{BT_{OM_;L{6BDesY)$$w(z2kG(nmiaE^@BybR2L&zKvHK|t3Iak32?C=0zdi~$;G-~dv9Nb#V*K0IrfkZB zvmi?vQB3)BZNQSwH*=BRE0zlOCry3KDp1Nx0=Bn^Lt5=c1}2$braiOFdjv+tyOYzB zQ$sBlLcZ?v#8rA;HwNne3Z{vn*%^w)&->Qz(G*Y)(;_*+d}vBbh5;37+uPRq988)K z!B)n5qD^WmAnVBX3jS!W1@N^ypaP%Hw#338W27HASMgDWx}qQW@>rjo!ik|TRUzM3 zc~FNZAmC?Vwa(-al;5V~zoDK?wVN}P_o)|=AWNgHy}vG8Y_|D=QId;8HX|CS*7-rt znPMtDtaKWlB=82Ai}ZJBn7S8oegiu$5B7fz4KnZ>=8k5{E{;yFOeRiFf2XKI!T>CA zii%$aNR0Jto{EUir-eWloQnt%mu_&*+KV>I3T@{t?oBZ#R&@TX{~b@$C0zS!B_3Ux zGcMs!Q;=xol$@_eO$-(Vjc=+LpSsP(B*7}k9_yZR#iG1Xp2T~Qq8#T3J5Ow;u@PbP z?ku}he|)kee)Th?Z0#$ymK6dP)TWeLl4<)k`kyD*0K`}DzunpO^IS~^-fSE2^}j!9 zlz;DznWKxv-~IrS4CDrx(IY-($NYWgFU4R0k<{QK&O7G64WttQ4GN;D7mrs8XEjAz z6yAk*PPdr3X4}a~ZoM3xr8*PTA`J1i?gsM}@0=Z_Lr`DSkK;yC3T77+G^8iXoZDST zis2Z1PzBaqZRFO&jCq1G5gvb3B+s(=anxav7#bjGj&Z4w?Z?%A9VJeK29q=~wIUOs zOV3Mx+;mSBmgz_IT^tb0u)H4p;@0!&X)J4bd%Tg28$m*ViH5~BvGekA>8vYg^YH8$ z-Ap@e%SaFt6eA9bM1@E9qKFuKmiwNe$uqeEqesEB(FE74e`E2X+F$>o^6bu8*BKMe zF71KTM#d$CbzO0IiuQLD;aqplp@8e90r>jAN72*K#heK^Gu#0{{Q5hJ=RE+|4b2S3 z*Y^)tOd$%jcx)yJnf7x3s)bFqg`-m6@=o9KnO|xZ>L4)d-O1tz5<1SF7P7LWav2nI z2?x&-jb>{JwFze9C)|xSna88-pdp2Suo#0Bl*u7uAjnhtM^dXCbkITVYpgC>Rt$Xw z?B4f2pUus=J#-inj^Ij!5?zVD-(>OT(3szqUJ_&uGr6znP!hOyNDHhCNvZ9fl zTGLn>GV=xm+#cP?mCdidxE_tLWiFz4{|Wt(#cTV)phcZ9Msc_Dd)PHK+|;8+d+giZ z-r>7s#LNwSE^8mFDiFRe!LfdZr`F!?!di^ z)uRfVE>nqq>ISN~mKBcboRzpfNF{w%zrPj2mXtZ)~`h%!mvmWWu0Qznyz7%%2hO<&0VnmBYHn zn`!9)_?xtLKP|qBMgPf;Yi`o!?p2|0+3t?Dxc&2)={#cX|BIT|ikQNp{jr48!{S-# zkRx@OVXCl<&P-v3`STCn8_Q3lu>jJQ&yTN_mgQeI`Y=ZFj`V1`YSvR2;jyOLCaP$w zVfSh6oDBR`XwqcElk0kQr;NH6Hx=$ES9KnY`2C)QI-f5)%zh|G2&L4cQ_8wm9^4jw zTPwL*27Nv<6|{T5D}K4l3^ySKB%n#KMmPv!rpUCuIr0$|+WGiJ$5DxW^l^4h72+~F z4+!1qb$CI%&l!ztOc&V(wSBD3-&^MC^^tu4dEDAD*zqboO%U(D<67r=cru20GismQ z+EvQX&TKx1NB<1b+Bnr zB+oLjKCb{ca($onc$hwW6fUghGjSTW7S12cE63ja+z~Psath@JK@H}U&aCU%=zDpp zw7Iwu1AHVq@5Q&?rw1G*pRga>buDENPYhTxFF7gycx_~)2i)awHhsNNvSAYvmW*fZ zbT3tg+CDkMJII&Vy90k;XHe5lw+mbf%&W5xsQ(!V2zYh~{M!8#-^yNpai!}#eRw5Y z6K`t3GOR23i=E4H$v1Z7HkhKN*5B+>4{*Zv{#fn#=BjXus|>R1C}h*51%Sw_xLAc;d9NsIJ4-$TG!d>h~(jhkJt48Tv6b}DE2KU^BmJ| zJ9ZUJu}$THy58^Fdit!KTnZ}&Mr-p}+ojkacAmns)cL{nNS);)TN4L7bPS-;t$><*FVx8 zK0T7a5J84l3!6jXgOrOLR-40hr@(dp7&p;Kwq2AX_)_Q+b6#$))CbmX=PeQG59^m> zTZ>9q5D)-&!_ua*Ncu5tmFF5wW zmJ=fKN(bocuKyx1^9P@IflDFE#KdHd2_9`V3yNj+^Y5fgjgk>(>1?gPqAEhf;-g2_ z()7G<*_@a#d>AkTIA|LA#o;Kh{BjD|`J+sII3Qn6PKh#nY}lGdB=o_MSNL66KYM$* zqWq!0kEM0zaTcEa7h$^qdW<)7Doyy`f9&M@Epjy-Pt=b@9+2Z4i3PzFCCGwg$(o-u z3+*PBSa$*W5`8rQ2MagUqHD;;mYz=Z&|(01!rVnGndR7QKSR30R*M{Y+6Fra_7+)2 znr9~^9)vp<9{Eu2WUX0t?hemOBn#hR67r2*0)u2l4$4`nS^dTn0 zsJqTXo0v7zQ97YHnN8wL|6>1+(vx!=!^YcoNh|uasgbaCw9@K#Ds~l@IEzWtTK{}) zc)g47*DmsVu=TgupXc(|{as&*7qgb6$)0T#(mK8<)C=O@ss7>8*I+1D!b3>Nq=NqN z#kE|oPlfK{5ss1aVSWj>(}oW>o4~mlNFR5=cJ)}FpP0^=eHFl1Z8&YU=o&~O6geU# zB*0aNSsR$x)lQY1Uih)32Y4x`=`%WrwH-M{wVa%VHK}77nwn_}A6HqFw@uIf7zC(s z_G!pv!s-;e4dsc-Bbau+%NjMGI=#y>j%Ot4hlgTXk=QtoROshsxIaJtGG9@cJeSi) z9>!$0X4CWamfHR^H_Awa90_S^cGeQ`^o6(_`}f3?@A}}`15K7v;ykhw;WNPED!BC_BMQ3F z=9`+yxTSjqWA0OxuV?(vimNuG7GuF5h@D+8V~H7Hwj4X`bJej}`@5>W?7OGj13R-| ztU7wlmH#Zzj@=Ob)AmgY&`Idn%3HU#KMN1Q@$35#C&aX`Jk8T-tAK4F%e6o{FAdt!YFmqjKd39FEgv5XqU` zg88VB6BO>My{JM&j?#hGtWhX$#v498>m99`u}v;AoJ<#lBxil?82QbH9dA%; znLHmI-j!nk{VoPZ@0F2UxEf36$|?kSy{7TsUpj-PB9#XBSK9%bG-H%#HKTgbItf^F zWLFB#k}C$B*&cQ(8wNJ>NNx6wuhm~fC4QtTNWzOTi>ZN5(&Tb2V@)MejU*{A(=wB% zxsy6*fG|S3AyAvpv3*-sPow4Oo_)Us_4m5QSzH1Jxg{x5YGAGu3hlQ#Wd(ql_ zXm$?fsA%8gFKr`Y(h&O`9!zo0gZxZnc`@DV^|TxNoG8h@CWD^4^T#W#>`*l+K%pRAPjB3_;jnYZ#SErFTJ89{h9_P z5DaM+H3&eKrt@!!Ag&E+cCjo?CohziDIR+xsU7<9LpM4V9r})T8P1ME5plI!&0N}R z?QG9}U{w!Hj@21SD`5S>Q?C~%>EtjIO$-!+!bD)p)O}HMo32nE_!KQn_Ks$n%W@Ak z&klMnD4l*m)4&{slbx^lkd!7Vn=y+if=?LnMn|E7qe04}Gpl@GfIx!;jOgTl2m`HvxNk7sHB}*qgsH$W-xrXC zIG%38dCX-TN_K&!L_$bGkV_yEL9Kw}Ht1JJT2cohF&Ob)Sg=o&^ABW(?oqkZ)!ET1 zy>ugaTV3I0L;L?c)!}Jv~ILW(f<`FXA{UT(VV3JLAWYpDp zGG%E`g67Q@=;>nI7R=84mb?Po#$fz?y}o(JzG?`LHIkd}UHOj>jvEeii_H&)6KXdF z=*>3#T!A!yeJ$Sz$YSu(pTuZkudFKBtnHkB_dUGgfeYRfMff%Dh9}Kdxu(OP5U*(b zCTn|`i#4Ar^RxKDD;9@-SwED}dmgx%aH%%+x1RbxY!x^78S}0T-deY&E1gQ(W9DZH z;*=<(&ZHtrj)R)+i}JTsb9#tVt|4YaUtql73%hN~NU8?_WR6zsO7HAVT+o~EG_wgx z^7WR{S;3C-=C9*VtUJmAVQ6IBUb5s$hfDZ8G0q!^c0#O&eshv!tqJlRwN`=bAcLIq zkxdmm2600m7b-|%+j}HkJH&KYErgIe?H?k(S1LiI3TQP8d;hnbZ!1IRwIDzTG{G;Vey?6Q4zga}+ zXZkVO1()8l0$keZ@Ab`PTvv}rxL8mZPUYR7V*;n~8X-OyA}N$9>|L+40F?l8o5v+d zQW>@&nH+&rlN58;lr&^W%E>0$)Uj(;va%HX)M;{Yid+>6!C_xHPX3H?8fQOV3fPp2 zu0=Es|3e#23tQdDovpfu=HHU6NxOibWK{KqX(ePe~+S+O2mLULnk6)!-X? zTfhCR*D|zEeik|Sz$(>IIGA&*dEFt=+wwK7aa7m3#L?4JhKRw~g-0z0@zCz4%>Ttw zNE4C8Jo;Y<=lOX8%be%-llI=el*kO=DpIIdH|2Pyw!osAn(8;S96)!~y>EJ@u~Hsq zwb0Ty>DgNCX_!0R#M!4Js7)5gim27R6v=YwZw)uR4%WvbYFuKb4IxbFp|@SIP=Rt= zgkyb$FjY)LMgF|t$(ji9*{dr?{!BKoZ57gIhjExj9`h%tirIJS;z=VUJC9!%=cqun zT}FRHY3@LG*h(p}sm$~WioRh;K>y_HvdbM8IO~dEraWTcYumdQT`I4Eo{7(X63z5+ zCq4SxpX^q*rf%P!?zT;c1U=MtSNAkFg&y+Y>P!t+`8q^L@iI@-TTl44?XeHZN6-DI zEa3aMTF$2AcLkpQv5#^>Q}aM6ILFdXWh9nyLlT(~kQ~#ZIX3=~B0dFZmL{4n9St_z zLFL#$40?H*z0~5kLab0A{N))e7gO8)`?>G2xkjc<@h8FTV+JIdu#|f`fwR>_Lb>PF z7FOf|mUtVOcjx3X<>$<f9hBV)h)S#QLuUs;V^`>{{`99zrEH)7NbPiqeGL0T4!rYPM-Vfe6T zhSY%jhKkh^rM*f)e7I{~fAp+!dzsRDKQh_@u8hQX%5-&{D`Yzv3nCcPpZQdvPN)F> zW^JOBH>d~`++QGjPILjJiNXT9-#jO-1i97}Ca~3srD>f|x9};shmURL3bWC3n9l z!57(O3)zG``B{GZg#fki&9*-I8NYw%h(2>ybsT-Bv@C9dyph$BLQoEVG#a1jJB&(- z4pH<>M_%4|B~P%*P+UJbYc@JrH0q?eIA_V2t>|xzH9f?uhDEFBy#*STB>v5^fkd_=6Y`f?f5L@MZsE@h<#LeXVCtrs+A=sL6zLTp|4@2_m6N{na6en++ z6q^!>2DfC#BVwh2$JzQXiUFIsAGvNg!)fK%|4phu|B@={w%Cc16YYlqF`&yaq`m#E z?iX8YYY{X$oha8qq46pWXzqRRh#&AeRi+?p*NfA*))!2tC!sm?h}1fr@N&mRv-1Gy z8UW+5>ty;1z~6SA!GOMMzO%n~qfhB{2o*(y6Co_K(|&@Fi6o0`K>c6f1AFr?@WE61 z-GCiQw|+Y4Wnh2f`3ARs8$Yo%dAjJo=cGAq75{T|sf+E@VhP{Q;-S3t4*`uyxTX<` z3ei)KEyxh)cA{x*7jtn!;w094QUZvJwlL*nVy{4j64{mv+pNJOT|)o_d6|o_RRD2- za%KkjZz-c%k|-ij@&IZYMw+OUerQoqbJVJrKviALthgVIF=;X}`V(+sB~n93D=|+r zGUzX$8r@H0FV`A31*HVq66*pg++$xe3UQxWz}^Z7t>lKckXB8zr!rYHP0K{>+mpEB z#eNOu^Q(6`epm`062>fh?stYYS__Qo&9v!|`2j>Ol0PfC_s;6fr|U- zmd1n`!H6Nb=sMs1{ROo2YUR2R-SBNX1VXztS=XCtTb&rehWoEQ*V=GYbl@Z~? zm657Smi_$*`8=fs&j`=4HOl1mZPCr*q2(p+wSN7O17KP5`n#_6ZR+Kkt*@FAl3ey{ z?{{vza}jLEPXW#_$!Y?Xc&Wtrg>fVSPQUSU?9j$TfQUS?iYjg`hk#Ua+}GU1j?(?d zhBQAA9+>o)m&CKVjgoKXY6+5@ z4ANjLI;Gt-^CQx(`(fT8_7HNLxFNSr1vFpy@clsAuT56ifP|ovv+Spzq4zsA1Z*S$ zq`@f{Lc0S47%}#_^EcHF4}I&&c&Y#l=fmxb84XR(=GwMp75#&zwyV+R z;f;gP;fW_9aYr<_3`W6|d{bFs7gc^69Km)ANUebB6VKL%7Q5FA9f0) zfm%eE{+1W~H}9!}InOrU=QStCO7+ki7LA{zriv^-I5M42?G`NfZ;>0k)ZlqA9brKp zTige#2yxzzzgJn^cKA{D>vTG?9jo&An`mxyE9J#cSUI>WmC)sM>0LcuxgyI9^0xe$ zLXXx9LNKIez}^eO_Tf~vYTp8Xhu{OB;qsesLOskF6UL%y# zGO)zQ4^$<%sq`KSz*9C5 zCgswRFk7_bUN5w?$7#OcS7?rDm&;9s>JlBau1#nY+K6;Cz7b@MQiCQOpiGh(m=ez3 zdVys_O_DKCYdsQL>p8-xjOdaFzR&cfr)U$=NaCN7ElYI{@fSTbXtH2Wk_{7|*&#I_ zuv1SYfGe$2XH8;SS<8?_Y2t0P!t?f}R@jpU66^|eU+SZ!*O{*jlbBx)9=dI1u$9BXz6IR zoX_rhXNQzJjln$`xEc#h$Oy0p;njVtDxqMCtt8Jq%$Cp5$PS~GiK`)Ji<=rp&76RR zEmZb%YfeL>FHGhR9;ejEmXYR3PZm!%6Q`(-Dfg|rt;~zu=zM5S`y1DbGrI{$s^6Kmz9`p%5i%F)Zz3y4@_Ep&`%gnD#4y)%mmg)s?|c{z>s&Ce|>Oi z7P@i0K@pf-k;9+!@E@D*4NbkbH=KbZx(<0STyaj~fCb?%9$aBA1aZ?Nd)l)2J2cWgLR=%lY)QELnXYPG@Cf6hMm@Ah00Me$O=E|U zX`lMOoo~())oBnaj8-8LHXig)jl4>_9~3mSKuQ#8qbSm|!VD^6tsX`=*n>snTSmqI z`Y$b?U_Q8Hp|<4>Gk^~Rgb@q=T*~6PD|@Z0ZceU(Av2bL*zZl}>TgY+`vbi6 zp3J4nw~^hNlo{OI#A;8g#mOq3<%v@TzzZKBpv3RHWF7E|mQAbgv*^`I+&K;XPbG3^ zjB&sS-!v3uNoL47?4`4r=%SZipPZFk^Dc z@01W~m^fl`amuebpHyWQ!Q<5Q)zaCeV`b(<`o;Q+)BB5g{nVzC#;DoyG0Ec57ObSX zD&`y{H!&3(gl4D|FXFPO5?g};r~JiPN~Cj@*5U!u;?61F#>wLRC_G~w8CJ^*dE)Lp zg439BJrv)`*#xthEEtDn!?hdO@$m}B%%}Q>1%B1}2FOhU$vKpW3Ji$EaGkq#s=&}X z=_*;Fiu1?{fyA)ahrC{=&Q_1%)&^(3jQ@om;LhY=q*^m4h|W>MO&l3|2`7%|GQ3Sx zv}6E28G-ue33;z9d>96@rjM*PPP-Er8!`k7@{lkxxClH1%XBGmOxA%q3mGea?i8~o z4jSc8O7RE_3Kf+DecH-WaNHs3rW)#bf}w9x7{6cUzWy~4=3=0|V67MHZ&BuzM3r=AQbYjHl=BO{t zUE+freF*sOUJmk2-#XiqgKex_BW`!6c|c#0pY7)FgADS4ZB)IC94e)U{`6rHU05L0 zl50)ApzTk!hrfoO0-vc|3(GtH0q*$6G_rF3c_~%kdc@J=wm&hjtp_UL-T;^XT3=B; z-P&h|WYwxI3l#7&()AvGni{F;aovMJCA9Eb-OCJAce-1B^#*#20Xg%1J`!~;x5D|8 z{&nT-VK-{-e1))@*xS={7xuGiQLArN>=;wWCjF+5D|Rezf3>~JTY`%3C|YP z9HI<8wtM+b$g(Bpsk{mzGq6EoIhB`yyYfkPnX-*hsXJ$|>6&S-eAlk>`~Iy*mo?uB zM3H5OfY)O%W&yHop&86I$25tqYkMF{3dRZhMtb1l{&@fx*z8YifdkGMPV5|WT#OZ1 zmcMT&hje9#1kot4jJ&Rc1AXS3KXQLl5$0dXA$)8q^i?HgUt`U_G0L$`ziu)BUUV!h z#Q56F4D{%U=rs{JC!y>P@(t)@j)t10`iXp=(?w0%OH|qdAi^kEix6m^I~NbEgX=f6 z^q_kkkY73*t?L$T3dT!y(q+D-42{XM4vyu~J6F~eUBe7R8ELg<^`TNRua(lW!O&E& zrhIdW`)018A{$LxIN`mQPiS^7i(jZ19kPGI=ssPiG~s||vm#^m=+T3;Rp+1Ynq^}+ z`5%xP@pR*38cQKL{pp|gt=k>1;Jv6tR)_geEYkCvL^gmOy>3=vODz&XxSf~w`1{x@(! zpa6lBSfGb4vD|D43Tf?j3(d^Gq|;?)RdkRw2V9 zp)~VS3(ejdJ4$?jRhQ3Ei$>PUqqcP9`Ts%IOW}0>q3ZenxC*YYAoaFgGsa(OH2jBCI^Ty0`ZIXZO@8X>DJ<+L~slAH7pyF&w@yBHWnOA|#?DKYh?(loafxnp-gVmnpVXC%8TLaMECe8kDEnVckk%WXalH!)cF%*$W8RnW(Xu1cw=1?QL6O^IA240%|7NN5guu0Ws*?&M!$7 zvd?PnG#cV-e1&T~tkT;{QHv}0gSqAtG>(8ef@$kBreFDFHQI~S(p(xzwM3vMjfltQ zP!uBH$heBgrHugOvLO)P$)$5L%koGY1sn|b8t%ID+wA1E0W`NrQgT~t(pI|{xT zvia_J`_o+CmDFmh-}U8-A27ibe+{F7Jqp<7lG;HxWJSi#S$JN9#vKOFDyIiE6*%6# z*?sub>+28WY-f9{QIsIcXW23HLAb45*Z{!X)+9{8&?)=32n;+*uvf)1H=)7f7`uQv zov~}~2cCgPWhAzN$5-f9(xOR+uztx*1E5)$xp`gcC^}`5t#RBaCopo{7|Ymo+{m}X z*j2uNc$(r%6z^eD+F%gRxO(h}2bwgu%}FX;(pT%qI=NkuVOO_sYWH5P6Zo&z5C)^XRd#Cr}k(=AF8+Y{C><)E$_yCjqCP>4RB)V{p92A*abtV zRUp~3W*e4o#Kez)`bFHYMJ_-R9s&WGU3j+t5)A?&>_B)n3cjD%8feo&;{v0zeNTp{ISHxX0|LP1w-HvBVR|`Sv)ZJ zv@wm<;5mKp!#hV+x~NUxicB`JcVS2d&`5&>pr7*z1gg`Vv9_!O+GSk!0y&qKG?kxr z4*w-l!Gf8SvUW*1eZGlB4@cJ*^gC{2=|6-1e-i zB=^A$m5JGr4u734dG5O6; zicHLNwTg3CVfD6l0eRLi>>v*h4I0n_*Bq0#HLz#aJ~$IhHCrrR)2n< zV?mFx;F0&r{A)p|WS3ZrEqLqy%YqOY*{2(X3uKmV=T~l8JwXWZjbgvyu0qrS(mITYKd(leplJTKAvGTr+pw zk&e(SqOhj!w(%B|RNd^m66VSenmx7CW#ctQdE8IkEL5+2ak$n?Z)Ie|**$%46sWbWDPZa*RnlGY-+7lk+u1rQdnDZM&M*<$;nzR=Dh;s9Ue6eX zF!zY;lV^bBAiai<)IS#8 zl~2+?lDrDa+o7?VA8v;LwLA9odFXp#iIy-v=6^HGFx3BFN&ZVFgZW%NlX=xWXQfQycX-q}`f zUvrE9l;P~;f9e!)X3C?j@A)>C`09VgQ{8CC4PpQuzS4Abbnb7pr3UB#ljhU+UkX*@ePV95oiXLZg;PY5h! znY(C#^SjbFxhT-L()wlO^Hh)qCFmz|OWJ1o1N3AImyjve=w^C08_sOA6x7^w?rL|I?5FyXQbdzp?Gn0g$xKn*?9*FQ1xQmlyWC%;Gon&K z$ra!*I-_bnOe;pOcznqS`b}8(r}^xv)sKz1X_|@7{Cv=UZV$EQTbYao!G%ME+vPSJ zSM5t01xT++5!C!tk27YWSqa)|uZ!dJVR$?E6Qzy7wq*mk-LyoapYt$IA>Nauv0iym zSI;b#AR8j0G^Ve+Z6z>Gz2BCCMFXlc)n!@hL=Gat`v*WLb%ysj2UB+igMW3s{R!NH zIq2KZ`U$?SaW`NRxo@C%0=&19($ME|SX@OH?W($>N>JKkXBX_~3ePW8ONe>U7cK8e zgHA;YkPPRY4ZX;0LNl0@QUmaI>1!;O+WwV6MZ&Np>|eg9Vkdyvvf-67Pk?@*iSvmP zfsMu7GP_OR4ap;5jjRelmFL2sjG#?-MQTZ=oE?{?I^NdW1U=ffONijikr0+HY5IJcz5dWHaGOq{ zzfeiOZz8=hXWI<4&3XK-W#Fv!c>Vmdoe+_WZ!W2=xYIK0S&n2V2aFWF7(jow6je{? zxM@^c_2x^6r5hhBS0GZwLscH~dGS%6wtP@64B#dnePNaQ%A(e~)pekq<0beQ$bD(W z|H&rutV4Rkrt+tc@G?Y^Jv7m0L5lNxp<~5Gg&m_zBEJ`qL#}xgJ2S$Mtc@9S3LlFv zs7-+K&ILJw^n#kA?cfoMZ?HBEa24SjUn6{r#P$od-i@bZ2eL^@c4Y`Xl%!{LlJ9e2 z5k66KVP9lE*<-z#n_v~Jzl~xmSkH!i`KOWO9m5T~umy5Wv>fOl;DsolxC++7%h9%? z@MFAx3r@};qH01Z*(+>C_ae(eKiH0i(1He&Fd3nL#yeSCzme+9w9$~M0(h>HpQ?dg zW7WkZ&(bW;oWtjzjsC)9aZM8w;yFDm)EFn76b0jcIWNU}@) z6v!*bBH3=Ts1t)iP+o*pL?z7EZ6kFN^b-}pT+4K>HN?^Ezwot;Z>@hpYl`kUw2qz0g3v)pUI6_M2m$mx^q#TN@d*n>dEhA$VFq z)Z329vP((oW098nA-uJ_4o}+qpAkrsp zZUBnFZ}x9vTpC&04@IvNQPOh${8>zvhok;4g$7Xlbg^e zB{EZ|HEY<-;K>NIxb>+onei?`0~RHOz~2H3%PKyIvewdwHx01j1BUPYF7US&G6dN? zk$}a+{uXjZSpjW*$~tSr&Jo+@4^Z3Ia9!)amZ&d-dwvHjUW4mwu~?+uIx3NNwd>|pZcF^&ttx1EMjkUveQ~A$4rbq7-#-K zn4dt>mETLoYTov=5S@Z@$-z$Ny$u?6aMkBCTQzyc!0 zWF-}8Ns1hB6&C1%gel$YnIa^aW-Dz50pbr*6mI%O6De4iGm%R&2c9?eYsMuH=Z6o0UC z_13uW4u4-%Yt$Ls2GhL2f$#%dW|YW7$q7E1y3l^$q9y0I0N~QAA_6YGQ!Iw=xz+?A zp0pyy7}J4i^Pzy@0nUKX133HQ^9(*Hb5J9?!F$Rgxxw39%O_?_whkmu1aa)G)gZv{ zfv~PGgIcSD2~z}($V9^9-g2meVRyfXQv~gleS|`8$T6PYlb4;LbeHG6;PM>yk4uy2@Dj;c<;&i&jj}*a zh^;PLOk`(A33yPXPA|wMfO|49icLaITmX&=gQUcYhBA4h!%-i-v@&G{+C)m~T7#@48pQkEC6 z!Og4M(zPPC-!4SdEWvDjiA$flgi;wIzHGfp;B_q%n-_PhyzKd7aSKScBtnP#QW@bj0H?1Goy4} zi7gdI!zAMi(k`ryEg*4rj!5xGMxq%NDF=mc;V5z$a-MWX{I01KnqgzKQqQ#Y;wG;p z#d4*$<;rtq^&a>b20l|$JhZc_W+%;?zN_RsUCYwH(n3?z7En`B=4B`cC?OB{4LK8FbMK%wYPM z+5+=xU#ckBT5h4a1Ch3{cEYz8Rh|IO?b^!Pj2@TQv^~OioqIY(9!ETA2AsH=xW_?( zGDwNeTxM<&D+Ugy5bO1g5H$4K9dd4wIY{h0;!~0@RMfg&CEH97$iN1DKof#pIR<(4 z_(@J+==cea6yc{S;Z%)ipAm3Q_Zi*436QN7Z9C!(6m2ub-Jh^SZ!AFv#|QMGfZ)6b zkb${9Yg2*q0>IZoJHxP(1YT_sL>{4$*9Nup))*bmDuiscN-k!Pn__+4jDflcQAbsI>TIVC|}E_=<4p-6WL>1?7Pbn>v3a!OZJ~9G>~5uT>Aq-KOy<_u8oj$ zAU-KNK|()81%`I|#Niz8r-0kw{ZugNYD^I65D=CGT!EbuaP2SosxuV|w9c}{fYw=d zXaUbZ4fDLnGO@iD83d=#p*V>7{Zusc?CxG3{BbjaI?^essqn)w(0yJa6IMsMpW-4&YvJPqW$^GTZ299n077k!D=_OeEQ{Qz~n>bzSv=4XT$*%C~ zQ^V+G{H5Dfh^*K(ze1Cn#=y_t_69spxAzT);@M+I+}bWKK(Zp0=oRNBMTbvN>+BV( zA$U&01_UmSap=a54KI!n$gxXeeWC`Ai;*4O*h6gZ4LN4#teH^-ZCxRD3p$(&AF~;hMwz26E2OpvnFE8akCproAf`d#dJ>ZWX5HEt*pW`gCTky zgK-O3T=H~@o2yhZrY|Hnu*$pkdk(FT6hi13M?rs)WQsYqw$>G!He@q)nOgFP^y3=8XJb z7|H~xpeL6T^Y0j1TrOyr)2YD$s4d#R@DQZUO*z94NufY8L`ExaYb8rl+0gJY`iN(jrW2>>hiQRX0>?Z-uZqMnK`#l6x;UZ!N7OZh{(l$};T{?6 zbYQYM643VZtMI^6OHndJgr-w1)okmBdj0Wvwmw7rSvu~)%^pMQ#aMfsFVnejufJR@ z0`v-V5)3W`H+GN;+1~1DZWn-hGD}!h3sihXlMxT3lJUpY}R)SMN8C9PXWN;yg6kBDOX9sOfzu=5A9H)g;@+ z4Qc?MaN^j9|0Sq;mCs`1fv2AS-HF(=$QM16628TQa$EmW4&x5PVw`iogd1XmN?Z=J zn>{2Sp7Fn@7~aN&$T>3f1*!`)>8WaQDeB^sB>OILDY8e{G{X&5IqF`nioP8Hv`KP` zD8Ce?QgW4jfnO!lEYUCI+4vP%aR#($uwuXiNJ>n5p}MG46`zM1HB>H;JbJfNX3?o* zRV$-wq>Wrq8FBw+NubRziT%rxp#4uvqUWzAp;Y@)L;-U7bwfDkgehT7=&wIa_a5$O z2*x*SgbI`~q%!fn3v^7{&Lc;Xfhen*KUW?rnvYiKs_P$^j83^Y?};iDsJyuY*VyZp zW8vc!;*4y|$l7nsP!qTnC7JT}DH;+AxP|W%QZuEq+Ss^F`J70bA@9WufAEN5HJ6=@ zyoyf!;n&N;hjgvfupG^jV;I5s4Q3WS4}yR#7p3-BFv&Rch*Z{<|(f{yv5jEZ+ z*m4a^H_ci~quXv??e=zr^?XnVel$}}A&kgMO~o4|Y{L58{ z%fWufpm&Ili2yT$`&~vv3Vo9;o6I$=ZLnPz&`sKCf2kPlYn5_5 z13Dz_>7<(idgR7~S?F#|m1g_Z%vg3$YrDCO{vBj1remxc9l52KScR%_d+m=WB^SAh5E#=?yD( z>@gE}VRL$yJ+Ss9>wROpLU6&db&`+CY+1|fkYc~}^^$e`eZeEMxr1+g9`7#qCs$*y zylN+HJ(1m8+s~s6Bt$woyoNsgsYL2qrwhBs)tZ|u&!}H1ff>595P1ko%Um2F9))(U zOf@Nih+gaSn%-Brr;8l#uST`F_u_K)dB048+@os@TQ)7Bgu|{AiYjZjP+ZK4uzvce z=s|8crxK4jK;q0`?H%o{Plas_hqC-QwavmDp~&`Ogp1^9@~a5-!QAy;yD~9M8+#^qQ{|6 z$Wx8cz)Hw~rL- zB(~&07nXf$iqjOG@R@+6$6lRsJ*3q$AvFpVI(ieP-d*vDDK(Go3KWP_c+7ciK9z?I z=j~w{HcNxyg+4wVDjGi#F_s!ksvj9v{49hiW4B)`K^a(Zr%BUgfRNrXY7S^QO?Iuo z_uN*ZRh8-7<+O>iOP(mbYYm=F8F(HEh*3w~K%PRFH7;FkRX_O8!Z6-+U}0Fk>|$ly z@gasgZ3#w*c>csOSR8LU&~@TX2i6+jFAw~!HU5E913bOe06|yne3kLxRC{a9tLcB8 z#Be3RvdmRQ_Y}u)|NjVk%cwep-@OFf;e08`=(UdYVpL)6sRf_z#Y)zv%M6JGK^FvU?>}{y|B8!8IN%r_P`S z%6{zrt2CaK1eC@YMD>8uxY}N@*xJBRF$D>n<6UXIga>Nmh{PcAA1!|I(D7io4g)*X z>{-g(+=En7VkRiU48v5VPSJ77c;#hML7ayTDXSdJ+{ytXl>Z~~iUuTJ!)eo8e`2|I z_I_>V>AU5r#wL(YMaFT3m3w$+(i3@F8Awr`Z;vUozuLSDy3T~B;CcnYcw%c2>&0Sx z(({rjK07z?#EiB2PU&&sDcg{Ub4T~q&cJ-t4^#50Pnh?|N~-&As}lJI!L0gTDx8&{ z(Hm(^!FCNaQwRZf1dF{D2_5L8u#rlfx z{w1~PHt>$|rvK#0;|xI1~Rsk)e)`Cb8iqlAV@YSb}wxRh}?#=SC%UT=oPd)@79mlNo|YF#_Z) z_D%_gns<&Vee^F3*IZYp?tQ6|k+FbDm_gv+GtdAkvYYUzTvQ%DYNVSYAIy;Wl{~Io zWEdXl(jm=MgX{Q3Yh+SDpl&ew!hnQ87;OF_U&R60K@waG>z*etQiB-C_4_X>_EWHF8cK)N z>XsLV73&~Xg2O_W;Lr&kMl9uZKDqz!i7fNL%64SaD_FUs>Lo$)@WM{)CmPy*o6!u= zSU6tDHa2;I%vrOlV*x!*pl7(B@O?v)?Zl0AqN_s?oh3tw$Ty~e<&_C%cA<=b5X8H* zJyvG>-8|Fx40JBGZB1uy^h)+L=Hu(PxBzA$r!zY0-r9Vu-Rsxp zm0tb3C}(S5m{;j~IQ33;>$a9y>3U4{^>)oiyagOwKwqI;y2j(DnqEMnp`7Mp3Jayo z9WULt0*(J?q2;i6jjKu6&>vfyxb13C4S*+NQo_r41J01nxDwj9<#;7+gDO3mupeVK z|7>#-`rMx8`o647yupN&xyxKLn^c%rD#bw92v%GncT9#n?0X3N6796TV&-yjSk zPiFhv4Ey@Kr4Nv%?>w&CzCP-|E=}$kpi02aFAdB@*`{Q00VOJIrNr8O-p(_bbJC#V zRcEysHu@&G>eYJbBLU}r&fa{(fXO$fc&Qo~f2DZ3aVnA&HSXl3__WB7@B{go2e*I} zZon>v9k;72E4(+MZN}|CYMP+KF;Q4ga#48TX|>?0Bu&W`ogx~I30P$G9@fp5ABy@e zqR6>A*ZU>5T^+JiREIk>Bx_0=2Ha`KZ8q}bo-R78+T8=qY?2^D4z>3piaS=)Lk=2Y zrhak^s8pu7@;}&RDDV;Um6a{$=6&M$dWW;+C3YX6Rmaa1O$A@r>kb@yan@Z;HV-2c zp8?5=I*v|zW64<+8*uPhUV6Yk15ksXa=KR{6lD{reHAHn6Fn0Df3dEg+yK@Uze8Iy z_Y;?#r(1jT9(OgN=h9WayQzj*(JG0WB=#SDKYMTZolh^%Ee z@#0U6#>cy{2znIBlC`(II#(Ir7BzP^{BnM`k>rQ(Sf{uPNuEO(=@D}kA1DvldTQn% zIgU9Z9)Sf=U{1z-u(E({J%?ER!Ortv>SzDk)uF#V4kRrn;9=e$`_shF9u+SR&)S!l zbM*5&?kSw8XsjG7WDwBhfWx{4{=ZH|$6YC2j9oi1ZEp){n<|FZhs`rai*ZgGPsVbU zhp2u{@@#evLiLNu62NuWlC+Sqa_zeENBq$sO2-(l>Rr}XK>ZJVavy~3?4{Fn6k!M# zD+0#C{O9RDlRrw5bV3lgX%8}78s1fXWb6n4u2O*Uj;nk*25^;9a=`)&Vau>ISb7<5 zF_W}RqIy{p`f=HGxr5Y#0W)&>?F1d+Yr&sHk;xjog+(efl*L%&rGWZxWFLz>X_8qv z-1swl>E%J@y~8Snu#7a)<3Dz2&lZWSNtByWb8fzjLBtfm4gK0#r?9i9G0JLBAs-R6)E`8FwZMT3uyB4B{_2aEzx;y9O ztm9WFuvPU9_|1-wu3PILfDQ*LVi);-1XVrrs`bhSqRAOHe&}T7hpg$o#GF_)_QMbI zRyQTiy zU5PSar4vv3!cvKZ(W0Cf9Nd#6{H+53>yqBVxMt)tG}LsX$krQXfR0)19Q5un z`pgp#cQF#=ngmAyLNDlApVyjwCLt*)8Ck&i6g4mcxTVR&p{STq7P_YLR$S+}uQU^% z5rPSvY3{9q@NuQpp^c|8eK5q8;^B*^W%(uhL*JJO5 zcJlXR(@yKryL-)qiHBU3S7Pv{_rpg^FUj50lkx^gme`>X0~>2I(x?~*zqlbl3M;t_ zaKu#Z^1un{jv!Wp4ti`1pnBv}fI^OZDo~KgZ@a)s4G0mt!^+>|mj3>C=$C}n46MC#)|9EbWkUaJ-IF{i&-zzpOV!DAj|e*L2Fw>f@>?l&7P+vf+9&g3~M0^ z!$}F<2KC3H&8ufb7UY1CL*}ksU?)E}Mn_HNEnS5DWc$2g%BK@LRuTA6bTr-tkk5zA z1y}-dx=w4@${#R-?zPurP6wX^Sc1^o1sTxq0C@k0sF6Q;e*|(vU=%g zCix+T#6LtVY^Ut_lwXLJ9;+s`raGb>G;kI)6(|K%&F=vIc`t`z4%X10K-7{A;+6;^ z2*6cuiGZ?{FBH%&AckXXG_EW1M`+41j}?$33Krq)h5fOR+A?g*4|p(5Tr9#fq-D$) zI6)TtGC4wVh^6QgZg@_?v-A?lCM+!MgYAv{c`xN|0ga@aPVmu`>dF&?+$Up_;8|s_ znLvVt5rd36X-CvH%zz3__5c`OC={159?yM(lngDGu0hkLkg?sY)`){Rf z-a!LB>3M9D5Q}I=`@oV~L<5!-yBObqfgB*0S5=G#8!O4d1J=6`x0ai}IHc(q^1&+) zGHI>n*<$m4R_XO*GY}3%Pq-G|xvIY<#(V&AUwqr>C%&H(?^IggN#h7IF!R#u@NHUH zjldgJ-DLl6fr6BlQBc#Js>Z7bGWql{VY|HUhkFas=Va)M)R`3KL_h&~xG_JpNeiuD zKjhlxc;r0G>+n5F4~xBMOcXPGFE1Iwf^8CWLL^nvs5 z%Ov*G`!YHCfK&=zn5>}xWtlMp95O}8jvJ(ygtAtoPZL(70njDW4Xn)mBvp9#^5{|< zSlc&w*w$V@)dJOXd`R_(m{o`F!W%k1D1@**RN4=VwpC~DWY?nGi^ax5+o&nT$cbzk z=!^nI@Gu^ikLR;J-=r6v`M3pjAM|;+VFty|_Eg3Y8~^qP@Fx~>0R@7-d2B1-PpAPK zz=S=a0u%Q7z6S~b6E-jVK4EF^6UGWm*m&BOnwr0LuFTJhk8XcEOs>=?4y;m*RzL&j zZf%bN)W)#3GW#BP8zgJ+;f4sG@4j;JC0gj#G8G|2p%2|ydl4>SYQ49J?YZWeMwsxXa!Pu+%7P@bZHWi9( zs#IQhU=x9LL3+!Pj9t|}kLqU|!A$~%6Mqb9ow8myxD^-Gn{I|k`Jg>a@Zp?Y5>+r>6-t8*NLBo&s^nmb&oV|# zjA-DMj9zAVc&g@@<@GX8ld{gv&+D(kcUPE6s}DbXRn<5ftsISqucts(-VlvzRNEj1@Pkdr`--XdTD9DU85sj8%hKb{} zP1}c{NRe|`W=ok2??{R7s1$P-qt4pm(nI0ayL3wN(A;oi8N3M?c+uK=)OW`nRWjVj zS2o)?-^wRrY@MW*G1SZ4D)_c_c6R28U?gediJw_AK*JK|HXII zeD+Iww`L|J)!}q)S6HP3xQF_O%F3}$2L@o*T3=u%r%%gZ8)&Su9CKfNCnLQa1)Sx= ztUnyMkNNF@ot0%w0>$>hFsdSTE&v zvk7Li*D^_F1X6sWoRR_uJlQfSNwT~?xuHu~)>HU=z8}rHCr1)y1>_gjSJTPeD=@{^R zF6_bv`XzTZ!;wW;CPu&S&bYmC;>6Safjq_adcE|@<~vd%>Zo@HN#YXlcmJ++L3*TAj44jU&61#MB`CJ z`9~?0f7A8NV?t)Su z-J6)WVJS4re?&8n2J6as&qG3zV9VLFB8}R4DP}Aa#RCq%P!k|`iA1yEl0)FT-T1}_ zKS2YnBjrX9i)bExEG~3yLEVx^QHaDrUyc|h4m>e5GW?#uJ}%=r?$i?crPSQk_IT&8Q$4TQ zc&dbmkc4sA8eCXtR)35jGKoLML9Lj8+cBKAi?xy{yX=r3T3Dtp;Wh;JdN_chgeay% zVDH5+BfO*A;Fp33Q+?cn1d#3W49L0mUV!rhEw-4z2rV|`o$7}u2kIHHYoMM1yA~Qj z`u4Xe@Z$xV0zY2Zk-x*gH3dY3z?mb+4>SdSyl%MfO@VDb&=hn;fn0GY3cjjB?!6#! zC<+kV?Sk83&Gtd&w)*fwny#t@ zipzK^_s#EKaCSF5GiE>eBAoVE)8E` z?~-yu;8OCwTH3%`z_cF60&HRTBwrY3>3~g#W?rv=z<6Fy^i1sUfOSmtFAcUZgFaqbV#JB!GpVDj zwbt~an(MgdYQaEo>qii)@Y?e_1X4O~ayR|h;UR+oQjmr)*4iz>G^0eruKb{fpjx9` z)e*E{dR)B#_+ngZYx~iqwQtzP1Qw}R7Tez2bsp5&*lHnknuca7kqthFO75=e`m1M& zZ7>xgG3*s?0K_Vh2Oy=fRmP@-Mh5r@MTPP}(8+`)@>HD3uJyvGSYAST z^;Xh>dUO}{Mxgq^l56`8hy=ZyPuuA4r=QNV?19V&>e$F@DYo8Tt^cOg&0Q|g^2OPQ+Dt2gq*d)$nu}3jnom}Kf?`h;(=&C5(Iu56N}tai zaqjnoyl3j}rge7+H;~}rnflird~PU=R7zw5X?%+NH)vKBNZrg>t>SmLP%=HY#&X=$ z+JkP7)^a}-|EB^FbJA$j%u25U0HlAE5Qfr;h=fOQb1N&s_qXou4^XbhQ$q*(|AdSV z9YO*K1X%cRE-*A?9g-bLMBWk6c0-qAFZjpegS zt?%0)VTjN31>uYRQHt-M7YUK>*DQrM|KLwCM&3fm&@DN?ZA~aSs)%MXz0W7xWUhuW zED`z?b9y%{?X6X!SwNQJhkQY?Bxw_jnDT4d2%!wHsN`1`GC!%+vVlyewNnZHglSu( zDLapJ&Ws7#^NN&_W|wGqG*+{FNtLOwA#83P9eud3$!fgV%fnboL+G=}>}cfre)@79 z-Suev%%|@;PcH^~LM-O(J_E6Vc#<3ida^CL?#jSK=M}2NiNXI9y_GE zrt0D9wuM_7p_p4V;dB5(_pX~_ekM}8%^SodKuRv9$&wRs%u}Ipo!t6Zu7n){Hz?L( zEew63#^=uWK(LNDjeDV#v2hrY7O>wQJOOpn+eq@@7eI-6{xrq)FK% zG`2Q=8&Y=w07AB{WSFnaBAVDL=_A#2=3v=>Wjq?fT!lN@Fi=OWW93@7-`2`6L+FNI{bt z{c;3))O&6v5igYYBw8T2)%YO@u$BL^D;dV&!2>XGP*^8h#Wr=_%>^K}p*ypF+(VE2 z?Y?uhY_pf^H$n(?oJtWY2R3MI*s9Gikg`yB9q9iNKRnuuiynPxI=7@c7#lwvY(977 zSPAX_f?dnKX#w)Z?<)E>#p9@cxys*@Po+KcNm~!tivqARtJVLBSO5GEc=aL%n!k}47xN8Fa27+&1pN9E#7CY9 zq=fG^|4rk18niMpWhMeu>MnUJ+xDUxrb-w}tBye3I z-tMP{DU&y{vq3?B-e0-gvTOH{C1>2aLAqmTe1#GoM>n(*)S{6%5 zfHHQlrt$cEs;9%;2P7~)}zYHP6Lq2XK_erPpUBl z(Y8|VOWv7~f1tdf(Nzy{dP~}ZI6x0W1nfcU@Ae@47p7`E!5PTIlTX_^rf>=Q2D**m zzp#2wM%ILtvwmAv{-AtNF%$I=MmgRln@QRokRsrBvDqQkG11=vY@H+mpv>Tu?Oem< z%-tRNwy#HrxBat(IRwh{DVTCw{vHjA7otT0LB+_<-j<_WUJJ~zA!7WZ@7ZubL)LYz zX0Rd>zyw3^MdkUu#_+~~3dur%?mB12UUgIdtKS>vY0uOarKmZy9nlmfgX5MVW=kH? zgjJkWrr-W%!ob}frYWRmB4j|uk!FDMMc9Olx{xXD>!*ZRNOIx==k1C;^5&^SRJ#rN0#6!5tU!9CDjoQ*=pKw8ItsLkoGrKn;sxNC z7cVjLvn zlbGKTbA;8kyxCj9tuOsH5^l-iegduC=S<-9aYCw<>&qiiu}?U!+K-IBL`DR5hM&dR z>Idp9kM@q!B0$ioi$y`Kb*g^FRJ&{T<;l~FpPQ9ZRR9CvFREWDSYarX$aqSKVfIoN zO37@FvXh1QJgo%j%>*XGPY@n0!KfFOx68FqI`I_^zZlN|7=RuDKL(%>KD|>2pZ-z^ zpVvqrIKra=I5a#u|A^}!ZTE~lpzWTqN5hwRA41{~>YxnUaR7@)y&7{lHB_62ZR^v{ zxo1Ufm*Gr`3&sRuL#y?J6H1Rf+%F(%`lBNOc1y-6Sn4vGi1mu?t{prSpHWSJ4v@y6 z7L9QLWYAivzZFfKe*uJV5;9~*0V8VtswIvKlfyKgQ?#Cp%rn|ML3b z_9)xVjX1+5`Uy5=VdpXRJzjzB;PYI^lj5Y;*Rmhm;mi;8WR7ImH{Rmc`3|ETT#kSQ z9BDGnjt;8W`b*gZM$|qy7X_N}dQ+M%2wA`x;$ZksRUR;kV55St2PJQN%;9k-@F)HnzY-oik0HuQ5Sqq3>iYzb8>))trbTecuM5U?s6XbEfh0Q<&l* z74(zSlEe}5jBLBQkE;qRzdqN|ProPd6&iR%2gU)t-Dc+Fr4nC6OGkYPz3pc;4jfbH zQhcdOCJr^KWD4<{-GizPF+V`}?f%wP`UT0L-9!70J&v(V6LZ<}88Ak> z;md&t`!xQP(`#+i#K(M>S{9uQ40y+#qx}hiw;`Xm14cYK9Mj+Z!p!9u!!c&jd?WTM zxBX==dO_O()aMmtb!F3RA)kC}`O`l6E~Ketb*FKPWqf`c?00?3kGS~GL-En{H|e9a z;f_Vs;s5`WGS1kt&0@Mvz4k>p2fOyT#CKEbB*2?;o%JYLE5^i5n{|Aa{;*RX#J8%l zRc=!k-7>=E?*Jm;Cow${xSnRKMlKDX^gogI}O^B-C7@VAQRRhqq~|3wv+ zd|mWYlZC$8-I3wn#J;;RB|LOj2NfM&TTyJO3-4RSf-Pg&z1wrpd zD<2J(FlJ;h=1nm^VzG_uzaFMQWF1;@@mSHi$a88QvnB^bCzW#LHqm7E0AEe+av96h zfN(gCP*%W6XR@@A_ODv$K-o(FB0uF%BYi{u&*Mm~Jp3-xYUHO6wRtz+n*;SrWilAaoNtTu*g4;#G2AxA7KfmhoeC~S#pyAx@;6~qE2$9Y*!j#nX8^rYa!N=O)Bd7Av7`yiUY$D z93#%t6a1b*|$D3k{?ak5**D15?l6Uq~Xr`EdRT!AoTmQyj#we zQ(E(P+w8e(_U8>!*P}h8iP|~Sv&Foo_Sr|L*6*OmhPJa)$1G{_OSvxgL%a%;sgy%@ zeUYeUn|)vSWQWWce`N4wKsimG4ihv5Z(#?SnrT-xXNOXN070FvQ=y>tl0^I&;D+zb z3=BBr|6wi={ZDBJkOW*4;eN_C$Y2?E5P$QvFQY^M`w0SH2w&dLyx&w-HJT2)cXpcH z+0P~G{?H33F0Avkukze%{Kz#Q8g2mEWu zR42dhQt3z^C*Ed_N3RwBz$wf&Mn;($j~d84_$aG@Ta!JWcun#>tT|3S;{CNUu*;Se zyeAwky~e67e^&>}amV&A_G4W=VRDx6j%Eah!A%?rq7ciOBf-_o z7qk(!k}(7hF)j@izpDG+b+7lUpAkdB&j|;$pig9=1flN8Nd*I+P)xBZ%nouszC)*` zSOhfvAa|w;^C>o`jDi6$D6$liOwd@aN2>0-3LKNMS1)Gp5&L1fvCx?fK!_l2u~3;r zIzGc&o|oqo(cb(ZrMU!)aU1?ocuaj&MV9@I`Cy{PupAzd&G{qOpvx9h!xcE8x$QML zVKHu8qnZpmmfFZkWZ;)+`&sl7P<%?2tCPu%k4IeopN_b&#qganZ%aXrxN^S`C^o9} z(#;wVWt4G_(5VMjs>zfK2$M4{RGY>(s%S)0y2m#wjh$!a1eQw;GTcI zG7tCY^EbKOE*+3XaLM&r!pf`IrT0mCY6kXL!dBw?JwwZ@JP84B2QK{_n%Lh<|Ka`8 zivgFu52V=AHnZrwj8V`MR$3*UMwn#iA)^sJZbi#cs7iAdM?a+?3iouZ^LhV;XMffP zmAA>682K~{``$KdJ}5RepHoBue2nNs6%j>H=mV^t99sLdP zl*0-y0Rc_6KtNOKVd$#d?7EhbYQIoIbJe!+{#I~Z^C69>vwepL{H{u~US=eshfMYc zg3hiYe}7~ZdMxAs$j}Iw5yTNgy`MoxX>Yr60X}L5Mvg+9rEmPmIPy#hshdN>FIYeR z!6VXE)X}Vp?{&nj^}s(knk9KI>QD^iZxvm%ef7G&8t#6+-L-mq(idorf2w%ql^$~- zojJ!p+h`vXjKbNpHM3vD*oe3$OjraQc@I_AZ_3P?e8thM9 z<3Y$G@}#I?IwW`dCb{kB7fLb=S%$afB=!{u#$WxI+AdxRte_D@+Nqp;FXViVUeCo% zDm4e_571G{Wj^w4X@+{|K=Y~)Z%y*Ud-QBPAU>BHF^R=t=xNf5C5=WD9=edWLyQYuri55>j+hqD% zzx$4PQ@d);z~?Z9^$nhkkPMi-0U9B-3z)acO+YbKxtaT+h{Ufw9LR%s$^!h`PwQ}& z|J-=7o4}2y)t!I$%x&hDcgLgE-7lGc$I}oERtbL03QiPwn}(dzix3|0QH|A6F&5B& zga&*>L^tunZ{T#9)}vvb1|fjo;;Ru5_*^8+x=I5{R&4LcB+f3gruCC?1YTZi_cVU* z_S|!S;~~iv_sdlhOJiP@wVM8v6^}}ixk_8Rml$W_b!uiiNlbB7%$~ElIhnRaX`9$rLGzPb6HuGf zx-)o}SuE)D=f8gXU7(c&UL-2b_qs}F!vepR9e{o=PbRQjWlY-f&t8C!$k3mM*GZF{ zI66^Y;kW_5ms5*=2@X+a?MZSc&`G;jvcEL-;3o|Tp4099?Q@fHJqRdC zHm~)Ryz7fFI8V}E{W6SNlt`lkCDmN>xI%xGvkwRrgdmV8k#49&xlK{VR-Ft)RZn+k zUUdD&fn1;}Y4Y5bXFSBGR#9BWXX)DHb#2TnfpgFU?sa+jhzvDeYK-uZ-*b-jjeG62 zOWLlk8BsbwO06?FK?!SSGC4D`%l1c3J+~n*n~sKh)-|_%%}Kk{#^C|4LZf(W|Xt@1;^!;M(;K3!2s!5E6q~)>x?1iM4-N45& z{fBW+ZT2UVm-H;~vwNQXV^(MLlixWa2BrC!f+8q!8u}P?j$m^0J^2`w7XTjJZRD^UCMVR($T<*;U;X&tx;Crb~a)NUdAT!NL+y(J84F42c zAXv*;B`0>}|K7#)WtkH`P#9Ezs#Ec55b3i?T1FhZM@1lFbJ>pMr-OZJ(I@kH68BhZYywUWlv7<*7%z(hvDqx(u$}0#8=Iyt&s0c zX|=WR$Db1Y70L;_6LP{|w5;uWKP0lcbs0Ku{h&@iX}N2cCHhQ@9m&;NUTl7mXyzlL zF|}1#Z7H2PZ(Av)tv_*PZTns}wRnrTkX=lZ;re8rmg!+Lelg!(9g4At02j)Tt~L%1 z=3>hT>2hY8A)e1eIQHzCbiD3x{OnMw%KvB5hCYjO+G@~p-=eKet*pwoT6QY-J`~g( z)*gA3ZjqRhdsvnt;W*Jv%e;;1*DVqqs4>`vXYQnz!dFN`A9em;9&<*qdKIFEP>i#N zyCsg+s|JV}8{|dmnjNQ_j#hXbYvrvW1W0;!$6pe_jV8T3BO-polUs>Aw}B%{4lsNX zMfk|e+|dgX4EEt^a#9gwM!h}~>;odYpkLS-Z|YJyC)bOiLHTE)Z}^Ql;NZA?qX|V3 zwnfB?ECNvV)eaLqW~MoA{j2zqJTOokOf~T029h}tH#ixqd`#8Ee}GkCpqGzlz*POd zH~n1dT2)sP=Dc3sBMn%1`G}m zrCF0-hxoxlVtnlN)4LDUq+9pSk8A5yslvJ6*nvO0g`~eh@gs|-|NJ_(Gc2(}y1zIj zESgbGdRm}xB=ArbG$XeXB=9UMj}y_E%ubJOdMZhj03Xc5lDq&sd+w@xED}(?VtsSi zku@AF6bDtx*}KN03jgnj)+Z|O=YA4F+)A6Pkd4tSPPVSMw3QWf-?5^1M-DwfGNKx8 zY5B*x@Gwz^xF?4?B3xc{U1yI_4Wr}8Jc07aJZH3gaM2k)G0z5ku8(I)f-@T#e*|ZM zbB`WfoPH491aZrXlt8nS}Bf_$u$vht!dACQa4LW3I=`E%U5tR+`D$*+hIe)r|e z`tpSQ$S$QzRXOZQ3wi!z?9=s9Hsy&~NAZv~Dba zW)a5}niG$8t#^|d6!s1D)0W9k%GU(wQuIzz{@DtXI{B=3DUiF`?#iB4eyi*c^NjVa ziWj<8Z<35Zb>8H5_%=T|EyTd`xp~{L3a;O-RAS!@F#Xp~r(9$%`A|ZvAnn`Y7&c>o z<`R1OKtt#vWy?vTr`xoLNupqSRYiQZ40*OHVnpf$Z_CCE@xe2}-R8-MXIa(zVx=PaJUhkO|-U{mKmhxu6s%BR??9akkTh~8b);86HzbD(b zS(1VR542e-c;WhMb&S{hw++kb-#4rzN8@)j*NYIn6m!Lov$8?z=P^T$LiQi{guJEX z`We8(>EuoNe*%H_s{sb`5GuOVUrtp-*-mC{oR71*-YGJdTSZO11#(W6lr>x6#z-1$ zPnyLTJ(e>gvwn8Jn`!X+ycnXb*;P%~rF6hC9zwfEaAu*otRE6rq!IY5xtstAdQBj_ z(Z)@7@(@-1SM)DKEep`J3c-`}$WKABloNu1oj8qu+YpTRHlz`YpllGt(^=uyLRAEl zj*@x;9z?!&Ren0GL*IVOhH>bCYnD$IHKC)q!G{s0Ldl;$X9q{%Nq$AZ@H)|SUy&no zOu-C+Na9B*YA-O3z{qQnHqdrkns!t<>e8;(kKGpC-#nj0ER3x*HN7aiCaWx8IZ<#f z3TDJA6zuoi*|^nM*?Mpk@K=_=W(nfp5Ebwt8HTKE_fK~ri7s`HB8o2Ah%PG2Elf@e z+n8rs2BKtCr6Lg4?vTh4Mr^kLUS~`f=`d>_fB3veNJn0lO`dj+Ly0i!6GX)a$`d|C zqmW+f1NHf-Vnw)v4H=qg7~7X!shH~z22r^(3!fl5C0!~w7p6s48O~XdQylC>mREWB zQyB^QJ|7m-oT{{ufp=pNLPQE|tSafFEo5KB!XgCrJMBk3nN@Z)=jaz!PQ2y2k@|`5 z4Vh@mR21pq3``eBNn1uxl~AiW-$%1FFPD_xStT7)TYJWYrmk%#BV*_#2+R+v!xY?` zTYSt7LL+Kq4B>Sb^{<()>48l>^$~mV`ci9f-dO;qTHj+7brn$-$AuAQ ziOf28-jbpO3Lj-Q8}#)PpHu5lES$~S9Lkt!)V+U7W7^NBe&TG5M>2=j$k*oF@ucFx zlU?mt#Z$a-z0p0mG*!KnnP2+vHR7tT6)io)cWKg*CD)d$Gf~%|Cnq z25DfB{Bfmdgm4e<)B8f#MD`1pfotx=`6qH#>a@76B9%znsFo&GmQO%&evDv47wO}V z3rBi~l(4=>UPmsd6NSD6kL>Pv3Z3H5AXgq#zJfo&Hg4+WQ1Lo$9)G8JI^X%K9z2jT z79B-?Yly;JcGEmwn8GlKWvCJP6XhpmUD}nfpD1(~w2aA#a=KD*VX4)gi4j}Q zm8(?Gx8v%L#S;@R?c*a0B?y=yj zudDtgP{O;`pb$N{k%a+yDc49H@S*D`I|Op-H(l4Vs5sIc1?C}>?r&TI3q+)o3VZ_^ zd1>GHl9Fop=RPtmPtmH-4lyd|Tk$j}rV-Lk6{T~$p3GJ`u<7bjdpPoN5k<7vtE9Q> zY^=T=j1Rc&NKaBVBlwV-s^~{y)fa(Jf$$P+2F4tHaDuPGH0V}DoeNe4oy4R^%2xUU z=IW1;vk#o#$f8mu!)dY>6*CoES&Sp7r23;S9awEi!~2kjQ!k0Zytgsq-{4cDN5IS4j^vBB+5 z7l+NsHC8Cw6$>Pk9s8NJjNYo5y=n1#>2&hxFFT2~g)Xd6+8H+%;p$f`e%;QR+`A4! zt|!1aWry>#y2w`B=%BtpANQ<|8ui%?yc+d@Dd!l>FZVmTf2sG+&^)#qhE|yGt`_3G-`R%0a!Q8x;??SDdbk4WEUpYE zYAwtIh*t{T*u(v`1v-J*!%J@ z`A8e^6=VxeA1xL$_;eKHY+cFCzrio6-Uc@p;&B}GFTw_0nkgO20v&VeC<+l zVJnBziZ_)WevAL{n=9DXa?dxiH_V#OMMTWjlGWwO+o{z`fC}A$G z+VpsDI9r5RPNpG z*)*jWfw^rbGJI3Vg%d*--*o&ba|zVP;N!L5am@L+Z|xkVr-W(xwt2=;L?q%UGtRiLcmNc6@-|OB&6{u8=>srP6yTHwuZ2}ox~J7R?Ib|w<%8B4+#=sGO}RdI19Ry zBVu(F$VaW6HvS%4vd1qJ4T71e5cm3Oq2W->}Yz%h%7R*wEtE{Lp=sytmFVAHUNm5a1rj~q1Yylky-QFU3lXX1p@5$lXK z2-|+#-Da9i{?yA>gO>CaI#l>H5tJVTwZZbT;UM{tOsf#FFx)6q{Lyy#VWVy! zL^({A7fp8Di-wBANrIaI1x1K@!f{4aV@KIxtbD^k1BtYjt7E^ub6=DpWybd*xmzc~ z6Q^swb(l8`f-%ZB)V*$ZFf16;eBZPuE|3&j0wFr{0-sw*`Y0MV1(t$z+fFf3e9wSx z%(2(x6Uu%(Gk;?AAm7QzRW8$1NWa})8&^+L8F!|qtlW=Sn?7?nRe>r(qT5TqxkkDV z?eJv!^ZDRuzeUHmZ@RJ(UJNA}J2$S#dC@6Z@9k zAg(cT>~b?t_R+qW((rDu%)?bg*P{@RzH~7DXI;?(L}(C*X4eBxrkn z<-{qW%(B(4K_VAD%iWo4+t=6S+a@{p1$tVD&zOmwn`PCaCw)!1c2DB{iTJ{w@7143 zRnd0_nBJLCu&HG)Iy>PdLxb@@V7Q{#5lZeRU9R`0T3-w68VhXR)_g>k2%JTH{n`YS ze!6Uw#++Z@DUHAt3Qvd@;?iT>lZ z0rwM8ToU0oYK`sPXcsy)hBteVWbLI+U+-+_dK zCgtbuqF~fjMFoaeEH{rr0oIWK0V+X?n__B7MyU{~XZwN^bJViB28JU~H)=M>`&4gK zPQYEONsg(TTid4mxq^@X!^(sHl=< z2aqH~6ah(+qcFfoPLd=C2NXn*93{g5k~5Ms2m%gSlqgAtpoB$oh9xUmas~mBe}~;w z^t`wKpWk!tgnPSs>Qr}ERaaG4<6?nXnJcWvy#Ew6Tqlg&=(9}PcnUX<>#rLl?>gx)Mu`KU}ak>VR^Ke!AsXU;AyWP!@ zZci{r>;AHAOxBWhJxs}Bxmw|$uODsv66@E@dyzFtLV!z3)MNnfj#2#`=-Dz#qaE4wj#@*zq83`$$zN@-1bkne|Fq8_{ zT!Ci|aF@)*0G|Lc15!gtx+z!2gJW?#T(M&;d!f+i&6Ad1ySu9CtiCI5tF)pz)6>U7 zS_^N2BGF=737>Gir@Q&u`SYLRZ#>U}ZyZWBtr9#q0t_y~oOEytTQ?k-n`}E^aLL^S z3@$J?z~E9S`p7+S81Pw1trE!bTt~~@1pDLv{X|FHifqr&{JO$4kT@rIlMaquD5@}R zl-p1wMF$TZ#s6@IQ7C#w2Y?eJq*i;$W@t!CcnYo1Qb}jnp~@2zEPm#+mSYY`jhnsi z3SVF~`ZT3&jaFog z3KQ z-U7%s-@3*oe{}yxUGr1A*}NcW7_%;c8mDiPrFf+r))(?HpSWo&-LJAk*4_!+)+VWy zcfZsds!L5iK0%Ycgp@Y23s-8}L2+wY2BxT$d9YSLDF0XmKPw*iMpb9Yb0 z^w1kexb2q29YkXZwfA&v!gs2S&zu(nEzS>@C9gyinkRaej)aP?ej9%IeFJY7`&5Va zmI#+pKQp%f**HYTNt%PjyIdjH_;7@h;rftF;q#~l#)OsnpwT#e$!uZzA(gUqco$@lB<@-gf<+HNWN0VG*+ISe8pdMIGzWC$667{ zI#R`CGaJ<^yh_i~A;`2y0mS)Lib)6y3@)y)gN2w8f3MM4(#>PzBs*~WE&9yI?2$w@ zCtJq4nqWTq+y~~#l~t;s9EMnCLV82Lb!d3dVTI@&e z%Xqu3?b>f?ym?sox#&1U2@}VIb;b8a->j~soL{MDZ==Gd0|!XZFT=)<<^YBiO%n=G9^QkQR1=2-rWpaY{i&f83`k2;s>hnSl zB%w?lyE8N!+x_`$gYh>BhP|8+SQ8OIu2w_?Xamy{!j0XC>*B2pC6qH{gGw0FoQbT) zO~@(|Q#g6ppNVEvSU#U;4W(VuM(IU44-<|*%(AsBD2(;EYw5T+zAxW_ZsR%M3}$?P zz5tsc77t|%Avn{WKSwswM$%@8sbyNlIG1>BgK1gC)ZEjzg=QU_=Yu|uH+&Qpc9i&T zerquBJ-ZKS%s1$i>Q?QGYcCaqL)8>v+(crxiM3Ro}N z(DU*MJ2I!lMpj@2)~Sv^E}l9otgnHmHTPty^4Q_#K|{VTcroSh8Vv8g#tz8BU(OkK zU$pUBEvXk|IYqcGO;?Chb`^dX{Ypy}vog6OJ~J@7bPy9|Nsn_=J)$UhyS!;sS_^(} zysm;vHueyIiU_u+sKC`4X2ch=6oWZa%@KdCnE=CKiF}N%Q|DF><6c9B;E--a`CodW z{SUp+%|O`dIW}Tb9Wcl~C%G`l@*X%-yJi@>kK|b_o_WK~&i8!%nF}bE_X^X4#t-pk zOdcxI1~2AQ%pw;@r+F7=Jaz2D%R>&#@wPgsZ3KJ1a&(<84ssVdzLxkZ*Km4u3Gm7r zH3HTWm8&gYfbiq}B+{w00}6Rzj4*mP-u%dSsFXx|ftWD{OqOnp2*cD~DoFYRQ^z6P zKLpbPZ|jW{_sfpC?s)S^&I;&iCsk|x%H12uFSzRADV}-()AZSzshPYmfam#Wl%EmQ?Die26T4)5oDK1)lD` z0R#ovzPyqb+w)|GdeV6lm`-elcukle(+}O%X+Kc}GfWvPK2unMQb&P$O2$&GEedj` zd%pY}Y0}=!ciZk<+Z*|P^ki&Blf`)n+IWkUWj6()k96IbkUz1MP%`_-eM9n`5jtyZ z3LEp_5`>Jp6B&vvtKalJIsJTpHfLMDelK*y7}n-ICDA-ZeS`3`{rUwNxsm+QcUbBZ-)@m(Kx&*AZ4O%XE=b*rzf!y zKIPY~H0h0=1G74fY@g}NaCk*I_ z0OpPKTk`P%5(ShXT1J$d#$d1Dd-P520vM?<1VGRHdS8sL8*iyWlSA z3t|C#2N(hGbV12}d!h-FNtOrejx}e{gt4@2~Bo>h)7zjkA(XVO|k=&8P;gHrK4)C@BU8l!+&);jK6{JDH!yyfbqwDm5 zINBEk#1Z>#CdVJ*$VsBR6f_O_MI2#Xh@+)ZX9~^__fP%ZZ_JrJUw2V)^;`FhC58eU z%UipFa0G)`*1L`I8$wLHfR6Z=HFcO{qWbZDgZJN9)8}43W6kJ@(pO$sAZGWp^}78c zmK&S$)JaB5Ap13QpiVrD%uf4I&=5!u(&#KZiec>kQo8(5wY@fRDh%@~#VPH4Tf2kb zvXFgRsz1^pf6u(1n9fU0bgq$s*b--!(8C=g`~?D>gq+7u%sL!l$pMc9n!QTr4tv)c z9ALyvMi}m-bhQn}%l$mM6LyzDYepz#x3$$!(hRP~K|}F-LaZl6F!-aL zr*3aTfW=v~on!w|8t26u9r}kU3iXV!qFAgS6J>>edHK?c0C;Z9_cf=4ugpJ9+l8fQsk{{ zS95_qPMB|l-82@2qCWyzcjqXFRtGC~zc(V^mY(k}Lwrgl&Zbo)PeU?7BH@OGg-m9x-7E8jCZ@W^dD@v8DrK=(%YA=*2?A^ zcIx`}D5xylI-8qMzfM$r*S(`(dj)wMk*KN~y+U~7ySj!lHV*bTJ1X215Xh#L1f-#i zafK9wgM$OA@~+eaT`JVTcQ<<{Q;30+nT<2V)ZPScYi8%d?QUzsjk;46-(lUtLv+Vi zuDc;QgFBIgIR)0v4|qG3=7V+d(iFqa8t>8*B7kC>{~BQk)|K`HEoIlD$B1w%TU9Ai!QySCnsmVG)X^a5SdjX8d%yy zP3_T0pDd{tTXT(!99L{#$oGRFT*+5*b&ZHZlfULXGGkXV(#O|f>-lcU68Be3bvdzU zVlL}bLS-Qnb)TmPHQEL!g7KZcI$j3I!GHn!7lIM}LojoDdlxe&UKFKhx}^x3>4<`7 z8w%*wHxPIxIQ{zS2yD8tYuWF?akzeJ89HHzKJfD`-eQR$rNI+v%97NAY$nnxadlhZGfML3y zLkPon_zXZ28|PWgV3-?#QXp{s_g?;iw56Gm=?|oN3DD^Qq-QjVJly)5DbQ_}arZ$~ zAA(Jd+QGyzkaDkM1BnWsE_#VLA(N`zw`}aj2NX`z#a`k!m+SPVJHQ#}VaLTqp-F{D z`DkMWIs%W&{t%g@Dzx!dn=qmazWar7FSip-JI_v|S}*T~E{4VD801Y9&XAJ<8rT07 zXA+>s#mKaS+ZvlWS=m{jNIjjbY}d>~6mldkXIj`bSKp2iS0-bu36~=L%tRo&i!n}6 zw*JtgGBY;>#BhDAh$Hy^Lh5nbIV2`Kwf8Vl=+Q0HmWNpNad_0-N{{(%lKlG?zj2{e zd^1D7C`%^O`BIjH%fea*E=rnEp-Wt()MS$P3QX-C(bLG?zgP4*I>MUKJuGjSiMiix z8V5Ga6T6oADpm;rYp5As$soIrWh}UV?OaE`nrVxvyKpe+ku0A^Wso7}XL`m-q{PST z97sfdvb%5L`SHo-+?))x1T(8%dV&;D9o)OUMH5X-AJ4OyR+9z{g9|B)%uBtFF1fE9 z+$tCJr{WjULWgk;S=yT2)u``XN?w14OWx+}(C2LGFUnPi8Wx`3#z^+)+vE7I@i>@< zt4s}fzc{JAk;VzkgKv{XPD%i%@NE72l z+QTzQZJ$R8=AlZN+1tSx)227xBIlT>?y}s28rY+OKtXu_HpGbk=`Lq87Z;$joKd}n zA8|1;KwFIg>(h1zhG6!MVn{zW5we=eETv4=tC8A*1ts+qv91t&zPo$RUG zwomb&the^etREDPa*yo$j|gDP7mqVo3$Z<=VY+s77#Hugq2p=;tJY(RwKfJw9mPDbOC6h|)03hQDT>pJW5dJ{d<*AL?U&u!OK-Qrqf;N&qui+To8W;5H~i&!`aAgLVTqb4 eKg%iNAMhXZMnf4B3snIRc)bJ0$Ez&Vum1sJ!8O+a literal 0 HcmV?d00001 diff --git a/doc/Q20航线任务.docx b/doc/Q20航线任务.docx new file mode 100644 index 0000000000000000000000000000000000000000..6f27500736f23ea49f24596ddfc3059d113fa691 GIT binary patch literal 42811 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 literal 0 HcmV?d00001 diff --git a/doc/Q20设备拓扑状态.docx b/doc/Q20设备拓扑状态.docx new file mode 100644 index 0000000000000000000000000000000000000000..0aba473e3fb5188a3e768ec9003047b7447b9088 GIT binary patch literal 5502 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