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 HSBY — WallClockTime.SyncStatus
/// (DINT-typed). Values: 0 = Standby, 1 = Synchronized / Active,
/// 2 = Disqualified. Other values map to .
/// - PLC-5 / SLC500 fallback — S: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;
}
}
}