From 26cd92a32efe935d96612d5015055c0aa906ebff Mon Sep 17 00:00:00 2001 From: Brad Barnett <127794626+bdbarnett@users.noreply.github.com> Date: Wed, 7 Oct 2026 12:34:23 -0500 Subject: [PATCH] Windows: the kit quits LVGL examples on micropython.exe; how to run on a desktop MicroPython The test kit: on a host without threads (micropython.exe), an LVGL example takes the wrapper's cooperative quit, which is pydevices_test_mode's deadline hook alone, and nothing calls that hook there: appdev's run() uses multimer.run_until() and the exit-hook loop uses _dispatch.sleep_ms(). So every kind = "lvgl" example ran until the kit's timeout and reported a hang. pydevices_test_mode.arm_quit_timer() arms a multimer one-shot for the duration, which both loops deliver; it asks every live app to quit and tries again in 250 ms if none exists yet. example_interpreters.toml: micropython.exe's display backend is WinDisplay, not SDLDisplay. README: --depth 1 for a desktop clone (67 MB, not 270), and how to run the examples with a micropython or micropython.exe built by micropython-pydevices (the pydevices variant; ';' separates MICROPYPATH on Windows). tools/README: the kit runs from native Windows PowerShell too. --- README.md | 24 +++++++++++-- tools/README.md | 14 ++++++++ tools/example_interpreters.toml | 2 +- tools/example_test_wrapper.py | 4 +++ tools/pydevices_test_mode.py | 60 +++++++++++++++++++++++++++++++++ 5 files changed, 101 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ece85691e..56cac272b 100644 --- a/README.md +++ b/README.md @@ -51,10 +51,11 @@ To run an example on an Android phone without building anything, install the prebuilt Runner and stage the script from this clone with `android.py`; see [pydevices' Android page](https://github.com/PyDevices/pydevices/blob/main/docs/android.md#staging-a-script-with-androidpy). -For a desktop clone: +For a desktop clone (`--depth 1` skips the history, which is most of the +download: about 67 MB instead of 270 MB): ```bash -git clone https://github.com/PyDevices/pydevices-examples.git +git clone --depth 1 https://github.com/PyDevices/pydevices-examples.git cd pydevices-examples python3 -m venv .venv .venv/bin/pip install --index-url https://test.pypi.org/simple/ \ @@ -89,6 +90,25 @@ See the and [board configs](https://github.com/PyDevices/pydevices/blob/main/docs/board-configs.md) for complete device setup. +To run the examples on MicroPython on your desktop, build it with +[micropython-pydevices](https://github.com/PyDevices/micropython-pydevices), +using its `pydevices` variant and every module. That variant freezes the +desktop board config and, on Windows, `uwin32`, which the Windows display +and audio backends need. On Windows, build from PowerShell as its +[Windows steps](https://github.com/PyDevices/micropython-pydevices/blob/main/docs/newcomers.md#on-windows) +say, then run an example from `lib/`: + +```powershell +python build_mp.py --port windows --variant pydevices --modules all # in micropython-pydevices +cd pydevices-examples\lib +..\..\micropython-pydevices\builds\windows\pydevices\micropython.exe -X heapsize=64M examples\hello.py +``` + +On Linux the same build with `--port unix` gives +`builds/unix/pydevices/micropython`. `micropython.exe` already has `lib` and +`utils` on its path, so it needs no `MICROPYPATH`; if you set one on Windows, +separate the entries with `;`, not `:`. + ## App ownership Board configs describe hardware. They export neutral pieces such as diff --git a/tools/README.md b/tools/README.md index 2a8a445c6..aa0b07128 100644 --- a/tools/README.md +++ b/tools/README.md @@ -128,6 +128,20 @@ via temp files so a timeout kill does not wipe stdout the way pipes often did. Fix the quit path (wrapper deadline / `pydevices_test_mode` / inject) rather than treating PE as “failed to launch.” +**From native Windows.** The kit also runs from PowerShell with python.org's +Python (3.11 or newer, for `tomllib`): it finds `micropython.exe` on `PATH`, +and nothing in its launch path needs WSL. With an exe built by +micropython-pydevices (`--port windows --variant pydevices --modules all`): + +```powershell +$env:PATH = "\builds\windows\pydevices;$env:PATH" +python tools/example_test_kit.py --no-unit-tests --only-interpreter micropython.exe +``` + +The exe has no threads, so the wrapper quits LVGL examples with a multimer +one-shot timer (`pydevices_test_mode.arm_quit_timer()`); the deadline hook +alone never fires there. + **Scheduling:** with `--order examples` and `--jobs 0` (default), **all** selected interpreters for an example — including both `.exe` launchers — run concurrently. diff --git a/tools/example_interpreters.toml b/tools/example_interpreters.toml index ad7d4357a..8ce3a0bc5 100644 --- a/tools/example_interpreters.toml +++ b/tools/example_interpreters.toml @@ -23,7 +23,7 @@ setup_notes = "MicroPython unix build with SDL display support resolved via PATH [interpreters."micropython.exe"] platform = "micropython" -display_backend = "SDLDisplay" +display_backend = "WinDisplay" available_on = ["windows", "wsl"] kind = "subprocess" command = ["micropython.exe", "-X", "heapsize=64M"] diff --git a/tools/example_test_wrapper.py b/tools/example_test_wrapper.py index fdd5c6bea..4050488ab 100755 --- a/tools/example_test_wrapper.py +++ b/tools/example_test_wrapper.py @@ -432,6 +432,10 @@ def delayed_inject(): pydevices_test_mode.ENABLED = True pydevices_test_mode.DURATION_S = duration_s pydevices_test_mode.install_deadline_hook() + if cooperative and not _has_background_inject(): + # No thread to inject quit, and the deadline hook is never called + # from appdev's run() or multimer's exit-hook loop: a timer quits. + pydevices_test_mode.arm_quit_timer() except ImportError: pass diff --git a/tools/pydevices_test_mode.py b/tools/pydevices_test_mode.py index fecf85e51..c152ab1ac 100644 --- a/tools/pydevices_test_mode.py +++ b/tools/pydevices_test_mode.py @@ -76,6 +76,66 @@ def check_deadline(): return True +def _current_apps(): + """Every live app: LVGL's ``display_driver.app`` and ``appdev.App._current`` + (an LVGL example can run both, e.g. drum_machine's ``appdev`` ``app.run()``).""" + import sys + + apps = [] + module = sys.modules.get("display_driver") + if module is not None and getattr(module, "app", None) is not None: + apps.append(module.app) + appdev = sys.modules.get("appdev") + current = ( + getattr(getattr(appdev, "App", None), "_current", None) if appdev is not None else None + ) + if current is not None and current not in apps: + apps.append(current) + return apps + + +def arm_quit_timer(): + """Request every app's quit ``DURATION_S`` from now, from a multimer timer. + + On a host without threads (``micropython.exe``) nothing else can end an + LVGL example: :func:`check_deadline` is gated on ``_blocking_run`` / + ``_blocking``, and neither ``appdev.App.run()`` (``multimer.run_until``) nor + multimer's exit-hook loop (examples that build their UI and end) calls the + deadline hook from their ``sleep_ms``, so such an example ran until the + kit's timeout. A multimer one-shot timer is delivered by both loops. When it + fires before any app exists (slow imports), it tries again in 250 ms. + Returns True when armed. + """ + if not ENABLED: + return False + try: + import multimer + except ImportError: + return False + + def _fire(_timer): + global _deadline_fired + apps = _current_apps() + if not apps: + multimer.after(250, _fire, name="test_mode.quit") + return + for rt in apps: + try: + request = getattr(rt, "request_quit", None) + if callable(request): + request() + else: + handle = getattr(rt, "_handle_quit", None) + if callable(handle): + handle() + except Exception: + pass + _deadline_fired = True + + multimer.after(int(float(DURATION_S) * 1000), _fire, name="test_mode.quit") + return True + + def install_deadline_hook(): """Register :func:`check_deadline` with ``multimer.set_deadline_hook``.