diff --git a/AGENTS.md b/AGENTS.md index dfce822..d4a0e22 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -290,8 +290,8 @@ All endpoints are prefixed with `/api` (e.g., `/api/health`). | Method | Endpoint | Description | |--------|----------|-------------| | GET | `/api/health` | Connection status, fanout statuses, bots_disabled flag | -| GET | `/api/radio/config` | Radio configuration, including `path_hash_mode` and `path_hash_mode_supported` | -| PATCH | `/api/radio/config` | Update name, location, radio params, and `path_hash_mode` when supported | +| 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 | | PUT | `/api/radio/private-key` | Import private key to radio | | POST | `/api/radio/advertise` | Send advertisement | | POST | `/api/radio/reboot` | Reboot radio or reconnect if disconnected | diff --git a/app/AGENTS.md b/app/AGENTS.md index 06726ad..022bbd7 100644 --- a/app/AGENTS.md +++ b/app/AGENTS.md @@ -146,7 +146,7 @@ app/ - `GET /health` ### Radio -- `GET /radio/config` — includes `path_hash_mode` and `path_hash_mode_supported` +- `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 - `PUT /radio/private-key` - `POST /radio/advertise` diff --git a/app/routers/radio.py b/app/routers/radio.py index bb68086..6114896 100644 --- a/app/routers/radio.py +++ b/app/routers/radio.py @@ -20,6 +20,8 @@ from app.websocket import broadcast_health logger = logging.getLogger(__name__) router = APIRouter(prefix="/radio", tags=["radio"]) +AdvertLocationSource = Literal["off", "current"] + async def _prepare_connected(*, broadcast_on_success: bool) -> bool: return await radio_manager.prepare_connected(broadcast_on_success=broadcast_on_success) @@ -52,9 +54,9 @@ class RadioConfigResponse(BaseModel): path_hash_mode_supported: bool = Field( default=False, description="Whether firmware supports path hash mode setting" ) - advert_location_source: Literal["off", "node_gps", "saved_coords"] = Field( - default="saved_coords", - description="Source used for location included in adverts", + advert_location_source: AdvertLocationSource = Field( + default="current", + description="Whether adverts include the node's current location state", ) @@ -70,9 +72,9 @@ class RadioConfigUpdate(BaseModel): le=2, description="Path hash mode (0=1-byte, 1=2-byte, 2=3-byte)", ) - advert_location_source: Literal["off", "node_gps", "saved_coords"] | None = Field( + advert_location_source: AdvertLocationSource | None = Field( default=None, - description="Source used for location included in adverts", + description="Whether adverts include the node's current location state", ) @@ -89,14 +91,8 @@ async def get_radio_config() -> RadioConfigResponse: if not info: raise HTTPException(status_code=503, detail="Radio info not available") - adv_loc_policy = info.get("adv_loc_policy", 2) - advert_location_source: Literal["off", "node_gps", "saved_coords"] - if adv_loc_policy == 0: - advert_location_source = "off" - elif adv_loc_policy == 1: - advert_location_source = "node_gps" - else: - advert_location_source = "saved_coords" + adv_loc_policy = info.get("adv_loc_policy", 1) + advert_location_source: AdvertLocationSource = "off" if adv_loc_policy == 0 else "current" return RadioConfigResponse( public_key=info.get("public_key", ""), diff --git a/app/services/radio_commands.py b/app/services/radio_commands.py index 53028c6..c1e6e18 100644 --- a/app/services/radio_commands.py +++ b/app/services/radio_commands.py @@ -33,11 +33,7 @@ async def apply_radio_config_update( ) -> None: """Apply a validated radio-config update to the connected radio.""" if update.advert_location_source is not None: - advert_loc_policy = { - "off": 0, - "node_gps": 1, - "saved_coords": 2, - }[update.advert_location_source] + advert_loc_policy = 0 if update.advert_location_source == "off" else 1 logger.info( "Setting advert location policy to %s", update.advert_location_source, diff --git a/frontend/AGENTS.md b/frontend/AGENTS.md index 72c051b..569359d 100644 --- a/frontend/AGENTS.md +++ b/frontend/AGENTS.md @@ -248,6 +248,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. +- 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. - Frontend `path_len` fields are hop counts, not raw byte lengths; multibyte path rendering must use the accompanying metadata before splitting hop identifiers. ## WebSocket (`useWebSocket.ts`) diff --git a/frontend/src/components/settings/SettingsRadioSection.tsx b/frontend/src/components/settings/SettingsRadioSection.tsx index 8c091d7..4c3dc7c 100644 --- a/frontend/src/components/settings/SettingsRadioSection.tsx +++ b/frontend/src/components/settings/SettingsRadioSection.tsx @@ -54,9 +54,7 @@ export function SettingsRadioSection({ const [sf, setSf] = useState(''); const [cr, setCr] = useState(''); const [pathHashMode, setPathHashMode] = useState('0'); - const [advertLocationSource, setAdvertLocationSource] = useState< - 'off' | 'node_gps' | 'saved_coords' - >('saved_coords'); + const [advertLocationSource, setAdvertLocationSource] = useState<'off' | 'current'>('current'); const [gettingLocation, setGettingLocation] = useState(false); const [busy, setBusy] = useState(false); const [rebooting, setRebooting] = useState(false); @@ -89,7 +87,7 @@ export function SettingsRadioSection({ setSf(String(config.radio.sf)); setCr(String(config.radio.cr)); setPathHashMode(String(config.path_hash_mode)); - setAdvertLocationSource(config.advert_location_source ?? 'saved_coords'); + setAdvertLocationSource(config.advert_location_source ?? 'current'); }, [config]); useEffect(() => { @@ -179,7 +177,7 @@ export function SettingsRadioSection({ lat: parsedLat, lon: parsedLon, tx_power: parsedTxPower, - ...(advertLocationSource !== (config.advert_location_source ?? 'saved_coords') + ...(advertLocationSource !== (config.advert_location_source ?? 'current') ? { advert_location_source: advertLocationSource } : {}), radio: { @@ -518,21 +516,18 @@ export function SettingsRadioSection({

