diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 1d3b367..c900688 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -20,7 +20,7 @@ jobs: with: python-version: '3.x' - name: Install dependencies - run: pip install mkdocs mkdocs-material + run: pip install mkdocs mkdocs-material markdown-include - name: Build site run: mkdocs build - name: Setup Pages diff --git a/build.sh b/build.sh index 1293a88..6328799 100755 --- a/build.sh +++ b/build.sh @@ -1,2 +1,2 @@ -pip3 install mkdocs mkdocs-material markdown-include +#pip3 install mkdocs mkdocs-material markdown-include mkdocs build diff --git a/docs/architecture/filesystem.md b/docs/architecture/filesystem.md index 3c7cffe..da66dd1 100644 --- a/docs/architecture/filesystem.md +++ b/docs/architecture/filesystem.md @@ -2,15 +2,15 @@ MicroPythonOS uses a structured filesystem to organize apps, data, and resources. -- **/apps/**: Directory for downloaded and installed apps. +- **apps/**: Directory for downloaded and installed apps. - **com.micropythonos.helloworld/**: Installation directory for HelloWorld App. See [Creating Apps](../apps/creating-apps.md). -- **/builtin/**: Read-only filesystem compiled into the OS, mounted at boot by `main.py`. +- **builtin/**: Read-only filesystem compiled into the OS, mounted at boot by `main.py`. - **apps/**: See [Built-in Apps](../apps/built-in-apps.md). - **res/mipmap-mdpi/default_icon_64x64.png**: Default icon for apps without one. - **lib/**: Libraries and frameworks - **mpos/**: MicroPythonOS libraries and frameworks - **ui/**: MicroPythonOS User Interface libraries and frameworks -- **/data/**: Storage for app data. +- **data/**: Storage for app data. - **com.micropythonos.helloworld/**: App-specific storage (e.g., `config.json`) - **com.micropythonos.settings/**: Storage used by the built-in Settings App - **com.micropythonos.wifi/**: Storage used by the built-in WiFi App diff --git a/docs/architecture/system-components.md b/docs/architecture/system-components.md index 954bfd2..a06990d 100644 --- a/docs/architecture/system-components.md +++ b/docs/architecture/system-components.md @@ -2,8 +2,10 @@ MicroPythonOS consists of several core components that initialize and manage the system. -- **boot.py**: Initializes hardware on ESP32 microcontrollers. -- **boot_unix.py**: Initializes hardware on Linux desktops (and potentially MacOS). +- **boot.py**: Initializes hardware on the [Waveshare ESP32-S3-Touch-LCD-2](https://www.waveshare.com/wiki/ESP32-S3-Touch-LCD-2) +- **boot_fri3d2024.py**: Initializes hardware on the [Fri3d Camp 2024 Badge](https://fri3d.be/badge/2024/) +- **boot_unix.py**: Initializes hardware on Linux and MacOS systems + - **main.py**: - Sets up the user interface. - Provides helper functions for apps. diff --git a/docs/building/index.md b/docs/building/index.md deleted file mode 100644 index a204773..0000000 --- a/docs/building/index.md +++ /dev/null @@ -1,7 +0,0 @@ -# Building MicroPythonOS - -Build MicroPythonOS for ESP32 microcontrollers or desktop environments. - -- [For ESP32](esp32.md): Build and flash for ESP32 devices. -- [For Desktop](desktop.md): Build and run on Linux or MacOS. -- [Release Checklist](release-checklist.md): Steps for creating a new release. diff --git a/docs/frameworks/audioflinger.md b/docs/frameworks/audioflinger.md new file mode 100644 index 0000000..2054032 --- /dev/null +++ b/docs/frameworks/audioflinger.md @@ -0,0 +1,432 @@ +# AudioFlinger + +MicroPythonOS provides a centralized audio service called **AudioFlinger**, inspired by Android's architecture. It manages audio playback across different hardware outputs with priority-based audio focus control. + +## Overview + +AudioFlinger provides: + +✅ **Priority-based audio focus** - Higher priority streams interrupt lower priority ones +✅ **Multiple audio devices** - I2S digital audio, PWM buzzer, or both +✅ **Background playback** - Runs in separate thread +✅ **WAV file support** - 8/16/24/32-bit PCM, mono/stereo, auto-upsampling +✅ **RTTTL ringtone support** - Full Ring Tone Text Transfer Language parser +✅ **Thread-safe** - Safe for concurrent access +✅ **Hardware-agnostic** - Apps work across all platforms without changes + +## Supported Audio Devices + +- **I2S**: Digital audio output for WAV file playback (Fri3d badge, Waveshare board) +- **Buzzer**: PWM-based tone/ringtone playback (Fri3d badge only) +- **Both**: Simultaneous I2S and buzzer support +- **Null**: No audio (desktop/Linux) + +## Quick Start + +### Playing WAV Files + +```python +from mpos.app.activity import Activity +import mpos.audio.audioflinger as AudioFlinger + +class MusicPlayerActivity(Activity): + def onCreate(self): + # Play a music file + success = AudioFlinger.play_wav( + "M:/sdcard/music/song.wav", + stream_type=AudioFlinger.STREAM_MUSIC, + volume=80, + on_complete=lambda msg: print(msg) + ) + + if not success: + print("Playback rejected (higher priority stream active)") +``` + +**Supported formats:** +- **Encoding**: PCM (8/16/24/32-bit) +- **Channels**: Mono or stereo +- **Sample rate**: Any rate (auto-upsampled to ≥22050 Hz) + +### Playing RTTTL Ringtones + +```python +# Play notification sound via buzzer +rtttl = "Nokia:d=4,o=5,b=225:8e6,8d6,8f#,8g#,8c#6,8b,d,8p,8b,8a,8c#,8e" +AudioFlinger.play_rtttl( + rtttl, + stream_type=AudioFlinger.STREAM_NOTIFICATION +) +``` + +**RTTTL format example:** +``` +name:settings:notes +Nokia:d=4,o=5,b=225:8e6,8d6,8f#,8g# +``` + +- `d=4` - Default duration (quarter note) +- `o=5` - Default octave +- `b=225` - Beats per minute +- Notes: `8e6` = 8th note, E in octave 6 + +### Volume Control + +```python +# Set volume (0-100) +AudioFlinger.set_volume(70) + +# Get current volume +volume = AudioFlinger.get_volume() +print(f"Current volume: {volume}") +``` + +### Stopping Playback + +```python +# Stop currently playing audio +AudioFlinger.stop() +``` + +## Audio Focus Priority + +AudioFlinger implements a 3-tier priority-based audio focus system inspired by Android: + +| Stream Type | Priority | Use Case | Behavior | +|-------------|----------|----------|----------| +| **STREAM_ALARM** | 2 (Highest) | Alarms, alerts | Interrupts all other streams | +| **STREAM_NOTIFICATION** | 1 (Medium) | Notifications, UI sounds | Interrupts music, rejected by alarms | +| **STREAM_MUSIC** | 0 (Lowest) | Music, podcasts | Interrupted by everything | + +### Priority Rules + +1. **Higher priority interrupts lower priority**: ALARM > NOTIFICATION > MUSIC +2. **Equal priority is rejected**: Can't play two alarms simultaneously +3. **Lower priority is rejected**: Can't start music while alarm is playing + +### Example: Priority in Action + +```python +# Start playing music (priority 0) +AudioFlinger.play_wav("music.wav", stream_type=AudioFlinger.STREAM_MUSIC) + +# Notification sound (priority 1) interrupts music +AudioFlinger.play_rtttl("beep:d=4:8c", stream_type=AudioFlinger.STREAM_NOTIFICATION) +# Music stops, notification plays + +# Try to play another notification while first is playing +success = AudioFlinger.play_rtttl("beep:d=4:8d", stream_type=AudioFlinger.STREAM_NOTIFICATION) +# Returns False - equal priority rejected + +# Alarm (priority 2) interrupts notification +AudioFlinger.play_wav("alarm.wav", stream_type=AudioFlinger.STREAM_ALARM) +# Notification stops, alarm plays +``` + +## Hardware Support Matrix + +| Board | I2S | Buzzer | Notes | +|-------|-----|--------|-------| +| **Fri3d 2024 Badge** | ✅ GPIO 2, 47, 16 | ✅ GPIO 46 | Both devices available | +| **Waveshare ESP32-S3** | ✅ GPIO 2, 47, 16 | ❌ | I2S only | +| **Linux/macOS** | ❌ | ❌ | No audio (desktop builds) | + +**I2S Pins:** +- **BCLK** (Bit Clock): GPIO 2 +- **WS** (Word Select): GPIO 47 +- **DOUT** (Data Out): GPIO 16 + +**Buzzer Pin:** +- **PWM**: GPIO 46 (Fri3d badge only) + +## Configuration + +Audio device preference is configured in the Settings app under **"Advanced Settings"**: + +- **Auto-detect**: Use available hardware (default) +- **I2S (Digital Audio)**: Digital audio only +- **Buzzer (PWM Tones)**: Tones/ringtones only +- **Both I2S and Buzzer**: Use both devices +- **Disabled**: No audio + +**⚠️ Important**: Changing the audio device requires a **restart** to take effect. + +### How Auto-Detect Works + +1. Checks if I2S hardware is available +2. Checks if Buzzer hardware is available +3. Selects the best available option: + - Both devices available → Use "Both" + - Only I2S → Use "I2S" + - Only Buzzer → Use "Buzzer" + - Neither → Use "Null" (no audio) + +## Complete Example: Music Player with Controls + +```python +from mpos.app.activity import Activity +import mpos.audio.audioflinger as AudioFlinger +import lvgl as lv + +class SimpleMusicPlayerActivity(Activity): + def onCreate(self): + self.screen = lv.obj() + + # Play button + play_btn = lv.button(self.screen) + play_btn.set_size(100, 50) + play_btn.set_pos(10, 50) + play_label = lv.label(play_btn) + play_label.set_text("Play") + play_label.center() + play_btn.add_event_cb(lambda e: self.play_music(), lv.EVENT.CLICKED, None) + + # Stop button + stop_btn = lv.button(self.screen) + stop_btn.set_size(100, 50) + stop_btn.set_pos(120, 50) + stop_label = lv.label(stop_btn) + stop_label.set_text("Stop") + stop_label.center() + stop_btn.add_event_cb(lambda e: AudioFlinger.stop(), lv.EVENT.CLICKED, None) + + # Volume slider + volume_label = lv.label(self.screen) + volume_label.set_text("Volume:") + volume_label.set_pos(10, 120) + + volume_slider = lv.slider(self.screen) + volume_slider.set_size(200, 10) + volume_slider.set_pos(10, 150) + volume_slider.set_range(0, 100) + volume_slider.set_value(AudioFlinger.get_volume(), False) + volume_slider.add_event_cb( + lambda e: AudioFlinger.set_volume(volume_slider.get_value()), + lv.EVENT.VALUE_CHANGED, + None + ) + + self.setContentView(self.screen) + + def play_music(self): + success = AudioFlinger.play_wav( + "M:/sdcard/music/song.wav", + stream_type=AudioFlinger.STREAM_MUSIC, + volume=AudioFlinger.get_volume(), + on_complete=lambda msg: print(f"Playback finished: {msg}") + ) + + if not success: + print("Playback rejected - higher priority audio active") +``` + +## API Reference + +### Functions + +**`play_wav(path, stream_type=STREAM_MUSIC, volume=None, on_complete=None)`** + +Play a WAV file. + +- **Parameters:** + - `path` (str): Path to WAV file (e.g., `"M:/sdcard/music/song.wav"`) + - `stream_type` (int): Stream type constant (STREAM_ALARM, STREAM_NOTIFICATION, STREAM_MUSIC) + - `volume` (int, optional): Volume 0-100. If None, uses current volume + - `on_complete` (callable, optional): Callback function called when playback completes + +- **Returns:** `bool` - `True` if playback started, `False` if rejected (higher/equal priority active) + +**`play_rtttl(rtttl, stream_type=STREAM_NOTIFICATION)`** + +Play an RTTTL ringtone. + +- **Parameters:** + - `rtttl` (str): RTTTL format string (e.g., `"Nokia:d=4,o=5,b=225:8e6,8d6"`) + - `stream_type` (int): Stream type constant + +- **Returns:** `bool` - `True` if playback started, `False` if rejected + +**`set_volume(volume)`** + +Set playback volume. + +- **Parameters:** + - `volume` (int): Volume level 0-100 + +**`get_volume()`** + +Get current playback volume. + +- **Returns:** `int` - Current volume (0-100) + +**`stop()`** + +Stop currently playing audio. + +### Stream Type Constants + +- `AudioFlinger.STREAM_ALARM` - Highest priority (value: 2) +- `AudioFlinger.STREAM_NOTIFICATION` - Medium priority (value: 1) +- `AudioFlinger.STREAM_MUSIC` - Lowest priority (value: 0) + +## Troubleshooting + +### Playback Rejected + +**Symptom**: `play_wav()` or `play_rtttl()` returns `False`, no sound plays + +**Cause**: Higher or equal priority stream is currently active + +**Solution**: +1. Check if higher priority audio is playing +2. Wait for higher priority stream to complete +3. Use `AudioFlinger.stop()` to force stop current playback (use sparingly) +4. Use equal or higher priority stream type + +```python +# Check if playback was rejected +success = AudioFlinger.play_wav("sound.wav", stream_type=AudioFlinger.STREAM_MUSIC) +if not success: + print("Higher priority audio is playing") + # Option 1: Wait and retry + # Option 2: Stop current playback (if appropriate) + AudioFlinger.stop() + AudioFlinger.play_wav("sound.wav", stream_type=AudioFlinger.STREAM_MUSIC) +``` + +### WAV File Not Playing + +**Symptom**: File exists but doesn't play, or plays with distortion + +**Cause**: Unsupported WAV format + +**Requirements:** +- **Encoding**: PCM only (not MP3, AAC, or other compressed formats) +- **Bit depth**: 8, 16, 24, or 32-bit +- **Channels**: Mono or stereo +- **Sample rate**: Any (auto-upsampled to ≥22050 Hz) + +**Solution**: Convert WAV file to supported format using ffmpeg or audacity: + +```bash +# Convert to 16-bit PCM, 44100 Hz, stereo +ffmpeg -i input.wav -acodec pcm_s16le -ar 44100 -ac 2 output.wav + +# Convert to 16-bit PCM, 22050 Hz, mono (smaller file) +ffmpeg -i input.wav -acodec pcm_s16le -ar 22050 -ac 1 output.wav +``` + +### No Sound on Device + +**Symptom**: Playback succeeds but no sound comes out + +**Possible causes:** + +1. **Volume set to 0** + ```python + AudioFlinger.set_volume(50) # Set to 50% + ``` + +2. **Wrong audio device selected** + - Check Settings → Advanced Settings → Audio Device + - Try "Auto-detect" or manually select device + - **Restart required** after changing audio device + +3. **Hardware not available (desktop)** + ```python + # Desktop builds have no audio hardware + # AudioFlinger will return success but produce no sound + ``` + +4. **I2C/Speaker not connected (Fri3d badge)** + - Check if speaker is properly connected + - Verify I2S pins are not used by other peripherals + +### Audio Cuts Out or Stutters + +**Symptom**: Playback starts but stops unexpectedly or has gaps + +**Cause**: Higher priority stream interrupting, or insufficient memory + +**Solution**: +1. **Check for interrupting streams:** + ```python + # Avoid notifications during music playback + # Or use callbacks to resume after interruption + ``` + +2. **Reduce memory usage:** + - Close unused apps + - Use lower bitrate WAV files (22050 Hz instead of 44100 Hz) + - Free up memory with `gc.collect()` + +3. **Check SD card speed:** + - Use Class 10 or faster SD card for smooth WAV playback + +### Restart Required After Configuration Change + +**Symptom**: Changed audio device in Settings but still using old device + +**Cause**: Audio device is initialized at boot time + +**Solution**: Restart the device after changing audio device preference + +```python +# After changing setting, restart is required +# Settings app should display a message: "Restart required to apply changes" +``` + +### RTTTL Not Playing on Waveshare + +**Symptom**: RTTTL tones don't play on Waveshare board + +**Cause**: Waveshare board has no buzzer (PWM speaker) + +**Solution**: +- Use WAV files instead of RTTTL on Waveshare +- RTTTL requires hardware buzzer (Fri3d badge only) +- I2S cannot produce RTTTL tones + +## Performance Tips + +### Optimizing WAV Files + +- **File size**: Use 22050 Hz instead of 44100 Hz to reduce file size by 50% +- **Channels**: Use mono instead of stereo if positional audio isn't needed +- **Bit depth**: Use 16-bit for good quality with reasonable size + +```bash +# Small file, good quality (recommended) +ffmpeg -i input.wav -acodec pcm_s16le -ar 22050 -ac 1 output.wav + +# High quality, larger file +ffmpeg -i input.wav -acodec pcm_s16le -ar 44100 -ac 2 output.wav +``` + +### Background Playback + +AudioFlinger runs in a separate thread, so playback doesn't block the UI: + +```python +# This doesn't block - returns immediately +AudioFlinger.play_wav("long_song.wav", stream_type=AudioFlinger.STREAM_MUSIC) + +# UI remains responsive while audio plays +# Use on_complete callback to know when finished +AudioFlinger.play_wav( + "song.wav", + on_complete=lambda msg: print("Playback finished") +) +``` + +### Memory Management + +- WAV files are streamed from SD card (not loaded into RAM) +- Buffers are allocated during playback and freed after +- Multiple simultaneous streams not supported (priority system prevents this) + +## See Also + +- [Creating Apps](../apps/creating-apps.md) - Learn how to create MicroPythonOS apps +- [LightsManager](lights-manager.md) - LED control for visual feedback +- [SharedPreferences](preferences.md) - Store audio preferences diff --git a/docs/frameworks/download-manager.md b/docs/frameworks/download-manager.md new file mode 100644 index 0000000..db2bbd2 --- /dev/null +++ b/docs/frameworks/download-manager.md @@ -0,0 +1,672 @@ +# DownloadManager + +MicroPythonOS provides a centralized HTTP download service called **DownloadManager** that handles async file downloads with support for progress tracking, streaming, and automatic session management. + +## Overview + +DownloadManager provides: + +✅ **Three output modes** - Download to memory, file, or stream with callbacks +✅ **Automatic session management** - Shared aiohttp session with connection reuse +✅ **Thread-safe** - Safe for concurrent downloads across apps +✅ **Progress tracking** - Real-time download progress callbacks +✅ **Retry logic** - Automatic retry on chunk failures (3 attempts) +✅ **Resume support** - HTTP Range headers for partial downloads +✅ **Memory efficient** - Chunked downloads (1KB chunks) + +## Quick Start + +### Download to Memory + +```python +from mpos import TaskManager, DownloadManager + +class MyActivity(Activity): + def onCreate(self): + TaskManager.create_task(self.fetch_data()) + + async def fetch_data(self): + # Download JSON data + data = await DownloadManager.download_url( + "https://api.example.com/data.json" + ) + + if data: + import json + parsed = json.loads(data) + print(f"Got {len(parsed)} items") + else: + print("Download failed") +``` + +**Returns:** +- `bytes` - Downloaded content on success +- `None` - On failure (network error, HTTP error, timeout) + +### Download to File + +```python +async def download_app(self, url): + # Download .mpk file + success = await DownloadManager.download_url( + "https://apps.micropythonos.com/app.mpk", + outfile="/sdcard/app.mpk" + ) + + if success: + print("Download complete!") + else: + print("Download failed") +``` + +**Returns:** +- `True` - File downloaded successfully +- `False` - Download failed + +### Download with Progress + +```python +async def download_with_progress(self): + progress_bar = lv.bar(self.screen) + progress_bar.set_range(0, 100) + + async def update_progress(percent): + progress_bar.set_value(percent, lv.ANIM.ON) + print(f"Downloaded: {percent}%") + + success = await DownloadManager.download_url( + "https://example.com/large_file.bin", + outfile="/sdcard/large_file.bin", + progress_callback=update_progress + ) +``` + +**Progress callback:** +- Called with percentage (0-100) as integer +- Must be an async function +- Called after each chunk is downloaded + +### Streaming with Callbacks + +```python +async def stream_download(self): + processed_bytes = 0 + + async def process_chunk(chunk): + nonlocal processed_bytes + # Process each chunk as it arrives + processed_bytes += len(chunk) + print(f"Processed {processed_bytes} bytes") + # Could write to custom location, parse, etc. + + success = await DownloadManager.download_url( + "https://example.com/stream", + chunk_callback=process_chunk + ) +``` + +**Chunk callback:** +- Called for each 1KB chunk received +- Must be an async function +- Cannot be used with `outfile` parameter + +## API Reference + +### `DownloadManager.download_url()` + +Download a URL with flexible output modes. + +```python +async def download_url(url, outfile=None, total_size=None, + progress_callback=None, chunk_callback=None, + headers=None) +``` + +**Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `url` | str | URL to download (required) | +| `outfile` | str | Path to write file (optional) | +| `total_size` | int | Expected size in bytes for progress tracking (optional) | +| `progress_callback` | async function | Callback for progress updates (optional) | +| `chunk_callback` | async function | Callback for streaming chunks (optional) | +| `headers` | dict | Custom HTTP headers (optional) | + +**Returns:** + +- **Memory mode** (no `outfile` or `chunk_callback`): `bytes` on success, `None` on failure +- **File mode** (`outfile` provided): `True` on success, `False` on failure +- **Stream mode** (`chunk_callback` provided): `True` on success, `False` on failure + +**Raises:** + +- `ValueError` - If both `outfile` and `chunk_callback` are provided + +**Example:** +```python +# Memory mode +data = await DownloadManager.download_url("https://example.com/data.json") + +# File mode +success = await DownloadManager.download_url( + "https://example.com/file.bin", + outfile="/sdcard/file.bin" +) + +# Stream mode +async def process(chunk): + print(f"Got {len(chunk)} bytes") + +success = await DownloadManager.download_url( + "https://example.com/stream", + chunk_callback=process +) +``` + +### Helper Functions + +#### `DownloadManager.is_session_active()` + +Check if an HTTP session is currently active. + +**Returns:** +- `bool` - True if session exists + +**Example:** +```python +if DownloadManager.is_session_active(): + print("Session active") +``` + +#### `DownloadManager.close_session()` + +Explicitly close the HTTP session (rarely needed). + +**Returns:** +- None (awaitable) + +**Example:** +```python +await DownloadManager.close_session() +``` + +**Note:** Sessions are automatically managed. This is mainly for testing. + +## Common Patterns + +### Download with Timeout + +```python +from mpos import TaskManager, DownloadManager + +async def download_with_timeout(self, url, timeout=10): + try: + data = await TaskManager.wait_for( + DownloadManager.download_url(url), + timeout=timeout + ) + return data + except asyncio.TimeoutError: + print(f"Download timed out after {timeout}s") + return None +``` + +### Download Multiple Files Concurrently + +```python +async def download_icons(self, apps): + """Download app icons concurrently with individual timeouts""" + for app in apps: + if not app.icon_data: + try: + app.icon_data = await TaskManager.wait_for( + DownloadManager.download_url(app.icon_url), + timeout=5 # 5 seconds per icon + ) + except Exception as e: + print(f"Icon download failed: {e}") + continue + + # Update UI with icon + if app.icon_data: + self.update_icon_display(app) +``` + +### Download with Explicit Size + +```python +async def download_mpk(self, app): + """Download app package with known size""" + await DownloadManager.download_url( + app.download_url, + outfile=f"/sdcard/{app.fullname}.mpk", + total_size=app.download_url_size, # Use known size + progress_callback=self.update_progress + ) +``` + +**Benefits of providing `total_size`:** +- More accurate progress percentages +- Avoids default 100KB assumption +- Better user experience + +### Resume Partial Download + +```python +import os + +async def resume_download(self, url, outfile): + """Resume a partial download using Range headers""" + bytes_written = 0 + + # Check if partial file exists + try: + bytes_written = os.stat(outfile)[6] # File size + print(f"Resuming from {bytes_written} bytes") + except OSError: + print("Starting new download") + + # Download remaining bytes + success = await DownloadManager.download_url( + url, + outfile=outfile, + headers={'Range': f'bytes={bytes_written}-'} + ) + + return success +``` + +**Note:** Server must support HTTP Range requests. + +### Error Handling + +```python +async def robust_download(self, url): + """Download with comprehensive error handling""" + try: + data = await DownloadManager.download_url(url) + + if data is None: + print("Download failed (network error or HTTP error)") + return None + + if len(data) == 0: + print("Warning: Downloaded empty file") + + return data + + except ValueError as e: + print(f"Invalid parameters: {e}") + return None + except Exception as e: + print(f"Unexpected error: {e}") + return None +``` + +## Session Management + +### Automatic Lifecycle + +DownloadManager automatically manages the aiohttp session: + +1. **Lazy initialization**: Session created on first download +2. **Connection reuse**: HTTP keep-alive for performance +3. **Automatic cleanup**: Session cleared when idle +4. **Thread-safe**: Safe for concurrent downloads + +### Session Behavior + +```python +# First download creates session +data1 = await DownloadManager.download_url(url1) +# Session is now active + +# Second download reuses session (faster) +data2 = await DownloadManager.download_url(url2) +# HTTP keep-alive connection reused + +# After download completes, session auto-closes if idle +# (no refcount - session cleared) +``` + +**Performance benefits:** +- **Connection reuse**: Avoid TCP handshake overhead +- **Shared session**: One session across all apps +- **Memory efficient**: Session cleared when not in use + +## Progress Tracking + +### Progress Calculation + +Progress is calculated based on: +1. **Content-Length header** (if provided by server) +2. **Explicit `total_size` parameter** (overrides header) +3. **Default assumption** (100KB if neither available) + +```python +async def download_with_unknown_size(self): + """Handle download without Content-Length""" + downloaded_bytes = [0] + + async def track_progress(percent): + # Percent may be inaccurate if size unknown + print(f"Progress: {percent}%") + + data = await DownloadManager.download_url( + url_without_content_length, + progress_callback=track_progress + ) +``` + +### Progress Callback Signature + +```python +async def progress_callback(percent: int): + """ + Args: + percent: Progress percentage (0-100) + """ + # Update UI, log, etc. + self.progress_bar.set_value(percent, lv.ANIM.ON) +``` + +## Retry Logic + +DownloadManager automatically retries failed chunk reads: + +- **Retry count**: 3 attempts per chunk +- **Timeout per attempt**: 10 seconds +- **Exponential backoff**: No (immediate retry) + +```python +# Automatic retry example +while tries_left > 0: + try: + chunk = await response.content.read(1024) + break # Success + except Exception as e: + print(f"Chunk read error: {e}") + tries_left -= 1 + +if tries_left == 0: + # All retries failed - abort download + return False +``` + +**Retry behavior:** +- Network hiccup: Automatic retry +- Permanent failure: Returns False/None after 3 attempts +- Partial download: File closed, may need cleanup + +## HTTP Headers + +### Custom Headers + +```python +# Example: Custom user agent +await DownloadManager.download_url( + url, + headers={ + 'User-Agent': 'MicroPythonOS/0.3.3', + 'Accept': 'application/json' + } +) + +# Example: API authentication +await DownloadManager.download_url( + api_url, + headers={ + 'Authorization': 'Bearer YOUR_TOKEN' + } +) +``` + +### Range Requests + +```python +# Download specific byte range +await DownloadManager.download_url( + url, + headers={ + 'Range': 'bytes=1000-2000' # Download bytes 1000-2000 + } +) + +# Resume from byte 5000 +await DownloadManager.download_url( + url, + outfile=partial_file, + headers={ + 'Range': 'bytes=5000-' # Download from byte 5000 to end + } +) +``` + +## Performance Considerations + +### Memory Usage + +**Per download:** +- Chunk buffer: 1KB +- Progress callback overhead: ~50 bytes +- Total: ~1-2KB per concurrent download + +**Shared:** +- aiohttp session: ~2KB +- Total baseline: ~3KB + +### Concurrent Downloads + +```python +# Good: Limited concurrency +async def download_batch(self, urls): + max_concurrent = 5 + for i in range(0, len(urls), max_concurrent): + batch = urls[i:i+max_concurrent] + results = [] + for url in batch: + try: + data = await TaskManager.wait_for( + DownloadManager.download_url(url), + timeout=10 + ) + results.append(data) + except Exception as e: + results.append(None) + # Process batch results +``` + +**Guidelines:** +- **Limit concurrent downloads**: 5-10 max recommended +- **Use timeouts**: Prevent stuck downloads +- **Handle failures**: Don't crash on individual failures +- **Monitor memory**: Check free RAM on device + +### Chunk Size + +Fixed at 1KB for balance between: +- **Memory**: Small chunks use less RAM +- **Performance**: Larger chunks reduce overhead +- **Responsiveness**: Small chunks = frequent progress updates + +## Troubleshooting + +### Download Fails (Returns None/False) + +**Possible causes:** +1. Network not connected +2. Invalid URL +3. HTTP error (404, 500, etc.) +4. Server timeout +5. SSL/TLS error + +**Solution:** +```python +# Check network first +try: + import network + if not network.WLAN(network.STA_IF).isconnected(): + print("WiFi not connected!") + return +except ImportError: + pass # Desktop mode + +# Download with error handling +data = await DownloadManager.download_url(url) +if data is None: + print("Download failed - check network and URL") +``` + +### Progress Callback Not Called + +**Possible causes:** +1. Server doesn't send Content-Length +2. total_size not provided +3. Download too fast (single chunk) + +**Solution:** +```python +# Provide explicit size +await DownloadManager.download_url( + url, + total_size=expected_size, # Provide if known + progress_callback=callback +) +``` + +### Memory Leak + +**Problem:** Memory usage grows over time + +**Cause:** Large files downloaded to memory + +**Solution:** +```python +# Bad: Download large file to memory +data = await DownloadManager.download_url(large_url) # OOM! + +# Good: Download to file +success = await DownloadManager.download_url( + large_url, + outfile="/sdcard/large.bin" # Streams to disk +) +``` + +### File Not Created + +**Possible causes:** +1. Directory doesn't exist +2. Insufficient storage space +3. Permission error + +**Solution:** +```python +import os + +# Ensure directory exists +try: + os.mkdir("/sdcard/downloads") +except OSError: + pass # Already exists + +# Check available space +# (No built-in function - monitor manually) + +# Download with error handling +success = await DownloadManager.download_url( + url, + outfile="/sdcard/downloads/file.bin" +) + +if not success: + print("Download failed - check storage and permissions") +``` + +### ValueError Exception + +**Cause:** Both `outfile` and `chunk_callback` provided + +**Solution:** +```python +# Bad: Conflicting parameters +await DownloadManager.download_url( + url, + outfile="file.bin", + chunk_callback=process # ERROR! +) + +# Good: Choose one output mode +await DownloadManager.download_url( + url, + outfile="file.bin" # File mode +) + +# Or: +await DownloadManager.download_url( + url, + chunk_callback=process # Stream mode +) +``` + +## Implementation Details + +**Location**: `/home/user/MicroPythonOS/internal_filesystem/lib/mpos/net/download_manager.py` + +**Pattern**: Module-level singleton (similar to AudioFlinger, SensorManager) + +**Key features:** +- **Session pooling**: Single shared aiohttp.ClientSession +- **Refcount tracking**: Session lifetime based on active downloads +- **Thread safety**: Uses `_thread.allocate_lock()` for session access +- **Graceful degradation**: Returns None/False on desktop if aiohttp unavailable + +**Dependencies:** +- `aiohttp` - HTTP client library (MicroPython port) +- `mpos.TaskManager` - For timeout handling (`wait_for`) + +## Migration from Direct aiohttp + +If you're currently using aiohttp directly: + +```python +# Old: Direct aiohttp usage +import aiohttp + +class MyApp(Activity): + def onCreate(self): + self.session = aiohttp.ClientSession() + + async def download(self, url): + async with self.session.get(url) as response: + return await response.read() + + def onDestroy(self, screen): + # Bug: Can't await in non-async method! + await self.session.close() +``` + +```python +# New: DownloadManager +from mpos import DownloadManager + +class MyApp(Activity): + # No session management needed! + + async def download(self, url): + return await DownloadManager.download_url(url) + + # No onDestroy cleanup needed! +``` + +**Benefits:** +- No session lifecycle management +- Automatic connection reuse +- Thread-safe +- Consistent error handling + +## See Also + +- [TaskManager](task-manager.md) - Async task management +- [Preferences](preferences.md) - Persistent configuration storage +- [SensorManager](sensor-manager.md) - Sensor data access diff --git a/docs/frameworks/lights-manager.md b/docs/frameworks/lights-manager.md new file mode 100644 index 0000000..cb88862 --- /dev/null +++ b/docs/frameworks/lights-manager.md @@ -0,0 +1,652 @@ +# LightsManager + +MicroPythonOS provides a simple LED control service called **LightsManager** for NeoPixel RGB LEDs on supported hardware. + +## Overview + +LightsManager provides: + +✅ **One-shot LED control** - Direct control of individual or all LEDs +✅ **Buffered updates** - Set multiple LEDs then apply all changes at once +✅ **Hardware abstraction** - Same API works across all boards +✅ **Predefined colors** - Quick access to common notification colors +✅ **Frame-based animations** - Integrate with TaskHandler for smooth animations +✅ **Low overhead** - Lightweight singleton pattern + +⚠️ **Note**: LightsManager provides primitives for LED control, not built-in animations. Apps implement custom animations using the `update_frame()` pattern. + +## Hardware Support + +| Board | LEDs | GPIO | Notes | +|-------|------|------|-------| +| **Fri3d 2024 Badge** | ✅ 5 NeoPixel RGB | GPIO 12 | Full support | +| **Waveshare ESP32-S3** | ❌ None | N/A | No LED hardware | +| **Desktop/Linux** | ❌ None | N/A | Functions return `False` | + +## Quick Start + +### Check Availability + +```python +from mpos.app.activity import Activity +import mpos.lights as LightsManager + +class MyActivity(Activity): + def onCreate(self): + if LightsManager.is_available(): + print(f"LEDs available: {LightsManager.get_led_count()}") + else: + print("No LED hardware on this device") +``` + +### Control Individual LEDs + +```python +# Set LED 0 to red (buffered) +LightsManager.set_led(0, 255, 0, 0) + +# Set LED 1 to green (buffered) +LightsManager.set_led(1, 0, 255, 0) + +# Set LED 2 to blue (buffered) +LightsManager.set_led(2, 0, 0, 255) + +# Apply all changes to hardware +LightsManager.write() +``` + +**Important**: LEDs are **buffered**. Changes won't appear until you call `write()`. + +### Control All LEDs + +```python +# Set all LEDs to blue +LightsManager.set_all(0, 0, 255) +LightsManager.write() + +# Turn off all LEDs +LightsManager.clear() +LightsManager.write() +``` + +### Notification Colors + +Quick shortcuts for common colors: + +```python +# Convenience method (sets all LEDs + calls write()) +LightsManager.set_notification_color("red") # Success/Error +LightsManager.set_notification_color("green") # Success +LightsManager.set_notification_color("blue") # Info +LightsManager.set_notification_color("yellow") # Warning +LightsManager.set_notification_color("orange") # Alert +LightsManager.set_notification_color("purple") # Special +LightsManager.set_notification_color("white") # General +``` + +## Custom Animations + +LightsManager provides one-shot control - apps create animations by updating LEDs over time. + +### Blink Pattern + +Simple on/off blinking: + +```python +import time +import mpos.lights as LightsManager + +def blink_pattern(): + """Blink all LEDs red 5 times.""" + for _ in range(5): + # Turn on + LightsManager.set_all(255, 0, 0) + LightsManager.write() + time.sleep_ms(200) + + # Turn off + LightsManager.clear() + LightsManager.write() + time.sleep_ms(200) +``` + +### Rainbow Cycle + +Display a rainbow pattern across all LEDs: + +```python +def rainbow_cycle(): + """Set each LED to a different rainbow color.""" + colors = [ + (255, 0, 0), # Red + (255, 128, 0), # Orange + (255, 255, 0), # Yellow + (0, 255, 0), # Green + (0, 0, 255), # Blue + ] + + for i, color in enumerate(colors): + LightsManager.set_led(i, *color) + + LightsManager.write() +``` + +### Chase Effect + +LEDs light up in sequence: + +```python +import time + +def chase_effect(color=(0, 255, 0), delay_ms=100, loops=3): + """Light up LEDs in sequence.""" + led_count = LightsManager.get_led_count() + + for _ in range(loops): + for i in range(led_count): + # Clear all + LightsManager.clear() + + # Light current LED + LightsManager.set_led(i, *color) + LightsManager.write() + + time.sleep_ms(delay_ms) + + # Clear after animation + LightsManager.clear() + LightsManager.write() +``` + +### Pulse/Breathing Effect + +Fade LEDs in and out smoothly: + +```python +import time + +def pulse_effect(color=(0, 0, 255), duration_ms=2000): + """Pulse LEDs with breathing effect.""" + steps = 20 # Number of brightness steps + delay = duration_ms // (steps * 2) # Time per step + + # Fade in + for i in range(steps): + brightness = i / steps + r = int(color[0] * brightness) + g = int(color[1] * brightness) + b = int(color[2] * brightness) + + LightsManager.set_all(r, g, b) + LightsManager.write() + time.sleep_ms(delay) + + # Fade out + for i in range(steps, 0, -1): + brightness = i / steps + r = int(color[0] * brightness) + g = int(color[1] * brightness) + b = int(color[2] * brightness) + + LightsManager.set_all(r, g, b) + LightsManager.write() + time.sleep_ms(delay) + + # Clear + LightsManager.clear() + LightsManager.write() +``` + +### Random Sparkle + +Random LEDs flash briefly: + +```python +import time +import random + +def sparkle_effect(duration_ms=5000): + """Random LEDs sparkle.""" + led_count = LightsManager.get_led_count() + start_time = time.ticks_ms() + + while time.ticks_diff(time.ticks_ms(), start_time) < duration_ms: + # Pick random LED + led = random.randint(0, led_count - 1) + + # Random color + r = random.randint(0, 255) + g = random.randint(0, 255) + b = random.randint(0, 255) + + # Flash on + LightsManager.clear() + LightsManager.set_led(led, r, g, b) + LightsManager.write() + time.sleep_ms(100) + + # Flash off + LightsManager.clear() + LightsManager.write() + time.sleep_ms(50) + + # Clear after animation + LightsManager.clear() + LightsManager.write() +``` + +## Frame-Based LED Animations + +For smooth, real-time animations that integrate with the game loop, use the **TaskHandler event system**: + +```python +from mpos.app.activity import Activity +import mpos.ui +import mpos.lights as LightsManager +import time + +class LEDAnimationActivity(Activity): + def onCreate(self): + self.screen = lv.obj() + self.last_time = 0 + self.led_index = 0 + self.setContentView(self.screen) + + def onResume(self, screen): + """Start animation when app resumes.""" + self.last_time = time.ticks_ms() + mpos.ui.task_handler.add_event_cb(self.update_frame, 1) + + def onPause(self, screen): + """Stop animation and clear LEDs when app pauses.""" + mpos.ui.task_handler.remove_event_cb(self.update_frame) + LightsManager.clear() + LightsManager.write() + + def update_frame(self, a, b): + """Called every frame - update animation.""" + current_time = time.ticks_ms() + delta_time = time.ticks_diff(current_time, self.last_time) / 1000.0 + + # Update every 0.5 seconds (2 Hz) + if delta_time > 0.5: + self.last_time = current_time + + # Clear all LEDs + LightsManager.clear() + + # Light up current LED + LightsManager.set_led(self.led_index, 0, 255, 0) + LightsManager.write() + + # Move to next LED + self.led_index = (self.led_index + 1) % LightsManager.get_led_count() +``` + +### Smooth Color Cycle Animation + +```python +import math + +class ColorCycleActivity(Activity): + def onCreate(self): + self.screen = lv.obj() + self.hue = 0.0 # Color hue (0-360) + self.last_time = time.ticks_ms() + self.setContentView(self.screen) + + def onResume(self, screen): + self.last_time = time.ticks_ms() + mpos.ui.task_handler.add_event_cb(self.update_frame, 1) + + def onPause(self, screen): + mpos.ui.task_handler.remove_event_cb(self.update_frame) + LightsManager.clear() + LightsManager.write() + + def update_frame(self, a, b): + current_time = time.ticks_ms() + delta_time = time.ticks_diff(current_time, self.last_time) / 1000.0 + self.last_time = current_time + + # Rotate hue + self.hue += 120 * delta_time # 120 degrees per second + if self.hue >= 360: + self.hue -= 360 + + # Convert HSV to RGB + r, g, b = self.hsv_to_rgb(self.hue, 1.0, 1.0) + + # Update all LEDs + LightsManager.set_all(r, g, b) + LightsManager.write() + + def hsv_to_rgb(self, h, s, v): + """Convert HSV to RGB (h: 0-360, s: 0-1, v: 0-1).""" + h = h / 60.0 + i = int(h) + f = h - i + p = v * (1 - s) + q = v * (1 - s * f) + t = v * (1 - s * (1 - f)) + + if i == 0: + r, g, b = v, t, p + elif i == 1: + r, g, b = q, v, p + elif i == 2: + r, g, b = p, v, t + elif i == 3: + r, g, b = p, q, v + elif i == 4: + r, g, b = t, p, v + else: + r, g, b = v, p, q + + return int(r * 255), int(g * 255), int(b * 255) +``` + +## API Reference + +### Functions + +**`is_available()`** + +Check if LED hardware is available. + +- **Returns:** `bool` - `True` if LEDs are available, `False` otherwise + +**`get_led_count()`** + +Get the number of available LEDs. + +- **Returns:** `int` - Number of LEDs (5 on Fri3d badge, 0 elsewhere) + +**`set_led(index, r, g, b)`** + +Set a single LED color (buffered). + +- **Parameters:** + - `index` (int): LED index (0 to led_count-1) + - `r` (int): Red component (0-255) + - `g` (int): Green component (0-255) + - `b` (int): Blue component (0-255) + +**`set_all(r, g, b)`** + +Set all LEDs to the same color (buffered). + +- **Parameters:** + - `r` (int): Red component (0-255) + - `g` (int): Green component (0-255) + - `b` (int): Blue component (0-255) + +**`clear()`** + +Turn off all LEDs (buffered). Equivalent to `set_all(0, 0, 0)`. + +**`write()`** + +Apply buffered LED changes to hardware. **Required** - changes won't appear until you call this. + +**`set_notification_color(color_name)`** + +Set all LEDs to a predefined color and immediately apply (convenience method). + +- **Parameters:** + - `color_name` (str): Color name ("red", "green", "blue", "yellow", "orange", "purple", "white") + +### Predefined Colors + +| Color Name | RGB Value | Use Case | +|------------|-----------|----------| +| `"red"` | (255, 0, 0) | Error, alert | +| `"green"` | (0, 255, 0) | Success, ready | +| `"blue"` | (0, 0, 255) | Info, processing | +| `"yellow"` | (255, 255, 0) | Warning, caution | +| `"orange"` | (255, 128, 0) | Alert, attention | +| `"purple"` | (255, 0, 255) | Special, unique | +| `"white"` | (255, 255, 255) | General, neutral | + +## Performance Tips + +### Buffering for Efficiency + +**❌ Bad** - Calling `write()` after each LED: +```python +for i in range(5): + LightsManager.set_led(i, 255, 0, 0) + LightsManager.write() # 5 hardware updates! +``` + +**✅ Good** - Set all LEDs then write once: +```python +for i in range(5): + LightsManager.set_led(i, 255, 0, 0) + +LightsManager.write() # 1 hardware update +``` + +### Update Rate Recommendations + +- **Static displays**: Update once, no frame loop needed +- **Notifications**: Update when event occurs +- **Smooth animations**: 20-30 Hz (every 33-50ms) +- **Fast effects**: Up to 60 Hz (but uses more power) + +**Example with rate limiting:** +```python +UPDATE_INTERVAL = 0.05 # 20 Hz (50ms) + +def update_frame(self, a, b): + current_time = time.ticks_ms() + delta_time = time.ticks_diff(current_time, self.last_time) / 1000.0 + + if delta_time >= UPDATE_INTERVAL: + self.last_time = current_time + # Update LEDs here + LightsManager.write() +``` + +### Cleanup in onPause() + +Always clear LEDs when your app exits: + +```python +def onPause(self, screen): + # Stop animations + mpos.ui.task_handler.remove_event_cb(self.update_frame) + + # Clear LEDs + LightsManager.clear() + LightsManager.write() +``` + +This prevents LEDs from staying lit after your app exits. + +## Troubleshooting + +### LEDs Not Updating + +**Symptom**: Called `set_led()` or `set_all()` but LEDs don't change + +**Cause**: Forgot to call `write()` to apply changes + +**Solution**: Always call `write()` after setting LED colors + +```python +# ❌ Wrong - no write() +LightsManager.set_all(255, 0, 0) + +# ✅ Correct +LightsManager.set_all(255, 0, 0) +LightsManager.write() +``` + +### Flickering LEDs + +**Symptom**: LEDs flicker or show inconsistent colors during animation + +**Possible causes:** + +1. **Update rate too high** + ```python + # ❌ Bad - updating every frame (60 Hz) + def update_frame(self, a, b): + LightsManager.set_all(random_color()) + LightsManager.write() # Too frequent! + + # ✅ Good - rate limited to 20 Hz + def update_frame(self, a, b): + if delta_time >= 0.05: # 50ms = 20 Hz + LightsManager.set_all(random_color()) + LightsManager.write() + ``` + +2. **Inconsistent timing** + ```python + # ✅ Use delta time for smooth animations + delta_time = time.ticks_diff(current_time, self.last_time) / 1000.0 + ``` + +3. **Race condition with multiple updates** + - Only update LEDs from one place (e.g., single update_frame callback) + - Avoid updating from multiple threads + +### LEDs Stay On After App Exits + +**Symptom**: LEDs remain lit when you leave the app + +**Cause**: Didn't clear LEDs in `onPause()` + +**Solution**: Always implement cleanup in `onPause()` + +```python +def onPause(self, screen): + # Stop animation callback + mpos.ui.task_handler.remove_event_cb(self.update_frame) + + # Clear LEDs + LightsManager.clear() + LightsManager.write() +``` + +### No LEDs on Waveshare Board + +**Symptom**: `is_available()` returns `False` on Waveshare + +**Cause**: Waveshare ESP32-S3-Touch-LCD-2 has no NeoPixel LEDs + +**Solution**: +- Check hardware before using LEDs: + ```python + if LightsManager.is_available(): + # Use LEDs + else: + # Fallback to screen indicators or sounds + ``` + +### Colors Look Wrong + +**Symptom**: LEDs show unexpected colors + +**Possible causes:** + +1. **RGB order confusion** - Make sure you're using (R, G, B) order: + ```python + LightsManager.set_led(0, 255, 0, 0) # Red (R=255, G=0, B=0) + LightsManager.set_led(1, 0, 255, 0) # Green (R=0, G=255, B=0) + LightsManager.set_led(2, 0, 0, 255) # Blue (R=0, G=0, B=255) + ``` + +2. **Value out of range** - RGB values must be 0-255: + ```python + # ❌ Wrong - values > 255 wrap around + LightsManager.set_led(0, 300, 0, 0) + + # ✅ Correct - clamp to 0-255 + LightsManager.set_led(0, min(300, 255), 0, 0) + ``` + +## Complete Example: LED Status Indicator + +```python +from mpos.app.activity import Activity +import mpos.lights as LightsManager +import lvgl as lv + +class StatusIndicatorActivity(Activity): + def onCreate(self): + self.screen = lv.obj() + + # Buttons for different statuses + success_btn = lv.button(self.screen) + success_btn.set_size(100, 50) + success_btn.set_pos(10, 10) + lv.label(success_btn).set_text("Success") + success_btn.add_event_cb( + lambda e: self.show_status("success"), + lv.EVENT.CLICKED, + None + ) + + error_btn = lv.button(self.screen) + error_btn.set_size(100, 50) + error_btn.set_pos(120, 10) + lv.label(error_btn).set_text("Error") + error_btn.add_event_cb( + lambda e: self.show_status("error"), + lv.EVENT.CLICKED, + None + ) + + warning_btn = lv.button(self.screen) + warning_btn.set_size(100, 50) + warning_btn.set_pos(10, 70) + lv.label(warning_btn).set_text("Warning") + warning_btn.add_event_cb( + lambda e: self.show_status("warning"), + lv.EVENT.CLICKED, + None + ) + + clear_btn = lv.button(self.screen) + clear_btn.set_size(100, 50) + clear_btn.set_pos(120, 70) + lv.label(clear_btn).set_text("Clear") + clear_btn.add_event_cb( + lambda e: self.show_status("clear"), + lv.EVENT.CLICKED, + None + ) + + self.setContentView(self.screen) + + def show_status(self, status_type): + """Show visual status with LEDs.""" + if not LightsManager.is_available(): + print("No LEDs available") + return + + if status_type == "success": + LightsManager.set_notification_color("green") + elif status_type == "error": + LightsManager.set_notification_color("red") + elif status_type == "warning": + LightsManager.set_notification_color("yellow") + elif status_type == "clear": + LightsManager.clear() + LightsManager.write() + + def onPause(self, screen): + # Clear LEDs when leaving app + if LightsManager.is_available(): + LightsManager.clear() + LightsManager.write() +``` + +## See Also + +- [Creating Apps](../apps/creating-apps.md) - Learn how to create MicroPythonOS apps +- [AudioFlinger](audioflinger.md) - Audio control for sound feedback +- [SensorManager](sensor-manager.md) - Sensor integration for interactive effects diff --git a/docs/frameworks/preferences.md b/docs/frameworks/preferences.md new file mode 100644 index 0000000..1b0911a --- /dev/null +++ b/docs/frameworks/preferences.md @@ -0,0 +1,125 @@ +MicroPythonOS provides a simple way to load and save preferences, similar to Android's "SharedPreferences" framework. + +Here's a simple example of how to add it to your app, taken from [QuasiNametag](https://github.com/QuasiKili/MPOS-QuasiNametag): + +
+```
+--- quasinametag.py.orig 2025-10-29 12:24:27.494193748 +0100
++++ quasinametag.py 2025-10-29 12:07:59.357264302 +0100
+@@ -1,4 +1,5 @@
+ from mpos.apps import Activity
++import mpos.config
+ import mpos.ui
+ import mpos.ui.anim
+ import mpos.ui.focus_direction
+@@ -42,6 +43,12 @@
+ # Add key event handler to container to catch all key events
+ container.add_event_cb(self.global_key_handler, lv.EVENT.KEY, None)
+
++ print("Loading preferences...")
++ prefs = mpos.config.SharedPreferences("com.quasikili.quasinametag")
++ self.name_text = prefs.get_string("name_text", self.name_text)
++ self.fg_color = prefs.get_int("fg_color", self.fg_color)
++ self.bg_color = prefs.get_int("bg_color", self.bg_color)
++
+ # Create both screens as children of the container
+ self.create_edit_screen(container)
+ self.create_display_screen(container)
+@@ -263,6 +270,13 @@
+ if focusgroup:
+ mpos.ui.focus_direction.emulate_focus_obj(focusgroup, self.display_screen)
+
++ print("Saving preferences...")
++ editor = mpos.config.SharedPreferences("com.quasikili.quasinametag").edit()
++ editor.put_string("name_text", self.name_text)
++ editor.put_int("fg_color", self.fg_color)
++ editor.put_int("bg_color", self.bg_color)
++ editor.commit()
++
+ def update_display_screen(self):
+ # Set background color
+ self.display_screen.set_style_bg_color(lv.color_hex(self.bg_color), 0)
+```
+
+
+Here's a more complete example:
+
+
+```
+# Example usage with access_points as a dictionary
+def main():
+ # Initialize SharedPreferences
+ prefs = SharedPreferences("com.example.test_shared_prefs")
+
+ # Save some simple settings and a dictionary-based access_points
+ editor = prefs.edit()
+ editor.put_string("someconfig", "somevalue")
+ editor.put_int("othervalue", 54321)
+ editor.put_dict("access_points", {
+ "example_ssid1": {"password": "examplepass1", "detail": "yes please", "numericalconf": 1234},
+ "example_ssid2": {"password": "examplepass2", "detail": "no please", "numericalconf": 9875}
+ })
+ editor.apply()
+
+ # Read back the settings
+ print("Simple settings:")
+ print("someconfig:", prefs.get_string("someconfig", "default_value"))
+ print("othervalue:", prefs.get_int("othervalue", 0))
+
+ print("\nAccess points (dictionary-based):")
+ ssids = prefs.get_dict_keys("access_points")
+ for ssid in ssids:
+ print(f"Access Point SSID: {ssid}")
+ print(f" Password: {prefs.get_dict_item_field('access_points', ssid, 'password', 'N/A')}")
+ print(f" Detail: {prefs.get_dict_item_field('access_points', ssid, 'detail', 'N/A')}")
+ print(f" Numerical Conf: {prefs.get_dict_item_field('access_points', ssid, 'numericalconf', 0)}")
+ print(f" Full config: {prefs.get_dict_item('access_points', ssid)}")
+
+ # Add a new access point
+ editor = prefs.edit()
+ editor.put_dict_item("access_points", "example_ssid3", {
+ "password": "examplepass3",
+ "detail": "maybe",
+ "numericalconf": 5555
+ })
+ editor.commit()
+
+ # Update an existing access point
+ editor = prefs.edit()
+ editor.put_dict_item("access_points", "example_ssid1", {
+ "password": "newpass1",
+ "detail": "updated please",
+ "numericalconf": 4321
+ })
+ editor.commit()
+
+ # Remove an access point
+ editor = prefs.edit()
+ editor.remove_dict_item("access_points", "example_ssid2")
+ editor.commit()
+
+ # Read updated access points
+ print("\nUpdated access points (dictionary-based):")
+ ssids = prefs.get_dict_keys("access_points")
+ for ssid in ssids:
+ print(f"Access Point SSID: {ssid}: {prefs.get_dict_item('access_points', ssid)}")
+
+ # Demonstrate compatibility with list-based configs
+ editor = prefs.edit()
+ editor.put_list("somelist", [
+ {"a": "ok", "numericalconf": 1111},
+ {"a": "not ok", "numericalconf": 2222}
+ ])
+ editor.apply()
+
+ print("\List-based config:")
+ somelist = prefs.get_list("somelist")
+ for i, ap in enumerate(somelist):
+ print(f"List item {i}:")
+ print(f" a: {prefs.get_list_item('somelist', i, 'a', 'N/A')}")
+ print(f" Full dict: {prefs.get_list_item_dict('somelist', i)}")
+
+if __name__ == '__main__':
+ main()
+```
+
diff --git a/docs/frameworks/sensor-manager.md b/docs/frameworks/sensor-manager.md
new file mode 100644
index 0000000..4efd8a4
--- /dev/null
+++ b/docs/frameworks/sensor-manager.md
@@ -0,0 +1,532 @@
+# SensorManager
+
+MicroPythonOS provides a unified sensor framework called **SensorManager**, inspired by Android's SensorManager API. It provides easy access to motion sensors (accelerometer, gyroscope) and temperature sensors across different hardware platforms.
+
+## Overview
+
+SensorManager automatically detects available sensors on your device:
+
+- **QMI8658 IMU** (Waveshare ESP32-S3-Touch-LCD-2)
+- **WSEN_ISDS IMU** (Fri3d Camp 2024 Badge)
+- **ESP32 MCU Temperature** (All ESP32 boards)
+
+The framework handles:
+
+✅ **Auto-detection** - Identifies which IMU is present
+✅ **Unit normalization** - Returns standard SI units (m/s², deg/s, °C)
+✅ **Persistent calibration** - Calibrate once, saved across reboots
+✅ **Thread-safe** - Safe for concurrent access
+✅ **Hardware-agnostic** - Apps work on all platforms without changes
+
+## Sensor Types
+
+```python
+import mpos.sensor_manager as SensorManager
+
+# Motion sensors
+SensorManager.TYPE_ACCELEROMETER # m/s² (meters per second squared)
+SensorManager.TYPE_GYROSCOPE # deg/s (degrees per second)
+
+# Temperature sensors
+SensorManager.TYPE_SOC_TEMPERATURE # °C (MCU internal temperature)
+SensorManager.TYPE_IMU_TEMPERATURE # °C (IMU chip temperature)
+```
+
+## Quick Start
+
+### Basic Usage
+
+```python
+from mpos.app.activity import Activity
+import mpos.sensor_manager as SensorManager
+
+class MyActivity(Activity):
+ def onCreate(self):
+ # Check if sensors are available
+ if not SensorManager.is_available():
+ print("No sensors available")
+ return
+
+ # Get sensors
+ self.accel = SensorManager.get_default_sensor(SensorManager.TYPE_ACCELEROMETER)
+ self.gyro = SensorManager.get_default_sensor(SensorManager.TYPE_GYROSCOPE)
+
+ # Read data
+ accel_data = SensorManager.read_sensor(self.accel)
+ gyro_data = SensorManager.read_sensor(self.gyro)
+
+ if accel_data:
+ ax, ay, az = accel_data # In m/s²
+ print(f"Acceleration: X={ax:.2f}, Y={ay:.2f}, Z={az:.2f} m/s²")
+
+ if gyro_data:
+ gx, gy, gz = gyro_data # In deg/s
+ print(f"Gyroscope: X={gx:.2f}, Y={gy:.2f}, Z={gz:.2f} deg/s")
+```
+
+### Reading Temperature
+
+```python
+# Get MCU internal temperature (most stable)
+temp_sensor = SensorManager.get_default_sensor(SensorManager.TYPE_SOC_TEMPERATURE)
+temperature = SensorManager.read_sensor(temp_sensor)
+print(f"MCU Temperature: {temperature:.1f}°C")
+
+# Get IMU chip temperature
+imu_temp_sensor = SensorManager.get_default_sensor(SensorManager.TYPE_IMU_TEMPERATURE)
+imu_temperature = SensorManager.read_sensor(imu_temp_sensor)
+if imu_temperature:
+ print(f"IMU Temperature: {imu_temperature:.1f}°C")
+```
+
+## Tilt-Controlled Game Example
+
+This example shows how to create a simple tilt-controlled ball game:
+
+```python
+from mpos.app.activity import Activity
+import mpos.sensor_manager as SensorManager
+import mpos.ui
+import lvgl as lv
+import time
+
+class TiltBallActivity(Activity):
+ def onCreate(self):
+ # Create screen
+ self.screen = lv.obj()
+
+ # Check sensors
+ if not SensorManager.is_available():
+ label = lv.label(self.screen)
+ label.set_text("No accelerometer available")
+ label.center()
+ self.setContentView(self.screen)
+ return
+
+ # Get accelerometer
+ self.accel = SensorManager.get_default_sensor(SensorManager.TYPE_ACCELEROMETER)
+
+ # Create ball
+ self.ball = lv.obj(self.screen)
+ self.ball.set_size(20, 20)
+ self.ball.set_style_radius(10, 0) # Make it circular
+ self.ball.set_style_bg_color(lv.color_hex(0xFF0000), 0)
+
+ # Ball physics
+ self.ball_x = 160.0 # Center X
+ self.ball_y = 120.0 # Center Y
+ self.ball_vx = 0.0 # Velocity X
+ self.ball_vy = 0.0 # Velocity Y
+ self.last_time = time.ticks_ms()
+
+ self.setContentView(self.screen)
+
+ def onResume(self, screen):
+ # Start physics updates
+ self.last_time = time.ticks_ms()
+ mpos.ui.task_handler.add_event_cb(self.update_physics, 1)
+
+ def onPause(self, screen):
+ # Stop physics updates
+ mpos.ui.task_handler.remove_event_cb(self.update_physics)
+
+ def update_physics(self, a, b):
+ # Calculate delta time
+ current_time = time.ticks_ms()
+ delta_time = time.ticks_diff(current_time, self.last_time) / 1000.0
+ self.last_time = current_time
+
+ # Read accelerometer (returns m/s²)
+ accel = SensorManager.read_sensor(self.accel)
+ if not accel:
+ return
+
+ ax, ay, az = accel
+
+ # Apply acceleration to velocity (scale down for gameplay)
+ # Tilt right (positive X) → ball moves right
+ # Tilt forward (positive Y) → ball moves down (flip Y)
+ self.ball_vx += (ax * 5.0) * delta_time # Scale factor for gameplay
+ self.ball_vy -= (ay * 5.0) * delta_time # Negative to flip Y
+
+ # Apply friction
+ self.ball_vx *= 0.98
+ self.ball_vy *= 0.98
+
+ # Update position
+ self.ball_x += self.ball_vx
+ self.ball_y += self.ball_vy
+
+ # Bounce off walls
+ if self.ball_x < 10 or self.ball_x > 310:
+ self.ball_vx *= -0.8 # Bounce with energy loss
+ self.ball_x = max(10, min(310, self.ball_x))
+
+ if self.ball_y < 10 or self.ball_y > 230:
+ self.ball_vy *= -0.8
+ self.ball_y = max(10, min(230, self.ball_y))
+
+ # Update ball position
+ self.ball.set_pos(int(self.ball_x) - 10, int(self.ball_y) - 10)
+```
+
+## Gesture Detection Example
+
+Detect device shake and rotation:
+
+```python
+import mpos.sensor_manager as SensorManager
+import math
+
+class GestureDetector(Activity):
+ def onCreate(self):
+ self.accel = SensorManager.get_default_sensor(SensorManager.TYPE_ACCELEROMETER)
+ self.gyro = SensorManager.get_default_sensor(SensorManager.TYPE_GYROSCOPE)
+
+ # Shake detection
+ self.shake_threshold = 15.0 # m/s²
+
+ # Rotation detection
+ self.rotation_threshold = 100.0 # deg/s
+
+ def onResume(self, screen):
+ mpos.ui.task_handler.add_event_cb(self.detect_gestures, 1)
+
+ def detect_gestures(self, a, b):
+ # Detect shake (sudden acceleration)
+ accel = SensorManager.read_sensor(self.accel)
+ if accel:
+ ax, ay, az = accel
+ magnitude = math.sqrt(ax*ax + ay*ay + az*az)
+ gravity = 9.80665
+
+ # Shake = acceleration magnitude significantly different from gravity
+ if abs(magnitude - gravity) > self.shake_threshold:
+ self.on_shake()
+
+ # Detect rotation (spinning device)
+ gyro = SensorManager.read_sensor(self.gyro)
+ if gyro:
+ gx, gy, gz = gyro
+ rotation_speed = math.sqrt(gx*gx + gy*gy + gz*gz)
+
+ if rotation_speed > self.rotation_threshold:
+ self.on_rotate(gx, gy, gz)
+
+ def on_shake(self):
+ print("Device shaken!")
+ # Trigger action (shuffle playlist, undo, etc.)
+
+ def on_rotate(self, gx, gy, gz):
+ print(f"Device rotating: {gx:.1f}, {gy:.1f}, {gz:.1f} deg/s")
+ # Trigger action (rotate view, spin wheel, etc.)
+```
+
+## Calibration
+
+Calibration removes sensor drift and improves accuracy. The device must be **stationary on a flat surface** during calibration.
+
+### Using the Built-in Calibration Tool
+
+The easiest way to calibrate your IMU is through the Settings app:
+
+1. Open **Settings** → **IMU** → **Calibrate IMU**
+2. Place your device on a flat, stable surface
+3. Tap **Calibrate Now**
+4. Keep the device still for ~2 seconds
+5. Done! Calibration is saved automatically
+
+The built-in tool performs stationarity checks and calibrates both the accelerometer and gyroscope with 100 samples each for optimal accuracy.
+
+### Manual Calibration (Programmatic)
+
+```python
+class SettingsActivity(Activity):
+ def calibrate_clicked(self, event):
+ # Get sensors
+ accel = SensorManager.get_default_sensor(SensorManager.TYPE_ACCELEROMETER)
+ gyro = SensorManager.get_default_sensor(SensorManager.TYPE_GYROSCOPE)
+
+ # Show instructions
+ self.status_label.set_text("Place device flat and still...")
+ wait_for_render() # Let UI update
+
+ # Calibrate accelerometer (100 samples)
+ self.status_label.set_text("Calibrating accelerometer...")
+ accel_offsets = SensorManager.calibrate_sensor(accel, samples=100)
+
+ # Calibrate gyroscope
+ self.status_label.set_text("Calibrating gyroscope...")
+ gyro_offsets = SensorManager.calibrate_sensor(gyro, samples=100)
+
+ # Done - calibration is automatically saved
+ self.status_label.set_text(f"Calibration complete!\n"
+ f"Accel: {accel_offsets}\n"
+ f"Gyro: {gyro_offsets}")
+```
+
+### Persistent Calibration
+
+Calibration data is automatically saved to `data/com.micropythonos.settings/sensors.json` and loaded on boot. You only need to calibrate once (unless the device is physically relocated or significantly re-oriented).
+
+## List Available Sensors
+
+```python
+# Get all sensors
+sensors = SensorManager.get_sensor_list()
+
+for sensor in sensors:
+ print(f"Name: {sensor.name}")
+ print(f"Type: {sensor.type}")
+ print(f"Vendor: {sensor.vendor}")
+ print(f"Max Range: {sensor.max_range}")
+ print(f"Resolution: {sensor.resolution}")
+ print(f"Power: {sensor.power} mA")
+ print("---")
+
+# Example output on Waveshare ESP32-S3:
+# Name: QMI8658 Accelerometer
+# Type: 1
+# Vendor: QST Corporation
+# Max Range: ±8G (78.4 m/s²)
+# Resolution: 0.0024 m/s²
+# Power: 0.2 mA
+# ---
+# Name: QMI8658 Gyroscope
+# Type: 4
+# Vendor: QST Corporation
+# Max Range: ±256 deg/s
+# Resolution: 0.002 deg/s
+# Power: 0.7 mA
+# ---
+# Name: QMI8658 Temperature
+# Type: 14
+# Vendor: QST Corporation
+# Max Range: -40°C to +85°C
+# Resolution: 0.004°C
+# Power: 0 mA
+# ---
+# Name: ESP32 MCU Temperature
+# Type: 15
+# Vendor: Espressif
+# Max Range: -40°C to +125°C
+# Resolution: 0.5°C
+# Power: 0 mA
+```
+
+## Performance Tips
+
+### Polling Rate
+
+IMU sensors can be read very quickly (~1-2ms per read), but polling every frame is unnecessary:
+
+```python
+# ❌ BAD: Poll every frame (60 Hz = 60 reads/sec)
+def update_frame(self, a, b):
+ accel = SensorManager.read_sensor(self.accel) # Too frequent!
+
+# ✅ GOOD: Poll every other frame (30 Hz)
+def update_frame(self, a, b):
+ self.frame_count += 1
+ if self.frame_count % 2 == 0:
+ accel = SensorManager.read_sensor(self.accel)
+
+# ✅ BETTER: Use timer instead of frame updates
+def onStart(self, screen):
+ # Poll at 20 Hz (every 50ms)
+ self.sensor_timer = lv.timer_create(self.read_sensors, 50, None)
+
+def read_sensors(self, timer):
+ accel = SensorManager.read_sensor(self.accel)
+```
+
+**Recommended rates:**
+- **Games**: 20-30 Hz (responsive but not excessive)
+- **UI feedback**: 10-15 Hz (smooth enough for tilt UI)
+- **Background monitoring**: 1-5 Hz (screen rotation, pedometer)
+
+### Thread Safety
+
+SensorManager is thread-safe and can be read from multiple threads:
+
+```python
+import _thread
+import mpos.apps
+
+def background_monitoring():
+ accel = SensorManager.get_default_sensor(SensorManager.TYPE_ACCELEROMETER)
+ while True:
+ accel_data = SensorManager.read_sensor(accel) # Thread-safe
+ # Process data...
+ time.sleep(1)
+
+# Start background thread
+_thread.stack_size(mpos.apps.good_stack_size())
+_thread.start_new_thread(background_monitoring, ())
+```
+
+## Platform Differences
+
+### Hardware Support
+
+| Platform | Accelerometer | Gyroscope | IMU Temp | MCU Temp |
+|----------|---------------|-----------|----------|----------|
+| Waveshare ESP32-S3 | ✅ QMI8658 | ✅ QMI8658 | ✅ QMI8658 | ✅ ESP32 |
+| Fri3d 2024 Badge | ✅ WSEN_ISDS | ✅ WSEN_ISDS | ❌ | ✅ ESP32 |
+| Desktop/Linux | ❌ | ❌ | ❌ | ❌ |
+
+### Graceful Degradation
+
+Always check if sensors are available:
+
+```python
+if SensorManager.is_available():
+ # Use real sensor data
+ accel = SensorManager.get_default_sensor(SensorManager.TYPE_ACCELEROMETER)
+ data = SensorManager.read_sensor(accel)
+else:
+ # Fallback for desktop/testing
+ data = (0.0, 0.0, 9.8) # Simulate device at rest
+```
+
+## Unit Conversions
+
+SensorManager returns standard SI units. Here are common conversions:
+
+### Acceleration
+
+```python
+# SensorManager returns m/s²
+accel = SensorManager.read_sensor(accel_sensor)
+ax, ay, az = accel
+
+# Convert to G-forces (1 G = 9.80665 m/s²)
+ax_g = ax / 9.80665
+ay_g = ay / 9.80665
+az_g = az / 9.80665
+print(f"Acceleration: {az_g:.2f} G") # At rest, Z ≈ 1.0 G
+```
+
+### Gyroscope
+
+```python
+# SensorManager returns deg/s
+gyro = SensorManager.read_sensor(gyro_sensor)
+gx, gy, gz = gyro
+
+# Convert to rad/s
+import math
+gx_rad = math.radians(gx)
+gy_rad = math.radians(gy)
+gz_rad = math.radians(gz)
+
+# Convert to RPM (rotations per minute)
+gz_rpm = gz / 6.0 # 360 deg/s = 60 RPM
+```
+
+## API Reference
+
+### Functions
+
+**`init(i2c_bus, address=0x6B)`**
+Initialize SensorManager. Called automatically in board init files. Returns `True` if any sensors detected.
+
+**`is_available()`**
+Returns `True` if sensors are available.
+
+**`get_sensor_list()`**
+Returns list of all available `Sensor` objects.
+
+**`get_default_sensor(sensor_type)`**
+Returns the default `Sensor` for the given type, or `None` if not available.
+
+**`read_sensor(sensor)`**
+Reads sensor data. Returns `(x, y, z)` tuple for motion sensors, single value for temperature, or `None` on error.
+
+**`calibrate_sensor(sensor, samples=100)`**
+Calibrates the sensor (device must be stationary). Returns calibration offsets. Saves to SharedPreferences automatically.
+
+### Sensor Object
+
+Properties:
+
+- `name` - Human-readable sensor name
+- `type` - Sensor type constant
+- `vendor` - Manufacturer name
+- `version` - Driver version
+- `max_range` - Maximum measurement range
+- `resolution` - Measurement resolution
+- `power` - Power consumption in mA
+
+## Troubleshooting
+
+### Sensor Returns None
+
+```python
+data = SensorManager.read_sensor(accel)
+if data is None:
+ # Possible causes:
+ # 1. Sensor not available (check is_available())
+ # 2. I2C communication error
+ # 3. Sensor not initialized
+ print("Sensor read failed")
+```
+
+### Inaccurate Readings
+
+- **Calibrate the sensors** - Run calibration with device stationary
+- **Check mounting** - Ensure device is flat during calibration
+- **Wait for warmup** - Sensors stabilize after 1-2 seconds
+
+### High Drift
+
+- **Re-calibrate** - Temperature changes can cause drift
+- **Check for interference** - Keep away from magnets, motors
+- **Use filtered data** - Apply low-pass filter for smoother readings
+
+```python
+# Simple low-pass filter
+class LowPassFilter:
+ def __init__(self, alpha=0.1):
+ self.alpha = alpha
+ self.value = None
+
+ def filter(self, new_value):
+ if self.value is None:
+ self.value = new_value
+ else:
+ self.value = self.alpha * new_value + (1 - self.alpha) * self.value
+ return self.value
+
+# Usage
+accel_filter_x = LowPassFilter(alpha=0.2)
+accel = SensorManager.read_sensor(accel_sensor)
+ax, ay, az = accel
+filtered_ax = accel_filter_x.filter(ax)
+```
+
+### ImportError: can't import name _CONSTANT
+
+If you try to directly import driver constants, you'll see this error:
+
+```python
+from mpos.hardware.drivers.qmi8658 import _QMI8685_PARTID # ERROR!
+# ImportError: can't import name _QMI8685_PARTID
+```
+
+**Cause**: Driver constants are defined with MicroPython's `const()` function, which makes them compile-time constants. They're inlined during compilation and the names aren't available for import at runtime.
+
+**Solution**: Use hardcoded values instead:
+
+```python
+from mpos.hardware.drivers.qmi8658 import QMI8658
+# Define constants locally
+_QMI8685_PARTID = 0x05
+_REG_PARTID = 0x00
+```
+
+## See Also
+
+- [Creating Apps](../apps/creating-apps.md) - Learn how to create MicroPythonOS apps
+- [SharedPreferences](preferences.md) - Persist app data
+- [System Components](../architecture/system-components.md) - OS architecture overview
diff --git a/docs/frameworks/task-manager.md b/docs/frameworks/task-manager.md
new file mode 100644
index 0000000..4931e52
--- /dev/null
+++ b/docs/frameworks/task-manager.md
@@ -0,0 +1,490 @@
+# TaskManager
+
+MicroPythonOS provides a centralized task management service called **TaskManager** that wraps MicroPython's `uasyncio` for managing asynchronous operations. It enables apps to run background tasks, schedule delayed operations, and coordinate concurrent activities.
+
+## Overview
+
+TaskManager provides:
+
+✅ **Simplified async interface** - Easy wrappers around uasyncio primitives
+✅ **Task creation** - Launch background coroutines without boilerplate
+✅ **Sleep operations** - Async delays in seconds or milliseconds
+✅ **Timeouts** - Wait for operations with automatic timeout handling
+✅ **Event notifications** - Simple async event signaling
+✅ **Centralized management** - Single point of control for all async operations
+
+## Quick Start
+
+### Creating Background Tasks
+
+```python
+from mpos.app.activity import Activity
+from mpos import TaskManager
+
+class MyActivity(Activity):
+ def onCreate(self):
+ # Launch a background task
+ TaskManager.create_task(self.download_data())
+
+ async def download_data(self):
+ print("Starting download...")
+ # Simulate network operation
+ await TaskManager.sleep(2)
+ print("Download complete!")
+```
+
+**Key points:**
+- Use `TaskManager.create_task()` to launch coroutines
+- Tasks run concurrently with UI operations
+- Tasks continue until completion or app termination
+
+### Delayed Operations
+
+```python
+async def delayed_operation(self):
+ # Wait 3 seconds
+ await TaskManager.sleep(3)
+ print("3 seconds later...")
+
+ # Wait 500 milliseconds
+ await TaskManager.sleep_ms(500)
+ print("Half a second later...")
+```
+
+**Available sleep methods:**
+- `sleep(seconds)` - Sleep for specified seconds (float)
+- `sleep_ms(milliseconds)` - Sleep for specified milliseconds (int)
+
+### Timeout Operations
+
+```python
+from mpos import TaskManager, DownloadManager
+
+async def download_with_timeout(self):
+ try:
+ # Wait max 10 seconds for download
+ data = await TaskManager.wait_for(
+ DownloadManager.download_url("https://example.com/data.json"),
+ timeout=10
+ )
+ print(f"Downloaded {len(data)} bytes")
+ except Exception as e:
+ print(f"Download timed out or failed: {e}")
+```
+
+**Timeout behavior:**
+- If operation completes within timeout: returns result
+- If operation exceeds timeout: raises `asyncio.TimeoutError`
+- Timeout is in seconds (float)
+
+### Event Notifications
+
+```python
+async def wait_for_event(self):
+ # Create an event
+ event = await TaskManager.notify_event()
+
+ # Wait for the event to be signaled
+ await event.wait()
+ print("Event occurred!")
+```
+
+## Common Patterns
+
+### Downloading Data
+
+```python
+from mpos import TaskManager, DownloadManager
+
+class AppStoreActivity(Activity):
+ def onResume(self, screen):
+ super().onResume(screen)
+ # Download app index in background
+ TaskManager.create_task(self.download_app_index())
+
+ async def download_app_index(self):
+ try:
+ data = await DownloadManager.download_url(
+ "https://apps.micropythonos.com/app_index.json"
+ )
+ if data:
+ parsed = json.loads(data)
+ self.update_ui(parsed)
+ except Exception as e:
+ print(f"Download failed: {e}")
+```
+
+### Periodic Tasks
+
+```python
+async def monitor_sensor(self):
+ """Check sensor every second"""
+ while self.monitoring:
+ value = sensor_manager.read_accelerometer()
+ self.update_display(value)
+ await TaskManager.sleep(1)
+
+def onCreate(self):
+ self.monitoring = True
+ TaskManager.create_task(self.monitor_sensor())
+
+def onDestroy(self, screen):
+ self.monitoring = False # Stop monitoring loop
+```
+
+### Download with Progress
+
+```python
+async def download_large_file(self):
+ progress_label = lv.label(self.screen)
+
+ async def update_progress(percent):
+ progress_label.set_text(f"Downloading: {percent}%")
+
+ success = await DownloadManager.download_url(
+ "https://example.com/large.bin",
+ outfile="/sdcard/large.bin",
+ progress_callback=update_progress
+ )
+
+ if success:
+ progress_label.set_text("Download complete!")
+```
+
+### Concurrent Downloads
+
+```python
+async def download_multiple_icons(self, apps):
+ """Download multiple icons concurrently"""
+ tasks = []
+ for app in apps:
+ task = DownloadManager.download_url(app.icon_url)
+ tasks.append(task)
+
+ # Wait for all downloads (with timeout per icon)
+ results = []
+ for i, task in enumerate(tasks):
+ try:
+ data = await TaskManager.wait_for(task, timeout=5)
+ results.append(data)
+ except Exception as e:
+ print(f"Icon {i} failed: {e}")
+ results.append(None)
+
+ return results
+```
+
+## API Reference
+
+### Task Creation
+
+#### `TaskManager.create_task(coroutine)`
+
+Create and schedule a background task.
+
+**Parameters:**
+- `coroutine` - Coroutine object to execute (must be async def)
+
+**Returns:**
+- Task object (usually not needed)
+
+**Example:**
+```python
+TaskManager.create_task(self.background_work())
+```
+
+### Sleep Operations
+
+#### `TaskManager.sleep(seconds)`
+
+Async sleep for specified seconds.
+
+**Parameters:**
+- `seconds` (float) - Time to sleep in seconds
+
+**Returns:**
+- None (awaitable)
+
+**Example:**
+```python
+await TaskManager.sleep(2.5) # Sleep 2.5 seconds
+```
+
+#### `TaskManager.sleep_ms(milliseconds)`
+
+Async sleep for specified milliseconds.
+
+**Parameters:**
+- `milliseconds` (int) - Time to sleep in milliseconds
+
+**Returns:**
+- None (awaitable)
+
+**Example:**
+```python
+await TaskManager.sleep_ms(500) # Sleep 500ms
+```
+
+### Timeout Operations
+
+#### `TaskManager.wait_for(awaitable, timeout)`
+
+Wait for an operation with timeout.
+
+**Parameters:**
+- `awaitable` - Coroutine or awaitable object
+- `timeout` (float) - Maximum time to wait in seconds
+
+**Returns:**
+- Result of the awaitable if completed in time
+
+**Raises:**
+- `asyncio.TimeoutError` - If operation exceeds timeout
+
+**Example:**
+```python
+try:
+ result = await TaskManager.wait_for(
+ download_operation(),
+ timeout=10
+ )
+except asyncio.TimeoutError:
+ print("Operation timed out")
+```
+
+### Event Notifications
+
+#### `TaskManager.notify_event()`
+
+Create an async event for coordination.
+
+**Returns:**
+- asyncio.Event object
+
+**Example:**
+```python
+event = await TaskManager.notify_event()
+await event.wait() # Wait for event
+event.set() # Signal event
+```
+
+## Integration with UI
+
+### Safe UI Updates from Async Tasks
+
+LVGL operations must run on the main thread. Use UI operations sparingly from async tasks:
+
+```python
+async def background_task(self):
+ # Do async work
+ data = await fetch_data()
+
+ # Update UI (safe - LVGL thread-safe)
+ self.label.set_text(f"Got {len(data)} items")
+```
+
+For complex UI updates, consider using callbacks or state variables:
+
+```python
+async def download_task(self):
+ self.download_complete = False
+ data = await DownloadManager.download_url(url)
+ self.data = data
+ self.download_complete = True
+
+def onCreate(self):
+ TaskManager.create_task(self.download_task())
+ # Check download_complete flag periodically
+```
+
+### Activity Lifecycle
+
+Tasks continue running even when activity is paused. Clean up tasks in `onDestroy()`:
+
+```python
+def onCreate(self):
+ self.running = True
+ TaskManager.create_task(self.monitor_loop())
+
+async def monitor_loop(self):
+ while self.running:
+ await self.check_status()
+ await TaskManager.sleep(1)
+
+def onDestroy(self, screen):
+ self.running = False # Stop task loop
+```
+
+## Common Use Cases
+
+### Network Operations
+
+```python
+# Download JSON data
+data = await DownloadManager.download_url("https://api.example.com/data")
+parsed = json.loads(data)
+
+# Download to file
+success = await DownloadManager.download_url(
+ "https://example.com/file.bin",
+ outfile="/sdcard/file.bin"
+)
+
+# Stream processing
+async def process_chunk(chunk):
+ # Process each chunk as it arrives
+ pass
+
+await DownloadManager.download_url(
+ "https://example.com/stream",
+ chunk_callback=process_chunk
+)
+```
+
+### WebSocket Communication
+
+```python
+from mpos import TaskManager
+import websocket
+
+async def websocket_listener(self):
+ ws = websocket.WebSocketApp(url, callbacks)
+ await ws.run_forever()
+
+def onCreate(self):
+ TaskManager.create_task(self.websocket_listener())
+```
+
+### Sensor Polling
+
+```python
+import mpos.sensor_manager as SensorManager
+from mpos import TaskManager
+
+async def poll_sensors(self):
+ while self.active:
+ accel = SensorManager.read_accelerometer()
+ gyro = SensorManager.read_gyroscope()
+
+ self.update_display(accel, gyro)
+ await TaskManager.sleep(0.1) # 100ms = 10 Hz
+```
+
+## Performance Considerations
+
+### Task Overhead
+
+- **Minimal overhead**: TaskManager is a thin wrapper around uasyncio
+- **Concurrent tasks**: Dozens of tasks can run simultaneously
+- **Memory**: Each task uses ~1-2KB of RAM
+- **Context switches**: Tasks yield automatically during await
+
+### Best Practices
+
+1. **Use async for I/O**: Network, file operations, sleep
+2. **Avoid blocking**: Don't use time.sleep() in async functions
+3. **Clean up tasks**: Set flags to stop loops in onDestroy()
+4. **Handle exceptions**: Always wrap operations in try/except
+5. **Limit concurrency**: Don't create unbounded task lists
+
+### Example: Bounded Concurrency
+
+```python
+async def download_with_limit(self, urls, max_concurrent=5):
+ """Download URLs with max concurrent downloads"""
+ results = []
+ for i in range(0, len(urls), max_concurrent):
+ batch = urls[i:i+max_concurrent]
+ batch_results = []
+
+ for url in batch:
+ task = DownloadManager.download_url(url)
+ try:
+ data = await TaskManager.wait_for(task, timeout=10)
+ batch_results.append(data)
+ except Exception as e:
+ batch_results.append(None)
+
+ results.extend(batch_results)
+
+ return results
+```
+
+## Troubleshooting
+
+### Task Never Completes
+
+**Problem:** Task hangs indefinitely
+
+**Solution:** Add timeout:
+```python
+try:
+ result = await TaskManager.wait_for(task, timeout=30)
+except asyncio.TimeoutError:
+ print("Task timed out")
+```
+
+### Memory Leak
+
+**Problem:** Memory usage grows over time
+
+**Solution:** Ensure task loops exit:
+```python
+async def loop_task(self):
+ while self.running: # Check flag
+ await work()
+ await TaskManager.sleep(1)
+
+def onDestroy(self, screen):
+ self.running = False # Stop loop
+```
+
+### UI Not Updating
+
+**Problem:** UI doesn't reflect async task results
+
+**Solution:** Ensure UI updates happen on main thread (LVGL is thread-safe in MicroPythonOS):
+```python
+async def task(self):
+ data = await fetch()
+ # Direct UI update is safe
+ self.label.set_text(str(data))
+```
+
+### Exception Not Caught
+
+**Problem:** Exception crashes app
+
+**Solution:** Wrap task in try/except:
+```python
+TaskManager.create_task(self.safe_task())
+
+async def safe_task(self):
+ try:
+ await risky_operation()
+ except Exception as e:
+ print(f"Task failed: {e}")
+```
+
+## Implementation Details
+
+**Location**: `/home/user/MicroPythonOS/internal_filesystem/lib/mpos/task_manager.py`
+
+**Pattern**: Wrapper around `uasyncio` module
+
+**Key features:**
+- `create_task()` - Wraps `asyncio.create_task()`
+- `sleep()` / `sleep_ms()` - Wrap `asyncio.sleep()`
+- `wait_for()` - Wraps `asyncio.wait_for()` with timeout handling
+- `notify_event()` - Creates `asyncio.Event()` objects
+
+**Thread model:**
+- All async tasks run on main asyncio event loop
+- No separate threads created (unless using `_thread` module separately)
+- Tasks cooperatively multitask via await points
+
+## See Also
+
+- [DownloadManager](download-manager.md) - HTTP download utilities
+- [AudioFlinger](audioflinger.md) - Audio playback (uses TaskManager internally)
+- [SensorManager](sensor-manager.md) - Sensor data access
diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md
index 08298fa..1bb0ddf 100644
--- a/docs/getting-started/installation.md
+++ b/docs/getting-started/installation.md
@@ -2,37 +2,16 @@
MicroPythonOS can be installed on supported microcontrollers (e.g., ESP32) and on desktop systems (Linux, Raspberry Pi, MacOS, etc).
-If you're a developer, you can [build it yourself and install from source](../building/index.md)
-
To simply install prebuilt software, read on!
## Installing on ESP32
Just use the [WebSerial installer at install.micropythonos.com](https://install.micropythonos.com).
-## Installing on Desktop (Linux/MacOS)
-
-Download the [latest release for desktop](https://github.com/MicroPythonOS/MicroPythonOS/releases).
-
-Here we'll assume you saved it in /tmp/MicroPythonOS_amd64_Linux_0.0.8
-
-Get the internal_filesystem files:
-
-```
-git clone https://github.com/MicroPythonOS/MicroPythonOS.git
-cd MicroPythonOS/
-cd internal_filesystem/
-```
-
-Now run it by starting the entry points, boot_unix.py and main.py:
-
-```
-/tmp/MicroPythonOS_amd64_Linux_0.0.8 -X heapsize=32M -v -i -c "$(cat boot_unix.py main.py)"
-```
+For advanced usage, such as installing development builds without any files, see [Installing on ESP32](../os-development/installing-on-esp32.md).
-You can also check out `scripts/run_desktop.sh` for more examples, such as immediately starting an app or starting fullscreen.
+{!os-development/running-on-desktop.md!}
## Next Steps
-- Check [Supported Hardware](supported-hardware.md) for compatible devices.
- Explore [Built-in Apps](../apps/built-in-apps.md) to get started with the system.
diff --git a/docs/getting-started/supported-hardware.md b/docs/getting-started/supported-hardware.md
index b71daf1..09ba3a9 100644
--- a/docs/getting-started/supported-hardware.md
+++ b/docs/getting-started/supported-hardware.md
@@ -10,13 +10,12 @@ MicroPythonOS runs on a variety of platforms, from microcontrollers to desktops.
## Desktop Computers
- **Linux**: Supported using SDL for display handling.
-- **MacOS**: Should work but untested.
+- **MacOS**: Supported as well.
## Raspberry Pi
-- **Raspbian/Linux-based**: Should work, especially with a Linux desktop. Untested.
+- **Raspbian and other Linux-based**: Should work!
## Notes
-- Ensure your hardware supports touch screens, IMUs, or cameras for full feature compatibility.
- Check [Installation](installation.md) for setup instructions.
diff --git a/docs/os-development/compile-and-run.md b/docs/os-development/compile-and-run.md
deleted file mode 100644
index b06019e..0000000
--- a/docs/os-development/compile-and-run.md
+++ /dev/null
@@ -1,67 +0,0 @@
-## Compile the code
-
-1. **Make sure you're in the main repository**:
-
- ```
- cd MicroPythonOS
- ```
-
-2. **Start the Compilation**
-
- Usage:
-
- - ``` - ./scripts/build_lvgl_micropython.sh- - **Target systems**: esp32, unix (= Linux) and macOS - - **Build types**: - - - A "prod" build includes the complete filesystem that's "frozen" into the build, so it's fast and all ready to go but the files in /lib and /builtin will be read-only. - - A "dev" build comes without a filesystem, so it's perfect for power users that want to work on MicroPythonOS internals. There's a simple script that will copy all the necessary files over later, and these will be writeable. - - _Note_: for unix and macOS systems, only "dev" has been tested. The "prod" builds might have issues but should be made to work soon. - - **Target devices**: waveshare-esp32-s3-touch-lcd-2 or fri3d-2024 - - **Examples**: - -[optional target device] - ``` -
- ``` - ./scripts/build_lvgl_micropython.sh esp32 prod fri3d-2024 - ./scripts/build_lvgl_micropython.sh esp32 dev waveshare-esp32-s3-touch-lcd-2 - ./scripts/build_lvgl_micropython.sh esp32 unix dev - ./scripts/build_lvgl_micropython.sh esp32 macOS dev - ``` -- - The resulting build file will be in `lvgl_micropython/build/`, for example: - - - lvgl_micropython/build/lvgl_micropy_unix - - lvgl_micropython/build/lvgl_micropy_macOS - - lvgl_micropython/build/lvgl_micropy_ESP32_GENERIC_S3-SPIRAM_OCT-16.bin - -## Running on Linux or MacOS - -1. Download a release binary (e.g., `MicroPythonOS_amd64_Linux`, `MicroPythonOS_amd64_MacOS`) or build your own [on MacOS](macos.md) or [Linux](linux.md). -2. Run the application: - -
- ``` - cd internal_filesystem/ # make sure you're in the right place to find the filesystem - /path/to/release_binary -X heapsize=32M -v -i -c "$(cat boot_unix.py main.py)" - ``` -- - There's also a convenient `./scripts/run_desktop.sh` script that will attempt to start the latest build that you compiled yourself. - -### Modifying files - -You'll notice that, whenever you change a file on your local system, the changes are immediately visible whenever you reload the file. - -This results in a very quick coding cycle. - -Give this a try by editing `internal_filesystem/builtin/apps/com.micropythonos.about/assets/about.py` and then restarting the "About" app. Powerful stuff! diff --git a/docs/os-development/compiling.md b/docs/os-development/compiling.md new file mode 100644 index 0000000..6e5f953 --- /dev/null +++ b/docs/os-development/compiling.md @@ -0,0 +1,36 @@ +## Compile the code + +1. **Make sure you're in the main repository**: + + ``` + cd MicroPythonOS/ + ``` + +2. **Start the Compilation** + + Usage: + +
+ ``` + ./scripts/build_mpos.sh+ + **Target systems**: `esp32`, `unix` (= Linux) and `macOS` + + **Examples**: + ++ ``` +
+ ``` + ./scripts/build_mpos.sh esp32 + ./scripts/build_mpos.sh unix + ./scripts/build_mpos.sh macOS + ``` ++ + The resulting build file will be in `lvgl_micropython/build/`, for example: + + - `lvgl_micropython/build/lvgl_micropy_unix` + - `lvgl_micropython/build/lvgl_micropy_macOS` + - `lvgl_micropython/build/lvgl_micropy_ESP32_GENERIC_S3-SPIRAM_OCT-16.bin` + diff --git a/docs/os-development/index.md b/docs/os-development/index.md new file mode 100644 index 0000000..43f668c --- /dev/null +++ b/docs/os-development/index.md @@ -0,0 +1,5 @@ +# OS Development + +Most users will just want to run MicroPythonOS and built apps for it. + +But if you want to work on stuff that's "under the hood", then choose one of the entries under "OS Development" in the menu. diff --git a/docs/os-development/installing-on-esp32.md b/docs/os-development/installing-on-esp32.md index 9d216fc..f97f6be 100644 --- a/docs/os-development/installing-on-esp32.md +++ b/docs/os-development/installing-on-esp32.md @@ -4,22 +4,23 @@ But if you need to install a version that's not available there, or you built yo 1. **Get the firmware** - - Download a release binary (e.g., `MicroPythonOS_fri3d-2024_prod_0.2.1.bin`, `MicroPythonOS_waveshare-esp32-s3-touch-lcd-2_prod_0.2.1.bin`, etc.) + - Download a release binary (e.g., `MicroPythonOS_esp32_0.5.0.bin`) - Or build your own [on MacOS](macos.md) or [Linux](linux.md) 2. **Put the ESP32 in Bootloader Mode** If you're already in MicroPythonOS: go to Settings - Restart to Bootloader - Bootloader - Save. - Otherwise, physically keep the "BOOT" (sometimes labeled "START") button pressed while briefly pressing the "RESET" button. + Otherwise, physically keep the "BOOT" (sometimes labeled "START") button pressed while powering up the board. + This is explained in more detail at [the webinstaller](https://install.micropythonos.com/) 3. **Flash the firmware** ``` - ~/.espressif/python_env/idf5.2_py3.9_env/bin/python -m esptool --chip esp32s3 0x0 firmware_file.bin + ~/.espressif/python_env/idf5.2_py3.9_env/bin/python -m esptool --chip esp32s3 write_flash 0 firmware_file.bin ``` - Add --erase-all if you want to erase the entire flash memory, so that no old files or apps will remain. + Add the `--erase-all` option if you want to erase the entire flash memory, so that no old files or apps will remain. There's also a convenient `./scripts/flash_over_usb.sh` script that will attempt to flash the latest firmware that you compiled yourself. @@ -33,33 +34,30 @@ But if you need to install a version that's not available there, or you built yo lvgl_micropython/lib/micropython/tools/mpremote/mpremote.py ``` -5. **Populate the filesystem** (only for "dev" builds) +5. **Populate the filesystem** (only for development) + + In development, you probably want to override the "frozen" libraries and apps that are compiled in, and replace them with source files, which you can edit. - The "dev" builds come without a filesystem so you probably want to copy the whole internal_filesystem/ folder over, as well as one of the device-specific boot*.py files and main.py. - There's a convenient script that will do this for you. Usage:
```
- ./scripts/install.sh
+ ./scripts/install.sh
```
- **Target devices**: waveshare-esp32-s3-touch-lcd-2 or fri3d-2024
- Examples:
+ Example:
```
- ./scripts/install.sh fri3d-2024
- ./scripts/install.sh waveshare-esp32-s3-touch-lcd-2
+ ./scripts/install.sh
```
## Notes
-- A "dev" build without frozen files is quite a bit slower when starting apps because all the libraries need to be compiled at runtime.
- Ensure your ESP32 is compatible (see [Supported Hardware](../getting-started/supported-hardware.md)). If it's not, then you might need the [Porting Guide](../os-development/porting-guide.md).
diff --git a/docs/os-development/linux.md b/docs/os-development/linux.md
index c125ead..1b39ea4 100644
--- a/docs/os-development/linux.md
+++ b/docs/os-development/linux.md
@@ -22,5 +22,6 @@ sudo apt-get install -y build-essential libffi-dev pkg-config cmake ninja-build
```
+{!os-development/compiling.md!}
-{!os-development/compile-and-run.md!}
+{!os-development/running-on-desktop.md!}
diff --git a/docs/os-development/macos.md b/docs/os-development/macos.md
index 2a9010b..3506848 100644
--- a/docs/os-development/macos.md
+++ b/docs/os-development/macos.md
@@ -17,9 +17,10 @@ That will take a while, because it recursively clones MicroPython, LVGL, ESP-IDF
While that's going on, make sure you have everything installed to compile code:
```
-xcode-select --install || true # already installed on github
+xcode-select --install
brew install pkg-config libffi ninja make SDL2
```
+{!os-development/compiling.md!}
-{!os-development/compile-and-run.md!}
+{!os-development/running-on-desktop.md!}
diff --git a/docs/os-development/porting-guide.md b/docs/os-development/porting-guide.md
index 03286bc..bf8b449 100644
--- a/docs/os-development/porting-guide.md
+++ b/docs/os-development/porting-guide.md
@@ -10,7 +10,8 @@ If you prefer to have the porting work done for you and you're open to making a
## What to write
-By design, the only device-specific code for MicroPythonOS is found in the ```internal_filesystem/boot*.py``` files.
+By design, the only device-specific code for MicroPythonOS is found in the ```internal_filesystem/lib/mpos/board/+ ``` + git clone --recurse-submodules https://github.com/MicroPythonOS/MicroPythonOS.git + cd MicroPythonOS/ + ``` ++ +3. Start it from the local_filesystem/ folder: + +
+ ``` + cd internal_filesystem/ # make sure you're in the right place to find the filesystem + /path/to/MicroPythonOS_executable_binary -X heapsize=32M -v -i -c "$(cat boot_unix.py main.py)" + ``` ++ + There's also a convenient `./scripts/run_desktop.sh` script that will attempt to start the latest build that you compiled yourself. + +### Development Workflow: Desktop vs Hardware + +**IMPORTANT**: Understanding the difference between desktop testing and hardware deployment is critical for efficient development. + +#### Desktop Development (Recommended for Most Development) + +When you run `./scripts/run_desktop.sh`, the OS runs **directly from `internal_filesystem/`**. This means: + +✅ **All changes to Python files are immediately active** - no build or install needed +✅ **Instant testing** - edit a file, restart the app, see the changes +✅ **Fast iteration cycle** - the recommended way to develop and test + +**DO NOT run `./scripts/install.sh` when testing on desktop!** That script is only for deploying to physical hardware. + +**Example workflow:** +```bash +# 1. Edit a file +nano internal_filesystem/builtin/apps/com.micropythonos.settings/assets/settings.py + +# 2. Run on desktop - changes are immediately active! +./scripts/run_desktop.sh + +# That's it! Your changes are live. +``` + +#### Hardware Deployment (Only After Desktop Testing) + +Once you've tested your changes on desktop and they work correctly, you can deploy to physical hardware: + +```bash +# Deploy to connected ESP32 device +./scripts/install.sh waveshare-esp32-s3-touch-lcd-2 +``` + +The `install.sh` script copies files from `internal_filesystem/` to the device's storage partition over USB/serial. + +### Modifying Files + +You'll notice that whenever you change a file in `internal_filesystem/`, the changes are immediately visible on desktop when you reload the file or restart the app. + +This results in a very quick coding cycle - no compilation or installation needed for Python code changes. + +**Try it yourself:** + +1. Edit `internal_filesystem/builtin/apps/com.micropythonos.about/assets/about.py` +2. Run `./scripts/run_desktop.sh` +3. Open the About app +4. See your changes immediately! + +**When you DO need to rebuild:** + +You only need to run `./scripts/build_mpos.sh` when: + +- Modifying C extension modules (`c_mpos/`, `secp256k1-embedded-ecdh/`) +- Changing MicroPython core or LVGL bindings +- Testing the frozen filesystem for production releases +- Creating firmware for distribution + +For **all Python code development**, just edit files in `internal_filesystem/` and run `./scripts/run_desktop.sh`. diff --git a/docs/os-development/windows.md b/docs/os-development/windows.md index a03a434..efaf001 100644 --- a/docs/os-development/windows.md +++ b/docs/os-development/windows.md @@ -2,8 +2,11 @@ As the main dependency ([lvgl_micropython](https://github.com/lvgl-micropython/lvgl_micropython), which bundles LVGL and MicroPython) doesn't support Windows, MicroPythonOS also doesn't support it. -But this is only necessary for OS development. +But perhaps it works with the Windows Subsystem for Linux (WSL)! +It would be great if someone could give this a try and report back with the results in the chat or in a GitHub issue. -You can still participate in the bulk of the fun: creating cool apps! +All of this is only necessary for OS development. -To do so, install a pre-built firmware on a [supported device](../getting-started/supported-hardware.md) and then hopping over to [Creating Apps](../apps/creating-apps.md). +Even without doing a local build, you can still participate in the bulk of the fun: creating cool apps! + +To do so, install a pre-built firmware on a [supported device](../getting-started/supported-hardware.md) and then over to [Creating Apps](../apps/creating-apps.md). diff --git a/docs/other/release-checklist.md b/docs/other/release-checklist.md index 83ed26b..50139fa 100644 --- a/docs/other/release-checklist.md +++ b/docs/other/release-checklist.md @@ -52,4 +52,4 @@ scripts/release_to_install.sh ## Notes - Ensure all repositories are pushed before tagging. -- Verify builds on target hardware (see [Building for ESP32](esp32.md)). +- Verify builds on target hardware diff --git a/docs/overview.md b/docs/overview.md index 8be0079..f6bf3f6 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -48,5 +48,3 @@ Explore MicroPythonOS in action: