feat(mtconnect): serve DATA_SET/TABLE as compact JSON strings; park EVENT enumerations (#650)
Parser captures Entry/Cell content; the observation index merges /sample deltas into the /current baseline (resetTriggered replaces, removed deletes, UNAVAILABLE drops). Fixture gains a TABLE item and periodic DATA_SET/TABLE churn. Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
+37
-14
@@ -103,8 +103,8 @@ request/response handshake — is a different mechanism and remains deferred; se
|
||||
| `TIME_SERIES` arrays (#627) | **Supported** as variable-length `double[]` (Float64 tags only) | [TIME_SERIES arrays](#time_series-arrays-627) |
|
||||
| One Agent client per driver, one `/raw` Device (#628) | **Enforced** (`MTConnectMultipleDevices` deploy error); multi-device Agents use several driver instances | [Known limitations](#known-limitations) |
|
||||
| Write-back (`IWritable`) | **Supported, off by default** (`Write.Enabled` + Agent `AllowPut`) | [Write-back](#write-back--observation-injection-off-by-default) |
|
||||
| EVENT controlled vocabularies as OPC UA enumerations | **Not supported** (stay String); not scheduled | See [Deferred](#deferred-not-in-this-build) |
|
||||
| `DATA_SET` / `TABLE` representations | **Not supported** - read as `BadNotSupported` | [Quality mapping](#quality-mapping) |
|
||||
| EVENT controlled vocabularies as OPC UA enumerations | **Parked / won't fix** (stay String) - see rationale | [EVENT enumerations](#event-enumerations-650-parked--wont-fix) |
|
||||
| `DATA_SET` / `TABLE` representations (#650) | **Supported** as a String carrying compact JSON | [DATA_SET / TABLE](#data_set--table-650) |
|
||||
| MTConnect Interfaces, SHDR ingest | Not built | [Deferred](#deferred-not-in-this-build) |
|
||||
|
||||
**`IReadable.ReadAsync` is never called by the running server.** The
|
||||
@@ -245,7 +245,7 @@ Other status codes an observation can surface:
|
||||
| `BadTypeMismatch` (`0x80740000`) | The Agent's text isn't a value of the tag's coerced type at all |
|
||||
| `BadOutOfRange` (`0x803C0000`) | The Agent reported a number the coerced type can't represent |
|
||||
| `BadDecodingError` (`0x80070000`) | A `TIME_SERIES` vector whose element count disagrees with the observation's own `sampleCount` (or the tag's fixed `ArrayDim`) — see [TIME_SERIES](#time_series-arrays-627) |
|
||||
| `BadNotSupported` (`0x803D0000`) | A shape this build doesn't materialize — a structured `DATA_SET`/`TABLE` observation (decided: stays unsupported, #627), or an array tag declared as anything other than Float64 |
|
||||
| `BadNotSupported` (`0x803D0000`) | A shape this build doesn't materialize — a `DATA_SET`/`TABLE` observation whose children are not `<Entry>` elements (a well-formed one is served as JSON since #650), or an array tag declared as anything other than Float64 |
|
||||
| `BadWaitingForInitialData` | Tag is authored but the Agent hasn't reported it yet |
|
||||
| `BadNodeIdUnknown` | DataItem id is neither authored nor ever observed |
|
||||
|
||||
@@ -272,8 +272,29 @@ in the Tag modal's array section).
|
||||
but the OPC UA node has no description/property slot through `DriverAttributeInfo`,
|
||||
and a sibling property node would need a new address-space mechanism. Decision:
|
||||
document it — the rate is a property of the Agent's DataItem (`sampleRate` in
|
||||
`/probe`), read it there. **`DATA_SET` / `TABLE` stay `BadNotSupported`** (decision, #627): they are
|
||||
key/value maps with no scalar or array OPC UA shape that preserves the keys.
|
||||
`/probe`), read it there.
|
||||
|
||||
### DATA_SET / TABLE (#650)
|
||||
|
||||
`DATA_SET` and `TABLE` observations carry their content in `<Entry key="…">` children (a `TABLE` entry holds
|
||||
`<Cell key="…">` children). They are served as a **String carrying compact, deterministic JSON** (OPC UA type
|
||||
String, never an array or structure - no new node shape, history/ACL/alarm paths unchanged; discovery and
|
||||
type inference already type these items String):
|
||||
|
||||
- `DATA_SET` -> `{"offsetX":1.25,"toolNumber":7}`; `TABLE` -> `{"G54":{"X":1.0,"Y":2},"G55":{"X":3}}`.
|
||||
- Keys are ordinal-sorted at every level, no whitespace - the same content always serialises to the same
|
||||
bytes, so value-dedup works. Values that are *strict JSON numbers* are emitted as numbers with their original
|
||||
token (lossless: `1.0` stays `1.0`); anything else (`007`, `+5`, `on`) is a string.
|
||||
- **Merge semantics follow MTConnect 2.x.** A `/current` document states the *whole* set and **replaces** it;
|
||||
a `/sample` chunk carries *deltas* that **merge** by key (a `TABLE` row is replaced as a unit by a later entry
|
||||
for the same row key). An observation with `resetTriggered` replaces the accumulated set, and a
|
||||
`resetTriggered` observation with no entries is the empty set `{}`. Several observations of one item in one
|
||||
document fold in document order. The accumulated set is dropped on an Agent restart (`instanceId` change).
|
||||
- `removed="true"` deletes the key (or row): it is **absent** from the JSON, never `null`.
|
||||
- `UNAVAILABLE` is `BadNoCommunication` as for any observation, and also drops the accumulated set, so a
|
||||
later delta cannot resurrect pre-outage entries.
|
||||
- An observation whose children are not `<Entry>` elements stays `BadNotSupported`. Writes to these items stay
|
||||
refused (`WriteAsync` excludes them).
|
||||
|
||||
### Subscribe — the `/sample` pump
|
||||
|
||||
@@ -483,9 +504,13 @@ and records a state only when an alarm handler was attached to receive it. Conse
|
||||
unchanged triggers no re-subscribe, and needs none — the server-side carry-over covers it.
|
||||
- An alarm-only driver (no value tags) still runs the `/sample` pump.
|
||||
|
||||
**EVENT controlled vocabularies as OPC UA enumerations — not built (parked, low priority).** EVENT values (`Execution`,
|
||||
`ControllerMode`, …) stay Strings carrying the Agent's own word; an enumeration would need a per-type
|
||||
vocabulary table that vendors extend freely.
|
||||
### EVENT enumerations (#650): parked / won't fix
|
||||
|
||||
EVENT values (`Execution`, `ControllerMode`, …) stay Strings carrying the Agent's own word. Modelling them as
|
||||
OPC UA enumerations was considered and **declined**: the vocabulary is vendor-extensible, so it needs a per-type
|
||||
table plus an unknown-value fallback, a per-type enum node shape through discovery and raw-tag type inference,
|
||||
and it would break every consumer that reads these items as String (ScadaBridge) for little gain. Reopen only
|
||||
with an opt-in, tag-level `enumValues` design.
|
||||
|
||||
## Known limitations
|
||||
|
||||
@@ -563,9 +588,8 @@ for anything beyond values-and-conditions.
|
||||
pin the parsing rule itself.
|
||||
5. **A CONDITION alarm shows one condition per DataItem** — the most severe active `nativeCode`, not one
|
||||
Part 9 condition per code (see [CONDITION alarms](#condition-alarms-626)).
|
||||
6. **Structured `DATA_SET`/`TABLE` observations surface as
|
||||
`BadNotSupported`**, not an approximation — see the Quality mapping table.
|
||||
`TIME_SERIES` is served as `double[]` (#627, see
|
||||
6. **`DATA_SET`/`TABLE` observations are JSON Strings** (#650, see
|
||||
[DATA_SET / TABLE](#data_set--table-650)); `TIME_SERIES` is served as `double[]` (#627, see
|
||||
[TIME_SERIES arrays](#time_series-arrays-627)); the sample rate is not
|
||||
surfaced on the node.
|
||||
|
||||
@@ -585,9 +609,8 @@ for anything beyond values-and-conditions.
|
||||
- **P1.5 fast-follow** (design
|
||||
[§9](../plans/2026-07-15-mtconnect-driver-design.md#9-phasing--effort)): CONDITION → native Part 9
|
||||
alarms **shipped** (#626, see [CONDITION alarms](#condition-alarms-626)); EVENT
|
||||
controlled-vocabulary values → OPC UA enumerations is **not built** (EVENTs stay Strings), and
|
||||
structured `DATA_SET`/`TABLE` observations stay `BadNotSupported`; both are tracked as a low-priority
|
||||
follow-up, not part of P1.5.
|
||||
controlled-vocabulary values → OPC UA enumerations is **parked / won't fix** (EVENTs stay Strings, #650);
|
||||
`DATA_SET`/`TABLE` are served as JSON Strings (#650).
|
||||
- **P2 (on demand): SHDR adapter ingest.** A `SourceMode: "Agent" | "Shdr"`
|
||||
switch that opens the raw pipe-delimited SHDR TCP socket directly. Loses
|
||||
auto-discovery (no device model without an Agent in front, so
|
||||
|
||||
Reference in New Issue
Block a user