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.
8 KiB
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.
- State the plan — what you are about to change, and the acceptance test that will prove it works.
- Make the minimal change for that step alone.
- Test it and show the evidence — command output, a Modbus read, a container log. Not a claim.
- Wait for the user to confirm before starting the next step. Never proceed on an untested or failing step; fix and re-test.
- 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-lin001is 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 (ufwinactive; 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.
%MWis not holding register 0.%QWoccupies HR 0–1023;%MWstarts at HR 1024, so%MW3is HR 1027. A wrong-by-1024 read succeeds and returns nonsense.%QW17(alarm bitmask) is unsigned. Bit 15 does not fit a signed INT.%QW7is genuinely signed.- The deployed build is the SIMULATION build. All
%IW/%IXread 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 commitand 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. PlaceholderPS_tag names were removed; they named nothing that exists and caused a real defect. CI Server items areAID.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-runtimecontainer — 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
%MWon the live PLC (setpoints and the command word are real commands;%MW1executes 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/, or03-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-cicore1is 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
# 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.stand50_prog_io_mux.stmay touch located variables, andbuild.pyenforces 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:
- None of the 20 acceptance tests in
01-design/WRPS-CTL-003have ever been run. The program compiles and publishes correct registers; its control behaviour is unverified. - There is no route to deploy a new program until the OpenPLC Editor is
installed somewhere that can reach
10.0.0.17:8443.