1.修改Q20飞机协议,删除部分无用协议,修改请求参数

This commit is contained in:
938693313@qq.com 2026-05-29 18:14:41 +08:00
parent 4ce9541397
commit 442cd50d21
15 changed files with 481 additions and 425 deletions

View File

@ -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<Object> 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<Object> otaUpgrade(@PathVariable String deviceSn, @RequestBody Q20OtaUpgradeDTO dto) {
// return new Result<>().ok(q20CommandService.otaUpgrade(deviceSn, dto));
// }
}

View File

@ -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<Object> 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<Object> routeInfo(@PathVariable String deviceSn,
// @RequestParam int mode,
// @RequestParam(required = false) String value) {
// return new Result<>().ok(q20RouteService.routeInfo(deviceSn, mode, value));
// }
// ==================== 获取执行进度 ====================
// ==================== 获取执行进度暂时停用先注释 ====================
@GetMapping("/progress/{deviceSn}")
@Operation(summary = "获取航线执行进度route_progress")
@RequiresPermissions("bus:q20:route")
public Result<Object> routeProgress(@PathVariable String deviceSn) {
return new Result<>().ok(q20RouteService.routeProgress(deviceSn));
}
// @GetMapping("/progress/{deviceSn}")
// @Operation(summary = "获取航线执行进度route_progress")
// @RequiresPermissions("bus:q20:route")
// public Result<Object> routeProgress(@PathVariable String deviceSn) {
// return new Result<>().ok(q20RouteService.routeProgress(deviceSn));
// }
// ==================== 暂停航线 ====================
@ -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<Object> routeFinish(@PathVariable String deviceSn) {
return new Result<>().ok(q20RouteService.routeFinish(deviceSn));
}
// ==================== 获取围栏信息 ====================
@GetMapping("/geofenceInfo/{deviceSn}")
@Operation(summary = "获取围栏信息geofence_info")
@RequiresPermissions("bus:q20:route")
public Result<Object> geofenceInfo(@PathVariable String deviceSn) {
return new Result<>().ok(q20RouteService.geofenceInfo(deviceSn));
}
// ==================== 上传围栏 ====================
@PostMapping("/geofenceUpload/{deviceSn}")
@LogOperation("上传围栏")
@Operation(summary = "上传围栏geofence_upload")
@RequiresPermissions("bus:q20:route")
public Result<Object> geofenceUpload(@PathVariable String deviceSn,
@RequestBody Q20GeofenceUploadDTO dto) {
return new Result<>().ok(q20RouteService.geofenceUpload(deviceSn, dto));
}
// @PostMapping("/finish/{deviceSn}")
// @LogOperation("退出航线")
// @Operation(summary = "退出航线route_finish")
// @RequiresPermissions("bus:q20:route")
// public Result<Object> routeFinish(@PathVariable String deviceSn) {
// return new Result<>().ok(q20RouteService.routeFinish(deviceSn));
// }
// ==================== 获取围栏信息暂时停用先注释 ====================
// @GetMapping("/geofenceInfo/{deviceSn}")
// @Operation(summary = "获取围栏信息geofence_info")
// @RequiresPermissions("bus:q20:route")
// public Result<Object> geofenceInfo(@PathVariable String deviceSn) {
// return new Result<>().ok(q20RouteService.geofenceInfo(deviceSn));
// }
// ==================== 上传围栏暂时停用先注释 ====================
// @PostMapping("/geofenceUpload/{deviceSn}")
// @LogOperation("上传围栏")
// @Operation(summary = "上传围栏geofence_upload")
// @RequiresPermissions("bus:q20:route")
// public Result<Object> geofenceUpload(@PathVariable String deviceSn,
// @RequestBody Q20GeofenceUploadDTO dto) {
// return new Result<>().ok(q20RouteService.geofenceUpload(deviceSn, dto));
// }
}

View File

@ -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;

View File

@ -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<Q20RouteActionDTO> action;
}

View File

@ -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;
}

View File

@ -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 = "航线IDmode=2时有效")
@Schema(description = "航线IDmode=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<Q20DockInfoItemDTO> dockInfo;
@JsonSetter(nulls = Nulls.AS_EMPTY)
private List<Q20DockInfoItemDTO> dockInfo = new ArrayList<>();
}

View File

@ -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;

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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) {

View File

@ -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;
}

View File

