Commit Graph
246 Commits
Author SHA1 Message Date
skjegg 0a2eeea8ee fix(companion): report real battery voltage in core stats
`_get_companion_stats` hardcoded `battery_mv: 0`, so every MeshCore companion
client connected to a Python repeater showed no battery, even when a sensor
plug-in was reporting one. The protocol side is already complete: the frame
server packs `battery_mv` into the STATS_TYPE_CORE frame and clients request it
via CMD_GET_BATT_AND_STORAGE.

Surface the first sensor reading that publishes a usable battery voltage,
accepting either `battery_voltage_mv` or `battery_voltage_v`. This covers an
attached UPS HAT as well as the battery of an openHop modem the repeater is
driving, which is the case that prompted this: a solar-powered modem whose
battery was invisible to the companion app right up until it went flat.

Returns 0 when no sensor reports a battery, which is what clients already
expect from a mains-powered node, and the lookup never raises — stats must
not be able to take down the companion connection.
2026-08-30 08:54:36 -04:00
yellowcooln 50921e1400 refactor(sensors): remove legacy Station G3 fallback 2026-09-01 10:48:36 -04:00
yellowcooln 0d28072063 merge: sync modem power telemetry PR with dev 2026-08-30 20:56:48 -04:00
yellowcooln 40a4e4f245 fix(sensors): normalize modem power telemetry 2026-08-30 20:55:48 -04:00
yellowcooln 9e79acc887 docs: update Proxmox LXC requirements 2026-08-29 23:46:49 -04:00
yellowcooln a6ab8bc906 feat: shallow clone Console distribution 2026-08-29 23:19:09 -04:00
yellowcooln def72a2b19 feat: fully update Debian before Repeater install 2026-08-29 23:04:59 -04:00
yellowcooln dc8a42a16a fix: set Proxmox container description correctly 2026-08-29 22:58:35 -04:00
yellowcooln 7664eb08d6 feat: confirm updates and manage Console checkout 2026-08-29 16:06:10 -04:00
yellowcooln 7fce3b0d74 feat: add VLAN option and valid default password 2026-08-29 15:55:11 -04:00
yellowcooln 4fadb3b767 fix: match LXC template to host architecture 2026-08-29 15:51:35 -04:00
yellowcooln 403151247b test: allow installer runs from fork branches 2026-08-29 15:47:53 -04:00
yellowcooln 4e8be755b8 fix: harden Proxmox installer inputs 2026-08-29 15:34:57 -04:00
yellowcooln 0431dfe883 feat: modernize Proxmox LXC installer 2026-08-29 15:31:33 -04:00
Rightup e28be27a77 Merge branch 'pr-402' into dev 2026-08-27 20:53:21 +01:00
Rightup 24a45ba641 Merge branch 'pr-397' into dev 2026-08-24 14:21:50 +01:00
yellowcooln f280bbdc5c fix: secure modem migration and backup paths 2026-08-24 09:00:50 -04:00
yellowcooln ada8ba98fb Merge current dev and preserve modem tokens 2026-08-24 08:50:44 -04:00
Rightup 047a68706d Merge branch 'pr-408' into dev 2026-08-24 13:48:47 +01:00
Rightup 7bd0c77c49 Merge branch 'pr-403' into dev 2026-08-24 13:28:04 +01:00
TeTeHackoandClaude Fable 5 8acba8394f fix(rooms): advertise settings.node_name from the CLI path too
The room CLI advert read only settings.room_name while the daemon
scheduler and the HTTP endpoint read settings.node_name -- so when the
two keys differed, a single `advert` issued over the mesh CLI renamed
the room on the mesh to the identity-name fallback. Both read sites now
resolve node_name -> room_name -> identity name.

Seen in production on 2026-08-19: a room configured with node_name only
renamed itself after one CLI advert and clients tracked it under the
identity name until the next scheduler advert.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 00:05:30 +02:00
agessaman f91d2e9504 test(acl): drive the wire tests from core's conformance vectors
The end-to-end tests asserted reply[7] == PERM_ACL_ADMIN, which follows
the constant wherever it goes — the one thing these tests exist to stop.
They now assert the literal bytes from openhop_core's acl_conformance
table, so both repos are pinned to the same numbers rather than to each
other's vocabulary. Checked out against 2a55dd6 with these tests copied
in, all seven fail.

