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 index 4f2134d..7899adf 100644 --- 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 @@ -332,13 +332,13 @@ public class Q20CommandController { return new Result<>().ok(q20CommandService.logData(deviceSn, dto)); } - // ==================== OTA ==================== + // ==================== 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)); - } +// @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/Q20RouteController.java b/admin/src/main/java/com/multictrl/modules/business/q20/controller/Q20RouteController.java index 2fea50f..9399d75 100644 --- 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 @@ -80,25 +80,25 @@ public class Q20RouteController { 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("/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)); - } +// @GetMapping("/progress/{deviceSn}") +// @Operation(summary = "获取航线执行进度(route_progress)") +// @RequiresPermissions("bus:q20:route") +// public Result routeProgress(@PathVariable String deviceSn) { +// return new Result<>().ok(q20RouteService.routeProgress(deviceSn)); +// } // ==================== 暂停航线 ==================== @@ -120,33 +120,33 @@ public class Q20RouteController { 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)); - } +// @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)); - } +// @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)); - } +// @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/Q20RouteActionDTO.java b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20RouteActionDTO.java index 0ca8973..d1a553e 100644 --- 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 @@ -20,7 +20,7 @@ public class Q20RouteActionDTO { @JsonProperty("action_id") private int actionId; - @Schema(description = "动作执行函数:takePhoto/startRecord/stopRecord/focus/zoom/customDirName/gimbalRotate/rotateYaw/hover/gimbalEvenlyRotate/orientedShoot/panoShot/recordPointCloud") + @Schema(description = "动作执行函数:takePhoto/startRecord/stopRecord/zoom/gimbalRotate/rotateYaw/hover") @JsonProperty("action_actuator_func") private String actionActuatorFunc; 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 index 666f9ab..ddbb92d 100644 --- 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 @@ -1,6 +1,5 @@ package com.multictrl.modules.business.q20.dto; -import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -16,22 +15,6 @@ import java.util.List; @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/Q20RouteDroneInfoDTO.java b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20RouteDroneInfoDTO.java deleted file mode 100644 index 8a6d79f..0000000 --- a/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20RouteDroneInfoDTO.java +++ /dev/null @@ -1,24 +0,0 @@ -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 index 27499fc..0d538b2 100644 --- 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 @@ -1,9 +1,12 @@ package com.multictrl.modules.business.q20.dto; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.Nulls; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; +import java.util.ArrayList; import java.util.List; /** @@ -19,15 +22,17 @@ public class Q20RouteExecuteDTO { @Schema(description = "执行模式:1=执行最近一次上传航线,2=执行指定route_id的航线") private int mode; - @Schema(description = "航线ID,mode=2时有效") + @Schema(description = "航线ID,mode=2时有效;mode=1时为空字符串") @JsonProperty("route_id") - private String routeId; + @JsonSetter(nulls = Nulls.AS_EMPTY) + private String routeId = ""; @Schema(description = "是否指定机库:0=未设置,1=已设置") @JsonProperty("specific_dock") private int specificDock; - @Schema(description = "机库信息列表") + @Schema(description = "机库信息列表,空时为空数组[]") @JsonProperty("dock_info") - private List dockInfo; + @JsonSetter(nulls = Nulls.AS_EMPTY) + private List dockInfo = new ArrayList<>(); } 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 index d3d56e7..72d9656 100644 --- 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 @@ -16,19 +16,11 @@ import java.util.List; @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") + @Schema(description = "执行高度模式:relativeToStartPoint(相对起飞点高度)/ WGS84(椭球高)") @JsonProperty("execute_height_mode") private String executeHeightMode; 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 index dfa13c9..ab58746 100644 --- 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 @@ -14,10 +14,6 @@ import lombok.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; @@ -38,27 +34,11 @@ public class Q20RouteMissionConfigDTO { @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)") + @Schema(description = "降落点海拔高度(m),默认5") @JsonProperty("land_height") - private Float landHeight; - - @Schema(description = "无人机信息") - @JsonProperty("drone_info") - private Q20RouteDroneInfoDTO droneInfo; - - @Schema(description = "挂载信息") - @JsonProperty("payload_info") - private Q20RoutePayloadInfoDTO payloadInfo; + private Float landHeight = 5f; } 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 deleted file mode 100644 index c295452..0000000 --- a/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20RoutePayloadInfoDTO.java +++ /dev/null @@ -1,24 +0,0 @@ -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 index 7048c13..7a10877 100644 --- 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 @@ -17,10 +17,6 @@ public class Q20RoutePlacemarkDTO { @Schema(description = "航点序号,从0开始") private int index; - @Schema(description = "是否危险点:0=安全,1=危险") - @JsonProperty("is_risky") - private Integer isRisky; - @Schema(description = "航点坐标") private Q20RoutePointDTO point; @@ -36,18 +32,6 @@ public class Q20RoutePlacemarkDTO { @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/Q20RouteWaypointHeadingDTO.java b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20RouteWaypointHeadingDTO.java deleted file mode 100644 index 49f9902..0000000 --- a/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20RouteWaypointHeadingDTO.java +++ /dev/null @@ -1,32 +0,0 @@ -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 deleted file mode 100644 index 5ea2fd6..0000000 --- a/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20RouteWaypointTurnDTO.java +++ /dev/null @@ -1,24 +0,0 @@ -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/service/impl/Q20CommandServiceImpl.java b/admin/src/main/java/com/multictrl/modules/business/q20/service/impl/Q20CommandServiceImpl.java index ab4400d..2dc3845 100644 --- 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 @@ -30,6 +30,7 @@ public class Q20CommandServiceImpl implements Q20CommandService { String topic = String.format(SERVICES_TOPIC, deviceSn); JSONObject payload = djiBaseService.getPayload(method, data); payload.set("gateway", deviceSn); + payload.set("need_reply", 1); return djiBaseService.sendWaitReplyJudgeResult(topic, payload); } @@ -37,6 +38,7 @@ public class Q20CommandServiceImpl implements Q20CommandService { String topic = String.format(SERVICES_TOPIC, deviceSn); JSONObject payload = djiBaseService.getPayload(method, data); payload.set("gateway", deviceSn); + payload.set("need_reply", 1); JSONObject reply = djiBaseService.sendWaitReply(topic, payload); if (reply == null) { throw new RenException("设备未响应,请检查设备是否在线"); @@ -71,6 +73,12 @@ public class Q20CommandServiceImpl implements Q20CommandService { @Override public String goHome(String deviceSn, Q20GoHomeDTO dto) { + if (dto.getMode() == 1 && dto.getSafeAltitude() == null) { + throw new RenException("返航模式为安全高度返回(mode=1)时,必须填写安全返航高度(safe_altitude)"); + } + if (dto.getSpecificHome() != null && dto.getSpecificHome() == 1 && dto.getHomePoint() == null) { + throw new RenException("指定返航点(specific_home=1)时,必须填写返航点坐标(home_point)"); + } JSONObject data = new JSONObject(); data.set("mode", dto.getMode()); if (dto.getSafeAltitude() != null) { 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 index e302dc6..a181c8b 100644 --- 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 @@ -67,6 +67,15 @@ public class Q20RouteServiceImpl implements Q20RouteService { return djiBaseService.sendWaitReplyJudgeResult(topic, djiBaseService.getPayload(method, data)); } + /** 发送指令并等待回复(携带 gateway 与 need_reply=1),返回结果描述字符串 */ + private String execCmdWithGateway(String deviceSn, String method, JSONObject data) { + String topic = String.format(SERVICES_TOPIC, deviceSn); + JSONObject payload = djiBaseService.getPayload(method, data); + payload.set("gateway", deviceSn); + payload.set("need_reply", 1); + return djiBaseService.sendWaitReplyJudgeResult(topic, payload); + } + /** 发送指令并等待回复,返回完整回复JSONObject;超时抛出RenException */ private JSONObject execCmdGetReply(String deviceSn, String method, JSONObject data) { String topic = String.format(SERVICES_TOPIC, deviceSn); @@ -77,6 +86,19 @@ public class Q20RouteServiceImpl implements Q20RouteService { return reply; } + /** 发送指令并等待回复(携带 gateway 与 need_reply=1),返回完整回复JSONObject;超时抛出RenException */ + private JSONObject execCmdGetReplyWithGateway(String deviceSn, String method, JSONObject data) { + String topic = String.format(SERVICES_TOPIC, deviceSn); + JSONObject payload = djiBaseService.getPayload(method, data); + payload.set("gateway", deviceSn); + payload.set("need_reply", 1); + JSONObject reply = djiBaseService.sendWaitReply(topic, payload); + if (reply == null) { + throw new RenException("设备未响应,请检查设备是否在线"); + } + return reply; + } + /** 利用Jackson @JsonProperty注解将DTO序列化为snake_case键的JSONObject */ private JSONObject dtoToJson(Object dto) { try { @@ -145,9 +167,10 @@ public class Q20RouteServiceImpl implements Q20RouteService { @Transactional(rollbackFor = Exception.class) public String routeUpload(String deviceSn, Q20RouteUploadDTO dto) { validateWaypoints(dto); + validateMissionConfig(dto); checkRouteNotExists(dto.getWayline(), dto.getRouteName()); JSONObject data = dtoToJson(dto); - String result = execCmd(deviceSn, "route_upload", data); + String result = execCmdWithGateway(deviceSn, "route_upload", data); saveRouteToLocal(deviceSn, dto); return result; } @@ -164,6 +187,34 @@ public class Q20RouteServiceImpl implements Q20RouteService { if (valid < 2) { throw new RenException("航线至少需要两个有效航点(含经纬度)"); } + // 不使用全局速度(use_global_speed=0)的航点必须填写航点飞行速度(waypoint_speed) + for (Q20RoutePlacemarkDTO p : placemarks) { + if (p != null && p.getUseGlobalSpeed() != null && p.getUseGlobalSpeed() == 0 + && p.getWaypointSpeed() == null) { + throw new RenException("航点" + p.getIndex() + "未使用全局速度时,必须填写航点飞行速度(waypoint_speed)"); + } + } + } + + /** + * 任务配置条件必填校验: + * preLand 时 action_value 必填;goHome 时 go_home_type 必填; + * exit_on_rc_lost 为 executeLostAction 时 execute_rc_lost_action 必填 + */ + private void validateMissionConfig(Q20RouteUploadDTO dto) { + Q20RouteMissionConfigDTO mc = dto != null ? dto.getMissionConfig() : null; + if (mc == null) { + return; + } + if ("preLand".equals(mc.getFinishAction()) && StrUtil.isBlank(mc.getActionValue())) { + throw new RenException("结束动作为精准降落(preLand)时,必须填写精准降落二维码值(action_value)"); + } + if ("goHome".equals(mc.getFinishAction()) && StrUtil.isBlank(mc.getGoHomeType())) { + throw new RenException("结束动作为返航(goHome)时,必须填写返航类型(go_home_type)"); + } + if ("executeLostAction".equals(mc.getExitOnRcLost()) && StrUtil.isBlank(mc.getExecuteRcLostAction())) { + throw new RenException("失联退出策略为执行失控动作(executeLostAction)时,必须填写失联执行动作(execute_rc_lost_action)"); + } } /** 上传前校验:航线ID和航线名称均不能与库中已有记录重复 */ @@ -189,7 +240,7 @@ public class Q20RouteServiceImpl implements Q20RouteService { @Override public String routeExecute(String deviceSn, Q20RouteExecuteDTO dto) { JSONObject data = dtoToJson(dto); - String result = execCmd(deviceSn, "route_execute", data); + String result = execCmdWithGateway(deviceSn, "route_execute", data); saveFlightTask(deviceSn, dto.getRouteId()); return result; } @@ -198,13 +249,14 @@ public class Q20RouteServiceImpl implements Q20RouteService { @Transactional(rollbackFor = Exception.class) public String routeAuto(String deviceSn, Q20RouteAutoDTO dto) { validateWaypoints(dto.getRouteInfo()); + validateMissionConfig(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); + String result = execCmdWithGateway(deviceSn, "route_auto", data); if (dto.getRouteInfo() != null) { saveRouteToLocal(deviceSn, dto.getRouteInfo()); } @@ -217,7 +269,7 @@ public class Q20RouteServiceImpl implements Q20RouteService { public String breakLoopHover(String deviceSn) { JSONObject data = new JSONObject(); data.set("value", 1); - return execCmd(deviceSn, "break_loop_hover", data); + return execCmdWithGateway(deviceSn, "break_loop_hover", data); } @Override @@ -232,28 +284,28 @@ public class Q20RouteServiceImpl implements Q20RouteService { public JSONObject routeProgress(String deviceSn) { JSONObject data = new JSONObject(); data.set("value", 1); - return execCmdGetReply(deviceSn, "route_progress", data); + return execCmdGetReplyWithGateway(deviceSn, "route_progress", data); } @Override public String routePause(String deviceSn) { JSONObject data = new JSONObject(); data.set("value", 1); - return execCmd(deviceSn, "route_pause", data); + return execCmdWithGateway(deviceSn, "route_pause", data); } @Override public String routeResume(String deviceSn) { JSONObject data = new JSONObject(); data.set("value", 1); - return execCmd(deviceSn, "route_resume", data); + return execCmdWithGateway(deviceSn, "route_resume", data); } @Override public String routeFinish(String deviceSn) { JSONObject data = new JSONObject(); data.set("value", 1); - return execCmd(deviceSn, "route_finish", data); + return execCmdWithGateway(deviceSn, "route_finish", data); } @Override @@ -306,19 +358,12 @@ public class Q20RouteServiceImpl implements Q20RouteService { 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(); @@ -439,26 +484,10 @@ public class Q20RouteServiceImpl implements Q20RouteService { ? (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"); + // 偏航/转弯模式不再由航点配置,统一使用默认值入库 + wp.setWaypointHeadingMode("followWayline"); + wp.setWaypointHeadingPathMode("followBadArc"); + wp.setWaypointTruningMode("toPointAndStopWithDiscontinuityCurvature"); return wp; } diff --git a/admin/src/main/resources/static/q20-ctrl.html b/admin/src/main/resources/static/q20-ctrl.html index 4f0a316..355b0f1 100644 --- a/admin/src/main/resources/static/q20-ctrl.html +++ b/admin/src/main/resources/static/q20-ctrl.html @@ -132,7 +132,7 @@ h6.cmd-title { font-size: 12px; color: #e6edf3; font-weight: 600; margin-bottom: - + @@ -193,12 +193,12 @@ h6.cmd-title { font-size: 12px; color: #e6edf3; font-weight: 600; margin-bottom:
-
紧急悬停
+
停止动作
-
stop — 立即悬停 (value 固定=1)
-

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

