AAComm Architecture

Client-side communication library for sending commands to hardware controllers via AACommServer.

Architecture Overview

AAComm is a client library that provides a high-level API for user applications to communicate with hardware controllers. It manages the connection to AACommServer, queues messages, handles responses, and maintains controller state information.

Communication Pipeline

The following diagram illustrates the bidirectional communication flow between user applications and AACommServer.

Note: Send and SendReceive use independent FIFO pipelines sharing a single underlying wire. Each path has its own queue (Channel<AACommMessage>), its own dispatcher task, and its own in-flight slot. Ordinary messages in the two flows do not gate each other — a long-running async Send callback does not block a concurrent SendReceive, and a SendReceive parked on its safety-net wait does not block subsequent Sends. Async Reset is the deliberate exception: later dispatch is parked until reset recovery completes. Per-flow FIFO is preserved (consecutive Sends fire callbacks in order; consecutive SRs return in order); cross-flow ordering is NOT guaranteed (an async Send and a sync SendReceive issued back-to-back may dispatch in either order).

Wire writes from the two dispatchers are serialized by an internal mutex; the receiver routes arriving replies in wire-arrival order via a _wireOrder queue (this works because AACommServer processes one message per channel at a time and replies in send order). For replies, the receiver thread hands the parsed args to the originating flow's dispatcher via a per-iteration TaskCompletionSource, and the dispatcher runs the user callback on its own thread — the receiver thread does not invoke OnReplyReceived. For push messages, the receiver hands the parsed payload to the OnPushMessageReceived event via Task.Run, so user code runs on the ThreadPool, not the receiver thread. The one exception is OnCommsError: it is invoked synchronously on the receiver thread, and long-running work in that handler will stall reply / push classification until it returns (after a fatal the session is being torn down anyway, but handlers that need to dispatch onto a UI thread should off-load with Task.Run). This receiver-never-blocks-on- reply-callbacks discipline is what unblocks the bug where an async Send callback parked on a UI dispatcher would deadlock a concurrent SendReceive from the UI thread.

SendReceive is implemented on top of the sync pipeline: it builds a tagged duplicate of the user's message (IsSyncRequest = true), installs an internal wait-handle hook as OnReplyReceived, and blocks until the reply arrives or the safety-net timeout expires. The per-message IsSyncRequest marker drives event-suppression (OnCommsError and OnResetProgress are not raised for sync requests; the Reset recovery sleep is skipped).

