# SuiteLink Tag Client Design ## Goal Build a cross-platform `.NET 10` C# client that communicates with AVEVA SuiteLink from macOS, Linux, and Windows for tag operations only. The v1 scope is limited to: - Connect to a SuiteLink endpoint - Subscribe to tags - Receive tag updates - Write tag values - Unsubscribe cleanly The v1 scope explicitly excludes: - AlarmMgr - Alarm and event handling - Secure SuiteLink V3 support - Automatic reconnect and subscription rebuild ## Constraints - The protocol is proprietary and the current design is based on reverse-engineered public evidence plus AVEVA product documentation. - The initial target is non-encrypted SuiteLink V2 behavior, or servers configured for mixed mode that still permit legacy SuiteLink traffic. - The first implementation only supports primitive value types: - `bool` - `int32` - `float32` - `string` - The design should remain extensible for later support of `double`, `int64`, and `DateTime` if packet captures confirm their wire representation. ## Protocol Target The target protocol surface is the normal SuiteLink tag exchange path, not AlarmMgr. Observed normal message structure: - `uint16 little-endian remaining_length` - `uint16 little-endian message_type` - payload - trailing byte `0xA5` Observed message types to support: - `CONNECT` - `ADVISE` - `ADVISE ACK` - `UPDATE` - `UNADVISE` - `UNADVISE ACK` - `POKE` - `POKE ACK` - `TIME` - Ping/pong keepalive messages Observed normal wire value types: - binary - integer - real - message ## Architecture The client is split into three layers. ### Transport Responsible for: - Opening and closing TCP connections - Reading complete SuiteLink frames - Writing complete SuiteLink frames - Cancellation and socket lifetime management ### Protocol Responsible for: - Encoding the startup handshake - Encoding `CONNECT` - Encoding `ADVISE`, `UNADVISE`, and `POKE` - Decoding handshake acknowledgements - Decoding `ADVISE ACK`, `UPDATE`, and keepalive traffic - Converting between wire values and typed client values ### Client API Responsible for: - Exposing a minimal public API for connect, subscribe, read, write, and disconnect - Hiding protocol details such as server-assigned tag ids - Dispatching updates to user callbacks or future stream abstractions ## Session Model The session uses one persistent TCP connection to one SuiteLink endpoint and one configured `application/topic` pair. State model: - `Disconnected` - `TcpConnected` - `HandshakeComplete` - `SessionConnected` - `Faulted` Startup flow: 1. Open TCP connection. 2. Send SuiteLink handshake for the normal tag protocol. 3. Validate handshake acknowledgement. 4. Send `CONNECT` with application, topic, and client identity fields. 5. Transition to connected session state. 6. Send `ADVISE` for one or more items. 7. Capture `tag_id` mappings from server responses. 8. Receive `UPDATE` frames and dispatch typed values to subscribers. 9. Send `POKE` for writes. 10. Send `UNADVISE` when a subscription is disposed. Read behavior in v1 is implemented as a temporary subscription: 1. Send `ADVISE` for the requested item. 2. Wait for the first matching `UPDATE`. 3. Return the decoded value. 4. Send `UNADVISE`. This is preferred over inventing a direct read request that has not yet been proven by packet captures. ## Public API ```csharp public sealed class SuiteLinkClient : IAsyncDisposable { Task ConnectAsync(SuiteLinkConnectionOptions options, CancellationToken ct = default); Task DisconnectAsync(CancellationToken ct = default); Task SubscribeAsync( string itemName, Action onUpdate, CancellationToken ct = default); Task ReadAsync( string itemName, TimeSpan timeout, CancellationToken ct = default); Task WriteAsync( string itemName, SuiteLinkValue value, CancellationToken ct = default); } ``` Supporting models: - `SuiteLinkConnectionOptions` - `Host` - `Port` default `5413` - `Application` - `Topic` - `ClientName` - `ClientNode` - `UserName` - `ServerNode` - `SuiteLinkValue` - typed union for `bool`, `int`, `float`, and `string` - `SuiteLinkTagUpdate` - `ItemName` - `TagId` - `Value` - `Quality` - `ElapsedMilliseconds` - `ReceivedAtUtc` - `SubscriptionHandle` - caller-facing subscription lifetime object - disposes via `UNADVISE` ## Internal Components ### `SuiteLinkFrameReader` Responsibilities: - Read complete normal SuiteLink frames from a `NetworkStream` - Parse the 2-byte little-endian remaining length - Validate trailing `0xA5` - Return frame payload slices to the codec ### `SuiteLinkFrameWriter` Responsibilities: - Build frames in memory - Write little-endian lengths and message types - Encode UTF-16LE strings where required - Append trailing `0xA5` ### `SuiteLinkMessageCodec` Responsibilities: - Encode handshake and normal session messages - Decode incoming acknowledgements and updates - Map wire value types to `SuiteLinkValue` Expected methods: - `EncodeHandshake` - `EncodeConnect` - `EncodeAdvise` - `EncodeUnadvise` - `EncodePoke` - `DecodeHandshakeAck` - `DecodeAdviseAck` - `DecodeUpdate` - `DecodeKeepAlive` ### `SuiteLinkSession` Responsibilities: - Own the send and receive loops - Maintain session state - Track `itemName <-> tagId` mappings - Track active subscriptions - Route decoded updates to the correct subscriber callbacks ## Type Strategy The first release supports only: - `bool` - `int32` - `float32` - `string` The public type wrapper must be extensible so later additions do not require replacing the whole API. The intended future-compatible additions are: - `double` - `int64` - `DateTime` The protocol layer should fail fast when an unsupported wire type or unsupported outgoing write type is encountered. ## Error Handling The v1 client should be explicit and conservative. - Any malformed frame transitions the session to `Faulted` - Any unexpected message type during startup fails the connection attempt - Write attempts for unsupported types fail immediately - Reconnect is not automatic in v1 - Subscription rebuild after reconnect is deferred to a later version ## Validation Strategy Testing is divided into three levels. ### Unit Tests Validate: - frame length handling - trailing marker validation - UTF-16LE string encoding/decoding - primitive value encoding/decoding ### Golden Packet Tests Use known byte sequences and captures to verify: - handshake - `CONNECT` - `ADVISE` - `ADVISE ACK` - `UPDATE` - `POKE` - `UNADVISE` ### Integration Tests Run against a real AVEVA/OI server configured to allow legacy or mixed-mode SuiteLink traffic. Success criteria: - connect successfully from macOS or Linux - subscribe to one boolean tag - subscribe to one integer tag - subscribe to one real tag - subscribe to one message tag - receive live updates for each - write to each supported tag type and verify the result - disconnect cleanly ## Non-Goals The following are intentionally deferred: - AlarmMgr support - Secure SuiteLink V3 support - automatic reconnect - batched subscription optimization - broad type support beyond the four proven primitive classes - production hardening for all undocumented server variants ## Recommended Next Step Create a detailed implementation plan that: - establishes the project structure - defines the test-first workflow - identifies capture-driven fixtures needed for codec tests - breaks the implementation into transport, codec, session, and API slices