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.
13 KiB
LmxProxy Protocol Specification
Note: This specification reflects the v2 protocol with native
TypedValuesupport. 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
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
ConnectRPC fails withsuccess=falseif 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_keyfield and as anx-api-keygRPC metadata header on theConnectcall.
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:
DateTimearrays are not natively supported in the proto — they are serialized asInt64Array(UTC ticks) by the Host.
The ScadaLink adapter normalizes ArrayValue objects to comma-separated display strings at the adapter boundary (see Component-DataConnectionLayer.md).
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
int64representingDateTime.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 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:
- All writes execute first. If any write fails, the operation returns immediately with
success=false. - If writes succeed, polls
flag_tagatpoll_interval_msintervals. - Compares the read result's
TypedValueagainstflag_value. - If flag matches before timeout:
success=true,flag_reached=true. - 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:
- Server validates the session. Invalid session →
RpcExceptionwithStatusCode.Unauthenticated. - Server registers monitored items on the backend (e.g., OPC UA subscriptions) for all requested tags.
- On each value change, the server pushes a
VtqMessageto the response stream. - The stream remains open indefinitely until:
- The client cancels (disposes the subscription).
- The server encounters an error (backend disconnect, etc.).
- The gRPC connection drops.
- On stream termination, the client's
onStreamErrorcallback 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 |