using Microsoft.Extensions.Logging; using Opc.Ua; using Opc.Ua.Server; using ZB.MOM.WW.OtOpcUa.Commons.OpcUa; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; using ZB.MOM.WW.OtOpcUa.Core.Authorization; using ZB.MOM.WW.OtOpcUa.OpcUaServer.Security; namespace ZB.MOM.WW.OtOpcUa.OpcUaServer; /// /// subclass that wires in the v2 . /// Exposes the live node manager after start so callers (, /// the fused Host's DI binding) can wrap it in a and hand /// it to OpcUaPublishActor. /// public sealed class OtOpcUaSdkServer : StandardServer { private readonly ILogger? _nodeManagerLogger; private OtOpcUaNodeManager? _otOpcUaNodeManager; /// Initializes a new instance of the class. /// Logger handed to the created ; /// null leaves it on a no-op logger. public OtOpcUaSdkServer(ILogger? nodeManagerLogger = null) => _nodeManagerLogger = nodeManagerLogger; /// The custom node manager once StartAsync has called /// . Null until the SDK has bootstrapped. public OtOpcUaNodeManager? NodeManager => _otOpcUaNodeManager; /// /// Wire the reverse-path sink for inbound Part 9 alarm method calls onto the created /// . The host calls this after start with a non-blocking /// mediator.Tell that publishes each onto the /// alarm-commands DistributedPubSub topic. No-op (returns false) when the node /// manager has not been created yet, so the caller can detect a too-early call. /// /// The router invoked by the condition handlers once the AlarmAck /// gate passes; may be null to clear it. /// true when the router was set on a live node manager; false when no node /// manager exists yet. public bool SetAlarmCommandRouter(Action? router) { if (_otOpcUaNodeManager is null) return false; _otOpcUaNodeManager.AlarmCommandRouter = router; return true; } /// /// Wire the reverse-path router for inbound Part 9 Acknowledge of a NATIVE (driver-fed, e.g. Galaxy) /// condition onto the created . The host calls this after start with a /// non-blocking router that Tells a DriverHostActor.RouteNativeAlarmAck into the local /// DriverHostActor, which (Primary-gated) routes the acknowledge to the owning driver child. /// Native conditions are not owned by the scripted-alarm engine, so the node manager branches their /// inbound Acknowledge to this seam instead of the scripted path. /// Passing null clears it. No-op (returns false) when the node manager has not been /// created yet, so the caller can detect a too-early call (mirrors ). /// /// The router invoked by the native condition's Acknowledge handler once the /// AlarmAck gate passes; may be null to clear it. /// true when the router was set on a live node manager; false when no node /// manager exists yet. public bool SetNativeAlarmAckRouter(Action? router) { if (_otOpcUaNodeManager is null) return false; _otOpcUaNodeManager.NativeAlarmAckRouter = router; return true; } /// /// Gitea #597 — wire the redundancy verdict that decides whether this node accepts an OPC UA Acknowledge /// of a NATIVE condition (); on any other node /// the ack is refused with BadInvalidState instead of being applied locally and reverted by the /// driver's next transition. Passing null clears it (allow). No-op (returns false) when the /// node manager has not been created yet (mirrors ). /// /// A cheap, non-blocking read of the data-plane Primary verdict; null to allow. /// true when set on a live node manager; false when no node manager exists yet. public bool SetNativeAlarmAckPrimaryGate(Func? isPrimary) { if (_otOpcUaNodeManager is null) return false; _otOpcUaNodeManager.NativeAlarmAckPrimaryGate = isPrimary; return true; } /// /// Gitea #674 — wire the redundancy verdict that decides whether this node serves an OPC UA HistoryUpdate /// (); on any other node every authorized node of a /// HistoryUpdate is refused BadInvalidState. Passing null clears it (allow). No-op (returns /// false) when the node manager has not been created yet (mirrors ). /// /// A cheap, non-blocking read of the data-plane Primary verdict; null to allow. /// true when set on a live node manager; false when no node manager exists yet. public bool SetHistoryUpdatePrimaryGate(Func? isPrimary) { if (_otOpcUaNodeManager is null) return false; _otOpcUaNodeManager.HistoryUpdatePrimaryGate = isPrimary; return true; } /// /// Wire the reverse-path gateway for inbound operator writes to writable equipment-tag nodes onto the /// created . The host calls this after start with an /// ActorNodeWriteGateway whose WriteAsync kicks off a bounded Ask of the local /// DriverHostActor (RouteNodeWrite); the node manager calls it fire-and-forget and uses /// the resolved NodeWriteOutcome to self-correct the node on a failed device write (mirrors /// ). Passing null restores the /// NullOpcUaNodeWriteGateway default (writes unavailable). No-op (returns false) when the /// node manager has not been created yet, so the caller can detect a too-early call. /// /// The gateway invoked by the writable node's OnWriteValue handler once the /// WriteOperate gate passes; may be null to restore the Null default. /// true when the gateway was set on a live node manager; false when no node /// manager exists yet. public bool SetNodeWriteGateway(IOpcUaNodeWriteGateway? gateway) { if (_otOpcUaNodeManager is null) return false; // The NodeWriteGateway setter null-coalesces to the Null default, so a null gateway is intentional // (restores "writes unavailable"); forgive the nullable-in here. _otOpcUaNodeManager.NodeWriteGateway = gateway!; return true; } /// /// Wire the server-side HistoryRead backend (the the node /// manager's HistoryRead overrides block-bridge to) onto the created /// . The host calls this after start with the DI-resolved source — /// the NullHistorianDataSource default (GoodNoData-empty reads) or the configured HistorianGateway /// read client. Passing null restores the Null default (the property setter null-coalesces), /// i.e. "no historian". No-op (returns false) when the node manager has not been created yet, /// so the caller can detect a too-early call (mirrors ). /// /// The read backend invoked by the node manager's HistoryRead overrides; may be /// null to restore the Null default (GoodNoData-empty reads). /// true when the source was set on a live node manager; false when no node /// manager exists yet. public bool SetHistorianDataSource(IHistorianDataSource? source) { if (_otOpcUaNodeManager is null) return false; // The HistorianDataSource setter null-coalesces to the Null default, so a null source is intentional // (restores GoodNoData-empty reads); forgive the nullable-in here. _otOpcUaNodeManager.HistorianDataSource = source!; return true; } /// /// Wire the optional HistoryUpdate backend (the the node manager's /// HistoryUpdate overrides block-bridge to) onto the created . Mirrors /// ; null means "this backend cannot modify history". /// /// ⚠️ Call this BEFORE any node materialises: whether an updater is wired decides the /// HistoryWrite AccessLevel bit at materialise time, and the SDK base admits a node to /// HistoryUpdate only when that bit is set. Nodes materialised earlier are not retro-updated. /// /// /// The update backend, or null for none. /// true when set on a live node manager; false when no node manager exists yet. public bool SetHistorianDataUpdater(IHistorianDataUpdater? updater) { if (_otOpcUaNodeManager is null) return false; _otOpcUaNodeManager.HistorianDataUpdater = updater; return true; } /// /// Wire the node's data-plane authorization inputs (#520 stage 4) onto the created /// — the cluster it serves, the current auth generation, and the /// ACL grants. The host calls this after start with the DI-resolved /// , which the artifact-apply path refreshes on every /// deploy. Passing null restores the empty source (authorizes nothing). No-op (returns /// false) when the node manager has not been created yet, so the caller can detect a /// too-early call (mirrors ). /// /// The node's data-plane authorization inputs; may be null to restore the /// empty default. /// true when the source was set on a live node manager; false when no node /// manager exists yet. public bool SetDataPlaneAuthority(IDataPlaneAuthoritySource? source) { if (_otOpcUaNodeManager is null) return false; // The DataPlaneAuthority setter null-coalesces to the empty source, so a null is intentional; // forgive the nullable-in here. _otOpcUaNodeManager.DataPlaneAuthority = source!; return true; } /// /// Wire the node-scoped ACL gate (#520 stage 5) onto the created . /// The host calls this after start; passing null detaches it, which returns the write and /// alarm seams to their server-wide role checks and leaves HistoryRead ungated. No-op (returns /// false) when the node manager has not been created yet, mirroring /// . /// /// The gate, or null to detach. /// true when set on a live node manager; false when none exists yet. public bool SetDataPlaneAuthorizer(DataPlaneAuthorizer? authorizer) { if (_otOpcUaNodeManager is null) return false; _otOpcUaNodeManager.Authorizer = authorizer; return true; } /// /// Gitea #682 — keep the SDK's full set of calculator factories (live-data AggregateFilter subscriptions /// need them, and ServerCapabilities/AggregateFunctions keeps listing all of them), then trim /// HistoryServerCapabilities/AggregateFunctions to the node manager's current historian source. At this /// point that is the Null default (no history aggregates); the Host's /// re-syncs to the configured backend's set right after start. /// /// protected override AggregateManager CreateAggregateManager( IServerInternal server, ApplicationConfiguration configuration) { var manager = base.CreateAggregateManager(server, configuration); _otOpcUaNodeManager?.SyncAdvertisedAggregates(); return manager; } /// protected override MasterNodeManager CreateMasterNodeManager( IServerInternal server, ApplicationConfiguration configuration) { _otOpcUaNodeManager = new OtOpcUaNodeManager(server, configuration, _nodeManagerLogger); return new MasterNodeManager(server, configuration, dynamicNamespaceUri: null, _otOpcUaNodeManager); } }