Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add basic app launcher #22

Merged
merged 3 commits into from
May 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions modules/app.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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

Expand All @@ -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))
3 changes: 3 additions & 0 deletions modules/events/input.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"<Buttons {self.buttons}>"
File renamed without changes.
File renamed without changes.
55 changes: 31 additions & 24 deletions modules/apps/intro_app.py → modules/firmware_apps/intro_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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()

Expand All @@ -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:",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
2 changes: 1 addition & 1 deletion modules/frontboards/twentyfour.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
4 changes: 2 additions & 2 deletions modules/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down
8 changes: 6 additions & 2 deletions modules/perf_timer.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import time

DEBUG_PERF = False


def perf_timer(func):
def wrapper(*args, **kwargs):
start = time.ticks_us()
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

Expand All @@ -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")
38 changes: 26 additions & 12 deletions modules/system/eventbus.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion modules/system/hexpansion/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Empty file.
153 changes: 153 additions & 0 deletions modules/system/launcher/app.py
Original file line number Diff line number Diff line change
@@ -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,))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently this will try to import the app name as-is. However, since the apps are all in the /apps/ folder and that is not in the sys.path this will fail. This should be replaced with __import__('apps.' + module_name, [...] or similar. Alternatively you can append /apps/ to the sys.path.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Thanks, I was just coming in to look at this. I assumed it had been a simulator path issue.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No problem! Glad I could help 😁

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)
Loading
Loading