Codec Layer
A codec turns a raw byte stream into discrete Messages and back again. It owns the
framing logic — where one message ends and the next begins — and the serialization of structured fields to
and from bytes. Line delimiters, length prefixes, Modbus framing, AT command parsing, COBS/SLIP encoding: all of it
lives behind one trait.
This page describes the codec contract as defined in the SDK (cycbox-sdk/src/codec.rs). Each concrete codec is
documented separately under this section.
Where codecs sit
Codecs only matter for byte-stream transports. A stream transport (serial, TCP, …) delivers unstructured
bytes; the engine wraps it in a CodecTransport, and that adapter calls the codec to cut the stream into messages.
See the Transport Layer page for the wrapping mechanics.
bytes off the wire ─► CodecTransport.buffer ─► Codec::decode ─► Message ─► engine pipeline
engine pipeline ─► Message ─► Codec::encode ─► frame bytes ─► onto the wire
Native message transports (MQTT and the like) may carry a codec for payload encoding but do not depend on it for framing, since each transport unit is already a message.
The Codec trait
#[async_trait]
pub trait Codec: Configurable + Manifestable + Send + Sync {
fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Message>, CycBoxError>;
fn decode_timeout(&mut self, src: &mut BytesMut) -> Result<Option<Message>, CycBoxError> {
self.decode(src)
}
fn decode_eof(&mut self, src: &mut BytesMut) -> Result<Option<Message>, CycBoxError> {
self.decode_timeout(src)
}
fn encode(&mut self, item: &mut Message) -> Result<(), CycBoxError>;
fn reset(&mut self) {}
async fn handle_command(&mut self, _command: &Message) -> Option<Message> { None }
}
A codec is both Configurable (it receives user configuration) and Manifestable (it advertises a UI form
schema), with category PluginCategory::Codec. It must be Send + Sync because it lives inside an async transport
task.
The decode family — decode, decode_timeout, decode_eof — share a single contract:
| Return | Meaning |
|---|---|
Ok(Some(msg)) | A complete message was framed and removed from src. |
Ok(None) | Not enough data yet; keep the buffer and read more. |
Err(e) | Unrecoverable parse error for the current data. |
In every case the codec mutates src (a BytesMut) in place: it consumes the bytes it claimed for the returned
message and leaves the remainder for the next call.
decode
The primary path. Called whenever new bytes arrive. CodecTransport calls it in a loop until it returns
Ok(None), so a single read that contains several frames produces several messages. A codec must therefore decode
exactly one message per call and return Ok(None) once the buffer holds only a partial frame.
On Err, CodecTransport logs the error and clears the buffer — the codec should return an error only for
genuinely unrecoverable input, since the side effect is to discard buffered bytes.
decode_timeout
Called when a read times out (the idle timeout passed to the transport's connect) while the buffer is non-empty.
This is the hook for gap-delimited protocols — classic Modbus RTU, some AT flows — where a message boundary is
defined by an inter-byte silence rather than by a delimiter in the data. The default simply delegates to decode,
which is correct for protocols that do not use timing as a delimiter.
decode_eof
Called when the connection closes (EOF) with data still buffered, and also when a transport reports a
session boundary. It is the codec's last chance to emit a final
frame from whatever remains. The default delegates to decode_timeout (and therefore, by default, to decode).
encode
Serializes an outgoing Message into bytes. The codec writes the framed bytes into the message's frame field
(adding headers, length prefixes, checksums, delimiters as needed). If the codec leaves frame empty,
CodecTransport falls back to sending the message's payload verbatim, so a pass-through codec can simply do
nothing.
reset
Clears internal codec state. Called when the connection is re-established and whenever a session boundary or EOF is flushed, so a stateful parser never carries one session's half-frame into the next. The default is a no-op, which is correct for stateless codecs.
handle_command
Handles a control command (MESSAGE_TYPE_REQUEST) addressed to the codec — for example a Modbus codec exposing a
"read holding registers" command that it translates into a request frame. Return Some(response) to handle it,
None to decline. In the transport's command dispatch order
the codec sits between the built-in SEND_RAW handler and the transport.
Stateful vs. stateless codecs
- Stateless codecs parse each buffer independently and need no
reset. A line codec or a pass-through codec fall here. - Stateful codecs carry parser state across calls — a partially received frame, a sequence number, an expected
response length. These must implement
resetso that reconnects and session rotations start clean.CodecTransportinvokesdecode_eofthenresetat every EOF and session boundary precisely to protect this state.
The decode lifecycle in context
The following shows how CodecTransport exercises the decode family across the connection's life. (See the
Transport Layer for the full receive loop.)
new bytes arrive
└─► loop: decode(src) until Ok(None) ─► each Ok(Some) → a message
read times out, buffer non-empty
└─► decode_timeout(src) ─► gap-delimited frame, if any
session rotates (take_session_boundary)
└─► decode_eof(src) ─► reset() ─► clear buffer
connection closes (EOF)
└─► decode_eof(src) ─► reset() ─► clear buffer ─► then Ok(None) to engine
reconnect
└─► reset() before any new bytes
Encoding and the Message frame
The frame field of a Message is the codec's I/O surface:
- On decode, the codec typically records the full on-wire bytes (
frame = header + payload + trailer) and the logical content (payload, plus structuredvalues/contentsfor display). - On encode, the codec writes the on-wire bytes back into
frame.CodecTransportwritesframeto the transport; ifframeis empty it sendspayloadinstead.
This split lets the UI show both the raw frame and the decoded fields for the same message.
Manifest and configuration
Because Codec: Configurable + Manifestable, each codec:
- advertises a
Manifest(categoryCodec) whoseFormGroups describe its options (delimiters, byte order, slave address, …), rendered by the UI as a form; and - receives the chosen values through
Configurable::configbefore it starts decoding.
Available codecs
Each codec has its own page with field-level details:
| Codec | Framing strategy |
|---|---|
| Pass-through | No framing — bytes in, bytes out. |
| Line | Delimiter-based (e.g. newline-terminated lines). |
| COBS | Consistent Overhead Byte Stuffing packet framing. |
| SLIP | Serial Line IP framing. |
| Timeout | Inter-byte-gap delimited (uses decode_timeout). |
| AT | AT command / response parsing. |
| Frame (classic / structured) | Length-prefixed and field-structured framing. |
| Modbus (RTU / TCP, client / server) | Modbus RTU and TCP framing, master and slave roles. |
Detailed per-codec documentation (configuration fields, framing rules, examples) is added in the pages that follow.