From c14a3388f1d83b2e2c3fee52603b6533fa7e7e8d Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 22 Oct 2026 15:35:55 -0400 Subject: [PATCH] fix(mqtt): Sparkplug array and unsupported-type metrics are not bindable from the browser (R06:M-5) The ingestor refuses array metrics and unmapped types, yet DescribeMetric still stated their binding tuple, so a browse-commit produced a tag stuck at BadWaitingForInitialData forever. Those leaves now state no address (the commit refuses them) and say why in the descriptor line. Refs #744 Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT --- docs/drivers/Mqtt.md | 5 ++++- .../MqttBrowseSession.cs | 13 +++++++++++-- .../MqttBrowseSessionTests.cs | 8 ++++++++ 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/docs/drivers/Mqtt.md b/docs/drivers/Mqtt.md index d8620d65..b9e62582 100644 --- a/docs/drivers/Mqtt.md +++ b/docs/drivers/Mqtt.md @@ -249,7 +249,10 @@ One subscription (`spBv1.0/{GroupId}/#`) feeds a per-edge-node state machine [Structured metrics](#structured-metrics-template--dataset--propertyset). - **Unsupported datatypes are skipped with a warning, never guessed**: `PropertySetList`, `Unknown`, extension values, and any `Template`/`DataSet` that cannot be flattened honestly. `Int8`→`Int16` and - `UInt8`→`UInt16` widen; `*Array` variants map to the element type with the array bit set. + `UInt8`→`UInt16` widen; `*Array` variants map to the element type with the array bit set — but + **array metrics are not read in v1** (warned once, never fed), so the browse picker offers them, and any + metric whose type is unsupported, **without an address**: the browse-commit refuses them rather than + committing a tag that waits for initial data forever (R06:M-5). - **Ingest state is reset at every session/authoring seam** — a reconnect or a `ReinitializeAsync` must not carry a previous session's alias table into a new one. diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Mqtt.Browser/MqttBrowseSession.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Mqtt.Browser/MqttBrowseSession.cs index 9d4f6772..b6bf8d9c 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Mqtt.Browser/MqttBrowseSession.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Mqtt.Browser/MqttBrowseSession.cs @@ -441,16 +441,25 @@ internal sealed class MqttBrowseSession : IRebirthCapableBrowseSession var mapped = metric.DataType?.ToDriverDataType(); var sparkplugName = metric.DataType?.ToString() ?? nameof(SparkplugDataType.Unknown); + var isArray = metric.DataType?.IsSparkplugArray() ?? false; + var descriptor = new StringBuilder(metric.BirthType.ToString()).Append(" · ").Append(sparkplugName); if (metric.Alias is { } alias) descriptor.Append(" · alias ").Append(alias); if (mapped is null) descriptor.Append(" · unsupported in v1 — not bindable"); + else if (isArray) descriptor.Append(" · array metrics are not read in v1 — not bindable"); + + // Unbindable = the ingestor refuses it: an unmapped type, or ANY array variant (SparkplugIngestor + // gates on the array bit and never feeds the tag). Such a leaf states NO address, so the + // browse-commit refuses it at commit instead of writing a tag that sits at + // BadWaitingForInitialData forever (R06:M-5). Labelling it alone was not enough — it still bound. + var bindable = mapped is not null && !isArray; return new AttributeInfo( metric.Name, mapped?.ToString() ?? sparkplugName, - metric.DataType?.IsSparkplugArray() ?? false, + isArray, descriptor.ToString(), - AddressFields: BuildMetricAddressFields(metric.Name, scope)); + AddressFields: bindable ? BuildMetricAddressFields(metric.Name, scope) : null); } /// diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Mqtt.Tests/MqttBrowseSessionTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Mqtt.Tests/MqttBrowseSessionTests.cs index bbc14bc1..fd624dde 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Mqtt.Tests/MqttBrowseSessionTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Mqtt.Tests/MqttBrowseSessionTests.cs @@ -779,6 +779,13 @@ public sealed class MqttBrowseSessionTests "Plant1/EdgeA::Profile", TestContext.Current.CancellationToken)).ShouldHaveSingleItem(); a.DriverDataType.ShouldBe(nameof(Core.Abstractions.DriverDataType.Int32)); // element type a.IsArray.ShouldBeTrue(); + + // R06:M-5: the ingestor refuses array metrics ("not supported in v1"), so the browser must not + // offer one as bindable — no stated address means the browse-commit refuses it in words, instead + // of committing a tag that sits at BadWaitingForInitialData forever. The scalar control is + // SparkplugAttributes_StateTheBindingTuple_SoTheCommitNeverParsesTheNodeId. + a.AddressFields.ShouldBeNull(); + a.SecurityClass.ShouldContain("not bindable"); } [Fact] @@ -796,6 +803,7 @@ public sealed class MqttBrowseSessionTests Enum.TryParse(a.DriverDataType, ignoreCase: true, out _) .ShouldBeFalse(); // not a guessed type — unmappable, and visibly so a.SecurityClass.ShouldContain("unsupported"); + a.AddressFields.ShouldBeNull(); // R06:M-5 — labelled AND unbindable, not just labelled } [Fact]