wrps-demo-kit/04-scada/modbus_points/README.md
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

10 KiB
Raw Blame History

modbus_points — the CI Server tag database

Named for the protocol: CI Server configures other protocols differently, so a folder called points would be ambiguous.

Three kinds of file live here, and only the first two are generated:

Generated scada-points.csv and the three wrps_*.qli
Generators gen_scada_points.py, gen_ciserver_qli.py
Hand-made historian/ — no generator, and it has drifted. See its README.

Deployed with dssqld, not by file copy — see ../QUICKLOAD.md.

python gen_scada_points.py     # register-map.csv -> scada-points.csv
python gen_ciserver_qli.py     # scada-points.csv -> the two .qli files
File What
scada-points.csv The point list, for reading and for configuring by hand
historian/ Collection groups and 49 item bindings — hand-made, see its README
wrps_section_df.qli @SECTION_DF — the six AID.WRPS.* sections
wrps_modbus_point_df.qli @MODBUS_POINT_DF — 49 Modbus point definitions, station WRPS_PLC
wrps_item_df.qli @ITEM_DF — 49 items in those sections, bound to those points

Import in that order. CI Server derives its hierarchy from the dots in a name, and a section must exist before an item can be created inside it.

The chain is ST sources → 03-plc/register-map.csvscada-points.csv.qli, so a register change propagates by re-running the scripts. Nothing here is hand-edited; if CI Server needs a field the generators do not emit, it goes in the generator.

Item structure under AID.WRPS

Section Points Contents
AID.WRPS.STN 16 Level, inflow, discharge, pumps running, speed, times, volumes, station state, duty pump, alarm word, command ack, in-auto, high level, spill
AID.WRPS.PU301/2/3 6 each Run command, running, available, tripped, pump state, run hours
AID.WRPS.SP 11 Mode, command word/param, level setpoints, high alarm, min speed, service interval
AID.WRPS.SIM 4 Scenario, manual inflow, reset, time scale

Equipment-oriented rather than address-oriented, so an HMI face for a pump binds to one folder, and the simulation controls sit apart from anything a real plant would have.

NSIDs

The NSID space is shared by sections and items. In your exports items occupy 3166 and sections 1170, so 171 is the first free id:

NSIDs
Sections STN, PU301, PU302, PU303, SP, SIM 171176
The 49 items 177225

AID (169) and AID.WRPS (170) already exist, so the section file emits only their six children, parented to 170. Pass --include-root to emit AID and AID.WRPS too, for a system that lacks them. Override with --nsid-base, --parent-nsid, or pin one with --section-nsid STN=41.

Address base — settled

--address-base 1 is correct. CI Server's IO_ADDRESS numbering is 1-based: holding register 0 (%QW0, wet well level) is RO:01.

Confirmed by import on 2026-08-14, cross-checking CI Server against a direct pymodbus read. The decisive check was self-consistency between two independent registers: STN.LEVEL 4140 mm and STN.VOL_TO_SPILL 223 m3 reconcile through the plant geometry, (6.000 - 4.140) x 120 = 223.2 m3. An off-by-one mapping cannot produce that agreement.

Engineering units — the SCADA side chooses them

The PLC publishes mm, L/s × 10 and Hz × 10, and that does not change: it is the contract in 03-plc/register-map.csv. What the operator reads is a presentation choice, made here, as a pure gain on the raw register:

engineering value = raw register × gain          (offset is always 0)
Item Unit Gain Basis
STN.LEVEL % 1/60 100% = the 6.000 m spill weir, register is mm
STN.INFLOW, STN.DISCHARGE, STN.NET_ACCUM, SIM.INFLOW m³/h 0.36 L/s × 3.6, register is L/s × 10
STN.SPEED % 0.2 100% = 50 Hz, the drive maximum
SP.LEVEL_SP, START_DUTY, START_P2/P3, STOP_ALL, HIGH_ALARM % 1/60 same scale as STN.LEVEL; entered in %
SP.MIN_SPEED % 0.2 same scale as STN.SPEED; entered in %

The gains live in one place — UNITS in gen_scada_points.py. Change a unit there and it propagates to scada-points.csv, to both .qli files and to the HMI masks.

Important

One item per I/O address — a register carries exactly one unit. Publishing level in m and % as two Modbus points on the same IO_ADDRESS was tried on 2026-08-14 and CI Server R1.03 refused the item import:

EQP-E-DUP_ITEM, I/O address of item already defined
DSSP-E-INSREC, Failed to insert a record in the dataset ITEM_DF

The point definitions are not the constraint; the item is. So a second unit for the same measurement needs a second PLC register publishing it, not a second view of one register — a register-map change, not a SCADA change. Level and speed are therefore published in % alone.

