TCP Transport¶
Start here: For overall communication architecture, see Communication Overview.
The framework provides a ready-made TCP transport for adapters communicating with devices over TCP/IP. You provide framing, response matching, and protocol logic. The framework handles sockets, buffering, backpressure, disconnect propagation, and the serialized request-response channel.
Architecture¶
+---------------------------------------------------------------------+
| Framework TCP Transport |
+---------------------------------------------------------------------+
| |
| Transport (singleton, per adapter) |
| +------------------------------------------------------------------+|
| | TcpTransportBase<TAddress, TFrame, TEvent> ||
| | ConnectChannel(address, host, port, router, ct, ...) ||
| | Disconnect(address) ||
| | GetChannel(address) ||
| +------------------------------------------------------------------+|
| | |
| | one per device |
| v |
| +------------------------------------------------------------------+|
| | ProtocolChannel<TFrame> where TFrame : FrameBase ||
| | ||
| | Send(request, options, ct) ||
| | SendBulk(...) ||
| | OnUnmatchedFrame / OnDisconnected ||
| +---------------------------+--------------------------------------+|
| | |
| v |
| +------------------------------------------------------------------+|
| | TcpConnection<TFrame> ||
| | ||
| | Socket ──> PipeReader ──> IFrameParser ──> Channel<TFrame> ||
| | ||
| | Write(bytes, ct) = raw socket write ||
| | Frames = parsed incoming frames ||
| +------------------------------------------------------------------+|
| |
+---------------------------------------------------------------------+
Key properties:
- ProtocolChannel owns request-response —
Send()serializes a frame, waits for matching response - Matching via FrameBase — your frame type defines how requests pair with responses
- Unmatched frames dispatched — unsolicited device events go to
OnUnmatchedFrame/TransformToEvents() - Zero-copy reads via System.IO.Pipelines (pooled buffers)
- Backpressure — if adapter can't process frames fast enough, TCP window closes
- Thread-safe writes — transport serializes writes internally
- Automatic disconnect signaling — framework handles reconnection via ConnectionStateTracker
- Channel poison on terminal timeout/cancel/disconnect — caller must reconnect after session loss
What You Provide vs What Framework Provides¶
| Concern | Framework | Adapter |
|---|---|---|
| TCP socket lifecycle | Yes | - |
| Read loop + buffering | Yes (PipeReader) | - |
| Frame parsing (bytes → frame) | - | Yes (IFrameParser) |
| Frame serialization (frame → bytes) | - | Yes (FrameBase.Serialize()) |
| Request-response matching | Yes (ProtocolChannel) | Yes (FrameBase.MatchResponse() / IsResponseTo()) |
| Request-response dispatch | Yes (ProtocolChannel.Send()) |
- |
| Unmatched frame routing | Yes | Yes (handler logic) |
| Write serialization | Yes (SemaphoreSlim) | - |
| Disconnect detection + reconnect | Yes | - |
| Keepalive heartbeat | Yes (idle-based) | Yes (frame factory) |
| Frame interpretation | - | Yes (Protocol) |
| Command sequencing | - | Yes (Protocol via CommandQueue) |
| Domain event conversion | - | Yes (Protocol) |
Core Concept: FrameBase¶
All TCP frame types must inherit FrameBase. This abstract class has three responsibilities:
- Serialize — convert a request frame to bytes for transmission
- ExpectsResponse — declare whether a request is request-response or fire-and-forget
- MatchResponse / IsResponseTo — determine if an incoming frame is the response to a given request, including transient busy responses
public abstract class FrameBase
{
public virtual bool ExpectsResponse => true;
/// <summary>
/// Serializes this frame to bytes for transmission.
/// </summary>
public abstract byte[] Serialize();
public virtual MatchResult MatchResponse(FrameBase request)
=> IsResponseTo(request) ? MatchResult.Match : MatchResult.NoMatch;
/// <summary>
/// Checks if this frame is a response to the given request frame.
/// </summary>
public abstract bool IsResponseTo(FrameBase request);
}
How matching works¶
When you call channel.Send(request):
- Framework registers a pending entry (before writing, to avoid race conditions)
- Calls
request.Serialize()→ sends bytes over TCP TcpConnectionread loop parses incoming framesProtocolChannelmatches each frame against the active pending request viaMatchResponse()- First match → completes the
Send()caller with that frame - No match → forwards to unmatched-frame handlers
If MatchResponse() returns MatchResult.Busy, the channel treats the frame as a matched response and applies the configured RetryPolicy without using exceptions for normal busy control flow.
Send(requestFrame) ProtocolChannel / TcpConnection
│ │
├─ register pending ├─ read frame from parser
├─ serialize + write bytes ├─ frame.MatchResponse(pending)?
└─ await response │ YES → complete Send() caller
│ NO → OnUnmatchedFrame(frame)
Matching strategies¶
Sequential protocol (one command at a time, e.g. Acme):
// All data packets share ctrl=0x09 — safe because CommandQueue serializes
public override bool IsResponseTo(FrameBase request) =>
request is MyFrame req && req.Control == Control;
Type-based protocol (e.g. Acme — ACK for any command, events are separate):
public override bool IsResponseTo(FrameBase request) =>
request is AcmeFrame req
&& req.Type != MessageType.EventNotification // request is a command
&& Type != MessageType.EventNotification; // response is not an event
Correlation-ID protocol (future — tagged requests):
public override bool IsResponseTo(FrameBase request) =>
request is TaggedFrame req && req.Tag == Tag;
Busy-aware protocol (device returns explicit busy response code):
public override MatchResult MatchResponse(FrameBase request)
{
if (!IsResponseTo(request))
return MatchResult.NoMatch;
return ResponseCode == 0xDD
? MatchResult.Busy
: MatchResult.Match;
}
Step-by-Step: Creating a TCP Adapter¶
1. Define transport in adapter.yaml¶
transport:
type: tcp
protocol:
address_type: byte
frame_type: "MyAdapter.Communication.MyFrame"
event_type: "MyAdapter.Communication.MyEvent"
2. Define your frame type (inheriting FrameBase)¶
Your frame type has two roles:
- Parsed incoming frame — created by
IFrameParser,RawPacketis null - Outgoing request frame — created by
CreateSend()factory,RawPacketholds pre-built bytes
using Pq.Adapters.Framework.Communication.Tcp;
namespace MyAdapter.Communication;
/// <summary>
/// Parsed protocol frame. Inherits FrameBase for request-response matching.
/// </summary>
public sealed class MyFrame : FrameBase
{
/// <summary>
/// Gets the message type code.
/// </summary>
public required byte Type { get; init; }
/// <summary>
/// Gets the payload data.
/// </summary>
public required byte[] Payload { get; init; }
/// <summary>
/// Gets pre-built packet bytes for outgoing requests. Null for parsed incoming frames.
/// </summary>
internal byte[]? RawPacket { get; init; }
/// <inheritdoc/>
public override byte[] Serialize() =>
RawPacket ?? throw new InvalidOperationException("Frame was not created as a request");
/// <inheritdoc/>
public override bool IsResponseTo(FrameBase request) =>
request is MyFrame req && req.Type == Type;
/// <summary>
/// Creates a request frame for sending.
/// </summary>
internal static MyFrame CreateSend(byte type, byte[] rawPacket) => new()
{
Type = type,
Payload = [],
RawPacket = rawPacket,
};
}
Key pattern: The parser creates frames from incoming bytes (RawPacket stays null). The protocol creates request frames via CreateSend() with pre-built packet bytes. The same class serves both roles.
3. Implement IFrameParser¶
Tells the framework where one frame ends and the next begins in the TCP byte stream:
using System.Buffers;
using Pq.Adapters.Framework.Communication.Tcp;
namespace MyAdapter.Communication;
/// <summary>
/// Parses protocol frames from the TCP byte stream.
/// </summary>
public sealed class MyFrameParser : IFrameParser<MyFrame>
{
/// <inheritdoc/>
public bool TryParse(ref ReadOnlySequence<byte> buffer, out MyFrame frame)
{
frame = null!;
// not enough data for a header?
if (buffer.Length < 4)
return false;
// read length from first 2 bytes (example: big-endian uint16)
var reader = new SequenceReader<byte>(buffer);
reader.TryReadBigEndian(out short lengthShort);
var totalLength = (ushort)lengthShort;
// not enough data for full frame?
if (buffer.Length < totalLength)
return false;
// parse the frame from buffer
reader.TryRead(out var type);
var payloadLength = totalLength - 3;
var payload = new byte[payloadLength];
for (var i = 0; i < payloadLength; i++)
reader.TryRead(out payload[i]);
frame = new MyFrame
{
Type = type,
Payload = payload,
};
buffer = buffer.Slice(totalLength); // advance past consumed bytes
return true;
}
}
Rules:
- Return
falsewhen buffer has insufficient data (framework reads more bytes automatically) - On success, advance
bufferpast consumed bytes viabuffer.Slice() - Never throw — return false for malformed data and advance past bad bytes
- The source generator auto-discovers and registers this class in DI
4. Write Protocol (using ProtocolChannel for request-response)¶
The generated Protocol extends TcpProtocolBase<MyEvent, MyFrame> when frame_type is declared in adapter-registration.yaml.
You write the partial class that uses the attached channel:
using Microsoft.Extensions.Logging;
using Pq.Adapters.Framework.Communication.Protocol;
using Pq.Adapters.Framework.Communication.Tcp;
using Pq.Adapters.Framework.Events;
namespace MyAdapter.Communication;
/// <summary>
/// Device protocol implementation.
/// </summary>
public partial class Protocol
{
/// <summary>
/// Connects to a device via the framework TCP transport.
/// </summary>
internal async Task<bool> Connect(
MyDevice device,
IEventRouter<MyEvent> router,
CancellationToken ct)
{
var connected = false;
// 1. Establish TCP channel (framework manages TCP + request-response orchestration)
var channel = await Transport.ConnectChannel(
device.Address,
device.IpAddress,
device.Port,
router,
ct);
AttachChannel(channel, router);
try
{
// 2. Protocol handshake using Send()
var helloPacket = BuildPacket(0x01, []);
var request = MyFrame.CreateSend(0x01, helloPacket);
var response = await Send(request, TimeSpan.FromSeconds(5), ct);
if (response.Payload[0] != 0x00)
{
Logger.LogWarning("Handshake rejected");
return false;
}
connected = true;
Logger.LogInformation("Connected and authenticated");
return true;
}
finally
{
if (!connected)
await Transport.Disconnect(device.Address);
}
}
/// <summary>
/// Disconnects from the device.
/// </summary>
internal async Task Disconnect(byte address)
{
DetachChannel();
await Transport.Disconnect(address);
}
/// <summary>
/// Sends a command and waits for the matching response.
/// </summary>
internal async Task<byte[]?> SendCommand(byte type, byte[] payload, CancellationToken ct)
{
if (Channel == null)
return null;
var packet = BuildPacket(type, payload);
var request = MyFrame.CreateSend(type, packet);
try
{
var response = await Send(request, TimeSpan.FromSeconds(2), ct);
return response.Payload;
}
catch (OperationCanceledException)
{
Logger.LogWarning("Command timeout for type {Type}", type);
return null;
}
}
/// <summary>
/// Handles frames that don't match any pending Send() — device events.
/// </summary>
protected override IEnumerable<MyEvent> TransformToEvents(MyFrame frame)
{
Logger.LogDebug("Unsolicited frame: type={Type}", frame.Type);
return [];
}
/// <summary>
/// Builds a protocol packet from type and payload.
/// </summary>
private byte[] BuildPacket(byte type, byte[] payload)
{
// your protocol's packet framing logic
var packet = new byte[3 + payload.Length];
var length = (ushort)packet.Length;
packet[0] = (byte)(length >> 8);
packet[1] = (byte)(length & 0xFF);
packet[2] = type;
Array.Copy(payload, 0, packet, 3, payload.Length);
return packet;
}
}
5. Implement Device Thing¶
using Pq.Adapters.Framework.Communication;
namespace MyAdapter;
/// <summary>
/// Device thing implementing framework connection interface.
/// </summary>
public partial class MyDevice : IDeviceConnection
{
/// <inheritdoc/>
public async Task<bool> Connect(ConnectionContext context, CancellationToken ct) =>
await Connection.Protocol.Connect(this, Connection, ct);
/// <inheritdoc/>
public async Task Disconnect(CancellationToken ct) =>
await Connection.Protocol.Disconnect(Address);
}
Connection Flow Summary¶
1. Framework calls IDeviceConnection.Connect()
│
2. Protocol calls Transport.ConnectChannel(address, host, port, router, ct)
│ → creates TcpConnection and ProtocolChannel
│ → binds event router, wires OnDisconnected for retry
│
3. Protocol calls AttachChannel(channel, router)
│ → unmatched frames can flow through TransformToEvents / OnUnmatchedFrame
│
4. Protocol does handshake using Send(frame, timeout, ct)
│ → framework serializes, sends, waits for matching response
│
5. Connection is live — adapter sends commands via Send()
│ → unsolicited events flow through TransformToEvents / OnUnmatchedFrame
│
6. On disconnect:
│ → read loop detects EOF/error
│ → OnDisconnected fires → framework retry via ConnectionStateTracker
│ → IDeviceConnection.Disconnect() called, then Connect() again
Keepalive¶
The framework no longer injects transport-level keepalive packets automatically. If your protocol requires keepalive, implement it explicitly in the protocol layer as a normal command or no-reply send. This keeps heartbeat semantics adapter-specific and avoids hidden traffic during reconnect and timeout scenarios.
API Reference¶
FrameBase (abstract, your frame inherits this)¶
| Member | Description |
|---|---|
Serialize() |
Convert frame to bytes for transmission |
ExpectsResponse |
Declare whether the request expects a reply |
MatchResponse(FrameBase request) |
Rich matching result (NoMatch, Match, Busy, Continue) |
IsResponseTo(FrameBase request) |
Backward-compatible boolean matching |
TcpTransportBase¶
Singleton per adapter. Generated Transport class inherits this.
| Method | Description |
|---|---|
ConnectChannel(address, host, port, router, ct, tcpOptions?, channelOptions?) |
Creates channel, binds router, returns ProtocolChannel<TFrame> |
Disconnect(address) |
Disposes connection, unbinds router |
GetChannel(address) |
Returns active channel or null |
ProtocolChannel where TFrame : FrameBase¶
One per device. Created by Transport.ConnectChannel().
| Member | Description |
|---|---|
Send(request, options?, ct) |
Request-response or no-reply send |
SendBulk(...) |
Sequential bulk send with stop-on-first-error semantics |
ExclusiveSession(operation, ct) |
Caller-driven exclusive lease for multi-packet logical operations |
OnUnmatchedFrame |
Hook for unsolicited frames not consumed by matching |
OnDisconnected |
Fires when the channel becomes terminally unusable |
TcpConnection |
Access to raw TCP transport when a protocol truly needs it |
IsConnected |
Whether the channel is still usable |
TcpConnection where TFrame : FrameBase¶
Raw framed transport under ProtocolChannel.
| Member | Description |
|---|---|
Write(data, ct) |
Fire-and-forget: send raw bytes (thread-safe) |
Frames |
ChannelReader<TFrame> with parsed incoming frames |
IsConnected |
Whether the connection is active |
OnDisconnected |
Callback (set by Transport, triggers framework retry) |
IFrameParser where TFrame : FrameBase¶
Adapter implements this. Auto-discovered and registered by source generator.
| Method | Description |
|---|---|
TryParse(ref buffer, out frame) |
Parse one frame from ReadOnlySequence<byte>, advance buffer on success |
TcpConnectionOptions¶
Optional configuration passed to Transport.ConnectChannel() as tcpOptions.
| Property | Default | Description |
|---|---|---|
FrameQueueCapacity |
256 | Max buffered frames before backpressure |
PipeReaderOptions |
null | PipeReader configuration (buffer sizes, pool) |
ConnectTimeout |
10s | TCP connect timeout |
ProtocolChannelOptions¶
Optional configuration passed to Transport.ConnectChannel() as channelOptions.
| Property | Default | Description |
|---|---|---|
DefaultTimeout |
5s | Default response timeout when no per-send timeout is provided |
MaxQueueSize |
256 | Buffered normal-priority requests |
QueueFullMode |
Wait |
Behavior when the normal queue is full |
HighPriorityBurstLimit |
10 | Fairness limit before one normal item is forced |
FollowUpQuietTime |
Zero |
Mandatory gap after a no-reply follow-up/acknowledge frame before the next request is dispatched |
For multi-step mandatory exchanges (e.g. a history/log drain that must run to completion on the wire), use the frame-typed subtype ProtocolChannelOptions<TFrame>, which adds a SequencePolicies list. See Sequence Policies below and compliance rule COMM-007.
ProtocolChannelOptions<TFrame> — Sequence Policies¶
Some packet types carry a protocol obligation: their response must be acknowledged and the request repeated until the device signals the end (GetLog → AckLog → GetLog → … → NoData). That obligation is a property of the protocol, not the call site, so it is declared once per channel instead of looped per call:
channelOptions: new ProtocolChannelOptions<XFrame>
{
FollowUpQuietTime = interFrameGap,
SequencePolicies = [GetLogSequence.Create(BuildFrame)],
}
A SequencePolicy<TFrame> is (request, response) → next step or null, consulted on the dispatch loop after every matched response. It must be pure decision logic — no blocking, no await, no I/O, no throwing. Returning an action makes the channel write the no-reply Acknowledge, honor FollowUpQuietTime, then send Continue; a null Continue (or a null action) ends the sequence.
While a sequence runs it occupies a single dispatch-queue slot — nothing interleaves between an ACK and the next request. Externally it is a plain Send that returns the final response; intermediate responses are forwarded, in order, to the unmatched-frame → event path after the sequence completes. SendOptions.Timeout applies per step. Define the policy as a static factory next to the packet definition, parameterized by the protocol's frame builder (which owns the packet-number counter). See Pattern 18: Multi-Step Protocol Sequences for the full behavior — lifecycle walkthrough, event delivery timing, per-step timeouts, failure modes — and compliance rule COMM-007 for the reviewer contract.
Exclusive Sessions¶
Use ExclusiveSession(...) when a caller-driven logical operation issues many request-reply packets and must not be interleaved with status polls or commands. Typical examples are device discovery, bulk configuration reads, and panel restore flows. A plain loop of await Send(...) is not enough for these operations: every awaited send releases the dispatch slot, so queued traffic can slip between two packets of the same logical operation.
ExclusiveSession takes the dispatch slot as one queued item, then holds it until the delegate completes. Inner Send(...) calls are unchanged; an ambient lease token lets the holder send on the held slot while all foreign unstamped sends wait. Interactive command sends during a foreign lease throw ChannelBusyException, which the framework maps to CommandResult.Busy.
Inside a TcpProtocolBase subclass, expose a narrow adapter-local wrapper when discovery or another capability needs to start a session from outside the protocol class:
internal Task<TResult> RunSession<TResult>(Func<CancellationToken, Task<TResult>> operation, CancellationToken ct)
=> ExclusiveSession(operation, ct);
Then wrap the whole logical operation once:
public static Task<AdapterDeviceCollection> Discover(Panel panel, Protocol protocol, CancellationToken ct)
=> protocol.RunSession(async leaseCt =>
{
var readers = await protocol.EnumerateReaders(leaseCt);
var doors = await protocol.EnumerateDoors(leaseCt);
return BuildTree(readers, doors);
}, ct);
Do not use SendBulk inside an exclusive session. SendBulk is deliberately interruptible and fills idle gaps; the held dispatch loop does not pull bulk items. Single request-reply operations do not need a session.
SendOptions¶
Per-request configuration passed to Send(request, options, ct).
| Property | Default | Description |
|---|---|---|
Timeout |
null (uses channel default) | Per-request timeout override |
RetryPolicy |
RetryPolicy.None |
Retry behavior on transient failures |
NoReply |
false | Fire-and-forget mode (no response expected) |
Priority |
Normal |
Queue priority (Normal or High) |
RetryPolicy¶
Configures automatic retry on transient failures, including matched MatchResult.Busy responses. Not retried: OperationCanceledException, TimeoutException, disconnect.
// No retry (default)
RetryPolicy.None
// Fixed delay between attempts
RetryPolicy.Fixed(maxAttempts: 3, delay: TimeSpan.FromMilliseconds(100))
// Exponential backoff: 50ms → 100ms → 200ms → 400ms
RetryPolicy.Exponential(maxAttempts: 5, initialDelay: TimeSpan.FromMilliseconds(50))
// Capped exponential backoff: 100ms → 200ms → 400ms → 500ms (cap)
RetryPolicy.Exponential(
maxAttempts: 5,
initialDelay: TimeSpan.FromMilliseconds(100),
maxDelay: TimeSpan.FromMilliseconds(500))
// Custom delay factory
new RetryPolicy(maxAttempts: 4, attempt => TimeSpan.FromMilliseconds(attempt * 50))
Use capped exponential when short retries are useful but unbounded growth would hurt command latency (for example, user-facing commands on temporarily busy controllers).
Priority Queues¶
ProtocolChannel maintains two queues:
- High priority (unbounded) — urgent commands, status polls
- Normal priority (bounded, backpressure) — bulk operations, background work
Fairness rule: after HighPriorityBurstLimit (default 10) consecutive high-priority items, one normal item is forced. This prevents starvation during access sync.
// High priority (e.g., user-initiated command)
await Send(request, new SendOptions { Priority = Priority.High }, ct);
// Normal priority (e.g., bulk credential upload)
await SendBulk(frames, new BulkOptions { Priority = Priority.Normal }, ct);
BulkOptions and SendBulk¶
For bulk operations (access sync, mass configuration):
var result = await SendBulk(frames, new BulkOptions
{
Timeout = TimeSpan.FromSeconds(2),
RetryPolicy = RetryPolicy.Fixed(2, TimeSpan.FromMilliseconds(50)),
Priority = Priority.Normal
}, ct);
// Check results
if (result.IsSuccess)
Logger.LogInformation("All {Count} items succeeded", result.SuccessCount);
else
Logger.LogWarning("Bulk stopped at index {Index}: {Error}",
result.StoppedAtIndex, result.Items.Last().Exception?.Message);
Semantics:
- Items sent sequentially (protocol constraint)
- First failure stops the bulk (no partial success hidden)
BulkResult<TFrame>contains per-item outcomes
Practical Examples¶
Simple command (most common)¶
public async Task<bool> Arm(int partition, CancellationToken ct)
{
var request = MyFrame.CreateSend(0x01, BuildPacket(partition));
var response = await Send(request, TimeSpan.FromSeconds(5), ct);
return response.Payload[0] == 0x00; // ACK
}
Command with capped exponential retry (unreliable device)¶
public async Task<byte[]?> ReadMemory(ushort address, CancellationToken ct)
{
var request = MyFrame.CreateSend(0x10, BitConverter.GetBytes(address));
try
{
var response = await Send(request, new SendOptions
{
Timeout = TimeSpan.FromSeconds(2),
RetryPolicy = RetryPolicy.Exponential(
maxAttempts: 3,
initialDelay: TimeSpan.FromMilliseconds(100),
maxDelay: TimeSpan.FromMilliseconds(300))
}, ct);
return response.Payload;
}
catch (TimeoutException)
{
Logger.LogWarning("ReadMemory timeout at 0x{Address:X4}", address);
return null;
}
}
Fire-and-forget (no response expected)¶
public async Task SendKeepalive(CancellationToken ct)
{
var request = MyFrame.CreateSend(0xFF, []);
await Send(request, new SendOptions { NoReply = true }, ct);
}
Bulk upload (access sync)¶
public async Task<int> UploadCredentials(IReadOnlyList<Credential> credentials, CancellationToken ct)
{
var frames = credentials
.Select(c => MyFrame.CreateSend(0x20, EncodeCredential(c)))
.ToList();
var result = await SendBulk(frames, new BulkOptions
{
Timeout = TimeSpan.FromSeconds(2),
RetryPolicy = RetryPolicy.Fixed(2, TimeSpan.FromMilliseconds(50))
}, ct);
if (!result.IsSuccess)
Logger.LogWarning("Upload stopped at {Index}/{Total}", result.StoppedAtIndex, credentials.Count);
return result.SuccessCount;
}
High-priority command (user action)¶
public async Task<bool> EmergencyUnlock(int door, CancellationToken ct)
{
var request = MyFrame.CreateSend(0x99, [(byte)door]);
var response = await Send(request, new SendOptions
{
Timeout = TimeSpan.FromSeconds(1),
Priority = Priority.High // jumps queue
}, ct);
return response.Payload[0] == 0x00;
}
Common Mistakes¶
| Mistake | Fix |
|---|---|
Manually reading from TcpConnection.Frames in adapter protocol code |
Use ProtocolChannel and TransformToEvents() / OnUnmatchedFrame instead |
Using TcpConnection directly for request-response |
Use Send() on TcpProtocolBase / ProtocolChannel |
IsResponseTo() matches unsolicited events |
Exclude event types in your matching logic (e.g. Type != "nq") |
Serialize() called on parsed frame (no RawPacket) |
Use a factory method like CreateSend() for outgoing frames |
Multiple Send() with same matching key without CommandQueue |
Serialize commands through ProtocolBase.Enqueue() to avoid collisions |
Multi-packet discovery/config read implemented as a plain Send() loop |
Wrap the whole logical operation in one ExclusiveSession(...) |
Returning false from Connect() after opening a channel without cleanup |
Always disconnect partial sessions in finally so reconnect starts clean |
See Also¶
- Protocol Layer — protocol responsibilities and patterns
- Pattern 3: Connection Management — IDeviceConnection lifecycle
- Communication Overview — end-to-end communication architecture