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,301 @@
# Component: Protocol
## Purpose
Defines the gRPC protocol specification for communication between the LmxProxy Client and Host, including the proto file definition, code-first contracts, message schemas, value type system, and quality codes. The authoritative specification is `docs/lmxproxy_updates.md`.
## Location
- `src/ZB.MOM.WW.LmxProxy.Host/Grpc/Protos/scada.proto` — proto file (Host, proto-generated).
- `src/ZB.MOM.WW.LmxProxy.Client/Domain/ScadaContracts.cs` — code-first contracts (Client, protobuf-net.Grpc).
- `docs/lmxproxy_updates.md` — authoritative protocol specification.
- `docs/lmxproxy_protocol.md` — legacy v1 protocol documentation (superseded).
## Responsibilities
- Define the gRPC service interface (`scada.ScadaService`) and all message types.
- Ensure wire compatibility between the Host's proto-generated code and the Client's code-first contracts.
- Specify the VTQ data model: `TypedValue` for values, `QualityCode` for quality.
- Document OPC UA-aligned quality codes filtered to AVEVA System Platform usage.
## 1. Service Definition
Service: `scada.ScadaService` (gRPC package: `scada`)
| RPC | Request | Response | Type |
|-----|---------|----------|------|
| Connect | ConnectRequest | ConnectResponse | Unary |
| Disconnect | DisconnectRequest | DisconnectResponse | Unary |
| GetConnectionState | GetConnectionStateRequest | GetConnectionStateResponse | Unary |
| Read | ReadRequest | ReadResponse | Unary |
| ReadBatch | ReadBatchRequest | ReadBatchResponse | Unary |
| Write | WriteRequest | WriteResponse | Unary |
| WriteBatch | WriteBatchRequest | WriteBatchResponse | Unary |
| WriteBatchAndWait | WriteBatchAndWaitRequest | WriteBatchAndWaitResponse | Unary |
| Subscribe | SubscribeRequest | stream VtqMessage | Server streaming |
| CheckApiKey | CheckApiKeyRequest | CheckApiKeyResponse | Unary |
## 2. Value Type System (TypedValue)
Values are transmitted in their native protobuf types via a `TypedValue` oneof. No string serialization or parsing heuristics are used.
```
TypedValue {
oneof value {
bool bool_value = 1
int32 int32_value = 2
int64 int64_value = 3
float float_value = 4
double double_value = 5
string string_value = 6
bytes bytes_value = 7
int64 datetime_value = 8 // UTC DateTime.Ticks (100ns intervals since 0001-01-01)
ArrayValue array_value = 9 // typed arrays
}
}
```
`ArrayValue` contains typed repeated fields via oneof: `BoolArray`, `Int32Array`, `Int64Array`, `FloatArray`, `DoubleArray`, `StringArray`. Each contains a `repeated` field of the corresponding primitive.
### 2.1 Null Handling
- Null is represented by an unset `oneof` (no field selected in `TypedValue`).
- A null or missing VTQ message is treated as Bad quality with null value and current UTC timestamp.
### 2.2 Type Mapping from Internal Tag Model
| Tag Data Type | TypedValue Field |
|---------------|-----------------|
| `bool` | `bool_value` |
| `int32` | `int32_value` |
| `int64` | `int64_value` |
| `float` | `float_value` |
| `double` | `double_value` |
| `string` | `string_value` |
| `byte[]` | `bytes_value` |
| `DateTime` | `datetime_value` (UTC Ticks as int64) |
| `float[]` | `array_value.float_values` |
| `int32[]` | `array_value.int32_values` |
| Other arrays | Corresponding `ArrayValue` field |
## 3. Quality System (QualityCode)
Quality is a structured message with an OPC UA-compatible numeric status code and a human-readable symbolic name:
```
QualityCode {
uint32 status_code = 1 // OPC UA-compatible numeric status code
string symbolic_name = 2 // Human-readable name (e.g., "Good", "BadSensorFailure")
}
```
### 3.1 Category Extraction
Category derived from high bits via `(statusCode & 0xC0000000)`:
- `0x00000000` = Good
- `0x40000000` = Uncertain
- `0x80000000` = Bad
```csharp
public static bool IsGood(uint statusCode) => (statusCode & 0xC0000000) == 0x00000000;
public static bool IsBad(uint statusCode) => (statusCode & 0xC0000000) == 0x80000000;
```
### 3.2 Supported Quality Codes
Filtered to codes actively used by AVEVA System Platform, InTouch, and OI Server/DAServer (per AVEVA Tech Note TN1305):
**Good Quality:**
| Symbolic Name | OPC UA Status Code | AVEVA OPC DA Hex | Description |
|--------------|-------------------|------------------|-------------|
| `Good` | `0x00000000` | `0x00C0` | Value is reliable, non-specific |
| `GoodLocalOverride` | `0x00D80000` | `0x00D8` | Manually overridden; input disconnected |
**Uncertain Quality:**
| Symbolic Name | OPC UA Status Code | AVEVA OPC DA Hex | Description |
|--------------|-------------------|------------------|-------------|
| `UncertainLastUsableValue` | `0x40900000` | `0x0044` | External source stopped writing; value is stale |
| `UncertainSensorNotAccurate` | `0x42390000` | `0x0050` | Sensor out of calibration or clamped |
| `UncertainEngineeringUnitsExceeded` | `0x40540000` | `0x0054` | Outside defined engineering limits |
| `UncertainSubNormal` | `0x40580000` | `0x0058` | Derived from insufficient good sources |
**Bad Quality:**
| Symbolic Name | OPC UA Status Code | AVEVA OPC DA Hex | Description |
|--------------|-------------------|------------------|-------------|
| `Bad` | `0x80000000` | `0x0000` | Non-specific bad; value not useful |
| `BadConfigurationError` | `0x80040000` | `0x0004` | Server config problem (e.g., item deleted) |
| `BadNotConnected` | `0x808A0000` | `0x0008` | Input not logically connected to source |
| `BadDeviceFailure` | `0x806B0000` | `0x000C` | Device failure detected |
| `BadSensorFailure` | `0x806D0000` | `0x0010` | Sensor failure detected |
| `BadLastKnownValue` | `0x80050000` | `0x0014` | Comm failed; last known value available |
| `BadCommunicationFailure` | `0x80050000` | `0x0018` | Comm failed; no last known value |
| `BadOutOfService` | `0x808F0000` | `0x001C` | Block off-scan/locked; item inactive |
| `BadWaitingForInitialData` | `0x80320000` | — | Initializing; OI Server establishing communication |
**Notes:**
- AVEVA OPC DA quality codes use a 16-bit structure: 2 bits major (Good/Bad/Uncertain), 4 bits minor (sub-status), 2 bits limit (Not Limited, Low, High, Constant). The OPC UA status codes above are the standard UA equivalents.
- The limit bits are appended to any quality code. For example, `Good + High Limited` = `0x00C2` in OPC DA. In OPC UA, limits are conveyed via separate status code bits but the base code remains the same.
### 3.3 Error Condition Mapping
| Scenario | Quality |
|----------|---------|
| Normal read | `Good` (`0x00000000`) |
| Tag not found | `BadConfigurationError` (`0x80040000`) |
| Tag read exception / comms loss | `BadCommunicationFailure` (`0x80050000`) |
| Sensor failure | `BadSensorFailure` (`0x806D0000`) |
| Device failure | `BadDeviceFailure` (`0x806B0000`) |
| Stale value | `UncertainLastUsableValue` (`0x40900000`) |
| Block off-scan / disabled | `BadOutOfService` (`0x808F0000`) |
| Local override active | `GoodLocalOverride` (`0x00D80000`) |
| Initializing / waiting for first value | `BadWaitingForInitialData` (`0x80320000`) |
| Write to read-only tag | `WriteResult.success=false`, message indicates read-only |
| Type mismatch on write | `WriteResult.success=false`, message indicates type mismatch |
## 4. Message Schemas
### 4.1 VtqMessage
The core data type for tag value transport:
| Field | Proto Type | Order | Description |
|-------|-----------|-------|-------------|
| tag | string | 1 | Tag address |
| value | TypedValue | 2 | Typed value (native protobuf types) |
| timestamp_utc_ticks | int64 | 3 | UTC DateTime.Ticks (100ns intervals since 0001-01-01) |
| quality | QualityCode | 4 | Structured quality with status code and symbolic name |
A null or missing VTQ message is treated as Bad quality with null value and current UTC timestamp.
### 4.2 Connection Messages
**ConnectRequest**: `client_id` (string), `api_key` (string)
**ConnectResponse**: `success` (bool), `message` (string), `session_id` (string — 32-char hex GUID)
**DisconnectRequest**: `session_id` (string)
**DisconnectResponse**: `success` (bool), `message` (string)
**GetConnectionStateRequest**: `session_id` (string)
**GetConnectionStateResponse**: `is_connected` (bool), `client_id` (string), `connected_since_utc_ticks` (int64)
### 4.3 Read Messages
**ReadRequest**: `session_id` (string), `tag` (string)
**ReadResponse**: `success` (bool), `message` (string), `vtq` (VtqMessage)
**ReadBatchRequest**: `session_id` (string), `tags` (repeated string)
**ReadBatchResponse**: `success` (bool), `message` (string), `vtqs` (repeated VtqMessage)
### 4.4 Write Messages
**WriteRequest**: `session_id` (string), `tag` (string), `value` (TypedValue)
**WriteResponse**: `success` (bool), `message` (string)
**WriteItem**: `tag` (string), `value` (TypedValue)
**WriteResult**: `tag` (string), `success` (bool), `message` (string)
**WriteBatchRequest**: `session_id` (string), `items` (repeated WriteItem)
**WriteBatchResponse**: `success` (bool), `message` (string), `results` (repeated WriteResult)
### 4.5 WriteBatchAndWait Messages
**WriteBatchAndWaitRequest**:
- `session_id` (string)
- `items` (repeated WriteItem) — values to write
- `flag_tag` (string) — tag to poll after writes
- `flag_value` (TypedValue) — expected value (type-aware comparison)
- `timeout_ms` (int32) — max wait time (default 5000ms if ≤ 0)
- `poll_interval_ms` (int32) — polling interval (default 100ms if ≤ 0)
**WriteBatchAndWaitResponse**:
- `success` (bool)
- `message` (string)
- `write_results` (repeated WriteResult)
- `flag_reached` (bool) — whether the flag value was matched
- `elapsed_ms` (int32) — total elapsed time
**Behavior:**
1. All writes execute first. If any write fails, returns immediately with `success=false`.
2. If writes succeed, polls `flag_tag` at `poll_interval_ms` intervals.
3. Uses type-aware `TypedValueEquals()` comparison (see Section 4.5.1).
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).
#### 4.5.1 Flag Comparison Rules
Type-aware comparison via `TypedValueEquals()`:
- Both values must have the same `oneof` case (same type). Mismatched types are never equal.
- Numeric comparison uses the native type's equality (no floating-point string round-trip issues).
- String comparison is case-sensitive.
- Bool comparison is direct equality.
- Null (unset `oneof`) equals null. Null does not equal any set value.
- Array comparison: element-by-element equality, same length required.
- `datetime_value` compared as `int64` equality (tick-level precision).
### 4.6 Subscription Messages
**SubscribeRequest**: `session_id` (string), `tags` (repeated string), `sampling_ms` (int32)
Response: streamed `VtqMessage` items.
### 4.7 API Key Messages
**CheckApiKeyRequest**: `api_key` (string)
**CheckApiKeyResponse**: `is_valid` (bool), `message` (string)
## 5. Dual gRPC Stack Compatibility
The Host and Client use different gRPC implementations:
| Aspect | Host | Client |
|--------|------|--------|
| Stack | Grpc.Core (C-core) | Grpc.Net.Client |
| Contract | Proto file (`scada.proto`) + Grpc.Tools codegen | Code-first (`[ServiceContract]`, `[DataContract]`) via protobuf-net.Grpc |
| Runtime | .NET Framework 4.8 | .NET 10 |
Both target `scada.ScadaService` and produce identical wire format. Field ordering in `[DataMember(Order = N)]` matches proto field numbers.
## 6. V1 Legacy Protocol
The current codebase implements the v1 protocol. The following describes v1 behavior that will be replaced during migration to v2.
### 6.1 V1 Value Encoding
All values transmitted as strings:
- Write direction: server parses string values in order: bool → int → long → double → DateTime → raw string.
- Read direction: server serializes via `.ToString()` (bool → lowercase, DateTime → ISO-8601, arrays → JSON).
- Client parses: double → bool → null (empty string) → raw string.
### 6.2 V1 Quality
Three-state string quality (`"Good"`, `"Uncertain"`, `"Bad"`, case-insensitive). OPC UA numeric ranges: ≥192 = Good, 64191 = Uncertain, <64 = Bad.
### 6.3 V1 → V2 Field Changes
| Message | Field | V1 Type | V2 Type |
|---------|-------|---------|---------|
| VtqMessage | value | string | TypedValue |
| VtqMessage | quality | string | QualityCode |
| WriteRequest | value | string | TypedValue |
| WriteItem | value | string | TypedValue |
| WriteBatchAndWaitRequest | flag_value | string | TypedValue |
All RPC signatures remain unchanged. Only value and quality fields change type.
### 6.4 Migration Strategy
Clean break — no backward compatibility layer. All clients and servers updated simultaneously. This is appropriate because LmxProxy is an internal protocol with a small, controlled client count. Dual-format support adds complexity with no long-term benefit.
## Dependencies
- **Grpc.Core** + **Grpc.Tools** — proto compilation and server hosting (Host).
- **protobuf-net.Grpc** — code-first contracts (Client).
- **Grpc.Net.Client** — HTTP/2 transport (Client).
## Interactions
- **GrpcServer** implements the service defined by this protocol.
- **Client** consumes the service defined by this protocol.
- **MxAccessClient** is the backend that executes the operations requested via the protocol.