Closes the §7.2 priority-3 item. Sql was a shipped, merged driver with NO
user-facing documentation at all -- only design and plan files, which describe
what was intended rather than what an operator needs to author one.
Sql.md covers: SQL Server as the only constructed provider (the other four
SqlProvider names are reserved, and authoring one fails rather than falling
back), the KeyValue / WideRow tag models with worked TagConfig, Query being
rejected end-to-end, the config table with real defaults read from
SqlDriverOptions, connectionStringRef never carrying credentials into an
artifact, per-source query grouping and its case-sensitive group keys, and the
value-bound-vs-identifier-validated split that makes it injection-safe.
Calculation.md covers: what a pseudo-driver is and why it occupies a driver slot,
a Calculation-vs-VirtualTags decision table (the two are easy to confuse), the
literal-only ctx.GetTag dependency extraction and the runtime-built-path trap,
the two deploy gates, the all-deps-arrived publish gate, and the Good->Bad-once
error semantics.
Two stale claims found by checking against source rather than inheriting them:
- The driver README's Calculation row still carried G-1 ("no typed config form,
RunTimeout unauthorable"), fixed on 2026-07-27 by CalculationDriverForm.
- docs/Raw.md's "one auto-created default Engine device" NEVER SHIPPED. I had
copied it into the new doc before checking; "Engine" has zero occurrences in
src/ and nothing special-cases Calculation at device creation. Corrected in
both files. The config key was also wrong in my first draft: runTimeoutMs, an
int in milliseconds, not a TimeSpan string -- and it is ignored on reinit
because the evaluator's timeout is constructor-fixed.
Full solution: 49 suites pass; the 2 failures are the known pre-existing
environment ones (AbLegacy.IntegrationTests docker fixture, Host.IntegrationTests).
OpcUaClient.IntegrationTests, which was silently failing, is now green.
Claude-Session: https://claude.ai/code/session_015p7wGqy3YpZNCpDzTpGMKo
5.5 KiB
The Raw project tree (/raw)
v3 authors device I/O in a Raw project tree — a cluster-rooted, Kepware-style hierarchy
that is the single surface for driver/device/tag authoring. It is the peer of the /uns
unified-namespace page; the two OPC UA namespaces (ns=Raw / ns=UNS) light up in Batch 4.
Enterprise → Cluster → Folder(s) → Driver → Device → TagGroup(s) → Tag
Every node's identity is its RawPath (<Folder/…>/<Driver>/<Device>/<TagGroup/…>/<Tag>,
cluster-scoped, /-separated, ordinal). Names are validated as RawPath segments (no /, no
leading/trailing whitespace) at authoring and at the deploy gate.
The tree
/raw (GlobalRaw.razor → RawTree.razor, backed by IRawTreeService) lazily loads each
container level on expand (unlike /uns, which eager-loads). Each node has a right-click
context menu and a ⋯ fallback (the reusable ContextMenu component):
| Node | Actions |
|---|---|
| Cluster | New folder · New driver |
| Folder | New folder · New driver · Rename · Delete |
| Driver | Configure · New device · Enable/Disable · Rename · Delete |
| Device | Configure (endpoint + Test connect) · New tag-group · Add tags ▸ · Delete |
| TagGroup | New group · Add tags ▸ · Rename · Delete |
| Tag | Edit · Delete |
Deletes are blocked while a node has children (or, for a raw tag, while a UnsTagReference
points at it — the error names the referencing equipment). Renames return non-blocking
warnings: renaming a node changes the RawPath of every historized / UNS-referenced tag
beneath it (its historian tagname / UNS projection path moves).
Endpoint → DeviceConfig (the channel/device split)
v3 moves the connection endpoint off the driver and onto the Device (DeviceConfig),
mirroring Kepware's channel/device split. The driver's DriverConfig holds protocol/channel
settings; the device's DeviceConfig holds host/port/endpoint. At deploy/probe time
DriverDeviceConfigMerger merges the device's endpoint up into the driver config, and Test
connect (inside the Device modal) probes that merged config. Driver forms no longer
serialize the endpoint keys, so DeviceConfig is the single source of truth.
Authoring tags
Add tags ▸ on a Device or TagGroup offers:
- Manual entry — a grid of Name / DataType / AccessLevel / WriteIdempotent / PollGroup +
the driver-typed
TagConfigeditor per row. - Import/Export CSV — a staged flow (upload → parse → review grid with per-row
verdicts → commit) over an RFC-4180 parser. Columns: the common set
(
Name, TagGroupPath, DataType, AccessLevel, WriteIdempotent, PollGroup, IsHistorized, HistorianTagname, IsArray, ArrayLength, Alarm.*, TagConfigJson) plus the per-driver typed columns (e.g. ModbusRegion, Address, ModbusDataType, ByteOrder, …).TagGroupPathauto-creates nested groups; typed columns win overTagConfigJsonon conflict (flagged in the review grid). Import is all-or-nothing — any invalid row blocks the whole commit. Export round-trips the same shape. - Browse device… — enabled per the two-tier resolution (bespoke
IDriverBrowser→ universalDiscoveryDriverBrowserwhen the driver'sITagDiscovery.SupportsOnlineDiscoveryis true → grayed out otherwise). Multi-selected leaves become rawTagrows under the target, with the browsed reference written into the driver-typedTagConfigaddress field (nodeId/tagPath/ …), never an identity key. An opt-in "create matching tag-groups" toggle mirrors the browse folder nesting. The browser dials a real ephemeral driver against the merged Driver+Device config.
The Calculation driver
Signal-level calculated tags are ordinary raw tags bound to a Calculation driver
(DriverTypeNames.Calculation), computed by C# scripts that read other tags' live values via
ctx.GetTag("<RawPath>"). See docs/plans/2026-07-15-calculation-driver-mini-design.md for
the full design. Highlights:
- ⚠️ Correction (2026-07-28): the "auto-created default
Enginedevice" below never shipped."Engine"has zero occurrences insrc/, and nothing special-casesCalculationat device creation — author the device like any other. Per-tagTagConfigis{ "scriptId": "…", "changeTriggered": true, "timerIntervalMs": 5000 }. - The host feeds dependency values via the new
IDependencyConsumercapability + aDependencyConsumerMuxAdapteron the per-node dependency mux; calc-of-calc chains work because a calc output re-enters the mux. The driver rebuilds its tag/dependency table on every redeploy (ReinitializeAsync) and re-registers its refs after the delta applies. - Deploy gates (
DraftValidator):scriptIdexistence, and a hard cycle gate (DependencyGraph.DetectCyclesover calc→calc edges; cross-driver refs are terminal) that rejects an A→B→A oscillation loop naming the members. Compile is not hard-gated (VirtualTag parity); a runtime compile/throw/timeout lands as Bad quality + ascript-logsentry.
Retirement note
The routed /clusters/{id}/drivers driver-authoring flow (DriverTypePicker,
DriverEditRouter, the 8 per-type *DriverPage shells, and the per-cluster Drivers list) is
retired — authoring lives in /raw. The extracted driver/device form bodies live on
inside the /raw modals. The DriverType dispatch maps (TagConfigEditorMap,
TagConfigValidator, EquipmentTagConfigInspector) are keyed off the single-source-of-truth
DriverTypeNames constants (fixing the historical TwinCat/Focas drift).