Badger2040: JSON app state.

This commit is contained in:
Phil Howard 2022-03-25 14:25:27 +00:00
parent b8d5a3db75
commit b85792f254
4 changed files with 166 additions and 182 deletions

View File

@ -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()

View File

@ -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()

View File

@ -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

View File

@ -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()