@ -132,7 +132,7 @@ h6.cmd-title { font-size: 12px; color: #e6edf3; font-weight: 600; margin-bottom:
<li class="nav-item"><a class="nav-link" data-bs-toggle="tab" href="#tab-safety">安全/降落伞</a></li>
<li class="nav-item"><a class="nav-link" data-bs-toggle="tab" href="#tab-status">状态信息</a></li>
<li class="nav-item"><a class="nav-link" data-bs-toggle="tab" href="#tab-log">日志</a></li>
<li class="nav-item"><a class="nav-link" data-bs-toggle="tab" href="#tab-ota">OTA</a></li>
<!-- OTA 暂时停用:<li class="nav-item"><a class="nav-link" data-bs-toggle="tab" href="#tab-ota">OTA</a></li> -->
<li class="nav-item"><a class="nav-link" data-bs-toggle="tab" href="#tab-route">航线任务</a></li>
</ul>
@ -193,12 +193,12 @@ h6.cmd-title { font-size: 12px; color: #e6edf3; font-weight: 600; margin-bottom:
</div>
<div class="card">
<div class="card-header"><i class="bi bi-pause-circle me-1"></i>紧急悬</div>
<div class="card-header"><i class="bi bi-pause-circle me-1"></i>止动作</div>
<div class="card-body">
<h6 class="cmd-title">stop — 立即悬停 (value 固定=1)</h6>
<p class="text-muted small">无需参数,发送后飞机立即悬</p>
<h6 class="cmd-title">stop — 停止动作 (value 固定=1)</h6>
<p class="text-muted small">无需参数,发送后飞机停止当前动作</p>
<button class="btn btn-danger w-100" onclick="cmd('stop','POST',null,'stop_resp')">
<i class="bi bi-stop-fill me-1"></i>紧急悬
<i class="bi bi-stop-fill me-1"></i>止动作
</button>
<div id="stop_resp" class="resp-box" style="display:none"></div>
</div>
@ -213,7 +213,6 @@ h6.cmd-title { font-size: 12px; color: #e6edf3; font-weight: 600; margin-bottom:
<label class="form-label">返航模式</label>
<select id="gh_mode" class="form-select">
<option value="1">1 安全高度返回</option>
<option value="2">2 直线飞行</option>
<option value="3">3 原路返回</option>
</select>
</div>
@ -841,7 +840,7 @@ h6.cmd-title { font-size: 12px; color: #e6edf3; font-weight: 600; margin-bottom:
</div>
</div>
<!-- ===== OTA ===== -->
<!-- ===== OTA(暂时停用,前端先注释) =====
<div class="tab-pane fade" id="tab-ota">
<div class="section-grid">
<div class="card" style="max-width:600px">
@ -863,6 +862,7 @@ h6.cmd-title { font-size: 12px; color: #e6edf3; font-weight: 600; margin-bottom:
</div>
</div>
</div>
-->
<!-- ===== 航线任务 ===== -->
<div class="tab-pane fade" id="tab-route">
@ -875,9 +875,9 @@ h6.cmd-title { font-size: 12px; color: #e6edf3; font-weight: 600; margin-bottom:
<div class="d-flex gap-2 flex-wrap">
<button class="btn btn-warning" onclick="route('pause','POST',null,'rq_resp')"><i class="bi bi-pause-fill me-1"></i>暂停</button>
<button class="btn btn-success" onclick="route('resume','POST',null,'rq_resp')"><i class="bi bi-play-fill me-1"></i>继续</button>
<button class="btn btn-danger" onclick="route('finish','POST',null,'rq_resp')"><i class="bi bi-stop-fill me-1"></i>退出航线</button>
<!-- 退出航线暂时停用:<button class="btn btn-danger" onclick="route('finish','POST',null,'rq_resp')"><i class="bi bi-stop-fill me-1"></i>退出航线</button> -->
<button class="btn btn-secondary" onclick="route('breakHover','POST',null,'rq_resp')"><i class="bi bi-arrow-repeat me-1"></i>取消悬停</button>
<button class="btn btn-info" onclick="routeGet('progress','rq_resp')"><i class="bi bi-bar-chart me-1"></i>获取进度</button>
<!-- 获取进度暂时停用:<button class="btn btn-info" onclick="routeGet('progress','rq_resp')"><i class="bi bi-bar-chart me-1"></i>获取进度</button> -->
</div>
<div id="rq_resp" class="resp-box" style="display:none"></div>
</div>
@ -900,13 +900,6 @@ h6.cmd-title { font-size: 12px; color: #e6edf3; font-weight: 600; margin-bottom:
<button class="btn btn-outline-secondary" type="button" onclick="fillNextWayline('ru_wayline','route')" title="按库中最新上传航线ID自动生成"><i class="bi bi-arrow-repeat"></i></button>
</div>
</div>
<div class="col-6">
<label class="form-label">起点飞行模式</label>
<select id="ru_fly_mode" class="form-select">
<option value="safely">safely 安全</option>
<option value="pointToPoint">pointToPoint 直达</option>
</select>
</div>
<div class="col-6">
<label class="form-label">完成动作</label>
<select id="ru_finish" class="form-select" onchange="document.getElementById('ru_action_value_row').style.display=this.value==='preLand'?'':'none'">
@ -920,21 +913,12 @@ h6.cmd-title { font-size: 12px; color: #e6edf3; font-weight: 600; margin-bottom:
<label class="form-label">精准降落标识 (actionValue) <small class="text-muted">{hangar_sn}-{dock_code}</small></label>
<input id="ru_action_value" class="form-control" placeholder="例: dock001-1">
</div>
<div class="col-6"><label class="form-label">安全起飞高 (m)</label><input id="ru_takeoff_h" class="form-control" type="number" value="30"></div>
<div class="col-6"><label class="form-label">全局返航高 (m)</label><input id="ru_rth_h" class="form-control" type="number" value="50"></div>
<div class="col-6"><label class="form-label">全局飞行速 (m/s)</label><input id="ru_speed" class="form-control" type="number" value="10" step="0.5"></div>
<div class="col-6"><label class="form-label">高度模式</label>
<select id="ru_height_mode" class="form-select">
<option value="relativeToStartPoint">相对起点</option>
<option value="WGS84">WGS84</option>
<option value="realTimeFollowSurface">实时仿地</option>
</select>
</div>
<div class="col-6">
<label class="form-label">返航类型</label>
<select id="ru_go_home_type" class="form-select">
<option value="directReturn">directReturn 直接</option>
<option value="originalReturn">originalReturn 原路</option>
<option value="relativeToStartPoint">relativeToStartPoint 相对起点</option>
<option value="WGS84">WGS84 椭球高</option>
</select>
</div>
<div class="col-6">
@ -952,18 +936,6 @@ h6.cmd-title { font-size: 12px; color: #e6edf3; font-weight: 600; margin-bottom:
<option value="hover">hover 悬停</option>
</select>
</div>
<div class="col-6">
<label class="form-label">低电动作</label>
<select id="ru_low_battery" class="form-select">
<option value="goBackCritAndLandEmerg">紧急返航降落</option>
<option value="landing">landing 降落</option>
<option value="warning">warning 警告</option>
</select>
</div>
<div class="col-6"><label class="form-label">无人机枚举 (20=Q20)</label><input id="ru_drone_enum" class="form-control" type="number" value="20"></div>
<div class="col-6"><label class="form-label">子枚举 (0=Q20)</label><input id="ru_drone_sub" class="form-control" type="number" value="0"></div>
<div class="col-6"><label class="form-label">挂载枚举</label><input id="ru_payload_enum" class="form-control" type="number" value="30" placeholder="30=ZT30"></div>
<div class="col-6"><label class="form-label">挂载位置索引</label><input id="ru_payload_pos" class="form-control" type="number" value="0"></div>
<div class="col-6"><label class="form-label">全局飞行高 (m)</label><input id="ru_flight_height" class="form-control" type="number" value="100" step="1"></div>
<div class="col-6 d-flex align-items-end pb-1">
<div class="form-check">
@ -972,6 +944,26 @@ h6.cmd-title { font-size: 12px; color: #e6edf3; font-weight: 600; margin-bottom:
</div>
</div>
</div>
<h6 class="cmd-title mt-3">无人机设置 (vehicle_config)</h6>
<div class="row g-2">
<div class="col-6">
<label class="form-label">返航类型</label>
<select id="ru_go_home_type" class="form-select">
<option value="directReturn">directReturn 直接</option>
<option value="originalReturn">originalReturn 原路</option>
</select>
</div>
<div class="col-6"><label class="form-label">返航高度 (m)</label><input id="ru_vc_rth_h" class="form-control" type="number" value="100"></div>
<div class="col-12">
<label class="form-label">低电动作</label>
<select id="ru_low_battery" class="form-select">
<option value="goBackCritAndLandEmerg">紧急返航降落</option>
<option value="landing">landing 降落</option>
<option value="warning">warning 警告</option>
</select>
</div>
</div>
</div>
<!-- 航点列表 -->
<div class="col-lg-8">
@ -986,6 +978,18 @@ h6.cmd-title { font-size: 12px; color: #e6edf3; font-weight: 600; margin-bottom:
<div id="ru_waypoints">
<!-- 航点行由 JS 动态生成 -->
</div>
<!-- 备降点列表 -->
<div class="d-flex align-items-center justify-content-between mb-1 mt-3">
<h6 class="cmd-title mb-0">备降点列表 (alternate_points) <small class="text-muted">选填</small></h6>
<div class="d-flex gap-2">
<button class="btn btn-primary btn-sm px-3" onclick="openMapModal('upload','alternate')"><i class="bi bi-map me-1"></i>地图选点</button>
<button class="btn btn-success btn-sm px-3" onclick="ruAddAlternate()"><i class="bi bi-plus-circle me-1"></i>添加备降点</button>
<button class="btn btn-danger btn-sm px-3" onclick="ruRemoveLastAlternate()"><i class="bi bi-dash-circle me-1"></i>删除末点</button>
</div>
</div>
<div id="ru_alternates">
<!-- 备降点行由 JS 动态生成 -->
</div>
</div>
</div>
<div class="d-flex gap-2 mt-3">
@ -1002,7 +1006,7 @@ h6.cmd-title { font-size: 12px; color: #e6edf3; font-weight: 600; margin-bottom:
<div class="section-grid">
<!-- 获取航线信息 -->
<!-- 获取航线信息(暂时停用,前后端接口先注释)
<div class="card">
<div class="card-header"><i class="bi bi-info-circle me-1"></i>获取航线信息</div>
<div class="card-body">
@ -1026,6 +1030,7 @@ h6.cmd-title { font-size: 12px; color: #e6edf3; font-weight: 600; margin-bottom:
<div id="ri_resp" class="resp-box" style="display:none"></div>
</div>
</div>
-->
<!-- 执行航线 -->
<div class="card">
@ -1054,7 +1059,7 @@ h6.cmd-title { font-size: 12px; color: #e6edf3; font-weight: 600; margin-bottom:
</div>
<div id="re_dock_fields" style="display:none">
<label class="form-label mt-2">机库信息 (JSON数组)</label>
<textarea id="re_dock_info" class="form-control" rows="4" style="font-family:monospace;font-size:11px">[{"index":0,"type":"takeoff","sn":"DOCK_SN","code":"","longitude":113.0,"latitude":23.0,"altitude":0,"heading":0,"alternateSet":0}]</textarea>
<textarea id="re_dock_info" class="form-control" rows="4" style="font-family:monospace;font-size:11px">[{"index":0,"type":"takeoff","sn":"DOCK_SN","code":"","longitude":113.0,"latitude":23.0,"altitude":0,"heading":0,"alternate_set":0}]</textarea>
</div>
<button class="btn btn-success w-100 mt-3" onclick="routeExecuteCmd()">
<i class="bi bi-send me-1"></i>执行航线
@ -1063,7 +1068,7 @@ h6.cmd-title { font-size: 12px; color: #e6edf3; font-weight: 600; margin-bottom:
</div>
</div>
<!-- 围栏信息 -->
<!-- 围栏管理(暂时停用,前端先注释)
<div class="card">
<div class="card-header"><i class="bi bi-hexagon me-1"></i>围栏管理</div>
<div class="card-body">
@ -1079,6 +1084,7 @@ h6.cmd-title { font-size: 12px; color: #e6edf3; font-weight: 600; margin-bottom:
<div id="gf_resp" class="resp-box" style="display:none"></div>
</div>
</div>
-->
</div>
@ -1099,13 +1105,6 @@ h6.cmd-title { font-size: 12px; color: #e6edf3; font-weight: 600; margin-bottom:
<button class="btn btn-outline-secondary" type="button" onclick="fillNextWayline('fa_wayline','auto')" title="按库中最新一键航线ID自动生成"><i class="bi bi-arrow-repeat"></i></button>
</div>
</div>
<div class="col-6">
<label class="form-label">起点飞行模式</label>
<select id="fa_fly_mode" class="form-select">
<option value="safely">safely 安全</option>
<option value="pointToPoint">pointToPoint 直达</option>
</select>
</div>
<div class="col-6">
<label class="form-label">完成动作</label>
<select id="fa_finish" class="form-select" onchange="document.getElementById('fa_action_value_row').style.display=this.value==='preLand'?'':'none'">
@ -1119,14 +1118,12 @@ h6.cmd-title { font-size: 12px; color: #e6edf3; font-weight: 600; margin-bottom:
<label class="form-label">精准降落标识 (actionValue) <small class="text-muted">{hangar_sn}-{dock_code}</small></label>
<input id="fa_action_value" class="form-control" placeholder="例: dock001-1">
</div>
<div class="col-6"><label class="form-label">安全起飞高 (m)</label><input id="fa_takeoff_h" class="form-control" type="number" value="30"></div>
<div class="col-6"><label class="form-label">全局返航高 (m)</label><input id="fa_rth_h" class="form-control" type="number" value="50"></div>
<div class="col-6"><label class="form-label">全局过渡速 (m/s)</label><input id="fa_speed" class="form-control" type="number" value="10" step="0.5"></div>
<div class="col-6"><label class="form-label">高度模式</label>
<select id="fa_height_mode" class="form-select">
<option value="relativeToStartPoint">相对起点</option>
<option value="WGS84">WGS84</option>
<option value="realTimeFollowSurface">实时仿地</option>
<option value="relativeToStartPoint">relativeToStartPoint 相对起点</option>
<option value="WGS84">WGS84 椭球高</option>
</select>
</div>
<div class="col-6">
@ -1144,10 +1141,6 @@ h6.cmd-title { font-size: 12px; color: #e6edf3; font-weight: 600; margin-bottom:
<option value="hover">hover 悬停</option>
</select>
</div>
<div class="col-6"><label class="form-label">无人机枚举 (20=Q20)</label><input id="fa_drone_enum" class="form-control" type="number" value="20"></div>
<div class="col-6"><label class="form-label">子枚举 (0=Q20)</label><input id="fa_drone_sub" class="form-control" type="number" value="0"></div>
<div class="col-6"><label class="form-label">挂载枚举</label><input id="fa_payload_enum" class="form-control" type="number" value="30" placeholder="30=ZT30"></div>
<div class="col-6"><label class="form-label">挂载位置索引</label><input id="fa_payload_pos" class="form-control" type="number" value="0"></div>
<div class="col-6"><label class="form-label">全局飞行高 (m)</label><input id="fa_flight_height" class="form-control" type="number" value="100" step="1"></div>
<div class="col-6 d-flex align-items-end pb-1">
<div class="form-check">
@ -1212,6 +1205,18 @@ h6.cmd-title { font-size: 12px; color: #e6edf3; font-weight: 600; margin-bottom:
<div id="fa_waypoints">
<!-- 航点行由 JS 动态生成 -->
</div>
<!-- 备降点列表 -->
<div class="d-flex align-items-center justify-content-between mb-1 mt-3">
<h6 class="cmd-title mb-0">备降点列表 (alternate_points) <small class="text-muted">选填</small></h6>
<div class="d-flex gap-2">
<button class="btn btn-primary btn-sm px-3" onclick="openMapModal('auto','alternate')"><i class="bi bi-map me-1"></i>地图选点</button>
<button class="btn btn-success btn-sm px-3" onclick="faAddAlternate()"><i class="bi bi-plus-circle me-1"></i>添加备降点</button>
<button class="btn btn-danger btn-sm px-3" onclick="faRemoveLastAlternate()"><i class="bi bi-dash-circle me-1"></i>删除末点</button>
</div>
</div>
<div id="fa_alternates">
<!-- 备降点行由 JS 动态生成 -->
</div>
</div>
</div>
<div class="d-flex gap-2 mt-3">
@ -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) {
<div class="col"><label class="form-label">经度<span class="req-star">*</span></label><input id="${prefix}_lng_${idx}" class="form-control form-control-sm" type="number" step="0.0000001" placeholder="113.000000"></div>
<div class="col"><label class="form-label">纬度<span class="req-star">*</span></label><input id="${prefix}_lat_${idx}" class="form-control form-control-sm" type="number" step="0.0000001" placeholder="23.000000"></div>
<div class="col-2"><label class="form-label">高度(m)<span class="req-star">*</span></label><input id="${prefix}_alt_${idx}" class="form-control form-control-sm" type="number" value="50"></div>
<div class="col-2"><label class="form-label">速度(m/s)</label><input id="${prefix}_speed_${idx}" class="form-control form-control-sm" type="number" value="10" step="0.5"></div>
<div class="col-2"><label class="form-label">速度(m/s)<span class="req-star" id="${prefix}_speedstar_${idx}" style="display:none">*</span></label><input id="${prefix}_speed_${idx}" class="form-control form-control-sm" type="number" value="10" step="0.5"></div>
<div class="col-auto"><label class="form-label">自定义速</label>
<select id="${prefix}_finish_${idx}" class="form-select form-select-sm">
<select id="${prefix}_finish_${idx}" class="form-select form-select-sm" onchange="toggleWaypointSpeedReq('${prefix}', ${idx})">
<option value="">全局</option><option value="custom">自定义</option>
</select>
</div>
</div>
<div class="row g-1 align-items-end">
<div class="col-auto" style="width:28px"></div>
<div class="col"><label class="form-label">偏航角模式</label>
<select id="${prefix}_heading_mode_${idx}" class="form-select form-select-sm">
<option value="followWayline">followWayline 跟随航线</option>
<option value="manually">manually 手动</option>
<option value="fixed">fixed 固定</option>
<option value="smoothTransition">smoothTransition 平滑过渡</option>
<option value="towardPOI">towardPOI 朝向POI</option>
</select>
</div>
<div class="col"><label class="form-label">偏航转向路径</label>
<select id="${prefix}_heading_path_mode_${idx}" class="form-select form-select-sm">
<option value="followBadArc">followBadArc 最短弧</option>
<option value="clockwise">clockwise 顺时针</option>
<option value="counterClockwise">counterClockwise 逆时针</option>
</select>
</div>
<div class="col"><label class="form-label">转弯模式</label>
<select id="${prefix}_turn_mode_${idx}" class="form-select form-select-sm">
<option value="toPointAndStopWithDiscontinuityCurvature">到点停止(非连续)</option>
<option value="toPointAndStopWithContinuityCurvature">到点停止(连续)</option>
<option value="toPointAndPassWithContinuityCurvature">飞越(连续)</option>
<option value="coordinateTurn">协调转弯</option>
</select>
</div>
<div class="d-flex align-items-center justify-content-between mt-1">
<small class="text-muted">动作组 (action_group)</small>
<button class="btn btn-outline-success btn-sm py-0" onclick="addAction('${prefix}', ${idx})"><i class="bi bi-plus-circle me-1"></i>添加动作</button>
</div>
<div id="${prefix}_actions_${idx}" class="mt-1"></div>
</div>`;
}
// 自定义速时速度为必填:切换红色*标记,并清除可能的错误高亮
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]) => `<option value="${val}">${val} ${lab}</option>`).join('');
const html = `<div class="border rounded p-2 mb-1 bg-light" id="${prefix}_act_${wpIdx}_${aIdx}">
<div class="d-flex align-items-end gap-2">
<div class="flex-grow-1"><label class="form-label small mb-0">动作类型</label>
<select id="${prefix}_actfunc_${wpIdx}_${aIdx}" class="form-select form-select-sm" onchange="renderActionParams('${prefix}', ${wpIdx}, ${aIdx})">${opts}</select>
</div>
<button class="btn btn-outline-danger btn-sm" onclick="removeAction('${prefix}', ${wpIdx}, ${aIdx})"><i class="bi bi-trash"></i></button>
</div>
<div class="row g-1 mt-1" id="${prefix}_actparams_${wpIdx}_${aIdx}"></div>
</div>`;
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'
? `<select id="${id}" class="form-select form-select-sm">${f.o.map(o => `<option value="${o}"${o === f.d ? ' selected' : ''}>${o}</option>`).join('')}</select>`
: `<input id="${id}" class="form-control form-control-sm" type="${f.t}" value="${f.d}">`;
return `<div class="col-4"><label class="form-label small mb-0">${f.l}</label>${input}</div>`;
}).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 `<div class="row g-1 align-items-end mb-1" id="${prefix}_row_${idx}">
<div class="col-auto" style="width:28px"><span class="form-label text-center d-block">${idx}</span></div>
<div class="col"><label class="form-label">经度</label><input id="${prefix}_lng_${idx}" class="form-control form-control-sm" type="number" step="0.0000001" placeholder="114.000000"></div>
<div class="col"><label class="form-label">纬度</label><input id="${prefix}_lat_${idx}" class="form-control form-control-sm" type="number" step="0.0000001" placeholder="22.000000"></div>
<div class="col-3"><label class="form-label">备降高度(m)</label><input id="${prefix}_alt_${idx}" class="form-control form-control-sm" type="number" value="74"></div>
</div>`;
}
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;
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,14 +2517,31 @@ 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';
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;
@ -2388,6 +2551,7 @@ function confirmMapPoints() {
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);
});