# 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

```protobuf
syntax = "proto3";
package scada;

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);
}
```

Proto file location: `src/ScadaLink.DataConnectionLayer/Adapters/Protos/scada.proto`

## Connection Lifecycle

### Session Model

Every client must call `Connect` before performing any read, write, or subscribe operation. The server returns a session ID (32-character hex GUID) that must be included in all subsequent requests. Sessions persist until `Disconnect` is called or the server restarts — there is no idle timeout.

### Authentication

API key authentication is optional, controlled by server configuration:

- **If required**: The `Connect` RPC fails with `success=false` if the API key doesn't match.
- **If not required**: All API keys are accepted (including empty).
- The API key is sent both in the `ConnectRequest.api_key` field and as an `x-api-key` gRPC metadata header on the `Connect` call.

### Connect

```
ConnectRequest {
  client_id: string     // Client identifier (e.g., "ScadaLink-{guid}")
  api_key:   string     // API key for authentication (empty if none)
}

ConnectResponse {
  success:    bool      // Whether connection succeeded
  message:    string    // Status message
  session_id: string    // 32-char hex GUID (only valid if success=true)
}
```

The client generates `client_id` as `"ScadaLink-{Guid:N}"` for uniqueness.

### Disconnect

```
DisconnectRequest {
  session_id: string
}

DisconnectResponse {
  success: bool
  message: string
}
```

Best-effort — the client calls disconnect but does not retry on failure.

### GetConnectionState

```
GetConnectionStateRequest {
  session_id: string
}

GetConnectionStateResponse {
  is_connected:            bool
  client_id:               string
  connected_since_utc_ticks: int64   // DateTime.UtcNow.Ticks at connect time
}
```

### CheckApiKey

```
CheckApiKeyRequest {
  api_key: string
}

CheckApiKeyResponse {
  is_valid: bool
  message:  string
}
```

Standalone API key validation without creating a session.

## Value-Timestamp-Quality (VTQ)

The core data structure for all read and subscription results:

```
VtqMessage {
  tag:                 string       // Tag address
  value:               TypedValue   // Native typed value (protobuf oneof)
  timestamp_utc_ticks: int64        // UTC DateTime.Ticks (100ns intervals since 0001-01-01)
  quality:             QualityCode  // OPC UA status code + symbolic name
}
```

### TypedValue

Values are transmitted as native types via a protobuf `oneof`:

| 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 |

### ArrayValue

`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 `QualityCode` enum with OPC UA status code semantics:

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

### Timestamps

- All timestamps are UTC.
- Encoded as `int64` representing `DateTime.Ticks` (100-nanosecond intervals since 0001-01-01 00:00:00 UTC).
- Client reconstructs via `new DateTime(ticks, DateTimeKind.Utc)`.

## Read Operations

### Read (Single Tag)

```
ReadRequest {
  session_id: string    // Valid session ID
  tag:        string    // Tag address
}

ReadResponse {
  success: bool         // Whether read succeeded
  message: string       // Error message if failed
  vtq:     VtqMessage   // Value-timestamp-quality result
}
```

### ReadBatch (Multiple Tags)

```
ReadBatchRequest {
  session_id: string
  tags:       repeated string    // Tag addresses
}

ReadBatchResponse {
  success: bool                  // false if any tag failed
  message: string                // Error message
  vtqs:    repeated VtqMessage   // Results in same order as request
}
```

Batch reads are **partially successful** — individual tags may have Bad quality while the overall response succeeds. If a tag read throws an exception, its VTQ is returned with Bad quality and current UTC timestamp.

## Write Operations

### Write (Single Tag)

```
WriteRequest {
  session_id: string
  tag:        string
  value:      TypedValue   // Native typed value (see TypedValue)
}

WriteResponse {
  success: bool
  message: string
}
```

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: TypedValue
}

WriteResult {
  tag:     string
  success: bool
  message: string
}

WriteBatchRequest {
  session_id: string
  items:      repeated WriteItem
}

WriteBatchResponse {
  success: bool                   // Overall success (all items must succeed)
  message: string
  results: repeated WriteResult   // Per-item results
}
```

Batch writes are **all-or-nothing** at the reporting level — if any item fails, overall `success` is `false`.

### WriteBatchAndWait (Atomic Write + Flag Polling)

A compound operation: write values, then poll a flag tag until it matches an expected value or times out.

```
WriteBatchAndWaitRequest {
  session_id:       string
  items:            repeated WriteItem   // Values to write (TypedValue)
  flag_tag:         string               // Tag to poll after writes
  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)
}

WriteBatchAndWaitResponse {
  success:       bool                    // Overall operation success
  message:       string
  write_results: repeated WriteResult    // Per-item write results
  flag_reached:  bool                    // Whether flag matched before timeout
  elapsed_ms:    int32                   // Total elapsed time
}
```

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

