mqttxx 3.1.2__py3-none-any.whl → 3.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
mqttxx/__init__.py CHANGED
@@ -9,15 +9,14 @@
9
9
  - 异常系统: 统一错误码和异常层次
10
10
  """
11
11
 
12
- __version__ = "3.1.0"
12
+ __version__ = "3.2.0"
13
13
  __author__ = "MQTTX Team"
14
14
 
15
15
  # 核心客户端
16
- from .client import MQTTClient, RawAPI
16
+ from .client import MQTTClient
17
17
 
18
18
  # RPC 管理器
19
19
  from .rpc import RPCManager
20
- from .conventions import ConventionalRPCManager
21
20
 
22
21
  # Event Channel 管理器
23
22
  from .events import EventChannelManager, EventMessage
@@ -58,10 +57,8 @@ from .exceptions import (
58
57
  __all__ = [
59
58
  # MQTT 客户端
60
59
  "MQTTClient",
61
- "RawAPI", # Raw API (逃生通道)
62
60
  # RPC 管理器
63
61
  "RPCManager",
64
- "ConventionalRPCManager", # 约定式 RPC(强约束系统)
65
62
  # Event Channel 管理器
66
63
  "EventChannelManager",
67
64
  "EventMessage",
mqttxx/client.py CHANGED
@@ -3,7 +3,7 @@
3
3
  import asyncio
4
4
  import os
5
5
  import ssl
6
- from typing import Callable, Optional, Any, TYPE_CHECKING
6
+ from typing import Callable, Optional, TYPE_CHECKING, Any
7
7
  from loguru import logger
8
8
  import aiomqtt
9
9
  from paho.mqtt.matcher import MQTTMatcher
@@ -14,155 +14,6 @@ if TYPE_CHECKING:
14
14
  from .client import MQTTClient
15
15
 
16
16
 
17
- class RawAPI:
18
- """Raw MQTT API (逃生通道)
19
-
20
- 绕过协议层,直接操作 bytes。
21
-
22
- 警告:
23
- 这是逃生通道,仅在以下场景使用:
24
- - 自定义协议 (非 JSON/RPC)
25
- - 性能敏感场景 (避免编解码)
26
- - 与第三方系统集成
27
-
28
- 示例:
29
- # 订阅原始 bytes
30
- client.raw.subscribe("sensor/+", lambda t, b: process(b))
31
-
32
- # 发布原始 bytes
33
- await client.raw.publish("topic", b"\\x01\\x02\\x03")
34
- """
35
-
36
- def __init__(self, client: "MQTTClient"):
37
- """初始化 Raw API
38
-
39
- Args:
40
- client: MQTTClient 实例
41
- """
42
- self._client = client
43
-
44
- def subscribe(self, pattern: str, handler: Callable[[str, bytes], Any]):
45
- """订阅原始消息 (bytes)
46
-
47
- Args:
48
- pattern: MQTT topic pattern (支持通配符 +/#)
49
- handler: 回调函数
50
- - 签名: async def handler(topic: str, payload: bytes)
51
- - topic: 实际收到消息的 topic
52
- - payload: 原始 bytes 数据 (未解码)
53
-
54
- 并发行为:
55
- - 同一 pattern 的多个 handlers 按注册顺序**顺序调用**(非并发)
56
- - 如需并发处理,请在 handler 内部使用 asyncio.create_task()
57
-
58
- 示例:
59
- # 基础用法
60
- async def on_message(topic: str, payload: bytes):
61
- data = json.loads(payload.decode('utf-8'))
62
- print(f"收到消息: {topic} -> {data}")
63
-
64
- client.raw.subscribe("sensor/+/data", on_message)
65
-
66
- # 顺序处理(默认)
67
- async def handler_seq(topic: str, payload: bytes):
68
- await process_data(payload) # 阻塞后续 handlers
69
-
70
- # 并发处理(手动)
71
- async def handler_async(topic: str, payload: bytes):
72
- asyncio.create_task(process_async(payload)) # 不阻塞
73
- """
74
- # 追踪 handlers (辅助结构)
75
- if pattern not in self._client._raw_handlers:
76
- self._client._raw_handlers[pattern] = []
77
- self._client._raw_handlers[pattern].append(handler)
78
-
79
- # 同步到 MQTTMatcher (核心匹配引擎)
80
- self._client._raw_matcher[pattern] = self._client._raw_handlers[pattern]
81
-
82
- # 订阅 MQTT topic
83
- self._client._subscriptions.add(pattern)
84
- if self._client._client:
85
- asyncio.create_task(self._client._do_subscribe(pattern))
86
- else:
87
- logger.info(f"Raw 订阅已队列化 (等待连接) - pattern: {pattern}")
88
-
89
- logger.debug(f"Raw 订阅成功 - pattern: {pattern}")
90
-
91
- async def publish(self, topic: str, payload: bytes, qos: int = 0):
92
- """发布原始消息 (bytes)
93
-
94
- Args:
95
- topic: 目标主题
96
- payload: 消息载荷 (bytes)
97
- qos: QoS 等级 (0/1/2)
98
-
99
- 异常:
100
- aiomqtt.MqttError: 发布失败
101
- """
102
- if not self._client._client:
103
- logger.warning(f"发布失败:客户端未连接 - topic: {topic}")
104
- return
105
-
106
- try:
107
- await self._client._client.publish(topic, payload, qos=qos)
108
- except aiomqtt.MqttError as e:
109
- logger.error(f"发布失败 - topic: {topic}, error: {e}")
110
-
111
- def unsubscribe(self, pattern: str, handler: Optional[Callable] = None):
112
- """取消订阅(修复 P0-B)
113
-
114
- Args:
115
- pattern: MQTT topic pattern
116
- handler: 要移除的 handler(None = 移除所有)
117
-
118
- 示例:
119
- # 移除特定 handler
120
- client.raw.unsubscribe("test/+", my_handler)
121
-
122
- # 移除所有 handler
123
- client.raw.unsubscribe("test/+")
124
- """
125
- if pattern not in self._client._raw_handlers:
126
- logger.debug(f"Raw 取消订阅失败:pattern 不存在 - {pattern}")
127
- return
128
-
129
- if handler is None:
130
- # 移除所有 handler
131
- del self._client._raw_handlers[pattern]
132
- del self._client._raw_matcher[pattern]
133
- self._client._subscriptions.discard(pattern)
134
-
135
- # 发送 MQTT UNSUBSCRIBE
136
- if self._client._client:
137
- asyncio.create_task(self._client._client.unsubscribe(pattern))
138
-
139
- logger.debug(f"Raw 取消订阅(全部)- pattern: {pattern}")
140
- else:
141
- # 移除特定 handler
142
- handlers = self._client._raw_handlers[pattern]
143
- if handler in handlers:
144
- handlers.remove(handler)
145
-
146
- # 如果没有 handler 了,完全取消订阅
147
- if not handlers:
148
- del self._client._raw_handlers[pattern]
149
- del self._client._raw_matcher[pattern]
150
- self._client._subscriptions.discard(pattern)
151
-
152
- if self._client._client:
153
- asyncio.create_task(self._client._client.unsubscribe(pattern))
154
-
155
- logger.debug(
156
- f"Raw 取消订阅(全部,最后一个 handler)- pattern: {pattern}"
157
- )
158
- else:
159
- logger.debug(
160
- f"Raw 取消订阅(部分)- pattern: {pattern}, 剩余 {len(handlers)} 个 handler"
161
- )
162
- else:
163
- logger.debug(f"Raw 取消订阅失败:handler 不存在 - pattern: {pattern}")
164
-
165
-
166
17
  class MQTTClient:
167
18
  """基于 aiomqtt 的 MQTT 客户端
168
19
 
@@ -171,11 +22,6 @@ class MQTTClient:
171
22
  - 不自动重连,需要手动实现重连循环(官方推荐模式)
172
23
  - 使用 `async for message in client.messages` 异步迭代器
173
24
 
174
- 设计约束:
175
- - **单 Event Loop 设计**: 所有方法(subscribe/unsubscribe/handler)
176
- 必须在同一个 asyncio event loop 中调用
177
- - **不支持多线程/多 loop 并发**: 如果需要在多个线程中使用,
178
- 请为每个线程创建独立的 MQTTClient 实例
179
25
 
180
26
  并发安全:
181
27
  - ✅ 单 loop 内并发调用 subscribe/unsubscribe:安全
@@ -185,20 +31,9 @@ class MQTTClient:
185
31
  async def main():
186
32
  client = MQTTClient(config)
187
33
  await client.connect()
188
- client.raw.subscribe("topic", handler1) # 安全
189
- client.raw.subscribe("topic", handler2) # 安全
190
-
191
- # ❌ 错误:多线程
192
- def thread1():
193
- client.raw.subscribe("topic", handler1) # 不安全
194
-
195
- def thread2():
196
- client.raw.subscribe("topic", handler2) # 不安全
34
+ client.subscribe("topic", handler1) # 安全
35
+ client.subscribe("topic", handler2) # 安全
197
36
 
198
- # ✅ 正确:多线程各自创建实例
199
- def thread1():
200
- client1 = MQTTClient(config)
201
- asyncio.run(client1.connect())
202
37
  """
203
38
 
204
39
  def __init__(self, config: MQTTConfig):
@@ -221,11 +56,12 @@ class MQTTClient:
221
56
  self._raw_handlers: dict[str, list[Callable]] = {}
222
57
 
223
58
  self._running = False
224
- self._connected = False # 修复 P0-2:真实连接状态标志
59
+ self._connected = False # 真实连接状态标志
225
60
  self._reconnect_task: Optional[asyncio.Task] = None
226
61
  self._message_task: Optional[asyncio.Task] = None
62
+ self._tls_context: Optional[ssl.SSLContext] = None # TLS 上下文(复用)
227
63
 
228
- # 消息处理队列和 Workers(修复 P0-A)
64
+ # 消息处理队列和 Workers
229
65
  self._message_queue: asyncio.Queue = asyncio.Queue(
230
66
  maxsize=config.message_queue_maxsize
231
67
  )
@@ -233,9 +69,6 @@ class MQTTClient:
233
69
  # Worker 数量:默认 CPU核数×2(IO-bound 最优)
234
70
  self._num_workers = config.num_workers or (os.cpu_count() or 1) * 2
235
71
 
236
- # Raw API (逃生通道)
237
- self._raw_api = RawAPI(self)
238
-
239
72
  async def connect(self):
240
73
  """连接到 MQTT Broker
241
74
 
@@ -248,22 +81,49 @@ class MQTTClient:
248
81
 
249
82
  self._running = True
250
83
 
251
- # 启动消息处理
84
+ # 创建 TLS 上下文(只创建一次,可复用)
85
+ if self.config.tls.enabled:
86
+ self._tls_context = self._create_tls_context()
87
+
88
+ # 创建 aiomqtt.Client(只创建一次,在重连循环中复用)
89
+ self._client = aiomqtt.Client(
90
+ hostname=self.config.broker_host,
91
+ port=self.config.broker_port,
92
+ username=self.config.auth.username,
93
+ password=self.config.auth.password,
94
+ identifier=self.config.client_id or None,
95
+ clean_session=self.config.clean_session,
96
+ keepalive=self.config.keepalive,
97
+ tls_context=self._tls_context,
98
+ max_queued_outgoing_messages=self.config.max_queued_messages or None,
99
+ )
100
+
101
+ # 启动消息处理 workers
252
102
  for i in range(self._num_workers):
253
103
  worker = asyncio.create_task(self._worker(i), name=f"mqtt_worker_{i}")
254
104
  self._workers.append(worker)
255
- logger.info(f"MQTT 已启动 {self._num_workers} 个消息处理 worker")
256
105
 
257
106
  self._reconnect_task = asyncio.create_task(
258
107
  self._reconnect_loop(), name="mqtt_reconnect"
259
108
  )
260
109
 
110
+ # 等待首次连接建立(最多 60 秒)
111
+ for _ in range(600): # 60 秒超时,每次检查 0.1 秒
112
+ if self._connected:
113
+ break
114
+ await asyncio.sleep(0.1)
115
+ else:
116
+ self._running = False # 清理状态
117
+ raise TimeoutError(
118
+ f"MQTT 连接超时(60秒):{self.config.broker_host}:{self.config.broker_port}"
119
+ )
120
+
261
121
  async def disconnect(self):
262
122
  """断开连接并清理资源"""
263
123
  self._running = False
264
- self._connected = False # 修复 P0-2:标记为未连接
124
+ self._connected = False # :标记为未连接
265
125
 
266
- # 等待 workers 处理完队列(修复 P0-A)
126
+ # 等待 workers 处理完队列
267
127
  if self._workers:
268
128
  # 等待所有 workers 完成(最多 5 秒)
269
129
  _, pending = await asyncio.wait(self._workers, timeout=5.0)
@@ -289,9 +149,9 @@ class MQTTClient:
289
149
  except asyncio.CancelledError:
290
150
  pass
291
151
 
292
- # aiomqtt.Client 使用 async with 管理连接,退出时自动清理
293
- # 不需要显式调用 disconnect()(该方法不存在)
152
+ # 清理 Client TLS 上下文
294
153
  self._client = None
154
+ self._tls_context = None
295
155
 
296
156
  logger.info("MQTT 客户端已断开")
297
157
 
@@ -325,40 +185,21 @@ class MQTTClient:
325
185
  异常处理:
326
186
  - aiomqtt.MqttError:连接/协议错误,触发重连
327
187
  - asyncio.CancelledError:任务被取消,退出循环
188
+
189
+ 关键改进:
190
+ - Client 在 connect() 中创建(只创建一次)
191
+ - 循环内只使用 async with self._client: 来连接
192
+ - 符合 aiomqtt 官方推荐模式
328
193
  """
329
194
  attempt = 0
330
195
  interval = self.config.reconnect.interval
331
196
 
332
197
  while self._running:
333
198
  try:
334
- # 创建 TLS 上下文
335
- tls_context = (
336
- self._create_tls_context() if self.config.tls.enabled else None
337
- )
338
-
339
- # region 调用代码溯源(aiomqtt.Client)
340
- # aiomqtt.Client 是异步上下文管理器
341
- # 文档:https://aiomqtt.bo3hm.com/reconnection.html
342
- # 进入上下文时自动连接
343
- # endregion
344
- async with aiomqtt.Client(
345
- hostname=self.config.broker_host,
346
- port=self.config.broker_port,
347
- username=self.config.auth.username,
348
- password=self.config.auth.password,
349
- identifier=self.config.client_id
350
- or None, # 空字符串 → None = 自动生成
351
- clean_session=self.config.clean_session,
352
- keepalive=self.config.keepalive,
353
- tls_context=tls_context,
354
- max_queued_outgoing_messages=self.config.max_queued_messages
355
- or None,
356
- ) as client:
357
- self._client = client
199
+ # 使用已创建的 Client(在 connect() 中创建)
200
+ # async with 会连接,退出时会自动断开
201
+ async with self._client:
358
202
  self._connected = True # 修复 P0-2:标记为已连接
359
- logger.success(
360
- f"MQTT 连接成功 - {self.config.broker_host}:{self.config.broker_port}"
361
- )
362
203
 
363
204
  # 重置重连计数
364
205
  attempt = 0
@@ -377,7 +218,6 @@ class MQTTClient:
377
218
 
378
219
  except aiomqtt.MqttError as e:
379
220
  logger.error(f"MQTT 连接失败: {e}")
380
- self._client = None
381
221
  self._connected = False # 修复 P0-2:标记为未连接
382
222
 
383
223
  # 检查重连次数限制
@@ -405,7 +245,7 @@ class MQTTClient:
405
245
  await asyncio.sleep(interval)
406
246
 
407
247
  async def _message_loop(self):
408
- """消息接收循环(修复 P0-A:只负责接收,不处理)
248
+ """消息接收循环
409
249
 
410
250
  使用 async for 迭代消息,将消息放入队列由 workers 处理
411
251
 
@@ -458,24 +298,107 @@ class MQTTClient:
458
298
  # 队列为空,继续等待
459
299
  continue
460
300
  except asyncio.CancelledError:
461
- logger.debug(f"Worker {worker_id} 被取消")
301
+ # Worker {worker_id} 被取消
462
302
  break
463
303
  except Exception as e:
464
304
  logger.exception(f"Worker {worker_id} 异常: {e}")
465
305
  # 继续运行,不退出
466
306
 
307
+ def subscribe(self, pattern: str, handler: Callable[[str, bytes], Any]):
308
+ """订阅原始消息 (bytes)
309
+
310
+ Args:
311
+ pattern: MQTT topic pattern (支持通配符 +/#)
312
+ handler: 回调函数
313
+ - 签名: async def handler(topic: str, payload: bytes)
314
+ - topic: 实际收到消息的 topic
315
+ - payload: 原始 bytes 数据 (未解码)
316
+
317
+ 并发行为:
318
+ - 同一 pattern 的多个 handlers 按注册顺序**顺序调用**(非并发)
319
+ - 如需并发处理,请在 handler 内部使用 asyncio.create_task()
320
+
321
+ 注意:
322
+ - 订阅在重连时自动恢复
323
+ """
324
+ if pattern not in self._raw_handlers:
325
+ self._raw_handlers[pattern] = []
326
+ self._raw_matcher[pattern] = self._raw_handlers[pattern]
327
+
328
+ # 立即向 MQTT broker 订阅
329
+ if self._client:
330
+ # 使用 asyncio.create_task 避免阻塞
331
+ asyncio.create_task(self._client.subscribe(pattern))
332
+
333
+ self._raw_handlers[pattern].append(handler)
334
+ self._subscriptions.add(pattern)
335
+
336
+ logger.debug(f"订阅已注册 - pattern: {pattern}")
337
+
467
338
  @property
468
- def raw(self) -> RawAPI:
469
- """访问 Raw MQTT API (逃生通道)
339
+ def raw(self) -> aiomqtt.Client:
340
+ """暴露底层 aiomqtt.Client,用于高级用法
470
341
 
471
- Returns:
472
- RawAPI 实例
342
+ 使用场景:
343
+ await client.raw.publish(topic, payload, qos=1, retain=False)
473
344
 
474
- 示例:
475
- client.raw.subscribe("topic", handler)
476
- await client.raw.publish("topic", b"data")
345
+ Raises:
346
+ RuntimeError: 客户端未连接
347
+ """
348
+ if not self._client:
349
+ raise RuntimeError("Client not connected")
350
+ return self._client
351
+
352
+ def unsubscribe(self, pattern: str, handler: Optional[Callable] = None):
353
+ """取消订阅
354
+
355
+ Args:
356
+ pattern: MQTT topic pattern
357
+ handler: 要移除的 handler(None = 移除所有)
358
+
359
+ 注意:
360
+ - 当某个 pattern 的最后一个 handler 被移除时:
361
+ - 若当前已连接,会向 broker 发送 MQTT UNSUBSCRIBE
362
+ - 无论是否连接,都会清理本地 matcher/handlers
477
363
  """
478
- return self._raw_api
364
+ if pattern not in self._raw_handlers:
365
+ logger.debug(f"取消订阅失败:pattern 不存在 - {pattern}")
366
+ return
367
+
368
+ should_broker_unsubscribe = False
369
+
370
+ if handler is None:
371
+ del self._raw_handlers[pattern]
372
+ del self._raw_matcher[pattern]
373
+ self._subscriptions.discard(pattern)
374
+ logger.debug(f"取消订阅(全部)- pattern: {pattern}")
375
+ should_broker_unsubscribe = True
376
+ else:
377
+ handlers = self._raw_handlers[pattern]
378
+ if handler in handlers:
379
+ handlers.remove(handler)
380
+
381
+ if not handlers:
382
+ del self._raw_handlers[pattern]
383
+ del self._raw_matcher[pattern]
384
+ self._subscriptions.discard(pattern)
385
+ logger.debug(
386
+ f"取消订阅(全部,最后一个 handler)- pattern: {pattern}"
387
+ )
388
+ should_broker_unsubscribe = True
389
+ else:
390
+ logger.debug(
391
+ f"取消订阅(部分)- pattern: {pattern}, 剩余 {len(handlers)} 个 handler"
392
+ )
393
+ else:
394
+ logger.debug(f"取消订阅失败:handler 不存在 - pattern: {pattern}")
395
+
396
+ if should_broker_unsubscribe and self._client and self.is_connected:
397
+ asyncio.create_task(
398
+ self._client.unsubscribe(pattern),
399
+ name=f"mqtt_unsub_{pattern}",
400
+ )
401
+ logger.debug(f"已向 broker 发送 UNSUBSCRIBE - pattern: {pattern}")
479
402
 
480
403
  async def _handle_message(self, message: aiomqtt.Message):
481
404
  """处理单条消息(传输层,只处理 bytes)
@@ -510,21 +433,6 @@ class MQTTClient:
510
433
  except Exception as e:
511
434
  logger.exception(f"Handler 异常 - topic: {topic_str}, error: {e}")
512
435
 
513
- async def _do_subscribe(self, topic: str):
514
- """执行订阅(内部方法)
515
-
516
- Args:
517
- topic: MQTT 主题
518
- """
519
- if not self._client:
520
- return
521
-
522
- try:
523
- await self._client.subscribe(topic)
524
- logger.success(f"订阅成功 - topic: {topic}")
525
- except aiomqtt.MqttError as e:
526
- logger.error(f"订阅失败 - topic: {topic}, error: {e}")
527
-
528
436
  async def _restore_subscriptions(self):
529
437
  """恢复所有订阅(重连后调用)
530
438
 
@@ -542,7 +450,10 @@ class MQTTClient:
542
450
  logger.info(f"恢复 {len(topics)} 个订阅...")
543
451
 
544
452
  for topic in topics:
545
- await self._do_subscribe(topic)
453
+ try:
454
+ await self._client.subscribe(topic)
455
+ except aiomqtt.MqttError as e:
456
+ logger.error(f"恢复订阅失败 - topic: {topic}, error: {e}")
546
457
 
547
458
  logger.success("订阅恢复完成")
548
459
 
mqttxx/events.py CHANGED
@@ -2,12 +2,12 @@
2
2
 
3
3
  import asyncio
4
4
  import time
5
- from typing import Callable, Any, Optional, Type
5
+ from typing import Callable, Any, Optional, overload
6
6
  from dataclasses import dataclass
7
7
  from loguru import logger
8
8
 
9
9
  from .client import MQTTClient
10
- from .protocol import Codec, JSONCodec
10
+ import json
11
11
 
12
12
 
13
13
  @dataclass
@@ -93,29 +93,29 @@ class EventMessage:
93
93
  source=data.get("source", ""),
94
94
  )
95
95
 
96
- def encode(self, codec: Type[Codec] = JSONCodec) -> bytes:
97
- """编码为 bytes
98
-
99
- Args:
100
- codec: 编解码器(默认 JSONCodec)
96
+ def encode(self) -> bytes:
97
+ """编码为 bytes(JSON 格式)
101
98
 
102
99
  Returns:
103
- 编码后的 bytes
100
+ UTF-8 编码的 JSON bytes
104
101
  """
105
- return codec.encode(self)
102
+ return json.dumps(self.to_dict()).encode('utf-8')
106
103
 
107
104
  @classmethod
108
- def decode(cls, data: bytes, codec: Type[Codec] = JSONCodec) -> "EventMessage":
109
- """从 bytes 解码
105
+ def decode(cls, data: bytes) -> "EventMessage":
106
+ """从 bytes 解码(JSON 格式)
110
107
 
111
108
  Args:
112
- data: 原始 bytes 数据
113
- codec: 编解码器(默认 JSONCodec)
109
+ data: UTF-8 编码的 JSON bytes
114
110
 
115
111
  Returns:
116
112
  EventMessage 对象
113
+
114
+ Raises:
115
+ UnicodeDecodeError: UTF-8 解码失败
116
+ json.JSONDecodeError: JSON 解析失败
117
117
  """
118
- obj = codec.decode(data)
118
+ obj = json.loads(data.decode('utf-8'))
119
119
  return cls.from_dict(obj)
120
120
 
121
121
 
@@ -175,6 +175,24 @@ class EventChannelManager:
175
175
 
176
176
  logger.info("EventChannelManager 已初始化")
177
177
 
178
+ @overload
179
+ def subscribe(
180
+ self,
181
+ pattern: str,
182
+ handler: None = None
183
+ ) -> Callable[[EventHandler], EventHandler]:
184
+ """装饰器模式:返回装饰器函数"""
185
+ ...
186
+
187
+ @overload
188
+ def subscribe(
189
+ self,
190
+ pattern: str,
191
+ handler: EventHandler
192
+ ) -> EventHandler:
193
+ """直接注册模式:返回处理器本身"""
194
+ ...
195
+
178
196
  def subscribe(self, pattern: str, handler: Optional[EventHandler] = None):
179
197
  """订阅事件主题(支持通配符)
180
198
 
@@ -211,8 +229,8 @@ class EventChannelManager:
211
229
  async def dispatcher(topic: str, payload: bytes):
212
230
  """专属 dispatcher(bytes → dict → handler)"""
213
231
  try:
214
- # 解码(使用 JSONCodec)
215
- data = JSONCodec.decode(payload)
232
+ # 解码(JSON 格式)
233
+ data = json.loads(payload.decode('utf-8'))
216
234
 
217
235
  # 分发到所有 handlers(使用 get 避免 KeyError)
218
236
  for h in self._patterns.get(pattern, []):
@@ -231,8 +249,8 @@ class EventChannelManager:
231
249
  # 保存 dispatcher 引用(修复 P0-B)
232
250
  self._dispatchers[pattern] = dispatcher
233
251
 
234
- # 注册到 MQTTClient 的 raw
235
- self._client.raw.subscribe(pattern, dispatcher)
252
+ # 注册到 MQTTClient 层
253
+ self._client.subscribe(pattern, dispatcher)
236
254
 
237
255
  self._patterns[pattern].append(func)
238
256
  logger.debug(
@@ -253,7 +271,7 @@ class EventChannelManager:
253
271
  handler: 要移除的处理器(None = 移除所有)
254
272
 
255
273
  改进:
256
- 现在会真正调用 RawAPI.unsubscribe 来清理底层 MQTT 订阅
274
+ 现在会真正调用 MQTTClient.unsubscribe 来清理底层 MQTT 订阅
257
275
  避免内存泄漏
258
276
  """
259
277
  if pattern not in self._patterns:
@@ -266,7 +284,7 @@ class EventChannelManager:
266
284
  # 清理 dispatcher(修复 P0-B)
267
285
  dispatcher = self._dispatchers.pop(pattern, None)
268
286
  if dispatcher:
269
- self._client.raw.unsubscribe(pattern, dispatcher)
287
+ self._client.unsubscribe(pattern, dispatcher)
270
288
 
271
289
  logger.info(f"已取消订阅 - pattern: {pattern}")
272
290
  else:
@@ -280,11 +298,13 @@ class EventChannelManager:
280
298
 
281
299
  dispatcher = self._dispatchers.pop(pattern, None)
282
300
  if dispatcher:
283
- self._client.raw.unsubscribe(pattern, dispatcher)
301
+ self._client.unsubscribe(pattern, dispatcher)
284
302
 
285
303
  logger.info(f"已取消订阅(最后一个 handler)- pattern: {pattern}")
286
304
  else:
287
- logger.debug(f"已移除处理器 - pattern: {pattern}, 剩余 {len(self._patterns[pattern])} 个")
305
+ logger.debug(
306
+ f"已移除处理器 - pattern: {pattern}, 剩余 {len(self._patterns[pattern])} 个"
307
+ )
288
308
 
289
309
  async def publish(
290
310
  self,
@@ -328,13 +348,12 @@ class EventChannelManager:
328
348
  """
329
349
  # 编码消息
330
350
  if isinstance(message, EventMessage):
331
- payload = message.encode(JSONCodec)
351
+ payload = message.encode()
332
352
  elif isinstance(message, dict):
333
- payload = JSONCodec.encode(message)
353
+ payload = json.dumps(message).encode('utf-8')
334
354
  else:
335
355
  # 其他类型自动包装
336
- payload = JSONCodec.encode({"data": message})
356
+ payload = json.dumps({"data": message}).encode('utf-8')
337
357
 
338
358
  # 直接发布(零开销)
339
359
  await self._client.raw.publish(topic, payload, qos=qos)
340
- logger.debug(f"事件已发布 - topic: {topic}, qos: {qos}")