fix(channels): honor device path_hash_mode when building raw_packet

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 <noreply@anthropic.com>
This commit is contained in:
MarekWo
2026-06-09 14:40:21 +02:00
parent d23e865f35
commit 10df8464b7
+26 -2
View File
@@ -205,6 +205,7 @@ class DeviceManager:
self._channel_secrets = {} # {channel_idx: secret_hex} for pkt_payload self._channel_secrets = {} # {channel_idx: secret_hex} for pkt_payload
self._max_channels = 8 # updated from device_info at connect 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._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._pending_echo = None # {'timestamp': float, 'channel_idx': int, 'msg_id': int, 'pkt_payload': str|None}
self._echo_lock = threading.Lock() self._echo_lock = threading.Lock()
self._send_lock = threading.Lock() # serialize set-scope + send-channel-message pair (used in PR #4) 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 {} dev_info = dev_info_event.payload or {}
self._max_channels = dev_info.get('max_channels', 8) self._max_channels = dev_info.get('max_channels', 8)
self._fw_ver_code = dev_info.get('fw ver') 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}, " 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: except Exception as e:
logger.warning(f"Could not fetch device_info: {e}") logger.warning(f"Could not fetch device_info: {e}")
@@ -1710,6 +1718,7 @@ class DeviceManager:
raw_packet = _build_grp_txt_raw_packet( raw_packet = _build_grp_txt_raw_packet(
guess_pkt_payload, guess_pkt_payload,
scope_key_hex=scope['key_hex'] if scope else None, scope_key_hex=scope['key_hex'] if scope else None,
path_hash_size=self.path_hash_size,
) )
if raw_packet: if raw_packet:
self.db.update_message_raw_packet(msg_id, raw_packet) self.db.update_message_raw_packet(msg_id, raw_packet)
@@ -1758,6 +1767,17 @@ class DeviceManager:
def supports_raw_resend(self) -> bool: def supports_raw_resend(self) -> bool:
return (self._fw_ver_code or 0) >= self._MIN_FW_VER_RAW_RESEND 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: def resend_channel_message(self, msg_id: int) -> Dict:
"""Re-broadcast an own channel message verbatim so repeaters can dedupe. """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 # Lib's internal default_timeout is 15s; give the outer wrapper
# enough headroom so we don't surface a bare TimeoutError when # enough headroom so we don't surface a bare TimeoutError when
# the device just needs a moment to acknowledge. # 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}'} return {'success': True, 'message': f'Path hash mode set to: {value}'}
elif param == 'help': elif param == 'help':
return {'success': True, 'help': 'set'} return {'success': True, 'help': 'set'}