diff --git a/admin/src/main/java/com/multictrl/modules/business/handler/TopicDistributor.java b/admin/src/main/java/com/multictrl/modules/business/handler/TopicDistributor.java index e0e436d..11dd069 100644 --- a/admin/src/main/java/com/multictrl/modules/business/handler/TopicDistributor.java +++ b/admin/src/main/java/com/multictrl/modules/business/handler/TopicDistributor.java @@ -2,6 +2,9 @@ package com.multictrl.modules.business.handler; import cn.hutool.core.util.StrUtil; import com.multictrl.common.constant.BusinessConstant; +import com.multictrl.modules.business.q20.handler.Q20EventsHandler; +import com.multictrl.modules.business.q20.handler.Q20OsdTopicHandler; +import com.multictrl.modules.business.q20.handler.Q20StateTopicHandler; import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -26,7 +29,13 @@ public class TopicDistributor { private final StateHandler stateHandler; private final ServicesReplyHandler servicesReplyHandler; private final EventsHandler eventsHandler; + private final Q20OsdTopicHandler q20OsdTopicHandler; + private final Q20EventsHandler q20EventsHandler; + private final Q20StateTopicHandler q20StateTopicHandler; private final Map handlerMap = new ConcurrentHashMap<>(); + private final Map q20HandlerMap = new ConcurrentHashMap<>(); + + private static final String Q20_TOPIC_PREFIX = "thing/device/"; @PostConstruct public void init() { @@ -36,13 +45,18 @@ public class TopicDistributor { handlerMap.put(BusinessConstant.STATE, stateHandler); handlerMap.put(BusinessConstant.EVENTS, eventsHandler); handlerMap.put(BusinessConstant.SERVICES_REPLY, servicesReplyHandler); + + q20HandlerMap.put(BusinessConstant.OSD, q20OsdTopicHandler); + q20HandlerMap.put(BusinessConstant.EVENTS, q20EventsHandler); + q20HandlerMap.put(BusinessConstant.STATE, q20StateTopicHandler); } public void route(String topic, String payload) { String method = StrUtil.subAfter(topic, "/", true); String gateway = StrUtil.subAfter(StrUtil.subBefore(topic, "/", true), "/", true); - MessageHandler handler = handlerMap.get(method); + Map map = topic.startsWith(Q20_TOPIC_PREFIX) ? q20HandlerMap : handlerMap; + MessageHandler handler = map.get(method); if (handler != null) { handler.handleMessage(topic, payload, gateway); } else { 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 new file mode 100644 index 0000000..74a8de8 --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/q20/handler/Q20BatteryHandler.java @@ -0,0 +1,74 @@ +package com.multictrl.modules.business.q20.handler; + +import cn.hutool.json.JSONObject; +import com.multictrl.common.constant.BusinessConstant; +import com.multictrl.common.utils.CacheUtils; +import com.multictrl.common.utils.JsonUtils; +import com.multictrl.modules.business.handler.MessageHandler; +import com.multictrl.modules.business.q20.influxdb.Q20BatteryReport; +import com.multictrl.modules.business.service.InfluxService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * Q20电池数据处理(method: battery) + * 上报智能电池详细状态:id, full_charge_capacity, charge_remaining, voltage, current, + * low_volt_warn_value, status(0异常/1开机/2充电中/3需保养), temperature, cycle_index, + * time_flying, time_remaining, charge_time_remaining, health, flags, uid, version + * + * @author 938693313@qq.com + * @since 1.0.0 2026/5/12 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class Q20BatteryHandler implements MessageHandler { + + private final InfluxService influxService; + + @Override + public void handleMessage(String topic, String payload, String gateway) { + JSONObject message = JsonUtils.parseObject(payload, JSONObject.class); + if (message != null) { + String deviceSn = message.getStr(BusinessConstant.GATEWAY); + JSONObject data = message.getJSONObject(BusinessConstant.DATA); + if (data != null) { + // 以电池id区分多块电池,缓存key加上电池id后缀 + Integer batteryId = data.getInt("id"); + String cacheKey = Q20Constant.Q20_BATTERY + deviceSn + + (batteryId != null ? "_" + batteryId : ""); + CacheUtils.set(cacheKey, data); + saveToInflux(deviceSn, batteryId, data); + 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"); + } + } + + private void saveToInflux(String deviceSn, Integer batteryId, JSONObject data) { + Q20BatteryReport report = Q20BatteryReport.builder() + .deviceSn(deviceSn) + .battery_id(batteryId != null ? String.valueOf(batteryId) : null) + .order_id(data.getStr("order_id")) + .full_charge_capacity(data.getInt("full_charge_capacity")) + .charge_remaining(data.getFloat("charge_remaining")) + .voltage(data.getInt("voltage")) + .current(data.getInt("current")) + .low_volt_warn_value(data.getInt("low_volt_warn_value")) + .status(data.getInt("status")) + .temperature(data.getFloat("temperature")) + .cycle_index(data.getInt("cycle_index")) + .time_flying(data.getLong("time_flying")) + .time_remaining(data.getInt("time_remaining")) + .charge_time_remaining(data.getInt("charge_time_remaining")) + .health(data.getInt("health")) + .flags(data.getInt("flags")) + .uid(data.getStr("uid")) + .version(data.getStr("version")) + .build(); + influxService.addRecord(report); + } +} diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/handler/Q20Constant.java b/admin/src/main/java/com/multictrl/modules/business/q20/handler/Q20Constant.java new file mode 100644 index 0000000..c3a5602 --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/q20/handler/Q20Constant.java @@ -0,0 +1,47 @@ +package com.multictrl.modules.business.q20.handler; + +/** + * Q20设备相关常量 + * + * @author 938693313@qq.com + * @since 1.0.0 2026/5/12 + */ +public interface Q20Constant { + + // Q20 osd topic method字段值 + String METHOD_OSD = "osd"; + String METHOD_RTK = "rtk"; + String METHOD_RC = "rc"; + String METHOD_MOUNT = "mount"; + String METHOD_BATTERY = "battery"; + String METHOD_OBS = "obs"; + // Q20 state topic method字段值 + String METHOD_VERSION = "version"; + String METHOD_NOTIFY = "notify"; + String METHOD_HMS = "hms"; + // Q20 events topic method字段值 + String METHOD_LIFTOFF = "liftoff"; + String METHOD_ARRIVAL = "arrival"; + String METHOD_TAKEOFF = "takeoff"; + String METHOD_LAND = "land"; + String METHOD_NAVIGATE = "navigate"; + String METHOD_ROUTE_UPLOAD = "route_upload"; + String METHOD_ROUTE_EXECUTE = "route_execute"; + String METHOD_ROUTE_AUTO = "route_auto"; + String METHOD_OTA = "ota"; + + // Q20 缓存key前缀 + String Q20_OSD = "q20_osd_"; + String Q20_RTK = "q20_rtk_"; + String Q20_RC = "q20_rc_"; + String Q20_MOUNT = "q20_mount_"; + String Q20_BATTERY = "q20_battery_"; + String Q20_OBS = "q20_obs_"; + String Q20_VERSION = "q20_version_"; + String Q20_HMS = "q20_hms_"; + String Q20_IN_WORK = "q20_in_work_"; + String Q20_LIFTOFF = "q20_liftoff_"; + String Q20_ARRIVAL = "q20_arrival_"; + String Q20_ROUTE_EXECUTE = "q20_route_execute_"; + String Q20_ROUTE_AUTO = "q20_route_auto_"; +} diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/handler/Q20EventsHandler.java b/admin/src/main/java/com/multictrl/modules/business/q20/handler/Q20EventsHandler.java new file mode 100644 index 0000000..5f3f153 --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/q20/handler/Q20EventsHandler.java @@ -0,0 +1,193 @@ +package com.multictrl.modules.business.q20.handler; + +import cn.hutool.json.JSONObject; +import com.multictrl.common.constant.BusinessConstant; +import com.multictrl.common.utils.CacheUtils; +import com.multictrl.common.utils.JsonUtils; +import com.multictrl.modules.business.handler.MessageHandler; +import com.multictrl.modules.business.service.MqttPushService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * Q20设备事件消息处理(topic: thing/device/{device_sn}/events) + * 若need_reply=1需回复 events_reply 主题 + * method: liftoff(起飞事件) | arrival(到达事件) | takeoff(起飞指令进度) | land(降落指令进度) | + * navigate(指点飞行进度) | route_upload(航线上传结果) | route_execute(航线执行进度) | + * route_auto(一键飞行进度) | ota(固件升级进度) + * + * @author 938693313@qq.com + * @since 1.0.0 2026/5/12 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class Q20EventsHandler implements MessageHandler { + + private final MqttPushService mqttPushService; + + @Override + public void handleMessage(String topic, String payload, String gateway) { + JSONObject message = JsonUtils.parseObject(payload, JSONObject.class); + if (message == null) { + log.debug("Q20 events --> payload解析失败,解析后为null"); + return; + } + + String deviceSn = message.getStr(BusinessConstant.GATEWAY); + Integer needReply = message.getInt(BusinessConstant.NEED_REPLY); + if (needReply != null && needReply == 1) { + JSONObject data; + Object dataObject = message.get(BusinessConstant.DATA); + if (dataObject instanceof JSONObject) { + data = (JSONObject) dataObject; + } else { + data = new JSONObject(); + message.set(BusinessConstant.DATA, data); + } + data.clear(); + data.set("result", 0); + message.remove(BusinessConstant.NEED_REPLY); + message.remove(BusinessConstant.GATEWAY); + mqttPushService.pushMessageByClient1(topic + BusinessConstant._REPLY, message.toString()); + log.debug("Q20 events --> 已回复, deviceSn: {}, topic: {}", deviceSn, topic + BusinessConstant._REPLY); + } + + // 重新解析,避免回复时修改了data对象 + message = JsonUtils.parseObject(payload, JSONObject.class); + if (message == null) { + return; + } + String method = message.getStr(BusinessConstant.METHOD); + if (method == null) { + log.debug("Q20 events --> method字段缺失, topic: {}", topic); + return; + } + JSONObject data = message.getJSONObject(BusinessConstant.DATA); + if (data == null) { + return; + } + + switch (method) { + case Q20Constant.METHOD_LIFTOFF: + handleLiftoff(deviceSn, data); + break; + case Q20Constant.METHOD_ARRIVAL: + handleArrival(deviceSn, data); + break; + case Q20Constant.METHOD_TAKEOFF: + handleProgress(deviceSn, "起飞指令", data); + break; + case Q20Constant.METHOD_LAND: + handleProgress(deviceSn, "降落指令", data); + break; + case Q20Constant.METHOD_NAVIGATE: + handleProgress(deviceSn, "指点飞行", data); + break; + case Q20Constant.METHOD_ROUTE_UPLOAD: + handleRouteUpload(deviceSn, data); + break; + case Q20Constant.METHOD_ROUTE_EXECUTE: + handleRouteExecute(deviceSn, data); + break; + case Q20Constant.METHOD_ROUTE_AUTO: + handleRouteAuto(deviceSn, data); + break; + case Q20Constant.METHOD_OTA: + handleOta(deviceSn, data); + break; + default: + log.debug("Q20 events --> 未知method: {}, deviceSn: {}", method, deviceSn); + } + } + + /** + * 起飞事件:飞机从停机状态转变到起飞状态时上报 + * type: command(指令飞行) | waypoint(航线飞行) | leapfrog(蛙跳飞行) | manual(手动飞行) + */ + private void handleLiftoff(String deviceSn, JSONObject data) { + CacheUtils.set(Q20Constant.Q20_LIFTOFF + deviceSn, data); + log.info("Q20 events --> 起飞事件, deviceSn: {}, type: {}, wayline: {}", + deviceSn, data.getStr("type"), data.getStr("wayline")); + } + + /** + * 到达事件:飞机飞到某位置停降时上报 + * type: land(直接降落) | preLand(准备降落) | emergency(应急降落) + */ + private void handleArrival(String deviceSn, JSONObject data) { + CacheUtils.set(Q20Constant.Q20_ARRIVAL + deviceSn, data); + log.info("Q20 events --> 到达事件, deviceSn: {}, type: {}, code: {}", + deviceSn, data.getStr("type"), data.getStr("code")); + } + + /** + * 通用指令进度:takeoff / land / navigate + * status: in_progress | ok | failed | canceled 等 + */ + private void handleProgress(String deviceSn, String label, JSONObject data) { + String status = data.getStr("status"); + JSONObject progress = data.getJSONObject("progress"); + int percent = progress != null ? progress.getInt("percent", 0) : 0; + log.debug("Q20 events --> {}进度, deviceSn: {}, status: {}, percent: {}%", + label, deviceSn, status, percent); + } + + /** + * 航线上传结果(need_reply=1已在上方统一处理) + */ + private void handleRouteUpload(String deviceSn, JSONObject data) { + String status = data.getStr("status"); + JSONObject progress = data.getJSONObject("progress"); + String stepKey = progress != null ? progress.getStr("step_key") : null; + log.info("Q20 events --> 航线上传结果, deviceSn: {}, status: {}, step: {}", + deviceSn, status, stepKey); + } + + /** + * 航线执行进度:缓存最新进度供前端轮询 + * progress含: step_key, percent, remaining_distance, first_point_distance, + * first_point_remaining_time, first_point_progress, landing_progress + */ + private void handleRouteExecute(String deviceSn, JSONObject data) { + CacheUtils.set(Q20Constant.Q20_ROUTE_EXECUTE + deviceSn, data); + String status = data.getStr("status"); + JSONObject progress = data.getJSONObject("progress"); + if (progress != null) { + log.debug("Q20 events --> 航线执行进度, deviceSn: {}, status: {}, step: {}, percent: {}%, remain: {}m", + deviceSn, status, progress.getStr("step_key"), + progress.getInt("percent"), progress.getInt("remaining_distance")); + } else { + log.debug("Q20 events --> 航线执行进度, deviceSn: {}, status: {}", deviceSn, status); + } + } + + /** + * 一键飞行执行进度:缓存最新进度供前端轮询 + */ + private void handleRouteAuto(String deviceSn, JSONObject data) { + CacheUtils.set(Q20Constant.Q20_ROUTE_AUTO + deviceSn, data); + String status = data.getStr("status"); + JSONObject progress = data.getJSONObject("progress"); + if (progress != null) { + log.debug("Q20 events --> 一键飞行进度, deviceSn: {}, status: {}, step: {}, percent: {}%, remain: {}m", + deviceSn, status, progress.getStr("step_key"), + progress.getInt("percent"), progress.getInt("remaining_distance")); + } else { + log.debug("Q20 events --> 一键飞行进度, deviceSn: {}, status: {}", deviceSn, status); + } + } + + /** + * 固件升级进度 + */ + private void handleOta(String deviceSn, JSONObject data) { + String status = data.getStr("status"); + JSONObject progress = data.getJSONObject("progress"); + int percent = progress != null ? progress.getInt("percent", 0) : 0; + log.info("Q20 events --> 固件升级进度, deviceSn: {}, status: {}, step: {}, percent: {}%", + deviceSn, status, + progress != null ? progress.getStr("step_key") : null, percent); + } +} 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 new file mode 100644 index 0000000..48785a7 --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/q20/handler/Q20HmsHandler.java @@ -0,0 +1,60 @@ +package com.multictrl.modules.business.q20.handler; + +import cn.hutool.json.JSONObject; +import com.multictrl.common.constant.BusinessConstant; +import com.multictrl.common.utils.CacheUtils; +import com.multictrl.common.utils.JsonUtils; +import com.multictrl.modules.business.handler.MessageHandler; +import com.multictrl.modules.business.service.MqttPushService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * Q20系统健康状态处理(method: hms,state topic) + * 设备状态变化时触发上报,包含各子系统故障码数组: + * gnss, ins, battery, power, gimbal, system, structure, radar, vision, flight_control + * 以及故障状态位码:code2, code3, code4 + * 若need_reply=1需回复 state_reply 主题 + * + * @author 938693313@qq.com + * @since 1.0.0 2026/5/12 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class Q20HmsHandler implements MessageHandler { + + private final MqttPushService mqttPushService; + + @Override + public void handleMessage(String topic, String payload, String gateway) { + JSONObject message = JsonUtils.parseObject(payload, JSONObject.class); + if (message != null) { + String deviceSn = message.getStr(BusinessConstant.GATEWAY); + JSONObject data = message.getJSONObject(BusinessConstant.DATA); + Integer needReply = message.getInt(BusinessConstant.NEED_REPLY); + if (needReply != null && needReply == 1) { + data.clear(); + data.set("result", 0); + message.remove(BusinessConstant.NEED_REPLY); + mqttPushService.pushMessageByClient2(topic + BusinessConstant._REPLY, message.toString()); + log.debug("Q20 hms --> 已回复, deviceSn: {}", deviceSn); + } + // 重新解析,避免回复时修改了data对象 + message = JsonUtils.parseObject(payload, JSONObject.class); + if (message == null) { + return; + } + 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")); + } + } 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 new file mode 100644 index 0000000..425ad55 --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/q20/handler/Q20MountHandler.java @@ -0,0 +1,64 @@ +package com.multictrl.modules.business.q20.handler; + +import cn.hutool.json.JSONObject; +import com.multictrl.common.constant.BusinessConstant; +import com.multictrl.common.utils.CacheUtils; +import com.multictrl.common.utils.JsonUtils; +import com.multictrl.modules.business.handler.MessageHandler; +import com.multictrl.modules.business.q20.influxdb.Q20MountReport; +import com.multictrl.modules.business.service.InfluxService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * Q20负载数据处理(method: mount) + * 上报挂载负载状态:mount_id, mount_port, mount_type, payload(yaw/pitch/roll/mode/zoom等) + * 支持ZT30(四光)、Z40T(双光)、A30(三光)、GL60P(探照灯)、4C(抛投器)、SZY/YKX/EayLoad10(索降)、 + * LC(物流箱)、PARACHUTE(降落伞)等各类型负载 + * + * @author 938693313@qq.com + * @since 1.0.0 2026/5/12 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class Q20MountHandler implements MessageHandler { + + private final InfluxService influxService; + + @Override + public void handleMessage(String topic, String payload, String gateway) { + JSONObject message = JsonUtils.parseObject(payload, JSONObject.class); + if (message != null) { + String deviceSn = message.getStr(BusinessConstant.GATEWAY); + JSONObject data = message.getJSONObject(BusinessConstant.DATA); + 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")); + } + } else { + log.debug("Q20 mount --> payload解析失败,解析后为null"); + } + } + + private void saveToInflux(String deviceSn, JSONObject data) { + JSONObject payload = data.getJSONObject("payload"); + + Q20MountReport report = Q20MountReport.builder() + .deviceSn(deviceSn) + .order_id(data.getStr("order_id")) + .mount_id(data.getStr("mount_id")) + .mount_port(data.getInt("mount_port")) + .mount_type(data.getStr("mount_type")) + .payload_yaw(payload != null ? payload.getFloat("yaw") : null) + .payload_pitch(payload != null ? payload.getFloat("pitch") : null) + .payload_roll(payload != null ? payload.getFloat("roll") : null) + .payload_mode(payload != null ? payload.getInt("mode") : null) + .payload_zoom(payload != null ? payload.getFloat("zoom") : null) + .build(); + influxService.addRecord(report); + } +} diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/handler/Q20NotifyHandler.java b/admin/src/main/java/com/multictrl/modules/business/q20/handler/Q20NotifyHandler.java new file mode 100644 index 0000000..6dc9c72 --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/q20/handler/Q20NotifyHandler.java @@ -0,0 +1,58 @@ +package com.multictrl.modules.business.q20.handler; + +import cn.hutool.json.JSONObject; +import com.multictrl.common.constant.BusinessConstant; +import com.multictrl.common.utils.CacheUtils; +import com.multictrl.common.utils.JsonUtils; +import com.multictrl.modules.business.handler.MessageHandler; +import com.multictrl.modules.business.service.MqttPushService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * Q20设备通知事件处理(method: notify,state topic) + * 设备状态变化时触发上报,包含:index, type, level(0紧急/2严重/4告警/6提示), + * message, timestamp, value(故障码), clear_codes(清除码) + * 若need_reply=1需回复 state_reply 主题 + * + * @author 938693313@qq.com + * @since 1.0.0 2026/5/12 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class Q20NotifyHandler implements MessageHandler { + + private final MqttPushService mqttPushService; + + @Override + public void handleMessage(String topic, String payload, String gateway) { + JSONObject message = JsonUtils.parseObject(payload, JSONObject.class); + if (message != null) { + String deviceSn = message.getStr(BusinessConstant.GATEWAY); + JSONObject data = message.getJSONObject(BusinessConstant.DATA); + Integer needReply = message.getInt(BusinessConstant.NEED_REPLY); + if (needReply != null && needReply == 1) { + data.clear(); + data.set("result", 0); + message.remove(BusinessConstant.NEED_REPLY); + mqttPushService.pushMessageByClient2(topic + BusinessConstant._REPLY, message.toString()); + log.debug("Q20 notify --> 已回复, deviceSn: {}", deviceSn); + } + // 重新解析,避免回复时修改了data对象 + message = JsonUtils.parseObject(payload, JSONObject.class); + if (message == null) { + return; + } + data = message.getJSONObject(BusinessConstant.DATA); + if (data != null) { + log.warn("Q20 notify --> deviceSn: {}, type: {}, level: {}, message: {}, value: {}", + deviceSn, data.getStr("type"), data.getInt("level"), + data.getStr("message"), data.getStr("value")); + } + } else { + log.debug("Q20 notify --> 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 new file mode 100644 index 0000000..305f891 --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/q20/handler/Q20ObsHandler.java @@ -0,0 +1,81 @@ +package com.multictrl.modules.business.q20.handler; + +import cn.hutool.json.JSONArray; +import cn.hutool.json.JSONObject; +import com.multictrl.common.constant.BusinessConstant; +import com.multictrl.common.utils.CacheUtils; +import com.multictrl.common.utils.JsonUtils; +import com.multictrl.modules.business.handler.MessageHandler; +import com.multictrl.modules.business.q20.influxdb.Q20ObsReport; +import com.multictrl.modules.business.service.InfluxService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * Q20避障数据处理(method: obs) + * 上报避障传感器状态:cp_distance(避障距离/m), cp_enable(避障开关), + * ver_distances(垂直方向[下方,上方]障碍物距离数组/-1表示无障碍物), + * around_distances(水平方向8方位障碍物距离数组/-1表示无障碍物,从正前方顺时针) + * + * @author 938693313@qq.com + * @since 1.0.0 2026/5/12 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class Q20ObsHandler implements MessageHandler { + + private final InfluxService influxService; + + @Override + public void handleMessage(String topic, String payload, String gateway) { + JSONObject message = JsonUtils.parseObject(payload, JSONObject.class); + if (message != null) { + String deviceSn = message.getStr(BusinessConstant.GATEWAY); + JSONObject data = message.getJSONObject(BusinessConstant.DATA); + 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")); + } + } else { + log.debug("Q20 obs --> payload解析失败,解析后为null"); + } + } + + private void saveToInflux(String deviceSn, JSONObject data) { + JSONArray verDistances = data.getJSONArray("ver_distances"); + JSONArray aroundDistances = data.getJSONArray("around_distances"); + + Q20ObsReport report = Q20ObsReport.builder() + .deviceSn(deviceSn) + .order_id(data.getStr("order_id")) + .cp_distance(data.getFloat("cp_distance")) + .cp_enable(data.getInt("cp_enable")) + .ver_down(getFloat(verDistances, 0)) + .ver_up(getFloat(verDistances, 1)) + .around_0(getFloat(aroundDistances, 0)) + .around_1(getFloat(aroundDistances, 1)) + .around_2(getFloat(aroundDistances, 2)) + .around_3(getFloat(aroundDistances, 3)) + .around_4(getFloat(aroundDistances, 4)) + .around_5(getFloat(aroundDistances, 5)) + .around_6(getFloat(aroundDistances, 6)) + .around_7(getFloat(aroundDistances, 7)) + .build(); + influxService.addRecord(report); + } + + private Float getFloat(JSONArray array, int index) { + if (array == null || array.size() <= index) { + return null; + } + Object val = array.get(index); + if (val == null) { + return null; + } + return ((Number) val).floatValue(); + } +} diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/handler/Q20OsdHandler.java b/admin/src/main/java/com/multictrl/modules/business/q20/handler/Q20OsdHandler.java new file mode 100644 index 0000000..e0fbcca --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/q20/handler/Q20OsdHandler.java @@ -0,0 +1,114 @@ +package com.multictrl.modules.business.q20.handler; + +import cn.hutool.json.JSONObject; +import com.multictrl.common.constant.BusinessConstant; +import com.multictrl.common.utils.CacheUtils; +import com.multictrl.common.utils.JsonUtils; +import com.multictrl.modules.business.handler.MessageHandler; +import com.multictrl.modules.business.q20.influxdb.Q20OsdReport; +import com.multictrl.modules.business.service.FlightTaskService; +import com.multictrl.modules.business.service.InfluxService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * Q20基础遥测数据处理(method: osd) + * 定频上报飞行数据:longitude, latitude, height, altitude, vs, gs, heading, + * pitch, roll, battery, voltage, mode, flying, fly_time, fly_distance, home_location, state等 + * + * @author 938693313@qq.com + * @since 1.0.0 2026/5/12 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class Q20OsdHandler implements MessageHandler { + + private final FlightTaskService flightTaskService; + private final InfluxService influxService; + + @Override + public void handleMessage(String topic, String payload, String gateway) { + JSONObject message = JsonUtils.parseObject(payload, JSONObject.class); + if (message != null) { + String deviceSn = message.getStr(BusinessConstant.GATEWAY); + JSONObject data = message.getJSONObject(BusinessConstant.DATA); + if (data != null) { + CacheUtils.set(Q20Constant.Q20_OSD + deviceSn, data); + trackFlyingState(deviceSn, data); + saveToInflux(deviceSn, data); + } + } else { + log.debug("Q20 osd --> payload解析失败,解析后为null"); + } + } + + private void trackFlyingState(String deviceSn, JSONObject data) { + Integer flying = data.getInt("flying"); + if (flying == null) { + return; + } + // 起飞中,首次检测到飞行状态时更新架次开始 + if (flying == 1) { + if (CacheUtils.get(Q20Constant.Q20_IN_WORK + deviceSn) == null) { + flightTaskService.updateTaskStart(deviceSn); + log.debug("{}设备架次开始,更新架次表", deviceSn); + } + CacheUtils.set(Q20Constant.Q20_IN_WORK + deviceSn, true); + } + // 刚从飞行切到非飞行,架次结束 + if (flying == 0 && CacheUtils.get(Q20Constant.Q20_IN_WORK + deviceSn) != null) { + CacheUtils.delete(Q20Constant.Q20_IN_WORK + deviceSn); + flightTaskService.updateTaskComplete(deviceSn); + log.debug("{}设备架次结束,更新架次表", deviceSn); + } + } + + private void saveToInflux(String deviceSn, JSONObject data) { + Double longitude = data.getDouble("longitude"); + Double latitude = data.getDouble("latitude"); + if (longitude == null || latitude == null || longitude == 0 || latitude == 0) { + return; + } + JSONObject homeLocation = data.getJSONObject("home_location"); + JSONObject state = data.getJSONObject("state"); + + Q20OsdReport report = Q20OsdReport.builder() + .deviceSn(deviceSn) + .order_id(data.getStr("order_id")) + .longitude(longitude) + .latitude(latitude) + .height(data.getFloat("height")) + .altitude(data.getFloat("altitude")) + .vs(data.getFloat("vs")) + .gs(data.getFloat("gs")) + .heading(data.getFloat("heading")) + .pitch(data.getFloat("pitch")) + .roll(data.getFloat("roll")) + .battery(data.getInt("battery")) + .voltage(data.getInt("voltage")) + .mode(data.getStr("mode")) + .flying(data.getInt("flying")) + .fly_time(data.getLong("fly_time")) + .fly_distance(data.getFloat("fly_distance")) + .home_set(data.getInt("home_set")) + .home_distance(data.getFloat("home_distance")) + .home_altitude(homeLocation != null ? homeLocation.getFloat("altitude") : null) + .home_latitude(homeLocation != null ? homeLocation.getFloat("latitude") : null) + .home_longitude(homeLocation != null ? homeLocation.getFloat("longitude") : null) + .state_safe_enabled(state != null ? state.getInt("safe_enabled") : null) + .state_rtk_enabled(state != null ? state.getInt("rtk_enabled") : null) + .state_rtk_connected(state != null ? state.getInt("rtk_connected") : null) + .state_rc_connected(state != null ? state.getInt("rc_connected") : null) + .state_obs_enabled(state != null ? state.getInt("obs_enabled") : null) + .state_accel_cal(state != null ? state.getInt("accel_cal") : null) + .state_gyro_cal(state != null ? state.getInt("gyro_cal") : null) + .state_hor_cal(state != null ? state.getInt("hor_cal") : null) + .state_mag_cal(state != null ? state.getInt("mag_cal") : null) + .state_fpv_live(state != null ? state.getInt("fpv_live") : null) + .state_stream_live(state != null ? state.getInt("stream_live") : null) + .build(); + influxService.addRecord(report); + } +} diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/handler/Q20OsdTopicHandler.java b/admin/src/main/java/com/multictrl/modules/business/q20/handler/Q20OsdTopicHandler.java new file mode 100644 index 0000000..1d81b05 --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/q20/handler/Q20OsdTopicHandler.java @@ -0,0 +1,66 @@ +package com.multictrl.modules.business.q20.handler; + +import cn.hutool.json.JSONObject; +import com.multictrl.common.constant.BusinessConstant; +import com.multictrl.common.utils.JsonUtils; +import com.multictrl.modules.business.handler.MessageHandler; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * Q20设备OSD主题分发器 + * 接收 thing/device/{device_sn}/osd 主题消息,根据payload中的method字段分发到对应处理器 + * method: osd(基础遥测) | rtk(RTK定位) | rc(遥控链路) | mount(负载) | battery(电池) | obs(避障) + * + * @author 938693313@qq.com + * @since 1.0.0 2026/5/12 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class Q20OsdTopicHandler implements MessageHandler { + + private final Q20OsdHandler q20OsdHandler; + private final Q20RtkHandler q20RtkHandler; + private final Q20RcHandler q20RcHandler; + private final Q20MountHandler q20MountHandler; + private final Q20BatteryHandler q20BatteryHandler; + private final Q20ObsHandler q20ObsHandler; + + @Override + public void handleMessage(String topic, String payload, String gateway) { + JSONObject message = JsonUtils.parseObject(payload, JSONObject.class); + if (message == null) { + log.debug("Q20 osd topic --> payload解析失败,解析后为null"); + return; + } + String method = message.getStr(BusinessConstant.METHOD); + if (method == null) { + log.debug("Q20 osd topic --> method字段缺失, topic: {}", topic); + return; + } + switch (method) { + case Q20Constant.METHOD_OSD: + q20OsdHandler.handleMessage(topic, payload, gateway); + break; + case Q20Constant.METHOD_RTK: + q20RtkHandler.handleMessage(topic, payload, gateway); + break; + case Q20Constant.METHOD_RC: + q20RcHandler.handleMessage(topic, payload, gateway); + break; + case Q20Constant.METHOD_MOUNT: + q20MountHandler.handleMessage(topic, payload, gateway); + break; + case Q20Constant.METHOD_BATTERY: + q20BatteryHandler.handleMessage(topic, payload, gateway); + break; + case Q20Constant.METHOD_OBS: + q20ObsHandler.handleMessage(topic, payload, gateway); + break; + default: + log.debug("Q20 osd topic --> 未知method: {}", method); + } + } +} 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 new file mode 100644 index 0000000..72b8b10 --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/q20/handler/Q20RcHandler.java @@ -0,0 +1,82 @@ +package com.multictrl.modules.business.q20.handler; + +import cn.hutool.json.JSONArray; +import cn.hutool.json.JSONObject; +import com.multictrl.common.constant.BusinessConstant; +import com.multictrl.common.utils.CacheUtils; +import com.multictrl.common.utils.JsonUtils; +import com.multictrl.modules.business.handler.MessageHandler; +import com.multictrl.modules.business.q20.influxdb.Q20RcReport; +import com.multictrl.modules.business.service.InfluxService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * Q20遥控链路数据处理(method: rc) + * 上报遥控与链路信息:priority, rc_channel_state, rc_state, + * link数组(id/type/connected/inuse/sqe/band), up_loss_rate, down_loss_rate, network信息 + * + * @author 938693313@qq.com + * @since 1.0.0 2026/5/12 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class Q20RcHandler implements MessageHandler { + + private final InfluxService influxService; + + @Override + public void handleMessage(String topic, String payload, String gateway) { + JSONObject message = JsonUtils.parseObject(payload, JSONObject.class); + if (message != null) { + String deviceSn = message.getStr(BusinessConstant.GATEWAY); + JSONObject data = message.getJSONObject(BusinessConstant.DATA); + 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")); + } + } else { + log.debug("Q20 rc --> payload解析失败,解析后为null"); + } + } + + private void saveToInflux(String deviceSn, JSONObject data) { + // 从link数组中取inuse=1的链路,无则取第一条 + JSONObject inuseLink = null; + JSONArray links = data.getJSONArray("link"); + if (links != null) { + for (int i = 0; i < links.size(); i++) { + JSONObject link = links.getJSONObject(i); + if (link != null && Integer.valueOf(1).equals(link.getInt("inuse"))) { + inuseLink = link; + break; + } + } + if (inuseLink == null && !links.isEmpty()) { + inuseLink = links.getJSONObject(0); + } + } + JSONObject network = data.getJSONObject("network"); + + Q20RcReport report = Q20RcReport.builder() + .deviceSn(deviceSn) + .order_id(data.getStr("order_id")) + .priority(data.getInt("priority")) + .rc_channel_state(data.getStr("rc_channel_state")) + .rc_state(data.getStr("rc_state")) + .up_loss_rate(data.getFloat("up_loss_rate")) + .down_loss_rate(data.getFloat("down_loss_rate")) + .link_inuse_id(inuseLink != null ? inuseLink.getInt("id") : null) + .link_inuse_type(inuseLink != null ? inuseLink.getInt("type") : null) + .link_inuse_sqe(inuseLink != null ? inuseLink.getInt("sqe") : null) + .link_inuse_band(inuseLink != null ? inuseLink.getInt("band") : null) + .network_type(network != null ? network.getInt("type") : null) + .network_quality(network != null ? network.getInt("quality") : null) + .build(); + influxService.addRecord(report); + } +} 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 new file mode 100644 index 0000000..83aa2e9 --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/q20/handler/Q20RtkHandler.java @@ -0,0 +1,67 @@ +package com.multictrl.modules.business.q20.handler; + +import cn.hutool.json.JSONObject; +import com.multictrl.common.constant.BusinessConstant; +import com.multictrl.common.utils.CacheUtils; +import com.multictrl.common.utils.JsonUtils; +import com.multictrl.modules.business.handler.MessageHandler; +import com.multictrl.modules.business.q20.influxdb.Q20RtkReport; +import com.multictrl.modules.business.service.InfluxService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * Q20 RTK数据处理(method: rtk) + * 上报RTK定位信息:fix_type, latitude, longitude, altitude, ellipsoid_height, + * satellite_visible, eph, epv, velocity, yaw, cog, horizontal_acc, vertical_acc, velocity_acc + * + * @author 938693313@qq.com + * @since 1.0.0 2026/5/12 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class Q20RtkHandler implements MessageHandler { + + private final InfluxService influxService; + + @Override + public void handleMessage(String topic, String payload, String gateway) { + JSONObject message = JsonUtils.parseObject(payload, JSONObject.class); + if (message != null) { + String deviceSn = message.getStr(BusinessConstant.GATEWAY); + JSONObject data = message.getJSONObject(BusinessConstant.DATA); + 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")); + } + } else { + log.debug("Q20 rtk --> payload解析失败,解析后为null"); + } + } + + private void saveToInflux(String deviceSn, JSONObject data) { + Q20RtkReport report = Q20RtkReport.builder() + .deviceSn(deviceSn) + .order_id(data.getStr("order_id")) + .fix_type(data.getInt("fix_type")) + .latitude(data.getDouble("latitude")) + .longitude(data.getDouble("longitude")) + .altitude(data.getFloat("altitude")) + .ellipsoid_height(data.getFloat("ellipsoid_height")) + .satellite_visible(data.getInt("satellite_visible")) + .eph(data.getFloat("eph")) + .epv(data.getFloat("epv")) + .velocity(data.getFloat("velocity")) + .yaw(data.getFloat("yaw")) + .cog(data.getFloat("cog")) + .horizontal_acc(data.getFloat("horizontal_acc")) + .vertical_acc(data.getFloat("vertical_acc")) + .velocity_acc(data.getFloat("velocity_acc")) + .build(); + influxService.addRecord(report); + } +} diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/handler/Q20StateTopicHandler.java b/admin/src/main/java/com/multictrl/modules/business/q20/handler/Q20StateTopicHandler.java new file mode 100644 index 0000000..0a5e2bf --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/q20/handler/Q20StateTopicHandler.java @@ -0,0 +1,54 @@ +package com.multictrl.modules.business.q20.handler; + +import cn.hutool.json.JSONObject; +import com.multictrl.common.constant.BusinessConstant; +import com.multictrl.common.utils.JsonUtils; +import com.multictrl.modules.business.handler.MessageHandler; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * Q20设备State主题分发器 + * 接收 thing/device/{device_sn}/state 主题消息,根据payload中的method字段分发到对应处理器 + * method: version(固件版本) | notify(设备通知事件) | hms(系统健康状态) + * + * @author 938693313@qq.com + * @since 1.0.0 2026/5/12 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class Q20StateTopicHandler implements MessageHandler { + + private final Q20VersionHandler q20VersionHandler; + private final Q20NotifyHandler q20NotifyHandler; + private final Q20HmsHandler q20HmsHandler; + + @Override + public void handleMessage(String topic, String payload, String gateway) { + JSONObject message = JsonUtils.parseObject(payload, JSONObject.class); + if (message == null) { + log.debug("Q20 state topic --> payload解析失败,解析后为null"); + return; + } + String method = message.getStr(BusinessConstant.METHOD); + if (method == null) { + log.debug("Q20 state topic --> method字段缺失, topic: {}", topic); + return; + } + switch (method) { + case Q20Constant.METHOD_VERSION: + q20VersionHandler.handleMessage(topic, payload, gateway); + break; + case Q20Constant.METHOD_NOTIFY: + q20NotifyHandler.handleMessage(topic, payload, gateway); + break; + case Q20Constant.METHOD_HMS: + q20HmsHandler.handleMessage(topic, payload, gateway); + break; + default: + log.debug("Q20 state topic --> 未知method: {}", method); + } + } +} diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/handler/Q20VersionHandler.java b/admin/src/main/java/com/multictrl/modules/business/q20/handler/Q20VersionHandler.java new file mode 100644 index 0000000..1bb899d --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/q20/handler/Q20VersionHandler.java @@ -0,0 +1,58 @@ +package com.multictrl.modules.business.q20.handler; + +import cn.hutool.json.JSONObject; +import com.multictrl.common.constant.BusinessConstant; +import com.multictrl.common.utils.CacheUtils; +import com.multictrl.common.utils.JsonUtils; +import com.multictrl.modules.business.handler.MessageHandler; +import com.multictrl.modules.business.service.MqttPushService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * Q20固件版本状态处理(method: version,state topic) + * 设备状态变化时触发上报,包含:firmware_version, offboard_version, upgrade_status, + * vision_version, protocol_version, vehicle_type + * 若need_reply=1需回复 state_reply 主题 + * + * @author 938693313@qq.com + * @since 1.0.0 2026/5/12 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class Q20VersionHandler implements MessageHandler { + + private final MqttPushService mqttPushService; + + @Override + public void handleMessage(String topic, String payload, String gateway) { + JSONObject message = JsonUtils.parseObject(payload, JSONObject.class); + if (message != null) { + String deviceSn = message.getStr(BusinessConstant.GATEWAY); + JSONObject data = message.getJSONObject(BusinessConstant.DATA); + Integer needReply = message.getInt(BusinessConstant.NEED_REPLY); + if (needReply != null && needReply == 1) { + data.clear(); + data.set("result", 0); + message.remove(BusinessConstant.NEED_REPLY); + mqttPushService.pushMessageByClient2(topic + BusinessConstant._REPLY, message.toString()); + log.debug("Q20 version --> 已回复, deviceSn: {}", deviceSn); + } + // 重新解析,避免回复时修改了data对象 + message = JsonUtils.parseObject(payload, JSONObject.class); + if (message == null) { + return; + } + data = message.getJSONObject(BusinessConstant.DATA); + if (data != null) { + CacheUtils.set(Q20Constant.Q20_VERSION + deviceSn, data); + log.debug("Q20 version --> deviceSn: {}, firmware: {}, vehicle_type: {}", + deviceSn, data.getStr("firmware_version"), data.getStr("vehicle_type")); + } + } else { + log.debug("Q20 version --> payload解析失败,解析后为null"); + } + } +} diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/influxdb/Q20MountReport.java b/admin/src/main/java/com/multictrl/modules/business/q20/influxdb/Q20MountReport.java new file mode 100644 index 0000000..76ede75 --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/q20/influxdb/Q20MountReport.java @@ -0,0 +1,80 @@ +package com.multictrl.modules.business.q20.influxdb; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.influxdb.annotations.Column; +import com.influxdb.annotations.Measurement; +import com.multictrl.common.utils.DateUtils; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.Instant; + +/** + * Q20设备负载数据 InfluxDB记录 + * + * @author 938693313@qq.com + * @since 1.0.0 2026/5/12 + */ +@AllArgsConstructor +@NoArgsConstructor +@Builder +@Data +@Schema(name = "Q20设备负载上报数据") +@Measurement(name = "q20_mount") +public class Q20MountReport { + + @Schema(description = "设备SN") + @Column(tag = true) + private String deviceSn; + + @Schema(description = "架次号") + @Column(tag = true) + private String order_id; + + @Schema(hidden = true) + @Column(timestamp = true) + @JsonProperty("_time") + private Instant time; + + @Schema(description = "负载ID") + @Column + private String mount_id; + + @Schema(description = "负载挂载口") + @Column + private Integer mount_port; + + @Schema(description = "负载类型:ZT30/Z40T/A30/GL60P/4C/SZY/YKX/EayLoad10/LC/PARACHUTE等") + @Column + private String mount_type; + + @Schema(description = "云台偏航角,单位°") + @Column + private Float payload_yaw; + + @Schema(description = "云台俯仰角,单位°") + @Column + private Float payload_pitch; + + @Schema(description = "云台横滚角,单位°") + @Column + private Float payload_roll; + + @Schema(description = "云台模式") + @Column + private Integer payload_mode; + + @Schema(description = "变焦倍数") + @Column + private Float payload_zoom; + + private String timeStr; + + public void setTime(Instant time) { + this.time = time; + this.timeStr = DateUtils.utcToTime(time); + } +} diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/influxdb/Q20ObsReport.java b/admin/src/main/java/com/multictrl/modules/business/q20/influxdb/Q20ObsReport.java new file mode 100644 index 0000000..959613b --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/q20/influxdb/Q20ObsReport.java @@ -0,0 +1,96 @@ +package com.multictrl.modules.business.q20.influxdb; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.influxdb.annotations.Column; +import com.influxdb.annotations.Measurement; +import com.multictrl.common.utils.DateUtils; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.Instant; + +/** + * Q20设备避障数据 InfluxDB记录 + * + * @author 938693313@qq.com + * @since 1.0.0 2026/5/12 + */ +@AllArgsConstructor +@NoArgsConstructor +@Builder +@Data +@Schema(name = "Q20设备避障上报数据") +@Measurement(name = "q20_obs") +public class Q20ObsReport { + + @Schema(description = "设备SN") + @Column(tag = true) + private String deviceSn; + + @Schema(description = "架次号") + @Column(tag = true) + private String order_id; + + @Schema(hidden = true) + @Column(timestamp = true) + @JsonProperty("_time") + private Instant time; + + @Schema(description = "避障距离,单位m") + @Column + private Float cp_distance; + + @Schema(description = "避障开关:1开启/0关闭") + @Column + private Integer cp_enable; + + @Schema(description = "下方障碍物距离,单位m,-1表示无障碍物") + @Column + private Float ver_down; + + @Schema(description = "上方障碍物距离,单位m,-1表示无障碍物") + @Column + private Float ver_up; + + @Schema(description = "水平正前方障碍物距离,单位m,-1表示无障碍物") + @Column + private Float around_0; + + @Schema(description = "水平右前方障碍物距离,单位m,-1表示无障碍物") + @Column + private Float around_1; + + @Schema(description = "水平正右方障碍物距离,单位m,-1表示无障碍物") + @Column + private Float around_2; + + @Schema(description = "水平右后方障碍物距离,单位m,-1表示无障碍物") + @Column + private Float around_3; + + @Schema(description = "水平正后方障碍物距离,单位m,-1表示无障碍物") + @Column + private Float around_4; + + @Schema(description = "水平左后方障碍物距离,单位m,-1表示无障碍物") + @Column + private Float around_5; + + @Schema(description = "水平正左方障碍物距离,单位m,-1表示无障碍物") + @Column + private Float around_6; + + @Schema(description = "水平左前方障碍物距离,单位m,-1表示无障碍物") + @Column + private Float around_7; + + private String timeStr; + + public void setTime(Instant time) { + this.time = time; + this.timeStr = DateUtils.utcToTime(time); + } +} diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/influxdb/Q20OsdReport.java b/admin/src/main/java/com/multictrl/modules/business/q20/influxdb/Q20OsdReport.java new file mode 100644 index 0000000..4d0c72a --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/q20/influxdb/Q20OsdReport.java @@ -0,0 +1,172 @@ +package com.multictrl.modules.business.q20.influxdb; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.influxdb.annotations.Column; +import com.influxdb.annotations.Measurement; +import com.multictrl.common.utils.DateUtils; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.Instant; + +/** + * Q20设备OSD基础遥测数据 InfluxDB记录 + * + * @author 938693313@qq.com + * @since 1.0.0 2026/5/12 + */ +@AllArgsConstructor +@NoArgsConstructor +@Builder +@Data +@Schema(name = "Q20设备OSD上报数据") +@Measurement(name = "q20_osd") +public class Q20OsdReport { + + @Schema(description = "设备SN") + @Column(tag = true) + private String deviceSn; + + @Schema(description = "架次号") + @Column(tag = true) + private String order_id; + + @Schema(hidden = true) + @Column(timestamp = true) + @JsonProperty("_time") + private Instant time; + + @Schema(description = "位置经度,WGS84,精确到小数点后7位") + @Column + private Double longitude; + + @Schema(description = "位置纬度,WGS84,精确到小数点后7位") + @Column + private Double latitude; + + @Schema(description = "相对起飞点高度,单位m") + @Column + private Float height; + + @Schema(description = "海拔高度,单位m") + @Column + private Float altitude; + + @Schema(description = "垂直飞行速度,单位m/s,向上为负向下为正") + @Column + private Float vs; + + @Schema(description = "水平飞行速度(对地速度),单位m/s") + @Column + private Float gs; + + @Schema(description = "航向,取值范围[0,360],正北为0顺时针递增,单位°") + @Column + private Float heading; + + @Schema(description = "俯仰角,取值范围[-90,90],仰为正俯为负,单位°") + @Column + private Float pitch; + + @Schema(description = "横滚角,取值范围[-90,90],向左为正向右为负,单位°") + @Column + private Float roll; + + @Schema(description = "电池容量百分比,多电池为融合总电量,单位%") + @Column + private Integer battery; + + @Schema(description = "电池电压,多电池为平均电压,单位V") + @Column + private Integer voltage; + + @Schema(description = "飞行模式:MANUAL/ALTCTL/POSCTL/AUTO.MISSION/AUTO.RTL/AUTO.LAND等") + @Column + private String mode; + + @Schema(description = "是否飞行中:1飞行中,0非飞行中") + @Column + private Integer flying; + + @Schema(description = "飞行时长,单位秒") + @Column + private Long fly_time; + + @Schema(description = "飞行里程,单位m") + @Column + private Float fly_distance; + + @Schema(description = "返航点是否已设置:1已设置,0未设置") + @Column + private Integer home_set; + + @Schema(description = "距离返航点距离,单位m") + @Column + private Float home_distance; + + @Schema(description = "返航点相对高度,单位m") + @Column + private Float home_altitude; + + @Schema(description = "返航点纬度") + @Column + private Float home_latitude; + + @Schema(description = "返航点经度") + @Column + private Float home_longitude; + + @Schema(description = "安全开关:1已启用,0未启用") + @Column + private Integer state_safe_enabled; + + @Schema(description = "RTK是否启用:1已启用,0未启用") + @Column + private Integer state_rtk_enabled; + + @Schema(description = "RTCM是否连接:1已连接,0未连接") + @Column + private Integer state_rtk_connected; + + @Schema(description = "遥控是否连接:1已连接,0未连接") + @Column + private Integer state_rc_connected; + + @Schema(description = "避障是否开启:1已启用,0未启用") + @Column + private Integer state_obs_enabled; + + @Schema(description = "加速度计校准状态:1已校准,0未校准") + @Column + private Integer state_accel_cal; + + @Schema(description = "陀螺仪校准状态:1已校准,0未校准") + @Column + private Integer state_gyro_cal; + + @Schema(description = "水平校准状态:1已校准,0未校准") + @Column + private Integer state_hor_cal; + + @Schema(description = "指南针校准状态:1已校准,0未校准") + @Column + private Integer state_mag_cal; + + @Schema(description = "FPV推流状态:1开启,0关闭") + @Column + private Integer state_fpv_live; + + @Schema(description = "云台推流状态:0关闭,1主码流,2子码流") + @Column + private Integer state_stream_live; + + private String timeStr; + + public void setTime(Instant time) { + this.time = time; + this.timeStr = DateUtils.utcToTime(time); + } +} diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/influxdb/Q20RcReport.java b/admin/src/main/java/com/multictrl/modules/business/q20/influxdb/Q20RcReport.java new file mode 100644 index 0000000..6e07802 --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/q20/influxdb/Q20RcReport.java @@ -0,0 +1,92 @@ +package com.multictrl.modules.business.q20.influxdb; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.influxdb.annotations.Column; +import com.influxdb.annotations.Measurement; +import com.multictrl.common.utils.DateUtils; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.Instant; + +/** + * Q20设备遥控链路数据 InfluxDB记录 + * + * @author 938693313@qq.com + * @since 1.0.0 2026/5/12 + */ +@AllArgsConstructor +@NoArgsConstructor +@Builder +@Data +@Schema(name = "Q20设备RC链路上报数据") +@Measurement(name = "q20_rc") +public class Q20RcReport { + + @Schema(description = "设备SN") + @Column(tag = true) + private String deviceSn; + + @Schema(description = "架次号") + @Column(tag = true) + private String order_id; + + @Schema(hidden = true) + @Column(timestamp = true) + @JsonProperty("_time") + private Instant time; + + @Schema(description = "遥控优先级") + @Column + private Integer priority; + + @Schema(description = "遥控通道状态") + @Column + private String rc_channel_state; + + @Schema(description = "遥控状态") + @Column + private String rc_state; + + @Schema(description = "上行丢包率,单位%") + @Column + private Float up_loss_rate; + + @Schema(description = "下行丢包率,单位%") + @Column + private Float down_loss_rate; + + @Schema(description = "当前使用链路id") + @Column + private Integer link_inuse_id; + + @Schema(description = "当前使用链路类型:0未知/1RC遥控/2图传/3网络") + @Column + private Integer link_inuse_type; + + @Schema(description = "当前使用链路信号质量,单位%") + @Column + private Integer link_inuse_sqe; + + @Schema(description = "当前使用链路频段,单位MHz") + @Column + private Integer link_inuse_band; + + @Schema(description = "网络类型:0未知/1WiFi/24G/35G") + @Column + private Integer network_type; + + @Schema(description = "网络信号质量,单位%") + @Column + private Integer network_quality; + + private String timeStr; + + public void setTime(Instant time) { + this.time = time; + this.timeStr = DateUtils.utcToTime(time); + } +} diff --git a/admin/src/main/java/com/multictrl/modules/business/q20/influxdb/Q20RtkReport.java b/admin/src/main/java/com/multictrl/modules/business/q20/influxdb/Q20RtkReport.java new file mode 100644 index 0000000..bb6fdf4 --- /dev/null +++ b/admin/src/main/java/com/multictrl/modules/business/q20/influxdb/Q20RtkReport.java @@ -0,0 +1,104 @@ +package com.multictrl.modules.business.q20.influxdb; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.influxdb.annotations.Column; +import com.influxdb.annotations.Measurement; +import com.multictrl.common.utils.DateUtils; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.Instant; + +/** + * Q20设备RTK定位数据 InfluxDB记录 + * + * @author 938693313@qq.com + * @since 1.0.0 2026/5/12 + */ +@AllArgsConstructor +@NoArgsConstructor +@Builder +@Data +@Schema(name = "Q20设备RTK上报数据") +@Measurement(name = "q20_rtk") +public class Q20RtkReport { + + @Schema(description = "设备SN") + @Column(tag = true) + private String deviceSn; + + @Schema(description = "架次号") + @Column(tag = true) + private String order_id; + + @Schema(hidden = true) + @Column(timestamp = true) + @JsonProperty("_time") + private Instant time; + + @Schema(description = "定位类型:0未定位/1单点定位/2差分定位/4固定解") + @Column + private Integer fix_type; + + @Schema(description = "纬度,WGS84") + @Column + private Double latitude; + + @Schema(description = "经度,WGS84") + @Column + private Double longitude; + + @Schema(description = "海拔高度,单位m") + @Column + private Float altitude; + + @Schema(description = "椭球高,单位m") + @Column + private Float ellipsoid_height; + + @Schema(description = "可见卫星数") + @Column + private Integer satellite_visible; + + @Schema(description = "水平位置精度因子,单位m") + @Column + private Float eph; + + @Schema(description = "垂直位置精度因子,单位m") + @Column + private Float epv; + + @Schema(description = "速度,单位m/s") + @Column + private Float velocity; + + @Schema(description = "偏航角,单位°") + @Column + private Float yaw; + + @Schema(description = "航迹角,单位°") + @Column + private Float cog; + + @Schema(description = "水平精度,单位m") + @Column + private Float horizontal_acc; + + @Schema(description = "垂直精度,单位m") + @Column + private Float vertical_acc; + + @Schema(description = "速度精度,单位m/s") + @Column + private Float velocity_acc; + + private String timeStr; + + public void setTime(Instant time) { + this.time = time; + this.timeStr = DateUtils.utcToTime(time); + } +} diff --git a/admin/src/main/resources/application-docker.yml b/admin/src/main/resources/application-docker.yml index 0e1f335..ed42310 100644 --- a/admin/src/main/resources/application-docker.yml +++ b/admin/src/main/resources/application-docker.yml @@ -45,7 +45,7 @@ mqtt: username: admin password: Aros2023 subClientId: dj-one-sub${random.int(10)} - subTopic: thing/product/#,sys/product/# + subTopic: thing/product/#,sys/product/#,thing/device/# pubClientId: dj-one-pub${random.int(10)} client2: url: tcp://${host.ip}:61637 diff --git a/doc/Q20数据上报.docx b/doc/Q20数据上报.docx new file mode 100644 index 0000000..a0a752c Binary files /dev/null and b/doc/Q20数据上报.docx differ diff --git a/doc/Q20状态事件.docx b/doc/Q20状态事件.docx new file mode 100644 index 0000000..06a34ec Binary files /dev/null and b/doc/Q20状态事件.docx differ