Auto: opcuaclient-8 — type definition mirroring

Adds an opt-in pass-3 walk of the upstream TypesFolder (i=86) so the OPC UA
Client driver can mirror upstream type definitions into the local address
space. Honours the curation rules from PR-7 (#359). Structural mirror only —
binary-encoding priming via LoadDataTypeSystem is tracked as a follow-up
because that helper was removed from the public ISession surface in
OPCFoundation.NetStandard 1.5.378+.

- IAddressSpaceBuilder.RegisterTypeNode (default no-op for back-compat)
- MirroredTypeNodeInfo + MirroredTypeKind in Core.Abstractions
- OpcUaClientDriverOptions.MirrorTypeDefinitions (default false)
- DiscoverAsync pass-3: FetchTypeTreeAsync + recursive HasSubtype walk per
  branch (ObjectTypes/VariableTypes/DataTypes/ReferenceTypes), best-effort
  IsAbstract read, IncludePaths/ExcludePaths still applied
- 6 new unit tests; all 153 OpcUaClient unit tests pass

Closes #280
This commit is contained in:
Joseph Doherty
2026-04-25 20:38:17 -04:00
parent 5e164dc965
commit cc21281cbb
4 changed files with 457 additions and 0 deletions

View File

@@ -35,8 +35,77 @@ public interface IAddressSpaceBuilder
/// <c>_base</c> equipment-class template).
/// </summary>
void AddProperty(string browseName, DriverDataType dataType, object? value);
/// <summary>
/// Register a type-definition node (ObjectType / VariableType / DataType / ReferenceType)
/// mirrored from an upstream OPC UA server. Optional surface — drivers that don't mirror
/// types simply never call it; address-space builders that don't materialise upstream
/// types can leave the default no-op in place. Default implementation drops the call so
/// adding this method doesn't break existing <see cref="IAddressSpaceBuilder"/>
/// implementations.
/// </summary>
/// <param name="info">Metadata describing the type-definition node to mirror.</param>
/// <remarks>
/// <para>
/// The OPC UA Client driver is the primary caller — it walks <c>i=86</c>
/// (TypesFolder) during <c>DiscoverAsync</c> when
/// <c>OpcUaClientDriverOptions.MirrorTypeDefinitions</c> is set so downstream clients
/// see the upstream type system instead of rendering structured-type values as opaque
/// strings.
/// </para>
/// <para>
/// The default no-op is intentional — most builders (Galaxy, Modbus, FOCAS, S7,
/// TwinCAT, AB-CIP) don't have a meaningful type folder to project into and would
/// otherwise need empty-stub overrides.
/// </para>
/// </remarks>
void RegisterTypeNode(MirroredTypeNodeInfo info) { /* default: no-op */ }
}
/// <summary>
/// Categorises a mirrored type-definition node so the receiving builder can route it into
/// the right OPC UA standard subtree (<c>ObjectTypesFolder</c>, <c>VariableTypesFolder</c>,
/// <c>DataTypesFolder</c>, <c>ReferenceTypesFolder</c>) when projecting upstream types into
/// the local address space.
/// </summary>
public enum MirroredTypeKind
{
ObjectType,
VariableType,
DataType,
ReferenceType,
}
/// <summary>
/// Metadata describing a single type-definition node mirrored from an upstream OPC UA
/// server. Built by the OPC UA Client driver during type-mirror pass and consumed by
/// <see cref="IAddressSpaceBuilder.RegisterTypeNode"/>.
/// </summary>
/// <param name="Kind">Type category — drives which standard sub-folder the node lives under.</param>
/// <param name="UpstreamNodeId">
/// Stringified upstream NodeId (e.g. <c>"ns=2;i=1234"</c>) — preserves the original identity
/// so a builder that wants to project the type with a stable cross-namespace reference can do
/// so. The driver applies any configured namespace remap before stamping this field.
/// </param>
/// <param name="BrowseName">OPC UA BrowseName segment from the upstream BrowseName.</param>
/// <param name="DisplayName">Human-readable display name; falls back to <paramref name="BrowseName"/>.</param>
/// <param name="SuperTypeNodeId">
/// Stringified upstream NodeId of the super-type (parent type), or <c>null</c> when the node
/// sits directly under the root (e.g. <c>BaseObjectType</c>, <c>BaseVariableType</c>). Lets
/// the builder reconstruct the inheritance chain.
/// </param>
/// <param name="IsAbstract">
/// <c>true</c> when the upstream node has the <c>IsAbstract</c> flag set (Object / Variable /
/// ReferenceType). DataTypes also expose this — the driver passes it through verbatim.
/// </param>
public sealed record MirroredTypeNodeInfo(
MirroredTypeKind Kind,
string UpstreamNodeId,
string BrowseName,
string DisplayName,
string? SuperTypeNodeId,
bool IsAbstract);
/// <summary>Opaque handle for a registered variable. Used by Core for subscription routing.</summary>
public interface IVariableHandle
{