using Opc.Ua; using Opc.Ua.Client; namespace ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient; /// /// Bidirectional namespace map for the OPC UA Client (gateway) driver, built at connect /// time from session.NamespaceUris per docs/v2/driver-specs.md §8 /// "Namespace Remapping". /// /// /// /// The session-relative namespace index embedded in an ns=N;… NodeId string is /// not stable: the OPC UA spec permits a server to reorder its namespace table /// across a restart. A driver that stores raw ns=N references and re-parses /// them verbatim for reads/writes will, after such a reorder, silently address the /// wrong namespace. /// /// /// This map captures the upstream namespace table as it was at connect time and lets /// the driver persist NodeIds in a server-stable form — the namespace /// URI plus the identifier — via . At /// read/write time re-binds that stable form against the /// current session's namespace table, so a table reorder is transparently /// corrected instead of silently misaddressing nodes. /// /// /// Stable references use the form nsu=<uri>;<idType>=<identifier>, /// which is the standard OPC UA namespace-URI NodeId encoding the SDK already /// understands. References that are already in plain ns=N;… form (e.g. a /// hand-entered config tag) still resolve — falls back to a /// direct parse against the current session. /// /// 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 _uriToIndex; private NamespaceMap(string[] uris) { _uris = uris; _uriToIndex = new Dictionary(uris.Length, StringComparer.Ordinal); for (var i = 0; i < uris.Length; i++) _uriToIndex[uris[i]] = (ushort)i; } /// Snapshot the namespace table from a live session. /// The OPC UA session to snapshot. public static NamespaceMap FromSession(ISession session) { ArgumentNullException.ThrowIfNull(session); return FromTable(session.NamespaceUris); } /// /// Snapshot a directly. Separated from /// so the URI encoding can be exercised without standing up /// a live . /// /// The namespace table to snapshot. public static NamespaceMap FromTable(NamespaceTable namespaceUris) { ArgumentNullException.ThrowIfNull(namespaceUris); return new NamespaceMap(namespaceUris.ToArray()); } /// Number of namespaces captured. Index 0 is always the OPC UA core namespace. public int Count => _uris.Length; /// The namespace URI at the given index, or null if out of range. /// The zero-based index in the namespace table. public string? UriForIndex(int index) => index >= 0 && index < _uris.Length ? _uris[index] : null; /// The index for a namespace URI, or null if the URI is not in the table. /// The namespace URI to look up. public ushort? IndexForUri(string uri) => _uriToIndex.TryGetValue(uri, out var idx) ? idx : null; /// /// Render a NodeId resolved against this map's session as a server-stable /// reference string — namespace URI plus identifier, in the SDK's nsu=… /// encoding. The OPC UA core namespace (index 0) keeps the compact i=…/ns=0 /// 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. /// /// The NodeId to render as a stable reference. 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=;= is the standard namespace-URI NodeId encoding. return $"nsu={uri};{IdentifierPart(nodeId)}"; } /// /// Resolve a reference string — either a stable nsu=… reference produced by /// or a plain ns=N;… NodeId — against the /// 's namespace table. A nsu=… 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. /// /// The OPC UA session whose namespace table to resolve against. /// The reference string to resolve (either nsu=… or ns=N;… format). /// On success, the resolved NodeId; otherwise NodeId.Null. 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; } } /// Render just the identifier portion (s=…, i=…, g=…, b=…) of a NodeId. 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}", }; }