docs: add LmxProxy requirements documentation with v2 protocol as authoritative design

Generate high-level requirements and 10 component documents derived from source code
and protocol specs. Uses lmxproxy_updates.md (v2 TypedValue/QualityCode) as the source
of truth, with v1 string-based encoding documented as legacy context.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-03-21 22:38:11 -04:00
parent 970d0a5cb3
commit 683aea0fbe
12 changed files with 1702 additions and 0 deletions

View File

@@ -0,0 +1,200 @@
# Component: Client
## Purpose
A .NET 10 class library providing a typed gRPC client for consuming the LmxProxy service. Used by ScadaLink's Data Connection Layer to connect to AVEVA System Platform via the LmxProxy Host.
## Location
`src/ZB.MOM.WW.LmxProxy.Client/` — all files in this project.
Key files:
- `ILmxProxyClient.cs` — public interface.
- `LmxProxyClient.cs` — main implementation (partial class across multiple files).
- `LmxProxyClientBuilder.cs` — fluent builder for client construction.
- `ServiceCollectionExtensions.cs` — DI integration and options classes.
- `ILmxProxyClientFactory.cs` — factory interface and implementation.
- `StreamingExtensions.cs` — batch and parallel streaming helpers.
- `Domain/ScadaContracts.cs` — code-first gRPC contracts.
- `Security/GrpcChannelFactory.cs` — TLS channel creation.
## Responsibilities
- Connect to and communicate with the LmxProxy Host gRPC service.
- Manage session lifecycle (connect, keep-alive, disconnect).
- Execute read, write, and subscribe operations with retry and concurrency control.
- Provide a fluent builder and DI integration for configuration.
- Track client-side performance metrics.
- Support TLS and mutual TLS connections.
## 1. Public Interface (ILmxProxyClient)
| Method | Description |
|--------|-------------|
| `ConnectAsync(ct)` | Establish gRPC channel and session |
| `DisconnectAsync()` | Graceful disconnect |
| `IsConnectedAsync()` | Thread-safe connection state check |
| `ReadAsync(address, ct)` | Read single tag, returns Vtq |
| `ReadBatchAsync(addresses, ct)` | Read multiple tags, returns dictionary |
| `WriteAsync(address, value, ct)` | Write single tag value |
| `WriteBatchAsync(values, ct)` | Write multiple tag values |
| `SubscribeAsync(addresses, onUpdate, onStreamError, ct)` | Subscribe to tag updates with value and error callbacks |
| `GetMetrics()` | Return operation counts, errors, latency stats |
| `DefaultTimeout` | Configurable timeout (default 30s, range 1s10min) |
Implements `IDisposable` and `IAsyncDisposable`.
## 2. Connection Management
### 2.1 Connect
`ConnectAsync()`:
1. Creates a gRPC channel via `GrpcChannelFactory` (HTTP or HTTPS based on TLS config).
2. Creates a `protobuf-net.Grpc` client for `IScadaService`.
3. Calls the `Connect` RPC with a client ID (format: `ScadaBridge-{guid}`) and optional API key.
4. Stores the returned session ID.
5. Starts the keep-alive timer.
### 2.2 Keep-Alive
- Timer-based ping every **30 seconds** (hardcoded).
- Sends a lightweight `GetConnectionState` RPC.
- On failure: stops the timer, marks disconnected, triggers subscription cleanup.
### 2.3 Disconnect
`DisconnectAsync()`:
1. Stops keep-alive timer.
2. Calls `Disconnect` RPC.
3. Clears session ID.
4. Disposes gRPC channel.
### 2.4 Connection State
`IsConnected` property: `!_disposed && _isConnected && !string.IsNullOrEmpty(_sessionId)`.
## 3. Builder Pattern (LmxProxyClientBuilder)
| Method | Default | Constraint |
|--------|---------|-----------|
| `WithHost(string)` | Required | Non-null/non-empty |
| `WithPort(int)` | 5050 | 165535 |
| `WithApiKey(string?)` | null | Optional |
| `WithTimeout(TimeSpan)` | 30 seconds | > 0 and ≤ 10 minutes |
| `WithLogger(ILogger)` | NullLogger | Optional |
| `WithSslCredentials(string?)` | Disabled | Optional cert path |
| `WithTlsConfiguration(ClientTlsConfiguration)` | null | Full TLS config |
| `WithRetryPolicy(int, TimeSpan)` | 3 attempts, 1s delay | maxAttempts > 0, delay > 0 |
| `WithMetrics()` | Disabled | Enables metric collection |
| `WithCorrelationIdHeader(string)` | null | Custom header name |
## 4. Retry Policy
Polly-based exponential backoff:
- Default: **3 attempts** with **1-second** initial delay.
- Backoff sequence: `delay * 2^(retryAttempt - 1)` → 1s, 2s, 4s.
- Transient errors retried: `Unavailable`, `DeadlineExceeded`, `ResourceExhausted`, `Aborted`.
- Each retry is logged with correlation ID at Warning level.
## 5. Subscription
### 5.1 Subscribe API
`SubscribeAsync(addresses, onUpdate, onStreamError, ct)` returns an `ISubscription`:
- Calls the `Subscribe` RPC (server streaming) with the tag list and default sampling interval (**1000ms**).
- Processes streamed `VtqMessage` items asynchronously, invoking the `onUpdate(tag, vtq)` callback for each.
- On stream termination (server disconnect, gRPC error, or connection drop), invokes the `onStreamError` callback exactly once.
- On stream error, the client immediately nullifies its session ID, causing `IsConnected` to return `false`. This triggers the DCL adapter's `Disconnected` event and reconnection cycle.
- Errors are logged per-subscription.
### 5.2 ISubscription
- `Dispose()` — synchronous disposal with **5-second** timeout.
- Automatic callback on disposal for cleanup.
## 6. DI Integration
### 6.1 Service Collection Extensions
| Method | Lifetime | Description |
|--------|----------|-------------|
| `AddLmxProxyClient(IConfiguration)` | Singleton | Bind `LmxProxy` config section |
| `AddLmxProxyClient(IConfiguration, string)` | Singleton | Bind named config section |
| `AddLmxProxyClient(Action<Builder>)` | Singleton | Builder action |
| `AddScopedLmxProxyClient(IConfiguration)` | Scoped | Per-scope lifetime |
| `AddNamedLmxProxyClient(string, Action<Builder>)` | Keyed singleton | Named/keyed registration |
### 6.2 Configuration Options (LmxProxyClientOptions)
Bound from `appsettings.json`:
| Setting | Default | Description |
|---------|---------|-------------|
| Host | `localhost` | Server hostname |
| Port | 5050 | Server port |
| ApiKey | null | API key |
| Timeout | 30 seconds | Operation timeout |
| UseSsl | false | Enable TLS |
| CertificatePath | null | SSL certificate path |
| EnableMetrics | false | Enable client metrics |
| CorrelationIdHeader | null | Custom correlation header |
| Retry:MaxAttempts | 3 | Retry attempts |
| Retry:Delay | 1 second | Initial retry delay |
### 6.3 Factory Pattern
`ILmxProxyClientFactory` creates configured clients:
- `CreateClient()` — uses default `LmxProxy` config section.
- `CreateClient(string)` — uses named config section.
- `CreateClient(Action<Builder>)` — uses builder action.
Registered as singleton in DI.
## 7. Streaming Extensions
Helper methods for large-scale batch operations:
| Method | Default Batch Size | Description |
|--------|--------------------|-------------|
| `ReadStreamAsync` | 100 | Batched reads, 2 retries per batch, stops after 3 consecutive errors. Returns `IAsyncEnumerable<KeyValuePair<string, Vtq>>`. |
| `WriteStreamAsync` | 100 | Batched writes from async enumerable input. Returns total count written. |
| `ProcessInParallelAsync` | — | Parallel processing with max concurrency of **4** (configurable). Semaphore-based rate limiting. |
| `SubscribeStreamAsync` | — | Wraps callback-based subscription into `IAsyncEnumerable<Vtq>` via `System.Threading.Channels`. |
## 8. Client Metrics
When metrics are enabled (`WithMetrics()`):
- Per-operation tracking: counts, error counts, latency.
- Rolling buffer of **1000** latency samples per operation (prevents memory growth).
- Snapshot via `GetMetrics()` returns: `{op}_count`, `{op}_errors`, `{op}_avg_latency_ms`, `{op}_p95_latency_ms`, `{op}_p99_latency_ms`.
## 9. Value and Quality Handling
### 9.1 Values (TypedValue)
Read responses and subscription updates return values as `TypedValue` (protobuf oneof). The client extracts the value directly from the appropriate oneof field (e.g., `vtq.Value.DoubleValue`, `vtq.Value.BoolValue`). Write operations construct `TypedValue` with the correct oneof case for the value's native type. No string serialization or parsing is needed.
### 9.2 Quality (QualityCode)
Quality is received as a `QualityCode` message. Category checks use bitmask: `IsGood = (statusCode & 0xC0000000) == 0x00000000`, `IsBad = (statusCode & 0xC0000000) == 0x80000000`. The `symbolic_name` field provides human-readable quality for logging and display.
### 9.3 Current Implementation (V1 Legacy)
The current codebase still uses v1 string-based encoding. During v2 migration, the following will be removed:
- `ConvertToVtq()` — parses string values via heuristic (double → bool → null → raw string).
- `ConvertToString()` — serializes values via `.ToString()`.
## Dependencies
- **protobuf-net.Grpc** — code-first gRPC client.
- **Grpc.Net.Client** — HTTP/2 gRPC transport.
- **Polly** — retry policies.
- **Microsoft.Extensions.DependencyInjection** — DI integration.
- **Microsoft.Extensions.Configuration** — options binding.
- **Microsoft.Extensions.Logging** — logging abstraction.
## Interactions
- **ScadaLink Data Connection Layer** consumes the client library via `ILmxProxyClient`.
- **Protocol** — the client uses code-first contracts (`IScadaService`) that are wire-compatible with the Host's proto-generated service.
- **Security** — `GrpcChannelFactory` creates TLS-configured channels matching the Host's TLS configuration.