Phase 6: Radio config + path hash mode

This commit is contained in:
Jack Kingsman
2026-03-07 19:37:47 -08:00
parent 5c413bf949
commit 8948f2e504
9 changed files with 89 additions and 0 deletions
+20
View File
@@ -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:
+20
View File
@@ -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)
@@ -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({
</div>
</div>
{config.path_hash_mode_supported && (
<>
<Separator />
<div className="space-y-2">
<Label htmlFor="path-hash-mode">Path Hash Mode</Label>
<select
id="path-hash-mode"
value={pathHashMode}
onChange={(e) => setPathHashMode(e.target.value)}
className="w-full h-10 px-3 rounded-md border border-input bg-background text-sm ring-offset-background focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
>
<option value="0">1 byte (default)</option>
<option value="1">2 bytes</option>
<option value="2">3 bytes</option>
</select>
<div className="rounded-md border border-amber-500/50 bg-amber-500/10 p-3 text-xs text-amber-200">
<p className="font-semibold mb-1">Compatibility Warning</p>
<p>
ALL nodes along a message&apos;s route &mdash; your radio, every repeater, and the
recipient &mdash; 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.
</p>
</div>
</div>
</>
)}
{error && (
<div className="text-sm text-destructive" role="alert">
{error}
+2
View File
@@ -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 = {
+2
View File
@@ -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,
@@ -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,
+2
View File
@@ -41,6 +41,8 @@ function createConfig(overrides: Partial<RadioConfig> = {}): 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,
};
}
+2
View File
@@ -30,6 +30,8 @@ const baseConfig: RadioConfig = {
sf: 7,
cr: 5,
},
path_hash_mode: 0,
path_hash_mode_supported: false,
};
const baseHealth: HealthStatus = {
+3
View File
@@ -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 {