Also cover the fail-closed import guard, which was the only untested
path protecting against a mixed-version install, plus its happy path so
a guard that always fires cannot pass unnoticed.
2026-08-21 18:56:23 -07:00
Thibault VINCENT 8008f09634 feat(ws): carry the sidebar vitals in the stats broadcast, decimate the 24h aggregate
The 5s WebSocket broadcast used to carry uptime_seconds alone — while
the dashboard sidebar starved: its duty-cycle gauge, noise floor and
mode never travel over WS, and the heartbeat marks stats fresh, which
suppresses the UI's HTTP fallback poll. The vitals now ride every
beat, all cheap in-memory scalars (~200 bytes): utilization_percent,
noise_floor_dbm (kept on record, no per-beat SQL), the repeater mode
(so a change made over RF or CLI reaches the web badge), and the
advert tier counters the sidebar's OK/Drop line shows, injected as a
callback from the daemon like the other collector hooks.

In exchange the heavy get_packet_stats(24h) SQL aggregate is decimated
to one beat in six (30s): nobody reads a 24h cumulative at 5s
resolution, and the UI already treats both payload fields as optional,
so older dashboards keep working. Net wire traffic goes down.

Signed-off-by: Thibault VINCENT <root@devcat.org>
2026-08-22 01:28:50 +02:00
Thibault VINCENT a725dfbe85 fix(api): persist the mode selected through set_mode
The mode selector applied its choice to the shared config dict — which
the engine reads live, so the switch felt effective — but nothing ever
wrote it to disk: a restart silently reverted the node to its previous
mode. The web UI is blameless; it calls /api/set_mode expecting it to
persist (SetCommand.ts even documents that expectation), and the RF
admin path already persists the same change through
_apply_config_update.

set_mode now saves through config_manager.update_and_save, the same
path the settings endpoints use, and reports persisted: true. If the
save fails the live switch stands — matching the old behaviour — but
the response is a loud error instead of a silent future loss.

Signed-off-by: Thibault VINCENT <root@devcat.org>
2026-08-21 22:16:22 +02:00
agessaman 7a2fb9b2df fix(acl): fail closed when openhop_core is too old for the role numbering
acl.py kept its own copy of the role constants, so an installation could
pair the new numbering with a core that still derives is_admin from the
0x02 bit. That combination announces a room server's READ_WRITE clients
(role 2) as admins on the login reply's is_admin byte — silently, and
exactly where this change is meant to make the two bytes agree. The
dependency is a mutable openhop_core@dev, so nothing prevented it.

Import the roles and acl_is_admin/acl_role from openhop_core instead of
duplicating them, and raise an actionable ImportError when they are
absent: refusing to start beats misannouncing a client's privileges. The
drift test no longer skips on an old core, so CI reports the mismatch
rather than hiding it.
2026-08-21 12:00:58 -07:00
agessaman 437a7ee332 fix(api): declare read_write in the ACL client permissions enum
The ACL client list now reports the real role, so a room server's
read-write clients emit "read_write" — a value the OpenAPI schema did
not allow, which generated clients and validators reject.

Add it to the enum, document all four roles, and add a test asserting
every role_name() we can emit is declared in the schema.
2026-08-21 11:57:24 -07:00
agessaman 3023eaca89 fix(acl): align ACL role numbering with upstream MeshCore
OpenHop numbered the ACL roles GUEST=0x01, ADMIN=0x02, READ_WRITE=0x01.
Firmware ClientACL.h uses GUEST=0, READ_ONLY=1, READ_WRITE=2, ADMIN=3 in
the low two bits, so our admin byte decoded as READ_WRITE in every stock
MeshCore client (KiekR showed admin sessions as guest and hid the admin
controls), and GUEST/READ_WRITE were indistinguishable from each other.

Adopt the firmware values and split the guest password by server type,
as firmware does: a repeater's guest password grants GUEST (fetch base
telemetry, no settings changes) and a room server's grants READ_WRITE
(post and read messages). Blank-password read-only access stays GUEST,
matching simple_room_server's allow_read_only branch.

