← Help contents

Lua Scripting

Automating a device when clicking Send by hand stops being enough

A macro sends one frame. A trigger answers one pattern. A script does what neither can: a sequence with waiting, decisions and a summary at the end — start a drive, hold it for five seconds, read the fault register, write the result to a chart. Scripts are written in Lua 5.4 and run inside a sandbox that cannot reach your disk outside its own folder, cannot open a second connection, and only sees the serial port through a small, dedicated API (33 functions — see the next page).

Where you write it

The editor fills the Script tab. What is on screen is what runs — nothing is compiled from a copy or from a file you saved earlier, so there is never a question of which version went out. The editor saves your typing to the module config after a short pause, so even a script you never named comes back after a restart.

The Script tab

The Script tab — editor on the left, file path and run control above it

You start a script from one of two places: the toggle switch on the top bar (next to the Script icon), or the run button next to the file path inside the tab. Both do exactly the same thing, and both show the real state — set by the script.started / script.stopped / script.error events, not by your click. So a script that crashes on its own turns both controls off, and starting it from the bar lights up the button inside the tab too.

The file path above the editor names the script. Load one of the fifteen built-in examples from the list — AT commands for an ESP, a Modbus read, timers, VT100 and ANSI demos, several chart recordings — or open one of your own from the scripts folder. Saving writes back to the same folder, never anywhere else on your disk.

⚠ The examples are read-only in practice. They live in scripts/examples and the app rewrites all fifteen at every start, so they are always the reference version — and any edit you make to one of them is gone the next time you open the app. Want to change an example? Save it under a new name in scripts first; that folder is yours and nothing touches it.

How a script runs

The script runs in the background, next to the interface rather than inside it — the app stays usable while it works. It is a single thread with cooperative scheduling: only one piece of Lua runs at a time.

One rule decides whether your script works at all: incoming data is handed to your callback only while the script is inside sleep() or wait_for(). Register on_data and then sit in a tight loop with neither of them, and the callback never fires — not slowly, never.

on_data(function(line)
  log('RX: ' .. line)
end)

send_raw('AT\r\n')
sleep(1000)          -- the callback fires here, not before

Stop the script with the same toggle or button you started it with, or with stop() inside the script. Disconnecting the port stops it as well.

What a script may and may not do

Scripts run in a sandbox — Lua 5.4 with the dangerous parts removed:

  • no os, io, debug, package, require, loadfile, dofile
  • no load, rawset, rawget, rawequal, rawlen, collectgarbage
  • no string.dump, and the shared string metatable is locked, so a script cannot replace the string library underneath everyone else
  • 16 MB of memory
  • file operations reach only the app's own script folder, never the rest of the disk

Everything that goes to the port goes through the app, so a script cannot open a second connection behind your back.

Inside the script folder, paths are relative, may have subdirectories, and .. and absolute paths are rejected — by one check that splits the working folder from the rest of the disk. The folder root is untouchable: delete_dir('.', true) is refused, so a runaway script cannot wipe the folder it never wrote. A symbolic link that leads out of the folder is rejected too, even when it has no .. in its name.

A first script

Ask a device its identity, wait up to a second for the reply, stop either way:

if not serial_connected() then
  log_warn('Open the port first')
  stop()
end

on_data(function(line) log('RX: ' .. line) end)

send_raw('*IDN?\r\n')
local ok = wait_for('OK', 1000)   -- plain text, not a Lua pattern
log(ok and 'device answered' or 'no answer in 1 s')
stop()

Three mistakes everybody makes once

  • Callbacks that never fire — no sleep() or wait_for() after registering them.
  • Lua patterns in wait_for() — it matches plain text, so wait_for('OK', 1000) works and wait_for('%a+OK', 1000) does not.
  • Sending on a closed portsend_raw() raises an error instead of returning one, so check serial_connected() first.
💡 send_raw() sends bytes, not text. It does not append a line ending, it does not turn \r into a real CR, and it does not parse hex. If you want any of those, write a one-line helper in your script — that keeps the port honest about what you send and lets each script pick its own convention.