Files
lmxopcua/docs/Uns.md
T

314 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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).