Merge b626-mtconnect (batch #626+)

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
Joseph Doherty
2026-10-08 14:43:45 -04:00
12 changed files with 373 additions and 54 deletions
+53 -20
View File
@@ -230,10 +230,37 @@ 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 |
| `BadNotSupported` (`0x803D0000`) | A shape this build doesn't materialize — a `TIME_SERIES` vector, or a structured `DATA_SET`/`TABLE` observation (deferred to P1.5; see [Known limitations](#known-limitations)) |
| `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 |
| `BadWaitingForInitialData` | Tag is authored but the Agent hasn't reported it yet |
| `BadNodeIdUnknown` | DataItem id is neither authored nor ever observed |
### TIME_SERIES arrays (#627)
A `SAMPLE` DataItem with `representation="TIME_SERIES"` is served as a
**variable-length `double[]`** (`ValueRank = 1`, no fixed `ArrayDimensions` —
a live Agent never declares `sampleCount` on the DataItem, see Known
limitations 4; a probe that does declare one yields a fixed `ArrayDim`).
Parsing: the observation text is split on any whitespace, each token parsed
invariant-culture. The result is **all-or-nothing** — one non-numeric /
non-finite token is `BadTypeMismatch`, a literal `UNAVAILABLE` token or an
empty body is `BadNoCommunication`, and a count that disagrees with the
observation's own `sampleCount` attribute (or a fixed `ArrayDim`) is
`BadDecodingError`. A partial vector is never served as Good. Only a
**Float64** array tag is served; any other declared element type stays
`BadNotSupported`. An authored tag needs `"isArray": true` and
`"dataType": "Float64"` in its TagConfig (the AdminUI editor shows an advisory
for any other array type; a browse-commit does not stamp `isArray`, so set it
in the Tag modal's array section).
**Sample rate is not exposed on the served node.** The observation's
`sampleRate` and `sampleCount` attributes are parsed (`MTConnectObservation`)
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.
### Subscribe — the `/sample` pump
One shared `/sample` long-poll stream per driver instance (the Agent streams
@@ -383,14 +410,24 @@ for anything beyond values-and-conditions.
`GetHostStatuses()` fresh, and the result rides `DriverHealthChanged` to
the AdminUI `/hosts` page (no SQL; the `DriverHostStatus` table was
dropped). See `docs/Telemetry.md`.
2. **`RawTagEntry.DeviceName` is accepted on the wire shape but ignored for
routing.** One driver instance owns exactly one Agent client scoped by
`MTConnectDriverOptions.DeviceName` (the top-level config key); the
per-tag `RawTagEntry.DeviceName` field the v3 raw-tag identity carries is
never read by the driver. Two `/raw` Devices authored under one MTConnect
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.
2. **One MTConnect driver instance = one Agent client = at most one `/raw`
Device (#628, decision (b)).** The driver never reads
`RawTagEntry.DeviceName`; every raw tag binds through the single Agent client
scoped by `MTConnectDriverOptions.DeviceName`. The v3 raw tree always has a
Device level (a new driver auto-creates `Device1`), and that single-Device
layout is the normal, valid shape — its `DeviceConfig` is merged up into the
driver config by `DriverDeviceConfigMerger` (this is how the endpoint lands).
With **two or more** Devices under one MTConnect driver there is no merge-up
and no routing, so which Device supplies the endpoint is ambiguous. That is
now a **deploy-time `DraftValidator` error** (`MTConnectMultipleDevices`),
and the AdminUI refuses to *create* the second Device
(`RawTreeService.CreateDeviceAsync`, same message): "MTConnect binds all tags
through one agent client per driver instance; author one MTConnect driver per
agent device." Option (a), a client per Device, was rejected as a design
change with no demand. A multi-device Agent is served by several driver
instances, each with its own `DeviceName` scope. The driver's init-time
Warning for differing `RawTagEntry.DeviceName` values stays as defence in
depth for configs that predate the rule.
**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
@@ -399,18 +436,13 @@ for anything beyond values-and-conditions.
So `MTConnectDriverOptions.DeviceName` does double duty — request scope for
reads/browse, fallback write target for writes.
**It is no longer silent (#613).** When the instance's raw tags carry two or
more distinct non-blank `RawTagEntry.DeviceName` values, driver init logs one
Warning naming them and the driver-level scope they collapse into. A single
value is not reported (it is just the `/raw` Device's own name). Real
per-device routing remains deferred.
3. **Authoring only *warns* when a declared coercion type cannot hold the
inferred one (#613).** The AdminUI tag editor (`MTConnectTagConfigModel.Warnings()`)
compares the declared type with `MTConnectDataTypeInference` over the
`/probe` metadata a browse-commit stamped (`mtCategory`/`mtType`), and shows an
advisory line — it never blocks a save (the deploy gate does not refuse it) and
is a no-op for hand-authored blobs with no stamped metadata. `representation`
is not on the model, so TIME_SERIES arrays are not covered. Beyond that,
is not on the model; TIME_SERIES arrays get their own `isArray` advisory (declare Float64). Beyond that,
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
@@ -427,9 +459,11 @@ for anything beyond values-and-conditions.
declare a `sampleCount`, is unrealistic on this point and exists only to
pin the parsing rule itself.
5. **CONDITION is a plain `String`, not a native Part 9 alarm** (see above).
6. **`TIME_SERIES` arrays and structured `DATA_SET`/`TABLE` observations
surface as `BadNotSupported`**, not an approximation — see the Quality
mapping table.
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
[TIME_SERIES arrays](#time_series-arrays-627)); the sample rate is not
surfaced on the node.
## Deferred (not in this build)
@@ -447,8 +481,7 @@ 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 OPC UA Part 9 alarms via `IAlarmSource` (the Galaxy
native-alarm pattern); `TIME_SERIES` SAMPLE arrays materialized as real
OPC UA arrays; EVENT controlled-vocabulary values → OPC UA enumerations.
native-alarm pattern); EVENT controlled-vocabulary values → OPC UA enumerations.
- **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