docs: MTConnect write-back recorded; stale MQTT write-through/browse-gap records reconciled
This commit is contained in:
+59
-12
@@ -123,14 +123,21 @@ reflection in `tests/Core/…Core.Abstractions.Tests/DriverTypeNamesGuardTests.c
|
||||
|
||||
| Driver | Verdict | Merged | Capabilities | Notes |
|
||||
|---|---|---|---|---|
|
||||
| **MTConnect** | Complete-with-gaps | `90bdaa44` (#506) | `IDriver, IReadable, ISubscribable, ITagDiscovery, IHostConnectivityProbe, IRediscoverable` (`MTConnectDriver.cs:63`) | No `IWritable` **by design** (`:50`). No device form. |
|
||||
| **MQTT / Sparkplug B** | **Complete** | `c3a2b0f7` | `+ IRediscoverable, IAsyncDisposable` (`MqttDriver.cs:65-66`) | Cleanest of the five: picker + driver form + device form + tag editor all present. 8 open follow-up issues. |
|
||||
| **MTConnect** | Complete-with-gaps | `90bdaa44` (#506) | `IDriver, IReadable, ISubscribable, ITagDiscovery, IHostConnectivityProbe, IRediscoverable` (`MTConnectDriver.cs:63`) | ~~No `IWritable` **by design** (`:50`).~~ ✅ **`IWritable` added 2026-08-05 (`b29623cf`)** — observation injection via the Agent's `AllowPut` surface, off by default at two gates (`Write.Enabled` + the Agent's own `AllowPut`); MTConnect *Interfaces* stays deferred. No device form. |
|
||||
| **MQTT / Sparkplug B** | **Complete** | `c3a2b0f7` | `+ IRediscoverable, IAsyncDisposable` (`MqttDriver.cs:65-66`) — **and `IWritable` since `72be1922`** | Cleanest of the five: picker + driver form + device form + tag editor all present. ~~8 open follow-up issues.~~ ✅ All eight (#508–#515) **closed 2026-07-31**. |
|
||||
| **Sql** | Complete-with-gaps | `4ad54037`, follow-ups `28c28667` | `IDriver, ITagDiscovery, IReadable, ISubscribable, IHostConnectivityProbe` (`SqlDriver.cs:28-29`) | No device form; no browse-commit branch; stale comments. |
|
||||
| **Modbus RTU-over-TCP** | **Complete** | `0f38f486` (#495) | transport mode inside Modbus (`ModbusTransportMode.cs`, `ModbusRtuOverTcpTransport.cs`) | Parses transport **by name** (`ModbusDriverFactoryExtensions.cs:77-78`) — dodges the systemic numeric-enum authoring bug. |
|
||||
| **Calculation** | **Partial (authoring gap)** | pre-existing | `IDriver, IDependencyConsumer, ISubscribable, IReadable` (`CalculationDriver.cs:38`) | **Registered + offered in the picker, but has no config form.** See §1.2. |
|
||||
|
||||
`IWritable` is absent from MTConnect, MQTT, Sql and Calculation. In all four this is **structural and
|
||||
documented**, not a stub (`MTConnectDriver.cs:50`, `MqttDriver.cs:483`, `SqlDriver.cs:15`).
|
||||
~~`IWritable` is absent from MTConnect, MQTT, Sql and Calculation. In all four this is **structural and
|
||||
documented**, not a stub (`MTConnectDriver.cs:50`, `MqttDriver.cs:483`, `SqlDriver.cs:15`).~~
|
||||
|
||||
✅ **Two of the four gained it since the audit. As of 2026-08-05 `IWritable` is absent from Sql and
|
||||
Calculation only** — and in both that absence is still structural and documented (`SqlDriver.cs:15`).
|
||||
**MQTT** gained it 2026-07-31 (`72be1922`,
|
||||
[#508](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/508), live-gated — plain `writeTopic`
|
||||
publish + Sparkplug NCMD/DCMD); **MTConnect** gained it 2026-08-05 (`b29623cf` — observation injection
|
||||
through the Agent's `AllowPut` surface, off by default at two independent gates).
|
||||
|
||||
**Zero `NotImplementedException`** across all five drivers. Every `NotSupportedException` is either a
|
||||
`catch` filter or a deliberate domain error.
|
||||
@@ -195,14 +202,14 @@ incomplete), 2 CANNOT VERIFY (correctly filed as hardware/fixture-gated).
|
||||
| **519** | No Bad/Uncertain **sub-code** reaches a client | **CONFIRMED** | Collapse at `DriverInstanceActor.cs:1033-1041` (`statusCode >> 30`), re-expand at `OtOpcUaNodeManager.cs:3318-3322`. Enum is 3-state at `IOpcUaAddressSpaceSink.cs:165`. **Wider than filed** — `StatusFromQuality` also used at `:429/:506/:576/:761`, so alarm-condition Quality collapses too. | Design consequence |
|
||||
| **517** | Health published AFTER teardown | **CONFIRMED, fleet-wide** | `ModbusDriver.cs:244-250`, `MTConnectDriver.cs:579-590`, `S7Driver.cs:292-326` — all `Teardown…` then `WriteHealth(Unknown)`; `GetHealth()` is a plain field read | Defect (race) |
|
||||
| **516** ✅ | Driver config edits silently discarded — **RESOLVED `d32d89c3` + `5184a2e1`: seam respawn + in-place re-parse; all 12 drivers now honour a changed config. Stale-open on tracker.** | **CONFIRMED — survey now complete** | `DriverInstanceActor.cs:316` (only `_driver` assignment) + `DriverHostActor.cs:2565` (`Tell` existing child, no respawn) + `:653/:660` (green seal). **Affected: Modbus, FOCAS, OpcUaClient, + AbLegacy (issue missed it), + Sql (deliberate/documented).** AbCip/TwinCAT re-parse ✅; Galaxy fails **loud** (throws, `GalaxyDriver.cs:600-641`) | Defect |
|
||||
| **515** | Sparkplug `DataSet`/`Template`/`PropertySet` | **CONFIRMED** | `SparkplugCodec.cs:207-210` decode to `Unsupported` — visible refusal, not silent drop. *(`PropertySet` isn't a `ValueOneofCase`; it rides `metric.Properties`)* | Deferred feature |
|
||||
| **514** | Rebirth unarmable on a plant that hasn't birthed | **CONFIRMED** | `RawBrowseModal.razor:119-120` disabled; `_rebirthTarget` set only by tree-click handlers (`:432`, `:437`, `:439-451`). Empty tree ⇒ permanently disabled | Defect (chicken-and-egg) |
|
||||
| **513** | Meaningless `payloadFormat` in Sparkplug blob | **CONFIRMED** | `MqttTagConfigModel.cs:210` unconditional `Set`, vs. mode-aware `:216` | Defect (cosmetic) |
|
||||
| **512** | `.Browser` → `.Driver` layering | **CONFIRMED** | `…Mqtt.Browser.csproj:43` (**not :44**), boxed "KNOWN, DELIBERATE EXCEPTION" at `:16-42` | Deferred refactor |
|
||||
| **511** | `ActAsPrimaryHost` does nothing | **CONFIRMED** | Option at `MqttDriverOptions.cs:193`, round-tripped in the UI, handled only by a warning at `MqttDriver.cs:933-948`. No STATE publish, no Last Will | Deferred feature |
|
||||
| **510** | `_canRebirth` captured at browse-open | **CONFIRMED** | `RawBrowseModal.razor:377` sole `true` assignment; failure path `:516-539` never re-evaluates | Defect (stale affordance) |
|
||||
| **509** | Metric name with `/` unbrowse-committable | **CONFIRMED** | `RawBrowseCommitMapper.cs:63` uses browse name verbatim → `RawPaths.cs:39` rejects `/` → `RawTreeService.cs:976` **all-or-nothing** kills the whole batch | Defect |
|
||||
| **508** | MQTT write-through | **CONFIRMED** | `MqttDriver.cs:66` — no `IWritable`; consequence at `DriverInstanceActor.cs:673-677` | Deferred feature |
|
||||
| **515** ✅ | Sparkplug `DataSet`/`Template`/`PropertySet` — **RESOLVED `c74ccc55`: structured metrics flattened to sibling tags; live-gated.** | **CONFIRMED** | `SparkplugCodec.cs:207-210` decode to `Unsupported` — visible refusal, not silent drop. *(`PropertySet` isn't a `ValueOneofCase`; it rides `metric.Properties`)* | Deferred feature |
|
||||
| **514** ✅ | Rebirth unarmable on a plant that hasn't birthed — **RESOLVED `7f53bd96`: the browse modal takes a typed `group/edgeNode` scope when no tree scope is armed (edge node only, never a group broadcast).** | **CONFIRMED** | `RawBrowseModal.razor:119-120` disabled; `_rebirthTarget` set only by tree-click handlers (`:432`, `:437`, `:439-451`). Empty tree ⇒ permanently disabled | Defect (chicken-and-egg) |
|
||||
| **513** ✅ | Meaningless `payloadFormat` in Sparkplug blob — **RESOLVED `7f53bd96`.** | **CONFIRMED** | `MqttTagConfigModel.cs:210` unconditional `Set`, vs. mode-aware `:216` | Defect (cosmetic) |
|
||||
| **512** ✅ | `.Browser` → `.Driver` layering — **RESOLVED `7f53bd96`.** | **CONFIRMED** | `…Mqtt.Browser.csproj:43` (**not :44**), boxed "KNOWN, DELIBERATE EXCEPTION" at `:16-42` | Deferred refactor |
|
||||
| **511** ✅ | `ActAsPrimaryHost` does nothing — **RESOLVED `7f2108dc`: Sparkplug primary-host STATE publishing + Will, Primary-gated; live-gated.** | **CONFIRMED** | Option at `MqttDriverOptions.cs:193`, round-tripped in the UI, handled only by a warning at `MqttDriver.cs:933-948`. No STATE publish, no Last Will | Deferred feature |
|
||||
| **510** ✅ | `_canRebirth` captured at browse-open — **RESOLVED `7f53bd96`.** | **CONFIRMED** | `RawBrowseModal.razor:377` sole `true` assignment; failure path `:516-539` never re-evaluates | Defect (stale affordance) |
|
||||
| **509** ✅ | Metric name with `/` unbrowse-committable — **RESOLVED `7f53bd96`: `RawBrowseCommitMapper.ToRawSegment` sanitises the segment and refuses a collision in words.** | **CONFIRMED** | `RawBrowseCommitMapper.cs:63` uses browse name verbatim → `RawPaths.cs:39` rejects `/` → `RawTreeService.cs:976` **all-or-nothing** kills the whole batch | Defect |
|
||||
| **508** ✅ | MQTT write-through — **RESOLVED `72be1922`: `IWritable` shipped (plain `writeTopic` publish + Sparkplug NCMD/DCMD), live-gated 2026-07-31. The evidence column is the audit snapshot; `MqttDriver.cs:66` now names `IWritable`.** | **CONFIRMED** | `MqttDriver.cs:66` — no `IWritable`; consequence at `DriverInstanceActor.cs:673-677` | Deferred feature |
|
||||
| **507**→ see above | | | | |
|
||||
| **491** | Continuous-historization live gate | **CANNOT VERIFY** (needs VPN'd gateway) | Code **is** complete: `AddressSpaceApplier.cs:238`→`:540`→`ActorHistorizedTagSubscriptionSink.cs:27-38`→`ContinuousHistorizationRecorder.cs:84`, wired `ServiceCollectionExtensions.cs:435` | Deferred test |
|
||||
| **489** | Hot-rebind renamed raw tag | **CONFIRMED** | No implementation exists | Deferred feature |
|
||||
@@ -1250,6 +1257,46 @@ drivers), 3 carried-open archreview findings led by 01/S-1 (`AddressSpaceApplier
|
||||
|
||||
---
|
||||
|
||||
### 2026-08-05 — MTConnect write-back; the MQTT closure records reconciled
|
||||
|
||||
Recorded from branch `feature/mqtt-mtconnect-write`. This is exactly the drift shape the 2026-08-04
|
||||
entry named — **work that closes an item elsewhere leaves the register asserting the item is open** —
|
||||
so it is written back here in the same change as the work.
|
||||
|
||||
**§1.1 corrected on two rows.** `IWritable` is no longer absent from four drivers; **after this branch
|
||||
it is absent from Sql and Calculation only**, and in both that is still structural.
|
||||
|
||||
| Driver | Gained `IWritable` | Commit | Shape |
|
||||
|---|---|---|---|
|
||||
| **MQTT / Sparkplug B** | 2026-07-31 ([#508](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/508), live-gated) | `72be1922` | Plain `writeTopic` publish + Sparkplug NCMD/DCMD |
|
||||
| **MTConnect** | 2026-08-05 | `b29623cf` | **Observation injection** through the Agent's optional `AllowPut` surface — off by default at two independent gates (`Write.Enabled` and the Agent's own `AllowPut`), CONDITION items refused |
|
||||
|
||||
⚠️ **MTConnect write-back is not a machine command, and the register should not be read as saying the
|
||||
driver can now command a CNC.** A write sets what the Agent *reports* for a DataItem; the next
|
||||
adapter/SHDR update overwrites it. MTConnect *Interfaces* — the mechanism the original deferral
|
||||
(`2026-07-15-mtconnect-driver-design.md` §3.6) assumed write-back would need — **remains deferred**, and
|
||||
§3.6 now carries a superseded note saying so. Design:
|
||||
`docs/plans/2026-08-05-mtconnect-writeback-design.md`.
|
||||
|
||||
⚠️ **It also does not make any existing node writable.** The OPC UA `CurrentWrite` bit is composed from
|
||||
the authored `Tag.AccessLevel` alone; driver capability is never consulted at compose time. An operator
|
||||
must author the raw tag **ReadWrite**. Browse's new `Operate` stamp on writable leaves is honest
|
||||
labelling only — nothing persists `SecurityClass`.
|
||||
|
||||
**§2's eight MQTT rows (#508–#515) marked ✅ in place** with the commit that resolved each (`72be1922`,
|
||||
`7f53bd96`, `7f2108dc`, `c74ccc55`), following the register's existing convention. Their evidence
|
||||
columns are left as the audit snapshot — #508's `MqttDriver.cs:66 — no IWritable` in particular is now
|
||||
false of the tree, and is annotated as such rather than rewritten.
|
||||
|
||||
**Docs reconciled in the same change** (they contradicted the code, and each other):
|
||||
`docs/drivers/MTConnect.md` (capability surface, new §Write-back, Deferred, the `DeviceName`
|
||||
double-duty note), `docs/drivers/Mqtt.md` (deleted the #514 "empty tree cannot arm a rebirth" known-gap
|
||||
block and the "Read-only. No writes reach the broker" operational note — both contradicted by that
|
||||
file's own closure line), and `docs/plans/2026-07-24-driver-expansion-tracking.md` (P2 gap bullets +
|
||||
§Next actions, which still listed MQTT P3 and both browse gaps as remaining work).
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
*§1–§8 generated 2026-07-27 against `master` @ `90bdaa44` by five parallel source-verification agents.
|
||||
|
||||
+120
-15
@@ -15,7 +15,7 @@ fixture recipe read
|
||||
|
||||
## What it talks to
|
||||
|
||||
A **MTConnect Agent** — the vendor-neutral read-only telemetry endpoint that
|
||||
A **MTConnect Agent** — the vendor-neutral telemetry endpoint that
|
||||
front-ends a machine tool (or fronts an adapter that itself talks to the
|
||||
machine over SHDR). The driver speaks plain HTTP + XML against the Agent's
|
||||
three standard REST paths:
|
||||
@@ -25,8 +25,13 @@ three standard REST paths:
|
||||
- `/sample` — a `multipart/x-mixed-replace` long-poll stream of observation
|
||||
deltas, keyed by a monotonic sequence number
|
||||
|
||||
v1 is **agent-first and read-only** — Discover + Read + Subscribe, no Write —
|
||||
because the mainstream Agent surface has no "set value" operation.
|
||||
Those three are the whole data plane: **agent-first** — Discover + Read +
|
||||
Subscribe. There is a fourth path, and it is not part of the data plane: the
|
||||
Agent's optional **`AllowPut`** surface, off by default in `agent.cfg`, which
|
||||
this driver's `IWritable` uses (`b29623cf`, 2026-08-05). It sets what the Agent
|
||||
*reports* for a DataItem; nothing reaches the machine. Read
|
||||
[Write-back](#write-back--observation-injection-off-by-default) before enabling
|
||||
it.
|
||||
|
||||
## Built vs. planned — read this before trusting the design doc's §2
|
||||
|
||||
@@ -68,18 +73,22 @@ below).
|
||||
|
||||
## Capability surface
|
||||
|
||||
`MTConnectDriver : IDriver, IReadable, ISubscribable, ITagDiscovery, IHostConnectivityProbe, IRediscoverable`
|
||||
`MTConnectDriver : IDriver, IReadable, IWritable, ISubscribable, ITagDiscovery, IHostConnectivityProbe, IRediscoverable`
|
||||
(`src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect/MTConnectDriver.cs`).
|
||||
|
||||
Deliberately **not** `IWritable` — the Agent surface is read-only by design.
|
||||
Write-back exists only via optional, rarely-deployed MTConnect *Interfaces*
|
||||
(a request/response handshake, not a "set value" operation), and is out of
|
||||
scope for this build — see [Deferred](#deferred-not-in-this-build).
|
||||
`IWritable` landed 2026-08-05 (`b29623cf`), against the Agent's `AllowPut`
|
||||
surface rather than MTConnect *Interfaces*. It is **observation injection, not
|
||||
machine command**, and is off at two independent gates by default — read
|
||||
[Write-back](#write-back--observation-injection-off-by-default) before
|
||||
authoring a writable MTConnect tag. *Interfaces* itself — the protocol's own
|
||||
request/response handshake — is a different mechanism and remains deferred; see
|
||||
[Deferred](#deferred-not-in-this-build).
|
||||
|
||||
| Capability | Path | Notes |
|
||||
|------------|------|-------|
|
||||
| `ITagDiscovery` | `DiscoverAsync` — streams `/probe`'s Device→Component→DataItem tree into the address-space builder | `SupportsOnlineDiscovery = true`, `RediscoverPolicy = Once` |
|
||||
| `IReadable` | `ReadAsync` → one `/current` per call | **Not the production data path** — see below |
|
||||
| `IWritable` | `WriteAsync` → `POST {agent}/{device}?{dataItemId}={value}` on the Agent's `AllowPut` surface | **Off by default** — needs `Write.Enabled` **and** the Agent's `AllowPut`; CONDITION items are refused |
|
||||
| `ISubscribable` | `SubscribeAsync`/`OnDataChange` — the shared `/sample` long-poll pump | **The production data path** |
|
||||
| `IHostConnectivityProbe` | periodic `/probe` under `Probe.*` | No consumer wires it today — see [Known limitations](#known-limitations) |
|
||||
| `IRediscoverable` | watches the Agent's `Header.instanceId` | No consumer wires it today — see [Known limitations](#known-limitations) |
|
||||
@@ -105,6 +114,7 @@ correct and tested, just not on the hot path.
|
||||
"HeartbeatMs": 10000,
|
||||
"Probe": { "Enabled": true, "IntervalMs": 5000, "TimeoutMs": 2000 },
|
||||
"Reconnect": { "MinBackoffMs": 0, "MaxBackoffMs": 30000, "BackoffMultiplier": 2.0 },
|
||||
"Write": { "Enabled": false },
|
||||
"Tags": []
|
||||
}
|
||||
}
|
||||
@@ -123,13 +133,14 @@ Read off `MTConnectDriverOptions` / `MTConnectDriverConfigDto`
|
||||
| Key | Default | Notes |
|
||||
|---|---|---|
|
||||
| `AgentUri` | — (required) | Agent base URI; the driver appends `/probe`, `/current`, `/sample` |
|
||||
| `DeviceName` | `null` | Scopes requests to `{AgentUri}/{DeviceName}/...`; `null` = whole Agent |
|
||||
| `DeviceName` | `null` | **Double duty.** (1) Scopes requests to `{AgentUri}/{DeviceName}/...`; `null` = whole Agent. (2) The write path's fallback device — the device a write is addressed to when the probe model does not say which device declared the DataItem (see [Write-back](#write-back--observation-injection-off-by-default)) |
|
||||
| `RequestTimeoutMs` | `5000` | Per-call deadline for `/probe` and `/current` |
|
||||
| `SampleIntervalMs` | `1000` | The Agent's `/sample?interval=` query param |
|
||||
| `SampleCount` | `1000` | The Agent's `/sample?count=` query param |
|
||||
| `HeartbeatMs` | `10000` | The Agent's `/sample?heartbeat=` query param — also the watchdog's liveness window |
|
||||
| `Probe.Enabled` / `Probe.IntervalMs` / `Probe.TimeoutMs` | `true` / `5000` / `2000` | Background connectivity-probe knobs (mirrors `ModbusProbeOptions`) |
|
||||
| `Reconnect.MinBackoffMs` / `MaxBackoffMs` / `BackoffMultiplier` | `0` / `30000` / `2.0` | Geometric backoff after a failed request or a dropped `/sample` stream |
|
||||
| `Write.Enabled` | `false` | The driver-side half of the write-back opt-in. An absent block means disabled — see [Write-back](#write-back--observation-injection-off-by-default) |
|
||||
| `Tags[]` | `[]` | Pre-v3 / CLI authoring surface — one `MTConnectTagDefinition` per DataItem `id` |
|
||||
| `RawTags[]` | `[]` (deploy-injected) | The v3 data-plane binding — see below |
|
||||
|
||||
@@ -224,6 +235,84 @@ the two need disambiguating, not just OR-ing together). On a changed
|
||||
`/hosts` page — see [Known limitations](#known-limitations) for what that does
|
||||
and does not do.
|
||||
|
||||
## Write-back — observation injection, off by default
|
||||
|
||||
> ⚠️ **A write sets what the Agent *reports*, not what the machine does.** The value
|
||||
> lands in the Agent's observation buffer exactly as if an adapter had reported it —
|
||||
> visible to **every** consumer of that Agent, and **overwritten by the next
|
||||
> adapter/SHDR update for that DataItem**. Nothing reaches the CNC. Never document,
|
||||
> script, or operate this as a machine command. It is genuinely useful for
|
||||
> operator-set EVENT values, for external systems feeding DataItems the adapter does
|
||||
> not own, and for test rigs — and nothing else.
|
||||
|
||||
The mechanism is the Agent's **`AllowPut`** surface (`POST
|
||||
{agent}/{device}?{dataItemId}={value}`), a standard cppagent feature that is **off by
|
||||
default**. It is not MTConnect *Interfaces* — see [Deferred](#deferred-not-in-this-build)
|
||||
for why Interfaces was rejected as the write-back shape. Design record:
|
||||
[`2026-08-05-mtconnect-writeback-design.md`](../plans/2026-08-05-mtconnect-writeback-design.md)
|
||||
§2.2; implementation `b29623cf`.
|
||||
|
||||
### Two independent gates, both off by default
|
||||
|
||||
1. **The Agent** — `AllowPut = true` in `agent.cfg`. Optionally scope it with
|
||||
`AllowPutFrom = <host list>` so only named hosts may write; that list is the Agent's
|
||||
own access control and this driver neither reads nor influences it.
|
||||
2. **The driver** — `"Write": { "Enabled": true }` in the driver config (default
|
||||
`false`; an absent block is disabled).
|
||||
|
||||
Either gate alone leaves the shipped read-only behaviour unchanged: with `Write.Enabled`
|
||||
off every slot is refused `BadNotWritable` without an Agent being touched, and with
|
||||
`AllowPut` off the Agent refuses and the driver reports `BadCommunicationError`.
|
||||
|
||||
### The OPC UA write bit is authored, not derived — read this before filing a bug
|
||||
|
||||
**Enabling write-back does not make any existing node writable over OPC UA.** The
|
||||
`CurrentWrite` access bit is composed from the **authored `Tag.AccessLevel`** alone
|
||||
(`AddressSpaceComposer` → `AddressSpaceApplier`, which additionally requires a
|
||||
non-array tag → `OtOpcUaNodeManager.ComposeAccessLevel`); driver capability is never
|
||||
consulted at compose time. An operator must set the raw tag to **ReadWrite** — a
|
||||
browse-committed tag is created `Read` (`RawBrowseCommitMapper.MapLeaf`) whatever the
|
||||
browse said.
|
||||
|
||||
Browse *does* stamp writable leaves `SecurityClassification.Operate` when
|
||||
`Write.Enabled` is on (and `ViewOnly` otherwise, CONDITION always `ViewOnly`), on the
|
||||
same "a node cannot advertise writability the driver would refuse" principle MQTT
|
||||
pinned. But that stamp is **honest labelling, not the access bit**: nothing persists
|
||||
`SecurityClass` — `Tag` has no such column — so it reaches the browse session's
|
||||
attribute side-panel and stops there.
|
||||
|
||||
### What a write does, slot by slot
|
||||
|
||||
`WriteAsync` never throws and one bad slot never fails the batch (matching `ReadAsync`);
|
||||
even cancellation returns a full-length result set. The refusal order is fixed, and each
|
||||
step answers a different operator question:
|
||||
|
||||
| Condition | Status | Meaning |
|
||||
|---|---|---|
|
||||
| `Write.Enabled` off, **or** the DataItem is CONDITION-category | `BadNotWritable` (`0x803B0000`) | Nothing was attempted; no Agent was involved |
|
||||
| No live Agent client (pre-`InitializeAsync`, faulted initialize, post-shutdown) | `BadNotConnected` (`0x808A0000`) | This driver is not running |
|
||||
| The reference resolves to no DataItem, or to one whose owning device cannot be named | `BadNodeIdUnknown` (`0x80340000`) | There is no Agent URL the write could be addressed to |
|
||||
| The value has no invariant string form — `null`, or non-scalar | `BadTypeMismatch` (`0x80740000`) | Arrays are refused rather than rendered (`Convert.ToString` would emit the literal `System.Double[]`, which the Agent stores happily) |
|
||||
| The Agent refused, or could not be reached | `BadCommunicationError` (`0x80050000`) | Includes an `MTConnectError` document returned under HTTP 200 |
|
||||
|
||||
**CONDITION DataItems are excluded deliberately** — they carry alarm semantics and a
|
||||
different `level|text` wire format, and are out of scope for v1.
|
||||
|
||||
**The Agent's refusal text is logged at Warning and never stored.** It stays out of
|
||||
`DriverHealth.LastError`, which is type-only by repo-wide discipline, and a refused write
|
||||
is not evidence the read path degraded — so a refusal leaves driver health untouched.
|
||||
|
||||
**Device resolution needs no extra config.** `TagBinding.WriteTargetsByDataItemId`,
|
||||
built at bind time from a walk of the cached probe model, remembers which device declared
|
||||
each DataItem, so a multi-device Agent is addressed correctly; the fallback is the
|
||||
configured `DeviceName`, and an unresolvable device is `BadNodeIdUnknown` rather than a
|
||||
guess.
|
||||
|
||||
**Values are rendered with `CultureInfo.InvariantCulture`.** A Boolean therefore writes
|
||||
the invariant string `"True"`/`"False"`, **not** `"1"`/`"0"` — if the consuming adapter or
|
||||
Agent vocabulary expects something else, author the tag as a `String` and write the exact
|
||||
token.
|
||||
|
||||
## Browse — free via the universal browser
|
||||
|
||||
MTConnect ships **no bespoke browser project**. Setting
|
||||
@@ -272,6 +361,13 @@ for anything beyond values-and-conditions.
|
||||
driver instance both resolve to the same one Agent connection. Real
|
||||
per-device routing (a client per device) would be a design change, not a
|
||||
bug fix.
|
||||
|
||||
**The write path is the one place a device name is resolved per DataItem**,
|
||||
and it does not use `RawTagEntry.DeviceName` either: it reads the owning
|
||||
device out of the cached probe model and falls back to the top-level
|
||||
`DeviceName` (see [Write-back](#write-back--observation-injection-off-by-default)).
|
||||
So `MTConnectDriverOptions.DeviceName` does double duty — request scope for
|
||||
reads/browse, fallback write target for writes.
|
||||
3. **Nothing validates a raw tag's declared OPC UA `DataType` against the
|
||||
blob's coercion type.** The `/probe`-sourced inference
|
||||
(`MTConnectDataTypeInference`) narrows how often an author picks a wrong
|
||||
@@ -294,11 +390,17 @@ for anything beyond values-and-conditions.
|
||||
|
||||
## Deferred (not in this build)
|
||||
|
||||
- **Write-back (MTConnect Interfaces).** Design
|
||||
[§3.6](../plans/2026-07-15-mtconnect-driver-design.md#36-iwritable--not-implemented-v1):
|
||||
the mainstream Agent surface is read-only by design; Interfaces is a rare,
|
||||
optional request/response handshake that would mislead if modelled as
|
||||
`IWritable`. Revisit only on a concrete deployment need.
|
||||
- **MTConnect *Interfaces*.** Still deferred, and for the reason design
|
||||
[§3.6](../plans/2026-07-15-mtconnect-driver-design.md#36-iwritable--not-implemented-v1)
|
||||
gave: it is a rare, optional request/response **state-machine handshake**
|
||||
requiring adapter-side cooperation, not a "set value" operation, and would
|
||||
mislead if modelled as `IWritable`.
|
||||
**Write-back itself is no longer deferred** — `b29623cf` shipped `IWritable`
|
||||
over the Agent's `AllowPut` observation-injection surface instead, a different
|
||||
mechanism entirely. See [Write-back](#write-back--observation-injection-off-by-default)
|
||||
and
|
||||
[`2026-08-05-mtconnect-writeback-design.md`](../plans/2026-08-05-mtconnect-writeback-design.md)
|
||||
§2.1.
|
||||
- **P1.5 fast-follow** (design
|
||||
[§9](../plans/2026-07-15-mtconnect-driver-design.md#9-phasing--effort)):
|
||||
CONDITION → native OPC UA Part 9 alarms via `IAlarmSource` (the Galaxy
|
||||
@@ -317,7 +419,10 @@ for anything beyond values-and-conditions.
|
||||
discovery tree shape, `MTConnectDataTypeInference`, observation indexing,
|
||||
`UNAVAILABLE` → `BadNoCommunication`, CONDITION state-word mapping,
|
||||
multipart chunk framing, and ring-buffer re-baseline paging (both the
|
||||
sequence-gap and the `OUT_OF_RANGE` legs).
|
||||
sequence-gap and the `OUT_OF_RANGE` legs). The write path adds
|
||||
`MTConnectAgentClientWriteTests` (`9f17d7ef` — verb, URL, agent-error-in-200
|
||||
detection) and `MTConnectWriteTests` (`b29623cf` — every refusal leg, the
|
||||
batch shape, the browse-stamping flip).
|
||||
- **Integration tests** —
|
||||
`tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.IntegrationTests/` — env-gated
|
||||
on `MTCONNECT_AGENT_ENDPOINT` (default `http://10.100.0.35:5000`), skips
|
||||
|
||||
+5
-10
@@ -521,6 +521,11 @@ and only** browse action that publishes anything.
|
||||
- **Scope** is the last node clicked in the tree. A device or metric resolves **up to its owning edge
|
||||
node** (Sparkplug has no device-scoped rebirth); a group fans out to every observed edge node
|
||||
beneath it and is refused **whole** past 32, publishing nothing.
|
||||
- **An empty tree can still arm a rebirth** ([#514](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/514),
|
||||
fixed `7f53bd96`): when no scope is selected the panel offers a `group/edgeNode` input, so a plant
|
||||
whose edge nodes all birthed before the window opened — the exact case the affordance exists for —
|
||||
is reachable without a tree click. Deliberately an **edge node**, never a group: a typed group
|
||||
broadcast is a self-inflicted broker storm on a large plant.
|
||||
- **Two clicks, never one** — `Request rebirth…` arms a confirm panel naming the resolved scope and
|
||||
its consequence; changing the selection disarms it.
|
||||
- **Gated on `DriverOperator`**, enforced server-side in `BrowserSessionService.RequestRebirthAsync`
|
||||
@@ -547,14 +552,6 @@ in the rebuilt tree.
|
||||
> affected **every** driver's picker since 2026-05-28; it surfaced here because choosing a rebirth
|
||||
> scope is the first flow that requires clicking a label rather than the ▶ toggle.
|
||||
|
||||
> ⚠️ **Known gap — a completely empty tree cannot arm a rebirth.** The rebirth scope is taken from the
|
||||
> last node clicked *in the tree*, so on a plant that has not birthed since the window opened there is
|
||||
> nothing to click and `Request rebirth…` stays disabled — the exact case the affordance exists for.
|
||||
> `MqttBrowseSession.ResolveRebirthTargets` already accepts a bare `{group}/{edgeNode}` for a node the
|
||||
> window has never seen (it calls that "the prime rebirth target"); what is missing is a UI path to
|
||||
> enter one, or to default the scope to the driver's own configured `GroupId`. Until then: use
|
||||
> **Refresh** after any birth, or author the tags by **Manual entry**, which is always available.
|
||||
|
||||
## Test Connect
|
||||
|
||||
`MqttDriverProbe` performs **one CONNECT** (no SUBSCRIBE, no publish) under a `-probe-` client id and
|
||||
@@ -583,8 +580,6 @@ identical words. See [TestConnectProbes.md](TestConnectProbes.md).
|
||||
|
||||
## Operational Notes
|
||||
|
||||
- **Read-only.** No writes reach the broker; every node is ViewOnly. Sparkplug NCMD is used for
|
||||
**rebirth requests only** — never to command a device.
|
||||
- **No alarms, no driver-side history.** Historization is a server-side concern (`isHistorized`).
|
||||
- **Both pair nodes subscribe independently** — MQTT fan-out is per-connection, and the Primary gate
|
||||
applies downstream, not to ingest. This is also why `clientId` must stay unset (above).
|
||||
|
||||
@@ -114,6 +114,21 @@ Pure function in `.Contracts` (shared by driver, browser-commit, and the typed e
|
||||
|
||||
### 3.6 `IWritable` — **not implemented (v1)**
|
||||
|
||||
> **SUPERSEDED 2026-08-05 — write-back shipped, by a different mechanism.**
|
||||
> `MTConnectDriver` now implements `IWritable` (`b29623cf`) over the Agent's optional
|
||||
> **`AllowPut`** surface (`POST {agent}/{device}?{dataItemId}={value}`), gated off by
|
||||
> default at both the Agent (`AllowPut`) and the driver (`Write.Enabled`). That is
|
||||
> **observation injection into the Agent's buffer, not machine command** — see
|
||||
> [`2026-08-05-mtconnect-writeback-design.md`](2026-08-05-mtconnect-writeback-design.md)
|
||||
> §2.2 and `docs/drivers/MTConnect.md` §Write-back.
|
||||
> **The paragraph below is retained because its rationale still holds for *Interfaces*,
|
||||
> which remains deferred** (design §2.1 of the write-back doc re-affirms it: rarely
|
||||
> deployed, needs adapter-side cooperation, a state machine rather than a value write).
|
||||
> Only its two incidental claims about this build are out of date — the driver is no
|
||||
> longer `IWritable`-less, and nodes no longer materialize without `CurrentWrite` *because
|
||||
> of a missing capability*; the `CurrentWrite` bit was in fact always composed from the
|
||||
> authored `Tag.AccessLevel` alone, never from driver capability.
|
||||
|
||||
Justified per research §2.1: the MTConnect Agent surface is read-only by design; write-back exists only via optional, rarely-deployed MTConnect *Interfaces* (a request/response state-machine handshake, not a "set value" — modelling it as `IWritable` would mislead). Capability interfaces are composable, so omitting `IWritable` is idiomatic (Galaxy's write path is even fire-and-forget). Nodes materialize **without** the `AccessLevels.CurrentWrite` bit automatically. Revisit Interfaces only if a concrete deployment needs it.
|
||||
|
||||
### 3.7 `IHostConnectivityProbe` + `IRediscoverable`
|
||||
|
||||
@@ -179,7 +179,8 @@ Both CI-simulatable on the shared docker host, no hardware.
|
||||
surface, data-plane precedence rules, quality-code mapping, and (most load-bearing) the **Known
|
||||
limitations** section.
|
||||
- **Scope shipped:** `IDriver`+`ITagDiscovery`+`IReadable`+`ISubscribable`+`IHostConnectivityProbe`+`IRediscoverable`
|
||||
(deliberately no `IWritable` — read-only Agent surface), browse **free via the Wave-0 universal
|
||||
(P1 shipped deliberately without `IWritable`; **`IWritable` added 2026-08-05, `b29623cf`** — see the
|
||||
Deferred line below), browse **free via the Wave-0 universal
|
||||
browser** (`SupportsOnlineDiscovery=true`, no browser project), typed AdminUI tag editor,
|
||||
`UNAVAILABLE→BadNoCommunication` mapping, CONDITION-as-`String`, ring-buffer re-baseline paging
|
||||
(both the sequence-gap and `OUT_OF_RANGE` legs).
|
||||
@@ -191,7 +192,18 @@ Both CI-simulatable on the shared docker host, no hardware.
|
||||
- **Test results:** MTConnect unit suite 491/491; integration suite 12/12 against a real Agent
|
||||
(`mtconnect/agent:2.7.0.12`, **not** `mtconnect/cppagent` — that Docker Hub repo doesn't exist);
|
||||
skips cleanly (12 Skipped) offline. AdminUI 749/749; full solution 0 build errors.
|
||||
- **Deferred (documented, not built):** write-back via MTConnect Interfaces (design §3.6); P1.5
|
||||
- **Write-back — SHIPPED 2026-08-05, not via Interfaces** (`b29623cf`, branch
|
||||
`feature/mqtt-mtconnect-write`; design
|
||||
[`2026-08-05-mtconnect-writeback-design.md`](2026-08-05-mtconnect-writeback-design.md)).
|
||||
`IWritable` over the Agent's optional **`AllowPut`** surface
|
||||
(`POST {agent}/{device}?{dataItemId}={value}`) — **observation injection into the Agent's
|
||||
buffer, never a machine command**, gated off by default at both the Agent (`AllowPut`) and the
|
||||
driver (`Write.Enabled`), CONDITION items refused. **MTConnect *Interfaces* remains deferred**
|
||||
— design §3.6's rationale still holds for it (rare, adapter-side cooperation, a state machine
|
||||
rather than a value write), and it is a different mechanism, not this one. Caveat recorded in
|
||||
the driver guide: the OPC UA `CurrentWrite` bit still comes from the authored
|
||||
`Tag.AccessLevel` alone, so an operator must author **ReadWrite**.
|
||||
- **Deferred (documented, not built):** MTConnect *Interfaces* (design §3.6, above); P1.5
|
||||
CONDITION→native Part-9 alarms, `TIME_SERIES` array materialization, EVENT→enum vocab (design §9);
|
||||
P2 SHDR adapter ingest (design §9). Also two pre-existing fleet-wide gaps this build surfaced
|
||||
rather than caused: `IRediscoverable`/`IHostConnectivityProbe` have no server-side consumer (eight
|
||||
@@ -210,7 +222,11 @@ Both CI-simulatable on the shared docker host, no hardware.
|
||||
`OnDataChange`, retained last-value read, `#`-observation browser); P2 Sparkplug B ingest
|
||||
(vendored Tahu proto + `Grpc.Tools` codegen, birth/alias/seq-gap/rebirth state machine,
|
||||
death→STALE, birth-driven browse tree, scoped Request-rebirth NCMD, typed tag editor).
|
||||
**Write-through (`IWritable`) is deferred to P3** — every MQTT node materializes read-only.
|
||||
**P3 write-through (`IWritable`) shipped 2026-07-31** — `72be1922`
|
||||
([#508](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/508), live-gated): plain
|
||||
`writeTopic` publish + Sparkplug NCMD/DCMD, with a node discovered `ViewOnly` when the encoder
|
||||
would refuse the write. Superseded the "every MQTT node materializes read-only" line this entry
|
||||
carried; see `docs/drivers/Mqtt.md` §Write-through.
|
||||
- **Fixture:** Mosquitto TLS+auth broker + JSON publisher sidecar, **live** at `10.100.0.35:8883`
|
||||
(`:1883` plaintext-but-authenticated); stack `/opt/otopcua-mqtt`, compose in
|
||||
`tests/Drivers/….Driver.Mqtt.IntegrationTests/Docker/`. Env-gated live suite
|
||||
@@ -236,13 +252,31 @@ Both CI-simulatable on the shared docker host, no hardware.
|
||||
Sparkplug keys, merge-not-replace on save, group-id segment validation) + 8 falsifiable
|
||||
`MqttDriverFormModelTests` cases. **Three gates, three instances of the same defect class: a
|
||||
hand-maintained AdminUI surface left behind by a driver-side feature.**
|
||||
- **P2 live gate — two open UI gaps, documented not fixed** (see `docs/drivers/Mqtt.md` §Known gaps):
|
||||
- **A completely empty browse tree cannot arm a rebirth.** The scope comes from a tree click, so on
|
||||
a plant that has not birthed since the window opened `Request rebirth…` stays disabled — the very
|
||||
case the affordance exists for. `MqttBrowseSession.ResolveRebirthTargets` already accepts a bare
|
||||
`{group}/{edgeNode}` ("the prime rebirth target"); only a UI path to enter one is missing.
|
||||
Mitigation shipped: a **Refresh** button on the browse tree (it re-reads the same session, which
|
||||
previously rendered exactly once at open and never again).
|
||||
- **P2 live gate — four UI gaps recorded, ALL FOUR NOW CLOSED 2026-07-31** (`7f53bd96`; see
|
||||
`docs/drivers/Mqtt.md` §Known gaps, which lists the closures). Recorded as found, with what
|
||||
actually fixed each:
|
||||
- **A completely empty browse tree cannot arm a rebirth**
|
||||
([#514](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/514)). The scope comes from a tree
|
||||
click, so on a plant that has not birthed since the window opened `Request rebirth…` stays
|
||||
disabled — the very case the affordance exists for. `MqttBrowseSession.ResolveRebirthTargets`
|
||||
already accepts a bare `{group}/{edgeNode}` ("the prime rebirth target"); only a UI path to
|
||||
enter one was missing. Mitigation shipped at the time: a **Refresh** button on the browse tree
|
||||
(it re-reads the same session, which previously rendered exactly once at open and never again).
|
||||
**FIXED —** `RawBrowseModal` now offers a `group/edgeNode` scope input whenever no tree scope is
|
||||
armed; edge-node only, never a typed group broadcast.
|
||||
- **A metric name containing `/` cannot be browse-committed**
|
||||
([#509](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/509)). The derived raw **tag Name**
|
||||
is a RawPath segment and may not contain `/`, while `metricName` legitimately may — so the
|
||||
spec-mandatory `Node Control/Rebirth` was refused at commit (`Row N: Name must not contain '/'`).
|
||||
The refusal was loud and all-or-nothing, never a silently mis-bound tag, and **Manual entry** was
|
||||
the working path. Fixing it needed a name-sanitisation policy with a collision answer, so it was
|
||||
recorded rather than guessed at. **FIXED —** `RawBrowseCommitMapper.ToRawSegment` sanitises the
|
||||
segment and refuses a collision explicitly.
|
||||
- **`_canRebirth` is captured at browse-open**, so a reaped session still drew the button
|
||||
([#510](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/510)). **FIXED.**
|
||||
- **The tag editor injects the Plain-only `payloadFormat` key into a Sparkplug blob**
|
||||
([#513](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/513)) — cosmetic; the factory
|
||||
routes on the Sparkplug tuple and ignored it. **FIXED.**
|
||||
- **P2 live gate also found a PRE-EXISTING, cross-driver AdminUI defect — FIXED:** every node label in
|
||||
the shared `DriverBrowseTree` was an `<a href="#">`. In a Blazor Web App, `blazor.web.js`'s
|
||||
enhanced-navigation click interceptor resolves a bare `#` against `<base href="/">`, so **clicking
|
||||
@@ -252,19 +286,14 @@ Both CI-simulatable on the shared docker host, no hardware.
|
||||
now `<button type="button">`. It dates to the component's introduction (2026-05-28) and affects
|
||||
**every** driver's picker; it only surfaced now because Sparkplug's Request-rebirth is the first
|
||||
feature that requires clicking a *label* rather than the ▶ toggle (always a `<button>`, always fine).
|
||||
- **A metric name containing `/` cannot be browse-committed.** The derived raw **tag Name** is a
|
||||
RawPath segment and may not contain `/`, while `metricName` legitimately may — so the
|
||||
spec-mandatory `Node Control/Rebirth` is refused at commit (`Row N: Name must not contain '/'`).
|
||||
The refusal is loud and all-or-nothing, never a silently mis-bound tag, and **Manual entry** is
|
||||
the working path (the Sparkplug tag editor accepts a slashed metric name). Fixing it needs a
|
||||
name-sanitisation policy with a collision answer, so it was recorded rather than guessed at.
|
||||
- `_canRebirth` is captured at browse-open, so a reaped session still draws the button.
|
||||
- The tag editor injects the Plain-only `payloadFormat` key into a Sparkplug blob — cosmetic; the
|
||||
factory routes on the Sparkplug tuple and ignores it.
|
||||
- **Rediscovery is inert in v3 (platform gap, not MQTT's).** The Sparkplug driver raises
|
||||
`OnRediscoveryNeeded` on a changed birth metric-set, but **nothing subscribes to it** and
|
||||
`DriverHostActor.HandleDiscoveredNodes` hard-returns. A DBIRTH introducing a metric does **not**
|
||||
change the OPC UA tree — redeploy. Driver-side behaviour is log-observable only.
|
||||
- **Rediscovery was inert in v3 (platform gap, not MQTT's) — HALF-CLOSED since 2026-07-27.** The
|
||||
Sparkplug driver raises `OnRediscoveryNeeded` on a changed birth metric-set; at the time nothing
|
||||
subscribed to it and `DriverHostActor.HandleDiscoveredNodes` hard-returned. `DriverInstanceActor`
|
||||
now **consumes** the raise (`09a401b8`) and it rides the driver-health snapshot to the AdminUI
|
||||
`/hosts` page as a **re-browse chip**, while the injection path was **deleted** rather than
|
||||
revived (#507, `adce27e7` — its two inputs are structurally empty in v3). So the standing part is
|
||||
unchanged and deliberate: a DBIRTH introducing a metric still does **not** change the served OPC
|
||||
UA tree; an operator re-browses and commits through `/raw`.
|
||||
- **Redundant-pair hazard (documented, not a code change):** both nodes of a pair run the driver, so a
|
||||
**fixed `clientId` makes them evict each other forever** — the broker logs
|
||||
`already connected, closing old connection` and both nodes reconnect every ~2 s while still
|
||||
@@ -285,7 +314,24 @@ Both CI-simulatable on the shared docker host, no hardware.
|
||||
`0f38f486` / #495) and **SQL poll** (merged `4ad54037`; follow-ups #496/#497/#498 closed in
|
||||
`28c28667`). Remaining SQL work is optional follow-on — Postgres/ODBC dialects behind
|
||||
`ISqlDialect`, and `SqlTagModel.Query` (P3, currently rejected end-to-end by design).
|
||||
3. **Wave 2 is done.** Both deliverables shipped and live-gated: **MQTT / Sparkplug B** (P1 + P2) and **MTConnect Agent** (P1 Agent MVP, PR #506). Remaining work on both is optional follow-on — MQTT P3 write-through (`IWritable`: NCMD/DCMD + plain publish) plus its two browse-UI
|
||||
gaps, and MTConnect write-back (deliberately deferred; the Agent surface is read-only).
|
||||
3. **Wave 2 is done, and so is the follow-on this entry used to list.** Both deliverables shipped and
|
||||
live-gated: **MQTT / Sparkplug B** (P1 + P2) and **MTConnect Agent** (P1 Agent MVP, PR #506).
|
||||
Since then:
|
||||
- **MQTT P3 write-through shipped and was live-gated 2026-07-31** — `72be1922`
|
||||
([#508](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/508)): plain `writeTopic` publish
|
||||
+ Sparkplug NCMD/DCMD.
|
||||
- **Both browse-UI gaps are closed**, with the other two P2 records, in `7f53bd96`
|
||||
([#509](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/509),
|
||||
[#510](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/510),
|
||||
[#513](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/513),
|
||||
[#514](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/514)) — see the P2 section above.
|
||||
- **MTConnect write-back is landing on `feature/mqtt-mtconnect-write`** (`b29623cf`), by
|
||||
**observation injection** through the Agent's `AllowPut` surface rather than the Interfaces
|
||||
handshake the original deferral assumed — design
|
||||
[`2026-08-05-mtconnect-writeback-design.md`](2026-08-05-mtconnect-writeback-design.md), which
|
||||
is also the record for this reconciliation. MTConnect *Interfaces* stays deferred.
|
||||
|
||||
Remaining optional follow-on for the pair: MTConnect P1.5 (CONDITION→Part-9 alarms,
|
||||
`TIME_SERIES` arrays, EVENT→enum vocab) and P2 SHDR ingest.
|
||||
|
||||
Update this file's Summary table and per-wave status whenever a deliverable changes state.
|
||||
|
||||
Reference in New Issue
Block a user