From 8948f2e504d8681d7d86744ac65016a84057df46 Mon Sep 17 00:00:00 2001 From: Jack Kingsman Date: Sat, 7 Mar 2026 19:37:47 -0800 Subject: [PATCH] Phase 6: Radio config + path hash mode --- app/radio.py | 20 +++++++++++ app/routers/radio.py | 20 +++++++++++ .../settings/SettingsRadioSection.tsx | 36 +++++++++++++++++++ frontend/src/test/appFavorites.test.tsx | 2 ++ frontend/src/test/appSearchJump.test.tsx | 2 ++ frontend/src/test/appStartupHash.test.tsx | 2 ++ frontend/src/test/pathUtils.test.ts | 2 ++ frontend/src/test/settingsModal.test.tsx | 2 ++ frontend/src/types.ts | 3 ++ 9 files changed, 89 insertions(+) diff --git a/app/radio.py b/app/radio.py index 95d5dd0..02837a1 100644 --- a/app/radio.py +++ b/app/radio.py @@ -128,6 +128,8 @@ class RadioManager: self._setup_lock: asyncio.Lock | None = None self._setup_in_progress: bool = False self._setup_complete: bool = False + self.path_hash_mode: int = 0 + self.path_hash_mode_supported: bool = False async def _acquire_operation_lock( self, @@ -272,6 +274,22 @@ class RadioManager: "set_flood_scope failed (firmware may not support it): %s", exc ) + # Query path hash mode support (best-effort; older firmware won't report it) + try: + device_query = await mc.commands.send_device_query() + if device_query and "path_hash_mode" in device_query.payload: + self.path_hash_mode = device_query.payload["path_hash_mode"] + self.path_hash_mode_supported = True + logger.info("Path hash mode: %d (supported)", self.path_hash_mode) + else: + self.path_hash_mode = 0 + self.path_hash_mode_supported = False + logger.debug("Firmware does not report path_hash_mode") + except Exception as exc: + self.path_hash_mode = 0 + self.path_hash_mode_supported = False + logger.debug("Failed to query path_hash_mode: %s", exc) + # Sync contacts/channels from radio to DB and clear radio logger.info("Syncing and offloading radio data...") result = await sync_and_offload_all(mc) @@ -412,6 +430,8 @@ class RadioManager: await self._meshcore.disconnect() self._meshcore = None self._setup_complete = False + self.path_hash_mode = 0 + self.path_hash_mode_supported = False logger.debug("Radio disconnected") async def reconnect(self, *, broadcast_on_success: bool = True) -> bool: diff --git a/app/routers/radio.py b/app/routers/radio.py index 9c53e77..93d8aec 100644 --- a/app/routers/radio.py +++ b/app/routers/radio.py @@ -28,6 +28,12 @@ class RadioConfigResponse(BaseModel): tx_power: int = Field(description="Transmit power in dBm") max_tx_power: int = Field(description="Maximum transmit power in dBm") radio: RadioSettings + path_hash_mode: int = Field( + default=0, description="Path hash mode (0=1-byte, 1=2-byte, 2=3-byte)" + ) + path_hash_mode_supported: bool = Field( + default=False, description="Whether firmware supports path hash mode setting" + ) class RadioConfigUpdate(BaseModel): @@ -36,6 +42,9 @@ class RadioConfigUpdate(BaseModel): lon: float | None = None tx_power: int | None = Field(default=None, description="Transmit power in dBm") radio: RadioSettings | None = None + path_hash_mode: int | None = Field( + default=None, description="Path hash mode (0=1-byte, 1=2-byte, 2=3-byte)" + ) class PrivateKeyUpdate(BaseModel): @@ -64,6 +73,8 @@ async def get_radio_config() -> RadioConfigResponse: sf=info.get("radio_sf", 0), cr=info.get("radio_cr", 0), ), + path_hash_mode=radio_manager.path_hash_mode, + path_hash_mode_supported=radio_manager.path_hash_mode_supported, ) @@ -103,6 +114,15 @@ async def update_radio_config(update: RadioConfigUpdate) -> RadioConfigResponse: cr=update.radio.cr, ) + if update.path_hash_mode is not None: + if not radio_manager.path_hash_mode_supported: + raise HTTPException( + status_code=400, detail="Firmware does not support path hash mode setting" + ) + logger.info("Setting path hash mode to %d", update.path_hash_mode) + await mc.commands.set_path_hash_mode(update.path_hash_mode) + radio_manager.path_hash_mode = update.path_hash_mode + # Sync time with system clock await sync_radio_time(mc) diff --git a/frontend/src/components/settings/SettingsRadioSection.tsx b/frontend/src/components/settings/SettingsRadioSection.tsx index 33b8e48..0aed907 100644 --- a/frontend/src/components/settings/SettingsRadioSection.tsx +++ b/frontend/src/components/settings/SettingsRadioSection.tsx @@ -47,6 +47,7 @@ export function SettingsRadioSection({ const [bw, setBw] = useState(''); const [sf, setSf] = useState(''); const [cr, setCr] = useState(''); + const [pathHashMode, setPathHashMode] = useState('0'); const [gettingLocation, setGettingLocation] = useState(false); const [busy, setBusy] = useState(false); const [rebooting, setRebooting] = useState(false); @@ -77,6 +78,7 @@ export function SettingsRadioSection({ setBw(String(config.radio.bw)); setSf(String(config.radio.sf)); setCr(String(config.radio.cr)); + setPathHashMode(String(config.path_hash_mode)); }, [config]); useEffect(() => { @@ -159,6 +161,8 @@ export function SettingsRadioSection({ return null; } + const parsedPathHashMode = parseInt(pathHashMode, 10); + return { name, lat: parsedLat, @@ -170,6 +174,11 @@ export function SettingsRadioSection({ sf: parsedSf, cr: parsedCr, }, + ...(config.path_hash_mode_supported && + !isNaN(parsedPathHashMode) && + parsedPathHashMode !== config.path_hash_mode + ? { path_hash_mode: parsedPathHashMode } + : {}), }; }; @@ -427,6 +436,33 @@ export function SettingsRadioSection({ + {config.path_hash_mode_supported && ( + <> + +
+ + +
+

