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).
15 KiB
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)
- Resolution model: independent driver subscription. The alias is a real
equipment
Tagbound to the Galaxy gateway; the runtime subscribes to itsFullNamedirectly. Self-sufficient — works whether or not the raw Galaxy SystemPlatform mirror is deployed. The GalaxySubscriptionRegistrydedups by reference, so MXAccess sees a single subscription if the mirror also exists. - Write semantics: configurable per alias, default read-only. Alias carries
an
AccessLevellike any tag; defaultsReadOnly, can be set writable. Write-through flows through the GalaxyIGalaxyDataWriter, gated by the existingSecurityClassification(FreeAccess/Operate/Tune/Configure) and the OPC UA NodeAcl on the equipment node. - Convert existing relays. Ship a converter that detects pure-relay
VirtualTags and rewrites each as an alias
Tag, removing the script + vtag. - 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.
- Approach A — reuse the
Tagentity; 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
AliasTagentity + 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:
Phase7Composer.equipmentTagsfilter requires the tag's driver namespace beNamespaceKind.Equipment→ a Galaxy-backed equipment tag is silently dropped. Must broaden.DeploymentArtifact.BuildEquipmentTagPlansmirrors that filter → changes in lockstep (an existing byte-parity test guards the pair).- AdminUI offers only Equipment-namespace drivers when adding an equipment tag → needs the "Add alias → Galaxy picker" path.
- NOT blocked:
DraftValidator.ValidateDriverNamespaceCompatibilitychecks the driver's home namespace (still SystemPlatform — unaffected); andsp_ValidateDrafthas no tag-level namespace-kind invariant. The "EquipmentIdrequired ⟺ Equipment-kind driver" line inTag.csis 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.csequipmentTags: predicate becomesns.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
DraftValidatorcheck — an equipment tag bound toGalaxyMxGatewaymust carry a non-emptyTagConfig.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/WriteConfigureperNodeScope). - 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'sSecurityClassification. A writable alias is gated by both — exactly like writing the raw Galaxy tag, reached via the UNS name. - Default & opt-in:
AccessLeveldefaultsReadOnly; 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
GalaxyMxGatewaydriver. - Alias modal — a focused sibling of
TagModal(same pattern asScriptedAlarmModal; avoids overloadingTagModal's driver-typed editor dispatch):- Galaxy driver resolution — from the equipment's pick-context, find
GalaxyMxGatewaydriver(s) in its cluster. One → auto-bind; several → select; none → action disabled. - Live-browse pick — embeds the existing
GalaxyAddressPickerBody/DriverTagPickerpointed at that gateway; operator drills the live Galaxy hierarchy, picks an attribute → fillsFullNameand suggestsDataTypefrom the attribute'smx_data_type. - Fields —
Name(UNS leaf; existing equipment signal-name uniqueness/regex applies),DataType(defaulted, editable),AccessLevel(defaultReadOnly). - Save — assembles
TagConfig = {"FullName": …}and writes aTagrow.
- Galaxy driver resolution — from the equipment's pick-context, find
- Service layer: thin
CreateAliasTagAsync/UpdateAliasTagAsynconIUnsTreeServicethat assemble theTagConfigand reuse the existing equipment-tag name-uniqueness guard; delete reusesDeleteTagAsync.LoadTagsForEquipmentAsyncgains 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
Scriptbody 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 multipleGetTagcalls is left untouched. Ref extraction reuses the CommonsEquipmentScriptPathsGetTag helper. {{equip}}expansion: if the ref contains the reserved token, expand it per-equipment viaDeriveEquipmentBase/SubstituteEquipmentToken; the sharedScriptis 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= vtagName;DataType= vtagDataType;AccessLevel=ReadOnly.ChangeTriggered/TimerIntervalMsdropped (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 (Taginsert,VirtualTag/Scriptdelete) — 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;DeploymentArtifactbyte-parity extended with an alias row. - DraftValidator: alias missing
TagConfig.FullName→ the new error; well-formed alias → clean;ValidateDriverNamespaceCompatibilitystill passes. - Service (in-memory EF):
CreateAliasTagAsyncwrites the expected row; signal-name collision rejected;LoadTagsForEquipmentAsyncsurfaces Source; delete viaDeleteTagAsync. - 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 onblender-20via 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 onblender-20previews 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'sBadNodeIdUnknown; 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
FullNamestring). - 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.