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/__init__.py +11 -6
- mqttxx/client.py +239 -183
- mqttxx/config.py +14 -0
- mqttxx/events.py +359 -0
- mqttxx/protocol.py +93 -22
- mqttxx/rpc.py +81 -22
- mqttxx-3.2.1.dist-info/METADATA +931 -0
- mqttxx-3.2.1.dist-info/RECORD +12 -0
- mqttxx/conventions.py +0 -145
- mqttxx-2.0.3.dist-info/METADATA +0 -490
- mqttxx-2.0.3.dist-info/RECORD +0 -12
- {mqttxx-2.0.3.dist-info → mqttxx-3.2.1.dist-info}/LICENSE +0 -0
- {mqttxx-2.0.3.dist-info → mqttxx-3.2.1.dist-info}/WHEEL +0 -0
- {mqttxx-2.0.3.dist-info → mqttxx-3.2.1.dist-info}/top_level.txt +0 -0
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
|
-
|
|
111
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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="
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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]}")
|