# 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.*
