# 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. ### CI fixture (task #180) The integration harness at `tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/` exposes two test-time contracts: - **`AbServerFixture(AbServerProfile)`** — starts the simulator with the CLI args composed from the profile's `--plc` family + seed-tag set. One fixture instance per family, one simulator process per test case (smoke tier). For larger suites that can share a simulator across several reads/writes, use a `IClassFixture` wrapper per family. - **`KnownProfiles.{ControlLogix, CompactLogix, Micro800, GuardLogix}`** — the four per-family profiles. Drives the simulator's `--plc` mode + the preseed `--tag name:type[:size]` set. Micro800 + GuardLogix fall back to `controllogix` under the hood because ab_server has no dedicated mode for them — the driver-side family profile still enforces the narrower connection shape / safety classification separately. **Pinned version** (recorded in `ci/ab-server.lock.json` so drift is one-file visible): - `libplctag` **v2.6.16** (published 2026-03-29) — `ab_server.exe` ships inside the `_tools.zip` asset alongside `plctag.dll` + two `list_tags_*` helpers. - Windows x64: `libplctag_2.6.16_windows_x64_tools.zip` — SHA256 `9b78a3dee73d9cd28ca348c090f453dbe3ad9d07ad6bf42865a9dc3a79bc2232` - Windows x86: `libplctag_2.6.16_windows_x86_tools.zip` — SHA256 `fdfefd58b266c5da9a1ded1a430985e609289c9e67be2544da7513b668761edf` - Windows ARM64: `libplctag_2.6.16_windows_arm64_tools.zip` — SHA256 `d747728e4c4958bb63b4ac23e1c820c4452e4778dfd7d58f8a0aecd5402d4944` **CI step:** ```yaml # GitHub Actions step placed before `dotnet test`: - name: Fetch ab_server (libplctag v2.6.16) shell: pwsh run: | $pin = Get-Content ci/ab-server.lock.json | ConvertFrom-Json $asset = $pin.assets.'windows-x64' # swap to windows-x86 / windows-arm64 on non-x64 runners $url = "https://github.com/libplctag/libplctag/releases/download/$($pin.tag)/$($asset.file)" $zip = Join-Path $env:RUNNER_TEMP 'libplctag-tools.zip' Invoke-WebRequest $url -OutFile $zip $actual = (Get-FileHash -Algorithm SHA256 $zip).Hash.ToLower() if ($actual -ne $asset.sha256) { throw "libplctag tools SHA256 mismatch: expected $($asset.sha256), got $actual" } $dest = Join-Path $env:RUNNER_TEMP 'libplctag-tools' Expand-Archive $zip -DestinationPath $dest Add-Content $env:GITHUB_PATH $dest ``` The fixture's `LocateBinary()` picks the binary up off PATH so the C# harness doesn't own the download — CI YAML is the right place for version pinning + hash verification. Developer workstations install the binary once from source (`cmake + make ab_server` under a libplctag clone) and the same fixture works identically. Tests without ab_server on PATH are marked `Skip` via `AbServerFactAttribute` / `AbServerTheoryAttribute`, so fresh-clone runs without the simulator still pass all unit suites in this project. --- ## 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.