API reference#

pw_hdlc: Simple, robust, and efficient serial communication

The pw_hdlc API has 3 conceptual parts:

  • Encoder: Encode data as HDLC unnumbered information frames.

  • Decoder: Decode HDLC frames from a stream of data.

  • RPC: Use RPC over HDLC.

Encoder#

Single-Function Encoding#

Pigweed offers a single function which will encode an HDLC frame in each of C++, Python, and TypeScript:

Status pw::hdlc::WriteUIFrame(uint64_t address, ConstByteSpan payload, stream::Writer &writer)#

Writes an HDLC unnumbered information frame (UI frame) to the provided pw::stream writer.

This function is a convenience alias for the more general Encoder type and set of functions.

Parameters:
  • address – The frame address.

  • payload – The frame data to encode.

  • writer – The pw::stream to write the frame to. The frame contains the following bytes. See Design for more information.

    • HDLC flag byte (0x7e)

    • Address (variable length, up to 10 bytes)

    • UI-frame control (metadata) byte

    • Payload (0 or more bytes)

    • Frame check sequence (CRC-32, 4 bytes)

    • HDLC flag byte (0x7e)

Returns:

Code

Description

OK

The write finished successfully.

RESOURCE_EXHAUSTED

The write failed because the size of the frame would be larger than the writer’s conservative limit.

INVALID_ARGUMENT

The start of the write failed. Check for problems in your address argument’s value.

Example:

// Writes a span of data to a pw::stream::Writer and returns the status. This
// implementation uses the pw_checksum module to compute the CRC-32 frame check
// sequence.

#include "pw_hdlc/encoder.h"
#include "pw_hdlc/sys_io_stream.h"

int main() {
  pw::stream::SysIoWriter serial_writer;
  Status status = WriteUIFrame(123 /* address */, data, serial_writer);
  if (!status.ok()) {
    PW_LOG_INFO("Writing frame failed! %s", status.str());
  }
}

The encode module supports encoding HDLC frames.

pw_hdlc.encode.ui_frame(address: int, data: bytes) bytes

Encodes an HDLC UI-frame with a CRC-32 frame check sequence.

Example:

# Read bytes from serial and encode HDLC frames

import serial
from pw_hdlc import encode

ser = serial.Serial()
address = 123
ser.write(encode.ui_frame(address, b'your data here!'))

Encoder provides a way to build complete, escaped HDLC unnumbered information frames.

Encoder.uiFrame(address, data)
Arguments:
  • address (number()) – frame address.

  • data (Uint8Array()) – frame data.

Returns:

Uint8Array containing a complete HDLC frame.

Piecemeal Encoding#

Additionally, the C++ API provides an API for piecemeal encoding of an HDLC frame. This allows frames to be encoded gradually without ever holding an entire frame in memory at once.

class Encoder#

Encodes and writes HDLC frames.

Decoder#

class Decoder#

Subclassed by pw::hdlc::DecoderBuffer< kSizeBytes >

Public Functions

Result<Frame> Process(std::byte new_byte)#

Parses a single byte of an HDLC stream.

Returns:

A pw::Result with the complete frame if the byte completes a frame. The status can be one of the following:

Code

Description

OK

A frame was successfully decoded. The Result contains the Frame, which is invalidated by the next Process() call.

UNAVAILABLE

No frame is available.

RESOURCE_EXHAUSTED

A frame completed, but it was too large to fit in the decoder’s buffer.

DATA_LOSS

A frame completed, but it was invalid. The frame was incomplete or the frame check sequence verification failed.

template<typename F, typename ...Args>
inline void Process(ConstByteSpan data, F &&callback, Args&&... args)#

Processes a span of data and calls the provided callback with each frame or error.

Example:

// Read individual bytes from pw::sys_io and decode HDLC frames.

#include "pw_hdlc/decoder.h"
#include "pw_sys_io/sys_io.h"

int main() {
  std::byte data;
  while (true) {
    if (!pw::sys_io::ReadByte(&data).ok()) {
      // Log serial reading error
    }
    Result<Frame> decoded_frame = decoder.Process(data);

    if (decoded_frame.ok()) {
      // Handle the decoded frame
    }
  }
}
class pw_hdlc.decode.FrameDecoder

