diff --git a/04-scada/README.md b/04-scada/README.md index bb76171..f0b2b24 100644 --- a/04-scada/README.md +++ b/04-scada/README.md @@ -30,7 +30,7 @@ Everything derives from the PLC. The PLC side leads, always. 03-plc/register-map.csv the contract - 69 points │ gen_scada_points.py ▼ -modbus_points/scada-points.csv the SCADA view - 49 points, poll groups, units +modbus_points/ci-server-points.csv the SCADA view - 49 points, poll groups, units │ gen_ciserver_qli.py ▼ modbus_points/*.qli 6 sections · 49 Modbus points · 49 items @@ -100,9 +100,9 @@ look. | | | |---|---| | Points and items | **Imported and reading live PLC values** (2026-08-14), verified against a direct Modbus read | -| Generators | Verified reproducible 2026-09-02 — `scada-points.csv` and all three `.qli` regenerate byte-identically | +| Generators | Verified reproducible 2026-09-02 — `ci-server-points.csv` and all three `.qli` regenerate byte-identically | | Displays | Six built; `build_display.py` runs clean | -| Historian | 3 groups, 49 bindings — **hand-made, and drifted from the server** | +| Historian | 3 groups, 49 bindings — **hand-made, and the live rates differ from these files.** The server runs `WRPS_ONE_SEC` at 5 s and `WRPS_THIRTY_SEC` at 30 s. See `modbus_points/historian/README.md`. | | CI Server state | ⚠️ **Unverified.** No access to `cicore1` during this audit. Everything here is what the repo believes, not what the server holds. | ### Two things left for you diff --git a/04-scada/ciserver-backup-2026-08/README.md b/04-scada/ciserver-backup-2026-08/README.md index e49d21b..66af1e7 100644 --- a/04-scada/ciserver-backup-2026-08/README.md +++ b/04-scada/ciserver-backup-2026-08/README.md @@ -21,9 +21,11 @@ as opposed to what the repo believes was configured. An export holds **every** record of its class, not only this project's. Filter on `AID.WRPS` when comparing. -## The drift they revealed +## The drift they revealed — and settled -Comparing these against `../modbus_points/historian/` shows the two do not agree: +Comparing these against `../modbus_points/historian/` shows the two do not agree. +**These exports are the authority**: they record what the running server actually +did, whereas `his_group.qli` records what the source project intended. | In this export | In the repo's source | |---|---| @@ -34,6 +36,11 @@ Comparing these against `../modbus_points/historian/` shows the two do not agree That is the whole reason this folder exists rather than being deleted. +**The rates matter more than the names.** The live `WRPS_ONE_SEC` runs at **5 +seconds**, not 1, and the second group is `WRPS_THIRTY_SEC` at **30 seconds**, not +a 60 second `WRPS_ONE_MIN`. Code that assumed 60 seconds was wrong by twelvefold. +Never hardcode a sample interval — read it from the item. + ## Replacing it When `cicore1` is reachable, re-export the five classes (`../../QUICKLOAD.md`), diff --git a/04-scada/hmi/DEPLOY.md b/04-scada/hmi/DEPLOY.md index cd6903b..2f7496a 100644 --- a/04-scada/hmi/DEPLOY.md +++ b/04-scada/hmi/DEPLOY.md @@ -252,7 +252,7 @@ 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 +`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: ``, ``, diff --git a/04-scada/hmi/point_format.py b/04-scada/hmi/point_format.py index 475af8e..4a1087a 100644 --- a/04-scada/hmi/point_format.py +++ b/04-scada/hmi/point_format.py @@ -35,7 +35,7 @@ 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`,** +**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`. """ diff --git a/04-scada/hmi/render_preview.py b/04-scada/hmi/render_preview.py index 3daeabf..5431e94 100644 --- a/04-scada/hmi/render_preview.py +++ b/04-scada/hmi/render_preview.py @@ -42,7 +42,7 @@ from pathlib import Path HERE = Path(__file__).resolve().parent OUT = HERE / "out" -POINTS = HERE.parent / "modbus_points" / "scada-points.csv" +POINTS = HERE.parent / "modbus_points" / "ci-server-points.csv" QLI_GEN = HERE.parent / "modbus_points" / "gen_ciserver_qli.py" PLC_HOST, PLC_PORT, PLC_UNIT = "192.168.153.192", 502, 1 diff --git a/04-scada/modbus_points/README.md b/04-scada/modbus_points/README.md index a1f96ce..d41e73b 100644 --- a/04-scada/modbus_points/README.md +++ b/04-scada/modbus_points/README.md @@ -7,20 +7,55 @@ Three kinds of file live here, and only the first two are generated: | | | |---|---| -| **Generated** | `scada-points.csv` and the three `wrps_*.qli` | +| **Generated** | `ci-server-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`. +## Four namespaces, and which one is which + +Getting these confused is the single most common mistake in this folder. + +``` +LIT-101 instrument tag the field device +%QW0 PLC symbol/address the register the PLC writes +WRPS_PLC:STN_LEVEL CI Server point Modbus station : point name +AID.WRPS.STN.LEVEL CI Server item what displays bind to, and what + the historian is keyed on +``` + +`ci-server-points.csv` carries all four, one row per point: `ci_item`, +`ci_station`, `ci_point`, `iec_address` and `plc_tag`. + +**The historian is keyed on the item, and only the item.** Modbus carries register +numbers, not names, so the point layer and the item layer are free to differ — and +they do. + +> [!WARNING] +> **`PS_*` is not one of these namespaces.** +> An earlier version of this file carried a `scada_tag` column with names like +> `PS_STN_WET_WELL_LEVEL`, derived from the PLC register map. **CI Server never +> adopted them.** They named nothing that exists, and they caused a real defect +> downstream when read as if they did. +> +> The **only** legitimate `PS_` names are the four Modbus **poll groups** in the +> `poll_group` column — `PS_STATUS_BITS`, `PS_PUBLISHED`, `PS_SETPOINTS`, +> `PS_SIM_CONTROL`. Those are live configuration. +> +> `ci-server-points.csv` is the as-built delivery from the CI Server side. +> `gen_scada_points.py` reproduces it byte-identically, taking the item names from +> the `LEAF` table in `gen_ciserver_qli.py` so the CSV and the `.qli` cannot +> disagree. + ```bash -python gen_scada_points.py # register-map.csv -> scada-points.csv -python gen_ciserver_qli.py # scada-points.csv -> the two .qli files +python gen_scada_points.py # register-map.csv -> ci-server-points.csv +python gen_ciserver_qli.py # ci-server-points.csv -> the two .qli files ``` | File | What | |---|---| -| `scada-points.csv` | The point list, for reading and for configuring by hand | +| `ci-server-points.csv` | The point list, keyed on the **CI Server item** each point feeds | | `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` | @@ -29,7 +64,7 @@ python gen_ciserver_qli.py # scada-points.csv -> the two .qli files **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.csv` → `scada-points.csv` → `.qli`, +The chain is ST sources → `03-plc/register-map.csv` → `ci-server-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. @@ -90,7 +125,7 @@ presentation choice, made here, as a pure gain on the raw register: | `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 +there and it propagates to `ci-server-points.csv`, to both `.qli` files and to the HMI masks. > [!IMPORTANT] @@ -128,7 +163,7 @@ 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 +generated from the ST sources) and rewrites `ci-server-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. diff --git a/04-scada/modbus_points/ci-server-points.csv b/04-scada/modbus_points/ci-server-points.csv new file mode 100644 index 0000000..54729b6 --- /dev/null +++ b/04-scada/modbus_points/ci-server-points.csv @@ -0,0 +1,50 @@ +ci_item,ci_station,ci_point,description,poll_group,function_code,modbus_address,data_type,access,eng_units,raw_to_eng,eng_gain,format_mask,iec_address,plc_tag,notes +AID.WRPS.PU301.RUN_CMD,WRPS_PLC,PU301_RUN_CMD,Run command,PS_STATUS_BITS,FC01,0,Boolean,Read,,value,1.0,99999,%QX0.0,PU-301, +AID.WRPS.PU302.RUN_CMD,WRPS_PLC,PU302_RUN_CMD,Run command,PS_STATUS_BITS,FC01,1,Boolean,Read,,value,1.0,99999,%QX0.1,PU-302, +AID.WRPS.PU303.RUN_CMD,WRPS_PLC,PU303_RUN_CMD,Run command,PS_STATUS_BITS,FC01,2,Boolean,Read,,value,1.0,99999,%QX0.2,PU-303, +AID.WRPS.PU301.RUNNING,WRPS_PLC,PU301_RUNNING,Running,PS_STATUS_BITS,FC01,3,Boolean,Read,,value,1.0,99999,%QX0.3,PU-301, +AID.WRPS.PU302.RUNNING,WRPS_PLC,PU302_RUNNING,Running,PS_STATUS_BITS,FC01,4,Boolean,Read,,value,1.0,99999,%QX0.4,PU-302, +AID.WRPS.PU303.RUNNING,WRPS_PLC,PU303_RUNNING,Running,PS_STATUS_BITS,FC01,5,Boolean,Read,,value,1.0,99999,%QX0.5,PU-303, +AID.WRPS.PU301.AVAILABLE,WRPS_PLC,PU301_AVAILABLE,Available,PS_STATUS_BITS,FC01,6,Boolean,Read,,value,1.0,99999,%QX0.6,PU-301, +AID.WRPS.PU302.AVAILABLE,WRPS_PLC,PU302_AVAILABLE,Available,PS_STATUS_BITS,FC01,7,Boolean,Read,,value,1.0,99999,%QX0.7,PU-302, +AID.WRPS.PU303.AVAILABLE,WRPS_PLC,PU303_AVAILABLE,Available,PS_STATUS_BITS,FC01,8,Boolean,Read,,value,1.0,99999,%QX1.0,PU-303, +AID.WRPS.STN.IN_AUTO,WRPS_PLC,STN_IN_AUTO,Station in auto,PS_STATUS_BITS,FC01,9,Boolean,Read,,value,1.0,99999,%QX1.1,STN, +AID.WRPS.STN.HIGH_LEVEL,WRPS_PLC,STN_HIGH_LEVEL,High level alarm,PS_STATUS_BITS,FC01,10,Boolean,Read,,value,1.0,99999,%QX1.2,STN, +AID.WRPS.STN.SPILL_ACTIVE,WRPS_PLC,STN_SPILL_ACTIVE,Spill active,PS_STATUS_BITS,FC01,11,Boolean,Read,,value,1.0,99999,%QX1.3,STN, +AID.WRPS.PU301.TRIPPED,WRPS_PLC,PU301_TRIPPED,Tripped,PS_STATUS_BITS,FC01,12,Boolean,Read,,value,1.0,99999,%QX1.4,PU-301, +AID.WRPS.PU302.TRIPPED,WRPS_PLC,PU302_TRIPPED,Tripped,PS_STATUS_BITS,FC01,13,Boolean,Read,,value,1.0,99999,%QX1.5,PU-302, +AID.WRPS.PU303.TRIPPED,WRPS_PLC,PU303_TRIPPED,Tripped,PS_STATUS_BITS,FC01,14,Boolean,Read,,value,1.0,99999,%QX1.6,PU-303, +AID.WRPS.STN.LEVEL,WRPS_PLC,STN_LEVEL,Wet well level,PS_PUBLISHED,FC03,0,Signed 16-bit,Read,%,value / 60,0.016666666666666666,999.9,%QW0,STN, +AID.WRPS.STN.INFLOW,WRPS_PLC,STN_INFLOW,Inflow,PS_PUBLISHED,FC03,1,Signed 16-bit,Read,m3/h,value * 0.36,0.36,9999.9,%QW1,STN, +AID.WRPS.STN.DISCHARGE,WRPS_PLC,STN_DISCHARGE,Total discharge flow,PS_PUBLISHED,FC03,2,Signed 16-bit,Read,m3/h,value * 0.36,0.36,9999.9,%QW2,STN, +AID.WRPS.STN.PUMPS_RUNNING,WRPS_PLC,STN_PUMPS_RUNNING,Pumps running,PS_PUBLISHED,FC03,3,Signed 16-bit,Read,count,value,1.0,9,%QW3,STN, +AID.WRPS.STN.SPEED,WRPS_PLC,STN_SPEED,Common drive speed,PS_PUBLISHED,FC03,4,Signed 16-bit,Read,%,value * 0.2,0.2,999.9,%QW4,STN, +AID.WRPS.STN.TIME_TO_SPILL,WRPS_PLC,STN_TIME_TO_SPILL,Time to spill weir (32767 = drawing down),PS_PUBLISHED,FC03,5,Signed 16-bit,Read,s,value,1.0,99999,%QW5,STN, +AID.WRPS.STN.TIME_TO_LSHH,WRPS_PLC,STN_TIME_TO_LSHH,Time to LSHH (32767 = drawing down),PS_PUBLISHED,FC03,6,Signed 16-bit,Read,s,value,1.0,99999,%QW6,STN, +AID.WRPS.STN.NET_ACCUM,WRPS_PLC,STN_NET_ACCUM,Net accumulation (signed),PS_PUBLISHED,FC03,7,Signed 16-bit,Read,m3/h,value * 0.36,0.36,9999.9,%QW7,STN, +AID.WRPS.PU301.RUN_HOURS,WRPS_PLC,PU301_RUN_HOURS,Run hours,PS_PUBLISHED,FC03,8,Signed 16-bit,Read,h,value,1.0,99999,%QW8,PU-301, +AID.WRPS.PU302.RUN_HOURS,WRPS_PLC,PU302_RUN_HOURS,Run hours,PS_PUBLISHED,FC03,9,Signed 16-bit,Read,h,value,1.0,99999,%QW9,PU-302, +AID.WRPS.PU303.RUN_HOURS,WRPS_PLC,PU303_RUN_HOURS,Run hours,PS_PUBLISHED,FC03,10,Signed 16-bit,Read,h,value,1.0,99999,%QW10,PU-303, +AID.WRPS.STN.VOL_TO_SPILL,WRPS_PLC,STN_VOL_TO_SPILL,Volume remaining to spill,PS_PUBLISHED,FC03,11,Signed 16-bit,Read,m3,value,1.0,9999,%QW11,STN, +AID.WRPS.STN.STATE,WRPS_PLC,STN_STATE,Station state (enum 3.1),PS_PUBLISHED,FC03,12,Signed 16-bit,Read,,value,1.0,9,%QW12,STN, +AID.WRPS.PU301.STATE,WRPS_PLC,PU301_STATE,Pump state (enum 3.2),PS_PUBLISHED,FC03,13,Signed 16-bit,Read,,value,1.0,9,%QW13,PU-301, +AID.WRPS.PU302.STATE,WRPS_PLC,PU302_STATE,Pump state (enum 3.2),PS_PUBLISHED,FC03,14,Signed 16-bit,Read,,value,1.0,9,%QW14,PU-302, +AID.WRPS.PU303.STATE,WRPS_PLC,PU303_STATE,Pump state (enum 3.2),PS_PUBLISHED,FC03,15,Signed 16-bit,Read,,value,1.0,9,%QW15,PU-303, +AID.WRPS.STN.DUTY_PUMP,WRPS_PLC,STN_DUTY_PUMP,"Current duty pump (0 = none, 1-3)",PS_PUBLISHED,FC03,16,Signed 16-bit,Read,,value,1.0,9,%QW16,STN, +AID.WRPS.STN.ALARM_WORD,WRPS_PLC,STN_ALARM_WORD,Alarm bitmask (section 6) - READ AS UNSIGNED,PS_PUBLISHED,FC03,17,Unsigned 16-bit,Read,,value,1.0,99999,%QW17,STN, +AID.WRPS.STN.CMD_ACK,WRPS_PLC,STN_CMD_ACK,Command acknowledge (echoes %MW1),PS_PUBLISHED,FC03,20,Signed 16-bit,Read,,value,1.0,99,%QW20,STN, +AID.WRPS.SP.MODE,WRPS_PLC,SP_MODE,"Station mode: 1 = auto, 2 = off",PS_SETPOINTS,FC03/FC06,1024,Signed 16-bit,Read/Write,,value,1.0,9,%MW0,STN, +AID.WRPS.SP.CMD_WORD,WRPS_PLC,SP_CMD_WORD,Command word (section 3.3),PS_SETPOINTS,FC03/FC06,1025,Signed 16-bit,Read/Write,,value,1.0,99,%MW1,STN, +AID.WRPS.SP.CMD_PARAM,WRPS_PLC,SP_CMD_PARAM,Command parameter (pump number),PS_SETPOINTS,FC03/FC06,1026,Signed 16-bit,Read/Write,,value,1.0,9,%MW2,STN, +AID.WRPS.SP.LEVEL_SP,WRPS_PLC,SP_LEVEL_SP,Level control setpoint,PS_SETPOINTS,FC03/FC06,1027,Signed 16-bit,Read/Write,%,value / 60,0.016666666666666666,999.9,%MW3,STN, +AID.WRPS.SP.START_DUTY,WRPS_PLC,SP_START_DUTY,Start duty level,PS_SETPOINTS,FC03/FC06,1028,Signed 16-bit,Read/Write,%,value / 60,0.016666666666666666,999.9,%MW4,STN, +AID.WRPS.SP.START_P2,WRPS_PLC,SP_START_P2,Start pump 2 level,PS_SETPOINTS,FC03/FC06,1029,Signed 16-bit,Read/Write,%,value / 60,0.016666666666666666,999.9,%MW5,STN, +AID.WRPS.SP.START_P3,WRPS_PLC,SP_START_P3,Start pump 3 level,PS_SETPOINTS,FC03/FC06,1030,Signed 16-bit,Read/Write,%,value / 60,0.016666666666666666,999.9,%MW6,STN, +AID.WRPS.SP.STOP_ALL,WRPS_PLC,SP_STOP_ALL,Stop all level,PS_SETPOINTS,FC03/FC06,1031,Signed 16-bit,Read/Write,%,value / 60,0.016666666666666666,999.9,%MW7,STN, +AID.WRPS.SP.HIGH_ALARM,WRPS_PLC,SP_HIGH_ALARM,High level alarm,PS_SETPOINTS,FC03/FC06,1032,Signed 16-bit,Read/Write,%,value / 60,0.016666666666666666,999.9,%MW8,STN, +AID.WRPS.SP.MIN_SPEED,WRPS_PLC,SP_MIN_SPEED,Minimum drive speed,PS_SETPOINTS,FC03/FC06,1033,Signed 16-bit,Read/Write,%,value * 0.2,0.2,999.9,%MW9,STN, +AID.WRPS.SP.SERVICE_HRS,WRPS_PLC,SP_SERVICE_HRS,Service interval,PS_SETPOINTS,FC03/FC06,1034,Signed 16-bit,Read/Write,h,value,1.0,99999,%MW10,STN, +AID.WRPS.SIM.INFLOW,WRPS_PLC,SIM_INFLOW,manual inflow (mode 0),PS_SIM_CONTROL,FC03/FC06,1044,Signed 16-bit,Read/Write,m3/h,value * 0.36,0.36,9999.9,%MW20,SIM,SIMULATION CONTROL - simulation build only +AID.WRPS.SIM.SCENARIO,WRPS_PLC,SIM_SCENARIO,scenario 0=man 1=diurnal 2=wet 3=ref,PS_SIM_CONTROL,FC03/FC06,1045,Signed 16-bit,Read/Write,,value,1.0,9,%MW21,SIM,SIMULATION CONTROL - simulation build only +AID.WRPS.SIM.RESET,WRPS_PLC,SIM_RESET,"write 1 to reset scenario, self-clearing",PS_SIM_CONTROL,FC03/FC06,1046,Signed 16-bit,Read/Write,,value,1.0,9,%MW22,SIM,SIMULATION CONTROL - simulation build only +AID.WRPS.SIM.TIME_SCALE,WRPS_PLC,SIM_TIME_SCALE,time scale 1-120,PS_SIM_CONTROL,FC03/FC06,1047,Signed 16-bit,Read/Write,x,value,1.0,999,%MW23,SIM,SIMULATION CONTROL - simulation build only diff --git a/04-scada/modbus_points/gen_ciserver_qli.py b/04-scada/modbus_points/gen_ciserver_qli.py index 487c1c5..3be4050 100644 --- a/04-scada/modbus_points/gen_ciserver_qli.py +++ b/04-scada/modbus_points/gen_ciserver_qli.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """Emit CI Server .qli imports for the WRPS points. - python gen_scada_points.py # first: refresh scada-points.csv + python gen_scada_points.py # first: refresh ci-server-points.csv python gen_ciserver_qli.py Writes, alongside this script: @@ -13,7 +13,7 @@ Writes, alongside this script: Import them 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 in it. -The chain is ST sources -> 03-plc/register-map.csv -> scada-points.csv -> +The chain is ST sources -> 03-plc/register-map.csv -> ci-server-points.csv -> these files, so a register change propagates by re-running the three scripts rather than by hand-editing anything. @@ -48,7 +48,7 @@ import sys from pathlib import Path HERE = Path(__file__).resolve().parent -POINTS = HERE / "scada-points.csv" +POINTS = HERE / "ci-server-points.csv" STATION = "WRPS_PLC" ROOT = "AID.WRPS" @@ -331,7 +331,7 @@ def main(): args = ap.parse_args() if not POINTS.is_file(): - sys.exit("missing scada-points.csv - run gen_scada_points.py first") + sys.exit("missing ci-server-points.csv - run gen_scada_points.py first") pinned = {} for spec in args.section_nsid: diff --git a/04-scada/modbus_points/gen_scada_points.py b/04-scada/modbus_points/gen_scada_points.py index 532ca9c..25742a4 100644 --- a/04-scada/modbus_points/gen_scada_points.py +++ b/04-scada/modbus_points/gen_scada_points.py @@ -4,7 +4,7 @@ python gen_scada_points.py Reads ../../03-plc/register-map.csv (PLC side, generated by build.py) -Writes ./scada-points.csv (SCADA side, for CI Server config) +Writes ./ci-server-points.csv (SCADA side, for CI Server config) CLAUDE.md section 3: the two sides are two views of the same points, and the PLC side leads. This script makes the SCADA view reproducible @@ -48,7 +48,7 @@ from pathlib import Path ROOT = Path(__file__).resolve().parents[2] SRC_MAP = ROOT / "03-plc" / "register-map.csv" -OUT = Path(__file__).resolve().parent / "scada-points.csv" +OUT = Path(__file__).resolve().parent / "ci-server-points.csv" # Modbus object -> (function code, poll group name) # @@ -165,17 +165,31 @@ def gain_expr(gain): return "value * %g" % gain -def tag_for(row, description): - """Tag name per CLAUDE.md section 8: PS__. +# The CI Server naming, imported from the .qli generator so the two files +# cannot disagree. LEAF is keyed on IEC address and is the single place +# section/leaf names are defined. +# +# An earlier version of this script emitted a `scada_tag` column holding +# names like PS_STN_WET_WELL_LEVEL, derived from the PLC register map. +# **CI Server never adopted them.** They named nothing that exists, and +# they caused a real defect downstream when read as if they did. The only +# legitimate PS_ names are the four Modbus POLL GROUPS below, which are +# live configuration. +sys.path.insert(0, str(Path(__file__).resolve().parent)) +from gen_ciserver_qli import LEAF, ROOT, STATION - Built from the cleaned description, so the "SIM ONLY:" marker does - not end up inside the tag name. + +def ci_names(iec): + """(item, station, point) - the three CI Server namespaces. + + AID.WRPS.STN.LEVEL item what the historian is keyed on + WRPS_PLC station the Modbus station + STN_LEVEL point the Modbus point on that station """ - equip = row["tag"].replace("-", "") - meas = description.split("(")[0].split(",")[0].strip() - meas = "".join(ch if ch.isalnum() else "_" for ch in meas) - meas = "_".join(p for p in meas.split("_") if p)[:28].upper() - return f"PS_{equip}_{meas}".upper() + if iec not in LEAF: + sys.exit(f"no leaf name defined for {iec} - add it to LEAF in gen_ciserver_qli.py") + section, leaf = LEAF[iec] + return (f"{ROOT}.{section}.{leaf}", STATION, ("%s_%s" % (section, leaf))[:24]) def main(): @@ -225,7 +239,9 @@ def main(): out_rows.append( { - "scada_tag": tag_for(r, description), + "ci_item": ci_names(iec)[0], + "ci_station": ci_names(iec)[1], + "ci_point": ci_names(iec)[2], "description": description, "poll_group": group, "function_code": fc, diff --git a/04-scada/modbus_points/historian/README.md b/04-scada/modbus_points/historian/README.md index dd8c0f0..153ad84 100644 --- a/04-scada/modbus_points/historian/README.md +++ b/04-scada/modbus_points/historian/README.md @@ -9,12 +9,30 @@ not, and that is the reason they have drifted from the server. | `his_group.qli` | `@HIS_GROUP_DF` | 3 collection groups | | `item_his.qli` | `@ITEM_HIS_DF` | 49 item → group bindings | -## The three groups +## ⚠️ These files are the INTENDED rates. The server runs different ones. -| Group | Items | Collection | +> **Resolved 2026-09-02.** The question of which was correct is now answered: +> `../../ciserver-backup-2026-08/export_his_group.qli`, taken from the running +> server, is what the historian actually did. **The live export wins.** + +| | `his_group.qli` says | The running server does | |---|---|---| -| `WRPS_ONE_SEC` | 5 | 1 s scan — the fast analogues: level, inflow, discharge | -| `WRPS_ONE_MIN` | 4 | 1 min scan — slower trends | +| Fast group | `WRPS_ONE_SEC` at **1 second** | `WRPS_ONE_SEC` at **5 seconds** | +| Second group | `WRPS_ONE_MIN` at **60 seconds** | `WRPS_THIRTY_SEC` at **30 seconds** | +| Event group | `WRPS_EVENT`, on change | `WRPS_EVENT`, on change ✅ | + +> [!CAUTION] +> **Never hardcode a sample interval from these files.** Anything assuming +> `WRPS_ONE_MIN` at 60 seconds is wrong by **twelvefold** against a group that is +> actually running at 5 seconds. This has already produced a real defect in a +> downstream project. Read the interval from the item's own configuration. + +## The three groups as written here (intended, not live) + +| Group | Items | Intended collection | +|---|---|---| +| `WRPS_ONE_SEC` | 5 | 1 s — the fast analogues: level, inflow, discharge | +| `WRPS_ONE_MIN` | 4 | 1 min — slower trends | | `WRPS_EVENT` | 40 | on change — states, commands, alarms, trips | Written 2026-08-21. @@ -31,25 +49,16 @@ and headroom — works from live values alone. The push-back that earns trust do not. **Without these two files there is no historian, and the demo loses the layer it was designed around.** -## ⚠️ These files disagree with the server +## Why they disagree -`ciserver-backup-2026-08/export_his_group.qli`, exported from CI Server around -2026-08-20, contains groups that `his_group.qli` does not define: +These files are the *source project's* intent. The server was tuned afterwards and +nobody brought the change back. That is the predictable cost of the one link in +this chain with no generator: the points and items cannot drift because they are +regenerated; the historian can, and did. -| In the CI Server export | In `his_group.qli` | -|---|---| -| `WRPS_EVENT` | ✅ | -| `WRPS_ONE_SEC` | ✅ | -| `WRPS_THIRTY_SEC` | ❌ not defined here | -| `WRPS_ONE_MIN` | — not in the export | -| `FIVE_SECONDS` (binding `STN.LEVEL`, `STN.INFLOW`) | ❌ not a WRPS group at all | - -**Nobody currently knows which is right.** The export is dated a day before these -files, but contains groups they do not. Resolve it by exporting from `cicore1` and -comparing — see `../../QUICKLOAD.md`. - -Until then: **do not import these files onto a working server** without checking -what is already there. You may remove a group something depends on. +Before importing these onto a working server, **export what is there first** +(`../../QUICKLOAD.md`) and compare. Importing blind may remove a group something +depends on. ## Worth doing: generate these too diff --git a/04-scada/modbus_points/scada-points.csv b/04-scada/modbus_points/scada-points.csv deleted file mode 100644 index 26d146a..0000000 --- a/04-scada/modbus_points/scada-points.csv +++ /dev/null @@ -1,50 +0,0 @@ -scada_tag,description,poll_group,function_code,modbus_address,data_type,access,eng_units,raw_to_eng,eng_gain,format_mask,iec_address,plc_tag,notes -PS_PU301_RUN_COMMAND,Run command,PS_STATUS_BITS,FC01,0,Boolean,Read,,value,1.0,99999,%QX0.0,PU-301, -PS_PU302_RUN_COMMAND,Run command,PS_STATUS_BITS,FC01,1,Boolean,Read,,value,1.0,99999,%QX0.1,PU-302, -PS_PU303_RUN_COMMAND,Run command,PS_STATUS_BITS,FC01,2,Boolean,Read,,value,1.0,99999,%QX0.2,PU-303, -PS_PU301_RUNNING,Running,PS_STATUS_BITS,FC01,3,Boolean,Read,,value,1.0,99999,%QX0.3,PU-301, -PS_PU302_RUNNING,Running,PS_STATUS_BITS,FC01,4,Boolean,Read,,value,1.0,99999,%QX0.4,PU-302, -PS_PU303_RUNNING,Running,PS_STATUS_BITS,FC01,5,Boolean,Read,,value,1.0,99999,%QX0.5,PU-303, -PS_PU301_AVAILABLE,Available,PS_STATUS_BITS,FC01,6,Boolean,Read,,value,1.0,99999,%QX0.6,PU-301, -PS_PU302_AVAILABLE,Available,PS_STATUS_BITS,FC01,7,Boolean,Read,,value,1.0,99999,%QX0.7,PU-302, -PS_PU303_AVAILABLE,Available,PS_STATUS_BITS,FC01,8,Boolean,Read,,value,1.0,99999,%QX1.0,PU-303, -PS_STN_STATION_IN_AUTO,Station in auto,PS_STATUS_BITS,FC01,9,Boolean,Read,,value,1.0,99999,%QX1.1,STN, -PS_STN_HIGH_LEVEL_ALARM,High level alarm,PS_STATUS_BITS,FC01,10,Boolean,Read,,value,1.0,99999,%QX1.2,STN, -PS_STN_SPILL_ACTIVE,Spill active,PS_STATUS_BITS,FC01,11,Boolean,Read,,value,1.0,99999,%QX1.3,STN, -PS_PU301_TRIPPED,Tripped,PS_STATUS_BITS,FC01,12,Boolean,Read,,value,1.0,99999,%QX1.4,PU-301, -PS_PU302_TRIPPED,Tripped,PS_STATUS_BITS,FC01,13,Boolean,Read,,value,1.0,99999,%QX1.5,PU-302, -PS_PU303_TRIPPED,Tripped,PS_STATUS_BITS,FC01,14,Boolean,Read,,value,1.0,99999,%QX1.6,PU-303, -PS_STN_WET_WELL_LEVEL,Wet well level,PS_PUBLISHED,FC03,0,Signed 16-bit,Read,%,value / 60,0.016666666666666666,999.9,%QW0,STN, -PS_STN_INFLOW,Inflow,PS_PUBLISHED,FC03,1,Signed 16-bit,Read,m3/h,value * 0.36,0.36,9999.9,%QW1,STN, -PS_STN_TOTAL_DISCHARGE_FLOW,Total discharge flow,PS_PUBLISHED,FC03,2,Signed 16-bit,Read,m3/h,value * 0.36,0.36,9999.9,%QW2,STN, -PS_STN_PUMPS_RUNNING,Pumps running,PS_PUBLISHED,FC03,3,Signed 16-bit,Read,count,value,1.0,9,%QW3,STN, -PS_STN_COMMON_DRIVE_SPEED,Common drive speed,PS_PUBLISHED,FC03,4,Signed 16-bit,Read,%,value * 0.2,0.2,999.9,%QW4,STN, -PS_STN_TIME_TO_SPILL_WEIR,Time to spill weir (32767 = drawing down),PS_PUBLISHED,FC03,5,Signed 16-bit,Read,s,value,1.0,99999,%QW5,STN, -PS_STN_TIME_TO_LSHH,Time to LSHH (32767 = drawing down),PS_PUBLISHED,FC03,6,Signed 16-bit,Read,s,value,1.0,99999,%QW6,STN, -PS_STN_NET_ACCUMULATION,Net accumulation (signed),PS_PUBLISHED,FC03,7,Signed 16-bit,Read,m3/h,value * 0.36,0.36,9999.9,%QW7,STN, -PS_PU301_RUN_HOURS,Run hours,PS_PUBLISHED,FC03,8,Signed 16-bit,Read,h,value,1.0,99999,%QW8,PU-301, -PS_PU302_RUN_HOURS,Run hours,PS_PUBLISHED,FC03,9,Signed 16-bit,Read,h,value,1.0,99999,%QW9,PU-302, -PS_PU303_RUN_HOURS,Run hours,PS_PUBLISHED,FC03,10,Signed 16-bit,Read,h,value,1.0,99999,%QW10,PU-303, -PS_STN_VOLUME_REMAINING_TO_SPILL,Volume remaining to spill,PS_PUBLISHED,FC03,11,Signed 16-bit,Read,m3,value,1.0,9999,%QW11,STN, -PS_STN_STATION_STATE,Station state (enum 3.1),PS_PUBLISHED,FC03,12,Signed 16-bit,Read,,value,1.0,9,%QW12,STN, -PS_PU301_PUMP_STATE,Pump state (enum 3.2),PS_PUBLISHED,FC03,13,Signed 16-bit,Read,,value,1.0,9,%QW13,PU-301, -PS_PU302_PUMP_STATE,Pump state (enum 3.2),PS_PUBLISHED,FC03,14,Signed 16-bit,Read,,value,1.0,9,%QW14,PU-302, -PS_PU303_PUMP_STATE,Pump state (enum 3.2),PS_PUBLISHED,FC03,15,Signed 16-bit,Read,,value,1.0,9,%QW15,PU-303, -PS_STN_CURRENT_DUTY_PUMP,"Current duty pump (0 = none, 1-3)",PS_PUBLISHED,FC03,16,Signed 16-bit,Read,,value,1.0,9,%QW16,STN, -PS_STN_ALARM_BITMASK,Alarm bitmask (section 6) - READ AS UNSIGNED,PS_PUBLISHED,FC03,17,Unsigned 16-bit,Read,,value,1.0,99999,%QW17,STN, -PS_STN_COMMAND_ACKNOWLEDGE,Command acknowledge (echoes %MW1),PS_PUBLISHED,FC03,20,Signed 16-bit,Read,,value,1.0,99,%QW20,STN, -PS_STN_STATION_MODE_1_AUTO,"Station mode: 1 = auto, 2 = off",PS_SETPOINTS,FC03/FC06,1024,Signed 16-bit,Read/Write,,value,1.0,9,%MW0,STN, -PS_STN_COMMAND_WORD,Command word (section 3.3),PS_SETPOINTS,FC03/FC06,1025,Signed 16-bit,Read/Write,,value,1.0,99,%MW1,STN, -PS_STN_COMMAND_PARAMETER,Command parameter (pump number),PS_SETPOINTS,FC03/FC06,1026,Signed 16-bit,Read/Write,,value,1.0,9,%MW2,STN, -PS_STN_LEVEL_CONTROL_SETPOINT,Level control setpoint,PS_SETPOINTS,FC03/FC06,1027,Signed 16-bit,Read/Write,%,value / 60,0.016666666666666666,999.9,%MW3,STN, -PS_STN_START_DUTY_LEVEL,Start duty level,PS_SETPOINTS,FC03/FC06,1028,Signed 16-bit,Read/Write,%,value / 60,0.016666666666666666,999.9,%MW4,STN, -PS_STN_START_PUMP_2_LEVEL,Start pump 2 level,PS_SETPOINTS,FC03/FC06,1029,Signed 16-bit,Read/Write,%,value / 60,0.016666666666666666,999.9,%MW5,STN, -PS_STN_START_PUMP_3_LEVEL,Start pump 3 level,PS_SETPOINTS,FC03/FC06,1030,Signed 16-bit,Read/Write,%,value / 60,0.016666666666666666,999.9,%MW6,STN, -PS_STN_STOP_ALL_LEVEL,Stop all level,PS_SETPOINTS,FC03/FC06,1031,Signed 16-bit,Read/Write,%,value / 60,0.016666666666666666,999.9,%MW7,STN, -PS_STN_HIGH_LEVEL_ALARM,High level alarm,PS_SETPOINTS,FC03/FC06,1032,Signed 16-bit,Read/Write,%,value / 60,0.016666666666666666,999.9,%MW8,STN, -PS_STN_MINIMUM_DRIVE_SPEED,Minimum drive speed,PS_SETPOINTS,FC03/FC06,1033,Signed 16-bit,Read/Write,%,value * 0.2,0.2,999.9,%MW9,STN, -PS_STN_SERVICE_INTERVAL,Service interval,PS_SETPOINTS,FC03/FC06,1034,Signed 16-bit,Read/Write,h,value,1.0,99999,%MW10,STN, -PS_SIM_MANUAL_INFLOW,manual inflow (mode 0),PS_SIM_CONTROL,FC03/FC06,1044,Signed 16-bit,Read/Write,m3/h,value * 0.36,0.36,9999.9,%MW20,SIM,SIMULATION CONTROL - simulation build only -PS_SIM_SCENARIO_0_MAN_1_DIURNAL_2_W,scenario 0=man 1=diurnal 2=wet 3=ref,PS_SIM_CONTROL,FC03/FC06,1045,Signed 16-bit,Read/Write,,value,1.0,9,%MW21,SIM,SIMULATION CONTROL - simulation build only -PS_SIM_WRITE_1_TO_RESET_SCENARIO,"write 1 to reset scenario, self-clearing",PS_SIM_CONTROL,FC03/FC06,1046,Signed 16-bit,Read/Write,,value,1.0,9,%MW22,SIM,SIMULATION CONTROL - simulation build only -PS_SIM_TIME_SCALE_1_120,time scale 1-120,PS_SIM_CONTROL,FC03/FC06,1047,Signed 16-bit,Read/Write,x,value,1.0,999,%MW23,SIM,SIMULATION CONTROL - simulation build only