"""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())