meshcore 2.3.5__tar.gz → 2.3.6__tar.gz
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.
- {meshcore-2.3.5 → meshcore-2.3.6}/PKG-INFO +1 -1
- {meshcore-2.3.5 → meshcore-2.3.6}/pyproject.toml +1 -1
- {meshcore-2.3.5 → meshcore-2.3.6}/src/meshcore/commands/base.py +1 -0
- meshcore-2.3.6/src/meshcore/commands/binary.py +357 -0
- {meshcore-2.3.5 → meshcore-2.3.6}/src/meshcore/commands/messaging.py +48 -10
- {meshcore-2.3.5 → meshcore-2.3.6}/tests/unit/test_commands.py +33 -43
- meshcore-2.3.6/tests/unit/test_lpp_parsing.py +39 -0
- {meshcore-2.3.5 → meshcore-2.3.6}/tests/unit/test_private_key_export.py +18 -9
- {meshcore-2.3.5 → meshcore-2.3.6}/tests/unit/test_reader.py +4 -3
- meshcore-2.3.5/src/meshcore/commands/binary.py +0 -356
- {meshcore-2.3.5 → meshcore-2.3.6}/.github/python-test.yml +0 -0
- {meshcore-2.3.5 → meshcore-2.3.6}/.gitignore +0 -0
- {meshcore-2.3.5 → meshcore-2.3.6}/LICENSE +0 -0
- {meshcore-2.3.5 → meshcore-2.3.6}/README.md +0 -0
- {meshcore-2.3.5 → meshcore-2.3.6}/examples/.pubsub_example.py.swp +0 -0
- {meshcore-2.3.5 → meshcore-2.3.6}/examples/ble_chat.py +0 -0
- {meshcore-2.3.5 → meshcore-2.3.6}/examples/ble_pin_pairing_example.py +0 -0
- {meshcore-2.3.5 → meshcore-2.3.6}/examples/ble_private_key_export.py +0 -0
- {meshcore-2.3.5 → meshcore-2.3.6}/examples/ble_sign_example.py +0 -0
- {meshcore-2.3.5 → meshcore-2.3.6}/examples/ble_stats.py +0 -0
- {meshcore-2.3.5 → meshcore-2.3.6}/examples/ble_t1000_chan_msg.py +0 -0
- {meshcore-2.3.5 → meshcore-2.3.6}/examples/ble_t1000_custom_vars.py +0 -0
- {meshcore-2.3.5 → meshcore-2.3.6}/examples/ble_t1000_infos.py +0 -0
- {meshcore-2.3.5 → meshcore-2.3.6}/examples/ble_t1000_msg.py +0 -0
- {meshcore-2.3.5 → meshcore-2.3.6}/examples/ble_t1000_msg_retries.py +0 -0
- {meshcore-2.3.5 → meshcore-2.3.6}/examples/ble_t1000_set_cv.py +0 -0
- {meshcore-2.3.5 → meshcore-2.3.6}/examples/chan_recv_with_path.py +0 -0
- {meshcore-2.3.5 → meshcore-2.3.6}/examples/connection_events_example.py +0 -0
- {meshcore-2.3.5 → meshcore-2.3.6}/examples/mepo_mc_gps.py +0 -0
- {meshcore-2.3.5 → meshcore-2.3.6}/examples/pubsub_example.py +0 -0
- {meshcore-2.3.5 → meshcore-2.3.6}/examples/rf_packet_monitor.py +0 -0
- {meshcore-2.3.5 → meshcore-2.3.6}/examples/serial_battery_monitor.py +0 -0
- {meshcore-2.3.5 → meshcore-2.3.6}/examples/serial_channel_manager.py +0 -0
- {meshcore-2.3.5 → meshcore-2.3.6}/examples/serial_chat.py +0 -0
- {meshcore-2.3.5 → meshcore-2.3.6}/examples/serial_contacts.py +0 -0
- {meshcore-2.3.5 → meshcore-2.3.6}/examples/serial_infos.py +0 -0
- {meshcore-2.3.5 → meshcore-2.3.6}/examples/serial_meshcore_ollama.py +0 -0
- {meshcore-2.3.5 → meshcore-2.3.6}/examples/serial_msg.py +0 -0
- {meshcore-2.3.5 → meshcore-2.3.6}/examples/serial_pingbot.py +0 -0
- {meshcore-2.3.5 → meshcore-2.3.6}/examples/serial_repeater_status.py +0 -0
- {meshcore-2.3.5 → meshcore-2.3.6}/examples/serial_repeater_telemetry.py +0 -0
- {meshcore-2.3.5 → meshcore-2.3.6}/examples/serial_rss_bot.py +0 -0
- {meshcore-2.3.5 → meshcore-2.3.6}/examples/serial_trace.py +0 -0
- {meshcore-2.3.5 → meshcore-2.3.6}/examples/tcp_chat.py +0 -0
- {meshcore-2.3.5 → meshcore-2.3.6}/examples/tcp_login_status.py +0 -0
- {meshcore-2.3.5 → meshcore-2.3.6}/examples/tcp_mchome_contacts.py +0 -0
- {meshcore-2.3.5 → meshcore-2.3.6}/examples/tcp_mchome_infos.py +0 -0
- {meshcore-2.3.5 → meshcore-2.3.6}/examples/tcp_mchome_msg.py +0 -0
- {meshcore-2.3.5 → meshcore-2.3.6}/examples/tcp_mchome_readmsgs.py +0 -0
- {meshcore-2.3.5 → meshcore-2.3.6}/pytest.ini +0 -0
- {meshcore-2.3.5 → meshcore-2.3.6}/src/meshcore/__init__.py +0 -0
- {meshcore-2.3.5 → meshcore-2.3.6}/src/meshcore/ble_cx.py +0 -0
- {meshcore-2.3.5 → meshcore-2.3.6}/src/meshcore/commands/__init__.py +0 -0
- {meshcore-2.3.5 → meshcore-2.3.6}/src/meshcore/commands/contact.py +0 -0
- {meshcore-2.3.5 → meshcore-2.3.6}/src/meshcore/commands/control_data.py +0 -0
- {meshcore-2.3.5 → meshcore-2.3.6}/src/meshcore/commands/device.py +0 -0
- {meshcore-2.3.5 → meshcore-2.3.6}/src/meshcore/connection_manager.py +0 -0
- {meshcore-2.3.5 → meshcore-2.3.6}/src/meshcore/events.py +0 -0
- {meshcore-2.3.5 → meshcore-2.3.6}/src/meshcore/lpp_json_encoder.py +0 -0
- {meshcore-2.3.5 → meshcore-2.3.6}/src/meshcore/meshcore.py +0 -0
- {meshcore-2.3.5 → meshcore-2.3.6}/src/meshcore/meshcore_parser.py +0 -0
- {meshcore-2.3.5 → meshcore-2.3.6}/src/meshcore/packets.py +0 -0
- {meshcore-2.3.5 → meshcore-2.3.6}/src/meshcore/parsing.py +0 -0
- {meshcore-2.3.5 → meshcore-2.3.6}/src/meshcore/reader.py +0 -0
- {meshcore-2.3.5 → meshcore-2.3.6}/src/meshcore/serial_cx.py +0 -0
- {meshcore-2.3.5 → meshcore-2.3.6}/src/meshcore/tcp_cx.py +0 -0
- {meshcore-2.3.5 → meshcore-2.3.6}/tests/README.md +0 -0
- {meshcore-2.3.5 → meshcore-2.3.6}/tests/test_ble_connection.py +0 -0
- {meshcore-2.3.5 → meshcore-2.3.6}/tests/test_ble_pin_pairing.py +0 -0
- {meshcore-2.3.5 → meshcore-2.3.6}/tests/test_meshcore_ble_pin.py +0 -0
- {meshcore-2.3.5 → meshcore-2.3.6}/tests/unit/test_events.py +0 -0
- {meshcore-2.3.5 → meshcore-2.3.6}/tests/unit/test_path_discovery_response.py +0 -0
- {meshcore-2.3.5 → meshcore-2.3.6}/tests/unit/test_serial_connection.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: meshcore
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.6
|
|
4
4
|
Summary: Base classes for communicating with meshcore companion radios
|
|
5
5
|
Project-URL: Homepage, https://github.com/fdlamotte/meshcore_py
|
|
6
6
|
Project-URL: Issues, https://github.com/fdlamotte/meshcore_py/issues
|
|
@@ -64,6 +64,7 @@ class CommandHandlerBase:
|
|
|
64
64
|
self._sender_func: Optional[Callable[[bytes], Coroutine[Any, Any, None]]] = None
|
|
65
65
|
self._reader: Optional[MessageReader] = None
|
|
66
66
|
self.dispatcher: Optional[EventDispatcher] = None
|
|
67
|
+
self._mesh_request_lock = asyncio.Lock()
|
|
67
68
|
self.default_timeout = (
|
|
68
69
|
default_timeout if default_timeout is not None else self.DEFAULT_TIMEOUT
|
|
69
70
|
)
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
import random
|
|
4
|
+
import io
|
|
5
|
+
|
|
6
|
+
from .base import CommandHandlerBase
|
|
7
|
+
from ..events import EventType
|
|
8
|
+
from ..packets import BinaryReqType
|
|
9
|
+
from ..packets import AnonReqType
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger("meshcore")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class BinaryCommandHandler(CommandHandlerBase):
|
|
15
|
+
"""Helper functions to handle binary requests through binary commands"""
|
|
16
|
+
|
|
17
|
+
async def req_status(self, contact, timeout=0, min_timeout=0):
|
|
18
|
+
logger.error("*** please consider using req_status_sync instead of req_status")
|
|
19
|
+
return await self.req_status_sync(contact, timeout, min_timeout)
|
|
20
|
+
|
|
21
|
+
async def req_status_sync(self, contact, timeout=0, min_timeout=0):
|
|
22
|
+
async with self._mesh_request_lock:
|
|
23
|
+
res = await self.send_binary_req(
|
|
24
|
+
contact,
|
|
25
|
+
BinaryReqType.STATUS,
|
|
26
|
+
timeout=timeout,
|
|
27
|
+
min_timeout=min_timeout
|
|
28
|
+
)
|
|
29
|
+
if res.type == EventType.ERROR:
|
|
30
|
+
return None
|
|
31
|
+
|
|
32
|
+
exp_tag = res.payload["expected_ack"].hex()
|
|
33
|
+
timeout = res.payload["suggested_timeout"] / 800 if timeout == 0 else timeout
|
|
34
|
+
timeout = timeout if min_timeout < timeout else min_timeout
|
|
35
|
+
|
|
36
|
+
if self.dispatcher is None:
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
status_event = await self.dispatcher.wait_for_event(
|
|
40
|
+
EventType.STATUS_RESPONSE,
|
|
41
|
+
attribute_filters={"tag": exp_tag},
|
|
42
|
+
timeout=timeout,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
return status_event.payload if status_event else None
|
|
46
|
+
|
|
47
|
+
async def req_telemetry(self, contact, timeout=0, min_timeout=0):
|
|
48
|
+
logger.error("*** please consider using req_telemetry_sync instead of req_telemetry")
|
|
49
|
+
return await self.req_telemetry_sync(contact, timeout, min_timeout)
|
|
50
|
+
|
|
51
|
+
async def req_telemetry_sync(self, contact, timeout=0, min_timeout=0):
|
|
52
|
+
async with self._mesh_request_lock:
|
|
53
|
+
res = await self.send_binary_req(
|
|
54
|
+
contact,
|
|
55
|
+
BinaryReqType.TELEMETRY,
|
|
56
|
+
timeout=timeout,
|
|
57
|
+
min_timeout=min_timeout
|
|
58
|
+
)
|
|
59
|
+
if res.type == EventType.ERROR:
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
timeout = res.payload["suggested_timeout"] / 800 if timeout == 0 else timeout
|
|
63
|
+
timeout = timeout if min_timeout < timeout else min_timeout
|
|
64
|
+
|
|
65
|
+
if self.dispatcher is None:
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
telem_event = await self.dispatcher.wait_for_event(
|
|
69
|
+
EventType.TELEMETRY_RESPONSE,
|
|
70
|
+
attribute_filters={"tag": res.payload["expected_ack"].hex()},
|
|
71
|
+
timeout=timeout,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
return telem_event.payload["lpp"] if telem_event else None
|
|
75
|
+
|
|
76
|
+
async def req_mma(self, contact, timeout=0, min_timeout=0):
|
|
77
|
+
logger.error("*** please consider using req_mma_sync instead of req_mma")
|
|
78
|
+
return await self.req_mma_sync(contact, start, end, timeout,min_timeout)
|
|
79
|
+
|
|
80
|
+
async def req_mma_sync(self, contact, start, end, timeout=0,min_timeout=0):
|
|
81
|
+
async with self._mesh_request_lock:
|
|
82
|
+
req = (
|
|
83
|
+
start.to_bytes(4, "little", signed=False)
|
|
84
|
+
+ end.to_bytes(4, "little", signed=False)
|
|
85
|
+
+ b"\0\0"
|
|
86
|
+
)
|
|
87
|
+
res = await self.send_binary_req(
|
|
88
|
+
contact,
|
|
89
|
+
BinaryReqType.MMA,
|
|
90
|
+
data=req,
|
|
91
|
+
timeout=timeout
|
|
92
|
+
)
|
|
93
|
+
if res.type == EventType.ERROR:
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
timeout = res.payload["suggested_timeout"] / 800 if timeout == 0 else timeout
|
|
97
|
+
timeout = timeout if min_timeout < timeout else min_timeout
|
|
98
|
+
|
|
99
|
+
if self.dispatcher is None:
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
mma_event = await self.dispatcher.wait_for_event(
|
|
103
|
+
EventType.MMA_RESPONSE,
|
|
104
|
+
attribute_filters={"tag": res.payload["expected_ack"].hex()},
|
|
105
|
+
timeout=timeout,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
return mma_event.payload["mma_data"] if mma_event else None
|
|
109
|
+
|
|
110
|
+
async def req_acl(self, contact, timeout=0, min_timeout=0):
|
|
111
|
+
logger.error("*** please consider using req_acl_sync instead of req_acl")
|
|
112
|
+
return await self.req_acl_sync(contact, timeout, min_timeout)
|
|
113
|
+
|
|
114
|
+
async def req_acl_sync(self, contact, timeout=0, min_timeout=0):
|
|
115
|
+
async with self._mesh_request_lock:
|
|
116
|
+
req = b"\0\0"
|
|
117
|
+
res = await self.send_binary_req(
|
|
118
|
+
contact,
|
|
119
|
+
BinaryReqType.ACL,
|
|
120
|
+
data=req,
|
|
121
|
+
timeout=timeout
|
|
122
|
+
)
|
|
123
|
+
if res.type == EventType.ERROR:
|
|
124
|
+
return None
|
|
125
|
+
|
|
126
|
+
timeout = res.payload["suggested_timeout"] / 800 if timeout == 0 else timeout
|
|
127
|
+
timeout = timeout if timeout > min_timeout else min_timeout
|
|
128
|
+
|
|
129
|
+
if self.dispatcher is None:
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
acl_event = await self.dispatcher.wait_for_event(
|
|
133
|
+
EventType.ACL_RESPONSE,
|
|
134
|
+
attribute_filters={"tag": res.payload["expected_ack"].hex()},
|
|
135
|
+
timeout=timeout,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
return acl_event.payload["acl_data"] if acl_event else None
|
|
139
|
+
|
|
140
|
+
async def req_neighbours_async(self,
|
|
141
|
+
contact,
|
|
142
|
+
count=255,
|
|
143
|
+
offset=0,
|
|
144
|
+
order_by=0,
|
|
145
|
+
pubkey_prefix_length=4,
|
|
146
|
+
timeout=0,
|
|
147
|
+
min_timeout=0
|
|
148
|
+
):
|
|
149
|
+
req = (b"\x00" # version : 0
|
|
150
|
+
+ count.to_bytes(1, "little", signed=False)
|
|
151
|
+
+ offset.to_bytes(2, "little", signed=False)
|
|
152
|
+
+ order_by.to_bytes(1, "little", signed=False)
|
|
153
|
+
+ pubkey_prefix_length.to_bytes(1, "little", signed=False)
|
|
154
|
+
+ random.randint(1, 0xFFFFFFFF).to_bytes(4, "little", signed=False)
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
logger.debug(f"Sending binary neighbours req, count: {count}, offset: {offset} {req.hex()}")
|
|
158
|
+
|
|
159
|
+
return await self.send_binary_req (
|
|
160
|
+
contact,
|
|
161
|
+
BinaryReqType.NEIGHBOURS,
|
|
162
|
+
data=req,
|
|
163
|
+
timeout=timeout,
|
|
164
|
+
context={"pubkey_prefix_length": pubkey_prefix_length}
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
async def req_neighbours_sync(self,
|
|
168
|
+
contact,
|
|
169
|
+
count=255,
|
|
170
|
+
offset=0,
|
|
171
|
+
order_by=0,
|
|
172
|
+
pubkey_prefix_length=4,
|
|
173
|
+
timeout=0,
|
|
174
|
+
min_timeout=0
|
|
175
|
+
):
|
|
176
|
+
async with self._mesh_request_lock:
|
|
177
|
+
res = await self.req_neighbours_async(contact,
|
|
178
|
+
count=count,
|
|
179
|
+
offset=offset,
|
|
180
|
+
order_by=order_by,
|
|
181
|
+
pubkey_prefix_length=pubkey_prefix_length,
|
|
182
|
+
timeout=timeout,
|
|
183
|
+
min_timeout=min_timeout)
|
|
184
|
+
|
|
185
|
+
if res is None or res.type == EventType.ERROR:
|
|
186
|
+
return None
|
|
187
|
+
|
|
188
|
+
timeout = res.payload["suggested_timeout"] / 800 if timeout == 0 else timeout
|
|
189
|
+
timeout = timeout if min_timeout < timeout else min_timeout
|
|
190
|
+
|
|
191
|
+
if self.dispatcher is None:
|
|
192
|
+
return None
|
|
193
|
+
|
|
194
|
+
neighbours_event = await self.dispatcher.wait_for_event(
|
|
195
|
+
EventType.NEIGHBOURS_RESPONSE,
|
|
196
|
+
attribute_filters={"tag": res.payload["expected_ack"].hex()},
|
|
197
|
+
timeout=timeout,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
return neighbours_event.payload if neighbours_event else None
|
|
201
|
+
|
|
202
|
+
# do several queries if not all neighbours have been obtained
|
|
203
|
+
async def fetch_all_neighbours(self,
|
|
204
|
+
contact,
|
|
205
|
+
order_by=0,
|
|
206
|
+
pubkey_prefix_length=4,
|
|
207
|
+
timeout=0,
|
|
208
|
+
min_timeout=0
|
|
209
|
+
):
|
|
210
|
+
|
|
211
|
+
# Initial request
|
|
212
|
+
res = await self.req_neighbours_sync(contact,
|
|
213
|
+
count=255,
|
|
214
|
+
offset=0,
|
|
215
|
+
order_by=order_by,
|
|
216
|
+
pubkey_prefix_length=pubkey_prefix_length,
|
|
217
|
+
timeout=timeout,
|
|
218
|
+
min_timeout=min_timeout)
|
|
219
|
+
|
|
220
|
+
if res is None:
|
|
221
|
+
return None
|
|
222
|
+
|
|
223
|
+
neighbours_count = res["neighbours_count"] # total neighbours
|
|
224
|
+
results_count = res["results_count"] # obtained neighbours
|
|
225
|
+
|
|
226
|
+
del res["tag"]
|
|
227
|
+
|
|
228
|
+
tries = 0
|
|
229
|
+
while results_count < neighbours_count:
|
|
230
|
+
#await asyncio.sleep(2) # wait 2s before next fetch
|
|
231
|
+
next_res = await self.req_neighbours_sync(contact,
|
|
232
|
+
count=255,
|
|
233
|
+
offset=results_count,
|
|
234
|
+
order_by=order_by,
|
|
235
|
+
pubkey_prefix_length=pubkey_prefix_length,
|
|
236
|
+
timeout=timeout,
|
|
237
|
+
min_timeout=min_timeout+25) # requests are close and responses big,
|
|
238
|
+
#so let's have some more timeout for duty cycle
|
|
239
|
+
if next_res is None :
|
|
240
|
+
if tries > 0:
|
|
241
|
+
return res # caller should check it has everything
|
|
242
|
+
else:
|
|
243
|
+
tries += 1
|
|
244
|
+
else:
|
|
245
|
+
tries = 0
|
|
246
|
+
results_count += next_res["results_count"]
|
|
247
|
+
res["results_count"] = results_count
|
|
248
|
+
res["neighbours"] += next_res["neighbours"]
|
|
249
|
+
|
|
250
|
+
return res
|
|
251
|
+
|
|
252
|
+
async def req_regions_async(self, contact, timeout=0, min_timeout=0):
|
|
253
|
+
req = b"\0" # The return path, I currently do nothing with, so direct only
|
|
254
|
+
return await self.send_anon_req(
|
|
255
|
+
contact,
|
|
256
|
+
AnonReqType.REGIONS,
|
|
257
|
+
data=req,
|
|
258
|
+
timeout=timeout
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
async def req_regions_sync(self, contact, timeout=0, min_timeout=0):
|
|
262
|
+
async with self._mesh_request_lock:
|
|
263
|
+
res = await self.req_regions_async(contact, timeout, min_timeout)
|
|
264
|
+
|
|
265
|
+
if res.type == EventType.ERROR:
|
|
266
|
+
return None
|
|
267
|
+
|
|
268
|
+
timeout = res.payload["suggested_timeout"] / 800 if timeout == 0 else timeout
|
|
269
|
+
timeout = timeout if timeout > min_timeout else min_timeout
|
|
270
|
+
|
|
271
|
+
if self.dispatcher is None:
|
|
272
|
+
return None
|
|
273
|
+
|
|
274
|
+
region_event = await self.dispatcher.wait_for_event(
|
|
275
|
+
EventType.BINARY_RESPONSE,
|
|
276
|
+
attribute_filters={"tag": res.payload["expected_ack"].hex()},
|
|
277
|
+
timeout=timeout,
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
if region_event is None:
|
|
281
|
+
return None
|
|
282
|
+
|
|
283
|
+
pkt = bytes().fromhex(region_event.payload["data"])
|
|
284
|
+
pbuf = io.BytesIO(pkt)
|
|
285
|
+
tag_again = pbuf.read(4)
|
|
286
|
+
return pbuf.read().decode("utf-8", "ignore").strip("\x00")
|
|
287
|
+
|
|
288
|
+
async def req_owner_async(self, contact, timeout=0, min_timeout=0):
|
|
289
|
+
req = b"\0"
|
|
290
|
+
return await self.send_anon_req(
|
|
291
|
+
contact,
|
|
292
|
+
AnonReqType.OWNER,
|
|
293
|
+
data=req,
|
|
294
|
+
timeout=timeout
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
async def req_owner_sync(self, contact, timeout=0, min_timeout=0):
|
|
298
|
+
async with self._mesh_request_lock:
|
|
299
|
+
res = await self.req_owner_async(contact, timeout, min_timeout)
|
|
300
|
+
|
|
301
|
+
if res.type == EventType.ERROR:
|
|
302
|
+
return None
|
|
303
|
+
|
|
304
|
+
timeout = res.payload["suggested_timeout"] / 800 if timeout == 0 else timeout
|
|
305
|
+
timeout = timeout if timeout > min_timeout else min_timeout
|
|
306
|
+
|
|
307
|
+
if self.dispatcher is None:
|
|
308
|
+
return None
|
|
309
|
+
|
|
310
|
+
owner_event = await self.dispatcher.wait_for_event(
|
|
311
|
+
EventType.BINARY_RESPONSE,
|
|
312
|
+
attribute_filters={"tag": res.payload["expected_ack"].hex()},
|
|
313
|
+
timeout=timeout,
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
if owner_event is None:
|
|
317
|
+
return None
|
|
318
|
+
|
|
319
|
+
pkt = bytes().fromhex(owner_event.payload["data"])
|
|
320
|
+
pbuf = io.BytesIO(pkt)
|
|
321
|
+
tag_again = pbuf.read(4)
|
|
322
|
+
strings = pbuf.read().decode("utf-8", "ignore").split("\n", 1)
|
|
323
|
+
|
|
324
|
+
return dict(name=strings[0], owner=strings[1].strip("\x00"))
|
|
325
|
+
|
|
326
|
+
async def req_basic_async(self, contact, timeout=0, min_timeout=0):
|
|
327
|
+
req = b"\0"
|
|
328
|
+
return await self.send_anon_req(
|
|
329
|
+
contact,
|
|
330
|
+
AnonReqType.BASIC,
|
|
331
|
+
data=req,
|
|
332
|
+
timeout=timeout
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
async def req_basic_sync(self, contact, timeout=0, min_timeout=0):
|
|
336
|
+
async with self._mesh_request_lock:
|
|
337
|
+
res = await self.req_basic_async(contact, timeout, min_timeout)
|
|
338
|
+
|
|
339
|
+
if res.type == EventType.ERROR:
|
|
340
|
+
return None
|
|
341
|
+
|
|
342
|
+
timeout = res.payload["suggested_timeout"] / 800 if timeout == 0 else timeout
|
|
343
|
+
timeout = timeout if timeout > min_timeout else min_timeout
|
|
344
|
+
|
|
345
|
+
if self.dispatcher is None:
|
|
346
|
+
return None
|
|
347
|
+
|
|
348
|
+
basic_event = await self.dispatcher.wait_for_event(
|
|
349
|
+
EventType.BINARY_RESPONSE,
|
|
350
|
+
attribute_filters={"tag": res.payload["expected_ack"].hex()},
|
|
351
|
+
timeout=timeout,
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
if basic_event is None:
|
|
355
|
+
return None
|
|
356
|
+
|
|
357
|
+
return basic_event.payload
|
|
@@ -24,18 +24,37 @@ class MessagingCommands(CommandHandlerBase):
|
|
|
24
24
|
timeout,
|
|
25
25
|
)
|
|
26
26
|
|
|
27
|
-
async def
|
|
27
|
+
async def _send_login_raw(self, dst: DestinationType, pwd: str) -> Event:
|
|
28
28
|
dst_bytes = _validate_destination(dst, prefix_length=32)
|
|
29
29
|
logger.debug(f"Sending login request to: {dst_bytes.hex()}")
|
|
30
30
|
data = b"\x1a" + dst_bytes + pwd.encode("utf-8")
|
|
31
31
|
return await self.send(data, [EventType.MSG_SENT, EventType.ERROR])
|
|
32
32
|
|
|
33
|
+
async def send_login(self, dst: DestinationType, pwd: str) -> Event:
|
|
34
|
+
logger.warning("*** please consider using send_login_sync instead of send_login")
|
|
35
|
+
return await self._send_login_raw(dst, pwd)
|
|
36
|
+
|
|
37
|
+
async def send_login_sync(self, dst: DestinationType, pwd: str, timeout=0, min_timeout=0) -> Optional[Event]:
|
|
38
|
+
"""Send login to a remote node and wait for the response."""
|
|
39
|
+
async with self._mesh_request_lock:
|
|
40
|
+
result = await self._send_login_raw(dst, pwd)
|
|
41
|
+
if result is None or result.type == EventType.ERROR:
|
|
42
|
+
return None
|
|
43
|
+
timeout = result.payload["suggested_timeout"] / 800 if timeout == 0 else timeout
|
|
44
|
+
timeout = timeout if timeout > min_timeout else min_timeout
|
|
45
|
+
login_event = await self.dispatcher.wait_for_event(
|
|
46
|
+
EventType.LOGIN_SUCCESS,
|
|
47
|
+
timeout=timeout,
|
|
48
|
+
)
|
|
49
|
+
return login_event
|
|
50
|
+
|
|
33
51
|
async def send_logout(self, dst: DestinationType) -> Event:
|
|
34
52
|
dst_bytes = _validate_destination(dst, prefix_length=32)
|
|
35
53
|
data = b"\x1d" + dst_bytes
|
|
36
54
|
return await self.send(data, [EventType.OK, EventType.ERROR])
|
|
37
55
|
|
|
38
56
|
async def send_statusreq(self, dst: DestinationType) -> Event:
|
|
57
|
+
logger.warning("*** please consider using req_status_sync instead of send_statusreq")
|
|
39
58
|
dst_bytes = _validate_destination(dst, prefix_length=32)
|
|
40
59
|
logger.debug(f"Sending status request to: {dst_bytes.hex()}")
|
|
41
60
|
data = b"\x1b" + dst_bytes
|
|
@@ -85,7 +104,7 @@ class MessagingCommands(CommandHandlerBase):
|
|
|
85
104
|
self, dst: DestinationType, msg: str, timestamp: Optional[int] = None,
|
|
86
105
|
max_attempts=3, max_flood_attempts=2, flood_after=2, timeout=0, min_timeout=0
|
|
87
106
|
) -> Event:
|
|
88
|
-
|
|
107
|
+
|
|
89
108
|
try:
|
|
90
109
|
dst_bytes = _validate_destination(dst, prefix_length=32)
|
|
91
110
|
# with 32 bytes we can reset to flood
|
|
@@ -105,8 +124,8 @@ class MessagingCommands(CommandHandlerBase):
|
|
|
105
124
|
# we can't know if we're flood without fetching all contacts
|
|
106
125
|
# if we have a full key (meaning we can reset path) consider direct
|
|
107
126
|
# else consider flood
|
|
108
|
-
flood = len(dst_bytes) < 32
|
|
109
|
-
logger.info(f"send_msg_with_retry: can't determine if flood, assume
|
|
127
|
+
flood = len(dst_bytes) < 32
|
|
128
|
+
logger.info(f"send_msg_with_retry: can't determine if flood, assume {flood}")
|
|
110
129
|
res = None
|
|
111
130
|
while attempts < max_attempts and res is None \
|
|
112
131
|
and (not flood or flood_attempts < max_flood_attempts):
|
|
@@ -122,8 +141,8 @@ class MessagingCommands(CommandHandlerBase):
|
|
|
122
141
|
contact["out_path_len"] = -1
|
|
123
142
|
|
|
124
143
|
if attempts > 0:
|
|
125
|
-
logger.info(f"Retry sending msg:
|
|
126
|
-
|
|
144
|
+
logger.info(f"Retry sending msg: {attempts + 1}")
|
|
145
|
+
|
|
127
146
|
result = await self.send_msg(dst, msg, timestamp, attempt=attempts)
|
|
128
147
|
if result.type == EventType.ERROR:
|
|
129
148
|
logger.error(f"⚠️ Failed to send message: {result.payload}")
|
|
@@ -131,14 +150,14 @@ class MessagingCommands(CommandHandlerBase):
|
|
|
131
150
|
exp_ack = result.payload["expected_ack"].hex()
|
|
132
151
|
timeout = result.payload["suggested_timeout"] / 1000 * 1.2 if timeout==0 else timeout
|
|
133
152
|
timeout = timeout if timeout > min_timeout else min_timeout
|
|
134
|
-
res = await self.dispatcher.wait_for_event(EventType.ACK,
|
|
135
|
-
attribute_filters={"code": exp_ack},
|
|
153
|
+
res = await self.dispatcher.wait_for_event(EventType.ACK,
|
|
154
|
+
attribute_filters={"code": exp_ack},
|
|
136
155
|
timeout=timeout)
|
|
137
156
|
|
|
138
157
|
attempts = attempts + 1
|
|
139
158
|
if flood :
|
|
140
159
|
flood_attempts = flood_attempts + 1
|
|
141
|
-
|
|
160
|
+
|
|
142
161
|
return None if res is None else result
|
|
143
162
|
|
|
144
163
|
async def send_chan_msg(self, chan: int, msg: str, timestamp: Optional[int|bytes] = None) -> Event:
|
|
@@ -166,17 +185,36 @@ class MessagingCommands(CommandHandlerBase):
|
|
|
166
185
|
return await self.send(data, [EventType.OK, EventType.ERROR])
|
|
167
186
|
|
|
168
187
|
async def send_telemetry_req(self, dst: DestinationType) -> Event:
|
|
188
|
+
logger.warning("*** please consider using req_telemetry_sync instead of send_telemetry_req")
|
|
169
189
|
dst_bytes = _validate_destination(dst, prefix_length=32)
|
|
170
190
|
logger.debug(f"Asking telemetry to {dst_bytes.hex()}")
|
|
171
191
|
data = b"\x27\x00\x00\x00" + dst_bytes
|
|
172
192
|
return await self.send(data, [EventType.MSG_SENT, EventType.ERROR])
|
|
173
193
|
|
|
174
|
-
async def
|
|
194
|
+
async def _send_path_discovery_raw(self, dst: DestinationType) -> Event:
|
|
175
195
|
dst_bytes = _validate_destination(dst, prefix_length=32)
|
|
176
196
|
logger.debug(f"Path discovery request for {dst_bytes.hex()}")
|
|
177
197
|
data = b"\x34\x00" + dst_bytes
|
|
178
198
|
return await self.send(data, [EventType.MSG_SENT, EventType.ERROR])
|
|
179
199
|
|
|
200
|
+
async def send_path_discovery(self, dst: DestinationType) -> Event:
|
|
201
|
+
logger.warning("*** please consider using send_path_discovery_sync instead of send_path_discovery")
|
|
202
|
+
return await self._send_path_discovery_raw(dst)
|
|
203
|
+
|
|
204
|
+
async def send_path_discovery_sync(self, dst: DestinationType, timeout=0, min_timeout=0) -> Optional[Event]:
|
|
205
|
+
"""Send path discovery request and wait for the response."""
|
|
206
|
+
async with self._mesh_request_lock:
|
|
207
|
+
result = await self._send_path_discovery_raw(dst)
|
|
208
|
+
if result is None or result.type == EventType.ERROR:
|
|
209
|
+
return None
|
|
210
|
+
timeout = result.payload["suggested_timeout"] / 800 if timeout == 0 else timeout
|
|
211
|
+
timeout = timeout if timeout > min_timeout else min_timeout
|
|
212
|
+
path_event = await self.dispatcher.wait_for_event(
|
|
213
|
+
EventType.PATH_RESPONSE,
|
|
214
|
+
timeout=timeout,
|
|
215
|
+
)
|
|
216
|
+
return path_event
|
|
217
|
+
|
|
180
218
|
async def send_trace(
|
|
181
219
|
self,
|
|
182
220
|
auth_code: int = 0,
|
|
@@ -2,10 +2,12 @@ import pytest
|
|
|
2
2
|
import asyncio
|
|
3
3
|
from unittest.mock import MagicMock, AsyncMock
|
|
4
4
|
from meshcore.commands import CommandHandler
|
|
5
|
-
from meshcore.events import EventType, Event
|
|
5
|
+
from meshcore.events import EventType, Event, Subscription
|
|
6
6
|
|
|
7
7
|
pytestmark = pytest.mark.asyncio
|
|
8
8
|
|
|
9
|
+
VALID_PUBKEY_HEX = "0123456789abcdef" * 4 # 64 hex chars = 32 bytes
|
|
10
|
+
|
|
9
11
|
|
|
10
12
|
# Fixtures
|
|
11
13
|
@pytest.fixture
|
|
@@ -20,6 +22,15 @@ def mock_dispatcher():
|
|
|
20
22
|
dispatcher = MagicMock()
|
|
21
23
|
dispatcher.wait_for_event = AsyncMock()
|
|
22
24
|
dispatcher.dispatch = AsyncMock()
|
|
25
|
+
|
|
26
|
+
def fake_subscribe(event_type, handler, attribute_filters=None):
|
|
27
|
+
sub = MagicMock(spec=Subscription)
|
|
28
|
+
sub.unsubscribe = MagicMock()
|
|
29
|
+
dispatcher._last_subscribe_handler = handler
|
|
30
|
+
dispatcher._last_subscribe_event_type = event_type
|
|
31
|
+
return sub
|
|
32
|
+
|
|
33
|
+
dispatcher.subscribe = MagicMock(side_effect=fake_subscribe)
|
|
23
34
|
return dispatcher
|
|
24
35
|
|
|
25
36
|
|
|
@@ -36,20 +47,17 @@ def command_handler(mock_connection, mock_dispatcher):
|
|
|
36
47
|
return handler
|
|
37
48
|
|
|
38
49
|
|
|
39
|
-
# Test helper
|
|
40
50
|
def setup_event_response(mock_dispatcher, event_type, payload, attribute_filters=None):
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
return Event(event_type, payload)
|
|
50
|
-
return None
|
|
51
|
+
def fake_subscribe(evt_type, handler, attr_filters=None):
|
|
52
|
+
sub = MagicMock(spec=Subscription)
|
|
53
|
+
sub.unsubscribe = MagicMock()
|
|
54
|
+
if evt_type == event_type:
|
|
55
|
+
asyncio.get_event_loop().call_soon(
|
|
56
|
+
handler, Event(event_type, payload)
|
|
57
|
+
)
|
|
58
|
+
return sub
|
|
51
59
|
|
|
52
|
-
mock_dispatcher.
|
|
60
|
+
mock_dispatcher.subscribe = MagicMock(side_effect=fake_subscribe)
|
|
53
61
|
|
|
54
62
|
|
|
55
63
|
# Basic tests
|
|
@@ -72,11 +80,9 @@ async def test_send_with_event(command_handler, mock_connection, mock_dispatcher
|
|
|
72
80
|
|
|
73
81
|
|
|
74
82
|
async def test_send_timeout(command_handler, mock_connection, mock_dispatcher):
|
|
75
|
-
mock_dispatcher.wait_for_event.side_effect = asyncio.TimeoutError
|
|
76
|
-
|
|
77
83
|
result = await command_handler.send(b"test_command", [EventType.OK], timeout=0.1)
|
|
78
84
|
assert result.type == EventType.ERROR
|
|
79
|
-
assert result.payload == {"reason": "
|
|
85
|
+
assert result.payload == {"reason": "no_event_received"}
|
|
80
86
|
|
|
81
87
|
|
|
82
88
|
# Destination validation tests
|
|
@@ -106,7 +112,7 @@ async def test_validate_destination_contact_object(command_handler, mock_connect
|
|
|
106
112
|
|
|
107
113
|
# Command tests
|
|
108
114
|
async def test_send_login(command_handler, mock_connection):
|
|
109
|
-
await command_handler.send_login(
|
|
115
|
+
await command_handler.send_login(VALID_PUBKEY_HEX, "password")
|
|
110
116
|
|
|
111
117
|
assert mock_connection.send.call_args[0][0].startswith(b"\x1a")
|
|
112
118
|
assert b"\x01\x23\x45\x67\x89\xab" in mock_connection.send.call_args[0][0]
|
|
@@ -198,15 +204,14 @@ async def test_get_contacts(command_handler, mock_connection):
|
|
|
198
204
|
|
|
199
205
|
|
|
200
206
|
async def test_reset_path(command_handler, mock_connection):
|
|
201
|
-
|
|
202
|
-
await command_handler.reset_path(
|
|
207
|
+
command_handler._get_contact_by_prefix = lambda prefix: None
|
|
208
|
+
await command_handler.reset_path(VALID_PUBKEY_HEX)
|
|
203
209
|
assert mock_connection.send.call_args[0][0].startswith(b"\x0d")
|
|
204
210
|
assert b"\x01\x23\x45\x67\x89\xab" in mock_connection.send.call_args[0][0]
|
|
205
211
|
|
|
206
212
|
|
|
207
213
|
async def test_share_contact(command_handler, mock_connection):
|
|
208
|
-
|
|
209
|
-
await command_handler.share_contact(dst)
|
|
214
|
+
await command_handler.share_contact(VALID_PUBKEY_HEX)
|
|
210
215
|
assert mock_connection.send.call_args[0][0].startswith(b"\x10")
|
|
211
216
|
assert b"\x01\x23\x45\x67\x89\xab" in mock_connection.send.call_args[0][0]
|
|
212
217
|
|
|
@@ -218,15 +223,13 @@ async def test_export_contact(command_handler, mock_connection):
|
|
|
218
223
|
|
|
219
224
|
# Test exporting specific contact
|
|
220
225
|
mock_connection.reset_mock()
|
|
221
|
-
|
|
222
|
-
await command_handler.export_contact(dst)
|
|
226
|
+
await command_handler.export_contact(VALID_PUBKEY_HEX)
|
|
223
227
|
assert mock_connection.send.call_args[0][0].startswith(b"\x11")
|
|
224
228
|
assert b"\x01\x23\x45\x67\x89\xab" in mock_connection.send.call_args[0][0]
|
|
225
229
|
|
|
226
230
|
|
|
227
231
|
async def test_remove_contact(command_handler, mock_connection):
|
|
228
|
-
|
|
229
|
-
await command_handler.remove_contact(dst)
|
|
232
|
+
await command_handler.remove_contact(VALID_PUBKEY_HEX)
|
|
230
233
|
assert mock_connection.send.call_args[0][0].startswith(b"\x0f")
|
|
231
234
|
assert b"\x01\x23\x45\x67\x89\xab" in mock_connection.send.call_args[0][0]
|
|
232
235
|
|
|
@@ -242,15 +245,13 @@ async def test_get_msg(command_handler, mock_connection):
|
|
|
242
245
|
|
|
243
246
|
|
|
244
247
|
async def test_send_logout(command_handler, mock_connection):
|
|
245
|
-
|
|
246
|
-
await command_handler.send_logout(dst)
|
|
248
|
+
await command_handler.send_logout(VALID_PUBKEY_HEX)
|
|
247
249
|
assert mock_connection.send.call_args[0][0].startswith(b"\x1d")
|
|
248
250
|
assert b"\x01\x23\x45\x67\x89\xab" in mock_connection.send.call_args[0][0]
|
|
249
251
|
|
|
250
252
|
|
|
251
253
|
async def test_send_statusreq(command_handler, mock_connection):
|
|
252
|
-
|
|
253
|
-
await command_handler.send_statusreq(dst)
|
|
254
|
+
await command_handler.send_statusreq(VALID_PUBKEY_HEX)
|
|
254
255
|
assert mock_connection.send.call_args[0][0].startswith(b"\x1b")
|
|
255
256
|
assert b"\x01\x23\x45\x67\x89\xab" in mock_connection.send.call_args[0][0]
|
|
256
257
|
|
|
@@ -261,10 +262,10 @@ async def test_send_trace(command_handler, mock_connection):
|
|
|
261
262
|
first_call = mock_connection.send.call_args[0][0]
|
|
262
263
|
assert first_call.startswith(b"\x24") # 36 in decimal = 0x24 in hex
|
|
263
264
|
|
|
264
|
-
# Test with all parameters
|
|
265
|
+
# Test with all parameters (flags=1 means path_hash_len=2, so 4 hex chars each)
|
|
265
266
|
mock_connection.reset_mock()
|
|
266
267
|
await command_handler.send_trace(
|
|
267
|
-
auth_code=12345, tag=67890, flags=1, path="
|
|
268
|
+
auth_code=12345, tag=67890, flags=1, path="0123,2345,4567"
|
|
268
269
|
)
|
|
269
270
|
second_call = mock_connection.send.call_args[0][0]
|
|
270
271
|
assert second_call.startswith(b"\x24")
|
|
@@ -273,25 +274,14 @@ async def test_send_trace(command_handler, mock_connection):
|
|
|
273
274
|
async def test_send_with_multiple_expected_events_returns_first_completed(
|
|
274
275
|
command_handler, mock_connection, mock_dispatcher
|
|
275
276
|
):
|
|
276
|
-
# Setup the dispatcher to return an ERROR event
|
|
277
277
|
error_payload = {"reason": "command_failed"}
|
|
278
|
+
setup_event_response(mock_dispatcher, EventType.ERROR, error_payload)
|
|
278
279
|
|
|
279
|
-
async def simulate_error_event(*args, **kwargs):
|
|
280
|
-
# Simulate an ERROR event being returned
|
|
281
|
-
return Event(EventType.ERROR, error_payload)
|
|
282
|
-
|
|
283
|
-
# Patch the wait_for_event method to return our simulated event
|
|
284
|
-
mock_dispatcher.wait_for_event.side_effect = simulate_error_event
|
|
285
|
-
|
|
286
|
-
# Call send with both OK and ERROR in the expected_events list, with OK first
|
|
287
280
|
result = await command_handler.send(
|
|
288
281
|
b"test_command", [EventType.OK, EventType.ERROR]
|
|
289
282
|
)
|
|
290
283
|
|
|
291
|
-
# Verify the command was sent
|
|
292
284
|
mock_connection.send.assert_called_once_with(b"test_command")
|
|
293
|
-
|
|
294
|
-
# Verify that even though OK was listed first, the ERROR event was returned
|
|
295
285
|
assert result.type == EventType.ERROR
|
|
296
286
|
assert result.payload == error_payload
|
|
297
287
|
|