+
stop — 停止动作 (value 固定=1)
+

无需参数,发送后飞机停止当前动作

@@ -213,7 +213,6 @@ h6.cmd-title { font-size: 12px; color: #e6edf3; font-weight: 600; margin-bottom:
@@ -841,7 +840,7 @@ h6.cmd-title { font-size: 12px; color: #e6edf3; font-weight: 600; margin-bottom: - +
@@ -875,9 +875,9 @@ h6.cmd-title { font-size: 12px; color: #e6edf3; font-weight: 600; margin-bottom:
- + - +
@@ -900,13 +900,6 @@ h6.cmd-title { font-size: 12px; color: #e6edf3; font-weight: 600; margin-bottom: -
- - -
-
-
-
- -
@@ -952,18 +936,6 @@ h6.cmd-title { font-size: 12px; color: #e6edf3; font-weight: 600; margin-bottom:
-
- - -
-
-
-
-
@@ -972,6 +944,26 @@ h6.cmd-title { font-size: 12px; color: #e6edf3; font-weight: 600; margin-bottom:
+ +
无人机设置 (vehicle_config)
+
+
+ + +
+
+
+ + +
+
@@ -986,6 +978,18 @@ h6.cmd-title { font-size: 12px; color: #e6edf3; font-weight: 600; margin-bottom:
+ +
+
备降点列表 (alternate_points) 选填
+
+ + + +
+
+
+ +
@@ -1002,7 +1006,7 @@ h6.cmd-title { font-size: 12px; color: #e6edf3; font-weight: 600; margin-bottom:
- +
@@ -1054,7 +1059,7 @@ h6.cmd-title { font-size: 12px; color: #e6edf3; font-weight: 600; margin-bottom:
- + @@ -1099,13 +1105,6 @@ h6.cmd-title { font-size: 12px; color: #e6edf3; font-weight: 600; margin-bottom: -
- - -
-
@@ -1144,10 +1141,6 @@ h6.cmd-title { font-size: 12px; color: #e6edf3; font-weight: 600; margin-bottom:
-
-
-
-
@@ -1212,6 +1205,18 @@ h6.cmd-title { font-size: 12px; color: #e6edf3; font-weight: 600; margin-bottom:
+ +
+
备降点列表 (alternate_points) 选填
+
+ + + +
+
+
+ +
@@ -1726,8 +1731,18 @@ async function cmdGetAndShow(action, displayId, respId) { // ==================== 飞行控制 ==================== function goHomeCmd() { - const body = { mode: +v('gh_mode'), safeAltitude: +v('gh_safe_alt'), speed: +v('gh_speed') }; - if (document.getElementById('gh_specific_home').checked) { + const mode = +v('gh_mode'); + const safeAltRaw = (v('gh_safe_alt') || '').trim(); + // mode=1(安全高度返回)时安全高度必填 + if (mode === 1 && !safeAltRaw) { toast('返航模式为安全高度返回(mode=1)时,必须填写安全高度', true); return; } + const specific = document.getElementById('gh_specific_home').checked; + // 指定返航点时返航点经纬度必填 + if (specific && (!(v('gh_hp_lng') || '').trim() || !(v('gh_hp_lat') || '').trim())) { + toast('指定返航点时,必须填写返航点经纬度', true); return; + } + const body = { mode, speed: +v('gh_speed') }; + if (safeAltRaw) body.safeAltitude = +safeAltRaw; + if (specific) { body.specificHome = 1; body.homePoint = { longitude: +v('gh_hp_lng'), @@ -1788,13 +1803,13 @@ function sendDrcManual() { sendDrc({ vx: +v('drc_vx'), vy: +v('drc_vy'), vz: +v('drc_vz'), vyaw: +v('drc_vyaw') }); } -// ==================== OTA ==================== -function otaCmd() { - cmd('otaUpgrade', 'POST', { - sn: v('ota_sn') || sn(), model: v('ota_model'), url: v('ota_url'), - md5: v('ota_md5'), packageName: v('ota_pkg_name'), version: v('ota_version'), amend: v('ota_amend') - }, 'ota_resp'); -} +// ==================== OTA(暂时停用,前端先注释) ==================== +// function otaCmd() { +// cmd('otaUpgrade', 'POST', { +// sn: v('ota_sn') || sn(), model: v('ota_model'), url: v('ota_url'), +// md5: v('ota_md5'), packageName: v('ota_pkg_name'), version: v('ota_version'), amend: v('ota_amend') +// }, 'ota_resp'); +// } // ==================== 航线任务 ==================== // 当前时间戳 yyyyMMddHHmmss @@ -1846,25 +1861,26 @@ async function routeJsonCmd(action, textareaId, respId) { request('POST', API_BASE + '/business/q20/route/' + action + '/' + s, body, respId); } -function routeInfo() { - const s = sn(); - const mode = v('ri_mode'), val = v('ri_value').trim(); - let url = API_BASE + '/business/q20/route/info/' + s + '?mode=' + mode; - if (val) url += '&value=' + encodeURIComponent(val); - request('GET', url, null, 'ri_resp'); -} +// 获取航线信息暂时停用(前后端接口先注释) +// function routeInfo() { +// const s = sn(); +// const mode = v('ri_mode'), val = v('ri_value').trim(); +// let url = API_BASE + '/business/q20/route/info/' + s + '?mode=' + mode; +// if (val) url += '&value=' + encodeURIComponent(val); +// request('GET', url, null, 'ri_resp'); +// } async function routeExecuteCmd() { if (!await cfm()) return; const s = sn(); const body = { mode: +v('re_mode'), - routeId: v('re_route_id') || undefined, - specificDock: +v('re_specific_dock'), - dockInfo: [] + route_id: v('re_route_id') || '', + specific_dock: +v('re_specific_dock'), + dock_info: [] }; - if (body.specificDock === 1) { - try { body.dockInfo = JSON.parse(document.getElementById('re_dock_info').value); } + if (body.specific_dock === 1) { + try { body.dock_info = JSON.parse(document.getElementById('re_dock_info').value); } catch (e) { show('re_resp', '机库JSON解析失败: ' + e.message, true); return; } } request('POST', API_BASE + '/business/q20/route/execute/' + s, body, 're_resp'); @@ -1881,43 +1897,106 @@ function buildWaypointRow(prefix, idx) {
-
+
-
-
-
-
- -
-
- -
-
- -
+
+ 动作组 (action_group) +
+
`; } +// 自定义速时速度为必填:切换红色*标记,并清除可能的错误高亮 +function toggleWaypointSpeedReq(prefix, idx) { + const custom = !!document.getElementById(prefix + '_finish_' + idx)?.value; + const star = document.getElementById(prefix + '_speedstar_' + idx); + if (star) star.style.display = custom ? '' : 'none'; + const speed = document.getElementById(prefix + '_speed_' + idx); + if (speed && (!custom || (speed.value || '').trim())) speed.classList.remove('field-missing'); +} + +// ---- 航点动作 (action_group.action) ---- +// 动作类型列表(依据 Q20航线任务.docx 动作类型) +const ACTION_FUNCS = [ + ['takePhoto', '单拍'], ['startRecord', '开始录像'], ['stopRecord', '停止录像'], + ['zoom', '变焦'], ['gimbalRotate', '旋转云台'], ['rotateYaw', '飞行器偏航'], + ['hover', '悬停等待'], +]; + +// 各动作类型的参数字段定义(依据 Q20航线任务.docx 动作类型参数) +// k=字段名 l=中文标签 t=类型(number/text/select) d=默认值 o=select选项 +const ACTION_PARAM_SCHEMA = { + takePhoto: [{k:'payload_position_index',l:'挂载位置',t:'number',d:0},{k:'file_suffix',l:'文件后缀',t:'text',d:''}], + startRecord: [{k:'payload_position_index',l:'挂载位置',t:'number',d:0},{k:'file_suffix',l:'文件后缀',t:'text',d:''}], + stopRecord: [{k:'payload_position_index',l:'挂载位置',t:'number',d:0}], + zoom: [{k:'payload_position_index',l:'挂载位置',t:'number',d:0},{k:'focal_length',l:'变焦焦距(mm)',t:'number',d:24}], + gimbalRotate:[{k:'payload_position_index',l:'挂载位置',t:'number',d:0},{k:'gimbal_heading_yaw_base',l:'偏航坐标系',t:'select',o:['north'],d:'north'},{k:'gimbal_rotate_mode',l:'转动模式',t:'select',o:['absoluteAngle'],d:'absoluteAngle'},{k:'gimbal_pitch_rotate_enable',l:'使能Pitch',t:'number',d:1},{k:'gimbal_pitch_rotate_angle',l:'Pitch角度',t:'number',d:0},{k:'gimbal_roll_rotate_enable',l:'使能Roll',t:'number',d:0},{k:'gimbal_roll_rotate_angle',l:'Roll角度',t:'number',d:0},{k:'gimbal_yaw_rotate_enable',l:'使能Yaw',t:'number',d:0},{k:'gimbal_yaw_rotate_angle',l:'Yaw角度',t:'number',d:0},{k:'gimbal_rotate_time_enable',l:'使能转动时间',t:'number',d:0},{k:'gimbal_rotate_time',l:'转动用时(s)',t:'number',d:0}], + rotateYaw: [{k:'aircraft_heading',l:'目标偏航角[-180,180]',t:'number',d:0},{k:'aircraft_path_mode',l:'转动方向',t:'select',o:['clockwise','counterClockwise'],d:'clockwise'}], + hover: [{k:'hover_time',l:'悬停时间(s,-1恒稳)',t:'number',d:1}], +}; + +// 每个航点的动作计数器,key = `${prefix}_${wpIdx}`,仅用于生成唯一动作行下标 +const actionCounters = {}; + +function addAction(prefix, wpIdx) { + const key = prefix + '_' + wpIdx; + const aIdx = (actionCounters[key] = (actionCounters[key] || 0) + 1) - 1; + const opts = ACTION_FUNCS.map(([val, lab]) => ``).join(''); + const html = `
+
+
+ +
+ +
+
+
`; + document.getElementById(`${prefix}_actions_${wpIdx}`).insertAdjacentHTML('beforeend', html); + renderActionParams(prefix, wpIdx, aIdx); +} + +function removeAction(prefix, wpIdx, aIdx) { + const row = document.getElementById(`${prefix}_act_${wpIdx}_${aIdx}`); + if (row) row.remove(); +} + +// 按动作类型动态渲染参数输入框 +function renderActionParams(prefix, wpIdx, aIdx) { + const func = document.getElementById(`${prefix}_actfunc_${wpIdx}_${aIdx}`).value; + const schema = ACTION_PARAM_SCHEMA[func] || []; + document.getElementById(`${prefix}_actparams_${wpIdx}_${aIdx}`).innerHTML = schema.map(f => { + const id = `${prefix}_actp_${wpIdx}_${aIdx}_${f.k}`; + const input = f.t === 'select' + ? `` + : ``; + return `
${input}
`; + }).join(''); +} + +// 收集某航点下的动作列表(按 DOM 顺序) +function collectActions(prefix, wpIdx) { + const container = document.getElementById(`${prefix}_actions_${wpIdx}`); + if (!container) return []; + const actions = []; + container.querySelectorAll(`select[id^="${prefix}_actfunc_${wpIdx}_"]`).forEach(sel => { + const func = sel.value; + const aIdx = sel.id.split('_').pop(); + const param = {}; + (ACTION_PARAM_SCHEMA[func] || []).forEach(f => { + const el = document.getElementById(`${prefix}_actp_${wpIdx}_${aIdx}_${f.k}`); + if (el) param[f.k] = f.t === 'number' ? +el.value : el.value; + }); + actions.push({ action_id: actions.length, action_actuator_func: func, action_actuator_func_param: param }); + }); + return actions; +} + function addWaypoint() { const container = document.getElementById('ru_waypoints'); container.insertAdjacentHTML('beforeend', buildWaypointRow('wp', waypointCount++)); @@ -1938,62 +2017,125 @@ function collectPlacemarks(prefix, count) { const lat = document.getElementById(prefix + '_lat_' + i)?.value; if (!lng || !lat) continue; const useCustomSpeed = !!document.getElementById(prefix + '_finish_' + i).value; - const headingMode = document.getElementById(prefix + '_heading_mode_' + i)?.value || 'followWayline'; - const headingPathMode = document.getElementById(prefix + '_heading_path_mode_' + i)?.value || 'followBadArc'; - const turnMode = document.getElementById(prefix + '_turn_mode_' + i)?.value || 'toPointAndStopWithDiscontinuityCurvature'; const pm = { index: placemarks.length, point: { coordinates: lng + ',' + lat }, execute_height: +document.getElementById(prefix + '_alt_' + i).value, - use_global_speed: useCustomSpeed ? 0 : 1, - waypoint_heading_param: { waypoint_heading_mode: headingMode, waypoint_heading_path_mode: headingPathMode }, - waypoint_turn_param: { waypoint_turn_mode: turnMode }, - use_straight_line: 1 + use_global_speed: useCustomSpeed ? 0 : 1 }; if (useCustomSpeed) { pm.waypoint_speed = +document.getElementById(prefix + '_speed_' + i).value; } + const actions = collectActions(prefix, i); + if (actions.length) { + pm.action_group = { action: actions }; + } placemarks.push(pm); } return placemarks; } +// 校验:未使用全局速度(自定义速)的航点必须填写航点速度 +function validateWaypointSpeed(prefix, count) { + for (let i = 0; i < count; i++) { + const lng = document.getElementById(prefix + '_lng_' + i)?.value; + const lat = document.getElementById(prefix + '_lat_' + i)?.value; + if (!lng || !lat) continue; + const custom = !!document.getElementById(prefix + '_finish_' + i)?.value; + const speedEl = document.getElementById(prefix + '_speed_' + i); + if (custom && !((speedEl && speedEl.value || '').trim())) { + toast('航点' + i + '为自定义速时,必须填写速度参数', true); + if (speedEl) { + speedEl.classList.add('field-missing'); + speedEl.addEventListener('input', function clr() { speedEl.classList.remove('field-missing'); speedEl.removeEventListener('input', clr); }); + speedEl.focus(); + } + return false; + } + } + return true; +} + +// ---- 备降点 (alternate_points) ---- +let ruAltCount = 0; +let faAltCount = 0; + +// 通用备降点行模板:prefix 区分上传航线('rualt')与一键飞行('faalt') +function buildAlternateRow(prefix, idx) { + return `
+
${idx}
+
+
+
+
`; +} + +function ruAddAlternate() { + document.getElementById('ru_alternates').insertAdjacentHTML('beforeend', buildAlternateRow('rualt', ruAltCount++)); +} +function ruRemoveLastAlternate() { + if (ruAltCount === 0) return; + ruAltCount--; + const row = document.getElementById('rualt_row_' + ruAltCount); + if (row) row.remove(); +} +function faAddAlternate() { + document.getElementById('fa_alternates').insertAdjacentHTML('beforeend', buildAlternateRow('faalt', faAltCount++)); +} +function faRemoveLastAlternate() { + if (faAltCount === 0) return; + faAltCount--; + const row = document.getElementById('faalt_row_' + faAltCount); + if (row) row.remove(); +} + +// 从指定前缀的备降点表单读取 alternate_points 数组(上传/一键飞行共用) +function collectAlternatePoints(prefix, count) { + const points = []; + for (let i = 0; i < count; i++) { + const lng = document.getElementById(prefix + '_lng_' + i)?.value; + const lat = document.getElementById(prefix + '_lat_' + i)?.value; + if (!lng || !lat) continue; + points.push({ + point: { coordinates: lng + ',' + lat }, + alternate_height: +document.getElementById(prefix + '_alt_' + i).value + }); + } + return points; +} + function buildUploadDto() { const placemarks = collectPlacemarks('wp', waypointCount); const finishAction = v('ru_finish'); const mc = { - fly_to_wayline_mode: v('ru_fly_mode'), finish_action: finishAction, go_home_type: v('ru_go_home_type'), exit_on_rc_lost: v('ru_exit_rc_lost'), execute_rc_lost_action: v('ru_rc_lost_action'), - takeoff_security_height: +v('ru_takeoff_h'), - global_transitional_speed: +v('ru_speed'), global_RTH_height: +v('ru_rth_h'), - land_height: 0, - drone_info: { drone_enum_value: +v('ru_drone_enum'), drone_sub_enum_value: +v('ru_drone_sub') }, - payload_info: { payload_enum_value: +v('ru_payload_enum'), payload_position_index: +v('ru_payload_pos') } + land_height: 5 }; if (finishAction === 'preLand') { const av = v('ru_action_value'); if (av) mc.action_value = av; } + const folder = { + auto_flight_speed: +v('ru_speed'), + execute_height_mode: v('ru_height_mode'), + placemark: placemarks + }; + const altPoints = collectAlternatePoints('rualt', ruAltCount); + if (altPoints.length) folder.alternate_points = altPoints; return { routeName: v('ru_name'), wayline: v('ru_wayline'), flight_height: +v('ru_flight_height'), accurate_import: document.getElementById('ru_accurate_import').checked, mission_config: mc, - folder: { - template_id: 0, - wayline_id: 0, - auto_flight_speed: +v('ru_speed'), - execute_height_mode: v('ru_height_mode'), - placemark: placemarks - }, + folder: folder, vehicle_config: { go_home_type: v('ru_go_home_type'), - global_RTH_height: +v('ru_rth_h'), + global_RTH_height: +v('ru_vc_rth_h'), low_battery_action: v('ru_low_battery') } }; @@ -2001,6 +2143,7 @@ function buildUploadDto() { async function routeUploadCmd() { if (collectPlacemarks('wp', waypointCount).length < 2) { toast('请至少填写两个有效航点(含经纬度)', true); return; } + if (!validateWaypointSpeed('wp', waypointCount)) return; if (!await cfm()) return; const s = sn(); request('POST', API_BASE + '/business/q20/route/upload/' + s, buildUploadDto(), 'ru_resp'); @@ -2031,35 +2174,31 @@ function buildAutoRouteInfo() { const placemarks = collectPlacemarks('fawp', faWaypointCount); const finishAction = v('fa_finish'); const mc = { - fly_to_wayline_mode: v('fa_fly_mode'), finish_action: finishAction, go_home_type: v('fa_go_home_type'), exit_on_rc_lost: v('fa_exit_rc_lost'), execute_rc_lost_action: v('fa_rc_lost_action'), - takeoff_security_height: +v('fa_takeoff_h'), - global_transitional_speed: +v('fa_speed'), global_RTH_height: +v('fa_rth_h'), - land_height: 0, - drone_info: { drone_enum_value: +v('fa_drone_enum'), drone_sub_enum_value: +v('fa_drone_sub') }, - payload_info: { payload_enum_value: +v('fa_payload_enum'), payload_position_index: +v('fa_payload_pos') } + land_height: 5 }; if (finishAction === 'preLand') { const av = v('fa_action_value'); if (av) mc.action_value = av; } + const folder = { + auto_flight_speed: +v('fa_speed'), + execute_height_mode: v('fa_height_mode'), + placemark: placemarks + }; + const altPoints = collectAlternatePoints('faalt', faAltCount); + if (altPoints.length) folder.alternate_points = altPoints; return { routeName: v('fa_name'), wayline: v('fa_wayline'), flight_height: +v('fa_flight_height'), accurate_import: document.getElementById('fa_accurate_import').checked, mission_config: mc, - folder: { - template_id: 0, - wayline_id: 0, - auto_flight_speed: +v('fa_speed'), - execute_height_mode: v('fa_height_mode'), - placemark: placemarks - }, + folder: folder, vehicle_config: { go_home_type: v('fa_go_home_type'), global_RTH_height: +v('fa_vc_rth_h'), @@ -2092,6 +2231,7 @@ function buildAutoDto() { async function routeAutoCmd() { if (collectPlacemarks('fawp', faWaypointCount).length < 2) { toast('请至少填写两个有效航点(含经纬度)', true); return; } + if (!validateWaypointSpeed('fawp', faWaypointCount)) return; if (!await cfm()) return; let body; try { body = buildAutoDto(); } @@ -2117,14 +2257,20 @@ let amapInitializing = false; let mapMarkers = []; let mapPolyline = null; let mapTarget = 'upload'; // 'upload' = 上传航线, 'auto' = 一键飞行 +let mapMode = 'waypoint'; // 'waypoint' = 航点, 'alternate' = 备降点 -function openMapModal(target) { +function openMapModal(target, mode) { mapTarget = target === 'auto' ? 'auto' : 'upload'; + mapMode = mode === 'alternate' ? 'alternate' : 'waypoint'; const modal = document.getElementById('mapModal'); modal.style.display = 'flex'; document.getElementById('map_api_key').value = AMAP_KEY; - const altSrc = mapTarget === 'auto' ? 'fa_flight_height' : 'ru_flight_height'; - document.getElementById('map_default_alt').value = document.getElementById(altSrc).value || 100; + if (mapMode === 'alternate') { + document.getElementById('map_default_alt').value = 74; + } else { + const altSrc = mapTarget === 'auto' ? 'fa_flight_height' : 'ru_flight_height'; + document.getElementById('map_default_alt').value = document.getElementById(altSrc).value || 100; + } if (!amapScriptLoaded && !amapInitializing) { loadAmapWithKey(); return; } if (amapScriptLoaded && !amapInstance) { initAmap(); return; } if (amapInstance) setTimeout(() => amapInstance.resize(), 150); @@ -2371,23 +2517,41 @@ function gcj02ToWgs84(lng, lat) { } function confirmMapPoints() { - if (mapMarkers.length === 0) { toast('请先在地图上选择至少一个航点', true); return; } + const isAlt = mapMode === 'alternate'; + if (mapMarkers.length === 0) { toast(isAlt ? '请先在地图上选择至少一个备降点' : '请先在地图上选择至少一个航点', true); return; } const doConvert = document.getElementById('map_wgs84_convert').checked; - const defaultAlt = +document.getElementById('map_default_alt').value || 100; + const defaultAlt = +document.getElementById('map_default_alt').value || (isAlt ? 74 : 100); const isAuto = mapTarget === 'auto'; - const prefix = isAuto ? 'fawp' : 'wp'; - const addFn = isAuto ? faAddWaypoint : addWaypoint; - document.getElementById(isAuto ? 'fa_waypoints' : 'ru_waypoints').innerHTML = ''; - if (isAuto) faWaypointCount = 0; else waypointCount = 0; - mapMarkers.forEach(mp => { - addFn(); - const i = (isAuto ? faWaypointCount : waypointCount) - 1; - let lng = mp.lng, lat = mp.lat; - if (doConvert) [lng, lat] = gcj02ToWgs84(lng, lat); - document.getElementById(prefix + '_lng_' + i).value = lng.toFixed(7); - document.getElementById(prefix + '_lat_' + i).value = lat.toFixed(7); - document.getElementById(prefix + '_alt_' + i).value = defaultAlt; - }); + if (isAlt) { + const prefix = isAuto ? 'faalt' : 'rualt'; + const addFn = isAuto ? faAddAlternate : ruAddAlternate; + document.getElementById(isAuto ? 'fa_alternates' : 'ru_alternates').innerHTML = ''; + if (isAuto) faAltCount = 0; else ruAltCount = 0; + mapMarkers.forEach(mp => { + addFn(); + const i = (isAuto ? faAltCount : ruAltCount) - 1; + let lng = mp.lng, lat = mp.lat; + if (doConvert) [lng, lat] = gcj02ToWgs84(lng, lat); + document.getElementById(prefix + '_lng_' + i).value = lng.toFixed(7); + document.getElementById(prefix + '_lat_' + i).value = lat.toFixed(7); + document.getElementById(prefix + '_alt_' + i).value = defaultAlt; + }); + } else { + const prefix = isAuto ? 'fawp' : 'wp'; + const addFn = isAuto ? faAddWaypoint : addWaypoint; + document.getElementById(isAuto ? 'fa_waypoints' : 'ru_waypoints').innerHTML = ''; + if (isAuto) faWaypointCount = 0; else waypointCount = 0; + Object.keys(actionCounters).forEach(k => { if (k.startsWith(prefix + '_')) delete actionCounters[k]; }); + mapMarkers.forEach(mp => { + addFn(); + const i = (isAuto ? faWaypointCount : waypointCount) - 1; + let lng = mp.lng, lat = mp.lat; + if (doConvert) [lng, lat] = gcj02ToWgs84(lng, lat); + document.getElementById(prefix + '_lng_' + i).value = lng.toFixed(7); + document.getElementById(prefix + '_lat_' + i).value = lat.toFixed(7); + document.getElementById(prefix + '_alt_' + i).value = defaultAlt; + }); + } closeMapModal(); } @@ -2427,19 +2591,19 @@ const FIELD_REQ = { // —— OTA —— ota_sn: 1, ota_model: 0, ota_url: 1, ota_md5: 1, ota_pkg_name: 0, ota_version: 1, ota_amend: 0, // —— 航线:上传航线 —— - ru_name: 1, ru_wayline: 1, ru_fly_mode: 0, ru_finish: 1, ru_action_value: 0, - ru_takeoff_h: 0, ru_rth_h: 0, ru_speed: 1, ru_height_mode: 1, - ru_go_home_type: 0, ru_exit_rc_lost: 0, ru_rc_lost_action: 0, ru_low_battery: 0, - ru_drone_enum: 0, ru_drone_sub: 0, ru_payload_enum: 0, ru_payload_pos: 0, ru_flight_height: 0, ru_accurate_import: 0, + ru_name: 1, ru_wayline: 1, ru_finish: 1, ru_action_value: 0, + ru_rth_h: 0, ru_speed: 1, ru_height_mode: 1, + ru_go_home_type: 0, ru_exit_rc_lost: 0, ru_rc_lost_action: 0, ru_low_battery: 0, ru_vc_rth_h: 0, + ru_flight_height: 0, ru_accurate_import: 0, // —— 航线:获取/执行/围栏 —— ri_mode: 1, ri_value: 0, re_mode: 1, re_route_id: 0, re_specific_dock: 0, re_dock_info: 0, gf_upload_json: 1, // —— 一键飞行 —— - fa_name: 1, fa_wayline: 1, fa_fly_mode: 0, fa_finish: 1, fa_action_value: 0, - fa_takeoff_h: 0, fa_rth_h: 0, fa_speed: 1, fa_height_mode: 1, + fa_name: 1, fa_wayline: 1, fa_finish: 1, fa_action_value: 0, + fa_rth_h: 0, fa_speed: 1, fa_height_mode: 1, fa_exit_rc_lost: 0, fa_rc_lost_action: 0, - fa_drone_enum: 0, fa_drone_sub: 0, fa_payload_enum: 0, fa_payload_pos: 0, fa_flight_height: 0, fa_accurate_import: 0, + fa_flight_height: 0, fa_accurate_import: 0, fa_go_home_type: 0, fa_vc_rth_h: 0, fa_low_battery: 0, fa_exec_mode: 1, fa_specific_dock: 0, fa_dock_info: 0, }; @@ -2478,6 +2642,19 @@ function markField(id, required) { const CONDITIONAL_REQ = { re_route_id: () => v('re_mode') === '2', ri_value: () => v('ri_mode') === '2', + // 结束动作为精准降落(preLand)时,必须填写精准降落二维码值(action_value) + ru_action_value: () => v('ru_finish') === 'preLand', + fa_action_value: () => v('fa_finish') === 'preLand', + // 结束动作为返航(goHome)时,必须填写返航类型(go_home_type) + ru_go_home_type: () => v('ru_finish') === 'goHome', + fa_go_home_type: () => v('fa_finish') === 'goHome', + // 失联退出策略为执行失控动作(executeLostAction)时,必须填写失联执行动作(execute_rc_lost_action) + ru_rc_lost_action: () => v('ru_exit_rc_lost') === 'executeLostAction', + fa_rc_lost_action: () => v('fa_exit_rc_lost') === 'executeLostAction', + // 返航:mode=1(安全高度返回)时安全高度必填;指定返航点时返航点经纬度必填 + gh_safe_alt: () => v('gh_mode') === '1', + gh_hp_lng: () => !!document.getElementById('gh_specific_home')?.checked, + gh_hp_lat: () => !!document.getElementById('gh_specific_home')?.checked, }; // 字段当前是否必填(含条件必填) function isRequiredNow(id) { @@ -2544,13 +2721,15 @@ document.addEventListener('DOMContentLoaded', () => { const savedSn = localStorage.getItem('q20_device_sn'); if (savedSn) { document.getElementById('deviceSn').value = savedSn; - document.getElementById('ota_sn').value = savedSn; + const otaSnEl = document.getElementById('ota_sn'); + if (otaSnEl) otaSnEl.value = savedSn; } document.getElementById('deviceSn').addEventListener('input', function () { const val = this.value.trim(); refreshSnState(); - document.getElementById('ota_sn').value = val; + const otaSnEl = document.getElementById('ota_sn'); + if (otaSnEl) otaSnEl.value = val; // 记住本次输入,下次进入页面自动回填 if (val) localStorage.setItem('q20_device_sn', val); else localStorage.removeItem('q20_device_sn'); @@ -2572,8 +2751,8 @@ document.addEventListener('DOMContentLoaded', () => { document.getElementById('fa_dock_fields').style.display = this.value === '1' ? '' : 'none'; }); - // 执行模式联动:选 2(指定 route_id) 时 route_id 变为必填,实时刷新标记 - ['re_mode', 'ri_mode'].forEach(id => { + // 条件必填联动:route_id / action_value / go_home_type / execute_rc_lost_action / 返航参数 随相关选择实时刷新标记 + ['re_mode', 'ri_mode', 'ru_finish', 'fa_finish', 'ru_exit_rc_lost', 'fa_exit_rc_lost', 'gh_mode', 'gh_specific_home'].forEach(id => { const el = document.getElementById(id); if (el) el.addEventListener('change', refreshConditionalMarkers); });