Decodes one or more HDLC frames from a stream of data.

__init__() None
process(data: bytes) Iterable[Frame]

Decodes and yields HDLC frames, including corrupt frames.

The ok() method on Frame indicates whether it is valid or represents a frame parsing error.

Yields:

Frames, which may be valid (frame.ok()) or corrupt (!frame.ok())

process_byte(byte: int) Frame | None

Processes a single byte and returns a frame if one was completed.

process_valid_frames(data: bytes) Iterable[Frame]

Decodes and yields valid HDLC frames, logging any errors.

Example:

# Decode data read from serial

import serial
from pw_hdlc import decode

ser = serial.Serial()
decoder = decode.FrameDecoder()

while True:
    for frame in decoder.process_valid_frames(ser.read()):
        # Handle the decoded frame

It is possible to decode HDLC frames from a stream using different protocols or unstructured data. This is not recommended, but may be necessary when introducing HDLC to an existing system.

The FrameAndNonFrameDecoder Python class supports working with raw data and HDLC frames in the same stream.

class pw_hdlc.decode.FrameAndNonFrameDecoder(
non_frame_data_handler: Callable[[bytes], Any],
*,
mtu: int | None = None,
timeout_s: float | None = None,
handle_shared_flags: bool = True,
)

Processes both HDLC frames and non-frame data in a stream.

__init__(
non_frame_data_handler: Callable[[bytes], Any],
*,
mtu: int | None = None,
timeout_s: float | None = None,
handle_shared_flags: bool = True,
) None

Yields valid HDLC frames and passes non-frame data to callback.

Parameters:
  • mtu – Maximum bytes to receive before flushing raw data. If a valid HDLC frame contains more than MTU bytes, the valid frame will be emitted, but part of the frame will be included in the raw data.

  • timeout_s – How long to wait before automatically flushing raw data. If a timeout occurs partway through a valid frame, the frame will be emitted, but part of the frame will be included in the raw data.

  • handle_shared_flags – Whether to permit HDLC frames to share a single flag byte between frames. If False, partial HDLC frames may be emitted as raw data when HDLC frames share a flag byte, but raw data won’t have to wait for a timeout or full MTU to be flushed.

flush_non_frame_data() None

Flushes any data in the buffer as non-frame data.

If a valid HDLC frame was flushed partway, the data for the first part of the frame will be included both in the raw data and in the frame.

process(data: bytes) Iterable[Frame]

Processes a stream of mixed HDLC and unstructured data.

Yields OK frames and calls non_frame_data_handler() with non-HDLC data.

Decoder unescapes received bytes and adds them to a buffer. Complete, valid HDLC frames are yielded as they are received.

Decoder.process(data)
Arguments:
  • data (Uint8Array()) – bytes to be decoded.

Yields:

HDLC frames, including corrupt frames. The Frame.ok() method whether the frame is valid.

processValidFrames(data)
Arguments:
  • data (Uint8Array()) – bytes to be decoded.

Yields:

Valid HDLC frames, logging any errors.

RPC#

RpcChannelOutput implements the pw::rpc::ChannelOutput interface of pw_rpc, simplifying the process of creating an RPC channel over HDLC. A pw::stream::Writer must be provided as the underlying transport implementation.

If your HDLC routing path has a Maximum Transmission Unit (MTU) limitation, use the FixedMtuChannelOutput to verify that the currently configured max RPC payload size (dictated by the static encode buffer of pw_rpc) will always fit safely within the limits of the fixed HDLC MTU after HDLC encoding.

The pw_hdlc Python package includes utilities to HDLC-encode and decode RPC packets, with examples of RPC client implementations in Python. It also provides abstractions for interfaces used to receive RPC Packets.

The pw_hdlc.rpc.CancellableReader and pw_hdlc.rpc.RpcClient classes and derived classes are context-managed to cleanly cancel the read process and stop the reader thread. The pw_hdlc.rpc.SocketReader and pw_hdlc.rpc.SerialReader also close the provided interface on context exit. It is recommended to use these in a context statement. For example:

import serial
from pw_hdlc import rpc
from pw_rpc import client_utils

