feat(path_hash_mode): add decode_path_len and fix RX_LOG_DATA parsing

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 <noreply@anthropic.com>
This commit is contained in:
MarekWo
2026-03-30 09:47:20 +02:00
parent 1d9742a1ee
commit 719e11e868
3 changed files with 38 additions and 8 deletions
+10 -3
View File
@@ -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]:
+27 -5
View File
@@ -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:
+1
View File
@@ -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
);