28e233520b
- 路由:数据大屏/数字孪生/AI/媒体轮播/语音/企业墙 - 多园区通用,园区账号登录显示本园区
117 lines
3.7 KiB
JavaScript
117 lines
3.7 KiB
JavaScript
import { useState, useEffect } from 'react';
|
||
import mqtt from 'mqtt';
|
||
import { getMqttUrl, getMqttCredentials, MQTT_TOPIC_COMMAND, MQTT_TOPIC_TICK } from '../config';
|
||
|
||
/* =========================================================
|
||
MQTT 客户端(mqtt.js)—— 后端控制通道
|
||
订阅 opc/display/command(页面/媒体/卡片控制)与 opc/dashboard/tick(数据快照)
|
||
断线自动重连;未连接时静默降级(页面仍可本地操作)
|
||
========================================================= */
|
||
|
||
let client = null;
|
||
let status = 'connecting'; // connecting | connected | offline
|
||
let clientId = null;
|
||
const listeners = new Set();
|
||
|
||
function notifyStatus() {
|
||
window.dispatchEvent(new CustomEvent('dpm:mqtt-status', { detail: { status } }));
|
||
}
|
||
|
||
/* 每个连接生成全局唯一 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:避免多屏/多实例/多标签页同 ID 被 broker 互相踢下线
|
||
clientId = makeClientId();
|
||
// 连接时惰性读取 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();
|
||
// 客户端只监听自己唯一的频道 opc/display/command/<clientId>(+ 数据 tick)。
|
||
// 不再订阅全局命令频道 → 后端按客户端路由时,他屏的指令绝收不到。
|
||
client.subscribe([MQTT_TOPIC_TICK, `${MQTT_TOPIC_COMMAND}/${clientId}`], { qos: 1 });
|
||
});
|
||
client.on('reconnect', () => {
|
||
status = 'connecting';
|
||
notifyStatus();
|
||
});
|
||
client.on('close', () => {
|
||
status = 'offline';
|
||
notifyStatus();
|
||
});
|
||
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;
|
||
}
|