- 后端 prrack:MQTT(phonerail-backend @ 127.0.0.1:1884)↔ 遥测入库 / 事件增量同步 / WebSocket 推送; 命令白名单 + 重传同 seq;审计记门户 X-Auth-User(只绑 127.0.0.1,main.py 拒绝非回环)。 - 自动恢复:只看电流(平线 / 过低,服务器够不到手机 adb),先长按电源再断电重启, 每小时限次后停手;默认逐台关闭。adb 探活接口 recovery.Prober 预留。 - 前端:总览 / 板子页(五路操作、电流电压曲线)/ 事件 / 操作记录 / 自动恢复;手机宽度可用。 - 部署 deploy/deploy.sh:服务器 Python 3.8 上先跑测试再替换;systemd phonerail-rack,每日备份。 - 门户应用 id 16、key rack(phonerail 已被需求文档站占用),授权 admin + staff。 测试 26 例(本机 3.14 / 服务器 3.8 均过)。线上板 b81f3fc9c8bc 在线,WS 经门户 101 + 1 条/s 遥测。 Co-Authored-By: Claude Opus 5.5 <[email protected]>
138 行
4.8 KiB
Python
138 行
4.8 KiB
Python
"""MQTT 总线的薄抽象:真实的 paho,和测试 / 本机开发用的进程内假 broker。
|
||
|
||
Hub 只认三件事:`publish(topic, payload, qos, retain)`、`subscribe(pattern)`、
|
||
收到消息时回调 `on_message(topic, payload_bytes)`。回调**可能在任意线程**里被调
|
||
(paho 的网络线程),Hub 自己负责把它搬回 asyncio 循环。
|
||
"""
|
||
import logging
|
||
import threading
|
||
from typing import Callable, Dict, List, Optional, Tuple
|
||
|
||
log = logging.getLogger("prrack.bus")
|
||
|
||
OnMessage = Callable[[str, bytes], None]
|
||
|
||
|
||
def topic_matches(pattern: str, topic: str) -> bool:
|
||
"""MQTT 通配:`+` 一级,`#` 余下全部。"""
|
||
p, t = pattern.split("/"), topic.split("/")
|
||
for i, seg in enumerate(p):
|
||
if seg == "#":
|
||
return True
|
||
if i >= len(t) or (seg != "+" and seg != t[i]):
|
||
return False
|
||
return len(p) == len(t)
|
||
|
||
|
||
class Bus:
|
||
on_message: Optional[OnMessage] = None
|
||
on_connect: Optional[Callable[[bool], None]] = None
|
||
|
||
def start(self):
|
||
raise NotImplementedError
|
||
|
||
def stop(self):
|
||
raise NotImplementedError
|
||
|
||
def publish(self, topic: str, payload: str, qos: int = 1, retain: bool = False):
|
||
raise NotImplementedError
|
||
|
||
def subscribe(self, pattern: str, qos: int = 1):
|
||
raise NotImplementedError
|
||
|
||
|
||
class PahoBus(Bus):
|
||
def __init__(self, host: str, port: int, user: str, password: str, client_id: str):
|
||
import paho.mqtt.client as mqtt # 只在真连 broker 时才需要
|
||
|
||
self._subs: List[Tuple[str, int]] = []
|
||
self._c = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=client_id,
|
||
clean_session=True)
|
||
self._c.username_pw_set(user, password)
|
||
self._c.reconnect_delay_set(1, 30)
|
||
self._c.on_connect = self._on_connect
|
||
self._c.on_disconnect = self._on_disconnect
|
||
self._c.on_message = lambda c, u, m: self.on_message and self.on_message(m.topic, m.payload)
|
||
self._host, self._port = host, port
|
||
|
||
def _on_connect(self, c, userdata, flags, reason, props=None):
|
||
ok = not reason.is_failure
|
||
log.info("MQTT %s(%s)", "已连上" if ok else "连接被拒", reason)
|
||
if ok:
|
||
# clean_session:每次重连都要重新订阅,否则断一次线就再也收不到东西
|
||
for pat, qos in self._subs:
|
||
c.subscribe(pat, qos)
|
||
if self.on_connect:
|
||
self.on_connect(ok)
|
||
|
||
def _on_disconnect(self, c, userdata, flags, reason, props=None):
|
||
log.warning("MQTT 断开(%s),paho 会自动重连", reason)
|
||
if self.on_connect:
|
||
self.on_connect(False)
|
||
|
||
def start(self):
|
||
self._c.connect_async(self._host, self._port, keepalive=30)
|
||
self._c.loop_start()
|
||
|
||
def stop(self):
|
||
self._c.loop_stop()
|
||
self._c.disconnect()
|
||
|
||
def publish(self, topic: str, payload: str, qos: int = 1, retain: bool = False):
|
||
self._c.publish(topic, payload, qos=qos, retain=retain)
|
||
|
||
def subscribe(self, pattern: str, qos: int = 1):
|
||
self._subs.append((pattern, qos))
|
||
self._c.subscribe(pattern, qos)
|
||
|
||
|
||
class MemBroker:
|
||
"""进程内的假 broker:多个 MemBus 挂在同一个上面互相收发,支持 retained。
|
||
投递是**同步**的(publish 返回前订阅者已经收到),测试因此是确定的。"""
|
||
|
||
def __init__(self):
|
||
self._lock = threading.RLock()
|
||
self.clients: List["MemBus"] = []
|
||
self.retained: Dict[str, str] = {}
|
||
self.log: List[Tuple[str, str]] = []
|
||
|
||
def deliver(self, topic: str, payload: str, retain: bool):
|
||
with self._lock:
|
||
self.log.append((topic, payload))
|
||
if retain:
|
||
self.retained[topic] = payload
|
||
targets = [c for c in self.clients if c.wants(topic)]
|
||
for c in targets:
|
||
if c.on_message:
|
||
c.on_message(topic, payload.encode())
|
||
|
||
|
||
class MemBus(Bus):
|
||
def __init__(self, broker: MemBroker):
|
||
self.broker = broker
|
||
self.subs: List[str] = []
|
||
self.up = False
|
||
|
||
def wants(self, topic: str) -> bool:
|
||
return self.up and any(topic_matches(p, topic) for p in self.subs)
|
||
|
||
def start(self):
|
||
self.up = True
|
||
self.broker.clients.append(self)
|
||
if self.on_connect:
|
||
self.on_connect(True)
|
||
|
||
def stop(self):
|
||
self.up = False
|
||
if self in self.broker.clients:
|
||
self.broker.clients.remove(self)
|
||
|
||
def publish(self, topic: str, payload: str, qos: int = 1, retain: bool = False):
|
||
self.broker.deliver(topic, payload, retain)
|
||
|
||
def subscribe(self, pattern: str, qos: int = 1):
|
||
self.subs.append(pattern)
|
||
for t, p in list(self.broker.retained.items()):
|
||
if topic_matches(pattern, t) and self.on_message:
|
||
self.on_message(t, p.encode())
|