StatusCode is not a .NET type reference in this assembly — replace the unresolvable <see cref="StatusCode"/> with prose text so TreatWarningsAsErrors does not fail the build on the CS1574 unresolved-cref warning. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
141 lines
6.8 KiB
C#
141 lines
6.8 KiB
C#
using Microsoft.Extensions.Logging;
|
|
using MxGateway.Client;
|
|
using MxGateway.Contracts.Proto;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Runtime;
|
|
|
|
/// <summary>
|
|
/// Maps the gateway's <see cref="MxStatusProxy"/> (raw MXAccess HRESULT + category bits)
|
|
/// to OPC UA <c>StatusCode</c> uints. Replaces the legacy
|
|
/// <c>MxAccessGalaxyBackend.ToWire</c> heuristic (Quality >= 192 → Good, else Uncertain)
|
|
/// with an explicit table that preserves specific codes (BadNotConnected, OutOfService,
|
|
/// UncertainSubNormal, etc.) instead of collapsing to category buckets.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// OPC DA quality bytes are 16-bit values arranged as <c>[QQSSSSSSLLNNNN]</c>:
|
|
/// Q = quality category (Bad/Uncertain/Good = 0/1/3), S = substatus, L = limit, N = vendor.
|
|
/// This mapper consumes the LOW byte (where the Q+S bits live) — the same byte the legacy
|
|
/// Wonderware Historian SDK exposed as the raw quality byte. Category-only fallback paths
|
|
/// handle deployment versions of MXAccess that surface unfamiliar substatuses.
|
|
///
|
|
/// Unknown substatus values fall back to the matching category bucket (<c>Good</c>,
|
|
/// <c>Uncertain</c>, <c>Bad</c>) and emit a single diagnostic log line per session via
|
|
/// the supplied logger so field captures can extend the table.
|
|
/// </remarks>
|
|
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.
|
|
|
|
public const uint Good = 0x00000000u;
|
|
public const uint GoodLocalOverride = 0x00D80000u;
|
|
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 Bad = 0x80000000u;
|
|
public const uint BadConfigurationError = 0x80890000u;
|
|
public const uint BadNotConnected = 0x808A0000u;
|
|
public const uint BadDeviceFailure = 0x808B0000u;
|
|
public const uint BadSensorFailure = 0x808C0000u;
|
|
public const uint BadCommunicationError = 0x80050000u;
|
|
public const uint BadOutOfService = 0x808D0000u;
|
|
public const uint BadWaitingForInitialData = 0x80320000u;
|
|
public const uint BadInternalError = 0x80020000u;
|
|
|
|
/// <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
|
|
/// low byte) to the OPC UA StatusCode uint.
|
|
/// </summary>
|
|
public static uint FromQualityByte(byte q, ILogger? logger = null) => q switch
|
|
{
|
|
// Good family — top two bits 11b (192-255).
|
|
192 => Good,
|
|
216 => GoodLocalOverride,
|
|
|
|
// Uncertain family — top two bits 01b (64-127).
|
|
64 => Uncertain,
|
|
68 => UncertainLastUsableValue,
|
|
80 => UncertainSensorNotAccurate,
|
|
84 => UncertainEngineeringUnitsExceeded,
|
|
88 => UncertainSubNormal,
|
|
|
|
// Bad family — top two bits 00b (0-63).
|
|
0 => Bad,
|
|
4 => BadConfigurationError,
|
|
8 => BadNotConnected,
|
|
12 => BadDeviceFailure,
|
|
16 => BadSensorFailure,
|
|
20 => BadCommunicationError,
|
|
24 => BadOutOfService,
|
|
32 => BadWaitingForInitialData,
|
|
|
|
_ => Categorize(q, logger),
|
|
};
|
|
|
|
/// <summary>
|
|
/// Map a gateway-reported <see cref="MxStatusProxy"/> to OPC UA StatusCode. Uses
|
|
/// <see cref="MxStatusProxyExtensions.IsSuccess"/> (category == OK AND success != 0)
|
|
/// as the canonical success test — the proto contract explicitly documents that
|
|
/// <c>success</c> is NOT a boolean and must not be checked in isolation; category is
|
|
/// the authoritative indicator. On failure, the detail byte (OPC DA quality substatus)
|
|
/// drives the specific code, with a transport-error fallback for pre-MXAccess failures.
|
|
/// </summary>
|
|
public static uint FromMxStatus(MxStatusProxy? status, ILogger? logger = null)
|
|
{
|
|
if (status is null) return Good;
|
|
if (status.IsSuccess()) return Good;
|
|
|
|
// Detail field carries the substatus when the worker translated MX-style codes;
|
|
// when zero, infer from detected_by.
|
|
var detail = (byte)(status.Detail & 0xFF);
|
|
if (detail != 0) return FromQualityByte(detail, logger);
|
|
|
|
// detected_by != Mxaccess (raw_detected_by != the MXAccess source enum) implies
|
|
// the failure happened pre-call (gateway, worker, transport) — surface as a
|
|
// communication error rather than a generic Bad.
|
|
if (status.RawDetectedBy != 0) return BadCommunicationError;
|
|
|
|
return Bad;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Convert an OPC UA status-code uint back to the OPC DA quality category
|
|
/// byte — Good=192, Uncertain=64, Bad=0 — by extracting the top-two bits of the
|
|
/// high word. This is the inverse of the category-bucket arm of
|
|
/// <see cref="FromQualityByte"/>. It is intentionally lossy (substatus bits are not
|
|
/// round-tripped) because the sole consumer
|
|
/// (<see cref="ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Health.PerPlatformProbeWatcher"/>)
|
|
/// only tests <c>qualityByte < 192</c> to distinguish Running from Stopped. Keeping
|
|
/// the round-trip in one place means a future change to the OPC UA bit layout cannot
|
|
/// silently desync the probe-health decode.
|
|
/// </summary>
|
|
public static byte ToQualityCategoryByte(uint statusCode) =>
|
|
(byte)(((statusCode >> 30) & 0x3u) switch
|
|
{
|
|
0u => 192u, // Good — top two bits 00b → OPC DA 0xC0
|
|
1u => 64u, // Uncertain — top two bits 01b → OPC DA 0x40
|
|
_ => 0u, // Bad — top two bits 10b/11b → OPC DA 0x00
|
|
});
|
|
|
|
private static uint Categorize(byte q, ILogger? logger)
|
|
{
|
|
if (q >= 192) { Log(logger, q, "Good"); return Good; }
|
|
if (q >= 64) { Log(logger, q, "Uncertain"); return Uncertain; }
|
|
Log(logger, q, "Bad");
|
|
return Bad;
|
|
}
|
|
|
|
private static void Log(ILogger? logger, byte q, string bucket)
|
|
{
|
|
// Best-effort diagnostic so field captures can extend the table — once per bucket
|
|
// per session is plenty (the LogWarning level is rate-limited by Serilog filters
|
|
// in production).
|
|
logger?.LogWarning(
|
|
"Unrecognised MXAccess quality byte 0x{Q:X2} — falling back to {Bucket} category. " +
|
|
"Field capture welcome — extend StatusCodeMap.FromQualityByte.", q, bucket);
|
|
}
|
|
}
|