Files
lmxopcua/docs/v2/driver-specs.md
Joseph Doherty 006af51768 docs: post-PR-7.2 cleanup — audit + three-track scrub
Audit (three parallel agent passes) found 43 markdown files carrying
stale references to the deleted Galaxy.Host/Proxy/Shared projects
after the v2-mxgw merge. This commit lands the prioritized fixes.

Track 1 — high-traffic in-place rewrites (3 files, ~454 lines deleted)
- README.md (202 → 91 lines): drops .NET 4.8 / x86 / TopShelf install
  text; leads with the multi-driver .NET 10 server identity and points
  at scripts/install/Install-Services.ps1 and the parity rig.
- docs/v2/driver-specs.md §1 Galaxy (~289 → ~66 lines): replaces the
  Tier-C out-of-process spec with a Tier-A in-process description
  matching the current GalaxyDriver code, with the four-section
  GalaxyDriverOptions JSON shape pulled verbatim from
  Config/GalaxyDriverOptions.cs.
- docs/drivers/Galaxy.md (211 → 92 lines): full rewrite around the
  current Browse/Runtime/Health/Config sub-folders.

Track 2 — historical banners (5 files)
- lmx_mxgw.md, lmx_mxgw_impl.md, lmx_backend.md,
  docs/v2/Galaxy.ParityMatrix.md,
  docs/v2/implementation/phase-2-galaxy-out-of-process.md each get a
  " Completed 2026-04-30 — historical record" banner block. lmx_mxgw.md
  also fixes two dead links (`docs/Galaxy.Driver.md` and
  `docs/v2/Galaxy.Driver.md`) → `docs/drivers/Galaxy.md`.

Track 3 — v1 archive sweep (10 git mv + 1 new index + 2 in-place scrubs)
- Moved 10 v1 docs under docs/v1/ preserving subpath structure:
  AlarmTracking, Configuration, DataTypeMapping, HistoricalDataAccess,
  Subscriptions (top-level); drivers/Galaxy-Repository,
  drivers/Galaxy-Test-Fixture; reqs/GalaxyRepositoryReqs,
  reqs/MxAccessClientReqs, reqs/ServiceHostReqs.
- New docs/v1/README.md is the shared archive banner + per-file table.
- docs/README.md repointed to the v1 paths and updated to reflect the
  v2 two-process deploy shape (Server + Admin + optional
  OtOpcUaWonderwareHistorian).
- docs/v2/Galaxy.ParityRig.md got a historical banner + four inline
  scrubs marking the OtOpcUaGalaxyHost service / Driver.Galaxy.Host
  EXE / Driver.Galaxy.ParityTests project as deleted-in-PR-7.2.

The repo's live-reading surface (README + CLAUDE.md + docs/v2/) now
describes only the post-PR-7.2 architecture. v1 docs are preserved as
a labelled archive under docs/v1/.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 08:59:59 -04:00

45 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

Galaxy (MXAccess) is a Tier-A in-process driver that runs in the OtOpcUa server's .NET 10 AnyCPU process and speaks gRPC to a separately installed mxaccessgw (sibling repo at c:\Users\dohertj2\Desktop\mxaccessgw\). The gateway owns the MXAccess COM apartment, the STA pump, and the Galaxy Repository / Historian SDK on its own host; the driver itself is platform-agnostic and carries no COM or x86 bitness constraint. Project lives at src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/.

Capability Surface

GalaxyDriver (in GalaxyDriver.cs) implements IDriver, IDisposable, plus six driver capabilities — eight interfaces total.

Capability Source files
ITagDiscovery Browse/GalaxyDiscoverer.cs, Browse/GatewayGalaxyHierarchySource.cs, Browse/DataTypeMap.cs, Browse/SecurityMap.cs, Browse/AlarmRefBuilder.cs
IRediscoverable Browse/DeployWatcher.cs, Browse/GatewayGalaxyDeployWatchSource.cs
IReadable Runtime/GalaxyMxSession.cs, Runtime/MxValueDecoder.cs, Runtime/StatusCodeMap.cs
IWritable Runtime/GatewayGalaxyDataWriter.cs (+ TracedGalaxyDataWriter.cs), Runtime/MxValueEncoder.cs
ISubscribable Runtime/GatewayGalaxySubscriber.cs (+ TracedGalaxySubscriber.cs), Runtime/EventPump.cs, Runtime/SubscriptionRegistry.cs, Runtime/ReconnectSupervisor.cs
IHostConnectivityProbe Health/HostStatusAggregator.cs, Health/HostConnectivityForwarder.cs, Health/PerPlatformProbeWatcher.cs

