The old 05-scada/, restructured around the distinction its README never
drew: configuration is deployed with dssqld, displays are deployed by
file copy. Conflating the two is what made the folder confusing.
modbus_points/ the tag database - named for the protocol,
since CI Server configures others differently
modbus_points/historian/ 3 groups, 49 bindings - HAND-MADE, no
generator, and drifted from the server
hmi/ displays and their generator
ciserver-backup-2026-08/ outdated exports, evidence only, never import
QUICKLOAD.md dssqld export/import, the 5 classes, the import
order, and why an item import kills every display
README.md the chain end to end, and the not-updating triage
Verified during the move - the whole chain is reproducible:
scada-points.csv and all three .qli regenerate byte-identically
all six displays build clean
Removed the K offset machinery from build_display.py, item-ids.meta.json
and DEPLOY.md. K was a consistency check on measured ids, not a source of
them, and diagnosing a dead screen by arithmetic is wasted effort when
validating the display in CI Server's Editor Module fixes it outright.
The guidance now leads with that one action.
Recorded, not fixed: the repo's historian config disagrees with the
2026-08 CI Server export - WRPS_THIRTY_SEC and a FIVE_SECONDS group exist
on the server and not here. cicore1 was unreachable during this audit, so
which is correct is unknown.
Not carried across: __pycache__, out/*.xml, and the top-level
WRPS_Overview.xml that was tracked despite .gitignore declaring the
display XMLs to be build output.
356 lines
18 KiB
Markdown
356 lines
18 KiB
Markdown
# 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`, 0–100%
|
||
- **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/scada-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.
|