using System.Collections.Concurrent;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
namespace ZB.MOM.WW.OtOpcUa.Core.OpcUa;
///
/// Generic, driver-agnostic backbone for populating the OPC UA address space from an
/// . Walks the driver's discovery, wires the alarm + data-change +
/// rediscovery subscription events, and hands each variable to the supplied
/// . Concrete OPC UA server implementations provide the
/// builder — see the Server project's OpcUaAddressSpaceBuilder for the materialization
/// against CustomNodeManager2.
///
///
/// Per docs/v2/plan.md decision #52 + #62 — Core owns the node tree, drivers stream
/// Folder/Variable calls, alarm-bearing variables are annotated via
/// and subsequent
/// payloads route to the sink the builder returned.
///
public class GenericDriverNodeManager(IDriver driver) : IDisposable
{
protected IDriver Driver { get; } = driver ?? throw new ArgumentNullException(nameof(driver));
public string DriverInstanceId => Driver.DriverInstanceId;
// Source tag (DriverAttributeInfo.FullName) → alarm-condition sink. Populated during
// BuildAddressSpaceAsync by a recording IAddressSpaceBuilder implementation that captures the
// IVariableHandle per attr.IsAlarm=true variable and calls MarkAsAlarmCondition.
private readonly ConcurrentDictionary _alarmSinks =
new(StringComparer.OrdinalIgnoreCase);
private EventHandler? _alarmForwarder;
private bool _disposed;
///
/// Populates the address space by streaming nodes from the driver into the supplied builder,
/// wraps the builder so alarm-condition sinks are captured, subscribes to the driver's
/// alarm event stream, and routes each transition to the matching sink by SourceNodeId.
/// Driver exceptions are isolated per decision #12 — the driver's subtree is marked Faulted,
/// but other drivers remain available.
///
public async Task BuildAddressSpaceAsync(IAddressSpaceBuilder builder, CancellationToken ct)
{
ArgumentNullException.ThrowIfNull(builder);
if (Driver is not ITagDiscovery discovery)
throw new NotSupportedException($"Driver '{Driver.DriverInstanceId}' does not implement ITagDiscovery.");
var capturing = new CapturingBuilder(builder, _alarmSinks);
await discovery.DiscoverAsync(capturing, ct);
if (Driver is IAlarmSource alarmSource)
{
_alarmForwarder = (_, e) =>
{
// Route the alarm to the sink registered for the originating variable, if any.
// Unknown source ids are dropped silently — they may belong to another driver or
// to a variable the builder chose not to flag as an alarm condition.
if (_alarmSinks.TryGetValue(e.SourceNodeId, out var sink))
sink.OnTransition(e);
};
alarmSource.OnAlarmEvent += _alarmForwarder;
}
}
public void Dispose()
{
if (_disposed) return;
_disposed = true;
if (_alarmForwarder is not null && Driver is IAlarmSource alarmSource)
{
alarmSource.OnAlarmEvent -= _alarmForwarder;
}
_alarmSinks.Clear();
}
///
/// Snapshot the current alarm-sink registry by source node id. Diagnostic + test hook;
/// not part of the hot path.
///
internal IReadOnlyCollection TrackedAlarmSources => _alarmSinks.Keys.ToList();
///
/// Wraps the caller-supplied so every
/// call registers the returned sink in
/// the node manager's source-node-id map. The builder itself drives materialization;
/// this wrapper only observes.
///
private sealed class CapturingBuilder(
IAddressSpaceBuilder inner,
ConcurrentDictionary sinks) : IAddressSpaceBuilder
{
public IAddressSpaceBuilder Folder(string browseName, string displayName)
=> new CapturingBuilder(inner.Folder(browseName, displayName), sinks);
public IVariableHandle Variable(string browseName, string displayName, DriverAttributeInfo attributeInfo)
=> new CapturingHandle(inner.Variable(browseName, displayName, attributeInfo), sinks);
public void AddProperty(string browseName, DriverDataType dataType, object? value)
=> inner.AddProperty(browseName, dataType, value);
}
private sealed class CapturingHandle(
IVariableHandle inner,
ConcurrentDictionary sinks) : IVariableHandle
{
public string FullReference => inner.FullReference;
public IAlarmConditionSink MarkAsAlarmCondition(AlarmConditionInfo info)
{
var sink = inner.MarkAsAlarmCondition(info);
// Register by the driver-side full reference so the alarm forwarder can look it up
// using AlarmEventArgs.SourceNodeId (which the driver populates with the same tag).
sinks[inner.FullReference] = sink;
return sink;
}
}
}