Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
89 commits
Select commit Hold shift + click to select a range
2aa0384
add .gitignore
jurgelenas Feb 11, 2026
299d1e3
Move blackwhite lua script to blackwhite dir
jurgelenas Feb 11, 2026
324d39b
Add lvgl lua, new lvgl based widgets
jurgelenas Feb 11, 2026
40aa9a5
de-box
jurgelenas Feb 11, 2026
66cc28e
Add readme
jurgelenas Feb 11, 2026
df2f12f
ui tweaks
jurgelenas Feb 11, 2026
0fb9e99
snooze warning for 60s
jurgelenas Feb 11, 2026
d254371
Rename ELRSlib to ELRS
jurgelenas Feb 11, 2026
c4e3989
Fix lvgl warning
jurgelenas Feb 11, 2026
ae9ec44
improve loading text
jurgelenas Feb 11, 2026
ffe847d
Add new BW Lua script
jurgelenas Feb 11, 2026
6903171
remove old one
jurgelenas Feb 11, 2026
815806c
implement slow loading simulator
jurgelenas Feb 11, 2026
c3dfd38
update crsf sim
jurgelenas Feb 11, 2026
d4b1684
Merge branch 'lvgl-lua' into blackwhite2
jurgelenas Feb 11, 2026
f939f9b
simplify full screen mode detection
jurgelenas Feb 14, 2026
8b57959
simulate CE long strings in TX Power folder
jurgelenas Feb 14, 2026
ef20f80
Migrate to 2 columns folder layout for all radios, except for 3 colum…
jurgelenas Feb 14, 2026
221aaba
borderPad layout improvements
jurgelenas Feb 14, 2026
213bfe7
fix el18 portrait layout
jurgelenas Feb 14, 2026
757c451
el18 improvements
jurgelenas Feb 14, 2026
930414c
improve tx16s
jurgelenas Feb 14, 2026
870769c
improve code style
jurgelenas Feb 14, 2026
eeec1e0
unify size naming convention
jurgelenas Feb 14, 2026
963b31c
Merge branch 'lvgl-lua' into blackwhite2
jurgelenas Feb 14, 2026
c12df12
refactor elrsv1 detected message
jurgelenas Feb 15, 2026
2fa0161
fix device info packets handling
jurgelenas Feb 15, 2026
5381af5
Merge branch 'lvgl-lua' into blackwhite2
jurgelenas Feb 15, 2026
3385bf3
fix device info handling
jurgelenas Feb 15, 2026
655617b
add generic alerts method
jurgelenas Feb 15, 2026
305ad21
remove debounced save
jurgelenas Feb 15, 2026
ffa77a9
Merge branch 'lvgl-lua' into blackwhite2
jurgelenas Feb 15, 2026
4a55bd2
remove debounced save
jurgelenas Feb 15, 2026
b2c83e4
Send initial DEVICE_PING after 1 second, then every 5 second while to…
jurgelenas Feb 15, 2026
1e849f8
Merge branch 'lvgl-lua' into blackwhite2
jurgelenas Feb 15, 2026
c959997
Send initial DEVICE_PING after 1 second, then every 5 second while to…
jurgelenas Feb 15, 2026
a961304
no periodic ping
jurgelenas Feb 17, 2026
6a4c1f8
Merge branch 'lvgl-lua' into blackwhite2
jurgelenas Feb 17, 2026
7c9f103
no periodic ping
jurgelenas Feb 17, 2026
6389699
migrate to unified lua for BW & Color lcd radios
jurgelenas Feb 17, 2026
e728662
remove per-byte lookup table allocation in string/options parser
jurgelenas Feb 17, 2026
9adb687
use table.concat instead of ..
jurgelenas Feb 17, 2026
08e1e4e
remove .. in favor of shim.tableConcat
jurgelenas Feb 17, 2026
446bc17
do not pass shim to ui
jurgelenas Feb 17, 2026
8274ca0
use sparse keys in protocol field allocation
jurgelenas Feb 17, 2026
e16f16d
Update readme for unified lua
jurgelenas Feb 17, 2026
ced30db
Add screenshots
jurgelenas Feb 17, 2026
c8ab817
resize screenshots
jurgelenas Feb 17, 2026
b969e1c
Revert "resize screenshots"
jurgelenas Feb 17, 2026
f0879ae
update image widths
jurgelenas Feb 17, 2026
93857d2
Migrate to lvgl constants, appyl diff from philmoz
jurgelenas Feb 18, 2026
f109fd0
fix code style issue
jurgelenas Feb 18, 2026
b3b9719
drop opentx support
jurgelenas Feb 18, 2026
34ad8fd
update lint
jurgelenas Feb 18, 2026
a46d011
Add BW screenshot
jurgelenas Feb 18, 2026
1b59070
Add lua-language-server, stylua, add edgetx lua api type definitions
jurgelenas Feb 18, 2026
3ca475c
stylua setup
jurgelenas Feb 18, 2026
21f8639
fix styling when battery voltage is present
jurgelenas Feb 24, 2026
906df0a
add shim
jurgelenas Feb 24, 2026
8ae2b53
armed flag text parity with the firmware
jurgelenas Feb 24, 2026
8b0eb7d
handle warnings better
jurgelenas Feb 24, 2026
a028b19
fix telemetry widget for elrs v4
jurgelenas Feb 24, 2026
1e74e66
Add separate alert for model mismatch
jurgelenas Feb 25, 2026
10d5bfb
improve copy on model mismatch
jurgelenas Feb 25, 2026
770c5f9
Add strings & float support lvgl
jurgelenas Feb 25, 2026
f39b601
Merge branch 'unified-lua-lsp-strings' into unified-lua-lsp
jurgelenas Feb 27, 2026
5cab7de
after number edit reload all releated fields
jurgelenas Feb 27, 2026
ba0b8ff
remove 2.11 support
jurgelenas Mar 4, 2026
4328aba
assign title to lvgl.choice
jurgelenas Mar 4, 2026
5569ebf
move device name to the bottom
jurgelenas Mar 4, 2026
9998b9d
rename "No link" to "No telemetry"
jurgelenas Mar 4, 2026
b763956
improve layout
jurgelenas Mar 4, 2026
6a7233d
add github workflow
jurgelenas Mar 4, 2026
bad38dd
fix workflow
jurgelenas Mar 4, 2026
2f6a55c
update github action runtime
jurgelenas Mar 4, 2026
683c6ef
add sync command
jurgelenas Mar 5, 2026
5dec8f9
update make file
jurgelenas Mar 5, 2026
3ca29df
Fix LVGL focus loss on field change by avoiding full UI rebuild
jurgelenas Mar 6, 2026
7acf530
count reloading fields as missing progress
jurgelenas Mar 6, 2026
a38d8b1
migrate to edgetx-cli pkg* commands
jurgelenas Mar 6, 2026
ebf6fc9
add license
jurgelenas Mar 6, 2026
e892fb3
add min edgetx version
jurgelenas Mar 6, 2026
35568a0
migrate from toml to yml for consistency with edgetx
jurgelenas Mar 6, 2026
17e45e3
Merge branch 'unified-lua-lsp-focus2' into unified-lua-lsp
jurgelenas Mar 6, 2026
1366aea
fix code style
jurgelenas Mar 6, 2026
fb8c77a
update readme
jurgelenas Mar 6, 2026
6e51c93
Move CRSFSimulator to src/
jurgelenas Mar 6, 2026
747c55a
mark CRSFSimulator as dev depency
jurgelenas Mar 6, 2026
9103748
remove test dir from lua checklist
jurgelenas Mar 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: CI

