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
59 lines
2.4 KiB
C#
59 lines
2.4 KiB
C#
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);
|
|
}
|
|
}
|