wrps-demo-kit/04-scada/hmi/DEPLOY.md
Clio Liu 0927323e13 fix(scada): PS_* placeholder tags out, CI Server item names in
scada-points.csv led with a scada_tag column of PS_* names derived from
the PLC register map - PS_STN_WET_WELL_LEVEL and the like. CI Server never
adopted them. They named nothing that exists, nothing in this repo read
them, and read as if authoritative they have already caused a real defect
in a downstream project.

Replaced with ci-server-points.csv, the as-built delivery from the CI
Server side (copied from yau-plant-assistant, not re-derived). It is keyed
on the CI Server item, with the station and point name beside it:

    ci_item,ci_station,ci_point,description,poll_group,...

gen_scada_points.py now reproduces that file byte-identically, taking the
names from the LEAF table in gen_ciserver_qli.py so the CSV and the .qli
cannot disagree. tag_for() is gone. Verified: the CSV and all three .qli
regenerate unchanged, and all six displays still build.

The four poll groups keep their PS_ names - PS_STATUS_BITS, PS_PUBLISHED,
PS_SETPOINTS, PS_SIM_CONTROL. Those are live configuration, and are the
only legitimate PS_ names.

modbus_points/README.md gains a 'four namespaces' section - instrument tag,
PLC address, CI Server point, CI Server item - since confusing them is what
produced the placeholder column in the first place.

Also settles the historian drift left open in the previous commit. The
live export is the authority: the server runs WRPS_ONE_SEC at 5 SECONDS,
not 1, and WRPS_THIRTY_SEC at 30 seconds rather than a 60 second
WRPS_ONE_MIN. Code assuming 60 s was wrong by twelvefold. Recorded in
historian/README.md with a caution never to hardcode a sample interval.

Every .qli in the delivery was byte-identical to ours - only the CSV
differed.
2026-09-02 17:21:57 +10:00

356 lines
18 KiB
Markdown
Raw Permalink 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.

