Phase 7 follow-up #245 — ScriptedAlarmReadable adapter over engine state
Task #245 — exposes each scripted alarm's current ActiveState as IReadable so OPC UA variable reads on Source=ScriptedAlarm nodes return the live predicate truth instead of BadNotFound. ## ScriptedAlarmReadable Wraps ScriptedAlarmEngine + implements IReadable: - Known alarm + Active → DataValueSnapshot(true, Good) - Known alarm + Inactive → DataValueSnapshot(false, Good) - Unknown alarm id → DataValueSnapshot(null, BadNodeIdUnknown) — surfaces misconfiguration rather than silently reading false - Batch reads preserve request order Phase7EngineComposer.Compose now returns this as ScriptedAlarmReadable when ScriptedAlarm rows are present. ScriptedAlarmSource (IAlarmSource for the event stream) stays in place — the IReadable is a separate adapter over the same engine. ## Tests — 6 new + 1 updated composer test = 19 total Phase 7 tests ScriptedAlarmReadableTests covers: inactive + active predicate → bool snapshot, unknown alarm id → BadNodeIdUnknown, batch order preservation, null-engine + null-fullReferences guards. The active-predicate test uses ctx.GetTag on a seeded upstream value to drive a real cascade through the engine. Updated Phase7EngineComposerTests to assert ScriptedAlarmReadable is non-null when alarms compose, null when only virtual tags. ## Follow-ups remaining - #244 — driver-bridge feed populating CachedTagUpstreamSource - #246 — Program.cs Compose call + SqliteStoreAndForwardSink lifecycle
This commit is contained in:
@@ -73,7 +73,7 @@ public static class Phase7EngineComposer
|
||||
disposables.Add(vtEngine);
|
||||
}
|
||||
|
||||
ScriptedAlarmSource? alarmSource = null;
|
||||
IReadable? alarmReadable = null;
|
||||
if (scriptedAlarms.Count > 0)
|
||||
{
|
||||
var alarmDefs = ProjectScriptedAlarms(scriptedAlarms, scriptById).ToList();
|
||||
@@ -83,17 +83,17 @@ public static class Phase7EngineComposer
|
||||
var engineLogger = loggerFactory.CreateLogger("Phase7HistorianRouter");
|
||||
alarmEngine.OnEvent += (_, e) => _ = RouteToHistorianAsync(e, historianSink, engineLogger);
|
||||
alarmEngine.LoadAsync(alarmDefs, CancellationToken.None).GetAwaiter().GetResult();
|
||||
alarmSource = new ScriptedAlarmSource(alarmEngine);
|
||||
var alarmSource = new ScriptedAlarmSource(alarmEngine);
|
||||
// Task #245 — expose each alarm's current Active state as IReadable so OPC UA
|
||||
// variable reads on Source=ScriptedAlarm nodes return the live predicate truth
|
||||
// instead of BadNotFound. ScriptedAlarmSource stays registered as IAlarmSource
|
||||
// for the event stream; the IReadable is a separate adapter over the same engine.
|
||||
alarmReadable = new ScriptedAlarmReadable(alarmEngine);
|
||||
disposables.Add(alarmEngine);
|
||||
disposables.Add(alarmSource);
|
||||
}
|
||||
|
||||
// ScriptedAlarmSource is an IAlarmSource, not an IReadable — scripted-alarm
|
||||
// variable-read dispatch (task #245) needs a dedicated engine-state adapter. Until
|
||||
// that ships, reads against Source=ScriptedAlarm nodes return BadNotFound per the
|
||||
// DriverNodeManager null-check path (the ADR-002 "misconfiguration not silent
|
||||
// fallback" signal). The alarm event stream still fires via IAlarmSource.
|
||||
return new Phase7ComposedSources(vtSource, ScriptedAlarmReadable: null, disposables);
|
||||
return new Phase7ComposedSources(vtSource, alarmReadable, disposables);
|
||||
}
|
||||
|
||||
internal static IEnumerable<VirtualTagDefinition> ProjectVirtualTags(
|
||||
|
||||
58
src/ZB.MOM.WW.OtOpcUa.Server/Phase7/ScriptedAlarmReadable.cs
Normal file
58
src/ZB.MOM.WW.OtOpcUa.Server/Phase7/ScriptedAlarmReadable.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Server.Phase7;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="IReadable"/> adapter exposing each scripted alarm's current
|
||||
/// <see cref="AlarmActiveState"/> as an OPC UA boolean. Phase 7 follow-up (task #245).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Paired with the <see cref="NodeSourceKind.ScriptedAlarm"/> dispatch in
|
||||
/// <c>DriverNodeManager.OnReadValue</c>. Full-reference lookup is the
|
||||
/// <c>ScriptedAlarmId</c> the walker wrote into <c>DriverAttributeInfo.FullName</c>
|
||||
/// when emitting the alarm variable node.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Unknown alarm ids return <c>BadNodeIdUnknown</c> so misconfiguration surfaces
|
||||
/// instead of silently reading <c>false</c>. Alarms whose predicate has never
|
||||
/// been evaluated (brand new, before the engine's first cascade tick) report
|
||||
/// <see cref="AlarmActiveState.Inactive"/> via <see cref="AlarmConditionState.Fresh"/>,
|
||||
/// which matches the Part 9 initial-state semantics.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed class ScriptedAlarmReadable : IReadable
|
||||
{
|
||||
/// <summary>OPC UA <c>StatusCodes.BadNodeIdUnknown</c> — kept local so we don't pull the OPC stack.</summary>
|
||||
private const uint BadNodeIdUnknown = 0x80340000;
|
||||
|
||||
private readonly ScriptedAlarmEngine _engine;
|
||||
|
||||
public ScriptedAlarmReadable(ScriptedAlarmEngine engine)
|
||||
{
|
||||
_engine = engine ?? throw new ArgumentNullException(nameof(engine));
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<DataValueSnapshot>> ReadAsync(
|
||||
IReadOnlyList<string> fullReferences, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(fullReferences);
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var results = new DataValueSnapshot[fullReferences.Count];
|
||||
for (var i = 0; i < fullReferences.Count; i++)
|
||||
{
|
||||
var alarmId = fullReferences[i];
|
||||
var state = _engine.GetState(alarmId);
|
||||
if (state is null)
|
||||
{
|
||||
results[i] = new DataValueSnapshot(null, BadNodeIdUnknown, null, now);
|
||||
continue;
|
||||
}
|
||||
var active = state.Active == AlarmActiveState.Active;
|
||||
results[i] = new DataValueSnapshot(active, 0u, now, now);
|
||||
}
|
||||
return Task.FromResult<IReadOnlyList<DataValueSnapshot>>(results);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user