docs(design): Galaxy alias tag (UNS) — approved brainstorming design
Equipment exposes a Galaxy attribute under a friendly UNS name as a first-class driver-bound Tag (alias) instead of a relay VirtualTag. Approach A: reuse the Tag entity, broaden the equipment-tag filter to admit GalaxyMxGateway-backed equipment tags; no entity/EF migration. Includes a relay->alias converter (per-equipment + fleet-wide).
This commit is contained in:
@@ -0,0 +1,266 @@
|
||||
# Galaxy Alias Tag (UNS) — Design
|
||||
|
||||
**Date:** 2026-06-11
|
||||
**Status:** Approved (brainstorming) — ready for implementation plan
|
||||
**Author:** design session
|
||||
|
||||
## Goal
|
||||
|
||||
Let an Equipment node expose a Galaxy attribute under a friendly UNS name as a
|
||||
**first-class driver-bound tag** — an *alias tag* — instead of a relay
|
||||
VirtualTag whose script is nothing but `return ctx.GetTag("…").Value;`. The
|
||||
alias is a normal `Tag` row bound to the Galaxy gateway driver, resolved by a
|
||||
direct driver subscription, with **no Roslyn script engine** in the path.
|
||||
|
||||
## Motivation
|
||||
|
||||
The UNS Equipment tree is an Equipment-kind namespace; the Galaxy gateway
|
||||
mirrors the raw ArchestrA hierarchy into a separate **SystemPlatform**-kind
|
||||
namespace. A driver instance populates one namespace kind, and
|
||||
`DraftValidator.ValidateDriverNamespaceCompatibility` forbids a `GalaxyMxGateway`
|
||||
driver in an Equipment namespace. So the only way today to surface a Galaxy
|
||||
attribute under an Equipment with a curated name (`speed-rpm` →
|
||||
`TestMachine_020.TestChangingInt`) is a VirtualTag whose script relays a single
|
||||
`ctx.GetTag(...)`. The `ctx.GetTag(FullName)` call is the one mechanism that
|
||||
bridges the two namespace kinds.
|
||||
|
||||
Inspecting one rig equipment (`EQ-89e6583eeb9c` / `blender-20`) showed **all 24**
|
||||
of its tags are exactly this pass-through, with zero computation, on a
|
||||
driver-less equipment. Across the rig that is ~960 scripts (24 × 40 machines),
|
||||
each spinning a compiled script that recompiles/evaluates on every input change
|
||||
just to copy one value — and flattening the source quality/timestamp through the
|
||||
script engine in the process.
|
||||
|
||||
An alias tag removes the script layer entirely: the runtime already materialises
|
||||
and subscribes equipment driver tags end-to-end (shipped 2026-06-06), and an
|
||||
alias is functionally identical to a Modbus/S7 equipment tag — the only thing
|
||||
special is that its driver happens to live in a SystemPlatform namespace.
|
||||
|
||||
## Decisions (settled in brainstorming)
|
||||
|
||||
1. **Resolution model: independent driver subscription.** The alias is a real
|
||||
equipment `Tag` bound to the Galaxy gateway; the runtime subscribes to its
|
||||
`FullName` directly. Self-sufficient — works whether or not the raw Galaxy
|
||||
SystemPlatform mirror is deployed. The Galaxy `SubscriptionRegistry` dedups by
|
||||
reference, so MXAccess sees a single subscription if the mirror also exists.
|
||||
2. **Write semantics: configurable per alias, default read-only.** Alias carries
|
||||
an `AccessLevel` like any tag; defaults `ReadOnly`, can be set writable.
|
||||
Write-through flows through the Galaxy `IGalaxyDataWriter`, gated by the
|
||||
existing `SecurityClassification` (FreeAccess/Operate/Tune/Configure) **and**
|
||||
the OPC UA NodeAcl on the equipment node.
|
||||
3. **Convert existing relays.** Ship a converter that detects pure-relay
|
||||
VirtualTags and rewrites each as an alias `Tag`, removing the script + vtag.
|
||||
4. **Authoring on the Tags tab.** An alias is an equipment tag bound to Galaxy,
|
||||
so it lives on the equipment page's existing **Tags** tab; an "Add alias"
|
||||
action opens the existing Galaxy live-browse picker. Aliases and native tags
|
||||
coexist in one list.
|
||||
5. **Approach A — reuse the `Tag` entity; broaden the equipment-tag path.** No
|
||||
new entity, **no EF migration**. "Alias-ness" is simply "an equipment tag
|
||||
bound to a SystemPlatform (Galaxy) driver."
|
||||
|
||||
## Approach (chosen) vs alternatives
|
||||
|
||||
- **A — Reuse `Tag`, broaden the equipment-tag filter (CHOSEN).** Smallest
|
||||
change; rides the proven equipment-tag composer/artifact/runtime/materialise
|
||||
rails; honours the no-migration constraint; the converter becomes a straight
|
||||
"rewrite relay VirtualTag rows as Tag rows" transform.
|
||||
- **B — New `AliasTag` entity + table.** Conceptually tidy but requires a
|
||||
migration and duplicates the entire equipment-tag machinery (composer,
|
||||
artifact, runtime, materialise, converter) for a near-identical row. More code,
|
||||
more risk, no functional gain. Rejected.
|
||||
- **C — OPC UA pointer/redirect to a mirror node.** Rejected by decision #1 —
|
||||
it would force the raw mirror to be deployed and is trickier in the node
|
||||
manager. Listed for completeness.
|
||||
|
||||
## Gate inventory (what blocks an equipment-scoped Galaxy tag today)
|
||||
|
||||
Verified against the code:
|
||||
|
||||
1. **`Phase7Composer.equipmentTags` filter** requires the tag's driver namespace
|
||||
be `NamespaceKind.Equipment` → a Galaxy-backed equipment tag is silently
|
||||
dropped. **Must broaden.**
|
||||
2. **`DeploymentArtifact.BuildEquipmentTagPlans`** mirrors that filter →
|
||||
changes in lockstep (an existing byte-parity test guards the pair).
|
||||
3. **AdminUI** offers only Equipment-namespace drivers when adding an equipment
|
||||
tag → needs the "Add alias → Galaxy picker" path.
|
||||
4. **NOT blocked:** `DraftValidator.ValidateDriverNamespaceCompatibility` checks
|
||||
the driver's *home* namespace (still SystemPlatform — unaffected); and
|
||||
`sp_ValidateDraft` has no tag-level namespace-kind invariant. The
|
||||
"`EquipmentId` required ⟺ Equipment-kind driver" line in `Tag.cs` is a doc
|
||||
convention, not enforced — it is reframed (below).
|
||||
|
||||
## Architecture
|
||||
|
||||
### 1. Data model & the core composer/artifact change
|
||||
|
||||
An alias is an ordinary `Tag` row (no new entity, no migration):
|
||||
|
||||
| Column | Value |
|
||||
|---|---|
|
||||
| `EquipmentId` | owning equipment (e.g. `EQ-89e6583eeb9c`) |
|
||||
| `DriverInstanceId` | the Galaxy gateway (`MAIN-galaxy-mxgw`) |
|
||||
| `Name` | friendly UNS leaf (`speed-rpm`) |
|
||||
| `DataType` | OPC UA type (`Int32`), operator-set, matches the Galaxy attribute |
|
||||
| `AccessLevel` | per-alias, default `ReadOnly` |
|
||||
| `FolderPath` | `null` (alias hangs directly under the equipment) |
|
||||
| `TagConfig` | `{"FullName":"TestMachine_020.TestChangingInt"}` |
|
||||
|
||||
`TagConfig.FullName` is exactly what `Phase7Composer.ExtractTagFullName` already
|
||||
reads for equipment tags — so the wire-reference plumbing is zero-new-code.
|
||||
|
||||
**Structural change (two byte-parity-locked places):**
|
||||
- `Phase7Composer.cs` `equipmentTags`: predicate becomes
|
||||
`ns.Kind == NamespaceKind.Equipment || di.DriverType == "GalaxyMxGateway"`.
|
||||
- `DeploymentArtifact.BuildEquipmentTagPlans`: the identical broadening; the
|
||||
existing parity test is extended with an alias row.
|
||||
|
||||
Everything downstream is reused unchanged: `Phase7Applier` materialises the
|
||||
variable under the equipment folder; the runtime subscribes by
|
||||
`DriverInstanceId` + `FullName` on the Galaxy driver. The SystemPlatform mirror
|
||||
filter (`EquipmentId == null`) is untouched, so the 396 raw mirror tags are
|
||||
unaffected.
|
||||
|
||||
**Convention reframe (doc-only):** the `Tag.cs` comment becomes "`EquipmentId`
|
||||
set ⟺ the tag participates in the Equipment tree, regardless of the driver's
|
||||
namespace kind; a Galaxy-gateway-bound equipment tag is an **alias**."
|
||||
|
||||
### 2. Validation, authorization & write-through
|
||||
|
||||
- **Validation:** add one positive `DraftValidator` check — an equipment tag
|
||||
bound to `GalaxyMxGateway` must carry a non-empty `TagConfig.FullName`
|
||||
(otherwise it subscribes to nothing). No gate is needed to *permit* aliases.
|
||||
- **Authz layer 1 (OPC UA permission):** the alias lives in the Equipment-kind
|
||||
namespace → governed by **NodeAcl** like any equipment tag
|
||||
(`ReadOnly`/`WriteOperate`/`WriteTune`/`WriteConfigure` per `NodeScope`).
|
||||
- **Authz layer 2 (write-through):** a write that passes layer 1 flows through
|
||||
the equipment-tag write path to the Galaxy `IGalaxyDataWriter`, which routes by
|
||||
the Galaxy attribute's `SecurityClassification`. A writable alias is gated by
|
||||
**both** — exactly like writing the raw Galaxy tag, reached via the UNS name.
|
||||
- **Default & opt-in:** `AccessLevel` defaults `ReadOnly`; writable is explicit.
|
||||
- **Quality/timestamp bonus:** the value comes straight off the driver
|
||||
subscription, so the alias carries the MXAccess quality + source timestamp
|
||||
natively (the relay flattened those through the script engine).
|
||||
- **Edge — cluster alignment:** the alias is hosted/subscribed on the Galaxy
|
||||
driver's cluster; the driver-less equipment is attributed by its UNS line's
|
||||
cluster. They must resolve to the same cluster (the existing same-cluster
|
||||
namespace-binding SQL check keeps a driver and its namespace aligned).
|
||||
Cross-cluster alias is out of scope — documented guardrail, not built.
|
||||
|
||||
### 3. AdminUI authoring (Tags tab)
|
||||
|
||||
- **Entry point:** the equipment page **Tags** tab gains **+ Add alias (browse
|
||||
Galaxy)** beside *+ Add tag*; disabled with a hint when the equipment's cluster
|
||||
has no `GalaxyMxGateway` driver.
|
||||
- **Alias modal** — a focused sibling of `TagModal` (same pattern as
|
||||
`ScriptedAlarmModal`; avoids overloading `TagModal`'s driver-typed editor
|
||||
dispatch):
|
||||
- **Galaxy driver resolution** — from the equipment's pick-context, find
|
||||
`GalaxyMxGateway` driver(s) in its cluster. One → auto-bind; several →
|
||||
select; none → action disabled.
|
||||
- **Live-browse pick** — embeds the existing
|
||||
`GalaxyAddressPickerBody`/`DriverTagPicker` pointed at that gateway; operator
|
||||
drills the live Galaxy hierarchy, picks an attribute → fills `FullName` and
|
||||
suggests `DataType` from the attribute's `mx_data_type`.
|
||||
- **Fields** — `Name` (UNS leaf; existing equipment signal-name
|
||||
uniqueness/regex applies), `DataType` (defaulted, editable), `AccessLevel`
|
||||
(default `ReadOnly`).
|
||||
- **Save** — assembles `TagConfig = {"FullName": …}` and writes a `Tag` row.
|
||||
- **Service layer:** thin `CreateAliasTagAsync` / `UpdateAliasTagAsync` on
|
||||
`IUnsTreeService` that assemble the `TagConfig` and reuse the existing
|
||||
equipment-tag name-uniqueness guard; **delete reuses `DeleteTagAsync`**.
|
||||
`LoadTagsForEquipmentAsync` gains a derived **Source** field.
|
||||
- **Display:** an alias row shows an **"alias" badge** and a **Source** column
|
||||
`galaxy: <FullName>`; native driver tags render their driver/register as today.
|
||||
|
||||
### 4. Relay → alias converter
|
||||
|
||||
- **Detection (conservative):** convertible only when the bound `Script` body is
|
||||
exactly a single relay return (modulo whitespace):
|
||||
`^\s*return\s+ctx\.GetTag\(\s*"([^"]+)"\s*\)\s*\.Value\s*;\s*$`. Anything with
|
||||
extra statements, conditionals, arithmetic, or multiple `GetTag` calls is left
|
||||
untouched. Ref extraction reuses the Commons `EquipmentScriptPaths` GetTag
|
||||
helper.
|
||||
- **`{{equip}}` expansion:** if the ref contains the reserved token, expand it
|
||||
per-equipment via `DeriveEquipmentBase`/`SubstituteEquipmentToken`; the shared
|
||||
`Script` is deleted only after its last referencing VirtualTag is converted.
|
||||
- **Safety guards (skip + report, never silently drop):** shared script still in
|
||||
use → drop vtag, keep script; `Historize = true` → skip (Tag has no historize
|
||||
column); no Galaxy gateway in the cluster → skip; non-relay body → skip.
|
||||
- **Field carry-over:** alias `Name` = vtag `Name`; `DataType` = vtag `DataType`;
|
||||
`AccessLevel` = `ReadOnly`. `ChangeTriggered`/`TimerIntervalMs` dropped (the
|
||||
subscription replaces them).
|
||||
- **Atomicity:** each conversion does delete-VirtualTag-(+orphan-Script)-and-
|
||||
insert-alias-Tag as one unit so a same-named alias and vtag never coexist (the
|
||||
equipment signal-name collision check stays satisfied).
|
||||
- **Invocation:** one method `ConvertRelayVirtualTagsToAliasesAsync(scope,
|
||||
dryRun)`, **FleetAdmin-gated**, operating on the **draft** generation (never
|
||||
bypasses publish). Exposed two ways sharing that method: **per-equipment**
|
||||
(Tags tab) and **fleet-wide** (a maintenance entry). Both run **dry-run preview
|
||||
first** (convertible refs + every skip with reason), then **confirm-apply**.
|
||||
Touches only existing entities (`Tag` insert, `VirtualTag`/`Script` delete) —
|
||||
**no migration**.
|
||||
|
||||
## Net service / type surface (no schema change)
|
||||
|
||||
New on `IUnsTreeService`: `CreateAliasTagAsync`, `UpdateAliasTagAsync`,
|
||||
`ConvertRelayVirtualTagsToAliasesAsync(scope, dryRun)`; `LoadTagsForEquipmentAsync`
|
||||
gains a derived Source/alias field. New AdminUI types: an alias input DTO + a
|
||||
converter preview/result DTO. New component: `AliasTagModal.razor` (+ reuse of
|
||||
`GalaxyAddressPickerBody`/`DriverTagPicker`). One positive `DraftValidator` check.
|
||||
Composer + artifact filter broadening (byte-parity pair).
|
||||
|
||||
**No Configuration entity change and no EF migration** — an alias is a `Tag`
|
||||
row; the converter only inserts `Tag` rows and deletes `VirtualTag`/`Script`
|
||||
rows.
|
||||
|
||||
## Testing
|
||||
|
||||
No bUnit — service/composer logic unit-tested; Razor proven live.
|
||||
|
||||
- **Composer/artifact:** a Galaxy-bound equipment tag now lands in
|
||||
`equipmentTags`; a SystemPlatform mirror tag (`EquipmentId == null`) still does
|
||||
not; `DeploymentArtifact` byte-parity extended with an alias row.
|
||||
- **DraftValidator:** alias missing `TagConfig.FullName` → the new error;
|
||||
well-formed alias → clean; `ValidateDriverNamespaceCompatibility` still passes.
|
||||
- **Service (in-memory EF):** `CreateAliasTagAsync` writes the expected row;
|
||||
signal-name collision rejected; `LoadTagsForEquipmentAsync` surfaces Source;
|
||||
delete via `DeleteTagAsync`.
|
||||
- **Converter (in-memory EF):** exact relay → converted (ref/name/DataType
|
||||
carried, vtag + orphan script removed); non-relay bodies → untouched; shared
|
||||
script kept while referenced, deleted on last consumer; `Historize=true` →
|
||||
skipped+reported; no-gateway → skipped+reported; `{{equip}}` → expanded
|
||||
per-equipment; dry-run mutates nothing; apply atomic.
|
||||
- **Live docker-dev `/run`** (user drives; agent does **not** sign in): create an
|
||||
alias on `blender-20` via the Galaxy picker → publish → read it live via
|
||||
Client.CLI (value + quality + timestamp); flip writable → write-through changes
|
||||
the Galaxy attribute, read-only rejects writes; converter dry-run on
|
||||
`blender-20` previews 24 → apply → 24 relays become alias Tags → values still
|
||||
live, no script-log entries for them.
|
||||
|
||||
**Done gate:** solution builds clean + `dotnet test` green + live `/run` pass.
|
||||
|
||||
## Error handling / edge cases
|
||||
|
||||
- **Unresolvable `FullName`** (typo, object not deployed) — the Galaxy
|
||||
subscription returns Bad; the alias node publishes Bad quality and recovers if
|
||||
the object appears. Same degrade as the relay's `BadNodeIdUnknown`; validation
|
||||
only checks non-empty (existence is a runtime concern).
|
||||
- **Underlying Galaxy object renamed/deleted** — alias goes Bad at runtime; no
|
||||
config-time coupling (it holds a `FullName` string).
|
||||
- **Gateway down** — alias quality goes Bad/uncertain like any Galaxy tag;
|
||||
recovers on reconnect.
|
||||
- **Cluster alignment** — the Section-2 guardrail; cross-cluster out of scope.
|
||||
|
||||
## Docs
|
||||
|
||||
- `docs/Uns.md` — alias tags on the Tags tab + the converter (per-equipment and
|
||||
fleet-wide).
|
||||
- `CLAUDE.md` — a short note near the Galaxy / contained-name-vs-tag-name concept.
|
||||
- A pointer wherever equipment tags are described.
|
||||
|
||||
## Guardrails
|
||||
|
||||
Feature branch off master; stage by explicit path (never `git add .`); never
|
||||
stage `sql_login.txt` or `src/Server/ZB.MOM.WW.OtOpcUa.Host/pki/`; never echo the
|
||||
gateway API key into a new tracked file; no force-push; no `--no-verify`; **no
|
||||
Configuration entity or EF migration change**.
|
||||
Reference in New Issue
Block a user