1.Q20新增飞机电压消息回复

This commit is contained in:
938693313@qq.com 2026-06-12 13:17:56 +08:00
parent 44b0221f5c
commit 0de80236d7
6 changed files with 170 additions and 131 deletions

View File

@ -4,6 +4,7 @@ import cn.hutool.core.util.StrUtil;
import com.multictrl.common.constant.BusinessConstant; import com.multictrl.common.constant.BusinessConstant;
import com.multictrl.modules.business.q20.handler.Q20EventsHandler; import com.multictrl.modules.business.q20.handler.Q20EventsHandler;
import com.multictrl.modules.business.q20.handler.Q20OsdTopicHandler; import com.multictrl.modules.business.q20.handler.Q20OsdTopicHandler;
import com.multictrl.modules.business.q20.handler.Q20RequestsHandler;
import com.multictrl.modules.business.q20.handler.Q20StateTopicHandler; import com.multictrl.modules.business.q20.handler.Q20StateTopicHandler;
import com.multictrl.modules.business.q20.handler.Q20StatusHandler; import com.multictrl.modules.business.q20.handler.Q20StatusHandler;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
@ -35,6 +36,7 @@ public class TopicDistributor {
private final Q20EventsHandler q20EventsHandler; private final Q20EventsHandler q20EventsHandler;
private final Q20StateTopicHandler q20StateTopicHandler; private final Q20StateTopicHandler q20StateTopicHandler;
private final Q20StatusHandler q20StatusHandler; private final Q20StatusHandler q20StatusHandler;
private final Q20RequestsHandler q20RequestsHandler;
private final Map<String, MessageHandler> handlerMap = new ConcurrentHashMap<>(); private final Map<String, MessageHandler> handlerMap = new ConcurrentHashMap<>();
private final Map<String, MessageHandler> q20HandlerMap = new ConcurrentHashMap<>(); private final Map<String, MessageHandler> q20HandlerMap = new ConcurrentHashMap<>();
@ -56,6 +58,7 @@ public class TopicDistributor {
q20HandlerMap.put(BusinessConstant.STATE, q20StateTopicHandler); q20HandlerMap.put(BusinessConstant.STATE, q20StateTopicHandler);
q20HandlerMap.put(BusinessConstant.STATUS, q20StatusHandler); q20HandlerMap.put(BusinessConstant.STATUS, q20StatusHandler);
q20HandlerMap.put(BusinessConstant.SERVICES_REPLY, servicesReplyHandler); q20HandlerMap.put(BusinessConstant.SERVICES_REPLY, servicesReplyHandler);
q20HandlerMap.put(BusinessConstant.REQUESTS, q20RequestsHandler);
} }
public void route(String topic, String payload) { public void route(String topic, String payload) {

View File

@ -32,6 +32,8 @@ public interface Q20Constant {
String METHOD_ROUTE_EXECUTE = "route_execute"; String METHOD_ROUTE_EXECUTE = "route_execute";
String METHOD_ROUTE_AUTO = "route_auto"; String METHOD_ROUTE_AUTO = "route_auto";
String METHOD_OTA = "ota"; String METHOD_OTA = "ota";
// Q20 requests topic method字段值thing/device/{sn}/requests
String METHOD_HANGAR_CHARGE_CONTROL = "hangar_charge_control";
// Q20 缓存key前缀 // Q20 缓存key前缀
String Q20_OSD = "q20_osd_"; String Q20_OSD = "q20_osd_";

View File

@ -0,0 +1,71 @@
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 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}/requests
* <p>method: hangar_charge_control(飞行器请求充电电压电流)
* 收到飞行器充电请求后平台充电器自动回复 requests_reply 主题
* 回传充电器实际可提供的电压/电流当前按请求值原样应答表示同意按需供电
*
* @author 938693313@qq.com
* @since 1.0.0 2026/6/12
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class Q20RequestsHandler 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 requests --> payload解析失败解析后为null");
return;
}
String method = message.getStr(BusinessConstant.METHOD);
if (method == null) {
log.debug("Q20 requests --> method字段缺失, topic: {}", topic);
return;
}
if (Q20Constant.METHOD_HANGAR_CHARGE_CONTROL.equals(method)) {
handleHangarChargeControl(topic, message);
} else {
log.debug("Q20 requests --> 未知method: {}, gateway: {}", method, gateway);
}
}
/**
* 飞行器请求充电自动回复充电器电压/电流
* <p>请求 data: {voltage(请求充电电压V*10), current(请求充电电流A*10)}
* 回复主题 thing/device/{sn}/requests_replydata 回传充电器电压/电流原样应答请求值
*/
private void handleHangarChargeControl(String topic, JSONObject message) {
JSONObject data = message.getJSONObject(BusinessConstant.DATA);
int voltage = data != null ? data.getInt("voltage", 0) : 0;
int current = data != null ? data.getInt("current", 0) : 0;
// 原样应答 tid/bid/method/gatewaydata 回传充电器电压/电流刷新时间戳
JSONObject reply = new JSONObject();
reply.set("voltage", voltage);
reply.set("current", current);
message.set(BusinessConstant.DATA, reply);
message.set("timestamp", System.currentTimeMillis());
String replyTopic = topic + BusinessConstant._REPLY;
mqttPushService.pushMessageByClient1(replyTopic, message.toString());
log.info("Q20 requests --> 飞行器请求充电已自动回复, topic: {}, voltage: {}, current: {}",
replyTopic, voltage, current);
}
}

