Phase 2 PR 12 � richer historian quality mapping #11
@@ -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