Improve the device picker and inventory in three ways: - Remember a device's mesh node name. On a successful connect, learn the radio's own name (adv_name/name, best-effort) and persist it with the remembered device; the picker leads its row with it and the devices table shows it. A failed probe never blocks the connection and preserves any name already on file. - Qualify devices in tiers instead of one over-generous "likely LoRa" flag. A native-USB dev board is a strong "LoRa device"; a bare USB-UART bridge (CP210x/CH340/FTDI) is only a "serial adapter" / "maybe", since the same chip shows up on plenty of non-LoRa hardware. Sorting and the table follow the board > bridge > unknown ranking; JSON gains a `confidence` field. - Stop repeating the COM port. Windows descriptions often already end with "(COM11)", so the label no longer renders "… (COM11) (COM11)". Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
111 lines
4.1 KiB
Python
111 lines
4.1 KiB
Python
"""Persistence for the last successfully connected companion device.
|
|
|
|
The "last known good" device is remembered in a small JSON file in the config directory
|
|
(``<config_dir>/devices.json``), deliberately *not* in the SQLite database: the database
|
|
can be swapped per-invocation with ``--db``, whereas the remembered device is global
|
|
machine state that should survive that. The record is matched back to currently attached
|
|
hardware by :attr:`~meshtools.core.discovery.DiscoveredDevice.stable_id`.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
from .discovery import DiscoveredDevice
|
|
from .models import utcnow
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class RememberedDevice:
|
|
"""A previously connected device recorded for next-time defaulting.
|
|
|
|
Attributes:
|
|
stable_id: The device's :attr:`DiscoveredDevice.stable_id` at connect time.
|
|
port: The serial port it was last seen on (informational; may have changed).
|
|
label: A friendly label for display in prompts and tables.
|
|
node_name: The device's own mesh node name, learned at connect time (may be empty).
|
|
last_connected: ISO-8601 timestamp of the last successful connection.
|
|
"""
|
|
|
|
stable_id: str
|
|
port: str
|
|
label: str
|
|
last_connected: str
|
|
node_name: str = ""
|
|
|
|
def matches(self, device: DiscoveredDevice) -> bool:
|
|
"""Return whether ``device`` is the same hardware as this record."""
|
|
return device.stable_id == self.stable_id
|
|
|
|
|
|
class DeviceStore:
|
|
"""Reads and writes the remembered "last known good" device."""
|
|
|
|
def __init__(self, path: Path) -> None:
|
|
"""Open the store against a JSON file location.
|
|
|
|
Args:
|
|
path: Path to the JSON state file (created lazily on first write).
|
|
"""
|
|
self._path = path
|
|
|
|
def load(self) -> Optional[RememberedDevice]:
|
|
"""Return the remembered device, or ``None`` if absent or unreadable.
|
|
|
|
A missing or corrupt file is treated as "nothing remembered" rather than an
|
|
error, so a stray edit never blocks startup.
|
|
|
|
Returns:
|
|
The :class:`RememberedDevice`, or ``None``.
|
|
"""
|
|
try:
|
|
data = json.loads(self._path.read_text(encoding="utf-8"))
|
|
except (OSError, ValueError):
|
|
return None
|
|
try:
|
|
return RememberedDevice(
|
|
stable_id=data["stable_id"],
|
|
port=data["port"],
|
|
label=data.get("label", data["port"]),
|
|
last_connected=data.get("last_connected", ""),
|
|
node_name=data.get("node_name", ""),
|
|
)
|
|
except (KeyError, TypeError):
|
|
return None
|
|
|
|
def remember(self, device: DiscoveredDevice, *, node_name: str = "") -> None:
|
|
"""Record ``device`` as the last known good connection.
|
|
|
|
Args:
|
|
device: The device that just connected successfully.
|
|
node_name: The device's own mesh node name, if known; preserved across
|
|
reconnects and shown in the picker. A blank value keeps any name already
|
|
on file rather than erasing it.
|
|
"""
|
|
if not node_name:
|
|
existing = self.load()
|
|
if existing is not None and existing.matches(device):
|
|
node_name = existing.node_name
|
|
record = RememberedDevice(
|
|
stable_id=device.stable_id,
|
|
port=device.port,
|
|
label=device.label,
|
|
last_connected=utcnow().isoformat(),
|
|
node_name=node_name,
|
|
)
|
|
self._path.parent.mkdir(parents=True, exist_ok=True)
|
|
payload = {
|
|
"stable_id": record.stable_id,
|
|
"port": record.port,
|
|
"label": record.label,
|
|
"last_connected": record.last_connected,
|
|
"node_name": record.node_name,
|
|
}
|
|
# Atomic-ish replace so a crash mid-write can't truncate the existing record.
|
|
tmp = self._path.with_suffix(self._path.suffix + ".tmp")
|
|
tmp.write_text(json.dumps(payload, indent=2), encoding="utf-8")
|
|
tmp.replace(self._path)
|