chore: organize solution into module folders (Core/Server/Drivers/Client/Tooling)
Group all 69 projects into category subfolders under src/ and tests/ so the Rider Solution Explorer mirrors the module structure. Folders: Core, Server, Drivers (with a nested Driver CLIs subfolder), Client, Tooling. - Move every project folder on disk with git mv (history preserved as renames). - Recompute relative paths in 57 .csproj files: cross-category ProjectReferences, the lib/ HintPath+None refs in Driver.Historian.Wonderware, and the external mxaccessgw refs in Driver.Galaxy and its test project. - Rebuild ZB.MOM.WW.OtOpcUa.slnx with nested solution folders. - Re-prefix project paths in functional scripts (e2e, compliance, smoke SQL, integration, install). Build green (0 errors); unit tests pass. Docs left for a separate pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
using System.Collections.Concurrent;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms;
|
||||
|
||||
/// <summary>
|
||||
/// Adapter that exposes <see cref="ScriptedAlarmEngine"/> through the driver-agnostic
|
||||
/// <see cref="IAlarmSource"/> surface. The existing Phase 6.1 <c>AlarmTracker</c>
|
||||
/// composition fan-out consumes this alongside Galaxy / AB CIP / FOCAS alarm
|
||||
/// sources — no per-source branching in the fan-out.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Per Phase 7 plan Stream C.6, ack / confirm / shelve / unshelve are OPC UA
|
||||
/// method calls per-condition. This adapter implements <see cref="AcknowledgeAsync"/>
|
||||
/// from the base interface; the richer Part 9 methods (Confirm / Shelve /
|
||||
/// Unshelve / AddComment) live directly on the engine, invoked from OPC UA
|
||||
/// method handlers wired up in Stream G.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// SubscribeAlarmsAsync takes a list of source-node-id filters (typically an
|
||||
/// Equipment path prefix). When the list is empty every alarm matches. The
|
||||
/// adapter doesn't maintain per-subscription state beyond the filter set — it
|
||||
/// checks each emission against every live subscription.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed class ScriptedAlarmSource : IAlarmSource, IDisposable
|
||||
{
|
||||
private readonly ScriptedAlarmEngine _engine;
|
||||
private readonly ConcurrentDictionary<string, Subscription> _subscriptions
|
||||
= new(StringComparer.Ordinal);
|
||||
private bool _disposed;
|
||||
|
||||
public ScriptedAlarmSource(ScriptedAlarmEngine engine)
|
||||
{
|
||||
_engine = engine ?? throw new ArgumentNullException(nameof(engine));
|
||||
_engine.OnEvent += OnEngineEvent;
|
||||
}
|
||||
|
||||
public event EventHandler<AlarmEventArgs>? OnAlarmEvent;
|
||||
|
||||
public Task<IAlarmSubscriptionHandle> SubscribeAlarmsAsync(
|
||||
IReadOnlyList<string> sourceNodeIds, CancellationToken cancellationToken)
|
||||
{
|
||||
if (sourceNodeIds is null) throw new ArgumentNullException(nameof(sourceNodeIds));
|
||||
var handle = new SubscriptionHandle(Guid.NewGuid().ToString("N"));
|
||||
_subscriptions[handle.DiagnosticId] = new Subscription(handle,
|
||||
new HashSet<string>(sourceNodeIds, StringComparer.Ordinal));
|
||||
return Task.FromResult<IAlarmSubscriptionHandle>(handle);
|
||||
}
|
||||
|
||||
public Task UnsubscribeAlarmsAsync(IAlarmSubscriptionHandle handle, CancellationToken cancellationToken)
|
||||
{
|
||||
if (handle is null) throw new ArgumentNullException(nameof(handle));
|
||||
_subscriptions.TryRemove(handle.DiagnosticId, out _);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task AcknowledgeAsync(
|
||||
IReadOnlyList<AlarmAcknowledgeRequest> acknowledgements, CancellationToken cancellationToken)
|
||||
{
|
||||
if (acknowledgements is null) throw new ArgumentNullException(nameof(acknowledgements));
|
||||
foreach (var a in acknowledgements)
|
||||
{
|
||||
// The base interface doesn't carry a user identity — Stream G provides the
|
||||
// authenticated principal at the OPC UA dispatch layer + proxies through
|
||||
// the engine's richer AcknowledgeAsync. Here we default to "opcua-client"
|
||||
// so callers using the raw IAlarmSource still produce an audit entry.
|
||||
await _engine.AcknowledgeAsync(a.ConditionId, "opcua-client", a.Comment, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEngineEvent(object? sender, ScriptedAlarmEvent evt)
|
||||
{
|
||||
if (_disposed) return;
|
||||
|
||||
foreach (var sub in _subscriptions.Values)
|
||||
{
|
||||
if (!Matches(sub, evt)) continue;
|
||||
var payload = new AlarmEventArgs(
|
||||
SubscriptionHandle: sub.Handle,
|
||||
SourceNodeId: evt.EquipmentPath,
|
||||
ConditionId: evt.AlarmId,
|
||||
AlarmType: evt.Kind.ToString(),
|
||||
Message: evt.Message,
|
||||
Severity: evt.Severity,
|
||||
SourceTimestampUtc: evt.TimestampUtc);
|
||||
try { OnAlarmEvent?.Invoke(this, payload); }
|
||||
catch { /* subscriber exceptions don't crash the adapter */ }
|
||||
}
|
||||
}
|
||||
|
||||
private static bool Matches(Subscription sub, ScriptedAlarmEvent evt)
|
||||
{
|
||||
if (sub.Filter.Count == 0) return true;
|
||||
// A subscription matches if any filter is a prefix of the alarm's equipment
|
||||
// path — typical use is "Enterprise/Site/Area/Line" filtering a whole line.
|
||||
foreach (var f in sub.Filter)
|
||||
{
|
||||
if (evt.EquipmentPath.Equals(f, StringComparison.Ordinal)) return true;
|
||||
if (evt.EquipmentPath.StartsWith(f + "/", StringComparison.Ordinal)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
_engine.OnEvent -= OnEngineEvent;
|
||||
_subscriptions.Clear();
|
||||
}
|
||||
|
||||
private sealed class SubscriptionHandle : IAlarmSubscriptionHandle
|
||||
{
|
||||
public SubscriptionHandle(string id) { DiagnosticId = id; }
|
||||
public string DiagnosticId { get; }
|
||||
}
|
||||
|
||||
private sealed record Subscription(SubscriptionHandle Handle, IReadOnlySet<string> Filter);
|
||||
}
|
||||
Reference in New Issue
Block a user