Files
scadalink-design/deprecated/lmxproxy/docs/lmxproxy_updates.md
Joseph Doherty 9dccf8e72f deprecate(lmxproxy): move all LmxProxy code, tests, and docs to deprecated/
LmxProxy is no longer needed. Moved the entire lmxproxy/ workspace, DCL
adapter files, and related docs to deprecated/. Removed LmxProxy registration
from DataConnectionFactory, project reference from DCL, protocol option from
UI, and cleaned up all requirement docs.
2026-04-08 15:56:23 -04:00

647 lines
26 KiB
Markdown

# LmxProxy Protocol v2 — OPC UA Alignment
This document specifies all changes to the LmxProxy gRPC protocol to align it with OPC UA semantics. The changes replace string-serialized values with typed values and simple quality strings with OPC UA-style status codes.
**Baseline:** `lmxproxy_protocol.md` (v1 protocol spec)
**Strategy:** Clean break — all clients and servers updated simultaneously. No backward compatibility layer.
---
## 1. Change Summary
| Message / Field | v1 Type | v2 Type | Breaking? |
|-----------------|---------|---------|-----------|
| `VtqMessage.value` | `string` | `TypedValue` | Yes |
| `VtqMessage.quality` | `string` | `QualityCode` | Yes |
| `WriteRequest.value` | `string` | `TypedValue` | Yes |
| `WriteItem.value` | `string` | `TypedValue` | Yes |
| `WriteBatchAndWaitRequest.flag_value` | `string` | `TypedValue` | Yes |
**Unchanged messages:** `ConnectRequest`, `ConnectResponse`, `DisconnectRequest`, `DisconnectResponse`, `GetConnectionStateRequest`, `GetConnectionStateResponse`, `CheckApiKeyRequest`, `CheckApiKeyResponse`, `ReadRequest`, `ReadBatchRequest`, `SubscribeRequest`, `WriteResponse`, `WriteBatchResponse`, `WriteBatchAndWaitResponse`, `WriteResult`.
**Unchanged RPCs:** The `ScadaService` definition is identical — same RPC names, same request/response pairing. Only the internal message shapes change.
---
## 2. Complete Updated Proto File
```protobuf
syntax = "proto3";
package scada;
// ============================================================
// Service Definition (unchanged)
// ============================================================
service ScadaService {
rpc Connect(ConnectRequest) returns (ConnectResponse);
rpc Disconnect(DisconnectRequest) returns (DisconnectResponse);
rpc GetConnectionState(GetConnectionStateRequest) returns (GetConnectionStateResponse);
rpc Read(ReadRequest) returns (ReadResponse);
rpc ReadBatch(ReadBatchRequest) returns (ReadBatchResponse);
rpc Write(WriteRequest) returns (WriteResponse);
rpc WriteBatch(WriteBatchRequest) returns (WriteBatchResponse);
rpc WriteBatchAndWait(WriteBatchAndWaitRequest) returns (WriteBatchAndWaitResponse);
rpc Subscribe(SubscribeRequest) returns (stream VtqMessage);
rpc CheckApiKey(CheckApiKeyRequest) returns (CheckApiKeyResponse);
}
// ============================================================
// NEW: Typed Value System
// ============================================================
// Replaces the v1 string-encoded value field.
// Exactly one field will be set. An unset oneof represents null.
message 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; // byte[]
int64 datetime_value = 8; // UTC DateTime.Ticks (100ns intervals since 0001-01-01)
ArrayValue array_value = 9; // arrays of primitives
}
}
// Container for typed arrays. Exactly one field will be set.
message ArrayValue {
oneof values {
BoolArray bool_values = 1;
Int32Array int32_values = 2;
Int64Array int64_values = 3;
FloatArray float_values = 4;
DoubleArray double_values = 5;
StringArray string_values = 6;
}
}
message BoolArray { repeated bool values = 1; }
message Int32Array { repeated int32 values = 1; }
message Int64Array { repeated int64 values = 1; }
message FloatArray { repeated float values = 1; }
message DoubleArray { repeated double values = 1; }
message StringArray { repeated string values = 1; }
// ============================================================
// NEW: OPC UA-Style Quality Codes
// ============================================================
// Replaces the v1 string quality field ("Good", "Bad", "Uncertain").
message QualityCode {
uint32 status_code = 1; // OPC UA-compatible numeric status code
string symbolic_name = 2; // Human-readable name (e.g., "Good", "BadSensorFailure")
}
// ============================================================
// Connection Lifecycle (unchanged)
// ============================================================
message ConnectRequest {
string client_id = 1;
string api_key = 2;
}
message ConnectResponse {
bool success = 1;
string message = 2;
string session_id = 3;
}
message DisconnectRequest {
string session_id = 1;
}
message DisconnectResponse {
bool success = 1;
string message = 2;
}
message GetConnectionStateRequest {
string session_id = 1;
}
message GetConnectionStateResponse {
bool is_connected = 1;
string client_id = 2;
int64 connected_since_utc_ticks = 3;
}
message CheckApiKeyRequest {
string api_key = 1;
}
message CheckApiKeyResponse {
bool is_valid = 1;
string message = 2;
}
// ============================================================
// Value-Timestamp-Quality (CHANGED)
// ============================================================
message VtqMessage {
string tag = 1; // Tag address (unchanged)
TypedValue value = 2; // CHANGED: typed value instead of string
int64 timestamp_utc_ticks = 3; // UTC DateTime.Ticks (unchanged)
QualityCode quality = 4; // CHANGED: structured quality instead of string
}
// ============================================================
// Read Operations (request unchanged, response uses new VtqMessage)
// ============================================================
message ReadRequest {
string session_id = 1;
string tag = 2;
}
message ReadResponse {
bool success = 1;
string message = 2;
VtqMessage vtq = 3; // Uses updated VtqMessage with TypedValue + QualityCode
}
message ReadBatchRequest {
string session_id = 1;
repeated string tags = 2;
}
message ReadBatchResponse {
bool success = 1;
string message = 2;
repeated VtqMessage vtqs = 3; // Uses updated VtqMessage
}
// ============================================================
// Write Operations (CHANGED: TypedValue instead of string)
// ============================================================
message WriteRequest {
string session_id = 1;
string tag = 2;
TypedValue value = 3; // CHANGED from string
}
message WriteResponse {
bool success = 1;
string message = 2;
}
message WriteItem {
string tag = 1;
TypedValue value = 2; // CHANGED from string
}
message WriteResult {
string tag = 1;
bool success = 2;
string message = 3;
}
message WriteBatchRequest {
string session_id = 1;
repeated WriteItem items = 2;
}
message WriteBatchResponse {
bool success = 1;
string message = 2;
repeated WriteResult results = 3;
}
// ============================================================
// WriteBatchAndWait (CHANGED: TypedValue for items and flag)
// ============================================================
message WriteBatchAndWaitRequest {
string session_id = 1;
repeated WriteItem items = 2; // Uses updated WriteItem with TypedValue
string flag_tag = 3;
TypedValue flag_value = 4; // CHANGED from string — type-aware comparison
int32 timeout_ms = 5;
int32 poll_interval_ms = 6;
}
message WriteBatchAndWaitResponse {
bool success = 1;
string message = 2;
repeated WriteResult write_results = 3;
bool flag_reached = 4;
int32 elapsed_ms = 5;
}
// ============================================================
// Subscription (request unchanged, stream uses new VtqMessage)
// ============================================================
message SubscribeRequest {
string session_id = 1;
repeated string tags = 2;
int32 sampling_ms = 3;
}
// Returns: stream of VtqMessage (updated with TypedValue + QualityCode)
```
---
## 3. Detailed Change Specifications
### 3.1 Typed Value Representation
**What changed:** The `string value` field throughout the protocol is replaced by `TypedValue`, a protobuf `oneof` that carries the value in its native type.
**v1 behavior (removed):**
- All values serialized to string via `.ToString()`
- Client-side parsing heuristic: numeric → bool → string → null
- Arrays JSON-serialized as strings (e.g., `"[1,2,3]"`)
- Empty string treated as null
**v2 behavior:**
- Values transmitted in their native protobuf type
- No parsing ambiguity — the `oneof` case tells you the type
- Arrays use dedicated repeated-field messages (`Int32Array`, `FloatArray`, etc.)
- Null represented by an unset `oneof` (no field selected in `TypedValue`)
- `datetime_value` uses `int64` UTC Ticks (same wire encoding as v1 timestamps, but now semantically typed as a DateTime value rather than a string)
**Null handling:**
| Scenario | v1 | v2 |
|----------|----|----|
| Null value | `value = ""` (empty string) | `TypedValue` with no `oneof` case set |
| Missing VTQ | Treated as Bad quality, null value | Same — Bad quality, unset `TypedValue` |
**Type mapping from internal tag model:**
| Tag Data Type | TypedValue Field | Notes |
|---------------|-----------------|-------|
| `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.2 OPC UA-Style Quality Codes
**What changed:** The `string quality` field (one of `"Good"`, `"Uncertain"`, `"Bad"`) is replaced by `QualityCode` containing a numeric OPC UA status code and a human-readable symbolic name.
**v1 behavior (removed):**
- Quality as case-insensitive string: `"Good"`, `"Uncertain"`, `"Bad"`
- No sub-codes — all failures were just `"Bad"`
**v2 behavior:**
- `status_code` is a `uint32` matching OPC UA `StatusCode` bit layout
- `symbolic_name` is the human-readable equivalent (for logging, debugging, display)
- Category derived from high bits: `0x00xxxxxx` = Good, `0x40xxxxxx` = Uncertain, `0x80xxxxxx` = Bad
**Supported quality codes:**
The quality codes below are filtered to those actively used by AVEVA System Platform, InTouch, and OI Server/DAServer (per AVEVA Tech Note TN1305). AVEVA's ecosystem maps OPC DA quality codes to OPC UA status codes when communicating over OPC UA. This table includes the OPC UA equivalents for the AVEVA-relevant quality states.
**Good Quality:**
| Symbolic Name | Status Code | AVEVA OPC DA Hex | AVEVA Description |
|---------------|-------------|------------------|-------------------|
| `Good` | `0x00000000` | `0x00C0` | Value is reliable, non-specific |
| `GoodLocalOverride` | `0x00D80000` | `0x00D8` | Value has been manually overridden; input disconnected |
**Uncertain Quality:**
| Symbolic Name | Status Code | AVEVA OPC DA Hex | AVEVA Description |
|---------------|-------------|------------------|-------------------|
| `UncertainLastUsableValue` | `0x40900000` | `0x0044` | External source stopped writing; value is stale |
| `UncertainSensorNotAccurate` | `0x42390000` | `0x0050` | Sensor out of calibration or clamped at limit |
| `UncertainEngineeringUnitsExceeded` | `0x40540000` | `0x0054` | Value is outside defined engineering limits |
| `UncertainSubNormal` | `0x40580000` | `0x0058` | Derived from multiple sources with insufficient good sources |
**Bad Quality:**
| Symbolic Name | Status Code | AVEVA OPC DA Hex | AVEVA Description |
|---------------|-------------|------------------|-------------------|
| `Bad` | `0x80000000` | `0x0000` | Non-specific bad; value is not useful |
| `BadConfigurationError` | `0x80040000` | `0x0004` | Server-specific configuration problem (e.g., item deleted) |
| `BadNotConnected` | `0x808A0000` | `0x0008` | Input not logically connected to a source |
| `BadDeviceFailure` | `0x806B0000` | `0x000C` | Device failure detected |
| `BadSensorFailure` | `0x806D0000` | `0x0010` | Sensor failure detected |
| `BadLastKnownValue` | `0x80050000` | `0x0014` | Communication failed; last known value available (check timestamp age) |
| `BadCommunicationFailure` | `0x80050000` | `0x0018` | Communication failed; no last known value available |
| `BadOutOfService` | `0x808F0000` | `0x001C` | Block is off-scan or locked; item/group is inactive |
**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 (Not Limited `0x00`, Low Limited `0x01`, High Limited `0x02`, Constant `0x03`) 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.
- AVEVA's "Initializing" state (seen when OI Server is still establishing communication) maps to `Bad` with no sub-code in OPC DA (`0x0000`). In OPC UA this is `BadWaitingForInitialData` (`0x80320000`).
- This is the minimum set needed to simulate realistic AVEVA System Platform behavior. Additional OPC UA codes can be added if specific simulation scenarios require them.
**Category helper logic (C#):**
```csharp
public static string GetCategory(uint statusCode) => statusCode switch
{
_ when (statusCode & 0xC0000000) == 0x00000000 => "Good",
_ when (statusCode & 0xC0000000) == 0x40000000 => "Uncertain",
_ when (statusCode & 0xC0000000) == 0x80000000 => "Bad",
_ => "Unknown"
};
public static bool IsGood(uint statusCode) => (statusCode & 0xC0000000) == 0x00000000;
public static bool IsBad(uint statusCode) => (statusCode & 0xC0000000) == 0x80000000;
```
### 3.3 WriteBatchAndWait Flag Comparison
**What changed:** `flag_value` is now `TypedValue` instead of `string`. The server uses type-aware equality comparison instead of string comparison.
**v1 behavior (removed):**
```csharp
// v1: string comparison
bool matched = readResult.Value?.ToString() == request.FlagValue;
```
**v2 behavior:**
```csharp
// v2: type-aware comparison
bool matched = TypedValueEquals(readResult.TypedValue, request.FlagValue);
```
**Comparison rules:**
- 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 (unchanged from v1).
- 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. Behavioral Changes
### 4.1 Read Operations
No RPC signature changes. The returned `VtqMessage` now uses `TypedValue` and `QualityCode` instead of strings.
**v1 client code:**
```csharp
var response = await client.ReadAsync(new ReadRequest { SessionId = sid, Tag = "Motor.Speed" });
double value = double.Parse(response.Vtq.Value); // string → double
bool isGood = response.Vtq.Quality.Equals("Good", ...); // string comparison
```
**v2 client code:**
```csharp
var response = await client.ReadAsync(new ReadRequest { SessionId = sid, Tag = "Motor.Speed" });
double value = response.Vtq.Value.DoubleValue; // direct typed access
bool isGood = response.Vtq.Quality.StatusCode == 0x00000000; // numeric comparison
// or: bool isGood = IsGood(response.Vtq.Quality.StatusCode); // helper method
```
### 4.2 Write Operations
Client must construct `TypedValue` instead of converting to string.
**v1 client code:**
```csharp
await client.WriteAsync(new WriteRequest
{
SessionId = sid,
Tag = "Motor.Speed",
Value = 42.5.ToString() // double → string
});
```
**v2 client code:**
```csharp
await client.WriteAsync(new WriteRequest
{
SessionId = sid,
Tag = "Motor.Speed",
Value = new TypedValue { DoubleValue = 42.5 } // native type
});
```
### 4.3 Subscription Stream
No RPC signature changes. The streamed `VtqMessage` items now use the updated format. Client `onUpdate` callbacks receive typed values and structured quality.
### 4.4 Error Conditions with New Quality Codes
The server now returns specific quality codes instead of generic `"Bad"`:
| Scenario | v1 Quality | v2 Quality |
|----------|-----------|-----------|
| Tag not found | `"Bad"` | `BadConfigurationError` (`0x80040000`) |
| Tag read exception / comms loss | `"Bad"` | `BadCommunicationFailure` (`0x80050000`) |
| Write to read-only tag | `success=false` | WriteResult.success=false, message indicates read-only |
| Type mismatch on write | `success=false` | WriteResult.success=false, message indicates type mismatch |
| Simulated sensor failure | `"Bad"` | `BadSensorFailure` (`0x806D0000`) |
| Simulated device failure | `"Bad"` | `BadDeviceFailure` (`0x806B0000`) |
| Stale value (fault injection) | `"Uncertain"` | `UncertainLastUsableValue` (`0x40900000`) |
| Block off-scan / disabled | `"Bad"` | `BadOutOfService` (`0x808F0000`) |
| Local override active | `"Good"` | `GoodLocalOverride` (`0x00D80000`) |
| Initializing / waiting for first value | `"Bad"` | `BadWaitingForInitialData` (`0x80320000`) |
---
## 5. Migration Guide
### 5.1 Strategy
**Clean break** — all clients and servers are updated simultaneously in a single coordinated release. No backward compatibility layer, no version negotiation, no dual-format support.
This is appropriate because:
- The LmxProxy is an internal protocol between ScadaLink components, not a public API
- The number of clients is small and controlled
- Maintaining dual formats adds complexity with no long-term benefit
### 5.2 Server-Side Changes
**Files to update:**
| File | Changes |
|------|---------|
| `scada.proto` | Replace with v2 proto (Section 2 of this document) |
| `ScadaServiceImpl.cs` | Update all RPC handlers to construct `TypedValue` and `QualityCode` instead of strings |
| `SessionManager.cs` | No changes (session model unchanged) |
| `TagMapper.cs` | Update to return `TypedValue` from tag reads instead of string conversion |
**Server implementation notes:**
- When reading a tag, construct `TypedValue` by setting the appropriate `oneof` field based on the tag's data type. Do not call `.ToString()`.
- When a tag read fails, return `QualityCode { StatusCode = 0x80050000, SymbolicName = "BadCommunicationFailure" }` (or a more specific code) instead of the string `"Bad"`.
- When handling writes, extract the value from the `TypedValue` oneof and apply it to the tag actor. If the `oneof` case doesn't match the tag's expected data type, return `WriteResult` with `success=false` and message indicating type mismatch.
- For `WriteBatchAndWait` flag comparison, implement `TypedValueEquals()` per the comparison rules in Section 3.3.
### 5.3 Client-Side Changes
**Files to update:**
| File | Changes |
|------|---------|
| `ILmxProxyClient.cs` | Interface unchanged (same method signatures, updated message types come from proto regeneration) |
| `RealLmxProxyClient.cs` | Update value construction in write methods; update value extraction in read callbacks |
| `LmxProxyDataConnection.cs` | Update DCL adapter to map between DCL's internal value model and `TypedValue`/`QualityCode` |
| `LmxProxyClientFactory.cs` | No changes |
**Client implementation notes:**
- Replace all `double.Parse(vtq.Value)` / `bool.Parse(vtq.Value)` calls with direct typed access (e.g., `vtq.Value.DoubleValue`).
- Replace all `vtq.Quality.Equals("Good", ...)` string comparisons with numeric status code checks or the `IsGood()`/`IsBad()` helpers.
- Replace all `.ToString()` value serialization in write paths with `TypedValue` construction.
- The `onUpdate` callback signature in `SubscribeAsync` doesn't change at the interface level, but the `VtqMessage` it receives now contains `TypedValue` and `QualityCode`.
### 5.4 Migration Checklist
```
[ ] Generate updated C# classes from v2 proto file
[ ] Update server: ScadaServiceImpl read handlers → TypedValue + QualityCode
[ ] Update server: ScadaServiceImpl write handlers → accept TypedValue
[ ] Update server: WriteBatchAndWait flag comparison → TypedValueEquals()
[ ] Update server: Error paths → specific QualityCode status codes
[ ] Update client: RealLmxProxyClient read paths → typed value extraction
[ ] Update client: RealLmxProxyClient write paths → TypedValue construction
[ ] Update client: Quality checks → numeric status code comparison
[ ] Update client: LmxProxyDataConnection DCL adapter → map TypedValue ↔ DCL values
[ ] Update all unit tests for new message shapes
[ ] Integration test: client ↔ server round-trip with all data types
[ ] Integration test: WriteBatchAndWait with typed flag comparison
[ ] Integration test: Subscription stream delivers typed VTQ messages
[ ] Integration test: Error paths return correct QualityCode sub-codes
[ ] Remove all string-based value parsing/serialization code
[ ] Remove all string-based quality comparison code
```
---
## 6. Test Scenarios for v2 Validation
These scenarios validate that the v2 protocol behaves correctly across all data types and quality codes.
### 6.1 Round-Trip Type Fidelity
For each supported data type, write a value via `Write`, read it back via `Read`, and verify the `TypedValue` oneof case and value match exactly:
| Data Type | Test Value | TypedValue Field | Verify |
|-----------|-----------|-----------------|--------|
| `bool` | `true` | `bool_value` | `== true` |
| `int32` | `2147483647` | `int32_value` | `== int.MaxValue` |
| `int64` | `9223372036854775807` | `int64_value` | `== long.MaxValue` |
| `float` | `3.14159f` | `float_value` | `== 3.14159f` (exact bits) |
| `double` | `2.718281828459045` | `double_value` | `== 2.718281828459045` (exact bits) |
| `string` | `"Hello World"` | `string_value` | `== "Hello World"` |
| `bytes` | `[0x00, 0xFF, 0x42]` | `bytes_value` | byte-for-byte match |
| `DateTime` | `638789000000000000L` | `datetime_value` | `== 638789000000000000L` |
| `float[]` | `[1.0f, 2.0f, 3.0f]` | `array_value.float_values` | element-wise match |
| `int32[]` | `[10, 20, 30]` | `array_value.int32_values` | element-wise match |
| null | (unset) | no oneof case | `Value case == None` |
### 6.2 Quality Code Propagation
| Scenario | Trigger | Expected QualityCode |
|----------|---------|---------------------|
| Normal read | Read a healthy tag | `{ 0x00000000, "Good" }` |
| Local override | Script sets `GoodLocalOverride` | `{ 0x00D80000, "GoodLocalOverride" }` |
| Fault injection: sensor failure | Script sets `BadSensorFailure` | `{ 0x806D0000, "BadSensorFailure" }` |
| Fault injection: device failure | Script sets `BadDeviceFailure` | `{ 0x806B0000, "BadDeviceFailure" }` |
| Fault injection: stale value | Script sets `UncertainLastUsableValue` | `{ 0x40900000, "UncertainLastUsableValue" }` |
| Fault injection: off-scan | Script sets `BadOutOfService` | `{ 0x808F0000, "BadOutOfService" }` |
| Fault injection: comms failure | Script sets `BadCommunicationFailure` | `{ 0x80050000, "BadCommunicationFailure" }` |
| Unknown tag | Read nonexistent tag | `{ 0x80040000, "BadConfigurationError" }` |
| Write to read-only | Write to a read-only tag | WriteResult.success=false, message contains "read-only" |
### 6.3 WriteBatchAndWait Typed Flag Comparison
| Flag Type | Written Value | Flag Value | Expected Result |
|-----------|--------------|-----------|-----------------|
| `bool` | `true` | `TypedValue { bool_value = true }` | `flag_reached = true` |
| `bool` | `false` | `TypedValue { bool_value = true }` | `flag_reached = false` (timeout) |
| `double` | `42.5` | `TypedValue { double_value = 42.5 }` | `flag_reached = true` |
| `double` | `42.500001` | `TypedValue { double_value = 42.5 }` | `flag_reached = false` |
| `string` | `"DONE"` | `TypedValue { string_value = "DONE" }` | `flag_reached = true` |
| `string` | `"done"` | `TypedValue { string_value = "DONE" }` | `flag_reached = false` (case-sensitive) |
| `int32` | `1` | `TypedValue { double_value = 1.0 }` | `flag_reached = false` (type mismatch) |
### 6.4 Subscription Stream
- Subscribe to tags of mixed data types
- Verify each streamed `VtqMessage` has the correct `oneof` case matching the tag's data type
- Inject a fault mid-stream and verify the quality code changes from `Good` to the injected code
- Cancel the subscription and verify the stream terminates cleanly
---
## 7. Appendix: v1 → v2 Quick Reference
**Reading a value:**
```csharp
// v1
string raw = vtq.Value;
if (double.TryParse(raw, out var d)) { /* use d */ }
else if (bool.TryParse(raw, out var b)) { /* use b */ }
else { /* it's a string */ }
// v2
switch (vtq.Value.ValueCase)
{
case TypedValue.ValueOneofCase.DoubleValue:
double d = vtq.Value.DoubleValue;
break;
case TypedValue.ValueOneofCase.BoolValue:
bool b = vtq.Value.BoolValue;
break;
case TypedValue.ValueOneofCase.StringValue:
string s = vtq.Value.StringValue;
break;
case TypedValue.ValueOneofCase.None:
// null value
break;
// ... other cases
}
```
**Writing a value:**
```csharp
// v1
new WriteItem { Tag = "Motor.Speed", Value = 42.5.ToString() }
// v2
new WriteItem { Tag = "Motor.Speed", Value = new TypedValue { DoubleValue = 42.5 } }
```
**Checking quality:**
```csharp
// v1
bool isGood = vtq.Quality.Equals("Good", StringComparison.OrdinalIgnoreCase);
bool isBad = vtq.Quality.Equals("Bad", StringComparison.OrdinalIgnoreCase);
// v2
bool isGood = (vtq.Quality.StatusCode & 0xC0000000) == 0x00000000;
bool isBad = (vtq.Quality.StatusCode & 0xC0000000) == 0x80000000;
// or use helper:
bool isGood = QualityHelper.IsGood(vtq.Quality.StatusCode);
```
**Constructing quality (server-side):**
```csharp
// v1
vtq.Quality = "Good";
// v2
vtq.Quality = new QualityCode { StatusCode = 0x00000000, SymbolicName = "Good" };
// or for errors:
vtq.Quality = new QualityCode { StatusCode = 0x806D0000, SymbolicName = "BadSensorFailure" };
vtq.Quality = new QualityCode { StatusCode = 0x80050000, SymbolicName = "BadCommunicationFailure" };
vtq.Quality = new QualityCode { StatusCode = 0x00D80000, SymbolicName = "GoodLocalOverride" };
```
---
*Document version: 1.0 — All decisions resolved. Complete proto, migration guide, and test scenarios.*