2026-08-23 22:34:45 +08:00
|
|
|
|
import { useState, useEffect } from 'react';
|
|
|
|
|
|
import mqtt from 'mqtt';
|
2026-08-24 20:14:15 +08:00
|
|
|
|
import { getMqttUrl, getMqttCredentials, getDeviceId, MQTT_TOPIC_COMMAND, MQTT_TOPIC_TICK, MQTT_TOPIC_BIND } from '../config';
|
2026-08-24 18:15:11 +08:00
|
|
|
|
import { getTenantId } from './tenant';
|
2026-08-23 22:34:45 +08:00
|
|
|
|
|
|
|
|
|
|
/* =========================================================
|
|
|
|
|
|
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 {
|
2026-08-24 20:14:15 +08:00
|
|
|
|
// 永久设备 ID 作为 MQTT 客户端名(稳定、可被园区端精准控制)
|
|
|
|
|
|
clientId = getDeviceId();
|
2026-08-23 22:34:45 +08:00
|
|
|
|
// 连接时惰性读取 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();
|
2026-08-24 20:14:15 +08:00
|
|
|
|
// 多租户:只监听本园区频道 command/<tenant_id>/<clientId>(+ tick/<tenant_id>);
|
|
|
|
|
|
// 未绑定时仍订阅 绑定频道 bind/<clientId>(设备绑定确认),并接收全局 tick 回退。
|
2026-08-24 18:15:11 +08:00
|
|
|
|
const tid = getTenantId();
|
|
|
|
|
|
client.subscribe([
|
2026-08-24 20:14:15 +08:00
|
|
|
|
`${MQTT_TOPIC_BIND}/${clientId}`, // 绑定确认(未绑定/重绑)
|
2026-08-24 18:15:11 +08:00
|
|
|
|
tid ? `${MQTT_TOPIC_TICK}/${tid}` : MQTT_TOPIC_TICK,
|
|
|
|
|
|
tid ? `${MQTT_TOPIC_COMMAND}/${tid}/${clientId}` : `${MQTT_TOPIC_COMMAND}/${clientId}`,
|
|
|
|
|
|
], { qos: 1 });
|
2026-08-23 22:34:45 +08:00
|
|
|
|
});
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|