graph TB
    subgraph "User Application"
        App[Application Code]
    end

    subgraph "AAComm Library"
        subgraph "Public API Layer"
            API[CommAPI<br/>- Connect/Disconnect<br/>- Send/SendReceive<br/>- Event Handlers<br/>- State Properties]
        end

        subgraph "Message Management Layer"
            AsyncDispatcher[Async Dispatcher<br/>- Channel&lt;T&gt; FIFO (async)<br/>- Long-running dispatcher task<br/>- _asyncInFlight slot]
            SyncDispatcher[Sync Dispatcher<br/>- Channel&lt;T&gt; FIFO (sync)<br/>- Long-running dispatcher task<br/>- _syncInFlight slot]
            WireRouting[Wire-Order Routing<br/>- _wireSendLock serializes wire writes<br/>- _wireOrder routes replies in send-order<br/>- _resetDispatchGate parks the other flow during async-Reset recovery]
            Containers[State Containers<br/>- ControllerIdentityContainer<br/>- MessagesContainer<br/>- UserProgVarsAdapter]
        end

        subgraph "Protocol Layer"
            Handler[CommHandler<br/>- Message Serialization<br/>- Protocol Formatting<br/>- Response Parsing<br/>- Bulk Message Handling]
        end

        subgraph "Transport Layer"
            Socket[SocketClient<br/>- TCP Socket<br/>- Connection Management<br/>- Receive Buffer<br/>- Auto-Reconnect]
        end

        subgraph "Server Management"
            Helper[AACommServerHelper<br/>- Process Launch<br/>- Health Check<br/>- Path Resolution]
        end
    end

    subgraph "AACommServer Process"
        Server[AACommServerAPI<br/>See AACommServerAPI/README.md]
    end

    %% Outbound Flow (App → Server)
    App -->|1a. Async Send| API
    App -->|1b. Sync SendReceive| API
    API -->|2a. Enqueue (async flow)| AsyncDispatcher
    API -->|2b. Enqueue tagged (sync flow)| SyncDispatcher
    AsyncDispatcher -->|3a. Atomic enqueue _wireOrder + SendMessage| WireRouting
    SyncDispatcher  -->|3b. Atomic enqueue _wireOrder + SendMessage| WireRouting
    WireRouting -->|4. Format Protocol| Handler
    Handler -->|5. Serialize Message| Socket
    Socket -->|6. TCP Send| Server

    %% Inbound Flow (Server → App)
    Server -->|7. TCP Response| Socket
    Socket -->|8. Buffer + Parse| Socket
    Socket -->|9. OnMessageReceived| Handler
    Handler -->|10. Parse Response| Handler
    Handler -->|11. Update State| Containers
    Handler -->|12. Route via _wireOrder head| WireRouting
    WireRouting -->|13a. Hand reply args via TCS| AsyncDispatcher
    WireRouting -->|13b. Hand reply args via TCS| SyncDispatcher
    AsyncDispatcher -->|14a. Run user callback on dispatcher thread<br/>(+ fire events)| API
    SyncDispatcher  -->|14b. Set wait-handle in SR wrapper| API
    API -->|15a. Fire OnReplyReceived (async)| App
    API -->|15b. Return AACommEventArgs (sync)| App

    %% Server Management
    App -.->|StartAACommServer| Helper
    Helper -.->|Launch Process| Server
    Helper -.->|Verify Running| Socket

    %% Styling
    classDef appClass fill:#e1f5ff,stroke:#01579b,stroke-width:2px
    classDef apiClass fill:#f3e5f5,stroke:#4a148c,stroke-width:2px
    classDef queueClass fill:#e8f5e9,stroke:#1b5e20,stroke-width:2px
    classDef routingClass fill:#dcedc8,stroke:#33691e,stroke-width:2px
    classDef containerClass fill:#fff3e0,stroke:#e65100,stroke-width:2px
    classDef handlerClass fill:#fce4ec,stroke:#880e4f,stroke-width:2px
    classDef socketClass fill:#fff9c4,stroke:#f57f17,stroke-width:2px
    classDef helperClass fill:#f1f8e9,stroke:#33691e,stroke-width:2px
    classDef serverClass fill:#efebe9,stroke:#3e2723,stroke-width:2px

    class App appClass
    class API apiClass
    class AsyncDispatcher queueClass
    class SyncDispatcher queueClass
    class WireRouting routingClass
    class Containers containerClass
    class Handler handlerClass
    class Socket socketClass
    class Helper helperClass
    class Server serverClass

Pipeline Flow Details

Outbound Path (Application → AACommServer)

  1. API Call: User calls CommAPI.Send() (async flow) or CommAPI.SendReceive() (sync flow).
  2. Enqueue Message: Message written to the FLOW-specific Channel<AACommMessage> (_asyncChannel for Send; _syncChannel for the SR-built tagged duplicate). Lock-free.
  3. Dispatch: The flow's long-running dispatcher Task drains its channel FIFO. Per-iteration, it publishes the message into its per-flow in-flight slot (_asyncInFlight / _syncInFlight), then waits at _resetDispatchGate if an async Reset is still in its recovery window.
  4. Atomic enqueue + Send: Under _wireSendLock (serializes the two dispatchers), the item is enqueued into _wireOrder AND CommHandler.SendMessage is invoked — so wire arrival order matches _wireOrder order.
  5. Format Protocol: CommHandler applies protocol-specific formatting (CAN, Ethernet bulk, etc.).
  6. TCP Send: SocketClient transmits via TCP to AACommServer loopback socket (127.0.0.1:399 on Windows, :1024 on Linux).

