Documentation
20 items
Layout and how to work in it
dokumentaciactrl32 Editor is an installed application, not a web page. The reason is simple: it draws exactly the pixels you will see on the panel — the same renderer as in the firmware is built into it, fonts, roundings and colours included. What you see is what gets downloaded.
Layout
| Part | Where | What is in it |
|---|---|---|
| Menu | top | File, Edit, View, Project, Build, Online, Debug, Tools, Window, Help |
| Toolbar | under the menu | new, save, undo/redo, TEST, ONLINE, download |
| Project tree | left | the board and everything running on it |
| Documents | centre | every tree node has a page, opened as a tab |
| Status bar | bottom | the result of the last build, the connection state |
The bug icon at the top right opens mail to bugs@ctrl32.com —
reporting a fault should be one click from wherever you hit it.
The tree follows the hardware, not the protocols
project
board
Application programs, variables, visualizations, axes
CAN the bus itself: bitrate, termination
Raw CAN frame-to-tag mapping
CANopen master + devices
RS485 the line: baud, parity, stop bits
Modbus RTU master + devices
Modbus RTU slave the panel as a slave
Ethernet DHCP/static IP, mDNS
Modbus TCP master and slave
MQTT telemetry / Home Assistant
I2C, SPI local buses and chips
Settings
One wire can carry several protocols, and the same protocol can run on two wires (Modbus on RS485 and on Ethernet). Sorting by protocol could not express that; this way matches both the hardware and CoDeSys.
Settings live on pages, not in the right-click menu
A context menu holds actions — add, delete, rename. Never settings. The bus speed is on the bus page, where a person looks for it. A popup is only for picking from a catalog (boards, devices, dictionary objects).
Nodes for features that are not finished yet are in the tree as well — their page says so. A customer has to see what the panel can have, not guess it.
Toolbar
| Icon | What it does |
|---|---|
| ⊞ | new project |
| 💾 | save project |
| ↶ ↷ | undo / redo (both unlimited; the count is in the Edit menu) |
| ▶ / ■ | TEST — simulation inside the editor, without a panel |
| ⏸ ⏭ | pause and single-cycle step (during a test) |
| 🔌 | ONLINE — connect to a real panel |
| ⭳ | download to the panel (F5) — the only coloured icon in the bar |
There is deliberately only one blue icon. Downloading into a machine is an action that must not be confused with any other; everything else is a flat icon like in any ordinary application.
TEST — simulation without a panel
The ▶ button switches the editor into running: the PLC cycle runs on the PC, screens react, blocks compute, tags change. It can be paused and stepped cycle by cycle (Debug → Step).
What runs is the same bytecode and the same block library as in the panel — the VM is compiled for the PC as well. So logic can be debugged at your desk and reach the machine finished.
The difference from online monitoring: TEST computes the values, ONLINE reads them from the machine.
Build (F7) and download (F5)
F7 only builds the project and reports the result. F5 builds and downloads it.
When a build fails, a window opens with the whole output — the text can
be selected and copied with a button, and the same text is in
build\last_build_error.txt. A truncated line in the status bar was never
enough for anyone to help you with.
Language and theme
View → Language and View → Dark theme. The choice is remembered between runs.
Licence
Help → Licence shows the level (DEMO / SMALL / MEDIUM / FULL) and lets you enter one. The rule is simple:
- drawing, programming and simulating work in DEMO too,
- talking to a real panel — download and online alike — is paid.
Details in Licensing.
Updates
Help → Check for updates looks at our web and tells you whether a newer version exists. Downloading is manual — the editor never updates itself behind your back, because the editor version is tied to what is running in customers' panels.
Licensing
dokumentaciaA licence is verified entirely offline. The editor never calls anywhere — an industrial PC usually has no internet, and licensing that needs a network would stop working on the machine exactly when you need it.
Levels
| Level | What it includes |
|---|---|
| DEMO (no licence, or after expiry) | the full editing scope of FULL, but nothing reaches the hardware (download to panel and runtime flash are locked) |
| SMALL | WEBview, tags, logic, simulation; no HMIview (the display shows a status screen), no CAN/Modbus |
| MEDIUM | as SMALL + HMIview screens; no CAN/Modbus |
| FULL | everything: CAN, Modbus, the board generator, custom component libraries |
WEBview is in all levels.
A lower level never discards your data
Content beyond the level is displayed, it just cannot be compiled or downloaded — and the editor lists exactly what exceeds the level. Your project is not trimmed because a licence expired.
Activation
- In the editor, Buy licence opens the checkout in a browser. Sales and taxes are handled by a merchant of record, so you get a proper invoice outside Slovakia as well.
- Back in the editor, paste the order number → Activate. The editor fetches the licence, verifies its signature and loads it immediately. No files, no waiting for a human.
- For a PC without internet the same step is available from a browser on another computer — you generate a request, carry the licence file over and import it.
The licence is tied to a machine fingerprint. Replacing the PC means a new request and a new issue — within the validity period at no extra charge. Validity is one year; the editor warns 30 days ahead.
Deployed panels run forever
This matters and it is deliberate: the licence is on the editor, not on the machine.
- A panel you have downloaded a program into keeps running after the licence expires.
- There are no monthly fees for a deployed device.
- After expiry exactly one thing stops working: downloading new projects.
So a machine at a customer's site never stops because you forgot to renew.
The runtime licence in the device
Besides the editor licence there is a runtime licence bound to the board's serial number. The editor writes it during commissioning — it is not something you have to handle when ordering hardware.
The record reserves room for feature packages (positioning, extra axes, more WEBview clients). The practical consequence: a unit bought today can be extended without reflashing and without shipping it back — the editor writes a new signed record to it.
A licensing fault never stops the machine
When a record fails to verify, the unit is marked unlicensed in the diagnostics and refuses extensions — but base control keeps running. The same rule applies here as everywhere else: logic comes before everything else.
Download to the panel
dokumentaciaDownloading is one button in the editor (the blue ⭳ icon, or F5).
There is no separate application, you need neither Python nor PlatformIO,
and you do not have to know what happens underneath. This chapter is about
what happens underneath — because during commissioning it comes in useful
exactly once.
Two different things: the runtime and the program
| Runtime | Program (pack) | |
|---|---|---|
| what it is | the panel's firmware | your screens, tags and logic |
| when it is written | on a new board or a new ctrl32 version | on every project change |
| how | Flash runtime on the board page (USB, esptool) | the ⭳ button / F5 |
| takes | tens of seconds | seconds |
Changing a screen does not mean reflashing the panel. The visualization is data, not code — the panel reads it straight from flash.
What happens when you press ⭳
- The editor builds the project into a binary pack. If the build fails, nothing else happens and a window opens with the whole output.
- The pack goes over the serial port in chunks (2 kB, each acknowledged).
- It is written into the inactive slot — the panel has two (
visu_a,visu_b) and is currently running from one of them. - The panel verifies the whole pack: CRC, format, schema version and target board.
- Only then does it switch the active slot and restart.
A download cannot brick the panel
When anything is wrong — a broken cable, a bad pack, the wrong board — the switch simply does not happen. The old pack stays in the other slot and the panel keeps running. That is not luck; it is the point of having two slots.
Pick the port here
The port selector in both transfer dialogs starts empty and the action stays disabled until you choose one. Nothing is preselected on purpose: a port nobody picked reads as a setting, and the first sign that it was not would be bytes going to a device that is not there.
Why the panel refuses a pack
| Message | Cause |
|---|---|
ERR: size/slot |
the pack is larger than the partition (or the slot cannot be opened) |
ERR: crc |
the transfer was corrupted — try again, check the cable |
| invalid target | the project is for a different board than the one connected |
| old format version | the runtime is older than the pack → flash the runtime |
The target-board check is deliberately strict. A pack for a panel with a different display and different pins would load and run — and the machine would do something other than what you expect.
Transport: always the serial port
Downloading goes over USB / the serial line, not over WiFi or Ethernet. That is a deliberate decision: during commissioning a cable is the one thing that always works, and putting a program into a machine over a network is something to secure properly first. Network download comes later.
The same port is used for online monitoring — monitoring therefore parks itself for the duration of a download and reconnects by itself after the panel restarts.
Download into a machine that is standing still
Writing to flash stalls cache access for both cores, so the logic does not run while it happens, even at the highest priority. The panel therefore allows a download only in STOP — the same reason an S7 has a RUN/STOP switch.
Runtime
The firmware is written from a catalog of prebuilt binaries for the board
(runtimes/), not compiled on your PC. You need no toolchain installed
and you get exactly the binary we tested.
It runs on its own thread, so the editor keeps drawing meanwhile, and it does not touch the pack slots — your program stays in the panel after a runtime flash.
What is downloaded together with the program
The pack contains everything the panel needs, in one file:
- the header (magic, schema version, target board, CRC32),
- the tables of strings, tags, alarms and styles,
- assets (fonts, icons, images),
- screens,
- the program bytecode and the ST source (used to prove identity when going online).
The panel parses nothing out of it — it maps it from flash and reads it directly. Loading a screen takes less than a millisecond.
Licence
Downloading into a real panel is a paid part. Drawing, programming and simulating work in DEMO too — see Licensing.
Raw CAN
dokumentaciaThe panel can speak two ways on the CAN bus:
| CANopen | Raw CAN | |
|---|---|---|
| Devices | frequency drives, I/O modules per CiA 301/402 | batteries and BMS, solar inverters, chargers, wallboxes |
| Configuration | EDS file, node-ID, SDO, PDO mapping | frame ID and byte layout |
| Who uses it | industry | home automation and energy |
Most devices in home automation do not speak CANopen and never will — they send their own frames with a fixed byte layout. That is why raw CAN is a separate part, not an add-on to CANopen.
Both paths run on the same bus at the same time. The panel can control a drive over CANopen and read a battery with raw frames simultaneously.
The signal model is DBC
A frame has an ID and up to 8 bytes. A signal inside it is defined by five things — exactly the ones you find in the vendor's DBC file:
| Field | Meaning |
|---|---|
start_bit |
0..63; with little it is the LSB of the signal, with big its MSB |
bits |
width 1..32 |
byte_order |
little (Intel) or big (Motorola) |
signed |
two's complement |
gain / offset |
tag = raw_value × gain + offset |
Thanks to that a DBC maps one to one and DBC import can be added without changing the pack format.
YAML
raw_can:
# BMS -> panel
- id: 0x355
dir: rx
dlc: 8
signals:
- { tag: bat_voltage, start_bit: 0, bits: 16, byte_order: little, gain: 0.01 }
- { tag: bat_current, start_bit: 16, bits: 16, byte_order: little, signed: true, gain: 0.1 }
- { tag: bat_soc, start_bit: 32, bits: 8, byte_order: little }
# 29-bit frame (chargers using J1939-like schemes)
- id: 0x18FF50E5
ext: true
dir: rx
dlc: 8
signals:
- { tag: cell_temp, start_bit: 7, bits: 16, byte_order: big, signed: true, gain: 0.1, offset: -40.0 }
# panel -> BMS, every 100 ms
- id: 0x305
dir: tx
dlc: 2
period_ms: 100
signals:
- { tag: charge_current, start_bit: 0, bits: 16, byte_order: little }
dir: rx = the panel receives the frame and unpacks it into tags.
dir: tx = the panel builds the frame from tags and transmits it.
period_ms: 0 means transmit on change — the tag deadband has
already filtered the noise, so only a real change is sent.
In the editor this is the tree → CAN → Raw CAN. The same fields, one table per frame.
Product limits
| Limit | Value |
|---|---|
| Frames | 32 |
| Signals in total | 128 |
The editor rejects a project over the limit at compile time.
Pitfalls
| Pitfall | Consequence |
|---|---|
Swapping little and big |
the values look "almost right" — the same class of fault as the word order on Modbus f32. Verify against a known value (voltage, temperature), not one that happens to be fluctuating. |
| An ID overlapping the CANopen range | the two protocols get in each other's way on one bus. Packbuild rejects such a project; the editor shows it in red right on the page. |
Forgotten signed |
a negative current reads as 65 A instead of −1 A |
A truncated frame (shorter dlc than the signal needs) |
the runtime skips it and the tag holds its last value — a truncated frame is common while a device is powering up and must not write nonsense |
period_ms too short |
at 500 kbit/s an 8-byte frame takes ~130 µs; 10 frames every 10 ms is ~13 % of the bus. Transmit as often as you need to, not as often as you can. |
How it works inside
TWAI has a single receive queue, so only one reader may drain it. Therefore:
- when CANopen is running, its
poll()picks the frames up and passes those that do not belong to CANopen on to raw CAN (a hook); - when CANopen is off, a separate
rawcan_taskruns (core 1, 10 ms) and reads the queue itself.
TX frames are assembled after reception in the same cycle, so that a value received in this cycle makes it into the reply.
The period is computed from monotonic milliseconds, never in cycles — with cycle jitter the timing would drift.
Diagnostics
The serial command VUI! prints rawcan=<received>,<sent>. If the
received counter does not grow, the device is silent or the bus rate
does not match; if it grows and the values are nonsense, byte_order
or start_bit is wrong.
Peripherals and services
dokumentaciaHow the panel works with the ADS1115 analog-to-digital converter, with
fitted retentive memory (FRAM/EEPROM) and with a real-time clock (RTC).
Technical background: specs/007-peripherals-services/.
ADS1115 — analog inputs (project peripheral)
You connect the converter to the I2C header of the board and add it in the editor as a peripheral (exactly like the TCA9555 expander) — the procedure is shown in the clip Adding peripherals to a board:
peripherals:
- { type: ADS1115, name: adc1, bus: i2c, addr: 0x48, range: 4.096 }
tags:
- name: temperature
datatype: f32
source: { periph: { chip: adc1, pin: 0 } } # pin = channel AIN0..3
gain: 0.0125 # raw value -> physical units
offset: -40.0
deadband: 0.5 # mandatory on analog tags — filters noise
unit: "°C"
- The address is 0x48–0x4B depending on the chip's ADDR pin; range is the full-scale range in volts (6.144 / 4.096 / 2.048 / 1.024 / 0.512 / 0.256, 4.096 without the key). The raw value is 16-bit signed; with single-ended wiring 0..32767.
- The channels are measured in turn, each one is refreshed within 4 PLC cycles at the latest (40 ms with a 10 ms cycle).
- A disconnected or unfitted chip: one event in the diagnostics, the tags hold their last value, the panel keeps running; once it is connected the chip comes back to life on its own.
Retention — values that survive a power-down
Tags in the RVAR (retain) list are stored automatically according to what the board has fitted — nothing is configured:
| The board has | Behaviour |
|---|---|
| FRAM | changes are stored within 1 s; no write-count limit |
| EEPROM | changes are stored within 5 minutes at the latest (to preserve chip life — even with a permanently changing value it lasts > 9 years) |
| nothing | internal flash (NVS), stored within 10 s — the original behaviour |
- The image in memory is protected by a checksum in two alternating copies — a power failure in the middle of a write can never corrupt both; after startup the last complete state applies.
- Changing the list of retain variables (a different project/version) means the variables start from their initial values plus an entry in the diagnostics. Values are never mixed up between variables.
- If the board catalog declares memory but the chip does not answer, the panel reports it and falls back to NVS — retention never stops working.
- Capacity: 64 retain variables (NVS: 16).
For frequently changing values (piece counters updated every cycle) FRAM is the right choice — EEPROM only stores them once every 5 minutes.
Real time (RTC)
A board with a PCF8563 fitted (battery-backed) knows the date and time even after a power-down. Alarms and diagnostics then carry the real time.
Setting the time:
- Service cable:
python tools/settime.py COM5 --apply(sets it from the PC; without--applyit only prints the time), or the serial commandVUT!2026-08-09 14:32:05directly. - WEBview: the "Set panel time" button in the status bar — it takes the time from the phone/PC. The button only appears on panels that support setting the time.
States (VUT! prints source= and state=):
rtc / ok— the time runs from the RTC chip and survives a power-downsys / none|invalid— there is no RTC (or its battery is flat): the time is valid until power-down, after a restart it begins at 2000-01-01; a flat battery is reported by the diagnostics
The time is local — the panel does not deal with time zones or daylight saving; at the changeover set the time the same way you would on a machine with a Simatic.
MQTT
dokumentaciaHome Assistant? Skip straight to Home Assistant mode — in that mode the panel acts as a process data server (an OPC equivalent).
The panel can push tag values to an MQTT broker — typically into ThingsBoard or OpenRemote, where the customer builds charts, dashboards and notifications. The panel is a data source (gateway role), not a historian: it sends the current state, the server keeps the history.
How to switch it on
- In the device tree, double-click the MQTT node (or right-click → MQTT settings…).
- Tick Publish telemetry to broker and fill in the broker address and credentials.
- In the tag list, tick the tags that should be sent.
- Apply → Download to panel. The configuration is part of the pack — changing the broker or the tag list does not require new firmware.
MQTT settings are part of the FULL level (same as CAN and Modbus).
| Field | Meaning |
|---|---|
| broker | IP address or hostname (e.g. 192.168.0.10) |
| port | 1883 (plain MQTT, no encryption) |
| username / token | in ThingsBoard this is the device access token |
| password | for a generic broker; ThingsBoard does not need it |
| topic | base topic; default v1/devices/me/telemetry |
| interval | how often a message may be sent at most (default 5 s, min 1 s) |
| setTag commands | allows writes from the dashboard (see below) |
What the panel sends
A flat JSON containing only the values that changed since the last message goes out on the base topic:
{"voltage_l1": 231.4, "contactor": true, "pieces": 1250}
- If nothing changed, nothing is sent — no pointless traffic.
- After connecting (including after an outage) the complete state is sent once, so the dashboard has everything.
- Decimal places follow the tag settings;
boolgoes out astrue/false. - The tag deadband filters noise — without it a noisy analog input would send messages forever.
Commands from the dashboard (setTag)
If commands are enabled, the panel listens on the ThingsBoard RPC topic and accepts:
{"method":"setTag","params":{"pieces":0}}
A write is a command, not direct memory access — the panel executes
it only if the tag is among the published ones and has rw access.
Otherwise it answers with an error and writes nothing. The reply goes to
the topic .../rpc/response/<id>.
Never use remote commands for safety functions. E-stop, limit switches and doors must be hard-wired.
Security and limits
- No TLS. Encryption is not enabled on the ESP32-S3 (it does not fit into RAM alongside control and graphics). The broker must live on the separate machine network, not on the internet. The panel does not belong on a public network.
- The token and password are stored in the project pack in plain text.
- Published tag limit: 64. The editor will not allow more.
- One broker per panel.
| Situation | Behaviour |
|---|---|
| Broker unreachable at startup | the panel runs normally and connects in the background |
| Broker outage | one diagnostic event, repeated reconnection, the complete state is sent once it is back |
| Slow network | telemetry never extends the control cycle (it runs on the other core) |
| Pack without MQTT | the client never starts, no memory overhead |
ThingsBoard step by step
- Devices → Add device, copy the Access token.
- In the editor: broker = the IP of the ThingsBoard server, username = the token, leave the topic at its default.
- After the upload the values appear under Latest telemetry of the device.
- A Switch or Knob widget in the dashboard → RPC method
setTag, parameters{"tag_name": value}.
For OpenRemote or any other broker only the topic and the credentials change — the message format is the same.
Home Assistant mode (one topic per tag)
The MQTT settings let you switch the publish mode to one topic per tag. The panel then behaves as a process data server — the equivalent of an OPC server:
| What | Topic | Note |
|---|---|---|
| tag value | <base>/<tag> |
retained — a new client gets the state immediately |
| write to a tag | <base>/<tag>/set |
only tags with writing enabled |
| availability | <base>/status |
online / offline (last will) |
For example, with the base topic ctrl32/boilerroom the temperature
goes to ctrl32/boilerroom/temperature and the setpoint is changed by
writing to ctrl32/boilerroom/setpoint/set.
Why retained and last will
- Retained: after a restart, Home Assistant immediately receives the last value of every tag from the broker. Without it the entities would stay empty until a value on the machine happened to change.
- Last will (LWT): when the panel drops out (network or power
failure), the broker itself publishes
offlineand the entities in HA grey out. Without it HA would keep showing the last known value as if it were current — which is worse than no value at all.
Enabling writes — per tag
Every published tag has its own write checkbox. It can only be
ticked for a tag that is rw in the project. Without it the panel
ignores a write from the broker, even if somebody sends one. So the
machine builder decides what the supervisory system may touch — not the
other way round.
The fastest route: MQTT Discovery
In one-topic-per-tag mode the MQTT Discovery option is enabled. On connect the panel sends the broker a configuration for every entity and Home Assistant creates them by itself — you write no YAML. All you need is the MQTT integration set up in HA.
The entities are grouped under a single device, so you see the panel in HA as a whole with all its values underneath. The entity type follows the tag:
| Tag | Entity in HA |
|---|---|
| numeric, read-only | sensor with a unit |
| boolean, read-only | binary sensor |
| numeric, writable | number (number) |
| boolean, writable | switch (switch) |
The configuration is published as retained, so it survives a restart of both HA and the broker. After a project change the entities update themselves.
Exporting the configuration (when you want YAML)
If you would rather keep the configuration under your own control (version control, manual edits), turn Discovery off and use the export:
The Export YAML for Home Assistant button produces a file with one entity per tag:
- numeric read-only tag →
sensor(with a unit) - boolean read-only tag →
binary_sensor - numeric writable tag →
number - boolean writable tag →
switch
Paste the file into configuration.yaml (or pull it in with
!include) and restart HA. The ranges on number are deliberately
wide — narrow them down in HA to suit the machine.
Control stays on the panel
The logic, the timers and the local buttons keep running even when Home Assistant is switched off, the network is dead and the broker is unreachable. MQTT is only a window to the outside. Verified by measurement: with an unreachable broker the cycle runs at 24 µs with zero overruns. That is why nothing on the machine stops while HA is being updated.
The price of MQTT is roughly 12 kB of RAM — and only when it is enabled in the project.
Ghosts after a project change
Retained messages stay on the broker even after a tag is renamed. Clear the old topic with an empty retained message:
mosquitto_pub -h <broker> -t ctrl32/boilerroom/old_tag -r -n
Typical pitfalls
| Pitfall | Fix |
|---|---|
| Nothing shows up in the dashboard | check the token (in ThingsBoard it goes into username, not the password) and whether the tag is ticked |
| Values arrive rarely | that is by design — only changes are sent; check the tag deadband |
| A command from the dashboard does nothing | the tag must be published and have rw access |
| Broker behind NAT or on the internet | not supported without TLS — the broker belongs on the machine network |
Modbus
dokumentaciaThe panel is a Modbus master — it cyclically reads (and writes) the registers of the connected devices into the tag table. Two transports are supported, both at the same time:
| Transport | Physical layer | When |
|---|---|---|
| RTU | RS485 terminals of the board | I/O modules, drives, sensors on a cable |
| TCP | Ethernet or WiFi | energy meters with a LAN module, RTU↔TCP gateways, cabinet devices with LAN |
Slave mode (the panel as a Modbus server for a supervisory system) is a planned extension — it is not part of the product yet.
A tag with a Modbus source
Every Modbus tag has: unit (device address), fc (coil / discrete /
holding / input), address (0-based, not the 3xxxx/4xxxx notation),
word_order (big/little — the word order of 32-bit values) and
optionally transport: tcp + ip + port.
- Without
transport(or withtransport: rtu) the tag goes over RS485 — the behaviour of older projects does not change. f32/u32tags are read from two registers. The word order differs from vendor to vendor and is the most common source of nonsensical values — when importing from the catalog it is set correctly for you.- Scaling:
value = raw × gain + offset. Analog tags need a deadband — without one a noisy value repaints the screen for nothing.
tags:
- name: voltage_l1
datatype: f32
source: { modbus: { transport: tcp, ip: 192.168.0.50, port: 502,
unit: 1, fc: input, address: 0, word_order: big } }
Limits and behaviour
| Limit | Value | Why |
|---|---|---|
| Distinct TCP targets (IP:port) per project | 4 | ~6–8 kB of RAM per connection on the ESP32-S3 |
| Tags in total | 512 | product limit |
- Tags on the same IP:port share a single connection — 20 tags from
one energy meter is still just 1 target. Different
unitvalues on the same IP (an RTU↔TCP gateway with several devices behind it) also count as 1 target. - The editor enforces the limit both on import and at compile time — a project over the limit is rejected on your desk, not as a stuttering panel on the machine.
- Target outage: tags hold their last value, one event goes into the diagnostic buffer when the target drops out and one when it returns. The panel reconnects automatically (1 s backoff); polling of the other targets keeps running.
- The poller runs outside the PLC cycle — a slow or unreachable Modbus device never extends the logic cycle.
Catalog of Modbus devices
An "EDS for Modbus": the directory devices/modbus/*.yaml, one file =
one device (vendor, model, datasheet, word_order) plus a register map
(address, fc, data type, scale, unit, description). The catalog is
curated by inovaea and is signed like all the other catalogs.
Using it in the editor: the Modbus RTU master or Modbus TCP
master page → Add Modbus device… → pick from the catalog → tick the
registers → choose the transport (RS485 / TCP + IP) → Import tags.
The tags get a prefix derived from the model (sdm630_voltage), the
right data type, word order and scaling.
First entries: Eastron SDM120 (single-phase meter) and SDM630 (three-phase meter).
Your own device
A device that is not in the catalog does not have to be written by hand:
- Add Modbus device… → in the right-hand pane (while nothing is
selected) fill in vendor and model → New device. This
creates the file
devices/modbus/<vendor>_<model>.yamlwith one sample register. - In the device window → the Registers tab → tick Edit register
map. The row below the table adds registers (name, address, class,
data type, unit); the
xon a row deletes a register. - Save to catalog writes the map back into the
.yaml.
The file is the single source of truth — the project only keeps the path to it, so the entry appears on disk right away, not only when the project is saved.
| Rule | Why |
|---|---|
file name <vendor>_<model>.yaml, lower-case ASCII |
the catalog is versioned in git and must be readable on every OS |
uid: 0x0000 for customer entries |
the numbered space belongs to the curator, otherwise uids would clash |
| one register per line | the parser reads line by line; a flow map wrapped onto two lines loses everything after the first line |
| renaming a model creates a NEW file | the old one may be used in another project, so it is not deleted |
The manual route remains: copy devices/modbus/_template.yaml and fill
in the register map from the device manual.
Modbus slave — the panel answers
The panel is not only a master. It can also be a slave: a supervisory SCADA, PLC or Home Assistant reads its state and sends setpoints. The panel still runs fully without them — SCADA is a window to the outside, not a condition for operation.
modbus_slave:
rtu: false # answer on the RS485 line
tcp: true # answer over TCP
unit: 9 # unit ID of the panel
port: 502
word_order: big # word order of 32-bit values
tags:
- { tag: boiler_temp } # 4x 0-1, read-only
- { tag: temp_setpoint, writable: true } # 4x 2-3
- { tag: pump } # 0x 0, bit
In the editor: RS485 → Modbus RTU slave or Ethernet → Modbus TCP slave. The page shows exactly the map the integrator will read — both the protocol address and the number from the manual.
How addresses are assigned
Automatically, in list order: bool → coils (0x) from 0, everything
else → holding registers (4x) from 0, where i32/u32/f32 take up
two registers. The address goes into the pack explicitly, so
reordering tags in the editor does not shift the map. The order in the
list can be changed with the arrow — it is part of the configuration,
not cosmetics.
A write is a command
A tag without writable ticked answers a write with exception
0x02; it never passes silently. The same rule as with WebSocket and
MQTT: the panel decides about a write, not whoever asks for it.
Further deliberate decisions:
FC6(write single register) on a 32-bit value is rejected — a half-written value is worse than a refusal.FC16first validates the whole range and only then writes — a half-executed write would leave the machine in an undefined state.- Reading a range that lies even partly outside the map returns an exception. Zeros would send the integrator looking for the fault at their end.
- A broadcast (address 0) on RTU is executed but not answered — the standard requires it; otherwise two slaves would transmit at once.
Pitfalls
| Pitfall | Fix |
|---|---|
| Master and slave on the same RS485 line | a line has one master — packbuild rejects this at compile time and the editor shows it in red right on the page |
| Wrong word order | word_order must match what the SCADA expects — the same trap as on the master side |
| Several SCADA clients at once | the TCP slave serves one connection; each further one would cost ~6–8 kB of RAM, which the S3 does not have to spare |
Diagnostics: VUI! prints mbslv=<requests>,<exceptions>. A growing
exception count means the SCADA is reading outside the map or trying to
write to a tag without permission.
Typical pitfalls
| Pitfall | Fix |
|---|---|
| Nonsensical f32 values | swap word_order (big ↔ little) |
| Address off by one | the vendor documentation counts from 1 or uses the 3xxxx notation — in ctrl32 the address is always the 0-based protocol address |
| Device does not answer over RTU | check the baud rate/parity, the A/B wires and the termination |
| TCP target behind NAT or in another segment | the panel and the device belong on the same (separate) machine network |
Adding a peripheral
dokumentaciaA fifteen-second procedure: open the board, right-click to add a chip to a bus, and it is in the project. The demo runs on the inoCORE32-TM board and adds a TCA9555 expander and an ADS1115 converter to I2C.
What those peripherals do at run time — mapping pins onto tags, converter ranges — is described in Board peripherals and system services.
Adding peripherals to a board
- 0:00 A project open on the inoCORE32-TM board — the tree shows only the interfaces the board really has, and the green dot next to CAN is the live status read from the panel.
- 0:02 The board node opens its page — the module card (ESP32-PICO-V3-02, Ethernet IP101GRI, 8 free pins) and the pin assignment from the catalog.
- 0:05 Right-clicking I2C offers the chips for THAT bus — TCA9555 and ADS1115. POWERSTEP01 is SPI, so it is not offered.
- 0:06 TCA9555 appears in the tree and its page opens — the expander pin table, every pin bindable to a tag.
- 0:09 The same for the ADS1115 — four analog inputs, address 0x48.
- 0:13 Back on the board page — both peripherals are listed with their addresses and the pin table has gained the pins they use.
How this clip was made What gets recorded is the editor itself, not the screen: the input is injected directly into ImGui before the frame is drawn. That makes it deterministic — the same script always yields the same recording, so the clip is not just a video but also a regression test of the procedure.Pins are not configured at run time. A peripheral added in the editor is compile-time configuration: it goes into the pack and the panel reads it at startup. The pin assignment never changes while running — a deliberate decision that protects you from a badly uploaded project switching a pin that has something else on it.
Quick start
dokumentaciaThe goal of this chapter: from an installed editor to a program running in the panel. It takes a few minutes and needs nothing but the panel, a USB cable and a PC.
1. Installation
Run the installer ctrl32-setup-<version>.exe. It installs:
| File | What it is for |
|---|---|
visu_editor.exe |
the editor itself |
visu_stc.exe |
the ST compiler — the editor calls it on every build |
packbuild\ |
the pack builder (it carries the schema and the board catalog) |
ctrl32.dat |
the catalog of boards and chips |
examples\ |
sample projects to open |
Why three programs and not one
The compiler and the pack builder are the same tools we use in development — so the pack the editor builds for you is byte for byte the one the command line builds. The format has a single author and no way to drift.
The three belong together: each carries the schema version it was built for, and the editor refuses a pack builder that does not match rather than quietly building the wrong thing.
2. Open a sample project
File → Open and pick something from examples\. Good places to start:
ladder_sealin.yaml— the classic seal-in circuit with a TON, a counter and a latch; the rungs come with the comments under the variables,tubex_panel.yaml— a real panel with Ethernet, a Modbus slave and NeoPixel LEDs,machine_blocks.yaml— motors, valves, PID and hysteresis,time_setpoint.yaml— a TIME setpoint the operator types on the screen, driving a timer's PT.
3. Pick the board
Click the board node in the project tree. The board decides the pins, the memory and which runtime is flashed into it — a project cannot be deployed without one.
4. Flash the runtime (the first time)
If the panel is new or carries old firmware, the board page has Flash runtime. It is written over USB (esptool) and takes tens of seconds.
An old runtime means the program will not load
The pack format evolves. If the runtime is older than the pack, the panel refuses it and the editor shows an error. The fix is always the same: flash the runtime.
5. Download the program
Pick the COM port in the toolbar and click the blue ⭳ icon (Download to panel). The editor builds the project and uploads it. What happens underneath:
- the project is built into a binary pack,
- the pack goes into the inactive slot in the panel,
- the panel verifies the CRC, the format and the target board,
- only then does it switch the active slot and restart.
That is why downloading cannot brick the panel: when anything is wrong, the old pack stays and keeps running.
No port is preselected
The port selector starts empty, and both transfer dialogs ask for it at the moment bytes are about to move. A port nobody chose reads as "configured", and the first sign that it was not would be a download going nowhere.
6. Watch what the machine is doing
Next to the port there is a plug icon — ONLINE. Switched on, the editor shows live values straight from the panel: in the ladder rungs, on block pins, in the tag list and in the screen preview.
There is one condition: the editor and the panel must run the same program. If they do not, the editor says so and offers to download it — more in Online monitoring.
What next
- Editor — how it is laid out and what is where
- Tags — variables, types, deadband and where values come from
- Ladder — drawing rungs
- Download to the panel — dual slot, runtime, what is verified when
What ctrl32 is
dokumentaciaHMI and PLC for small machines on a single ESP32 panel. You draw the screens, define the tags, write the logic in ladder, in a function block diagram or in ST — and download it to the panel. The same visualization also runs in a browser.
The equivalent of CoDeSys + WebVisu, on hardware that costs an order of magnitude less.
What it is made of
| Part | What it does |
|---|---|
| Panel (firmware) | runs on the ESP32, draws the visualization on a touch display, reads Modbus, executes the logic |
| ctrl32 Editor | an installed desktop application — you draw screens, define tags, write the program and download it to the panel |
| WEBview | the same visualization in a browser, served straight from the panel |
The core principle: the visualization is DATA, not code
Screens are not compiled into the firmware. The editor packs them into a binary pack and the panel reads it directly from flash. Changing a screen therefore means uploading a file, not reflashing the panel — and when something goes wrong, the old pack stays in the other slot and the panel keeps running.
What the panel really handles
The numbers are measured on the slowest board we ship (a classic ESP32 at 240 MHz), with Ethernet and the CAN bus running alongside:
| 400 ladder blocks | 0.52 ms out of a 10 ms cycle (5 %) |
| a whole machine program (190 ST lines, 215 tags) | 0.30 ms (3 %) |
| 10 000 ST instructions | 1.42 ms (14 %) |
For comparison: a Siemens LOGO! stops at 400 blocks. We run them with a cycle that is 95 % idle. Details and method in Performance and limits.
What it is NOT
Safety is not handled through the panel
The device is not intended for safety functions. Emergency stop, doors and light curtains must be hard-wired — never routed through the panel's logic. Stopping a drive over CANopen is not a safety function; that is what the drive's STO terminals are for.
Nor is it a real-time system. It holds a 10–20 ms cycle reliably, but jitter from the network stack exists and we never promise it away.