From f2276a01dd073f33b7ab57a29072d9ffc60d4c95 Mon Sep 17 00:00:00 2001 From: Brad Barnett <127794626+bdbarnett@users.noreply.github.com> Date: Wed, 7 Oct 2026 16:04:24 -0500 Subject: [PATCH] Using LVGL with PyDevices moves to pydevices' docs/lvgl.md display_driver lives in PyDevices/pydevices' lib/ since #25, so the page that explains it moves there too (PyDevices/pydevices#132). Both README links point at its new home. --- README.md | 7 +- docs/README.md | 7 +- docs/using-lvgl-with-pydevices.md | 173 ------------------------------ 3 files changed, 8 insertions(+), 179 deletions(-) delete mode 100644 docs/using-lvgl-with-pydevices.md diff --git a/README.md b/README.md index d39a536..dd46530 100644 --- a/README.md +++ b/README.md @@ -26,8 +26,9 @@ consumer repo get silently overwritten by the next sync. See ## Documentation -- [Using LVGL with PyDevices](docs/using-lvgl-with-pydevices.md) — how the three - sister projects fit together, and what PyDevices' `display_driver` does. +- [Using LVGL with PyDevices](https://github.com/PyDevices/pydevices/blob/main/docs/lvgl.md) (in PyDevices/pydevices, + beside `display_driver`) — how the three sister projects fit together, and + what `display_driver` does. - [Loading fonts at runtime](docs/fonts.md) — `fonts/*.bin` and `python/fs_driver.py`: any built-in font without a firmware rebuild. - [Generator architecture](docs/generator-architecture.md) — canonical model, @@ -155,7 +156,7 @@ input devices and the `multimer` event loop) is `display_driver.py` in [PyDevices/pydevices `lib/`](https://github.com/PyDevices/pydevices/blob/main/lib/display_driver.py). It ships with `pydevices` because it needs `appdev`, `events`, `keys` and `multimer`; the bindings don't, so you can use them with plumbing of your own. -[Using LVGL with PyDevices](docs/using-lvgl-with-pydevices.md) covers what it does. +[Using LVGL with PyDevices](https://github.com/PyDevices/pydevices/blob/main/docs/lvgl.md) covers what it does. ## Consumers diff --git a/docs/README.md b/docs/README.md index 85cb0cb..f0b423f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,8 +1,9 @@ # lvgl-bindings documentation -- [Using LVGL with PyDevices](using-lvgl-with-pydevices.md) — the sister - projects, wiring `board_config` to LVGL, what PyDevices' `display_driver` does, and the - sync/async timer contract. +- [Using LVGL with PyDevices](https://github.com/PyDevices/pydevices/blob/main/docs/lvgl.md) — the sister + projects, wiring `board_config` to LVGL, what `display_driver` does, and the + sync/async timer contract. It lives in PyDevices/pydevices now, beside + `display_driver`. - [Loading fonts at runtime](fonts.md) — the `fonts/*.bin` collection, `fs_driver.py`, and converting your own fonts; no firmware rebuild needed. - [releasing-bindings.md](releasing-bindings.md) — the binding release chain diff --git a/docs/using-lvgl-with-pydevices.md b/docs/using-lvgl-with-pydevices.md deleted file mode 100644 index 74d5975..0000000 --- a/docs/using-lvgl-with-pydevices.md +++ /dev/null @@ -1,173 +0,0 @@ -# Using LVGL with PyDevices - -How to run [LVGL](https://lvgl.io/) on the PyDevices display, input, and timing -packages, and what PyDevices' -[`display_driver.py`](https://github.com/PyDevices/pydevices/blob/main/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 here, 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](https://github.com/PyDevices/pydevices/blob/main/docs/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](https://github.com/PyDevices/pydevices/blob/main/docs/board-configs.md), -or follow -[install workflows](https://github.com/PyDevices/pydevices/blob/main/docs/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`](https://github.com/PyDevices/pydevices/blob/main/lib/display_driver.py) -is the LVGL coordinator. It lives in PyDevices/pydevices 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 here carry only -`lvgl` and `fs_driver`. - -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, set it -before `board_config` loads: - -```python -import os -os.environ["PYDEVICES_TIMER_ASYNC"] = "1" -import display_driver -``` - -Or export `PYDEVICES_TIMER_ASYNC=1` in the shell that launches the process. - -## 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` via -`app.run()`. Its UI reports the autodetected interpreter, OS, display -driver class, timer backend, mode (`sync` / `async`), and LVGL version, plus a -seconds counter, spinning arc, and tap button. It deliberately does **not** read -or write environment variables — set `PYDEVICES_TIMER_ASYNC` 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](https://github.com/PyDevices/pydevices/blob/main/docs/architecture.md) -- [displaydev](https://github.com/PyDevices/pydevices/blob/main/docs/displaydev.md) -- [multimer](https://github.com/PyDevices/pydevices/blob/main/docs/multimer.md) -- [appdev](https://github.com/PyDevices/pydevices/blob/main/docs/appdev.md)