# Software and versions

Everything below was **read from the running system on 2026-09-02**, not taken
from documentation. Where a version matters — where a different one behaves
differently — that is said explicitly.

## The PLC

| Component | Version | Where it runs | How to check |
|---|---|---|---|
| **OpenPLC Runtime** | **v4.1.10** | `openplc-runtime` container on `yau-sls-poc-lin001` | `docker exec openplc-runtime cat /workdir/VERSION` |
| Docker image | `openplc-runtime-migrated:v4.1.10` | local to the host, **not in any registry** | `docker images` |
| Base OS in container | Debian 12 (bookworm) | | `docker exec openplc-runtime cat /etc/os-release` |
| Python (runtime + plugins) | 3.11.2 | | `docker exec openplc-runtime python3 --version` |
| g++ | 12.2.0 | used to build the PLC program | `docker exec openplc-runtime g++ --version` |
| GNU Make | 4.3 | drives `scripts/Makefile.strucpp` | |
| pymodbus (in the slave plugin) | 3.11.2 | | `docker exec openplc-runtime ./venvs/modbus_slave/bin/pip list` |

### Runtime plugins

`/workdir/plugins.conf` — the third field is enabled (1) / disabled (0):

| Plugin | Enabled | Note |
|---|---|---|
| `modbus_slave` | **1** | This is the one that matters. It is what makes the PLC a Modbus **server** on 502. |
| `modbus_master` | 0 | Correct — the PLC must not poll anyone. |
| `opcua` | 0 | |
| `s7comm` | 0 | |
| `ethercat` | **1** | ⚠️ Enabled for no reason. It came from an `ethercat.json` the Editor project emits by default (the file is 0 bytes). Harmless so far, but it is a plugin running with nothing to do. |

## The toolchain

| Component | Version | Notes |
|---|---|---|
| **OpenPLC Editor** | **v4.2.11** (AppImage) | Needs `--no-sandbox` (Electron) and `libfuse2t64`. ⚠️ **Not currently installed anywhere** — see `DEPLOY.md` §0. |
| **STruC++** | **v0.6.2** | The ST → C++ compiler. **Ships only inside the Editor AppImage**; there is no standalone binary. This is the single reason the Editor is unavoidable. |

## The SCADA

| Component | Version | Where |
|---|---|---|
| **Yokogawa CI Server** | R1.03 | `yau-poc-cicore1` (`10.0.0.21`) |

## Versions that actually matter

**OpenPLC Runtime v3 → v4 is a breaking change, and most guidance you'll find
online is for v3.**

| | v3 | **v4 (what we run)** |
|---|---|---|
| ST compiler | MatIEC | **STruC++** |
| Accepts a flat `.st` upload | yes | **no** — `scripts/compile.sh` explicitly rejects MatIEC files (`Config0.c`, `glueVars.c`) |
| Web UI on the runtime | yes | **no** — REST API only, port 8443, JWT |
| Editor project format | one file | **a folder** — `project.json` + one file per POU |
| `%MW` Modbus mapping | starts at HR 0 | **starts at HR 1024** (`%QW` occupies 0–1023) |

That last row is the one that silently produces wrong data rather than an error.
See `register-map.csv`, which carries resolved addresses.

**The Modbus buffer layout is configuration, not a constant.** From
`core/generated/conf/modbus_slave.json` in the running container:

```json
"holding_registers": { "qw_count": 1024, "mw_count": 1024, ... }
```

`qw_count: 1024` is *why* `%MW0` is holding register 1024. Change that file and
every SCADA address moves.

**CI Server R1.03 vs R1.05.** One R1.03 behaviour is load-bearing here: an item
import is rejected if two items share an `IO_ADDRESS` (`EQP-E-DUP_ITEM`). The SCADA
design works around it by publishing one unit per register. If you move to R1.05,
re-check that before assuming the workaround is still needed.
