- Set a warning once the interface has accepted no packets for a minute,
and log refused dictionary packets as data packets are
- Clear remote entities that a provider's dictionary does not name also
when the provider goes back to a dictionary already seen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Check a data packet before asking for its dictionary, so a damaged
packet does not make the provider resend everything
- When a provider's complete dictionary does not name a remote entity,
warn and clear that entity's state, as a typo in provider or remote_id,
or an entity the provider no longer sends, was otherwise silent
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A dictionary packet now carries its part index and a last-part flag. A dictionary counts as received, and is not
asked for again, only once its last part follows every other part in order. Until then, data under it sends a
request even when some remote entities are already known.
A refused request no longer starts the request interval. A short or malformed packet, or one with another format
version, is logged as a warning at most once a minute.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With a session and a reliability layer, the update interval defaults to
never, and unchanged states are not sent again, so a receiver's timeout
cleared them on a working link. The session layer already clears the
states when the other device cannot be reached.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each data record now starts with a 16-bit tag holding the entity's number
and type, and the form of its payload, so that more entity types can be
added and a receiver can skip records of types it does not know. A binary
sensor's state is in the tag; a sensor has a 4 byte state, or none.
Data and dictionary packets are checked in full before anything in them
is used, so a damaged or forged packet changes no state and teaches nothing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A dictionary that names none of the remote entities was asked for again
every 5 seconds while any remote entity was unresolved, and each request
made its provider resend everything. A provider's new dictionary was not
asked for once every remote entity was resolved.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The mock link takes max_packet_size in its constructor, so a test that
needs a different size builds a local link.
Co-Authored-By: Claude Code <noreply@anthropic.com>
- Compare received ids by length and bytes, so an id with an embedded NUL
cannot read past the configured string
- Fail setup when max_packet_size cannot hold the device name and the
longest entity id, instead of skipping the id at every resend
- Reject two entities of one type sent with the same id
- Avoid an unused variable without remote entities
- Use matching ids in the test configuration
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sends sensor and binary sensor states to other devices over a packet
interface. A dictionary sent on connect, on request and at an interval
names the entities, so data packets refer to them by number. The update
interval and the handling of a lost peer follow the layers the interface
has, and remote states can time out.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The rolling code accepts a packet only once the peer has sent back this
device's boot number, so a device that only receives never gets a packet.
The session's keep-alives make sure packets go both ways.
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>
Such a type would be missing from the link schema, and selecting it would
fail with an unrelated error.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
While the peer is away the layers above keep sending, so the message
repeated about once a second until the link came up again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A frame larger than max_packet_size, usually from a peer with a larger
setting, is warned about once. A link reset that the peer does not answer
is warned about after max_retries tries, and a full queue is logged at
debug level while the link is down.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every module in layers/ is imported automatically, so two modules
registering the same name silently replaced one with the other, in an
order decided by pkgutil. Registration now raises, so the mistake is
found when the configuration is loaded.
Co-Authored-By: Claude Code <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>
The link derives max_packet_size from the span it is given, so a packet
larger than the copy buffer cannot be copied into it.
Co-Authored-By: Claude Code <noreply@anthropic.com>
The copy buffer is sized by code generation to max_packet_size, but
set_max_packet_size() can raise the limit afterwards, and copying a split
packet would then overflow it. The link now takes the buffer as a span
and refuses a split packet larger than it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A read cuts a datagram longer than the buffer to fit, so a 600 byte
datagram arrived as a valid looking 508 byte packet. The buffer is now one
byte longer, and a read that fills it is dropped. The split packet test
also checks the bytes joined in the copy buffer.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Code generation declares the UDP link's buffer for packets split across
buffers as a static array of max_packet_size bytes, so it is not
allocated at run time.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Enforce max_packet_size on send and receive
- Copy split packets into a buffer allocated in setup, not on the stack
- Add unit tests for the link's send and receive paths
- Explain why the broadcast socket is always enabled
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 receive buffer is sized by code generation to max_packet_size, but
set_max_packet_size() can raise the limit afterwards, and the received
bytes would then overflow the buffer. The link now takes the buffer as a
span and stops at its size too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>