diff --git a/src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host/Backend/Historian/HistorianQualityMapper.cs b/src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host/Backend/Historian/HistorianQualityMapper.cs new file mode 100644 index 0000000..d84318a --- /dev/null +++ b/src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host/Backend/Historian/HistorianQualityMapper.cs @@ -0,0 +1,46 @@ +namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Backend.Historian; + +/// +/// Maps a raw OPC DA quality byte (as returned by Wonderware Historian's OpcQuality) +/// to an OPC UA StatusCode uint. Preserves specific codes (BadNotConnected, +/// UncertainSubNormal, etc.) instead of collapsing to Good/Uncertain/Bad categories. +/// Mirrors v1 QualityMapper.MapToOpcUaStatusCode without pulling in OPC UA types — +/// the returned value is the 32-bit OPC UA StatusCode wire encoding that the Proxy +/// surfaces directly as DataValueSnapshot.StatusCode. +/// +public static class HistorianQualityMapper +{ + /// + /// Map an 8-bit OPC DA quality byte to the corresponding OPC UA StatusCode. The byte + /// family bits decide the category (Good >= 192, Uncertain 64-191, Bad 0-63); the + /// low-nibble subcode selects the specific code. + /// + public static uint Map(byte q) => q switch + { + // Good family (192+) + 192 => 0x00000000u, // Good + 216 => 0x00D80000u, // Good_LocalOverride + + // Uncertain family (64-191) + 64 => 0x40000000u, // Uncertain + 68 => 0x40900000u, // Uncertain_LastUsableValue + 80 => 0x40930000u, // Uncertain_SensorNotAccurate + 84 => 0x40940000u, // Uncertain_EngineeringUnitsExceeded + 88 => 0x40950000u, // Uncertain_SubNormal + + // Bad family (0-63) + 0 => 0x80000000u, // Bad + 4 => 0x80890000u, // Bad_ConfigurationError + 8 => 0x808A0000u, // Bad_NotConnected + 12 => 0x808B0000u, // Bad_DeviceFailure + 16 => 0x808C0000u, // Bad_SensorFailure + 20 => 0x80050000u, // Bad_CommunicationError + 24 => 0x808D0000u, // Bad_OutOfService + 32 => 0x80320000u, // Bad_WaitingForInitialData + + // Unknown code — fall back to the category so callers still get a sensible bucket. + _ when q >= 192 => 0x00000000u, + _ when q >= 64 => 0x40000000u, + _ => 0x80000000u, + }; +} diff --git a/src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host/Backend/MxAccessGalaxyBackend.cs b/src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host/Backend/MxAccessGalaxyBackend.cs index 35cf733..a537263 100644 --- a/src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host/Backend/MxAccessGalaxyBackend.cs +++ b/src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host/Backend/MxAccessGalaxyBackend.cs @@ -431,19 +431,11 @@ public sealed class MxAccessGalaxyBackend : IGalaxyBackend, IDisposable TagReference = reference, ValueBytes = sample.Value is null ? null : MessagePackSerializer.Serialize(sample.Value), ValueMessagePackType = 0, - StatusCode = MapHistorianQualityToOpcUa(sample.Quality), + StatusCode = HistorianQualityMapper.Map(sample.Quality), SourceTimestampUtcUnixMs = new DateTimeOffset(sample.TimestampUtc, TimeSpan.Zero).ToUnixTimeMilliseconds(), ServerTimestampUtcUnixMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), }; - private static uint MapHistorianQualityToOpcUa(byte q) - { - // Category-only mapping — mirrors QualityMapper.MapToOpcUaStatusCode for the common ranges. - // The Proxy may refine this when it decodes the wire frame. - if (q >= 192) return 0x00000000u; // Good - if (q >= 64) return 0x40000000u; // Uncertain - return 0x80000000u; // Bad - } /// /// Maps a (one aggregate bucket) to the IPC wire diff --git a/tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Tests/HistorianQualityMapperTests.cs b/tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Tests/HistorianQualityMapperTests.cs new file mode 100644 index 0000000..8e75348 --- /dev/null +++ b/tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Tests/HistorianQualityMapperTests.cs @@ -0,0 +1,61 @@ +using Shouldly; +using Xunit; +using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Backend.Historian; + +namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Tests; + +[Trait("Category", "Unit")] +public sealed class HistorianQualityMapperTests +{ + /// + /// Rich mapping preserves specific OPC DA subcodes through the historian ToWire path. + /// Before PR 12 the category-only fallback collapsed e.g. BadNotConnected(8) to + /// Bad(0x80000000) so downstream OPC UA clients could not distinguish transport issues + /// from sensor issues. After PR 12 every known subcode round-trips to its canonical + /// uint32 StatusCode and Proxy translation stays byte-for-byte with v1 QualityMapper. + /// + [Theory] + [InlineData((byte)192, 0x00000000u)] // Good + [InlineData((byte)216, 0x00D80000u)] // Good_LocalOverride + [InlineData((byte)64, 0x40000000u)] // Uncertain + [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 + [InlineData((byte)12, 0x808B0000u)] // Bad_DeviceFailure + [InlineData((byte)16, 0x808C0000u)] // Bad_SensorFailure + [InlineData((byte)20, 0x80050000u)] // Bad_CommunicationError + [InlineData((byte)24, 0x808D0000u)] // Bad_OutOfService + [InlineData((byte)32, 0x80320000u)] // Bad_WaitingForInitialData + public void Maps_specific_OPC_DA_codes_to_canonical_StatusCode(byte quality, uint expected) + { + HistorianQualityMapper.Map(quality).ShouldBe(expected); + } + + [Theory] + [InlineData((byte)200)] // Good — unknown subcode in Good family + [InlineData((byte)255)] // Good — unknown + public void Unknown_good_family_codes_fall_back_to_plain_Good(byte q) + { + HistorianQualityMapper.Map(q).ShouldBe(0x00000000u); + } + + [Theory] + [InlineData((byte)100)] // Uncertain — unknown subcode + [InlineData((byte)150)] // Uncertain — unknown + public void Unknown_uncertain_family_codes_fall_back_to_plain_Uncertain(byte q) + { + HistorianQualityMapper.Map(q).ShouldBe(0x40000000u); + } + + [Theory] + [InlineData((byte)1)] // Bad — unknown subcode + [InlineData((byte)50)] // Bad — unknown + public void Unknown_bad_family_codes_fall_back_to_plain_Bad(byte q) + { + HistorianQualityMapper.Map(q).ShouldBe(0x80000000u); + } +}