Skip to content

Latest commit

 

History

History
356 lines (265 loc) · 15.1 KB

File metadata and controls

356 lines (265 loc) · 15.1 KB

Lua Test Scripts

Test scripts are written in Lua 5.4 and automate simulator interaction for testing. Pass a script file with --script:

edgetx-cli dev simulator --radio "Radiomaster TX16S" --headless --script test.lua --timeout 30s --screenshot result.png

Key commands

Function Description
key.press(key) Tap: key down, 100ms pause, key up
key.longpress(key) Long press: key down, 1s pause, key up
key.down(key) Hold a key down
key.up(key) Release a key

Key arguments accept a KEY constant or a string: key.press(KEY.ENTER) or key.press("ENTER").

KEY constants: KEY.MENU, KEY.EXIT, KEY.ENTER, KEY.PAGEUP, KEY.PAGEDN, KEY.UP, KEY.DOWN, KEY.LEFT, KEY.RIGHT, KEY.PLUS, KEY.MINUS, KEY.MODEL, KEY.TELE, KEY.SYS

Touch commands

Function Description
touch.tap(x, y) Tap: touch down, 100ms pause, touch up
touch.longpress(x, y) Long press: down, 1s pause, up
touch.down(x, y) Hold touch at coordinates
touch.release() Release touch

Trim commands

Function Description
trim.press(name) Tap: down, 100ms pause, up
trim.longpress(name) Long press: down, 1s pause, up
trim.down(name) Hold trim button
trim.up(name) Release trim button
trim.get(name) Get current trim value
trim.set(name, value) Set trim to an exact value
trim.range() Get trim min/max limits (returns two values)

Trim arguments accept a TRIM constant, string name, or raw index: trim.press(TRIM.T1) or trim.press("T1").

Hardware inputs

Function Description
switch(name, state) Set switch position (-1, 0, 1)
analog(name, value) Set analog input (0-4096)
rotary(delta) Rotary encoder delta

Switch and analog accept a SWITCH/INPUT constant, string name, or raw index: switch(SWITCH.SA, -1) or switch("SA", -1).

SWITCH / INPUT / TRIM constants are radio-specific and auto-populated from the radio definition (e.g., SWITCH.SA, SWITCH.SB, INPUT.LH, INPUT.P1, TRIM.T1, TRIM.T4).

Monitor commands

Channel outputs

Function Description
channel.get(index) Get channel output value (1-based, -1024 to 1024)
channel.mixer(index) Get mixer output value (1-based)
channel.count() Number of output channels
channel.used(index) Whether channel is in use (boolean, 1-based)
channel.mix_count() Number of active mixes in the model

Logical switches

Function Description
logicalswitch.get(index) Get logical switch state (true/false, 1-based)
logicalswitch.count() Number of logical switches

Global variables

Function Description
gvar.get(gvar, flightmode) Get GVar value (both 1-based)
gvar.count() Number of global variables
gvar.flightmodes() Number of flight modes
gvar.flightmode() Current active flight mode (0-based)

Example:

-- Read all active logical switches
for i = 1, logicalswitch.count() do
    if logicalswitch.get(i) then
        print("L" .. i .. " is ON")
    end
end

-- Read channel outputs
for i = 1, channel.count() do
    if channel.used(i) then
        print("CH" .. i .. " = " .. channel.get(i))
    end
end

Screen inspection

Color-LCD radios expose their live LVGL widget tree. It is the basis for structural UI checks (which texts are on screen, what has focus, where a button is) without pixel matching. B&W radios draw straight into the framebuffer and have no tree; screen.* raises an error there.

Function Description
screen.get([opts]) UI tree as a nested Lua table
screen.json([opts]) UI tree as a JSON string
screen.dump(path[, opts]) Write the UI tree JSON to a file
wait_ready([timeout]) Block until the firmware has booted, its Lua interpreter is running and the UI loop is cycling (default timeout 10 s)

opts is an optional table: hidden = false omits hidden subtrees (by default they are included and marked hidden); styles = true adds a style table per node (colors, font height, padding).

