Phase 2 PR 12 — richer historian quality mapping. Replace MxAccessGalaxyBackend's inline MapHistorianQualityToOpcUa category-only helper (192+→Good, 64-191→Uncertain, 0-63→Bad) with a new public HistorianQualityMapper.Map utility that preserves specific OPC DA subcodes — BadNotConnected(8)→0x808A0000u instead of generic Bad(0x80000000u), UncertainSubNormal(88)→0x40950000u instead of generic Uncertain, Good_LocalOverride(216)→0x00D80000u instead of generic Good, etc. Mirrors v1 QualityMapper.MapToOpcUaStatusCode byte-for-byte without pulling in OPC UA types — the function returns uint32 literals that are the canonical OPC UA StatusCode wire encoding, surfaced directly as DataValueSnapshot.StatusCode on the Proxy side with no additional translation. Unknown subcodes fall back to the family category (255→Good, 150→Uncertain, 50→Bad) so a future SDK change that adds a quality code we don't map yet still gets a sensible bucket. GalaxyDataValue wire shape unchanged (StatusCode stays uint) — this is a pure fidelity upgrade on the Host side. Downstream callers (Admin UI status dashboard, OPC UA clients receiving historian samples) can now distinguish e.g. a transport outage (BadNotConnected) from a sensor fault (BadSensorFailure) from a warm-up delay (BadWaitingForInitialData) without a second round-trip or dashboard heuristic. 21 new tests (HistorianQualityMapperTests): theory with 15 rows covering every specific mapping from the v1 QualityMapper table, plus 6 fallback tests verifying unknown-subcode codes in each family (Good/Uncertain/Bad) collapse to the family default. Galaxy.Host.Tests Unit suite 56/0 (21 new + 35 existing). Galaxy.Host builds clean (0/0). Branches off v2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Backend.Historian;
|
||||
|
||||
/// <summary>
|
||||
/// Maps a raw OPC DA quality byte (as returned by Wonderware Historian's <c>OpcQuality</c>)
|
||||
/// to an OPC UA <c>StatusCode</c> uint. Preserves specific codes (BadNotConnected,
|
||||
/// UncertainSubNormal, etc.) instead of collapsing to Good/Uncertain/Bad categories.
|
||||
/// Mirrors v1 <c>QualityMapper.MapToOpcUaStatusCode</c> without pulling in OPC UA types —
|
||||
/// the returned value is the 32-bit OPC UA <c>StatusCode</c> wire encoding that the Proxy
|
||||
/// surfaces directly as <c>DataValueSnapshot.StatusCode</c>.
|
||||
/// </summary>
|
||||
public static class HistorianQualityMapper
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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,
|
||||
};
|
||||
}
|
||||
@@ -355,19 +355,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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps a <see cref="HistorianAggregateSample"/> (one aggregate bucket) to the IPC wire
|
||||
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user