# 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, 64–191 = 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.