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>
807 lines
45 KiB
Markdown
807 lines
45 KiB
Markdown
# 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`:
|
||
|
||
```jsonc
|
||
{
|
||
"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](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](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 |
|
||
| `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 = 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 (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 `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:
|
||
|
||
- **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 `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) | IPC serialization between Proxy and Host |
|
||
| **Named pipes** | `System.IO.Pipes` (BCL) | BCL | both sides | IPC transport with mandatory pipe ACL (server service SID only) |
|
||
|
||
### 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.
|