Setpoints are written in %. A setpoint entered as 70.0% arrives at the PLC as 4200 mm. One decimal of % is a 6 mm step, so not every mm value is reachable — 66.7% writes 4002 mm, not 4000. That is the intended trade for a single scale shared by the level reading and its setpoints.

Ranges

Analog points use Linear with a full-scale mapping: raw 32768..32767 to that range × the gain. That is exact and invents no plant range — which is why the offset must stay zero on both ends. It does make some PHYS values large (STN_SPEED spans ±6553.6%); that is the arithmetic of a full-scale map, not a plant range. Display ranges, trend limits and alarm limits are deliberately left at defaults for you to set per item — the generator does not guess at them.

HAS_SIGN is 1 for signed registers and 0 for STN_ALARM_WORD, which is unsigned; see below.

The point list

gen_scada_points.py reads 03-plc/register-map.csv (the PLC-side list, itself generated from the ST sources) and rewrites scada-points.csv. Re-run it after any register change so the two sides cannot drift — that is the §3 invariant. Anything changed by hand here is lost on the next run; if CI Server needs a field the script does not emit, it goes in the generator.

Connection

Protocol Modbus TCP, CI Server is the client/master
Host yau-sls-poc-lin00110.0.0.17, static
Port 502
Unit / slave id 1

The PLC publishes 502 on 10.0.0.17 only, not 0.0.0.0. CI Server reaches it over the LAN. This is the one place in the project where a literal IP is unavoidable — see ../../02-environment/.

Poll groups

Group FC Range Points What
PS_STATUS_BITS FC01 014 15 Run commands, running, available, in-auto, alarms, trips
PS_PUBLISHED FC03 020 19 Every live measurement and station/pump state
PS_SETPOINTS FC03/06 10241034 11 Mode, command word/param, level setpoints, limits
PS_SIM_CONTROL FC03/06 10441047 4 Scenario, manual inflow, reset, time scale

Each group is contiguous, so each is one request.

Three things that will bite

1. Do not poll %IW / %IX (FC04 / FC02). They are the field inputs. In the simulation build nothing writes them — they read 0 forever, and an HMI built on them shows a dead plant. Every live value is published in the %QW block instead. The generator excludes them by default; --include-field emits them for a real field deployment.

2. %QW17 (holding register 17, alarm bitmask) is UNSIGNED. Bit 15 does not fit a signed INT, so a signed configuration turns the alarm word negative exactly when the most severe alarm sets. The CSV marks it Unsigned 16-bit.

3. %MW is not holding register 0. %QW occupies 01023 and %MW starts at 1024, so the level setpoint %MW3 is holding register 1027. The CSV carries resolved addresses — use them, not the IEC names.

Driving the simulation from CI Server

The four PS_SIM_CONTROL points exist so scenarios can be run from the HMI rather than from a script:

Tag HR Values
PS_SIM_SCENARIO_* 1045 0 manual · 1 diurnal dry weather · 2 wet weather · 3 demo reference
PS_SIM_MANUAL_INFLOW 1044 m³/h in CI Server (the register is L/s × 10), scenario 0 only
PS_SIM_WRITE_1_TO_RESET_SCENARIO 1046 write 1 to reset; self-clearing
PS_SIM_TIME_SCALE_1_120 1047 1 while testing, 3060 when presenting

Reset restores the scenario's initial level and volume. It does not clear run hours or trip states — those live in the control logic, and need command word %MW1 = 1 (reset trips) or 5 (reset hours). See 03-plc/README.md.

These four registers only do anything in the simulation build. In a field build they exist but nothing reads them.

Scaling

raw_to_eng says how to convert, and eng_gain is the same thing as a number for the generators to use. Points with no unit override keep the PLC's own scaling — value / 10 where it publishes ×10, value otherwise. The points whose unit the SCADA side changes are listed under Engineering units above. format_mask is the display mask that becomes VALUE_FORMAT on the item, and it must stay in step with ../hmi/point_format.py.

Status

CI Server reads live PLC values (2026-08-14) — sections, points and items imported, values verified against a direct Modbus read. That is Phase 4's definition of done.

Units re-cut, 2026-08-14 — level in % of the spill weir, drive speed in % of 50 Hz, flows in m³/h, level and speed setpoints entered in %. No new items and no renumbering: the same 49 items, same NSIDs, same ID_NUMBERs, so a re-import of wrps_modbus_point_df.qli and wrps_item_df.qli updates them in place and the HMI's harvested item-ids.csv stays valid. Verified against a live read: raw 4217 → 70.3%, reconciling with VOL_TO_SPILL 214 m³ = (6.000 4.217) × 120; raw 404 → 80.8% of 50 Hz.

One thing left for you to set, deliberately not guessed at:

  • Alarm and trend limits. Every item imports with alarming off and limits at 0. Which points alarm, at what thresholds and priority, is engineering judgement about the plant, not something to derive from a register map.