Draft v2 multi-driver planning docs (docs/v2/) so Phase 0–5 work has a complete reference: rename to OtOpcUa, migrate to .NET 10 x64 (Galaxy stays .NET 4.8 x86 out-of-process), add seven new drivers behind composable capability interfaces (Modbus TCP / DL205, AB CIP, AB Legacy, S7, TwinCAT, FOCAS, OPC UA Client), introduce a central MSSQL config DB with cluster-scoped immutable generations and per-node credential binding, deploy as two-node site clusters with non-transparent redundancy and minimal per-node overrides, classify drivers by stability tier (A pure-managed / B wrapped-native / C out-of-process Windows service) with Tier C deep dives for both Galaxy and FOCAS, define per-driver test data sources (libplctag ab_server, Snap7, NModbus in-proc, TwinCAT XAR VM, FOCAS TCP stub plus native FaultShim) plus a 6-axis cross-driver test matrix, and ship a Blazor Server admin UI mirroring ScadaLink CentralUI's Bootstrap 5 / LDAP cookie auth / dark-sidebar look-and-feel — 106 numbered decisions across six docs (plan.md, driver-specs.md, driver-stability.md, test-data-sources.md, config-db-schema.md, admin-ui.md), DRAFT only and intentionally not yet wired to code.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
499
docs/v2/test-data-sources.md
Normal file
499
docs/v2/test-data-sources.md
Normal file
@@ -0,0 +1,499 @@
|
||||
# Test Data Sources — OtOpcUa v2
|
||||
|
||||
> **Status**: DRAFT — companion to `plan.md`. Identifies the simulator/emulator/stub each driver will be developed and integration-tested against, so a developer laptop and a CI runner can exercise every driver without physical hardware.
|
||||
>
|
||||
> **Branch**: `v2`
|
||||
> **Created**: 2026-04-17
|
||||
|
||||
## Scope
|
||||
|
||||
The v2 plan adds eight drivers (Galaxy, Modbus TCP, AB CIP, AB Legacy, S7, TwinCAT, FOCAS, OPC UA Client). Each needs a repeatable, low-friction data source for:
|
||||
|
||||
- **Inner-loop development** — a developer running tests on their own machine
|
||||
- **CI integration tests** — automated runs against a known-good fixture
|
||||
- **Pre-release fidelity validation** — at least one "golden" rig with the highest-fidelity option available, even if paid/heavy
|
||||
|
||||
Two drivers are already covered and are **out of scope** for this document:
|
||||
|
||||
| Driver | Existing source | Why no work needed |
|
||||
|--------|-----------------|---------------------|
|
||||
| Galaxy | Real System Platform Galaxy on the dev machine | MXAccess requires a deployed ArchestrA Platform anyway; the dev box already has one |
|
||||
| OPC UA Client | OPC Foundation `ConsoleReferenceServer` from UA-.NETStandard | Reference-grade simulator from the same SDK we depend on; trivial to spin up |
|
||||
|
||||
The remaining six drivers are the subject of this document.
|
||||
|
||||
## Standard Test Scenario
|
||||
|
||||
Each simulator must expose a fixture that lets cross-driver integration tests exercise three orthogonal axes: the **data type matrix**, the **behavior matrix**, and **capability-gated extras**. v1 LMX testing already exercises ~12 Galaxy types plus 1D arrays plus security classifications plus historized attrs — the v2 fixture per driver has to reach at least that bar.
|
||||
|
||||
### A. Data type matrix (every driver, scalar and array)
|
||||
|
||||
Each simulator exposes one tag per cell where the protocol supports the type natively:
|
||||
|
||||
| Type family | Scalar | 1D array (small, ~10) | 1D array (large, ~500) | Notes |
|
||||
|-------------|:------:|:---------------------:|:----------------------:|-------|
|
||||
| Bool | ✔ | ✔ | — | Discrete subscription test |
|
||||
| Int16 (signed) | ✔ | ✔ | ✔ | Where protocol distinguishes from Int32 |
|
||||
| Int32 (signed) | ✔ | ✔ | ✔ | Universal |
|
||||
| Int64 | ✔ | ✔ | — | Where protocol supports it |
|
||||
| UInt16 / UInt32 | ✔ | — | — | Where protocol distinguishes signed/unsigned (Modbus, S7) |
|
||||
| Float32 | ✔ | ✔ | ✔ | Endianness test |
|
||||
| Float64 | ✔ | ✔ | — | Where protocol supports it |
|
||||
| String | ✔ | ✔ (Galaxy/AB/TwinCAT) | — | Include empty, ASCII, UTF-8/Unicode, max-length |
|
||||
| DateTime | ✔ | — | — | Galaxy, TwinCAT, OPC UA Client only |
|
||||
|
||||
Large arrays (~500 elements) catch paged-read, fragmentation, and PDU-batching bugs that small arrays miss.
|
||||
|
||||
### B. Behavior matrix (applied to a subset of the type matrix)
|
||||
|
||||
| Behavior | Applied to | Validates |
|
||||
|----------|------------|-----------|
|
||||
| **Static read** | One tag per type in matrix A | Type mapping, value decoding |
|
||||
| **Ramp** | Int32, Float32 | Subscription delivery cadence, source timestamps |
|
||||
| **Write-then-read-back** | Bool, Int32, Float32, String | Round-trip per type family, idempotent-write path |
|
||||
| **Array element write** | Int32[10] | Partial-write paths (where protocol supports them); whole-array replace where it doesn't |
|
||||
| **Large-array read** | Int32[500] | Paged reads, PDU batching, no truncation |
|
||||
| **Bool toggle on cadence** | Bool | Discrete subscription, change detection |
|
||||
| **Bad-quality on demand** | Any tag | Polly circuit-breaker → quality fan-out |
|
||||
| **Disconnect / reconnect** | Whole simulator | Reconnect, subscription replay, status dashboard, redundancy failover |
|
||||
|
||||
### C. Capability-gated extras (only where the driver supports them)
|
||||
|
||||
| Extra | Drivers | Fixture requirement |
|
||||
|-------|---------|---------------------|
|
||||
| **Security / access levels** | Galaxy, OPC UA Client | At least one read-only and one read-write tag of the same type |
|
||||
| **Alarms** | Galaxy, FOCAS, OPC UA Client | One alarm that fires after N seconds; one that the test can acknowledge; one that auto-clears |
|
||||
| **HistoryRead** | Galaxy, OPC UA Client | One historized tag with a known back-fill of >100 samples spanning >1 hour |
|
||||
| **String edge cases** | All with String support | Empty string, max-length string, embedded nulls, UTF-8 multi-byte chars |
|
||||
| **Endianness round-trip** | Modbus, S7 | Float32 written by test, read back, byte-for-byte equality |
|
||||
|
||||
Each driver section below maps these axes to concrete addresses/tags in that protocol's namespace. Where the protocol has no native equivalent (e.g. Modbus has no String type), the row is marked **N/A** and the driver-side tests skip it.
|
||||
|
||||
---
|
||||
|
||||
## 1. Modbus TCP (and DL205)
|
||||
|
||||
### Recommendation
|
||||
|
||||
**Default**: `oitc/modbus-server` Docker image for CI; in-process `NModbus` slave for xUnit fixtures.
|
||||
|
||||
Both speak real Modbus TCP wire protocol. The Docker image is a one-line `docker run` for whole-system tests; the in-proc slave gives per-test deterministic state with no new dependencies (NModbus is already on the driver-side dependency list).
|
||||
|
||||
### Options Evaluated
|
||||
|
||||
| Option | License | Platform | Notes |
|
||||
|--------|---------|----------|-------|
|
||||
| **oitc/modbus-server** ([Docker Hub](https://hub.docker.com/r/oitc/modbus-server), [GitHub](https://github.com/cybcon/modbus-server)) | MIT | Docker | YAML preload of all 4 register areas; `docker run -p 502:502` |
|
||||
| **NModbus `ModbusTcpSlave`** ([GitHub](https://github.com/NModbus/NModbus)) | MIT | In-proc .NET 10 | ~20 LOC fixture; programmatic register control |
|
||||
| **diagslave** ([modbusdriver.com](https://www.modbusdriver.com/diagslave.html)) | Free (proprietary) | Win/Linux/QNX | Single binary; free mode times out hourly |
|
||||
| **EasyModbusTCP** | LGPL | .NET / Java / Python | MSI installer |
|
||||
| **ModbusPal** ([SourceForge](https://sourceforge.net/projects/modbuspal/)) | BSD | Java | Register automation scripting; needs a JVM |
|
||||
|
||||
### DL205 Coverage
|
||||
|
||||
DL205 PLCs are accessed via H2-ECOM100, which exposes plain Modbus TCP. The `AddressFormat=DL205` feature is purely an octal-to-decimal **address translation** in the driver — the simulator only needs to expose the underlying Modbus registers. Unit-test the translation by preloading specific Modbus addresses (`HR 1024 = V2000`, `DI 15 = X17`, `Coil 8 = Y10`) and asserting the driver reads them via DL205 notation.
|
||||
|
||||
### Native Type Coverage
|
||||
|
||||
Modbus has no native String, DateTime, or Int64 — those rows are skipped on this driver. Native primitives are coil/discrete-input (Bool) and 16-bit registers; everything wider is composed from contiguous registers with explicit byte/word ordering.
|
||||
|
||||
| Type | Modbus mapping | Supported |
|
||||
|------|----------------|:---------:|
|
||||
| Bool | Coil / DI | ✔ |
|
||||
| Int16 / UInt16 | One HR/IR | ✔ |
|
||||
| Int32 / UInt32 | Two HR (big-endian word) | ✔ |
|
||||
| Float32 | Two HR | ✔ |
|
||||
| Float64 | Four HR | ✔ |
|
||||
| String | — | N/A |
|
||||
| DateTime | — | N/A |
|
||||
|
||||
### Standard Scenario Mapping
|
||||
|
||||
| Axis | Address |
|
||||
|------|---------|
|
||||
| Bool scalar / Bool[10] | Coil 1000 / Coils 1010–1019 |
|
||||
| Int16 scalar / Int16[10] / Int16[500] | HR 0 / HR 10–19 / HR 500–999 |
|
||||
| Int32 scalar / Int32[10] | HR 2000–2001 / HR 2010–2029 |
|
||||
| UInt16 scalar | HR 50 |
|
||||
| UInt32 scalar | HR 60–61 |
|
||||
| Float32 scalar / Float32[10] / Float32[500] | HR 3000–3001 / HR 3010–3029 / HR 4000–4999 |
|
||||
| Float64 scalar | HR 5000–5003 |
|
||||
| Ramp (Int32) | HR 100–101 — 0→1000 @ 1 Hz |
|
||||
| Ramp (Float32) | HR 110–111 — sine wave |
|
||||
| Write-read-back (Bool / Int32 / Float32) | Coil 1100 / HR 2100–2101 / HR 3100–3101 |
|
||||
| Array element write (Int32[10]) | HR 2200–2219 |
|
||||
| Bool toggle on cadence | Coil 0 — toggles @ 2 Hz |
|
||||
| Endianness round-trip (Float32) | HR 6000–6001, written then read |
|
||||
| Bad on demand | Coil 99 — write `1` to make the slave drop the TCP socket |
|
||||
| Disconnect | restart container / dispose in-proc slave |
|
||||
|
||||
### Gotchas
|
||||
|
||||
- **Byte order** is simulator-configurable. Pin a default in our test harness (big-endian word, big-endian byte) and document.
|
||||
- **diagslave free mode** restarts every hour — fine for inner-loop, not CI.
|
||||
- **Docker image defaults registers to 0** — ship a YAML config in the test repo.
|
||||
|
||||
---
|
||||
|
||||
## 2. Allen-Bradley CIP (ControlLogix / CompactLogix)
|
||||
|
||||
### Recommendation
|
||||
|
||||
**Default**: `ab_server` from the libplctag repo. Real CIP-over-EtherNet/IP, written by the same project that owns the libplctag NuGet our driver consumes — every tag shape the simulator handles is one the driver can address.
|
||||
|
||||
**Pre-release fidelity tier**: Studio 5000 Logix Emulate on one designated "golden" dev box for cases that need full UDT / Program-scope fidelity. Not a default because of cost (~$1k+ Pro-edition add-on) and toolchain weight.
|
||||
|
||||
### Options Evaluated
|
||||
|
||||
| Option | License | Platform | Notes |
|
||||
|--------|---------|----------|-------|
|
||||
| **ab_server** ([libplctag](https://github.com/libplctag/libplctag), [kyle-github/ab_server](https://github.com/kyle-github/ab_server)) | MIT | Win/Linux/macOS | Build from source; CI-grade fixture |
|
||||
| **Studio 5000 Logix Emulate** | Rockwell paid (~$1k+) | Windows | 100% firmware fidelity |
|
||||
| **Factory I/O + PLCSIM** | Paid | Windows | Visual sim, not raw CIP |
|
||||
|
||||
### Native Type Coverage
|
||||
|
||||
| Type | CIP mapping | Supported by ab_server |
|
||||
|------|-------------|:----------------------:|
|
||||
| Bool | BOOL | ✔ |
|
||||
| Int16 | INT | ✔ |
|
||||
| Int32 | DINT | ✔ |
|
||||
| Int64 | LINT | ✔ |
|
||||
| Float32 | REAL | ✔ |
|
||||
| Float64 | LREAL | ✔ |
|
||||
| String | STRING (built-in struct) | ✔ basic only |
|
||||
| DateTime | — | N/A |
|
||||
| UDT | user-defined STRUCT | not in ab_server CI scope |
|
||||
|
||||
### Standard Scenario Mapping
|
||||
|
||||
| Axis | Tag |
|
||||
|------|-----|
|
||||
| Bool scalar / Bool[10] | `bTest` / `abTest[10]` |
|
||||
| Int16 scalar / Int16[10] | `iTest` / `aiTest[10]` |
|
||||
| Int32 scalar / Int32[10] / Int32[500] | `diTest` / `adiTest[10]` / `adiBig[500]` |
|
||||
| Int64 scalar | `liTest` |
|
||||
| Float32 scalar / Float32[10] / Float32[500] | `Motor1_Speed` / `aReal[10]` / `aRealBig[500]` |
|
||||
| Float64 scalar | `Motor1_Position` (LREAL) |
|
||||
| String scalar / String[10] | `sIdentity` / `asNames[10]` |
|
||||
| Ramp (Float32) | `Motor1_Speed` (0→60 @ 0.5 Hz) |
|
||||
| Ramp (Int32) | `StepCounter` (0→10000 @ 1 Hz) |
|
||||
| Write-read-back (Bool / Int32 / Float32 / String) | `bWriteTarget` / `StepIndex` / `rSetpoint` / `sLastWrite` |
|
||||
| Array element write (Int32[10]) | `adiWriteTarget[10]` |
|
||||
| Bool toggle on cadence | `Flags[0]` toggling @ 2 Hz; `Flags[1..15]` latched |
|
||||
| Bad on demand | Test harness flag that makes ab_server refuse the next read |
|
||||
| Disconnect | Stop ab_server process |
|
||||
|
||||
### Gotchas
|
||||
|
||||
- **ab_server tag-type coverage is finite** (BOOL, DINT, REAL, arrays, basic strings). UDTs and `Program:` scoping are not fully implemented. Document an "ab_server-supported tag set" in the harness and exclude the rest from default CI; UDT coverage moves to the Studio 5000 Emulate golden-box tier.
|
||||
- CIP has no native subscriptions, so polling behavior matches real hardware.
|
||||
|
||||
---
|
||||
|
||||
## 3. Allen-Bradley Legacy (SLC 500 / MicroLogix, PCCC)
|
||||
|
||||
### Recommendation
|
||||
|
||||
**Default**: `ab_server` in PCCC mode, with a small in-repo PCCC stub for any file types ab_server doesn't fully cover (notably Timer/Counter `.ACC`/`.PRE`/`.DN` decomposition).
|
||||
|
||||
The same binary covers AB CIP and AB Legacy via a `plc=slc500` (or `micrologix`) flag, so we get one fixture for two drivers. If the timer/counter fidelity is too thin in practice, fall back to a ~200-line `TcpListener` stub answering the specific PCCC function codes the driver issues.
|
||||
|
||||
### Options Evaluated
|
||||
|
||||
| Option | License | Platform | Notes |
|
||||
|--------|---------|----------|-------|
|
||||
| **ab_server PCCC mode** | MIT | cross-platform | Same binary as AB CIP; partial T/C/R structure fidelity |
|
||||
| **Rockwell RSEmulate 500** | Rockwell legacy paid | Windows | EOL, ages poorly on modern Windows |
|
||||
| **In-repo PCCC stub** | Own | .NET 10 | Fallback only — covers what we P/Invoke |
|
||||
|
||||
### Native Type Coverage
|
||||
|
||||
PCCC types are file-based. Int32/Float64/DateTime are not native to SLC/MicroLogix.
|
||||
|
||||
| Type | PCCC mapping | Supported |
|
||||
|------|--------------|:---------:|
|
||||
| Bool | `B3:n/b` (bit in B file) | ✔ |
|
||||
| Int16 | `N7:n` | ✔ |
|
||||
| Int32 | — (decompose in driver from two N words) | partial |
|
||||
| Float32 | `F8:n` | ✔ |
|
||||
| String | `ST9:n` | ✔ |
|
||||
| Timer struct | `T4:n.ACC` / `.PRE` / `/DN` | ✔ |
|
||||
| Counter struct | `C5:n.ACC` / `.PRE` / `/DN` | ✔ |
|
||||
|
||||
### Standard Scenario Mapping
|
||||
|
||||
| Axis | Address |
|
||||
|------|---------|
|
||||
| Bool scalar / Bool[16] | `B3:0/0` / `B3:0` (treated as bit array) |
|
||||
| Int16 scalar / Int16[10] / Int16[500] | `N7:0` / `N7:0..9` / `N10:0..499` (separate file) |
|
||||
| Float32 scalar / Float32[10] | `F8:0` / `F8:0..9` |
|
||||
| String scalar / String[10] | `ST9:0` / `ST9:0..9` |
|
||||
| Ramp (Int16) | `N7:1` 0→1000 |
|
||||
| Ramp (Float32) | `F8:1` sine wave |
|
||||
| Write-read-back (Bool / Int16 / Float32 / String) | `B3:1/0` / `N7:100` / `F8:100` / `ST9:100` |
|
||||
| Array element write (Int16[10]) | `N7:200..209` |
|
||||
| Timer fidelity | `T4:0.ACC`, `T4:0.PRE`, `T4:0/DN` |
|
||||
| Counter fidelity | `C5:0.ACC`, `C5:0.PRE`, `C5:0/DN` |
|
||||
| Connection-limit refusal | Driver harness toggle to simulate 4-conn limit |
|
||||
| Bad on demand | Connection-refused toggle |
|
||||
|
||||
### Gotchas
|
||||
|
||||
- **Real SLC/MicroLogix enforce 4–8 connection limits**; ab_server does not. Add a test-only toggle in the driver (or in the stub) to refuse connections so we exercise the queuing path.
|
||||
- Timer/Counter structures are the most likely place ab_server fidelity falls short — design the test harness so we can drop in a stub for those specific files without rewriting the rest.
|
||||
|
||||
---
|
||||
|
||||
## 4. Siemens S7 (S7-300/400/1200/1500)
|
||||
|
||||
### Recommendation
|
||||
|
||||
**Default**: Snap7 Server. Real S7comm over ISO-on-TCP, free, cross-platform, and the same wire protocol the S7netplus driver emits.
|
||||
|
||||
**Pre-release fidelity tier**: PLCSIM Advanced on one golden dev box (7-day renewable trial; paid for production). Required for true firmware-level validation and for testing programs that include actual ladder logic.
|
||||
|
||||
### Options Evaluated
|
||||
|
||||
| Option | License | Platform | Notes |
|
||||
|--------|---------|----------|-------|
|
||||
| **Snap7 Server** ([snap7](https://snap7.sourceforge.net/snap7_server.html)) | LGPLv3 | Win/Linux/macOS, 32/64-bit | CP emulator; no PLC logic execution |
|
||||
| **PLCSIM Advanced** ([Siemens](https://www.siemens.com/en-us/products/simatic/s7-plcsim-advanced/)) | Siemens trial / paid | Windows + VM | Full S7-1500 fidelity, runs TIA programs |
|
||||
| **S7-PLCSIM (classic)** | Bundled with TIA | Windows | S7-300/400; no external S7comm without PLCSIM Advanced |
|
||||
|
||||
### Native Type Coverage
|
||||
|
||||
S7 has a rich native type system; Snap7 supports the wire-level read/write of all of them via DB byte access.
|
||||
|
||||
| Type | S7 mapping | Notes |
|
||||
|------|------------|-------|
|
||||
| Bool | `M0.0`, `DBn.DBXm.b` | ✔ |
|
||||
| Byte / Word / DWord | `DBn.DBB`, `.DBW`, `.DBD` | unsigned |
|
||||
| Int (Int16) / DInt (Int32) | `DBn.DBW`, `.DBD` | signed, big-endian |
|
||||
| LInt (Int64) | `DBn.DBLW` | S7-1500 only |
|
||||
| Real (Float32) / LReal (Float64) | `.DBD`, `.DBLW` | big-endian IEEE |
|
||||
| String | `DBn.DBB[]` (length-prefixed: max+actual+chars) | length-prefixed |
|
||||
| Char / WChar | byte / word with semantic | |
|
||||
| Date / Time / DT / TOD | structured byte layouts | |
|
||||
|
||||
### Standard Scenario Mapping
|
||||
|
||||
All in `DB1` unless noted; host script provides ramp behavior since Snap7 has no logic.
|
||||
|
||||
| Axis | Address |
|
||||
|------|---------|
|
||||
| Bool scalar / Bool[16] | `M0.0` / `DB1.DBX0.0..1.7` |
|
||||
| Int16 scalar / Int16[10] / Int16[500] | `DB1.DBW10` / `DB1.DBW20..38` / `DB2.DBW0..998` |
|
||||
| Int32 scalar / Int32[10] | `DB1.DBD100` / `DB1.DBD110..146` |
|
||||
| Int64 scalar | `DB1.DBLW200` |
|
||||
| UInt16 / UInt32 | `DB1.DBW300` / `DB1.DBD310` |
|
||||
| Float32 scalar / Float32[10] / Float32[500] | `DB1.DBD400` / `DB1.DBD410..446` / `DB3.DBD0..1996` |
|
||||
| Float64 scalar | `DB1.DBLW500` |
|
||||
| String scalar / String[10] | `DB1.STRING600` (max 254) / `DB1.STRING700..` |
|
||||
| DateTime scalar | `DB1.DT800` |
|
||||
| Ramp (Int16) | `DB1.DBW10` 0→1000 @ 1 Hz (host script) |
|
||||
| Ramp (Float32) | `DB1.DBD400` sine (host script) |
|
||||
| Write-read-back (Bool / Int16 / Float32 / String) | `M1.0` / `DB1.DBW900` / `DB1.DBD904` / `DB1.STRING908` |
|
||||
| Array element write (Int16[10]) | `DB1.DBW1000..1018` |
|
||||
| Endianness round-trip (Float32) | `DB1.DBD1100` |
|
||||
| Big-endian Int32 check | `DB2.DBD0` |
|
||||
| PUT/GET disabled simulation | Refuse-connection toggle |
|
||||
| Bad on demand | Stop Snap7 host process |
|
||||
| Re-download | Swap DB definitions (exercises symbol-version handling) |
|
||||
|
||||
### Gotchas
|
||||
|
||||
- **Snap7 is not a SoftPLC** — no logic runs. Ramps must be scripted by the test host writing to a DB on a timer.
|
||||
- **PUT/GET enforcement** is a property of real S7-1200/1500 (disabled by default in TIA). Snap7 doesn't enforce it. Add a test case that simulates "PUT/GET disabled" via a deliberately refused connection.
|
||||
- **Snap7 binary bitness**: some distributions are 32-bit only — match the test harness bitness.
|
||||
- **PLCSIM Advanced in VMs** is officially supported but trips up on nested virtualization and time-sync.
|
||||
|
||||
---
|
||||
|
||||
## 5. Beckhoff TwinCAT (ADS)
|
||||
|
||||
### Recommendation
|
||||
|
||||
**Default**: TwinCAT XAR runtime in a dev VM under Beckhoff's 7-day renewable dev/test trial. Required because TwinCAT is the only one of three native-subscription drivers (Galaxy, TwinCAT, OPC UA Client) that doesn't have a separate stub option — exercising native ADS notifications without a real XAR would hide the most important driver bugs.
|
||||
|
||||
The OtOpcUa test process talks to the VM over the network using `Beckhoff.TwinCAT.Ads` v6's in-process router (`AmsTcpIpRouter`), so individual dev machines and CI runners don't need the full TwinCAT stack installed locally.
|
||||
|
||||
### Options Evaluated
|
||||
|
||||
| Option | License | Platform | Notes |
|
||||
|--------|---------|----------|-------|
|
||||
| **TwinCAT 3 XAE + XAR** ([Beckhoff](https://www.beckhoff.com/en-us/products/automation/twincat/), [licensing](https://infosys.beckhoff.com/content/1033/tc3_licensing/921947147.html)) | Free dev download; 7-day renewable trial; paid for production | Windows + Hyper-V/VMware | Full ADS fidelity with real PLC runtime |
|
||||
| **Beckhoff.TwinCAT.Ads.TcpRouter** ([NuGet](https://www.nuget.org/packages/Beckhoff.TwinCAT.Ads.TcpRouter)) | Free, bundled | NuGet, in-proc | Router only — needs a real XAR on the other end |
|
||||
| **TwinCAT XAR in Docker** ([Beckhoff/TC_XAR_Container_Sample](https://github.com/Beckhoff/TC_XAR_Container_Sample)) | Same trial license; no prebuilt image | **Linux host with PREEMPT_RT** | Evaluated and rejected — see "Why not Docker" below |
|
||||
| **Roll-our-own ADS stub** | Own | .NET 10 | Would have to fake notifications; significant effort |
|
||||
|
||||
### Why not Docker (evaluated 2026-04-17)
|
||||
|
||||
Beckhoff publishes an [official sample](https://github.com/Beckhoff/TC_XAR_Container_Sample) for running XAR in a container, but it's not a viable replacement for the VM in our environment. Four blockers:
|
||||
|
||||
1. **Linux-only host with PREEMPT_RT.** The container is a Linux container that requires a Beckhoff RT Linux host (or equivalent PREEMPT_RT kernel). Docker Desktop on Windows forces Hyper-V, which [TwinCAT runtime cannot coexist with](https://hellotwincat.dev/disable-hyper-v-vs-twincat-problem-solution/). Our CI and dev boxes are Windows.
|
||||
2. **ADS-over-MQTT, not classic TCP/48898.** The official sample exposes ADS through a containerized mosquitto broker. Real field deployments use TCP/48898; testing against MQTT reduces the fidelity we're paying for.
|
||||
3. **XAE-on-Windows still required for project deployment.** No headless `.tsproj` deploy path exists. We don't escape the Windows dependency by going to Docker.
|
||||
4. **Same trial license either way.** No licensing win — 7-day renewable applies identically to bare-metal XAR and containerized XAR.
|
||||
|
||||
Revisit if Beckhoff publishes a prebuilt image with classic TCP ADS exposure, or if our CI fleet gains a Linux RT runner. Until then, Windows VM with XAR + XAE + trial license is the pragmatic answer.
|
||||
|
||||
### Native Type Coverage
|
||||
|
||||
TwinCAT exposes the full IEC 61131-3 type system; the test PLC project includes one symbol per cell.
|
||||
|
||||
| Type | TwinCAT mapping | Supported |
|
||||
|------|-----------------|:---------:|
|
||||
| Bool | `BOOL` | ✔ |
|
||||
| Int16 / UInt16 | `INT` / `UINT` | ✔ |
|
||||
| Int32 / UInt32 | `DINT` / `UDINT` | ✔ |
|
||||
| Int64 / UInt64 | `LINT` / `ULINT` | ✔ |
|
||||
| Float32 / Float64 | `REAL` / `LREAL` | ✔ |
|
||||
| String | `STRING(255)` | ✔ |
|
||||
| WString | `WSTRING(255)` | ✔ Unicode coverage |
|
||||
| DateTime | `DT`, `DATE`, `TOD`, `TIME`, `LTIME` | ✔ |
|
||||
| STRUCT / ENUM / ALIAS | user-defined | ✔ |
|
||||
|
||||
### Standard Scenario Mapping
|
||||
|
||||
In a tiny test project — `MAIN` (PLC code) + `GVL` (constants and write targets):
|
||||
|
||||
| Axis | Symbol |
|
||||
|------|--------|
|
||||
| Bool scalar / Bool[10] | `GVL.bTest` / `GVL.abTest` |
|
||||
| Int16 / Int32 / Int64 scalars | `GVL.iTest` / `GVL.diTest` / `GVL.liTest` |
|
||||
| UInt16 / UInt32 scalars | `GVL.uiTest` / `GVL.udiTest` |
|
||||
| Int32[10] / Int32[500] | `GVL.adiTest` / `GVL.adiBig` |
|
||||
| Float32 / Float64 scalars | `GVL.rTest` / `GVL.lrTest` |
|
||||
| Float32[10] / Float32[500] | `GVL.arTest` / `GVL.arBig` |
|
||||
| String / WString / String[10] | `GVL.sIdentity` / `GVL.wsIdentity` / `GVL.asNames` |
|
||||
| DateTime (DT) | `GVL.dtTimestamp` |
|
||||
| STRUCT member access | `GVL.fbMotor.rSpeed` (REAL inside FB) |
|
||||
| Ramp (DINT) | `MAIN.nRamp` — PLC increments each cycle |
|
||||
| Ramp (REAL) | `MAIN.rSine` — PLC computes sine |
|
||||
| Write-read-back (Bool / DINT / REAL / STRING / WSTRING) | `GVL.bWriteTarget` / `GVL.diWriteTarget` / `GVL.rWriteTarget` / `GVL.sWriteTarget` / `GVL.wsWriteTarget` |
|
||||
| Array element write (DINT[10]) | `GVL.adiWriteTarget` |
|
||||
| Native ADS notification | every scalar above subscribed via OnDataChange |
|
||||
| Bad on demand | Stop the runtime — driver gets port-not-found |
|
||||
| Re-download | Re-deploy the project to exercise symbol-version-changed (`0x0702`) |
|
||||
|
||||
### Gotchas
|
||||
|
||||
- **AMS route table** — XAR refuses ADS connections from unknown hosts. Test setup must add a backroute for each dev machine and CI runner (scriptable via `AddRoute` on the NuGet API).
|
||||
- **7-day trial reset** requires a click in the XAE UI; investigate scripting it for unattended CI.
|
||||
- **Symbol-version-changed** is the hardest path to exercise — needs a PLC re-download mid-test, so structure the integration suite to accommodate that step.
|
||||
|
||||
---
|
||||
|
||||
## 6. FANUC FOCAS (FOCAS2)
|
||||
|
||||
### Recommendation
|
||||
|
||||
**No good off-the-shelf simulator exists. Build two test artifacts** that cover different layers of the FOCAS surface:
|
||||
|
||||
1. **`Driver.Focas.TestStub`** — a TCP listener mimicking a real CNC over the FOCAS wire protocol. Covers functional behavior (reads, writes, ramps, alarms, network failures).
|
||||
2. **`Driver.Focas.FaultShim`** — a test-only native DLL that masquerades as `Fwlib64.dll` and injects faults inside the host process (AVs, handle leaks, orphan handles). Covers the stability-recovery paths in `driver-stability.md` that the TCP stub physically cannot exercise.
|
||||
|
||||
CNC Guide is the only off-the-shelf FOCAS-capable simulator and gating every dev rig on a FANUC purchase isn't viable. There are no open-source FOCAS server stubs at useful fidelity. The FOCAS SDK license is already secured (decision #61), so we own the API contract — build both artifacts ourselves against captured Wireshark traces from a real CNC.
|
||||
|
||||
### Artifact 1 — TCP Stub (functional coverage)
|
||||
|
||||
A `TcpListener` on port 8193 that answers only the FOCAS2 functions the driver P/Invokes:
|
||||
|
||||
```
|
||||
cnc_allclibhndl3, cnc_freelibhndl, cnc_sysinfo, cnc_statinfo,
|
||||
cnc_actf, cnc_acts, cnc_absolute, cnc_machine, cnc_rdaxisname,
|
||||
cnc_rdspmeter, cnc_rdprgnum, cnc_rdparam, cnc_rdalmmsg,
|
||||
pmc_rdpmcrng, cnc_rdmacro, cnc_getfigure
|
||||
```
|
||||
|
||||
Capture the wire framing once against a real CNC (or a colleague's CNC Guide seat), then the stub becomes a fixed-point reference. For pre-release validation, run the driver against a real CNC.
|
||||
|
||||
**Covers**: read/write/poll behavior, scaled-integer round-trip, alarm fire/clear, network slowness, network hang, network disconnect, FOCAS-error-code → StatusCode mapping. Roughly 80% of real-world FOCAS failure modes.
|
||||
|
||||
### Artifact 2 — FaultShim (native fault injection, host-side)
|
||||
|
||||
A separate test-only native DLL named `Fwlib64.dll` that exports the same function surface but instead of calling FANUC's library, performs configurable fault behaviors: deliberate AV at a chosen call site, return success but never release allocated buffers (memory leak), accept `cnc_freelibhndl` but keep handle table populated (orphan handle), simulate a wedged native call that doesn't return.
|
||||
|
||||
Activated by DLL search-path order in the test fixture only; production builds load FANUC's real `Fwlib64.dll`. The Host code is unchanged — it just experiences different symptoms depending on which DLL is loaded.
|
||||
|
||||
**Covers**: supervisor respawn after AV, post-mortem MMF readability after hard crash, watchdog → recycle path on simulated leak, Abandoned-handle path when a wedged native call exceeds recycle grace. The remaining ~20% of failure modes that live below the network layer.
|
||||
|
||||
### What neither artifact covers
|
||||
|
||||
Vendor-specific Fwlib quirks that depend on the real `Fwlib64.dll` interacting with a real CNC firmware version. These remain hardware/manual-test-only and are validated on the pre-release real-CNC tier, not in CI.
|
||||
|
||||
### Options Evaluated
|
||||
|
||||
| Option | License | Platform | Notes |
|
||||
|--------|---------|----------|-------|
|
||||
| **FANUC CNC Guide** ([FANUC](https://www.fanuc.co.jp/en/product/cnc/f_ncguide.html)) | Paid, dealer-ordered | Windows | High fidelity; FOCAS-over-Ethernet not enabled in all editions |
|
||||
| **FANUC Roboguide** | Paid | Windows | Robot-focused, not CNC FOCAS |
|
||||
| **MTConnect agents** | various | — | Different protocol; not a FOCAS source |
|
||||
| **Public FOCAS stubs** | — | — | None at useful fidelity |
|
||||
| **In-repo TCP stub + FaultShim DLL** | Own | .NET 10 + native | Recommended path — two artifacts, see above |
|
||||
|
||||
### Native Type Coverage
|
||||
|
||||
FOCAS does not have a tag system in the conventional sense — it has a fixed set of API calls returning structured CNC data. Tag families the driver exposes:
|
||||
|
||||
| Type | FOCAS source | Notes |
|
||||
|------|--------------|-------|
|
||||
| Bool | PMC bit | discrete inputs/outputs |
|
||||
| Int16 / Int32 | PMC R/D word & dword, status fields | |
|
||||
| Int64 | composed from PMC | rare |
|
||||
| Float32 / Float64 | macros (`cnc_rdmacro`), some params | |
|
||||
| Scaled integer | position values + `cnc_getfigure()` decimal places | THE FOCAS-specific bug surface |
|
||||
| String | alarm messages, program names | length-bounded |
|
||||
| Array | PMC ranges (`pmc_rdpmcrng`), per-axis arrays | |
|
||||
|
||||
### Standard Scenario Mapping
|
||||
|
||||
| Axis | Element |
|
||||
|------|---------|
|
||||
| Static identity (struct) | `cnc_sysinfo` — series, version, axis count |
|
||||
| Bool scalar / Bool[16] | PMC `G0.0` / PMC `G0` (bits 0–15) |
|
||||
| Int16 / Int32 PMC scalars | PMC `R200` / PMC `D300` |
|
||||
| Int32 PMC array (small / large) | PMC `R1000..R1019` / PMC `R5000..R5499` |
|
||||
| Float64 macro variable | macro `#100` |
|
||||
| Macro array | macro `#500..#509` |
|
||||
| String | active program name; alarm message text |
|
||||
| Scaled integer round-trip | X-axis position (decimal-place conversion via `cnc_getfigure`) |
|
||||
| State machine | `RunState` cycling Stop → Running → Hold |
|
||||
| Ramp (scaled int) | X-axis position 0→100.000 mm |
|
||||
| Step (Int32) | `ActualFeedRate` stepping on `cnc_actf` |
|
||||
| Write-read-back (PMC Int32) | PMC `R100` 32-bit scratch register |
|
||||
| PMC array element write | PMC `R200..R209` |
|
||||
| Alarms | one alarm appears after N seconds; one is acknowledgeable; one auto-clears |
|
||||
| Bad on demand | Stub closes the socket on a marker request |
|
||||
|
||||
### Gotchas
|
||||
|
||||
- **FOCAS wire framing is proprietary** — stub fidelity depends entirely on Wireshark captures from a real CNC. Plan to do that capture early.
|
||||
- **Fwlib is thread-unsafe per handle** — the stub must serialize so we don't accidentally test threading behavior the driver can't rely on in production.
|
||||
- **Scaled-integer position values** require the stub to return a credible `cnc_getfigure()` so the driver's decimal-place conversion is exercised.
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
| Driver | Primary | License | Fallback / fidelity tier |
|
||||
|--------|---------|---------|---------------------------|
|
||||
| Galaxy | Real Galaxy on dev box | — | (n/a — already covered) |
|
||||
| Modbus TCP / DL205 | `oitc/modbus-server` + NModbus in-proc | MIT | diagslave for wire-inspection |
|
||||
| AB CIP | libplctag `ab_server` | MIT | Studio 5000 Logix Emulate (golden box) |
|
||||
| AB Legacy | `ab_server` PCCC mode + in-repo PCCC stub | MIT | Real SLC/MicroLogix on lab rig |
|
||||
| S7 | Snap7 Server | LGPLv3 | PLCSIM Advanced (golden box) |
|
||||
| TwinCAT | TwinCAT XAR in dev VM | Free trial | — |
|
||||
| FOCAS | **In-repo `Driver.Focas.TestStub` (TCP)** + `Driver.Focas.FaultShim` (native DLL) | Own code | CNC Guide / real CNC pre-release |
|
||||
| OPC UA Client | OPC Foundation `ConsoleReferenceServer` | OPC Foundation | — |
|
||||
|
||||
Six of eight drivers have a free, scriptable, cross-platform test source we can check into CI. TwinCAT requires a VM but no recurring cost. FOCAS is the one case with no public answer — we own the stub. The driver specs in `driver-specs.md` enumerate every API call we make, which scopes the FOCAS stub.
|
||||
|
||||
## Resolved Defaults
|
||||
|
||||
The questions raised by the initial draft are resolved as planning defaults below. Each carries an operational dependency that needs site/team confirmation before Phase 1 work depends on it; flagged inline so the dependency stays visible.
|
||||
|
||||
- **CI tiering: PR-CI uses only in-process simulators; nightly/integration CI runs on a dedicated host with Docker + TwinCAT VM.** PR builds need to be fast and need to run on minimal Windows/Linux build agents; standardizing on the in-process subset (`NModbus` server fixture for Modbus, OPC Foundation `ConsoleReferenceServer` in-process for OPC UA Client, and the FOCAS TCP stub from the test project) covers ~70% of cross-driver behavior with no infrastructure dependency. Anything needing Docker (`oitc/modbus-server`), the TwinCAT XAR VM, the libplctag `ab_server` binary, or the Snap7 Server runs on a single dedicated integration host that runs the full suite nightly and on demand. **Operational dependency**: stand up one Windows host with Docker Desktop + Hyper-V before Phase 3 (Modbus driver) — without it, integration tests for Modbus/AB CIP/AB Legacy/S7/TwinCAT all queue behind the same scarcity.
|
||||
- **Studio 5000 Logix Emulate: not assumed in CI; pre-release validation only.** Don't gate any phase on procuring a license. If an existing org license can be earmarked, designate one Windows machine as the AB CIP golden box and run a quarterly UDT/Program-scope fidelity pass against it. If no license is available, the AB CIP driver ships validated against `ab_server` only, with a documented limitation that UDTs and `Program:` scoping are exercised at customer sites during UAT, not in our CI.
|
||||
- **FANUC CNC Wireshark captures: scheduled as a Phase 5 prerequisite.** During Phase 4 (PLC drivers), the team identifies a target CNC — production machine accessible during a maintenance window, a colleague's CNC Guide seat, or a customer who'll allow a one-day on-site capture. Capture the wire framing for every FOCAS function in the call list (per `driver-stability.md` §FOCAS) plus a 30-min poll trace, before Phase 5 starts. If no target is identified by Phase 4 mid-point, escalate to procurement: a CNC Guide license seat (1-time cost) or a small dev-rig CNC purchase becomes a Phase 5 dependency.
|
||||
Reference in New Issue
Block a user