From fb04b54d62a8e9884d81a7c8d11582bb46eee607 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Tue, 10 Nov 2026 04:48:33 -0500 Subject: [PATCH] fix(dcl): map OPC UA status by severity bits so all Good subcodes are Good (ScadaBridge#117) MapStatusCode returned Uncertain for any non-zero non-Bad code, so GoodLocalOverride, GoodClamped and info-bit Good variants degraded to Uncertain. Now top two bits decide: 00 Good, 01 Uncertain, 10/11 Bad (BadResourceUnavailable stays Bad, #112 untouched). The SDK IsGood/IsUncertain(uint) helpers were tried first and failed the subcode tests, so an explicit mask is used. MxGateway mapper uses the OPC-DA quality byte (>=192 Good) and is not affected. Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV --- .../Adapters/OpcUaDataConnection.cs | 19 ++++++++++----- .../OpcUaStatusCodeMappingTests.cs | 24 +++++++++++++++++++ 2 files changed, 37 insertions(+), 6 deletions(-) create mode 100644 tests/ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests/OpcUaStatusCodeMappingTests.cs diff --git a/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/OpcUaDataConnection.cs b/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/OpcUaDataConnection.cs index 4252a900..3f927c84 100644 --- a/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/OpcUaDataConnection.cs +++ b/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/OpcUaDataConnection.cs @@ -528,13 +528,20 @@ public class OpcUaDataConnection } /// - /// Maps OPC UA StatusCode to QualityCode. - /// StatusCode 0 = Good, high bit set = Bad, otherwise Uncertain. + /// Maps an OPC UA StatusCode to QualityCode by its severity bits (top two bits: 00 Good, + /// 01 Uncertain, 10/11 Bad), so every Good subcode (GoodLocalOverride, GoodClamped, info-bit + /// variants, ...) is Good (ScadaBridge#117). BadResourceUnavailable is a plain Bad here + /// (ScadaBridge#112 handles it separately and never as a connection fault). /// - private static QualityCode MapStatusCode(uint statusCode) + internal static QualityCode MapStatusCode(uint statusCode) { - if (statusCode == 0) return QualityCode.Good; - if ((statusCode & 0x80000000) != 0) return QualityCode.Bad; - return QualityCode.Uncertain; + // Explicit severity mask: the SDK's StatusCode.IsGood/IsUncertain(uint) helpers did not + // treat Good subcodes / info-bit variants as Good in the pinned SDK (tests prove it). + return (statusCode & 0xC0000000u) switch + { + 0x00000000u => QualityCode.Good, + 0x40000000u => QualityCode.Uncertain, + _ => QualityCode.Bad, + }; } } diff --git a/tests/ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests/OpcUaStatusCodeMappingTests.cs b/tests/ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests/OpcUaStatusCodeMappingTests.cs new file mode 100644 index 00000000..f669c0b5 --- /dev/null +++ b/tests/ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests/OpcUaStatusCodeMappingTests.cs @@ -0,0 +1,24 @@ +using Opc.Ua; +using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Protocol; +using ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Adapters; + +namespace ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests; + +/// ScadaBridge#117: quality follows the StatusCode severity bits, not "== 0". +public class OpcUaStatusCodeMappingTests +{ + [Theory] + [InlineData(StatusCodes.Good, QualityCode.Good)] + [InlineData(StatusCodes.GoodLocalOverride, QualityCode.Good)] + [InlineData(StatusCodes.GoodClamped, QualityCode.Good)] + [InlineData(StatusCodes.Good | 0x0480u, QualityCode.Good)] // info bits set + [InlineData(StatusCodes.Uncertain, QualityCode.Uncertain)] + [InlineData(StatusCodes.UncertainLastUsableValue, QualityCode.Uncertain)] + [InlineData(StatusCodes.UncertainSensorNotAccurate, QualityCode.Uncertain)] + [InlineData(StatusCodes.Bad, QualityCode.Bad)] + [InlineData(StatusCodes.BadNodeIdUnknown, QualityCode.Bad)] + [InlineData(StatusCodes.BadResourceUnavailable, QualityCode.Bad)] + [InlineData(0xC0000000u, QualityCode.Bad)] + public void MapStatusCode_UsesSeverityBits(uint code, QualityCode expected) => + Assert.Equal(expected, OpcUaDataConnection.MapStatusCode(code)); +}