Files
suitelinkclient/docs/plans/2026-03-16-suitelink-client-design.md
2026-03-16 14:43:31 -04:00

315 lines
7.5 KiB
Markdown

# 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<SubscriptionHandle> SubscribeAsync(
string itemName,
Action<SuiteLinkTagUpdate> onUpdate,
CancellationToken ct = default);
Task<SuiteLinkTagUpdate> 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