PLC writes typed data to /dev/shm. Go reads it, holds a snapshot, and
re-publishes it as Modbus TCP (and later: HTTP/WebSocket for Svelte).
The PLC has no Modbus code. There is no register table inside CODESYS. The register layout lives in Go and is one file: internal/modbus/addresses.go.
Putting Modbus inside the PLC is what we did before
(Device/Application/Programs/ModbusProgram — legacy, frozen).
Maintaining a flat 16-bit register array next to the control logic
forced manual offset arithmetic, alignment hazards, and two sources
of truth whenever a new variable was added.
This bridge makes the PLC publish typed state — one struct, with named fields. Go is the only place that knows about Modbus addressing, JSON, WebSocket, etc. Adding a new external interface never touches the PLC.
CODESYS RT task external SCADA / HMI
| |
| writes PlcData (seqlock) | Modbus TCP (:5020)
v v
/dev/shm/plc_data +----------------------------+
/dev/shm/plc_cmd <--------- | plc_bridge (Go) |
| - shm reader (10 ms) |
| - state.Snapshot |
| - modbus tcp server |
+----------------------------+
PLC and Go agree on two things: the byte layout of PlcData and
PlcCommand, and their (magic, version) headers. Everything else is
internal to one side.
| File | Side | Role |
|---|---|---|
| ../codesys_export/Device/Application/ | PLC | DUTs + GVL + publisher POU, auto-exported from the CODESYS IDE. One file per IDE object. |
| internal/shm/layout.go | Go | mirror DUTs |
Both files declare Magic, Version. Bump Version on every layout
change, in both files. The Go reader refuses to mount a segment
whose version it does not recognise, instead of silently reading
garbage.
- Add the field to a payload struct (
MachineState,MotionState, or a new domain struct) in:- the matching DUT under ../codesys_export/Device/Application/DUT/ShmBridge/
- internal/shm/layout.go
- Bump
PLC_DATA_VERSION/PlcDataVersionin both. - If you want it on Modbus, add an address constant + an
EncodeDataline in internal/modbus/addresses.go.
Same pattern, but on PlcCommand and commandFields in
internal/modbus/addresses.go.
make deploy # build web + go, rsync, sudo install, systemctl restart
make rollback # revert to previous binary
make logs # follow journaldCODESYS creates shm files as rwxr-x---. This drop-in chowns them to 664
before plc_bridge opens them, so plc_bridge user (in root group) can write plc_cmd.
[Service]
ExecStartPre=-+/bin/chmod 664 /dev/shm/plc_data /dev/shm/plc_cmdAfter creating: sudo systemctl daemon-reload
| Segment | Size | Version | Direction |
|---|---|---|---|
plc_data |
88 B | v2 | PLC → Go → HMI |
plc_cmd |
48 B | v1 | HMI → Go → PLC |
Version mismatch → plc_bridge logs error and exits (Restart=on-failure retries until PLC is updated).
# 1. PLC must already be publishing /dev/shm/plc_data and /dev/shm/plc_cmd.
# 2. Pin to a non-isolated CPU (PLC owns CPU 2-3).
taskset -c 0 go run ./backend/cmd/plc_bridgeFlags: --modbus :5020 --http :8080 --poll 10ms --push 100ms
- Not a Modbus master — only a slave.
- Not a control loop — control stays in the PLC.
- Not a multi-tenant gateway — assumes one PLC instance per process.