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}",
};
}