docs(dcl): update protocol and type mapping docs to reflect v2 TypedValue and SDK integration

This commit is contained in:
Joseph Doherty
2026-03-22 14:53:41 -04:00
parent ce3942990e
commit af7335f9e2
2 changed files with 83 additions and 35 deletions

View File

@@ -60,10 +60,30 @@ Both protocols produce the same value tuple consumed by Instance Actors. Before
| Concept | ScadaLink Design | LmxProxy Wire Format | Local Type |
|---|---|---|---|
| Value container | `TagValue(Value, Quality, Timestamp)` | `VtqMessage { Tag, Value, TimestampUtcTicks, Quality }` | `LmxVtq(Value, TimestampUtc, Quality)` — readonly record struct |
| Quality | `QualityCode` enum: Good / Bad / Uncertain | String: `"Good"` / `"Uncertain"` / `"Bad"` | `LmxQuality` enum: Good / Uncertain / Bad |
| Value container | `TagValue(Value, Quality, Timestamp)` | `VtqMessage { Tag, TypedValue, TimestampUtcTicks, QualityCode }` | `LmxVtq(Value, TimestampUtc, Quality)` — readonly record struct |
| Quality | `QualityCode` enum: Good / Bad / Uncertain | `QualityCode` enum (OPC-style byte values) | Mapped via `IsGood()`/`IsUncertain()`/`IsBad()` extensions |
| Timestamp | `DateTimeOffset` (UTC) | `int64` (DateTime.Ticks, UTC) | `DateTime` (UTC) |
| Value type | `object?` | `string` (parsed by client to double, bool, or string) | `object?` |
| Value type | `object?` | `TypedValue` (protobuf oneof: bool, int32, int64, float, double, string, datetime, array) | `object?` (native typed from SDK's `Vtq.Value`) |
### Value Serialization
**Inbound (reads/subscriptions)**: The LmxProxy SDK returns `Vtq(object? Value, DateTime Timestamp, Quality Quality)`. The adapter's `NormalizeValue` converts complex types (`ArrayValue`, raw arrays) to display strings before passing them into the ScadaLink system. Scalar types (`bool`, `int`, `double`, `string`, `DateTime`) pass through unchanged.
**Outbound (writes)**: The adapter's `ToTypedValue` converts `object?` values to the SDK's `TypedValue` for native typed writes:
| Source Type | TypedValue Variant |
|---|---|
| `bool` | `BoolValue` |
| `int` | `Int32Value` |
| `long` | `Int64Value` |
| `float` | `FloatValue` |
| `double` | `DoubleValue` |
| `string` | `StringValue` |
| `DateTime` | `DatetimeValue` (UTC ticks) |
| `null` | `StringValue` (empty) |
| fallback | `StringValue` (`.ToString()`) |
**Array normalization**: `ValueFormatter.FormatDisplayValue` uses reflection to extract typed array contents from LmxProxy `ArrayValue` objects and formats them as comma-separated strings. This ensures downstream code (Instance Actors, debug views, gRPC streaming) never sees opaque SDK types.
## Supported Protocols
@@ -76,10 +96,10 @@ Both protocols produce the same value tuple consumed by Instance Actors. Before
### LmxProxy (Custom Protocol)
LmxProxy is a gRPC-based protocol for communicating with LMX data servers. The DCL includes its own proto-generated gRPC client (`RealLmxProxyClient`) — no external SDK dependency.
LmxProxy is a gRPC-based protocol for communicating with LMX data servers. The DCL uses the real `ZB.MOM.WW.LmxProxy.Client` SDK library via project reference. `RealLmxProxyClient` is a thin adapter wrapper around the SDK client for testability — it implements a local `ILmxProxyClient` interface while delegating to the SDK. The SDK handles gRPC channel management, retry policies (Polly), keep-alive, and TLS.
**Transport & Connection**:
- gRPC over HTTP/2, using proto-generated client stubs from `scada.proto` (service: `scada.ScadaService`). Pre-generated C# files are checked into `Adapters/LmxProxyGrpc/` to avoid running `protoc` in Docker (ARM64 compatibility).
- gRPC over HTTP/2 via the SDK's managed channel.
- Default port: **50051**.
- Session-based: `Connect` RPC returns a `SessionId` used for all subsequent operations.
- Keep-alive: Managed by the LmxProxy server's session timeout. The DCL reconnect cycle handles session loss.
@@ -91,16 +111,16 @@ LmxProxy is a gRPC-based protocol for communicating with LMX data servers. The D
**Subscriptions**:
- Server-streaming gRPC (`Subscribe` RPC returns `stream VtqMessage`).
- Configurable sampling interval (default: 0 = on-change).
- Wire format: `VtqMessage { tag, value (string), timestamp_utc_ticks (int64), quality (string: "Good"/"Uncertain"/"Bad") }`.
- Wire format: `VtqMessage` with `TypedValue` (protobuf oneof) for value, `QualityCode` enum for quality — see [lmxproxy_protocol.md](lmxproxy_protocol.md) for wire details.
- Subscription lifetime managed by `CancellationTokenSource` — cancellation stops the streaming RPC.
**Client Implementation** (`RealLmxProxyClient`):
- Uses `Google.Protobuf` + `Grpc.Net.Client` (standard proto-generated stubs, no protobuf-net runtime IL emit).
- Thin adapter over the `ZB.MOM.WW.LmxProxy.Client` SDK. Implements `ILmxProxyClient` for testability.
- `ILmxProxyClientFactory` creates instances configured with host, port, and API key.
- Value conversion: string values from `VtqMessage` are parsed to `double`, `bool`, or left as `string`.
- Quality mapping: `"Good"``LmxQuality.Good`, `"Uncertain"``LmxQuality.Uncertain`, else `LmxQuality.Bad`.
- Read values arrive as native typed objects from the SDK's `Vtq.Value` — no string parsing needed.
- Quality mapping: SDK's `Quality` enum (OPC-style byte values) mapped to ScadaLink's `QualityCode` via `IsGood()`/`IsUncertain()`/`IsBad()` extension methods.
**Proto Source**: The `.proto` file originates from the LmxProxy server repository (`lmx/Proxy/Grpc/Protos/scada.proto` in ScadaBridge). The C# stubs are pre-generated and stored at `Adapters/LmxProxyGrpc/`.
**Proto Source**: The `.proto` file originates from the LmxProxy server repository (`lmx/Proxy/Grpc/Protos/scada.proto` in ScadaBridge). The SDK provides the generated client stubs.
**Test Infrastructure**: The `infra/lmxfakeproxy/` project provides a fake LmxProxy server that bridges to the OPC UA test server. It implements the full `scada.ScadaService` proto, enabling end-to-end testing of `RealLmxProxyClient` without a Windows LmxProxy deployment. See [test_infra_lmxfakeproxy.md](../test_infra/test_infra_lmxfakeproxy.md) for setup.

View File

@@ -1,5 +1,7 @@
# LmxProxy Protocol Specification
> **Note:** This specification reflects the v2 protocol with native `TypedValue` support. The original v1 string-based protocol (string values, string quality) has been replaced.
The LmxProxy protocol is a gRPC-based SCADA read/write interface for bridging ScadaLink's Data Connection Layer to devices via an intermediary proxy server (LmxProxy). The proxy translates LmxProxy protocol operations into backend device calls (e.g., OPC UA). All communication uses HTTP/2 gRPC with Protocol Buffers.
## Service Definition
@@ -106,36 +108,60 @@ The core data structure for all read and subscription results:
```
VtqMessage {
tag: string // Tag address
value: string // Value encoded as string (see Value Encoding)
value: TypedValue // Native typed value (protobuf oneof)
timestamp_utc_ticks: int64 // UTC DateTime.Ticks (100ns intervals since 0001-01-01)
quality: string // "Good", "Uncertain", or "Bad"
quality: QualityCode // OPC UA status code + symbolic name
}
```
### Value Encoding
### TypedValue
All values are transmitted as strings on the wire. Both client and server use the same parsing order:
Values are transmitted as native types via a protobuf `oneof`:
| Wire String | Parsed Type | Example |
|-------------|------------|---------|
| Numeric (double-parseable) | `double` | `"42.5"``42.5` |
| `"true"` / `"false"` (case-insensitive) | `bool` | `"True"``true` |
| Everything else | `string` | `"Running"` `"Running"` |
| Empty string | `null` | `""` `null` |
| Oneof Variant | Proto Type | .NET Type |
|---|---|---|
| `bool_value` | `bool` | `bool` |
| `int32_value` | `int32` | `int` |
| `int64_value` | `int64` | `long` |
| `float_value` | `float` | `float` |
| `double_value` | `double` | `double` |
| `string_value` | `string` | `string` |
| `bytes_value` | `bytes` | `byte[]` |
| `datetime_value` | `int64` | `DateTime` (UTC ticks) |
| `array_value` | `ArrayValue` | See below |
For write operations, values are converted to strings via `.ToString()` before transmission.
### ArrayValue
Arrays and lists are JSON-serialized (e.g., `[1,2,3]`).
`ArrayValue` contains typed sub-arrays via a protobuf `oneof`:
| Sub-array | Element Type |
|---|---|
| `BoolArray` | `repeated bool` |
| `Int32Array` | `repeated int32` |
| `Int64Array` | `repeated int64` |
| `FloatArray` | `repeated float` |
| `DoubleArray` | `repeated double` |
| `StringArray` | `repeated string` |
> **Note:** `DateTime` arrays are not natively supported in the proto — they are serialized as `Int64Array` (UTC ticks) by the Host.
The ScadaLink adapter normalizes `ArrayValue` objects to comma-separated display strings at the adapter boundary (see [Component-DataConnectionLayer.md](Component-DataConnectionLayer.md#value-serialization)).
### Value Encoding (v1 — deprecated)
The v1 protocol transmitted all values as strings with client-side parsing (`double.TryParse`, `bool.TryParse`). This has been replaced by native `TypedValue`. The v1 heuristics are no longer used.
### Quality Codes
Quality is transmitted as a case-insensitive string:
Quality is transmitted as a `QualityCode` enum with OPC UA status code semantics:
| Wire Value | Meaning | OPC UA Status Code |
|-----------|---------|-------------------|
| `"Good"` | Value is reliable | `0x00000000` (StatusCode == 0) |
| `"Uncertain"` | Value may not be current | Non-zero, high bit clear |
| `"Bad"` | Value is unreliable or unavailable | High bit set (`0x80000000`) |
| QualityCode | Meaning | OPC UA Mapping |
|---|---|---|
| Good | Value is reliable | StatusCode high bits clear |
| Uncertain | Value may not be current | Non-zero, high bit clear |
| Bad | Value is unreliable or unavailable | High bit set (`0x80000000`) |
The SDK provides `IsGood()`, `IsUncertain()`, and `IsBad()` extension methods on the `Quality` enum. The adapter maps these to ScadaLink's `QualityCode`.
A null or missing VTQ message is treated as Bad quality with null value and current UTC timestamp.
@@ -187,7 +213,7 @@ Batch reads are **partially successful** — individual tags may have Bad qualit
WriteRequest {
session_id: string
tag: string
value: string // Value as string (parsed server-side)
value: TypedValue // Native typed value (see TypedValue)
}
WriteResponse {
@@ -196,12 +222,14 @@ WriteResponse {
}
```
The client adapter's `ToTypedValue` method converts `object?` values to the appropriate `TypedValue` variant before transmission. See [Component-DataConnectionLayer.md](Component-DataConnectionLayer.md#value-serialization) for the mapping table.
### WriteBatch (Multiple Tags)
```
WriteItem {
tag: string
value: string
value: TypedValue
}
WriteResult {
@@ -231,9 +259,9 @@ A compound operation: write values, then poll a flag tag until it matches an exp
```
WriteBatchAndWaitRequest {
session_id: string
items: repeated WriteItem // Values to write
items: repeated WriteItem // Values to write (TypedValue)
flag_tag: string // Tag to poll after writes
flag_value: string // Expected value (string comparison)
flag_value: TypedValue // Expected value (typed comparison)
timeout_ms: int32 // Timeout in ms (default 5000 if ≤ 0)
poll_interval_ms: int32 // Poll interval in ms (default 100 if ≤ 0)
}
@@ -250,7 +278,7 @@ WriteBatchAndWaitResponse {
**Behavior:**
1. All writes execute first. If any write fails, the operation returns immediately with `success=false`.
2. If writes succeed, polls `flag_tag` at `poll_interval_ms` intervals.
3. Compares `readResult.Value?.ToString() == flag_value` (case-sensitive string comparison).
3. Compares the read result's `TypedValue` against `flag_value`.
4. If flag matches before timeout: `success=true`, `flag_reached=true`.
5. If timeout expires: `success=true`, `flag_reached=false` (timeout is not an error).