feat(focas): OtOpcUa-owned otopcua-focas-fixture stack (#651) + FOCAS backlog docs (#633)

Fixture renamed off ignitionoee's otopcua-focas-sim, port 8193 published, project label,
default endpoint 10.100.0.35:8193; infra/dev-environment docs. FOCAS.md: MACRO:3901 parts
count, PMC override + cycle-time recipes, won't-do/blocked backlog.

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
Joseph Doherty
2026-10-08 19:12:13 -04:00
parent 8801631739
commit 7dd64b8ccd
9 changed files with 125 additions and 25 deletions
+1 -1
View File
@@ -510,7 +510,7 @@ These are consumed by the driver **integration-test fixtures** (under `tests/Dri
| `AB_SERVER_ENDPOINT` | `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/AbServerFixture.cs` | `10.100.0.35:44818` | | `AB_SERVER_ENDPOINT` | `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/AbServerFixture.cs` | `10.100.0.35:44818` |
| `S7_SIM_ENDPOINT` | `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests/Snap7ServerFixture.cs` | `10.100.0.35:1102` (non-privileged; not S7-standard 102) | | `S7_SIM_ENDPOINT` | `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests/Snap7ServerFixture.cs` | `10.100.0.35:1102` (non-privileged; not S7-standard 102) |
| `OPCUA_SIM_ENDPOINT` | `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests/OpcPlcFixture.cs` | `opc.tcp://10.100.0.35:50000` | | `OPCUA_SIM_ENDPOINT` | `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests/OpcPlcFixture.cs` | `opc.tcp://10.100.0.35:50000` |
| `OTOPCUA_FOCAS_SIM_ENDPOINT` | `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/FocasSimFixture.cs` | `localhost:8193` | | `OTOPCUA_FOCAS_SIM_ENDPOINT` | `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/FocasSimFixture.cs` | `10.100.0.35:8193` |
> Additional harness/parity/soak env vars (`OTOPCUA_FOCAS_*`, `OTOPCUA_PARITY_*`, `OTOPCUA_SOAK_*`, `OTOPCUA_HARNESS_USE_SQL`) exist only in the test/parity/soak harnesses, not in production source, and are out of scope for this reference. > Additional harness/parity/soak env vars (`OTOPCUA_FOCAS_*`, `OTOPCUA_PARITY_*`, `OTOPCUA_SOAK_*`, `OTOPCUA_HARNESS_USE_SQL`) exist only in the test/parity/soak harnesses, not in production source, and are out of scope for this reference.
+3 -2
View File
@@ -52,8 +52,9 @@ message naming the CNC series + documented limit.
managed `FocasDriver` end-to-end. A single gate: managed `FocasDriver` end-to-end. A single gate:
**Docker compose up** — tests skip when the TCP probe fails, with a **Docker compose up** — tests skip when the TCP probe fails, with a
pointer to the compose command. The endpoint defaults to `localhost:8193` pointer to the compose command. The endpoint defaults to `10.100.0.35:8193`
and is overridable via `OTOPCUA_FOCAS_SIM_ENDPOINT`. (the OtOpcUa-owned `otopcua-focas-fixture` container, stack `/opt/otopcua-focas`; see
`infra/README.md`) and is overridable via `OTOPCUA_FOCAS_SIM_ENDPOINT`.
When the mock is up, `WireFocasClient` dials it over TCP exactly like a When the mock is up, `WireFocasClient` dials it over TCP exactly like a
real CNC, and the mock's native FOCAS Ethernet responder replies with real CNC, and the mock's native FOCAS Ethernet responder replies with
+91 -12
View File
@@ -286,22 +286,69 @@ fallen back to `PositionDecimalPlaces` and published raw counts.
numbers whose meaning is MTB-specific, and which no browse can discover. Those numbers whose meaning is MTB-specific, and which no browse can discover. Those
go under the device folder alongside the fixed subtree. go under the device folder alongside the fixed subtree.
### Parts count (`PARAM:6711`) ### Parts count (`MACRO:3901`, or `PARAM:6711`)
There is deliberately **no** dedicated parts-count node (#258): the count is an ordinary CNC parameter, so it There is deliberately **no** dedicated parts-count node (#258): the count is an ordinary CNC value, so it
is already readable as a hand-authored `PARAM:` tag (Int32), and a fixed `Parts/` subtree would hard-code is already readable as a hand-authored tag, and a fixed `Parts/` subtree would hard-code numbers that are
parameter numbers that are control- and MTB-specific. On the standard FANUC 0i / 16i / 30i parameter map: control- and MTB-specific. Two addresses carry it; **which one works depends on the control**:
| Address | Parameter | Meaning | | Address | What | Use when |
|---|---|---| |---|---|---|
| `PARAM:6711` | No. 6711 | Number of parts machined (resets with the operator's clear) | | `MACRO:3901` | Custom macro system variable #3901 — parts machined (`#3902` = parts required) | **Recommended.** Works over the wire on a Makino 31i-B (Z-34184, G431), where `cnc_rdparam` is refused |
| `PARAM:6712` | No. 6712 | Total number of parts machined (cumulative) | | `PARAM:6711` | CNC parameter No. 6711 — number of parts machined (`6712` total, `6713` required) | Controls that answer `cnc_rdparam` (standard FANUC 0i / 16i / 30i parameter map) |
| `PARAM:6713` | No. 6713 | Number of parts required (the target) |
The count-up trigger (which M code or cycle end increments it) is set by No. 6710 and bit 0 of No. 6700, ⚠️ **`PARAM:` reads do not work on the Makino.** Its 31i-B answers `cnc_rdparam` (0x000E) with `EW_FUNC`, so a
so confirm in the machine's parameter manual that the counter is actually enabled and that 6711–6713 mean `PARAM:6711` tag reads `BadNotSupported` there. `cnc_rdmacro` is accepted, so `MACRO:3901` is the address to
what the MTB says they mean before relying on them. Cycle time per part is not provided; the `Timers/` author on that machine. Both are Int32 tags.
subtree exposes `CycleSeconds` and a client can difference it across a `PARAM:6711` change.
The count-up trigger (which M code or cycle end increments the counter) is set by No. 6710 and bit 0 of No.
6700, so confirm in the machine's manual that the counter is actually enabled before relying on either
address — a disabled counter reads a constant `0` (a *vacant* macro reads `BadNoData`, never `0`; see
[Wire decode corrections](#wire-decode-corrections-2026-10)). A `0` from `MACRO:3901` is therefore a valid
decode of "counter not counting", not a driver fault. Cycle time per part is not provided as a node — see the
[virtual-tag recipe](#recipe-cycle-time-per-part-virtual-tag-272).
### Recipe: feed / rapid / spindle override via PMC (#259)
The CNC's override values are ordinary PMC signals (the MTB ladder copies the operator-panel switches into the
G area), so they are readable today with plain PMC tags — read-only, no new driver surface. On the standard
FANUC signal map:
| Signal | PMC address | Notes |
|---|---|---|
| Feed override `*FV0-*FV7` | `G12` (Byte) | **Inverted**: the byte holds the one's complement. Percent = `255 - value` for a binary-coded switch (`255` = 0%, `155` = 100%); gray-coded/ladder-converted panels differ |
| Rapid override `ROV1`, `ROV2` | `G14.0`, `G14.1` (Bit) | Two bits select 100% / 50% / 25% / F0 |
| Spindle override `SOV0-SOV7` | `G30` (Byte) | Percent, usually a direct 0-150 value |
Author them as normal PMC tags (`G12`, `G14.0`, `G30` — G is accepted on the 30i family and the 16i). To publish a
percentage rather than the raw byte, derive it with a virtual tag (`255 - ctx.GetTag("{{equip}}/FeedOverrideRaw")`).
⚠️ **The addresses above are the FANUC *standard* signal assignment; the meaning is defined by the machine
builder's ladder.** Confirm each against the machine's own PMC signal list before trusting the number, and
validate with the panel switches at known positions (read-only observation — never write PMC). **Modal G/M/T
codes are not available**: `cnc_modal` has no known wire command id here (see
[Not ported](#not-ported-633-backlog)).
### Recipe: cycle time per part (virtual tag, #272)
A per-part cycle time is **not** a driver feature: `FIXED:Timers/CycleSeconds` and the parts count
(`MACRO:3901` / `PARAM:6711`) are both already readable, and the result is a pure function of them. Author the
count as an equipment tag named `PartsCount` and `FIXED:Timers/CycleSeconds` as `CycleSeconds`, then add a
virtual tag (Double, change-triggered by those two references):
```csharp
// Average cycle seconds per part since the counters were last cleared.
var parts = (double)ctx.GetTag("{{equip}}/PartsCount").Value;
var cycle = (double)ctx.GetTag("{{equip}}/CycleSeconds").Value;
return parts > 0 ? cycle / parts : null; // no parts yet: no average
```
This is a **running average**, valid only while both counters accumulate from the same reset (an operator clearing
one but not the other skews it until they are cleared together). The exact *last-cycle delta* (difference
`CycleSeconds` across a parts-count step) needs the script to remember the previous sample, and virtual-tag scripts
have no documented persistent state (see [`docs/ScriptEditor.md`](../ScriptEditor.md)) — compute that in the
consuming system (e.g. ScadaBridge) from the two raw tags instead. Both inputs hold while either is Bad, per the
quality contract.
## PMC range coalescing ## PMC range coalescing
@@ -448,6 +495,38 @@ re-interpretation of `cnc_rdparam` (no successful `cnc_rdparam` capture exists o
the reference 31i-B refuses it with `EW_FUNC`, which already reads `BadNotSupported` here); and the reference 31i-B refuses it with `EW_FUNC`, which already reads `BadNotSupported` here); and
the all-axes multi-block `cnc_rddynamic2` batching (a latency optimisation, not a correctness fix). the all-axes multi-block `cnc_rddynamic2` batching (a latency optimisation, not a correctness fix).
## Not ported (#633 backlog)
The #607 porting backlog (#257-#272) was triaged against this read-only driver. Disposition:
**Done / documented:** #257 `Status/` fixed subtree, #258 parts count ([`MACRO:3901`](#parts-count-macro3901-or-param6711)),
#259 overrides ([PMC recipe](#recipe-feed--rapid--spindle-override-via-pmc-259)), #265 16i PMC F/G, #266 PMC range
coalescing, #272 ([virtual-tag recipe](#recipe-cycle-time-per-part-virtual-tag-272)).
**Won't do — the read-only policy (#268-#271).** The FOCAS driver is read-only by design
(`WireFocasClient.WriteAsync` returns `BadNotWritable`) and that is a safety decision for real CNCs, not a
gap: no write infrastructure / per-tag opt-in (#268), no `cnc_wrmacro` / `cnc_wrparam` (#269), no `pmc_wrpmcrng`
or PMC bit read-modify-write (#270), no password / unlock parameter (#271). Nothing in the driver or CLI should be
read as a path toward them.
**Blocked — need an authoritative wire capture first.** None of the following has a wire command id known to this
repo or to the sister ignitionoee client, and neither mock serves them over the binary wire:
| Item | Capability | Blocked by |
|---|---|---|
| #259 (modal part) | Modal G / M / T codes (`cnc_modal`) | no wire id |
| #260 | Tool number / tool life, work-coordinate and tool offsets (`cnc_rdtofs` / `rdzofs`) | no wire ids |
| #261 | Operator messages (`cnc_rdopmsg`) and the currently-executing block text (`cnc_rdexecprog`) | no wire ids |
| #263 | `DIAG:` address area (`cnc_diagnoss`) — the highest-value one: a single generic area would unlock many fields | no wire id |
| #264 | Multi-path / multi-channel (`cnc_getpath`) — the wire layer has `PathId` plumbing but no per-device/tag option | no wire id, and no multi-path control to validate against (the Makino is single-path) |
| #267 | Alarm history (`cnc_rdalmhistry`) | no wire id; the live alarm surface ([#647](#alarm-projection)) comes first |
⚠️ **Do not discover these ids by probing the production Makino.** An unknown or guessed command id could map to a
function that is not a read, and even known ids misbehave on the 31i-B (`cnc_rdparam` 0x000E answers `EW_FUNC`). The
0x00D3 `cnc_getfigure` lesson (see Wire decode corrections) already showed that an id agreed between two mocks can be
fiction. The prerequisite is an authoritative capture: real FWLIB on a Windows host against a **non-production**
control, or a passive capture of an existing FWLIB application's traffic.
## Further reading ## Further reading
- [`docs/v2/driver-specs.md §7`](../v2/driver-specs.md) — full OPC UA node - [`docs/v2/driver-specs.md §7`](../v2/driver-specs.md) — full OPC UA node
+1 -1
View File
@@ -67,7 +67,7 @@ Running record of v2 dev services on the Windows dev VM. Updated on every instal
| Dev Galaxy (AVEVA System Platform) | Local install on this dev box — full ArchestrA + Historian + OI-Server stack | v1 baseline | Local COM via MXAccess (`C:\Program Files (x86)\ArchestrA\Framework\bin\ArchestrA.MXAccess.dll`); Historian via `aaH*` services; SuiteLink via `slssvc` | Windows Auth | Galaxy repository DB `ZB` on local SQL Server (separate instance from `otopcua-mssql` — legacy v1 Galaxy DB, not related to v2 config DB) | ✅ **Fully available — Phase 2 lift unblocked.** 27 ArchestrA / AVEVA / Wonderware services running incl. `aaBootstrap`, `aaGR` (Galaxy Repository), `aaLogger`, `aaUserValidator`, `aaPim`, `ArchestrADataStore`, `AsbServiceManager`, `AutoBuild_Service`; full Historian set (`aahClientAccessPoint`, `aahGateway`, `aahInSight`, `aahSearchIndexer`, `aahSupervisor`, `InSQLStorage`, `InSQLConfiguration`, `InSQLEventSystem`, `InSQLIndexing`, `InSQLIOServer`, `InSQLManualStorage`, `InSQLSystemDriver`, `HistorianSearch-x64`); `slssvc` (Wonderware SuiteLink); `OI-Gateway` install present at `C:\Program Files (x86)\Wonderware\OI-Server\OI-Gateway\` (decision #142 AppServer-via-OI-Gateway smoke test now also unblocked) | | Dev Galaxy (AVEVA System Platform) | Local install on this dev box — full ArchestrA + Historian + OI-Server stack | v1 baseline | Local COM via MXAccess (`C:\Program Files (x86)\ArchestrA\Framework\bin\ArchestrA.MXAccess.dll`); Historian via `aaH*` services; SuiteLink via `slssvc` | Windows Auth | Galaxy repository DB `ZB` on local SQL Server (separate instance from `otopcua-mssql` — legacy v1 Galaxy DB, not related to v2 config DB) | ✅ **Fully available — Phase 2 lift unblocked.** 27 ArchestrA / AVEVA / Wonderware services running incl. `aaBootstrap`, `aaGR` (Galaxy Repository), `aaLogger`, `aaUserValidator`, `aaPim`, `ArchestrADataStore`, `AsbServiceManager`, `AutoBuild_Service`; full Historian set (`aahClientAccessPoint`, `aahGateway`, `aahInSight`, `aahSearchIndexer`, `aahSupervisor`, `InSQLStorage`, `InSQLConfiguration`, `InSQLEventSystem`, `InSQLIndexing`, `InSQLIOServer`, `InSQLManualStorage`, `InSQLSystemDriver`, `HistorianSearch-x64`); `slssvc` (Wonderware SuiteLink); `OI-Gateway` install present at `C:\Program Files (x86)\Wonderware\OI-Server\OI-Gateway\` (decision #142 AppServer-via-OI-Gateway smoke test now also unblocked) |
| GLAuth (LDAP) | Docker container `zb-shared-glauth` on the Linux Docker host — managed via `scadaproj/infra/glauth/` | v2.4.0 | `10.100.0.35:3893` (LDAP plaintext; LDAPS disabled) | Bind account `cn=serviceaccount,dc=zb,dc=local` / `serviceaccount123`; all test users password `password`; baseDN `dc=zb,dc=local` | `scadaproj/infra/glauth/` (source of truth + deploy/verify runbook) | ✅ Running on Docker host. Shared across OtOpcUa, MxAccessGateway, ScadaBridge. OtOpcUa groups: `OtOpcUa-Admins`→Administrator, `OtOpcUa-Designers`→Designer, `OtOpcUa-Viewers`→Viewer. The per-VM NSSM service at `C:\publish\glauth\` and old base DNs `dc=lmxopcua,dc=local` / `dc=otopcua,dc=local` are obsolete. | | GLAuth (LDAP) | Docker container `zb-shared-glauth` on the Linux Docker host — managed via `scadaproj/infra/glauth/` | v2.4.0 | `10.100.0.35:3893` (LDAP plaintext; LDAPS disabled) | Bind account `cn=serviceaccount,dc=zb,dc=local` / `serviceaccount123`; all test users password `password`; baseDN `dc=zb,dc=local` | `scadaproj/infra/glauth/` (source of truth + deploy/verify runbook) | ✅ Running on Docker host. Shared across OtOpcUa, MxAccessGateway, ScadaBridge. OtOpcUa groups: `OtOpcUa-Admins`→Administrator, `OtOpcUa-Designers`→Designer, `OtOpcUa-Viewers`→Viewer. The per-VM NSSM service at `C:\publish\glauth\` and old base DNs `dc=lmxopcua,dc=local` / `dc=otopcua,dc=local` are obsolete. |
| OPC Foundation reference server | Not yet built | — | `10.100.0.35:62541` (target) | `user1` / `password1` (reference-server defaults) | — | Pending (needed for Phase 5 OPC UA Client driver testing) | | OPC Foundation reference server | Not yet built | — | `10.100.0.35:62541` (target) | `user1` / `password1` (reference-server defaults) | — | Pending (needed for Phase 5 OPC UA Client driver testing) |
| FOCAS TCP stub | Not yet built | — | `10.100.0.35:8193` (target) | n/a | — | Pending (built in Phase 5; runs on Docker host) | | FOCAS fixture (`otopcua-focas-fixture`, vendored focas-mock) | Docker compose at `/opt/otopcua-focas/` on Docker host (source: `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/Docker/`) | vendored snapshot | `10.100.0.35:8193` (published; `OTOPCUA_FOCAS_SIM_ENDPOINT`) | n/a | n/a | Owned by OtOpcUa. NB `otopcua-focas-sim` on the same host is **ignitionoee's** mock (#651) — do not touch it |
| Modbus simulator (`otopcua-pymodbus:3.13.0`) | Docker compose at `/opt/otopcua-modbus/` on Docker host | pinned 3.13.0 | `10.100.0.35:5020` | n/a | n/a | Stack staged; bring up with `lmxopcua-fix up modbus <profile>` from this VM | | Modbus simulator (`otopcua-pymodbus:3.13.0`) | Docker compose at `/opt/otopcua-modbus/` on Docker host | pinned 3.13.0 | `10.100.0.35:5020` | n/a | n/a | Stack staged; bring up with `lmxopcua-fix up modbus <profile>` from this VM |
| AB CIP fixture (`otopcua-ab-server:libplctag-release`) | Docker compose at `/opt/otopcua-abcip/` on Docker host | source-pinned `release` tag | `10.100.0.35:44818` | n/a | n/a | Stack staged; bring up with `lmxopcua-fix up abcip <profile>` from this VM | | AB CIP fixture (`otopcua-ab-server:libplctag-release`) | Docker compose at `/opt/otopcua-abcip/` on Docker host | source-pinned `release` tag | `10.100.0.35:44818` | n/a | n/a | Stack staged; bring up with `lmxopcua-fix up abcip <profile>` from this VM |
| S7 fixture (`otopcua-python-snap7:1.0`) | Docker compose at `/opt/otopcua-s7/` on Docker host | python-snap7 ≥2.0 | `10.100.0.35:1102` | n/a | n/a | Stack staged; bring up with `lmxopcua-fix up s7 s7_1500` from this VM | | S7 fixture (`otopcua-python-snap7:1.0`) | Docker compose at `/opt/otopcua-s7/` on Docker host | python-snap7 ≥2.0 | `10.100.0.35:1102` | n/a | n/a | Stack staged; bring up with `lmxopcua-fix up s7 s7_1500` from this VM |
+12 -4
View File
@@ -37,8 +37,8 @@ ssh dohertj2@10.100.0.35 'docker ps --filter label=project=lmxopcua --format "{{
| Stack dir | Purpose | | Stack dir | Purpose |
|---|---| |---|---|
| `/opt/otopcua-mssql` | central SQL (always-on) | | `/opt/otopcua-mssql` | central SQL (always-on) |
| `/opt/otopcua-modbus` · `/opt/otopcua-abcip` · `/opt/otopcua-s7` · `/opt/otopcua-opcuaclient` · `/opt/otopcua-mqtt` · `/opt/otopcua-mtconnect` | driver fixtures | | `/opt/otopcua-modbus` · `/opt/otopcua-abcip` · `/opt/otopcua-s7` · `/opt/otopcua-opcuaclient` · `/opt/otopcua-mqtt` · `/opt/otopcua-mtconnect` · `/opt/otopcua-focas` | driver fixtures |
| `~/otopcua-ablegacy` · `~/otopcua-focas` | driver fixtures (user-owned) | | `~/otopcua-ablegacy` | driver fixture (user-owned) |
| `~/otopcua-harness` | Host.IntegrationTests real-mode deps (SQL + GLAuth) — see §4 | | `~/otopcua-harness` | Host.IntegrationTests real-mode deps (SQL + GLAuth) — see §4 |
- The repo's `tests/.../Docker/` trees are the **source of truth**; the host dirs are mirrored - The repo's `tests/.../Docker/` trees are the **source of truth**; the host dirs are mirrored
@@ -68,14 +68,22 @@ when it isn't running. Bring one up, `dotnet test`, tear down. Repo compose live
| AB Legacy (PCCC) | `otopcua-ab-server` (PCCC mode) | `10.100.0.35:44818` | `AB_LEGACY_COMPOSE_PROFILE=<…> docker compose up -d` (stack `~/otopcua-ablegacy`) | | AB Legacy (PCCC) | `otopcua-ab-server` (PCCC mode) | `10.100.0.35:44818` | `AB_LEGACY_COMPOSE_PROFILE=<…> docker compose up -d` (stack `~/otopcua-ablegacy`) |
| S7 | `otopcua-python-snap7` | `10.100.0.35:1102` | `docker compose --profile s7_1500 up -d` | | S7 | `otopcua-python-snap7` | `10.100.0.35:1102` | `docker compose --profile s7_1500 up -d` |
| OpcUaClient | `mcr.microsoft.com/iotedge/opc-plc:2.14.10` | `opc.tcp://10.100.0.35:50000` | `docker compose up -d` | | OpcUaClient | `mcr.microsoft.com/iotedge/opc-plc:2.14.10` | `opc.tcp://10.100.0.35:50000` | `docker compose up -d` |
| FOCAS | `otopcua-focas-sim` (vendored focas-mock) | `10.100.0.35:8193` | `docker compose up -d` (stack `~/otopcua-focas`) | | FOCAS | `otopcua-focas-fixture` (vendored focas-mock, built on the host) | `10.100.0.35:8193` (published) | `docker compose up -d --build` (stack `/opt/otopcua-focas`; rsync the repo's FOCAS `Docker/` dir first) |
| MQTT | `eclipse-mosquitto:2.0.22` (+ a publisher sidecar) | `10.100.0.35:8883` TLS+auth · `:1883` plaintext+auth | `MQTT_FIXTURE_USERNAME=otopcua MQTT_FIXTURE_PASSWORD=<pw> docker compose up -d` (stack `/opt/otopcua-mqtt`) | | MQTT | `eclipse-mosquitto:2.0.22` (+ a publisher sidecar) | `10.100.0.35:8883` TLS+auth · `:1883` plaintext+auth | `MQTT_FIXTURE_USERNAME=otopcua MQTT_FIXTURE_PASSWORD=<pw> docker compose up -d` (stack `/opt/otopcua-mqtt`) |
| MQTT — Sparkplug B | `otopcua-sparkplug-sim` (project-owned C# edge-node simulator) | same broker; group **`OtOpcUaSim`**, edge nodes **`EdgeA`** / **`EdgeB`**, `EdgeA` device **`Filler1`**, ~2 s cadence | `docker compose --profile sparkplug up -d` (same stack). Answers rebirth NCMDs; restart it to force a fresh birth | | MQTT — Sparkplug B | `otopcua-sparkplug-sim` (project-owned C# edge-node simulator) | same broker; group **`OtOpcUaSim`**, edge nodes **`EdgeA`** / **`EdgeB`**, `EdgeA` device **`Filler1`**, ~2 s cadence | `docker compose --profile sparkplug up -d` (same stack). Answers rebirth NCMDs; restart it to force a fresh birth |
| MTConnect | `mtconnect/agent:2.7.0.12` + `python:3.13-alpine` adapter | `http://10.100.0.35:5000` | `docker compose up -d --wait` | | MTConnect | `mtconnect/agent:2.7.0.12` + `python:3.13-alpine` adapter | `http://10.100.0.35:5000` | `docker compose up -d --wait` |
**Endpoint overrides** point a suite at a real PLC instead of the sim: **Endpoint overrides** point a suite at a real PLC instead of the sim:
`MODBUS_SIM_ENDPOINT` · `AB_SERVER_ENDPOINT` · `S7_SIM_ENDPOINT` · `OPCUA_SIM_ENDPOINT` · `MODBUS_SIM_ENDPOINT` · `AB_SERVER_ENDPOINT` · `S7_SIM_ENDPOINT` · `OPCUA_SIM_ENDPOINT` ·
`MQTT_FIXTURE_ENDPOINT` / `MQTT_FIXTURE_PLAIN_ENDPOINT` · `MTCONNECT_AGENT_ENDPOINT`. `MQTT_FIXTURE_ENDPOINT` / `MQTT_FIXTURE_PLAIN_ENDPOINT` · `MTCONNECT_AGENT_ENDPOINT` · `OTOPCUA_FOCAS_SIM_ENDPOINT`.
> **`otopcua-focas-sim` on the host is NOT ours (#651).** It is ignitionoee's mock (created by that
> project's `provision-oee-focas.sh` for `ignition-site`; no published port, different defaults:
> big-endian timers, position exponent 3, `fwlib30i64` profile). OtOpcUa's fixture is
> `otopcua-focas-fixture`; the two once shared a name and overwrote each other. Never `docker rm -f`,
> rebuild or `mock_patch` `otopcua-focas-sim` or any `ignition-*`/`oee-*` container from here. The stale
> `~/otopcua-focas` dir on the host predates the rename — do not `compose up` it; retire it after
> `/opt/otopcua-focas` is deployed.
> **MQTT needs credentials + material, unlike the other fixtures.** The broker has **no anonymous > **MQTT needs credentials + material, unlike the other fixtures.** The broker has **no anonymous
> fallback on either listener** (deliberate — an anonymous broker would let a bad auth config pass), > fallback on either listener** (deliberate — an anonymous broker would let a bad auth config pass),
+2
View File
@@ -106,6 +106,8 @@ else {
Write-Step "dotnet test (wire-backend integration)" Write-Step "dotnet test (wire-backend integration)"
$env:OTOPCUA_FOCAS_SIM_PROFILE = $Profile $env:OTOPCUA_FOCAS_SIM_PROFILE = $Profile
# The fixture default is the shared Docker host (10.100.0.35:8193); this script runs the mock locally.
if (-not $env:OTOPCUA_FOCAS_SIM_ENDPOINT) { $env:OTOPCUA_FOCAS_SIM_ENDPOINT = "localhost:8193" }
dotnet test (Join-Path $integTests "ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests.csproj") ` dotnet test (Join-Path $integTests "ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests.csproj") `
--configuration $Configuration --no-build --nologo --verbosity minimal --configuration $Configuration --no-build --nologo --verbosity minimal
@@ -7,6 +7,14 @@
# docker compose -f Docker/docker-compose.yml up -d --wait # docker compose -f Docker/docker-compose.yml up -d --wait
# docker compose -f Docker/docker-compose.yml down # docker compose -f Docker/docker-compose.yml down
# #
# OWNERSHIP (#651): the container/image are named `otopcua-focas-fixture` on
# purpose. The name `otopcua-focas-sim` on 10.100.0.35 belongs to ignitionoee
# (its provision-oee-focas.sh `docker rm -f`s and recreates it for ignition-site);
# an earlier version of this file used that name and the two projects overwrote each
# other's fixture. Never start, stop or rebuild `otopcua-focas-sim` or any
# ignition-* / oee-* container from here.
# Deployed at /opt/otopcua-focas on the Docker host (rsync this Docker/ dir there).
#
# One service, one container — the mock's native FOCAS Ethernet responder # One service, one container — the mock's native FOCAS Ethernet responder
# auto-detects the binary PDU prefix (`a0 a0 a0 a0`) on the same TCP port # auto-detects the binary PDU prefix (`a0 a0 a0 a0`) on the same TCP port
# that serves JSON admin commands. Tests that need per-series behaviour # that serves JSON admin commands. Tests that need per-series behaviour
@@ -17,14 +25,16 @@
services: services:
focas-sim: focas-sim:
image: otopcua-focas-sim:latest image: otopcua-focas-fixture:latest
build: build:
context: ./focas-mock context: ./focas-mock
dockerfile: Dockerfile dockerfile: Dockerfile
container_name: otopcua-focas-sim container_name: otopcua-focas-fixture
ports: ports:
- "8193:8193" - "8193:8193"
restart: "no" restart: "no"
labels:
project: lmxopcua
command: ["--profile", "FWLIB64"] command: ["--profile", "FWLIB64"]
healthcheck: healthcheck:
test: ["CMD-SHELL", "python -c \"import socket; s=socket.create_connection(('127.0.0.1',8193),timeout=2); s.close()\" || exit 1"] test: ["CMD-SHELL", "python -c \"import socket; s=socket.create_connection(('127.0.0.1',8193),timeout=2); s.close()\" || exit 1"]
@@ -13,14 +13,14 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests;
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// Single skip gate: <see cref="SkipReason"/> is non-null when the /// Single skip gate: <see cref="SkipReason"/> is non-null when the
/// <c>localhost:8193</c> TCP probe fails. Tests call /// <c>10.100.0.35:8193</c> TCP probe (the <c>otopcua-focas-fixture</c> container; override with <c>OTOPCUA_FOCAS_SIM_ENDPOINT</c>) fails. Tests call
/// <c>Assert.Skip</c>. /// <c>Assert.Skip</c>.
/// </remarks> /// </remarks>
public sealed class FocasSimFixture : IAsyncDisposable public sealed class FocasSimFixture : IAsyncDisposable
{ {
private const string EndpointEnvVar = "OTOPCUA_FOCAS_SIM_ENDPOINT"; private const string EndpointEnvVar = "OTOPCUA_FOCAS_SIM_ENDPOINT";
private const string ProfileEnvVar = "OTOPCUA_FOCAS_SIM_PROFILE"; private const string ProfileEnvVar = "OTOPCUA_FOCAS_SIM_PROFILE";
private const string DefaultHost = "localhost"; private const string DefaultHost = "10.100.0.35";
private const int DefaultPort = 8193; private const int DefaultPort = 8193;
/// <summary>Gets the hostname or IP address of the focas-mock simulator.</summary> /// <summary>Gets the hostname or IP address of the focas-mock simulator.</summary>
@@ -7,7 +7,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests.Series;
/// <summary> /// <summary>
/// Live-wire coverage (focas-mock) for #257 (<c>cnc_statinfo</c> status flags) and #266 (PMC /// Live-wire coverage (focas-mock) for #257 (<c>cnc_statinfo</c> status flags) and #266 (PMC
/// range coalescing). Skips cleanly when <c>OTOPCUA_FOCAS_SIM_ENDPOINT</c> (default /// range coalescing). Skips cleanly when <c>OTOPCUA_FOCAS_SIM_ENDPOINT</c> (default
/// <c>localhost:8193</c>) is unreachable. Read-only: no write path is exercised. /// <c>10.100.0.35:8193</c>, the <c>otopcua-focas-fixture</c> container) is unreachable. Read-only: no write path is exercised.
/// </summary> /// </summary>
[Collection(FocasSimCollection.Name)] [Collection(FocasSimCollection.Name)]
public sealed class WireBackendStatusAndPmcTests public sealed class WireBackendStatusAndPmcTests