7cd5cde315
Browser project (Phase 3) needs to share namespace-stable address encoding with the runtime driver. Move keeps the same namespace, so existing usages in OpcUaClientDriver compile unchanged.
147 lines
7.1 KiB
C#
147 lines
7.1 KiB
C#
using Opc.Ua;
|
|
using Opc.Ua.Client;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient;
|
|
|
|
/// <summary>
|
|
/// Bidirectional namespace map for the OPC UA Client (gateway) driver, built at connect
|
|
/// time from <c>session.NamespaceUris</c> per <c>docs/v2/driver-specs.md</c> §8
|
|
/// "Namespace Remapping".
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// The session-relative namespace index embedded in an <c>ns=N;…</c> NodeId string is
|
|
/// <b>not</b> stable: the OPC UA spec permits a server to reorder its namespace table
|
|
/// across a restart. A driver that stores raw <c>ns=N</c> references and re-parses
|
|
/// them verbatim for reads/writes will, after such a reorder, silently address the
|
|
/// wrong namespace.
|
|
/// </para>
|
|
/// <para>
|
|
/// This map captures the upstream namespace table as it was at connect time and lets
|
|
/// the driver persist NodeIds in a <b>server-stable</b> form — the namespace
|
|
/// <i>URI</i> plus the identifier — via <see cref="ToStableReference"/>. At
|
|
/// read/write time <see cref="TryResolve"/> re-binds that stable form against the
|
|
/// <i>current</i> session's namespace table, so a table reorder is transparently
|
|
/// corrected instead of silently misaddressing nodes.
|
|
/// </para>
|
|
/// <para>
|
|
/// Stable references use the form <c>nsu=<uri>;<idType>=<identifier></c>,
|
|
/// which is the standard OPC UA namespace-URI NodeId encoding the SDK already
|
|
/// understands. References that are already in plain <c>ns=N;…</c> form (e.g. a
|
|
/// hand-entered config tag) still resolve — <see cref="TryResolve"/> falls back to a
|
|
/// direct parse against the current session.
|
|
/// </para>
|
|
/// </remarks>
|
|
public sealed class NamespaceMap
|
|
{
|
|
// index -> URI and URI -> index, as the upstream server published them at connect time.
|
|
private readonly string[] _uris;
|
|
private readonly Dictionary<string, ushort> _uriToIndex;
|
|
|
|
private NamespaceMap(string[] uris)
|
|
{
|
|
_uris = uris;
|
|
_uriToIndex = new Dictionary<string, ushort>(uris.Length, StringComparer.Ordinal);
|
|
for (var i = 0; i < uris.Length; i++)
|
|
_uriToIndex[uris[i]] = (ushort)i;
|
|
}
|
|
|
|
/// <summary>Snapshot the namespace table from a live session.</summary>
|
|
/// <param name="session">The OPC UA session to snapshot.</param>
|
|
public static NamespaceMap FromSession(ISession session)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(session);
|
|
return FromTable(session.NamespaceUris);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Snapshot a <see cref="NamespaceTable"/> directly. Separated from
|
|
/// <see cref="FromSession"/> so the URI encoding can be exercised without standing up
|
|
/// a live <see cref="ISession"/>.
|
|
/// </summary>
|
|
/// <param name="namespaceUris">The namespace table to snapshot.</param>
|
|
public static NamespaceMap FromTable(NamespaceTable namespaceUris)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(namespaceUris);
|
|
return new NamespaceMap(namespaceUris.ToArray());
|
|
}
|
|
|
|
/// <summary>Number of namespaces captured. Index 0 is always the OPC UA core namespace.</summary>
|
|
public int Count => _uris.Length;
|
|
|
|
/// <summary>The namespace URI at the given index, or null if out of range.</summary>
|
|
/// <param name="index">The zero-based index in the namespace table.</param>
|
|
public string? UriForIndex(int index) =>
|
|
index >= 0 && index < _uris.Length ? _uris[index] : null;
|
|
|
|
/// <summary>The index for a namespace URI, or null if the URI is not in the table.</summary>
|
|
/// <param name="uri">The namespace URI to look up.</param>
|
|
public ushort? IndexForUri(string uri) =>
|
|
_uriToIndex.TryGetValue(uri, out var idx) ? idx : null;
|
|
|
|
/// <summary>
|
|
/// Render a NodeId resolved against this map's session as a <b>server-stable</b>
|
|
/// reference string — namespace URI plus identifier, in the SDK's <c>nsu=…</c>
|
|
/// encoding. The OPC UA core namespace (index 0) keeps the compact <c>i=…</c>/<c>ns=0</c>
|
|
/// form since URI 0 never moves. Used to persist a discovered NodeId into the local
|
|
/// address space so it survives a remote namespace-table reorder.
|
|
/// </summary>
|
|
/// <param name="nodeId">The NodeId to render as a stable reference.</param>
|
|
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;
|
|
|
|
var uri = UriForIndex(nodeId.NamespaceIndex);
|
|
if (uri is null)
|
|
// Namespace index not in the captured table — fall back to the raw form rather
|
|
// than throwing; the read/write path will surface BadNodeIdInvalid if it truly
|
|
// can't resolve.
|
|
return nodeId.ToString() ?? string.Empty;
|
|
|
|
// nsu=<uri>;<idType>=<identifier> is the standard namespace-URI NodeId encoding.
|
|
return $"nsu={uri};{IdentifierPart(nodeId)}";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolve a reference string — either a stable <c>nsu=…</c> reference produced by
|
|
/// <see cref="ToStableReference"/> or a plain <c>ns=N;…</c> NodeId — against the
|
|
/// <paramref name="currentSession"/>'s namespace table. A <c>nsu=…</c> reference is
|
|
/// re-bound through the current session's URI table so a remote reorder since connect
|
|
/// time is corrected. Returns false for empty/malformed input or an unknown URI.
|
|
/// </summary>
|
|
/// <param name="currentSession">The OPC UA session whose namespace table to resolve against.</param>
|
|
/// <param name="reference">The reference string to resolve (either <c>nsu=…</c> or <c>ns=N;…</c> format).</param>
|
|
/// <param name="nodeId">On success, the resolved NodeId; otherwise NodeId.Null.</param>
|
|
public static bool TryResolve(ISession currentSession, string reference, out NodeId nodeId)
|
|
{
|
|
nodeId = NodeId.Null;
|
|
if (currentSession is null || string.IsNullOrWhiteSpace(reference)) return false;
|
|
try
|
|
{
|
|
// NodeId.Parse with a NamespaceUriString form (nsu=…) re-maps the URI against
|
|
// the supplied context's NamespaceUris — i.e. the *current* session table — so a
|
|
// reorder since the reference was captured resolves correctly. Plain ns=N forms
|
|
// resolve directly.
|
|
nodeId = NodeId.Parse(currentSession.MessageContext, reference);
|
|
return !NodeId.IsNull(nodeId);
|
|
}
|
|
catch
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <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}",
|
|
};
|
|
}
|