The firmware takes the snapshot on its GUI task between two LVGL cycles, so the tree is always self-consistent. A call takes up to one GUI cycle (about 50 ms).

Tree structure

The root is a pseudo node of type root. Its meta table carries lvgl (version), tick (firmware milliseconds), flags, focus (id of the focused widget, when any) and editing (true while the focused widget is in edit mode). The root's children are the LVGL layers, tagged layer = "screen" (the current page), "top" (keyboards, popups, dialogs) and "sys".

Every node has:

Field Meaning
id Widget identity, stable while the widget exists
type obj, window, label, btn, textarea, keyboard, btnmatrix, checkbox, switch, slider, bar, arc, table, img, canvas, line, tileview, tile
x, y, w, h Absolute screen rectangle in pixels
children Child nodes in draw order (absent on leaves)

Boolean attributes are present only when true: visible (not hidden and inside the visible area after clipping), hidden, clickable, scrollable, checkable, floating, focused, focus_key, edited, checked, pressed, disabled, scrolled. Scrolled containers carry scroll = {x, y}.

Type-specific fields:

Type Fields
label, checkbox text
textarea text, cursor, password
btnmatrix, keyboard buttons (list; "\n" marks a row break), selected, selected_text; keyboards add mode and textarea (id)
slider, bar, arc value, min, max
table rows, cols, cells (list of rows of strings), row/col (selected cell)
img src (file path or symbol; absent for in-memory bitmaps)
tileview tile (id of the active tile)

EdgeTX's own widgets are built from these primitives: a Choice is an obj holding an img and a label, a TextButton is a btn holding a label, pages and dialogs are window nodes.

Example:

wait_ready(10)

