53d222e0f7
Adds the Calculation pseudo-driver (tags computed by C# scripts over other tags' live values) plus the host seam that feeds it: - IDependencyConsumer capability interface (Core.Abstractions) + DriverTypeNames.Calculation. - CalculationDriver : IDriver, ISubscribable, IReadable, IDependencyConsumer — change-gate + dedupe (VirtualTagActor parity), timer-group scheduler, Bad-transition + script-log emission, Good recovery; reuses the T0-4 CalculationEvaluator. - DriverHostActor spawns a DependencyConsumerMuxAdapter per dependency-consuming driver, registering its refs on the per-node DependencyMuxActor and forwarding DependencyValueChanged → OnDependencyValue; re-registers on every apply, torn down with the driver. - Factory + probe registered in DriverFactoryBootstrap (keeps the T0-3 guard green); guard test csproj references the driver so the bin-scan discovers the factory. - DeploymentArtifact injects the resolved scriptSource (scriptId → SourceCode) into a calc tag's delivered TagConfig so the host-blind driver can compile it. - DraftValidator deploy gates: CalculationScriptMissing / CalculationScriptNotFound (scriptId existence) + CalculationDependencyCycle (DependencyGraph.DetectCycles over calc→calc edges). - Tests: driver units (dep-ref extraction, change-gate, dedupe, Bad+script-log+recovery, timer, read), tag-config parsing, DraftValidator gates (self/2-cycle/cross-driver-terminal), and a Runtime integration test proving values FLOW mux → adapter → driver → calc-of-calc end-to-end. Claude-Session: https://claude.ai/code/session_01LVneM3eh1UtJxEisFXgmox
97 lines
5.2 KiB
C#
97 lines
5.2 KiB
C#
using System.Text.Json;
|
|
using System.Text.Json.Serialization;
|
|
using Microsoft.Extensions.Logging;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Hosting;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Scripting;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.Calculation;
|
|
|
|
/// <summary>
|
|
/// Static factory registration helper for <see cref="CalculationDriver"/>. The Server's
|
|
/// <c>DriverFactoryBootstrap</c> calls <see cref="Register"/> once at startup; the bootstrapper then
|
|
/// materialises <c>Calculation</c> DriverInstance rows into live driver instances. Mirrors
|
|
/// <c>ModbusDriverFactoryExtensions</c>, with the addition of a <see cref="ScriptRootLogger"/> so a
|
|
/// script failure fans out onto the <c>script-logs</c> topic (via the root logger's
|
|
/// <c>ScriptLogTopicSink</c>) — the calc driver has no Akka access of its own.
|
|
/// </summary>
|
|
public static class CalculationDriverFactoryExtensions
|
|
{
|
|
/// <summary>The <c>DriverInstance.DriverType</c> string this factory registers under.</summary>
|
|
public const string DriverTypeName = "Calculation";
|
|
|
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
|
{
|
|
PropertyNameCaseInsensitive = true,
|
|
ReadCommentHandling = JsonCommentHandling.Skip,
|
|
AllowTrailingCommas = true,
|
|
Converters = { new JsonStringEnumConverter() },
|
|
};
|
|
|
|
/// <summary>
|
|
/// Register the Calculation factory with the driver registry. The optional
|
|
/// <paramref name="loggerFactory"/> + <paramref name="scriptRoot"/> are captured at registration
|
|
/// time; both may be null (the guard test invokes <c>Register(registry)</c> reflectively with the
|
|
/// trailing parameters defaulted), in which case the driver runs with the null logger + a no-op
|
|
/// script-logger.
|
|
/// </summary>
|
|
/// <param name="registry">The driver factory registry to register with.</param>
|
|
/// <param name="loggerFactory">Optional logger factory for per-instance diagnostics loggers.</param>
|
|
/// <param name="scriptRoot">Optional root script logger (fans failure entries onto the <c>script-logs</c> topic).</param>
|
|
public static void Register(
|
|
DriverFactoryRegistry registry,
|
|
ILoggerFactory? loggerFactory = null,
|
|
ScriptRootLogger? scriptRoot = null)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(registry);
|
|
registry.Register(DriverTypeName, (id, json) => CreateInstance(id, json, loggerFactory, scriptRoot));
|
|
}
|
|
|
|
/// <summary>Public overload for the Server-side bootstrapper + test consumers.</summary>
|
|
/// <param name="driverInstanceId">The unique identifier for the driver instance.</param>
|
|
/// <param name="driverConfigJson">The merged driver config JSON (carries the <c>RawTags</c> array).</param>
|
|
/// <returns>The constructed <see cref="CalculationDriver"/>.</returns>
|
|
public static CalculationDriver CreateInstance(string driverInstanceId, string driverConfigJson)
|
|
=> CreateInstance(driverInstanceId, driverConfigJson, loggerFactory: null, scriptRoot: null);
|
|
|
|
/// <summary>Logger/script-logger-aware overload — used by <see cref="Register"/>'s closure via DI.</summary>
|
|
/// <param name="driverInstanceId">The unique identifier for the driver instance.</param>
|
|
/// <param name="driverConfigJson">The merged driver config JSON (carries the <c>RawTags</c> array).</param>
|
|
/// <param name="loggerFactory">Optional logger factory for per-instance diagnostics loggers.</param>
|
|
/// <param name="scriptRoot">Optional root script logger for failure fan-out.</param>
|
|
/// <returns>The constructed <see cref="CalculationDriver"/>.</returns>
|
|
public static CalculationDriver CreateInstance(
|
|
string driverInstanceId, string driverConfigJson,
|
|
ILoggerFactory? loggerFactory, ScriptRootLogger? scriptRoot)
|
|
{
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(driverInstanceId);
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(driverConfigJson);
|
|
|
|
var dto = JsonSerializer.Deserialize<CalculationDriverConfigDto>(driverConfigJson, JsonOptions)
|
|
?? throw new InvalidOperationException(
|
|
$"Calculation driver config for '{driverInstanceId}' deserialised to null");
|
|
|
|
var options = new CalculationDriverOptions
|
|
{
|
|
RawTags = dto.RawTags is { Count: > 0 } ? [.. dto.RawTags] : [],
|
|
RunTimeout = dto.RunTimeoutMs is { } ms && ms > 0
|
|
? TimeSpan.FromMilliseconds(ms) : TimeSpan.FromSeconds(2),
|
|
};
|
|
|
|
return new CalculationDriver(
|
|
options, driverInstanceId, scriptRoot,
|
|
logger: loggerFactory?.CreateLogger<CalculationDriver>());
|
|
}
|
|
|
|
/// <summary>The merged-config DTO the calc factory binds from. The Calculation driver has no
|
|
/// backend/endpoint config — only the injected raw tags (+ an optional evaluation-timeout override).</summary>
|
|
internal sealed class CalculationDriverConfigDto
|
|
{
|
|
/// <summary>The driver's authored raw calc tags (RawPath + TagConfig blob + WriteIdempotent + DeviceName).</summary>
|
|
public List<RawTagEntry>? RawTags { get; init; }
|
|
|
|
/// <summary>Optional per-script evaluation wall-clock budget override, in milliseconds (default 2000).</summary>
|
|
public int? RunTimeoutMs { get; init; }
|
|
}
|
|
}
|