Move mpos.apps functionality to PackageManager

This commit is contained in:
Thomas Farstrike
2026-01-25 00:08:01 +01:00
parent ffdabf1342
commit 31dcfba683
24 changed files with 211 additions and 185 deletions
@@ -9,7 +9,6 @@
# All icons took: 1250ms
# Most of this time is actually spent reading and parsing manifests.
import lvgl as lv
import mpos.apps
from mpos import AppearanceManager, PackageManager, Activity, DisplayMetrics
import time
import uhashlib
@@ -129,7 +128,7 @@ class Launcher(Activity):
# ----- events --------------------------------------------------
app_cont.add_event_cb(
lambda e, fullname=app.fullname: mpos.apps.start_app(fullname),
lambda e, fullname=app.fullname: PackageManager.start_app(fullname),
lv.EVENT.CLICKED, None)
app_cont.add_event_cb(
lambda e, cont=app_cont: self.focus_app_cont(cont),
+1 -2
View File
@@ -46,7 +46,6 @@ from .ui.widget_animator import WidgetAnimator
from .ui import focus_direction
# Utility modules
from . import apps
from . import bootloader
from . import ui
from . import config
@@ -91,7 +90,7 @@ __all__ = [
"click_button", "click_label", "click_keyboard_button", "find_button_with_text",
"get_all_widgets_with_text",
# Submodules
"apps", "ui", "config", "net", "content", "time", "sensor_manager",
"ui", "config", "net", "content", "time", "sensor_manager",
"camera_manager", "sdcard", "battery_voltage", "audio", "hardware", "bootloader",
# Timezone utilities
"TimeZone"
-115
View File
@@ -1,115 +0,0 @@
import lvgl as lv
import _thread
import traceback
import mpos.ui
# Run the script in the current thread:
# Returns True if successful
def execute_script(script_source, is_file, classname, cwd=None):
import utime # for timing read and compile
thread_id = _thread.get_ident()
compile_name = 'script' if not is_file else script_source
print(f"Thread {thread_id}: executing script with cwd: {cwd}")
try:
if is_file:
print(f"Thread {thread_id}: reading script from file {script_source}")
with open(script_source, 'r') as f: # No need to check if it exists as exceptions are caught
start_time = utime.ticks_ms()
script_source = f.read()
read_time = utime.ticks_diff(utime.ticks_ms(), start_time)
print(f"execute_script: reading script_source took {read_time}ms")
script_globals = {
'lv': lv,
'__name__': "__main__"
}
print(f"Thread {thread_id}: starting script")
import sys
path_before = sys.path[:] # Make a copy, not a reference
if cwd:
sys.path.append(cwd)
try:
start_time = utime.ticks_ms()
compiled_script = compile(script_source, compile_name, 'exec')
compile_time = utime.ticks_diff(utime.ticks_ms(), start_time)
print(f"execute_script: compiling script_source took {compile_time}ms")
start_time = utime.ticks_ms()
exec(compiled_script, script_globals)
end_time = utime.ticks_diff(utime.ticks_ms(), start_time)
print(f"apps.py execute_script: exec took {end_time}ms")
# Introspect globals
classes = {k: v for k, v in script_globals.items() if isinstance(v, type)}
functions = {k: v for k, v in script_globals.items() if callable(v) and not isinstance(v, type)}
variables = {k: v for k, v in script_globals.items() if not callable(v)}
print("Classes:", classes.keys()) # This lists a whole bunch of classes, including lib/mpos/ stuff
print("Functions:", functions.keys())
print("Variables:", variables.keys())
main_activity = script_globals.get(classname)
if main_activity:
from .app.activity import Activity
from .content.intent import Intent
start_time = utime.ticks_ms()
Activity.startActivity(None, Intent(activity_class=main_activity))
end_time = utime.ticks_diff(utime.ticks_ms(), start_time)
print(f"execute_script: Activity.startActivity took {end_time}ms")
else:
print(f"Warning: could not find app's main_activity {classname}")
return False
except Exception as e:
print(f"Thread {thread_id}: exception during execution:")
# Print stack trace with exception type, value, and traceback
tb = getattr(e, '__traceback__', None)
traceback.print_exception(type(e), e, tb)
return False
finally:
# Always restore sys.path, even if we return early or raise an exception
print(f"Thread {thread_id}: script {compile_name} finished, restoring sys.path from {sys.path} to {path_before}")
sys.path = path_before
return True
except Exception as e:
print(f"Thread {thread_id}: error:")
tb = getattr(e, '__traceback__', None)
traceback.print_exception(type(e), e, tb)
return False
# Returns True if successful
def start_app(fullname):
from .content.package_manager import PackageManager
mpos.ui.set_foreground_app(fullname)
import utime
start_time = utime.ticks_ms()
app = PackageManager.get(fullname)
if not app:
print(f"Warning: start_app can't find app {fullname}")
return
if not app.installed_path:
print(f"Warning: start_app can't start {fullname} because no it doesn't have an installed_path")
return
entrypoint = "assets/main.py"
classname = "Main"
if not app.main_launcher_activity:
print(f"WARNING: app {fullname} doesn't have a main_launcher_activity, defaulting to class {classname} in {entrypoint}")
else:
entrypoint = app.main_launcher_activity.get('entrypoint')
classname = app.main_launcher_activity.get("classname")
result = execute_script(app.installed_path + "/" + entrypoint, True, classname, app.installed_path + "/assets/")
# Launchers have the bar, other apps don't have it
if app.is_valid_launcher():
mpos.ui.topmenu.open_bar()
else:
mpos.ui.topmenu.close_bar()
end_time = utime.ticks_diff(utime.ticks_ms(), start_time)
print(f"start_app() took {end_time}ms")
return result
# Starts the first launcher that's found
def restart_launcher():
from .content.package_manager import PackageManager
print("restart_launcher")
# Stop all apps
mpos.ui.remove_and_stop_all_activities()
# No need to stop the other launcher first, because it exits after building the screen
return start_app(PackageManager.get_launcher().fullname)
@@ -1,4 +1,5 @@
import os
import traceback
try:
import zipfile
@@ -232,3 +233,115 @@ class PackageManager:
print(f"Checking if app {app_fullname} is installed...")
return PackageManager.is_installed_by_path(f"apps/{app_fullname}") or PackageManager.is_installed_by_path(f"builtin/apps/{app_fullname}")
@staticmethod
def execute_script(script_source, is_file, classname, cwd=None):
"""Run the script in the current thread. Returns True if successful."""
import utime # for timing read and compile
import lvgl as lv
import mpos.ui
import _thread
thread_id = _thread.get_ident()
compile_name = 'script' if not is_file else script_source
print(f"Thread {thread_id}: executing script with cwd: {cwd}")
try:
if is_file:
print(f"Thread {thread_id}: reading script from file {script_source}")
with open(script_source, 'r') as f: # No need to check if it exists as exceptions are caught
start_time = utime.ticks_ms()
script_source = f.read()
read_time = utime.ticks_diff(utime.ticks_ms(), start_time)
print(f"execute_script: reading script_source took {read_time}ms")
script_globals = {
'lv': lv,
'__name__': "__main__"
}
print(f"Thread {thread_id}: starting script")
import sys
path_before = sys.path[:] # Make a copy, not a reference
if cwd:
sys.path.append(cwd)
try:
start_time = utime.ticks_ms()
compiled_script = compile(script_source, compile_name, 'exec')
compile_time = utime.ticks_diff(utime.ticks_ms(), start_time)
print(f"execute_script: compiling script_source took {compile_time}ms")
start_time = utime.ticks_ms()
exec(compiled_script, script_globals)
end_time = utime.ticks_diff(utime.ticks_ms(), start_time)
print(f"apps.py execute_script: exec took {end_time}ms")
# Introspect globals
classes = {k: v for k, v in script_globals.items() if isinstance(v, type)}
functions = {k: v for k, v in script_globals.items() if callable(v) and not isinstance(v, type)}
variables = {k: v for k, v in script_globals.items() if not callable(v)}
print("Classes:", classes.keys()) # This lists a whole bunch of classes, including lib/mpos/ stuff
print("Functions:", functions.keys())
print("Variables:", variables.keys())
main_activity = script_globals.get(classname)
if main_activity:
from ..app.activity import Activity
from .intent import Intent
start_time = utime.ticks_ms()
Activity.startActivity(None, Intent(activity_class=main_activity))
end_time = utime.ticks_diff(utime.ticks_ms(), start_time)
print(f"execute_script: Activity.startActivity took {end_time}ms")
else:
print(f"Warning: could not find app's main_activity {classname}")
return False
except Exception as e:
print(f"Thread {thread_id}: exception during execution:")
# Print stack trace with exception type, value, and traceback
tb = getattr(e, '__traceback__', None)
traceback.print_exception(type(e), e, tb)
return False
finally:
# Always restore sys.path, even if we return early or raise an exception
print(f"Thread {thread_id}: script {compile_name} finished, restoring sys.path from {sys.path} to {path_before}")
sys.path = path_before
return True
except Exception as e:
print(f"Thread {thread_id}: error:")
tb = getattr(e, '__traceback__', None)
traceback.print_exception(type(e), e, tb)
return False
@staticmethod
def start_app(fullname):
"""Start an app by fullname. Returns True if successful."""
import mpos.ui
mpos.ui.set_foreground_app(fullname)
import utime
start_time = utime.ticks_ms()
app = PackageManager.get(fullname)
if not app:
print(f"Warning: start_app can't find app {fullname}")
return
if not app.installed_path:
print(f"Warning: start_app can't start {fullname} because no it doesn't have an installed_path")
return
entrypoint = "assets/main.py"
classname = "Main"
if not app.main_launcher_activity:
print(f"WARNING: app {fullname} doesn't have a main_launcher_activity, defaulting to class {classname} in {entrypoint}")
else:
entrypoint = app.main_launcher_activity.get('entrypoint')
classname = app.main_launcher_activity.get("classname")
result = PackageManager.execute_script(app.installed_path + "/" + entrypoint, True, classname, app.installed_path + "/assets/")
# Launchers have the bar, other apps don't have it
if app.is_valid_launcher():
mpos.ui.topmenu.open_bar()
else:
mpos.ui.topmenu.close_bar()
end_time = utime.ticks_diff(utime.ticks_ms(), start_time)
print(f"start_app() took {end_time}ms")
return result
@staticmethod
def restart_launcher():
"""Restart the launcher by stopping all activities and starting the launcher app."""
import mpos.ui
print("restart_launcher")
# Stop all apps
mpos.ui.remove_and_stop_all_activities()
# No need to stop the other launcher first, because it exits after building the screen
return PackageManager.start_app(PackageManager.get_launcher().fullname)
+2 -3
View File
@@ -2,7 +2,6 @@ import task_handler
import _thread
import lvgl as lv
import mpos.apps
import mpos.ui
import mpos.ui.topmenu
@@ -126,11 +125,11 @@ except Exception as e:
# Start launcher so it's always at bottom of stack
launcher_app = PackageManager.get_launcher()
started_launcher = mpos.apps.start_app(launcher_app.fullname)
started_launcher = PackageManager.start_app(launcher_app.fullname)
# Then start auto_start_app if configured
auto_start_app = prefs.get_string("auto_start_app", None)
if auto_start_app and launcher_app.fullname != auto_start_app:
result = mpos.apps.start_app(auto_start_app)
result = PackageManager.start_app(auto_start_app)
if result is not True:
print(f"WARNING: could not run {auto_start_app} app")
@@ -1,6 +1,5 @@
import asyncio # this is the only place where asyncio is allowed to be imported - apps should not use it directly but use this TaskManager
import _thread
import mpos.apps
class TaskManager:
+40 -4
View File
@@ -815,13 +815,49 @@ class MockThread:
class MockApps:
"""
Mock mpos.apps module for testing.
Mock mpos.apps module for testing (deprecated, use MockPackageManager instead).
This is kept for backward compatibility with existing tests.
Usage:
sys.modules['mpos.apps'] = MockApps
"""
@staticmethod
def good_stack_size():
"""Return a reasonable stack size for testing."""
return 8192
def start_app(fullname):
"""Mock start_app function."""
return True
@staticmethod
def restart_launcher():
"""Mock restart_launcher function."""
return True
@staticmethod
def execute_script(script_source, is_file, classname, cwd=None):
"""Mock execute_script function."""
return True
class MockPackageManager:
"""
Mock mpos.content.package_manager module for testing.
Usage:
sys.modules['mpos.content.package_manager'] = MockPackageManager
"""
@staticmethod
def start_app(fullname):
"""Mock start_app function."""
return True
@staticmethod
def restart_launcher():
"""Mock restart_launcher function."""
return True
@staticmethod
def execute_script(script_source, is_file, classname, cwd=None):
"""Mock execute_script function."""
return True
+4 -2
View File
@@ -13,9 +13,10 @@ infrastructure are already initialized (boot.py and main.py executed).
Usage in tests:
from mpos.ui.testing import wait_for_render, capture_screenshot
from mpos import PackageManager
# Start your app
mpos.apps.start_app("com.example.myapp")
PackageManager.start_app("com.example.myapp")
# Wait for UI to render
wait_for_render()
@@ -62,7 +63,8 @@ def wait_for_render(iterations=10):
iterations: Number of task handler iterations to run (default: 10)
Example:
mpos.apps.start_app("com.example.myapp")
from mpos import PackageManager
PackageManager.start_app("com.example.myapp")
wait_for_render() # Ensure UI is ready
assert verify_text_present(lv.screen_active(), "Welcome")
"""
+5 -4
View File
@@ -7,6 +7,7 @@ from .appearance_manager import AppearanceManager
from .util import (get_foreground_app)
from . import focus_direction
from .widget_animator import WidgetAnimator
from mpos.content.package_manager import PackageManager
CLOCK_UPDATE_INTERVAL = 1000 # 10 or even 1 ms doesn't seem to change the framerate but 100ms is enough
WIFI_ICON_UPDATE_INTERVAL = 1500
@@ -267,7 +268,7 @@ def create_drawer():
wifi_label.center()
def wifi_event(e):
close_drawer()
mpos.apps.start_app("com.micropythonos.wifi")
PackageManager.start_app("com.micropythonos.wifi")
wifi_btn.add_event_cb(wifi_event,lv.EVENT.CLICKED,None)
settings_btn=lv.button(drawer)
settings_btn.set_size(lv.pct(drawer_button_pct),lv.pct(20))
@@ -277,7 +278,7 @@ def create_drawer():
settings_label.center()
def settings_event(e):
close_drawer()
mpos.apps.start_app("com.micropythonos.settings")
PackageManager.start_app("com.micropythonos.settings")
settings_btn.add_event_cb(settings_event,lv.EVENT.CLICKED,None)
launcher_btn=lv.button(drawer)
launcher_btn.set_size(lv.pct(drawer_button_pct),lv.pct(20))
@@ -288,7 +289,7 @@ def create_drawer():
def launcher_event(e):
print("Launch button pressed!")
close_drawer(True)
mpos.apps.restart_launcher()
PackageManager.restart_launcher()
launcher_btn.add_event_cb(launcher_event,lv.EVENT.CLICKED,None)
'''
sleep_btn=lv.button(drawer)
@@ -307,7 +308,7 @@ def create_drawer():
else: # assume unix:
# maybe do a system suspend here? or at least show a popup toast "not supported"
close_drawer(True)
mpos.apps.restart_launcher()
PackageManager.restart_launcher()
sleep_btn.add_event_cb(sleep_event,lv.EVENT.CLICKED,None)
'''
restart_btn=lv.button(drawer)
-1
View File
@@ -1,7 +1,6 @@
# lib/mpos/ui/util.py
import lvgl as lv
import sys
from ..apps import restart_launcher
_foreground_app_name = None
-1
View File
@@ -1,7 +1,6 @@
import lvgl as lv
import sys
from ..apps import restart_launcher
from .focus import save_and_clear_current_focusgroup
from .topmenu import open_bar
-1
View File
@@ -3,7 +3,6 @@
import _thread
from mpos.task_manager import TaskManager
import mpos.apps
class Thread:
def __init__(self, group=None, target=None, name=None, args=(), kwargs=None, daemon=None):