Files
DPM/backend/app/mqtt.py
T
Pine 07dbd404a9 refactor: 弱化媒体播放控制区 + MQTT 客户端 ID 唯一化
播放控制区:
- 从底部中央移到右下角,整体缩小(播放键 42→30px、导航键 34→26px)
- 背景更透(0.8→0.55)、阴影更轻,视觉弱化不抢内容
- 播放名不再显示完整地址:URL 媒体只显示文件名(cleanName 清洗,去掉协议/路径/查询)

MQTT 客户端 ID:
- 后端 client_id 由固定 dpm-backend 改为 dpm-backend-{pid}-{随机},多实例不互踢
- 前端 client_id 增加时间戳 + 更高熵随机,避免多屏/多标签竞争
2026-08-17 22:16:22 +08:00

151 lines
5.3 KiB
Python
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.
# -*- coding: utf-8 -*-
"""MQTT 发布/订阅中心 —— 后端 ↔ 前端控制通道
发布:opc/display/command(页面/媒体/卡片控制)、opc/dashboard/tick(数据快照)
订阅:opc/display/heartbeat(大屏在线心跳,统计在线大屏数)
"""
import json
import logging
import os
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]
def _unique_client_id(base):
"""每个客户端进程使用唯一 client_id,避免与其它实例竞争被 broker 踢下线"""
return f"{base}-{os.getpid()}-{uuid.uuid4().hex[:6]}"
class MqttHub:
def __init__(self):
self.client = None
self.connected = False
self._lock = threading.RLock()
self._screens = {} # client_id -> last_seen_ts
self.last_command = None # {action, params, ts, published}
# ---------- 生命周期 ----------
def start(self):
if not settings.MQTT_ENABLED:
log.info("MQTT 已禁用(DPM_MQTT_ENABLED=0")
return
try:
self.client = mqtt.Client(
client_id=_unique_client_id(settings.MQTT_CLIENT_ID),
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
self.client.on_message = self._on_message
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)
client.subscribe(settings.TOPIC_HEARTBEAT, qos=0)
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)
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,
}
# ---------- 发布 ----------
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
with self._lock:
self.last_command = {
"action": action,
"params": params or {},
"ts": payload["ts"],
"published": ok,
}
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()