Corrected in README.md, CLAUDE.md, 02-environment/README.md and 03-plc/VERSIONS.md. The @VERSION 1.03.00 header inside the .qli files is the quickload FILE FORMAT version, not the CI Server release. Left alone. This also sharpens a caveat that was previously stated wrongly. The EQP-E-DUP_ITEM rejection - an item import fails if two items share an IO_ADDRESS - was observed on 2026-08-14 on the DEVELOPMENT machine, which ran R1.03. Production runs R1.05, where it has never been tested. The design still works around it by publishing level and speed in % only, and that workaround costs nothing; but anyone wanting a second unit for a measurement should try it on R1.05 before assuming they need a second PLC register to carry it.
186 lines
8 KiB
Markdown
186 lines
8 KiB
Markdown
# CLAUDE.md — wrps-demo-kit
|
||
|
||
Instructions for Claude Code working in this repository. Human-facing orientation
|
||
is in `README.md`; read that first, then this.
|
||
|
||
---
|
||
|
||
> [!IMPORTANT]
|
||
> ## 🪜 Working protocol — one small step at a time
|
||
>
|
||
> This project is built **incrementally, with every step tested and confirmed
|
||
> before the next one starts.** Do not batch phases or run ahead.
|
||
>
|
||
> 1. **State the plan** — what you are about to change, and the **acceptance test**
|
||
> that will prove it works.
|
||
> 2. **Make the minimal change** for that step alone.
|
||
> 3. **Test it and show the evidence** — command output, a Modbus read, a
|
||
> container log. Not a claim.
|
||
> 4. **Wait for the user to confirm** before starting the next step. Never proceed
|
||
> on an untested or failing step; fix and re-test.
|
||
> 5. After each confirmed step, **commit** with a clear message.
|
||
>
|
||
> If a step is too big for one testable change, split it. When unsure whether
|
||
> something counts as done, ask.
|
||
|
||
---
|
||
|
||
## 1. What this is
|
||
|
||
A reusable method for building demo automation use cases on **Yokogawa CI Server**
|
||
(SCADA) and **OpenPLC Runtime v4** (soft PLC, in Docker), with the Waterloo Road
|
||
Pump Station as its worked example.
|
||
|
||
**The demo is live and in use.** A simulated three-pump station runs on the PLC and
|
||
is polled by CI Server. Treat everything on those machines as production.
|
||
|
||
## 2. The two machines
|
||
|
||
| Role | Host | Address |
|
||
|---|---|---|
|
||
| SCADA — CI Server R1.05 | `yau-poc-cicore1` | `10.0.0.21` |
|
||
| PLC — OpenPLC Runtime v4 container | `yau-sls-poc-lin001` | `10.0.0.17`, public `20.211.144.151` |
|
||
|
||
SSH alias `lin001` → `azureuser@20.211.144.151`. `10.0.0.17` is only routable from
|
||
inside the VNet or over the WireGuard VPN.
|
||
|
||
> [!WARNING]
|
||
> **`yau-sls-poc-lin001` is a shared, live host** running ~28 containers for
|
||
> several unrelated projects behind Caddy and Authelia, serving customer-facing
|
||
> demos. **This project owns exactly one container, `openplc-runtime`.**
|
||
>
|
||
> Never restart Caddy or Authelia. Never publish a port on `0.0.0.0` — the host has
|
||
> a public IP and no firewall (`ufw` inactive; the Azure NSG governs). Never put
|
||
> growing data on the 62 GB root disk.
|
||
|
||
Full detail: `02-environment/README.md`. The host owner's own brief, including
|
||
rules that exist because breaking them caused outages:
|
||
`02-environment/YAU_Linux_Host_Onboarding.md`.
|
||
|
||
## 3. Architecture invariant — do not violate
|
||
|
||
**OpenPLC Runtime is the Modbus TCP _server_ (slave) on port 502. CI Server is the
|
||
_client_ (master) and polls it.** Never the reverse. The PLC must never be
|
||
configured as a Modbus master.
|
||
|
||
Modbus unit / slave id: `1`.
|
||
|
||
## 4. The generation chain — never hand-edit downstream
|
||
|
||
```
|
||
03-plc/src/*.st ─build.py─► 03-plc/register-map.csv ─gen_scada_points.py─►
|
||
04-scada/modbus_points/ci-server-points.csv ─gen_ciserver_qli.py─► *.qli
|
||
```
|
||
|
||
To change an address, type or scaling: edit the **`REGISTERS` table in
|
||
`03-plc/build.py`**, rebuild, then re-run both SCADA generators. Propagate all the
|
||
way or the two sides disagree silently — the reads still succeed, they are just
|
||
wrong.
|
||
|
||
Every generated file reproduces byte-identically. **If a regeneration produces a
|
||
diff you did not intend, stop and find out why** before committing it.
|
||
|
||
Hand-maintained exceptions, both documented in place:
|
||
`04-scada/modbus_points/historian/` and `03-plc/editor-devices/`.
|
||
|
||
## 5. Things that are true and non-obvious
|
||
|
||
Getting any of these wrong produces plausible-looking wrong answers rather than
|
||
errors.
|
||
|
||
- **`%MW` is not holding register 0.** `%QW` occupies HR 0–1023; `%MW` starts at
|
||
**HR 1024**, so `%MW3` is HR 1027. A wrong-by-1024 read succeeds and returns
|
||
nonsense.
|
||
- **`%QW17` (alarm bitmask) is unsigned.** Bit 15 does not fit a signed INT.
|
||
`%QW7` is genuinely signed.
|
||
- **The deployed build is the SIMULATION build.** All `%IW`/`%IX` read 0 — correct,
|
||
the mux takes simulated values. Do not poll FC04/FC02 expecting live data.
|
||
- **Port 502 answers only while a program is running** *and* the Editor project
|
||
defines a Modbus Server. A refusal looks like a firewall drop and is not.
|
||
- **STruC++ ships only inside the OpenPLC Editor GUI.** There is no CLI. Any change
|
||
to PLC logic goes through the Editor — and **the Editor is not currently
|
||
installed anywhere** (`03-plc/DEPLOY.md` §0).
|
||
- **The runtime image was made with `docker commit`** and exists in no registry.
|
||
Deleting it loses the compiled program.
|
||
- **Every CI Server item import renumbers every item**, killing every display. The
|
||
fix is to **validate the display in CI Server's Editor Module** — one action,
|
||
before any other diagnosis.
|
||
- **`PS_*` names the four Modbus poll groups and nothing else.** Placeholder `PS_`
|
||
*tag* names were removed; they named nothing that exists and caused a real
|
||
defect. CI Server items are `AID.WRPS.<SECTION>.<LEAF>`.
|
||
- **Live setpoint tuning is not in this repo.** Operators retune over Modbus; those
|
||
values live only in the container volume and revert on restart.
|
||
|
||
## 6. Guardrails
|
||
|
||
**MAY do freely:** edit repo files, SSH to `lin001` for **read-only** inspection
|
||
(`docker ps`, `docker logs`, `docker inspect`, reading files), run the generators,
|
||
run `05-tests/` against a fake PLC or a live read.
|
||
|
||
**MUST ask first:**
|
||
- anything that stops, restarts, recreates or updates the `openplc-runtime`
|
||
container — **it is live control and CI Server is polling it**
|
||
- anything touching another container on that host, or any host-level config
|
||
- changing the register map — it is a contract with a system you cannot see
|
||
- writing to `%MW` on the live PLC (setpoints and the command word are real
|
||
commands; `%MW1` executes on a rising edge)
|
||
- pushing to `origin`, or any history rewrite
|
||
|
||
**MUST NOT:**
|
||
- advance past an untested or unconfirmed step
|
||
- commit secrets — `*.local.md`, `*.pem`, tokens
|
||
- edit `99-reference/`, or `03-plc/as-built/`
|
||
- hand-edit any generated file
|
||
- change CI Server configuration — the user does that; you prepare the files
|
||
- widen a port bind to `0.0.0.0`
|
||
|
||
## 7. Verification, not assertion
|
||
|
||
The user is an automation engineer auditing this work. **Show evidence.**
|
||
|
||
- Read the live PLC rather than inferring its state.
|
||
- After changing a generator, prove the output still reproduces.
|
||
- Say plainly when something is unverified. `yau-poc-cicore1` is **not reachable**
|
||
from here — anything about CI Server's own state is belief, not fact, and must be
|
||
labelled as such.
|
||
- Do not describe a step as done until its acceptance test has passed.
|
||
|
||
## 8. Key commands
|
||
|
||
```bash
|
||
# the PLC container (read-only)
|
||
ssh lin001 "docker ps --filter name=openplc-runtime"
|
||
ssh lin001 "docker logs --tail 50 openplc-runtime"
|
||
|
||
# poll the live PLC from inside its own network (10.0.0.17 is not routable here)
|
||
ssh lin001 "docker run --rm --network openplc-net python:3.12-alpine \
|
||
sh -c 'pip install -q pymodbus && python -c \"...\"'"
|
||
|
||
# rebuild the chain
|
||
cd 03-plc && python build.py --mode sim
|
||
cd 04-scada/modbus_points && python gen_scada_points.py && python gen_ciserver_qli.py
|
||
cd 04-scada/hmi && python build_display.py
|
||
|
||
# verify the map (needs a route to 10.0.0.17, or use fake_plc.py locally)
|
||
cd 05-tests && python verify_modbus.py --host 10.0.0.17 --port 502 --unit 1
|
||
```
|
||
|
||
## 9. Conventions
|
||
|
||
- **ST style:** IEC keywords uppercase, 4-space indent, one action per line, a
|
||
`(* ... *)` header on every POU. No vendor extensions.
|
||
- **Control and simulation never interleave.** Separate POUs; only `10_globals.st`
|
||
and `50_prog_io_mux.st` may touch located variables, and `build.py` enforces it.
|
||
- **CI Server items:** `AID.WRPS.<SECTION>.<LEAF>`.
|
||
- **Commits:** one per confirmed step. Say what was proven, not just what changed.
|
||
|
||
## 10. Open items
|
||
|
||
Do not present these as solved. `README.md` carries the full list; the two that
|
||
most often catch people out:
|
||
|
||
1. **None of the 20 acceptance tests in `01-design/WRPS-CTL-003` have ever been
|
||
run.** The program compiles and publishes correct registers; its control
|
||
behaviour is unverified.
|
||
2. **There is no route to deploy a new program** until the OpenPLC Editor is
|
||
installed somewhere that can reach `10.0.0.17:8443`.
|