mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-03-28 17:43:05 +01:00
Be clearer about reality of location inclusion. Closes #53
This commit is contained in:
@@ -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 |
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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", ""),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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`)
|
||||
|
||||
@@ -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({
|
||||
<select
|
||||
id="advert-location-source"
|
||||
value={advertLocationSource}
|
||||
onChange={(e) =>
|
||||
setAdvertLocationSource(e.target.value as 'off' | 'node_gps' | 'saved_coords')
|
||||
}
|
||||
onChange={(e) => setAdvertLocationSource(e.target.value as 'off' | 'current')}
|
||||
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="off">Off</option>
|
||||
<option value="node_gps">Use Node GPS</option>
|
||||
<option value="saved_coords">Use Saved Coordinates</option>
|
||||
<option value="current">Include Node Location</option>
|
||||
</select>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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' })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user