Commit Graph

176 Commits

Author SHA1 Message Date
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
agessaman 2139e41fc5 fix(protocol): answer telemetry requests with cayenne lpp sensor data 2026-07-16 23:37:37 -07: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 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
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
agessaman d886af0991 fix(engine): relay routed ACKs through their MeshCore branch
An ACK at an intermediate direct hop is now regenerated as a plain
DIRECT ACK (transport codes dropped, version bits cleared) and relayed
with zero retransmit delay, matching routeDirectRecvAcks, instead of
taking the generic delayed direct forward. The repeater.multi_acks
preference (already exposed by the mesh CLI) is now consumed by the
engine: when enabled, both ACK relay sites precede the plain ACK with
a MULTIPART-wrapped redundancy copy spaced by the direct retransmit
delay + 300 ms. The multipart relay seen-key now keeps the MULTIPART
payload type over the unwrapped payload, matching firmware hasSeen so
a redundancy pair survives each hop.
2026-07-16 10:48:52 -07:00
Adam Gessaman 40f0891785 Merge branch 'openhop-dev:fix/all-the-things' into fix/all-the-things 2026-07-16 10:22:25 -07:00
agessaman b69e1c89ee test(web): pin /api/identities payload with the repeater identity
The daemon registers its own default identity so companion and
room-server hash collisions against the repeater are caught, which
adds a repeater:repeater entry to the endpoint's raw registered list
and counts it in total_registered. The web UI only renders the
per-entry registered boolean on configured room servers and
companions (matched by prefixed name), so it is unaffected; pin the
payload shape for external API consumers, and pin that the bare name
"repeater" is reserved — a room server configured with that name is
rejected by the collision rules and reported unregistered.
2026-07-16 08:48:57 -07:00
Rightup 7149337b7c refactor(metrics): replace packet score calculation with flood metrics from core 2026-07-16 13:32:49 +01:00
agessaman a620433312 Merge upstream/fix/all-the-things into fix/all-the-things
Keep both migration 12 (companion message signal/channel data) and
migration 13 (packet upstream hash for neighbour links), and retain
ACK/MULTIPART plus TRACE imports in engine tests.
2026-07-15 22:51:15 -07:00
agessaman 7c2e121d08 refactor(daemon): resolve companion max TX power via the core resolver
The daemon's max-TX-power lookup duplicated the core fallback chain and
ended in a radio_type string match to know the SX1262's 22 dBm limit --
a driver constant that every new backend name would silently lose. The
SX1262 driver class now declares max_tx_power_dbm itself (the CH341
variant instantiates the same class, only the transport differs), so
the lookup collapses to the shared resolver over the radio object and
the validated deployment settings.
2026-07-15 22:27:25 -07:00
agessaman f91a30afa1 docs(engine): state TX delay factor semantics at startup
The TX delay factors changed from absolute seconds to firmware-matching
airtime multipliers (random delay in [0, 5 * airtime * factor], per
MeshCore getRetransmitDelay), but deployed configs tuned under the old
seconds interpretation get no signal on upgrade. Log the interpretation
and both factors at startup, and fix the stale wording in
config.yaml.example and the web API docstring. Defaults are unchanged
(flood 1.0, direct 0.5; firmware ships 0.5/0.3).
2026-07-15 22:02:18 -07:00
agessaman 8a776a122e fix(cli): point txdelay settings at the delays config section
The mesh CLI's get/set txdelay and direct.txdelay read and wrote
repeater.tx_delay_factor / repeater.direct_tx_delay_factor, but the
engine and web API use the delays section, so the CLI knobs were
silently dead: set appeared to succeed while changing nothing, and get
echoed the unused value back. Same defect and same fix as the recent
rxdelay repoint.
2026-07-15 22:02:18 -07:00
agessaman 2d6b1131ac fix(storage): evict offline-queue rows set-based in insertion order
The companion queue's capacity path ran a Python loop of single-row
SELECT+DELETE pairs ordered by created_at. Wall-clock ordering meant a
backwards clock step (NTP correction) could make the just-inserted
channel row sort as oldest and wrongly reject the incoming message while
older channel rows remained evictable.

Replace the loop with one set-based DELETE ordered by id (AUTOINCREMENT,
i.e. insertion order, immune to clock steps), with an evictable-count
pre-check preserving the all-or-nothing rejection rule: never displace a
direct message, never evict the incoming row to make room for itself,
roll back the insert entirely when channel rows cannot make room. The
queue load and pop queries move to id ordering for the same reason.
2026-07-15 22:02:18 -07:00
agessaman 3e2231f2bc fix(repeater): wire the flood reception delay base into the dispatcher
The delays.rx_delay_base config value was stored and reported but never
applied. Now that the core dispatcher implements MeshCore's flood
reception-quality delay, feed it through:

- set dispatcher.rx_delay_base from delays.rx_delay_base at startup and
  re-apply it on live config updates of the delays section (web API
  saves already pass that section)
- point the mesh CLI get/set rxdelay at the delays section; it read and
  wrote repeater.rx_delay_base, which nothing consumes, so the CLI knob
  was silently dead
- delegate RepeaterHandler.calculate_packet_score to the shared core
  packet_score (same firmware packetScoreInt formula, one impl)

Suppression works through the existing engine seen-cache because it
dedupes at process time, after the dispatcher hold. Default remains 0
(delay disabled), matching firmware.
2026-07-15 21:22:32 -07:00
agessaman b9579b6c15 fix(airtime): delegate airtime math to the shared core estimator
AirtimeManager.calculate_airtime now uses openhop_core's
calculate_lora_airtime_ms, which matches RadioLib's getTimeOnAir. This
adopts the radio driver's symbol-time auto-LDRO rule in place of the
SF/BW shorthand, which diverged from hardware at 16.384 ms symbol
settings such as SF12 @ 250 kHz and SF10 @ 62.5 kHz. Test oracles
follow the same rule, with literal RadioLib-derived regression values
for the divergent settings.
2026-07-15 20:35:07 -07:00
agessaman 034cd6f566 fix(companion): persist queued message signal and channel data 2026-07-15 17:06:00 -07:00
agessaman a208fc3763 fix(repeater): relay multipart ACKs as regenerated direct ACKs
Match MeshCore's forwardMultipartDirect: a multipart packet is never
relayed as an ordinary directed packet. At an intermediate direct hop a
multipart ACK is re-emitted as a plain DIRECT ACK with the wrapper byte
stripped and this node removed from the path, deduped on the regenerated
ACK form and spaced by (remaining + 1) * 300ms. Flood routing, final
hops, and non-ACK embedded types are dropped rather than forwarded.

Coverage includes literal firmware-format wire vectors for one- and
two-byte hash widths and transport-direct input, end-to-end receive-to-
transmit relaying, and the drop cases.
2026-07-15 16:29:20 -07:00
Rightup 9688fe70e4 feat(neighbour-links): implement neighbour link tracking and history retrieval 2026-07-15 17:08:40 +01:00
Lloyd 6aafa7fe99 Merge pull request #359 from agessaman/fix/all-the-things
Fix router and companion issues; refactor identity validation
2026-07-15 08:41:32 +01:00
Adam Gessaman 2646e11f8e Merge pull request #2 from agessaman/codex/local-identity-preflight-056
Consolidate identity collision validation in IdentityManager
2026-07-15 00:36:56 -07:00
agessaman 9a2559a253 refactor(companion): register persistence hooks via message events
Use openhop_core's new single-argument event registrations for the
frame server's SQLite persistence callbacks instead of the deprecated
positional on_*_received forms.
2026-07-15 00:31:32 -07:00
agessaman c22e4df37d refactor(repeater): consolidate identity collision validation
The startup preflight re-implemented the identity collision rules in
main.py, reconstructing IdentityManager state by string-parsing its
'type:name' labels, and every configured identity was parsed and
constructed twice (once for preflight, once for loading), duplicating
config warnings on each start.

Move batch validation into IdentityManager.validate_specs(), which
checks a batch of IdentitySpec entries against registered identities
and against each other without mutating state, and relocate
IdentityConfigurationError next to it. The daemon now parses room
server and companion configs once during preflight and the identity
loaders reuse the cached specs.
2026-07-15 00:12:42 -07:00
agessaman 697056aa93 fix(airtime): normalize legacy coding-rate indices
AirtimeManager fed the configured coding rate straight into the Semtech
symbol formula, so a config using the legacy index form (1..4) would
multiply payload symbols by the raw index instead of the 4/5..4/8
denominator and undercount airtime. Normalize through openhop_core's
shared coding_rate_denominator() before calculating.
2026-07-14 23:57:17 -07:00
agessaman 1b5cc71ed5 fix(repeater): validate local identity collisions 2026-07-14 22:54:27 -07:00
agessaman 74528af3ae fix(companion): expose repeater radio state 2026-07-14 21:44:27 -07:00
agessaman b5a327b925 fix(router): defer direct payload handling 2026-07-14 21:08:42 -07:00
agessaman 612a4c3f0e fix(companion): route group and raw data like MeshCore 2026-07-14 20:40:13 -07:00
agessaman 06c0f5268d fix(companion): persist messages before client connection 2026-07-14 18:35:05 -07:00
agessaman 164539597c fix(companion): keep restarted queue rows single-owner 2026-07-14 17:55:52 -07:00
agessaman 79cba76b4d fix(companion): protect direct offline messages 2026-07-14 16:59:34 -07:00
agessaman 1fe3fb1779 fix(storage): retain signed advert packets 2026-07-14 15:51:40 -07:00
agessaman 7621074d05 fix(router): accept maximum direct paths 2026-07-14 15:22:20 -07:00
Rightup f10b512c91 fix(advert): set default values to False for adaptive, rate limit, and penalty configurations 2026-07-14 23:07:12 +01:00
agessaman 059065ce9c fix(router): never forward control packets to the engine
MeshCore never relays control packets: the direct high-bit discovery
subset is explicitly released and any other control payload hits the
switch default that does not flood-route unknown types. Mark control
packets do-not-retransmit unconditionally instead of only when discovery
is enabled, so a repeater with discovery disabled no longer forwards
control/discovery traffic.
2026-07-13 20:40:22 -07:00
agessaman 62ad7424c2 fix(router): consume PATH and RESPONSE only after MAC authentication
Extend the authenticated-ownership model to the PATH and RESPONSE
routing branches so a packet is consumed (do-not-retransmit) only when
a local identity MAC-verifies it. Prefix-only collisions and forged
traffic stay eligible for the forwarding engine.

- PathHelper marks do-not-retransmit and reports authenticated only
  after a successful MAC decrypt with a valid path envelope; invalid or
  truncated envelopes remain forwardable.
- PacketRouter aggregates authenticated results across the path helper
  and companion bridges for PATH and RESPONSE, skipping the engine only
  on authenticated ownership while preserving empty-path DIRECT release
  hygiene.
2026-07-13 19:54:59 -07:00