local function find(node, pred, out)
    out = out or {}
    if pred(node) then out[#out + 1] = node end
    for _, child in ipairs(node.children or {}) do find(child, pred, out) end
    return out
end

local tree = screen.get()
for _, l in ipairs(find(tree, function(n) return n.type == "label" and n.visible end)) do
    print(l.text, l.x, l.y)
end

-- tap the first visible button labelled "Save"
local save = find(tree, function(n)
    return n.type == "btn" and n.visible
        and #find(n, function(c) return c.text == "Save" end) > 0
end)[1]
if save then touch.tap(save.x + save.w // 2, save.y + save.h // 2) end

screen.dump("ui.json")  -- keep a copy for offline inspection

Utilities

Function Description
wait(seconds) Wait for a duration (float, in seconds)
wait_ready([timeout]) Wait until the firmware has booted and its UI is responsive
screenshot(path) Save LCD framebuffer as PNG
reset() Full simulator restart — reloads all scripts, widgets, and resets screen
reload() Reload Lua scripts from SD card (mix, function, telemetry — not widgets)
exit(code) Exit with a process exit code
print(...) Debug logging (Lua standard library)

Exit codes

Scripts return exit code 0 by default. Use exit(code) to terminate early with a specific exit code. This is useful for CI pipelines where you need to signal pass/fail.

Error handling

Scripts halt immediately on any error. Error messages include file name, line number, and a description of the problem (e.g., unknown key "BOGUS" (available: MENU, EXIT, ...)). Script errors produce a non-zero exit code.

Example script

test.lua:

-- Wait for boot
wait(5)

-- Navigate to the tools menu
key.press(KEY.SYS)
wait(1)
key.press(KEY.PAGEDN)
wait(0.5)

-- Take a screenshot
screenshot("tools-menu.png")

CI example

edgetx-cli dev simulator \
  --radio "Radiomaster TX16S" \
  --headless \
  --script test.lua \
  --timeout 30s \
  --screenshot final.png

Stdin streaming

Use --script - or --script-stdin to read Lua commands from stdin. This enables AI-driven testing and interactive piped scripting. Multi-line constructs (e.g., for/end blocks) are automatically detected and buffered until complete.

# Pipe commands
echo 'print("hello")' | edgetx-cli dev simulator \
  --radio "Radiomaster TX16S" --headless --script - --timeout 10s

# Multi-line via stdin
printf 'for i=1,3 do\nprint(i)\nend\n' | edgetx-cli dev simulator \
  --radio "Radiomaster TX16S" --headless --script-stdin --timeout 10s

# Exit with a specific code
echo 'exit(42)' | edgetx-cli dev simulator \
  --radio "Radiomaster TX16S" --headless --script - --timeout 10s
echo $?  # prints 42

Interactive Lua scripting via stdin

When an AI agent (e.g. Claude Code) needs to interactively control the simulator — sending commands one at a time, observing the screen, and deciding what to do next — a simple pipe won't work because each shell invocation is a separate process. Instead, use tail -f on a regular file to keep the simulator's stdin open across multiple shell calls.

Setup — launch the simulator with tail -f

# Create a fresh (empty) command file and log file.
# Truncate with `: >`, do NOT `touch`: touch leaves any old contents in place,
# and those stale commands would be replayed into the freshly-booted simulator.
: > /tmp/sim-cmds
: > /tmp/sim-log

# Launch the simulator in the background, feeding commands via tail -f.
# `-n 0` makes tail follow from the end, so only lines appended *after* launch
# are sent to stdin — pre-existing content is never replayed.
tail -n 0 -f /tmp/sim-cmds | edgetx-cli dev simulator \
  --radio "Radiomaster TX16S" \
  --headless \
  --script-stdin \
  --timeout 120s \
  > /tmp/sim-log 2>&1 &
SIM_PID=$!

The key insight: each echo 'cmd' >> /tmp/sim-cmds is a separate shell invocation that appends to a regular file. tail -f watches that file for new content and continuously feeds new lines into the simulator's stdin — bridging separate shell calls into one persistent stream.

Boot wait — the simulator needs a few seconds to fully start

# Always wait for the simulator to boot before sending navigation commands
echo 'wait_ready(10)' >> /tmp/sim-cmds

wait_ready returns once the firmware has booted, its Lua interpreter is running and the UI loop is cycling. Firmware builds without that capability report an error; fall back to wait(3) there.

Observe-act loop — the core interaction pattern

  1. Send Lua commands by appending to the command file (see scripting API above)
  2. Take a screenshot to observe the result
  3. Read the screenshot PNG to see what happened
  4. Decide the next action and repeat
# Send a command
echo 'key.press(KEY.SYS)' >> /tmp/sim-cmds

# Wait for the UI to update, then capture a screenshot
echo 'wait(1)' >> /tmp/sim-cmds
echo 'screenshot("/tmp/sim-screen.png")' >> /tmp/sim-cmds

# Give the screenshot time to be written, then read the PNG to observe the result
sleep 1
# (AI agent reads /tmp/sim-screen.png to see the current screen)

# Send the next command based on what was observed
echo 'touch.tap(240, 20)' >> /tmp/sim-cmds

print() output goes to stdout, which is captured in the log file (/tmp/sim-log). Read the log file to see Lua print output.

For multi-line Lua blocks (e.g. for/end), use a heredoc so all lines arrive together for the block detector:

cat >> /tmp/sim-cmds << 'CMDS'
for i = 1, 5 do
    rotary(1)
    wait(0.3)
end
CMDS

Exit code — signal pass/fail to the calling process

# Tell the simulator to exit with a specific code
echo 'exit(0)' >> /tmp/sim-cmds

# Wait for the process to finish and capture its exit code
wait $SIM_PID
echo $?  # prints 0

Cleanup — always clean up when done

# Kill the simulator if still running
kill $SIM_PID 2>/dev/null
wait $SIM_PID 2>/dev/null

# Remove temp files
rm -f /tmp/sim-cmds /tmp/sim-log /tmp/sim-screen.png

Advanced example (loops, functions)

-- Helper to navigate down N times
function nav_down(n)
    for i = 1, n do
        key.press(KEY.DOWN)
        wait(0.2)
    end
end

wait(3)
key.press(KEY.SYS)
wait(1)
nav_down(5)
screenshot("result.png")

Complete API showcase

A single script demonstrating every available Lua function and constant table: showcase.lua.