From dd6d6350d9e753fd4ab2d69bd654eb0e815c8a42 Mon Sep 17 00:00:00 2001 From: Florent Date: Thu, 26 Feb 2026 22:51:52 -0400 Subject: [PATCH] multibyte trace support --- src/meshcore/commands/messaging.py | 72 ++++++++++++++++++++---------- src/meshcore/reader.py | 32 +++++++------ 2 files changed, 63 insertions(+), 41 deletions(-) diff --git a/src/meshcore/commands/messaging.py b/src/meshcore/commands/messaging.py index e6f4dba..29bded0 100644 --- a/src/meshcore/commands/messaging.py +++ b/src/meshcore/commands/messaging.py @@ -181,7 +181,7 @@ class MessagingCommands(CommandHandlerBase): self, auth_code: int = 0, tag: Optional[int] = None, - flags: int = 0, + flags = None, path: Optional[Union[str, bytes, bytearray]] = None, ) -> Event: """ @@ -190,7 +190,8 @@ class MessagingCommands(CommandHandlerBase): Args: auth_code: 32-bit authentication code (default: 0) tag: 32-bit integer to identify this trace (default: random) - flags: 8-bit flags field (default: 0) + flags: 8-bit flags field (default: None) + lower two bytes set the path hash size (1 << s) => 1, 2, 4 bytes path: Optional string with comma-separated hex values representing repeater pubkeys (e.g. "23,5f,3a") or a bytes/bytearray object with the raw path data @@ -203,39 +204,62 @@ class MessagingCommands(CommandHandlerBase): if auth_code is None: auth_code = random.randint(1, 0xFFFFFFFF) - logger.debug( - f"Sending trace: tag={tag}, auth={auth_code}, flags={flags}, path={path}" - ) + path_hash_len = 1 # default + if flags is None: + if isinstance(path, str): # get flags from path string + path_hash_len = int(len(path.split(",")[0]) / 2) + if path_hash_len == 1 : + flags = 0 + elif path_hash_len == 2 : + flags = 1 + elif path_hash_len == 4 : + flags = 2 + elif path_hash_len == 8 : + flags = 3 + else : + logger.error(f"Invalid path format: {e}") + return Event(EventType.ERROR, {"reason": "invalid_path_format"}) + else: + flags = 0 + else: + path_hash_len = 1 << (flags & 3) + + # Process path if provided + path_bytes = bytearray() + if path: + if isinstance(path, str): + # Convert comma-separated hex values to bytes + try: + for hex_val in path.split(","): + hex_val = hex_val.strip() + if hex_val == "": + break + elif len(hex_val) != path_hash_len * 2 : + raise(ValueError()) + path_bytes.extend(bytes.fromhex(hex_val)) + except ValueError as e: + logger.error(f"Invalid path format: {e}") + return Event(EventType.ERROR, {"reason": "invalid_path_format"}) + elif isinstance(path, (bytes, bytearray)): + path_bytes = path + else: + logger.error(f"Unsupported path type: {type(path)}") + return Event(EventType.ERROR, {"reason": "unsupported_path_type"}) # Prepare the command packet: CMD(1) + tag(4) + auth_code(4) + flags(1) + [path] cmd_data = bytearray([36]) # CMD_SEND_TRACE_PATH cmd_data.extend(tag.to_bytes(4, "little")) cmd_data.extend(auth_code.to_bytes(4, "little")) cmd_data.append(flags) + cmd_data.extend(path_bytes) - # Process path if provided - if path: - if isinstance(path, str): - # Convert comma-separated hex values to bytes - try: - path_bytes = bytearray() - for hex_val in path.split(","): - hex_val = hex_val.strip() - path_bytes.append(int(hex_val, 16)) - cmd_data.extend(path_bytes) - except ValueError as e: - logger.error(f"Invalid path format: {e}") - return Event(EventType.ERROR, {"reason": "invalid_path_format"}) - elif isinstance(path, (bytes, bytearray)): - cmd_data.extend(path) - else: - logger.error(f"Unsupported path type: {type(path)}") - return Event(EventType.ERROR, {"reason": "unsupported_path_type"}) + logger.debug( + f"Sending trace: tag={tag}, auth={auth_code}, flags={flags}, path={path_bytes.hex()}" + ) return await self.send(cmd_data, [EventType.MSG_SENT, EventType.ERROR]) async def set_flood_scope(self, scope): - if scope is None: logger.debug(f"Resetting scope") scope_key = b"\0"*16 diff --git a/src/meshcore/reader.py b/src/meshcore/reader.py index bd86949..d403d16 100644 --- a/src/meshcore/reader.py +++ b/src/meshcore/reader.py @@ -532,10 +532,14 @@ class MessageReader: # According to the source, format is: # 0x89, reserved(0), path_len, flags, tag(4), auth(4), path_hashes[], path_snrs[], final_snr - path_len = data[2] - flags = data[3] - tag = int.from_bytes(data[4:8], byteorder="little") - auth_code = int.from_bytes(data[8:12], byteorder="little") + reserved = dbuf.read(1)[0] + path_len = dbuf.read(1)[0] + flags = dbuf.read(1)[0] + tag = int.from_bytes(dbuf.read(4), byteorder="little") + auth_code = int.from_bytes(dbuf.read(4), byteorder="little") + + path_hash_len = 1 << (flags&3) + path_len = int(path_len / path_hash_len) # Initialize result res["tag"] = tag @@ -546,26 +550,20 @@ class MessageReader: # Process path as array of objects with hash and SNR path_nodes = [] - if path_len > 0 and len(data) >= 12 + path_len * 2 + 1: + if path_len > 0 and len(data) >= 12 + path_len + (path_len * path_hash_len) + 1: # Extract path with hash and SNR pairs for i in range(path_len): node = { - "hash": f"{data[12+i]:02x}", - # SNR is stored as a signed byte representing SNR * 4 - "snr": ( - data[12 + path_len + i] - if data[12 + path_len + i] < 128 - else data[12 + path_len + i] - 256 - ) - / 4.0, + "hash": dbuf.read(path_hash_len).hex(), } path_nodes.append(node) + for n in path_nodes: + node_snr = int.from_bytes(dbuf.read(1), byteorder="little", signed=True) + n["snr"] = node_snr / 4.0 + # Add the final node (our device) with its SNR - final_snr_byte = data[12 + path_len * 2] - final_snr = ( - final_snr_byte if final_snr_byte < 128 else final_snr_byte - 256 - ) / 4.0 + final_snr = int.from_bytes(dbuf.read(1), byteorder="little", signed=True) / 4.0 path_nodes.append({"snr": final_snr}) res["path"] = path_nodes