# 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/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/` is Docker-only — `ab_server` is a source-only tool under libplctag's `src/tools/ab_server/`, and the fixture's multi-stage `Docker/Dockerfile` is the only supported reproducible build path.

- **`AbServerFixture(AbServerProfile)`** — thin TCP probe against `127.0.0.1:44818` (or `AB_SERVER_ENDPOINT` override). Does not spawn the simulator; the operator brings up the compose service for whichever family the test class targets (`controllogix` / `compactlogix` / `micro800` / `guardlogix`).
- **`KnownProfiles.{ControlLogix, CompactLogix, Micro800, GuardLogix}`** — thin `(Family, ComposeProfile, Notes)` records. The compose file (`Docker/docker-compose.yml`) is the canonical source of truth for which tags each family seeds + which `--plc` mode the simulator boots in. `Micro800` uses the dedicated `--plc=Micro800` mode; `GuardLogix` uses `ControlLogix` emulation because ab_server has no safety subsystem (the `_S`-suffixed seed tag triggers driver-side ViewOnly classification only).

**Pinned version**: the `Docker/Dockerfile` clones libplctag at a pinned tag (currently the `release` branch) via its `LIBPLCTAG_TAG` build-arg and compiles `ab_server` from source. Bump deliberately alongside a driver-side change that needs the newer simulator.

**CI step:**

```yaml
# GitHub Actions step placed before `dotnet test`:
- name: Start ab_server Docker container
  shell: pwsh
  run: |
    docker compose -f tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/Docker/docker-compose.yml `
      --profile controllogix up -d --build
    # Wait for :44818 to accept connections (compose healthcheck-equivalent)
    for ($i = 0; $i -lt 30; $i++) {
      if ((Test-NetConnection -ComputerName localhost -Port 44818 -WarningAction SilentlyContinue).TcpTestSucceeded) { break }
      Start-Sleep -Seconds 1
    }
```

Tests skip via `AbServerFactAttribute` / `AbServerTheoryAttribute` when the probe fails, so fresh-clone runs without Docker 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.
