feat(repeater): poll repeater statistics on a configurable interval
Configured repeaters are logged into, queried for their status and
logged out again on a configurable interval. Raw values are archived
with the moment of each poll, so battery voltage can be tracked over
multiple days.
New modules
- services/repeater_config_store.py — per-device configuration in
~/.meshcore-gui/repeaters/<device>_repeaters.json (directory 0700,
file 0600). Holds the full 32-byte public key, display name, login
password, poll interval and enabled flag. A key that is not 64 hex
characters is rejected: the meshcore library refuses a prefix for
login, status and logout requests. An absent password means "not
configured", an empty string is a valid credential.
- services/repeater_stats_archive.py — append-only JSONL at
~/.meshcore-gui/archive/<device>_repeater_stats.jsonl, one record per
poll, retention through the existing daily cleanup task.
- services/repeater_poller.py — send_login_sync, req_status_sync,
send_logout. The sync variants are used because plain send_login and
send_statusreq return as soon as the frame is on the wire and say
nothing about the outcome.
- gui/panels/repeater_stats_panel.py — read-only REPEATERS panel showing
the latest values, the age of the last successful poll and the last
error per repeater.
Changed
- config.py: VERSION 1.23.0, REPEATERS_DIR, REPEATER_POLL_ENABLED,
REPEATER_POLL_INTERVAL (900 s), REPEATER_POLL_CHECK_INTERVAL,
REPEATER_LOGIN_TIMEOUT, REPEATER_STATUS_TIMEOUT,
REPEATER_STATS_RETENTION_DAYS.
- ble/worker.py: _main_loop runs the repeater poll alongside the existing
periodic tasks, following the same interval-guarded pattern;
_cleanup_old_data applies retention to the new archive.
- __main__.py, gui/dashboard.py, gui/panels/__init__.py: wiring.
Behaviour
- At most one repeater is polled per check, each with its own start
offset within the interval, so two repeaters are never queried back to
back. Two repeaters on the default interval land ~7.5 minutes apart.
- A failed login, an absent status response or an exception is recorded
as a poll attempt with ok=false and a reason, and blocks neither the
other repeater nor the application. Logout is always sent once a login
has been attempted, including after a timeout — a login that returned
no confirmation may still have succeeded on the far side.
- Values are stored exactly as reported: no scaling, rounding or
averaging. Battery voltage keeps the unit the firmware returns.
- The password is reachable through a single method that only the poller
calls. Objects handed to the GUI have no password field, and no
password reaches the archive, the log or any error message.
- Polling stays idle until a repeater is configured, so an installation
without a configuration file behaves exactly as before.
No change to the archive JSON schema, the public REST API or any
existing public method signature. Worker and dashboard constructors
gained optional keyword arguments only.