Files
park-desktop/src/utils/mqtt.js
T

123 lines
4.0 KiB
JavaScript
Raw Normal View History

import { useState, useEffect } from 'react';
import mqtt from 'mqtt';
import { getMqttUrl, getMqttCredentials, getDeviceId, MQTT_TOPIC_COMMAND, MQTT_TOPIC_TICK, MQTT_TOPIC_BIND } from '../config';
import { getTenantId } from './tenant';
/* =========================================================
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 作为 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 });
});
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;
}