on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
workflow_dispatch:

jobs:
check:
name: Lint and Typecheck
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6

- name: Cache cargo
uses: actions/cache@v5
with:
path: ~/.cargo
key: cargo-stylua-${{ runner.os }}

- name: Install tools
run: make install-tools

- name: Run checks
run: make check
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
*.luac
color/WIDGETS/ELRSVTXAdmin/presets.txt
bin/
.claude/plans/
16 changes: 16 additions & 0 deletions .luarc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"runtime.version": "Lua 5.3",
"runtime.builtin": {
"os": "disable",
"debug": "disable",
"package": "disable",
"coroutine": "disable",
"utf8": "disable",
"io": "disable",
"bit32": "disable"
},
"workspace.library": ["edgetx-lua-stdlib"],
"workspace.ignoreDir": ["bin", "edgetx-lua-stdlib"],
"workspace.checkThirdParty": false,
"diagnostics.disable": ["lowercase-global"]
}
7 changes: 7 additions & 0 deletions .stylua.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
column_width = 120
line_endings = "Unix"
indent_type = "Spaces"
indent_width = 2
quote_style = "AutoPreferDouble"
call_parentheses = "Always"
collapse_simple_statement = "Never"
48 changes: 48 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
SRC_DIR := src

LUALS_VERSION := 3.17.1
LUALS_DIR := bin/lua-language-server
LUALS := $(LUALS_DIR)/bin/lua-language-server
LUALS_URL := https://github.com/LuaLS/lua-language-server/releases/download/$(LUALS_VERSION)/lua-language-server-$(LUALS_VERSION)-linux-x64.tar.gz

