87e57ea486
- mqtt.js:connect 后立即+每10s 上报 opc/display/heartbeat(client_id/tenant_id/page), 绑定页(BindScreen)不再只连不报——admin 端屏幕管理即可看到在线大屏; 断线 close 时清定时器,重连后自动恢复 - useMqttControl.js:移除重复心跳(避免双发),保留注册/状态上报/订阅逻辑
148 lines
5.0 KiB
JavaScript
148 lines
5.0 KiB
JavaScript
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;
|
||
}
|