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.
Files changed (73) hide show
  1. {meshcore-2.3.5 → meshcore-2.3.6}/PKG-INFO +1 -1
  2. {meshcore-2.3.5 → meshcore-2.3.6}/pyproject.toml +1 -1
  3. {meshcore-2.3.5 → meshcore-2.3.6}/src/meshcore/commands/base.py +1 -0
  4. meshcore-2.3.6/src/meshcore/commands/binary.py +357 -0
  5. {meshcore-2.3.5 → meshcore-2.3.6}/src/meshcore/commands/messaging.py +48 -10
  6. {meshcore-2.3.5 → meshcore-2.3.6}/tests/unit/test_commands.py +33 -43
  7. meshcore-2.3.6/tests/unit/test_lpp_parsing.py +39 -0
  8. {meshcore-2.3.5 → meshcore-2.3.6}/tests/unit/test_private_key_export.py +18 -9
  9. {meshcore-2.3.5 → meshcore-2.3.6}/tests/unit/test_reader.py +4 -3
  10. meshcore-2.3.5/src/meshcore/commands/binary.py +0 -356
  11. {meshcore-2.3.5 → meshcore-2.3.6}/.github/python-test.yml +0 -0
  12. {meshcore-2.3.5 → meshcore-2.3.6}/.gitignore +0 -0
  13. {meshcore-2.3.5 → meshcore-2.3.6}/LICENSE +0 -0
  14. {meshcore-2.3.5 → meshcore-2.3.6}/README.md +0 -0
  15. {meshcore-2.3.5 → meshcore-2.3.6}/examples/.pubsub_example.py.swp +0 -0
  16. {meshcore-2.3.5 → meshcore-2.3.6}/examples/ble_chat.py +0 -0
  17. {meshcore-2.3.5 → meshcore-2.3.6}/examples/ble_pin_pairing_example.py +0 -0
  18. {meshcore-2.3.5 → meshcore-2.3.6}/examples/ble_private_key_export.py +0 -0
  19. {meshcore-2.3.5 → meshcore-2.3.6}/examples/ble_sign_example.py +0 -0
  20. {meshcore-2.3.5 → meshcore-2.3.6}/examples/ble_stats.py +0 -0
  21. {meshcore-2.3.5 → meshcore-2.3.6}/examples/ble_t1000_chan_msg.py +0 -0
  22. {meshcore-2.3.5 → meshcore-2.3.6}/examples/ble_t1000_custom_vars.py +0 -0
  23. {meshcore-2.3.5 → meshcore-2.3.6}/examples/ble_t1000_infos.py +0 -0
  24. {meshcore-2.3.5 → meshcore-2.3.6}/examples/ble_t1000_msg.py +0 -0
  25. {meshcore-2.3.5 → meshcore-2.3.6}/examples/ble_t1000_msg_retries.py +0 -0
  26. {meshcore-2.3.5 → meshcore-2.3.6}/examples/ble_t1000_set_cv.py +0 -0
  27. {meshcore-2.3.5 → meshcore-2.3.6}/examples/chan_recv_with_path.py +0 -0
  28. {meshcore-2.3.5 → meshcore-2.3.6}/examples/connection_events_example.py +0 -0
  29. {meshcore-2.3.5 → meshcore-2.3.6}/examples/mepo_mc_gps.py +0 -0
  30. {meshcore-2.3.5 → meshcore-2.3.6}/examples/pubsub_example.py +0 -0
  31. {meshcore-2.3.5 → meshcore-2.3.6}/examples/rf_packet_monitor.py +0 -0
  32. {meshcore-2.3.5 → meshcore-2.3.6}/examples/serial_battery_monitor.py +0 -0
  33. {meshcore-2.3.5 → meshcore-2.3.6}/examples/serial_channel_manager.py +0 -0
  34. {meshcore-2.3.5 → meshcore-2.3.6}/examples/serial_chat.py +0 -0
  35. {meshcore-2.3.5 → meshcore-2.3.6}/examples/serial_contacts.py +0 -0
  36. {meshcore-2.3.5 → meshcore-2.3.6}/examples/serial_infos.py +0 -0
  37. {meshcore-2.3.5 → meshcore-2.3.6}/examples/serial_meshcore_ollama.py +0 -0
  38. {meshcore-2.3.5 → meshcore-2.3.6}/examples/serial_msg.py +0 -0
  39. {meshcore-2.3.5 → meshcore-2.3.6}/examples/serial_pingbot.py +0 -0
  40. {meshcore-2.3.5 → meshcore-2.3.6}/examples/serial_repeater_status.py +0 -0
  41. {meshcore-2.3.5 → meshcore-2.3.6}/examples/serial_repeater_telemetry.py +0 -0
  42. {meshcore-2.3.5 → meshcore-2.3.6}/examples/serial_rss_bot.py +0 -0
  43. {meshcore-2.3.5 → meshcore-2.3.6}/examples/serial_trace.py +0 -0
  44. {meshcore-2.3.5 → meshcore-2.3.6}/examples/tcp_chat.py +0 -0
  45. {meshcore-2.3.5 → meshcore-2.3.6}/examples/tcp_login_status.py +0 -0
  46. {meshcore-2.3.5 → meshcore-2.3.6}/examples/tcp_mchome_contacts.py +0 -0
  47. {meshcore-2.3.5 → meshcore-2.3.6}/examples/tcp_mchome_infos.py +0 -0
  48. {meshcore-2.3.5 → meshcore-2.3.6}/examples/tcp_mchome_msg.py +0 -0
  49. {meshcore-2.3.5 → meshcore-2.3.6}/examples/tcp_mchome_readmsgs.py +0 -0
  50. {meshcore-2.3.5 → meshcore-2.3.6}/pytest.ini +0 -0
  51. {meshcore-2.3.5 → meshcore-2.3.6}/src/meshcore/__init__.py +0 -0
  52. {meshcore-2.3.5 → meshcore-2.3.6}/src/meshcore/ble_cx.py +0 -0
  53. {meshcore-2.3.5 → meshcore-2.3.6}/src/meshcore/commands/__init__.py +0 -0
  54. {meshcore-2.3.5 → meshcore-2.3.6}/src/meshcore/commands/contact.py +0 -0
  55. {meshcore-2.3.5 → meshcore-2.3.6}/src/meshcore/commands/control_data.py +0 -0
  56. {meshcore-2.3.5 → meshcore-2.3.6}/src/meshcore/commands/device.py +0 -0
  57. {meshcore-2.3.5 → meshcore-2.3.6}/src/meshcore/connection_manager.py +0 -0
  58. {meshcore-2.3.5 → meshcore-2.3.6}/src/meshcore/events.py +0 -0
  59. {meshcore-2.3.5 → meshcore-2.3.6}/src/meshcore/lpp_json_encoder.py +0 -0
  60. {meshcore-2.3.5 → meshcore-2.3.6}/src/meshcore/meshcore.py +0 -0
  61. {meshcore-2.3.5 → meshcore-2.3.6}/src/meshcore/meshcore_parser.py +0 -0
  62. {meshcore-2.3.5 → meshcore-2.3.6}/src/meshcore/packets.py +0 -0
  63. {meshcore-2.3.5 → meshcore-2.3.6}/src/meshcore/parsing.py +0 -0
  64. {meshcore-2.3.5 → meshcore-2.3.6}/src/meshcore/reader.py +0 -0
  65. {meshcore-2.3.5 → meshcore-2.3.6}/src/meshcore/serial_cx.py +0 -0
  66. {meshcore-2.3.5 → meshcore-2.3.6}/src/meshcore/tcp_cx.py +0 -0
  67. {meshcore-2.3.5 → meshcore-2.3.6}/tests/README.md +0 -0
  68. {meshcore-2.3.5 → meshcore-2.3.6}/tests/test_ble_connection.py +0 -0
  69. {meshcore-2.3.5 → meshcore-2.3.6}/tests/test_ble_pin_pairing.py +0 -0
  70. {meshcore-2.3.5 → meshcore-2.3.6}/tests/test_meshcore_ble_pin.py +0 -0
  71. {meshcore-2.3.5 → meshcore-2.3.6}/tests/unit/test_events.py +0 -0
  72. {meshcore-2.3.5 → meshcore-2.3.6}/tests/unit/test_path_discovery_response.py +0 -0
  73. {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.5
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "meshcore"
7
- version = "2.3.5"
7
+ version = "2.3.6"
8
8
  authors = [
9
9
  { name="Florent de Lamotte", email="florent@frizoncorrea.fr" },
10
10
  { name="Alex Wolden", email="awolden@gmail.com" },
@@ -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 send_login(self, dst: DestinationType, pwd: str) -> Event:
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 {flood}")
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: {attempts + 1}")
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 send_path_discovery(self, dst: DestinationType) -> Event:
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
- async def wait_response(requested_type, filters=None, timeout=None):
42
- if requested_type == event_type:
43
- if filters and attribute_filters:
44
- if not all(
45
- attribute_filters.get(key) == value
46
- for key, value in filters.items()
47
- ):
48
- return None
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.wait_for_event.side_effect = wait_response
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": "timeout"}
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("0123456789abcdef", "password")
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
- dst = "0123456789abcdef"
202
- await command_handler.reset_path(dst)
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
- dst = "0123456789abcdef"
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
- dst = "0123456789abcdef"
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
- dst = "0123456789abcdef"
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
- dst = "0123456789abcdef"
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
- dst = "0123456789abcdef"
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="01,23,45"
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