"""Optional MQTT 3.1.1 client used by the browser workstation.""" from __future__ import annotations import json import threading import uuid from collections.abc import Callable from typing import Any StateCallback = Callable[[str, str], None] PayloadCallback = Callable[[str, str], None] class MqttSeatPoseClient: def __init__( self, host: str, port: int, topic: str, on_state: StateCallback, on_payload: PayloadCallback, ) -> None: self.host = host self.port = port self.topic = topic self.test_topic = f"n7calibration/seatpose_test/{uuid.uuid4().hex[:12]}" self.on_state = on_state self.on_payload = on_payload self._client: Any = None self._lock = threading.Lock() def connect(self) -> None: try: import paho.mqtt.client as mqtt # type: ignore except ImportError as error: raise RuntimeError("缺少 paho-mqtt;请安装项目的 ir 可选依赖") from error self.close() try: client = mqtt.Client( callback_api_version=mqtt.CallbackAPIVersion.VERSION2, client_id=f"n7cal_{uuid.uuid4().hex[:12]}", protocol=mqtt.MQTTv311, ) except (AttributeError, TypeError): client = mqtt.Client(client_id=f"n7cal_{uuid.uuid4().hex[:12]}") client.on_connect = self._on_connect client.on_disconnect = self._on_disconnect client.on_message = self._on_message with self._lock: self._client = client self.on_state("connecting", f"正在连接 {self.host}:{self.port}") client.connect_async(self.host, self.port, keepalive=60) client.loop_start() def publish_test(self, payload: dict[str, Any] | None = None) -> str: with self._lock: client = self._client if client is None or not client.is_connected(): raise RuntimeError("MQTT 尚未连接") body = payload or sample_payload() result = client.publish(self.test_topic, json.dumps(body, ensure_ascii=False), qos=0) if result.rc != 0: raise RuntimeError(f"MQTT 发送失败,返回码 {result.rc}") return self.test_topic def close(self) -> None: with self._lock: client, self._client = self._client, None if client is not None: try: client.disconnect() except Exception: pass try: client.loop_stop() except Exception: pass def _on_connect( self, client: Any, _userdata: Any, _flags: Any, reason_code: Any, *_: Any, ) -> None: if int(reason_code) != 0: self.on_state("error", f"MQTT 连接被拒绝:{reason_code}") return client.subscribe([(self.topic, 0), (self.test_topic, 0)]) self.on_state("subscribed", f"已订阅 {self.topic}") def _on_disconnect(self, _client: Any, _userdata: Any, *args: Any) -> None: reason = args[-2] if len(args) >= 2 else args[-1] if args else "" self.on_state("disconnected", f"MQTT 已断开{f':{reason}' if reason else ''}") def _on_message(self, _client: Any, _userdata: Any, message: Any) -> None: try: payload = message.payload.decode("utf-8") except UnicodeDecodeError: self.on_state("error", "MQTT 收到非 UTF-8 数据") return self.on_payload(message.topic, payload) def sample_payload(frame: int = 1) -> dict[str, Any]: return { "type": "seatpose", "frame": frame, "timestamp_us": frame * 33_333, "area_poses": [ { "area": "Seat_cushion", "axis_angle": {"valid": True, "degree": 0.0}, "current_frame_to_reference": {"valid": True, "T": [0.12, 0.04, 0.01]}, }, { "area": "Seat_backrest", "axis_angle": {"valid": True, "degree": 34.0}, "current_frame_to_reference": {"valid": True, "T": [0.0, 0.0, 0.0]}, }, { "area": "Seat_leg_rest", "axis_angle": {"valid": True, "degree": 22.0}, "current_frame_to_reference": {"valid": True, "T": [0.0, 0.0, 0.0]}, }, ], }