Files
lmxopcua/docs/Raw.md
T
Joseph Doherty 0b537b5dec docs: record surgical-deploy semantics + artifact compression ops notes (plan 2026-08-15)
CLAUDE.md — a new "How a deploy reaches that address space" block under the v3
Batch 4 address-space section. The blanket ChangedRawTags/ChangedUnsReferenceVariables
=> Rebuild rule is gone, so the doc now names both halves explicitly: what still
rebuilds (alarm-bearing raw-tag deltas, raw-tag identity changes, raw container
renames/removals, changed equipment/alarms) and what is now surgical (non-alarm
attribute deltas, every UnsReferenceDelta). Also records the three semantics that
are easy to break by accident: mirror repair rides UnsReferenceVariable's inherited
backing fields (omitting a newly-inherited field silently reopens the staleness),
a degraded non-rebuild apply deliberately does not advance _lastApplied, and the
raw-realm announce gap is no longer neutral now that surgical adds keep
subscriptions alive.

CLAUDE.md ConfigSource — the RevisionHash == SHA-256(artifact bytes) paragraph is
still true and stays; it gains the clause that the blob is gzip-compressed since
2026-08-15 and the hash covers the COMPRESSED bytes, which is precisely why the
fetcher, cache and serve path were untouched.

docs/Uns.md + docs/Raw.md — one section each on what a deploy now tears down, from
the authoring surface's point of view, including the mirror repair semantics
(identity-preserving backing edits update in place and keep their MonitoredItems;
re-points and effective-name edits remove+recreate that one node).

docs/Configuration.md — "Artifact compression (2026-08-15)" under ConfigSource/
ConfigServe: gzip inside the hashed bytes, magic-sniffed on read, pre-upgrade
uncompressed rows still parse, plus the two operational facts an operator needs.
The first deploy after upgrade re-hashes every revision (same config, new bytes,
new RevisionHash) so it is a one-time full re-dispatch rather than a no-op; and
rollout is ordered — nodes must run artifact-sniffing code BEFORE central starts
emitting compressed blobs, since a pre-upgrade node handed a gzip artifact fails
the apply and keeps last-known-good until it is upgraded.

Plan doc — a "Follow-ups (recorded, not implemented)" section covering the seven
items surfaced during implementation and review: the raw-realm announce gap, the
RemoveEquipmentSubtree dangling-inverse-edge exposure, the missing end-to-end test
for a degraded remove-bearing apply's ratcheted rebuild, the scripted-alarm
thin-plan-ids-against-rich-rows blindness, the absent monotonic dispatch sequence,
LocalDb store ordering under concurrent StoreAsync, and threading the in-hand blob
through RestoreApplied.

tasks.json — all nine tasks completed.

Docs only; no source changes.
2026-08-15 18:02:21 -04:00

8.3 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.

Authoring tags

Add tags ▸ on a Device or TagGroup offers:

  • Manual entry — a grid of Name / DataType / AccessLevel / WriteIdempotent / PollGroup + the driver-typed TagConfig editor 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. Modbus Region, Address, ModbusDataType, ByteOrder, …). TagGroupPath auto-creates nested groups; typed columns win over TagConfigJson on 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 → universal DiscoveryDriverBrowser when the driver's ITagDiscovery.SupportsOnlineDiscovery is true → grayed out otherwise). Multi-selected leaves become raw Tag rows under the target, with the browsed reference written into the driver-typed TagConfig address 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 Engine device" below never shipped. "Engine" has zero occurrences in src/, and nothing special-cases Calculation at device creation — author the device like any other. Per-tag TagConfig is { "scriptId": "…", "changeTriggered": true, "timerIntervalMs": 5000 }.
  • The host feeds dependency values via the new IDependencyConsumer capability + a DependencyConsumerMuxAdapter on 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): scriptId existence, and a hard cycle gate (DependencyGraph.DetectCycles over 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 + a script-logs entry.

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).