From b85792f2544f45411ee21ccdfd8c81af0a8ca8bb Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Fri, 25 Mar 2022 14:25:27 +0000 Subject: [PATCH] Badger2040: JSON app state. --- micropython/examples/badger2040/badge.py | 48 +------ micropython/examples/badger2040/badger_os.py | 137 +++++++++++++------ micropython/examples/badger2040/image.py | 67 +++------ micropython/examples/badger2040/launcher.py | 96 ++++++------- 4 files changed, 166 insertions(+), 182 deletions(-) diff --git a/micropython/examples/badger2040/badge.py b/micropython/examples/badger2040/badge.py index 352c35df..a72a674b 100644 --- a/micropython/examples/badger2040/badge.py +++ b/micropython/examples/badger2040/badge.py @@ -20,10 +20,6 @@ LEFT_PADDING = 5 NAME_PADDING = 20 DETAIL_SPACING = 10 -OVERLAY_BORDER = 40 -OVERLAY_SPACING = 20 -OVERLAY_TEXT_SIZE = 0.6 - DEFAULT_TEXT = """mustelid inc H. Badger RP2040 @@ -63,42 +59,6 @@ def truncatestring(text, text_size, width): # Drawing functions # ------------------------------ -# Draw an overlay box with a given message within it -def draw_overlay(message, width, height, line_spacing, text_size): - - # Draw a light grey background - display.pen(12) - display.rectangle((WIDTH - width) // 2, (HEIGHT - height) // 2, width, height) - - # Take the provided message and split it up into - # lines that fit within the specified width - words = message.split(" ") - lines = [] - line = "" - appended_line = "" - for word in words: - if len(word) > 0: - appended_line += " " - appended_line += word - if display.measure_text(appended_line, text_size) >= width: - lines.append(line) - appended_line = word - else: - line = appended_line - if len(line) != 0: - lines.append(line) - - display.pen(0) - display.thickness(2) - - # Display each line of text from the message, centre-aligned - num_lines = len(lines) - for i in range(num_lines): - length = display.measure_text(lines[i], text_size) - current_line = (i * line_spacing) - ((num_lines - 1) * line_spacing) // 2 - display.text(lines[i], (WIDTH - length) // 2, (HEIGHT // 2) + current_line, text_size) - - # Draw the badge, including user text def draw_badge(): display.pen(0) @@ -203,10 +163,6 @@ detail2_title = truncatestring(detail2_title, DETAILS_TEXT_SIZE, TEXT_WIDTH) detail2_text = truncatestring(detail2_text, DETAILS_TEXT_SIZE, TEXT_WIDTH - DETAIL_SPACING - display.measure_text(detail2_title, DETAILS_TEXT_SIZE)) -# Tell launcher to relaunch this app on wake -if not display.woken(): - badger_os.state_save("badge") - # ------------------------------ # Main program @@ -216,9 +172,7 @@ draw_badge() while True: if display.pressed(badger2040.BUTTON_A) or display.pressed(badger2040.BUTTON_B) or display.pressed(badger2040.BUTTON_C) or display.pressed(badger2040.BUTTON_UP) or display.pressed(badger2040.BUTTON_DOWN): - draw_overlay("To change the text, connect Badger2040 to a PC, load up Thonny, and modify badge.txt", - WIDTH - OVERLAY_BORDER, HEIGHT - OVERLAY_BORDER, OVERLAY_SPACING, OVERLAY_TEXT_SIZE) - display.update() + badger_os.warning(display, "To change the text, connect Badger2040 to a PC, load up Thonny, and modify badge.txt") time.sleep(4) draw_badge() diff --git a/micropython/examples/badger2040/badger_os.py b/micropython/examples/badger2040/badger_os.py index 0aeb83d2..ef2a1e20 100644 --- a/micropython/examples/badger2040/badger_os.py +++ b/micropython/examples/badger2040/badger_os.py @@ -2,11 +2,11 @@ import os import gc +import time +import json import machine import badger2040 -STATE_FILE = "appstate.txt" - def get_battery_level(): # Battery measurement @@ -47,55 +47,62 @@ def get_disk_usage(): return f_total_size, f_used, f_free -def state_app(): - try: - with open(STATE_FILE, "r") as f: - return f.readline().strip() - except OSError: - return None +def state_running(): + state = {"running": "launcher"} + state_load("launcher", state) + return state["running"] + + +def state_clear_running(): + state_modify("launcher", {"running": "launcher"}) + + +def state_set_running(app): + state_modify("launcher", {"running": app}) def state_launch(): - app = state_app() - if app is not None: + app = state_running() + if app is not None and app != "launcher": launch("_" + app) -def state_delete(): +def state_delete(app): try: - os.remove(STATE_FILE) + os.remove("{}_state.txt".format(app)) except OSError: pass -def state_save(title, *args): - with open(STATE_FILE, "w") as f: - f.write("{}\n".format(title)) - for arg in args: - f.write("{}\n".format(arg)) +def state_save(app, data): + with open("{}_state.txt".format(app), "w") as f: + f.write(json.dumps(data)) + f.flush() -def state_load(title, *defaults): - data = [] +def state_modify(app, data): + state = {} + state_load(app, state) + state.update(data) + state_save(app, state) + + +def state_load(app, defaults): try: - with open(STATE_FILE, "r") as f: - if f.readline().strip() != title: - return defaults - for default in defaults: - t = type(default) - if t is bool: - data.append(f.readline().strip() == "True") - else: - data.append(t(f.readline().strip())) - return data - except OSError: - return defaults + data = json.loads(open("{}_state.txt".format(app), "r").read()) + if type(data) is dict: + defaults.update(data) + return True + except (OSError, ValueError): + pass + + state_save(app, defaults) + return False def launch(file): - for k in locals().keys(): - if k not in ("gc", "file", "machine"): - del locals()[k] + state_set_running(file[1:]) + gc.collect() button_a = machine.Pin(badger2040.BUTTON_A, machine.Pin.IN, machine.Pin.PULL_DOWN) @@ -103,18 +110,66 @@ def launch(file): def quit_to_launcher(pin): if button_a.value() and button_c.value(): - import os - try: - os.remove(STATE_FILE) - except OSError: - pass + state_clear_running() + time.sleep(0.1) # Needed to stop write fail machine.reset() button_a.irq(trigger=machine.Pin.IRQ_RISING, handler=quit_to_launcher) button_c.irq(trigger=machine.Pin.IRQ_RISING, handler=quit_to_launcher) try: - __import__(file[1:]) # Try to import _[file] (drop underscore prefix) + try: + __import__(file[1:]) # Try to import _[file] (drop underscore prefix) + except ImportError: + __import__(file) # Failover to importing [_file] + except ImportError: - __import__(file) # Failover to importing [_file] + # If the app doesn't exist, notify the user + warning(None, "Could not launch: " + file[1:]) + time.sleep(4.0) + except Exception as e: + # If the app throws an error, catch it and display! + print(e) + warning(None, str(e)) + time.sleep(4.0) + + # If the app exits or errors, do not relaunch! + state_clear_running() machine.reset() # Exit back to launcher + + +# Draw an overlay box with a given message within it +def warning(display, message, width=badger2040.WIDTH - 40, height=badger2040.HEIGHT - 40, line_spacing=20, text_size=0.6): + if display is None: + display = badger2040.Badger2040() + display.led(128) + + # Draw a light grey background + display.pen(12) + display.rectangle((badger2040.WIDTH - width) // 2, (badger2040.HEIGHT - height) // 2, width, height) + + # Take the provided message and split it up into + # lines that fit within the specified width + words = message.split(" ") + + lines = [] + current_line = "" + for word in words: + if display.measure_text(current_line + word + " ", text_size) < width: + current_line += word + " " + else: + lines.append(current_line.strip()) + current_line = word + " " + lines.append(current_line.strip()) + + display.pen(0) + display.thickness(2) + + # Display each line of text from the message, centre-aligned + num_lines = len(lines) + for i in range(num_lines): + length = display.measure_text(lines[i], text_size) + current_line = (i * line_spacing) - ((num_lines - 1) * line_spacing) // 2 + display.text(lines[i], (badger2040.WIDTH - length) // 2, (badger2040.HEIGHT // 2) + current_line, text_size) + + display.update() diff --git a/micropython/examples/badger2040/image.py b/micropython/examples/badger2040/image.py index 402f5070..ccaf01e5 100644 --- a/micropython/examples/badger2040/image.py +++ b/micropython/examples/badger2040/image.py @@ -2,7 +2,7 @@ import os import sys import time import badger2040 -from badger2040 import WIDTH, HEIGHT +from badger2040 import HEIGHT import badger_os @@ -50,40 +50,11 @@ except OSError: image = bytearray(int(296 * 128 / 8)) -current_image = 0 -show_info = True - -# Draw an overlay box with a given message within it -def draw_overlay(message, width, height, line_spacing, text_size): - - # Draw a light grey background - display.pen(12) - display.rectangle((WIDTH - width) // 2, (HEIGHT - height) // 2, width, height) - - # Take the provided message and split it up into - # lines that fit within the specified width - words = message.split(" ") - - lines = [] - current_line = "" - for word in words: - if display.measure_text(current_line + word + " ", text_size) < width: - current_line += word + " " - else: - lines.append(current_line.strip()) - current_line = word + " " - lines.append(current_line.strip()) - - display.pen(0) - display.thickness(2) - - # Display each line of text from the message, centre-aligned - num_lines = len(lines) - for i in range(num_lines): - length = display.measure_text(lines[i], text_size) - current_line = (i * line_spacing) - ((num_lines - 1) * line_spacing) // 2 - display.text(lines[i], (WIDTH - length) // 2, (HEIGHT // 2) + current_line, text_size) +state = { + "current_image": 0, + "show_info": True +} def show_image(n): @@ -92,7 +63,7 @@ def show_image(n): open("images/{}".format(file), "r").readinto(image) display.image(image) - if show_info: + if state["show_info"]: name_length = display.measure_text(name, 0.5) display.pen(0) display.rectangle(0, HEIGHT - 21, name_length + 11, 21) @@ -106,7 +77,7 @@ def show_image(n): y = int((128 / 2) - (TOTAL_IMAGES * 10 / 2) + (i * 10)) display.pen(0) display.rectangle(x, y, 8, 8) - if current_image != i: + if state["current_image"] != i: display.pen(15) display.rectangle(x + 1, y + 1, 6, 6) @@ -116,40 +87,40 @@ def show_image(n): if TOTAL_IMAGES == 0: display.pen(15) display.clear() - draw_overlay("To run this demo, create an /images directory on your device and upload some 1bit 296x128 pixel images.", WIDTH - OVERLAY_BORDER, HEIGHT - OVERLAY_BORDER, OVERLAY_SPACING, OVERLAY_TEXT_SIZE) - display.update() + badger_os.warning(display, "To run this demo, create an /images directory on your device and upload some 1bit 296x128 pixel images.") + time.sleep(4.0) sys.exit() -current_image, show_info = badger_os.state_load("image", 0, True) +badger_os.state_load("image", state) changed = not display.woken() while True: if display.pressed(badger2040.BUTTON_UP): - if current_image > 0: - current_image -= 1 + if state["current_image"] > 0: + state["current_image"] -= 1 changed = True if display.pressed(badger2040.BUTTON_DOWN): - if current_image < TOTAL_IMAGES - 1: - current_image += 1 + if state["current_image"] < TOTAL_IMAGES - 1: + state["current_image"] += 1 changed = True if display.pressed(badger2040.BUTTON_A): - show_info = not show_info + state["show_info"] = not state["show_info"] changed = True if display.pressed(badger2040.BUTTON_B) or display.pressed(badger2040.BUTTON_C): display.pen(15) display.clear() - draw_overlay("To add images connect Badger2040 to a PC, load up Thonny, and see readme.txt in images/", WIDTH - OVERLAY_BORDER, HEIGHT - OVERLAY_BORDER, OVERLAY_SPACING, 0.5) + badger_os.warning(display, "To add images connect Badger2040 to a PC, load up Thonny, and see readme.txt in images/") display.update() - print(current_image) + print(state["current_image"]) time.sleep(4) changed = True if changed: - badger_os.state_save("image", current_image, show_info) - show_image(current_image) + badger_os.state_save("image", state) + show_image(state["current_image"]) changed = False # Halt the Badger to save power, it will wake up if any of the front buttons are pressed diff --git a/micropython/examples/badger2040/launcher.py b/micropython/examples/badger2040/launcher.py index 90815767..5c33b892 100644 --- a/micropython/examples/badger2040/launcher.py +++ b/micropython/examples/badger2040/launcher.py @@ -1,3 +1,4 @@ +import gc import time import math import machine @@ -6,35 +7,33 @@ from badger2040 import WIDTH import launchericons import badger_os +# Reduce clock speed to 48MHz, that's fast enough! +machine.freq(48000000) + +if badger2040.pressed_to_wake(badger2040.BUTTON_A) and badger2040.pressed_to_wake(badger2040.BUTTON_C): + # Pressing A and C together at start quits app + badger_os.state_clear_running() + badger2040.clear_pressed_to_wake() +else: + # Otherwise restore previously running app + badger_os.state_launch() + # for e.g. 2xAAA batteries, try max 3.4 min 3.0 MAX_BATTERY_VOLTAGE = 4.0 MIN_BATTERY_VOLTAGE = 3.2 -# Reduce clock speed to 48MHz, that's fast enough! -machine.freq(48000000) - - -# Restore previously running app -try: - # Pressing A and C together at start quits app - if badger2040.pressed_to_wake(badger2040.BUTTON_A) and badger2040.pressed_to_wake(badger2040.BUTTON_C): - badger_os.state_delete() - else: - if badger_os.state_app() != "launcher": - badger_os.state_launch() -except OSError: - pass -except ImportError: - # Happens if appstate names an unknown app. Delete appstate and reset - badger_os.state_delete() - machine.reset() - - display = badger2040.Badger2040() display.led(128) -page, font_size, inverted = badger_os.state_load("launcher", 0, 1, False) -changed = badger_os.state_app() != "launcher" +state = { + "page": 0, + "font_size": 1, + "inverted": False, + "running": "launcher" +} + +badger_os.state_load("launcher", state) +changed = state["running"] != "launcher" icons = bytearray(launchericons.data()) icons_width = 576 @@ -123,23 +122,23 @@ def render(): display.pen(0) display.thickness(2) - max_icons = min(3, len(examples[(page * 3):])) + max_icons = min(3, len(examples[(state["page"] * 3):])) for i in range(max_icons): x = centers[i] - label, icon = examples[i + (page * 3)] + label, icon = examples[i + (state["page"] * 3)] label = label[1:].replace("_", " ") display.pen(0) display.icon(icons, icon, icons_width, 64, x - 32, 24) - w = display.measure_text(label, font_sizes[font_size]) - display.text(label, x - int(w / 2), 16 + 80, font_sizes[font_size]) + w = display.measure_text(label, font_sizes[state["font_size"]]) + display.text(label, x - int(w / 2), 16 + 80, font_sizes[state["font_size"]]) for i in range(MAX_PAGE): x = 286 y = int((128 / 2) - (MAX_PAGE * 10 / 2) + (i * 10)) display.pen(0) display.rectangle(x, y, 8, 8) - if page != i: + if state["page"] != i: display.pen(15) display.rectangle(x + 1, y + 1, 6, 6) @@ -159,15 +158,20 @@ def render(): def launch_example(index): while display.pressed(badger2040.BUTTON_A) or display.pressed(badger2040.BUTTON_B) or display.pressed(badger2040.BUTTON_C) or display.pressed(badger2040.BUTTON_UP) or display.pressed(badger2040.BUTTON_DOWN): time.sleep(0.01) - try: - badger_os.launch(examples[(page * 3) + index][0]) - return True - except IndexError: - return False + + file = examples[(state["page"] * 3) + index][0] + + for k in locals().keys(): + if k not in ("gc", "file", "badger_os"): + del locals()[k] + + gc.collect() + + badger_os.launch(file) def button(pin): - global page, font_size, inverted, changed + global changed changed = True if not display.pressed(badger2040.BUTTON_USER): # User button is NOT held down @@ -178,27 +182,27 @@ def button(pin): if pin == badger2040.BUTTON_C: launch_example(2) if pin == badger2040.BUTTON_UP: - if page > 0: - page -= 1 + if state["page"] > 0: + state["page"] -= 1 render() if pin == badger2040.BUTTON_DOWN: - if page < MAX_PAGE - 1: - page += 1 + if state["page"] < MAX_PAGE - 1: + state["page"] += 1 render() else: # User button IS held down if pin == badger2040.BUTTON_UP: - font_size += 1 - if font_size == len(font_sizes): - font_size = 0 + state["font_size"] += 1 + if state["font_size"] == len(font_sizes): + state["font_size"] = 0 render() if pin == badger2040.BUTTON_DOWN: - font_size -= 1 - if font_size < 0: - font_size = 0 + state["font_size"] -= 1 + if state["font_size"] < 0: + state["font_size"] = 0 render() if pin == badger2040.BUTTON_A: - inverted = not inverted - display.invert(inverted) + state["inverted"] = not state["inverted"] + display.invert(state["inverted"]) render() @@ -227,7 +231,7 @@ while True: button(badger2040.BUTTON_DOWN) if changed: - badger_os.state_save("launcher", page, font_size, inverted) + badger_os.state_save("launcher", state) changed = False display.halt()