CallAndResponse.Transport.Serial 2.0.0-alpha.7

This is a prerelease version of CallAndResponse.Transport.Serial.
dotnet add package CallAndResponse.Transport.Serial --version 2.0.0-alpha.7
                    
NuGet\Install-Package CallAndResponse.Transport.Serial -Version 2.0.0-alpha.7
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="CallAndResponse.Transport.Serial" Version="2.0.0-alpha.7" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="CallAndResponse.Transport.Serial" Version="2.0.0-alpha.7" />
                    
Directory.Packages.props
<PackageReference Include="CallAndResponse.Transport.Serial" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add CallAndResponse.Transport.Serial --version 2.0.0-alpha.7
                    
#r "nuget: CallAndResponse.Transport.Serial, 2.0.0-alpha.7"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package CallAndResponse.Transport.Serial@2.0.0-alpha.7
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=CallAndResponse.Transport.Serial&version=2.0.0-alpha.7&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=CallAndResponse.Transport.Serial&version=2.0.0-alpha.7&prerelease
                    
Install as a Cake Tool

CallAndResponse

A .NET library for structured call-and-response communication over byte-oriented transports. Swap between serial, BLE, and anything else you can express as a pipe without touching your protocol code.

The library is pure framing and protocol logic. It never opens, closes, or manages transport connections — you provide an active IDuplexPipe from System.IO.Pipelines, and CallAndResponse handles message framing on top of it.

Getting Started

Prerequisites

  • .NET SDK 9.0.200 or later (required by the .slnx solution format; all projects target net8.0)

Install

dotnet add package CallAndResponse
dotnet add package CallAndResponse.Transport.Serial
dotnet add package CallAndResponse.Protocol.Modbus

Quick Example — Modbus over Serial

using CallAndResponse;
using CallAndResponse.Protocol.Modbus;
using CallAndResponse.Transport.Serial;
using RJCP.IO.Ports;

// You own the serial port lifecycle
using var port = new SerialPortStream("COM5", 115200, 8, Parity.None, StopBits.One);
port.Open();

// Wrap the open port in a duplex pipe
await using var pipe = new SerialDuplexPipe(port);

var transceiver = new Transceiver(pipe);

// Bind Modbus RTU framing: the inter-frame gap plus the CRC-16
var channel = ModbusRtu.Channel(transceiver, baudRate: 115200);

// Use it with a protocol client
var modbus = new ModbusRtuClient(channel);

var registers = await modbus.ReadHoldingRegisters(
    unitIdentifier: 1,
    startingAddress: 0x0000,
    numRegisters: 10,
    cancellationToken);

Quick Example — Custom Framing

Framing is a value you pass in, so the strategies compose.

// Receive the bytes between a header and a footer
var payload = await transceiver.SendReceive(
    new byte[] { 0x01, 0x02 },
    Frame.Between(header: new byte[] { 0xAA }, footer: new byte[] { 0x55 }),
    cancellationToken);

// Temporal framing for unsolicited data (barcode scanners, NMEA bursts)
var burst = await transceiver.Receive(
    Frame.UntilIdle(TimeSpan.FromMilliseconds(100)),
    cancellationToken);

// A length-prefixed reply, checked before it reaches you
var frame = await transceiver.Receive(
    Frame.LengthPrefixed(prefixOffset: 1, prefixSize: 2).Validated(MyChecksum),
    cancellationToken);

// Bound a stalled reply: fail after a 50ms gap rather than waiting out your token
var reply = await transceiver.Receive(
    Frame.Exactly(16).WithIdleTimeout(TimeSpan.FromMilliseconds(50)),
    cancellationToken);

Quick Example — SLIP or PPP-style framing

For a self-delimiting link, bind a codec once and then send and receive payloads. Delimiters, escapes, and the checksum stop being your problem.

IMessageTransceiver channel = transceiver.WithFraming(new SlipCodec());

var reply = await channel.SendReceiveMessage(request, cancellationToken);

HdlcCodec is the same for RFC 1662 asynchronous HDLC framing, including the FCS. It is the framing half of PPP and nothing above it — no LCP, no authentication, no NCPs.

Quick Example — STM32 Firmware Update

using CallAndResponse.Protocol.Stm32Bootloader;

var bootloader = new Stm32BootloaderClient(transceiver);

if (await bootloader.Ping(cancellationToken))
{
    var info = await bootloader.GetSupportedCommands(cancellationToken);
    var chipId = await bootloader.GetId(cancellationToken);

    // Read 1024 bytes of flash
    var flash = await bootloader.ReadMemory(
        Stm32BootloaderClient.Stm32BaseAddress, 1024, cancellationToken);
}

Packages

Package Description
CallAndResponse Core library — the channel contracts, Transceiver, the Frame catalogue, SLIP and HDLC codecs, exceptions
CallAndResponse.Transport.Serial SerialDuplexPipe over RJCP.SerialPortStream
CallAndResponse.Protocol.Modbus Modbus RTU client (FC03 read, FC16 write)
CallAndResponse.Protocol.Stm32Bootloader STM32 system bootloader commands (read/write/erase flash)

CallAndResponse.Transport.BleNordicUart ships in the repo but is not published to NuGet. Reference the project directly, or copy BleNordicUartPipe.cs.

Architecture

The library has three layers that only depend downward:

Protocol Layer       (Modbus, STM32 — depend only on the channel abstractions)
    ↓
Core Abstraction     (ITransceiver, IMessageTransceiver, Transceiver)
    ↓
Framing Layer        (Frame.* decoders, SlipCodec, HdlcCodec, ModbusRtu.Codec)
    ↓
Transport Layer      (SerialDuplexPipe, BleNordicUartPipe — implement IDuplexPipe)
  • ITransceiver is a byte channel: sends go out verbatim, and each receive is directed by the decoder you pass in. IMessageTransceiver is a message channel whose framing is fixed by the link, so you send and receive payloads. WithFraming and AsByteStream move between them.
  • Framing is a value, not a method per strategy. Frame.Exactly(4), Frame.UntilIdle(gap), and new SlipCodec() are all things you pass, and the combinators compose them.
  • Protocol clients accept whichever channel their protocol actually needs. They never reference a transport package.
  • Transport packages each provide a single IDuplexPipe. The caller owns the underlying connection and its lifecycle.

See docs/ARCHITECTURE.md for the full architecture document.

Adding a Transport

A transport package is only worth writing when the adaptation is non-trivial — a background pump, a framing quirk, a vendor SDK that is not stream-shaped. Most transports need no package at all.

// Any Stream — serial, TCP, named pipe. No package required.
var transceiver = new Transceiver(
    PipeReader.Create(stream),
    PipeWriter.Create(stream));

// Any IDuplexPipe, via the AsTransceiver() extension
ITransceiver transceiver = myDuplexPipe.AsTransceiver();

// Event-based transports (BLE notifications, etc.) need a pipe you drive
var pipe = new BleNordicUartPipe();
device.DataReceived += async (s, e) => await pipe.RxWriter.WriteAsync(e.Data);
// ...and a loop draining pipe.TxReader out to the device
var transceiver = new Transceiver(pipe);

See Examples/Example.Transport.Serial/ and Examples/Example.Transport.Ble/ for complete working examples.

Adding a Protocol

Take ITransceiver when the protocol decides its own frame boundaries:

public class MyProtocolClient
{
    private readonly ITransceiver _transceiver;

    public MyProtocolClient(ITransceiver transceiver)
        => _transceiver = transceiver;

    public async Task<byte[]> ReadDeviceId(CancellationToken token)
    {
        var response = await _transceiver.SendReceive(
            new byte[] { 0x01 },
            Frame.Exactly(4),
            token);

        return response.ToArray();
    }
}

Take IMessageTransceiver when the link is self-delimiting. Such a client runs over SLIP, over HDLC, or over a terminator codec without modification, because it never states a byte boundary:

public class MyMessageClient(IMessageTransceiver channel)
{
    public async Task<Reply> Ask(Request request, CancellationToken token) =>
        Parse(await channel.SendReceiveMessage(Build(request), token));
}

If your protocol has framing of its own — a checksum, a delimiter, an inter-frame gap — put it in an IFrameCodec and hand out a channel type built from it, the way ModbusRtu.Channel does. A client that accepts any IMessageTransceiver while assuming its own framing will accept the wrong one silently.

Project Structure

CallAndResponse/
├── Source/
│   ├── CallAndResponse/                          Core library
│   │   ├── ITransceiver.cs                       Protocol-facing contract
│   │   ├── Transceiver.cs                        Pipe-backed implementation
│   │   ├── IMessageTransceiver.cs                Message-channel contract
│   │   ├── MessageTransceiver.cs                 Binds a codec to a link
│   │   ├── ByteStreamAdapter.cs                  Message channel as a byte channel
│   │   ├── TransceiverExtensions.cs              SendReceive, WithFraming, AsByteStream
│   │   ├── DuplexPipeExtensions.cs               AsTransceiver() extension
│   │   ├── Framing/                              Decoders, codecs, and combinators
│   │   └── TransceiverTransportException.cs      I/O-level exception
│   │
│   ├── CallAndResponse.Transport.Serial/         SerialDuplexPipe (RJCP)
│   ├── CallAndResponse.Transport.BleNordicUart/  BleNordicUartPipe (unpublished)
│   ├── CallAndResponse.Protocol.Modbus/          Modbus RTU protocol
│   └── CallAndResponse.Protocol.Stm32Bootloader/ STM32 bootloader protocol
│
├── Examples/
│   ├── Example.Transport.Serial/                 Serial + Modbus
│   └── Example.Transport.Ble/                    BLE Nordic UART
│
├── Test/
│   └── CallAndResponse.Test.Unit/                Unit tests (xUnit)
│
├── docs/
│   ├── ARCHITECTURE.md
│   └── adr/                                      Architecture decision records
│
└── CallAndResponse.slnx

Building

dotnet build CallAndResponse.slnx
dotnet test CallAndResponse.slnx

License

MIT © Charles Lee

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on CallAndResponse.Transport.Serial:

Package Downloads
Datafeel.NET.Serial

Package Description

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2.0.0-alpha.7 39 9/4/2026
2.0.0-alpha.6 181 8/28/2026
1.6.1-alpha 245 9/8/2025
1.6.0-alpha 164 9/5/2025
1.5.0-alpha 304 3/7/2025
1.4.0-alpha 461 2/6/2025
1.3.1-alpha 238 2/5/2025
1.3.0-alpha 188 2/5/2025
1.2.3-alpha 233 1/19/2025
1.2.1-alpha 250 1/18/2025
1.2.0-alpha 233 1/16/2025
1.1.1 222 1/7/2025
1.1.0 343 1/2/2025
1.0.7 205 12/28/2024