Skip to content

Protocol Layer

The Protocol layer is your code that communicates with physical devices using their native language.

For overall communication architecture, see Communication Overview first.


What is Protocol Layer?

graph TB
    subgraph Framework["Adapter Framework"]
        Things["Things, Functions"]
        API["Framework API"]
    end

    subgraph Protocol["Protocol Layer (YOUR CODE)"]
        Translate["Translate messages"]
        Manage["Manage connections"]
        Quirks["Handle device quirks"]
    end

    subgraph Device["Physical Device"]
        HW["Hardware"]
    end

    Framework <-->|"Framework types"| Protocol
    Protocol <-->|"Device protocol"| Device

Protocol layer is:

  • Bridge between framework and device
  • Translator of message formats
  • Your responsibility - you write this code
  • Device-specific - different for each device type

Protocol Layer Responsibilities

graph LR
    subgraph Connection["1. Connection Management"]
        C1["Connect"]
        C2["Disconnect"]
        C3["Reconnect"]
        C4["Health check"]
    end

    subgraph Command["2. Command Execution"]
        CM1["Receive command"]
        CM2["Translate to device"]
        CM3["Send + wait ACK"]
        CM4["Return result"]
    end

    subgraph Event["3. Event Reception"]
        E1["Listen for events"]
        E2["Receive from device"]
        E3["Translate to framework"]
        E4["Dispatch to router"]
    end

    C1 --> C2 --> C3 --> C4
    CM1 --> CM2 --> CM3 --> CM4
    E1 --> E2 --> E3 --> E4

Protocol is NOT

The protocol layer is NOT:

  • Part of the framework (you write it)
  • Standardized (each device is different)
  • Optional (you must implement it)

The framework provides:

  • Message types (EventMessage, CommandMessage)
  • Channels (how messages flow)
  • Base classes (Thing, Function)

You provide:

  • How to connect to device
  • How to translate commands
  • How to receive events

Common Protocol Types

gRPC

Adapter <---[gRPC channel]---> Device Gateway
          (protobuf messages)

+ Efficient binary protocol
+ Streaming support
+ Strong typing
- Requires gRPC SDK

REST/HTTP

Adapter ---[HTTP request]---> Device API
        <--[JSON response]---

+ Universal, simple
+ Easy debugging
- No streaming (must poll)
- Higher latency

TCP/IP Socket

Adapter <---[TCP stream]---> Device
          (custom protocol)

+ Real-time
+ Full control
+ Framework provides TCP transport (PipeReader, backpressure, zero-copy)
- Must implement frame parsing (IFrameParser)

For TCP devices, use transport: { type: tcp } in adapter.yaml. The framework handles sockets, buffering, and reconnection. See TCP Transport for details.

RS-485 / Serial

Adapter <---[serial port]---> Device
          (binary frames)

+ Direct hardware access
- Complex protocol handling
- Single device per port

Protocol Layer Structure

YourAdapter/
├── Communication/
│   ├── Protocol.cs          # main protocol class
│   ├── DeviceClient.cs      # device SDK wrapper
│   ├── EventStream.cs       # event subscription
│   └── MessageTranslator.cs # format conversion
└── ...

Implementing Protocol

public class Protocol : IAsyncDisposable
{
    private readonly ILogger<Protocol> _logger;
    private DeviceClient? _client;
    private EventStream? _eventStream;

    public Protocol(ILogger<Protocol> logger)
    {
        _logger = logger;
    }

    // --- CONNECTION ---

    public async Task ConnectAsync(string host, int port, CancellationToken ct)
    {
        _client = new DeviceClient(host, port);
        await _client.ConnectAsync(ct);

        // start receiving events
        _eventStream = new EventStream(_client);
        await _eventStream.StartAsync(ct);

        _logger.LogInformation("Connected to device at {Host}:{Port}", host, port);
    }

    public async Task DisconnectAsync()
    {
        if (_eventStream != null)
        {
            await _eventStream.StopAsync();
        }

        _client?.Disconnect();
        _logger.LogInformation("Disconnected from device");
    }

    // --- COMMANDS ---

    public async Task<bool> UnlockDoorAsync(uint doorId, int duration, CancellationToken ct)
    {
        // translate to device format
        var deviceCommand = new DeviceUnlockCommand
        {
            DoorNumber = doorId,
            DurationMs = duration * 1000
        };

        // send to device
        var response = await _client!.SendCommandAsync(deviceCommand, ct);

        // translate response
        return response.ResultCode == 0;
    }

    public async Task<bool> LockDoorAsync(uint doorId, CancellationToken ct)
    {
        var deviceCommand = new DeviceLockCommand { DoorNumber = doorId };
        var response = await _client!.SendCommandAsync(deviceCommand, ct);
        return response.ResultCode == 0;
    }

