Files
lmxopcua/docs/v2/test-data-sources.md
Joseph Doherty 8ce5791f49 Pin libplctag ab_server to v2.6.16 — real release tag + SHA256 hashes for all three Windows arches. Closes the "pick a current version + pin" deferral left by the #180 PR docs stub. Verified the release lands ab_server.exe inside libplctag_2.6.16_windows_<arch>_tools.zip alongside plctag.dll + list_tags_* helpers by downloading each tools zip + unzip -l'ing to confirm ab_server.exe is present at 331264 bytes. New ci/ab-server.lock.json is the single source of truth — one file the CI YAML reads via ConvertFrom-Json instead of duplicating the hash across the workflow + the docs. Structure: repo (libplctag/libplctag) + tag (v2.6.16) + published date (2026-03-29) + assets keyed by platform (windows-x64 / windows-x86 / windows-arm64) each carrying filename + sha256. docs/v2/test-data-sources.md §2.CI updated — replaces the prior placeholder (ver = '<pinned libplctag release tag>', expected = '<pinned sha256>') with the real v2.6.16 + 9b78a3de... hashes pinned table, and replaces the hardcoded URL with a lockfile-driven pwsh step that picks windows-x64 by default but swaps to x86/arm64 by changing one line for non-x64 CI runners. Hash-mismatch path throws with both the expected + actual values so on the first drift the CI log tells the maintainer exactly what to update in the lockfile. Two verification notes from the release fetch: (1) libplctag v2.6.16 tools zips ship ab_server.exe + plctag.dll together — tests don't need a separate libplctag NuGet download for the integration path, the extracted tools dir covers both the simulator + the driver's native dependency; (2) the three Windows arches all carry ab_server.exe, so ARM64 Windows GitHub runners (when they arrive) can run the integration suite without changes beyond swapping the asset key. No code changes in this PR — purely docs + the new lockfile. Admin tests + Core tests unchanged + passing per the prior commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 00:04:35 -04:00

34 KiB
Raw Blame History

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, GitHub) MIT Docker YAML preload of all 4 register areas; docker run -p 502:502
NModbus ModbusTcpSlave (GitHub) MIT In-proc .NET 10 ~20 LOC fixture; programmatic register control
diagslave (modbusdriver.com) Free (proprietary) Win/Linux/QNX Single binary; free mode times out hourly
EasyModbusTCP LGPL .NET / Java / Python MSI installer
ModbusPal (SourceForge) 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 10101019
Int16 scalar / Int16[10] / Int16[500] HR 0 / HR 1019 / HR 500999
Int32 scalar / Int32[10] HR 20002001 / HR 20102029
UInt16 scalar HR 50
UInt32 scalar HR 6061
Float32 scalar / Float32[10] / Float32[500] HR 30003001 / HR 30103029 / HR 40004999
Float64 scalar HR 50005003
Ramp (Int32) HR 100101 — 0→1000 @ 1 Hz
Ramp (Float32) HR 110111 — sine wave
Write-read-back (Bool / Int32 / Float32) Coil 1100 / HR 21002101 / HR 31003101
Array element write (Int32[10]) HR 22002219
Bool toggle on cadence Coil 0 — toggles @ 2 Hz
Endianness round-trip (Float32) HR 60006001, 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, 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<AbServerFixture> 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:

# 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 48 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) LGPLv3 Win/Linux/macOS, 32/64-bit CP emulator; no PLC logic execution
PLCSIM Advanced (Siemens) 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, licensing) 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) Free, bundled NuGet, in-proc Router only — needs a real XAR on the other end
TwinCAT XAR in Docker (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 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. 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) 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 015)
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.