14 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project Overview
MicroPythonOS is an embedded operating system that runs on ESP32 hardware (particularly the Waveshare ESP32-S3-Touch-LCD-2) and desktop Linux/macOS. It provides an LVGL-based UI framework with an Android-inspired app architecture featuring Activities, Intents, and a PackageManager.
The OS supports:
- Touch and non-touch input devices (keyboard/joystick navigation)
- Camera with QR decoding (using quirc)
- WiFi connectivity
- Over-the-air (OTA) firmware updates
- App installation via MPK packages
- Bitcoin Lightning and Nostr protocols
Repository Structure
Core Directories
-
internal_filesystem/: The runtime filesystem containing the OS and appsboot.py: Hardware initialization for ESP32-S3-Touch-LCD-2boot_unix.py: Desktop-specific boot initializationmain.py: UI initialization, theme setup, and launcher startlib/mpos/: Core OS library (apps, config, UI, content management)apps/: User-installed apps (symlinks to external app repos)builtin/: System apps frozen into the firmware (launcher, appstore, settings, etc.)data/: Static data filessdcard/: SD card mount point
-
lvgl_micropython/: Submodule containing LVGL bindings for MicroPython -
micropython-camera-API/: Submodule for camera support -
micropython-nostr/: Submodule for Nostr protocol -
c_mpos/: C extension modules (includes quirc for QR decoding) -
secp256k1-embedded-ecdh/: Submodule for cryptographic operations -
manifests/: Build manifests defining what gets frozen into firmware -
freezeFS/: Files to be frozen into the built-in filesystem -
scripts/: Build and deployment scripts -
tests/: Test suite (both unit tests and manual tests)
Key Architecture Components
App System: Similar to Android
- Apps are identified by reverse-domain names (e.g.,
com.micropythonos.camera) - Each app has a
META-INF/MANIFEST.JSONwith metadata and activity definitions - Activities extend
mpos.app.activity.Activityclass (import:from mpos.app.activity import Activity) - Apps implement
onCreate()to set up their UI andonDestroy()for cleanup - Activity lifecycle:
onCreate()→onStart()→onResume()→onPause()→onStop()→onDestroy() - Apps are packaged as
.mpkfiles (zip archives) - Built-in system apps (frozen into firmware): launcher, appstore, settings, wifi, osupdate, about
UI Framework: Built on LVGL 9.3.0
mpos.ui.topmenu: Notification bar and drawer (top menu)mpos.ui.display: Root screen initialization- Gesture support: left-edge swipe for back, top-edge swipe for menu
- Theme system with configurable colors and light/dark modes
- Focus groups for keyboard/joystick navigation
Content Management:
PackageManager: Install/uninstall/query appsIntent: Launch activities with action/category filtersSharedPreferences: Per-app key-value storage (similar to Android)
Hardware Abstraction:
boot.pyconfigures SPI, I2C, display (ST7789), touchscreen (CST816S), and battery ADC- Platform detection via
sys.platform("esp32" vs others) - Different boot files per hardware variant (boot_fri3d-2024.py, etc.)
Build System
Building Firmware
The main build script is scripts/build_mpos.sh:
# Development build (no frozen filesystem, requires ./scripts/install.sh after flashing)
./scripts/build_mpos.sh unix dev
# Production build (with frozen filesystem)
./scripts/build_mpos.sh unix prod
# ESP32 builds (specify hardware variant)
./scripts/build_mpos.sh esp32 dev waveshare-esp32-s3-touch-lcd-2
./scripts/build_mpos.sh esp32 prod fri3d-2024
Build types:
dev: No preinstalled files or builtin filesystem. Boots to black screen until you run./scripts/install.shprod: Files frommanifest*.pyare frozen into firmware. Run./scripts/freezefs_mount_builtin.shbefore building
Targets:
esp32: ESP32-S3 hardware (requires subtarget:waveshare-esp32-s3-touch-lcd-2orfri3d-2024)unix: Linux desktopmacOS: macOS desktop
The build system uses lvgl_micropython/make.py which wraps MicroPython's build system. It:
- Fetches SDL tags for desktop builds
- Patches manifests to include camera and asyncio support
- Creates symlinks for C modules (secp256k1, c_mpos)
- Runs the lvgl_micropython build with appropriate flags
ESP32 build configuration:
- Board:
ESP32_GENERIC_S3withSPIRAM_OCTvariant - Display driver:
st7789 - Input device:
cst816s - OTA enabled with 4MB partition size (16MB total flash)
- Dual-core threading enabled (no GIL)
- User C modules: camera, secp256k1, c_mpos/quirc
Desktop build configuration:
- Display:
sdl_display - Input:
sdl_pointer,sdl_keyboard - Compiler flags:
-g -O0 -ggdb -ljpeg(debug symbols enabled) - STRIP is disabled to keep debug symbols
Building and Bundling Apps
Apps can be bundled into .mpk files:
./scripts/bundle_apps.sh
Running on Desktop
# Run normally (starts launcher)
./scripts/run_desktop.sh
# Run a specific Python script directly
./scripts/run_desktop.sh path/to/script.py
# Run a specific app by name
./scripts/run_desktop.sh com.micropythonos.camera
Important environment variables:
HEAPSIZE: Set heap size (default 8M, matches ESP32-S3 PSRAM). Increase for memory-intensive appsSDL_WINDOW_FULLSCREEN: Set totruefor fullscreen mode
The script automatically selects the correct binary (lvgl_micropy_unix or lvgl_micropy_macOS) and runs from the internal_filesystem/ directory.
Deploying to Hardware
Flashing Firmware
# Flash firmware over USB
./scripts/flash_over_usb.sh
Installing Files to Device
# Install all files to device (boot.py, main.py, lib/, apps/, builtin/)
./scripts/install.sh waveshare-esp32-s3-touch-lcd-2
# Install a single app to device
./scripts/install.sh waveshare-esp32-s3-touch-lcd-2 camera
Uses mpremote from MicroPython tools to copy files over serial connection.
Testing
Running Tests
Tests are in the tests/ directory. There are two types: unit tests and manual tests.
Unit tests (automated, run on desktop or device):
# Run all unit tests on desktop
./tests/unittest.sh
# Run a specific test file on desktop
./tests/unittest.sh tests/test_shared_preferences.py
./tests/unittest.sh tests/test_intent.py
./tests/unittest.sh tests/test_package_manager.py
./tests/unittest.sh tests/test_start_app.py
# Run a specific test on connected device (via mpremote)
./tests/unittest.sh tests/test_shared_preferences.py ondevice
The unittest.sh script:
- Automatically detects the platform (Linux/macOS) and uses the correct binary
- Sets up the proper paths and heapsize
- Can run tests on device using
mpremotewith theondeviceargument - Runs all
test_*.pyfiles when no argument is provided
Available unit test modules:
test_shared_preferences.py: Tests formpos.config.SharedPreferences(configuration storage)test_intent.py: Tests formpos.content.intent.Intent(intent creation, extras, flags)test_package_manager.py: Tests forPackageManager(version comparison, app discovery)test_start_app.py: Tests for app launching (requires SDL display initialization)
Manual tests (interactive, for hardware-specific features):
manual_test_camera.py: Camera and QR scanningmanual_test_nostr_asyncio.py: Nostr protocolmanual_test_nwcwallet*.py: Lightning wallet connectivity (Alby, Cashu)manual_test_lnbitswallet.py: LNbits wallet integrationtest_websocket.py: WebSocket functionalitytest_multi_connect.py: Multiple concurrent connections
Run manual tests with:
./scripts/run_desktop.sh tests/manual_test_camera.py
Writing New Tests
Unit test guidelines:
- Use Python's
unittestmodule (compatible with MicroPython) - Place tests in
tests/directory withtest_*.pynaming - Use
setUp()andtearDown()for test fixtures - Clean up any created files/directories in
tearDown() - Tests should be runnable on desktop (unix build) without hardware dependencies
- Use descriptive test names:
test_<what_is_being_tested> - Group related tests in test classes
Example test structure:
import unittest
from mpos.some_module import SomeClass
class TestSomeClass(unittest.TestCase):
def setUp(self):
# Initialize test fixtures
pass
def tearDown(self):
# Clean up after test
pass
def test_some_functionality(self):
# Arrange
obj = SomeClass()
# Act
result = obj.some_method()
# Assert
self.assertEqual(result, expected_value)
Development Workflow
Creating a New App
- Create app directory:
internal_filesystem/apps/com.example.myapp/ - Create
META-INF/MANIFEST.JSONwith app metadata and activities - Create
assets/directory for Python code - Create main activity file extending
Activityclass - Implement
onCreate()method to build UI - Optional: Create
res/directory for resources (icons, images)
Minimal app structure:
com.example.myapp/
├── META-INF/
│ └── MANIFEST.JSON
├── assets/
│ └── main_activity.py
└── res/
└── mipmap-mdpi/
└── icon_64x64.png
Minimal Activity code:
from mpos.app.activity import Activity
import lvgl as lv
class MainActivity(Activity):
def onCreate(self):
screen = lv.obj()
label = lv.label(screen)
label.set_text('Hello World!')
label.center()
self.setContentView(screen)
See internal_filesystem/apps/com.micropythonos.helloworld/ for a minimal example and built-in apps in internal_filesystem/builtin/apps/ for more complex examples.
Testing App Changes
For rapid iteration on desktop:
# Build desktop version (only needed once)
./scripts/build_mpos.sh unix dev
# Install filesystem to device (run after code changes)
./scripts/install.sh waveshare-esp32-s3-touch-lcd-2
# Or run directly on desktop
./scripts/run_desktop.sh com.example.myapp
Debugging
Desktop builds include debug symbols by default. Use GDB:
gdb --args ./lvgl_micropython/build/lvgl_micropy_unix -X heapsize=8M -v -i -c "$(cat boot_unix.py main.py)"
For ESP32 debugging, enable core dumps:
./scripts/core_dump_activate.sh
Important Constraints
Memory Management
ESP32-S3 has 8MB PSRAM. Memory-intensive operations:
- Camera images consume ~2.5MB per frame
- LVGL image cache must be managed with
lv.image.cache_drop(None) - Large UI components should be created/destroyed rather than hidden
- Use
gc.collect()strategically after deallocating large objects
Threading
- Main UI/LVGL operations must run on main thread
- Background tasks use
_thread.start_new_thread() - Stack size: 16KB for ESP32, 24KB for desktop (see
mpos.apps.good_stack_size()) - Use
mpos.ui.async_call()to safely invoke UI operations from background threads
Async Operations
- OS uses
uasynciofor networking (WebSockets, HTTP, Nostr) - WebSocket library is custom
websocket.pyusing uasyncio - HTTP uses
aiohttppackage (inlib/aiohttp/) - Async tasks are throttled per frame to prevent memory overflow
File Paths
- Use
M:/path/to/fileprefix for LVGL file operations (registered in main.py) - Absolute paths for Python imports
- Apps run with their directory added to
sys.path
Build Dependencies
The build requires all git submodules checked out recursively:
git submodule update --init --recursive
Desktop dependencies: See .github/workflows/build.yml for full list including:
- SDL2 development libraries
- Mesa/EGL libraries
- libjpeg
- Python 3.8+
- cmake, ninja-build
Manifest System
Manifests define what gets frozen into firmware:
manifests/manifest.py: ESP32 production buildsmanifests/manifest_fri3d-2024.py: Fri3d Camp 2024 Badge variantmanifests/manifest_unix.py: Desktop builds
Manifests use freeze() directives to include files in the frozen filesystem. Frozen files are baked into the firmware and cannot be modified at runtime.
Version Management
Versions are tracked in:
CHANGELOG.md: User-facing changelog with release history- App versions in
META-INF/MANIFEST.JSONfiles - OS update system checks
hardware_idfrommpos.info.get_hardware_id()
Current stable version: 0.3.3 (as of latest CHANGELOG entry)
Critical Code Locations
- App lifecycle:
internal_filesystem/lib/mpos/apps.py:execute_script() - Activity base class:
internal_filesystem/lib/mpos/app/activity.py - Package management:
internal_filesystem/lib/mpos/content/package_manager.py - Intent system:
internal_filesystem/lib/mpos/content/intent.py - UI initialization:
internal_filesystem/main.py - Hardware init:
internal_filesystem/boot.py - Config/preferences:
internal_filesystem/lib/mpos/config.py - Top menu/drawer:
internal_filesystem/lib/mpos/ui/topmenu.py - Activity navigation:
internal_filesystem/lib/mpos/activity_navigator.py
Common Utilities and Helpers
SharedPreferences: Persistent key-value storage per app
from mpos.config import SharedPreferences
# Load preferences
prefs = SharedPreferences("com.example.myapp")
value = prefs.get_string("key", "default_value")
number = prefs.get_int("count", 0)
data = prefs.get_dict("data", {})
# Save preferences
editor = prefs.edit()
editor.put_string("key", "value")
editor.put_int("count", 42)
editor.put_dict("data", {"key": "value"})
editor.commit()
Intent system: Launch activities and pass data
from mpos.content.intent import Intent
# Launch activity by name
intent = Intent()
intent.setClassName("com.micropythonos.camera", "Camera")
self.startActivity(intent)
# Launch with extras
intent.putExtra("key", "value")
self.startActivityForResult(intent, self.handle_result)
def handle_result(self, result):
if result["result_code"] == Activity.RESULT_OK:
data = result["data"]
UI utilities:
mpos.ui.async_call(func, *args, **kwargs): Safely call UI operations from background threadsmpos.ui.back_screen(): Navigate back to previous screenmpos.ui.focus_direction: Keyboard/joystick navigation helpersmpos.ui.anim: Animation utilities
Other utilities:
mpos.apps.good_stack_size(): Returns appropriate thread stack size for platform (16KB ESP32, 24KB desktop)mpos.wifi: WiFi management utilitiesmpos.sdcard.SDCardManager: SD card mounting and managementmpos.clipboard: System clipboard accessmpos.battery_voltage: Battery level reading (ESP32 only)