adapt apps to new "back button" paradigm

This commit is contained in:
Thomas Farstrike
2025-05-24 18:02:18 +02:00
parent d891edaacd
commit 68dd24dc03
9 changed files with 230 additions and 103 deletions
@@ -7,6 +7,12 @@
import time
import mpos.apps
import mpos.ui
# screens:
main_screen = None
keepliveqrdecoding = False
width = 240
height = 240
@@ -72,7 +78,7 @@ def qrdecode_one():
def close_button_click(e):
print("Close button clicked")
show_launcher()
mpos.ui.back_screen()
def snap_button_click(e):
@@ -142,20 +148,20 @@ def try_capture(event):
def build_ui():
global image, image_dsc,qr_label, status_label, cam, use_webcam, qr_button, snap_button, status_label_cont
cont = lv.obj(appscreen)
cont.set_style_pad_all(0, 0)
cont.set_style_border_width(0, 0)
cont.set_size(lv.pct(100), lv.pct(100))
cont.set_scrollbar_mode(lv.SCROLLBAR_MODE.OFF)
close_button = lv.button(cont)
global image, image_dsc,qr_label, status_label, cam, use_webcam, qr_button, snap_button, status_label_cont, main_screen
main_screen = lv.obj()
main_screen.set_style_pad_all(0, 0)
main_screen.set_style_border_width(0, 0)
main_screen.set_size(lv.pct(100), lv.pct(100))
main_screen.set_scrollbar_mode(lv.SCROLLBAR_MODE.OFF)
close_button = lv.button(main_screen)
close_button.set_size(60,60)
close_button.align(lv.ALIGN.TOP_RIGHT, 0, 0)
close_label = lv.label(close_button)
close_label.set_text(lv.SYMBOL.CLOSE)
close_label.center()
close_button.add_event_cb(close_button_click,lv.EVENT.CLICKED,None)
snap_button = lv.button(cont)
snap_button = lv.button(main_screen)
snap_button.set_size(60, 60)
snap_button.align(lv.ALIGN.RIGHT_MID, 0, 0)
snap_button.add_flag(lv.obj.FLAG.HIDDEN)
@@ -163,7 +169,7 @@ def build_ui():
snap_label.set_text(lv.SYMBOL.OK)
snap_label.center()
snap_button.add_event_cb(snap_button_click,lv.EVENT.CLICKED,None)
qr_button = lv.button(cont)
qr_button = lv.button(main_screen)
qr_button.set_size(60, 60)
qr_button.add_flag(lv.obj.FLAG.HIDDEN)
qr_button.align(lv.ALIGN.BOTTOM_RIGHT, 0, 0)
@@ -172,7 +178,7 @@ def build_ui():
qr_label.center()
qr_button.add_event_cb(qr_button_click,lv.EVENT.CLICKED,None)
# Initialize LVGL image widget
image = lv.image(cont)
image = lv.image(main_screen)
image.align(lv.ALIGN.LEFT_MID, 0, 0)
# Create image descriptor once
image_dsc = lv.image_dsc_t({
@@ -188,7 +194,7 @@ def build_ui():
'data': None # Will be updated per frame
})
image.set_src(image_dsc)
status_label_cont = lv.obj(appscreen)
status_label_cont = lv.obj(main_screen)
status_label_cont.set_size(lv.pct(66),lv.pct(60))
status_label_cont.align(lv.ALIGN.LEFT_MID, lv.pct(5), 0)
status_label_cont.set_style_bg_color(lv.color_white(), 0)
@@ -200,6 +206,7 @@ def build_ui():
status_label.set_style_text_color(lv.color_white(), 0)
status_label.set_width(lv.pct(100))
status_label.center()
mpos.ui.load_screen(main_screen)
def init_cam():
@@ -235,7 +242,7 @@ def init_cam():
def check_running(timer):
if lv.screen_active() != appscreen:
if lv.screen_active() != main_screen:
print("camtest.py backgrounded, cleaning up...")
check_running_timer.delete()
if capture_timer:
@@ -248,7 +255,6 @@ def check_running(timer):
appscreen = lv.screen_active()
build_ui()
cam = init_cam()
+1 -1
View File
@@ -61,7 +61,7 @@ def swipe_read_cb(indev_drv, data):
# Mouse/touch pressed (start of potential swipe)
start_y = y # Store Y for vertical swipe detection
start_x = x # Store X for horizontal swipe detection
print(f"Mouse press at X={start_x}, Y={start_y}")
#print(f"Mouse press at X={start_x}, Y={start_y}")
# Check if press is in notification bar (for swipe down)
if y <= mpos.ui.NOTIFICATION_BAR_HEIGHT:
@@ -7,10 +7,11 @@ import time
import _thread
import mpos.apps
import mpos.ui
# Screens:
app_detail_screen = None
appscreen = lv.screen_active()
main_screen = None
apps = []
update_button = None
@@ -58,10 +59,10 @@ def is_update_available(app_fullname, new_version):
installed_app=None
if is_installed_by_path(appdir):
print(f"{appdir} found, getting version...")
installed_app = parse_manifest(f"{appdir}/META-INF/MANIFEST.JSON")
installed_app = mpos.apps.parse_manifest(f"{appdir}/META-INF/MANIFEST.JSON")
elif is_installed_by_path(builtinappdir):
print(f"{builtinappdir} found, getting version...")
installed_app = parse_manifest(f"{builtinappdir}/META-INF/MANIFEST.JSON")
installed_app = mpos.apps.parse_manifest(f"{builtinappdir}/META-INF/MANIFEST.JSON")
if not installed_app or installed_app.version == "0.0.0": # special case, if the installed app doesn't have a version number then there's no update
return False
return compare_versions(new_version, installed_app.version)
@@ -264,10 +265,14 @@ def load_icon(icon_path):
return image_dsc
def create_apps_list():
global apps
global apps, main_screen
main_screen = lv.obj()
please_wait_label = lv.label(main_screen)
please_wait_label.set_text("Downloading app index...")
please_wait_label.center()
default_icon_dsc = load_icon("builtin/res/mipmap-mdpi/default_icon_64x64.png")
print("create_apps_list")
apps_list = lv.list(appscreen)
apps_list = lv.list(main_screen)
apps_list.set_style_pad_all(0, 0)
apps_list.set_size(lv.pct(100), lv.pct(100))
print("create_apps_list iterating")
@@ -300,6 +305,7 @@ def create_apps_list():
desc_label.set_style_text_font(lv.font_montserrat_12, 0)
desc_label.add_event_cb(lambda e, a=app: show_app_detail(a), lv.EVENT.CLICKED, None)
print("create_apps_list app done")
mpos.ui.load_screen(main_screen)
try:
_thread.stack_size(16*1024)
_thread.start_new_thread(download_icons,())
@@ -309,21 +315,21 @@ def create_apps_list():
def show_app_detail(app):
global app_detail_screen, install_button, progress_bar, install_label
#app_detail_screen = lv.obj()
#app_detail_screen.set_size(lv.pct(100), lv.pct(100))
#back_button = lv.button(app_detail_screen)
#back_button.set_width(lv.pct(15))
#back_button.add_flag(lv.obj.FLAG.CLICKABLE)
#back_button.add_event_cb(back_to_main, lv.EVENT.CLICKED, None)
#back_label = lv.label(back_button)
#back_label.set_text(lv.SYMBOL.LEFT)
#back_label.center()
app_detail_screen = lv.obj()
app_detail_screen.set_size(lv.pct(100), lv.pct(100))
back_button = lv.button(app_detail_screen)
back_button.set_width(lv.pct(15))
back_button.add_flag(lv.obj.FLAG.CLICKABLE)
back_button.add_event_cb(back_to_main, lv.EVENT.CLICKED, None)
back_label = lv.label(back_button)
back_label.set_text(lv.SYMBOL.LEFT)
back_label.center()
cont = lv.obj(app_detail_screen)
cont.set_size(lv.pct(100), lv.pct(100))
cont.set_pos(0, 40)
cont.set_flex_flow(lv.FLEX_FLOW.COLUMN)
app_detail_screen.set_pos(0, 40)
app_detail_screen.set_flex_flow(lv.FLEX_FLOW.COLUMN)
#
headercont = lv.obj(cont)
headercont = lv.obj(app_detail_screen)
headercont.set_style_pad_all(0, 0)
headercont.set_flex_flow(lv.FLEX_FLOW.ROW)
headercont.set_size(lv.pct(100), lv.SIZE_CONTENT)
@@ -344,12 +350,12 @@ def show_app_detail(app):
publisher_label.set_text(app.publisher)
publisher_label.set_style_text_font(lv.font_montserrat_16, 0)
#
progress_bar = lv.bar(cont)
progress_bar = lv.bar(app_detail_screen)
progress_bar.set_width(lv.pct(100))
progress_bar.set_range(0, 100)
progress_bar.add_flag(lv.obj.FLAG.HIDDEN)
# Always have this button:
buttoncont = lv.obj(cont)
buttoncont = lv.obj(app_detail_screen)
buttoncont.set_style_pad_all(0, 0)
buttoncont.set_flex_flow(lv.FLEX_FLOW.ROW)
buttoncont.set_size(lv.pct(100), lv.SIZE_CONTENT)
@@ -373,17 +379,17 @@ def show_app_detail(app):
update_label.set_text("Update")
update_label.center()
# version label:
version_label = lv.label(cont)
version_label = lv.label(app_detail_screen)
version_label.set_width(lv.pct(100))
version_label.set_text(f"Latest version: {app.version}") # make this bold if this is newer than the currently installed one
version_label.set_style_text_font(lv.font_montserrat_12, 0)
version_label.align_to(install_button, lv.ALIGN.OUT_BOTTOM_MID, 0, lv.pct(5))
long_desc_label = lv.label(cont)
long_desc_label = lv.label(app_detail_screen)
long_desc_label.align_to(version_label, lv.ALIGN.OUT_BOTTOM_MID, 0, lv.pct(5))
long_desc_label.set_text(app.long_description)
long_desc_label.set_style_text_font(lv.font_montserrat_12, 0)
long_desc_label.set_width(lv.pct(100))
lv.screen_load(app_detail_screen)
mpos.ui.load_screen(app_detail_screen)
def toggle_install(download_url, fullname):
@@ -416,28 +422,17 @@ def update_button_click(download_url, fullname):
print("Could not start download_and_unzip thread: ", e)
def back_to_main(event):
global app_detail_screen
if app_detail_screen:
app_detail_screen.delete()
app_detail_screen = None
lv.screen_load(appscreen)
def janitor_cb(timer):
global appscreen, app_detail_screen
if lv.screen_active() != appscreen and lv.screen_active() != app_detail_screen:
global main_screen, app_detail_screen
if lv.screen_active() != main_screen and lv.screen_active() != app_detail_screen:
print("appstore.py backgrounded, cleaning up...")
janitor.delete()
restart_launcher() # refresh the launcher
mpos.apps.restart_launcher() # refresh the launcher
print("appstore.py ending")
janitor = lv.timer_create(janitor_cb, 400, None)
please_wait_label = lv.label(appscreen)
please_wait_label.set_text("Downloading app index...")
please_wait_label.center()
can_check_network = True
try:
import network
@@ -9,17 +9,19 @@
# All icons took: 1250ms
# Most of this time is actually spent reading and parsing manifests.
appscreen = lv.screen_active()
import uos
import lvgl as lv
import mpos.apps
import mpos.ui
# Create a container for the grid
cont = lv.obj(appscreen)
cont.set_pos(0, NOTIFICATION_BAR_HEIGHT) # leave some margin for the notification bar
main_screen = lv.obj()
cont = lv.obj(main_screen)
cont.set_pos(0, mpos.ui.NOTIFICATION_BAR_HEIGHT) # leave some margin for the notification bar
cont.set_size(lv.pct(100), lv.pct(100))
cont.set_style_pad_all(10, 0)
cont.set_flex_flow(lv.FLEX_FLOW.ROW_WRAP)
mpos.ui.load_screen(main_screen)
# Grid parameters
icon_size = 64 # Adjust based on your display
@@ -56,7 +58,7 @@ for dir_path in [apps_dir, apps_dir_builtin]:
base_name = d
if base_name not in seen_base_names: # Avoid duplicates
seen_base_names.add(base_name)
app = parse_manifest(f"{full_path}/META-INF/MANIFEST.JSON")
app = mpos.apps.parse_manifest(f"{full_path}/META-INF/MANIFEST.JSON")
if app.category != "launcher": # Skip launchers
app_list.append((app.name, full_path))
except OSError:
@@ -94,7 +96,7 @@ for app_name, app_dir_fullpath in app_list:
label.set_width(iconcont_width)
label.align(lv.ALIGN.BOTTOM_MID, 0, 0)
label.set_style_text_align(lv.TEXT_ALIGN.CENTER, 0)
app_cont.add_event_cb(lambda e, path=app_dir_fullpath: start_app(path), lv.EVENT.CLICKED, None)
app_cont.add_event_cb(lambda e, path=app_dir_fullpath: mpos.apps.start_app(path), lv.EVENT.CLICKED, None)
end = time.ticks_ms()
print(f"Displaying all icons took: {end-start}ms")
@@ -1,12 +1,14 @@
appscreen = lv.screen_active()
appscreen.clean()
import lvgl as lv
import requests
import ujson
import time
import _thread
import mpos.info
import mpos.ui
main_screen = None
status_label=None
install_button=None
@@ -34,7 +36,7 @@ def compare_versions(ver1: str, ver2: str) -> bool:
# Custom OTA update with LVGL progress
def update_with_lvgl(url):
global install_button, status_label
global install_button, status_label, main_screen
install_button.add_flag(lv.obj.FLAG.HIDDEN) # or change to cancel button?
status_label.set_text("Update in progress.\nNavigate away to cancel.")
import ota.update
@@ -45,10 +47,10 @@ def update_with_lvgl(url):
print(f"Current partition: {current_partition}")
next_partition = current_partition.get_next_update()
print(f"Next partition: {next_partition}")
label = lv.label(appscreen)
label = lv.label(main_screen)
label.set_text("OS Update: 0.00%")
label.align(lv.ALIGN.CENTER, 0, -30)
progress_bar = lv.bar(appscreen)
progress_bar = lv.bar(main_screen)
progress_bar.set_size(200, 20)
progress_bar.align(lv.ALIGN.BOTTOM_MID, 0, -50)
progress_bar.set_range(0, 100)
@@ -65,7 +67,7 @@ def update_with_lvgl(url):
chunk_size = 4096
i = 0
print(f"Starting OTA update of size: {total_size}")
while appscreen == lv.screen_active(): # stop if the user navigates away
while main_screen == lv.screen_active(): # stop if the user navigates away
time.sleep_ms(100) # don't hog the CPU
chunk = response.raw.read(chunk_size)
if not chunk:
@@ -96,8 +98,8 @@ def install_button_click(download_url):
def handle_update_info(version, download_url, changelog):
global install_button, status_label
label = f"Installed OS version: {CURRENT_OS_VERSION}\n"
if compare_versions(version, CURRENT_OS_VERSION):
label = f"Installed OS version: {mpos.info.CURRENT_OS_VERSION}\n"
if compare_versions(version, mpos.info.CURRENT_OS_VERSION):
label += "Available new"
install_button.remove_flag(lv.obj.FLAG.HIDDEN)
install_button.add_event_cb(lambda e, u=download_url: install_button_click(u), lv.EVENT.CLICKED, None)
@@ -137,15 +139,17 @@ def show_update_info():
print("Error:", str(e))
install_button = lv.button(appscreen)
install_button.align(lv.ALIGN.TOP_RIGHT, 0, NOTIFICATION_BAR_HEIGHT)
main_screen = lv.obj()
install_button = lv.button(main_screen)
install_button.align(lv.ALIGN.TOP_RIGHT, 0, mpos.ui.NOTIFICATION_BAR_HEIGHT)
install_button.add_flag(lv.obj.FLAG.HIDDEN) # button will be shown if there is an update available
install_button.set_size(lv.SIZE_CONTENT, lv.pct(20))
install_label = lv.label(install_button)
install_label.set_text("Update OS")
install_label.center()
status_label = lv.label(appscreen)
status_label.align(lv.ALIGN.TOP_LEFT,0,NOTIFICATION_BAR_HEIGHT)
status_label = lv.label(main_screen)
status_label.align(lv.ALIGN.TOP_LEFT,0,mpos.ui.NOTIFICATION_BAR_HEIGHT)
mpos.ui.load_screen(main_screen)
network_connected = True
try:
+12 -32
View File
@@ -7,11 +7,12 @@ import uos
import _thread
import traceback
import mpos.apps
import mpos.info
import mpos.ui
# Run the script in the current thread:
def execute_script(script_source, is_file, is_launcher, is_graphical):
def execute_script(script_source, is_file):
thread_id = _thread.get_ident()
compile_name = 'script' if not is_file else script_source
print(f"Thread {thread_id}: executing script")
@@ -20,31 +21,10 @@ def execute_script(script_source, is_file, is_launcher, is_graphical):
print(f"Thread {thread_id}: reading script from file {script_source}")
with open(script_source, 'r') as f: # TODO: check if file exists first?
script_source = f.read()
if not is_graphical:
script_globals = {
'__name__': "__main__"
}
else: # is_graphical
if is_launcher:
prevscreen = None
newscreen = mpos.ui.rootscreen
else:
prevscreen = lv.screen_active()
newscreen=lv.obj()
newscreen.set_size(lv.pct(100),lv.pct(100))
mpos.ui.load_screen(newscreen)
script_globals = {
'lv': lv,
'th': mpos.ui.th,
'NOTIFICATION_BAR_HEIGHT': mpos.ui.NOTIFICATION_BAR_HEIGHT, # for apps that want to leave space for notification bar
'appscreen': newscreen,
'start_app': start_app, # for launcher apps
'parse_manifest': parse_manifest, # for launcher apps
'restart_launcher': restart_launcher, # for appstore apps
'show_launcher': mpos.ui.show_launcher, # for apps that want to show the launcher
'CURRENT_OS_VERSION': mpos.info.CURRENT_OS_VERSION, # for osupdate
'__name__': "__main__"
}
script_globals = {
'lv': lv,
'__name__': "__main__"
}
print(f"Thread {thread_id}: starting script")
try:
compiled_script = compile(script_source, compile_name, 'exec')
@@ -63,8 +43,8 @@ def execute_script(script_source, is_file, is_launcher, is_graphical):
# Run the script in a new thread:
# TODO: check if the script exists here instead of launching a new thread?
def execute_script_new_thread(scriptname, is_file, is_launcher, is_graphical):
print(f"main.py: execute_script_new_thread({scriptname},{is_file},{is_launcher})")
def execute_script_new_thread(scriptname, is_file):
print(f"main.py: execute_script_new_thread({scriptname},{is_file})")
try:
# 168KB maximum at startup but 136KB after loading display, drivers, LVGL gui etc so let's go for 128KB for now, still a lot...
# But then no additional threads can be created. A stacksize of 32KB allows for 4 threads, so 3 in the app itself, which might be tight.
@@ -77,12 +57,12 @@ def execute_script_new_thread(scriptname, is_file, is_launcher, is_graphical):
stack=32*1024
elif "appstore"in scriptname:
print("Starting appstore with extra stack size!")
stack=24*1024
stack=24*1024 # this doesn't do anything because it's all started in the same thread
else:
stack=16*1024 # 16KB doesn't seem to be enough for the AppStore app on desktop
print(f"app.py: setting stack size for script to {stack}")
_thread.stack_size(stack)
_thread.start_new_thread(execute_script, (scriptname, is_file, is_launcher, is_graphical))
_thread.start_new_thread(execute_script, (scriptname, is_file))
except Exception as e:
print("main.py: execute_script_new_thread(): error starting new thread thread: ", e)
@@ -100,10 +80,10 @@ def start_app(app_dir, is_launcher=False):
print(f"main.py start_app({app_dir},{is_launcher})")
mpos.ui.set_foreground_app(app_dir) # would be better to store only the app name...
manifest_path = f"{app_dir}/META-INF/MANIFEST.JSON"
app = parse_manifest(manifest_path)
app = mpos.apps.parse_manifest(manifest_path)
start_script_fullpath = f"{app_dir}/{app.entrypoint}"
#execute_script_new_thread(start_script_fullpath, True, is_launcher, True) # Starting (GUI?) apps in a new thread can cause hangs (GIL lock?)
execute_script(start_script_fullpath, True, is_launcher, True)
execute_script(start_script_fullpath, True)
# Launchers have the bar, other apps don't have it
if is_launcher:
mpos.ui.open_bar()
+138
View File
@@ -0,0 +1,138 @@
import ujson
import os
class SharedPreferences:
def __init__(self, appname, filename="config.json"):
"""Initialize with appname and filename for preferences."""
self.appname = appname
self.filename = filename
self.filepath = f"data/{self.appname}/{self.filename}"
self.data = {}
self.load() # Load existing preferences
def make_folder_structure(self):
"""Create directory structure if it doesn't exist."""
#print("Checking if data/ exists")
try:
os.stat('data')
#print("data/ exists")
except OSError:
print("Creating data/ directory")
os.mkdir('data')
#print(f"Checking if data/{self.appname}/ exists")
try:
os.stat(f"data/{self.appname}")
#print(f"data/{self.appname}/ exists")
except OSError:
#print(f"Creating data/{self.appname} directory")
os.mkdir(f"data/{self.appname}")
def load(self):
"""Load preferences from the JSON file."""
try:
with open(self.filepath, 'r') as f:
self.data = ujson.load(f)
print(f"load: Loaded preferences: {self.data}")
except Exception as e:
print(f"load: Got exception {e}, assuming empty or non-existent preferences.")
self.data = {} # Default to empty dict on error
def get_string(self, key, default=None):
"""Retrieve a string value for the given key, with a default if not found."""
return str(self.data.get(key, default))
def get_int(self, key, default=0):
"""Retrieve an integer value for the given key, with a default if not found."""
try:
return int(self.data.get(key, default))
except (TypeError, ValueError):
return default
def get_bool(self, key, default=False):
"""Retrieve a boolean value for the given key, with a default if not found."""
try:
return bool(self.data.get(key, default))
except (TypeError, ValueError):
return default
def edit(self):
"""Return an Editor object to modify preferences."""
return Editor(self)
def save_config(self):
"""Save preferences to the JSON file."""
self.make_folder_structure() # Ensure directories exist
print(f"save_config: Saving preferences to {self.filepath}")
try:
with open(self.filepath, 'w') as f:
ujson.dump(self.data, f)
print("save_config: Saved")
except Exception as e:
print(f"save_config: Got exception {e}")
class Editor:
def __init__(self, preferences):
"""Initialize Editor with a reference to SharedPreferences."""
self.preferences = preferences
self.temp_data = preferences.data.copy() # Work on a copy of the data
def put_string(self, key, value):
"""Store a string value."""
self.temp_data[key] = str(value)
return self
def put_int(self, key, value):
"""Store an integer value."""
self.temp_data[key] = int(value)
return self
def put_bool(self, key, value):
"""Store a boolean value."""
self.temp_data[key] = bool(value)
return self
def apply(self):
"""Save changes to the file asynchronously (emulated)."""
self.preferences.data = self.temp_data.copy()
self.preferences.save_config()
def commit(self):
"""Save changes to the file synchronously."""
self.preferences.data = self.temp_data.copy()
self.preferences.save_config()
return True
# Example usage
def main():
# Initialize SharedPreferences with an appname
prefs = SharedPreferences("com.example.test_shared_prefs")
print("Getting preferences:")
print(f"theme: {prefs.get_string('theme')}")
print(f"volume: {prefs.get_int('volume')}")
print(f"notifications: {prefs.get_bool('notifications')}")
print("Done getting preferences.")
# Save some settings using Editor
editor = prefs.edit()
editor.put_string("theme", "dark")
editor.put_int("volume", 75)
editor.put_bool("notifications", True)
editor.apply() # Asynchronous save (emulated)
# Read back the settings
print("Theme:", prefs.get_string("theme", "light"))
print("Volume:", prefs.get_int("volume", 50))
print("Notifications:", prefs.get_bool("notifications", False))
# Modify a setting
editor = prefs.edit()
editor.put_string("theme", "light")
success = editor.commit() # Synchronous save
print("Save successful:", success)
# Read updated setting
print("Updated Theme:", prefs.get_string("theme", "light"))
if __name__ == '__main__':
main()
+2
View File
@@ -457,5 +457,7 @@ def back_screen():
screen_stack.pop() # Remove current screen
prevscreen = screen_stack[-1] # load previous screen
lv.screen_load(prevscreen)
if len(screen_stack) == 1:
open_bar()
else:
print("Warning: can't go back because screen_stack is empty.")
+1 -1
View File
@@ -13,7 +13,7 @@ except Exception as e:
print("main.py: WARNING: could not import/run freezefs_mount_builtin: ", e)
from mpos import apps
apps.execute_script("builtin/system/button.py", True, False, False) # Install button handler through IRQ
apps.execute_script("builtin/system/button.py", True) # Install button handler through IRQ
def dummy():
pass