- This only controls which location source the radio puts into adverts. If you choose Use - Node GPS, GPS still has to be enabled manually on the node itself for live coordinates - to take effect; RemoteTerm cannot turn it on through the interface library. RemoteTerm - still uses the saved coordinates above for local distance math and similar UI - calculations. + Companion-radio firmware does not distinguish between saved coordinates and live GPS + here. When enabled, adverts include the node's current location state. That may be + the last coordinates you set from RemoteTerm or live GPS coordinates if the node itself + is already updating them. RemoteTerm cannot enable GPS on the node through the interface + library.

diff --git a/frontend/src/test/settingsModal.test.tsx b/frontend/src/test/settingsModal.test.tsx index 078cd70..9d2144b 100644 --- a/frontend/src/test/settingsModal.test.tsx +++ b/frontend/src/test/settingsModal.test.tsx @@ -32,7 +32,7 @@ const baseConfig: RadioConfig = { }, path_hash_mode: 0, path_hash_mode_supported: false, - advert_location_source: 'saved_coords', + advert_location_source: 'current', }; const baseHealth: HealthStatus = { @@ -209,13 +209,13 @@ describe('SettingsModal', () => { openRadioSection(); fireEvent.change(screen.getByLabelText('Advert Location Source'), { - target: { value: 'node_gps' }, + target: { value: 'off' }, }); fireEvent.click(screen.getByRole('button', { name: 'Save' })); await waitFor(() => { expect(onSave).toHaveBeenCalledWith( - expect.objectContaining({ advert_location_source: 'node_gps' }) + expect.objectContaining({ advert_location_source: 'off' }) ); }); }); diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 5bcb1a8..82f13dd 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -15,7 +15,7 @@ export interface RadioConfig { radio: RadioSettings; path_hash_mode: number; path_hash_mode_supported: boolean; - advert_location_source?: 'off' | 'node_gps' | 'saved_coords'; + advert_location_source?: 'off' | 'current'; } export interface RadioConfigUpdate { @@ -25,7 +25,7 @@ export interface RadioConfigUpdate { tx_power?: number; radio?: RadioSettings; path_hash_mode?: number; - advert_location_source?: 'off' | 'node_gps' | 'saved_coords'; + advert_location_source?: 'off' | 'current'; } export interface FanoutStatusEntry { diff --git a/tests/test_radio_commands_service.py b/tests/test_radio_commands_service.py index 4e10db7..84ba427 100644 --- a/tests/test_radio_commands_service.py +++ b/tests/test_radio_commands_service.py @@ -75,7 +75,7 @@ class TestApplyRadioConfigUpdate: await apply_radio_config_update( mc, - RadioConfigUpdate(advert_location_source="node_gps"), + RadioConfigUpdate(advert_location_source="current"), path_hash_mode_supported=True, set_path_hash_mode=MagicMock(), sync_radio_time_fn=AsyncMock(), diff --git a/tests/test_radio_router.py b/tests/test_radio_router.py index a09a7b6..0d4bcdb 100644 --- a/tests/test_radio_router.py +++ b/tests/test_radio_router.py @@ -96,17 +96,17 @@ class TestGetRadioConfig: assert response.lon == 20.0 assert response.radio.freq == 910.525 assert response.radio.cr == 5 - assert response.advert_location_source == "saved_coords" + assert response.advert_location_source == "current" @pytest.mark.asyncio - async def test_maps_node_gps_advert_location_source(self): + async def test_maps_any_nonzero_advert_location_policy_to_current(self): mc = _mock_meshcore_with_info() mc.self_info["adv_loc_policy"] = 1 with patch("app.routers.radio.require_connected", return_value=mc): response = await get_radio_config() - assert response.advert_location_source == "node_gps" + assert response.advert_location_source == "current" @pytest.mark.asyncio async def test_returns_503_when_self_info_missing(self): @@ -164,7 +164,7 @@ class TestUpdateRadioConfig: radio=RadioSettings(freq=910.525, bw=62.5, sf=7, cr=5), path_hash_mode=0, path_hash_mode_supported=False, - advert_location_source="node_gps", + advert_location_source="current", ) with ( @@ -175,7 +175,7 @@ class TestUpdateRadioConfig: "app.routers.radio.get_radio_config", new_callable=AsyncMock, return_value=expected ), ): - result = await update_radio_config(RadioConfigUpdate(advert_location_source="node_gps")) + result = await update_radio_config(RadioConfigUpdate(advert_location_source="current")) mc.commands.set_advert_loc_policy.assert_awaited_once_with(1) assert result == expected