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.
126 lines
5 KiB
Python
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/ci-server-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))
|