using Microsoft.Extensions.Logging; using ZB.MOM.WW.MxGateway.Client; using ZB.MOM.WW.MxGateway.Contracts.Proto; namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Runtime; /// /// Maps the gateway's (raw MXAccess HRESULT + category bits) /// to OPC UA StatusCode uints. Replaces the legacy /// MxAccessGalaxyBackend.ToWire heuristic (Quality >= 192 → Good, else Uncertain) /// with an explicit table that preserves specific codes (BadNotConnected, OutOfService, /// UncertainSubNormal, etc.) instead of collapsing to category buckets. /// /// /// OPC DA quality bytes are 16-bit values arranged as [QQSSSSSSLLNNNN]: /// 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 (Good, /// Uncertain, Bad) and emit a single diagnostic log line per session via /// the supplied logger so field captures can extend the table. /// 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; /// /// Map a raw OPC DA quality byte (the low byte of an OPC DA OpcQuality ushort, /// which is what Wonderware Historian + MXAccess surface as OPCITEMSTATE.qLong's /// low byte) to the OPC UA StatusCode uint. /// 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), }; /// /// Map a gateway-reported to OPC UA StatusCode. Uses /// (category == OK AND success != 0) /// as the canonical success test — the proto contract explicitly documents that /// success 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. /// 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; } /// /// 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 /// . It is intentionally lossy (substatus bits are not /// round-tripped) because the sole consumer /// () /// only tests qualityByte < 192 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. /// 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); } }