Files
park-desktop/src/utils/mqtt.js
T
Pine 28e233520b feat: 园区大屏前端(React+Vite+Tailwind+three.js+MQTT)
- 路由:数据大屏/数字孪生/AI/媒体轮播/语音/企业墙
- 多园区通用,园区账号登录显示本园区
2026-08-23 22:34:45 +08:00

117 lines
3.7 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, 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;
}