History reads + alarm condition tracking now live in the server-layer IHistoryRouter and AlarmConditionService (PR 7.2). Galaxy no longer carries IHistoryProvider or IAlarmSource of its own.

DriverConfig JSON shape

Per src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Config/GalaxyDriverOptions.cs:

{
  "Gateway": {
    "Endpoint": "http://localhost:5120",
    "ApiKeySecretRef": "secret:galaxy-gw-api-key",
    "UseTls": true,
    "CaCertificatePath": null,
    "ConnectTimeoutSeconds": 10,
    "DefaultCallTimeoutSeconds": 30,
    "StreamTimeoutSeconds": 0
  },
  "MxAccess": {
    "ClientName": "OtOpcUa",
    "PublishingIntervalMs": 1000,
    "WriteUserId": 0,
    "EventPumpChannelCapacity": 50000
  },
  "Repository": {
    "DiscoverPageSize": 5000,
    "WatchDeployEvents": true
  },
  "Reconnect": {
    "InitialBackoffMs": 500,
    "MaxBackoffMs": 30000,
    "ReplayOnSessionLost": true
  }
}

Gateway.ApiKeySecretRef resolves through the server-side secret store (DPAPI in production, env override in dev) — the API key never appears in cleartext config. MxAccess.ClientName MUST be unique per OtOpcUa instance; redundancy pairs enforce uniqueness at install time. StreamTimeoutSeconds = 0 keeps the StreamEvents RPC alive for the lifetime of the driver.

Performance, tracing, soak

See Galaxy.Performance.md for the OpenTelemetry trace map, the per-RPC metric set (galaxy.events.dropped, channel headroom, reconnect backoff distribution), and the soak-run profile.

Parity rig + gateway setup

See Galaxy.ParityRig.md and the mxaccessgw repo for the gateway worker layout and the dev-rig recipe.


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.

Namespace Assignment

OPC UA Client is the only driver that supports either namespace kind, decided per driver instance via DriverConfig.TargetNamespaceKind:

  • Equipment: when gatewaying a remote OPC UA server that exposes raw equipment data (e.g. another vendor's OPC UA-native PLC stack). The driver must remap remote browse paths to UNS via a config-driven mapping table — remote nodes don't conform to UNS by default. Each remote node group → an Equipment row with its own UNS Area/Line/Name and stable UUID.
  • SystemPlatform: when gatewaying a remote OPC UA server that exposes processed/derived data (e.g. another System Platform endpoint). Hierarchy is preserved via Tag.FolderPath mirroring the remote browse path; no UNS conversion.

The driver enforces the namespace choice at startup — a misconfigured remote (raw signals routed to SystemPlatform, or processed data routed to Equipment without a UNS mapping) fails draft validation, not runtime.


Driver Comparison Summary

Driver Library License Stability Tier Namespace Kind .NET Target Native Subs Tag Discovery Connection Limit Required Infrastructure
Galaxy MXAccess COM + MessagePack IPC Proprietary C — out-of-process SystemPlatform .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 Equipment .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 Equipment .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 Equipment .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 Equipment .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 Equipment .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 Equipment .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 Equipment OR SystemPlatform (per-instance) .NET 10 x64 Yes (native) Browse remote Varies Certificate trust

Tier definitions and per-tier protections: see driver-stability.md. Namespace model and UNS naming rules: see plan.md §4 and config-db-schema.md (Equipment table). Equipment-namespace drivers populate Equipment rows whose UNS path comes from ServerCluster.Enterprise/Site + Equipment.Area/Line/Name; SystemPlatform-namespace drivers (Galaxy) preserve their own hierarchy via Tag.FolderPath as v1 LmxOpcUa expressed it.