314 lines
14 KiB
Markdown
314 lines
14 KiB
Markdown
# UNS — Global Unified-Namespace Management
|
||
|
||
The **UNS** page (`/uns` in the AdminUI) is the single surface for managing
|
||
the Unified Namespace across the whole fleet. It replaces the old
|
||
per-cluster **UNS**, **Equipment**, and **Tags** tabs and the standalone
|
||
virtual-tags list — those have been removed; everything now lives in one
|
||
global master tree.
|
||
|
||
## The tree
|
||
|
||
The page shows every layer of the UNS as one expandable tree. Equipment is a
|
||
**leaf** in the tree — tags, virtual tags, and alarms live on a dedicated
|
||
equipment page, not inline:
|
||
|
||
```
|
||
Enterprise (read-only grouping — ServerCluster.Enterprise)
|
||
└─ Site / Cluster (read-only grouping — a ServerCluster row)
|
||
└─ Area (editable — UnsArea)
|
||
└─ Line (editable — UnsLine)
|
||
└─ Equipment (leaf — Equipment; opens its own page)
|
||
```
|
||
|
||
**Enterprise and Site/Cluster can be deleted from the tree** (see
|
||
[Cluster and Enterprise delete](#cluster-and-enterprise-delete) below).
|
||
Other than deletion, they are configured on the **Clusters** pages
|
||
(`/clusters`) — on a cluster row the **⚙ settings** link jumps to that
|
||
cluster. Editable UNS entities start at **Area**.
|
||
|
||
Count badges next to a node show how many direct children it has (for
|
||
equipment, the combined tag + virtual-tag count).
|
||
|
||
### Navigating
|
||
|
||
- **Expand all / Collapse all** toggle the structural levels.
|
||
- **Filter by name** does a case-insensitive substring match on the names of
|
||
a node's direct children.
|
||
|
||
## Creating and editing
|
||
|
||
Every editable row has inline actions:
|
||
|
||
| Node | Actions |
|
||
|---|---|
|
||
| Cluster | **+ Area**, Delete |
|
||
| Area | **+ Line**, Edit, Delete |
|
||
| Line | **+ Equipment**, Edit, Delete |
|
||
| Equipment | **Open**, Delete |
|
||
|
||
A **+ Child** action pre-fills the parent for you (e.g. **+ Line** on an
|
||
area opens the Line modal with that area already selected). Build a branch
|
||
top-down: Area → Line → Equipment. Tags, virtual tags, and scripted alarms
|
||
are managed on the equipment page (see below).
|
||
|
||
## Equipment page (`/uns/equipment/{id}`)
|
||
|
||
Clicking **Open** on an Equipment row (or **+ Equipment** on a Line) navigates
|
||
to the equipment page. The page is organised into four tabs:
|
||
|
||
| Tab | Content |
|
||
|---|---|
|
||
| **Details** | Equipment identity — name, description, OPC-40010 namespace fields, driver binding, and served-by cluster. (This is the former EquipmentModal, now a full page.) |
|
||
| **Tags** | Equipment-bound driver tags. Each tag uses the driver-typed config editor (same editors as before — Modbus, S7, AB CIP, etc.) with the same client-side validation. |
|
||
| **Virtual Tags** | Virtual tags driven by C# scripts. The inline Monaco script editor with Roslyn IntelliSense, completions, and live diagnostics is available here per tag. |
|
||
| **Alarms** | Scripted-alarm definitions bound to this equipment. Create, edit, or delete predicates here; the Monaco editor is available for each predicate script. |
|
||
|
||
**"Add equipment"** under a Line uses the URL `/uns/equipment/new?lineId=...`,
|
||
pre-filling the parent line. Saving redirects to the new equipment's page.
|
||
|
||
### Served-by cluster
|
||
|
||
An area's **cluster assignment is its "served-by" cluster** — the cluster
|
||
node that runs it. It's set when you create the area (under a cluster) and
|
||
changed by editing the area's cluster in the Area modal, which moves the
|
||
whole branch. There is no separate "served-by" concept and no migration —
|
||
it is simply `UnsArea.ClusterId`.
|
||
|
||
### Tags
|
||
|
||
Tags created on the equipment page are **equipment-bound** and require a driver
|
||
instance. The driver list on the Tags tab is scoped to the equipment's cluster
|
||
and to drivers on an **Equipment-kind** namespace, so a driver-less equipment
|
||
shows no eligible drivers until you bind one (edit the equipment on the Details
|
||
tab and pick a driver).
|
||
|
||
**Galaxy / AVEVA System Platform points are ordinary equipment tags** bound to
|
||
a `GalaxyMxGateway` driver instance. Author them on the Tags tab using the
|
||
standard Tag modal; the Galaxy address picker browses the live Galaxy hierarchy
|
||
so you can select the attribute and set `TagConfig.FullName`. There is no
|
||
separate alias concept or `SystemPlatform`-kind namespace.
|
||
|
||
### Virtual tags
|
||
|
||
A virtual tag is bound to an equipment and driven by a **script** (no driver).
|
||
Add and edit virtual tags on the equipment page's **Virtual Tags** tab; the
|
||
data type is chosen from the standard OPC UA type list and the Monaco script
|
||
editor is available inline.
|
||
|
||
### Galaxy tags
|
||
|
||
`GalaxyMxGateway` is an **Equipment-kind driver** — Galaxy points are ordinary
|
||
equipment tags authored through the standard Tag modal, exactly like Modbus or
|
||
S7 tags. The Galaxy reference is stored as `TagConfig.FullName`
|
||
(`tag_name.AttributeName`). The Galaxy address picker on the Tags tab lets you
|
||
browse the live Galaxy hierarchy to select an attribute; after selecting, set
|
||
the tag **Name**, **DataType**, and **AccessLevel** (default: read-only). There
|
||
is no alias concept, no `SystemPlatform`-kind namespace, and no relay→alias
|
||
converter.
|
||
|
||
## Typed TagConfig editors
|
||
|
||
The **Tag modal** dispatches a driver-typed config editor for the following
|
||
drivers:
|
||
|
||
| Driver | Fields in the typed editor |
|
||
|---|---|
|
||
| Modbus | Register type, address, data type, word order, etc. |
|
||
| S7 | Data block, offset, data type, etc. |
|
||
| AB CIP | Tag path, data type, etc. |
|
||
| AB Legacy (DF1/DH+) | Address, data type, etc. |
|
||
| TwinCAT | Symbol path, data type, etc. |
|
||
| FOCAS | PMC address, data type, etc. |
|
||
| **OpcUaClient** | `FullName` (the remote OPC UA node id string) |
|
||
| **Historian.Wonderware** | `FullName` (the Wonderware tagname to read) |
|
||
|
||
**OpcUaClient** and **Historian.Wonderware** were previously raw-JSON
|
||
fallback only; they now have first-class typed editors that expose a single
|
||
`FullName` field (PascalCase JSON key, consistent with the Galaxy editor
|
||
convention). Both are registered in `TagConfigEditorMap` and
|
||
`TagConfigValidator`; unknown keys in the stored JSON blob are preserved on
|
||
round-trip.
|
||
|
||
Drivers not yet listed above (e.g. Galaxy — which uses the Galaxy address
|
||
picker described below) still use the generic raw-`TagConfig`-JSON textarea.
|
||
|
||
### "Build address" pickers in protocol-driver editors
|
||
|
||
The **Modbus, S7, AB CIP, AB Legacy, TwinCAT, and FOCAS** typed editors
|
||
include a **Build address** button. Clicking it opens the driver's existing
|
||
address-builder UI inside the shared `DriverTagPicker` overlay; confirming a
|
||
selection writes the fully constructed address string back into the editor's
|
||
address field. This means you can visually compose a register reference
|
||
(e.g. select "Holding Register → 100 → Int16") and have it serialised into
|
||
the correct JSON without hand-editing.
|
||
|
||
## Historizing tags (first-class controls)
|
||
|
||
The Tag modal exposes **Historize this tag** and **Historian tagname
|
||
(override)** as explicit controls that work for **all drivers** — including
|
||
protocol drivers (Modbus, S7, etc.) that use the typed editor and raw-JSON
|
||
drivers (Galaxy) alike.
|
||
|
||
| Control | JSON key | Type | Behaviour |
|
||
|---|---|---|---|
|
||
| **Historize this tag** checkbox | `isHistorized` | bool | When checked, the OPC UA node materialises with `Historizing=true` and the `HistoryRead` AccessLevel bit set. |
|
||
| **Historian tagname (override)** textbox | `historianTagname` | string (optional) | Explicit tagname the historian backend will query. When left blank, the server defaults to the tag's driver `FullName`. |
|
||
|
||
These fields are merged into the `TagConfig` JSON blob via the pure
|
||
`TagHistorizeConfig` helper, which preserves all other keys byte-stable.
|
||
The server's OPC UA HistoryRead dispatch already consumes these keys from the
|
||
`TagConfig` blob — see [Historian.md](Historian.md) for the full server
|
||
behaviour, continuation-point paging, and aggregates.
|
||
|
||
> **Note — native-alarm `HistorizeToAveva`:** a tag that carries a native
|
||
> `"alarm"` object has a **separate** opt-out field `alarm.historizeToAveva`
|
||
> (a checkbox labelled "Historize to AVEVA" on the Tag modal's alarm section).
|
||
> That field controls whether the alarm's **transition events** are written to
|
||
> the AVEVA historian — it does not affect tag-value history (which is
|
||
> controlled by `isHistorized`). See [ScriptedAlarms.md §Native driver
|
||
> alarms](ScriptedAlarms.md#native-driver-alarms-equipment-tag-path) for
|
||
> details.
|
||
|
||
## Array tags (1-D)
|
||
|
||
A tag can be made a **1-D OPC UA array node** by setting two keys in its `TagConfig`
|
||
JSON blob. The controls are exposed as first-class UI fields in the Tag modal (an
|
||
**Is array** checkbox + an **Array length** numeric input), available for all drivers
|
||
— typed editors (Modbus, S7, etc.) and the raw-JSON textarea (Galaxy) alike.
|
||
|
||
### Canonical rule
|
||
|
||
| Condition | OPC UA node shape |
|
||
|---|---|
|
||
| `isArray: true` AND `arrayLength >= 1` | 1-D array node (`ValueRank = OneDimension`, `ArrayDimensions = [arrayLength]`) |
|
||
| `isArray: false` (any `arrayLength`) | Scalar node (default) |
|
||
| `isArray` absent | Scalar node (default) |
|
||
|
||
A single-element array (`isArray: true, arrayLength: 1`) is valid and materialises as a
|
||
`[1]` node.
|
||
|
||
### Fields
|
||
|
||
| Field | Type | Required | Description |
|
||
|---|---|---|---|
|
||
| `isArray` | bool | no | When `true` (and `arrayLength >= 1`), makes the node a 1-D array. Absent or `false` → scalar. |
|
||
| `arrayLength` | uint (≥ 1) | when `isArray: true` | The element count for the OPC UA `ArrayDimensions` declaration. Must be ≥ 1 when `isArray` is set. |
|
||
|
||
UI validation rejects `arrayLength = 0` when `isArray` is checked.
|
||
|
||
### Examples
|
||
|
||
```json
|
||
{"FullName":"PLC1.TemperatureArray","isArray":true,"arrayLength":10}
|
||
```
|
||
|
||
```json
|
||
{"Register":"HR100","DataType":"Float32","isArray":true,"arrayLength":5}
|
||
```
|
||
|
||
Combined with historization (values are arrays — history of the whole array snapshot):
|
||
|
||
```json
|
||
{"Register":"HR200","DataType":"Int16","isArray":true,"arrayLength":20,"isHistorized":true}
|
||
```
|
||
|
||
### Per-driver read mechanism and live-verify status
|
||
|
||
| Driver | Read mechanism | Live-verify |
|
||
|---|---|---|
|
||
| **Modbus** | Contiguous FC03/FC04 block (`arrayLength × registers-per-element`); String and BitInRegister array modes also supported | Mac-verifiable (sim `10.100.0.35:5020`) |
|
||
| **S7** | Contiguous `ReadBytesAsync` block over the declared address span + per-element decode loop | Unit-proven (sim fixture down) |
|
||
| **AB CIP** | libplctag native array read (atomic and UDT member arrays) | Unit-proven (sim fixture down) |
|
||
| **AB Legacy** | PCCC multi-element file read via libplctag (cap 256 elements) | Unit-proven (sim fixture down) |
|
||
| **TwinCAT** | ADS native array symbol read against the declared `SymbolPath` | Unit-proven (sim fixture down) |
|
||
|
||
### Out of scope (named deferrals)
|
||
|
||
- **Array writes** (inbound client→device write of an array value) — tagged for a follow-up phase.
|
||
- **Multi-dimensional arrays** (`ValueRank > 1`) — not supported; all arrays are 1-D.
|
||
- **Array historization** — a historized array tag materialises with the correct `Historizing` flag, but the Wonderware sidecar historian treats the value as an opaque blob; per-element history is out of scope.
|
||
|
||
See the individual driver docs under `docs/drivers/` for per-driver implementation details.
|
||
|
||
## Galaxy address picker — native-alarm pre-fill
|
||
|
||
When the Galaxy address picker selects an attribute that is itself an alarm
|
||
(`IsAlarm == true` in the Galaxy hierarchy), the Tag modal automatically
|
||
seeds a default `alarm` object in the tag config:
|
||
|
||
```json
|
||
{"alarmType":"OffNormalAlarm","severity":700}
|
||
```
|
||
|
||
This lets the operator author the native alarm in a single picker pass
|
||
without hand-editing JSON. The pre-fill **never overwrites** an alarm object
|
||
that is already present — if the tag already has a custom `alarm` section, the
|
||
picker leaves it untouched.
|
||
|
||
## Cluster and Enterprise delete
|
||
|
||
### Cluster delete
|
||
|
||
A **Delete** action is available on Cluster rows in the UNS tree. The server
|
||
refuses the delete if the cluster still has any Areas (children) — the same
|
||
refuse-if-children guard used by Area and Line delete. Remove all Areas (and
|
||
their descendant Lines/Equipment) first, then delete the cluster.
|
||
|
||
> **No RowVersion concurrency check.** `ServerCluster` does not carry a
|
||
> concurrency token, so the delete does not have the last-writer-wins protection
|
||
> that Area/Line/Equipment deletes have. A follow-up migration will add the
|
||
> token; for now, coordinate cluster deletes manually.
|
||
|
||
### Enterprise delete
|
||
|
||
An **Enterprise** row is a read-only grouping label (the `Enterprise` column
|
||
of `ServerCluster`) — it is not a separate entity. Deleting an Enterprise row
|
||
**deletes all clusters whose `Enterprise` matches that label**, all-or-nothing:
|
||
|
||
- If any cluster under the enterprise still has children, the entire delete is
|
||
refused and no clusters are removed.
|
||
- If all clusters are empty, every cluster under that enterprise is deleted in a
|
||
single transaction.
|
||
|
||
Remove all Areas under every cluster in the enterprise first, then delete the
|
||
enterprise label.
|
||
|
||
## Create-new-script inline (virtual-tag panel)
|
||
|
||
On the equipment page's **Virtual Tags** tab, when a virtual tag is not yet
|
||
bound to any script, the inline script panel shows a **Create new script**
|
||
button. Clicking it:
|
||
|
||
1. Generates a new blank Script record with an auto-generated `SC-…` id.
|
||
2. Binds the virtual tag to that script.
|
||
3. Expands the Monaco editor inline so you can begin authoring immediately.
|
||
|
||
This removes the previous two-step flow (create a script on a separate page,
|
||
then attach it to the virtual tag); the entire lifecycle now lives on the
|
||
equipment page.
|
||
|
||
## Hosts page per-driver-instance rows (deferred)
|
||
|
||
Phase 6 did **not** implement per-driver-instance status rows on the Hosts
|
||
page. This item (H7-runtime) is **F7-runtime-blocked**: the runtime plumbing
|
||
needed to surface per-instance health rows is not yet in place. It remains on
|
||
the backlog.
|
||
|
||
## Bulk import
|
||
|
||
**Import equipment CSV** (toolbar) bulk-creates equipment across many lines
|
||
and clusters in one pass. After an import the whole tree reloads.
|
||
|
||
## Applying changes
|
||
|
||
Edits here change the configuration only. As the page header notes,
|
||
**changes apply on the next deployment** — run a **Deploy** (Deployments
|
||
page) to push them into the running address space.
|
||
|
||
## See also
|
||
|
||
- [Configuration.md](Configuration.md) — the underlying config entities.
|
||
- [VirtualTags.md](VirtualTags.md) — the scripting/virtual-tag engine.
|
||
- [ScriptedAlarms.md](ScriptedAlarms.md) — scripted-alarm engine internals (predicates, state machine, ack/shelve).
|
||
- Design + decision log: [plans/2026-06-08-global-uns-management-design.md](plans/2026-06-08-global-uns-management-design.md).
|