184 lines
9.8 KiB
Markdown
184 lines
9.8 KiB
Markdown
# MTConnect write-back Implementation Plan
|
|
|
|
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers-extended-cc:subagent-driven-development
|
|
> (opus implementers per user instruction).
|
|
|
|
**Goal:** Implement `IWritable` on `MTConnectDriver` via the Agent's HTTP PUT/POST surface
|
|
(opt-in at agent `AllowPut` AND driver `write.enabled`), and reconcile the stale MQTT
|
|
write-through/browse-gap records.
|
|
|
|
**Architecture:** See `docs/plans/2026-08-05-mtconnect-writeback-design.md` (in this branch).
|
|
Write = observation injection into the agent buffer, never machine command. Batch-safe
|
|
`WriteAsync` mirroring `MqttDriver.cs:670-745`; new seam method on `IMTConnectAgentClient`;
|
|
discovery stamps `Operate` only for leaves the driver would accept.
|
|
|
|
**Tech Stack:** .NET 10, xUnit v3 + Shouldly, existing `CannedAgentClient` / stub
|
|
`HttpMessageHandler` seams. No new packages.
|
|
|
|
**Worktree:** `/Users/dohertj2/Desktop/OtOpcUa/.claude/worktrees/mqtt-mtconnect-write`
|
|
(branch `feature/mqtt-mtconnect-write`). All paths below relative to it.
|
|
|
|
---
|
|
|
|
### Task 0: Write options block (config + parse)
|
|
|
|
**Classification:** small
|
|
**Estimated implement time:** ~4 min
|
|
**Parallelizable with:** Task 1
|
|
|
|
**Files:**
|
|
- Modify: `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Contracts/MTConnectDriverOptions.cs`
|
|
- Modify: `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect/MTConnectDriver.cs` (config DTO region
|
|
~:3257-3345 + `ParseOptions` ~:2222)
|
|
- Test: `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Tests/MTConnectDriverOptionsTests.cs`,
|
|
`tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Tests/MTConnectFactoryTests.cs`
|
|
|
|
TDD: failing test first — `{"write":{"enabled":true}}` parses to `Options.Write.Enabled == true`;
|
|
absent block defaults to `false`; malformed value behaves like the sibling `Probe`/`Reconnect`
|
|
blocks (match their existing tolerance/failure semantics exactly — read them first). Add
|
|
`MTConnectWriteOptions` (sealed record, `Enabled`, default false) on `MTConnectDriverOptions`
|
|
following the `Probe`/`Reconnect` pattern, a nullable `WriteDto` on the config DTO, wire through
|
|
`ParseOptions`. Commit `feat(mtconnect): parse the write.enabled config block`.
|
|
|
|
### Task 1: Agent-client write seam
|
|
|
|
**Classification:** standard
|
|
**Estimated implement time:** ~5 min
|
|
**Parallelizable with:** Task 0
|
|
|
|
**Files:**
|
|
- Modify: `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect/IMTConnectAgentClient.cs`
|
|
- Modify: `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect/MTConnectAgentClient.cs`
|
|
- Test: `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Tests/MTConnectAgentClientWriteTests.cs` (new)
|
|
|
|
Add to the seam:
|
|
|
|
```csharp
|
|
Task<MTConnectWriteOutcome> WriteObservationAsync(
|
|
string deviceName, string dataItemId, string value, CancellationToken cancellationToken);
|
|
```
|
|
|
|
with `MTConnectWriteOutcome` a small readonly record: `Accepted` bool + `AgentError` string?
|
|
(agent-produced MTConnectError text) — transport failures THROW (HttpRequestException etc.), the
|
|
driver maps them; agent refusals return `Accepted=false` with the text. Implementation in
|
|
`MTConnectAgentClient` over `_http` (the bounded-timeout unary client): `HttpMethod.Post` to
|
|
`{base}/{deviceName}?{Uri.EscapeDataString(dataItemId)}={Uri.EscapeDataString(value)}` built with
|
|
the same left-part logic as `BuildRequestUri` (:382-403). A 2xx whose body parses as an
|
|
`MTConnectError` document is a refusal (reuse the LocalName-only XML helpers in
|
|
`MTConnectXml.cs`); non-2xx is a refusal carrying the status; empty 2xx is accepted.
|
|
|
|
TDD via the internal `HttpMessageHandler` ctor seam (`MTConnectAgentClient.cs:107`), the same
|
|
pattern `MTConnectDriverProbeTests.cs:22-28` uses: assert verb, exact URI (device path + escaped
|
|
query), accepted on empty 200, refusal on MTConnectError-in-200 (agent text surfaced), refusal on
|
|
404, throw on handler-thrown transport error. Commit
|
|
`feat(mtconnect): agent-client observation write (HTTP POST, AllowPut surface)`.
|
|
|
|
### Task 2: `MTConnectDriver` implements `IWritable` + discovery stamping
|
|
|
|
**Classification:** high-risk
|
|
**Estimated implement time:** ~5 min
|
|
**Parallelizable with:** none
|
|
|
|
**Files:**
|
|
- Modify: `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect/MTConnectDriver.cs` (class decl :63,
|
|
remarks :46-53, WriteAsync new, `TagBinding` device-name map ~:3170-3213, `DiscoverAsync`
|
|
stamping :1030-1046)
|
|
- Modify: `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect/MTConnectDriverFactoryExtensions.cs`
|
|
(remark :33-39)
|
|
- Modify: `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Contracts/MTConnectTagDefinition.cs`
|
|
(remark :10-14)
|
|
- Modify: `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Tests/CannedAgentClient.cs`
|
|
(record writes, scripted outcomes)
|
|
- Test: `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Tests/MTConnectWriteTests.cs` (new),
|
|
`MTConnectDiscoverTests.cs` (stamping cases)
|
|
|
|
Per design §2.3. Status-code constants: follow MQTT's named-const pattern
|
|
(`MqttDriver.cs:76-89`) — `StatusBadNodeIdUnknown 0x80340000`, `StatusBadNotWritable 0x803B0000`,
|
|
`StatusBadTypeMismatch 0x80740000`, `StatusBadCommunicationError 0x80050000`, plus
|
|
`BadNotConnected` matching `ReadAsync`'s existing constant. Refusal order per slot: disabled →
|
|
BadNotWritable; no client → BadNotConnected; unresolved RawPath → BadNodeIdUnknown; CONDITION
|
|
category → BadNotWritable; null/unrepresentable value → BadTypeMismatch; then the seam call —
|
|
`Accepted=false` or thrown transport error → BadCommunicationError (log agent text at Warning,
|
|
never into `LastError`). `OperationCanceledException` fills remaining slots with
|
|
BadCommunicationError and returns. Never throw. Success = `WriteResult(0u)`.
|
|
|
|
Device-name resolution: extend `TagBinding` (or a parallel FrozenDictionary built in
|
|
`BuildBinding` ~:2887-2980) with `dataItemId → owning deviceName` from the probe model; fall back
|
|
to `options.DeviceName`; unresolvable device → BadNodeIdUnknown. Values format via
|
|
`Convert.ToString(value, CultureInfo.InvariantCulture)`.
|
|
|
|
Discovery: `CanWrite(dataItem)` = `options.Write.Enabled && !IsCondition(category)` →
|
|
`SecurityClassification.Operate` else `ViewOnly`; update the :1040-1042 comment. **Verify** how
|
|
materialization derives `CurrentWrite` (search for how MQTT's Operate-stamped nodes get the write
|
|
bit — SecurityClass + capability) and state the finding in the commit message; if a server-side
|
|
gate keys on the driver capability alone, note that existing ViewOnly-committed raw tags keep
|
|
their authored SecurityClass.
|
|
|
|
TDD: every refusal leg observed red first where feasible; the batch-shape test (one bad slot
|
|
doesn't fail the batch) mirrors `MqttDriverWriteTests.cs:74`. The publish itself is proven by
|
|
Task 3 live legs, not by asserting a fake was called beyond the seam-call recording (same
|
|
philosophy as `MqttDriverWriteTests.cs:11-15`). Commit
|
|
`feat(mtconnect): IWritable via agent observation writes (#468-era deferral closed)`.
|
|
|
|
### Task 3: Integration fixture + write legs
|
|
|
|
**Classification:** standard
|
|
**Estimated implement time:** ~5 min
|
|
**Parallelizable with:** Task 4
|
|
|
|
**Files:**
|
|
- Modify: `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.IntegrationTests/Docker/agent.cfg`
|
|
(`AllowPut = true` + comment rewrite)
|
|
- Modify: `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.IntegrationTests/MTConnectAgentIntegrationTests.cs`
|
|
(remove the "(b) Writes" not-covered remark :64-65; add write legs)
|
|
|
|
Legs (env-gated by the existing `MTCONNECT_AGENT_ENDPOINT` fixture, `Assert.Skip` clean):
|
|
1. Driver with `write.enabled` writes a distinct value to a writable EVENT data item (pick one
|
|
from `Devices.xml` the SHDR adapter does not continuously overwrite — read `adapter.py` to
|
|
choose; if every item is pumped, add a static EVENT item to `Devices.xml`) → poll `/current`
|
|
until the observation carries the value (bounded wait, `StreamWait` pattern).
|
|
2. A write naming a nonexistent dataItemId → `BadCommunicationError` (agent refusal mapping).
|
|
3. `write.enabled` absent → same write returns `BadNotWritable` without touching the agent.
|
|
Commit `test(mtconnect): live write legs against the AllowPut fixture`.
|
|
|
|
### Task 4: Docs reconciliation (MTConnect + stale MQTT records)
|
|
|
|
**Classification:** small
|
|
**Estimated implement time:** ~5 min
|
|
**Parallelizable with:** Task 3
|
|
|
|
**Files:**
|
|
- Modify: `docs/drivers/MTConnect.md` (capability :71-77, Deferred :295-301, new §Write-back)
|
|
- Modify: `docs/plans/2026-07-15-mtconnect-driver-design.md` (§3.6 superseded note)
|
|
- Modify: `docs/drivers/Mqtt.md` (delete stale :550-556 and :586-587)
|
|
- Modify: `docs/plans/2026-07-24-driver-expansion-tracking.md` (P2 gap bullets :239-263 → closed
|
|
#509/#510/#513/#514; §Next actions :288-289 rewritten)
|
|
- Modify: `deferment.md` (:126 MTConnect row, :132-133 four-driver IWritable line → Sql +
|
|
Calculation only)
|
|
|
|
Per design §1 + §3. The Write-back section MUST carry the §2.2 semantics warning (observation
|
|
injection, adapter overwrites, two-layer opt-in, `AllowPutFrom` pointer). Commit
|
|
`docs: MTConnect write-back recorded; stale MQTT write-through/browse-gap records reconciled`.
|
|
|
|
### Task 5: Full-suite verification
|
|
|
|
**Classification:** small
|
|
**Estimated implement time:** ~4 min
|
|
**Parallelizable with:** none
|
|
|
|
`dotnet build ZB.MOM.WW.OtOpcUa.slnx` (0 errors) + full MTConnect unit suite + AdminUI suite +
|
|
`dotnet test --filter "FullyQualifiedName~Mqtt"` (docs-only but cheap insurance). Integration
|
|
suite compiles; live legs skip offline. Fix fallout, commit if changes.
|
|
|
|
### Task 6: Live gate (main session, headless — NO chrome)
|
|
|
|
**Classification:** standard
|
|
**Estimated implement time:** ~10 min (main session, after merge)
|
|
|
|
Main session, not a subagent: bring up the MTConnect fixture (`ssh dohertj2@10.100.0.35` compose
|
|
in `/opt/otopcua-mtconnect/` — `lmxopcua-fix sync` equivalent via rsync first, since agent.cfg
|
|
changed), run the Task 3 legs live (`dotnet test --filter` the new write tests with
|
|
`MTCONNECT_AGENT_ENDPOINT` set), then the docker-dev rig leg: author + deploy an MTConnect driver
|
|
with `write.enabled`, `Client.CLI write` a value, read it back via Client.CLI and `/current`.
|
|
Record results in the design doc §Outcome.
|