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<T> FIFO (async)<br/>- Long-running dispatcher task<br/>- _asyncInFlight slot]
SyncDispatcher[Sync Dispatcher<br/>- Channel<T> 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)
- API Call: User calls
CommAPI.Send()(async flow) orCommAPI.SendReceive()(sync flow). - Enqueue Message: Message written to the FLOW-specific
Channel<AACommMessage>(_asyncChannelforSend;_syncChannelfor the SR-built tagged duplicate). Lock-free. - 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_resetDispatchGateif an async Reset is still in its recovery window. - Atomic enqueue + Send: Under
_wireSendLock(serializes the two dispatchers), the item is enqueued into_wireOrderANDCommHandler.SendMessageis invoked — so wire arrival order matches_wireOrderorder. - Format Protocol:
CommHandlerapplies protocol-specific formatting (CAN, Ethernet bulk, etc.). - TCP Send:
SocketClienttransmits via TCP to AACommServer loopback socket (127.0.0.1:399 on Windows, :1024 on Linux).
Inbound Path (AACommServer → Application)
- TCP Response: Response received from AACommServer via TCP.
- Buffer and Parse:
SocketClientbuffers data until complete message (>\rdelimiter). - OnMessageReceived: Callback fires with complete message string on the receiver thread.
- Parse Response:
Session.HandleReplyclassifies the line as Fatal / Push / Reply and applies bulk-error mangling via per-messageReplyPostProcessingState(attached toAACommMessage.BulkPostProcessingbyCommHandler.SendMessage). - Route:
Fatals invoke
OnCommsErrorsynchronously on the receiver (the suppression rule on that event is documented on the event itself). Pushes are dispatched toOnPushMessageReceivedviaTask.Run. Replies pop the head of_wireOrderandTrySetResultthe originating in-flight item'sReplyDone— the receiver thread does NOT invokeOnReplyReceivedfor replies. - Dispatcher resumes: The flow's dispatcher wakes from its
await item.ReplyDone.Taskwith the parsed args. State containers updated (e.g.,_userProg.Clear()on ProgErase ack). - User callback / sync return:
- Async (
Send):OnReplyReceivedinvoked on the async dispatcher thread.OnResetProgress(begin → recovery sleep → end) runs here for async Reset only, gated onIsSyncRequest == false. - Sync (
SendReceive): The internal wait-handle setter (installed asOnReplyReceivedon the tagged duplicate) sets the SR's wait handle; SR returns the capturedAACommEventArgs. NoOnCommsError/OnResetProgressevent is fired for sync requests (suppressed by tag).
- Async (
- 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 OnResetProgressandOnCommsErrorevents 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 asOnReplyReceived, registers aCancellationTokenRegistrationon the liveSession.Cts.Tokenthat wakes the same wait handle on session-death, enqueues to the sync dispatcher, then blocks on the handle. - Returns
AACommEventArgsto the caller after the handle fires. Three return shapes: reply (the dominant path),SyncErr Timeouton safety-net expiry,SyncErr Session terminatedwhen the session died (dispatcher crash, fatal comms error, concurrent Disconnect) before any reply arrived. OnCommsErrorandOnResetProgressare 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
_wireOrderand callsICommHandler.SendMessage, then awaits a per-iterationTaskCompletionSource<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 itsReplyDonewith 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.
IsQueueEmptyis_asyncEnqueuedCount == 0 && _syncEnqueuedCount == 0 && _asyncInFlight == null && _syncInFlight == null. The counters are needed becauseChannelReader<T>.CountthrowsNotSupportedExceptionat runtime on the netstandard2.0 build ofSystem.Threading.Channelsresolved 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
nullargs = "session torn down") so dispatchers' awaits unblock, drains_wireOrderso 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 (seeDisconnect_ClearsAsyncQueue_ReplyCallbacksMaySilentlyDrop). In-flight async callbacks are also dropped on a fatal teardown — the dispatcher observes anullreply args, skipsRunUserCallback, and exits. The async consumer observes the fatal only via theOnCommsErrorevent (which the orchestrator fires unless suppressed — see the suppression rule onCommAPI.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
- StartAACommServer(): Launches AACommServer process (optional, auto-starts on connect)
- TestConnection(): Verifies hardware connectivity without full connection
- Connect(): Establishes session, queries controller identity, initializes containers
- Send()/SendReceive(): Message exchange
- Disconnect(): Closes session, clears state
- 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
AllowSimenabled)
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
Sendonly. Suppressed forSendReceive(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 betweenOnResetProgress(true)andOnResetProgress(false)(insideResetCoordinator.RunPostResetWait); the sync flow is parked at_resetDispatchGatefor the same window. Skipped entirely for syncSendReceiveof 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.ResetReplyHelperclassifies the markers;Session.SendReceivemasks the race for syncAResetso 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
TimeoutMsproperty
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:
AACommExclusiveCommsManagerservice class - Error handling: Other clients receive
CHANNEL_IS_EXCLUSIVEerrors during exclusive mode
Thread Safety
Connect/Disconnect/TestConnection/CloseAACommServermutually serialize via the orchestrator's connect lock (also held during the post-Reset recovery wait, so reset and connect/disconnect cannot race)SendandSendReceiveare 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
ICommHandlerinstance. 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 toOnPushMessageReceivedviaTask.Run(no user code on the receiver). For fatals it invokesOnCommsErrorsynchronously on the receiver — long-running fatal handlers block reply / push classification until they return. SendReceivefrom inside anOnReplyReceivedcallback (on the dispatcher thread) returns an error to avoid same-flow pipeline deadlock;SendfromOnReplyReceivedis fine. The guard catches sync re-entry only — anasync voidcallback that issues SR from a continuation that resumed after anawaitis NOT rejected (out of scope: the post-await continuation no longer holds the dispatcher's frame).OnPushMessageReceivedinvoked viaTask.Run(no blocking of the receiver thread).OnResetProgresshandler invocations are dispatched viaTask.Run, butResetCoordinator.RunPostResetWaitruns the recovery sleep synchronously on the async dispatcher thread — the async pipeline is blocked for the recovery window.OnCommsErroris 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
IsResettingis 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.Sessioncalls_raiseCommsError(message), never_owner.OnCommsError?.Invoke(...). Events are part of the public facade, not the internal seam. - Both
SessionandConnectionOrchestratorhold an explicitCommAPIowner. This is intentional:ICommHandler.SendMessage(AACommMessage, CommAPI),ConnectHelper.*(CommAPI, ...), and the container bootstraps still take the concrete API. Detangling that to a narrowIApiRuntimeContextis a separate follow-up refactor. Send/SendReceiveare wired here:CommAPI.Sendshort-circuits to API_NOT_CONNECTED if the orchestrator has no current session; otherwise it forwards toSession.Send. Same forSendReceive.- 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.