wrps-demo-kit/04-scada/hmi/point_format.py
Clio Liu 947f632d7f feat(scada): CI Server tag database, historian, displays
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.
2026-09-02 17:16:18 +10:00

126 lines
5 KiB
Python

#!/usr/bin/env python3
"""Display format mask for every project item. Shared by both generators.
CI Server's `<format>` is a **digit mask**, not a Java DecimalFormat pattern:
<format>99.99</format> -> two integer digits, always two decimals
That is what CI View wrote when the FIT-201 INLET value was set to 99.99 by
hand, and it matches the `VALUE_FORMAT` masks in the item export
(`"99999"` on our items, `"99.99"` on the reference analog IO). `9` is a
digit position; the count of positions after the point is the number of
decimals always shown.
The first build emitted `<format>0</format>` and `<format>0.0</format>` -
DecimalFormat patterns, which read as masks **one digit wide**. Every value
wider than that had nowhere to render. That, not the missing `<value>`, is
why the displays read 0: `WRPS_TagTest` carried no `<format>` at all and all
98 bindings were live.
So a mask must be **wide enough for the point's full range**, or the value
may not fit. The masks below are sized from the register map's engineering
ranges, and points of the same kind share a mask so the screens stay
consistent:
level & speed in %, and their setpoints 999.9
flows (m3/h, three pumps reach ~1300) 9999.9
durations & hour counters (s / h) 99999
volume (m3) 9999
alarm bitmask (unsigned 16-bit) 99999
enums, counts, modes, booleans 9
Decimals follow the point's real resolution, not a fixed two: level in % of
the 6.000 m spill weir resolves to 0.017% because the register is mm, so one
decimal is honest and two would not be; a flow in m3/h resolves to 0.1.
Integer registers get no decimal at all - a decimal place there would be
invented precision.
**These masks must agree with `format_mask` in `04-scada/modbus_points/scada-points.csv`,**
which is what the item export carries into CI Server as `VALUE_FORMAT`. The
units and their gains are defined once, in `gen_scada_points.py`.
"""
# item short name (after "AID.WRPS.") -> mask
MASKS = {
# --- digital status, one digit ------------------------------------
"PU301.RUN_CMD": "9",
"PU302.RUN_CMD": "9",
"PU303.RUN_CMD": "9",
"PU301.RUNNING": "9",
"PU302.RUNNING": "9",
"PU303.RUNNING": "9",
"PU301.AVAILABLE": "9",
"PU302.AVAILABLE": "9",
"PU303.AVAILABLE": "9",
"STN.IN_AUTO": "9",
"STN.HIGH_LEVEL": "9",
"STN.SPILL_ACTIVE": "9",
"PU301.TRIPPED": "9",
"PU302.TRIPPED": "9",
"PU303.TRIPPED": "9",
# --- analog measurements ------------------------------------------
"STN.LEVEL": "999.9", # %, 100% = the 6.000 m spill weir
"STN.INFLOW": "9999.9", # m3/h, raw (L/s x10) x 0.36
"STN.DISCHARGE": "9999.9", # m3/h, three pumps reach ~1300
"STN.PUMPS_RUNNING": "9", # count 0-3
"STN.SPEED": "999.9", # %, 100% = 50 Hz
"STN.TIME_TO_SPILL": "99999", # s, 32767 = drawing down
"STN.TIME_TO_LSHH": "99999", # s, 32767 = drawing down
"STN.NET_ACCUM": "9999.9", # m3/h, SIGNED
"PU301.RUN_HOURS": "99999", # h
"PU302.RUN_HOURS": "99999",
"PU303.RUN_HOURS": "99999",
"STN.VOL_TO_SPILL": "9999", # m3
# --- enums and words ----------------------------------------------
"STN.STATE": "9", # enum 3.1
"PU301.STATE": "9", # enum 3.2
"PU302.STATE": "9",
"PU303.STATE": "9",
"STN.DUTY_PUMP": "9", # 0 = none, 1-3
"STN.ALARM_WORD": "99999", # unsigned 16-bit bitmask
"STN.CMD_ACK": "99",
# --- setpoints ------------------------------------------------------
"SP.MODE": "9", # 1 = auto, 2 = off
"SP.CMD_WORD": "99",
"SP.CMD_PARAM": "9", # pump number
"SP.LEVEL_SP": "999.9", # %, entered in % of spill weir
"SP.START_DUTY": "999.9", # %
"SP.START_P2": "999.9", # %
"SP.START_P3": "999.9", # %
"SP.STOP_ALL": "999.9", # %
"SP.HIGH_ALARM": "999.9", # %
"SP.MIN_SPEED": "999.9", # %, 100% = 50 Hz
"SP.SERVICE_HRS": "99999", # h
# --- simulation ------------------------------------------------------
"SIM.INFLOW": "9999.9", # m3/h
"SIM.SCENARIO": "9", # 0-3
"SIM.RESET": "9", # write 1, self-clearing
"SIM.TIME_SCALE": "999", # 1-120
}
PREFIX = "AID.WRPS."
def mask(item):
"""Mask for an item, by full or short name. Unknown -> widest integer."""
short = item[len(PREFIX):] if item.startswith(PREFIX) else item
return MASKS.get(short, "99999")
def decimals(item):
m = mask(item)
return len(m.split(".")[1]) if "." in m else 0
def check(names):
"""Fail loudly if an item has no mask - a new point must be given one."""
missing = [n for n in names
if (n[len(PREFIX):] if n.startswith(PREFIX) else n) not in MASKS]
if missing:
import sys
sys.exit("no format mask for: %s\n add it to point_format.py"
% ", ".join(missing))