diff --git a/AGENTS.md b/AGENTS.md index cf01398..dfb3d08 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -307,8 +307,8 @@ All endpoints are prefixed with `/api` (e.g., `/api/health`). |--------|----------|-------------| | GET | `/api/health` | Connection status, fanout statuses, bots_disabled flag | | GET | `/api/debug` | Support snapshot: recent logs, live radio probe, contact/channel drift audit, and running version/git info | -| GET | `/api/radio/config` | Radio configuration, including `path_hash_mode`, `path_hash_mode_supported`, and whether adverts include current node location | -| PATCH | `/api/radio/config` | Update name, location, advert-location on/off, radio params, and `path_hash_mode` when supported | +| GET | `/api/radio/config` | Radio configuration, including `path_hash_mode`, `path_hash_mode_supported`, advert-location on/off, and `multi_acks_enabled` | +| PATCH | `/api/radio/config` | Update name, location, advert-location on/off, `multi_acks_enabled`, radio params, and `path_hash_mode` when supported | | PUT | `/api/radio/private-key` | Import private key to radio | | POST | `/api/radio/advertise` | Send advertisement (`mode`: `flood` or `zero_hop`, default `flood`) | | POST | `/api/radio/discover` | Run a short mesh discovery sweep for nearby repeaters/sensors | diff --git a/app/AGENTS.md b/app/AGENTS.md index 186b1da..be2d2d0 100644 --- a/app/AGENTS.md +++ b/app/AGENTS.md @@ -169,8 +169,8 @@ app/ - `GET /debug` — support snapshot with recent logs, live radio probe, slot/contact audits, and version/git info ### Radio -- `GET /radio/config` — includes `path_hash_mode`, `path_hash_mode_supported`, and advert-location on/off -- `PATCH /radio/config` — may update `path_hash_mode` (`0..2`) when firmware supports it +- `GET /radio/config` — includes `path_hash_mode`, `path_hash_mode_supported`, advert-location on/off, and `multi_acks_enabled` +- `PATCH /radio/config` — may update `path_hash_mode` (`0..2`) when firmware supports it, and `multi_acks_enabled` - `PUT /radio/private-key` - `POST /radio/advertise` — manual advert send; request body may set `mode` to `flood` or `zero_hop` (defaults to `flood`) - `POST /radio/discover` — short mesh discovery sweep for nearby repeaters/sensors diff --git a/app/routers/debug.py b/app/routers/debug.py index a290a94..c98f2f9 100644 --- a/app/routers/debug.py +++ b/app/routers/debug.py @@ -78,6 +78,7 @@ class DebugChannelAudit(BaseModel): class DebugRadioProbe(BaseModel): performed: bool errors: list[str] = Field(default_factory=list) + multi_acks_enabled: bool | None = None self_info: dict[str, Any] | None = None device_info: dict[str, Any] | None = None stats_core: dict[str, Any] | None = None @@ -234,6 +235,9 @@ async def _probe_radio() -> DebugRadioProbe: return DebugRadioProbe( performed=True, errors=errors, + multi_acks_enabled=bool(mc.self_info.get("multi_acks", 0)) + if mc.self_info is not None + else None, self_info=dict(mc.self_info or {}), device_info=device_info, stats_core=stats_core, diff --git a/app/routers/radio.py b/app/routers/radio.py index 7c7b594..795c106 100644 --- a/app/routers/radio.py +++ b/app/routers/radio.py @@ -81,6 +81,10 @@ class RadioConfigResponse(BaseModel): default="current", description="Whether adverts include the node's current location state", ) + multi_acks_enabled: bool = Field( + default=False, + description="Whether the radio sends an extra direct ACK transmission", + ) class RadioConfigUpdate(BaseModel): @@ -99,6 +103,10 @@ class RadioConfigUpdate(BaseModel): default=None, description="Whether adverts include the node's current location state", ) + multi_acks_enabled: bool | None = Field( + default=None, + description="Whether the radio sends an extra direct ACK transmission", + ) class PrivateKeyUpdate(BaseModel): @@ -222,6 +230,7 @@ async def get_radio_config() -> RadioConfigResponse: path_hash_mode=radio_manager.path_hash_mode, path_hash_mode_supported=radio_manager.path_hash_mode_supported, advert_location_source=advert_location_source, + multi_acks_enabled=bool(info.get("multi_acks", 0)), ) diff --git a/app/services/radio_commands.py b/app/services/radio_commands.py index c1e6e18..9718a37 100644 --- a/app/services/radio_commands.py +++ b/app/services/radio_commands.py @@ -44,6 +44,13 @@ async def apply_radio_config_update( f"Failed to set advert location policy: {result.payload}" ) + if update.multi_acks_enabled is not None: + multi_acks = 1 if update.multi_acks_enabled else 0 + logger.info("Setting multi ACKs to %d", multi_acks) + result = await mc.commands.set_multi_acks(multi_acks) + if result is not None and result.type == EventType.ERROR: + raise RadioCommandRejectedError(f"Failed to set multi ACKs: {result.payload}") + if update.name is not None: logger.info("Setting radio name to %s", update.name) await mc.commands.set_name(update.name) diff --git a/frontend/AGENTS.md b/frontend/AGENTS.md index 4a92efe..d92a280 100644 --- a/frontend/AGENTS.md +++ b/frontend/AGENTS.md @@ -250,6 +250,7 @@ High-level state is delegated to hooks: ### Radio settings behavior - `SettingsRadioSection.tsx` surfaces `path_hash_mode` only when `config.path_hash_mode_supported` is true. +- `SettingsRadioSection.tsx` also exposes `multi_acks_enabled` as a checkbox for the radio's extra direct-ACK transmission behavior. - Advert-location control is intentionally only `off` vs `include node location`. Companion-radio firmware does not reliably distinguish saved coordinates from live GPS in this path. - The advert action is mode-aware: the radio settings section exposes both flood and zero-hop manual advert buttons, both routed through the same `onAdvertise(mode)` seam. - Mesh discovery in the radio section is limited to node classes that currently answer discovery control-data requests in firmware: repeaters and sensors. diff --git a/frontend/src/components/settings/SettingsRadioSection.tsx b/frontend/src/components/settings/SettingsRadioSection.tsx index d562dad..4b3f68e 100644 --- a/frontend/src/components/settings/SettingsRadioSection.tsx +++ b/frontend/src/components/settings/SettingsRadioSection.tsx @@ -5,6 +5,7 @@ import { Label } from '../ui/label'; import { Button } from '../ui/button'; import { Separator } from '../ui/separator'; import { toast } from '../ui/sonner'; +import { Checkbox } from '../ui/checkbox'; import { RADIO_PRESETS } from '../../utils/radioPresets'; import { stripRegionScopePrefix } from '../../utils/regionScope'; import type { @@ -64,6 +65,7 @@ export function SettingsRadioSection({ const [cr, setCr] = useState(''); const [pathHashMode, setPathHashMode] = useState('0'); const [advertLocationSource, setAdvertLocationSource] = useState<'off' | 'current'>('current'); + const [multiAcksEnabled, setMultiAcksEnabled] = useState(false); const [gettingLocation, setGettingLocation] = useState(false); const [busy, setBusy] = useState(false); const [rebooting, setRebooting] = useState(false); @@ -98,6 +100,7 @@ export function SettingsRadioSection({ setCr(String(config.radio.cr)); setPathHashMode(String(config.path_hash_mode)); setAdvertLocationSource(config.advert_location_source ?? 'current'); + setMultiAcksEnabled(config.multi_acks_enabled ?? false); }, [config]); useEffect(() => { @@ -190,6 +193,9 @@ export function SettingsRadioSection({ ...(advertLocationSource !== (config.advert_location_source ?? 'current') ? { advert_location_source: advertLocationSource } : {}), + ...(multiAcksEnabled !== (config.multi_acks_enabled ?? false) + ? { multi_acks_enabled: multiAcksEnabled } + : {}), radio: { freq: parsedFreq, bw: parsedBw, @@ -579,6 +585,24 @@ export function SettingsRadioSection({ library.
++ When enabled, the radio sends one extra direct ACK transmission before the normal + ACK for received direct messages. This is a firmware-level receive behavior, not a + RemoteTerm retry setting. +
+