# Calculation Driver Getting-started guide for the `Calculation` **pseudo-driver** — signal-level calculated tags computed by C# scripts over other tags' live values. For the design rationale read [`docs/plans/2026-07-15-calculation-driver-mini-design.md`](../plans/2026-07-15-calculation-driver-mini-design.md); [`docs/Raw.md`](../Raw.md) covers where it sits in the `/raw` tree. ## What it talks to **Nothing.** There is no backend, no endpoint, no connection and no discovery. Its inputs are other tags' live values, fed to it by the driver host; its outputs are ordinary raw tags. That is why it is called a pseudo-driver — it occupies a driver slot so its tags get the same authoring, deployment, fan-out, historization and alarming as any device tag, without any of the I/O. Because it is an ordinary driver from the host's point of view, a calc tag's value flows out through `OnDataChange` exactly like a Modbus register's — so the dual-namespace fan-out, historian registration and **calc-of-calc** (a calc tag reading another calc tag, via mux re-entry) all work with no special-casing. ## Calculation vs. Virtual Tags — which one to use They are close cousins and easy to confuse: | | **Calculation** driver | **Virtual tags** | |---|---|---| | Lives in | the `/raw` tree, as a raw tag | the UNS tree, on an equipment | | Identity | a **RawPath** | `{Equipment}/{EffectiveName}` | | Reads by | `ctx.GetTag("")` — literal raw paths | `ctx.GetTag("{{equip}}/")` — equipment-relative | | Use when | the computation is a property of the *signal* (scaling, a derived rate, combining two registers) | the computation is a property of the *equipment* (an OEE roll-up, a per-machine state) | If the same script should apply to many machines, you want a virtual tag with `{{equip}}`. If a single derived signal belongs next to the device tags it is derived from, you want a calc tag. ## Authoring a calc tag Calc tags are authored in `/raw` like any other tag: create a `Calculation` driver, add a device under it, and add tags under that. > ⚠️ **`docs/Raw.md` says a default `Engine` device is auto-created. It is not** — verified 2026-07-28, > the string `"Engine"` has zero occurrences in `src/`, and nothing in the AdminUI or the config layer > special-cases `Calculation` at device-creation time. Create the device yourself; the name is > arbitrary but becomes a segment of every calc tag's RawPath, so pick it deliberately. Per-tag `TagConfig`: ```json { "scriptId": "scr-oven-delta", "changeTriggered": true, "timerIntervalMs": 5000 } ``` | Field | Meaning | |---|---| | `scriptId` | FK to the shared `Script` entity. **Required** — a tag with no identifiable script is not a calc tag and is rejected at parse. | | `changeTriggered` | Re-evaluate whenever any declared dependency changes. | | `timerIntervalMs` | Also re-evaluate on this cadence. Absent ⇒ change-trigger only. | `scriptSource` is **resolved at deploy time** from `scriptId` and travels in the artifact — the driver never reads the config DB. A `scriptId` that resolves to empty source still maps: the driver logs it and the tag simply never computes, rather than the whole driver failing. ## Dependencies are discovered from the script You do not declare dependencies. The deploy pipeline extracts them from the script's **literal** `ctx.GetTag("…")` reads and registers exactly those RawPaths with the dependency mux. ⚠️ **Literal means literal.** A path built at runtime — `ctx.GetTag("Plant/" + line + "/Temp")` — is invisible to the extractor, so that dependency is never fed and the tag never sees its value. Write the path as a constant string. Two deploy-time gates run over the result: - **`scriptId` existence** — a calc tag naming a script that does not exist fails the deploy. - **Cycle detection (Tarjan)** — calc-of-calc is supported, but a cycle fails the deploy rather than livelocking at runtime. ## Trigger and publish semantics - **Nothing publishes until every declared dependency has arrived at least once.** Before that a read returns `BadWaitingForInitialData`. This is the same gate `VirtualTagActor` uses, and it exists so a calculation cannot publish a confident-looking value derived from defaults. - **Equal results are deduped** — an evaluation producing the same value as last time does not re-publish. - **Timers are grouped by interval**, one timer per distinct interval, not one per tag. ## Error semantics An evaluator failure publishes **Bad quality carrying the last-known value** — the value is not zeroed, because a stale-but-flagged reading is more useful to an operator than a fabricated one. It publishes: - **once per Good→Bad transition**, not on every failed evaluation (a permanently broken script does not flood), and only after all dependencies have arrived; - with a `ScriptLogEntry` on the `script-logs` topic, attributed to the tag's `scriptId`, so the AdminUI `/script-log` page names the failing script; - recovery to Good is **force-published**, bypassing the dedup, so a recovered tag does not sit Bad waiting for its value to change. A historized Bad records `BadInternalError`. ## Config ```json { "runTimeoutMs": 2000 } ``` | Key | Default | Notes | |---|---|---| | `runTimeoutMs` | `2000` (2 s) | Per-script wall-clock evaluation budget, in **milliseconds** — not a `TimeSpan` string, unlike most driver timeouts in this repo. Parity with the virtual-tag evaluator. | ⚠️ **`runTimeoutMs` is ignored on a config *reinit*** — the evaluator's timeout is fixed at construction. A changed value takes effect because a changed `DriverConfig` respawns the driver (#516), not because the driver re-reads it in place. There is nothing else — no endpoint, no credentials, no pooling. The driver is **single-connection** in the AdminUI's sense (nothing lives on the device). ## Capabilities `IDriver` (lifecycle + always-Connected health), `IDependencyConsumer` (the host feeds it upstream values), `ISubscribable` (computed values flow out), `IReadable` (returns the last computed snapshot). No `IWritable` — a computed value has no meaningful write target. No `ITagDiscovery` — there is nothing to browse. ## See also - [`docs/Raw.md`](../Raw.md) — the `/raw` authoring tree - [`docs/VirtualTags.md`](../VirtualTags.md) — the equipment-scoped alternative - [`docs/ScriptEditor.md`](../ScriptEditor.md) — the Roslyn-backed script editor