Also fix the mesh-CLI admin gate in text.py, which shadowed a local
PERM_ACL_ADMIN = 0x02 and tested the bit rather than the role — under
the corrected numbering that would have let READ_WRITE clients run admin
commands. The web ACL list now reports the real role name instead of
collapsing every non-admin to "guest".

Requires openhop_core with acl_is_admin() in the login reply
(openhop_core fix/login-perms); the two must land together.

Fixes #388
2026-08-21 11:17:15 -07:00
yellowcooln 301ba2cce8 fix: read canonical modem token status 2026-08-20 13:21:51 -04:00
yellowcooln 76e9a9c745 feat: migrate openHop modem sensor 2026-08-20 12:17:26 -04:00
yellowcooln 9511a8477f feat: migrate openHop modem transport names 2026-08-20 09:40:09 -04:00
Rightup 653733a8b5 feat(mqtt): add support for custom base topic in MQTT configuration UI 2026-08-18 22:33:47 +01:00
Rightup 1035666066 feat(config): add direct_advert_interval_hours to repeater configuration stats 2026-08-18 17:17:23 +01:00
Rightup aa59c313e4 Merge branch 'pr-395' into dev 2026-08-18 08:40:11 +01:00
Rightup e840c10ed9 Merge branch 'pr-392' into dev 2026-08-18 08:33:11 +01:00
Rightup f75200de6c Merge branch 'pr-393' into dev 2026-08-18 08:30:55 +01:00
Thibault VINCENT 9c1b3b4650 fix(update): never install a fabricated version
The OTA updater could stamp a build with a version that did not come from
the code being installed, which left nodes stuck on a version they could
not upgrade away from.

Two independent defects combined:

  * _fetch_latest_version() returned the newest repository tag whenever the
    GitHub lookup failed. On a dynamic channel such as dev, that value is
    from a different branch entirely, so the UI reported "up to date" while
    tracking a channel that was hundreds of commits ahead. The fallback also
    swallowed _RateLimitError, defeating the backoff path already built for
    it.

  * _do_install() then passed that predicted version to setuptools_scm as
    SETUPTOOLS_SCM_PRETEND_VERSION (and as the pretend-version argument of
    pymc-do-upgrade), so pip built the channel's code but recorded the
    predicted version as the installed one. When the two matched, every
    later upgrade — including Force Reinstall — resolved as "requirement
    already satisfied" and did nothing.

Observed on a node tracking dev: installed 1.1.1, latest 1.1.1, no update
offered, while dev was at 1.1.2.dev201.

A version lookup that cannot be resolved now raises, so a failed check is
reported as a failed check instead of a wrong version, and the install
derives its version from the checkout pip clones.

The remaining pretend-version uses in manage.sh are scoped to this
distribution. The unscoped variable applies to every setuptools_scm-based
project built in the same pip invocation, not only ours: dependencies
normally arrive as wheels and are unaffected, but one built from an sdist
inherits our version. With it set, pip wheel on the portend sdist produces
portend-9.9.9; portend and jaraco.functools both build with setuptools_scm.
buildroot-manage.sh already exports the scoped variant.

