Skip to main content

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:

ReturnMeaning
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 reset so that reconnects and session rotations start clean. CodecTransport invokes decode_eof then reset at 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 structured values / contents for display).
  • On encode, the codec writes the on-wire bytes back into frame. CodecTransport writes frame to the transport; if frame is empty it sends payload instead.

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 (category Codec) whose FormGroups describe its options (delimiters, byte order, slave address, …), rendered by the UI as a form; and
  • receives the chosen values through Configurable::config before it starts decoding.

Available codecs​

Each codec has its own page with field-level details:

CodecFraming strategy
Pass-throughNo framing — bytes in, bytes out.
LineDelimiter-based (e.g. newline-terminated lines).
COBSConsistent Overhead Byte Stuffing packet framing.
SLIPSerial Line IP framing.
TimeoutInter-byte-gap delimited (uses decode_timeout).
ATAT 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.