Skip to content
Snippets Groups Projects
Commit 63086117 authored by rahix's avatar rahix
Browse files

Merge 'Use simple_menu module'

See merge request card10/firmware!145
parents fe9c4218 7d32047b
No related branches found
No related tags found
No related merge requests found
...@@ -25,4 +25,6 @@ displaying menus. You can use it like this: ...@@ -25,4 +25,6 @@ displaying menus. You can use it like this:
.. autoclass:: simple_menu.Menu .. autoclass:: simple_menu.Menu
:members: :members:
.. autodata:: simple_menu.TIMEOUT
.. autofunction:: simple_menu.button_events .. autofunction:: simple_menu.button_events
""" """
Personal State Script Personal State Script
=========== =====================
With this script you can
""" """
import buttons
import color import color
import display
import os import os
import personal_state import personal_state
import simple_menu
states = [ states = [
("No State", personal_state.NO_STATE), ("No State", personal_state.NO_STATE),
...@@ -18,75 +16,35 @@ states = [ ...@@ -18,75 +16,35 @@ states = [
] ]
def button_events(): class StateMenu(simple_menu.Menu):
"""Iterate over button presses (event-loop).""" color_sel = color.WHITE
yield 0
button_pressed = False
while True:
v = buttons.read(buttons.BOTTOM_LEFT | buttons.BOTTOM_RIGHT | buttons.TOP_RIGHT)
if v == 0: def on_scroll(self, item, index):
button_pressed = False personal_state.set(item[1], False)
if not button_pressed and v & buttons.BOTTOM_LEFT != 0: def on_select(self, item, index):
button_pressed = True personal_state.set(item[1], True)
yield buttons.BOTTOM_LEFT os.exit()
if not button_pressed and v & buttons.BOTTOM_RIGHT != 0: def draw_entry(self, item, index, offset):
button_pressed = True if item[1] == personal_state.NO_CONTACT:
yield buttons.BOTTOM_RIGHT 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: self.disp.print(" " + str(item[0]) + " " * 9, posy=offset, fg=fg, bg=bg)
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)
if __name__ == "__main__": 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 ...@@ -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 the default version, just delete this file; the firmware will recreate it on
next run. next run.
""" """
import buttons import collections
import color import color
import display import display
import os import os
import utime import simple_menu
import ujson
import sys import sys
import ujson
import utime
BUTTON_TIMER_POPPED = -1 App = collections.namedtuple("App", ["name", "path"])
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,
}
def list_apps():
"""Create a list of available apps."""
apps = []
# add main application def enumerate_apps():
for mainFile in os.listdir("/"): """List all installed apps."""
if mainFile == HOMEAPP: for f in os.listdir("/"):
apps.append( if f == "main.py":
[ yield App("Home", f)
"/%s" % HOMEAPP,
{
"author": "card10badge Team",
"name": "Home",
"description": "",
"category": "",
"revision": 0,
},
]
)
dirlist = [ for app in sorted(os.listdir("/apps")):
entry for entry in sorted(os.listdir("/apps")) if not entry.startswith(".") if app.startswith("."):
] continue
# list all hatchary style apps (not .elf and not .py) if app.endswith(".py") or app.endswith(".elf"):
# with or without metadata.json yield App(app, "/apps/" + app)
for appFolder in dirlist: continue
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])
# list simple python scripts try:
for pyFile in dirlist: with open("/apps/" + app + "/metadata.json") as f:
if pyFile.endswith(".py"): info = ujson.load(f)
apps.append(
[
"/apps/%s" % pyFile,
{
"author": "",
"name": pyFile,
"description": "",
"category": "",
"revision": 0,
},
]
)
# list simple elf binaries yield App(
for elfFile in dirlist: info["name"], "/apps/{}/{}".format(app, info.get("bin", "__init__.py"))
if elfFile.endswith(".elf"):
apps.append(
[
"/apps/%s" % elfFile,
{
"author": "",
"name": elfFile,
"description": "",
"category": "",
"revision": 0,
},
]
) )
except Exception as e:
print("Could not load /apps/{}/metadata.json!".format(app))
sys.print_exception(e)
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)
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)
class MainMenu(simple_menu.Menu):
timeout = 30.0
def draw_menu(disp, applist, pos, appcount, lineoffset): def entry2name(self, app):
disp.clear() return app.name
start = 0 def on_select(self, app, index):
if pos > 0: self.disp.clear().update()
start = pos - 1 try:
if start + 4 > appcount: print("Trying to load " + app.path)
start = appcount - 4 os.exec(app.path)
if start < 0: except OSError as e:
start = 0 print("Loading failed: ")
sys.print_exception(e)
self.error("Loading", "failed")
utime.sleep(1.0)
os.exit(1)
for i, app in enumerate(applist): def on_timeout(self):
if i >= start + 4 or i >= appcount: try:
break f = open("main.py")
if i >= start: f.close()
disp.rect( os.exec("main.py")
0, except OSError:
(i - start) * 20, pass
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 def no_apps_message():
if i == pos and linelength > (MAXCHARS - 1) and lineoffset > 0: """Display a warning if no apps are installed."""
off = ( with display.open() as disp:
lineoffset disp.clear(color.COMMYELLOW)
if lineoffset + (MAXCHARS - 1) < linelength disp.print(
else linelength - (MAXCHARS - 1) " No apps ", posx=17, posy=20, fg=color.COMMYELLOW_DARK, bg=color.COMMYELLOW
) )
if lineoffset > linelength:
off = 0
disp.print( disp.print(
" " + line[off : (off + (MAXCHARS - 1))], "available", posx=17, posy=40, fg=color.COMMYELLOW_DARK, bg=color.COMMYELLOW
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() disp.update()
continue
if ev == buttons.BOTTOM_RIGHT:
# Scroll down
current = (current + 1) % numapps
lineoffset = 0
timercountpopped = 0
elif ev == buttons.BOTTOM_LEFT: while True:
# Scroll up utime.sleep(0.5)
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: if __name__ == "__main__":
# Select & start apps = list(enumerate_apps())
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__": MainMenu(apps).run()
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)
import buttons import buttons
import color import color
import display 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). Iterate over button presses (event-loop).
...@@ -26,11 +31,30 @@ def button_events(): ...@@ -26,11 +31,30 @@ def button_events():
pass pass
.. versionadded:: 1.4 .. 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 yield 0
v = buttons.read(buttons.BOTTOM_LEFT | buttons.BOTTOM_RIGHT | buttons.TOP_RIGHT) v = buttons.read(buttons.BOTTOM_LEFT | buttons.BOTTOM_RIGHT | buttons.TOP_RIGHT)
button_pressed = True if v != 0 else False 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: 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) v = buttons.read(buttons.BOTTOM_LEFT | buttons.BOTTOM_RIGHT | buttons.TOP_RIGHT)
if v == 0: if v == 0:
...@@ -49,6 +73,10 @@ def button_events(): ...@@ -49,6 +73,10 @@ def button_events():
yield buttons.TOP_RIGHT yield buttons.TOP_RIGHT
class _ExitMenuException(Exception):
pass
class Menu: class Menu:
""" """
A simple menu for card10. A simple menu for card10.
...@@ -77,6 +105,21 @@ class Menu: ...@@ -77,6 +105,21 @@ class Menu:
color_sel = color.COMMYELLOW color_sel = color.COMMYELLOW
"""Color of the selector.""" """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): def on_scroll(self, item, index):
""" """
Hook when the selector scrolls to a new item. Hook when the selector scrolls to a new item.
...@@ -102,12 +145,30 @@ class Menu: ...@@ -102,12 +145,30 @@ class Menu:
""" """
pass 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): def __init__(self, entries):
if len(entries) == 0: if len(entries) == 0:
raise ValueError("at least one entry is required") raise ValueError("at least one entry is required")
self.entries = entries self.entries = entries
self.idx = 0 self.idx = 0
self.select_time = utime.time_ms()
self.disp = display.open() self.disp = display.open()
def entry2name(self, value): def entry2name(self, value):
...@@ -142,8 +203,21 @@ class Menu: ...@@ -142,8 +203,21 @@ class Menu:
but **not** an index into ``entries``. but **not** an index into ``entries``.
:param int offset: Y-offset for this entry. :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.disp.print(
" " + self.entry2name(value) + " " * 9, string,
posy=offset, posy=offset,
fg=self.color_text, fg=self.color_text,
bg=self.color_1 if index % 2 == 0 else self.color_2, bg=self.color_1 if index % 2 == 0 else self.color_2,
...@@ -171,18 +245,70 @@ class Menu: ...@@ -171,18 +245,70 @@ class Menu:
self.disp.update() self.disp.update()
def error(self, line1, line2=""):
"""
Display an error message.
:param str line1: First line of the error message.
:param str line2: Second line of the error message.
.. versionadded:: 1.9
"""
self.disp.clear(color.COMMYELLOW)
offset = max(0, (160 - len(line1) * 14) // 2)
self.disp.print(
line1, posx=offset, posy=20, fg=color.COMMYELLOW_DARK, bg=color.COMMYELLOW
)
offset = max(0, (160 - len(line2) * 14) // 2)
self.disp.print(
line2, posx=offset, posy=40, fg=color.COMMYELLOW_DARK, bg=color.COMMYELLOW
)
self.disp.update()
def run(self): def run(self):
"""Start the event-loop.""" """Start the event-loop."""
for ev in button_events(): try:
timeout = self.scroll_speed
if self.timeout is not None and self.timeout < self.scroll_speed:
timeout = self.timeout
for ev in button_events(timeout):
if ev == buttons.BOTTOM_RIGHT: if ev == buttons.BOTTOM_RIGHT:
self.select_time = utime.time_ms()
self.draw_menu(-8) self.draw_menu(-8)
self.idx = (self.idx + 1) % len(self.entries) self.idx = (self.idx + 1) % len(self.entries)
try:
self.on_scroll(self.entries[self.idx], self.idx) self.on_scroll(self.entries[self.idx], self.idx)
except Exception as e:
print("Exception during menu.on_scroll():")
sys.print_exception(e)
elif ev == buttons.BOTTOM_LEFT: elif ev == buttons.BOTTOM_LEFT:
self.select_time = utime.time_ms()
self.draw_menu(8) self.draw_menu(8)
self.idx = (self.idx + len(self.entries) - 1) % len(self.entries) self.idx = (self.idx + len(self.entries) - 1) % len(self.entries)
try:
self.on_scroll(self.entries[self.idx], self.idx) self.on_scroll(self.entries[self.idx], self.idx)
except Exception as e:
print("Exception during menu.on_scroll():")
sys.print_exception(e)
elif ev == buttons.TOP_RIGHT: elif ev == buttons.TOP_RIGHT:
try:
self.on_select(self.entries[self.idx], self.idx) self.on_select(self.entries[self.idx], self.idx)
self.select_time = utime.time_ms()
except Exception as e:
print("Menu crashed!")
sys.print_exception(e)
self.error("Menu", "crashed")
utime.sleep(1.0)
self.draw_menu() self.draw_menu()
if self.timeout is not None and (
utime.time_ms() - self.select_time
) > int(self.timeout * 1000):
self.on_timeout()
except _ExitMenuException:
pass
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment