Files
park-desktop/src/utils/mqtt.js
T
Pine 87e57ea486 fix(mqtt): MQTT 连接成功后自动周期上报心跳(绑定页/主屏通用)
- mqtt.js:connect 后立即+每10s 上报 opc/display/heartbeat(client_id/tenant_id/page),
  绑定页(BindScreen)不再只连不报——admin 端屏幕管理即可看到在线大屏;
  断线 close 时清定时器,重连后自动恢复
- useMqttControl.js:移除重复心跳(避免双发),保留注册/状态上报/订阅逻辑
2026-09-16 22:34:28 +08:00

148 lines
5.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect } from 'react';
import mqtt from 'mqtt';
import { getMqttUrl, getMqttCredentials, getDeviceId, MQTT_TOPIC_COMMAND, MQTT_TOPIC_TICK, MQTT_TOPIC_BIND, MQTT_TOPIC_HEARTBEAT } from '../config';
import { getTenantId } from './tenant';
/* =========================================================
MQTT 客户端(mqtt.js)—— 后端控制通道
订阅 opc/display/command(页面/媒体/卡片控制)与 opc/dashboard/tick(数据快照)
连接成功后自动周期上报心跳 opc/display/heartbeat(绑定页/主屏通用,
后端据此统计在线大屏,admin 端屏幕管理显示在线状态)
断线自动重连;未连接时静默降级(页面仍可本地操作)
========================================================= */
const HEARTBEAT_MS = 10000;
let client = null;
let status = 'connecting'; // connecting | connected | offline
let clientId = null;
let heartbeatTimer = null;
const listeners = new Set();
function notifyStatus() {
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 铁律):
优先 crypto.randomUUID(WebView2/现代浏览器),回退时间戳+双段高熵随机 */
function makeClientId() {
try {
if (window.crypto && typeof window.crypto.randomUUID === 'function') {
return `dpm-screen-${crypto.randomUUID().replace(/-/g, '')}`;
}
} catch { /* 继续回退 */ }
return `dpm-screen-${Date.now().toString(36)}-${Math.random().toString(16).slice(2, 12)}-${Math.random().toString(16).slice(2, 10)}`;
}
export function connectMqtt() {
if (client) return;
try {
// 永久设备 ID 作为 MQTT 客户端名(稳定、可被园区端精准控制)
clientId = getDeviceId();
// 连接时惰性读取 MQTT 地址/账号:打包部署时 main.jsx 引导已从后端 /api/config 覆盖
const { username, password } = getMqttCredentials();
client = mqtt.connect(getMqttUrl(), {
reconnectPeriod: 3000,
connectTimeout: 8000,
clientId,
clean: true,
username: username || undefined,
password: password || undefined,
});
client.on('connect', () => {
status = 'connected';
notifyStatus();
// 多租户:只监听本园区频道 command/<tenant_id>/<clientId>(+ tick/<tenant_id>);
// 未绑定时仍订阅 绑定频道 bind/<clientId>(设备绑定确认),并接收全局 tick 回退。
const tid = getTenantId();
client.subscribe([
`${MQTT_TOPIC_BIND}/${clientId}`, // 绑定确认(未绑定/重绑)
tid ? `${MQTT_TOPIC_TICK}/${tid}` : MQTT_TOPIC_TICK,
tid ? `${MQTT_TOPIC_COMMAND}/${tid}/${clientId}` : `${MQTT_TOPIC_COMMAND}/${clientId}`,
], { qos: 1 });
// 连接成功即启动心跳(绑定页/主屏通用;幂等防重复定时器)
if (!heartbeatTimer) {
sendHeartbeat();
heartbeatTimer = setInterval(sendHeartbeat, HEARTBEAT_MS);
}
});
client.on('reconnect', () => {
status = 'connecting';
notifyStatus();
});
client.on('close', () => {
status = 'offline';
notifyStatus();
if (heartbeatTimer) { clearInterval(heartbeatTimer); heartbeatTimer = null; }
});
client.on('error', () => {
status = 'offline';
notifyStatus();
});
client.on('message', (topic, payload) => {
let data = null;
try {
data = JSON.parse(payload.toString());
} catch {
return;
}
listeners.forEach((fn) => fn(topic, data));
});
} catch (e) {
status = 'offline';
notifyStatus();
}
}
export function getMqttStatus() {
return status;
}
/** 当前连接的稳定 client_id */
export function getClientId() {
return clientId;
}
/** 发布消息(大屏心跳等) */
export function publishMqtt(topic, payload) {
if (!client || status !== 'connected') return false;
try {
client.publish(topic, JSON.stringify(payload), { qos: 1 });
return true;
} catch (e) {
return false;
}
}
/** 订阅 MQTT 消息,返回取消订阅函数 */
export function onMqttMessage(fn) {
listeners.add(fn);
return () => listeners.delete(fn);
}
/** React Hook:MQTT 连接状态 */
export function useMqttStatus() {
const [s, setS] = useState(status);
useEffect(() => {
connectMqtt();
const on = (e) => setS(e.detail.status);
window.addEventListener('dpm:mqtt-status', on);
return () => window.removeEventListener('dpm:mqtt-status', on);
}, []);
return s;
}