From 7f8867d4b74463398d9577f1037eceaefb8afba4 Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Sat, 25 Oct 2025 21:30:23 +0200 Subject: [PATCH 01/18] Fix github workflow --- .github/workflows/deploy-docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 17619bbb10997633c459756a2a01bf20cb8be449 Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Sat, 25 Oct 2025 21:54:27 +0200 Subject: [PATCH 02/18] improve docs --- docs/architecture/system-components.md | 6 ++-- docs/building/index.md | 7 ----- docs/getting-started/installation.md | 25 ++------------- docs/getting-started/supported-hardware.md | 5 ++- .../{compile-and-run.md => compiling.md} | 21 ------------- docs/os-development/index.md | 5 +++ docs/os-development/linux.md | 4 ++- docs/os-development/macos.md | 3 +- docs/os-development/running-on-desktop.md | 31 +++++++++++++++++++ docs/overview.md | 2 -- 10 files changed, 49 insertions(+), 60 deletions(-) delete mode 100644 docs/building/index.md rename docs/os-development/{compile-and-run.md => compiling.md} (63%) create mode 100644 docs/os-development/index.md create mode 100644 docs/os-development/running-on-desktop.md 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/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/compiling.md similarity index 63% rename from docs/os-development/compile-and-run.md rename to docs/os-development/compiling.md index b06019e..3095ab8 100644 --- a/docs/os-development/compile-and-run.md +++ b/docs/os-development/compiling.md @@ -44,24 +44,3 @@ - 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/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/linux.md b/docs/os-development/linux.md index c125ead..7ec0f8c 100644 --- a/docs/os-development/linux.md +++ b/docs/os-development/linux.md @@ -23,4 +23,6 @@ sudo apt-get install -y build-essential libffi-dev pkg-config cmake ninja-build -{!os-development/compile-and-run.md!} +{!os-development/compiling.md!} + +{!os-development/running-on-desktop.md!} diff --git a/docs/os-development/macos.md b/docs/os-development/macos.md index 2a9010b..549b319 100644 --- a/docs/os-development/macos.md +++ b/docs/os-development/macos.md @@ -21,5 +21,6 @@ xcode-select --install || true # already installed on github 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/running-on-desktop.md b/docs/os-development/running-on-desktop.md new file mode 100644 index 0000000..2ea2b2c --- /dev/null +++ b/docs/os-development/running-on-desktop.md @@ -0,0 +1,31 @@ +## 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. If you don't have a local clone yet then do it now, so you have the local_filesystem/ folder: + +
+    ```
+    git clone https://github.com/MicroPythonOS/MicroPythonOS.git
+    cd MicroPythonOS/
+    ```
+    
+ +3. Start it: + +
+    ```
+    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/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:
WiFi Settings
- -[See more screenshots](#screenshots) From f50ebdb0b05b7cb3acb4e41a55377d3495333ae7 Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Sun, 26 Oct 2025 06:29:57 +0100 Subject: [PATCH 03/18] Rename build_lvgl_micropython.sh to build_mpos.sh --- docs/os-development/compiling.md | 10 +++++----- docs/os-development/linux.md | 1 - docs/os-development/porting-guide.md | 2 +- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/docs/os-development/compiling.md b/docs/os-development/compiling.md index 3095ab8..93b7977 100644 --- a/docs/os-development/compiling.md +++ b/docs/os-development/compiling.md @@ -12,7 +12,7 @@
     ```
-    ./scripts/build_lvgl_micropython.sh   [optional target device]
+    ./scripts/build_mpos.sh   [optional target device]
     ```
     
@@ -31,10 +31,10 @@
     ```
