namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip; /// /// PR abcip-5.1 — resolved HSBY role for one chassis in a ControlLogix Hot-Standby pair. /// covers "couldn't read the role tag" (transport failure, tag not /// found, decode failure); the driver treats it as "no information yet, don't change /// ActiveAddress" rather than as a vote for Standby. /// public enum HsbyRole { /// Read failed or value was not decodable. Surface as "no information". Unknown = 0, /// Chassis is the active member of the HSBY pair (Synchronized + serving I/O). Active = 1, /// Chassis is the standby member — Synchronized but not driving I/O. Standby = 2, /// Chassis has been disqualified by the HSBY module (e.g. firmware mismatch). Disqualified = 3, } /// /// PR abcip-5.1 — reads a ControlLogix HSBY role tag from one chassis and maps the value /// to . Two address formats are supported: /// /// v20 / v24 / v32+ ControlLogix HSBYWallClockTime.SyncStatus /// (DINT-typed). Values: 0 = Standby, 1 = Synchronized / Active, /// 2 = Disqualified. Other values map to . /// PLC-5 / SLC500 fallbackS:34 Module Status word. Bit 0 of the /// integer value indicates "this chassis is Active"; the prober applies the /// bit-mask interpretation when the address starts with "S:" + maps /// (value & 1) == 1 → Active, otherwise → Standby. /// /// Read failure (initialise / read throw, non-zero libplctag status, undecodable buffer) /// returns — callers (the driver's HSBY probe loop) /// interpret Unknown as "leave ActiveAddress alone for this tick". /// /// /// The prober is stateless / static — the per-chassis runtime is provided by /// + drives initialise / read on the runtime /// before delegating to . Keeping the value-mapping logic isolated /// here lets unit tests assert the matrix (0 / 1 / 2 / S:34 bit 0 / unknown values) without /// standing up a probe loop. /// public static class AbCipHsbyRoleProber { /// /// Read on + map the /// decoded value to a . The runtime is already initialised by /// the caller ( shares the same lazy-init /// pattern with the regular probe loop); this method only issues the read + decodes. /// public static async Task ProbeAsync( IAbCipTagRuntime runtime, string roleTagAddress, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(runtime); ArgumentException.ThrowIfNullOrWhiteSpace(roleTagAddress); try { await runtime.ReadAsync(cancellationToken).ConfigureAwait(false); if (runtime.GetStatus() != 0) return HsbyRole.Unknown; var raw = runtime.DecodeValue(AbCipDataType.DInt, bitIndex: null); return MapValueToRole(raw, roleTagAddress); } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { throw; } catch { // Wire / init / decode failure — surface as Unknown so the caller doesn't // misinterpret a transient transport hiccup as "this chassis went Standby". return HsbyRole.Unknown; } } /// /// Pure value-to-role mapper. Exposed for unit tests so the matrix assertions can run /// without a runtime in scope. is the production entry point. /// public static HsbyRole MapValueToRole(object? raw, string roleTagAddress) { if (raw is null) return HsbyRole.Unknown; if (!TryToInt64(raw, out var value)) return HsbyRole.Unknown; // PLC-5 / SLC500 status-file fallback — bit 0 of S:34 is the role bit. Pattern-match // on the "S:" prefix because operators do put the file number after it (S:34, S:2, // etc) + the role bit lives in S:34 specifically on PLC-5 fronts but the bit-mask // semantics apply to any S:NN address an integration plumbs in. if (roleTagAddress.StartsWith("S:", StringComparison.OrdinalIgnoreCase)) return (value & 1) == 1 ? HsbyRole.Active : HsbyRole.Standby; // Default — WallClockTime.SyncStatus matrix (v20 / v24 / v32+ ControlLogix HSBY). return value switch { 0 => HsbyRole.Standby, 1 => HsbyRole.Active, 2 => HsbyRole.Disqualified, _ => HsbyRole.Unknown, }; } private static bool TryToInt64(object raw, out long value) { switch (raw) { case long l: value = l; return true; case int i: value = i; return true; case short s: value = s; return true; case sbyte sb: value = sb; return true; case byte b: value = b; return true; case ushort us: value = us; return true; case uint ui: value = ui; return true; case ulong ul when ul <= long.MaxValue: value = (long)ul; return true; case bool boolean: value = boolean ? 1 : 0; return true; case string str when long.TryParse(str, System.Globalization.NumberStyles.Integer, System.Globalization.CultureInfo.InvariantCulture, out var parsed): value = parsed; return true; default: value = 0; return false; } } }