Zeus PLC IDE — Help
Program, download and monitor a Zeus soft‑PLC controller (Raspberry Pi CM4/CM5) in IEC 61131‑3 Structured Text, Ladder and SFC.
Overview
The Zeus PLC IDE runs in your browser and talks to a Zeus controller (or the built‑in simulator). You write control logic as programs (POUs), configure the IO hardware, download to the controller, and watch it run online. Everything the controller runs is compiled from your project; the controller itself runs a small real‑time scan engine.
Key concepts
| Term | Meaning |
|---|---|
| Project | Everything on disk for one machine: programs, global variables, the IO/device tree, HMI screens, controller settings. One project can run on several controllers. |
| Controller / Target | The device the IDE connects to and downloads to — a real CM4/CM5, or the local Simulator. |
| POU | Program Organisation Unit: a PROGRAM, FUNCTION_BLOCK or FUNCTION, written in ST or drawn as Ladder. |
| Scan | The controller reads inputs → runs the program → writes outputs, over and over at the task cycle time. |
| Process image | The IO memory: %I inputs, %Q outputs, %M internal memory. |
| Program vs Hardware download | Download program sends the logic without stopping the loop. Download hardware sends the IO configuration and restarts the runtime (briefly stops control). |
Getting started
- Project… → open or create a project.
- Target… → choose the Simulator (safe) or your controller.
- Add IO: IO Devices → Add device, or Scan for RIO… to discover real hardware.
- New program → write ST, or choose Ladder Program (LD).
- Debug → Build (F7) to compile; fix any red messages.
- Debug → Download program (F8) — or Download all the first time (sends hardware + program).
- Debug → Online (◉) to watch live values.
Opening and closing the IDE
Start the IDE with Zeus PLC IDE.cmd (or its shortcut). It opens in its own window; no console window
is left open. Closing the window does not stop the IDE server: it keeps running in the background, so the
next start opens instantly. To stop it completely, run "Zeus PLC IDE.cmd" stop
(status shows whether it is running).
The controller is not affected either way — it keeps running its program whether or not an IDE is connected.
The interface
Top toolbar
| Item | What it does |
|---|---|
| Project… | Open / create / switch project. |
| Save project (Ctrl+S) | Saves unsaved programs and globals. The device tree/rack are saved as you edit them. |
| Save As… | Save the active editor under a new name. |
| Debug ▾ | Build, Download & Upload, Online, login, run/pause/step, verify, forces — see below. |
| Import ST… | Import from PLCopen XML or a text export (.exp/.export). |
| Browse IO… (Ctrl+Space) | Browse IO addresses and insert a declaration. |
| ? Help | Opens this manual. |
| ⟳ Check for updates | Asks optizeus.org whether a newer IDE release is out and answers in a window. While one is waiting, the blue ⬆ Update 0.1.x.y button takes its place - see Updates. |
| Target… | Choose which controller to connect/download to. |
Status badges (top‑right)
- CM4 · SIM IO — the board and whether IO is real or simulated (green = real, amber = simulated).
- RUNNING — the controller run state (RUNNING / PAUSED / STOPPED / CONFIG / FAULT).
- DEMO 1:42:10 — this runtime has no Zeus license and uses real IO: the time left before the program stops. DEMO ENDED once it has. Click it for the license. See License & demo time.
- admin · engineer — the logged‑in session.
- IN SYNC — whether the controller runs exactly what is open here.
- FORCE — one or more points are forced; click to clear.
- the coloured dot — connection to the controller.
Device tree (left)
The tree is the project: the board and process image, Application (POUs, Task Configuration, Global Variables, HMI, Library), IO Devices, Network Interfaces, Modbus TCP Slave, OPC UA Server, Users & Roles, Controller Settings, and the Simulator. Right‑click nodes for actions.
POUs & editors
- New program creates a PROGRAM; New POU… lets you choose PROGRAM / FUNCTION_BLOCK / FUNCTION, ST or Ladder.
- A ST POU has two panes: declarations (VAR…END_VAR) on top, body below — it is one file, shown in two boxes.
- Per‑POU tools: Find… (Ctrl+F), Rename variable… (F2), Rename POU…, Where used… (Shift+F12).
- A FUNCTION_BLOCK that is instantiated more than once shows an Instance picker when online, so you can watch a specific machine.
- View as ladder shows a PROGRAM, FUNCTION_BLOCK or METHOD as a ladder diagram - online too, with power flow.
- Disable / Enable (right-click a POU): a disabled POU is dimmed and struck through, not built, not downloaded and not compared with the controller - so it never makes the IDE show PROGRAM CHANGED. The file is not touched. To stop a program running but keep it enabled, set its task to — not running — in Task Configuration.
Find and replace
- In a POU (Ctrl+F): Aa match case, whole word, then Replace (this match, then the next) or Replace all (declarations and code). Ctrl+Z undoes.
- In the whole project (Ctrl+Shift+F, or In project… in the find bar): every POU,
globals.st, GVLs and data types. Find all lists each match by file and line - click one to open it; untick files to leave alone, then Replace in ticked files. A ladder POU is found but never replaced as text - change it in the ladder editor. - To rename a variable correctly everywhere (skipping comments and strings), use Rename variable… (F2) instead.
Errors in the code
After Build or Build All each message is also shown in the code: a wavy underline (red = error, amber = warning) under the word, and the message at the end of the line. Clicking a message in the Messages panel opens the right POU at the right line - including errors the controller reports on a download. The underlines go as soon as you edit. Build All builds what you see: an open POU with unsaved edits is built from the editor. It compiles several POUs at once, so a project of a few dozen POUs takes a few seconds.
Task configuration
Task Configuration → MainTask sets the cycle time (how often the program runs), the real‑time priority, and which program(s) the task calls. A shorter cycle reacts faster but leaves less CPU headroom; watch the overrun counter online.
Programs at different rates
Every PROGRAM in the project runs on every scan by default, top to bottom.
A controller usually wants otherwise: an interlock at 10 ms beside a totaliser that has no
business running a hundred times a second and costs real time when it does.
Task Configuration → Properties lists the programs with a period each. A period must be a whole multiple of the task period — 25 ms on a 10 ms scan is refused, naming 20 and 30, rather than being rounded to something nobody chose.
"task": {
"period_ms": 10,
"programs": [
{ "name": "Reporting", "period_ms": 1000, "offset": 3 },
{ "name": "Totaliser", "period_ms": 1000, "offset": 7 }
]
}
Offsets stagger them. Two 1-second programs on a 10 ms scan otherwise land on the same cycle, and that one scan in a hundred carries the cost of both — which shows up as jitter nobody can account for.
A program that sits out a scan loses nothing: its variables, timers and function-block instances are exactly as it left them, because a skipped cycle is no different to it than the gap between two scans. The controller reports the list it is really running, so a period named for a program that was renamed shows up as scheduling nothing rather than failing silently.
Library versions
A library file declares itself in a header comment:
(* @library zeus-control @version 1.4.0 *)
Library → Versions… shows what the library is against what this project was locked to, and Lock this library records it — a version and a SHA-256 per file, with who locked it and why.
The hash is what makes the version trustworthy. A version number is a claim; an edited file
that kept its number is the case worth catching, and it is the one that looks correct on every
other screen. A block that has been deleted is reported as REMOVED rather than quietly
dropped, because the alternative is unknown type 'PID' at the next compile.
Global variables
The Global Variables file (globals.st) holds VAR_GLOBAL … END_VAR and is
prepended to every POU on Build and Download. Use a global anywhere by name — no re‑declaration.
VAR_GLOBAL
machine_state : INT;
e_stop_ok : BOOL;
END_VAR
VAR_GLOBAL RETAIN (* survives a power cycle *)
batch_number : DINT;
END_VAR
VAR_GLOBAL block put at the top of a normal POU file is visible only to POUs
in that file. For globals shared across the project, use the Global Variables file. After editing it,
Download for the change to take effect.Data types (DUT)
Application → Data Types (DUT) holds your own types — a structure, an enumeration or an
alias. Right‑click → New DUT…, choose the kind and name it. Each one is saved as
dut/<Name>.st and compiled ahead of the global variables and every POU, so any of them
can use it.
TYPE Door :
STRUCT
Name : STRING;
Open : BOOL;
Closed : BOOL;
Travel : TIME := T#8S;
END_STRUCT
END_TYPE
TYPE DoorState : (Idle, Opening, Opened, Closing, Faulted); END_TYPE
Use it like a built‑in type:
VAR_GLOBAL
doors : ARRAY[1..4] OF Door;
state1 : DoorState := DoorState.Idle;
END_VAR
doors[2].Open := %IX0.3;
doors[3] := doors[2]; (* whole-structure copy *)
IF state1 = DoorState.Opening THEN ... END_IF
- A structure can be copied whole (
a := b;) when both sides are the same type, and passed as a function or function‑block input. - Enumeration values can be written plain (
Idle) or qualified (DoorState.Idle). - Most DUT text copied from another IEC 61131‑3 tool can be pasted in as‑is; pragmas such as
{attribute 'qualified_only'}are accepted and ignored. - Online, a structure opens with ⊞ in the variable tables, and array elements are listed as
doors[2].Open— each one can be watched, written and traced.
Projects, archives & data
A project is a directory, not a file: controller.json, globals.st,
pou/, dut/, lib/, hmi/ and data/.
Project menu:
| Command | What it does |
|---|---|
| Save project (Ctrl+S) | Writes every unsaved program and the globals. The device tree and rack are written as you edit them, so they are already saved. |
| Save As… (POU) | Copies the active POU to a new name. Enabled only on a POU tab — the globals, task and HMI are single project files. |
| Save project as… | Copies the whole project to a new directory and opens the copy; the original is left as it was. Unsaved editors are saved first. backups/ and uploads/ stay with the original — they are its history, not the copy's. |
| 📦 Create archive… | Packs the project into one .tar.gz for a handover, a validation file or an off-site copy. |
| 📂 Restore archive… | Unpacks an archive into a new directory and opens it. |
tar xzf or 7‑Zip opens it years from now
without this IDE. It carries a manifest (zeus-archive.json) naming the project, the date, the IDE
version and a SHA‑256 per file, and the restore compares every file against it — so a damaged archive is
reported rather than half‑restored. backups/ is excluded; it is usually most of the size.The controller's data travels with the program
Source alone does not bring a machine back: setpoints, recipe parameters and tuning constants live in the running controller. So, whenever a controller is connected:
- Save writes every variable's current value to
data/saved.data.csv— even when no program is dirty, because values change while source does not. - Upload from controller also writes
data/<time>-upload_<program>.data.csv. - Online → Restore data snapshot… lists the files the project holds, newest first, and writes them back.
Library & methods
The Library holds reusable function blocks (motors, valves, alternation, trip logic, signal scaling…) that are compiled ahead of your program, so you can call them from any POU. A FUNCTION_BLOCK can also have methods (Add method…). Call a block by declaring an instance and invoking it:
VAR m1 : MOTOR_DOL; END_VAR
m1(run_cmd := start AND e_stop_ok, fb_running := aux1, reset := ack);
motor_out := m1.run_cmd; (* read the block's outputs by name *)
HVAC & control library blocks
Application blocks in lib/40_hvac.st and lib/22_autotune.st, built on the
library PID. They are FUNCTION_BLOCKs (they hold state across scans). Outputs come as both a
percentage (_Eu, 0–100 %) and a scaled raw analog value (_Ao, using
Ao_low/Ao_high).
PID — closed-loop control (the building block)
A single feedback loop with anti-windup. reverse := TRUE when more output lowers the
measurement (cooling, level draining); man := TRUE holds man_out and tracks for a
bumpless return; enable := FALSE parks it at out_min. Tune with kp,
integral time ti_s (s, 0 = off) and derivative td_s (s, 0 = off) — or let
PID_AutoTune below find them.
VAR heat : PID; END_VAR
heat(pv := TT_TEMP.EU_Value, sp := 22.0,
kp := 3.0, ti_s := 90.0, td_s := 0.0,
out_min := 0.0, out_max := 100.0,
reverse := FALSE, enable := run); (* heating: more output = warmer *)
valve_ao := REAL_TO_INT(heat.out * 273.0); (* 0..100 % -> 0..27300 raw *)
(* heat.err, heat.sat (at a limit), heat.integral are exposed for diagnosis *)
TempHumidityCoolReheat — cooling + heating coil, humidity reduce‑only
One air handler, two transmitters (temperature and humidity/dewpoint), two valves. Temperature is one split‑range PID: above mid‑scale the heating coil opens, below it the cooling coil opens — it heats or cools, never both. Humidity is a reduce‑only, reverse‑acting loop: opening the cooling coil condenses moisture, so its output is an extra cooling demand. The cooling coil follows the greater of the two; when drying over‑cools the space, the temperature loop calls for heat — that is the reheat.
Energy interlock: dehumidification only runs when humidity is genuinely high (with hysteresis
Hum_Hyst) and cool+heat never run together unless actually drying — so a dry space never wastes
energy cooling‑and‑reheating.
Key inputs: Enable, SetPoint_Temp/PresentValue_Temp,
SetPoint_Hum/PresentValue_Hum, Hum_Hyst, Deadband,
per‑loop Kp_/Ti_/Td_Temp & _Hum, Manual/Fail_Safe
overrides, Ao_low/Ao_high.
Outputs: Cool_Eu/Cool_Ao, Heat_Eu/Heat_Ao, Dehumidifying,
Mode (0 satisfied·1 heat·2 cool·3 dehumidify+reheat·4 manual·5 fail‑safe).
VAR AHU : TempHumidityCoolReheat; END_VAR
AHU(Enable := TRUE,
SetPoint_Temp := 22.0, PresentValue_Temp := TT_TEMP.EU_Value,
SetPoint_Hum := 8.0, PresentValue_Hum := TT_DEWPT.EU_Value); (* dewpoint °C *)
cool_ao := AHU.Cool_Ao; heat_ao := AHU.Heat_Ao;
AhuTempHumidity — cooling + heating + humidifier (humidity both ways)
Everything the cool‑reheat block does, plus a humidifier: humidity below setpoint is raised,
not just humidity above setpoint lowered. Three loops — temperature (split‑range), dehumidify (adds cooling),
humidify (drives the humidifier). A humidity deadband Hum_DB around setpoint (with hysteresis)
runs neither inside the band, so the humidifier and cooling coil never fight.
Extra I/O over the cool‑reheat block: inputs Hum_DB, ManualHumid,
FailSafeHumid; outputs Humid_Eu/Humid_Ao, Humidifying, and
Mode gains 6 = humidify.
VAR AHU : AhuTempHumidity; END_VAR
AHU(Enable := TRUE,
SetPoint_Temp := 22.0, PresentValue_Temp := TT_TEMP.EU_Value,
SetPoint_Hum := 45.0, PresentValue_Hum := TT_HUM.EU_Value,
Hum_DB := 3.0, Kp_Hum := 2.0, Ti_Hum := 180.0);
cool_ao := AHU.Cool_Ao; heat_ao := AHU.Heat_Ao; humid_ao := AHU.Humid_Ao;
Commissioning: for the dehumidify side control dewpoint (or absolute humidity), not %RH — cooling air raises its %RH until it condenses, so a %RH loop can push the wrong way.
PID_AutoTune — relay‑feedback auto‑tuner
Finds PID gains automatically. Instead of the PID, a relay steps the output around a bias to force
a steady oscillation; from its amplitude and period it computes the ultimate gain/period and the gains
(Tyreus‑Luyben by default — robust; Ziegler‑Nichols via rule := 0). Drive the actuator from
out while tuning; when done is TRUE, copy Kp, Ti_s,
Td_s into your PID and hand control back. Set reverse := TRUE for cooling. It
deliberately oscillates the loop, so tune during commissioning, supervised.
VAR tuner : PID_AutoTune; loop : PID; kp:REAL:=1.0; ti:REAL:=10.0; td:REAL:=0.0; busy:BOOL; END_VAR
IF Start_Tune THEN busy := TRUE; END_IF;
tuner(Start := Start_Tune, pv := TT.EU_Value, sp := 22.0,
reverse := TRUE, out_bias := 40.0, relay_amp := 15.0);
IF tuner.done THEN kp := tuner.Kp; ti := tuner.Ti_s; td := tuner.Td_s; busy := FALSE; END_IF;
loop(pv := TT.EU_Value, sp := 22.0, kp := kp, ti_s := ti, td_s := td,
reverse := TRUE, enable := NOT busy);
IF busy THEN valve := tuner.out; ELSE valve := loop.out; END_IF;
Function & block examples
These are the IEC 61131‑3 standard blocks the runtime implements, with worked examples. In Ladder, drop a Block and pick the type; in ST, declare an instance and call it. Every instance needs its own variable (in Ladder, Add missing declarations does this for you).
TON — on‑delay timer
Q turns on PT after IN turns on; drops immediately when IN drops.
VAR t1 : TON; END_VAR
t1(IN := level_low, PT := T#5s);
start_pump := t1.Q; (* pump starts 5 s after the level stays low *)
(* t1.ET is the elapsed time so far *)
TOF — off‑delay timer
Q follows IN up immediately, and stays on for PT after IN drops (e.g. a run‑on fan).
VAR fan_off : TOF; END_VAR
fan_off(IN := heater_on, PT := T#30s);
fan := fan_off.Q; (* fan runs 30 s after the heater turns off *)
TP — pulse timer
A rising edge on IN gives exactly PT of Q, ignoring further edges during the pulse.
VAR horn : TP; END_VAR
horn(IN := start_button, PT := T#2s);
beeper := horn.Q; (* a 2‑second beep on each press *)
CTU — count up (with CTD / CTUD)
CV counts rising edges of CU; Q is true when CV ≥ PV; R resets.
VAR parts : CTU; END_VAR
parts(CU := part_sensor, R := reset_button, PV := 100);
total := parts.CV;
box_full := parts.Q; (* true at 100 parts *)
CTD counts down from PV (LD loads PV, Q when CV ≤ 0). CTUD does both (CU/CD, QU/QD).
R_TRIG / F_TRIG — edge detection
Q is true for one scan on the rising (R_TRIG) or falling (F_TRIG) edge of CLK. In Ladder
these are the P and N contacts.
VAR press : R_TRIG; END_VAR
press(CLK := button);
IF press.Q THEN count := count + 1; END_IF; (* once per press, not every scan *)
RS / SR — latches (memory)
RS is reset‑dominant, SR is set‑dominant. In Ladder these are the Set/Reset coils.
VAR latch : RS; END_VAR
latch(S := start_pb, R1 := stop_pb OR trip);
running := latch.Q; (* start latches on; stop/trip wins *)
Motor seal‑in (start/stop, no block)
running := (start_pb OR running) AND NOT stop_pb AND e_stop_ok;
motor := running;
In Ladder: start (NO) in parallel with a running (NO) contact, then
stop (NC) in series, driving the running coil.
Scale an analog input to engineering units
Use the library scaling block (raw counts → EU), then act on the value.
VAR s1 : SCALE; END_VAR
s1(raw := level_raw, raw_min := 0, raw_max := 27648, eu_min := 0.0, eu_max := 100.0);
tank_pct := s1.eu;
high_alarm := s1.eu > 90.0;
T#500ms, T#5s, T#2m30s.
Numbers can be hex (16#FF) or binary (2#1010).Ladder editor
Ladder POUs open in a graphical editor (contacts, coils, compare boxes, function‑block boxes, parallel branches, online power‑flow, and per‑rung ST editing). It has its own detailed help: open the Ladder editor help →
T1.Q, T1.ET),
each shown with its type. A contact or coil lists the BOOLs; a compare lists the numeric variables. A name
that is not declared yet can still be typed — Add missing declarations then writes it.SFC — sequences
A Sequential Function Chart describes a sequence: steps that are active or not, transitions between them, and actions that run while a step is active. It is how a batch or a phase is written, and it is read as a chart rather than as a page of IF statements.
Create one with New POU → Program — SFC (sequence), which writes the opening step for you. Any POU whose body starts with INITIAL_STEP is a chart. The tree then
offers a Chart view under that POU, which draws the sequence and lights up the step
the controller is in, with the time it has been there.
PROGRAM Fill
VAR start : BOOL; full : BOOL; pump : BOOL; END_VAR
INITIAL_STEP Idle:
END_STEP
STEP Filling:
ACTION RunPump(N); (* while the step is active *)
ACTION CountBatch(P); (* once, when it is entered *)
END_STEP
STEP Draining:
pump := FALSE; (* a statement works too *)
END_STEP
ACTION RunPump:
pump := TRUE;
END_ACTION
TRANSITION FROM Idle TO Filling := start; END_TRANSITION
TRANSITION FROM Filling TO Draining := full; END_TRANSITION
TRANSITION FROM Draining TO Idle := NOT full; END_TRANSITION
END_PROGRAM
| Qualifier | When the action runs |
|---|---|
N | Every scan the step is active. The default. |
P | Once, on the scan the step becomes active. |
S | Sets a BOOL and leaves it set: ACTION pump(S); |
R | Resets it. |
The timed qualifiers (L, D, SD, DS,
SL) are refused with a message rather than approximated. Use the step’s
own timer, which does the same job in a form anyone can read:
IF Filling.T > T#5s THEN …
Every step is a variable
Filling.X is TRUE while the step is active and Filling.T is how long
it has been active — ordinary symbols, so they can be watched on the variables page,
plotted on the trace, read over OPC UA, or used in your own logic. A step
that is not active reads T#0ms, never the time it spent there last time round.
Parallel and alternative branches
Two steps at once — a simultaneous divergence — is a list on either side:
TRANSITION FROM Start TO (LeftArm, RightArm) := go; END_TRANSITION
TRANSITION FROM (LeftArm, RightArm) TO Done := join; END_TRANSITION
The join waits for both arms. Where two transitions leave the same step, that is an alternative branch and the first one written wins — the chart is never in two places at once.
How it runs
On each scan: the elapsed times update, the actions of active steps run, and then every transition is evaluated before any of them is applied. That means one transition per branch per scan: a chart cannot race through five steps in one cycle, and every step it passes through is visible. A step entered on this scan runs its actions on the next one.
IO devices & scanning
Under IO Devices, add and configure the fieldbus/IO the controller talks to. Supported drivers include Profinet, Modbus TCP, EtherCAT, EtherNet/IP, and the sim driver (generates test signals against the real address map).
- Scan for RIO… / Scan Profinet network… — discover a Profinet coupler by DCP and commission it (station name / IP, import its module rack).
- Scan EtherCAT segment… / Add EtherCAT rack from ESI… — build an EtherCAT rack.
- Scan for EtherNet/IP… — discover EtherNet/IP adapters.
- Each device page sets connection parameters, poll cycle, exchange mode, and enable.
Addresses & process image
IO points live at %I (inputs), %Q (outputs), %M (memory), by bit
(%IX0.0), byte (%IB34), word (%IW2) or dword. Declare an alias and use
the name in your logic:
VAR
start_pb AT %IX0.0 : BOOL; (* a real input bit *)
level AT %IW2 : INT; (* an analog input *)
pump AT %QX0.0 : BOOL; (* a real output bit *)
END_VAR
Browse IO… (Ctrl+Space, outside the code editor's body pane) lists the addresses the device tree defines and inserts the declaration for you.
Signal ranges & scaling
An analogue card puts raw counts on the wire — the range it is wired for changes what those counts mean without changing the counts. The IDE reads the card's type and the ranges it supports from the vendor file (GSDML / ESI / EDS) and shows them on the device page and in the device tree:
2 CT-3238 8×AI · 0-20mA/4-20mA %IW2 ×8 4 CT-4234 4×AO · 0-20mA/4-20mA %IB34 ×1 · %QW0 ×4
In the device page's Modules table, the Signal range column opens a dialog with one row per channel — inputs and outputs — each with its own range, plus a "set every channel to" control for the common case. A rack routinely mixes a 4‑20 mA transmitter and a 0‑10 V positioner on one card, which is why the setting is per channel and not per card.
Pick a range and that channel is converted in the controller, the way an Allen‑Bradley analogue module does it:
| Range | What the program reads / writes |
|---|---|
| 4-20mA | 4000 … 20000 (microamps) |
| 0-20mA | 0 … 20000 (microamps) |
| 0-10V | 0 … 10000 (millivolts) |
Outputs are converted the other way: the program writes 4000…20000 and the card receives its counts. Leave a channel on raw counts to keep the previous behaviour and scale in the program.
Transmitter block defaults Ai_High to. A wrong raw limit gives a plausible
reading that is wrong by a constant, which is the hardest kind to spot.%IW holds: engineering units, not what the card sent. It
takes effect on the next Download hardware.IO-Link ports
An EtherCAT IO‑Link master module (for example the ODOT IP‑EC‑8IA) is added to the rack from its full ESI. Its device page then shows IO-Link ports (N)…: one page per port, with
- the port mode, cycle time and pin‑2 behaviour;
- device validation — expected Vendor ID and Device ID, so a wrong sensor is refused;
- start‑up ISDU parameters (index, subindex, data) written to the sensor at every start.
These are sent to the module as start‑up SDOs while the bus starts (PRE‑OP), and take effect on
Download hardware. In the program, the IO‑Link library blocks (lib/70_iolink.st) read and write
ISDU parameters, read events, and decode the big‑endian process data of an IO‑Link device.
Cores & threads
Task Configuration → Task Groups shows every long‑lived thread on the controller and which core it runs on: MainTask (the scan), each async IO driver, the OPC UA, HMI/web, Modbus and Monitor services. One Apply core assignment saves them together.
The program itself is single‑threaded: every PROGRAM in the task runs in sequence, in one thread, on one core. More cores do not make a scan faster — they keep other work off its back.
isolcpus=3 nohz_full=3 rcu_nocbs=3 in cmdline.txt and MainTask pinned to core 3, Linux
keeps every ordinary thread off that core by itself — stronger than pinning, and the services can stay
unpinned. The per‑service cores are for a controller whose kernel command line cannot be changed. The
one worth pinning either way is the async IO driver thread, the other time‑sensitive thread on the box.Panel lamps
| Lamp | Meaning |
|---|---|
| RUN solid | The scan is cycling and the program is in sole control. |
| RUN fast blink | Cycling, but one or more points are forced — the machine is not obeying its program alone. |
| RUN off | Stopped, faulted, or the runtime is not running. |
| IO solid | Remote IO configured and exchanging data. |
| IO blink | Configured but not all devices are connected. |
| IO off | No remote IO configured — nothing to be wrong. |
Build & download
| Action | Effect |
|---|---|
| Build (F7) / Build All (Ctrl+F7) | Compile the active / every enabled POU. Errors show in the Messages panel with clickable positions. |
| Download program (F8) | Send the logic to the running controller — the scan is not stopped (online change). If the controller's settings differ from the project they are sent with it: without a question when they apply while running, otherwise after asking. |
| Download hardware | Send the controller configuration. Without stopping when only the OPC UA server, screens server, Modbus slave or EtherNet/IP tag server changed (the toast says "the controller kept running"); a restart (control stops briefly) for IO devices, process image, task timing, accounts and the rest. |
| Download all | Hardware then program, in order. Use it the first time or after IO changes. |
| Download screens | Send the HMI screens so the controller serves them itself. |
| Upload | Read the running program back off the controller. |
| Compare… / Verify | Check the controller runs exactly what is open here (program and hardware). |
Online & debugging
- Online (◉) shows live values beside declarations and animates Ladder power flow.
- Inline values (editor toolbar, on by default) shows every variable's value right beside it in the code. The values stay while you read and click; typing, dragging over code or double-clicking a word puts them aside to edit, and Esc, a click outside the editor, a save or 10 s without changes brings them back. Double-click a value to write it.
- Timers and counters show their state on the call line and the declaration: IN and Q as lamps (green = TRUE) and the elapsed time against the preset, e.g.
IN Q 1.25s / 3s- amber while counting. Counters show CU/CD, Q and CV / PV. Double-click the badge for all members. - A folder in the tree shows RUNNING when a program inside it runs (N RUNNING for several).
- Run / Pause — start the scan / freeze the program while IO keeps running.
- Config mode — halt for hardware work: outputs go fail‑safe, inputs keep being read (watch channels while wiring). Leaving lands in PAUSED.
- Step over (F10), Step into (F11), Step out (Shift+F11), Continue (F5), and Breakpoints… for source‑level debugging.
- Reset fault — clear a fault and resume next cycle (fix the cause first).
- Controller log… reads the controller's log without SSH; Versions… shows IDE and runtime versions.
How long things have been running
- Task Configuration → Monitor — "Running for …", the time since the last runtime restart (the scan clock, not a wall clock).
- An IO device's page — Connected for … and the number of reconnects since the runtime started. The pair is what answers "has this link been solid?": a count alone cannot tell a rack that dropped an hour ago from one that dropped last week.
Minor faults: what stops the PLC and what does not
The controller stops only for what makes the program's state untrustworthy. Arithmetic that has no answer does not stop it — the same call a Logix controller makes:
| Condition | Result |
|---|---|
| Divide or MOD by zero | Minor — counted and reported with its line; the scan keeps running. Integer division yields 0; REAL follows IEEE (±INF, NaN). |
| Array index out of range | Major — the scan stops. The program's idea of its own memory is wrong. |
| Runaway loop (instruction budget), stack overflow | Major — the scan stops. |
Where is this used? (cross-reference)
Put the caret on a variable and press Where used in the editor toolbar. It searches
every POU and globals.st, and classifies each hit: a declaration, a
write, or a read. That distinction is the point — before changing a value on a
running machine you need to know who depends on it and who else can change it, and one
undifferentiated list makes you work that out by eye.
Each hit names the scope it is in, so Main3.st:12 tells you whether it is
the global or a local of the same name. Mentions inside comments and string literals are not
reported.
batch_total was a global DINT and a local BOOL at
the same time.Online change — downloading without losing the batch
A download normally starts the new program from its initial values, which for a machine mid-batch throws away the batch. Online change is on by default: every variable that still exists with the same name and the same type keeps its value.
| What happens to a variable | When |
|---|---|
| Carried over | Same name, same type. |
| Reset to its initial value | New, or the type changed. An INT
that became a REAL is a different variable that happens to share a name —
copying the bits would give a plausible reading that is wrong. |
| Dropped, and named in the report | It no longer exists. The value that was lost is usually the one that mattered, so it is named rather than counted. |
Matching is by name and type, never by address: offsets shift as soon as a declaration is added, and copying by offset would quietly move one variable's value into another.
Timers and counters keep their state — a valve that has been open 40 s does not
become one that has been open for nothing because a rung was edited — but their preset
(PT, PV) comes from the new program text, so shortening a delay
takes effect immediately. A sequence stays in the step it is in.
Variable tables: write & force
A POU's Variables view and the Global Variables page show, when connected, a live table: Expression · Type · Value · Prepared value · Address. Open a function‑block instance, a structure or an array with ⊞ (or ⊞ Expand all).
- Type the new value into Prepared value. For a BOOL, click the box to cycle TRUE / FALSE / (nothing). Double‑click a Value to copy it into the box. Enter only moves to the next box — it never writes.
- Write values (Ctrl+F7) sends every prepared value once. The program may change it again on its next scan if it assigns that variable.
- Force values (F7) holds the prepared values of %‑addressed points every scan until released. Forced rows are highlighted and the FORCE badge appears.
- Release force (Alt+F7) releases the forces on the rows shown; the FORCE badge releases them all.
Right‑click a row for Write all prepared values (N), write only that row, force or release.
Trace / scope
Online → Trace / scope… records up to eight variables at scan rate and plots them. It answers a different question from the monitor page. The monitor says what a value is, a few times a second; a trace says what it did — between two scans, during the step, while the valve was opening. A PID that overshoots, a sensor that glitches for one cycle, an interlock that drops for 20 ms: none of that is visible to anything that polls.
Pick the variables, choose a depth and press Arm & record. Members of a function block
are listed with their instance — a timer’s ET, a transmitter’s flags — and can be
recorded; the instance itself has no single value to plot and is refused.
| Control | What it does |
|---|---|
| Depth | How many samples the controller’s ring holds. When it is full the oldest are overwritten, so depth sets how far back the plot reaches. |
| Sample every | Record one scan in N. At 10 ms, “every 20 scans” gives 200 ms resolution over twenty times the history. |
| Stop / Clear | Stop leaves the samples on screen. Clear releases the buffer on the controller. |
| Export CSV | Scan number, time in milliseconds, and one column per channel. |
The x axis is the controller’s scan number, converted to time with the task period — not when the browser received the samples. Two traces of the same machine line up by it.
Each channel is scaled to its own range, as on any trend tool: a 0–27648 analogue input and a BOOL on one shared axis would draw the BOOL flat on the floor. The legend carries each channel’s minimum and maximum, and the value under the crosshair as you move across the plot. BOOLs, counters and timers are drawn as steps, because they held their value until a scan changed it; a REAL is drawn straight between samples.
A download stops the recording rather than sampling memory that now belongs to a different program. Arming is recorded in the audit trail with the user, the channels and the depth; reading a trace someone else armed needs only viewer rights, arming one needs engineer.
Batch phases (ISA-88)
A recipe says “charge 500 kg, then heat to 60 °C”. It does not say which valve, and it must never need to. Between the recipe and the machine sits a phase: a fixed interface the batch engine drives, so one recipe can drive any phase and one phase can be written once. Without it every phase is bespoke and every recipe change touches PLC code — which in a regulated plant means revalidation for a change of setpoint.
Create one with New POU → Program — S88 phase (batch). The template is already wired: the state machine is called every scan, and a chart underneath it does the work.
The phase state machine
| State | Meaning |
|---|---|
| IDLE | Waiting to be started |
| RUNNING | Doing its work |
| COMPLETE | Finished normally; waits for RESET |
| HOLDING → HELD | Going to a safe condition, then held |
| RESTARTING | Coming back from HELD |
| STOPPING → STOPPED | Controlled stop; needs RESET |
| ABORTING → ABORTED | Fastest safe end; needs RESET |
| RESETTING | Clearing back to IDLE |
Commands are cmd: 1 START, 2 HOLD, 3 RESTART, 4 STOP,
5 ABORT, 6 RESET, 7 PAUSE, 8 RESUME.
An interlock that drops mid-run holds the phase rather than aborting it, for the same reason. A failure aborts, and its code is kept so the record can say what failed.
Writing the logic
The chart reads the phase’s outputs instead of decoding the state:
| Output | True while |
|---|---|
ph.run_logic | RUNNING or RESTARTING — do the work |
ph.hold_logic | HOLDING — go to the safe condition |
ph.stop_logic / ph.abort_logic | STOPPING / ABORTING |
Set step_done when the work is finished, or failed with a
fail_code when it cannot continue.
Units, arbitration and mode
| Block | What it is for |
|---|---|
S88_UNIT | The vessel or skid a batch is made in: which batch owns it, its mode, whether a phase is active |
S88_RESOURCE | Shared equipment — a transfer line, a CIP skid. First come, first served, with a queue you can see |
S88_MODE | AUTO / SEMI / MANUAL and PROGRAM / OPERATOR / EXTERNAL: who may command this equipment |
Controller alarms
Online → Alarms… shows the alarms the controller raises. They are evaluated on the scan thread, so the time on an alarm is when the condition changed — not when a screen noticed. On a 10 ms scan behind a screen polling twice a second those differ by enough to put two alarms in the wrong order, and “what happened first” is the question an excursion is investigated with.
| State | Meaning |
|---|---|
| UNACK_ACTIVE | Active, nobody has seen it |
| ACK_ACTIVE | Active, somebody has |
| UNACK_RTN | It went away before anybody saw it — kept, not forgotten |
| SHELVED | An operator silenced it, temporarily |
| SUPPRESSED | The design says it is meaningless now (low flow on a stopped pump) |
| OUT_OF_SERVICE | Maintenance removed it |
Shelving is always temporary. It expires by itself and the attempt to shelve for zero time is refused — an alarm silenced for ever is a deleted alarm. Every acknowledgement, shelve and out-of-service is recorded with the name of whoever did it.
Configuring them
"alarms": [
{ "id": 1, "name": "TT_01_HIGH", "message": "Reactor temperature high",
"priority": "high", "area": "M", "offset": 0, "bit": 0, "delay_ms": 500 },
{ "id": 2, "name": "LOW_FLOW", "priority": "low",
"area": "M", "offset": 0, "bit": 2,
"suppress": { "area": "M", "offset": 0, "bit": 3 } }
]
The condition is not configured here. level > 9000 AND pump_running is
logic and belongs in the program; the program sets a bit and the alarm watches it. That keeps the
cost at one bit test per alarm and keeps the alarm’s meaning readable in the program.
Electronic signatures
Online → Sign… records that a named person took responsibility for one specific thing, at a stated time, for a stated reason — the three components 21 CFR Part 11 requires.
A signature carries a meaning from a fixed list — approved, reviewed, released, rejected, verified, authored — because that field is read across hundreds of records and free text turns it into archaeology. It binds to a SHA-256 rather than to the document, so anyone can check that what they are holding is what was signed.
An account that still carries the password it was created with cannot sign at all: a credential anybody could guess is not evidence that a particular person took responsibility.
Online → Signatures… lists what this controller has taken since it started; its audit log holds them all, and that is the record of legal weight.
Validation documents
If you are putting this controller into a GxP plant, your validation team will ask for a specification chain before they ask for anything else. It ships with the product, in docs/:
| Document | What it is for |
|---|---|
| 27 — Validation Plan | How the platform is validated, and where our responsibility stops. The boundary is the download: the platform is ours, the application program is yours. |
| 28 — User Requirements | 99 requirements, each risk-classed and each tied to the regulation or process need behind it. Your own URS is written against this one. |
| 29 — Functional Specification | How the product satisfies each requirement. |
| 30 — IQ / OQ protocols | Ready to execute on your installation, step by step, with the evidence to attach at each step. |
| 31 — Traceability Matrix | Requirement → design → test evidence, checked in both directions. |
| 25 — QA and Memory Report | What was actually run, what it found, and what was done about it — including the defects. |
This controller is not a safety system. It carries no SIL or PL rating and must not implement a safety function. Emergency stop and guard interlocking belong in a certified safety relay, wired independently of this controller. No document in the set changes that.
Online → Controller
Everything about the machine rather than the project on it, grouped at the top of the Online menu: its log, its clock, its versions, and its run state.
Controller log… reads the runtime log without an SSH session. Set clock / time… reads and sets the controller time and writes the hardware RTC. Versions… shows what the IDE and the runtime are. License… shows whether this controller is licensed, its hardware id, and installs a license file. Check for updates… asks optizeus.org for a newer IDE.
Targets & connection
Target… lists the controllers this IDE can use: the local Simulator and your real controllers (by host/IP, optionally over an SSH tunnel). The active target is what Build/Download/Online act on. Login is per connection — a reconnect starts a fresh session, so log in again after reconnecting.
OPC UA server
OPC UA Server publishes the process image and program variables to OPC UA clients. Settings: port (default 4840), security policies and certificate, anonymous vs username/password, and which tags to expose. See the server's page for details; changes reach the controller on Download hardware.
Modbus TCP slave
Modbus TCP Slave exposes ranges of the process image as Modbus coils/registers for a SCADA/HMI master. Map each range to an area and object type, enable it, and Download hardware.
EtherNet/IP tags & EDS
Controller Settings → HMI tag server (EtherNet/IP) serves Zeus symbols by name to an
EtherNet/IP HMI or SCADA — including function‑block members such as TT_01_01.EU_Value — with no
register mapping. On the panel, use its Allen‑Bradley CompactLogix / TreeTag driver. Off by default, and
read‑only unless you enable writes.
Create EDS file…
An EtherNet/IP engineering tool shows the controller as an unknown device until its EDS is registered on that PC. Controller Settings → Create EDS file… generates it for the model you pick (IPC‑CM400200, IPC‑CM500200, or both). Register it with the tool's EDS hardware installation utility, then browse again.
- Save the tag‑server settings first — the EDS records them.
- The controller answers discovery on UDP and TCP (port 44818 by default), so it appears in a browse.
- The EDS matches the controller on vendor, device type, product code and major revision; the product code follows the board (CM4 / CM5) and the serial number is the board's own.
- No Class 1 (cyclic I/O) connection is declared — the controller serves tag messaging, not produced/consumed assemblies.
HMI screens
HMI holds operator Screens, Alarms and Events. Add screens (Add operator screens…), draw them, and Download screens so the controller serves them. Bind screen objects to program/global variables; alarms are defined against tags.
Besides the live widgets there are plain shapes — Rectangle, Ellipse, Line, Polygon — and Image, which shows a picture from the project's image pool (Images…: PNG/JPEG up to 1 MB, or SVG cleaned like an imported symbol). Every widget has a Style section: fill, line colour and width, corner radius, rotation, opacity, text colour and alignment.
Symbol library. Symbol library… in the screen editor (also at the bottom of a plant
symbol's Shape list) browses about 1,080 built-in plant drawings in 39 categories — tanks, pumps,
valves, instruments, piping, 3D equipment, HVAC, callouts… — with a search over every title and tag.
Click a thumbnail to insert it at the centre of the screen, double-click to insert and close.
The drawing is copied into the project (hmi/symbols/, cleaned like Import SVG…) and goes
to the controller with Download screens; inserting it again reuses the copy. Colour with state
(imported drawings only): none — drawn as it is (the default for library drawings); whole
symbol — the whole drawing tinted green running / red fault / grey off or unknown, shading kept;
elements with class zeus-state — only the marked parts follow. Manual §32.4.4.
Dynamics make one property of a widget follow one variable, with a declared rule rather than code:
| Property | Rules |
|---|---|
| X, Y, Width, Height, Rotation, Opacity, Line width | Scale — from a value range to a property range, linear and clamped |
| Fill, Line colour, Text colour | Bool (one colour when TRUE, another when FALSE) or Map (first matching range wins, else a default) |
| Visible, Blink (1 Hz) | Cond — a comparison: == != < <= > >= and a value |
| Text | Format (%d %i %u %x %X %f %.1f %e %s %%, e.g. %.1f °C) or Map |
| Enabled (buttons, input fields) | Cond |
A rule whose variable the controller does not report draws the widget grey with ? — never the healthy colour — and disables it if it writes. Rules that cannot be drawn (wrong rule type, bad operator, colour or format) are refused when the screen is saved. Manual chapter 32, §32.4.6–32.4.8, has a worked example.
Faceplates (templates). A template is drawn once for a kind of equipment with its
variables written against a parameter — $M.running, or ${M}_speed when more
letters follow — and placed many times: create it with New screen… → Template (faceplate),
list its parameters (up to 16) in the properties panel with nothing selected, then put it on screens with
Add: Faceplate, choosing the template and an argument for every parameter (M = P101).
Fit keeps proportions or stretches to the box. A Screen link with Open as dialog
over this screen opens a template (with arguments) or a screen as a dialog — close with × or Esc, up to
3 stacked. $M in text and titles is replaced too. Saving refuses a reference to an undeclared
parameter, a missing or extra argument, a template that does not exist, nesting deeper than 4 and loops
(A framing B framing A) — save the template first.
Generate faceplate… builds a template from a function-block type (library or project): parameter
I, lamps for BOOL outputs, values for numeric outputs, buttons for BOOL inputs (momentary for
start/stop/reset/ack, toggle otherwise) and input fields for numeric inputs. A start-like input next to a
stop-like one (MOTOR_DOL start/stop, read as levels) becomes a latching pair —
START = reset stop, set start (asks first); STOP = reset start, set stop — and inputs the field drives
(fb_…, ext_…, …running/trip/fault/interlock…) are lamps, not buttons. Check the
buttons against how the block reads its inputs. Manual chapter 32, §32.4.9.
Input elements. Slider and Knob (minimum, maximum, step; written once, on
release), Meter (round gauge with coloured bands), 7-segment, Checkbox (BOOL),
Radio buttons and Drop-down (options: value written + text shown), Array table (bind
the ARRAY; rows are the name[i] elements the controller reports, columns are members of an
array of structures; optionally Editable), Tabs (pages: a screen, or a template with
arguments, checked like a Faceplate) and Date/time (the panel clock, or a DT/TIME variable, with a
format such as yyyy-MM-dd HH:mm:ss). A variable the controller does not report — or a value
matching no option — shows grey with ?. An Input field (and an editable table) with
Limits ticked refuses a value outside minimum … maximum on the panel before anything is written.
Keypad. An input field opens an on-screen keypad (Keypad: on touch panels — the default, when the device has no fine pointer — always or never): numbers with the allowed range, or QWERTY for a STRING. OK writes; Cancel, Esc or a tap outside write nothing.
Actions. A Button, or any display element with On click ticked, can run a list of actions in order: set, reset, toggle, write a constant, increase/decrease by a step (clamped), go to a screen, open a dialog (screen or template with arguments), close this dialog. The confirm text is asked once; the list stops at the first refused write; an element disabled by an Enabled rule does nothing. A button has either On press or actions, never both. Targets and arguments are checked on save.
Hotkeys. With nothing selected, Hotkeys binds F1…F12 or Esc (optionally with Shift) to an actions list. They act on the top view only (the top dialog while one is open), never while typing in a field or with the keypad open. Manual chapter 32, §32.4.10–32.4.13, has a worked example.
Alarm table. Shows the controller's alarms (manual chapter 27) — not bits of the
screen. Choose and order the columns (time, priority, state, name, message, value, acknowledged by), the
lowest priority shown, an optional name filter (exact names or prefixes like TT_*), a row limit
and a colour per priority. Unacknowledged alarms come first and blink until acknowledged.
Acknowledge takes the selected row, Acknowledge all shown (after a confirmation) the
unacknowledged rows the table shows; both need an operator log-in and are recorded with the user.
History shows the controller's journal, newest first. A time marked ~ was stamped while the
controller clock was not synchronised. Bind @alarms.unacked or @alarms.active
(read-only counts) to a lamp or value, or follow them with a rule, for a header lamp that blinks while
anything is unacknowledged. Manual §32.4.14.
Text lists and languages. Text lists… edits hmi/texts.json: the languages,
the default, and named lists of key → text per language (import/export CSV
list,key,en,he…). In any text write @List.key for an entry, or @List
on a value, lamp, button, checkbox or symbol for the entry whose key is its value (0,
1, TRUE…); also in option texts, alarm-list rows, tab and dialog titles and
map text rules. A missing translation falls back to the default language, then to the key.
Language selector in the header (screen properties) lets the panel choose; the choice is kept per
panel; Hebrew/Arabic set the text direction (the layout is not mirrored). Manual §32.4.15.
Access. Every widget: Visible to (viewer/operator/engineer) and Operated by (operator/engineer) — below it the element is hidden or drawn locked, from the logged-in user's role. A writing element needs an operator by default. Display only: the controller still refuses what the role may not do. Manual §32.4.16.
Recorded trends. Tick Recorded on a trend (optional sample period and samples kept): the
controller records it in memory and the panel shows the history when the screen opens, then continues live.
The IDE builds web.trends from every recorded trend on every screen and faceplate (arguments
filled in, duplicates merged, at most 32 — the rest is a warning in Messages) — it takes effect after
Download hardware. Manual §32.4.17.
Recipes. Recipes… defines a recipe: variables in write order, label, unit, optional
min/max (hmi/recipes/<name>.json). The Recipe widget lets the operator choose a
set, Load from PLC, edit values on the keypad (range-checked), Save / Save as… /
Delete (sets stored on the controller) and Download to PLC — every value checked first, then
written in order after a confirmation, stopping at the first refusal. Saving, deleting and downloading
need an operator. Manual §32.4.18.
Log-in on a panel. With accounts, the operator page shows its own log-in page (keypad on touch panels), shows the user and Log out in its header, counts down a lockout after failed log-ins, and returns to the log-in page when the session ends. Manual §32.6.3.
Local display (HDMI). The controller can show its screens on a monitor or touch panel on its own
HDMI port: Controller Settings → Local display (HDMI) (engineer, a controller target — not the
Simulator). Install on controller… once (installs a full-screen browser over SSH with sudo; the
controller needs internet access for about 250 MB), then set Enable, Start screen,
Rotation, Hide cursor, Wait timeout and press Apply…; Restart display…
reloads it. The display opens http://127.0.0.1:<web port>/, so the screens server must be
enabled on 127.0.0.1 or 0.0.0.0. With accounts it shows the panel log-in page with
the keypad. Not yet bench-tested on every display. Manual §32.11.
Users & roles
Users & Roles defines who may connect to the controller and what each may do (the same accounts are used by the IDE and by the operator screens). Set passwords here; a user still on its creation password may be blocked from downloading until it is changed.
Simulator
127.0.0.1) is not simulated:
the simulator really connects to it, so a 3D Factory I/O scene (Modbus TCP/IP Server driver, port 502) can be driven
from your program - with no time limit. Open the example F1-FactoryIO-Conveyor; Factory I/O "Input n" is
%IX(n/8).(n mod 8), "Output n" is %QX(n/8).(n mod 8), register input/output n is
%IW/%QW(2+2n). Operation Manual 26.6a.The Simulator runs the exact controller binary locally with your configuration, so you can develop and test with no hardware. Start/stop and configure it from its tree node; a program downloaded to it is compiled and bound against the same addresses the real controller uses. A restart clears the downloaded program (there is no boot application), so re‑download after starting it.
License & demo time
The IDE is free. The runtime decides what it may do: a Zeus controller - IPC-CM400200 (CM4) or IPC-CM500200 (CM5) - carries a license, signed by Zeus Automation, for its own CPU serial number, and runs without a limit.
| Runtime | Behaviour |
|---|---|
| Zeus controller (licensed) | No limit. |
| Simulator, or any other hardware, with simulated IO only | No limit - learn, test and train freely. |
| Simulator or other hardware with real IO: any fieldbus device (Profinet, EtherCAT, EtherNet/IP, Modbus TCP RIO) or the Modbus TCP slave | Demo: the program runs for 2 hours, then stops - the controller goes to CONFIG (outputs no longer driven, inputs still read) and refuses Run until the runtime is restarted (another 2 hours) or a license is installed. |
The DEMO pill counts down; Messages warns at 10 minutes and 1 minute, and a window says when the program has stopped. Online → Controller → License… shows the status and the machine's hardware id (with Copy): send it to Zeus Automation, and install the file you receive with Install license file… - it takes effect at once, also after the demo has ended (the controller goes to PAUSED; press Run). Not for production on anything but a Zeus controller.
Updates
15 seconds after it starts and every 12 hours the IDE reads a small file on optizeus.org with the latest
release number (nothing about this PC or its projects is sent). When a newer one is out, a blue
⬆ Update 0.1.x.y button appears in the top bar: click it for what is new, then Download,
Later or Skip this version. ⟳ Check for updates asks at once. Offline it stays silent;
ZPR_UPDATE_URL=off turns the check off. To update, unzip the new package next to the old one
(Operation Manual 2.8) - projects are not touched, and the controller's runtime is updated separately with
Deploy runtime.
Keyboard shortcuts
| Key | Action |
|---|---|
| Ctrl+S | Save project |
| Ctrl+Shift+S | Save As… |
| F7 / Ctrl+F7 | Build / Build All |
| F8 | Download program |
| Ctrl+F | Find in this POU |
| Ctrl+Shift+F | Find / replace in the whole project |
| Esc | In the code editor online: bring the inline values back |
| F2 | Rename variable at caret |
| Shift+F12 | Where used |
| Ctrl+Space | Browse IO / insert declaration; in the code editor's body pane, the completion list only |
| F10 / F11 / Shift+F11 / F5 | Step over / into / out / Continue |
| Ctrl+Z / Ctrl+Y | Undo / redo in the code editor |
| Alt+drag | Column selection in the code editor |
| In an online variable table | |
| Ctrl+F7 | Write every prepared value once |
| F7 | Force the prepared values of %‑addressed points |
| Alt+F7 | Release the forces on the rows shown |
| Esc | Clear the prepared value being edited |
Limits
| Item | Limit |
|---|---|
| POUs in one download (library blocks, FBs, methods, functions, programs) | 256 |
| Parameters of one FB or function | 64 |
| Members of one STRUCT or FB | 128 |
| Programs in the task call list | 16 |
| IO devices | 16 |
| Controller alarms | 10 000 |
| OPC UA instance folders | 1 000 |
| Array elements listed per array online | 100 (the rest are reachable by index) |
| Symbol list sent to the IDE | about 12 MB |
| Trace channels | 8 |
| Signal‑range scalings per device | 32 |
The Operation Manual, Appendix D, lists every limit and what to do when you reach one.
Troubleshooting
| Symptom | Cause & fix |
|---|---|
| Badge shows SIM IO (amber) though hardware is wired | The IO device uses the sim driver. Switch it to the real driver (e.g. Profinet) and Download hardware. |
| PROGRAM CHANGED after editing | The open program differs from the controller. Download it. If you are editing a program the controller isn't running, that's expected until you download it. |
| Online shows no values | Turn Online on, make sure you are logged in, and that this program is the one downloaded. |
| "not in static storage" writing a variable | The name collides with a library block member (start, stop, reset…). Use a distinct name, and make sure the program that declares it is the one running. |
| Can't connect over OPC UA | Check the endpoint/port, security policy, and whether anonymous is allowed or a username is required. |
| A rung's contacts all read TRUE but the timer never starts | A local variable shadows a global of the same name. The program uses the local; the monitor, the ladder's live values, a force and a write all resolve the name to the global, so the screen shows a different variable. Build shows a warning naming both declarations — rename one of them. |
| An analogue channel reads a plausible but wrong number | Check the channel's signal range on the device page, and the raw full‑scale figure against the card's manual. A wrong raw limit is wrong by a constant. |
| Restoring a data snapshot takes a long time | An older runtime writes one value per request. Deploy the current runtime: the whole set goes in one request, and values that cannot change are skipped before sending. |
| "Connection time not reported by this runtime" | The controller runs a runtime older than this field. Deploy the current runtime. |
| Controller not connected | Check the Target… host/IP and that the runtime is running; log in again after a reconnect. |