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