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
21 changes: 12 additions & 9 deletions docs/board-bringup.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ modules (micropython-pydevices' `build_mp.py --modules
displayif,lvgl-micropython,usbif,pygraphics,audiodsp,ulab`, say) the following
are **frozen or built in** — installing them is wasted effort:

- `lvgl` and `display_driver` (from lvgl-bindings)
- `lvgl` (from lvgl-micropython)
- `dotclockframebuffer` (from displayif) — the RGB panel interface
- `_usbif` and the `usbif` Python package: usbif's manifest freezes its
Python half beside its C half, because the two version together
Expand All @@ -32,7 +32,8 @@ are **frozen or built in** — installing them is wasted effort:
and the following are **not**, and must be installed:

- `displaydev`, `appdev`, `multimer`, `audiodev`, `boarddev`, `events`, `keys`
(pydevices). An image built with `--modules all` freezes these too, and
and `display_driver`, LVGL's coordinator (pydevices). An image built with
`--modules all` freezes these too, and
`.frozen` comes before `/lib` on `sys.path`, so there a mip-installed copy
never runs: rebuild to update them.
- `board_config` and `board_peripherals` for the specific board
Expand All @@ -50,13 +51,15 @@ for m in ("lvgl", "display_driver", "dotclockframebuffer", "mipidsi",
(`mipidsi` is the panel interface on the ESP32-P4 boards, `dotclockframebuffer`
on the RGB-panel S3 boards; whichever is not yours will simply be missing.)

**Trap.** `import display_driver` fails with `ImportError` on a bare board even
though it is frozen — because `display_driver` imports `appdev`, and it is
*that* import failing (`no module named 'appdev'`). The same shape hides
behind `usbif`, which trips on `events`. The probe prints the exception text
so you can see which module is really absent; a frozen module that reports
`MISS` with somebody else's name in the message is present and fine. Install
`pydevices` (which carries `appdev` and `events`), then re-probe.
**Trap.** `import usbif` fails with `ImportError` on a bare board even
though it is frozen — because `usbif` imports `events`, and it is *that*
import failing (`no module named 'events'`). The probe prints the exception
text so you can see which module is really absent; a frozen module that
reports `MISS` with somebody else's name in the message is present and fine.
Install `pydevices` (which carries `events`, `appdev` and `display_driver`),
then re-probe. Firmware built before display_driver moved to pydevices
(2026-10) froze it from lvgl-micropython, where it showed the same trap with
`appdev`.

## 2. Installing, over Wi-Fi

Expand Down
58 changes: 58 additions & 0 deletions docs/displaydev.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,64 @@ pydevices-examples does not include a task scheduler. Options:
- **`asyncio`** — works on CPython, MicroPython, and PyScript (required there)
- **[multimer](multimer.md)** — one `Timer` on every host; it rides the page's loop on PyScript

## Frame rate

To see how fast an app draws, switch on the display's meter and read it:

```python
display_drv.measure_fps(True) # or PYDEVICES_FPS=1 in the environment
...
display_drv.fps()
# {"fps": 29.8, "avg_fps": 29.5, "frames": 1180, "seconds": 40.0,
# "present_ms": 4.2, "present_ms_max": 12.0, "busy": 0.13,
# "flushes": 0, "pixels": 0}
display_drv.fps_reset() # start a fresh measurement
display_drv.fps_print(5) # one line every 5 s; 0 stops it
```

`PYDEVICES_FPS=1` measures any example without editing it, and
`PYDEVICES_FPS_PRINT=5` also prints a line every five seconds, for tailing a
log:

```text
fps 29.8 avg 29.5 frames 1180 present 4.2 ms (max 12.0) busy 13%
```

`fps` is the last whole second and `avg_fps` everything since it was switched
on or reset. `present_ms` is the time spent inside the present call per frame,
and `busy` is the share of wall time spent there: a slow frame rate with a high
`busy` means the display is the bottleneck, a low one means the app is.

A frame is one `show()`. On a panel LVGL draws into directly, LVGL presents
with `flush_rect()` once per dirty area, so `display_driver` calls
`frame_done()` when a frame's last area is out; those `flush_rect()` calls are
counted as `flushes` and `pixels`, not frames.

It is off by default and costs nothing then: `show` and `flush_rect` are the
class's own methods until you switch it on, and again after
`measure_fps(False)`.

### Refresh rate

To run any app at another frame rate without touching its code or its board
config, set `PYDEVICES_REFRESH_MS`:

```bash
PYDEVICES_REFRESH_MS=16 PYDEVICES_FPS_PRINT=5 micropython lv_test_timer.py # about 60 fps
PYDEVICES_REFRESH_MS=50 PYDEVICES_FPS_PRINT=5 micropython lv_test_timer.py # about 20 fps
```

It replaces the display's `refresh_period_ms` (33 ms, or 16 in the browser)
when the driver starts. Everything that paces frames reads that one attribute:
`appdev.App`'s refresh timer, the display's frame clock, and
`display_driver`'s LVGL refresh timer, so LVGL and non-LVGL apps both follow
it. A board config can set it with `displaydev.env_set("PYDEVICES_REFRESH_MS",
16)` before it builds the display, on hosts where there is no environment.

Unset, zero or not a number leaves the display's own period. An app that
passes `appdev.App(..., refresh_period=N)` has chosen its rate in code and keeps
it; `refresh_period=0` still means the app presents frames itself.

## Vertical scrolling

Many drivers expose **ILI9341-style** vertical scroll: a top fixed band (TFA), a scrollable middle (VSA), and a bottom fixed band (BFA). You define regions with `set_vscroll(tfa, bfa)` or `vscrdef`, then move content with the `vscroll` property (wrapper around `vscsad`).
Expand Down
2 changes: 1 addition & 1 deletion docs/timing-hardware-tests.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ re-armed from its own (scheduled) callback. Nothing in the interpreter
changes.

1. Flash the kitchen-sink image; `mpftp put` the changed `pydevices/lib`
(multimer, appdev, displaydev) and `lvgl-bindings/python/display_driver.py`
(multimer, appdev, displaydev, display_driver.py)
to `/lib`, which beats the frozen copies.
2. **Function check, no display:** `mpftp exec` the body of
`demo_timers.py` (or put it as `/demo_timers.py` and `import demo_timers`).
Expand Down
9 changes: 5 additions & 4 deletions lib/appdev/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -445,14 +445,15 @@ def _wire_one_refresh(self, display, refresh_period):
period = int(refresh_period)
if period <= 0:
return
show = getattr(display, "show", None)
if not callable(show):
if not callable(getattr(display, "show", None)):
return

def _show(timer_obj, _display=display, _show=show):
# Look show up on every tick: a wrapper installed after the App started
# (displaydev's measure_fps) must be the one that is called.
def _show(timer_obj, _display=display):
if self._refresh_paused:
return
_show(timer_obj)
_display.show(timer_obj)

name = "refresh:%s" % (getattr(display, "__class__", type(display)).__name__,)
tim = multimer.every(period, _show, name=name)
Expand Down
Loading
Loading