# Deploying the WRPS displays
Six displays are generated into `04-scada/hmi/out/` by `build_display.py`, plus
`WRPS_TagTest.xml` by `build_tagtest.py`:
| File | What | Size | Components |
|---|---|---|---|
| `WRPS_Overview.xml` | The operator display | 1920 × 1080 | 374 |
| `WRPS_Tank.xml` | Wet well detail; carries the setpoint ladder, opened from the WELL | 1920 × 1080 | 180 |
| `WRPS_FP_PU301.xml` | PU-301 pump faceplate | 760 × 700 | 109 |
| `WRPS_FP_PU302.xml` | PU-302 pump faceplate | 760 × 700 | 109 |
| `WRPS_FP_PU303.xml` | PU-303 pump faceplate | 760 × 700 | 109 |
| `WRPS_FP_Setpoints.xml` | Station setpoint faceplate | 900 × 700 | 51 |
| `WRPS_TagTest.xml` | Diagnostic list — every project item, plain vs masked | — | 49 items, 98 bindings |
Sizes and counts are what the build printed on 2026-08-20. It prints them on
every run — trust that over this table.
> `04-scada/hmi/WRPS_Overview.xml` — at the **root**, not in `out/` — is not one
> of these. It is the copy CI View itself saved during the id harvest, kept as
> evidence for the `<format>` finding below (it carries exactly one `<format>`,
> the `99.99` set by hand). Do not deploy it and do not rebuild over it.
Regenerate with:
```bash
python 04-scada/hmi/build_display.py && python 04-scada/hmi/build_tagtest.py
```
> [!IMPORTANT]
> **After any `.qli` import, re-harvest the item ids before rebuilding** — an
> import moves every id and the screens go dead without one visible symptom.
> The build now refuses to run when it cannot prove the ids are current; see
> **Item ids** below for the four-line ritual.
>
> This applies to a `.qli` import on **the same** server. Moving a display to a
> **different** CI Server instance is a different case and needs none of it —
> see *Moving a display to another CI Server instance*.
> **Never edit the XML by hand and never edit anything under
> `C:\Users\Public\Yokogawa\tls`.** Change `build_display.py` and rebuild —
> otherwise the next build silently discards the edit.
## WHERE / WHAT / WHAT FOR
**WHERE:** the CI Server machine (`PXiSEDev`).
**WHAT:** copy the files into the displays directory of the deployment —
alongside the existing `Straddle_*.xml`, i.e. the same folder the copy in
`99-reference/ciserver-hmi-deployment/displays/` came from. Then
open `WRPS_Overview` in the CI View editor.
**WHAT FOR:** CI Server loads displays from that directory by file name;
`WRPS_Overview` is what `actionActivateDisplay` targets from the faceplates, and
the faceplate names are what the pump symbols and the SETPOINTS… button target.
## Import order
The items must exist before the displays can bind to them:
1. `04-scada/modbus_points/wrps_section_df.qli` — sections
2. `04-scada/modbus_points/wrps_modbus_point_df.qli` — Modbus points
3. `04-scada/modbus_points/wrps_item_df.qli` — items
4. these displays
## What is live, and what is not
**Live on first open:**
- every number, bound to its item through `number.value``ItemValue`
- every button that writes — station mode, reset trips, reset hours, all four
simulation scenarios, time scale, reset scenario — via `actionSetItemAttribute`
with the value carried in the filter
- navigation — pump symbols open `WRPS_FP_Pump`, SETPOINTS… opens
`WRPS_FP_Setpoints`, both faceplates return to `WRPS_Overview`
- **the wet well fill**, a `dataBar` bound to `STN.LEVEL`, 0100%
- **every setpoint is editable** — a `numberField` on the eight boxes of
`WRPS_FP_Setpoints` and on SETPOINT in the overview rail
**Not yet, and deliberately:**
- **State colouring is static.** The pump bodies and status boxes are drawn in
their normal colours. Driving a *colour* from a value still needs item alarm
limits (which you deferred along with trends and alarms) or a threshold
configuration I could not confirm from the deployment, and guessing would have
produced silently wrong colours. The layout has the right shapes in the right
places, so this is a bind, not a redraw.
The level *fill* is no longer in this category: a `dataBar` carries its own
`lowLimit`/`highLimit`, so it needs no item limits at all. That was a wrong
assumption here, corrected after you added one by hand.
- **The alarm banner is not in this build**, for the same reason — it exists in
the preview and in `component-kit.md`, and follows once alarm limits are set.
- **Blink** likewise: the display sets `blinkEnabled` and `blinkDelay` 700 ms, so
the mechanism is ready, but nothing drives it yet.
## Item ids — the one thing that makes a screen lie
A connection needs **both** `itemName` and `itemId`. With the name alone every
value reads 0, and the screen looks perfectly healthy while meaning nothing.
This has bitten twice. It is now enforced by the build rather than by memory.
### If a display's values are not updating, do this first
> **Open the display in CI Server's Editor Module and validate it.**
Validation resolves every connection by name and rewrites the ids in the file. It
fixes the common case in seconds. Investigate nothing else until it has been ruled
out.
### Every item import kills every display
On 2026-08-14 the items were re-imported to change engineering units, keeping
every `NAME`, `NSID` and `ID_NUMBER` byte-identical. **Every itemId still moved.**
CI Server recreates items on import rather than updating them in place. Every
value on every screen went dead at once — that simultaneity is the signature.
So the sequence after any `dssqld -i` of items is always: **import → validate the
displays → re-harvest**, so the next build ships the current ids.
### Nothing is inferred — every id is measured
An id comes from CI Server or the build stops. There is no rule, no default and
no fallback. An id we invent that happens to be wrong produces the failure that
is hardest to see: a screen full of live-looking zeroes.
`WRPS_TagTest` binds **every** project item, which makes it the measuring
instrument: save it once in CI View and all 49 ids come back at once.
### CI View repairs ids on save — that is what makes this workable
Confirmed 2026-08-14. `WRPS_TagTest` was deployed with 25 of its 49 ids as
placeholders; those 25 rows read **0** while the other 24 read correctly. One
binding was then added by hand in CI View and the file saved — and CI View
rewrote **every** connection in the file with the right id, resolving them by
name. All 49 rows went live, and the saved file harvested all 49 in one pass.
Two things follow, and they are not in tension:
- **At runtime the id is what binds.** A wrong or placeholder id reads 0, name
notwithstanding. That is why the build refuses to emit an unmeasured id.
- **In the editor the name is authoritative.** So a display can always be healed
by opening and saving it, and that same save is the measurement.
This is why the bootstrap works at all: ship placeholders, let CI View resolve
them, harvest the result.
### Moving a display to another CI Server instance — import, then validate
Observed 2026-08-20, migrating to node `CORE_0001`.
The ids in `out/*.xml` are `PXiSEDev`'s. A different instance is a different
import, so a different `K`, so every one of them is wrong on the target. That
looks like it needs the full harvest ritual below. **It does not.** Importing
the display into the target resolves every connection by name — the same repair
described above, applied on the way in. The ids are healed by the import itself.
So moving displays between instances is **import, then validate.** No
re-harvest, no rebuild, no `.qli` round-trip, no regenerated XML. The files
built for one server are the files you ship to the next.
Validate after importing — the screen reading plausible live values rather than
a field of `0` is the signal. For the rigorous check, save the imported display
in CI View on the target and run `--verify` against it.
> [!WARNING]
> `--verify` compares against `item-ids.csv`, which still holds the **source**
> server's numbering, so expect it to flag every item on a freshly migrated
> target. That is the id file being stale for this server, not the display being
> broken. `--harvest` the saved file to re-pin it — but note `item-ids.csv`
> describes exactly one server at a time, so harvesting against `CORE_0001`
> makes it wrong for `PXiSEDev`. Branch the file per instance before doing this
> if both servers still matter.
Two things this depends on:
- **Item names must be identical.** The binding survives the move because the
name does. `AID.WRPS.*` is built from `ROOT`, `INSTALL` and `STATION` in
`gen_ciserver_qli.py:53-54` — if the target imported under different values,
the names differ and nothing resolves.
- **The node name is not in any display.** `FRONT_END_NODE` (`UNLICENCED`
`CORE_0001`) is a `.qli` field only, `gen_ciserver_qli.py:289`. No display
carries a node reference, so a node rename needs no edit to `out/*.xml`.
### The ritual — after ANY .qli import
```bash
python build_tagtest.py # the measuring display
# deploy WRPS_TagTest.xml, open it in CI View, link one value, save
python build_display.py --harvest <the saved WRPS_TagTest.xml> # all 49 measured
python build_display.py && python build_tagtest.py # now the real screens
# deploy out/*.xml, open WRPS_Overview in CI View, save
python build_display.py --verify <that saved display> # prove it
```
`build_tagtest.py` is the one build allowed to run with unmeasured ids — it has
to be, or there would be no way to measure the first one. It prints exactly how
many are placeholders, and those read 0 by design until harvested. Every other
display refuses to build until all of its items are measured.
`--verify` is the part that makes a dead screen impossible to miss. CI View
resolves every connection **by name** when it saves, so the ids in a saved file
are CI Server's own truth. The check compares them against what the build emits
and names any item that would read 0, exiting non-zero. Nobody has to look at a
screen and decide whether a `0` is a real zero.
### The guards, all hard failures
| Guard | Catches |
|---|---|
| **Every bound item must have a measured id**`check_ids()` lists all gaps at once | an inferred id, which is a guess about someone else's numbering |
| `item-ids.meta.json` records the SHA of the `wrps_item_df.qli` harvested against | items regenerated, so an import is coming or has happened |
| No measured ids at all → build fails | an uncalibrated build, which is a dead screen |
| `--verify` mismatch → exit 1 | ids that are already wrong, item by item |
None is a warning. Each error names the same one-command cure.
## `<format>` is a digit mask — the reason values read 0
`<format>` on a `number` is a **digit mask**, not a Java `DecimalFormat`
pattern:
```xml
<format>99.99</format> <!-- two integer digits, always two decimals -->
```
Each `9` is one digit position; the positions after the point are the decimals
always shown. This is what CI View itself wrote when the FIT-201 INLET value was
set to two decimals by hand, and it matches the `VALUE_FORMAT` masks in the item
exports (`"99999"` on our items, `"99.99"` on the reference analog IO).
The first build emitted `<format>0</format>` and `<format>0.0</format>`
`DecimalFormat` patterns, which read as masks **one digit wide**. Nothing wider
than a single digit had anywhere to render, so every level, flow and timer came
up empty. That is the whole bug.
**A mask must therefore be wide enough for the point's full engineering range.**
The masks live in `point_format.py`, one per item, sized from the register map
and shared between points of the same kind so the screens stay consistent:
| Kind | Mask | Why |
|---|---|---|
| level & level setpoints (%) | `999.9` | % of the 6.000 m spill weir |
| flows, net accumulation (m³/h) | `9999.9` | three pumps reach ~1300 |
| drive speed, min speed (%) | `999.9` | % of 50 Hz |
| durations (s), hour counters (h) | `99999` | 32767 = "drawing down" sentinel |
| volume to spill (m³) | `9999` | |
| alarm bitmask | `99999` | unsigned 16-bit |
| enums, counts, modes, booleans | `9` | single digit by definition |
Decimals follow each point's real resolution rather than a fixed two. Level in %
of the spill weir resolves to 0.017% because the register underneath is mm, and
a flow in m³/h to 0.1 — so both get one decimal, and a second would be invented
precision. Integer registers get none. `point_format.check()` fails the build if
a new item has no mask, and the masks are cross-checked against `format_mask` in
`04-scada/modbus_points/ci-server-points.csv`, which is what CI Server imports as
`VALUE_FORMAT`.
Element order matters to match what CI View saves: `<htmlId>`, `<format>`,
`<font>`, …, `<data>`. A masked number carries no `<value>`.
### Two earlier conclusions here were wrong
- **"Actions must be shared objects."** Not true. `WRPS_TagTest` round 1 bound
all 49 items twice — once as `<data ref>` in `globalSection`, once inline as
`<data type="data">` — and both columns updated identically. The generator
still emits shared objects because that is what CI View writes on save and it
de-duplicates, but it is a preference.
- **"A bound number needs `<value>` and must not have `<format>`."** Also not
true, and it only looked true because every `<format>` seen up to that point
was a one-digit DecimalFormat pattern. A correctly sized mask works, and
`<value>` is just the placeholder shown before the first update.
What round 1 *did* establish and still holds: the itemIds are right, including
all 23 that were only ever derived from the creation-index rule, and the CI
Server side — items, sections, Modbus points — is fine.
## Fixed after the first deploy
Three things the screenshot showed, all now corrected in the generator:
- **Popups opened full-screen.** `actionActivateDisplay` needs
`<type value="ActivateSpecific"/>` plus `<layout>FixedPopup</layout>` and
`<layoutFrame>default</layoutFrame>`; without them it replaces the current
display. Faceplate CLOSE buttons now use `actionExitDisplay`, which closes the
popup rather than activating another display over it.
- **Grey blobs over the pump symbols.** I had used a real `button` as the click
target, so it drew its own face. Replaced with a fully transparent rectangle
(`a="0"` paint) carrying the action — which is how the Straddle screens do
invisible hit areas.
- **Value boxes sitting on the pipes.** The per-pump boxes were at fixed
positions that crossed the discharge lines. Pump data now sits in its own
column at x=1380, the ladder labels moved outside the well, and the mimic was
re-spaced around a wider well.
## Other things I could not verify
1. **Text placement is approximate.** `keepOriginalSize` is set and the width is
estimated from character count, so a label may sit a few pixels off. The editor
will normalise on save.
3. **Pipes are rectangles, not `line` components.** The `line` component positions
itself with `curvePoints`/`curveSegments`, and the single example in the
deployment did not make its anchor convention unambiguous. Filled rectangles
are exact and are what the site's own screens use. Cost: no diagonals, which a
P&ID does not need.
4. **`number.format`** is emitted as a Java `DecimalFormat` pattern (`0`, `0.0`).
If the editor shows a different precision, that is the property to adjust.
## If the display opens empty or unbound
Deploy `WRPS_TagTest.xml` and open it — it takes graphics, layout and every
other display out of the picture, and shows all 49 items twice: unmasked on the
left, masked on the right. A blank on the right beside a live value on the left
is a mask that is too narrow. If it is live
and another display is not, the fault is in that display, not in the items or
the Modbus chain.
Otherwise check in this order: the `<format>` mask width (above) →
items imported (step 3 above) → item names match `AID.WRPS.*` exactly → the
`itemId` note above. `build_display.py` refuses to
build if any display binds to an item that is not in `wrps_item_df.qli`, so a
name mismatch cannot be the cause unless the items were imported under a
different root.
## Editable fields and bars — the two components CI View taught us
Both were added by hand in CI View on 2026-08-14 and copied out of the saved
file, rather than guessed from the component definitions:
| | `numberField` | `dataBar` |
|---|---|---|
| Property | `numberField.value` | `dataBar.value` |
| Geometry | **element attributes**, not child elements | same |
| Connection | `<itemAttribute>ItemValue</itemAttribute>`, **no** `<direction>` | ordinary read connection |
| Range | n/a | `<lowLimit>` / `<highLimit>` **on the component** |
| Format | none — a mask fights the editing | n/a |
The geometry difference is the trap: every other component here stores `x`, `y`,
`top`, `bottom`, `left`, `right` as child elements, and these two store them as
attributes on the opening tag.
`dataBar` needing no item configuration is what makes the live well fill
possible — `lowLimit` 0, `highLimit` 100 against a level that now reads in % of
the spill weir, so the bar maps onto the well with no scaling anywhere.
`Straddle_Detail.xml` in the reference deployment does the same for a fuel level.
## Layout is audited, not eyeballed
```bash
python check_layout.py
```
Estimates each text run's drawn extent and reports collisions. It caught the
wet well caption running into the top ladder label after the units relabel —
the 100% mark *is* the well top, so the caption and that label share a line.
Approximate by construction (it estimates glyph widths), so it is a net for
gross collisions, not a substitute for looking at the screen.