Files
lmxopcua/docs/v2/driver-specs.md

58 KiB
Raw Blame History

OtOpcUa v2 — Driver Implementation Specifications

Status: DRAFT — reference document for plan.md

Created: 2026-04-16


1. Galaxy (MXAccess) Driver

Summary

Out-of-process Tier C driver bridging AVEVA System Platform (Wonderware) Galaxies. The existing v1 implementation is refactored behind the new driver capability interfaces and hosted in a separate Windows service (.NET 4.8 x86) that communicates with the main OtOpcUa server (.NET 10 x64) via named pipes + MessagePack. Hosted out-of-process for two reasons: COM/.NET 4.8 x86 bitness constraint and Tier C stability isolation (per driver-stability.md). FOCAS is the second Tier C driver, also out-of-process — see §7.

Library & Dependencies

Component Package / Source Version Target Notes
MXAccess COM ArchestrA.MxAccess (GAC / lib/ArchestrA.MxAccess.dll) version-neutral late-bound .NET 4.8 x86 Pinned via <Reference Include="ArchestrA.MxAccess"> with EmbedInteropTypes=false; interfaces: LMXProxyServer, ILMXProxyServerEvents, MXSTATUS_PROXY
Galaxy DB client System.Data.SqlClient (BCL) BCL .NET 4.8 x86 Direct SQL for hierarchy/attribute/change-detection queries
Wonderware Historian SDK aahClientManaged, aahClientCommon Historian-shipped .NET 4.8 x86 Optional — loaded only when Historian.Enabled=true
MessagePack-CSharp MessagePack NuGet 2.x .NET Standard 2.0 (Shared) IPC serialization; shared contract between Proxy and Host
Named pipes System.IO.Pipes (BCL) BCL both sides IPC transport, localhost only

Required Components

  • AVEVA System Platform / ArchestrA Platform deployed on the same machine as Galaxy.Host (installs MXAccess COM objects into the GAC)
  • A deployed Galaxy with at least one $WinPlatform object hosting $AppEngine(s) hosting AutomationObjects
  • SQL Server reachable from Galaxy.Host with the Galaxy repository database (default ZB); Windows Auth by default
  • 32-bit .NET Framework 4.8 runtime on the Host machine (MXAccess is 32-bit COM, no 64-bit variant)
  • STA thread + Win32 message pump inside the Host process for all COM calls and event callbacks (see §13)
  • Wonderware Historian installed on-box or reachable via aah SDK — only if HDA is enabled
  • No external firewall ports — MXAccess is local-machine COM/IPC; pipe is localhost-only. Galaxy DB port (default SQL 1433) if the ZB database is remote.

Connection Settings (per driver instance, from central config DB)

All settings live under a schemaless DriverConfig JSON blob on the DriverInstance row. Current v1 equivalents (defaults and source file references in parentheses):

MXAccess (MxAccessConfiguration.cs):

Setting Type Default Description
ClientName string "LmxOpcUa" Registration name passed to LMXProxyServer.Register()
NodeName string? null Optional ArchestrA node override (null = local)
GalaxyName string? null Optional Galaxy name override
ReadTimeoutSeconds int 5 Per-read timeout
WriteTimeoutSeconds int 5 Per-write timeout
RequestTimeoutSeconds int 30 Outer safety timeout around any MXAccess request
MaxConcurrentOperations int 10 Pool bound on in-flight MXAccess work items
MonitorIntervalSeconds int 5 Connectivity heartbeat probe interval
AutoReconnect bool true Replay stored subscriptions on COM reconnect
ProbeTag string? null Optional heartbeat tag for health monitoring
ProbeStaleThresholdSeconds int 60 Mark connection stale if no probe callback within
RuntimeStatusProbesEnabled bool true Auto-subscribe ScanState for $WinPlatform / $AppEngine
RuntimeStatusUnknownTimeoutSeconds int 15 Grace period before an un-probed host is assumed Stopped

Galaxy repository (GalaxyRepositoryConfiguration.cs):

Setting Type Default Description
ConnectionString string Server=localhost;Database=ZB;Integrated Security=true; ZB SQL Server connection
ChangeDetectionIntervalSeconds int 30 Poll interval for galaxy.time_of_last_deploy
CommandTimeoutSeconds int 30 SQL command timeout
ExtendedAttributes bool false Include extended attribute metadata in discovery
Scope enum (Galaxy | LocalPlatform) Galaxy Address-space scope filter (commit bc282b6)
PlatformName string? Environment.MachineName Platform to scope to when Scope=LocalPlatform

IPC (new for v2):

Setting Type Default Description
PipeName string otopcua-galaxy-{InstanceId} Named pipe name
HostStartupTimeoutMs int 30000 Proxy wait for Host Ready handshake
IpcCallTimeoutMs int 15000 Per-call RPC timeout

Addressing

Galaxy objects carry two names:

  • contained_name — human-readable, scoped to parent; used for OPC UA browse tree
  • tag_name — globally unique system identifier; used for MXAccess runtime references
Layer Example
OPC UA browse path TestMachine_001/DelmiaReceiver/DownloadPath
OPC UA NodeId ns=<galaxyNs>;s=<tagName>.<AttributeName>
MXAccess reference DelmiaReceiver_001.DownloadPath (passed to AddItem())

Tag discovery is dynamic — driven by the Galaxy repository DB (gobject, dynamic_attribute, primitive_instance, template_definition). Optional Scope=LocalPlatform filters the hierarchy via the hosted_by_gobject_id chain to the subtree rooted at the local $WinPlatform (on a dev Galaxy: 49→3 objects, 4206→386 attributes).

Data Type Mapping (MxDataTypeMapper.cs, gr/data_type_mapping.md)

mx_data_type Galaxy Type OPC UA BuiltInType CLR Type
1 Boolean Boolean (i=1) bool
2 Integer Int32 (i=6) int
3 Float Float (i=10) float
4 Double Double (i=11) double
5 String String (i=12) string
6 Time DateTime (i=13) DateTime
7 ElapsedTime Double (i=11) double (seconds)
8 Reference String (i=12) string
13 Enumeration Int32 (i=6) int
14 / 16 Custom String (i=12) string
15 InternationalizedString LocalizedText (i=21) string
(default) Unknown String (i=12) string

Arrays: is_array=0 → ValueRank -1 (Scalar); is_array=1 → ValueRank 1 (OneDimension), ArrayDimensions = [array_dimension].

Security Classification Mapping (SecurityClassificationMapper.cs)

security_classification Galaxy Level OPC UA Write Permission
0 FreeAccess WriteOperate
1 Operate WriteOperate
2 SecuredWrite — (read-only in v1)
3 VerifiedWrite — (read-only in v1)
4 Tune WriteTune
5 Configure WriteConfigure
6 ViewOnly — (read-only)

Maps to the OPC UA roles ReadOnly / WriteOperate / WriteTune / WriteConfigure defined in the LDAP role provider (see docs/security.md).

