Implementation Guide
This guide covers implementing the Fusain protocol in firmware and software applications. For protocol specification, see Packet Format. For message definitions, see Message Types. For communication patterns and telemetry, see Communication Patterns.
Buffers
Implementations MUST allocate buffers large enough to handle worst-case byte stuffing.
Receive Buffer
The receive buffer holds stuffed bytes between START and END delimiters.
Calculation |
Size |
|---|---|
Unstuffed content (127-byte packet minus START and END delimiters) |
127 − 2 = 125 bytes |
Worst-case stuffing (every byte escaped) |
125 × 2 = 250 bytes |
Minimum buffer size |
256 bytes |
If 256 bytes are received without an END delimiter, the buffer MUST be discarded and the receiver MUST resynchronize by waiting for a new START delimiter.
Transmit Buffer
The transmit buffer holds the encoded packet before transmission.
Calculation |
Size |
|---|---|
Maximum packet size |
127 bytes |
Worst-case stuffing |
250 bytes |
START and END delimiters |
2 bytes |
Minimum buffer size |
256 bytes |
Timeouts
Inter-Byte Timeout
Implementations MUST discard partial packets after 100ms of silence (no bytes received).
This timeout ensures that incomplete packets due to transmission errors, disconnections, or noise do not permanently block the decoder. When the timeout elapses mid-packet, the receiver resets to searching for a new START byte.
Parameter |
Value |
|---|---|
Inter-byte timeout |
100ms |
Action on timeout |
Discard partial packet, reset to START search |
Communication Timeout
Appliances track time since last PING_REQUEST received. This provides a safety mechanism for unattended operation.
Parameter |
Value |
|---|---|
Default timeout |
30 seconds |
Configurable range |
5-60 seconds |
Important: Only PING_REQUEST resets the communication timeout timer. Other commands (STATE_COMMAND, MOTOR_COMMAND, SEND_TELEMETRY, etc.) do NOT reset the timer.
Controllers SHOULD send PING_REQUEST every 10-15 seconds to maintain communication. For physical layer timing, see Physical Layer.
Timeout Behavior
When the communication timeout elapses, the appliance performs two independent actions:
Disable telemetry broadcasts - Stops all periodic telemetry transmission. This quiets the bus while waiting for the controller to reconnect.
Initiate safe shutdown - If the appliance is operating (not already IDLE), transition to IDLE mode. If temperature is elevated, this triggers a COOLING cycle before reaching IDLE to ensure safe shutdown.
These are separate actions:
Telemetry is always disabled, even if the appliance is already in IDLE state
The state transition only occurs if the appliance is not already in IDLE
Telemetry remains disabled until explicitly re-enabled via TELEMETRY_CONFIG
Rationale: Disabling telemetry reduces bus traffic, allowing the controller to cleanly re-establish communication when it reconnects. The safe shutdown ensures the appliance does not continue operating without supervision.
Autonomous Operation
The communication timeout can be disabled via TIMEOUT_CONFIG
(enabled = false). When disabled, the appliance continues operating indefinitely
without controller supervision.
This is useful for scenarios where:
The controller configures the appliance and disconnects
Telemetry is in polling mode (
interval_ms= 0 in TELEMETRY_CONFIG)The appliance should maintain its current operating state
Byte Synchronization
All implementations MUST ignore bytes received on the serial line until a valid
START byte (0x7E) is observed.
Rationale: This ensures proper frame synchronization and prevents misinterpretation of noise, garbage bytes, or mid-packet data as valid packets.
Behavior:
Discard all bytes until START byte detected
After START byte, begin packet decoding
On decode error, reset to searching for START byte
Continue until valid packet received or error occurs
Packet Decoder
The packet decoder is a state machine that processes incoming bytes.
States
State |
Description |
|---|---|
WAIT_START |
Waiting for START delimiter ( |
READ_LENGTH |
Reading LENGTH byte |
READ_ADDRESS |
Reading 8-byte ADDRESS field |
READ_PAYLOAD |
Reading CBOR payload bytes (length from LENGTH field) |
READ_CRC |
Reading 2-byte CRC field |
WAIT_END |
Waiting for END delimiter ( |
State Transitions
WAIT_START
|
| [0x7E received]
v
READ_LENGTH
|
| [LENGTH byte received, LENGTH <= 114]
v
READ_ADDRESS
|
| [8 bytes received]
v
READ_PAYLOAD
|
| [LENGTH bytes received]
v
READ_CRC
|
| [2 bytes received]
v
WAIT_END
|
| [0x7F received] → Validate CRC → Decode CBOR → Process message
v
WAIT_START
Byte Unstuffing
Apply byte unstuffing during reception for all bytes between START and END. For escape sequences, see Byte Stuffing in Packet Format.
If
0x7Dreceived, read next byte and XOR with0x20Otherwise, use byte as-is
Error Conditions
START byte (0x7E) received mid-packet:
Abandon current packet immediately
Treat the new START byte as beginning of a new packet
Log error if applicable (previous packet was corrupted or incomplete)
LENGTH field exceeds maximum (>114):
Immediately reject the packet
Reset receive buffer
Discard all bytes until next START byte detected
Buffer overflow (>256 bytes without END):
Reset receive buffer immediately
Discard all bytes until next START byte detected
END byte (0x7F) received before expected:
Packet incomplete or corrupted
Reset receive buffer immediately
Discard all bytes until next START byte detected
CRC validation failure:
Discard packet silently
Reset to WAIT_START state
Do NOT send error response
Packet Encoder
The packet encoder builds a complete packet for transmission.
Encoding Steps
Encode the message as CBOR:
[type, payload_map]Build the unstuffed packet:
LENGTH (1 byte): CBOR payload length
ADDRESS (8 bytes): destination or source address
PAYLOAD (0-114 bytes): CBOR-encoded message
Calculate CRC-16-CCITT over the unstuffed packet
Append CRC (2 bytes, big-endian: MSB first, then LSB)
Apply byte stuffing to all bytes (LENGTH through CRC)
Add START delimiter (
0x7E) before stuffed dataAdd END delimiter (
0x7F) after stuffed data
Byte Stuffing
Apply byte stuffing to all bytes between START and END. For escape sequences, see Byte Stuffing in Packet Format.
CRC Calculation
Calculate CRC-16-CCITT over the unstuffed packet content. For algorithm parameters, see CRC Calculation in Packet Format.
Transmission Requirements
Appliance Transmission Rules
Appliances MUST NOT transmit data messages (0x30-0x35) or
PING_RESPONSE (0x3F) unless:
Responding to DISCOVERY_REQUEST with DEVICE_ANNOUNCE, OR
Responding to PING_REQUEST with PING_RESPONSE, OR
Telemetry broadcasting has been enabled via TELEMETRY_CONFIG, OR
Appliance is in E_STOP state (see Emergency Stop Behavior)
Rationale: This prevents boot synchronization errors by ensuring the controller is ready to receive data before the appliance begins broadcasting.
Telemetry Default State
Telemetry broadcasts are disabled by default on boot
Controller MUST explicitly enable telemetry with TELEMETRY_CONFIG
No data messages (except PING_RESPONSE and E_STOP broadcasts) are sent until telemetry is enabled
Controller Recovery
If the controller’s receive buffer becomes out of sync (repeated decode errors):
Send TELEMETRY_CONFIG with
enabled=0to stop incoming telemetryClear receive buffer and reset decoder state
Wait for silence (100ms with no bytes)
Send TELEMETRY_CONFIG with
enabled=1to resume telemetry
This is particularly useful during boot or after communication errors when packet boundaries are lost.
Emergency Stop Behavior
Emergency stop requires special handling for safety-critical shutdown. See STATE_COMMAND for command details.
Controller Behavior
When transmitting STATE_COMMAND with mode=EMERGENCY:
Retransmit the EMERGENCY command every 250ms
Continue retransmitting until STATE_DATA received with
state=E_STOPOnce confirmed, stop retransmitting
Controller sends: STATE_COMMAND (mode=EMERGENCY)
Controller waits: 250ms
Controller sends: STATE_COMMAND (mode=EMERGENCY)
...
Appliance enters: E_STOP state
Appliance begins: Broadcasting telemetry every 250ms
Controller receives: STATE_DATA (state=E_STOP)
Controller stops: Retransmitting EMERGENCY command
Appliance Behavior
When entering emergency stop state:
Suspend the communication timeout (timeout MUST NOT cause transition to IDLE)
Ignore ALL received commands (including PING_REQUEST and SEND_TELEMETRY)
Transmit all telemetry messages every 250ms:
STATE_DATA (with
state=E_STOPand appropriate error code)MOTOR_DATA (for each motor)
TEMPERATURE_DATA (for each sensor)
Continue broadcasting regardless of TELEMETRY_CONFIG state
Recovery: Emergency stop can ONLY be cleared by:
Power cycle (complete power loss and restoration)
Hardware reset (physical reset button or watchdog)
Software commands MUST NOT clear emergency stop state.
Broadcast Emergency Stop
Controllers can send STATE_COMMAND with
mode=EMERGENCY to the broadcast address to stop all appliances simultaneously.
Per broadcast rules, appliances process the command but do NOT respond.
Controller sends: STATE_COMMAND (mode=EMERGENCY, ADDRESS=broadcast)
Appliance 1: Receives, enters E_STOP, does NOT respond
Appliance 2: Receives, enters E_STOP, does NOT respond
Appliance 3: Receives, enters E_STOP, does NOT respond
Controller: Does NOT expect responses (broadcast command)
Because broadcast commands do not receive responses, the controller MUST verify each appliance has entered E_STOP by checking subsequent STATE_DATA messages. Each appliance in E_STOP broadcasts telemetry every 250ms regardless of whether the original command was addressed or broadcast.
Recommended Approach:
Send STATE_COMMAND (mode=EMERGENCY) to broadcast address
Continue retransmitting every 250ms
Monitor incoming STATE_DATA messages
Track which appliances have reported
state=E_STOPStop retransmitting only when ALL appliances are confirmed in E_STOP
Error Handling
Transmit Errors
Error |
Action |
|---|---|
Buffer full |
Drop oldest packet or block until space available |
UART error |
Log error, attempt retransmit once |
Receive Errors
Error |
Action |
|---|---|
CRC failure |
Discard packet silently, resync to next START byte |
Framing error |
Discard packet silently, resync to next START byte |
Inter-byte timeout |
Discard partial packet after 100ms silence |
Invalid command |
Send ERROR_INVALID_CMD with appropriate error code |
Recovery Strategies
Consecutive CRC failures:
On 3 consecutive CRC failures, check for baud rate mismatch
Verify physical layer configuration matches between devices
Persistent framing errors:
Check physical connection (cable, connectors)
Verify termination resistors (RS-485)
Check for electrical noise or interference
No response from appliance:
Retransmit important commands if no acknowledgment received
For critical commands (emergency stop), continue retransmitting until confirmed via STATE_DATA
Broadcast Retry Requirements
Controllers Only
If a controller enables broadcast mode on an appliance via TELEMETRY_CONFIG, the controller MUST retransmit the broadcast enable command every time a PING_RESPONSE is received from that appliance if the controller has NOT received a corresponding broadcast data message.
Rationale: This provides automatic recovery if:
Appliance was power-cycled (telemetry disabled on boot)
Appliance reset for any reason
Communication was interrupted and broadcasting stopped
Implementation:
Track which broadcasts have been enabled per appliance
Track which broadcast data has been received
On PING_RESPONSE, compare enabled vs received
If enabled but no data received, retransmit TELEMETRY_CONFIG
CBOR Encoding
Fusain payloads use CBOR (Concise Binary Object Representation) encoding.
The canonical schema is defined in fusain.cddl.
Message Structure
All messages are encoded as a CBOR array with two elements:
[type, payload]
type: Message type identifier (uint, e.g.,
0x30for STATE_DATA)payload: CBOR map with integer keys, or
nilfor empty payloads
Example STATE_DATA encoding:
[0x30, {0: false, 1: 0, 2: 1, 3: 12345}]
CBOR bytes: 82 18 30 A4 00 F4 01 00 02 01 03 19 30 39
zcbor Integration (Zephyr)
For Zephyr builds, use zcbor for CBOR encoding/decoding. Enable in Kconfig:
CONFIG_ZCBOR=y
CONFIG_ZCBOR_CANONICAL=y
Generate encode/decode functions from the CDDL schema:
zcbor_generate(
fusain_cbor
${CMAKE_CURRENT_SOURCE_DIR}/fusain.cddl
${CMAKE_CURRENT_BINARY_DIR}/generated
--decode --encode
)
Usage example:
#include "fusain_cbor_types.h"
#include "fusain_cbor_encode.h"
int fusain_encode_state_data(uint8_t *buf, size_t buf_size,
bool error, int code, int state, uint32_t ts)
{
ZCBOR_STATE_E(zs, 1, buf, buf_size, 1);
bool ok = zcbor_list_start_encode(zs, 2);
ok = ok && zcbor_uint32_put(zs, 0x30); // MSG_STATE_DATA
ok = ok && zcbor_map_start_encode(zs, 4);
ok = ok && zcbor_uint32_put(zs, 0) && zcbor_bool_put(zs, error);
ok = ok && zcbor_uint32_put(zs, 1) && zcbor_int32_put(zs, code);
ok = ok && zcbor_uint32_put(zs, 2) && zcbor_uint32_put(zs, state);
ok = ok && zcbor_uint32_put(zs, 3) && zcbor_uint32_put(zs, ts);
ok = ok && zcbor_map_end_encode(zs, 4);
ok = ok && zcbor_list_end_encode(zs, 2);
return ok ? (buf_size - zs->payload_end + zs->payload) : -1;
}
Zephyr Integration
For Zephyr RTOS builds, these subsystems are recommended for Fusain implementations.
CRC Implementation
Use Zephyr’s native CRC when built as a Zephyr module:
#ifdef CONFIG_FUSAIN
/* Zephyr module - use optimized CRC */
#include <zephyr/sys/crc.h>
#define fusain_crc16(data, len) crc16_itu_t(0xFFFF, data, len)
#else
/* Standalone build - use built-in CRC */
uint16_t fusain_crc16(const uint8_t *data, size_t len);
#endif
Note
CONFIG_FUSAIN is defined by Kconfig when the Fusain module is enabled
in a Zephyr build (CONFIG_FUSAIN=y in prj.conf). This is the standard
pattern for Zephyr modules to detect they are being built within Zephyr.
Buffer Management
Use net_buf for packet buffer management:
#include <zephyr/net/buf.h>
#define FUSAIN_MAX_PACKET_SIZE 256 // See packet-format.rst Wire Format Size
NET_BUF_POOL_DEFINE(fusain_pool, 8, FUSAIN_MAX_PACKET_SIZE, 0, NULL);
struct net_buf *buf = net_buf_alloc(&fusain_pool, K_NO_WAIT);
// ... use buffer ...
net_buf_unref(buf);
Benefits:
Pool-based allocation (no heap fragmentation)
Reference counting for safe buffer sharing
Consistent patterns across Zephyr subsystems
Logging
Register a logging module for debug output:
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(fusain, CONFIG_FUSAIN_LOG_LEVEL);
Add Kconfig for log level control:
module = FUSAIN
module-str = Fusain Protocol
source "subsys/logging/Kconfig.template.log_config"
Packet Reception Architecture
For platforms using UART polling (such as RP2350), use a ring buffer for byte accumulation combined with a message queue for thread-safe packet handoff:
#include <zephyr/sys/ring_buffer.h>
#include <zephyr/net/buf.h>
RING_BUF_DECLARE(uart_rx_ring, 256);
K_MSGQ_DEFINE(packet_queue, sizeof(struct net_buf *), 8, 4);
// UART polling thread
void uart_poll_thread(void)
{
uint8_t byte;
while (1) {
while (uart_poll_in(uart_dev, &byte) == 0) {
ring_buf_put(&uart_rx_ring, &byte, 1);
}
struct net_buf *buf = try_extract_packet(&uart_rx_ring);
if (buf) {
k_msgq_put(&packet_queue, &buf, K_NO_WAIT);
}
k_sleep(K_MSEC(1));
}
}
// Processing thread
void process_thread(void)
{
struct net_buf *buf;
while (1) {
if (k_msgq_get(&packet_queue, &buf, K_FOREVER) == 0) {
fusain_decode_from_buf(buf, &msg);
net_buf_unref(buf);
}
}
}
Framing Layer Types
The framing layer (outside CBOR) uses fixed-size types:
Field |
Encoding |
|---|---|
ADDRESS |
64-bit unsigned integer, little-endian |
CRC |
16-bit unsigned integer, big-endian |
Performance Characteristics
This section provides guidance on expected protocol performance.
Link Bandwidth Requirements
When selecting a physical layer for controller-to-appliance communication, the baud rate MUST be sufficient to receive state and all peripheral telemetry at least every 500ms, plus a reasonable margin determined by hardware selection.
500ms Minimum Interval
Controllers with user interfaces typically require telemetry updates at least every 500ms for responsive display. This applies to any physical layer (LIN, RS-485, plain UART), not just specific transports.
Calculating Required Bandwidth
Determine total telemetry size per interval (STATE_DATA + MOTOR_DATA per motor + TEMPERATURE_DATA per sensor + event-driven messages)
Add margin for command traffic and retries (~20%)
Select baud rate that delivers this within 500ms
Slower Links
Slower links (e.g., low baud rates, constrained wireless) MAY be used on the last hop to a client, but require polling mode to request only the specific state and telemetry needed for display. See SEND_TELEMETRY.
Throughput
Telemetry bandwidth depends on the number of peripherals and configured interval.
At 100ms Telemetry Interval:
Configuration |
Bytes per Interval |
Throughput |
|---|---|---|
1 motor, 1 temperature sensor |
~70 bytes |
~700 bytes/sec |
3 motors, 3 temperature sensors |
~200 bytes |
~2000 bytes/sec |
At 500ms Telemetry Interval:
Configuration |
Bytes per Interval |
Throughput |
|---|---|---|
1 motor, 1 temperature sensor |
~70 bytes |
~140 bytes/sec |
3 motors, 3 temperature sensors |
~200 bytes |
~400 bytes/sec |
Bandwidth Utilization:
At 115200 baud (effective ~11,520 bytes/sec) or 230400 baud (~23,040 bytes/sec), telemetry overhead is typically 1-17% of available bandwidth depending on interval and peripheral count.
Latency
Operation |
Latency |
|---|---|
Command processing |
< 5ms (typical) |
Telemetry delay (100ms interval) |
0-100ms |
Telemetry delay (500ms interval) |
0-500ms |
Multi-hop routing (per hop) |
50-200ms |
Reliability
CRC-16-CCITT: Detects all single-bit and double-bit errors
Byte stuffing: Prevents false START/END detection in data
Framing: Robust resynchronization on errors
Timeout mode: Automatic IDLE transition on communication loss