From 7f8867d4b74463398d9577f1037eceaefb8afba4 Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Sat, 25 Oct 2025 21:30:23 +0200 Subject: [PATCH 001/110] 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 002/110] 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 003/110] 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 004/110] 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 005/110] 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 006/110] 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 007/110] 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 008/110] 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 009/110] 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 010/110] 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 011/110] 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 012/110] 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 013/110] 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 014/110] 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 015/110] 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 016/110] 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 017/110] 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 018/110] 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 From ea35e0e6d4df6a672dade556ac471f0bbd616003 Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Wed, 17 Dec 2025 13:18:22 +0100 Subject: [PATCH 019/110] Fix enumerations --- docs/frameworks/audioflinger.md | 14 +++++++------- docs/frameworks/download-manager.md | 14 +++++++------- docs/frameworks/lights-manager.md | 12 ++++++------ docs/frameworks/sensor-manager.md | 10 +++++----- docs/frameworks/task-manager.md | 12 ++++++------ docs/os-development/running-on-desktop.md | 6 +++--- 6 files changed, 34 insertions(+), 34 deletions(-) diff --git a/docs/frameworks/audioflinger.md b/docs/frameworks/audioflinger.md index 2054032..470bd5c 100644 --- a/docs/frameworks/audioflinger.md +++ b/docs/frameworks/audioflinger.md @@ -6,13 +6,13 @@ MicroPythonOS provides a centralized audio service called **AudioFlinger**, insp 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 +- **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 diff --git a/docs/frameworks/download-manager.md b/docs/frameworks/download-manager.md index db2bbd2..e114691 100644 --- a/docs/frameworks/download-manager.md +++ b/docs/frameworks/download-manager.md @@ -6,13 +6,13 @@ MicroPythonOS provides a centralized HTTP download service called **DownloadMana 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) +- **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 diff --git a/docs/frameworks/lights-manager.md b/docs/frameworks/lights-manager.md index cb88862..33ac64e 100644 --- a/docs/frameworks/lights-manager.md +++ b/docs/frameworks/lights-manager.md @@ -6,12 +6,12 @@ MicroPythonOS provides a simple LED control service called **LightsManager** for 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 +- **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. diff --git a/docs/frameworks/sensor-manager.md b/docs/frameworks/sensor-manager.md index 4efd8a4..1fa88c7 100644 --- a/docs/frameworks/sensor-manager.md +++ b/docs/frameworks/sensor-manager.md @@ -12,11 +12,11 @@ SensorManager automatically detects available sensors on your device: 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 +- **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 diff --git a/docs/frameworks/task-manager.md b/docs/frameworks/task-manager.md index 4931e52..ae95647 100644 --- a/docs/frameworks/task-manager.md +++ b/docs/frameworks/task-manager.md @@ -6,12 +6,12 @@ MicroPythonOS provides a centralized task management service called **TaskManage 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 +- **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 diff --git a/docs/os-development/running-on-desktop.md b/docs/os-development/running-on-desktop.md index cec26c3..85bc317 100644 --- a/docs/os-development/running-on-desktop.md +++ b/docs/os-development/running-on-desktop.md @@ -42,9 +42,9 @@ 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 +- **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. From 690a2a9d7ba025f39b16007617ca9a3a350fd857 Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Wed, 17 Dec 2025 21:53:34 +0100 Subject: [PATCH 020/110] Update AudioFlinger docs --- docs/frameworks/audioflinger.md | 270 ++++++++++++++++++++++++++++++-- 1 file changed, 257 insertions(+), 13 deletions(-) diff --git a/docs/frameworks/audioflinger.md b/docs/frameworks/audioflinger.md index 470bd5c..b22b73f 100644 --- a/docs/frameworks/audioflinger.md +++ b/docs/frameworks/audioflinger.md @@ -1,6 +1,6 @@ # 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. +MicroPythonOS provides a centralized audio service called **AudioFlinger**, inspired by Android's architecture. It manages audio playback and recording across different hardware outputs with priority-based audio focus control. ## Overview @@ -8,18 +8,20 @@ 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 +- **Background playback/recording** - Runs in separate thread - **WAV file support** - 8/16/24/32-bit PCM, mono/stereo, auto-upsampling +- **WAV recording** - 16-bit mono PCM from I2S microphone - **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) +- **I2S Output**: Digital audio output for WAV file playback (Fri3d badge, Waveshare board) +- **I2S Input**: Microphone recording (Fri3d badge only) - **Buzzer**: PWM-based tone/ringtone playback (Fri3d badge only) - **Both**: Simultaneous I2S and buzzer support -- **Null**: No audio (desktop/Linux) +- **Null**: No audio (desktop/Linux - simulated for testing) ## Quick Start @@ -88,6 +90,130 @@ print(f"Current volume: {volume}") AudioFlinger.stop() ``` +## Recording Audio + +AudioFlinger supports recording audio from an I2S microphone to WAV files. + +### Basic Recording + +```python +import mpos.audio.audioflinger as AudioFlinger + +# Check if microphone is available +if AudioFlinger.has_microphone(): + # Record for 10 seconds + success = AudioFlinger.record_wav( + file_path="data/my_recording.wav", + duration_ms=10000, + sample_rate=16000, + on_complete=lambda msg: print(msg) + ) + + if success: + print("Recording started...") + else: + print("Recording failed to start") +``` + +### Recording Parameters + +- **file_path**: Path to save the WAV file +- **duration_ms**: Maximum recording duration in milliseconds (default: 60000 = 60 seconds) +- **sample_rate**: Sample rate in Hz (default: 16000 - good for voice) +- **on_complete**: Callback function called when recording finishes + +### Stopping Recording + +```python +# Stop recording early +AudioFlinger.stop() +``` + +### Recording Constraints + +1. **Cannot record while playing**: I2S can only be TX (output) or RX (input) at one time +2. **Cannot start new recording while recording**: Only one recording at a time +3. **Microphone required**: `has_microphone()` must return `True` + +```python +# Check recording state +if AudioFlinger.is_recording(): + print("Currently recording...") + +# Check playback state +if AudioFlinger.is_playing(): + print("Currently playing...") +``` + +### Complete Recording Example + +```python +from mpos.apps import Activity +import mpos.audio.audioflinger as AudioFlinger +import lvgl as lv +import time + +class SoundRecorderActivity(Activity): + def onCreate(self): + self.screen = lv.obj() + self.is_recording = False + + # Status label + self.status = lv.label(self.screen) + self.status.set_text("Ready") + self.status.align(lv.ALIGN.TOP_MID, 0, 20) + + # Record button + self.record_btn = lv.button(self.screen) + self.record_btn.set_size(120, 50) + self.record_btn.align(lv.ALIGN.CENTER, 0, 0) + self.record_label = lv.label(self.record_btn) + self.record_label.set_text("Record") + self.record_label.center() + self.record_btn.add_event_cb(self.toggle_recording, lv.EVENT.CLICKED, None) + + # Check microphone availability + if not AudioFlinger.has_microphone(): + self.status.set_text("No microphone") + self.record_btn.add_flag(lv.obj.FLAG.HIDDEN) + + self.setContentView(self.screen) + + def toggle_recording(self, event): + if self.is_recording: + AudioFlinger.stop() + self.is_recording = False + self.record_label.set_text("Record") + self.status.set_text("Stopped") + else: + # Generate timestamped filename + t = time.localtime() + filename = f"data/recording_{t[0]}{t[1]:02d}{t[2]:02d}_{t[3]:02d}{t[4]:02d}.wav" + + success = AudioFlinger.record_wav( + file_path=filename, + duration_ms=60000, # 60 seconds max + sample_rate=16000, + on_complete=self.on_recording_complete + ) + + if success: + self.is_recording = True + self.record_label.set_text("Stop") + self.status.set_text("Recording...") + else: + self.status.set_text("Failed to start") + + def on_recording_complete(self, message): + # Called from recording thread - update UI safely + self.update_ui_threadsafe_if_foreground(self._update_ui_after_recording, message) + + def _update_ui_after_recording(self, message): + self.is_recording = False + self.record_label.set_text("Record") + self.status.set_text(message) +``` + ## Audio Focus Priority AudioFlinger implements a 3-tier priority-based audio focus system inspired by Android: @@ -125,20 +251,29 @@ AudioFlinger.play_wav("alarm.wav", stream_type=AudioFlinger.STREAM_ALARM) ## 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) | +| Board | I2S Output | I2S Microphone | Buzzer | Notes | +|-------|------------|----------------|--------|-------| +| **Fri3d 2024 Badge** | ✅ | ✅ | ✅ | Full audio support | +| **Waveshare ESP32-S3** | ✅ | ❌ | ❌ | I2S output only | +| **Linux/macOS** | ❌ | ✅ (simulated) | ❌ | Simulated recording for testing | -**I2S Pins:** -- **BCLK** (Bit Clock): GPIO 2 +**I2S Output Pins (DAC/Speaker):** +- **BCK** (Bit Clock): GPIO 2 - **WS** (Word Select): GPIO 47 - **DOUT** (Data Out): GPIO 16 +**I2S Input Pins (Microphone):** +- **SCLK** (Serial Clock): GPIO 17 +- **WS** (Word Select): GPIO 47 (shared with output) +- **DIN** (Data In): GPIO 15 + **Buzzer Pin:** - **PWM**: GPIO 46 (Fri3d badge only) +!!! note "I2S Limitation" + The ESP32 I2S peripheral can only be in TX (output) or RX (input) mode at one time. + You cannot play and record simultaneously. + ## Configuration Audio device preference is configured in the Settings app under **"Advanced Settings"**: @@ -222,7 +357,7 @@ class SimpleMusicPlayerActivity(Activity): ## API Reference -### Functions +### Playback Functions **`play_wav(path, stream_type=STREAM_MUSIC, volume=None, on_complete=None)`** @@ -246,6 +381,39 @@ Play an RTTTL ringtone. - **Returns:** `bool` - `True` if playback started, `False` if rejected +### Recording Functions + +**`record_wav(file_path, duration_ms=None, on_complete=None, sample_rate=16000)`** + +Record audio from I2S microphone to WAV file. + +- **Parameters:** + - `file_path` (str): Path to save WAV file (e.g., `"data/recording.wav"`) + - `duration_ms` (int, optional): Recording duration in milliseconds. Default: 60000 (60 seconds) + - `on_complete` (callable, optional): Callback function called when recording finishes + - `sample_rate` (int, optional): Sample rate in Hz. Default: 16000 (good for voice) + +- **Returns:** `bool` - `True` if recording started, `False` if rejected + +- **Rejection reasons:** + - No microphone available (`has_microphone()` returns `False`) + - Currently playing audio (I2S can only be TX or RX) + - Already recording + +**`is_recording()`** + +Check if audio is currently being recorded. + +- **Returns:** `bool` - `True` if recording active, `False` otherwise + +**`has_microphone()`** + +Check if I2S microphone is available for recording. + +- **Returns:** `bool` - `True` if microphone configured, `False` otherwise + +### Volume and Control Functions + **`set_volume(volume)`** Set playback volume. @@ -261,7 +429,27 @@ Get current playback volume. **`stop()`** -Stop currently playing audio. +Stop currently playing audio or recording. + +**`is_playing()`** + +Check if audio is currently playing. + +- **Returns:** `bool` - `True` if playback active, `False` otherwise + +### Hardware Detection Functions + +**`has_i2s()`** + +Check if I2S audio output is available for WAV playback. + +- **Returns:** `bool` - `True` if I2S configured, `False` otherwise + +**`has_buzzer()`** + +Check if buzzer is available for RTTTL playback. + +- **Returns:** `bool` - `True` if buzzer configured, `False` otherwise ### Stream Type Constants @@ -387,6 +575,45 @@ ffmpeg -i input.wav -acodec pcm_s16le -ar 22050 -ac 1 output.wav - RTTTL requires hardware buzzer (Fri3d badge only) - I2S cannot produce RTTTL tones +### Recording Not Working + +**Symptom**: `record_wav()` returns `False` or no audio recorded + +**Possible causes:** + +1. **No microphone available** + ```python + if not AudioFlinger.has_microphone(): + print("No microphone on this device") + ``` + +2. **Currently playing audio** + ```python + # I2S can only be TX or RX, not both + if AudioFlinger.is_playing(): + AudioFlinger.stop() + time.sleep_ms(100) # Wait for cleanup + AudioFlinger.record_wav("recording.wav") + ``` + +3. **Already recording** + ```python + if AudioFlinger.is_recording(): + print("Already recording") + ``` + +4. **Wrong board** - Waveshare doesn't have a microphone + +### Recording Quality Issues + +**Symptom**: Recording sounds distorted or has noise + +**Solutions:** + +1. **Use appropriate sample rate**: 16000 Hz is good for voice, 44100 Hz for music +2. **Check microphone placement**: Keep microphone away from noise sources +3. **Verify I2S pin configuration**: Check board file for correct pin assignments + ## Performance Tips ### Optimizing WAV Files @@ -425,6 +652,23 @@ AudioFlinger.play_wav( - Buffers are allocated during playback and freed after - Multiple simultaneous streams not supported (priority system prevents this) +## Desktop Testing + +On desktop builds (Linux/macOS), AudioFlinger provides simulated recording for testing: + +- **Microphone simulation**: Generates a 440Hz sine wave instead of real audio +- **WAV file generation**: Creates valid WAV files that can be played back +- **Real-time simulation**: Recording runs at realistic speed + +This allows testing the Sound Recorder app and other recording features without hardware. + +```python +# Desktop simulation is automatic when machine.I2S is not available +# The generated WAV file contains a 440Hz tone +AudioFlinger.record_wav("test.wav", duration_ms=5000) +# Creates a valid 5-second WAV file with 440Hz sine wave +``` + ## See Also - [Creating Apps](../apps/creating-apps.md) - Learn how to create MicroPythonOS apps From 17b76c3a406a1c12bd6420d9eb14fdb28070e059 Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Fri, 19 Dec 2025 15:25:37 +0100 Subject: [PATCH 021/110] Document ConnectivityManager and WifiService --- docs/frameworks/connectivity-manager.md | 467 +++++++++++++++++ docs/frameworks/wifi-service.md | 638 ++++++++++++++++++++++++ mkdocs.yml | 2 + 3 files changed, 1107 insertions(+) create mode 100644 docs/frameworks/connectivity-manager.md create mode 100644 docs/frameworks/wifi-service.md diff --git a/docs/frameworks/connectivity-manager.md b/docs/frameworks/connectivity-manager.md new file mode 100644 index 0000000..5f1b508 --- /dev/null +++ b/docs/frameworks/connectivity-manager.md @@ -0,0 +1,467 @@ +# ConnectivityManager + +MicroPythonOS provides a connectivity monitoring service called **ConnectivityManager** that tracks network status and notifies apps when connectivity changes. It's inspired by Android's ConnectivityManager API. + +## Overview + +ConnectivityManager provides: + +- **Connectivity monitoring** - Periodic checks of network status +- **Callback notifications** - Get notified when connectivity changes +- **Singleton pattern** - Single instance shared across all apps +- **Platform-agnostic** - Works on ESP32 and desktop +- **Android-like API** - Familiar patterns for Android developers + +## Quick Start + +### Checking Connectivity + +```python +from mpos.net.connectivity_manager import ConnectivityManager + +# Get the singleton instance +cm = ConnectivityManager.get() + +# Check if online (has internet) +if cm.is_online(): + print("Connected to internet") +else: + print("No internet connection") + +# Check if WiFi is connected (local network) +if cm.is_wifi_connected(): + print("WiFi connected") +``` + +### Registering for Connectivity Changes + +```python +from mpos.app.activity import Activity +from mpos.net.connectivity_manager import ConnectivityManager + +class MyActivity(Activity): + def onCreate(self): + self.cm = ConnectivityManager.get() + + def onResume(self, screen): + super().onResume(screen) + # Register for connectivity changes + self.cm.register_callback(self.on_connectivity_changed) + + # Update UI with current status + self.update_status(self.cm.is_online()) + + def onPause(self, screen): + super().onPause(screen) + # Unregister when activity is paused + self.cm.unregister_callback(self.on_connectivity_changed) + + def on_connectivity_changed(self, is_online): + """Called when connectivity status changes.""" + if is_online: + print("Now online!") + self.fetch_data() + else: + print("Gone offline") + self.show_offline_message() + + def update_status(self, is_online): + status = "Online" if is_online else "Offline" + self.status_label.set_text(status) +``` + +### Waiting for Connectivity + +```python +from mpos.net.connectivity_manager import ConnectivityManager + +def download_with_wait(): + cm = ConnectivityManager.get() + + # Wait up to 30 seconds for connectivity + if cm.wait_until_online(timeout=30): + print("Connected! Starting download...") + # Proceed with download + else: + print("Timeout waiting for connection") +``` + +## Common Patterns + +### Network-Aware App + +```python +from mpos.app.activity import Activity +from mpos.net.connectivity_manager import ConnectivityManager +from mpos import TaskManager, DownloadManager +import lvgl as lv + +class NewsReaderActivity(Activity): + def onCreate(self): + self.screen = lv.obj() + self.cm = ConnectivityManager.get() + + # Status indicator + self.status_icon = lv.label(self.screen) + self.status_icon.align(lv.ALIGN.TOP_RIGHT, -10, 10) + + # Content area + self.content = lv.label(self.screen) + self.content.set_width(280) + self.content.align(lv.ALIGN.CENTER, 0, 0) + + self.setContentView(self.screen) + + def onResume(self, screen): + super().onResume(screen) + self.cm.register_callback(self.on_connectivity_changed) + self.update_ui() + + def onPause(self, screen): + super().onPause(screen) + self.cm.unregister_callback(self.on_connectivity_changed) + + def on_connectivity_changed(self, is_online): + self.update_ui() + if is_online: + TaskManager.create_task(self.fetch_news()) + + def update_ui(self): + if self.cm.is_online(): + self.status_icon.set_text(lv.SYMBOL.WIFI) + else: + self.status_icon.set_text(lv.SYMBOL.WARNING) + self.content.set_text("No internet connection") + + async def fetch_news(self): + data = await DownloadManager.download_url( + "https://api.example.com/news" + ) + if data: + self.content.set_text(data.decode()) +``` + +### Retry on Reconnect + +```python +from mpos.net.connectivity_manager import ConnectivityManager +from mpos import TaskManager, DownloadManager + +class SyncManager: + def __init__(self): + self.cm = ConnectivityManager.get() + self.pending_sync = False + self.cm.register_callback(self.on_connectivity_changed) + + def sync_data(self): + if self.cm.is_online(): + TaskManager.create_task(self._do_sync()) + else: + # Mark for retry when online + self.pending_sync = True + print("Offline - sync queued") + + def on_connectivity_changed(self, is_online): + if is_online and self.pending_sync: + print("Back online - retrying sync") + self.pending_sync = False + TaskManager.create_task(self._do_sync()) + + async def _do_sync(self): + success = await DownloadManager.download_url( + "https://api.example.com/sync", + # ... sync data + ) + if not success: + self.pending_sync = True +``` + +### Conditional Feature Loading + +```python +from mpos.net.connectivity_manager import ConnectivityManager + +class AppStoreActivity(Activity): + def onCreate(self): + self.cm = ConnectivityManager.get() + + if self.cm.is_online(): + # Load full app store with remote data + self.load_remote_apps() + else: + # Show only installed apps + self.load_local_apps() + self.show_offline_banner() +``` + +## API Reference + +### Getting the Instance + +#### `ConnectivityManager.get()` + +Get the singleton ConnectivityManager instance. + +**Returns:** +- `ConnectivityManager` - The singleton instance + +**Example:** +```python +cm = ConnectivityManager.get() +``` + +**Note:** The first call initializes the manager and starts periodic connectivity checks. + +### Connectivity Status + +#### `is_online()` + +Check if the device has internet connectivity. + +**Returns:** +- `bool` - `True` if online, `False` otherwise + +**Example:** +```python +if cm.is_online(): + print("Internet available") +``` + +--- + +#### `is_wifi_connected()` + +Check if WiFi is connected (local network). + +**Returns:** +- `bool` - `True` if WiFi connected, `False` otherwise + +**Note:** A device can be WiFi-connected but not online (e.g., no internet on the network). + +**Example:** +```python +if cm.is_wifi_connected(): + print("WiFi connected") +``` + +--- + +#### `wait_until_online(timeout=60)` + +Block until the device is online or timeout expires. + +**Parameters:** +- `timeout` (int) - Maximum seconds to wait (default: 60) + +**Returns:** +- `bool` - `True` if online, `False` if timeout expired + +**Example:** +```python +if cm.wait_until_online(timeout=30): + print("Connected!") +else: + print("Timeout - still offline") +``` + +### Callback Management + +#### `register_callback(callback)` + +Register a callback to be notified of connectivity changes. + +**Parameters:** +- `callback` (callable) - Function that takes one boolean parameter (`is_online`) + +**Example:** +```python +def on_change(is_online): + print(f"Connectivity: {'online' if is_online else 'offline'}") + +cm.register_callback(on_change) +``` + +--- + +#### `unregister_callback(callback)` + +Unregister a previously registered callback. + +**Parameters:** +- `callback` (callable) - The callback to remove + +**Example:** +```python +cm.unregister_callback(on_change) +``` + +**Important:** Always unregister callbacks when your activity is paused or destroyed to prevent memory leaks and stale references. + +## Monitoring Behavior + +ConnectivityManager uses a periodic timer to check connectivity: + +| Setting | Value | +|---------|-------| +| Check interval | 8 seconds | +| Timer ID | 1 (Timer 0 is used by task_handler) | +| Check method | `wlan.isconnected()` | + +### How Connectivity is Determined + +1. **ESP32/Hardware:** + - Uses `network.WLAN(network.STA_IF).isconnected()` + - Returns `True` if WiFi is connected to an access point + +2. **Desktop/Linux:** + - No network module available + - Always reports as "connected" for testing + +### Callback Timing + +- Callbacks are only called when status **changes** +- Initial status is checked at startup (no callback) +- Callbacks receive `True` when going online, `False` when going offline + +```python +# Example callback sequence: +# Boot: offline (no callback - initial state) +# WiFi connects: callback(True) +# WiFi disconnects: callback(False) +# WiFi reconnects: callback(True) +``` + +## Platform Differences + +| Platform | Behavior | +|----------|----------| +| **ESP32** | Real WiFi monitoring via `network` module | +| **Desktop** | Always reports online (no network module) | + +### Desktop Testing + +On desktop, ConnectivityManager always reports as connected: + +```python +cm = ConnectivityManager.get() +print(cm.is_online()) # Always True on desktop +print(cm.is_wifi_connected()) # Always True on desktop +``` + +This allows apps to be tested without actual network connectivity. + +## Troubleshooting + +### Callbacks Not Being Called + +**Symptom:** Registered callback never fires + +**Possible causes:** +1. Connectivity never changes +2. Callback was unregistered +3. Callback raises exception (silently caught) + +**Solution:** +```python +# Check current status +print(f"Current status: {cm.is_online()}") + +# Verify callback is registered +print(f"Callbacks: {len(cm.callbacks)}") + +# Ensure callback doesn't raise exceptions +def safe_callback(is_online): + try: + # Your logic here + pass + except Exception as e: + print(f"Callback error: {e}") + +cm.register_callback(safe_callback) +``` + +### Memory Leak from Callbacks + +**Symptom:** Memory usage grows over time + +**Cause:** Callbacks not unregistered when activity destroyed + +**Solution:** +```python +class MyActivity(Activity): + def onResume(self, screen): + super().onResume(screen) + self.cm = ConnectivityManager.get() + self.cm.register_callback(self.on_change) + + def onPause(self, screen): + super().onPause(screen) + # Always unregister! + self.cm.unregister_callback(self.on_change) +``` + +### Status Always Shows Online + +**Symptom:** `is_online()` returns `True` even when disconnected + +**Cause:** Running on desktop (no network module) + +**Solution:** This is expected behavior on desktop. Test on real hardware for accurate connectivity status. + +### Timer Conflict + +**Symptom:** Strange behavior with other timers + +**Cause:** ConnectivityManager uses Timer(1) + +**Solution:** Use different timer IDs in your app: +```python +from machine import Timer + +# ConnectivityManager uses Timer(1) +# task_handler uses Timer(0) +# Use Timer(2) or higher for your app +my_timer = Timer(2) +``` + +## Integration with WifiService + +ConnectivityManager works alongside WifiService: + +- **WifiService** - Manages WiFi connections (connect, disconnect, scan) +- **ConnectivityManager** - Monitors connection status and notifies apps + +```python +from mpos.net.wifi_service import WifiService +from mpos.net.connectivity_manager import ConnectivityManager + +# WifiService for connection management +WifiService.attempt_connecting("MyNetwork", "password") + +# ConnectivityManager for status monitoring +cm = ConnectivityManager.get() +cm.register_callback(lambda online: print(f"Status: {online}")) +``` + +## Implementation Details + +**Location:** `MicroPythonOS/internal_filesystem/lib/mpos/net/connectivity_manager.py` + +**Pattern:** Singleton with periodic timer + +**Key features:** +- `_instance` - Class-level singleton instance +- `callbacks` - List of registered callback functions +- `_check_timer` - Machine Timer for periodic checks +- `_is_online` - Cached online status + +**Dependencies:** +- `network` module (MicroPython, optional) +- `machine.Timer` - Periodic connectivity checks +- `requests` / `usocket` - Imported but not currently used for connectivity checks + +## See Also + +- [WifiService](wifi-service.md) - WiFi connection management +- [DownloadManager](download-manager.md) - HTTP downloads +- [TaskManager](task-manager.md) - Async task management diff --git a/docs/frameworks/wifi-service.md b/docs/frameworks/wifi-service.md new file mode 100644 index 0000000..1cfd41a --- /dev/null +++ b/docs/frameworks/wifi-service.md @@ -0,0 +1,638 @@ +# WifiService + +MicroPythonOS provides a centralized WiFi management service called **WifiService** that handles WiFi connections, network scanning, and credential storage. It's designed to work alongside ConnectivityManager for comprehensive network management. + +## Overview + +WifiService provides: + +- **Auto-connect on boot** - Automatically connects to saved networks when device starts +- **Network scanning** - Scan for available WiFi networks +- **Credential management** - Save, retrieve, and forget network passwords +- **Hidden network support** - Connect to networks that don't broadcast SSID +- **Concurrent access locking** - Thread-safe WiFi operations +- **Desktop simulation** - Mock WiFi for desktop testing +- **ADC2 compatibility** - Temporarily disable WiFi for ESP32-S3 ADC2 operations + +## Quick Start + +### Checking Connection Status + +```python +from mpos.net.wifi_service import WifiService + +# Check if WiFi is connected +if WifiService.is_connected(): + ssid = WifiService.get_current_ssid() + print(f"Connected to: {ssid}") +else: + print("Not connected to WiFi") +``` + +### Scanning for Networks + +```python +from mpos.net.wifi_service import WifiService + +# Scan for available networks +networks = WifiService.scan_networks() + +for ssid in networks: + print(f"Found network: {ssid}") +``` + +**Note:** `scan_networks()` manages the busy flag automatically. If WiFi is already busy (e.g., connecting), it returns an empty list. + +### Connecting to a Network + +```python +from mpos.net.wifi_service import WifiService + +# Save network credentials first +WifiService.save_network("MyNetwork", "password123") + +# Connect to the network +success = WifiService.attempt_connecting("MyNetwork", "password123") + +if success: + print("Connected!") +else: + print("Connection failed") +``` + +### Managing Saved Networks + +```python +from mpos.net.wifi_service import WifiService + +# Save a network +WifiService.save_network("HomeWiFi", "mypassword") + +# Save a hidden network +WifiService.save_network("HiddenNetwork", "secret", hidden=True) + +# Get list of saved networks +saved = WifiService.get_saved_networks() +print(f"Saved networks: {saved}") + +# Get password for a network +password = WifiService.get_network_password("HomeWiFi") + +# Forget a network +WifiService.forget_network("OldNetwork") +``` + +## Common Patterns + +### WiFi Settings Screen + +```python +from mpos.app.activity import Activity +from mpos.net.wifi_service import WifiService +import lvgl as lv + +class WifiSettingsActivity(Activity): + def onCreate(self): + self.screen = lv.obj() + + # Status label + self.status = lv.label(self.screen) + self.update_status() + self.status.align(lv.ALIGN.TOP_MID, 0, 10) + + # Scan button + scan_btn = lv.button(self.screen) + scan_btn.set_size(120, 40) + scan_btn.align(lv.ALIGN.TOP_MID, 0, 50) + scan_label = lv.label(scan_btn) + scan_label.set_text("Scan") + scan_label.center() + scan_btn.add_event_cb(self.on_scan, lv.EVENT.CLICKED, None) + + # Network list + self.network_list = lv.list(self.screen) + self.network_list.set_size(280, 150) + self.network_list.align(lv.ALIGN.CENTER, 0, 30) + + self.setContentView(self.screen) + + def update_status(self): + if WifiService.is_connected(): + ssid = WifiService.get_current_ssid() + self.status.set_text(f"Connected: {ssid}") + else: + self.status.set_text("Not connected") + + def on_scan(self, event): + if WifiService.is_busy(): + print("WiFi is busy") + return + + # Clear list + self.network_list.clean() + + # Scan for networks + networks = WifiService.scan_networks() + saved = WifiService.get_saved_networks() + + for ssid in networks: + btn = self.network_list.add_button(None, ssid) + if ssid in saved: + # Mark saved networks + btn.set_style_bg_color(lv.color_hex(0x4CAF50), 0) + btn.add_event_cb( + lambda e, s=ssid: self.on_network_selected(s), + lv.EVENT.CLICKED, + None + ) + + def on_network_selected(self, ssid): + # Show password dialog or connect if already saved + password = WifiService.get_network_password(ssid) + if password: + success = WifiService.attempt_connecting(ssid, password) + if success: + self.update_status() +``` + +### Background Auto-Connect + +WifiService automatically handles auto-connect on boot. This is typically started from `main.py`: + +```python +import _thread +from mpos.net.wifi_service import WifiService +import mpos.apps + +# Start auto-connect in background thread +_thread.stack_size(mpos.apps.good_stack_size()) +_thread.start_new_thread(WifiService.auto_connect, ()) +``` + +**Auto-connect behavior:** +1. Loads saved networks from SharedPreferences +2. Scans for available networks +3. Tries to connect to saved networks (strongest signal first) +4. Also tries hidden networks that don't appear in scan +5. Syncs time via NTP on successful connection +6. Disables WiFi if no networks found (power saving) + +### Temporarily Disabling WiFi for ADC2 + +On ESP32-S3, ADC2 pins (GPIO11-20) don't work when WiFi is active. WifiService provides methods to temporarily disable WiFi: + +```python +from mpos.net.wifi_service import WifiService + +def read_adc2_sensor(): + """Read from ADC2 pin which requires WiFi to be disabled.""" + try: + # Disable WiFi (raises RuntimeError if WiFi is busy) + was_connected = WifiService.temporarily_disable() + + # Now safe to read ADC2 + from machine import ADC, Pin + adc = ADC(Pin(15)) # GPIO15 is on ADC2 + value = adc.read() + + return value + + finally: + # Re-enable WiFi (reconnects if was connected) + WifiService.temporarily_enable(was_connected) +``` + +**Important:** Always call `temporarily_enable()` in a `finally` block to ensure WiFi is re-enabled. + +## API Reference + +### Connection Functions + +#### `WifiService.connect(network_module=None)` + +Scan for available networks and connect to the first saved network found. + +**Parameters:** +- `network_module` - Network module for dependency injection (testing) + +**Returns:** +- `bool` - `True` if successfully connected, `False` otherwise + +**Example:** +```python +if WifiService.connect(): + print("Connected to a saved network") +``` + +--- + +#### `WifiService.attempt_connecting(ssid, password, network_module=None, time_module=None)` + +Attempt to connect to a specific WiFi network. + +**Parameters:** +- `ssid` (str) - Network SSID to connect to +- `password` (str) - Network password +- `network_module` - Network module for dependency injection (testing) +- `time_module` - Time module for dependency injection (testing) + +**Returns:** +- `bool` - `True` if successfully connected, `False` otherwise + +**Behavior:** +- Waits up to 10 seconds for connection +- Syncs time via NTP on success +- Returns `False` if WiFi is disabled during connection + +**Example:** +```python +success = WifiService.attempt_connecting("MyNetwork", "password123") +``` + +--- + +#### `WifiService.auto_connect(network_module=None, time_module=None)` + +Auto-connect to a saved WiFi network on boot. + +**Parameters:** +- `network_module` - Network module for dependency injection (testing) +- `time_module` - Time module for dependency injection (testing) + +**Behavior:** +- Loads saved networks from SharedPreferences +- Checks if WiFi is busy before proceeding +- Tries saved networks in order of signal strength +- Also tries hidden networks +- Disables WiFi if no connection made (power saving) + +**Example:** +```python +import _thread +_thread.start_new_thread(WifiService.auto_connect, ()) +``` + +--- + +#### `WifiService.disconnect(network_module=None)` + +Disconnect from current WiFi network and disable WiFi. + +**Parameters:** +- `network_module` - Network module for dependency injection (testing) + +**Example:** +```python +WifiService.disconnect() +``` + +--- + +#### `WifiService.is_connected(network_module=None)` + +Check if WiFi is currently connected. + +**Parameters:** +- `network_module` - Network module for dependency injection (testing) + +**Returns:** +- `bool` - `True` if connected, `False` otherwise + +**Note:** Returns `False` if WiFi operations are in progress (`wifi_busy` is `True`). + +**Example:** +```python +if WifiService.is_connected(): + print("WiFi is connected") +``` + +--- + +#### `WifiService.get_current_ssid(network_module=None)` + +Get the SSID of the currently connected network. + +**Parameters:** +- `network_module` - Network module for dependency injection (testing) + +**Returns:** +- `str` or `None` - Current SSID if connected, `None` otherwise + +**Example:** +```python +ssid = WifiService.get_current_ssid() +if ssid: + print(f"Connected to: {ssid}") +``` + +### Scanning Functions + +#### `WifiService.scan_networks(network_module=None)` + +Scan for available WiFi networks. + +**Parameters:** +- `network_module` - Network module for dependency injection (testing) + +**Returns:** +- `list` - List of SSID strings found + +**Behavior:** +- Manages `wifi_busy` flag automatically +- Returns empty list if WiFi is already busy +- Filters out empty SSIDs and invalid lengths +- Returns mock data on desktop + +**Example:** +```python +networks = WifiService.scan_networks() +for ssid in networks: + print(f"Found: {ssid}") +``` + +--- + +#### `WifiService.is_busy()` + +Check if WiFi operations are currently in progress. + +**Returns:** +- `bool` - `True` if WiFi is busy, `False` if available + +**Example:** +```python +if not WifiService.is_busy(): + networks = WifiService.scan_networks() +``` + +### Credential Management + +#### `WifiService.save_network(ssid, password, hidden=False)` + +Save a new WiFi network credential. + +**Parameters:** +- `ssid` (str) - Network SSID +- `password` (str) - Network password +- `hidden` (bool) - Whether this is a hidden network (default: `False`) + +**Behavior:** +- Saves to SharedPreferences (`com.micropythonos.system.wifiservice`) +- Updates class-level cache +- Hidden networks are always tried during auto-connect + +**Example:** +```python +# Save regular network +WifiService.save_network("HomeWiFi", "password123") + +# Save hidden network +WifiService.save_network("SecretNetwork", "hidden_pass", hidden=True) +``` + +--- + +#### `WifiService.forget_network(ssid)` + +Remove a saved WiFi network. + +**Parameters:** +- `ssid` (str) - Network SSID to forget + +**Returns:** +- `bool` - `True` if network was found and removed, `False` otherwise + +**Example:** +```python +if WifiService.forget_network("OldNetwork"): + print("Network forgotten") +else: + print("Network not found") +``` + +--- + +#### `WifiService.get_saved_networks()` + +Get list of saved network SSIDs. + +**Returns:** +- `list` - List of saved SSID strings + +**Example:** +```python +saved = WifiService.get_saved_networks() +print(f"Saved networks: {saved}") +``` + +--- + +#### `WifiService.get_network_password(ssid)` + +Get the saved password for a network. + +**Parameters:** +- `ssid` (str) - Network SSID + +**Returns:** +- `str` or `None` - Password if found, `None` otherwise + +**Example:** +```python +password = WifiService.get_network_password("HomeWiFi") +if password: + print("Password found") +``` + +### ADC2 Compatibility + +#### `WifiService.temporarily_disable(network_module=None)` + +Temporarily disable WiFi for operations that require it (e.g., ESP32-S3 ADC2). + +**Parameters:** +- `network_module` - Network module for dependency injection (testing) + +**Returns:** +- `bool` - `True` if WiFi was connected before disabling, `False` otherwise + +**Raises:** +- `RuntimeError` - If WiFi operations are already in progress + +**Example:** +```python +try: + was_connected = WifiService.temporarily_disable() + # Do ADC2 operations here +finally: + WifiService.temporarily_enable(was_connected) +``` + +--- + +#### `WifiService.temporarily_enable(was_connected, network_module=None)` + +Re-enable WiFi after temporary disable operation. + +**Parameters:** +- `was_connected` (bool) - Return value from `temporarily_disable()` +- `network_module` - Network module for dependency injection (testing) + +**Behavior:** +- Clears `wifi_busy` flag +- Starts auto-connect thread if WiFi was previously connected + +**Example:** +```python +WifiService.temporarily_enable(was_connected) +``` + +## Desktop Mode + +On desktop (Linux/macOS), WifiService provides simulated behavior for testing: + +| Method | Desktop Behavior | +|--------|------------------| +| `is_connected()` | Always returns `True` | +| `scan_networks()` | Returns mock SSIDs: "Home WiFi", "Pretty Fly for a Wi Fi", etc. | +| `attempt_connecting()` | Simulates 2-second connection delay, always succeeds | +| `get_current_ssid()` | Returns simulated connected SSID | +| `disconnect()` | Prints message, no-op | + +This allows WiFi-related apps to be tested on desktop without actual WiFi hardware. + +## Credential Storage + +Network credentials are stored using SharedPreferences: + +- **Preference name:** `com.micropythonos.system.wifiservice` +- **Key:** `access_points` +- **Format:** Dictionary `{ssid: {password: "...", hidden: bool}}` + +**Storage location:** `data/com.micropythonos.system.wifiservice/prefs.json` + +```json +{ + "access_points": { + "HomeWiFi": {"password": "mypassword"}, + "HiddenNetwork": {"password": "secret", "hidden": true} + } +} +``` + +## Troubleshooting + +### WiFi Won't Connect + +**Symptom:** `attempt_connecting()` returns `False` + +**Possible causes:** +1. Wrong password +2. Network out of range +3. WiFi hardware issue + +**Solution:** +```python +# Check if WiFi is busy +if WifiService.is_busy(): + print("WiFi is busy with another operation") + return + +# Try connecting with verbose output +success = WifiService.attempt_connecting("SSID", "password") +# Check console for "WifiService:" messages +``` + +### Scan Returns Empty List + +**Symptom:** `scan_networks()` returns `[]` + +**Possible causes:** +1. WiFi is busy (connecting, scanning) +2. No networks in range +3. WiFi hardware not initialized + +**Solution:** +```python +# Check if busy first +if WifiService.is_busy(): + print("WiFi is busy, try again later") +else: + networks = WifiService.scan_networks() + if not networks: + print("No networks found in range") +``` + +### Auto-Connect Not Working + +**Symptom:** Device doesn't connect to saved networks on boot + +**Possible causes:** +1. No networks saved +2. Saved networks not in range +3. WiFi busy flag stuck + +**Solution:** +```python +# Check saved networks +saved = WifiService.get_saved_networks() +print(f"Saved networks: {saved}") + +# Check busy state +print(f"WiFi busy: {WifiService.is_busy()}") + +# Manually trigger auto-connect +import _thread +_thread.start_new_thread(WifiService.auto_connect, ()) +``` + +### ADC2 RuntimeError + +**Symptom:** `RuntimeError: Cannot disable WiFi: WifiService is already busy` + +**Cause:** Trying to disable WiFi while scanning or connecting + +**Solution:** +```python +# Wait for WiFi to be available +import time +while WifiService.is_busy(): + time.sleep(0.1) + +# Now safe to disable +was_connected = WifiService.temporarily_disable() +``` + +### Hidden Network Not Connecting + +**Symptom:** Hidden network saved but never connects + +**Cause:** Network not marked as hidden when saved + +**Solution:** +```python +# Save with hidden=True +WifiService.save_network("HiddenSSID", "password", hidden=True) +``` + +## Implementation Details + +**Location:** `MicroPythonOS/internal_filesystem/lib/mpos/net/wifi_service.py` + +**Pattern:** Static class with class-level state + +**Key features:** +- `wifi_busy` - Class-level lock for concurrent access +- `access_points` - Cached dictionary of saved networks +- `_desktop_connected_ssid` - Simulated SSID for desktop mode + +**Dependencies:** +- `network` module (MicroPython, not available on desktop) +- `mpos.config.SharedPreferences` - Credential storage +- `mpos.time` - NTP time sync after connection + +## See Also + +- [ConnectivityManager](connectivity-manager.md) - Network connectivity monitoring +- [DownloadManager](download-manager.md) - HTTP downloads +- [SharedPreferences](preferences.md) - Persistent storage diff --git a/mkdocs.yml b/mkdocs.yml index 9ca73a1..fc2107f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -50,6 +50,8 @@ nav: - Preferences: frameworks/preferences.md - TaskManager: frameworks/task-manager.md - DownloadManager: frameworks/download-manager.md + - WifiService: frameworks/wifi-service.md + - ConnectivityManager: frameworks/connectivity-manager.md - SensorManager: frameworks/sensor-manager.md - AudioFlinger: frameworks/audioflinger.md - LightsManager: frameworks/lights-manager.md From cb5f4ffade8bfa36855109a0e6ea24e9e086f339 Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Fri, 19 Dec 2025 15:28:01 +0100 Subject: [PATCH 022/110] Alphabetical --- mkdocs.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/mkdocs.yml b/mkdocs.yml index fc2107f..6a13c23 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -47,14 +47,14 @@ nav: - Creating Apps: apps/creating-apps.md - Bundling Apps: apps/bundling-apps.md - Frameworks: + - AudioFlinger: frameworks/audioflinger.md + - ConnectivityManager: frameworks/connectivity-manager.md + - DownloadManager: frameworks/download-manager.md + - LightsManager: frameworks/lights-manager.md - Preferences: frameworks/preferences.md + - SensorManager: frameworks/sensor-manager.md - TaskManager: frameworks/task-manager.md - - DownloadManager: frameworks/download-manager.md - WifiService: frameworks/wifi-service.md - - ConnectivityManager: frameworks/connectivity-manager.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 1dc6ab31aa5fdb55fd9b33061e3a834b23c8ba30 Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Wed, 24 Dec 2025 10:56:58 +0100 Subject: [PATCH 023/110] Fix broken links --- docs/os-development/porting-guide.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/os-development/porting-guide.md b/docs/os-development/porting-guide.md index bf8b449..a6a311b 100644 --- a/docs/os-development/porting-guide.md +++ b/docs/os-development/porting-guide.md @@ -27,7 +27,7 @@ By design, the only device-specific code for MicroPythonOS is found in the ```in Use the MicroPython REPL shell on the serial port to type or paste (CTRL-E) MicroPython code manually at first to see what works. - 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). + 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). @@ -58,7 +58,7 @@ By design, the only device-specific code for MicroPythonOS is found in the ```in 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/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). + 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. From b295b3bcb678c21a4f979cca244c550635ddae6f Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Wed, 24 Dec 2025 14:34:56 +0100 Subject: [PATCH 024/110] Update docs --- docs/frameworks/download-manager.md | 143 ++++++++++++++++++++++------ 1 file changed, 112 insertions(+), 31 deletions(-) diff --git a/docs/frameworks/download-manager.md b/docs/frameworks/download-manager.md index e114691..f208844 100644 --- a/docs/frameworks/download-manager.md +++ b/docs/frameworks/download-manager.md @@ -164,7 +164,70 @@ success = await DownloadManager.download_url( ) ``` -### Helper Functions +### Utility Functions + +#### `DownloadManager.is_network_error(exception)` + +Check if an exception is a recoverable network error. + +Recognizes common network error codes and messages that indicate temporary connectivity issues that can be retried. + +**Parameters:** +- `exception` - Exception to check + +**Returns:** +- `bool` - True if this is a network error that can be retried + +**Example:** +```python +try: + await DownloadManager.download_url(url) +except Exception as e: + if DownloadManager.is_network_error(e): + # Network error - wait and retry + await TaskManager.sleep(2) + # Retry with resume... + else: + # Fatal error - show error to user + raise +``` + +**Detected error codes:** +- `-110` - ETIMEDOUT (connection timed out) +- `-113` - ECONNABORTED (connection aborted) +- `-104` - ECONNRESET (connection reset by peer) +- `-118` - EHOSTUNREACH (no route to host) +- `-202` - DNS/connection error (network not ready) + +**Detected error messages:** +- "connection reset", "connection aborted" +- "broken pipe", "network unreachable", "host unreachable" +- "failed to download chunk" + +#### `DownloadManager.get_resume_position(outfile)` + +Get the current size of a partially downloaded file. + +Useful for implementing resume functionality with Range headers. + +**Parameters:** +- `outfile` - Path to file + +**Returns:** +- `int` - File size in bytes, or 0 if file doesn't exist + +**Example:** +```python +resume_from = DownloadManager.get_resume_position("/sdcard/file.bin") +if resume_from > 0: + headers = {'Range': f'bytes={resume_from}-'} + await DownloadManager.download_url(url, outfile=outfile, headers=headers) +else: + # Start new download + await DownloadManager.download_url(url, outfile=outfile) +``` + +### Session Management Functions #### `DownloadManager.is_session_active()` @@ -254,24 +317,23 @@ async def download_mpk(self, app): ### 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 + # Use DownloadManager utility to get resume position + bytes_written = DownloadManager.get_resume_position(outfile) + + if bytes_written > 0: print(f"Resuming from {bytes_written} bytes") - except OSError: + headers = {'Range': f'bytes={bytes_written}-'} + else: print("Starting new download") + headers = None # Download remaining bytes success = await DownloadManager.download_url( url, outfile=outfile, - headers={'Range': f'bytes={bytes_written}-'} + headers=headers ) return success @@ -279,31 +341,50 @@ async def resume_download(self, url, outfile): **Note:** Server must support HTTP Range requests. -### Error Handling +### Error Handling with Network Detection ```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 +async def robust_download(self, url, outfile): + """Download with comprehensive error handling and retry""" + max_retries = 3 + retry_delay = 2 + + for attempt in range(max_retries): + try: + success = await DownloadManager.download_url(url, outfile=outfile) + + if success: + return True + else: + print("Download failed") + return False + + except Exception as e: + if DownloadManager.is_network_error(e): + # Network error - retry with resume + print(f"Network error (attempt {attempt + 1}/{max_retries}): {e}") + + if attempt < max_retries - 1: + await TaskManager.sleep(retry_delay) + # Resume from last position + continue + else: + print("Max retries reached") + return False + else: + # Non-network error - don't retry + print(f"Fatal error: {e}") + return False + + return False ``` +**Benefits:** +- Automatic retry on network errors +- Resume from last position +- No retry on fatal errors (404, invalid URL, etc.) +- User-friendly error messages + ## Session Management ### Automatic Lifecycle From 534210529c3e229eab9d0923f962342a6c44284a Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Tue, 13 Jan 2026 12:11:20 +0100 Subject: [PATCH 025/110] Add Setting(s)Activity to the docs --- docs/frameworks/setting-activity.md | 342 ++++++++++++++++++++++++ docs/frameworks/settings-activity.md | 374 +++++++++++++++++++++++++++ docs/os-development/linux.md | 2 +- mkdocs.yml | 2 + 4 files changed, 719 insertions(+), 1 deletion(-) create mode 100644 docs/frameworks/setting-activity.md create mode 100644 docs/frameworks/settings-activity.md diff --git a/docs/frameworks/setting-activity.md b/docs/frameworks/setting-activity.md new file mode 100644 index 0000000..5a52d71 --- /dev/null +++ b/docs/frameworks/setting-activity.md @@ -0,0 +1,342 @@ +# SettingActivity + +`SettingActivity` is used to edit a single setting with various UI options. It provides a flexible framework for collecting user input with support for text input, radio buttons, dropdowns, and custom Activity implementations. + +## Overview + +`SettingActivity` displays a single setting that the user can modify. The setting is passed via Intent extras and can be configured with different UI types. When the user saves the setting, it's automatically persisted to SharedPreferences (unless `dont_persist` is set to true). + +## Basic Usage + +```python +from mpos import Activity, Intent, SettingActivity, SharedPreferences + +class MyApp(Activity): + def onCreate(self): + self.prefs = SharedPreferences("com.example.myapp") + # ... create UI ... + + def open_setting(self): + intent = Intent(activity_class=SettingActivity) + intent.putExtra("prefs", self.prefs) + intent.putExtra("setting", { + "title": "Your Name", + "key": "user_name", + "placeholder": "Enter your name" + }) + self.startActivity(intent) +``` + +## Setting Dictionary Structure + +Each setting is defined as a dictionary with the following properties: + +### Required Properties +- **`title`** (string): Display name of the setting shown at the top +- **`key`** (string): Unique identifier used as the SharedPreferences key + +### Optional Properties +- **`ui`** (string): UI type to use for editing. Options: `"textarea"` (default), `"radiobuttons"`, `"dropdown"`, `"activity"` +- **`ui_options`** (list): Options for `radiobuttons` and `dropdown` UI types +- **`placeholder`** (string): Placeholder text for textarea input +- **`changed_callback`** (function): Callback function called when the setting value changes +- **`should_show`** (function): Function to determine if this setting should be displayed +- **`dont_persist`** (bool): If `True`, the setting won't be saved to SharedPreferences +- **`activity_class`** (class): Custom Activity class for `"activity"` UI type +- **`value_label`** (widget): Internal reference to the value label (set by SettingsActivity) +- **`cont`** (widget): Internal reference to the container (set by SettingsActivity) + +## UI Types + +### 1. Textarea (Default) + +Text input with an on-screen keyboard and optional QR code scanner. + +**When to use:** For text input like URLs, API keys, names, etc. + +**Example:** +```python +{ + "title": "API Key", + "key": "api_key", + "placeholder": "Enter your API key", + "ui": "textarea" # or omit this, textarea is default +} +``` + +**Features:** +- On-screen keyboard for text input +- QR code scanner button (for scanning data from QR codes) +- Placeholder text support +- Single-line input + +### 2. Radio Buttons + +Mutually exclusive selection from a list of options. + +**When to use:** For choosing one option from a small set of choices. + +**Format for `ui_options`:** +```python +ui_options = [ + ("Display Label 1", "value1"), + ("Display Label 2", "value2"), + ("Display Label 3", "value3") +] +``` + +**Example:** +```python +{ + "title": "Theme", + "key": "theme", + "ui": "radiobuttons", + "ui_options": [ + ("Light", "light"), + ("Dark", "dark"), + ("Auto", "auto") + ] +} +``` + +**Features:** +- Circular radio button indicators +- Only one option can be selected at a time +- Displays both label and value (if different) + +### 3. Dropdown + +Dropdown selection from a list of options. + +**When to use:** For choosing one option from a larger set of choices. + +**Format for `ui_options`:** +```python +ui_options = [ + ("Display Label 1", "value1"), + ("Display Label 2", "value2"), + ("Display Label 3", "value3") +] +``` + +**Example:** +```python +{ + "title": "Language", + "key": "language", + "ui": "dropdown", + "ui_options": [ + ("English", "en"), + ("German", "de"), + ("French", "fr"), + ("Spanish", "es") + ] +} +``` + +**Features:** +- Compact dropdown menu +- Shows label and value if different +- Automatically selects the current value + +### 4. Custom Activity + +Use a custom Activity class for advanced UI implementations. + +**When to use:** For complex settings that need custom UI beyond the built-in options. + +**Example:** +```python +class ColorPickerActivity(Activity): + def onCreate(self): + # Custom color picker UI + pass + +setting = { + "title": "Pick a Color", + "key": "color", + "ui": "activity", + "activity_class": ColorPickerActivity +} +``` + +**Requirements:** +- Must provide `activity_class` parameter +- The custom Activity receives the setting and prefs via Intent extras +- Must call `self.finish()` to return to the previous screen + +## Callbacks and Advanced Features + +### changed_callback + +Called when the setting value changes (after saving). Useful for triggering UI updates or reloading data. + +**Example:** +```python +def on_theme_changed(new_value): + print(f"Theme changed to: {new_value}") + # Reload UI with new theme + self.apply_theme(new_value) + +setting = { + "title": "Theme", + "key": "theme", + "ui": "radiobuttons", + "ui_options": [("Light", "light"), ("Dark", "dark")], + "changed_callback": on_theme_changed +} +``` + +**Important:** The callback is only called if the value actually changed (old value != new value). + +### should_show + +Function to conditionally show/hide a setting. Used primarily in SettingsActivity. + +**Example:** +```python +def should_show_advanced_options(setting): + prefs = SharedPreferences("com.example.app") + return prefs.get_string("mode") == "advanced" + +setting = { + "title": "Advanced Option", + "key": "advanced_setting", + "should_show": should_show_advanced_options +} +``` + +### dont_persist + +Prevent the setting from being saved to SharedPreferences. + +**Example:** +```python +setting = { + "title": "Temporary Value", + "key": "temp_value", + "dont_persist": True +} +``` + +## Complete Example: Simple Settings App + +Here's a minimal app that uses SettingActivity to edit a single setting: + +```python +import lvgl as lv +from mpos import Activity, Intent, SettingActivity, SharedPreferences + +class SimpleSettingsApp(Activity): + def onCreate(self): + self.prefs = SharedPreferences("com.example.simplesettings") + + # Create main screen + self.main_screen = lv.obj() + self.main_screen.set_flex_flow(lv.FLEX_FLOW.COLUMN) + + # Display current value + self.value_label = lv.label(self.main_screen) + self.update_value_label() + + # Settings button + settings_btn = lv.button(self.main_screen) + settings_btn.set_size(lv.pct(80), lv.SIZE_CONTENT) + settings_label = lv.label(settings_btn) + settings_label.set_text("Edit Setting") + settings_label.center() + settings_btn.add_event_cb(self.open_settings, lv.EVENT.CLICKED, None) + + self.setContentView(self.main_screen) + + def onResume(self, screen): + super().onResume(screen) + self.update_value_label() + + def update_value_label(self): + value = self.prefs.get_string("my_setting", "(not set)") + self.value_label.set_text(f"Current value: {value}") + + def open_settings(self, event): + intent = Intent(activity_class=SettingActivity) + intent.putExtra("prefs", self.prefs) + intent.putExtra("setting", { + "title": "My Setting", + "key": "my_setting", + "placeholder": "Enter a value", + "changed_callback": self.on_setting_changed + }) + self.startActivity(intent) + + def on_setting_changed(self, new_value): + print(f"Setting changed to: {new_value}") + self.update_value_label() +``` + +## Real-World Example: AppStore Backend Selection + +This example is inspired by the AppStore app, showing how to use SettingActivity with radio buttons and a callback: + +```python +import lvgl as lv +from mpos import Activity, Intent, SettingActivity, SharedPreferences + +class AppStore(Activity): + BACKENDS = [ + ("MPOS GitHub", "github,https://apps.micropythonos.com/app_index.json"), + ("BadgeHub Test", "badgehub,https://badgehub.p1m.nl/api/v3"), + ("BadgeHub Prod", "badgehub,https://badge.why2025.org/api/v3") + ] + + def onCreate(self): + self.prefs = SharedPreferences("com.micropythonos.appstore") + + self.main_screen = lv.obj() + self.main_screen.set_flex_flow(lv.FLEX_FLOW.COLUMN) + + # Settings button + settings_btn = lv.button(self.main_screen) + settings_btn.set_size(lv.pct(20), lv.pct(25)) + settings_btn.align(lv.ALIGN.TOP_RIGHT, 0, 0) + settings_label = lv.label(settings_btn) + settings_label.set_text(lv.SYMBOL.SETTINGS) + settings_label.center() + settings_btn.add_event_cb(self.open_backend_settings, lv.EVENT.CLICKED, None) + + self.setContentView(self.main_screen) + + def open_backend_settings(self, event): + intent = Intent(activity_class=SettingActivity) + intent.putExtra("prefs", self.prefs) + intent.putExtra("setting", { + "title": "AppStore Backend", + "key": "backend", + "ui": "radiobuttons", + "ui_options": self.BACKENDS, + "changed_callback": self.backend_changed + }) + self.startActivity(intent) + + def backend_changed(self, new_value): + print(f"Backend changed to {new_value}, refreshing app list...") + # Trigger app list refresh with new backend + self.refresh_app_list() + + def refresh_app_list(self): + # Implementation to refresh the app list + pass +``` + +## Tips and Best Practices + +1. **Load prefs once in onCreate()**: Load SharedPreferences in your main Activity's `onCreate()` and pass it to SettingActivity. This is faster than loading it multiple times. + +2. **Use changed_callback for side effects**: If changing a setting requires reloading data or updating the UI, use `changed_callback` instead of checking the value on resume. + +3. **Validate input**: For textarea inputs, consider validating the input in your `changed_callback` before using it. + +4. **Use radio buttons for small sets**: Use radio buttons for 2-5 options, dropdown for larger lists. + +5. **Provide meaningful placeholders**: Help users understand what format is expected with clear placeholder text. + +6. **Consider conditional visibility**: Use `should_show` in SettingsActivity to hide settings that don't apply based on other settings. diff --git a/docs/frameworks/settings-activity.md b/docs/frameworks/settings-activity.md new file mode 100644 index 0000000..26e0a09 --- /dev/null +++ b/docs/frameworks/settings-activity.md @@ -0,0 +1,374 @@ +# SettingsActivity + +`SettingsActivity` is used to display and edit a list of multiple settings. It provides a scrollable list interface where users can tap on each setting to edit it individually. Each setting can have different UI types and optional conditional visibility. + +## Overview + +`SettingsActivity` displays a list of settings that users can browse and edit. When a user taps on a setting, it opens a [`SettingActivity`](setting-activity.md) for editing that individual setting. After editing, the value is automatically saved to SharedPreferences and the list is updated. + +## Basic Usage + +```python +from mpos import Activity, Intent, SettingsActivity, SharedPreferences + +class MyApp(Activity): + def onCreate(self): + self.prefs = SharedPreferences("com.example.myapp") + # ... create UI ... + + def open_settings(self): + intent = Intent(activity_class=SettingsActivity) + intent.putExtra("prefs", self.prefs) + intent.putExtra("settings", [ + {"title": "Your Name", "key": "user_name", "placeholder": "Enter your name"}, + {"title": "Theme", "key": "theme", "ui": "radiobuttons", "ui_options": [("Light", "light"), ("Dark", "dark")]}, + ]) + self.startActivity(intent) +``` + +## Settings List Structure + +`SettingsActivity` expects a list of setting dictionaries. Each setting dictionary has the same structure as in [`SettingActivity`](setting-activity.md): + +### Required Properties +- **`title`** (string): Display name of the setting shown in the list +- **`key`** (string): Unique identifier used as the SharedPreferences key + +### Optional Properties +- **`ui`** (string): UI type for editing. Options: `"textarea"` (default), `"radiobuttons"`, `"dropdown"`, `"activity"` +- **`ui_options`** (list): Options for `radiobuttons` and `dropdown` UI types +- **`placeholder`** (string): Placeholder text for textarea input +- **`changed_callback`** (function): Callback function called when the setting value changes +- **`should_show`** (function): Function to determine if this setting should be displayed in the list +- **`dont_persist`** (bool): If `True`, the setting won't be saved to SharedPreferences +- **`activity_class`** (class): Custom Activity class for `"activity"` UI type +- **`value_label`** (widget): Internal reference to the value label (set by SettingsActivity) +- **`cont`** (widget): Internal reference to the container (set by SettingsActivity) + +## UI Types + +All UI types available in [`SettingActivity`](setting-activity.md) are supported: + +### 1. Textarea (Default) + +Text input with on-screen keyboard and optional QR code scanner. + +```python +{ + "title": "API Key", + "key": "api_key", + "placeholder": "Enter your API key" +} +``` + +### 2. Radio Buttons + +Mutually exclusive selection from a list of options. + +```python +{ + "title": "Wallet Type", + "key": "wallet_type", + "ui": "radiobuttons", + "ui_options": [ + ("LNBits", "lnbits"), + ("Nostr Wallet Connect", "nwc") + ] +} +``` + +### 3. Dropdown + +Dropdown selection from a list of options. + +```python +{ + "title": "Language", + "key": "language", + "ui": "dropdown", + "ui_options": [ + ("English", "en"), + ("German", "de"), + ("French", "fr") + ] +} +``` + +### 4. Custom Activity + +Use a custom Activity class for advanced UI implementations. + +```python +{ + "title": "Pick a Color", + "key": "color", + "ui": "activity", + "activity_class": ColorPickerActivity +} +``` + +## Advanced Features + +### Conditional Visibility with should_show + +The `should_show` function allows you to conditionally display settings based on other settings or app state. + +**Function signature:** +```python +def should_show(setting): + # Return True to show, False to hide + return condition +``` + +**Example:** +```python +def should_show_lnbits_settings(setting): + prefs = SharedPreferences("com.example.wallet") + wallet_type = prefs.get_string("wallet_type") + return wallet_type == "lnbits" + +settings = [ + {"title": "Wallet Type", "key": "wallet_type", "ui": "radiobuttons", + "ui_options": [("LNBits", "lnbits"), ("NWC", "nwc")]}, + {"title": "LNBits URL", "key": "lnbits_url", "placeholder": "https://...", + "should_show": should_show_lnbits_settings}, + {"title": "LNBits Read Key", "key": "lnbits_readkey", + "should_show": should_show_lnbits_settings}, +] +``` + +### Callbacks on Value Change + +The `changed_callback` is called when a setting value changes. Useful for triggering UI updates or reloading data. + +```python +def on_wallet_type_changed(new_value): + print(f"Wallet type changed to: {new_value}") + # Reconnect to new wallet backend + reconnect_wallet(new_value) + +settings = [ + {"title": "Wallet Type", "key": "wallet_type", "ui": "radiobuttons", + "ui_options": [("LNBits", "lnbits"), ("NWC", "nwc")], + "changed_callback": on_wallet_type_changed}, +] +``` + +### Preventing Persistence + +Use `dont_persist` to prevent a setting from being saved to SharedPreferences. + +```python +{ + "title": "Temporary Value", + "key": "temp_value", + "dont_persist": True +} +``` + +## Complete Example: Simple Settings List + +Here's a minimal app that uses SettingsActivity to edit multiple settings: + +```python +import lvgl as lv +from mpos import Activity, Intent, SettingsActivity, SharedPreferences + +class SimpleSettingsApp(Activity): + def onCreate(self): + self.prefs = SharedPreferences("com.example.simplesettings") + + # Create main screen + self.main_screen = lv.obj() + self.main_screen.set_flex_flow(lv.FLEX_FLOW.COLUMN) + + # Settings button + settings_btn = lv.button(self.main_screen) + settings_btn.set_size(lv.pct(80), lv.SIZE_CONTENT) + settings_label = lv.label(settings_btn) + settings_label.set_text("Open Settings") + settings_label.center() + settings_btn.add_event_cb(self.open_settings, lv.EVENT.CLICKED, None) + + self.setContentView(self.main_screen) + + def open_settings(self, event): + intent = Intent(activity_class=SettingsActivity) + intent.putExtra("prefs", self.prefs) + intent.putExtra("settings", [ + { + "title": "Your Name", + "key": "user_name", + "placeholder": "Enter your name" + }, + { + "title": "Theme", + "key": "theme", + "ui": "radiobuttons", + "ui_options": [ + ("Light", "light"), + ("Dark", "dark") + ] + }, + { + "title": "Language", + "key": "language", + "ui": "dropdown", + "ui_options": [ + ("English", "en"), + ("German", "de"), + ("French", "fr") + ] + } + ]) + self.startActivity(intent) +``` + +## Real-World Example: Lightning Piggy Wallet + +This example is inspired by the Lightning Piggy DisplayWallet app, showing how to use SettingsActivity with conditional visibility and callbacks: + +```python +import lvgl as lv +from mpos import Activity, Intent, SettingsActivity, SharedPreferences + +class DisplayWallet(Activity): + def onCreate(self): + self.prefs = SharedPreferences("com.lightningpiggy.displaywallet") + + # Create main screen + self.main_screen = lv.obj() + + # Settings button + settings_button = lv.button(self.main_screen) + settings_button.set_size(lv.pct(20), lv.pct(25)) + settings_button.align(lv.ALIGN.BOTTOM_RIGHT, 0, 0) + settings_button.add_event_cb(self.settings_button_tap, lv.EVENT.CLICKED, None) + settings_label = lv.label(settings_button) + settings_label.set_text(lv.SYMBOL.SETTINGS) + settings_label.center() + + self.setContentView(self.main_screen) + + def should_show_lnbits_settings(self, setting): + """Only show LNBits settings if LNBits is selected""" + wallet_type = self.prefs.get_string("wallet_type") + return wallet_type == "lnbits" + + def should_show_nwc_settings(self, setting): + """Only show NWC settings if NWC is selected""" + wallet_type = self.prefs.get_string("wallet_type") + return wallet_type == "nwc" + + def on_wallet_type_changed(self, new_value): + """Called when wallet type changes""" + print(f"Wallet type changed to: {new_value}") + # Reconnect to new wallet backend + self.reconnect_wallet(new_value) + + def settings_button_tap(self, event): + intent = Intent(activity_class=SettingsActivity) + intent.putExtra("prefs", self.prefs) + intent.putExtra("settings", [ + { + "title": "Wallet Type", + "key": "wallet_type", + "ui": "radiobuttons", + "ui_options": [ + ("LNBits", "lnbits"), + ("Nostr Wallet Connect", "nwc") + ], + "changed_callback": self.on_wallet_type_changed + }, + { + "title": "LNBits URL", + "key": "lnbits_url", + "placeholder": "https://demo.lnpiggy.com", + "should_show": self.should_show_lnbits_settings + }, + { + "title": "LNBits Read Key", + "key": "lnbits_readkey", + "placeholder": "fd92e3f8168ba314dc22e54182784045", + "should_show": self.should_show_lnbits_settings + }, + { + "title": "Optional LN Address", + "key": "lnbits_static_receive_code", + "placeholder": "Will be fetched if empty.", + "should_show": self.should_show_lnbits_settings + }, + { + "title": "Nostr Wallet Connect", + "key": "nwc_url", + "placeholder": "nostr+walletconnect://69effe7b...", + "should_show": self.should_show_nwc_settings + }, + { + "title": "Optional LN Address", + "key": "nwc_static_receive_code", + "placeholder": "Optional if present in NWC URL.", + "should_show": self.should_show_nwc_settings + } + ]) + self.startActivity(intent) + + def reconnect_wallet(self, wallet_type): + """Reconnect to the selected wallet backend""" + if wallet_type == "lnbits": + url = self.prefs.get_string("lnbits_url") + readkey = self.prefs.get_string("lnbits_readkey") + # Initialize LNBits wallet + print(f"Connecting to LNBits at {url}") + elif wallet_type == "nwc": + nwc_url = self.prefs.get_string("nwc_url") + # Initialize NWC wallet + print(f"Connecting to NWC at {nwc_url}") +``` + +## UI Layout + +SettingsActivity displays settings in a scrollable list with the following layout for each setting: + +``` +┌─────────────────────────────────┐ +│ Setting Title │ +│ Current Value (smaller text) │ +└─────────────────────────────────┘ +``` + +- **Title**: Bold, larger font (montserrat_16) +- **Value**: Smaller font (montserrat_12), gray color +- **Container**: Full width, clickable, with border on focus +- **Scrollable**: Settings list scrolls if it exceeds screen height + +## Tips and Best Practices + +1. **Load prefs once in onCreate()**: Load SharedPreferences in your main Activity's `onCreate()` and pass it to SettingsActivity. This is faster than loading it multiple times. + +2. **Use should_show for conditional settings**: Hide settings that don't apply based on other settings using the `should_show` function. This keeps the UI clean and prevents confusion. + +3. **Use changed_callback for side effects**: If changing a setting requires reloading data or updating the UI, use `changed_callback` instead of checking the value on resume. + +4. **Group related settings**: Organize settings logically in the list so related options are near each other. + +5. **Provide meaningful placeholders**: Help users understand what format is expected with clear placeholder text. + +6. **Use radio buttons for small sets**: Use radio buttons for 2-5 options, dropdown for larger lists. + +7. **Consider performance**: If you have many settings with complex `should_show` functions, the list rebuild might be slow. Optimize your condition checks. + +## Comparison with SettingActivity + +| Feature | SettingActivity | SettingsActivity | +|---------|-----------------|------------------| +| **Purpose** | Edit a single setting | Edit multiple settings | +| **UI** | Full-screen editor | Scrollable list | +| **Use case** | Detailed editing of one setting | Browse and edit multiple settings | +| **Callbacks** | `changed_callback` | `changed_callback` + `should_show` | +| **Best for** | Settings that need focus | Quick access to many settings | + +## See Also + +- [`SettingActivity`](setting-activity.md) - For editing a single setting +- [`SharedPreferences`](preferences.md) - For persisting settings diff --git a/docs/os-development/linux.md b/docs/os-development/linux.md index 1b39ea4..5e59dd7 100644 --- a/docs/os-development/linux.md +++ b/docs/os-development/linux.md @@ -18,7 +18,7 @@ While that's going on, make sure you have everything installed to compile code: ``` sudo apt update -sudo apt-get install -y build-essential libffi-dev pkg-config cmake ninja-build gnome-desktop-testing libasound2-dev libpulse-dev libaudio-dev libjack-dev libsndio-dev libx11-dev libxext-dev libxrandr-dev libxcursor-dev libxfixes-dev libxi-dev libxss-dev libxkbcommon-dev libdrm-dev libgbm-dev libgl1-mesa-dev libgles2-mesa-dev libegl1-mesa-dev libdbus-1-dev libibus-1.0-dev libudev-dev fcitx-libs-dev libpipewire-0.3-dev libwayland-dev libdecor-0-dev libv4l-dev +sudo apt-get install -y build-essential libffi-dev pkg-config cmake ninja-build gnome-desktop-testing libasound2-dev libpulse-dev libaudio-dev libjack-dev libsndio-dev libx11-dev libxext-dev libxrandr-dev libxcursor-dev libxfixes-dev libxi-dev libxss-dev libxkbcommon-dev libdrm-dev libgbm-dev libgl1-mesa-dev libgles2-mesa-dev libegl1-mesa-dev libdbus-1-dev libibus-1.0-dev libudev-dev fcitx-libs-dev libpipewire-0.3-dev libwayland-dev libdecor-0-dev libv4l-dev librlottie-dev ``` diff --git a/mkdocs.yml b/mkdocs.yml index 6a13c23..f9ed295 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -53,6 +53,8 @@ nav: - LightsManager: frameworks/lights-manager.md - Preferences: frameworks/preferences.md - SensorManager: frameworks/sensor-manager.md + - SettingActivity: frameworks/setting-activity.md + - SettingsActivity: frameworks/settings-activity.md - TaskManager: frameworks/task-manager.md - WifiService: frameworks/wifi-service.md - Architecture: From d89a78777ea1505095f18551ad1e4871fc1074b0 Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Tue, 13 Jan 2026 12:16:08 +0100 Subject: [PATCH 026/110] Simplify imports --- docs/apps/creating-apps.md | 2 +- docs/frameworks/audioflinger.md | 6 +++--- docs/frameworks/connectivity-manager.md | 19 +++++++------------ docs/frameworks/lights-manager.md | 6 +++--- docs/frameworks/preferences.md | 2 +- docs/frameworks/sensor-manager.md | 4 ++-- docs/frameworks/task-manager.md | 3 +-- docs/frameworks/wifi-service.md | 15 +++++++-------- 8 files changed, 25 insertions(+), 32 deletions(-) diff --git a/docs/apps/creating-apps.md b/docs/apps/creating-apps.md index 37e0eeb..3d173e6 100644 --- a/docs/apps/creating-apps.md +++ b/docs/apps/creating-apps.md @@ -26,7 +26,7 @@ com.micropythonos.helloworld/ In `hello.py`, put: ``` -from mpos.apps import Activity +from mpos import Activity class Hello(Activity): diff --git a/docs/frameworks/audioflinger.md b/docs/frameworks/audioflinger.md index b22b73f..ede91ff 100644 --- a/docs/frameworks/audioflinger.md +++ b/docs/frameworks/audioflinger.md @@ -28,7 +28,7 @@ AudioFlinger provides: ### Playing WAV Files ```python -from mpos.app.activity import Activity +from mpos import Activity import mpos.audio.audioflinger as AudioFlinger class MusicPlayerActivity(Activity): @@ -148,7 +148,7 @@ if AudioFlinger.is_playing(): ### Complete Recording Example ```python -from mpos.apps import Activity +from mpos import Activity import mpos.audio.audioflinger as AudioFlinger import lvgl as lv import time @@ -299,7 +299,7 @@ Audio device preference is configured in the Settings app under **"Advanced Sett ## Complete Example: Music Player with Controls ```python -from mpos.app.activity import Activity +from mpos import Activity import mpos.audio.audioflinger as AudioFlinger import lvgl as lv diff --git a/docs/frameworks/connectivity-manager.md b/docs/frameworks/connectivity-manager.md index 5f1b508..9fa56a9 100644 --- a/docs/frameworks/connectivity-manager.md +++ b/docs/frameworks/connectivity-manager.md @@ -17,7 +17,7 @@ ConnectivityManager provides: ### Checking Connectivity ```python -from mpos.net.connectivity_manager import ConnectivityManager +from mpos import ConnectivityManager # Get the singleton instance cm = ConnectivityManager.get() @@ -36,8 +36,7 @@ if cm.is_wifi_connected(): ### Registering for Connectivity Changes ```python -from mpos.app.activity import Activity -from mpos.net.connectivity_manager import ConnectivityManager +from mpos import Activity, ConnectivityManager class MyActivity(Activity): def onCreate(self): @@ -73,7 +72,7 @@ class MyActivity(Activity): ### Waiting for Connectivity ```python -from mpos.net.connectivity_manager import ConnectivityManager +from mpos import ConnectivityManager def download_with_wait(): cm = ConnectivityManager.get() @@ -91,9 +90,7 @@ def download_with_wait(): ### Network-Aware App ```python -from mpos.app.activity import Activity -from mpos.net.connectivity_manager import ConnectivityManager -from mpos import TaskManager, DownloadManager +from mpos import Activity, ConnectivityManager, TaskManager, DownloadManager import lvgl as lv class NewsReaderActivity(Activity): @@ -144,8 +141,7 @@ class NewsReaderActivity(Activity): ### Retry on Reconnect ```python -from mpos.net.connectivity_manager import ConnectivityManager -from mpos import TaskManager, DownloadManager +from mpos import ConnectivityManager, TaskManager, DownloadManager class SyncManager: def __init__(self): @@ -179,7 +175,7 @@ class SyncManager: ### Conditional Feature Loading ```python -from mpos.net.connectivity_manager import ConnectivityManager +from mpos import ConnectivityManager class AppStoreActivity(Activity): def onCreate(self): @@ -432,8 +428,7 @@ ConnectivityManager works alongside WifiService: - **ConnectivityManager** - Monitors connection status and notifies apps ```python -from mpos.net.wifi_service import WifiService -from mpos.net.connectivity_manager import ConnectivityManager +from mpos import WifiService, ConnectivityManager # WifiService for connection management WifiService.attempt_connecting("MyNetwork", "password") diff --git a/docs/frameworks/lights-manager.md b/docs/frameworks/lights-manager.md index 33ac64e..9eb6245 100644 --- a/docs/frameworks/lights-manager.md +++ b/docs/frameworks/lights-manager.md @@ -28,7 +28,7 @@ LightsManager provides: ### Check Availability ```python -from mpos.app.activity import Activity +from mpos import Activity import mpos.lights as LightsManager class MyActivity(Activity): @@ -240,7 +240,7 @@ def sparkle_effect(duration_ms=5000): For smooth, real-time animations that integrate with the game loop, use the **TaskHandler event system**: ```python -from mpos.app.activity import Activity +from mpos import Activity import mpos.ui import mpos.lights as LightsManager import time @@ -571,7 +571,7 @@ def onPause(self, screen): ## Complete Example: LED Status Indicator ```python -from mpos.app.activity import Activity +from mpos import Activity import mpos.lights as LightsManager import lvgl as lv diff --git a/docs/frameworks/preferences.md b/docs/frameworks/preferences.md index 1b0911a..39cbcb3 100644 --- a/docs/frameworks/preferences.md +++ b/docs/frameworks/preferences.md @@ -7,7 +7,7 @@ Here's a simple example of how to add it to your app, taken from [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 + from mpos import Activity +import mpos.config import mpos.ui import mpos.ui.anim diff --git a/docs/frameworks/sensor-manager.md b/docs/frameworks/sensor-manager.md index 1fa88c7..3d4bbfe 100644 --- a/docs/frameworks/sensor-manager.md +++ b/docs/frameworks/sensor-manager.md @@ -37,7 +37,7 @@ SensorManager.TYPE_IMU_TEMPERATURE # °C (IMU chip temperature) ### Basic Usage ```python -from mpos.app.activity import Activity +from mpos import Activity import mpos.sensor_manager as SensorManager class MyActivity(Activity): @@ -84,7 +84,7 @@ if imu_temperature: This example shows how to create a simple tilt-controlled ball game: ```python -from mpos.app.activity import Activity +from mpos import Activity import mpos.sensor_manager as SensorManager import mpos.ui import lvgl as lv diff --git a/docs/frameworks/task-manager.md b/docs/frameworks/task-manager.md index ae95647..0d44866 100644 --- a/docs/frameworks/task-manager.md +++ b/docs/frameworks/task-manager.md @@ -18,8 +18,7 @@ TaskManager provides: ### Creating Background Tasks ```python -from mpos.app.activity import Activity -from mpos import TaskManager +from mpos import Activity, TaskManager class MyActivity(Activity): def onCreate(self): diff --git a/docs/frameworks/wifi-service.md b/docs/frameworks/wifi-service.md index 1cfd41a..b9efebd 100644 --- a/docs/frameworks/wifi-service.md +++ b/docs/frameworks/wifi-service.md @@ -19,7 +19,7 @@ WifiService provides: ### Checking Connection Status ```python -from mpos.net.wifi_service import WifiService +from mpos import WifiService # Check if WiFi is connected if WifiService.is_connected(): @@ -32,7 +32,7 @@ else: ### Scanning for Networks ```python -from mpos.net.wifi_service import WifiService +from mpos import WifiService # Scan for available networks networks = WifiService.scan_networks() @@ -46,7 +46,7 @@ for ssid in networks: ### Connecting to a Network ```python -from mpos.net.wifi_service import WifiService +from mpos import WifiService # Save network credentials first WifiService.save_network("MyNetwork", "password123") @@ -63,7 +63,7 @@ else: ### Managing Saved Networks ```python -from mpos.net.wifi_service import WifiService +from mpos import WifiService # Save a network WifiService.save_network("HomeWiFi", "mypassword") @@ -87,8 +87,7 @@ WifiService.forget_network("OldNetwork") ### WiFi Settings Screen ```python -from mpos.app.activity import Activity -from mpos.net.wifi_service import WifiService +from mpos import Activity, WifiService import lvgl as lv class WifiSettingsActivity(Activity): @@ -161,7 +160,7 @@ WifiService automatically handles auto-connect on boot. This is typically starte ```python import _thread -from mpos.net.wifi_service import WifiService +from mpos import WifiService import mpos.apps # Start auto-connect in background thread @@ -182,7 +181,7 @@ _thread.start_new_thread(WifiService.auto_connect, ()) On ESP32-S3, ADC2 pins (GPIO11-20) don't work when WiFi is active. WifiService provides methods to temporarily disable WiFi: ```python -from mpos.net.wifi_service import WifiService +from mpos import WifiService def read_adc2_sensor(): """Read from ADC2 pin which requires WiFi to be disabled.""" From 219c7d3f05c3b174137af64761b338bef6249d32 Mon Sep 17 00:00:00 2001 From: Kili <60932529+QuasiKili@users.noreply.github.com> Date: Tue, 13 Jan 2026 19:12:43 +0100 Subject: [PATCH 027/110] Update supported-hardware.md --- docs/getting-started/supported-hardware.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/getting-started/supported-hardware.md b/docs/getting-started/supported-hardware.md index 09ba3a9..36415b7 100644 --- a/docs/getting-started/supported-hardware.md +++ b/docs/getting-started/supported-hardware.md @@ -5,7 +5,7 @@ MicroPythonOS runs on a variety of platforms, from microcontrollers to desktops. ## ESP32 Microcontrollers - [Waveshare ESP32-S3-Touch-LCD-2](https://www.waveshare.com/wiki/ESP32-S3-Touch-LCD-2): Fully Supported -- [Fri3d Camp 2024 Badge](https://fri3d.be/badge/2024/): Good support but missing a bit of hardware support (IMU, Buzzer, Multi-Color LEDs, Communicator Add-On) +- [Fri3d Camp 2024 Badge](https://fri3d.be/badge/2024/): Fully Supported ## Desktop Computers From e4f658544151e38e51f2b82857b53ba7f2d553e0 Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Wed, 14 Jan 2026 20:46:52 +0100 Subject: [PATCH 028/110] Update AudioFlinger docs --- docs/frameworks/audioflinger.md | 76 ++++++++++++++++++++++++--------- 1 file changed, 56 insertions(+), 20 deletions(-) diff --git a/docs/frameworks/audioflinger.md b/docs/frameworks/audioflinger.md index ede91ff..d444b43 100644 --- a/docs/frameworks/audioflinger.md +++ b/docs/frameworks/audioflinger.md @@ -25,15 +25,32 @@ AudioFlinger provides: ## Quick Start +### Initialization + +AudioFlinger uses a singleton pattern. Initialize it once at startup with your hardware configuration: + +```python +from mpos.audio.audioflinger import AudioFlinger + +# Initialize with I2S and buzzer (typical Fri3d badge setup) +i2s_pins = {'sck': 2, 'ws': 47, 'sd': 16, 'sd_in': 15} +buzzer = machine.PWM(machine.Pin(46)) +AudioFlinger(i2s_pins=i2s_pins, buzzer_instance=buzzer) + +# After initialization, use class methods directly (no .get() needed) +AudioFlinger.play_wav("music.wav") +AudioFlinger.set_volume(70) +``` + ### Playing WAV Files ```python from mpos import Activity -import mpos.audio.audioflinger as AudioFlinger +from mpos.audio.audioflinger import AudioFlinger class MusicPlayerActivity(Activity): def onCreate(self): - # Play a music file + # Play a music file (class method - no .get() needed) success = AudioFlinger.play_wav( "M:/sdcard/music/song.wav", stream_type=AudioFlinger.STREAM_MUSIC, @@ -53,6 +70,8 @@ class MusicPlayerActivity(Activity): ### Playing RTTTL Ringtones ```python +from mpos.audio.audioflinger import AudioFlinger + # 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( @@ -75,6 +94,8 @@ Nokia:d=4,o=5,b=225:8e6,8d6,8f#,8g# ### Volume Control ```python +from mpos.audio.audioflinger import AudioFlinger + # Set volume (0-100) AudioFlinger.set_volume(70) @@ -97,7 +118,7 @@ AudioFlinger supports recording audio from an I2S microphone to WAV files. ### Basic Recording ```python -import mpos.audio.audioflinger as AudioFlinger +from mpos.audio.audioflinger import AudioFlinger # Check if microphone is available if AudioFlinger.has_microphone(): @@ -149,7 +170,7 @@ if AudioFlinger.is_playing(): ```python from mpos import Activity -import mpos.audio.audioflinger as AudioFlinger +from mpos.audio.audioflinger import AudioFlinger import lvgl as lv import time @@ -233,6 +254,8 @@ AudioFlinger implements a 3-tier priority-based audio focus system inspired by A ### Example: Priority in Action ```python +from mpos.audio.audioflinger import AudioFlinger + # Start playing music (priority 0) AudioFlinger.play_wav("music.wav", stream_type=AudioFlinger.STREAM_MUSIC) @@ -300,7 +323,7 @@ Audio device preference is configured in the Settings app under **"Advanced Sett ```python from mpos import Activity -import mpos.audio.audioflinger as AudioFlinger +from mpos.audio.audioflinger import AudioFlinger import lvgl as lv class SimpleMusicPlayerActivity(Activity): @@ -472,6 +495,8 @@ Check if buzzer is available for RTTTL playback. 4. Use equal or higher priority stream type ```python +from mpos.audio.audioflinger import AudioFlinger + # Check if playback was rejected success = AudioFlinger.play_wav("sound.wav", stream_type=AudioFlinger.STREAM_MUSIC) if not success: @@ -582,25 +607,32 @@ ffmpeg -i input.wav -acodec pcm_s16le -ar 22050 -ac 1 output.wav **Possible causes:** 1. **No microphone available** - ```python - if not AudioFlinger.has_microphone(): - print("No microphone on this device") - ``` + ```python + from mpos.audio.audioflinger import AudioFlinger + + if not AudioFlinger.has_microphone(): + print("No microphone on this device") + ``` 2. **Currently playing audio** - ```python - # I2S can only be TX or RX, not both - if AudioFlinger.is_playing(): - AudioFlinger.stop() - time.sleep_ms(100) # Wait for cleanup - AudioFlinger.record_wav("recording.wav") - ``` + ```python + from mpos.audio.audioflinger import AudioFlinger + import time + + # I2S can only be TX or RX, not both + if AudioFlinger.is_playing(): + AudioFlinger.stop() + time.sleep_ms(100) # Wait for cleanup + AudioFlinger.record_wav("recording.wav") + ``` 3. **Already recording** - ```python - if AudioFlinger.is_recording(): - print("Already recording") - ``` + ```python + from mpos.audio.audioflinger import AudioFlinger + + if AudioFlinger.is_recording(): + print("Already recording") + ``` 4. **Wrong board** - Waveshare doesn't have a microphone @@ -635,6 +667,8 @@ ffmpeg -i input.wav -acodec pcm_s16le -ar 44100 -ac 2 output.wav AudioFlinger runs in a separate thread, so playback doesn't block the UI: ```python +from mpos.audio.audioflinger import AudioFlinger + # This doesn't block - returns immediately AudioFlinger.play_wav("long_song.wav", stream_type=AudioFlinger.STREAM_MUSIC) @@ -663,6 +697,8 @@ On desktop builds (Linux/macOS), AudioFlinger provides simulated recording for t This allows testing the Sound Recorder app and other recording features without hardware. ```python +from mpos.audio.audioflinger import AudioFlinger + # Desktop simulation is automatic when machine.I2S is not available # The generated WAV file contains a 440Hz tone AudioFlinger.record_wav("test.wav", duration_ms=5000) From 18fd3c8459e92af89aa3871601c4f6f626f692a7 Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Wed, 14 Jan 2026 20:55:29 +0100 Subject: [PATCH 029/110] Add WidgetAnimator docs --- docs/frameworks/audioflinger.md | 1 + docs/frameworks/lights-manager.md | 1 + docs/frameworks/widget-animator.md | 564 +++++++++++++++++++++++++++++ mkdocs.yml | 1 + 4 files changed, 567 insertions(+) create mode 100644 docs/frameworks/widget-animator.md diff --git a/docs/frameworks/audioflinger.md b/docs/frameworks/audioflinger.md index d444b43..bac3172 100644 --- a/docs/frameworks/audioflinger.md +++ b/docs/frameworks/audioflinger.md @@ -708,5 +708,6 @@ AudioFlinger.record_wav("test.wav", duration_ms=5000) ## See Also - [Creating Apps](../apps/creating-apps.md) - Learn how to create MicroPythonOS apps +- [WidgetAnimator](widget-animator.md) - Smooth UI animations - [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 index 9eb6245..c3f316c 100644 --- a/docs/frameworks/lights-manager.md +++ b/docs/frameworks/lights-manager.md @@ -648,5 +648,6 @@ class StatusIndicatorActivity(Activity): ## See Also - [Creating Apps](../apps/creating-apps.md) - Learn how to create MicroPythonOS apps +- [WidgetAnimator](widget-animator.md) - Smooth UI animations - [AudioFlinger](audioflinger.md) - Audio control for sound feedback - [SensorManager](sensor-manager.md) - Sensor integration for interactive effects diff --git a/docs/frameworks/widget-animator.md b/docs/frameworks/widget-animator.md new file mode 100644 index 0000000..61b9d5e --- /dev/null +++ b/docs/frameworks/widget-animator.md @@ -0,0 +1,564 @@ +# WidgetAnimator + +MicroPythonOS provides a **WidgetAnimator** utility for creating smooth, non-blocking animations on LVGL widgets. It handles common animation patterns like fade-in/fade-out, slide transitions, and value interpolation with automatic cleanup and error handling. + +## Overview + +WidgetAnimator provides: + +- **Fade animations** - Smooth opacity transitions (fade-in/fade-out) +- **Slide animations** - Smooth position transitions (slide-up/slide-down) +- **Value interpolation** - Animate numeric values with custom display callbacks +- **Non-blocking** - Animations run asynchronously without blocking the UI +- **Safe widget access** - Automatically handles deleted widgets without crashes +- **Automatic cleanup** - Stops previous animations to prevent visual glitches +- **Easing functions** - Built-in ease-in-out path for smooth motion + +## Quick Start + +### Fade In/Out + +```python +from mpos.ui.anim import WidgetAnimator +import lvgl as lv + +# Create a widget +my_widget = lv.label(screen) +my_widget.set_text("Hello") + +# Fade in (500ms) +WidgetAnimator.show_widget(my_widget, anim_type="fade", duration=500) + +# Fade out (500ms) +WidgetAnimator.hide_widget(my_widget, anim_type="fade", duration=500) +``` + +### Convenience Functions + +For the most common use case (fade animations), use the convenience functions: + +```python +from mpos import smooth_show, smooth_hide + +# Fade in with default 500ms duration +smooth_show(my_widget) + +# Fade out with default 500ms duration +smooth_hide(my_widget) + +# Fade out without hiding the widget (just opacity) +smooth_hide(my_widget, hide=False) +``` + +### Slide Animations + +```python +from mpos.ui.anim import WidgetAnimator + +# Slide down from above +WidgetAnimator.show_widget(my_widget, anim_type="slide_down", duration=500) + +# Slide up from below +WidgetAnimator.show_widget(my_widget, anim_type="slide_up", duration=500) + +# Hide by sliding down +WidgetAnimator.hide_widget(my_widget, anim_type="slide_down", duration=500) + +# Hide by sliding up +WidgetAnimator.hide_widget(my_widget, anim_type="slide_up", duration=500) +``` + +### Value Interpolation + +Animate numeric values with a custom display callback: + +```python +from mpos.ui.anim import WidgetAnimator + +# Animate balance from 1000 to 1500 over 5 seconds +def display_balance(value): + balance_label.set_text(f"{int(value)} sats") + +WidgetAnimator.change_widget( + balance_label, + anim_type="interpolate", + duration=5000, + begin_value=1000, + end_value=1500, + display_change=display_balance +) +``` + +## API Reference + +### show_widget() + +Show a widget with an animation. + +```python +WidgetAnimator.show_widget(widget, anim_type="fade", duration=500, delay=0) +``` + +**Parameters:** +- `widget` (lv.obj): The widget to show +- `anim_type` (str): Animation type - `"fade"`, `"slide_down"`, or `"slide_up"` (default: `"fade"`) +- `duration` (int): Animation duration in milliseconds (default: 500) +- `delay` (int): Animation delay in milliseconds (default: 0) + +**Returns:** The animation object + +**Behavior:** +- Removes the `HIDDEN` flag at the start of animation +- Animates opacity (fade) or position (slide) +- Resets final state after animation completes + +### hide_widget() + +Hide a widget with an animation. + +```python +WidgetAnimator.hide_widget(widget, anim_type="fade", duration=500, delay=0, hide=True) +``` + +**Parameters:** +- `widget` (lv.obj): The widget to hide +- `anim_type` (str): Animation type - `"fade"`, `"slide_down"`, or `"slide_up"` (default: `"fade"`) +- `duration` (int): Animation duration in milliseconds (default: 500) +- `delay` (int): Animation delay in milliseconds (default: 0) +- `hide` (bool): If `True`, adds `HIDDEN` flag after animation. If `False`, only animates opacity/position (default: `True`) + +**Returns:** The animation object + +**Behavior:** +- Animates opacity (fade) or position (slide) +- Adds `HIDDEN` flag after animation (if `hide=True`) +- Restores original position after slide animations + +### change_widget() + +Animate a numeric value with a custom display callback. + +```python +WidgetAnimator.change_widget( + widget, + anim_type="interpolate", + duration=5000, + delay=0, + begin_value=0, + end_value=100, + display_change=None +) +``` + +**Parameters:** +- `widget` (lv.obj): The widget being animated (used for cleanup) +- `anim_type` (str): Animation type - currently only `"interpolate"` is supported +- `duration` (int): Animation duration in milliseconds (default: 5000) +- `delay` (int): Animation delay in milliseconds (default: 0) +- `begin_value` (int/float): Starting value for interpolation (default: 0) +- `end_value` (int/float): Ending value for interpolation (default: 100) +- `display_change` (callable, optional): Callback function called with each interpolated value. If `None`, calls `widget.set_text(str(value))` + +**Returns:** The animation object + +**Behavior:** +- Interpolates between `begin_value` and `end_value` +- Calls `display_change(value)` for each frame +- Ensures final value is set after animation completes +- Uses ease-in-out path for smooth motion + +### Convenience Functions + +**`smooth_show(widget, duration=500, delay=0)`** + +Fade in a widget (shorthand for `show_widget()` with fade animation). + +**`smooth_hide(widget, hide=True, duration=500, delay=0)`** + +Fade out a widget (shorthand for `hide_widget()` with fade animation). + +## Animation Types + +### Fade + +Animates widget opacity from 0 to 255 (show) or 255 to 0 (hide). + +```python +# Fade in +WidgetAnimator.show_widget(widget, anim_type="fade", duration=500) + +# Fade out +WidgetAnimator.hide_widget(widget, anim_type="fade", duration=500) +``` + +**Use cases:** +- Smooth appearance/disappearance of UI elements +- Transitioning between screens +- Highlighting important information + +### Slide Down + +Animates widget position from above the screen to its original position (show) or from original position to below (hide). + +```python +# Slide down from above +WidgetAnimator.show_widget(widget, anim_type="slide_down", duration=500) + +# Slide down to below +WidgetAnimator.hide_widget(widget, anim_type="slide_down", duration=500) +``` + +**Use cases:** +- Drawer-like panels sliding down +- Notifications appearing from top +- Menu items sliding in + +### Slide Up + +Animates widget position from below the screen to its original position (show) or from original position to above (hide). + +```python +# Slide up from below +WidgetAnimator.show_widget(widget, anim_type="slide_up", duration=500) + +# Slide up to above +WidgetAnimator.hide_widget(widget, anim_type="slide_up", duration=500) +``` + +**Use cases:** +- Keyboard sliding up from bottom +- Bottom sheets appearing +- Menu items sliding in from bottom + +### Interpolate + +Animates numeric values with smooth easing. + +```python +WidgetAnimator.change_widget( + widget, + anim_type="interpolate", + duration=5000, + begin_value=0, + end_value=100, + display_change=lambda v: label.set_text(f"{int(v)}") +) +``` + +**Use cases:** +- Animating balance updates +- Progress indicators +- Counters +- Numeric displays + +## Complete Examples + +### Image Viewer with Fade Transitions + +```python +from mpos import Activity, smooth_show, smooth_hide +import lvgl as lv + +class ImageViewerActivity(Activity): + def onCreate(self): + self.screen = lv.obj() + + # Image widget + self.image = lv.image(self.screen) + self.image.center() + self.image.add_flag(lv.obj.FLAG.HIDDEN) # Start hidden + + # Navigation buttons + prev_btn = lv.button(self.screen) + prev_btn.align(lv.ALIGN.BOTTOM_LEFT, 0, 0) + prev_btn.add_event_cb(lambda e: self.show_prev_image(), lv.EVENT.CLICKED, None) + + next_btn = lv.button(self.screen) + next_btn.align(lv.ALIGN.BOTTOM_RIGHT, 0, 0) + next_btn.add_event_cb(lambda e: self.show_next_image(), lv.EVENT.CLICKED, None) + + self.setContentView(self.screen) + + def show_prev_image(self): + # Fade out current image + smooth_hide(self.image) + # Load and fade in next image + self.load_image("prev_image.png") + smooth_show(self.image) + + def show_next_image(self): + # Fade out current image + smooth_hide(self.image) + # Load and fade in next image + self.load_image("next_image.png") + smooth_show(self.image) + + def load_image(self, path): + self.image.set_src(f"M:{path}") +``` + +### Wallet Balance Animation + +```python +from mpos import Activity +from mpos.ui.anim import WidgetAnimator +import lvgl as lv + +class WalletActivity(Activity): + def onCreate(self): + self.screen = lv.obj() + + self.balance_label = lv.label(self.screen) + self.balance_label.set_text("0 sats") + self.balance_label.align(lv.ALIGN.TOP_MID, 0, 20) + + self.setContentView(self.screen) + + def on_balance_received(self, old_balance, new_balance, sats_added): + """Animate balance update when payment received.""" + def display_balance(value): + self.balance_label.set_text(f"{int(value)} sats") + + # Animate from old balance to new balance over 3 seconds + WidgetAnimator.change_widget( + self.balance_label, + anim_type="interpolate", + duration=3000, + begin_value=old_balance, + end_value=new_balance, + display_change=display_balance + ) +``` + +### Fullscreen Mode Toggle with Slide Animations + +```python +from mpos import Activity, smooth_show, smooth_hide +from mpos.ui.anim import WidgetAnimator +import lvgl as lv + +class ImageViewerActivity(Activity): + def onCreate(self): + self.screen = lv.obj() + self.fullscreen = False + + # Image + self.image = lv.image(self.screen) + self.image.center() + + # UI controls + self.controls = lv.obj(self.screen) + self.controls.set_size(lv.pct(100), 50) + self.controls.align(lv.ALIGN.BOTTOM_MID, 0, 0) + + prev_btn = lv.button(self.controls) + prev_btn.align(lv.ALIGN.LEFT, 0, 0) + + next_btn = lv.button(self.controls) + next_btn.align(lv.ALIGN.RIGHT, 0, 0) + + self.image.add_event_cb(lambda e: self.toggle_fullscreen(), lv.EVENT.CLICKED, None) + + self.setContentView(self.screen) + + def toggle_fullscreen(self): + if self.fullscreen: + # Exit fullscreen - slide controls back up + self.fullscreen = False + WidgetAnimator.show_widget(self.controls, anim_type="slide_up", duration=300) + else: + # Enter fullscreen - slide controls down + self.fullscreen = True + WidgetAnimator.hide_widget(self.controls, anim_type="slide_down", duration=300, hide=False) +``` + +## Best Practices + +### 1. Use Convenience Functions for Simple Cases + +```python +# Good - simple and readable +smooth_show(widget) +smooth_hide(widget) + +# Less readable - more verbose +WidgetAnimator.show_widget(widget, anim_type="fade", duration=500, delay=0) +``` + +### 2. Provide Custom Display Callbacks for Value Animations + +```python +# Good - custom formatting +def display_balance(value): + balance_label.set_text(f"{int(value):,} sats") + +WidgetAnimator.change_widget( + balance_label, + anim_type="interpolate", + duration=3000, + begin_value=old_balance, + end_value=new_balance, + display_change=display_balance +) + +# Less good - default string conversion +WidgetAnimator.change_widget( + balance_label, + anim_type="interpolate", + duration=3000, + begin_value=old_balance, + end_value=new_balance +) +``` + +### 3. Use Appropriate Durations + +```python +# Quick feedback (UI elements) +smooth_show(button, duration=200) + +# Standard transitions (screens, panels) +smooth_hide(panel, duration=500) + +# Slow animations (value updates, important transitions) +WidgetAnimator.change_widget( + balance_label, + anim_type="interpolate", + duration=3000, + begin_value=old, + end_value=new, + display_change=display_fn +) +``` + +### 4. Handle Widget Deletion Gracefully + +WidgetAnimator automatically handles deleted widgets through `safe_widget_access()`. No special handling needed: + +```python +# Safe - animation continues even if widget is deleted +WidgetAnimator.show_widget(widget, duration=500) +# ... widget might be deleted here ... +# Animation completes silently without crashing +``` + +### 5. Combine Animations for Complex Transitions + +```python +# Fade out old content +smooth_hide(old_content, duration=300) + +# After fade completes, show new content +# (Use delay to sequence animations) +smooth_show(new_content, duration=300, delay=300) +``` + +## Performance Tips + +### 1. Use Shorter Durations for Frequent Animations + +```python +# Good - quick feedback for user interactions +smooth_show(button, duration=200) + +# Avoid - slow animations for frequent events +smooth_show(button, duration=2000) +``` + +### 2. Limit Simultaneous Animations + +WidgetAnimator automatically stops previous animations on the same widget: + +```python +# Safe - previous animation is stopped +WidgetAnimator.show_widget(widget, duration=500) +WidgetAnimator.show_widget(widget, duration=500) # Previous animation stops +``` + +### 3. Use Appropriate Easing + +All animations use `path_ease_in_out` for smooth, natural motion. This is optimal for most use cases. + +## Troubleshooting + +### Animation Not Visible + +**Symptom**: Animation runs but widget doesn't appear to move/fade + +**Possible causes:** +1. Widget is already in the target state +2. Widget is outside the visible area +3. Animation duration is too short to notice + +**Solution:** +```python +# Ensure widget is in correct initial state +widget.remove_flag(lv.obj.FLAG.HIDDEN) # For show animations +widget.set_style_opa(255, 0) # For fade animations + +# Use longer duration for testing +smooth_show(widget, duration=1000) +``` + +### Widget Disappears After Animation + +**Symptom**: Widget fades out but doesn't reappear + +**Possible cause**: Using `hide=True` (default) which adds `HIDDEN` flag + +**Solution:** +```python +# If you want to keep widget visible but transparent +smooth_hide(widget, hide=False, duration=500) + +# If you want to hide it completely +smooth_hide(widget, hide=True, duration=500) +``` + +### Animation Stutters or Jitters + +**Symptom**: Animation is not smooth, has visual glitches + +**Possible causes:** +1. Too many simultaneous animations +2. Heavy processing during animation +3. Widget being modified during animation + +**Solution:** +```python +# Avoid modifying widget during animation +# Stop any previous animations first +lv.anim_delete(widget, None) + +# Then start new animation +smooth_show(widget, duration=500) +``` + +### Value Animation Shows Wrong Final Value + +**Symptom**: Animation completes but final value is incorrect + +**Possible cause**: `display_change` callback not handling final value correctly + +**Solution:** +```python +# Ensure callback handles all values correctly +def display_balance(value): + # Handle both intermediate and final values + self.balance_label.set_text(f"{int(value)} sats") + +WidgetAnimator.change_widget( + balance_label, + anim_type="interpolate", + duration=3000, + begin_value=old_balance, + end_value=new_balance, + display_change=display_balance +) +``` + +## See Also + +- [Creating Apps](../apps/creating-apps.md) - Learn how to create MicroPythonOS apps +- [Activity](activity.md) - Activity lifecycle and UI management +- [LVGL Documentation](https://docs.lvgl.io/) - LVGL animation API reference diff --git a/mkdocs.yml b/mkdocs.yml index f9ed295..1e423ed 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -56,6 +56,7 @@ nav: - SettingActivity: frameworks/setting-activity.md - SettingsActivity: frameworks/settings-activity.md - TaskManager: frameworks/task-manager.md + - WidgetAnimator: frameworks/widget-animator.md - WifiService: frameworks/wifi-service.md - Architecture: - Overview: architecture/overview.md From 368c24c126000a2531fb957d8fabab2613afc5c0 Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Wed, 14 Jan 2026 21:13:25 +0100 Subject: [PATCH 030/110] SettingActivity: add default_value option --- docs/frameworks/setting-activity.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/frameworks/setting-activity.md b/docs/frameworks/setting-activity.md index 5a52d71..60e0f6a 100644 --- a/docs/frameworks/setting-activity.md +++ b/docs/frameworks/setting-activity.md @@ -39,6 +39,7 @@ Each setting is defined as a dictionary with the following properties: - **`ui`** (string): UI type to use for editing. Options: `"textarea"` (default), `"radiobuttons"`, `"dropdown"`, `"activity"` - **`ui_options`** (list): Options for `radiobuttons` and `dropdown` UI types - **`placeholder`** (string): Placeholder text for textarea input +- **`default_value`** (string): Default value to select or fill in - **`changed_callback`** (function): Callback function called when the setting value changes - **`should_show`** (function): Function to determine if this setting should be displayed - **`dont_persist`** (bool): If `True`, the setting won't be saved to SharedPreferences From fb56f9c89cfec56fa5141bfd68c5f6b4e6bbea57 Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Thu, 15 Jan 2026 12:14:25 +0100 Subject: [PATCH 031/110] Update release-checklist.md --- docs/other/release-checklist.md | 62 +++++++++++++++++++-------------- 1 file changed, 35 insertions(+), 27 deletions(-) diff --git a/docs/other/release-checklist.md b/docs/other/release-checklist.md index 50139fa..8b3adbd 100644 --- a/docs/other/release-checklist.md +++ b/docs/other/release-checklist.md @@ -2,51 +2,59 @@ Follow these steps to create a new release of MicroPythonOS. -**Update Changelog**: - -Document changes in `CHANGELOG.md`. - **Update Version Numbers**: - Increment `CURRENT_OS_VERSION` in `internal_filesystem/lib/mpos/info.py`. - Update version numbers for modified apps: ``` -git diff --stat 0.0.4 internal_filesystem/ # Check changes since last release -git diff 0.0.4 -- internal_filesystem/apps/*/META-INF/* # Check app manifests -git diff 0.0.4 -- internal_filesystem/builtin/apps/*/META-INF/* # Check built-in app manifests +git diff --stat 0.6.0 internal_filesystem/ # Check changes since last release, make sure each app change is accompanied by a META-INF/MANIFEST.json change ``` -**Commit and push** all changes, also in external repositories (e.g., [LightningPiggy](https://github.com/LightningPiggy/LightningPiggyApp)): +**Update Changelog**: -**Bundle and publish apps**: +- Document changes in `CHANGELOG.md` +- Run `./scripts/changelog_to_json.sh` to make sure the CHANGELOG.md is json-friendly -``` -./scripts/bundle_apps.sh -pushd ../apps/ -git add apps/ -git commit -a -git push -``` +**Commit and push** all changes, also in external repositories (e.g., [LightningPiggy](https://github.com/LightningPiggy/LightningPiggyApp)). -**Build for all supported devices** +This will trigger the GitHub builds at https://github.com/MicroPythonOS/MicroPythonOS/actions -``` -./scripts/build_all.sh -``` +**Download the builds** + +When finished, download and extract these builds as artifacts from the [GitHub actions](https://github.com/MicroPythonOS/MicroPythonOS/actions): + +- `MicroPythonOS_esp32_0.6.0.ota.zip` +- `MicroPythonOS_esp32_0.6.0.bin.zip` +- `MicroPythonOS_amd64_linux_0.6.0.elf.zip` +- `MicroPythonOS_amd64_macOS_0.6.0.bin.zip` + +**Release to Over-The-Air update** + +- Copy `MicroPythonOS_esp32_0.6.0.ota` to the [updates repository](https://github.com/MicroPythonOS/updates) +- Update the `osupdate*.json` metadata files as specified in the [README.md](https://github.com/MicroPythonOS/updates/blob/main/README.md) + +**Release to the web installer** + +- Copy `MicroPythonOS_esp32_0.6.0.bin` file to the [web installer](https://github.com/MicroPythonOS/install) +- Update the [manifest.json metadata file](https://github.com/MicroPythonOS/install/blob/master/manifests/esp32/MicroPythonOS_esp32_0.6.x.json) +- Update `index.html` if necessary (for example, if you added a new metadata.json you need to update the 2 references) **Release to GitHub** -- Upload ``` ../build_outputs/ ``` to a [new GitHub release](https://github.com/MicroPythonOS/MicroPythonOS/releases/new) -- Add the CHANGELOG.md -- Tag the code with the new release +- Upload the builds a [new GitHub release](https://github.com/MicroPythonOS/MicroPythonOS/releases/new) +- Fill in the new tag (e.g. 0.6.0) +- Copy-paste the list from `CHANGELOG.md` in it +- Add the "What to do" section at the bottom -**Copy the builds to the [install](https://github.com/MicroPythonOS/install) and [updates](https://github.com/MicroPythonOS/updates) repositories**: +**Bundle and publish apps**: -This is a manual action, but check out these old scripts for inspiration: ``` -scripts/release_to_updates.sh -scripts/release_to_install.sh +./scripts/bundle_apps.sh +pushd ../apps/ +git add apps/ +git commit -a +git push ``` ## Notes From d4dc3e183838ad089748eaaeebc5d8928391fc51 Mon Sep 17 00:00:00 2001 From: Quasi Kili Date: Thu, 22 Jan 2026 13:15:14 +0100 Subject: [PATCH 032/110] added logo and site icon --- docs/overrides/main.html | 10 +++++++++- mkdocs.yml | 1 + 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/overrides/main.html b/docs/overrides/main.html index e6bf5cb..e17c762 100644 --- a/docs/overrides/main.html +++ b/docs/overrides/main.html @@ -1,7 +1,15 @@ {% extends "base.html" %} {% block styles %} - {{ super() }} +{{ super() }} + + + + + + + + + + +
+

Emoji Frequency (Canonical)

+

1429 rows

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
rankfrequency_groupposition_in_grouphtml_entityemojiunicode_codepoint_hexpromotedcanonical_groupgroup_typegroup_sizegroup_membersdropped_similar
0637&amp;#9889;26A1yessingle::⚡single1
0731&amp;#x1f44e;👎1F44Eyessingle::👎single1👎
00🥳🥳1F973yessynthetic::🥳single1🥳
101&amp;#x1f602;😂1F602nomanual::tears_laughingmanual2😂 🤣🤣
202&amp;#10084;&amp;#65039;❤️2764-FE0Fnomanual::heartsmanual16❤️ 💕 ♥️ 💔 💖 💙 💜 💞 💗 💚 💛 💓 🖤 ❣️ 💘 💝💕 ♥️ 💔 💖 💙 💜 💞 💗 💚 💛 💓 🖤 ❣️ 💘 💝
311&amp;#x1f60d;😍1F60Dnomanual::smile_lovemanual4😍 😻 😈 😹😻 😈 😹
421&amp;#x1f60a;😊1F60Anosingle::😊single1😊
522&amp;#x1f64f;🙏1F64Fnosingle::🙏single1🙏
624&amp;#x1f62d;😭1F62Dnosingle::😭single1😭
725&amp;#x1f618;😘1F618nosingle::😘single1😘
831&amp;#x1f44d;👍1F44Dnomanual::good_handmanual4👍 👏 👌 🙌👏 👌 🙌
932&amp;#x1f605;😅1F605nosingle::😅single1😅
1034&amp;#x1f601;😁1F601nomanual::smile_beammanual2😁 😄😄
1136&amp;#x1f525;🔥1F525nosingle::🔥single1🔥
12310&amp;#x1f622;😢1F622nomanual::sad_crymanual4😢 😪 😥 😓😪 😥 😓
13311&amp;#x1f914;🤔1F914nosingle::🤔single1🤔
14312&amp;#x1f606;😆1F606nosingle::😆single1😆
15313&amp;#x1f644;🙄1F644nosingle::🙄single1🙄
16314&amp;#x1f4aa;💪1F4AAnosingle::💪single1💪
17315&amp;#x1f609;😉1F609nosingle::😉single1😉
18316&amp;#9786;&amp;#65039;☺️263A-FE0Fnosingle::☺️single1☺️
19318&amp;#x1f917;🤗1F917nosingle::🤗single1🤗
2042&amp;#x1f614;😔1F614nosingle::😔single1😔
2143&amp;#x1f60e;😎1F60Enosingle::😎single1😎
2244&amp;#x1f607;😇1F607nosingle::😇single1😇
2345&amp;#x1f339;🌹1F339nomanual::flower_groupmanual7🌹 🌸 🌷 🎊 🌺 🌻 🌼🌸 🌷 🎊 🌺 🌻 🌼
2446&amp;#x1f926;🤦1F926nosingle::🤦single1🤦
2547&amp;#x1f389;🎉1F389nosingle::🎉single1🎉
2648&amp;#8252;&amp;#65039;‼️203C-FE0Fnomanual::exclamationmanual2‼️ ❗
27410&amp;#9996;&amp;#65039;✌️270C-FE0Fnosingle::✌️single1✌️
28411&amp;#10024;2728nosingle::✨single1
29412&amp;#x1f937;🤷1F937nosingle::🤷single1🤷
30413&amp;#x1f631;😱1F631nosingle::😱single1😱
31414&amp;#x1f60c;😌1F60Cnosingle::😌single1😌
32417&amp;#x1f60b;😋1F60Bnomanual::tongue_groupmanual2😋 😛😛
33420&amp;#x1f60f;😏1F60Fnosingle::😏single1😏
34422&amp;#x1f642;🙂1F642nosingle::🙂single1🙂
35424&amp;#x1f929;🤩1F929nosingle::🤩single1🤩
36426&amp;#x1f600;😀1F600nomanual::smile_grinmanual2😀 😃😃
37429&amp;#x1f4af;💯1F4AFnosingle::💯single1💯
38430&amp;#x1f648;🙈1F648nosingle::🙈single1🙈
39431&amp;#x1f447;👇1F447nosingle::👇single1👇
40432&amp;#x1f3b6;🎶1F3B6nomanual::music_groupmanual2🎶 🎵🎵
41433&amp;#x1f612;😒1F612nosingle::😒single1😒
42434&amp;#x1f92d;🤭1F92Dnosingle::🤭single1🤭
4352&amp;#x1f61c;😜1F61Cnosingle::😜single1😜
4453&amp;#x1f48b;💋1F48Bnosingle::💋single1💋
4554&amp;#x1f440;👀1F440nosingle::👀single1👀
4656&amp;#x1f611;😑1F611nosingle::😑single1😑
4757&amp;#x1f4a5;💥1F4A5nosingle::💥single1💥
4858&amp;#x1f64b;🙋1F64Bnosingle::🙋single1🙋
4959&amp;#x1f61e;😞1F61Enosingle::😞single1😞
50510&amp;#x1f629;😩1F629nosingle::😩single1😩
51511&amp;#x1f621;😡1F621nomanual::angrymanual3😡 🔴 😤🔴 😤
52512&amp;#x1f92a;🤪1F92Anosingle::🤪single1🤪
53513&amp;#x1f44a;👊1F44Anosingle::👊single1👊
54514&amp;#9728;&amp;#65039;☀️2600-FE0Fnosingle::☀️single1☀️
55516&amp;#x1f924;🤤1F924nosingle::🤤single1🤤
56517&amp;#x1f449;👉1F449nosingle::👉single1👉
57518&amp;#x1f483;💃1F483nosingle::💃single1💃
58519&amp;#x1f633;😳1F633nosingle::😳single1😳
59520&amp;#9995;270Bnosingle::✋single1
60521&amp;#x1f61a;😚1F61Anosingle::😚single1😚
61522&amp;#x1f61d;😝1F61Dnosingle::😝single1😝
62523&amp;#x1f634;😴1F634nosingle::😴single1😴
63524&amp;#x1f31f;🌟1F31Fnosingle::🌟single1🌟
64525&amp;#x1f62c;😬1F62Cnosingle::😬single1😬
65526&amp;#x1f643;🙃1F643nosingle::🙃single1🙃
66527&amp;#x1f340;🍀1F340nosingle::🍀single1🍀
67531&amp;#11088;2B50nosingle::⭐single1
68532&amp;#9989;2705nomanual::checkmarksmanual2✅ ✔️✔️
69533&amp;#x1f308;🌈1F308nosingle::🌈single1🌈
70535&amp;#x1f918;🤘1F918nosingle::🤘single1🤘
7161&amp;#x1f4a6;💦1F4A6nosingle::💦single1💦
7263&amp;#x1f623;😣1F623nosingle::😣single1😣
7364&amp;#x1f3c3;🏃1F3C3nosingle::🏃single1🏃
7465&amp;#x1f490;💐1F490nosingle::💐single1💐
7566&amp;#9785;&amp;#65039;☹️2639-FE0Fnosingle::☹️single1☹️
7669&amp;#x1f620;😠1F620nosingle::😠single1😠
77610&amp;#9757;&amp;#65039;☝️261D-FE0Fnosingle::☝️single1☝️
78611&amp;#x1f615;😕1F615nosingle::😕single1😕
79613&amp;#x1f382;🎂1F382nomanual::birthday_cakesmanual2🎂 👑👑
80615&amp;#x1f610;😐1F610nosingle::😐single1😐
81616&amp;#x1f595;🖕1F595nosingle::🖕single1🖕
82618&amp;#x1f64a;🙊1F64Anosingle::🙊single1🙊
83620&amp;#x1f5e3;&amp;#65039;🗣️1F5E3-FE0Fnosingle::🗣️single1🗣️
84621&amp;#x1f4ab;💫1F4ABnosingle::💫single1💫
85622&amp;#x1f480;💀1F480nosingle::💀single1💀
86625&amp;#x1f91e;🤞1F91Enosingle::🤞single1🤞
87630&amp;#x1f62b;😫1F62Bnosingle::😫single1😫
88631&amp;#9917;26BDnosingle::⚽single1
89632&amp;#x1f919;🤙1F919nosingle::🤙single1🤙
90633&amp;#9749;2615nosingle::☕single1
91634&amp;#x1f3c6;🏆1F3C6nosingle::🏆single1🏆
92635&amp;#x1f9e1;🧡1F9E1nosingle::🧡single1🧡
93636&amp;#x1f381;🎁1F381nosingle::🎁single1🎁
94638&amp;#x1f31e;🌞1F31Enosingle::🌞single1🌞
95639&amp;#x1f388;🎈1F388nosingle::🎈single1🎈
96640&amp;#10060;274Cnosingle::❌single1
97641&amp;#9994;270Anosingle::✊single1
98642&amp;#x1f44b;👋1F44Bnosingle::👋single1👋
99643&amp;#x1f632;😲1F632nosingle::😲single1😲
100644&amp;#x1f33f;🌿1F33Fnosingle::🌿single1🌿
101645&amp;#x1f92b;🤫1F92Bnosingle::🤫single1🤫
102646&amp;#x1f448;👈1F448nosingle::👈single1👈
103647&amp;#x1f62e;😮1F62Enosingle::😮single1😮
104648&amp;#x1f646;🙆1F646nosingle::🙆single1🙆
105649&amp;#x1f37b;🍻1F37Bnosingle::🍻single1🍻
106650&amp;#x1f343;🍃1F343nosingle::🍃single1🍃
107651&amp;#x1f436;🐶1F436nosingle::🐶single1🐶
108652&amp;#x1f481;💁1F481nosingle::💁single1💁
109653&amp;#x1f630;😰1F630nosingle::😰single1😰
110654&amp;#x1f928;🤨1F928nosingle::🤨single1🤨
111655&amp;#x1f636;😶1F636nosingle::😶single1😶
112656&amp;#x1f91d;🤝1F91Dnosingle::🤝single1🤝
113657&amp;#x1f6b6;🚶1F6B6nosingle::🚶single1🚶
114658&amp;#x1f4b0;💰1F4B0nosingle::💰single1💰
115659&amp;#x1f353;🍓1F353nosingle::🍓single1🍓
116660&amp;#x1f4a2;💢1F4A2nosingle::💢single1💢
11771&amp;#x1f1fa;&amp;#x1f1f8;🇺🇸1F1FA-1F1F8nosingle::🇺🇸single1🇺🇸
11872&amp;#x1f91f;🤟1F91Fnosingle::🤟single1🤟
11973&amp;#x1f641;🙁1F641nosingle::🙁single1🙁
12074&amp;#x1f6a8;🚨1F6A8nosingle::🚨single1🚨
12175&amp;#x1f4a8;💨1F4A8nosingle::💨single1💨
12276&amp;#x1f92c;🤬1F92Cnosingle::🤬single1🤬
12377&amp;#9992;&amp;#65039;✈️2708-FE0Fnosingle::✈️single1✈️
12478&amp;#x1f380;🎀1F380nosingle::🎀single1🎀
12579&amp;#x1f37a;🍺1F37Anosingle::🍺single1🍺
126710&amp;#x1f913;🤓1F913nosingle::🤓single1🤓
127711&amp;#x1f619;😙1F619nosingle::😙single1😙
128712&amp;#x1f49f;💟1F49Fnosingle::💟single1💟
129713&amp;#x1f331;🌱1F331nosingle::🌱single1🌱
130714&amp;#x1f616;😖1F616nosingle::😖single1😖
131715&amp;#x1f476;👶1F476nosingle::👶single1👶
132716&amp;#9654;&amp;#65039;▶️25B6-FE0Fnosingle::▶️single1▶️
133717&amp;#10145;&amp;#65039;➡️27A1-FE0Fnosingle::➡️single1➡️
134718&amp;#10067;2753nosingle::❓single1
135719&amp;#x1f48e;💎1F48Enosingle::💎single1💎
136720&amp;#x1f4b8;💸1F4B8nosingle::💸single1💸
137721&amp;#11015;&amp;#65039;⬇️2B07-FE0Fnosingle::⬇️single1⬇️
138722&amp;#x1f628;😨1F628nosingle::😨single1😨
139723&amp;#x1f31a;🌚1F31Anosingle::🌚single1🌚
140724&amp;#x1f98b;🦋1F98Bnosingle::🦋single1🦋
141725&amp;#x1f637;😷1F637nosingle::😷single1😷
142726&amp;#x1f57a;🕺1F57Anosingle::🕺single1🕺
143727&amp;#9888;&amp;#65039;⚠️26A0-FE0Fnosingle::⚠️single1⚠️
144728&amp;#x1f645;🙅1F645nosingle::🙅single1🙅
145729&amp;#x1f61f;😟1F61Fnosingle::😟single1😟
146730&amp;#x1f635;😵1F635nosingle::😵single1😵
147732&amp;#x1f932;🤲1F932nosingle::🤲single1🤲
148733&amp;#x1f920;🤠1F920nosingle::🤠single1🤠
149734&amp;#x1f927;🤧1F927nosingle::🤧single1🤧
150735&amp;#x1f4cc;📌1F4CCnosingle::📌single1📌
151736&amp;#x1f535;🔵1F535nosingle::🔵single1🔵
152737&amp;#x1f485;💅1F485nosingle::💅single1💅
153738&amp;#x1f9d0;🧐1F9D0nosingle::🧐single1🧐
154739&amp;#x1f43e;🐾1F43Enosingle::🐾single1🐾
155740&amp;#x1f352;🍒1F352nosingle::🍒single1🍒
156741&amp;#x1f617;😗1F617nosingle::😗single1😗
157742&amp;#x1f911;🤑1F911nosingle::🤑single1🤑
158743&amp;#x1f680;🚀1F680nosingle::🚀single1🚀
159744&amp;#x1f30a;🌊1F30Anosingle::🌊single1🌊
160745&amp;#x1f92f;🤯1F92Fnosingle::🤯single1🤯
161746&amp;#x1f437;🐷1F437nosingle::🐷single1🐷
162747&amp;#9742;&amp;#65039;☎️260E-FE0Fnosingle::☎️single1☎️
163748&amp;#x1f4a7;💧1F4A7nosingle::💧single1💧
164749&amp;#x1f62f;😯1F62Fnosingle::😯single1😯
165750&amp;#x1f486;💆1F486nosingle::💆single1💆
166751&amp;#x1f446;👆1F446nosingle::👆single1👆
167752&amp;#x1f3a4;🎤1F3A4nosingle::🎤single1🎤
168753&amp;#x1f647;🙇1F647nosingle::🙇single1🙇
169754&amp;#x1f351;🍑1F351nosingle::🍑single1🍑
170755&amp;#10052;&amp;#65039;❄️2744-FE0Fnosingle::❄️single1❄️
171756&amp;#x1f334;🌴1F334nosingle::🌴single1🌴
172757&amp;#x1f1e7;&amp;#x1f1f7;🇧🇷1F1E7-1F1F7nosingle::🇧🇷single1🇧🇷
173758&amp;#x1f4a3;💣1F4A3nosingle::💣single1💣
174759&amp;#x1f438;🐸1F438nosingle::🐸single1🐸
175760&amp;#x1f48c;💌1F48Cnosingle::💌single1💌
176761&amp;#x1f4cd;📍1F4CDnosingle::📍single1📍
177762&amp;#x1f940;🥀1F940nosingle::🥀single1🥀
178763&amp;#x1f922;🤢1F922nosingle::🤢single1🤢
179764&amp;#x1f445;👅1F445nosingle::👅single1👅
180765&amp;#x1f4a1;💡1F4A1nosingle::💡single1💡
181766&amp;#x1f4a9;💩1F4A9nosingle::💩single1💩
182767&amp;#8265;&amp;#65039;⁉️2049-FE0Fnosingle::⁉️single1⁉️
183768&amp;#x1f450;👐1F450nosingle::👐single1👐
184769&amp;#x1f4f8;📸1F4F8nosingle::📸single1📸
185770&amp;#x1f47b;👻1F47Bnosingle::👻single1👻
186771&amp;#x1f910;🤐1F910nosingle::🤐single1🤐
187772&amp;#x1f92e;🤮1F92Enosingle::🤮single1🤮
188773&amp;#x1f3bc;🎼1F3BCnosingle::🎼single1🎼
189774&amp;#9997;&amp;#65039;✍️270D-FE0Fnosingle::✍️single1✍️
190775&amp;#x1f6a9;🚩1F6A9nosingle::🚩single1🚩
191776&amp;#x1f34e;🍎1F34Enosingle::🍎single1🍎
192777&amp;#x1f34a;🍊1F34Anosingle::🍊single1🍊
193778&amp;#x1f47c;👼1F47Cnosingle::👼single1👼
194779&amp;#x1f48d;💍1F48Dnosingle::💍single1💍
195780&amp;#x1f4e3;📣1F4E3nosingle::📣single1📣
196781&amp;#x1f942;🥂1F942nosingle::🥂single1🥂
197782&amp;#10549;&amp;#65039;⤵️2935-FE0Fnosingle::⤵️single1⤵️
198783&amp;#x1f4f1;📱1F4F1nosingle::📱single1📱
199784&amp;#9748;2614nosingle::☔single1
200785&amp;#x1f319;🌙1F319nosingle::🌙single1🌙
20181&amp;#x1f37e;🍾1F37Enosingle::🍾single1🍾
20282&amp;#x1f3a7;🎧1F3A7nosingle::🎧single1🎧
20383&amp;#x1f341;🍁1F341nosingle::🍁single1🍁
20484&amp;#11093;2B55nosingle::⭕single1
20585&amp;#x1f3c0;🏀1F3C0nosingle::🏀single1🏀
20686&amp;#9760;&amp;#65039;☠️2620-FE0Fnosingle::☠️single1☠️
20787&amp;#9899;26ABnosingle::⚫single1
20888&amp;#x1f590;&amp;#65039;🖐️1F590-FE0Fnosingle::🖐️single1🖐️
20989&amp;#x1f627;😧1F627nosingle::😧single1😧
210810&amp;#x1f3af;🎯1F3AFnosingle::🎯single1🎯
211811&amp;#x1f4f2;📲1F4F2nosingle::📲single1📲
212812&amp;#9752;&amp;#65039;☘️2618-FE0Fnosingle::☘️single1☘️
213813&amp;#x1f441;&amp;#65039;👁️1F441-FE0Fnosingle::👁️single1👁️
214814&amp;#x1f377;🍷1F377nosingle::🍷single1🍷
215815&amp;#x1f444;👄1F444nosingle::👄single1👄
216816&amp;#x1f41f;🐟1F41Fnosingle::🐟single1🐟
217817&amp;#x1f370;🍰1F370nosingle::🍰single1🍰
218818&amp;#x1f4a4;💤1F4A4nosingle::💤single1💤
219819&amp;#x1f54a;&amp;#65039;🕊️1F54A-FE0Fnosingle::🕊️single1🕊️
220820&amp;#x1f4fa;📺1F4FAnosingle::📺single1📺
221821&amp;#x1f4ad;💭1F4ADnosingle::💭single1💭
222822&amp;#x1f431;🐱1F431nosingle::🐱single1🐱
223823&amp;#x1f41d;🐝1F41Dnosingle::🐝single1🐝
224824&amp;#x1f1f2;&amp;#x1f1fd;🇲🇽1F1F2-1F1FDnosingle::🇲🇽single1🇲🇽
225825&amp;#x1f9da;🧚1F9DAnosingle::🧚single1🧚
226826&amp;#x1f51d;🔝1F51Dnosingle::🔝single1🔝
227827&amp;#x1f4e2;📢1F4E2nosingle::📢single1📢
228828&amp;#x1f4f7;📷1F4F7nosingle::📷single1📷
229829&amp;#x1f415;🐕1F415nosingle::🐕single1🐕
230830&amp;#x1f3b8;🎸1F3B8nosingle::🎸single1🎸
231831&amp;#x1f52b;🔫1F52Bnosingle::🔫single1🔫
232832&amp;#x1f91a;🤚1F91Anosingle::🤚single1🤚
233833&amp;#x1f36d;🍭1F36Dnosingle::🍭single1🍭
234834&amp;#x1f346;🍆1F346nosingle::🍆single1🍆
235835&amp;#x1f489;💉1F489nosingle::💉single1💉
236836&amp;#x1f30e;🌎1F30Enosingle::🌎single1🌎
237837&amp;#x1f626;😦1F626nosingle::😦single1😦
238838&amp;#x1f300;🌀1F300nosingle::🌀single1🌀
239839&amp;#x1f47f;👿1F47Fnosingle::👿single1👿
240840&amp;#9745;&amp;#65039;☑️2611-FE0Fnosingle::☑️single1☑️
241841&amp;#x1f3a5;🎥1F3A5nosingle::🎥single1🎥
242842&amp;#x1f327;&amp;#65039;🌧️1F327-FE0Fnosingle::🌧️single1🌧️
243843&amp;#x1f47d;👽1F47Dnosingle::👽single1👽
244844&amp;#x1f34b;🍋1F34Bnosingle::🍋single1🍋
245845&amp;#x1f912;🤒1F912nosingle::🤒single1🤒
246846&amp;#x1f921;🤡1F921nosingle::🤡single1🤡
247847&amp;#x1f36b;🍫1F36Bnosingle::🍫single1🍫
248848&amp;#x1f4da;📚1F4DAnosingle::📚single1📚
249849&amp;#x1f3c1;🏁1F3C1nosingle::🏁single1🏁
250850&amp;#x1f915;🤕1F915nosingle::🤕single1🤕
251851&amp;#x1f984;🦄1F984nosingle::🦄single1🦄
252852&amp;#x1f345;🍅1F345nosingle::🍅single1🍅
253853&amp;#x1f697;🚗1F697nosingle::🚗single1🚗
254854&amp;#x1f6ab;🚫1F6ABnosingle::🚫single1🚫
255855&amp;#x1f4b5;💵1F4B5nosingle::💵single1💵
256856&amp;#9918;26BEnosingle::⚾single1
257857&amp;#x1f52a;🔪1F52Anosingle::🔪single1🔪
258858&amp;#x1f514;🔔1F514nosingle::🔔single1🔔
259859&amp;#9832;&amp;#65039;♨️2668-FE0Fnosingle::♨️single1♨️
260860&amp;#x1f333;🌳1F333nosingle::🌳single1🌳
261861&amp;#x1f50a;🔊1F50Anosingle::🔊single1🔊
262862&amp;#x1f36c;🍬1F36Cnosingle::🍬single1🍬
263863&amp;#x1f48f;💏1F48Fnosingle::💏single1💏
264864&amp;#x1f37c;🍼1F37Cnosingle::🍼single1🍼
265865&amp;#x1f35c;🍜1F35Cnosingle::🍜single1🍜
266866&amp;#x1f43c;🐼1F43Cnosingle::🐼single1🐼
267867&amp;#x1f649;🙉1F649nosingle::🙉single1🙉
268868&amp;#x1f408;🐈1F408nosingle::🐈single1🐈
269869&amp;#x1f43b;🐻1F43Bnosingle::🐻single1🐻
270870&amp;#x1f938;🤸1F938nosingle::🤸single1🤸
271871&amp;#x1f31d;🌝1F31Dnosingle::🌝single1🌝
272872&amp;#x1f478;👸1F478nosingle::👸single1👸
273873&amp;#x1f355;🍕1F355nosingle::🍕single1🍕
274874&amp;#x1f34c;🍌1F34Cnosingle::🍌single1🍌
275875&amp;#x1f366;🍦1F366nosingle::🍦single1🍦
276876&amp;#9898;26AAnosingle::⚪single1
277877&amp;#x1f469;👩1F469nosingle::👩single1👩
278878&amp;#x1f63f;😿1F63Fnosingle::😿single1😿
279879&amp;#x1f342;🍂1F342nosingle::🍂single1🍂
280880&amp;#x1f4de;📞1F4DEnosingle::📞single1📞
281881&amp;#9200;23F0nosingle::⏰single1
282882&amp;#x1f51e;🔞1F51Enosingle::🔞single1🔞
283883&amp;#x1f30d;🌍1F30Dnosingle::🌍single1🌍
284884&amp;#x1f320;🌠1F320nosingle::🌠single1🌠
285885&amp;#x1f640;🙀1F640nosingle::🙀single1🙀
286886&amp;#9642;&amp;#65039;▪️25AA-FE0Fnosingle::▪️single1▪️
287887&amp;#9729;&amp;#65039;☁️2601-FE0Fnosingle::☁️single1☁️
288888&amp;#x1f479;👹1F479nosingle::👹single1👹
289889&amp;#x1f349;🍉1F349nosingle::🍉single1🍉
290890&amp;#x1f425;🐥1F425nosingle::🐥single1🐥
291891&amp;#x1f336;&amp;#65039;🌶️1F336-FE0Fnosingle::🌶️single1🌶️
2928921&amp;#65039;&amp;#8419;1️⃣31-FE0F-20E3nosingle::1️⃣single11️⃣
293893&amp;#x1f335;🌵1F335nosingle::🌵single1🌵
294894&amp;#x1f1ee;&amp;#x1f1f3;🇮🇳1F1EE-1F1F3nosingle::🇮🇳single1🇮🇳
295895&amp;#x1f467;👧1F467nosingle::👧single1👧
296896&amp;#x1f344;🍄1F344nosingle::🍄single1🍄
297897&amp;#x1f46e;👮1F46Enosingle::👮single1👮
298898&amp;#x1f4ae;💮1F4AEnosingle::💮single1💮
299899&amp;#x1f430;🐰1F430nosingle::🐰single1🐰
3008100&amp;#x1f537;🔷1F537nosingle::🔷single1🔷
3018101&amp;#x1f33e;🌾1F33Enosingle::🌾single1🌾
3028102&amp;#x1f539;🔹1F539nosingle::🔹single1🔹
3038103&amp;#x1f1f9;&amp;#x1f1f7;🇹🇷1F1F9-1F1F7nosingle::🇹🇷single1🇹🇷
3048104&amp;#x1f947;🥇1F947nosingle::🥇single1🥇
3058105&amp;#x1f1ee;&amp;#x1f1f9;🇮🇹1F1EE-1F1F9nosingle::🇮🇹single1🇮🇹
30691&amp;#x1f36a;🍪1F36Anosingle::🍪single1🍪
30792&amp;#x1f1e6;&amp;#x1f1f7;🇦🇷1F1E6-1F1F7nosingle::🇦🇷single1🇦🇷
30893&amp;#x1f6d1;🛑1F6D1nosingle::🛑single1🛑
30994&amp;#x1f40d;🐍1F40Dnosingle::🐍single1🐍
31095&amp;#x1f393;🎓1F393nosingle::🎓single1🎓
31196&amp;#x1f1e8;&amp;#x1f1e6;🇨🇦1F1E8-1F1E6nosingle::🇨🇦single1🇨🇦
31297&amp;#x1f34f;🍏1F34Fnosingle::🍏single1🍏
31398&amp;#x1f981;🦁1F981nosingle::🦁single1🦁
31499&amp;#x1f63d;😽1F63Dnosingle::😽single1😽
315910&amp;#x1f6ac;🚬1F6ACnosingle::🚬single1🚬
316911&amp;#x1f356;🍖1F356nosingle::🍖single1🍖
317912&amp;#x1f374;🍴1F374nosingle::🍴single1🍴
318913&amp;#x1f198;🆘1F198nosingle::🆘single1🆘
319914&amp;#x1f91c;🤜1F91Cnosingle::🤜single1🤜
320915&amp;#x1f37f;🍿1F37Fnosingle::🍿single1🍿
321916&amp;#x1f354;🍔1F354nosingle::🍔single1🍔
322917&amp;#x1f4dd;📝1F4DDnosingle::📝single1📝
323918&amp;#x1f1ef;&amp;#x1f1f5;🇯🇵1F1EF-1F1F5nosingle::🇯🇵single1🇯🇵
324919&amp;#x1f36e;🍮1F36Enosingle::🍮single1🍮
325920&amp;#x1f347;🍇1F347nosingle::🍇single1🍇
3269212&amp;#65039;&amp;#8419;2️⃣32-FE0F-20E3nosingle::2️⃣single12️⃣
327922&amp;#x1f3e0;🏠1F3E0nosingle::🏠single1🏠
328923&amp;#x1f930;🤰1F930nosingle::🤰single1🤰
329924&amp;#x1f423;🐣1F423nosingle::🐣single1🐣
330925&amp;#x1f412;🐒1F412nosingle::🐒single1🐒
331926&amp;#x1f466;👦1F466nosingle::👦single1👦
332927&amp;#x1f369;🍩1F369nosingle::🍩single1🍩
333928&amp;#x1f363;🍣1F363nosingle::🍣single1🍣
334929&amp;#x1f91b;🤛1F91Bnosingle::🤛single1🤛
335930&amp;#x1f46f;👯1F46Fnosingle::👯single1👯
336931&amp;#x1f3f3;&amp;#65039;&amp;zwj;&amp;#x1f308;🏳️‍🌈1F3F3-FE0F-200D-1F308nosingle::🏳️‍🌈single1🏳️‍🌈
337932&amp;spades;&amp;#65039;♠️2660-FE0Fnosingle::♠️single1♠️
338933&amp;#x1f332;🌲1F332nosingle::🌲single1🌲
339934&amp;#x1f434;🐴1F434nosingle::🐴single1🐴
340935&amp;#x1f35b;🍛1F35Bnosingle::🍛single1🍛
341936&amp;#x1f386;🎆1F386nosingle::🎆single1🎆
342937&amp;#x1f491;💑1F491nosingle::💑single1💑
343938&amp;#x1f35e;🍞1F35Enosingle::🍞single1🍞
344939&amp;#x1f36f;🍯1F36Fnosingle::🍯single1🍯
345940&amp;#9732;&amp;#65039;☄️2604-FE0Fnosingle::☄️single1☄️
346941&amp;#x1f638;😸1F638nosingle::😸single1😸
347942&amp;#x1f35a;🍚1F35Anosingle::🍚single1🍚
348943&amp;#x1f3ac;🎬1F3ACnosingle::🎬single1🎬
349944&amp;#x1f399;&amp;#65039;🎙️1F399-FE0Fnosingle::🎙️single1🎙️
350945&amp;#x1f1e8;&amp;#x1f1f4;🇨🇴1F1E8-1F1F4nosingle::🇨🇴single1🇨🇴
351946&amp;#x1f433;🐳1F433nosingle::🐳single1🐳
352947&amp;#x1f980;🦀1F980nosingle::🦀single1🦀
353948&amp;#x1f943;🥃1F943nosingle::🥃single1🥃
354949&amp;#x1f538;🔸1F538nosingle::🔸single1🔸
355950&amp;#x1f48a;💊1F48Anosingle::💊single1💊
356951&amp;#x1f40e;🐎1F40Enosingle::🐎single1🐎
357952&amp;#x1f379;🍹1F379nosingle::🍹single1🍹
358953&amp;diams;&amp;#65039;♦️2666-FE0Fnosingle::♦️single1♦️
359954&amp;#x1f52e;🔮1F52Enosingle::🔮single1🔮
360955&amp;#x1f468;👨1F468nosingle::👨single1👨
361956&amp;#x1f378;🍸1F378nosingle::🍸single1🍸
362957&amp;#x1f30f;🌏1F30Fnosingle::🌏single1🌏
363958&amp;#x1f474;👴1F474nosingle::👴single1👴
364959&amp;#x1f9e2;🧢1F9E2nosingle::🧢single1🧢
365960&amp;#x1f43d;🐽1F43Dnosingle::🐽single1🐽
366961&amp;#x1f414;🐔1F414nosingle::🐔single1🐔
367962&amp;#x1f3bb;🎻1F3BBnosingle::🎻single1🎻
368963&amp;#11014;&amp;#65039;⬆️2B06-FE0Fnosingle::⬆️single1⬆️
369964&amp;#9986;&amp;#65039;✂️2702-FE0Fnosingle::✂️single1✂️
370965&amp;#x1f46b;👫1F46Bnosingle::👫single1👫
371966&amp;#x1f463;👣1F463nosingle::👣single1👣
372967&amp;#x1f42f;🐯1F42Fnosingle::🐯single1🐯
373968&amp;#x1f3ae;🎮1F3AEnosingle::🎮single1🎮
374969&amp;#x1f375;🍵1F375nosingle::🍵single1🍵
375970&amp;#x1f426;🐦1F426nosingle::🐦single1🐦
376971&amp;#x1f1ec;&amp;#x1f1e7;🇬🇧1F1EC-1F1E7nosingle::🇬🇧single1🇬🇧
377972&amp;#12336;&amp;#65039;〰️3030-FE0Fnosingle::〰️single1〰️
378973&amp;#x1f46d;👭1F46Dnosingle::👭single1👭
379974&amp;#x1f42c;🐬1F42Cnosingle::🐬single1🐬
380975&amp;#x1f35f;🍟1F35Fnosingle::🍟single1🍟
381976&amp;#x1f459;👙1F459nosingle::👙single1👙
382977&amp;#10006;&amp;#65039;✖️2716-FE0Fnosingle::✖️single1✖️
383978&amp;#x1f4e9;📩1F4E9nosingle::📩single1📩
384979&amp;#x1f475;👵1F475nosingle::👵single1👵
385980&amp;#x1f368;🍨1F368nosingle::🍨single1🍨
386981&amp;#x1f1eb;&amp;#x1f1f7;🇫🇷1F1EB-1F1F7nosingle::🇫🇷single1🇫🇷
387982&amp;#x1f416;🐖1F416nosingle::🐖single1🐖
388983&amp;#10013;&amp;#65039;✝️271D-FE0Fnosingle::✝️single1✝️
389984&amp;#9851;&amp;#65039;♻️267B-FE0Fnosingle::♻️single1♻️
390985&amp;#x1f94a;🥊1F94Anosingle::🥊single1🥊
391986&amp;#x1f985;🦅1F985nosingle::🦅single1🦅
392987&amp;#x1f4ac;💬1F4ACnosingle::💬single1💬
393988&amp;#x1f1e8;&amp;#x1f1f1;🇨🇱1F1E8-1F1F1nosingle::🇨🇱single1🇨🇱
394989&amp;#x1f422;🐢1F422nosingle::🐢single1🐢
395990&amp;#x1f530;🔰1F530nosingle::🔰single1🔰
396991&amp;#x1f536;🔶1F536nosingle::🔶single1🔶
397992&amp;#x1f397;&amp;#65039;🎗️1F397-FE0Fnosingle::🎗️single1🎗️
398993&amp;#x1f484;💄1F484nosingle::💄single1💄
399994&amp;#x1f460;👠1F460nosingle::👠single1👠
400995&amp;#x1f955;🥕1F955nosingle::🥕single1🥕
401996&amp;#10134;2796nosingle::➖single1
402997&amp;#x1f43a;🐺1F43Anosingle::🐺single1🐺
403998&amp;#x1f4d6;📖1F4D6nosingle::📖single1📖
404999&amp;#x1f34d;🍍1F34Dnosingle::🍍single1🍍
4059100&amp;#x1f303;🌃1F303nosingle::🌃single1🌃
4069101&amp;#10036;&amp;#65039;✴️2734-FE0Fnosingle::✴️single1✴️
4079102&amp;#x1f30c;🌌1F30Cnosingle::🌌single1🌌
4089103&amp;#x1f413;🐓1F413nosingle::🐓single1🐓
4099104&amp;#x1f442;👂1F442nosingle::👂single1👂
4109105&amp;#x1f364;🍤1F364nosingle::🍤single1🍤
4119106&amp;#x1f410;🐐1F410nosingle::🐐single1🐐
4129107&amp;#x1f53b;🔻1F53Bnosingle::🔻single1🔻
4139108&amp;#x1f4bb;💻1F4BBnosingle::💻single1💻
4149109&amp;#x1f990;🦐1F990nosingle::🦐single1🦐
4159110&amp;#x1f1e9;&amp;#x1f1ea;🇩🇪1F1E9-1F1EAnosingle::🇩🇪single1🇩🇪
4169111&amp;#x1f31b;🌛1F31Bnosingle::🌛single1🌛
4179112&amp;#8600;&amp;#65039;↘️2198-FE0Fnosingle::↘️single1↘️
4189113&amp;#9999;&amp;#65039;✏️270F-FE0Fnosingle::✏️single1✏️
4199114&amp;#x1f9d8;🧘1F9D8nosingle::🧘single1🧘
4209115&amp;#x1f941;🥁1F941nosingle::🥁single1🥁
4219116&amp;#x1f3d6;&amp;#65039;🏖️1F3D6-FE0Fnosingle::🏖️single1🏖️
4229117&amp;#9884;&amp;#65039;⚜️269C-FE0Fnosingle::⚜️single1⚜️
4239118&amp;#10069;2755nosingle::❕single1
4249119&amp;#x1f170;&amp;#65039;🅰️1F170-FE0Fnosingle::🅰️single1🅰️
4259120&amp;#x1f6b4;🚴1F6B4nosingle::🚴single1🚴
4269121&amp;#x1f4a0;💠1F4A0nosingle::💠single1💠
4279122&amp;#12951;&amp;#65039;㊗️3297-FE0Fnosingle::㊗️single1㊗️
4289123&amp;#x1f419;🐙1F419nosingle::🐙single1🐙
4299124&amp;clubs;&amp;#65039;♣️2663-FE0Fnosingle::♣️single1♣️
4309125&amp;#x1f361;🍡1F361nosingle::🍡single1🍡
4319126&amp;#9193;23E9nosingle::⏩single1
4329127&amp;#x1f3a8;🎨1F3A8nosingle::🎨single1🎨
4339128&amp;#x1f420;🐠1F420nosingle::🐠single1🐠
4349129&amp;#x1f1f0;&amp;#x1f1f7;🇰🇷1F1F0-1F1F7nosingle::🇰🇷single1🇰🇷
4359130&amp;#x1f357;🍗1F357nosingle::🍗single1🍗
4369131&amp;#x1f6ae;🚮1F6AEnosingle::🚮single1🚮
4379132&amp;#9643;&amp;#65039;▫️25AB-FE0Fnosingle::▫️single1▫️
4389133&amp;#x1f32a;&amp;#65039;🌪️1F32A-FE0Fnosingle::🌪️single1🌪️
4399134&amp;#x1f63c;😼1F63Cnosingle::😼single1😼
4409135&amp;#x1f464;👤1F464nosingle::👤single1👤
4419136&amp;#x1f3ca;🏊1F3CAnosingle::🏊single1🏊
4429137&amp;#x1f33d;🌽1F33Dnosingle::🌽single1🌽
4439138&amp;#x1f3a9;🎩1F3A9nosingle::🎩single1🎩
4449139&amp;#x1f1ea;&amp;#x1f1f8;🇪🇸1F1EA-1F1F8nosingle::🇪🇸single1🇪🇸
4459140&amp;#x1f3b9;🎹1F3B9nosingle::🎹single1🎹
4469141&amp;#x1f348;🍈1F348nosingle::🍈single1🍈
4479142&amp;#9664;&amp;#65039;◀️25C0-FE0Fnosingle::◀️single1◀️
4489143&amp;harr;&amp;#65039;↔️2194-FE0Fnosingle::↔️single1↔️
4499144&amp;#x1f3e1;🏡1F3E1nosingle::🏡single1🏡
4509145&amp;#x1f1f5;&amp;#x1f1f0;🇵🇰1F1F5-1F1F0nosingle::🇵🇰single1🇵🇰
4519146&amp;#x1f387;🎇1F387nosingle::🎇single1🎇
4529147&amp;#x1f969;🥩1F969nosingle::🥩single1🥩
4539148&amp;#x1f41e;🐞1F41Enosingle::🐞single1🐞
45491493&amp;#65039;&amp;#8419;3️⃣33-FE0F-20E3nosingle::3️⃣single13️⃣
4559150&amp;#11013;&amp;#65039;⬅️2B05-FE0Fnosingle::⬅️single1⬅️
4569151&amp;#x1f310;🌐1F310nosingle::🌐single1🌐
4579152&amp;#8599;&amp;#65039;↗️2197-FE0Fnosingle::↗️single1↗️
4589153&amp;#x1f37d;&amp;#65039;🍽️1F37D-FE0Fnosingle::🍽️single1🍽️
4599154&amp;#x1f9c0;🧀1F9C0nosingle::🧀single1🧀
4609155&amp;#x1f966;🥦1F966nosingle::🥦single1🥦
4619156&amp;#x1f41c;🐜1F41Cnosingle::🐜single1🐜
4629157&amp;#9876;&amp;#65039;⚔️2694-FE0Fnosingle::⚔️single1⚔️
463101&amp;#x1f63a;😺1F63Anosingle::😺single1😺
464102&amp;#x1f95e;🥞1F95Enosingle::🥞single1🥞
465103&amp;#x1f3c4;🏄1F3C4nosingle::🏄single1🏄
466104&amp;#x1f528;🔨1F528nosingle::🔨single1🔨
467105&amp;#x1f3dd;&amp;#65039;🏝️1F3DD-FE0Fnosingle::🏝️single1🏝️
468106&amp;#x1f506;🔆1F506nosingle::🔆single1🔆
469107&amp;#x1f465;👥1F465nosingle::👥single1👥
470108&amp;#x1f453;👓1F453nosingle::👓single1👓
471109&amp;#x1f952;🥒1F952nosingle::🥒single1🥒
4721010&amp;#x1f3c8;🏈1F3C8nosingle::🏈single1🏈
4731011&amp;#x1f1f5;&amp;#x1f1ed;🇵🇭1F1F5-1F1EDnosingle::🇵🇭single1🇵🇭
4741012&amp;#x1f3cb;&amp;#65039;🏋️1F3CB-FE0Fnosingle::🏋️single1🏋️
47510130&amp;#65039;&amp;#8419;0️⃣30-FE0F-20E3nosingle::0️⃣single10️⃣
4761014&amp;#x1f698;🚘1F698nosingle::🚘single1🚘
4771015&amp;#x1f996;🦖1F996nosingle::🦖single1🦖
4781016&amp;#x1f315;🌕1F315nosingle::🌕single1🌕
4791017&amp;#x1f3ad;🎭1F3ADnosingle::🎭single1🎭
4801018&amp;#x1f47e;👾1F47Enosingle::👾single1👾
4811019&amp;#x1f373;🍳1F373nosingle::🍳single1🍳
4821020&amp;#x1f3f5;&amp;#65039;🏵️1F3F5-FE0Fnosingle::🏵️single1🏵️
4831021&amp;#x1f367;🍧1F367nosingle::🍧single1🍧
4841022&amp;#x1f517;🔗1F517nosingle::🔗single1🔗
4851023&amp;#x1f54b;🕋1F54Bnosingle::🕋single1🕋
4861024&amp;#9731;&amp;#65039;☃️2603-FE0Fnosingle::☃️single1☃️
4871025&amp;#x1f305;🌅1F305nosingle::🌅single1🌅
4881026&amp;#x1f934;🤴1F934nosingle::🤴single1🤴
4891027&amp;#x1f596;🖖1F596nosingle::🖖single1🖖
4901028&amp;#x1f40a;🐊1F40Anosingle::🐊single1🐊
4911029&amp;#x1f418;🐘1F418nosingle::🐘single1🐘
4921030&amp;#x1f324;&amp;#65039;🌤️1F324-FE0Fnosingle::🌤️single1🌤️
4931031&amp;#x1f951;🥑1F951nosingle::🥑single1🥑
4941032&amp;#x1f95a;🥚1F95Anosingle::🥚single1🥚
4951033&amp;#9928;&amp;#65039;⛈️26C8-FE0Fnosingle::⛈️single1⛈️
4961034&amp;#x1f435;🐵1F435nosingle::🐵single1🐵
4971035&amp;#x1f51c;🔜1F51Cnosingle::🔜single1🔜
4981036&amp;#x1f376;🍶1F376nosingle::🍶single1🍶
4991037&amp;#x1f404;🐄1F404nosingle::🐄single1🐄
5001038&amp;#x1f1fb;&amp;#x1f1ea;🇻🇪1F1FB-1F1EAnosingle::🇻🇪single1🇻🇪
5011039&amp;#x1f42e;🐮1F42Enosingle::🐮single1🐮
5021040&amp;#x1f988;🦈1F988nosingle::🦈single1🦈
5031041&amp;#x1f6b2;🚲1F6B2nosingle::🚲single1🚲
5041042&amp;#9940;26D4nosingle::⛔single1
5051043&amp;#x1f56f;&amp;#65039;🕯️1F56F-FE0Fnosingle::🕯️single1🕯️
5061044&amp;#10133;2795nosingle::➕single1
5071045&amp;#x1f53a;🔺1F53Anosingle::🔺single1🔺
5081046&amp;#x1f487;💇1F487nosingle::💇single1💇
5091047&amp;#x1f9e0;🧠1F9E0nosingle::🧠single1🧠
5101048&amp;#x1f4fb;📻1F4FBnosingle::📻single1📻
5111049&amp;#x1f964;🥤1F964nosingle::🥤single1🥤
5121050&amp;#x1f35d;🍝1F35Dnosingle::🍝single1🍝
5131051&amp;#x1f365;🍥1F365nosingle::🍥single1🍥
5141052&amp;#x1f4b4;💴1F4B4nosingle::💴single1💴
5151053&amp;#x1f32c;&amp;#65039;🌬️1F32C-FE0Fnosingle::🌬️single1🌬️
5161054&amp;#x1f953;🥓1F953nosingle::🥓single1🥓
5171055&amp;#x1f64d;🙍1F64Dnosingle::🙍single1🙍
5181056&amp;#9875;2693nosingle::⚓single1
5191057&amp;#x1f470;👰1F470nosingle::👰single1👰
5201058&amp;#x1f402;🐂1F402nosingle::🐂single1🐂
5211059&amp;#x1f4fd;&amp;#65039;📽️1F4FD-FE0Fnosingle::📽️single1📽️
5221060&amp;#x1f3c5;🏅1F3C5nosingle::🏅single1🏅
5231061&amp;#9925;26C5nosingle::⛅single1
5241062&amp;#x1f1e6;&amp;#x1f1ea;🇦🇪1F1E6-1F1EAnosingle::🇦🇪single1🇦🇪
5251063&amp;#x1f1f5;&amp;#x1f1ea;🇵🇪1F1F5-1F1EAnosingle::🇵🇪single1🇵🇪
5261064&amp;#x1f9dc;🧜1F9DCnosingle::🧜single1🧜
5271065&amp;#x1f4ee;📮1F4EEnosingle::📮single1📮
5281066&amp;#9971;26F3nosingle::⛳single1
5291067&amp;#x1f53d;🔽1F53Dnosingle::🔽single1🔽
5301068&amp;#x1f682;🚂1F682nosingle::🚂single1🚂
5311069&amp;#x1f3cc;&amp;#65039;🏌️1F3CC-FE0Fnosingle::🏌️single1🏌️
5321070&amp;#x1f407;🐇1F407nosingle::🐇single1🐇
5331071&amp;#x1f3cd;&amp;#65039;🏍️1F3CD-FE0Fnosingle::🏍️single1🏍️
5341072&amp;#x1f3b2;🎲1F3B2nosingle::🎲single1🎲
5351073&amp;#x1f95b;🥛1F95Bnosingle::🥛single1🥛
5361074&amp;#x1f3a3;🎣1F3A3nosingle::🎣single1🎣
5371075&amp;#x1f471;👱1F471nosingle::👱single1👱
5381076&amp;#x1f38f;🎏1F38Fnosingle::🎏single1🎏
5391077&amp;#x1f577;&amp;#65039;🕷️1F577-FE0Fnosingle::🕷️single1🕷️
5401078&amp;#x1f98d;🦍1F98Dnosingle::🦍single1🦍
5411079&amp;#x1f518;🔘1F518nosingle::🔘single1🔘
5421080&amp;#x1f405;🐅1F405nosingle::🐅single1🐅
5431081&amp;#x1f3c7;🏇1F3C7nosingle::🏇single1🏇
5441082&amp;#x1f510;🔐1F510nosingle::🔐single1🔐
5451083&amp;#x1f3e9;🏩1F3E9nosingle::🏩single1🏩
5461084&amp;#x1f47a;👺1F47Anosingle::👺single1👺
5471085&amp;#x1f171;&amp;#65039;🅱️1F171-FE0Fnosingle::🅱️single1🅱️
5481086&amp;#x1f699;🚙1F699nosingle::🚙single1🚙
5491087&amp;#x1f427;🐧1F427nosingle::🐧single1🐧
5501088&amp;#9878;&amp;#65039;⚖️2696-FE0Fnosingle::⚖️single1⚖️
5511089&amp;#x1f383;🎃1F383nosingle::🎃single1🎃
5521090&amp;#x1f304;🌄1F304nosingle::🌄single1🌄
5531091&amp;#x1f3be;🎾1F3BEnosingle::🎾single1🎾
5541092&amp;#x1f41a;🐚1F41Anosingle::🐚single1🐚
5551093&amp;#x1f3ba;🎺1F3BAnosingle::🎺single1🎺
5561094&amp;#10055;&amp;#65039;❇️2747-FE0Fnosingle::❇️single1❇️
5571095&amp;#x1f3ab;🎫1F3ABnosingle::🎫single1🎫
5581096&amp;#8986;231Anosingle::⌚single1
5591097&amp;#x1f30b;🌋1F30Bnosingle::🌋single1🌋
5601098&amp;#x1f492;💒1F492nosingle::💒single1💒
5611099&amp;#x1f473;👳1F473nosingle::👳single1👳
56210100&amp;#10062;274Enosingle::❎single1
56310101&amp;#x1f45f;👟1F45Fnosingle::👟single1👟
56410102&amp;#x1f443;👃1F443nosingle::👃single1👃
56510103&amp;#x1f6cc;🛌1F6CCnosingle::🛌single1🛌
56610104&amp;#x1f693;🚓1F693nosingle::🚓single1🚓
56710105&amp;#9196;23ECnosingle::⏬single1
56810106&amp;#x1f4c8;📈1F4C8nosingle::📈single1📈
56910107&amp;#9924;26C4nosingle::⛄single1
57010108&amp;#9201;&amp;#65039;⏱️23F1-FE0Fnosingle::⏱️single1⏱️
57110109&amp;#x1f63e;😾1F63Enosingle::😾single1😾
57210110&amp;#x1f6eb;🛫1F6EBnosingle::🛫single1🛫
57310111&amp;#x1f931;🤱1F931nosingle::🤱single1🤱
57410112&amp;#x1f350;🍐1F350nosingle::🍐single1🍐
57510113&amp;#9774;&amp;#65039;☮️262E-FE0Fnosingle::☮️single1☮️
57610114&amp;#x1f683;🚃1F683nosingle::🚃single1🚃
57710115&amp;#9203;23F3nosingle::⏳single1
57810116&amp;#x1f31c;🌜1F31Cnosingle::🌜single1🌜
57910117&amp;#x1f4f9;📹1F4F9nosingle::📹single1📹
58010118&amp;#x1f41b;🐛1F41Bnosingle::🐛single1🐛
58110119&amp;#x1f454;👔1F454nosingle::👔single1👔
58210120&amp;#x1f457;👗1F457nosingle::👗single1👗
58310121&amp;#x1f40c;🐌1F40Cnosingle::🐌single1🐌
58410122&amp;#x1f3b1;🎱1F3B1nosingle::🎱single1🎱
58510123&amp;#x1f330;🌰1F330nosingle::🌰single1🌰
58610124&amp;#x1f32e;🌮1F32Enosingle::🌮single1🌮
58710125&amp;#x1f575;&amp;#65039;🕵️1F575-FE0Fnosingle::🕵️single1🕵️
58810126&amp;#x1f505;🔅1F505nosingle::🔅single1🔅
58910127&amp;#9993;&amp;#65039;✉️2709-FE0Fnosingle::✉️single1✉️
59010128&amp;#x1f1ea;&amp;#x1f1ec;🇪🇬1F1EA-1F1ECnosingle::🇪🇬single1🇪🇬
59110129&amp;#x1f691;🚑1F691nosingle::🚑single1🚑
59210130&amp;#x1f4e6;📦1F4E6nosingle::📦single1📦
59310131&amp;#x1f925;🤥1F925nosingle::🤥single1🤥
59410132&amp;#x1f504;🔄1F504nosingle::🔄single1🔄
59510133&amp;#x1f933;🤳1F933nosingle::🤳single1🤳
59610134&amp;#x1f4b2;💲1F4B2nosingle::💲single1💲
59710135&amp;#x1f38b;🎋1F38Bnosingle::🎋single1🎋
59810136&amp;#x1f5d3;&amp;#65039;🗓️1F5D3-FE0Fnosingle::🗓️single1🗓️
59910137&amp;#x1f916;🤖1F916nosingle::🤖single1🤖
60010138&amp;#x1f954;🥔1F954nosingle::🥔single1🥔
60110139&amp;#x1f197;🆗1F197nosingle::🆗single1🆗
60210140&amp;#x1f511;🔑1F511nosingle::🔑single1🔑
60310141&amp;#x1f1e8;&amp;#x1f1f3;🇨🇳1F1E8-1F1F3nosingle::🇨🇳single1🇨🇳
60410142&amp;#x1f424;🐤1F424nosingle::🐤single1🐤
605101434&amp;#65039;&amp;#8419;4️⃣34-FE0F-20E3nosingle::4️⃣single14️⃣
60610144&amp;#x1f411;🐑1F411nosingle::🐑single1🐑
60710145&amp;#10160;27B0nosingle::➰single1
60810146&amp;#x1f469;&amp;zwj;&amp;#x1f393;👩‍🎓1F469-200D-1F393nosingle::👩‍🎓single1👩‍🎓
60910147&amp;#9730;&amp;#65039;☂️2602-FE0Fnosingle::☂️single1☂️
61010148&amp;#x1f1e6;&amp;#x1f1f9;🇦🇹1F1E6-1F1F9nosingle::🇦🇹single1🇦🇹
61110149&amp;#x1f986;🦆1F986nosingle::🦆single1🦆
61210150&amp;#x1f68c;🚌1F68Cnosingle::🚌single1🚌
61310151&amp;#x1f4bf;💿1F4BFnosingle::💿single1💿
61410152&amp;#x1f3e5;🏥1F3E5nosingle::🏥single1🏥
61510153&amp;#x1f40b;🐋1F40Bnosingle::🐋single1🐋
61610154&amp;#x1f692;🚒1F692nosingle::🚒single1🚒
61710155&amp;#x1f3d0;🏐1F3D0nosingle::🏐single1🏐
61810156&amp;#x1f1ea;&amp;#x1f1e8;🇪🇨1F1EA-1F1E8nosingle::🇪🇨single1🇪🇨
61910157&amp;#x1f950;🥐1F950nosingle::🥐single1🥐
62010158&amp;#x1f3b7;🎷1F3B7nosingle::🎷single1🎷
62110159&amp;#x1f5fd;🗽1F5FDnosingle::🗽single1🗽
62210160&amp;#x1f5e1;&amp;#65039;🗡️1F5E1-FE0Fnosingle::🗡️single1🗡️
62310161&amp;#x1f3cf;🏏1F3CFnosingle::🏏single1🏏
62410162&amp;#x1f42d;🐭1F42Dnosingle::🐭single1🐭
62510163&amp;#x1f64e;🙎1F64Enosingle::🙎single1🙎
62610164&amp;#x1f311;🌑1F311nosingle::🌑single1🌑
62710165&amp;#x1f694;🚔1F694nosingle::🚔single1🚔
62810166&amp;#x1f1ee;&amp;#x1f1e9;🇮🇩1F1EE-1F1E9nosingle::🇮🇩single1🇮🇩
62910167&amp;#x1f6bf;🚿1F6BFnosingle::🚿single1🚿
63010168&amp;#x1f95d;🥝1F95Dnosingle::🥝single1🥝
63110169&amp;#x1f54c;🕌1F54Cnosingle::🕌single1🕌
63210170&amp;#x1f400;🐀1F400nosingle::🐀single1🐀
63310171&amp;#x1f6e1;&amp;#65039;🛡️1F6E1-FE0Fnosingle::🛡️single1🛡️
63410172&amp;#x1f512;🔒1F512nosingle::🔒single1🔒
63510173&amp;#10035;&amp;#65039;✳️2733-FE0Fnosingle::✳️single1✳️
63610174&amp;#x1f576;&amp;#65039;🕶️1F576-FE0Fnosingle::🕶️single1🕶️
63710175&amp;#x1f469;&amp;zwj;&amp;#10084;&amp;#65039;&amp;zwj;&amp;#x1f48b;&amp;zwj;&amp;#x1f469;👩‍❤️‍💋‍👩1F469-200D-2764-FE0F-200D-1F48B-200D-1F469nosingle::👩‍❤️‍💋‍👩single1👩‍❤️‍💋‍👩
63810176&amp;#x1f39f;&amp;#65039;🎟️1F39F-FE0Fnosingle::🎟️single1🎟️
63910177&amp;#x1f409;🐉1F409nosingle::🐉single1🐉
64010178&amp;#x1f531;🔱1F531nosingle::🔱single1🔱
64110179&amp;#x1f50e;🔎1F50Enosingle::🔎single1🔎
64210180&amp;#x1f1e6;&amp;#x1f1fa;🇦🇺1F1E6-1F1FAnosingle::🇦🇺single1🇦🇺
64310181&amp;#9904;&amp;#65039;⚰️26B0-FE0Fnosingle::⚰️single1⚰️
64410182&amp;#x1f429;🐩1F429nosingle::🐩single1🐩
64510183&amp;#x1f991;🦑1F991nosingle::🦑single1🦑
64610184&amp;#x1f9df;🧟1F9DFnosingle::🧟single1🧟
64710185&amp;#x1f195;🆕1F195nosingle::🆕single1🆕
64810186&amp;#x1f98a;🦊1F98Anosingle::🦊single1🦊
64910187&amp;#x1f455;👕1F455nosingle::👕single1👕
65010188&amp;#x1f3f9;🏹1F3F9nosingle::🏹single1🏹
65110189&amp;#x1f1e9;&amp;#x1f1ff;🇩🇿1F1E9-1F1FFnosingle::🇩🇿single1🇩🇿
65210190&amp;#x1f46c;👬1F46Cnosingle::👬single1👬
65310191&amp;#x1f371;🍱1F371nosingle::🍱single1🍱
65410192&amp;#x1f4f0;📰1F4F0nosingle::📰single1📰
65510193&amp;#x1f94b;🥋1F94Bnosingle::🥋single1🥋
65610194&amp;#x1f6a4;🚤1F6A4nosingle::🚤single1🚤
65710195&amp;#x1f3f0;🏰1F3F0nosingle::🏰single1🏰
658101965&amp;#65039;&amp;#8419;5️⃣35-FE0F-20E3nosingle::5️⃣single15️⃣
65910197&amp;#x1f989;🦉1F989nosingle::🦉single1🦉
66010198&amp;#x1f6a2;🚢1F6A2nosingle::🚢single1🚢
66110199&amp;#x1f328;&amp;#65039;🌨️1F328-FE0Fnosingle::🌨️single1🌨️
66210200&amp;#x1f4c6;📆1F4C6nosingle::📆single1📆
66310201&amp;#x1f5dd;&amp;#65039;🗝️1F5DD-FE0Fnosingle::🗝️single1🗝️
66410202&amp;#x1f38c;🎌1F38Cnosingle::🎌single1🎌
66510203&amp;#x1f9d4;🧔1F9D4nosingle::🧔single1🧔
66610204&amp;#x1f4b3;💳1F4B3nosingle::💳single1💳
66710205&amp;#x1f1fa;&amp;#x1f1fe;🇺🇾1F1FA-1F1FEnosingle::🇺🇾single1🇺🇾
66810206&amp;#x1f957;🥗1F957nosingle::🥗single1🥗
66910207&amp;#9775;&amp;#65039;☯️262F-FE0Fnosingle::☯️single1☯️
67010208&amp;#9881;&amp;#65039;⚙️2699-FE0Fnosingle::⚙️single1⚙️
67110209&amp;#x1f4b6;💶1F4B6nosingle::💶single1💶
67210210&amp;#9961;&amp;#65039;⛩️26E9-FE0Fnosingle::⛩️single1⛩️
67310211&amp;#x1f5fb;🗻1F5FBnosingle::🗻single1🗻
67410212&amp;#10002;&amp;#65039;✒️2712-FE0Fnosingle::✒️single1✒️
67510213&amp;#x1f1fa;&amp;#x1f1f2;🇺🇲1F1FA-1F1F2nosingle::🇺🇲single1🇺🇲
67610214&amp;#x1f1f5;&amp;#x1f1f9;🇵🇹1F1F5-1F1F9nosingle::🇵🇹single1🇵🇹
67710215&amp;#x1f360;🍠1F360nosingle::🍠single1🍠
678111&amp;#x1f477;👷1F477nosingle::👷single1👷
679112&amp;#x1f6cd;&amp;#65039;🛍️1F6CD-FE0Fnosingle::🛍️single1🛍️
680113&amp;#x1f3ce;&amp;#65039;🏎️1F3CE-FE0Fnosingle::🏎️single1🏎️
681114&amp;#x1f51b;🔛1F51Bnosingle::🔛single1🔛
682115&amp;#x1f519;🔙1F519nosingle::🔙single1🔙
683116&amp;#x1f5fc;🗼1F5FCnosingle::🗼single1🗼
684117&amp;#x1f396;&amp;#65039;🎖️1F396-FE0Fnosingle::🎖️single1🎖️
685118&amp;#x1f6ba;🚺1F6BAnosingle::🚺single1🚺
686119&amp;#x1f439;🐹1F439nosingle::🐹single1🐹
6871110&amp;#x1f956;🥖1F956nosingle::🥖single1🥖
6881111&amp;#x1f1f5;&amp;#x1f1f7;🇵🇷1F1F5-1F1F7nosingle::🇵🇷single1🇵🇷
6891112&amp;#x1f3a1;🎡1F3A1nosingle::🎡single1🎡
6901113&amp;#x1f50d;🔍1F50Dnosingle::🔍single1🔍
6911114&amp;#x1f3df;&amp;#65039;🏟️1F3DF-FE0Fnosingle::🏟️single1🏟️
6921115&amp;#x1f5a5;&amp;#65039;🖥️1F5A5-FE0Fnosingle::🖥️single1🖥️
6931116&amp;#x1f32d;🌭1F32Dnosingle::🌭single1🌭
6941117&amp;#x1f1f9;&amp;#x1f1ed;🇹🇭1F1F9-1F1EDnosingle::🇹🇭single1🇹🇭
6951118&amp;#10017;&amp;#65039;✡️2721-FE0Fnosingle::✡️single1✡️
6961119&amp;#x1f3d2;🏒1F3D2nosingle::🏒single1🏒
6971120&amp;#x1f6c0;🛀1F6C0nosingle::🛀single1🛀
6981121&amp;#x1f4c5;📅1F4C5nosingle::📅single1📅
6991122&amp;#x1f58b;&amp;#65039;🖋️1F58B-FE0Fnosingle::🖋️single1🖋️
7001123&amp;#x1f1f8;&amp;#x1f1ea;🇸🇪1F1F8-1F1EAnosingle::🇸🇪single1🇸🇪
7011124&amp;#x1f39e;&amp;#65039;🎞️1F39E-FE0Fnosingle::🎞️single1🎞️
7021125&amp;#x1f3aa;🎪1F3AAnosingle::🎪single1🎪
7031126&amp;#x1f359;🍙1F359nosingle::🍙single1🍙
7041127&amp;#x1f6c1;🛁1F6C1nosingle::🛁single1🛁
7051128&amp;#x1f4d5;📕1F4D5nosingle::📕single1📕
7061129&amp;#x1f452;👒1F452nosingle::👒single1👒
7071130&amp;#x1f587;&amp;#65039;🖇️1F587-FE0Fnosingle::🖇️single1🖇️
7081131&amp;#10068;2754nosingle::❔single1
7091132&amp;#x1f384;🎄1F384nosingle::🎄single1🎄
7101133&amp;#x1f578;&amp;#65039;🕸️1F578-FE0Fnosingle::🕸️single1🕸️
7111134&amp;#x1f95c;🥜1F95Cnosingle::🥜single1🥜
7121135&amp;#x1f574;&amp;#65039;🕴️1F574-FE0Fnosingle::🕴️single1🕴️
7131136&amp;#x1f95f;🥟1F95Fnosingle::🥟single1🥟
7141137&amp;#8601;&amp;#65039;↙️2199-FE0Fnosingle::↙️single1↙️
7151138&amp;#9726;25FEnosingle::◾single1
7161139&amp;#x1f432;🐲1F432nosingle::🐲single1🐲
7171140&amp;#x1f3f4;🏴1F3F4nosingle::🏴single1🏴
7181141&amp;#x1f98c;🦌1F98Cnosingle::🦌single1🦌
7191142&amp;#x1f194;🆔1F194nosingle::🆔single1🆔
7201143&amp;#x1f45b;👛1F45Bnosingle::👛single1👛
7211144&amp;#x1f69a;🚚1F69Anosingle::🚚single1🚚
7221145&amp;#8618;&amp;#65039;↪️21AA-FE0Fnosingle::↪️single1↪️
7231146&amp;#x1f1ec;&amp;#x1f1f7;🇬🇷1F1EC-1F1F7nosingle::🇬🇷single1🇬🇷
7241147&amp;#x1f4ff;📿1F4FFnosingle::📿single1📿
7251148&amp;#x1f32b;&amp;#65039;🌫️1F32B-FE0Fnosingle::🌫️single1🌫️
7261149&amp;#x1f302;🌂1F302nosingle::🌂single1🌂
7271150&amp;#x1f549;&amp;#65039;🕉️1F549-FE0Fnosingle::🕉️single1🕉️
7281151&amp;#x1f19a;🆚1F19Anosingle::🆚single1🆚
7291152&amp;#x1f4c9;📉1F4C9nosingle::📉single1📉
7301153&amp;#x1f5e8;&amp;#65039;🗨️1F5E8-FE0Fnosingle::🗨️single1🗨️
7311154&amp;#x1f6d2;🛒1F6D2nosingle::🛒single1🛒
7321155&amp;#x1f4db;📛1F4DBnosingle::📛single1📛
7331156&amp;#x1f1f3;&amp;#x1f1f1;🇳🇱1F1F3-1F1F1nosingle::🇳🇱single1🇳🇱
7341157&amp;#x1f43f;&amp;#65039;🐿️1F43F-FE0Fnosingle::🐿️single1🐿️
7351158&amp;#x1f17e;&amp;#65039;🅾️1F17E-FE0Fnosingle::🅾️single1🅾️
7361159&amp;#9762;&amp;#65039;☢️2622-FE0Fnosingle::☢️single1☢️
7371160&amp;#x1f1f2;&amp;#x1f1e6;🇲🇦1F1F2-1F1E6nosingle::🇲🇦single1🇲🇦
7381161&amp;#x1f403;🐃1F403nosingle::🐃single1🐃
7391162&amp;#9981;26FDnosingle::⛽single1
7401163&amp;#x1f17f;&amp;#65039;🅿️1F17F-FE0Fnosingle::🅿️single1🅿️
7411164&amp;#x1f987;🦇1F987nosingle::🦇single1🦇
7421165&amp;#10548;&amp;#65039;⤴️2934-FE0Fnosingle::⤴️single1⤴️
7431166&amp;#9973;26F5nosingle::⛵single1
7441167&amp;#x1f45e;👞1F45Enosingle::👞single1👞
7451168&amp;#x1f513;🔓1F513nosingle::🔓single1🔓
7461169&amp;#x1f9de;🧞1F9DEnosingle::🧞single1🧞
7471170&amp;#9968;&amp;#65039;⛰️26F0-FE0Fnosingle::⛰️single1⛰️
7481171&amp;#x1f4ce;📎1F4CEnosingle::📎single1📎
7491172&amp;#x1f98e;🦎1F98Enosingle::🦎single1🦎
7501173&amp;#x1f50c;🔌1F50Cnosingle::🔌single1🔌
7511174&amp;#x1f3f3;&amp;#65039;🏳️1F3F3-FE0Fnosingle::🏳️single1🏳️
7521175&amp;#x1f6bd;🚽1F6BDnosingle::🚽single1🚽
7531176&amp;#x1f372;🍲1F372nosingle::🍲single1🍲
7541177&amp;#9874;&amp;#65039;⚒️2692-FE0Fnosingle::⚒️single1⚒️
7551178&amp;#9802;264Anosingle::♊single1
7561179&amp;#x1f462;👢1F462nosingle::👢single1👢
7571180&amp;#x1f9d9;🧙1F9D9nosingle::🧙single1🧙
7581181&amp;#x1f1f5;&amp;#x1f1f1;🇵🇱1F1F5-1F1F1nosingle::🇵🇱single1🇵🇱
7591182&amp;#9962;26EAnosingle::⛪single1
7601183&amp;#x1f1e7;&amp;#x1f1ea;🇧🇪1F1E7-1F1EAnosingle::🇧🇪single1🇧🇪
7611184&amp;#x1f38e;🎎1F38Enosingle::🎎single1🎎
7621185&amp;#x1f516;🔖1F516nosingle::🔖single1🔖
7631186&amp;#x1f50b;🔋1F50Bnosingle::🔋single1🔋
7641187&amp;#x1f4e1;📡1F4E1nosingle::📡single1📡
7651188&amp;#x1f307;🌇1F307nosingle::🌇single1🌇
7661189&amp;#x1f3c9;🏉1F3C9nosingle::🏉single1🏉
7671190&amp;#x1f46a;👪1F46Anosingle::👪single1👪
7681191&amp;#x1f456;👖1F456nosingle::👖single1👖
7691192&amp;#x1f4ca;📊1F4CAnosingle::📊single1📊
7701193&amp;#x1f406;🐆1F406nosingle::🐆single1🐆
7711194&amp;#x1f501;🔁1F501nosingle::🔁single1🔁
7721195&amp;#x1f6e9;&amp;#65039;🛩️1F6E9-FE0Fnosingle::🛩️single1🛩️
7731196&amp;#x1f421;🐡1F421nosingle::🐡single1🐡
7741197&amp;#x1f0cf;🃏1F0CFnosingle::🃏single1🃏
7751198&amp;#x1f469;&amp;zwj;&amp;#x1f4bb;👩‍💻1F469-200D-1F4BBnosingle::👩‍💻single1👩‍💻
7761199&amp;#x1f329;&amp;#65039;🌩️1F329-FE0Fnosingle::🌩️single1🌩️
77711100&amp;#x1f1f3;&amp;#x1f1ec;🇳🇬1F1F3-1F1ECnosingle::🇳🇬single1🇳🇬
77811101&amp;#x1f40f;🐏1F40Fnosingle::🐏single1🐏
779111026&amp;#65039;&amp;#8419;6️⃣36-FE0F-20E3nosingle::6️⃣single16️⃣
78011103&amp;#9977;&amp;#65039;⛹️26F9-FE0Fnosingle::⛹️single1⛹️
78111104&amp;#x1f5ef;&amp;#65039;🗯️1F5EF-FE0Fnosingle::🗯️single1🗯️
78211105&amp;#x1f1e8;&amp;#x1f1ed;🇨🇭1F1E8-1F1EDnosingle::🇨🇭single1🇨🇭
78311106&amp;#x1f362;🍢1F362nosingle::🍢single1🍢
78411107&amp;#x1f5fa;&amp;#65039;🗺️1F5FA-FE0Fnosingle::🗺️single1🗺️
78511108&amp;#x1f695;🚕1F695nosingle::🚕single1🚕
78611109&amp;#x1f1f7;&amp;#x1f1fa;🇷🇺1F1F7-1F1FAnosingle::🇷🇺single1🇷🇺
78711110&amp;#9410;&amp;#65039;Ⓜ️24C2-FE0Fnosingle::Ⓜ️single1Ⓜ️
78811111&amp;#9969;&amp;#65039;⛱️26F1-FE0Fnosingle::⛱️single1⛱️
78911112&amp;#x1f3eb;🏫1F3EBnosingle::🏫single1🏫
79011113&amp;#x1f4c0;📀1F4C0nosingle::📀single1📀
79111114&amp;#x1f199;🆙1F199nosingle::🆙single1🆙
79211115&amp;#x1f93c;🤼1F93Cnosingle::🤼single1🤼
79311116&amp;#x1f428;🐨1F428nosingle::🐨single1🐨
79411117&amp;#x1f982;🦂1F982nosingle::🦂single1🦂
79511118&amp;#x1f4b7;💷1F4B7nosingle::💷single1💷
79611119&amp;#x1f948;🥈1F948nosingle::🥈single1🥈
79711120&amp;#x1f468;&amp;zwj;&amp;#x1f4bb;👨‍💻1F468-200D-1F4BBnosingle::👨‍💻single1👨‍💻
79811121&amp;#x1f6cf;&amp;#65039;🛏️1F6CF-FE0Fnosingle::🛏️single1🛏️
79911122&amp;#9724;&amp;#65039;◼️25FC-FE0Fnosingle::◼️single1◼️
80011123&amp;#x1f684;🚄1F684nosingle::🚄single1🚄
80111124&amp;#x1f9d6;🧖1F9D6nosingle::🧖single1🧖
80211125&amp;#x1f3a0;🎠1F3A0nosingle::🎠single1🎠
80311126&amp;#x1f401;🐁1F401nosingle::🐁single1🐁
80411127&amp;#x1f5f3;&amp;#65039;🗳️1F5F3-FE0Fnosingle::🗳️single1🗳️
80511128&amp;#x1f3ee;🏮1F3EEnosingle::🏮single1🏮
80611129&amp;#x1f96a;🥪1F96Anosingle::🥪single1🥪
80711130&amp;#x1f325;&amp;#65039;🌥️1F325-FE0Fnosingle::🌥️single1🌥️
80811131&amp;#x1f6aa;🚪1F6AAnosingle::🚪single1🚪
80911132&amp;#12953;&amp;#65039;㊙️3299-FE0Fnosingle::㊙️single1㊙️
81011133&amp;#x1f5de;&amp;#65039;🗞️1F5DE-FE0Fnosingle::🗞️single1🗞️
81111134&amp;#x1f468;&amp;zwj;&amp;#9877;&amp;#65039;👨‍⚕️1F468-200D-2695-FE0Fnosingle::👨‍⚕️single1👨‍⚕️
81211135&amp;#x1f58a;&amp;#65039;🖊️1F58A-FE0Fnosingle::🖊️single1🖊️
81311136&amp;#11035;2B1Bnosingle::⬛single1
81411137&amp;#x1f192;🆒1F192nosingle::🆒single1🆒
81511138&amp;#x1f1e8;&amp;#x1f1fa;🇨🇺1F1E8-1F1FAnosingle::🇨🇺single1🇨🇺
81611139&amp;#x1f967;🥧1F967nosingle::🥧single1🥧
81711140&amp;#x1f469;&amp;zwj;&amp;#9877;&amp;#65039;👩‍⚕️1F469-200D-2695-FE0Fnosingle::👩‍⚕️single1👩‍⚕️
81811141&amp;#x1f1f3;&amp;#x1f1ee;🇳🇮1F1F3-1F1EEnosingle::🇳🇮single1🇳🇮
81911142&amp;#x1f468;&amp;zwj;&amp;#x1f393;👨‍🎓1F468-200D-1F393nosingle::👨‍🎓single1👨‍🎓
82011143&amp;#x1f6a7;🚧1F6A7nosingle::🚧single1🚧
82111144&amp;#9939;&amp;#65039;⛓️26D3-FE0Fnosingle::⛓️single1⛓️
82211145&amp;#x1f6f5;🛵1F6F5nosingle::🛵single1🛵
82311146&amp;#x1f945;🥅1F945nosingle::🥅single1🥅
82411147&amp;#x1f685;🚅1F685nosingle::🚅single1🚅
82511148&amp;#x1f5bc;&amp;#65039;🖼️1F5BC-FE0Fnosingle::🖼️single1🖼️
82611149&amp;#9855;267Fnosingle::♿single1
82711150&amp;#x1f4cf;📏1F4CFnosingle::📏single1📏
82811151&amp;#x1f004;🀄1F004nosingle::🀄single1🀄
82911152&amp;#x1f58c;&amp;#65039;🖌️1F58C-FE0Fnosingle::🖌️single1🖌️
83011153&amp;#x1f250;🉐1F250nosingle::🉐single1🉐
83111154&amp;#x1f4ec;📬1F4ECnosingle::📬single1📬
83211155&amp;#x1f681;🚁1F681nosingle::🚁single1🚁
83311156&amp;#x1f5d1;&amp;#65039;🗑️1F5D1-FE0Fnosingle::🗑️single1🗑️
83411157&amp;#x1f935;🤵1F935nosingle::🤵single1🤵
83511158&amp;#x1f3f7;&amp;#65039;🏷️1F3F7-FE0Fnosingle::🏷️single1🏷️
83611159&amp;#x1f321;&amp;#65039;🌡️1F321-FE0Fnosingle::🌡️single1🌡️
83711160&amp;#x1f390;🎐1F390nosingle::🎐single1🎐
83811161&amp;#x1f1ef;&amp;#x1f1f2;🇯🇲1F1EF-1F1F2nosingle::🇯🇲single1🇯🇲
83911162&amp;#x1f992;🦒1F992nosingle::🦒single1🦒
84011163&amp;#x1f392;🎒1F392nosingle::🎒single1🎒
84111164&amp;#x1f1f5;&amp;#x1f1fe;🇵🇾1F1F5-1F1FEnosingle::🇵🇾single1🇵🇾
84211165&amp;#x1f1fa;&amp;#x1f1e6;🇺🇦1F1FA-1F1E6nosingle::🇺🇦single1🇺🇦
84311166&amp;#x1f1f2;&amp;#x1f1e8;🇲🇨1F1F2-1F1E8nosingle::🇲🇨single1🇲🇨
84411167&amp;#x1f949;🥉1F949nosingle::🥉single1🥉
845111687&amp;#65039;&amp;#8419;7️⃣37-FE0F-20E3nosingle::7️⃣single17️⃣
84611169&amp;#8987;231Bnosingle::⌛single1
84711170&amp;#x1f312;🌒1F312nosingle::🌒single1🌒
84811171&amp;#x1f965;🥥1F965nosingle::🥥single1🥥
84911172&amp;#x1f385;🎅1F385nosingle::🎅single1🎅
85011173&amp;#x1f9d5;🧕1F9D5nosingle::🧕single1🧕
85111174&amp;#x1f6d0;🛐1F6D0nosingle::🛐single1🛐
85211175&amp;#9803;264Bnosingle::♋single1
85311176&amp;#x1f9d1;🧑1F9D1nosingle::🧑single1🧑
85411177&amp;#x1f1e9;&amp;#x1f1f4;🇩🇴1F1E9-1F1F4nosingle::🇩🇴single1🇩🇴
85511178&amp;#x1f1ee;&amp;#x1f1ea;🇮🇪1F1EE-1F1EAnosingle::🇮🇪single1🇮🇪
85611179&amp;#x1f6f8;🛸1F6F8nosingle::🛸single1🛸
85711180&amp;#x1f9d2;🧒1F9D2nosingle::🧒single1🧒
85811181&amp;#x1f4e7;📧1F4E7nosingle::📧single1📧
85911182&amp;#x1f962;🥢1F962nosingle::🥢single1🥢
86011183&amp;#x1f3a2;🎢1F3A2nosingle::🎢single1🎢
86111184&amp;#x1f42a;🐪1F42Anosingle::🐪single1🐪
86211185&amp;#x1f1ee;&amp;#x1f1f1;🇮🇱1F1EE-1F1F1nosingle::🇮🇱single1🇮🇱
86311186&amp;#x1f468;&amp;zwj;&amp;#x1f373;👨‍🍳1F468-200D-1F373nosingle::👨‍🍳single1👨‍🍳
86411187&amp;#x1f69b;🚛1F69Bnosingle::🚛single1🚛
86511188&amp;#x1f944;🥄1F944nosingle::🥄single1🥄
86611189&amp;#x1f939;🤹1F939nosingle::🤹single1🤹
86711190&amp;#x1f3e2;🏢1F3E2nosingle::🏢single1🏢
86811191&amp;#x1f488;💈1F488nosingle::💈single1💈
86911192&amp;#x1f458;👘1F458nosingle::👘single1👘
87011193&amp;#x1f1f3;&amp;#x1f1f4;🇳🇴1F1F3-1F1F4nosingle::🇳🇴single1🇳🇴
87111194&amp;#x1f1ec;&amp;#x1f1ed;🇬🇭1F1EC-1F1EDnosingle::🇬🇭single1🇬🇭
87211195&amp;#x1f3d4;&amp;#65039;🏔️1F3D4-FE0Fnosingle::🏔️single1🏔️
87311196&amp;#12349;&amp;#65039;〽️303D-FE0Fnosingle::〽️single1〽️
87411197&amp;#x1f6a6;🚦1F6A6nosingle::🚦single1🚦
87511198&amp;#x1f1ff;&amp;#x1f1e6;🇿🇦1F1FF-1F1E6nosingle::🇿🇦single1🇿🇦
87611199&amp;#x1f1f9;&amp;#x1f1f3;🇹🇳1F1F9-1F1F3nosingle::🇹🇳single1🇹🇳
87711200&amp;#x1f4bc;💼1F4BCnosingle::💼single1💼
87811201&amp;#9800;2648nosingle::♈single1
87911202&amp;#x1f993;🦓1F993nosingle::🦓single1🦓
88011203&amp;#x1f6bb;🚻1F6BBnosingle::🚻single1🚻
88111204&amp;#x1f3b0;🎰1F3B0nosingle::🎰single1🎰
88211205&amp;#9725;25FDnosingle::◽single1
88311206&amp;#x1f997;🦗1F997nosingle::🦗single1🦗
88411207&amp;#x1f93e;🤾1F93Enosingle::🤾single1🤾
88511208&amp;#x1f45a;👚1F45Anosingle::👚single1👚
88611209&amp;#x1f461;👡1F461nosingle::👡single1👡
88711210&amp;#9935;&amp;#65039;⛏️26CF-FE0Fnosingle::⛏️single1⛏️
888112119&amp;#65039;&amp;#8419;9️⃣39-FE0F-20E3nosingle::9️⃣single19️⃣
88911212&amp;#x1f6ce;&amp;#65039;🛎️1F6CE-FE0Fnosingle::🛎️single1🛎️
89011213&amp;#x1f417;🐗1F417nosingle::🐗single1🐗
89111214&amp;#x1f326;&amp;#65039;🌦️1F326-FE0Fnosingle::🌦️single1🌦️
89211215&amp;#x1f1f8;&amp;#x1f1e6;🇸🇦1F1F8-1F1E6nosingle::🇸🇦single1🇸🇦
89311216&amp;#x1f6b9;🚹1F6B9nosingle::🚹single1🚹
89411217&amp;#x1f1fe;&amp;#x1f1ea;🇾🇪1F1FE-1F1EAnosingle::🇾🇪single1🇾🇪
89511218&amp;#9770;&amp;#65039;☪️262A-FE0Fnosingle::☪️single1☪️
89611219&amp;#x1f93a;🤺1F93Anosingle::🤺single1🤺
89711220&amp;#x1f6a3;🚣1F6A3nosingle::🚣single1🚣
89811221&amp;#x1f5ff;🗿1F5FFnosingle::🗿single1🗿
89911222&amp;#x1f958;🥘1F958nosingle::🥘single1🥘
90011223&amp;#x1f1e9;&amp;#x1f1f0;🇩🇰1F1E9-1F1F0nosingle::🇩🇰single1🇩🇰
90111224&amp;#x1f995;🦕1F995nosingle::🦕single1🦕
90211225&amp;#x1f1f1;&amp;#x1f1f7;🇱🇷1F1F1-1F1F7nosingle::🇱🇷single1🇱🇷
90311226&amp;#x1f3d5;&amp;#65039;🏕️1F3D5-FE0Fnosingle::🏕️single1🏕️
90411227&amp;#9763;&amp;#65039;☣️2623-FE0Fnosingle::☣️single1☣️
90511228&amp;#x1f306;🌆1F306nosingle::🌆single1🌆
90611229&amp;#8597;&amp;#65039;↕️2195-FE0Fnosingle::↕️single1↕️
90711230&amp;#x1f4e8;📨1F4E8nosingle::📨single1📨
90811231&amp;#x1f3e8;🏨1F3E8nosingle::🏨single1🏨
90911232&amp;#x1f579;&amp;#65039;🕹️1F579-FE0Fnosingle::🕹️single1🕹️
91011233&amp;#x1f469;&amp;zwj;&amp;#x1f373;👩‍🍳1F469-200D-1F373nosingle::👩‍🍳single1👩‍🍳
91111234&amp;#x1f1e8;&amp;#x1f1ee;🇨🇮1F1E8-1F1EEnosingle::🇨🇮single1🇨🇮
912121&amp;#x1f4ef;📯1F4EFnosingle::📯single1📯
913122&amp;#x1f45c;👜1F45Cnosingle::👜single1👜
914123&amp;#x1f3d8;&amp;#65039;🏘️1F3D8-FE0Fnosingle::🏘️single1🏘️
915124&amp;#x1f42b;🐫1F42Bnosingle::🐫single1🐫
916125&amp;#9978;26FAnosingle::⛺single1
917126&amp;#x1f469;&amp;zwj;&amp;#x1f3eb;👩‍🏫1F469-200D-1F3EBnosingle::👩‍🏫single1👩‍🏫
918127&amp;#x1f573;&amp;#65039;🕳️1F573-FE0Fnosingle::🕳️single1🕳️
919128&amp;#x1f3ef;🏯1F3EFnosingle::🏯single1🏯
920129&amp;#x1f313;🌓1F313nosingle::🌓single1🌓
9211210&amp;#x1f309;🌉1F309nosingle::🌉single1🌉
92212118&amp;#65039;&amp;#8419;8️⃣38-FE0F-20E3nosingle::8️⃣single18️⃣
9231212&amp;#x1f482;💂1F482nosingle::💂single1💂
9241213&amp;#x1f509;🔉1F509nosingle::🔉single1🔉
9251214&amp;#x1f468;&amp;zwj;&amp;#x1f469;&amp;zwj;&amp;#x1f467;&amp;zwj;&amp;#x1f466;👨‍👩‍👧‍👦1F468-200D-1F469-200D-1F467-200D-1F466nosingle::👨‍👩‍👧‍👦single1👨‍👩‍👧‍👦
9261215&amp;#x1f1ea;&amp;#x1f1fa;🇪🇺1F1EA-1F1FAnosingle::🇪🇺single1🇪🇺
9271216&amp;#x1f69c;🚜1F69Cnosingle::🚜single1🚜
9281217&amp;#x1f515;🔕1F515nosingle::🔕single1🔕
9291218&amp;#x1f4e5;📥1F4E5nosingle::📥single1📥
9301219&amp;#x1f314;🌔1F314nosingle::🌔single1🌔
9311220&amp;#x1f3d9;&amp;#65039;🏙️1F3D9-FE0Fnosingle::🏙️single1🏙️
9321221&amp;#x1f968;🥨1F968nosingle::🥨single1🥨
9331222&amp;#x1f1f2;&amp;#x1f1fe;🇲🇾1F1F2-1F1FEnosingle::🇲🇾single1🇲🇾
9341223&amp;#x1f4d8;📘1F4D8nosingle::📘single1📘
9351224&amp;#x1f4d7;📗1F4D7nosingle::📗single1📗
9361225&amp;#x1f4dc;📜1F4DCnosingle::📜single1📜
9371226&amp;#9807;264Fnosingle::♏single1
9381227&amp;#x1f1e7;&amp;#x1f1f4;🇧🇴1F1E7-1F1F4nosingle::🇧🇴single1🇧🇴
9391228&amp;#x1f3dc;&amp;#65039;🏜️1F3DC-FE0Fnosingle::🏜️single1🏜️
9401229&amp;#x1f358;🍘1F358nosingle::🍘single1🍘
9411230&amp;#x1f6ec;🛬1F6ECnosingle::🛬single1🛬
9421231&amp;#x1f32f;🌯1F32Fnosingle::🌯single1🌯
9431232&amp;#x1f3d7;&amp;#65039;🏗️1F3D7-FE0Fnosingle::🏗️single1🏗️
9441233&amp;#x1f3f4;&amp;#xe0067;&amp;#xe0062;&amp;#xe0065;&amp;#xe006e;&amp;#xe0067;&amp;#xe007f;🏴󠁧󠁢󠁥󠁮󠁧󠁿1F3F4-E0067-E0062-E0065-E006E-E0067-E007Fnosingle::🏴󠁧󠁢󠁥󠁮󠁧󠁿single1🏴󠁧󠁢󠁥󠁮󠁧󠁿
9451234&amp;#x1f6bc;🚼1F6BCnosingle::🚼single1🚼
9461235&amp;#x1f3de;&amp;#65039;🏞️1F3DE-FE0Fnosingle::🏞️single1🏞️
9471236&amp;#x1f508;🔈1F508nosingle::🔈single1🔈
9481237&amp;#x1f570;&amp;#65039;🕰️1F570-FE0Fnosingle::🕰️single1🕰️
9491238&amp;#x1f3d3;🏓1F3D3nosingle::🏓single1🏓
9501239&amp;#x1f1e8;&amp;#x1f1f7;🇨🇷1F1E8-1F1F7nosingle::🇨🇷single1🇨🇷
9511240&amp;#x1f468;&amp;zwj;&amp;#x1f33e;👨‍🌾1F468-200D-1F33Enosingle::👨‍🌾single1👨‍🌾
9521241&amp;#x1f983;🦃1F983nosingle::🦃single1🦃
9531242&amp;#x1f526;🔦1F526nosingle::🔦single1🔦
9541243&amp;#x1f503;🔃1F503nosingle::🔃single1🔃
9551244&amp;#x1f1f8;&amp;#x1f1fb;🇸🇻1F1F8-1F1FBnosingle::🇸🇻single1🇸🇻
9561245&amp;#x1f6f3;&amp;#65039;🛳️1F6F3-FE0Fnosingle::🛳️single1🛳️
9571246&amp;#x1f318;🌘1F318nosingle::🌘single1🌘
9581247&amp;#x1f1e7;&amp;#x1f1e9;🇧🇩1F1E7-1F1E9nosingle::🇧🇩single1🇧🇩
9591248&amp;#x1f9db;🧛1F9DBnosingle::🧛single1🧛
9601249&amp;#x1f469;&amp;zwj;&amp;#10084;&amp;#65039;&amp;zwj;&amp;#x1f469;👩‍❤️‍👩1F469-200D-2764-FE0F-200D-1F469nosingle::👩‍❤️‍👩single1👩‍❤️‍👩
9611250&amp;#x1f51a;🔚1F51Anosingle::🔚single1🔚
9621251&amp;#x1f51f;🔟1F51Fnosingle::🔟single1🔟
9631252&amp;#8505;&amp;#65039;ℹ️2139-FE0Fnosingle::ℹ️single1ℹ️
9641253&amp;#x1f6e2;&amp;#65039;🛢️1F6E2-FE0Fnosingle::🛢️single1🛢️
9651254&amp;#x1f3f4;&amp;#xe0067;&amp;#xe0062;&amp;#xe0073;&amp;#xe0063;&amp;#xe0074;&amp;#xe007f;🏴󠁧󠁢󠁳󠁣󠁴󠁿1F3F4-E0067-E0062-E0073-E0063-E0074-E007Fnosingle::🏴󠁧󠁢󠁳󠁣󠁴󠁿single1🏴󠁧󠁢󠁳󠁣󠁴󠁿
9661255&amp;#x1f1f8;&amp;#x1f1e9;🇸🇩1F1F8-1F1E9nosingle::🇸🇩single1🇸🇩
9671256&amp;#x1f52d;🔭1F52Dnosingle::🔭single1🔭
9681257&amp;#x1f1ee;&amp;#x1f1f6;🇮🇶1F1EE-1F1F6nosingle::🇮🇶single1🇮🇶
9691258&amp;#x1f38d;🎍1F38Dnosingle::🎍single1🎍
9701259&amp;#x1f963;🥣1F963nosingle::🥣single1🥣
9711260&amp;#x1f317;🌗1F317nosingle::🌗single1🌗
9721261&amp;#8598;&amp;#65039;↖️2196-FE0Fnosingle::↖️single1↖️
9731262&amp;#x1f3b3;🎳1F3B3nosingle::🎳single1🎳
9741263&amp;#x1f468;&amp;zwj;&amp;#10084;&amp;#65039;&amp;zwj;&amp;#x1f48b;&amp;zwj;&amp;#x1f468;👨‍❤️‍💋‍👨1F468-200D-2764-FE0F-200D-1F48B-200D-1F468nosingle::👨‍❤️‍💋‍👨single1👨‍❤️‍💋‍👨
9751264&amp;#x1f468;&amp;zwj;&amp;#x1f3eb;👨‍🏫1F468-200D-1F3EBnosingle::👨‍🏫single1👨‍🏫
9761265&amp;#x1f4c4;📄1F4C4nosingle::📄single1📄
9771266&amp;#x1f1f6;&amp;#x1f1e6;🇶🇦1F1F6-1F1E6nosingle::🇶🇦single1🇶🇦
9781267&amp;#x1f468;&amp;zwj;&amp;#x1f469;&amp;zwj;&amp;#x1f467;👨‍👩‍👧1F468-200D-1F469-200D-1F467nosingle::👨‍👩‍👧single1👨‍👩‍👧
9791268&amp;#x1f39a;&amp;#65039;🎚️1F39A-FE0Fnosingle::🎚️single1🎚️
9801269&amp;#x1f6e0;&amp;#65039;🛠️1F6E0-FE0Fnosingle::🛠️single1🛠️
9811270&amp;#x1f193;🆓1F193nosingle::🆓single1🆓
9821271&amp;#x1f1f5;&amp;#x1f1f8;🇵🇸1F1F5-1F1F8nosingle::🇵🇸single1🇵🇸
9831272&amp;#x1f3db;&amp;#65039;🏛️1F3DB-FE0Fnosingle::🏛️single1🏛️
9841273&amp;#9808;2650nosingle::♐single1
9851274&amp;#x1f9e6;🧦1F9E6nosingle::🧦single1🧦
9861275&amp;#x1f5fe;🗾1F5FEnosingle::🗾single1🗾
9871276&amp;#x1f469;&amp;zwj;&amp;#x1f33e;👩‍🌾1F469-200D-1F33Enosingle::👩‍🌾single1👩‍🌾
9881277&amp;#x1f391;🎑1F391nosingle::🎑single1🎑
9891278&amp;#x1f1ec;&amp;#x1f1f9;🇬🇹1F1EC-1F1F9nosingle::🇬🇹single1🇬🇹
9901279&amp;#x1f9e4;🧤1F9E4nosingle::🧤single1🧤
9911280&amp;#x1f9dd;🧝1F9DDnosingle::🧝single1🧝
9921281&amp;#10175;27BFnosingle::➿single1
9931282&amp;#x1f1fb;&amp;#x1f1f3;🇻🇳1F1FB-1F1F3nosingle::🇻🇳single1🇻🇳
9941283&amp;#x1f4d2;📒1F4D2nosingle::📒single1📒
9951284&amp;#9801;2649nosingle::♉single1
9961285&amp;#9811;2653nosingle::♓single1
9971286&amp;#x1f6ad;🚭1F6ADnosingle::🚭single1🚭
9981287&amp;#x1f527;🔧1F527nosingle::🔧single1🔧
9991288&amp;#9804;264Cnosingle::♌single1
10001289&amp;#8617;&amp;#65039;↩️21A9-FE0Fnosingle::↩️single1↩️
10011290&amp;#9810;2652nosingle::♒single1
10021291&amp;#x1f3e6;🏦1F3E6nosingle::🏦single1🏦
10031292&amp;#x1f235;🈵1F235nosingle::🈵single1🈵
10041293&amp;#x1f96b;🥫1F96Bnosingle::🥫single1🥫
10051294&amp;#x1f316;🌖1F316nosingle::🌖single1🌖
10061295&amp;#x1f6f4;🛴1F6F4nosingle::🛴single1🛴
10071296&amp;#x1f696;🚖1F696nosingle::🚖single1🚖
10081297&amp;#x1f959;🥙1F959nosingle::🥙single1🥙
10091298&amp;#x1f6f0;&amp;#65039;🛰️1F6F0-FE0Fnosingle::🛰️single1🛰️
10101299&amp;#x1f98f;🦏1F98Fnosingle::🦏single1🦏
101112100&amp;#x1f1f8;&amp;#x1f1ec;🇸🇬1F1F8-1F1ECnosingle::🇸🇬single1🇸🇬
101212101&amp;#9210;&amp;#65039;⏺️23FA-FE0Fnosingle::⏺️single1⏺️
101312102&amp;#x1f58d;&amp;#65039;🖍️1F58D-FE0Fnosingle::🖍️single1🖍️
101412103&amp;#9976;&amp;#65039;⛸️26F8-FE0Fnosingle::⛸️single1⛸️
101512104&amp;#x1f4d9;📙1F4D9nosingle::📙single1📙
101612105&amp;#x1f472;👲1F472nosingle::👲single1👲
101712106&amp;#x1f3c2;🏂1F3C2nosingle::🏂single1🏂
101812107*&amp;#65039;&amp;#8419;*️⃣2A-FE0F-20E3nosingle::*️⃣single1*️⃣
101912108&amp;#x1f532;🔲1F532nosingle::🔲single1🔲
102012109&amp;#x1f1ed;&amp;#x1f1fa;🇭🇺1F1ED-1F1FAnosingle::🇭🇺single1🇭🇺
102112110&amp;#x1f690;🚐1F690nosingle::🚐single1🚐
102212111&amp;#x1f468;&amp;zwj;&amp;#10084;&amp;#65039;&amp;zwj;&amp;#x1f468;👨‍❤️‍👨1F468-200D-2764-FE0F-200D-1F468nosingle::👨‍❤️‍👨single1👨‍❤️‍👨
102312112&amp;#x1f507;🔇1F507nosingle::🔇single1🔇
102412113&amp;#x1f994;🦔1F994nosingle::🦔single1🦔
102512114&amp;#x1f52f;🔯1F52Fnosingle::🔯single1🔯
102612115&amp;#x1f6b5;🚵1F6B5nosingle::🚵single1🚵
102712116&amp;#x1f6af;🚯1F6AFnosingle::🚯single1🚯
102812117&amp;#x1f55b;🕛1F55Bnosingle::🕛single1🕛
102912118&amp;#x1f4fc;📼1F4FCnosingle::📼single1📼
103012119&amp;#x1f9d3;🧓1F9D3nosingle::🧓single1🧓
103112120&amp;#x1f39b;&amp;#65039;🎛️1F39B-FE0Fnosingle::🎛️single1🎛️
103212121&amp;#x1f1e8;&amp;#x1f1ff;🇨🇿1F1E8-1F1FFnosingle::🇨🇿single1🇨🇿
103312122&amp;#x1f1ed;&amp;#x1f1f0;🇭🇰1F1ED-1F1F0nosingle::🇭🇰single1🇭🇰
103412123&amp;#x1f4ba;💺1F4BAnosingle::💺single1💺
103512124&amp;#x1f469;&amp;zwj;&amp;#x1f3a4;👩‍🎤1F469-200D-1F3A4nosingle::👩‍🎤single1👩‍🎤
103612125&amp;#9970;26F2nosingle::⛲single1
103712126&amp;#9883;&amp;#65039;⚛️269B-FE0Fnosingle::⚛️single1⚛️
103812127&amp;#x1f552;🕒1F552nosingle::🕒single1🕒
103912128&amp;#x1f4f6;📶1F4F6nosingle::📶single1📶
104012129&amp;#x1f68b;🚋1F68Bnosingle::🚋single1🚋
104112130&amp;#x1f1f1;&amp;#x1f1e7;🇱🇧1F1F1-1F1E7nosingle::🇱🇧single1🇱🇧
104212131&amp;#x1f4e0;📠1F4E0nosingle::📠single1📠
104312132&amp;#x1f687;🚇1F687nosingle::🚇single1🚇
104412133&amp;#9202;&amp;#65039;⏲️23F2-FE0Fnosingle::⏲️single1⏲️
104512134&amp;#9194;23EAnosingle::⏪single1
104612135&amp;#x1f1f0;&amp;#x1f1fc;🇰🇼1F1F0-1F1FCnosingle::🇰🇼single1🇰🇼
104712136&amp;#x1f6c3;🛃1F6C3nosingle::🛃single1🛃
104812137&amp;#x1f1ed;&amp;#x1f1f3;🇭🇳1F1ED-1F1F3nosingle::🇭🇳single1🇭🇳
104912138&amp;#x1f468;&amp;zwj;&amp;#9992;&amp;#65039;👨‍✈️1F468-200D-2708-FE0Fnosingle::👨‍✈️single1👨‍✈️
105012139&amp;#x1f4d3;📓1F4D3nosingle::📓single1📓
105112140&amp;#x1f469;&amp;zwj;&amp;#x1f467;👩‍👧1F469-200D-1F467nosingle::👩‍👧single1👩‍👧
105212141&amp;#9806;264Enosingle::♎single1
105312142&amp;#x1f3d1;🏑1F3D1nosingle::🏑single1🏑
105412143&amp;#x1f4cb;📋1F4CBnosingle::📋single1📋
105512144&amp;#x1f53c;🔼1F53Cnosingle::🔼single1🔼
105612145&amp;#x1f1e8;&amp;#x1f1f5;🇨🇵1F1E8-1F1F5nosingle::🇨🇵single1🇨🇵
105712146&amp;#x1f1ef;&amp;#x1f1f4;🇯🇴1F1EF-1F1F4nosingle::🇯🇴single1🇯🇴
105812147&amp;#x1f4c3;📃1F4C3nosingle::📃single1📃
105912148&amp;#x1f52c;🔬1F52Cnosingle::🔬single1🔬
106012149&amp;#x1f1eb;&amp;#x1f1ee;🇫🇮1F1EB-1F1EEnosingle::🇫🇮single1🇫🇮
106112150&amp;#x1f6a5;🚥1F6A5nosingle::🚥single1🚥
106212151&amp;#x1f1f1;&amp;#x1f1f0;🇱🇰1F1F1-1F1F0nosingle::🇱🇰single1🇱🇰
106312152&amp;#9805;264Dnosingle::♍single1
106412153&amp;#x1f1f5;&amp;#x1f1e6;🇵🇦1F1F5-1F1E6nosingle::🇵🇦single1🇵🇦
106512154&amp;#x1f1f8;&amp;#x1f1fe;🇸🇾1F1F8-1F1FEnosingle::🇸🇾single1🇸🇾
106612155&amp;#x1f6be;🚾1F6BEnosingle::🚾single1🚾
106712156&amp;#x1f1ed;&amp;#x1f1f7;🇭🇷1F1ED-1F1F7nosingle::🇭🇷single1🇭🇷
106812157&amp;#x1f1f3;&amp;#x1f1ff;🇳🇿1F1F3-1F1FFnosingle::🇳🇿single1🇳🇿
106912158&amp;#x1f68d;🚍1F68Dnosingle::🚍single1🚍
107012159&amp;#x1f3f8;🏸1F3F8nosingle::🏸single1🏸
107112160&amp;#x1f533;🔳1F533nosingle::🔳single1🔳
107212161&amp;#x1f6c2;🛂1F6C2nosingle::🛂single1🛂
107312162&amp;#x1f4f5;📵1F4F5nosingle::📵single1📵
107412163&amp;#9937;&amp;#65039;⛑️26D1-FE0Fnosingle::⛑️single1⛑️
107512164&amp;#x1f3da;&amp;#65039;🏚️1F3DA-FE0Fnosingle::🏚️single1🏚️
107612165&amp;#x1f3bd;🎽1F3BDnosingle::🎽single1🎽
107712166&amp;#x1f6b0;🚰1F6B0nosingle::🚰single1🚰
107812167&amp;#x1f3ea;🏪1F3EAnosingle::🏪single1🏪
107912168&amp;#9975;&amp;#65039;⛷️26F7-FE0Fnosingle::⛷️single1⛷️
108012169&amp;#x1f469;&amp;zwj;&amp;#x1f4bc;👩‍💼1F469-200D-1F4BCnosingle::👩‍💼single1👩‍💼
108112170&amp;#9809;2651nosingle::♑single1
108212171&amp;#x1f1e8;&amp;#x1f1f2;🇨🇲1F1E8-1F1F2nosingle::🇨🇲single1🇨🇲
108312172&amp;#x1f1f1;&amp;#x1f1fe;🇱🇾1F1F1-1F1FEnosingle::🇱🇾single1🇱🇾
108412173&amp;#9195;23EBnosingle::⏫single1
108512174&amp;#x1f3e3;🏣1F3E3nosingle::🏣single1🏣
108612175&amp;#x1f689;🚉1F689nosingle::🚉single1🚉
108712176&amp;#x1f6c5;🛅1F6C5nosingle::🛅single1🛅
108812177&amp;#9877;&amp;#65039;⚕️2695-FE0Fnosingle::⚕️single1⚕️
108912178&amp;#9972;&amp;#65039;⛴️26F4-FE0Fnosingle::⛴️single1⛴️
109012179&amp;#x1f441;&amp;#65039;&amp;zwj;&amp;#x1f5e8;&amp;#65039;👁️‍🗨️1F441-FE0F-200D-1F5E8-FE0Fnosingle::👁️‍🗨️single1👁️‍🗨️
109112180&amp;#x1f3e7;🏧1F3E7nosingle::🏧single1🏧
109212181&amp;#9723;&amp;#65039;◻️25FB-FE0Fnosingle::◻️single1◻️
109312182&amp;#x1f469;&amp;zwj;&amp;#x1f52c;👩‍🔬1F469-200D-1F52Cnosingle::👩‍🔬single1👩‍🔬
109412183&amp;#9209;&amp;#65039;⏹️23F9-FE0Fnosingle::⏹️single1⏹️
109512184&amp;#x1f1f7;&amp;#x1f1f4;🇷🇴1F1F7-1F1F4nosingle::🇷🇴single1🇷🇴
109612185&amp;#x1f469;&amp;zwj;&amp;#x1f466;👩‍👦1F469-200D-1F466nosingle::👩‍👦single1👩‍👦
109712186&amp;#11036;2B1Cnosingle::⬜single1
1098131#&amp;#65039;&amp;#8419;#️⃣23-FE0F-20E3nosingle::#️⃣single1#️⃣
1099132&amp;#x1f1e6;&amp;#x1f1f1;🇦🇱1F1E6-1F1F1nosingle::🇦🇱single1🇦🇱
1100133&amp;#x1f1f9;&amp;#x1f1fc;🇹🇼1F1F9-1F1FCnosingle::🇹🇼single1🇹🇼
1101134&amp;#x1f6e3;&amp;#65039;🛣️1F6E3-FE0Fnosingle::🛣️single1🛣️
1102135&amp;#x1f3e4;🏤1F3E4nosingle::🏤single1🏤
1103136&amp;#x1f6c4;🛄1F6C4nosingle::🛄single1🛄
1104137&amp;#x1f1ee;&amp;#x1f1f8;🇮🇸1F1EE-1F1F8nosingle::🇮🇸single1🇮🇸
1105138&amp;#x1f3ec;🏬1F3ECnosingle::🏬single1🏬
1106139&amp;#x1f468;&amp;zwj;&amp;#x1f3a4;👨‍🎤1F468-200D-1F3A4nosingle::👨‍🎤single1👨‍🎤
11071310&amp;#x1f550;🕐1F550nosingle::🕐single1🕐
11081311&amp;#x1f3a6;🎦1F3A6nosingle::🎦single1🎦
11091312&amp;#x1f468;&amp;zwj;&amp;#x1f4bc;👨‍💼1F468-200D-1F4BCnosingle::👨‍💼single1👨‍💼
11101313&amp;#x1f4b9;💹1F4B9nosingle::💹single1💹
11111314&amp;#9784;&amp;#65039;☸️2638-FE0Fnosingle::☸️single1☸️
11121315&amp;#x1f4f4;📴1F4F4nosingle::📴single1📴
11131316&amp;#x1f4d0;📐1F4D0nosingle::📐single1📐
11141317&amp;#x1f5b1;&amp;#65039;🖱️1F5B1-FE0Fnosingle::🖱️single1🖱️
11151318&amp;#x1f1ed;&amp;#x1f1f9;🇭🇹1F1ED-1F1F9nosingle::🇭🇹single1🇭🇹
11161319&amp;#x1f1f8;&amp;#x1f1f3;🇸🇳1F1F8-1F1F3nosingle::🇸🇳single1🇸🇳
11171320&amp;#x1f529;🔩1F529nosingle::🔩single1🔩
11181321&amp;#x1f232;🈲1F232nosingle::🈲single1🈲
11191322&amp;#x1f3b4;🎴1F3B4nosingle::🎴single1🎴
11201323&amp;#x1f4f3;📳1F4F3nosingle::📳single1📳
11211324&amp;#9766;&amp;#65039;☦️2626-FE0Fnosingle::☦️single1☦️
11221325&amp;#x1f686;🚆1F686nosingle::🚆single1🚆
11231326&amp;#x1f4c1;📁1F4C1nosingle::📁single1📁
11241327&amp;#x1f4d4;📔1F4D4nosingle::📔single1📔
11251328&amp;#x1f1e6;&amp;#x1f1eb;🇦🇫1F1E6-1F1EBnosingle::🇦🇫single1🇦🇫
11261329&amp;#x1f469;&amp;zwj;&amp;#x1f3a8;👩‍🎨1F469-200D-1F3A8nosingle::👩‍🎨single1👩‍🎨
11271330&amp;#x1f551;🕑1F551nosingle::🕑single1🕑
11281331&amp;#x1f6b8;🚸1F6B8nosingle::🚸single1🚸
11291332&amp;#x1f9e3;🧣1F9E3nosingle::🧣single1🧣
11301333&amp;#x1f468;&amp;zwj;&amp;#x1f680;👨‍🚀1F468-200D-1F680nosingle::👨‍🚀single1👨‍🚀
11311334&amp;#x1f1e7;&amp;#x1f1ed;🇧🇭1F1E7-1F1EDnosingle::🇧🇭single1🇧🇭
11321335&amp;#x1f3ed;🏭1F3EDnosingle::🏭single1🏭
11331336&amp;#x1f3fa;🏺1F3FAnosingle::🏺single1🏺
11341337&amp;#x1f1ec;&amp;#x1f1f3;🇬🇳1F1EC-1F1F3nosingle::🇬🇳single1🇬🇳
11351338&amp;#x1f93d;🤽1F93Dnosingle::🤽single1🤽
11361339&amp;#x1f6e5;&amp;#65039;🛥️1F6E5-FE0Fnosingle::🛥️single1🛥️
11371340&amp;#x1f1f2;&amp;#x1f1f1;🇲🇱1F1F2-1F1F1nosingle::🇲🇱single1🇲🇱
11381341&amp;#x1f301;🌁1F301nosingle::🌁single1🌁
11391342&amp;#x1f936;🤶1F936nosingle::🤶single1🤶
11401343&amp;#x1f1e6;&amp;#x1f1ff;🇦🇿1F1E6-1F1FFnosingle::🇦🇿single1🇦🇿
11411344&amp;#x1f4bd;💽1F4BDnosingle::💽single1💽
11421345&amp;#x1f1e6;&amp;#x1f1f4;🇦🇴1F1E6-1F1F4nosingle::🇦🇴single1🇦🇴
11431346&amp;#x1f68e;🚎1F68Enosingle::🚎single1🚎
11441347&amp;#x1f1ee;&amp;#x1f1f7;🇮🇷1F1EE-1F1F7nosingle::🇮🇷single1🇮🇷
11451348&amp;#x1f1f3;&amp;#x1f1f5;🇳🇵1F1F3-1F1F5nosingle::🇳🇵single1🇳🇵
11461349&amp;#x1f5d2;&amp;#65039;🗒️1F5D2-FE0Fnosingle::🗒️single1🗒️
11471350&amp;#x1f1e8;&amp;#x1f1e9;🇨🇩1F1E8-1F1E9nosingle::🇨🇩single1🇨🇩
11481351&amp;#x1f6cb;&amp;#65039;🛋️1F6CB-FE0Fnosingle::🛋️single1🛋️
11491352&amp;#9000;&amp;#65039;⌨️2328-FE0Fnosingle::⌨️single1⌨️
11501353&amp;#x1f18e;🆎1F18Enosingle::🆎single1🆎
11511354&amp;#x1f9d7;🧗1F9D7nosingle::🧗single1🧗
11521355&amp;#x1f9e5;🧥1F9E5nosingle::🧥single1🧥
11531356&amp;#x1f1f0;&amp;#x1f1ea;🇰🇪1F1F0-1F1EAnosingle::🇰🇪single1🇰🇪
11541357&amp;#x1f6f6;🛶1F6F6nosingle::🛶single1🛶
11551358&amp;#x1f4d1;📑1F4D1nosingle::📑single1📑
11561359&amp;#x1f45d;👝1F45Dnosingle::👝single1👝
11571360&amp;#x1f1e6;&amp;#x1f1f2;🇦🇲1F1E6-1F1F2nosingle::🇦🇲single1🇦🇲
11581361&amp;#9199;&amp;#65039;⏯️23EF-FE0Fnosingle::⏯️single1⏯️
11591362&amp;#x1f1ec;&amp;#x1f1ea;🇬🇪1F1EC-1F1EAnosingle::🇬🇪single1🇬🇪
11601363&amp;#x1f4b1;💱1F4B1nosingle::💱single1💱
11611364&amp;#x1f468;&amp;zwj;&amp;#x1f3a8;👨‍🎨1F468-200D-1F3A8nosingle::👨‍🎨single1👨‍🎨
11621365&amp;#x1f1e7;&amp;#x1f1ec;🇧🇬1F1E7-1F1ECnosingle::🇧🇬single1🇧🇬
11631366&amp;#x1f1ea;&amp;#x1f1e6;🇪🇦1F1EA-1F1E6nosingle::🇪🇦single1🇪🇦
11641367&amp;#x1f469;&amp;zwj;&amp;#9878;&amp;#65039;👩‍⚖️1F469-200D-2696-FE0Fnosingle::👩‍⚖️single1👩‍⚖️
11651368&amp;#x1f553;🕓1F553nosingle::🕓single1🕓
11661369&amp;#x1f469;&amp;zwj;&amp;#x1f680;👩‍🚀1F469-200D-1F680nosingle::👩‍🚀single1👩‍🚀
11671370&amp;#x1f69e;🚞1F69Enosingle::🚞single1🚞
11681371&amp;#x1f468;&amp;zwj;&amp;#x1f692;👨‍🚒1F468-200D-1F692nosingle::👨‍🚒single1👨‍🚒
11691372&amp;#x1f196;🆖1F196nosingle::🆖single1🆖
11701373&amp;#x1f500;🔀1F500nosingle::🔀single1🔀
11711374&amp;#x1f468;&amp;zwj;&amp;#x1f52c;👨‍🔬1F468-200D-1F52Cnosingle::👨‍🔬single1👨‍🔬
11721375&amp;#x1f468;&amp;zwj;&amp;#9878;&amp;#65039;👨‍⚖️1F468-200D-2696-FE0Fnosingle::👨‍⚖️single1👨‍⚖️
11731376&amp;#x1f468;&amp;zwj;&amp;#x1f469;&amp;zwj;&amp;#x1f466;&amp;zwj;&amp;#x1f466;👨‍👩‍👦‍👦1F468-200D-1F469-200D-1F466-200D-1F466nosingle::👨‍👩‍👦‍👦single1👨‍👩‍👦‍👦
11741377&amp;#x1f3f4;&amp;#xe0067;&amp;#xe0062;&amp;#xe0077;&amp;#xe006c;&amp;#xe0073;&amp;#xe007f;🏴󠁧󠁢󠁷󠁬󠁳󠁿1F3F4-E0067-E0062-E0077-E006C-E0073-E007Fnosingle::🏴󠁧󠁢󠁷󠁬󠁳󠁿single1🏴󠁧󠁢󠁷󠁬󠁳󠁿
11751378&amp;#x1f960;🥠1F960nosingle::🥠single1🥠
11761379&amp;#x1f6b3;🚳1F6B3nosingle::🚳single1🚳
11771380&amp;#x1f1e7;&amp;#x1f1ef;🇧🇯1F1E7-1F1EFnosingle::🇧🇯single1🇧🇯
11781381&amp;#x1f1f2;&amp;#x1f1ec;🇲🇬1F1F2-1F1ECnosingle::🇲🇬single1🇲🇬
11791382&amp;#x1f1ea;&amp;#x1f1ea;🇪🇪1F1EA-1F1EAnosingle::🇪🇪single1🇪🇪
11801383&amp;#9197;&amp;#65039;⏭️23ED-FE0Fnosingle::⏭️single1⏭️
11811384&amp;#x1f54e;🕎1F54Enosingle::🕎single1🕎
11821385&amp;#x1f21a;🈚1F21Anosingle::🈚single1🈚
11831386&amp;#x1f6a1;🚡1F6A1nosingle::🚡single1🚡
11841387&amp;#x1f555;🕕1F555nosingle::🕕single1🕕
11851388&amp;#x1f1f7;&amp;#x1f1f8;🇷🇸1F1F7-1F1F8nosingle::🇷🇸single1🇷🇸
11861389&amp;#x1f251;🉑1F251nosingle::🉑single1🉑
11871390&amp;#x1f4c2;📂1F4C2nosingle::📂single1📂
11881391&amp;#x1f1f9;&amp;#x1f1ff;🇹🇿1F1F9-1F1FFnosingle::🇹🇿single1🇹🇿
11891392&amp;#x1f4eb;📫1F4EBnosingle::📫single1📫
11901393&amp;#x1f558;🕘1F558nosingle::🕘single1🕘
11911394&amp;#x1f68a;🚊1F68Anosingle::🚊single1🚊
11921395&amp;#x1f688;🚈1F688nosingle::🚈single1🚈
11931396&amp;#x1f502;🔂1F502nosingle::🔂single1🔂
11941397&amp;#10135;2797nosingle::➗single1
11951398&amp;#x1f557;🕗1F557nosingle::🕗single1🕗
11961399&amp;#x1f468;&amp;zwj;&amp;#x1f469;&amp;zwj;&amp;#x1f467;&amp;zwj;&amp;#x1f467;👨‍👩‍👧‍👧1F468-200D-1F469-200D-1F467-200D-1F467nosingle::👨‍👩‍👧‍👧single1👨‍👩‍👧‍👧
119713100&amp;#x1f239;🈹1F239nosingle::🈹single1🈹
119813101&amp;#x1f1f1;&amp;#x1f1fa;🇱🇺1F1F1-1F1FAnosingle::🇱🇺single1🇱🇺
119913102&amp;#x1f6e4;&amp;#65039;🛤️1F6E4-FE0Fnosingle::🛤️single1🛤️
120013103&amp;#x1f1fa;&amp;#x1f1ec;🇺🇬1F1FA-1F1ECnosingle::🇺🇬single1🇺🇬
120113104&amp;#x1f556;🕖1F556nosingle::🕖single1🕖
120213105&amp;#x1f234;🈴1F234nosingle::🈴single1🈴
120313106&amp;#x1f468;&amp;zwj;&amp;#x1f527;👨‍🔧1F468-200D-1F527nosingle::👨‍🔧single1👨‍🔧
120413107&amp;#x1f50f;🔏1F50Fnosingle::🔏single1🔏
120513108&amp;#x1f1e7;&amp;#x1f1fc;🇧🇼1F1E7-1F1FCnosingle::🇧🇼single1🇧🇼
120613109&amp;#x1f6b7;🚷1F6B7nosingle::🚷single1🚷
120713110&amp;#x1f1f2;&amp;#x1f1f0;🇲🇰1F1F2-1F1F0nosingle::🇲🇰single1🇲🇰
120813111&amp;#x1f1f1;&amp;#x1f1fb;🇱🇻1F1F1-1F1FBnosingle::🇱🇻single1🇱🇻
120913112&amp;#x1f559;🕙1F559nosingle::🕙single1🕙
121013113&amp;#x1f1f9;&amp;#x1f1f9;🇹🇹1F1F9-1F1F9nosingle::🇹🇹single1🇹🇹
121113114&amp;#x1f1f1;&amp;#x1f1f9;🇱🇹1F1F1-1F1F9nosingle::🇱🇹single1🇱🇹
121213115&amp;#x1f469;&amp;zwj;&amp;#10084;&amp;#65039;&amp;zwj;&amp;#x1f48b;&amp;zwj;&amp;#x1f468;👩‍❤️‍💋‍👨1F469-200D-2764-FE0F-200D-1F48B-200D-1F468nosingle::👩‍❤️‍💋‍👨single1👩‍❤️‍💋‍👨
121313116&amp;#x1f468;&amp;zwj;&amp;#x1f467;👨‍👧1F468-200D-1F467nosingle::👨‍👧single1👨‍👧
121413117&amp;#x1f4df;📟1F4DFnosingle::📟single1📟
121513118&amp;#x1f469;&amp;zwj;&amp;#9992;&amp;#65039;👩‍✈️1F469-200D-2708-FE0Fnosingle::👩‍✈️single1👩‍✈️
121613119&amp;#9208;&amp;#65039;⏸️23F8-FE0Fnosingle::⏸️single1⏸️
121713120&amp;#x1f469;&amp;zwj;&amp;#x1f692;👩‍🚒1F469-200D-1F692nosingle::👩‍🚒single1👩‍🚒
121813121&amp;#x1f1ff;&amp;#x1f1fc;🇿🇼1F1FF-1F1FCnosingle::🇿🇼single1🇿🇼
121913122&amp;#x1f961;🥡1F961nosingle::🥡single1🥡
122013123&amp;#x1f1f8;&amp;#x1f1f0;🇸🇰1F1F8-1F1F0nosingle::🇸🇰single1🇸🇰
122113124&amp;#x1f554;🕔1F554nosingle::🕔single1🕔
1222141&amp;#x1f1f4;&amp;#x1f1f2;🇴🇲1F1F4-1F1F2nosingle::🇴🇲single1🇴🇲
1223142&amp;#x1f4be;💾1F4BEnosingle::💾single1💾
1224143&amp;#x1f4e4;📤1F4E4nosingle::📤single1📤
1225144&amp;#9879;&amp;#65039;⚗️2697-FE0Fnosingle::⚗️single1⚗️
1226145&amp;#x1f520;🔠1F520nosingle::🔠single1🔠
1227146&amp;#x1f69d;🚝1F69Dnosingle::🚝single1🚝
1228147&amp;#x1f191;🆑1F191nosingle::🆑single1🆑
1229148&amp;#x1f1f2;&amp;#x1f1f2;🇲🇲1F1F2-1F1F2nosingle::🇲🇲single1🇲🇲
1230149&amp;#x1f468;&amp;zwj;&amp;#x1f466;👨‍👦1F468-200D-1F466nosingle::👨‍👦single1👨‍👦
12311410&amp;#x1f1f2;&amp;#x1f1f9;🇲🇹1F1F2-1F1F9nosingle::🇲🇹single1🇲🇹
12321411&amp;#x1f55a;🕚1F55Anosingle::🕚single1🕚
12331412&amp;#x1f1e7;&amp;#x1f1e6;🇧🇦1F1E7-1F1E6nosingle::🇧🇦single1🇧🇦
12341413&amp;#x1f469;&amp;zwj;&amp;#x1f467;&amp;zwj;&amp;#x1f466;👩‍👧‍👦1F469-200D-1F467-200D-1F466nosingle::👩‍👧‍👦single1👩‍👧‍👦
12351414&amp;#x1f3bf;🎿1F3BFnosingle::🎿single1🎿
12361415&amp;#x1f524;🔤1F524nosingle::🔤single1🔤
12371416&amp;#x1f236;🈶1F236nosingle::🈶single1🈶
12381417&amp;#x1f6b1;🚱1F6B1nosingle::🚱single1🚱
12391418&amp;#x1f68f;🚏1F68Fnosingle::🚏single1🚏
12401419&amp;#x1f1f3;&amp;#x1f1ea;🇳🇪1F1F3-1F1EAnosingle::🇳🇪single1🇳🇪
12411420&amp;#x1f469;&amp;zwj;&amp;#x1f527;👩‍🔧1F469-200D-1F527nosingle::👩‍🔧single1👩‍🔧
12421421&amp;#x1f1e7;&amp;#x1f1f8;🇧🇸1F1E7-1F1F8nosingle::🇧🇸single1🇧🇸
12431422&amp;#x1f54d;🕍1F54Dnosingle::🕍single1🕍
12441423&amp;#x1f1f0;&amp;#x1f1ed;🇰🇭1F1F0-1F1EDnosingle::🇰🇭single1🇰🇭
12451424&amp;#x1f522;🔢1F522nosingle::🔢single1🔢
12461425&amp;#x1f1e8;&amp;#x1f1fe;🇨🇾1F1E8-1F1FEnosingle::🇨🇾single1🇨🇾
12471426&amp;#x1f4ed;📭1F4EDnosingle::📭single1📭
12481427&amp;#x1f4ea;📪1F4EAnosingle::📪single1📪
12491428&amp;#x1f233;🈳1F233nosingle::🈳single1🈳
12501429&amp;#x1f1f2;&amp;#x1f1f7;🇲🇷1F1F2-1F1F7nosingle::🇲🇷single1🇲🇷
12511430&amp;#x1f469;&amp;zwj;&amp;#10084;&amp;#65039;&amp;zwj;&amp;#x1f468;👩‍❤️‍👨1F469-200D-2764-FE0F-200D-1F468nosingle::👩‍❤️‍👨single1👩‍❤️‍👨
12521431&amp;#x1f469;&amp;zwj;&amp;#x1f469;&amp;zwj;&amp;#x1f467;👩‍👩‍👧1F469-200D-1F469-200D-1F467nosingle::👩‍👩‍👧single1👩‍👩‍👧
12531432&amp;#x1f1f0;&amp;#x1f1ff;🇰🇿1F1F0-1F1FFnosingle::🇰🇿single1🇰🇿
12541433&amp;#x1f5a8;&amp;#65039;🖨️1F5A8-FE0Fnosingle::🖨️single1🖨️
12551434&amp;#9198;&amp;#65039;⏮️23EE-FE0Fnosingle::⏮️single1⏮️
12561435&amp;#x1f1ec;&amp;#x1f1fe;🇬🇾1F1EC-1F1FEnosingle::🇬🇾single1🇬🇾
12571436&amp;#x1f1f8;&amp;#x1f1ee;🇸🇮1F1F8-1F1EEnosingle::🇸🇮single1🇸🇮
12581437&amp;#x1f1e7;&amp;#x1f1fe;🇧🇾1F1E7-1F1FEnosingle::🇧🇾single1🇧🇾
12591438&amp;#x1f1f8;&amp;#x1f1f4;🇸🇴1F1F8-1F1F4nosingle::🇸🇴single1🇸🇴
12601439&amp;#x1f560;🕠1F560nosingle::🕠single1🕠
12611440&amp;#x1f1e6;&amp;#x1f1fc;🇦🇼1F1E6-1F1FCnosingle::🇦🇼single1🇦🇼
12621441&amp;#x1f5b2;&amp;#65039;🖲️1F5B2-FE0Fnosingle::🖲️single1🖲️
12631442&amp;#9905;&amp;#65039;⚱️26B1-FE0Fnosingle::⚱️single1⚱️
12641443&amp;#x1f1f2;&amp;#x1f1ea;🇲🇪1F1F2-1F1EAnosingle::🇲🇪single1🇲🇪
12651444&amp;#x1f1ff;&amp;#x1f1f2;🇿🇲1F1FF-1F1F2nosingle::🇿🇲single1🇿🇲
12661445&amp;#x1f1e7;&amp;#x1f1e7;🇧🇧1F1E7-1F1E7nosingle::🇧🇧single1🇧🇧
12671446&amp;#x1f1f9;&amp;#x1f1ef;🇹🇯1F1F9-1F1EFnosingle::🇹🇯single1🇹🇯
12681447&amp;#x1f1e8;&amp;#x1f1ec;🇨🇬1F1E8-1F1ECnosingle::🇨🇬single1🇨🇬
12691448&amp;#x1f1fa;&amp;#x1f1ff;🇺🇿1F1FA-1F1FFnosingle::🇺🇿single1🇺🇿
12701449&amp;#x1f1f8;&amp;#x1f1f1;🇸🇱1F1F8-1F1F1nosingle::🇸🇱single1🇸🇱
12711450&amp;#x1f1f3;&amp;#x1f1e6;🇳🇦1F1F3-1F1E6nosingle::🇳🇦single1🇳🇦
12721451&amp;#x1f69f;🚟1F69Fnosingle::🚟single1🚟
12731452&amp;#x1f1e7;&amp;#x1f1ee;🇧🇮1F1E7-1F1EEnosingle::🇧🇮single1🇧🇮
12741453&amp;#x1f1fd;&amp;#x1f1f0;🇽🇰1F1FD-1F1F0nosingle::🇽🇰single1🇽🇰
12751454&amp;#x1f1e6;&amp;#x1f1ee;🇦🇮1F1E6-1F1EEnosingle::🇦🇮single1🇦🇮
12761455&amp;#x1f1ea;&amp;#x1f1f9;🇪🇹1F1EA-1F1F9nosingle::🇪🇹single1🇪🇹
12771456&amp;#x1f469;&amp;zwj;&amp;#x1f469;&amp;zwj;&amp;#x1f467;&amp;zwj;&amp;#x1f467;👩‍👩‍👧‍👧1F469-200D-1F469-200D-1F467-200D-1F467nosingle::👩‍👩‍👧‍👧single1👩‍👩‍👧‍👧
12781457&amp;#x1f1e6;&amp;#x1f1ec;🇦🇬1F1E6-1F1ECnosingle::🇦🇬single1🇦🇬
12791458&amp;#x1f468;&amp;zwj;&amp;#x1f3ed;👨‍🏭1F468-200D-1F3EDnosingle::👨‍🏭single1👨‍🏭
12801459&amp;#x1f469;&amp;zwj;&amp;#x1f467;&amp;zwj;&amp;#x1f467;👩‍👧‍👧1F469-200D-1F467-200D-1F467nosingle::👩‍👧‍👧single1👩‍👧‍👧
12811460&amp;#x1f468;&amp;zwj;&amp;#x1f468;&amp;zwj;&amp;#x1f467;&amp;zwj;&amp;#x1f467;👨‍👨‍👧‍👧1F468-200D-1F468-200D-1F467-200D-1F467nosingle::👨‍👨‍👧‍👧single1👨‍👨‍👧‍👧
12821461&amp;#x1f22f;🈯1F22Fnosingle::🈯single1🈯
12831462&amp;#x1f6a0;🚠1F6A0nosingle::🚠single1🚠
12841463&amp;#x1f55f;🕟1F55Fnosingle::🕟single1🕟
12851464&amp;#x1f468;&amp;zwj;&amp;#x1f468;&amp;zwj;&amp;#x1f466;&amp;zwj;&amp;#x1f466;👨‍👨‍👦‍👦1F468-200D-1F468-200D-1F466-200D-1F466nosingle::👨‍👨‍👦‍👦single1👨‍👨‍👦‍👦
12861465&amp;#x1f469;&amp;zwj;&amp;#x1f469;&amp;zwj;&amp;#x1f467;&amp;zwj;&amp;#x1f466;👩‍👩‍👧‍👦1F469-200D-1F469-200D-1F467-200D-1F466nosingle::👩‍👩‍👧‍👦single1👩‍👩‍👧‍👦
12871466&amp;#x1f1f7;&amp;#x1f1fc;🇷🇼1F1F7-1F1FCnosingle::🇷🇼single1🇷🇼
12881467&amp;#x1f1e6;&amp;#x1f1fd;🇦🇽1F1E6-1F1FDnosingle::🇦🇽single1🇦🇽
12891468&amp;#x1f469;&amp;zwj;&amp;#x1f466;&amp;zwj;&amp;#x1f466;👩‍👦‍👦1F469-200D-1F466-200D-1F466nosingle::👩‍👦‍👦single1👩‍👦‍👦
12901469&amp;#x1f1eb;&amp;#x1f1ef;🇫🇯1F1EB-1F1EFnosingle::🇫🇯single1🇫🇯
12911470&amp;#x1f94c;🥌1F94Cnosingle::🥌single1🥌
12921471&amp;#x1f1e6;&amp;#x1f1f8;🇦🇸1F1E6-1F1F8nosingle::🇦🇸single1🇦🇸
12931472&amp;#x1f1f9;&amp;#x1f1e9;🇹🇩1F1F9-1F1E9nosingle::🇹🇩single1🇹🇩
12941473&amp;#x1f468;&amp;zwj;&amp;#x1f467;&amp;zwj;&amp;#x1f466;👨‍👧‍👦1F468-200D-1F467-200D-1F466nosingle::👨‍👧‍👦single1👨‍👧‍👦
12951474&amp;#x1f1e6;&amp;#x1f1e9;🇦🇩1F1E6-1F1E9nosingle::🇦🇩single1🇦🇩
12961475&amp;#x1f1ec;&amp;#x1f1f2;🇬🇲1F1EC-1F1F2nosingle::🇬🇲single1🇬🇲
12971476&amp;#x1f1ec;&amp;#x1f1e6;🇬🇦1F1EC-1F1E6nosingle::🇬🇦single1🇬🇦
12981477&amp;#x1f5c2;&amp;#65039;🗂️1F5C2-FE0Fnosingle::🗂️single1🗂️
12991478&amp;#x1f1e7;&amp;#x1f1f2;🇧🇲1F1E7-1F1F2nosingle::🇧🇲single1🇧🇲
13001479&amp;#x1f469;&amp;zwj;&amp;#x1f3ed;👩‍🏭1F469-200D-1F3EDnosingle::👩‍🏭single1👩‍🏭
1301151&amp;#x1f55d;🕝1F55Dnosingle::🕝single1🕝
1302152&amp;#x1f566;🕦1F566nosingle::🕦single1🕦
1303153&amp;#x1f562;🕢1F562nosingle::🕢single1🕢
1304154&amp;#x1f55c;🕜1F55Cnosingle::🕜single1🕜
1305155&amp;#x1f1f2;&amp;#x1f1f3;🇲🇳1F1F2-1F1F3nosingle::🇲🇳single1🇲🇳
1306156&amp;#x1f1f9;&amp;#x1f1ec;🇹🇬1F1F9-1F1ECnosingle::🇹🇬single1🇹🇬
1307157&amp;#x1f564;🕤1F564nosingle::🕤single1🕤
1308158&amp;#x1f1f2;&amp;#x1f1ff;🇲🇿1F1F2-1F1FFnosingle::🇲🇿single1🇲🇿
1309159&amp;#x1f468;&amp;zwj;&amp;#x1f468;&amp;zwj;&amp;#x1f466;👨‍👨‍👦1F468-200D-1F468-200D-1F466nosingle::👨‍👨‍👦single1👨‍👨‍👦
13101510&amp;#x1f202;&amp;#65039;🈂️1F202-FE0Fnosingle::🈂️single1🈂️
13111511&amp;#x1f1ec;&amp;#x1f1fc;🇬🇼1F1EC-1F1FCnosingle::🇬🇼single1🇬🇼
13121512&amp;#x1f1e7;&amp;#x1f1f3;🇧🇳1F1E7-1F1F3nosingle::🇧🇳single1🇧🇳
13131513&amp;#x1f1f2;&amp;#x1f1fb;🇲🇻1F1F2-1F1FBnosingle::🇲🇻single1🇲🇻
13141514&amp;#x1f567;🕧1F567nosingle::🕧single1🕧
13151515&amp;#x1f1f2;&amp;#x1f1f4;🇲🇴1F1F2-1F1F4nosingle::🇲🇴single1🇲🇴
13161516&amp;#x1f561;🕡1F561nosingle::🕡single1🕡
13171517&amp;#9934;26CEnosingle::⛎single1
13181518&amp;#x1f1f9;&amp;#x1f1f4;🇹🇴1F1F9-1F1F4nosingle::🇹🇴single1🇹🇴
13191519&amp;#x1f563;🕣1F563nosingle::🕣single1🕣
13201520&amp;#x1f468;&amp;zwj;&amp;#x1f468;&amp;zwj;&amp;#x1f467;&amp;zwj;&amp;#x1f466;👨‍👨‍👧‍👦1F468-200D-1F468-200D-1F467-200D-1F466nosingle::👨‍👨‍👧‍👦single1👨‍👨‍👧‍👦
13211521&amp;#x1f1ea;&amp;#x1f1f7;🇪🇷1F1EA-1F1F7nosingle::🇪🇷single1🇪🇷
13221522&amp;#x1f1e6;&amp;#x1f1e8;🇦🇨1F1E6-1F1E8nosingle::🇦🇨single1🇦🇨
13231523&amp;#x1f1f0;&amp;#x1f1ec;🇰🇬1F1F0-1F1ECnosingle::🇰🇬single1🇰🇬
13241524&amp;#x1f1e6;&amp;#x1f1f6;🇦🇶1F1E6-1F1F6nosingle::🇦🇶single1🇦🇶
13251525&amp;#x1f1e8;&amp;#x1f1fb;🇨🇻1F1E8-1F1FBnosingle::🇨🇻single1🇨🇻
13261526&amp;#x1f1e7;&amp;#x1f1eb;🇧🇫1F1E7-1F1EBnosingle::🇧🇫single1🇧🇫
13271527&amp;#x1f1f0;&amp;#x1f1f5;🇰🇵1F1F0-1F1F5nosingle::🇰🇵single1🇰🇵
13281528&amp;#x1f468;&amp;zwj;&amp;#x1f466;&amp;zwj;&amp;#x1f466;👨‍👦‍👦1F468-200D-1F466-200D-1F466nosingle::👨‍👦‍👦single1👨‍👦‍👦
13291529&amp;#x1f565;🕥1F565nosingle::🕥single1🕥
13301530&amp;#x1f1f2;&amp;#x1f1e9;🇲🇩1F1F2-1F1E9nosingle::🇲🇩single1🇲🇩
13311531&amp;#x1f1ee;&amp;#x1f1e8;🇮🇨1F1EE-1F1E8nosingle::🇮🇨single1🇮🇨
13321532&amp;#x1f1fb;&amp;#x1f1e6;🇻🇦1F1FB-1F1E6nosingle::🇻🇦single1🇻🇦
13331533&amp;#x1f1ec;&amp;#x1f1e9;🇬🇩1F1EC-1F1E9nosingle::🇬🇩single1🇬🇩
13341534&amp;#x1f1ee;&amp;#x1f1f2;🇮🇲1F1EE-1F1F2nosingle::🇮🇲single1🇮🇲
13351535&amp;#x1f469;&amp;zwj;&amp;#x1f469;&amp;zwj;&amp;#x1f466;&amp;zwj;&amp;#x1f466;👩‍👩‍👦‍👦1F469-200D-1F469-200D-1F466-200D-1F466nosingle::👩‍👩‍👦‍👦single1👩‍👩‍👦‍👦
13361536&amp;#x1f1f2;&amp;#x1f1eb;🇲🇫1F1F2-1F1EBnosingle::🇲🇫single1🇲🇫
13371537&amp;#x1f4c7;📇1F4C7nosingle::📇single1📇
13381538&amp;#x1f23a;🈺1F23Anosingle::🈺single1🈺
13391539&amp;#x1f1f1;&amp;#x1f1e6;🇱🇦1F1F1-1F1E6nosingle::🇱🇦single1🇱🇦
13401540&amp;#x1f6f7;🛷1F6F7nosingle::🛷single1🛷
13411541&amp;#x1f55e;🕞1F55Enosingle::🕞single1🕞
13421542&amp;#x1f1f2;&amp;#x1f1fc;🇲🇼1F1F2-1F1FCnosingle::🇲🇼single1🇲🇼
13431543&amp;#x1f469;&amp;zwj;&amp;#x1f469;&amp;zwj;&amp;#x1f466;👩‍👩‍👦1F469-200D-1F469-200D-1F466nosingle::👩‍👩‍👦single1👩‍👩‍👦
13441544&amp;#x1f468;&amp;zwj;&amp;#x1f468;&amp;zwj;&amp;#x1f467;👨‍👨‍👧1F468-200D-1F468-200D-1F467nosingle::👨‍👨‍👧single1👨‍👨‍👧
13451545&amp;#x1f237;&amp;#65039;🈷️1F237-FE0Fnosingle::🈷️single1🈷️
13461546&amp;#x1f468;&amp;zwj;&amp;#x1f467;&amp;zwj;&amp;#x1f467;👨‍👧‍👧1F468-200D-1F467-200D-1F467nosingle::👨‍👧‍👧single1👨‍👧‍👧
13471547&amp;#x1f1fa;&amp;#x1f1f3;🇺🇳1F1FA-1F1F3nosingle::🇺🇳single1🇺🇳
13481548&amp;#x1f5c3;&amp;#65039;🗃️1F5C3-FE0Fnosingle::🗃️single1🗃️
13491549&amp;#x1f1f1;&amp;#x1f1f8;🇱🇸1F1F1-1F1F8nosingle::🇱🇸single1🇱🇸
13501550&amp;#x1f5dc;&amp;#65039;🗜️1F5DC-FE0Fnosingle::🗜️single1🗜️
13511551&amp;#x1f1ec;&amp;#x1f1fa;🇬🇺1F1EC-1F1FAnosingle::🇬🇺single1🇬🇺
13521552&amp;#x1f1e7;&amp;#x1f1fb;🇧🇻1F1E7-1F1FBnosingle::🇧🇻single1🇧🇻
13531553&amp;#x1f521;🔡1F521nosingle::🔡single1🔡
13541554&amp;#x1f1eb;&amp;#x1f1f4;🇫🇴1F1EB-1F1F4nosingle::🇫🇴single1🇫🇴
13551555&amp;#x1f1f2;&amp;#x1f1fa;🇲🇺1F1F2-1F1FAnosingle::🇲🇺single1🇲🇺
13561556&amp;#x1f1f5;&amp;#x1f1ec;🇵🇬1F1F5-1F1ECnosingle::🇵🇬single1🇵🇬
13571557&amp;#x1f1e8;&amp;#x1f1fc;🇨🇼1F1E8-1F1FCnosingle::🇨🇼single1🇨🇼
13581558&amp;#x1f1ec;&amp;#x1f1ec;🇬🇬1F1EC-1F1ECnosingle::🇬🇬single1🇬🇬
13591559&amp;#x1f201;🈁1F201nosingle::🈁single1🈁
13601560&amp;#x1f523;🔣1F523nosingle::🔣single1🔣
13611561&amp;#x1f1fc;&amp;#x1f1f8;🇼🇸1F1FC-1F1F8nosingle::🇼🇸single1🇼🇸
13621562&amp;#x1f1f8;&amp;#x1f1f7;🇸🇷1F1F8-1F1F7nosingle::🇸🇷single1🇸🇷
13631563&amp;#x1f1f9;&amp;#x1f1f2;🇹🇲1F1F9-1F1F2nosingle::🇹🇲single1🇹🇲
13641564&amp;#x1f1f8;&amp;#x1f1f8;🇸🇸1F1F8-1F1F8nosingle::🇸🇸single1🇸🇸
13651565&amp;#x1f5c4;&amp;#65039;🗄️1F5C4-FE0Fnosingle::🗄️single1🗄️
13661566&amp;#x1f1e7;&amp;#x1f1f9;🇧🇹1F1E7-1F1F9nosingle::🇧🇹single1🇧🇹
13671567&amp;#x1f1f8;&amp;#x1f1fd;🇸🇽1F1F8-1F1FDnosingle::🇸🇽single1🇸🇽
13681568&amp;#x1f1f0;&amp;#x1f1f2;🇰🇲1F1F0-1F1F2nosingle::🇰🇲single1🇰🇲
13691569&amp;#x1f238;🈸1F238nosingle::🈸single1🈸
13701570&amp;#x1f1e9;&amp;#x1f1f2;🇩🇲1F1E9-1F1F2nosingle::🇩🇲single1🇩🇲
13711571&amp;#x1f1ea;&amp;#x1f1ed;🇪🇭1F1EA-1F1EDnosingle::🇪🇭single1🇪🇭
13721572&amp;#x1f1e7;&amp;#x1f1ff;🇧🇿1F1E7-1F1FFnosingle::🇧🇿single1🇧🇿
13731573&amp;#x1f1fb;&amp;#x1f1ee;🇻🇮1F1FB-1F1EEnosingle::🇻🇮single1🇻🇮
13741574&amp;#x1f1ec;&amp;#x1f1eb;🇬🇫1F1EC-1F1EBnosingle::🇬🇫single1🇬🇫
13751575&amp;#x1f1f8;&amp;#x1f1f2;🇸🇲1F1F8-1F1F2nosingle::🇸🇲single1🇸🇲
13761576&amp;#x1f1e9;&amp;#x1f1ef;🇩🇯1F1E9-1F1EFnosingle::🇩🇯single1🇩🇯
13771577&amp;#x1f1f9;&amp;#x1f1e8;🇹🇨1F1F9-1F1E8nosingle::🇹🇨single1🇹🇨
1378161&amp;#x1f1f1;&amp;#x1f1e8;🇱🇨1F1F1-1F1E8nosingle::🇱🇨single1🇱🇨
1379162&amp;#x1f1fb;&amp;#x1f1fa;🇻🇺1F1FB-1F1FAnosingle::🇻🇺single1🇻🇺
1380163&amp;#x1f1f5;&amp;#x1f1eb;🇵🇫1F1F5-1F1EBnosingle::🇵🇫single1🇵🇫
1381164&amp;#x1f1ec;&amp;#x1f1ee;🇬🇮1F1EC-1F1EEnosingle::🇬🇮single1🇬🇮
1382165&amp;#x1f1f8;&amp;#x1f1e8;🇸🇨1F1F8-1F1E8nosingle::🇸🇨single1🇸🇨
1383166&amp;#x1f1f0;&amp;#x1f1f3;🇰🇳1F1F0-1F1F3nosingle::🇰🇳single1🇰🇳
1384167&amp;#x1f1ef;&amp;#x1f1ea;🇯🇪1F1EF-1F1EAnosingle::🇯🇪single1🇯🇪
1385168&amp;#x1f1f2;&amp;#x1f1f6;🇲🇶1F1F2-1F1F6nosingle::🇲🇶single1🇲🇶
1386169&amp;#x1f468;&amp;zwj;&amp;#x1f469;&amp;zwj;&amp;#x1f466;👨‍👩‍👦1F468-200D-1F469-200D-1F466nosingle::👨‍👩‍👦single1👨‍👩‍👦
13871610&amp;#x1f1f7;&amp;#x1f1ea;🇷🇪1F1F7-1F1EAnosingle::🇷🇪single1🇷🇪
13881611&amp;#x1f1f8;&amp;#x1f1ff;🇸🇿1F1F8-1F1FFnosingle::🇸🇿single1🇸🇿
13891612&amp;#x1f1f0;&amp;#x1f1fe;🇰🇾1F1F0-1F1FEnosingle::🇰🇾single1🇰🇾
13901613&amp;#9167;&amp;#65039;⏏️23CF-FE0Fnosingle::⏏️single1⏏️
13911614&amp;#x1f1fb;&amp;#x1f1e8;🇻🇨1F1FB-1F1E8nosingle::🇻🇨single1🇻🇨
13921615&amp;#x1f1fb;&amp;#x1f1ec;🇻🇬1F1FB-1F1ECnosingle::🇻🇬single1🇻🇬
13931616&amp;#x1f1f1;&amp;#x1f1ee;🇱🇮1F1F1-1F1EEnosingle::🇱🇮single1🇱🇮
13941617&amp;#x1f1e8;&amp;#x1f1e8;🇨🇨1F1E8-1F1E8nosingle::🇨🇨single1🇨🇨
13951618&amp;#x1f1e8;&amp;#x1f1eb;🇨🇫1F1E8-1F1EBnosingle::🇨🇫single1🇨🇫
13961619&amp;#x1f1f8;&amp;#x1f1f9;🇸🇹1F1F8-1F1F9nosingle::🇸🇹single1🇸🇹
13971620&amp;#x1f1ec;&amp;#x1f1f1;🇬🇱1F1EC-1F1F1nosingle::🇬🇱single1🇬🇱
13981621&amp;#x1f1f5;&amp;#x1f1fc;🇵🇼1F1F5-1F1FCnosingle::🇵🇼single1🇵🇼
13991622&amp;#x1f1ec;&amp;#x1f1f5;🇬🇵1F1EC-1F1F5nosingle::🇬🇵single1🇬🇵
14001623&amp;#x1f1ed;&amp;#x1f1f2;🇭🇲1F1ED-1F1F2nosingle::🇭🇲single1🇭🇲
14011624&amp;#x1f1e8;&amp;#x1f1f0;🇨🇰1F1E8-1F1F0nosingle::🇨🇰single1🇨🇰
14021625&amp;#x1f1eb;&amp;#x1f1f0;🇫🇰1F1EB-1F1F0nosingle::🇫🇰single1🇫🇰
14031626&amp;#x1f1f5;&amp;#x1f1f3;🇵🇳1F1F5-1F1F3nosingle::🇵🇳single1🇵🇳
14041627&amp;#x1f1f3;&amp;#x1f1eb;🇳🇫1F1F3-1F1EBnosingle::🇳🇫single1🇳🇫
14051628&amp;#x1f1f8;&amp;#x1f1e7;🇸🇧1F1F8-1F1E7nosingle::🇸🇧single1🇸🇧
14061629&amp;#x1f1f9;&amp;#x1f1f1;🇹🇱1F1F9-1F1F1nosingle::🇹🇱single1🇹🇱
14071630&amp;#x1f1eb;&amp;#x1f1f2;🇫🇲1F1EB-1F1F2nosingle::🇫🇲single1🇫🇲
14081631&amp;#x1f1ee;&amp;#x1f1f4;🇮🇴1F1EE-1F1F4nosingle::🇮🇴single1🇮🇴
14091632&amp;#x1f1f2;&amp;#x1f1ed;🇲🇭1F1F2-1F1EDnosingle::🇲🇭single1🇲🇭
14101633&amp;#x1f1ec;&amp;#x1f1f6;🇬🇶1F1EC-1F1F6nosingle::🇬🇶single1🇬🇶
14111634&amp;#x1f1f9;&amp;#x1f1f0;🇹🇰1F1F9-1F1F0nosingle::🇹🇰single1🇹🇰
14121635&amp;hellip;2026nosingle::…single1
141317+1&amp;#x1f1e7;&amp;#x1f1f6;🇧🇶1F1E7-1F1F6nosingle::🇧🇶single1🇧🇶
141417+2&amp;#x1f1f2;&amp;#x1f1f5;🇲🇵1F1F2-1F1F5nosingle::🇲🇵single1🇲🇵
141517+3&amp;#x1f1e8;&amp;#x1f1fd;🇨🇽1F1E8-1F1FDnosingle::🇨🇽single1🇨🇽
141617+4&amp;#x1f1f0;&amp;#x1f1ee;🇰🇮1F1F0-1F1EEnosingle::🇰🇮single1🇰🇮
141717+5&amp;#x1f1f3;&amp;#x1f1fa;🇳🇺1F1F3-1F1FAnosingle::🇳🇺single1🇳🇺
141817+6&amp;#x1f1f8;&amp;#x1f1ef;🇸🇯1F1F8-1F1EFnosingle::🇸🇯single1🇸🇯
141917+7&amp;#x1f1f2;&amp;#x1f1f8;🇲🇸1F1F2-1F1F8nosingle::🇲🇸single1🇲🇸
142017+8&amp;#x1f1f8;&amp;#x1f1ed;🇸🇭1F1F8-1F1EDnosingle::🇸🇭single1🇸🇭
142117+9&amp;#x1f1f9;&amp;#x1f1fb;🇹🇻1F1F9-1F1FBnosingle::🇹🇻single1🇹🇻
142217+10&amp;#x1f1f3;&amp;#x1f1e8;🇳🇨1F1F3-1F1E8nosingle::🇳🇨single1🇳🇨
142317+11&amp;#x1f1f3;&amp;#x1f1f7;🇳🇷1F1F3-1F1F7nosingle::🇳🇷single1🇳🇷
142417+12&amp;#x1f1e7;&amp;#x1f1f1;🇧🇱1F1E7-1F1F1nosingle::🇧🇱single1🇧🇱
142517+13&amp;#x1f1fc;&amp;#x1f1eb;🇼🇫1F1FC-1F1EBnosingle::🇼🇫single1🇼🇫
142617+14&amp;#x1f1ec;&amp;#x1f1f8;&amp;hellip;🇬🇸…1F1EC-1F1F8-2026nosingle::🇬🇸…single1🇬🇸…
+
+
+ + diff --git a/docs/frameworks/font-manager.md b/docs/frameworks/font-manager.md index 90b93f1..c2a5b8d 100644 --- a/docs/frameworks/font-manager.md +++ b/docs/frameworks/font-manager.md @@ -185,6 +185,10 @@ cp original.png 20x20/CODEPOINT.png 2. The new emoji will be picked up automatically on next boot — no code changes needed. +### Emoji reference + +For a canonical frequency-ordered listing of all available emoji codepoints, see [emoji_frequency_canonical.html](emoji_frequency_canonical.html). + ## File Structure ``` From ad5a91a96a67badbb42d1069c9e3be1c67578323 Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Fri, 29 May 2026 12:43:39 +0200 Subject: [PATCH 093/110] Update docs --- docs/frameworks/font-manager.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/frameworks/font-manager.md b/docs/frameworks/font-manager.md index c2a5b8d..ab4c2a1 100644 --- a/docs/frameworks/font-manager.md +++ b/docs/frameworks/font-manager.md @@ -163,7 +163,7 @@ clean = FontManager.normalizeEmojiText("❤️") # removes U+FE0F after ❤ ## Emoji Assets -Emoji PNGs are stored in `internal_filesystem/builtin/res/emojis/` and are included in the firmware image at `/builtin/res/emojis/` on the device filesystem: +To keep firmware image size small, the OS bundles ~50 of the most frequently-used emoji codepoints. Individual apps can bundle additional emoji PNGs in their own assets directory. Emoji PNGs are stored in `internal_filesystem/builtin/res/emojis/` and are included in the firmware image at `/builtin/res/emojis/` on the device filesystem: ``` builtin/res/emojis/ From eb456b2bb78ddf7dcd1960043448beff4bc458e2 Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Fri, 29 May 2026 12:55:49 +0200 Subject: [PATCH 094/110] Improve table layout --- .../frameworks/emoji_frequency_canonical.html | 2862 ++++++++--------- 1 file changed, 1429 insertions(+), 1433 deletions(-) diff --git a/docs/frameworks/emoji_frequency_canonical.html b/docs/frameworks/emoji_frequency_canonical.html index a527325..d0758b3 100644 --- a/docs/frameworks/emoji_frequency_canonical.html +++ b/docs/frameworks/emoji_frequency_canonical.html @@ -20,9 +20,7 @@ color: var(--text); } .wrap { - max-width: 1200px; - margin: 24px auto; - padding: 0 16px; + margin: 24px 16px; } h1 { margin: 0 0 8px; @@ -40,8 +38,6 @@ box-shadow: 0 8px 30px rgba(15, 23, 42, 0.08); } table { - width: 180%; - table-layout: fixed; border-collapse: collapse; } thead th { @@ -74,1435 +70,1435 @@

Emoji Frequency (Canonical)

rankfrequency_groupposition_in_grouphtml_entityemojiunicode_codepoint_hexpromotedcanonical_groupgroup_typegroup_sizegroup_membersdropped_similar - 0637&amp;#9889;⚡26A1yessingle::⚡single1⚡ -0731&amp;#x1f44e;👎1F44Eyessingle::👎single1👎 + 0637&amp;#9889…⚡26A1yessingle::⚡single1⚡ +0731&amp;#x1f4…👎1F44Eyessingle::👎single1👎 00🥳🥳1F973yessynthetic::🥳single1🥳 -101&amp;#x1f602;😂1F602nomanual::tears_laughingmanual2😂 🤣🤣 -202&amp;#10084;&amp;#65039;❤️2764-FE0Fnomanual::heartsmanual16❤️ 💕 ♥️ 💔 💖 💙 💜 💞 💗 💚 💛 💓 🖤 ❣️ 💘 💝💕 ♥️ 💔 💖 💙 💜 💞 💗 💚 💛 💓 🖤 ❣️ 💘 💝 -311&amp;#x1f60d;😍1F60Dnomanual::smile_lovemanual4😍 😻 😈 😹😻 😈 😹 -421&amp;#x1f60a;😊1F60Anosingle::😊single1😊 -522&amp;#x1f64f;🙏1F64Fnosingle::🙏single1🙏 -624&amp;#x1f62d;😭1F62Dnosingle::😭single1😭 -725&amp;#x1f618;😘1F618nosingle::😘single1😘 -831&amp;#x1f44d;👍1F44Dnomanual::good_handmanual4👍 👏 👌 🙌👏 👌 🙌 -932&amp;#x1f605;😅1F605nosingle::😅single1😅 -1034&amp;#x1f601;😁1F601nomanual::smile_beammanual2😁 😄😄 -1136&amp;#x1f525;🔥1F525nosingle::🔥single1🔥 -12310&amp;#x1f622;😢1F622nomanual::sad_crymanual4😢 😪 😥 😓😪 😥 😓 -13311&amp;#x1f914;🤔1F914nosingle::🤔single1🤔 -14312&amp;#x1f606;😆1F606nosingle::😆single1😆 -15313&amp;#x1f644;🙄1F644nosingle::🙄single1🙄 -16314&amp;#x1f4aa;💪1F4AAnosingle::💪single1💪 -17315&amp;#x1f609;😉1F609nosingle::😉single1😉 -18316&amp;#9786;&amp;#65039;☺️263A-FE0Fnosingle::☺️single1☺️ -19318&amp;#x1f917;🤗1F917nosingle::🤗single1🤗 -2042&amp;#x1f614;😔1F614nosingle::😔single1😔 -2143&amp;#x1f60e;😎1F60Enosingle::😎single1😎 -2244&amp;#x1f607;😇1F607nosingle::😇single1😇 -2345&amp;#x1f339;🌹1F339nomanual::flower_groupmanual7🌹 🌸 🌷 🎊 🌺 🌻 🌼🌸 🌷 🎊 🌺 🌻 🌼 -2446&amp;#x1f926;🤦1F926nosingle::🤦single1🤦 -2547&amp;#x1f389;🎉1F389nosingle::🎉single1🎉 -2648&amp;#8252;&amp;#65039;‼️203C-FE0Fnomanual::exclamationmanual2‼️ ❗❗ -27410&amp;#9996;&amp;#65039;✌️270C-FE0Fnosingle::✌️single1✌️ -28411&amp;#10024;✨2728nosingle::✨single1✨ -29412&amp;#x1f937;🤷1F937nosingle::🤷single1🤷 -30413&amp;#x1f631;😱1F631nosingle::😱single1😱 -31414&amp;#x1f60c;😌1F60Cnosingle::😌single1😌 -32417&amp;#x1f60b;😋1F60Bnomanual::tongue_groupmanual2😋 😛😛 -33420&amp;#x1f60f;😏1F60Fnosingle::😏single1😏 -34422&amp;#x1f642;🙂1F642nosingle::🙂single1🙂 -35424&amp;#x1f929;🤩1F929nosingle::🤩single1🤩 -36426&amp;#x1f600;😀1F600nomanual::smile_grinmanual2😀 😃😃 -37429&amp;#x1f4af;💯1F4AFnosingle::💯single1💯 -38430&amp;#x1f648;🙈1F648nosingle::🙈single1🙈 -39431&amp;#x1f447;👇1F447nosingle::👇single1👇 -40432&amp;#x1f3b6;🎶1F3B6nomanual::music_groupmanual2🎶 🎵🎵 -41433&amp;#x1f612;😒1F612nosingle::😒single1😒 -42434&amp;#x1f92d;🤭1F92Dnosingle::🤭single1🤭 -4352&amp;#x1f61c;😜1F61Cnosingle::😜single1😜 -4453&amp;#x1f48b;💋1F48Bnosingle::💋single1💋 -4554&amp;#x1f440;👀1F440nosingle::👀single1👀 -4656&amp;#x1f611;😑1F611nosingle::😑single1😑 -4757&amp;#x1f4a5;💥1F4A5nosingle::💥single1💥 -4858&amp;#x1f64b;🙋1F64Bnosingle::🙋single1🙋 -4959&amp;#x1f61e;😞1F61Enosingle::😞single1😞 -50510&amp;#x1f629;😩1F629nosingle::😩single1😩 -51511&amp;#x1f621;😡1F621nomanual::angrymanual3😡 🔴 😤🔴 😤 -52512&amp;#x1f92a;🤪1F92Anosingle::🤪single1🤪 -53513&amp;#x1f44a;👊1F44Anosingle::👊single1👊 -54514&amp;#9728;&amp;#65039;☀️2600-FE0Fnosingle::☀️single1☀️ -55516&amp;#x1f924;🤤1F924nosingle::🤤single1🤤 -56517&amp;#x1f449;👉1F449nosingle::👉single1👉 -57518&amp;#x1f483;💃1F483nosingle::💃single1💃 -58519&amp;#x1f633;😳1F633nosingle::😳single1😳 -59520&amp;#9995;✋270Bnosingle::✋single1✋ -60521&amp;#x1f61a;😚1F61Anosingle::😚single1😚 -61522&amp;#x1f61d;😝1F61Dnosingle::😝single1😝 -62523&amp;#x1f634;😴1F634nosingle::😴single1😴 -63524&amp;#x1f31f;🌟1F31Fnosingle::🌟single1🌟 -64525&amp;#x1f62c;😬1F62Cnosingle::😬single1😬 -65526&amp;#x1f643;🙃1F643nosingle::🙃single1🙃 -66527&amp;#x1f340;🍀1F340nosingle::🍀single1🍀 -67531&amp;#11088;⭐2B50nosingle::⭐single1⭐ -68532&amp;#9989;✅2705nomanual::checkmarksmanual2✅ ✔️✔️ -69533&amp;#x1f308;🌈1F308nosingle::🌈single1🌈 -70535&amp;#x1f918;🤘1F918nosingle::🤘single1🤘 -7161&amp;#x1f4a6;💦1F4A6nosingle::💦single1💦 -7263&amp;#x1f623;😣1F623nosingle::😣single1😣 -7364&amp;#x1f3c3;🏃1F3C3nosingle::🏃single1🏃 -7465&amp;#x1f490;💐1F490nosingle::💐single1💐 -7566&amp;#9785;&amp;#65039;☹️2639-FE0Fnosingle::☹️single1☹️ -7669&amp;#x1f620;😠1F620nosingle::😠single1😠 -77610&amp;#9757;&amp;#65039;☝️261D-FE0Fnosingle::☝️single1☝️ -78611&amp;#x1f615;😕1F615nosingle::😕single1😕 -79613&amp;#x1f382;🎂1F382nomanual::birthday_cakesmanual2🎂 👑👑 -80615&amp;#x1f610;😐1F610nosingle::😐single1😐 -81616&amp;#x1f595;🖕1F595nosingle::🖕single1🖕 -82618&amp;#x1f64a;🙊1F64Anosingle::🙊single1🙊 -83620&amp;#x1f5e3;&amp;#65039;🗣️1F5E3-FE0Fnosingle::🗣️single1🗣️ -84621&amp;#x1f4ab;💫1F4ABnosingle::💫single1💫 -85622&amp;#x1f480;💀1F480nosingle::💀single1💀 -86625&amp;#x1f91e;🤞1F91Enosingle::🤞single1🤞 -87630&amp;#x1f62b;😫1F62Bnosingle::😫single1😫 -88631&amp;#9917;⚽26BDnosingle::⚽single1⚽ -89632&amp;#x1f919;🤙1F919nosingle::🤙single1🤙 -90633&amp;#9749;☕2615nosingle::☕single1☕ -91634&amp;#x1f3c6;🏆1F3C6nosingle::🏆single1🏆 -92635&amp;#x1f9e1;🧡1F9E1nosingle::🧡single1🧡 -93636&amp;#x1f381;🎁1F381nosingle::🎁single1🎁 -94638&amp;#x1f31e;🌞1F31Enosingle::🌞single1🌞 -95639&amp;#x1f388;🎈1F388nosingle::🎈single1🎈 -96640&amp;#10060;❌274Cnosingle::❌single1❌ -97641&amp;#9994;✊270Anosingle::✊single1✊ -98642&amp;#x1f44b;👋1F44Bnosingle::👋single1👋 -99643&amp;#x1f632;😲1F632nosingle::😲single1😲 -100644&amp;#x1f33f;🌿1F33Fnosingle::🌿single1🌿 -101645&amp;#x1f92b;🤫1F92Bnosingle::🤫single1🤫 -102646&amp;#x1f448;👈1F448nosingle::👈single1👈 -103647&amp;#x1f62e;😮1F62Enosingle::😮single1😮 -104648&amp;#x1f646;🙆1F646nosingle::🙆single1🙆 -105649&amp;#x1f37b;🍻1F37Bnosingle::🍻single1🍻 -106650&amp;#x1f343;🍃1F343nosingle::🍃single1🍃 -107651&amp;#x1f436;🐶1F436nosingle::🐶single1🐶 -108652&amp;#x1f481;💁1F481nosingle::💁single1💁 -109653&amp;#x1f630;😰1F630nosingle::😰single1😰 -110654&amp;#x1f928;🤨1F928nosingle::🤨single1🤨 -111655&amp;#x1f636;😶1F636nosingle::😶single1😶 -112656&amp;#x1f91d;🤝1F91Dnosingle::🤝single1🤝 -113657&amp;#x1f6b6;🚶1F6B6nosingle::🚶single1🚶 -114658&amp;#x1f4b0;💰1F4B0nosingle::💰single1💰 -115659&amp;#x1f353;🍓1F353nosingle::🍓single1🍓 -116660&amp;#x1f4a2;💢1F4A2nosingle::💢single1💢 -11771&amp;#x1f1fa;&amp;#x1f1f8;🇺🇸1F1FA-1F1F8nosingle::🇺🇸single1🇺🇸 -11872&amp;#x1f91f;🤟1F91Fnosingle::🤟single1🤟 -11973&amp;#x1f641;🙁1F641nosingle::🙁single1🙁 -12074&amp;#x1f6a8;🚨1F6A8nosingle::🚨single1🚨 -12175&amp;#x1f4a8;💨1F4A8nosingle::💨single1💨 -12276&amp;#x1f92c;🤬1F92Cnosingle::🤬single1🤬 -12377&amp;#9992;&amp;#65039;✈️2708-FE0Fnosingle::✈️single1✈️ -12478&amp;#x1f380;🎀1F380nosingle::🎀single1🎀 -12579&amp;#x1f37a;🍺1F37Anosingle::🍺single1🍺 -126710&amp;#x1f913;🤓1F913nosingle::🤓single1🤓 -127711&amp;#x1f619;😙1F619nosingle::😙single1😙 -128712&amp;#x1f49f;💟1F49Fnosingle::💟single1💟 -129713&amp;#x1f331;🌱1F331nosingle::🌱single1🌱 -130714&amp;#x1f616;😖1F616nosingle::😖single1😖 -131715&amp;#x1f476;👶1F476nosingle::👶single1👶 -132716&amp;#9654;&amp;#65039;▶️25B6-FE0Fnosingle::▶️single1▶️ -133717&amp;#10145;&amp;#65039;➡️27A1-FE0Fnosingle::➡️single1➡️ -134718&amp;#10067;❓2753nosingle::❓single1❓ -135719&amp;#x1f48e;💎1F48Enosingle::💎single1💎 -136720&amp;#x1f4b8;💸1F4B8nosingle::💸single1💸 -137721&amp;#11015;&amp;#65039;⬇️2B07-FE0Fnosingle::⬇️single1⬇️ -138722&amp;#x1f628;😨1F628nosingle::😨single1😨 -139723&amp;#x1f31a;🌚1F31Anosingle::🌚single1🌚 -140724&amp;#x1f98b;🦋1F98Bnosingle::🦋single1🦋 -141725&amp;#x1f637;😷1F637nosingle::😷single1😷 -142726&amp;#x1f57a;🕺1F57Anosingle::🕺single1🕺 -143727&amp;#9888;&amp;#65039;⚠️26A0-FE0Fnosingle::⚠️single1⚠️ -144728&amp;#x1f645;🙅1F645nosingle::🙅single1🙅 -145729&amp;#x1f61f;😟1F61Fnosingle::😟single1😟 -146730&amp;#x1f635;😵1F635nosingle::😵single1😵 -147732&amp;#x1f932;🤲1F932nosingle::🤲single1🤲 -148733&amp;#x1f920;🤠1F920nosingle::🤠single1🤠 -149734&amp;#x1f927;🤧1F927nosingle::🤧single1🤧 -150735&amp;#x1f4cc;📌1F4CCnosingle::📌single1📌 -151736&amp;#x1f535;🔵1F535nosingle::🔵single1🔵 -152737&amp;#x1f485;💅1F485nosingle::💅single1💅 -153738&amp;#x1f9d0;🧐1F9D0nosingle::🧐single1🧐 -154739&amp;#x1f43e;🐾1F43Enosingle::🐾single1🐾 -155740&amp;#x1f352;🍒1F352nosingle::🍒single1🍒 -156741&amp;#x1f617;😗1F617nosingle::😗single1😗 -157742&amp;#x1f911;🤑1F911nosingle::🤑single1🤑 -158743&amp;#x1f680;🚀1F680nosingle::🚀single1🚀 -159744&amp;#x1f30a;🌊1F30Anosingle::🌊single1🌊 -160745&amp;#x1f92f;🤯1F92Fnosingle::🤯single1🤯 -161746&amp;#x1f437;🐷1F437nosingle::🐷single1🐷 -162747&amp;#9742;&amp;#65039;☎️260E-FE0Fnosingle::☎️single1☎️ -163748&amp;#x1f4a7;💧1F4A7nosingle::💧single1💧 -164749&amp;#x1f62f;😯1F62Fnosingle::😯single1😯 -165750&amp;#x1f486;💆1F486nosingle::💆single1💆 -166751&amp;#x1f446;👆1F446nosingle::👆single1👆 -167752&amp;#x1f3a4;🎤1F3A4nosingle::🎤single1🎤 -168753&amp;#x1f647;🙇1F647nosingle::🙇single1🙇 -169754&amp;#x1f351;🍑1F351nosingle::🍑single1🍑 -170755&amp;#10052;&amp;#65039;❄️2744-FE0Fnosingle::❄️single1❄️ -171756&amp;#x1f334;🌴1F334nosingle::🌴single1🌴 -172757&amp;#x1f1e7;&amp;#x1f1f7;🇧🇷1F1E7-1F1F7nosingle::🇧🇷single1🇧🇷 -173758&amp;#x1f4a3;💣1F4A3nosingle::💣single1💣 -174759&amp;#x1f438;🐸1F438nosingle::🐸single1🐸 -175760&amp;#x1f48c;💌1F48Cnosingle::💌single1💌 -176761&amp;#x1f4cd;📍1F4CDnosingle::📍single1📍 -177762&amp;#x1f940;🥀1F940nosingle::🥀single1🥀 -178763&amp;#x1f922;🤢1F922nosingle::🤢single1🤢 -179764&amp;#x1f445;👅1F445nosingle::👅single1👅 -180765&amp;#x1f4a1;💡1F4A1nosingle::💡single1💡 -181766&amp;#x1f4a9;💩1F4A9nosingle::💩single1💩 -182767&amp;#8265;&amp;#65039;⁉️2049-FE0Fnosingle::⁉️single1⁉️ -183768&amp;#x1f450;👐1F450nosingle::👐single1👐 -184769&amp;#x1f4f8;📸1F4F8nosingle::📸single1📸 -185770&amp;#x1f47b;👻1F47Bnosingle::👻single1👻 -186771&amp;#x1f910;🤐1F910nosingle::🤐single1🤐 -187772&amp;#x1f92e;🤮1F92Enosingle::🤮single1🤮 -188773&amp;#x1f3bc;🎼1F3BCnosingle::🎼single1🎼 -189774&amp;#9997;&amp;#65039;✍️270D-FE0Fnosingle::✍️single1✍️ -190775&amp;#x1f6a9;🚩1F6A9nosingle::🚩single1🚩 -191776&amp;#x1f34e;🍎1F34Enosingle::🍎single1🍎 -192777&amp;#x1f34a;🍊1F34Anosingle::🍊single1🍊 -193778&amp;#x1f47c;👼1F47Cnosingle::👼single1👼 -194779&amp;#x1f48d;💍1F48Dnosingle::💍single1💍 -195780&amp;#x1f4e3;📣1F4E3nosingle::📣single1📣 -196781&amp;#x1f942;🥂1F942nosingle::🥂single1🥂 -197782&amp;#10549;&amp;#65039;⤵️2935-FE0Fnosingle::⤵️single1⤵️ -198783&amp;#x1f4f1;📱1F4F1nosingle::📱single1📱 -199784&amp;#9748;☔2614nosingle::☔single1☔ -200785&amp;#x1f319;🌙1F319nosingle::🌙single1🌙 -20181&amp;#x1f37e;🍾1F37Enosingle::🍾single1🍾 -20282&amp;#x1f3a7;🎧1F3A7nosingle::🎧single1🎧 -20383&amp;#x1f341;🍁1F341nosingle::🍁single1🍁 -20484&amp;#11093;⭕2B55nosingle::⭕single1⭕ -20585&amp;#x1f3c0;🏀1F3C0nosingle::🏀single1🏀 -20686&amp;#9760;&amp;#65039;☠️2620-FE0Fnosingle::☠️single1☠️ -20787&amp;#9899;⚫26ABnosingle::⚫single1⚫ -20888&amp;#x1f590;&amp;#65039;🖐️1F590-FE0Fnosingle::🖐️single1🖐️ -20989&amp;#x1f627;😧1F627nosingle::😧single1😧 -210810&amp;#x1f3af;🎯1F3AFnosingle::🎯single1🎯 -211811&amp;#x1f4f2;📲1F4F2nosingle::📲single1📲 -212812&amp;#9752;&amp;#65039;☘️2618-FE0Fnosingle::☘️single1☘️ -213813&amp;#x1f441;&amp;#65039;👁️1F441-FE0Fnosingle::👁️single1👁️ -214814&amp;#x1f377;🍷1F377nosingle::🍷single1🍷 -215815&amp;#x1f444;👄1F444nosingle::👄single1👄 -216816&amp;#x1f41f;🐟1F41Fnosingle::🐟single1🐟 -217817&amp;#x1f370;🍰1F370nosingle::🍰single1🍰 -218818&amp;#x1f4a4;💤1F4A4nosingle::💤single1💤 -219819&amp;#x1f54a;&amp;#65039;🕊️1F54A-FE0Fnosingle::🕊️single1🕊️ -220820&amp;#x1f4fa;📺1F4FAnosingle::📺single1📺 -221821&amp;#x1f4ad;💭1F4ADnosingle::💭single1💭 -222822&amp;#x1f431;🐱1F431nosingle::🐱single1🐱 -223823&amp;#x1f41d;🐝1F41Dnosingle::🐝single1🐝 -224824&amp;#x1f1f2;&amp;#x1f1fd;🇲🇽1F1F2-1F1FDnosingle::🇲🇽single1🇲🇽 -225825&amp;#x1f9da;🧚1F9DAnosingle::🧚single1🧚 -226826&amp;#x1f51d;🔝1F51Dnosingle::🔝single1🔝 -227827&amp;#x1f4e2;📢1F4E2nosingle::📢single1📢 -228828&amp;#x1f4f7;📷1F4F7nosingle::📷single1📷 -229829&amp;#x1f415;🐕1F415nosingle::🐕single1🐕 -230830&amp;#x1f3b8;🎸1F3B8nosingle::🎸single1🎸 -231831&amp;#x1f52b;🔫1F52Bnosingle::🔫single1🔫 -232832&amp;#x1f91a;🤚1F91Anosingle::🤚single1🤚 -233833&amp;#x1f36d;🍭1F36Dnosingle::🍭single1🍭 -234834&amp;#x1f346;🍆1F346nosingle::🍆single1🍆 -235835&amp;#x1f489;💉1F489nosingle::💉single1💉 -236836&amp;#x1f30e;🌎1F30Enosingle::🌎single1🌎 -237837&amp;#x1f626;😦1F626nosingle::😦single1😦 -238838&amp;#x1f300;🌀1F300nosingle::🌀single1🌀 -239839&amp;#x1f47f;👿1F47Fnosingle::👿single1👿 -240840&amp;#9745;&amp;#65039;☑️2611-FE0Fnosingle::☑️single1☑️ -241841&amp;#x1f3a5;🎥1F3A5nosingle::🎥single1🎥 -242842&amp;#x1f327;&amp;#65039;🌧️1F327-FE0Fnosingle::🌧️single1🌧️ -243843&amp;#x1f47d;👽1F47Dnosingle::👽single1👽 -244844&amp;#x1f34b;🍋1F34Bnosingle::🍋single1🍋 -245845&amp;#x1f912;🤒1F912nosingle::🤒single1🤒 -246846&amp;#x1f921;🤡1F921nosingle::🤡single1🤡 -247847&amp;#x1f36b;🍫1F36Bnosingle::🍫single1🍫 -248848&amp;#x1f4da;📚1F4DAnosingle::📚single1📚 -249849&amp;#x1f3c1;🏁1F3C1nosingle::🏁single1🏁 -250850&amp;#x1f915;🤕1F915nosingle::🤕single1🤕 -251851&amp;#x1f984;🦄1F984nosingle::🦄single1🦄 -252852&amp;#x1f345;🍅1F345nosingle::🍅single1🍅 -253853&amp;#x1f697;🚗1F697nosingle::🚗single1🚗 -254854&amp;#x1f6ab;🚫1F6ABnosingle::🚫single1🚫 -255855&amp;#x1f4b5;💵1F4B5nosingle::💵single1💵 -256856&amp;#9918;⚾26BEnosingle::⚾single1⚾ -257857&amp;#x1f52a;🔪1F52Anosingle::🔪single1🔪 -258858&amp;#x1f514;🔔1F514nosingle::🔔single1🔔 -259859&amp;#9832;&amp;#65039;♨️2668-FE0Fnosingle::♨️single1♨️ -260860&amp;#x1f333;🌳1F333nosingle::🌳single1🌳 -261861&amp;#x1f50a;🔊1F50Anosingle::🔊single1🔊 -262862&amp;#x1f36c;🍬1F36Cnosingle::🍬single1🍬 -263863&amp;#x1f48f;💏1F48Fnosingle::💏single1💏 -264864&amp;#x1f37c;🍼1F37Cnosingle::🍼single1🍼 -265865&amp;#x1f35c;🍜1F35Cnosingle::🍜single1🍜 -266866&amp;#x1f43c;🐼1F43Cnosingle::🐼single1🐼 -267867&amp;#x1f649;🙉1F649nosingle::🙉single1🙉 -268868&amp;#x1f408;🐈1F408nosingle::🐈single1🐈 -269869&amp;#x1f43b;🐻1F43Bnosingle::🐻single1🐻 -270870&amp;#x1f938;🤸1F938nosingle::🤸single1🤸 -271871&amp;#x1f31d;🌝1F31Dnosingle::🌝single1🌝 -272872&amp;#x1f478;👸1F478nosingle::👸single1👸 -273873&amp;#x1f355;🍕1F355nosingle::🍕single1🍕 -274874&amp;#x1f34c;🍌1F34Cnosingle::🍌single1🍌 -275875&amp;#x1f366;🍦1F366nosingle::🍦single1🍦 -276876&amp;#9898;⚪26AAnosingle::⚪single1⚪ -277877&amp;#x1f469;👩1F469nosingle::👩single1👩 -278878&amp;#x1f63f;😿1F63Fnosingle::😿single1😿 -279879&amp;#x1f342;🍂1F342nosingle::🍂single1🍂 -280880&amp;#x1f4de;📞1F4DEnosingle::📞single1📞 -281881&amp;#9200;⏰23F0nosingle::⏰single1⏰ -282882&amp;#x1f51e;🔞1F51Enosingle::🔞single1🔞 -283883&amp;#x1f30d;🌍1F30Dnosingle::🌍single1🌍 -284884&amp;#x1f320;🌠1F320nosingle::🌠single1🌠 -285885&amp;#x1f640;🙀1F640nosingle::🙀single1🙀 -286886&amp;#9642;&amp;#65039;▪️25AA-FE0Fnosingle::▪️single1▪️ -287887&amp;#9729;&amp;#65039;☁️2601-FE0Fnosingle::☁️single1☁️ -288888&amp;#x1f479;👹1F479nosingle::👹single1👹 -289889&amp;#x1f349;🍉1F349nosingle::🍉single1🍉 -290890&amp;#x1f425;🐥1F425nosingle::🐥single1🐥 -291891&amp;#x1f336;&amp;#65039;🌶️1F336-FE0Fnosingle::🌶️single1🌶️ -2928921&amp;#65039;&amp;#8419;1️⃣31-FE0F-20E3nosingle::1️⃣single11️⃣ -293893&amp;#x1f335;🌵1F335nosingle::🌵single1🌵 -294894&amp;#x1f1ee;&amp;#x1f1f3;🇮🇳1F1EE-1F1F3nosingle::🇮🇳single1🇮🇳 -295895&amp;#x1f467;👧1F467nosingle::👧single1👧 -296896&amp;#x1f344;🍄1F344nosingle::🍄single1🍄 -297897&amp;#x1f46e;👮1F46Enosingle::👮single1👮 -298898&amp;#x1f4ae;💮1F4AEnosingle::💮single1💮 -299899&amp;#x1f430;🐰1F430nosingle::🐰single1🐰 -3008100&amp;#x1f537;🔷1F537nosingle::🔷single1🔷 -3018101&amp;#x1f33e;🌾1F33Enosingle::🌾single1🌾 -3028102&amp;#x1f539;🔹1F539nosingle::🔹single1🔹 -3038103&amp;#x1f1f9;&amp;#x1f1f7;🇹🇷1F1F9-1F1F7nosingle::🇹🇷single1🇹🇷 -3048104&amp;#x1f947;🥇1F947nosingle::🥇single1🥇 -3058105&amp;#x1f1ee;&amp;#x1f1f9;🇮🇹1F1EE-1F1F9nosingle::🇮🇹single1🇮🇹 -30691&amp;#x1f36a;🍪1F36Anosingle::🍪single1🍪 -30792&amp;#x1f1e6;&amp;#x1f1f7;🇦🇷1F1E6-1F1F7nosingle::🇦🇷single1🇦🇷 -30893&amp;#x1f6d1;🛑1F6D1nosingle::🛑single1🛑 -30994&amp;#x1f40d;🐍1F40Dnosingle::🐍single1🐍 -31095&amp;#x1f393;🎓1F393nosingle::🎓single1🎓 -31196&amp;#x1f1e8;&amp;#x1f1e6;🇨🇦1F1E8-1F1E6nosingle::🇨🇦single1🇨🇦 -31297&amp;#x1f34f;🍏1F34Fnosingle::🍏single1🍏 -31398&amp;#x1f981;🦁1F981nosingle::🦁single1🦁 -31499&amp;#x1f63d;😽1F63Dnosingle::😽single1😽 -315910&amp;#x1f6ac;🚬1F6ACnosingle::🚬single1🚬 -316911&amp;#x1f356;🍖1F356nosingle::🍖single1🍖 -317912&amp;#x1f374;🍴1F374nosingle::🍴single1🍴 -318913&amp;#x1f198;🆘1F198nosingle::🆘single1🆘 -319914&amp;#x1f91c;🤜1F91Cnosingle::🤜single1🤜 -320915&amp;#x1f37f;🍿1F37Fnosingle::🍿single1🍿 -321916&amp;#x1f354;🍔1F354nosingle::🍔single1🍔 -322917&amp;#x1f4dd;📝1F4DDnosingle::📝single1📝 -323918&amp;#x1f1ef;&amp;#x1f1f5;🇯🇵1F1EF-1F1F5nosingle::🇯🇵single1🇯🇵 -324919&amp;#x1f36e;🍮1F36Enosingle::🍮single1🍮 -325920&amp;#x1f347;🍇1F347nosingle::🍇single1🍇 -3269212&amp;#65039;&amp;#8419;2️⃣32-FE0F-20E3nosingle::2️⃣single12️⃣ -327922&amp;#x1f3e0;🏠1F3E0nosingle::🏠single1🏠 -328923&amp;#x1f930;🤰1F930nosingle::🤰single1🤰 -329924&amp;#x1f423;🐣1F423nosingle::🐣single1🐣 -330925&amp;#x1f412;🐒1F412nosingle::🐒single1🐒 -331926&amp;#x1f466;👦1F466nosingle::👦single1👦 -332927&amp;#x1f369;🍩1F369nosingle::🍩single1🍩 -333928&amp;#x1f363;🍣1F363nosingle::🍣single1🍣 -334929&amp;#x1f91b;🤛1F91Bnosingle::🤛single1🤛 -335930&amp;#x1f46f;👯1F46Fnosingle::👯single1👯 -336931&amp;#x1f3f3;&amp;#65039;&amp;zwj;&amp;#x1f308;🏳️‍🌈1F3F3-FE0F-200D-1F308nosingle::🏳️‍🌈single1🏳️‍🌈 -337932&amp;spades;&amp;#65039;♠️2660-FE0Fnosingle::♠️single1♠️ -338933&amp;#x1f332;🌲1F332nosingle::🌲single1🌲 -339934&amp;#x1f434;🐴1F434nosingle::🐴single1🐴 -340935&amp;#x1f35b;🍛1F35Bnosingle::🍛single1🍛 -341936&amp;#x1f386;🎆1F386nosingle::🎆single1🎆 -342937&amp;#x1f491;💑1F491nosingle::💑single1💑 -343938&amp;#x1f35e;🍞1F35Enosingle::🍞single1🍞 -344939&amp;#x1f36f;🍯1F36Fnosingle::🍯single1🍯 -345940&amp;#9732;&amp;#65039;☄️2604-FE0Fnosingle::☄️single1☄️ -346941&amp;#x1f638;😸1F638nosingle::😸single1😸 -347942&amp;#x1f35a;🍚1F35Anosingle::🍚single1🍚 -348943&amp;#x1f3ac;🎬1F3ACnosingle::🎬single1🎬 -349944&amp;#x1f399;&amp;#65039;🎙️1F399-FE0Fnosingle::🎙️single1🎙️ -350945&amp;#x1f1e8;&amp;#x1f1f4;🇨🇴1F1E8-1F1F4nosingle::🇨🇴single1🇨🇴 -351946&amp;#x1f433;🐳1F433nosingle::🐳single1🐳 -352947&amp;#x1f980;🦀1F980nosingle::🦀single1🦀 -353948&amp;#x1f943;🥃1F943nosingle::🥃single1🥃 -354949&amp;#x1f538;🔸1F538nosingle::🔸single1🔸 -355950&amp;#x1f48a;💊1F48Anosingle::💊single1💊 -356951&amp;#x1f40e;🐎1F40Enosingle::🐎single1🐎 -357952&amp;#x1f379;🍹1F379nosingle::🍹single1🍹 -358953&amp;diams;&amp;#65039;♦️2666-FE0Fnosingle::♦️single1♦️ -359954&amp;#x1f52e;🔮1F52Enosingle::🔮single1🔮 -360955&amp;#x1f468;👨1F468nosingle::👨single1👨 -361956&amp;#x1f378;🍸1F378nosingle::🍸single1🍸 -362957&amp;#x1f30f;🌏1F30Fnosingle::🌏single1🌏 -363958&amp;#x1f474;👴1F474nosingle::👴single1👴 -364959&amp;#x1f9e2;🧢1F9E2nosingle::🧢single1🧢 -365960&amp;#x1f43d;🐽1F43Dnosingle::🐽single1🐽 -366961&amp;#x1f414;🐔1F414nosingle::🐔single1🐔 -367962&amp;#x1f3bb;🎻1F3BBnosingle::🎻single1🎻 -368963&amp;#11014;&amp;#65039;⬆️2B06-FE0Fnosingle::⬆️single1⬆️ -369964&amp;#9986;&amp;#65039;✂️2702-FE0Fnosingle::✂️single1✂️ -370965&amp;#x1f46b;👫1F46Bnosingle::👫single1👫 -371966&amp;#x1f463;👣1F463nosingle::👣single1👣 -372967&amp;#x1f42f;🐯1F42Fnosingle::🐯single1🐯 -373968&amp;#x1f3ae;🎮1F3AEnosingle::🎮single1🎮 -374969&amp;#x1f375;🍵1F375nosingle::🍵single1🍵 -375970&amp;#x1f426;🐦1F426nosingle::🐦single1🐦 -376971&amp;#x1f1ec;&amp;#x1f1e7;🇬🇧1F1EC-1F1E7nosingle::🇬🇧single1🇬🇧 -377972&amp;#12336;&amp;#65039;〰️3030-FE0Fnosingle::〰️single1〰️ -378973&amp;#x1f46d;👭1F46Dnosingle::👭single1👭 -379974&amp;#x1f42c;🐬1F42Cnosingle::🐬single1🐬 -380975&amp;#x1f35f;🍟1F35Fnosingle::🍟single1🍟 -381976&amp;#x1f459;👙1F459nosingle::👙single1👙 -382977&amp;#10006;&amp;#65039;✖️2716-FE0Fnosingle::✖️single1✖️ -383978&amp;#x1f4e9;📩1F4E9nosingle::📩single1📩 -384979&amp;#x1f475;👵1F475nosingle::👵single1👵 -385980&amp;#x1f368;🍨1F368nosingle::🍨single1🍨 -386981&amp;#x1f1eb;&amp;#x1f1f7;🇫🇷1F1EB-1F1F7nosingle::🇫🇷single1🇫🇷 -387982&amp;#x1f416;🐖1F416nosingle::🐖single1🐖 -388983&amp;#10013;&amp;#65039;✝️271D-FE0Fnosingle::✝️single1✝️ -389984&amp;#9851;&amp;#65039;♻️267B-FE0Fnosingle::♻️single1♻️ -390985&amp;#x1f94a;🥊1F94Anosingle::🥊single1🥊 -391986&amp;#x1f985;🦅1F985nosingle::🦅single1🦅 -392987&amp;#x1f4ac;💬1F4ACnosingle::💬single1💬 -393988&amp;#x1f1e8;&amp;#x1f1f1;🇨🇱1F1E8-1F1F1nosingle::🇨🇱single1🇨🇱 -394989&amp;#x1f422;🐢1F422nosingle::🐢single1🐢 -395990&amp;#x1f530;🔰1F530nosingle::🔰single1🔰 -396991&amp;#x1f536;🔶1F536nosingle::🔶single1🔶 -397992&amp;#x1f397;&amp;#65039;🎗️1F397-FE0Fnosingle::🎗️single1🎗️ -398993&amp;#x1f484;💄1F484nosingle::💄single1💄 -399994&amp;#x1f460;👠1F460nosingle::👠single1👠 -400995&amp;#x1f955;🥕1F955nosingle::🥕single1🥕 -401996&amp;#10134;➖2796nosingle::➖single1➖ -402997&amp;#x1f43a;🐺1F43Anosingle::🐺single1🐺 -403998&amp;#x1f4d6;📖1F4D6nosingle::📖single1📖 -404999&amp;#x1f34d;🍍1F34Dnosingle::🍍single1🍍 -4059100&amp;#x1f303;🌃1F303nosingle::🌃single1🌃 -4069101&amp;#10036;&amp;#65039;✴️2734-FE0Fnosingle::✴️single1✴️ -4079102&amp;#x1f30c;🌌1F30Cnosingle::🌌single1🌌 -4089103&amp;#x1f413;🐓1F413nosingle::🐓single1🐓 -4099104&amp;#x1f442;👂1F442nosingle::👂single1👂 -4109105&amp;#x1f364;🍤1F364nosingle::🍤single1🍤 -4119106&amp;#x1f410;🐐1F410nosingle::🐐single1🐐 -4129107&amp;#x1f53b;🔻1F53Bnosingle::🔻single1🔻 -4139108&amp;#x1f4bb;💻1F4BBnosingle::💻single1💻 -4149109&amp;#x1f990;🦐1F990nosingle::🦐single1🦐 -4159110&amp;#x1f1e9;&amp;#x1f1ea;🇩🇪1F1E9-1F1EAnosingle::🇩🇪single1🇩🇪 -4169111&amp;#x1f31b;🌛1F31Bnosingle::🌛single1🌛 -4179112&amp;#8600;&amp;#65039;↘️2198-FE0Fnosingle::↘️single1↘️ -4189113&amp;#9999;&amp;#65039;✏️270F-FE0Fnosingle::✏️single1✏️ -4199114&amp;#x1f9d8;🧘1F9D8nosingle::🧘single1🧘 -4209115&amp;#x1f941;🥁1F941nosingle::🥁single1🥁 -4219116&amp;#x1f3d6;&amp;#65039;🏖️1F3D6-FE0Fnosingle::🏖️single1🏖️ -4229117&amp;#9884;&amp;#65039;⚜️269C-FE0Fnosingle::⚜️single1⚜️ -4239118&amp;#10069;❕2755nosingle::❕single1❕ -4249119&amp;#x1f170;&amp;#65039;🅰️1F170-FE0Fnosingle::🅰️single1🅰️ -4259120&amp;#x1f6b4;🚴1F6B4nosingle::🚴single1🚴 -4269121&amp;#x1f4a0;💠1F4A0nosingle::💠single1💠 -4279122&amp;#12951;&amp;#65039;㊗️3297-FE0Fnosingle::㊗️single1㊗️ -4289123&amp;#x1f419;🐙1F419nosingle::🐙single1🐙 -4299124&amp;clubs;&amp;#65039;♣️2663-FE0Fnosingle::♣️single1♣️ -4309125&amp;#x1f361;🍡1F361nosingle::🍡single1🍡 -4319126&amp;#9193;⏩23E9nosingle::⏩single1⏩ -4329127&amp;#x1f3a8;🎨1F3A8nosingle::🎨single1🎨 -4339128&amp;#x1f420;🐠1F420nosingle::🐠single1🐠 -4349129&amp;#x1f1f0;&amp;#x1f1f7;🇰🇷1F1F0-1F1F7nosingle::🇰🇷single1🇰🇷 -4359130&amp;#x1f357;🍗1F357nosingle::🍗single1🍗 -4369131&amp;#x1f6ae;🚮1F6AEnosingle::🚮single1🚮 -4379132&amp;#9643;&amp;#65039;▫️25AB-FE0Fnosingle::▫️single1▫️ -4389133&amp;#x1f32a;&amp;#65039;🌪️1F32A-FE0Fnosingle::🌪️single1🌪️ -4399134&amp;#x1f63c;😼1F63Cnosingle::😼single1😼 -4409135&amp;#x1f464;👤1F464nosingle::👤single1👤 -4419136&amp;#x1f3ca;🏊1F3CAnosingle::🏊single1🏊 -4429137&amp;#x1f33d;🌽1F33Dnosingle::🌽single1🌽 -4439138&amp;#x1f3a9;🎩1F3A9nosingle::🎩single1🎩 -4449139&amp;#x1f1ea;&amp;#x1f1f8;🇪🇸1F1EA-1F1F8nosingle::🇪🇸single1🇪🇸 -4459140&amp;#x1f3b9;🎹1F3B9nosingle::🎹single1🎹 -4469141&amp;#x1f348;🍈1F348nosingle::🍈single1🍈 -4479142&amp;#9664;&amp;#65039;◀️25C0-FE0Fnosingle::◀️single1◀️ -4489143&amp;harr;&amp;#65039;↔️2194-FE0Fnosingle::↔️single1↔️ -4499144&amp;#x1f3e1;🏡1F3E1nosingle::🏡single1🏡 -4509145&amp;#x1f1f5;&amp;#x1f1f0;🇵🇰1F1F5-1F1F0nosingle::🇵🇰single1🇵🇰 -4519146&amp;#x1f387;🎇1F387nosingle::🎇single1🎇 -4529147&amp;#x1f969;🥩1F969nosingle::🥩single1🥩 -4539148&amp;#x1f41e;🐞1F41Enosingle::🐞single1🐞 -45491493&amp;#65039;&amp;#8419;3️⃣33-FE0F-20E3nosingle::3️⃣single13️⃣ -4559150&amp;#11013;&amp;#65039;⬅️2B05-FE0Fnosingle::⬅️single1⬅️ -4569151&amp;#x1f310;🌐1F310nosingle::🌐single1🌐 -4579152&amp;#8599;&amp;#65039;↗️2197-FE0Fnosingle::↗️single1↗️ -4589153&amp;#x1f37d;&amp;#65039;🍽️1F37D-FE0Fnosingle::🍽️single1🍽️ -4599154&amp;#x1f9c0;🧀1F9C0nosingle::🧀single1🧀 -4609155&amp;#x1f966;🥦1F966nosingle::🥦single1🥦 -4619156&amp;#x1f41c;🐜1F41Cnosingle::🐜single1🐜 -4629157&amp;#9876;&amp;#65039;⚔️2694-FE0Fnosingle::⚔️single1⚔️ -463101&amp;#x1f63a;😺1F63Anosingle::😺single1😺 -464102&amp;#x1f95e;🥞1F95Enosingle::🥞single1🥞 -465103&amp;#x1f3c4;🏄1F3C4nosingle::🏄single1🏄 -466104&amp;#x1f528;🔨1F528nosingle::🔨single1🔨 -467105&amp;#x1f3dd;&amp;#65039;🏝️1F3DD-FE0Fnosingle::🏝️single1🏝️ -468106&amp;#x1f506;🔆1F506nosingle::🔆single1🔆 -469107&amp;#x1f465;👥1F465nosingle::👥single1👥 -470108&amp;#x1f453;👓1F453nosingle::👓single1👓 -471109&amp;#x1f952;🥒1F952nosingle::🥒single1🥒 -4721010&amp;#x1f3c8;🏈1F3C8nosingle::🏈single1🏈 -4731011&amp;#x1f1f5;&amp;#x1f1ed;🇵🇭1F1F5-1F1EDnosingle::🇵🇭single1🇵🇭 -4741012&amp;#x1f3cb;&amp;#65039;🏋️1F3CB-FE0Fnosingle::🏋️single1🏋️ -47510130&amp;#65039;&amp;#8419;0️⃣30-FE0F-20E3nosingle::0️⃣single10️⃣ -4761014&amp;#x1f698;🚘1F698nosingle::🚘single1🚘 -4771015&amp;#x1f996;🦖1F996nosingle::🦖single1🦖 -4781016&amp;#x1f315;🌕1F315nosingle::🌕single1🌕 -4791017&amp;#x1f3ad;🎭1F3ADnosingle::🎭single1🎭 -4801018&amp;#x1f47e;👾1F47Enosingle::👾single1👾 -4811019&amp;#x1f373;🍳1F373nosingle::🍳single1🍳 -4821020&amp;#x1f3f5;&amp;#65039;🏵️1F3F5-FE0Fnosingle::🏵️single1🏵️ -4831021&amp;#x1f367;🍧1F367nosingle::🍧single1🍧 -4841022&amp;#x1f517;🔗1F517nosingle::🔗single1🔗 -4851023&amp;#x1f54b;🕋1F54Bnosingle::🕋single1🕋 -4861024&amp;#9731;&amp;#65039;☃️2603-FE0Fnosingle::☃️single1☃️ -4871025&amp;#x1f305;🌅1F305nosingle::🌅single1🌅 -4881026&amp;#x1f934;🤴1F934nosingle::🤴single1🤴 -4891027&amp;#x1f596;🖖1F596nosingle::🖖single1🖖 -4901028&amp;#x1f40a;🐊1F40Anosingle::🐊single1🐊 -4911029&amp;#x1f418;🐘1F418nosingle::🐘single1🐘 -4921030&amp;#x1f324;&amp;#65039;🌤️1F324-FE0Fnosingle::🌤️single1🌤️ -4931031&amp;#x1f951;🥑1F951nosingle::🥑single1🥑 -4941032&amp;#x1f95a;🥚1F95Anosingle::🥚single1🥚 -4951033&amp;#9928;&amp;#65039;⛈️26C8-FE0Fnosingle::⛈️single1⛈️ -4961034&amp;#x1f435;🐵1F435nosingle::🐵single1🐵 -4971035&amp;#x1f51c;🔜1F51Cnosingle::🔜single1🔜 -4981036&amp;#x1f376;🍶1F376nosingle::🍶single1🍶 -4991037&amp;#x1f404;🐄1F404nosingle::🐄single1🐄 -5001038&amp;#x1f1fb;&amp;#x1f1ea;🇻🇪1F1FB-1F1EAnosingle::🇻🇪single1🇻🇪 -5011039&amp;#x1f42e;🐮1F42Enosingle::🐮single1🐮 -5021040&amp;#x1f988;🦈1F988nosingle::🦈single1🦈 -5031041&amp;#x1f6b2;🚲1F6B2nosingle::🚲single1🚲 -5041042&amp;#9940;⛔26D4nosingle::⛔single1⛔ -5051043&amp;#x1f56f;&amp;#65039;🕯️1F56F-FE0Fnosingle::🕯️single1🕯️ -5061044&amp;#10133;➕2795nosingle::➕single1➕ -5071045&amp;#x1f53a;🔺1F53Anosingle::🔺single1🔺 -5081046&amp;#x1f487;💇1F487nosingle::💇single1💇 -5091047&amp;#x1f9e0;🧠1F9E0nosingle::🧠single1🧠 -5101048&amp;#x1f4fb;📻1F4FBnosingle::📻single1📻 -5111049&amp;#x1f964;🥤1F964nosingle::🥤single1🥤 -5121050&amp;#x1f35d;🍝1F35Dnosingle::🍝single1🍝 -5131051&amp;#x1f365;🍥1F365nosingle::🍥single1🍥 -5141052&amp;#x1f4b4;💴1F4B4nosingle::💴single1💴 -5151053&amp;#x1f32c;&amp;#65039;🌬️1F32C-FE0Fnosingle::🌬️single1🌬️ -5161054&amp;#x1f953;🥓1F953nosingle::🥓single1🥓 -5171055&amp;#x1f64d;🙍1F64Dnosingle::🙍single1🙍 -5181056&amp;#9875;⚓2693nosingle::⚓single1⚓ -5191057&amp;#x1f470;👰1F470nosingle::👰single1👰 -5201058&amp;#x1f402;🐂1F402nosingle::🐂single1🐂 -5211059&amp;#x1f4fd;&amp;#65039;📽️1F4FD-FE0Fnosingle::📽️single1📽️ -5221060&amp;#x1f3c5;🏅1F3C5nosingle::🏅single1🏅 -5231061&amp;#9925;⛅26C5nosingle::⛅single1⛅ -5241062&amp;#x1f1e6;&amp;#x1f1ea;🇦🇪1F1E6-1F1EAnosingle::🇦🇪single1🇦🇪 -5251063&amp;#x1f1f5;&amp;#x1f1ea;🇵🇪1F1F5-1F1EAnosingle::🇵🇪single1🇵🇪 -5261064&amp;#x1f9dc;🧜1F9DCnosingle::🧜single1🧜 -5271065&amp;#x1f4ee;📮1F4EEnosingle::📮single1📮 -5281066&amp;#9971;⛳26F3nosingle::⛳single1⛳ -5291067&amp;#x1f53d;🔽1F53Dnosingle::🔽single1🔽 -5301068&amp;#x1f682;🚂1F682nosingle::🚂single1🚂 -5311069&amp;#x1f3cc;&amp;#65039;🏌️1F3CC-FE0Fnosingle::🏌️single1🏌️ -5321070&amp;#x1f407;🐇1F407nosingle::🐇single1🐇 -5331071&amp;#x1f3cd;&amp;#65039;🏍️1F3CD-FE0Fnosingle::🏍️single1🏍️ -5341072&amp;#x1f3b2;🎲1F3B2nosingle::🎲single1🎲 -5351073&amp;#x1f95b;🥛1F95Bnosingle::🥛single1🥛 -5361074&amp;#x1f3a3;🎣1F3A3nosingle::🎣single1🎣 -5371075&amp;#x1f471;👱1F471nosingle::👱single1👱 -5381076&amp;#x1f38f;🎏1F38Fnosingle::🎏single1🎏 -5391077&amp;#x1f577;&amp;#65039;🕷️1F577-FE0Fnosingle::🕷️single1🕷️ -5401078&amp;#x1f98d;🦍1F98Dnosingle::🦍single1🦍 -5411079&amp;#x1f518;🔘1F518nosingle::🔘single1🔘 -5421080&amp;#x1f405;🐅1F405nosingle::🐅single1🐅 -5431081&amp;#x1f3c7;🏇1F3C7nosingle::🏇single1🏇 -5441082&amp;#x1f510;🔐1F510nosingle::🔐single1🔐 -5451083&amp;#x1f3e9;🏩1F3E9nosingle::🏩single1🏩 -5461084&amp;#x1f47a;👺1F47Anosingle::👺single1👺 -5471085&amp;#x1f171;&amp;#65039;🅱️1F171-FE0Fnosingle::🅱️single1🅱️ -5481086&amp;#x1f699;🚙1F699nosingle::🚙single1🚙 -5491087&amp;#x1f427;🐧1F427nosingle::🐧single1🐧 -5501088&amp;#9878;&amp;#65039;⚖️2696-FE0Fnosingle::⚖️single1⚖️ -5511089&amp;#x1f383;🎃1F383nosingle::🎃single1🎃 -5521090&amp;#x1f304;🌄1F304nosingle::🌄single1🌄 -5531091&amp;#x1f3be;🎾1F3BEnosingle::🎾single1🎾 -5541092&amp;#x1f41a;🐚1F41Anosingle::🐚single1🐚 -5551093&amp;#x1f3ba;🎺1F3BAnosingle::🎺single1🎺 -5561094&amp;#10055;&amp;#65039;❇️2747-FE0Fnosingle::❇️single1❇️ -5571095&amp;#x1f3ab;🎫1F3ABnosingle::🎫single1🎫 -5581096&amp;#8986;⌚231Anosingle::⌚single1⌚ -5591097&amp;#x1f30b;🌋1F30Bnosingle::🌋single1🌋 -5601098&amp;#x1f492;💒1F492nosingle::💒single1💒 -5611099&amp;#x1f473;👳1F473nosingle::👳single1👳 -56210100&amp;#10062;❎274Enosingle::❎single1❎ -56310101&amp;#x1f45f;👟1F45Fnosingle::👟single1👟 -56410102&amp;#x1f443;👃1F443nosingle::👃single1👃 -56510103&amp;#x1f6cc;🛌1F6CCnosingle::🛌single1🛌 -56610104&amp;#x1f693;🚓1F693nosingle::🚓single1🚓 -56710105&amp;#9196;⏬23ECnosingle::⏬single1⏬ -56810106&amp;#x1f4c8;📈1F4C8nosingle::📈single1📈 -56910107&amp;#9924;⛄26C4nosingle::⛄single1⛄ -57010108&amp;#9201;&amp;#65039;⏱️23F1-FE0Fnosingle::⏱️single1⏱️ -57110109&amp;#x1f63e;😾1F63Enosingle::😾single1😾 -57210110&amp;#x1f6eb;🛫1F6EBnosingle::🛫single1🛫 -57310111&amp;#x1f931;🤱1F931nosingle::🤱single1🤱 -57410112&amp;#x1f350;🍐1F350nosingle::🍐single1🍐 -57510113&amp;#9774;&amp;#65039;☮️262E-FE0Fnosingle::☮️single1☮️ -57610114&amp;#x1f683;🚃1F683nosingle::🚃single1🚃 -57710115&amp;#9203;⏳23F3nosingle::⏳single1⏳ -57810116&amp;#x1f31c;🌜1F31Cnosingle::🌜single1🌜 -57910117&amp;#x1f4f9;📹1F4F9nosingle::📹single1📹 -58010118&amp;#x1f41b;🐛1F41Bnosingle::🐛single1🐛 -58110119&amp;#x1f454;👔1F454nosingle::👔single1👔 -58210120&amp;#x1f457;👗1F457nosingle::👗single1👗 -58310121&amp;#x1f40c;🐌1F40Cnosingle::🐌single1🐌 -58410122&amp;#x1f3b1;🎱1F3B1nosingle::🎱single1🎱 -58510123&amp;#x1f330;🌰1F330nosingle::🌰single1🌰 -58610124&amp;#x1f32e;🌮1F32Enosingle::🌮single1🌮 -58710125&amp;#x1f575;&amp;#65039;🕵️1F575-FE0Fnosingle::🕵️single1🕵️ -58810126&amp;#x1f505;🔅1F505nosingle::🔅single1🔅 -58910127&amp;#9993;&amp;#65039;✉️2709-FE0Fnosingle::✉️single1✉️ -59010128&amp;#x1f1ea;&amp;#x1f1ec;🇪🇬1F1EA-1F1ECnosingle::🇪🇬single1🇪🇬 -59110129&amp;#x1f691;🚑1F691nosingle::🚑single1🚑 -59210130&amp;#x1f4e6;📦1F4E6nosingle::📦single1📦 -59310131&amp;#x1f925;🤥1F925nosingle::🤥single1🤥 -59410132&amp;#x1f504;🔄1F504nosingle::🔄single1🔄 -59510133&amp;#x1f933;🤳1F933nosingle::🤳single1🤳 -59610134&amp;#x1f4b2;💲1F4B2nosingle::💲single1💲 -59710135&amp;#x1f38b;🎋1F38Bnosingle::🎋single1🎋 -59810136&amp;#x1f5d3;&amp;#65039;🗓️1F5D3-FE0Fnosingle::🗓️single1🗓️ -59910137&amp;#x1f916;🤖1F916nosingle::🤖single1🤖 -60010138&amp;#x1f954;🥔1F954nosingle::🥔single1🥔 -60110139&amp;#x1f197;🆗1F197nosingle::🆗single1🆗 -60210140&amp;#x1f511;🔑1F511nosingle::🔑single1🔑 -60310141&amp;#x1f1e8;&amp;#x1f1f3;🇨🇳1F1E8-1F1F3nosingle::🇨🇳single1🇨🇳 -60410142&amp;#x1f424;🐤1F424nosingle::🐤single1🐤 -605101434&amp;#65039;&amp;#8419;4️⃣34-FE0F-20E3nosingle::4️⃣single14️⃣ -60610144&amp;#x1f411;🐑1F411nosingle::🐑single1🐑 -60710145&amp;#10160;➰27B0nosingle::➰single1➰ -60810146&amp;#x1f469;&amp;zwj;&amp;#x1f393;👩‍🎓1F469-200D-1F393nosingle::👩‍🎓single1👩‍🎓 -60910147&amp;#9730;&amp;#65039;☂️2602-FE0Fnosingle::☂️single1☂️ -61010148&amp;#x1f1e6;&amp;#x1f1f9;🇦🇹1F1E6-1F1F9nosingle::🇦🇹single1🇦🇹 -61110149&amp;#x1f986;🦆1F986nosingle::🦆single1🦆 -61210150&amp;#x1f68c;🚌1F68Cnosingle::🚌single1🚌 -61310151&amp;#x1f4bf;💿1F4BFnosingle::💿single1💿 -61410152&amp;#x1f3e5;🏥1F3E5nosingle::🏥single1🏥 -61510153&amp;#x1f40b;🐋1F40Bnosingle::🐋single1🐋 -61610154&amp;#x1f692;🚒1F692nosingle::🚒single1🚒 -61710155&amp;#x1f3d0;🏐1F3D0nosingle::🏐single1🏐 -61810156&amp;#x1f1ea;&amp;#x1f1e8;🇪🇨1F1EA-1F1E8nosingle::🇪🇨single1🇪🇨 -61910157&amp;#x1f950;🥐1F950nosingle::🥐single1🥐 -62010158&amp;#x1f3b7;🎷1F3B7nosingle::🎷single1🎷 -62110159&amp;#x1f5fd;🗽1F5FDnosingle::🗽single1🗽 -62210160&amp;#x1f5e1;&amp;#65039;🗡️1F5E1-FE0Fnosingle::🗡️single1🗡️ -62310161&amp;#x1f3cf;🏏1F3CFnosingle::🏏single1🏏 -62410162&amp;#x1f42d;🐭1F42Dnosingle::🐭single1🐭 -62510163&amp;#x1f64e;🙎1F64Enosingle::🙎single1🙎 -62610164&amp;#x1f311;🌑1F311nosingle::🌑single1🌑 -62710165&amp;#x1f694;🚔1F694nosingle::🚔single1🚔 -62810166&amp;#x1f1ee;&amp;#x1f1e9;🇮🇩1F1EE-1F1E9nosingle::🇮🇩single1🇮🇩 -62910167&amp;#x1f6bf;🚿1F6BFnosingle::🚿single1🚿 -63010168&amp;#x1f95d;🥝1F95Dnosingle::🥝single1🥝 -63110169&amp;#x1f54c;🕌1F54Cnosingle::🕌single1🕌 -63210170&amp;#x1f400;🐀1F400nosingle::🐀single1🐀 -63310171&amp;#x1f6e1;&amp;#65039;🛡️1F6E1-FE0Fnosingle::🛡️single1🛡️ -63410172&amp;#x1f512;🔒1F512nosingle::🔒single1🔒 -63510173&amp;#10035;&amp;#65039;✳️2733-FE0Fnosingle::✳️single1✳️ -63610174&amp;#x1f576;&amp;#65039;🕶️1F576-FE0Fnosingle::🕶️single1🕶️ -63710175&amp;#x1f469;&amp;zwj;&amp;#10084;&amp;#65039;&amp;zwj;&amp;#x1f48b;&amp;zwj;&amp;#x1f469;👩‍❤️‍💋‍👩1F469-200D-2764-FE0F-200D-1F48B-200D-1F469nosingle::👩‍❤️‍💋‍👩single1👩‍❤️‍💋‍👩 -63810176&amp;#x1f39f;&amp;#65039;🎟️1F39F-FE0Fnosingle::🎟️single1🎟️ -63910177&amp;#x1f409;🐉1F409nosingle::🐉single1🐉 -64010178&amp;#x1f531;🔱1F531nosingle::🔱single1🔱 -64110179&amp;#x1f50e;🔎1F50Enosingle::🔎single1🔎 -64210180&amp;#x1f1e6;&amp;#x1f1fa;🇦🇺1F1E6-1F1FAnosingle::🇦🇺single1🇦🇺 -64310181&amp;#9904;&amp;#65039;⚰️26B0-FE0Fnosingle::⚰️single1⚰️ -64410182&amp;#x1f429;🐩1F429nosingle::🐩single1🐩 -64510183&amp;#x1f991;🦑1F991nosingle::🦑single1🦑 -64610184&amp;#x1f9df;🧟1F9DFnosingle::🧟single1🧟 -64710185&amp;#x1f195;🆕1F195nosingle::🆕single1🆕 -64810186&amp;#x1f98a;🦊1F98Anosingle::🦊single1🦊 -64910187&amp;#x1f455;👕1F455nosingle::👕single1👕 -65010188&amp;#x1f3f9;🏹1F3F9nosingle::🏹single1🏹 -65110189&amp;#x1f1e9;&amp;#x1f1ff;🇩🇿1F1E9-1F1FFnosingle::🇩🇿single1🇩🇿 -65210190&amp;#x1f46c;👬1F46Cnosingle::👬single1👬 -65310191&amp;#x1f371;🍱1F371nosingle::🍱single1🍱 -65410192&amp;#x1f4f0;📰1F4F0nosingle::📰single1📰 -65510193&amp;#x1f94b;🥋1F94Bnosingle::🥋single1🥋 -65610194&amp;#x1f6a4;🚤1F6A4nosingle::🚤single1🚤 -65710195&amp;#x1f3f0;🏰1F3F0nosingle::🏰single1🏰 -658101965&amp;#65039;&amp;#8419;5️⃣35-FE0F-20E3nosingle::5️⃣single15️⃣ -65910197&amp;#x1f989;🦉1F989nosingle::🦉single1🦉 -66010198&amp;#x1f6a2;🚢1F6A2nosingle::🚢single1🚢 -66110199&amp;#x1f328;&amp;#65039;🌨️1F328-FE0Fnosingle::🌨️single1🌨️ -66210200&amp;#x1f4c6;📆1F4C6nosingle::📆single1📆 -66310201&amp;#x1f5dd;&amp;#65039;🗝️1F5DD-FE0Fnosingle::🗝️single1🗝️ -66410202&amp;#x1f38c;🎌1F38Cnosingle::🎌single1🎌 -66510203&amp;#x1f9d4;🧔1F9D4nosingle::🧔single1🧔 -66610204&amp;#x1f4b3;💳1F4B3nosingle::💳single1💳 -66710205&amp;#x1f1fa;&amp;#x1f1fe;🇺🇾1F1FA-1F1FEnosingle::🇺🇾single1🇺🇾 -66810206&amp;#x1f957;🥗1F957nosingle::🥗single1🥗 -66910207&amp;#9775;&amp;#65039;☯️262F-FE0Fnosingle::☯️single1☯️ -67010208&amp;#9881;&amp;#65039;⚙️2699-FE0Fnosingle::⚙️single1⚙️ -67110209&amp;#x1f4b6;💶1F4B6nosingle::💶single1💶 -67210210&amp;#9961;&amp;#65039;⛩️26E9-FE0Fnosingle::⛩️single1⛩️ -67310211&amp;#x1f5fb;🗻1F5FBnosingle::🗻single1🗻 -67410212&amp;#10002;&amp;#65039;✒️2712-FE0Fnosingle::✒️single1✒️ -67510213&amp;#x1f1fa;&amp;#x1f1f2;🇺🇲1F1FA-1F1F2nosingle::🇺🇲single1🇺🇲 -67610214&amp;#x1f1f5;&amp;#x1f1f9;🇵🇹1F1F5-1F1F9nosingle::🇵🇹single1🇵🇹 -67710215&amp;#x1f360;🍠1F360nosingle::🍠single1🍠 -678111&amp;#x1f477;👷1F477nosingle::👷single1👷 -679112&amp;#x1f6cd;&amp;#65039;🛍️1F6CD-FE0Fnosingle::🛍️single1🛍️ -680113&amp;#x1f3ce;&amp;#65039;🏎️1F3CE-FE0Fnosingle::🏎️single1🏎️ -681114&amp;#x1f51b;🔛1F51Bnosingle::🔛single1🔛 -682115&amp;#x1f519;🔙1F519nosingle::🔙single1🔙 -683116&amp;#x1f5fc;🗼1F5FCnosingle::🗼single1🗼 -684117&amp;#x1f396;&amp;#65039;🎖️1F396-FE0Fnosingle::🎖️single1🎖️ -685118&amp;#x1f6ba;🚺1F6BAnosingle::🚺single1🚺 -686119&amp;#x1f439;🐹1F439nosingle::🐹single1🐹 -6871110&amp;#x1f956;🥖1F956nosingle::🥖single1🥖 -6881111&amp;#x1f1f5;&amp;#x1f1f7;🇵🇷1F1F5-1F1F7nosingle::🇵🇷single1🇵🇷 -6891112&amp;#x1f3a1;🎡1F3A1nosingle::🎡single1🎡 -6901113&amp;#x1f50d;🔍1F50Dnosingle::🔍single1🔍 -6911114&amp;#x1f3df;&amp;#65039;🏟️1F3DF-FE0Fnosingle::🏟️single1🏟️ -6921115&amp;#x1f5a5;&amp;#65039;🖥️1F5A5-FE0Fnosingle::🖥️single1🖥️ -6931116&amp;#x1f32d;🌭1F32Dnosingle::🌭single1🌭 -6941117&amp;#x1f1f9;&amp;#x1f1ed;🇹🇭1F1F9-1F1EDnosingle::🇹🇭single1🇹🇭 -6951118&amp;#10017;&amp;#65039;✡️2721-FE0Fnosingle::✡️single1✡️ -6961119&amp;#x1f3d2;🏒1F3D2nosingle::🏒single1🏒 -6971120&amp;#x1f6c0;🛀1F6C0nosingle::🛀single1🛀 -6981121&amp;#x1f4c5;📅1F4C5nosingle::📅single1📅 -6991122&amp;#x1f58b;&amp;#65039;🖋️1F58B-FE0Fnosingle::🖋️single1🖋️ -7001123&amp;#x1f1f8;&amp;#x1f1ea;🇸🇪1F1F8-1F1EAnosingle::🇸🇪single1🇸🇪 -7011124&amp;#x1f39e;&amp;#65039;🎞️1F39E-FE0Fnosingle::🎞️single1🎞️ -7021125&amp;#x1f3aa;🎪1F3AAnosingle::🎪single1🎪 -7031126&amp;#x1f359;🍙1F359nosingle::🍙single1🍙 -7041127&amp;#x1f6c1;🛁1F6C1nosingle::🛁single1🛁 -7051128&amp;#x1f4d5;📕1F4D5nosingle::📕single1📕 -7061129&amp;#x1f452;👒1F452nosingle::👒single1👒 -7071130&amp;#x1f587;&amp;#65039;🖇️1F587-FE0Fnosingle::🖇️single1🖇️ -7081131&amp;#10068;❔2754nosingle::❔single1❔ -7091132&amp;#x1f384;🎄1F384nosingle::🎄single1🎄 -7101133&amp;#x1f578;&amp;#65039;🕸️1F578-FE0Fnosingle::🕸️single1🕸️ -7111134&amp;#x1f95c;🥜1F95Cnosingle::🥜single1🥜 -7121135&amp;#x1f574;&amp;#65039;🕴️1F574-FE0Fnosingle::🕴️single1🕴️ -7131136&amp;#x1f95f;🥟1F95Fnosingle::🥟single1🥟 -7141137&amp;#8601;&amp;#65039;↙️2199-FE0Fnosingle::↙️single1↙️ -7151138&amp;#9726;◾25FEnosingle::◾single1◾ -7161139&amp;#x1f432;🐲1F432nosingle::🐲single1🐲 -7171140&amp;#x1f3f4;🏴1F3F4nosingle::🏴single1🏴 -7181141&amp;#x1f98c;🦌1F98Cnosingle::🦌single1🦌 -7191142&amp;#x1f194;🆔1F194nosingle::🆔single1🆔 -7201143&amp;#x1f45b;👛1F45Bnosingle::👛single1👛 -7211144&amp;#x1f69a;🚚1F69Anosingle::🚚single1🚚 -7221145&amp;#8618;&amp;#65039;↪️21AA-FE0Fnosingle::↪️single1↪️ -7231146&amp;#x1f1ec;&amp;#x1f1f7;🇬🇷1F1EC-1F1F7nosingle::🇬🇷single1🇬🇷 -7241147&amp;#x1f4ff;📿1F4FFnosingle::📿single1📿 -7251148&amp;#x1f32b;&amp;#65039;🌫️1F32B-FE0Fnosingle::🌫️single1🌫️ -7261149&amp;#x1f302;🌂1F302nosingle::🌂single1🌂 -7271150&amp;#x1f549;&amp;#65039;🕉️1F549-FE0Fnosingle::🕉️single1🕉️ -7281151&amp;#x1f19a;🆚1F19Anosingle::🆚single1🆚 -7291152&amp;#x1f4c9;📉1F4C9nosingle::📉single1📉 -7301153&amp;#x1f5e8;&amp;#65039;🗨️1F5E8-FE0Fnosingle::🗨️single1🗨️ -7311154&amp;#x1f6d2;🛒1F6D2nosingle::🛒single1🛒 -7321155&amp;#x1f4db;📛1F4DBnosingle::📛single1📛 -7331156&amp;#x1f1f3;&amp;#x1f1f1;🇳🇱1F1F3-1F1F1nosingle::🇳🇱single1🇳🇱 -7341157&amp;#x1f43f;&amp;#65039;🐿️1F43F-FE0Fnosingle::🐿️single1🐿️ -7351158&amp;#x1f17e;&amp;#65039;🅾️1F17E-FE0Fnosingle::🅾️single1🅾️ -7361159&amp;#9762;&amp;#65039;☢️2622-FE0Fnosingle::☢️single1☢️ -7371160&amp;#x1f1f2;&amp;#x1f1e6;🇲🇦1F1F2-1F1E6nosingle::🇲🇦single1🇲🇦 -7381161&amp;#x1f403;🐃1F403nosingle::🐃single1🐃 -7391162&amp;#9981;⛽26FDnosingle::⛽single1⛽ -7401163&amp;#x1f17f;&amp;#65039;🅿️1F17F-FE0Fnosingle::🅿️single1🅿️ -7411164&amp;#x1f987;🦇1F987nosingle::🦇single1🦇 -7421165&amp;#10548;&amp;#65039;⤴️2934-FE0Fnosingle::⤴️single1⤴️ -7431166&amp;#9973;⛵26F5nosingle::⛵single1⛵ -7441167&amp;#x1f45e;👞1F45Enosingle::👞single1👞 -7451168&amp;#x1f513;🔓1F513nosingle::🔓single1🔓 -7461169&amp;#x1f9de;🧞1F9DEnosingle::🧞single1🧞 -7471170&amp;#9968;&amp;#65039;⛰️26F0-FE0Fnosingle::⛰️single1⛰️ -7481171&amp;#x1f4ce;📎1F4CEnosingle::📎single1📎 -7491172&amp;#x1f98e;🦎1F98Enosingle::🦎single1🦎 -7501173&amp;#x1f50c;🔌1F50Cnosingle::🔌single1🔌 -7511174&amp;#x1f3f3;&amp;#65039;🏳️1F3F3-FE0Fnosingle::🏳️single1🏳️ -7521175&amp;#x1f6bd;🚽1F6BDnosingle::🚽single1🚽 -7531176&amp;#x1f372;🍲1F372nosingle::🍲single1🍲 -7541177&amp;#9874;&amp;#65039;⚒️2692-FE0Fnosingle::⚒️single1⚒️ -7551178&amp;#9802;♊264Anosingle::♊single1♊ -7561179&amp;#x1f462;👢1F462nosingle::👢single1👢 -7571180&amp;#x1f9d9;🧙1F9D9nosingle::🧙single1🧙 -7581181&amp;#x1f1f5;&amp;#x1f1f1;🇵🇱1F1F5-1F1F1nosingle::🇵🇱single1🇵🇱 -7591182&amp;#9962;⛪26EAnosingle::⛪single1⛪ -7601183&amp;#x1f1e7;&amp;#x1f1ea;🇧🇪1F1E7-1F1EAnosingle::🇧🇪single1🇧🇪 -7611184&amp;#x1f38e;🎎1F38Enosingle::🎎single1🎎 -7621185&amp;#x1f516;🔖1F516nosingle::🔖single1🔖 -7631186&amp;#x1f50b;🔋1F50Bnosingle::🔋single1🔋 -7641187&amp;#x1f4e1;📡1F4E1nosingle::📡single1📡 -7651188&amp;#x1f307;🌇1F307nosingle::🌇single1🌇 -7661189&amp;#x1f3c9;🏉1F3C9nosingle::🏉single1🏉 -7671190&amp;#x1f46a;👪1F46Anosingle::👪single1👪 -7681191&amp;#x1f456;👖1F456nosingle::👖single1👖 -7691192&amp;#x1f4ca;📊1F4CAnosingle::📊single1📊 -7701193&amp;#x1f406;🐆1F406nosingle::🐆single1🐆 -7711194&amp;#x1f501;🔁1F501nosingle::🔁single1🔁 -7721195&amp;#x1f6e9;&amp;#65039;🛩️1F6E9-FE0Fnosingle::🛩️single1🛩️ -7731196&amp;#x1f421;🐡1F421nosingle::🐡single1🐡 -7741197&amp;#x1f0cf;🃏1F0CFnosingle::🃏single1🃏 -7751198&amp;#x1f469;&amp;zwj;&amp;#x1f4bb;👩‍💻1F469-200D-1F4BBnosingle::👩‍💻single1👩‍💻 -7761199&amp;#x1f329;&amp;#65039;🌩️1F329-FE0Fnosingle::🌩️single1🌩️ -77711100&amp;#x1f1f3;&amp;#x1f1ec;🇳🇬1F1F3-1F1ECnosingle::🇳🇬single1🇳🇬 -77811101&amp;#x1f40f;🐏1F40Fnosingle::🐏single1🐏 -779111026&amp;#65039;&amp;#8419;6️⃣36-FE0F-20E3nosingle::6️⃣single16️⃣ -78011103&amp;#9977;&amp;#65039;⛹️26F9-FE0Fnosingle::⛹️single1⛹️ -78111104&amp;#x1f5ef;&amp;#65039;🗯️1F5EF-FE0Fnosingle::🗯️single1🗯️ -78211105&amp;#x1f1e8;&amp;#x1f1ed;🇨🇭1F1E8-1F1EDnosingle::🇨🇭single1🇨🇭 -78311106&amp;#x1f362;🍢1F362nosingle::🍢single1🍢 -78411107&amp;#x1f5fa;&amp;#65039;🗺️1F5FA-FE0Fnosingle::🗺️single1🗺️ -78511108&amp;#x1f695;🚕1F695nosingle::🚕single1🚕 -78611109&amp;#x1f1f7;&amp;#x1f1fa;🇷🇺1F1F7-1F1FAnosingle::🇷🇺single1🇷🇺 -78711110&amp;#9410;&amp;#65039;Ⓜ️24C2-FE0Fnosingle::Ⓜ️single1Ⓜ️ -78811111&amp;#9969;&amp;#65039;⛱️26F1-FE0Fnosingle::⛱️single1⛱️ -78911112&amp;#x1f3eb;🏫1F3EBnosingle::🏫single1🏫 -79011113&amp;#x1f4c0;📀1F4C0nosingle::📀single1📀 -79111114&amp;#x1f199;🆙1F199nosingle::🆙single1🆙 -79211115&amp;#x1f93c;🤼1F93Cnosingle::🤼single1🤼 -79311116&amp;#x1f428;🐨1F428nosingle::🐨single1🐨 -79411117&amp;#x1f982;🦂1F982nosingle::🦂single1🦂 -79511118&amp;#x1f4b7;💷1F4B7nosingle::💷single1💷 -79611119&amp;#x1f948;🥈1F948nosingle::🥈single1🥈 -79711120&amp;#x1f468;&amp;zwj;&amp;#x1f4bb;👨‍💻1F468-200D-1F4BBnosingle::👨‍💻single1👨‍💻 -79811121&amp;#x1f6cf;&amp;#65039;🛏️1F6CF-FE0Fnosingle::🛏️single1🛏️ -79911122&amp;#9724;&amp;#65039;◼️25FC-FE0Fnosingle::◼️single1◼️ -80011123&amp;#x1f684;🚄1F684nosingle::🚄single1🚄 -80111124&amp;#x1f9d6;🧖1F9D6nosingle::🧖single1🧖 -80211125&amp;#x1f3a0;🎠1F3A0nosingle::🎠single1🎠 -80311126&amp;#x1f401;🐁1F401nosingle::🐁single1🐁 -80411127&amp;#x1f5f3;&amp;#65039;🗳️1F5F3-FE0Fnosingle::🗳️single1🗳️ -80511128&amp;#x1f3ee;🏮1F3EEnosingle::🏮single1🏮 -80611129&amp;#x1f96a;🥪1F96Anosingle::🥪single1🥪 -80711130&amp;#x1f325;&amp;#65039;🌥️1F325-FE0Fnosingle::🌥️single1🌥️ -80811131&amp;#x1f6aa;🚪1F6AAnosingle::🚪single1🚪 -80911132&amp;#12953;&amp;#65039;㊙️3299-FE0Fnosingle::㊙️single1㊙️ -81011133&amp;#x1f5de;&amp;#65039;🗞️1F5DE-FE0Fnosingle::🗞️single1🗞️ -81111134&amp;#x1f468;&amp;zwj;&amp;#9877;&amp;#65039;👨‍⚕️1F468-200D-2695-FE0Fnosingle::👨‍⚕️single1👨‍⚕️ -81211135&amp;#x1f58a;&amp;#65039;🖊️1F58A-FE0Fnosingle::🖊️single1🖊️ -81311136&amp;#11035;⬛2B1Bnosingle::⬛single1⬛ -81411137&amp;#x1f192;🆒1F192nosingle::🆒single1🆒 -81511138&amp;#x1f1e8;&amp;#x1f1fa;🇨🇺1F1E8-1F1FAnosingle::🇨🇺single1🇨🇺 -81611139&amp;#x1f967;🥧1F967nosingle::🥧single1🥧 -81711140&amp;#x1f469;&amp;zwj;&amp;#9877;&amp;#65039;👩‍⚕️1F469-200D-2695-FE0Fnosingle::👩‍⚕️single1👩‍⚕️ -81811141&amp;#x1f1f3;&amp;#x1f1ee;🇳🇮1F1F3-1F1EEnosingle::🇳🇮single1🇳🇮 -81911142&amp;#x1f468;&amp;zwj;&amp;#x1f393;👨‍🎓1F468-200D-1F393nosingle::👨‍🎓single1👨‍🎓 -82011143&amp;#x1f6a7;🚧1F6A7nosingle::🚧single1🚧 -82111144&amp;#9939;&amp;#65039;⛓️26D3-FE0Fnosingle::⛓️single1⛓️ -82211145&amp;#x1f6f5;🛵1F6F5nosingle::🛵single1🛵 -82311146&amp;#x1f945;🥅1F945nosingle::🥅single1🥅 -82411147&amp;#x1f685;🚅1F685nosingle::🚅single1🚅 -82511148&amp;#x1f5bc;&amp;#65039;🖼️1F5BC-FE0Fnosingle::🖼️single1🖼️ -82611149&amp;#9855;♿267Fnosingle::♿single1♿ -82711150&amp;#x1f4cf;📏1F4CFnosingle::📏single1📏 -82811151&amp;#x1f004;🀄1F004nosingle::🀄single1🀄 -82911152&amp;#x1f58c;&amp;#65039;🖌️1F58C-FE0Fnosingle::🖌️single1🖌️ -83011153&amp;#x1f250;🉐1F250nosingle::🉐single1🉐 -83111154&amp;#x1f4ec;📬1F4ECnosingle::📬single1📬 -83211155&amp;#x1f681;🚁1F681nosingle::🚁single1🚁 -83311156&amp;#x1f5d1;&amp;#65039;🗑️1F5D1-FE0Fnosingle::🗑️single1🗑️ -83411157&amp;#x1f935;🤵1F935nosingle::🤵single1🤵 -83511158&amp;#x1f3f7;&amp;#65039;🏷️1F3F7-FE0Fnosingle::🏷️single1🏷️ -83611159&amp;#x1f321;&amp;#65039;🌡️1F321-FE0Fnosingle::🌡️single1🌡️ -83711160&amp;#x1f390;🎐1F390nosingle::🎐single1🎐 -83811161&amp;#x1f1ef;&amp;#x1f1f2;🇯🇲1F1EF-1F1F2nosingle::🇯🇲single1🇯🇲 -83911162&amp;#x1f992;🦒1F992nosingle::🦒single1🦒 -84011163&amp;#x1f392;🎒1F392nosingle::🎒single1🎒 -84111164&amp;#x1f1f5;&amp;#x1f1fe;🇵🇾1F1F5-1F1FEnosingle::🇵🇾single1🇵🇾 -84211165&amp;#x1f1fa;&amp;#x1f1e6;🇺🇦1F1FA-1F1E6nosingle::🇺🇦single1🇺🇦 -84311166&amp;#x1f1f2;&amp;#x1f1e8;🇲🇨1F1F2-1F1E8nosingle::🇲🇨single1🇲🇨 -84411167&amp;#x1f949;🥉1F949nosingle::🥉single1🥉 -845111687&amp;#65039;&amp;#8419;7️⃣37-FE0F-20E3nosingle::7️⃣single17️⃣ -84611169&amp;#8987;⌛231Bnosingle::⌛single1⌛ -84711170&amp;#x1f312;🌒1F312nosingle::🌒single1🌒 -84811171&amp;#x1f965;🥥1F965nosingle::🥥single1🥥 -84911172&amp;#x1f385;🎅1F385nosingle::🎅single1🎅 -85011173&amp;#x1f9d5;🧕1F9D5nosingle::🧕single1🧕 -85111174&amp;#x1f6d0;🛐1F6D0nosingle::🛐single1🛐 -85211175&amp;#9803;♋264Bnosingle::♋single1♋ -85311176&amp;#x1f9d1;🧑1F9D1nosingle::🧑single1🧑 -85411177&amp;#x1f1e9;&amp;#x1f1f4;🇩🇴1F1E9-1F1F4nosingle::🇩🇴single1🇩🇴 -85511178&amp;#x1f1ee;&amp;#x1f1ea;🇮🇪1F1EE-1F1EAnosingle::🇮🇪single1🇮🇪 -85611179&amp;#x1f6f8;🛸1F6F8nosingle::🛸single1🛸 -85711180&amp;#x1f9d2;🧒1F9D2nosingle::🧒single1🧒 -85811181&amp;#x1f4e7;📧1F4E7nosingle::📧single1📧 -85911182&amp;#x1f962;🥢1F962nosingle::🥢single1🥢 -86011183&amp;#x1f3a2;🎢1F3A2nosingle::🎢single1🎢 -86111184&amp;#x1f42a;🐪1F42Anosingle::🐪single1🐪 -86211185&amp;#x1f1ee;&amp;#x1f1f1;🇮🇱1F1EE-1F1F1nosingle::🇮🇱single1🇮🇱 -86311186&amp;#x1f468;&amp;zwj;&amp;#x1f373;👨‍🍳1F468-200D-1F373nosingle::👨‍🍳single1👨‍🍳 -86411187&amp;#x1f69b;🚛1F69Bnosingle::🚛single1🚛 -86511188&amp;#x1f944;🥄1F944nosingle::🥄single1🥄 -86611189&amp;#x1f939;🤹1F939nosingle::🤹single1🤹 -86711190&amp;#x1f3e2;🏢1F3E2nosingle::🏢single1🏢 -86811191&amp;#x1f488;💈1F488nosingle::💈single1💈 -86911192&amp;#x1f458;👘1F458nosingle::👘single1👘 -87011193&amp;#x1f1f3;&amp;#x1f1f4;🇳🇴1F1F3-1F1F4nosingle::🇳🇴single1🇳🇴 -87111194&amp;#x1f1ec;&amp;#x1f1ed;🇬🇭1F1EC-1F1EDnosingle::🇬🇭single1🇬🇭 -87211195&amp;#x1f3d4;&amp;#65039;🏔️1F3D4-FE0Fnosingle::🏔️single1🏔️ -87311196&amp;#12349;&amp;#65039;〽️303D-FE0Fnosingle::〽️single1〽️ -87411197&amp;#x1f6a6;🚦1F6A6nosingle::🚦single1🚦 -87511198&amp;#x1f1ff;&amp;#x1f1e6;🇿🇦1F1FF-1F1E6nosingle::🇿🇦single1🇿🇦 -87611199&amp;#x1f1f9;&amp;#x1f1f3;🇹🇳1F1F9-1F1F3nosingle::🇹🇳single1🇹🇳 -87711200&amp;#x1f4bc;💼1F4BCnosingle::💼single1💼 -87811201&amp;#9800;♈2648nosingle::♈single1♈ -87911202&amp;#x1f993;🦓1F993nosingle::🦓single1🦓 -88011203&amp;#x1f6bb;🚻1F6BBnosingle::🚻single1🚻 -88111204&amp;#x1f3b0;🎰1F3B0nosingle::🎰single1🎰 -88211205&amp;#9725;◽25FDnosingle::◽single1◽ -88311206&amp;#x1f997;🦗1F997nosingle::🦗single1🦗 -88411207&amp;#x1f93e;🤾1F93Enosingle::🤾single1🤾 -88511208&amp;#x1f45a;👚1F45Anosingle::👚single1👚 -88611209&amp;#x1f461;👡1F461nosingle::👡single1👡 -88711210&amp;#9935;&amp;#65039;⛏️26CF-FE0Fnosingle::⛏️single1⛏️ -888112119&amp;#65039;&amp;#8419;9️⃣39-FE0F-20E3nosingle::9️⃣single19️⃣ -88911212&amp;#x1f6ce;&amp;#65039;🛎️1F6CE-FE0Fnosingle::🛎️single1🛎️ -89011213&amp;#x1f417;🐗1F417nosingle::🐗single1🐗 -89111214&amp;#x1f326;&amp;#65039;🌦️1F326-FE0Fnosingle::🌦️single1🌦️ -89211215&amp;#x1f1f8;&amp;#x1f1e6;🇸🇦1F1F8-1F1E6nosingle::🇸🇦single1🇸🇦 -89311216&amp;#x1f6b9;🚹1F6B9nosingle::🚹single1🚹 -89411217&amp;#x1f1fe;&amp;#x1f1ea;🇾🇪1F1FE-1F1EAnosingle::🇾🇪single1🇾🇪 -89511218&amp;#9770;&amp;#65039;☪️262A-FE0Fnosingle::☪️single1☪️ -89611219&amp;#x1f93a;🤺1F93Anosingle::🤺single1🤺 -89711220&amp;#x1f6a3;🚣1F6A3nosingle::🚣single1🚣 -89811221&amp;#x1f5ff;🗿1F5FFnosingle::🗿single1🗿 -89911222&amp;#x1f958;🥘1F958nosingle::🥘single1🥘 -90011223&amp;#x1f1e9;&amp;#x1f1f0;🇩🇰1F1E9-1F1F0nosingle::🇩🇰single1🇩🇰 -90111224&amp;#x1f995;🦕1F995nosingle::🦕single1🦕 -90211225&amp;#x1f1f1;&amp;#x1f1f7;🇱🇷1F1F1-1F1F7nosingle::🇱🇷single1🇱🇷 -90311226&amp;#x1f3d5;&amp;#65039;🏕️1F3D5-FE0Fnosingle::🏕️single1🏕️ -90411227&amp;#9763;&amp;#65039;☣️2623-FE0Fnosingle::☣️single1☣️ -90511228&amp;#x1f306;🌆1F306nosingle::🌆single1🌆 -90611229&amp;#8597;&amp;#65039;↕️2195-FE0Fnosingle::↕️single1↕️ -90711230&amp;#x1f4e8;📨1F4E8nosingle::📨single1📨 -90811231&amp;#x1f3e8;🏨1F3E8nosingle::🏨single1🏨 -90911232&amp;#x1f579;&amp;#65039;🕹️1F579-FE0Fnosingle::🕹️single1🕹️ -91011233&amp;#x1f469;&amp;zwj;&amp;#x1f373;👩‍🍳1F469-200D-1F373nosingle::👩‍🍳single1👩‍🍳 -91111234&amp;#x1f1e8;&amp;#x1f1ee;🇨🇮1F1E8-1F1EEnosingle::🇨🇮single1🇨🇮 -912121&amp;#x1f4ef;📯1F4EFnosingle::📯single1📯 -913122&amp;#x1f45c;👜1F45Cnosingle::👜single1👜 -914123&amp;#x1f3d8;&amp;#65039;🏘️1F3D8-FE0Fnosingle::🏘️single1🏘️ -915124&amp;#x1f42b;🐫1F42Bnosingle::🐫single1🐫 -916125&amp;#9978;⛺26FAnosingle::⛺single1⛺ -917126&amp;#x1f469;&amp;zwj;&amp;#x1f3eb;👩‍🏫1F469-200D-1F3EBnosingle::👩‍🏫single1👩‍🏫 -918127&amp;#x1f573;&amp;#65039;🕳️1F573-FE0Fnosingle::🕳️single1🕳️ -919128&amp;#x1f3ef;🏯1F3EFnosingle::🏯single1🏯 -920129&amp;#x1f313;🌓1F313nosingle::🌓single1🌓 -9211210&amp;#x1f309;🌉1F309nosingle::🌉single1🌉 -92212118&amp;#65039;&amp;#8419;8️⃣38-FE0F-20E3nosingle::8️⃣single18️⃣ -9231212&amp;#x1f482;💂1F482nosingle::💂single1💂 -9241213&amp;#x1f509;🔉1F509nosingle::🔉single1🔉 -9251214&amp;#x1f468;&amp;zwj;&amp;#x1f469;&amp;zwj;&amp;#x1f467;&amp;zwj;&amp;#x1f466;👨‍👩‍👧‍👦1F468-200D-1F469-200D-1F467-200D-1F466nosingle::👨‍👩‍👧‍👦single1👨‍👩‍👧‍👦 -9261215&amp;#x1f1ea;&amp;#x1f1fa;🇪🇺1F1EA-1F1FAnosingle::🇪🇺single1🇪🇺 -9271216&amp;#x1f69c;🚜1F69Cnosingle::🚜single1🚜 -9281217&amp;#x1f515;🔕1F515nosingle::🔕single1🔕 -9291218&amp;#x1f4e5;📥1F4E5nosingle::📥single1📥 -9301219&amp;#x1f314;🌔1F314nosingle::🌔single1🌔 -9311220&amp;#x1f3d9;&amp;#65039;🏙️1F3D9-FE0Fnosingle::🏙️single1🏙️ -9321221&amp;#x1f968;🥨1F968nosingle::🥨single1🥨 -9331222&amp;#x1f1f2;&amp;#x1f1fe;🇲🇾1F1F2-1F1FEnosingle::🇲🇾single1🇲🇾 -9341223&amp;#x1f4d8;📘1F4D8nosingle::📘single1📘 -9351224&amp;#x1f4d7;📗1F4D7nosingle::📗single1📗 -9361225&amp;#x1f4dc;📜1F4DCnosingle::📜single1📜 -9371226&amp;#9807;♏264Fnosingle::♏single1♏ -9381227&amp;#x1f1e7;&amp;#x1f1f4;🇧🇴1F1E7-1F1F4nosingle::🇧🇴single1🇧🇴 -9391228&amp;#x1f3dc;&amp;#65039;🏜️1F3DC-FE0Fnosingle::🏜️single1🏜️ -9401229&amp;#x1f358;🍘1F358nosingle::🍘single1🍘 -9411230&amp;#x1f6ec;🛬1F6ECnosingle::🛬single1🛬 -9421231&amp;#x1f32f;🌯1F32Fnosingle::🌯single1🌯 -9431232&amp;#x1f3d7;&amp;#65039;🏗️1F3D7-FE0Fnosingle::🏗️single1🏗️ -9441233&amp;#x1f3f4;&amp;#xe0067;&amp;#xe0062;&amp;#xe0065;&amp;#xe006e;&amp;#xe0067;&amp;#xe007f;🏴󠁧󠁢󠁥󠁮󠁧󠁿1F3F4-E0067-E0062-E0065-E006E-E0067-E007Fnosingle::🏴󠁧󠁢󠁥󠁮󠁧󠁿single1🏴󠁧󠁢󠁥󠁮󠁧󠁿 -9451234&amp;#x1f6bc;🚼1F6BCnosingle::🚼single1🚼 -9461235&amp;#x1f3de;&amp;#65039;🏞️1F3DE-FE0Fnosingle::🏞️single1🏞️ -9471236&amp;#x1f508;🔈1F508nosingle::🔈single1🔈 -9481237&amp;#x1f570;&amp;#65039;🕰️1F570-FE0Fnosingle::🕰️single1🕰️ -9491238&amp;#x1f3d3;🏓1F3D3nosingle::🏓single1🏓 -9501239&amp;#x1f1e8;&amp;#x1f1f7;🇨🇷1F1E8-1F1F7nosingle::🇨🇷single1🇨🇷 -9511240&amp;#x1f468;&amp;zwj;&amp;#x1f33e;👨‍🌾1F468-200D-1F33Enosingle::👨‍🌾single1👨‍🌾 -9521241&amp;#x1f983;🦃1F983nosingle::🦃single1🦃 -9531242&amp;#x1f526;🔦1F526nosingle::🔦single1🔦 -9541243&amp;#x1f503;🔃1F503nosingle::🔃single1🔃 -9551244&amp;#x1f1f8;&amp;#x1f1fb;🇸🇻1F1F8-1F1FBnosingle::🇸🇻single1🇸🇻 -9561245&amp;#x1f6f3;&amp;#65039;🛳️1F6F3-FE0Fnosingle::🛳️single1🛳️ -9571246&amp;#x1f318;🌘1F318nosingle::🌘single1🌘 -9581247&amp;#x1f1e7;&amp;#x1f1e9;🇧🇩1F1E7-1F1E9nosingle::🇧🇩single1🇧🇩 -9591248&amp;#x1f9db;🧛1F9DBnosingle::🧛single1🧛 -9601249&amp;#x1f469;&amp;zwj;&amp;#10084;&amp;#65039;&amp;zwj;&amp;#x1f469;👩‍❤️‍👩1F469-200D-2764-FE0F-200D-1F469nosingle::👩‍❤️‍👩single1👩‍❤️‍👩 -9611250&amp;#x1f51a;🔚1F51Anosingle::🔚single1🔚 -9621251&amp;#x1f51f;🔟1F51Fnosingle::🔟single1🔟 -9631252&amp;#8505;&amp;#65039;ℹ️2139-FE0Fnosingle::ℹ️single1ℹ️ -9641253&amp;#x1f6e2;&amp;#65039;🛢️1F6E2-FE0Fnosingle::🛢️single1🛢️ -9651254&amp;#x1f3f4;&amp;#xe0067;&amp;#xe0062;&amp;#xe0073;&amp;#xe0063;&amp;#xe0074;&amp;#xe007f;🏴󠁧󠁢󠁳󠁣󠁴󠁿1F3F4-E0067-E0062-E0073-E0063-E0074-E007Fnosingle::🏴󠁧󠁢󠁳󠁣󠁴󠁿single1🏴󠁧󠁢󠁳󠁣󠁴󠁿 -9661255&amp;#x1f1f8;&amp;#x1f1e9;🇸🇩1F1F8-1F1E9nosingle::🇸🇩single1🇸🇩 -9671256&amp;#x1f52d;🔭1F52Dnosingle::🔭single1🔭 -9681257&amp;#x1f1ee;&amp;#x1f1f6;🇮🇶1F1EE-1F1F6nosingle::🇮🇶single1🇮🇶 -9691258&amp;#x1f38d;🎍1F38Dnosingle::🎍single1🎍 -9701259&amp;#x1f963;🥣1F963nosingle::🥣single1🥣 -9711260&amp;#x1f317;🌗1F317nosingle::🌗single1🌗 -9721261&amp;#8598;&amp;#65039;↖️2196-FE0Fnosingle::↖️single1↖️ -9731262&amp;#x1f3b3;🎳1F3B3nosingle::🎳single1🎳 -9741263&amp;#x1f468;&amp;zwj;&amp;#10084;&amp;#65039;&amp;zwj;&amp;#x1f48b;&amp;zwj;&amp;#x1f468;👨‍❤️‍💋‍👨1F468-200D-2764-FE0F-200D-1F48B-200D-1F468nosingle::👨‍❤️‍💋‍👨single1👨‍❤️‍💋‍👨 -9751264&amp;#x1f468;&amp;zwj;&amp;#x1f3eb;👨‍🏫1F468-200D-1F3EBnosingle::👨‍🏫single1👨‍🏫 -9761265&amp;#x1f4c4;📄1F4C4nosingle::📄single1📄 -9771266&amp;#x1f1f6;&amp;#x1f1e6;🇶🇦1F1F6-1F1E6nosingle::🇶🇦single1🇶🇦 -9781267&amp;#x1f468;&amp;zwj;&amp;#x1f469;&amp;zwj;&amp;#x1f467;👨‍👩‍👧1F468-200D-1F469-200D-1F467nosingle::👨‍👩‍👧single1👨‍👩‍👧 -9791268&amp;#x1f39a;&amp;#65039;🎚️1F39A-FE0Fnosingle::🎚️single1🎚️ -9801269&amp;#x1f6e0;&amp;#65039;🛠️1F6E0-FE0Fnosingle::🛠️single1🛠️ -9811270&amp;#x1f193;🆓1F193nosingle::🆓single1🆓 -9821271&amp;#x1f1f5;&amp;#x1f1f8;🇵🇸1F1F5-1F1F8nosingle::🇵🇸single1🇵🇸 -9831272&amp;#x1f3db;&amp;#65039;🏛️1F3DB-FE0Fnosingle::🏛️single1🏛️ -9841273&amp;#9808;♐2650nosingle::♐single1♐ -9851274&amp;#x1f9e6;🧦1F9E6nosingle::🧦single1🧦 -9861275&amp;#x1f5fe;🗾1F5FEnosingle::🗾single1🗾 -9871276&amp;#x1f469;&amp;zwj;&amp;#x1f33e;👩‍🌾1F469-200D-1F33Enosingle::👩‍🌾single1👩‍🌾 -9881277&amp;#x1f391;🎑1F391nosingle::🎑single1🎑 -9891278&amp;#x1f1ec;&amp;#x1f1f9;🇬🇹1F1EC-1F1F9nosingle::🇬🇹single1🇬🇹 -9901279&amp;#x1f9e4;🧤1F9E4nosingle::🧤single1🧤 -9911280&amp;#x1f9dd;🧝1F9DDnosingle::🧝single1🧝 -9921281&amp;#10175;➿27BFnosingle::➿single1➿ -9931282&amp;#x1f1fb;&amp;#x1f1f3;🇻🇳1F1FB-1F1F3nosingle::🇻🇳single1🇻🇳 -9941283&amp;#x1f4d2;📒1F4D2nosingle::📒single1📒 -9951284&amp;#9801;♉2649nosingle::♉single1♉ -9961285&amp;#9811;♓2653nosingle::♓single1♓ -9971286&amp;#x1f6ad;🚭1F6ADnosingle::🚭single1🚭 -9981287&amp;#x1f527;🔧1F527nosingle::🔧single1🔧 -9991288&amp;#9804;♌264Cnosingle::♌single1♌ -10001289&amp;#8617;&amp;#65039;↩️21A9-FE0Fnosingle::↩️single1↩️ -10011290&amp;#9810;♒2652nosingle::♒single1♒ -10021291&amp;#x1f3e6;🏦1F3E6nosingle::🏦single1🏦 -10031292&amp;#x1f235;🈵1F235nosingle::🈵single1🈵 -10041293&amp;#x1f96b;🥫1F96Bnosingle::🥫single1🥫 -10051294&amp;#x1f316;🌖1F316nosingle::🌖single1🌖 -10061295&amp;#x1f6f4;🛴1F6F4nosingle::🛴single1🛴 -10071296&amp;#x1f696;🚖1F696nosingle::🚖single1🚖 -10081297&amp;#x1f959;🥙1F959nosingle::🥙single1🥙 -10091298&amp;#x1f6f0;&amp;#65039;🛰️1F6F0-FE0Fnosingle::🛰️single1🛰️ -10101299&amp;#x1f98f;🦏1F98Fnosingle::🦏single1🦏 -101112100&amp;#x1f1f8;&amp;#x1f1ec;🇸🇬1F1F8-1F1ECnosingle::🇸🇬single1🇸🇬 -101212101&amp;#9210;&amp;#65039;⏺️23FA-FE0Fnosingle::⏺️single1⏺️ -101312102&amp;#x1f58d;&amp;#65039;🖍️1F58D-FE0Fnosingle::🖍️single1🖍️ -101412103&amp;#9976;&amp;#65039;⛸️26F8-FE0Fnosingle::⛸️single1⛸️ -101512104&amp;#x1f4d9;📙1F4D9nosingle::📙single1📙 -101612105&amp;#x1f472;👲1F472nosingle::👲single1👲 -101712106&amp;#x1f3c2;🏂1F3C2nosingle::🏂single1🏂 -101812107*&amp;#65039;&amp;#8419;*️⃣2A-FE0F-20E3nosingle::*️⃣single1*️⃣ -101912108&amp;#x1f532;🔲1F532nosingle::🔲single1🔲 -102012109&amp;#x1f1ed;&amp;#x1f1fa;🇭🇺1F1ED-1F1FAnosingle::🇭🇺single1🇭🇺 -102112110&amp;#x1f690;🚐1F690nosingle::🚐single1🚐 -102212111&amp;#x1f468;&amp;zwj;&amp;#10084;&amp;#65039;&amp;zwj;&amp;#x1f468;👨‍❤️‍👨1F468-200D-2764-FE0F-200D-1F468nosingle::👨‍❤️‍👨single1👨‍❤️‍👨 -102312112&amp;#x1f507;🔇1F507nosingle::🔇single1🔇 -102412113&amp;#x1f994;🦔1F994nosingle::🦔single1🦔 -102512114&amp;#x1f52f;🔯1F52Fnosingle::🔯single1🔯 -102612115&amp;#x1f6b5;🚵1F6B5nosingle::🚵single1🚵 -102712116&amp;#x1f6af;🚯1F6AFnosingle::🚯single1🚯 -102812117&amp;#x1f55b;🕛1F55Bnosingle::🕛single1🕛 -102912118&amp;#x1f4fc;📼1F4FCnosingle::📼single1📼 -103012119&amp;#x1f9d3;🧓1F9D3nosingle::🧓single1🧓 -103112120&amp;#x1f39b;&amp;#65039;🎛️1F39B-FE0Fnosingle::🎛️single1🎛️ -103212121&amp;#x1f1e8;&amp;#x1f1ff;🇨🇿1F1E8-1F1FFnosingle::🇨🇿single1🇨🇿 -103312122&amp;#x1f1ed;&amp;#x1f1f0;🇭🇰1F1ED-1F1F0nosingle::🇭🇰single1🇭🇰 -103412123&amp;#x1f4ba;💺1F4BAnosingle::💺single1💺 -103512124&amp;#x1f469;&amp;zwj;&amp;#x1f3a4;👩‍🎤1F469-200D-1F3A4nosingle::👩‍🎤single1👩‍🎤 -103612125&amp;#9970;⛲26F2nosingle::⛲single1⛲ -103712126&amp;#9883;&amp;#65039;⚛️269B-FE0Fnosingle::⚛️single1⚛️ -103812127&amp;#x1f552;🕒1F552nosingle::🕒single1🕒 -103912128&amp;#x1f4f6;📶1F4F6nosingle::📶single1📶 -104012129&amp;#x1f68b;🚋1F68Bnosingle::🚋single1🚋 -104112130&amp;#x1f1f1;&amp;#x1f1e7;🇱🇧1F1F1-1F1E7nosingle::🇱🇧single1🇱🇧 -104212131&amp;#x1f4e0;📠1F4E0nosingle::📠single1📠 -104312132&amp;#x1f687;🚇1F687nosingle::🚇single1🚇 -104412133&amp;#9202;&amp;#65039;⏲️23F2-FE0Fnosingle::⏲️single1⏲️ -104512134&amp;#9194;⏪23EAnosingle::⏪single1⏪ -104612135&amp;#x1f1f0;&amp;#x1f1fc;🇰🇼1F1F0-1F1FCnosingle::🇰🇼single1🇰🇼 -104712136&amp;#x1f6c3;🛃1F6C3nosingle::🛃single1🛃 -104812137&amp;#x1f1ed;&amp;#x1f1f3;🇭🇳1F1ED-1F1F3nosingle::🇭🇳single1🇭🇳 -104912138&amp;#x1f468;&amp;zwj;&amp;#9992;&amp;#65039;👨‍✈️1F468-200D-2708-FE0Fnosingle::👨‍✈️single1👨‍✈️ -105012139&amp;#x1f4d3;📓1F4D3nosingle::📓single1📓 -105112140&amp;#x1f469;&amp;zwj;&amp;#x1f467;👩‍👧1F469-200D-1F467nosingle::👩‍👧single1👩‍👧 -105212141&amp;#9806;♎264Enosingle::♎single1♎ -105312142&amp;#x1f3d1;🏑1F3D1nosingle::🏑single1🏑 -105412143&amp;#x1f4cb;📋1F4CBnosingle::📋single1📋 -105512144&amp;#x1f53c;🔼1F53Cnosingle::🔼single1🔼 -105612145&amp;#x1f1e8;&amp;#x1f1f5;🇨🇵1F1E8-1F1F5nosingle::🇨🇵single1🇨🇵 -105712146&amp;#x1f1ef;&amp;#x1f1f4;🇯🇴1F1EF-1F1F4nosingle::🇯🇴single1🇯🇴 -105812147&amp;#x1f4c3;📃1F4C3nosingle::📃single1📃 -105912148&amp;#x1f52c;🔬1F52Cnosingle::🔬single1🔬 -106012149&amp;#x1f1eb;&amp;#x1f1ee;🇫🇮1F1EB-1F1EEnosingle::🇫🇮single1🇫🇮 -106112150&amp;#x1f6a5;🚥1F6A5nosingle::🚥single1🚥 -106212151&amp;#x1f1f1;&amp;#x1f1f0;🇱🇰1F1F1-1F1F0nosingle::🇱🇰single1🇱🇰 -106312152&amp;#9805;♍264Dnosingle::♍single1♍ -106412153&amp;#x1f1f5;&amp;#x1f1e6;🇵🇦1F1F5-1F1E6nosingle::🇵🇦single1🇵🇦 -106512154&amp;#x1f1f8;&amp;#x1f1fe;🇸🇾1F1F8-1F1FEnosingle::🇸🇾single1🇸🇾 -106612155&amp;#x1f6be;🚾1F6BEnosingle::🚾single1🚾 -106712156&amp;#x1f1ed;&amp;#x1f1f7;🇭🇷1F1ED-1F1F7nosingle::🇭🇷single1🇭🇷 -106812157&amp;#x1f1f3;&amp;#x1f1ff;🇳🇿1F1F3-1F1FFnosingle::🇳🇿single1🇳🇿 -106912158&amp;#x1f68d;🚍1F68Dnosingle::🚍single1🚍 -107012159&amp;#x1f3f8;🏸1F3F8nosingle::🏸single1🏸 -107112160&amp;#x1f533;🔳1F533nosingle::🔳single1🔳 -107212161&amp;#x1f6c2;🛂1F6C2nosingle::🛂single1🛂 -107312162&amp;#x1f4f5;📵1F4F5nosingle::📵single1📵 -107412163&amp;#9937;&amp;#65039;⛑️26D1-FE0Fnosingle::⛑️single1⛑️ -107512164&amp;#x1f3da;&amp;#65039;🏚️1F3DA-FE0Fnosingle::🏚️single1🏚️ -107612165&amp;#x1f3bd;🎽1F3BDnosingle::🎽single1🎽 -107712166&amp;#x1f6b0;🚰1F6B0nosingle::🚰single1🚰 -107812167&amp;#x1f3ea;🏪1F3EAnosingle::🏪single1🏪 -107912168&amp;#9975;&amp;#65039;⛷️26F7-FE0Fnosingle::⛷️single1⛷️ -108012169&amp;#x1f469;&amp;zwj;&amp;#x1f4bc;👩‍💼1F469-200D-1F4BCnosingle::👩‍💼single1👩‍💼 -108112170&amp;#9809;♑2651nosingle::♑single1♑ -108212171&amp;#x1f1e8;&amp;#x1f1f2;🇨🇲1F1E8-1F1F2nosingle::🇨🇲single1🇨🇲 -108312172&amp;#x1f1f1;&amp;#x1f1fe;🇱🇾1F1F1-1F1FEnosingle::🇱🇾single1🇱🇾 -108412173&amp;#9195;⏫23EBnosingle::⏫single1⏫ -108512174&amp;#x1f3e3;🏣1F3E3nosingle::🏣single1🏣 -108612175&amp;#x1f689;🚉1F689nosingle::🚉single1🚉 -108712176&amp;#x1f6c5;🛅1F6C5nosingle::🛅single1🛅 -108812177&amp;#9877;&amp;#65039;⚕️2695-FE0Fnosingle::⚕️single1⚕️ -108912178&amp;#9972;&amp;#65039;⛴️26F4-FE0Fnosingle::⛴️single1⛴️ -109012179&amp;#x1f441;&amp;#65039;&amp;zwj;&amp;#x1f5e8;&amp;#65039;👁️‍🗨️1F441-FE0F-200D-1F5E8-FE0Fnosingle::👁️‍🗨️single1👁️‍🗨️ -109112180&amp;#x1f3e7;🏧1F3E7nosingle::🏧single1🏧 -109212181&amp;#9723;&amp;#65039;◻️25FB-FE0Fnosingle::◻️single1◻️ -109312182&amp;#x1f469;&amp;zwj;&amp;#x1f52c;👩‍🔬1F469-200D-1F52Cnosingle::👩‍🔬single1👩‍🔬 -109412183&amp;#9209;&amp;#65039;⏹️23F9-FE0Fnosingle::⏹️single1⏹️ -109512184&amp;#x1f1f7;&amp;#x1f1f4;🇷🇴1F1F7-1F1F4nosingle::🇷🇴single1🇷🇴 -109612185&amp;#x1f469;&amp;zwj;&amp;#x1f466;👩‍👦1F469-200D-1F466nosingle::👩‍👦single1👩‍👦 -109712186&amp;#11036;⬜2B1Cnosingle::⬜single1⬜ -1098131#&amp;#65039;&amp;#8419;#️⃣23-FE0F-20E3nosingle::#️⃣single1#️⃣ -1099132&amp;#x1f1e6;&amp;#x1f1f1;🇦🇱1F1E6-1F1F1nosingle::🇦🇱single1🇦🇱 -1100133&amp;#x1f1f9;&amp;#x1f1fc;🇹🇼1F1F9-1F1FCnosingle::🇹🇼single1🇹🇼 -1101134&amp;#x1f6e3;&amp;#65039;🛣️1F6E3-FE0Fnosingle::🛣️single1🛣️ -1102135&amp;#x1f3e4;🏤1F3E4nosingle::🏤single1🏤 -1103136&amp;#x1f6c4;🛄1F6C4nosingle::🛄single1🛄 -1104137&amp;#x1f1ee;&amp;#x1f1f8;🇮🇸1F1EE-1F1F8nosingle::🇮🇸single1🇮🇸 -1105138&amp;#x1f3ec;🏬1F3ECnosingle::🏬single1🏬 -1106139&amp;#x1f468;&amp;zwj;&amp;#x1f3a4;👨‍🎤1F468-200D-1F3A4nosingle::👨‍🎤single1👨‍🎤 -11071310&amp;#x1f550;🕐1F550nosingle::🕐single1🕐 -11081311&amp;#x1f3a6;🎦1F3A6nosingle::🎦single1🎦 -11091312&amp;#x1f468;&amp;zwj;&amp;#x1f4bc;👨‍💼1F468-200D-1F4BCnosingle::👨‍💼single1👨‍💼 -11101313&amp;#x1f4b9;💹1F4B9nosingle::💹single1💹 -11111314&amp;#9784;&amp;#65039;☸️2638-FE0Fnosingle::☸️single1☸️ -11121315&amp;#x1f4f4;📴1F4F4nosingle::📴single1📴 -11131316&amp;#x1f4d0;📐1F4D0nosingle::📐single1📐 -11141317&amp;#x1f5b1;&amp;#65039;🖱️1F5B1-FE0Fnosingle::🖱️single1🖱️ -11151318&amp;#x1f1ed;&amp;#x1f1f9;🇭🇹1F1ED-1F1F9nosingle::🇭🇹single1🇭🇹 -11161319&amp;#x1f1f8;&amp;#x1f1f3;🇸🇳1F1F8-1F1F3nosingle::🇸🇳single1🇸🇳 -11171320&amp;#x1f529;🔩1F529nosingle::🔩single1🔩 -11181321&amp;#x1f232;🈲1F232nosingle::🈲single1🈲 -11191322&amp;#x1f3b4;🎴1F3B4nosingle::🎴single1🎴 -11201323&amp;#x1f4f3;📳1F4F3nosingle::📳single1📳 -11211324&amp;#9766;&amp;#65039;☦️2626-FE0Fnosingle::☦️single1☦️ -11221325&amp;#x1f686;🚆1F686nosingle::🚆single1🚆 -11231326&amp;#x1f4c1;📁1F4C1nosingle::📁single1📁 -11241327&amp;#x1f4d4;📔1F4D4nosingle::📔single1📔 -11251328&amp;#x1f1e6;&amp;#x1f1eb;🇦🇫1F1E6-1F1EBnosingle::🇦🇫single1🇦🇫 -11261329&amp;#x1f469;&amp;zwj;&amp;#x1f3a8;👩‍🎨1F469-200D-1F3A8nosingle::👩‍🎨single1👩‍🎨 -11271330&amp;#x1f551;🕑1F551nosingle::🕑single1🕑 -11281331&amp;#x1f6b8;🚸1F6B8nosingle::🚸single1🚸 -11291332&amp;#x1f9e3;🧣1F9E3nosingle::🧣single1🧣 -11301333&amp;#x1f468;&amp;zwj;&amp;#x1f680;👨‍🚀1F468-200D-1F680nosingle::👨‍🚀single1👨‍🚀 -11311334&amp;#x1f1e7;&amp;#x1f1ed;🇧🇭1F1E7-1F1EDnosingle::🇧🇭single1🇧🇭 -11321335&amp;#x1f3ed;🏭1F3EDnosingle::🏭single1🏭 -11331336&amp;#x1f3fa;🏺1F3FAnosingle::🏺single1🏺 -11341337&amp;#x1f1ec;&amp;#x1f1f3;🇬🇳1F1EC-1F1F3nosingle::🇬🇳single1🇬🇳 -11351338&amp;#x1f93d;🤽1F93Dnosingle::🤽single1🤽 -11361339&amp;#x1f6e5;&amp;#65039;🛥️1F6E5-FE0Fnosingle::🛥️single1🛥️ -11371340&amp;#x1f1f2;&amp;#x1f1f1;🇲🇱1F1F2-1F1F1nosingle::🇲🇱single1🇲🇱 -11381341&amp;#x1f301;🌁1F301nosingle::🌁single1🌁 -11391342&amp;#x1f936;🤶1F936nosingle::🤶single1🤶 -11401343&amp;#x1f1e6;&amp;#x1f1ff;🇦🇿1F1E6-1F1FFnosingle::🇦🇿single1🇦🇿 -11411344&amp;#x1f4bd;💽1F4BDnosingle::💽single1💽 -11421345&amp;#x1f1e6;&amp;#x1f1f4;🇦🇴1F1E6-1F1F4nosingle::🇦🇴single1🇦🇴 -11431346&amp;#x1f68e;🚎1F68Enosingle::🚎single1🚎 -11441347&amp;#x1f1ee;&amp;#x1f1f7;🇮🇷1F1EE-1F1F7nosingle::🇮🇷single1🇮🇷 -11451348&amp;#x1f1f3;&amp;#x1f1f5;🇳🇵1F1F3-1F1F5nosingle::🇳🇵single1🇳🇵 -11461349&amp;#x1f5d2;&amp;#65039;🗒️1F5D2-FE0Fnosingle::🗒️single1🗒️ -11471350&amp;#x1f1e8;&amp;#x1f1e9;🇨🇩1F1E8-1F1E9nosingle::🇨🇩single1🇨🇩 -11481351&amp;#x1f6cb;&amp;#65039;🛋️1F6CB-FE0Fnosingle::🛋️single1🛋️ -11491352&amp;#9000;&amp;#65039;⌨️2328-FE0Fnosingle::⌨️single1⌨️ -11501353&amp;#x1f18e;🆎1F18Enosingle::🆎single1🆎 -11511354&amp;#x1f9d7;🧗1F9D7nosingle::🧗single1🧗 -11521355&amp;#x1f9e5;🧥1F9E5nosingle::🧥single1🧥 -11531356&amp;#x1f1f0;&amp;#x1f1ea;🇰🇪1F1F0-1F1EAnosingle::🇰🇪single1🇰🇪 -11541357&amp;#x1f6f6;🛶1F6F6nosingle::🛶single1🛶 -11551358&amp;#x1f4d1;📑1F4D1nosingle::📑single1📑 -11561359&amp;#x1f45d;👝1F45Dnosingle::👝single1👝 -11571360&amp;#x1f1e6;&amp;#x1f1f2;🇦🇲1F1E6-1F1F2nosingle::🇦🇲single1🇦🇲 -11581361&amp;#9199;&amp;#65039;⏯️23EF-FE0Fnosingle::⏯️single1⏯️ -11591362&amp;#x1f1ec;&amp;#x1f1ea;🇬🇪1F1EC-1F1EAnosingle::🇬🇪single1🇬🇪 -11601363&amp;#x1f4b1;💱1F4B1nosingle::💱single1💱 -11611364&amp;#x1f468;&amp;zwj;&amp;#x1f3a8;👨‍🎨1F468-200D-1F3A8nosingle::👨‍🎨single1👨‍🎨 -11621365&amp;#x1f1e7;&amp;#x1f1ec;🇧🇬1F1E7-1F1ECnosingle::🇧🇬single1🇧🇬 -11631366&amp;#x1f1ea;&amp;#x1f1e6;🇪🇦1F1EA-1F1E6nosingle::🇪🇦single1🇪🇦 -11641367&amp;#x1f469;&amp;zwj;&amp;#9878;&amp;#65039;👩‍⚖️1F469-200D-2696-FE0Fnosingle::👩‍⚖️single1👩‍⚖️ -11651368&amp;#x1f553;🕓1F553nosingle::🕓single1🕓 -11661369&amp;#x1f469;&amp;zwj;&amp;#x1f680;👩‍🚀1F469-200D-1F680nosingle::👩‍🚀single1👩‍🚀 -11671370&amp;#x1f69e;🚞1F69Enosingle::🚞single1🚞 -11681371&amp;#x1f468;&amp;zwj;&amp;#x1f692;👨‍🚒1F468-200D-1F692nosingle::👨‍🚒single1👨‍🚒 -11691372&amp;#x1f196;🆖1F196nosingle::🆖single1🆖 -11701373&amp;#x1f500;🔀1F500nosingle::🔀single1🔀 -11711374&amp;#x1f468;&amp;zwj;&amp;#x1f52c;👨‍🔬1F468-200D-1F52Cnosingle::👨‍🔬single1👨‍🔬 -11721375&amp;#x1f468;&amp;zwj;&amp;#9878;&amp;#65039;👨‍⚖️1F468-200D-2696-FE0Fnosingle::👨‍⚖️single1👨‍⚖️ -11731376&amp;#x1f468;&amp;zwj;&amp;#x1f469;&amp;zwj;&amp;#x1f466;&amp;zwj;&amp;#x1f466;👨‍👩‍👦‍👦1F468-200D-1F469-200D-1F466-200D-1F466nosingle::👨‍👩‍👦‍👦single1👨‍👩‍👦‍👦 -11741377&amp;#x1f3f4;&amp;#xe0067;&amp;#xe0062;&amp;#xe0077;&amp;#xe006c;&amp;#xe0073;&amp;#xe007f;🏴󠁧󠁢󠁷󠁬󠁳󠁿1F3F4-E0067-E0062-E0077-E006C-E0073-E007Fnosingle::🏴󠁧󠁢󠁷󠁬󠁳󠁿single1🏴󠁧󠁢󠁷󠁬󠁳󠁿 -11751378&amp;#x1f960;🥠1F960nosingle::🥠single1🥠 -11761379&amp;#x1f6b3;🚳1F6B3nosingle::🚳single1🚳 -11771380&amp;#x1f1e7;&amp;#x1f1ef;🇧🇯1F1E7-1F1EFnosingle::🇧🇯single1🇧🇯 -11781381&amp;#x1f1f2;&amp;#x1f1ec;🇲🇬1F1F2-1F1ECnosingle::🇲🇬single1🇲🇬 -11791382&amp;#x1f1ea;&amp;#x1f1ea;🇪🇪1F1EA-1F1EAnosingle::🇪🇪single1🇪🇪 -11801383&amp;#9197;&amp;#65039;⏭️23ED-FE0Fnosingle::⏭️single1⏭️ -11811384&amp;#x1f54e;🕎1F54Enosingle::🕎single1🕎 -11821385&amp;#x1f21a;🈚1F21Anosingle::🈚single1🈚 -11831386&amp;#x1f6a1;🚡1F6A1nosingle::🚡single1🚡 -11841387&amp;#x1f555;🕕1F555nosingle::🕕single1🕕 -11851388&amp;#x1f1f7;&amp;#x1f1f8;🇷🇸1F1F7-1F1F8nosingle::🇷🇸single1🇷🇸 -11861389&amp;#x1f251;🉑1F251nosingle::🉑single1🉑 -11871390&amp;#x1f4c2;📂1F4C2nosingle::📂single1📂 -11881391&amp;#x1f1f9;&amp;#x1f1ff;🇹🇿1F1F9-1F1FFnosingle::🇹🇿single1🇹🇿 -11891392&amp;#x1f4eb;📫1F4EBnosingle::📫single1📫 -11901393&amp;#x1f558;🕘1F558nosingle::🕘single1🕘 -11911394&amp;#x1f68a;🚊1F68Anosingle::🚊single1🚊 -11921395&amp;#x1f688;🚈1F688nosingle::🚈single1🚈 -11931396&amp;#x1f502;🔂1F502nosingle::🔂single1🔂 -11941397&amp;#10135;➗2797nosingle::➗single1➗ -11951398&amp;#x1f557;🕗1F557nosingle::🕗single1🕗 -11961399&amp;#x1f468;&amp;zwj;&amp;#x1f469;&amp;zwj;&amp;#x1f467;&amp;zwj;&amp;#x1f467;👨‍👩‍👧‍👧1F468-200D-1F469-200D-1F467-200D-1F467nosingle::👨‍👩‍👧‍👧single1👨‍👩‍👧‍👧 -119713100&amp;#x1f239;🈹1F239nosingle::🈹single1🈹 -119813101&amp;#x1f1f1;&amp;#x1f1fa;🇱🇺1F1F1-1F1FAnosingle::🇱🇺single1🇱🇺 -119913102&amp;#x1f6e4;&amp;#65039;🛤️1F6E4-FE0Fnosingle::🛤️single1🛤️ -120013103&amp;#x1f1fa;&amp;#x1f1ec;🇺🇬1F1FA-1F1ECnosingle::🇺🇬single1🇺🇬 -120113104&amp;#x1f556;🕖1F556nosingle::🕖single1🕖 -120213105&amp;#x1f234;🈴1F234nosingle::🈴single1🈴 -120313106&amp;#x1f468;&amp;zwj;&amp;#x1f527;👨‍🔧1F468-200D-1F527nosingle::👨‍🔧single1👨‍🔧 -120413107&amp;#x1f50f;🔏1F50Fnosingle::🔏single1🔏 -120513108&amp;#x1f1e7;&amp;#x1f1fc;🇧🇼1F1E7-1F1FCnosingle::🇧🇼single1🇧🇼 -120613109&amp;#x1f6b7;🚷1F6B7nosingle::🚷single1🚷 -120713110&amp;#x1f1f2;&amp;#x1f1f0;🇲🇰1F1F2-1F1F0nosingle::🇲🇰single1🇲🇰 -120813111&amp;#x1f1f1;&amp;#x1f1fb;🇱🇻1F1F1-1F1FBnosingle::🇱🇻single1🇱🇻 -120913112&amp;#x1f559;🕙1F559nosingle::🕙single1🕙 -121013113&amp;#x1f1f9;&amp;#x1f1f9;🇹🇹1F1F9-1F1F9nosingle::🇹🇹single1🇹🇹 -121113114&amp;#x1f1f1;&amp;#x1f1f9;🇱🇹1F1F1-1F1F9nosingle::🇱🇹single1🇱🇹 -121213115&amp;#x1f469;&amp;zwj;&amp;#10084;&amp;#65039;&amp;zwj;&amp;#x1f48b;&amp;zwj;&amp;#x1f468;👩‍❤️‍💋‍👨1F469-200D-2764-FE0F-200D-1F48B-200D-1F468nosingle::👩‍❤️‍💋‍👨single1👩‍❤️‍💋‍👨 -121313116&amp;#x1f468;&amp;zwj;&amp;#x1f467;👨‍👧1F468-200D-1F467nosingle::👨‍👧single1👨‍👧 -121413117&amp;#x1f4df;📟1F4DFnosingle::📟single1📟 -121513118&amp;#x1f469;&amp;zwj;&amp;#9992;&amp;#65039;👩‍✈️1F469-200D-2708-FE0Fnosingle::👩‍✈️single1👩‍✈️ -121613119&amp;#9208;&amp;#65039;⏸️23F8-FE0Fnosingle::⏸️single1⏸️ -121713120&amp;#x1f469;&amp;zwj;&amp;#x1f692;👩‍🚒1F469-200D-1F692nosingle::👩‍🚒single1👩‍🚒 -121813121&amp;#x1f1ff;&amp;#x1f1fc;🇿🇼1F1FF-1F1FCnosingle::🇿🇼single1🇿🇼 -121913122&amp;#x1f961;🥡1F961nosingle::🥡single1🥡 -122013123&amp;#x1f1f8;&amp;#x1f1f0;🇸🇰1F1F8-1F1F0nosingle::🇸🇰single1🇸🇰 -122113124&amp;#x1f554;🕔1F554nosingle::🕔single1🕔 -1222141&amp;#x1f1f4;&amp;#x1f1f2;🇴🇲1F1F4-1F1F2nosingle::🇴🇲single1🇴🇲 -1223142&amp;#x1f4be;💾1F4BEnosingle::💾single1💾 -1224143&amp;#x1f4e4;📤1F4E4nosingle::📤single1📤 -1225144&amp;#9879;&amp;#65039;⚗️2697-FE0Fnosingle::⚗️single1⚗️ -1226145&amp;#x1f520;🔠1F520nosingle::🔠single1🔠 -1227146&amp;#x1f69d;🚝1F69Dnosingle::🚝single1🚝 -1228147&amp;#x1f191;🆑1F191nosingle::🆑single1🆑 -1229148&amp;#x1f1f2;&amp;#x1f1f2;🇲🇲1F1F2-1F1F2nosingle::🇲🇲single1🇲🇲 -1230149&amp;#x1f468;&amp;zwj;&amp;#x1f466;👨‍👦1F468-200D-1F466nosingle::👨‍👦single1👨‍👦 -12311410&amp;#x1f1f2;&amp;#x1f1f9;🇲🇹1F1F2-1F1F9nosingle::🇲🇹single1🇲🇹 -12321411&amp;#x1f55a;🕚1F55Anosingle::🕚single1🕚 -12331412&amp;#x1f1e7;&amp;#x1f1e6;🇧🇦1F1E7-1F1E6nosingle::🇧🇦single1🇧🇦 -12341413&amp;#x1f469;&amp;zwj;&amp;#x1f467;&amp;zwj;&amp;#x1f466;👩‍👧‍👦1F469-200D-1F467-200D-1F466nosingle::👩‍👧‍👦single1👩‍👧‍👦 -12351414&amp;#x1f3bf;🎿1F3BFnosingle::🎿single1🎿 -12361415&amp;#x1f524;🔤1F524nosingle::🔤single1🔤 -12371416&amp;#x1f236;🈶1F236nosingle::🈶single1🈶 -12381417&amp;#x1f6b1;🚱1F6B1nosingle::🚱single1🚱 -12391418&amp;#x1f68f;🚏1F68Fnosingle::🚏single1🚏 -12401419&amp;#x1f1f3;&amp;#x1f1ea;🇳🇪1F1F3-1F1EAnosingle::🇳🇪single1🇳🇪 -12411420&amp;#x1f469;&amp;zwj;&amp;#x1f527;👩‍🔧1F469-200D-1F527nosingle::👩‍🔧single1👩‍🔧 -12421421&amp;#x1f1e7;&amp;#x1f1f8;🇧🇸1F1E7-1F1F8nosingle::🇧🇸single1🇧🇸 -12431422&amp;#x1f54d;🕍1F54Dnosingle::🕍single1🕍 -12441423&amp;#x1f1f0;&amp;#x1f1ed;🇰🇭1F1F0-1F1EDnosingle::🇰🇭single1🇰🇭 -12451424&amp;#x1f522;🔢1F522nosingle::🔢single1🔢 -12461425&amp;#x1f1e8;&amp;#x1f1fe;🇨🇾1F1E8-1F1FEnosingle::🇨🇾single1🇨🇾 -12471426&amp;#x1f4ed;📭1F4EDnosingle::📭single1📭 -12481427&amp;#x1f4ea;📪1F4EAnosingle::📪single1📪 -12491428&amp;#x1f233;🈳1F233nosingle::🈳single1🈳 -12501429&amp;#x1f1f2;&amp;#x1f1f7;🇲🇷1F1F2-1F1F7nosingle::🇲🇷single1🇲🇷 -12511430&amp;#x1f469;&amp;zwj;&amp;#10084;&amp;#65039;&amp;zwj;&amp;#x1f468;👩‍❤️‍👨1F469-200D-2764-FE0F-200D-1F468nosingle::👩‍❤️‍👨single1👩‍❤️‍👨 -12521431&amp;#x1f469;&amp;zwj;&amp;#x1f469;&amp;zwj;&amp;#x1f467;👩‍👩‍👧1F469-200D-1F469-200D-1F467nosingle::👩‍👩‍👧single1👩‍👩‍👧 -12531432&amp;#x1f1f0;&amp;#x1f1ff;🇰🇿1F1F0-1F1FFnosingle::🇰🇿single1🇰🇿 -12541433&amp;#x1f5a8;&amp;#65039;🖨️1F5A8-FE0Fnosingle::🖨️single1🖨️ -12551434&amp;#9198;&amp;#65039;⏮️23EE-FE0Fnosingle::⏮️single1⏮️ -12561435&amp;#x1f1ec;&amp;#x1f1fe;🇬🇾1F1EC-1F1FEnosingle::🇬🇾single1🇬🇾 -12571436&amp;#x1f1f8;&amp;#x1f1ee;🇸🇮1F1F8-1F1EEnosingle::🇸🇮single1🇸🇮 -12581437&amp;#x1f1e7;&amp;#x1f1fe;🇧🇾1F1E7-1F1FEnosingle::🇧🇾single1🇧🇾 -12591438&amp;#x1f1f8;&amp;#x1f1f4;🇸🇴1F1F8-1F1F4nosingle::🇸🇴single1🇸🇴 -12601439&amp;#x1f560;🕠1F560nosingle::🕠single1🕠 -12611440&amp;#x1f1e6;&amp;#x1f1fc;🇦🇼1F1E6-1F1FCnosingle::🇦🇼single1🇦🇼 -12621441&amp;#x1f5b2;&amp;#65039;🖲️1F5B2-FE0Fnosingle::🖲️single1🖲️ -12631442&amp;#9905;&amp;#65039;⚱️26B1-FE0Fnosingle::⚱️single1⚱️ -12641443&amp;#x1f1f2;&amp;#x1f1ea;🇲🇪1F1F2-1F1EAnosingle::🇲🇪single1🇲🇪 -12651444&amp;#x1f1ff;&amp;#x1f1f2;🇿🇲1F1FF-1F1F2nosingle::🇿🇲single1🇿🇲 -12661445&amp;#x1f1e7;&amp;#x1f1e7;🇧🇧1F1E7-1F1E7nosingle::🇧🇧single1🇧🇧 -12671446&amp;#x1f1f9;&amp;#x1f1ef;🇹🇯1F1F9-1F1EFnosingle::🇹🇯single1🇹🇯 -12681447&amp;#x1f1e8;&amp;#x1f1ec;🇨🇬1F1E8-1F1ECnosingle::🇨🇬single1🇨🇬 -12691448&amp;#x1f1fa;&amp;#x1f1ff;🇺🇿1F1FA-1F1FFnosingle::🇺🇿single1🇺🇿 -12701449&amp;#x1f1f8;&amp;#x1f1f1;🇸🇱1F1F8-1F1F1nosingle::🇸🇱single1🇸🇱 -12711450&amp;#x1f1f3;&amp;#x1f1e6;🇳🇦1F1F3-1F1E6nosingle::🇳🇦single1🇳🇦 -12721451&amp;#x1f69f;🚟1F69Fnosingle::🚟single1🚟 -12731452&amp;#x1f1e7;&amp;#x1f1ee;🇧🇮1F1E7-1F1EEnosingle::🇧🇮single1🇧🇮 -12741453&amp;#x1f1fd;&amp;#x1f1f0;🇽🇰1F1FD-1F1F0nosingle::🇽🇰single1🇽🇰 -12751454&amp;#x1f1e6;&amp;#x1f1ee;🇦🇮1F1E6-1F1EEnosingle::🇦🇮single1🇦🇮 -12761455&amp;#x1f1ea;&amp;#x1f1f9;🇪🇹1F1EA-1F1F9nosingle::🇪🇹single1🇪🇹 -12771456&amp;#x1f469;&amp;zwj;&amp;#x1f469;&amp;zwj;&amp;#x1f467;&amp;zwj;&amp;#x1f467;👩‍👩‍👧‍👧1F469-200D-1F469-200D-1F467-200D-1F467nosingle::👩‍👩‍👧‍👧single1👩‍👩‍👧‍👧 -12781457&amp;#x1f1e6;&amp;#x1f1ec;🇦🇬1F1E6-1F1ECnosingle::🇦🇬single1🇦🇬 -12791458&amp;#x1f468;&amp;zwj;&amp;#x1f3ed;👨‍🏭1F468-200D-1F3EDnosingle::👨‍🏭single1👨‍🏭 -12801459&amp;#x1f469;&amp;zwj;&amp;#x1f467;&amp;zwj;&amp;#x1f467;👩‍👧‍👧1F469-200D-1F467-200D-1F467nosingle::👩‍👧‍👧single1👩‍👧‍👧 -12811460&amp;#x1f468;&amp;zwj;&amp;#x1f468;&amp;zwj;&amp;#x1f467;&amp;zwj;&amp;#x1f467;👨‍👨‍👧‍👧1F468-200D-1F468-200D-1F467-200D-1F467nosingle::👨‍👨‍👧‍👧single1👨‍👨‍👧‍👧 -12821461&amp;#x1f22f;🈯1F22Fnosingle::🈯single1🈯 -12831462&amp;#x1f6a0;🚠1F6A0nosingle::🚠single1🚠 -12841463&amp;#x1f55f;🕟1F55Fnosingle::🕟single1🕟 -12851464&amp;#x1f468;&amp;zwj;&amp;#x1f468;&amp;zwj;&amp;#x1f466;&amp;zwj;&amp;#x1f466;👨‍👨‍👦‍👦1F468-200D-1F468-200D-1F466-200D-1F466nosingle::👨‍👨‍👦‍👦single1👨‍👨‍👦‍👦 -12861465&amp;#x1f469;&amp;zwj;&amp;#x1f469;&amp;zwj;&amp;#x1f467;&amp;zwj;&amp;#x1f466;👩‍👩‍👧‍👦1F469-200D-1F469-200D-1F467-200D-1F466nosingle::👩‍👩‍👧‍👦single1👩‍👩‍👧‍👦 -12871466&amp;#x1f1f7;&amp;#x1f1fc;🇷🇼1F1F7-1F1FCnosingle::🇷🇼single1🇷🇼 -12881467&amp;#x1f1e6;&amp;#x1f1fd;🇦🇽1F1E6-1F1FDnosingle::🇦🇽single1🇦🇽 -12891468&amp;#x1f469;&amp;zwj;&amp;#x1f466;&amp;zwj;&amp;#x1f466;👩‍👦‍👦1F469-200D-1F466-200D-1F466nosingle::👩‍👦‍👦single1👩‍👦‍👦 -12901469&amp;#x1f1eb;&amp;#x1f1ef;🇫🇯1F1EB-1F1EFnosingle::🇫🇯single1🇫🇯 -12911470&amp;#x1f94c;🥌1F94Cnosingle::🥌single1🥌 -12921471&amp;#x1f1e6;&amp;#x1f1f8;🇦🇸1F1E6-1F1F8nosingle::🇦🇸single1🇦🇸 -12931472&amp;#x1f1f9;&amp;#x1f1e9;🇹🇩1F1F9-1F1E9nosingle::🇹🇩single1🇹🇩 -12941473&amp;#x1f468;&amp;zwj;&amp;#x1f467;&amp;zwj;&amp;#x1f466;👨‍👧‍👦1F468-200D-1F467-200D-1F466nosingle::👨‍👧‍👦single1👨‍👧‍👦 -12951474&amp;#x1f1e6;&amp;#x1f1e9;🇦🇩1F1E6-1F1E9nosingle::🇦🇩single1🇦🇩 -12961475&amp;#x1f1ec;&amp;#x1f1f2;🇬🇲1F1EC-1F1F2nosingle::🇬🇲single1🇬🇲 -12971476&amp;#x1f1ec;&amp;#x1f1e6;🇬🇦1F1EC-1F1E6nosingle::🇬🇦single1🇬🇦 -12981477&amp;#x1f5c2;&amp;#65039;🗂️1F5C2-FE0Fnosingle::🗂️single1🗂️ -12991478&amp;#x1f1e7;&amp;#x1f1f2;🇧🇲1F1E7-1F1F2nosingle::🇧🇲single1🇧🇲 -13001479&amp;#x1f469;&amp;zwj;&amp;#x1f3ed;👩‍🏭1F469-200D-1F3EDnosingle::👩‍🏭single1👩‍🏭 -1301151&amp;#x1f55d;🕝1F55Dnosingle::🕝single1🕝 -1302152&amp;#x1f566;🕦1F566nosingle::🕦single1🕦 -1303153&amp;#x1f562;🕢1F562nosingle::🕢single1🕢 -1304154&amp;#x1f55c;🕜1F55Cnosingle::🕜single1🕜 -1305155&amp;#x1f1f2;&amp;#x1f1f3;🇲🇳1F1F2-1F1F3nosingle::🇲🇳single1🇲🇳 -1306156&amp;#x1f1f9;&amp;#x1f1ec;🇹🇬1F1F9-1F1ECnosingle::🇹🇬single1🇹🇬 -1307157&amp;#x1f564;🕤1F564nosingle::🕤single1🕤 -1308158&amp;#x1f1f2;&amp;#x1f1ff;🇲🇿1F1F2-1F1FFnosingle::🇲🇿single1🇲🇿 -1309159&amp;#x1f468;&amp;zwj;&amp;#x1f468;&amp;zwj;&amp;#x1f466;👨‍👨‍👦1F468-200D-1F468-200D-1F466nosingle::👨‍👨‍👦single1👨‍👨‍👦 -13101510&amp;#x1f202;&amp;#65039;🈂️1F202-FE0Fnosingle::🈂️single1🈂️ -13111511&amp;#x1f1ec;&amp;#x1f1fc;🇬🇼1F1EC-1F1FCnosingle::🇬🇼single1🇬🇼 -13121512&amp;#x1f1e7;&amp;#x1f1f3;🇧🇳1F1E7-1F1F3nosingle::🇧🇳single1🇧🇳 -13131513&amp;#x1f1f2;&amp;#x1f1fb;🇲🇻1F1F2-1F1FBnosingle::🇲🇻single1🇲🇻 -13141514&amp;#x1f567;🕧1F567nosingle::🕧single1🕧 -13151515&amp;#x1f1f2;&amp;#x1f1f4;🇲🇴1F1F2-1F1F4nosingle::🇲🇴single1🇲🇴 -13161516&amp;#x1f561;🕡1F561nosingle::🕡single1🕡 -13171517&amp;#9934;⛎26CEnosingle::⛎single1⛎ -13181518&amp;#x1f1f9;&amp;#x1f1f4;🇹🇴1F1F9-1F1F4nosingle::🇹🇴single1🇹🇴 -13191519&amp;#x1f563;🕣1F563nosingle::🕣single1🕣 -13201520&amp;#x1f468;&amp;zwj;&amp;#x1f468;&amp;zwj;&amp;#x1f467;&amp;zwj;&amp;#x1f466;👨‍👨‍👧‍👦1F468-200D-1F468-200D-1F467-200D-1F466nosingle::👨‍👨‍👧‍👦single1👨‍👨‍👧‍👦 -13211521&amp;#x1f1ea;&amp;#x1f1f7;🇪🇷1F1EA-1F1F7nosingle::🇪🇷single1🇪🇷 -13221522&amp;#x1f1e6;&amp;#x1f1e8;🇦🇨1F1E6-1F1E8nosingle::🇦🇨single1🇦🇨 -13231523&amp;#x1f1f0;&amp;#x1f1ec;🇰🇬1F1F0-1F1ECnosingle::🇰🇬single1🇰🇬 -13241524&amp;#x1f1e6;&amp;#x1f1f6;🇦🇶1F1E6-1F1F6nosingle::🇦🇶single1🇦🇶 -13251525&amp;#x1f1e8;&amp;#x1f1fb;🇨🇻1F1E8-1F1FBnosingle::🇨🇻single1🇨🇻 -13261526&amp;#x1f1e7;&amp;#x1f1eb;🇧🇫1F1E7-1F1EBnosingle::🇧🇫single1🇧🇫 -13271527&amp;#x1f1f0;&amp;#x1f1f5;🇰🇵1F1F0-1F1F5nosingle::🇰🇵single1🇰🇵 -13281528&amp;#x1f468;&amp;zwj;&amp;#x1f466;&amp;zwj;&amp;#x1f466;👨‍👦‍👦1F468-200D-1F466-200D-1F466nosingle::👨‍👦‍👦single1👨‍👦‍👦 -13291529&amp;#x1f565;🕥1F565nosingle::🕥single1🕥 -13301530&amp;#x1f1f2;&amp;#x1f1e9;🇲🇩1F1F2-1F1E9nosingle::🇲🇩single1🇲🇩 -13311531&amp;#x1f1ee;&amp;#x1f1e8;🇮🇨1F1EE-1F1E8nosingle::🇮🇨single1🇮🇨 -13321532&amp;#x1f1fb;&amp;#x1f1e6;🇻🇦1F1FB-1F1E6nosingle::🇻🇦single1🇻🇦 -13331533&amp;#x1f1ec;&amp;#x1f1e9;🇬🇩1F1EC-1F1E9nosingle::🇬🇩single1🇬🇩 -13341534&amp;#x1f1ee;&amp;#x1f1f2;🇮🇲1F1EE-1F1F2nosingle::🇮🇲single1🇮🇲 -13351535&amp;#x1f469;&amp;zwj;&amp;#x1f469;&amp;zwj;&amp;#x1f466;&amp;zwj;&amp;#x1f466;👩‍👩‍👦‍👦1F469-200D-1F469-200D-1F466-200D-1F466nosingle::👩‍👩‍👦‍👦single1👩‍👩‍👦‍👦 -13361536&amp;#x1f1f2;&amp;#x1f1eb;🇲🇫1F1F2-1F1EBnosingle::🇲🇫single1🇲🇫 -13371537&amp;#x1f4c7;📇1F4C7nosingle::📇single1📇 -13381538&amp;#x1f23a;🈺1F23Anosingle::🈺single1🈺 -13391539&amp;#x1f1f1;&amp;#x1f1e6;🇱🇦1F1F1-1F1E6nosingle::🇱🇦single1🇱🇦 -13401540&amp;#x1f6f7;🛷1F6F7nosingle::🛷single1🛷 -13411541&amp;#x1f55e;🕞1F55Enosingle::🕞single1🕞 -13421542&amp;#x1f1f2;&amp;#x1f1fc;🇲🇼1F1F2-1F1FCnosingle::🇲🇼single1🇲🇼 -13431543&amp;#x1f469;&amp;zwj;&amp;#x1f469;&amp;zwj;&amp;#x1f466;👩‍👩‍👦1F469-200D-1F469-200D-1F466nosingle::👩‍👩‍👦single1👩‍👩‍👦 -13441544&amp;#x1f468;&amp;zwj;&amp;#x1f468;&amp;zwj;&amp;#x1f467;👨‍👨‍👧1F468-200D-1F468-200D-1F467nosingle::👨‍👨‍👧single1👨‍👨‍👧 -13451545&amp;#x1f237;&amp;#65039;🈷️1F237-FE0Fnosingle::🈷️single1🈷️ -13461546&amp;#x1f468;&amp;zwj;&amp;#x1f467;&amp;zwj;&amp;#x1f467;👨‍👧‍👧1F468-200D-1F467-200D-1F467nosingle::👨‍👧‍👧single1👨‍👧‍👧 -13471547&amp;#x1f1fa;&amp;#x1f1f3;🇺🇳1F1FA-1F1F3nosingle::🇺🇳single1🇺🇳 -13481548&amp;#x1f5c3;&amp;#65039;🗃️1F5C3-FE0Fnosingle::🗃️single1🗃️ -13491549&amp;#x1f1f1;&amp;#x1f1f8;🇱🇸1F1F1-1F1F8nosingle::🇱🇸single1🇱🇸 -13501550&amp;#x1f5dc;&amp;#65039;🗜️1F5DC-FE0Fnosingle::🗜️single1🗜️ -13511551&amp;#x1f1ec;&amp;#x1f1fa;🇬🇺1F1EC-1F1FAnosingle::🇬🇺single1🇬🇺 -13521552&amp;#x1f1e7;&amp;#x1f1fb;🇧🇻1F1E7-1F1FBnosingle::🇧🇻single1🇧🇻 -13531553&amp;#x1f521;🔡1F521nosingle::🔡single1🔡 -13541554&amp;#x1f1eb;&amp;#x1f1f4;🇫🇴1F1EB-1F1F4nosingle::🇫🇴single1🇫🇴 -13551555&amp;#x1f1f2;&amp;#x1f1fa;🇲🇺1F1F2-1F1FAnosingle::🇲🇺single1🇲🇺 -13561556&amp;#x1f1f5;&amp;#x1f1ec;🇵🇬1F1F5-1F1ECnosingle::🇵🇬single1🇵🇬 -13571557&amp;#x1f1e8;&amp;#x1f1fc;🇨🇼1F1E8-1F1FCnosingle::🇨🇼single1🇨🇼 -13581558&amp;#x1f1ec;&amp;#x1f1ec;🇬🇬1F1EC-1F1ECnosingle::🇬🇬single1🇬🇬 -13591559&amp;#x1f201;🈁1F201nosingle::🈁single1🈁 -13601560&amp;#x1f523;🔣1F523nosingle::🔣single1🔣 -13611561&amp;#x1f1fc;&amp;#x1f1f8;🇼🇸1F1FC-1F1F8nosingle::🇼🇸single1🇼🇸 -13621562&amp;#x1f1f8;&amp;#x1f1f7;🇸🇷1F1F8-1F1F7nosingle::🇸🇷single1🇸🇷 -13631563&amp;#x1f1f9;&amp;#x1f1f2;🇹🇲1F1F9-1F1F2nosingle::🇹🇲single1🇹🇲 -13641564&amp;#x1f1f8;&amp;#x1f1f8;🇸🇸1F1F8-1F1F8nosingle::🇸🇸single1🇸🇸 -13651565&amp;#x1f5c4;&amp;#65039;🗄️1F5C4-FE0Fnosingle::🗄️single1🗄️ -13661566&amp;#x1f1e7;&amp;#x1f1f9;🇧🇹1F1E7-1F1F9nosingle::🇧🇹single1🇧🇹 -13671567&amp;#x1f1f8;&amp;#x1f1fd;🇸🇽1F1F8-1F1FDnosingle::🇸🇽single1🇸🇽 -13681568&amp;#x1f1f0;&amp;#x1f1f2;🇰🇲1F1F0-1F1F2nosingle::🇰🇲single1🇰🇲 -13691569&amp;#x1f238;🈸1F238nosingle::🈸single1🈸 -13701570&amp;#x1f1e9;&amp;#x1f1f2;🇩🇲1F1E9-1F1F2nosingle::🇩🇲single1🇩🇲 -13711571&amp;#x1f1ea;&amp;#x1f1ed;🇪🇭1F1EA-1F1EDnosingle::🇪🇭single1🇪🇭 -13721572&amp;#x1f1e7;&amp;#x1f1ff;🇧🇿1F1E7-1F1FFnosingle::🇧🇿single1🇧🇿 -13731573&amp;#x1f1fb;&amp;#x1f1ee;🇻🇮1F1FB-1F1EEnosingle::🇻🇮single1🇻🇮 -13741574&amp;#x1f1ec;&amp;#x1f1eb;🇬🇫1F1EC-1F1EBnosingle::🇬🇫single1🇬🇫 -13751575&amp;#x1f1f8;&amp;#x1f1f2;🇸🇲1F1F8-1F1F2nosingle::🇸🇲single1🇸🇲 -13761576&amp;#x1f1e9;&amp;#x1f1ef;🇩🇯1F1E9-1F1EFnosingle::🇩🇯single1🇩🇯 -13771577&amp;#x1f1f9;&amp;#x1f1e8;🇹🇨1F1F9-1F1E8nosingle::🇹🇨single1🇹🇨 -1378161&amp;#x1f1f1;&amp;#x1f1e8;🇱🇨1F1F1-1F1E8nosingle::🇱🇨single1🇱🇨 -1379162&amp;#x1f1fb;&amp;#x1f1fa;🇻🇺1F1FB-1F1FAnosingle::🇻🇺single1🇻🇺 -1380163&amp;#x1f1f5;&amp;#x1f1eb;🇵🇫1F1F5-1F1EBnosingle::🇵🇫single1🇵🇫 -1381164&amp;#x1f1ec;&amp;#x1f1ee;🇬🇮1F1EC-1F1EEnosingle::🇬🇮single1🇬🇮 -1382165&amp;#x1f1f8;&amp;#x1f1e8;🇸🇨1F1F8-1F1E8nosingle::🇸🇨single1🇸🇨 -1383166&amp;#x1f1f0;&amp;#x1f1f3;🇰🇳1F1F0-1F1F3nosingle::🇰🇳single1🇰🇳 -1384167&amp;#x1f1ef;&amp;#x1f1ea;🇯🇪1F1EF-1F1EAnosingle::🇯🇪single1🇯🇪 -1385168&amp;#x1f1f2;&amp;#x1f1f6;🇲🇶1F1F2-1F1F6nosingle::🇲🇶single1🇲🇶 -1386169&amp;#x1f468;&amp;zwj;&amp;#x1f469;&amp;zwj;&amp;#x1f466;👨‍👩‍👦1F468-200D-1F469-200D-1F466nosingle::👨‍👩‍👦single1👨‍👩‍👦 -13871610&amp;#x1f1f7;&amp;#x1f1ea;🇷🇪1F1F7-1F1EAnosingle::🇷🇪single1🇷🇪 -13881611&amp;#x1f1f8;&amp;#x1f1ff;🇸🇿1F1F8-1F1FFnosingle::🇸🇿single1🇸🇿 -13891612&amp;#x1f1f0;&amp;#x1f1fe;🇰🇾1F1F0-1F1FEnosingle::🇰🇾single1🇰🇾 -13901613&amp;#9167;&amp;#65039;⏏️23CF-FE0Fnosingle::⏏️single1⏏️ -13911614&amp;#x1f1fb;&amp;#x1f1e8;🇻🇨1F1FB-1F1E8nosingle::🇻🇨single1🇻🇨 -13921615&amp;#x1f1fb;&amp;#x1f1ec;🇻🇬1F1FB-1F1ECnosingle::🇻🇬single1🇻🇬 -13931616&amp;#x1f1f1;&amp;#x1f1ee;🇱🇮1F1F1-1F1EEnosingle::🇱🇮single1🇱🇮 -13941617&amp;#x1f1e8;&amp;#x1f1e8;🇨🇨1F1E8-1F1E8nosingle::🇨🇨single1🇨🇨 -13951618&amp;#x1f1e8;&amp;#x1f1eb;🇨🇫1F1E8-1F1EBnosingle::🇨🇫single1🇨🇫 -13961619&amp;#x1f1f8;&amp;#x1f1f9;🇸🇹1F1F8-1F1F9nosingle::🇸🇹single1🇸🇹 -13971620&amp;#x1f1ec;&amp;#x1f1f1;🇬🇱1F1EC-1F1F1nosingle::🇬🇱single1🇬🇱 -13981621&amp;#x1f1f5;&amp;#x1f1fc;🇵🇼1F1F5-1F1FCnosingle::🇵🇼single1🇵🇼 -13991622&amp;#x1f1ec;&amp;#x1f1f5;🇬🇵1F1EC-1F1F5nosingle::🇬🇵single1🇬🇵 -14001623&amp;#x1f1ed;&amp;#x1f1f2;🇭🇲1F1ED-1F1F2nosingle::🇭🇲single1🇭🇲 -14011624&amp;#x1f1e8;&amp;#x1f1f0;🇨🇰1F1E8-1F1F0nosingle::🇨🇰single1🇨🇰 -14021625&amp;#x1f1eb;&amp;#x1f1f0;🇫🇰1F1EB-1F1F0nosingle::🇫🇰single1🇫🇰 -14031626&amp;#x1f1f5;&amp;#x1f1f3;🇵🇳1F1F5-1F1F3nosingle::🇵🇳single1🇵🇳 -14041627&amp;#x1f1f3;&amp;#x1f1eb;🇳🇫1F1F3-1F1EBnosingle::🇳🇫single1🇳🇫 -14051628&amp;#x1f1f8;&amp;#x1f1e7;🇸🇧1F1F8-1F1E7nosingle::🇸🇧single1🇸🇧 -14061629&amp;#x1f1f9;&amp;#x1f1f1;🇹🇱1F1F9-1F1F1nosingle::🇹🇱single1🇹🇱 -14071630&amp;#x1f1eb;&amp;#x1f1f2;🇫🇲1F1EB-1F1F2nosingle::🇫🇲single1🇫🇲 -14081631&amp;#x1f1ee;&amp;#x1f1f4;🇮🇴1F1EE-1F1F4nosingle::🇮🇴single1🇮🇴 -14091632&amp;#x1f1f2;&amp;#x1f1ed;🇲🇭1F1F2-1F1EDnosingle::🇲🇭single1🇲🇭 -14101633&amp;#x1f1ec;&amp;#x1f1f6;🇬🇶1F1EC-1F1F6nosingle::🇬🇶single1🇬🇶 -14111634&amp;#x1f1f9;&amp;#x1f1f0;🇹🇰1F1F9-1F1F0nosingle::🇹🇰single1🇹🇰 -14121635&amp;hellip;…2026nosingle::…single1… -141317+1&amp;#x1f1e7;&amp;#x1f1f6;🇧🇶1F1E7-1F1F6nosingle::🇧🇶single1🇧🇶 -141417+2&amp;#x1f1f2;&amp;#x1f1f5;🇲🇵1F1F2-1F1F5nosingle::🇲🇵single1🇲🇵 -141517+3&amp;#x1f1e8;&amp;#x1f1fd;🇨🇽1F1E8-1F1FDnosingle::🇨🇽single1🇨🇽 -141617+4&amp;#x1f1f0;&amp;#x1f1ee;🇰🇮1F1F0-1F1EEnosingle::🇰🇮single1🇰🇮 -141717+5&amp;#x1f1f3;&amp;#x1f1fa;🇳🇺1F1F3-1F1FAnosingle::🇳🇺single1🇳🇺 -141817+6&amp;#x1f1f8;&amp;#x1f1ef;🇸🇯1F1F8-1F1EFnosingle::🇸🇯single1🇸🇯 -141917+7&amp;#x1f1f2;&amp;#x1f1f8;🇲🇸1F1F2-1F1F8nosingle::🇲🇸single1🇲🇸 -142017+8&amp;#x1f1f8;&amp;#x1f1ed;🇸🇭1F1F8-1F1EDnosingle::🇸🇭single1🇸🇭 -142117+9&amp;#x1f1f9;&amp;#x1f1fb;🇹🇻1F1F9-1F1FBnosingle::🇹🇻single1🇹🇻 -142217+10&amp;#x1f1f3;&amp;#x1f1e8;🇳🇨1F1F3-1F1E8nosingle::🇳🇨single1🇳🇨 -142317+11&amp;#x1f1f3;&amp;#x1f1f7;🇳🇷1F1F3-1F1F7nosingle::🇳🇷single1🇳🇷 -142417+12&amp;#x1f1e7;&amp;#x1f1f1;🇧🇱1F1E7-1F1F1nosingle::🇧🇱single1🇧🇱 -142517+13&amp;#x1f1fc;&amp;#x1f1eb;🇼🇫1F1FC-1F1EBnosingle::🇼🇫single1🇼🇫 -142617+14&amp;#x1f1ec;&amp;#x1f1f8;&amp;hellip;🇬🇸…1F1EC-1F1F8-2026nosingle::🇬🇸…single1🇬🇸… +101&amp;#x1f6…😂1F602nomanual::tears_laughingmanual2😂 🤣🤣 +202&amp;#1008…❤️2764-FE0Fnomanual::heartsmanual16❤️ 💕 ♥️ 💔 💖 💙 💜 💞 💗 💚 💛 💓 🖤 ❣️ 💘 💝💕 ♥️ 💔 💖 💙 💜 💞 💗 💚 💛 💓 🖤 ❣️ 💘 💝 +311&amp;#x1f6…😍1F60Dnomanual::smile_lovemanual4😍 😻 😈 😹😻 😈 😹 +421&amp;#x1f6…😊1F60Anosingle::😊single1😊 +522&amp;#x1f6…🙏1F64Fnosingle::🙏single1🙏 +624&amp;#x1f6…😭1F62Dnosingle::😭single1😭 +725&amp;#x1f6…😘1F618nosingle::😘single1😘 +831&amp;#x1f4…👍1F44Dnomanual::good_handmanual4👍 👏 👌 🙌👏 👌 🙌 +932&amp;#x1f6…😅1F605nosingle::😅single1😅 +1034&amp;#x1f6…😁1F601nomanual::smile_beammanual2😁 😄😄 +1136&amp;#x1f5…🔥1F525nosingle::🔥single1🔥 +12310&amp;#x1f6…😢1F622nomanual::sad_crymanual4😢 😪 😥 😓😪 😥 😓 +13311&amp;#x1f9…🤔1F914nosingle::🤔single1🤔 +14312&amp;#x1f6…😆1F606nosingle::😆single1😆 +15313&amp;#x1f6…🙄1F644nosingle::🙄single1🙄 +16314&amp;#x1f4…💪1F4AAnosingle::💪single1💪 +17315&amp;#x1f6…😉1F609nosingle::😉single1😉 +18316&amp;#9786…☺️263A-FE0Fnosingle::☺️single1☺️ +19318&amp;#x1f9…🤗1F917nosingle::🤗single1🤗 +2042&amp;#x1f6…😔1F614nosingle::😔single1😔 +2143&amp;#x1f6…😎1F60Enosingle::😎single1😎 +2244&amp;#x1f6…😇1F607nosingle::😇single1😇 +2345&amp;#x1f3…🌹1F339nomanual::flower_groupmanual7🌹 🌸 🌷 🎊 🌺 🌻 🌼🌸 🌷 🎊 🌺 🌻 🌼 +2446&amp;#x1f9…🤦1F926nosingle::🤦single1🤦 +2547&amp;#x1f3…🎉1F389nosingle::🎉single1🎉 +2648&amp;#8252…‼️203C-FE0Fnomanual::exclamationmanual2‼️ ❗❗ +27410&amp;#9996…✌️270C-FE0Fnosingle::✌️single1✌️ +28411&amp;#1002…✨2728nosingle::✨single1✨ +29412&amp;#x1f9…🤷1F937nosingle::🤷single1🤷 +30413&amp;#x1f6…😱1F631nosingle::😱single1😱 +31414&amp;#x1f6…😌1F60Cnosingle::😌single1😌 +32417&amp;#x1f6…😋1F60Bnomanual::tongue_groupmanual2😋 😛😛 +33420&amp;#x1f6…😏1F60Fnosingle::😏single1😏 +34422&amp;#x1f6…🙂1F642nosingle::🙂single1🙂 +35424&amp;#x1f9…🤩1F929nosingle::🤩single1🤩 +36426&amp;#x1f6…😀1F600nomanual::smile_grinmanual2😀 😃😃 +37429&amp;#x1f4…💯1F4AFnosingle::💯single1💯 +38430&amp;#x1f6…🙈1F648nosingle::🙈single1🙈 +39431&amp;#x1f4…👇1F447nosingle::👇single1👇 +40432&amp;#x1f3…🎶1F3B6nomanual::music_groupmanual2🎶 🎵🎵 +41433&amp;#x1f6…😒1F612nosingle::😒single1😒 +42434&amp;#x1f9…🤭1F92Dnosingle::🤭single1🤭 +4352&amp;#x1f6…😜1F61Cnosingle::😜single1😜 +4453&amp;#x1f4…💋1F48Bnosingle::💋single1💋 +4554&amp;#x1f4…👀1F440nosingle::👀single1👀 +4656&amp;#x1f6…😑1F611nosingle::😑single1😑 +4757&amp;#x1f4…💥1F4A5nosingle::💥single1💥 +4858&amp;#x1f6…🙋1F64Bnosingle::🙋single1🙋 +4959&amp;#x1f6…😞1F61Enosingle::😞single1😞 +50510&amp;#x1f6…😩1F629nosingle::😩single1😩 +51511&amp;#x1f6…😡1F621nomanual::angrymanual3😡 🔴 😤🔴 😤 +52512&amp;#x1f9…🤪1F92Anosingle::🤪single1🤪 +53513&amp;#x1f4…👊1F44Anosingle::👊single1👊 +54514&amp;#9728…☀️2600-FE0Fnosingle::☀️single1☀️ +55516&amp;#x1f9…🤤1F924nosingle::🤤single1🤤 +56517&amp;#x1f4…👉1F449nosingle::👉single1👉 +57518&amp;#x1f4…💃1F483nosingle::💃single1💃 +58519&amp;#x1f6…😳1F633nosingle::😳single1😳 +59520&amp;#9995…✋270Bnosingle::✋single1✋ +60521&amp;#x1f6…😚1F61Anosingle::😚single1😚 +61522&amp;#x1f6…😝1F61Dnosingle::😝single1😝 +62523&amp;#x1f6…😴1F634nosingle::😴single1😴 +63524&amp;#x1f3…🌟1F31Fnosingle::🌟single1🌟 +64525&amp;#x1f6…😬1F62Cnosingle::😬single1😬 +65526&amp;#x1f6…🙃1F643nosingle::🙃single1🙃 +66527&amp;#x1f3…🍀1F340nosingle::🍀single1🍀 +67531&amp;#1108…⭐2B50nosingle::⭐single1⭐ +68532&amp;#9989…✅2705nomanual::checkmarksmanual2✅ ✔️✔️ +69533&amp;#x1f3…🌈1F308nosingle::🌈single1🌈 +70535&amp;#x1f9…🤘1F918nosingle::🤘single1🤘 +7161&amp;#x1f4…💦1F4A6nosingle::💦single1💦 +7263&amp;#x1f6…😣1F623nosingle::😣single1😣 +7364&amp;#x1f3…🏃1F3C3nosingle::🏃single1🏃 +7465&amp;#x1f4…💐1F490nosingle::💐single1💐 +7566&amp;#9785…☹️2639-FE0Fnosingle::☹️single1☹️ +7669&amp;#x1f6…😠1F620nosingle::😠single1😠 +77610&amp;#9757…☝️261D-FE0Fnosingle::☝️single1☝️ +78611&amp;#x1f6…😕1F615nosingle::😕single1😕 +79613&amp;#x1f3…🎂1F382nomanual::birthday_cakesmanual2🎂 👑👑 +80615&amp;#x1f6…😐1F610nosingle::😐single1😐 +81616&amp;#x1f5…🖕1F595nosingle::🖕single1🖕 +82618&amp;#x1f6…🙊1F64Anosingle::🙊single1🙊 +83620&amp;#x1f5…🗣️1F5E3-FE0Fnosingle::🗣️single1🗣️ +84621&amp;#x1f4…💫1F4ABnosingle::💫single1💫 +85622&amp;#x1f4…💀1F480nosingle::💀single1💀 +86625&amp;#x1f9…🤞1F91Enosingle::🤞single1🤞 +87630&amp;#x1f6…😫1F62Bnosingle::😫single1😫 +88631&amp;#9917…⚽26BDnosingle::⚽single1⚽ +89632&amp;#x1f9…🤙1F919nosingle::🤙single1🤙 +90633&amp;#9749…☕2615nosingle::☕single1☕ +91634&amp;#x1f3…🏆1F3C6nosingle::🏆single1🏆 +92635&amp;#x1f9…🧡1F9E1nosingle::🧡single1🧡 +93636&amp;#x1f3…🎁1F381nosingle::🎁single1🎁 +94638&amp;#x1f3…🌞1F31Enosingle::🌞single1🌞 +95639&amp;#x1f3…🎈1F388nosingle::🎈single1🎈 +96640&amp;#1006…❌274Cnosingle::❌single1❌ +97641&amp;#9994…✊270Anosingle::✊single1✊ +98642&amp;#x1f4…👋1F44Bnosingle::👋single1👋 +99643&amp;#x1f6…😲1F632nosingle::😲single1😲 +100644&amp;#x1f3…🌿1F33Fnosingle::🌿single1🌿 +101645&amp;#x1f9…🤫1F92Bnosingle::🤫single1🤫 +102646&amp;#x1f4…👈1F448nosingle::👈single1👈 +103647&amp;#x1f6…😮1F62Enosingle::😮single1😮 +104648&amp;#x1f6…🙆1F646nosingle::🙆single1🙆 +105649&amp;#x1f3…🍻1F37Bnosingle::🍻single1🍻 +106650&amp;#x1f3…🍃1F343nosingle::🍃single1🍃 +107651&amp;#x1f4…🐶1F436nosingle::🐶single1🐶 +108652&amp;#x1f4…💁1F481nosingle::💁single1💁 +109653&amp;#x1f6…😰1F630nosingle::😰single1😰 +110654&amp;#x1f9…🤨1F928nosingle::🤨single1🤨 +111655&amp;#x1f6…😶1F636nosingle::😶single1😶 +112656&amp;#x1f9…🤝1F91Dnosingle::🤝single1🤝 +113657&amp;#x1f6…🚶1F6B6nosingle::🚶single1🚶 +114658&amp;#x1f4…💰1F4B0nosingle::💰single1💰 +115659&amp;#x1f3…🍓1F353nosingle::🍓single1🍓 +116660&amp;#x1f4…💢1F4A2nosingle::💢single1💢 +11771&amp;#x1f1…🇺🇸1F1FA-1F1F8nosingle::🇺🇸single1🇺🇸 +11872&amp;#x1f9…🤟1F91Fnosingle::🤟single1🤟 +11973&amp;#x1f6…🙁1F641nosingle::🙁single1🙁 +12074&amp;#x1f6…🚨1F6A8nosingle::🚨single1🚨 +12175&amp;#x1f4…💨1F4A8nosingle::💨single1💨 +12276&amp;#x1f9…🤬1F92Cnosingle::🤬single1🤬 +12377&amp;#9992…✈️2708-FE0Fnosingle::✈️single1✈️ +12478&amp;#x1f3…🎀1F380nosingle::🎀single1🎀 +12579&amp;#x1f3…🍺1F37Anosingle::🍺single1🍺 +126710&amp;#x1f9…🤓1F913nosingle::🤓single1🤓 +127711&amp;#x1f6…😙1F619nosingle::😙single1😙 +128712&amp;#x1f4…💟1F49Fnosingle::💟single1💟 +129713&amp;#x1f3…🌱1F331nosingle::🌱single1🌱 +130714&amp;#x1f6…😖1F616nosingle::😖single1😖 +131715&amp;#x1f4…👶1F476nosingle::👶single1👶 +132716&amp;#9654…▶️25B6-FE0Fnosingle::▶️single1▶️ +133717&amp;#1014…➡️27A1-FE0Fnosingle::➡️single1➡️ +134718&amp;#1006…❓2753nosingle::❓single1❓ +135719&amp;#x1f4…💎1F48Enosingle::💎single1💎 +136720&amp;#x1f4…💸1F4B8nosingle::💸single1💸 +137721&amp;#1101…⬇️2B07-FE0Fnosingle::⬇️single1⬇️ +138722&amp;#x1f6…😨1F628nosingle::😨single1😨 +139723&amp;#x1f3…🌚1F31Anosingle::🌚single1🌚 +140724&amp;#x1f9…🦋1F98Bnosingle::🦋single1🦋 +141725&amp;#x1f6…😷1F637nosingle::😷single1😷 +142726&amp;#x1f5…🕺1F57Anosingle::🕺single1🕺 +143727&amp;#9888…⚠️26A0-FE0Fnosingle::⚠️single1⚠️ +144728&amp;#x1f6…🙅1F645nosingle::🙅single1🙅 +145729&amp;#x1f6…😟1F61Fnosingle::😟single1😟 +146730&amp;#x1f6…😵1F635nosingle::😵single1😵 +147732&amp;#x1f9…🤲1F932nosingle::🤲single1🤲 +148733&amp;#x1f9…🤠1F920nosingle::🤠single1🤠 +149734&amp;#x1f9…🤧1F927nosingle::🤧single1🤧 +150735&amp;#x1f4…📌1F4CCnosingle::📌single1📌 +151736&amp;#x1f5…🔵1F535nosingle::🔵single1🔵 +152737&amp;#x1f4…💅1F485nosingle::💅single1💅 +153738&amp;#x1f9…🧐1F9D0nosingle::🧐single1🧐 +154739&amp;#x1f4…🐾1F43Enosingle::🐾single1🐾 +155740&amp;#x1f3…🍒1F352nosingle::🍒single1🍒 +156741&amp;#x1f6…😗1F617nosingle::😗single1😗 +157742&amp;#x1f9…🤑1F911nosingle::🤑single1🤑 +158743&amp;#x1f6…🚀1F680nosingle::🚀single1🚀 +159744&amp;#x1f3…🌊1F30Anosingle::🌊single1🌊 +160745&amp;#x1f9…🤯1F92Fnosingle::🤯single1🤯 +161746&amp;#x1f4…🐷1F437nosingle::🐷single1🐷 +162747&amp;#9742…☎️260E-FE0Fnosingle::☎️single1☎️ +163748&amp;#x1f4…💧1F4A7nosingle::💧single1💧 +164749&amp;#x1f6…😯1F62Fnosingle::😯single1😯 +165750&amp;#x1f4…💆1F486nosingle::💆single1💆 +166751&amp;#x1f4…👆1F446nosingle::👆single1👆 +167752&amp;#x1f3…🎤1F3A4nosingle::🎤single1🎤 +168753&amp;#x1f6…🙇1F647nosingle::🙇single1🙇 +169754&amp;#x1f3…🍑1F351nosingle::🍑single1🍑 +170755&amp;#1005…❄️2744-FE0Fnosingle::❄️single1❄️ +171756&amp;#x1f3…🌴1F334nosingle::🌴single1🌴 +172757&amp;#x1f1…🇧🇷1F1E7-1F1F7nosingle::🇧🇷single1🇧🇷 +173758&amp;#x1f4…💣1F4A3nosingle::💣single1💣 +174759&amp;#x1f4…🐸1F438nosingle::🐸single1🐸 +175760&amp;#x1f4…💌1F48Cnosingle::💌single1💌 +176761&amp;#x1f4…📍1F4CDnosingle::📍single1📍 +177762&amp;#x1f9…🥀1F940nosingle::🥀single1🥀 +178763&amp;#x1f9…🤢1F922nosingle::🤢single1🤢 +179764&amp;#x1f4…👅1F445nosingle::👅single1👅 +180765&amp;#x1f4…💡1F4A1nosingle::💡single1💡 +181766&amp;#x1f4…💩1F4A9nosingle::💩single1💩 +182767&amp;#8265…⁉️2049-FE0Fnosingle::⁉️single1⁉️ +183768&amp;#x1f4…👐1F450nosingle::👐single1👐 +184769&amp;#x1f4…📸1F4F8nosingle::📸single1📸 +185770&amp;#x1f4…👻1F47Bnosingle::👻single1👻 +186771&amp;#x1f9…🤐1F910nosingle::🤐single1🤐 +187772&amp;#x1f9…🤮1F92Enosingle::🤮single1🤮 +188773&amp;#x1f3…🎼1F3BCnosingle::🎼single1🎼 +189774&amp;#9997…✍️270D-FE0Fnosingle::✍️single1✍️ +190775&amp;#x1f6…🚩1F6A9nosingle::🚩single1🚩 +191776&amp;#x1f3…🍎1F34Enosingle::🍎single1🍎 +192777&amp;#x1f3…🍊1F34Anosingle::🍊single1🍊 +193778&amp;#x1f4…👼1F47Cnosingle::👼single1👼 +194779&amp;#x1f4…💍1F48Dnosingle::💍single1💍 +195780&amp;#x1f4…📣1F4E3nosingle::📣single1📣 +196781&amp;#x1f9…🥂1F942nosingle::🥂single1🥂 +197782&amp;#1054…⤵️2935-FE0Fnosingle::⤵️single1⤵️ +198783&amp;#x1f4…📱1F4F1nosingle::📱single1📱 +199784&amp;#9748…☔2614nosingle::☔single1☔ +200785&amp;#x1f3…🌙1F319nosingle::🌙single1🌙 +20181&amp;#x1f3…🍾1F37Enosingle::🍾single1🍾 +20282&amp;#x1f3…🎧1F3A7nosingle::🎧single1🎧 +20383&amp;#x1f3…🍁1F341nosingle::🍁single1🍁 +20484&amp;#1109…⭕2B55nosingle::⭕single1⭕ +20585&amp;#x1f3…🏀1F3C0nosingle::🏀single1🏀 +20686&amp;#9760…☠️2620-FE0Fnosingle::☠️single1☠️ +20787&amp;#9899…⚫26ABnosingle::⚫single1⚫ +20888&amp;#x1f5…🖐️1F590-FE0Fnosingle::🖐️single1🖐️ +20989&amp;#x1f6…😧1F627nosingle::😧single1😧 +210810&amp;#x1f3…🎯1F3AFnosingle::🎯single1🎯 +211811&amp;#x1f4…📲1F4F2nosingle::📲single1📲 +212812&amp;#9752…☘️2618-FE0Fnosingle::☘️single1☘️ +213813&amp;#x1f4…👁️1F441-FE0Fnosingle::👁️single1👁️ +214814&amp;#x1f3…🍷1F377nosingle::🍷single1🍷 +215815&amp;#x1f4…👄1F444nosingle::👄single1👄 +216816&amp;#x1f4…🐟1F41Fnosingle::🐟single1🐟 +217817&amp;#x1f3…🍰1F370nosingle::🍰single1🍰 +218818&amp;#x1f4…💤1F4A4nosingle::💤single1💤 +219819&amp;#x1f5…🕊️1F54A-FE0Fnosingle::🕊️single1🕊️ +220820&amp;#x1f4…📺1F4FAnosingle::📺single1📺 +221821&amp;#x1f4…💭1F4ADnosingle::💭single1💭 +222822&amp;#x1f4…🐱1F431nosingle::🐱single1🐱 +223823&amp;#x1f4…🐝1F41Dnosingle::🐝single1🐝 +224824&amp;#x1f1…🇲🇽1F1F2-1F1FDnosingle::🇲🇽single1🇲🇽 +225825&amp;#x1f9…🧚1F9DAnosingle::🧚single1🧚 +226826&amp;#x1f5…🔝1F51Dnosingle::🔝single1🔝 +227827&amp;#x1f4…📢1F4E2nosingle::📢single1📢 +228828&amp;#x1f4…📷1F4F7nosingle::📷single1📷 +229829&amp;#x1f4…🐕1F415nosingle::🐕single1🐕 +230830&amp;#x1f3…🎸1F3B8nosingle::🎸single1🎸 +231831&amp;#x1f5…🔫1F52Bnosingle::🔫single1🔫 +232832&amp;#x1f9…🤚1F91Anosingle::🤚single1🤚 +233833&amp;#x1f3…🍭1F36Dnosingle::🍭single1🍭 +234834&amp;#x1f3…🍆1F346nosingle::🍆single1🍆 +235835&amp;#x1f4…💉1F489nosingle::💉single1💉 +236836&amp;#x1f3…🌎1F30Enosingle::🌎single1🌎 +237837&amp;#x1f6…😦1F626nosingle::😦single1😦 +238838&amp;#x1f3…🌀1F300nosingle::🌀single1🌀 +239839&amp;#x1f4…👿1F47Fnosingle::👿single1👿 +240840&amp;#9745…☑️2611-FE0Fnosingle::☑️single1☑️ +241841&amp;#x1f3…🎥1F3A5nosingle::🎥single1🎥 +242842&amp;#x1f3…🌧️1F327-FE0Fnosingle::🌧️single1🌧️ +243843&amp;#x1f4…👽1F47Dnosingle::👽single1👽 +244844&amp;#x1f3…🍋1F34Bnosingle::🍋single1🍋 +245845&amp;#x1f9…🤒1F912nosingle::🤒single1🤒 +246846&amp;#x1f9…🤡1F921nosingle::🤡single1🤡 +247847&amp;#x1f3…🍫1F36Bnosingle::🍫single1🍫 +248848&amp;#x1f4…📚1F4DAnosingle::📚single1📚 +249849&amp;#x1f3…🏁1F3C1nosingle::🏁single1🏁 +250850&amp;#x1f9…🤕1F915nosingle::🤕single1🤕 +251851&amp;#x1f9…🦄1F984nosingle::🦄single1🦄 +252852&amp;#x1f3…🍅1F345nosingle::🍅single1🍅 +253853&amp;#x1f6…🚗1F697nosingle::🚗single1🚗 +254854&amp;#x1f6…🚫1F6ABnosingle::🚫single1🚫 +255855&amp;#x1f4…💵1F4B5nosingle::💵single1💵 +256856&amp;#9918…⚾26BEnosingle::⚾single1⚾ +257857&amp;#x1f5…🔪1F52Anosingle::🔪single1🔪 +258858&amp;#x1f5…🔔1F514nosingle::🔔single1🔔 +259859&amp;#9832…♨️2668-FE0Fnosingle::♨️single1♨️ +260860&amp;#x1f3…🌳1F333nosingle::🌳single1🌳 +261861&amp;#x1f5…🔊1F50Anosingle::🔊single1🔊 +262862&amp;#x1f3…🍬1F36Cnosingle::🍬single1🍬 +263863&amp;#x1f4…💏1F48Fnosingle::💏single1💏 +264864&amp;#x1f3…🍼1F37Cnosingle::🍼single1🍼 +265865&amp;#x1f3…🍜1F35Cnosingle::🍜single1🍜 +266866&amp;#x1f4…🐼1F43Cnosingle::🐼single1🐼 +267867&amp;#x1f6…🙉1F649nosingle::🙉single1🙉 +268868&amp;#x1f4…🐈1F408nosingle::🐈single1🐈 +269869&amp;#x1f4…🐻1F43Bnosingle::🐻single1🐻 +270870&amp;#x1f9…🤸1F938nosingle::🤸single1🤸 +271871&amp;#x1f3…🌝1F31Dnosingle::🌝single1🌝 +272872&amp;#x1f4…👸1F478nosingle::👸single1👸 +273873&amp;#x1f3…🍕1F355nosingle::🍕single1🍕 +274874&amp;#x1f3…🍌1F34Cnosingle::🍌single1🍌 +275875&amp;#x1f3…🍦1F366nosingle::🍦single1🍦 +276876&amp;#9898…⚪26AAnosingle::⚪single1⚪ +277877&amp;#x1f4…👩1F469nosingle::👩single1👩 +278878&amp;#x1f6…😿1F63Fnosingle::😿single1😿 +279879&amp;#x1f3…🍂1F342nosingle::🍂single1🍂 +280880&amp;#x1f4…📞1F4DEnosingle::📞single1📞 +281881&amp;#9200…⏰23F0nosingle::⏰single1⏰ +282882&amp;#x1f5…🔞1F51Enosingle::🔞single1🔞 +283883&amp;#x1f3…🌍1F30Dnosingle::🌍single1🌍 +284884&amp;#x1f3…🌠1F320nosingle::🌠single1🌠 +285885&amp;#x1f6…🙀1F640nosingle::🙀single1🙀 +286886&amp;#9642…▪️25AA-FE0Fnosingle::▪️single1▪️ +287887&amp;#9729…☁️2601-FE0Fnosingle::☁️single1☁️ +288888&amp;#x1f4…👹1F479nosingle::👹single1👹 +289889&amp;#x1f3…🍉1F349nosingle::🍉single1🍉 +290890&amp;#x1f4…🐥1F425nosingle::🐥single1🐥 +291891&amp;#x1f3…🌶️1F336-FE0Fnosingle::🌶️single1🌶️ +2928921&amp;#650…1️⃣31-FE0F-20E3nosingle::1️⃣single11️⃣ +293893&amp;#x1f3…🌵1F335nosingle::🌵single1🌵 +294894&amp;#x1f1…🇮🇳1F1EE-1F1F3nosingle::🇮🇳single1🇮🇳 +295895&amp;#x1f4…👧1F467nosingle::👧single1👧 +296896&amp;#x1f3…🍄1F344nosingle::🍄single1🍄 +297897&amp;#x1f4…👮1F46Enosingle::👮single1👮 +298898&amp;#x1f4…💮1F4AEnosingle::💮single1💮 +299899&amp;#x1f4…🐰1F430nosingle::🐰single1🐰 +3008100&amp;#x1f5…🔷1F537nosingle::🔷single1🔷 +3018101&amp;#x1f3…🌾1F33Enosingle::🌾single1🌾 +3028102&amp;#x1f5…🔹1F539nosingle::🔹single1🔹 +3038103&amp;#x1f1…🇹🇷1F1F9-1F1F7nosingle::🇹🇷single1🇹🇷 +3048104&amp;#x1f9…🥇1F947nosingle::🥇single1🥇 +3058105&amp;#x1f1…🇮🇹1F1EE-1F1F9nosingle::🇮🇹single1🇮🇹 +30691&amp;#x1f3…🍪1F36Anosingle::🍪single1🍪 +30792&amp;#x1f1…🇦🇷1F1E6-1F1F7nosingle::🇦🇷single1🇦🇷 +30893&amp;#x1f6…🛑1F6D1nosingle::🛑single1🛑 +30994&amp;#x1f4…🐍1F40Dnosingle::🐍single1🐍 +31095&amp;#x1f3…🎓1F393nosingle::🎓single1🎓 +31196&amp;#x1f1…🇨🇦1F1E8-1F1E6nosingle::🇨🇦single1🇨🇦 +31297&amp;#x1f3…🍏1F34Fnosingle::🍏single1🍏 +31398&amp;#x1f9…🦁1F981nosingle::🦁single1🦁 +31499&amp;#x1f6…😽1F63Dnosingle::😽single1😽 +315910&amp;#x1f6…🚬1F6ACnosingle::🚬single1🚬 +316911&amp;#x1f3…🍖1F356nosingle::🍖single1🍖 +317912&amp;#x1f3…🍴1F374nosingle::🍴single1🍴 +318913&amp;#x1f1…🆘1F198nosingle::🆘single1🆘 +319914&amp;#x1f9…🤜1F91Cnosingle::🤜single1🤜 +320915&amp;#x1f3…🍿1F37Fnosingle::🍿single1🍿 +321916&amp;#x1f3…🍔1F354nosingle::🍔single1🍔 +322917&amp;#x1f4…📝1F4DDnosingle::📝single1📝 +323918&amp;#x1f1…🇯🇵1F1EF-1F1F5nosingle::🇯🇵single1🇯🇵 +324919&amp;#x1f3…🍮1F36Enosingle::🍮single1🍮 +325920&amp;#x1f3…🍇1F347nosingle::🍇single1🍇 +3269212&amp;#650…2️⃣32-FE0F-20E3nosingle::2️⃣single12️⃣ +327922&amp;#x1f3…🏠1F3E0nosingle::🏠single1🏠 +328923&amp;#x1f9…🤰1F930nosingle::🤰single1🤰 +329924&amp;#x1f4…🐣1F423nosingle::🐣single1🐣 +330925&amp;#x1f4…🐒1F412nosingle::🐒single1🐒 +331926&amp;#x1f4…👦1F466nosingle::👦single1👦 +332927&amp;#x1f3…🍩1F369nosingle::🍩single1🍩 +333928&amp;#x1f3…🍣1F363nosingle::🍣single1🍣 +334929&amp;#x1f9…🤛1F91Bnosingle::🤛single1🤛 +335930&amp;#x1f4…👯1F46Fnosingle::👯single1👯 +336931&amp;#x1f3…🏳️‍🌈1F3F3-FE0F-200D-1F308nosingle::🏳️‍🌈single1🏳️‍🌈 +337932&amp;spade…♠️2660-FE0Fnosingle::♠️single1♠️ +338933&amp;#x1f3…🌲1F332nosingle::🌲single1🌲 +339934&amp;#x1f4…🐴1F434nosingle::🐴single1🐴 +340935&amp;#x1f3…🍛1F35Bnosingle::🍛single1🍛 +341936&amp;#x1f3…🎆1F386nosingle::🎆single1🎆 +342937&amp;#x1f4…💑1F491nosingle::💑single1💑 +343938&amp;#x1f3…🍞1F35Enosingle::🍞single1🍞 +344939&amp;#x1f3…🍯1F36Fnosingle::🍯single1🍯 +345940&amp;#9732…☄️2604-FE0Fnosingle::☄️single1☄️ +346941&amp;#x1f6…😸1F638nosingle::😸single1😸 +347942&amp;#x1f3…🍚1F35Anosingle::🍚single1🍚 +348943&amp;#x1f3…🎬1F3ACnosingle::🎬single1🎬 +349944&amp;#x1f3…🎙️1F399-FE0Fnosingle::🎙️single1🎙️ +350945&amp;#x1f1…🇨🇴1F1E8-1F1F4nosingle::🇨🇴single1🇨🇴 +351946&amp;#x1f4…🐳1F433nosingle::🐳single1🐳 +352947&amp;#x1f9…🦀1F980nosingle::🦀single1🦀 +353948&amp;#x1f9…🥃1F943nosingle::🥃single1🥃 +354949&amp;#x1f5…🔸1F538nosingle::🔸single1🔸 +355950&amp;#x1f4…💊1F48Anosingle::💊single1💊 +356951&amp;#x1f4…🐎1F40Enosingle::🐎single1🐎 +357952&amp;#x1f3…🍹1F379nosingle::🍹single1🍹 +358953&amp;diams…♦️2666-FE0Fnosingle::♦️single1♦️ +359954&amp;#x1f5…🔮1F52Enosingle::🔮single1🔮 +360955&amp;#x1f4…👨1F468nosingle::👨single1👨 +361956&amp;#x1f3…🍸1F378nosingle::🍸single1🍸 +362957&amp;#x1f3…🌏1F30Fnosingle::🌏single1🌏 +363958&amp;#x1f4…👴1F474nosingle::👴single1👴 +364959&amp;#x1f9…🧢1F9E2nosingle::🧢single1🧢 +365960&amp;#x1f4…🐽1F43Dnosingle::🐽single1🐽 +366961&amp;#x1f4…🐔1F414nosingle::🐔single1🐔 +367962&amp;#x1f3…🎻1F3BBnosingle::🎻single1🎻 +368963&amp;#1101…⬆️2B06-FE0Fnosingle::⬆️single1⬆️ +369964&amp;#9986…✂️2702-FE0Fnosingle::✂️single1✂️ +370965&amp;#x1f4…👫1F46Bnosingle::👫single1👫 +371966&amp;#x1f4…👣1F463nosingle::👣single1👣 +372967&amp;#x1f4…🐯1F42Fnosingle::🐯single1🐯 +373968&amp;#x1f3…🎮1F3AEnosingle::🎮single1🎮 +374969&amp;#x1f3…🍵1F375nosingle::🍵single1🍵 +375970&amp;#x1f4…🐦1F426nosingle::🐦single1🐦 +376971&amp;#x1f1…🇬🇧1F1EC-1F1E7nosingle::🇬🇧single1🇬🇧 +377972&amp;#1233…〰️3030-FE0Fnosingle::〰️single1〰️ +378973&amp;#x1f4…👭1F46Dnosingle::👭single1👭 +379974&amp;#x1f4…🐬1F42Cnosingle::🐬single1🐬 +380975&amp;#x1f3…🍟1F35Fnosingle::🍟single1🍟 +381976&amp;#x1f4…👙1F459nosingle::👙single1👙 +382977&amp;#1000…✖️2716-FE0Fnosingle::✖️single1✖️ +383978&amp;#x1f4…📩1F4E9nosingle::📩single1📩 +384979&amp;#x1f4…👵1F475nosingle::👵single1👵 +385980&amp;#x1f3…🍨1F368nosingle::🍨single1🍨 +386981&amp;#x1f1…🇫🇷1F1EB-1F1F7nosingle::🇫🇷single1🇫🇷 +387982&amp;#x1f4…🐖1F416nosingle::🐖single1🐖 +388983&amp;#1001…✝️271D-FE0Fnosingle::✝️single1✝️ +389984&amp;#9851…♻️267B-FE0Fnosingle::♻️single1♻️ +390985&amp;#x1f9…🥊1F94Anosingle::🥊single1🥊 +391986&amp;#x1f9…🦅1F985nosingle::🦅single1🦅 +392987&amp;#x1f4…💬1F4ACnosingle::💬single1💬 +393988&amp;#x1f1…🇨🇱1F1E8-1F1F1nosingle::🇨🇱single1🇨🇱 +394989&amp;#x1f4…🐢1F422nosingle::🐢single1🐢 +395990&amp;#x1f5…🔰1F530nosingle::🔰single1🔰 +396991&amp;#x1f5…🔶1F536nosingle::🔶single1🔶 +397992&amp;#x1f3…🎗️1F397-FE0Fnosingle::🎗️single1🎗️ +398993&amp;#x1f4…💄1F484nosingle::💄single1💄 +399994&amp;#x1f4…👠1F460nosingle::👠single1👠 +400995&amp;#x1f9…🥕1F955nosingle::🥕single1🥕 +401996&amp;#1013…➖2796nosingle::➖single1➖ +402997&amp;#x1f4…🐺1F43Anosingle::🐺single1🐺 +403998&amp;#x1f4…📖1F4D6nosingle::📖single1📖 +404999&amp;#x1f3…🍍1F34Dnosingle::🍍single1🍍 +4059100&amp;#x1f3…🌃1F303nosingle::🌃single1🌃 +4069101&amp;#1003…✴️2734-FE0Fnosingle::✴️single1✴️ +4079102&amp;#x1f3…🌌1F30Cnosingle::🌌single1🌌 +4089103&amp;#x1f4…🐓1F413nosingle::🐓single1🐓 +4099104&amp;#x1f4…👂1F442nosingle::👂single1👂 +4109105&amp;#x1f3…🍤1F364nosingle::🍤single1🍤 +4119106&amp;#x1f4…🐐1F410nosingle::🐐single1🐐 +4129107&amp;#x1f5…🔻1F53Bnosingle::🔻single1🔻 +4139108&amp;#x1f4…💻1F4BBnosingle::💻single1💻 +4149109&amp;#x1f9…🦐1F990nosingle::🦐single1🦐 +4159110&amp;#x1f1…🇩🇪1F1E9-1F1EAnosingle::🇩🇪single1🇩🇪 +4169111&amp;#x1f3…🌛1F31Bnosingle::🌛single1🌛 +4179112&amp;#8600…↘️2198-FE0Fnosingle::↘️single1↘️ +4189113&amp;#9999…✏️270F-FE0Fnosingle::✏️single1✏️ +4199114&amp;#x1f9…🧘1F9D8nosingle::🧘single1🧘 +4209115&amp;#x1f9…🥁1F941nosingle::🥁single1🥁 +4219116&amp;#x1f3…🏖️1F3D6-FE0Fnosingle::🏖️single1🏖️ +4229117&amp;#9884…⚜️269C-FE0Fnosingle::⚜️single1⚜️ +4239118&amp;#1006…❕2755nosingle::❕single1❕ +4249119&amp;#x1f1…🅰️1F170-FE0Fnosingle::🅰️single1🅰️ +4259120&amp;#x1f6…🚴1F6B4nosingle::🚴single1🚴 +4269121&amp;#x1f4…💠1F4A0nosingle::💠single1💠 +4279122&amp;#1295…㊗️3297-FE0Fnosingle::㊗️single1㊗️ +4289123&amp;#x1f4…🐙1F419nosingle::🐙single1🐙 +4299124&amp;clubs…♣️2663-FE0Fnosingle::♣️single1♣️ +4309125&amp;#x1f3…🍡1F361nosingle::🍡single1🍡 +4319126&amp;#9193…⏩23E9nosingle::⏩single1⏩ +4329127&amp;#x1f3…🎨1F3A8nosingle::🎨single1🎨 +4339128&amp;#x1f4…🐠1F420nosingle::🐠single1🐠 +4349129&amp;#x1f1…🇰🇷1F1F0-1F1F7nosingle::🇰🇷single1🇰🇷 +4359130&amp;#x1f3…🍗1F357nosingle::🍗single1🍗 +4369131&amp;#x1f6…🚮1F6AEnosingle::🚮single1🚮 +4379132&amp;#9643…▫️25AB-FE0Fnosingle::▫️single1▫️ +4389133&amp;#x1f3…🌪️1F32A-FE0Fnosingle::🌪️single1🌪️ +4399134&amp;#x1f6…😼1F63Cnosingle::😼single1😼 +4409135&amp;#x1f4…👤1F464nosingle::👤single1👤 +4419136&amp;#x1f3…🏊1F3CAnosingle::🏊single1🏊 +4429137&amp;#x1f3…🌽1F33Dnosingle::🌽single1🌽 +4439138&amp;#x1f3…🎩1F3A9nosingle::🎩single1🎩 +4449139&amp;#x1f1…🇪🇸1F1EA-1F1F8nosingle::🇪🇸single1🇪🇸 +4459140&amp;#x1f3…🎹1F3B9nosingle::🎹single1🎹 +4469141&amp;#x1f3…🍈1F348nosingle::🍈single1🍈 +4479142&amp;#9664…◀️25C0-FE0Fnosingle::◀️single1◀️ +4489143&amp;harr;…↔️2194-FE0Fnosingle::↔️single1↔️ +4499144&amp;#x1f3…🏡1F3E1nosingle::🏡single1🏡 +4509145&amp;#x1f1…🇵🇰1F1F5-1F1F0nosingle::🇵🇰single1🇵🇰 +4519146&amp;#x1f3…🎇1F387nosingle::🎇single1🎇 +4529147&amp;#x1f9…🥩1F969nosingle::🥩single1🥩 +4539148&amp;#x1f4…🐞1F41Enosingle::🐞single1🐞 +45491493&amp;#650…3️⃣33-FE0F-20E3nosingle::3️⃣single13️⃣ +4559150&amp;#1101…⬅️2B05-FE0Fnosingle::⬅️single1⬅️ +4569151&amp;#x1f3…🌐1F310nosingle::🌐single1🌐 +4579152&amp;#8599…↗️2197-FE0Fnosingle::↗️single1↗️ +4589153&amp;#x1f3…🍽️1F37D-FE0Fnosingle::🍽️single1🍽️ +4599154&amp;#x1f9…🧀1F9C0nosingle::🧀single1🧀 +4609155&amp;#x1f9…🥦1F966nosingle::🥦single1🥦 +4619156&amp;#x1f4…🐜1F41Cnosingle::🐜single1🐜 +4629157&amp;#9876…⚔️2694-FE0Fnosingle::⚔️single1⚔️ +463101&amp;#x1f6…😺1F63Anosingle::😺single1😺 +464102&amp;#x1f9…🥞1F95Enosingle::🥞single1🥞 +465103&amp;#x1f3…🏄1F3C4nosingle::🏄single1🏄 +466104&amp;#x1f5…🔨1F528nosingle::🔨single1🔨 +467105&amp;#x1f3…🏝️1F3DD-FE0Fnosingle::🏝️single1🏝️ +468106&amp;#x1f5…🔆1F506nosingle::🔆single1🔆 +469107&amp;#x1f4…👥1F465nosingle::👥single1👥 +470108&amp;#x1f4…👓1F453nosingle::👓single1👓 +471109&amp;#x1f9…🥒1F952nosingle::🥒single1🥒 +4721010&amp;#x1f3…🏈1F3C8nosingle::🏈single1🏈 +4731011&amp;#x1f1…🇵🇭1F1F5-1F1EDnosingle::🇵🇭single1🇵🇭 +4741012&amp;#x1f3…🏋️1F3CB-FE0Fnosingle::🏋️single1🏋️ +47510130&amp;#650…0️⃣30-FE0F-20E3nosingle::0️⃣single10️⃣ +4761014&amp;#x1f6…🚘1F698nosingle::🚘single1🚘 +4771015&amp;#x1f9…🦖1F996nosingle::🦖single1🦖 +4781016&amp;#x1f3…🌕1F315nosingle::🌕single1🌕 +4791017&amp;#x1f3…🎭1F3ADnosingle::🎭single1🎭 +4801018&amp;#x1f4…👾1F47Enosingle::👾single1👾 +4811019&amp;#x1f3…🍳1F373nosingle::🍳single1🍳 +4821020&amp;#x1f3…🏵️1F3F5-FE0Fnosingle::🏵️single1🏵️ +4831021&amp;#x1f3…🍧1F367nosingle::🍧single1🍧 +4841022&amp;#x1f5…🔗1F517nosingle::🔗single1🔗 +4851023&amp;#x1f5…🕋1F54Bnosingle::🕋single1🕋 +4861024&amp;#9731…☃️2603-FE0Fnosingle::☃️single1☃️ +4871025&amp;#x1f3…🌅1F305nosingle::🌅single1🌅 +4881026&amp;#x1f9…🤴1F934nosingle::🤴single1🤴 +4891027&amp;#x1f5…🖖1F596nosingle::🖖single1🖖 +4901028&amp;#x1f4…🐊1F40Anosingle::🐊single1🐊 +4911029&amp;#x1f4…🐘1F418nosingle::🐘single1🐘 +4921030&amp;#x1f3…🌤️1F324-FE0Fnosingle::🌤️single1🌤️ +4931031&amp;#x1f9…🥑1F951nosingle::🥑single1🥑 +4941032&amp;#x1f9…🥚1F95Anosingle::🥚single1🥚 +4951033&amp;#9928…⛈️26C8-FE0Fnosingle::⛈️single1⛈️ +4961034&amp;#x1f4…🐵1F435nosingle::🐵single1🐵 +4971035&amp;#x1f5…🔜1F51Cnosingle::🔜single1🔜 +4981036&amp;#x1f3…🍶1F376nosingle::🍶single1🍶 +4991037&amp;#x1f4…🐄1F404nosingle::🐄single1🐄 +5001038&amp;#x1f1…🇻🇪1F1FB-1F1EAnosingle::🇻🇪single1🇻🇪 +5011039&amp;#x1f4…🐮1F42Enosingle::🐮single1🐮 +5021040&amp;#x1f9…🦈1F988nosingle::🦈single1🦈 +5031041&amp;#x1f6…🚲1F6B2nosingle::🚲single1🚲 +5041042&amp;#9940…⛔26D4nosingle::⛔single1⛔ +5051043&amp;#x1f5…🕯️1F56F-FE0Fnosingle::🕯️single1🕯️ +5061044&amp;#1013…➕2795nosingle::➕single1➕ +5071045&amp;#x1f5…🔺1F53Anosingle::🔺single1🔺 +5081046&amp;#x1f4…💇1F487nosingle::💇single1💇 +5091047&amp;#x1f9…🧠1F9E0nosingle::🧠single1🧠 +5101048&amp;#x1f4…📻1F4FBnosingle::📻single1📻 +5111049&amp;#x1f9…🥤1F964nosingle::🥤single1🥤 +5121050&amp;#x1f3…🍝1F35Dnosingle::🍝single1🍝 +5131051&amp;#x1f3…🍥1F365nosingle::🍥single1🍥 +5141052&amp;#x1f4…💴1F4B4nosingle::💴single1💴 +5151053&amp;#x1f3…🌬️1F32C-FE0Fnosingle::🌬️single1🌬️ +5161054&amp;#x1f9…🥓1F953nosingle::🥓single1🥓 +5171055&amp;#x1f6…🙍1F64Dnosingle::🙍single1🙍 +5181056&amp;#9875…⚓2693nosingle::⚓single1⚓ +5191057&amp;#x1f4…👰1F470nosingle::👰single1👰 +5201058&amp;#x1f4…🐂1F402nosingle::🐂single1🐂 +5211059&amp;#x1f4…📽️1F4FD-FE0Fnosingle::📽️single1📽️ +5221060&amp;#x1f3…🏅1F3C5nosingle::🏅single1🏅 +5231061&amp;#9925…⛅26C5nosingle::⛅single1⛅ +5241062&amp;#x1f1…🇦🇪1F1E6-1F1EAnosingle::🇦🇪single1🇦🇪 +5251063&amp;#x1f1…🇵🇪1F1F5-1F1EAnosingle::🇵🇪single1🇵🇪 +5261064&amp;#x1f9…🧜1F9DCnosingle::🧜single1🧜 +5271065&amp;#x1f4…📮1F4EEnosingle::📮single1📮 +5281066&amp;#9971…⛳26F3nosingle::⛳single1⛳ +5291067&amp;#x1f5…🔽1F53Dnosingle::🔽single1🔽 +5301068&amp;#x1f6…🚂1F682nosingle::🚂single1🚂 +5311069&amp;#x1f3…🏌️1F3CC-FE0Fnosingle::🏌️single1🏌️ +5321070&amp;#x1f4…🐇1F407nosingle::🐇single1🐇 +5331071&amp;#x1f3…🏍️1F3CD-FE0Fnosingle::🏍️single1🏍️ +5341072&amp;#x1f3…🎲1F3B2nosingle::🎲single1🎲 +5351073&amp;#x1f9…🥛1F95Bnosingle::🥛single1🥛 +5361074&amp;#x1f3…🎣1F3A3nosingle::🎣single1🎣 +5371075&amp;#x1f4…👱1F471nosingle::👱single1👱 +5381076&amp;#x1f3…🎏1F38Fnosingle::🎏single1🎏 +5391077&amp;#x1f5…🕷️1F577-FE0Fnosingle::🕷️single1🕷️ +5401078&amp;#x1f9…🦍1F98Dnosingle::🦍single1🦍 +5411079&amp;#x1f5…🔘1F518nosingle::🔘single1🔘 +5421080&amp;#x1f4…🐅1F405nosingle::🐅single1🐅 +5431081&amp;#x1f3…🏇1F3C7nosingle::🏇single1🏇 +5441082&amp;#x1f5…🔐1F510nosingle::🔐single1🔐 +5451083&amp;#x1f3…🏩1F3E9nosingle::🏩single1🏩 +5461084&amp;#x1f4…👺1F47Anosingle::👺single1👺 +5471085&amp;#x1f1…🅱️1F171-FE0Fnosingle::🅱️single1🅱️ +5481086&amp;#x1f6…🚙1F699nosingle::🚙single1🚙 +5491087&amp;#x1f4…🐧1F427nosingle::🐧single1🐧 +5501088&amp;#9878…⚖️2696-FE0Fnosingle::⚖️single1⚖️ +5511089&amp;#x1f3…🎃1F383nosingle::🎃single1🎃 +5521090&amp;#x1f3…🌄1F304nosingle::🌄single1🌄 +5531091&amp;#x1f3…🎾1F3BEnosingle::🎾single1🎾 +5541092&amp;#x1f4…🐚1F41Anosingle::🐚single1🐚 +5551093&amp;#x1f3…🎺1F3BAnosingle::🎺single1🎺 +5561094&amp;#1005…❇️2747-FE0Fnosingle::❇️single1❇️ +5571095&amp;#x1f3…🎫1F3ABnosingle::🎫single1🎫 +5581096&amp;#8986…⌚231Anosingle::⌚single1⌚ +5591097&amp;#x1f3…🌋1F30Bnosingle::🌋single1🌋 +5601098&amp;#x1f4…💒1F492nosingle::💒single1💒 +5611099&amp;#x1f4…👳1F473nosingle::👳single1👳 +56210100&amp;#1006…❎274Enosingle::❎single1❎ +56310101&amp;#x1f4…👟1F45Fnosingle::👟single1👟 +56410102&amp;#x1f4…👃1F443nosingle::👃single1👃 +56510103&amp;#x1f6…🛌1F6CCnosingle::🛌single1🛌 +56610104&amp;#x1f6…🚓1F693nosingle::🚓single1🚓 +56710105&amp;#9196…⏬23ECnosingle::⏬single1⏬ +56810106&amp;#x1f4…📈1F4C8nosingle::📈single1📈 +56910107&amp;#9924…⛄26C4nosingle::⛄single1⛄ +57010108&amp;#9201…⏱️23F1-FE0Fnosingle::⏱️single1⏱️ +57110109&amp;#x1f6…😾1F63Enosingle::😾single1😾 +57210110&amp;#x1f6…🛫1F6EBnosingle::🛫single1🛫 +57310111&amp;#x1f9…🤱1F931nosingle::🤱single1🤱 +57410112&amp;#x1f3…🍐1F350nosingle::🍐single1🍐 +57510113&amp;#9774…☮️262E-FE0Fnosingle::☮️single1☮️ +57610114&amp;#x1f6…🚃1F683nosingle::🚃single1🚃 +57710115&amp;#9203…⏳23F3nosingle::⏳single1⏳ +57810116&amp;#x1f3…🌜1F31Cnosingle::🌜single1🌜 +57910117&amp;#x1f4…📹1F4F9nosingle::📹single1📹 +58010118&amp;#x1f4…🐛1F41Bnosingle::🐛single1🐛 +58110119&amp;#x1f4…👔1F454nosingle::👔single1👔 +58210120&amp;#x1f4…👗1F457nosingle::👗single1👗 +58310121&amp;#x1f4…🐌1F40Cnosingle::🐌single1🐌 +58410122&amp;#x1f3…🎱1F3B1nosingle::🎱single1🎱 +58510123&amp;#x1f3…🌰1F330nosingle::🌰single1🌰 +58610124&amp;#x1f3…🌮1F32Enosingle::🌮single1🌮 +58710125&amp;#x1f5…🕵️1F575-FE0Fnosingle::🕵️single1🕵️ +58810126&amp;#x1f5…🔅1F505nosingle::🔅single1🔅 +58910127&amp;#9993…✉️2709-FE0Fnosingle::✉️single1✉️ +59010128&amp;#x1f1…🇪🇬1F1EA-1F1ECnosingle::🇪🇬single1🇪🇬 +59110129&amp;#x1f6…🚑1F691nosingle::🚑single1🚑 +59210130&amp;#x1f4…📦1F4E6nosingle::📦single1📦 +59310131&amp;#x1f9…🤥1F925nosingle::🤥single1🤥 +59410132&amp;#x1f5…🔄1F504nosingle::🔄single1🔄 +59510133&amp;#x1f9…🤳1F933nosingle::🤳single1🤳 +59610134&amp;#x1f4…💲1F4B2nosingle::💲single1💲 +59710135&amp;#x1f3…🎋1F38Bnosingle::🎋single1🎋 +59810136&amp;#x1f5…🗓️1F5D3-FE0Fnosingle::🗓️single1🗓️ +59910137&amp;#x1f9…🤖1F916nosingle::🤖single1🤖 +60010138&amp;#x1f9…🥔1F954nosingle::🥔single1🥔 +60110139&amp;#x1f1…🆗1F197nosingle::🆗single1🆗 +60210140&amp;#x1f5…🔑1F511nosingle::🔑single1🔑 +60310141&amp;#x1f1…🇨🇳1F1E8-1F1F3nosingle::🇨🇳single1🇨🇳 +60410142&amp;#x1f4…🐤1F424nosingle::🐤single1🐤 +605101434&amp;#650…4️⃣34-FE0F-20E3nosingle::4️⃣single14️⃣ +60610144&amp;#x1f4…🐑1F411nosingle::🐑single1🐑 +60710145&amp;#1016…➰27B0nosingle::➰single1➰ +60810146&amp;#x1f4…👩‍🎓1F469-200D-1F393nosingle::👩‍🎓single1👩‍🎓 +60910147&amp;#9730…☂️2602-FE0Fnosingle::☂️single1☂️ +61010148&amp;#x1f1…🇦🇹1F1E6-1F1F9nosingle::🇦🇹single1🇦🇹 +61110149&amp;#x1f9…🦆1F986nosingle::🦆single1🦆 +61210150&amp;#x1f6…🚌1F68Cnosingle::🚌single1🚌 +61310151&amp;#x1f4…💿1F4BFnosingle::💿single1💿 +61410152&amp;#x1f3…🏥1F3E5nosingle::🏥single1🏥 +61510153&amp;#x1f4…🐋1F40Bnosingle::🐋single1🐋 +61610154&amp;#x1f6…🚒1F692nosingle::🚒single1🚒 +61710155&amp;#x1f3…🏐1F3D0nosingle::🏐single1🏐 +61810156&amp;#x1f1…🇪🇨1F1EA-1F1E8nosingle::🇪🇨single1🇪🇨 +61910157&amp;#x1f9…🥐1F950nosingle::🥐single1🥐 +62010158&amp;#x1f3…🎷1F3B7nosingle::🎷single1🎷 +62110159&amp;#x1f5…🗽1F5FDnosingle::🗽single1🗽 +62210160&amp;#x1f5…🗡️1F5E1-FE0Fnosingle::🗡️single1🗡️ +62310161&amp;#x1f3…🏏1F3CFnosingle::🏏single1🏏 +62410162&amp;#x1f4…🐭1F42Dnosingle::🐭single1🐭 +62510163&amp;#x1f6…🙎1F64Enosingle::🙎single1🙎 +62610164&amp;#x1f3…🌑1F311nosingle::🌑single1🌑 +62710165&amp;#x1f6…🚔1F694nosingle::🚔single1🚔 +62810166&amp;#x1f1…🇮🇩1F1EE-1F1E9nosingle::🇮🇩single1🇮🇩 +62910167&amp;#x1f6…🚿1F6BFnosingle::🚿single1🚿 +63010168&amp;#x1f9…🥝1F95Dnosingle::🥝single1🥝 +63110169&amp;#x1f5…🕌1F54Cnosingle::🕌single1🕌 +63210170&amp;#x1f4…🐀1F400nosingle::🐀single1🐀 +63310171&amp;#x1f6…🛡️1F6E1-FE0Fnosingle::🛡️single1🛡️ +63410172&amp;#x1f5…🔒1F512nosingle::🔒single1🔒 +63510173&amp;#1003…✳️2733-FE0Fnosingle::✳️single1✳️ +63610174&amp;#x1f5…🕶️1F576-FE0Fnosingle::🕶️single1🕶️ +63710175&amp;#x1f4…👩‍❤️‍💋‍👩1F469-200D-2764-FE0F-200D-1F48B-200D-1F469nosingle::👩‍❤️‍💋‍👩single1👩‍❤️‍💋‍👩 +63810176&amp;#x1f3…🎟️1F39F-FE0Fnosingle::🎟️single1🎟️ +63910177&amp;#x1f4…🐉1F409nosingle::🐉single1🐉 +64010178&amp;#x1f5…🔱1F531nosingle::🔱single1🔱 +64110179&amp;#x1f5…🔎1F50Enosingle::🔎single1🔎 +64210180&amp;#x1f1…🇦🇺1F1E6-1F1FAnosingle::🇦🇺single1🇦🇺 +64310181&amp;#9904…⚰️26B0-FE0Fnosingle::⚰️single1⚰️ +64410182&amp;#x1f4…🐩1F429nosingle::🐩single1🐩 +64510183&amp;#x1f9…🦑1F991nosingle::🦑single1🦑 +64610184&amp;#x1f9…🧟1F9DFnosingle::🧟single1🧟 +64710185&amp;#x1f1…🆕1F195nosingle::🆕single1🆕 +64810186&amp;#x1f9…🦊1F98Anosingle::🦊single1🦊 +64910187&amp;#x1f4…👕1F455nosingle::👕single1👕 +65010188&amp;#x1f3…🏹1F3F9nosingle::🏹single1🏹 +65110189&amp;#x1f1…🇩🇿1F1E9-1F1FFnosingle::🇩🇿single1🇩🇿 +65210190&amp;#x1f4…👬1F46Cnosingle::👬single1👬 +65310191&amp;#x1f3…🍱1F371nosingle::🍱single1🍱 +65410192&amp;#x1f4…📰1F4F0nosingle::📰single1📰 +65510193&amp;#x1f9…🥋1F94Bnosingle::🥋single1🥋 +65610194&amp;#x1f6…🚤1F6A4nosingle::🚤single1🚤 +65710195&amp;#x1f3…🏰1F3F0nosingle::🏰single1🏰 +658101965&amp;#650…5️⃣35-FE0F-20E3nosingle::5️⃣single15️⃣ +65910197&amp;#x1f9…🦉1F989nosingle::🦉single1🦉 +66010198&amp;#x1f6…🚢1F6A2nosingle::🚢single1🚢 +66110199&amp;#x1f3…🌨️1F328-FE0Fnosingle::🌨️single1🌨️ +66210200&amp;#x1f4…📆1F4C6nosingle::📆single1📆 +66310201&amp;#x1f5…🗝️1F5DD-FE0Fnosingle::🗝️single1🗝️ +66410202&amp;#x1f3…🎌1F38Cnosingle::🎌single1🎌 +66510203&amp;#x1f9…🧔1F9D4nosingle::🧔single1🧔 +66610204&amp;#x1f4…💳1F4B3nosingle::💳single1💳 +66710205&amp;#x1f1…🇺🇾1F1FA-1F1FEnosingle::🇺🇾single1🇺🇾 +66810206&amp;#x1f9…🥗1F957nosingle::🥗single1🥗 +66910207&amp;#9775…☯️262F-FE0Fnosingle::☯️single1☯️ +67010208&amp;#9881…⚙️2699-FE0Fnosingle::⚙️single1⚙️ +67110209&amp;#x1f4…💶1F4B6nosingle::💶single1💶 +67210210&amp;#9961…⛩️26E9-FE0Fnosingle::⛩️single1⛩️ +67310211&amp;#x1f5…🗻1F5FBnosingle::🗻single1🗻 +67410212&amp;#1000…✒️2712-FE0Fnosingle::✒️single1✒️ +67510213&amp;#x1f1…🇺🇲1F1FA-1F1F2nosingle::🇺🇲single1🇺🇲 +67610214&amp;#x1f1…🇵🇹1F1F5-1F1F9nosingle::🇵🇹single1🇵🇹 +67710215&amp;#x1f3…🍠1F360nosingle::🍠single1🍠 +678111&amp;#x1f4…👷1F477nosingle::👷single1👷 +679112&amp;#x1f6…🛍️1F6CD-FE0Fnosingle::🛍️single1🛍️ +680113&amp;#x1f3…🏎️1F3CE-FE0Fnosingle::🏎️single1🏎️ +681114&amp;#x1f5…🔛1F51Bnosingle::🔛single1🔛 +682115&amp;#x1f5…🔙1F519nosingle::🔙single1🔙 +683116&amp;#x1f5…🗼1F5FCnosingle::🗼single1🗼 +684117&amp;#x1f3…🎖️1F396-FE0Fnosingle::🎖️single1🎖️ +685118&amp;#x1f6…🚺1F6BAnosingle::🚺single1🚺 +686119&amp;#x1f4…🐹1F439nosingle::🐹single1🐹 +6871110&amp;#x1f9…🥖1F956nosingle::🥖single1🥖 +6881111&amp;#x1f1…🇵🇷1F1F5-1F1F7nosingle::🇵🇷single1🇵🇷 +6891112&amp;#x1f3…🎡1F3A1nosingle::🎡single1🎡 +6901113&amp;#x1f5…🔍1F50Dnosingle::🔍single1🔍 +6911114&amp;#x1f3…🏟️1F3DF-FE0Fnosingle::🏟️single1🏟️ +6921115&amp;#x1f5…🖥️1F5A5-FE0Fnosingle::🖥️single1🖥️ +6931116&amp;#x1f3…🌭1F32Dnosingle::🌭single1🌭 +6941117&amp;#x1f1…🇹🇭1F1F9-1F1EDnosingle::🇹🇭single1🇹🇭 +6951118&amp;#1001…✡️2721-FE0Fnosingle::✡️single1✡️ +6961119&amp;#x1f3…🏒1F3D2nosingle::🏒single1🏒 +6971120&amp;#x1f6…🛀1F6C0nosingle::🛀single1🛀 +6981121&amp;#x1f4…📅1F4C5nosingle::📅single1📅 +6991122&amp;#x1f5…🖋️1F58B-FE0Fnosingle::🖋️single1🖋️ +7001123&amp;#x1f1…🇸🇪1F1F8-1F1EAnosingle::🇸🇪single1🇸🇪 +7011124&amp;#x1f3…🎞️1F39E-FE0Fnosingle::🎞️single1🎞️ +7021125&amp;#x1f3…🎪1F3AAnosingle::🎪single1🎪 +7031126&amp;#x1f3…🍙1F359nosingle::🍙single1🍙 +7041127&amp;#x1f6…🛁1F6C1nosingle::🛁single1🛁 +7051128&amp;#x1f4…📕1F4D5nosingle::📕single1📕 +7061129&amp;#x1f4…👒1F452nosingle::👒single1👒 +7071130&amp;#x1f5…🖇️1F587-FE0Fnosingle::🖇️single1🖇️ +7081131&amp;#1006…❔2754nosingle::❔single1❔ +7091132&amp;#x1f3…🎄1F384nosingle::🎄single1🎄 +7101133&amp;#x1f5…🕸️1F578-FE0Fnosingle::🕸️single1🕸️ +7111134&amp;#x1f9…🥜1F95Cnosingle::🥜single1🥜 +7121135&amp;#x1f5…🕴️1F574-FE0Fnosingle::🕴️single1🕴️ +7131136&amp;#x1f9…🥟1F95Fnosingle::🥟single1🥟 +7141137&amp;#8601…↙️2199-FE0Fnosingle::↙️single1↙️ +7151138&amp;#9726…◾25FEnosingle::◾single1◾ +7161139&amp;#x1f4…🐲1F432nosingle::🐲single1🐲 +7171140&amp;#x1f3…🏴1F3F4nosingle::🏴single1🏴 +7181141&amp;#x1f9…🦌1F98Cnosingle::🦌single1🦌 +7191142&amp;#x1f1…🆔1F194nosingle::🆔single1🆔 +7201143&amp;#x1f4…👛1F45Bnosingle::👛single1👛 +7211144&amp;#x1f6…🚚1F69Anosingle::🚚single1🚚 +7221145&amp;#8618…↪️21AA-FE0Fnosingle::↪️single1↪️ +7231146&amp;#x1f1…🇬🇷1F1EC-1F1F7nosingle::🇬🇷single1🇬🇷 +7241147&amp;#x1f4…📿1F4FFnosingle::📿single1📿 +7251148&amp;#x1f3…🌫️1F32B-FE0Fnosingle::🌫️single1🌫️ +7261149&amp;#x1f3…🌂1F302nosingle::🌂single1🌂 +7271150&amp;#x1f5…🕉️1F549-FE0Fnosingle::🕉️single1🕉️ +7281151&amp;#x1f1…🆚1F19Anosingle::🆚single1🆚 +7291152&amp;#x1f4…📉1F4C9nosingle::📉single1📉 +7301153&amp;#x1f5…🗨️1F5E8-FE0Fnosingle::🗨️single1🗨️ +7311154&amp;#x1f6…🛒1F6D2nosingle::🛒single1🛒 +7321155&amp;#x1f4…📛1F4DBnosingle::📛single1📛 +7331156&amp;#x1f1…🇳🇱1F1F3-1F1F1nosingle::🇳🇱single1🇳🇱 +7341157&amp;#x1f4…🐿️1F43F-FE0Fnosingle::🐿️single1🐿️ +7351158&amp;#x1f1…🅾️1F17E-FE0Fnosingle::🅾️single1🅾️ +7361159&amp;#9762…☢️2622-FE0Fnosingle::☢️single1☢️ +7371160&amp;#x1f1…🇲🇦1F1F2-1F1E6nosingle::🇲🇦single1🇲🇦 +7381161&amp;#x1f4…🐃1F403nosingle::🐃single1🐃 +7391162&amp;#9981…⛽26FDnosingle::⛽single1⛽ +7401163&amp;#x1f1…🅿️1F17F-FE0Fnosingle::🅿️single1🅿️ +7411164&amp;#x1f9…🦇1F987nosingle::🦇single1🦇 +7421165&amp;#1054…⤴️2934-FE0Fnosingle::⤴️single1⤴️ +7431166&amp;#9973…⛵26F5nosingle::⛵single1⛵ +7441167&amp;#x1f4…👞1F45Enosingle::👞single1👞 +7451168&amp;#x1f5…🔓1F513nosingle::🔓single1🔓 +7461169&amp;#x1f9…🧞1F9DEnosingle::🧞single1🧞 +7471170&amp;#9968…⛰️26F0-FE0Fnosingle::⛰️single1⛰️ +7481171&amp;#x1f4…📎1F4CEnosingle::📎single1📎 +7491172&amp;#x1f9…🦎1F98Enosingle::🦎single1🦎 +7501173&amp;#x1f5…🔌1F50Cnosingle::🔌single1🔌 +7511174&amp;#x1f3…🏳️1F3F3-FE0Fnosingle::🏳️single1🏳️ +7521175&amp;#x1f6…🚽1F6BDnosingle::🚽single1🚽 +7531176&amp;#x1f3…🍲1F372nosingle::🍲single1🍲 +7541177&amp;#9874…⚒️2692-FE0Fnosingle::⚒️single1⚒️ +7551178&amp;#9802…♊264Anosingle::♊single1♊ +7561179&amp;#x1f4…👢1F462nosingle::👢single1👢 +7571180&amp;#x1f9…🧙1F9D9nosingle::🧙single1🧙 +7581181&amp;#x1f1…🇵🇱1F1F5-1F1F1nosingle::🇵🇱single1🇵🇱 +7591182&amp;#9962…⛪26EAnosingle::⛪single1⛪ +7601183&amp;#x1f1…🇧🇪1F1E7-1F1EAnosingle::🇧🇪single1🇧🇪 +7611184&amp;#x1f3…🎎1F38Enosingle::🎎single1🎎 +7621185&amp;#x1f5…🔖1F516nosingle::🔖single1🔖 +7631186&amp;#x1f5…🔋1F50Bnosingle::🔋single1🔋 +7641187&amp;#x1f4…📡1F4E1nosingle::📡single1📡 +7651188&amp;#x1f3…🌇1F307nosingle::🌇single1🌇 +7661189&amp;#x1f3…🏉1F3C9nosingle::🏉single1🏉 +7671190&amp;#x1f4…👪1F46Anosingle::👪single1👪 +7681191&amp;#x1f4…👖1F456nosingle::👖single1👖 +7691192&amp;#x1f4…📊1F4CAnosingle::📊single1📊 +7701193&amp;#x1f4…🐆1F406nosingle::🐆single1🐆 +7711194&amp;#x1f5…🔁1F501nosingle::🔁single1🔁 +7721195&amp;#x1f6…🛩️1F6E9-FE0Fnosingle::🛩️single1🛩️ +7731196&amp;#x1f4…🐡1F421nosingle::🐡single1🐡 +7741197&amp;#x1f0…🃏1F0CFnosingle::🃏single1🃏 +7751198&amp;#x1f4…👩‍💻1F469-200D-1F4BBnosingle::👩‍💻single1👩‍💻 +7761199&amp;#x1f3…🌩️1F329-FE0Fnosingle::🌩️single1🌩️ +77711100&amp;#x1f1…🇳🇬1F1F3-1F1ECnosingle::🇳🇬single1🇳🇬 +77811101&amp;#x1f4…🐏1F40Fnosingle::🐏single1🐏 +779111026&amp;#650…6️⃣36-FE0F-20E3nosingle::6️⃣single16️⃣ +78011103&amp;#9977…⛹️26F9-FE0Fnosingle::⛹️single1⛹️ +78111104&amp;#x1f5…🗯️1F5EF-FE0Fnosingle::🗯️single1🗯️ +78211105&amp;#x1f1…🇨🇭1F1E8-1F1EDnosingle::🇨🇭single1🇨🇭 +78311106&amp;#x1f3…🍢1F362nosingle::🍢single1🍢 +78411107&amp;#x1f5…🗺️1F5FA-FE0Fnosingle::🗺️single1🗺️ +78511108&amp;#x1f6…🚕1F695nosingle::🚕single1🚕 +78611109&amp;#x1f1…🇷🇺1F1F7-1F1FAnosingle::🇷🇺single1🇷🇺 +78711110&amp;#9410…Ⓜ️24C2-FE0Fnosingle::Ⓜ️single1Ⓜ️ +78811111&amp;#9969…⛱️26F1-FE0Fnosingle::⛱️single1⛱️ +78911112&amp;#x1f3…🏫1F3EBnosingle::🏫single1🏫 +79011113&amp;#x1f4…📀1F4C0nosingle::📀single1📀 +79111114&amp;#x1f1…🆙1F199nosingle::🆙single1🆙 +79211115&amp;#x1f9…🤼1F93Cnosingle::🤼single1🤼 +79311116&amp;#x1f4…🐨1F428nosingle::🐨single1🐨 +79411117&amp;#x1f9…🦂1F982nosingle::🦂single1🦂 +79511118&amp;#x1f4…💷1F4B7nosingle::💷single1💷 +79611119&amp;#x1f9…🥈1F948nosingle::🥈single1🥈 +79711120&amp;#x1f4…👨‍💻1F468-200D-1F4BBnosingle::👨‍💻single1👨‍💻 +79811121&amp;#x1f6…🛏️1F6CF-FE0Fnosingle::🛏️single1🛏️ +79911122&amp;#9724…◼️25FC-FE0Fnosingle::◼️single1◼️ +80011123&amp;#x1f6…🚄1F684nosingle::🚄single1🚄 +80111124&amp;#x1f9…🧖1F9D6nosingle::🧖single1🧖 +80211125&amp;#x1f3…🎠1F3A0nosingle::🎠single1🎠 +80311126&amp;#x1f4…🐁1F401nosingle::🐁single1🐁 +80411127&amp;#x1f5…🗳️1F5F3-FE0Fnosingle::🗳️single1🗳️ +80511128&amp;#x1f3…🏮1F3EEnosingle::🏮single1🏮 +80611129&amp;#x1f9…🥪1F96Anosingle::🥪single1🥪 +80711130&amp;#x1f3…🌥️1F325-FE0Fnosingle::🌥️single1🌥️ +80811131&amp;#x1f6…🚪1F6AAnosingle::🚪single1🚪 +80911132&amp;#1295…㊙️3299-FE0Fnosingle::㊙️single1㊙️ +81011133&amp;#x1f5…🗞️1F5DE-FE0Fnosingle::🗞️single1🗞️ +81111134&amp;#x1f4…👨‍⚕️1F468-200D-2695-FE0Fnosingle::👨‍⚕️single1👨‍⚕️ +81211135&amp;#x1f5…🖊️1F58A-FE0Fnosingle::🖊️single1🖊️ +81311136&amp;#1103…⬛2B1Bnosingle::⬛single1⬛ +81411137&amp;#x1f1…🆒1F192nosingle::🆒single1🆒 +81511138&amp;#x1f1…🇨🇺1F1E8-1F1FAnosingle::🇨🇺single1🇨🇺 +81611139&amp;#x1f9…🥧1F967nosingle::🥧single1🥧 +81711140&amp;#x1f4…👩‍⚕️1F469-200D-2695-FE0Fnosingle::👩‍⚕️single1👩‍⚕️ +81811141&amp;#x1f1…🇳🇮1F1F3-1F1EEnosingle::🇳🇮single1🇳🇮 +81911142&amp;#x1f4…👨‍🎓1F468-200D-1F393nosingle::👨‍🎓single1👨‍🎓 +82011143&amp;#x1f6…🚧1F6A7nosingle::🚧single1🚧 +82111144&amp;#9939…⛓️26D3-FE0Fnosingle::⛓️single1⛓️ +82211145&amp;#x1f6…🛵1F6F5nosingle::🛵single1🛵 +82311146&amp;#x1f9…🥅1F945nosingle::🥅single1🥅 +82411147&amp;#x1f6…🚅1F685nosingle::🚅single1🚅 +82511148&amp;#x1f5…🖼️1F5BC-FE0Fnosingle::🖼️single1🖼️ +82611149&amp;#9855…♿267Fnosingle::♿single1♿ +82711150&amp;#x1f4…📏1F4CFnosingle::📏single1📏 +82811151&amp;#x1f0…🀄1F004nosingle::🀄single1🀄 +82911152&amp;#x1f5…🖌️1F58C-FE0Fnosingle::🖌️single1🖌️ +83011153&amp;#x1f2…🉐1F250nosingle::🉐single1🉐 +83111154&amp;#x1f4…📬1F4ECnosingle::📬single1📬 +83211155&amp;#x1f6…🚁1F681nosingle::🚁single1🚁 +83311156&amp;#x1f5…🗑️1F5D1-FE0Fnosingle::🗑️single1🗑️ +83411157&amp;#x1f9…🤵1F935nosingle::🤵single1🤵 +83511158&amp;#x1f3…🏷️1F3F7-FE0Fnosingle::🏷️single1🏷️ +83611159&amp;#x1f3…🌡️1F321-FE0Fnosingle::🌡️single1🌡️ +83711160&amp;#x1f3…🎐1F390nosingle::🎐single1🎐 +83811161&amp;#x1f1…🇯🇲1F1EF-1F1F2nosingle::🇯🇲single1🇯🇲 +83911162&amp;#x1f9…🦒1F992nosingle::🦒single1🦒 +84011163&amp;#x1f3…🎒1F392nosingle::🎒single1🎒 +84111164&amp;#x1f1…🇵🇾1F1F5-1F1FEnosingle::🇵🇾single1🇵🇾 +84211165&amp;#x1f1…🇺🇦1F1FA-1F1E6nosingle::🇺🇦single1🇺🇦 +84311166&amp;#x1f1…🇲🇨1F1F2-1F1E8nosingle::🇲🇨single1🇲🇨 +84411167&amp;#x1f9…🥉1F949nosingle::🥉single1🥉 +845111687&amp;#650…7️⃣37-FE0F-20E3nosingle::7️⃣single17️⃣ +84611169&amp;#8987…⌛231Bnosingle::⌛single1⌛ +84711170&amp;#x1f3…🌒1F312nosingle::🌒single1🌒 +84811171&amp;#x1f9…🥥1F965nosingle::🥥single1🥥 +84911172&amp;#x1f3…🎅1F385nosingle::🎅single1🎅 +85011173&amp;#x1f9…🧕1F9D5nosingle::🧕single1🧕 +85111174&amp;#x1f6…🛐1F6D0nosingle::🛐single1🛐 +85211175&amp;#9803…♋264Bnosingle::♋single1♋ +85311176&amp;#x1f9…🧑1F9D1nosingle::🧑single1🧑 +85411177&amp;#x1f1…🇩🇴1F1E9-1F1F4nosingle::🇩🇴single1🇩🇴 +85511178&amp;#x1f1…🇮🇪1F1EE-1F1EAnosingle::🇮🇪single1🇮🇪 +85611179&amp;#x1f6…🛸1F6F8nosingle::🛸single1🛸 +85711180&amp;#x1f9…🧒1F9D2nosingle::🧒single1🧒 +85811181&amp;#x1f4…📧1F4E7nosingle::📧single1📧 +85911182&amp;#x1f9…🥢1F962nosingle::🥢single1🥢 +86011183&amp;#x1f3…🎢1F3A2nosingle::🎢single1🎢 +86111184&amp;#x1f4…🐪1F42Anosingle::🐪single1🐪 +86211185&amp;#x1f1…🇮🇱1F1EE-1F1F1nosingle::🇮🇱single1🇮🇱 +86311186&amp;#x1f4…👨‍🍳1F468-200D-1F373nosingle::👨‍🍳single1👨‍🍳 +86411187&amp;#x1f6…🚛1F69Bnosingle::🚛single1🚛 +86511188&amp;#x1f9…🥄1F944nosingle::🥄single1🥄 +86611189&amp;#x1f9…🤹1F939nosingle::🤹single1🤹 +86711190&amp;#x1f3…🏢1F3E2nosingle::🏢single1🏢 +86811191&amp;#x1f4…💈1F488nosingle::💈single1💈 +86911192&amp;#x1f4…👘1F458nosingle::👘single1👘 +87011193&amp;#x1f1…🇳🇴1F1F3-1F1F4nosingle::🇳🇴single1🇳🇴 +87111194&amp;#x1f1…🇬🇭1F1EC-1F1EDnosingle::🇬🇭single1🇬🇭 +87211195&amp;#x1f3…🏔️1F3D4-FE0Fnosingle::🏔️single1🏔️ +87311196&amp;#1234…〽️303D-FE0Fnosingle::〽️single1〽️ +87411197&amp;#x1f6…🚦1F6A6nosingle::🚦single1🚦 +87511198&amp;#x1f1…🇿🇦1F1FF-1F1E6nosingle::🇿🇦single1🇿🇦 +87611199&amp;#x1f1…🇹🇳1F1F9-1F1F3nosingle::🇹🇳single1🇹🇳 +87711200&amp;#x1f4…💼1F4BCnosingle::💼single1💼 +87811201&amp;#9800…♈2648nosingle::♈single1♈ +87911202&amp;#x1f9…🦓1F993nosingle::🦓single1🦓 +88011203&amp;#x1f6…🚻1F6BBnosingle::🚻single1🚻 +88111204&amp;#x1f3…🎰1F3B0nosingle::🎰single1🎰 +88211205&amp;#9725…◽25FDnosingle::◽single1◽ +88311206&amp;#x1f9…🦗1F997nosingle::🦗single1🦗 +88411207&amp;#x1f9…🤾1F93Enosingle::🤾single1🤾 +88511208&amp;#x1f4…👚1F45Anosingle::👚single1👚 +88611209&amp;#x1f4…👡1F461nosingle::👡single1👡 +88711210&amp;#9935…⛏️26CF-FE0Fnosingle::⛏️single1⛏️ +888112119&amp;#650…9️⃣39-FE0F-20E3nosingle::9️⃣single19️⃣ +88911212&amp;#x1f6…🛎️1F6CE-FE0Fnosingle::🛎️single1🛎️ +89011213&amp;#x1f4…🐗1F417nosingle::🐗single1🐗 +89111214&amp;#x1f3…🌦️1F326-FE0Fnosingle::🌦️single1🌦️ +89211215&amp;#x1f1…🇸🇦1F1F8-1F1E6nosingle::🇸🇦single1🇸🇦 +89311216&amp;#x1f6…🚹1F6B9nosingle::🚹single1🚹 +89411217&amp;#x1f1…🇾🇪1F1FE-1F1EAnosingle::🇾🇪single1🇾🇪 +89511218&amp;#9770…☪️262A-FE0Fnosingle::☪️single1☪️ +89611219&amp;#x1f9…🤺1F93Anosingle::🤺single1🤺 +89711220&amp;#x1f6…🚣1F6A3nosingle::🚣single1🚣 +89811221&amp;#x1f5…🗿1F5FFnosingle::🗿single1🗿 +89911222&amp;#x1f9…🥘1F958nosingle::🥘single1🥘 +90011223&amp;#x1f1…🇩🇰1F1E9-1F1F0nosingle::🇩🇰single1🇩🇰 +90111224&amp;#x1f9…🦕1F995nosingle::🦕single1🦕 +90211225&amp;#x1f1…🇱🇷1F1F1-1F1F7nosingle::🇱🇷single1🇱🇷 +90311226&amp;#x1f3…🏕️1F3D5-FE0Fnosingle::🏕️single1🏕️ +90411227&amp;#9763…☣️2623-FE0Fnosingle::☣️single1☣️ +90511228&amp;#x1f3…🌆1F306nosingle::🌆single1🌆 +90611229&amp;#8597…↕️2195-FE0Fnosingle::↕️single1↕️ +90711230&amp;#x1f4…📨1F4E8nosingle::📨single1📨 +90811231&amp;#x1f3…🏨1F3E8nosingle::🏨single1🏨 +90911232&amp;#x1f5…🕹️1F579-FE0Fnosingle::🕹️single1🕹️ +91011233&amp;#x1f4…👩‍🍳1F469-200D-1F373nosingle::👩‍🍳single1👩‍🍳 +91111234&amp;#x1f1…🇨🇮1F1E8-1F1EEnosingle::🇨🇮single1🇨🇮 +912121&amp;#x1f4…📯1F4EFnosingle::📯single1📯 +913122&amp;#x1f4…👜1F45Cnosingle::👜single1👜 +914123&amp;#x1f3…🏘️1F3D8-FE0Fnosingle::🏘️single1🏘️ +915124&amp;#x1f4…🐫1F42Bnosingle::🐫single1🐫 +916125&amp;#9978…⛺26FAnosingle::⛺single1⛺ +917126&amp;#x1f4…👩‍🏫1F469-200D-1F3EBnosingle::👩‍🏫single1👩‍🏫 +918127&amp;#x1f5…🕳️1F573-FE0Fnosingle::🕳️single1🕳️ +919128&amp;#x1f3…🏯1F3EFnosingle::🏯single1🏯 +920129&amp;#x1f3…🌓1F313nosingle::🌓single1🌓 +9211210&amp;#x1f3…🌉1F309nosingle::🌉single1🌉 +92212118&amp;#650…8️⃣38-FE0F-20E3nosingle::8️⃣single18️⃣ +9231212&amp;#x1f4…💂1F482nosingle::💂single1💂 +9241213&amp;#x1f5…🔉1F509nosingle::🔉single1🔉 +9251214&amp;#x1f4…👨‍👩‍👧‍👦1F468-200D-1F469-200D-1F467-200D-1F466nosingle::👨‍👩‍👧‍👦single1👨‍👩‍👧‍👦 +9261215&amp;#x1f1…🇪🇺1F1EA-1F1FAnosingle::🇪🇺single1🇪🇺 +9271216&amp;#x1f6…🚜1F69Cnosingle::🚜single1🚜 +9281217&amp;#x1f5…🔕1F515nosingle::🔕single1🔕 +9291218&amp;#x1f4…📥1F4E5nosingle::📥single1📥 +9301219&amp;#x1f3…🌔1F314nosingle::🌔single1🌔 +9311220&amp;#x1f3…🏙️1F3D9-FE0Fnosingle::🏙️single1🏙️ +9321221&amp;#x1f9…🥨1F968nosingle::🥨single1🥨 +9331222&amp;#x1f1…🇲🇾1F1F2-1F1FEnosingle::🇲🇾single1🇲🇾 +9341223&amp;#x1f4…📘1F4D8nosingle::📘single1📘 +9351224&amp;#x1f4…📗1F4D7nosingle::📗single1📗 +9361225&amp;#x1f4…📜1F4DCnosingle::📜single1📜 +9371226&amp;#9807…♏264Fnosingle::♏single1♏ +9381227&amp;#x1f1…🇧🇴1F1E7-1F1F4nosingle::🇧🇴single1🇧🇴 +9391228&amp;#x1f3…🏜️1F3DC-FE0Fnosingle::🏜️single1🏜️ +9401229&amp;#x1f3…🍘1F358nosingle::🍘single1🍘 +9411230&amp;#x1f6…🛬1F6ECnosingle::🛬single1🛬 +9421231&amp;#x1f3…🌯1F32Fnosingle::🌯single1🌯 +9431232&amp;#x1f3…🏗️1F3D7-FE0Fnosingle::🏗️single1🏗️ +9441233&amp;#x1f3…🏴󠁧󠁢󠁥󠁮󠁧󠁿1F3F4-E0067-E0062-E0065-E006E-E0067-E007Fnosingle::🏴󠁧󠁢󠁥󠁮󠁧󠁿single1🏴󠁧󠁢󠁥󠁮󠁧󠁿 +9451234&amp;#x1f6…🚼1F6BCnosingle::🚼single1🚼 +9461235&amp;#x1f3…🏞️1F3DE-FE0Fnosingle::🏞️single1🏞️ +9471236&amp;#x1f5…🔈1F508nosingle::🔈single1🔈 +9481237&amp;#x1f5…🕰️1F570-FE0Fnosingle::🕰️single1🕰️ +9491238&amp;#x1f3…🏓1F3D3nosingle::🏓single1🏓 +9501239&amp;#x1f1…🇨🇷1F1E8-1F1F7nosingle::🇨🇷single1🇨🇷 +9511240&amp;#x1f4…👨‍🌾1F468-200D-1F33Enosingle::👨‍🌾single1👨‍🌾 +9521241&amp;#x1f9…🦃1F983nosingle::🦃single1🦃 +9531242&amp;#x1f5…🔦1F526nosingle::🔦single1🔦 +9541243&amp;#x1f5…🔃1F503nosingle::🔃single1🔃 +9551244&amp;#x1f1…🇸🇻1F1F8-1F1FBnosingle::🇸🇻single1🇸🇻 +9561245&amp;#x1f6…🛳️1F6F3-FE0Fnosingle::🛳️single1🛳️ +9571246&amp;#x1f3…🌘1F318nosingle::🌘single1🌘 +9581247&amp;#x1f1…🇧🇩1F1E7-1F1E9nosingle::🇧🇩single1🇧🇩 +9591248&amp;#x1f9…🧛1F9DBnosingle::🧛single1🧛 +9601249&amp;#x1f4…👩‍❤️‍👩1F469-200D-2764-FE0F-200D-1F469nosingle::👩‍❤️‍👩single1👩‍❤️‍👩 +9611250&amp;#x1f5…🔚1F51Anosingle::🔚single1🔚 +9621251&amp;#x1f5…🔟1F51Fnosingle::🔟single1🔟 +9631252&amp;#8505…ℹ️2139-FE0Fnosingle::ℹ️single1ℹ️ +9641253&amp;#x1f6…🛢️1F6E2-FE0Fnosingle::🛢️single1🛢️ +9651254&amp;#x1f3…🏴󠁧󠁢󠁳󠁣󠁴󠁿1F3F4-E0067-E0062-E0073-E0063-E0074-E007Fnosingle::🏴󠁧󠁢󠁳󠁣󠁴󠁿single1🏴󠁧󠁢󠁳󠁣󠁴󠁿 +9661255&amp;#x1f1…🇸🇩1F1F8-1F1E9nosingle::🇸🇩single1🇸🇩 +9671256&amp;#x1f5…🔭1F52Dnosingle::🔭single1🔭 +9681257&amp;#x1f1…🇮🇶1F1EE-1F1F6nosingle::🇮🇶single1🇮🇶 +9691258&amp;#x1f3…🎍1F38Dnosingle::🎍single1🎍 +9701259&amp;#x1f9…🥣1F963nosingle::🥣single1🥣 +9711260&amp;#x1f3…🌗1F317nosingle::🌗single1🌗 +9721261&amp;#8598…↖️2196-FE0Fnosingle::↖️single1↖️ +9731262&amp;#x1f3…🎳1F3B3nosingle::🎳single1🎳 +9741263&amp;#x1f4…👨‍❤️‍💋‍👨1F468-200D-2764-FE0F-200D-1F48B-200D-1F468nosingle::👨‍❤️‍💋‍👨single1👨‍❤️‍💋‍👨 +9751264&amp;#x1f4…👨‍🏫1F468-200D-1F3EBnosingle::👨‍🏫single1👨‍🏫 +9761265&amp;#x1f4…📄1F4C4nosingle::📄single1📄 +9771266&amp;#x1f1…🇶🇦1F1F6-1F1E6nosingle::🇶🇦single1🇶🇦 +9781267&amp;#x1f4…👨‍👩‍👧1F468-200D-1F469-200D-1F467nosingle::👨‍👩‍👧single1👨‍👩‍👧 +9791268&amp;#x1f3…🎚️1F39A-FE0Fnosingle::🎚️single1🎚️ +9801269&amp;#x1f6…🛠️1F6E0-FE0Fnosingle::🛠️single1🛠️ +9811270&amp;#x1f1…🆓1F193nosingle::🆓single1🆓 +9821271&amp;#x1f1…🇵🇸1F1F5-1F1F8nosingle::🇵🇸single1🇵🇸 +9831272&amp;#x1f3…🏛️1F3DB-FE0Fnosingle::🏛️single1🏛️ +9841273&amp;#9808…♐2650nosingle::♐single1♐ +9851274&amp;#x1f9…🧦1F9E6nosingle::🧦single1🧦 +9861275&amp;#x1f5…🗾1F5FEnosingle::🗾single1🗾 +9871276&amp;#x1f4…👩‍🌾1F469-200D-1F33Enosingle::👩‍🌾single1👩‍🌾 +9881277&amp;#x1f3…🎑1F391nosingle::🎑single1🎑 +9891278&amp;#x1f1…🇬🇹1F1EC-1F1F9nosingle::🇬🇹single1🇬🇹 +9901279&amp;#x1f9…🧤1F9E4nosingle::🧤single1🧤 +9911280&amp;#x1f9…🧝1F9DDnosingle::🧝single1🧝 +9921281&amp;#1017…➿27BFnosingle::➿single1➿ +9931282&amp;#x1f1…🇻🇳1F1FB-1F1F3nosingle::🇻🇳single1🇻🇳 +9941283&amp;#x1f4…📒1F4D2nosingle::📒single1📒 +9951284&amp;#9801…♉2649nosingle::♉single1♉ +9961285&amp;#9811…♓2653nosingle::♓single1♓ +9971286&amp;#x1f6…🚭1F6ADnosingle::🚭single1🚭 +9981287&amp;#x1f5…🔧1F527nosingle::🔧single1🔧 +9991288&amp;#9804…♌264Cnosingle::♌single1♌ +10001289&amp;#8617…↩️21A9-FE0Fnosingle::↩️single1↩️ +10011290&amp;#9810…♒2652nosingle::♒single1♒ +10021291&amp;#x1f3…🏦1F3E6nosingle::🏦single1🏦 +10031292&amp;#x1f2…🈵1F235nosingle::🈵single1🈵 +10041293&amp;#x1f9…🥫1F96Bnosingle::🥫single1🥫 +10051294&amp;#x1f3…🌖1F316nosingle::🌖single1🌖 +10061295&amp;#x1f6…🛴1F6F4nosingle::🛴single1🛴 +10071296&amp;#x1f6…🚖1F696nosingle::🚖single1🚖 +10081297&amp;#x1f9…🥙1F959nosingle::🥙single1🥙 +10091298&amp;#x1f6…🛰️1F6F0-FE0Fnosingle::🛰️single1🛰️ +10101299&amp;#x1f9…🦏1F98Fnosingle::🦏single1🦏 +101112100&amp;#x1f1…🇸🇬1F1F8-1F1ECnosingle::🇸🇬single1🇸🇬 +101212101&amp;#9210…⏺️23FA-FE0Fnosingle::⏺️single1⏺️ +101312102&amp;#x1f5…🖍️1F58D-FE0Fnosingle::🖍️single1🖍️ +101412103&amp;#9976…⛸️26F8-FE0Fnosingle::⛸️single1⛸️ +101512104&amp;#x1f4…📙1F4D9nosingle::📙single1📙 +101612105&amp;#x1f4…👲1F472nosingle::👲single1👲 +101712106&amp;#x1f3…🏂1F3C2nosingle::🏂single1🏂 +101812107*&amp;#650…*️⃣2A-FE0F-20E3nosingle::*️⃣single1*️⃣ +101912108&amp;#x1f5…🔲1F532nosingle::🔲single1🔲 +102012109&amp;#x1f1…🇭🇺1F1ED-1F1FAnosingle::🇭🇺single1🇭🇺 +102112110&amp;#x1f6…🚐1F690nosingle::🚐single1🚐 +102212111&amp;#x1f4…👨‍❤️‍👨1F468-200D-2764-FE0F-200D-1F468nosingle::👨‍❤️‍👨single1👨‍❤️‍👨 +102312112&amp;#x1f5…🔇1F507nosingle::🔇single1🔇 +102412113&amp;#x1f9…🦔1F994nosingle::🦔single1🦔 +102512114&amp;#x1f5…🔯1F52Fnosingle::🔯single1🔯 +102612115&amp;#x1f6…🚵1F6B5nosingle::🚵single1🚵 +102712116&amp;#x1f6…🚯1F6AFnosingle::🚯single1🚯 +102812117&amp;#x1f5…🕛1F55Bnosingle::🕛single1🕛 +102912118&amp;#x1f4…📼1F4FCnosingle::📼single1📼 +103012119&amp;#x1f9…🧓1F9D3nosingle::🧓single1🧓 +103112120&amp;#x1f3…🎛️1F39B-FE0Fnosingle::🎛️single1🎛️ +103212121&amp;#x1f1…🇨🇿1F1E8-1F1FFnosingle::🇨🇿single1🇨🇿 +103312122&amp;#x1f1…🇭🇰1F1ED-1F1F0nosingle::🇭🇰single1🇭🇰 +103412123&amp;#x1f4…💺1F4BAnosingle::💺single1💺 +103512124&amp;#x1f4…👩‍🎤1F469-200D-1F3A4nosingle::👩‍🎤single1👩‍🎤 +103612125&amp;#9970…⛲26F2nosingle::⛲single1⛲ +103712126&amp;#9883…⚛️269B-FE0Fnosingle::⚛️single1⚛️ +103812127&amp;#x1f5…🕒1F552nosingle::🕒single1🕒 +103912128&amp;#x1f4…📶1F4F6nosingle::📶single1📶 +104012129&amp;#x1f6…🚋1F68Bnosingle::🚋single1🚋 +104112130&amp;#x1f1…🇱🇧1F1F1-1F1E7nosingle::🇱🇧single1🇱🇧 +104212131&amp;#x1f4…📠1F4E0nosingle::📠single1📠 +104312132&amp;#x1f6…🚇1F687nosingle::🚇single1🚇 +104412133&amp;#9202…⏲️23F2-FE0Fnosingle::⏲️single1⏲️ +104512134&amp;#9194…⏪23EAnosingle::⏪single1⏪ +104612135&amp;#x1f1…🇰🇼1F1F0-1F1FCnosingle::🇰🇼single1🇰🇼 +104712136&amp;#x1f6…🛃1F6C3nosingle::🛃single1🛃 +104812137&amp;#x1f1…🇭🇳1F1ED-1F1F3nosingle::🇭🇳single1🇭🇳 +104912138&amp;#x1f4…👨‍✈️1F468-200D-2708-FE0Fnosingle::👨‍✈️single1👨‍✈️ +105012139&amp;#x1f4…📓1F4D3nosingle::📓single1📓 +105112140&amp;#x1f4…👩‍👧1F469-200D-1F467nosingle::👩‍👧single1👩‍👧 +105212141&amp;#9806…♎264Enosingle::♎single1♎ +105312142&amp;#x1f3…🏑1F3D1nosingle::🏑single1🏑 +105412143&amp;#x1f4…📋1F4CBnosingle::📋single1📋 +105512144&amp;#x1f5…🔼1F53Cnosingle::🔼single1🔼 +105612145&amp;#x1f1…🇨🇵1F1E8-1F1F5nosingle::🇨🇵single1🇨🇵 +105712146&amp;#x1f1…🇯🇴1F1EF-1F1F4nosingle::🇯🇴single1🇯🇴 +105812147&amp;#x1f4…📃1F4C3nosingle::📃single1📃 +105912148&amp;#x1f5…🔬1F52Cnosingle::🔬single1🔬 +106012149&amp;#x1f1…🇫🇮1F1EB-1F1EEnosingle::🇫🇮single1🇫🇮 +106112150&amp;#x1f6…🚥1F6A5nosingle::🚥single1🚥 +106212151&amp;#x1f1…🇱🇰1F1F1-1F1F0nosingle::🇱🇰single1🇱🇰 +106312152&amp;#9805…♍264Dnosingle::♍single1♍ +106412153&amp;#x1f1…🇵🇦1F1F5-1F1E6nosingle::🇵🇦single1🇵🇦 +106512154&amp;#x1f1…🇸🇾1F1F8-1F1FEnosingle::🇸🇾single1🇸🇾 +106612155&amp;#x1f6…🚾1F6BEnosingle::🚾single1🚾 +106712156&amp;#x1f1…🇭🇷1F1ED-1F1F7nosingle::🇭🇷single1🇭🇷 +106812157&amp;#x1f1…🇳🇿1F1F3-1F1FFnosingle::🇳🇿single1🇳🇿 +106912158&amp;#x1f6…🚍1F68Dnosingle::🚍single1🚍 +107012159&amp;#x1f3…🏸1F3F8nosingle::🏸single1🏸 +107112160&amp;#x1f5…🔳1F533nosingle::🔳single1🔳 +107212161&amp;#x1f6…🛂1F6C2nosingle::🛂single1🛂 +107312162&amp;#x1f4…📵1F4F5nosingle::📵single1📵 +107412163&amp;#9937…⛑️26D1-FE0Fnosingle::⛑️single1⛑️ +107512164&amp;#x1f3…🏚️1F3DA-FE0Fnosingle::🏚️single1🏚️ +107612165&amp;#x1f3…🎽1F3BDnosingle::🎽single1🎽 +107712166&amp;#x1f6…🚰1F6B0nosingle::🚰single1🚰 +107812167&amp;#x1f3…🏪1F3EAnosingle::🏪single1🏪 +107912168&amp;#9975…⛷️26F7-FE0Fnosingle::⛷️single1⛷️ +108012169&amp;#x1f4…👩‍💼1F469-200D-1F4BCnosingle::👩‍💼single1👩‍💼 +108112170&amp;#9809…♑2651nosingle::♑single1♑ +108212171&amp;#x1f1…🇨🇲1F1E8-1F1F2nosingle::🇨🇲single1🇨🇲 +108312172&amp;#x1f1…🇱🇾1F1F1-1F1FEnosingle::🇱🇾single1🇱🇾 +108412173&amp;#9195…⏫23EBnosingle::⏫single1⏫ +108512174&amp;#x1f3…🏣1F3E3nosingle::🏣single1🏣 +108612175&amp;#x1f6…🚉1F689nosingle::🚉single1🚉 +108712176&amp;#x1f6…🛅1F6C5nosingle::🛅single1🛅 +108812177&amp;#9877…⚕️2695-FE0Fnosingle::⚕️single1⚕️ +108912178&amp;#9972…⛴️26F4-FE0Fnosingle::⛴️single1⛴️ +109012179&amp;#x1f4…👁️‍🗨️1F441-FE0F-200D-1F5E8-FE0Fnosingle::👁️‍🗨️single1👁️‍🗨️ +109112180&amp;#x1f3…🏧1F3E7nosingle::🏧single1🏧 +109212181&amp;#9723…◻️25FB-FE0Fnosingle::◻️single1◻️ +109312182&amp;#x1f4…👩‍🔬1F469-200D-1F52Cnosingle::👩‍🔬single1👩‍🔬 +109412183&amp;#9209…⏹️23F9-FE0Fnosingle::⏹️single1⏹️ +109512184&amp;#x1f1…🇷🇴1F1F7-1F1F4nosingle::🇷🇴single1🇷🇴 +109612185&amp;#x1f4…👩‍👦1F469-200D-1F466nosingle::👩‍👦single1👩‍👦 +109712186&amp;#1103…⬜2B1Cnosingle::⬜single1⬜ +1098131#&amp;#650…#️⃣23-FE0F-20E3nosingle::#️⃣single1#️⃣ +1099132&amp;#x1f1…🇦🇱1F1E6-1F1F1nosingle::🇦🇱single1🇦🇱 +1100133&amp;#x1f1…🇹🇼1F1F9-1F1FCnosingle::🇹🇼single1🇹🇼 +1101134&amp;#x1f6…🛣️1F6E3-FE0Fnosingle::🛣️single1🛣️ +1102135&amp;#x1f3…🏤1F3E4nosingle::🏤single1🏤 +1103136&amp;#x1f6…🛄1F6C4nosingle::🛄single1🛄 +1104137&amp;#x1f1…🇮🇸1F1EE-1F1F8nosingle::🇮🇸single1🇮🇸 +1105138&amp;#x1f3…🏬1F3ECnosingle::🏬single1🏬 +1106139&amp;#x1f4…👨‍🎤1F468-200D-1F3A4nosingle::👨‍🎤single1👨‍🎤 +11071310&amp;#x1f5…🕐1F550nosingle::🕐single1🕐 +11081311&amp;#x1f3…🎦1F3A6nosingle::🎦single1🎦 +11091312&amp;#x1f4…👨‍💼1F468-200D-1F4BCnosingle::👨‍💼single1👨‍💼 +11101313&amp;#x1f4…💹1F4B9nosingle::💹single1💹 +11111314&amp;#9784…☸️2638-FE0Fnosingle::☸️single1☸️ +11121315&amp;#x1f4…📴1F4F4nosingle::📴single1📴 +11131316&amp;#x1f4…📐1F4D0nosingle::📐single1📐 +11141317&amp;#x1f5…🖱️1F5B1-FE0Fnosingle::🖱️single1🖱️ +11151318&amp;#x1f1…🇭🇹1F1ED-1F1F9nosingle::🇭🇹single1🇭🇹 +11161319&amp;#x1f1…🇸🇳1F1F8-1F1F3nosingle::🇸🇳single1🇸🇳 +11171320&amp;#x1f5…🔩1F529nosingle::🔩single1🔩 +11181321&amp;#x1f2…🈲1F232nosingle::🈲single1🈲 +11191322&amp;#x1f3…🎴1F3B4nosingle::🎴single1🎴 +11201323&amp;#x1f4…📳1F4F3nosingle::📳single1📳 +11211324&amp;#9766…☦️2626-FE0Fnosingle::☦️single1☦️ +11221325&amp;#x1f6…🚆1F686nosingle::🚆single1🚆 +11231326&amp;#x1f4…📁1F4C1nosingle::📁single1📁 +11241327&amp;#x1f4…📔1F4D4nosingle::📔single1📔 +11251328&amp;#x1f1…🇦🇫1F1E6-1F1EBnosingle::🇦🇫single1🇦🇫 +11261329&amp;#x1f4…👩‍🎨1F469-200D-1F3A8nosingle::👩‍🎨single1👩‍🎨 +11271330&amp;#x1f5…🕑1F551nosingle::🕑single1🕑 +11281331&amp;#x1f6…🚸1F6B8nosingle::🚸single1🚸 +11291332&amp;#x1f9…🧣1F9E3nosingle::🧣single1🧣 +11301333&amp;#x1f4…👨‍🚀1F468-200D-1F680nosingle::👨‍🚀single1👨‍🚀 +11311334&amp;#x1f1…🇧🇭1F1E7-1F1EDnosingle::🇧🇭single1🇧🇭 +11321335&amp;#x1f3…🏭1F3EDnosingle::🏭single1🏭 +11331336&amp;#x1f3…🏺1F3FAnosingle::🏺single1🏺 +11341337&amp;#x1f1…🇬🇳1F1EC-1F1F3nosingle::🇬🇳single1🇬🇳 +11351338&amp;#x1f9…🤽1F93Dnosingle::🤽single1🤽 +11361339&amp;#x1f6…🛥️1F6E5-FE0Fnosingle::🛥️single1🛥️ +11371340&amp;#x1f1…🇲🇱1F1F2-1F1F1nosingle::🇲🇱single1🇲🇱 +11381341&amp;#x1f3…🌁1F301nosingle::🌁single1🌁 +11391342&amp;#x1f9…🤶1F936nosingle::🤶single1🤶 +11401343&amp;#x1f1…🇦🇿1F1E6-1F1FFnosingle::🇦🇿single1🇦🇿 +11411344&amp;#x1f4…💽1F4BDnosingle::💽single1💽 +11421345&amp;#x1f1…🇦🇴1F1E6-1F1F4nosingle::🇦🇴single1🇦🇴 +11431346&amp;#x1f6…🚎1F68Enosingle::🚎single1🚎 +11441347&amp;#x1f1…🇮🇷1F1EE-1F1F7nosingle::🇮🇷single1🇮🇷 +11451348&amp;#x1f1…🇳🇵1F1F3-1F1F5nosingle::🇳🇵single1🇳🇵 +11461349&amp;#x1f5…🗒️1F5D2-FE0Fnosingle::🗒️single1🗒️ +11471350&amp;#x1f1…🇨🇩1F1E8-1F1E9nosingle::🇨🇩single1🇨🇩 +11481351&amp;#x1f6…🛋️1F6CB-FE0Fnosingle::🛋️single1🛋️ +11491352&amp;#9000…⌨️2328-FE0Fnosingle::⌨️single1⌨️ +11501353&amp;#x1f1…🆎1F18Enosingle::🆎single1🆎 +11511354&amp;#x1f9…🧗1F9D7nosingle::🧗single1🧗 +11521355&amp;#x1f9…🧥1F9E5nosingle::🧥single1🧥 +11531356&amp;#x1f1…🇰🇪1F1F0-1F1EAnosingle::🇰🇪single1🇰🇪 +11541357&amp;#x1f6…🛶1F6F6nosingle::🛶single1🛶 +11551358&amp;#x1f4…📑1F4D1nosingle::📑single1📑 +11561359&amp;#x1f4…👝1F45Dnosingle::👝single1👝 +11571360&amp;#x1f1…🇦🇲1F1E6-1F1F2nosingle::🇦🇲single1🇦🇲 +11581361&amp;#9199…⏯️23EF-FE0Fnosingle::⏯️single1⏯️ +11591362&amp;#x1f1…🇬🇪1F1EC-1F1EAnosingle::🇬🇪single1🇬🇪 +11601363&amp;#x1f4…💱1F4B1nosingle::💱single1💱 +11611364&amp;#x1f4…👨‍🎨1F468-200D-1F3A8nosingle::👨‍🎨single1👨‍🎨 +11621365&amp;#x1f1…🇧🇬1F1E7-1F1ECnosingle::🇧🇬single1🇧🇬 +11631366&amp;#x1f1…🇪🇦1F1EA-1F1E6nosingle::🇪🇦single1🇪🇦 +11641367&amp;#x1f4…👩‍⚖️1F469-200D-2696-FE0Fnosingle::👩‍⚖️single1👩‍⚖️ +11651368&amp;#x1f5…🕓1F553nosingle::🕓single1🕓 +11661369&amp;#x1f4…👩‍🚀1F469-200D-1F680nosingle::👩‍🚀single1👩‍🚀 +11671370&amp;#x1f6…🚞1F69Enosingle::🚞single1🚞 +11681371&amp;#x1f4…👨‍🚒1F468-200D-1F692nosingle::👨‍🚒single1👨‍🚒 +11691372&amp;#x1f1…🆖1F196nosingle::🆖single1🆖 +11701373&amp;#x1f5…🔀1F500nosingle::🔀single1🔀 +11711374&amp;#x1f4…👨‍🔬1F468-200D-1F52Cnosingle::👨‍🔬single1👨‍🔬 +11721375&amp;#x1f4…👨‍⚖️1F468-200D-2696-FE0Fnosingle::👨‍⚖️single1👨‍⚖️ +11731376&amp;#x1f4…👨‍👩‍👦‍👦1F468-200D-1F469-200D-1F466-200D-1F466nosingle::👨‍👩‍👦‍👦single1👨‍👩‍👦‍👦 +11741377&amp;#x1f3…🏴󠁧󠁢󠁷󠁬󠁳󠁿1F3F4-E0067-E0062-E0077-E006C-E0073-E007Fnosingle::🏴󠁧󠁢󠁷󠁬󠁳󠁿single1🏴󠁧󠁢󠁷󠁬󠁳󠁿 +11751378&amp;#x1f9…🥠1F960nosingle::🥠single1🥠 +11761379&amp;#x1f6…🚳1F6B3nosingle::🚳single1🚳 +11771380&amp;#x1f1…🇧🇯1F1E7-1F1EFnosingle::🇧🇯single1🇧🇯 +11781381&amp;#x1f1…🇲🇬1F1F2-1F1ECnosingle::🇲🇬single1🇲🇬 +11791382&amp;#x1f1…🇪🇪1F1EA-1F1EAnosingle::🇪🇪single1🇪🇪 +11801383&amp;#9197…⏭️23ED-FE0Fnosingle::⏭️single1⏭️ +11811384&amp;#x1f5…🕎1F54Enosingle::🕎single1🕎 +11821385&amp;#x1f2…🈚1F21Anosingle::🈚single1🈚 +11831386&amp;#x1f6…🚡1F6A1nosingle::🚡single1🚡 +11841387&amp;#x1f5…🕕1F555nosingle::🕕single1🕕 +11851388&amp;#x1f1…🇷🇸1F1F7-1F1F8nosingle::🇷🇸single1🇷🇸 +11861389&amp;#x1f2…🉑1F251nosingle::🉑single1🉑 +11871390&amp;#x1f4…📂1F4C2nosingle::📂single1📂 +11881391&amp;#x1f1…🇹🇿1F1F9-1F1FFnosingle::🇹🇿single1🇹🇿 +11891392&amp;#x1f4…📫1F4EBnosingle::📫single1📫 +11901393&amp;#x1f5…🕘1F558nosingle::🕘single1🕘 +11911394&amp;#x1f6…🚊1F68Anosingle::🚊single1🚊 +11921395&amp;#x1f6…🚈1F688nosingle::🚈single1🚈 +11931396&amp;#x1f5…🔂1F502nosingle::🔂single1🔂 +11941397&amp;#1013…➗2797nosingle::➗single1➗ +11951398&amp;#x1f5…🕗1F557nosingle::🕗single1🕗 +11961399&amp;#x1f4…👨‍👩‍👧‍👧1F468-200D-1F469-200D-1F467-200D-1F467nosingle::👨‍👩‍👧‍👧single1👨‍👩‍👧‍👧 +119713100&amp;#x1f2…🈹1F239nosingle::🈹single1🈹 +119813101&amp;#x1f1…🇱🇺1F1F1-1F1FAnosingle::🇱🇺single1🇱🇺 +119913102&amp;#x1f6…🛤️1F6E4-FE0Fnosingle::🛤️single1🛤️ +120013103&amp;#x1f1…🇺🇬1F1FA-1F1ECnosingle::🇺🇬single1🇺🇬 +120113104&amp;#x1f5…🕖1F556nosingle::🕖single1🕖 +120213105&amp;#x1f2…🈴1F234nosingle::🈴single1🈴 +120313106&amp;#x1f4…👨‍🔧1F468-200D-1F527nosingle::👨‍🔧single1👨‍🔧 +120413107&amp;#x1f5…🔏1F50Fnosingle::🔏single1🔏 +120513108&amp;#x1f1…🇧🇼1F1E7-1F1FCnosingle::🇧🇼single1🇧🇼 +120613109&amp;#x1f6…🚷1F6B7nosingle::🚷single1🚷 +120713110&amp;#x1f1…🇲🇰1F1F2-1F1F0nosingle::🇲🇰single1🇲🇰 +120813111&amp;#x1f1…🇱🇻1F1F1-1F1FBnosingle::🇱🇻single1🇱🇻 +120913112&amp;#x1f5…🕙1F559nosingle::🕙single1🕙 +121013113&amp;#x1f1…🇹🇹1F1F9-1F1F9nosingle::🇹🇹single1🇹🇹 +121113114&amp;#x1f1…🇱🇹1F1F1-1F1F9nosingle::🇱🇹single1🇱🇹 +121213115&amp;#x1f4…👩‍❤️‍💋‍👨1F469-200D-2764-FE0F-200D-1F48B-200D-1F468nosingle::👩‍❤️‍💋‍👨single1👩‍❤️‍💋‍👨 +121313116&amp;#x1f4…👨‍👧1F468-200D-1F467nosingle::👨‍👧single1👨‍👧 +121413117&amp;#x1f4…📟1F4DFnosingle::📟single1📟 +121513118&amp;#x1f4…👩‍✈️1F469-200D-2708-FE0Fnosingle::👩‍✈️single1👩‍✈️ +121613119&amp;#9208…⏸️23F8-FE0Fnosingle::⏸️single1⏸️ +121713120&amp;#x1f4…👩‍🚒1F469-200D-1F692nosingle::👩‍🚒single1👩‍🚒 +121813121&amp;#x1f1…🇿🇼1F1FF-1F1FCnosingle::🇿🇼single1🇿🇼 +121913122&amp;#x1f9…🥡1F961nosingle::🥡single1🥡 +122013123&amp;#x1f1…🇸🇰1F1F8-1F1F0nosingle::🇸🇰single1🇸🇰 +122113124&amp;#x1f5…🕔1F554nosingle::🕔single1🕔 +1222141&amp;#x1f1…🇴🇲1F1F4-1F1F2nosingle::🇴🇲single1🇴🇲 +1223142&amp;#x1f4…💾1F4BEnosingle::💾single1💾 +1224143&amp;#x1f4…📤1F4E4nosingle::📤single1📤 +1225144&amp;#9879…⚗️2697-FE0Fnosingle::⚗️single1⚗️ +1226145&amp;#x1f5…🔠1F520nosingle::🔠single1🔠 +1227146&amp;#x1f6…🚝1F69Dnosingle::🚝single1🚝 +1228147&amp;#x1f1…🆑1F191nosingle::🆑single1🆑 +1229148&amp;#x1f1…🇲🇲1F1F2-1F1F2nosingle::🇲🇲single1🇲🇲 +1230149&amp;#x1f4…👨‍👦1F468-200D-1F466nosingle::👨‍👦single1👨‍👦 +12311410&amp;#x1f1…🇲🇹1F1F2-1F1F9nosingle::🇲🇹single1🇲🇹 +12321411&amp;#x1f5…🕚1F55Anosingle::🕚single1🕚 +12331412&amp;#x1f1…🇧🇦1F1E7-1F1E6nosingle::🇧🇦single1🇧🇦 +12341413&amp;#x1f4…👩‍👧‍👦1F469-200D-1F467-200D-1F466nosingle::👩‍👧‍👦single1👩‍👧‍👦 +12351414&amp;#x1f3…🎿1F3BFnosingle::🎿single1🎿 +12361415&amp;#x1f5…🔤1F524nosingle::🔤single1🔤 +12371416&amp;#x1f2…🈶1F236nosingle::🈶single1🈶 +12381417&amp;#x1f6…🚱1F6B1nosingle::🚱single1🚱 +12391418&amp;#x1f6…🚏1F68Fnosingle::🚏single1🚏 +12401419&amp;#x1f1…🇳🇪1F1F3-1F1EAnosingle::🇳🇪single1🇳🇪 +12411420&amp;#x1f4…👩‍🔧1F469-200D-1F527nosingle::👩‍🔧single1👩‍🔧 +12421421&amp;#x1f1…🇧🇸1F1E7-1F1F8nosingle::🇧🇸single1🇧🇸 +12431422&amp;#x1f5…🕍1F54Dnosingle::🕍single1🕍 +12441423&amp;#x1f1…🇰🇭1F1F0-1F1EDnosingle::🇰🇭single1🇰🇭 +12451424&amp;#x1f5…🔢1F522nosingle::🔢single1🔢 +12461425&amp;#x1f1…🇨🇾1F1E8-1F1FEnosingle::🇨🇾single1🇨🇾 +12471426&amp;#x1f4…📭1F4EDnosingle::📭single1📭 +12481427&amp;#x1f4…📪1F4EAnosingle::📪single1📪 +12491428&amp;#x1f2…🈳1F233nosingle::🈳single1🈳 +12501429&amp;#x1f1…🇲🇷1F1F2-1F1F7nosingle::🇲🇷single1🇲🇷 +12511430&amp;#x1f4…👩‍❤️‍👨1F469-200D-2764-FE0F-200D-1F468nosingle::👩‍❤️‍👨single1👩‍❤️‍👨 +12521431&amp;#x1f4…👩‍👩‍👧1F469-200D-1F469-200D-1F467nosingle::👩‍👩‍👧single1👩‍👩‍👧 +12531432&amp;#x1f1…🇰🇿1F1F0-1F1FFnosingle::🇰🇿single1🇰🇿 +12541433&amp;#x1f5…🖨️1F5A8-FE0Fnosingle::🖨️single1🖨️ +12551434&amp;#9198…⏮️23EE-FE0Fnosingle::⏮️single1⏮️ +12561435&amp;#x1f1…🇬🇾1F1EC-1F1FEnosingle::🇬🇾single1🇬🇾 +12571436&amp;#x1f1…🇸🇮1F1F8-1F1EEnosingle::🇸🇮single1🇸🇮 +12581437&amp;#x1f1…🇧🇾1F1E7-1F1FEnosingle::🇧🇾single1🇧🇾 +12591438&amp;#x1f1…🇸🇴1F1F8-1F1F4nosingle::🇸🇴single1🇸🇴 +12601439&amp;#x1f5…🕠1F560nosingle::🕠single1🕠 +12611440&amp;#x1f1…🇦🇼1F1E6-1F1FCnosingle::🇦🇼single1🇦🇼 +12621441&amp;#x1f5…🖲️1F5B2-FE0Fnosingle::🖲️single1🖲️ +12631442&amp;#9905…⚱️26B1-FE0Fnosingle::⚱️single1⚱️ +12641443&amp;#x1f1…🇲🇪1F1F2-1F1EAnosingle::🇲🇪single1🇲🇪 +12651444&amp;#x1f1…🇿🇲1F1FF-1F1F2nosingle::🇿🇲single1🇿🇲 +12661445&amp;#x1f1…🇧🇧1F1E7-1F1E7nosingle::🇧🇧single1🇧🇧 +12671446&amp;#x1f1…🇹🇯1F1F9-1F1EFnosingle::🇹🇯single1🇹🇯 +12681447&amp;#x1f1…🇨🇬1F1E8-1F1ECnosingle::🇨🇬single1🇨🇬 +12691448&amp;#x1f1…🇺🇿1F1FA-1F1FFnosingle::🇺🇿single1🇺🇿 +12701449&amp;#x1f1…🇸🇱1F1F8-1F1F1nosingle::🇸🇱single1🇸🇱 +12711450&amp;#x1f1…🇳🇦1F1F3-1F1E6nosingle::🇳🇦single1🇳🇦 +12721451&amp;#x1f6…🚟1F69Fnosingle::🚟single1🚟 +12731452&amp;#x1f1…🇧🇮1F1E7-1F1EEnosingle::🇧🇮single1🇧🇮 +12741453&amp;#x1f1…🇽🇰1F1FD-1F1F0nosingle::🇽🇰single1🇽🇰 +12751454&amp;#x1f1…🇦🇮1F1E6-1F1EEnosingle::🇦🇮single1🇦🇮 +12761455&amp;#x1f1…🇪🇹1F1EA-1F1F9nosingle::🇪🇹single1🇪🇹 +12771456&amp;#x1f4…👩‍👩‍👧‍👧1F469-200D-1F469-200D-1F467-200D-1F467nosingle::👩‍👩‍👧‍👧single1👩‍👩‍👧‍👧 +12781457&amp;#x1f1…🇦🇬1F1E6-1F1ECnosingle::🇦🇬single1🇦🇬 +12791458&amp;#x1f4…👨‍🏭1F468-200D-1F3EDnosingle::👨‍🏭single1👨‍🏭 +12801459&amp;#x1f4…👩‍👧‍👧1F469-200D-1F467-200D-1F467nosingle::👩‍👧‍👧single1👩‍👧‍👧 +12811460&amp;#x1f4…👨‍👨‍👧‍👧1F468-200D-1F468-200D-1F467-200D-1F467nosingle::👨‍👨‍👧‍👧single1👨‍👨‍👧‍👧 +12821461&amp;#x1f2…🈯1F22Fnosingle::🈯single1🈯 +12831462&amp;#x1f6…🚠1F6A0nosingle::🚠single1🚠 +12841463&amp;#x1f5…🕟1F55Fnosingle::🕟single1🕟 +12851464&amp;#x1f4…👨‍👨‍👦‍👦1F468-200D-1F468-200D-1F466-200D-1F466nosingle::👨‍👨‍👦‍👦single1👨‍👨‍👦‍👦 +12861465&amp;#x1f4…👩‍👩‍👧‍👦1F469-200D-1F469-200D-1F467-200D-1F466nosingle::👩‍👩‍👧‍👦single1👩‍👩‍👧‍👦 +12871466&amp;#x1f1…🇷🇼1F1F7-1F1FCnosingle::🇷🇼single1🇷🇼 +12881467&amp;#x1f1…🇦🇽1F1E6-1F1FDnosingle::🇦🇽single1🇦🇽 +12891468&amp;#x1f4…👩‍👦‍👦1F469-200D-1F466-200D-1F466nosingle::👩‍👦‍👦single1👩‍👦‍👦 +12901469&amp;#x1f1…🇫🇯1F1EB-1F1EFnosingle::🇫🇯single1🇫🇯 +12911470&amp;#x1f9…🥌1F94Cnosingle::🥌single1🥌 +12921471&amp;#x1f1…🇦🇸1F1E6-1F1F8nosingle::🇦🇸single1🇦🇸 +12931472&amp;#x1f1…🇹🇩1F1F9-1F1E9nosingle::🇹🇩single1🇹🇩 +12941473&amp;#x1f4…👨‍👧‍👦1F468-200D-1F467-200D-1F466nosingle::👨‍👧‍👦single1👨‍👧‍👦 +12951474&amp;#x1f1…🇦🇩1F1E6-1F1E9nosingle::🇦🇩single1🇦🇩 +12961475&amp;#x1f1…🇬🇲1F1EC-1F1F2nosingle::🇬🇲single1🇬🇲 +12971476&amp;#x1f1…🇬🇦1F1EC-1F1E6nosingle::🇬🇦single1🇬🇦 +12981477&amp;#x1f5…🗂️1F5C2-FE0Fnosingle::🗂️single1🗂️ +12991478&amp;#x1f1…🇧🇲1F1E7-1F1F2nosingle::🇧🇲single1🇧🇲 +13001479&amp;#x1f4…👩‍🏭1F469-200D-1F3EDnosingle::👩‍🏭single1👩‍🏭 +1301151&amp;#x1f5…🕝1F55Dnosingle::🕝single1🕝 +1302152&amp;#x1f5…🕦1F566nosingle::🕦single1🕦 +1303153&amp;#x1f5…🕢1F562nosingle::🕢single1🕢 +1304154&amp;#x1f5…🕜1F55Cnosingle::🕜single1🕜 +1305155&amp;#x1f1…🇲🇳1F1F2-1F1F3nosingle::🇲🇳single1🇲🇳 +1306156&amp;#x1f1…🇹🇬1F1F9-1F1ECnosingle::🇹🇬single1🇹🇬 +1307157&amp;#x1f5…🕤1F564nosingle::🕤single1🕤 +1308158&amp;#x1f1…🇲🇿1F1F2-1F1FFnosingle::🇲🇿single1🇲🇿 +1309159&amp;#x1f4…👨‍👨‍👦1F468-200D-1F468-200D-1F466nosingle::👨‍👨‍👦single1👨‍👨‍👦 +13101510&amp;#x1f2…🈂️1F202-FE0Fnosingle::🈂️single1🈂️ +13111511&amp;#x1f1…🇬🇼1F1EC-1F1FCnosingle::🇬🇼single1🇬🇼 +13121512&amp;#x1f1…🇧🇳1F1E7-1F1F3nosingle::🇧🇳single1🇧🇳 +13131513&amp;#x1f1…🇲🇻1F1F2-1F1FBnosingle::🇲🇻single1🇲🇻 +13141514&amp;#x1f5…🕧1F567nosingle::🕧single1🕧 +13151515&amp;#x1f1…🇲🇴1F1F2-1F1F4nosingle::🇲🇴single1🇲🇴 +13161516&amp;#x1f5…🕡1F561nosingle::🕡single1🕡 +13171517&amp;#9934…⛎26CEnosingle::⛎single1⛎ +13181518&amp;#x1f1…🇹🇴1F1F9-1F1F4nosingle::🇹🇴single1🇹🇴 +13191519&amp;#x1f5…🕣1F563nosingle::🕣single1🕣 +13201520&amp;#x1f4…👨‍👨‍👧‍👦1F468-200D-1F468-200D-1F467-200D-1F466nosingle::👨‍👨‍👧‍👦single1👨‍👨‍👧‍👦 +13211521&amp;#x1f1…🇪🇷1F1EA-1F1F7nosingle::🇪🇷single1🇪🇷 +13221522&amp;#x1f1…🇦🇨1F1E6-1F1E8nosingle::🇦🇨single1🇦🇨 +13231523&amp;#x1f1…🇰🇬1F1F0-1F1ECnosingle::🇰🇬single1🇰🇬 +13241524&amp;#x1f1…🇦🇶1F1E6-1F1F6nosingle::🇦🇶single1🇦🇶 +13251525&amp;#x1f1…🇨🇻1F1E8-1F1FBnosingle::🇨🇻single1🇨🇻 +13261526&amp;#x1f1…🇧🇫1F1E7-1F1EBnosingle::🇧🇫single1🇧🇫 +13271527&amp;#x1f1…🇰🇵1F1F0-1F1F5nosingle::🇰🇵single1🇰🇵 +13281528&amp;#x1f4…👨‍👦‍👦1F468-200D-1F466-200D-1F466nosingle::👨‍👦‍👦single1👨‍👦‍👦 +13291529&amp;#x1f5…🕥1F565nosingle::🕥single1🕥 +13301530&amp;#x1f1…🇲🇩1F1F2-1F1E9nosingle::🇲🇩single1🇲🇩 +13311531&amp;#x1f1…🇮🇨1F1EE-1F1E8nosingle::🇮🇨single1🇮🇨 +13321532&amp;#x1f1…🇻🇦1F1FB-1F1E6nosingle::🇻🇦single1🇻🇦 +13331533&amp;#x1f1…🇬🇩1F1EC-1F1E9nosingle::🇬🇩single1🇬🇩 +13341534&amp;#x1f1…🇮🇲1F1EE-1F1F2nosingle::🇮🇲single1🇮🇲 +13351535&amp;#x1f4…👩‍👩‍👦‍👦1F469-200D-1F469-200D-1F466-200D-1F466nosingle::👩‍👩‍👦‍👦single1👩‍👩‍👦‍👦 +13361536&amp;#x1f1…🇲🇫1F1F2-1F1EBnosingle::🇲🇫single1🇲🇫 +13371537&amp;#x1f4…📇1F4C7nosingle::📇single1📇 +13381538&amp;#x1f2…🈺1F23Anosingle::🈺single1🈺 +13391539&amp;#x1f1…🇱🇦1F1F1-1F1E6nosingle::🇱🇦single1🇱🇦 +13401540&amp;#x1f6…🛷1F6F7nosingle::🛷single1🛷 +13411541&amp;#x1f5…🕞1F55Enosingle::🕞single1🕞 +13421542&amp;#x1f1…🇲🇼1F1F2-1F1FCnosingle::🇲🇼single1🇲🇼 +13431543&amp;#x1f4…👩‍👩‍👦1F469-200D-1F469-200D-1F466nosingle::👩‍👩‍👦single1👩‍👩‍👦 +13441544&amp;#x1f4…👨‍👨‍👧1F468-200D-1F468-200D-1F467nosingle::👨‍👨‍👧single1👨‍👨‍👧 +13451545&amp;#x1f2…🈷️1F237-FE0Fnosingle::🈷️single1🈷️ +13461546&amp;#x1f4…👨‍👧‍👧1F468-200D-1F467-200D-1F467nosingle::👨‍👧‍👧single1👨‍👧‍👧 +13471547&amp;#x1f1…🇺🇳1F1FA-1F1F3nosingle::🇺🇳single1🇺🇳 +13481548&amp;#x1f5…🗃️1F5C3-FE0Fnosingle::🗃️single1🗃️ +13491549&amp;#x1f1…🇱🇸1F1F1-1F1F8nosingle::🇱🇸single1🇱🇸 +13501550&amp;#x1f5…🗜️1F5DC-FE0Fnosingle::🗜️single1🗜️ +13511551&amp;#x1f1…🇬🇺1F1EC-1F1FAnosingle::🇬🇺single1🇬🇺 +13521552&amp;#x1f1…🇧🇻1F1E7-1F1FBnosingle::🇧🇻single1🇧🇻 +13531553&amp;#x1f5…🔡1F521nosingle::🔡single1🔡 +13541554&amp;#x1f1…🇫🇴1F1EB-1F1F4nosingle::🇫🇴single1🇫🇴 +13551555&amp;#x1f1…🇲🇺1F1F2-1F1FAnosingle::🇲🇺single1🇲🇺 +13561556&amp;#x1f1…🇵🇬1F1F5-1F1ECnosingle::🇵🇬single1🇵🇬 +13571557&amp;#x1f1…🇨🇼1F1E8-1F1FCnosingle::🇨🇼single1🇨🇼 +13581558&amp;#x1f1…🇬🇬1F1EC-1F1ECnosingle::🇬🇬single1🇬🇬 +13591559&amp;#x1f2…🈁1F201nosingle::🈁single1🈁 +13601560&amp;#x1f5…🔣1F523nosingle::🔣single1🔣 +13611561&amp;#x1f1…🇼🇸1F1FC-1F1F8nosingle::🇼🇸single1🇼🇸 +13621562&amp;#x1f1…🇸🇷1F1F8-1F1F7nosingle::🇸🇷single1🇸🇷 +13631563&amp;#x1f1…🇹🇲1F1F9-1F1F2nosingle::🇹🇲single1🇹🇲 +13641564&amp;#x1f1…🇸🇸1F1F8-1F1F8nosingle::🇸🇸single1🇸🇸 +13651565&amp;#x1f5…🗄️1F5C4-FE0Fnosingle::🗄️single1🗄️ +13661566&amp;#x1f1…🇧🇹1F1E7-1F1F9nosingle::🇧🇹single1🇧🇹 +13671567&amp;#x1f1…🇸🇽1F1F8-1F1FDnosingle::🇸🇽single1🇸🇽 +13681568&amp;#x1f1…🇰🇲1F1F0-1F1F2nosingle::🇰🇲single1🇰🇲 +13691569&amp;#x1f2…🈸1F238nosingle::🈸single1🈸 +13701570&amp;#x1f1…🇩🇲1F1E9-1F1F2nosingle::🇩🇲single1🇩🇲 +13711571&amp;#x1f1…🇪🇭1F1EA-1F1EDnosingle::🇪🇭single1🇪🇭 +13721572&amp;#x1f1…🇧🇿1F1E7-1F1FFnosingle::🇧🇿single1🇧🇿 +13731573&amp;#x1f1…🇻🇮1F1FB-1F1EEnosingle::🇻🇮single1🇻🇮 +13741574&amp;#x1f1…🇬🇫1F1EC-1F1EBnosingle::🇬🇫single1🇬🇫 +13751575&amp;#x1f1…🇸🇲1F1F8-1F1F2nosingle::🇸🇲single1🇸🇲 +13761576&amp;#x1f1…🇩🇯1F1E9-1F1EFnosingle::🇩🇯single1🇩🇯 +13771577&amp;#x1f1…🇹🇨1F1F9-1F1E8nosingle::🇹🇨single1🇹🇨 +1378161&amp;#x1f1…🇱🇨1F1F1-1F1E8nosingle::🇱🇨single1🇱🇨 +1379162&amp;#x1f1…🇻🇺1F1FB-1F1FAnosingle::🇻🇺single1🇻🇺 +1380163&amp;#x1f1…🇵🇫1F1F5-1F1EBnosingle::🇵🇫single1🇵🇫 +1381164&amp;#x1f1…🇬🇮1F1EC-1F1EEnosingle::🇬🇮single1🇬🇮 +1382165&amp;#x1f1…🇸🇨1F1F8-1F1E8nosingle::🇸🇨single1🇸🇨 +1383166&amp;#x1f1…🇰🇳1F1F0-1F1F3nosingle::🇰🇳single1🇰🇳 +1384167&amp;#x1f1…🇯🇪1F1EF-1F1EAnosingle::🇯🇪single1🇯🇪 +1385168&amp;#x1f1…🇲🇶1F1F2-1F1F6nosingle::🇲🇶single1🇲🇶 +1386169&amp;#x1f4…👨‍👩‍👦1F468-200D-1F469-200D-1F466nosingle::👨‍👩‍👦single1👨‍👩‍👦 +13871610&amp;#x1f1…🇷🇪1F1F7-1F1EAnosingle::🇷🇪single1🇷🇪 +13881611&amp;#x1f1…🇸🇿1F1F8-1F1FFnosingle::🇸🇿single1🇸🇿 +13891612&amp;#x1f1…🇰🇾1F1F0-1F1FEnosingle::🇰🇾single1🇰🇾 +13901613&amp;#9167…⏏️23CF-FE0Fnosingle::⏏️single1⏏️ +13911614&amp;#x1f1…🇻🇨1F1FB-1F1E8nosingle::🇻🇨single1🇻🇨 +13921615&amp;#x1f1…🇻🇬1F1FB-1F1ECnosingle::🇻🇬single1🇻🇬 +13931616&amp;#x1f1…🇱🇮1F1F1-1F1EEnosingle::🇱🇮single1🇱🇮 +13941617&amp;#x1f1…🇨🇨1F1E8-1F1E8nosingle::🇨🇨single1🇨🇨 +13951618&amp;#x1f1…🇨🇫1F1E8-1F1EBnosingle::🇨🇫single1🇨🇫 +13961619&amp;#x1f1…🇸🇹1F1F8-1F1F9nosingle::🇸🇹single1🇸🇹 +13971620&amp;#x1f1…🇬🇱1F1EC-1F1F1nosingle::🇬🇱single1🇬🇱 +13981621&amp;#x1f1…🇵🇼1F1F5-1F1FCnosingle::🇵🇼single1🇵🇼 +13991622&amp;#x1f1…🇬🇵1F1EC-1F1F5nosingle::🇬🇵single1🇬🇵 +14001623&amp;#x1f1…🇭🇲1F1ED-1F1F2nosingle::🇭🇲single1🇭🇲 +14011624&amp;#x1f1…🇨🇰1F1E8-1F1F0nosingle::🇨🇰single1🇨🇰 +14021625&amp;#x1f1…🇫🇰1F1EB-1F1F0nosingle::🇫🇰single1🇫🇰 +14031626&amp;#x1f1…🇵🇳1F1F5-1F1F3nosingle::🇵🇳single1🇵🇳 +14041627&amp;#x1f1…🇳🇫1F1F3-1F1EBnosingle::🇳🇫single1🇳🇫 +14051628&amp;#x1f1…🇸🇧1F1F8-1F1E7nosingle::🇸🇧single1🇸🇧 +14061629&amp;#x1f1…🇹🇱1F1F9-1F1F1nosingle::🇹🇱single1🇹🇱 +14071630&amp;#x1f1…🇫🇲1F1EB-1F1F2nosingle::🇫🇲single1🇫🇲 +14081631&amp;#x1f1…🇮🇴1F1EE-1F1F4nosingle::🇮🇴single1🇮🇴 +14091632&amp;#x1f1…🇲🇭1F1F2-1F1EDnosingle::🇲🇭single1🇲🇭 +14101633&amp;#x1f1…🇬🇶1F1EC-1F1F6nosingle::🇬🇶single1🇬🇶 +14111634&amp;#x1f1…🇹🇰1F1F9-1F1F0nosingle::🇹🇰single1🇹🇰 +14121635&amp;helli……2026nosingle::…single1… +141317+1&amp;#x1f1…🇧🇶1F1E7-1F1F6nosingle::🇧🇶single1🇧🇶 +141417+2&amp;#x1f1…🇲🇵1F1F2-1F1F5nosingle::🇲🇵single1🇲🇵 +141517+3&amp;#x1f1…🇨🇽1F1E8-1F1FDnosingle::🇨🇽single1🇨🇽 +141617+4&amp;#x1f1…🇰🇮1F1F0-1F1EEnosingle::🇰🇮single1🇰🇮 +141717+5&amp;#x1f1…🇳🇺1F1F3-1F1FAnosingle::🇳🇺single1🇳🇺 +141817+6&amp;#x1f1…🇸🇯1F1F8-1F1EFnosingle::🇸🇯single1🇸🇯 +141917+7&amp;#x1f1…🇲🇸1F1F2-1F1F8nosingle::🇲🇸single1🇲🇸 +142017+8&amp;#x1f1…🇸🇭1F1F8-1F1EDnosingle::🇸🇭single1🇸🇭 +142117+9&amp;#x1f1…🇹🇻1F1F9-1F1FBnosingle::🇹🇻single1🇹🇻 +142217+10&amp;#x1f1…🇳🇨1F1F3-1F1E8nosingle::🇳🇨single1🇳🇨 +142317+11&amp;#x1f1…🇳🇷1F1F3-1F1F7nosingle::🇳🇷single1🇳🇷 +142417+12&amp;#x1f1…🇧🇱1F1E7-1F1F1nosingle::🇧🇱single1🇧🇱 +142517+13&amp;#x1f1…🇼🇫1F1FC-1F1EBnosingle::🇼🇫single1🇼🇫 +142617+14&amp;#x1f1…🇬🇸…1F1EC-1F1F8-2026nosingle::🇬🇸…single1🇬🇸… From 5dc72d79c17fb6e1affcba581310950420ee36cb Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Sun, 31 May 2026 22:33:13 +0200 Subject: [PATCH 095/110] Document Service and boot_completed intent filter --- docs/apps/app-lifecycle.md | 40 +++++ docs/apps/creating-apps.md | 28 +++ docs/architecture/boot-sequence.md | 11 +- docs/frameworks/app-manager.md | 50 ++++++ docs/frameworks/service.md | 267 +++++++++++++++++++++++++++++ mkdocs.yml | 1 + 6 files changed, 394 insertions(+), 3 deletions(-) create mode 100644 docs/frameworks/service.md diff --git a/docs/apps/app-lifecycle.md b/docs/apps/app-lifecycle.md index 36abcfb..c970a16 100644 --- a/docs/apps/app-lifecycle.md +++ b/docs/apps/app-lifecycle.md @@ -413,6 +413,45 @@ class NetworkAwareActivity(Activity): ❌ Don't assume the activity is still visible after async operations ❌ Don't store references to LVGL objects after `onDestroy()` +## Services — Background Components + +Alongside Activities, MicroPythonOS provides **Services** for background work that runs without a user interface. Unlike Activities, Services have no screen lifecycle — they are created, started, and destroyed by intent actions (e.g., `boot_completed`). + +### Service Lifecycle + +``` +┌────────────┐ +│ onCreate │ ← One-time initialization +└─────┬──────┘ + │ + ▼ +┌────────────┐ +│ onStart │ ← Receive intent, begin work +└─────┬──────┘ + │ + ▼ +┌────────────┐ +│ onDestroy │ ← Cleanup +└────────────┘ +``` + +### Service vs. Activity Comparison + +| Method | Activity | Service | +|--------|----------|---------| +| `onCreate()` | Build UI, set up screen | Initialize resources | +| `onStart()` | Screen about to become visible | Start background work (receives Intent) | +| `onDestroy()` | Activity removed from stack | Stop threads, cancel tasks | + +### When to Use a Service + +- Starting WiFi auto-connect at boot +- Launching a background web server +- Running a periodic update check +- Starting an async REPL task + +Services are declared either programmatically (system services) or via `"services"` in the app's `MANIFEST.JSON`. See the [Service documentation](../frameworks/service.md) for a complete reference. + ## Lifecycle Comparison with Android | MicroPythonOS | Android | Notes | @@ -435,3 +474,4 @@ class NetworkAwareActivity(Activity): - [AppManager](../frameworks/app-manager.md) - App management and launching - [Intent System](../frameworks/app-manager.md#intent-resolution) - Intent-based navigation - [TaskManager](../frameworks/task-manager.md) - Async task management +- [Service](../frameworks/service.md) - Background services for boot-time and long-running tasks diff --git a/docs/apps/creating-apps.md b/docs/apps/creating-apps.md index 38c78af..b221e50 100644 --- a/docs/apps/creating-apps.md +++ b/docs/apps/creating-apps.md @@ -69,6 +69,34 @@ In `MANIFEST.JSON`, put: } ``` +### Services + +Apps can also declare **services** — background components that run at boot time with no user interface. Services are defined in the `"services"` array of the manifest: + +```json +"services": [ + { + "entrypoint": "assets/my_boot_service.py", + "classname": "MyBootService", + "intent_filters": [ + { + "action": "boot_completed" + } + ] + } +] +``` + +Each service entry has: + +| Field | Description | +|-------|-------------| +| `entrypoint` | Path to the Python file (relative to the app root) | +| `classname` | Name of the `Service` subclass in that file | +| `intent_filters` | Array of `{ "action": "..." }` objects. Use `"boot_completed"` to run at startup | + +Services that subscribe to `"boot_completed"` are started automatically during system boot, after the launcher is displayed. See the [Service documentation](../frameworks/service.md) for details on writing and using services. + ## Icon The icon is a [simple 64x64 pixel PNG image](https://github.com/MicroPythonOS/MicroPythonOS/blob/main/internal_filesystem/builtin/apps/com.micropythonos.launcher/res/mipmap-mdpi/icon_64x64.png), which you can create with any tool, such as GIMP. diff --git a/docs/architecture/boot-sequence.md b/docs/architecture/boot-sequence.md index cd25a31..5d5010d 100644 --- a/docs/architecture/boot-sequence.md +++ b/docs/architecture/boot-sequence.md @@ -12,11 +12,16 @@ MicroPythonOS consists of several core components that initialize and manage the - Mounts the freezefs into /builtin/ - Loads the com.micropythonos.settings - Initializes the user interface. - - Starts the WiFi autoconnect thread - Launches the `launcher` app that shows the icons - - Starts the asyncio REPL + - Runs auto-start apps (`auto_start_app_early`, `auto_start_app`) + - Starts **boot services** — all services that subscribe to the `boot_completed` intent are instantiated and receive `onStart()`: + - `WifiBootService` — auto-connects WiFi in a background thread + - `WebServerBootService` — starts the HTTP web server + - `AIOReplService` — starts the asyncio REPL task + - App-specific services (e.g., `OSUpdateService`) - Marks the current boot as successful (cancel rollback) - - Starts the TaskManager + - Starts the TaskManager (asyncio event loop) See [Filesystem Layout](filesystem.md) for where apps and data are stored. +See [Service](../frameworks/service.md) for details on writing boot services. diff --git a/docs/frameworks/app-manager.md b/docs/frameworks/app-manager.md index d9bf3b7..722cb9b 100644 --- a/docs/frameworks/app-manager.md +++ b/docs/frameworks/app-manager.md @@ -14,6 +14,7 @@ AppManager provides: - **Version management** - Compares versions and detects available updates - **Intent resolution** - Resolves activities that handle specific intents (Android-inspired) - **Launcher management** - Finds and restarts the system launcher +- **Service management** - Registers, resolves, and starts boot services ## Quick Start @@ -606,6 +607,54 @@ Stop all activities and restart launcher. 2. Starts launcher app 3. Useful for returning to home screen +### Service Management + +**`register_service(action, service_cls, fullname=None)`** + +Register a service class to handle an intent action (programmatic registration, used by system services). + +- **Parameters:** + - `action` (str): Intent action (e.g., `"boot_completed"`) + - `service_cls` (type): Service subclass + - `fullname` (str, optional): App fullname to associate with the service + +- **Example:** + ```python + from mpos.content.app_manager import AppManager + from mpos import Service + + class MyBootService(Service): + def onStart(self, intent): + print("Boot service started") + + AppManager.register_service("boot_completed", MyBootService, fullname="com.example.myapp") + ``` + +**`get_services_for_action(action)`** + +Resolve all services (manifest-declared and programmatically registered) that handle a given intent action. + +- **Parameters:** + - `action` (str): Intent action (e.g., `"boot_completed"`) + +- **Returns:** `list[(str, type)]` - List of `(app_fullname, ServiceClass)` tuples + +- **Behavior:** + 1. Scans all installed apps' manifests for matching service entries + 2. Imports each service module and extracts the class + 3. Merges with programmatically registered services from `_service_registry` + +**`start_boot_services()`** + +Start all services that subscribe to the `boot_completed` intent. Called once during system boot after the launcher is displayed. + +- **Behavior:** + 1. Calls `get_services_for_action("boot_completed")` to discover all boot services + 2. For each service: instantiate, set `appFullName`, call `onCreate()`, then `onStart(boot_intent)` + 3. Handles errors per-service so one failing service does not block others + +- **Called by:** `main.py` during system boot sequence + ## Best Practices ### Do's @@ -731,3 +780,4 @@ if app: - [Activity Framework](../apps/app-lifecycle.md) - Activity lifecycle and management - [Intent System](../architecture/intents.md) - Intent-based app communication - [Frameworks Overview](../architecture/frameworks.md) - All available frameworks +- [Service](../frameworks/service.md) - Background services and boot-time execution diff --git a/docs/frameworks/service.md b/docs/frameworks/service.md new file mode 100644 index 0000000..50e1809 --- /dev/null +++ b/docs/frameworks/service.md @@ -0,0 +1,267 @@ +# Service + +MicroPythonOS provides a **Service** framework for background operations that run independently of the UI, inspired by Android's Service model. Services are ideal for WiFi auto-connect, web server startup, async REPL tasks, periodic update checks, and similar boot-time or long-running background work. + +## Overview + +Services differ from Activities in key ways: + +- **No user interface** — A Service has no screen, no LVGL objects, and no lifecycle tied to visibility. +- **Independent lifecycle** — Services are created, started, and destroyed by the system without user interaction. +- **Boot-time execution** — Services can subscribe to the `boot_completed` intent to run automatically at system startup. +- **Long-running** — A Service can run indefinitely (e.g., polling for updates) or terminate after completing a task. + +``` +┌───────────────────────────────────────────┐ +│ Service Lifecycle │ +├───────────────────────────────────────────┤ +│ │ +│ ┌────────────┐ │ +│ │ onCreate │ ← Initialize resources │ +│ └─────┬──────┘ │ +│ │ │ +│ ▼ │ +│ ┌────────────┐ │ +│ │ onStart │ ← Receive intent, work │ +│ └─────┬──────┘ │ +│ │ │ +│ ▼ │ +│ ┌────────────┐ │ +│ │ onDestroy │ ← Cleanup resources │ +│ └────────────┘ │ +│ │ +└───────────────────────────────────────────┘ +``` + +## Service Base Class + +The `Service` class is defined in `mpos.app.service` and re-exported from `mpos`: + +```python +from mpos import Service + +class MyService(Service): + + def __init__(self): + super().__init__() + + def onCreate(self): + # One-time initialization + pass + + def onStart(self, intent=None): + # Service is starting — do the work + pass + + def onDestroy(self): + # Clean up resources + pass +``` + +### Lifecycle Methods + +| Method | When Called | Purpose | +|--------|-------------|---------| +| `onCreate()` | After instantiation, before `onStart()` | One-time setup (allocating resources, opening files) | +| `onStart(intent)` | After `onCreate()` when the service is started | Perform the service's work; `intent` contains the action that triggered it | +| `onDestroy()` | When the service is being shut down | Release all resources, stop threads, cancel tasks | + +### Properties + +- `self.appFullName` — Set automatically by the system before `onCreate()`. Contains the fullname of the app that owns this service (e.g., `"com.micropythonos.osupdate"`). Use it instead of hard-coding the package name. + +## Declaring Services + +Services can be registered in two ways: + +### 1. Programmatic Registration (System Services) + +System services register themselves directly with `AppManager.register_service()` at module level: + +```python +from mpos import Service +from mpos.content.app_manager import AppManager + +class WifiBootService(Service): + + def onStart(self, intent): + import _thread + from mpos import WifiService, TaskManager + _thread.stack_size(TaskManager.good_stack_size()) + _thread.start_new_thread(WifiService.auto_connect, ()) + +# Register at module level +AppManager.register_service("boot_completed", WifiBootService, fullname="com.micropythonos.system") +``` + +Parameters for `register_service()`: + +- `action` (str) — Intent action string. Use `"boot_completed"` for boot-time services. +- `service_cls` (type) — The Service subclass. +- `fullname` (str, optional) — App fullname to associate with the service. Defaults to `None`. + +### 2. Manifest-Declared Services (App Services) + +Apps declare services in their `META-INF/MANIFEST.JSON` under a `"services"` array: + +```json +{ + "name": "OSUpdate", + "fullname": "com.micropythonos.osupdate", + "version": "0.1.5", + "activities": [ + { + "entrypoint": "assets/osupdate.py", + "classname": "OSUpdate", + "intent_filters": [ + { "action": "main", "category": "launcher" } + ] + } + ], + "services": [ + { + "entrypoint": "assets/osupdate_boot_service.py", + "classname": "OSUpdateService", + "intent_filters": [ + { "action": "boot_completed" } + ] + } + ] +} +``` + +Each service entry requires: + +| Field | Description | +|-------|-------------| +| `entrypoint` | Path to the Python file (relative to the app root) | +| `classname` | Name of the Service subclass in that file | +| `intent_filters` | Array of `{ "action": "..." }` objects. `"boot_completed"` triggers the service at startup | + +The corresponding service code: + +```python +# assets/osupdate_boot_service.py +from mpos import Service, ConnectivityManager, TaskManager + +class OSUpdateService(Service): + + def __init__(self): + super().__init__() + self._running = False + + def onStart(self, intent): + self._running = True + TaskManager.create_task(self._boot_loop()) + + def onDestroy(self): + self._running = False + + async def _boot_loop(self): + cm = ConnectivityManager.get() + while self._running: + await TaskManager.sleep(30) + if cm.is_online(): + print("OSUpdateService: network connected") + else: + print("OSUpdateService: network not connected") +``` + +## Boot Services + +Services that subscribe to the `boot_completed` intent are started automatically during system boot. The current boot-time service set includes: + +| Service | App | Purpose | +|---------|-----|---------| +| `WifiBootService` | `com.micropythonos.system` | Auto-connects WiFi in a background thread | +| `WebServerBootService` | `com.micropythonos.system` | Starts the HTTP web server if enabled | +| `AIOReplService` | `com.micropythonos.system` | Starts the asyncio REPL task for interactive debugging | +| `OSUpdateService` | `com.micropythonos.osupdate` | Periodically checks network connectivity for OTA updates | + +### Boot Order + +Services are started after the launcher is displayed, in this sequence: + +1. Launcher Activity is created and displayed +2. `auto_start_app_early()` runs (early-start apps) +3. `auto_start_app()` runs (normal auto-start apps) +4. **Boot services are started** — all `boot_completed` services are instantiated and `onStart()` is called +5. `TaskManager.start()` runs the asyncio event loop + +The order among boot services is non-deterministic. + +## Complete Example + +### System Service + +```python +# my_service.py +from mpos import Service +from mpos.content.app_manager import AppManager + +class BootLogger(Service): + + def __init__(self): + super().__init__() + + def onCreate(self): + print(f"BootLogger: initialized from {self.appFullName}") + + def onStart(self, intent): + print(f"BootLogger: received intent action={intent.action}") + print("BootLogger: system boot is complete") + + def onDestroy(self): + print("BootLogger: shutting down") + +AppManager.register_service("boot_completed", BootLogger, fullname="com.example.myservice") +``` + +### App Service (manifest-declared) + +``` +com.example.myapp/ +├── META-INF/ +│ └── MANIFEST.JSON # Contains "services" array +├── assets/ +│ ├── main.py # Activity entry point +│ └── my_service.py # Service entry point +└── res/ + └── mipmap-mdpi/ + └── icon_64x64.png +``` + +## Service vs. Activity + +| Aspect | Activity | Service | +|--------|----------|---------| +| UI | Has a screen with LVGL widgets | No UI | +| Lifecycle triggers | User navigation, back button | Intent actions (e.g., `boot_completed`) | +| Persistence | Exists only while in the activity stack | Can run indefinitely after boot | +| Foreground state | Tracked via `has_foreground()` | Not applicable | +| Thread usage | UI runs on main LVGL thread | May spawn threads or use asyncio tasks | + +## Best Practices + +### Do's + +✅ Use `self.appFullName` instead of hard-coding the package name +✅ Call `super().__init__()` in your `__init__` method +✅ Clean up threads and tasks in `onDestroy()` +✅ Use `TaskManager.create_task()` for async operations in services +✅ Keep `onCreate()` lightweight — do heavy work in `onStart()` + +### Don'ts + +❌ Don't create LVGL UI objects in a Service (no screen context) +❌ Don't block `onStart()` with synchronous waiting — use asyncio or threads +❌ Don't forget to stop background tasks when the service is destroyed +❌ Don't assume boot services run in a specific order + +## See Also + +- [AppManager](../frameworks/app-manager.md) — Service registration and boot management +- [App Lifecycle](../apps/app-lifecycle.md) — Activity lifecycle for UI apps +- [Boot Sequence](../architecture/boot-sequence.md) — Detailed boot flow +- [TaskManager](../frameworks/task-manager.md) — Async task management for services +- [Creating Apps](../apps/creating-apps.md) — How to add services to your app's manifest diff --git a/mkdocs.yml b/mkdocs.yml index 9238367..202f495 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -65,6 +65,7 @@ nav: - NumberFormat: frameworks/number-format.md - Preferences: frameworks/preferences.md - SensorManager: frameworks/sensor-manager.md + - Service: frameworks/service.md - SettingActivity: frameworks/setting-activity.md - SettingsActivity: frameworks/settings-activity.md - TaskManager: frameworks/task-manager.md From 4207671ca2d12ced7f73d7f9de86123c2d2b9046 Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Thu, 18 Jun 2026 09:08:13 +0200 Subject: [PATCH 096/110] Update docs --- docs/apps/bundling-apps.md | 41 +++-- docs/apps/creating-apps.md | 40 +++-- docs/architecture/filesystem.md | 2 +- docs/frameworks/app-manager.md | 32 ++-- docs/frameworks/notification-manager.md | 184 ++++++++++++++++++++++ docs/frameworks/service.md | 38 ++--- docs/frameworks/setting-activity.md | 21 +++ docs/frameworks/settings-activity.md | 1 + docs/getting-started/running.md | 6 +- docs/os-development/running-on-desktop.md | 54 +++---- docs/other/merge-checklist.md | 2 +- docs/other/release-checklist.md | 2 +- mkdocs.yml | 1 + 13 files changed, 328 insertions(+), 96 deletions(-) create mode 100644 docs/frameworks/notification-manager.md diff --git a/docs/apps/bundling-apps.md b/docs/apps/bundling-apps.md index 215f8d1..dc55af8 100644 --- a/docs/apps/bundling-apps.md +++ b/docs/apps/bundling-apps.md @@ -1,22 +1,41 @@ # Bundling Apps -To bundle your app in an .mpk file, just make an uncompressed "zip" file of it, without including the top-level `com.micropythonos.helloworld/` folder. +Apps are distributed as `.mpk` files. An `.mpk` is a ZIP archive (usually stored without compression to speed up installation on device) with a strict layout: -It's recommended to make the .mpk file deterministic by: -- setting the file timestamps to a fixed value -- sorting the files, so the order is fixed -- excluding extra file attributes and directories +- The **first entry** in the ZIP stream **must** be a top-level directory whose name matches the app's fullname exactly, followed by `/` (for example, `com.micropythonos.helloworld/`). +- That top-level directory **must** be the only top-level entry in the archive. +- All other files and subdirectories live under that single top-level directory. -For example: +So a valid archive looks like this in stream order: ``` -cd com.micropythonos.helloworld/ -find . -type f -exec touch -t 202501010000.00 {} \; # set fixed timestamp to have a deterministic zip file -find . -type f | sort | TZ=CET zip -X -r -0 /tmp/com.micropythonos.helloworld_0.0.2.mpk -@ # sort, -Xclude extra attributes, -recurse into directories and -0 compression +com.micropythonos.helloworld/ +com.micropythonos.helloworld/MANIFEST.JSON +com.micropythonos.helloworld/icon_64x64.png +com.micropythonos.helloworld/hello.py ``` +MicroPythonOS validates this layout while extracting, and rejects packages that do not follow it. This ensures packages are unambiguous and can be streamed safely onto devices with limited storage. + +## Creating an .mpk + +From the parent directory that contains your app folder, create a stored (uncompressed) ZIP that includes the top-level folder. It's recommended to make the `.mpk` deterministic by setting file timestamps to a fixed value, sorting entries, and excluding extra file attributes: + +``` +cd internal_filesystem/apps/ +find com.micropythonos.helloworld -exec touch -t 202501010000.00 {} \; +(find com.micropythonos.helloworld -type d; find com.micropythonos.helloworld -type f) | sort | TZ=CET zip -X -r -0 /tmp/com.micropythonos.helloworld_0.0.2.mpk -@ +``` + +This: + +- sorts directories before files +- uses `-0` for stored (uncompressed) entries +- uses `-X` to exclude extra file attributes +- places `com.micropythonos.helloworld/` as the first entry + ## AppStore bundling -The apps at https://apps.MicroPythonOS.com are a curated, manually reviewed, vetted collection, often created and maintainced by the MicroPythonOS core team. +The apps at https://apps.MicroPythonOS.com are a curated, manually reviewed, vetted collection, often created and maintained by the MicroPythonOS core team. -These are manually bundled into a [app_index.json](https://github.com/MicroPythonOS/apps/blob/main/app_index.json) using [`scripts/bundleapps.sh`](https://github.com/MicroPythonOS/MicroPythonOS/blob/main/scripts/bundleapps.sh) and then pushed to the [apps repo](https://github.com/MicroPythonOS/apps). +These are bundled into an [`app_index.json`](https://github.com/MicroPythonOS/apps/blob/main/app_index.json) using [`scripts/bundle_apps.sh`](https://github.com/MicroPythonOS/MicroPythonOS/blob/main/scripts/bundle_apps.sh) and then pushed to the [apps repo](https://github.com/MicroPythonOS/apps). diff --git a/docs/apps/creating-apps.md b/docs/apps/creating-apps.md index b221e50..b103560 100644 --- a/docs/apps/creating-apps.md +++ b/docs/apps/creating-apps.md @@ -12,15 +12,13 @@ Create the following file and folder structure: ``` com.micropythonos.helloworld/ -├── assets/ -│   └── hello.py -├── META-INF/ -│   └── MANIFEST.JSON -└── res/ - └── mipmap-mdpi/ - └── icon_64x64.png +├── MANIFEST.JSON +├── icon_64x64.png +└── hello.py ``` +This flat layout puts the required `MANIFEST.JSON` and app icon at the top level, alongside your Python files. Subfolders are still allowed if you want to organize larger apps, but **each directory costs about 8 KiB of storage in LittleFS**, so use them sparingly on device. + ## App code In `hello.py`, put: @@ -45,18 +43,18 @@ The code above creates a new screen, adds a label, sets the label text, centers In `MANIFEST.JSON`, put: -``` +```json { -"name": "HelloWorld", -"publisher": "MicroPythonOS", -"short_description": "Minimal app", -"long_description": "Demonstrates the simplest app.", -"fullname": "com.micropythonos.helloworld", -"version": "0.0.2", -"category": "development", -"activities": [ + "name": "HelloWorld", + "publisher": "MicroPythonOS", + "short_description": "Minimal app", + "long_description": "Demonstrates the simplest app.", + "fullname": "com.micropythonos.helloworld", + "version": "0.0.2", + "category": "development", + "activities": [ { - "entrypoint": "assets/hello.py", + "entrypoint": "hello.py", "classname": "Hello", "intent_filters": [ { @@ -76,7 +74,7 @@ Apps can also declare **services** — background components that run at boot ti ```json "services": [ { - "entrypoint": "assets/my_boot_service.py", + "entrypoint": "my_boot_service.py", "classname": "MyBootService", "intent_filters": [ { @@ -99,7 +97,7 @@ Services that subscribe to `"boot_completed"` are started automatically during s ## Icon -The icon is a [simple 64x64 pixel PNG image](https://github.com/MicroPythonOS/MicroPythonOS/blob/main/internal_filesystem/builtin/apps/com.micropythonos.launcher/res/mipmap-mdpi/icon_64x64.png), which you can create with any tool, such as GIMP. +The icon is a simple 64x64 pixel PNG image named `icon_64x64.png` in the app root, which you can create with any tool, such as GIMP. It's recommended to keep it as small as possible by setting compression level to 9 and not storing any metadata such as background color, resolution, creation time, comments, Exif data, XMP data, thumbnail or color profile. @@ -111,9 +109,7 @@ The app can be installed by copying the top-level folder `com.micropythonos.hell ### On Desktop -You probably already have a clone of the [internal_filesystem](https://github.com/MicroPythonOS/MicroPythonOS/tree/main/internal_filesystem) that you're using to [run MicroPythonOS on desktop](../os-development/running-on-desktop.md). - -Just copy or move your the top-level folder `com.micropythonos.helloworld/` (and its contents) to `internal_filesystem/apps/` and you're good to go! +If you are [running MicroPythonOS on desktop](../os-development/running-on-desktop.md) from a source checkout, copy or move the top-level folder `com.micropythonos.helloworld/` (and its contents) to `internal_filesystem/apps/` and you're good to go. ### On ESP32 diff --git a/docs/architecture/filesystem.md b/docs/architecture/filesystem.md index da66dd1..48fc7a6 100644 --- a/docs/architecture/filesystem.md +++ b/docs/architecture/filesystem.md @@ -5,7 +5,7 @@ MicroPythonOS uses a structured filesystem to organize apps, data, and resources - **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`. - - **apps/**: See [Built-in Apps](../apps/built-in-apps.md). + - **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 diff --git a/docs/frameworks/app-manager.md b/docs/frameworks/app-manager.md index 722cb9b..4c8954f 100644 --- a/docs/frameworks/app-manager.md +++ b/docs/frameworks/app-manager.md @@ -121,15 +121,14 @@ Each app must have this directory structure: ``` apps/com.example.myapp/ -├── META-INF/ -│ └── MANIFEST.JSON # App metadata -├── assets/ -│ ├── main.py # Main activity entry point -│ ├── icon.png # App icon -│ └── ... # Other assets -└── ... +├── MANIFEST.JSON # App metadata +├── icon_64x64.png # App icon +├── main.py # Main activity entry point +└── ... # Other files and (optional) subfolders ``` +This flat layout keeps `MANIFEST.JSON` and `icon_64x64.png` at the app root. Subfolders are still allowed for larger apps, but remember that **each directory uses roughly 8 KiB of storage in LittleFS**, so use them sparingly on device. + The `MANIFEST.JSON` file contains app metadata: ```json @@ -138,10 +137,15 @@ The `MANIFEST.JSON` file contains app metadata: "name": "My App", "version": "1.0.0", "description": "A sample app", - "main_launcher_activity": { - "entrypoint": "assets/main.py", - "classname": "Main" - } + "activities": [ + { + "entrypoint": "main.py", + "classname": "Main", + "intent_filters": [ + { "action": "main", "category": "launcher" } + ] + } + ] } ``` @@ -339,9 +343,9 @@ from mpos import AppManager # Execute a script file success = AppManager.execute_script( - script_source="assets/main.py", + script_source="main.py", classname="Main", - cwd="apps/com.example.myapp/assets/" + cwd="apps/com.example.myapp/" ) ``` @@ -548,7 +552,7 @@ Start an app by fullname. Execute a Python script with proper environment. - **Parameters:** - - `script_source` (str): Script path used to derive module name (e.g., `assets/main.py` -> `main`) + - `script_source` (str): Script path used to derive module name (e.g., `main.py` -> `main`) - `classname` (str): Name of main activity class to instantiate - `cwd` (str, optional): Working directory to add to sys.path diff --git a/docs/frameworks/notification-manager.md b/docs/frameworks/notification-manager.md new file mode 100644 index 0000000..6911201 --- /dev/null +++ b/docs/frameworks/notification-manager.md @@ -0,0 +1,184 @@ +# NotificationManager + +`NotificationManager` is a system-wide framework for posting, displaying, and dispatching notifications. It is inspired by Android's notification system, but tailored for resource-constrained devices. + +Notifications appear in the top notification bar and in the pull-down drawer. Tapping a notification dispatches its attached `Intent`, which can open an activity or start an app. + +## Overview + +- Apps post `Notification` objects through `NotificationManager.notify()`. +- The system UI listens for changes and updates the notification bar and drawer automatically. +- Notifications are persisted to storage (with a debounced write) so they survive reboots. +- Notifications can be cancelled individually, in bulk, or automatically when tapped. + +## Basic Usage + +```python +from mpos import NotificationManager, Notification, Intent + +notification = Notification( + notification_id="myapp.event", + icon=lv.SYMBOL.BELL, + title="Event", + text="Something happened!", + intent=Intent(action="main", app_fullname="com.example.myapp"), + auto_cancel=True, +) + +NotificationManager.notify(notification) +``` + +To remove a notification: + +```python +NotificationManager.cancel("myapp.event") +``` + +## Notification Object + +`Notification` is a plain data object that describes a single notification. + +### Constructor Arguments + +| Argument | Type | Description | +|----------|------|-------------| +| `notification_id` | `str` | Unique identifier. Also accepts `uniqueidString` for compatibility. Required. | +| `icon` | `str` or `lv.image_dsc_t` | Icon shown in the bar/drawer. Can be an `lv.SYMBOL.*` string, another string, or an LVGL image descriptor. | +| `title` | `str` | Short title. | +| `text` | `str` | Longer body text. | +| `priority` | `int` | One of `Notification.PRIORITY_MIN`, `PRIORITY_LOW`, `PRIORITY_DEFAULT`, `PRIORITY_HIGH`, `PRIORITY_MAX`. Higher priority notifications are shown first. | +| `intent` | `Intent` | `Intent` to dispatch when the user taps the notification. | +| `auto_cancel` | `bool` | If `True` (default), the notification is cancelled automatically after it is tapped. | +| `app_fullname` | `str` | App that owns the notification. Used as a fallback target if the intent has no explicit target. | + +### Priority Levels + +```python +Notification.PRIORITY_MIN = -1 +Notification.PRIORITY_LOW = 0 +Notification.PRIORITY_DEFAULT = 1 +Notification.PRIORITY_HIGH = 2 +Notification.PRIORITY_MAX = 3 +``` + +The drawer sorts notifications by priority, then by most recent update time. + +## NotificationManager API + +All methods are class methods. `NotificationManager` initializes itself lazily on first use. + +### `notify(notification)` + +Post or update a notification. + +- If a notification with the same ID already exists, its content is updated in place without a new persistence flash. +- If it is new, it is added, the list is trimmed to `MAX_NOTIFICATIONS` (20), and persistence is scheduled. + +```python +NotificationManager.notify(Notification( + notification_id="osupdate.available", + icon=lv.SYMBOL.DOWNLOAD, + title="Update available", + text="MicroPythonOS 1.2.3 is ready to install", + priority=Notification.PRIORITY_HIGH, +)) +``` + +### `cancel(notification_id)` + +Remove a single notification. Writes immediately so the notification does not reappear after reboot. + +```python +NotificationManager.cancel("osupdate.available") +``` + +### `cancel_all()` + +Remove all notifications. + +```python +NotificationManager.cancel_all() +``` + +### `get_notifications()` + +Return a sorted list of active notifications, highest priority first. + +```python +for n in NotificationManager.get_notifications(): + print(n.title, n.text) +``` + +### `get_notification(notification_id)` + +Return a single notification by ID, or `None` if it doesn't exist. + +```python +n = NotificationManager.get_notification("osupdate.available") +``` + +### `trigger(notification_id_or_object)` + +Dispatch the notification's intent. This is what the system calls when the user taps a notification in the drawer. + +- If the intent has an explicit `activity_class` or `action`, `ActivityNavigator.startActivity()` is used. +- Otherwise, the owning `app_fullname` is started via `AppManager.start_app()`. +- If `auto_cancel` is enabled and the dispatch succeeds, the notification is cancelled. + +```python +NotificationManager.trigger("osupdate.available") +``` + +### `register_listener(callback, notify_immediately=True)` / `unregister_listener(callback)` + +Register a function to be called whenever notifications change. The top menu/drawer uses this to refresh the UI. + +```python +def on_notifications_changed(): + print("Notifications updated") + +NotificationManager.register_listener(on_notifications_changed) +``` + +## Complete Example + +```python +import lvgl as lv +from mpos import Activity, NotificationManager, Notification, Intent + +class MyApp(Activity): + + def show_notification(self): + intent = Intent(action="main", app_fullname="com.example.myapp") + notification = Notification( + notification_id="com.example.myapp.done", + icon=lv.SYMBOL.OK, + title="Done", + text="The operation finished successfully.", + intent=intent, + auto_cancel=True, + app_fullname="com.example.myapp", + ) + NotificationManager.notify(notification) +``` + +## Best Practices + +### Do's + +✅ Use a stable, unique `notification_id` so the same event doesn't create duplicate notifications +✅ Set a meaningful `app_fullname` so tapping the notification can fall back to launching your app +✅ Cancel notifications when they are no longer relevant +✅ Use `auto_cancel=True` for one-shot notifications that should disappear after being tapped + +### Don'ts + +❌ Don't rely on `lv.image_dsc_t` icons surviving a reboot — only string icons are persisted +❌ Don't post large amounts of text; storage and display space are limited +❌ Don't use more than `MAX_NOTIFICATIONS` (20) active notifications + +## See Also + +- [Service](service.md) — Background services that often post notifications at boot +- [SettingActivity](setting-activity.md) — Per-app settings storage, useful with notification preferences +- [App Lifecycle](../apps/app-lifecycle.md) — Foreground handling when notifications launch your app diff --git a/docs/frameworks/service.md b/docs/frameworks/service.md index 50e1809..9562d9a 100644 --- a/docs/frameworks/service.md +++ b/docs/frameworks/service.md @@ -1,6 +1,6 @@ # Service -MicroPythonOS provides a **Service** framework for background operations that run independently of the UI, inspired by Android's Service model. Services are ideal for WiFi auto-connect, web server startup, async REPL tasks, periodic update checks, and similar boot-time or long-running background work. +MicroPythonOS provides a **Service** framework for background operations that run independently of the UI, inspired by Android's Service model. Services are ideal for WiFi auto-connect, web server startup, async REPL tasks, periodic update checks, posting notifications, and similar boot-time or long-running background work. ## Overview @@ -8,7 +8,7 @@ Services differ from Activities in key ways: - **No user interface** — A Service has no screen, no LVGL objects, and no lifecycle tied to visibility. - **Independent lifecycle** — Services are created, started, and destroyed by the system without user interaction. -- **Boot-time execution** — Services can subscribe to the `boot_completed` intent to run automatically at system startup. +- **Boot-time execution** — Services can subscribe to the `boot_completed` intent to run automatically at system startup. This is the primary way to register a "start at boot" receiver. - **Long-running** — A Service can run indefinitely (e.g., polling for updates) or terminate after completing a task. ``` @@ -76,7 +76,7 @@ Services can be registered in two ways: ### 1. Programmatic Registration (System Services) -System services register themselves directly with `AppManager.register_service()` at module level: +System services register themselves directly with `AppManager.register_service()` at module level. This is how the OS itself registers receivers for the `boot_completed` intent. ```python from mpos import Service @@ -100,9 +100,11 @@ Parameters for `register_service()`: - `service_cls` (type) — The Service subclass. - `fullname` (str, optional) — App fullname to associate with the service. Defaults to `None`. +This pattern is the "boot service receiver" mechanism: any `Service` class registered for `boot_completed` will be instantiated and started once the system has finished launching the launcher. + ### 2. Manifest-Declared Services (App Services) -Apps declare services in their `META-INF/MANIFEST.JSON` under a `"services"` array: +Apps declare services in their `MANIFEST.JSON` under a `"services"` array: ```json { @@ -111,7 +113,7 @@ Apps declare services in their `META-INF/MANIFEST.JSON` under a `"services"` arr "version": "0.1.5", "activities": [ { - "entrypoint": "assets/osupdate.py", + "entrypoint": "osupdate.py", "classname": "OSUpdate", "intent_filters": [ { "action": "main", "category": "launcher" } @@ -120,7 +122,7 @@ Apps declare services in their `META-INF/MANIFEST.JSON` under a `"services"` arr ], "services": [ { - "entrypoint": "assets/osupdate_boot_service.py", + "entrypoint": "osupdate_boot_service.py", "classname": "OSUpdateService", "intent_filters": [ { "action": "boot_completed" } @@ -141,7 +143,7 @@ Each service entry requires: The corresponding service code: ```python -# assets/osupdate_boot_service.py +# osupdate_boot_service.py from mpos import Service, ConnectivityManager, TaskManager class OSUpdateService(Service): @@ -169,14 +171,16 @@ class OSUpdateService(Service): ## Boot Services -Services that subscribe to the `boot_completed` intent are started automatically during system boot. The current boot-time service set includes: +Services that subscribe to the `boot_completed` intent are started automatically during system boot. This applies both to system services registered programmatically with `AppManager.register_service()` and to services declared in an app's `MANIFEST.JSON`. + +The current boot-time service set includes: | Service | App | Purpose | |---------|-----|---------| | `WifiBootService` | `com.micropythonos.system` | Auto-connects WiFi in a background thread | | `WebServerBootService` | `com.micropythonos.system` | Starts the HTTP web server if enabled | | `AIOReplService` | `com.micropythonos.system` | Starts the asyncio REPL task for interactive debugging | -| `OSUpdateService` | `com.micropythonos.osupdate` | Periodically checks network connectivity for OTA updates | +| `OSUpdateService` | `com.micropythonos.osupdate` | Periodically checks for OTA updates | ### Boot Order @@ -188,7 +192,7 @@ Services are started after the launcher is displayed, in this sequence: 4. **Boot services are started** — all `boot_completed` services are instantiated and `onStart()` is called 5. `TaskManager.start()` runs the asyncio event loop -The order among boot services is non-deterministic. +The order among boot services is non-deterministic. Each service is isolated: a failure in one service does not prevent other services from starting. ## Complete Example @@ -221,14 +225,10 @@ AppManager.register_service("boot_completed", BootLogger, fullname="com.example. ``` com.example.myapp/ -├── META-INF/ -│ └── MANIFEST.JSON # Contains "services" array -├── assets/ -│ ├── main.py # Activity entry point -│ └── my_service.py # Service entry point -└── res/ - └── mipmap-mdpi/ - └── icon_64x64.png +├── MANIFEST.JSON # Contains "services" array +├── icon_64x64.png +├── main.py # Activity entry point +└── my_service.py # Service entry point ``` ## Service vs. Activity @@ -240,6 +240,7 @@ com.example.myapp/ | Persistence | Exists only while in the activity stack | Can run indefinitely after boot | | Foreground state | Tracked via `has_foreground()` | Not applicable | | Thread usage | UI runs on main LVGL thread | May spawn threads or use asyncio tasks | +| Typical use | Interactive screens | Boot receivers, background work, notifications | ## Best Practices @@ -264,4 +265,5 @@ com.example.myapp/ - [App Lifecycle](../apps/app-lifecycle.md) — Activity lifecycle for UI apps - [Boot Sequence](../architecture/boot-sequence.md) — Detailed boot flow - [TaskManager](../frameworks/task-manager.md) — Async task management for services +- [NotificationManager](notification-manager.md) — Posting notifications from services - [Creating Apps](../apps/creating-apps.md) — How to add services to your app's manifest diff --git a/docs/frameworks/setting-activity.md b/docs/frameworks/setting-activity.md index 74aa462..ec7f4ca 100644 --- a/docs/frameworks/setting-activity.md +++ b/docs/frameworks/setting-activity.md @@ -46,6 +46,7 @@ Each setting is defined as a dictionary with the following properties: - **`min`** (int): Minimum value for `"slider"` UI (default: `0`) - **`max`** (int): Maximum value for `"slider"` UI (default: `100`) - **`activity_class`** (class): Custom Activity class for `"activity"` UI type +- **`note`** (string): Optional short informational text shown below the input widget (useful for context, caveats, or privacy nudges) - **`value_label`** (widget): Internal reference to the value label (set by SettingsActivity) - **`cont`** (widget): Internal reference to the container (set by SettingsActivity) @@ -197,6 +198,26 @@ setting = { - The custom Activity receives the setting and prefs via Intent extras - Must call `self.finish()` to return to the previous screen +## Adding an informational note + +Use the optional `note` field to display a short explanation below the input widget. This is useful for privacy nudges, hardware caveats, or any context that doesn't fit in the title or placeholder. + +**Example:** +```python +setting = { + "title": "Wallet Type", + "key": "wallet_type", + "ui": "radiobuttons", + "ui_options": [ + ("On-chain xpub", "xpub"), + ("Single address", "address") + ], + "note": "Tip: reusing one address erodes on-chain privacy." +} +``` + +The note is rendered as small, muted, wrapping text below the radio buttons, dropdown, slider, or textarea and above the Cancel/Save row. Settings without a `note` look identical to before. + ## Callbacks and Advanced Features ### changed_callback diff --git a/docs/frameworks/settings-activity.md b/docs/frameworks/settings-activity.md index 23d6ef0..36e5fae 100644 --- a/docs/frameworks/settings-activity.md +++ b/docs/frameworks/settings-activity.md @@ -44,6 +44,7 @@ class MyApp(Activity): - **`min`** (int): Minimum value for `"slider"` UI (default: `0`) - **`max`** (int): Maximum value for `"slider"` UI (default: `100`) - **`activity_class`** (class): Custom Activity class for `"activity"` UI type +- **`note`** (string): Optional short informational text shown when editing the individual setting (see [SettingActivity](setting-activity.md#adding-an-informational-note)) - **`value_label`** (widget): Internal reference to the value label (set by SettingsActivity) - **`cont`** (widget): Internal reference to the container (set by SettingsActivity) diff --git a/docs/getting-started/running.md b/docs/getting-started/running.md index 1d9173f..f1d1597 100644 --- a/docs/getting-started/running.md +++ b/docs/getting-started/running.md @@ -12,7 +12,11 @@ Just use the [WebSerial installer at install.micropythonos.com](https://install. For advanced usage, such as installing development builds without any files, see [Installing on ESP32](../os-development/installing-on-esp32.md). -{!os-development/running-on-desktop.md!} +## Running on desktop + +MicroPythonOS desktop builds do not require a full git clone. A single pre-built binary contains the frozen filesystem, so you can download it, make it executable, and run it. + +For detailed steps, see [Running on Desktop](../os-development/running-on-desktop.md). ## Next Steps diff --git a/docs/os-development/running-on-desktop.md b/docs/os-development/running-on-desktop.md index 402b800..f980408 100644 --- a/docs/os-development/running-on-desktop.md +++ b/docs/os-development/running-on-desktop.md @@ -1,36 +1,39 @@ ## Running on desktop -1. **Make sure you have the `internal_filesystem/` folder** +Desktop builds of MicroPythonOS can run without a local source checkout. The `internal_filesystem/` is frozen into the binary at build time, so a single pre-built executable is enough to try the OS. - If you built from source, you will already have a local clone that contains it. +### Download a pre-built binary - If you'll be using a pre-built binary, you can download the accompanying "Source code" zip from the [releases page](https://github.com/MicroPythonOS/MicroPythonOS/releases) or the [main branch](https://github.com/MicroPythonOS/MicroPythonOS/archive/refs/heads/main.zip) and extract it. +1. Go to the [releases page](https://github.com/MicroPythonOS/MicroPythonOS/releases). +2. Download the binary for your platform: + - Linux / WSL2 on Windows: `lvgl_micropy_unix` + - macOS (Apple Silicon or Intel): `lvgl_micropy_macOS` +3. Make it executable: -2. **Make sure you have the software** +``` +chmod +x lvgl_micropy_unix +``` - The easiest is to downloaded a pre-built binary (like `MicroPythonOS_arm64_macOS_0.9.0.bin` or `MicroPythonOS_x64_linux_0.9.0.elf`) from the [releases page](https://github.com/MicroPythonOS/MicroPythonOS/releases). - - After downloading it, put it in the right location like this: +4. Place it where `scripts/run_desktop.sh` expects it. The script looks for: + - `lvgl_micropython/build/lvgl_micropy_unix` on Linux + - `lvgl_micropython/build/lvgl_micropy_macOS` on macOS -
-    ```
-    mkdir -p lvgl_micropython/build # do this from the top level folder MicroPythonOS from step 1
-    cp /Users/yourname/MicroPythonOS_amd64_macOS_0.7.1.bin lvgl_micropython/build/lvgl_micropy_macOS # for macOS
-    cp /home/yourname/MicroPythonOS_amd64_linux_0.7.1.elf lvgl_micropython/build/lvgl_micropy_unix # for Linux or WSL2 on Windows 11
-    ``` 
-    
+You can create that folder and copy the binary there: - Alternatively, instead of downloading a pre-built binary, you can [build it from source](../os-development/compiling.md), and then you will have the built binary in `lvgl_micropython/build/lvgl_micropy_XXX` where XXX is unix or macOS. +``` +mkdir -p lvgl_micropython/build +cp /path/to/downloaded/lvgl_micropy_unix lvgl_micropython/build/lvgl_micropy_unix +``` -3. **Start the software:** - - You're now ready to run it with: +5. Run it: -
-    ```
-    ./scripts/run_desktop.sh
-    ```
-    
+``` +./scripts/run_desktop.sh +``` + +### Build from source + +If you want to modify the OS itself or run the very latest code, you can [build it from source](compiling.md). The built binary will already be in `lvgl_micropython/build/lvgl_micropy_XXX` where `XXX` is `unix` or `macOS`. ### Notes on MacOS @@ -43,9 +46,7 @@ If you get an error about the code being unsigned, then allow it like this: ## Making Changes on Desktop -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. - -When you run `./scripts/run_desktop.sh`, the OS runs the MicroPythonOS scripts **directly from `internal_filesystem/`**. This means: +If you do have a source checkout, you can still run the OS directly from `internal_filesystem/`. When you run `./scripts/run_desktop.sh`, the OS runs the MicroPythonOS scripts **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 @@ -65,4 +66,3 @@ Once you've tested your changes on desktop and they work correctly, or you're do {!os-development/installing-on-esp32.md!} - diff --git a/docs/other/merge-checklist.md b/docs/other/merge-checklist.md index ad73557..7a23c10 100644 --- a/docs/other/merge-checklist.md +++ b/docs/other/merge-checklist.md @@ -3,7 +3,7 @@ Before merging a pull request, we should consider the following: Making sure to update related things: - does the "Future release (next version)" section at the top of [CHANGELOG.md](https://github.com/MicroPythonOS/MicroPythonOS/blob/main/CHANGELOG.md) need to be expanded? -- does an App's version number (META-INF/MANIFEST.JSON) need incrementing? Normally only when you modify an App. +- does an App's version number (MANIFEST.JSON) need incrementing? Normally only when you modify an App. - does the [documentation](https://GitHub.com/MicroPythonOS/docs) need updating? Usually when you modify or add a Framework, but can also be for other things. Always good! - does [MAINTAINERS.md](https://github.com/MicroPythonOS/MicroPythonOS/blob/main/MAINTAINERS.md) need to be updated? Usually only when adding a new board that you'll be maintaining. diff --git a/docs/other/release-checklist.md b/docs/other/release-checklist.md index 456e2ef..dd8a845 100644 --- a/docs/other/release-checklist.md +++ b/docs/other/release-checklist.md @@ -8,7 +8,7 @@ Follow these steps to create a new release of MicroPythonOS. - Update version numbers for modified apps: ``` -git diff --stat 0.6.0 internal_filesystem/ # Check changes since last release, make sure each app change is accompanied by a META-INF/MANIFEST.json change +git diff --stat 0.6.0 internal_filesystem/ # Check changes since last release, make sure each app change is accompanied by a MANIFEST.json change ``` **Update Changelog**: diff --git a/mkdocs.yml b/mkdocs.yml index 202f495..491239e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -62,6 +62,7 @@ nav: - DownloadManager: frameworks/download-manager.md - InputManager: frameworks/input-manager.md - LightsManager: frameworks/lights-manager.md + - NotificationManager: frameworks/notification-manager.md - NumberFormat: frameworks/number-format.md - Preferences: frameworks/preferences.md - SensorManager: frameworks/sensor-manager.md From be5a86fa353f5d8756421195827c0d6539272a7f Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Sat, 20 Jun 2026 14:50:12 +0200 Subject: [PATCH 097/110] Update docs --- docs/apps/app-lifecycle.md | 33 ++++++++++++++++- docs/apps/appstore.md | 4 +++ docs/frameworks/font-manager.md | 53 +++++++++++++++++++++------- docs/other/supported-file-formats.md | 31 ++++++++++++++++ mkdocs.yml | 1 + 5 files changed, 108 insertions(+), 14 deletions(-) create mode 100644 docs/other/supported-file-formats.md diff --git a/docs/apps/app-lifecycle.md b/docs/apps/app-lifecycle.md index c970a16..fb52cf9 100644 --- a/docs/apps/app-lifecycle.md +++ b/docs/apps/app-lifecycle.md @@ -140,6 +140,32 @@ def onPause(self, screen): **Important:** Call `super().onPause(screen)` to properly track foreground state. +### onBackPressed(screen) + +Called when the user performs the back/close gesture (e.g. swiping from the left edge) **before** the activity is paused. This is the right place to ask the user for confirmation when there is unsaved work. + +Return `True` to consume the event and keep the activity in the foreground. In that case the activity is responsible for calling `finish()` later when it is ready to close. Return `False` (the default) to let the framework finish the activity normally. + +```python +def onBackPressed(self, screen): + if self._has_unsaved_changes(): + # Show a dialog; return True so the activity stays alive + self._show_exit_confirm() + return True + return False +``` + +In the dialog callback, call `self.finish()` to actually close the activity: + +```python +def _on_exit_confirmed(self, dialog): + dialog.close() + self._save_changes() + self.finish() +``` + +**Important:** `finish()` does **not** call `onBackPressed()` again, so your dialog callbacks can safely call it. + ### onStop(screen) Called when the activity is no longer visible (fully covered by another activity). @@ -189,7 +215,7 @@ Activity Stack: 1. **Starting a new activity:** Current activity receives `onPause()` → `onStop()`, new activity receives `onCreate()` → `onStart()` → `onResume()` -2. **Going back:** Current activity receives `onPause()` → `onStop()` → `onDestroy()`, previous activity receives `onResume()` +2. **Going back:** Framework first calls `onBackPressed()`. If it returns `True`, the activity stays foreground and must call `finish()` itself when ready. If it returns `False`, the current activity receives `onPause()` → `onStop()` → `onDestroy()`, and the previous activity receives `onResume()` ## Starting Activities @@ -404,6 +430,8 @@ class NetworkAwareActivity(Activity): ✅ Check `has_foreground()` before updating UI from async operations ✅ Use `setContentView()` at the end of `onCreate()` ✅ Clean up resources in `onDestroy()` +✅ Use `onBackPressed()` to ask before discarding unsaved changes + ### Don'ts @@ -412,6 +440,8 @@ class NetworkAwareActivity(Activity): ❌ Don't update UI from background threads without `update_ui_threadsafe_if_foreground()` ❌ Don't assume the activity is still visible after async operations ❌ Don't store references to LVGL objects after `onDestroy()` +❌ Don't show back-navigation confirmation dialogs in `onPause()` - use `onBackPressed()` instead + ## Services — Background Components @@ -460,6 +490,7 @@ Services are declared either programmatically (system services) or via `"service | `onStart(screen)` | `onStart()` | MicroPythonOS passes screen | | `onResume(screen)` | `onResume()` | MicroPythonOS passes screen | | `onPause(screen)` | `onPause()` | MicroPythonOS passes screen | +| `onBackPressed(screen)` | `onBackPressed()` | MicroPythonOS passes screen; called before `onPause()` on back gesture | | `onStop(screen)` | `onStop()` | MicroPythonOS passes screen | | `onDestroy(screen)` | `onDestroy()` | MicroPythonOS passes screen | | `finish()` | `finish()` | Same purpose | diff --git a/docs/apps/appstore.md b/docs/apps/appstore.md index 55443f3..f3c859e 100644 --- a/docs/apps/appstore.md +++ b/docs/apps/appstore.md @@ -11,6 +11,10 @@ App discovery is currently done by downloading the app list from [apps.micropyth - **Image Viewer**: Displays images stored in `/data/images/`. - **IMU**: Visualize data from the Intertial Measurement Unit, also known as the accellerometer. +## Image Viewer + +The **Image Viewer** app displays images stored in `/data/images/`. See [Supported File Formats](../other/supported-file-formats.md) for the list of image formats the OS can decode. + ## Screenshots
diff --git a/docs/frameworks/font-manager.md b/docs/frameworks/font-manager.md index ab4c2a1..58759f2 100644 --- a/docs/frameworks/font-manager.md +++ b/docs/frameworks/font-manager.md @@ -1,13 +1,13 @@ # FontManager -FontManager is a singleton framework for loading, caching, and composing LVGL fonts — including built-in bitmap fonts, TrueType fonts, and emoji image fonts. It uses LVGL's imgfont fallback mechanism to render 20×20 emoji PNGs inline with text, with nearest-neighbour scaling to match any font size. +FontManager is a singleton framework for loading, caching, and composing LVGL fonts — including built-in bitmap fonts, TrueType fonts, and emoji image fonts. It uses LVGL's imgfont fallback mechanism to render 32×32 emoji PNGs inline with text, with nearest-neighbour scaling to match any font size. ## Overview FontManager centralizes all font concerns in a single class: - **Unified API** - One call (`getFont`) to get any font, with or without emoji support -- **Emoji Compositing** - Transparently layers 20×20 emoji PNGs via LVGL's imgfont fallback, with nearest-neighbour scaling to match any font size +- **Emoji Compositing** - Transparently layers 32×32 emoji PNGs via LVGL's imgfont fallback, with nearest-neighbour scaling to match any font size - **Lazy Caching** - Fonts and scaled image descriptors are cached on first use; no redundant work on subsequent calls - **Android-Inspired** - Follows the same singleton/class-method pattern as other MicroPythonOS frameworks @@ -29,9 +29,13 @@ ttf_font = FontManager.getFont(size=42, ttf="M:apps/com.myapp/assets/MyFont.ttf" for info in FontManager.listFonts(): print(info["name"], info["size"]) -# Get all available emoji codepoints +# Get all available emoji codepoints (base codepoints only) for cp in FontManager.getEmojiCodepoints(): print(hex(cp)) + +# Get all available emoji strings (full sequences, including flag pairs) +for s in FontManager.getEmojiStrings(): + print(s) ``` ## Architecture @@ -56,9 +60,9 @@ Emoji PNGs are stored in `builtin/res/emojis/`: | Directory | Used for | |-----------|----------| -| `20x20/` | All fonts — emoji are rendered at 20×20 px and nearest-neighbour scaled up or down by LVGL as needed | +| `32x32/` | All fonts — emoji are rendered at 32×32 px and nearest-neighbour scaled up or down by LVGL as needed | -`_imgfont_path_cb` receives the rendering font's pixel height and returns the 20×20 emoji source. LVGL's software renderer performs nearest-neighbour scaling to fit the target font size. +`_imgfont_path_cb` receives the rendering font's pixel height and returns the 32×32 emoji source. LVGL's software renderer performs nearest-neighbour scaling to fit the target font size. ### Caching layers @@ -66,7 +70,8 @@ Emoji PNGs are stored in `builtin/res/emojis/`: |-------|-----|-------| | `_composed_font_cache` | `(font_id, emoji_size)` | Composed imgfont object | | `_ttf_font_cache` | `(path, size)` | `lv.tiny_ttf_create_file` result | -| `_emoji_maps` | `dir_name` | `{codepoint: src_path}` dict | +| `_emoji_map` | (none) | `{codepoint: src_path}` dict | +| `_emoji_strings` | (none) | Sorted list of complete emoji strings | | `_imgfont_scaled_src_cache` | `(src, target_height)` | Scaled `lv.image_dsc_t` or original src | | `_imgfont_source_size_cache` | `src` | `(width, height)` tuple | | `_imgfont_empty_src_cache` | `target_height` | 1×h transparent `lv.image_dsc_t` | @@ -128,7 +133,9 @@ for info in FontManager.listFonts(emojis=True): ### `getEmojiCodepoints()` -Return a sorted list of all available emoji codepoints. +Return a sorted list of the base emoji codepoints available in the emoji map. + +For multi-codepoint emoji such as flag sequences (e.g. `"🇸🇻"`), only the first codepoint is returned. Use `getEmojiStrings()` when you need complete, renderable emoji sequences. **Returns:** list of int @@ -143,6 +150,25 @@ for cp in FontManager.getEmojiCodepoints(): --- +### `getEmojiStrings()` + +Return a sorted list of all available, complete emoji strings. + +Unlike `getEmojiCodepoints()`, this returns full sequences: flag emoji include both regional indicators, and emoji with variation selectors keep their trailing selector. This is the preferred API for building a visual list of every supported emoji. + +**Returns:** list of str + +**Example:** + +```python +from mpos import FontManager + +for s in FontManager.getEmojiStrings(): + print(s) +``` + +--- + ### `normalizeEmojiText(text)` Strip Unicode variation selectors (U+FE0E text selector, U+FE0F emoji selector) from a string. Useful before storing or comparing text that may have been pasted from a source that appends these codepoints. @@ -167,20 +193,21 @@ To keep firmware image size small, the OS bundles ~50 of the most frequently-use ``` builtin/res/emojis/ -└── 20x20/ # Pre-rendered at 20×20 px +└── 32x32/ # Pre-rendered at 32×32 px ├── 1F600.png - ├── 263A.png + ├── 1F3CE-FE0F.png + ├── 1F1F8-1F1FB.png └── ... ``` -Files are named by their Unicode codepoint in uppercase hex (e.g. `1F600.png` for 😀). FontManager scans the directory at runtime and builds a `{codepoint: path}` map. +Files are named by their Unicode codepoint(s) in uppercase hex, with multiple codepoints joined by `-` (e.g. `1F600.png` for 😀, `1F3CE-FE0F.png` for 🏎️, `1F1F8-1F1FB.png` for 🇸🇻). FontManager scans the directory at runtime and builds a `{codepoint: path}` map. ### Adding new emoji -1. Add a PNG named `.png` to `20x20/`: +1. Add a PNG named `.png` to `32x32/`: ```bash -cp original.png 20x20/CODEPOINT.png +cp original.png 32x32/CODEPOINT.png ``` 2. The new emoji will be picked up automatically on next boot — no code changes needed. @@ -196,7 +223,7 @@ internal_filesystem/ ├── lib/mpos/ui/ │ └── font_manager.py # FontManager class └── builtin/res/emojis/ - └── 20x20/ # 20×20 px emoji PNGs + └── 32x32/ # 32×32 px emoji PNGs ``` ## Related Frameworks diff --git a/docs/other/supported-file-formats.md b/docs/other/supported-file-formats.md new file mode 100644 index 0000000..93cb966 --- /dev/null +++ b/docs/other/supported-file-formats.md @@ -0,0 +1,31 @@ +# Supported File Formats + +MicroPythonOS uses LVGL's built-in image decoders and its own audio stack. The formats below are supported out of the box for apps such as the Image Viewer and Music Player. + +## Image formats + +| Format | Extensions | Notes | +|--------|------------|-------| +| PNG | `.png` | Fully supported via LodePNG. | +| Baseline JPEG | `.jpg`, `.jpeg` | Fully supported via TJpgD. | +| Progressive JPEG | `.jpg`, `.jpeg` | **Not supported.** Files will decode as `0x0` and appear blank. | +| RAW | `.raw` | Supported by the Image Viewer app if named as `_x_RGB565.raw` or `_x_GRAY.raw`. | + +## Audio formats + +| Format | Extensions | Notes | +|--------|------------|-------| +| Plain PCM WAV | `.wav` | Standard RIFF/WAVE with `WAVE_FORMAT_PCM` (0x0001) or `WAVE_FORMAT_EXTENSIBLE` (0xFFFE) and 16-bit samples. | +| IMA ADPCM WAV | `.wav` | RIFF/WAVE with `WAVE_FORMAT_ADPCM` (0x0011), decoded by the built-in `adpcm_ima` module. Must have 4 or 16 bits per sample. | + +### JPEG conversion + +The built-in JPEG decoder only supports baseline JPEGs. The file extension is also treated case-sensitively, so `.JPG` or `.JPEG` will **not** be recognised. + +To convert an image to a supported baseline JPEG: + +```bash +convert input.jpg -interlace none output.jpg +``` + +`input.jpg` can be any image format that ImageMagick supports (including PNG), and `-interlace none` forces a non-progressive JPEG. diff --git a/mkdocs.yml b/mkdocs.yml index 491239e..01cd941 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -92,6 +92,7 @@ nav: - Other: - Release Process: other/release-checklist.md - Merge Checklist: other/merge-checklist.md + - Supported File Formats: other/supported-file-formats.md extra: social: - icon: fontawesome/brands/github From 0a5c6306109a83e05ad652139ef18b543c5f0ce3 Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Sun, 21 Jun 2026 08:47:19 +0200 Subject: [PATCH 098/110] Update docs --- docs/apps/built-in-apps.md | 3 +- docs/apps/creating-apps.md | 55 +++++++ docs/architecture/frameworks.md | 27 ++++ docs/architecture/intents.md | 188 +++++++++++++++++++--- docs/frameworks/app-manager.md | 91 +++++++++-- docs/frameworks/file-explorer-activity.md | 133 +++++++++++++++ docs/frameworks/focus.md | 100 ++++++++++++ mkdocs.yml | 2 + 8 files changed, 557 insertions(+), 42 deletions(-) create mode 100644 docs/frameworks/file-explorer-activity.md create mode 100644 docs/frameworks/focus.md diff --git a/docs/apps/built-in-apps.md b/docs/apps/built-in-apps.md index 22255a1..0a13673 100644 --- a/docs/apps/built-in-apps.md +++ b/docs/apps/built-in-apps.md @@ -6,7 +6,8 @@ MicroPythonOS includes essential apps to bootstrap the system, located in [`/bui - **WiFi**: Configures WiFi connections. - **AppStore**: Downloads and installs new apps. - **OSUpdate**: Manages Over-The-Air (OTA) system updates. -- **Settings**: Configuration for MicroPythonOS +- **Settings**: Configuration for MicroPythonOS. +- **File Manager**: Browses the filesystem and opens files with the appropriate viewer. ## Screenshots diff --git a/docs/apps/creating-apps.md b/docs/apps/creating-apps.md index b103560..f8763c3 100644 --- a/docs/apps/creating-apps.md +++ b/docs/apps/creating-apps.md @@ -95,6 +95,61 @@ Each service entry has: Services that subscribe to `"boot_completed"` are started automatically during system boot, after the launcher is displayed. See the [Service documentation](../frameworks/service.md) for details on writing and using services. +## Handling the view action + +Apps can register themselves as file viewers by declaring an `intent_filter` with `action: "view"` and a `pathPattern` list in `MANIFEST.JSON`. This lets other apps (and the built-in `FileExplorerActivity`) open files with your app. + +### Manifest example + +```json +{ + "fullname": "com.example.imageviewer", + "name": "Image Viewer", + "version": "1.0.0", + "activities": [ + { + "entrypoint": "imageview.py", + "classname": "ImageView", + "intent_filters": [ + { "action": "main", "category": "launcher" }, + { "action": "view", "mimeType": "image/*", "pathPattern": [".png", ".jpg", ".jpeg"] } + ] + } + ] +} +``` + +`pathPattern` entries are matched case-insensitively against the file extension. A leading `*` is optional. `mimeType` is recorded for documentation but is not used for matching. + +### Receiving the file + +When the system opens your activity via the `view` action, the file path is available in `intent.data` and also in the `filename` extra: + +```python +from mpos import Activity + +class ImageView(Activity): + def onResume(self, screen): + super().onResume(screen) + path = self.getIntent().extras.get("filename") or self.getIntent().data + if path: + self.open_image(path) +``` + +### Opening a file from your app + +To open a file in whatever app is registered for it, send a `view` intent: + +```python +from mpos import Intent, Activity + +class MyActivity(Activity): + def on_file_click(self, path): + self.startActivity(Intent(action="view", data=path)) +``` + +If multiple apps can handle the file type, MicroPythonOS automatically shows an "Open with" chooser. + ## Icon The icon is a simple 64x64 pixel PNG image named `icon_64x64.png` in the app root, which you can create with any tool, such as GIMP. diff --git a/docs/architecture/frameworks.md b/docs/architecture/frameworks.md index 059dc04..228fd7b 100644 --- a/docs/architecture/frameworks.md +++ b/docs/architecture/frameworks.md @@ -198,6 +198,33 @@ if ConnectivityManager.is_online(): print("Connected to network") ``` +### FileExplorerActivity +Reusable file browser and picker activity. Can browse the filesystem or return selected files to the caller via `startActivityForResult`. + +```python +from mpos import Intent, FileExplorerActivity, Activity + +class MyActivity(Activity): + def pick_file(self): + intent = Intent(action="pick_file") + intent.putExtra("mode", "pick") + intent.putExtra("path_pattern", [".wav"]) + self.startActivityForResult(intent, self.on_file_picked) +``` + +See [FileExplorerActivity](../frameworks/file-explorer-activity.md) for details. + +### Focus Borders +Provides a single helper, `add_focus_border`, for drawing a focus border around any widget. It replaces the duplicated `FOCUSED`/`DEFOCUSED` handlers that used to exist in many apps. + +```python +from mpos import add_focus_border + +add_focus_border(button, width=2) +``` + +See [Focus Borders](../frameworks/focus.md) for details. + ### CameraManager Provides access to camera hardware. diff --git a/docs/architecture/intents.md b/docs/architecture/intents.md index 6023ade..516ca7d 100644 --- a/docs/architecture/intents.md +++ b/docs/architecture/intents.md @@ -80,6 +80,61 @@ class HomeActivity(Activity): # Or launches directly if only one handler ``` +### File-type intents and the view action + +A common implicit intent is the `view` action, which asks the system to open a file with the most appropriate app. Apps declare which file types they can open by adding an `intent_filter` with `action: "view"` and a `pathPattern` list to their manifest. `pathPattern` entries are matched case-insensitively against the file extension; a leading `*` is optional. + +**Example manifest declaring an image viewer:** + +```json +{ + "fullname": "com.example.imageviewer", + "name": "Image Viewer", + "version": "1.0.0", + "activities": [ + { + "entrypoint": "imageview.py", + "classname": "ImageView", + "intent_filters": [ + { "action": "main", "category": "launcher" }, + { "action": "view", "mimeType": "image/*", "pathPattern": [".png", ".jpg", ".jpeg", ".raw"] } + ] + } + ] +} +``` + +**Opening a file from another app:** + +```python +from mpos import Intent, Activity + +class GalleryActivity(Activity): + def on_photo_selected(self, path): + self.startActivity(Intent(action="view", data=path)) +``` + +**Receiving a file in the target app:** + +```python +from mpos import Activity + +class ImageView(Activity): + def onResume(self, screen): + path = self.getIntent().extras.get("filename") or self.getIntent().data + if path: + self.load_image(path) +``` + +When a `view` intent is fired: + +1. `AppManager.resolve_activity()` looks for installed apps whose manifest `pathPattern` matches the file path. +2. If one or more specific handlers match, those handlers are returned. +3. If multiple handlers match, `ChooserActivity` shows an "Open with" dialog. +4. If no specific handler matches, the system falls back to generic handlers registered for `view`, such as the framework's built-in `ViewActivity`. + +The framework `ViewActivity` provides a last-resort preview for unknown files by reading and displaying the first 512 bytes as text. + ## Intent Class Reference ### Constructor @@ -342,13 +397,24 @@ class HomeActivity(Activity): ## AppManager Intent Resolution -The `AppManager` maintains a registry of activities and their associated actions, enabling implicit intent resolution. +The `AppManager` maintains a registry of activities and their associated actions, enabling implicit intent resolution. Resolution works for both programmatically registered handlers and handlers declared in app manifests. **Location:** [`MicroPythonOS/internal_filesystem/lib/mpos/content/app_manager.py`](https://github.com/MicroPythonOS/MicroPythonOS/blob/main/internal_filesystem/lib/mpos/content/app_manager.py) +### HandlerInfo + +`resolve_activity()` returns a list of `HandlerInfo` objects rather than raw activity classes: + +| Attribute | Type | Purpose | +|-----------|------|---------| +| `activity_class` | `type` | The `Activity` subclass that can handle the intent. | +| `app_fullname` | `str` or `None` | The owning app's fullname for manifest-declared handlers, or `None` for framework handlers. | + +Use `handler.activity_class` when inspecting a resolved handler and `handler.app_fullname` when you need to know which installed app provided it. + ### `register_activity(action, activity_cls)` -Register an activity to handle a specific action. +Register an activity programmatically to handle a specific action. This is useful for framework activities and dynamic handlers. **Parameters:** @@ -363,35 +429,58 @@ from mpos import AppManager # Register handlers for SEND action AppManager.register_activity("android.intent.action.SEND", ShareActivity) AppManager.register_activity("android.intent.action.SEND", EmailActivity) +``` -# Register handler for VIEW action -AppManager.register_activity("android.intent.action.VIEW", BrowserActivity) +### Manifest-based file handlers + +Apps declare file-type handlers in `MANIFEST.JSON`. When an implicit intent carries a string `data` payload (usually a file path), MicroPythonOS preferentially returns handlers whose `pathPattern` matches the path. + +```json +{ + "activities": [ + { + "entrypoint": "player.py", + "classname": "Player", + "intent_filters": [ + { "action": "view", "mimeType": "audio/wav", "pathPattern": [".wav"] } + ] + } + ] +} ``` +- `pathPattern` may be a single string or a list of strings. +- Matching is case-insensitive and suffix-based; `"*.wav"` and `".wav"` are equivalent. +- `mimeType` is recorded but is not currently used for matching. + ### `resolve_activity(intent)` -Find all activities that handle an intent's action. +Find all handlers that can handle an intent's action, with file-type preference when `intent.data` is a path. **Parameters:** -- `intent` (Intent): Intent with action attribute +- `intent` (Intent): Intent with `action` attribute -**Returns:** List of Activity classes (empty if no handlers) +**Returns:** List of `HandlerInfo` objects (empty if no handlers) + +**Resolution order:** + +1. If `intent.data` is a string and one or more manifest file handlers match its extension, return only those handlers. +2. If no file handler matches, return all generic handlers registered for the action. +3. If the list contains multiple handlers, the framework shows `ChooserActivity`. **Example:** ```python from mpos import Intent, AppManager -intent = Intent(action="android.intent.action.SEND") +intent = Intent(action="view", data="/data/audio/song.wav") handlers = AppManager.resolve_activity(intent) -if len(handlers) == 0: - print("No handler for SEND action") -elif len(handlers) == 1: - print(f"Single handler: {handlers[0]}") -else: - print(f"Multiple handlers: {handlers}") +for handler in handlers: + print(f"Handler: {handler.activity_class.__name__}") + if handler.app_fullname: + print(f" from app: {handler.app_fullname}") ``` ### `query_intent_activities(intent)` @@ -400,16 +489,16 @@ Android-compatible alias for `resolve_activity()`. Identical behavior. **Parameters:** -- `intent` (Intent): Intent with action attribute +- `intent` (Intent): Intent object with `action` attribute -**Returns:** List of Activity classes +**Returns:** List of `HandlerInfo` objects **Example:** ```python from mpos import Intent, AppManager -intent = Intent(action="android.intent.action.SEND") +intent = Intent(action="view", data="/data/notes.txt") handlers = AppManager.query_intent_activities(intent) ``` @@ -548,7 +637,37 @@ class HomeActivity(Activity): --- -### Pattern 5: Method Chaining +### Pattern 5: Open a File with the View Action + +Use the `view` action to open a file in the app that is registered for its type. If more than one app can handle the type, the system shows an "Open with" chooser. + +```python +from mpos import Intent, Activity + +class FileListActivity(Activity): + def on_file_click(self, path): + self.startActivity(Intent(action="view", data=path)) +``` + +In the receiving app, read the file path from `intent.data` or from the `filename` extra: + +```python +class ImageView(Activity): + def onResume(self, screen): + path = self.getIntent().extras.get("filename") or self.getIntent().data + if path: + self.load_image(path) +``` + +**Key Points:** +- The file path is passed in `intent.data`. +- Apps declare supported extensions in `MANIFEST.JSON` with `action: "view"` and `pathPattern`. +- `ChooserActivity` is shown automatically when multiple handlers exist. +- The framework provides a fallback `ViewActivity` for unhandled file types. + +--- + +### Pattern 6: Method Chaining Use fluent API for readable intent construction. @@ -573,7 +692,7 @@ class HomeActivity(Activity): --- -### Pattern 6: Complex Data Passing +### Pattern 7: Complex Data Passing Pass complex objects and references between activities. @@ -797,12 +916,29 @@ item_id = intent.extras["item_id"] # KeyError if missing Register implicit intent handlers during app initialization. +**Programmatic registration** is useful for framework activities and dynamic handlers: + ```python # In app initialization from mpos import AppManager AppManager.register_activity("android.intent.action.SEND", ShareActivity) -AppManager.register_activity("android.intent.action.VIEW", BrowserActivity) +``` + +**Manifest registration** is preferred for file-type handlers. Add the `intent_filters` to `MANIFEST.JSON` so `AppManager.refresh_apps()` discovers the handler automatically: + +```json +{ + "activities": [ + { + "entrypoint": "player.py", + "classname": "Player", + "intent_filters": [ + { "action": "view", "pathPattern": [".wav"] } + ] + } + ] +} ``` --- @@ -852,9 +988,9 @@ MicroPythonOS Intents are inspired by Android's Intent system but simplified for | **Implicit Intents** | ✅ Supported | ✅ Supported | Action-based routing | | **Intent Extras** | ✅ Dict-based | ✅ Bundle-based | MicroPythonOS simpler | | **Intent Flags** | ⚠️ Partial | ✅ Full | Limited flag support | -| **Intent Filters** | ❌ Programmatic only | ✅ In manifest | No manifest-based registration | +| **Intent Filters** | ✅ Manifest + programmatic | ✅ In manifest | File-type filters in `MANIFEST.JSON`, generic handlers via code | | **Categories** | ❌ Not supported | ✅ Supported | Simplified routing | -| **Data Types** | ❌ Not matched | ✅ MIME types | No type filtering | +| **Data Types** | ⚠️ Path patterns | ✅ MIME types | `pathPattern` suffix matching; `mimeType` is stored but not used for matching | | **URI Schemes** | ❌ Not matched | ✅ Supported | No scheme filtering | | **Chooser UI** | ✅ ChooserActivity | ✅ Intent chooser | Custom implementation | | **Result Callbacks** | ✅ Callback-based | ✅ onActivityResult() | Different mechanism | @@ -864,9 +1000,9 @@ MicroPythonOS Intents are inspired by Android's Intent system but simplified for **Simplified Intent Filters:** -- MicroPythonOS uses programmatic registration via `AppManager.register_activity()` -- Android uses manifest-based intent filters -- MicroPythonOS approach is more flexible for dynamic app loading +- MicroPythonOS supports both programmatic registration via `AppManager.register_activity()` and manifest-based file-type filters in `MANIFEST.JSON` +- Android uses manifest-based intent filters exclusively +- MicroPythonOS approach is more flexible for dynamic app loading while still allowing apps to declare file handlers declaratively **Callback-Based Results:** @@ -893,4 +1029,6 @@ The Intent system is implemented across these core files: - [App Lifecycle](../apps/app-lifecycle.md) - Activity lifecycle and Intent basics - [AppManager](../frameworks/app-manager.md) - Intent resolution and activity registration +- [FileExplorerActivity](../frameworks/file-explorer-activity.md) - Browsing files and sending `view` intents +- [Focus Borders](../frameworks/focus.md) - Reusable focus highlighting for intent-driven UIs - [SettingActivity](../frameworks/setting-activity.md) - Intent extras for settings configuration diff --git a/docs/frameworks/app-manager.md b/docs/frameworks/app-manager.md index 4c8954f..05a6b4a 100644 --- a/docs/frameworks/app-manager.md +++ b/docs/frameworks/app-manager.md @@ -12,7 +12,7 @@ AppManager provides: - **App uninstallation** - Removes user-installed apps (preserves built-in apps) - **App launching** - Starts apps with proper environment setup and activity management - **Version management** - Compares versions and detects available updates -- **Intent resolution** - Resolves activities that handle specific intents (Android-inspired) +- **Intent resolution** - Resolves activities that handle specific intents, including manifest-declared file-type handlers (Android-inspired) - **Launcher management** - Finds and restarts the system launcher - **Service management** - Registers, resolves, and starts boot services @@ -250,11 +250,21 @@ AppManager.uninstall_app("com.example.myapp") ## Intent Resolution -AppManager implements Android-inspired intent resolution for activity discovery: +AppManager implements Android-inspired intent resolution for activity discovery. It supports both programmatic registration and manifest-declared file-type handlers. + +### HandlerInfo + +`resolve_activity()` and `query_intent_activities()` return a list of `HandlerInfo` objects: + +```python +class HandlerInfo: + activity_class # Activity subclass that can handle the intent + app_fullname # Owning app fullname, or None for framework handlers +``` ### Registering Activities -Activities register themselves with AppManager to handle specific intents: +Activities can be registered programmatically. This is commonly used by framework components: ```python from mpos import AppManager @@ -267,24 +277,58 @@ class ShareActivity(Activity): AppManager.register_activity("android.intent.action.SEND", ShareActivity) ``` +Apps can also declare handlers in `MANIFEST.JSON` so they are discovered during `refresh_apps()`: + +```json +{ + "activities": [ + { + "entrypoint": "imageview.py", + "classname": "ImageView", + "intent_filters": [ + { "action": "view", "pathPattern": [".png", ".jpg", ".jpeg"] } + ] + } + ] +} +``` + ### Resolving Intents ```python from mpos import AppManager, Intent -# Create an intent +# Generic action: find all registered handlers intent = Intent(action="android.intent.action.SEND") +handlers = AppManager.resolve_activity(intent) + +for handler in handlers: + print(f"Handler: {handler.activity_class.__name__}") + if handler.app_fullname: + print(f" from app: {handler.app_fullname}") +``` + +When `intent.data` is a string path, MicroPythonOS preferentially returns manifest handlers whose `pathPattern` matches the file extension. If no file handler matches, it falls back to generic handlers registered for that action. -# Find all activities that handle this intent -activities = AppManager.resolve_activity(intent) +```python +from mpos import AppManager, Intent -# Or use the Android-like method name -activities = AppManager.query_intent_activities(intent) +# File-type intent: resolved to matching manifest handler(s) +intent = Intent(action="view", data="/data/photo.jpg") +handlers = AppManager.resolve_activity(intent) -for activity_class in activities: - print(f"Activity: {activity_class.__name__}") +if not handlers: + print("No handler for this file") +elif len(handlers) == 1: + print(f"Opening with {handlers[0].activity_class.__name__}") +else: + print(f"Multiple handlers; ChooserActivity will be shown") ``` +### pathPattern matching + +`pathPattern` may be a string or a list of strings. Matching is case-insensitive and suffix-based. A leading `*` is optional, so `".wav"` and `"*.wav"` are equivalent. + ## Launcher Management AppManager provides utilities for launcher discovery and restart: @@ -532,19 +576,20 @@ Check if user app overrides a built-in app. ### App Execution -**`start_app(fullname)`** +**`start_app(fullname, intent=None)`** Start an app by fullname. - **Parameters:** - `fullname` (str): App fullname + - `intent` (Intent, optional): Intent to deliver to the app's main launcher activity - **Returns:** `bool` - `True` if successful - **Behavior:** 1. Sets app as foreground 2. Loads app metadata - 3. Executes main activity script + 3. Executes main activity script with the provided intent 4. Shows/hides top menu bar based on app type **`execute_script(script_source, classname, cwd=None)`** @@ -570,7 +615,7 @@ Execute a Python script with proper environment. **`register_activity(action, activity_cls)`** -Register activity to handle intent action. +Register an activity programmatically to handle an intent action. - **Parameters:** - `action` (str): Intent action (e.g., `"android.intent.action.SEND"`) @@ -578,12 +623,17 @@ Register activity to handle intent action. **`resolve_activity(intent)`** -Find activities that handle intent. +Find handlers that can handle the intent. - **Parameters:** - `intent` (Intent): Intent object with `action` attribute -- **Returns:** `list[type]` - List of Activity classes +- **Returns:** `list[HandlerInfo]` - List of handler descriptors + +- **Behavior:** + 1. If `intent.data` is a string path, searches installed app manifests for matching `pathPattern` filters. + 2. Returns matching manifest handlers if any are found. + 3. Otherwise returns all generic handlers registered for the action. **`query_intent_activities(intent)`** @@ -592,7 +642,16 @@ Same as `resolve_activity()` (Android-like naming). - **Parameters:** - `intent` (Intent): Intent object -- **Returns:** `list[type]` - List of Activity classes +- **Returns:** `list[HandlerInfo]` - List of handler descriptors + +**`get_handler_display_name(activity_class)`** + +Return a human-readable name for a resolved handler class. For manifest handlers this is the owning app's display name; for framework handlers it is the class name. + +- **Parameters:** + - `activity_class` (type): Activity class from a `HandlerInfo` + +- **Returns:** `str` - Display name ### Launcher Management diff --git a/docs/frameworks/file-explorer-activity.md b/docs/frameworks/file-explorer-activity.md new file mode 100644 index 0000000..ae97f75 --- /dev/null +++ b/docs/frameworks/file-explorer-activity.md @@ -0,0 +1,133 @@ +# FileExplorerActivity + +`FileExplorerActivity` is a reusable file browser built into MicroPythonOS. It can browse the local filesystem or let the user pick one or more files, and it integrates with the [view action](../architecture/intents.md#file-type-intents-and-the-view-action) to open files in the appropriate app. + +## Overview + +The activity is registered for the `pick_file` action, so any app can ask the user to choose files without implementing its own file picker. It is also used directly as the built-in **File Manager** app. + +Two modes are supported: + +- **`browse`** (default): navigate directories, open files, rename or delete them. +- **`pick`**: select one or more files and return them to the caller. + +## Launching + +### Pick files with `startActivityForResult` + +```python +from mpos import Intent, Activity + +class MyActivity(Activity): + def _open_file_clicked(self, event): + intent = Intent(action="pick_file") + intent.putExtra("mode", "pick") + intent.putExtra("start_dir", "/data/audio") + intent.putExtra("path_pattern", [".wav"]) + self.startActivityForResult(intent, self._on_file_picked) + + def _on_file_picked(self, result): + if result and result.get("result_code"): + paths = result.get("data", {}).get("paths", []) + for path in paths: + print("Selected:", path) +``` + +### Browse the filesystem + +```python +from mpos import Intent, FileExplorerActivity + +class MyActivity(Activity): + def _browse_files(self): + intent = Intent(activity_class=FileExplorerActivity) + intent.putExtra("start_dir", "/sdcard") + self.startActivity(intent) +``` + +## Intent extras + +| Extra | Type | Default | Description | +|-------|------|---------|-------------| +| `mode` | `str` | `"browse"` | `"browse"` or `"pick"`. | +| `start_dir` | `str` | `"."` | Directory to open. Non-existent paths are walked up to the first existing parent, falling back to `"/"`. | +| `path_pattern` | `str` or `list` | `[]` | File extensions to accept in pick mode, e.g. `[".png", ".jpg"]`. Strings may include a leading `*` (`"*.wav"`). An empty list accepts all files. | + +## Pick mode result + +When the user confirms the selection, the result callback receives: + +```python +{ + "result_code": True, + "data": { + "paths": ["/path/to/file1.wav", "/path/to/file2.wav"] + } +} +``` + +If no file is selected, the current directory path is returned instead: + +```python +{ + "result_code": True, + "data": { + "paths": ["/data/audio/"] + } +} +``` + +When the user cancels, the result is: + +```python +{ + "result_code": False, + "data": {} +} +``` + +## Browse mode behavior + +In browse mode, tapping a directory navigates into it. Tapping a file sends a `view` intent: + +```python +self.startActivity(Intent(action="view", data=path)) +``` + +The system then resolves the file to the appropriate viewer using the manifest-declared `pathPattern` of installed apps, or falls back to the framework's generic `ViewActivity`. + +Long-pressing a file or folder opens an action bar with **Delete**, **Rename**, and **Cancel** options. Delete shows a confirmation dialog; Rename launches `RenameActivity`. + +## Example: image picker + +```python +import lvgl as lv +from mpos import Activity, Intent + +class GalleryLauncher(Activity): + def onCreate(self): + screen = lv.obj() + btn = lv.button(screen) + lv.label(btn).set_text("Choose image") + btn.add_event_cb(self._choose_image, lv.EVENT.CLICKED, None) + self.setContentView(screen) + + def _choose_image(self, event): + intent = Intent(action="pick_file") + intent.putExtra("mode", "pick") + intent.putExtra("start_dir", "/data/images") + intent.putExtra("path_pattern", [".png", ".jpg", ".jpeg", ".raw"]) + self.startActivityForResult(intent, self._on_image_picked) + + def _on_image_picked(self, result): + if result and result.get("result_code"): + paths = result.get("data", {}).get("paths", []) + if paths and not paths[0].endswith("/"): + print("Opening image:", paths[0]) +``` + +## See Also + +- [Intents](../architecture/intents.md) - How implicit intents and the `view` action work +- [AppManager](app-manager.md) - How file-type handlers are resolved +- [Creating Apps](../apps/creating-apps.md) - Declaring `view` handlers in an app manifest diff --git a/docs/frameworks/focus.md b/docs/frameworks/focus.md new file mode 100644 index 0000000..9d35d9b --- /dev/null +++ b/docs/frameworks/focus.md @@ -0,0 +1,100 @@ +# Focus Borders + +MicroPythonOS provides a single, reusable helper for focus highlighting so that apps and framework screens do not have to duplicate the same `FOCUSED`/`DEFOCUSED` event handlers. + +## Overview + +`add_focus_border` registers two LVGL event callbacks on a widget: + +- **`FOCUSED`**: draw a border around the widget and scroll it into view. +- **`DEFOCUSED`**: hide the border again. + +The helper is re-exported from the top-level `mpos` module, so apps can import it directly. + +## API + +```python +from mpos import add_focus_border + +add_focus_border(widget) +add_focus_border(widget, width=2) +add_focus_border(widget, width=2, color=lv.color_hex(0xFFFFFF)) +add_focus_border(widget, width=2, opacity=lv.OPA._50, radius=5) +``` + +### Parameters + +| Parameter | Default | Description | +|-----------|---------|-------------| +| `widget` | required | The LVGL object that should receive focus highlighting. | +| `width` | `1` | Border width in pixels when focused. | +| `color` | theme primary color | Border color. Falls back to `lv.theme_get_color_primary(None)` when omitted. | +| `opacity`| `None` | Optional `lv.OPA.*` value for the focused border. | +| `radius` | `None` | Optional corner radius for the focused border. | + +### Requirements + +For the callbacks to fire, the widget must be part of the default LVGL focus group: + +```python +focusgroup = lv.group_get_default() +if focusgroup: + focusgroup.add_obj(my_widget) +``` + +`add_focus_border` only draws the visual feedback; it does **not** add the widget to the focus group. + +## Examples + +### Basic usage + +```python +import lvgl as lv +from mpos import Activity, add_focus_border + +class MyActivity(Activity): + def onCreate(self): + screen = lv.obj() + screen.set_flex_flow(lv.FLEX_FLOW.COLUMN) + + btn = lv.button(screen) + btn_label = lv.label(btn) + btn_label.set_text("Focus me") + + add_focus_border(btn) + + focusgroup = lv.group_get_default() + if focusgroup: + focusgroup.add_obj(btn) + + self.setContentView(screen) +``` + +### Thicker, semi-transparent border + +```python +add_focus_border(btn, width=3, opacity=lv.OPA._50, radius=5) +``` + +### Focus highlight on a label + +Labels are not focusable by default, but they can be added to the focus group when they act as list rows: + +```python +row = lv.label(screen) +row.set_text("Setting row") +row.add_flag(lv.obj.FLAG.CLICKABLE) +add_focus_border(row, width=2) +focusgroup.add_obj(row) +``` + +## When to use it + +Use `add_focus_border` whenever you previously wrote a pair of `FOCUSED`/`DEFOCUSED` callbacks whose only job was to show or hide a border. It is used by the framework itself in places such as the launcher, settings screens, the top-menu drawer, `ImageView`, `MusicPlayer`, `ShowFonts`, `Connect4`, `About`, `HowTo`, and `WiFi Settings`. + +If you need additional behavior on focus (for example, starting a timer or loading data), keep your own callbacks and add `add_focus_border` alongside them. + +## See Also + +- [InputManager](input-manager.md) - Pointer and focus-group utilities +- [App Lifecycle](../apps/app-lifecycle.md) - Activity lifecycle and screen construction diff --git a/mkdocs.yml b/mkdocs.yml index 01cd941..8936471 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -60,6 +60,8 @@ nav: - DisplayMetrics: frameworks/display-metrics.md - FontManager: frameworks/font-manager.md - DownloadManager: frameworks/download-manager.md + - FileExplorerActivity: frameworks/file-explorer-activity.md + - Focus: frameworks/focus.md - InputManager: frameworks/input-manager.md - LightsManager: frameworks/lights-manager.md - NotificationManager: frameworks/notification-manager.md From 24adc8d6f92648eb32e42a3caf354b248bdad702 Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Sun, 21 Jun 2026 14:32:01 +0200 Subject: [PATCH 099/110] document input_activity.py --- docs/frameworks/input-activity.md | 158 +++++++++++++++++++++++++++ docs/frameworks/setting-activity.md | 15 ++- docs/frameworks/settings-activity.md | 3 +- mkdocs.yml | 1 + 4 files changed, 175 insertions(+), 2 deletions(-) create mode 100644 docs/frameworks/input-activity.md diff --git a/docs/frameworks/input-activity.md b/docs/frameworks/input-activity.md new file mode 100644 index 0000000..7005bea --- /dev/null +++ b/docs/frameworks/input-activity.md @@ -0,0 +1,158 @@ +# InputActivity + +`InputActivity` is a generic, reusable single-value input screen. It handles the UI and user input for one setting, then returns the value to the caller via `startActivityForResult`. It does **not** persist anything itself. + +## Overview + +`InputActivity` is useful any time you need to ask the user for a single value: a text string, a choice from radio buttons or a dropdown, or an integer from a slider. It supports the same input widgets as `SettingActivity`, but is independent of `SharedPreferences`. + +`SettingActivity` is a thin wrapper around `InputActivity` that adds `SharedPreferences` persistence, row-label updates, and `changed_callback` handling. Apps that only need the input UI can use `InputActivity` directly. + +## Basic Usage + +```python +from mpos import Activity, Intent, InputActivity + +class MyApp(Activity): + def ask_for_password(self): + intent = Intent(activity_class=InputActivity) + intent.putExtra("setting", { + "title": "WiFi Password", + "placeholder": "Enter password", + "ui": "textarea" + }) + intent.putExtra("value", "") + self.startActivityForResult(intent, self.on_password_result) + + def on_password_result(self, result): + if result and result.get("result_code"): + password = result["data"]["value"] + print("Password entered:", password) +``` + +## Intent Extras + +### Required extras + +- **`setting`** (dict): Metadata describing the input. + +### Optional extras + +- **`value`** (string): Initial value to display. If omitted, `setting["default_value"]` is used, falling back to `""`. + +## Setting Dictionary Structure + +The `setting` dict has the same shape as the one used by `SettingActivity` and `SettingsActivity`: + +### Required properties + +- **`title`** (string): Display name shown at the top of the input screen + +### Optional properties + +- **`key`** (string): Identifier returned in the result. Not used by `InputActivity` itself, but helpful for callers. +- **`ui`** (string): UI type. Options: `"textarea"` (default), `"radiobuttons"`, `"dropdown"`, `"slider"`, `"activity"` +- **`ui_options`** (list): List of `(label, value)` tuples for `radiobuttons` and `dropdown` +- **`placeholder`** (string): Placeholder text for `textarea` +- **`default_value`** (string): Default value if no `value` extra is provided +- **`note`** (string): Short informational text shown below the input widget +- **`min`** (int): Minimum value for `"slider"` (default: `0`) +- **`max`** (int): Maximum value for `"slider"` (default: `100`) +- **`allow_deselect`** (bool): For `radiobuttons`, allow the user to un-select the active option (default: `False`) + +## Result Contract + +`InputActivity` returns a result dict with the standard `Activity` result shape: + +```python +{ + "result_code": True, # True on Save, False on Cancel/back + "data": { + "value": "", + "key": "", + "ui": "" + } +} +``` + +The caller is responsible for persisting or acting on the returned `value`. + +## UI Types + +### Textarea (default) + +Text input with an on-screen keyboard. If the device has a camera, a "Scan data from QR code" button is also shown. + +```python +{ + "title": "API Key", + "key": "api_key", + "placeholder": "Enter your API key" +} +``` + +### Radio Buttons + +Mutually exclusive selection from a small list of options. + +```python +{ + "title": "Theme", + "key": "theme", + "ui": "radiobuttons", + "ui_options": [ + ("Light", "light"), + ("Dark", "dark"), + ("Auto", "auto") + ] +} +``` + +### Dropdown + +Compact selection from a larger list of options. + +```python +{ + "title": "Language", + "key": "language", + "ui": "dropdown", + "ui_options": [ + ("English", "en"), + ("German", "de"), + ("French", "fr"), + ("Spanish", "es") + ] +} +``` + +### Slider + +Integer selection within a range. + +```python +{ + "title": "Volume", + "key": "volume", + "ui": "slider", + "default_value": "50", + "min": 0, + "max": 100 +} +``` + +## Cancel and Back Handling + +Pressing the **Cancel** button or swiping back returns: + +```python +{"result_code": False, "data": {"value": "...", "key": "...", "ui": "..."}} +``` + +Callers should check `result.get("result_code")` before using the value. + +## See Also + +- [`SettingActivity`](setting-activity.md) - Wrapper that persists the input value to `SharedPreferences` +- [`SettingsActivity`](settings-activity.md) - List of multiple settings, each edited through `SettingActivity`/`InputActivity` +- [`SharedPreferences`](preferences.md) - For persisting values yourself diff --git a/docs/frameworks/setting-activity.md b/docs/frameworks/setting-activity.md index ec7f4ca..d3ed6aa 100644 --- a/docs/frameworks/setting-activity.md +++ b/docs/frameworks/setting-activity.md @@ -4,7 +4,9 @@ ## Overview -`SettingActivity` displays a single setting that the user can modify. The setting is passed via Intent extras and can be configured with different UI types. When the user saves the setting, it's automatically persisted to SharedPreferences (unless `dont_persist` is set to true). +`SettingActivity` is a thin wrapper around [`InputActivity`](input-activity.md). It passes the setting metadata and current value to `InputActivity`, waits for the user to save or cancel, then persists the returned value to `SharedPreferences` (unless `dont_persist` is set to true). It also updates the value label in `SettingsActivity` and fires `changed_callback` when the value changes. + +If you only need the input UI without persistence (for example, a WiFi password prompt), use [`InputActivity`](input-activity.md) directly. ## Basic Usage @@ -393,3 +395,14 @@ class AppStore(Activity): 5. **Provide meaningful placeholders**: Help users understand what format is expected with clear placeholder text. 6. **Consider conditional visibility**: Use `should_show` in SettingsActivity to hide settings that don't apply based on other settings. + +## How it works + +When `SettingActivity` is launched, it: + +1. Reads the current value from `SharedPreferences`. +2. Starts [`InputActivity`](input-activity.md) with the setting metadata and current value. +3. Receives the new value when the user saves. +4. Persists the value, updates the settings row label, and calls `changed_callback`. + +The `SettingActivity` API visible to apps is unchanged; existing settings code continues to work exactly as before. diff --git a/docs/frameworks/settings-activity.md b/docs/frameworks/settings-activity.md index 36e5fae..fc1d606 100644 --- a/docs/frameworks/settings-activity.md +++ b/docs/frameworks/settings-activity.md @@ -4,7 +4,7 @@ ## Overview -`SettingsActivity` displays a list of settings that users can browse and edit. When a user taps on a setting, it opens a [`SettingActivity`](setting-activity.md) for editing that individual setting. After editing, the value is automatically saved to SharedPreferences and the list is updated. +`SettingsActivity` displays a list of settings that users can browse and edit. When a user taps on a setting, it opens a [`SettingActivity`](setting-activity.md) for editing that individual setting. `SettingActivity` internally uses [`InputActivity`](input-activity.md) for the actual input UI, then persists the returned value to `SharedPreferences` and updates the list. ## Basic Usage @@ -396,4 +396,5 @@ SettingsActivity displays settings in a scrollable list with the following layou ## See Also - [`SettingActivity`](setting-activity.md) - For editing a single setting +- [`InputActivity`](input-activity.md) - Generic input UI used by `SettingActivity` - [`SharedPreferences`](preferences.md) - For persisting settings diff --git a/mkdocs.yml b/mkdocs.yml index 8936471..a043c0a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -62,6 +62,7 @@ nav: - DownloadManager: frameworks/download-manager.md - FileExplorerActivity: frameworks/file-explorer-activity.md - Focus: frameworks/focus.md + - InputActivity: frameworks/input-activity.md - InputManager: frameworks/input-manager.md - LightsManager: frameworks/lights-manager.md - NotificationManager: frameworks/notification-manager.md From 6f49c74abf7712a4e19dd931faf2444b5a65fd5f Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Wed, 24 Jun 2026 01:15:41 +0200 Subject: [PATCH 100/110] Update docs --- docs/architecture/frameworks.md | 1 - docs/frameworks/file-explorer-activity.md | 8 ++++---- docs/other/supported-file-formats.md | 1 + 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/architecture/frameworks.md b/docs/architecture/frameworks.md index 228fd7b..925838c 100644 --- a/docs/architecture/frameworks.md +++ b/docs/architecture/frameworks.md @@ -207,7 +207,6 @@ from mpos import Intent, FileExplorerActivity, Activity class MyActivity(Activity): def pick_file(self): intent = Intent(action="pick_file") - intent.putExtra("mode", "pick") intent.putExtra("path_pattern", [".wav"]) self.startActivityForResult(intent, self.on_file_picked) ``` diff --git a/docs/frameworks/file-explorer-activity.md b/docs/frameworks/file-explorer-activity.md index ae97f75..cd9bd08 100644 --- a/docs/frameworks/file-explorer-activity.md +++ b/docs/frameworks/file-explorer-activity.md @@ -9,19 +9,20 @@ The activity is registered for the `pick_file` action, so any app can ask the us Two modes are supported: - **`browse`** (default): navigate directories, open files, rename or delete them. -- **`pick`**: select one or more files and return them to the caller. +- **`pick`**: select one or more files and return them to the caller. The `pick_file` action automatically uses pick mode, so callers do not need to set the `"mode"` extra. ## Launching ### Pick files with `startActivityForResult` +Using the `pick_file` action automatically opens `FileExplorerActivity` in pick mode, so you do **not** need to set the `"mode"` extra. + ```python from mpos import Intent, Activity class MyActivity(Activity): def _open_file_clicked(self, event): intent = Intent(action="pick_file") - intent.putExtra("mode", "pick") intent.putExtra("start_dir", "/data/audio") intent.putExtra("path_pattern", [".wav"]) self.startActivityForResult(intent, self._on_file_picked) @@ -49,7 +50,7 @@ class MyActivity(Activity): | Extra | Type | Default | Description | |-------|------|---------|-------------| -| `mode` | `str` | `"browse"` | `"browse"` or `"pick"`. | +| `mode` | `str` | `"browse"` (or `"pick"` when action is `"pick_file"`) | `"browse"` or `"pick"`. Only needed when launching `FileExplorerActivity` directly; the `"pick_file"` action implies picker mode automatically. | | `start_dir` | `str` | `"."` | Directory to open. Non-existent paths are walked up to the first existing parent, falling back to `"/"`. | | `path_pattern` | `str` or `list` | `[]` | File extensions to accept in pick mode, e.g. `[".png", ".jpg"]`. Strings may include a leading `*` (`"*.wav"`). An empty list accepts all files. | @@ -114,7 +115,6 @@ class GalleryLauncher(Activity): def _choose_image(self, event): intent = Intent(action="pick_file") - intent.putExtra("mode", "pick") intent.putExtra("start_dir", "/data/images") intent.putExtra("path_pattern", [".png", ".jpg", ".jpeg", ".raw"]) self.startActivityForResult(intent, self._on_image_picked) diff --git a/docs/other/supported-file-formats.md b/docs/other/supported-file-formats.md index 93cb966..454ea6f 100644 --- a/docs/other/supported-file-formats.md +++ b/docs/other/supported-file-formats.md @@ -6,6 +6,7 @@ MicroPythonOS uses LVGL's built-in image decoders and its own audio stack. The f | Format | Extensions | Notes | |--------|------------|-------| +| BMP | `.bmp` | Supports RGB565 and RGB888 | | PNG | `.png` | Fully supported via LodePNG. | | Baseline JPEG | `.jpg`, `.jpeg` | Fully supported via TJpgD. | | Progressive JPEG | `.jpg`, `.jpeg` | **Not supported.** Files will decode as `0x0` and appear blank. | From 2564e0ba218c433cfa971b8675a6d12a8b87b280 Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Thu, 2 Jul 2026 11:09:54 +0200 Subject: [PATCH 101/110] Update docs --- docs/apps/appstore.md | 17 +++- docs/apps/badgehub.md | 54 +++++++++++++ docs/apps/bundling-apps.md | 37 ++++++++- docs/apps/index.md | 7 +- docs/apps/native-apps.md | 93 ++++++++++++++++++++++ docs/frameworks/audiomanager.md | 23 +++++- docs/frameworks/file-explorer-activity.md | 7 ++ docs/frameworks/focus.md | 13 +++ docs/frameworks/font-manager.md | 6 ++ docs/frameworks/notification-manager.md | 23 ++++++ docs/getting-started/supported-hardware.md | 16 +++- mkdocs.yml | 2 + 12 files changed, 290 insertions(+), 8 deletions(-) create mode 100644 docs/apps/badgehub.md create mode 100644 docs/apps/native-apps.md diff --git a/docs/apps/appstore.md b/docs/apps/appstore.md index f3c859e..63cdf9f 100644 --- a/docs/apps/appstore.md +++ b/docs/apps/appstore.md @@ -2,14 +2,25 @@ The MicroPythonOS App Store allows users to download and install new apps to extend system functionality. -App discovery is currently done by downloading the app list from [apps.micropythonos.com](https://apps.micropythonos.com). +## Backends + +The AppStore app can pull apps from more than one source: + +- **BadgeHub.eu** — the community appstore for event badges and community-built apps at [badgehub.eu](https://badgehub.eu). On supported firmware builds this is the default backend. See [BadgeHub Apps](badgehub.md) for details on publishing there. +- **MicroPythonOS Apps** — the curated, manually reviewed app index at [apps.micropythonos.com](https://apps.micropythonos.com). + +Use the backend selector in the AppStore UI to switch between sources. ## Example Apps - **Hello World**: A sample app demonstrating basic functionality. - **Camera**: Captures images and scans QR codes. - **Image Viewer**: Displays images stored in `/data/images/`. -- **IMU**: Visualize data from the Intertial Measurement Unit, also known as the accellerometer. +- **IMU**: Visualize data from the Inertial Measurement Unit, also known as the accelerometer. +- **Nostr**: A decentralized chat client using the Nostr protocol, shipped as a Python package. +- **Sorter**: A puzzle game where you sort items into the right bins. +- **The Free Lantern Player**: A media player app. +- **Breakout**: A classic brick-breaking game that uses a native C extension module. ## Image Viewer @@ -34,4 +45,4 @@ The **Image Viewer** app displays images stored in `/data/images/`. See [Support ## Developing Apps -Apps are written in MicroPython and installed in `/apps/`. See [Filesystem Layout](../architecture/filesystem.md) for the app directory structure. +Apps are written in MicroPython and installed in `/apps/`. See [Filesystem Layout](../architecture/filesystem.md) for the app directory structure, [Bundling Apps](bundling-apps.md) for packaging, and [Native C/C++ Apps](native-apps.md) for apps that need compiled extensions. diff --git a/docs/apps/badgehub.md b/docs/apps/badgehub.md new file mode 100644 index 0000000..ef64590 --- /dev/null +++ b/docs/apps/badgehub.md @@ -0,0 +1,54 @@ +# BadgeHub.eu Apps + +[BadgeHub.eu](https://badgehub.eu) is a community appstore for event badges and MicroPythonOS devices. MicroPythonOS ships with a BadgeHub backend in the AppStore app, so users can browse, install, and update BadgeHub-published apps directly on their device. + +## What is BadgeHub? + +BadgeHub hosts `.mpk` packages and exposes a JSON API that MicroPythonOS queries for app listings, project details, and downloadable releases. It is particularly useful for: + +- conference or camp badge apps +- community-built utilities +- distributing apps without going through the curated MicroPythonOS app index + +## Backend endpoints + +The BadgeHub backend uses API version 3: + +- `https://badgehub.eu/api/v3/project-summaries` — list of all published projects with summary metadata. +- `https://badgehub.eu/api/v3/projects/` — full project details, including releases, icons, and descriptions. + +Each project maps to one MicroPythonOS app package. The project's slug becomes the app identifier in the store. + +## Project metadata + +A project detail response contains fields such as: + +- `name` — display name in the AppStore. +- `description` / `long_description` — short and long descriptions. +- `icon_url` — URL to a 64×64 or larger app icon. +- `publisher` — author or organization. +- `releases` — list of published releases, each with a version and a download URL. +- `main_executable` — optional flag indicating the primary app executable for this project. + +The AppManager uses the latest release's download URL to fetch the `.mpk`. + +## Releasing an app on BadgeHub + +1. Build your `.mpk` following the [Bundling Apps](bundling-apps.md) guide. The top-level folder in the ZIP must match the app's `fullname` exactly. +2. Ensure the archive contains a valid `MANIFEST.JSON`. +3. Upload the `.mpk` to your BadgeHub project. +4. MicroPythonOS will detect the new release the next time the AppStore refreshes the BadgeHub backend. + +## Version handling + +Releases on BadgeHub should use [semantic versioning](https://semver.org/) strings (for example, `1.2.3`). The AppStore compares the installed version against the latest BadgeHub release and offers an update when the BadgeHub version is newer. + +## Switching backends in the AppStore app + +The AppStore app can switch between the curated MicroPythonOS backend and the BadgeHub backend. On supported firmware builds, BadgeHub is the default backend. Tap the backend selector in the AppStore UI to switch sources. + +## See also + +- [Bundling Apps](bundling-apps.md) — creating `.mpk` packages +- [Creating Apps](creating-apps.md) — writing an app from scratch +- [App Lifecycle](app-lifecycle.md) — `Activity` and `Service` lifecycle diff --git a/docs/apps/bundling-apps.md b/docs/apps/bundling-apps.md index dc55af8..8187ead 100644 --- a/docs/apps/bundling-apps.md +++ b/docs/apps/bundling-apps.md @@ -21,7 +21,7 @@ MicroPythonOS validates this layout while extracting, and rejects packages that From the parent directory that contains your app folder, create a stored (uncompressed) ZIP that includes the top-level folder. It's recommended to make the `.mpk` deterministic by setting file timestamps to a fixed value, sorting entries, and excluding extra file attributes: -``` +```bash cd internal_filesystem/apps/ find com.micropythonos.helloworld -exec touch -t 202501010000.00 {} \; (find com.micropythonos.helloworld -type d; find com.micropythonos.helloworld -type f) | sort | TZ=CET zip -X -r -0 /tmp/com.micropythonos.helloworld_0.0.2.mpk -@ @@ -34,8 +34,43 @@ This: - uses `-X` to exclude extra file attributes - places `com.micropythonos.helloworld/` as the first entry +## Python packages + +An app can also be shipped as a proper Python package. This lets you split an app into many modules, use relative imports, and import shared code cleanly. + +To opt in, the app folder must contain an `__init__.py` file, and **every directory on the path to each entrypoint** must also contain an `__init__.py` (or compiled `__init__.mpy`). The AppManager then imports the entrypoint as part of the package. The module name becomes the package fullname dotted with the entrypoint path, for example `com_micropythonos_nostr.chat_list_activity`. + +Example `com_micropythonos_nostr` layout: + +``` +com_micropythonos_nostr/ +com_micropythonos_nostr/__init__.py +com_micropythonos_nostr/MANIFEST.JSON +com_micropythonos_nostr/icon_64x64.png +com_micropythonos_nostr/chat_list_activity.py +com_micropythonos_nostr/nostr_service.py +com_micropythonos_nostr/... +``` + +The `MANIFEST.JSON` still declares `chat_list_activity.py` as the entrypoint; the framework detects the `__init__.py` and loads it as a package automatically. + +Relative imports work as usual inside the package: + +```python +from . import nostr_service +from .nostr_service import NostrService +``` + +## Native modules + +Apps can include native C/C++ extension modules compiled as MicroPython `.native.mpy` files. Place the compiled `.mpy` in the app folder and import it like a normal module. For build instructions and an example, see [Native C/C++ Apps](native-apps.md). + ## AppStore bundling The apps at https://apps.MicroPythonOS.com are a curated, manually reviewed, vetted collection, often created and maintained by the MicroPythonOS core team. These are bundled into an [`app_index.json`](https://github.com/MicroPythonOS/apps/blob/main/app_index.json) using [`scripts/bundle_apps.sh`](https://github.com/MicroPythonOS/MicroPythonOS/blob/main/scripts/bundle_apps.sh) and then pushed to the [apps repo](https://github.com/MicroPythonOS/apps). + +## BadgeHub.eu publishing + +For community and event-badge publishing, MicroPythonOS also supports the [BadgeHub.eu](https://badgehub.eu) appstore backend. See [BadgeHub Apps](badgehub.md) for how to publish an `.mpk` there. diff --git a/docs/apps/index.md b/docs/apps/index.md index eb75db6..fb2a926 100644 --- a/docs/apps/index.md +++ b/docs/apps/index.md @@ -3,4 +3,9 @@ MicroPythonOS is built around an app-centric model, with built-in apps for core functionality and an App Store for additional apps. - [Built-in Apps](built-in-apps.md): Core apps included with the OS. -- [App Store](appstore.md): Download and install new apps. +- [App Store](appstore.md): Download and install new apps, including BadgeHub.eu community apps. +- [Creating Apps](creating-apps.md): Write an app from scratch. +- [App Lifecycle](app-lifecycle.md): Activity and Service lifecycle. +- [Bundling Apps](bundling-apps.md): Package apps as `.mpk` files, including Python packages. +- [Native C/C++ Apps](native-apps.md): Add compiled native modules to an app. +- [BadgeHub Apps](badgehub.md): Publish apps on BadgeHub.eu. diff --git a/docs/apps/native-apps.md b/docs/apps/native-apps.md new file mode 100644 index 0000000..d1d2c47 --- /dev/null +++ b/docs/apps/native-apps.md @@ -0,0 +1,93 @@ +# Native C/C++ Apps + +Most MicroPythonOS apps are written in MicroPython, but apps can also include native extension modules written in C or C++. These modules are compiled to MicroPython `.native.mpy` files and bundled inside the app's `.mpk` like any other module. + +## When to use native code + +Use a native module when: + +- You need more CPU performance than pure MicroPython can provide (for example, a game loop or signal processing). +- You need to call C libraries that have no MicroPython equivalent. +- You want to reuse existing C/C++ code. + +For everything else, prefer plain MicroPython — native modules add build complexity and are architecture-specific. + +## How native modules work + +MicroPython supports [native machine code modules](https://docs.micropython.org/en/latest/develop/natmod.html) (often called *natmods*). A natmod is a self-contained `.mpy` file that contains compiled machine code and a MicroPython module descriptor. At runtime it is imported just like a `.py` file: + +```python +import breakout +breakout.start() +``` + +MicroPythonOS places the `.mpy` file in the app folder when the `.mpk` is installed. The AppManager's import path then finds it automatically. + +## Example: `com.micropythonos.breakout` + +The built-in **Breakout** game uses a native C module for the game logic. Its source lives in `c_mpos/breakout/` in the main repository. + +Project layout: + +``` +c_mpos/breakout/ +├── Makefile +├── build.sh +└── breakout.c +``` + +`Makefile`: + +```makefile +MPY_DIR = ../../lvgl_micropython/lib/micropython/ + +MOD = breakout +SRC = breakout.c +LINK_RUNTIME = 1 +ARCHES = x64 xtensawin + +ifeq ($(ARCH),) +.PHONY: all $(ARCHES) + +all: $(ARCHES) + +$(ARCHES): + $(MAKE) -f $(lastword $(MAKEFILE_LIST)) ARCH=$@ MOD=$(MOD)_$@ +else +include $(MPY_DIR)/py/dynruntime.mk +endif +``` + +This builds two artifacts: + +- `breakout_x64.native.mpy` — for the desktop simulator. +- `breakout_xtensawin.native.mpy` — for ESP32-S3 devices. + +The `build.sh` script sets up the Espressif toolchain and then copies the resulting `.mpy` files into the app's folder at `internal_filesystem/apps/com.micropythonos.breakout/`. + +## Bundling a native module + +1. Write your C/C++ source and a `Makefile` based on the MicroPython `dynruntime.mk` rules. +2. Build the module for every architecture you want to support. +3. Rename or place the output `.mpy` file in the app folder so its import name matches what your MicroPython code imports. For Breakout, the `xtensawin` build is named `breakout.mpy` inside the app folder. +4. Build the `.mpk` normally. The native `.mpy` is packaged alongside the Python files. + +``` +com.micropythonos.breakout/ +├── MANIFEST.JSON +├── icon_64x64.png +├── main.py +└── breakout.mpy # native module, imported by main.py +``` + +## Limitations + +- Native modules must be compiled separately for each target architecture (for example, `x64` for desktop and `xtensawin` for ESP32). +- The MicroPython natmod ABI can change between releases, so rebuild modules when the `lvgl_micropython` submodule is updated. +- Native code has the same filesystem and memory constraints as the rest of the app. + +## See also + +- [Bundling Apps](bundling-apps.md) — packaging apps as `.mpk` files +- [MicroPython Native Module Documentation](https://docs.micropython.org/en/latest/develop/natmod.html) +- [Creating Apps](creating-apps.md) — writing an app from scratch diff --git a/docs/frameworks/audiomanager.md b/docs/frameworks/audiomanager.md index 1a1168c..8128618 100644 --- a/docs/frameworks/audiomanager.md +++ b/docs/frameworks/audiomanager.md @@ -121,6 +121,27 @@ AudioManager.record_wav_adc( ) ``` +### Desktop WAV Playback + +On Linux and macOS desktop builds, WAV playback uses an external audio player subprocess instead of I2S hardware. `AudioManager` automatically detects one of the following players in order of preference: + +1. `ffplay` (FFmpeg) +2. `aplay` (ALSA) +3. `paplay` (PulseAudio) +4. `afplay` (macOS) + +If none is installed, the player still runs in "timing simulation" mode so apps can test playback flow without sound. + +```bash +# Ubuntu/Debian +sudo apt install ffmpeg + +# macOS +# afplay is pre-installed +``` + +Priority and volume controls apply conceptually on desktop, although the external player handles the actual output. + ## Audio Focus Priority AudioManager implements a 3-tier priority-based audio focus system inspired by Android: @@ -240,7 +261,7 @@ Set or get the global volume (0–100). | **Fri3d 2024 Badge** | ✅ | ✅ | ❌ | ✅ | Full audio support | | **Fri3d 2026 Badge** | ✅ | ❌ | ✅ | ✅ | ADC mic via `adc_mic` | | **Waveshare ESP32-S3** | ✅ | ❌ | ❌ | ❌ | I2S output only | -| **Linux/macOS** | ❌ | ✅ (simulated) | ✅ (simulated) | ❌ | Simulated recording for testing | +| **Linux/macOS** | ✅ (external player) | ✅ (simulated) | ✅ (simulated) | ❌ | WAV playback via `ffplay`/`aplay`/`paplay`/`afplay` | ## Troubleshooting diff --git a/docs/frameworks/file-explorer-activity.md b/docs/frameworks/file-explorer-activity.md index cd9bd08..0a34e1c 100644 --- a/docs/frameworks/file-explorer-activity.md +++ b/docs/frameworks/file-explorer-activity.md @@ -99,6 +99,13 @@ The system then resolves the file to the appropriate viewer using the manifest-d Long-pressing a file or folder opens an action bar with **Delete**, **Rename**, and **Cancel** options. Delete shows a confirmation dialog; Rename launches `RenameActivity`. +### Deleting directories + +Deleting a folder removes it recursively, including all files and subdirectories inside it. A confirmation dialog is shown before any deletion begins. + +!!! warning + Directory deletion cannot be undone. Use the confirmation dialog carefully, especially when deleting paths under `/data/` or `/apps/`. + ## Example: image picker ```python diff --git a/docs/frameworks/focus.md b/docs/frameworks/focus.md index 9d35d9b..d68e0c7 100644 --- a/docs/frameworks/focus.md +++ b/docs/frameworks/focus.md @@ -44,6 +44,19 @@ if focusgroup: `add_focus_border` only draws the visual feedback; it does **not** add the widget to the focus group. +### Touch-aware behavior + +On devices that are primarily controlled with a touchscreen, the focus border is intentionally hidden until the user first navigates with a directional input (keyboard, hardware buttons, or a rotary encoder). This keeps touch-only screens clean while still making focused elements obvious once physical navigation begins. + +If you want a widget to reveal the focus border immediately, ensure it is added to the default focus group and focus it programmatically: + +```python +focusgroup = lv.group_get_default() +if focusgroup: + focusgroup.add_obj(btn) + focusgroup.focus_obj(btn) +``` + ## Examples ### Basic usage diff --git a/docs/frameworks/font-manager.md b/docs/frameworks/font-manager.md index 58759f2..984a9b5 100644 --- a/docs/frameworks/font-manager.md +++ b/docs/frameworks/font-manager.md @@ -91,6 +91,8 @@ Return a font object suitable for use with `set_style_text_font()`. **Returns:** `lv.font_t` — the requested font, possibly wrapped with emoji support. +If the requested `family` is not available, FontManager falls back to Montserrat, then to the first available built-in font, and finally to `lv.font_montserrat_12`. + **Example:** ```python @@ -202,6 +204,10 @@ builtin/res/emojis/ Files are named by their Unicode codepoint(s) in uppercase hex, with multiple codepoints joined by `-` (e.g. `1F600.png` for 😀, `1F3CE-FE0F.png` for 🏎️, `1F1F8-1F1FB.png` for 🇸🇻). FontManager scans the directory at runtime and builds a `{codepoint: path}` map. +### Missing emoji fallback + +If the exact PNG for an emoji is not bundled, FontManager tries visually similar emoji before giving up. For example, several kissing-face variants can substitute for each other. This keeps text readable even when only a small emoji set is included in the firmware. + ### Adding new emoji 1. Add a PNG named `.png` to `32x32/`: diff --git a/docs/frameworks/notification-manager.md b/docs/frameworks/notification-manager.md index 6911201..787f25c 100644 --- a/docs/frameworks/notification-manager.md +++ b/docs/frameworks/notification-manager.md @@ -63,6 +63,29 @@ Notification.PRIORITY_MAX = 3 The drawer sorts notifications by priority, then by most recent update time. +## Notification sounds + +When a new notification arrives, the system can play a short RTTTL ringtone on the device's buzzer output. The sound is stored as an RTTTL string in the Settings app's `SharedPreferences` under the key `notification_sound`. + +Available sounds: + +| Label | RTTTL string | +|-------|--------------| +| `None` | (empty string, no sound) | +| `Coin` | `coin:d=8,o=6,b=200:16b5,e6` | +| `Scale up` | `scale_up:d=32,o=5,b=100:c,c#,d#,e,f#,g#,a#,b` | +| `Superhappy` | `superhappy:d=8,o=5,b=635:c,e,g,c,e,g,c,e,g,c6,e6,g6,c6,e6,g6,c7,e7,g7,c7,e7,g7,c7,e7,g7` | + +The default sound is `Coin`. If no buzzer output is available, the notification posts silently. Apps can change the sound programmatically: + +```python +from mpos import SharedPreferences + +prefs = SharedPreferences("com.micropythonos.settings") +prefs.put_string("notification_sound", "coin:d=8,o=6,b=200:16b5,e6") +prefs.commit() +``` + ## NotificationManager API All methods are class methods. `NotificationManager` initializes itself lazily on first use. diff --git a/docs/getting-started/supported-hardware.md b/docs/getting-started/supported-hardware.md index 5d93228..a4fcaa0 100644 --- a/docs/getting-started/supported-hardware.md +++ b/docs/getting-started/supported-hardware.md @@ -5,22 +5,34 @@ MicroPythonOS runs on a variety of platforms, from microcontrollers to desktops. ## ESP32 Microcontrollers - [M5Stack Fire](https://docs.m5stack.com/en/core/fire): Fully Supported +- [M5Stack Core2](https://docs.m5stack.com/en/core/core2): Fully Supported - [HardKernel ODROID-GO](https://wiki.odroid.com/odroid_go/odroid_go): Fully Supported +- [LilyGo T4 V1.3](https://github.com/Xinyuan-LilyGO/LilyGo_Txx): Fully Supported +- [unPhone 9](https://unphone.net/): Fully Supported ## ESP32-S3 Microcontrollers +- [DFRobot UniHiker K10](https://www.dfrobot.com/product-2676.html): Fully Supported - [Fri3d Camp 2024 Badge](https://fri3d.be/badge/2024/): Fully Supported - [Fri3d Camp 2026 Badge](https://fri3d.be/badge/2026/): Experimental Support (Hardware Prototype Phase) -- [Makerfabs Matouch ESP32-S3 SPI IPS 2.8" with Camera OV3660](https://www.makerfabs.com/matouch-esp32-s3-spi-ips-2-8-with-camera-ov3660.html): Fully Supported +- [Freenove ESP32-S3 Display](https://github.com/Freenove/Freenove_ESP32_S3_Display): Fully Supported +- [LilyGo T-Display S3](https://lilygo.cc/products/t-display-s3): Fully Supported +- [LilyGo T-HMI](https://lilygo.cc/en-us/products/t-hmi): Fully Supported +- [LilyGo T-Watch S3 Plus](https://lilygo.cc/products/t-watch-s3-plus): Fully Supported +- [Makerfabs MaTouch ESP32-S3 SPI IPS 2.8" with Camera OV3660](https://www.makerfabs.com/matouch-esp32-s3-spi-ips-2-8-with-camera-ov3660.html): Fully Supported +- [SQUiXL](https://squixl.io): Fully Supported - [Waveshare ESP32-S3-Touch-LCD-2](https://www.waveshare.com/wiki/ESP32-S3-Touch-LCD-2): Fully Supported ## Desktop Computers - **Linux**: Supported using SDL for display handling. - **MacOS**: Supported as well. -- **Windows**: [users report](https://github.com/MicroPythonOS/MicroPythonOS/issues/31) that the Linux desktop version runs fine under WSL2 on Windows 11. +- **Windows**: [Users report](https://github.com/MicroPythonOS/MicroPythonOS/issues/31) that the Linux desktop version runs fine under WSL2 on Windows 11. ## Raspberry Pi - **Raspbian and other Linux-based**: Should work! +## Adding a new board + +If your device is not listed, see the [Porting Guide](../os-development/porting-guide.md). diff --git a/mkdocs.yml b/mkdocs.yml index a043c0a..f3b7fb2 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -49,6 +49,8 @@ nav: - Creating Apps: apps/creating-apps.md - App Lifecycle: apps/app-lifecycle.md - Bundling Apps: apps/bundling-apps.md + - Native C/C++ Apps: apps/native-apps.md + - BadgeHub Apps: apps/badgehub.md - Frameworks: - AppearanceManager: frameworks/appearance-manager.md - AppManager: frameworks/app-manager.md From a5c6951fd3ea77ca69e34300d87903492992e16a Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Thu, 2 Jul 2026 13:57:20 +0200 Subject: [PATCH 102/110] Add web-port docs --- docs/web-port/developer.md | 254 +++++++++++++++++++++++++++++++++++++ docs/web-port/using.md | 36 ++++++ mkdocs.yml | 3 + 3 files changed, 293 insertions(+) create mode 100644 docs/web-port/developer.md create mode 100644 docs/web-port/using.md diff --git a/docs/web-port/developer.md b/docs/web-port/developer.md new file mode 100644 index 0000000..68e0ca0 --- /dev/null +++ b/docs/web-port/developer.md @@ -0,0 +1,254 @@ +# Web Port Developer Info + +This section describes how MicroPythonOS runs in the browser, how to build and serve it, and everything you need to know to make changes. **The entire web port is self-contained in the main MicroPythonOS repository** — no edits to the `lvgl_micropython` submodule (or its nested `micropython`/`lvgl`) need to be committed. The submodule modifications required by the web target are stored under [`scripts/web_port/`](https://github.com/MicroPythonOS/MicroPythonOS/tree/main/scripts/web_port/) and are applied automatically at build time. + +## TL;DR + +```bash +# 1. One-time: install & activate the Emscripten SDK (see "Prerequisites"). +# The build auto-activates ../emsdk or ../../emsdk if emcc is not on PATH. + +# 2. Build the web target. +scripts/build_mpos_web.sh + +# 3. Build (if needed) and serve locally at http://localhost:8080/ +scripts/run_web.sh +# scripts/run_web.sh --no-build # serve existing web/ without rebuilding +# PORT=9000 scripts/run_web.sh # serve on a different port +``` + +Output artifacts land in `web/`: `micropython.{html,js,wasm,data}`, plus copies `index.html` and `mpos.html`. + +## Prerequisites + +- **Emscripten SDK** (tested with **6.0.0**). Either have `emcc` on `PATH`, or place an activated `emsdk` checkout one or two directories above this repo (`../emsdk` or `../../emsdk`). `scripts/build_mpos_web.sh` sources `emsdk_env.sh` automatically when `emcc` is missing. +- Standard host toolchain to build `mpy-cross` (built with the host compiler, not emcc) and the usual MicroPython build dependencies (`python3`, `make`). +- The git submodules must be checked out (`git submodule update --init --recursive`). A **clean** submodule checkout is fine — the build re-applies the web changes every time. + +## Build from a fresh clone + +The entire web port lives in the main repository, so a fork is self-contained: a fresh clone plus the submodules plus an Emscripten SDK is all that is needed. The submodule C changes are re-applied automatically by the build, so **nothing needs to be committed into any submodule.** + +```bash +# 1. Clone your fork with submodules. +git clone --recursive https://github.com//MicroPythonOS +cd MicroPythonOS +# (if you forgot --recursive:) +# git submodule update --init --recursive + +# 2. Install + activate Emscripten 6.0.0 somewhere the build can find it +# (emcc on PATH, or an activated emsdk at ../emsdk or ../../emsdk). + +# 3. Build and serve. +scripts/build_mpos_web.sh +scripts/run_web.sh +``` + +### Pinned submodule commits (known-good) + +The two C patches apply against specific upstream revisions. If a submodule is advanced past these, `patch --forward` may reject a hunk (the build logs the failure but continues, so a broken boot can result). These are the commits this port was verified against: + +| Submodule | Commit | Tag/branch | +| --- | --- | --- | +| `lvgl_micropython` | `a491b2a` | `integration` | +| `lvgl_micropython/lib/micropython` | `78ff170` | `v1.27.0` | +| `lvgl_micropython/lib/lvgl` | `c016f72` | `v9.3.0-556` | +| `lvgl_micropython/lib/SDL` | `6ad390fc` | `release-2.26.0-4202` | +| `freezeFS` | `5f211e3` | `main` | +| `secp256k1-embedded-ecdh` | `f86eb16` | `micropython_1.25.0` | +| `micropython-camera-API` | `f88b29d` | `master` | +| `micropython-nostr` | `2375c45` | `0.9-22` | + +The critical two for the web patches are `lvgl_micropython` (for `sdl_bus.h`) and `lvgl_micropython/lib/micropython` (for `gccollect.c`). A fork should pin at least those. If you bump them, regenerate the patches (see *Updating a submodule patch*) and re-verify a clean boot. + +## How it works (architecture) + +The web target reuses the **unix port** of MicroPython (LVGL + SDL display/input drivers, the frozen manifest, the `ext_mod` C modules) but compiles it with the Emscripten toolchain (`emcc`/`em++`/`emar`) and links Emscripten's bundled SDL2 port (`-sUSE_SDL=2`) instead of a natively built `libSDL2.a`. The result renders into an HTML ``. + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Browser tab (web/mpos.html → micropython.js + .wasm + .data) │ +│ │ +│ ◀── SDL2 (Emscripten port) ◀── LVGL ◀── MPOS │ +│ │ +│ asyncio event loop (TaskManager.start → asyncio.run) │ +│ ├─ task_handler (drives lv.task_handler + lv.tick_inc) │ +│ └─ machine.Timer (asyncio-backed periodic/one-shot) │ +└─────────────────────────────────────────────────────────────┘ +``` + +Key design points: + +- **`REAL_PORT = 'unix'`** — all unix patches and `machine_sdl` reuse unchanged. +- **No native threads / sockets / ffi / termios / bluetooth.** The web build sets `MICROPY_PY_THREAD/SOCKET/FFI/TERMIOS/BLUETOOTH=0`. Python modules that expect those are satisfied with small web-only shims (see below). +- **No native `machine.Timer`.** `machine_timer.c` is dropped from the web Makefile; an asyncio-backed `Timer` is injected at boot. +- **The preloaded filesystem is mounted at `/`** (root), matching the on-device layout, because `main.py` does `sys.path.insert(0, "lib")` and apps use root-relative paths like `/apps` and `/builtin`. + +## Where everything lives (all in this repo) + +| Path | Purpose | +| --- | --- | +| [`scripts/build_mpos.sh`](https://github.com/MicroPythonOS/MicroPythonOS/blob/main/scripts/build_mpos.sh) | Central build orchestration. The `web` target branch does all web-specific work (patching the submodule, staging the FS, injecting shims, invoking `make.py web`, collecting artifacts). | +| [`scripts/build_mpos_web.sh`](https://github.com/MicroPythonOS/MicroPythonOS/blob/main/scripts/build_mpos_web.sh) | Convenience wrapper: activates emsdk then runs `build_mpos.sh web`. | +| [`scripts/run_web.sh`](https://github.com/MicroPythonOS/MicroPythonOS/blob/main/scripts/run_web.sh) | Build (optional) + serve `web/` with `python3 -m http.server`. | +| [`scripts/web_port/web.py`](https://github.com/MicroPythonOS/MicroPythonOS/blob/main/scripts/web_port/web.py) | The Emscripten build backend. Copied into `lvgl_micropython/builder/web.py` at build time; consumed by `make.py web`. | +| [`scripts/web_port/sdl_bus.h.patch`](https://github.com/MicroPythonOS/MicroPythonOS/blob/main/scripts/web_port/sdl_bus.h.patch) | C struct-layout fix applied to the `lcd_bus` SDL bus (see "Submodule patches"). | +| [`scripts/web_port/gccollect.c.patch`](https://github.com/MicroPythonOS/MicroPythonOS/blob/main/scripts/web_port/gccollect.c.patch) | Conservative-GC fix for wasm applied to the unix port (see "Submodule patches"). | +| [`web/shell.html`](https://github.com/MicroPythonOS/MicroPythonOS/blob/main/web/shell.html) | The HTML shell template (`--shell-file`). Build copies the produced `micropython.html` to `index.html` and `mpos.html`. | +| `web/.preload_internal_filesystem/` | Auto-generated staging copy of `internal_filesystem/` (with web shims injected). Recreated on every build; do not edit by hand. | + +The web-only Python shims (`_thread.py`, `socket.py`, `_webrepl.py`, `websocket.py`, `task_handler.py`, `_web_machine_timer.py`) and the boot-time `machine.Timer` injection are written into the staged filesystem by `build_mpos.sh` (heredocs in the `web` target). They are **not** committed into `internal_filesystem/` so device builds are unaffected. + +## Submodule patches (applied automatically) + +The web target requires four changes inside the `lvgl_micropython` submodule. Rather than committing them to the submodule, they are stored in this repo and applied at the start of the `web` build (`patch --forward` makes re-application a no-op; the file copies are idempotent): + +1. **`builder/web.py`** (full file) — the Emscripten build backend. Copied from `scripts/web_port/web.py`. +2. **`ext_mod/lcd_bus/sdl_bus/sdl_bus.h`** — adds a missing `uint32_t buffer_flags;` field to `mp_lcd_sdl_bus_obj_t` so its layout matches the generic `mp_lcd_bus_obj_t`. `lcd_panel_io_init()` casts the SDL object to the generic type and calls `panel_io_handle.init(...)`; without this field the offset of `panel_io_handle` differs on 32-bit/wasm and the indirect call reads the wrong function pointer → `function signature mismatch` trap. (On 64-bit native, struct padding hid the bug.) +3. **`lib/micropython/ports/unix/gccollect.c`** — adds an `__EMSCRIPTEN__` branch to `gc_collect()` that uses `emscripten_scan_stack()` + `emscripten_scan_registers()` instead of the setjmp-based `gc_helper_collect_regs_and_stack()`. On wasm, live object pointers live in wasm locals/registers (not linear memory) and are invisible to a memory scan, so the stock collector freed in-use objects → `memory access out of bounds`. This matches the upstream MicroPython `ports/webassembly` approach and **requires ASYNCIFY** (so registers can be spilled), enabled via `-sASYNCIFY=1` in `web.py`. +4. **`ext_mod/_webnet/`** (new files, not a patch) — the browser `fetch()` bridge for HTTP networking. `webnet.c` + `micropython.mk` are copied from `scripts/web_port/ext_mod/_webnet/`; the `.mk` only compiles it when `MPOS_WEB=1`. See *Networking* below. + +### Updating a submodule patch + +If you need to change one of the patched submodule files: + +```bash +# Edit the file directly in the submodule, then regenerate the patch: +cd lvgl_micropython +git diff -- ext_mod/lcd_bus/sdl_bus/sdl_bus.h \ + > ../scripts/web_port/sdl_bus.h.patch + +cd lib/micropython +git diff -- ports/unix/gccollect.c \ + > ../../../scripts/web_port/gccollect.c.patch + +# For web.py, just copy it back: +cp lvgl_micropython/builder/web.py scripts/web_port/web.py +``` + +To verify reproducibility, revert the submodule files and rebuild — the build should re-apply everything: + +```bash +( cd lvgl_micropython \ + && git checkout -- ext_mod/lcd_bus/sdl_bus/sdl_bus.h \ + && rm -f builder/web.py ext_mod/lcd_bus/sdl_bus/sdl_bus.h.rej \ + && rm -rf ext_mod/_webnet ) +( cd lvgl_micropython/lib/micropython && git checkout -- ports/unix/gccollect.c ) +scripts/build_mpos_web.sh +``` + +The two C patches were verified to apply cleanly (`patch --forward`, exit 0, no `.rej`) against the pinned commits listed in *Build from a fresh clone*. + +## Build flags (in `scripts/web_port/web.py`) + +The Emscripten link flags (`web_ldflags`) are the main tuning surface: + +| Flag | Why | +| --- | --- | +| `-sUSE_SDL=2` | Use Emscripten's bundled SDL2 port. | +| `-sALLOW_MEMORY_GROWTH=1`, `-sINITIAL_MEMORY=...`, `-sMAXIMUM_MEMORY=...` | Heap sizing. | +| `-sSTACK_SIZE=8388608` | Larger stack for deep LVGL/MPOS call chains. | +| `-sASYNCIFY=1 -sASYNCIFY_STACK_SIZE=32768` | **Required** for `emscripten_scan_registers()` in the GC fix. | +| `-sFORCE_FILESYSTEM=1 -sEXIT_RUNTIME=0` | Keep the runtime + virtual FS alive. | +| `-Wl,--allow-multiple-definition` | MicroPython's libc `printf` vs Emscripten libc `printf` duplicate. | +| `-sASSERTIONS=2 --profiling-funcs` | **Debug only.** Symbolized stacks. Remove for an optimized production build. | + +Module disabling and SDL include/link selection happen via `MPOS_WEB=1` and the `MICROPY_PY_*=0` make variables, also set in `web.py`. + +## Web-only Python shims (staged into `lib/`) + +Written by `build_mpos.sh` into `web/.preload_internal_filesystem/lib/` so they shadow frozen/native modules at runtime without affecting device builds: + +| Shim | Replaces | Behavior | +| --- | --- | --- | +| `_thread.py` | native `_thread` | Cooperative: runs thread bodies as asyncio tasks; locks are no-ops; `get_ident()==1`. | +| `socket.py` | native `socket` | Stub; raises `OSError` on use (no browser raw sockets). | +| `_webrepl.py` / `websocket.py` | native modules | Stubs; raise `OSError` on use. | +| `task_handler.py` | frozen `task_handler` | Drives `lv.task_handler()` + `lv.tick_inc()` from an asyncio task instead of `machine.Timer`. Same public API (`TaskHandler`, `TASK_HANDLER_STARTED/FINISHED`, `add_event_cb`, `disable/enable`, `deinit`). | +| `_web_machine_timer.py` | `machine.Timer` | asyncio-backed periodic/one-shot timer. | + +### `machine.Timer` injection + +The native `machine` module dict is read-only, so `machine.Timer = ...` fails. Instead, `build_mpos.sh` patches the **staged** copy of `main.py` (never the device source) right after `sys.path.insert(0, "lib")` to replace `sys.modules["machine"]` with a thin wrapper exposing `Timer` and delegating all other attributes to the native module. + +## Networking (HTTP via browser `fetch()`) + +The browser has no raw TCP/UDP sockets, so `socket` is stubbed and `MICROPY_PY_SOCKET=0`. HTTP instead goes through the browser's `fetch()` API via a small native module plus an `aiohttp` shim: + +| Piece | Location | Role | +| --- | --- | --- | +| `_webnet` native module | `scripts/web_port/ext_mod/_webnet/webnet.c` (+ `micropython.mk`) | C/`EM_JS` bridge to the browser `fetch()` and `WebSocket` APIs. Non-blocking, poll-based so the asyncio/UI loop keeps running. Built only for web (`MPOS_WEB=1`); copied into `lvgl_micropython/ext_mod/_webnet/` by `build_mpos.sh`. | +| `aiohttp` shim | staged `lib/aiohttp/__init__.py` (heredoc in `build_mpos.sh`) | Re-implements `ClientSession.get/post/put/...` on top of `_webnet`'s fetch bridge, and `ClientSession.ws_connect()` on top of `_webnet`'s WebSocket bridge, polling with `await asyncio.sleep_ms(...)`. Imports `WSMsgType` from the device `aiohttp_ws.py`. | + +`_webnet` HTTP API: `fetch_start(method, url, headers_json, body)` returns an int handle; then `poll(h)` (0 pending / 1 done / -1 error), `status(h)`, `headers(h)`, `body(h)`, `error(h)`, and `free(h)`. + +`_webnet` WebSocket API: `ws_open(url, protocols_json)` returns a handle; `ws_state(h)` (0 connecting / 1 open / 2 closing / 3 closed), `ws_peek_type(h)` (0 none / 1 text / 2 binary), `ws_peek_len(h)`, `ws_read(h)` (pops the front message as bytes), `ws_send_text(h, str)`, `ws_send_bytes(h, bytes)`, `ws_close(h)`, `ws_error(h)`, and `ws_free(h)`. + +**Limitations:** + +- **CORS applies.** Cross-origin requests fail unless the server sends `Access-Control-Allow-Origin`. The default app-store/OTA hosts do not, so those downloads fail in the browser with `fetch failed: TypeError: Failed to fetch` — this is a server-side policy, not a port bug. Same-origin or CORS-enabled endpoints work. +- **WebSockets work**, via the browser `WebSocket` API (`ClientSession.ws_connect`). Unlike `fetch()`, cross-origin WebSockets are not blocked by CORS (the server decides via the `Origin` header), so e.g. Nostr relays connect. Limitation: the browser WebSocket API cannot set custom request headers, so any `headers` passed to `ws_connect`/`ClientSession` are ignored for the WS handshake. + +## Persistence (writable FS via IndexedDB / IDBFS) + +The bulk of the filesystem (`/lib`, `/builtin`, `main.py`) is baked read-only into `micropython.data` at link time. Two paths are instead mounted from the browser's IndexedDB so writes survive a page reload: + +| Path | Backing | Contents | +| --- | --- | --- | +| `/data` | IDBFS (IndexedDB) | App preferences / config (`SharedPreferences` writes `data//config.json`). | +| `/apps` | IDBFS (IndexedDB) | User-installed apps (`AppManager` installs to `apps/`) and a one-time copy of the bundled demo apps. | + +How it is wired up: + +- **Link flag:** `-lidbfs.js` is added to `web_ldflags` in `scripts/web_port/web.py` so the IDBFS backend is available. +- **Mount + load (boot):** `web/shell.html`'s `Module.preRun` mounts IDBFS at `/data` and `/apps`, then gates the runtime start on `FS.syncfs(true, …)` (wrapped in `addRunDependency`/`removeRunDependency`) so Python only starts once the persisted contents are loaded from IndexedDB. +- **Why those two paths are excluded from the preload:** `FS.syncfs(true)` reconciles the in-memory FS to match the IndexedDB store, so a mount point must not also be a `--preload-file` target — otherwise the first boot (empty store) would wipe the preloaded files. `build_mpos.sh` therefore stages the tree **without** `apps/` and `data/`: the bundled demo apps are packaged separately at `/.bundled_apps` (`--preload-file …@/.bundled_apps`) and `data/` is dropped (IDBFS recreates it empty). +- **Seeding bundled apps:** on first run `seedBundledApps()` copies `/.bundled_apps` into `/apps` once and writes a `/apps/.seeded` marker, so the bundled apps appear via the normal `AppManager` scan. The marker means a user uninstalling a bundled app makes the removal stick across reloads instead of being re-seeded every boot. +- **Flushing writes:** `startPersistFlush()` periodically calls `FS.syncfs(false, …)` (every few seconds, plus on `pagehide` and when the tab is hidden) to push writes back to IndexedDB. + +Notes / limitations: + +- Persistence is per-origin and subject to the browser's IndexedDB storage quota and eviction policy (clearing site data wipes it). +- No device-side code changed: `AppManager` install/uninstall and `SharedPreferences` use their normal relative paths (`apps/…`, `data/…`), which resolve to the IDBFS mounts because the working directory is `/`. + +## Making changes — common scenarios + +- **Change MPOS Python code / apps:** edit under `internal_filesystem/` as usual, then rebuild (`scripts/build_mpos_web.sh`). The staged FS is rebuilt every time, so changes are picked up. (Most of the FS is baked into `micropython.data` at link time; a rebuild is required — there is no live file mount. `/data` and `/apps` are the exception: they persist in IndexedDB across reloads — see "Persistence".) +- **Change the HTML/JS shell:** edit `web/shell.html`, rebuild. `index.html` and `mpos.html` are regenerated from it. +- **Change build/link flags:** edit `scripts/web_port/web.py`, rebuild. +- **Change a patched submodule C file:** see "Updating a submodule patch". +- **Add another web-only shim:** add a heredoc in the `web` branch of `scripts/build_mpos.sh` next to the existing shims. + +## Testing in a browser + +Serve with `scripts/run_web.sh` and open the page. When iterating, note that `micropython.data` (the preloaded FS) is cached **separately** from the wasm, so hard-reload / disable cache (or append a cache-busting query like `?v=2`) when testing filesystem changes. + +A clean boot prints the banner, the `RAM: ... free` line, then `Passing execution over to mpos.main`, and renders the launcher. The following console messages are **expected and non-fatal** in the browser: + +- `no ADC` / `module 'machine' has no attribute 'ADC'` (no battery ADC) +- `mpos.imu.drivers.iio: Error listing dir` (no IMU) +- `download error` / `fetch failed: TypeError: Failed to fetch` for the default app-store/OTA hosts (CORS-blocked cross-origin requests — see *Networking*). +- `no module named 'esp32'` +- `aiorepl ... EIO` (browser stdin cannot be read) + +A **fatal** problem would instead show `memory access out of bounds`, `function signature mismatch`, or `MicroPythonOS exiting`. + +## Production build checklist + +- Remove the debug flags `-sASSERTIONS=2 --profiling-funcs` from `web_ldflags` in `scripts/web_port/web.py`. +- Rebuild and serve the contents of `web/` from any static host. + +## Deploying to GitHub Pages + +[`scripts/deploy_web_pages.sh`](https://github.com/MicroPythonOS/MicroPythonOS/blob/main/scripts/deploy_web_pages.sh) builds the web export and force-pushes the contents of `web/` as a single commit to a `gh-pages` branch. It is a plain command (no GitHub Action / CI), so you run it whenever you want to update the live site. + +```bash +scripts/deploy_web_pages.sh # build, then deploy to remote `fork` +scripts/deploy_web_pages.sh --no-build # deploy the existing web/ as-is +REMOTE=origin scripts/deploy_web_pages.sh # deploy to a different remote +BRANCH=gh-pages scripts/deploy_web_pages.sh # use a different branch +``` + +One-time setup on GitHub: **Settings → Pages → Build and deployment → Source: "Deploy from a branch", Branch: `gh-pages` / `(root)`**. The site is then served at `https://.github.io//`. + +The script adds a `.nojekyll` file so Pages serves the underscore-prefixed `.preload_internal_filesystem/` directory verbatim (Jekyll would otherwise skip it). The `gh-pages` branch holds only generated artifacts and is overwritten on every deploy. diff --git a/docs/web-port/using.md b/docs/web-port/using.md new file mode 100644 index 0000000..6e8e10b --- /dev/null +++ b/docs/web-port/using.md @@ -0,0 +1,36 @@ +# Using the Web Port + +MicroPythonOS can run entirely in a web browser thanks to a WebAssembly build. This is the easiest way to try the latest bleeding-edge code without flashing hardware. + +## Live demo + +The latest `main` branch is automatically built and published to GitHub Pages: + +**https://micropythonos.github.io/MicroPythonOS/** + +Open that link in a modern desktop browser and the OS will boot directly into the launcher. + +## Refreshing the filesystem + +The web port stores writable data (`/data` and `/apps`) in the browser's IndexedDB. The read-only system files are baked into the WebAssembly preload. If you want a completely clean start — for example, after a major upstream change or to clear stale app data — you need to wipe the stored filesystem: + +1. Open the developer tools (`F12`). +2. Go to **Application** → **Storage** → **Local Storage / IndexedDB** (or the equivalent in your browser). +3. Clear site data / delete everything for `micropythonos.github.io`. +4. Close the tab or press `F5` to reload the page. + +After the reload the OS will boot with a fresh filesystem. The bundled demo apps will be re-seeded on first run. + +## Limitations in the browser + +The web port is a faithful approximation of the real OS, but a browser is not a microcontroller. Expect the following differences: + +- **No raw network sockets** — HTTP is handled through the browser's `fetch()` API, and CORS applies to cross-origin requests. +- **No Bluetooth, ADC, IMU, or camera** — hardware-specific features either raise errors or return stubs. +- **Persistence is per-origin** — clearing site data wipes `/data` and `/apps`. + +For everything else, the web port behaves like a desktop build: the same apps, the same launcher, and the same Python runtime. + +## Reporting issues + +If the live demo fails to boot, open the browser console and look for fatal errors such as `memory access out of bounds`, `function signature mismatch`, or `MicroPythonOS exiting`. Transient messages like `no ADC`, `fetch failed`, or `aiorepl ... EIO` are expected and harmless. diff --git a/mkdocs.yml b/mkdocs.yml index f3b7fb2..700030b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -94,6 +94,9 @@ nav: - Automated Testing: os-development/automated-testing.md - Emulated ESP32: os-development/emulating-esp32-on-desktop.md - Porting Guide: os-development/porting-guide.md + - Web Port: + - Using the Web Port: web-port/using.md + - Developer Info: web-port/developer.md - Other: - Release Process: other/release-checklist.md - Merge Checklist: other/merge-checklist.md From aad0143fac1a2e4009ed36a896fd422463b3ee04 Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Thu, 2 Jul 2026 15:16:37 +0200 Subject: [PATCH 103/110] Remove build_mpos_web.sh --- docs/web-port/developer.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/docs/web-port/developer.md b/docs/web-port/developer.md index 68e0ca0..e80b86e 100644 --- a/docs/web-port/developer.md +++ b/docs/web-port/developer.md @@ -9,7 +9,7 @@ This section describes how MicroPythonOS runs in the browser, how to build and s # The build auto-activates ../emsdk or ../../emsdk if emcc is not on PATH. # 2. Build the web target. -scripts/build_mpos_web.sh +scripts/build_mpos.sh web # 3. Build (if needed) and serve locally at http://localhost:8080/ scripts/run_web.sh @@ -21,7 +21,7 @@ Output artifacts land in `web/`: `micropython.{html,js,wasm,data}`, plus copies ## Prerequisites -- **Emscripten SDK** (tested with **6.0.0**). Either have `emcc` on `PATH`, or place an activated `emsdk` checkout one or two directories above this repo (`../emsdk` or `../../emsdk`). `scripts/build_mpos_web.sh` sources `emsdk_env.sh` automatically when `emcc` is missing. +- **Emscripten SDK** (tested with **6.0.0**). Either have `emcc` on `PATH`, or place an activated `emsdk` checkout one or two directories above this repo (`../emsdk` or `../../emsdk`). `scripts/build_mpos.sh web` sources `emsdk_env.sh` automatically when `emcc` is missing. - Standard host toolchain to build `mpy-cross` (built with the host compiler, not emcc) and the usual MicroPython build dependencies (`python3`, `make`). - The git submodules must be checked out (`git submodule update --init --recursive`). A **clean** submodule checkout is fine — the build re-applies the web changes every time. @@ -40,7 +40,7 @@ cd MicroPythonOS # (emcc on PATH, or an activated emsdk at ../emsdk or ../../emsdk). # 3. Build and serve. -scripts/build_mpos_web.sh +scripts/build_mpos.sh web scripts/run_web.sh ``` @@ -89,7 +89,6 @@ Key design points: | Path | Purpose | | --- | --- | | [`scripts/build_mpos.sh`](https://github.com/MicroPythonOS/MicroPythonOS/blob/main/scripts/build_mpos.sh) | Central build orchestration. The `web` target branch does all web-specific work (patching the submodule, staging the FS, injecting shims, invoking `make.py web`, collecting artifacts). | -| [`scripts/build_mpos_web.sh`](https://github.com/MicroPythonOS/MicroPythonOS/blob/main/scripts/build_mpos_web.sh) | Convenience wrapper: activates emsdk then runs `build_mpos.sh web`. | | [`scripts/run_web.sh`](https://github.com/MicroPythonOS/MicroPythonOS/blob/main/scripts/run_web.sh) | Build (optional) + serve `web/` with `python3 -m http.server`. | | [`scripts/web_port/web.py`](https://github.com/MicroPythonOS/MicroPythonOS/blob/main/scripts/web_port/web.py) | The Emscripten build backend. Copied into `lvgl_micropython/builder/web.py` at build time; consumed by `make.py web`. | | [`scripts/web_port/sdl_bus.h.patch`](https://github.com/MicroPythonOS/MicroPythonOS/blob/main/scripts/web_port/sdl_bus.h.patch) | C struct-layout fix applied to the `lcd_bus` SDL bus (see "Submodule patches"). | @@ -134,7 +133,7 @@ To verify reproducibility, revert the submodule files and rebuild — the build && rm -f builder/web.py ext_mod/lcd_bus/sdl_bus/sdl_bus.h.rej \ && rm -rf ext_mod/_webnet ) ( cd lvgl_micropython/lib/micropython && git checkout -- ports/unix/gccollect.c ) -scripts/build_mpos_web.sh +scripts/build_mpos.sh web ``` The two C patches were verified to apply cleanly (`patch --forward`, exit 0, no `.rej`) against the pinned commits listed in *Build from a fresh clone*. @@ -213,7 +212,7 @@ Notes / limitations: ## Making changes — common scenarios -- **Change MPOS Python code / apps:** edit under `internal_filesystem/` as usual, then rebuild (`scripts/build_mpos_web.sh`). The staged FS is rebuilt every time, so changes are picked up. (Most of the FS is baked into `micropython.data` at link time; a rebuild is required — there is no live file mount. `/data` and `/apps` are the exception: they persist in IndexedDB across reloads — see "Persistence".) +- **Change MPOS Python code / apps:** edit under `internal_filesystem/` as usual, then rebuild (`scripts/build_mpos.sh web`). The staged FS is rebuilt every time, so changes are picked up. (Most of the FS is baked into `micropython.data` at link time; a rebuild is required — there is no live file mount. `/data` and `/apps` are the exception: they persist in IndexedDB across reloads — see "Persistence".) - **Change the HTML/JS shell:** edit `web/shell.html`, rebuild. `index.html` and `mpos.html` are regenerated from it. - **Change build/link flags:** edit `scripts/web_port/web.py`, rebuild. - **Change a patched submodule C file:** see "Updating a submodule patch". From 49775424a01c72f25609ed449f078dba70398cc4 Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Thu, 2 Jul 2026 15:23:09 +0200 Subject: [PATCH 104/110] Compressed is also supported --- docs/apps/bundling-apps.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/apps/bundling-apps.md b/docs/apps/bundling-apps.md index 8187ead..459463f 100644 --- a/docs/apps/bundling-apps.md +++ b/docs/apps/bundling-apps.md @@ -19,7 +19,10 @@ MicroPythonOS validates this layout while extracting, and rejects packages that ## Creating an .mpk -From the parent directory that contains your app folder, create a stored (uncompressed) ZIP that includes the top-level folder. It's recommended to make the `.mpk` deterministic by setting file timestamps to a fixed value, sorting entries, and excluding extra file attributes: +From the parent directory that contains your app folder, create a ZIP that includes the top-level folder. +You can usually do that by just right-clicking the folder (like `com.example.yourapp`) in your file explorer and choosing "Add to .zip" or "Compress". + +Although you don't have to, it's recommended to make the `.mpk` deterministic by setting file timestamps to a fixed value, sorting entries, and excluding extra file attributes. That way, your .mpk will only change if the actual contents changed. Here's a script to do that: ```bash cd internal_filesystem/apps/ From 5cc0b58891af733c43846ecae1ea7f4418cf2895 Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Thu, 2 Jul 2026 19:30:38 +0200 Subject: [PATCH 105/110] update docs --- docs/getting-started/supported-hardware.md | 4 ++-- docs/web-port/using.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/getting-started/supported-hardware.md b/docs/getting-started/supported-hardware.md index a4fcaa0..345c0fc 100644 --- a/docs/getting-started/supported-hardware.md +++ b/docs/getting-started/supported-hardware.md @@ -13,8 +13,8 @@ MicroPythonOS runs on a variety of platforms, from microcontrollers to desktops. ## ESP32-S3 Microcontrollers - [DFRobot UniHiker K10](https://www.dfrobot.com/product-2676.html): Fully Supported -- [Fri3d Camp 2024 Badge](https://fri3d.be/badge/2024/): Fully Supported -- [Fri3d Camp 2026 Badge](https://fri3d.be/badge/2026/): Experimental Support (Hardware Prototype Phase) +- [Fri3d Camp 2024 Badge](https://fri3d.be/badge/2024/): Fully Supported (including Communicator Add-On) +- [Fri3d Camp 2026 Badge](https://fri3d.be/badge/2026/): Fully Supported (including Communicator Add-On) - [Freenove ESP32-S3 Display](https://github.com/Freenove/Freenove_ESP32_S3_Display): Fully Supported - [LilyGo T-Display S3](https://lilygo.cc/products/t-display-s3): Fully Supported - [LilyGo T-HMI](https://lilygo.cc/en-us/products/t-hmi): Fully Supported diff --git a/docs/web-port/using.md b/docs/web-port/using.md index 6e8e10b..dfbb3d4 100644 --- a/docs/web-port/using.md +++ b/docs/web-port/using.md @@ -6,7 +6,7 @@ MicroPythonOS can run entirely in a web browser thanks to a WebAssembly build. T The latest `main` branch is automatically built and published to GitHub Pages: -**https://micropythonos.github.io/MicroPythonOS/** +**[MicroPythonOS.github.io/MicroPythonOS](https://MicroPythonOS.github.io/MicroPythonOS/)** Open that link in a modern desktop browser and the OS will boot directly into the launcher. From 33f1bb6b226040baa2f02e32d3125a23ea1ca7ee Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Thu, 2 Jul 2026 23:29:34 +0200 Subject: [PATCH 106/110] Update docs --- docs/apps/badgehub.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/apps/badgehub.md b/docs/apps/badgehub.md index ef64590..4a45ee6 100644 --- a/docs/apps/badgehub.md +++ b/docs/apps/badgehub.md @@ -36,8 +36,10 @@ The AppManager uses the latest release's download URL to fetch the `.mpk`. 1. Build your `.mpk` following the [Bundling Apps](bundling-apps.md) guide. The top-level folder in the ZIP must match the app's `fullname` exactly. 2. Ensure the archive contains a valid `MANIFEST.JSON`. -3. Upload the `.mpk` to your BadgeHub project. -4. MicroPythonOS will detect the new release the next time the AppStore refreshes the BadgeHub backend. +3. If it's the first time, fill out the "Create Project" form on BadgeHub.eu after logging in +4. Make sure you select **`mpos_api_0`** under "Badge" to indicate your app is built for MicroPythonOS +5. Upload the `.mpk` to your BadgeHub project. +6. MicroPythonOS will detect the new release the next time the AppStore refreshes the BadgeHub backend. ## Version handling From 7c50e39e845f83a40e90c4b4a558b9e694f203aa Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Fri, 3 Jul 2026 11:00:25 +0200 Subject: [PATCH 107/110] Improve docs --- AGENTS.md | 18 ++++ docs/getting-started/running.md | 4 +- docs/os-development/running-on-desktop.md | 118 ++++++++++++++++------ 3 files changed, 105 insertions(+), 35 deletions(-) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..e92c29d --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,18 @@ +# AGENTS.md + +This file provides guidance to agents when working with documentation in this repository. + +## Where the docs live + +This repository (`../docs/` relative to the MicroPythonOS code repo) is the public documentation site for MicroPythonOS. It is built with MkDocs. + +The source Markdown files are under `docs/`. `mkdocs.yml` at the repository root drives navigation and includes. + +## Included files + +Some Markdown files are intentionally included by other pages rather than listed directly in `mkdocs.yml`. Examples in `docs/os-development/`: + +- `compiling.md` is included by `linux.md` and `macos.md`. +- `running-on-desktop.md` is included by `linux.md` and `macos.md`. + +This is why `mkdocs build` warns "The following pages exist ... but are not included in the nav" for those files. Do not add them to `nav` unless explicitly requested. diff --git a/docs/getting-started/running.md b/docs/getting-started/running.md index f1d1597..d46411b 100644 --- a/docs/getting-started/running.md +++ b/docs/getting-started/running.md @@ -14,9 +14,9 @@ For advanced usage, such as installing development builds without any files, see ## Running on desktop -MicroPythonOS desktop builds do not require a full git clone. A single pre-built binary contains the frozen filesystem, so you can download it, make it executable, and run it. +On desktop you can choose how deep you want to go: run a pre-built binary, develop apps using a pre-built binary with a source checkout, or build the operating system from source. -For detailed steps, see [Running on Desktop](../os-development/running-on-desktop.md). +See [Running on Desktop](../os-development/running-on-desktop.md) for detailed steps. ## Next Steps diff --git a/docs/os-development/running-on-desktop.md b/docs/os-development/running-on-desktop.md index f980408..808cc34 100644 --- a/docs/os-development/running-on-desktop.md +++ b/docs/os-development/running-on-desktop.md @@ -1,68 +1,120 @@ ## Running on desktop -Desktop builds of MicroPythonOS can run without a local source checkout. The `internal_filesystem/` is frozen into the binary at build time, so a single pre-built executable is enough to try the OS. +MicroPythonOS runs on Linux, macOS and Raspberry Pi desktops. The desktop build is a single executable that contains the whole OS, so you usually do not need to compile anything. -### Download a pre-built binary +Pick the level that matches what you want to do. -1. Go to the [releases page](https://github.com/MicroPythonOS/MicroPythonOS/releases). -2. Download the binary for your platform: - - Linux / WSL2 on Windows: `lvgl_micropy_unix` - - macOS (Apple Silicon or Intel): `lvgl_micropy_macOS` -3. Make it executable: +--- -``` +### Level 1: Just want to run it? + +Grab a pre-built binary and launch it. + +1. Download the binary for your platform from the [releases page](https://github.com/MicroPythonOS/MicroPythonOS/releases): + - **Linux, Raspberry Pi, WSL2 on Windows:** `lvgl_micropy_unix` + - **macOS (Apple Silicon or Intel):** `lvgl_micropy_macOS` +2. Make it executable: + +```bash chmod +x lvgl_micropy_unix ``` -4. Place it where `scripts/run_desktop.sh` expects it. The script looks for: - - `lvgl_micropython/build/lvgl_micropy_unix` on Linux - - `lvgl_micropython/build/lvgl_micropy_macOS` on macOS +3. Run it. From a terminal: + +```bash +./lvgl_micropy_unix +``` + +Or double-click it in your file manager. You may want to rename it to `MicroPythonOS` first. + +--- + +### Level 2: Want to develop an app but use a prebuilt OS? -You can create that folder and copy the binary there: +You can use a downloaded OS binary with a full source checkout so you can edit apps and see changes immediately. +1. Clone the repository: + +```bash +git clone --recurse-submodules https://github.com/MicroPythonOS/MicroPythonOS.git +cd MicroPythonOS ``` + +2. Download the binary for your platform from the [releases page](https://github.com/MicroPythonOS/MicroPythonOS/releases). Rename it and put it where the runner script expects it: + - Linux / Raspberry Pi / WSL2 → `lvgl_micropython/build/lvgl_micropy_unix` + - macOS → `lvgl_micropython/build/lvgl_micropy_macOS` + +```bash mkdir -p lvgl_micropython/build -cp /path/to/downloaded/lvgl_micropy_unix lvgl_micropython/build/lvgl_micropy_unix +cp /path/to/downloaded/binary lvgl_micropython/build/lvgl_micropy_unix +chmod +x lvgl_micropython/build/lvgl_micropy_unix ``` -5. Run it: +3. Run it with the helper script: -``` +```bash ./scripts/run_desktop.sh ``` -### Build from source +`scripts/run_desktop.sh` launches the OS using the Python files in `internal_filesystem/` directly, so any edit you make there appears the next time you restart the app. No rebuild is needed. -If you want to modify the OS itself or run the very latest code, you can [build it from source](compiling.md). The built binary will already be in `lvgl_micropython/build/lvgl_micropy_XXX` where `XXX` is `unix` or `macOS`. +**Try it:** -### Notes on MacOS +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 change immediately -If you get an error about a missing `/opt/homebrew/opt/libffi/lib/libffi.8.dylib` then fix that with: `brew install libffi` +Once your app works on desktop, deploy it to a physical device with [Installing on ESP32](installing-on-esp32.md). -If you get an error about the code being unsigned, then allow it like this: +--- -![Allow Anyway on MacOS](/os-development/macos-allow-anyway.png) +### Level 3: Want to build the OS yourself? +If you need to change the OS itself, add C extensions, modify MicroPython/LVGL bindings, or run the very latest code, build from source. The binary will already land in `lvgl_micropython/build/lvgl_micropy_XXX` where `XXX` is `unix` or `macOS`. -## Making Changes on Desktop +```bash +./scripts/build_mpos.sh unix # Linux, Raspberry Pi, WSL2 +./scripts/build_mpos.sh macOS # macOS +``` -If you do have a source checkout, you can still run the OS directly from `internal_filesystem/`. When you run `./scripts/run_desktop.sh`, the OS runs the MicroPythonOS scripts **directly from `internal_filesystem/`**. This means: +See [Compiling](compiling.md) for the full instructions, including cloning and dependencies. -- **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 +--- -**Try it yourself:** +## Known issues and fixes -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! +### Linux: "No such file or directory" when running the binary + +Make sure it is executable: + +```bash +chmod +x lvgl_micropy_unix +``` + +### macOS: missing `libffi.8.dylib` + +Install libffi: + +```bash +brew install libffi +``` + +### macOS: "cannot be opened because the developer cannot be verified" + +The prebuilt binary is not signed. Open **System Settings → Privacy & Security** and click **Allow Anyway** next to the blocked item, then run it again. + +![Allow Anyway on MacOS](/os-development/macos-allow-anyway.png) + +### Windows + +Native Windows builds are not supported. [Users report](https://github.com/MicroPythonOS/MicroPythonOS/issues/31) that the Linux desktop binary works under WSL2 on Windows 11. Alternatively, you can try the [web port](../web-port/using.md), which runs a desktop build in the browser. +--- -## Making Changes on ESP32 +## Deploying to hardware -Once you've tested your changes on desktop and they work correctly, or you're doing things you can't test on desktop, then you can deploy to physical hardware. +Once your app works on desktop, install it on a supported ESP32 device. {!os-development/installing-on-esp32.md!} From c77c47dbb5ea14f027a5ff4fc0b8aa490df209a6 Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Tue, 7 Jul 2026 19:51:02 +0200 Subject: [PATCH 108/110] Add WebAssembly info --- docs/getting-started/running.md | 8 +++++++- docs/getting-started/supported-hardware.md | 8 ++++++++ docs/os-development/compiling.md | 2 +- docs/web-port/using.md | 8 +++++--- 4 files changed, 21 insertions(+), 5 deletions(-) diff --git a/docs/getting-started/running.md b/docs/getting-started/running.md index d46411b..0d1c420 100644 --- a/docs/getting-started/running.md +++ b/docs/getting-started/running.md @@ -1,6 +1,6 @@ # Running it -MicroPythonOS can run on supported microcontrollers (e.g., ESP32) and on desktop systems (Linux, Raspberry Pi, MacOS, Windows etc). +MicroPythonOS can run on supported microcontrollers (e.g., ESP32), on desktop systems (Linux, Raspberry Pi, MacOS, Windows), and in a web browser via WebAssembly. To install prebuilt software, read on! @@ -18,6 +18,12 @@ On desktop you can choose how deep you want to go: run a pre-built binary, devel See [Running on Desktop](../os-development/running-on-desktop.md) for detailed steps. +## Running in the browser + +Open **[web.micropythonos.com](https://web.micropythonos.com/)** for the latest release, or **[micropythonos.github.io/MicroPythonOS](https://micropythonos.github.io/MicroPythonOS/)** for the bleeding-edge main branch. No install required. + +See [Using the Web Port](../web-port/using.md) for details and limitations. + ## Next Steps - 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 345c0fc..64ac8e0 100644 --- a/docs/getting-started/supported-hardware.md +++ b/docs/getting-started/supported-hardware.md @@ -23,6 +23,14 @@ MicroPythonOS runs on a variety of platforms, from microcontrollers to desktops. - [SQUiXL](https://squixl.io): Fully Supported - [Waveshare ESP32-S3-Touch-LCD-2](https://www.waveshare.com/wiki/ESP32-S3-Touch-LCD-2): Fully Supported +## WebAssembly (Browser) + +- **Latest release**: [web.micropythonos.com](https://web.micropythonos.com/) +- **Main branch CI**: [micropythonos.github.io/MicroPythonOS](https://micropythonos.github.io/MicroPythonOS/) +- Runs entirely in a modern desktop browser — no install, no flash. +- Built from source with `./scripts/build_mpos.sh web` (requires [Emscripten SDK](../web-port/developer/#prerequisites)). +- See [CI config](https://github.com/MicroPythonOS/MicroPythonOS/blob/main/.github/workflows/web-pages.yml) for automated build details. + ## Desktop Computers - **Linux**: Supported using SDL for display handling. diff --git a/docs/os-development/compiling.md b/docs/os-development/compiling.md index b69987a..eff418b 100644 --- a/docs/os-development/compiling.md +++ b/docs/os-development/compiling.md @@ -32,7 +32,7 @@ Usage: ./scripts/build_mpos.sh ``` -**Target systems**: `esp32`, `esp32s3`, `unix` (= Linux) and `macOS` +**Target systems**: `esp32`, `esp32s3`, `unix` (= Linux), `macOS` and `web` (= WebAssembly) **Examples**: diff --git a/docs/web-port/using.md b/docs/web-port/using.md index dfbb3d4..c9dad6b 100644 --- a/docs/web-port/using.md +++ b/docs/web-port/using.md @@ -15,9 +15,11 @@ Open that link in a modern desktop browser and the OS will boot directly into th The web port stores writable data (`/data` and `/apps`) in the browser's IndexedDB. The read-only system files are baked into the WebAssembly preload. If you want a completely clean start — for example, after a major upstream change or to clear stale app data — you need to wipe the stored filesystem: 1. Open the developer tools (`F12`). -2. Go to **Application** → **Storage** → **Local Storage / IndexedDB** (or the equivalent in your browser). -3. Clear site data / delete everything for `micropythonos.github.io`. -4. Close the tab or press `F5` to reload the page. +2. Go to **Application** → **Storage** (Chromium) or **Storage** → **IndexedDB** (Firefox). +3. Clear site data for the origin (e.g. `micropythonos.github.io` or `web.micropythonos.com`). +4. Reload the page. + +Alternatively, you can update installed apps through the built-in App Store without clearing storage. After the reload the OS will boot with a fresh filesystem. The bundled demo apps will be re-seeded on first run. From 97dde8634432147d0b2a84dd3e4304793e2225ab Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Mon, 13 Jul 2026 08:25:22 +0200 Subject: [PATCH 109/110] Update docs --- docs/architecture/frameworks.md | 26 +++- docs/frameworks/focus.md | 92 +++++++----- docs/frameworks/lights-manager.md | 213 +++++++++++++++------------ docs/frameworks/settings-activity.md | 6 +- 4 files changed, 198 insertions(+), 139 deletions(-) diff --git a/docs/architecture/frameworks.md b/docs/architecture/frameworks.md index 925838c..9040cbf 100644 --- a/docs/architecture/frameworks.md +++ b/docs/architecture/frameworks.md @@ -213,16 +213,32 @@ class MyActivity(Activity): See [FileExplorerActivity](../frameworks/file-explorer-activity.md) for details. -### Focus Borders -Provides a single helper, `add_focus_border`, for drawing a focus border around any widget. It replaces the duplicated `FOCUSED`/`DEFOCUSED` handlers that used to exist in many apps. +### Focus Highlight +Provides a single helper, `add_focus_highlight`, for drawing a focus border or background tint around any widget. It replaces the duplicated `FOCUSED`/`DEFOCUSED` handlers that used to exist in many apps. ```python -from mpos import add_focus_border +from mpos import add_focus_highlight -add_focus_border(button, width=2) +# Border mode (default) +add_focus_highlight(button, width=2) + +# Background mode (keep existing border intact) +add_focus_highlight(button, mode="bg") +``` + +See [Focus Highlight](../frameworks/focus.md) for details. + +### LightsManager +Controls NeoPixel RGB LEDs on supported hardware. Provides buffered LED control, predefined notification colors, and frame-based animation support. + +```python +from mpos import lights + +lights.set_all(0, 0, 255) +lights.write() ``` -See [Focus Borders](../frameworks/focus.md) for details. +See [LightsManager](../frameworks/lights-manager.md) for details. ### CameraManager Provides access to camera hardware. diff --git a/docs/frameworks/focus.md b/docs/frameworks/focus.md index d68e0c7..44f4e2f 100644 --- a/docs/frameworks/focus.md +++ b/docs/frameworks/focus.md @@ -1,25 +1,37 @@ -# Focus Borders +# Focus Highlight MicroPythonOS provides a single, reusable helper for focus highlighting so that apps and framework screens do not have to duplicate the same `FOCUSED`/`DEFOCUSED` event handlers. ## Overview -`add_focus_border` registers two LVGL event callbacks on a widget: +`add_focus_highlight` registers two LVGL event callbacks on a widget: -- **`FOCUSED`**: draw a border around the widget and scroll it into view. -- **`DEFOCUSED`**: hide the border again. +- **`FOCUSED`**: draw a border around the widget (or tint its background) and scroll it into view. +- **`DEFOCUSED`**: remove the highlight again. The helper is re-exported from the top-level `mpos` module, so apps can import it directly. +Two highlight modes are available: + +| Mode | Effect | +|------|--------| +| `"border"` (default) | Draws a border around the widget on focus. | +| `"bg"` | Tints the widget's background on focus, leaving border styles intact. | + ## API ```python -from mpos import add_focus_border +from mpos import add_focus_highlight + +# Border mode (default) +add_focus_highlight(widget) +add_focus_highlight(widget, width=2) +add_focus_highlight(widget, width=2, color=lv.color_hex(0xFFFFFF)) +add_focus_highlight(widget, width=2, opacity=lv.OPA._50, radius=5) -add_focus_border(widget) -add_focus_border(widget, width=2) -add_focus_border(widget, width=2, color=lv.color_hex(0xFFFFFF)) -add_focus_border(widget, width=2, opacity=lv.OPA._50, radius=5) +# Background mode +add_focus_highlight(widget, mode="bg") +add_focus_highlight(widget, mode="bg", color=lv.color_hex(0x444444)) ``` ### Parameters @@ -27,43 +39,35 @@ add_focus_border(widget, width=2, opacity=lv.OPA._50, radius=5) | Parameter | Default | Description | |-----------|---------|-------------| | `widget` | required | The LVGL object that should receive focus highlighting. | -| `width` | `1` | Border width in pixels when focused. | -| `color` | theme primary color | Border color. Falls back to `lv.theme_get_color_primary(None)` when omitted. | -| `opacity`| `None` | Optional `lv.OPA.*` value for the focused border. | -| `radius` | `None` | Optional corner radius for the focused border. | - -### Requirements - -For the callbacks to fire, the widget must be part of the default LVGL focus group: +| `mode` | `"border"` | Highlight mode: `"border"` draws a border, `"bg"` tints the background. | +| `width` | `1` | Border width in pixels when focused (ignored in `"bg"` mode). | +| `color` | theme primary color | Highlight color. Falls back to `lv.theme_get_color_primary(None)` when omitted. | +| `opacity` | `None` | Optional `lv.OPA.*` value for the focused border (ignored in `"bg"` mode). | +| `radius` | `None` | Optional corner radius for the focused border (ignored in `"bg"` mode). | -```python -focusgroup = lv.group_get_default() -if focusgroup: - focusgroup.add_obj(my_widget) -``` +### Widget and focus group -`add_focus_border` only draws the visual feedback; it does **not** add the widget to the focus group. +`add_focus_highlight` adds the widget to the default LVGL focus group automatically. You do not need to call `focusgroup.add_obj()` manually. ### Touch-aware behavior -On devices that are primarily controlled with a touchscreen, the focus border is intentionally hidden until the user first navigates with a directional input (keyboard, hardware buttons, or a rotary encoder). This keeps touch-only screens clean while still making focused elements obvious once physical navigation begins. +On devices that are primarily controlled with a touchscreen, the focus highlight is intentionally hidden until the user first navigates with a directional input (keyboard, hardware buttons, or a rotary encoder). This keeps touch-only screens clean while still making focused elements obvious once physical navigation begins. -If you want a widget to reveal the focus border immediately, ensure it is added to the default focus group and focus it programmatically: +If you want a widget to reveal the focus highlight immediately, focus it programmatically: ```python focusgroup = lv.group_get_default() if focusgroup: - focusgroup.add_obj(btn) focusgroup.focus_obj(btn) ``` ## Examples -### Basic usage +### Basic usage (border mode) ```python import lvgl as lv -from mpos import Activity, add_focus_border +from mpos import Activity, add_focus_highlight class MyActivity(Activity): def onCreate(self): @@ -74,38 +78,48 @@ class MyActivity(Activity): btn_label = lv.label(btn) btn_label.set_text("Focus me") - add_focus_border(btn) - - focusgroup = lv.group_get_default() - if focusgroup: - focusgroup.add_obj(btn) + add_focus_highlight(btn) self.setContentView(screen) ``` +### Background mode + +Use `mode="bg"` when the widget already has a persistent border that shouldn't be overwritten: + +```python +btn = lv.button(screen) +btn.set_style_border_width(2, lv.PART.MAIN) +btn.set_style_border_color(lv.color_hex(0x333333), lv.PART.MAIN) +add_focus_highlight(btn, mode="bg") +``` + ### Thicker, semi-transparent border ```python -add_focus_border(btn, width=3, opacity=lv.OPA._50, radius=5) +add_focus_highlight(btn, width=3, opacity=lv.OPA._50, radius=5) ``` ### Focus highlight on a label -Labels are not focusable by default, but they can be added to the focus group when they act as list rows: +Labels are not focusable by default, but they can be made focusable when they act as list rows: ```python row = lv.label(screen) row.set_text("Setting row") row.add_flag(lv.obj.FLAG.CLICKABLE) -add_focus_border(row, width=2) -focusgroup.add_obj(row) +add_focus_highlight(row, width=2) ``` +## Compatibility + +`add_focus_border` is kept as a compatibility wrapper for the pre‑0.15.0 API. It delegates to `add_focus_highlight` with `mode="border"`. New code should use `add_focus_highlight` directly. + ## When to use it -Use `add_focus_border` whenever you previously wrote a pair of `FOCUSED`/`DEFOCUSED` callbacks whose only job was to show or hide a border. It is used by the framework itself in places such as the launcher, settings screens, the top-menu drawer, `ImageView`, `MusicPlayer`, `ShowFonts`, `Connect4`, `About`, `HowTo`, and `WiFi Settings`. +Use `add_focus_highlight` whenever you previously wrote a pair of `FOCUSED`/`DEFOCUSED` callbacks whose only job was to show or hide a border or background tint. It is used by the framework itself in places such as the launcher, settings screens, the top-menu drawer, `ImageView`, `MusicPlayer`, `ShowFonts`, `Connect4`, `About`, `HowTo`, and `WiFi Settings`. -If you need additional behavior on focus (for example, starting a timer or loading data), keep your own callbacks and add `add_focus_border` alongside them. +If you need additional behavior on focus (for example, starting a timer or loading data), keep your own callbacks and add `add_focus_highlight` alongside them. ## See Also diff --git a/docs/frameworks/lights-manager.md b/docs/frameworks/lights-manager.md index 0ca850e..af93d36 100644 --- a/docs/frameworks/lights-manager.md +++ b/docs/frameworks/lights-manager.md @@ -11,10 +11,24 @@ LightsManager provides: - **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 +- **Low overhead** - Module-level functions (no singleton class) ⚠️ **Note**: LightsManager provides primitives for LED control, not built-in animations. Apps implement custom animations using the `update_frame()` pattern. +## Import + +```python +from mpos import lights +``` + +You can also use the submodule import if preferred: + +```python +import mpos.lights as LightsManager +``` + +All examples below use the recommended `from mpos import lights` style. + ## Hardware Support | Board | LEDs | GPIO | Notes | @@ -28,13 +42,12 @@ LightsManager provides: ### Check Availability ```python -from mpos import Activity -import mpos.lights as LightsManager +from mpos import Activity, lights class MyActivity(Activity): def onCreate(self): - if LightsManager.is_available(): - print(f"LEDs available: {LightsManager.get_led_count()}") + if lights.is_available(): + print(f"LEDs available: {lights.get_led_count()}") else: print("No LED hardware on this device") ``` @@ -42,17 +55,19 @@ class MyActivity(Activity): ### Control Individual LEDs ```python +from mpos import lights + # Set LED 0 to red (buffered) -LightsManager.set_led(0, 255, 0, 0) +lights.set_led(0, 255, 0, 0) # Set LED 1 to green (buffered) -LightsManager.set_led(1, 0, 255, 0) +lights.set_led(1, 0, 255, 0) # Set LED 2 to blue (buffered) -LightsManager.set_led(2, 0, 0, 255) +lights.set_led(2, 0, 0, 255) # Apply all changes to hardware -LightsManager.write() +lights.write() ``` **Important**: LEDs are **buffered**. Changes won't appear until you call `write()`. @@ -60,13 +75,15 @@ LightsManager.write() ### Control All LEDs ```python +from mpos import lights + # Set all LEDs to blue -LightsManager.set_all(0, 0, 255) -LightsManager.write() +lights.set_all(0, 0, 255) +lights.write() # Turn off all LEDs -LightsManager.clear() -LightsManager.write() +lights.clear() +lights.write() ``` ### Notification Colors @@ -74,14 +91,16 @@ LightsManager.write() Quick shortcuts for common colors: ```python +from mpos import lights + # 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 +lights.set_notification_color("red") # Error +lights.set_notification_color("green") # Success +lights.set_notification_color("blue") # Info +lights.set_notification_color("yellow") # Warning +lights.set_notification_color("orange") # Alert +lights.set_notification_color("purple") # Special +lights.set_notification_color("white") # General ``` ## Custom Animations @@ -94,19 +113,19 @@ Simple on/off blinking: ```python import time -import mpos.lights as LightsManager +from mpos import lights def blink_pattern(): """Blink all LEDs red 5 times.""" for _ in range(5): # Turn on - LightsManager.set_all(255, 0, 0) - LightsManager.write() + lights.set_all(255, 0, 0) + lights.write() time.sleep_ms(200) # Turn off - LightsManager.clear() - LightsManager.write() + lights.clear() + lights.write() time.sleep_ms(200) ``` @@ -115,6 +134,8 @@ def blink_pattern(): Display a rainbow pattern across all LEDs: ```python +from mpos import lights + def rainbow_cycle(): """Set each LED to a different rainbow color.""" colors = [ @@ -126,9 +147,9 @@ def rainbow_cycle(): ] for i, color in enumerate(colors): - LightsManager.set_led(i, *color) + lights.set_led(i, *color) - LightsManager.write() + lights.write() ``` ### Chase Effect @@ -137,25 +158,26 @@ LEDs light up in sequence: ```python import time +from mpos import lights def chase_effect(color=(0, 255, 0), delay_ms=100, loops=3): """Light up LEDs in sequence.""" - led_count = LightsManager.get_led_count() + led_count = lights.get_led_count() for _ in range(loops): for i in range(led_count): # Clear all - LightsManager.clear() + lights.clear() # Light current LED - LightsManager.set_led(i, *color) - LightsManager.write() + lights.set_led(i, *color) + lights.write() time.sleep_ms(delay_ms) # Clear after animation - LightsManager.clear() - LightsManager.write() + lights.clear() + lights.write() ``` ### Pulse/Breathing Effect @@ -164,6 +186,7 @@ Fade LEDs in and out smoothly: ```python import time +from mpos import lights def pulse_effect(color=(0, 0, 255), duration_ms=2000): """Pulse LEDs with breathing effect.""" @@ -177,8 +200,8 @@ def pulse_effect(color=(0, 0, 255), duration_ms=2000): g = int(color[1] * brightness) b = int(color[2] * brightness) - LightsManager.set_all(r, g, b) - LightsManager.write() + lights.set_all(r, g, b) + lights.write() time.sleep_ms(delay) # Fade out @@ -188,13 +211,13 @@ def pulse_effect(color=(0, 0, 255), duration_ms=2000): g = int(color[1] * brightness) b = int(color[2] * brightness) - LightsManager.set_all(r, g, b) - LightsManager.write() + lights.set_all(r, g, b) + lights.write() time.sleep_ms(delay) # Clear - LightsManager.clear() - LightsManager.write() + lights.clear() + lights.write() ``` ### Random Sparkle @@ -204,10 +227,11 @@ Random LEDs flash briefly: ```python import time import random +from mpos import lights def sparkle_effect(duration_ms=5000): """Random LEDs sparkle.""" - led_count = LightsManager.get_led_count() + led_count = lights.get_led_count() start_time = time.ticks_ms() while time.ticks_diff(time.ticks_ms(), start_time) < duration_ms: @@ -220,19 +244,19 @@ def sparkle_effect(duration_ms=5000): b = random.randint(0, 255) # Flash on - LightsManager.clear() - LightsManager.set_led(led, r, g, b) - LightsManager.write() + lights.clear() + lights.set_led(led, r, g, b) + lights.write() time.sleep_ms(100) # Flash off - LightsManager.clear() - LightsManager.write() + lights.clear() + lights.write() time.sleep_ms(50) # Clear after animation - LightsManager.clear() - LightsManager.write() + lights.clear() + lights.write() ``` ## Frame-Based LED Animations @@ -240,9 +264,8 @@ def sparkle_effect(duration_ms=5000): For smooth, real-time animations that integrate with the game loop, use the **TaskHandler event system**: ```python -from mpos import Activity +from mpos import Activity, lights import mpos.ui -import mpos.lights as LightsManager import time class LEDAnimationActivity(Activity): @@ -260,8 +283,8 @@ class LEDAnimationActivity(Activity): 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() + lights.clear() + lights.write() def update_frame(self, a, b): """Called every frame - update animation.""" @@ -273,20 +296,21 @@ class LEDAnimationActivity(Activity): self.last_time = current_time # Clear all LEDs - LightsManager.clear() + lights.clear() # Light up current LED - LightsManager.set_led(self.led_index, 0, 255, 0) - LightsManager.write() + lights.set_led(self.led_index, 0, 255, 0) + lights.write() # Move to next LED - self.led_index = (self.led_index + 1) % LightsManager.get_led_count() + self.led_index = (self.led_index + 1) % lights.get_led_count() ``` ### Smooth Color Cycle Animation ```python import math +from mpos import lights class ColorCycleActivity(Activity): def onCreate(self): @@ -301,8 +325,8 @@ class ColorCycleActivity(Activity): def onPause(self, screen): mpos.ui.task_handler.remove_event_cb(self.update_frame) - LightsManager.clear() - LightsManager.write() + lights.clear() + lights.write() def update_frame(self, a, b): current_time = time.ticks_ms() @@ -318,8 +342,8 @@ class ColorCycleActivity(Activity): r, g, b = self.hsv_to_rgb(self.hue, 1.0, 1.0) # Update all LEDs - LightsManager.set_all(r, g, b) - LightsManager.write() + lights.set_all(r, g, b) + lights.write() def hsv_to_rgb(self, h, s, v): """Convert HSV to RGB (h: 0-360, s: 0-1, v: 0-1).""" @@ -414,17 +438,21 @@ Set all LEDs to a predefined color and immediately apply (convenience method). **❌ Bad** - Calling `write()` after each LED: ```python +from mpos import lights + for i in range(5): - LightsManager.set_led(i, 255, 0, 0) - LightsManager.write() # 5 hardware updates! + lights.set_led(i, 255, 0, 0) + lights.write() # 5 hardware updates! ``` **✅ Good** - Set all LEDs then write once: ```python +from mpos import lights + for i in range(5): - LightsManager.set_led(i, 255, 0, 0) + lights.set_led(i, 255, 0, 0) -LightsManager.write() # 1 hardware update +lights.write() # 1 hardware update ``` ### Update Rate Recommendations @@ -445,7 +473,7 @@ def update_frame(self, a, b): if delta_time >= UPDATE_INTERVAL: self.last_time = current_time # Update LEDs here - LightsManager.write() + lights.write() ``` ### Cleanup in onPause() @@ -458,8 +486,8 @@ def onPause(self, screen): mpos.ui.task_handler.remove_event_cb(self.update_frame) # Clear LEDs - LightsManager.clear() - LightsManager.write() + lights.clear() + lights.write() ``` This prevents LEDs from staying lit after your app exits. @@ -476,11 +504,11 @@ This prevents LEDs from staying lit after your app exits. ```python # ❌ Wrong - no write() -LightsManager.set_all(255, 0, 0) +lights.set_all(255, 0, 0) # ✅ Correct -LightsManager.set_all(255, 0, 0) -LightsManager.write() +lights.set_all(255, 0, 0) +lights.write() ``` ### Flickering LEDs @@ -493,14 +521,14 @@ LightsManager.write() ```python # ❌ Bad - updating every frame (60 Hz) def update_frame(self, a, b): - LightsManager.set_all(random_color()) - LightsManager.write() # Too frequent! + lights.set_all(random_color()) + lights.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() + lights.set_all(random_color()) + lights.write() ``` 2. **Inconsistent timing** @@ -527,8 +555,8 @@ def onPause(self, screen): mpos.ui.task_handler.remove_event_cb(self.update_frame) # Clear LEDs - LightsManager.clear() - LightsManager.write() + lights.clear() + lights.write() ``` ### No LEDs on Waveshare Board @@ -540,7 +568,9 @@ def onPause(self, screen): **Solution**: - Check hardware before using LEDs: ```python - if LightsManager.is_available(): + from mpos import lights + + if lights.is_available(): # Use LEDs else: # Fallback to screen indicators or sounds @@ -554,25 +584,24 @@ def onPause(self, screen): 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) + lights.set_led(0, 255, 0, 0) # Red (R=255, G=0, B=0) + lights.set_led(1, 0, 255, 0) # Green (R=0, G=255, B=0) + lights.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) + lights.set_led(0, 300, 0, 0) # ✅ Correct - clamp to 0-255 - LightsManager.set_led(0, min(300, 255), 0, 0) + lights.set_led(0, min(300, 255), 0, 0) ``` ## Complete Example: LED Status Indicator ```python -from mpos import Activity -import mpos.lights as LightsManager +from mpos import Activity, lights import lvgl as lv class StatusIndicatorActivity(Activity): @@ -624,25 +653,25 @@ class StatusIndicatorActivity(Activity): def show_status(self, status_type): """Show visual status with LEDs.""" - if not LightsManager.is_available(): + if not lights.is_available(): print("No LEDs available") return if status_type == "success": - LightsManager.set_notification_color("green") + lights.set_notification_color("green") elif status_type == "error": - LightsManager.set_notification_color("red") + lights.set_notification_color("red") elif status_type == "warning": - LightsManager.set_notification_color("yellow") + lights.set_notification_color("yellow") elif status_type == "clear": - LightsManager.clear() - LightsManager.write() + lights.clear() + lights.write() def onPause(self, screen): # Clear LEDs when leaving app - if LightsManager.is_available(): - LightsManager.clear() - LightsManager.write() + if lights.is_available(): + lights.clear() + lights.write() ``` ## See Also diff --git a/docs/frameworks/settings-activity.md b/docs/frameworks/settings-activity.md index fc1d606..12e39c8 100644 --- a/docs/frameworks/settings-activity.md +++ b/docs/frameworks/settings-activity.md @@ -39,7 +39,7 @@ class MyApp(Activity): - **`ui_options`** (list): Options for `radiobuttons` and `dropdown` UI types - **`placeholder`** (string): Placeholder text for textarea input - **`changed_callback`** (function): Callback function called when the setting value changes -- **`should_show`** (function): Function to determine if this setting should be displayed in the list +- **`should_show`** (function): Boolean or function to determine if this setting should be displayed in the list - **`dont_persist`** (bool): If `True`, the setting won't be saved to SharedPreferences - **`min`** (int): Minimum value for `"slider"` UI (default: `0`) - **`max`** (int): Maximum value for `"slider"` UI (default: `100`) @@ -136,7 +136,7 @@ Use a custom Activity class for advanced UI implementations. ### Conditional Visibility with should_show -The `should_show` function allows you to conditionally display settings based on other settings or app state. +The `should_show` boolean or callable function allows you to conditionally display settings based on other settings or app state. **Function signature:** ```python @@ -371,7 +371,7 @@ SettingsActivity displays settings in a scrollable list with the following layou 1. **Load prefs once in onCreate()**: Load SharedPreferences in your main Activity's `onCreate()` and pass it to SettingsActivity. This is faster than loading it multiple times. -2. **Use should_show for conditional settings**: Hide settings that don't apply based on other settings using the `should_show` function. This keeps the UI clean and prevents confusion. +2. **Use should_show for conditional settings**: Hide settings that don't apply based on other settings using the `should_show` boolean/function. This keeps the UI clean and prevents confusion. 3. **Use changed_callback for side effects**: If changing a setting requires reloading data or updating the UI, use `changed_callback` instead of checking the value on resume. From badccb6ac092f88278d508d95f2d6179a3bd6614 Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Mon, 13 Jul 2026 11:23:40 +0200 Subject: [PATCH 110/110] Update docs --- docs/architecture/frameworks.md | 6 +- docs/frameworks/lights-manager.md | 210 +++++++++++++++--------------- 2 files changed, 105 insertions(+), 111 deletions(-) diff --git a/docs/architecture/frameworks.md b/docs/architecture/frameworks.md index 9040cbf..ce3c2fb 100644 --- a/docs/architecture/frameworks.md +++ b/docs/architecture/frameworks.md @@ -232,10 +232,10 @@ See [Focus Highlight](../frameworks/focus.md) for details. Controls NeoPixel RGB LEDs on supported hardware. Provides buffered LED control, predefined notification colors, and frame-based animation support. ```python -from mpos import lights +from mpos import LightsManager -lights.set_all(0, 0, 255) -lights.write() +LightsManager.set_all(0, 0, 255) +LightsManager.write() ``` See [LightsManager](../frameworks/lights-manager.md) for details. diff --git a/docs/frameworks/lights-manager.md b/docs/frameworks/lights-manager.md index af93d36..92e02df 100644 --- a/docs/frameworks/lights-manager.md +++ b/docs/frameworks/lights-manager.md @@ -11,23 +11,17 @@ LightsManager provides: - **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** - Module-level functions (no singleton class) +- **Low overhead** - Singleton class with direct LED control ⚠️ **Note**: LightsManager provides primitives for LED control, not built-in animations. Apps implement custom animations using the `update_frame()` pattern. ## Import ```python -from mpos import lights +from mpos import LightsManager ``` -You can also use the submodule import if preferred: - -```python -import mpos.lights as LightsManager -``` - -All examples below use the recommended `from mpos import lights` style. +All examples below use the recommended `from mpos import LightsManager` style. ## Hardware Support @@ -42,12 +36,12 @@ All examples below use the recommended `from mpos import lights` style. ### Check Availability ```python -from mpos import Activity, lights +from mpos import Activity, LightsManager class MyActivity(Activity): def onCreate(self): - if lights.is_available(): - print(f"LEDs available: {lights.get_led_count()}") + if LightsManager.is_available(): + print(f"LEDs available: {LightsManager.get_led_count()}") else: print("No LED hardware on this device") ``` @@ -55,19 +49,19 @@ class MyActivity(Activity): ### Control Individual LEDs ```python -from mpos import lights +from mpos import LightsManager # Set LED 0 to red (buffered) -lights.set_led(0, 255, 0, 0) +LightsManager.set_led(0, 255, 0, 0) # Set LED 1 to green (buffered) -lights.set_led(1, 0, 255, 0) +LightsManager.set_led(1, 0, 255, 0) # Set LED 2 to blue (buffered) -lights.set_led(2, 0, 0, 255) +LightsManager.set_led(2, 0, 0, 255) # Apply all changes to hardware -lights.write() +LightsManager.write() ``` **Important**: LEDs are **buffered**. Changes won't appear until you call `write()`. @@ -75,15 +69,15 @@ lights.write() ### Control All LEDs ```python -from mpos import lights +from mpos import LightsManager # Set all LEDs to blue -lights.set_all(0, 0, 255) -lights.write() +LightsManager.set_all(0, 0, 255) +LightsManager.write() # Turn off all LEDs -lights.clear() -lights.write() +LightsManager.clear() +LightsManager.write() ``` ### Notification Colors @@ -91,16 +85,16 @@ lights.write() Quick shortcuts for common colors: ```python -from mpos import lights +from mpos import LightsManager # Convenience method (sets all LEDs + calls write()) -lights.set_notification_color("red") # Error -lights.set_notification_color("green") # Success -lights.set_notification_color("blue") # Info -lights.set_notification_color("yellow") # Warning -lights.set_notification_color("orange") # Alert -lights.set_notification_color("purple") # Special -lights.set_notification_color("white") # General +LightsManager.set_notification_color("red") # 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 @@ -113,19 +107,19 @@ Simple on/off blinking: ```python import time -from mpos import lights +from mpos import LightsManager def blink_pattern(): """Blink all LEDs red 5 times.""" for _ in range(5): # Turn on - lights.set_all(255, 0, 0) - lights.write() + LightsManager.set_all(255, 0, 0) + LightsManager.write() time.sleep_ms(200) # Turn off - lights.clear() - lights.write() + LightsManager.clear() + LightsManager.write() time.sleep_ms(200) ``` @@ -134,7 +128,7 @@ def blink_pattern(): Display a rainbow pattern across all LEDs: ```python -from mpos import lights +from mpos import LightsManager def rainbow_cycle(): """Set each LED to a different rainbow color.""" @@ -147,9 +141,9 @@ def rainbow_cycle(): ] for i, color in enumerate(colors): - lights.set_led(i, *color) + LightsManager.set_led(i, *color) - lights.write() + LightsManager.write() ``` ### Chase Effect @@ -158,26 +152,26 @@ LEDs light up in sequence: ```python import time -from mpos import lights +from mpos import LightsManager def chase_effect(color=(0, 255, 0), delay_ms=100, loops=3): """Light up LEDs in sequence.""" - led_count = lights.get_led_count() + led_count = LightsManager.get_led_count() for _ in range(loops): for i in range(led_count): # Clear all - lights.clear() + LightsManager.clear() # Light current LED - lights.set_led(i, *color) - lights.write() + LightsManager.set_led(i, *color) + LightsManager.write() time.sleep_ms(delay_ms) # Clear after animation - lights.clear() - lights.write() + LightsManager.clear() + LightsManager.write() ``` ### Pulse/Breathing Effect @@ -186,7 +180,7 @@ Fade LEDs in and out smoothly: ```python import time -from mpos import lights +from mpos import LightsManager def pulse_effect(color=(0, 0, 255), duration_ms=2000): """Pulse LEDs with breathing effect.""" @@ -200,8 +194,8 @@ def pulse_effect(color=(0, 0, 255), duration_ms=2000): g = int(color[1] * brightness) b = int(color[2] * brightness) - lights.set_all(r, g, b) - lights.write() + LightsManager.set_all(r, g, b) + LightsManager.write() time.sleep_ms(delay) # Fade out @@ -211,13 +205,13 @@ def pulse_effect(color=(0, 0, 255), duration_ms=2000): g = int(color[1] * brightness) b = int(color[2] * brightness) - lights.set_all(r, g, b) - lights.write() + LightsManager.set_all(r, g, b) + LightsManager.write() time.sleep_ms(delay) # Clear - lights.clear() - lights.write() + LightsManager.clear() + LightsManager.write() ``` ### Random Sparkle @@ -227,11 +221,11 @@ Random LEDs flash briefly: ```python import time import random -from mpos import lights +from mpos import LightsManager def sparkle_effect(duration_ms=5000): """Random LEDs sparkle.""" - led_count = lights.get_led_count() + led_count = LightsManager.get_led_count() start_time = time.ticks_ms() while time.ticks_diff(time.ticks_ms(), start_time) < duration_ms: @@ -244,19 +238,19 @@ def sparkle_effect(duration_ms=5000): b = random.randint(0, 255) # Flash on - lights.clear() - lights.set_led(led, r, g, b) - lights.write() + LightsManager.clear() + LightsManager.set_led(led, r, g, b) + LightsManager.write() time.sleep_ms(100) # Flash off - lights.clear() - lights.write() + LightsManager.clear() + LightsManager.write() time.sleep_ms(50) # Clear after animation - lights.clear() - lights.write() + LightsManager.clear() + LightsManager.write() ``` ## Frame-Based LED Animations @@ -264,7 +258,7 @@ def sparkle_effect(duration_ms=5000): For smooth, real-time animations that integrate with the game loop, use the **TaskHandler event system**: ```python -from mpos import Activity, lights +from mpos import Activity, LightsManager import mpos.ui import time @@ -283,8 +277,8 @@ class LEDAnimationActivity(Activity): def onPause(self, screen): """Stop animation and clear LEDs when app pauses.""" mpos.ui.task_handler.remove_event_cb(self.update_frame) - lights.clear() - lights.write() + LightsManager.clear() + LightsManager.write() def update_frame(self, a, b): """Called every frame - update animation.""" @@ -296,21 +290,21 @@ class LEDAnimationActivity(Activity): self.last_time = current_time # Clear all LEDs - lights.clear() + LightsManager.clear() # Light up current LED - lights.set_led(self.led_index, 0, 255, 0) - lights.write() + LightsManager.set_led(self.led_index, 0, 255, 0) + LightsManager.write() # Move to next LED - self.led_index = (self.led_index + 1) % lights.get_led_count() + self.led_index = (self.led_index + 1) % LightsManager.get_led_count() ``` ### Smooth Color Cycle Animation ```python import math -from mpos import lights +from mpos import LightsManager class ColorCycleActivity(Activity): def onCreate(self): @@ -325,8 +319,8 @@ class ColorCycleActivity(Activity): def onPause(self, screen): mpos.ui.task_handler.remove_event_cb(self.update_frame) - lights.clear() - lights.write() + LightsManager.clear() + LightsManager.write() def update_frame(self, a, b): current_time = time.ticks_ms() @@ -342,8 +336,8 @@ class ColorCycleActivity(Activity): r, g, b = self.hsv_to_rgb(self.hue, 1.0, 1.0) # Update all LEDs - lights.set_all(r, g, b) - lights.write() + 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).""" @@ -438,21 +432,21 @@ Set all LEDs to a predefined color and immediately apply (convenience method). **❌ Bad** - Calling `write()` after each LED: ```python -from mpos import lights +from mpos import LightsManager for i in range(5): - lights.set_led(i, 255, 0, 0) - lights.write() # 5 hardware updates! + LightsManager.set_led(i, 255, 0, 0) + LightsManager.write() # 5 hardware updates! ``` **✅ Good** - Set all LEDs then write once: ```python -from mpos import lights +from mpos import LightsManager for i in range(5): - lights.set_led(i, 255, 0, 0) + LightsManager.set_led(i, 255, 0, 0) -lights.write() # 1 hardware update +LightsManager.write() # 1 hardware update ``` ### Update Rate Recommendations @@ -473,7 +467,7 @@ def update_frame(self, a, b): if delta_time >= UPDATE_INTERVAL: self.last_time = current_time # Update LEDs here - lights.write() + LightsManager.write() ``` ### Cleanup in onPause() @@ -486,8 +480,8 @@ def onPause(self, screen): mpos.ui.task_handler.remove_event_cb(self.update_frame) # Clear LEDs - lights.clear() - lights.write() + LightsManager.clear() + LightsManager.write() ``` This prevents LEDs from staying lit after your app exits. @@ -504,11 +498,11 @@ This prevents LEDs from staying lit after your app exits. ```python # ❌ Wrong - no write() -lights.set_all(255, 0, 0) +LightsManager.set_all(255, 0, 0) # ✅ Correct -lights.set_all(255, 0, 0) -lights.write() +LightsManager.set_all(255, 0, 0) +LightsManager.write() ``` ### Flickering LEDs @@ -521,14 +515,14 @@ lights.write() ```python # ❌ Bad - updating every frame (60 Hz) def update_frame(self, a, b): - lights.set_all(random_color()) - lights.write() # Too frequent! + 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 - lights.set_all(random_color()) - lights.write() + LightsManager.set_all(random_color()) + LightsManager.write() ``` 2. **Inconsistent timing** @@ -555,8 +549,8 @@ def onPause(self, screen): mpos.ui.task_handler.remove_event_cb(self.update_frame) # Clear LEDs - lights.clear() - lights.write() + LightsManager.clear() + LightsManager.write() ``` ### No LEDs on Waveshare Board @@ -568,9 +562,9 @@ def onPause(self, screen): **Solution**: - Check hardware before using LEDs: ```python - from mpos import lights + from mpos import LightsManager - if lights.is_available(): + if LightsManager.is_available(): # Use LEDs else: # Fallback to screen indicators or sounds @@ -584,24 +578,24 @@ def onPause(self, screen): 1. **RGB order confusion** - Make sure you're using (R, G, B) order: ```python - lights.set_led(0, 255, 0, 0) # Red (R=255, G=0, B=0) - lights.set_led(1, 0, 255, 0) # Green (R=0, G=255, B=0) - lights.set_led(2, 0, 0, 255) # Blue (R=0, G=0, B=255) + 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 - lights.set_led(0, 300, 0, 0) + LightsManager.set_led(0, 300, 0, 0) # ✅ Correct - clamp to 0-255 - lights.set_led(0, min(300, 255), 0, 0) + LightsManager.set_led(0, min(300, 255), 0, 0) ``` ## Complete Example: LED Status Indicator ```python -from mpos import Activity, lights +from mpos import Activity, LightsManager import lvgl as lv class StatusIndicatorActivity(Activity): @@ -653,25 +647,25 @@ class StatusIndicatorActivity(Activity): def show_status(self, status_type): """Show visual status with LEDs.""" - if not lights.is_available(): + if not LightsManager.is_available(): print("No LEDs available") return if status_type == "success": - lights.set_notification_color("green") + LightsManager.set_notification_color("green") elif status_type == "error": - lights.set_notification_color("red") + LightsManager.set_notification_color("red") elif status_type == "warning": - lights.set_notification_color("yellow") + LightsManager.set_notification_color("yellow") elif status_type == "clear": - lights.clear() - lights.write() + LightsManager.clear() + LightsManager.write() def onPause(self, screen): # Clear LEDs when leaving app - if lights.is_available(): - lights.clear() - lights.write() + if LightsManager.is_available(): + LightsManager.clear() + LightsManager.write() ``` ## See Also