diff --git a/docs/README.md b/docs/README.md index fce7fe38..44a8a8d6 100644 --- a/docs/README.md +++ b/docs/README.md @@ -23,6 +23,7 @@ - [audio.md](audio.md) — `audiodev` interfaces - [bledev.md](bledev.md) — Bluetooth Low Energy: one async API on boards, laptops and browsers - [app-and-board-config.md](app-and-board-config.md) — the application loop +- [lvgl.md](lvgl.md) — LVGL on PyDevices: `display_driver`, the three sister projects, sync and async timers ## Drivers diff --git a/docs/architecture.md b/docs/architecture.md index 0f9712af..afd569f1 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -85,7 +85,7 @@ what you can compile: |---|---|---| | **Raw graphics / canvas** | Direct pixel, line, and shape drawing | [`displaydev`](displaydev.md) with [`pygraphics`](https://github.com/PyDevices/pygraphics) | | **Pure-Python GUI** | Portable buttons, lists, themes, and screen management with nothing to compile | [`pdwidgets`](https://github.com/PyDevices/pdwidgets) | -| **C-native GUI** | Complex vector widgets and a C-accelerated animation engine | [`lvgl`](https://github.com/PyDevices/lvgl-bindings) — see [using LVGL with PyDevices](https://github.com/PyDevices/lvgl-bindings/blob/main/docs/using-lvgl-with-pydevices.md) | +| **C-native GUI** | Complex vector widgets and a C-accelerated animation engine | [`lvgl`](https://github.com/PyDevices/lvgl-bindings) — see [using LVGL with PyDevices](lvgl.md) | All three sit on the same board contract, so the choice does not change how your hardware is configured. diff --git a/docs/lvgl.md b/docs/lvgl.md new file mode 100644 index 00000000..8f133d47 --- /dev/null +++ b/docs/lvgl.md @@ -0,0 +1,178 @@ +# Using LVGL with PyDevices + +How to run [LVGL](https://lvgl.io/) on the PyDevices display, input, and timing +packages, and what [`display_driver.py`](../lib/display_driver.py) does for you. + +## The three sister projects + +The PyDevices LVGL family bundles this integration for each interpreter: + +| Repository | Interpreter | +|---|---| +| [lvgl-micropython](https://github.com/PyDevices/lvgl-micropython) | MicroPython firmware | +| [lvgl-circuitpython](https://github.com/PyDevices/lvgl-circuitpython) | CircuitPython firmware | +| [lvgl-python](https://github.com/PyDevices/lvgl-python) | CPython (`pydevices-lvgl` wheels on TestPyPI) | + +All three consume the bindings generated by +[lvgl-bindings](https://github.com/PyDevices/lvgl-bindings), and all three share `displaydev`, +neutral board capabilities, and `multimer` — so the same LVGL Python code is +portable across them. You can even develop it interactively in +[Jupyter](jupyter.md). + +The rest of this page covers wiring LVGL up by hand, which is what you need with +upstream [lv_micropython](https://github.com/lvgl/lv_micropython) rather than one +of the sister projects. + +## 1. Install the minimum PyDevices packages + +```python +import mip +INDEX = "https://PyDevices.github.io/mip" +mip.install("pydevices", index=INDEX) # displaydev, appdev, display_driver and the rest of lib/ +mip.install("github:PyDevices/pydevices/board_configs/") +``` + +Replace `` with a path from +[board configs](board-configs.md), +or follow +[install workflows](install-workflows.md). + +## 2. Build or obtain LVGL firmware + +Follow upstream [lv_micropython](https://github.com/lvgl/lv_micropython) for your +board. PyDevices supplies the flush and input glue via `board_config.py`; LVGL +supplies the UI toolkit. + +For production ESP32 projects, consider +[kdschlosser's lvgl_micropython](https://github.com/kdschlosser/lvgl_micropython) +C drivers wired through `BusDisplay`. + +## 3. Wire `board_config` to LVGL + +Your `board_config.py` should expose: + +- `display_drv` — a `displaydev` driver with `blit_rect`, dimensions, and rotation +- neutral input callables where present: `host_read`, `touch_read`, + `keypad_read`, `encoder_read`, `encoder_button_read` + +Board configs describe hardware; they do not create an application-level `appdev.App`. +Connect LVGL's display flush callback to copy LVGL's draw buffer through +`display_drv.blit_rect` — or use the packaged `display_driver`, which does it +for you. + +## `display_driver` + +[`display_driver.py`](../lib/display_driver.py) is the LVGL coordinator. It +lives in this repository's `lib/` and ships with `pydevices` (the mip package, +the wheel, and the frozen module in a micropython-pydevices build), beside the +`board_config` idiom, `appdev`, `events`, `keys` and `multimer` it needs. The +sister projects carry only `lvgl` and `fs_driver` +([loading fonts at runtime](https://github.com/PyDevices/lvgl-bindings/blob/main/docs/fonts.md)). + +With `display_driver`, LVGL input is wired automatically through its own +`app` (an `appdev.App`) and virtual touch / encoder / keypad devices. + +> **Do not instantiate or poll `appdev` in an LVGL app.** `lv.task_handler()`, +> driven by `display_driver.event_loop` plus `multimer`, already drains input. +> Window-close (`QUIT`) is handled by the bridge's `HostInput` path. + +Build the UI, then hand control over with `app.run()`. + +### Wheel and swipe input + +A mouse wheel or two-finger trackpad swipe drives the virtual encoder +device, so it adjusts whichever control the LVGL group has focused — +provided that control entered edit mode, which only happens once a click +lands on it (an LVGL core behavior: changing focus always drops edit mode, +so a handler that sets it back on must run on the `FOCUSED` event, not +`PRESSED`, which fires too early). + +By default the encoder reads a single axis and never touches focus, matching +prior versions. Call `display_driver.set_wheel_mapping()` to change that: + +```python +import display_driver + +# Horizontal sliders read best adjusted along their own axis, with the +# perpendicular swipe stepping between them: +display_driver.set_wheel_mapping(adjust_axis="h", adjust_sign=-1, navigate=True) +``` + +- `adjust_axis`: `"v"` (default) or `"h"` — which axis adjusts the focused + control's value. Match it to the control's orientation: horizontal + sliders read best with `"h"`; vertical sliders and knobs, `"v"`. +- `adjust_sign`: `1` (default) or `-1` to flip the adjust direction. +- `navigate`: `False` (default) or `True` to let the other axis move group + focus between controls (`lv.group_t.focus_next`/`focus_prev` on the + default group) — wheel-only browse-and-tweak, with no keyboard or extra + indev required. +- `navigate_sign`: `1` (default) or `-1` to flip which way focus travels. + Separate from `adjust_sign` because the two axes come from different + sources with their own conventions — SDL and Win32 disagree about the + sign of vertical scroll, so "swipe down goes to the next control" is a + claim about that axis on that platform, not about the mapping as a whole. + +A wheel event's legacy integer delta and its float "precise" delta +disagree about which one is real depending on the platform's `usdl2` +build — confirmed empirically to differ between a desktop MicroPython +build and a CPython one on the same machine. `display_driver` resolves +this per event already; if you ever read wheel deltas directly instead of +through `display_driver`, don't assume either field is the trustworthy one +without checking on your own target. + +## Sync versus async timers + +`display_driver` includes the LVGL `event_loop`, which requires `multimer`. +Inspect **`app.timer_async`** — derived from `board_config.timer_async`, or +from the display driver's `requires_async_timer` — to see which backend was +selected: + +| `app.timer_async` | Applies to | +|---|---| +| `False` (the desktop default) | MCUs, MicroPython Unix, CPython Linux — uses `multimer.auto.Timer` | +| `True` | PyScript, Jupyter, or desktop with `PYDEVICES_TIMER_ASYNC=1` — uses `multimer.AsyncTimer` | + +`display_driver` passes it straight through as +`event_loop(asynchronous=app.timer_async)`. When it is true, `display_driver` +drives both ticks and `display.show()` from its asynchronous LVGL refresh loop. + +The desktop `board_config` reads **`PYDEVICES_TIMER_ASYNC`** for the PG/SDL +branch (the default comes from `AutoDisplay` and is normally `False`); PyScript +and Jupyter always use `timer_async=True`. To force async on desktop, export +`PYDEVICES_TIMER_ASYNC=1` in the shell that launches the process, or set it +before `board_config` loads: + +```python +from boarddev import env_set +env_set("PYDEVICES_TIMER_ASYNC", "1") +import display_driver +``` + +`env_set` works on every interpreter; `os.environ` exists only on CPython. + +The frame rate is the display's: `PYDEVICES_REFRESH_MS` changes it for LVGL and +non-LVGL apps alike, and `PYDEVICES_FPS_PRINT` prints it +([displaydev](displaydev.md)). + +## Verifying an integration + +[`lv_test_timer.py`](https://github.com/PyDevices/pydevices-examples/blob/main/lib/examples/lv_test_timer.py) +is a single smoke test that follows `app.timer_async`. Its UI reports the +autodetected interpreter, OS, display driver class, screen, timer backend, and +LVGL version, plus a seconds counter, spinning arc, frame rate, and tap button. +It never sets `PYDEVICES_TIMER_ASYNC` itself — set it in the parent shell if you +want a specific desktop mode. + +`python examples/lv_test_timer.py kit` runs a timed LVGL timer and input check, +prints a `KIT_RESULT=` JSON line, then quits. To drive that across every desktop +interpreter in both modes, use the harnesses documented in +[pydevices-examples/tools/README.md](https://github.com/PyDevices/pydevices-examples/blob/main/tools/README.md). + +## See also + +- [Architecture](architecture.md) +- [displaydev](displaydev.md) +- [multimer](multimer.md) +- [appdev](appdev.md) +- [lvgl-bindings](https://github.com/PyDevices/lvgl-bindings) — the generator, + and [loading fonts at runtime](https://github.com/PyDevices/lvgl-bindings/blob/main/docs/fonts.md) diff --git a/docs/newcomers.md b/docs/newcomers.md index a4ddd910..b4b6e4d9 100644 --- a/docs/newcomers.md +++ b/docs/newcomers.md @@ -50,7 +50,7 @@ Read [the board contract](board-peripherals.md) before adding a board and [app a - Use displaydev and pygraphics for direct drawing. - Use [pdwidgets](https://github.com/PyDevices/pdwidgets) for a pure-Python widget toolkit. -- Use [LVGL](https://github.com/PyDevices/lvgl-bindings) when the firmware or the pydevices-lvgl wheel provides the lvgl module. +- Use [LVGL](lvgl.md) when the firmware or the pydevices-lvgl wheel provides the lvgl module. The hardware configuration remains the same whichever layer you choose. Ready-to-run programs live in [pydevices-examples](https://github.com/PyDevices/pydevices-examples), not this product repository.