diff --git a/docs/plans/2026-07-24-mtconnect-driver.md b/docs/plans/2026-07-24-mtconnect-driver.md index 09039f5c..faefaa62 100644 --- a/docs/plans/2026-07-24-mtconnect-driver.md +++ b/docs/plans/2026-07-24-mtconnect-driver.md @@ -894,7 +894,7 @@ git commit -m "test(mtconnect): live /run gate on docker-dev — browse/read/sub --- -### LIVE-GATE RESULT (2026-07-24) — 4 of 5 legs PASSED; the deploy leg is BLOCKED on a rig artifact, not a defect +### LIVE-GATE RESULT (2026-07-24, completed 2026-07-27) — ALL 5 LEGS PASSED **Rig.** The shared `otopcua-dev` rig was NOT used: three sibling driver worktrees (`modbus-rtu`, `mqtt-sparkplug`, `sql-poll-driver`) share its `otopcua-host:dev` image, and it had @@ -917,9 +917,96 @@ serves the seeded `OtFixtureCnc` model with live-moving values. Note it answers | 2. Typed config modal renders | ✅ | "Configure driver · MTConnect" with all four sections (Agent / Polling / Connectivity probe / Reconnect), **not** the "no typed config form" banner. **This is the mutation that survived all 749 AdminUI tests** — the `switch` in `DriverConfigModal.razor` compiles to `BuildRenderTree` and is unreachable by reflection. | | 3. Config persists + round-trips | ✅ | Stored `{"agentUri":"http://10.100.0.35:5000","requestTimeoutMs":5000,…,"probe":{"enabled":true,…},"reconnect":{…}}`; reopening the modal displays the saved URI (Razor binding verified — no bUnit in this repo). | | 4. Browse picker → commit | ✅ | "Connect & browse" dialled the live Agent; tree rendered `Agent` + `OtFixtureCnc` with `Favail` (name ≠ id) beside id-named leaves, proving `browseName = Name ?? Id`. Committed TagConfig is **`{"fullName":"fixture_asset_changed","dataType":"String"}`** — the `fullName`-not-`address` fix, live. Wave-0 #468 did **not** block this. | -| 5. Deploy → OPC UA read/subscribe | ⛔ **BLOCKED — rig artifact** | see below | +| 5. Deploy → OPC UA read/subscribe | ✅ **PASSED (2026-07-27, post-rebase)** | see "Leg 5" below | -**Why leg 5 is blocked, and why it is not a product defect.** Browser cookies are scoped to a +--- + +#### Leg 5 — PASSED 2026-07-27, on the rebased branch + +Re-run after the rebase onto master (`123ddc3f`), against a **freshly rebuilt** `otopcua-host:mtconnect` +image, so this gate exercises the merged code and not the pre-rebase tree. + +**The cookie blocker was sidestepped, not worked around.** Rather than moving hosts or stopping the +`otopcua-dev` rig, the deploy went through the **headless deploy API** — +`POST /api/deployments` with `X-Api-Key` (`DeployApiEndpoints`, `Security:DeployApiKey`). It is +`AllowAnonymous().DisableAntiforgery()` by design ("machine endpoint, not a browser form post"), so +the antiforgery cookie collision is structurally irrelevant to it. This is the better recipe for any +future isolated-stack gate — it needs no browser at all: + +```bash +curl -s -X POST http://localhost:9220/api/deployments \ + -H "X-Api-Key: docker-dev-deploy-key" -H "Content-Type: application/json" \ + -d '{"CreatedBy":"mtconnect-live-gate"}' +# {"outcome":"Accepted","deploymentId":"…","revisionHash":"…"} HTTP 202 +``` + +Both deployments sealed `Status = 2` (`DeploymentStatus.Sealed`) with `FailureReason = NULL` — i.e. +**both** MAIN nodes acked. No `MaintenanceMode` hatch was needed: the isolated stack's topology is +MAIN-only with both central nodes enabled and running. + +**Tag set.** Leg 4's AdminUI-committed tag (`fixture_asset_changed`) is an `AssetChanged` EVENT that +never moves, so three live-changing tags were added **directly via SQL** (deliberately bypassing the +typed editor — see the finding below) to exercise all three type paths. + +**Results — read (`Client.CLI read`, `opc.tcp://localhost:4860`):** + +| NodeId (`ns=2`, Raw realm) | Value | Status | +|---|---|---| +| `s=mtc-1/vmc/fixture_partcount` | `48099` (Int64) | `0x00000000` Good | +| `s=mtc-1/vmc/fixture_x_pos` | `96.4416` (Float64) | `0x00000000` Good | +| `s=mtc-1/vmc/fixture_execution` | `ACTIVE` (String) | `0x00000000` Good | +| `s=mtc-1/vmc/fixture_asset_changed` | *(null)* | `0x80000000` — see finding 6 | + +The RawPath is `mtc-1/vmc/` (driver/device/tag; no folder prefix), confirmed by +`browse -r`, which rendered all four as `[Variable]` under the `vmc` device object. + +**Results — subscribe (`Client.CLI subscribe -r` over the whole device, 500 ms):** all four monitored; +live updates flowed continuously from the `/sample` long-poll pump through to OPC UA — `fixture_x_pos` +tracing its sinusoid (`103.66 → 123.18 → 141.90 → 155.27 → 159.99 → 154.93 → 141.32 → 122.48 → 103.03 +→ 87.75 → 80.35`), `fixture_partcount` incrementing monotonically (`48128 → 48129 → 48130`), and +`fixture_execution` transitioning `READY → INTERRUPTED`. **This closes the last unproven link**: the +driver seam was already covered by the live integration suite, and leg 5 adds `DeploymentArtifact` → +address-space materialisation → OPC UA publish on top of it. + +**Additional findings from leg 5:** + +5. **A bad `dataType` degrades to exactly one skipped tag, loudly — verified by accident.** The + hand-written SQL used `"dataType":"Double"`; the vocabulary is `DriverDataType`, whose member is + **`Float64`**. The driver logged, per tag: + > `could not map the raw tag mtc-1/vmc/fixture_x_pos to an Agent DataItem; it names no readable id + > (expected 'fullName', 'dataItemId' or 'address') or declares an unrecognised driverDataType. The + > tag is skipped and reports BadNodeIdUnknown; every other tag is unaffected.` + + …and the other three tags kept streaming. That is the intended fail-isolated behaviour, + demonstrated live rather than argued. Correcting the row to `Float64` and redeploying turned the + tag Good with **no container restart** — which incidentally shows **MTConnect is not subject to the + separately-tracked config-edits-silently-discarded defect** that affects Modbus/FOCAS/OpcUaClient. + Note also that the typed AdminUI editor would have prevented the mistake outright (it offers a + `DriverDataType` dropdown); the error was an artifact of authoring straight into SQL. + +6. **`fixture_asset_changed` reading `0x80000000` is NOT an MTConnect defect — it is a pre-existing, + deliberate fidelity gap on the shared publish path.** The agent does return the tag in `/current` + as `UNAVAILABLE`, and the driver maps it correctly to + `BadNoCommunication` (`0x80310000`, `MTConnectObservationIndex.cs:255`). The node nevertheless + reports generic `Bad` (`0x80000000`) — while carrying the agent's exact source timestamp, which + proves the publish landed and only the status differs. Cause, verified by reading the source: + `DriverInstanceActor.QualityFromStatus` (`DriverInstanceActor.cs:1032-1041`) projects the driver's + `uint` onto the 3-state `OpcUaQuality` enum using only the top two severity bits (`statusCode >> 30`), + and `OtOpcUaNodeManager.StatusFromQuality` (`OtOpcUaNodeManager.cs:3318-3322`) re-expands `Bad` to + `StatusCodes.Bad`. It is intentional — `OpcUaQuality`'s own doc comment says *"Real SDK has + finer-grained codes; the engine actors only need this 3-state classification."* + + The consequence appears undocumented and is client-visible: **no driver's Bad/Uncertain sub-code + ever reaches an OPC UA client.** Issue #497's 16 corrected constants and the new + `StatusCodeParityTests` guard keep the constants internally right, but a client cannot tell + `BadNoCommunication` from `BadTypeMismatch` from `BadNotSupported`. (The one survivor is + `BadWaitingForInitialData`, written directly at the node at `OtOpcUaNodeManager.cs:1806/1901`, + bypassing the projection.) Tracked separately; out of scope for this plan. + +--- + +**Historical — why leg 5 was blocked on 2026-07-24 (superseded by the API-key recipe above).** +Browser cookies are scoped to a **host, not a port**, so the AdminUI antiforgery cookie issued by the still-running `otopcua-dev` rig on `localhost:9200` is sent to the isolated stack on `localhost:9220`, whose data-protection keys differ: @@ -933,12 +1020,9 @@ The `Deploy current configuration` POST is rejected before reaching any MTConnec (the antiforgery cookie is `HttpOnly`). This is a two-stacks-on-one-host collision, entirely outside the driver. -**To finish leg 5**, do any one of: (a) use a distinct host so cookies don't collide (a hosts entry, -or bind `127.0.0.2`), (b) drive `:9220` from an incognito/separate browser profile, or (c) stop the -`otopcua-dev` rig first. Then Deploy and: -`dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- read -u opc.tcp://localhost:4860 -n "ns=2;s=docker-dev/mtc-1/vmc/fixture_asset_changed"`. -Note the RawPath keying is already proven at the driver seam by the live integration test above; -what leg 5 adds is the `DeploymentArtifact` → address-space → OPC UA materialisation on top. +The three browser-side remedies originally listed (distinct host / incognito profile / stop the +`otopcua-dev` rig) all remain valid, but none was needed — the headless deploy API above avoids the +browser entirely and is the recommended recipe. **Incidental findings from the rig work:** 1. ~~`ClusterNode` has no `MaintenanceMode` column, so CLAUDE.md is ahead of the migration.~~ @@ -946,10 +1030,15 @@ what leg 5 adds is the `DeploymentArtifact` → address-space → OPC UA materia `20260722125506_AddClusterNodeMaintenanceMode` exists on master, along with `AkkaPort`/`GrpcPort`. **This branch is 39 commits behind master** (base `963eec1b`, master `28c28667`) and simply predates them, so the isolated stack's schema lacked the column and I reduced the seeded topology - by deleting the SITE-A/SITE-B rows instead. CLAUDE.md is accurate. **Rebase onto master before - merging** — and once rebased, `MaintenanceMode = 1` on the site nodes is the correct, documented - hatch for a partial-topology live gate (it is what the SQL-poll driver's gate used), in place of - the row deletion I resorted to. + by deleting the SITE-A/SITE-B rows instead. CLAUDE.md is accurate. **DONE 2026-07-27 — rebased** + onto master `123ddc3f` (67 commits by then, not 39). Conflicts were all of one shape — this driver + and the newly-merged SQL-poll driver each adding their entry to the same list — resolved by keeping + **both** in: `ZB.MOM.WW.OtOpcUa.slnx`, `DriverTypeNames.cs`, `DriverFactoryBootstrap.cs`, + `Core.Abstractions.Tests.csproj`, `TagConfigEditorMap.cs`, `TagConfigValidator.cs`, + `DriverConfigModal.razor`, `RawDriverTypeDialog.razor`. Post-rebase: solution build 0 errors, + MTConnect 491/491, Core.Abstractions 256/256, AdminUI 800/800. For a future partial-topology gate, + `MaintenanceMode = 1` is now available and is the correct hatch (it is what the SQL-poll driver's + gate used), in place of the row deletion I resorted to. 2. `sqlcmd` against this schema needs `-I` (`SET QUOTED_IDENTIFIER ON`); without it every DML statement fails with `Msg 1934` because of the filtered/computed indexes. 3. **Status-code parity is pre-verified against master's new guard.** Master gained @@ -961,7 +1050,8 @@ what leg 5 adds is the `DeploymentArtifact` → address-space → OPC UA materia `BadNodeIdUnknown 0x80340000`, `BadNotConnected 0x808A0000`, `BadNotSupported 0x803D0000`, `BadOutOfRange 0x803C0000`, `BadTypeMismatch 0x80740000`, `BadWaitingForInitialData 0x80320000` — **0 mismatches**. Note the guard only sees *named* constants; an inline status literal at a call - site is invisible to it, so keep hoisting them. + site is invisible to it, so keep hoisting them. **Confirmed after the rebase**: the guard reports + 9 MTConnect constants in scope (the 8 above plus `Good`), all passing — the pre-verification held. 4. **The separately-tracked `BadTypeMismatch` defect in FOCAS/TwinCAT/AbLegacy/AbCip is already fixed on master** (issue #497 grew it to 16 wrong constants across 6 drivers). This driver independently chose the correct `0x80740000` and pinned it with a mutation test, so the two agree; no action.