Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
9.4 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).
A rename rebinds live data on the deploy — no node restart is needed (Gitea #489, live-gated
2026-07-31 for both a tag rename and a device rename). The mechanism is #516's respawn: a rename
changes the merged DriverConfig, and DriverSpawnPlan.Compute stops and respawns the driver child
on any DriverConfig change, so the driver rebuilds against the new RawPaths and re-subscribes.
⚠️ The flip side is that a rename reconnects the driver — briefly dropping every tag on it, not
just the renamed one. That is the accepted cost #516 documents; it is not specific to renames.
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.
Creating a driver auto-creates an empty Device1 (by design, #596). Every driver needs at least one
device to hang browse and tags off, and for a single-device driver the merger treats that device's
DeviceConfig as the endpoint, so an empty, renamable, deletable Device1 is the lowest-friction start
(RawTreeService.CreateDriverAsync; pinned by RawTreeServiceWaveBTests). It is not a placeholder to be
removed later.
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.
What a deploy tears down (surgical deploys, 2026-08-15)
Editing a raw tag's attributes is no longer a fleet-wide event. A non-alarm
raw-tag delta whose only differences are the in-place-updatable attributes —
Writable, IsHistorized, HistorianTagname, DataType, IsArray, ArrayLength —
and/or its set of referencing equipment is applied in place on the existing Raw-realm
node (ISurgicalAddressSpaceSink.UpdateTagAttributes), so every client subscription on
that tag and on the rest of the server survives. The same is true of every UNS
tag-reference change: adding, removing or re-pointing a reference used to flip the
backing raw tag to Changed (the referencing set is part of its record equality) and
rebuild the whole address space for a one-variable edit.
Still a full rebuild, deliberately: an alarm-bearing raw tag (a delta with
Alarm != null on either side — only a rebuild clears and re-wires the native-alarm
notifiers that fan the condition to its referencing equipment folders); an identity
change on a tag (TagId, DriverInstanceId, Name, ParentNodeId, realm); and any
change to a raw container — a Folder / Driver / Device / TagGroup rename or
removal — because the surgical sink has no folder-remove. The classifier is
default-closed: any field it does not explicitly whitelist, including fields added
later, still routes to a rebuild, and any surgical call returning false or throwing
ratchets back to one.
Mirror repair. A raw-side historian or shape edit also reaches every UNS reference
mirroring the tag: the reference record carries the backing tag's historian/shape, so
the edit emits a coincident UNS-reference delta. Identity-preserving repairs update the
mirror in place (its MonitoredItems survive); a re-point or effective-name change
removes and recreates that one mirror node. See Uns.md and
plans/2026-08-15-surgical-deploys.md.
⚠️ A surgically added raw container/tag is materialised but not announced —
AnnounceAddedNodes is UNS-only, so model-aware clients see new raw nodes on their next
browse. This mattered less when every add forced a rebuild (and therefore a re-browse).
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.
Quality: a raw tag's node when its driver stops communicating
A raw tag's node — and every UNS mirror of it — goes BadNoCommunication with its last value
retained when the owning driver is declared comms-lost, and clears when the driver re-publishes. It is
never cleared by the server on reconnect: recovery is a value, never an assumption. The rule and the
seams that implement it are stated once in
docs/DriverLifecycle.md §"The quality contract".
Calc tags authored here inherit the same rule from their inputs — see
docs/drivers/Calculation.md §"Input quality".
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).