Compatibility Warning

+

+ ALL nodes along a message's route — your radio, every repeater, and the + recipient — must be running firmware that supports the selected mode. Messages + sent with 2-byte or 3-byte hops will be dropped by any node on older firmware. +

+
+
+ + )} + {error && (
{error} diff --git a/frontend/src/test/appFavorites.test.tsx b/frontend/src/test/appFavorites.test.tsx index 2d9c7e4..7037da5 100644 --- a/frontend/src/test/appFavorites.test.tsx +++ b/frontend/src/test/appFavorites.test.tsx @@ -174,6 +174,8 @@ const baseConfig = { tx_power: 17, max_tx_power: 22, radio: { freq: 910.525, bw: 62.5, sf: 7, cr: 5 }, + path_hash_mode: 0, + path_hash_mode_supported: false, }; const baseSettings = { diff --git a/frontend/src/test/appSearchJump.test.tsx b/frontend/src/test/appSearchJump.test.tsx index dd4075f..68182b4 100644 --- a/frontend/src/test/appSearchJump.test.tsx +++ b/frontend/src/test/appSearchJump.test.tsx @@ -202,6 +202,8 @@ describe('App search jump target handling', () => { tx_power: 17, max_tx_power: 22, radio: { freq: 910.525, bw: 62.5, sf: 7, cr: 5 }, + path_hash_mode: 0, + path_hash_mode_supported: false, }); mocks.api.getSettings.mockResolvedValue({ max_radio_contacts: 200, diff --git a/frontend/src/test/appStartupHash.test.tsx b/frontend/src/test/appStartupHash.test.tsx index c738a68..bbbb07a 100644 --- a/frontend/src/test/appStartupHash.test.tsx +++ b/frontend/src/test/appStartupHash.test.tsx @@ -158,6 +158,8 @@ describe('App startup hash resolution', () => { tx_power: 17, max_tx_power: 22, radio: { freq: 910.525, bw: 62.5, sf: 7, cr: 5 }, + path_hash_mode: 0, + path_hash_mode_supported: false, }); mocks.api.getSettings.mockResolvedValue({ max_radio_contacts: 200, diff --git a/frontend/src/test/pathUtils.test.ts b/frontend/src/test/pathUtils.test.ts index 1678389..8ab0186 100644 --- a/frontend/src/test/pathUtils.test.ts +++ b/frontend/src/test/pathUtils.test.ts @@ -41,6 +41,8 @@ function createConfig(overrides: Partial = {}): RadioConfig { tx_power: 10, max_tx_power: 20, radio: { freq: 915, bw: 250, sf: 10, cr: 8 }, + path_hash_mode: 0, + path_hash_mode_supported: false, ...overrides, }; } diff --git a/frontend/src/test/settingsModal.test.tsx b/frontend/src/test/settingsModal.test.tsx index cffd125..92e8890 100644 --- a/frontend/src/test/settingsModal.test.tsx +++ b/frontend/src/test/settingsModal.test.tsx @@ -30,6 +30,8 @@ const baseConfig: RadioConfig = { sf: 7, cr: 5, }, + path_hash_mode: 0, + path_hash_mode_supported: false, }; const baseHealth: HealthStatus = { diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 291aee0..33583b9 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -13,6 +13,8 @@ export interface RadioConfig { tx_power: number; max_tx_power: number; radio: RadioSettings; + path_hash_mode: number; + path_hash_mode_supported: boolean; } export interface RadioConfigUpdate { @@ -21,6 +23,7 @@ export interface RadioConfigUpdate { lon?: number; tx_power?: number; radio?: RadioSettings; + path_hash_mode?: number; } export interface FanoutStatusEntry {