机架管理前后端(rack/):FastAPI + SQLite + Vue3,经统一门户上线

- 后端 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]>
这个提交包含在:
zhou yang
2026-09-27 12:33:28 +08:00
共同撰写人 Claude Opus 5.5
父节点 a32fa06c43
当前提交 26c04ce878
共修改 34 个文件,包含 4031 行新增和 0 行删除
+137
查看文件
@@ -0,0 +1,137 @@
"""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())