cf03ca279d
- v3 design finalized: all 7 open items resolved (live historian probe:
255-char tagname limit; SDK multi-notifier spike: native AddNotifier,
never duplicate ReportEvent; NamespaceKind.Simulated retired; CSV column
dictionaries; ScadaBridge re-bind sized; rename-warning scan decided),
reconciled with the pre-v3 universal discovery browser (its new §11
v3 forward note re-targets the browse commit contract).
- Calculation pseudo-driver mini-design (IDependencyConsumer capability,
mux-fed change/timer triggers, Tarjan cycle deploy gate, VT-parity
error semantics).
- Implementation plans for Opus-agent execution, grounded against exact
files/symbols: master plan (batch DAG, worktree-isolated parallel
waves, contract-first fan-out, global gotcha list) + one plan per
batch (1 schema+RawPath identity, 2 /raw UI+Calculation, 3 UNS
reference-only+{{equip}}, 4 dual-namespace address space = v3.0),
each with per-wave file ownership and a mandatory docker-dev live gate.
162 lines
9.1 KiB
Markdown
162 lines
9.1 KiB
Markdown
# `Calculation` pseudo-driver — mini-design (v3 task #11)
|
|
|
|
**Date:** 2026-07-15
|
|
**Status:** design complete (resolves v3 open item "Calculation driver mini-design").
|
|
**Parent:** `docs/plans/2026-07-15-raw-uns-two-subtree-v3-design.md` §"Calculated tags (two-tier)".
|
|
|
|
Signal-level calculated tags are ordinary **raw tags** bound to a `Calculation` driver. They
|
|
inherit the entire raw pipeline (folders, tag-groups, CSV, historization, native alarms, UNS
|
|
references) with no special-casing. This doc pins the runtime mechanism, storage, triggers,
|
|
gates, and error semantics.
|
|
|
|
## 1. Driver shape
|
|
|
|
`CalculationDriver : IDriver, ISubscribable, IReadable` in a new
|
|
`src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Calculation/` project.
|
|
|
|
- **Not `IWritable`** — calc tags are computed sources; author them read-only
|
|
(`AccessLevel` enforced at authoring + CSV validation).
|
|
- **Not `ITagDiscovery`** — tags are authored, never discovered
|
|
(`SupportsOnlineDiscovery` default false ⇒ no Browse button, manual/CSV entry only —
|
|
consistent with the universal-browser gate).
|
|
- `IReadable.ReadAsync` returns the last computed snapshot per ref
|
|
(`BadWaitingForInitialData` before first evaluation).
|
|
- Registration: `CalculationDriverFactoryExtensions.DriverTypeName = "Calculation"` + one
|
|
line in `DriverFactoryBootstrap.AddOtOpcUaDriverFactories` and one `TryAddEnumerable` in
|
|
`AddOtOpcUaDriverProbes` — the standard pattern. *(Note: the live registration seam is
|
|
`DriverFactoryRegistry`; `DriverTypeRegistry` (metadata/JSON-schema) is vestigial — nothing
|
|
in `src/` calls its `Register`.)*
|
|
- Devices: one auto-created default device (`Engine`), empty `DeviceConfig`. No PollGroups.
|
|
- `GetHealth()` = always Connected (no backend); memory footprint = compiled-cache size.
|
|
|
|
## 2. Per-tag `TagConfig`
|
|
|
|
```json
|
|
{ "scriptId": "…", "changeTriggered": true, "timerIntervalMs": 5000 }
|
|
```
|
|
|
|
- `scriptId` (required) — logical FK to the **shared `Script` entity** (same convention as
|
|
`VirtualTag.ScriptId`; generation-scoped, `SourceHash` compile-cache key). The AdminUI tag
|
|
editor reuses the VirtualTagModal's script dropdown + "New script" + inline Monaco panel
|
|
components; the standalone `/scripts/{id}` page works unchanged.
|
|
- Trigger: `changeTriggered` (default **true**) and/or `timerIntervalMs` (≥ 50), at least one
|
|
required — the authored shape mirrors `VirtualTag`, but see §4 for the runtime reality.
|
|
|
|
## 3. Value production — how inputs reach the driver
|
|
|
|
**The gap:** drivers are host-blind; nothing feeds one driver another driver's values. The
|
|
per-node `DependencyMuxActor` already receives **every** `AttributeValuePublished` (via
|
|
`DriverHostActor.ForwardToMux`), keyed by wire-ref — under v3, RawPath.
|
|
|
|
**Mechanism — a new composable capability interface** (in the spirit of `IDriver`'s
|
|
capability set):
|
|
|
|
```csharp
|
|
/// <summary>Optional capability: the driver consumes other tags' live values.
|
|
/// The host registers the declared refs with its dependency mux and forwards changes.</summary>
|
|
public interface IDependencyConsumer
|
|
{
|
|
/// <summary>Wire-refs (RawPaths) this driver needs, derived from its authored tags.
|
|
/// Re-read by the host after Initialize/Reinitialize.</summary>
|
|
IReadOnlyCollection<string> DependencyRefs { get; }
|
|
|
|
/// <summary>Host push of a dependency value change. Called from an actor context —
|
|
/// implementations must be non-blocking.</summary>
|
|
void OnDependencyValue(string rawPath, object? value, uint statusCode, DateTime timestampUtc);
|
|
}
|
|
```
|
|
|
|
`DriverHostActor` wiring (~30 lines): when a spawned driver implements `IDependencyConsumer`,
|
|
spawn a tiny mux-adapter child that `RegisterInterest(driver.DependencyRefs, Self)` on the
|
|
existing `DependencyMuxActor` and forwards `DependencyValueChanged` into the driver callback.
|
|
Re-register on every apply (refs change when tags/scripts change).
|
|
|
|
**Output path is the ordinary driver path** — evaluation raises `ISubscribable.OnDataChange`
|
|
→ `DriverInstanceActor` → `AttributeValuePublished` → node fan-out **and** back into the mux.
|
|
Two things fall out for free:
|
|
|
|
1. **Calc-of-calc chains work with no extra machinery** — calc A's published output re-enters
|
|
the mux, calc B (subscribed to A's RawPath) re-evaluates. This is exactly why the
|
|
deploy-time **cycle gate** (§5) is mandatory: an undetected A→B→A cycle is a live
|
|
oscillation loop, not just a stale value.
|
|
2. Historization, native alarms, and UNS references attach to the calc tag's raw node exactly
|
|
like any driver tag.
|
|
|
|
`DependencyRefs` = the union of `EquipmentScriptPaths.ExtractDependencyRefs(source)` over the
|
|
driver's authored tags' scripts (literal-only `ctx.GetTag` paths, enforced by
|
|
`DependencyExtractor`).
|
|
|
|
## 4. Evaluation engine + triggers
|
|
|
|
- **Evaluator:** a `CalculationEvaluator` with the same construction as
|
|
`RoslynVirtualTagEvaluator` — `CompiledScriptCache<VirtualTagContext, object?>` keyed by
|
|
source, `TimedScriptEvaluator` (2 s default timeout), passthrough fast-path, single-tag
|
|
mode (`ctx.SetVirtualTag` dropped + logged). Reuses `VirtualTagContext` so the Monaco
|
|
editor, sandbox, and diagnostics pipeline apply verbatim. Catalog keys for completions are
|
|
RawPaths; the `{{equip}}` branch is already conditional in `ScriptAnalysisService` and
|
|
simply never triggers for raw-level scripts.
|
|
- **Change trigger:** evaluate on `OnDependencyValue` for any declared dep of that tag,
|
|
gated like `VirtualTagActor`: publish nothing until all declared deps have arrived at least
|
|
once; dedupe equal results.
|
|
- **Timer trigger:** owned **inside the driver** by reusing the dormant-but-tested
|
|
`TimerTriggerScheduler` (`Core.VirtualTags`) — one timer per distinct interval group,
|
|
per-group in-flight skip. *(Fact: the live VirtualTag runtime is change-trigger only —
|
|
`VirtualTagActor` has no timer handler and `EquipmentVirtualTagPlan` drops the trigger
|
|
fields. The Calc driver revives the scheduler without touching the VT runtime; whether the
|
|
VT plan seam later gains timer support is out of scope here.)*
|
|
- **Concurrency:** evaluations run inline on the driver's dispatch (same "fast enough to run
|
|
inline" contract as the VT evaluator); the compile cache makes steady-state evaluation
|
|
cheap. Compiled scripts are dropped on `ReinitializeAsync` when the source set changed
|
|
(`SourceHash` comparison, mirroring `IScriptCacheOwner` usage).
|
|
|
|
## 5. Deploy gates (new — `DraftValidator`)
|
|
|
|
Today `DraftValidator` checks only name uniqueness; `DependencyGraph` (Tarjan SCC) exists but
|
|
is dormant. v3 adds, for `Calculation`-bound tags:
|
|
|
|
1. **`scriptId` existence** — the referenced `Script` row must exist in the draft generation.
|
|
2. **Cycle gate (hard error)** — build the calc→calc edge set (tag → deps that are themselves
|
|
Calculation tags) and run `DependencyGraph.DetectCycles`; any SCC ⇒ deploy error naming
|
|
the cycle members. Cross-driver refs (calc reading a Modbus tag) are terminal nodes,
|
|
never edges.
|
|
3. **Compile: deliberately NOT a hard gate** — parity with VirtualTags. The editor's live
|
|
diagnostics mirror the compile exactly, the `StartDeployment` compile-cost advisory stays
|
|
warn-only, and a runtime compile failure lands as Bad quality + script-log (§6). A hard
|
|
compile gate would make deploys O(scripts) slow and diverge Calc tags from VT behavior.
|
|
|
|
## 6. Error semantics (mirrors `VirtualTagActor` 02/S13)
|
|
|
|
- Evaluator throw / compile error / timeout ⇒ publish **Bad** quality carrying the last-known
|
|
value, once per Good→Bad transition, only after all deps have arrived; emit a
|
|
`ScriptLogEntry` on the `script-logs` DPS topic per failure (visible on the script-log
|
|
panel). Recovery to Good is force-published. No retry — re-evaluation on next trigger.
|
|
- Historized Bad results record `BadInternalError` (0x80020000), matching VT historization.
|
|
|
|
## 7. Probe
|
|
|
|
`CalculationDriverProbe : IDriverProbe` (`DriverType = "Calculation"`). `ProbeAsync` =
|
|
validate the driver config parses; there is no backend to connect. (Per-script compile
|
|
verification belongs to the editor diagnostics + the tag editor's inline panel, not the
|
|
driver probe — the probe receives only `DriverConfig`, which for Calculation is empty.)
|
|
Returns Ok with negligible latency; never throws.
|
|
|
|
## 8. Testing
|
|
|
|
- **Unit:** evaluator parity tests vs the VT evaluator (passthrough, timeout, sandbox
|
|
violation, SetVirtualTag drop); dependency-ref extraction from authored tags; change-gate
|
|
(no publish until all deps seen); dedupe; timer-group scheduling via
|
|
`TimerTriggerScheduler`; Bad-transition + script-log emission; cycle gate (self-cycle,
|
|
2-cycle, cross-driver terminal).
|
|
- **Integration:** deploy a Calc tag reading a fixture-driver tag on the 2-node harness;
|
|
assert the raw node publishes computed values, a UNS reference sees them, and calc-of-calc
|
|
chains propagate.
|
|
- **Live `/run` (Batch 2 gate):** author a Calc driver + tag on docker-dev via `/raw`,
|
|
script it against a Modbus fixture tag, watch the value compute; break the script, watch
|
|
Bad + script-log row; fix, watch recovery.
|
|
|
|
## 9. Out of scope
|
|
|
|
- Multi-tag scripts / `ctx.SetVirtualTag` fan-out (single-tag mode, as VT today).
|
|
- Timer support in the UNS VirtualTag runtime (dormant there; revived only inside this driver).
|
|
- Write-through calc tags (inverse calculations).
|