Inbound Path (AACommServer → Application)

  1. TCP Response: Response received from AACommServer via TCP.
  2. Buffer and Parse: SocketClient buffers data until complete message (>\r delimiter).
  3. OnMessageReceived: Callback fires with complete message string on the receiver thread.
  4. Parse Response: Session.HandleReply classifies the line as Fatal / Push / Reply and applies bulk-error mangling via per-message ReplyPostProcessingState (attached to AACommMessage.BulkPostProcessing by CommHandler.SendMessage).
  5. Route: Fatals invoke OnCommsError synchronously on the receiver (the suppression rule on that event is documented on the event itself). Pushes are dispatched to OnPushMessageReceived via Task.Run. Replies pop the head of _wireOrder and TrySetResult the originating in-flight item's ReplyDone — the receiver thread does NOT invoke OnReplyReceived for replies.
  6. Dispatcher resumes: The flow's dispatcher wakes from its await item.ReplyDone.Task with the parsed args. State containers updated (e.g., _userProg.Clear() on ProgErase ack).
  7. User callback / sync return:
    • Async (Send): OnReplyReceived invoked on the async dispatcher thread. OnResetProgress (begin → recovery sleep → end) runs here for async Reset only, gated on IsSyncRequest == false.
    • Sync (SendReceive): The internal wait-handle setter (installed as OnReplyReceived on the tagged duplicate) sets the SR's wait handle; SR returns the captured AACommEventArgs. No OnCommsError / OnResetProgress event is fired for sync requests (suppressed by tag).
  8. Loop: Each dispatcher loops to its next channel message. Per-flow FIFO holds; cross-flow ordering is NOT guaranteed.

Server Management (Dotted Lines)

  • StartAACommServer: Static method launches AACommServer process if not running
  • Launch Process: Attempts to start AACommServer.exe or .dll via dotnet
  • Verify Running: Polls TCP socket to confirm AACommServer is accepting connections

Key Features

Send and SendReceive — Two-Flow Pipeline

Send and SendReceive have independent FIFO queues and dispatchers. They cooperate freely on the same CommAPI instance from any number of threads — neither rejects the other, and ordinary messages on one flow do not gate the other.

Asynchronous (Send):

  • Non-blocking; returns immediately after enqueue
  • User callback (OnReplyReceived) invoked after the reply arrives, on the async dispatcher thread
  • OnResetProgress and OnCommsError events fired as applicable
  • Suitable for multi-threaded applications and event-driven flows
  • Flow: Send() → Queue → Dispatch → CommHandler → reply → callback (+ events)

