Two test/fixture bugs; NO OtOpcUa driver change. 1. Topology: FocasSimFixture reads an env-overridable endpoint (OTOPCUA_FOCAS_SIM_ENDPOINT, default localhost:8193) and exposes Host/Port, but WireBackendTests + WireBackendCoverageTests hardcoded focas://127.0.0.1:8193. Against a remote fixture the skip-gate passed (remote reachable) but the driver dialed localhost -> KeyNotFound. Added a DeviceUri property to the fixture; each test now derives DeviceHost from it. Recovered 8 of 9. 2. Mock timer endianness (surfaced once #1 let the tests reach the mock): the last failure read 60397977600 for a 3600 s power-on timer (= 0x3C000000 x 60). The focas-mock's _wire_timer encoded the minute/msec fields big-endian (_u32), but cnc_rdtimer's timer fields are LITTLE-endian on real hardware — a documented, live-31i-B-validated quirk that FocasWireClient.ParseTimer decodes LE (docs/plans/2026-06-25-focas-pdu-v3-30i-b-support.md). The driver was correct; the mock was wrong. _wire_timer now emits little-endian (_u32_le). Suite 9F/1P -> 10/10 against the remote fixture at ~/otopcua-focas. Integration-sweep follow-up #3.
FOCAS Docker simulator — focas-mock + shim DLL
Hardware-free FOCAS fixture for OtOpcUa's integration test matrix. Runs
the vendored focas-mock Python server under
Docker and pairs it with the shim DLL that
masquerades as Fwlib64.dll inside the .NET test process.
Architecture
┌────────────────────────────┐ cnc_allclibhndl3 / cnc_rdparam / ...
│ xunit test process │ (P/Invoke, __stdcall)
│ ├── Driver.FOCAS │
│ │ └── FwlibNative.cs ─┼─┐
│ └── FocasSimFixture │ │ resolves to...
└────────────────────────────┘ │
▼
┌────────────────────────────┐
│ Fwlib64.dll (shim) │ JSON over TCP
│ tests/.../Shim/focas_ │──────────────────────┐
│ shim.c compiled here │ │
└────────────────────────────┘ │
▼
┌─────────────────────────────┐
│ focas-mock (Docker) │
│ python:3.11-slim │
│ profile-aware responses │
│ mock_load_profile / │
│ mock_patch admin methods │
└─────────────────────────────┘
The shim bridges the binary ABI (C __stdcall exports with FOCAS struct
shapes) to the mock's newline-delimited JSON protocol. OtOpcUa's
FocasSimFixture seeds per-test state by sending mock_load_profile +
mock_patch admin calls on the same socket. Tests assert the managed
driver sees the seeded values through its normal P/Invoke path.
Running
Pick one compose profile (they all publish 8193 — only one at a time):
docker compose -f Docker/docker-compose.yml --profile thirtyone up -d
dotnet test tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests
docker compose -f Docker/docker-compose.yml --profile thirtyone down
Available profiles + their focas-mock target:
| compose --profile | focas-mock profile | Covers |
|---|---|---|
thirtyone / thirty / thirtytwo |
fwlib30i64 |
30i / 31i / 32i series |
sixteen |
FWLIB64 |
16i / 18i / 21i legacy family |
zerod / zerof / zeromf / zerotf |
fwlib0iD64 |
0i-D / 0i-F / 0i-MF / 0i-TF |
powermotion |
fwlib0DN64 |
Power Motion i |
ethernet |
fwlibe64 |
Ethernet-variant DLL |
ncguide |
fwlibNCG64 |
NC Guide PC simulator |
What this covers — and what it doesn't
Covered:
- All 10 FOCAS functions
FwlibNative.csP/Invokes - Read-after-write round-trip for parameters, macros, PMC ranges
- PMC bit read-modify-write path (via the
pmc_wrpmcrngseam) IAlarmSourceraise + clear transitions (viamock_schedule_alarms)- Per-series profile selection — tests can pin one and assert series-gated behaviour
Not covered (still hardware-gated):
- Real FOCAS2 TCP wire protocol (this is a JSON mock; the shim hides the real protocol entirely)
- CNC-specific firmware quirks (position scaling across power cycles, edit-mode session locks, MTB custom screens)
- Concurrent-read behaviour on the real
Fwlib64.dll— the shim is single-threaded per connection
See docs/drivers/FOCAS-Test-Fixture.md
for the full coverage map.
Skip behaviour
FocasSimFixture probes the mock at collection init time:
- Mock unreachable → tests skip with the compose-up command to run
- Mock reachable but shim DLL not loaded → tests skip with a pointer
at
Shim/build.ps1 - Both available → tests run
This lets the same test assembly be green on a fresh CI box without docker, green on a dev box with just the docker compose up, and exercise the full wire path when the shim is built.