mqttxx 2.0.3__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/rpc.py CHANGED
@@ -1,7 +1,6 @@
1
1
  # MQTT RPC 模块 - 基于 aiomqtt 的双向对等 RPC 调用
2
2
 
3
3
  import asyncio
4
- import json
5
4
  import uuid
6
5
  from typing import Any, Callable, Optional
7
6
  from loguru import logger
@@ -14,8 +13,13 @@ from .exceptions import (
14
13
  TooManyConcurrentCallsError,
15
14
  MQTTXError,
16
15
  ErrorCode,
16
+ MessageError,
17
+ )
18
+ from .protocol import (
19
+ RPCRequest,
20
+ RPCResponse,
21
+ parse_message_from_bytes,
17
22
  )
18
- from .protocol import RPCRequest, RPCResponse
19
23
 
20
24
 
21
25
  # 权限检查回调类型
@@ -74,6 +78,7 @@ class RPCManager:
74
78
  def __init__(
75
79
  self,
76
80
  client: MQTTClient,
81
+ my_topic: Optional[str] = None,
77
82
  config: Optional[RPCConfig] = None,
78
83
  auth_callback: Optional[AuthCallback] = None,
79
84
  ):
@@ -81,36 +86,83 @@ class RPCManager:
81
86
 
82
87
  Args:
83
88
  client: MQTTClient 实例(用于底层消息收发)
89
+ my_topic: 本节点的响应主题(可选,提供后自动订阅并注入到 reply_to)
84
90
  config: RPC 配置(可选,默认使用标准配置)
85
91
  auth_callback: 权限检查回调函数(可选)
86
92
  签名:async def auth_callback(caller_id: str, method: str, request: RPCRequest) -> bool
87
93
  返回:True = 允许,False = 拒绝
94
+ codec: 编解码器(默认 JSONCodec)
88
95
 
89
96
  使用示例:
90
97
  client = MQTTClient(...)
91
98
  await client.connect()
92
99
 
93
- # 基础用法(无权限控制)
100
+ # 约定式用法(推荐)
101
+ rpc = RPCManager(client, my_topic="edge/device_123")
102
+ # 自动订阅 edge/device_123,调用时自动注入 reply_to
103
+
104
+ # 手动设置响应主题
94
105
  rpc = RPCManager(client)
106
+ rpc.setup("my/rpc/responses")
95
107
 
96
108
  # 带权限控制
97
109
  async def auth_check(caller_id, method, request):
98
110
  return caller_id in ALLOWED_CLIENTS
99
111
 
100
- rpc = RPCManager(client, auth_callback=auth_check)
112
+ rpc = RPCManager(client, my_topic="server/node", auth_callback=auth_check)
101
113
  """
102
114
  self._client = client
115
+ self._my_topic = my_topic
103
116
  self.config = config or RPCConfig()
104
117
  self._auth_callback = auth_callback
105
118
 
106
119
  # RPC 状态
107
120
  self._pending_calls: dict[str, asyncio.Future] = {} # request_id → Future
108
121
  self._handlers: dict[str, Callable] = {} # method_name → handler
109
- self._pending_calls_lock = (
110
- asyncio.Lock()
111
- ) # 修复 P0-1:保护 _pending_calls 并发访问
122
+ self._pending_calls_lock = asyncio.Lock() # 保护 _pending_calls 并发访问
123
+
124
+ # 如果提供了 my_topic,自动订阅
125
+ if my_topic:
126
+ self.setup(my_topic)
127
+
128
+ @property
129
+ def my_topic(self) -> Optional[str]:
130
+ """获取本节点的响应主题"""
131
+ return self._my_topic
132
+
133
+ def setup(self, reply_topic: str):
134
+ """设置 RPC 响应主题并自动订阅
112
135
 
113
- logger.info("RPCManager 已初始化")
136
+ 这个方法会:
137
+ 1. 订阅 reply_topic(接收 RPC 响应)
138
+ 2. 注册消息处理器(自动解码 + 分发)
139
+
140
+ Args:
141
+ reply_topic: 响应主题(例如:server/rpc_responses)
142
+
143
+ 示例:
144
+ rpc = RPCManager(client)
145
+ rpc.setup("my/rpc/responses")
146
+ """
147
+
148
+ async def handle_bytes(topic: str, payload: bytes):
149
+ """bytes → RPC message → handle"""
150
+ try:
151
+ # 解码
152
+ message = parse_message_from_bytes(payload)
153
+
154
+ # 路由
155
+ if isinstance(message, RPCRequest):
156
+ await self._handle_request(topic, message)
157
+ elif isinstance(message, RPCResponse):
158
+ await self._handle_response(topic, message)
159
+ except MessageError as e:
160
+ logger.debug(f"非 RPC 消息 - topic: {topic}, reason: {e}")
161
+ except Exception as e:
162
+ logger.exception(f"RPC 消息处理失败: {e}")
163
+
164
+ # 订阅 raw bytes
165
+ self._client.subscribe(reply_topic, handle_bytes)
114
166
 
115
167
  def register(self, method_name: str):
116
168
  """装饰器:注册本地 RPC 方法供远程调用
