# 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.
.`. - **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.
.`. - **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`.