meshcore 2.3.14__tar.gz → 2.3.16__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.14 → meshcore-2.3.16}/PKG-INFO +1 -1
- {meshcore-2.3.14 → meshcore-2.3.16}/pyproject.toml +1 -1
- {meshcore-2.3.14 → meshcore-2.3.16}/src/meshcore/commands/messaging.py +42 -2
- {meshcore-2.3.14 → meshcore-2.3.16}/src/meshcore/lpp_json_encoder.py +1 -1
- {meshcore-2.3.14 → meshcore-2.3.16}/src/meshcore/meshcore.py +10 -5
- {meshcore-2.3.14 → meshcore-2.3.16}/src/meshcore/packets.py +1 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/src/meshcore/reader.py +1 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/tests/unit/test_asyncio_lifecycle.py +64 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/tests/unit/test_lpp_parsing.py +17 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/tests/unit/test_protocol_surface_gaps.py +116 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/.github/funding.yml +0 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/.github/python-test.yml +0 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/.gitignore +0 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/LICENSE +0 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/README.md +0 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/examples/.pubsub_example.py.swp +0 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/examples/ble_chat.py +0 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/examples/ble_pin_pairing_example.py +0 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/examples/ble_private_key_export.py +0 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/examples/ble_sign_example.py +0 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/examples/ble_stats.py +0 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/examples/ble_t1000_chan_msg.py +0 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/examples/ble_t1000_custom_vars.py +0 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/examples/ble_t1000_infos.py +0 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/examples/ble_t1000_msg.py +0 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/examples/ble_t1000_msg_retries.py +0 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/examples/ble_t1000_set_cv.py +0 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/examples/chan_recv_with_path.py +0 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/examples/connection_events_example.py +0 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/examples/mepo_mc_gps.py +0 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/examples/pubsub_example.py +0 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/examples/rf_packet_monitor.py +0 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/examples/serial_battery_monitor.py +0 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/examples/serial_channel_manager.py +0 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/examples/serial_chat.py +0 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/examples/serial_contacts.py +0 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/examples/serial_infos.py +0 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/examples/serial_meshcore_ollama.py +0 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/examples/serial_msg.py +0 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/examples/serial_pingbot.py +0 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/examples/serial_repeater_status.py +0 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/examples/serial_repeater_telemetry.py +0 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/examples/serial_rss_bot.py +0 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/examples/serial_trace.py +0 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/examples/tcp_chat.py +0 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/examples/tcp_login_status.py +0 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/examples/tcp_mchome_contacts.py +0 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/examples/tcp_mchome_infos.py +0 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/examples/tcp_mchome_msg.py +0 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/examples/tcp_mchome_readmsgs.py +0 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/pytest.ini +0 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/src/meshcore/__init__.py +0 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/src/meshcore/ble_cx.py +0 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/src/meshcore/commands/__init__.py +0 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/src/meshcore/commands/base.py +0 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/src/meshcore/commands/binary.py +0 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/src/meshcore/commands/contact.py +0 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/src/meshcore/commands/control_data.py +0 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/src/meshcore/commands/device.py +0 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/src/meshcore/connection_manager.py +0 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/src/meshcore/events.py +0 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/src/meshcore/meshcore_parser.py +0 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/src/meshcore/parsing.py +0 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/src/meshcore/serial_cx.py +0 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/src/meshcore/tcp_cx.py +0 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/tests/README.md +0 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/tests/test_ble_connection.py +0 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/tests/test_ble_pin_pairing.py +0 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/tests/test_meshcore_ble_pin.py +0 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/tests/unit/test_ble_reconnect_cleanup.py +0 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/tests/unit/test_commands.py +0 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/tests/unit/test_connection_manager.py +0 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/tests/unit/test_error_handling.py +0 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/tests/unit/test_events.py +0 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/tests/unit/test_parse_status.py +0 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/tests/unit/test_path_discovery_response.py +0 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/tests/unit/test_private_key_export.py +0 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/tests/unit/test_reader.py +0 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/tests/unit/test_rxlog_keyerror_regression.py +0 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/tests/unit/test_send_msg_with_retry.py +0 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/tests/unit/test_serial_connection.py +0 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/tests/unit/test_standalone_fixes.py +0 -0
- {meshcore-2.3.14 → meshcore-2.3.16}/tests/unit/test_transport_symmetry.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: meshcore
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.16
|
|
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
|
|
@@ -19,6 +19,7 @@ class MessagingCommands(CommandHandlerBase):
|
|
|
19
19
|
[
|
|
20
20
|
EventType.CONTACT_MSG_RECV,
|
|
21
21
|
EventType.CHANNEL_MSG_RECV,
|
|
22
|
+
EventType.CHANNEL_DATA_RECV,
|
|
22
23
|
EventType.ERROR,
|
|
23
24
|
EventType.NO_MORE_MSGS,
|
|
24
25
|
],
|
|
@@ -327,7 +328,7 @@ class MessagingCommands(CommandHandlerBase):
|
|
|
327
328
|
return Event(EventType.ERROR, {"reason": "unsupported_path_type"})
|
|
328
329
|
|
|
329
330
|
# Prepare the command packet: CMD(1) + tag(4) + auth_code(4) + flags(1) + [path]
|
|
330
|
-
cmd_data = bytearray([
|
|
331
|
+
cmd_data = bytearray([CommandType.SEND_TRACE_PATH.value]) # CMD_SEND_TRACE_PATH
|
|
331
332
|
cmd_data.extend(tag.to_bytes(4, "little"))
|
|
332
333
|
cmd_data.extend(auth_code.to_bytes(4, "little"))
|
|
333
334
|
cmd_data.append(flags)
|
|
@@ -365,7 +366,46 @@ class MessagingCommands(CommandHandlerBase):
|
|
|
365
366
|
if len(payload) < 4:
|
|
366
367
|
raise ValueError("payload must be at least 4 bytes")
|
|
367
368
|
path = bytes(path)
|
|
368
|
-
data = bytes([
|
|
369
|
+
data = bytes([CommandType.SEND_RAW_DATA.value, len(path)]) + path + bytes(payload)
|
|
370
|
+
return await self.send(data, [EventType.OK, EventType.ERROR])
|
|
371
|
+
|
|
372
|
+
async def send_channel_data(
|
|
373
|
+
self, chan: int, data_type: int, payload: bytes,
|
|
374
|
+
path: Optional[bytes] = None, path_hash_mode: Optional[int] = 0
|
|
375
|
+
) -> Event:
|
|
376
|
+
"""Send a binary datagram to a channel via CMD_SEND_CHANNEL_DATA (0x3E).
|
|
377
|
+
|
|
378
|
+
Command format:
|
|
379
|
+
0x3E | channel_idx(1) | path_len(1) | path(path_len bytes) | data_type(2, LE) | payload
|
|
380
|
+
|
|
381
|
+
Args:
|
|
382
|
+
chan: Channel index (0-255, must fit in one byte; the firmware rejects unknown channels).
|
|
383
|
+
data_type: Application identifier, 0x0001-0xFFFF (0xFFFF is the developer namespace).
|
|
384
|
+
payload: Binary payload, at most 163 bytes (MAX_CHANNEL_DATA_LENGTH).
|
|
385
|
+
path: Optional path bytes to send along (default None = flood, path_len 0xFF).
|
|
386
|
+
path_hash_mode: Optional path_hash_mode (size of the hash - 1)
|
|
387
|
+
|
|
388
|
+
Returns:
|
|
389
|
+
Event with OK or ERROR.
|
|
390
|
+
"""
|
|
391
|
+
if not isinstance(payload, (bytes, bytearray)):
|
|
392
|
+
raise TypeError("payload must be bytes-like")
|
|
393
|
+
if not 0 <= chan <= 0xFF:
|
|
394
|
+
raise ValueError("chan must fit in one byte (0-255)")
|
|
395
|
+
if not 0x0001 <= data_type <= 0xFFFF:
|
|
396
|
+
raise ValueError("data_type must be between 0x0001 and 0xFFFF")
|
|
397
|
+
if len(payload) > 163:
|
|
398
|
+
raise ValueError("payload must be at most 163 bytes")
|
|
399
|
+
if path is None:
|
|
400
|
+
path_bytes = b"\xff"
|
|
401
|
+
else:
|
|
402
|
+
path = bytes(path)
|
|
403
|
+
if len(path) >= 0xFF:
|
|
404
|
+
raise ValueError("path must be shorter than 255 bytes")
|
|
405
|
+
path_len = int(len(path) / (path_hash_mode + 1))
|
|
406
|
+
path_bytes = bytes([path_len | (path_hash_mode << 6)]) + path
|
|
407
|
+
logger.debug(f"Sending raw data {payload.hex()} through path {"Flood" if path is None else path.hex()} of len {"-1" if path is None else path_len} with mode {path_hash_mode} (raw path: {path_bytes.hex()})")
|
|
408
|
+
data = bytes([CommandType.SEND_CHANNEL_DATA.value, chan]) + path_bytes + data_type.to_bytes(2, "little") + bytes(payload)
|
|
369
409
|
return await self.send(data, [EventType.OK, EventType.ERROR])
|
|
370
410
|
|
|
371
411
|
async def set_flood_scope(self, scope, force_unscoped=False):
|
|
@@ -207,11 +207,16 @@ class MeshCore:
|
|
|
207
207
|
|
|
208
208
|
async def connect(self):
|
|
209
209
|
await self.dispatcher.start()
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
210
|
+
try:
|
|
211
|
+
result = await self.connection_manager.connect()
|
|
212
|
+
if result is None:
|
|
213
|
+
raise ConnectionError("Failed to connect to device")
|
|
214
|
+
res = await self.commands.send_appstart(timeout=2)
|
|
215
|
+
except BaseException:
|
|
216
|
+
# The create_* factories drop this instance when connect() raises, so
|
|
217
|
+
# nothing else can stop the dispatcher task or close an opened transport.
|
|
218
|
+
await self.disconnect()
|
|
219
|
+
raise
|
|
215
220
|
if res is None or res.type == EventType.ERROR:
|
|
216
221
|
return None
|
|
217
222
|
return res
|
|
@@ -86,6 +86,7 @@ class CommandType(Enum):
|
|
|
86
86
|
GET_ALLOWED_REPEAT_FREQ = 60
|
|
87
87
|
GET_STATS = 56 # R04: CMD_GET_STATS — used by get_stats_core/radio/packets
|
|
88
88
|
SET_PATH_HASH_MODE = 61
|
|
89
|
+
SEND_CHANNEL_DATA = 62
|
|
89
90
|
SET_DEFAULT_FLOOD_SCOPE = 63
|
|
90
91
|
GET_DEFAULT_FLOOD_SCOPE = 64
|
|
91
92
|
SEND_RAW_PACKET = 65
|
|
@@ -360,6 +360,7 @@ class MessageReader:
|
|
|
360
360
|
logger.debug(f"CHANNEL_DATA_RECV frame too short ({len(data)} bytes < 9), skipping parse")
|
|
361
361
|
return
|
|
362
362
|
res = {}
|
|
363
|
+
res["type"] = "DATA"
|
|
363
364
|
res["SNR"] = int.from_bytes(dbuf.read(1), byteorder="little", signed=True) / 4
|
|
364
365
|
dbuf.read(2) # reserved
|
|
365
366
|
res["channel_idx"] = dbuf.read(1)[0]
|
|
@@ -4,9 +4,11 @@ Verification tests for asyncio lifecycle fixes.
|
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
6
|
import gc
|
|
7
|
+
import socket
|
|
7
8
|
import unittest
|
|
8
9
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
9
10
|
|
|
11
|
+
from meshcore import MeshCore
|
|
10
12
|
from meshcore.events import Event, EventDispatcher, EventType
|
|
11
13
|
from meshcore.tcp_cx import TCPConnection
|
|
12
14
|
from meshcore.serial_cx import SerialConnection
|
|
@@ -231,5 +233,67 @@ class TestGetRunningLoop(unittest.TestCase):
|
|
|
231
233
|
asyncio.run(_run())
|
|
232
234
|
|
|
233
235
|
|
|
236
|
+
class TestConnectFailureCleanup(unittest.TestCase):
|
|
237
|
+
"""A connect() that raises or is cancelled must not leave the dispatcher running."""
|
|
238
|
+
|
|
239
|
+
def setUp(self):
|
|
240
|
+
# Without collection, a leaked task stays visible instead of being destroyed
|
|
241
|
+
gc.disable()
|
|
242
|
+
self.addCleanup(gc.enable)
|
|
243
|
+
|
|
244
|
+
@staticmethod
|
|
245
|
+
def _dispatcher_tasks():
|
|
246
|
+
return [
|
|
247
|
+
task
|
|
248
|
+
for task in asyncio.all_tasks()
|
|
249
|
+
if not task.done()
|
|
250
|
+
and task.get_coro().__qualname__ == "EventDispatcher._process_events"
|
|
251
|
+
]
|
|
252
|
+
|
|
253
|
+
def test_refused_create_tcp_leaves_no_dispatcher_task(self):
|
|
254
|
+
"""create_tcp() raising on a refused connection stops the dispatcher."""
|
|
255
|
+
async def _run():
|
|
256
|
+
with socket.socket() as sock:
|
|
257
|
+
sock.bind(("127.0.0.1", 0))
|
|
258
|
+
port = sock.getsockname()[1]
|
|
259
|
+
|
|
260
|
+
with self.assertRaises(OSError):
|
|
261
|
+
await MeshCore.create_tcp("127.0.0.1", port)
|
|
262
|
+
|
|
263
|
+
assert self._dispatcher_tasks() == []
|
|
264
|
+
|
|
265
|
+
asyncio.run(_run())
|
|
266
|
+
|
|
267
|
+
def test_cancelled_connect_closes_transport_and_dispatcher(self):
|
|
268
|
+
"""Cancelling connect() while it waits for appstart releases everything."""
|
|
269
|
+
async def _run():
|
|
270
|
+
peers = []
|
|
271
|
+
accepted = asyncio.Event()
|
|
272
|
+
|
|
273
|
+
async def accept(reader, _writer):
|
|
274
|
+
peers.append(reader)
|
|
275
|
+
accepted.set()
|
|
276
|
+
|
|
277
|
+
server = await asyncio.start_server(accept, "127.0.0.1", 0)
|
|
278
|
+
port = server.sockets[0].getsockname()[1]
|
|
279
|
+
try:
|
|
280
|
+
mc = MeshCore(TCPConnection("127.0.0.1", port))
|
|
281
|
+
attempt = asyncio.create_task(mc.connect())
|
|
282
|
+
await asyncio.wait_for(accepted.wait(), 1.0)
|
|
283
|
+
attempt.cancel()
|
|
284
|
+
with self.assertRaises(asyncio.CancelledError):
|
|
285
|
+
await attempt
|
|
286
|
+
|
|
287
|
+
assert self._dispatcher_tasks() == []
|
|
288
|
+
assert not mc.dispatcher.running
|
|
289
|
+
# EOF on the server side means the client transport was closed
|
|
290
|
+
await asyncio.wait_for(peers[0].read(), 1.0)
|
|
291
|
+
finally:
|
|
292
|
+
server.close()
|
|
293
|
+
await server.wait_closed()
|
|
294
|
+
|
|
295
|
+
asyncio.run(_run())
|
|
296
|
+
|
|
297
|
+
|
|
234
298
|
if __name__ == "__main__":
|
|
235
299
|
unittest.main()
|
|
@@ -37,3 +37,20 @@ class TestLppCurrentParsing:
|
|
|
37
37
|
lpp = json.loads(json.dumps(LppFrame([lppdata]), default=lpp_json_encoder))
|
|
38
38
|
|
|
39
39
|
assert lpp[0]['value'] == 0.5
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class TestLppDirectionParsing:
|
|
43
|
+
"""Tests to verify LPP direction values are encoded as scalars."""
|
|
44
|
+
|
|
45
|
+
def test_direction_value_is_scalar(self):
|
|
46
|
+
"""A direction value should not be serialized as a one-item list."""
|
|
47
|
+
from meshcore.lpp_json_encoder import lpp_json_encoder
|
|
48
|
+
|
|
49
|
+
# Channel 2, Type 132 (direction), 90 degrees = 0x00 0x5A.
|
|
50
|
+
raw_bytes = bytes([2, 132, 0x00, 0x5A])
|
|
51
|
+
lppdata = LppData.from_bytes(raw_bytes)
|
|
52
|
+
|
|
53
|
+
lpp = json.loads(json.dumps(LppFrame([lppdata]), default=lpp_json_encoder))
|
|
54
|
+
|
|
55
|
+
assert lpp[0]['type'] == 'direction'
|
|
56
|
+
assert lpp[0]['value'] == 90.0
|
|
@@ -12,6 +12,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
12
12
|
from meshcore.events import Event, EventType, EventDispatcher
|
|
13
13
|
from meshcore.reader import MessageReader
|
|
14
14
|
from meshcore.packets import PacketType, CommandType
|
|
15
|
+
from meshcore.commands.messaging import MessagingCommands
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
# ---------------------------------------------------------------------------
|
|
@@ -489,6 +490,121 @@ async def test_channel_data_recv_widened_data_type():
|
|
|
489
490
|
assert evt.payload["payload"] == ""
|
|
490
491
|
|
|
491
492
|
|
|
493
|
+
@pytest.mark.asyncio
|
|
494
|
+
async def test_get_msg_returns_channel_data_recv():
|
|
495
|
+
"""get_msg() resolves on CHANNEL_DATA_RECV instead of timing out.
|
|
496
|
+
|
|
497
|
+
The companion protocol lists PACKET_CHANNEL_DATA_RECV (0x1B) as an answer
|
|
498
|
+
to GET_MESSAGE (0x0A). When the next queued message is a channel datagram
|
|
499
|
+
the device answers with that frame, and get_msg() must return it rather
|
|
500
|
+
than an ERROR event with reason "timeout".
|
|
501
|
+
"""
|
|
502
|
+
dispatcher = EventDispatcher()
|
|
503
|
+
await dispatcher.start()
|
|
504
|
+
reader = MessageReader(dispatcher)
|
|
505
|
+
commands = MessagingCommands()
|
|
506
|
+
commands.dispatcher = dispatcher
|
|
507
|
+
|
|
508
|
+
frame = bytes([
|
|
509
|
+
PacketType.CHANNEL_DATA_RECV.value,
|
|
510
|
+
0x10, # SNR
|
|
511
|
+
0x00, 0x00, # reserved
|
|
512
|
+
0x01, # channel_idx
|
|
513
|
+
0xFF, # path_len direct
|
|
514
|
+
0x23, 0x01, # data_type = 0x0123 (little-endian)
|
|
515
|
+
0x04, # data_len
|
|
516
|
+
0xDE, 0xAD, 0xBE, 0xEF, # payload
|
|
517
|
+
])
|
|
518
|
+
|
|
519
|
+
async def device(data):
|
|
520
|
+
# The device answers GET_MESSAGE with the queued channel datagram.
|
|
521
|
+
assert data == b"\x0a"
|
|
522
|
+
await reader.handle_rx(bytearray(frame))
|
|
523
|
+
|
|
524
|
+
commands._sender_func = device
|
|
525
|
+
|
|
526
|
+
try:
|
|
527
|
+
result = await commands.get_msg(timeout=2.0)
|
|
528
|
+
finally:
|
|
529
|
+
await dispatcher.stop()
|
|
530
|
+
|
|
531
|
+
assert result.type == EventType.CHANNEL_DATA_RECV
|
|
532
|
+
assert result.payload["channel_idx"] == 1
|
|
533
|
+
assert result.payload["data_type"] == 0x0123
|
|
534
|
+
assert result.payload["payload"] == "deadbeef"
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
# ---------------------------------------------------------------------------
|
|
538
|
+
# Command wrapper: send_channel_data (CMD_SEND_CHANNEL_DATA, 0x3E)
|
|
539
|
+
# ---------------------------------------------------------------------------
|
|
540
|
+
|
|
541
|
+
def _make_channel_data_commands():
|
|
542
|
+
"""MessagingCommands whose send() records the written frame and expected events."""
|
|
543
|
+
from meshcore.commands.messaging import MessagingCommands
|
|
544
|
+
|
|
545
|
+
cmd = MessagingCommands.__new__(MessagingCommands)
|
|
546
|
+
sent = []
|
|
547
|
+
|
|
548
|
+
async def mock_send(data, expected_events, timeout=None):
|
|
549
|
+
sent.append((bytes(data), expected_events))
|
|
550
|
+
return Event(EventType.OK, {})
|
|
551
|
+
|
|
552
|
+
cmd.send = mock_send
|
|
553
|
+
return cmd, sent
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
@pytest.mark.asyncio
|
|
557
|
+
async def test_send_channel_data_flood_matches_protocol_example():
|
|
558
|
+
"""Flood frame equals the companion protocol example: 3E 01 FF FF FF A1 B2 C3."""
|
|
559
|
+
cmd, sent = _make_channel_data_commands()
|
|
560
|
+
|
|
561
|
+
evt = await cmd.send_channel_data(1, 0xFFFF, b"\xA1\xB2\xC3")
|
|
562
|
+
|
|
563
|
+
assert evt.type == EventType.OK
|
|
564
|
+
assert sent == [
|
|
565
|
+
(bytes.fromhex("3E01FFFFFFA1B2C3"), [EventType.OK, EventType.ERROR]),
|
|
566
|
+
]
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
@pytest.mark.asyncio
|
|
570
|
+
async def test_send_channel_data_with_path():
|
|
571
|
+
"""A path is sent after its length byte; data_type is little-endian."""
|
|
572
|
+
cmd, sent = _make_channel_data_commands()
|
|
573
|
+
|
|
574
|
+
await cmd.send_channel_data(2, 0x0102, b"\x07", path=b"\xAA\xBB")
|
|
575
|
+
|
|
576
|
+
assert sent[0][0] == bytes.fromhex("3E02" "02" "AABB" "0201" "07")
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
@pytest.mark.asyncio
|
|
580
|
+
async def test_send_channel_data_max_payload_accepted():
|
|
581
|
+
"""A 163-byte payload (MAX_CHANNEL_DATA_LENGTH) is sent."""
|
|
582
|
+
cmd, sent = _make_channel_data_commands()
|
|
583
|
+
|
|
584
|
+
await cmd.send_channel_data(0, 0x0100, b"\x00" * 163)
|
|
585
|
+
|
|
586
|
+
assert len(sent[0][0]) == 3 + 2 + 163
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
@pytest.mark.asyncio
|
|
590
|
+
async def test_send_channel_data_rejects_invalid_arguments():
|
|
591
|
+
"""Reserved data_type, oversized payload, bad chan and non-bytes payload never reach send()."""
|
|
592
|
+
cmd, sent = _make_channel_data_commands()
|
|
593
|
+
|
|
594
|
+
with pytest.raises(ValueError):
|
|
595
|
+
await cmd.send_channel_data(1, 0x0000, b"\x01")
|
|
596
|
+
with pytest.raises(ValueError):
|
|
597
|
+
await cmd.send_channel_data(1, 0x10000, b"\x01")
|
|
598
|
+
with pytest.raises(ValueError):
|
|
599
|
+
await cmd.send_channel_data(1, 0xFFFF, b"\x00" * 164)
|
|
600
|
+
with pytest.raises(ValueError):
|
|
601
|
+
await cmd.send_channel_data(256, 0xFFFF, b"\x01")
|
|
602
|
+
with pytest.raises(TypeError):
|
|
603
|
+
await cmd.send_channel_data(1, 0xFFFF, "text")
|
|
604
|
+
|
|
605
|
+
assert sent == []
|
|
606
|
+
|
|
607
|
+
|
|
492
608
|
# ---------------------------------------------------------------------------
|
|
493
609
|
# Wire-format parity bundle: AUTOADD_CONFIG (max_hops trailing byte)
|
|
494
610
|
# ---------------------------------------------------------------------------
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|