View File

@ -72,7 +72,6 @@ public class ShiroConfig {
filterMap.put("/swagger-resources/**", "anon"); filterMap.put("/swagger-resources/**", "anon");
filterMap.put("/captcha", "anon"); filterMap.put("/captcha", "anon");
filterMap.put("/", "anon"); filterMap.put("/", "anon");
filterMap.put("/q20-login.html", "anon");
filterMap.put("/q20-ctrl.html", "anon"); filterMap.put("/q20-ctrl.html", "anon");
filterMap.put("/srs/**", "anon"); filterMap.put("/srs/**", "anon");
filterMap.put("/mqtt/auth", "anon"); filterMap.put("/mqtt/auth", "anon");

View File

@ -118,6 +118,30 @@ h6.cmd-title { font-size: 12px; color: #e6edf3; font-weight: 600; margin-bottom:
</head> </head>
<body> <body>
<!-- ===== 登录遮罩(与控制台合并为一页;账号 admin1 / 密码 admin 已内置,仅需验证码) ===== -->
<div id="loginOverlay" style="display:none;position:fixed;inset:0;z-index:3000;background:#0d1117;align-items:center;justify-content:center">
<div class="card p-4" style="max-width:400px;width:100%">
<div class="text-center mb-4">
<i class="bi bi-send-fill" style="font-size:2rem;color:var(--accent)"></i>
<h5 class="mt-2 mb-0" style="color:#e6edf3">Q20 飞行控制台</h5>
<small class="text-muted">Drone Control Panel · 自动登录 admin1</small>
</div>
<div class="mb-3">
<label class="form-label">验证码</label>
<div class="d-flex gap-2">
<input id="loginCaptcha" class="form-control" placeholder="点击图片刷新" autocomplete="off"
onkeydown="if(event.key==='Enter')doAutoLogin()">
<img id="captchaImg" src="" alt="captcha" onclick="refreshCaptcha()" title="点击刷新"
style="cursor:pointer;border:1px solid var(--border-color);border-radius:4px;height:38px">
</div>
</div>
<button class="btn btn-primary w-100" onclick="doAutoLogin()">
<i class="bi bi-box-arrow-in-right me-1"></i>登 录
</button>
<div id="loginErr" class="mt-2 text-danger small" style="display:none"></div>
</div>
</div>
<div style="position:sticky;top:0;z-index:200"> <div style="position:sticky;top:0;z-index:200">
<!-- 顶栏 --> <!-- 顶栏 -->
<div class="topbar d-flex align-items-center gap-3 flex-wrap" style="position:static"> <div class="topbar d-flex align-items-center gap-3 flex-wrap" style="position:static">
@ -1404,13 +1428,75 @@ h6.cmd-title { font-size: 12px; color: #e6edf3; font-weight: 600; margin-bottom:
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<script> <script>
// ==================== 初始化:未登录则跳回登录页 ==================== // ==================== 初始化:未登录则在本页弹出登录遮罩 ====================
// 登录与控制台已合并为同一页面,不再跳转 q20-login.html
const token = sessionStorage.getItem('q20_token'); const token = sessionStorage.getItem('q20_token');
if (!token) { window.location.replace('q20-login.html'); }
const API_BASE = (sessionStorage.getItem('q20_api_base') || '/multictrl').replace(/\/$/, ''); // 所有业务请求地址固定走该 IP 端口
const API_BASE = 'http://223.108.157.174:61620/api';
const username = sessionStorage.getItem('q20_username') || ''; const username = sessionStorage.getItem('q20_username') || '';
// ==================== 内置自动登录(账号/密码写死,仅需验证码) ====================
const AUTO_LOGIN_USER = 'admin1';
const AUTO_LOGIN_PASS = 'admin';
// 验证码 / 登录 接口固定走该 IP 端口(与业务接口 API_BASE 相互独立)
const AUTH_BASE = 'http://223.108.157.174:61620/api';
let loginUuid = '';
function genUuid() {
return crypto.randomUUID ? crypto.randomUUID()
: Math.random().toString(36).slice(2) + Date.now().toString(36);
}
function refreshCaptcha() {
loginUuid = genUuid();
document.getElementById('captchaImg').src =
AUTH_BASE + '/captcha?uuid=' + loginUuid + '&t=' + Date.now();
}
// 显示登录遮罩并加载验证码(未登录时调用)
function showLoginOverlay() {
const ov = document.getElementById('loginOverlay');
ov.style.display = 'flex';
refreshCaptcha();
setTimeout(() => document.getElementById('loginCaptcha').focus(), 50);
}
// 使用内置账号 admin1/admin 自动登录,用户仅需填写验证码
async function doAutoLogin() {
const err = document.getElementById('loginErr');
err.style.display = 'none';
const captcha = document.getElementById('loginCaptcha').value.trim();
if (!captcha) {
err.textContent = '请输入验证码';
err.style.display = '';
return;
}
try {
const res = await fetch(AUTH_BASE + '/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: AUTO_LOGIN_USER, password: AUTO_LOGIN_PASS, captcha, uuid: loginUuid })
});
const json = await res.json();
if (json.code !== 0) {
err.textContent = json.msg || '登录失败';
err.style.display = '';
refreshCaptcha();
document.getElementById('loginCaptcha').value = '';
return;
}
sessionStorage.setItem('q20_token', json.data.token);
sessionStorage.setItem('q20_api_base', API_BASE);
sessionStorage.setItem('q20_username', AUTO_LOGIN_USER);
// 登录成功后重新加载本页,令控制台以已登录状态完整初始化
window.location.reload();
} catch (e) {
err.textContent = '网络错误: ' + e.message;
err.style.display = '';
}
}
let drcTimer = null; let drcTimer = null;
let currentDrcAction = ''; let currentDrcAction = '';
let autoMockEnabled = false; let autoMockEnabled = false;
@ -2215,7 +2301,8 @@ async function doLogout() {
sessionStorage.removeItem('q20_token'); sessionStorage.removeItem('q20_token');
sessionStorage.removeItem('q20_api_base'); sessionStorage.removeItem('q20_api_base');
sessionStorage.removeItem('q20_username'); sessionStorage.removeItem('q20_username');
window.location.href = 'q20-login.html'; // 登录已合并到本页,退出后重新加载即回到登录遮罩
window.location.reload();
} }
// ==================== 命令辅助 ==================== // ==================== 命令辅助 ====================
@ -3363,6 +3450,9 @@ function validateRequiredInCard(btn) {
// ==================== 页面初始化 ==================== // ==================== 页面初始化 ====================
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
// 未登录:显示登录遮罩,登录成功后会 reload 本页再完整初始化
if (!token) { showLoginOverlay(); return; }
document.getElementById('userInfo').textContent = username ? '用户: ' + username : ''; document.getElementById('userInfo').textContent = username ? '用户: ' + username : '';
applyFieldMarkers(); applyFieldMarkers();

View File

@ -1,126 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Q20 登录</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet">
<style>
:root {
--bs-body-bg: #0d1117;
--bs-body-color: #c9d1d9;
--card-bg: #161b22;
--border-color: #30363d;
--accent: #388bfd;
--danger: #f85149;
--muted: #8b949e;
}
body { background: var(--bs-body-bg); color: var(--bs-body-color); font-size: 13px; }
.card { background: var(--card-bg); border: 1px solid var(--border-color); border-radius: 8px; max-width: 400px; width: 100%; }
.form-control { background: #0d1117; border-color: var(--border-color); color: var(--bs-body-color); font-size: 12px; }
.form-control:focus { background: #0d1117; border-color: var(--accent); color: var(--bs-body-color); box-shadow: 0 0 0 2px rgba(56,139,253,.25); }
.form-label { font-size: 11px; color: var(--muted); margin-bottom: 2px; }
.btn-primary { background: var(--accent); border-color: var(--accent); font-size: 12px; }
#captchaImg { cursor: pointer; border: 1px solid var(--border-color); border-radius: 4px; height: 38px; }
</style>
</head>
<body class="d-flex justify-content-center align-items-center" style="min-height:100vh">
<div class="card p-4">
<div class="text-center mb-4">
<i class="bi bi-send-fill" style="font-size:2rem;color:var(--accent)"></i>
<h5 class="mt-2 mb-0" style="color:#e6edf3">Q20 飞行控制台</h5>
<small class="text-muted">Drone Control Panel</small>
</div>
<div class="mb-2">
<label class="form-label">服务地址</label>
<input id="apiBase" class="form-control" value="/multictrl" placeholder="如 http://192.168.1.1:61620/api">
</div>
<div class="mb-2">
<label class="form-label">用户名</label>
<input id="loginUser" class="form-control" placeholder="admin">
</div>
<div class="mb-2">
<label class="form-label">密码</label>
<input id="loginPass" class="form-control" type="password" placeholder="••••••••">
</div>
<div class="mb-3">
<label class="form-label">验证码</label>
<div class="d-flex gap-2">
<input id="loginCaptcha" class="form-control" placeholder="点击图片刷新" onkeydown="if(event.key==='Enter')doLogin()">
<img id="captchaImg" src="" alt="captcha" onclick="refreshCaptcha()" title="点击刷新">
</div>
</div>
<button class="btn btn-primary w-100" onclick="doLogin()">
<i class="bi bi-box-arrow-in-right me-1"></i>登 录
</button>
<div id="loginErr" class="mt-2 text-danger small" style="display:none"></div>
</div>
<script>
let loginUuid = '';
function genUuid() {
return crypto.randomUUID ? crypto.randomUUID()
: Math.random().toString(36).slice(2) + Date.now().toString(36);
}
function baseUrl() {
return (document.getElementById('apiBase').value || '/multictrl').replace(/\/$/, '');
}
function refreshCaptcha() {
loginUuid = genUuid();
document.getElementById('captchaImg').src =
baseUrl() + '/captcha?uuid=' + loginUuid + '&t=' + Date.now();
}
async function doLogin() {
const err = document.getElementById('loginErr');
err.style.display = 'none';
const username = document.getElementById('loginUser').value.trim();
const password = document.getElementById('loginPass').value;
const captcha = document.getElementById('loginCaptcha').value.trim();
if (!username || !password || !captcha) {
err.textContent = '请填写完整信息';
err.style.display = '';
return;
}
try {
const res = await fetch(baseUrl() + '/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password, captcha, uuid: loginUuid })
});
const json = await res.json();
if (json.code !== 0) {
err.textContent = json.msg || '登录失败';
err.style.display = '';
refreshCaptcha();
document.getElementById('loginCaptcha').value = '';
return;
}
sessionStorage.setItem('q20_token', json.data.token);
sessionStorage.setItem('q20_api_base', baseUrl());
sessionStorage.setItem('q20_username', username);
window.location.href = 'q20-ctrl.html';
} catch (e) {
err.textContent = '网络错误: ' + e.message;
err.style.display = '';
}
}
document.addEventListener('DOMContentLoaded', () => {
// 已登录则直接跳控制台
if (sessionStorage.getItem('q20_token')) {
window.location.href = 'q20-ctrl.html';
return;
}
refreshCaptcha();
});
</script>
</body>
</html>