Add cross-platform OPC UA client stack: shared library, CLI tool, and Avalonia UI

Implements Client.Shared (IOpcUaClientService with connection lifecycle, failover,
browse, read/write, subscriptions, alarms, history, redundancy), Client.CLI (8 CliFx
commands mirroring tools/opcuacli-dotnet), and Client.UI (Avalonia desktop app with
tree browser, read/write, subscriptions, alarms, and history tabs). All three target
.NET 10 and are covered by 249 unit tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-03-30 15:49:42 -04:00
parent 50b85d41bd
commit a2883b82d9
109 changed files with 8571 additions and 0 deletions

View File

@@ -0,0 +1,62 @@
using CliFx.Attributes;
using CliFx.Infrastructure;
using ZB.MOM.WW.LmxOpcUa.Client.CLI.Helpers;
using ZB.MOM.WW.LmxOpcUa.Client.Shared;
namespace ZB.MOM.WW.LmxOpcUa.Client.CLI.Commands;
[Command("subscribe", Description = "Monitor a node for value changes")]
public class SubscribeCommand : CommandBase
{
[CommandOption("node", 'n', Description = "Node ID to monitor", IsRequired = true)]
public string NodeId { get; init; } = default!;
[CommandOption("interval", 'i', Description = "Sampling interval in milliseconds")]
public int Interval { get; init; } = 1000;
public SubscribeCommand(IOpcUaClientServiceFactory factory) : base(factory) { }
public override async ValueTask ExecuteAsync(IConsole console)
{
ConfigureLogging();
IOpcUaClientService? service = null;
try
{
var ct = console.RegisterCancellationHandler();
(service, _) = await CreateServiceAndConnectAsync(ct);
var nodeId = NodeIdParser.ParseRequired(NodeId);
service.DataChanged += (_, e) =>
{
console.Output.WriteLine(
$"[{e.Value.SourceTimestamp:O}] {e.NodeId} = {e.Value.Value} ({e.Value.StatusCode})");
};
await service.SubscribeAsync(nodeId, Interval, ct);
await console.Output.WriteLineAsync(
$"Subscribed to {NodeId} (interval: {Interval}ms). Press Ctrl+C to stop.");
// Wait until cancellation
try
{
await Task.Delay(Timeout.Infinite, ct);
}
catch (OperationCanceledException)
{
// Expected on Ctrl+C
}
await service.UnsubscribeAsync(nodeId, default);
await console.Output.WriteLineAsync("Unsubscribed.");
}
finally
{
if (service != null)
{
await service.DisconnectAsync();
service.Dispose();
}
}
}
}