Subscription Model — Native MXAccess Advisories

Galaxy is one of three drivers with native subscriptions (Galaxy, TwinCAT, OPC UA Client). No polling.

  • Mechanism: LMXProxyServer.AddItem()AdviseSupervisory(handle, itemHandle); callbacks delivered through the ILMXProxyServerEvents.OnDataChange COM event
  • Callback signature: MxDataChangeHandler(itemHandle, MXSTATUS_PROXY, value, quality, timestamp)
  • Dispatch: STA COM event → dispatch-thread queue → OPC UA ClearChangeMasks fan-out (decouples COM thread from UA stack lock — commit c76ab8f)
  • Stored subscriptions replayed on reconnect via ReplayStoredSubscriptionsAsync()
  • Probe tag + runtime-status probes provide connection-health visibility (see §14)
  • Bad-quality fan-out: when a host ($WinPlatform or $AppEngine) ScanState transitions to Stopped, every attribute under that host is immediately published as BadOutOfService (commits 7310925, c76ab8f)

Alarm Model

In-process alarm-condition tracking (v1 baseline; extended in v2 to match IAlarmSource):

  • Auto-subscribed attributes per alarm-eligible object: InAlarm, Priority, Description (cached for severity and message)
  • Filtering: AlarmFilterConfiguration.ObjectFilters[] — include/exclude by template chain (empty = all eligible)
  • Transitions: InAlarm change → OPC UA A&C AlarmConditionState event (Active / Return to Normal)
  • Severity: Galaxy Priority (1 = highest) mapped to OPC UA 11000 severity (higher = more severe)
  • Acknowledgment: local OPC UA ack forwards to MXAccess write on the Ack attribute of the alarm-bearing object

History Model — Wonderware Historian (optional plugin)

  • Loaded at runtime from ZB.MOM.WW.LmxOpcUa.Historian.Aveva.dll when Historian.Enabled=true; compile-time optional
  • SDK: aahClientManaged / aahClientCommon
  • Supported OPC UA HDA calls:
    • HistoryReadRawModified (raw values with bounds)
    • HistoryReadProcessed (Historian aggregates: AVG, MIN, MAX, TIMEAVG, etc. — mapped to OPC UA aggregates)
    • Continuation points for paged reads
  • Only attributes flagged historize=1 in the Galaxy DB expose AccessLevel.HistoryRead

Error Mapping — MXAccess → Quality → OPC UA StatusCode

Byte quality (OPC DA convention)QualityMapper.cs:

OPC DA Quality Category
>= 192 Good
64191 Uncertain
< 64 Bad

MXAccess error codes → Quality (MxErrorCodes.cs):

Code Name Quality
1008 MX_E_InvalidReference BadConfigError
1012 MX_E_WrongDataType BadConfigError
1013 MX_E_NotWritable BadOutOfService
1014 MX_E_RequestTimedOut BadCommFailure
1015 MX_E_CommFailure BadCommFailure
1016 MX_E_NotConnected BadNotConnected

Quality → OPC UA StatusCode (QualityMapper.cs):

Quality StatusCode
Good 0x00000000
GoodLocalOverride 0x00D80000
Uncertain 0x40000000
Bad (generic) 0x80000000
BadCommFailure 0x80050000
BadNotConnected 0x808A0000
BadOutOfService 0x808D0000

Change Detection

  • ChangeDetectionService polls galaxy.time_of_last_deploy at ChangeDetectionIntervalSeconds (default 30s)
  • On timestamp change, OnGalaxyChanged fires → Host re-queries hierarchy/attributes → emits TagSetChanged over IPC → Proxy implements IRediscoverable and rebuilds the affected subtree in the address space
  • Platform-scope filter (commit bc282b6) applied during hierarchy load when Scope=LocalPlatform

IPC Contract (Proxy ↔ Host) — Galaxy.Shared

.NET Standard 2.0 MessagePack contracts. Every request carries a correlation ID; responses carry the same ID plus success/error.

Lifecycle / handshake:

Message Direction Payload
ClientHello Proxy → Host InstanceId, expected protocol version
HostReady Host → Proxy Host version, Galaxy name, capabilities
Shutdown Proxy → Host Graceful stop

Tag discovery (ITagDiscovery):

Message Direction Payload
DiscoverHierarchyRequest Proxy → Host Scope, PlatformName
DiscoverHierarchyResponse Host → Proxy GalaxyObjectInfo[] (TagName, ContainedName, ParentTagName, TemplateChain, category)
DiscoverAttributesRequest Proxy → Host TagName[]
DiscoverAttributesResponse Host → Proxy GalaxyAttributeInfo[] (Name, MxDataType, IsArray, ArrayDim, SecurityClass, Historized, WriteableRuntimeChecked)
TagSetChangedNotification Host → Proxy New deploy timestamp; triggers re-discover

Read / Write (IReadable, IWritable):

Message Direction Payload
ReadRequest Proxy → Host TagRef[] (tag_name + attribute)
ReadResponse Host → Proxy VtqPayload[] (value, quality, timestamp, statusCode)
WriteRequest Proxy → Host (TagRef, Value, ExpectedDataType)[]
WriteResponse Host → Proxy (TagRef, StatusCode)[]

Subscription (ISubscribable):

Message Direction Payload
SubscribeRequest Proxy → Host TagRef[] + Proxy-generated subscription ID
SubscribeResponse Host → Proxy Per-tag subscribe ack + handle
UnsubscribeRequest Proxy → Host handles
DataChangeNotification Host → Proxy (push) handle, VTQ, sequence number
ProbeHealthNotification Host → Proxy (push) probe tag staleness, ScanState transitions, overall connected/disconnected

Alarms (IAlarmSource):

Message Direction Payload
AlarmEventNotification Host → Proxy (push) source tag, InAlarm, Priority, Description, severity, transition type
AlarmAckRequest Proxy → Host source tag, user, comment

History (IHistoryProvider):

Message Direction Payload
HistoryReadRawRequest Proxy → Host TagRef, start, end, numValues, returnBounds, continuationPoint
HistoryReadRawResponse Host → Proxy values + next continuation point
HistoryReadProcessedRequest Proxy → Host TagRef, aggregateId, start, end, resampleInterval
HistoryReadProcessedResponse Host → Proxy aggregated values

Framing: length-prefixed MessagePack frames over a single NamedPipeServerStream in PipeTransmissionMode.Byte. Separate outgoing pipe for push notifications or multiplex via message type tag.

Threading / COM Constraints

  • STA thread (StaComThread.cs) hosts MXAccess: ApartmentState.STA, raw Win32 GetMessage / DispatchMessage loop
  • Work items marshaled in via PostThreadMessage(WM_APP=0x8000)
  • Per-handle serialization: LMXProxyServer is not thread-safe — all Read/Write/Subscribe calls on one handle run serially via the STA queue
  • Dispatch thread (separate from STA thread) drains _pendingDataChanges to the OPC UA framework; decouples the STA pump from UA stack locks so a slow subscriber can't back up COM event delivery
  • Reentrancy guards — event unwiring must precede Marshal.ReleaseComObject() on disconnect

