# 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 `` finding below (it carries exactly one ``, > 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 # 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 # 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. ## `` is a digit mask — the reason values read 0 `` on a `number` is a **digit mask**, not a Java `DecimalFormat` pattern: ```xml 99.99 ``` 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 `0` and `0.0` — `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: ``, ``, ``, …, ``. A masked number carries no ``. ### Two earlier conclusions here were wrong - **"Actions must be shared objects."** Not true. `WRPS_TagTest` round 1 bound all 49 items twice — once as `` in `globalSection`, once inline as `` — 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 `` and must not have ``."** Also not true, and it only looked true because every `` seen up to that point was a one-digit DecimalFormat pattern. A correctly sized mask works, and `` 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 `` plus `FixedPopup` and `default`; 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 `` 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 | `ItemValue`, **no** `` | ordinary read connection | | Range | n/a | `` / `` **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.