LVGL¶
Use pydisplay as the display and input layer for LVGL on MicroPython.
Walkthrough¶
1. Install minimum pydisplay packages¶
Or use installer.py for a one-shot setup.
2. Build or obtain LVGL MicroPython firmware¶
Follow upstream lv_micropython for your board. pydisplay supplies the flush and input glue via board_config.py; LVGL supplies the UI toolkit.
3. Wire board_config to LVGL¶
Your board_config.py should expose:
display— pydisplay driver withblit_rect, dimensions, rotation- Touch broker —
eventsysbroker that enqueues touch/mouse events
Connect LVGL's display flush callback to copy LVGL's draw buffer through display.blit_rect (or the pattern documented in lv_micropython for your port).
With display_driver, LVGL input is wired automatically: each indev read_cb polls the broker's queue device via virtual touch/encoder/keypad devices. Do not call broker.poll() in your LVGL main loop — lv.task_handler() (driven by lv_utils + multimer) already drains input. Calling both competes for the same event queue and breaks clicks. Window-close (QUIT) is handled on the same path inside QueueDevice.poll().
4. Run the touch test example¶
Install examples package, then on device:
import lib.path # development layout only
import lv_touch_test
Requires LVGL-enabled firmware. See src/examples/lv_touch_test.py in the repo.
5. Faster ESP32 buses¶
For production ESP32 projects, consider kdschlosser's lvgl_micropython C drivers wired through BusDisplay.
Wokwi minimum project¶
Try displaysys + eventsys without LVGL first: Wokwi minimum (hosted).
Helper add-ons¶
src/add_ons/lv_utils.py — LVGL event loop helper (requires multimer).
Set TIMER_ASYNC in board_config.py to choose the timer backend:
TIMER_ASYNC |
Use when |
|---|---|
False (default) |
MCU, MicroPython unix, CPython Linux — default multimer.Timer |
True |
PyScript and other asyncio-native apps — multimer.aio.Timer |
display_driver passes this to lv_utils.event_loop(asynchronous=TIMER_ASYNC).
When TIMER_ASYNC = True, display_driver disables SDL's sync auto_refresh timer and calls display.show() from the aio LVGL refresh loop instead. CircuitPython's default multimer.Timer uses a background thread and requires run_queued() — which an asyncio app does not call — so the window would never be presented otherwise.
On CPython Win/mac (TIMER_ASYNC = False), call multimer.run_queued() from your main loop when using threaded timer backends — see multimer. Full apps call display_driver.run() after UI setup: it returns immediately on MicroPython unix and CPython Linux (REPL stays live) and blocks only on Windows SDL or macOS.
Override before import:
import board_config
board_config.TIMER_ASYNC = True
import display_driver
Timer test examples¶
Three scripts share the same UI via lv_test_timer_common.build_ui() and differ only in how multimer drives LVGL ticks:
| Script | When to run |
|---|---|
lv_test_timer_sync.py |
MCU, MP-unix, CPython Linux — no main loop; exits on queued-only platforms |
lv_test_timer_queued.py |
CPython Win/mac — run_queued() drain loop only |
lv_test_timer_async.py |
PyScript / asyncio — TIMER_ASYNC = True, deferred import display_driver, await asyncio.sleep(0) loop |
The shared UI (lv_test_timer_common.py) shows autodetected runtime, OS, display driver class, timer backend, and LVGL version.
Automated harness¶
lv_test_timer_harness.py runs a timed LVGL timer + input check and prints a KIT_RESULT= JSON line on stdout (for CI and tooling). Run from src/:
cd src
micropython examples/lv_test_timer_harness.py queued
.venv/bin/python examples/lv_test_timer_harness.py async
Modes: sync, queued, async.
Desktop test suite¶
tools/run_desktop_lv_tests.py runs the harness across five desktop Python+LVGL executables in sequence (nine subprocess runs total — queued and async per runtime; async is omitted on MicroPython Windows because that port has no asyncio).
| Executable | How resolved |
|---|---|
| MicroPython (Unix) | micropython on PATH |
| CircuitPython | circuitpython on PATH |
| MicroPython (Windows) | micropython.exe on PATH |
| CPython (Windows) | python.exe on PATH |
| CPython (Linux venv) | src/.venv/bin/python |
Each run uses cwd=src/ and opens a window until the harness exits (~4 s). Missing executables are skipped (missing in the summary table).
From the repository root:
python tools/run_desktop_lv_tests.py
./tools/run_desktop_lv_tests.py
From src/:
../tools/run_desktop_lv_tests.py
The script prints a summary table (queued / async columns) and writes full results to .cursor/desktop_lv_test_results.json. Exit code 1 if any run hangs, crashes, fails timers, or fails click checks (strict policy).
For a smaller 3×3 matrix (micropython, circuitpython, cpython-venv × sync/queued/async), use tools/lv_timer_test_kit.py:
python tools/lv_timer_test_kit.py
python tools/lv_timer_test_kit.py --only cpython queued async