125 lines
5.9 KiB
C#
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 & 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;
|
|
}
|
|
}
|
|
}
|