From b95b99ba8e80e059c75119609c8b1a382a7e4bd2 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sat, 25 Jul 2026 15:34:31 -0400 Subject: [PATCH] fix(drivers): correct 16 wrong OPC UA status-code constants + guard them by reflection (#497) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every value verified against Opc.Ua.StatusCodes in the pinned SDK (1.5.378.106) by reflection, not by copying siblings. All are client-visible: OPC UA clients branch on status. The three named in #497: - Historian.Gateway SampleMapper "BadNoData" 0x800E0000 -> 0x809B0000 (0x800E0000 is BadServerHalted) - Galaxy "BadTimeout" 0x800B0000 -> 0x800A0000 (0x800B0000 is BadServiceUnsupported) - TwinCAT BadTypeMismatch 0x80730000 -> 0x80740000 (0x80730000 is BadWriteNotSupported) BadTypeMismatch was wrong in three MORE drivers the issue did not name — FOCAS, AbLegacy and AbCip carried the same 0x80730000. Every call site is a FormatException/InvalidCastException conversion failure, so the name was right and the value was wrong in all four. Adding the reflection guard then surfaced nine further defects offline tests had never checked: - Galaxy GoodLocalOverride 0x00D80000 -> 0x00960000 (not a UA code at all) - Galaxy UncertainLastUsableValue 0x40A40000 -> 0x40900000 - Galaxy UncertainSensorNotAccurate 0x408D0000 -> 0x40930000 - Galaxy UncertainEngineeringUnitsExceeded 0x408E0000 -> 0x40940000 - Galaxy UncertainSubNormal 0x408F0000 -> 0x40950000 - TwinCAT BadOutOfService 0x80BE0000 -> 0x808D0000 (was BadProtocolVersionUnsupported) - TwinCAT BadInvalidState 0x80350000 -> 0x80AF0000 (was BadAttributeIdInvalid) - AbCip/AbLegacy GoodMoreData 0x00A70000 -> 0x00A60000 (was GoodCommunicationEvent) - Historian.Gateway GatewayQualityMapper Good_LocalOverride 0x00D80000 -> 0x00960000 Galaxy's whole Uncertain block read as though the OPC DA quality byte could be shifted into the UA substatus position; it cannot — the substatuses are an unrelated enumeration. GatewayQualityMapper already held the correct table, which is what the Galaxy values are now reconciled against. Guard: StatusCodeParityTests (Core.Abstractions.Tests, which already project- references every driver) reflects over every `const uint` in the deployed ZB.MOM.WW.OtOpcUa.Driver.*.dll whose name reads as a status code, and asserts it equals Opc.Ua.StatusCodes.. Discovery is by convention, so a new driver assembly referenced by that project is covered with no edit here. It went red on all nine defects above before the fixes and now checks 109 constants green. Because reflection can only see a NAMED constant, two inline literals were hoisted so the guard can reach them: Galaxy's BadTimeout/Bad into StatusCodeMap, and GatewayQualityMapper's 15-entry table into a new HistorianStatusCodes. An inline `0x800B0000u, // BadTimeout` at a call site is exactly how the Galaxy defect survived. The drivers stay SDK-free by design; only the test project references Opc.Ua.Core, as the oracle. Also corrected: tests that pinned the wrong values (Galaxy StatusCodeMapTests, SampleMapperTests, GatewayQualityMapperTests — all written from the same bad source), a mislabelled comment in DriverInstanceActorTests, and stale constants in two design docs, one of them the unexecuted MTConnect driver design that would have propagated BadNoData's wrong value into a new driver. The CLI's SnapshotFormatter value->name table was checked against the SDK and is correct; no change needed. --- Directory.Packages.props | 4 + ...6-otopcua-historian-gateway-integration.md | 6 +- .../2026-07-15-mtconnect-driver-design.md | 2 +- .../AbCipStatusMapper.cs | 4 +- .../AbLegacyStatusMapper.cs | 4 +- .../FocasStatusMapper.cs | 2 +- .../GalaxyDriver.cs | 4 +- .../Runtime/StatusCodeMap.cs | 27 ++- .../Mapping/GatewayQualityMapper.cs | 36 ++-- .../Mapping/HistorianStatusCodes.cs | 75 ++++++++ .../Mapping/SampleMapper.cs | 4 +- .../TwinCATStatusMapper.cs | 6 +- .../StatusCodeParityTests.cs | 160 ++++++++++++++++++ ....WW.OtOpcUa.Core.Abstractions.Tests.csproj | 6 + .../Runtime/StatusCodeMapTests.cs | 14 +- .../Mapping/GatewayQualityMapperTests.cs | 2 +- .../Mapping/SampleMapperTests.cs | 2 +- .../Drivers/DriverInstanceActorTests.cs | 2 +- 18 files changed, 311 insertions(+), 49 deletions(-) create mode 100644 src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/Mapping/HistorianStatusCodes.cs create mode 100644 tests/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests/StatusCodeParityTests.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index a30e3e2d..87785628 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -77,6 +77,10 @@ + + + all @@ -35,6 +37,10 @@ + + diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests/Runtime/StatusCodeMapTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests/Runtime/StatusCodeMapTests.cs index c048431c..3ec18e17 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests/Runtime/StatusCodeMapTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests/Runtime/StatusCodeMapTests.cs @@ -19,12 +19,12 @@ public sealed class StatusCodeMapTests /// The expected OPC UA status code. [Theory] [InlineData((byte)192, 0x00000000u)] // Good - [InlineData((byte)216, 0x00D80000u)] // Good_LocalOverride + [InlineData((byte)216, 0x00960000u)] // Good_LocalOverride [InlineData((byte)64, 0x40000000u)] // Uncertain - [InlineData((byte)68, 0x40A40000u)] // Uncertain_LastUsableValue - [InlineData((byte)80, 0x408D0000u)] // Uncertain_SensorNotAccurate - [InlineData((byte)84, 0x408E0000u)] // Uncertain_EngineeringUnitsExceeded - [InlineData((byte)88, 0x408F0000u)] // Uncertain_SubNormal + [InlineData((byte)68, 0x40900000u)] // Uncertain_LastUsableValue + [InlineData((byte)80, 0x40930000u)] // Uncertain_SensorNotAccurate + [InlineData((byte)84, 0x40940000u)] // Uncertain_EngineeringUnitsExceeded + [InlineData((byte)88, 0x40950000u)] // Uncertain_SubNormal [InlineData((byte)0, 0x80000000u)] // Bad [InlineData((byte)4, 0x80890000u)] // Bad_ConfigurationError [InlineData((byte)8, 0x808A0000u)] // Bad_NotConnected @@ -132,9 +132,9 @@ public sealed class StatusCodeMapTests /// The expected OPC DA category byte. [Theory] [InlineData(0x00000000u, (byte)192)] // Good - [InlineData(0x00D80000u, (byte)192)] // GoodLocalOverride — still Good category + [InlineData(0x00960000u, (byte)192)] // GoodLocalOverride — still Good category [InlineData(0x40000000u, (byte)64)] // Uncertain - [InlineData(0x408F0000u, (byte)64)] // UncertainSubNormal — still Uncertain category + [InlineData(0x40950000u, (byte)64)] // UncertainSubNormal — still Uncertain category [InlineData(0x80000000u, (byte)0)] // Bad [InlineData(0x808A0000u, (byte)0)] // BadNotConnected — still Bad category [InlineData(0x80020000u, (byte)0)] // BadInternalError — still Bad category diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/Mapping/GatewayQualityMapperTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/Mapping/GatewayQualityMapperTests.cs index 15c7fced..c2cba1f6 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/Mapping/GatewayQualityMapperTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/Mapping/GatewayQualityMapperTests.cs @@ -7,7 +7,7 @@ public sealed class GatewayQualityMapperTests { [Theory] [InlineData(192, 0x00000000u)] // Good - [InlineData(216, 0x00D80000u)] // Good_LocalOverride + [InlineData(216, 0x00960000u)] // Good_LocalOverride [InlineData(64, 0x40000000u)] // Uncertain [InlineData(0, 0x80000000u)] // Bad [InlineData(8, 0x808A0000u)] // Bad_NotConnected diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/Mapping/SampleMapperTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/Mapping/SampleMapperTests.cs index 969b9764..29092ae4 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/Mapping/SampleMapperTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/Mapping/SampleMapperTests.cs @@ -37,7 +37,7 @@ public sealed class SampleMapperTests { var a = new HistorianAggregateSample { Tag = "T", /* Value unset, no Good quality */ EndTime = Ts(2026, 1, 1, 0, 0, 0) }; var snap = SampleMapper.ToAggregateSnapshot(a); - Assert.Equal(0x800E0000u, snap.StatusCode); // BadNoData + Assert.Equal(0x809B0000u, snap.StatusCode); // BadNoData Assert.Null(snap.Value); } diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DriverInstanceActorTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DriverInstanceActorTests.cs index 381756e3..59794e08 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DriverInstanceActorTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DriverInstanceActorTests.cs @@ -88,7 +88,7 @@ public sealed class DriverInstanceActorTests : RuntimeActorTestBase [Fact] public async Task Write_propagates_status_code_on_Bad_result() { - const uint badStatus = 0x80340000; // BadOutOfService — top severity bits = 10b + const uint badStatus = 0x80340000; // BadNodeIdUnknown — top severity bits = 10b var driver = new WritableStubDriver { NextStatusCode = badStatus }; var actor = Sys.ActorOf(DriverInstanceActor.Props(driver));