9cff87fe85
Remove project bookkeeping citations from shipped code comments across the solution: hyphenated task IDs (WP-14, StoreAndForward-025), milestone/task/ issue refs (M3, Task 4, Audit Log #23, #21), Bundle X task-bundle labels, and C/D/K/S/T phase labels. Comment text only — no code logic, string/log literals, or XML-doc structure changed. Genuine descriptions are preserved (only the citation is stripped), and technical lookalikes are retained (UTF-8, SHA-256, T00:00:00, M365, UTC-5, pre-C4/pre-C5 schema versions). Flagged by the new CommentChecker TaskReferenceInComment / TrackingReferenceInComment checks plus targeted grep passes; full solution builds clean, append-only guard tests pass.
324 lines
16 KiB
C#
324 lines
16 KiB
C#
using ZB.MOM.WW.ScadaBridge.Commons.Types;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts;
|
|
|
|
/// <summary>
|
|
/// Scope-aware view onto the instance's attributes, anchored at a path prefix.
|
|
/// <c>Attributes["X"]</c> on the root scope resolves to canonical name "X";
|
|
/// on a composition with prefix "TempSensor" it resolves to "TempSensor.X".
|
|
///
|
|
/// <para>
|
|
/// Thread-model note: the indexer get/set block synchronously
|
|
/// on the Instance Actor Ask (and, for data-connected attributes, the DCL
|
|
/// round-trip). This is safe because script bodies execute on the dedicated
|
|
/// <see cref="ScriptExecutionScheduler"/> threads, not the
|
|
/// shared <see cref="System.Threading.ThreadPool"/> — so a blocked accessor
|
|
/// cannot starve unrelated Akka dispatchers or HTTP request handling. The async
|
|
/// variants (<see cref="GetAsync"/>/<see cref="SetAsync"/>) are still preferred
|
|
/// where the script can await, as they avoid holding a dedicated thread idle for
|
|
/// the duration of each round-trip.
|
|
/// </para>
|
|
/// </summary>
|
|
public class AttributeAccessor
|
|
{
|
|
private readonly ScriptRuntimeContext _ctx;
|
|
|
|
/// <summary>Canonical-name prefix, e.g. "" for root or "TempSensor" for a composition.</summary>
|
|
public string ScopePrefix { get; }
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the AttributeAccessor with the specified context and prefix.
|
|
/// </summary>
|
|
/// <param name="ctx">The script runtime context.</param>
|
|
/// <param name="prefix">The canonical-name prefix for attribute resolution.</param>
|
|
public AttributeAccessor(ScriptRuntimeContext ctx, string prefix)
|
|
{
|
|
_ctx = ctx;
|
|
ScopePrefix = prefix;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolves a key to its full canonical name by applying the scope prefix.
|
|
/// </summary>
|
|
/// <param name="key">The attribute key to resolve.</param>
|
|
/// <returns>The fully qualified canonical name (e.g. "TempSensor.X" or "X" for the root scope).</returns>
|
|
public string Resolve(string key) =>
|
|
ScopePrefix.Length == 0 ? key : ScopePrefix + "." + key;
|
|
|
|
/// <summary>
|
|
/// Gets or sets an attribute value synchronously by canonical name.
|
|
/// </summary>
|
|
/// <param name="key">The attribute key.</param>
|
|
public object? this[string key]
|
|
{
|
|
// Both reads and writes block on the actor Ask; the write also blocks
|
|
// on the DCL round-trip for data-connected attributes. The async
|
|
// variants (GetAsync/SetAsync) are preferred where awaiting is possible.
|
|
get => _ctx.GetAttribute(Resolve(key)).GetAwaiter().GetResult();
|
|
set => _ctx.SetAttribute(Resolve(key), AttributeValueCodec.Encode(value) ?? string.Empty).GetAwaiter().GetResult();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets an attribute value asynchronously.
|
|
/// </summary>
|
|
/// <param name="key">The attribute key.</param>
|
|
/// <returns>A task that resolves to the attribute value, or null if not set.</returns>
|
|
public Task<object?> GetAsync(string key) => _ctx.GetAttribute(Resolve(key));
|
|
|
|
/// <summary>
|
|
/// Sets an attribute value asynchronously.
|
|
/// </summary>
|
|
/// <param name="key">The attribute key.</param>
|
|
/// <param name="value">The value to set.</param>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
public Task SetAsync(string key, object? value)
|
|
=> _ctx.SetAttribute(Resolve(key), AttributeValueCodec.Encode(value) ?? string.Empty);
|
|
|
|
/// <summary>
|
|
/// Writes a SET of data-sourced attributes to the device in ONE gateway batch
|
|
/// round-trip, then writes <paramref name="flagKey"/>=<paramref name="flagValue"/>,
|
|
/// then waits EVENT-DRIVEN (reusing the existing <c>WaitForAttribute</c> waiter — not a
|
|
/// poll) for <paramref name="responseKey"/> to reach <paramref name="responseValue"/>,
|
|
/// bounded by <paramref name="timeout"/>. Replaces N sequential per-attribute writes
|
|
/// with one batched call before the wait. All keys are scope-resolved (<see cref="Resolve"/>)
|
|
/// and all values codec-encoded just like the other accessors. Returns <c>true</c> if
|
|
/// the response was observed within the timeout, <c>false</c> on timeout (no throw on
|
|
/// timeout); throws <see cref="System.InvalidOperationException"/> if the batch/trigger
|
|
/// write itself fails.
|
|
/// </summary>
|
|
/// <param name="values">Attribute key → value to batch-write (keys scope-resolved, values codec-encoded).</param>
|
|
/// <param name="flagKey">Trigger attribute key written AFTER the batch.</param>
|
|
/// <param name="flagValue">Value to write to the trigger.</param>
|
|
/// <param name="responseKey">Attribute key to wait on.</param>
|
|
/// <param name="responseValue">Target value to wait for (<c>null</c> ⇒ any change).</param>
|
|
/// <param name="timeout">How long to wait before returning false.</param>
|
|
/// <returns><c>true</c> on response match within the timeout; <c>false</c> on timeout.</returns>
|
|
public Task<bool> WriteBatchAndWaitAsync(
|
|
IReadOnlyDictionary<string, object?> values, string flagKey, object? flagValue,
|
|
string responseKey, object? responseValue, TimeSpan timeout)
|
|
{
|
|
var encoded = new Dictionary<string, string?>(values.Count);
|
|
foreach (var kv in values)
|
|
encoded[Resolve(kv.Key)] = AttributeValueCodec.Encode(kv.Value);
|
|
|
|
return _ctx.WriteBatchAndWait(
|
|
encoded,
|
|
Resolve(flagKey), AttributeValueCodec.Encode(flagValue),
|
|
Resolve(responseKey), AttributeValueCodec.Encode(responseValue),
|
|
timeout);
|
|
}
|
|
|
|
/// <summary>
|
|
/// WaitForAttribute (spec §3-§5): waits event-driven until the attribute equals
|
|
/// <paramref name="targetValue"/> (value-equality, codec-normalized), bounded by
|
|
/// <paramref name="timeout"/>. Returns <c>true</c> if matched within the timeout,
|
|
/// <c>false</c> on timeout (no throw). Honors the script's execution-timeout token.
|
|
/// Scope/composition path resolution (<see cref="Resolve"/>) is applied just like
|
|
/// <see cref="GetAsync"/> / <see cref="SetAsync"/>.
|
|
///
|
|
/// <para>
|
|
/// <b>Quality-agnostic by default (spec §4.2):</b> matching tests the VALUE, not
|
|
/// the quality — a value arriving at Bad quality still satisfies the wait. Pass
|
|
/// <paramref name="requireGoodQuality"/><c>:true</c> for quality-gated ("Good"-only)
|
|
/// matching: a value reaching the target at Bad/Uncertain quality is ignored and
|
|
/// the wait holds until the target is reached at "Good" quality (or times out).
|
|
/// </para>
|
|
///
|
|
/// <para>
|
|
/// Passing a <b>null</b> <paramref name="targetValue"/> means "match on any change":
|
|
/// the wait then matches the next value the attribute receives — and matches
|
|
/// IMMEDIATELY (fast-path) if the attribute already holds any value at registration.
|
|
/// </para>
|
|
/// </summary>
|
|
/// <param name="key">The attribute key (scope-resolved before the wait is registered).</param>
|
|
/// <param name="targetValue">
|
|
/// The value to wait for (codec-encoded for comparison); <c>null</c> means
|
|
/// "match on any change" (matches immediately if the attribute already has a value).
|
|
/// </param>
|
|
/// <param name="timeout">How long to wait before returning false.</param>
|
|
/// <param name="requireGoodQuality">
|
|
/// <c>true</c> for quality-gated ("Good"-only) matching (spec §4.2); defaults to
|
|
/// <c>false</c> (quality-agnostic — Bad/Uncertain-quality transients still match).
|
|
/// </param>
|
|
/// <returns><c>true</c> on match within the timeout; <c>false</c> on timeout.</returns>
|
|
public Task<bool> WaitAsync(string key, object? targetValue, TimeSpan timeout, bool requireGoodQuality = false)
|
|
=> _ctx.WaitAttribute(Resolve(key), AttributeValueCodec.Encode(targetValue), null, timeout, requireGoodQuality);
|
|
|
|
/// <summary>
|
|
/// WaitForAttribute (spec §3-§5): predicate form — waits event-driven until
|
|
/// <paramref name="predicate"/> returns <c>true</c> for the attribute's current
|
|
/// value, bounded by <paramref name="timeout"/>. Site-local only (the predicate
|
|
/// is an in-process delegate). Returns <c>true</c> if matched within the timeout,
|
|
/// <c>false</c> on timeout (no throw). Scope/composition path resolution applies.
|
|
///
|
|
/// <para>
|
|
/// <b>Quality-agnostic by default (spec §4.2):</b> the predicate is tested against
|
|
/// the VALUE, regardless of quality — a value arriving at Bad quality still
|
|
/// satisfies the wait if the predicate passes. Pass <paramref name="requireGoodQuality"/>
|
|
/// <c>:true</c> for quality-gated ("Good"-only) matching: a value satisfying the
|
|
/// predicate at Bad/Uncertain quality is ignored until it does so at "Good" quality.
|
|
/// </para>
|
|
/// </summary>
|
|
/// <param name="key">The attribute key (scope-resolved before the wait is registered).</param>
|
|
/// <param name="predicate">The site-local predicate tested against the current value.</param>
|
|
/// <param name="timeout">How long to wait before returning false.</param>
|
|
/// <param name="requireGoodQuality">
|
|
/// <c>true</c> for quality-gated ("Good"-only) matching (spec §4.2); defaults to
|
|
/// <c>false</c> (quality-agnostic).
|
|
/// </param>
|
|
/// <returns><c>true</c> on match within the timeout; <c>false</c> on timeout.</returns>
|
|
public Task<bool> WaitAsync(string key, Func<object?, bool> predicate, TimeSpan timeout, bool requireGoodQuality = false)
|
|
=> _ctx.WaitAttribute(Resolve(key), null, predicate, timeout, requireGoodQuality);
|
|
|
|
/// <summary>
|
|
/// WaitForAttribute (spec §3): richer value-equality form — like
|
|
/// <see cref="WaitAsync(string, object?, TimeSpan, bool)"/> but returns the full
|
|
/// <see cref="WaitResult"/> (matched flag + matched value + quality + timed-out
|
|
/// flag) instead of a bare bool. Scope/composition path resolution
|
|
/// (<see cref="Resolve"/>) is applied to <paramref name="key"/> just like the
|
|
/// other accessors. Never throws on timeout — a timeout yields
|
|
/// <c>WaitResult { Matched = false, TimedOut = true }</c>.
|
|
/// </summary>
|
|
/// <param name="key">The attribute key (scope-resolved before the wait is registered).</param>
|
|
/// <param name="targetValue">
|
|
/// The value to wait for (codec-encoded for comparison); <c>null</c> means
|
|
/// "match on any change".
|
|
/// </param>
|
|
/// <param name="timeout">How long to wait before returning a timed-out result.</param>
|
|
/// <param name="requireGoodQuality">
|
|
/// <c>true</c> for quality-gated ("Good"-only) matching (spec §4.2); defaults to <c>false</c>.
|
|
/// </param>
|
|
/// <returns>The full <see cref="WaitResult"/> for the wait.</returns>
|
|
public Task<WaitResult> WaitForAsync(string key, object? targetValue, TimeSpan timeout, bool requireGoodQuality = false)
|
|
=> _ctx.WaitAttributeFull(Resolve(key), AttributeValueCodec.Encode(targetValue), null, timeout, requireGoodQuality);
|
|
|
|
/// <summary>
|
|
/// WaitForAttribute (spec §3): richer predicate form — like
|
|
/// <see cref="WaitAsync(string, Func{object?, bool}, TimeSpan, bool)"/> but returns
|
|
/// the full <see cref="WaitResult"/>. Site-local only (the predicate is an
|
|
/// in-process delegate). Scope/composition path resolution applies. Never throws
|
|
/// on timeout (<c>WaitResult { Matched = false, TimedOut = true }</c>).
|
|
/// </summary>
|
|
/// <param name="key">The attribute key (scope-resolved before the wait is registered).</param>
|
|
/// <param name="predicate">The site-local predicate tested against the current value.</param>
|
|
/// <param name="timeout">How long to wait before returning a timed-out result.</param>
|
|
/// <param name="requireGoodQuality">
|
|
/// <c>true</c> for quality-gated ("Good"-only) matching (spec §4.2); defaults to <c>false</c>.
|
|
/// </param>
|
|
/// <returns>The full <see cref="WaitResult"/> for the wait.</returns>
|
|
public Task<WaitResult> WaitForAsync(string key, Func<object?, bool> predicate, TimeSpan timeout, bool requireGoodQuality = false)
|
|
=> _ctx.WaitAttributeFull(Resolve(key), null, predicate, timeout, requireGoodQuality);
|
|
}
|
|
|
|
/// <summary>
|
|
/// A view of one composition at a path. Exposes its attributes via
|
|
/// <see cref="AttributeAccessor"/> and an invokable <c>CallScript</c>.
|
|
/// </summary>
|
|
public class CompositionAccessor
|
|
{
|
|
private readonly ScriptRuntimeContext _ctx;
|
|
|
|
/// <summary>Canonical-name path this composition is rooted at.</summary>
|
|
public string Path { get; }
|
|
|
|
/// <summary>
|
|
/// Accessor for attributes within this composition.
|
|
/// </summary>
|
|
public AttributeAccessor Attributes { get; }
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the CompositionAccessor with the specified context and path.
|
|
/// </summary>
|
|
/// <param name="ctx">The script runtime context.</param>
|
|
/// <param name="path">The canonical-name path for the composition.</param>
|
|
public CompositionAccessor(ScriptRuntimeContext ctx, string path)
|
|
{
|
|
_ctx = ctx;
|
|
Path = path;
|
|
Attributes = new AttributeAccessor(ctx, path);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolves a script name to its full canonical name by applying the composition path.
|
|
/// </summary>
|
|
/// <param name="scriptName">The script name to resolve.</param>
|
|
/// <returns>The fully qualified canonical script name (e.g. "Module.MyScript" or "MyScript" at root).</returns>
|
|
public string ResolveScript(string scriptName) =>
|
|
Path.Length == 0 ? scriptName : Path + "." + scriptName;
|
|
|
|
/// <summary>
|
|
/// Calls a script within this composition.
|
|
/// </summary>
|
|
/// <param name="scriptName">The name of the script to call.</param>
|
|
/// <param name="parameters">Optional parameters to pass to the script.</param>
|
|
/// <returns>A task that resolves to the script's return value, or null if none.</returns>
|
|
public Task<object?> CallScript(string scriptName, object? parameters = null)
|
|
=> _ctx.CallScript(ResolveScript(scriptName), parameters);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Dictionary-style accessor for the script's child compositions. Indexing
|
|
/// returns a <see cref="CompositionAccessor"/> rooted at the child's path.
|
|
/// </summary>
|
|
public class ChildrenAccessor
|
|
{
|
|
private readonly ScriptRuntimeContext _ctx;
|
|
private readonly string _selfPath;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the ChildrenAccessor with the specified context and path.
|
|
/// </summary>
|
|
/// <param name="ctx">The script runtime context.</param>
|
|
/// <param name="selfPath">The canonical-name path of the parent composition.</param>
|
|
public ChildrenAccessor(ScriptRuntimeContext ctx, string selfPath)
|
|
{
|
|
_ctx = ctx;
|
|
_selfPath = selfPath;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a composition accessor for a child by name.
|
|
/// </summary>
|
|
/// <param name="compositionName">The name of the child composition.</param>
|
|
public CompositionAccessor this[string compositionName]
|
|
{
|
|
get
|
|
{
|
|
var path = _selfPath.Length == 0
|
|
? compositionName
|
|
: _selfPath + "." + compositionName;
|
|
return new CompositionAccessor(_ctx, path);
|
|
}
|
|
}
|
|
}
|
|
|
|
internal static class ScopeAccessorFactory
|
|
{
|
|
/// <summary>
|
|
/// Creates an AttributeAccessor for the specified context and path.
|
|
/// </summary>
|
|
/// <param name="ctx">The script runtime context.</param>
|
|
/// <param name="selfPath">The canonical-name path.</param>
|
|
/// <returns>A new <see cref="AttributeAccessor"/> rooted at <paramref name="selfPath"/>.</returns>
|
|
public static AttributeAccessor AttributesFor(ScriptRuntimeContext ctx, string selfPath)
|
|
=> new(ctx, selfPath);
|
|
|
|
/// <summary>
|
|
/// Creates a ChildrenAccessor for the specified context and path.
|
|
/// </summary>
|
|
/// <param name="ctx">The script runtime context.</param>
|
|
/// <param name="selfPath">The canonical-name path.</param>
|
|
/// <returns>A new <see cref="ChildrenAccessor"/> rooted at <paramref name="selfPath"/>.</returns>
|
|
public static ChildrenAccessor ChildrenFor(ScriptRuntimeContext ctx, string selfPath)
|
|
=> new(ctx, selfPath);
|
|
|
|
/// <summary>
|
|
/// Creates a CompositionAccessor for the specified context and path, or null if no parent exists.
|
|
/// </summary>
|
|
/// <param name="ctx">The script runtime context.</param>
|
|
/// <param name="parentPath">The parent path, or null if no parent.</param>
|
|
/// <returns>A <see cref="CompositionAccessor"/> for the parent, or null when <paramref name="parentPath"/> is null.</returns>
|
|
public static CompositionAccessor? ParentFor(ScriptRuntimeContext ctx, string? parentPath)
|
|
=> parentPath == null ? null : new CompositionAccessor(ctx, parentPath);
|
|
}
|