From c0b9f68ae8b402c102888ab6aaa5495aa8f96135 Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Thu, 11 Dec 2025 14:38:12 +0100 Subject: [PATCH] AppStore app: eliminate thread --- CHANGELOG.md | 1 + .../assets/appstore.py | 95 ++++++++++--------- internal_filesystem/lib/mpos/app/activity.py | 8 +- 3 files changed, 55 insertions(+), 49 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 05fe5fd0..5136df52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ 0.5.2 ===== - AudioFlinger: optimize WAV volume scaling for speed and immediately set volume +- API: add TaskManager that wraps asyncio 0.5.1 ===== diff --git a/internal_filesystem/builtin/apps/com.micropythonos.appstore/assets/appstore.py b/internal_filesystem/builtin/apps/com.micropythonos.appstore/assets/appstore.py index ff1674dd..41f18d95 100644 --- a/internal_filesystem/builtin/apps/com.micropythonos.appstore/assets/appstore.py +++ b/internal_filesystem/builtin/apps/com.micropythonos.appstore/assets/appstore.py @@ -1,3 +1,4 @@ +import aiohttp import lvgl as lv import json import requests @@ -6,8 +7,10 @@ import os import time import _thread + from mpos.apps import Activity, Intent from mpos.app import App +from mpos import TaskManager import mpos.ui from mpos.content.package_manager import PackageManager @@ -16,6 +19,7 @@ class AppStore(Activity): apps = [] app_index_url = "https://apps.micropythonos.com/app_index.json" can_check_network = True + aiohttp_session = None # one session for the whole app is more performant # Widgets: main_screen = None @@ -26,6 +30,7 @@ class AppStore(Activity): progress_bar = None def onCreate(self): + self.aiohttp_session = aiohttp.ClientSession() self.main_screen = lv.obj() self.please_wait_label = lv.label(self.main_screen) self.please_wait_label.set_text("Downloading app index...") @@ -43,38 +48,39 @@ class AppStore(Activity): if self.can_check_network and not network.WLAN(network.STA_IF).isconnected(): self.please_wait_label.set_text("Error: WiFi is not connected.") else: - _thread.stack_size(mpos.apps.good_stack_size()) - _thread.start_new_thread(self.download_app_index, (self.app_index_url,)) + TaskManager.create_task(self.download_app_index(self.app_index_url)) - def download_app_index(self, json_url): - try: - response = requests.get(json_url, timeout=10) - except Exception as e: - print("Download failed:", e) - self.update_ui_threadsafe_if_foreground(self.please_wait_label.set_text, f"App index download \n{json_url}\ngot error: {e}") + def onDestroy(self, screen): + await self.aiohttp_session.close() + + async def download_app_index(self, json_url): + response = await self.download_url(json_url) + if not response: + self.please_wait_label.set_text(f"Could not download app index from\n{json_url}") return - if response and response.status_code == 200: - #print(f"Got response text: {response.text}") - try: - for app in json.loads(response.text): - try: - self.apps.append(App(app["name"], app["publisher"], app["short_description"], app["long_description"], app["icon_url"], app["download_url"], app["fullname"], app["version"], app["category"], app["activities"])) - except Exception as e: - print(f"Warning: could not add app from {json_url} to apps list: {e}") - except Exception as e: - print(f"ERROR: could not parse reponse.text JSON: {e}") - finally: - response.close() - # Remove duplicates based on app.name - seen = set() - self.apps = [app for app in self.apps if not (app.fullname in seen or seen.add(app.fullname))] - # Sort apps by app.name - self.apps.sort(key=lambda x: x.name.lower()) # Use .lower() for case-insensitive sorting - time.sleep_ms(200) - self.update_ui_threadsafe_if_foreground(self.please_wait_label.add_flag, lv.obj.FLAG.HIDDEN) - self.update_ui_threadsafe_if_foreground(self.create_apps_list) - time.sleep(0.1) # give the UI time to display the app list before starting to download - self.download_icons() + print(f"Got response text: {response[0:20]}") + try: + for app in json.loads(response): + try: + self.apps.append(App(app["name"], app["publisher"], app["short_description"], app["long_description"], app["icon_url"], app["download_url"], app["fullname"], app["version"], app["category"], app["activities"])) + except Exception as e: + print(f"Warning: could not add app from {json_url} to apps list: {e}") + except Exception as e: + self.please_wait_label.set_text(f"ERROR: could not parse reponse.text JSON: {e}") + return + print("Remove duplicates based on app.name") + seen = set() + self.apps = [app for app in self.apps if not (app.fullname in seen or seen.add(app.fullname))] + print("Sort apps by app.name") + self.apps.sort(key=lambda x: x.name.lower()) # Use .lower() for case-insensitive sorting + print("Hiding please wait label...") + self.update_ui_threadsafe_if_foreground(self.please_wait_label.add_flag, lv.obj.FLAG.HIDDEN) + print("Creating apps list...") + created_app_list_event = TaskManager.notify_event() # wait for the list to be shown before downloading the icons + self.update_ui_threadsafe_if_foreground(self.create_apps_list, event=created_app_list_event) + await created_app_list_event.wait() + print("awaiting self.download_icons()") + await self.download_icons() def create_apps_list(self): print("create_apps_list") @@ -119,14 +125,15 @@ class AppStore(Activity): desc_label.set_style_text_font(lv.font_montserrat_12, 0) desc_label.add_event_cb(lambda e, a=app: self.show_app_detail(a), lv.EVENT.CLICKED, None) print("create_apps_list app done") - - def download_icons(self): + + async def download_icons(self): + print("Downloading icons...") for app in self.apps: if not self.has_foreground(): print(f"App is stopping, aborting icon downloads.") break - if not app.icon_data: - app.icon_data = self.download_icon_data(app.icon_url) + #if not app.icon_data: + app.icon_data = await self.download_url(app.icon_url) if app.icon_data: print("download_icons has icon_data, showing it...") image_icon_widget = None @@ -147,20 +154,16 @@ class AppStore(Activity): intent.putExtra("app", app) self.startActivity(intent) - @staticmethod - def download_icon_data(url): - print(f"Downloading icon from {url}") + async def download_url(self, url): + print(f"Downloading {url}") + #await TaskManager.sleep(1) try: - response = requests.get(url, timeout=5) - if response.status_code == 200: - image_data = response.content - print("Downloaded image, size:", len(image_data), "bytes") - return image_data - else: - print("Failed to download image: Status code", response.status_code) + async with self.aiohttp_session.get(url) as response: + if response.status >= 200 and response.status < 400: + return await response.read() + print(f"Done downloading {url}") except Exception as e: - print(f"Exception during download of icon: {e}") - return None + print(f"download_url got exception {e}") class AppDetail(Activity): diff --git a/internal_filesystem/lib/mpos/app/activity.py b/internal_filesystem/lib/mpos/app/activity.py index c8373710..e0cd71c2 100644 --- a/internal_filesystem/lib/mpos/app/activity.py +++ b/internal_filesystem/lib/mpos/app/activity.py @@ -73,10 +73,12 @@ class Activity: self.throttle_async_call_counter = 0 # Execute a function if the Activity is in the foreground - def if_foreground(self, func, *args, **kwargs): + def if_foreground(self, func, *args, event=None, **kwargs): if self._has_foreground: #print(f"executing {func} with args {args} and kwargs {kwargs}") result = func(*args, **kwargs) + if event: + event.set() return result else: #print(f"[if_foreground] Skipped {func} because _has_foreground=False") @@ -86,11 +88,11 @@ class Activity: # The call may get throttled, unless important=True is added to it. # The order of these update_ui calls are not guaranteed, so a UI update might be overwritten by an "earlier" update. # To avoid this, use lv.timer_create() with .set_repeat_count(1) as examplified in osupdate.py - def update_ui_threadsafe_if_foreground(self, func, *args, important=False, **kwargs): + def update_ui_threadsafe_if_foreground(self, func, *args, important=False, event=None, **kwargs): self.throttle_async_call_counter += 1 if not important and self.throttle_async_call_counter > 100: # 250 seems to be okay, so 100 is on the safe side print(f"update_ui_threadsafe_if_foreground called more than 100 times for one UI frame, which can overflow - throttling!") return None # lv.async_call() is needed to update the UI from another thread than the main one (as LVGL is not thread safe) - result = lv.async_call(lambda _: self.if_foreground(func, *args, **kwargs),None) + result = lv.async_call(lambda _: self.if_foreground(func, *args, event=event, **kwargs), None) return result