Skip to content

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 with blit_rect, dimensions, rotation
  • Touch broker — eventsys broker 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 looplv.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

Next