LVGL header-to-C binding generator for MicroPython, CircuitPython, and CPython.
This repo owns the binding tags and generated sources used by the downstream LVGL repos, and is the canonical description of how the family fits together:
- lvgl-bindings (this repo) — generates the C bindings (
generated/lvgl_*.c,lvgl.pyi) from the LVGL headers for all three targets, and owns the release chain. - lvgl-python — the publishing endpoint. Turns
synced bindings into versioned
pydevices-lvglwheels on TestPyPI. It is the only repo in the family that publishes a package. - lvgl-circuitpython and lvgl-micropython — consumer/build repos. They consume the generated bindings and rebuild their own CircuitPython / MicroPython targets, but do not publish their own packages to TestPyPI.
Every other repo in the family links back to this section rather than repeating it.
SINGLE WRITER: python/ helpers, lv_conf.h, and the generated bindings under generated/
are edited only here. Consumer repos sync these files in; any edits made directly in a
consumer repo get silently overwritten by the next sync. See
"Releases and propagation" below.
- Using LVGL with PyDevices — how the three
sister projects fit together, and what
python/display_driver.pydoes. - Loading fonts at runtime —
fonts/*.binandpython/fs_driver.py: any built-in font without a firmware rebuild. - Generator architecture — canonical model, policy, target lowering, parser pin, and validation invariants.
- Generator migration — clean-break command and artifact changes from the pre-rebuild generator.
- docs/ — the full index.
lvgl-bindings/
binding/ # Modular Python generator (binding.generate is the entry point)
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)
packages/ # Optional MIP manifests
regenerate_all.sh # Primary entry point for regenerating all binding artifacts
docs/ # Architecture, migration, release, and fonts documentation
fonts/ # Committed *.bin font artifacts loaded at runtime by fs_driver.py
tests/ # Generator unit tests
scratch/ # Tracked upstream-baseline oracle used to diff against LVGL upstream
tools/ # Artifact hashing and smoke checks
scripts/ # Verification and release utilities
git clone https://github.com/PyDevices/lvgl-bindings.git lvgl-bindings
cd lvgl-bindings
git submodule update --init lvglPlace lvgl-bindings/ as a sibling of lvgl-micropython/, lvgl-circuitpython/, and/or lvgl-python/ in your workspace. (cmods is an optional convenience workspace — not required.)
python3 -m venv .venv
.venv/bin/pip install -r requirements-dev.txtThe runtime generator dependency set is available separately through
requirements.txt; requirements-dev.txt also installs the pinned test and
static-checking tools used by the validation commands below.
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.
Regenerate after changing lvgl/, lv_conf.h, or binding/, then commit the updated files under generated/:
./regenerate_all.sh # all targets
./regenerate_all.sh --target micropython
./regenerate_all.sh --target circuitpython
./regenerate_all.sh --target cpython
./regenerate_all.sh --pyi-only # shared stub only
./regenerate_all.sh --check --hash # read-only reproducibility check, same gate release usesregenerate_all.sh is the primary entry point; it never commits, tags,
pushes, or dispatches a release. Under the hood it calls the underlying
generator directly:
PYTHONPATH=. .venv/bin/python -m binding.generateUse -m binding.generate directly only when you need flags the wrapper
doesn't expose, such as binding.api_report:
PYTHONPATH=. .venv/bin/python -m binding.api_report generated/api.json \
--baseline docs/baseline/lvgl-bindings-api-baseline.json.gz \
--classification docs/baseline/lvgl-bindings-api-baseline-classification.json \
--format markdownThe unified command preprocesses LVGL once and writes the selected target C
source, the target-neutral api.json, shared lvgl.pp, the CircuitPython
generated header, and the shared lvgl.pyi. The API model is
hashed and includes visibility and target availability; deliberate exceptions
are recorded in binding/api_policy.json. Preprocessing removes compiler line markers so the
inputs are reproducible across checkout paths. The command's --check mode
generates into a temporary directory and never changes the working tree.
The shared stub is generated exclusively from the canonical
generated/api.json model. Use --pyi-only when changing typing emission so
the C bindings, canonical API model, and preprocessed input are not
regenerated. The public names are the single established upstream-compatible
profile; the rebuild intentionally has no alternate naming mode.
binding.api_report validates the canonical model and reports qualified export
counts, common-target coverage, target availability exceptions,
inheritance-expanded object APIs, generated target-artifact hashes, and the
diagnostic projection against the historical baseline.
All targets receive the same parsed declaration IR, canonical API model,
context-local generation state, conversion discovery, inheritance order,
registration plan, and diagnostics policy. MicroPython and CircuitPython share
the mp_obj_t native lowering because CircuitPython embeds the same object API;
target registration and VM/GC lifecycle mechanics remain in target glue.
CPython lowering is native PyObject * code with its own GIL/lock and module
initialization glue. Target emitters may choose different C representations,
but they do not choose different public declarations.
./scripts/verify_bindings.sh # Read-only checksAfter regen, rebuild the consumer repo(s) (lvgl-micropython,
lvgl-circuitpython, lvgl-python) as usual.
Release workflow and tagging: releasing-bindings.md.
display_driver.py is the canonical PyDevices LVGL coordinator:
- It connects the LVGL event loop to
displaydevandmultimerwithout requiringappdev. - Automatic Timer Startup: Simply importing
display_driverinitializes 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. Edit python/display_driver.py here,
commit the complete regenerated source, then sync that exact 40-character commit
or release tag into each consumer.
| Repo | Role & Sync |
|---|---|
| lvgl-micropython | MicroPython C module: generated/lvgl_micropython.c, lvgl/, lv_conf.h, python/display_driver.py → lib/ |
| lvgl-circuitpython | CircuitPython tree patches: generated/lvgl_circuitpython.c, generated/lvgl_circuitpython.h, lvgl/, lv_conf.h, python/display_driver.py → lib/ |
| 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) |
Each consumer records the resolved source SHA in LVGL_BINDINGS_COMMIT.
Consumer sync scripts reject branch names so downstream builds cannot silently
move to a different generator or artifact set.
This repo publishes source tags, not wheels or GitHub Releases — an empty Releases sidebar here is deliberate, not a broken pipeline.
Releases are explicit, never automatic:
./scripts/publish_release_tag.sh --push(or with an explicit version) cuts and pushes an annotatedvX.Y.Ztag. Pushing the tag does not trigger anything by itself.- Someone then dispatches
release.ymlwithpublish=truefor that tag/SHA, which validates the source across all three consumers and, only then, tellslvgl-pythonto sync and publish.
Merging to main releases and propagates nothing. Consumers do not track main; each pins an
exact LVGL_BINDINGS_COMMIT and only moves forward when its own sync is run against a specific
commit or release tag. Of the family, only lvgl-python publishes a package (pydevices-lvgl on
TestPyPI); lvgl-micropython and lvgl-circuitpython rebuild from the synced files but publish
nothing themselves.
See releasing-bindings.md for the full release chain.