.PHONY: help install-tools install-stylua install-luals format format-check typecheck check sync push

help:
@echo "Usage: make <target>"
@echo ""
@echo " install-tools Install stylua and lua-language-server"
@echo " install-stylua Install stylua (via cargo)"
@echo " install-luals Install lua-language-server"
@echo " format Format Lua files with stylua"
@echo " format-check Check formatting without modifying files"
@echo " typecheck Run lua-language-server type checking"
@echo " check Run format-check and typecheck"
@echo " sync Sync source files to EdgeTX simulator SD card"
@echo " push Install package to EdgeTX radio and eject"

install-stylua:
@command -v cargo >/dev/null 2>&1 || { echo "cargo is required (install Rust: https://rustup.rs)"; exit 1; }
cargo install stylua --features lua53

install-luals:
mkdir -p $(LUALS_DIR)
curl -fSL $(LUALS_URL) | tar xz -C $(LUALS_DIR)

install-tools: install-stylua install-luals

format:
stylua $(SRC_DIR)

format-check:
stylua --check $(SRC_DIR)

typecheck:
$(LUALS) --check .

check: format-check typecheck

sync:
edgetx-cli dev sync ../edgetx-sdcard

push:
edgetx-cli pkg install . --eject
130 changes: 122 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,127 @@
# ExpressLRS Lua Scripts

`elrs.lua` works with all versions of ExpressLRS from v2.0 to current. There is no requirement to use an e.g. elrsV3.lua for 3.x, just use elrs.lua.
Lua configuration tool for ExpressLRS on EdgeTX radios. Works on both black & white LCD and color LCD radios.

## Old ELRS.lua
The package also includes two color-LCD widgets: the **ELRS Telemetry Widget** and the **VTX Administrator Widget**.

When copying the lua to you handset, delete any old versions such as ELRS.lua, elrsV2.lua, or elrsV3.lua. The version-labeled filenames have been obsoleted.
## Features

### Downloading from Github:
Click the file link above, find the "Raw" button near the top of that page. Right-click, Save link as.., copy the .lua file into the /SCRIPTS/TOOLS directory of the SD card on your handset.
- Configure packet rate, telemetry ratio, switch mode, model match, antenna mode, TX power, WiFi connectivity, and more
- Compatible with **ExpressLRS v3.0+**

