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 +2 -5
- mqttxx/client.py +144 -233
- mqttxx/events.py +45 -26
- mqttxx/protocol.py +45 -149
- mqttxx/rpc.py +48 -29
- {mqttxx-3.1.2.dist-info → mqttxx-3.2.1.dist-info}/METADATA +74 -53
- mqttxx-3.2.1.dist-info/RECORD +12 -0
- mqttxx/conventions.py +0 -148
- mqttxx-3.1.2.dist-info/RECORD +0 -13
- {mqttxx-3.1.2.dist-info → mqttxx-3.2.1.dist-info}/LICENSE +0 -0
- {mqttxx-3.1.2.dist-info → mqttxx-3.2.1.dist-info}/WHEEL +0 -0
- {mqttxx-3.1.2.dist-info → mqttxx-3.2.1.dist-info}/top_level.txt +0 -0
mqttxx/protocol.py
CHANGED
|
@@ -2,106 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
from dataclasses import dataclass
|
|
5
|
-
from
|
|
6
|
-
|
|
5
|
+
from dataclasses import asdict, is_dataclass
|
|
6
|
+
from typing import Any, Optional, Literal
|
|
7
7
|
from .exceptions import MessageError, ErrorCode
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
@runtime_checkable
|
|
15
|
-
class Codec(TypingProtocol):
|
|
16
|
-
"""编解码器接口(协议无关)
|
|
17
|
-
|
|
18
|
-
实现此接口可支持不同的序列化协议(JSON、MessagePack、Protobuf 等)
|
|
19
|
-
"""
|
|
20
|
-
|
|
21
|
-
@staticmethod
|
|
22
|
-
def encode(obj: Any) -> bytes:
|
|
23
|
-
"""对象 → bytes
|
|
24
|
-
|
|
25
|
-
Args:
|
|
26
|
-
obj: 要编码的对象(RPCRequest/RPCResponse/EventMessage/dict)
|
|
27
|
-
|
|
28
|
-
Returns:
|
|
29
|
-
编码后的 bytes
|
|
30
|
-
|
|
31
|
-
Raises:
|
|
32
|
-
ValueError: 无法编码的类型
|
|
33
|
-
"""
|
|
34
|
-
...
|
|
35
|
-
|
|
36
|
-
@staticmethod
|
|
37
|
-
def decode(data: bytes) -> dict:
|
|
38
|
-
"""bytes → dict
|
|
39
|
-
|
|
40
|
-
Args:
|
|
41
|
-
data: 原始 bytes 数据
|
|
42
|
-
|
|
43
|
-
Returns:
|
|
44
|
-
解码后的 dict
|
|
45
|
-
|
|
46
|
-
Raises:
|
|
47
|
-
UnicodeDecodeError: UTF-8 解码失败
|
|
48
|
-
JSONDecodeError: JSON 解析失败(或其他格式解析失败)
|
|
49
|
-
"""
|
|
50
|
-
...
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
class JSONCodec:
|
|
54
|
-
"""JSON 编解码器(默认实现)
|
|
55
|
-
|
|
56
|
-
使用标准 JSON 格式进行序列化/反序列化
|
|
57
|
-
支持 Pydantic BaseModel 自动序列化
|
|
58
|
-
"""
|
|
59
|
-
|
|
60
|
-
@staticmethod
|
|
61
|
-
def encode(obj: Any) -> bytes:
|
|
62
|
-
"""对象 → bytes
|
|
63
|
-
|
|
64
|
-
支持:
|
|
65
|
-
- 有 to_dict() 方法的对象(RPCRequest/RPCResponse/EventMessage)
|
|
66
|
-
- Pydantic BaseModel(自动调用 model_dump())
|
|
67
|
-
- dict 对象
|
|
68
|
-
|
|
69
|
-
Args:
|
|
70
|
-
obj: 要编码的对象
|
|
71
|
-
|
|
72
|
-
Returns:
|
|
73
|
-
UTF-8 编码的 JSON bytes
|
|
10
|
+
def _to_jsonable(obj: Any) -> Any:
|
|
11
|
+
if obj is None:
|
|
12
|
+
return None
|
|
74
13
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
"""
|
|
78
|
-
if hasattr(obj, 'to_dict'):
|
|
79
|
-
data = obj.to_dict()
|
|
80
|
-
elif hasattr(obj, 'model_dump'):
|
|
81
|
-
data = obj.model_dump()
|
|
82
|
-
elif isinstance(obj, dict):
|
|
83
|
-
data = obj
|
|
84
|
-
else:
|
|
85
|
-
raise ValueError(f"无法编码类型: {type(obj)}")
|
|
14
|
+
if is_dataclass(obj):
|
|
15
|
+
return _to_jsonable(asdict(obj))
|
|
86
16
|
|
|
87
|
-
|
|
17
|
+
if isinstance(obj, dict):
|
|
18
|
+
return {k: _to_jsonable(v) for k, v in obj.items()}
|
|
88
19
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
"""bytes → dict
|
|
20
|
+
if isinstance(obj, (list, tuple, set)):
|
|
21
|
+
return [_to_jsonable(v) for v in obj]
|
|
92
22
|
|
|
93
|
-
|
|
94
|
-
|
|
23
|
+
if hasattr(obj, "model_dump") and callable(getattr(obj, "model_dump")):
|
|
24
|
+
return _to_jsonable(obj.model_dump(mode="json"))
|
|
95
25
|
|
|
96
|
-
|
|
97
|
-
|
|
26
|
+
if hasattr(obj, "dict") and callable(getattr(obj, "dict")):
|
|
27
|
+
return _to_jsonable(obj.dict())
|
|
98
28
|
|
|
99
|
-
|
|
100
|
-
UnicodeDecodeError: UTF-8 解码失败
|
|
101
|
-
json.JSONDecodeError: JSON 解析失败
|
|
102
|
-
"""
|
|
103
|
-
text = data.decode('utf-8')
|
|
104
|
-
return json.loads(text)
|
|
29
|
+
return obj
|
|
105
30
|
|
|
106
31
|
|
|
107
32
|
@dataclass
|
|
@@ -146,16 +71,11 @@ class RPCRequest:
|
|
|
146
71
|
Returns:
|
|
147
72
|
包含所有字段的字典
|
|
148
73
|
"""
|
|
149
|
-
# 处理 Pydantic 模型
|
|
150
|
-
params = self.params
|
|
151
|
-
if hasattr(params, 'model_dump'):
|
|
152
|
-
params = params.model_dump()
|
|
153
|
-
|
|
154
74
|
return {
|
|
155
75
|
"type": self.type,
|
|
156
76
|
"request_id": self.request_id,
|
|
157
77
|
"method": self.method,
|
|
158
|
-
"params": params,
|
|
78
|
+
"params": _to_jsonable(self.params),
|
|
159
79
|
"reply_to": self.reply_to,
|
|
160
80
|
"caller_id": self.caller_id,
|
|
161
81
|
}
|
|
@@ -183,46 +103,33 @@ class RPCRequest:
|
|
|
183
103
|
)
|
|
184
104
|
except KeyError as e:
|
|
185
105
|
raise MessageError(
|
|
186
|
-
f"RPC 请求缺少必需字段: {e}",
|
|
187
|
-
ErrorCode.MISSING_REQUIRED_FIELD
|
|
106
|
+
f"RPC 请求缺少必需字段: {e}", ErrorCode.MISSING_REQUIRED_FIELD
|
|
188
107
|
)
|
|
189
108
|
|
|
190
|
-
def encode(self
|
|
191
|
-
"""编码为 bytes
|
|
192
|
-
|
|
193
|
-
Args:
|
|
194
|
-
codec: 编解码器(默认 JSONCodec)
|
|
109
|
+
def encode(self) -> bytes:
|
|
110
|
+
"""编码为 bytes(JSON 格式)
|
|
195
111
|
|
|
196
112
|
Returns:
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
示例:
|
|
200
|
-
request = RPCRequest(request_id="123", method="test")
|
|
201
|
-
payload = request.encode() # 使用默认 JSONCodec
|
|
202
|
-
# 或自定义 codec
|
|
203
|
-
payload = request.encode(MessagePackCodec)
|
|
113
|
+
UTF-8 编码的 JSON bytes
|
|
204
114
|
"""
|
|
205
|
-
return
|
|
115
|
+
return json.dumps(self.to_dict()).encode("utf-8")
|
|
206
116
|
|
|
207
117
|
@classmethod
|
|
208
|
-
def decode(cls, data: bytes
|
|
209
|
-
"""从 bytes
|
|
118
|
+
def decode(cls, data: bytes) -> "RPCRequest":
|
|
119
|
+
"""从 bytes 解码(JSON 格式)
|
|
210
120
|
|
|
211
121
|
Args:
|
|
212
|
-
data:
|
|
213
|
-
codec: 编解码器(默认 JSONCodec)
|
|
122
|
+
data: UTF-8 编码的 JSON bytes
|
|
214
123
|
|
|
215
124
|
Returns:
|
|
216
125
|
RPCRequest 对象
|
|
217
126
|
|
|
218
127
|
Raises:
|
|
219
128
|
MessageError: 解码失败或缺少必需字段
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
payload = b'{"type":"rpc_request","request_id":"123",...}'
|
|
223
|
-
request = RPCRequest.decode(payload)
|
|
129
|
+
UnicodeDecodeError: UTF-8 解码失败
|
|
130
|
+
json.JSONDecodeError: JSON 解析失败
|
|
224
131
|
"""
|
|
225
|
-
obj =
|
|
132
|
+
obj = json.loads(data.decode("utf-8"))
|
|
226
133
|
return cls.from_dict(obj)
|
|
227
134
|
|
|
228
135
|
|
|
@@ -274,11 +181,7 @@ class RPCResponse:
|
|
|
274
181
|
if self.error is not None:
|
|
275
182
|
data["error"] = self.error
|
|
276
183
|
else:
|
|
277
|
-
|
|
278
|
-
result = self.result
|
|
279
|
-
if hasattr(result, 'model_dump'):
|
|
280
|
-
result = result.model_dump()
|
|
281
|
-
data["result"] = result
|
|
184
|
+
data["result"] = _to_jsonable(self.result)
|
|
282
185
|
|
|
283
186
|
return data
|
|
284
187
|
|
|
@@ -303,45 +206,41 @@ class RPCResponse:
|
|
|
303
206
|
)
|
|
304
207
|
except KeyError as e:
|
|
305
208
|
raise MessageError(
|
|
306
|
-
f"RPC 响应缺少必需字段: {e}",
|
|
307
|
-
ErrorCode.MISSING_REQUIRED_FIELD
|
|
209
|
+
f"RPC 响应缺少必需字段: {e}", ErrorCode.MISSING_REQUIRED_FIELD
|
|
308
210
|
)
|
|
309
211
|
|
|
310
|
-
def encode(self
|
|
311
|
-
"""编码为 bytes
|
|
312
|
-
|
|
313
|
-
Args:
|
|
314
|
-
codec: 编解码器(默认 JSONCodec)
|
|
212
|
+
def encode(self) -> bytes:
|
|
213
|
+
"""编码为 bytes(JSON 格式)
|
|
315
214
|
|
|
316
215
|
Returns:
|
|
317
|
-
|
|
216
|
+
UTF-8 编码的 JSON bytes
|
|
318
217
|
"""
|
|
319
|
-
return
|
|
218
|
+
return json.dumps(self.to_dict()).encode("utf-8")
|
|
320
219
|
|
|
321
220
|
@classmethod
|
|
322
|
-
def decode(cls, data: bytes
|
|
323
|
-
"""从 bytes
|
|
221
|
+
def decode(cls, data: bytes) -> "RPCResponse":
|
|
222
|
+
"""从 bytes 解码(JSON 格式)
|
|
324
223
|
|
|
325
224
|
Args:
|
|
326
|
-
data:
|
|
327
|
-
codec: 编解码器(默认 JSONCodec)
|
|
225
|
+
data: UTF-8 编码的 JSON bytes
|
|
328
226
|
|
|
329
227
|
Returns:
|
|
330
228
|
RPCResponse 对象
|
|
331
229
|
|
|
332
230
|
Raises:
|
|
333
231
|
MessageError: 解码失败或缺少必需字段
|
|
232
|
+
UnicodeDecodeError: UTF-8 解码失败
|
|
233
|
+
json.JSONDecodeError: JSON 解析失败
|
|
334
234
|
"""
|
|
335
|
-
obj =
|
|
235
|
+
obj = json.loads(data.decode("utf-8"))
|
|
336
236
|
return cls.from_dict(obj)
|
|
337
237
|
|
|
338
238
|
|
|
339
|
-
def parse_message_from_bytes(data: bytes
|
|
340
|
-
"""从 bytes 解析 RPC
|
|
239
|
+
def parse_message_from_bytes(data: bytes) -> RPCRequest | RPCResponse:
|
|
240
|
+
"""从 bytes 解析 RPC 消息(JSON 格式)
|
|
341
241
|
|
|
342
242
|
Args:
|
|
343
|
-
data:
|
|
344
|
-
codec: 编解码器(默认 JSONCodec)
|
|
243
|
+
data: UTF-8 编码的 JSON bytes
|
|
345
244
|
|
|
346
245
|
Returns:
|
|
347
246
|
RPCRequest 或 RPCResponse 对象
|
|
@@ -362,7 +261,7 @@ def parse_message_from_bytes(data: bytes, codec: Type[Codec] = JSONCodec) -> RPC
|
|
|
362
261
|
# 处理响应
|
|
363
262
|
pass
|
|
364
263
|
"""
|
|
365
|
-
obj =
|
|
264
|
+
obj = json.loads(data.decode("utf-8"))
|
|
366
265
|
msg_type = obj.get("type")
|
|
367
266
|
|
|
368
267
|
if msg_type == "rpc_request":
|
|
@@ -370,7 +269,4 @@ def parse_message_from_bytes(data: bytes, codec: Type[Codec] = JSONCodec) -> RPC
|
|
|
370
269
|
elif msg_type == "rpc_response":
|
|
371
270
|
return RPCResponse.from_dict(obj)
|
|
372
271
|
else:
|
|
373
|
-
raise MessageError(
|
|
374
|
-
f"未知消息类型: {msg_type}",
|
|
375
|
-
ErrorCode.INVALID_MESSAGE_TYPE
|
|
376
|
-
)
|
|
272
|
+
raise MessageError(f"未知消息类型: {msg_type}", ErrorCode.INVALID_MESSAGE_TYPE)
|
mqttxx/rpc.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import uuid
|
|
5
|
-
from typing import Any, Callable, Optional
|
|
5
|
+
from typing import Any, Callable, Optional
|
|
6
6
|
from loguru import logger
|
|
7
7
|
|
|
8
8
|
from .client import MQTTClient
|
|
@@ -15,7 +15,11 @@ from .exceptions import (
|
|
|
15
15
|
ErrorCode,
|
|
16
16
|
MessageError,
|
|
17
17
|
)
|
|
18
|
-
from .protocol import
|
|
18
|
+
from .protocol import (
|
|
19
|
+
RPCRequest,
|
|
20
|
+
RPCResponse,
|
|
21
|
+
parse_message_from_bytes,
|
|
22
|
+
)
|
|
19
23
|
|
|
20
24
|
|
|
21
25
|
# 权限检查回调类型
|
|
@@ -74,14 +78,15 @@ 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
|
-
codec: Type[Codec] = JSONCodec, # 新增:可插拔编解码器
|
|
80
84
|
):
|
|
81
85
|
"""初始化 RPC 管理器
|
|
82
86
|
|
|
83
87
|
Args:
|
|
84
88
|
client: MQTTClient 实例(用于底层消息收发)
|
|
89
|
+
my_topic: 本节点的响应主题(可选,提供后自动订阅并注入到 reply_to)
|
|
85
90
|
config: RPC 配置(可选,默认使用标准配置)
|
|
86
91
|
auth_callback: 权限检查回调函数(可选)
|
|
87
92
|
签名:async def auth_callback(caller_id: str, method: str, request: RPCRequest) -> bool
|
|
@@ -92,29 +97,38 @@ class RPCManager:
|
|
|
92
97
|
client = MQTTClient(...)
|
|
93
98
|
await client.connect()
|
|
94
99
|
|
|
95
|
-
#
|
|
100
|
+
# 约定式用法(推荐)
|
|
101
|
+
rpc = RPCManager(client, my_topic="edge/device_123")
|
|
102
|
+
# 自动订阅 edge/device_123,调用时自动注入 reply_to
|
|
103
|
+
|
|
104
|
+
# 手动设置响应主题
|
|
96
105
|
rpc = RPCManager(client)
|
|
97
|
-
rpc.setup("my/rpc/responses")
|
|
106
|
+
rpc.setup("my/rpc/responses")
|
|
98
107
|
|
|
99
108
|
# 带权限控制
|
|
100
109
|
async def auth_check(caller_id, method, request):
|
|
101
110
|
return caller_id in ALLOWED_CLIENTS
|
|
102
111
|
|
|
103
|
-
rpc = RPCManager(client, auth_callback=auth_check)
|
|
112
|
+
rpc = RPCManager(client, my_topic="server/node", auth_callback=auth_check)
|
|
104
113
|
"""
|
|
105
114
|
self._client = client
|
|
115
|
+
self._my_topic = my_topic
|
|
106
116
|
self.config = config or RPCConfig()
|
|
107
117
|
self._auth_callback = auth_callback
|
|
108
|
-
self._codec = codec # 保存编解码器
|
|
109
118
|
|
|
110
119
|
# RPC 状态
|
|
111
120
|
self._pending_calls: dict[str, asyncio.Future] = {} # request_id → Future
|
|
112
121
|
self._handlers: dict[str, Callable] = {} # method_name → handler
|
|
113
|
-
self._pending_calls_lock = (
|
|
114
|
-
asyncio.Lock()
|
|
115
|
-
) # 修复 P0-1:保护 _pending_calls 并发访问
|
|
122
|
+
self._pending_calls_lock = asyncio.Lock() # 保护 _pending_calls 并发访问
|
|
116
123
|
|
|
117
|
-
|
|
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
|
|
118
132
|
|
|
119
133
|
def setup(self, reply_topic: str):
|
|
120
134
|
"""设置 RPC 响应主题并自动订阅
|
|
@@ -130,11 +144,12 @@ class RPCManager:
|
|
|
130
144
|
rpc = RPCManager(client)
|
|
131
145
|
rpc.setup("my/rpc/responses")
|
|
132
146
|
"""
|
|
147
|
+
|
|
133
148
|
async def handle_bytes(topic: str, payload: bytes):
|
|
134
149
|
"""bytes → RPC message → handle"""
|
|
135
150
|
try:
|
|
136
151
|
# 解码
|
|
137
|
-
message = parse_message_from_bytes(payload
|
|
152
|
+
message = parse_message_from_bytes(payload)
|
|
138
153
|
|
|
139
154
|
# 路由
|
|
140
155
|
if isinstance(message, RPCRequest):
|
|
@@ -147,8 +162,7 @@ class RPCManager:
|
|
|
147
162
|
logger.exception(f"RPC 消息处理失败: {e}")
|
|
148
163
|
|
|
149
164
|
# 订阅 raw bytes
|
|
150
|
-
self._client.
|
|
151
|
-
|
|
165
|
+
self._client.subscribe(reply_topic, handle_bytes)
|
|
152
166
|
|
|
153
167
|
def register(self, method_name: str):
|
|
154
168
|
"""装饰器:注册本地 RPC 方法供远程调用
|
|
@@ -226,52 +240,57 @@ class RPCManager:
|
|
|
226
240
|
method: str,
|
|
227
241
|
params: Any = None,
|
|
228
242
|
timeout: Optional[float] = None,
|
|
229
|
-
reply_to: str = None,
|
|
243
|
+
reply_to: Optional[str] = None,
|
|
230
244
|
) -> Any:
|
|
231
245
|
"""远程调用 RPC 方法
|
|
232
246
|
|
|
233
247
|
修复点:
|
|
234
248
|
- ✅ 新增并发限制检查
|
|
249
|
+
- ✅ 自动注入 reply_to(如果初始化时提供了 my_topic)
|
|
235
250
|
|
|
236
251
|
Args:
|
|
237
252
|
topic: 目标 MQTT 主题(例如:bots/456)
|
|
238
253
|
method: 远程方法名
|
|
239
254
|
params: 方法参数(可选)
|
|
240
255
|
timeout: 超时时间(秒,None 则使用配置的默认值)
|
|
241
|
-
reply_to:
|
|
256
|
+
reply_to: 响应主题(可选,默认使用初始化时的 my_topic)
|
|
242
257
|
|
|
243
258
|
Returns:
|
|
244
259
|
远程方法的返回值
|
|
245
260
|
|
|
246
261
|
Raises:
|
|
247
262
|
MQTTXError: 客户端未连接
|
|
248
|
-
ValueError: reply_to
|
|
263
|
+
ValueError: reply_to 参数缺失且初始化时未提供 my_topic
|
|
249
264
|
TooManyConcurrentCallsError: 并发调用超限
|
|
250
265
|
RPCTimeoutError: 调用超时
|
|
251
266
|
RPCRemoteError: 远程执行失败
|
|
252
267
|
|
|
253
268
|
使用示例:
|
|
254
|
-
#
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
reply_to="server/device_123"
|
|
259
|
-
)
|
|
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"
|
|
260
273
|
|
|
261
|
-
#
|
|
274
|
+
# 手动指定 reply_to
|
|
262
275
|
result = await rpc.call(
|
|
263
276
|
topic="bots/456",
|
|
264
277
|
method="process_command",
|
|
265
278
|
params={"command": "restart"},
|
|
266
|
-
reply_to="
|
|
279
|
+
reply_to="custom/reply/topic",
|
|
267
280
|
timeout=60.0
|
|
268
281
|
)
|
|
269
282
|
"""
|
|
270
283
|
if not self._client.is_connected:
|
|
271
284
|
raise MQTTXError("MQTT 客户端未连接", ErrorCode.NOT_CONNECTED)
|
|
272
285
|
|
|
286
|
+
# 自动注入 reply_to
|
|
273
287
|
if reply_to is None:
|
|
274
|
-
|
|
288
|
+
reply_to = self._my_topic
|
|
289
|
+
|
|
290
|
+
if reply_to is None:
|
|
291
|
+
raise ValueError(
|
|
292
|
+
"reply_to 参数是必需的,或在初始化时提供 my_topic"
|
|
293
|
+
)
|
|
275
294
|
|
|
276
295
|
# 生成请求
|
|
277
296
|
request_id = str(uuid.uuid4())
|
|
@@ -295,8 +314,8 @@ class RPCManager:
|
|
|
295
314
|
future = asyncio.get_event_loop().create_future()
|
|
296
315
|
self._pending_calls[request_id] = future
|
|
297
316
|
|
|
298
|
-
#
|
|
299
|
-
payload = request.encode(
|
|
317
|
+
# 发送请求
|
|
318
|
+
payload = request.encode()
|
|
300
319
|
await self._client.raw.publish(topic, payload, qos=1)
|
|
301
320
|
logger.debug(f"RPC 请求已发送 - method: {method}, request_id: {request_id[:8]}")
|
|
302
321
|
|
|
@@ -445,6 +464,6 @@ class RPCManager:
|
|
|
445
464
|
topic: 响应主题
|
|
446
465
|
response: RPC 响应消息
|
|
447
466
|
"""
|
|
448
|
-
payload = response.encode(
|
|
467
|
+
payload = response.encode()
|
|
449
468
|
await self._client.raw.publish(topic, payload, qos=1)
|
|
450
469
|
logger.debug(f"RPC 响应已发送 - request_id: {response.request_id[:8]}")
|