Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .github/workflows/linux-integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ jobs:
uses: actions/checkout@v7
with:
repository: PyDevices/lvgl-python
ref: 7883313f802d891232fe6af0c8cd1e362d4662f6
ref: 4bc903ce98b5c2862e56e6bf2c4a38b85b09d58e
path: lvgl-python
submodules: recursive

Expand Down
27 changes: 12 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ 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 `python/display_driver.py` does.
sister projects fit together, and what PyDevices' `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,
Expand All @@ -44,7 +44,7 @@ lvgl-bindings/
lvgl/ # LVGL submodule (git submodule update --init)
lv_conf.h # Shared LVGL config for all targets
generated/ # Generated bindings, API model, and shared stub (committed)
python/ # Hand-written helpers (display_driver.py — committed)
python/ # Hand-written helpers (fs_driver.py — committed)
packages/ # Optional MIP manifests
regenerate_all.sh # Primary entry point for regenerating all binding artifacts
docs/ # Architecture, migration, release, and fonts documentation
Expand Down Expand Up @@ -78,7 +78,7 @@ static-checking tools used by the validation commands below.

## Typical workflow

The practical flow is: make a small change in **`binding/`** or the LVGL submodule, regenerate the binding target you need, sync the generated files into the consumer repo, and then rebuild that repo’s firmware or extension. If you only touched the Python-side glue, start with **`python/display_driver.py`** and the consumer sync script; if you changed the C API surface or LVGL headers, regenerate the relevant target first and rebuild the consumer before trusting the result.
The practical flow is: make a small change in **`binding/`** or the LVGL submodule, regenerate the binding target you need, sync the generated files into the consumer repo, and then rebuild that repo’s firmware or extension. If you only touched the Python-side glue, start with **`python/fs_driver.py`** and the consumer sync script (LVGL's PyDevices coordinator, `display_driver.py`, is in PyDevices/pydevices and ships without a bindings release); if you changed the C API surface or LVGL headers, regenerate the relevant target first and rebuild the consumer before trusting the result.

## Generate bindings

Expand Down Expand Up @@ -148,24 +148,21 @@ After regen, rebuild the consumer repo(s) (`lvgl-micropython`,

Release workflow and tagging: [releasing-bindings.md](docs/releasing-bindings.md).

## `display_driver.py` & Timer Integration
## `display_driver`

`display_driver.py` is the canonical PyDevices LVGL coordinator:
- It connects the LVGL event loop to `displaydev` and `multimer` without requiring `appdev`.
- **Automatic Timer Startup**: Simply importing `display_driver` initializes the display, registers input devices, and starts the background hardware interrupt/signal timer.
- **Interactive REPL**: On MicroPython (`machine.Timer`), Linux desktop (`librt`), and Windows (`uwin32`), you can construct LVGL widgets and drop out to the interactive `>>>` prompt without any loop—the UI and animations keep running live in the background.
- **Standalone Desktop Applications**: Standalone scripts include `app.run()` to keep the desktop process alive.

See [`python/README.md`](python/README.md). Edit `python/display_driver.py` here,
commit the complete regenerated source, then sync that exact 40-character commit
or release tag into each consumer.
The coordinator that wires LVGL to a PyDevices `board_config` (display flush,
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.

## Consumers

| Repo | Role & Sync |
|---|---|
| [lvgl-micropython](https://github.com/PyDevices/lvgl-micropython) | MicroPython C module: `generated/lvgl_micropython.c`, `lvgl/`, `lv_conf.h`, `python/display_driver.py` → `lib/` |
| [lvgl-circuitpython](https://github.com/PyDevices/lvgl-circuitpython) | CircuitPython tree patches: `generated/lvgl_circuitpython.c`, `generated/lvgl_circuitpython.h`, `lvgl/`, `lv_conf.h`, `python/display_driver.py` → `lib/` |
| [lvgl-micropython](https://github.com/PyDevices/lvgl-micropython) | MicroPython C module: `generated/lvgl_micropython.c`, `lvgl/`, `lv_conf.h`, `python/fs_driver.py` → `lib/` |
| [lvgl-circuitpython](https://github.com/PyDevices/lvgl-circuitpython) | CircuitPython tree patches: `generated/lvgl_circuitpython.c`, `generated/lvgl_circuitpython.h`, `lvgl/`, `lv_conf.h`, `python/fs_driver.py` → `lib/` |
| [lvgl-python](https://github.com/PyDevices/lvgl-python) | CPython extension & TestPyPI wheel publisher: exact-commit `generated/lvgl_python.c`, `generated/lvgl.pyi`, `lvgl/`, `lv_conf.h`, and helpers (see [releasing-bindings.md](docs/releasing-bindings.md)) |

Each consumer records the resolved source SHA in `LVGL_BINDINGS_COMMIT`.
Expand Down
7 changes: 4 additions & 3 deletions binding/analyze.py
Original file line number Diff line number Diff line change
Expand Up @@ -396,9 +396,10 @@ def analyze(ctx=None):
# counter (see the matching mp_lv_funcptr/{func}_callback increments in
# emit_c_micropython_style.py). It is deliberately private in the
# canonical API model (see api_model.build_api_model) yet still emitted
# as a real MicroPython/CircuitPython module global, because helper
# code this repo ships (python/display_driver.py) reads it to detect
# reentrant lv.task_handler() calls from inside an LVGL callback. Do not
# as a real MicroPython/CircuitPython module global, because PyDevices'
# LVGL coordinator (display_driver.py in PyDevices/pydevices lib/) reads
# it to detect reentrant lv.task_handler() calls from inside an LVGL
# callback. Do not
# remove this on the assumption it is unused C state; its only reader
# is Python code outside this translation unit.
blobs["_nesting"] = parser.parse("extern int _nesting;").ext[0].type.type
Expand Down
2 changes: 1 addition & 1 deletion binding/api_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -1189,7 +1189,7 @@ def referenced_type_names(type_):
# analyze.py's `blobs` (see analyze.py). It is deliberately private
# (not part of the documented public API) but still emitted as a real
# MicroPython/CircuitPython module global and stubbed, because
# python/display_driver.py (shipped in this repo) depends on it to
# PyDevices' display_driver.py (pydevices lib/) depends on it to
# detect reentrant lv.task_handler() calls. See emit_backend.py's
# module_registration_plan and emit_pyi_canonical.py's CanonicalPyiEmitter
# for the matching audited exceptions.
Expand Down
4 changes: 2 additions & 2 deletions binding/emit_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -444,8 +444,8 @@ def module_registration_plan(
# _nesting (the binding-internal callback re-entrancy counter; see
# analyze.py and emit_c_micropython_style.py) is deliberately kept
# in the emitted module globals here even though the canonical API
# model marks it private: python/display_driver.py, shipped in this
# repo, reads it at runtime on MicroPython/CircuitPython. CPython's
# model marks it private: PyDevices' display_driver.py (pydevices
# lib/) reads it at runtime on MicroPython/CircuitPython. CPython's
# native emitter never adds it to generated_globals in the first
# place (it uses its own ContextVar-scoped lvpy_nesting_inc/dec
# instead), so this is a no-op there.
Expand Down
2 changes: 1 addition & 1 deletion binding/emit_pyi_canonical.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ def _emit_helpers(self) -> None:
# _nesting is deliberately private in the canonical API model (see
# api_model.build_api_model) but still stubbed here: it is a real
# runtime attribute on MicroPython/CircuitPython, and
# python/display_driver.py (shipped in this repo) depends on its
# PyDevices' display_driver.py (pydevices lib/) depends on its
# public shape (`.value: int`).
self._add("class _Nesting:")
self._add("value: int", 1)
Expand Down
2 changes: 1 addition & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# lvgl-bindings documentation

- [Using LVGL with PyDevices](using-lvgl-with-pydevices.md) — the sister
projects, wiring `board_config` to LVGL, what `display_driver` does, and the
projects, wiring `board_config` to LVGL, what PyDevices' `display_driver` does, and the
sync/async timer contract.
- [Loading fonts at runtime](fonts.md) — the `fonts/*.bin` collection,
`fs_driver.py`, and converting your own fonts; no firmware rebuild needed.
Expand Down
2 changes: 1 addition & 1 deletion docs/fonts.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ image sources like `lv.image.set_src("S:logo.png")` work through it too.
### Where fs_driver comes from

The canonical copy is `python/fs_driver.py` in this repo. Each consumer
ships a synced copy the same way as `display_driver.py`: frozen into
ships a synced copy: frozen into
lvgl-micropython and lvgl-circuitpython builds, `py_modules` in the
lvgl-python wheel — so `import fs_driver` just works. For a stock MicroPython
board it is also mip-installable:
Expand Down
10 changes: 5 additions & 5 deletions docs/generator-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,15 +89,15 @@ with `visibility="private"`. Despite being private, it is deliberately still
emitted as a real MicroPython/CircuitPython module global (see
`emit_backend.module_registration_plan` and the blob-table loop in
`emit_c_micropython_style.py`) and stubbed as `_nesting: _Nesting` (see
`CanonicalPyiEmitter`), because `python/display_driver.py` — the LVGL
event-loop helper this repo ships and every consumer syncs verbatim — reads
`CanonicalPyiEmitter`), because `display_driver.py` — PyDevices' LVGL
coordinator, in PyDevices/pydevices `lib/` — reads
`lv._nesting.value` at runtime to detect reentrant `lv.task_handler()`
calls from inside an LVGL callback. From inside this translation unit the
counter looks unused (nothing here reads it back), but its only reader is
Python code outside the generated C; do not remove it on that "dead code"
appearance. `tests/test_display_driver_nesting_integration.py` guards this
by executing the helper's actual re-entrancy-guarded code path against a
mock built from the freshly generated MicroPython/CircuitPython namespaces.
appearance. `tests/test_nesting_export.py` guards this by reading the names
the freshly generated MicroPython/CircuitPython C exports; pydevices'
`tests/test_lvgl_display_driver.py` holds the reader's side.

## Commands and validation

Expand Down
16 changes: 9 additions & 7 deletions docs/using-lvgl-with-pydevices.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# Using LVGL with PyDevices

How to run [LVGL](https://lvgl.io/) on the PyDevices display, input, and timing
packages, and what [`python/display_driver.py`](../python/display_driver.py)
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
Expand All @@ -28,7 +29,7 @@ of the sister projects.
```python
import mip
INDEX = "https://PyDevices.github.io/mip"
mip.install("pydevices", index=INDEX) # displaydev, appdev, and the rest of lib/
mip.install("pydevices", index=INDEX) # displaydev, appdev, display_driver and the rest of lib/
mip.install("github:PyDevices/pydevices/board_configs/<your_board>")
```

Expand Down Expand Up @@ -62,11 +63,12 @@ for you.

## `display_driver`

[`python/display_driver.py`](../python/display_driver.py) is the LVGL coordinator.
It ships with all three sister projects (frozen into the MicroPython and
CircuitPython firmwares, bundled with `pydevices-lvgl`). It requires a PyDevices
`board_config`, `events`, `keys`, and `multimer`, and is **independent of the
optional `appdev` package**.
[`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.
Expand Down
9 changes: 0 additions & 9 deletions packages/display_driver.json

This file was deleted.

17 changes: 9 additions & 8 deletions python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,20 @@ generator output — edit them here, then sync into consumer repos.

| File | Import | Notes |
|------|--------|-------|
| `display_driver.py` | `import display_driver` | LVGL-owned display/input/`event_loop` glue for PyDevices (`board_config`, `events`, `keys`, `multimer`); independent of optional `appdev` |
| `fs_driver.py` | `import fs_driver` | Python-backed `lv_fs` driver: `fs_driver.register("S")`, then `lv.binfont_create("S:fonts/x.bin")` streams from the platform filesystem. Pairs with the runtime-loadable fonts in [`../fonts/`](../fonts/) |

## Sync into consumers
`display_driver.py`, the coordinator that wires LVGL to a PyDevices
`board_config`, lives in
[PyDevices/pydevices `lib/`](https://github.com/PyDevices/pydevices/blob/main/lib/display_driver.py)
and ships with `pydevices`, beside the `appdev`, `events`, `keys` and `multimer`
it needs. It was here until 2026-10.

Both `display_driver.py` and `fs_driver.py` follow the same path:
## Sync into consumers

| Repo | Destination | How it ships |
|------|-------------|--------------|
| [lvgl-micropython](https://github.com/PyDevices/lvgl-micropython) | `lib/*.py` | Frozen via `manifest.py` |
| [lvgl-circuitpython](https://github.com/PyDevices/lvgl-circuitpython) | `lib/*.py` | Frozen via `manifest.py` (unix builds) |
| [lvgl-micropython](https://github.com/PyDevices/lvgl-micropython) | `lib/fs_driver.py` | Frozen via `manifest.py` |
| [lvgl-circuitpython](https://github.com/PyDevices/lvgl-circuitpython) | `lib/fs_driver.py` | Frozen via `manifest.py` (unix builds) |
| [lvgl-python](https://github.com/PyDevices/lvgl-python) | repo root | `py_modules` in the `pydevices-lvgl` wheel |

Each consumer has `scripts/sync_from_lvgl_bindings.sh` (or extends the CPython
Expand All @@ -24,9 +27,7 @@ one) that copies this file from **PyDevices/lvgl-bindings on GitHub**.
## MIP (optional)

```text
mip.install("github:PyDevices/lvgl-bindings/packages/display_driver.json")
mip.install("github:PyDevices/lvgl-bindings/packages/fs_driver.json")
```

Requires a PyDevices `board_config` (from a hardware board package or
`pydevices-desktop`), `events`, `keys`, `multimer`, and an `lvgl` binding.
`display_driver` comes with `mip.install("pydevices", index="https://PyDevices.github.io/mip")`.
Loading
Loading