Commit Graph
4 Commits
Author SHA1 Message Date
James Rich dcf3bdcdbc Send the important frames first
The TX queue was arrival-ordered, and extended advertising is set-and-repeat: a
frame that leaves first holds the radio for a whole burst. So a position update
queued ahead of an ack went out first and the ack waited behind it, which is
exactly backwards for the one a sender is timing out waiting on.

It is now a bag rather than a ring. runOnce picks the highest-priority slot and
closes the gap; equal priorities still leave oldest-first, because priority is
meant to order the queue, not to reorder within itself. A full queue makes room
only for something strictly more important, the same trade
MeshPacketQueue::replaceLowerPriorityPacket makes for LoRa, and the slot it
displaces is the newest of the least important, so a frame that has already
waited its turn is not the one thrown away. Refusals are counted in
txDroppedQueueFull, kept separate from txDroppedTooLarge: one says the bearer
cannot carry this packet at all, the other that it could not carry it right now.

The slot carries the priority because the air copy no longer does - strippedForAir
drops it, being local scheduling the receiver overwrites.
2026-09-15 07:04:09 -05:00
James Rich 8d85416fc4 Stop advertising the fields the receiver is going to overwrite
buildAdvPayload encoded the packet as it stood, so every frame carried
transport_mechanism, via_mqtt, tx_after, priority, pki_encrypted, public_key and
the rx_snr/rx_rssi/rx_time it was received with. deliverToRouter rewrites all of
those on arrival and Router::handleReceived stamps rx_time, so they were budget
spent on bytes the far side discards. rx_rssi was the worst of them twice over: a
negative int32 is a ten-byte varint, and it published the relayer's own link
quality to anyone scanning.

A relay now reaches exactly as far as an originator, where before it reached 21
bytes less far. public_key alone was up to 34 bytes on a PKC direct message.

The ceiling tests measured a fixture rather than the bearer: a packet off
Router::send also carries priority, which fixPriority never leaves UNSET, and
relay_node, which FloodingRouter::send stamps on everything. Re-shaped, and the
number is now 216 - three bytes for relay_node, nothing for priority because it
is stripped. node-kmp measures 214 for the same packet because it has no
equivalent strip.

Sizing now runs before encoding rather than being inferred from a short buffer,
so an over-budget packet is counted and logged with the overshoot while a genuine
encode failure is an error, not a silent tie.
2026-09-15 06:30:52 -05:00
James Rich 42e64cfa59 Suppress duplicate BLE relays, and measure what an advertisement actually holds
perhapsCancelDupe was gated on TRANSPORT_LORA, so a node that overheard a
neighbour relaying a packet over BLE advertised its own copy anyway. The gate is
now per medium: a LoRa dupe cancels the LoRa queue as before, a BLE dupe cancels
the BLE ring and nothing else. Cancelling across media would be wrong in the
other direction - hearing a neighbour on BLE is no evidence at all about who
heard us on LoRa, and standing the LoRa rebroadcast down on that would thin the
flood wherever the two meshes overlap.

MeshTransportBase gains cancelTransportsOn(), carrying the medium so each
transport can ignore a cancel that is not for its own radio. AdvSlot carries
from/id, and runOnce remembers the identity of the burst it started, so a cancel
reaches a payload already repeating on air as well as the ones still queued.

Separately, buildAdvPayload has always refused packets that do not fit one
unfragmented advertisement, and the existing test only proved that at 256 bytes.
The real ceiling is 219 bytes of ciphertext against the 239 a LoRa frame carries,
and 198 for a relay, because the packet is encoded as it stands and so puts the
rx_rssi, rx_snr and rx_time it was received with on the air - rx_rssi being a
negative int32 at ten bytes, and the relayer's own link quality. Both numbers are
now binary-searched and pinned, so a field added to MeshPacket shows up as a
failing test rather than as quietly shorter reach, and the refusals are counted
rather than only logged.
2026-09-14 17:36:03 -05:00
James RichandClaude Opus 5 890f12ec27 Add native tests for the BLE mesh transport
Thirteen cases over the half of the transport that has no BLE in it: building an
advertisement, the ingress guards, the TX ring and the readiness pump. Full
suite 1385/1385.

Testable because BLEMeshHandler has no platform BLE dependency - ESP32BLEMesh
and NRF52BLEMesh do - so native now compiles it via HAS_BLE_MESH=1 on
native_base. Nothing runs there: main() instantiates neither platform subclass
on portduino, so bleMeshHandler stays null.

Adds one seam for it. deliverToRouter called router->enqueueReceivedMessage
directly, which meant the ingress guards could only be tested by standing up a
live Router; it now goes through a virtual enqueueReceived() that the test
overrides to observe what survives. Production always takes the default.

The cases are the bugs this transport actually had, or the ones its guards
exist to stop: a relayed packet must not be refused (refusing it capped the mesh
at one hop), onSend must queue rather than transmit (advertising inline stalled
the router for the length of every burst), a spoofed from=0 and an out-of-range
hop count must be dropped, claimed PKI authentication must be stripped rather
than believed off the wire, and our own advertisement heard by our own scanner
must not loop back in.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-01 16:35:08 -05:00