## Subscription (Server Streaming)

### Subscribe

```
SubscribeRequest {
  session_id:  string
  tags:        repeated string   // Tag addresses to monitor
  sampling_ms: int32             // Backend sampling interval in milliseconds
}

// Returns: stream of VtqMessage
```

**Behavior:**

1. Server validates the session. Invalid session → `RpcException` with `StatusCode.Unauthenticated`.
2. Server registers monitored items on the backend (e.g., OPC UA subscriptions) for all requested tags.
3. On each value change, the server pushes a `VtqMessage` to the response stream.
4. The stream remains open indefinitely until:
   - The client cancels (disposes the subscription).
   - The server encounters an error (backend disconnect, etc.).
   - The gRPC connection drops.
5. On stream termination, the client's `onStreamError` callback fires exactly once.

**Client-side subscription lifecycle:**

```
ILmxSubscription subscription = await client.SubscribeAsync(
    addresses: ["Motor.Speed", "Motor.Temperature"],
    onUpdate: (tag, vtq) => { /* handle value change */ },
    onStreamError: () => { /* handle disconnect */ });

// Later:
await subscription.DisposeAsync();  // Cancels the stream
```

Disposing the subscription cancels the underlying `CancellationTokenSource`, which terminates the background stream-reading task and triggers server-side cleanup of monitored items.

## Tag Addressing

Tags are string addresses that identify data points. The proxy maps tag addresses to backend-specific identifiers.

**LmxFakeProxy example** (OPC UA backend):

Tag addresses are concatenated with a configurable prefix to form OPC UA node IDs:

```
Prefix: "ns=3;s="
Tag:    "Motor.Speed"
NodeId: "ns=3;s=Motor.Speed"
```

The prefix is configured at server startup via the `OPC_UA_PREFIX` environment variable.

## Transport Details

| Setting | Value |
|---------|-------|
| Protocol | gRPC over HTTP/2 |
| Default port | 50051 |
| TLS | Optional (controlled by `UseTls` connection parameter) |
| Metadata headers | `x-api-key` (sent on Connect call if API key configured) |

### Connection Parameters

The ScadaLink DCL configures LmxProxy connections via a string dictionary:

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `Host` | string | `"localhost"` | gRPC server hostname |
| `Port` | string (parsed as int) | `"50051"` | gRPC server port |
| `ApiKey` | string | (none) | API key for authentication |
| `SamplingIntervalMs` | string (parsed as int) | `"0"` | Backend sampling interval for subscriptions |
| `UseTls` | string (parsed as bool) | `"false"` | Use HTTPS instead of HTTP |

## Error Handling

| Operation | Error Mechanism | Client Behavior |
|-----------|----------------|-----------------|
| Connect | `success=false` in response | Throws `InvalidOperationException` |
| Read/ReadBatch | `success=false` in response | Throws `InvalidOperationException` |
| Write/WriteBatch | `success=false` in response | Throws `InvalidOperationException` |
| WriteBatchAndWait | `success=false` or `flag_reached=false` | Returns result (timeout is not an exception) |
| Subscribe (auth) | `RpcException` with `Unauthenticated` | Propagated to caller |
| Subscribe (stream) | Stream ends or gRPC error | `onStreamError` callback invoked; `sessionId` nullified |
| Any (disconnected) | Client checks `IsConnected` | Throws `InvalidOperationException("not connected")` |

When a subscription stream ends unexpectedly, the client immediately nullifies its session ID, causing `IsConnected` to return `false`. The DCL adapter fires its `Disconnected` event, which triggers the reconnection cycle in the `DataConnectionActor`.

## Implementation Files

| Component | File |
|-----------|------|
| Proto definition | `src/ScadaLink.DataConnectionLayer/Adapters/Protos/scada.proto` |
| Client interface | `src/ScadaLink.DataConnectionLayer/Adapters/ILmxProxyClient.cs` |
| Client implementation | `src/ScadaLink.DataConnectionLayer/Adapters/RealLmxProxyClient.cs` |
| DCL adapter | `src/ScadaLink.DataConnectionLayer/Adapters/LmxProxyDataConnection.cs` |
| Client factory | `src/ScadaLink.DataConnectionLayer/Adapters/LmxProxyClientFactory.cs` |
| Server implementation | `infra/lmxfakeproxy/Services/ScadaServiceImpl.cs` |
| Session manager | `infra/lmxfakeproxy/Sessions/SessionManager.cs` |
| Tag mapper | `infra/lmxfakeproxy/TagMapper.cs` |
| OPC UA bridge interface | `infra/lmxfakeproxy/Bridge/IOpcUaBridge.cs` |
| OPC UA bridge impl | `infra/lmxfakeproxy/Bridge/OpcUaBridge.cs` |
