119 lines
5.4 KiB
C#
119 lines
5.4 KiB
C#
using System.Collections.Concurrent;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Core.OpcUa;
|
|
|
|
/// <summary>
|
|
/// Generic, driver-agnostic backbone for populating the OPC UA address space from an
|
|
/// <see cref="IDriver"/>. Walks the driver's discovery, wires the alarm + data-change +
|
|
/// rediscovery subscription events, and hands each variable to the supplied
|
|
/// <see cref="IAddressSpaceBuilder"/>. Concrete OPC UA server implementations provide the
|
|
/// builder — see the Server project's <c>OpcUaAddressSpaceBuilder</c> for the materialization
|
|
/// against <c>CustomNodeManager2</c>.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Per <c>docs/v2/plan.md</c> decision #52 + #62 — Core owns the node tree, drivers stream
|
|
/// <c>Folder</c>/<c>Variable</c> calls, alarm-bearing variables are annotated via
|
|
/// <see cref="IVariableHandle.MarkAsAlarmCondition"/> and subsequent
|
|
/// <see cref="IAlarmSource.OnAlarmEvent"/> payloads route to the sink the builder returned.
|
|
/// </remarks>
|
|
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<string, IAlarmConditionSink> _alarmSinks =
|
|
new(StringComparer.OrdinalIgnoreCase);
|
|
|
|
private EventHandler<AlarmEventArgs>? _alarmForwarder;
|
|
private bool _disposed;
|
|
|
|
/// <summary>
|
|
/// 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 <c>SourceNodeId</c>.
|
|
/// Driver exceptions are isolated per decision #12 — the driver's subtree is marked Faulted,
|
|
/// but other drivers remain available.
|
|
/// </summary>
|
|
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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Snapshot the current alarm-sink registry by source node id. Diagnostic + test hook;
|
|
/// not part of the hot path.
|
|
/// </summary>
|
|
internal IReadOnlyCollection<string> TrackedAlarmSources => _alarmSinks.Keys.ToList();
|
|
|
|
/// <summary>
|
|
/// Wraps the caller-supplied <see cref="IAddressSpaceBuilder"/> so every
|
|
/// <see cref="IVariableHandle.MarkAsAlarmCondition"/> call registers the returned sink in
|
|
/// the node manager's source-node-id map. The builder itself drives materialization;
|
|
/// this wrapper only observes.
|
|
/// </summary>
|
|
private sealed class CapturingBuilder(
|
|
IAddressSpaceBuilder inner,
|
|
ConcurrentDictionary<string, IAlarmConditionSink> 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<string, IAlarmConditionSink> 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;
|
|
}
|
|
}
|
|
}
|