From 719e11e868d71e3504b1ef944c06c5241c7345a1 Mon Sep 17 00:00:00 2001 From: MarekWo Date: Mon, 30 Mar 2026 09:47:20 +0200 Subject: [PATCH] feat(path_hash_mode): add decode_path_len and fix RX_LOG_DATA parsing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stage 1 of path_hash_mode support. The critical bug in _on_rx_log_data treated the raw path_len byte as a direct byte count, which breaks with mode>0 (e.g. mode=1, 0 hops → path_len=0x40=64, reading 64 bytes of non-existent path data). Now properly decodes the encoded path_len byte into hop_count, hash_size, and path_byte_len. Changes: - Add decode_path_len() utility for MeshCore v1.14+ path_len encoding - Fix _on_rx_log_data binary parsing to use decoded path length - Pass hash_size through _process_echo → DB insert → SocketIO emission - Add hash_size column to echoes table (schema + migration) - Update insert_echo() to store hash_size (default 1 for backward compat) Co-Authored-By: Claude Opus 4.6 --- app/database.py | 13 ++++++++++--- app/device_manager.py | 32 +++++++++++++++++++++++++++----- app/schema.sql | 1 + 3 files changed, 38 insertions(+), 8 deletions(-) diff --git a/app/database.py b/app/database.py index 4576f75..9b47797 100644 --- a/app/database.py +++ b/app/database.py @@ -56,6 +56,12 @@ class Database: conn.execute(f"ALTER TABLE direct_messages ADD COLUMN {col} {typedef}") logger.info(f"Migration: added direct_messages.{col} column") + # Add hash_size column to echoes (path_hash_mode support) + echo_columns = {r[1] for r in conn.execute("PRAGMA table_info(echoes)").fetchall()} + if 'hash_size' not in echo_columns: + conn.execute("ALTER TABLE echoes ADD COLUMN hash_size INTEGER NOT NULL DEFAULT 1") + logger.info("Migration: added echoes.hash_size column") + @contextmanager def _connect(self): """Yield a connection with auto-commit/rollback.""" @@ -768,13 +774,14 @@ class Database: def insert_echo(self, pkt_payload: str, **kwargs) -> None: with self._connect() as conn: conn.execute( - """INSERT INTO echoes (pkt_payload, path, snr, direction, cm_id) - VALUES (?, ?, ?, ?, ?)""", + """INSERT INTO echoes (pkt_payload, path, snr, direction, cm_id, hash_size) + VALUES (?, ?, ?, ?, ?, ?)""", (pkt_payload, kwargs.get('path'), kwargs.get('snr'), kwargs.get('direction', 'incoming'), - kwargs.get('cm_id')) + kwargs.get('cm_id'), + kwargs.get('hash_size', 1)) ) def get_echoes_for_message(self, pkt_payload: str) -> List[Dict]: diff --git a/app/device_manager.py b/app/device_manager.py index 8fa958e..a250f2c 100644 --- a/app/device_manager.py +++ b/app/device_manager.py @@ -22,6 +22,24 @@ logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) +def decode_path_len(path_len_raw: int) -> tuple: + """Decode the path_len byte (MeshCore v1.14+ encoding). + + Bits 7-6: hash_size - 1 (00=1B, 01=2B, 10=3B, 11=reserved/direct) + Bits 5-0: hop_count (0-63) + + Special case: 0xFF = direct routing (not flood) — returns (0, 1, 0). + + Returns: + (hop_count, hash_size, path_byte_len) + """ + if path_len_raw == 0xFF: + return 0, 1, 0 + hash_size = (path_len_raw >> 6) + 1 + hop_count = path_len_raw & 0x3F + return hop_count, hash_size, hop_count * hash_size + + def _to_str(val) -> str: """Convert bytes or other types to string. Used for expected_ack, pkt_payload, etc.""" if val is None: @@ -927,8 +945,9 @@ class DeviceManager: if route_type == 0x00 or route_type == 0x03: pbuf.read(4) # discard transport code - path_len = pbuf.read(1)[0] - path = pbuf.read(path_len).hex() + path_len_raw = pbuf.read(1)[0] + hop_count, hash_size, path_byte_len = decode_path_len(path_len_raw) + path = pbuf.read(path_byte_len).hex() pkt_payload = pbuf.read().hex() # Only process GRP_TXT channel message echoes @@ -939,7 +958,7 @@ class DeviceManager: return snr = data.get('snr') - self._process_echo(pkt_payload, path, snr) + self._process_echo(pkt_payload, path, snr, hash_size=hash_size) except Exception as e: logger.error(f"Error handling RX_LOG_DATA: {e}") @@ -952,7 +971,8 @@ class DeviceManager: return None return hashlib.sha256(bytes.fromhex(secret_hex)).digest()[0:1].hex() - def _process_echo(self, pkt_payload: str, path: str, snr: float = None): + def _process_echo(self, pkt_payload: str, path: str, snr: float = None, + hash_size: int = 1): """Classify and store an echo: sent echo or incoming echo. For sent messages: correlate with pending echo to get pkt_payload. @@ -994,9 +1014,10 @@ class DeviceManager: path=path, snr=snr, direction=direction, + hash_size=hash_size, ) - logger.debug(f"Echo ({direction}): path={path} snr={snr} pkt={pkt_payload[:16]}...") + logger.debug(f"Echo ({direction}): path={path} snr={snr} hash_size={hash_size} pkt={pkt_payload[:16]}...") # Emit SocketIO event for real-time UI update if self.socketio: @@ -1005,6 +1026,7 @@ class DeviceManager: 'path': path, 'snr': snr, 'direction': direction, + 'hash_size': hash_size, }, namespace='/chat') def _is_manual_approval_enabled(self) -> bool: diff --git a/app/schema.sql b/app/schema.sql index bc80ae4..e4f5e50 100644 --- a/app/schema.sql +++ b/app/schema.sql @@ -101,6 +101,7 @@ CREATE TABLE IF NOT EXISTS echoes ( received_at TEXT NOT NULL DEFAULT (datetime('now')), direction TEXT DEFAULT 'incoming', -- 'sent' or 'incoming' cm_id INTEGER, -- FK to channel_messages (nullable) + hash_size INTEGER NOT NULL DEFAULT 1, -- bytes per hop hash: 1, 2, or 3 FOREIGN KEY (cm_id) REFERENCES channel_messages(id) ON DELETE SET NULL );