-    ./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
+    ./scripts/build_mpos.sh esp32 prod fri3d-2024
+    ./scripts/build_mpos.sh esp32 dev waveshare-esp32-s3-touch-lcd-2
+    ./scripts/build_mpos.sh esp32 unix dev
+    ./scripts/build_mpos.sh esp32 macOS dev
     ```
     
diff --git a/docs/os-development/linux.md b/docs/os-development/linux.md index 7ec0f8c..1b39ea4 100644 --- a/docs/os-development/linux.md +++ b/docs/os-development/linux.md @@ -22,7 +22,6 @@ sudo apt-get install -y build-essential libffi-dev pkg-config cmake ninja-build ``` - {!os-development/compiling.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..7030c99 100644 --- a/docs/os-development/porting-guide.md +++ b/docs/os-development/porting-guide.md @@ -18,7 +18,7 @@ By design, the only device-specific code for MicroPythonOS is found in the ```in The goal is to have it boot and show a MicroPython REPL shell on the serial line. - Take a look at our [build_lvgl_micropython.sh](https://github.com/MicroPythonOS/MicroPythonOS/blob/main/scripts/build_lvgl_micropython.sh) script. A "dev" build (without any "frozen" filesystem) is preferred as this will still change a lot. + Take a look at our [build_mpos.sh](https://github.com/MicroPythonOS/MicroPythonOS/blob/main/scripts/build_mpos.sh) script. A "dev" build (without any "frozen" filesystem) is preferred as this will still change a lot. Also go over the [official lvgl_micropython documentation](https://github.com/lvgl-micropython/lvgl_micropython/blob/main/README.md) for porting instructions. If you're in luck, your device is already listed in the esp32 BOARD list. Otherwise use a generic one like `BOARD=ESP32_GENERIC` with `BOARD_VARIANT=SPIRAM` or `BOARD=ESP32_GENERIC_S3` with `BOARD_VARIANT=SPIRAM_OCT` if it has an SPIRAM. From b8418a2996d3d49cf69136f08253c1c9cc11f490 Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Sun, 26 Oct 2025 06:39:09 +0100 Subject: [PATCH 04/18] improve docs --- docs/os-development/installing-on-esp32.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/os-development/installing-on-esp32.md b/docs/os-development/installing-on-esp32.md index 9d216fc..4c3c2a4 100644 --- a/docs/os-development/installing-on-esp32.md +++ b/docs/os-development/installing-on-esp32.md @@ -35,7 +35,10 @@ But if you need to install a version that's not available there, or you built yo 5. **Populate the filesystem** (only for "dev" builds) - 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. + The "dev" builds come without a filesystem so you probably want to copy: + + - the whole internal_filesystem/ folder, including main.py + - the appropriate device-specific internal_filesystem/boot*.py file to /boot.py on the device There's a convenient script that will do this for you. From c5cf67e94a3ef97ca26a0134dab0242921f634ee Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Sun, 26 Oct 2025 06:41:39 +0100 Subject: [PATCH 05/18] Fix broken links --- docs/os-development/running-on-desktop.md | 2 +- docs/other/release-checklist.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/os-development/running-on-desktop.md b/docs/os-development/running-on-desktop.md index 2ea2b2c..af55eea 100644 --- a/docs/os-development/running-on-desktop.md +++ b/docs/os-development/running-on-desktop.md @@ -1,6 +1,6 @@ ## 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). +1. Download a release binary (e.g., `MicroPythonOS_amd64_Linux`, `MicroPythonOS_amd64_MacOS`) or build your own [on MacOS](../os-development/macos.md) or [Linux](../os-development/linux.md). 2. If you don't have a local clone yet then do it now, so you have the local_filesystem/ folder: 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 From 52daadfe21dab4ca8ab828c131e4e6caa4fbe16b Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Sun, 26 Oct 2025 10:32:15 +0100 Subject: [PATCH 06/18] Update docs --- docs/os-development/installing-on-esp32.md | 4 ++-- docs/os-development/running-on-desktop.md | 10 +++++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/docs/os-development/installing-on-esp32.md b/docs/os-development/installing-on-esp32.md index 4c3c2a4..90d72eb 100644 --- a/docs/os-development/installing-on-esp32.md +++ b/docs/os-development/installing-on-esp32.md @@ -16,10 +16,10 @@ But if you need to install a version that's not available there, or you built yo 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 0x0 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. diff --git a/docs/os-development/running-on-desktop.md b/docs/os-development/running-on-desktop.md index af55eea..cf36ef9 100644 --- a/docs/os-development/running-on-desktop.md +++ b/docs/os-development/running-on-desktop.md @@ -2,16 +2,20 @@ 1. Download a release binary (e.g., `MicroPythonOS_amd64_Linux`, `MicroPythonOS_amd64_MacOS`) or build your own [on MacOS](../os-development/macos.md) or [Linux](../os-development/linux.md). -2. If you don't have a local clone yet then do it now, so you have the local_filesystem/ folder: +2. Get the `local_filesystem/` folder + + You probably already have a local clone that contains the [internal_filesystem](https://github.com/MicroPythonOS/MicroPythonOS/tree/main/internal_filesystem). + + If not, then clone it now:
     ```
-    git clone https://github.com/MicroPythonOS/MicroPythonOS.git
+    git clone --recurse-submodules https://github.com/MicroPythonOS/MicroPythonOS.git
     cd MicroPythonOS/
     ```
     
-3. Start it: +3. Start it from the local_filesystem/ folder:
     ```

From 9f6307ca1c609bace29c8b89ef1da342fe9273fb Mon Sep 17 00:00:00 2001
From: Thomas Farstrike 
Date: Sun, 26 Oct 2025 11:28:34 +0100
Subject: [PATCH 07/18] Update docs

---
 docs/os-development/compiling.md          |  2 +-
 docs/os-development/running-on-desktop.md | 14 +++++++++++---
 2 files changed, 12 insertions(+), 4 deletions(-)

diff --git a/docs/os-development/compiling.md b/docs/os-development/compiling.md
index 93b7977..9e3f5cc 100644
--- a/docs/os-development/compiling.md
+++ b/docs/os-development/compiling.md
@@ -3,7 +3,7 @@
 1. **Make sure you're in the main repository**:
 
     ```
-    cd MicroPythonOS
+    cd MicroPythonOS/
     ```
 
 2. **Start the Compilation**
diff --git a/docs/os-development/running-on-desktop.md b/docs/os-development/running-on-desktop.md
index cf36ef9..a9d796d 100644
--- a/docs/os-development/running-on-desktop.md
+++ b/docs/os-development/running-on-desktop.md
@@ -1,8 +1,16 @@
 ## Running on Linux or MacOS
 
-1. Download a release binary (e.g., `MicroPythonOS_amd64_Linux`, `MicroPythonOS_amd64_MacOS`) or build your own [on MacOS](../os-development/macos.md) or [Linux](../os-development/linux.md).
+1. Make sure you have the software
 
-2. Get the `local_filesystem/` folder
+    Either you built your own [on MacOS](../os-development/macos.md) or [Linux](../os-development/linux.md) or you can download a pre-built executable binary (e.g., `MicroPythonOS_amd64_Linux`, `MicroPythonOS_amd64_MacOS`) from the [releases page](https://github.com/MicroPythonOS/MicroPythonOS/releases).
+
+    Give it executable permissions:
+
+    ```
+    chmod +x /path/to/MicroPythonOS_executable_binary
+    ``` 
+
+2. Make sure you have the `local_filesystem/` folder
 
     You probably already have a local clone that contains the [internal_filesystem](https://github.com/MicroPythonOS/MicroPythonOS/tree/main/internal_filesystem).
 
@@ -20,7 +28,7 @@
     
     ```
     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)"
+    /path/to/MicroPythonOS_executable_binary -X heapsize=32M -v -i -c "$(cat boot_unix.py main.py)"
     ```
     
From 711648c3de77b9bbd22d503f2ed4d3d9bae734df Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Sun, 26 Oct 2025 11:33:42 +0100 Subject: [PATCH 08/18] update docs --- docs/os-development/compiling.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/os-development/compiling.md b/docs/os-development/compiling.md index 9e3f5cc..33e4aa1 100644 --- a/docs/os-development/compiling.md +++ b/docs/os-development/compiling.md @@ -40,7 +40,7 @@ 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 + - `lvgl_micropython/build/lvgl_micropy_unix` + - `lvgl_micropython/build/lvgl_micropy_macOS` + - `lvgl_micropython/build/lvgl_micropy_ESP32_GENERIC_S3-SPIRAM_OCT-16.bin` From 0dee4682ffe176e47a5a7a1df17e8f0b1ab223d9 Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Wed, 29 Oct 2025 12:14:16 +0100 Subject: [PATCH 09/18] Align paths --- docs/architecture/filesystem.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 From 301132f2dba32705a17d1d8e41e4e9832c836d92 Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Wed, 29 Oct 2025 14:13:13 +0100 Subject: [PATCH 10/18] Start framework docs --- docs/frameworks/preferences.md | 125 +++++++++++++++++++++++++++++++++ mkdocs.yml | 2 + 2 files changed, 127 insertions(+) create mode 100644 docs/frameworks/preferences.md 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/mkdocs.yml b/mkdocs.yml index 9ea3382..7d17df0 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -46,6 +46,8 @@ nav: - App Store: apps/appstore.md - Creating Apps: apps/creating-apps.md - Bundling Apps: apps/bundling-apps.md + - Frameworks: + - Preferences: frameworks/preferences.md - Architecture: - Overview: architecture/overview.md - System Components: architecture/system-components.md From d211d00b946dfb69b18da605d04edf7b74f8f4b3 Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Wed, 29 Oct 2025 14:20:35 +0100 Subject: [PATCH 11/18] Simplify --- docs/os-development/macos.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/os-development/macos.md b/docs/os-development/macos.md index 549b319..3506848 100644 --- a/docs/os-development/macos.md +++ b/docs/os-development/macos.md @@ -17,7 +17,7 @@ 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 ``` From 3c03cf2317b28bf4fd2da3ff91638e00492602cb Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Wed, 29 Oct 2025 14:22:04 +0100 Subject: [PATCH 12/18] Styling --- docs/os-development/compiling.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/os-development/compiling.md b/docs/os-development/compiling.md index 33e4aa1..afacf62 100644 --- a/docs/os-development/compiling.md +++ b/docs/os-development/compiling.md @@ -16,16 +16,16 @@ ```
- **Target systems**: esp32, unix (= Linux) and macOS + **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. + - 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. + _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 + **Target devices**: `waveshare-esp32-s3-touch-lcd-2` or `fri3d-2024` **Examples**: From c919777889f1c61edfb078ab09dca892dcb27061 Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Wed, 29 Oct 2025 16:33:52 +0100 Subject: [PATCH 13/18] os-development/windows.md: add WSL remark --- docs/os-development/windows.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) 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). From edb11be8b21fecea6df7c2e3cd6ec179fb65b2da Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Sat, 22 Nov 2025 15:56:27 +0100 Subject: [PATCH 14/18] Update docs --- docs/os-development/compiling.md | 18 ++++------------- docs/os-development/installing-on-esp32.md | 23 +++++++++------------- docs/os-development/porting-guide.md | 22 +++++++++++---------- 3 files changed, 25 insertions(+), 38 deletions(-) diff --git a/docs/os-development/compiling.md b/docs/os-development/compiling.md index afacf62..6e5f953 100644 --- a/docs/os-development/compiling.md +++ b/docs/os-development/compiling.md @@ -12,29 +12,19 @@
     ```
-    ./scripts/build_mpos.sh   [optional target device]
+    ./scripts/build_mpos.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**:
     ```
-    ./scripts/build_mpos.sh esp32 prod fri3d-2024
-    ./scripts/build_mpos.sh esp32 dev waveshare-esp32-s3-touch-lcd-2
-    ./scripts/build_mpos.sh esp32 unix dev
-    ./scripts/build_mpos.sh esp32 macOS dev
+    ./scripts/build_mpos.sh esp32
+    ./scripts/build_mpos.sh unix
+    ./scripts/build_mpos.sh macOS
     ```
     
diff --git a/docs/os-development/installing-on-esp32.md b/docs/os-development/installing-on-esp32.md index 90d72eb..f97f6be 100644 --- a/docs/os-development/installing-on-esp32.md +++ b/docs/os-development/installing-on-esp32.md @@ -4,19 +4,20 @@ 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 write_flash 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 the `--erase-all` option if you want to erase the entire flash memory, so that no old files or apps will remain. @@ -33,36 +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) - The "dev" builds come without a filesystem so you probably want to copy: + 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 whole internal_filesystem/ folder, including main.py - - the appropriate device-specific internal_filesystem/boot*.py file to /boot.py on the device - 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/porting-guide.md b/docs/os-development/porting-guide.md index 7030c99..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/.py``` files. + ## Steps to port to a new device @@ -18,15 +19,17 @@ By design, the only device-specific code for MicroPythonOS is found in the ```in The goal is to have it boot and show a MicroPython REPL shell on the serial line. - Take a look at our [build_mpos.sh](https://github.com/MicroPythonOS/MicroPythonOS/blob/main/scripts/build_mpos.sh) script. A "dev" build (without any "frozen" filesystem) is preferred as this will still change a lot. + Take a look at our [build_mpos.sh](https://github.com/MicroPythonOS/MicroPythonOS/blob/main/scripts/build_mpos.sh) script. Also go over the [official lvgl_micropython documentation](https://github.com/lvgl-micropython/lvgl_micropython/blob/main/README.md) for porting instructions. If you're in luck, your device is already listed in the esp32 BOARD list. Otherwise use a generic one like `BOARD=ESP32_GENERIC` with `BOARD_VARIANT=SPIRAM` or `BOARD=ESP32_GENERIC_S3` with `BOARD_VARIANT=SPIRAM_OCT` if it has an SPIRAM. 2. Figure out how to initialize the display for the new device - Use the MicroPython REPL shell on the serial port to type or paste (CTRL-E) MicroPython code. + Use the MicroPython REPL shell on the serial port to type or paste (CTRL-E) MicroPython code manually at first to see what works. - Check out how it's done for the [Waveshare 2 inch Touch Screen](https://github.com/MicroPythonOS/MicroPythonOS/blob/main/internal_filesystem/boot.py) and for the [Fri3d Camp 2024 Badge](https://github.com/MicroPythonOS/MicroPythonOS/blob/main/internal_filesystem/boot_fri3d-2024.py). You essentially need to set the correct pins to which the display is connected (like `LCD_SCLK`, `LCD_MOSI`, `LCD_MOSI` etc.) and also set the resolution of the display (`TFT_HOR_RES`, `TFT_VER_RE`S). + Take a look at [```waveshare_esp32_s3_touch_lcd_2.py```](https://github.com/MicroPythonOS/MicroPythonOS/blob/main/internal_filesystem/lib/mpos/board/waveshare_esp32_s3_touch_lcd_2.py) or [```fri3d_2024.py```](https://github.com/MicroPythonOS/MicroPythonOS/blob/main/internal_filesystem/lib/mpos/board/fri3d-2024.py). + + You essentially need to set the correct pins to which the display is connected (like `LCD_SCLK`, `LCD_MOSI`, `LCD_MOSI` etc.) and also set the resolution of the display (`TFT_HOR_RES`, `TFT_VER_RE`S). After a failed attempt, reset the device to make sure the hardware is in a known initial state again. @@ -47,17 +50,16 @@ By design, the only device-specific code for MicroPythonOS is found in the ```in ``` -3. Put the initialization code in a custom boot_...py file for your device +3. Put the initialization code in a custom ```.py``` file for your device -4. Copy the custom boot_...py file and the generic MicroPythonOS files to your device (see [install.sh](https://github.com/MicroPythonOS/MicroPythonOS/blob/main/scripts/install.sh) +4. Copy the custom ```.py``` file and the generic MicroPythonOS files to your device (see [install.sh](https://github.com/MicroPythonOS/MicroPythonOS/blob/main/scripts/install.sh) - After reset, your custom boot_...py file should initialize the display and then MicroPythonOS should start, run the launcher, which shows the icons etc. + After reset, your custom ```.py``` file should initialize the display and then MicroPythonOS should start, run the launcher, which shows the icons etc. 5. Add more hardware support - If your device has a touch screen, check out how it's initialized for the [Waveshare 2 inch Touch Screen](https://github.com/MicroPythonOS/MicroPythonOS/blob/main/internal_filesystem/boot.py). - If it has buttons for input, check out the KeyPad code for the [Fri3d Camp 2024 Badge](https://github.com/MicroPythonOS/MicroPythonOS/blob/main/internal_filesystem/boot_fri3d-2024.py). + If your device has a touch screen, check out how it's initialized for the [Waveshare 2 inch Touch Screen](https://github.com/MicroPythonOS/MicroPythonOS/blob/main/internal_filesystem/lib/mpos/board/waveshare_esp32_s3_touch_lcd_2.py). If it has buttons for input, check out the KeyPad code for the [Fri3d Camp 2024 Badge](https://github.com/MicroPythonOS/MicroPythonOS/blob/main/internal_filesystem/lib/mpos/board/fri3d-2024.py). Now you should be able to control the device, connect to WiFi and install more apps from the AppStore. - This would be a good time to create a pull-request to merge your boot_...py file into the main codebase so the support becomes official! + This would be a good time to create a pull-request to merge your ```.py``` file into the main codebase so the support becomes official! From 47739cc4128c1bbc6cf47398a56b926f6e684b2b Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Thu, 4 Dec 2025 14:02:53 +0100 Subject: [PATCH 15/18] Add docs --- docs/frameworks/sensor-manager.md | 519 ++++++++++++++++++++++++++++++ mkdocs.yml | 1 + 2 files changed, 520 insertions(+) create mode 100644 docs/frameworks/sensor-manager.md diff --git a/docs/frameworks/sensor-manager.md b/docs/frameworks/sensor-manager.md new file mode 100644 index 0000000..6e20f65 --- /dev/null +++ b/docs/frameworks/sensor-manager.md @@ -0,0 +1,519 @@ +# 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** during calibration. + +### Manual Calibration + +```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...") + time.sleep(2) + + # 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.sensors/config.json` and loaded on boot. You only need to calibrate once (unless the device is moved to a different location or orientation changes significantly). + +## 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/mkdocs.yml b/mkdocs.yml index 7d17df0..bd37202 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -48,6 +48,7 @@ nav: - Bundling Apps: apps/bundling-apps.md - Frameworks: - Preferences: frameworks/preferences.md + - SensorManager: frameworks/sensor-manager.md - Architecture: - Overview: architecture/overview.md - System Components: architecture/system-components.md From f3f0f6cc716c41dee2ad4d6a83fa03bd18c956e5 Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Thu, 4 Dec 2025 14:26:03 +0100 Subject: [PATCH 16/18] Document AudioFlinger and LightsManager --- docs/frameworks/audioflinger.md | 432 ++++++++++++++++++++ docs/frameworks/lights-manager.md | 652 ++++++++++++++++++++++++++++++ mkdocs.yml | 2 + 3 files changed, 1086 insertions(+) create mode 100644 docs/frameworks/audioflinger.md create mode 100644 docs/frameworks/lights-manager.md 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/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/mkdocs.yml b/mkdocs.yml index bd37202..3f2b608 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -49,6 +49,8 @@ nav: - Frameworks: - Preferences: frameworks/preferences.md - SensorManager: frameworks/sensor-manager.md + - AudioFlinger: frameworks/audioflinger.md + - LightsManager: frameworks/lights-manager.md - Architecture: - Overview: architecture/overview.md - System Components: architecture/system-components.md From efc4b3e7046afef40670c457fa6304df8571220b Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Sun, 7 Dec 2025 09:11:47 +0100 Subject: [PATCH 17/18] Update docs --- build.sh | 2 +- docs/frameworks/sensor-manager.md | 21 +++++++++++++++++---- 2 files changed, 18 insertions(+), 5 deletions(-) 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/frameworks/sensor-manager.md b/docs/frameworks/sensor-manager.md index 6e20f65..4efd8a4 100644 --- a/docs/frameworks/sensor-manager.md +++ b/docs/frameworks/sensor-manager.md @@ -224,9 +224,21 @@ class GestureDetector(Activity): ## Calibration -Calibration removes sensor drift and improves accuracy. The device must be **stationary** during calibration. +Calibration removes sensor drift and improves accuracy. The device must be **stationary on a flat surface** during calibration. -### Manual 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): @@ -237,7 +249,7 @@ class SettingsActivity(Activity): # Show instructions self.status_label.set_text("Place device flat and still...") - time.sleep(2) + wait_for_render() # Let UI update # Calibrate accelerometer (100 samples) self.status_label.set_text("Calibrating accelerometer...") @@ -255,7 +267,7 @@ class SettingsActivity(Activity): ### Persistent Calibration -Calibration data is automatically saved to `data/com.micropythonos.sensors/config.json` and loaded on boot. You only need to calibrate once (unless the device is moved to a different location or orientation changes significantly). +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 @@ -437,6 +449,7 @@ Calibrates the sensor (device must be stationary). Returns calibration offsets. ### Sensor Object Properties: + - `name` - Human-readable sensor name - `type` - Sensor type constant - `vendor` - Manufacturer name From b068d85d480dfed1379086e35804b141643ee5da Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Wed, 17 Dec 2025 12:43:20 +0100 Subject: [PATCH 18/18] Add more frameworks --- docs/frameworks/download-manager.md | 672 ++++++++++++++++++++++ docs/frameworks/task-manager.md | 490 ++++++++++++++++ docs/os-development/running-on-desktop.md | 60 +- mkdocs.yml | 2 + 4 files changed, 1220 insertions(+), 4 deletions(-) create mode 100644 docs/frameworks/download-manager.md create mode 100644 docs/frameworks/task-manager.md 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/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/os-development/running-on-desktop.md b/docs/os-development/running-on-desktop.md index a9d796d..cec26c3 100644 --- a/docs/os-development/running-on-desktop.md +++ b/docs/os-development/running-on-desktop.md @@ -34,10 +34,62 @@ There's also a convenient `./scripts/run_desktop.sh` script that will attempt to start the latest build that you compiled yourself. -### Modifying files +### Development Workflow: Desktop vs Hardware -You'll notice that, whenever you change a file on your local system, the changes are immediately visible whenever you reload the file. +**IMPORTANT**: Understanding the difference between desktop testing and hardware deployment is critical for efficient development. -This results in a very quick coding cycle. +#### Desktop Development (Recommended for Most Development) -Give this a try by editing `internal_filesystem/builtin/apps/com.micropythonos.about/assets/about.py` and then restarting the "About" app. Powerful stuff! +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/mkdocs.yml b/mkdocs.yml index 3f2b608..9ca73a1 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -48,6 +48,8 @@ nav: - Bundling Apps: apps/bundling-apps.md - Frameworks: - Preferences: frameworks/preferences.md + - TaskManager: frameworks/task-manager.md + - DownloadManager: frameworks/download-manager.md - SensorManager: frameworks/sensor-manager.md - AudioFlinger: frameworks/audioflinger.md - LightsManager: frameworks/lights-manager.md