Commit Graph

1082 Commits

Author SHA1 Message Date
Lloyd aae40fd1e1 Merge pull request #375 from lbibass/ina219-telemetry-fix 2026-07-26 16:06:03 +01:00
Lloyd 0c9980badf Merge pull request #376 from agessaman/fix/more-things 2026-07-26 16:02:21 +01:00
lbibass b955f1d0a3 added current and power to telemetry page 2026-07-25 22:41:01 -04:00
lbibass 30fe28fb6a added current and power to telemetry page. 2026-07-25 22:40:20 -04:00
lbibass 6cb3166a3a enable battery percentage on repeater stats page 2026-07-25 18:08:55 -04:00
agessaman 8310ccdee6 fix(repeater): make SIGTERM actually shut the daemon down
SIGTERM did not stop the daemon. Observed: the process stayed alive 18+
minutes with all three companion listen sockets still bound and the serial
port still held, so a restart could not reopen the radio. Two defects
compounded.

Cleanup never ran. The signal handler cancelled run(), whose finally then
awaited _shutdown() from inside an already-cancelled task -- so the first
await raised CancelledError, run() returned, and asyncio.run() tore the loop
down before a single shutdown step executed. Not one step logged. Running
cleanup in a sibling task instead does not fix it either: run() returns as
soon as the dispatcher stops and asyncio.run() cancels every leftover task
on the way out, which showed up as 'frame server :5050' being cancelled
mid-stop. The handler now unwinds run() cooperatively by stopping the
dispatcher, so run_forever() returns on its own and cleanup runs in a task
that is not being cancelled. Cancelling is kept only as a fallback for a
failure before the dispatcher exists.

Nothing bounded the steps. Frame servers, bridges, router and Glass had no
timeout, so one stuck step stranded every step after it -- including
releasing the radio. Each step is now bounded by SHUTDOWN_STEP_TIMEOUT_S and
logged by name, so a hang is both survivable and diagnosable, and the sync
steps (HTTP stop, sensor manager, GPS, radio cleanup) run off-loop so a
blocking close cannot stall the sequence. The dispatcher is stopped first so
RX ends before its radio is released.

Even with cleanup fixed, a single non-daemon thread that never returns hangs
SIGTERM forever: interpreter finalization joins them with no timeout, which
is where the original 18-minute hang sat (main thread parked in
Py_FinalizeEx -> wait_for_thread_shutdown). Report any that are still alive
by name so the offender can be fixed at the source, excluding asyncio's own
executor workers since asyncio.run() joins those under its own timeout. Then
arm a daemon watchdog that forces the process down SHUTDOWN_EXIT_GRACE_S
after cleanup finishes, so a future stray thread costs a delayed exit rather
than a stuck service.

Verified on hardware: SIGTERM and SIGINT both exit in 1s with zero warnings,
all ports and the serial device released, and MQTT publishing its offline
status before disconnecting.
2026-07-24 15:29:40 -07:00
agessaman 05dae761e7 fix(repeater): feed each bridge every pre-dedup flood copy
A bridge's return-path teacher can only pick the best-received route if it
sees every copy of a flood reply. It does not: the router hands a bridge
only the first copy, later ones being dropped by the engine's seen-table,
and the pre-dedup firehose lives on the dispatcher, which the bridge does
not own. So the teacher always taught from the first-arrived route, which on
a live mesh is routinely the worst one -- observed here, four copies of one
login reply landed over ~1.8s and the teach went out 0.4s in, embedding the
marginal first route.

Subscribe each bridge's note_flood_copy to the dispatcher's raw packet
subscribers at both bridge-creation sites, next to the existing region_map
handoff.
2026-07-24 15:07:12 -07:00
Lloyd ebf05db44b Merge pull request #372 from yellowcooln/fix/web-request-handling
fix(web): normalize OPTIONS handling
2026-07-24 20:56:37 +01:00
yellowcooln a321fce014 fix(web): normalize request handling 2026-07-24 15:52:01 -04:00
Lloyd 3d03864f98 Merge pull request #371 from agessaman/fix/more-things 2026-07-24 19:13:14 +01:00
Lloyd b1ea25727c Merge pull request #370 from agessaman/fix/more-things
Region-scoped flood replies, relaxed identity guard, cleaner config errors
2026-07-24 17:22:32 +01:00
agessaman 3d89174038 fix(repeater): report all boot-time config errors without a stack trace
Startup config mistakes were handled inconsistently: an identity collision
exited cleanly, but a missing or invalid config file (FileNotFoundError /
RuntimeError from load_config) and a missing identity key (RuntimeError) dumped
a full traceback -- and load_config ran outside main()'s try, so those errors
escaped the fatal handler entirely.

Add repeater/exceptions.py with ConfigurationError(RuntimeError) and re-parent
IdentityConfigurationError onto it (it stays a RuntimeError, so existing
except-sites are unaffected). Raise ConfigurationError for the missing/invalid
config file, the config-load failure, and the missing identity key. Move
load_config and daemon construction inside main()'s try and catch
ConfigurationError there, logging just the message and exiting 1; unexpected
failures still log with a traceback.
2026-07-24 08:50:34 -07:00
agessaman 6e5d02999b fix(repeater): report identity config errors without a stack trace
A configured-identity collision (IdentityConfigurationError from the startup
preflight) is an actionable config problem, not a crash, but the top-level
fatal handler logged every exception with exc_info=True, burying the message
under a full traceback. Catch IdentityConfigurationError in main() and log just
the message before exiting 1; unexpected errors still get the traceback.
2026-07-24 08:33:36 -07:00
agessaman 95555e0c12 fix(repeater): relax identity prefix guard to same-namespace collisions
Local identities occupy two routing/persistence namespaces: companions
(companion_bridges[hash] plus the companion_* tables keyed by the hash byte)
and server-side identities (the repeater and every room server, which share
the login/text/protocol helper handlers[hash] slot and the room_* tables). A
one-byte prefix collision is only unrepresentable when both identities share a
namespace; a companion and a server-side identity live in physically separate
stores, and the packet router (_consume_via_local_candidates) already offers a
colliding packet to both and lets HMAC pick the owner.

Key IdentityManager state by (hash_byte, namespace) instead of the bare hash so
the guard rejects only same-namespace collisions. This keeps blocking the pairs
that actually break -- companion<->companion (bridge overwrite plus
companion_prefs PRIMARY KEY corruption) and server<->server, i.e.
repeater<->room-server and room-server<->room-server (handlers[hash] overwrite
plus room_* corruption) -- while allowing a companion to share a prefix with the
repeater or a room server, which was previously rejected despite being only a
cosmetic label clash.
2026-07-24 08:24:23 -07:00
agessaman 72e874f838 fix(regions): scope flood replies to the request's region
Build a core RegionMap from the node's served regions and wire it into
the dispatcher and every companion bridge, so a flood reply is re-scoped
to the region its request arrived under (or left plain for a wildcard /
direct request) -- matching firmware simple_repeater::sendFloodReply.
Previously replies went out plain, so a reply to a request in region B
was dropped by B-only repeaters.

The map is built once from the node-wide transport_keys table (each named
region -> RegionEntry, flags=REGION_DENY_FLOOD for deny-flood regions; the
'*' wildcard is deliberately not an entry so plain floods reply plain). A
single shared instance reaches the dispatcher and all bridges. Public
regions rely on name-hashing for their key; a stored key is carried only
when it is genuinely custom material the name would not reproduce, keeping
reply-matching aligned with the forwarding transport-code check.

Region edits at runtime (CLI, web API, Glass sync) all funnel through the
transport_keys CRUD methods, which now fire a post-commit change callback;
the daemon rebuilds the map and reassigns a fresh instance to the
dispatcher and every live bridge (atomic rebind, safe against an in-flight
find_match on the RX thread).

Requires openhop_core with Dispatcher.region_map / CompanionBridge.region_map.
2026-07-24 07:52:16 -07:00
Lloyd c2d63968bb Merge pull request #367 from openhop-dev/fix/all-the-things
Key changes include:

Strengthened packet authentication, consumption, forwarding, and ACK handling across router, repeater, and companion flows.
Improved offline message persistence, queue ownership, storage eviction, and restart behaviour.
Added neighbour-link tracking, history, UI updates, and enhanced metrics retrieval with RRDtool fallback.
Refined TX delay, airtime, flood timing, advert handling, and radio parameter configuration.
Fixed room server locking, timestamp synchronisation, firmware post limits, and ACL replay protection.
Improved CLI configuration handling, validation, help text, and dependency references.
Added telemetry responses using Cayenne LPP sensor data.
Expanded regression coverage and reorganised logging, imports, and shared routing logic.
Overall, the changes improve protocol correctness, delivery reliability, configuration safety, and runtime diagnostics.
2026-07-20 21:28:05 +01:00
Rightup 6e5c4e1b0d fix: refactor SQL queries for better readability and maintainability 2026-07-20 21:25:56 +01:00
Rightup b3e4649f54 fix: add nosec comments for intentional LAN bind defaults and controlled SQL fragments 2026-07-20 21:10:45 +01:00
Rightup dc33c82f66 fix: streamline cleanup_old_data calls by consolidating argument formatting 2026-07-20 20:59:47 +01:00
Lloyd 6bd2ec4586 Merge pull request #365 from agessaman/fix/all-the-things
Fix/all the things
2026-07-20 17:30:00 +01:00
Rightup 8ba0f89a66 fix(dependencies): update openhop_core dependency references to use 'dev' branch 2026-07-20 17:28:49 +01:00
Rightup 025d8c141d fix(tests): reorder import statements 2026-07-20 13:45:09 +01:00
Rightup efb7e4a319 feat(metrics): implement metrics data retrieval with RRDtool fallback 2026-07-20 13:44:14 +01:00
Lloyd 7de15b135e Merge pull request #364 from agessaman/fix/all-the-things 2026-07-19 15:50:19 +01:00
agessaman 8fcd7902c2 docs(cli): state the zero semantics of flood.max in the set help
OpenHop treats max_flood_hops 0 as unlimited while firmware's flood.max 0
forwards nothing — the inverse. Until that edge is decided alongside the
policy-engine flood caps, say so in the help so a firmware-habituated
admin does not set 0 expecting to disable forwarding (that is what
'set repeat off' is for) and silently get unlimited flooding instead.
2026-07-19 07:40:08 -07:00
agessaman 0a65e0566b fix(router): mark companion delivery only after a bridge receives the packet
The PATH and protocol-response paths recorded the companion dedupe
unconditionally after the bridge fan-out, so a delivery where every
bridge raised was still suppressed for the full dedupe TTL — the client
lost the packet even though later mesh copies arrived. Have the fan-out
report delivery (at least one bridge completed without raising) next to
authentication, and mark the dedupe only on delivery, which is the
behaviour the marking helper's contract already documented. One healthy
bridge still counts as delivered, so duplicate suppression of repeated
mesh copies is unchanged.
2026-07-19 07:12:24 -07:00
agessaman bf72735233 fix(router): keep offering local candidates when a companion bridge raises
_consume_via_local_candidates awaited the targeted companion bridge with
no exception guard, so a raising bridge aborted the whole candidate loop:
the hash-colliding room-server or repeater identity registered at the
same one-byte dest hash was never offered the packet, and an unclaimed
packet never fell through to the forwarding engine. Wrap the bridge call
with the same log-and-continue handling the fan-out path already uses.
2026-07-19 07:12:10 -07:00
agessaman 1bbd2156a1 fix(trace): gate intermediate-hop trace relay on forward mode
Firmware only relays a TRACE at an intermediate hop when
allowPacketForward passes, so disabling forwarding stops trace relay.
The trace helper forwarded via packet injection, which is gated only by
the local-TX check — a repeater in monitor mode kept repeating traces
while reporting repeat off. Consult the repeater mode before relaying;
locally originated pings are injected directly and keep working in
monitor mode, and ping-response matching still runs before the relay
decision.
2026-07-19 07:11:49 -07:00
agessaman f19421ee6c fix(cli): clamp delay factors to the firmware ranges
Firmware rejects rxdelay outside 0-20 and txdelay/direct.txdelay outside
0-2.0; the CLI only rejected negatives, so a remote admin could set
delay factors far beyond what any firmware node would accept. Apply the
firmware ranges with the firmware error strings and state the ranges in
the help text. Existing configs with out-of-range values are untouched —
only new CLI sets are gated. Also adds an end-to-end regression test
running every set command against the real ConfigManager on a temp
config file.
2026-07-19 07:11:24 -07:00
agessaman 6db8ac97f5 fix(cli): point the flood advert interval at the engine timer key
set flood.advert.interval wrote flood_advert_interval_hours, a key
nothing reads, and get reported it with a default of 24 — the engine's
flood-advert timer (and the web API) consume send_advert_interval_hours
with a default of 10. Read and write the consumed key so the remote
command actually reschedules the timer; the orphan key is left in place
and ignored.
2026-07-19 07:10:37 -07:00
agessaman 101681fad4 fix(cli): accept radio parameters in MHz and stage them until restart
set freq and set radio stored the CLI's MHz/kHz inputs directly into
radio.frequency and radio.bandwidth, which the rest of the stack treats
as Hz — a freq change tuned the radio to a few hundred hertz. Convert to
Hz on write and validate set radio with the firmware gate (freq 150-2500,
bw 7-500, sf 5-12, cr 5-8, same error string; set freq stays unvalidated
like firmware). Radio changes are now saved without a live apply so the
reply's restart-to-apply contract is real: a live retune would cut off
the remote admin mid-session, and the all-or-nothing live radio path
would also have dragged staged frequency changes along with a tx tweak.
2026-07-19 07:10:20 -07:00
agessaman eae42d9e19 fix(cli): store security settings where login authentication reads them
The password, guest.password, and allow.read.only commands wrote a
top-level security section with a stale key name, while LoginHelper
authenticates from repeater.security.admin_password/.guest_password/
.allow_read_only — so remote password changes never took effect. Point
the set and get commands at the real subtree, and push repeater.security
onto the live repeater ACL during a repeater-section live update: the
ACL captures its passwords at registration, so without the refresh a
saved change would still only apply after a restart. Room-server ACLs
keep their per-identity settings passwords.
2026-07-19 07:09:42 -07:00
agessaman eaab0e5dca fix(cli): honor the boolean config save contract in set commands
ConfigManager.save_to_file returns a bare bool, but every mesh CLI set
command (and the password command) still tuple-unpacked it, so each one
wrote the YAML and then raised, skipping the live update and returning an
error to the remote admin. Route every save through one helper that
checks the bool, reports save failures honestly, and only live-applies
after a successful write. The test fixture mocked the stale tuple shape,
which is why the suite stayed green while the CLI was broken on the real
manager.
2026-07-19 07:08:44 -07:00
agessaman 0ed92013f9 refactor(frame_server): enhance message persistence handling in CompanionFrameServer
Updated the _persist_companion_message method to accept a queue_entry parameter for more precise removal of messages from the bridge queue. Introduced a new _remove_queue_entry method to ensure messages are removed by identity, preventing potential message loss during concurrent operations. Adjusted related tests to reflect these changes and verify correct behavior.
2026-07-18 19:37:03 -07:00
agessaman 2432332f87 refactor(airtime): introduce refresh_radio_params method for dynamic modulation updates
Added a new method to the AirtimeManager class to refresh modulation parameters without clearing transmission history. Updated ConfigManager to call this method after applying live radio configurations. Enhanced tests to verify that modulation updates occur correctly during live updates.
2026-07-17 17:38:11 -07:00
agessaman b11d4972ff test(repeater): add regression tests for client-repeat forwarding behavior
Introduced tests to verify that the repeater's bridge does not enable client-repeat forwarding in Core's Dispatcher. The tests confirm that the repeater maintains the expected inert behavior regarding client-repeat capabilities and preferences.
2026-07-17 09:23:49 -07:00
Rightup 0bbc373421 refactor(imports): reorganize import statements for logging imports 2026-07-17 08:48:44 +01:00
Rightup f1f39c71f2 refactor(logging): enhance logging configuration and add TRACE level support 2026-07-17 08:48:01 +01:00
agessaman 2139e41fc5 fix(protocol): answer telemetry requests with cayenne lpp sensor data 2026-07-16 23:37:37 -07:00
Lloyd 5dfed4118d Merge pull request #363 from agessaman/fix/all-the-things 2026-07-17 07:34:11 +01:00
agessaman 2cfeef588d fix(room_server): stop advancing the author sync watermark on post 2026-07-16 23:22:02 -07:00
agessaman 90eb3cae07 fix(acl): keep the session replay watermark monotonic 2026-07-16 22:42:34 -07:00
agessaman 1687c299d1 refactor(daemon): drop the neutralized own-packet stub 2026-07-16 22:39:15 -07:00
Adam Gessaman b664ab5959 Merge branch 'openhop-dev:fix/all-the-things' into fix/all-the-things 2026-07-16 22:29:24 -07:00
agessaman 377b054ab4 fix(room_server): push the stored post timestamp with a random attempt nonce
The pushed frame serialized the delivery wall-clock time instead of the
post's own stored timestamp, and left the flag byte's low 2 attempt bits
always zero. Firmware sends the stored post timestamp and ORs in a fresh
random attempt so a retried post produces a different packet hash and
ACK; with a fixed timestamp two retries within the same second would be
byte-identical and dropped as mesh duplicates. The serialized timestamp
now also equals the push_post_timestamp sync watermark, matching the
firmware invariant.
2026-07-16 20:37:30 -07:00
agessaman ef234f143b fix(room_server): hold the global transmit lock across the whole send
GlobalRateLimiter.acquire() enforced the inter-message gap inside an
async-with that released the lock before returning, so the caller's
radio transmission ran with no lock held and every room's sync loop
could push concurrently. acquire() now returns with the lock held
(released via release() in a finally, including on cancellation during
the gap wait), uses a monotonic clock so wall-clock jumps cannot skip
or stretch the gap, and push_post_to_client acquires it only around the
sync-state write plus the blocking send, so the ACK-timeout clock
starts when the transmission actually begins. Regression tests cover
two rooms sharing one limiter (no concurrent sends, gap enforced) and
lock release when the send raises.
2026-07-16 20:28:13 -07:00
agessaman 5a1822e642 test(engine): stop building reserved-version packets in fixtures
Firmware's Dispatcher::tryParsePacket drops any payload version above 0
at parse, and core's Packet.read_from now enforces the same rule, so a
version-1 routed ACK can never reach the relay; the wire vector
exercising that unreachable input is removed (header regeneration is
still covered by the remaining vectors, including transport-direct).
The advert fixture also OR'd the version-1 bit into its header; it now
builds the version-0 header that PacketBuilder.create_advert emits.
2026-07-16 20:28:04 -07:00
Lloyd c6ace1f04a Merge pull request #362 from agessaman/fix/all-the-things 2026-07-16 22:35:40 +01:00
agessaman 1a4df1a151 fix(room_server): enforce firmware post text budget in bytes
The room post limit was 160 Python characters; firmware caps post text
at MAX_POST_TEXT_LEN = 160-9 = 151 bytes (the 160-byte encrypted text
budget minus the timestamp/flags/author-prefix header). Store posts
truncated to 151 UTF-8 bytes at a codepoint boundary, and clamp again
when pushing so previously stored oversized posts cannot produce
oversized frames.
2026-07-16 14:28:55 -07:00
agessaman 5d390ef694 Merge remote-tracking branch 'origin/fix/all-the-things' into fix/all-the-things
Keep PacketHashingUtils for ACK relay hashing alongside flood_rx_metrics
from the metrics refactor.
2026-07-16 12:20:06 -07:00