@@ -188,52 +240,57 @@ class RPCManager:
188
240
  method: str,
189
241
  params: Any = None,
190
242
  timeout: Optional[float] = None,
191
- reply_to: str = None,
243
+ reply_to: Optional[str] = None,
192
244
  ) -> Any:
193
245
  """远程调用 RPC 方法
194
246
 
195
247
  修复点:
196
248
  - ✅ 新增并发限制检查
249
+ - ✅ 自动注入 reply_to(如果初始化时提供了 my_topic)
197
250
 
198
251
  Args:
199
252
  topic: 目标 MQTT 主题(例如:bots/456)
200
253
  method: 远程方法名
201
254
  params: 方法参数(可选)
202
255
  timeout: 超时时间(秒,None 则使用配置的默认值)
203
- reply_to: 响应主题(必需,例如:server/device_123
256
+ reply_to: 响应主题(可选,默认使用初始化时的 my_topic
204
257
 
205
258
  Returns:
206
259
  远程方法的返回值
207
260
 
208
261
  Raises:
209
262
  MQTTXError: 客户端未连接
210
- ValueError: reply_to 参数缺失
263
+ ValueError: reply_to 参数缺失且初始化时未提供 my_topic
211
264
  TooManyConcurrentCallsError: 并发调用超限
212
265
  RPCTimeoutError: 调用超时
213
266
  RPCRemoteError: 远程执行失败
214
267
 
215
268
  使用示例:
216
- # 基础调用
217
- result = await rpc.call(
218
- topic="bots/456",
219
- method="get_status",
220
- reply_to="server/device_123"
221
- )
269
+ # 约定式用法(推荐)
270
+ rpc = RPCManager(client, my_topic="server/device_123")
271
+ result = await rpc.call("bots/456", "get_status")
272
+ # reply_to 自动注入为 "server/device_123"
222
273
 
223
- # 带参数调用
274
+ # 手动指定 reply_to
224
275
  result = await rpc.call(
225
276
  topic="bots/456",
226
277
  method="process_command",
227
278
  params={"command": "restart"},
228
- reply_to="server/device_123",
279
+ reply_to="custom/reply/topic",
229
280
  timeout=60.0
230
281
  )
231
282
  """
232
283
  if not self._client.is_connected:
233
284
  raise MQTTXError("MQTT 客户端未连接", ErrorCode.NOT_CONNECTED)
234
285
 
286
+ # 自动注入 reply_to
287
+ if reply_to is None:
288
+ reply_to = self._my_topic
289
+
235
290
  if reply_to is None:
236
- raise ValueError("reply_to 参数是必需的,不能为 None")
291
+ raise ValueError(
292
+ "reply_to 参数是必需的,或在初始化时提供 my_topic"
293
+ )
237
294
 
238
295
  # 生成请求
239
296
  request_id = str(uuid.uuid4())
@@ -258,7 +315,8 @@ class RPCManager:
258
315
  self._pending_calls[request_id] = future
259
316
 
260
317
  # 发送请求
261
- await self._client.publish(topic, json.dumps(request.to_dict()), qos=1)
318
+ payload = request.encode()
319
+ await self._client.raw.publish(topic, payload, qos=1)
262
320
  logger.debug(f"RPC 请求已发送 - method: {method}, request_id: {request_id[:8]}")
263
321
 
264
322
  try:
@@ -406,5 +464,6 @@ class RPCManager:
406
464
  topic: 响应主题
407
465
  response: RPC 响应消息
408
466
  """
409
- await self._client.publish(topic, json.dumps(response.to_dict()), qos=1)
467
+ payload = response.encode()
468
+ await self._client.raw.publish(topic, payload, qos=1)
410
469
  logger.debug(f"RPC 响应已发送 - request_id: {response.request_id[:8]}")