refactor: migrate OPC UA driver and browser to SDK 2.0
This commit is contained in:
+12
-12
@@ -73,10 +73,10 @@ internal sealed class OpcUaClientBrowseSession : IBrowseSession
|
||||
Attributes.NodeClass, Attributes.DisplayName, Attributes.DataType, Attributes.ValueRank,
|
||||
Attributes.UserAccessLevel,
|
||||
];
|
||||
var toRead = new ReadValueIdCollection(attributeIds.Length);
|
||||
var toRead = new List<ReadValueId>(attributeIds.Length);
|
||||
foreach (var id in attributeIds) toRead.Add(new ReadValueId { NodeId = resolved, AttributeId = id });
|
||||
|
||||
DataValueCollection values;
|
||||
ArrayOf<DataValue> values;
|
||||
await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
@@ -97,15 +97,15 @@ internal sealed class OpcUaClientBrowseSession : IBrowseSession
|
||||
DataValue? At(int i) => i < values.Count && StatusCode.IsGood(values[i].StatusCode)
|
||||
? values[i] : null;
|
||||
|
||||
var nodeClass = At(0)?.Value is int nc ? (NodeClass)nc : NodeClass.Unspecified;
|
||||
var nodeClass = At(0)?.WrappedValue.AsBoxedObject() is int nc ? (NodeClass)nc : NodeClass.Unspecified;
|
||||
if (nodeClass != NodeClass.Variable) return Array.Empty<AttributeInfo>();
|
||||
|
||||
var name = (At(1)?.Value as LocalizedText)?.Text ?? resolved.ToString() ?? nodeId;
|
||||
var dataType = At(2)?.Value is NodeId dt
|
||||
var name = (At(1)?.WrappedValue.AsBoxedObject() as LocalizedText?)?.Text ?? resolved.ToString() ?? nodeId;
|
||||
var dataType = At(2)?.WrappedValue.AsBoxedObject() is NodeId dt
|
||||
? UpstreamAttributeMapping.MapDataType(dt)
|
||||
: ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverDataType.String;
|
||||
var valueRank = At(3)?.Value is int vr ? vr : ValueRanks.Scalar;
|
||||
var access = At(4)?.Value is byte ab ? ab : (byte)0;
|
||||
var valueRank = At(3)?.WrappedValue.AsBoxedObject() is int vr ? vr : ValueRanks.Scalar;
|
||||
var access = At(4)?.WrappedValue.AsBoxedObject() is byte ab ? ab : (byte)0;
|
||||
|
||||
return
|
||||
[
|
||||
@@ -126,7 +126,7 @@ internal sealed class OpcUaClientBrowseSession : IBrowseSession
|
||||
await _gate.WaitAsync(ct).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
var descriptions = new BrowseDescriptionCollection
|
||||
var descriptions = new List<BrowseDescription>
|
||||
{
|
||||
new()
|
||||
{
|
||||
@@ -154,7 +154,7 @@ internal sealed class OpcUaClientBrowseSession : IBrowseSession
|
||||
}
|
||||
|
||||
var result = resp.Results[0];
|
||||
var refs = result.References;
|
||||
var refs = result.References.ToList();
|
||||
|
||||
// Follow browse continuation points so folders larger than the server's per-call
|
||||
// cap aren't silently truncated.
|
||||
@@ -190,7 +190,7 @@ internal sealed class OpcUaClientBrowseSession : IBrowseSession
|
||||
if (next.Results.Count == 0) break;
|
||||
var nextResult = next.Results[0];
|
||||
if (nextResult.References is { Count: > 0 })
|
||||
refs.AddRange(nextResult.References);
|
||||
refs.AddRange(nextResult.References.ToList());
|
||||
cp = nextResult.ContinuationPoint;
|
||||
}
|
||||
|
||||
@@ -214,8 +214,8 @@ internal sealed class OpcUaClientBrowseSession : IBrowseSession
|
||||
{
|
||||
var childId = ExpandedNodeId.ToNodeId(rf.NodeId, _session.NamespaceUris);
|
||||
var isObject = rf.NodeClass == NodeClass.Object;
|
||||
var displayName = rf.DisplayName?.Text
|
||||
?? rf.BrowseName?.Name
|
||||
var displayName = rf.DisplayName.Text
|
||||
?? rf.BrowseName.Name
|
||||
?? childId.ToString()
|
||||
?? "(unnamed)";
|
||||
return new BrowseNode(
|
||||
|
||||
+6
-7
@@ -101,7 +101,7 @@ public sealed class OpcUaClientDriverBrowser : IDriverBrowser
|
||||
sessionName: "OtOpcUa AdminUI Browse",
|
||||
(uint)opts.SessionTimeout.TotalMilliseconds,
|
||||
identity,
|
||||
preferredLocales: null,
|
||||
preferredLocales: default,
|
||||
connectCts.Token).ConfigureAwait(false);
|
||||
|
||||
try
|
||||
@@ -224,8 +224,7 @@ public sealed class OpcUaClientDriverBrowser : IDriverBrowser
|
||||
return (OpcUaUserIdentityFactory.Build(resolved), null);
|
||||
}
|
||||
|
||||
var certificate = OpcUaUserIdentityFactory.LoadCertificate(resolved);
|
||||
return (new UserIdentity(certificate), certificate);
|
||||
return (OpcUaUserIdentityFactory.FromCertificate(resolved), null);
|
||||
}
|
||||
|
||||
/// <summary>Select the endpoint matching the requested SecurityPolicy + SecurityMode pair.</summary>
|
||||
@@ -240,7 +239,7 @@ public sealed class OpcUaClientDriverBrowser : IDriverBrowser
|
||||
{
|
||||
using var client = await DiscoveryClient.CreateAsync(
|
||||
appConfig, new Uri(url), DiagnosticsMasks.None, ct).ConfigureAwait(false);
|
||||
var all = await client.GetEndpointsAsync(null, ct).ConfigureAwait(false);
|
||||
var all = await client.GetEndpointsAsync(default, ct).ConfigureAwait(false);
|
||||
|
||||
var wantedPolicy = MapPolicy(policy);
|
||||
var wantedMode = mode switch
|
||||
@@ -250,11 +249,11 @@ public sealed class OpcUaClientDriverBrowser : IDriverBrowser
|
||||
OpcUaSecurityMode.SignAndEncrypt => MessageSecurityMode.SignAndEncrypt,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(mode)),
|
||||
};
|
||||
var match = all.FirstOrDefault(e =>
|
||||
var match = all.ToList().FirstOrDefault(e =>
|
||||
e.SecurityPolicyUri == wantedPolicy && e.SecurityMode == wantedMode);
|
||||
if (match is null)
|
||||
{
|
||||
var advertised = string.Join(", ", all.Select(e =>
|
||||
var advertised = string.Join(", ", all.ToList().Select(e =>
|
||||
$"{ShortName(e.SecurityPolicyUri)}/{e.SecurityMode}"));
|
||||
throw new InvalidOperationException(
|
||||
$"No endpoint at '{url}' matches SecurityPolicy={policy} + SecurityMode={mode}. " +
|
||||
@@ -278,7 +277,7 @@ public sealed class OpcUaClientDriverBrowser : IDriverBrowser
|
||||
|
||||
/// <summary>Render an OPC UA security-policy URI as its short suffix for diag messages.</summary>
|
||||
/// <param name="uri">Full policy URI to shorten.</param>
|
||||
private static string ShortName(string uri) =>
|
||||
private static string ShortName(string? uri) =>
|
||||
uri?.Substring(uri.LastIndexOf('#') + 1) ?? "(null)";
|
||||
}
|
||||
|
||||
|
||||
-1
@@ -13,7 +13,6 @@
|
||||
<ProjectReference Include="..\ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Session\ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Session.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="OPCFoundation.NetStandard.Opc.Ua.MigrationAnalyzer" PrivateAssets="all" IncludeAssets="analyzers;build;buildtransitive" />
|
||||
<PackageReference Include="OPCFoundation.NetStandard.Opc.Ua.Client" />
|
||||
<PackageReference Include="OPCFoundation.NetStandard.Opc.Ua.Configuration" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
|
||||
|
||||
@@ -196,7 +196,6 @@ public sealed class NamespaceMap
|
||||
/// <returns>The server-stable reference string for <paramref name="nodeId"/>.</returns>
|
||||
public string ToStableReference(NodeId nodeId)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(nodeId);
|
||||
// Namespace 0 is fixed by the spec — the compact form is already stable.
|
||||
if (nodeId.NamespaceIndex == 0)
|
||||
return nodeId.ToString() ?? string.Empty;
|
||||
@@ -238,7 +237,7 @@ public sealed class NamespaceMap
|
||||
// reorder since the reference was captured resolves correctly. Plain ns=N forms
|
||||
// resolve directly.
|
||||
nodeId = NodeId.Parse(currentSession.MessageContext, reference);
|
||||
return !NodeId.IsNull(nodeId);
|
||||
return !nodeId.IsNull;
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -249,10 +248,10 @@ public sealed class NamespaceMap
|
||||
/// <summary>Render just the identifier portion (<c>s=…</c>, <c>i=…</c>, <c>g=…</c>, <c>b=…</c>) of a NodeId.</summary>
|
||||
private static string IdentifierPart(NodeId nodeId) => nodeId.IdType switch
|
||||
{
|
||||
IdType.Numeric => $"i={nodeId.Identifier}",
|
||||
IdType.String => $"s={nodeId.Identifier}",
|
||||
IdType.Guid => $"g={nodeId.Identifier}",
|
||||
IdType.Opaque => $"b={Convert.ToBase64String((byte[])nodeId.Identifier)}",
|
||||
_ => $"s={nodeId.Identifier}",
|
||||
IdType.Numeric => $"i={nodeId.IdentifierAsString}",
|
||||
IdType.String => $"s={nodeId.IdentifierAsString}",
|
||||
IdType.Guid => $"g={nodeId.IdentifierAsString}",
|
||||
IdType.Opaque => $"b={nodeId.IdentifierAsString}",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(nodeId)),
|
||||
};
|
||||
}
|
||||
|
||||
+32
-1
@@ -1,4 +1,5 @@
|
||||
using Opc.Ua;
|
||||
using Opc.Ua.Security.Certificates;
|
||||
using ZB.MOM.WW.Secrets.Abstractions;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient;
|
||||
@@ -37,7 +38,37 @@ public static class OpcUaUserIdentityFactory
|
||||
/// </summary>
|
||||
/// <param name="options">Driver options containing certificate configuration.</param>
|
||||
/// <returns>The certificate-backed <see cref="UserIdentity"/>.</returns>
|
||||
public static UserIdentity FromCertificate(OpcUaClientDriverOptions options) => new(LoadCertificate(options));
|
||||
public static UserIdentity FromCertificate(OpcUaClientDriverOptions options)
|
||||
{
|
||||
using var certificate = LoadCertificate(options);
|
||||
var identifier = new CertificateIdentifier { Thumbprint = certificate.Thumbprint };
|
||||
return new UserIdentity(new X509IdentityTokenHandler(identifier,
|
||||
new CertificatePasswordProvider(), new FileCertificateProvider(options, certificate.Thumbprint)));
|
||||
}
|
||||
|
||||
// The configured credential is a PFX file, rather than an SDK certificate store. SDK 2 resolves
|
||||
// signing keys on demand and disposes each returned Certificate. Pin the public identity for this
|
||||
// lifecycle; a file/secret rotation is picked up only by the next initialise/open.
|
||||
private sealed class FileCertificateProvider(OpcUaClientDriverOptions options, string thumbprint) : ICertificateProvider
|
||||
{
|
||||
public Certificate? TryGetPrivateKeyCertificate(string requestedThumbprint) => null;
|
||||
|
||||
public ValueTask<Certificate?> GetPrivateKeyCertificateAsync(CertificateIdentifier identifier,
|
||||
ICertificatePasswordProvider? passwordProvider = null, string? applicationUri = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
var certificate = LoadCertificate(options);
|
||||
if (!string.Equals(certificate.Thumbprint, thumbprint, StringComparison.OrdinalIgnoreCase)
|
||||
|| !string.Equals(identifier.Thumbprint, thumbprint, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
certificate.Dispose();
|
||||
throw new ServiceResultException(StatusCodes.BadIdentityTokenInvalid,
|
||||
"The user certificate changed; reopen the session to use the rotated credential.");
|
||||
}
|
||||
return ValueTask.FromResult<Certificate?>(Certificate.From(certificate));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads (and validates) the user-authentication certificate <see cref="FromCertificate"/> wraps. The caller owns
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
using Opc.Ua;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient;
|
||||
|
||||
/// <summary>Detaches SDK values at the driver boundary while preserving deployed CLR payload types.</summary>
|
||||
public static class UpstreamValueMapping
|
||||
{
|
||||
/// <summary>Materializes scalar and one-dimensional array values without flattening matrices.</summary>
|
||||
/// <param name="value">The upstream value.</param>
|
||||
/// <returns>The independent CLR payload.</returns>
|
||||
public static object? ToClr(Variant value) => Detach(value.AsBoxedObject(Variant.BoxingBehavior.Legacy));
|
||||
|
||||
private static object? Detach(object? value) => value switch
|
||||
{
|
||||
ByteString bytes => bytes.IsNull ? null : bytes.ToArray(),
|
||||
DateTimeUtc time => time.ToDateTime(),
|
||||
Uuid uuid => (Guid)uuid,
|
||||
ByteString[] bytes => bytes.Select(b => b.IsNull ? null : b.ToArray()).ToArray(),
|
||||
DateTimeUtc[] times => times.Select(t => t.ToDateTime()).ToArray(),
|
||||
Uuid[] uuids => uuids.Select(u => (Guid)u).ToArray(),
|
||||
Variant[] variants => variants.Select(ToClr).ToArray(),
|
||||
object?[] objects => objects.Select(Detach).ToArray(),
|
||||
Array array => array.Clone(),
|
||||
_ => value,
|
||||
};
|
||||
}
|
||||
-1
@@ -34,7 +34,6 @@
|
||||
ContractsIsDependencyLightTests in Driver.OpcUaClient.Tests.
|
||||
-->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="OPCFoundation.NetStandard.Opc.Ua.MigrationAnalyzer" PrivateAssets="all" IncludeAssets="analyzers;build;buildtransitive" />
|
||||
<PackageReference Include="OPCFoundation.NetStandard.Opc.Ua.Client" />
|
||||
<!-- OpcUaUserIdentityFactory: the secret: resolution of the credential fields feeding the SDK UserIdentity is
|
||||
shared by the driver and the AdminUI browse (review 2026-10-21 O-5), so the browse presents exactly the
|
||||
|
||||
@@ -274,12 +274,12 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
/// <summary>
|
||||
/// Cert-validation delegate wired when <see cref="OpcUaClientDriverOptions.AutoAcceptCertificates"/>
|
||||
/// is <c>true</c>. Stored so <see cref="Dispose"/> / <see cref="DisposeAsync"/> can
|
||||
/// detach it from the (potentially process-shared) <see cref="CertificateValidator"/>
|
||||
/// detach it from the (potentially process-shared) <see cref="ICertificateManager"/>
|
||||
/// and avoid leaking the closure.
|
||||
/// </summary>
|
||||
private CertificateValidationEventHandler? _certValidationHandler;
|
||||
/// <summary>The <see cref="CertificateValidator"/> that owns <see cref="_certValidationHandler"/>.</summary>
|
||||
private CertificateValidator? _certValidatorRef;
|
||||
private Func<Opc.Ua.Security.Certificates.Certificate, ServiceResult, bool>? _certValidationHandler;
|
||||
/// <summary>The <see cref="ICertificateManager"/> that owns <see cref="_certValidationHandler"/>.</summary>
|
||||
private ICertificateManager? _certValidatorRef;
|
||||
/// <summary>
|
||||
/// 1 while the CURRENT session's keep-alive is bad (an outage is in progress), else 0. The
|
||||
/// 0→1 edge publishes the outage once (Bad per subscribed ref + a quality-only alarm annotation); the 1→0
|
||||
@@ -657,14 +657,14 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
/// <summary>The auto-accept certificate-validation handler one initialise attached, handed back to it (review m2).</summary>
|
||||
private sealed class CertValidationHook
|
||||
{
|
||||
public CertificateValidationEventHandler? Handler { get; set; }
|
||||
public CertificateValidator? Validator { get; set; }
|
||||
public Func<Opc.Ua.Security.Certificates.Certificate, ServiceResult, bool>? Handler { get; set; }
|
||||
public ICertificateManager? Validator { get; set; }
|
||||
}
|
||||
|
||||
private static void DetachCertValidation(CertificateValidator? validator, CertificateValidationEventHandler? handler)
|
||||
private static void DetachCertValidation(ICertificateManager? validator, Func<Opc.Ua.Security.Certificates.Certificate, ServiceResult, bool>? handler)
|
||||
{
|
||||
if (validator is null || handler is null) return;
|
||||
try { validator.CertificateValidation -= handler; } catch { /* best-effort */ }
|
||||
try { if (ReferenceEquals(validator.AcceptError, handler)) validator.AcceptError = null; } catch { /* best-effort */ }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -801,24 +801,23 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
// the latter would silently fail for those certs. A REVOKED certificate is the one
|
||||
// exception: auto-accept is for dev self-signed certs, never for a cert its issuer
|
||||
// has explicitly withdrawn.
|
||||
CertificateValidationEventHandler handler = (_, e) =>
|
||||
Func<Opc.Ua.Security.Certificates.Certificate, ServiceResult, bool> handler = (certificate, error) =>
|
||||
{
|
||||
if (IsRevocationFailure(e.Error.StatusCode))
|
||||
if (IsRevocationFailure(error.StatusCode))
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"OpcUaClientDriver '{DriverInstanceId}': REVOKED server certificate rejected " +
|
||||
"(subject={Subject}, status=0x{Status:X8}) — AutoAcceptCertificates does not apply to revoked certs.",
|
||||
_driverInstanceId, e.Certificate?.Subject, e.Error.StatusCode.Code);
|
||||
e.Accept = false;
|
||||
return;
|
||||
_driverInstanceId, certificate.Subject, error.StatusCode.Code);
|
||||
return false;
|
||||
}
|
||||
e.Accept = true;
|
||||
return true;
|
||||
};
|
||||
config.CertificateValidator.CertificateValidation += handler;
|
||||
config.CertificateManager.AcceptError = handler;
|
||||
// Handed back to the caller, which stores the refs (so ShutdownAsync + Dispose can detach the delegate)
|
||||
// only while its initialise is the current lifecycle generation.
|
||||
certValidation.Handler = handler;
|
||||
certValidation.Validator = config.CertificateValidator;
|
||||
certValidation.Validator = config.CertificateManager;
|
||||
}
|
||||
|
||||
// Ensure an application certificate exists. The SDK auto-generates one if missing.
|
||||
@@ -888,31 +887,31 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
var wantedPolicyUri = MapSecurityPolicy(policy);
|
||||
var wantedMode = ToMessageSecurityMode(mode);
|
||||
|
||||
ApplicationDescriptionCollection servers;
|
||||
ArrayOf<ApplicationDescription> servers;
|
||||
using (var lds = await DiscoveryClient.CreateAsync(
|
||||
appConfig, new Uri(discoveryUrl), Opc.Ua.DiagnosticsMasks.None, ct).ConfigureAwait(false))
|
||||
{
|
||||
servers = await lds.FindServersAsync(null, ct).ConfigureAwait(false);
|
||||
servers = await lds.FindServersAsync(default, ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var found = new List<DiscoveredEndpoint>();
|
||||
foreach (var server in servers)
|
||||
foreach (var server in servers.ToList())
|
||||
{
|
||||
if (server.DiscoveryUrls is null) continue;
|
||||
foreach (var serverDiscoveryUrl in server.DiscoveryUrls)
|
||||
if (server.DiscoveryUrls.IsNull) continue;
|
||||
foreach (var serverDiscoveryUrl in server.DiscoveryUrls.ToList())
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(serverDiscoveryUrl)) continue;
|
||||
EndpointDescriptionCollection endpoints;
|
||||
ArrayOf<EndpointDescription> endpoints;
|
||||
try
|
||||
{
|
||||
using var ep = await DiscoveryClient.CreateAsync(
|
||||
appConfig, new Uri(serverDiscoveryUrl), Opc.Ua.DiagnosticsMasks.None, ct).ConfigureAwait(false);
|
||||
endpoints = await ep.GetEndpointsAsync(null, ct).ConfigureAwait(false);
|
||||
endpoints = await ep.GetEndpointsAsync(default, ct).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException) when (ct.IsCancellationRequested) { throw; }
|
||||
catch { continue; }
|
||||
|
||||
found.AddRange(FilterEndpointUrls(endpoints, wantedPolicyUri, wantedMode)
|
||||
found.AddRange(FilterEndpointUrls(endpoints.ToList(), wantedPolicyUri, wantedMode)
|
||||
.Select(u => new DiscoveredEndpoint(server.ApplicationUri ?? string.Empty, u)));
|
||||
}
|
||||
}
|
||||
@@ -965,7 +964,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
endpoints
|
||||
.Where(e => e.SecurityPolicyUri == wantedPolicyUri && e.SecurityMode == wantedMode
|
||||
&& !string.IsNullOrWhiteSpace(e.EndpointUrl))
|
||||
.Select(e => e.EndpointUrl);
|
||||
.Select(e => e.EndpointUrl!);
|
||||
|
||||
private static MessageSecurityMode ToMessageSecurityMode(OpcUaSecurityMode mode) => mode switch
|
||||
{
|
||||
@@ -1052,7 +1051,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
_options.SessionName,
|
||||
(uint)_options.SessionTimeout.TotalMilliseconds,
|
||||
identity,
|
||||
null, // preferredLocales
|
||||
default, // preferredLocales
|
||||
cts.Token).ConfigureAwait(false);
|
||||
|
||||
session.KeepAliveInterval = (int)_options.KeepAliveInterval.TotalMilliseconds;
|
||||
@@ -1080,17 +1079,17 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
// all deprecated.
|
||||
using var client = await DiscoveryClient.CreateAsync(
|
||||
appConfig, new Uri(endpointUrl), Opc.Ua.DiagnosticsMasks.None, ct).ConfigureAwait(false);
|
||||
var all = await client.GetEndpointsAsync(null, ct).ConfigureAwait(false);
|
||||
var all = await client.GetEndpointsAsync(default, ct).ConfigureAwait(false);
|
||||
|
||||
var wantedPolicyUri = MapSecurityPolicy(policy);
|
||||
var wantedMode = ToMessageSecurityMode(mode);
|
||||
|
||||
var match = all.FirstOrDefault(e =>
|
||||
var match = all.ToList().FirstOrDefault(e =>
|
||||
e.SecurityPolicyUri == wantedPolicyUri && e.SecurityMode == wantedMode);
|
||||
|
||||
if (match is null)
|
||||
{
|
||||
var advertised = string.Join(", ", all
|
||||
var advertised = string.Join(", ", all.ToList()
|
||||
.Select(e => $"{ShortPolicyName(e.SecurityPolicyUri)}/{e.SecurityMode}"));
|
||||
throw new InvalidOperationException(
|
||||
$"No endpoint at '{endpointUrl}' matches SecurityPolicy={policy} + SecurityMode={mode}. " +
|
||||
@@ -1157,7 +1156,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(policy), policy, null),
|
||||
};
|
||||
|
||||
private static string ShortPolicyName(string policyUri) =>
|
||||
private static string ShortPolicyName(string? policyUri) =>
|
||||
policyUri?.Substring(policyUri.LastIndexOf('#') + 1) ?? "(null)";
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -1347,7 +1346,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
_namespaceMap = null;
|
||||
_connectedEndpointUrl = null;
|
||||
|
||||
// Detach the cert-validation handler so the (potentially process-shared) CertificateValidator doesn't
|
||||
// Detach the cert-validation handler so the (potentially process-shared) ICertificateManager doesn't
|
||||
// hold a delegate to a shutting-down driver — generation-checked like the map (r1 N-3): a newer init
|
||||
// that installed its own handler meanwhile keeps it.
|
||||
DetachCertValidation(_certValidatorRef, _certValidationHandler);
|
||||
@@ -1424,7 +1423,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
// A RawPath the authored RawTags table doesn't know — or a node id that won't parse —
|
||||
// gets BadNodeIdInvalid and is omitted from the wire request, saving a round-trip for a
|
||||
// fault the driver can detect locally.
|
||||
var toSend = new ReadValueIdCollection();
|
||||
var toSend = new List<ReadValueId>();
|
||||
var indexMap = new List<int>(fullReferences.Count); // maps wire-index -> results-index
|
||||
for (var i = 0; i < fullReferences.Count; i++)
|
||||
{
|
||||
@@ -1457,10 +1456,10 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
// §8's cascading-quality rule. Also preserve SourceTimestamp so downstream
|
||||
// clients can detect stale upstream data.
|
||||
results[r] = new DataValueSnapshot(
|
||||
Value: dv.Value,
|
||||
Value: UpstreamValueMapping.ToClr(dv.WrappedValue),
|
||||
StatusCode: dv.StatusCode.Code,
|
||||
SourceTimestampUtc: dv.SourceTimestamp == DateTime.MinValue ? null : dv.SourceTimestamp,
|
||||
ServerTimestampUtc: dv.ServerTimestamp == DateTime.MinValue ? now : dv.ServerTimestamp);
|
||||
SourceTimestampUtc: dv.SourceTimestamp == DateTime.MinValue ? null : dv.SourceTimestamp.ToDateTime(),
|
||||
ServerTimestampUtc: dv.ServerTimestamp == DateTime.MinValue ? now : dv.ServerTimestamp.ToDateTime());
|
||||
}
|
||||
RecordDeviceAnswered(generation, now);
|
||||
}
|
||||
@@ -1506,7 +1505,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
return results;
|
||||
}
|
||||
|
||||
var toSend = new WriteValueCollection();
|
||||
var toSend = new List<WriteValue>();
|
||||
var indexMap = new List<int>(writes.Count);
|
||||
for (var i = 0; i < writes.Count; i++)
|
||||
{
|
||||
@@ -1515,11 +1514,16 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
results[i] = new WriteResult(StatusBadNodeIdInvalid);
|
||||
continue;
|
||||
}
|
||||
if (!VariantHelper.TryCastFrom(writes[i].Value, out Variant value))
|
||||
{
|
||||
results[i] = new WriteResult(StatusCodes.BadTypeMismatch.Code);
|
||||
continue;
|
||||
}
|
||||
toSend.Add(new WriteValue
|
||||
{
|
||||
NodeId = nodeId,
|
||||
AttributeId = Attributes.Value,
|
||||
Value = new DataValue(new Variant(writes[i].Value)),
|
||||
Value = new DataValue(value),
|
||||
});
|
||||
indexMap.Add(i);
|
||||
}
|
||||
@@ -1674,7 +1678,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
if (discovered() >= _options.MaxDiscoveredNodes) return;
|
||||
if (!visited.Add(node)) return;
|
||||
|
||||
var browseDescriptions = new BrowseDescriptionCollection
|
||||
var browseDescriptions = new List<BrowseDescription>
|
||||
{
|
||||
new()
|
||||
{
|
||||
@@ -1688,7 +1692,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
}
|
||||
};
|
||||
|
||||
ReferenceDescriptionCollection refs;
|
||||
List<ReferenceDescription> refs;
|
||||
try
|
||||
{
|
||||
var resp = await session.BrowseAsync(
|
||||
@@ -1700,7 +1704,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
|
||||
if (resp.Results.Count == 0) return;
|
||||
var result = resp.Results[0];
|
||||
refs = result.References;
|
||||
refs = result.References.ToList();
|
||||
|
||||
// Follow browse continuation points. OPC UA servers cap the references returned
|
||||
// per node in a single response; when a folder has more children than the cap,
|
||||
@@ -1741,7 +1745,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
if (next.Results.Count == 0) break;
|
||||
var nextResult = next.Results[0];
|
||||
if (nextResult.References is { Count: > 0 })
|
||||
refs.AddRange(nextResult.References);
|
||||
refs.AddRange(nextResult.References.ToList());
|
||||
continuationPoint = nextResult.ContinuationPoint;
|
||||
}
|
||||
}
|
||||
@@ -1764,10 +1768,10 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
if (discovered() >= _options.MaxDiscoveredNodes) break;
|
||||
|
||||
var childId = ExpandedNodeId.ToNodeId(rf.NodeId, session.NamespaceUris);
|
||||
if (NodeId.IsNull(childId)) continue;
|
||||
if (childId.IsNull) continue;
|
||||
|
||||
var browseName = rf.BrowseName?.Name ?? childId.ToString();
|
||||
var displayName = rf.DisplayName?.Text ?? browseName;
|
||||
var browseName = rf.BrowseName.Name ?? childId.ToString();
|
||||
var displayName = rf.DisplayName.Text ?? browseName;
|
||||
|
||||
if (rf.NodeClass == NodeClass.Object)
|
||||
{
|
||||
@@ -1812,7 +1816,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
if (pending.Count == 0) return;
|
||||
|
||||
// 4 attributes per variable: DataType, ValueRank, UserAccessLevel, Historizing.
|
||||
var nodesToRead = new ReadValueIdCollection(pending.Count * 4);
|
||||
var nodesToRead = new List<ReadValueId>(pending.Count * 4);
|
||||
foreach (var pv in pending)
|
||||
{
|
||||
nodesToRead.Add(new ReadValueId { NodeId = pv.NodeId, AttributeId = Attributes.DataType });
|
||||
@@ -1821,7 +1825,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
nodesToRead.Add(new ReadValueId { NodeId = pv.NodeId, AttributeId = Attributes.Historizing });
|
||||
}
|
||||
|
||||
DataValueCollection values;
|
||||
ArrayOf<DataValue> values;
|
||||
try
|
||||
{
|
||||
var resp = await session.ReadAsync(
|
||||
@@ -1851,10 +1855,10 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
var accessDv = values[baseIdx + 2];
|
||||
var histDv = values[baseIdx + 3];
|
||||
|
||||
var dataType = StatusCode.IsGood(dataTypeDv.StatusCode) && dataTypeDv.Value is NodeId dtId
|
||||
var dataType = StatusCode.IsGood(dataTypeDv.StatusCode) && UpstreamValueMapping.ToClr(dataTypeDv.WrappedValue) is NodeId dtId
|
||||
? MapUpstreamDataType(dtId)
|
||||
: DriverDataType.Int32;
|
||||
var valueRank = StatusCode.IsGood(valueRankDv.StatusCode) && valueRankDv.Value is int vr ? vr : -1;
|
||||
var valueRank = StatusCode.IsGood(valueRankDv.StatusCode) && UpstreamValueMapping.ToClr(valueRankDv.WrappedValue) is int vr ? vr : -1;
|
||||
// OPC UA Part 3 ValueRank constants: -3 = ScalarOrOneDimension, -2 = Any,
|
||||
// -1 = Scalar, 0 = OneOrMoreDimensions, 1 = OneDimension, >1 = N specific dimensions.
|
||||
// Deliberate choice: treat anything >= 0 as an array (the spec guarantees -3/-2/-1
|
||||
@@ -1863,9 +1867,9 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
// as scalar — array-of-one is exposed as scalar to the local address space until
|
||||
// the upstream variable carries a concrete dimensioned rank.
|
||||
var isArray = UpstreamAttributeMapping.IsArray(valueRank);
|
||||
var access = StatusCode.IsGood(accessDv.StatusCode) && accessDv.Value is byte ab ? ab : (byte)0;
|
||||
var access = StatusCode.IsGood(accessDv.StatusCode) && UpstreamValueMapping.ToClr(accessDv.WrappedValue) is byte ab ? ab : (byte)0;
|
||||
var securityClass = MapAccessLevelToSecurityClass(access);
|
||||
var historizing = StatusCode.IsGood(histDv.StatusCode) && histDv.Value is bool b && b;
|
||||
var historizing = StatusCode.IsGood(histDv.StatusCode) && UpstreamValueMapping.ToClr(histDv.WrappedValue) is bool b && b;
|
||||
|
||||
pv.ParentFolder.Variable(pv.BrowseName, pv.DisplayName, new DriverAttributeInfo(
|
||||
FullName: StableReference(pv.NodeId),
|
||||
@@ -2077,7 +2081,6 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
// subscriptions; extract its DataValue. The Handle property carries our tag string.
|
||||
if (args.NotificationValue is not MonitoredItemNotification mn) return;
|
||||
var dv = mn.Value;
|
||||
if (dv is null) return;
|
||||
PublishNotification(handle, RefOf(item), dv);
|
||||
}
|
||||
|
||||
@@ -2091,10 +2094,10 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
// without this a busy subscription-only driver's LastSuccessfulRead stayed at its connect time.
|
||||
RecordLivenessEvidence();
|
||||
var snapshot = new DataValueSnapshot(
|
||||
Value: dv.Value,
|
||||
Value: UpstreamValueMapping.ToClr(dv.WrappedValue),
|
||||
StatusCode: dv.StatusCode.Code,
|
||||
SourceTimestampUtc: dv.SourceTimestamp == DateTime.MinValue ? null : dv.SourceTimestamp,
|
||||
ServerTimestampUtc: dv.ServerTimestamp == DateTime.MinValue ? DateTime.UtcNow : dv.ServerTimestamp);
|
||||
SourceTimestampUtc: dv.SourceTimestamp == DateTime.MinValue ? null : dv.SourceTimestamp.ToDateTime(),
|
||||
ServerTimestampUtc: dv.ServerTimestamp == DateTime.MinValue ? DateTime.UtcNow : dv.ServerTimestamp.ToDateTime());
|
||||
// Review of 3b88056f: the replay check and the publish are one critical section shared with the recovery and
|
||||
// trailing reads, so an older value can never be delivered after a newer one (see _deliveryLock).
|
||||
lock (_deliveryLock)
|
||||
@@ -2415,7 +2418,8 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
// constants so the notification handler indexes in O(1) without re-examining the
|
||||
// QualifiedName BrowsePaths.
|
||||
var filter = new EventFilter();
|
||||
void AddField(string browseName) => filter.SelectClauses.Add(new SimpleAttributeOperand
|
||||
var selectClauses = new List<SimpleAttributeOperand>();
|
||||
void AddField(string browseName) => selectClauses.Add(new SimpleAttributeOperand
|
||||
{
|
||||
TypeDefinitionId = ObjectTypeIds.BaseEventType,
|
||||
BrowsePath = [new QualifiedName(browseName)],
|
||||
@@ -2429,7 +2433,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
AddField("Time");
|
||||
// ConditionId on ConditionType nodes is the branch identifier for
|
||||
// acknowledgeable conditions. Not a BaseEventType field — reach it via the typed path.
|
||||
filter.SelectClauses.Add(new SimpleAttributeOperand
|
||||
selectClauses.Add(new SimpleAttributeOperand
|
||||
{
|
||||
TypeDefinitionId = ObjectTypeIds.ConditionType,
|
||||
BrowsePath = [], // empty path = the condition node itself
|
||||
@@ -2437,13 +2441,13 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
});
|
||||
// the host derives Active/Acked solely from AlarmEventArgs.Kind, so the condition state
|
||||
// must be selected. TWO-element browse paths — a single "ActiveState/Id" QualifiedName names nothing.
|
||||
filter.SelectClauses.Add(new SimpleAttributeOperand
|
||||
selectClauses.Add(new SimpleAttributeOperand
|
||||
{
|
||||
TypeDefinitionId = ObjectTypeIds.AlarmConditionType,
|
||||
BrowsePath = [new QualifiedName(BrowseNames.ActiveState), new QualifiedName(BrowseNames.Id)],
|
||||
AttributeId = Attributes.Value,
|
||||
});
|
||||
filter.SelectClauses.Add(new SimpleAttributeOperand
|
||||
selectClauses.Add(new SimpleAttributeOperand
|
||||
{
|
||||
TypeDefinitionId = ObjectTypeIds.AcknowledgeableConditionType,
|
||||
BrowsePath = [new QualifiedName(BrowseNames.AckedState), new QualifiedName(BrowseNames.Id)],
|
||||
@@ -2451,13 +2455,15 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
});
|
||||
// BranchId — a non-null one marks a condition BRANCH event (a previous state, with its own
|
||||
// EventId). Only the trunk (null BranchId) is the condition's current state.
|
||||
filter.SelectClauses.Add(new SimpleAttributeOperand
|
||||
selectClauses.Add(new SimpleAttributeOperand
|
||||
{
|
||||
TypeDefinitionId = ObjectTypeIds.ConditionType,
|
||||
BrowsePath = [new QualifiedName(BrowseNames.BranchId)],
|
||||
AttributeId = Attributes.Value,
|
||||
});
|
||||
|
||||
filter.SelectClauses = selectClauses;
|
||||
|
||||
// Set by CreateAlarmEventItemAsync so a failure after the handler is attached can detach it.
|
||||
MonitoredItem? eventItem = null;
|
||||
MonitoredItemNotificationEventHandler? notifHandler = null;
|
||||
@@ -2598,7 +2604,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
// OPC UA A&C: call the AcknowledgeableConditionType.Acknowledge method on each
|
||||
// condition node with EventId + Comment arguments. CallAsync accepts a batch —
|
||||
// one CallMethodRequest per ack.
|
||||
var callRequests = new CallMethodRequestCollection();
|
||||
var callRequests = new List<CallMethodRequest>();
|
||||
foreach (var ack in acknowledgements)
|
||||
{
|
||||
// the host acks by RawPath; Acknowledge must hit the upstream CONDITION node.
|
||||
@@ -2711,7 +2717,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
// without this it would feed RouteAlarmEvent alongside the real one.
|
||||
if (!_alarmSubscriptions.ContainsKey(handle.Id)) return;
|
||||
RecordLivenessEvidence();
|
||||
RouteAlarmEvent(efl.EventFields, sourceFilter, handle);
|
||||
RouteAlarmEvent(efl.EventFields.ToList(), sourceFilter, handle);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -2805,7 +2811,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
/// <param name="fields">The event's fields.</param>
|
||||
/// <returns>Whether the event is a RefreshRequired marker.</returns>
|
||||
internal static bool IsRefreshRequired(IReadOnlyList<Variant> fields) =>
|
||||
fields.Count > AlarmFieldEventType && fields[AlarmFieldEventType].Value is NodeId t
|
||||
fields.Count > AlarmFieldEventType && UpstreamValueMapping.ToClr(fields[AlarmFieldEventType]) is NodeId t
|
||||
&& t == ObjectTypeIds.RefreshRequiredEventType;
|
||||
|
||||
/// <summary>True for the three ConditionRefresh marker event types — never alarms.</summary>
|
||||
@@ -3017,21 +3023,21 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
state = null;
|
||||
if (fields.Count <= AlarmFieldConditionId) return null;
|
||||
// ConditionRefresh brackets its re-delivery with RefreshStart/RefreshEnd — markers, never alarms.
|
||||
if (IsRefreshMarker(fields[AlarmFieldEventType].Value)) return null;
|
||||
if (IsRefreshMarker(UpstreamValueMapping.ToClr(fields[AlarmFieldEventType]))) return null;
|
||||
// A condition BRANCH event reports a previous state with its own EventId: projecting it would flip the
|
||||
// trunk's state, remembering it would make Acknowledge ack the branch.
|
||||
if (fields.Count > AlarmFieldBranchId && fields[AlarmFieldBranchId].Value is NodeId branch && !NodeId.IsNull(branch))
|
||||
if (fields.Count > AlarmFieldBranchId && UpstreamValueMapping.ToClr(fields[AlarmFieldBranchId]) is NodeId branch && !branch.IsNull)
|
||||
return null;
|
||||
if (fields.Count > AlarmFieldAckedState
|
||||
&& fields[AlarmFieldActiveState].Value is bool active
|
||||
&& fields[AlarmFieldAckedState].Value is bool acked)
|
||||
&& UpstreamValueMapping.ToClr(fields[AlarmFieldActiveState]) is bool active
|
||||
&& UpstreamValueMapping.ToClr(fields[AlarmFieldAckedState]) is bool acked)
|
||||
state = (active, acked);
|
||||
|
||||
// Pass-through (no authored refs) keeps the raw serialized ids; an authored index compares
|
||||
// server-stable keys (see AlarmIndexKey), matching the CONDITION node first — the only way to tell
|
||||
// several conditions on one source apart — and then the source node.
|
||||
var sourceField = fields[AlarmFieldSourceNode].Value;
|
||||
var conditionField = fields[AlarmFieldConditionId].Value;
|
||||
var sourceField = UpstreamValueMapping.ToClr(fields[AlarmFieldSourceNode]);
|
||||
var conditionField = UpstreamValueMapping.ToClr(fields[AlarmFieldConditionId]);
|
||||
string reported, upstreamCondition;
|
||||
if (sourceIndex.Count == 0)
|
||||
{
|
||||
@@ -3040,23 +3046,23 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
}
|
||||
else
|
||||
{
|
||||
upstreamCondition = conditionField is NodeId cn && !NodeId.IsNull(cn) ? keyOf(cn) : string.Empty;
|
||||
var sourceKey = sourceField is NodeId sn && !NodeId.IsNull(sn) ? keyOf(sn) : string.Empty;
|
||||
upstreamCondition = conditionField is NodeId cn && !cn.IsNull ? keyOf(cn) : string.Empty;
|
||||
var sourceKey = sourceField is NodeId sn && !sn.IsNull ? keyOf(sn) : string.Empty;
|
||||
if (!(upstreamCondition.Length > 0 && sourceIndex.TryGetValue(upstreamCondition, out reported!))
|
||||
&& !(sourceKey.Length > 0 && sourceIndex.TryGetValue(sourceKey, out reported!)))
|
||||
return null;
|
||||
}
|
||||
|
||||
var eventType = fields[AlarmFieldEventType].Value?.ToString() ?? "BaseEventType";
|
||||
var message = (fields[AlarmFieldMessage].Value as LocalizedText)?.Text ?? string.Empty;
|
||||
var severity = fields[AlarmFieldSeverity].Value is ushort sev ? sev : (ushort)0;
|
||||
var time = fields[AlarmFieldTime].Value is DateTime t ? t : DateTime.UtcNow;
|
||||
var eventType = UpstreamValueMapping.ToClr(fields[AlarmFieldEventType])?.ToString() ?? "BaseEventType";
|
||||
var message = (UpstreamValueMapping.ToClr(fields[AlarmFieldMessage]) as LocalizedText?)?.Text ?? string.Empty;
|
||||
var severity = UpstreamValueMapping.ToClr(fields[AlarmFieldSeverity]) is ushort sev ? sev : (ushort)0;
|
||||
var time = UpstreamValueMapping.ToClr(fields[AlarmFieldTime]) is DateTime t ? t : DateTime.UtcNow;
|
||||
|
||||
if (sourceIndex.Count != 0 && upstreamCondition.Length > 0)
|
||||
rememberCondition(reported, new AlarmAckTarget(
|
||||
upstreamCondition,
|
||||
// A copy: the decoded notification's array is not ours to alias for the ack's lifetime.
|
||||
(fields[AlarmFieldEventId].Value as byte[])?.ToArray() ?? Array.Empty<byte>()));
|
||||
(UpstreamValueMapping.ToClr(fields[AlarmFieldEventId]) as byte[])?.ToArray() ?? Array.Empty<byte>()));
|
||||
|
||||
return new AlarmEventArgs(
|
||||
SubscriptionHandle: handle,
|
||||
@@ -3166,10 +3172,10 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
catch { /* best-effort — SDK may have already cleared its invocation lists on session loss */ }
|
||||
}
|
||||
|
||||
private static int CountNotifications(ExtensionObject nd) => nd.Body switch
|
||||
private static int CountNotifications(ExtensionObject nd) => (nd.TryGetValue(out IEncodeable? body) ? body : null) switch
|
||||
{
|
||||
DataChangeNotification dc => dc.MonitoredItems?.Count ?? 0,
|
||||
EventNotificationList ev => ev.Events?.Count ?? 0,
|
||||
DataChangeNotification dc => dc.MonitoredItems.Count,
|
||||
EventNotificationList ev => ev.Events.Count,
|
||||
StatusChangeNotification => 1,
|
||||
_ => 1,
|
||||
};
|
||||
@@ -3268,7 +3274,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
PublishCommsLoss(now);
|
||||
}
|
||||
if (definitive)
|
||||
RaiseConnectionLost($"upstream session keep-alive reported {StatusCodes.GetBrowseName(keepAliveCode.Code)}");
|
||||
RaiseConnectionLost($"upstream session keep-alive reported {keepAliveCode.SymbolicId}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -3719,11 +3725,11 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
}
|
||||
|
||||
// Detach the cert-validation handler registered during InitializeAsync so the
|
||||
// CertificateValidator (which may be process-shared) doesn't hold a reference to
|
||||
// ICertificateManager (which may be process-shared) doesn't hold a reference to
|
||||
// a disposed driver.
|
||||
if (_certValidationHandler is not null && _certValidatorRef is not null)
|
||||
{
|
||||
try { _certValidatorRef.CertificateValidation -= _certValidationHandler; } catch { }
|
||||
try { if (ReferenceEquals(_certValidatorRef.AcceptError, _certValidationHandler)) _certValidatorRef.AcceptError = null; } catch { }
|
||||
_certValidationHandler = null;
|
||||
_certValidatorRef = null;
|
||||
}
|
||||
@@ -3764,7 +3770,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
// Detach the cert-validation handler.
|
||||
if (_certValidationHandler is not null && _certValidatorRef is not null)
|
||||
{
|
||||
try { _certValidatorRef.CertificateValidation -= _certValidationHandler; } catch { }
|
||||
try { if (ReferenceEquals(_certValidatorRef.AcceptError, _certValidationHandler)) _certValidatorRef.AcceptError = null; } catch { }
|
||||
_certValidationHandler = null;
|
||||
_certValidatorRef = null;
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ public sealed class OpcUaClientDriverProbe : IDriverProbe
|
||||
var appConfig = BuildMinimalAppConfig();
|
||||
using var client = await DiscoveryClient.CreateAsync(
|
||||
appConfig, new Uri(endpointUrl), DiagnosticsMasks.None, cts.Token).ConfigureAwait(false);
|
||||
var endpoints = await client.GetEndpointsAsync(null, cts.Token).ConfigureAwait(false);
|
||||
var endpoints = await client.GetEndpointsAsync(default, cts.Token).ConfigureAwait(false);
|
||||
sw.Stop();
|
||||
|
||||
if (endpoints is { Count: > 0 })
|
||||
|
||||
-1
@@ -21,7 +21,6 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="OPCFoundation.NetStandard.Opc.Ua.MigrationAnalyzer" PrivateAssets="all" IncludeAssets="analyzers;build;buildtransitive" />
|
||||
<PackageReference Include="OPCFoundation.NetStandard.Opc.Ua.Client"/>
|
||||
<PackageReference Include="OPCFoundation.NetStandard.Opc.Ua.Configuration"/>
|
||||
<PackageReference Include="ZB.MOM.WW.Secrets.Abstractions"/>
|
||||
|
||||
Reference in New Issue
Block a user