if __name__ == '__main__':
    serial_device = serial.Serial('/dev/ttyACM0')
    with client_utils.SerialReader(serial_device) as reader:
        with rpc.HdlcRpcClient(
            reader,
            [],
            rpc.default_channels(serial_device.write)) as rpc_client:
            # Do something with rpc_client.

    # The serial_device object is closed, and reader thread stopped.
    return 0
class pw_hdlc.rpc.channel_output(writer: Callable[[bytes], Any], address: int = 82, delay_s: float = 0)

Returns a function that can be used as a channel output for pw_rpc.

class pw_hdlc.rpc.default_channels(write: Callable[[bytes], Any])

Default Channel with HDLC encoding.

class pw_hdlc.rpc.HdlcRpcClient(reader: ~pw_stream.stream_readers.CancellableReader, paths_or_modules: ~typing.Iterable[str | ~pathlib.Path | ~types.ModuleType] | ~pw_protobuf_compiler.python_protos.Library, channels: ~typing.Iterable[~pw_rpc.descriptors.Channel], output: ~typing.Callable[[bytes], ~typing.Any] = <function write_to_file>, client_impl: ~pw_rpc.client.ClientImpl | None = None, *, _incoming_packet_filter_for_testing: ~pw_rpc.descriptors.ChannelManipulator | None = None, rpc_frames_address: int = 82, log_frames_address: int = 1, extra_frame_handlers: dict[int, ~typing.Callable[[~pw_hdlc.decode.Frame], ~typing.Any]] | None = None)

An RPC client configured to run over HDLC.

Expects HDLC frames to have addresses that dictate how to parse the HDLC payloads.

__init__(reader: ~pw_stream.stream_readers.CancellableReader, paths_or_modules: ~typing.Iterable[str | ~pathlib.Path | ~types.ModuleType] | ~pw_protobuf_compiler.python_protos.Library, channels: ~typing.Iterable[~pw_rpc.descriptors.Channel], output: ~typing.Callable[[bytes], ~typing.Any] = <function write_to_file>, client_impl: ~pw_rpc.client.ClientImpl | None = None, *, _incoming_packet_filter_for_testing: ~pw_rpc.descriptors.ChannelManipulator | None = None, rpc_frames_address: int = 82, log_frames_address: int = 1, extra_frame_handlers: dict[int, ~typing.Callable[[~pw_hdlc.decode.Frame], ~typing.Any]] | None = None)

Creates an RPC client configured to communicate using HDLC.

Parameters:
  • reader – Readable object used to receive RPC packets.

  • paths_or_modules – paths to .proto files or proto modules.

  • channels – RPC channels to use for output.

  • output – where to write stdout output from the device.

  • client_impl – The RPC Client implementation. Defaults to the callback client implementation if not provided.

  • rpc_frames_address – the address used in the HDLC frames for RPC packets. This can be the channel ID, or any custom address.

  • log_frames_address – the address used in the HDLC frames for stdout output from the device.

  • extra_fram_handlers – Optional mapping of HDLC frame addresses to their callbacks.

class pw_hdlc.rpc.HdlcRpcLocalServerAndClient(
server_command: Sequence,
port: int,
protos: Iterable[str | Path | ModuleType] | Library,
*,
incoming_processor: ChannelManipulator | None = None,
outgoing_processor: ChannelManipulator | None = None,
)

Runs an RPC server in a subprocess and connects to it over a socket.

This can be used to run a local RPC server in an integration test.

__init__(
server_command: Sequence,
port: int,
protos: Iterable[str | Path | ModuleType] | Library,
*,
incoming_processor: ChannelManipulator | None = None,
outgoing_processor: ChannelManipulator | None = None,
) None

Creates a new HdlcRpcLocalServerAndClient.

The TypeScript library doesn’t have an RPC interface.

More pw_hdlc docs#

Get started & guides

How to set up and use pw_hdlc

API reference

Reference details about the pw_hdlc API

Design

Design details about pw_hdlc

Code size analysis

The code size impact of pw_hdlc

RPC over HDLC example

A step-by-step example of sending RPCs over HDLC

Experimental async router

An experimental asynchronous HDLC router using pw_channel