### Downloading from Configurator:
Use the button shown in the image below to download the .lua script into the /SCRIPTS/TOOLS directory of the SD card on your handset.
![downloadlua](https://user-images.githubusercontent.com/68074253/129203116-c1234719-3e8c-4cbf-a391-b7fb8dc0262d.png)
## Installation

Copy the contents of the `src/` directory to the **root** of your radio's SD card, preserving the directory structure. Delete any old ELRS scripts (`ELRS.lua`, `elrsV2.lua`, `elrsV3.lua`, `expresslrs.lua` and their `.luac` counterparts) from `SCRIPTS/TOOLS/`.

When done, your SD card should contain:

```
SCRIPTS/
ELRS/
crsf.lua -- shared CRSF protocol library
TOOLS/
ExpressLRS/
main.lua -- entry point
protocol.lua -- CRSF protocol handling
navigation.lua -- folder navigation
shim.lua -- BW compatibility shim
ui/
lvgl.lua -- color LCD UI (LVGL)
lcd.lua -- black & white LCD UI
WIDGETS/
ELRSTelemetry/
main.lua
loadable.lua
ui/
...
ELRSVTXAdmin/
main.lua
loadable.lua
presets.txt
ui/
...
```

The shared library `SCRIPTS/ELRS/crsf.lua` is required by both widgets.

### Install with edgetx-cli

You can also install this package using [edgetx-cli](https://github.com/jurgelenas/edgetx-cli):

```sh
edgetx-cli pkg install ExpressLRS/Lua-Scripts@unified-lua-lsp
```

Use the `--eject` flag to automatically unmount the SD card after installation.

## ExpressLRS Configuration Tool

The main tool (`SCRIPTS/TOOLS/ExpressLRS/`) lets you configure your ExpressLRS transmitter and receiver settings directly from your radio.

<img src="screenshots/tool_main_bw.png" width="256" alt="ExpressLRS Configuration Tool"><br/>

<img src="screenshots/tool_main.png" width="472" alt="ExpressLRS Configuration Tool">

### Architecture

| Module | Purpose |
|--------|---------|
| `main.lua` | Entry point and run-loop orchestrator |
| `protocol.lua` | CRSF frame parsing, device discovery, parameter read/write |
| `navigation.lua` | Folder and device navigation stack |
| `shim.lua` | Polyfills for BW radios missing standard Lua functions |
| `ui/lvgl.lua` | Color LCD interface (LVGL dialogs, command pages, warnings) |
| `ui/lcd.lua` | BW LCD interface (text cursor, popups) |

## Widgets

Both widgets running side-by-side on the home screen:

<img src="screenshots/widgets.png" width="472" alt="ELRS Widgets">

## ELRS Telemetry Widget

The telemetry widget (`WIDGETS/ELRSTelemetry/`) displays real-time link statistics on your home screen: link quality, RSSI, range, RF mode, TX power, battery voltage, current, GPS, and flight mode. It supports multiple screen resolutions (800x480, 480x320, 480x272, 320x480, 320x240).

<img src="screenshots/widget_telemetry_fullscren.png" width="472" alt="ELRS Telemetry Widget">

## VTX Administrator Widget

The VTX Administrator widget (`WIDGETS/ELRSVTXAdmin/`) provides control over your video transmitter settings -- band, channel, power level, and pit mode -- directly from your radio telemetry screen. It also supports 6POS quick change for rapid VTX channel switching via a 6POS switch.

<img src="screenshots/widget_vtxadmin_fullscreen.png" width="472" alt="VTX Administrator Widget">

## CRSF Simulator (Testing)

The `test/` directory contains a CRSF protocol simulator for development and testing without real hardware.

**File:** `test/SCRIPTS/CRSFSimulator/csrfsimulator.lua`

The simulator provides a packet-level mock of `crossfireTelemetryPop` and `crossfireTelemetryPush`, allowing the ELRS tool to exercise the full communication flow (device discovery, parameter loading, value writes, ELRS status) inside the EdgeTX simulator. Multiple scenarios are available to simulate different states such as normal operation, disconnected links, model mismatch, and more.

### How it works

When the tool detects it is running in the EdgeTX simulator (version string ends with `-simu`), `main.lua` automatically loads the simulator module from `/SCRIPTS/CRSFSimulator/csrfsimulator.lua` and patches the protocol's `pop`, `push`, and `hasCrsfModule` functions with the mock implementations.

To use the simulator, copy the `test/` directory contents onto the SD card alongside `src/` so that `SCRIPTS/CRSFSimulator/csrfsimulator.lua` is present. The simulator is ignored on real hardware.

### Scenarios

The simulator supports multiple test scenarios, configurable via the `config.scenario` variable at the top of the file:

| Scenario | Description |
|----------|-------------|
| `normal` | TX + RX connected. Happy path with full telemetry and all parameters. |
| `no_telemetry` | TX present but no RX telemetry. Shows "No telemetry" state. |
| `reconnect` | Starts disconnected, transitions to connected after ~5 seconds. |
| `model_mismatch` | TX + RX connected with Model ID mismatch flag. Triggers warning dialog. |
| `armed` | TX + RX connected with "is Armed" warning flag. |
| `slow_loading` | Parameter reads delayed by ~2 seconds each. Tests loading UI states. |
| `no_module` | No CRSF module found. Triggers "No Module Found" error dialog. |

## Compatibility

| Radio type | Firmware | ExpressLRS |
|------------|----------|------------|
| Black & white LCD | EdgeTX 2.12-rc4+ or 3.0+ | v3.0+ |
| Color LCD | EdgeTX 2.12-rc4+ or 3.0+ | v3.0+ |
56 changes: 56 additions & 0 deletions edgetx-lua-stdlib/audio.d.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
---@meta

-- EdgeTX audio and haptic playback functions
-- Source: edgetx/radio/src/lua/api_general.cpp

--- Play an audio file from the SD card.
--- @since 2.0.0
---@param path string Audio file path (e.g. "/SOUNDS/en/hello.wav")
---@param volume? integer Volume override (1-5, omit to use system setting)
function playFile(path, volume) end

--- Play a number with optional unit announcement.
--- @since 2.0.0
---@param value integer Number to play
---@param unit integer Unit constant (UNIT_*)
---@param attributes? integer
---@param volume? integer Volume override (1-5, omit to use system setting)
function playNumber(value, unit, attributes, volume) end

--- Play a duration value (hours, minutes, seconds).
--- @since 2.0.0
---@param duration integer Seconds
---@param playTime? integer Non-zero to announce as time of day
---@param volume? integer Volume override (1-5, omit to use system setting)
function playDuration(duration, playTime, volume) end

--- Play a tone with configurable frequency, duration, and pattern.
--- @since 2.1.0
---@param frequency integer Hz
---@param duration integer ms
---@param pause integer ms of silence after tone
---@param flags? integer 0 or PLAY_NOW
---@param freqIncr? integer Frequency increment per repeat
---@param volume? integer Volume override (1-5, omit to use system setting)
function playTone(frequency, duration, pause, flags, freqIncr, volume) end

--- Generate haptic feedback.
--- @since 2.2.0
---@param duration integer ms
---@param pause? integer ms of silence after haptic
---@param flags? integer 0 or PLAY_NOW
function playHaptic(duration, pause, flags) end

--- Flush the audio queue, stopping all current and pending audio playback.
function flushAudio() end

-- ==========================================================================
-- Audio constants
-- ==========================================================================

---@type integer
PLAY_NOW = 0
---@type integer
PLAY_BACKGROUND = 0
---@type integer
TIMEHOUR = 0
67 changes: 67 additions & 0 deletions edgetx-lua-stdlib/bit32.d.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
---@meta

-- Lua 5.2 bit32 library (available in EdgeTX runtime)
-- Custom definition to avoid deprecation warnings from lua-language-server.
-- The built-in bit32 is disabled in .luarc.json because LuaLS marks it
-- deprecated under Lua 5.3, but EdgeTX's runtime still requires it.

---@class bit32lib
bit32 = {}

---@param ... integer
---@return integer
function bit32.band(...) end

---@param ... integer
---@return integer
function bit32.bor(...) end

---@param ... integer
---@return integer
function bit32.bxor(...) end

---@param x integer
---@return integer
function bit32.bnot(x) end

---@param ... integer
---@return boolean
function bit32.btest(...) end

---@param x integer
---@param disp integer
---@return integer
function bit32.lshift(x, disp) end

---@param x integer
---@param disp integer
---@return integer
function bit32.rshift(x, disp) end

---@param x integer
---@param disp integer
---@return integer
function bit32.arshift(x, disp) end

---@param x integer
---@param disp integer
---@return integer
function bit32.lrotate(x, disp) end

---@param x integer
---@param disp integer
---@return integer
function bit32.rrotate(x, disp) end

---@param n integer
---@param field integer
---@param width? integer
---@return integer
function bit32.extract(n, field, width) end

---@param n integer
---@param v integer
---@param field integer
---@param width? integer
---@return integer
function bit32.replace(n, v, field, width) end
Loading