From 124df71ae71782d09e04e14c05a92774db8b8884 Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Sat, 1 Nov 2025 09:05:24 +0100 Subject: [PATCH] OSUpdate app: use update_ui_threadsafe_if_foreground Now it no longer crashes if the user moves away while the OSUpdate is ongoing. --- .../assets/osupdate.py | 21 ++++++--------- internal_filesystem/lib/mpos/app/activity.py | 26 +++++++++++++++++-- 2 files changed, 32 insertions(+), 15 deletions(-) diff --git a/internal_filesystem/builtin/apps/com.micropythonos.osupdate/assets/osupdate.py b/internal_filesystem/builtin/apps/com.micropythonos.osupdate/assets/osupdate.py index dee8933c..79630de4 100644 --- a/internal_filesystem/builtin/apps/com.micropythonos.osupdate/assets/osupdate.py +++ b/internal_filesystem/builtin/apps/com.micropythonos.osupdate/assets/osupdate.py @@ -10,7 +10,6 @@ import mpos.ui class OSUpdate(Activity): - keep_running = True download_update_url = None # Widgets: @@ -58,9 +57,6 @@ class OSUpdate(Activity): print("Showing update info...") self.show_update_info() - def onStop(self, screen): - self.keep_running = False # this is checked by the update_with_lvgl thread - def show_update_info(self): self.status_label.set_text("Checking for OS updates...") hwid = mpos.info.get_hardware_id() @@ -140,8 +136,8 @@ class OSUpdate(Activity): def progress_callback(self, percent): print(f"OTA Update: {percent:.1f}%") - lv.async_call(lambda l: self.progress_label.set_text(f"OTA Update: {percent:.2f}%"), None) - lv.async_call(lambda l: self.progress_bar.set_value(int(percent), True), None) + self.update_ui_threadsafe_if_foreground(self.progress_bar.set_value, int(percent), True) + self.update_ui_threadsafe_if_foreground(self.progress_label.set_text, f"OTA Update: {percent:.2f}%") time.sleep_ms(100) # Custom OTA update with LVGL progress @@ -168,7 +164,7 @@ class OSUpdate(Activity): i = 0 total_size = round_up_to_multiple(total_size, chunk_size) print(f"Starting OTA update of size: {total_size}") - while self.keep_running: # stop if the user navigates away + while self.has_foreground(): # stop if the user navigates away time.sleep_ms(100) # don't hog the CPU chunk = response.raw.read(chunk_size) if not chunk: @@ -187,7 +183,7 @@ class OSUpdate(Activity): response.close() try: if bytes_written >= total_size: - lv.async_call(lambda l: self.status_label.set_text("Update finished! Please restart."), None) + lv.update_ui_threadsafe_if_foreground(self.status_label.set_text, "Update finished! Please restart.") if not simulate: # if the update was completely installed next_partition.set_boot() import machine @@ -196,12 +192,11 @@ class OSUpdate(Activity): else: print("This is an OSUpdate simulation, not attempting to restart the device.") else: - lv.async_call(lambda l: self.status_label.set_text(f"Wrote {bytes_written} < {total_size} so not enough!"), None) - self.install_button.remove_state(lv.STATE.DISABLED) # allow retry + self.update_ui_threadsafe_if_foreground(self.status_label.set_text, f"Wrote {bytes_written} < {total_size} so not enough!") + self.update_ui_threadsafe_if_foreground(self.install_button.remove_state, lv.STATE.DISABLED) # allow retry except Exception as e: - if self.keep_running: - lv.async_call(lambda l: self.status_label.set_text(f"Update error: {e}"), None) - self.install_button.remove_state(lv.STATE.DISABLED) # allow retry + self.update_ui_threadsafe_if_foreground(self.status_label.set_text, f"Update error: {e}") + self.update_ui_threadsafe_if_foreground(self.install_button.remove_state, lv.STATE.DISABLED) # Non-class functions: diff --git a/internal_filesystem/lib/mpos/app/activity.py b/internal_filesystem/lib/mpos/app/activity.py index 4518803f..b4022364 100644 --- a/internal_filesystem/lib/mpos/app/activity.py +++ b/internal_filesystem/lib/mpos/app/activity.py @@ -1,3 +1,4 @@ +import lvgl as lv import mpos.ui class Activity: @@ -6,15 +7,16 @@ class Activity: self.intent = None # Store the intent that launched this activity self.result = None self._result_callback = None + self._has_foreground = None def onCreate(self): pass def onStart(self, screen): pass def onResume(self, screen): # app gets foreground - pass + self._has_foreground = True def onPause(self, screen): # app goes to background - pass + self._has_foreground = False def onStop(self, screen): pass def onDestroy(self, screen): @@ -55,3 +57,23 @@ class Activity: self._result_callback = None # Clean up except AttributeError as e: self.initError(e) + + # Apps may want to check this to cancel heavy operations if the user moves away + def has_foreground(self): + return self._has_foreground + + # Execute a function if the Activity is in the foreground + def if_foreground(self, func, *args, **kwargs): + if self._has_foreground: + return func(*args, **kwargs) + else: + print(f"[if_foreground] Skipped {func} because _has_foreground=False") + return None + + # Update the UI in a threadsafe way if the Activity is in the foreground + def update_ui_threadsafe_if_foreground(self, func, *args, **kwargs): + # lv.async_call() is needed to update the UI from another thread than the main one (as LVGL is not thread safe) + lv.async_call( + lambda _: self.if_foreground(func, *args, **kwargs), + None + )