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:
@@ -0,0 +1,27 @@
|
||||
using Opc.Ua;
|
||||
using ZB.MOM.WW.LmxOpcUa.Client.Shared.Models;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Client.Shared.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Maps the library's AggregateType enum to OPC UA aggregate function NodeIds.
|
||||
/// </summary>
|
||||
public static class AggregateTypeMapper
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns the OPC UA NodeId for the specified aggregate type.
|
||||
/// </summary>
|
||||
public static NodeId ToNodeId(AggregateType aggregate)
|
||||
{
|
||||
return aggregate switch
|
||||
{
|
||||
AggregateType.Average => ObjectIds.AggregateFunction_Average,
|
||||
AggregateType.Minimum => ObjectIds.AggregateFunction_Minimum,
|
||||
AggregateType.Maximum => ObjectIds.AggregateFunction_Maximum,
|
||||
AggregateType.Count => ObjectIds.AggregateFunction_Count,
|
||||
AggregateType.Start => ObjectIds.AggregateFunction_Start,
|
||||
AggregateType.End => ObjectIds.AggregateFunction_End,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(aggregate), aggregate, "Unknown AggregateType value.")
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Client.Shared.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Parses and normalizes failover URL sets for redundant OPC UA connections.
|
||||
/// </summary>
|
||||
public static class FailoverUrlParser
|
||||
{
|
||||
/// <summary>
|
||||
/// Parses a comma-separated failover URL string, prepending the primary URL.
|
||||
/// Trims whitespace and deduplicates.
|
||||
/// </summary>
|
||||
/// <param name="primaryUrl">The primary endpoint URL.</param>
|
||||
/// <param name="failoverCsv">Optional comma-separated failover URLs.</param>
|
||||
/// <returns>An array with the primary URL first, followed by unique failover URLs.</returns>
|
||||
public static string[] Parse(string primaryUrl, string? failoverCsv)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(failoverCsv))
|
||||
return new[] { primaryUrl };
|
||||
|
||||
var urls = new List<string> { primaryUrl };
|
||||
foreach (var url in failoverCsv.Split(',', StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
var trimmed = url.Trim();
|
||||
if (!string.IsNullOrEmpty(trimmed) && !urls.Contains(trimmed, StringComparer.OrdinalIgnoreCase))
|
||||
urls.Add(trimmed);
|
||||
}
|
||||
return urls.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a failover URL set from the primary URL and an optional array of failover URLs.
|
||||
/// </summary>
|
||||
/// <param name="primaryUrl">The primary endpoint URL.</param>
|
||||
/// <param name="failoverUrls">Optional failover URLs.</param>
|
||||
/// <returns>An array with the primary URL first, followed by unique failover URLs.</returns>
|
||||
public static string[] Parse(string primaryUrl, string[]? failoverUrls)
|
||||
{
|
||||
if (failoverUrls == null || failoverUrls.Length == 0)
|
||||
return new[] { primaryUrl };
|
||||
|
||||
var urls = new List<string> { primaryUrl };
|
||||
foreach (var url in failoverUrls)
|
||||
{
|
||||
var trimmed = url?.Trim();
|
||||
if (!string.IsNullOrEmpty(trimmed) && !urls.Contains(trimmed, StringComparer.OrdinalIgnoreCase))
|
||||
urls.Add(trimmed);
|
||||
}
|
||||
return urls.ToArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using Opc.Ua;
|
||||
using ZB.MOM.WW.LmxOpcUa.Client.Shared.Models;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Client.Shared.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Maps between the library's SecurityMode enum and OPC UA SDK MessageSecurityMode.
|
||||
/// </summary>
|
||||
public static class SecurityModeMapper
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts a <see cref="SecurityMode"/> to an OPC UA <see cref="MessageSecurityMode"/>.
|
||||
/// </summary>
|
||||
public static MessageSecurityMode ToMessageSecurityMode(SecurityMode mode)
|
||||
{
|
||||
return mode switch
|
||||
{
|
||||
SecurityMode.None => MessageSecurityMode.None,
|
||||
SecurityMode.Sign => MessageSecurityMode.Sign,
|
||||
SecurityMode.SignAndEncrypt => MessageSecurityMode.SignAndEncrypt,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(mode), mode, "Unknown SecurityMode value.")
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a string to a <see cref="SecurityMode"/> value, case-insensitively.
|
||||
/// </summary>
|
||||
/// <param name="value">The string to parse (e.g., "none", "sign", "encrypt", "signandencrypt").</param>
|
||||
/// <returns>The corresponding SecurityMode.</returns>
|
||||
/// <exception cref="ArgumentException">Thrown for unrecognized values.</exception>
|
||||
public static SecurityMode FromString(string value)
|
||||
{
|
||||
return (value ?? "none").Trim().ToLowerInvariant() switch
|
||||
{
|
||||
"none" => SecurityMode.None,
|
||||
"sign" => SecurityMode.Sign,
|
||||
"encrypt" or "signandencrypt" => SecurityMode.SignAndEncrypt,
|
||||
_ => throw new ArgumentException(
|
||||
$"Unknown security mode '{value}'. Valid values: none, sign, encrypt, signandencrypt")
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Client.Shared.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Converts raw string values into typed values based on the current value's runtime type.
|
||||
/// Ported from the CLI tool's OpcUaHelper.ConvertValue.
|
||||
/// </summary>
|
||||
public static class ValueConverter
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts a raw string value into the runtime type expected by the target node.
|
||||
/// </summary>
|
||||
/// <param name="rawValue">The raw string supplied by the user.</param>
|
||||
/// <param name="currentValue">The current node value used to infer the target type. May be null.</param>
|
||||
/// <returns>A typed value suitable for an OPC UA write request.</returns>
|
||||
public static object ConvertValue(string rawValue, object? currentValue)
|
||||
{
|
||||
return currentValue switch
|
||||
{
|
||||
bool => bool.Parse(rawValue),
|
||||
byte => byte.Parse(rawValue),
|
||||
short => short.Parse(rawValue),
|
||||
ushort => ushort.Parse(rawValue),
|
||||
int => int.Parse(rawValue),
|
||||
uint => uint.Parse(rawValue),
|
||||
long => long.Parse(rawValue),
|
||||
ulong => ulong.Parse(rawValue),
|
||||
float => float.Parse(rawValue),
|
||||
double => double.Parse(rawValue),
|
||||
_ => rawValue
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user