Skip to content

Runtime and board_config

Every pydisplay app expects a board_config.py on sys.path that exports:

Symbol Required Role
display_drv yes Display backend from displaysys
runtime when display_drv.needs_refresh eventsys Runtime — shared timer, optional input, quit lifecycle

runtime = None is allowed only on MCU boards whose display does not need periodic presentation (needs_refresh is false): bus displays, e-paper, pixel grids driven explicitly by the app. Hosted backends (SDL, pygame, PyScript, Jupyter) always export a Runtime.

Quick start — hosted desktop

from sdldisplay import SDLDisplay, get_events
import eventsys

display_drv = SDLDisplay(width=320, height=480, rotation=0, scale=2.0, title="My app")

runtime = eventsys.Runtime(
    display=display_drv,
    host_read=get_events,
)

The runtime wires periodic display_drv.show() automatically when display_drv.needs_refresh is true (~30 FPS by default). No app code calls on_tick for refresh.

Quick start — MCU with touch

import eventsys
from machine import I2C, Pin
from ft6x36 import FT6x36
from st7796 import ST7796

# ... bus, display_drv, touch setup ...

runtime = eventsys.Runtime(
    display=display_drv,
    touch_read=touch.read_points,
)

Apps discover touch through runtime.touch_dev, not by importing the driver from board_config. See Board devices.

Quick start — display-only MCU

import eventsys
from st7789 import ST7789

display_drv = ST7789(...)  # bus display; needs_refresh is False

runtime = None

App loop

from board_config import display_drv, runtime

while runtime is None or not runtime.quit_requested:
    if runtime is not None:
        for event in runtime.poll():
            handle(event)
    draw_frame()

Output-only demos that never read input still need runtime on hosted boards (for auto-refresh and window quit):

while not runtime.quit_requested:
    draw_frame()
    runtime.poll()

Runtime constructor

eventsys.Runtime(
    display=None,              # duck-typed: show(), quit(), optional needs_refresh
    host_read=None,            # hosted event pump (SDL/pygame/PyScript/Jupyter)
    touch_read=None,           # callable returning touch point(s) or falsy
    touch_rotation_table=None, # optional 4-item rotation mask table
    refresh_period=None,       # ms; None = use DEFAULT_REFRESH_MS when needs_refresh
    timer_async=False,         # True for PyScript / Jupyter; desktop default in lib/board_config
)

timer_async in src/lib/board_config.py

The shipped default config sets timer_async per host:

Host Value
PyScript True
Jupyter True
PG/SDL desktop False, or env_bool("PYDISPLAY_TIMER_ASYNC", False)

Library / host only — examples do not read PYDISPLAY_TIMER_ASYNC. Set it in the process environment before board_config is imported on desktop hosts that have getenv, or prefer test-kit / matrix --timer-async (wrapper env_set) so Windows PE under WSL works. MCU board configs and PyScript / Jupyter use their platform defaults without shell env. See displaysys.env_bool and Board configs — default.

On SDL2 / Win32 sync timer hosts (micropython.exe, and similar), display refresh is deferred until the first runtime.poll() so importing board_config in a REPL does not start an SDL timer without a drain loop.

Additional inputs after construction:

runtime.add_keypad(read=buttons.read)
runtime.add_joystick(joystick_driver=drv)
runtime.add_encoder(read=pos_read, button_read=btn_read, button=2)

Bare Runtime() with no arguments is valid for tests and custom wiring via register().

Touch read contract

Pass a callable as touch_read= (typically touch.read_points). Each poll, the runtime calls it once. TouchDevice maps the primary contact to MOUSE* events and exposes all rotated contacts as runtime.touch_dev.points (for LVGL gestures and other multipoint consumers).

Return value Meaning
falsy (None, (), []) no touch — emits MOUSEBUTTONUP if a press was active
sequence of (x, y[, id[, …]]) contacts; empty sequence means up
legacy bare (x, y[, …]) with int first element treated as a single point (compat)

Prefer the sequence shape from read_points() — never return a bare (x, y) from that method when documenting a new driver (it is ambiguous with “one 2-tuple point” vs “two ints”). Coordinates are in panel / pre-rotation pixel space; TouchDevice applies touch_rotation_table= (4-tuple of rotation masks, one per 90° step). See Events — touch and Board devices — touch duck-type.

Touch drivers live under drivers/touch/. OSError from touch_read is treated as no touch for that poll.

Display refresh takeover (LVGL, games)

GUI layers that present frames themselves can pause runtime-driven refresh:

claim = runtime.claim_display_refresh()
try:
    ...  # present frames yourself
finally:
    claim.release()

Or use the context manager:

with runtime.display_refresh_paused():
    run_game()

Quit lifecycle

On QUIT, the runtime runs (in order): before_quit hook (if set) → display.quit()stop_timer(). Set runtime.before_quit for LVGL shutdown before the display is released.

runtime.quit_requested becomes true after the first quit and stays true (sticky flag).

Package boundaries

  • displaysys declares needs_refresh (boolean only); no timer code.
  • eventsys owns DEFAULT_REFRESH_MS and the shared timer; duck-types display without importing displaysys.
  • board_config is the only place that names both packages together.

See also: Events, Architecture, Board configs.