From 10df8464b7c9893e64c1ee80212db6771f3d08f1 Mon Sep 17 00:00:00 2001 From: MarekWo Date: Tue, 9 Jun 2026 14:40:21 +0200 Subject: [PATCH] fix(channels): honor device path_hash_mode when building raw_packet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resends were building raw_packet with the default 1-byte path-hash size, ignoring the device's actual path_hash_mode. When path_hash_mode=1 (2-byte hashes) the original send produced 2-byte path entries in repeater echoes, but the resend's path_len byte said "1-byte" — so post-resend echoes appended 1-byte hashes, mixing into the badge as inconsistent tokens (e.g. "44D8, D103, E7" — the trailing E7 was a 1-byte fragment). Cache path_hash_mode from DEVICE_INFO at connect (fw_ver_code >= 10) and expose path_hash_size = max(1, mode+1). Pass it through to _build_grp_txt_raw_packet in send_channel_message and the clock-drift refresh path. Keep cache in sync with set_param('path_hash_mode', N). Co-Authored-By: Claude Opus 4.7 --- app/device_manager.py | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/app/device_manager.py b/app/device_manager.py index 4affc71..d581f2a 100644 --- a/app/device_manager.py +++ b/app/device_manager.py @@ -205,6 +205,7 @@ class DeviceManager: self._channel_secrets = {} # {channel_idx: secret_hex} for pkt_payload self._max_channels = 8 # updated from device_info at connect self._fw_ver_code = None # FIRMWARE_VER_CODE from DEVICE_INFO; gates feature support + self._path_hash_mode = 0 # 0=1B, 1=2B, 2=3B per hop hash; refreshed on set_param self._pending_echo = None # {'timestamp': float, 'channel_idx': int, 'msg_id': int, 'pkt_payload': str|None} self._echo_lock = threading.Lock() self._send_lock = threading.Lock() # serialize set-scope + send-channel-message pair (used in PR #4) @@ -402,8 +403,15 @@ class DeviceManager: dev_info = dev_info_event.payload or {} self._max_channels = dev_info.get('max_channels', 8) self._fw_ver_code = dev_info.get('fw ver') + # path_hash_mode in DEVICE_INFO requires fw_ver_code >= 10 + # (companion-v1.14+); older firmware omits it, leaving 0 + # (= 1-byte hashes) which is also the legacy default. + phm = dev_info.get('path_hash_mode') + if isinstance(phm, int) and phm >= 0: + self._path_hash_mode = phm logger.info(f"Device max_channels={self._max_channels}, " - f"fw_ver_code={self._fw_ver_code}") + f"fw_ver_code={self._fw_ver_code}, " + f"path_hash_mode={self._path_hash_mode}") except Exception as e: logger.warning(f"Could not fetch device_info: {e}") @@ -1710,6 +1718,7 @@ class DeviceManager: raw_packet = _build_grp_txt_raw_packet( guess_pkt_payload, scope_key_hex=scope['key_hex'] if scope else None, + path_hash_size=self.path_hash_size, ) if raw_packet: self.db.update_message_raw_packet(msg_id, raw_packet) @@ -1758,6 +1767,17 @@ class DeviceManager: def supports_raw_resend(self) -> bool: return (self._fw_ver_code or 0) >= self._MIN_FW_VER_RAW_RESEND + @property + def path_hash_size(self) -> int: + """Bytes per path-hop hash, derived from cached _path_hash_mode. + + Matches firmware's `_prefs.path_hash_mode + 1` used by sendFlood — + so raw_packet snapshots have the same path_len byte the firmware + would have produced, and post-resend repeater echoes use the same + hash size as the original (no mixed 1B/2B entries on the badge). + """ + return max(1, (self._path_hash_mode or 0) + 1) + def resend_channel_message(self, msg_id: int) -> Dict: """Re-broadcast an own channel message verbatim so repeaters can dedupe. @@ -3589,7 +3609,11 @@ class DeviceManager: # Lib's internal default_timeout is 15s; give the outer wrapper # enough headroom so we don't surface a bare TimeoutError when # the device just needs a moment to acknowledge. - self.execute(self.mc.commands.set_path_hash_mode(int(value)), timeout=20) + phm = int(value) + self.execute(self.mc.commands.set_path_hash_mode(phm), timeout=20) + # Keep cache in sync so subsequent raw_packet snapshots use + # the new hash size without needing a reconnect. + self._path_hash_mode = phm return {'success': True, 'message': f'Path hash mode set to: {value}'} elif param == 'help': return {'success': True, 'help': 'set'}