Runtime Status (recent commits bc282b6 / 4b209f6 / 7310925 / c76ab8f / 0003984)

  • GalaxyRuntimeProbeManager auto-subscribes <ObjectName>.ScanState for every $WinPlatform (category 1) and $AppEngine (category 3) in scope
  • Per-host state machine: Unknown → Running | Stopped; transitions fire _onHostStopped / _onHostRunning callbacks on the dispatch thread
  • Synthetic OPC UA nodes expose ScanState per host as read-only variables so clients see runtime topology without the dashboard
  • HealthCheck Rule 2e monitors probe subscription health; a failed probe can no longer leave phantom entries that fan out false BadOutOfService
  • Generalizes to the driver-agnostic IHostConnectivityProbe capability interface in v2 (see plan.md §5a)

Implementation Notes

  • First Tier C out-of-process driver — uses the Galaxy.Proxy / Galaxy.Host / Galaxy.Shared three-project split. The pattern is reusable; FOCAS is the second adopter (see §7), and any future driver with bitness, licensing, or stability-isolation needs reuses the same template. See driver-stability.md for the generalized contract
  • Galaxy.Proxy (in the main server) implements IDriver, ITagDiscovery, IRediscoverable, IReadable, IWritable, ISubscribable, IAlarmSource, IHistoryProvider, IHostConnectivityProbe
  • Galaxy.Host owns MxAccessBridge, GalaxyRepository, alarm tracking, GalaxyRuntimeProbeManager, and the Historian plugin — no reference to Core.Abstractions
  • Galaxy.Shared is .NET Standard 2.0, referenced by both sides
  • Existing v1 code is the implementation — refactor in place (extract capability interfaces first, then move behind IPC — see plan.md Decision #55)
  • Parity gate: v2 driver must pass v1 IntegrationTests suite + scripted Client.CLI walkthrough before Phase 3 begins

Operational Stability Notes

Galaxy has a Tier C deep dive in driver-stability.md covering the STA pump, COM object lifetime, subscription replay, recycle policy, and post-mortem contents. Driver-instance specifics:

  • Memory baseline scales with Galaxy size. Watchdog floor of 200 MB above baseline + 1.5 GB hard ceiling — higher than FOCAS because legitimate Galaxy footprints are larger.
  • Slope tolerance is 5 MB/min (more permissive than FOCAS) because address-space rebuild on redeploy can transiently allocate large amounts.
  • Known regression-prone failure modes (closed in commits c76ab8f and 7310925, must remain closed): phantom probe subscription flipping Tick() to Stopped; cross-host quality clear wiping sibling state during recovery; sync-over-async on the OPC UA stack thread; fire-and-forget alarm tasks racing shutdown. Each should have a regression test in the v2 parity suite.
  • STA pump health probe every 10 s (separate from the proxy↔host heartbeat). A wedged pump is the most likely Tier C failure mode for Galaxy.
  • Recycle preserves cached time_of_last_deploy watermark — the common case (crash unrelated to redeploy) skips full DB rediscovery for faster recovery.

2. Modbus TCP Driver

Summary

In-process polled driver for Modbus TCP devices. Flat register-based addressing with config-driven tag definitions. Supports multiple devices per driver instance.

Library & Dependencies

Component Package Version Target
NModbus NModbus NuGet 3.0.x .NET Standard 2.0

No external runtime dependencies. Pure managed .NET.

Required Components

  • None beyond network access to Modbus TCP devices (port 502)

Connection Settings (per device, from central config DB)

Setting Type Default Description
Host string Device IP address or hostname
Port int 502 Modbus TCP port
UnitId byte 1 Slave/unit ID (0-247)
ConnectTimeoutMs int 3000 TCP connect timeout
ResponseTimeoutMs int 2000 Modbus response timeout
RetryCount int 2 Retries before marking bad
ReconnectDelayMs int 5000 Delay before reconnect attempt
ByteOrder enum BigEndian BigEndian, LittleEndian, MidBigEndian, MidLittleEndian
AddressFormat enum Standard Standard (0-based decimal), DL205 (octal V/X/Y/C/T/CT notation)

Tag Definition (from central config DB)

Field Type Description
Name string Tag name (OPC UA browse name)
FolderPath string Address space hierarchy path
RegisterArea enum Coil, DiscreteInput, HoldingRegister, InputRegister
Address int 0-based register/coil address
DataType enum Boolean, Int16, UInt16, Int32, UInt32, Float32, Float64, Int64, UInt64, String
StringLength int? Character count for string tags
AccessLevel enum Read, ReadWrite
PollGroup string Poll group name for scan rate assignment
ByteOrder enum? Per-tag override (defaults to device setting)
BitIndex int? For bit-within-register access (0-15)
ScaleGain double? Linear scaling: displayed = raw * gain + offset
ScaleOffset double? Linear scaling offset

Addressing Model

Area Classic Address Protocol Address Function Code Size
Coils 00001-09999 0x0000-0xFFFF FC 01 (read), FC 05/15 (write) 1 bit
Discrete Inputs 10001-19999 0x0000-0xFFFF FC 02 (read) 1 bit
Input Registers 30001-39999 0x0000-0xFFFF FC 04 (read) 16 bit
Holding Registers 40001-49999 0x0000-0xFFFF FC 03 (read), FC 06/16 (write) 16 bit

Multi-register data types (32-bit float, 64-bit double) span consecutive registers. Byte order is configurable per-device or per-tag — critical since there is no standard.

Poll Architecture

  • Poll groups with configurable intervals (e.g. Fast=250ms, Medium=1000ms, Slow=5000ms)
  • Block read optimization: coalesce contiguous/nearby registers into single FC 03/04 requests (max 125 registers per request)
  • Gap tolerance: configurable — read through gaps of N unused registers to reduce request count
  • Demand-based: only poll registers with active OPC UA MonitoredItems

Error Mapping

Modbus Exception OPC UA StatusCode
0x01 Illegal Function BadNotSupported
0x02 Illegal Data Address BadNodeIdUnknown
0x03 Illegal Data Value BadOutOfRange
0x04 Slave Device Failure BadInternalError
0x0A Gateway Path Unavailable BadNoCommunication
0x0B Gateway Target Failed BadTimeout
TCP timeout/disconnect BadNoCommunication

DL205 Compatibility (AutomationDirect)

The AutomationDirect DL205 PLC supports Modbus TCP via the H2-ECOM100 Ethernet module (port 502). Rather than a separate driver, the Modbus TCP driver supports DL205 natively via the AddressFormat = DL205 setting.

DL205 uses octal addressing. When AddressFormat is DL205, users configure tags using native DL205 notation and the driver translates to Modbus addresses:

DL205 Address Modbus Area Modbus Address Formula
V2000 Holding Register (FC 03) 1024 OctalToDecimal(2000) = 1024
V2400 Holding Register (FC 03) 1280 OctalToDecimal(2400) = 1280
V7777 Holding Register (FC 03) 4095 OctalToDecimal(7777) = 4095
X0 Discrete Input (FC 02) 0 OctalToDecimal(0) = 0
X17 Discrete Input (FC 02) 15 OctalToDecimal(17) = 15
Y0 Coil (FC 01) 0 OctalToDecimal(0) = 0
Y10 Coil (FC 01) 8 OctalToDecimal(10) = 8
C0C777 Coil (FC 01) 5121023 512 + OctalToDecimal(addr)
T0T377 Coil (FC 01) 20482303 2048 + OctalToDecimal(addr) (status bits)
CT0CT177 Coil (FC 01) 25602687 2560 + OctalToDecimal(addr)

DL205-specific notes:

  • 32-bit values (Float32, Int32) use low word at lower address (little-endian word order) — set ByteOrder = LittleEndian for the device
  • H2-ECOM100 supports 8 simultaneous TCP connections — use a single connection
  • Float32 requires DL250-1 or DL260 CPU (DL240 has no float support)
  • Timers/counters: current values are in V-memory (V0V377 for timers, V1000V1177 for counters); status bits are in the coil area
  • DL205 is still manufactured but considered legacy — BRX series is the successor

Operational Stability Notes

Tier A (pure managed). Universal protections (SafeHandle on socket handles, bounded per-device queue, crash-loop breaker, IDriver.Reinitialize()) are sufficient. No driver-specific watchdog. NModbus is mature and has no known leak surfaces. The only operational concern worth calling out: byte/word-order misconfiguration is the most common production bug — not a stability issue but a correctness one. Surface byte-order mismatches as data validation alerts in the dashboard rather than as quality codes, since a wrong-endian reading is silently plausible.


3. Allen-Bradley CIP Driver (ControlLogix / CompactLogix)

Summary

In-process polled driver for modern AB Logix PLCs. Symbolic tag-based addressing. Supports controller-scoped and program-scoped tags.

Library & Dependencies

Component Package Version Target
libplctag libplctag NuGet 1.6.x .NET Standard 2.0

The NuGet package bundles native C library for Windows x64 (also Linux, macOS). No separate install needed.

Required Components

  • None beyond network access to PLC (port 44818)
  • PLC firmware 20+ recommended for request packing

Connection Settings (per device, from central config DB)

Setting Type Default Description
Host string PLC IP address
Path string 1,0 CIP routing path (backplane, slot)
PlcType enum ControlLogix ControlLogix, CompactLogix, Micro800
TimeoutMs int 5000 Read/write timeout
AllowPacking bool true CIP request packing (firmware 20+)
ConnectionSize int 4002 Max CIP packet size

Tag Definition (from central config DB)

Field Type Description
Name string OPC UA browse name
FolderPath string Address space hierarchy
TagPath string CIP tag path (e.g. Motor1_Speed, Program:MainProgram.StepIndex)
DataType enum BOOL, SINT, INT, DINT, LINT, REAL, LREAL, STRING
ArraySize int? Element count for array tags
AccessLevel enum Read, ReadWrite
PollGroup string Poll group name

Addressing Examples

Motor1_Speed                          — controller-scoped REAL
MyArray[5]                            — array element
Program:MainProgram.StepIndex         — program-scoped DINT
MyUDT.Member1                         — UDT member
MyDINT.5                              — bit 5 of a DINT
MyTimer.ACC                           — Timer accumulated value

Implementation Notes

  • libplctag automatically pools CIP connections per gateway+path — no manual connection pooling needed
  • Thread-safe — multiple tags can be read concurrently
  • No native subscriptions — polled only
  • CIP has no tag discovery API that libplctag exposes well; tags are config-driven from central DB

Operational Stability Notes

Tier B (libplctag is a C library accessed via P/Invoke). Mature and widely deployed; manages its own threads and memory pool internally.

  • Wrap every libplctag handle in a SafeHandle with finalizer calling plc_tag_destroy — leaked tags accumulate in libplctag's internal table and eventually exhaust resources.
  • Watch for libplctag-internal memory growth: per-instance allocation tracking (per Tier A/B contract in driver-stability.md) cannot see libplctag's native heap. Monitor process RSS attributable to this driver via GC.GetAllocatedBytesForCurrentThread() proxies for sanity, and rely on the universal slope-detection over the whole-server RSS to flag growth that correlates with this driver's reload cycles.
  • Tier promotion trigger: if production telemetry shows libplctag-attributable native growth that can't be bounded by IDriver.Reinitialize() (which destroys + recreates the tag pool), promote to Tier C. Older libplctag versions had known leaks; 1.6.x has not shown them in field reports but we have no internal SLA.
  • No native subscriptions means no callback-pump concerns. The driver-internal poll loop is fully managed C# and has standard cancellation behavior.

4. Allen-Bradley Legacy Driver (SLC 500 / MicroLogix)

Summary

In-process polled driver for legacy AB PLCs using PCCC over EtherNet/IP. File-based addressing (not symbolic).

Library & Dependencies

Component Package Version Target
libplctag libplctag NuGet 1.6.x .NET Standard 2.0

Same library as AB CIP — libplctag handles both protocols via the plc attribute.

Required Components

  • Network access to PLC/adapter (port 44818)
  • SLC 5/05, MicroLogix 1100/1400 for built-in Ethernet
  • SLC 5/03/04 requires external Ethernet adapter module (1761-NET-ENI or 1747-AENTR)

Connection Settings (per device, from central config DB)

Setting Type Default Description
Host string PLC or Ethernet adapter IP
Path string 1,0 Routing path (backplane, slot)
PlcType enum SLC500 SLC500, MicroLogix
TimeoutMs int 5000 Response timeout

Tag Definition (from central config DB)

Field Type Description
Name string OPC UA browse name
FolderPath string Address space hierarchy
FileAddress string PLC file address (e.g. N7:0, F8:5, T4:0.ACC, B3:0/5)
DataType enum Boolean, Int16, Float32, Int32, String, TimerStruct, CounterStruct
ElementCount int Number of consecutive elements (for block reads)
AccessLevel enum Read, ReadWrite
PollGroup string Poll group name

File-Based Addressing Reference

File Type Default # Element Size Data Type Examples
O (Output) 0 2 bytes INT16/bit O:0, O:0/0
I (Input) 1 2 bytes INT16/bit I:0, I:0/3
S (Status) 2 2 bytes INT16/bit S:0, S:1/0
B (Binary) 3 2 bytes INT16/bit B3:0, B3:0/5
T (Timer) 4 6 bytes Struct T4:0.ACC, T4:0.PRE, T4:0/DN
C (Counter) 5 6 bytes Struct C5:0.ACC, C5:0/DN, C5:0/CU
R (Control) 6 6 bytes Struct R6:0.LEN, R6:0.POS, R6:0/DN
N (Integer) 7 2 bytes INT16 N7:0, N7:0/5, N10:50
F (Float) 8 4 bytes FLOAT32 F8:0, F20:10
ST (String) 9 84 bytes STRING ST9:0 (82 char max)
L (Long) 4 bytes INT32 L10:0 (user-created, 5/03+)

Connection Limits — CRITICAL

PLC Model Max Connections
SLC 5/05 4
SLC 5/04 + AENTR 4-8
SLC 5/03 + NET-ENI 1-4
MicroLogix 1100 4
MicroLogix 1400 8

Driver MUST use a single TCP connection per PLC and serialize all requests.

Implementation Notes

  • No tag discovery — all addresses must be configured up front
  • Timer/Counter/Control structures should be decomposed into child OPC UA nodes (ACC, PRE, DN, EN, etc.)
  • Max PCCC data payload: 244 bytes per request (limits batch read size)
  • String/Long support requires SLC 5/03+ or MicroLogix 1100+

Operational Stability Notes

Tier B (same libplctag library as AB CIP). Same SafeHandle/leak-watch protections apply. Two PCCC-specific concerns beyond CIP:

  • 48 connection limit per PLC is enforced by the device, not the library. Connection-refused errors must be handled distinctly from network-down — bound the in-flight request count per device to 1, and queue the rest. Universal bounded queue (default 1000) is generous here; consider lowering to 50 per device for SLC/MicroLogix to fail fast when the device is overcommitted.
  • PCCC reconnect is more expensive than CIP (no connection multiplexing on legacy PLCs). Polly retry should use longer backoff for AB Legacy than for AB CIP — recommend doubled intervals.

5. Siemens S7 Driver

Summary

In-process polled driver for Siemens S7 PLCs. Area-based addressing (DB, M, I, Q). PDU-size-aware request batching for optimal read performance.

Library & Dependencies

Component Package Version Target
S7.Net S7netplus NuGet latest .NET Standard 2.0

Pure managed .NET, MIT license. No native dependencies.

Required Components

  • Network access to PLC (port 102, ISO-on-TCP)
  • S7-1200/1500: PUT/GET communication must be enabled in TIA Portal (Hardware Config > Protection & Security > "Permit access with PUT/GET communication") — disabled by default
  • S7-1500 access level: Must be set to level 1 (no protection) for S7comm access

Connection Settings (per device, from central config DB)

Setting Type Default Description
Host string PLC IP address
Rack int 0 Hardware rack number
Slot int 0 CPU slot (S7-300=2, S7-1200/1500=0)
CpuType enum S71500 S7300, S7400, S71200, S71500
TimeoutMs int 5000 Read/write timeout
MaxConcurrentConnections int 1 Connections to this PLC (usually 1)

Typical Rack/Slot by PLC Family

PLC Rack Slot
S7-300 0 2
S7-400 0 2 or 3
S7-1200 0 0
S7-1500 0 0

Tag Definition (from central config DB)

Field Type Description
Name string OPC UA browse name
FolderPath string Address space hierarchy
S7Address string S7 address (e.g. DB1.DBW0, MW10, I0.0, Q0.0)
DataType enum Bool, Byte, Int16, UInt16, Int32, UInt32, Float32, Float64, Int64, String, DateTime
StringLength int? For S7 String type (default 254)
AccessLevel enum Read, ReadWrite
PollGroup string Poll group name

Addressing Reference

Area Address Syntax Area Code Examples
Data Block DB{n}.DB{X|B|W|D}{offset}[.bit] 0x84 DB1.DBX0.0, DB1.DBW0, DB1.DBD4
Merkers M{B|W|D}{offset} or M{offset}.{bit} 0x83 M0.0, MW0, MD4
Inputs I{B|W|D}{offset} or I{offset}.{bit} 0x81 I0.0, IW0, ID0
Outputs Q{B|W|D}{offset} or Q{offset}.{bit} 0x82 Q0.0, QW0, QD0
Timers T{n} 0x1D T0, T15
Counters C{n} 0x1C C0, C10

PDU Size & Optimization

PLC PDU Size Max Data per Read
S7-300 240 bytes ~218 bytes
S7-400 480 bytes ~458 bytes
S7-1200 240 bytes ~218 bytes
S7-1500 480-960 bytes ~458-938 bytes

Optimization: Group reads by DB/area, merge contiguous addresses, pack into PDU-sized batches (max 20 items per multi-read PDU).

Connection Limits

PLC Max Connections Available for S7 Basic
S7-300 8-32 6-30
S7-1200 8-16 3-8
S7-1500 32-64 16-32

Driver should use 1 connection per PLC, serialize with SemaphoreSlim.

Byte Order

S7 is big-endian. All multi-byte values need byte-swap on .NET (little-endian x64). Use BinaryPrimitives.ReadXxxBigEndian().

Operational Stability Notes

Tier B (S7netplus is mostly managed; small native bits in the ISO-on-TCP transport). Stable in the field. Universal protections cover it. Two notes:

  • PUT/GET disabled on S7-1200/1500 manifests as a hard "function not allowed" error from the PLC, not a connection failure. The driver must distinguish these — universal Polly retry on a PUT/GET-disabled error is wasted effort and noise. Map PUT/GET-disabled to BadNotSupported and surface as a configuration alert in the dashboard, not a transient failure.
  • Single connection per PLC with SemaphoreSlim serialization is the documented S7netplus pattern. Don't try to parallelize against one PLC; you'll just queue at the wire level with worse latency.

6. Beckhoff TwinCAT (ADS) Driver

Summary

In-process driver with native subscription support via ADS device notifications. Symbol-based addressing with automatic tag discovery via symbol upload.

Library & Dependencies

Component Package Version Target
Beckhoff.TwinCAT.Ads NuGet 6.x .NET Standard 2.0 / .NET 6+
Beckhoff.TwinCAT.Ads.Reactive NuGet 6.x Optional — Rx wrappers for notifications

Required Components

  • No TwinCAT installation required on the OtOpcUa server machine (v6+ supports in-process ADS router via AmsRouter class)
  • Network access to TwinCAT device (TCP port 48898 — fixed ADS-over-TCP port)
  • AMS route must be configured on the target TwinCAT device (route back to the OtOpcUa server)
  • Target PLC runtime must be in Run state

Connection Settings (per device, from central config DB)

Setting Type Default Description
Host string TwinCAT device IP address
AmsNetId string Target AMS Net ID (e.g. 192.168.1.50.1.1)
AmsPort int 851 Target AMS port (851=TC3 PLC Runtime 1)
TimeoutMs int 5000 Read/write timeout
NotificationCycleTimeMs int 100 Default ADS notification check interval
NotificationMaxDelayMs int 0 Notification batching delay (0=immediate)

AMS Port Reference

Port Subsystem
851 TwinCAT 3 PLC Runtime 1
852 TwinCAT 3 PLC Runtime 2
853 TwinCAT 3 PLC Runtime 3
854 TwinCAT 3 PLC Runtime 4
500 NC PTP
300 TwinCAT 2 PLC Runtime 1

Tag Discovery

TwinCAT supports automatic symbol upload — the driver can enumerate all PLC variables with their types, sizes, and comments at connect time:

  1. Use SymbolLoaderFactory.Create(client, SymbolLoaderSettings.Default) to get an ISymbolLoader
  2. Enumerate all symbols with full type information
  3. Build OPC UA address space from discovered symbols
  4. Re-upload on symbol version change (error 0x0702) — detects PLC program re-download

Tags can also be user-configured in the central config DB (to expose a subset or apply custom naming).

Addressing Examples

MAIN.nCounter              — local variable in MAIN program
GVL.Motor1.Speed           — Global Variable List struct member
MAIN.aValues[3]            — array element
MAIN.fbPID.fOutput         — function block output

Symbol paths are case-insensitive.

Subscription Model — Native ADS Notifications

TwinCAT is one of only three drivers (along with Galaxy and OPC UA Client) that supports native subscriptions.

  • AdsTransMode.OnChange — notification only when value changes (checked at cycle time interval)
  • AdsTransMode.ServerOnChange — more efficient, evaluated in PLC task cycle
  • Map OPC UA MonitoredItem → ADS notification 1:1
  • Dynamic subscribe/unsubscribe as OPC UA clients add/remove MonitoredItems
  • Limit: ~500 notifications per ADS connection (configurable in TwinCAT runtime)
  • For high-tag-count scenarios, fall back to sum-up (batch) reads with 0xF080 index group

Error Handling

Error Code Meaning Recovery
Target port not found 0x0006 PLC runtime not running Retry with backoff
Target machine not found 0x0007 AMS route missing or network error Check route config
Timeout 0x000A Device not responding Retry, then reconnect
Symbol not found 0x0701 Variable name doesn't exist Validate config
Symbol version changed 0x0702 PLC program re-downloaded Re-upload symbols, rebuild address space
Not ready 0x0710 PLC in Config mode Wait for Run state

ADS State Monitoring

Subscribe to PLC runtime state transitions (Invalid→Init→Run→Stop→Config→Error) for health reporting to the status dashboard.

Operational Stability Notes

Tier B with the most native involvement of any in-process driver. Beckhoff.TwinCAT.Ads v6 is mostly managed but the AMS router and the ADS-notification callback pump have native components. Three driver-specific concerns warrant explicit handling:

  • Symbol-version-changed (0x0702) is the unique TwinCAT failure mode. A PLC re-download invalidates every symbol handle and notification handle the driver holds. The driver must catch 0x0702, mark its symbol cache invalid, re-upload symbols, rebuild the address space subtree, and re-establish all notifications — without losing the OPC UA subscriptions they back. Treat this as a IRediscoverable invocation, not as a connection error. Universal IDriver.Reinitialize() is the right entry point; the driver implementation honors it by walking the symbol-handle table.
  • Native ADS notification callbacks run on the AMS router thread, not the driver's polling thread. Callbacks must marshal to a managed work queue immediately (no driver logic on the router thread) — blocking the router thread blocks every ADS notification across the process. This is a common bug class for ADS drivers and worth a code-review checklist item.
  • AMS route table failures are silent: if the route is misconfigured, the driver gets 0x0007 (target machine not found) but the underlying cause is configuration, not network. Surface as a configuration alert, not a transient failure; don't waste Polly retry budget on it.
  • Memory growth signal: cached symbol info is the largest in-driver allocation. Per-instance budget should bound it; on breach, flush the symbol cache (forcing the next operation to re-upload). If symbol cache flush doesn't bound growth, this is a Tier C promotion candidate.

7. FANUC FOCAS Driver

Summary

Tier C out-of-process polled driver for FANUC CNC machines, hosted via the Focas.Proxy / Focas.Host / Focas.Shared three-project split (same pattern as Galaxy — see driver-stability.md). Fundamentally different from PLC drivers — data is accessed via discrete API functions, not tag-based addressing. The driver exposes a pre-defined set of OPC UA nodes that map to specific FOCAS2 API calls.

Hosted out-of-process because Fwlib64.dll is a black-box vendor DLL with no public stability SLA; an AccessViolationException from native code is uncatchable in modern .NET and would tear down the entire OtOpcUa server with all other drivers and sessions if FOCAS were in-process. The Focas.Host Windows service runs on .NET 10 x64 (no bitness constraint) and is recycled, watchdogged, and supervised independently of the main server.

Library & Dependencies

Component Source Version Target
Fwlib64.dll FANUC FOCAS2 SDK FOCAS2 x64 native DLL (P/Invoke) — loaded only by Focas.Host, never by the main server
Focas2.cs FANUC SDK P/Invoke declarations + struct definitions, in Focas.Host
MessagePack-CSharp MessagePack NuGet 2.x .NET Standard 2.0 (Shared)
Named pipes System.IO.Pipes (BCL) BCL both sides

Required Components

  • FANUC FOCAS2 SDK — requires license agreement from FANUC or authorized distributor
  • Fwlib64.dll (64-bit) redistributed with the Focas.Host Windows service installer (not the main server)
  • CNC must have Ethernet function option enabled
  • CNC network accessible (TCP port 8193) from the Focas.Host machine (typically the same machine as the OtOpcUa server)
  • Very limited connections: 5-10 max per CNC — driver MUST use exactly one connection
  • Focas.Host installed as a separate Windows service alongside the OtOpcUa server (same machine; localhost-only IPC)

Connection Settings (per device, from central config DB)

Setting Type Default Description
Host string CNC IP address
Port int 8193 FOCAS TCP port
TimeoutSeconds int 10 Connection timeout (FOCAS uses seconds)
CncSeries string? Optional hint (e.g. 0i-F, 30i-B) — for capability filtering

Pre-Defined Tag Set — FOCAS API Mapping

Unlike PLC drivers where tags are user-defined, the FOCAS driver exposes a fixed hierarchy of nodes that are populated by specific API calls. Users can enable/disable categories, but cannot define arbitrary tags.

CNC Identity (read once at connect)

OPC UA Node FOCAS API Return Type FOCAS2 Min Version Notes
Identity/SeriesNumber cnc_sysinfo()ODBSYS.series String FOCAS1 e.g. "0i-F"
Identity/Version cnc_sysinfo()ODBSYS.version String FOCAS1 CNC software version
Identity/MaxAxes cnc_sysinfo()ODBSYS.max_axis Int32 FOCAS1
Identity/CncType cnc_sysinfo()ODBSYS.cnc_type String FOCAS1 "M" (mill) or "T" (lathe)
Identity/MtType cnc_sysinfo()ODBSYS.mt_type String FOCAS1 Machine tool type
Identity/AxisCount cnc_rdaxisname() → count Int32 FOCAS2 Dynamic axis count

CNC Status (polled fast — 250-500ms)

OPC UA Node FOCAS API Return Type FOCAS2 Min Version Notes
Status/RunState cnc_statinfo()ODBST.run Int32 FOCAS1 0=STOP, 1=HOLD, 2=START, 3=MSTR
Status/RunStateText (derived from RunState) String "Stop", "Hold", "Running"
Status/Mode cnc_statinfo()ODBST.aut Int32 FOCAS1 0=MDI, 1=AUTO, 3=EDIT, 4=HANDLE, 5=JOG, 7=REF
Status/ModeText (derived from Mode) String "MDI", "Auto", "Edit", "Jog"
Status/MotionState cnc_statinfo()ODBST.motion Int32 FOCAS1 0=idle, 1=MOTION, 2=DWELL, 3=WAIT
Status/EmergencyStop cnc_statinfo()ODBST.emergency Boolean FOCAS1
Status/AlarmActive cnc_statinfo()ODBST.alarm Int32 FOCAS1 Bitmask of alarm categories

Axis Data (polled fast — 100-250ms, dynamically created per axis)

OPC UA Node FOCAS API Return Type FOCAS2 Min Version Notes
Axes/{name}/AbsolutePosition cnc_absolute(handle, axis, ...) Double FOCAS1 Scaled integer → double via cnc_getfigure()
Axes/{name}/MachinePosition cnc_machine(handle, axis, ...) Double FOCAS1
Axes/{name}/RelativePosition cnc_relative(handle, axis, ...) Double FOCAS1
Axes/{name}/DistanceToGo cnc_distance(handle, axis, ...) Double FOCAS1
Axes/{name}/ServoLoad cnc_rdsvmeter() Double FOCAS2 Percentage
Axes/FeedRate/Actual cnc_actf()ODBACT.data Double FOCAS1 mm/min or inch/min
Axes/FeedRate/Commanded cnc_rdspeed() Double FOCAS2

Axis names (X, Y, Z, A, B, C, etc.) are discovered dynamically via cnc_rdaxisname() at connect time.

Position value conversion: Values are scaled integers. Use cnc_getfigure() to get decimal places per axis, then: double position = rawValue / Math.Pow(10, decimalPlaces).

Spindle Data (polled medium — 250-500ms)

OPC UA Node FOCAS API Return Type FOCAS2 Min Version Notes
Spindle/{n}/ActualSpeed cnc_acts() or cnc_acts2(handle, sp_no, ...) Double FOCAS1 / FOCAS2 RPM
Spindle/{n}/Load cnc_rdspmeter() Double FOCAS2 Percentage
Spindle/{n}/Override cnc_rdspeed() type=1 Double FOCAS2 Percentage

Program Info (polled slow — 1000ms)

OPC UA Node FOCAS API Return Type FOCAS2 Min Version Notes
Program/MainProgramNumber cnc_rdprgnum()ODBPRO.mdata Int32 FOCAS1
Program/RunningProgramNumber cnc_rdprgnum()ODBPRO.data Int32 FOCAS1
Program/RunningProgramName cnc_exeprgname() String FOCAS2 Full path/name
Program/SequenceNumber cnc_rdseqnum()ODBSEQ.data Int32 FOCAS1 Current N-number
Program/BlockCount cnc_rdblkcount() Int64 FOCAS2
Program/PartsCount cnc_rdparam(6711/6712) Int64 FOCAS1 Parameter-based
Program/CycleTime cnc_rdtimer(handle, 0/1/2, ...) Double FOCAS1 Seconds
Program/OperatingTime cnc_rdtimer(handle, 0, ...) Double FOCAS1 Hours

Tool Data (polled slow — 1000-2000ms)

OPC UA Node FOCAS API Return Type FOCAS2 Min Version Notes
Tool/CurrentToolNumber cnc_rdtofs() Int32 FOCAS1 Active tool offset #
Tool/Offsets/{n}/GeometryX cnc_rdtofsr() Double FOCAS1 Tool offset values
Tool/Offsets/{n}/GeometryZ cnc_rdtofsr() Double FOCAS1
Tool/Offsets/{n}/WearX cnc_rdtofsr() Double FOCAS1
Tool/Offsets/{n}/WearZ cnc_rdtofsr() Double FOCAS1

Alarms (polled — 500-1000ms)

OPC UA Node FOCAS API Return Type FOCAS2 Min Version Notes
Alarms/ActiveAlarmCount cnc_rdalmmsg() → count Int32 FOCAS2
Alarms/Active/{n}/Number cnc_rdalmmsg()ODBALMMSG.alm_no Int32 FOCAS2
Alarms/Active/{n}/Message cnc_rdalmmsg()ODBALMMSG.alm_msg String FOCAS2
Alarms/Active/{n}/Type cnc_rdalmmsg()ODBALMMSG.type String FOCAS2 P/S, OT, SV, etc.
Alarms/StatusBitmask cnc_alarm()ODBALM.data Int32 FOCAS1 Quick alarm check

PMC Data (user-configured addresses, polled 100-500ms)

OPC UA Node FOCAS API Return Type FOCAS2 Min Version Notes
PMC/{type}/{address} pmc_rdpmcrng(handle, adr_type, data_type, start, end, ...) varies FOCAS1 Batch-readable

PMC address types: R (5), D (9), E (11), T (4), C (3), K (6), A (7), G (0), F (1), X, Y. Data types: 0=byte, 1=word, 2=long, 4=float, 5=double.

PMC addresses are user-configured in the central config DB (address type, start, end, data type, friendly name). The driver does NOT auto-discover PMC layout.

Macro Variables (user-configured ranges, polled 500-1000ms)

OPC UA Node FOCAS API Return Type FOCAS2 Min Version Notes
Macro/Common/Var{n} cnc_rdmacro(handle, n, ...) Double FOCAS1 #100-#199 (volatile)
Macro/Persistent/Var{n} cnc_rdmacro(handle, n, ...) Double FOCAS1 #500-#999 (retained)
Macro/System/Var{n} cnc_rdmacro(handle, n, ...) Double FOCAS1 #1000+ (system vars)
(batch) cnc_rdmacror(handle, start, end, ...) Double[] FOCAS1 Range read

Macro variable conversion: ODBM.mcr_val / 10^ODBM.dec_val = actual double value.

FOCAS Error Codes

Code Constant Meaning OPC UA StatusCode
0 EW_OK Success Good
-1 EW_SOCKET Socket error (CNC off/network) BadNoCommunication
-7 EW_HANDLE Invalid handle BadNoCommunication
-17 EW_BUSY CNC busy (retry) BadResourceUnavailable
1 EW_FUNC Function not supported BadNotSupported
3 EW_NUMBER Invalid number BadNodeIdUnknown
6 EW_NOOPT CNC option not installed BadNotSupported
7 EW_PROT Write-protected BadNotWritable
10 EW_REJECT Request rejected BadInvalidState

Implementation Notes

  • Tier C hosting: Focas.Proxy runs in the main OtOpcUa server (.NET 10 x64) and implements IDriver + capability interfaces, forwarding every call over named-pipe IPC to the Focas.Host Windows service. Focas.Host is the only process that loads Fwlib64.dll. See driver-stability.md for the full Tier C contract (memory watchdog, scheduled recycle, crash-loop circuit breaker, post-mortem MMF, IPC ACL).
  • Wedged native calls escalate to hard exit, never handle-free-during-call — per the recycle policy, an in-flight Fwlib call that exceeds the recycle grace window leaves its handle Abandoned and the host hard-exits. The supervisor respawns. Calling cnc_freelibhndl on a handle with an active native call is undefined behavior and is the AV path the isolation is designed to prevent.
  • FOCAS calls are not thread-safe per handle — serialize all calls on a single handle (handle-affinity worker thread per handle in Focas.Host)
  • Use cnc_statinfo() as a connection heartbeat (CNC-level — distinct from the proxy↔host heartbeat)
  • Build a capability discovery layer — test key functions on first connect, expose only supported nodes
  • Position values are scaled integers — must read increment system via cnc_getfigure() and convert
  • Consider OPC UA Companion Spec OPC 40502 (CNC Systems) for standard node type definitions

Operational Stability Notes

FOCAS has the canonical Tier C deep dive in driver-stability.md — handle pool design, per-handle thread serialization, watchdog thresholds (1.5×/2× baseline + 75 MB floor + 300 MB hard ceiling), recycle policy with hard-exit-on-wedged-call escalation, post-mortem MMF contents, and the two-artifact test approach (TCP stub + native FaultShim). Refer to that section rather than duplicating here.


8. OPC UA Client (Gateway) Driver

Summary

In-process driver that connects to a remote OPC UA server as a client and re-exposes its address space through the local OtOpcUa server. Supports full OPC UA capability proxying: subscriptions, alarms, and history.

Library & Dependencies

Component Package Version Target
OPC Foundation Client OPCFoundation.NetStandard.Opc.Ua.Client NuGet 1.5.x .NET 10

Already used by Client.Shared in the existing codebase.

Required Components

  • Network access to remote OPC UA server
  • Certificate trust (or auto-accept for dev)

Connection Settings (per remote server, from central config DB)

Setting Type Default Description
EndpointUrl string Remote server endpoint (e.g. opc.tcp://host:4840)
SecurityPolicy string None None, Basic256Sha256, Aes128_Sha256_RsaOaep
SecurityMode enum None None, Sign, SignAndEncrypt
AuthType enum Anonymous Anonymous, Username, Certificate
Username string? For Username auth
Password string? For Username auth
SessionTimeoutMs int 120000 Server-negotiated session timeout
KeepAliveIntervalMs int 5000 Session keep-alive interval
ReconnectPeriodMs int 5000 Initial reconnect delay
BrowseStrategy enum Full Full (browse all at startup), Lazy, Hybrid
BrowseRoot string? Optional root NodeId to mirror (default: ObjectsFolder)
AutoAcceptCertificates bool false Accept untrusted server certificates

Tag Discovery

Tags are discovered by browsing the remote server's address space — no central config DB tag definitions needed (though an optional filter/subset config could restrict what's exposed).

Subscription Proxying

  1. Local OPC UA client subscribes to a node → gateway creates MonitoredItem on remote server
  2. Remote server pushes DataChange → gateway updates local variable + ClearChangeMasks()
  3. Reference counting: only one remote MonitoredItem per unique tag, regardless of local subscriber count
  4. Use SessionReconnectHandler for automatic reconnection + subscription transfer

Namespace Remapping

Remote namespace indices must be remapped to local indices using namespace URI lookup. Build a bidirectional map at connect time from session.NamespaceUris.

Alarm Forwarding

  • Subscribe to events on remote server (Attributes.EventNotifier on ObjectIds.Server)
  • Maintain local AlarmConditionState per remote alarm source
  • Forward Acknowledge/Confirm calls from local clients to remote server

History Forwarding

  • Override HistoryReadRawModified etc. in the gateway NodeManager
  • Forward to remote server via session.HistoryRead()
  • Pass-through — no local storage needed

Operational Stability Notes

Tier A (pure managed, OPC Foundation reference SDK). Universal protections cover it. Three operational concerns specific to a gateway driver:

  • Subscription drift on remote-server reconnect: when the upstream server restarts and the session reconnects, the SDK by default creates fresh subscriptions, leaving the local NodeManager's monitored-item handles dangling. The driver must track upstream subscription IDs and reissue TransferSubscriptions (or rebuild) on reconnect. This is the largest gateway-specific bug surface.
  • Cascading quality: when the upstream server reports Bad on a node, fan it out locally with the same StatusCode (don't translate to a generic Bad) so downstream clients can distinguish "remote source down" from "local driver failure." Preserve upstream timestamps too — overwriting them with DateTime.UtcNow masks staleness.
  • Browse cache memory: BrowseStrategy=Full against a large remote server can cache tens of thousands of node descriptions. Per-instance budget should bound this; on breach, switch to Lazy strategy and let cache pressure drive eviction. No process action.

Driver Comparison Summary

Driver Library License Stability Tier .NET Target Native Subs Tag Discovery Connection Limit Required Infrastructure
Galaxy MXAccess COM + MessagePack IPC Proprietary C — out-of-process .NET 4.8 x86 (Host) + .NET 10 x64 (Proxy) Yes (MXAccess advisory) Galaxy DB query + IRediscoverable on deploy Per-Galaxy (one Host per machine) ArchestrA Platform, SQL Server (ZB DB), Historian (optional)
Modbus TCP NModbus 3.x MIT A — in-process .NET 10 x64 No (polled) Config DB 2-8 per device None (also covers DL205 via octal address translation)
AB CIP libplctag 1.6.x LGPL/MIT B — in-process with guards .NET 10 x64 No (polled) Config DB 32-128 per PLC None
AB Legacy libplctag 1.6.x LGPL/MIT B — in-process with guards .NET 10 x64 No (polled) Config DB 4-8 per PLC Ethernet adapter for some models
Siemens S7 S7netplus MIT B — in-process with guards .NET 10 x64 No (polled) Config DB 3-30 per PLC PUT/GET enabled (S7-1200/1500)
TwinCAT Beckhoff.TwinCAT.Ads 6.x Proprietary B — in-process with guards .NET 10 x64 Yes (ADS) Symbol upload 64-128 AMS route configured
FOCAS Fwlib64.dll (P/Invoke) + MessagePack IPC FANUC SDK license C — out-of-process .NET 10 x64 (Host + Proxy) No (polled) Built-in + Config DB 5-10 per CNC FANUC SDK license, CNC Ethernet option, Focas.Host Windows service
OPC UA Client OPC Foundation 1.5.x GPL/RCL A — in-process .NET 10 x64 Yes (native) Browse remote Varies Certificate trust

Tier definitions and per-tier protections: see driver-stability.md.