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
24 changes: 22 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/ \
Expand Down Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions tools/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "<micropython-pydevices>\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.
Expand Down
2 changes: 1 addition & 1 deletion tools/example_interpreters.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
4 changes: 4 additions & 0 deletions tools/example_test_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
60 changes: 60 additions & 0 deletions tools/pydevices_test_mode.py
Original file line number Diff line number Diff line change
Expand Up @@ -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``.

Expand Down