2026-08-17 21:25:46 +08:00
|
|
|
|
# -*- coding: utf-8 -*-
|
2026-08-17 21:54:23 +08:00
|
|
|
|
"""MQTT 发布/订阅中心 —— 后端 ↔ 前端控制通道
|
|
|
|
|
|
发布:opc/display/command(页面/媒体/卡片控制)、opc/dashboard/tick(数据快照)
|
|
|
|
|
|
订阅:opc/display/heartbeat(大屏在线心跳,统计在线大屏数)
|
2026-08-17 21:25:46 +08:00
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
import json
|
|
|
|
|
|
import logging
|
2026-08-17 22:16:22 +08:00
|
|
|
|
import os
|
2026-08-17 21:25:46 +08:00
|
|
|
|
import threading
|
|
|
|
|
|
import time
|
|
|
|
|
|
import uuid
|
|
|
|
|
|
|
|
|
|
|
|
import paho.mqtt.client as mqtt
|
|
|
|
|
|
|
|
|
|
|
|
from .config import settings
|
|
|
|
|
|
from .event_bus import bus
|
|
|
|
|
|
|
|
|
|
|
|
log = logging.getLogger("dpm.mqtt")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def new_cmd_id():
|
|
|
|
|
|
return uuid.uuid4().hex[:12]
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-17 22:16:22 +08:00
|
|
|
|
def _unique_client_id(base):
|
|
|
|
|
|
"""每个客户端进程使用唯一 client_id,避免与其它实例竞争被 broker 踢下线"""
|
|
|
|
|
|
return f"{base}-{os.getpid()}-{uuid.uuid4().hex[:6]}"
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-17 21:25:46 +08:00
|
|
|
|
class MqttHub:
|
|
|
|
|
|
def __init__(self):
|
|
|
|
|
|
self.client = None
|
|
|
|
|
|
self.connected = False
|
|
|
|
|
|
self._lock = threading.RLock()
|
2026-08-17 21:54:23 +08:00
|
|
|
|
self._screens = {} # client_id -> last_seen_ts
|
|
|
|
|
|
self.last_command = None # {action, params, ts, published}
|
2026-08-17 21:25:46 +08:00
|
|
|
|
|
|
|
|
|
|
# ---------- 生命周期 ----------
|
|
|
|
|
|
def start(self):
|
|
|
|
|
|
if not settings.MQTT_ENABLED:
|
|
|
|
|
|
log.info("MQTT 已禁用(DPM_MQTT_ENABLED=0)")
|
|
|
|
|
|
return
|
|
|
|
|
|
try:
|
|
|
|
|
|
self.client = mqtt.Client(
|
2026-08-17 22:16:22 +08:00
|
|
|
|
client_id=_unique_client_id(settings.MQTT_CLIENT_ID),
|
2026-08-17 21:25:46 +08:00
|
|
|
|
clean_session=True,
|
|
|
|
|
|
protocol=mqtt.MQTTv311,
|
|
|
|
|
|
)
|
|
|
|
|
|
if settings.MQTT_USERNAME:
|
|
|
|
|
|
self.client.username_pw_set(settings.MQTT_USERNAME, settings.MQTT_PASSWORD)
|
|
|
|
|
|
self.client.on_connect = self._on_connect
|
|
|
|
|
|
self.client.on_disconnect = self._on_disconnect
|
2026-08-17 21:54:23 +08:00
|
|
|
|
self.client.on_message = self._on_message
|
2026-08-17 21:25:46 +08:00
|
|
|
|
self.client.connect_async(settings.MQTT_HOST, settings.MQTT_PORT, keepalive=30)
|
|
|
|
|
|
self.client.loop_start()
|
|
|
|
|
|
log.info("MQTT 连接中 %s:%s ...", settings.MQTT_HOST, settings.MQTT_PORT)
|
|
|
|
|
|
except Exception as e: # noqa: BLE001
|
|
|
|
|
|
log.warning("MQTT 初始化失败(数据 API 不受影响): %s", e)
|
|
|
|
|
|
|
|
|
|
|
|
def stop(self):
|
|
|
|
|
|
if self.client:
|
|
|
|
|
|
try:
|
|
|
|
|
|
self.client.loop_stop()
|
|
|
|
|
|
self.client.disconnect()
|
|
|
|
|
|
except Exception: # noqa: BLE001
|
|
|
|
|
|
pass
|
|
|
|
|
|
self.client = None
|
|
|
|
|
|
|
|
|
|
|
|
def _on_connect(self, client, userdata, flags, rc):
|
|
|
|
|
|
if rc == 0:
|
|
|
|
|
|
self.connected = True
|
|
|
|
|
|
log.info("MQTT 已连接 %s:%s", settings.MQTT_HOST, settings.MQTT_PORT)
|
2026-08-17 21:54:23 +08:00
|
|
|
|
client.subscribe(settings.TOPIC_HEARTBEAT, qos=0)
|
2026-08-17 21:25:46 +08:00
|
|
|
|
else:
|
|
|
|
|
|
log.warning("MQTT 连接失败 rc=%s", rc)
|
|
|
|
|
|
|
|
|
|
|
|
def _on_disconnect(self, client, userdata, rc):
|
|
|
|
|
|
self.connected = False
|
|
|
|
|
|
if rc != 0:
|
|
|
|
|
|
log.warning("MQTT 断开(rc=%s),自动重连中...", rc)
|
|
|
|
|
|
|
2026-08-17 21:54:23 +08:00
|
|
|
|
def _on_message(self, client, userdata, msg):
|
|
|
|
|
|
"""接收大屏心跳:记录 client_id 与时间"""
|
|
|
|
|
|
if msg.topic == settings.TOPIC_HEARTBEAT:
|
|
|
|
|
|
try:
|
|
|
|
|
|
payload = json.loads(msg.payload.decode("utf-8"))
|
|
|
|
|
|
cid = payload.get("client_id") or msg.topic
|
|
|
|
|
|
with self._lock:
|
|
|
|
|
|
self._screens[cid] = time.time()
|
|
|
|
|
|
# 清理过期
|
|
|
|
|
|
cutoff = time.time() - settings.SCREEN_TTL
|
|
|
|
|
|
self._screens = {k: v for k, v in self._screens.items() if v > cutoff}
|
|
|
|
|
|
except Exception: # noqa: BLE001
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
# ---------- 状态查询 ----------
|
|
|
|
|
|
def screens_online(self):
|
|
|
|
|
|
with self._lock:
|
|
|
|
|
|
cutoff = time.time() - settings.SCREEN_TTL
|
|
|
|
|
|
return sum(1 for v in self._screens.values() if v > cutoff)
|
|
|
|
|
|
|
|
|
|
|
|
def status(self):
|
|
|
|
|
|
return {
|
|
|
|
|
|
"mqtt_connected": self.connected,
|
|
|
|
|
|
"mqtt_host": f"{settings.MQTT_HOST}:{settings.MQTT_PORT}",
|
|
|
|
|
|
"screens_online": self.screens_online(),
|
|
|
|
|
|
"last_command": self.last_command,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-17 21:25:46 +08:00
|
|
|
|
# ---------- 发布 ----------
|
|
|
|
|
|
def publish(self, topic, payload, qos=1, retain=False):
|
|
|
|
|
|
if not self.client or not self.connected:
|
|
|
|
|
|
log.debug("MQTT 未连接,丢弃发布 %s", topic)
|
|
|
|
|
|
return False
|
|
|
|
|
|
try:
|
|
|
|
|
|
info = self.client.publish(topic, json.dumps(payload, ensure_ascii=False), qos=qos, retain=retain)
|
|
|
|
|
|
return info.rc == mqtt.MQTT_ERR_SUCCESS
|
|
|
|
|
|
except Exception as e: # noqa: BLE001
|
|
|
|
|
|
log.warning("MQTT 发布失败: %s", e)
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
def publish_command(self, action, params=None):
|
|
|
|
|
|
"""统一指令信封:{cmd_id, ts, action, params, published}
|
|
|
|
|
|
MQTT 广播 + SSE 兼容推送;published 标记是否真正发布成功"""
|
|
|
|
|
|
payload = {
|
|
|
|
|
|
"cmd_id": new_cmd_id(),
|
|
|
|
|
|
"ts": int(time.time() * 1000),
|
|
|
|
|
|
"action": action,
|
|
|
|
|
|
"params": params or {},
|
|
|
|
|
|
}
|
|
|
|
|
|
ok = self.publish(settings.TOPIC_COMMAND, payload)
|
|
|
|
|
|
payload["published"] = ok
|
2026-08-17 21:54:23 +08:00
|
|
|
|
with self._lock:
|
|
|
|
|
|
self.last_command = {
|
|
|
|
|
|
"action": action,
|
|
|
|
|
|
"params": params or {},
|
|
|
|
|
|
"ts": payload["ts"],
|
|
|
|
|
|
"published": ok,
|
|
|
|
|
|
}
|
2026-08-17 21:25:46 +08:00
|
|
|
|
if ok:
|
|
|
|
|
|
bus.emit(payload) # SSE 兼容通道(仅发布成功时推送)
|
|
|
|
|
|
return payload
|
|
|
|
|
|
|
|
|
|
|
|
def publish_tick(self, snapshot):
|
|
|
|
|
|
payload = {"ts": int(time.time() * 1000), "snapshot": snapshot}
|
|
|
|
|
|
self.publish(settings.TOPIC_TICK, payload, qos=0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
hub = MqttHub()
|