From 4b42fbeb4b2b55648d46f6aad5b453248c8d2e7b Mon Sep 17 00:00:00 2001 From: Brad Barnett <127794626+bdbarnett@users.noreply.github.com> Date: Fri, 9 Oct 2026 09:49:09 -0500 Subject: [PATCH] run_cast_example: end a board run from the top level On a board the runner ended a timed run by calling machine.soft_reset() from inside the display's show(). show() runs from a callback there (the app's refresh timer, or an LVGL flush), and anything raised in a callback is printed and dropped. Now that display_driver's flush always ends (pydevices#158), the SystemExit no longer freezes the board but no longer ends the run either: the example kept running after its CAST line. The runner now holds the main thread once the example's script body returns, reports when SECONDS are up, and soft-resets from its own top level. An example that loops in its own body is unwound by an exception raised from show() back to the runner, which then soft-resets the same way. --- tools/run_cast_example.py | 55 ++++++++++++++++++++++++++++++--------- 1 file changed, 43 insertions(+), 12 deletions(-) diff --git a/tools/run_cast_example.py b/tools/run_cast_example.py index 7a2932368..9e079e564 100644 --- a/tools/run_cast_example.py +++ b/tools/run_cast_example.py @@ -9,8 +9,9 @@ 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. +On a board, set CFG, EXAMPLE and SECONDS below and run it with mpftp. There +the run ends after the first report: the line goes to /cast.txt and the board +soft-resets, so its REPL answers again. Any runtime: python, python.exe, micropython, micropython.exe, a board. """ @@ -51,6 +52,13 @@ def now(): _reports = [0] +# On a board: True once the example's script body has returned and this runner +# holds the main thread, so the report and the soft reset happen out here. +_held = [False] + + +class _RunOver(BaseException): + """Raised from show() to end a board run whose example loops in its own body.""" def report(): @@ -92,11 +100,6 @@ def report(): if board and _reports[0] == 1: with open("/cast.txt", "w") as f: f.write(line + "\n") - # the example, the frame server and this thread would all keep running; - # a soft reset ends every thread, so the board answers its REPL again - import machine - - machine.soft_reset() def measure_shows(): @@ -116,19 +119,28 @@ def report_loop(): report() +_due = [0] + + def report_from_show(): - """No threads (micropython.exe): report from inside the display's show().""" + """No threads (micropython.exe, a board): report from inside the display's show().""" import board_config d = board_config.display_drv show = d.show - due = [now() + seconds] def timed_show(*args, **kwargs): r = show(*args, **kwargs) - if now() >= due[0]: - due[0] += seconds + if not _held[0] and now() >= _due[0]: + _due[0] += seconds report() + if board: + # Only an example still in its own loop gets here on a board. + # Raised into that loop, this unwinds it to the runner. Raised + # from a timer or an LVGL flush, it is printed and dropped, as + # anything raised in a callback is, which is why the soft reset + # happens below and never in here. + raise _RunOver return r d.show = timed_show @@ -165,6 +177,7 @@ def timed_show(*args, **kwargs): roku_companion.ROKU_IP = target measure_shows() +_due[0] = now() + seconds if board: # an LVGL app holds a board's interpreter and starves a reporting thread report_from_show() @@ -180,4 +193,22 @@ def timed_show(*args, **kwargs): _thread.start_new_thread(report_loop, ()) except ImportError: # micropython.exe report_from_show() -__import__(example) +try: + __import__(example) +except _RunOver: + pass # the example looped in its own body; show() reported and unwound it +else: + if board: + # The example built its app and returned; the app's timers run it from + # here. Wait out the run at the top level, then report. + _held[0] = True + while now() < _due[0]: + time.sleep(0.05) + report() +if board: + # The example's timers, the frame server and the cast task would all keep + # running. A soft reset ends them, and it can only take effect out here: + # raised inside a timer or a display callback it is printed and dropped. + import machine + + machine.soft_reset()