fix(mqtt): MQTT 连接成功后自动周期上报心跳(绑定页/主屏通用)
- mqtt.js:connect 后立即+每10s 上报 opc/display/heartbeat(client_id/tenant_id/page), 绑定页(BindScreen)不再只连不报——admin 端屏幕管理即可看到在线大屏; 断线 close 时清定时器,重连后自动恢复 - useMqttControl.js:移除重复心跳(避免双发),保留注册/状态上报/订阅逻辑
This commit is contained in:
+26
-1
@@ -1,23 +1,42 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import mqtt from 'mqtt';
|
import mqtt from 'mqtt';
|
||||||
import { getMqttUrl, getMqttCredentials, getDeviceId, MQTT_TOPIC_COMMAND, MQTT_TOPIC_TICK, MQTT_TOPIC_BIND } from '../config';
|
import { getMqttUrl, getMqttCredentials, getDeviceId, MQTT_TOPIC_COMMAND, MQTT_TOPIC_TICK, MQTT_TOPIC_BIND, MQTT_TOPIC_HEARTBEAT } from '../config';
|
||||||
import { getTenantId } from './tenant';
|
import { getTenantId } from './tenant';
|
||||||
|
|
||||||
/* =========================================================
|
/* =========================================================
|
||||||
MQTT 客户端(mqtt.js)—— 后端控制通道
|
MQTT 客户端(mqtt.js)—— 后端控制通道
|
||||||
订阅 opc/display/command(页面/媒体/卡片控制)与 opc/dashboard/tick(数据快照)
|
订阅 opc/display/command(页面/媒体/卡片控制)与 opc/dashboard/tick(数据快照)
|
||||||
|
连接成功后自动周期上报心跳 opc/display/heartbeat(绑定页/主屏通用,
|
||||||
|
后端据此统计在线大屏,admin 端屏幕管理显示在线状态)
|
||||||
断线自动重连;未连接时静默降级(页面仍可本地操作)
|
断线自动重连;未连接时静默降级(页面仍可本地操作)
|
||||||
========================================================= */
|
========================================================= */
|
||||||
|
|
||||||
|
const HEARTBEAT_MS = 10000;
|
||||||
|
|
||||||
let client = null;
|
let client = null;
|
||||||
let status = 'connecting'; // connecting | connected | offline
|
let status = 'connecting'; // connecting | connected | offline
|
||||||
let clientId = null;
|
let clientId = null;
|
||||||
|
let heartbeatTimer = null;
|
||||||
const listeners = new Set();
|
const listeners = new Set();
|
||||||
|
|
||||||
function notifyStatus() {
|
function notifyStatus() {
|
||||||
window.dispatchEvent(new CustomEvent('dpm:mqtt-status', { detail: { status } }));
|
window.dispatchEvent(new CustomEvent('dpm:mqtt-status', { detail: { status } }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 心跳上报(连接成功后自动周期调用;绑定页/主屏均生效) */
|
||||||
|
function sendHeartbeat() {
|
||||||
|
if (status !== 'connected' || !client) return;
|
||||||
|
try {
|
||||||
|
const page = (typeof window !== 'undefined' && window.location) ? window.location.pathname : '';
|
||||||
|
client.publish(MQTT_TOPIC_HEARTBEAT, JSON.stringify({
|
||||||
|
client_id: getClientId() || getDeviceId(),
|
||||||
|
tenant_id: getTenantId(),
|
||||||
|
page,
|
||||||
|
ts: Date.now(),
|
||||||
|
}), { qos: 1 });
|
||||||
|
} catch { /* 忽略单次失败 */ }
|
||||||
|
}
|
||||||
|
|
||||||
/* 每个连接生成全局唯一 client_id(独立客户端 ID 铁律):
|
/* 每个连接生成全局唯一 client_id(独立客户端 ID 铁律):
|
||||||
优先 crypto.randomUUID(WebView2/现代浏览器),回退时间戳+双段高熵随机 */
|
优先 crypto.randomUUID(WebView2/现代浏览器),回退时间戳+双段高熵随机 */
|
||||||
function makeClientId() {
|
function makeClientId() {
|
||||||
@@ -55,6 +74,11 @@ export function connectMqtt() {
|
|||||||
tid ? `${MQTT_TOPIC_TICK}/${tid}` : MQTT_TOPIC_TICK,
|
tid ? `${MQTT_TOPIC_TICK}/${tid}` : MQTT_TOPIC_TICK,
|
||||||
tid ? `${MQTT_TOPIC_COMMAND}/${tid}/${clientId}` : `${MQTT_TOPIC_COMMAND}/${clientId}`,
|
tid ? `${MQTT_TOPIC_COMMAND}/${tid}/${clientId}` : `${MQTT_TOPIC_COMMAND}/${clientId}`,
|
||||||
], { qos: 1 });
|
], { qos: 1 });
|
||||||
|
// 连接成功即启动心跳(绑定页/主屏通用;幂等防重复定时器)
|
||||||
|
if (!heartbeatTimer) {
|
||||||
|
sendHeartbeat();
|
||||||
|
heartbeatTimer = setInterval(sendHeartbeat, HEARTBEAT_MS);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
client.on('reconnect', () => {
|
client.on('reconnect', () => {
|
||||||
status = 'connecting';
|
status = 'connecting';
|
||||||
@@ -63,6 +87,7 @@ export function connectMqtt() {
|
|||||||
client.on('close', () => {
|
client.on('close', () => {
|
||||||
status = 'offline';
|
status = 'offline';
|
||||||
notifyStatus();
|
notifyStatus();
|
||||||
|
if (heartbeatTimer) { clearInterval(heartbeatTimer); heartbeatTimer = null; }
|
||||||
});
|
});
|
||||||
client.on('error', () => {
|
client.on('error', () => {
|
||||||
status = 'offline';
|
status = 'offline';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useNavigate, useLocation } from 'react-router-dom';
|
import { useNavigate, useLocation } from 'react-router-dom';
|
||||||
import { onMqttMessage, connectMqtt, publishMqtt, getClientId, useMqttStatus } from './mqtt';
|
import { onMqttMessage, connectMqtt, publishMqtt, getClientId, useMqttStatus } from './mqtt';
|
||||||
import { MQTT_TOPIC_HEARTBEAT, MQTT_TOPIC_COMMAND, MQTT_TOPIC_BIND, getApiBase as API_BASE } from '../config';
|
import { MQTT_TOPIC_COMMAND, MQTT_TOPIC_BIND, getApiBase as API_BASE } from '../config';
|
||||||
import { getTenantId, clearBinding } from './tenant';
|
import { getTenantId, clearBinding } from './tenant';
|
||||||
import { Sfx } from './sounds';
|
import { Sfx } from './sounds';
|
||||||
import { PAGE_ORDER } from './pageNav';
|
import { PAGE_ORDER } from './pageNav';
|
||||||
@@ -75,23 +75,7 @@ export function useMqttControl() {
|
|||||||
return off;
|
return off;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 大屏在线心跳(后端据此统计在线大屏数)
|
// 大屏在线心跳已由 mqtt.js 连接成功后自动周期上报(绑定页/主屏通用),此处不再重复发送
|
||||||
useEffect(() => {
|
|
||||||
connectMqtt();
|
|
||||||
const sendHeartbeat = () => {
|
|
||||||
if (mqttStatus === 'connected') {
|
|
||||||
publishMqtt(MQTT_TOPIC_HEARTBEAT, {
|
|
||||||
client_id: getClientId(),
|
|
||||||
tenant_id: getTenantId(),
|
|
||||||
page: location.pathname,
|
|
||||||
ts: Date.now(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
sendHeartbeat();
|
|
||||||
const hb = setInterval(sendHeartbeat, 10000);
|
|
||||||
return () => clearInterval(hb);
|
|
||||||
}, [mqttStatus, location.pathname]);
|
|
||||||
|
|
||||||
// 页面状态即时上报:路由变化即通知服务端当前所在页面
|
// 页面状态即时上报:路由变化即通知服务端当前所在页面
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user