diff --git a/README.md b/README.md index 4a6171d9..735c64b9 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ for complete device setup. ## App ownership Board configs describe hardware. They export neutral pieces such as -`display_drv`, `touch_read`, `host_read`, and `timer_async`; they do not create +`display_drv`, `touch_read`, and `host_read`; they do not create an application-level `appdev.App`. Non-LVGL examples in this repo opt into the optional `appdev` coordinator: diff --git a/lib/examples/alien/alien.py b/lib/examples/alien/alien.py index 8b002476..72160076 100644 --- a/lib/examples/alien/alien.py +++ b/lib/examples/alien/alien.py @@ -90,5 +90,5 @@ def _tick(_=None): tft_bitmap.bitmap(tft, alien, col, row) st["last_col"], st["old_row"] = col, row - app.every(_tick, period=10, async_=app.timer_async) + app.every(_tick, period=10) main() \ No newline at end of file diff --git a/lib/examples/apollo/apollo.py b/lib/examples/apollo/apollo.py index e5ae96a6..ef2eb12c 100644 --- a/lib/examples/apollo/apollo.py +++ b/lib/examples/apollo/apollo.py @@ -143,4 +143,4 @@ def _tick(_=None): _init_apollo() -app.every(_tick, period=20, async_=app.timer_async) \ No newline at end of file +app.every(_tick, period=20) \ No newline at end of file diff --git a/lib/examples/appdev_simpletest.py b/lib/examples/appdev_simpletest.py index 3e1747e2..adef02f7 100644 --- a/lib/examples/appdev_simpletest.py +++ b/lib/examples/appdev_simpletest.py @@ -14,7 +14,7 @@ def _on_event(e): print("appdev_simpletest: started — click the canvas to see pointer events") -app.every(_heartbeat, period=2000, async_=app.timer_async) +app.every(_heartbeat, period=2000) # Subscribe broadly via device type so any pointer/key event prints. for et in ( diff --git a/lib/examples/appdev_touch_coords.py b/lib/examples/appdev_touch_coords.py index f893974b..93a6a5ce 100644 --- a/lib/examples/appdev_touch_coords.py +++ b/lib/examples/appdev_touch_coords.py @@ -11,7 +11,7 @@ import appdev import events -from multimer import auto as timer +import multimer FG, BG, ARM = 0xFFFF, 0x0000, 12 W, H = display_drv.width, display_drv.height @@ -66,13 +66,13 @@ def _on_up(e): % (gx, gy, tx, ty, gx - tx, gy - ty) ) idx += 1 - timer.sleep_ms(400) + multimer.sleep_ms(400) if idx >= len(TARGETS): _out("done %s" % (results,)) display_drv.fill_rect(0, 0, W, H, BG) else: _show() - timer.sleep_ms(400) + multimer.sleep_ms(400) finally: _busy = False diff --git a/lib/examples/audiolive_rack.py b/lib/examples/audiolive_rack.py index ffaa12f0..99a80a4e 100644 --- a/lib/examples/audiolive_rack.py +++ b/lib/examples/audiolive_rack.py @@ -379,6 +379,6 @@ def _on_quit(_e=None): app.on(app.events.QUIT, _on_quit) _draw_tick() -app.every(_audio_tick, period=AUDIO_MS, async_=app.timer_async) -app.every(_draw_tick, period=DRAW_MS, async_=app.timer_async) -app.every(_next_patch, period=PATCH_MS, async_=app.timer_async) +app.every(_audio_tick, period=AUDIO_MS) +app.every(_draw_tick, period=DRAW_MS) +app.every(_next_patch, period=PATCH_MS) diff --git a/lib/examples/bmp565_scroll.py b/lib/examples/bmp565_scroll.py index bd34750d..3e42d65e 100644 --- a/lib/examples/bmp565_scroll.py +++ b/lib/examples/bmp565_scroll.py @@ -43,7 +43,7 @@ def _tick(_=None): draw_bg(0, i % display_drv.height, 0, i % image.height) st["i"] = i + 1 - app.every(_tick, period=1, async_=app.timer_async) + app.every(_tick, period=1) main() \ No newline at end of file diff --git a/lib/examples/bmp565_scroll_sprite.py b/lib/examples/bmp565_scroll_sprite.py index 8247da40..7fbc0052 100644 --- a/lib/examples/bmp565_scroll_sprite.py +++ b/lib/examples/bmp565_scroll_sprite.py @@ -118,7 +118,7 @@ def _tick(_=None): app.on(app.events.MOUSEMOTION, _on_motion) # ~20 fps once scrolling; first columns also tick so quit is always serviced. - app.every(_tick, period=50, async_=app.timer_async) + app.every(_tick, period=50) main() \ No newline at end of file diff --git a/lib/examples/bmp565_sprite.py b/lib/examples/bmp565_sprite.py index 95eefa83..3f8adfa0 100644 --- a/lib/examples/bmp565_sprite.py +++ b/lib/examples/bmp565_sprite.py @@ -103,4 +103,4 @@ def _tick(_=None): st["pos_i"] = (st["pos_i"] + 1) % len(positions) -app.every(_tick, period=100, async_=app.timer_async) \ No newline at end of file +app.every(_tick, period=100) \ No newline at end of file diff --git a/lib/examples/bmp565_sprite_transparent.py b/lib/examples/bmp565_sprite_transparent.py index 44d4c204..5f4e8416 100644 --- a/lib/examples/bmp565_sprite_transparent.py +++ b/lib/examples/bmp565_sprite_transparent.py @@ -102,4 +102,4 @@ def _tick(_=None): st["pos_i"] = (st["pos_i"] + 1) % len(positions) -app.every(_tick, period=100, async_=app.timer_async) \ No newline at end of file +app.every(_tick, period=100) \ No newline at end of file diff --git a/lib/examples/bouncing_balls.py b/lib/examples/bouncing_balls.py index 31fa5397..835c2e16 100644 --- a/lib/examples/bouncing_balls.py +++ b/lib/examples/bouncing_balls.py @@ -103,4 +103,4 @@ def _tick(_=None): pygraphics.circle(display_drv, int(ball.x), int(ball.y), ball.r, ball.color, True) -app.every(_tick, period=10, async_=app.timer_async) \ No newline at end of file +app.every(_tick, period=10) \ No newline at end of file diff --git a/lib/examples/boxlines.py b/lib/examples/boxlines.py index 7bfdeb7f..92722078 100644 --- a/lib/examples/boxlines.py +++ b/lib/examples/boxlines.py @@ -82,4 +82,4 @@ def _tick(_=None): poll() -app.every(_tick, period=1, async_=app.timer_async) \ No newline at end of file +app.every(_tick, period=1) \ No newline at end of file diff --git a/lib/examples/calc_graphics.py b/lib/examples/calc_graphics.py index d12a690d..a4ad3ec8 100644 --- a/lib/examples/calc_graphics.py +++ b/lib/examples/calc_graphics.py @@ -25,7 +25,7 @@ from calc_engine import CalcEngine import keys from pygraphics import RGB565, FrameBuffer -from multimer import auto as timer +import multimer from palettes import get_palette from appdev import TouchGrid @@ -122,7 +122,7 @@ def __init__(self): # have no grid cell so the touch mapping never indexes them. keypad_keys = [None] * self.COLS + list(_CODES) + list(_KEY_ALIASES.keys()) self._pending_release = None - self._release_timer = timer.Timer(-1) + self._release_timer = multimer.Timer(-1) self.keypad = TouchGrid( app, 0, @@ -264,7 +264,7 @@ def _on_press(self, code): self._handle_press(col, row, label) self._pending_release = (col, row, label) self._release_timer.init( - mode=timer.Timer.ONE_SHOT, period=150, callback=self._release + mode=multimer.Timer.ONE_SHOT, period=150, callback=self._release ) return # Keyboard alias / digit without a grid cell — feed the engine only. diff --git a/lib/examples/car_cluster/car_cluster.py b/lib/examples/car_cluster/car_cluster.py index d6e9e179..85d4501d 100644 --- a/lib/examples/car_cluster/car_cluster.py +++ b/lib/examples/car_cluster/car_cluster.py @@ -143,7 +143,7 @@ def _drain_rails(_t): if _ui is not None and _ui.rails is not None: _ui.rails.drain_pending() - app.on_tick(_drain_rails, period=30, async_=False) + app.on_tick(_drain_rails, period=30) _ui.rails.drain_pending() if _FREEZE: freeze(True) diff --git a/lib/examples/color_test.py b/lib/examples/color_test.py index 761d13cc..a2d0f44f 100644 --- a/lib/examples/color_test.py +++ b/lib/examples/color_test.py @@ -99,5 +99,5 @@ def poll(): def _tick(_=None): poll() - app.every(_tick, period=1000, async_=app.timer_async) + app.every(_tick, period=1000) main() \ No newline at end of file diff --git a/lib/examples/dino.py b/lib/examples/dino.py index fc97720d..bad781f2 100644 --- a/lib/examples/dino.py +++ b/lib/examples/dino.py @@ -545,5 +545,5 @@ def _tick(timer=None): game.tick(timer) -_tick_subscription = app.every(_tick, period=FRAME_MS, async_=app.timer_async) +_tick_subscription = app.every(_tick, period=FRAME_MS) game.draw() \ No newline at end of file diff --git a/lib/examples/displaysys_block_test.py b/lib/examples/displaysys_block_test.py index 49207932..0ae5cbb1 100644 --- a/lib/examples/displaysys_block_test.py +++ b/lib/examples/displaysys_block_test.py @@ -80,4 +80,4 @@ def _tick(_=None): app.request_quit() -app.every(_tick, period=1, async_=app.timer_async) \ No newline at end of file +app.every(_tick, period=1) \ No newline at end of file diff --git a/lib/examples/displaysys_fill_rect_test.py b/lib/examples/displaysys_fill_rect_test.py index 0ea0f814..fb124864 100644 --- a/lib/examples/displaysys_fill_rect_test.py +++ b/lib/examples/displaysys_fill_rect_test.py @@ -65,4 +65,4 @@ def _tick(_=None): app.request_quit() -app.every(_tick, period=1, async_=app.timer_async) \ No newline at end of file +app.every(_tick, period=1) \ No newline at end of file diff --git a/lib/examples/feathers.py b/lib/examples/feathers.py index 6851b297..9494b6fd 100644 --- a/lib/examples/feathers.py +++ b/lib/examples/feathers.py @@ -123,4 +123,4 @@ def _tick(_=None): poll() -app.every(_tick, period=1, async_=app.timer_async) \ No newline at end of file +app.every(_tick, period=1) \ No newline at end of file diff --git a/lib/examples/font_simpletest.py b/lib/examples/font_simpletest.py index 4e0a5f01..d9612b12 100644 --- a/lib/examples/font_simpletest.py +++ b/lib/examples/font_simpletest.py @@ -139,7 +139,7 @@ def main(): def _tick(_=None): poll() - app.every(_tick, period=1, async_=app.timer_async) + app.every(_tick, period=1) main() \ No newline at end of file diff --git a/lib/examples/fonts.py b/lib/examples/fonts.py index ff4f21a7..16e227e0 100644 --- a/lib/examples/fonts.py +++ b/lib/examples/fonts.py @@ -105,5 +105,5 @@ def main(): def _tick(_=None): poll() - app.every(_tick, period=1, async_=app.timer_async) + app.every(_tick, period=1) main() \ No newline at end of file diff --git a/lib/examples/google_photos/google_photos.py b/lib/examples/google_photos/google_photos.py index 1cf6da75..9d2152c6 100644 --- a/lib/examples/google_photos/google_photos.py +++ b/lib/examples/google_photos/google_photos.py @@ -115,7 +115,7 @@ def _block_if_batch(): path, MCU REPL, PyScript, the example kit) keeps the app alive by itself. """ try: - from appdev import _hostloop + from multimer import _hostloop from display_driver import app if not _launched_with_m(_hostloop) or _hostloop.ambient(): @@ -124,19 +124,16 @@ def _block_if_batch(): return except Exception: return - if app.strategy != _hostloop.AMBIENT or app.timer_async: - app.run() # "none": blocking service loop (async timers: asyncio.run) + if app.strategy != _hostloop.AMBIENT: + app.run() # "none": blocking service loop return # "ambient" was chosen at import time; the timer already drives the app, - # so just hold the main thread until the window closes. Hold it with the - # timer backend's own sleep, never time.sleep: on Windows the timer fires - # only while this thread is in an alertable wait (SleepEx), and a plain - # time.sleep starves it -- the window then never repaints and Windows - # marks it "Not Responding". - from multimer import auto as timer + # so just hold the main thread until the window closes, delivering on + # the way (multimer.sleep_ms serves the timers on every host). + import multimer while not app.quit_requested: - timer.sleep_ms(50) + multimer.sleep_ms(50) def _launched_with_m(_hostloop): diff --git a/lib/examples/hello.py b/lib/examples/hello.py index 8308286c..5c90da97 100644 --- a/lib/examples/hello.py +++ b/lib/examples/hello.py @@ -99,5 +99,5 @@ def main(): def _tick(_=None): poll() - app.every(_tick, period=1, async_=app.timer_async) + app.every(_tick, period=1) main() \ No newline at end of file diff --git a/lib/examples/lv_test_timer.py b/lib/examples/lv_test_timer.py index 4ec67a80..a5dd8405 100644 --- a/lib/examples/lv_test_timer.py +++ b/lib/examples/lv_test_timer.py @@ -38,7 +38,7 @@ from board_config import display_drv from displaydev import env_get -from multimer import auto as timer +import multimer # Optional logical orientation for LVGL (hw MADCTL/SDL/PG or software rotate). _lv_rot = env_get("PYDEVICES_LV_ROTATION") @@ -61,7 +61,7 @@ def _mode_label(): - return "async" if getattr(app, "timer_async", False) else "sync" + return multimer.info().get("source") or "sync" def get_state(): @@ -124,18 +124,9 @@ def _lvgl_label(): def _timer_type(): - # Deliberately not named ``timer``: that would shadow the module-level - # ``multimer.auto`` import this function falls back to, and the fallback - # then reads None on any provider that has not armed yet. - armed = getattr(app, "_timer", None) - if armed is not None: - return _format_timer_type(type(armed)) - try: - from multimer import AsyncTimer - - return _format_timer_type(AsyncTimer if app.timer_async else timer.Timer) - except ImportError: - return "?" + # multimer has one Timer class; what differs per host is the wake source. + info = multimer.info() + return "%s/%s" % (info.get("source"), info.get("delivery")) def get_platform_info(): @@ -285,9 +276,8 @@ def mock_read(): try: deadline = time.time() + 1.5 while (pending or get_state()["taps"] < 1) and time.time() < deadline: - # Pump: the host queue is drained from the app tick, which - # pump-based backends only deliver while the main thread sleeps here. - timer.sleep_ms(10) + # multimer.sleep_ms delivers on every host while this thread waits. + multimer.sleep_ms(10) finally: queue_dev._read = orig_read return get_state()["taps"] @@ -364,10 +354,9 @@ def _run_kit_sync(): deadline = time.time() + _DURATION_S clicked_taps = None while time.time() < deadline: - # timer.sleep_ms, not time.sleep: pump-based backends (threading on - # CircuitPython / Windows CPython, SDL2) deliver callbacks only - # while the main thread pumps. For librt this resolves to a plain sleep. - timer.sleep_ms(10) + # multimer.sleep_ms, not time.sleep: a host with no wake source + # (CircuitPython) delivers only while this thread waits here. + multimer.sleep_ms(10) if clicked_taps is None and get_state()["seconds"] >= 2: cx, cy = _button_center(btn) clicked_taps = _inject_click(cx, cy) @@ -405,7 +394,7 @@ def run_kit(): ``app.poll()`` while LVGL owns the host queue. """ try: - if app.timer_async: + if multimer.loop_running(): payload = app.run_async(_run_kit_async) if payload is not None and hasattr(payload, "done"): _quit_and_exit(1) diff --git a/lib/examples/palettes_demo.py b/lib/examples/palettes_demo.py index 3c6628d0..da0af2c2 100644 --- a/lib/examples/palettes_demo.py +++ b/lib/examples/palettes_demo.py @@ -149,4 +149,4 @@ def _tick(_=None): poll() -app.every(_tick, period=1, async_=app.timer_async) \ No newline at end of file +app.every(_tick, period=1) \ No newline at end of file diff --git a/lib/examples/pixel_sim_demos.py b/lib/examples/pixel_sim_demos.py index 89ae26df..e4795885 100644 --- a/lib/examples/pixel_sim_demos.py +++ b/lib/examples/pixel_sim_demos.py @@ -443,4 +443,4 @@ def _tick(_=None): _next_at = now -app.every(_tick, period=1, async_=app.timer_async) \ No newline at end of file +app.every(_tick, period=1) \ No newline at end of file diff --git a/lib/examples/proverbs/proverbs.py b/lib/examples/proverbs/proverbs.py index 0bf8eb0c..adcdbdc7 100644 --- a/lib/examples/proverbs/proverbs.py +++ b/lib/examples/proverbs/proverbs.py @@ -108,7 +108,7 @@ def _tick(_=None): st["index"] = (st["index"] + 1) % len(proverbs) - app.every(_tick, period=5000, async_=app.timer_async) + app.every(_tick, period=5000) main() \ No newline at end of file diff --git a/lib/examples/pydevices_demo.py b/lib/examples/pydevices_demo.py index a37a9853..3cf35d23 100644 --- a/lib/examples/pydevices_demo.py +++ b/lib/examples/pydevices_demo.py @@ -152,5 +152,5 @@ def _on_click(e): setup_scroll() redraw() -app.every(_scroll_tick, period=40, async_=app.timer_async) +app.every(_scroll_tick, period=40) app.on(app.events.MOUSEBUTTONDOWN, _on_click) \ No newline at end of file diff --git a/lib/examples/roku_remote/roku_graphics.py b/lib/examples/roku_remote/roku_graphics.py index 531c3b59..74f9f23a 100644 --- a/lib/examples/roku_remote/roku_graphics.py +++ b/lib/examples/roku_remote/roku_graphics.py @@ -43,7 +43,7 @@ app = appdev.App(board_config) import keys from pygraphics import RGB565, Area, FrameBuffer -from multimer import auto as timer +import multimer from roku_engine import ( FRONTEND_BUTTONS, app_label, @@ -223,7 +223,7 @@ def __init__(self, engine=None, start_page="devices"): self.buttons = [] self._by_id = {} - self._status_timer = timer.Timer(-1) + self._status_timer = multimer.Timer(-1) self._pending_status = None self._pending_state = None self._playback_busy = False @@ -311,7 +311,7 @@ def _soft(): # Must not call Timer.deinit from this callback (librt deadlock). try: self._status_timer.init( - mode=timer.Timer.PERIODIC, + mode=multimer.Timer.PERIODIC, period=250, callback=self._status_pump, hard=False, diff --git a/lib/examples/roku_remote/roku_widgets.py b/lib/examples/roku_remote/roku_widgets.py index 2de16c39..035907fb 100644 --- a/lib/examples/roku_remote/roku_widgets.py +++ b/lib/examples/roku_remote/roku_widgets.py @@ -297,7 +297,6 @@ def __init__(self, engine=None, start_page="devices"): self._pump_sub = self.app.every( self._pump, period=250, - async_=getattr(self.app, "timer_async", False), ) except Exception: self._pump_sub = None diff --git a/lib/examples/rotations.py b/lib/examples/rotations.py index c3b326e0..5be70617 100644 --- a/lib/examples/rotations.py +++ b/lib/examples/rotations.py @@ -77,7 +77,7 @@ def poll(): def _tick(_=None): poll() - app.every(_tick, period=2000, async_=app.timer_async) + app.every(_tick, period=2000) main() \ No newline at end of file diff --git a/lib/examples/scroll.py b/lib/examples/scroll.py index a0556782..840bb2e7 100644 --- a/lib/examples/scroll.py +++ b/lib/examples/scroll.py @@ -80,4 +80,4 @@ def _tick(_=None): poll() -app.every(_tick, period=10, async_=app.timer_async) \ No newline at end of file +app.every(_tick, period=10) \ No newline at end of file diff --git a/lib/examples/scroll_touch_test.py b/lib/examples/scroll_touch_test.py index 073be470..e0df4430 100644 --- a/lib/examples/scroll_touch_test.py +++ b/lib/examples/scroll_touch_test.py @@ -104,4 +104,4 @@ def _on_mode_tick(_=None): _rebuild() app.on(app.events.MOUSEBUTTONDOWN, _on_click) -app.every(_on_mode_tick, period=100, async_=app.timer_async) \ No newline at end of file +app.every(_on_mode_tick, period=100) \ No newline at end of file diff --git a/lib/examples/simon.py b/lib/examples/simon.py index 29d8fc57..23b18e02 100644 --- a/lib/examples/simon.py +++ b/lib/examples/simon.py @@ -314,4 +314,4 @@ def _on_tick(_=None): draw_board() app.on(events.MOUSEBUTTONUP, _on_up) -app.every(_on_tick, period=20, async_=getattr(app, "timer_async", False)) \ No newline at end of file +app.every(_on_tick, period=20) \ No newline at end of file diff --git a/lib/examples/testris.py b/lib/examples/testris.py index ca92e9b6..7ad2ba4f 100644 --- a/lib/examples/testris.py +++ b/lib/examples/testris.py @@ -756,4 +756,4 @@ def _tick(_=None): app.request_quit() -app.every(_tick, period=20, async_=app.timer_async) \ No newline at end of file +app.every(_tick, period=20) \ No newline at end of file diff --git a/lib/examples/tiny_hello.py b/lib/examples/tiny_hello.py index 7e268303..9dcdac3e 100644 --- a/lib/examples/tiny_hello.py +++ b/lib/examples/tiny_hello.py @@ -140,5 +140,5 @@ def main(): def _tick(_=None): poll() - app.every(_tick, period=1, async_=app.timer_async) + app.every(_tick, period=1) main() \ No newline at end of file diff --git a/lib/examples/tiny_toasters/tiny_toasters.py b/lib/examples/tiny_toasters/tiny_toasters.py index de99eb7f..8f69e106 100644 --- a/lib/examples/tiny_toasters/tiny_toasters.py +++ b/lib/examples/tiny_toasters/tiny_toasters.py @@ -228,5 +228,5 @@ def _tick(_=None): gc.collect() - app.every(_tick, period=50, async_=app.timer_async) + app.every(_tick, period=50) main() \ No newline at end of file diff --git a/lib/examples/tower_climb/tower_climb.py b/lib/examples/tower_climb/tower_climb.py index 9a6179e5..fed457c3 100644 --- a/lib/examples/tower_climb/tower_climb.py +++ b/lib/examples/tower_climb/tower_climb.py @@ -1230,7 +1230,7 @@ def main(): else: _start_round() - app.every(_tick, period=16, async_=app.timer_async) + app.every(_tick, period=16) main() \ No newline at end of file diff --git a/lib/examples/widgets_energy_panel.py b/lib/examples/widgets_energy_panel.py index 97d5f335..1b168012 100644 --- a/lib/examples/widgets_energy_panel.py +++ b/lib/examples/widgets_energy_panel.py @@ -255,6 +255,6 @@ def _on_spike(_s=None, _e=None): seg.set_change_cb(_on_seg) spike_btn.add_event_cb(pd.events.MOUSEBUTTONDOWN, _on_spike) -app.every(_live_tick, period=500, async_=app.timer_async) +app.every(_live_tick, period=500) screen.visible = True \ No newline at end of file diff --git a/lib/utils/console.py b/lib/utils/console.py index 9a819d1e..55ed7f55 100644 --- a/lib/utils/console.py +++ b/lib/utils/console.py @@ -664,7 +664,7 @@ def _ensure_timer(self): self._subscription = self._app.every(self._refresh_ms, self._tick) return try: - from multimer import auto as multimer + import multimer except ImportError: return if not multimer.Timer: diff --git a/lib/utils/tft_config.py b/lib/utils/tft_config.py index 3319d6d7..cb1ddc5d 100644 --- a/lib/utils/tft_config.py +++ b/lib/utils/tft_config.py @@ -19,7 +19,7 @@ if BUFFERED: from displaybuf import DisplayBuffer - from multimer import auto as timer + import multimer font_dir = sep.join(tft_text.__file__.split(sep)[:-1]) + sep + "fonts" @@ -61,9 +61,9 @@ def show(area=None): display.show = show display.draw = display - tim = timer.Timer() + tim = multimer.Timer() tim.init( - mode=timer.Timer.PERIODIC, + mode=multimer.Timer.PERIODIC, period=33, callback=lambda t: display.show(), ) diff --git a/tools/README.md b/tools/README.md index 1c7f9f6a..76e9dceb 100644 --- a/tools/README.md +++ b/tools/README.md @@ -68,96 +68,45 @@ concrete launcher used in automation. .venv/bin/python -m unittest discover -s tests ``` -### Preferred method (parallel interpreters, fail-fast, both timer modes) +### Preferred method (parallel interpreters, fail-fast) -For thorough verification (timer/multimer/interpreter changes, or “run the full -matrix”), prefer **example-by-example**, **all selected interpreters in parallel** -per example (`--jobs 0`, default), **`--fail-fast`**, and **both** -`PYDEVICES_TIMER_ASYNC` modes as separate kit runs. +For thorough verification (timer/multimer/interpreter changes, or "run the full +matrix"), prefer **example-by-example**, **all selected interpreters in parallel** +per example (`--jobs 0`, default) and **`--fail-fast`**. There is one timer +mode: multimer delivers on every host by itself, so the old sync/async split +(`PYDEVICES_TIMER_ASYNC`) is gone; the kit still accepts the variable and +ignores it. -| Mode | Interpreters | -|------|----------| -| Sync (`PYDEVICES_TIMER_ASYNC=0`) | **5** desktop SDL: `micropython`, `micropython.exe`, `circuitpython`, `cpython-venv`, `python.exe` | -| Async (`PYDEVICES_TIMER_ASYNC=1`) | **7** — the five above plus `pyscript`, `jupyter` | -| Android (opt-in) | `android` — `pydevices/bin/android.py` (or `~/bin/android.py` on PATH) + emulator/device + `org.pydevices.runner` APK; **not** in the default 5/7 lists (`--only-interpreter android`) | +| Interpreters | +|----------| +| **7** — desktop SDL: `micropython`, `micropython.exe`, `circuitpython`, `cpython-venv`, `python.exe`; plus `pyscript`, `jupyter` | +| Android (opt-in) | `android` — `pydevices/bin/android.py` (or `~/bin/android.py` on PATH) + emulator/device + `org.pydevices.runner` APK; **not** in the default list (`--only-interpreter android`) | Default timing is already short (`duration_s=2`, `timeout_s=15` in the -interpreters/manifest defaults). After each example’s parallel wave finishes, if +interpreters/manifest defaults). After each example's parallel wave finishes, if any cell failed, stop before the next example; fix the root cause, then resume. ```bash -# PyScript needs the static server (async mode) +# PyScript needs the static server python tools/serve.py # separate terminal; reuse if already on :8000 export SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy PYTHONUNBUFFERED=1 mkdir -p /tmp/pydevices-examples-matrix -SYNC_RT="micropython micropython.exe circuitpython cpython-venv python.exe" -ASYNC_RT="$SYNC_RT pyscript jupyter" +RT="micropython micropython.exe circuitpython cpython-venv python.exe pyscript jupyter" set -o pipefail # keep kit exit status through tee -# Sync — 5 interpreters concurrently per example -PYDEVICES_TIMER_ASYNC=0 stdbuf -oL -eL \ +stdbuf -oL -eL \ .venv/bin/python tools/example_test_kit.py --no-unit-tests --fail-fast \ - --only-interpreter $SYNC_RT \ - --results-json /tmp/pydevices-examples-matrix/sync.json \ - 2>&1 | stdbuf -oL -eL tee /tmp/pydevices-examples-matrix/sync.log - -# After sync is clean — async, all 7 -PYDEVICES_TIMER_ASYNC=1 stdbuf -oL -eL \ - .venv/bin/python tools/example_test_kit.py --no-unit-tests --fail-fast \ - --only-interpreter $ASYNC_RT \ - --results-json /tmp/pydevices-examples-matrix/async.json \ - 2>&1 | stdbuf -oL -eL tee /tmp/pydevices-examples-matrix/async.log -``` - -Live log lines: `Running @ N interpreter(s) in parallel...`, then -`start` / `done` per interpreter. `--fail-fast` waits for the current example’s -workers, then exits if any cell failed. Resume with `--only-example` -(remaining ids) or by restarting that mode from the failed example. Use -`--jobs 1` for fully serial interpreters when isolating races. See -[Windows PE under WSL](#windows-pe-under-wsl) for PE window / quit notes. - -`--curated-only` is a smoke shortcut, not a substitute for the preferred gate. - -### Matrix commands (scoped / smoke) - -```bash -# Curated set across available interpreters (smoke) -.venv/bin/python tools/example_test_kit.py --curated-only - -# Scope (space-separated ids on one flag; see note below) -.venv/bin/python tools/example_test_kit.py --only-example calculator --only-interpreter micropython -.venv/bin/python tools/example_test_kit.py --no-unit-tests --only-interpreter cpython-venv micropython -.venv/bin/python tools/example_test_kit.py --no-unit-tests \ - --only-example calc_lvgl lv_test_timer --only-interpreter circuitpython - -# Order: --order examples (default) / --order interpreters -# Broader: --all-except-harness -``` - -`--only-example` and `--only-interpreter` use `nargs="+"`: pass multiple ids -space-separated after **one** occurrence of the flag. Repeating the flag -silently keeps only the last list (`--only-interpreter circuitpython --only-interpreter -python.exe` runs just `python.exe`). Same rule for `lv_timer_test_kit.py` -`--only` / `--modes`. - -**Headless desktop** (dummy SDL — default for matrix/smoke): - -```bash -SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy \ - .venv/bin/python tools/example_test_kit.py --no-unit-tests --only-interpreter cpython-venv + --only-interpreter $RT \ + --results-json /tmp/pydevices-examples-matrix/all.json \ + 2>&1 | stdbuf -oL -eL tee /tmp/pydevices-examples-matrix/all.log ``` Unix subprocesses see that shell export. Windows `.exe` behavior is different — see [Windows PE under WSL](#windows-pe-under-wsl). -**Async timers on desktop:** the kit forwards `PYDEVICES_TIMER_ASYNC` as wrapper -`--timer-async` (uses `env_set`, works for Windows PE under WSL). Shell export -is the preferred way to select mode for a full kit run (see Preferred method -above). Semantics: [App and board config — timer_async](https://github.com/PyDevices/pydevices/blob/main/docs/app-and-board-config.md#timer_async-in-srclibboard_configpy). - ### Windows PE under WSL `micropython.exe` and `python.exe` are Windows PE binaries launched from WSL. diff --git a/tools/example_test_kit.py b/tools/example_test_kit.py index b96a9fa3..2d81e16b 100755 --- a/tools/example_test_kit.py +++ b/tools/example_test_kit.py @@ -474,6 +474,8 @@ def run_subprocess_case( apply_sibling_env(env, repo_root=str(REPO)) _ensure_user_micropy_lib(env) # Windows PE under WSL cannot read Linux-exported env; pass via argv + env_set. + # PYDEVICES_TIMER_ASYNC is retired (multimer has one timer model); it is + # still forwarded so old matrix invocations run, and the wrapper ignores it. timer_async = env.get("PYDEVICES_TIMER_ASYNC") if timer_async is not None: cmd.extend(["--timer-async", str(timer_async)]) diff --git a/tools/example_test_manifest.toml b/tools/example_test_manifest.toml index 2adaab1c..44735ee0 100644 --- a/tools/example_test_manifest.toml +++ b/tools/example_test_manifest.toml @@ -4,8 +4,8 @@ # kind: oneshot | loop | async | lvgl | pdwidgets | harness | legacy # quit: native_exit | poll | inject | pending | interactive # bootstrap: full (default) | headless — headless skips utils.path / display init -# Timer mode for kit runs: host PYDEVICES_TIMER_ASYNC is forwarded as wrapper -# --timer-async (board_config / displaydev.env_set). Examples do not read env. +# There is one timer mode: multimer delivers on every host by itself, so the +# old PYDEVICES_TIMER_ASYNC sync/async split is gone (it is still accepted). [defaults] duration_s = 2 @@ -146,7 +146,6 @@ deps = ["utils", "tft_config"] # --- LVGL timer smoke test (matrix) --- # Also covered by tools/lv_timer_test_kit.py (``lv_test_timer.py kit``). -# Follows app.timer_async; does not set PYDEVICES_TIMER_ASYNC itself. [examples.lv_test_timer] import = "lv_test_timer" diff --git a/tools/example_test_wrapper.py b/tools/example_test_wrapper.py index f1e0fec7..de943f5f 100755 --- a/tools/example_test_wrapper.py +++ b/tools/example_test_wrapper.py @@ -699,8 +699,8 @@ def main(argv=None): key, value = item.split("=", 1) if key: env_set(key, value) - if args.get("timer_async") is not None: - env_set("PYDEVICES_TIMER_ASYNC", args["timer_async"]) + # --timer-async is accepted and ignored: multimer has one timer model + # on every host, and board configs no longer export timer_async. # Install the deadline hook AFTER bootstrap: it imports multimer, which is # only on sys.path once _setup_bootstrap has run. The hook drives quit for diff --git a/tools/lv_timer_test_kit.py b/tools/lv_timer_test_kit.py index 3105c163..0b988233 100755 --- a/tools/lv_timer_test_kit.py +++ b/tools/lv_timer_test_kit.py @@ -6,14 +6,13 @@ checks, then an injected ``events.Quit``). Each child prints ``KIT_RESULT=`` on stdout; exit code 0 is expected on success. -The example follows ``app.timer_async`` (it does not set env vars). This kit -sets ``PYDEVICES_TIMER_ASYNC`` in the child environment so ``board_config`` -constructs sync or async timers; modes are ``sync`` and ``async``. +multimer has one timer model on every host now, so the kit runs one mode, +``sync`` (the name is kept so old result files still line up). Where a host +owns an asyncio loop, multimer rides it by itself. From repo root: python tools/lv_timer_test_kit.py python tools/lv_timer_test_kit.py --only cpython-venv - python tools/lv_timer_test_kit.py --only cpython-venv --modes async python tools/lv_timer_test_kit.py --backend sdl2 ``--backend`` forces one multimer backend through @@ -67,7 +66,7 @@ def _temp_dir() -> Path: # Back-compat alias for docs/CLI that used ``cpython``. INTERPRETER_ALIASES = {"cpython": "cpython-venv"} -MODES = ("sync", "async") +MODES = ("sync",) sys.path.insert(0, str(TOOLS)) from example_test_kit import load_interpreters, resolve_interpreter_exe # noqa: E402 diff --git a/tools/prove_hostloop/_app.py b/tools/prove_hostloop/_app.py index e068c226..7f8fb178 100644 --- a/tools/prove_hostloop/_app.py +++ b/tools/prove_hostloop/_app.py @@ -23,9 +23,9 @@ def quit(self): print("[app] display released") -def make_app(timer_async=False): - app = App(displays=[Display()], timer_async=timer_async, refresh_period=0) - print("[app] strategy:", app.strategy, "async:", app.timer_async) +def make_app(): + app = App(displays=[Display()], refresh_period=0) + print("[app] strategy:", app.strategy) ticks = [] @app.every(TICK_MS) diff --git a/tools/prove_hostloop/demo_async.py b/tools/prove_hostloop/demo_async.py deleted file mode 100644 index 9f40e0b7..00000000 --- a/tools/prove_hostloop/demo_async.py +++ /dev/null @@ -1,6 +0,0 @@ -"""Async timers, no app.run(). Needs hostloop's drive path.""" - -from _app import make_app - -app = make_app(timer_async=True) -print("-- script body ends here --") diff --git a/tools/prove_hostloop/prove.sh b/tools/prove_hostloop/prove.sh index 03262790..3cb059c7 100755 --- a/tools/prove_hostloop/prove.sh +++ b/tools/prove_hostloop/prove.sh @@ -21,7 +21,7 @@ run() { # run