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; /// /// Static factory registration helper for . The Server's /// DriverFactoryBootstrap calls once at startup; the bootstrapper then /// materialises Calculation DriverInstance rows into live driver instances. Mirrors /// ModbusDriverFactoryExtensions, with the addition of a so a /// script failure fans out onto the script-logs topic (via the root logger's /// ScriptLogTopicSink) — the calc driver has no Akka access of its own. /// public static class CalculationDriverFactoryExtensions { /// The DriverInstance.DriverType string this factory registers under. public const string DriverTypeName = "Calculation"; private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNameCaseInsensitive = true, ReadCommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true, Converters = { new JsonStringEnumConverter() }, }; /// /// Register the Calculation factory with the driver registry. The optional /// + are captured at registration /// time; both may be null (the guard test invokes Register(registry) reflectively with the /// trailing parameters defaulted), in which case the driver runs with the null logger + a no-op /// script-logger. /// /// The driver factory registry to register with. /// Optional logger factory for per-instance diagnostics loggers. /// Optional root script logger (fans failure entries onto the script-logs topic). 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)); } /// Public overload for the Server-side bootstrapper + test consumers. /// The unique identifier for the driver instance. /// The merged driver config JSON (carries the RawTags array). /// The constructed . public static CalculationDriver CreateInstance(string driverInstanceId, string driverConfigJson) => CreateInstance(driverInstanceId, driverConfigJson, loggerFactory: null, scriptRoot: null); /// Logger/script-logger-aware overload — used by 's closure via DI. /// The unique identifier for the driver instance. /// The merged driver config JSON (carries the RawTags array). /// Optional logger factory for per-instance diagnostics loggers. /// Optional root script logger for failure fan-out. /// The constructed . public static CalculationDriver CreateInstance( string driverInstanceId, string driverConfigJson, ILoggerFactory? loggerFactory, ScriptRootLogger? scriptRoot) { ArgumentException.ThrowIfNullOrWhiteSpace(driverInstanceId); ArgumentException.ThrowIfNullOrWhiteSpace(driverConfigJson); var dto = JsonSerializer.Deserialize(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()); } /// 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). internal sealed class CalculationDriverConfigDto { /// The driver's authored raw calc tags (RawPath + TagConfig blob + WriteIdempotent + DeviceName). public List? RawTags { get; init; } /// Optional per-script evaluation wall-clock budget override, in milliseconds (default 2000). public int? RunTimeoutMs { get; init; } } }