Merge pull request 'Phase 2 PR 12 � richer historian quality mapping' (#11) from phase-2-pr12-quality-mapper into v2
This commit was merged in pull request #11.
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -431,19 +431,11 @@ public sealed class MxAccessGalaxyBackend : IGalaxyBackend, IDisposable
|
|||||||
TagReference = reference,
|
TagReference = reference,
|
||||||
ValueBytes = sample.Value is null ? null : MessagePackSerializer.Serialize(sample.Value),
|
ValueBytes = sample.Value is null ? null : MessagePackSerializer.Serialize(sample.Value),
|
||||||
ValueMessagePackType = 0,
|
ValueMessagePackType = 0,
|
||||||
StatusCode = MapHistorianQualityToOpcUa(sample.Quality),
|
StatusCode = HistorianQualityMapper.Map(sample.Quality),
|
||||||
SourceTimestampUtcUnixMs = new DateTimeOffset(sample.TimestampUtc, TimeSpan.Zero).ToUnixTimeMilliseconds(),
|
SourceTimestampUtcUnixMs = new DateTimeOffset(sample.TimestampUtc, TimeSpan.Zero).ToUnixTimeMilliseconds(),
|
||||||
ServerTimestampUtcUnixMs = DateTimeOffset.UtcNow.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>
|
/// <summary>
|
||||||
/// Maps a <see cref="HistorianAggregateSample"/> (one aggregate bucket) to the IPC wire
|
/// 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