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(): def enumerate_apps():
"""Create a list of available apps.""" """List all installed apps."""
apps = [] for f in os.listdir("/"):
if f == "main.py":
yield App("Home", f)
# add main application for app in sorted(os.listdir("/apps")):
for mainFile in os.listdir("/"): if app.startswith("."):
if mainFile == HOMEAPP: continue
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(".")
]
# 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)
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: while True:
v = buttons.read(buttons.BOTTOM_LEFT | buttons.BOTTOM_RIGHT | buttons.TOP_RIGHT) utime.sleep(0.5)
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)
if __name__ == "__main__":
apps = list(enumerate_apps())
def draw_menu(disp, applist, pos, appcount, lineoffset): if apps == []:
disp.clear() no_apps_message()
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 __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:
if ev == buttons.BOTTOM_RIGHT: timeout = self.scroll_speed
self.draw_menu(-8) if self.timeout is not None and self.timeout < self.scroll_speed:
self.idx = (self.idx + 1) % len(self.entries) timeout = self.timeout
self.on_scroll(self.entries[self.idx], self.idx)
elif ev == buttons.BOTTOM_LEFT: for ev in button_events(timeout):
self.draw_menu(8) if ev == buttons.BOTTOM_RIGHT:
self.idx = (self.idx + len(self.entries) - 1) % len(self.entries) self.select_time = utime.time_ms()
self.on_scroll(self.entries[self.idx], self.idx) self.draw_menu(-8)
elif ev == buttons.TOP_RIGHT: self.idx = (self.idx + 1) % len(self.entries)
self.on_select(self.entries[self.idx], self.idx) try:
self.on_scroll(self.entries[self.idx], self.idx)
self.draw_menu() except Exception as e:
print("Exception during menu.on_scroll():")
sys.print_exception(e)
elif ev == buttons.BOTTOM_LEFT:
self.select_time = utime.time_ms()
self.draw_menu(8)
self.idx = (self.idx + len(self.entries) - 1) % len(self.entries)
try:
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:
try:
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()
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.
Finish editing this message first!
Please register or to comment