Files
lmxopcua/src/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipHsbyRoleProber.cs
2026-04-26 07:51:44 -04:00

125 lines
5.9 KiB
C#

namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
/// <summary>
/// PR abcip-5.1 — resolved HSBY role for one chassis in a ControlLogix Hot-Standby pair.
/// <see cref="Unknown"/> 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.
/// </summary>
public enum HsbyRole
{
/// <summary>Read failed or value was not decodable. Surface as "no information".</summary>
Unknown = 0,
/// <summary>Chassis is the active member of the HSBY pair (Synchronized + serving I/O).</summary>
Active = 1,
/// <summary>Chassis is the standby member — Synchronized but not driving I/O.</summary>
Standby = 2,
/// <summary>Chassis has been disqualified by the HSBY module (e.g. firmware mismatch).</summary>
Disqualified = 3,
}
/// <summary>
/// PR abcip-5.1 — reads a ControlLogix HSBY role tag from one chassis and maps the value
/// to <see cref="HsbyRole"/>. Two address formats are supported:
/// <list type="bullet">
/// <item><b>v20 / v24 / v32+ ControlLogix HSBY</b> — <c>WallClockTime.SyncStatus</c>
/// (DINT-typed). Values: <c>0 = Standby</c>, <c>1 = Synchronized / Active</c>,
/// <c>2 = Disqualified</c>. Other values map to <see cref="HsbyRole.Unknown"/>.</item>
/// <item><b>PLC-5 / SLC500 fallback</b> — <c>S:34</c> 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 <c>"S:"</c> + maps
/// <c>(value &amp; 1) == 1 → Active</c>, otherwise → Standby.</item>
/// </list>
/// Read failure (initialise / read throw, non-zero libplctag status, undecodable buffer)
/// returns <see cref="HsbyRole.Unknown"/> — callers (the driver's HSBY probe loop)
/// interpret Unknown as "leave ActiveAddress alone for this tick".
/// </summary>
/// <remarks>
/// The prober is stateless / static — the per-chassis runtime is provided by
/// <see cref="AbCipDriver.ProbeLoopAsync"/> + drives initialise / read on the runtime
/// before delegating to <see cref="ProbeAsync"/>. 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.
/// </remarks>
public static class AbCipHsbyRoleProber
{
/// <summary>
/// Read <paramref name="roleTagAddress"/> on <paramref name="runtime"/> + map the
/// decoded value to a <see cref="HsbyRole"/>. The runtime is already initialised by
/// the caller (<see cref="AbCipDriver.ProbeLoopAsync"/> shares the same lazy-init
/// pattern with the regular probe loop); this method only issues the read + decodes.
/// </summary>
public static async Task<HsbyRole> 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;
}
}
/// <summary>
/// Pure value-to-role mapper. Exposed for unit tests so the matrix assertions can run
/// without a runtime in scope. <see cref="ProbeAsync"/> is the production entry point.
/// </summary>
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;
}
}
}