diff --git a/python_payload/st3m/application.py b/python_payload/st3m/application.py index 8f6a86e2f162cbae66eedf0c1a9d1650a56e8249..c8a777ab7d97083afad1829f57b54548188ae85b 100644 --- a/python_payload/st3m/application.py +++ b/python_payload/st3m/application.py @@ -6,7 +6,7 @@ from st3m.ui.view import ( ) from st3m.ui.menu import MenuItem from st3m.input import InputState -from st3m.goose import Optional, List, Enum +from st3m.goose import Optional, List, Enum, Dict from st3m.logging import Log import toml @@ -97,7 +97,7 @@ class BundleMetadata: This data is used to discover bundles and load them as applications. """ - __slots__ = ["path", "name", "menu", "_t"] + __slots__ = ["path", "name", "menu", "_t", "version"] def __init__(self, path: str) -> None: self.path = path.rstrip("/") @@ -126,6 +126,11 @@ class BundleMetadata: if self.menu not in ["Apps", "Music", "Badge", "Hidden"]: raise BundleMetadataBroken("app.menu must be either Apps, Music or Badge") + version = 0 + if t.get("metadata") is not None: + version = t["metadata"].get("version", 0) + self.version = version + self._t = t @staticmethod @@ -193,8 +198,16 @@ class BundleMetadata: return [] return [MenuItemAppLaunch(self)] + @property + def source(self) -> str: + return os.path.dirname(self.path) + + @property + def id(self) -> str: + return os.path.basename(self.path) + def __repr__(self) -> str: - return f"<BundleMetadata: {self.name} at {self.path}>" + return f"<BundleMetadata: {self.id} at {self.path}>" class MenuItemAppLaunch(MenuItem): @@ -226,6 +239,80 @@ class MenuItemAppLaunch(MenuItem): return self._bundle.name +class BundleManager: + """ + The BundleManager maintains information about BundleMetadata at different + locations in the badge filesystem. + + It also manages updating/reloading bundles. + """ + + def __init__(self) -> None: + self.bundles: Dict[str, BundleMetadata] = {} + + @staticmethod + def _source_trumps(a: str, b: str) -> bool: + prios = { + "/flash/sys/apps": 200, + "/sd/apps": 120, + "/flash/apps": 100, + } + prio_a = prios.get(a, 0) + prio_b = prios.get(b, 0) + return prio_a > prio_b + + def _discover_at(self, path: str) -> None: + path = path.rstrip("/") + try: + l = os.listdir(path) + except Exception as e: + log.warning(f"Could not discover bundles in {path}: {e}") + l = [] + + for d in l: + dirpath = path + "/" + d + st = os.stat(dirpath) + if not stat.S_ISDIR(st[0]): + continue + + tomlpath = dirpath + "/flow3r.toml" + try: + st = os.stat(tomlpath) + if not stat.S_ISREG(st[0]): + continue + except Exception: + continue + + try: + b = BundleMetadata(dirpath) + except BundleLoadException as e: + log.error(f"Failed to bundle from {dirpath}: {e}") + continue + + id_ = b.id + if id_ not in self.bundles: + self.bundles[id_] = b + continue + ex = self.bundles[id_] + + # Do we have a newer version? + if b.version > ex.version: + self.bundles[id_] = b + continue + # Do we have a higher priority source? + if self._source_trumps(b.source, ex.source): + self.bundles[id_] = b + continue + log.warning( + f"Ignoring {id_} at {b.source} as it already exists at {ex.source}" + ) + + def update(self) -> None: + self._discover_at("/flash/sys/apps") + self._discover_at("/flash/apps") + self._discover_at("/sd/apps") + + def discover_bundles(path: str) -> List[BundleMetadata]: """ Discover valid bundles (directories containing flow3r.toml) inside a given diff --git a/python_payload/st3m/run.py b/python_payload/st3m/run.py index fafa9b8d6dcf3f7ee9d2c95477655785393564ea..ef1378d9e61b1d8ee6cbe831afeec36bf2af6808 100644 --- a/python_payload/st3m/run.py +++ b/python_payload/st3m/run.py @@ -11,7 +11,7 @@ from st3m.ui.menu import ( from st3m.ui.elements import overlays from st3m.ui.view import View, ViewManager, ViewTransitionBlend from st3m.ui.elements.menus import SimpleMenu, SunMenu -from st3m.application import discover_bundles, BundleMetadata, MenuItemAppLaunch +from st3m.application import BundleManager, BundleMetadata, MenuItemAppLaunch from st3m.about import About from st3m import settings, logging, processors, wifi @@ -52,9 +52,11 @@ def run_responder(r: Responder) -> None: reactor.run() -def _make_bundle_menu(bundles: List[BundleMetadata], kind: str) -> SimpleMenu: +def _make_bundle_menu(mgr: BundleManager, kind: str) -> SimpleMenu: entries: List[MenuItem] = [MenuItemBack()] - for bundle in bundles: + ids = sorted(mgr.bundles.keys()) + for id in ids: + bundle = mgr.bundles[id] entries += bundle.menu_entries(kind) return SimpleMenu(entries) @@ -127,7 +129,8 @@ def run_main() -> None: audio.set_volume_dB(-10) leds.set_rgb(0, 255, 0, 0) leds.update() - bundles = discover_bundles("/flash/sys/apps") + bundles = BundleManager() + bundles.update() settings.load_all() menu_settings = settings.build_menu() @@ -152,8 +155,7 @@ def run_main() -> None: ], ) if override_main_app is not None: - requested = [b for b in bundles if b.name == override_main_app] - print([b.name for b in bundles]) + requested = [b for b in bundles.bundles.values() if b.name == override_main_app] if len(requested) > 1: raise Exception(f"More than one bundle named {override_main_app}") if len(requested) == 0: