- Without LoRa, require the SX127x radio to have a sync word or preamble
detection, as listen before talk cannot see a packet arriving otherwise
- Warn once when a send finds the radio failed
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Warn when a send goes ahead over a busy period at its limit, as that
means a stuck busy indication or a jammed channel
- Log an empty packet at verbose level, and say why it counts as sent
- Name the radio id if code generation cannot find its configuration
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Make both links one RadioLink template in packet_interface, with shared
code generation in radio_link.py, so a fix applies to both radios
- While sending is held back, watch the radio from loop() and end the
busy period once it is free, so a later reception starts a new period
instead of being sent over
- Reject an SX127x link without LoRa unless the radio is in packet mode
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Hold a send back for at most the airtime of the largest packet, worked
out from the radio settings and at least 2 s, instead of a fixed 2 s
that a LoRa packet at a high spreading factor outlasts
- End a busy period when a send finds the radio free, so a busy flag that
never clears is released however rarely sends are tried
- Detect an FSK reception on the SX127x from its preamble and sync word
flags
- Test the radio links' final validation
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each packet is one radio packet, with the RSSI and SNR of received
packets in the metadata. A radio cannot hear while it sends, so a link
refuses to send while its radio is receiving a packet, for up to two
seconds, and the layers above send again shortly.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Work out each layer's packet size during validation, as the C++ does,
and reject a fragmentation max_packet_size whose fragments do not all
fit in the reliability queue at once, or need more than 128 fragments
- Fit the test peer's largest packet in its queue
- Reuse interface_layers() in final_validate_interface()
- Keep the earlier tests in place in test_packet_interface.py
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A broadcast from a device with its own port went to that port, whose
layers drop it, so a broadcast port could not reach reliable peers. It now
goes to the broadcast port when there is one.
Fragmentation allocates its buffers with RAMAllocator and fails setup when
they cannot be allocated. A fragment from another source no longer drops
the packet being joined.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AddressingPort derives max_packet_size from the link in its constructor,
and the fragmentation layer passes its configured size to the base
constructor.
Co-Authored-By: Claude Code <noreply@anthropic.com>
As in the other layers, addressing ports and fragmentation get no room
when max_packet_size is smaller than their header, and fragmentation
fails its setup instead of dividing by zero.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The addressing layer lets several devices share a channel such as an
RS485 bus or a radio, with 16-bit addresses. A link can carry several
peers, each with its own stack of layers and its own id. Layers that work
between two devices only are rejected with broadcast addressing. The
fragmentation layer splits packets larger than the layers below allow,
and joins them again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both layers derive max_packet_size from the layer below in their
constructors instead of assigning it in the body.
Co-Authored-By: Claude Code <noreply@anthropic.com>
- Fail setup, instead of underflowing, when max_packet_size leaves the
encryption or session layer no room
- Warn when the boot number cannot be saved
- Warn once when the peer's packets carry an older boot number, which
happens when its flash is erased
- Test the counter wrapping to a new boot number
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The session layer sends keep-alives and reports whether the peer can be
reached, including after it restarts or a reliability layer below loses
data. The encryption layer encrypts and checks each packet with XXTEA,
with an optional rolling code against replays. Components that a layer
needs, such as binary_sensor, are loaded only when it is configured.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A peer that has not heard the UA repeats its SABM, and cannot have sent a
numbered frame meanwhile. Treating that as a restart threw away queued
frames and reported the session lost for a single dropped UA. A SABM is
now a restart only if the peer has sent a numbered frame since the link
came up.
Co-Authored-By: Claude Code <noreply@anthropic.com>
max_packet_size is derived from the layer below in the constructor
instead of being assigned in the body.
Co-Authored-By: Claude Code <noreply@anthropic.com>
- Keep App alive across the HDLC tests, as the test main requires
- Run a layer's loop in the tests only while it has asked for it, so
the loop's disable and request bookkeeping is tested, and wait for the
retry timer in the tests of refused sends
- Report no send capacity while the layer has no send queue
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Reset the link after max_retries even when it is 255, where the retry
counter used to wrap
- Require a reliability timeout of at least 50 ms
- Drop received frames larger than the layer's max_packet_size
- Fail setup, instead of aborting, when the send queue cannot be
allocated or max_packet_size leaves no room
- Test the ack delay timer and a refused REJ
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Layers stack on a link and are configured on its packet_interface entry.
Each layer type registers itself with its kind, so a new type needs only
a new module. The first is a reliability layer in the style of HDLC
asynchronous balanced mode, with go-back-N retransmission.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each packet is one UDP datagram, using the settings of the udp component.
The udp component now reports whether a packet was sent, and an ENOMEM
refusal from lwIP is logged at verbose level as the packet can be sent
again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
max_packet_size was a default with a setter that code generation called
after construction, so it could be raised above the size of a link's
buffer. It is now a constructor parameter, and a link with a fixed buffer
takes the buffer as a std::span, so its size is the link's limit and the
two cannot differ.
Co-Authored-By: Claude Code <noreply@anthropic.com>
The test main constructs App once, and later tests and its listener use
it, so the fixture now replaces App in SetUp and constructs it again in
TearDown, instead of constructing over a live App and leaving it
destroyed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A fake link with a layer stacked on it checks that the bridge sends
through the top of the stack, passes a packet in one buffer to
packet_transport, and drops a packet split across buffers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
It allocated a vector for each call, which invites heap churn. Only the
tests used it; they now have a local helper.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A packet interface moves packets over a channel, with a uart link and a
packet_transport platform that uses any packet interface. Later layers
stack on the same base class.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>