Auto: abcip-5.1 — HSBY paired-IP role probing

Closes #242
This commit is contained in:
Joseph Doherty
2026-04-26 07:51:44 -04:00
parent 349aa5c6f4
commit 561b0f9ea9
12 changed files with 1260 additions and 9 deletions

View File

@@ -40,6 +40,18 @@ public abstract class AbCipCommandBase : DriverCommandBase
"walk; unsupported on Micro800 (silent fallback to Symbolic with warning).")]
public AddressingMode AddressingMode { get; init; } = AddressingMode.Auto;
/// <summary>
/// PR abcip-5.1 — partner gateway URI for HSBY (Hot-Standby) paired chassis. When
/// supplied, every CLI command auto-enables HSBY role probing on the device options
/// so subcommands like <c>hsby-status</c> + diagnostics surface the active chassis
/// without extra flags. Unset for non-redundant deployments.
/// </summary>
[CommandOption("partner", Description =
"Partner gateway URI for ControlLogix HSBY pair (e.g. ab://10.0.0.6/1,0). When " +
"set, the driver runs a second role-probe loop and the hsby-status command can " +
"surface which chassis is currently Active. Optional.")]
public string? Partner { get; init; }
/// <inheritdoc />
public override TimeSpan Timeout
{
@@ -58,7 +70,17 @@ public abstract class AbCipCommandBase : DriverCommandBase
HostAddress: Gateway,
PlcFamily: Family,
DeviceName: $"cli-{Family}",
AddressingMode: AddressingMode)],
AddressingMode: AddressingMode,
// PR abcip-5.1 — surface --partner through the device options so commands that
// use BuildOptions can take advantage of HSBY role probing without subclassing.
// Hsby auto-enables only when a partner was actually supplied; pre-5.1 invocations
// (no --partner) see exactly the legacy options shape.
PartnerHostAddress: Partner,
Hsby: string.IsNullOrWhiteSpace(Partner) ? null : new AbCipHsbyOptions
{
Enabled = true,
ProbeInterval = TimeSpan.FromSeconds(2),
})],
Tags = tags,
Timeout = Timeout,
Probe = new AbCipProbeOptions { Enabled = false },

View File

@@ -0,0 +1,103 @@
using CliFx.Attributes;
using CliFx.Infrastructure;
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Cli.Commands;
/// <summary>
/// PR abcip-5.1 — print the current HSBY role on each chassis of a paired ControlLogix
/// ControlLogix Hot-Standby setup. Requires <c>--partner</c> on the base command +
/// reads <c>WallClockTime.SyncStatus</c> on both gateways once before printing.
/// </summary>
[Command("hsby-status", Description =
"Read the WallClockTime.SyncStatus role tag on a ControlLogix HSBY pair and print " +
"which chassis is currently Active. Requires --partner.")]
public sealed class HsbyStatusCommand : AbCipCommandBase
{
[CommandOption("role-tag", Description =
"Role-tag address. Default WallClockTime.SyncStatus matches v20+ ControlLogix HSBY; " +
"use S:34 for legacy SLC500 / PLC-5 status-byte fronts.")]
public string RoleTagAddress { get; init; } = "WallClockTime.SyncStatus";
[CommandOption("samples", Description =
"Number of role-probe ticks to wait for before printing (default 3). Larger values " +
"give the role-prober loop more chances to sample both chassis through transient " +
"transport hiccups.")]
public int Samples { get; init; } = 3;
public override async ValueTask ExecuteAsync(IConsole console)
{
ConfigureLogging();
var ct = console.RegisterCancellationHandler();
if (string.IsNullOrWhiteSpace(Partner))
{
await console.Error.WriteLineAsync(
"hsby-status requires --partner <ab://gateway/cip-path>. Without a partner the " +
"command has no second chassis to compare roles against.");
return;
}
// Override the base BuildOptions so we can pin the role-tag address + a tight probe
// interval — the default 2 s would mean Samples * 2 s before the print fires, too slow
// for an interactive CLI. Tag list stays empty; only the role probe runs.
var options = new AbCipDriverOptions
{
Devices = [new AbCipDeviceOptions(
HostAddress: Gateway,
PlcFamily: Family,
DeviceName: $"cli-{Family}",
AddressingMode: AddressingMode,
PartnerHostAddress: Partner,
Hsby: new AbCipHsbyOptions
{
Enabled = true,
RoleTagAddress = RoleTagAddress,
ProbeInterval = TimeSpan.FromMilliseconds(500),
})],
Tags = [],
Timeout = Timeout,
Probe = new AbCipProbeOptions { Enabled = false },
EnableControllerBrowse = false,
EnableAlarmProjection = false,
};
await using var driver = new AbCipDriver(options, DriverInstanceId);
try
{
await driver.InitializeAsync("{}", ct);
// Wait Samples * ProbeInterval so the role probe has had time to sample each
// chassis at least <Samples> times. The role probe loop spins inside the driver;
// we just sleep + read GetDeviceState's ActiveAddress.
await Task.Delay(TimeSpan.FromMilliseconds(500 * Math.Max(1, Samples)), ct);
// Pull HSBY state out via DriverHealth.Diagnostics. Single-pair config emits
// the flat AbCip.HsbyActive / AbCip.HsbyPrimaryRole / AbCip.HsbyPartnerRole keys.
var diag = driver.GetHealth().Diagnostics
?? new Dictionary<string, double>();
var primaryRole = diag.TryGetValue("AbCip.HsbyPrimaryRole", out var pr)
? (HsbyRole)(int)pr : HsbyRole.Unknown;
var partnerRole = diag.TryGetValue("AbCip.HsbyPartnerRole", out var qr)
? (HsbyRole)(int)qr : HsbyRole.Unknown;
var activeCode = diag.TryGetValue("AbCip.HsbyActive", out var ac) ? (int)ac : 0;
var activeAddress = activeCode switch
{
1 => Gateway,
2 => Partner,
_ => null,
};
await console.Output.WriteLineAsync($"Primary: {Gateway}");
await console.Output.WriteLineAsync($"Partner: {Partner}");
await console.Output.WriteLineAsync($"Role tag: {RoleTagAddress}");
await console.Output.WriteLineAsync();
await console.Output.WriteLineAsync($"Primary role: {primaryRole}");
await console.Output.WriteLineAsync($"Partner role: {partnerRole}");
await console.Output.WriteLineAsync($"Active chassis: {activeAddress ?? "<none>"}");
}
finally
{
await driver.ShutdownAsync(CancellationToken.None);
}
}
}