Synchronous (SendReceive):

  • Sync wrapper over the sync pipeline. Builds a tagged duplicate of the message (IsSyncRequest = true), installs an internal wait-handle hook as OnReplyReceived, registers a CancellationTokenRegistration on the live Session.Cts.Token that wakes the same wait handle on session-death, enqueues to the sync dispatcher, then blocks on the handle.
  • Returns AACommEventArgs to the caller after the handle fires. Three return shapes: reply (the dominant path), SyncErr Timeout on safety-net expiry, SyncErr Session terminated when the session died (dispatcher crash, fatal comms error, concurrent Disconnect) before any reply arrived.
  • OnCommsError and OnResetProgress are suppressed for sync requests (the caller observes errors via the return value; reset recovery timing is the caller's problem).
  • Disconnects the API on safety-net wait expiry (preserves the documented contract).
  • Flow: SendReceive() → tagged sync message → Queue → Dispatch → reply → wait handle set → return

Re-entry guard: calling SendReceive from inside an OnReplyReceived callback (i.e. on that callback's dispatcher thread) returns an error rather than deadlocking the pipeline. Calling Send from OnReplyReceived is fine and is the normal way to chain async work.

Concurrent-call timing: a deep queue × per-message reply latency can exceed an individual SendReceive's TimeoutMs. Callers issuing many concurrent SRs should pass a generous TimeoutMs that accounts for queue depth.

Message Queue Management

  • TWO System.Threading.Channels.Channel<AACommMessage> (unbounded, single-reader) per session — one for the async flow (Send), one for the sync flow (SendReceive). Lock-free writes from any number of threads.
  • TWO long-running dispatcher tasks per session (one per flow). Each drains its own channel FIFO, publishes the in-flight message into its per-flow slot, atomically (under the wire-send lock) enqueues the item into _wireOrder and calls ICommHandler.SendMessage, then awaits a per-iteration TaskCompletionSource<AACommEventArgs> set by the receiver when the reply arrives. After the await unblocks, the dispatcher runs the user callback on its own thread (NOT the receiver thread), then loops to the next message. That gate enforces per-flow FIFO-with-full-callback-completion WITHIN the flow.
  • The receiver thread (single per session) routes by wire FIFO: pop the head of _wireOrder, set its ReplyDone with the parsed args. The dispatcher whose item just got args-routed wakes up and runs the callback. Receiver never invokes user code.
  • Async Reset remains a global transport interlock: once an async Reset is sent, both dispatchers are prevented from sending later messages until the post-Reset recovery window completes. Sync Reset keeps the existing contract and skips that recovery wait; its caller is responsible for reset timing.
  • IsQueueEmpty is _asyncEnqueuedCount == 0 && _syncEnqueuedCount == 0 && _asyncInFlight == null && _syncInFlight == null. The counters are needed because ChannelReader<T>.Count throws NotSupportedException at runtime on the netstandard2.0 build of System.Threading.Channels resolved against net48 — it is NOT a safe primitive on this TFM.
  • Disconnect completes both channel writers, cancels the dispatchers' token, signals the in-flight TCSes (with null args = "session torn down") so dispatchers' awaits unblock, drains _wireOrder so late replies drop cleanly, and bounds the wait for both dispatchers to exit. Queued messages whose dispatch was not started have their callbacks silently dropped (see Disconnect_ClearsAsyncQueue_ReplyCallbacksMaySilentlyDrop). In-flight async callbacks are also dropped on a fatal teardown — the dispatcher observes a null reply args, skips RunUserCallback, and exits. The async consumer observes the fatal only via the OnCommsError event (which the orchestrator fires unless suppressed — see the suppression rule on CommAPI.OnCommsError).

Buffer Management

  • Socket receive buffer: 100,000 bytes
  • Message delimiter: >\r (greater-than + carriage return)
  • Internal message separator: #@# between message fields
  • Wire format: <ConnectionData>#@#<MessageType>#@#<TimeoutMs>#@#<Message>>\r

IPC Framing

TCP is a byte stream; a single recv can carry zero, one, or many complete IPC envelopes plus a partial tail. Both ends use a shared EnvelopeAccumulator (AAComm/InternalServices/): bytes go in, complete envelopes terminated by >\r come out, partial tails persist until the next recv. The accumulator drops its internal buffer when fully drained so multi-MB RecUpload payloads don't hold their peak buffer for the life of the socket. Direct ctor bypass of the accumulator is guarded by a backstop in SocketMessageData that throws FormatException when an envelope doesn't have exactly 4 #@#-separated fields.

Hex-sigil escape for binary firmware payloads. AGM800 MAS02 firmware download sends raw binary bytes that may legitimately contain 0x3E 0x0D — the IPC envelope terminator. To keep the on-wire IPC body ASCII-safe, the client wraps the binary chunk as <prefix>@#HEX:<hex-pairs>\r (CommCommons.HexEncodedPrefix); the server-side SocketMessageData ctor detects the sigil, strips it, decodes the hex pairs back to raw bytes, and forwards a byte-identical payload to the controller. The sigil follows the message-type prefix (@#FW:) and precedes the trailing CR, so the decoder must search by IndexOf and preserve both the prefix and the CR verbatim. See GitHub issue #18 for the protocol redesign that obsoletes this whole layer.

Connection Lifecycle

  1. StartAACommServer(): Launches AACommServer process (optional, auto-starts on connect)
  2. TestConnection(): Verifies hardware connectivity without full connection
  3. Connect(): Establishes session, queries controller identity, initializes containers
  4. Send()/SendReceive(): Message exchange
  5. Disconnect(): Closes session, clears state
  6. CloseAACommServer(): Shuts down AACommServer process (optional)

State Management

ControllerIdentityContainer:

  • Controller type, firmware version, serial number
  • Feature flags (user units, variable names support, etc.)
  • Number of axes

MessagesContainer:

  • Controller-specific message definitions from XML
  • Parameter info database for localization
  • User units conversion (if enabled)

UserProgVarsAdapter:

  • Maps user program variable names to controller addresses
  • Auto-refreshed on user program download

Error Handling

  • OnCommsError event: Fatal errors (timeout, connection loss, AACommServer crash)
  • ConnectResult enum: Detailed error codes for all operations
  • Automatic cleanup: API auto-disconnects on fatal errors
  • Exception handling: TCP socket errors converted to events

Protocol Support

  • CAN: Binary protocol with CAN ID and data bytes (Windows only - requires Kvaser CanLib)
  • RS-232: Serial port text-based communication
  • Ethernet: TCP/IP socket-based communication with bulk message support (up to 50 messages per bulk)
  • Simulator: File-based simulation for testing (when AACommServer AllowSim enabled)

Message Types

  • Regular Messages: Text-based commands with parameters
  • Bulk Messages: Ethernet-only batch sending (up to 50 messages per bulk)
  • Binary Uploads: Firmware and user program downloads
  • Special Messages: FPGA downloads, ASCII bulk, CNC-specific bulk messages

Reset Handling

  • OnResetProgress event: Fires on reset start/complete for async Send only. Suppressed for SendReceive (sync caller manages reset timing).
  • Extended timeout: 10+ seconds for controller reset (10001ms standard controller, varies by type)
  • After-reset wait: 3000ms for standard controllers, 10000ms for MAS02 (AfterResetWait / AfterResetWaitMAS02). The async dispatcher thread sleeps between OnResetProgress(true) and OnResetProgress(false) (inside ResetCoordinator.RunPostResetWait); the sync flow is parked at _resetDispatchGate for the same window. Skipped entirely for sync SendReceive of a Reset (the caller manages reset timing).
  • Reconnection: Automatic reconnection after Ethernet reset
  • State preservation: Connection data maintained across reset
  • Post-reset disconnect transparency: After a controller-rebooting step (FW DL EOF, FPGA EOF, sync AReset) the controller-side TCP socket goes away; AACommServer surfaces this as a disconnect-style reply (PC Suite: ERR 4021 / ERR 4020 / channel-fatal broadcast). These are not failures — they're proof the reset took effect. ResetReplyHelper classifies the markers; Session.SendReceive masks the race for sync AReset so consumers always see the success shape regardless of which signal arrived first. The helper is only meaningful AFTER the reset-triggering step has been issued — before, the same markers indicate genuine connectivity failure.

Timeout Configuration

  • Default timeout: 2000ms for standard messages
  • Reset timeout: 10001ms (standard controller), varies by controller type
  • Firmware download: 6000ms
  • User program download: 3000ms
  • Open channel: 1000ms (RS-232/CAN), 10000ms (Ethernet), 20000ms (Simulator)
  • All timeouts configurable per-message via TimeoutMs property

Exclusive Channel Access

  • Request exclusive access: Prevents other clients from using the same hardware channel
  • Managed server-side: AACommServer enforces exclusive mode per channel
  • Use case: Critical operations like firmware updates, calibration
  • API methods: AACommExclusiveCommsManager service class
  • Error handling: Other clients receive CHANNEL_IS_EXCLUSIVE errors during exclusive mode

Thread Safety

  • Connect / Disconnect / TestConnection / CloseAACommServer mutually serialize via the orchestrator's connect lock (also held during the post-Reset recovery wait, so reset and connect/disconnect cannot race)
  • Send and SendReceive are thread-safe and may be interleaved freely from any number of threads. The two have INDEPENDENT pipelines: a long-running async callback does not block sync dispatch, and vice versa.
  • Each dispatcher is the single reader of its own message channel; per-flow FIFO is preserved by the Channel<T> writer order and the dispatcher's gated single-step loop
  • The receiver thread is single-threaded per ICommHandler instance. For replies it hands parsed args to the originating flow's dispatcher via a per-iteration TCS (no user code on the receiver). For pushes it hands the payload to OnPushMessageReceived via Task.Run (no user code on the receiver). For fatals it invokes OnCommsError synchronously on the receiver — long-running fatal handlers block reply / push classification until they return.
  • SendReceive from inside an OnReplyReceived callback (on the dispatcher thread) returns an error to avoid same-flow pipeline deadlock; Send from OnReplyReceived is fine. The guard catches sync re-entry only — an async void callback that issues SR from a continuation that resumed after an await is NOT rejected (out of scope: the post-await continuation no longer holds the dispatcher's frame).
  • OnPushMessageReceived invoked via Task.Run (no blocking of the receiver thread). OnResetProgress handler invocations are dispatched via Task.Run, but ResetCoordinator.RunPostResetWait runs the recovery sleep synchronously on the async dispatcher thread — the async pipeline is blocked for the recovery window. OnCommsError is the deliberate exception (sync on receiver) — see the suppression rule on the event for why mixed-mode fatals still fire it
  • Socket operations thread-safe via async callbacks
  • IsResetting is a volatile bool readable from any thread; flipped by the async dispatcher thread around the post-Reset recovery sleep

Typical Usage Patterns

Asynchronous Mode (Event-Driven)

var api = new CommAPI();

// 1. Start server (optional - auto-starts if needed)
CommAPI.StartAACommServer();

// 2. Subscribe to events (REQUIRED for async mode)
api.OnCommsError += err => Console.WriteLine($"Error: {err}");
api.OnResetProgress += inProgress => Console.WriteLine($"Reset: {inProgress}");

// 3. Connect to controller
var connData = new ConnectionData {
    CommChannelType = ChannelType.Ethernet,
    ET_IP_Str = "192.168.1.100"
};
var result = api.Connect(connData);

// 4. Send commands with callbacks (callback is supplied via AACommMessage)
api.Send(new AACommMessage((_, args) => Console.WriteLine($"Version: {args.MsgReceived}"), "VER"));
api.Send(new AACommMessage((_, args) => Console.WriteLine($"System: {args.MsgReceived}"),  "SYSINFO"));

// Messages processed asynchronously in queue order
// ...

// 5. Disconnect
api.Disconnect();

Synchronous Mode (Blocking)

var api = new CommAPI();

// 1. Start server
CommAPI.StartAACommServer();

// 2. Events optional (not fired during SendReceive)
// api.OnCommsError += ...

// 3. Connect to controller
var connData = new ConnectionData {
    CommChannelType = ChannelType.Ethernet,
    ET_IP_Str = "192.168.1.100"
};
var result = api.Connect(connData);

// 4. Send commands synchronously (blocks until reply)
var version = api.SendReceive(new AACommMessage("VER"));
Console.WriteLine($"Version: {version.MsgReceived}");

var sysInfo = api.SendReceive(new AACommMessage("SYSINFO"));
Console.WriteLine($"System: {sysInfo.MsgReceived}");

// Each call blocks until response received
// ...

// 5. Disconnect
api.Disconnect();

Internal layout

CommAPI.cs is a thin public facade over two internal collaborators (CommHandler + ConnectionOrchestrator). Production behavior is documented above; the file map below is the navigation aid for code work.

AAComm/
  CommAPI.cs                              public facade — events, properties, forwarders
  Internal/
    Transport/
      Session.cs                          per-connection runtime: channel, dispatcher,
                                          in-flight slot, Send/SendReceive,
                                          dispatcher callback re-entry guard
                                          (IsOnReceiverThread / _inCallbackForSession),
                                          HandleReply, NotifyInFlightFatal
      ReplyClassifier.cs                  pure helper: classify raw line as
                                          Fatal / Push / Reply (with content)
      BulkReplyParser.cs                  pure helper: bulk reply normalization,
                                          ProgErase ack detection, length-mismatch
                                          truncation to min(queries, replies)
      ReplyPostProcessingState.cs        state-on-message carrier: bulk-error string
                                          and BulkIgnore per-index error map, attached
                                          to AACommMessage.BulkPostProcessing by
                                          CommHandler, consumed by Session.HandleReply
    Lifecycle/
      ConnectionOrchestrator.cs           Connect / ConnectSilent / TestConnection /
                                          Disconnect / CloseAACommServer; owns the
                                          connect lock and creates/destroys Sessions;
                                          fatal teardown path
      ResetCoordinator.cs                 post-Reset recovery block: lock, IsResetting
                                          flag, raiseResetProgress dispatch, sleep
                                          (AfterResetWait / AfterResetWaitMAS02)

Conventions worth knowing before editing:

  • Events stay on CommAPI. Collaborators raise via injected delegates. Session calls _raiseCommsError(message), never _owner.OnCommsError?.Invoke(...). Events are part of the public facade, not the internal seam.
  • Both Session and ConnectionOrchestrator hold an explicit CommAPI owner. This is intentional: ICommHandler.SendMessage(AACommMessage, CommAPI), ConnectHelper.*(CommAPI, ...), and the container bootstraps still take the concrete API. Detangling that to a narrow IApiRuntimeContext is a separate follow-up refactor.
  • Send / SendReceive are wired here: CommAPI.Send short-circuits to API_NOT_CONNECTED if the orchestrator has no current session; otherwise it forwards to Session.Send. Same for SendReceive.
  • Fatal teardown order is contractual: Session.NotifyInFlightFatal(message)Session.Stop()OnCommsError (suppressed if the in-flight call was sync-tagged). Inverting these breaks issue-#32 sync-tag suppression — the sync waiter would wake via the session-cancellation fallback before the specific error reached it.

Relationship to AACommServer

AAComm is the client library, AACommServerAPI is the server middleware.

  • AAComm: Embedded in user applications, provides high-level API
  • AACommServerAPI: Standalone process, manages hardware connections

Multiple AAComm clients can connect to a single AACommServer instance, sharing hardware channels. See AACommServerAPI/README.md for the server-side architecture.