    // --- EVENTS ---

    public void OnEventReceived(Action<DeviceEvent> handler)
    {
        _eventStream!.EventReceived += handler;
    }

    // --- CLEANUP ---

    public async ValueTask DisposeAsync()
    {
        await DisconnectAsync();
    }
}

Event Flow Through Protocol

sequenceDiagram
    participant Device
    participant Protocol
    participant EventRouter
    participant Thing
    participant NATS

    Device->>Protocol: Device event (wire format)
    Protocol->>Protocol: Translate to domain event
    Protocol->>EventRouter: Dispatch
    EventRouter->>Thing: Matched handler
    Thing->>NATS: Publish PQ event

Command Flow Through Protocol

sequenceDiagram
    participant NATS
    participant Framework
    participant Handler
    participant Protocol
    participant Device

    NATS->>Framework: Command message
    Framework->>Handler: Dispatch to handler
    Handler->>Protocol: Call protocol method
    Protocol->>Protocol: Build device packet
    Protocol->>Device: Send command
    Device-->>Protocol: ACK/Response
    Protocol-->>Handler: Result
    Handler-->>Framework: DeviceCommandResult
    Framework-->>NATS: Response message

Connection Management

Protocol must handle connection lifecycle:

stateDiagram-v2
    [*] --> Connecting: Start

    Connecting --> Online: Success
    Connecting --> Retry: Failure

    Online --> Reconnect: Connection lost
    Retry --> Connecting: After backoff

    Reconnect --> Connecting: Immediate
public class Protocol
{
    private async Task MaintainConnection(CancellationToken ct)
    {
        while (!ct.IsCancellationRequested)
        {
            try
            {
                await ConnectAsync(ct);

                // stay connected until failure
                await WaitForDisconnect(ct);
            }
            catch (Exception ex)
            {
                _logger.LogWarning(ex, "Connection lost, reconnecting...");

                // exponential backoff
                await Task.Delay(GetBackoffDelay(), ct);
            }
        }
    }
}

Error Handling in Protocol

public async Task<bool> UnlockDoorAsync(uint doorId, CancellationToken ct)
{
    try
    {
        var response = await _client!.SendCommandAsync(
            new UnlockCommand { DoorId = doorId },
            ct);

        return response.Success;
    }
    catch (DeviceOfflineException)
    {
        _logger.LogWarning("Device offline, cannot unlock door {DoorId}", doorId);
        throw;  // let framework handle
    }
    catch (TimeoutException)
    {
        _logger.LogWarning("Timeout unlocking door {DoorId}", doorId);
        throw;
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "Unexpected error unlocking door {DoorId}", doorId);
        throw;
    }
}

Protocol Independence

Keep protocol separate from business logic:

// GOOD - protocol just communicates
public class Protocol
{
    public Task<bool> UnlockDoorAsync(uint doorId, int duration, CancellationToken ct);
    public Task<bool> LockDoorAsync(uint doorId, CancellationToken ct);
    public Task<DoorState> GetDoorStateAsync(uint doorId, CancellationToken ct);
}

// command handler uses protocol
public class UnlockHandler
{
    public async Task<ResponseMessage> Handle(CommandMessage cmd, CancellationToken ct)
    {
        var doorId = cmd.Data.GetProperty("doorId").GetUInt32();
        var duration = cmd.Data.GetProperty("duration").GetInt32();

        var success = await _protocol.UnlockDoorAsync(doorId, duration, ct);

        return new ResponseMessage(cmd.CorrelationId, new { success });
    }
}

Common Mistakes

1. Protocol Knows Too Much

// WRONG - protocol knows about PQ concepts
public class Protocol
{
    public async Task HandleAccessEvent(PersonAccess person)  // framework type!
    {
        ...
    }
}

// RIGHT - protocol uses device types
public class Protocol
{
    public async Task<DeviceAccessLog[]> GetAccessLogsAsync(CancellationToken ct)
    {
        ...
    }
}
// translation happens in adapter layer

2. No Reconnection Logic

// WRONG - single connection attempt
await _client.ConnectAsync();
// if this fails or drops, adapter is dead

// RIGHT - reconnection loop
while (!ct.IsCancellationRequested)
{
    try { await ConnectAndMaintain(ct); }
    catch { await Task.Delay(backoff, ct); }
}

3. Blocking Calls

// WRONG - blocks thread
var result = _client.SendCommand(cmd);  // sync!

// RIGHT - async
var result = await _client.SendCommandAsync(cmd, ct);

Next: Channels - Message pipelines