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>
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.
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>
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>
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.
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.
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
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>
- 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.
Added a new `neighbors: true` setting to the `meshat-se.yaml` configuration file under the MQTT brokers section, allowing for neighbor discovery functionality to be enabled in this preset.