From 18c6de38ca37ee611239b3f1906fc601fb431a94 Mon Sep 17 00:00:00 2001 From: "938693313@qq.com" <938693313@qq.com> Date: Thu, 4 Jun 2026 17:49:42 +0800 Subject: [PATCH] =?UTF-8?q?1.q20=E5=8E=BB=E9=99=A4=E5=BF=83=E8=B7=B3?= =?UTF-8?q?=E7=AD=89debug=E6=97=A5=E5=BF=97=202.q20=E4=BF=AE=E6=94=B9?= =?UTF-8?q?=E8=88=AA=E7=BA=BF=E6=8F=90=E4=BA=A4=E5=8F=82=E6=95=B0=203.?= =?UTF-8?q?=E5=85=B6=E4=BB=96Q20=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../handler/ServicesReplyHandler.java | 6 +- .../q20/controller/Q20RouteController.java | 30 +- .../business/q20/dto/Q20RouteUploadDTO.java | 3 +- .../q20/handler/Q20BatteryHandler.java | 27 +- .../business/q20/handler/Q20HmsHandler.java | 8 +- .../business/q20/handler/Q20MountHandler.java | 4 +- .../business/q20/handler/Q20ObsHandler.java | 4 +- .../business/q20/handler/Q20RcHandler.java | 4 +- .../business/q20/handler/Q20RtkHandler.java | 4 +- .../q20/handler/Q20StatusHandler.java | 4 +- .../business/q20/service/Q20RouteService.java | 13 + .../service/impl/Q20DeviceServiceImpl.java | 10 + .../q20/service/impl/Q20RouteServiceImpl.java | 60 ++ .../business/q20/vo/Q20DeviceStatusVO.java | 17 +- .../service/impl/DJIBaseServiceImpl.java | 3 +- admin/src/main/resources/static/q20-ctrl.html | 673 +++++++++++++++++- 16 files changed, 832 insertions(+), 38 deletions(-) diff --git a/admin/src/main/java/com/multictrl/modules/business/handler/ServicesReplyHandler.java b/admin/src/main/java/com/multictrl/modules/business/handler/ServicesReplyHandler.java index 76a61ee..b0aaa48 100644 --- a/admin/src/main/java/com/multictrl/modules/business/handler/ServicesReplyHandler.java +++ b/admin/src/main/java/com/multictrl/modules/business/handler/ServicesReplyHandler.java @@ -23,15 +23,15 @@ public class ServicesReplyHandler implements MessageHandler { log.debug("services reply --> topic: {}, payload: {}", topic, payload); JSONObject message = JsonUtils.parseObject(payload, JSONObject.class); if (message != null) { + // 回复仅 bid 与请求一致,tid 为设备新生成的值,不可参与关联匹配,故只按 bid 关联 String bid = message.getStr("bid"); - String tid = message.getStr("tid"); - if (StrUtil.isNotBlank(bid) && StrUtil.isNotBlank(tid)) { + if (StrUtil.isNotBlank(bid)) { JSONObject data = message.getJSONObject(BusinessConstant.DATA); if (data == null) { data = new JSONObject(); data.set("result", 0); } - CacheUtils.set(bid + "_" + tid, data); + CacheUtils.set(bid, data); } } else { log.debug("services reply --> payload解析失败,解析后为null"); 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 9399d75..fe532db 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 @@ -2,15 +2,20 @@ package com.multictrl.modules.business.q20.controller; import com.multictrl.common.annotation.ApiOrder; import com.multictrl.common.annotation.LogOperation; +import com.multictrl.common.page.PageData; import com.multictrl.common.utils.Result; +import com.multictrl.modules.business.dto.RouteDTO; import com.multictrl.modules.business.q20.dto.*; import com.multictrl.modules.business.q20.service.Q20RouteService; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.apache.shiro.authz.annotation.RequiresPermissions; import org.springframework.web.bind.annotation.*; +import java.util.Map; + /** * Q20航线任务接口 * topic: thing/device/{device_sn}/services (下行) @@ -28,6 +33,15 @@ public class Q20RouteController { private final Q20RouteService q20RouteService; + // ==================== 航线库分页查询 ==================== + + @GetMapping("/page") + @Operation(summary = "分页查询本地Q20航线(含航点),支持按航线名称/航线ID查询") + @RequiresPermissions("bus:q20:route") + public Result> page(@Parameter(hidden = true) @RequestParam Map params) { + return new Result>().ok(q20RouteService.pageQ20Routes(params)); + } + // ==================== 下一个航线ID ==================== @GetMapping("/nextWayline") @@ -82,14 +96,14 @@ public class Q20RouteController { // ==================== 获取航线信息(暂时停用,先注释) ==================== -// @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)); + } // ==================== 获取执行进度(暂时停用,先注释) ==================== diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20RouteUploadDTO.java b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20RouteUploadDTO.java index 132633c..f3b70ff 100644 --- a/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20RouteUploadDTO.java +++ b/admin/src/main/java/com/multictrl/modules/business/q20/dto/Q20RouteUploadDTO.java @@ -1,6 +1,5 @@ package com.multictrl.modules.business.q20.dto; -import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -18,7 +17,7 @@ import static com.fasterxml.jackson.annotation.JsonProperty.Access.WRITE_ONLY; public class Q20RouteUploadDTO { @Schema(description = "航线名称(仅本地存储用,不下发给设备)") - @JsonIgnore + @JsonProperty(value = "routeName", access = WRITE_ONLY) private String routeName; @Schema(description = "全局飞行高度(m),仅本地存储用,不下发给设备") diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/handler/Q20BatteryHandler.java b/admin/src/main/java/com/multictrl/modules/business/q20/handler/Q20BatteryHandler.java index 74a8de8..7b04340 100644 --- a/admin/src/main/java/com/multictrl/modules/business/q20/handler/Q20BatteryHandler.java +++ b/admin/src/main/java/com/multictrl/modules/business/q20/handler/Q20BatteryHandler.java @@ -11,6 +11,9 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; +import java.util.LinkedHashMap; +import java.util.Map; + /** * Q20电池数据处理(method: battery) * 上报智能电池详细状态:id, full_charge_capacity, charge_remaining, voltage, current, @@ -34,20 +37,32 @@ public class Q20BatteryHandler implements MessageHandler { String deviceSn = message.getStr(BusinessConstant.GATEWAY); JSONObject data = message.getJSONObject(BusinessConstant.DATA); if (data != null) { - // 以电池id区分多块电池,缓存key加上电池id后缀 + // 以电池id区分多块电池,按id聚合缓存到同一key下,便于在线查询时一次取出全部电池 Integer batteryId = data.getInt("id"); - String cacheKey = Q20Constant.Q20_BATTERY + deviceSn - + (batteryId != null ? "_" + batteryId : ""); - CacheUtils.set(cacheKey, data); + cacheBattery(deviceSn, batteryId, data); saveToInflux(deviceSn, batteryId, data); - log.debug("Q20 battery --> deviceSn: {}, id: {}, charge_remaining: {}%, status: {}", - deviceSn, batteryId, data.getFloat("charge_remaining"), data.getInt("status")); +// log.debug("Q20 battery --> deviceSn: {}, id: {}, charge_remaining: {}%, status: {}", +// deviceSn, batteryId, data.getFloat("charge_remaining"), data.getInt("status")); } } else { log.debug("Q20 battery --> payload解析失败,解析后为null"); } } + /** + * 将单块电池按id聚合到 {@code q20_battery_{sn}} 缓存项下(LinkedHashMap:id -> 电池数据) + */ + @SuppressWarnings("unchecked") + private void cacheBattery(String deviceSn, Integer batteryId, JSONObject data) { + String cacheKey = Q20Constant.Q20_BATTERY + deviceSn; + Object cached = CacheUtils.get(cacheKey); + Map batteries = cached instanceof Map + ? (Map) cached + : new LinkedHashMap<>(); + batteries.put(batteryId, data); + CacheUtils.set(cacheKey, batteries); + } + private void saveToInflux(String deviceSn, Integer batteryId, JSONObject data) { Q20BatteryReport report = Q20BatteryReport.builder() .deviceSn(deviceSn) diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/handler/Q20HmsHandler.java b/admin/src/main/java/com/multictrl/modules/business/q20/handler/Q20HmsHandler.java index 48785a7..5548680 100644 --- a/admin/src/main/java/com/multictrl/modules/business/q20/handler/Q20HmsHandler.java +++ b/admin/src/main/java/com/multictrl/modules/business/q20/handler/Q20HmsHandler.java @@ -39,7 +39,7 @@ public class Q20HmsHandler implements MessageHandler { data.set("result", 0); message.remove(BusinessConstant.NEED_REPLY); mqttPushService.pushMessageByClient2(topic + BusinessConstant._REPLY, message.toString()); - log.debug("Q20 hms --> 已回复, deviceSn: {}", deviceSn); +// log.debug("Q20 hms --> 已回复, deviceSn: {}", deviceSn); } // 重新解析,避免回复时修改了data对象 message = JsonUtils.parseObject(payload, JSONObject.class); @@ -49,9 +49,9 @@ public class Q20HmsHandler implements MessageHandler { data = message.getJSONObject(BusinessConstant.DATA); if (data != null) { CacheUtils.set(Q20Constant.Q20_HMS + deviceSn, data); - log.debug("Q20 hms --> deviceSn: {}, gnss: {}, ins: {}, battery: {}", - deviceSn, data.getJSONArray("gnss"), data.getJSONArray("ins"), - data.getJSONArray("battery")); +// log.debug("Q20 hms --> deviceSn: {}, gnss: {}, ins: {}, battery: {}", +// deviceSn, data.getJSONArray("gnss"), data.getJSONArray("ins"), +// data.getJSONArray("battery")); } } else { log.debug("Q20 hms --> payload解析失败,解析后为null"); diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/handler/Q20MountHandler.java b/admin/src/main/java/com/multictrl/modules/business/q20/handler/Q20MountHandler.java index 425ad55..6450ded 100644 --- a/admin/src/main/java/com/multictrl/modules/business/q20/handler/Q20MountHandler.java +++ b/admin/src/main/java/com/multictrl/modules/business/q20/handler/Q20MountHandler.java @@ -36,8 +36,8 @@ public class Q20MountHandler implements MessageHandler { if (data != null) { CacheUtils.set(Q20Constant.Q20_MOUNT + deviceSn, data); saveToInflux(deviceSn, data); - log.debug("Q20 mount --> deviceSn: {}, mount_type: {}, mount_port: {}", - deviceSn, data.getStr("mount_type"), data.getInt("mount_port")); +// log.debug("Q20 mount --> deviceSn: {}, mount_type: {}, mount_port: {}", +// deviceSn, data.getStr("mount_type"), data.getInt("mount_port")); } } else { log.debug("Q20 mount --> payload解析失败,解析后为null"); diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/handler/Q20ObsHandler.java b/admin/src/main/java/com/multictrl/modules/business/q20/handler/Q20ObsHandler.java index 305f891..a1556a6 100644 --- a/admin/src/main/java/com/multictrl/modules/business/q20/handler/Q20ObsHandler.java +++ b/admin/src/main/java/com/multictrl/modules/business/q20/handler/Q20ObsHandler.java @@ -37,8 +37,8 @@ public class Q20ObsHandler implements MessageHandler { if (data != null) { CacheUtils.set(Q20Constant.Q20_OBS + deviceSn, data); saveToInflux(deviceSn, data); - log.debug("Q20 obs --> deviceSn: {}, cp_distance: {}m, cp_enable: {}", - deviceSn, data.getFloat("cp_distance"), data.getInt("cp_enable")); +// log.debug("Q20 obs --> deviceSn: {}, cp_distance: {}m, cp_enable: {}", +// deviceSn, data.getFloat("cp_distance"), data.getInt("cp_enable")); } } else { log.debug("Q20 obs --> payload解析失败,解析后为null"); diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/handler/Q20RcHandler.java b/admin/src/main/java/com/multictrl/modules/business/q20/handler/Q20RcHandler.java index 72b8b10..a9b6f8a 100644 --- a/admin/src/main/java/com/multictrl/modules/business/q20/handler/Q20RcHandler.java +++ b/admin/src/main/java/com/multictrl/modules/business/q20/handler/Q20RcHandler.java @@ -36,8 +36,8 @@ public class Q20RcHandler implements MessageHandler { if (data != null) { CacheUtils.set(Q20Constant.Q20_RC + deviceSn, data); saveToInflux(deviceSn, data); - log.debug("Q20 rc --> deviceSn: {}, priority: {}, rc_state: {}", - deviceSn, data.getInt("priority"), data.getStr("rc_state")); +// log.debug("Q20 rc --> deviceSn: {}, priority: {}, rc_state: {}", +// deviceSn, data.getInt("priority"), data.getStr("rc_state")); } } else { log.debug("Q20 rc --> payload解析失败,解析后为null"); diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/handler/Q20RtkHandler.java b/admin/src/main/java/com/multictrl/modules/business/q20/handler/Q20RtkHandler.java index 83aa2e9..e716c93 100644 --- a/admin/src/main/java/com/multictrl/modules/business/q20/handler/Q20RtkHandler.java +++ b/admin/src/main/java/com/multictrl/modules/business/q20/handler/Q20RtkHandler.java @@ -35,8 +35,8 @@ public class Q20RtkHandler implements MessageHandler { if (data != null) { CacheUtils.set(Q20Constant.Q20_RTK + deviceSn, data); saveToInflux(deviceSn, data); - log.debug("Q20 rtk --> deviceSn: {}, fix_type: {}, satellites: {}", - deviceSn, data.getInt("fix_type"), data.getInt("satellite_visible")); +// log.debug("Q20 rtk --> deviceSn: {}, fix_type: {}, satellites: {}", +// deviceSn, data.getInt("fix_type"), data.getInt("satellite_visible")); } } else { log.debug("Q20 rtk --> payload解析失败,解析后为null"); diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/handler/Q20StatusHandler.java b/admin/src/main/java/com/multictrl/modules/business/q20/handler/Q20StatusHandler.java index a8689bc..72add74 100644 --- a/admin/src/main/java/com/multictrl/modules/business/q20/handler/Q20StatusHandler.java +++ b/admin/src/main/java/com/multictrl/modules/business/q20/handler/Q20StatusHandler.java @@ -69,7 +69,7 @@ public class Q20StatusHandler implements MessageHandler { */ private void handleHeartbeat(String topic, JSONObject message, String deviceSn) { CacheUtils.set(Q20Constant.Q20_ONLINE + deviceSn, true, Q20Constant.ONLINE_TTL_MS); - log.debug("Q20 status --> 心跳, deviceSn: {}", deviceSn); +// log.debug("Q20 status --> 心跳, deviceSn: {}", deviceSn); Integer needReply = message.getInt(BusinessConstant.NEED_REPLY); if (needReply != null && needReply == 1) { long uplink = 0; @@ -92,6 +92,6 @@ public class Q20StatusHandler implements MessageHandler { message.remove(BusinessConstant.NEED_REPLY); String replyTopic = topic + Q20Constant.STATUS_REPLY_SUFFIX; mqttPushService.pushMessageByClient1(replyTopic, message.toString()); - log.debug("Q20 status --> 已回复, topic: {}", replyTopic); +// log.debug("Q20 status --> 已回复, topic: {}", replyTopic); } } diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/service/Q20RouteService.java b/admin/src/main/java/com/multictrl/modules/business/q20/service/Q20RouteService.java index d3a9793..5d09777 100644 --- a/admin/src/main/java/com/multictrl/modules/business/q20/service/Q20RouteService.java +++ b/admin/src/main/java/com/multictrl/modules/business/q20/service/Q20RouteService.java @@ -1,8 +1,12 @@ package com.multictrl.modules.business.q20.service; import cn.hutool.json.JSONObject; +import com.multictrl.common.page.PageData; +import com.multictrl.modules.business.dto.RouteDTO; import com.multictrl.modules.business.q20.dto.*; +import java.util.Map; + /** * Q20航线任务服务接口 * topic: thing/device/{device_sn}/services (下行) @@ -13,6 +17,15 @@ import com.multictrl.modules.business.q20.dto.*; */ public interface Q20RouteService { + /** + * 分页查询本地已保存的Q20航线(bus_route,关联bus_route_waypoint航点) + * 支持按航线名称(routeName)、航线ID(q20RouteId)模糊查询 + * + * @param params 查询参数:page、limit、routeName(可选)、q20RouteId(可选) + * @return 分页结果,每条含完整航点列表 + */ + PageData pageQ20Routes(Map params); + /** * 获取下一个航线ID(根据库中最新Q20航线ID自动递增,无记录则返回默认ID) * diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/service/impl/Q20DeviceServiceImpl.java b/admin/src/main/java/com/multictrl/modules/business/q20/service/impl/Q20DeviceServiceImpl.java index 74d8a85..3679010 100644 --- a/admin/src/main/java/com/multictrl/modules/business/q20/service/impl/Q20DeviceServiceImpl.java +++ b/admin/src/main/java/com/multictrl/modules/business/q20/service/impl/Q20DeviceServiceImpl.java @@ -8,8 +8,10 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; +import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; /** @@ -30,6 +32,14 @@ public class Q20DeviceServiceImpl implements Q20DeviceService { vo.setOnline(online); if (online) { vo.setOsd(CacheUtils.get(Q20Constant.Q20_OSD + deviceSn)); + vo.setRtk(CacheUtils.get(Q20Constant.Q20_RTK + deviceSn)); + vo.setRc(CacheUtils.get(Q20Constant.Q20_RC + deviceSn)); + vo.setObs(CacheUtils.get(Q20Constant.Q20_OBS + deviceSn)); + vo.setMount(CacheUtils.get(Q20Constant.Q20_MOUNT + deviceSn)); + Object batteries = CacheUtils.get(Q20Constant.Q20_BATTERY + deviceSn); + if (batteries instanceof Map) { + vo.setBatteries(new ArrayList<>(((Map) batteries).values())); + } } return vo; } 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 a181c8b..ed17eee 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 @@ -6,11 +6,18 @@ import cn.hutool.core.util.StrUtil; import cn.hutool.json.JSONObject; import cn.hutool.json.JSONUtil; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.fasterxml.jackson.databind.ObjectMapper; import com.multictrl.common.exception.RenException; +import com.multictrl.common.page.PageData; +import com.multictrl.common.utils.ConvertUtils; import com.multictrl.modules.business.dao.RouteDao; import com.multictrl.modules.business.dao.RouteWaypointDao; import com.multictrl.modules.business.dao.WaypointActionDao; +import com.multictrl.modules.business.dto.RouteDTO; +import com.multictrl.modules.business.dto.RouteWaypointDTO; +import com.multictrl.modules.business.dto.WaypointActionDTO; import com.multictrl.modules.business.entity.RouteEntity; import com.multictrl.modules.business.entity.RouteWaypointEntity; import com.multictrl.modules.business.entity.WaypointActionEntity; @@ -25,7 +32,9 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.math.BigInteger; +import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -113,6 +122,57 @@ public class Q20RouteServiceImpl implements Q20RouteService { // 接口实现 // ──────────────────────────────────────────────── + @Override + public PageData pageQ20Routes(Map params) { + long curPage = parseLong(params.get("page"), 1); + long limit = parseLong(params.get("limit"), 10); + String routeName = (String) params.get("routeName"); + String q20RouteId = (String) params.get("q20RouteId"); + + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.eq("template_type", TEMPLATE_TYPE_Q20); + wrapper.like(StrUtil.isNotBlank(routeName), "route_name", routeName); + wrapper.like(StrUtil.isNotBlank(q20RouteId), "q20_route_id", q20RouteId); + wrapper.orderByDesc("update_date", "create_date"); + + IPage result = routeDao.selectPage(new Page<>(curPage, limit), wrapper); + + List list = new ArrayList<>(); + for (RouteEntity entity : result.getRecords()) { + RouteDTO dto = ConvertUtils.sourceToTarget(entity, RouteDTO.class); + // 关联航点(bus_route_waypoint),按航点顺序升序 + List waypoints = routeWaypointDao.selectList( + new QueryWrapper() + .eq("route_id", entity.getId()) + .orderByAsc("waypoint_sort")); + List waypointDtos = ConvertUtils.sourceToTarget(waypoints, RouteWaypointDTO.class); + // 关联每个航点的动作(bus_waypoint_action),按动作顺序升序 + for (RouteWaypointDTO wp : waypointDtos) { + List actions = waypointActionDao.selectList( + new QueryWrapper() + .eq("route_id", entity.getId()) + .eq("waypoint_id", wp.getId()) + .orderByAsc("action_sort")); + wp.setWaypointActionList(ConvertUtils.sourceToTarget(actions, WaypointActionDTO.class)); + } + dto.setRouteWaypointList(waypointDtos); + list.add(dto); + } + return new PageData<>(list, result.getTotal()); + } + + /** 解析分页参数(字符串/数字均兼容),无效时返回默认值 */ + private long parseLong(Object val, long defaultVal) { + if (val == null) { + return defaultVal; + } + try { + return Long.parseLong(val.toString()); + } catch (NumberFormatException e) { + return defaultVal; + } + } + @Override public String nextWayline(String type) { // 按类型区分前缀:一键航线用 q20_auto_,上传航线用 q20_route_ diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/vo/Q20DeviceStatusVO.java b/admin/src/main/java/com/multictrl/modules/business/q20/vo/Q20DeviceStatusVO.java index ffb10f2..3199f7b 100644 --- a/admin/src/main/java/com/multictrl/modules/business/q20/vo/Q20DeviceStatusVO.java +++ b/admin/src/main/java/com/multictrl/modules/business/q20/vo/Q20DeviceStatusVO.java @@ -19,6 +19,21 @@ public class Q20DeviceStatusVO { @Schema(description = "是否在线(35s内有心跳则为true)") private boolean online; - @Schema(description = "最新OSD遥测数据,仅在线时有值") + @Schema(description = "最新OSD基础遥测数据(method=osd),仅在线时有值") private Object osd; + + @Schema(description = "最新RTK定位数据(method=rtk),仅在线时有值") + private Object rtk; + + @Schema(description = "最新遥控链路数据(method=rc),仅在线时有值") + private Object rc; + + @Schema(description = "最新避障数据(method=obs),仅在线时有值") + private Object obs; + + @Schema(description = "最新负载数据(method=mount),仅在线时有值") + private Object mount; + + @Schema(description = "最新电池数据列表(method=battery,按电池id聚合),仅在线时有值") + private Object batteries; } diff --git a/admin/src/main/java/com/multictrl/modules/business/service/impl/DJIBaseServiceImpl.java b/admin/src/main/java/com/multictrl/modules/business/service/impl/DJIBaseServiceImpl.java index 580d46e..da138c6 100644 --- a/admin/src/main/java/com/multictrl/modules/business/service/impl/DJIBaseServiceImpl.java +++ b/admin/src/main/java/com/multictrl/modules/business/service/impl/DJIBaseServiceImpl.java @@ -50,7 +50,8 @@ public class DJIBaseServiceImpl implements DJIBaseService { mqttPushService.pushMessageByClient1(topic, payload.toString()); String bid = payload.getStr("bid"); String tid = payload.getStr("tid"); - String cacheKey = bid + "_" + tid; + // 设备回复只回传相同的 bid(tid 会重新生成),因此以 bid 作为关联回复的缓存键 + String cacheKey = bid; // 记录当前等待回复的命令,供前端模拟回复使用 String deviceSn = extractDeviceSn(topic); diff --git a/admin/src/main/resources/static/q20-ctrl.html b/admin/src/main/resources/static/q20-ctrl.html index 355b0f1..e124711 100644 --- a/admin/src/main/resources/static/q20-ctrl.html +++ b/admin/src/main/resources/static/q20-ctrl.html @@ -7,6 +7,7 @@ + +
-
+
Q20 Control @@ -120,6 +138,35 @@ h6.cmd-title { font-size: 12px; color: #e6edf3; font-weight: 600; margin-bottom:
+ +
+ 实时 +
飞行状态
+
+
经度
+
纬度
+
相对高(m)
+
海拔(m)
+
+
水平速(m/s)
+
垂直速(m/s)
+
航向(°)
+
俯仰(°)
+
横滚(°)
+
+
避障
+
+
电池1 (%/V)
+
电池2 (%/V)
+
+ RTK +
经度
+
纬度
+
海拔(m)
+
椭球高(m)
+
+
+
字段标记:* 必填项 选填 非必填项(依据 doc 协议文档)
@@ -781,7 +829,76 @@ h6.cmd-title { font-size: 12px; color: #e6edf3; font-weight: 600; margin-bottom:
+ + +
+
+ 飞机实时遥测 + MQTT 实时订阅,自动连接并断线重连 +
+ 未连接 + + +
+
+
+
+ + +
+
飞机实时 OSD
+
+
+
暂无数据,等待实时遥测…
+
+
+ + +
+
飞机实时状态 state
+
+
+
暂无数据,等待实时遥测…
+
+
+ + +
+
RTK 定位 (rtk)
+
+
+
暂无 RTK 数据
+
+
+ + +
+
遥控链路 (rc)
+
+
+
暂无遥控链路数据
+
+
+ + +
+
避障 (obs)
+
+
+
暂无避障数据
+
+
+ + +
+
电池 (battery)
+
+
+
暂无电池数据
+
+
+
查询飞行状态
@@ -1233,6 +1350,55 @@ h6.cmd-title { font-size: 12px; color: #e6edf3; font-weight: 600; margin-bottom:
+ +
+
+
Q20 航线库(本地已保存航线,含航点)
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + + + + + + + + + + +
序号航线名称航线ID机库SN航点数速度(m/s)高度(m)返航高(m)结束动作更新时间航点
暂无数据,点击「查询」加载
+
+ +
+ +
+ 每页 + +
+
+
+ +
+
+
+
@@ -1362,6 +1528,323 @@ async function request(method, url, body, respId) { } } +// ==================== 飞机实时遥测 (OSD / state) ==================== +const _fmt = (n, digits) => { + if (n === null || n === undefined || n === '') return '—'; + const x = Number(n); + if (isNaN(x)) return String(n); + return digits != null ? x.toFixed(digits) : String(x); +}; +// 0/1 状态徽标 +const _stBadge = (val, onText, offText) => { + const on = Number(val) === 1; + return '' + (on ? (onText || '开') : (offText || '关')) + ''; +}; +const _arr = a => Array.isArray(a) ? '[' + a.join(', ') + ']' : (a ?? '—'); +const _kv = (k, val) => '
' + k + '
' + val + '
'; + +async function refreshOnlineStatus() { + let s; + try { s = sn(); } catch { return; } + const badge = document.getElementById('osd_online_badge'); + try { + const res = await fetch(API_BASE + '/business/q20/online/' + s, { headers: headers() }); + if (res.status === 401) { doLogout(); return; } + const json = await res.json(); + const d = (json && json.data) || {}; + const online = !!d.online; + badge.textContent = online ? '在线' : '离线'; + badge.className = 'st-badge ' + (online ? 'st-on' : 'st-off'); + document.getElementById('osd_update_time').textContent = '更新于 ' + new Date().toLocaleTimeString(); + renderOsd(d.osd); + renderRtk(d.rtk); + renderRc(d.rc); + renderObs(d.obs); + renderBatteries(d.batteries); + // 同步顶部 HUD + liveOsd = d.osd || null; liveRtk = d.rtk || null; liveObs = d.obs || null; + liveBatteries = {}; + (Array.isArray(d.batteries) ? d.batteries : []).forEach(b => { if (b && b.id != null) liveBatteries[b.id] = b; }); + renderHud(); + } catch (e) { + badge.textContent = '请求失败'; + badge.className = 'st-badge st-warn'; + document.getElementById('osd_update_time').textContent = ''; + ['osd_grid','state_grid','rtk_grid','rc_grid','obs_grid'].forEach(id => document.getElementById(id).innerHTML = ''); + document.getElementById('bat_wrap').innerHTML = ''; + ['osd_empty','state_empty','rtk_empty','rc_empty','obs_empty','bat_empty'].forEach(id => { + const el = document.getElementById(id); el.style.display = ''; el.textContent = '请求失败:' + e.message; + }); + } +} + +function renderOsd(osd) { + const og = document.getElementById('osd_grid'), sg = document.getElementById('state_grid'); + const oe = document.getElementById('osd_empty'), se = document.getElementById('state_empty'); + if (!osd) { + og.innerHTML = ''; sg.innerHTML = ''; + oe.style.display = ''; oe.textContent = '暂无 OSD 数据(飞机离线或未上报遥测)'; + se.style.display = ''; se.textContent = '暂无状态数据'; + return; + } + oe.style.display = 'none'; + const flying = Number(osd.flying) === 1; + og.innerHTML = + _kv('飞行模式 mode', osd.mode ?? '—') + + _kv('飞行状态 flying', flying ? '飞行中' : '地面/待机') + + _kv('经度 longitude', _fmt(osd.longitude, 7)) + + _kv('纬度 latitude', _fmt(osd.latitude, 7)) + + _kv('相对高度 height (m)', _fmt(osd.height, 2)) + + _kv('海拔 altitude (m)', _fmt(osd.altitude, 2)) + + _kv('水平速度 gs (m/s)', _fmt(osd.gs, 2)) + + _kv('垂直速度 vs (m/s)', _fmt(osd.vs, 2)) + + _kv('航向 heading (°)', _fmt(osd.heading, 1)) + + _kv('俯仰 pitch (°)', _fmt(osd.pitch, 1)) + + _kv('横滚 roll (°)', _fmt(osd.roll, 1)) + + _kv('电量 battery (%)', _fmt(osd.battery)) + + _kv('电压 voltage (V)', _fmt(osd.voltage)) + + _kv('飞行时长 fly_time (s)', _fmt(osd.fly_time)) + + _kv('飞行距离 fly_distance (m)', _fmt(osd.fly_distance, 1)) + + _kv('返航点 home_set', _stBadge(osd.home_set, '已设', '未设')) + + _kv('距返航点 home_distance (m)', _fmt(osd.home_distance, 1)); + + const st = osd.state; + if (!st) { sg.innerHTML = ''; se.style.display = ''; se.textContent = '暂无 state 数据'; return; } + se.style.display = 'none'; + sg.innerHTML = + _kv('负载状态 payload_state', _arr(st.payload_state)) + + _kv('电池状态 battery_state', _arr(st.battery_state)) + + _kv('安全开关 safe_enabled', _stBadge(st.safe_enabled)) + + _kv('RTK启用 rtk_enabled', _stBadge(st.rtk_enabled)) + + _kv('RTK连接 rtk_connected', _stBadge(st.rtk_connected, '已连接', '未连接')) + + _kv('遥控连接 rc_connected', _stBadge(st.rc_connected, '已连接', '未连接')) + + _kv('避障 obs_enabled', _stBadge(st.obs_enabled)) + + _kv('加速度校准 accel_cal', _stBadge(st.accel_cal, '已校准', '未校准')) + + _kv('陀螺仪校准 gyro_cal', _stBadge(st.gyro_cal, '已校准', '未校准')) + + _kv('水平校准 hor_cal', _stBadge(st.hor_cal, '已校准', '未校准')) + + _kv('磁罗盘校准 mag_cal', _stBadge(st.mag_cal, '已校准', '未校准')) + + _kv('FPV直播 fpv_live', _stBadge(st.fpv_live, '推流中', '未推流')) + + _kv('图传直播 stream_live', _stBadge(st.stream_live, '推流中', '未推流')); +} + +// 通用:渲染一个键值网格,data 为空时显示占位 +function _renderKv(gridId, emptyId, data, rowsFn, emptyText) { + const g = document.getElementById(gridId), e = document.getElementById(emptyId); + if (!data) { g.innerHTML = ''; e.style.display = ''; e.textContent = emptyText; return; } + e.style.display = 'none'; + g.innerHTML = rowsFn(data); +} + +// RTK 定位 (method=rtk) +const RTK_FIX = { 0:'0 无定位', 1:'1 单点', 2:'2 差分', 4:'4 RTK固定', 5:'5 RTK浮动', 6:'6 估算' }; +function renderRtk(rtk) { + _renderKv('rtk_grid', 'rtk_empty', rtk, d => + _kv('定位类型 fix_type', RTK_FIX[d.fix_type] ?? _fmt(d.fix_type)) + + _kv('可见卫星 satellite_visible', _fmt(d.satellite_visible)) + + _kv('经度 longitude', _fmt(d.longitude, 7)) + + _kv('纬度 latitude', _fmt(d.latitude, 7)) + + _kv('海拔 altitude (m)', _fmt(d.altitude, 2)) + + _kv('椭球高 ellipsoid_height (m)', _fmt(d.ellipsoid_height, 2)) + + _kv('水平精度 eph', _fmt(d.eph, 2)) + + _kv('垂直精度 epv', _fmt(d.epv, 2)) + + _kv('速度 velocity (m/s)', _fmt(d.velocity, 2)) + + _kv('航向 yaw (°)', _fmt(d.yaw, 2)) + + _kv('航迹角 cog (°)', _fmt(d.cog, 2)) + + _kv('水平精度因子 horizontal_acc', _fmt(d.horizontal_acc, 2)) + + _kv('垂直精度因子 vertical_acc', _fmt(d.vertical_acc, 2)) + + _kv('速度精度 velocity_acc', _fmt(d.velocity_acc, 2)), + '暂无 RTK 数据'); +} + +// 遥控链路 (method=rc) +function renderRc(rc) { + _renderKv('rc_grid', 'rc_empty', rc, d => { + let rows = + _kv('优先级 priority', _fmt(d.priority)) + + _kv('遥控状态 rc_state', d.rc_state ?? '—') + + _kv('通道状态 rc_channel_state', d.rc_channel_state ?? '—') + + _kv('上行丢包率 up_loss_rate', _fmt(d.up_loss_rate, 2)) + + _kv('下行丢包率 down_loss_rate', _fmt(d.down_loss_rate, 2)); + if (d.network) { + rows += _kv('网络类型 network.type', _fmt(d.network.type)) + + _kv('网络质量 network.quality', _fmt(d.network.quality)); + } + if (Array.isArray(d.link)) { + d.link.forEach((lk, i) => { + const inuse = Number(lk.inuse) === 1; + rows += _kv('链路' + (lk.id ?? i) + (inuse ? ' (使用中)' : ''), + 'type=' + _fmt(lk.type) + ' conn=' + _fmt(lk.connected) + + ' sqe=' + _fmt(lk.sqe) + ' band=' + _fmt(lk.band)); + }); + } + return rows; + }, '暂无遥控链路数据'); +} + +// 避障 (method=obs),距离 -1 表示无障碍物 +const _obsDist = x => (Number(x) === -1 ? '' : _fmt(x, 2) + ' m'); +const OBS_DIR = ['前','右前','右','右后','后','左后','左','左前']; +function renderObs(obs) { + _renderKv('obs_grid', 'obs_empty', obs, d => { + let rows = + _kv('避障开关 cp_enable', _stBadge(d.cp_enable)) + + _kv('避障距离 cp_distance (m)', _fmt(d.cp_distance, 2)); + if (Array.isArray(d.ver_distances)) { + rows += _kv('下方障碍 ver[下]', _obsDist(d.ver_distances[0])) + + _kv('上方障碍 ver[上]', _obsDist(d.ver_distances[1])); + } + if (Array.isArray(d.around_distances)) { + d.around_distances.forEach((x, i) => { + rows += _kv('水平 ' + (OBS_DIR[i] || ('方位' + i)), _obsDist(x)); + }); + } + return rows; + }, '暂无避障数据'); +} + +// 电池 (method=battery),可能多块 +const BAT_STATUS = { 0:'0 异常', 1:'1 开机', 2:'2 充电中', 3:'3 需保养' }; +function renderBatteries(list) { + const wrap = document.getElementById('bat_wrap'), empty = document.getElementById('bat_empty'); + if (!Array.isArray(list) || list.length === 0) { + wrap.innerHTML = ''; empty.style.display = ''; empty.textContent = '暂无电池数据'; return; + } + empty.style.display = 'none'; + wrap.innerHTML = list.map(b => { + const title = '电池 #' + (b.id ?? '—') + (b.model ? ' · ' + b.model : ''); + const grid = + _kv('剩余电量 charge_remaining (%)', _fmt(b.charge_remaining)) + + _kv('状态 status', BAT_STATUS[b.status] ?? _fmt(b.status)) + + _kv('电压 voltage (V)', _fmt(b.voltage, 2)) + + _kv('电流 current (A)', _fmt(b.current, 2)) + + _kv('温度 temperature (℃)', _fmt(b.temperature, 1)) + + _kv('健康度 health (%)', _fmt(b.health)) + + _kv('循环次数 cycle_index', _fmt(b.cycle_index)) + + _kv('剩余飞行时间 time_remaining (s)', _fmt(b.time_remaining)) + + _kv('满充容量 full_charge_capacity', _fmt(b.full_charge_capacity)) + + _kv('低压告警 low_volt_warn_value', _fmt(b.low_volt_warn_value)) + + _kv('UID', b.uid ?? '—') + + _kv('固件版本 version', b.version ?? '—'); + return '
' + title + + '
' + grid + '
'; + }).join('
'); +} + +// ==================== MQTT 实时订阅 OSD 遥测(自动连接 + 断线重连) ==================== +const MQTT_URL = 'ws://223.108.157.174:61628/mqtt'; +const MQTT_USER = 'dock'; +const MQTT_PASS = 'Dock@2023'; + +let mqttClient = null; +let mqttSubTopic = null; +let liveBatteries = {}; // 客户端按电池id累积(MQTT每次只推一块电池) +let liveOsd = null, liveRtk = null, liveObs = null; // 最新各遥测,供顶部 HUD 汇总 + +// 顶部 HUD:汇总两块电池电量/电压、飞行/位置/速度/姿态、避障、RTK 定位,实时刷新 +function renderHud() { + const set = (id, val) => { const el = document.getElementById(id); if (el) el.innerHTML = val; }; + const o = liveOsd; + if (o) { + const flying = Number(o.flying) === 1; + set('hud_flying', flying + ? '飞行中' + : '地面/待机'); + set('hud_lng', _fmt(o.longitude, 7)); + set('hud_lat', _fmt(o.latitude, 7)); + set('hud_height', _fmt(o.height, 1)); + set('hud_alt', _fmt(o.altitude, 1)); + set('hud_gs', _fmt(o.gs, 1)); + set('hud_vs', _fmt(o.vs, 1)); + set('hud_heading', _fmt(o.heading, 1)); + set('hud_pitch', _fmt(o.pitch, 1)); + set('hud_roll', _fmt(o.roll, 1)); + } + // 避障:优先用 obs 报文(开关+距离),否则回退 osd.state.obs_enabled + if (liveObs) { + const en = Number(liveObs.cp_enable) === 1; + set('hud_obs', (en ? '' : '') + + ' ' + _fmt(liveObs.cp_distance, 1) + 'm'); + } else if (o && o.state) { + set('hud_obs', Number(o.state.obs_enabled) === 1 + ? '' : ''); + } + // 两块电池(按 id 升序取前两块):剩余电量% / 电压V + const bats = Object.values(liveBatteries).sort((a, b) => (a.id || 0) - (b.id || 0)); + const fmtBat = b => b ? (_fmt(b.charge_remaining) + '% / ' + _fmt(b.voltage, 1) + 'V') : '—'; + set('hud_bat1', fmtBat(bats[0])); + set('hud_bat2', fmtBat(bats[1])); + // RTK 定位 + const r = liveRtk; + if (r) { + set('hud_rtk_lng', _fmt(r.longitude, 7)); + set('hud_rtk_lat', _fmt(r.latitude, 7)); + set('hud_rtk_alt', _fmt(r.altitude, 2)); + set('hud_rtk_ell', _fmt(r.ellipsoid_height, 2)); + } +} + +function setOsdBadge(text, cls) { + const b = document.getElementById('osd_online_badge'); + if (!b) return; + b.textContent = text; + b.className = 'st-badge ' + cls; +} +function stampUpdate() { + document.getElementById('osd_update_time').textContent = '更新于 ' + new Date().toLocaleTimeString(); +} + +// 自动连接 MQTT(断线由 mqtt.js 按 reconnectPeriod 自动重连);已连接则确保订阅当前 SN +function mqttAutoConnect() { + if (typeof mqtt === 'undefined') { setOsdBadge('MQTT库未加载', 'st-warn'); return; } + if (mqttClient) { subscribeCurrentSn(); return; } + setOsdBadge('连接中…', 'st-off'); + liveBatteries = {}; + mqttClient = mqtt.connect(MQTT_URL, { + username: MQTT_USER, password: MQTT_PASS, + clientId: 'q20web_' + Math.random().toString(16).slice(2, 10), + clean: true, keepalive: 30, connectTimeout: 8000, reconnectPeriod: 4000, + }); + mqttClient.on('connect', () => { setOsdBadge('已连接', 'st-on'); subscribeCurrentSn(); }); + mqttClient.on('message', onMqttMessage); + mqttClient.on('reconnect', () => setOsdBadge('重连中…', 'st-off')); + mqttClient.on('error', () => setOsdBadge('连接错误', 'st-warn')); + mqttClient.on('close', () => setOsdBadge('已断开(重连中)', 'st-off')); +} + +// 订阅当前顶栏 SN 的 osd 主题;SN 变化时自动切换订阅 +function subscribeCurrentSn() { + if (!mqttClient || !mqttClient.connected) return; + const s = v('deviceSn').trim(); + if (!s) return; + const topic = 'thing/device/' + s + '/osd'; + if (topic === mqttSubTopic) return; + if (mqttSubTopic) mqttClient.unsubscribe(mqttSubTopic); + liveBatteries = {}; liveOsd = liveRtk = liveObs = null; renderHud(); + mqttSubTopic = topic; + mqttClient.subscribe(topic, { qos: 0 }, err => { if (err) setOsdBadge('订阅失败', 'st-warn'); }); +} + +function onMqttMessage(topic, payload) { + let msg; + try { msg = JSON.parse(payload.toString()); } catch { return; } + const method = msg && msg.method; + const data = msg && msg.data; + if (!method || !data) return; + switch (method) { + case 'osd': liveOsd = data; renderOsd(data); setOsdBadge('在线', 'st-on'); break; + case 'rtk': liveRtk = data; renderRtk(data); break; + case 'rc': renderRc(data); break; + case 'obs': liveObs = data; renderObs(data); break; + case 'battery': + if (data.id != null) liveBatteries[data.id] = data; + renderBatteries(Object.values(liveBatteries)); + break; + default: return; + } + renderHud(); + stampUpdate(); +} + // ==================== 飞机模拟回复 ==================== function toggleAircraftPanel() { const panel = document.getElementById('aircraft-mock-panel'); @@ -1420,6 +1903,42 @@ function amSendOsd() { amPublish('osd', { method: 'osd', data }, 'am_osd_resp'); } +// 模拟上报其余 OSD 子遥测(rtk / rc / obs / battery),默认值取自协议示例 +function amSendRtk() { + amPublish('osd', { method: 'rtk', source: 'eth', data: { + fix_type: 4, longitude: parseFloat(v('am_lng'))||113.1234567, latitude: parseFloat(v('am_lat'))||23.1234567, + altitude: parseFloat(v('am_height'))||100, ellipsoid_height: parseFloat(v('am_height'))||100, + satellite_visible: 18, eph: 0.8, epv: 1.2, velocity: 0, yaw: parseFloat(v('am_heading'))||0, + cog: 0, horizontal_acc: 0.5, vertical_acc: 0.8, velocity_acc: 0.1, hdg_acc: 0 + } }, 'am_osd_resp'); +} + +function amSendRc() { + amPublish('osd', { method: 'rc', source: 'eth', data: { + priority: 0, rc_channel_state: 'normal', rc_state: 'connected', + up_loss_rate: 0, down_loss_rate: 0, + link: [ { id: 0, type: 0, connected: 1, inuse: 1, sqe: 100, band: 1 } ], + network: { type: 1, quality: 5 } + } }, 'am_osd_resp'); +} + +function amSendObs() { + amPublish('osd', { method: 'obs', source: 'eth', data: { + cp_distance: 5, cp_enable: 1, ver_distances: [40, -1], + around_distances: [40, -1, 40, -1, 3.06, -1, 40, -1] + } }, 'am_osd_resp'); +} + +function amSendBattery() { + amPublish('osd', { method: 'battery', source: 'eth', data: { + id: 20, full_charge_capacity: -1, charge_remaining: parseInt(v('am_battery'))||97, + voltage: 49.75, current: 0, low_volt_warn_value: -1, status: 1, temperature: 31, + cycle_index: 26, active: -1, using_capacity: -1, time_flying: -1, health: 94, + time_remaining: 4200, charge_time_remaining: -1, flags: -1, + uid: 'ZABN2410240124', version: 'd1.1.60', model: 'EQ20-N' + } }, 'am_osd_resp'); +} + function amSendLiftoff() { const type = document.getElementById('am_liftoff_type').value; amPublish('events', { method: 'liftoff', data: { type, wayline: '', code: '' }, need_reply: 0 }, 'am_events_resp'); @@ -1519,7 +2038,7 @@ const METHOD_TEMPLATES = { query_status: { result: 0, output: { status: 0, message: "" }, fly_state: "hover", fly_mode: "auto", battery: 80 }, get_config: { result: 0, output: { status: 0, message: "" }, config: {} }, stereo_image: { result: 0, output: { status: 0, message: "" } }, - version: { result: 0, output: { status: 0, message: "" }, version: "2.0.0", sn: "", firmware: "1.0.0" }, + version: { result: 0, offboard_version: "6.1.23", firmware_version: "V1.22.1", protocol_version: "2.0.6", vision_version: "1.8.3", vehicle_type: "Q20N", output: { status: 0, message: "" } }, // ── 日志 ── log_count: { result: 0, output: { status: 0, message: "" }, count: 10 }, log_list: { result: 0, output: { status: 0, message: "" }, list: [] }, @@ -1527,7 +2046,7 @@ const METHOD_TEMPLATES = { // ── OTA ── ota_upgrade: { result: 0, output: { status: 1, message: "" } }, // ── 航线(async) ── - route_upload: { result: 0, output: { status: 1, message: "" } }, + route_upload: { result: 0, output: { status: 0, message: "Mission uploaded successfully!" } }, route_execute: { result: 0, output: { status: 1, message: "" } }, route_auto: { result: 0, output: { status: 1, message: "" } }, // ── 航线(sync) ── @@ -1861,6 +2380,143 @@ async function routeJsonCmd(action, textareaId, respId) { request('POST', API_BASE + '/business/q20/route/' + action + '/' + s, body, respId); } +// ==================== 航线库(分页查询本地 bus_route + bus_route_waypoint) ==================== +let rlPage = 1, rlSize = 10, rlTotal = 0; +const _esc = s => String(s == null ? '' : s).replace(/[&<>"]/g, c => ({'&':'&','<':'<','>':'>','"':'"'}[c])); +const _fmtTs = ts => { if (!ts) return '—'; const d = new Date(ts); return isNaN(d) ? _esc(ts) : d.toLocaleString(); }; + +function rlReset() { + document.getElementById('rl_name').value = ''; + document.getElementById('rl_wayline').value = ''; + rlSearch(1); +} + +// 计算页码窗口:首尾页 + 当前页两侧各2页,间断处以 '...' 表示 +function pageWindow(cur, max) { + const set = new Set([1, max]); + for (let p = cur - 2; p <= cur + 2; p++) if (p >= 1 && p <= max) set.add(p); + const sorted = [...set].sort((a, b) => a - b); + const out = []; + let prev = 0; + for (const p of sorted) { + if (prev && p - prev > 1) out.push('...'); + out.push(p); prev = p; + } + return out; +} + +// 渲染底部页码按钮(« 上一页 + 数字页码 + 下一页 ») +function buildPager() { + const maxPage = Math.max(1, Math.ceil(rlTotal / rlSize)); + if (rlPage > maxPage) rlPage = maxPage; + const btn = (label, page, o) => { + o = o || {}; + if (o.ellipsis) return ''; + if (o.active) return ''; + if (o.disabled) return ''; + return ''; + }; + let html = btn('«', rlPage - 1, { disabled: rlPage <= 1 }); + for (const p of pageWindow(rlPage, maxPage)) { + html += (p === '...') ? btn('…', 0, { ellipsis: true }) : btn(p, p, { active: p === rlPage }); + } + html += btn('»', rlPage + 1, { disabled: rlPage >= maxPage }); + document.getElementById('rl_pager').innerHTML = html; +} + +async function rlSearch(page) { + rlPage = page || 1; + rlSize = +v('rl_size') || 10; + const params = new URLSearchParams({ page: rlPage, limit: rlSize }); + const name = v('rl_name').trim(); if (name) params.set('routeName', name); + const wl = v('rl_wayline').trim(); if (wl) params.set('q20RouteId', wl); + const respEl = document.getElementById('rl_resp'); + respEl.style.display = 'none'; + try { + const res = await fetch(API_BASE + '/business/q20/route/page?' + params.toString(), { headers: headers() }); + if (res.status === 401) { doLogout(); return; } + const json = await res.json(); + if (!res.ok || (json.code != null && json.code !== 0)) { + throw new Error(json.msg || ('HTTP ' + res.status)); + } + const data = json.data || {}; + rlTotal = data.total || 0; + renderRouteLib(data.list || []); + } catch (e) { + respEl.style.display = 'block'; + respEl.textContent = '查询失败:' + e.message; + document.getElementById('rl_tbody').innerHTML = + '查询失败'; + } +} + +function renderRouteLib(list) { + const tbody = document.getElementById('rl_tbody'); + if (!list.length) { + tbody.innerHTML = '无符合条件的航线'; + } else { + const base = (rlPage - 1) * rlSize; // 跨页累计序号起点 + tbody.innerHTML = list.map((r, i) => { + const wps = Array.isArray(r.routeWaypointList) ? r.routeWaypointList : []; + const detailId = 'rl_wp_' + i; + const main = + '' + + '' + (base + i + 1) + '' + + '' + _esc(r.routeName) + '' + + '' + _esc(r.q20RouteId || '—') + '' + + '' + _esc(r.dockSn || '—') + '' + + '' + (r.waypointNum != null ? r.waypointNum : wps.length) + '' + + '' + _fmt(r.flightSpeed, 1) + '' + + '' + _fmt(r.flightHeight, 1) + '' + + '' + _fmt(r.globalRthHeight, 1) + '' + + '' + _esc(r.finishAction || '—') + '' + + '' + _fmtTs(r.updateDate) + '' + + '' + + ''; + const detail = + '' + + renderWaypoints(wps) + ''; + return main + detail; + }).join(''); + } + // 分页信息与页码 + const maxPage = Math.max(1, Math.ceil(rlTotal / rlSize)); + document.getElementById('rl_info').textContent = '共 ' + rlTotal + ' 条 · 第 ' + rlPage + '/' + maxPage + ' 页'; + buildPager(); +} + +function renderWaypoints(wps) { + if (!wps.length) return '
该航线无航点记录
'; + let rows = wps.map(w => + '' + + '' + (w.waypointSort != null ? w.waypointSort : '—') + '' + + '' + _fmt(w.longitude, 7) + '' + + '' + _fmt(w.latitude, 7) + '' + + '' + _fmt(w.flightHeight, 1) + '' + + '' + _fmt(w.flightSpeed, 1) + '' + + '' + (w.followRouteSpeed ? '是' : '否') + '' + + '' + renderActions(w.waypointActionList) + '' + + '').join(''); + return '' + + '' + + '' + rows + '
序号经度纬度高度(m)速度(m/s)跟随全局速度航点动作
'; +} + +// 航点动作:动作类型 + 可选动作值,按顺序以徽标列出 +function renderActions(actions) { + if (!Array.isArray(actions) || !actions.length) return ''; + return actions.map(a => { + const val = (a.actionValue != null && a.actionValue !== '') ? '=' + _esc(a.actionValue) : ''; + return '' + _esc(a.actionType || '?') + val + ''; + }).join(' '); +} + +function rlToggleWp(id) { + const el = document.getElementById(id); + if (el) el.style.display = el.style.display === 'none' ? '' : 'none'; +} + // 获取航线信息暂时停用(前后端接口先注释) // function routeInfo() { // const s = sn(); @@ -2733,10 +3389,15 @@ document.addEventListener('DOMContentLoaded', () => { // 记住本次输入,下次进入页面自动回填 if (val) localStorage.setItem('q20_device_sn', val); else localStorage.removeItem('q20_device_sn'); + // SN 变化时自动连接/切换订阅到新设备的 osd 主题 + if (val) mqttAutoConnect(); }); // 初始渲染:未填写时即显示醒目提示 refreshSnState(); + // 页面加载即自动连接 MQTT,订阅当前 SN 的实时遥测(断线由库自动重连) + mqttAutoConnect(); + document.getElementById('sh_current_gps').addEventListener('change', function () { document.getElementById('sh_manual_fields').style.display = this.checked ? 'none' : ''; }); @@ -2850,6 +3511,12 @@ document.addEventListener('DOMContentLoaded', () => {
+
+ + + + +