From 6f36a0f449165c37237605cf9bf27e8e19b7189e Mon Sep 17 00:00:00 2001 From: Brad Barnett <127794626+bdbarnett@users.noreply.github.com> Date: Wed, 7 Oct 2026 12:55:58 -0500 Subject: [PATCH 1/5] lv_test_timer shows the frame rate; the cast runner reads displaydev's meter lv_test_timer switches on display_drv.measure_fps and shows the rate, present time and busy share on screen, updated each second; kit mode puts the fps dict in KIT_RESULT and lv_timer_test_kit's table shows the average. run_cast_example.py drops its own show() wrapper (time_shows) for measure_fps. The CAST line keeps show_ms_mean and show_ms_max and adds show_fps, show_frames and show_busy. --- lib/examples/lv_test_timer.py | 34 +++++++++++++++++++++++++- tools/lv_timer_test_kit.py | 3 +++ tools/run_cast_example.py | 46 ++++++++++++++++------------------- 3 files changed, 57 insertions(+), 26 deletions(-) diff --git a/lib/examples/lv_test_timer.py b/lib/examples/lv_test_timer.py index 4385101b7..df0348e2c 100644 --- a/lib/examples/lv_test_timer.py +++ b/lib/examples/lv_test_timer.py @@ -6,7 +6,9 @@ already has. Shows interpreter, OS, display, timer backend, and LVGL version; a seconds counter -and spinning arc prove LVGL timers fire; a tap button exercises input. +and spinning arc prove LVGL timers fire; a tap button exercises input. The +display's frame rate (``display_drv.measure_fps``) is on screen, updated each +second, and in kit mode's ``KIT_RESULT`` line as ``fps``. Interactive (default): build the UI and let the app run itself — no trailing ``app.run()``. At a REPL the prompt comes back for introspection while LVGL @@ -59,6 +61,29 @@ _DURATION_S = 4 _RESULT_PREFIX = "KIT_RESULT=" +# A displaydev older than measure_fps has no meter: the label says so. +_measure = getattr(display_drv, "measure_fps", None) +if _measure is not None: + _measure(True) + + +def get_fps(): + """The display's frame-rate dict, or None without a meter.""" + fps = getattr(display_drv, "fps", None) + return fps() if fps is not None else None + + +def _fps_text(): + s = get_fps() + if s is None: + return "fps: n/a" + return "%.1f fps (avg %.1f) %.1f ms busy %d%%" % ( + s["fps"], + s["avg_fps"], + s["present_ms"], + round(s["busy"] * 100), + ) + def _mode_label(): return multimer.info().get("source") or "sync" @@ -207,10 +232,16 @@ def build_ui(): seconds_lbl.set_text("Seconds: 0") seconds_lbl.align_to(arc, lv.ALIGN.OUT_TOP_MID, 0, -4) + fps_lbl = lv.label(scr) + fps_lbl.set_text(_fps_text()) + fps_lbl.align_to(seconds_lbl, lv.ALIGN.OUT_TOP_MID, 0, -4) + def on_seconds_timer(_t): global _seconds _seconds += 1 seconds_lbl.set_text(f"Seconds: {_seconds}") + fps_lbl.set_text(_fps_text()) + fps_lbl.align_to(seconds_lbl, lv.ALIGN.OUT_TOP_MID, 0, -4) def on_arc_timer(_t): global _arc_angle @@ -329,6 +360,7 @@ def _emit_result(state, taps): "backend": timer_backend_name(), "seconds": seconds, "taps": taps, + "fps": get_fps(), } print(_RESULT_PREFIX + json.dumps(payload, separators=(",", ":"))) sys.stdout.flush() diff --git a/tools/lv_timer_test_kit.py b/tools/lv_timer_test_kit.py index 1cb9aa92a..f23c9dc80 100755 --- a/tools/lv_timer_test_kit.py +++ b/tools/lv_timer_test_kit.py @@ -119,6 +119,9 @@ def summarize(result: dict | None, returncode: int, timed_out: bool) -> str: if status == "skip": return "NA" if status == "ok": + fps = result.get("fps") + if isinstance(fps, dict): + return f"{backend}, ok, {fps.get('avg_fps', 0):.1f} fps" return f"{backend}, ok" if status == "error": return f"{backend}, error" diff --git a/tools/run_cast_example.py b/tools/run_cast_example.py index 4103e26cb..7a2932368 100644 --- a/tools/run_cast_example.py +++ b/tools/run_cast_example.py @@ -4,8 +4,10 @@ The first argument is the method's board_config folder in lib/examples (roku, miracast or roku_hls). The example runs unchanged; after SECONDS -(default 30) one CAST line reports the display's stats(), and it reports -again every SECONDS after that until the example ends or is stopped. +(default 30) one CAST line reports the display's stats() and its frame +rate (displaydev's measure_fps: show_fps, show_frames, show_ms_mean, +show_ms_max, show_busy), and it reports again every SECONDS after that until +the example ends or is stopped. On a board, set CFG, EXAMPLE and SECONDS below and run it with mpftp; the line also goes to /cast.txt, written once, after the first report. @@ -56,10 +58,15 @@ def report(): try: import board_config - st = board_config.display_drv.stats() - if _shows[0]: - st["show_ms_mean"] = _shows[1] / _shows[0] - st["show_ms_max"] = _shows[2] + d = board_config.display_drv + st = d.stats() + fps = d.fps() if hasattr(d, "fps") else None + if fps and fps["frames"]: + st["show_fps"] = fps["avg_fps"] + st["show_frames"] = fps["frames"] + st["show_ms_mean"] = fps["present_ms"] + st["show_ms_max"] = fps["present_ms_max"] + st["show_busy"] = fps["busy"] line = "CAST %s %s %s %s %s" % ( cfg, example, @@ -92,26 +99,15 @@ def report(): machine.soft_reset() -_shows = [0, 0.0, 0.0] # count, total ms, longest ms - - -def time_shows(): - """Every show() timed, for the report: where an app's frame time goes.""" +def measure_shows(): + """The display's frame meter, for the report: where an app's frame time goes.""" import board_config - d = board_config.display_drv - show = d.show - - def timed(*args, **kwargs): - t = now() - r = show(*args, **kwargs) - ms = (now() - t) * 1000 - _shows[0] += 1 - _shows[1] += ms - _shows[2] = max(_shows[2], ms) - return r - - d.show = timed + measure = getattr(board_config.display_drv, "measure_fps", None) + if measure is None: + print("run_cast_example: this displaydev has no measure_fps; no show_* fields") + else: + measure(True) def report_loop(): @@ -168,7 +164,7 @@ def timed_show(*args, **kwargs): from utils import roku_companion roku_companion.ROKU_IP = target -time_shows() +measure_shows() if board: # an LVGL app holds a board's interpreter and starves a reporting thread report_from_show() From 1fb4dd4501c9a5949a92a7fc1ce27976f5b88f38 Mon Sep 17 00:00:00 2001 From: Brad Barnett <127794626+bdbarnett@users.noreply.github.com> Date: Wed, 7 Oct 2026 14:09:28 -0500 Subject: [PATCH 2/5] lv_test_timer: a facelift that fits 240x240 to 1280x720 Two cards on the dark default theme. The info card reads like a spec sheet: Python, Display, Screen (size, rotation, and a desktop window's scale), Timer, LVGL. The live card holds every moving part, so the dirty areas stay together: the arc turns once a second on its 50 ms timer with the seconds in its middle (drift between the two timers shows), the frame rate, the tap button. Two layout rules: side by side only when the screen is more than 1.5 times wider than tall, and the font is the largest built-in size at or below a twentieth of the short side. --- lib/examples/lv_test_timer.py | 230 +++++++++++++++++++++++++--------- 1 file changed, 170 insertions(+), 60 deletions(-) diff --git a/lib/examples/lv_test_timer.py b/lib/examples/lv_test_timer.py index df0348e2c..a92175a42 100644 --- a/lib/examples/lv_test_timer.py +++ b/lib/examples/lv_test_timer.py @@ -5,10 +5,19 @@ LVGL timer smoke test. Uses whatever timer mode ``board_config`` / ``app`` already has. -Shows interpreter, OS, display, timer backend, and LVGL version; a seconds counter -and spinning arc prove LVGL timers fire; a tap button exercises input. The -display's frame rate (``display_drv.measure_fps``) is on screen, updated each -second, and in kit mode's ``KIT_RESULT`` line as ``fps``. +Two cards. One says what this run is: the interpreter and OS, the display +driver, the screen (size, rotation and, on a desktop, the window's scale), the +timer's wake source, and LVGL's version. The other holds every moving part, so +the dirty areas stay together: an arc that turns once a second on a 50 ms +timer, with the seconds counted in its middle (if the two timers drift apart, +it shows), the display's frame rate (``display_drv.measure_fps``), and a tap +button for input. The frame rate is also in kit mode's ``KIT_RESULT`` line as +``fps``. + +The layout fits any screen from 240x240 to 1280x720 with two rules: the cards +sit side by side only when the screen is more than 1.5 times wider than tall, +and the font is the largest built-in size at or below a twentieth of the +screen's short side. Interactive (default): build the UI and let the app run itself — no trailing ``app.run()``. At a REPL the prompt comes back for introspection while LVGL @@ -73,18 +82,6 @@ def get_fps(): return fps() if fps is not None else None -def _fps_text(): - s = get_fps() - if s is None: - return "fps: n/a" - return "%.1f fps (avg %.1f) %.1f ms busy %d%%" % ( - s["fps"], - s["avg_fps"], - s["present_ms"], - round(s["busy"] * 100), - ) - - def _mode_label(): return multimer.info().get("source") or "sync" @@ -166,6 +163,8 @@ def get_platform_info(): "lvgl": _lvgl_label(), "mode": _mode_label(), "rotation": int(getattr(display_drv, "rotation", 0) or 0), + # a desktop window's scale (after fitting the desktop); boards have none + "scale": getattr(display_drv, "scale", None) or getattr(display_drv, "_scale", None), } @@ -173,22 +172,41 @@ def timer_backend_name(): return get_platform_info()["timer"] -def _add_info_labels(scr, info, y_start=26, line_h=16): - lines = ( - f"Mode: {info['mode']}", - f"Interpreter: {info['interpreter']}", - f"OS: {info['os']}", - f"Display: {info['display']} {info.get('resolution', '?')}", - f"Timer: {info['timer']}", - f"LVGL: {info['lvgl']}", - f"Rotation: {info.get('rotation', 0)}", +_FONT_SIZES = (14, 16, 24, 32, 40) + + +def _font_for(short_side): + """The largest built-in Montserrat at or below a twentieth of the short side.""" + best = None + for size in _FONT_SIZES: + font = getattr(lv, "font_montserrat_%d" % size, None) + if font is not None and (best is None or size <= short_side // 20): + best = font + return best + + +def _fps_short(split=False): + """The frame rate, the time each present takes, and the share of time + spent presenting; on two lines when it sits beside the arc.""" + s = get_fps() + if s is None: + return "fps n/a" + return "%.1f fps%s%.1f ms, %d%% busy" % ( + s["fps"], + "\n" if split else ", ", + s["present_ms"], + round(s["busy"] * 100), ) - y = y_start - for text in lines: - lbl = lv.label(scr) - lbl.set_text(text) - lbl.align(lv.ALIGN.TOP_MID, 0, y) - y += line_h + + +def _card(parent, flow, pad): + c = lv.obj(parent) + c.set_style_pad_all(pad, 0) + c.set_style_pad_gap(pad // 2, 0) + c.set_flex_flow(flow) + c.remove_flag(lv.obj.FLAG.SCROLLABLE) + c.set_flex_grow(1) + return c def build_ui(): @@ -198,7 +216,6 @@ def build_ui(): _taps = 0 _arc_angle = 0 - # Pause shared LVGL task_handler while constructing widgets (not re-entrant). import display_driver inst = display_driver.event_loop.current_instance() @@ -206,52 +223,145 @@ def build_ui(): inst.disable() try: scr = lv.screen_active() - info = get_platform_info() - - title = lv.label(scr) - title.set_text("LVGL Timer Test") - title.align(lv.ALIGN.TOP_MID, 0, 8) - _add_info_labels(scr, info) + w = scr.get_width() + h = scr.get_height() + short = min(w, h) + font = _font_for(short) + big = _font_for(short * 2) or font + pad = max(4, short // 40) + try: + th = lv.theme_default_init( + lv.display_get_default(), + lv.palette_main(lv.PALETTE.BLUE), + lv.palette_main(lv.PALETTE.TEAL), + True, + font, + ) + lv.display_get_default().set_theme(th) + except AttributeError: + pass + scr.set_style_text_font(font, 0) + scr.set_style_pad_all(pad, 0) + scr.set_style_pad_gap(pad, 0) + scr.remove_flag(lv.obj.FLAG.SCROLLABLE) + landscape = ( + w * 2 > h * 3 + ) # side by side only when the screen is more than 1.5 times wider than tall + scr.set_flex_flow(lv.FLEX_FLOW.ROW if landscape else lv.FLEX_FLOW.COLUMN) - btn = lv.button(scr) - btn.set_size(120, 50) - btn.align(lv.ALIGN.BOTTOM_MID, 0, -30) - btn_lbl = lv.label(btn) - btn_lbl.set_text("Tap me (0)") - btn_lbl.center() + info = get_platform_info() + # The info card: what this run is, read once. + card = _card(scr, lv.FLEX_FLOW.COLUMN, pad) + if landscape: + card.set_height(lv.pct(100)) + else: + card.set_width(lv.pct(100)) + title = lv.label(card) + title.set_text("LVGL timer test") + title.set_style_text_color(lv.palette_main(lv.PALETTE.BLUE), 0) + rows = ( + ("Python", "%s, %s" % (info["interpreter"], info["os"])), + ("Display", info["display"]), + ( + "Screen", + "%s, rot %d%s" + % ( + info["resolution"], + info["rotation"], + ", x%.3g" % info["scale"] if info["scale"] and info["scale"] != 1 else "", + ), + ), + ("Timer", info["timer"]), + ("LVGL", info["lvgl"]), + ) + muted = lv.palette_main(lv.PALETTE.GREY) + for key, value in rows: + row = lv.obj(card) + row.remove_style_all() + row.set_size(lv.pct(100), lv.SIZE_CONTENT) + row.set_flex_flow(lv.FLEX_FLOW.ROW) + row.set_flex_align( + lv.FLEX_ALIGN.SPACE_BETWEEN, lv.FLEX_ALIGN.CENTER, lv.FLEX_ALIGN.CENTER + ) + row.set_style_pad_column(pad, 0) # a key never runs into its value + k = lv.label(row) + k.set_text(key) + k.set_style_text_color(muted, 0) + v = lv.label(row) + v.set_text(value) + v.set_long_mode(lv.label.LONG_MODE.DOTS) + v.set_style_max_width(lv.pct(72), 0) + + if landscape: + card.set_flex_align(lv.FLEX_ALIGN.CENTER, lv.FLEX_ALIGN.START, lv.FLEX_ALIGN.START) + else: + card.set_flex_grow(0) + card.set_height(lv.SIZE_CONTENT) + # The live card lays out along its own long side: down when it's tall + # (beside the info card, or under it on a portrait screen), across when + # it's wide (under the info card on a square or landscape screen). + across = not landscape and w * 10 >= h * 9 + + # The live card: every moving part, so the dirty areas stay together. + live = _card(scr, lv.FLEX_FLOW.ROW if across else lv.FLEX_FLOW.COLUMN, pad) + live.set_flex_align(lv.FLEX_ALIGN.SPACE_EVENLY, lv.FLEX_ALIGN.CENTER, lv.FLEX_ALIGN.CENTER) + if landscape: + live.set_height(lv.pct(100)) + else: + live.set_width(lv.pct(100)) - arc = lv.arc(scr) - arc.set_size(80, 80) - arc.align_to(btn, lv.ALIGN.OUT_TOP_MID, 0, -8) + arc_d = short // 3 if across else short * 2 // 5 + arc = lv.arc(live) + arc.set_size(arc_d, arc_d) arc.set_bg_angles(0, 360) + arc.set_rotation(270) arc.set_angles(0, 0) + arc.set_style_arc_width(max(4, arc_d // 12), lv.PART.MAIN) + arc.set_style_arc_width(max(4, arc_d // 12), lv.PART.INDICATOR) arc.remove_style(None, lv.PART.KNOB) arc.remove_flag(lv.obj.FLAG.CLICKABLE) - - seconds_lbl = lv.label(scr) - seconds_lbl.set_text("Seconds: 0") - seconds_lbl.align_to(arc, lv.ALIGN.OUT_TOP_MID, 0, -4) - - fps_lbl = lv.label(scr) - fps_lbl.set_text(_fps_text()) - fps_lbl.align_to(seconds_lbl, lv.ALIGN.OUT_TOP_MID, 0, -4) + seconds_lbl = lv.label(arc) + seconds_lbl.set_style_text_font(big, 0) + seconds_lbl.set_text("0") + seconds_lbl.center() + + side = live + if across: + # beside the arc: the frame rate over the button + side = lv.obj(live) + side.remove_style_all() + side.set_size(lv.SIZE_CONTENT, lv.SIZE_CONTENT) + side.set_flex_flow(lv.FLEX_FLOW.COLUMN) + side.set_flex_align(lv.FLEX_ALIGN.CENTER, lv.FLEX_ALIGN.CENTER, lv.FLEX_ALIGN.CENTER) + side.set_style_pad_gap(pad, 0) + fps_lbl = lv.label(side) + fps_lbl.set_style_text_color(muted, 0) + fps_lbl.set_style_text_align(lv.TEXT_ALIGN.CENTER, 0) + fps_lbl.set_text(_fps_short(across)) + + btn = lv.button(side) + btn.set_style_pad_hor(pad * 2, 0) + btn.set_style_pad_ver(pad, 0) + btn_lbl = lv.label(btn) + btn_lbl.set_text("Tap 0") + btn_lbl.center() def on_seconds_timer(_t): global _seconds _seconds += 1 - seconds_lbl.set_text(f"Seconds: {_seconds}") - fps_lbl.set_text(_fps_text()) - fps_lbl.align_to(seconds_lbl, lv.ALIGN.OUT_TOP_MID, 0, -4) + seconds_lbl.set_text(str(_seconds)) + fps_lbl.set_text(_fps_short(across)) def on_arc_timer(_t): + # 18 degrees every 50 ms: one turn a second, in step with the counter. global _arc_angle - _arc_angle = (_arc_angle + 10) % 360 + _arc_angle = (_arc_angle + 18) % 360 arc.set_angles(0, _arc_angle) def on_click(_e): global _taps _taps += 1 - btn_lbl.set_text(f"Tap me ({_taps})") + btn_lbl.set_text("Tap %d" % _taps) lv.timer_create(on_seconds_timer, 1000, None) lv.timer_create(on_arc_timer, 50, None) From 2406edefe2518bde23d3bec957fbca755ebc544d Mon Sep 17 00:00:00 2001 From: Brad Barnett <127794626+bdbarnett@users.noreply.github.com> Date: Wed, 7 Oct 2026 14:26:42 -0500 Subject: [PATCH 3/5] lv_test_timer: the Timer row names multimer's wake source, and says idle only when delivery is idle (bytecode is the norm everywhere else) --- lib/examples/lv_test_timer.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/examples/lv_test_timer.py b/lib/examples/lv_test_timer.py index a92175a42..11acd9046 100644 --- a/lib/examples/lv_test_timer.py +++ b/lib/examples/lv_test_timer.py @@ -146,9 +146,13 @@ def _lvgl_label(): def _timer_type(): - # multimer has one Timer class; what differs per host is the wake source. + """multimer's wake source on this host (machine, native, signal, pending, + asyncio, wasm or none). Callbacks run between bytecodes almost everywhere, + so the delivery is shown only when it's the other kind, ``idle``, where + they run only when the program yields to its loop (asyncio, a browser).""" info = multimer.info() - return "%s/%s" % (info.get("source"), info.get("delivery")) + source = info.get("source") + return source if info.get("delivery") != "idle" else "%s, idle" % source def get_platform_info(): From 96df81a63978beacaf2d9f73cbbb05f71b85c991 Mon Sep 17 00:00:00 2001 From: Brad Barnett <127794626+bdbarnett@users.noreply.github.com> Date: Wed, 7 Oct 2026 14:44:04 -0500 Subject: [PATCH 4/5] lv_test_timer: the arc steps every frame, at the display's refresh period, so the frame rate shown is the display's (30 at 33 ms) rather than the app's pace; its angle comes from the clock, one turn a second at any period --- lib/examples/lv_test_timer.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/lib/examples/lv_test_timer.py b/lib/examples/lv_test_timer.py index 11acd9046..54172b391 100644 --- a/lib/examples/lv_test_timer.py +++ b/lib/examples/lv_test_timer.py @@ -8,9 +8,9 @@ Two cards. One says what this run is: the interpreter and OS, the display driver, the screen (size, rotation and, on a desktop, the window's scale), the timer's wake source, and LVGL's version. The other holds every moving part, so -the dirty areas stay together: an arc that turns once a second on a 50 ms -timer, with the seconds counted in its middle (if the two timers drift apart, -it shows), the display's frame rate (``display_drv.measure_fps``), and a tap +the dirty areas stay together: an arc that turns once a second, stepping +every frame at the display's refresh period, with the seconds counted in its +middle (if the two timers drift apart, it shows), the display's frame rate (``display_drv.measure_fps``), and a tap button for input. The frame rate is also in kit mode's ``KIT_RESULT`` line as ``fps``. @@ -356,10 +356,18 @@ def on_seconds_timer(_t): seconds_lbl.set_text(str(_seconds)) fps_lbl.set_text(_fps_short(across)) + # The arc steps at the display's refresh period (33 ms unless the + # display or PYDEVICES_REFRESH_MS says otherwise), so it changes every + # frame and the frame rate shown is the display's, not this app's pace. + # Its angle comes from the clock: one turn a second at any period, in + # step with the seconds counter, which shows if the two timers drift. + frame_ms = int(getattr(display_drv, "refresh_period_ms", 0) or 33) + t0 = multimer.ticks_ms() + def on_arc_timer(_t): - # 18 degrees every 50 ms: one turn a second, in step with the counter. global _arc_angle - _arc_angle = (_arc_angle + 18) % 360 + ms = multimer.ticks_diff(multimer.ticks_ms(), t0) % 1000 + _arc_angle = ms * 360 // 1000 arc.set_angles(0, _arc_angle) def on_click(_e): @@ -368,7 +376,7 @@ def on_click(_e): btn_lbl.set_text("Tap %d" % _taps) lv.timer_create(on_seconds_timer, 1000, None) - lv.timer_create(on_arc_timer, 50, None) + lv.timer_create(on_arc_timer, frame_ms, None) btn.add_event_cb(on_click, lv.EVENT.CLICKED, None) return btn finally: From 6ec00664463a435ccc4981a55d009a7ce1580730 Mon Sep 17 00:00:00 2001 From: Brad Barnett <127794626+bdbarnett@users.noreply.github.com> Date: Wed, 7 Oct 2026 15:15:34 -0500 Subject: [PATCH 5/5] display_driver comes from pydevices/lib: tools and docs stop pointing at lvgl-bindings display_driver moved to PyDevices/pydevices lib/ (PyDevices/pydevices#129). lvgl_input_probe and multimer_source_preload put lvgl-bindings/python ahead of pydevices/lib on the path, which would have picked the old copy until lvgl-bindings drops it; they now take it from pydevices/lib with the rest. --- AGENTS.md | 15 ++++++++------- README.md | 2 +- tools/lvgl_input_probe.py | 1 - tools/multimer_source_preload.py | 1 - tools/timing_bench/README.md | 4 ++-- 5 files changed, 11 insertions(+), 12 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index d90a4f5be..4492de155 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -124,8 +124,8 @@ passes `--timer-async`. ### Architecture note: timers and refresh - Non-LVGL examples instantiate `appdev.App(board_config)`. - LVGL's frozen/bundled `display_driver` owns an independent coordinator and - does not import `appdev`. Both consume neutral board-config callables and + LVGL's `display_driver` (pydevices) builds its own coordinator on + `appdev.App`. Both consume neutral board-config callables and use `multimer`; display drivers remain policy-free. ### MCU: no `_thread` for network / blocking work @@ -141,11 +141,12 @@ App pattern: queue work and drain on the main tick — see `roku_widgets` / - Install the CPython LVGL binding from TestPyPI (import name `lvgl`): `.venv/bin/pip install -i https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ pydevices-lvgl` (see https://github.com/PyDevices/lvgl-python). The update script installs it. -- `display_driver` (frozen in MP/CP LVGL firmwares; bundled with `pydevices-lvgl`) - owns the LVGL `event_loop` (tick via `app.on_tick`, `asyncio` from - `multimer`) and claims app display refresh so LVGL presents frames from - `task_handler`. SoT: [lvgl-bindings](https://github.com/PyDevices/lvgl-bindings) - `python/display_driver.py` — not shipped from this repo. +- `display_driver` owns the LVGL `event_loop` (tick via `app.on_tick`, + `asyncio` from `multimer`) and claims app display refresh so LVGL presents + frames from `task_handler`. SoT: + [pydevices](https://github.com/PyDevices/pydevices) `lib/display_driver.py`, + shipped with pydevices (mip, wheel, frozen) — not from this repo, and since + 2026-10 not from lvgl-bindings either. - Test LVGL timers with `tools/lv_timer_test_kit.py` (modes: `sync`, `async`). Headless: `SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy .venv/bin/python tools/lv_timer_test_kit.py --only cpython-venv`. - Non-obvious: the sync `multimer.auto.Timer` provider on CPython/Linux delivers via a diff --git a/README.md b/README.md index ece85691e..970b56f28 100644 --- a/README.md +++ b/README.md @@ -150,7 +150,7 @@ a single `pydevices` package, not one per component. - [pydevices](https://github.com/PyDevices/pydevices) — canonical product source and core driver package home - [mip](https://github.com/PyDevices/mip) — PyDevices MIP index fork -- [lvgl-bindings](https://github.com/PyDevices/lvgl-bindings) — shared LVGL binding and `display_driver` source +- [lvgl-bindings](https://github.com/PyDevices/lvgl-bindings) — shared LVGL binding source (LVGL's `display_driver` is in pydevices) - [lvgl-micropython](https://github.com/PyDevices/lvgl-micropython), [lvgl-circuitpython](https://github.com/PyDevices/lvgl-circuitpython), and [lvgl-python](https://github.com/PyDevices/lvgl-python) — interpreter-specific LVGL distributions diff --git a/tools/lvgl_input_probe.py b/tools/lvgl_input_probe.py index 5d51f157e..018f3502d 100755 --- a/tools/lvgl_input_probe.py +++ b/tools/lvgl_input_probe.py @@ -20,7 +20,6 @@ for _path in ( _workspace + "/pydevices/lib", _workspace + "/pydevices/tools", - _workspace + "/lvgl-bindings/python", _root + "/lib", ): if _path not in sys.path: diff --git a/tools/multimer_source_preload.py b/tools/multimer_source_preload.py index ee864dbf3..1e8fabfd7 100755 --- a/tools/multimer_source_preload.py +++ b/tools/multimer_source_preload.py @@ -81,7 +81,6 @@ def _bootstrap_path(source_workspace=False): "../../pydevices/drivers", "../../pydevices/utils", "../../pydevices/lib", - "../../lvgl-bindings/python", ) ) for directory in directories: diff --git a/tools/timing_bench/README.md b/tools/timing_bench/README.md index ad6566623..eed5db73d 100644 --- a/tools/timing_bench/README.md +++ b/tools/timing_bench/README.md @@ -17,8 +17,8 @@ run at a time, nothing else running. `lv_pace.py` builds an LVGL screen with an arc moved by a 16 ms LVGL timer, records `REFR_READY` events for `PACE_MS` (3000) and prints one `PACE=` line. `PACE_API=OLD|NEW`, `PACE_STATIC=1` for the static-screen case. The -path also has `lvgl-bindings/python` (the driver under test) and -`pydevices/board_configs/desktop` on it. Both interpreters were run +path also had `lvgl-bindings/python` (the driver under test, which has lived in +`pydevices/lib` since 2026-10) and `pydevices/board_configs/desktop` on it. Both interpreters were run headless with SDL's dummy video driver. Results: