A learning-focused project for the M5Stack Cardputer ADV. Every file is heavily commented to teach MicroPython and embedded development concepts.
The code IS the documentation. Each file contains extensive comments explaining:
- What each section does and WHY
- Common patterns and best practices
- API references and quick lookups
- Gotchas and important notes
Start with apps/hello_world.py - it's the template with the most detailed explanations.
M5Stack Cardputer ADV - ESP32-S3 with built-in 240x135 LCD and keyboard.
Uses a custom MicroPython firmware based on uiflow-micropython.
Custom firmware repo: https://github.com/TheRealHaoLiu/cardputer-adv-micropython (branch: custom-firmware)
Local convention: Clone alongside this repo at ../cardputer-adv-micropython
Why custom firmware?
- Based on M5Stack's UIFlow2 with hardware libraries (Lcd, Widgets, Speaker, etc.)
- Customizations specific to this project's needs
- Hardware abstraction for Cardputer ADV's display, keyboard, speaker
Available modules: See MODULES.md in the firmware repo for the full list of available imports including M5, hardware drivers, sensors, networking, and unit classes.
Firmware version: Check in Settings > About, or programmatically:
import firmware_info
print(firmware_info.CUSTOM_VERSION) # "2.4.1+therealhaoliu.1"
print(firmware_info.UPSTREAM_VERSION) # "2.4.1"Clone the firmware repo alongside this one:
# From parent directory of cardputer-adv
git clone -b custom-firmware https://github.com/TheRealHaoLiu/cardputer-adv-micropython
# Follow build instructions in that repoFlash to device using esptool or the build system's flash target.
If mpremote can't connect, try: turn off the device, then plug in USB to power it on. Not sure why this helps, but it does.
# Install dependencies
uv sync
# Install pre-commit hooks (required for contributors)
uv run pre-commit installThis installs git hooks that run gitleaks (secret scanning) and ruff (linting/formatting) on every commit.
Run the launcher (menu to select apps):
uv run poe runRun a specific app directly (skip the menu):
uv run poe run apps/hello_world.py
uv run poe run apps/demo_anim.py
uv run poe run apps/notepad.py
# etc.Each app has standalone support via if __name__ == "__main__" block, so you can test individual apps without going through the launcher menu.
poe run mounts your local directory to /remote/ on the device and executes the file. All files are loaded from your local machine - including lib/ (framework.py, app_base.py) and apps/. Your local edits are immediately available - no copying needed!
poe run(no args) → runsmain.py→ shows launcher menupoe run apps/foo.py→ runs that app directly with hardware init
Hot-reloading: In remote mode, press 'r' in the launcher to reload all app modules. ESC now returns instantly without reloading.
Press ESC to exit any app and return to the launcher (or REPL if running standalone).
uv run poe deployCopies all files to device flash. The device runs independently after this - no computer needed.
WARNING: Deploy replaces all of the following on the device:
/flash/main.py/flash/lib/*(framework.py, app_base.py, etc.)/flash/apps/*(all app files)
Any changes made directly on the device will be lost!
uv run mpremote connect /dev/tty.usbmodem* replFor interactive debugging. Exit with Ctrl+].
uv run poe --help # List all tasks
uv run poe run [file] # Run file (default: main.py)
uv run poe deploy # Copy files to flash
uv run poe ls [path] # List files on device
uv run poe cat <path> # Show file contents from device
uv run poe reset # Reset device
uv run poe firmware-download [version] # Download firmware (default: pyproject.toml version)
uv run poe lint # Check code for errors
uv run poe format # Format code with ruffEach demo teaches specific concepts. Run them with uv run poe run apps/<name>.py.
| App | Concepts Taught |
|---|---|
hello_world |
START HERE - App structure, keyboard callbacks, main loop pattern |
notepad |
Text editing, cursor management, incremental screen updates |
demo_anim |
Double buffering - The key to smooth animation (canvas vs direct draw) |
demo_text |
Fonts, colors (Lcd.COLOR.*), text alignment, scrolling marquee |
demo_lcd |
Shape drawing, brightness control, QR codes, display info |
demo_keyboard |
Key events, modifier detection, FN combinations, matrix layout |
demo_sound |
Tones, musical notes, volume control, sound effects |
demo_widgets |
High-level UI components (Label), text alignment, UI patterns |
demo_nvs |
Persistent storage, saving settings, understanding boot options |
- Copy
apps/hello_world.pyto your new app file - Rename the class (must inherit from
AppBase) - Update the
if __name__ == "__main__"block to use your class - Register in manifest.json - add your app to the appropriate manifest:
// apps/manifest.json (top-level menu)
{
"hello_world": "Hello World",
"my_app": "My App Name"
}
// apps/demo/manifest.json (Demo submenu)
{
"sound_demo": "Sound Demo",
...
}Directory structure = menu hierarchy:
apps/*.py+apps/manifest.json→ top-level menu itemsapps/demo/*.py+apps/demo/manifest.json→ Demo/ submenu- Create new subdirectories with manifest.json for more submenus
Hot-reload in dev mode: Press 'r' in the launcher to reload all apps.
See apps/hello_world.py for the required structure and keyboard handling patterns.
Every app needs this structure:
while not exit_flag:
M5.update() # REQUIRED - processes hardware events
# Your logic here
time.sleep(0.02) # Prevent busy-waitingNever do slow operations in callbacks! Use flags:
exit_flag = False
def on_key(keyboard):
nonlocal exit_flag
while keyboard._keyevents:
event = keyboard._keyevents.pop(0)
if event.keycode == 0x1B: # ESC
exit_flag = True # Just set flag, don't do heavy work
kb.set_keyevent_callback(on_key)canvas = Lcd.newCanvas(240, 135) # Create off-screen buffer
canvas.fillScreen(Lcd.COLOR.BLACK) # Draw to canvas (invisible)
canvas.fillRect(x, y, w, h, color)
canvas.push(0, 0) # Copy to screen (atomic, no flicker!)
canvas.delete() # Free memory when doneUse built-in color constants for cleaner code:
Lcd.COLOR.BLACK, Lcd.COLOR.WHITE
Lcd.COLOR.RED, Lcd.COLOR.GREEN, Lcd.COLOR.BLUE
Lcd.COLOR.YELLOW, Lcd.COLOR.MAGENTA, Lcd.COLOR.CYAN
Lcd.COLOR.ORANGE, Lcd.COLOR.PINK, Lcd.COLOR.PURPLESee TROUBLESHOOTING.md for common issues like:
- mpremote can't connect (launcher blocking)
- REPL modes (raw vs normal)
- Boot option values
- NVS initialization