fix(drivers): correct 16 wrong OPC UA status-code constants + guard them by reflection (#497)
Every value verified against Opc.Ua.StatusCodes in the pinned SDK (1.5.378.106) by reflection, not by copying siblings. All are client-visible: OPC UA clients branch on status. The three named in #497: - Historian.Gateway SampleMapper "BadNoData" 0x800E0000 -> 0x809B0000 (0x800E0000 is BadServerHalted) - Galaxy "BadTimeout" 0x800B0000 -> 0x800A0000 (0x800B0000 is BadServiceUnsupported) - TwinCAT BadTypeMismatch 0x80730000 -> 0x80740000 (0x80730000 is BadWriteNotSupported) BadTypeMismatch was wrong in three MORE drivers the issue did not name — FOCAS, AbLegacy and AbCip carried the same 0x80730000. Every call site is a FormatException/InvalidCastException conversion failure, so the name was right and the value was wrong in all four. Adding the reflection guard then surfaced nine further defects offline tests had never checked: - Galaxy GoodLocalOverride 0x00D80000 -> 0x00960000 (not a UA code at all) - Galaxy UncertainLastUsableValue 0x40A40000 -> 0x40900000 - Galaxy UncertainSensorNotAccurate 0x408D0000 -> 0x40930000 - Galaxy UncertainEngineeringUnitsExceeded 0x408E0000 -> 0x40940000 - Galaxy UncertainSubNormal 0x408F0000 -> 0x40950000 - TwinCAT BadOutOfService 0x80BE0000 -> 0x808D0000 (was BadProtocolVersionUnsupported) - TwinCAT BadInvalidState 0x80350000 -> 0x80AF0000 (was BadAttributeIdInvalid) - AbCip/AbLegacy GoodMoreData 0x00A70000 -> 0x00A60000 (was GoodCommunicationEvent) - Historian.Gateway GatewayQualityMapper Good_LocalOverride 0x00D80000 -> 0x00960000 Galaxy's whole Uncertain block read as though the OPC DA quality byte could be shifted into the UA substatus position; it cannot — the substatuses are an unrelated enumeration. GatewayQualityMapper already held the correct table, which is what the Galaxy values are now reconciled against. Guard: StatusCodeParityTests (Core.Abstractions.Tests, which already project- references every driver) reflects over every `const uint` in the deployed ZB.MOM.WW.OtOpcUa.Driver.*.dll whose name reads as a status code, and asserts it equals Opc.Ua.StatusCodes.<name>. Discovery is by convention, so a new driver assembly referenced by that project is covered with no edit here. It went red on all nine defects above before the fixes and now checks 109 constants green. Because reflection can only see a NAMED constant, two inline literals were hoisted so the guard can reach them: Galaxy's BadTimeout/Bad into StatusCodeMap, and GatewayQualityMapper's 15-entry table into a new HistorianStatusCodes. An inline `0x800B0000u, // BadTimeout` at a call site is exactly how the Galaxy defect survived. The drivers stay SDK-free by design; only the test project references Opc.Ua.Core, as the oracle. Also corrected: tests that pinned the wrong values (Galaxy StatusCodeMapTests, SampleMapperTests, GatewayQualityMapperTests — all written from the same bad source), a mislabelled comment in DriverInstanceActorTests, and stale constants in two design docs, one of them the unexecuted MTConnect driver design that would have propagated BadNoData's wrong value into a new driver. The CLI's SnapshotFormatter value->name table was checked against the SDK and is correct; no change needed.
This commit is contained in:
@@ -35,7 +35,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
|
||||
public static class AbCipStatusMapper
|
||||
{
|
||||
public const uint Good = 0u;
|
||||
public const uint GoodMoreData = 0x00A70000u;
|
||||
public const uint GoodMoreData = 0x00A60000u;
|
||||
public const uint BadInternalError = 0x80020000u;
|
||||
public const uint BadNodeIdUnknown = 0x80340000u;
|
||||
public const uint BadNotWritable = 0x803B0000u;
|
||||
@@ -44,7 +44,7 @@ public static class AbCipStatusMapper
|
||||
public const uint BadDeviceFailure = 0x808B0000u;
|
||||
public const uint BadCommunicationError = 0x80050000u;
|
||||
public const uint BadTimeout = 0x800A0000u;
|
||||
public const uint BadTypeMismatch = 0x80730000u;
|
||||
public const uint BadTypeMismatch = 0x80740000u;
|
||||
|
||||
/// <summary>Map a CIP general-status byte to an OPC UA StatusCode.</summary>
|
||||
/// <param name="status">The CIP general-status byte value.</param>
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy;
|
||||
public static class AbLegacyStatusMapper
|
||||
{
|
||||
public const uint Good = 0u;
|
||||
public const uint GoodMoreData = 0x00A70000u;
|
||||
public const uint GoodMoreData = 0x00A60000u;
|
||||
public const uint BadInternalError = 0x80020000u;
|
||||
public const uint BadNodeIdUnknown = 0x80340000u;
|
||||
public const uint BadNotWritable = 0x803B0000u;
|
||||
@@ -19,7 +19,7 @@ public static class AbLegacyStatusMapper
|
||||
public const uint BadDeviceFailure = 0x808B0000u;
|
||||
public const uint BadCommunicationError = 0x80050000u;
|
||||
public const uint BadTimeout = 0x800A0000u;
|
||||
public const uint BadTypeMismatch = 0x80730000u;
|
||||
public const uint BadTypeMismatch = 0x80740000u;
|
||||
|
||||
/// <summary>
|
||||
/// Map a libplctag return/status code to an OPC UA StatusCode. The integer passed here
|
||||
|
||||
@@ -17,7 +17,7 @@ public static class FocasStatusMapper
|
||||
public const uint BadDeviceFailure = 0x808B0000u;
|
||||
public const uint BadCommunicationError = 0x80050000u;
|
||||
public const uint BadTimeout = 0x800A0000u;
|
||||
public const uint BadTypeMismatch = 0x80730000u;
|
||||
public const uint BadTypeMismatch = 0x80740000u;
|
||||
|
||||
/// <summary>
|
||||
/// Map common FWLIB <c>EW_*</c> return codes. The values below match Fanuc's published
|
||||
|
||||
@@ -856,7 +856,7 @@ public sealed class GalaxyDriver
|
||||
{
|
||||
rejectedTcs.TrySetResult(new DataValueSnapshot(
|
||||
Value: null,
|
||||
StatusCode: 0x80000000u, // Bad
|
||||
StatusCode: StatusCodeMap.Bad,
|
||||
SourceTimestampUtc: null,
|
||||
ServerTimestampUtc: DateTime.UtcNow));
|
||||
}
|
||||
@@ -875,7 +875,7 @@ public sealed class GalaxyDriver
|
||||
{
|
||||
tcs.TrySetResult(new DataValueSnapshot(
|
||||
Value: null,
|
||||
StatusCode: 0x800B0000u, // BadTimeout
|
||||
StatusCode: StatusCodeMap.BadTimeout,
|
||||
SourceTimestampUtc: null,
|
||||
ServerTimestampUtc: DateTime.UtcNow));
|
||||
}
|
||||
|
||||
@@ -26,14 +26,23 @@ internal static class StatusCodeMap
|
||||
{
|
||||
// OPC UA Part 4 standard StatusCodes — top-byte categories are 0x00 (Good),
|
||||
// 0x40 (Uncertain), 0x80 (Bad). Specific codes layer onto the category byte.
|
||||
//
|
||||
// The substatus nibbles are NOT derivable from the OPC DA quality byte this mapper consumes —
|
||||
// they are an unrelated OPC UA enumeration and have to be looked up. Five constants here were
|
||||
// originally written as though the DA byte could be shifted into the substatus position, which
|
||||
// produced values naming entirely different UA codes (Gitea #497): UncertainLastUsableValue held
|
||||
// UncertainDataSubNormal's value, UncertainSubNormal held UncertainNoCommunicationLastUsableValue's,
|
||||
// and GoodLocalOverride / UncertainSensorNotAccurate / UncertainEngineeringUnitsExceeded held values
|
||||
// that are not OPC UA status codes at all. StatusCodeParityTests now checks every constant below
|
||||
// against the pinned SDK's Opc.Ua.StatusCodes.
|
||||
|
||||
public const uint Good = 0x00000000u;
|
||||
public const uint GoodLocalOverride = 0x00D80000u;
|
||||
public const uint GoodLocalOverride = 0x00960000u;
|
||||
public const uint Uncertain = 0x40000000u;
|
||||
public const uint UncertainLastUsableValue = 0x40A40000u;
|
||||
public const uint UncertainSensorNotAccurate = 0x408D0000u;
|
||||
public const uint UncertainEngineeringUnitsExceeded = 0x408E0000u;
|
||||
public const uint UncertainSubNormal = 0x408F0000u;
|
||||
public const uint UncertainLastUsableValue = 0x40900000u;
|
||||
public const uint UncertainSensorNotAccurate = 0x40930000u;
|
||||
public const uint UncertainEngineeringUnitsExceeded = 0x40940000u;
|
||||
public const uint UncertainSubNormal = 0x40950000u;
|
||||
public const uint Bad = 0x80000000u;
|
||||
public const uint BadConfigurationError = 0x80890000u;
|
||||
public const uint BadNotConnected = 0x808A0000u;
|
||||
@@ -44,6 +53,14 @@ internal static class StatusCodeMap
|
||||
public const uint BadWaitingForInitialData = 0x80320000u;
|
||||
public const uint BadInternalError = 0x80020000u;
|
||||
|
||||
/// <summary>
|
||||
/// Fills a still-pending read when the caller's token fires before the gateway answers. Named here
|
||||
/// rather than written inline at the call site so <c>StatusCodeParityTests</c> can reflect over it —
|
||||
/// an inline literal is invisible to that guard, which is exactly how this constant spent its life
|
||||
/// as <c>0x800B0000</c> (<c>BadServiceUnsupported</c>) under a <c>// BadTimeout</c> comment.
|
||||
/// </summary>
|
||||
public const uint BadTimeout = 0x800A0000u;
|
||||
|
||||
/// <summary>
|
||||
/// Map a raw OPC DA quality byte (the low byte of an OPC DA <c>OpcQuality</c> ushort,
|
||||
/// which is what Wonderware Historian + MXAccess surface as <c>OPCITEMSTATE.qLong</c>'s
|
||||
|
||||
+18
-18
@@ -18,29 +18,29 @@ internal static class GatewayQualityMapper
|
||||
public static uint Map(byte q) => q switch
|
||||
{
|
||||
// Good family (192+)
|
||||
192 => 0x00000000u, // Good
|
||||
216 => 0x00D80000u, // Good_LocalOverride
|
||||
192 => HistorianStatusCodes.Good,
|
||||
216 => HistorianStatusCodes.GoodLocalOverride,
|
||||
|
||||
// Uncertain family (64-191)
|
||||
64 => 0x40000000u, // Uncertain
|
||||
68 => 0x40900000u, // Uncertain_LastUsableValue
|
||||
80 => 0x40930000u, // Uncertain_SensorNotAccurate
|
||||
84 => 0x40940000u, // Uncertain_EngineeringUnitsExceeded
|
||||
88 => 0x40950000u, // Uncertain_SubNormal
|
||||
64 => HistorianStatusCodes.Uncertain,
|
||||
68 => HistorianStatusCodes.UncertainLastUsableValue,
|
||||
80 => HistorianStatusCodes.UncertainSensorNotAccurate,
|
||||
84 => HistorianStatusCodes.UncertainEngineeringUnitsExceeded,
|
||||
88 => HistorianStatusCodes.UncertainSubNormal,
|
||||
|
||||
// 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
|
||||
0 => HistorianStatusCodes.Bad,
|
||||
4 => HistorianStatusCodes.BadConfigurationError,
|
||||
8 => HistorianStatusCodes.BadNotConnected,
|
||||
12 => HistorianStatusCodes.BadDeviceFailure,
|
||||
16 => HistorianStatusCodes.BadSensorFailure,
|
||||
20 => HistorianStatusCodes.BadCommunicationError,
|
||||
24 => HistorianStatusCodes.BadOutOfService,
|
||||
32 => HistorianStatusCodes.BadWaitingForInitialData,
|
||||
|
||||
// Unknown — fall back to category bucket so callers still get something usable.
|
||||
_ when q >= 192 => 0x00000000u,
|
||||
_ when q >= 64 => 0x40000000u,
|
||||
_ => 0x80000000u,
|
||||
_ when q >= 192 => HistorianStatusCodes.Good,
|
||||
_ when q >= 64 => HistorianStatusCodes.Uncertain,
|
||||
_ => HistorianStatusCodes.Bad,
|
||||
};
|
||||
}
|
||||
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Mapping;
|
||||
|
||||
/// <summary>
|
||||
/// The OPC UA status codes this driver publishes, as named constants.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>The driver layer is deliberately free of an OPC UA SDK reference, so status codes are spelled
|
||||
/// as bare <c>uint</c>s. The cost of that is a value nobody checks against the name it is written
|
||||
/// under — the defect class Gitea #497 found in six places across four drivers, one of them the
|
||||
/// <c>BadNoData</c> in <see cref="SampleMapper"/> that was really <c>BadServerHalted</c>.</para>
|
||||
/// <para><b>Named, not inline, on purpose.</b> <c>StatusCodeParityTests</c> guards these by reflecting
|
||||
/// over <c>const uint</c> fields and comparing each against <c>Opc.Ua.StatusCodes</c> in the pinned SDK.
|
||||
/// A literal written at a call site is invisible to that guard, which is exactly how
|
||||
/// <see cref="GatewayQualityMapper"/> kept an incorrect <c>Good_LocalOverride</c> through every test it
|
||||
/// had. Add a constant here rather than a literal in a <c>switch</c> arm.</para>
|
||||
/// </remarks>
|
||||
internal static class HistorianStatusCodes
|
||||
{
|
||||
// ---- Good family ----
|
||||
|
||||
/// <summary>The value is good; no qualification.</summary>
|
||||
public const uint Good = 0x00000000u;
|
||||
|
||||
/// <summary>The value has been overridden locally (OPC DA quality 216).</summary>
|
||||
public const uint GoodLocalOverride = 0x00960000u;
|
||||
|
||||
// ---- Uncertain family ----
|
||||
|
||||
/// <summary>The value is uncertain; no specific reason.</summary>
|
||||
public const uint Uncertain = 0x40000000u;
|
||||
|
||||
/// <summary>Communication has failed; the last known value is returned (OPC DA quality 68).</summary>
|
||||
public const uint UncertainLastUsableValue = 0x40900000u;
|
||||
|
||||
/// <summary>The sensor is known not to be accurate (OPC DA quality 80).</summary>
|
||||
public const uint UncertainSensorNotAccurate = 0x40930000u;
|
||||
|
||||
/// <summary>The value is outside the engineering-unit range for the sensor (OPC DA quality 84).</summary>
|
||||
public const uint UncertainEngineeringUnitsExceeded = 0x40940000u;
|
||||
|
||||
/// <summary>The value is derived from fewer sources than required (OPC DA quality 88).</summary>
|
||||
public const uint UncertainSubNormal = 0x40950000u;
|
||||
|
||||
// ---- Bad family ----
|
||||
|
||||
/// <summary>The value is bad; no specific reason.</summary>
|
||||
public const uint Bad = 0x80000000u;
|
||||
|
||||
/// <summary>A configuration problem prevents the value being produced (OPC DA quality 4).</summary>
|
||||
public const uint BadConfigurationError = 0x80890000u;
|
||||
|
||||
/// <summary>The source is not connected (OPC DA quality 8).</summary>
|
||||
public const uint BadNotConnected = 0x808A0000u;
|
||||
|
||||
/// <summary>The device reported a failure (OPC DA quality 12).</summary>
|
||||
public const uint BadDeviceFailure = 0x808B0000u;
|
||||
|
||||
/// <summary>The sensor reported a failure (OPC DA quality 16).</summary>
|
||||
public const uint BadSensorFailure = 0x808C0000u;
|
||||
|
||||
/// <summary>Communication with the source failed (OPC DA quality 20).</summary>
|
||||
public const uint BadCommunicationError = 0x80050000u;
|
||||
|
||||
/// <summary>The source is out of service (OPC DA quality 24).</summary>
|
||||
public const uint BadOutOfService = 0x808D0000u;
|
||||
|
||||
/// <summary>No initial value has arrived from the source yet (OPC DA quality 32).</summary>
|
||||
public const uint BadWaitingForInitialData = 0x80320000u;
|
||||
|
||||
/// <summary>
|
||||
/// The historian returned no data for the requested tag/interval. Distinct from a transport
|
||||
/// failure: the query succeeded and the answer was empty.
|
||||
/// </summary>
|
||||
public const uint BadNoData = 0x809B0000u;
|
||||
}
|
||||
@@ -10,8 +10,8 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Mapping;
|
||||
/// </summary>
|
||||
internal static class SampleMapper
|
||||
{
|
||||
private const uint StatusGood = 0x00000000u;
|
||||
private const uint StatusBadNoData = 0x800E0000u;
|
||||
private const uint StatusGood = HistorianStatusCodes.Good;
|
||||
private const uint StatusBadNoData = HistorianStatusCodes.BadNoData;
|
||||
|
||||
/// <summary>OPC DA "Good" family floor — a quality byte at/above this carries usable data.</summary>
|
||||
private const byte GoodQualityFloor = 192;
|
||||
|
||||
@@ -19,9 +19,9 @@ public static class TwinCATStatusMapper
|
||||
public const uint BadDeviceFailure = 0x808B0000u;
|
||||
public const uint BadCommunicationError = 0x80050000u;
|
||||
public const uint BadTimeout = 0x800A0000u;
|
||||
public const uint BadTypeMismatch = 0x80730000u;
|
||||
public const uint BadOutOfService = 0x80BE0000u;
|
||||
public const uint BadInvalidState = 0x80350000u;
|
||||
public const uint BadTypeMismatch = 0x80740000u;
|
||||
public const uint BadOutOfService = 0x808D0000u;
|
||||
public const uint BadInvalidState = 0x80AF0000u;
|
||||
|
||||
// ---- AdsErrorCode numeric values (confirmed from Beckhoff.TwinCAT.Ads 7.0.172) ----
|
||||
|
||||
|
||||
Reference in New Issue
Block a user