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.
This commit is contained in:
parent
0bbff348f8
commit
947f632d7f
27 changed files with 11169 additions and 0 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -38,3 +38,7 @@ venv/
|
||||||
# Generated by 03-plc/gen_project.py - the OpenPLC Editor v4 project, rebuilt
|
# Generated by 03-plc/gen_project.py - the OpenPLC Editor v4 project, rebuilt
|
||||||
# from src/. Regenerate it; never hand-edit it, never commit it.
|
# from src/. Regenerate it; never hand-edit it, never commit it.
|
||||||
03-plc/editor-project/
|
03-plc/editor-project/
|
||||||
|
|
||||||
|
# Generated by 04-scada/hmi/build_display.py - rebuild, don't commit.
|
||||||
|
# (build_display.py is the source; the display XMLs are its output.)
|
||||||
|
04-scada/hmi/out/
|
||||||
|
|
|
||||||
119
04-scada/QUICKLOAD.md
Normal file
119
04-scada/QUICKLOAD.md
Normal file
|
|
@ -0,0 +1,119 @@
|
||||||
|
# Quickload — moving configuration in and out of CI Server
|
||||||
|
|
||||||
|
`dssqld` is CI Server's quickload utility. It exports and imports **configuration
|
||||||
|
datasets** as `.qli` files: sections, items, Modbus points, historian groups.
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> **Quickload is for configuration only. It is not how displays are deployed.**
|
||||||
|
> HMI displays are plain file copy/replace into the deployment's `displays`
|
||||||
|
> directory — see `hmi/DEPLOY.md`. Do not look for a `dssqld` command for them;
|
||||||
|
> there isn't one.
|
||||||
|
|
||||||
|
## The two commands
|
||||||
|
|
||||||
|
**Export** — writes every record of one class to a file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dssqld -d MODBUS_POINT_DF -e modbus-points.qli
|
||||||
|
```
|
||||||
|
|
||||||
|
`-d` names the class, `-e` the output file.
|
||||||
|
|
||||||
|
**Import** — loads a file back in:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dssqld -i modbus-points.qli
|
||||||
|
```
|
||||||
|
|
||||||
|
The class is read from the file's own `@` header, so import takes no `-d`.
|
||||||
|
|
||||||
|
> Other classes may need different flags. These two forms are the ones proven on
|
||||||
|
> this project. **If a command fails, ask** — do not guess at flags against a live
|
||||||
|
> configuration.
|
||||||
|
|
||||||
|
## The classes this project uses
|
||||||
|
|
||||||
|
| Class | File | Holds | Generated? |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `SECTION_DF` | `modbus_points/wrps_section_df.qli` | the 6 `AID.WRPS.*` sections | ✅ `gen_ciserver_qli.py` |
|
||||||
|
| `MODBUS_POINT_DF` | `modbus_points/wrps_modbus_point_df.qli` | 49 Modbus point definitions | ✅ |
|
||||||
|
| `ITEM_DF` | `modbus_points/wrps_item_df.qli` | 49 items, bound to those points | ✅ |
|
||||||
|
| `HIS_GROUP_DF` | `modbus_points/historian/his_group.qli` | 3 collection groups | ❌ hand-made |
|
||||||
|
| `ITEM_HIS_DF` | `modbus_points/historian/item_his.qli` | 49 item→group bindings | ❌ hand-made |
|
||||||
|
|
||||||
|
## Import order — it matters
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dssqld -i wrps_section_df.qli # 1. sections
|
||||||
|
dssqld -i wrps_modbus_point_df.qli # 2. points
|
||||||
|
dssqld -i wrps_item_df.qli # 3. items
|
||||||
|
dssqld -i historian/his_group.qli # 4. collection groups
|
||||||
|
dssqld -i historian/item_his.qli # 5. item -> group bindings
|
||||||
|
```
|
||||||
|
|
||||||
|
Each step depends on the one before it:
|
||||||
|
|
||||||
|
- **Sections first.** CI Server derives its hierarchy from the dots in a name, and
|
||||||
|
a section must exist before an item can be created inside it.
|
||||||
|
- **Points before items**, because an item binds to a point.
|
||||||
|
- **Groups before bindings**, because `ITEM_HIS_DF` names both an item and a group
|
||||||
|
and needs both to exist.
|
||||||
|
|
||||||
|
## ⚠️ After importing items, every display goes dead
|
||||||
|
|
||||||
|
**An item import renumbers every item.** Proven on 2026-08-14: items were
|
||||||
|
re-imported to change engineering units, keeping every `NAME`, `NSID` and
|
||||||
|
`ID_NUMBER` byte-identical — and every itemId still moved. CI Server recreates
|
||||||
|
items on import rather than updating them in place.
|
||||||
|
|
||||||
|
Displays bind by **itemId** at runtime, so every value on every screen dies at
|
||||||
|
once.
|
||||||
|
|
||||||
|
**The fix is one action, and it is not a rebuild:**
|
||||||
|
|
||||||
|
> **Open each display in CI Server's Editor Module and validate it.**
|
||||||
|
|
||||||
|
Validation resolves every connection by name and rewrites the ids. Then re-harvest
|
||||||
|
so the next build ships the current ids:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python hmi/build_display.py --harvest <the saved display>.xml
|
||||||
|
```
|
||||||
|
|
||||||
|
`WRPS_TagTest` binds every project item, so harvesting that one display measures
|
||||||
|
all 49 in a single pass.
|
||||||
|
|
||||||
|
**Moving displays to a different CI Server** needs no rebuild and no re-harvest:
|
||||||
|
importing a display into the target resolves its connections by name on the way
|
||||||
|
in. **Import, then validate.** The files built for one server are the files you
|
||||||
|
ship to the next — provided the item names are identical, which means `ROOT`,
|
||||||
|
`INSTALL` and `STATION` in `gen_ciserver_qli.py` must match on both.
|
||||||
|
|
||||||
|
## Exporting, to check what the server actually holds
|
||||||
|
|
||||||
|
The repo cannot see `cicore1`, so the only way to confirm the server agrees with
|
||||||
|
this folder is to export and diff:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dssqld -d SECTION_DF -e check_section_df.qli
|
||||||
|
dssqld -d MODBUS_POINT_DF -e check_modbus_point_df.qli
|
||||||
|
dssqld -d ITEM_DF -e check_item_df.qli
|
||||||
|
dssqld -d HIS_GROUP_DF -e check_his_group.qli
|
||||||
|
dssqld -d ITEM_HIS_DF -e check_item_his_df.qli
|
||||||
|
```
|
||||||
|
|
||||||
|
An export contains **every** record of that class, not just this project's — the
|
||||||
|
2026-08 export in `ciserver-backup-2026-08/` holds 147 WRPS item records among
|
||||||
|
others. Filter on `AID.WRPS` when comparing.
|
||||||
|
|
||||||
|
**This has already caught a real drift.** The 2026-08 export shows historian
|
||||||
|
groups (`WRPS_THIRTY_SEC`, `FIVE_SECONDS`) that
|
||||||
|
`modbus_points/historian/his_group.qli` does not define. One of the two is stale
|
||||||
|
and nobody currently knows which.
|
||||||
|
|
||||||
|
## Format reference
|
||||||
|
|
||||||
|
CI Server publishes no documentation for the `.qli` format. Ten real exports are
|
||||||
|
kept in `99-reference/ciserver-qli-exports/` — every field layout and constant in
|
||||||
|
`gen_ciserver_qli.py` was copied from them. If you need a class this project does
|
||||||
|
not use, export one record of it and read the header.
|
||||||
115
04-scada/README.md
Normal file
115
04-scada/README.md
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
# 04-scada — the CI Server side
|
||||||
|
|
||||||
|
Everything that lives on `yau-poc-cicore1` (`10.0.0.21`): the tag database that
|
||||||
|
reads the PLC, and the displays an operator looks at.
|
||||||
|
|
||||||
|
## Two halves, two deployment mechanisms
|
||||||
|
|
||||||
|
This is the distinction to get straight before anything else. Conflating them is
|
||||||
|
what makes this folder confusing.
|
||||||
|
|
||||||
|
| | **Configuration** | **Displays** |
|
||||||
|
|---|---|---|
|
||||||
|
| What | sections, items, Modbus points, historian groups | the `.xml` screens |
|
||||||
|
| Lives in | `modbus_points/` | `hmi/` |
|
||||||
|
| **Deployed by** | **`dssqld`** — the quickload utility | **file copy** into the displays directory |
|
||||||
|
| Generated from | `03-plc/register-map.csv` | `build_display.py` |
|
||||||
|
| Binds by | `NSID` / `ID_NUMBER` | **itemId** — healed by name on import |
|
||||||
|
|
||||||
|
`dssqld` is for configuration only. It has nothing to do with displays. See
|
||||||
|
`QUICKLOAD.md`.
|
||||||
|
|
||||||
|
## The chain
|
||||||
|
|
||||||
|
Everything derives from the PLC. The PLC side leads, always.
|
||||||
|
|
||||||
|
```
|
||||||
|
03-plc/src/*.st
|
||||||
|
│ build.py
|
||||||
|
▼
|
||||||
|
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
|
||||||
|
│ gen_ciserver_qli.py
|
||||||
|
▼
|
||||||
|
modbus_points/*.qli 6 sections · 49 Modbus points · 49 items
|
||||||
|
│ dssqld -i
|
||||||
|
▼
|
||||||
|
CI SERVER ◄──── Modbus TCP ──── the PLC on 10.0.0.17:502
|
||||||
|
▲
|
||||||
|
│ file copy
|
||||||
|
│
|
||||||
|
hmi/out/*.xml 6 displays
|
||||||
|
▲ build_display.py
|
||||||
|
│
|
||||||
|
hmi/item-ids.csv 49 itemIds, MEASURED from CI Server
|
||||||
|
```
|
||||||
|
|
||||||
|
**Nothing in this chain is hand-edited.** Change a register in `03-plc/build.py`,
|
||||||
|
rebuild, then re-run both generators. If CI Server needs a field the generators do
|
||||||
|
not emit, it goes in the generator.
|
||||||
|
|
||||||
|
**One exception:** the historian has no generator. See
|
||||||
|
`modbus_points/historian/README.md`.
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
QUICKLOAD.md dssqld - export and import configuration
|
||||||
|
modbus_points/ the tag database (named for the protocol: CI Server
|
||||||
|
configures other protocols differently)
|
||||||
|
historian/ collection groups and item bindings - HAND-MADE
|
||||||
|
hmi/ displays, and the generator that builds them
|
||||||
|
ciserver-backup-2026-08/ ⚠️ outdated exports, evidence only - never import
|
||||||
|
```
|
||||||
|
|
||||||
|
## ⚠️ "The values on my display are not updating"
|
||||||
|
|
||||||
|
**First, and before any other investigation:**
|
||||||
|
|
||||||
|
> **Open the display in CI Server's Editor Module and validate it.
|
||||||
|
> Then report what happened.**
|
||||||
|
|
||||||
|
Validation resolves every connection by name and repairs the file. It fixes the
|
||||||
|
overwhelmingly common cause in seconds — an itemId that no longer matches, which
|
||||||
|
happens **every time the items are re-imported**. CI Server recreates items on
|
||||||
|
import rather than updating them in place, so every id in every display moves at
|
||||||
|
once, and every value goes dead together.
|
||||||
|
|
||||||
|
That simultaneity is the signature: everything dying at the same moment is ids,
|
||||||
|
not communications.
|
||||||
|
|
||||||
|
**Only once validation has been ruled out**, work down these:
|
||||||
|
|
||||||
|
| Symptom | Cause |
|
||||||
|
|---|---|
|
||||||
|
| Some values dead, others fine | Those items were never measured. Harvest the ids — see `hmi/DEPLOY.md`. |
|
||||||
|
| Every analogue reads 0, always | You are polling `%IW`/`%IX` (FC04/FC02). Those are **field** inputs; in the simulation build nothing writes them. Live values are in the `%QW` block (FC03). |
|
||||||
|
| Values frozen at a plausible number | Communications stopped. Is the container up? Is a **program running**? The runtime opens its Modbus slave only while a program runs — a stopped program means port 502 refuses. |
|
||||||
|
| Values moving but wrong | Addressing. `%MW` starts at holding register **1024**, not 0. A wrong-by-1024 read succeeds and returns plausible nonsense. |
|
||||||
|
| Alarm word goes negative | `%QW17` must be read **unsigned**. Bit 15 does not fit a signed INT. |
|
||||||
|
|
||||||
|
**What this repo cannot tell you:** whether CI Server's own poll groups are
|
||||||
|
actually scanning. That lives on `cicore1`. If the PLC polls correctly with
|
||||||
|
`05-tests/verify_modbus.py` and the ids are good, that is the remaining place to
|
||||||
|
look.
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| 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 |
|
||||||
|
| Displays | Six built; `build_display.py` runs clean |
|
||||||
|
| Historian | 3 groups, 49 bindings — **hand-made, and drifted from the server** |
|
||||||
|
| 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
|
||||||
|
|
||||||
|
- **Alarm and trend limits.** Every item imports with alarming off and limits at 0.
|
||||||
|
Which points alarm, at what threshold and priority, is engineering judgement
|
||||||
|
about the plant — not something to derive from a register map.
|
||||||
|
- **Re-export the configuration from `cicore1`** and diff it against this folder.
|
||||||
|
Until then, `ciserver-backup-2026-08/` is the only record of the server's own
|
||||||
|
state, and it is known to disagree with `modbus_points/historian/`.
|
||||||
42
04-scada/ciserver-backup-2026-08/README.md
Normal file
42
04-scada/ciserver-backup-2026-08/README.md
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
# ciserver-backup-2026-08 — ⚠️ OUTDATED. Evidence only.
|
||||||
|
|
||||||
|
> [!CAUTION]
|
||||||
|
> **Do not import anything in this folder.** These are exports taken *out of*
|
||||||
|
> CI Server around **2026-08-20**. They are a record of what the server held at
|
||||||
|
> that moment, not source, and they are known to be superseded.
|
||||||
|
|
||||||
|
## Why they are kept
|
||||||
|
|
||||||
|
The CI Server side of this project has no other record. `cicore1` was not
|
||||||
|
reachable during the 2026-09 audit, so nothing here could be checked against the
|
||||||
|
live system. These exports are the only evidence of what was actually configured,
|
||||||
|
as opposed to what the repo believes was configured.
|
||||||
|
|
||||||
|
| File | Class | Contains |
|
||||||
|
|---|---|---|
|
||||||
|
| `export_items.qli` | `ITEM_DF` | 147 records mentioning `AID.WRPS` |
|
||||||
|
| `export_his_group.qli` | `HIS_GROUP_DF` | historian collection groups |
|
||||||
|
| `export_item_his_df.qli` | `ITEM_HIS_DF` | item → group bindings |
|
||||||
|
|
||||||
|
An export holds **every** record of its class, not only this project's. Filter on
|
||||||
|
`AID.WRPS` when comparing.
|
||||||
|
|
||||||
|
## The drift they revealed
|
||||||
|
|
||||||
|
Comparing these against `../modbus_points/historian/` shows the two do not agree:
|
||||||
|
|
||||||
|
| In this export | In the repo's source |
|
||||||
|
|---|---|
|
||||||
|
| `WRPS_EVENT`, `WRPS_ONE_SEC` | ✅ both |
|
||||||
|
| `WRPS_THIRTY_SEC` | ❌ absent |
|
||||||
|
| `FIVE_SECONDS` (binding `STN.LEVEL`, `STN.INFLOW`) | ❌ absent — not a WRPS group |
|
||||||
|
| — | `WRPS_ONE_MIN`, absent from the export |
|
||||||
|
|
||||||
|
That is the whole reason this folder exists rather than being deleted.
|
||||||
|
|
||||||
|
## Replacing it
|
||||||
|
|
||||||
|
When `cicore1` is reachable, re-export the five classes (`../../QUICKLOAD.md`),
|
||||||
|
diff them against `../modbus_points/`, resolve the differences, and **replace this
|
||||||
|
folder** with a dated export that has been verified. Do not accumulate backups —
|
||||||
|
one known-good snapshot is more useful than three of uncertain age.
|
||||||
373
04-scada/ciserver-backup-2026-08/export_his_group.qli
Normal file
373
04-scada/ciserver-backup-2026-08/export_his_group.qli
Normal file
|
|
@ -0,0 +1,373 @@
|
||||||
|
|
||||||
|
@LANGUAGE
|
||||||
|
ENGLISH
|
||||||
|
|
||||||
|
|
||||||
|
@VERSION
|
||||||
|
1.03.00
|
||||||
|
|
||||||
|
|
||||||
|
!==================================================================================================================================
|
||||||
|
|
||||||
|
@FIELDS
|
||||||
|
NAME,DESCRIPTION,START_TIME,STOP_TIME,FIRST_ROLLOVER
|
||||||
|
LIFE_TIME,WARN_TIME,ROLLOVER_INT,FORCE_ROLLOVER,FILL_OLD,STORE_QUALITY
|
||||||
|
SAVE_TIME,SCAN_INTERVAL,CORRECT_DAYLIGHT,EXCLUDE_ARCHIVE,DATA_COMP,AGG_PERIOD_HOUR
|
||||||
|
AGG_PERIOD_SHIFT,AGG_PERIOD_DAY,AGG_PERIOD_WEEK,AGG_PERIOD_MONTH,AGG_PERIOD_YEAR,AGG_PERIOD_30MIN
|
||||||
|
AGG_CORRECT_DAY_DST,AGG_CORRECT_SHIFT_DST,NUMBER,NODE,NODE_NAME,NEXT_UNIT_SEQ
|
||||||
|
NEXT_DATA_SEQ,AVERAGING_INTERVAL,PRIORITY,SAMPLES_RECORD,TYPE,AVERAGING_METHOD
|
||||||
|
COL_STOR_TYPE,AGG_SHIFT_LENGTH,AGG_WEEK_START,AGG_SHIFT_START,AGG_DAY_START,AGG_COMP_NOT_ITEM
|
||||||
|
|
||||||
|
@HIS_GROUP_DF
|
||||||
|
"ADT","","04-11-2025 14:43:17","","20-08-2026 14:43:17",\
|
||||||
|
"52 weeks","","1 days",0,0,0,\
|
||||||
|
0,0,1,0,0,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,2,1,"",68,\
|
||||||
|
2,0,0,0,"Audit","",\
|
||||||
|
"0","0","Thursday","00:00","00:00",""
|
||||||
|
|
||||||
|
"ALARM_COUNT","","04-11-2025 14:44:36","","",\
|
||||||
|
"1 weeks","","",0,0,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,5,1,"",69,\
|
||||||
|
2,0,0,0,"Report","",\
|
||||||
|
"0","0","Thursday","00:00","00:00",""
|
||||||
|
|
||||||
|
"ALARM_CURRENT","","04-11-2025 14:44:36","","",\
|
||||||
|
"1 weeks","","",0,0,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,6,1,"",69,\
|
||||||
|
2,0,0,0,"Report","",\
|
||||||
|
"0","0","Thursday","00:00","00:00",""
|
||||||
|
|
||||||
|
"ALARM_EXT_CURR","","04-11-2025 14:44:36","","",\
|
||||||
|
"1 weeks","","",0,0,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,7,1,"",69,\
|
||||||
|
2,0,0,0,"Report","",\
|
||||||
|
"0","0","Thursday","00:00","00:00",""
|
||||||
|
|
||||||
|
"ALARM_HISTORY","","04-11-2025 14:44:36","","",\
|
||||||
|
"1 weeks","","",0,0,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,8,1,"",69,\
|
||||||
|
2,0,0,0,"Report","",\
|
||||||
|
"0","0","Thursday","00:00","00:00",""
|
||||||
|
|
||||||
|
"ALARM_SHELVE","","04-11-2025 14:44:36","","",\
|
||||||
|
"1 weeks","","",0,0,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,9,1,"",69,\
|
||||||
|
2,0,0,0,"Report","",\
|
||||||
|
"0","0","Thursday","00:00","00:00",""
|
||||||
|
|
||||||
|
"ALH","","04-11-2025 14:43:19","","20-08-2026 14:43:19",\
|
||||||
|
"52 weeks","","1 days",0,0,0,\
|
||||||
|
0,0,1,0,0,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,3,1,"",68,\
|
||||||
|
2,0,0,0,"Alarm","",\
|
||||||
|
"0","0","Thursday","00:00","00:00",""
|
||||||
|
|
||||||
|
"ALM_ATTRS_CUR","","04-11-2025 14:44:36","","",\
|
||||||
|
"1 weeks","","",0,0,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,10,1,"",69,\
|
||||||
|
2,0,0,0,"Report","",\
|
||||||
|
"0","0","Thursday","00:00","00:00",""
|
||||||
|
|
||||||
|
"ALM_ATTRS_HIS","","04-11-2025 14:44:36","","",\
|
||||||
|
"1 weeks","","",0,0,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,11,1,"",69,\
|
||||||
|
2,0,0,0,"Report","",\
|
||||||
|
"0","0","Thursday","00:00","00:00",""
|
||||||
|
|
||||||
|
"AUDIT_TRAIL","","04-11-2025 14:44:37","","",\
|
||||||
|
"1 weeks","","",0,0,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,12,1,"",69,\
|
||||||
|
2,0,0,0,"Report","",\
|
||||||
|
"0","0","Thursday","00:00","00:00",""
|
||||||
|
|
||||||
|
"AUTH_GROUP","","04-11-2025 14:44:37","","",\
|
||||||
|
"1 weeks","","",0,0,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,13,1,"",69,\
|
||||||
|
2,0,0,0,"Report","",\
|
||||||
|
"0","0","Thursday","00:00","00:00",""
|
||||||
|
|
||||||
|
"EVENT","","","","21-08-2026 00:00:00",\
|
||||||
|
"1 weeks","","1 days",0,0,1,\
|
||||||
|
0,0,0,0,0,1,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,33,1,"",71,\
|
||||||
|
2,0,11,100,"Item","",\
|
||||||
|
"Event/Item","8 hours","Monday","00:00","00:00",""
|
||||||
|
|
||||||
|
"FIVE_MINUTES","","","","21-08-2026 00:00:00",\
|
||||||
|
"1 weeks","","1 days",0,0,1,\
|
||||||
|
0,300,1,0,0,1,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,34,1,"",68,\
|
||||||
|
2,0,3,0,"Item","",\
|
||||||
|
"Scan/Time","8 hours","Monday","00:00","00:00",""
|
||||||
|
|
||||||
|
"FIVE_SECONDS","","","","20-08-2026 10:00:00",\
|
||||||
|
"1 days","","1 hours",0,0,1,\
|
||||||
|
0,5,0,0,0,1,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,35,1,"",70,\
|
||||||
|
2,0,8,0,"Item","",\
|
||||||
|
"Scan/Time","8 hours","Monday","00:00","00:00",""
|
||||||
|
|
||||||
|
"ITEM_ACK","","04-11-2025 14:44:37","","",\
|
||||||
|
"1 weeks","","",0,0,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,14,1,"",69,\
|
||||||
|
2,0,0,0,"Report","",\
|
||||||
|
"0","0","Thursday","00:00","00:00",""
|
||||||
|
|
||||||
|
"ITEM_ALM","","04-11-2025 14:44:37","","",\
|
||||||
|
"1 weeks","","",0,0,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,15,1,"",69,\
|
||||||
|
2,0,0,0,"Report","",\
|
||||||
|
"0","0","Thursday","00:00","00:00",""
|
||||||
|
|
||||||
|
"ITEM_EQP","","04-11-2025 14:44:37","","",\
|
||||||
|
"1 weeks","","",0,0,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,16,1,"",69,\
|
||||||
|
2,0,0,0,"Report","",\
|
||||||
|
"0","0","Thursday","00:00","00:00",""
|
||||||
|
|
||||||
|
"ITEM_FO","","04-11-2025 14:44:37","","",\
|
||||||
|
"1 weeks","","",0,0,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,17,1,"",69,\
|
||||||
|
2,0,0,0,"Report","",\
|
||||||
|
"0","0","Thursday","00:00","00:00",""
|
||||||
|
|
||||||
|
"ITEM_HISTORY","","04-11-2025 14:44:37","","",\
|
||||||
|
"1 weeks","","",0,0,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,18,1,"",69,\
|
||||||
|
2,0,0,0,"Report","",\
|
||||||
|
"0","0","Thursday","00:00","00:00",""
|
||||||
|
|
||||||
|
"ITEM_HISTORY_AG","","04-11-2025 14:44:37","","",\
|
||||||
|
"1 weeks","","",0,0,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,19,1,"",69,\
|
||||||
|
2,0,0,0,"Report","",\
|
||||||
|
"0","0","Thursday","00:00","00:00",""
|
||||||
|
|
||||||
|
"ITEM_ID","","04-11-2025 14:44:37","","",\
|
||||||
|
"1 weeks","","",0,0,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,20,1,"",69,\
|
||||||
|
2,0,0,0,"Report","",\
|
||||||
|
"0","0","Thursday","00:00","00:00",""
|
||||||
|
|
||||||
|
"ITEM_ITM","","04-11-2025 14:44:37","","",\
|
||||||
|
"1 weeks","","",0,0,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,21,1,"",69,\
|
||||||
|
2,0,0,0,"Report","",\
|
||||||
|
"0","0","Thursday","00:00","00:00",""
|
||||||
|
|
||||||
|
"ITEM_OTH","","04-11-2025 14:44:37","","",\
|
||||||
|
"1 weeks","","",0,0,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,22,1,"",69,\
|
||||||
|
2,0,0,0,"Report","",\
|
||||||
|
"0","0","Thursday","00:00","00:00",""
|
||||||
|
|
||||||
|
"ITEM_TYPE","","04-11-2025 14:44:37","","",\
|
||||||
|
"1 weeks","","",0,0,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,23,1,"",69,\
|
||||||
|
2,0,0,0,"Report","",\
|
||||||
|
"0","0","Thursday","00:00","00:00",""
|
||||||
|
|
||||||
|
"ITEM_VAL_STA","","04-11-2025 14:44:37","","",\
|
||||||
|
"1 weeks","","",0,0,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,24,1,"",69,\
|
||||||
|
2,0,0,0,"Report","",\
|
||||||
|
"0","0","Thursday","00:00","00:00",""
|
||||||
|
|
||||||
|
"ITM_VAL_CSV","","04-11-2025 14:44:37","","",\
|
||||||
|
"1 weeks","","",0,0,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,25,1,"",69,\
|
||||||
|
2,0,0,0,"Report","",\
|
||||||
|
"0","0","Thursday","00:00","00:00",""
|
||||||
|
|
||||||
|
"ONE_HOUR","","","","21-08-2026 00:00:00",\
|
||||||
|
"52 weeks","","1 days",0,0,1,\
|
||||||
|
0,60,1,0,0,1,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,36,1,"",69,\
|
||||||
|
2,3600,1,0,"Item","Normal",\
|
||||||
|
"Scan/Time","8 hours","Monday","00:00","00:00",""
|
||||||
|
|
||||||
|
"ONE_MINUTE","","","","20-08-2026 12:00:00",\
|
||||||
|
"2 days","","12 hours",0,0,1,\
|
||||||
|
0,60,0,0,0,1,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,37,1,"",68,\
|
||||||
|
2,0,5,0,"Item","",\
|
||||||
|
"Scan/Time","8 hours","Monday","00:00","00:00",""
|
||||||
|
|
||||||
|
"STATION","","04-11-2025 14:44:37","","",\
|
||||||
|
"1 weeks","","",0,0,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,26,1,"",69,\
|
||||||
|
2,0,0,0,"Report","",\
|
||||||
|
"0","0","Thursday","00:00","00:00",""
|
||||||
|
|
||||||
|
"STATION_STAT","","04-11-2025 14:43:21","","20-08-2026 14:43:21",\
|
||||||
|
"31 days","","1 days",0,0,0,\
|
||||||
|
0,0,1,1,0,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,4,1,"",68,\
|
||||||
|
2,0,0,0,"Station I/O","",\
|
||||||
|
"0","0","Thursday","00:00","00:00",""
|
||||||
|
|
||||||
|
"STATION_STATS","","04-11-2025 14:44:38","","",\
|
||||||
|
"1 weeks","","",0,0,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,27,1,"",69,\
|
||||||
|
2,0,0,0,"Report","",\
|
||||||
|
"0","0","Thursday","00:00","00:00",""
|
||||||
|
|
||||||
|
"SYSTEM_LOG","","04-11-2025 14:44:38","","",\
|
||||||
|
"1 weeks","","",0,0,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,28,1,"",69,\
|
||||||
|
2,0,0,0,"Report","",\
|
||||||
|
"0","0","Thursday","00:00","00:00",""
|
||||||
|
|
||||||
|
"SYSTEM_LOG_HTML","","04-11-2025 14:44:38","","",\
|
||||||
|
"1 weeks","","",0,0,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,29,1,"",69,\
|
||||||
|
2,0,0,0,"Report","",\
|
||||||
|
"0","0","Thursday","00:00","00:00",""
|
||||||
|
|
||||||
|
"SYS_LOG","","04-11-2025 14:43:16","","20-08-2026 14:43:16",\
|
||||||
|
"31 days","","1 days",0,0,0,\
|
||||||
|
0,0,1,0,0,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,1,1,"",68,\
|
||||||
|
2,0,0,0,"System Log","",\
|
||||||
|
"0","0","Thursday","00:00","00:00",""
|
||||||
|
|
||||||
|
"TEN_SECONDS","","","","20-08-2026 10:00:00",\
|
||||||
|
"1 days","","1 hours",0,0,1,\
|
||||||
|
0,10,0,0,0,1,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,38,1,"",68,\
|
||||||
|
2,0,7,0,"Item","",\
|
||||||
|
"Scan/Time","8 hours","Monday","00:00","00:00",""
|
||||||
|
|
||||||
|
"THIRTY_MINUTES","","","","21-08-2026 00:00:00",\
|
||||||
|
"30 days","","1 days",0,0,1,\
|
||||||
|
0,30,1,0,0,1,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,39,1,"",68,\
|
||||||
|
2,1800,2,0,"Item","Normal",\
|
||||||
|
"Scan/Time","8 hours","Monday","00:00","00:00",""
|
||||||
|
|
||||||
|
"THIRTY_SECONDS","","","","20-08-2026 12:00:00",\
|
||||||
|
"2 days","","6 hours",0,0,1,\
|
||||||
|
0,30,0,0,0,1,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,40,1,"",68,\
|
||||||
|
2,0,6,0,"Item","",\
|
||||||
|
"Scan/Time","8 hours","Monday","00:00","00:00",""
|
||||||
|
|
||||||
|
"TWO_MINUTES","","","","21-08-2026 00:00:00",\
|
||||||
|
"1 weeks","","1 days",0,0,1,\
|
||||||
|
0,120,0,0,0,1,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,41,1,"",68,\
|
||||||
|
2,0,4,0,"Item","",\
|
||||||
|
"Scan/Time","8 hours","Monday","00:00","00:00",""
|
||||||
|
|
||||||
|
"UA_SERVER_AUDIT","","04-11-2025 14:44:38","","",\
|
||||||
|
"1 weeks","","",0,0,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,30,1,"",69,\
|
||||||
|
2,0,0,0,"Report","",\
|
||||||
|
"0","0","Thursday","00:00","00:00",""
|
||||||
|
|
||||||
|
"USER","","04-11-2025 14:44:39","","",\
|
||||||
|
"1 weeks","","",0,0,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,31,1,"",69,\
|
||||||
|
2,0,0,0,"Report","",\
|
||||||
|
"0","0","Thursday","00:00","00:00",""
|
||||||
|
|
||||||
|
"USER_VAL","","04-11-2025 14:44:39","","",\
|
||||||
|
"1 weeks","","",0,0,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,32,1,"",69,\
|
||||||
|
2,0,0,0,"Report","",\
|
||||||
|
"0","0","Thursday","00:00","00:00",""
|
||||||
|
|
||||||
|
"WRPS_EVENT","","","","21-08-2026 00:00:00",\
|
||||||
|
"1 weeks","","1 days",0,0,1,\
|
||||||
|
0,0,0,0,0,1,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,42,1,"",1,\
|
||||||
|
2,0,11,100,"Item","",\
|
||||||
|
"Event/Item","8 hours","Monday","00:00","00:00",""
|
||||||
|
|
||||||
|
"WRPS_ONE_SEC","","","","20-08-2026 10:00:00",\
|
||||||
|
"1 weeks","","1 hours",0,0,1,\
|
||||||
|
0,5,0,0,0,1,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,43,1,"",1,\
|
||||||
|
2,0,8,0,"Item","",\
|
||||||
|
"Scan/Time","8 hours","Monday","00:00","00:00",""
|
||||||
|
|
||||||
|
"WRPS_THIRTY_SEC","","","","20-08-2026 12:00:00",\
|
||||||
|
"1 weeks","","6 hours",0,0,1,\
|
||||||
|
0,30,0,0,0,1,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,44,1,"",1,\
|
||||||
|
2,0,6,0,"Item","",\
|
||||||
|
"Scan/Time","8 hours","Monday","00:00","00:00",""
|
||||||
|
|
||||||
61
04-scada/ciserver-backup-2026-08/export_item_his_df.qli
Normal file
61
04-scada/ciserver-backup-2026-08/export_item_his_df.qli
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
|
||||||
|
@LANGUAGE
|
||||||
|
ENGLISH
|
||||||
|
|
||||||
|
|
||||||
|
@VERSION
|
||||||
|
1.03.00
|
||||||
|
|
||||||
|
|
||||||
|
!==================================================================================================================================
|
||||||
|
|
||||||
|
@FIELDS
|
||||||
|
NAME,ITEM_NAME,GROUP_NAME,ON_EACH_UPDATE,ON_FIRST_UPDATE
|
||||||
|
ON_OPTION_CHANGE,ON_PASS_PIT,ON_QUALITY_CHANGE,ON_STATUS_CHANGE,ON_VALUE_CHANGE,STORE_DEADBAND
|
||||||
|
STORE_HIGH_HIGH_LIMIT,STORE_HIGH_LIMIT,STORE_LOW_LIMIT,STORE_LOW_LOW_LIMIT,STORE_VALUE,STORE_AGG_MAX
|
||||||
|
STORE_AGG_MIN,STORE_AGG_AVG,STORE_AGG_INTG,STORE_AGG_STDV,STORE_AGG_CNT,STORE_AGG_DIFF_SUM
|
||||||
|
AGG_DIFF_SUM_RANGE,AGG_DIFF_SUM_PERC,AGG_INTG_PERIOD
|
||||||
|
|
||||||
|
@ITEM_HIS_DF
|
||||||
|
"EVENT:ESDS32DCD.HeTwTwLocksClos","ESDS32DCD.HeTwTwLocksClos","EVENT",0,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
1000000,10,"Second"
|
||||||
|
|
||||||
|
"EVENT:ESDS32DCD.HeTwTwLocksClosTest","ESDS32DCD.HeTwTwLocksClosTest","EVENT",0,0,\
|
||||||
|
0,0,0,1,1,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
1000000,10,"Second"
|
||||||
|
|
||||||
|
"FIVE_SECONDS:AID.WRPS.STN.INFLOW","AID.WRPS.STN.INFLOW","FIVE_SECONDS",0,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
1000000,10,"Second"
|
||||||
|
|
||||||
|
"FIVE_SECONDS:AID.WRPS.STN.LEVEL","AID.WRPS.STN.LEVEL","FIVE_SECONDS",0,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
1000000,10,"Second"
|
||||||
|
|
||||||
|
"ONE_HOUR:Site1.Comms_Status","Site1.Comms_Status","ONE_HOUR",0,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
1000000,10,"Second"
|
||||||
|
|
||||||
|
"WRPS_EVENT:AID.WRPS.STN.CMD_ACK","AID.WRPS.STN.CMD_ACK","WRPS_EVENT",1,1,\
|
||||||
|
1,0,1,1,1,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
1000000,10,"Second"
|
||||||
|
|
||||||
|
"WRPS_ONE_SEC:AID.WRPS.STN.LEVEL","AID.WRPS.STN.LEVEL","WRPS_ONE_SEC",0,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
1000000,10,"Second"
|
||||||
|
|
||||||
4268
04-scada/ciserver-backup-2026-08/export_items.qli
Normal file
4268
04-scada/ciserver-backup-2026-08/export_items.qli
Normal file
File diff suppressed because it is too large
Load diff
356
04-scada/hmi/DEPLOY.md
Normal file
356
04-scada/hmi/DEPLOY.md
Normal file
|
|
@ -0,0 +1,356 @@
|
||||||
|
# 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 in `out/` — 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>`,
|
||||||
|
> the `99.99` set by hand). Do not deploy it and do not rebuild over it.
|
||||||
|
|
||||||
|
Regenerate with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python 04-scada/hmi/build_display.py && python 04-scada/hmi/build_tagtest.py
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> **After any `.qli` import, 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 `.qli` import 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`.** Change `build_display.py` and 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:
|
||||||
|
|
||||||
|
1. `04-scada/modbus_points/wrps_section_df.qli` — sections
|
||||||
|
2. `04-scada/modbus_points/wrps_modbus_point_df.qli` — Modbus points
|
||||||
|
3. `04-scada/modbus_points/wrps_item_df.qli` — items
|
||||||
|
4. 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 `actionSetItemAttribute`
|
||||||
|
with the value carried in the filter
|
||||||
|
- navigation — pump symbols open `WRPS_FP_Pump`, SETPOINTS… opens
|
||||||
|
`WRPS_FP_Setpoints`, both faceplates return to `WRPS_Overview`
|
||||||
|
- **the wet well fill**, a `dataBar` bound to `STN.LEVEL`, 0–100%
|
||||||
|
- **every setpoint is editable** — a `numberField` on the eight boxes of
|
||||||
|
`WRPS_FP_Setpoints` and 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 `dataBar` carries its own
|
||||||
|
`lowLimit`/`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 `blinkEnabled` and `blinkDelay` 700 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]
|
||||||
|
> `--verify` compares against `item-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. `--harvest` the saved file to re-pin it — but note `item-ids.csv`
|
||||||
|
> describes exactly one server at a time, so harvesting against `CORE_0001`
|
||||||
|
> makes it wrong for `PXiSEDev`. 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 from `ROOT`, `INSTALL` and `STATION` in
|
||||||
|
`gen_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 `.qli` field only, `gen_ciserver_qli.py:289`. No display
|
||||||
|
carries a node reference, so a node rename needs no edit to `out/*.xml`.
|
||||||
|
|
||||||
|
### The ritual — after ANY .qli import
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<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/scada-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_TagTest` round 1 bound
|
||||||
|
all 49 items twice — once as `<data ref>` in `globalSection`, 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.** `actionActivateDisplay` needs
|
||||||
|
`<type value="ActivateSpecific"/>` plus `<layout>FixedPopup</layout>` and
|
||||||
|
`<layoutFrame>default</layoutFrame>`; without them it replaces the current
|
||||||
|
display. Faceplate CLOSE buttons now use `actionExitDisplay`, which closes the
|
||||||
|
popup rather than activating another display over it.
|
||||||
|
- **Grey blobs over the pump symbols.** I had used a real `button` as 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
|
||||||
|
|
||||||
|
1. **Text placement is approximate.** `keepOriginalSize` is set and the width is
|
||||||
|
estimated from character count, so a label may sit a few pixels off. The editor
|
||||||
|
will normalise on save.
|
||||||
|
3. **Pipes are rectangles, not `line` components.** The `line` component positions
|
||||||
|
itself with `curvePoints`/`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.
|
||||||
|
4. **`number.format`** is emitted as a Java `DecimalFormat` pattern (`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
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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.
|
||||||
146
04-scada/hmi/README.md
Normal file
146
04-scada/hmi/README.md
Normal file
|
|
@ -0,0 +1,146 @@
|
||||||
|
# hmi — what CI Server can draw, and the kit we build with
|
||||||
|
|
||||||
|
Study of the live CI Server deployment copied to
|
||||||
|
`99-reference/ciserver-hmi-deployment/`: 53 components, 3130
|
||||||
|
symbols (177 of them the AOG standard set), 74 displays, 2 layouts.
|
||||||
|
|
||||||
|
> **`C:\Users\Public\Yokogawa\tls` is reference only.** Never edit it. Everything
|
||||||
|
> is authored in this repo and handed over for the user to deploy.
|
||||||
|
|
||||||
|
## 1. Components — the primitives a display is made of
|
||||||
|
|
||||||
|
| Group | Components |
|
||||||
|
|---|---|
|
||||||
|
| Drawing | `Rectangle` `Ellipse` `Arc` `Line` `Polygon` `Spline` `Text` `Icon` |
|
||||||
|
| Structure | `Display` `GraphicSet` `Symbol` `SymbolInstance` `VisibilityGroup` `VisualizationLayer` `VisualizationParameter` `Link` |
|
||||||
|
| Data display | `Number` `DataBar` `CircularScale` `RectangularScale` `Radial` `MicroTrend` `Chart` `DtsChart` |
|
||||||
|
| Input | `JButton` `JToggleButton` `JCheckBox` `JRadioButton` `JComboBox` `JSlider` `JSpinner` `JFormattedNumberField` `JFormattedTextField` `JTextArea` `JDateTimeField` `JList` `JNavigationTree` |
|
||||||
|
| System | `AlarmOverview` `ShelvedOverview` `BlockedItems` `AlarmSound` `Playback` `Reporting` `MapViewer` `PDFViewer` `DatasetTable` `DatasetForm` `ScriptedFunction` |
|
||||||
|
|
||||||
|
So the earlier claim that CI View was "rectangles and text only" was wrong — that
|
||||||
|
was an artefact of the *Straddle* screens, which happen to use little else.
|
||||||
|
Ellipses, lines, polygons, trends, gauges and real input widgets are all available.
|
||||||
|
|
||||||
|
## 2. AOG — the standard symbol library
|
||||||
|
|
||||||
|
`symbols/AOG_*.xml` is Yokogawa's **Advanced Operating Graphics** set: a
|
||||||
|
high-performance HMI standard in the ISA-101 tradition. Relevant here:
|
||||||
|
|
||||||
|
| Purpose | Symbol |
|
||||||
|
|---|---|
|
||||||
|
| Analog value | `AOG_ProcessIndicator`, `AOG_NumberIndicator` |
|
||||||
|
| Typed instrument | `AOG_IndicatorLevel` `AOG_IndicatorFlow` `AOG_IndicatorPressure` `AOG_IndicatorTemperature` |
|
||||||
|
| Bar / needle | `AOG_VerticalBarIndicator` `AOG_HorizontalBarIndicator` `AOG_*NeedleIndicator` `AOG_RadialNeedleIndicator` |
|
||||||
|
| Digital state | `AOG_OnOffIndicator` `AOG_AlarmIndicator` `AOG_TextIndicator` `AOG_ManualIndicator` |
|
||||||
|
| Equipment | `AOG_Pump` `AOG_PumpSimple` `AOG_PumpWithOutlet` `AOG_Motor` `AOG_Tank` `AOG_Vessel` `AOG_AtmosphericTank` |
|
||||||
|
| Valves | `AOG_CheckValve` `AOG_Horizontal2WayValve` `AOG_VerticalOnOffValve` `AOG_ButterflyValve` `AOG_BlockValveStation` |
|
||||||
|
| Instrument bubble | `AOG_Instrument` `AOG_Transmitter` |
|
||||||
|
| Faceplate | `AOG_Faceplate` `AOG_FP_Frame` `AOG_FP_PVValue` `AOG_FP_SVValue` `AOG_FP_Mode` `AOG_FP_Status` `AOG_FP_BarIndicator` |
|
||||||
|
| Trend | `AOG_Trend` `AOG_TrendIndicator` |
|
||||||
|
| Screen furniture | `AOG_DisplayTitle` `AOG_SelectionBox` `AOG_NavigationSelectionBox` `AOG_TagNameComponent` |
|
||||||
|
|
||||||
|
An `AOG_O*` variant exists for most (`AOG_OPump`, `AOG_ONumberIndicator` …).
|
||||||
|
|
||||||
|
### The parameter contract — this is what standardisation means here
|
||||||
|
|
||||||
|
Every dynamic AOG symbol takes the same external parameters:
|
||||||
|
|
||||||
|
```
|
||||||
|
item the SCADA item to display (AID.WRPS.STN.LEVEL)
|
||||||
|
setPointItem optional companion item
|
||||||
|
tagName label, with alwaysShowTagName / neverShowTagName
|
||||||
|
format numeric format
|
||||||
|
showBackground draw the indicator box
|
||||||
|
showAlarmIndicator built-in alarm annunciation
|
||||||
|
selectable can the operator click it
|
||||||
|
displayToActivate display opened on click <- this is how faceplates work
|
||||||
|
```
|
||||||
|
|
||||||
|
Two consequences worth stating plainly:
|
||||||
|
|
||||||
|
1. **Analog values are consistent by construction.** Every one is an
|
||||||
|
`AOG_ProcessIndicator`; nothing is hand-drawn, so nothing can drift.
|
||||||
|
2. **Faceplates are separate displays**, not overlays. A symbol names one in
|
||||||
|
`displayToActivate`. `AOG_ItemFaceplate` and `AOG_ObjectFacePlate` already
|
||||||
|
exist and can be used as-is, or copied and specialised for a pump.
|
||||||
|
|
||||||
|
### Ready-made displays
|
||||||
|
|
||||||
|
`displays/AOG_displays/` includes `AOG_DisplayTemplate` (start a new display from
|
||||||
|
it), `AOG_Style` (the colour tokens), `AOG_ItemFaceplate`, `AOG_ItemTuningPanel`,
|
||||||
|
`AOG_AlarmBanner`, `AOG_AlarmCurrent`, `AOG_MenuMain`, `AOG_SetNumberPV/SV/SH/SL`
|
||||||
|
(the standard numeric-entry popups — use these for setpoint writes rather than
|
||||||
|
building an entry field).
|
||||||
|
|
||||||
|
## 3. Two styles exist on this system — pick one
|
||||||
|
|
||||||
|
| | AOG standard | Site style (Straddle screens) |
|
||||||
|
|---|---|---|
|
||||||
|
| Ground | `#E3E3E3` light grey | `#171B22` near-black |
|
||||||
|
| Panels | `#F0F0F0` / `#F2F2F2` | `#262D38` / `#333B47` |
|
||||||
|
| Text | `#404040` / `#6D6D6D` | `#C8D0DA` |
|
||||||
|
| Accent | none — colour is reserved | `#5FB8D0` cyan |
|
||||||
|
| Alarm | `#FF0000` `#FFA500` `#FFFF00` | `#D65B5B` |
|
||||||
|
| Built from | AOG symbols | hand-drawn rectangles + text |
|
||||||
|
|
||||||
|
The AOG palette follows the high-performance HMI convention: a grey plant, and
|
||||||
|
**colour only where something is abnormal**. The site's own Straddle screens are a
|
||||||
|
custom dark theme that does not use the AOG library at all — no display in the
|
||||||
|
deployment instantiates a single `AOG_*` symbol.
|
||||||
|
|
||||||
|
**Recommendation: AOG.** It is the product standard, it brings the faceplate
|
||||||
|
machinery and alarm annunciation for free, and its parameter contract is what
|
||||||
|
keeps every indicator identical. The cost is that the demo will not match the
|
||||||
|
Straddle screens an operator may already know.
|
||||||
|
|
||||||
|
## 4. Decision: our own kit, AOG as reference only
|
||||||
|
|
||||||
|
Settled 2026-08-14 with the user:
|
||||||
|
|
||||||
|
- **Build our own components**, not AOG instances — only what this project needs.
|
||||||
|
AOG's *parameter contract* is the thing worth copying, not its artwork.
|
||||||
|
- **Light theme, but not AOG's.** The customer reads AOG as dated; the cause is
|
||||||
|
the bevelled 3D symbols and mid-grey ground, so the kit is flat, square, hairline
|
||||||
|
bordered, white on cool neutral.
|
||||||
|
- **16:9**, and **operators may write** setpoint, command and simulation items.
|
||||||
|
|
||||||
|
The kit is specified in `component-kit.md`. The table below is superseded by it
|
||||||
|
and kept only to show which AOG symbol each of our components was modelled on.
|
||||||
|
|
||||||
|
## 5. AOG reference for each component we built
|
||||||
|
|
||||||
|
What each element of the display is built from, so the same thing always looks the
|
||||||
|
same:
|
||||||
|
|
||||||
|
| Element | Component | Bound to |
|
||||||
|
|---|---|---|
|
||||||
|
| Title bar | `AOG_DisplayTitle` | — |
|
||||||
|
| Wet well | `AOG_Tank` + `AOG_VerticalBarIndicator` | `STN.LEVEL` |
|
||||||
|
| Level, flows, pressures, speed | `AOG_ProcessIndicator` | `STN.*`, `PU30x.*` |
|
||||||
|
| Level switches, run/avail/trip | `AOG_OnOffIndicator` | `PU30x.*`, `STN.*` |
|
||||||
|
| Pumps | `AOG_Pump`, `displayToActivate` = pump faceplate | `PU30x.RUNNING` |
|
||||||
|
| Check / isolation valves | `AOG_CheckValve`, `AOG_Horizontal2WayValve` | static |
|
||||||
|
| Instrument bubbles | `AOG_Instrument` | tag text |
|
||||||
|
| Setpoint entry | `AOG_SetNumberSV` popup | `SP.*` |
|
||||||
|
| Pump faceplate | copy of `AOG_ItemFaceplate` | `PU30x.*` |
|
||||||
|
| Buttons (mode, commands, simulation) | `AOG_SelectionBox` / `JButton` | `SP.MODE`, `SP.CMD_WORD`, `SIM.*` |
|
||||||
|
|
||||||
|
## Scripts in this directory
|
||||||
|
|
||||||
|
| Command | What it does |
|
||||||
|
|---|---|
|
||||||
|
| `python build_display.py` | Builds the three displays into `out/`. **Refuses to build if the item ids cannot be proven current.** |
|
||||||
|
| `python build_display.py --harvest <saved.xml>` | Re-calibrates the item ids from a display CI View saved, and records provenance |
|
||||||
|
| `python build_display.py --verify <saved.xml>` | Compares CI Server's own ids against what the build emits; exit 1 on any mismatch |
|
||||||
|
| `python build_tagtest.py` | Builds the diagnostic display that lists every item |
|
||||||
|
| `python check_layout.py` | Reports text that collides with other text; exit 1 on any collision |
|
||||||
|
|
||||||
|
The two `--` modes exist because of the failure that keeps recurring: an item
|
||||||
|
id that no longer matches CI Server binds silently to nothing, and the screen
|
||||||
|
looks fine. `DEPLOY.md` has the ritual.
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Study complete; nothing built yet. The layout is under review as a mockup — see
|
||||||
|
the preview link in the conversation, and `04-scada/hmi/` will hold the display
|
||||||
|
XML once the style question above is settled.
|
||||||
1591
04-scada/hmi/build_display.py
Normal file
1591
04-scada/hmi/build_display.py
Normal file
File diff suppressed because it is too large
Load diff
327
04-scada/hmi/build_tagtest.py
Normal file
327
04-scada/hmi/build_tagtest.py
Normal file
|
|
@ -0,0 +1,327 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Build WRPS_TagTest.xml - a bare diagnostic display, no graphics.
|
||||||
|
|
||||||
|
Why this exists
|
||||||
|
---------------
|
||||||
|
Round 1 (shared <data ref> vs inline <data type="data">) came back with both
|
||||||
|
columns live for all 49 items, which ruled out the binding form, the derived
|
||||||
|
itemIds and the CI Server side. The remaining difference was <format>: the
|
||||||
|
generated displays carried DecimalFormat patterns ("0", "0.0"), and CI View
|
||||||
|
writes <format> as a DIGIT MASK ("99.99"). A one-digit-wide mask has nowhere
|
||||||
|
to render a four-digit level.
|
||||||
|
|
||||||
|
Round 2, this build, tests the masks in point_format.py before they go
|
||||||
|
anywhere near the operator screens:
|
||||||
|
|
||||||
|
* every item in the project, one row each, numbered - nothing else on
|
||||||
|
the screen, so nothing else can be blamed
|
||||||
|
* each item is bound TWICE, side by side:
|
||||||
|
PLAIN - no <format> at all. The control: this is exactly what was
|
||||||
|
live in round 1, so it must stay live.
|
||||||
|
MASKED - the same binding plus <format> from point_format.py, written
|
||||||
|
where CI View puts it (straight after <htmlId>, no <value>)
|
||||||
|
* so each row answers, independently: does the mask render the value, is
|
||||||
|
it wide enough for the real range, and does it round to the right
|
||||||
|
number of decimals
|
||||||
|
|
||||||
|
Read it as: PLAIN live + MASKED blank -> that mask is too narrow or wrong.
|
||||||
|
both live, MASKED rounded -> the mask is good.
|
||||||
|
both blank -> that item, not the mask.
|
||||||
|
|
||||||
|
python 04-scada/hmi/build_tagtest.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import point_format as pf
|
||||||
|
|
||||||
|
HERE = Path(__file__).resolve().parent
|
||||||
|
OUT = HERE / "out" / "WRPS_TagTest.xml"
|
||||||
|
ITEM_FILE = HERE.parents[1] / "04-scada" / "modbus_points" / "wrps_item_df.qli"
|
||||||
|
IDS_FILE = HERE / "item-ids.csv"
|
||||||
|
|
||||||
|
W, H = 1920, 1080
|
||||||
|
|
||||||
|
# This display is the BOOTSTRAP: it binds every project item, so saving it
|
||||||
|
# once in CI View measures all 49 ids in a single pass. It is therefore
|
||||||
|
# the one display allowed to carry ids that are not measured yet - it has
|
||||||
|
# to be buildable before anything has been measured, or there is no way to
|
||||||
|
# measure anything. CI View resolves each connection by NAME on save, so
|
||||||
|
# a placeholder id is enough for the harvest to work.
|
||||||
|
#
|
||||||
|
# It used to carry its own copy of the id rule, with the base hardcoded at
|
||||||
|
# 136 - which silently went a whole generation stale when the items were
|
||||||
|
# re-imported. Ids now come from build_display, the single source, and
|
||||||
|
# anything unmeasured is an obvious placeholder rather than a plausible
|
||||||
|
# wrong number.
|
||||||
|
PLACEHOLDER = "1.1.0.0.0.1.1.%d"
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ items
|
||||||
|
|
||||||
|
|
||||||
|
def items():
|
||||||
|
"""Project items in creation order, with their itemId.
|
||||||
|
|
||||||
|
(index, name, representation, itemId, measured?)
|
||||||
|
"""
|
||||||
|
import build_display as bd
|
||||||
|
measured = bd.measured_ids()
|
||||||
|
reps = bd.item_reps()
|
||||||
|
|
||||||
|
q = ITEM_FILE.read_text(encoding="utf-8")
|
||||||
|
order, seen = [], set()
|
||||||
|
for m in re.finditer(r'^"(AID\.WRPS\.[A-Z0-9_.]+)"', q, re.M):
|
||||||
|
if m.group(1) not in seen:
|
||||||
|
seen.add(m.group(1))
|
||||||
|
order.append(m.group(1))
|
||||||
|
|
||||||
|
out = []
|
||||||
|
for k, name in enumerate(order):
|
||||||
|
rep = reps.get(name, "Real")
|
||||||
|
iid = measured.get(name, PLACEHOLDER % (1 if rep == "Boolean" else 7))
|
||||||
|
out.append((k, name, rep, iid, name in measured))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ layout
|
||||||
|
ROWS_PER_COL = 25
|
||||||
|
ROW_PITCH = 38
|
||||||
|
TOP = 92
|
||||||
|
COL_X = (40, 990) # left edge of each page column
|
||||||
|
W_IDX, W_NAME, W_REF = 34, 470, 150
|
||||||
|
|
||||||
|
LAB = "Segoe UI"
|
||||||
|
VAL = "Consolas"
|
||||||
|
|
||||||
|
|
||||||
|
def esc(s):
|
||||||
|
return (s.replace("&", "&").replace("<", "<").replace(">", ">"))
|
||||||
|
|
||||||
|
|
||||||
|
def half_w(text, size, font):
|
||||||
|
"""Half the rendered width. Approximate - the editor normalises on save."""
|
||||||
|
per = 0.60 if font == VAL else 0.52
|
||||||
|
return round(len(text) * size * per / 2.0, 4)
|
||||||
|
|
||||||
|
|
||||||
|
class Doc:
|
||||||
|
def __init__(self):
|
||||||
|
self.refs = [] # (id, xml) in globalSection
|
||||||
|
self.parts = [] # component xml
|
||||||
|
self._next_ref = 10
|
||||||
|
self._next_html = 100
|
||||||
|
self._cache = {}
|
||||||
|
|
||||||
|
def ref(self, key, xml):
|
||||||
|
if key in self._cache:
|
||||||
|
return self._cache[key]
|
||||||
|
rid = self._next_ref
|
||||||
|
self._next_ref += 1
|
||||||
|
self.refs.append((rid, xml))
|
||||||
|
self._cache[key] = rid
|
||||||
|
return rid
|
||||||
|
|
||||||
|
def hid(self):
|
||||||
|
self._next_html += 1
|
||||||
|
return self._next_html
|
||||||
|
|
||||||
|
def paint(self, rgb):
|
||||||
|
return self.ref(("paint", rgb),
|
||||||
|
'<sharedObject type="paint">\n'
|
||||||
|
' <paint type="colorSet" r="%d" g="%d" b="%d"/>\n'
|
||||||
|
' </sharedObject>' % rgb)
|
||||||
|
|
||||||
|
def filt(self, value="0.0"):
|
||||||
|
return self.ref(("filter", value),
|
||||||
|
'<sharedObject type="filter">\n'
|
||||||
|
' <value>%s</value>\n'
|
||||||
|
' </sharedObject>' % value)
|
||||||
|
|
||||||
|
# --- components ----------------------------------------------------
|
||||||
|
def text(self, x, y, value, size=14, colour=(14, 22, 33), bold=False,
|
||||||
|
font=LAB):
|
||||||
|
"""x is the LEFT edge; CI View stores centre + half extents."""
|
||||||
|
hw = half_w(value, size, font)
|
||||||
|
hh = round(size * 0.62 / 2.0 + 2.0, 4)
|
||||||
|
self.parts.append(
|
||||||
|
' <text type="componentData">\n'
|
||||||
|
' <htmlId>%d</htmlId>\n'
|
||||||
|
' <value>%s</value>\n'
|
||||||
|
' <font type="font" name="%s"%s size="%d" underline="false" '
|
||||||
|
'strikethrough="false"/>\n'
|
||||||
|
' <fillPaint ref="%d"/>\n'
|
||||||
|
' <stroke type="stroke" width="1.0"/>\n'
|
||||||
|
' <keepOriginalSize>true</keepOriginalSize>\n'
|
||||||
|
' <x>%s</x>\n <y>%s</y>\n'
|
||||||
|
' <top>%s</top>\n <bottom>%s</bottom>\n'
|
||||||
|
' <left>%s</left>\n <right>%s</right>\n'
|
||||||
|
' <rotation>0.0</rotation>\n <shear>0.0</shear>\n'
|
||||||
|
' </text>'
|
||||||
|
% (self.hid(), esc(value), font, ' bold="true"' if bold else '',
|
||||||
|
size, self.paint(colour), round(x + hw, 4), float(y),
|
||||||
|
hh, hh, hw, hw))
|
||||||
|
|
||||||
|
def action(self, item, iid, filter_ref):
|
||||||
|
"""The actionConnectTo block, byte-for-byte as CI View saved it."""
|
||||||
|
return ('<action type="actionConnectTo">\n'
|
||||||
|
' <property type="property" name="number.value"/>\n'
|
||||||
|
' <filter ref="%d"/>\n'
|
||||||
|
' <connection type="connection">\n'
|
||||||
|
' <direction>1</direction>\n'
|
||||||
|
' <itemName>%s</itemName>\n'
|
||||||
|
' <itemId>%s</itemId>\n'
|
||||||
|
' </connection>\n'
|
||||||
|
' </action>' % (filter_ref, item, iid))
|
||||||
|
|
||||||
|
def number(self, x, y, item, iid, mask=None, size=14,
|
||||||
|
colour=(14, 22, 33)):
|
||||||
|
"""A bound number. mask=None -> no <format> (the control).
|
||||||
|
|
||||||
|
Element order copies what CI View wrote when the FIT-201 INLET value
|
||||||
|
was formatted by hand: <htmlId>, <format>, <font>, ... , <data>.
|
||||||
|
A masked number carries no <value>.
|
||||||
|
"""
|
||||||
|
rid = self.ref(("data", item),
|
||||||
|
'<sharedObject type="data">\n %s\n '
|
||||||
|
'</sharedObject>' % self.action(item, iid, self.filt("0.0")))
|
||||||
|
if mask:
|
||||||
|
head = ' <format>%s</format>\n' % mask
|
||||||
|
sizer = mask
|
||||||
|
else:
|
||||||
|
head = ' <value>0</value>\n'
|
||||||
|
sizer = "0000000"
|
||||||
|
hw = half_w(sizer, size, VAL)
|
||||||
|
hh = round(size * 0.62 / 2.0 + 2.0, 4)
|
||||||
|
self.parts.append(
|
||||||
|
' <number type="componentData">\n'
|
||||||
|
' <htmlId>%d</htmlId>\n'
|
||||||
|
'%s'
|
||||||
|
' <font type="font" name="%s" size="%d" underline="false" '
|
||||||
|
'strikethrough="false"/>\n'
|
||||||
|
' <fillPaint ref="%d"/>\n'
|
||||||
|
' <stroke type="stroke" width="1.0"/>\n'
|
||||||
|
' <keepOriginalSize>true</keepOriginalSize>\n'
|
||||||
|
' <x>%s</x>\n <y>%s</y>\n'
|
||||||
|
' <top>%s</top>\n <bottom>%s</bottom>\n'
|
||||||
|
' <left>%s</left>\n <right>%s</right>\n'
|
||||||
|
' <rotation>0.0</rotation>\n <shear>0.0</shear>\n'
|
||||||
|
' <data ref="%d"/>\n'
|
||||||
|
' </number>'
|
||||||
|
% (self.hid(), head, VAL, size, self.paint(colour),
|
||||||
|
round(x + hw, 4), float(y), hh, hh, hw, hw, rid))
|
||||||
|
|
||||||
|
# --- serialise ------------------------------------------------------
|
||||||
|
def xml(self, title):
|
||||||
|
when = datetime.datetime.now().strftime("%Y.%m.%d %H:%M:%S.0 AEST")
|
||||||
|
g = "\n".join(' <intRef id="%d">\n %s\n </intRef>'
|
||||||
|
% (rid, x) for rid, x in self.refs)
|
||||||
|
vis = "".join(
|
||||||
|
' <visibilityGroup type="componentData">\n'
|
||||||
|
' <htmlId>%d</htmlId>\n <name>%s</name>\n'
|
||||||
|
' <description>%s</description>\n'
|
||||||
|
' <minimumZoomEnabled>true</minimumZoomEnabled>\n'
|
||||||
|
' <minimumZoomFactor>%s</minimumZoomFactor>\n'
|
||||||
|
' </visibilityGroup>\n'
|
||||||
|
% (h, n, d, z) for h, n, d, z in (
|
||||||
|
(2, "Overview", "Always shown", "10.0"),
|
||||||
|
(3, "Rough", "Shown when viewing a large area", "25.0"),
|
||||||
|
(4, "Standard", "Shown when using the default view setting", "100.0"),
|
||||||
|
(5, "Detailed", "Shown when zoomed in", "200.0"),
|
||||||
|
(6, "Fine", "Shown when zoomed in closely", "400.0"),
|
||||||
|
))
|
||||||
|
return (
|
||||||
|
'<?xml version="1.0" encoding="utf-8" ?>\n'
|
||||||
|
'<visualization protocolVersion="1.3.0.0">\n'
|
||||||
|
' <globalSection>\n%s\n </globalSection>\n'
|
||||||
|
' <coreObjectDefinition type="displayDefinition">\n'
|
||||||
|
' <version type="version" value="1.3.0.0"/>\n'
|
||||||
|
' <width>%d</width>\n <height>%d</height>\n'
|
||||||
|
' <referenceCheck>5</referenceCheck>\n'
|
||||||
|
' <defaultBgColor type="colorSet" r="255" g="255" b="255"/>\n'
|
||||||
|
' <defaultFgColor type="colorSet" r="14" g="22" b="33"/>\n'
|
||||||
|
' <defaultFont type="font" name="Segoe UI" size="14" '
|
||||||
|
'underline="false" strikethrough="false"/>\n'
|
||||||
|
' <defaultStroke type="stroke" width="1.0"/>\n'
|
||||||
|
' <grid type="grid" gridVisible="true" snappingActive="true" '
|
||||||
|
'verticalSnapInterval="4" horizontalSnapInterval="4" onTop="false">\n'
|
||||||
|
' <color type="colorSet" r="0" g="0" b="0"/>\n'
|
||||||
|
' </grid>\n'
|
||||||
|
' <revisionHistory type="revisionHistory">\n'
|
||||||
|
' <revision type="revision" who="ADMIN" when="%s" '
|
||||||
|
'what="%s" where="PXiSEDev"/>\n'
|
||||||
|
' </revisionHistory>\n'
|
||||||
|
' <blinkDelay>700</blinkDelay>\n'
|
||||||
|
' <blinkEnabled>true</blinkEnabled>\n'
|
||||||
|
' <mousePassThrough>false</mousePassThrough>\n'
|
||||||
|
'%s'
|
||||||
|
' <visualizationLayer type="componentData">\n'
|
||||||
|
' <htmlId>1</htmlId>\n <name>Layer1</name>\n'
|
||||||
|
' </visualizationLayer>\n'
|
||||||
|
' <componentCountHint>%d</componentCountHint>\n'
|
||||||
|
'%s\n'
|
||||||
|
' </coreObjectDefinition>\n'
|
||||||
|
'</visualization>\n'
|
||||||
|
% (g, W, H, when, esc(title), vis,
|
||||||
|
len(self.parts) + 6, "\n".join(self.parts)))
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ build
|
||||||
|
def build():
|
||||||
|
d = Doc()
|
||||||
|
rows = items()
|
||||||
|
|
||||||
|
grey = (70, 84, 104)
|
||||||
|
d.text(40, 30, "WRPS TAG TEST 2 - format masks, every project item",
|
||||||
|
size=20, bold=True)
|
||||||
|
d.text(40, 56, "left number = PLAIN, no <format> (this was live last time, "
|
||||||
|
"so it must stay live) | right number = the same binding "
|
||||||
|
"MASKED with the mask printed beside it | masked blank "
|
||||||
|
"while plain is live means that mask is wrong",
|
||||||
|
size=12, colour=grey)
|
||||||
|
|
||||||
|
for k, name, rep, iid, measured in rows:
|
||||||
|
col, row = divmod(k, ROWS_PER_COL)
|
||||||
|
x = COL_X[col]
|
||||||
|
y = TOP + row * ROW_PITCH
|
||||||
|
short = name[len("AID.WRPS."):]
|
||||||
|
mask = pf.mask(name)
|
||||||
|
|
||||||
|
d.text(x, y, "%02d" % (k + 1), size=13, colour=grey, font=VAL)
|
||||||
|
d.text(x + W_IDX, y, short, size=13, font=VAL)
|
||||||
|
d.text(x + W_IDX + 210, y, "%s%s" % (rep[0], "" if measured else " *"),
|
||||||
|
size=11, colour=grey, font=VAL)
|
||||||
|
|
||||||
|
nx = x + W_IDX + 250
|
||||||
|
d.number(nx, y, name, iid, mask=None)
|
||||||
|
d.text(nx + 120, y, mask.rjust(5), size=11, colour=grey, font=VAL)
|
||||||
|
d.number(nx + 190, y, name, iid, mask=mask)
|
||||||
|
|
||||||
|
unmeasured = [r for r in rows if not r[4]]
|
||||||
|
d.text(40, 1050,
|
||||||
|
"* itemId NOT measured - placeholder. Save this display in CI View "
|
||||||
|
"and harvest it to measure all %d. "
|
||||||
|
"%d items, %d bindings, %d distinct masks."
|
||||||
|
% (len(rows), len(rows), len(rows) * 2, len(set(pf.MASKS.values()))),
|
||||||
|
size=12, colour=grey)
|
||||||
|
|
||||||
|
OUT.parent.mkdir(exist_ok=True)
|
||||||
|
OUT.write_text(d.xml("Generated by build_tagtest.py"), encoding="utf-8")
|
||||||
|
print("wrote %s (%d items, %d numbers, %d shared objects)"
|
||||||
|
% (OUT, len(rows), len(rows) * 2, len(d.refs)))
|
||||||
|
if unmeasured:
|
||||||
|
print(" %d of %d ids are PLACEHOLDERS and will read 0: %s"
|
||||||
|
% (len(unmeasured), len(rows),
|
||||||
|
", ".join(r[1].replace("AID.WRPS.", "") for r in unmeasured[:6])
|
||||||
|
+ (" ..." if len(unmeasured) > 6 else "")))
|
||||||
|
print(" That is expected before a harvest - this display exists to "
|
||||||
|
"measure them.\n"
|
||||||
|
" Deploy it, open it in CI View, link one value, save, then:\n"
|
||||||
|
" python build_display.py --harvest <the saved file>")
|
||||||
|
else:
|
||||||
|
print(" all %d ids measured" % len(rows))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
build()
|
||||||
225
04-scada/hmi/check_layout.py
Normal file
225
04-scada/hmi/check_layout.py
Normal file
|
|
@ -0,0 +1,225 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Overlap audit for the generated displays.
|
||||||
|
|
||||||
|
python check_layout.py [display.xml ...]
|
||||||
|
|
||||||
|
Reads the built XML and reports text that collides with other text, or
|
||||||
|
that escapes its panel. Written after a units relabel pushed the wet
|
||||||
|
well caption into the top ladder label and nobody noticed until the
|
||||||
|
screens were on a monitor - the generator places components by
|
||||||
|
arithmetic, so a collision is arithmetic too, and worth catching here
|
||||||
|
rather than in CI View.
|
||||||
|
|
||||||
|
Text width is estimated, not measured: the value font is Consolas, which
|
||||||
|
is monospaced at ~0.55 em, and the label font is close enough at this
|
||||||
|
size. So the check is approximate by construction - it is a net for
|
||||||
|
gross collisions, not a typesetter.
|
||||||
|
|
||||||
|
Some components no longer HAVE one position. A level mark bound to its
|
||||||
|
setpoint through `<kind>.y` sits wherever the operator last typed, so
|
||||||
|
checking it where the generator happened to draw it checks the one
|
||||||
|
arrangement that is guaranteed not to be the problem. Those are swept
|
||||||
|
across their whole input range instead - see `sweep`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
HERE = Path(__file__).resolve().parent
|
||||||
|
CHAR_W = 0.55 # em, Consolas advance width
|
||||||
|
PAD = 2.0 # px of slack before two boxes count as colliding
|
||||||
|
|
||||||
|
|
||||||
|
def components(xml):
|
||||||
|
"""(kind, x1, y1, x2, y2, value, size) for everything positioned."""
|
||||||
|
out = []
|
||||||
|
for kind, body in re.findall(
|
||||||
|
r'<(rectangle|text|number|ellipse) type="componentData">(.*?)</\1>',
|
||||||
|
xml, re.S):
|
||||||
|
def num(tag):
|
||||||
|
m = re.search(r"<%s>(-?[\d.]+)</%s>" % (tag, tag), body)
|
||||||
|
return float(m.group(1)) if m else None
|
||||||
|
|
||||||
|
cx, cy = num("x"), num("y")
|
||||||
|
top, bot, left, right = num("top"), num("bottom"), num("left"), num("right")
|
||||||
|
if None in (cx, cy, top, bot, left, right):
|
||||||
|
continue
|
||||||
|
val = re.search(r"<value>(.*?)</value>", body)
|
||||||
|
size = re.search(r'size="(\d+)"', body)
|
||||||
|
out.append((kind, cx - left, cy - top, cx + right, cy + bot,
|
||||||
|
val.group(1) if val else "", int(size.group(1)) if size else 0,
|
||||||
|
"<data ref=" in body, round(cx, 1), round(cy, 1)))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def stacked(a, b):
|
||||||
|
"""True if two texts are alternative states of one thing.
|
||||||
|
|
||||||
|
A state stack - OFF / IDLE / PUMPING - is deliberately drawn at ONE
|
||||||
|
position with one member visible at a time. That is not a collision,
|
||||||
|
and reporting it as one buries the real ones. The signature is:
|
||||||
|
both carry a visibility binding, and both sit at the same anchor.
|
||||||
|
"""
|
||||||
|
return (a[7] and b[7]
|
||||||
|
and round(a[1], 0) == round(b[1], 0) # same left edge
|
||||||
|
and round(a[9], 0) == round(b[9], 0)) # same centre line
|
||||||
|
|
||||||
|
|
||||||
|
def text_box(c):
|
||||||
|
"""A text component's *drawn* extent, which is not its stored box.
|
||||||
|
|
||||||
|
The stored left/right on a text component is its anchor box, not the
|
||||||
|
glyph run - a right-anchored label stores a wide box and draws inside
|
||||||
|
the right edge of it. Estimate the run instead.
|
||||||
|
"""
|
||||||
|
kind, x1, y1, x2, y2, val, size = c[:7]
|
||||||
|
w = len(val) * size * CHAR_W
|
||||||
|
# right-anchored labels store the anchor at their right edge
|
||||||
|
if x2 - x1 > w * 1.5:
|
||||||
|
x1 = x2 - w
|
||||||
|
else:
|
||||||
|
x2 = x1 + w
|
||||||
|
return x1, y1, x2, y2
|
||||||
|
|
||||||
|
|
||||||
|
def overlaps(a, b):
|
||||||
|
ax1, ay1, ax2, ay2 = text_box(a)
|
||||||
|
bx1, by1, bx2, by2 = text_box(b)
|
||||||
|
return not (ax2 - PAD <= bx1 or bx2 - PAD <= ax1
|
||||||
|
or ay2 - PAD <= by1 or by2 - PAD <= ay1)
|
||||||
|
|
||||||
|
|
||||||
|
def line_boxes(xml):
|
||||||
|
"""Process lines as thin boxes - the obstacles text must not sit on.
|
||||||
|
|
||||||
|
THE GAP THIS CLOSES. The first version of this checker compared text
|
||||||
|
against text only, so it passed a screen whose top row of values was
|
||||||
|
sitting on the inlet pipe and whose data column ran across the
|
||||||
|
discharge manifold. It measured what was easy, not what was wrong.
|
||||||
|
"""
|
||||||
|
out = []
|
||||||
|
for body in re.findall(r'<line type="componentData">(.*?)</line>', xml, re.S):
|
||||||
|
pts = [float(v) for v in re.findall(r"<(?:x|y)>(-?[\d.]+)</(?:x|y)>", body)]
|
||||||
|
if len(pts) < 2:
|
||||||
|
continue
|
||||||
|
cx, cy = pts[0], pts[1]
|
||||||
|
def num(tag):
|
||||||
|
m = re.search(r"<%s>(-?[\d.]+)</%s>" % (tag, tag), body)
|
||||||
|
return float(m.group(1)) if m else 0.0
|
||||||
|
l, r_, t, b = num("left"), num("right"), num("top"), num("bottom")
|
||||||
|
out.append(("line", cx - l, cy - t, cx + r_, cy + b, "", 0, False, 0, 0))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def shared_data(xml):
|
||||||
|
"""globalSection data objects by id - where a component's actions live."""
|
||||||
|
return dict(re.findall(
|
||||||
|
r'<intRef id="(\d+)"><sharedObject type="data">(.*?)</sharedObject></intRef>',
|
||||||
|
xml, re.S))
|
||||||
|
|
||||||
|
|
||||||
|
def movers(xml):
|
||||||
|
"""Components whose y is bound to an item, with the law that moves them.
|
||||||
|
|
||||||
|
Yields (component, y_at_input_0, y_at_input_end, input_end, item). The
|
||||||
|
component tuple is the one `components()` produces, so it drops straight
|
||||||
|
into the same overlap test.
|
||||||
|
"""
|
||||||
|
shared = shared_data(xml)
|
||||||
|
out = []
|
||||||
|
for kind, body in re.findall(
|
||||||
|
r'<(rectangle|text|number) type="componentData">(.*?)</\1>', xml, re.S):
|
||||||
|
ref = re.search(r'<data ref="(\d+)"/>', body)
|
||||||
|
if not ref or ref.group(1) not in shared:
|
||||||
|
continue
|
||||||
|
m = re.search(r'name="%s\.y"/>.*?<inputValueEnd>([\d.]+)</inputValueEnd>'
|
||||||
|
r"<outputValueStart>(-?[\d.]+)</outputValueStart>"
|
||||||
|
r"<outputValueEnd>(-?[\d.]+)</outputValueEnd>.*?"
|
||||||
|
r"<itemName>([^<]+)</itemName>" % kind,
|
||||||
|
shared[ref.group(1)], re.S)
|
||||||
|
if not m:
|
||||||
|
continue
|
||||||
|
end, y0, y1 = float(m.group(1)), float(m.group(2)), float(m.group(3))
|
||||||
|
# a number carries no <value>; its mask is what it will be as wide as
|
||||||
|
val = re.search(r"<value>(.*?)</value>", body)
|
||||||
|
fmt = re.search(r"<format>([^<]*)</format>", body)
|
||||||
|
text = val.group(1) if val else (fmt.group(1) if fmt else "")
|
||||||
|
for c in components("<%s type=\"componentData\">%s</%s>" % (kind, body, kind)):
|
||||||
|
out.append((c[:5] + (text,) + c[6:], y0, y1, end, m.group(4)))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def sweep(xml, statics, step=0.5):
|
||||||
|
"""Drive every moving component across its range against what stays put.
|
||||||
|
|
||||||
|
The invariant being enforced is that a mark which can be moved and a
|
||||||
|
mark which cannot never share a column - because a mark that moves will
|
||||||
|
eventually be driven to the height of one that does not. Checking the
|
||||||
|
positions the generator drew proves nothing about that; only the sweep
|
||||||
|
does.
|
||||||
|
|
||||||
|
Moving-against-moving is deliberately NOT reported. Two setpoints set
|
||||||
|
close together really do put their marks close together, and that is the
|
||||||
|
screen telling the truth about the thresholds, not a layout fault.
|
||||||
|
"""
|
||||||
|
hits = {}
|
||||||
|
for c, y0, y1, end, item in movers(xml):
|
||||||
|
cy = (c[2] + c[4]) / 2.0
|
||||||
|
v = 0.0
|
||||||
|
while v <= end + 1e-9:
|
||||||
|
dy = (y0 + (y1 - y0) * v / end) - cy
|
||||||
|
moved = (c[0], c[1], c[2] + dy, c[3], c[4] + dy) + c[5:]
|
||||||
|
for s in statics:
|
||||||
|
if overlaps(moved, s):
|
||||||
|
key = (item, c[5] or c[0], s[5] or s[0])
|
||||||
|
lo, hi = hits.get(key, (v, v))
|
||||||
|
hits[key] = (min(lo, v), max(hi, v))
|
||||||
|
v += step
|
||||||
|
return hits
|
||||||
|
|
||||||
|
|
||||||
|
def audit(path):
|
||||||
|
xml = path.read_text(encoding="utf-8")
|
||||||
|
comps = components(xml)
|
||||||
|
texts = [c for c in comps if c[0] == "text" and c[5].strip()]
|
||||||
|
lines = line_boxes(xml)
|
||||||
|
hits = []
|
||||||
|
for i, a in enumerate(texts):
|
||||||
|
for b in texts[i + 1:]:
|
||||||
|
if overlaps(a, b) and not stacked(a, b):
|
||||||
|
hits.append((a[5], b[5]))
|
||||||
|
for a in texts:
|
||||||
|
for b in lines:
|
||||||
|
ax1, ay1, ax2, ay2 = text_box(a)
|
||||||
|
if not (ax2 <= b[1] or b[3] <= ax1 or ay2 <= b[2] or b[4] <= ay1):
|
||||||
|
hits.append((a[5], "<process line at %d,%d>" % (b[1], b[2])))
|
||||||
|
# things that stay put, which is what a moving mark must never reach
|
||||||
|
moving_at = {(round(c[1], 1), round(c[2], 1)) for c, _, _, _, _ in movers(xml)}
|
||||||
|
statics = [t for t in texts if (round(t[1], 1), round(t[2], 1)) not in moving_at]
|
||||||
|
swept = sweep(xml, statics + lines)
|
||||||
|
|
||||||
|
n = len(hits) + len(swept)
|
||||||
|
print("%-28s %3d components, %3d text, %2d moving"
|
||||||
|
% (path.name, len(comps), len(texts), len(moving_at)), end="")
|
||||||
|
if n:
|
||||||
|
print(" -> %d COLLISIONS" % n)
|
||||||
|
for a, b in hits:
|
||||||
|
print(" %-40s x %s" % (a[:40], b[:40]))
|
||||||
|
for (item, a, b), (lo, hi) in sorted(swept.items()):
|
||||||
|
print(" %-22s %-16s x %-22s at %g-%g%%"
|
||||||
|
% (item.replace("AID.WRPS.", ""), a[:16], b[:22], lo, hi))
|
||||||
|
else:
|
||||||
|
print(" -> clear")
|
||||||
|
return n
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
args = sys.argv[1:]
|
||||||
|
files = [Path(a) for a in args] if args else sorted((HERE / "out").glob("WRPS_*.xml"))
|
||||||
|
bad = sum(audit(f) for f in files if f.is_file())
|
||||||
|
sys.exit(1 if bad else 0)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
104
04-scada/hmi/component-kit.md
Normal file
104
04-scada/hmi/component-kit.md
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
# The WRPS component kit
|
||||||
|
|
||||||
|
The house style for this project's displays. AOG is the **reference** for how a
|
||||||
|
component is parameterised — it is not used directly, and only the components
|
||||||
|
this project needs exist.
|
||||||
|
|
||||||
|
Applies to this display and every future display in the project. A new
|
||||||
|
requirement adds a component here; it never adds a second style.
|
||||||
|
|
||||||
|
## Tokens
|
||||||
|
|
||||||
|
Named like AOG's `AOG_C*` so they map onto CI Server global parameters. A restyle
|
||||||
|
is an edit to this table and nothing else.
|
||||||
|
|
||||||
|
| Token | Value | Use |
|
||||||
|
|---|---|---|
|
||||||
|
| `WRPS_CBG` | `#EDF0F4` | display ground |
|
||||||
|
| `WRPS_CSF` | `#FFFFFF` | panel / surface |
|
||||||
|
| `WRPS_CSF2` | `#F5F8FB` | recessed surface, inside a value box |
|
||||||
|
| `WRPS_CBD` | `#C3CCD8` | border — heavier than a desktop design, for distance |
|
||||||
|
| `WRPS_CBD2` | `#DFE5EC` | hairline, internal dividers |
|
||||||
|
| `WRPS_CT1` | `#0E1621` | primary ink |
|
||||||
|
| `WRPS_CT2` | `#465468` | labels, secondary ink |
|
||||||
|
| `WRPS_CAC` | `#0B7DA8` | **interaction only** — selected, focus, faceplate rule |
|
||||||
|
| `WRPS_CAC2` | `#12A5D9` | accent bright — focus ring |
|
||||||
|
| `WRPS_CRUN` / bg | `#0F8A45` / `#E4F6EA` | running / on |
|
||||||
|
| `WRPS_CWRN` / bg | `#A85B00` / `#FFEFD2` | attention |
|
||||||
|
| `WRPS_CALM` / border | `#D32029` / `#96131A` | **alarm — always a filled block** |
|
||||||
|
| `WRPS_CPRC` | `#2B3F52` | pipe and vessel outline |
|
||||||
|
| `WRPS_CWTR` | `#9FCFE6` | liquid fill |
|
||||||
|
|
||||||
|
**Colour policy.** The plant is drawn in ink on white. Cyan means *you can act on
|
||||||
|
this*; green, amber and red mean *the plant is telling you something*. Nothing
|
||||||
|
else is coloured, so an abnormal state is the only colour on the screen.
|
||||||
|
|
||||||
|
**Abnormal is a filled block, never coloured text.** At distance, red text on
|
||||||
|
white reads as slightly darker grey; a filled red block with white text does not.
|
||||||
|
One fault propagates to every place it is relevant — banner, symbol, instrument
|
||||||
|
bubble, value box, faceplate badge — so it cannot be missed from any part of the
|
||||||
|
screen.
|
||||||
|
|
||||||
|
**Type.** Labels **Segoe UI**, values and tag names **Consolas**. Both ship with
|
||||||
|
Windows, so nothing depends on a font that might not be installed — but Segoe is
|
||||||
|
the modern face and Tahoma is what dates a screen. Consolas is monospaced, so
|
||||||
|
digits keep their column as values update at 100 ms, and using it for tag names
|
||||||
|
gives the display its technical read.
|
||||||
|
|
||||||
|
**Sized for a TV.** Nothing on the display is smaller than ~1.35% of screen
|
||||||
|
height (about 15 px at 1080p); the wet well level is the largest thing at 4.6%,
|
||||||
|
because it is the number a room actually watches. Ink is near-black on white and
|
||||||
|
borders are heavier than a desktop design would use — contrast is what survives a
|
||||||
|
bright room and a projector.
|
||||||
|
|
||||||
|
**Why it does not look like AOG.** What dates AOG is the bevelled 3D artwork and
|
||||||
|
mid-grey ground, not the grey itself. This kit is flat, square-cornered, hairline
|
||||||
|
bordered, white on cool neutral.
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
Every one takes the AOG-shaped parameter set where it applies: `item`,
|
||||||
|
`setPointItem`, `tagName`, `format`, `showAlarmIndicator`, `selectable`,
|
||||||
|
`displayToActivate`.
|
||||||
|
|
||||||
|
| Component | Purpose | Key parameters | States |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `WRPS_Display` | The 16:9 shell: ground, title bar, body, simulation band | — | — |
|
||||||
|
| `WRPS_Title` | Display header — name, drawing ref, station states right | `title`, `subtitle` | — |
|
||||||
|
| `WRPS_Panel` | Every framed region. Panels never nest | `header` | — |
|
||||||
|
| `WRPS_Value` | **Every** analog value | `item`, `tagName`, `format`, `units`, `size`, `displayToActivate` | normal · large · warn · alarm |
|
||||||
|
| `WRPS_State` | **Every** discrete status | `item`, `onText`, `offText`, `inverse`, `severity` | off · on · warn · alarm |
|
||||||
|
| `WRPS_Button` | **Every** command and mode selection | `label`, `item`, `writeValue`, `pressed`, `variant` | normal · pressed · destructive |
|
||||||
|
| `WRPS_Pump` | Pump symbol, circle with impeller cross | `item`, `tagName`, `displayToActivate` | stopped · running · tripped |
|
||||||
|
| `WRPS_Valve` | Swing check and manual isolation, from the P&ID legend | `kind` | static |
|
||||||
|
| `WRPS_Instrument` | ISA bubble, function letters over loop number | `letters`, `loop`, `item` | static · with value |
|
||||||
|
| `WRPS_Vessel` | Wet well: outline, liquid fill, setpoint ladder at true heights | `levelItem`, `setpointItems`, `range` | — |
|
||||||
|
| `WRPS_Faceplate` | Popup display frame — cyan rule, kit components inside | `title`, `subtitle`, `item` | — |
|
||||||
|
| `WRPS_AlarmBanner` | Present **only** when something is abnormal; pushes the display down | `alarmList`, `count` | hidden · active · acknowledged |
|
||||||
|
|
||||||
|
`WRPS_SimBand` is **not** another component: it is `WRPS_Button` and `WRPS_Value`
|
||||||
|
inside a fenced region with a cyan rule and a standing label, so the simulation
|
||||||
|
controls can never be mistaken for plant control.
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
1. **No hand-drawn values or buttons.** If it shows a number it is `WRPS_Value`;
|
||||||
|
if it is clickable it is `WRPS_Button`. This is what stops displays drifting.
|
||||||
|
2. **Faceplates are separate displays** opened via `displayToActivate`, matching
|
||||||
|
how AOG works — not overlay groups on the parent display.
|
||||||
|
3. **Setpoint edits go through a numeric entry popup**, not an inline field.
|
||||||
|
4. **State is never colour alone.** `WRPS_State` carries a square marker and its
|
||||||
|
own text, so it survives a projector or a colour-blind operator.
|
||||||
|
5. **Simulation controls stay in the band.** They never appear beside plant
|
||||||
|
controls.
|
||||||
|
6. **An abnormal condition propagates everywhere it is relevant** — banner, plant
|
||||||
|
symbol, instrument bubble, value box, faceplate badge. One fault, five places.
|
||||||
|
7. **Blink is reserved for unacknowledged alarms**, and only on the tag name, not
|
||||||
|
whole regions. CI Server supports this natively (`blinkEnabled`, `blinkDelay`,
|
||||||
|
700 ms in the reference screens).
|
||||||
|
8. **Green is only for genuinely-running equipment.** If normal operation is
|
||||||
|
colourful, red has competition and stops being an alarm.
|
||||||
|
|
||||||
|
## Geometry
|
||||||
|
|
||||||
|
16:9. Laid out on the editor's 4 px snap grid; 1920 × 1080 unless told otherwise.
|
||||||
50
04-scada/hmi/item-ids.csv
Normal file
50
04-scada/hmi/item-ids.csv
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
item,itemId
|
||||||
|
AID.WRPS.PU301.AVAILABLE,1.1.291.0.0.1.1.1
|
||||||
|
AID.WRPS.PU301.RUNNING,1.1.288.0.0.1.1.1
|
||||||
|
AID.WRPS.PU301.RUN_CMD,1.1.285.0.0.1.1.1
|
||||||
|
AID.WRPS.PU301.RUN_HOURS,1.1.308.0.0.1.1.7
|
||||||
|
AID.WRPS.PU301.STATE,1.1.313.0.0.1.1.7
|
||||||
|
AID.WRPS.PU301.TRIPPED,1.1.297.0.0.1.1.1
|
||||||
|
AID.WRPS.PU302.AVAILABLE,1.1.292.0.0.1.1.1
|
||||||
|
AID.WRPS.PU302.RUNNING,1.1.289.0.0.1.1.1
|
||||||
|
AID.WRPS.PU302.RUN_CMD,1.1.286.0.0.1.1.1
|
||||||
|
AID.WRPS.PU302.RUN_HOURS,1.1.309.0.0.1.1.7
|
||||||
|
AID.WRPS.PU302.STATE,1.1.314.0.0.1.1.7
|
||||||
|
AID.WRPS.PU302.TRIPPED,1.1.298.0.0.1.1.1
|
||||||
|
AID.WRPS.PU303.AVAILABLE,1.1.293.0.0.1.1.1
|
||||||
|
AID.WRPS.PU303.RUNNING,1.1.290.0.0.1.1.1
|
||||||
|
AID.WRPS.PU303.RUN_CMD,1.1.287.0.0.1.1.1
|
||||||
|
AID.WRPS.PU303.RUN_HOURS,1.1.310.0.0.1.1.7
|
||||||
|
AID.WRPS.PU303.STATE,1.1.315.0.0.1.1.7
|
||||||
|
AID.WRPS.PU303.TRIPPED,1.1.299.0.0.1.1.1
|
||||||
|
AID.WRPS.SIM.INFLOW,1.1.330.0.0.1.1.7
|
||||||
|
AID.WRPS.SIM.RESET,1.1.332.0.0.1.1.7
|
||||||
|
AID.WRPS.SIM.SCENARIO,1.1.331.0.0.1.1.7
|
||||||
|
AID.WRPS.SIM.TIME_SCALE,1.1.333.0.0.1.1.7
|
||||||
|
AID.WRPS.SP.CMD_PARAM,1.1.321.0.0.1.1.7
|
||||||
|
AID.WRPS.SP.CMD_WORD,1.1.320.0.0.1.1.7
|
||||||
|
AID.WRPS.SP.HIGH_ALARM,1.1.327.0.0.1.1.7
|
||||||
|
AID.WRPS.SP.LEVEL_SP,1.1.322.0.0.1.1.7
|
||||||
|
AID.WRPS.SP.MIN_SPEED,1.1.328.0.0.1.1.7
|
||||||
|
AID.WRPS.SP.MODE,1.1.319.0.0.1.1.7
|
||||||
|
AID.WRPS.SP.SERVICE_HRS,1.1.329.0.0.1.1.7
|
||||||
|
AID.WRPS.SP.START_DUTY,1.1.323.0.0.1.1.7
|
||||||
|
AID.WRPS.SP.START_P2,1.1.324.0.0.1.1.7
|
||||||
|
AID.WRPS.SP.START_P3,1.1.325.0.0.1.1.7
|
||||||
|
AID.WRPS.SP.STOP_ALL,1.1.326.0.0.1.1.7
|
||||||
|
AID.WRPS.STN.ALARM_WORD,1.1.317.0.0.1.1.7
|
||||||
|
AID.WRPS.STN.CMD_ACK,1.1.318.0.0.1.1.7
|
||||||
|
AID.WRPS.STN.DISCHARGE,1.1.302.0.0.1.1.7
|
||||||
|
AID.WRPS.STN.DUTY_PUMP,1.1.316.0.0.1.1.7
|
||||||
|
AID.WRPS.STN.HIGH_LEVEL,1.1.295.0.0.1.1.1
|
||||||
|
AID.WRPS.STN.INFLOW,1.1.301.0.0.1.1.7
|
||||||
|
AID.WRPS.STN.IN_AUTO,1.1.294.0.0.1.1.1
|
||||||
|
AID.WRPS.STN.LEVEL,1.1.300.0.0.1.1.7
|
||||||
|
AID.WRPS.STN.NET_ACCUM,1.1.307.0.0.1.1.7
|
||||||
|
AID.WRPS.STN.PUMPS_RUNNING,1.1.303.0.0.1.1.7
|
||||||
|
AID.WRPS.STN.SPEED,1.1.304.0.0.1.1.7
|
||||||
|
AID.WRPS.STN.SPILL_ACTIVE,1.1.296.0.0.1.1.1
|
||||||
|
AID.WRPS.STN.STATE,1.1.312.0.0.1.1.7
|
||||||
|
AID.WRPS.STN.TIME_TO_LSHH,1.1.306.0.0.1.1.7
|
||||||
|
AID.WRPS.STN.TIME_TO_SPILL,1.1.305.0.0.1.1.7
|
||||||
|
AID.WRPS.STN.VOL_TO_SPILL,1.1.311.0.0.1.1.7
|
||||||
|
8
04-scada/hmi/item-ids.meta.json
Normal file
8
04-scada/hmi/item-ids.meta.json
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"harvested_from": "<scratch copy of a saved WRPS_TagTest.xml>",
|
||||||
|
"harvested_at": "2026-08-14T16:50:54",
|
||||||
|
"item_file": "wrps_item_df.qli",
|
||||||
|
"item_file_sha": "326d3bc8e525cebf0d3eba0df73b10b487f11b43b9459e2438e370f605a15bd1",
|
||||||
|
"measured": 49,
|
||||||
|
"note": "Every .qli item import renumbers every item. Validate the displays in CI Server's Editor Module, then re-harvest - otherwise the screens go dead."
|
||||||
|
}
|
||||||
126
04-scada/hmi/point_format.py
Normal file
126
04-scada/hmi/point_format.py
Normal file
|
|
@ -0,0 +1,126 @@
|
||||||
|
#!/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))
|
||||||
319
04-scada/hmi/render_preview.py
Normal file
319
04-scada/hmi/render_preview.py
Normal file
|
|
@ -0,0 +1,319 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Render built CI Server display XML back to a browser preview.
|
||||||
|
|
||||||
|
python render_preview.py -> out/preview.html (all displays)
|
||||||
|
python render_preview.py WRPS_FP_Pump -> just that one
|
||||||
|
|
||||||
|
WHY THIS EXISTS
|
||||||
|
---------------
|
||||||
|
Every preview up to now was a drawing of what I *meant* to build, made
|
||||||
|
alongside the generator rather than from it. That is the same mistake as
|
||||||
|
a test that checks the intent instead of the artefact, and it has already
|
||||||
|
cost this project a round: a script that failed before writing its file,
|
||||||
|
while a checker with hard-coded coordinates cheerfully reported the file
|
||||||
|
was fine.
|
||||||
|
|
||||||
|
This reads the XML that was actually generated and draws it. If a
|
||||||
|
component is in the file it appears here; if it is not, it does not.
|
||||||
|
|
||||||
|
It also resolves what a static picture normally cannot:
|
||||||
|
|
||||||
|
live values read over Modbus, formatted through the point's own
|
||||||
|
gain and display mask, so a number shows what the
|
||||||
|
operator would see rather than "9999.9"
|
||||||
|
visibility a component bound to a Boolean item is drawn only if
|
||||||
|
that item is true; one bound to a display parameter is
|
||||||
|
resolved through the genericFunctions equality that
|
||||||
|
writes the parameter. Without this, every state stack
|
||||||
|
renders at once and the preview is a smear.
|
||||||
|
|
||||||
|
Approximate by construction: browser font metrics are not Java's, so
|
||||||
|
text width is estimated. It is a faithful reading of the geometry, not
|
||||||
|
a pixel-exact simulation of CI View.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import csv
|
||||||
|
import html
|
||||||
|
import importlib.util
|
||||||
|
import math
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
HERE = Path(__file__).resolve().parent
|
||||||
|
OUT = HERE / "out"
|
||||||
|
POINTS = HERE.parent / "modbus_points" / "scada-points.csv"
|
||||||
|
QLI_GEN = HERE.parent / "modbus_points" / "gen_ciserver_qli.py"
|
||||||
|
PLC_HOST, PLC_PORT, PLC_UNIT = "192.168.153.192", 502, 1
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ data
|
||||||
|
def leaf_to_iec():
|
||||||
|
spec = importlib.util.spec_from_file_location("q", QLI_GEN)
|
||||||
|
q = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(q)
|
||||||
|
return {"AID.WRPS.%s.%s" % v: k for k, v in q.LEAF.items()}
|
||||||
|
|
||||||
|
|
||||||
|
def live_values():
|
||||||
|
"""{itemName: (raw, formatted_string)} - or {} if the PLC is unreachable."""
|
||||||
|
rows = {r["iec_address"]: r for r in csv.DictReader(POINTS.open(newline="", encoding="utf-8"))}
|
||||||
|
names = leaf_to_iec()
|
||||||
|
try:
|
||||||
|
from pymodbus.client import ModbusTcpClient
|
||||||
|
c = ModbusTcpClient(PLC_HOST, port=PLC_PORT, timeout=3)
|
||||||
|
if not c.connect():
|
||||||
|
return {}
|
||||||
|
hr = c.read_holding_registers(address=0, count=21, device_id=PLC_UNIT).registers
|
||||||
|
sp = c.read_holding_registers(address=1024, count=11, device_id=PLC_UNIT).registers
|
||||||
|
sim = c.read_holding_registers(address=1044, count=4, device_id=PLC_UNIT).registers
|
||||||
|
co = [int(b) for b in c.read_coils(address=0, count=15, device_id=PLC_UNIT).bits[:15]]
|
||||||
|
c.close()
|
||||||
|
except Exception:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
raw = {"%%QW%d" % i: v for i, v in enumerate(hr)}
|
||||||
|
raw.update({"%%MW%d" % i: v for i, v in enumerate(sp)})
|
||||||
|
raw.update({"%%MW%d" % (20 + i): v for i, v in enumerate(sim)})
|
||||||
|
raw.update({"%%QX0.%d" % i: co[i] for i in range(8)})
|
||||||
|
raw.update({"%%QX1.%d" % i: co[8 + i] for i in range(7)})
|
||||||
|
|
||||||
|
out = {}
|
||||||
|
for name, iec in names.items():
|
||||||
|
if iec not in raw or iec not in rows:
|
||||||
|
continue
|
||||||
|
r, v = rows[iec], raw[iec]
|
||||||
|
if r["data_type"] == "Boolean":
|
||||||
|
out[name] = (v, str(v))
|
||||||
|
continue
|
||||||
|
if v > 32767 and iec != "%QW17":
|
||||||
|
v -= 65536
|
||||||
|
dec = len(r["format_mask"].split(".")[1]) if "." in r["format_mask"] else 0
|
||||||
|
out[name] = (v, "%.*f" % (dec, v * float(r["eng_gain"])))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ parse
|
||||||
|
def num(body, tag, default=None):
|
||||||
|
m = re.search(r"<%s>(-?[\d.]+)</%s>" % (tag, tag), body)
|
||||||
|
return float(m.group(1)) if m else default
|
||||||
|
|
||||||
|
|
||||||
|
def attr(tag_text, name, default=None):
|
||||||
|
m = re.search(r'%s="(-?[\d.]+)"' % name, tag_text)
|
||||||
|
return float(m.group(1)) if m else default
|
||||||
|
|
||||||
|
|
||||||
|
class Doc:
|
||||||
|
def __init__(self, path):
|
||||||
|
self.xml = path.read_text(encoding="utf-8")
|
||||||
|
self.name = path.stem
|
||||||
|
self.w = int(num(self.xml, "width", 1920) or 1920)
|
||||||
|
self.h = int(num(self.xml, "height", 1080) or 1080)
|
||||||
|
self.paints, self.datas = {}, {}
|
||||||
|
for rid, body in re.findall(r'<intRef id="(\d+)">(.*?)</intRef>', self.xml, re.S):
|
||||||
|
p = re.search(r'colorSet r="(\d+)" g="(\d+)" b="(\d+)"', body)
|
||||||
|
if p:
|
||||||
|
self.paints[rid] = "rgb(%s,%s,%s)" % p.groups()
|
||||||
|
else:
|
||||||
|
self.datas[rid] = body
|
||||||
|
# genericFunctions: parameter name -> (item, constant)
|
||||||
|
self.funcs = {}
|
||||||
|
for body in re.findall(r"<genericFunctions type=\"componentData\">(.*?)</genericFunctions>",
|
||||||
|
self.xml, re.S):
|
||||||
|
pname = re.search(r"<name>([^<]+)</name>", body)
|
||||||
|
const = num(body, "numberArgument2")
|
||||||
|
item = re.search(r"<itemName>([^<]+)</itemName>", body)
|
||||||
|
if pname and const is not None and item:
|
||||||
|
self.funcs[pname.group(1)] = (item.group(1), const)
|
||||||
|
|
||||||
|
def paint(self, body, tag):
|
||||||
|
m = re.search(r'<%s ref="(\d+)"/>' % tag, body)
|
||||||
|
if m:
|
||||||
|
return self.paints.get(m.group(1), "none")
|
||||||
|
m = re.search(r'<%s type="paint"><paint type="colorSet" r="(\d+)" g="(\d+)" b="(\d+)"' % tag, body)
|
||||||
|
return "rgb(%s,%s,%s)" % m.groups() if m else "none"
|
||||||
|
|
||||||
|
def bound_item(self, body):
|
||||||
|
m = re.search(r"<itemName>([^<]+)</itemName>", body)
|
||||||
|
if m:
|
||||||
|
return m.group(1)
|
||||||
|
m = re.search(r'<data ref="(\d+)"/>', body)
|
||||||
|
if m:
|
||||||
|
d = self.datas.get(m.group(1), "")
|
||||||
|
mm = re.search(r"<itemName>([^<]+)</itemName>", d)
|
||||||
|
if mm:
|
||||||
|
return mm.group(1)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def visible(self, body, live):
|
||||||
|
"""Resolve a .visible binding. Unbound components are always drawn."""
|
||||||
|
refs = re.findall(r'<data ref="(\d+)"/>', body)
|
||||||
|
blocks = [self.datas.get(r, "") for r in refs] + [body]
|
||||||
|
for blk in blocks:
|
||||||
|
if ".visible" not in blk:
|
||||||
|
continue
|
||||||
|
p = re.search(r"<parameter>([^<]+)</parameter>", blk)
|
||||||
|
if p:
|
||||||
|
item, const = self.funcs.get(p.group(1), (None, None))
|
||||||
|
if item is None or item not in live:
|
||||||
|
return None
|
||||||
|
return abs(live[item][0] - const) < 0.001
|
||||||
|
i = re.search(r"<itemName>([^<]+)</itemName>", blk)
|
||||||
|
if i:
|
||||||
|
if i.group(1) not in live:
|
||||||
|
return None
|
||||||
|
return bool(live[i.group(1)][0])
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def render(doc, live):
|
||||||
|
"""SVG for one display, components in document order."""
|
||||||
|
svg = ['<svg viewBox="0 0 %d %d" width="%d" height="%d" xmlns="http://www.w3.org/2000/svg">'
|
||||||
|
% (doc.w, doc.h, doc.w, doc.h),
|
||||||
|
'<rect width="%d" height="%d" fill="rgb(237,240,244)"/>' % (doc.w, doc.h)]
|
||||||
|
unknown = 0
|
||||||
|
|
||||||
|
pattern = (r'<(rectangle|ellipse|line|text|number|numberField|dataBar|button|microTrend)'
|
||||||
|
r'( type="componentData"[^>]*)>(.*?)</\1>')
|
||||||
|
for kind, head, body in re.findall(pattern, doc.xml, re.S):
|
||||||
|
vis = doc.visible(body, live)
|
||||||
|
if vis is False:
|
||||||
|
continue
|
||||||
|
if vis is None:
|
||||||
|
unknown += 1
|
||||||
|
op = ' opacity="0.35"' if vis is None else ""
|
||||||
|
|
||||||
|
# geometry: child elements, or attributes on the tag (numberField/dataBar/button)
|
||||||
|
cx = num(body, "x", attr(head, "x"))
|
||||||
|
cy = num(body, "y", attr(head, "y"))
|
||||||
|
if cx is None or cy is None:
|
||||||
|
continue
|
||||||
|
t = num(body, "top", attr(head, "top", 0)) or 0
|
||||||
|
b = num(body, "bottom", attr(head, "bottom", 0)) or 0
|
||||||
|
l = num(body, "left", attr(head, "left", 0)) or 0
|
||||||
|
r = num(body, "right", attr(head, "right", 0)) or 0
|
||||||
|
x, y, w, h = cx - l, cy - t, l + r, t + b
|
||||||
|
|
||||||
|
if kind == "rectangle":
|
||||||
|
svg.append('<rect x="%.1f" y="%.1f" width="%.1f" height="%.1f" fill="%s" stroke="%s" stroke-width="%.1f"%s/>'
|
||||||
|
% (x, y, w, h, doc.paint(body, "fillPaint"), doc.paint(body, "strokePaint"),
|
||||||
|
num(body, "width", 1) or attr(body, "width", 1) or 1, op))
|
||||||
|
elif kind == "ellipse":
|
||||||
|
svg.append('<ellipse cx="%.1f" cy="%.1f" rx="%.1f" ry="%.1f" fill="%s" stroke="%s" stroke-width="2"%s/>'
|
||||||
|
% (cx, cy, l, t, doc.paint(body, "fillPaint"), doc.paint(body, "strokePaint"), op))
|
||||||
|
elif kind == "line":
|
||||||
|
pts = re.findall(r"<point><x>(-?[\d.]+)</x><y>(-?[\d.]+)</y></point>", body)
|
||||||
|
if len(pts) >= 2:
|
||||||
|
svg.append('<polyline points="%s" fill="none" stroke="%s" stroke-width="%.1f"%s/>'
|
||||||
|
% (" ".join("%s,%s" % p for p in pts), doc.paint(body, "strokePaint"),
|
||||||
|
float(re.search(r'stroke" width="([\d.]+)"', body).group(1)) if re.search(r'stroke" width="([\d.]+)"', body) else 2, op))
|
||||||
|
else:
|
||||||
|
svg.append('<line x1="%.1f" y1="%.1f" x2="%.1f" y2="%.1f" stroke="%s" stroke-width="2"%s/>'
|
||||||
|
% (x, y, x + w, y + h, doc.paint(body, "strokePaint"), op))
|
||||||
|
elif kind == "text":
|
||||||
|
v = re.search(r"<value>(.*?)</value>", body, re.S)
|
||||||
|
f = re.search(r'name="([^"]+)" bold="(\w+)"[^>]*size="(\d+)"', body)
|
||||||
|
fam, bold, size = (f.group(1), f.group(2), int(f.group(3))) if f else ("Segoe UI", "false", 13)
|
||||||
|
svg.append('<text x="%.1f" y="%.1f" text-anchor="middle" font-family="%s" font-size="%d" '
|
||||||
|
'font-weight="%s" fill="%s"%s>%s</text>'
|
||||||
|
% (cx, cy + size * 0.35, fam, size, "700" if bold == "true" else "400",
|
||||||
|
doc.paint(body, "fillPaint"), op, html.escape(v.group(1) if v else "")))
|
||||||
|
elif kind == "number":
|
||||||
|
item = doc.bound_item(body)
|
||||||
|
mask = re.search(r"<format>([^<]*)</format>", body)
|
||||||
|
shown = live.get(item, (None, mask.group(1) if mask else "?"))[1]
|
||||||
|
f = re.search(r'size="(\d+)"', body)
|
||||||
|
size = int(f.group(1)) if f else 20
|
||||||
|
svg.append('<text x="%.1f" y="%.1f" text-anchor="middle" font-family="Consolas,monospace" '
|
||||||
|
'font-size="%d" font-weight="700" fill="%s"%s>%s</text>'
|
||||||
|
% (cx, cy + size * 0.35, size, doc.paint(body, "fillPaint"), op,
|
||||||
|
html.escape(str(shown))))
|
||||||
|
elif kind == "numberField":
|
||||||
|
item = doc.bound_item(body)
|
||||||
|
shown = live.get(item, (None, ""))[1]
|
||||||
|
svg.append('<rect x="%.1f" y="%.1f" width="%.1f" height="%.1f" fill="#fff" stroke="rgb(11,125,168)"%s/>'
|
||||||
|
% (x, y, w, h, op))
|
||||||
|
svg.append('<text x="%.1f" y="%.1f" font-family="Consolas,monospace" font-size="14" fill="#0e1621"%s>%s</text>'
|
||||||
|
% (x + 8, cy + 5, op, html.escape(str(shown))))
|
||||||
|
elif kind == "dataBar":
|
||||||
|
item = doc.bound_item(body)
|
||||||
|
lo = num(body, "lowLimit", 0) or 0
|
||||||
|
hi = num(body, "highLimit", 100) or 100
|
||||||
|
val = live.get(item, (lo, ""))[0]
|
||||||
|
try:
|
||||||
|
pct = max(0.0, min(1.0, (float(live[item][1]) - lo) / (hi - lo)))
|
||||||
|
except Exception:
|
||||||
|
pct = 0.0
|
||||||
|
svg.append('<rect x="%.1f" y="%.1f" width="%.1f" height="%.1f" fill="%s"%s/>'
|
||||||
|
% (x, y, w, h, doc.paint(body, "background"), op))
|
||||||
|
svg.append('<rect x="%.1f" y="%.1f" width="%.1f" height="%.1f" fill="%s"%s/>'
|
||||||
|
% (x, y + h * (1 - pct), w, h * pct, doc.paint(body, "foreground"), op))
|
||||||
|
elif kind == "microTrend":
|
||||||
|
# the trace itself is CI Server's; what the preview has to show is
|
||||||
|
# the FOOTPRINT, so a trend that lands on a pipe is visible here
|
||||||
|
# rather than after deployment
|
||||||
|
item = doc.bound_item(body)
|
||||||
|
hi = num(body, "value", 100) or 100 # scaleMax[0]
|
||||||
|
svg.append('<rect x="%.1f" y="%.1f" width="%.1f" height="%.1f" fill="rgb(220,220,220)" '
|
||||||
|
'stroke="rgb(195,204,216)"%s/>' % (x, y, w, h, op))
|
||||||
|
try:
|
||||||
|
pct = max(0.0, min(1.0, float(live[item][1]) / hi))
|
||||||
|
except Exception:
|
||||||
|
pct = 0.5
|
||||||
|
svg.append('<polyline points="%s" fill="none" stroke="rgb(11,125,168)" stroke-width="1.5"%s/>'
|
||||||
|
% (" ".join("%.1f,%.1f" % (x + w * k / 24.0,
|
||||||
|
y + h * (1 - pct * (0.55 + 0.45 * math.sin(k / 2.4))))
|
||||||
|
for k in range(25)), op))
|
||||||
|
svg.append('<text x="%.1f" y="%.1f" font-family="Consolas,monospace" font-size="9" '
|
||||||
|
'fill="rgb(70,84,104)"%s>%s</text>'
|
||||||
|
% (x + 4, y + 11, op, html.escape((item or "").replace("AID.WRPS.", ""))))
|
||||||
|
elif kind == "button":
|
||||||
|
lab = re.search(r"<label>([^<]*)</label>", body)
|
||||||
|
fg = re.search(r'foregroundColor type="colorSet" r="(\d+)" g="(\d+)" b="(\d+)"', body)
|
||||||
|
bg = re.search(r'backgroundColor type="colorSet" r="(\d+)" g="(\d+)" b="(\d+)"', body)
|
||||||
|
svg.append('<rect x="%.1f" y="%.1f" width="%.1f" height="%.1f" fill="%s" stroke="rgb(195,204,216)"%s/>'
|
||||||
|
% (x, y, w, h, "rgb(%s,%s,%s)" % bg.groups() if bg else "#fff", op))
|
||||||
|
svg.append('<text x="%.1f" y="%.1f" text-anchor="middle" font-family="Segoe UI" font-size="15" '
|
||||||
|
'font-weight="700" fill="%s"%s>%s</text>'
|
||||||
|
% (cx, cy + 5, "rgb(%s,%s,%s)" % fg.groups() if fg else "#0e1621", op,
|
||||||
|
html.escape(lab.group(1) if lab else "")))
|
||||||
|
svg.append("</svg>")
|
||||||
|
return "\n".join(svg), unknown
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
want = sys.argv[1:]
|
||||||
|
files = sorted(OUT.glob("WRPS_*.xml"))
|
||||||
|
if want:
|
||||||
|
files = [f for f in files if f.stem in want]
|
||||||
|
if not files:
|
||||||
|
sys.exit("nothing to render in %s" % OUT)
|
||||||
|
|
||||||
|
live = live_values()
|
||||||
|
parts = ['<!doctype html><meta charset="utf-8"><title>WRPS displays — rendered from the built XML</title>',
|
||||||
|
'<style>body{margin:0;background:#eef1f5;font-family:"Segoe UI",sans-serif;color:#0e1621}'
|
||||||
|
'.w{padding:20px}h1{font-size:15px;margin:0 0 2px}p.s{font-size:12px;color:#465468;margin:0 0 16px}'
|
||||||
|
'h2{font-size:13px;margin:22px 0 6px}.box{background:#fff;border:1px solid #c3ccd8;overflow:auto;display:inline-block}'
|
||||||
|
'</style><div class="w">',
|
||||||
|
"<h1>Rendered from the generated XML in <code>out/</code></h1>",
|
||||||
|
'<p class="s">%s. Components are drawn only if the file contains them; '
|
||||||
|
"values and visibility resolved against the live PLC.</p>"
|
||||||
|
% ("Live values read from the PLC" if live else
|
||||||
|
"PLC unreachable - numbers show their format mask, state-bound shapes are dimmed")]
|
||||||
|
for f in files:
|
||||||
|
doc = Doc(f)
|
||||||
|
svg, unknown = render(doc, live)
|
||||||
|
note = " <span style='color:#b45309'>%d component(s) whose visibility could not be resolved, shown dimmed</span>" % unknown if unknown else ""
|
||||||
|
parts.append("<h2>%s <span style='font-weight:400;color:#465468'>%d x %d</span>%s</h2>"
|
||||||
|
% (doc.name, doc.w, doc.h, note))
|
||||||
|
parts.append('<div class="box">%s</div>' % svg)
|
||||||
|
print("rendered %-22s %d x %d" % (doc.name, doc.w, doc.h))
|
||||||
|
parts.append("</div>")
|
||||||
|
dest = OUT / "preview.html"
|
||||||
|
dest.write_text("\n".join(parts), encoding="utf-8")
|
||||||
|
print("wrote %s" % dest)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
222
04-scada/modbus_points/README.md
Normal file
222
04-scada/modbus_points/README.md
Normal file
|
|
@ -0,0 +1,222 @@
|
||||||
|
# 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`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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.csv` → `scada-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
|
||||||
|
3–166 and sections 1–170, so 171 is the first free id:
|
||||||
|
|
||||||
|
| | NSIDs |
|
||||||
|
|---|---|
|
||||||
|
| Sections `STN`, `PU301`, `PU302`, `PU303`, `SP`, `SIM` | 171–176 |
|
||||||
|
| The 49 items | 177–225 |
|
||||||
|
|
||||||
|
`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-lin001` — **`10.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 | 0–14 | 15 | Run commands, running, available, in-auto, alarms, trips |
|
||||||
|
| `PS_PUBLISHED` | FC03 | 0–20 | 19 | Every live measurement and station/pump state |
|
||||||
|
| `PS_SETPOINTS` | FC03/06 | 1024–1034 | 11 | Mode, command word/param, level setpoints, limits |
|
||||||
|
| `PS_SIM_CONTROL` | FC03/06 | 1044–1047 | 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 0–1023 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, 30–60 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_NUMBER`s, 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.
|
||||||
416
04-scada/modbus_points/gen_ciserver_qli.py
Normal file
416
04-scada/modbus_points/gen_ciserver_qli.py
Normal file
|
|
@ -0,0 +1,416 @@
|
||||||
|
#!/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_ciserver_qli.py
|
||||||
|
|
||||||
|
Writes, alongside this script:
|
||||||
|
|
||||||
|
wrps_section_df.qli @SECTION_DF - the AID.WRPS.* sections
|
||||||
|
wrps_modbus_point_df.qli @MODBUS_POINT_DF - the Modbus point definitions
|
||||||
|
wrps_item_df.qli @ITEM_DF - the items in those sections
|
||||||
|
|
||||||
|
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 ->
|
||||||
|
these files, so a register change propagates by re-running the three
|
||||||
|
scripts rather than by hand-editing anything.
|
||||||
|
|
||||||
|
Field layouts and every constant here were copied from the user's own
|
||||||
|
exports in 99-reference/ciserver-qli-exports/ (section_df.qli,
|
||||||
|
modbus_point_df.qli, item_df.qli), not invented.
|
||||||
|
|
||||||
|
NSIDs
|
||||||
|
-----
|
||||||
|
The NSID space is shared by sections and items. In the reference
|
||||||
|
exports items occupy 3-166 and sections 1-170, so 171 is the first free
|
||||||
|
id: sections take 171-176 and items 177 onward (--nsid-base).
|
||||||
|
|
||||||
|
AID (169) and AID.WRPS (170) already exist, so this script emits only
|
||||||
|
their six children and hangs them off 170 (--parent-nsid). Pass
|
||||||
|
--include-root to emit AID and AID.WRPS as well, for a system that does
|
||||||
|
not have them yet.
|
||||||
|
|
||||||
|
THE ONE THING TO VERIFY ON A SMALL TEST IMPORT
|
||||||
|
----------------------------------------------
|
||||||
|
**Address base** (--address-base, default 1). The reference export maps
|
||||||
|
point AI_01 to IO_ADDRESS "RI:01". Whether CI Server's 01 means
|
||||||
|
protocol address 0 or 1 cannot be told from the file. Default here is
|
||||||
|
1-based, i.e. IO_ADDRESS = modbus address + 1, so holding register 0
|
||||||
|
(%QW0, wet well level) becomes "RO:01". If the level shows up where the
|
||||||
|
inflow should be, it is 0-based: re-run with --address-base 0.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import csv
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
HERE = Path(__file__).resolve().parent
|
||||||
|
POINTS = HERE / "scada-points.csv"
|
||||||
|
|
||||||
|
STATION = "WRPS_PLC"
|
||||||
|
ROOT = "AID.WRPS"
|
||||||
|
|
||||||
|
# IO_ADDRESS prefixes, from the reference export:
|
||||||
|
# RI = register input (FC04) RO = register output (FC03/06)
|
||||||
|
# DI = digital input (FC02) DO = digital output (FC01)
|
||||||
|
PREFIX = {"FC01": "DO", "FC02": "DI", "FC03": "RO", "FC03/FC06": "RO", "FC04": "RI"}
|
||||||
|
|
||||||
|
# Section per poll group; PS_PUBLISHED and PS_STATUS_BITS are split by
|
||||||
|
# equipment so each pump gets its own folder.
|
||||||
|
GROUP_SECTION = {"PS_SETPOINTS": "SP", "PS_SIM_CONTROL": "SIM"}
|
||||||
|
|
||||||
|
SECTION_ORDER = ["STN", "PU301", "PU302", "PU303", "SP", "SIM"]
|
||||||
|
|
||||||
|
SECTION_DESC = {
|
||||||
|
"STN": "Waterloo Road PS - station wide measurements and status",
|
||||||
|
"PU301": "Pump PU-301",
|
||||||
|
"PU302": "Pump PU-302",
|
||||||
|
"PU303": "Pump PU-303",
|
||||||
|
"SP": "Operator setpoints and commands",
|
||||||
|
"SIM": "Simulation control - simulation build only",
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION_FIELDS = [
|
||||||
|
"NAME", "SECTION_PATH", "SECTION_NAME", "BLOCKED", "PARENT_BLOCKED",
|
||||||
|
"ALARM_INHIBIT", "PARENT_ALARM_INHIBIT", "OPC_VISIBLE", "PARENT_OPC_VISIBLE", "NSID", "PARENT_NSID",
|
||||||
|
"DESCRIPTION", "CREATED_BY",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Short, stable leaf names. Keyed on IEC address so they never drift
|
||||||
|
# with a description edit.
|
||||||
|
LEAF = {
|
||||||
|
"%QX0.0": ("PU301", "RUN_CMD"), "%QX0.1": ("PU302", "RUN_CMD"),
|
||||||
|
"%QX0.2": ("PU303", "RUN_CMD"), "%QX0.3": ("PU301", "RUNNING"),
|
||||||
|
"%QX0.4": ("PU302", "RUNNING"), "%QX0.5": ("PU303", "RUNNING"),
|
||||||
|
"%QX0.6": ("PU301", "AVAILABLE"), "%QX0.7": ("PU302", "AVAILABLE"),
|
||||||
|
"%QX1.0": ("PU303", "AVAILABLE"), "%QX1.1": ("STN", "IN_AUTO"),
|
||||||
|
"%QX1.2": ("STN", "HIGH_LEVEL"), "%QX1.3": ("STN", "SPILL_ACTIVE"),
|
||||||
|
"%QX1.4": ("PU301", "TRIPPED"), "%QX1.5": ("PU302", "TRIPPED"),
|
||||||
|
"%QX1.6": ("PU303", "TRIPPED"),
|
||||||
|
"%QW0": ("STN", "LEVEL"), "%QW1": ("STN", "INFLOW"),
|
||||||
|
"%QW2": ("STN", "DISCHARGE"), "%QW3": ("STN", "PUMPS_RUNNING"),
|
||||||
|
"%QW4": ("STN", "SPEED"), "%QW5": ("STN", "TIME_TO_SPILL"),
|
||||||
|
"%QW6": ("STN", "TIME_TO_LSHH"), "%QW7": ("STN", "NET_ACCUM"),
|
||||||
|
"%QW8": ("PU301", "RUN_HOURS"), "%QW9": ("PU302", "RUN_HOURS"),
|
||||||
|
"%QW10": ("PU303", "RUN_HOURS"), "%QW11": ("STN", "VOL_TO_SPILL"),
|
||||||
|
"%QW12": ("STN", "STATE"), "%QW13": ("PU301", "STATE"),
|
||||||
|
"%QW14": ("PU302", "STATE"), "%QW15": ("PU303", "STATE"),
|
||||||
|
"%QW16": ("STN", "DUTY_PUMP"), "%QW17": ("STN", "ALARM_WORD"),
|
||||||
|
"%QW20": ("STN", "CMD_ACK"),
|
||||||
|
"%MW0": ("SP", "MODE"), "%MW1": ("SP", "CMD_WORD"),
|
||||||
|
"%MW2": ("SP", "CMD_PARAM"), "%MW3": ("SP", "LEVEL_SP"),
|
||||||
|
"%MW4": ("SP", "START_DUTY"), "%MW5": ("SP", "START_P2"),
|
||||||
|
"%MW6": ("SP", "START_P3"), "%MW7": ("SP", "STOP_ALL"),
|
||||||
|
"%MW8": ("SP", "HIGH_ALARM"), "%MW9": ("SP", "MIN_SPEED"),
|
||||||
|
"%MW10": ("SP", "SERVICE_HRS"),
|
||||||
|
"%MW20": ("SIM", "INFLOW"), "%MW21": ("SIM", "SCENARIO"),
|
||||||
|
"%MW22": ("SIM", "RESET"), "%MW23": ("SIM", "TIME_SCALE"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def q(v):
|
||||||
|
"""Quote a .qli value the way the reference exports do."""
|
||||||
|
return '"%s"' % ("" if v is None else str(v))
|
||||||
|
|
||||||
|
|
||||||
|
def wrap(values):
|
||||||
|
"""Split into the reference exports' line shape: 5, then 6 per line.
|
||||||
|
|
||||||
|
Both reference files wrap this way, in the @FIELDS header and in
|
||||||
|
every record. Reproduced exactly rather than assuming the importer
|
||||||
|
tolerates a different grouping.
|
||||||
|
"""
|
||||||
|
out = [list(values[:5])]
|
||||||
|
rest = list(values[5:])
|
||||||
|
for i in range(0, len(rest), 6):
|
||||||
|
out.append(rest[i:i + 6])
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def record(rows):
|
||||||
|
"""Format one record, backslash continuations between lines."""
|
||||||
|
lines = [",".join(str(v) for v in chunk) for chunk in wrap(rows)]
|
||||||
|
return (",\\\n".join(lines)) + "\n"
|
||||||
|
|
||||||
|
|
||||||
|
def header(version, fields, tag):
|
||||||
|
lines = ["", "@LANGUAGE", "ENGLISH", "", "", "@VERSION", version, "", "", "!" + "=" * 130, "", "@FIELDS"]
|
||||||
|
lines += [",".join(chunk) for chunk in wrap(fields)]
|
||||||
|
lines += ["", tag]
|
||||||
|
return "\n".join(lines) + "\n"
|
||||||
|
|
||||||
|
|
||||||
|
POINT_FIELDS = [
|
||||||
|
"NAME", "STATION", "POINT", "DESCRIPTION", "IO_ADDRESS",
|
||||||
|
"EXTERNAL_RELATION", "SCAN_TYPE", "CONV_TYPE", "DELTA_LIMIT", "MAX_INSENS", "OFFSET",
|
||||||
|
"PHYS_LOW", "PHYS_HIGH", "STEP", "INVERS", "HAS_SIGN", "OVERFL_DET",
|
||||||
|
"SWAP_BYTES", "SWAP_WORDS", "ELEC_LOW", "ELEC_HIGH", "TMO_NONE", "TMO_BOTH",
|
||||||
|
"AVE_UPD_INTERVAL", "NO_ZERO", "BURST_LIMIT", "BITS", "CHARS", "DIGITS",
|
||||||
|
"FLOAT_TYPE", "TIME_ZONE", "TIME_REPRES", "WLS_VAL_TYPE",
|
||||||
|
]
|
||||||
|
|
||||||
|
ITEM_FIELDS = [
|
||||||
|
"NAME", "NSID", "PARENT_NSID", "SECTION_PATH", "DESCRIPTION",
|
||||||
|
"DEADBAND", "LOW_LIMIT", "HIGH_LIMIT", "LOW_LOW_LIMIT", "HIGH_HIGH_LIMIT", "T_VALUE",
|
||||||
|
"P_VALUE", "I_VALUE", "TREND_UP_LIMIT", "TREND_LOW_LIMIT", "SCALE_HIGH_LIMIT", "SCALE_LOW_LIMIT",
|
||||||
|
"DUMMY2", "COMMENT_1", "COMMENT_2", "VALUE_FORMAT", "POSITIONED", "LONGITUDE",
|
||||||
|
"LATITUDE", "ALARMING", "DIAG", "NAME_IN_ITM_TAB", "STORAGE", "AUDIT_INFO",
|
||||||
|
"FO_ITEM", "BLOCKED", "PARENT_BLOCKED", "ALARM_INHIBIT", "PARENT_ALARM_INHIBIT", "OPC_VISIBLE",
|
||||||
|
"PARENT_OPC_VISIBLE", "OPC_READ", "OPC_WRITE", "OPC_ALARM_DETECTION", "HAS_SUB", "STRING_LENGTH",
|
||||||
|
"DELAY", "REPEAT", "ID_GROUP", "ID_NUMBER", "ITEM_REP", "ITEM_TYPE",
|
||||||
|
"ITEM_SPECIAL", "ACKN_TYPE", "ALARM_GROUP", "FRONT_END_NODE", "DISTR_TYPE", "INSTALL",
|
||||||
|
"UNIT", "TAG", "LIMIT_CLAMP", "OUT_OF_RANGE", "COL_GROUP", "STATION",
|
||||||
|
"POINT", "FO_GROUP", "ITEM_STAT_1", "ITEM_STAT_2", "ITEM_STAT_3", "ITEM_STAT_4",
|
||||||
|
"ITEM_STAT_5", "ITEM_STAT_6", "ALARM_STATE_1", "ALARM_STATE_2", "ALARM_STATE_3", "ALARM_STATE_4",
|
||||||
|
"ALARM_STATE_5", "ALARM_STATE_6", "PRIORITY_1", "PRIORITY_2", "PRIORITY_3", "PRIORITY_4",
|
||||||
|
"PRIORITY_5", "PRIORITY_6", "ALARM_TEXT_1", "ALARM_TEXT_2", "ALARM_TEXT_3", "ALARM_TEXT_4",
|
||||||
|
"ALARM_TEXT_5", "ALARM_TEXT_6", "ALARM_COLOR_1", "ALARM_COLOR_2", "ALARM_COLOR_3", "ALARM_COLOR_4",
|
||||||
|
"ALARM_COLOR_5", "ALARM_COLOR_6", "MNEMONIC_1", "MNEMONIC_2", "MNEMONIC_3", "MNEMONIC_4",
|
||||||
|
"MNEMONIC_5", "MNEMONIC_6", "AOI_1", "AOI_2", "AOI_3", "AOI_4",
|
||||||
|
"AOI_5", "AOI_6", "AOI_7", "AOI_8", "AOI_9", "AOI_10",
|
||||||
|
"AOI_11", "AOI_12", "AOI_13", "AOI_14", "AOI_15", "AOI_16",
|
||||||
|
"ENG_UNIT", "PROCESS_LIST", "POINT_NAME", "OPC_AE_STATION_NAME", "OPC_EVENT_SOURCE", "OPC_EVENT_SOURCE_NAME",
|
||||||
|
"CREATED_BY", "SHELVE_ENABLED_1", "SHELVE_ENABLED_2", "SHELVE_ENABLED_3", "SHELVE_ENABLED_4", "SHELVE_ENABLED_5",
|
||||||
|
"SHELVE_ENABLED_6", "AGG_INTERVAL",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def build_section(name, parent_path, leaf, nsid, parent_nsid, description):
|
||||||
|
"""One @SECTION_DF record, shaped like the reference export.
|
||||||
|
|
||||||
|
PARENT_OPC_VISIBLE is 1 for a root section and 0 for a child, which
|
||||||
|
is what the reference shows for AID (root) versus AID.WRPS (child).
|
||||||
|
"""
|
||||||
|
return record([
|
||||||
|
q(name), q(parent_path), q(leaf), 0, 0,
|
||||||
|
0, 0, 0, 1 if parent_path == "" else 0, nsid, parent_nsid,
|
||||||
|
q(description), q("unknown"),
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
def trimmed_span(gain, signed):
|
||||||
|
"""The ELEC range to map from, trimmed so PHYS lands on exact decimals.
|
||||||
|
|
||||||
|
CI Server stores the conversion as two endpoints, not as a gain, so
|
||||||
|
it recovers the gain by dividing. With a gain of 1/60 the full-scale
|
||||||
|
endpoint 32767/60 = 546.11666... has to be written rounded, and the
|
||||||
|
recovered gain is then slightly off: setpoint 4200 came back as
|
||||||
|
70.00003333333336 rather than 70.
|
||||||
|
|
||||||
|
So when the gain is 1/n, trim the span to the nearest multiple of n
|
||||||
|
inside it - 32760 instead of 32767, giving PHYS exactly +/-546 and a
|
||||||
|
gain CI Server recovers exactly. Nothing is lost: the trimmed 7
|
||||||
|
counts are 0.4 mm of a level register that tops out at 7000.
|
||||||
|
|
||||||
|
Gains that already terminate (0.1, 0.2, 0.36) keep the full span.
|
||||||
|
"""
|
||||||
|
lo, hi = (-32768, 32767) if signed else (0, 65535)
|
||||||
|
inv = 1.0 / gain
|
||||||
|
n = int(round(inv))
|
||||||
|
if n > 1 and abs(inv - n) < 1e-9:
|
||||||
|
lo, hi = -((-lo) // n) * n, (hi // n) * n
|
||||||
|
return lo, hi
|
||||||
|
|
||||||
|
|
||||||
|
def build_point(row, io_addr):
|
||||||
|
digital = row["data_type"] == "Boolean"
|
||||||
|
writable = row["access"] == "Read/Write"
|
||||||
|
gain = float(row["eng_gain"])
|
||||||
|
signed = row["data_type"] == "Signed 16-bit"
|
||||||
|
|
||||||
|
if digital:
|
||||||
|
conv, bits = "Digital", 16
|
||||||
|
elec_lo, elec_hi, phys_lo, phys_hi = 0, 100, 0, 100
|
||||||
|
else:
|
||||||
|
conv, bits = "Linear", 16
|
||||||
|
# Full-scale linear mapping. Exact, and invents no plant range;
|
||||||
|
# display ranges and alarm limits are set per item in CI Server.
|
||||||
|
# The engineering unit is carried entirely by the gain (see UNITS
|
||||||
|
# in gen_scada_points.py), so PHYS is ELEC scaled by it - which is
|
||||||
|
# why the offset must stay zero on both ends.
|
||||||
|
elec_lo, elec_hi = trimmed_span(gain, signed)
|
||||||
|
phys_lo, phys_hi = elec_lo * gain, elec_hi * gain
|
||||||
|
|
||||||
|
external = "Input + Output" if writable else "Input"
|
||||||
|
scan = "MOD_SCAN"
|
||||||
|
|
||||||
|
name = "%s:%s" % (STATION, row["point"])
|
||||||
|
return record([
|
||||||
|
q(name), q(STATION), q(row["point"]), q(row["description"][:60]), q(io_addr),
|
||||||
|
q(external), q(scan), q(conv), 0, 0, 0,
|
||||||
|
_num(phys_lo), _num(phys_hi), 1, 0, 1 if signed else 0, 0,
|
||||||
|
0, 0, _num(elec_lo), _num(elec_hi), 0, 0,
|
||||||
|
0, 0, 0, bits, 16, 4,
|
||||||
|
q("Intel"), q("Date+time GMT"), q("7 bytes IEC"), q("Float value"),
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
def _num(v):
|
||||||
|
"""Whole numbers stay whole; fractions keep enough digits to be exact.
|
||||||
|
|
||||||
|
Four decimals, not one: a gain of 0.001 puts PHYS_HIGH at 32.767, and
|
||||||
|
rounding that to 32.8 would bend the conversion by 0.1%.
|
||||||
|
"""
|
||||||
|
return int(v) if float(v) == int(float(v)) else round(float(v), 4)
|
||||||
|
|
||||||
|
|
||||||
|
def build_item(row, nsid, parent_nsid, id_number):
|
||||||
|
digital = row["data_type"] == "Boolean"
|
||||||
|
section = row["section"]
|
||||||
|
path = "%s.%s" % (ROOT, section)
|
||||||
|
name = "%s.%s" % (path, row["leaf"])
|
||||||
|
|
||||||
|
item_rep = "Boolean" if digital else "Real"
|
||||||
|
value_format = "" if digital else row["format_mask"]
|
||||||
|
eng_unit = row["eng_units"]
|
||||||
|
|
||||||
|
f = {k: 0 for k in ITEM_FIELDS}
|
||||||
|
f.update({k: q("") for k in ITEM_FIELDS if k.startswith(
|
||||||
|
("COMMENT", "ITEM_STAT", "ALARM_TEXT", "ALARM_COLOR", "MNEMONIC", "AOI"))})
|
||||||
|
|
||||||
|
f["NAME"] = q(name)
|
||||||
|
f["NSID"] = nsid
|
||||||
|
f["PARENT_NSID"] = parent_nsid
|
||||||
|
f["SECTION_PATH"] = q(path)
|
||||||
|
f["DESCRIPTION"] = q(row["description"][:60])
|
||||||
|
f["VALUE_FORMAT"] = q(value_format)
|
||||||
|
f["NAME_IN_ITM_TAB"] = 1
|
||||||
|
f["STRING_LENGTH"] = 1
|
||||||
|
f["ID_GROUP"] = 1
|
||||||
|
f["ID_NUMBER"] = id_number
|
||||||
|
f["ITEM_REP"] = q(item_rep)
|
||||||
|
f["ITEM_TYPE"] = q("")
|
||||||
|
f["ITEM_SPECIAL"] = q("")
|
||||||
|
f["ACKN_TYPE"] = q("")
|
||||||
|
f["ALARM_GROUP"] = q("")
|
||||||
|
f["FRONT_END_NODE"] = q("UNLICENCED")
|
||||||
|
f["DISTR_TYPE"] = q("Local Host")
|
||||||
|
f["INSTALL"] = q("WRPS")
|
||||||
|
f["UNIT"] = q(section)
|
||||||
|
f["TAG"] = q(row["leaf"])
|
||||||
|
f["LIMIT_CLAMP"] = q("")
|
||||||
|
f["COL_GROUP"] = q("")
|
||||||
|
f["STATION"] = q(STATION)
|
||||||
|
f["POINT"] = q(row["point"])
|
||||||
|
f["FO_GROUP"] = q("")
|
||||||
|
for i in range(1, 7):
|
||||||
|
f["ALARM_STATE_%d" % i] = q("Normal")
|
||||||
|
if digital:
|
||||||
|
f["ITEM_STAT_1"] = q("BOOLEAN 0")
|
||||||
|
f["ITEM_STAT_2"] = q("BOOLEAN 1")
|
||||||
|
f["ENG_UNIT"] = q(eng_unit)
|
||||||
|
f["PROCESS_LIST"] = q("")
|
||||||
|
f["POINT_NAME"] = q("%s:%s" % (STATION, row["point"]))
|
||||||
|
f["OPC_AE_STATION_NAME"] = q("")
|
||||||
|
f["OPC_EVENT_SOURCE"] = q("")
|
||||||
|
f["OPC_EVENT_SOURCE_NAME"] = q(":")
|
||||||
|
f["CREATED_BY"] = q("unknown")
|
||||||
|
for i in range(1, 7):
|
||||||
|
f["SHELVE_ENABLED_%d" % i] = 1
|
||||||
|
f["AGG_INTERVAL"] = q("")
|
||||||
|
|
||||||
|
return record([f[k] for k in ITEM_FIELDS])
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser(description="Emit CI Server .qli imports.")
|
||||||
|
ap.add_argument("--address-base", type=int, choices=(0, 1), default=1,
|
||||||
|
help="IO_ADDRESS numbering: 1 = protocol address + 1 (default)")
|
||||||
|
ap.add_argument("--nsid-base", type=int, default=171,
|
||||||
|
help="first free NSID; sections take six from here, items follow")
|
||||||
|
ap.add_argument("--parent-nsid", type=int, default=170,
|
||||||
|
help="NSID of AID.WRPS, which the six sections hang off")
|
||||||
|
ap.add_argument("--include-root", action="store_true",
|
||||||
|
help="also emit AID and AID.WRPS sections (they already exist here)")
|
||||||
|
ap.add_argument("--section-nsid", action="append", default=[],
|
||||||
|
metavar="SECTION=NSID", help="pin a section's NSID, e.g. STN=41")
|
||||||
|
ap.add_argument("--version", default="1.03.00", help="@VERSION written into both files")
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
if not POINTS.is_file():
|
||||||
|
sys.exit("missing scada-points.csv - run gen_scada_points.py first")
|
||||||
|
|
||||||
|
pinned = {}
|
||||||
|
for spec in args.section_nsid:
|
||||||
|
k, _, v = spec.partition("=")
|
||||||
|
pinned[k.strip().upper()] = int(v)
|
||||||
|
|
||||||
|
rows = list(csv.DictReader(open(POINTS, newline="", encoding="utf-8")))
|
||||||
|
prepared = []
|
||||||
|
for r in rows:
|
||||||
|
iec = r["iec_address"]
|
||||||
|
if iec not in LEAF:
|
||||||
|
sys.exit("no leaf name defined for %s - add it to LEAF" % iec)
|
||||||
|
section, leaf = LEAF[iec]
|
||||||
|
r["section"] = section
|
||||||
|
r["leaf"] = leaf
|
||||||
|
r["point"] = ("%s_%s" % (section, leaf))[:24]
|
||||||
|
prepared.append(r)
|
||||||
|
|
||||||
|
# NSIDs: one per section, then one per item, from --nsid-base.
|
||||||
|
next_id = args.nsid_base
|
||||||
|
section_nsid = {}
|
||||||
|
for s in SECTION_ORDER:
|
||||||
|
if s in pinned:
|
||||||
|
section_nsid[s] = pinned[s]
|
||||||
|
else:
|
||||||
|
section_nsid[s] = next_id
|
||||||
|
next_id += 1
|
||||||
|
|
||||||
|
section_out = [header(args.version, SECTION_FIELDS, "@SECTION_DF")]
|
||||||
|
if args.include_root:
|
||||||
|
root, _, wrps_leaf = ROOT.partition(".")
|
||||||
|
section_out.append(build_section(root, "", root, args.parent_nsid - 1, 0,
|
||||||
|
"Waterloo Road Pump Station demo"))
|
||||||
|
section_out.append(build_section(ROOT, root, wrps_leaf, args.parent_nsid,
|
||||||
|
args.parent_nsid - 1,
|
||||||
|
"Waterloo Road Pump Station"))
|
||||||
|
for s in SECTION_ORDER:
|
||||||
|
section_out.append(build_section("%s.%s" % (ROOT, s), ROOT, s,
|
||||||
|
section_nsid[s], args.parent_nsid,
|
||||||
|
SECTION_DESC[s]))
|
||||||
|
|
||||||
|
point_out = [header(args.version, POINT_FIELDS, "@MODBUS_POINT_DF")]
|
||||||
|
item_out = [header(args.version, ITEM_FIELDS, "@ITEM_DF")]
|
||||||
|
|
||||||
|
# NSID and ID_NUMBER follow register order, which is also the order the
|
||||||
|
# items were first imported in. Keep it stable: a re-import must update
|
||||||
|
# the existing items in place, not renumber them - the HMI's item ids in
|
||||||
|
# 05-scada/hmi/item-ids.csv are derived from that creation order.
|
||||||
|
for n, r in enumerate(prepared):
|
||||||
|
addr = int(r["modbus_address"]) + args.address_base
|
||||||
|
# Zero-padded to at least two digits, as the reference export does
|
||||||
|
# ("RI:01", not "RI:1"). Wider addresses keep their own width.
|
||||||
|
io_addr = "%s:%02d" % (PREFIX[r["function_code"]], addr)
|
||||||
|
point_out.append(build_point(r, io_addr))
|
||||||
|
item_out.append(build_item(r, next_id + n, section_nsid[r["section"]], n + 1))
|
||||||
|
|
||||||
|
# The header already ends in a newline, so the first record follows it
|
||||||
|
# directly; records are then separated by one blank line, exactly as
|
||||||
|
# the reference exports are.
|
||||||
|
def assemble(parts):
|
||||||
|
return parts[0] + "\n".join(parts[1:])
|
||||||
|
|
||||||
|
(HERE / "wrps_section_df.qli").write_text(assemble(section_out), encoding="utf-8")
|
||||||
|
(HERE / "wrps_modbus_point_df.qli").write_text(assemble(point_out), encoding="utf-8")
|
||||||
|
(HERE / "wrps_item_df.qli").write_text(assemble(item_out), encoding="utf-8")
|
||||||
|
|
||||||
|
n_sections = len(SECTION_ORDER) + (2 if args.include_root else 0)
|
||||||
|
print("OK wrps_section_df.qli (%d sections under %s)" % (n_sections, ROOT))
|
||||||
|
print("OK wrps_modbus_point_df.qli (%d points, station %s)" % (len(prepared), STATION))
|
||||||
|
print("OK wrps_item_df.qli (%d items under %s.*)" % (len(prepared), ROOT))
|
||||||
|
print(" address base : %d (holding register 0 -> RO:%02d)" % (args.address_base, args.address_base))
|
||||||
|
print(" section NSIDs: " + ", ".join("%s=%d" % (s, section_nsid[s]) for s in SECTION_ORDER))
|
||||||
|
print(" item NSIDs : %d-%d" % (next_id, next_id + len(prepared) - 1))
|
||||||
|
by_sec = {}
|
||||||
|
for r in prepared:
|
||||||
|
by_sec.setdefault(r["section"], 0)
|
||||||
|
by_sec[r["section"]] += 1
|
||||||
|
print(" per section : " + ", ".join("%s=%d" % (s, by_sec.get(s, 0)) for s in SECTION_ORDER))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
265
04-scada/modbus_points/gen_scada_points.py
Normal file
265
04-scada/modbus_points/gen_scada_points.py
Normal file
|
|
@ -0,0 +1,265 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Derive the SCADA-side point list from the PLC-side register map.
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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
|
||||||
|
instead of hand-transcribed, so the two cannot drift apart silently.
|
||||||
|
|
||||||
|
It is a *starting point* for CI Server configuration, not a CI Server
|
||||||
|
import file. CI Server's .qli item exports carry no Modbus addressing
|
||||||
|
(that lives in the front-end I/O configuration), so the mapping is
|
||||||
|
applied by hand in CI Server from this list.
|
||||||
|
|
||||||
|
The four judgements this script encodes
|
||||||
|
---------------------------------------
|
||||||
|
1. **Poll groups.** Four, not one per function code. The holding
|
||||||
|
registers split by purpose and address range: read-only published
|
||||||
|
data at 0-20, operator setpoints at 1024-1034, simulation control at
|
||||||
|
1044-1047. Each group is then contiguous and polls as one request;
|
||||||
|
a single holding-register group would span 1048 mostly-empty
|
||||||
|
addresses.
|
||||||
|
|
||||||
|
2. **%QW17 is UNSIGNED.** Bit 15 does not fit a signed INT. Configure
|
||||||
|
it as a 16-bit unsigned register or the alarm word goes negative
|
||||||
|
exactly when the most severe alarm is set.
|
||||||
|
|
||||||
|
3. **Engineering units are chosen here, not in the PLC.** The PLC
|
||||||
|
publishes mm, L/s x10 and Hz x10 and keeps doing so; a unit on the
|
||||||
|
SCADA side is a gain on the raw register (see UNITS below). One
|
||||||
|
unit per register, though - CI Server rejects a second item on an
|
||||||
|
I/O address it already has.
|
||||||
|
|
||||||
|
4. **%IW / %IX are excluded by default.** They are the *field* inputs.
|
||||||
|
In the simulation build nothing writes them, so they read 0 forever
|
||||||
|
and would show a dead plant on the HMI. Every live measurement is
|
||||||
|
published in the %QW block. Pass --include-field to emit them
|
||||||
|
anyway, for a real field deployment.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import csv
|
||||||
|
import sys
|
||||||
|
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"
|
||||||
|
|
||||||
|
# Modbus object -> (function code, poll group name)
|
||||||
|
#
|
||||||
|
# Holding registers are split into three groups rather than one. They
|
||||||
|
# share a function code but not a purpose or an address range: %QW is
|
||||||
|
# read-only published data at 0-1023, %MW is writable at 1024+. One
|
||||||
|
# group spanning both would be a single 1048-register request over a
|
||||||
|
# mostly empty address space.
|
||||||
|
GROUPS = {
|
||||||
|
"Coil": ("FC01", "PS_STATUS_BITS"),
|
||||||
|
"Discrete input": ("FC02", "PS_FIELD_BITS"),
|
||||||
|
"Input register": ("FC04", "PS_FIELD_ANALOG"),
|
||||||
|
}
|
||||||
|
|
||||||
|
HR_PUBLISHED = ("FC03", "PS_PUBLISHED") # %QW, read-only
|
||||||
|
HR_SETPOINTS = ("FC03/FC06", "PS_SETPOINTS") # %MW0-10, operator writable
|
||||||
|
HR_SIM = ("FC03/FC06", "PS_SIM_CONTROL") # %MW20-23, simulation only
|
||||||
|
|
||||||
|
FIELD_ONLY = {"Input register", "Discrete input"}
|
||||||
|
UNSIGNED = {"%QW17"}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Engineering units, SCADA side
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
#
|
||||||
|
# The PLC publishes mm, L/s x10 and Hz x10 - that is the contract in
|
||||||
|
# register-map.csv and it does not change. What the operator reads is a
|
||||||
|
# presentation choice, and CI Server already applies a linear conversion
|
||||||
|
# per Modbus point, so a unit here is nothing but a gain on the raw
|
||||||
|
# register:
|
||||||
|
#
|
||||||
|
# engineering value = raw register * gain (offset is always 0)
|
||||||
|
#
|
||||||
|
# Zero offset matters: it keeps the full-scale ELEC -> PHYS mapping in
|
||||||
|
# gen_ciserver_qli.py exact, and it means a second unit on the same
|
||||||
|
# register is just a second point with a different gain.
|
||||||
|
#
|
||||||
|
# ONE ITEM PER I/O ADDRESS. A register carries exactly one engineering
|
||||||
|
# unit, because CI Server will not accept two items on the same Modbus
|
||||||
|
# address - an import that tries it fails with
|
||||||
|
#
|
||||||
|
# EQP-E-DUP_ITEM, I/O address of item already defined
|
||||||
|
# DSSP-E-INSREC, Failed to insert a record in the dataset ITEM_DF
|
||||||
|
#
|
||||||
|
# (confirmed on R1.03, 2026-08-14, trying to publish level in m *and* %).
|
||||||
|
# So showing one measurement in two units needs two *PLC* registers, not
|
||||||
|
# two views of one. Level and speed are published in % alone.
|
||||||
|
#
|
||||||
|
# Gains chosen 2026-08-14:
|
||||||
|
# level % 100% = the 6.000 m spill weir, so a full bar means spilling
|
||||||
|
# and the reading reconciles with VOL_TO_SPILL. 1/60 per mm.
|
||||||
|
# flow m3/h = L/s x 3.6, and the register is L/s x10: 0.36.
|
||||||
|
# speed % 100% = 50 Hz, the drive's maximum. Register Hz x10: 0.2.
|
||||||
|
#
|
||||||
|
# iec address -> (units, gain, format mask)
|
||||||
|
UNITS = {
|
||||||
|
"%QW0": ("%", 1 / 60.0, "999.9"), # wet well level, was mm
|
||||||
|
"%QW1": ("m3/h", 0.36, "9999.9"), # inflow, was L/s
|
||||||
|
"%QW2": ("m3/h", 0.36, "9999.9"), # total discharge, was L/s
|
||||||
|
"%QW4": ("%", 0.2, "999.9"), # common drive speed, was Hz
|
||||||
|
"%QW7": ("m3/h", 0.36, "9999.9"), # net accumulation, signed
|
||||||
|
"%MW3": ("%", 1 / 60.0, "999.9"), # level setpoint, was mm
|
||||||
|
"%MW4": ("%", 1 / 60.0, "999.9"), # start duty
|
||||||
|
"%MW5": ("%", 1 / 60.0, "999.9"), # start pump 2
|
||||||
|
"%MW6": ("%", 1 / 60.0, "999.9"), # start pump 3
|
||||||
|
"%MW7": ("%", 1 / 60.0, "999.9"), # stop all
|
||||||
|
"%MW8": ("%", 1 / 60.0, "999.9"), # high level alarm
|
||||||
|
"%MW9": ("%", 0.2, "999.9"), # minimum drive speed, was Hz
|
||||||
|
"%MW20": ("m3/h", 0.36, "9999.9"), # SIM manual inflow, was L/s
|
||||||
|
}
|
||||||
|
|
||||||
|
# Points that keep their PLC unit but not the default mask. Enums, modes
|
||||||
|
# and small counts are one or two digits wide; a five-digit mask on them
|
||||||
|
# is not wrong, only wide, and it would disagree with the HMI's masks in
|
||||||
|
# 04-scada/hmi/point_format.py. iec address -> mask
|
||||||
|
MASK_ONLY = {
|
||||||
|
"%QW3": "9", # pumps running, 0-3
|
||||||
|
"%QW11": "9999", # volume remaining to spill, m3
|
||||||
|
"%QW12": "9", # station state enum
|
||||||
|
"%QW13": "9", "%QW14": "9", "%QW15": "9", # pump state enums
|
||||||
|
"%QW16": "9", # duty pump, 0-3
|
||||||
|
"%QW20": "99", # command acknowledge
|
||||||
|
"%MW0": "9", # station mode
|
||||||
|
"%MW1": "99", # command word
|
||||||
|
"%MW2": "9", # command parameter, pump number
|
||||||
|
"%MW21": "9", # SIM scenario 0-3
|
||||||
|
"%MW22": "9", # SIM reset, write 1
|
||||||
|
"%MW23": "999", # SIM time scale 1-120
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def eng_view(iec, scaling):
|
||||||
|
"""(units override, gain, mask override) for a register.
|
||||||
|
|
||||||
|
Falls back to the PLC's own unit and the raw scaling when the point
|
||||||
|
is not in UNITS - most points are already in the unit the operator
|
||||||
|
wants, and inventing a conversion for them would only add risk.
|
||||||
|
"""
|
||||||
|
if iec in UNITS:
|
||||||
|
return UNITS[iec]
|
||||||
|
gain = 0.1 if scaling == "x10" else 1.0
|
||||||
|
default = "9999.9" if scaling == "x10" else "99999"
|
||||||
|
return None, gain, MASK_ONLY.get(iec, default)
|
||||||
|
|
||||||
|
|
||||||
|
def gain_expr(gain):
|
||||||
|
"""The gain as the human-readable expression the CSV carries."""
|
||||||
|
if gain == 1.0:
|
||||||
|
return "value"
|
||||||
|
if gain == 0.1:
|
||||||
|
return "value / 10"
|
||||||
|
if abs(gain - 1 / 60.0) < 1e-12:
|
||||||
|
return "value / 60"
|
||||||
|
return "value * %g" % gain
|
||||||
|
|
||||||
|
|
||||||
|
def tag_for(row, description):
|
||||||
|
"""Tag name per CLAUDE.md section 8: PS_<EQUIP><NN>_<MEAS>.
|
||||||
|
|
||||||
|
Built from the cleaned description, so the "SIM ONLY:" marker does
|
||||||
|
not end up inside the tag name.
|
||||||
|
"""
|
||||||
|
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()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser(description="Derive the SCADA point list.")
|
||||||
|
ap.add_argument(
|
||||||
|
"--include-field",
|
||||||
|
action="store_true",
|
||||||
|
help="also emit %%IW/%%IX field inputs (they read 0 in the simulation build)",
|
||||||
|
)
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
if not SRC_MAP.is_file():
|
||||||
|
sys.exit(f"missing {SRC_MAP} - run 04-plc/wrps-plc/build.py first")
|
||||||
|
|
||||||
|
rows = list(csv.DictReader(open(SRC_MAP, newline="", encoding="utf-8")))
|
||||||
|
out_rows, skipped = [], 0
|
||||||
|
|
||||||
|
for r in rows:
|
||||||
|
obj = r["modbus_object"]
|
||||||
|
if obj in FIELD_ONLY and not args.include_field:
|
||||||
|
skipped += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
iec = r["iec_address"]
|
||||||
|
sim_only = r["description"].startswith("SIM ONLY")
|
||||||
|
writable = r["access"] == "RW"
|
||||||
|
description = r["description"].replace("SIM ONLY: ", "")
|
||||||
|
|
||||||
|
if obj == "Holding register":
|
||||||
|
if sim_only:
|
||||||
|
fc, group = HR_SIM
|
||||||
|
elif writable:
|
||||||
|
fc, group = HR_SETPOINTS
|
||||||
|
else:
|
||||||
|
fc, group = HR_PUBLISHED
|
||||||
|
else:
|
||||||
|
fc, group = GROUPS[obj]
|
||||||
|
|
||||||
|
if obj in ("Coil", "Discrete input"):
|
||||||
|
data_type = "Boolean"
|
||||||
|
elif iec in UNSIGNED:
|
||||||
|
data_type = "Unsigned 16-bit"
|
||||||
|
else:
|
||||||
|
data_type = "Signed 16-bit"
|
||||||
|
|
||||||
|
units_override, gain, mask = eng_view(iec, r["scaling"])
|
||||||
|
|
||||||
|
out_rows.append(
|
||||||
|
{
|
||||||
|
"scada_tag": tag_for(r, description),
|
||||||
|
"description": description,
|
||||||
|
"poll_group": group,
|
||||||
|
"function_code": fc,
|
||||||
|
"modbus_address": r["modbus_address"],
|
||||||
|
"data_type": data_type,
|
||||||
|
"access": "Read/Write" if writable else "Read",
|
||||||
|
"eng_units": units_override if units_override is not None else r["units"],
|
||||||
|
"raw_to_eng": gain_expr(gain),
|
||||||
|
"eng_gain": repr(gain),
|
||||||
|
"format_mask": mask,
|
||||||
|
"iec_address": iec,
|
||||||
|
"plc_tag": r["tag"],
|
||||||
|
"notes": "SIMULATION CONTROL - simulation build only" if sim_only else "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
with open(OUT, "w", newline="", encoding="utf-8") as fh:
|
||||||
|
w = csv.DictWriter(fh, fieldnames=list(out_rows[0].keys()))
|
||||||
|
w.writeheader()
|
||||||
|
w.writerows(out_rows)
|
||||||
|
|
||||||
|
groups = {}
|
||||||
|
for r in out_rows:
|
||||||
|
groups.setdefault(r["poll_group"], []).append(int(r["modbus_address"]))
|
||||||
|
|
||||||
|
print(f"OK {OUT} ({len(out_rows)} points)")
|
||||||
|
for g, addrs in groups.items():
|
||||||
|
print(f" {g:<18} {len(addrs):>3} points, addresses {min(addrs)}-{max(addrs)}")
|
||||||
|
if skipped:
|
||||||
|
print(
|
||||||
|
f" skipped {skipped} %IW/%IX field points - they read 0 in the\n"
|
||||||
|
f" simulation build; pass --include-field for a real field deployment"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
62
04-scada/modbus_points/historian/README.md
Normal file
62
04-scada/modbus_points/historian/README.md
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
# historian — collection groups and item bindings
|
||||||
|
|
||||||
|
**Hand-made. There is no generator for this.** Everything else under
|
||||||
|
`modbus_points/` is generated from `03-plc/register-map.csv`; these two files are
|
||||||
|
not, and that is the reason they have drifted from the server.
|
||||||
|
|
||||||
|
| File | Class | Holds |
|
||||||
|
|---|---|---|
|
||||||
|
| `his_group.qli` | `@HIS_GROUP_DF` | 3 collection groups |
|
||||||
|
| `item_his.qli` | `@ITEM_HIS_DF` | 49 item → group bindings |
|
||||||
|
|
||||||
|
## The three groups
|
||||||
|
|
||||||
|
| Group | Items | Collection |
|
||||||
|
|---|---|---|
|
||||||
|
| `WRPS_ONE_SEC` | 5 | 1 s scan — the fast analogues: level, inflow, discharge |
|
||||||
|
| `WRPS_ONE_MIN` | 4 | 1 min scan — slower trends |
|
||||||
|
| `WRPS_EVENT` | 40 | on change — states, commands, alarms, trips |
|
||||||
|
|
||||||
|
Written 2026-08-21.
|
||||||
|
|
||||||
|
## Why this matters more than it looks
|
||||||
|
|
||||||
|
The demo this project exists to support is built on **historical grounding**. From
|
||||||
|
`01-design/00-origin/`:
|
||||||
|
|
||||||
|
> "Are you sure? What did inflow actually do last time we had rain like this?"
|
||||||
|
|
||||||
|
The answer to that question comes from the historian. Layer 1 of the demo — risk
|
||||||
|
and headroom — works from live values alone. The push-back that earns trust does
|
||||||
|
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
|
||||||
|
|
||||||
|
`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:
|
||||||
|
|
||||||
|
| 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.
|
||||||
|
|
||||||
|
## Worth doing: generate these too
|
||||||
|
|
||||||
|
The points and items chain is generated end to end, which is what keeps the PLC
|
||||||
|
and SCADA from drifting apart. The historian stops short of that, is maintained by
|
||||||
|
hand, and has drifted — exactly the failure the generated chain exists to prevent.
|
||||||
|
|
||||||
|
Extending `../gen_ciserver_qli.py` to emit `@HIS_GROUP_DF` and `@ITEM_HIS_DF`
|
||||||
|
would close the gap. The item list is already there; what it needs is a table
|
||||||
|
saying which item belongs to which group at what rate. Not done in this audit.
|
||||||
44
04-scada/modbus_points/historian/his_group.qli
Normal file
44
04-scada/modbus_points/historian/his_group.qli
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
|
||||||
|
@LANGUAGE
|
||||||
|
ENGLISH
|
||||||
|
|
||||||
|
|
||||||
|
@VERSION
|
||||||
|
1.03.00
|
||||||
|
|
||||||
|
|
||||||
|
!==================================================================================================================================
|
||||||
|
|
||||||
|
@FIELDS
|
||||||
|
NAME,DESCRIPTION,START_TIME,STOP_TIME,FIRST_ROLLOVER
|
||||||
|
LIFE_TIME,WARN_TIME,ROLLOVER_INT,FORCE_ROLLOVER,FILL_OLD,STORE_QUALITY
|
||||||
|
SAVE_TIME,SCAN_INTERVAL,CORRECT_DAYLIGHT,EXCLUDE_ARCHIVE,DATA_COMP,AGG_PERIOD_HOUR
|
||||||
|
AGG_PERIOD_SHIFT,AGG_PERIOD_DAY,AGG_PERIOD_WEEK,AGG_PERIOD_MONTH,AGG_PERIOD_YEAR,AGG_PERIOD_30MIN
|
||||||
|
AGG_CORRECT_DAY_DST,AGG_CORRECT_SHIFT_DST,NUMBER,NODE,NODE_NAME,NEXT_UNIT_SEQ
|
||||||
|
NEXT_DATA_SEQ,AVERAGING_INTERVAL,PRIORITY,SAMPLES_RECORD,TYPE,AVERAGING_METHOD
|
||||||
|
COL_STOR_TYPE,AGG_SHIFT_LENGTH,AGG_WEEK_START,AGG_SHIFT_START,AGG_DAY_START,AGG_COMP_NOT_ITEM
|
||||||
|
|
||||||
|
@HIS_GROUP_DF
|
||||||
|
"WRPS_ONE_SEC","","","","21-08-2026 00:00:00",\
|
||||||
|
"7 days","","1 hours",0,0,1,\
|
||||||
|
0,1,0,0,0,1,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,50,1,"",1,\
|
||||||
|
2,0,8,0,"Item","",\
|
||||||
|
"Scan/Time","8 hours","Monday","00:00","00:00",""
|
||||||
|
|
||||||
|
"WRPS_ONE_MIN","","","","21-08-2026 00:00:00",\
|
||||||
|
"7 days","","6 hours",0,0,1,\
|
||||||
|
0,60,0,0,0,1,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,51,1,"",1,\
|
||||||
|
2,0,5,0,"Item","",\
|
||||||
|
"Scan/Time","8 hours","Monday","00:00","00:00",""
|
||||||
|
|
||||||
|
"WRPS_EVENT","","","","21-08-2026 00:00:00",\
|
||||||
|
"7 days","","1 days",0,0,1,\
|
||||||
|
0,0,0,0,0,1,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,52,1,"",1,\
|
||||||
|
2,0,11,100,"Item","",\
|
||||||
|
"Event/Item","8 hours","Monday","00:00","00:00",""
|
||||||
312
04-scada/modbus_points/historian/item_his.qli
Normal file
312
04-scada/modbus_points/historian/item_his.qli
Normal file
|
|
@ -0,0 +1,312 @@
|
||||||
|
|
||||||
|
@LANGUAGE
|
||||||
|
ENGLISH
|
||||||
|
|
||||||
|
|
||||||
|
@VERSION
|
||||||
|
1.03.00
|
||||||
|
|
||||||
|
|
||||||
|
!==================================================================================================================================
|
||||||
|
|
||||||
|
@FIELDS
|
||||||
|
NAME,ITEM_NAME,GROUP_NAME,ON_EACH_UPDATE,ON_FIRST_UPDATE
|
||||||
|
ON_OPTION_CHANGE,ON_PASS_PIT,ON_QUALITY_CHANGE,ON_STATUS_CHANGE,ON_VALUE_CHANGE,STORE_DEADBAND
|
||||||
|
STORE_HIGH_HIGH_LIMIT,STORE_HIGH_LIMIT,STORE_LOW_LIMIT,STORE_LOW_LOW_LIMIT,STORE_VALUE,STORE_AGG_MAX
|
||||||
|
STORE_AGG_MIN,STORE_AGG_AVG,STORE_AGG_INTG,STORE_AGG_STDV,STORE_AGG_CNT,STORE_AGG_DIFF_SUM
|
||||||
|
AGG_DIFF_SUM_RANGE,AGG_DIFF_SUM_PERC,AGG_INTG_PERIOD
|
||||||
|
|
||||||
|
@ITEM_HIS_DF
|
||||||
|
"WRPS_ONE_SEC:AID.WRPS.STN.LEVEL","AID.WRPS.STN.LEVEL","WRPS_ONE_SEC",0,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
1000000,10,"Second"
|
||||||
|
|
||||||
|
"WRPS_ONE_SEC:AID.WRPS.STN.INFLOW","AID.WRPS.STN.INFLOW","WRPS_ONE_SEC",0,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
1000000,10,"Second"
|
||||||
|
|
||||||
|
"WRPS_ONE_SEC:AID.WRPS.STN.DISCHARGE","AID.WRPS.STN.DISCHARGE","WRPS_ONE_SEC",0,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
1000000,10,"Second"
|
||||||
|
|
||||||
|
"WRPS_ONE_SEC:AID.WRPS.STN.SPEED","AID.WRPS.STN.SPEED","WRPS_ONE_SEC",0,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
1000000,10,"Second"
|
||||||
|
|
||||||
|
"WRPS_ONE_SEC:AID.WRPS.STN.NET_ACCUM","AID.WRPS.STN.NET_ACCUM","WRPS_ONE_SEC",0,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
1000000,10,"Second"
|
||||||
|
|
||||||
|
"WRPS_ONE_MIN:AID.WRPS.STN.VOL_TO_SPILL","AID.WRPS.STN.VOL_TO_SPILL","WRPS_ONE_MIN",0,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
1000000,10,"Second"
|
||||||
|
|
||||||
|
"WRPS_ONE_MIN:AID.WRPS.STN.TIME_TO_SPILL","AID.WRPS.STN.TIME_TO_SPILL","WRPS_ONE_MIN",0,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
1000000,10,"Second"
|
||||||
|
|
||||||
|
"WRPS_ONE_MIN:AID.WRPS.STN.TIME_TO_LSHH","AID.WRPS.STN.TIME_TO_LSHH","WRPS_ONE_MIN",0,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
1000000,10,"Second"
|
||||||
|
|
||||||
|
"WRPS_ONE_MIN:AID.WRPS.STN.PUMPS_RUNNING","AID.WRPS.STN.PUMPS_RUNNING","WRPS_ONE_MIN",0,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
1000000,10,"Second"
|
||||||
|
|
||||||
|
"WRPS_EVENT:AID.WRPS.PU301.RUN_CMD","AID.WRPS.PU301.RUN_CMD","WRPS_EVENT",0,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
1000000,10,"Second"
|
||||||
|
|
||||||
|
"WRPS_EVENT:AID.WRPS.PU302.RUN_CMD","AID.WRPS.PU302.RUN_CMD","WRPS_EVENT",0,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
1000000,10,"Second"
|
||||||
|
|
||||||
|
"WRPS_EVENT:AID.WRPS.PU303.RUN_CMD","AID.WRPS.PU303.RUN_CMD","WRPS_EVENT",0,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
1000000,10,"Second"
|
||||||
|
|
||||||
|
"WRPS_EVENT:AID.WRPS.PU301.RUNNING","AID.WRPS.PU301.RUNNING","WRPS_EVENT",0,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
1000000,10,"Second"
|
||||||
|
|
||||||
|
"WRPS_EVENT:AID.WRPS.PU302.RUNNING","AID.WRPS.PU302.RUNNING","WRPS_EVENT",0,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
1000000,10,"Second"
|
||||||
|
|
||||||
|
"WRPS_EVENT:AID.WRPS.PU303.RUNNING","AID.WRPS.PU303.RUNNING","WRPS_EVENT",0,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
1000000,10,"Second"
|
||||||
|
|
||||||
|
"WRPS_EVENT:AID.WRPS.PU301.AVAILABLE","AID.WRPS.PU301.AVAILABLE","WRPS_EVENT",0,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
1000000,10,"Second"
|
||||||
|
|
||||||
|
"WRPS_EVENT:AID.WRPS.PU302.AVAILABLE","AID.WRPS.PU302.AVAILABLE","WRPS_EVENT",0,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
1000000,10,"Second"
|
||||||
|
|
||||||
|
"WRPS_EVENT:AID.WRPS.PU303.AVAILABLE","AID.WRPS.PU303.AVAILABLE","WRPS_EVENT",0,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
1000000,10,"Second"
|
||||||
|
|
||||||
|
"WRPS_EVENT:AID.WRPS.PU301.TRIPPED","AID.WRPS.PU301.TRIPPED","WRPS_EVENT",0,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
1000000,10,"Second"
|
||||||
|
|
||||||
|
"WRPS_EVENT:AID.WRPS.PU302.TRIPPED","AID.WRPS.PU302.TRIPPED","WRPS_EVENT",0,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
1000000,10,"Second"
|
||||||
|
|
||||||
|
"WRPS_EVENT:AID.WRPS.PU303.TRIPPED","AID.WRPS.PU303.TRIPPED","WRPS_EVENT",0,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
1000000,10,"Second"
|
||||||
|
|
||||||
|
"WRPS_EVENT:AID.WRPS.STN.IN_AUTO","AID.WRPS.STN.IN_AUTO","WRPS_EVENT",0,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
1000000,10,"Second"
|
||||||
|
|
||||||
|
"WRPS_EVENT:AID.WRPS.STN.HIGH_LEVEL","AID.WRPS.STN.HIGH_LEVEL","WRPS_EVENT",0,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
1000000,10,"Second"
|
||||||
|
|
||||||
|
"WRPS_EVENT:AID.WRPS.STN.SPILL_ACTIVE","AID.WRPS.STN.SPILL_ACTIVE","WRPS_EVENT",0,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
1000000,10,"Second"
|
||||||
|
|
||||||
|
"WRPS_EVENT:AID.WRPS.STN.STATE","AID.WRPS.STN.STATE","WRPS_EVENT",0,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
1000000,10,"Second"
|
||||||
|
|
||||||
|
"WRPS_EVENT:AID.WRPS.PU301.STATE","AID.WRPS.PU301.STATE","WRPS_EVENT",0,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
1000000,10,"Second"
|
||||||
|
|
||||||
|
"WRPS_EVENT:AID.WRPS.PU302.STATE","AID.WRPS.PU302.STATE","WRPS_EVENT",0,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
1000000,10,"Second"
|
||||||
|
|
||||||
|
"WRPS_EVENT:AID.WRPS.PU303.STATE","AID.WRPS.PU303.STATE","WRPS_EVENT",0,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
1000000,10,"Second"
|
||||||
|
|
||||||
|
"WRPS_EVENT:AID.WRPS.STN.DUTY_PUMP","AID.WRPS.STN.DUTY_PUMP","WRPS_EVENT",0,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
1000000,10,"Second"
|
||||||
|
|
||||||
|
"WRPS_EVENT:AID.WRPS.STN.ALARM_WORD","AID.WRPS.STN.ALARM_WORD","WRPS_EVENT",0,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
1000000,10,"Second"
|
||||||
|
|
||||||
|
"WRPS_EVENT:AID.WRPS.STN.CMD_ACK","AID.WRPS.STN.CMD_ACK","WRPS_EVENT",0,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
1000000,10,"Second"
|
||||||
|
|
||||||
|
"WRPS_EVENT:AID.WRPS.PU301.RUN_HOURS","AID.WRPS.PU301.RUN_HOURS","WRPS_EVENT",0,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
1000000,10,"Second"
|
||||||
|
|
||||||
|
"WRPS_EVENT:AID.WRPS.PU302.RUN_HOURS","AID.WRPS.PU302.RUN_HOURS","WRPS_EVENT",0,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
1000000,10,"Second"
|
||||||
|
|
||||||
|
"WRPS_EVENT:AID.WRPS.PU303.RUN_HOURS","AID.WRPS.PU303.RUN_HOURS","WRPS_EVENT",0,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
1000000,10,"Second"
|
||||||
|
|
||||||
|
"WRPS_EVENT:AID.WRPS.SP.MODE","AID.WRPS.SP.MODE","WRPS_EVENT",0,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
1000000,10,"Second"
|
||||||
|
|
||||||
|
"WRPS_EVENT:AID.WRPS.SP.CMD_WORD","AID.WRPS.SP.CMD_WORD","WRPS_EVENT",0,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
1000000,10,"Second"
|
||||||
|
|
||||||
|
"WRPS_EVENT:AID.WRPS.SP.CMD_PARAM","AID.WRPS.SP.CMD_PARAM","WRPS_EVENT",0,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
1000000,10,"Second"
|
||||||
|
|
||||||
|
"WRPS_EVENT:AID.WRPS.SP.LEVEL_SP","AID.WRPS.SP.LEVEL_SP","WRPS_EVENT",0,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
1000000,10,"Second"
|
||||||
|
|
||||||
|
"WRPS_EVENT:AID.WRPS.SP.START_DUTY","AID.WRPS.SP.START_DUTY","WRPS_EVENT",0,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
1000000,10,"Second"
|
||||||
|
|
||||||
|
"WRPS_EVENT:AID.WRPS.SP.START_P2","AID.WRPS.SP.START_P2","WRPS_EVENT",0,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
1000000,10,"Second"
|
||||||
|
|
||||||
|
"WRPS_EVENT:AID.WRPS.SP.START_P3","AID.WRPS.SP.START_P3","WRPS_EVENT",0,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
1000000,10,"Second"
|
||||||
|
|
||||||
|
"WRPS_EVENT:AID.WRPS.SP.STOP_ALL","AID.WRPS.SP.STOP_ALL","WRPS_EVENT",0,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
1000000,10,"Second"
|
||||||
|
|
||||||
|
"WRPS_EVENT:AID.WRPS.SP.HIGH_ALARM","AID.WRPS.SP.HIGH_ALARM","WRPS_EVENT",0,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
1000000,10,"Second"
|
||||||
|
|
||||||
|
"WRPS_EVENT:AID.WRPS.SP.MIN_SPEED","AID.WRPS.SP.MIN_SPEED","WRPS_EVENT",0,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
1000000,10,"Second"
|
||||||
|
|
||||||
|
"WRPS_EVENT:AID.WRPS.SP.SERVICE_HRS","AID.WRPS.SP.SERVICE_HRS","WRPS_EVENT",0,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
1000000,10,"Second"
|
||||||
|
|
||||||
|
"WRPS_EVENT:AID.WRPS.SIM.INFLOW","AID.WRPS.SIM.INFLOW","WRPS_EVENT",0,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
1000000,10,"Second"
|
||||||
|
|
||||||
|
"WRPS_EVENT:AID.WRPS.SIM.SCENARIO","AID.WRPS.SIM.SCENARIO","WRPS_EVENT",0,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
1000000,10,"Second"
|
||||||
|
|
||||||
|
"WRPS_EVENT:AID.WRPS.SIM.RESET","AID.WRPS.SIM.RESET","WRPS_EVENT",0,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
1000000,10,"Second"
|
||||||
|
|
||||||
|
"WRPS_EVENT:AID.WRPS.SIM.TIME_SCALE","AID.WRPS.SIM.TIME_SCALE","WRPS_EVENT",0,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,1,0,\
|
||||||
|
0,0,0,0,0,0,\
|
||||||
|
1000000,10,"Second"
|
||||||
50
04-scada/modbus_points/scada-points.csv
Normal file
50
04-scada/modbus_points/scada-points.csv
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
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
|
||||||
|
1162
04-scada/modbus_points/wrps_item_df.qli
Normal file
1162
04-scada/modbus_points/wrps_item_df.qli
Normal file
File diff suppressed because it is too large
Load diff
362
04-scada/modbus_points/wrps_modbus_point_df.qli
Normal file
362
04-scada/modbus_points/wrps_modbus_point_df.qli
Normal file
|
|
@ -0,0 +1,362 @@
|
||||||
|
|
||||||
|
@LANGUAGE
|
||||||
|
ENGLISH
|
||||||
|
|
||||||
|
|
||||||
|
@VERSION
|
||||||
|
1.03.00
|
||||||
|
|
||||||
|
|
||||||
|
!==================================================================================================================================
|
||||||
|
|
||||||
|
@FIELDS
|
||||||
|
NAME,STATION,POINT,DESCRIPTION,IO_ADDRESS
|
||||||
|
EXTERNAL_RELATION,SCAN_TYPE,CONV_TYPE,DELTA_LIMIT,MAX_INSENS,OFFSET
|
||||||
|
PHYS_LOW,PHYS_HIGH,STEP,INVERS,HAS_SIGN,OVERFL_DET
|
||||||
|
SWAP_BYTES,SWAP_WORDS,ELEC_LOW,ELEC_HIGH,TMO_NONE,TMO_BOTH
|
||||||
|
AVE_UPD_INTERVAL,NO_ZERO,BURST_LIMIT,BITS,CHARS,DIGITS
|
||||||
|
FLOAT_TYPE,TIME_ZONE,TIME_REPRES,WLS_VAL_TYPE
|
||||||
|
|
||||||
|
@MODBUS_POINT_DF
|
||||||
|
"WRPS_PLC:PU301_RUN_CMD","WRPS_PLC","PU301_RUN_CMD","Run command","DO:01",\
|
||||||
|
"Input","MOD_SCAN","Digital",0,0,0,\
|
||||||
|
0,100,1,0,0,0,\
|
||||||
|
0,0,0,100,0,0,\
|
||||||
|
0,0,0,16,16,4,\
|
||||||
|
"Intel","Date+time GMT","7 bytes IEC","Float value"
|
||||||
|
|
||||||
|
"WRPS_PLC:PU302_RUN_CMD","WRPS_PLC","PU302_RUN_CMD","Run command","DO:02",\
|
||||||
|
"Input","MOD_SCAN","Digital",0,0,0,\
|
||||||
|
0,100,1,0,0,0,\
|
||||||
|
0,0,0,100,0,0,\
|
||||||
|
0,0,0,16,16,4,\
|
||||||
|
"Intel","Date+time GMT","7 bytes IEC","Float value"
|
||||||
|
|
||||||
|
"WRPS_PLC:PU303_RUN_CMD","WRPS_PLC","PU303_RUN_CMD","Run command","DO:03",\
|
||||||
|
"Input","MOD_SCAN","Digital",0,0,0,\
|
||||||
|
0,100,1,0,0,0,\
|
||||||
|
0,0,0,100,0,0,\
|
||||||
|
0,0,0,16,16,4,\
|
||||||
|
"Intel","Date+time GMT","7 bytes IEC","Float value"
|
||||||
|
|
||||||
|
"WRPS_PLC:PU301_RUNNING","WRPS_PLC","PU301_RUNNING","Running","DO:04",\
|
||||||
|
"Input","MOD_SCAN","Digital",0,0,0,\
|
||||||
|
0,100,1,0,0,0,\
|
||||||
|
0,0,0,100,0,0,\
|
||||||
|
0,0,0,16,16,4,\
|
||||||
|
"Intel","Date+time GMT","7 bytes IEC","Float value"
|
||||||
|
|
||||||
|
"WRPS_PLC:PU302_RUNNING","WRPS_PLC","PU302_RUNNING","Running","DO:05",\
|
||||||
|
"Input","MOD_SCAN","Digital",0,0,0,\
|
||||||
|
0,100,1,0,0,0,\
|
||||||
|
0,0,0,100,0,0,\
|
||||||
|
0,0,0,16,16,4,\
|
||||||
|
"Intel","Date+time GMT","7 bytes IEC","Float value"
|
||||||
|
|
||||||
|
"WRPS_PLC:PU303_RUNNING","WRPS_PLC","PU303_RUNNING","Running","DO:06",\
|
||||||
|
"Input","MOD_SCAN","Digital",0,0,0,\
|
||||||
|
0,100,1,0,0,0,\
|
||||||
|
0,0,0,100,0,0,\
|
||||||
|
0,0,0,16,16,4,\
|
||||||
|
"Intel","Date+time GMT","7 bytes IEC","Float value"
|
||||||
|
|
||||||
|
"WRPS_PLC:PU301_AVAILABLE","WRPS_PLC","PU301_AVAILABLE","Available","DO:07",\
|
||||||
|
"Input","MOD_SCAN","Digital",0,0,0,\
|
||||||
|
0,100,1,0,0,0,\
|
||||||
|
0,0,0,100,0,0,\
|
||||||
|
0,0,0,16,16,4,\
|
||||||
|
"Intel","Date+time GMT","7 bytes IEC","Float value"
|
||||||
|
|
||||||
|
"WRPS_PLC:PU302_AVAILABLE","WRPS_PLC","PU302_AVAILABLE","Available","DO:08",\
|
||||||
|
"Input","MOD_SCAN","Digital",0,0,0,\
|
||||||
|
0,100,1,0,0,0,\
|
||||||
|
0,0,0,100,0,0,\
|
||||||
|
0,0,0,16,16,4,\
|
||||||
|
"Intel","Date+time GMT","7 bytes IEC","Float value"
|
||||||
|
|
||||||
|
"WRPS_PLC:PU303_AVAILABLE","WRPS_PLC","PU303_AVAILABLE","Available","DO:09",\
|
||||||
|
"Input","MOD_SCAN","Digital",0,0,0,\
|
||||||
|
0,100,1,0,0,0,\
|
||||||
|
0,0,0,100,0,0,\
|
||||||
|
0,0,0,16,16,4,\
|
||||||
|
"Intel","Date+time GMT","7 bytes IEC","Float value"
|
||||||
|
|
||||||
|
"WRPS_PLC:STN_IN_AUTO","WRPS_PLC","STN_IN_AUTO","Station in auto","DO:10",\
|
||||||
|
"Input","MOD_SCAN","Digital",0,0,0,\
|
||||||
|
0,100,1,0,0,0,\
|
||||||
|
0,0,0,100,0,0,\
|
||||||
|
0,0,0,16,16,4,\
|
||||||
|
"Intel","Date+time GMT","7 bytes IEC","Float value"
|
||||||
|
|
||||||
|
"WRPS_PLC:STN_HIGH_LEVEL","WRPS_PLC","STN_HIGH_LEVEL","High level alarm","DO:11",\
|
||||||
|
"Input","MOD_SCAN","Digital",0,0,0,\
|
||||||
|
0,100,1,0,0,0,\
|
||||||
|
0,0,0,100,0,0,\
|
||||||
|
0,0,0,16,16,4,\
|
||||||
|
"Intel","Date+time GMT","7 bytes IEC","Float value"
|
||||||
|
|
||||||
|
"WRPS_PLC:STN_SPILL_ACTIVE","WRPS_PLC","STN_SPILL_ACTIVE","Spill active","DO:12",\
|
||||||
|
"Input","MOD_SCAN","Digital",0,0,0,\
|
||||||
|
0,100,1,0,0,0,\
|
||||||
|
0,0,0,100,0,0,\
|
||||||
|
0,0,0,16,16,4,\
|
||||||
|
"Intel","Date+time GMT","7 bytes IEC","Float value"
|
||||||
|
|
||||||
|
"WRPS_PLC:PU301_TRIPPED","WRPS_PLC","PU301_TRIPPED","Tripped","DO:13",\
|
||||||
|
"Input","MOD_SCAN","Digital",0,0,0,\
|
||||||
|
0,100,1,0,0,0,\
|
||||||
|
0,0,0,100,0,0,\
|
||||||
|
0,0,0,16,16,4,\
|
||||||
|
"Intel","Date+time GMT","7 bytes IEC","Float value"
|
||||||
|
|
||||||
|
"WRPS_PLC:PU302_TRIPPED","WRPS_PLC","PU302_TRIPPED","Tripped","DO:14",\
|
||||||
|
"Input","MOD_SCAN","Digital",0,0,0,\
|
||||||
|
0,100,1,0,0,0,\
|
||||||
|
0,0,0,100,0,0,\
|
||||||
|
0,0,0,16,16,4,\
|
||||||
|
"Intel","Date+time GMT","7 bytes IEC","Float value"
|
||||||
|
|
||||||
|
"WRPS_PLC:PU303_TRIPPED","WRPS_PLC","PU303_TRIPPED","Tripped","DO:15",\
|
||||||
|
"Input","MOD_SCAN","Digital",0,0,0,\
|
||||||
|
0,100,1,0,0,0,\
|
||||||
|
0,0,0,100,0,0,\
|
||||||
|
0,0,0,16,16,4,\
|
||||||
|
"Intel","Date+time GMT","7 bytes IEC","Float value"
|
||||||
|
|
||||||
|
"WRPS_PLC:STN_LEVEL","WRPS_PLC","STN_LEVEL","Wet well level","RO:01",\
|
||||||
|
"Input","MOD_SCAN","Linear",0,0,0,\
|
||||||
|
-546,546,1,0,1,0,\
|
||||||
|
0,0,-32760,32760,0,0,\
|
||||||
|
0,0,0,16,16,4,\
|
||||||
|
"Intel","Date+time GMT","7 bytes IEC","Float value"
|
||||||
|
|
||||||
|
"WRPS_PLC:STN_INFLOW","WRPS_PLC","STN_INFLOW","Inflow","RO:02",\
|
||||||
|
"Input","MOD_SCAN","Linear",0,0,0,\
|
||||||
|
-11796.48,11796.12,1,0,1,0,\
|
||||||
|
0,0,-32768,32767,0,0,\
|
||||||
|
0,0,0,16,16,4,\
|
||||||
|
"Intel","Date+time GMT","7 bytes IEC","Float value"
|
||||||
|
|
||||||
|
"WRPS_PLC:STN_DISCHARGE","WRPS_PLC","STN_DISCHARGE","Total discharge flow","RO:03",\
|
||||||
|
"Input","MOD_SCAN","Linear",0,0,0,\
|
||||||
|
-11796.48,11796.12,1,0,1,0,\
|
||||||
|
0,0,-32768,32767,0,0,\
|
||||||
|
0,0,0,16,16,4,\
|
||||||
|
"Intel","Date+time GMT","7 bytes IEC","Float value"
|
||||||
|
|
||||||
|
"WRPS_PLC:STN_PUMPS_RUNNING","WRPS_PLC","STN_PUMPS_RUNNING","Pumps running","RO:04",\
|
||||||
|
"Input","MOD_SCAN","Linear",0,0,0,\
|
||||||
|
-32768,32767,1,0,1,0,\
|
||||||
|
0,0,-32768,32767,0,0,\
|
||||||
|
0,0,0,16,16,4,\
|
||||||
|
"Intel","Date+time GMT","7 bytes IEC","Float value"
|
||||||
|
|
||||||
|
"WRPS_PLC:STN_SPEED","WRPS_PLC","STN_SPEED","Common drive speed","RO:05",\
|
||||||
|
"Input","MOD_SCAN","Linear",0,0,0,\
|
||||||
|
-6553,6553,1,0,1,0,\
|
||||||
|
0,0,-32765,32765,0,0,\
|
||||||
|
0,0,0,16,16,4,\
|
||||||
|
"Intel","Date+time GMT","7 bytes IEC","Float value"
|
||||||
|
|
||||||
|
"WRPS_PLC:STN_TIME_TO_SPILL","WRPS_PLC","STN_TIME_TO_SPILL","Time to spill weir (32767 = drawing down)","RO:06",\
|
||||||
|
"Input","MOD_SCAN","Linear",0,0,0,\
|
||||||
|
-32768,32767,1,0,1,0,\
|
||||||
|
0,0,-32768,32767,0,0,\
|
||||||
|
0,0,0,16,16,4,\
|
||||||
|
"Intel","Date+time GMT","7 bytes IEC","Float value"
|
||||||
|
|
||||||
|
"WRPS_PLC:STN_TIME_TO_LSHH","WRPS_PLC","STN_TIME_TO_LSHH","Time to LSHH (32767 = drawing down)","RO:07",\
|
||||||
|
"Input","MOD_SCAN","Linear",0,0,0,\
|
||||||
|
-32768,32767,1,0,1,0,\
|
||||||
|
0,0,-32768,32767,0,0,\
|
||||||
|
0,0,0,16,16,4,\
|
||||||
|
"Intel","Date+time GMT","7 bytes IEC","Float value"
|
||||||
|
|
||||||
|
"WRPS_PLC:STN_NET_ACCUM","WRPS_PLC","STN_NET_ACCUM","Net accumulation (signed)","RO:08",\
|
||||||
|
"Input","MOD_SCAN","Linear",0,0,0,\
|
||||||
|
-11796.48,11796.12,1,0,1,0,\
|
||||||
|
0,0,-32768,32767,0,0,\
|
||||||
|
0,0,0,16,16,4,\
|
||||||
|
"Intel","Date+time GMT","7 bytes IEC","Float value"
|
||||||
|
|
||||||
|
"WRPS_PLC:PU301_RUN_HOURS","WRPS_PLC","PU301_RUN_HOURS","Run hours","RO:09",\
|
||||||
|
"Input","MOD_SCAN","Linear",0,0,0,\
|
||||||
|
-32768,32767,1,0,1,0,\
|
||||||
|
0,0,-32768,32767,0,0,\
|
||||||
|
0,0,0,16,16,4,\
|
||||||
|
"Intel","Date+time GMT","7 bytes IEC","Float value"
|
||||||
|
|
||||||
|
"WRPS_PLC:PU302_RUN_HOURS","WRPS_PLC","PU302_RUN_HOURS","Run hours","RO:10",\
|
||||||
|
"Input","MOD_SCAN","Linear",0,0,0,\
|
||||||
|
-32768,32767,1,0,1,0,\
|
||||||
|
0,0,-32768,32767,0,0,\
|
||||||
|
0,0,0,16,16,4,\
|
||||||
|
"Intel","Date+time GMT","7 bytes IEC","Float value"
|
||||||
|
|
||||||
|
"WRPS_PLC:PU303_RUN_HOURS","WRPS_PLC","PU303_RUN_HOURS","Run hours","RO:11",\
|
||||||
|
"Input","MOD_SCAN","Linear",0,0,0,\
|
||||||
|
-32768,32767,1,0,1,0,\
|
||||||
|
0,0,-32768,32767,0,0,\
|
||||||
|
0,0,0,16,16,4,\
|
||||||
|
"Intel","Date+time GMT","7 bytes IEC","Float value"
|
||||||
|
|
||||||
|
"WRPS_PLC:STN_VOL_TO_SPILL","WRPS_PLC","STN_VOL_TO_SPILL","Volume remaining to spill","RO:12",\
|
||||||
|
"Input","MOD_SCAN","Linear",0,0,0,\
|
||||||
|
-32768,32767,1,0,1,0,\
|
||||||
|
0,0,-32768,32767,0,0,\
|
||||||
|
0,0,0,16,16,4,\
|
||||||
|
"Intel","Date+time GMT","7 bytes IEC","Float value"
|
||||||
|
|
||||||
|
"WRPS_PLC:STN_STATE","WRPS_PLC","STN_STATE","Station state (enum 3.1)","RO:13",\
|
||||||
|
"Input","MOD_SCAN","Linear",0,0,0,\
|
||||||
|
-32768,32767,1,0,1,0,\
|
||||||
|
0,0,-32768,32767,0,0,\
|
||||||
|
0,0,0,16,16,4,\
|
||||||
|
"Intel","Date+time GMT","7 bytes IEC","Float value"
|
||||||
|
|
||||||
|
"WRPS_PLC:PU301_STATE","WRPS_PLC","PU301_STATE","Pump state (enum 3.2)","RO:14",\
|
||||||
|
"Input","MOD_SCAN","Linear",0,0,0,\
|
||||||
|
-32768,32767,1,0,1,0,\
|
||||||
|
0,0,-32768,32767,0,0,\
|
||||||
|
0,0,0,16,16,4,\
|
||||||
|
"Intel","Date+time GMT","7 bytes IEC","Float value"
|
||||||
|
|
||||||
|
"WRPS_PLC:PU302_STATE","WRPS_PLC","PU302_STATE","Pump state (enum 3.2)","RO:15",\
|
||||||
|
"Input","MOD_SCAN","Linear",0,0,0,\
|
||||||
|
-32768,32767,1,0,1,0,\
|
||||||
|
0,0,-32768,32767,0,0,\
|
||||||
|
0,0,0,16,16,4,\
|
||||||
|
"Intel","Date+time GMT","7 bytes IEC","Float value"
|
||||||
|
|
||||||
|
"WRPS_PLC:PU303_STATE","WRPS_PLC","PU303_STATE","Pump state (enum 3.2)","RO:16",\
|
||||||
|
"Input","MOD_SCAN","Linear",0,0,0,\
|
||||||
|
-32768,32767,1,0,1,0,\
|
||||||
|
0,0,-32768,32767,0,0,\
|
||||||
|
0,0,0,16,16,4,\
|
||||||
|
"Intel","Date+time GMT","7 bytes IEC","Float value"
|
||||||
|
|
||||||
|
"WRPS_PLC:STN_DUTY_PUMP","WRPS_PLC","STN_DUTY_PUMP","Current duty pump (0 = none, 1-3)","RO:17",\
|
||||||
|
"Input","MOD_SCAN","Linear",0,0,0,\
|
||||||
|
-32768,32767,1,0,1,0,\
|
||||||
|
0,0,-32768,32767,0,0,\
|
||||||
|
0,0,0,16,16,4,\
|
||||||
|
"Intel","Date+time GMT","7 bytes IEC","Float value"
|
||||||
|
|
||||||
|
"WRPS_PLC:STN_ALARM_WORD","WRPS_PLC","STN_ALARM_WORD","Alarm bitmask (section 6) - READ AS UNSIGNED","RO:18",\
|
||||||
|
"Input","MOD_SCAN","Linear",0,0,0,\
|
||||||
|
0,65535,1,0,0,0,\
|
||||||
|
0,0,0,65535,0,0,\
|
||||||
|
0,0,0,16,16,4,\
|
||||||
|
"Intel","Date+time GMT","7 bytes IEC","Float value"
|
||||||
|
|
||||||
|
"WRPS_PLC:STN_CMD_ACK","WRPS_PLC","STN_CMD_ACK","Command acknowledge (echoes %MW1)","RO:21",\
|
||||||
|
"Input","MOD_SCAN","Linear",0,0,0,\
|
||||||
|
-32768,32767,1,0,1,0,\
|
||||||
|
0,0,-32768,32767,0,0,\
|
||||||
|
0,0,0,16,16,4,\
|
||||||
|
"Intel","Date+time GMT","7 bytes IEC","Float value"
|
||||||
|
|
||||||
|
"WRPS_PLC:SP_MODE","WRPS_PLC","SP_MODE","Station mode: 1 = auto, 2 = off","RO:1025",\
|
||||||
|
"Input + Output","MOD_SCAN","Linear",0,0,0,\
|
||||||
|
-32768,32767,1,0,1,0,\
|
||||||
|
0,0,-32768,32767,0,0,\
|
||||||
|
0,0,0,16,16,4,\
|
||||||
|
"Intel","Date+time GMT","7 bytes IEC","Float value"
|
||||||
|
|
||||||
|
"WRPS_PLC:SP_CMD_WORD","WRPS_PLC","SP_CMD_WORD","Command word (section 3.3)","RO:1026",\
|
||||||
|
"Input + Output","MOD_SCAN","Linear",0,0,0,\
|
||||||
|
-32768,32767,1,0,1,0,\
|
||||||
|
0,0,-32768,32767,0,0,\
|
||||||
|
0,0,0,16,16,4,\
|
||||||
|
"Intel","Date+time GMT","7 bytes IEC","Float value"
|
||||||
|
|
||||||
|
"WRPS_PLC:SP_CMD_PARAM","WRPS_PLC","SP_CMD_PARAM","Command parameter (pump number)","RO:1027",\
|
||||||
|
"Input + Output","MOD_SCAN","Linear",0,0,0,\
|
||||||
|
-32768,32767,1,0,1,0,\
|
||||||
|
0,0,-32768,32767,0,0,\
|
||||||
|
0,0,0,16,16,4,\
|
||||||
|
"Intel","Date+time GMT","7 bytes IEC","Float value"
|
||||||
|
|
||||||
|
"WRPS_PLC:SP_LEVEL_SP","WRPS_PLC","SP_LEVEL_SP","Level control setpoint","RO:1028",\
|
||||||
|
"Input + Output","MOD_SCAN","Linear",0,0,0,\
|
||||||
|
-546,546,1,0,1,0,\
|
||||||
|
0,0,-32760,32760,0,0,\
|
||||||
|
0,0,0,16,16,4,\
|
||||||
|
"Intel","Date+time GMT","7 bytes IEC","Float value"
|
||||||
|
|
||||||
|
"WRPS_PLC:SP_START_DUTY","WRPS_PLC","SP_START_DUTY","Start duty level","RO:1029",\
|
||||||
|
"Input + Output","MOD_SCAN","Linear",0,0,0,\
|
||||||
|
-546,546,1,0,1,0,\
|
||||||
|
0,0,-32760,32760,0,0,\
|
||||||
|
0,0,0,16,16,4,\
|
||||||
|
"Intel","Date+time GMT","7 bytes IEC","Float value"
|
||||||
|
|
||||||
|
"WRPS_PLC:SP_START_P2","WRPS_PLC","SP_START_P2","Start pump 2 level","RO:1030",\
|
||||||
|
"Input + Output","MOD_SCAN","Linear",0,0,0,\
|
||||||
|
-546,546,1,0,1,0,\
|
||||||
|
0,0,-32760,32760,0,0,\
|
||||||
|
0,0,0,16,16,4,\
|
||||||
|
"Intel","Date+time GMT","7 bytes IEC","Float value"
|
||||||
|
|
||||||
|
"WRPS_PLC:SP_START_P3","WRPS_PLC","SP_START_P3","Start pump 3 level","RO:1031",\
|
||||||
|
"Input + Output","MOD_SCAN","Linear",0,0,0,\
|
||||||
|
-546,546,1,0,1,0,\
|
||||||
|
0,0,-32760,32760,0,0,\
|
||||||
|
0,0,0,16,16,4,\
|
||||||
|
"Intel","Date+time GMT","7 bytes IEC","Float value"
|
||||||
|
|
||||||
|
"WRPS_PLC:SP_STOP_ALL","WRPS_PLC","SP_STOP_ALL","Stop all level","RO:1032",\
|
||||||
|
"Input + Output","MOD_SCAN","Linear",0,0,0,\
|
||||||
|
-546,546,1,0,1,0,\
|
||||||
|
0,0,-32760,32760,0,0,\
|
||||||
|
0,0,0,16,16,4,\
|
||||||
|
"Intel","Date+time GMT","7 bytes IEC","Float value"
|
||||||
|
|
||||||
|
"WRPS_PLC:SP_HIGH_ALARM","WRPS_PLC","SP_HIGH_ALARM","High level alarm","RO:1033",\
|
||||||
|
"Input + Output","MOD_SCAN","Linear",0,0,0,\
|
||||||
|
-546,546,1,0,1,0,\
|
||||||
|
0,0,-32760,32760,0,0,\
|
||||||
|
0,0,0,16,16,4,\
|
||||||
|
"Intel","Date+time GMT","7 bytes IEC","Float value"
|
||||||
|
|
||||||
|
"WRPS_PLC:SP_MIN_SPEED","WRPS_PLC","SP_MIN_SPEED","Minimum drive speed","RO:1034",\
|
||||||
|
"Input + Output","MOD_SCAN","Linear",0,0,0,\
|
||||||
|
-6553,6553,1,0,1,0,\
|
||||||
|
0,0,-32765,32765,0,0,\
|
||||||
|
0,0,0,16,16,4,\
|
||||||
|
"Intel","Date+time GMT","7 bytes IEC","Float value"
|
||||||
|
|
||||||
|
"WRPS_PLC:SP_SERVICE_HRS","WRPS_PLC","SP_SERVICE_HRS","Service interval","RO:1035",\
|
||||||
|
"Input + Output","MOD_SCAN","Linear",0,0,0,\
|
||||||
|
-32768,32767,1,0,1,0,\
|
||||||
|
0,0,-32768,32767,0,0,\
|
||||||
|
0,0,0,16,16,4,\
|
||||||
|
"Intel","Date+time GMT","7 bytes IEC","Float value"
|
||||||
|
|
||||||
|
"WRPS_PLC:SIM_INFLOW","WRPS_PLC","SIM_INFLOW","manual inflow (mode 0)","RO:1045",\
|
||||||
|
"Input + Output","MOD_SCAN","Linear",0,0,0,\
|
||||||
|
-11796.48,11796.12,1,0,1,0,\
|
||||||
|
0,0,-32768,32767,0,0,\
|
||||||
|
0,0,0,16,16,4,\
|
||||||
|
"Intel","Date+time GMT","7 bytes IEC","Float value"
|
||||||
|
|
||||||
|
"WRPS_PLC:SIM_SCENARIO","WRPS_PLC","SIM_SCENARIO","scenario 0=man 1=diurnal 2=wet 3=ref","RO:1046",\
|
||||||
|
"Input + Output","MOD_SCAN","Linear",0,0,0,\
|
||||||
|
-32768,32767,1,0,1,0,\
|
||||||
|
0,0,-32768,32767,0,0,\
|
||||||
|
0,0,0,16,16,4,\
|
||||||
|
"Intel","Date+time GMT","7 bytes IEC","Float value"
|
||||||
|
|
||||||
|
"WRPS_PLC:SIM_RESET","WRPS_PLC","SIM_RESET","write 1 to reset scenario, self-clearing","RO:1047",\
|
||||||
|
"Input + Output","MOD_SCAN","Linear",0,0,0,\
|
||||||
|
-32768,32767,1,0,1,0,\
|
||||||
|
0,0,-32768,32767,0,0,\
|
||||||
|
0,0,0,16,16,4,\
|
||||||
|
"Intel","Date+time GMT","7 bytes IEC","Float value"
|
||||||
|
|
||||||
|
"WRPS_PLC:SIM_TIME_SCALE","WRPS_PLC","SIM_TIME_SCALE","time scale 1-120","RO:1048",\
|
||||||
|
"Input + Output","MOD_SCAN","Linear",0,0,0,\
|
||||||
|
-32768,32767,1,0,1,0,\
|
||||||
|
0,0,-32768,32767,0,0,\
|
||||||
|
0,0,0,16,16,4,\
|
||||||
|
"Intel","Date+time GMT","7 bytes IEC","Float value"
|
||||||
40
04-scada/modbus_points/wrps_section_df.qli
Normal file
40
04-scada/modbus_points/wrps_section_df.qli
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
|
||||||
|
@LANGUAGE
|
||||||
|
ENGLISH
|
||||||
|
|
||||||
|
|
||||||
|
@VERSION
|
||||||
|
1.03.00
|
||||||
|
|
||||||
|
|
||||||
|
!==================================================================================================================================
|
||||||
|
|
||||||
|
@FIELDS
|
||||||
|
NAME,SECTION_PATH,SECTION_NAME,BLOCKED,PARENT_BLOCKED
|
||||||
|
ALARM_INHIBIT,PARENT_ALARM_INHIBIT,OPC_VISIBLE,PARENT_OPC_VISIBLE,NSID,PARENT_NSID
|
||||||
|
DESCRIPTION,CREATED_BY
|
||||||
|
|
||||||
|
@SECTION_DF
|
||||||
|
"AID.WRPS.STN","AID.WRPS","STN",0,0,\
|
||||||
|
0,0,0,0,171,170,\
|
||||||
|
"Waterloo Road PS - station wide measurements and status","unknown"
|
||||||
|
|
||||||
|
"AID.WRPS.PU301","AID.WRPS","PU301",0,0,\
|
||||||
|
0,0,0,0,172,170,\
|
||||||
|
"Pump PU-301","unknown"
|
||||||
|
|
||||||
|
"AID.WRPS.PU302","AID.WRPS","PU302",0,0,\
|
||||||
|
0,0,0,0,173,170,\
|
||||||
|
"Pump PU-302","unknown"
|
||||||
|
|
||||||
|
"AID.WRPS.PU303","AID.WRPS","PU303",0,0,\
|
||||||
|
0,0,0,0,174,170,\
|
||||||
|
"Pump PU-303","unknown"
|
||||||
|
|
||||||
|
"AID.WRPS.SP","AID.WRPS","SP",0,0,\
|
||||||
|
0,0,0,0,175,170,\
|
||||||
|
"Operator setpoints and commands","unknown"
|
||||||
|
|
||||||
|
"AID.WRPS.SIM","AID.WRPS","SIM",0,0,\
|
||||||
|
0,0,0,0,176,170,\
|
||||||
|
"Simulation control - simulation build only","unknown"
|
||||||
Loading…
Add table
Reference in a new issue