Commit 63086117 authored by Rahix's avatar Rahix

Merge 'Use simple_menu module'

See merge request card10/firmware!145
parents fe9c4218 7d32047b
......@@ -25,4 +25,6 @@ displaying menus. You can use it like this:
.. autoclass:: simple_menu.Menu
:members:
.. autodata:: simple_menu.TIMEOUT
.. autofunction:: simple_menu.button_events
"""
Personal State Script
===========
With this script you can
=====================
"""
import buttons
import color
import display
import os
import personal_state
import simple_menu
states = [
("No State", personal_state.NO_STATE),
......@@ -18,75 +16,35 @@ states = [
]
def button_events():
"""Iterate over button presses (event-loop)."""
yield 0
button_pressed = False
while True:
v = buttons.read(buttons.BOTTOM_LEFT | buttons.BOTTOM_RIGHT | buttons.TOP_RIGHT)
class StateMenu(simple_menu.Menu):
color_sel = color.WHITE
if v == 0:
button_pressed = False
def on_scroll(self, item, index):
personal_state.set(item[1], False)
if not button_pressed and v & buttons.BOTTOM_LEFT != 0:
button_pressed = True
yield buttons.BOTTOM_LEFT
def on_select(self, item, index):
personal_state.set(item[1], True)
os.exit()
if not button_pressed and v & buttons.BOTTOM_RIGHT != 0:
button_pressed = True
yield buttons.BOTTOM_RIGHT
def draw_entry(self, item, index, offset):
if item[1] == personal_state.NO_CONTACT:
bg = color.RED
fg = color.WHITE
elif item[1] == personal_state.CHAOS:
bg = color.CHAOSBLUE
fg = color.CHAOSBLUE_DARK
elif item[1] == personal_state.COMMUNICATION:
bg = color.COMMYELLOW
fg = color.COMMYELLOW_DARK
elif item[1] == personal_state.CAMP:
bg = color.CAMPGREEN
fg = color.CAMPGREEN_DARK
else:
bg = color.Color(100, 100, 100)
fg = color.Color(200, 200, 200)
if not button_pressed and v & buttons.TOP_RIGHT != 0:
button_pressed = True
yield buttons.TOP_RIGHT
COLOR1, COLOR2 = (color.CHAOSBLUE_DARK, color.CHAOSBLUE)
def draw_menu(disp, idx, offset):
disp.clear()
for y, i in enumerate(range(len(states) + idx - 3, len(states) + idx + 4)):
selected = states[i % len(states)]
disp.print(
" " + selected[0] + " " * (11 - len(selected[0])),
posy=offset + y * 20 - 40,
bg=COLOR1 if i % 2 == 0 else COLOR2,
)
disp.print(">", posy=20, fg=color.COMMYELLOW, bg=COLOR2 if idx % 2 == 0 else COLOR1)
disp.update()
def main():
disp = display.open()
numstates = len(states)
current, _ = personal_state.get()
for ev in button_events():
if ev == buttons.BOTTOM_RIGHT:
# Scroll down
draw_menu(disp, current, -8)
current = (current + 1) % numstates
state = states[current]
personal_state.set(state[1], False)
elif ev == buttons.BOTTOM_LEFT:
# Scroll up
draw_menu(disp, current, 8)
current = (current + numstates - 1) % numstates
state = states[current]
personal_state.set(state[1], False)
elif ev == buttons.TOP_RIGHT:
state = states[current]
personal_state.set(state[1], True)
# Select & start
disp.clear().update()
disp.close()
os.exit(0)
draw_menu(disp, current, 0)
self.disp.print(" " + str(item[0]) + " " * 9, posy=offset, fg=fg, bg=bg)
if __name__ == "__main__":
main()
StateMenu(states).run()
......@@ -5,286 +5,91 @@ You can customize this script however you want :) If you want to go back to
the default version, just delete this file; the firmware will recreate it on
next run.
"""
import buttons
import collections
import color
import display
import os
import utime
import ujson
import simple_menu
import sys
import ujson
import utime
BUTTON_TIMER_POPPED = -1
COLOR_BG = color.CHAOSBLUE_DARK
COLOR_BG_SEL = color.CHAOSBLUE
COLOR_ARROW = color.COMMYELLOW
COLOR_TEXT = color.COMMYELLOW
MAXCHARS = 11
HOMEAPP = "main.py"
def create_folders():
try:
os.mkdir("/apps")
except:
pass
def read_metadata(app_folder):
try:
info_file = "/apps/%s/metadata.json" % (app_folder)
with open(info_file) as f:
information = f.read()
return ujson.loads(information)
except Exception as e:
print("Failed to read metadata for %s" % (app_folder))
sys.print_exception(e)
return {
"author": "",
"name": app_folder,
"description": "",
"category": "",
"revision": 0,
}
App = collections.namedtuple("App", ["name", "path"])
def list_apps():
"""Create a list of available apps."""
apps = []
def enumerate_apps():
"""List all installed apps."""
for f in os.listdir("/"):
if f == "main.py":
yield App("Home", f)
# add main application
for mainFile in os.listdir("/"):
if mainFile == HOMEAPP:
apps.append(
[
"/%s" % HOMEAPP,
{
"author": "card10badge Team",
"name": "Home",
"description": "",
"category": "",
"revision": 0,
},
]
)
dirlist = [
entry for entry in sorted(os.listdir("/apps")) if not entry.startswith(".")
]
for app in sorted(os.listdir("/apps")):
if app.startswith("."):
continue
# list all hatchary style apps (not .elf and not .py)
# with or without metadata.json
for appFolder in dirlist:
if not (appFolder.endswith(".py") or appFolder.endswith(".elf")):
metadata = read_metadata(appFolder)
if not metadata.get("bin", None):
fileName = "/apps/%s/__init__.py" % appFolder
else:
fileName = "/apps/%s/%s" % (appFolder, metadata["bin"])
apps.append([fileName, metadata])
if app.endswith(".py") or app.endswith(".elf"):
yield App(app, "/apps/" + app)
continue
# list simple python scripts
for pyFile in dirlist:
if pyFile.endswith(".py"):
apps.append(
[
"/apps/%s" % pyFile,
{
"author": "",
"name": pyFile,
"description": "",
"category": "",
"revision": 0,
},
]
)
try:
with open("/apps/" + app + "/metadata.json") as f:
info = ujson.load(f)
# list simple elf binaries
for elfFile in dirlist:
if elfFile.endswith(".elf"):
apps.append(
[
"/apps/%s" % elfFile,
{
"author": "",
"name": elfFile,
"description": "",
"category": "",
"revision": 0,
},
]
yield App(
info["name"], "/apps/{}/{}".format(app, info.get("bin", "__init__.py"))
)
except Exception as e:
print("Could not load /apps/{}/metadata.json!".format(app))
sys.print_exception(e)
class MainMenu(simple_menu.Menu):
timeout = 30.0
def entry2name(self, app):
return app.name
def on_select(self, app, index):
self.disp.clear().update()
try:
print("Trying to load " + app.path)
os.exec(app.path)
except OSError as e:
print("Loading failed: ")
sys.print_exception(e)
self.error("Loading", "failed")
utime.sleep(1.0)
os.exit(1)
def on_timeout(self):
try:
f = open("main.py")
f.close()
os.exec("main.py")
except OSError:
pass
def no_apps_message():
"""Display a warning if no apps are installed."""
with display.open() as disp:
disp.clear(color.COMMYELLOW)
disp.print(
" No apps ", posx=17, posy=20, fg=color.COMMYELLOW_DARK, bg=color.COMMYELLOW
)
disp.print(
"available", posx=17, posy=40, fg=color.COMMYELLOW_DARK, bg=color.COMMYELLOW
)
disp.update()
return apps
def button_events(timeout=0):
"""Iterate over button presses (event-loop)."""
yield 0
button_pressed = False
count = 0
while True:
v = buttons.read(buttons.BOTTOM_LEFT | buttons.BOTTOM_RIGHT | buttons.TOP_RIGHT)
if timeout > 0 and count > 0 and count % timeout == 0:
yield BUTTON_TIMER_POPPED
if timeout > 0:
count += 1
if v == 0:
button_pressed = False
if not button_pressed and v & buttons.BOTTOM_LEFT != 0:
button_pressed = True
yield buttons.BOTTOM_LEFT
if not button_pressed and v & buttons.BOTTOM_RIGHT != 0:
button_pressed = True
yield buttons.BOTTOM_RIGHT
if not button_pressed and v & buttons.TOP_RIGHT != 0:
button_pressed = True
yield buttons.TOP_RIGHT
utime.sleep_ms(10)
utime.sleep(0.5)
def triangle(disp, x, y, left, scale=6, color=[255, 0, 0]):
"""Draw a triangle to show there's more text in this line"""
yf = 1 if left else -1
disp.line(x - scale * yf, int(y + scale / 2), x, y, col=color)
disp.line(x, y, x, y + scale, col=color)
disp.line(x, y + scale, x - scale * yf, y + int(scale / 2), col=color)
if __name__ == "__main__":
apps = list(enumerate_apps())
def draw_menu(disp, applist, pos, appcount, lineoffset):
disp.clear()
start = 0
if pos > 0:
start = pos - 1
if start + 4 > appcount:
start = appcount - 4
if start < 0:
start = 0
for i, app in enumerate(applist):
if i >= start + 4 or i >= appcount:
break
if i >= start:
disp.rect(
0,
(i - start) * 20,
159,
(i - start) * 20 + 20,
col=COLOR_BG_SEL if i == pos else COLOR_BG,
)
line = app[1]["name"]
linelength = len(line)
off = 0
# calc line offset for scrolling
if i == pos and linelength > (MAXCHARS - 1) and lineoffset > 0:
off = (
lineoffset
if lineoffset + (MAXCHARS - 1) < linelength
else linelength - (MAXCHARS - 1)
)
if lineoffset > linelength:
off = 0
disp.print(
" " + line[off : (off + (MAXCHARS - 1))],
posy=(i - start) * 20,
fg=COLOR_TEXT,
bg=COLOR_BG_SEL if i == pos else COLOR_BG,
)
if i == pos:
disp.print(">", posy=(i - start) * 20, fg=COLOR_ARROW, bg=COLOR_BG_SEL)
if linelength > (MAXCHARS - 1) and off < linelength - (MAXCHARS - 1):
triangle(disp, 153, (i - start) * 20 + 6, False, 6)
triangle(disp, 154, (i - start) * 20 + 7, False, 4)
triangle(disp, 155, (i - start) * 20 + 8, False, 2)
if off > 0:
triangle(disp, 24, (i - start) * 20 + 6, True, 6)
triangle(disp, 23, (i - start) * 20 + 7, True, 4)
triangle(disp, 22, (i - start) * 20 + 8, True, 2)
disp.update()
def main():
create_folders()
disp = display.open()
applist = list_apps()
numapps = len(applist)
current = 0
lineoffset = 0
timerscrollspeed = 1
timerstartscroll = 5
timercountpopped = 0
timerinactivity = 100
for ev in button_events(10):
if numapps == 0:
disp.clear(COLOR_BG)
disp.print(" No apps ", posx=17, posy=20, fg=COLOR_TEXT, bg=COLOR_BG)
disp.print("available", posx=17, posy=40, fg=COLOR_TEXT, bg=COLOR_BG)
disp.update()
continue
if ev == buttons.BOTTOM_RIGHT:
# Scroll down
current = (current + 1) % numapps
lineoffset = 0
timercountpopped = 0
elif ev == buttons.BOTTOM_LEFT:
# Scroll up
current = (current + numapps - 1) % numapps
lineoffset = 0
timercountpopped = 0
elif ev == BUTTON_TIMER_POPPED:
timercountpopped += 1
if (
timercountpopped >= timerstartscroll
and (timercountpopped - timerstartscroll) % timerscrollspeed == 0
):
lineoffset += 1
if applist[0][0] == "/%s" % HOMEAPP and timercountpopped >= timerinactivity:
print("Inactivity timer popped")
disp.clear().update()
disp.close()
try:
os.exec("/%s" % HOMEAPP)
except OSError as e:
print("Loading failed: ", e)
os.exit(1)
elif ev == buttons.TOP_RIGHT:
# Select & start
disp.clear().update()
disp.close()
try:
os.exec(applist[current][0])
except OSError as e:
print("Loading failed: ", e)
os.exit(1)
draw_menu(disp, applist, current, numapps, lineoffset)
if apps == []:
no_apps_message()
if __name__ == "__main__":
try:
main()
except Exception as e:
sys.print_exception(e)
with display.open() as d:
d.clear(color.COMMYELLOW)
d.print("Menu", posx=52, posy=20, fg=color.COMMYELLOW_DARK, bg=color.COMMYELLOW)
d.print("crashed", posx=31, posy=40, fg=color.COMMYELLOW_DARK, bg=color.COMMYELLOW)
d.update()
utime.sleep(2)
os.exit(1)
MainMenu(apps).run()
import buttons
import color
import display
import sys
import utime
TIMEOUT = 0x100
""":py:func:`~simple_menu.button_events` timeout marker."""
def button_events():
def button_events(timeout=None):
"""
Iterate over button presses (event-loop).
......@@ -26,11 +31,30 @@ def button_events():
pass
.. versionadded:: 1.4
:param float,optional timeout:
Timeout after which the generator should yield in any case. If a
timeout is defined, the generator will periodically yield
:py:data:`simple_menu.TIMEOUT`.
.. versionadded:: 1.9
"""
yield 0
v = buttons.read(buttons.BOTTOM_LEFT | buttons.BOTTOM_RIGHT | buttons.TOP_RIGHT)
button_pressed = True if v != 0 else False
if timeout is not None:
timeout = int(timeout * 1000)
next_tick = utime.time_ms() + timeout
while True:
if timeout is not None:
current_time = utime.time_ms()
if current_time >= next_tick:
next_tick += timeout
yield TIMEOUT
v = buttons.read(buttons.BOTTOM_LEFT | buttons.BOTTOM_RIGHT | buttons.TOP_RIGHT)
if v == 0:
......@@ -49,6 +73,10 @@ def button_events():
yield buttons.TOP_RIGHT
class _ExitMenuException(Exception):
pass
class Menu:
"""
A simple menu for card10.
......@@ -77,6 +105,21 @@ class Menu:
color_sel = color.COMMYELLOW
"""Color of the selector."""
scroll_speed = 0.5
"""
Time to wait before scrolling to the right.
.. versionadded:: 1.9
"""
timeout = None
"""
Optional timeout for inactivity. Once this timeout is reached,
:py:meth:`~simple_menu.Menu.on_timeout` will be called.
.. versionadded:: 1.9
"""
def on_scroll(self, item, index):
"""
Hook when the selector scrolls to a new item.
......@@ -102,12 +145,30 @@ class Menu:
"""
pass
def on_timeout(self):
"""
The inactivity timeout has been triggered. See
:py:attr:`simple_menu.Menu.timeout`.
.. versionadded:: 1.9
"""
self.exit()
def exit(self):
"""
Exit the event-loop. This should be called from inside an ``on_*`` hook.
.. versionadded:: 1.9
"""
raise _ExitMenuException()
def __init__(self, entries):
if len(entries) == 0:
raise ValueError("at least one entry is required")
self.entries = entries
self.idx = 0
self.select_time = utime.time_ms()
self.disp = display.open()
def entry2name(self, value):
......@@ -142,8 +203,21 @@ class Menu:
but **not** an index into ``entries``.
:param int offset: Y-offset for this entry.
"""
string = self.entry2name(value)
if offset != 20 or len(string) < 10:
string = " " + string + " " * 9
else:
# Slowly scroll entry to the side
time_offset = (utime.time_ms() - self.select_time) // int(
self.scroll_speed * 1000
)
time_offset = time_offset % (len(string) - 7) - 1
time_offset = min(len(string) - 10, max(0, time_offset))
string = " " + string[time_offset:]
self.disp.print(
" " + self.entry2name(value) + " " * 9,
string,
posy=offset,
fg=self.color_text,
bg=self.color_1 if index % 2 == 0 else self.color_2,
......@@ -171,18 +245,70 @@ class Menu:
self.disp.update()