wrps-demo-kit/03-plc/GETTING-STARTED.md
Clio Liu 55279ca78f docs(plc): as-built copy, versions, and a getting-started guide
Adds what the folder was missing for someone picking it up cold.

as-built/  the STruC++ output copied out of the live container - the C++
           the PLC is actually executing, plus program.st and the Modbus
           buffer config. The running image was made with docker commit
           and exists in no registry, so this is the only other copy of
           the compiled form.

           Verified while copying: the deployed program.st matches
           build/wrps.st generated from src/. Identical POU structure,
           ZERO differences in non-declaration lines. src/ is genuinely
           canonical and the running PLC agrees with it.

VERSIONS.md         every version read from the running system, not from
                    documentation: runtime v4.1.10, STruC++ 0.6.2, Editor
                    4.2.11, Debian 12, g++ 12.2, pymodbus 3.11.2, CI
                    Server R1.03. Plus which plugins are enabled -
                    ethercat is on for no reason - and a v3-vs-v4 table,
                    since most OpenPLC guidance online is for v3 and the
                    %MW HR1024 change silently produces wrong data.

GETTING-STARTED.md  three questions answered in a page: how to modify the
                    program, how to stand up a new PLC container from
                    scratch, how to move this one. Includes the compose
                    file, the Editor steps that are not generated and are
                    always missed, and a symptom-to-document table.
2026-09-02 16:48:30 +10:00

6.7 KiB

Getting started — three things you might need to do

Short answers. Detail is in DEPLOY.md, VERSIONS.md and README.md.

Before anything else, know the two hard facts about this setup:

  1. The ST → C++ compiler (STruC++) ships only inside the OpenPLC Editor GUI. There is no CLI, no library, no container image of it. Any change to the PLC logic goes through the Editor. Nothing else will do it.
  2. The running PLC's image was made with docker commit and holds the compiled program in its writable layer. It is not in a registry and cannot be re-pulled.

1. I need to modify the PLC program

What you need: OpenPLC Editor v4.2.11, on a machine with a graphical desktop that can reach 10.0.0.17:8443 (LAN or WireGuard VPN).

⚠️ The Editor is not currently installed anywhere. It lived on the retired dev-ubuntu host. Installing it is step one — see DEPLOY.md §0.

Then:

# 1. edit the ST - this is the only place you edit
vi src/30_prog_control.st

# 2. rebuild. This also regenerates register-map.csv
python build.py --mode sim

# 3. generate the Editor project
python gen_project.py --mode sim --out editor-project

Copy editor-project/project.json and editor-project/pous/ into the Editor project folder — with the Editor closed — then open it, Build, and Start PLC. DEPLOY.md §B is the full procedure; every warning in it was earned.

Then verify:

python ../05-tests/verify_modbus.py --host 10.0.0.17 --port 502 --unit 1

Three things to get right

  • Never hand-edit register-map.csv. It is generated. Change the REGISTERS table in build.py and rebuild. It is the contract with CI Server — if it moves, the SCADA point list in 04-scada/modbus/ must be regenerated too.
  • Never edit the Editor project directly. gen_project.py overwrites it. Fixes go into src/.
  • Never edit as-built/. That is a snapshot of what is deployed, not source.

If you add or move a register

The whole chain has to be re-run, in order:

src/*.st  →  build.py  →  register-map.csv
                              ↓
                        04-scada/modbus/gen_scada_points.py
                              ↓
                        gen_ciserver_qli.py  →  re-import into CI Server

Skip a step and the PLC and SCADA disagree silently — the reads still succeed, they are just wrong.


2. I need to create a new PLC container from scratch

For a new demo, not for recovering this one.

What you need: a Docker host, and the OpenPLC Editor to compile a program.

# ~/myplc-compose.yml
services:
  myplc-runtime:
    image: ghcr.io/autonomy-logic/openplc-runtime:latest
    container_name: myplc-runtime
    restart: unless-stopped
    cap_add: [SYS_NICE, SYS_RESOURCE]     # required - the runtime needs
                                          # real-time scheduling or it breaks
    ports:
      - "<host-lan-ip>:502:502"           # Modbus TCP, for SCADA
      - "<host-lan-ip>:8443:8443"         # REST API, for the Editor
    volumes:
      - myplc-data:/var/run/runtime
    logging:
      driver: json-file
      options: { max-size: "10m", max-file: "3" }
volumes:
  myplc-data:
docker compose -f ~/myplc-compose.yml up -d

Bind the ports to the host's LAN address, never 0.0.0.0. Modbus has no authentication or encryption, and 8443 is the control channel with only JWT in front of it. On a host with a public IP, the bind address is the security control.

Then, in the Editor — the part that is not obvious

Three steps that are configured through GUI dialogs and are not generated. The reference copies of what they produce are in editor-devices/.

  1. Device → Configuration — device OpenPLC Runtime V4, IP = the host's LAN address, port 8443, user admin.
  2. Device → Servers — add a Modbus TCP server, Enable Server on, networkInterface 0.0.0.0, port 502.

    This is the step everyone misses. It is what makes the Editor ship conf/modbus_slave.json, which enables the runtime's modbus_slave plugin. Without it the plugin stays off and nothing ever binds port 502, however the container is configured. 0.0.0.0 here is correct — it is inside the container; Docker's publish is what restricts exposure.

  3. Do not touch Device → Remote Devices. That is the Modbus master — it makes the PLC poll someone else, and its IO groups claim %IW addresses. Your SCADA needs no entry in the PLC project at all; it is a client and simply connects.

Then Build → the Editor uploads → Start PLC. Port 502 only answers while a program is running.

What to copy from this project

The method transfers even when the plant does not:

Take Why
build.py's REGISTERS table + modbus_address() Generating the register map from one table is what stops the PLC and SCADA drifting. Change the tags, keep the machinery.
gen_project.py The Editor has no flat-.st import, and it will not let you type located addresses by hand. This writes them into project.json.
The IO_MUX pattern One POU owns every located variable; control logic reads globals only. It is what lets the same control code run against a simulation or a real field.
05-tests/verify_modbus.py Proves the map against the live PLC before you touch SCADA.

3. I need to move this PLC to a different host

The compiled program is in the container's writable layer, so docker commit, never docker pull. Full procedure with the real checksums from the 2026-08-19 move: ../02-environment/MIGRATION.md.


Where to look when something is wrong

Symptom Look at
Port 502 refuses connections README.md — the two conditions. A program must be running, and the Editor project must define a Modbus Server.
SCADA reads plausible but wrong values %MW starts at HR 1024, not 0. register-map.csv has resolved addresses.
The alarm word goes negative %QW17 must be read as unsigned 16-bit. Bit 15 does not fit a signed INT.
Every measurement reads 0 You are polling %IW/%IX (FC04/FC02). In the simulation build nothing writes those. Live values are in the %QW block.
Editor build fails on VAR_EXTERNAL … has no matching VAR_GLOBAL The Editor was open while files were copied in and wrote its cached project.json back. DEPLOY.md §B2.
Console says "Compile only mode — skipping upload" Compile-only is on; nothing was uploaded. DEPLOY.md §B4.
Setpoints reverted to unfamiliar values The runtime restarted and IO_MUX re-seeded the defaults. Live tuning is not stored in this repo. README.md.