58 KiB
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.Hostwith the Galaxy repository database (defaultZB); 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 treetag_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 theILMXProxyServerEvents.OnDataChangeCOM event - Callback signature:
MxDataChangeHandler(itemHandle, MXSTATUS_PROXY, value, quality, timestamp) - Dispatch: STA COM event → dispatch-thread queue → OPC UA
ClearChangeMasksfan-out (decouples COM thread from UA stack lock — commitc76ab8f) - 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(commits7310925,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:
InAlarmchange → OPC UA A&CAlarmConditionStateevent (Active / Return to Normal) - Severity: Galaxy
Priority(1 = highest) mapped to OPC UA 1–1000 severity (higher = more severe) - Acknowledgment: local OPC UA ack forwards to MXAccess write on the
Ackattribute of the alarm-bearing object
History Model — Wonderware Historian (optional plugin)
- Loaded at runtime from
ZB.MOM.WW.LmxOpcUa.Historian.Aveva.dllwhenHistorian.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=1in the Galaxy DB exposeAccessLevel.HistoryRead
Error Mapping — MXAccess → Quality → OPC UA StatusCode
Byte quality (OPC DA convention) — QualityMapper.cs:
| OPC DA Quality | Category |
|---|---|
>= 192 |
Good |
64–191 |
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
ChangeDetectionServicepollsgalaxy.time_of_last_deployatChangeDetectionIntervalSeconds(default 30s)- On timestamp change,
OnGalaxyChangedfires → Host re-queries hierarchy/attributes → emitsTagSetChangedover IPC → Proxy implementsIRediscoverableand rebuilds the affected subtree in the address space - Platform-scope filter (commit
bc282b6) applied during hierarchy load whenScope=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 Win32GetMessage/DispatchMessageloop - 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
_pendingDataChangesto 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)
GalaxyRuntimeProbeManagerauto-subscribes<ObjectName>.ScanStatefor every $WinPlatform (category 1) and $AppEngine (category 3) in scope- Per-host state machine:
Unknown → Running | Stopped; transitions fire_onHostStopped/_onHostRunningcallbacks on the dispatch thread - Synthetic OPC UA nodes expose
ScanStateper 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
IHostConnectivityProbecapability interface in v2 (seeplan.md§5a)
Implementation Notes
- First Tier C out-of-process driver — uses the
Galaxy.Proxy/Galaxy.Host/Galaxy.Sharedthree-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. Seedriver-stability.mdfor the generalized contract Galaxy.Proxy(in the main server) implementsIDriver,ITagDiscovery,IRediscoverable,IReadable,IWritable,ISubscribable,IAlarmSource,IHistoryProvider,IHostConnectivityProbeGalaxy.HostownsMxAccessBridge,GalaxyRepository, alarm tracking,GalaxyRuntimeProbeManager, and the Historian plugin — no reference toCore.AbstractionsGalaxy.Sharedis .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.mdDecision #55) - Parity gate: v2 driver must pass v1
IntegrationTestssuite + 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
c76ab8fand7310925, 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_deploywatermark — 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 |
C0–C777 |
Coil (FC 01) | 512–1023 | 512 + OctalToDecimal(addr) |
T0–T377 |
Coil (FC 01) | 2048–2303 | 2048 + OctalToDecimal(addr) (status bits) |
CT0–CT177 |
Coil (FC 01) | 2560–2687 | 2560 + OctalToDecimal(addr) |
DL205-specific notes:
- 32-bit values (Float32, Int32) use low word at lower address (little-endian word order) — set
ByteOrder = LittleEndianfor 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 (V0–V377 for timers, V1000–V1177 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
SafeHandlewith finalizer callingplc_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 viaGC.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:
- 4–8 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
BadNotSupportedand surface as a configuration alert in the dashboard, not a transient failure. - Single connection per PLC with
SemaphoreSlimserialization 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
AmsRouterclass) - 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:
- Use
SymbolLoaderFactory.Create(client, SymbolLoaderSettings.Default)to get anISymbolLoader - Enumerate all symbols with full type information
- Build OPC UA address space from discovered symbols
- 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
0xF080index 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 catch0x0702, 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 aIRediscoverableinvocation, not as a connection error. UniversalIDriver.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.Proxyruns in the main OtOpcUa server (.NET 10 x64) and implementsIDriver+ capability interfaces, forwarding every call over named-pipe IPC to theFocas.HostWindows service.Focas.Hostis the only process that loadsFwlib64.dll. Seedriver-stability.mdfor 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_freelibhndlon 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
- Local OPC UA client subscribes to a node → gateway creates
MonitoredItemon remote server - Remote server pushes DataChange → gateway updates local variable +
ClearChangeMasks() - Reference counting: only one remote MonitoredItem per unique tag, regardless of local subscriber count
- Use
SessionReconnectHandlerfor 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.EventNotifieronObjectIds.Server) - Maintain local
AlarmConditionStateper remote alarm source - Forward Acknowledge/Confirm calls from local clients to remote server
History Forwarding
- Override
HistoryReadRawModifiedetc. 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.UtcNowmasks staleness. - Browse cache memory:
BrowseStrategy=Fullagainst a large remote server can cache tens of thousands of node descriptions. Per-instance budget should bound this; on breach, switch toLazystrategy 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.