diff --git a/modules/app.py b/modules/app.py index 32cc683..b7596d6 100644 --- a/modules/app.py +++ b/modules/app.py @@ -1,6 +1,10 @@ import asyncio import time +from perf_timer import PerfTimer +from system.eventbus import eventbus +from system.scheduler.events import RequestForegroundPopEvent + class App: def __init__(self): @@ -15,7 +19,8 @@ async def run(self, render_update): while True: cur_time = time.ticks_ms() delta_ticks = time.ticks_diff(cur_time, last_time) - self.update(delta_ticks) + with PerfTimer(f"Updating {self}"): + self.update(delta_ticks) await render_update() last_time = cur_time @@ -38,8 +43,11 @@ async def background_task(self): cur_time = time.ticks_ms() delta_ticks = time.ticks_diff(cur_time, last_time) self.background_update(delta_ticks) - await asyncio.sleep(0) + await asyncio.sleep(0.05) last_time = cur_time def background_update(self, delta): pass + + def minimise(self): + eventbus.emit(RequestForegroundPopEvent(self)) diff --git a/modules/events/input.py b/modules/events/input.py index e6d141e..d69ab79 100644 --- a/modules/events/input.py +++ b/modules/events/input.py @@ -96,5 +96,8 @@ def get(self, button, default=None): ] return any(matching_values) + def clear(self): + self.buttons.clear() + def __repr__(self): return f"" diff --git a/modules/apps/__init__.py b/modules/firmware_apps/__init__.py similarity index 100% rename from modules/apps/__init__.py rename to modules/firmware_apps/__init__.py diff --git a/modules/apps/basic_app.py b/modules/firmware_apps/basic_app.py similarity index 100% rename from modules/apps/basic_app.py rename to modules/firmware_apps/basic_app.py diff --git a/modules/apps/hexpansion_test.py b/modules/firmware_apps/hexpansion_test.py similarity index 100% rename from modules/apps/hexpansion_test.py rename to modules/firmware_apps/hexpansion_test.py diff --git a/modules/apps/intro_app.py b/modules/firmware_apps/intro_app.py similarity index 70% rename from modules/apps/intro_app.py rename to modules/firmware_apps/intro_app.py index 2f96ecd..30cef19 100644 --- a/modules/apps/intro_app.py +++ b/modules/firmware_apps/intro_app.py @@ -5,7 +5,7 @@ import random import gc -from events.input import Buttons +from events.input import Buttons, BUTTON_TYPES from tildagonos import tildagonos, led_colours from frontboards import twentyfour @@ -40,17 +40,44 @@ def draw(self, ctx): class IntroApp(app.App): - def __init__(self, text="EMF Camp", n_hexagons=10): + def __init__(self, text="EMF Camp", n_hexagons=5): self.text = text self.hexagons = [Hexagon() for _ in range(n_hexagons)] self.time_elapsed = 0 - self.dialog = None + self.button_states = Buttons(self) + self.back_time = 0 def update(self, delta): self.time_elapsed += delta / 1_000 for hexagon in self.hexagons: hexagon.update(self.time_elapsed) + buttons = [twentyfour.BUTTONS[name] for name in "ABCDEF"] + for i, button in enumerate(buttons): + if self.button_states.get(button): + color = led_colours[i] + else: + color = (0, 0, 0) + if i: + tildagonos.leds[i * 2] = color + else: + tildagonos.leds[12] = color + tildagonos.leds[1 + i * 2] = color + + # Hold back to quit + if self.button_states.get(BUTTON_TYPES["CANCEL"]): + self.back_time += delta / 1_000 + else: + self.back_time = 0 + if self.back_time > 1: + self.back_time = 0 + self.minimise() + self.button_states.clear() + for i in range(12): + tildagonos.leds[i] = (0, 0, 0) + + tildagonos.leds.write() + def draw_background(self, ctx): ctx.gray(1 - min(1, self.time_elapsed)).rectangle(-120, -120, 240, 240).fill() @@ -73,29 +100,9 @@ def draw(self, ctx): self.draw_text(ctx) ctx.restore() - ctx.save() - if self.dialog: - self.dialog.draw(ctx) - ctx.restore() - async def background_task(self): - print(self) - button_states = Buttons(self) while True: - await asyncio.sleep(0.05) - - for i, button in enumerate(twentyfour.BUTTONS.values()): - if button_states.get(button): - color = led_colours[i] - else: - color = (0, 0, 0) - if i: - tildagonos.leds[i * 2] = color - else: - tildagonos.leds[12] = color - tildagonos.leds[1 + i * 2] = color - - tildagonos.leds.write() + await asyncio.sleep(1) print( "fps:", diff --git a/modules/apps/menu_demo.py b/modules/firmware_apps/menu_demo.py similarity index 98% rename from modules/apps/menu_demo.py rename to modules/firmware_apps/menu_demo.py index aea3f38..1c01872 100644 --- a/modules/apps/menu_demo.py +++ b/modules/firmware_apps/menu_demo.py @@ -63,7 +63,7 @@ def set_menu(self, menu_name: Literal["main", "numbers", "letters", "words"]): def back_handler(self): if self.current_menu == "main": - return + self.minimise() self.set_menu("main") def draw_background(self, ctx): diff --git a/modules/apps/pingpong_app.py b/modules/firmware_apps/pingpong_app.py similarity index 100% rename from modules/apps/pingpong_app.py rename to modules/firmware_apps/pingpong_app.py diff --git a/modules/apps/test_app.py b/modules/firmware_apps/test_app.py similarity index 100% rename from modules/apps/test_app.py rename to modules/firmware_apps/test_app.py diff --git a/modules/apps/tick_app.py b/modules/firmware_apps/tick_app.py similarity index 100% rename from modules/apps/tick_app.py rename to modules/firmware_apps/tick_app.py diff --git a/modules/frontboards/twentyfour.py b/modules/frontboards/twentyfour.py index d940397..36ca8f4 100644 --- a/modules/frontboards/twentyfour.py +++ b/modules/frontboards/twentyfour.py @@ -39,4 +39,4 @@ async def background_task(self): if not button_down and button_states[button]: await eventbus.emit_async(ButtonUpEvent(button=button)) button_states[button] = button_down - await asyncio.sleep(0.001) + await asyncio.sleep(0.01) diff --git a/modules/main.py b/modules/main.py index c71807f..287a37e 100644 --- a/modules/main.py +++ b/modules/main.py @@ -3,8 +3,8 @@ from system.scheduler import scheduler from system.hexpansion.app import HexpansionManagerApp from system.notification.app import NotificationService +from system.launcher.app import Launcher -from apps.basic_app import BasicApp from frontboards.twentyfour import TwentyTwentyFour # Start front-board interface @@ -14,7 +14,7 @@ scheduler.start_app(HexpansionManagerApp()) # Start root app -scheduler.start_app(BasicApp(), foreground=True) +scheduler.start_app(Launcher(), foreground=True) # Start notification handler scheduler.start_app(NotificationService(), always_on_top=True) diff --git a/modules/perf_timer.py b/modules/perf_timer.py index 571742a..165304b 100644 --- a/modules/perf_timer.py +++ b/modules/perf_timer.py @@ -1,5 +1,7 @@ import time +DEBUG_PERF = False + def perf_timer(func): def wrapper(*args, **kwargs): @@ -7,7 +9,8 @@ def wrapper(*args, **kwargs): func(*args, **kwargs) end = time.ticks_us() delta = time.ticks_diff(end, start) - print(f"{func.__name__} took {delta} us") + if DEBUG_PERF: + print(f"{func.__name__} took {delta} us") return wrapper @@ -21,4 +24,5 @@ def __enter__(self): def __exit__(self, exc_type, exc_value, exc_tb): delta = time.ticks_diff(time.ticks_us(), self.start) - print(f"{self.name} took {delta} us") + if DEBUG_PERF: + print(f"{self.name} took {delta} us") diff --git a/modules/system/eventbus.py b/modules/system/eventbus.py index a3bb973..8776570 100644 --- a/modules/system/eventbus.py +++ b/modules/system/eventbus.py @@ -60,21 +60,35 @@ async def run(self): event = await self.event_queue.get() requires_focus = hasattr(event, "requires_focus") and event.requires_focus - async_tasks = [] - with PerfTimer("handle events"): - for app in self.handlers.keys(): - if app._focused or not requires_focus: - for event_type in self.handlers[app]: + # For both synchronous and asynchronous handlers, loop over the apps + # that have registered handlers, then if the app is eligible to receive + # the event, loop over the event types. If any match, loop over the handlers + # and invoke. + # + # N.B. These loops use tuple(...) as event handlers may themselves trigger + # new handlers to be registered. We don't make any guarantee if these handlers + # will be invoked or not for the event that triggered their registration, but + # we must avoid RuntimeError due to dictionary edits. + with PerfTimer("Synchronous event handlers"): + for app in tuple(self.handlers.keys()): + if getattr(app, "_focused", False) or not requires_focus: + for event_type in tuple(self.handlers[app]): if isinstance(event, event_type): - for handler in self.handlers[app][event_type]: + for handler in tuple(self.handlers[app][event_type]): handler(event) - for app in self.async_handlers.keys(): - if app._focused or not requires_focus: - for event_type in self.async_handlers[app]: - if isinstance(event, event_type): - for handler in self.async_handlers[app][event_type]: - async_tasks.append(asyncio.create_task(handler(event))) + async_tasks = [] + with PerfTimer("Asynchronous event handlers"): + for app in tuple(self.async_handlers.keys()): + if getattr(app, "_focused", False) or not requires_focus: + for event_type in tuple(self.async_handlers[app]): + if isinstance(event, event_type): + for handler in tuple( + self.async_handlers[app][event_type] + ): + async_tasks.append( + asyncio.create_task(handler(event)) + ) if async_tasks: await asyncio.gather(*async_tasks) diff --git a/modules/system/hexpansion/app.py b/modules/system/hexpansion/app.py index 25e2580..7bc415c 100644 --- a/modules/system/hexpansion/app.py +++ b/modules/system/hexpansion/app.py @@ -235,4 +235,4 @@ async def background_task(self): eventbus.emit(HexpansionRemovalEvent(port=i + 1)) tildagonos.leds.write() - await asyncio.sleep(0.001) + await asyncio.sleep(0.05) diff --git a/modules/system/launcher/__init__.py b/modules/system/launcher/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/modules/system/launcher/app.py b/modules/system/launcher/app.py new file mode 100644 index 0000000..74e7e63 --- /dev/null +++ b/modules/system/launcher/app.py @@ -0,0 +1,153 @@ +import os +import json + +from app import App +from app_components.menu import Menu +from perf_timer import PerfTimer +from system.eventbus import eventbus +from system.scheduler.events import RequestStartAppEvent, RequestForegroundPushEvent + + +def path_isfile(path): + # Wow totally an elegant way to do os.path.isfile... + try: + return (os.stat(path)[0] & 0x8000) != 0 + except OSError: + return False + + +def path_isdir(path): + try: + return (os.stat(path)[0] & 0x4000) != 0 + except OSError: + return False + + +def recursive_delete(path): + contents = os.listdir(path) + for name in contents: + entry_path = f"{path}/{name}" + if path_isdir(entry_path): + recursive_delete(entry_path) + else: + os.remove(entry_path) + os.rmdir(path) + + +class Launcher(App): + def list_user_apps(self): + with PerfTimer("List user apps"): + apps = [] + app_dir = "/apps" + try: + contents = os.listdir(app_dir) + except OSError: + # No apps dir full stop + return [] + + for name in contents: + if not path_isfile(f"{app_dir}/{name}/__init__.py"): + continue + app = { + "path": name, + "callable": "main", + "name": name, + "icon": None, + "category": "unknown", + "hidden": False, + } + metadata = self.loadInfo(app_dir, name) + app.update(metadata) + if not app["hidden"]: + apps.append(app) + return apps + + def loadInfo(self, folder, name): + try: + info_file = "{}/{}/metadata.json".format(folder, name) + with open(info_file) as f: + information = f.read() + return json.loads(information) + except BaseException: + return {} + + def list_core_apps(self): + core_app_info = [ + # ("App store", "app_store", "Store"), + # ("Name Badge", "hello", "Hello"), + ("Logo", "firmware_apps.intro_app", "IntroApp"), + ("Menu demo", "firmware_apps.menu_demo", "MenuDemo"), + # ("Update Firmware", "otaupdate", "OtaUpdate"), + # ("Wi-Fi Connect", "wifi_client", "WifiClient"), + # ("Sponsors", "sponsors", "Sponsors"), + # ("Battery", "battery", "Battery"), + # ("Accelerometer", "accel_app", "Accel"), + # ("Magnetometer", "magnet_app", "Magnetometer"), + # ("Settings", "settings_app", "SettingsApp"), + ] + core_apps = [] + for core_app in core_app_info: + core_apps.append( + { + "path": core_app[1], + "callable": core_app[2], + "name": core_app[0], + "icon": None, + "category": "unknown", + } + ) + return core_apps + + def update_menu(self): + self.menu_items = self.list_core_apps() + self.list_user_apps() + self.menu = Menu( + self, + [app["name"] for app in self.menu_items], + select_handler=self.select_handler, + back_handler=self.back_handler, + ) + + def launch(self, item): + module_name = item["path"] + fn = item["callable"] + app_id = f"{module_name}.{fn}" + app = self._apps.get(app_id) + print(self._apps) + if app is None: + print(f"Creating app {app_id}...") + module = __import__(module_name, None, None, (fn,)) + app = getattr(module, fn)() + self._apps[app_id] = app + eventbus.emit(RequestStartAppEvent(app, foreground=True)) + else: + eventbus.emit(RequestForegroundPushEvent(app)) + # with open("/lastapplaunch.txt", "w") as f: + # f.write(str(self.window.focus_idx())) + # eventbus.emit(RequestForegroundPopEvent(self)) + + def __init__(self): + self.update_menu() + self._apps = {} + + def select_handler(self, item): + for app in self.menu_items: + if item == app["name"]: + self.launch(app) + break + + def back_handler(self): + self.update_menu() + return + # if self.current_menu == "main": + # return + # self.set_menu("main") + + def draw_background(self, ctx): + ctx.gray(0).rectangle(-120, -120, 240, 240).fill() + + def draw(self, ctx): + self.draw_background(ctx) + self.menu.draw(ctx) + + def update(self, delta): + self.menu.update(delta) diff --git a/modules/system/scheduler/__init__.py b/modules/system/scheduler/__init__.py index 6ed3632..98f1c60 100644 --- a/modules/system/scheduler/__init__.py +++ b/modules/system/scheduler/__init__.py @@ -101,7 +101,7 @@ def stop_app(self, app): eventbus.deregister(app) def app_is_foregrounded(self, app): - return app in self.foreground_stack or app in self.on_top_stack + return self.app_is_focused(app) or app in self.on_top_stack def app_is_focused(self, app): return self.foreground_stack and app == self.foreground_stack[-1] @@ -192,13 +192,15 @@ async def _render_task(self): with PerfTimer("render"): ctx = display.get_ctx() for app in self.foreground_stack: - ctx.save() - app.draw(ctx) - ctx.restore() + with PerfTimer(f"rendering {app}"): + ctx.save() + app.draw(ctx) + ctx.restore() for app in self.on_top_stack: - ctx.save() - app.draw(ctx) - ctx.restore() + with PerfTimer(f"rendering {app}"): + ctx.save() + app.draw(ctx) + ctx.restore() display.end_frame(ctx) await asyncio.sleep(0.001) diff --git a/sim/apps/example/__init__.py b/sim/apps/example/__init__.py new file mode 100644 index 0000000..748b7f4 --- /dev/null +++ b/sim/apps/example/__init__.py @@ -0,0 +1 @@ +from .app import ExampleApp \ No newline at end of file diff --git a/sim/apps/example/app.py b/sim/apps/example/app.py new file mode 100644 index 0000000..83faf71 --- /dev/null +++ b/sim/apps/example/app.py @@ -0,0 +1,19 @@ +import asyncio +import app + +from events.input import Buttons, BUTTON_TYPES + + +class ExampleApp(app.App): + def __init__(self): + self.button_states = Buttons(self) + + def update(self, delta): + if self.button_states.get(BUTTON_TYPES["CANCEL"]): + self.minimise() + + def draw(self, ctx): + ctx.save() + ctx.rgb(0.2,0,0).rectangle(-120,-120,240,240).fill() + ctx.rgb(1,0,0).move_to(-80,0).text("Hello world") + ctx.restore() diff --git a/sim/apps/example/metadata.json b/sim/apps/example/metadata.json new file mode 100644 index 0000000..5ad5659 --- /dev/null +++ b/sim/apps/example/metadata.json @@ -0,0 +1,6 @@ +{ + "callable": "ExampleApp", + "name": "Example app", + "category": "unknown", + "hidden": false +} \ No newline at end of file diff --git a/sim/run.py b/sim/run.py index 9667687..3d6aad4 100755 --- a/sim/run.py +++ b/sim/run.py @@ -62,6 +62,7 @@ def find_spec(self, fullname, path, target=None): sys.path = [ os.path.join(projectpath, "sim", "fakes"), + os.path.join(projectpath, "sim", "apps"), os.path.join(projectpath, "modules"), os.path.join(projectpath, "modules/lib"), os.path.join(projectpath, "micropython/ports/esp32/build-tildagon/frozen_mpy"), @@ -99,7 +100,9 @@ def _path_replace(p): p = p[len("/flash") :] p = simpath + p return p - + if p.startswith("/apps"): + dir = os.path.dirname(__file__) + p = f"{dir}{p}" return p