Signed-off-by: Thibault VINCENT <root@devcat.org>
2026-08-17 18:00:46 +02:00
Chicagoland Mesh 1de4aa2ae3 feat(presets): add chimesh broker preset for ChicagolandMesh.org 2026-08-15 21:44:00 -05:00
ViezeVingertjes 823f604585 Flag serial ports whose path Docker cannot map in the port picker 2026-08-15 10:38:47 +02:00
Rightup 8fc0a2965d fix(api_endpoints): use getattr for request headers to avoid attribute errors
fix(tests): update _DummySX1262Radio to inherit from _DummyRadio for better structure
2026-08-13 16:27:16 +01:00
Rightup a85a04bffc Merge branch 'pr-385' into intergration 2026-08-13 16:06:08 +01:00
Rightup fd2c6102f3 Merge branch 'pr-386' into intergration 2026-08-13 16:06:08 +01:00
Rightup 51e16efcbb Merge branch 'pr-387' into intergration 2026-08-13 16:06:08 +01:00
Étienne Fesser 258c256ba4 feat(telemetry): emit barometric pressure in environment telemetry 2026-08-12 17:24:38 +02:00
Étienne Fesser 22acdff6d3 feat(sensors): add BME280 temperature/humidity/pressure plug-in 2026-08-12 17:24:38 +02:00
Rightup 4c2a3af30a feat: add multi-radio stack support and related tests
- Added tests for multi-radio stack functionality, including merging radio entries and building radio stacks.
- Implemented policy context tests to ensure correct handling of radio IDs in multi-radio scenarios.
2026-08-11 16:14:36 +01:00
yellowcooln 5b5f2a0b84 docs: point Repeater guidance to central site 2026-08-10 21:42:34 -04:00
Étienne Fesser 43c1b342bd fix: assign one telemetry channel per sensor reading 2026-08-10 00:39:23 +02:00
agessaman 1221734ca2 feat(discovery): preserve existing advert metadata during discovery result persistence
Enhanced the `persist_discovery_result` function to retain advert metadata such as `node_name`, `latitude`, and `longitude` from existing records when updating neighbor information. This prevents overwriting valuable metadata with `None` values during SQLite updates. Added a corresponding test to verify that existing metadata is preserved correctly during the discovery process.
2026-07-29 13:41:27 -07:00
agessaman 2b8be8cb7e feat(api): enhance neighbor scopes API with served scopes
Added a new `served` field to the neighbor scopes API response, which includes the node's own advertised scopes. This allows clients to identify which scopes they already share with neighbors without needing to re-derive the rules. Updated the OpenAPI specification to reflect this change and added a new private method to retrieve the served scopes. Additionally, rebuilt the UI assets to incorporate these changes.
2026-07-29 13:17:15 -07:00
agessaman 9c417478f3 feat(neighbors): persist neighbour scopes and expose them over the API
Scope answers were built into the MQTT payload and then discarded, so the web UI
had nothing to show between cycles and no way to ask a single repeater. Adds a
store for them, a read endpoint, and a single-target query.

Migration 15 adds neighbor_scopes: one row per queried neighbour holding the last
answer (`scopes`, `responded_at`) alongside the last query's outcome
(`status`, `queried_at`). The two are kept apart deliberately -- a failed query
updates the outcome but leaves the answer in place, because the responder
rate-limits anonymous replies to 4 every 3 minutes and one timeout is weak
evidence that a neighbour's scopes changed. An empty answer is stored as a real
answer: it means the neighbour serves unscoped traffic only.

A row is written for any query the node attempted. `timeout` alone cannot carry
that: the sweep reports it both for a neighbour that was asked and stayed silent
and for one it never reached, which is what ScopeResult.transmitted separates.
`send_failed` is recorded too even though nothing reached the air -- it was
attempted and the duty cycle refused, and skipping it left a repeater that keeps
refusing reading as "never queried" however often it was asked.

Scope rows follow their neighbour out of the database, on all four paths that
delete adverts: the two explicit deletes, the 6-hourly retention cleanup, and a
purge of the adverts table. Without the last two the table grew without bound and
a purge left scope counts on screen for repeaters no longer listed.

GET /api/neighbor_scopes serves the table; it stays separate from the paginated
advert queries, which are read per contact type on every page load.

POST /api/query_neighbor_scopes asks one neighbour now. Unlike publish_neighbors
it holds the request open for the reply, since the response window is normally its
5 s floor; past 45 s it returns and leaves the query running so a late answer
still lands rather than throwing away spent airtime. It returns the stored view,
not the raw result, so a failed query cannot tell the client to forget scopes the
database still holds. Nothing is published -- the periodic cycle owns the topic.

A query and a cycle must not collide over the scope helper. A cycle holds it for
its whole run including the discovery window, so a query refuses while one is
active; a cycle defers while a query is in flight; and if the two still race, the
cycle abandons the pass on the short retry delay instead of publishing a table
with every scope missing or dying and re-spending its discovery broadcast. Queries
are tracked so shutdown cancels them rather than transmitting through teardown.

The endpoint needs json_in: it is not on globally for /api and cherrypy's Request
has no `json` attribute without it, so reading the body would have failed on every
real request. A test asserts the decorator rather than trusting a fabricated
cherrypy.request, which is how it went unnoticed.
2026-07-29 09:58:41 -07:00