Saved settings: never trade a set position for an undefined one

A no-GPS device that restarted reports no usable fix at all - an empty
slot, not a competing value - so a drifted position restores silently,
the channel-replay rule, and only real conflicts reach the operator.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jean-Pierre MartineauandClaude Fable 5 committed 2026-08-08 19:12:14 -04:00
1 parent e9af03f227
commit de17cee757
1 file changed
+47
+47
View File
@@ -8,6 +8,11 @@ value, so restoring a stale one could clobber a change made elsewhere. This surf
MeshTerm saved for it, it offers to restore the saved values, adopt the device's current ones, or
stop remembering the device (see :mod:`meshterm.core.settings_store`).
One drift is the channel case in disguise and acts silently (JP, 2026-08-08): a manually set
position on a device with no GPS. After a restart such a device reports no usable fix at all —
an *empty slot*, not a competing value — so the saved position is written straight back instead
of asking the operator whether to override a position with an undefined one.
It runs once at startup, on the splash, only when a device is connected and something is actually
remembered for it — so a firmware radio you don't manage through MeshTerm is never interrupted.
"""
@@ -18,6 +23,7 @@ from rich.text import Text
from ..context import AppContext
from ..core.device_config import build_snapshot, get_spec
from ..core.geo import usable_fix
from ..core.settings_store import SettingDrift, adopt, restore, settings_drift
from .menus import menu_rows
from .tui import Separator
@@ -27,6 +33,25 @@ _RESTORE = "restore"
_ADOPT = "adopt"
_FORGET = "forget"
#: The advertised-position pair — the settings whose drift is judged as one fix, not two
#: scalars, against :func:`_position_undefined`.
_POSITION_KEYS = frozenset({"adv_lat", "adv_lon"})
def _position_undefined(snapshot: dict) -> bool:
"""Whether the device currently advertises no usable fix at all.
The same :func:`~meshterm.core.geo.usable_fix` judgment the map surfaces make: the
0/0 null island a GPS-less (or freshly restarted) device reports is no position, and
out-of-range junk is no position either.
"""
try:
lat = float(snapshot.get("adv_lat") or 0.0)
lon = float(snapshot.get("adv_lon") or 0.0)
except (TypeError, ValueError):
return True
return not usable_fix(lat, lon)
async def offer_remembered_settings(ctx: AppContext) -> None:
"""Offer to reconcile the connected device's settings with MeshTerm's saved copy.
@@ -56,6 +81,28 @@ async def offer_remembered_settings(ctx: AppContext) -> None:
if not drifted:
return
# A drifted position on a device that reports none is the empty-slot case: restoring
# it overwrites nothing, and the alternative on offer — "keep the device's settings"
# — would mean trading a deliberately set position for an undefined one. So those
# keys restore silently, and only real value-against-value conflicts reach the
# operator.
if _position_undefined(snapshot):
silent = [d for d in drifted if d.key in _POSITION_KEYS]
if silent:
try:
restored = await restore(store, device, snapshot, [d.key for d in silent])
ctx.devstate.invalidate_config()
ctx.log.info(
"settings: restored the saved position (%d value(s)) — the device "
"reported no fix to weigh it against",
restored,
)
except Exception as exc: # noqa: BLE001 - best-effort, like the rest of the offer
ctx.log.debug("settings: silent position restore failed: %s", exc)
drifted = [d for d in drifted if d.key not in _POSITION_KEYS]
if not drifted:
return
choice = await _prompt(ctx, drifted)
keys = [d.key for d in drifted]
try: