App starter¶
Copy one of the scripts below to start your first pydisplay app. Each is a single file that uses only src/lib modules — no add_ons, no tft_config, no displaybuf.
| Use this | When you want… |
|---|---|
| App starter (this page) | A minimal template: draw the UI, handle clicks, run the recommended main loop |
| Dual tab | One file for desktop and PyScript (dual_main + board_config.TIMER_ASYNC) |
| pydisplay_demo | A feature tour: rotation, hardware scrolling, buffered text, timers |
Prerequisites¶
- A working board config on your path (from a full clone or MIP install).
- In a development clone, use
import lib.pathbefore importing your script solib/andexamples/are onsys.path.
Save the boilerplate as main.py (or any name you prefer) and run it from the REPL or as your device's entry point.
Boilerplate¶
Blocking main loop via multimer.run_forever(). Use on MCU, desktop CPython, and any port where your app is not asyncio-native. Tagged # multimer types: queued, sync.
# multimer types: queued, sync
"""
my_app.py — starting point for a pydisplay app.
Copy and rename to build your own project. Uses board_config, graphics,
multimer, and eventsys only.
"""
from board_config import broker, display_drv
from graphics import Area
from multimer import run_forever
# --- customize: colors and layout ---
BG = 0
BTN = 0xF800 # red
BTN_ON = 0x07E0 # green
button = None
pressed = False
def redraw():
global button
w, h = display_drv.width, display_drv.height
display_drv.fill(BG)
color = BTN_ON if pressed else BTN
button = Area(display_drv.fill_rect(w // 2 - 50, h // 2 - 25, 100, 50, color))
display_drv.show()
def handle_event(e):
global pressed
if e.type == broker.events.MOUSEBUTTONDOWN:
if button.contains(e.pos):
pressed = not pressed
redraw()
# elif e.type == broker.events.KEYDOWN:
# ...
# elif e.type == broker.events.ENCODER:
# ...
# elif e.type == broker.events.QUIT:
# return True # exit main loop if you add a break
def poll_events():
if elist := broker.poll():
for e in elist:
handle_event(e)
def main():
redraw()
run_forever(poll_events)
main()
Asyncio main loop via multimer.run_forever(). Use on PyScript or any port where the app already runs under asyncio / uasyncio. Tagged # multimer types: async.
# multimer types: async
"""
my_app_async.py — asyncio starting point for a pydisplay app.
Copy and rename to build your own project. Uses board_config, graphics,
multimer, and eventsys only.
"""
import board_config
board_config.TIMER_ASYNC = True
from board_config import broker, display_drv
from graphics import Area
from multimer import run, run_forever
# --- customize: colors and layout ---
BG = 0
BTN = 0xF800 # red
BTN_ON = 0x07E0 # green
button = None
pressed = False
def redraw():
global button
w, h = display_drv.width, display_drv.height
display_drv.fill(BG)
color = BTN_ON if pressed else BTN
button = Area(display_drv.fill_rect(w // 2 - 50, h // 2 - 25, 100, 50, color))
display_drv.show()
def handle_event(e):
global pressed
if e.type == broker.events.MOUSEBUTTONDOWN:
if button.contains(e.pos):
pressed = not pressed
redraw()
# elif e.type == broker.events.KEYDOWN:
# ...
# elif e.type == broker.events.ENCODER:
# ...
# elif e.type == broker.events.QUIT:
# return True
def poll_events():
if elist := broker.poll():
for e in elist:
handle_event(e)
async def main():
redraw()
await run_forever(poll_events, delay_ms=10)
run(main)
One entry file for desktop and PyScript. board_config.TIMER_ASYNC selects the path; dual_main() calls main_sync() or schedules main_async(). Tagged # multimer types: async (PyScript gallery convention). Pass async_=TIMER_ASYNC to periodic() when you add periodic timers.
# multimer types: async
"""
my_app_dual.py — one file for sync desktop and async PyScript.
Copy and rename to build your own project. Uses board_config, graphics,
multimer, multimer, and eventsys only.
"""
from board_config import TIMER_ASYNC, broker, display_drv
from graphics import Area
from multimer import run_forever
from multimer import dual_main, run_forever as aio_run_forever
# --- customize: colors and layout ---
BG = 0
BTN = 0xF800 # red
BTN_ON = 0x07E0 # green
button = None
pressed = False
def redraw():
global button
w, h = display_drv.width, display_drv.height
display_drv.fill(BG)
color = BTN_ON if pressed else BTN
button = Area(display_drv.fill_rect(w // 2 - 50, h // 2 - 25, 100, 50, color))
display_drv.show()
def handle_event(e):
global pressed
if e.type == broker.events.MOUSEBUTTONDOWN:
if button.contains(e.pos):
pressed = not pressed
redraw()
# elif e.type == broker.events.KEYDOWN:
# ...
# elif e.type == broker.events.ENCODER:
# ...
# elif e.type == broker.events.QUIT:
# return True
def poll_events():
if elist := broker.poll():
for e in elist:
handle_event(e)
def main_sync():
redraw()
run_forever(poll_events)
async def main_async():
redraw()
await aio_run_forever(poll_events, delay_ms=10)
dual_main(main_sync, main_async, async_mode=TIMER_ASYNC)
Hit testing and graphics.Area¶
The boilerplate imports Area from graphics only for hit-testing. display_drv.fill_rect(...) returns an (x, y, w, h) tuple; wrapping it in Area lets you write button.contains(e.pos) instead of inline coordinate math.
displaysys and eventsys do not depend on graphics. If you want a stack with no graphics import — or you install only those packages — keep the tuple from fill_rect and test clicks directly:
# displaysys + eventsys only — no graphics import
button = None # (x, y, w, h)
def redraw():
global button
w, h = display_drv.width, display_drv.height
display_drv.fill(BG)
color = BTN_ON if pressed else BTN
button = display_drv.fill_rect(w // 2 - 50, h // 2 - 25, 100, 50, color)
display_drv.show()
def hit(rect, pos):
x, y, w, h = rect
px, py = pos
return x <= px < x + w and y <= py < y + h
def handle_event(e):
global pressed
if e.type == broker.events.MOUSEBUTTONDOWN:
if hit(button, e.pos):
pressed = not pressed
redraw()
Stick with from graphics import Area when you also use rectangle helpers from graphics — union (area1 + area2), clip, inset, or dirty rects returned by graphics draw functions. See Drawing and fonts.
Run it¶
From a full clone with board_config on your path, paste the script into src/main.py (or run from the REPL):
import lib.path # adds lib/, examples/ to sys.path (dev clone only)
# import my_app # or paste/run your saved script
Desktop (SDL board config):
cd src
PYTHONPATH=../board_configs/sdldisplay:lib micropython -i lib/path.py
On MCU, install the matching board config, copy or symlink it as board_config.py, and run main.py from flash or the REPL.
Interact: tap or click the centered rectangle — it toggles between red and green.
Walkthrough¶
redraw()¶
Clears the screen, draws one clickable rectangle, and calls display_drv.show() once. fill_rect returns (x, y, w, h); the boilerplate wraps that in Area for button.contains(event.pos).
Recreate Area objects whenever you change layout (same pattern as real apps with moving widgets).
handle_event(e) and poll_events()¶
Per-event handling stays in handle_event(). The main loop calls poll_events(), which drains broker.poll() and dispatches each event. The starter handles MOUSEBUTTONDOWN only. Uncomment the stubs or add branches for keys, encoders, and quit — see Events.
Main loop¶
All three boilerplate tabs use the shared run_forever(poll) helpers instead of hand-rolled while True loops:
| Tab | Helper | Each iteration |
|---|---|---|
| Sync | multimer.run_forever(poll_events) |
pump() → poll_events() → sleep_ms(1) |
| Async | await multimer.run_forever(poll_events, delay_ms=10) |
yield to asyncio → poll_events() → short sleep |
| Dual | dual_main(main_sync, main_async, async_mode=TIMER_ASYNC) |
picks sync or async path from board config |
On backends that queue timer callbacks to the main thread, pump() inside run_forever delivers them — see multimer.
For the dual tab, board_config sets TIMER_ASYNC = True on PyScript and Jupyter; desktop MCU/SDL configs leave it False. Your app reads that flag and passes it to dual_main() — multimer does not import board_config.
To migrate between styles, compare the tabs above or see the sync/async table in pydisplay_demo → Async variant.
Customize¶
- Rename the file and module docstring; keep the multimer first-line tag accurate if you add timers later.
- Layout — add more
Arearegions, sprites, or shapes inredraw(). - Text — for labels and lists, use
Font+FrameBuffer+blit_rect(Drawing and fonts,font_simpletest.py). - Timers — call
periodic(callback, period=…, async_=TIMER_ASYNC)when you need periodic updates (use the dual tab pattern and importTIMER_ASYNCfromboard_config); see multimer and pydisplay_demo for scrolling.
Next steps beyond this template
- Rotation and hardware scroll — pydisplay_demo
- Event types and brokers — Events
- All example scripts — Examples catalog
Related docs¶
- Board configs — choose and customize
board_config.py - Architecture — how board_config, displaysys, and eventsys fit together
- pydisplay_demo — flagship feature demo (rotation, scroll, buffered text)
- Examples catalog — full list of scripts