Skip to content
GitLab
Menu
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
Menu
Open sidebar
Stefan Haun
firmware
Commits
63086117
Commit
63086117
authored
Aug 28, 2019
by
Rahix
Browse files
Merge 'Use simple_menu module'
See merge request
card10/firmware!145
parents
fe9c4218
7d32047b
Changes
4
Hide whitespace changes
Inline
Side-by-side
Documentation/pycardium/simple_menu.rst
View file @
63086117
...
...
@@ -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
preload/apps/personal_state/__init__.py
View file @
63086117
"""
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__"
:
mai
n
()
StateMenu
(
states
).
ru
n
()
preload/menu.py
View file @
63086117
...
...
@@ -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
butt
ons
import
collecti
ons
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
()
pycardium/modules/py/simple_menu.py
View file @
63086117
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: