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

104 lines
4.7 KiB
C#

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);
}
}
}