Skip to content

feat: always carry the canonical componentPath in the update extra - #7

Merged
mbreissi merged 1 commit into
mainfrom
feat/componentpath-extra
Jul 28, 2026
Merged

feat: always carry the canonical componentPath in the update extra#7
mbreissi merged 1 commit into
mainfrom
feat/componentpath-extra

Conversation

@mbreissi

Copy link
Copy Markdown
Contributor

Every published SouthboundSignalUpdate now carries the signal's full, untruncated MTConnect component path as componentPath on the update-level extra map — unconditionally, for a truncated derived channel, an untruncated one and an explicitly configured signal alike. One consistent place, no conditional branch in reader code.

Placement — verified against the core, not assumed

Checked against proto/edgecommons/v1/telemetry.proto and the Rust codec at the pinned rev (a14a328):

  • SouthboundSignalUpdate declares map<string, EcValue> extra = 100.
  • to_telemetry collects every body key that is not signal/samples into that map (copy_extra(obj, &["signal", "samples"])); from_telemetry puts them back. So the update-level extra genuinely round-trips — it is what already carries the device block today.
  • componentPath collides with nothing: the seven reserved sample keys are value, quality, qualityRaw, sourceTs, serverTs, sourceTsMs, serverTsMs; the update level reserves only signal and samples. No fallback to sourcePath was needed.
  • EcValue has a NullValue arm, so a JSON null survives the codec — which is what makes unconditional presence possible in every case.

tests/component_path.rs asserts this on the decoded body after a real Message::to_vec()Message::from_slice() round-trip, not on the JSON.

The probe-absent edge case

An explicit signals[] entry whose dataItemId is not in the probe model is the permanent-BAD case (MTC_NO_SUCH_DATAITEM). sb/signals does not serve nothing for it — unlearned_address serves address.componentPath: null. So the update stamps null and the key is still present. There is no exception: the field is on every update, always.

Three values, all of them present:

Value When
"Axes[Axes]/Linear[X]" The component chain, slash-joined, exactly as the probe declares it.
"" A device-level data item that hangs off no component (Lavail on the live demo Okuma).
null No device model describes the signal — the same null sb/signals reports.

One source of truth

ProbeModel::component_path_of is the only formatter. address_of (what sb/signals and sb/browse serve) now reads it too, so the update and the control plane cannot drift apart. Reading.component_path carries it through the seam; the derived channel may be shortened to the UNS topic budget (L12), this never is.

No core change

The SignalUpdate builder has no update-level extra setter, so supervisor.rs uses the facade's own two-step form rather than hand-building anything:

DataFacade::build_body (the whole SOUTHBOUND §2.1 contract — quality defaulting, the qualityRaw marker, the serverTs fill, the samples wrapper) ▸ app::stamp_component_path (one additive key) ▸ DataFacade::publish_body_via (mints data/{channel} from the same effective_signal_path, stamps identity, honours the per-call channel override). Both structural checks publish() makes are re-made here rather than dropped.

Live evidence

demo.mtconnect.org was up; the real probe parsed through this adapter's own ProbeModel:

LX1actm     componentPath="Axes[Axes]/Linear[X]"
Lpexecution componentPath="Controller[Controller]"
Lavail      componentPath=""            <- the device-level empty-path case, on a real machine

The README's captured Okuma envelope now shows the field with that verified value.

Tests & gates

  • cargo test390 passing, 0 failing (308 unit + 82 integration), of which 12 new: 7 unit in app.rs (unconditional presence, exact key, no reserved-name collision, "", null, truncated/untruncated/explicit parity, idempotent stamping), 4 seam tests in device.rs (value equals what sb/signals serves, device-level "", derived-signal untruncated path, probe-absent → None), plus 5 in the new tests/component_path.rs (protobuf round-trip at the update level, deep path, ""/null, batched window carries it once, immediate/flush paths keep it) and extended assertions in the deep-path poll_acquisition e2e and the live agent_integration case.
  • cargo clippy --all-targets -- -D warnings — clean.
  • cargo llvm-cov --fail-under-lines 9096.68% lines (97.16% regions), gate 90.

Docs

  • docs/reference/messaging-interface.md — new componentPath section with the value table, the "always present, no exception" statement, and the update-vs-channel distinction; the body example is now MTConnect-shaped.
  • docs/explanation.md — why presence is unconditional rather than truncation-conditional.
  • DESIGN.mdD-MtconnectAdapter-L13 records the user decision and the two rejected alternatives: (a) truncation-conditional stamping (conditional reader code, presence becomes a function of the topic budget) and (b) promoting the path to a first-class address block (a four-language wire-contract change for one protocol's shape; reconsider only if a second deep-path protocol appears). L12's "nothing is lost" bullet updated.

Every published SouthboundSignalUpdate now carries the signal's full,
untruncated MTConnect component path as `componentPath` on the update-level
extra map — unconditionally, for a truncated derived channel, an untruncated
one and an explicitly configured signal alike, so an MTConnect-aware consumer
reads one field with no branch.

* Placement is the update level, not the sample: the path is per-signal-static
  and one update is one signal's readings, so a batched window carries exactly
  one of them. The library's SouthboundSignalUpdate reserves only `signal` and
  `samples` there and round-trips every other body key through the protobuf
  `extra` map, so this is a first-class round-trip; `componentPath` collides
  with none of the seven reserved sample keys.
* One source of truth for the value: ProbeModel::component_path_of, which
  address_of (what sb/signals and sb/browse serve) now also reads. The update
  and the control plane cannot drift apart.
* Three values, all present: the slash-joined path; "" for a device-level data
  item; null for a signal no device model describes — the permanent-BAD case,
  where sb/signals reports address.componentPath: null and the update says the
  same. The key is never omitted.
* No core change. The SignalUpdate builder has no update-level extra setter, so
  the supervisor uses the facade's own two-step form: build_body applies the
  whole SOUTHBOUND §2.1 contract, stamp_component_path adds the one additive
  key, publish_body_via mints the topic and stamps identity. Both structural
  checks publish() makes are re-made rather than dropped.

Recorded as D-MtconnectAdapter-L13, with the two rejected alternatives:
truncation-conditional stamping (conditional reader code, inconsistent
presence) and promoting the path to a first-class address block (a
four-language wire-contract change for one protocol's shape).
@mbreissi
mbreissi merged commit e6928b7 into main Jul 28, 2026
4 checks passed
@mbreissi
mbreissi deleted the feat/componentpath-extra branch July 28, 2026 19:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant