This file provides guidance to agents when working with code in this repository.
- Build is driven by
./scripts/build_mpos.sh <target>; it mutates tracked files (patcheslvgl_micropython/lib/micropython/ports/esp32/main/idf_component.yml, appends include tomicropython-camera-API/src/manifest.py, and toggles@micropython.viperininternal_filesystem/lib/mpos/audio/stream_wav.py). Re-run builds expecting these edits to persist unless reverted. - Unix/macOS builds rely on symlinks created by
build_mpos.shinlvgl_micropython/ext_mod/forc_mposandsecp256k1-embedded-ecdhbecauseUSER_C_MODULEis unreliable on those targets. - Syntax tests run via
./tests/syntax.shand compile everyinternal_filesystem/**/*.pywithmpy-cross; failing files are reported by path. - Unit tests run via
./tests/unittest.sh [test_file] [--ondevice]; runner injectsmain.pyand disablesmpos.TaskManagerfor desktop, but on-device runs must NOT re-run boot/main (the script handles this). - Graphical tests are detected by filename containing
graphicaland run with LVGL boot/main injected; non-graphical tests run without boot files. - To run a single test, pass a file path to
./tests/unittest.sh(absolute path is resolved inside the script). - Testing workflow details and examples live in
tests/README.md; check it before adding new tests. - To install an app on a physical device:
./scripts/install.sh com.micropythonos.appname - After installing an app, call
AppManager.refresh_apps()to reload the app registry beforestart_app()can find it. - To deploy updated files to a physical device (e.g. updated
testing.py):python3 lvgl_micropython/lib/micropython/tools/mpremote/mpremote.py cp internal_filesystem/lib/mpos/ui/testing.py :/lib/mpos/ui/testing.pythenimport machine; machine.reset()and wait 30 seconds for the device to boot. - Code formatting for Python in this repo is ruff with double quotes configured in
ruff.toml(quote-style = "double").
Guidelines:
- If something is incomplete or lacks functionality that is needed to finish the task, then implement the missing functionality, rather than working around it.
- Always add a timeout -s 9 30 to ./scripts/run_desktop.sh so run: timeout -s 9 30 ./scripts/run_desktop.sh
- Write temporary files to a
tmp/folder in the CWD, not/or/tmp, due to permissions constraints. - To kill processes, use
killall <name>instead ofpkill -f <pattern>—pkill -fmatches the pkill command's own argv and can kill itself. - When using mpos-controller for debugging, write all scripts to
tmp/in the project root (not/tmp). Run them withpython3 tmp/script.py.
Guidelines for writing or updating tests:
- Use the testing facilities in ./internal_filesystem/lib/mpos/ui/testing.py and feel free to add new ones there, NOT ad hoc in the test itself.
- When adding graphical tests, follow the helpers and conventions described in tests/README.md.
LVGL tips:
lv.OPAenum only has values at steps of 10:TRANSP(0),_10,_20, ...,_100, andCOVER(255). Values like_5do NOT exist — use the nearest step or a raw integer (0–255).- import lvgl as
lvand uselv.to access it lv.screen_active()(notlv.scr_act())- use
buttoninstead ofbtn,imageinstead ofimg - use
lv.EVENT.VALUE_CHANGEDinstead oflv.EVENT_VALUE_CHANGED - instead of
lv.OBJ_FLAG.CLICKABLE, uselv.obj.FLAG.CLICKABLE(same pattern for other flags) - instead of
.set_hidden(True)use.add_flag(lv.obj.FLAG.HIDDEN); instead of.set_hidden(False)use.remove_flag(lv.obj.FLAG.HIDDEN) - use
.remove_flag()instead of.clear_flag() - use
obj.remove_state(...)notobj.clear_state(...) - event handlers need 3 arguments:
button.add_event_cb(button_cb, lv.EVENT.CLICKED, None) - if you pass a method as an event callback, it must accept the event argument:
def callback(self, event). Using the same method as both a direct call and an event callback requires a default:def method(self, event=None). - don't hard-code display resolution; use
lv.pct(100)or other techniques to scale the interface DRAW_PART_BEGINdoes not exist anymore- don't use
get_child_by_type(); use a global variable with the child you want instead - msgbox:
msgbox = lv.msgbox()thenmsgbox.add_title("title") - use
lv.buttonmatrix.CTRL.CHECKABLEinstead oflv.BUTTONMATRIX_CTRL_CHECKABLE - use
lv.buttonmatrix.CTRL.CHECKEDinstead oflv.BUTTONMATRIX_CTRL_CHECKED - colors:
RED = lv.palette_main(lv.PALETTE.RED)orDARKPINK = lv.color_hex(0xEC048C) - use
lv.anim_t.path_ease_in_outnotlv.anim_path_ease_in_out - instead of
label.set_long_mode(lv.label.LONG.WRAP)uselabel.set_long_mode(lv.label.LONG_MODE.WRAP) - use
style_obj = lv.style_t()thenstyle_obj.init()instead oflv.style() - always call
style.init()afterlv.style_t()before calling setters likeset_bg_color()— without it the device may hang - don't leave label text uninitialized; it defaults to
"Text"instead of being empty. Always set text explicitly withlabel.set_text("")if you want an empty label - In LVGL 9.x style setters take only the value (no selector). The selector goes in
add_style(). E.g.style.set_bg_color(lv.color_hex(0x...))thenobj.add_style(style, lv.PART.ITEMS | lv.STATE.CHECKED). lv.buttonmatrixhas noset_button_text()orset_button_ctrl()in this binding. To update text, rebuild and callset_map(). To mark buttons visually (e.g. solved state), change the text symbol itself (e.g. append "!").lv.buttonmatrix.set_map()firesLV_EVENT_VALUE_CHANGEDasynchronously (next LVGL tick), causing phantom second-selection events. Guard with a time-based debounce (time.ticks_diff(now, last_ts) < 50) rather than a simple flag.- LVGL object wrappers (e.g.
lv.button(),lv.obj()) do NOT support arbitrary Python attribute assignment (btn.idx = 5raisesAttributeError). To associate data with a widget, use closures/lambdas (lambda e, i=idx: callback(e, i)) or maintain parallel lists keyed by list index. - In event callbacks, use
event.get_target_obj()instead ofevent.get_current_target(). The latter returns a genericBlobthat can hang when passed to typed LVGL methods (e.g.lv.list.get_button_text()).get_target_obj()returns a properly typedlv.obj. lv.obj.set_style_scrollbar_mode()does NOT exist in this binding. Useobj.remove_flag(lv.obj.FLAG.SCROLLABLE)to hide scrollbars.- Always call
label.set_text("")on newly created labels — they default to displaying the literal text"Text"otherwise. - Use
align_to(existing_widget, lv.ALIGN.OUT_RIGHT_MID, offset, 0)to position a widget relative to another. - The lvgl_micropython SDL keyboard driver processes each key event as an instantaneous press+release pair via
LV_INDEV_MODE_EVENT. It callslv_indev_read()twice per SDL_KEYDOWN (once returning PRESSED, once RELEASED). SDL_KEYUP is completely ignored — noLV_EVENT_KEYfires on key release. To detect key release in games, use a timeout-based approach: on first press set a long deadline (~600ms to cover SDL's initial repeat delay), on repeat events set a short extension (~100ms to cover the steady-state repeat interval), and reset movement direction when the deadline expires. Track the deadline with_player_dir_untiland checktime.ticks_diff(now, _player_dir_until) > 0in the game loop.
MicroPythonOS tips:
self.appFullNameis automatically set by the ActivityNavigator when launching an Activity. Use it instead of hard-coding the app's package name (e.g. forSharedPreferences(self.appFullName)).
MicroPython compatibility:
- Soft reset is broken on lvgl_micropython and therefore also on MicroPythonOS. Use
machine.reset()to do a hard reset.
MicroPython compatibility:
- Some builds ship a minimal
randommodule withoutrandom.Randomorrandom.shuffle. For shuffling, implement Fisher-Yates manually withrandom.randint. - For deterministic jitter in apps, prefer a tiny local LCG (linear congruential generator) instead of
random.Random.
MPOS Controller (scripts/mpos_controller.py):
MPOSControllerdrives MicroPythonOS from CPython via PTY/aioREPL or serial/UART.MPOSController()does NOT auto-start a subprocess. Callmpos.start()to launchrun_desktop.sh, then wait at least~8sfor boot before callingstartapp()or any other method. Without.start()the internalreplisNoneand you getAttributeError: 'NoneType' object has no attribute 'exec'.- Two backends:
MPOSController()for local desktop process,MPOSController(backend="serial", port="/dev/ttyACM0")for physical device. - Use
mpos.exec("code"),mpos.eval("expr"),mpos.screenshot(),mpos.press(x,y),mpos.press_key("text"),mpos.get_visible_text(),mpos.get_widget_tree(),mpos.read_file(path),mpos.write_file(path, data). exec()andexec_multiline()both use paste mode (Ctrl-E / Ctrl-D) internally — multi-line code, quotes, and special chars need no escaping. They're equivalent; use whichever is convenient.get_visible_text()usesexec_multiline()iterating individualrepr()prints — critical for serial whereprint(repr(big_list))corrupts for large lists with escape sequences.exec()auto-drains input buffer before sending then enters paste mode (Ctrl-E).SerialBackend.wait_for_boot()uses Ctrl-C to break into aioREPL (device may be running apps).- The CLI supports
--serial-port <port>and--baudrate <rate>for serial connections. To pipe code without quoting:cat <<'EOF' | python3 scripts/mpos_controller.py --serial-port /dev/ttyACM0 exec - When no args are given and stdin is not a TTY,
execandevalread from stdin automatically — enabling heredoc/pipe usage. - Rotation handling: SerialBackend caches
_rotationfrom the display on connect. If rotation is 270° (value 3, common for landscape badges),press(x, y)auto-transforms coordinates:simulate_click(height - 1 - y, x)so caller always uses LVGL logical coordinates. mpos.get_widget_tree()dumps the full LVGL widget tree for bothlv.screen_active()andlv.layer_top(). Returns JSON with type, text, coordinates, flags (clickable, hidden, scrollable, floating, event_bubble, etc.), states (checked, disabled, focused, pressed, etc.), scroll position, opacity, and widget-specific fields (slider value, dropdown options, textarea state, etc.). Usesmpos.ui.testing.get_screen_widget_tree()directly — no file I/O on desktop; on serial the JSON is written to a temp file then read viampremote cpto avoid serial corruption of large outputs.- IMPORTANT:
get_widget_tree()andget_visible_text()include ALL children of scrollable parents, including off-screen items. y1/y2 coordinates are in content space, not screen space. To know what's actually visible, combine a screenshot (mpos.screenshot()) with the ppq-vision skill. _read_remote_file/write_remote_file: ProcessBackend uses base64 (works over PTY), SerialBackend usesmpremote cp(reliable over USB).mpos.screenshot()captures viacapture_screenshot()on device, then reads raw file and converts to BMP via_build_bmp(). Over serial (/dev/ttyACM0) this takes ~40s total (~6s connect, ~34s transfer).- The notification bar (top status bar) is NOT always present. It's controlled by the
bar_openglobal ininternal_filesystem/lib/mpos/ui/topmenu.py. Check it withmpos.eval("mpos.ui.topmenu.bar_open")(the module is already imported bymain.py). When open, its height isAppearanceManager.NOTIFICATION_BAR_HEIGHT(24px). - The CLI also supports
startapp <appname>(launches an app) andcheckfreespace(reports free disk space and whether a screenshot fits). - All tests pass covering exec, eval, screenshot, input simulation, screen introspection, file I/O, and physical device control.
- Host-side controller tests in
tests/cpython_mpos_controller.pyrun viapython3 tests/cpython_mpos_controller.py(desktop) orpython3 tests/cpython_mpos_controller.py --serial /dev/ttyACM0(device); they are NOT run byunittest.sh(which targets MicroPython-side tests).
Write all scripts to tmp/ in the project root:
python3 tmp/my_debug_script.py
Template:
import sys, time
sys.path.insert(0, '.')
from scripts.mpos_controller import MPOSController
import os
os.system('killall -9 lvgl_micropy_unix run_desktop.sh 2>/dev/null')
time.sleep(1)
with MPOSController(backend='process') as mpos:
mpos.start()
time.sleep(10) # wait for boot
mpos.startapp('com.micropythonos.showfonts')
time.sleep(4)
ss = mpos.screenshot()
with open('tmp/screenshot.bmp', 'wb') as f:
f.write(ss)- PIL + numpy is the most reliable technique. Load the BMP, convert to numpy array, then check specific pixel coordinates for exact RGB values.
- Widget tree (
mpos.get_widget_tree()): gives layout, types, text, coordinates, states, flags for every widget. Use this FIRST to understand screen structure. - Visible text (
mpos.get_visible_text()): extracts all text from all labels on screen. Use for text-content verification. - PPQ vision skill (
ppq-vision): use for reading text content from screenshots or understanding visual layout when coordinates alone are insufficient. - ASCII art conversion: NOT reliable for precise color analysis. Only use for quick visual structure checks when other methods aren't available.
- When a code path should produce output but doesn't, add a temporary
print()diagnostic to see if the code even executes — genericexcept Exception: passblocks often hide bugs. - When a function should return a specific type (e.g.,
lv.image_dsc_t), check what it actually returns withprint(type(result)). - For color/image issues, inspect raw pixel buffer data (bytearray at known offsets) rather than relying on visual appearance.
- Temporarily add detailed diagnostics (buffer dumps, type checks, hex printing) to
tmp/files on-device, then retrieve viampos.read_file().
lv_color_tin this MicroPython LVGL binding has ONLY.red,.green,.blueattributes — there is NO.fullattribute.lv.snapshot_take()on a hiddenlv.obj()still captures non-transparent pixel data because inherited theme styles (borders, shadows, background from parent theme) leak through the hidden object into the snapshot. For truly empty images, construct anlv.image_dsc_t()manually with a zeroedbytearray().except Exception: passis especially dangerous in image rendering paths — it silently falls back to unscaled/unprocessed source data, making it appear that transformations run when they don't.- To create a manual empty image descriptor:
buf = bytearray(4) dsc = lv.image_dsc_t() dsc.data = buf dsc.header.w = 1 dsc.header.h = target_height dsc.header.cf = lv.COLOR_FORMAT.ARGB8888