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.
18 KiB
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 inout/— 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>, the99.99set by hand). Do not deploy it and do not rebuild over it.
Regenerate with:
python 04-scada/hmi/build_display.py && python 04-scada/hmi/build_tagtest.py
Important
After any
.qliimport, 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
.qliimport 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. Changebuild_display.pyand 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:
04-scada/modbus_points/wrps_section_df.qli— sections04-scada/modbus_points/wrps_modbus_point_df.qli— Modbus points04-scada/modbus_points/wrps_item_df.qli— items- 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
actionSetItemAttributewith the value carried in the filter - navigation — pump symbols open
WRPS_FP_Pump, SETPOINTS… opensWRPS_FP_Setpoints, both faceplates return toWRPS_Overview - the wet well fill, a
dataBarbound toSTN.LEVEL, 0–100% - every setpoint is editable — a
numberFieldon the eight boxes ofWRPS_FP_Setpointsand 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
dataBarcarries its ownlowLimit/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
blinkEnabledandblinkDelay700 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
--verifycompares againstitem-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.--harvestthe saved file to re-pin it — but noteitem-ids.csvdescribes exactly one server at a time, so harvesting againstCORE_0001makes it wrong forPXiSEDev. 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 fromROOT,INSTALLandSTATIONingen_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.qlifield only,gen_ciserver_qli.py:289. No display carries a node reference, so a node rename needs no edit toout/*.xml.
The ritual — after ANY .qli import
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:
<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_TagTestround 1 bound all 49 items twice — once as<data ref>inglobalSection, 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.
actionActivateDisplayneeds<type value="ActivateSpecific"/>plus<layout>FixedPopup</layout>and<layoutFrame>default</layoutFrame>; without them it replaces the current display. Faceplate CLOSE buttons now useactionExitDisplay, which closes the popup rather than activating another display over it. - Grey blobs over the pump symbols. I had used a real
buttonas 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
- Text placement is approximate.
keepOriginalSizeis set and the width is estimated from character count, so a label may sit a few pixels off. The editor will normalise on save. - Pipes are rectangles, not
linecomponents. Thelinecomponent positions itself withcurvePoints/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. number.formatis emitted as a JavaDecimalFormatpattern (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
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.