Lua API reference
33 functions a script can call, grouped by what they do
Sending 1
send_raw()The ONLY way a script sends to the port.
send_raw(data: string)
The ONLY way a script sends to the port. The string's bytes go out exactly as they are: nothing is appended, no escape sequence is expanded, no hex is parsed.
send_raw('AT\r\n') -- AT followed by CRLF
A Lua string is a byte string, so send_raw('\xFF') puts byte 0xFF on the wire. Throws if the port is not connected.
Receiving 3
on_data()Registers a callback that fires for each received serial data chunk.
on_data(callback: function(line: string))
Registers a callback that fires for each received serial data chunk. Only ONE on_data callback active at a time (last call wins). Callback is dispatched during sleep()/wait_for() — NOT immediately.
on_data(function(line)
log('RX: ' .. line)
if line:find('OK') then
log('Got OK response')
end
end)
CRITICAL: If your script has no sleep() or wait_for(), on_data will NEVER fire. You must have a sleep loop or wait_for() to allow cooperative dispatch.
on_raw()Registers a callback that fires for each RAW received chunk as a hex string (e.g.
on_raw(callback: function(hex: string))
Registers a callback that fires for each RAW received chunk as a hex string (e.g. '01 44 03 53 01'), WITHOUT UTF-8 conversion and WITHOUT splitting on newlines. Use this for BINARY protocols like Modbus RTU, where frames have no newline terminator and on_data would never deliver them. Only ONE on_raw callback active at a time. Dispatched cooperatively during sleep()/wait_for(), exactly like on_data. on_data (text lines) and on_raw (binary) can be used together.
local buf = ''
on_raw(function(hex)
buf = buf .. hex:gsub('%s','')
-- assemble a Modbus frame from buf yourself (by expected length or by a silence gap),
-- then verify CRC and decode. on_raw gives you the exact bytes the device sent.
log('RAW: ' .. hex)
end)
while true do sleep(150) end -- needed so on_raw fires (cooperative dispatch)
Same rule as on_data: without a sleep()/wait_for() loop the callback NEVER fires. Choose on_raw for Modbus RTU / binary framing; on_data for AT/text line protocols. The hex is the raw RX exactly as received — reassembly and framing (length or T3.5 silence) is up to your script.
wait_for()Synchronously waits for received data containing 'pattern' (substring match).
wait_for(pattern: string, timeout_ms: number) → string|nil
Synchronously waits for received data containing 'pattern' (substring match). Returns the matching line, or nil on timeout. Also dispatches on_data callbacks while waiting.
send_raw('AT\r\n')
local resp = wait_for('OK', 2000)
if resp then
log('Device responded: ' .. resp)
else
log_warn('Timeout — no OK received')
end
Pattern is a simple substring match (not Lua pattern). Consumes the matched line from the queue.
Terminal output 2
emit()Writes one line to the terminal as SCRIPT OUTPUT, on the script's own channel.
emit(text: string)
Writes one line to the terminal as SCRIPT OUTPUT, on the script's own channel. It does NOT pretend to be received data: the RX counters do not move, triggers do not fire, on_data callbacks do not run. Use it for progress messages.
emit('step 3 of 6 — calibration done')
Its own channel is what keeps script messages on separate lines instead of glued onto device frames — which is what happens with a protocol that has no line terminator, such as Modbus RTU. The text is NOT run through the ANSI parser and the VT100 emulator ignores this channel entirely, so colour codes, cursor moves and screen clears do nothing here. To make the app react as if the device had sent something — triggers, parsers, the emulator — use emit_rx.
emit_rx()Publishes BYTES as data RECEIVED from the port — the app reacts exactly as if the device had sent them: triggers fire,…
emit_rx(data: string) -- BYTES, not text
Publishes BYTES as data RECEIVED from the port — the app reacts exactly as if the device had sent them: triggers fire, the buffer grows, on_data callbacks run, the VT100 emulator renders them. Takes a raw byte string, so zeros and non-UTF-8 are fine: emit_rx('\1\3\2\0\10\56\67') injects a complete Modbus RTU frame.
Logging 2
log()Sends an info-level log message.
log(message: string)
Sends an info-level log message. Displayed in the script log panel in the UI.
log('Script started, connecting...')
log_warn()Sends a warning-level log message.
log_warn(message: string)
Sends a warning-level log message. Displayed with warning styling in the UI.
log_warn('No response after 3 retries')
Timing & control 7
sleep()Pauses script execution for ms milliseconds.
sleep(ms: number)
Pauses script execution for ms milliseconds. CRITICAL: During sleep, on_data callbacks and timer callbacks are dispatched cooperatively (in 50ms chunks). This is the primary mechanism for callback execution.
send_raw('AT\r\n')
sleep(1000) -- wait 1 second for response, on_data fires during this
millis()Returns milliseconds elapsed since script started.
millis() → number
Returns milliseconds elapsed since script started. Replacement for os.clock() (unavailable in sandbox).
local start = millis()
-- ... do work ...
log('Took ' .. (millis() - start) .. 'ms')
now()Date and time from the user's clock, as text.
now([pattern: string]) → string
Date and time from the user's clock, as text. The sandbox removes `os`, so this is the only way a script can tell what day it is — and 'measured on' is the most natural entry of a chart's meta box. LOCAL time, not UTC: whoever describes a measurement wants the hour from their own clock. The pattern is strftime-style and optional, defaulting to '%Y-%m-%d %H:%M:%S'; an invalid pattern raises an ordinary error you can catch, it does not kill the script engine. Use millis() instead for measuring how long something took — now() follows the wall clock and jumps when the system time changes.
create_chart_file('run', {
unit = 'V', step = 0.01,
meta = { measured = now(), operator = 'lab 2' },
})
-- a name that will not collide on the next run
local nazwa = 'run_' .. now('%Y%m%d_%H%M%S')
stop()Gracefully stops the currently running script from Lua side.
stop() → boolean
Gracefully stops the currently running script from Lua side. Sets internal running flag to false and cancels all timers.
log('Test complete')
stop()
Recommended for finite diagnostics that register on_data/on_error callbacks.
set_timeout()Fires callback ONCE after ms milliseconds.
set_timeout(ms: number, callback: function) → timer_id: number
Fires callback ONCE after ms milliseconds. Returns timer ID for cancellation. Cooperative — fires during sleep()/wait_for().
local id = set_timeout(5000, function()
log('5 seconds elapsed')
send_raw('AT+STATUS\r\n')
end)
set_interval()Fires callback repeatedly every ms milliseconds.
set_interval(ms: number, callback: function) → timer_id: number
Fires callback repeatedly every ms milliseconds. Minimum interval: 10ms. Returns timer ID. Cooperative dispatch.
-- Poll device status every 2 seconds
local id = set_interval(2000, function()
send_raw('AT+STATUS\r\n')
end)
clear_timer()Cancels a timer created by set_timeout or set_interval.
clear_timer(timer_id: number) → boolean
Cancels a timer created by set_timeout or set_interval. Returns true if timer was found and cancelled.
local id = set_interval(1000, function() send_raw('PING\r\n') end)
sleep(10000) -- ping for 10 seconds
clear_timer(id)
Status 2
serial_connected()Returns true if serial port is currently connected.
serial_connected() → boolean
Returns true if serial port is currently connected. Checks live status via bridge.
if not serial_connected() then
log_warn('Serial port not connected!')
return
end
send_raw('AT\r\n')
on_error()Registers a callback for serial port errors and disconnections.
on_error(callback: function(error_msg: string))
Registers a callback for serial port errors and disconnections. Fired when serial_port.disconnected or serial_port.error events occur. Only ONE on_error callback active at a time.
on_error(function(err)
log_warn('Serial error: ' .. err)
end)
Charts 8
A recording IS a file. The script writes measurements straight to disk; the chart tab only reads them and redraws as the file grows. There is no separate 'live mode': a chart drawn from a file that is still growing IS the live view. A recording therefore survives closing the tab, restarting, even a crash — and an older measurement is simply another file, so showing it does not interrupt one in progress.
create_chart_file()Creates a NEW recording and writes its header.
create_chart_file(name: string, options: table)
Creates a NEW recording and writes its header. A taken name is an ERROR that stops the script — this is deliberate, so two measurements never get glued into one curve. Use exists_chart_file to pick a free name, or add_to_chart_file to extend a recording that already exists — appending needs no create call.
| Option | Meaning |
|---|---|
unit | unit of the measured quantity, e.g. 'V', 'A', 'dB' — drives which vertical scale the curve lands on |
unit_x | unit of the horizontal axis, default 's' |
step | spacing between samples; when > 0 the X coordinate is COMPUTED and not stored, which halves the file |
start | first X coordinate for computed mode, default 0 |
type | 'u8' | 'i8' | 'u16' | 'i16' | 'u32' | 'i32' | 'f32' | 'f64' — how the value is stored, default 'f64' |
factor | value = raw * mnoznik + przesuniecie; lets you store a raw 12-bit ADC reading in u16 and convert on read |
offset | offset added after multiplying, default 0 |
scale_y | 'linear' | 'logarithmic' — presentation only, changes no bytes in the file |
scale_x | same for the horizontal axis; a 20 Hz–20 kHz sweep is unreadable without 'logarithmic' |
color | curve colour as '#rrggbb'; omit to let the chart pick one |
draw | 'line' | 'points' | 'steps' | 'bars' | 'area' — how to DRAW the curve, default 'line'. Presentation only, the stored measurement is identical either way. Use 'points' whenever X is not time and the sweep may go both ways: an I-V curve drawn as a line folds back over itself and cannot be read. Use 'schodki' for a value that switches in steps and holds until the next sample. Changeable later from the chart tab via chart.ustaw_rysunek. |
vector | how many values ONE record carries, default 1. Above 1 the recording becomes a waterfall: every record is a whole spectrum for one moment, written with add_vector_to_chart_file. Chart tab draws it as a 3D surface. |
vector_start | coordinate of the first element of the vector, e.g. 20 (Hz). Only matters when wektor > 1 |
vector_step | spacing between vector elements, e.g. fs/N for FFT bins. Only matters when wektor > 1 |
vector_unit | unit of the vector axis, e.g. 'Hz' |
vector_scale | 'linear' | 'logarithmic' — presentation only. Bins stay linear in the file; a log axis is what keeps 20 Hz-20 kHz readable |
meta | table of free-form text entries drawn as a small box in the corner of the chart — typically who ran the measurement, when, and on what device. Keys are yours to choose: whatever you put in shows up as a row, so no code changes when you add one. Values are converted to text; tables and functions are skipped. Recordings that carry meta each get their own block, headed by the recording name when more than one is on the chart. The whole header must fit in its block, so keep entries short. Use now() for the date — the sandbox removes os, so that is the only clock a script has. One key has a fixed meaning: 'logo' is the NAME OF AN IMAGE FILE in the sandbox, not text — its picture is drawn above that recording's block instead of being written out as a row. Use png, jpg, gif or webp, up to 2 MB, path relative to the sandbox root. A logo the machine does not have is not an error: the chart simply draws without it, because a recording made elsewhere must still open here. |
create_chart_file('prad', {unit='A', step=0.001, type='u16', factor=0.00488})
CRITICAL: 'krok' must match the real sampling interval. With step=0.2 and sleep(500) the time axis shows 20 s where 50 s passed, and nothing detects it — computed X stores no real timestamps. For irregular intervals omit 'krok' and pass x to add_to_chart_file instead.
add_to_chart_file()Appends one measurement, in PHYSICAL units — conversion to the stored type uses the multiplier and offset from the head…
add_to_chart_file(name: string, value: number [, x: number])
Appends one measurement, in PHYSICAL units — conversion to the stored type uses the multiplier and offset from the header. Pass x only when the X axis is explicit (no 'krok' given at creation).
add_to_chart_file('prad', 1.23)
Writing to a recording that does not exist fails — create it first.
exists_chart_file()Whether a recording of that name already exists.
exists_chart_file(name: string) -> boolean
Whether a recording of that name already exists. Needed because a taken name is an error.
local n, nazwa = 1, 'pomiar'
while exists_chart_file(nazwa) do n = n + 1; nazwa = 'pomiar_' .. n end
create_chart_file(nazwa, {unit='V', step=0.5})
add_vector_to_chart_file()Appends a WHOLE VECTOR as one record — one moment of a waterfall.
add_vector_to_chart_file(name: string, values: table [, x: number])
Appends a WHOLE VECTOR as one record — one moment of a waterfall. The table length must match the 'wektor' declared in create_chart_file; a mismatch is an error at write time, not a silent truncation.
add_vector_to_chart_file('csd', {-20, -24, -31, -45})
Separate function instead of overloading add_to_chart_file on purpose: there the second argument is a number, and Lua could not tell 'I passed a table by mistake' from intent.
read_chart_file()Reads records back from a recording.
read_chart_file(name: string [, from: number [, count: number]]) → table, number
Reads records back from a recording. Returns TWO values: the records read, and the total number of records in the file. Omit 'count' to read to the end, omit 'from' to start at zero.
local samples, total = read_chart_file('waveform')
local window = read_chart_file('waveform', 0, 4096)
Reads what is on disk, nothing more. Reading a very long recording builds a Lua table of the same size and the VM has a 16 MB limit, so take it in windows rather than all at once.
read_chart_header()Returns the recording header plus the current record count: name, chart_type, records_count, axis_x, axis_y, axis_vecto…
read_chart_header(name: string) → table
Returns the recording header plus the current record count: name, chart_type, records_count, axis_x, axis_y, axis_vector, data. Field names match the JSON in the file.
show_chart_file()Opens the Charts tab and puts this recording on the plot.
show_chart_file(name: string)
Opens the Charts tab and puts this recording on the plot.
hide_chart_file()Takes the recording off the plot.
hide_chart_file(name: string)
Takes the recording off the plot. Does not touch the file.
Files 8
General file primitives. They know nothing about charts, recordings or any other format: a path relative to the sandbox goes in, a file operation happens. Paths may contain subdirectories; '..' and absolute paths are rejected. Data is passed as a byte string, so the same functions handle text and binary files.
delete_file()Deletes a file inside the sandbox.
delete_file(path: string)
Deletes a file inside the sandbox. A missing file is NOT an error — the point is to reach the state 'this file is gone'.
read_file()Returns the file contents, or nil when the file does not exist.
read_file(path: string) -> string | nil
Returns the file contents, or nil when the file does not exist. A missing file is NOT an error, so asking 'do I have state from the previous run' does not need a guard.
write_file()Creates the file or OVERWRITES it.
write_file(path: string, data: string) -> number
Creates the file or OVERWRITES it. Missing directories along the path are created, so write_file('logs/day1/current.csv', ...) works straight away. Returns the number of bytes written.
append_file()Adds to the END of the file, keeping what is already there.
append_file(path: string, data: string) -> number
Adds to the END of the file, keeping what is already there. Creates the file and missing directories if needed. Returns the number of bytes APPENDED, not the resulting file size.
exists_file()Checks whether a file exists, without reading it.
exists_file(path: string) -> boolean
Checks whether a file exists, without reading it.
list_files()Lists what is in a sandbox directory.
list_files(subdir: string | nil) -> table
Lists what is in a sandbox directory. Returns ENTRIES, not plain names: {name, is_dir, size}, sorted by name. Pass nil for the sandbox root. A missing directory gives an empty table, not an error.
exists_dir()Checks whether a directory exists.
exists_dir(path: string) -> boolean
Checks whether a directory exists.
delete_dir()Deletes a directory.
delete_dir(path: string, with_contents: boolean | nil)
Deletes a directory. By default only an EMPTY one; pass true as the second argument to delete it with everything inside. A missing directory is not an error.
Nothing matches that. Try a shorter word — chart, file, send.