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.
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).
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
Route login, text, and protocol-request packets through a single
_consume_via_local_candidates fan-out that offers each one to every local
candidate sharing the one-byte destination hash — the companion bridge and the
room-server/repeater identity — and consumes it only when one MAC-verifies it.
A prefix collision with a remote node (or a forged packet) is left for the
forwarding engine instead of being swallowed.
- gate processed_by_injection on the core HandlerResult.authenticated
- protocol-request now forwards on collision instead of always marking handled
- add real-crypto integration tests (text, room-server login, protocol-request)
and router fan-out tests covering companion + room-server collisions
- Implemented `lbt_diagnostics` API endpoint to return aggregated Listen Before Talk (LBT) diagnostics aligned with RF metrics.
- Introduced methods for calculating Pearson correlation coefficients and auto-bucket sizing for diagnostics.
- Enhanced data aggregation logic in `StorageCollector` for LBT diagnostics.
- Updated OpenAPI specification to include new endpoint and response schemas.
- Added comprehensive unit tests for LBT diagnostics, including validation of correlation calculations and data integrity.
standardize default region on mesh.default_region (remove legacy region_default_scope usage)
add/align CLI support for owner.info, path.hash.mode, and loop.detect with validation
wire UI terminal get/set + autocomplete/help for owner.info, path.hash.mode, loop.detect
extend update_radio_config to persist owner_info for UI set owner.info
add and use shared packet utility for advert creation + default-region transport scoping
refactor repeater and room-server advert paths to reuse shared packet logic
The GROUP BY type and GROUP BY drop_reason sub-queries in
get_packet_stats (and get_packet_type_stats) filter by a time window but
let the planner pick idx_packets_type / idx_packets_transmitted to get
grouping for free. It then heap-checks the timestamp filter across the
entire table, turning a bounded window into a full scan: on a 1.5M-row
packets table the 24h stats load spent ~4.8s (type) + ~2.3s
(drop_reason) instead of ~0.1s each.
Pin these to idx_packets_timestamp with INDEXED BY so they range-scan the
window (~50k rows) and group via a small temp b-tree. Verified on the
live DB: 4.80s -> 0.10s and 2.32s -> 0.10s. Unlike a covering index this
adds no write-path cost on the packet-insert hot path.