wrps-demo-kit/CLAUDE.md
Clio Liu 5be48b6bbb fix: CI Server on cicore1 is R1.05, not R1.03
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.
2026-09-02 17:34:58 +10:00

186 lines
8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 01023; `%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`.