Skip to content

Commit

Permalink
Merge pull request #85 from ZeroPhone/staging
Browse files Browse the repository at this point in the history
Contexts, Canvas, new apps, Input device capabilities, UniversalInput, prettier UI, tests, safer shell scripts
  • Loading branch information
CRImier committed Jun 7, 2018
2 parents 65da23a + 1d452a5 commit 76de273
Show file tree
Hide file tree
Showing 131 changed files with 5,514 additions and 983 deletions.
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ var/
pip-log.txt
pip-delete-this-directory.txt

#PyTest
.pytest_cache

# Unit test / coverage reports
htmlcov/
.tox/
Expand Down Expand Up @@ -64,3 +67,6 @@ config.json
# We ship do_not_load files in some directories, but do not track user-created files
# (only user-removed ones)
do_not_load
log_conf.ini
# Ignoring the default screenshot folder
screenshots/
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ language: python
python:
- "2.7"
# command to install dependencies
script: python -B -m pytest --doctest-modules -v --doctest-ignore-import-errors --ignore=output/drivers --ignore=input/drivers --ignore=apps/hardware_apps/ --ignore=apps/example_apps/fire_detector --ignore=apps/test_hardware
script: python -B -m pytest --doctest-modules -v --doctest-ignore-import-errors --ignore=output/drivers --ignore=input/drivers --ignore=apps/hardware_apps/ --ignore=apps/example_apps/fire_detector --ignore=apps/test_hardware --ignore=docs/ --ignore=apps/hardware_apps --ignore=utils/
branches:
except:
- test_branch_doug
Expand Down
72 changes: 51 additions & 21 deletions apps/app_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ class AppManager(object):
"""
ordering_cache = {}

def __init__(self, app_directory, i, o, config=None):
def __init__(self, app_directory, context_manager, config=None):
self.app_directory = app_directory
self.i = i
self.o = o
self.cm = context_manager
self.i, self.o = self.cm.get_io_for_context("main")
self.config = config if config else {}

def load_all_apps(self):
Expand Down Expand Up @@ -86,39 +86,69 @@ def load_all_apps(self):

def bind_callback(self, app, app_path, menu_name, ordering, subdir_path):
if hasattr(app, "callback") and callable(app.callback): # for function based apps
subdir_menu = self.subdir_menus[subdir_path]
subdir_menu_contents = self.insert_by_ordering([menu_name, app.callback], os.path.split(app_path)[1],
subdir_menu.contents, ordering)
subdir_menu.set_contents(subdir_menu_contents)
return True
if hasattr(app, "on_start") and callable(app.on_start): # for class based apps
subdir_menu = self.subdir_menus[subdir_path]
subdir_menu_contents = self.insert_by_ordering([menu_name, app.on_start], os.path.split(app_path)[1],
subdir_menu.contents, ordering)
subdir_menu.set_contents(subdir_menu_contents)
app_callback = app.callback
elif hasattr(app, "on_start") and callable(app.on_start): # for class based apps
app_callback = app.on_start
else:
logger.debug("App \"{}\" has no callback; loading silently".format(menu_name))
return
self.cm.register_context_target(app_path, app_callback)
menu_callback = lambda: self.cm.switch_to_context(app_path)
#App callback is available and wrapped, inserting
subdir_menu = self.subdir_menus[subdir_path]
subdir_menu_contents = self.insert_by_ordering([menu_name, menu_callback], os.path.split(app_path)[1],
subdir_menu.contents, ordering)
subdir_menu.set_contents(subdir_menu_contents)

def load_app(self, app_path):
def get_app_path_for_cmdline(self, cmdline_app_path):
main_py_string = "/main.py"
if app_path.endswith(main_py_string):
app_path = app_path[:-len(main_py_string)]
if cmdline_app_path.endswith(main_py_string):
app_path = cmdline_app_path[:-len(main_py_string)]
elif cmdline_app_path.endswith("/"):
app_path = cmdline_app_path[:-1]
else:
app_path = cmdline_app_path
return app_path

def load_app(self, app_path, threaded = True):
if "__init__.py" not in os.listdir(app_path):
raise ImportError("Trying to import an app with no __init__.py in its folder!")
app_import_path = app_path.replace('/', '.')
# If user runs in single-app mode and by accident
# autocompletes the app name too far, it shouldn't fail
app = importlib.import_module(app_import_path + '.main', package='apps')
context = self.cm.create_context(app_path)
context.threaded = threaded
i, o = self.cm.get_io_for_context(app_path)
if is_class_based_module(app):
zero_app_subclass = get_zeroapp_class_in_module(app)
app = zero_app_subclass(self.i, self.o)
app_class = get_zeroapp_class_in_module(app)
app = app_class(i, o)
else:
app.init_app(self.i, self.o)
app.init_app(i, o)
self.pass_context_to_app(app, app_path, context)
return app

def pass_context_to_app(self, app, app_path, context):
"""
This is a function to pass context objects to apps. For now, it works
with both class-based and module-based apps. It only passes the context
if it detects that the app has the appropriate function to do that.
"""
if hasattr(app, "set_context") and callable(app.set_context):
try:
app.set_context(context)
except Exception as e:
logger.exception("App {}: app class has 'set_context' but raised exception when passed a context".format(app_path))
else:
logger.info("Passed context to app {}".format(app_path))

def get_subdir_menu_name(self, subdir_path):
"""This function gets a subdirectory path and imports __init__.py from it. It then gets _menu_name attribute from __init__.py and returns it.
If failed to either import __init__.py or get the _menu_name attribute, it returns the subdirectory name."""
"""
This function gets a subdirectory path and imports __init__.py from it.
It then gets _menu_name attribute from __init__.py and returns it.
If failed to either import __init__.py or get the _menu_name attribute,
it returns the subdirectory name.
"""
subdir_import_path = subdir_path.replace('/', '.')
try:
subdir_object = importlib.import_module(subdir_import_path + '.__init__')
Expand Down
125 changes: 91 additions & 34 deletions apps/clock/main.py
Original file line number Diff line number Diff line change
@@ -1,70 +1,127 @@
from __future__ import division

import math
from datetime import datetime

from luma.core.render import canvas
from datetime import datetime, timedelta

from apps import ZeroApp
from ui import Refresher
from ui.loading_indicators import CenteredTextRenderer
from ui import Menu, Refresher, Canvas, IntegerAdjustInput

from helpers import read_or_create_config, local_path_gen

local_path = local_path_gen(__name__)

class ClockApp(ZeroApp, Refresher, CenteredTextRenderer):
class ClockApp(ZeroApp, Refresher):

def __init__(self, i, o, *args, **kwargs):
super(ClockApp, self).__init__(i, o)
self.menu_name = "Clock"
self.refresher = Refresher(self.on_refresh, i, o)
self.countdown = None
self.refresher = Refresher(self.on_refresh, i, o, keymap={"KEY_RIGHT":self.countdown_settings})
default_config = '{}'
config_filename = "config.json"
self.config = read_or_create_config(local_path(config_filename), default_config, self.menu_name+" app")

def draw_analog_clock(self, draw, time, radius="min(*self.o.device.size) / 3", clock_x = "center_x+32", clock_y = "center_y+5", h_len = "radius / 2", m_len = "radius - 5", s_len = "radius - 3", **kwargs):
"""Draws the analog clock, with parameters configurable through config.txt."""
center_x, center_y = self.get_center(self.o.device.size)
def format_countdown(self):
if not self.countdown: return None
h, m, s, sign = self.get_countdown_time_left()
if sign: return None
return "{}m".format(h*60+m)

def get_countdown_time_left(self):
delta = self.countdown["time"]-datetime.now()
print(delta)
seconds = delta.seconds
sign = None
if delta.days < 0:
seconds = -seconds
sign = "+"
hours, remainder = divmod(seconds, 3600)
minutes, seconds = divmod(remainder, 60)
if sign == "+":
hours = hours+24
return hours, minutes, seconds, sign

def countdown_settings(self):
# Setting an absolute countdown is not yet possible
# because we don't yet have a TimePicker UI element
def gmc(): #get menu contents
countdown_label = self.format_countdown()
contents = []
if countdown_label: contents.append(["Countdown: {}".format(countdown_label)])
#contents.append(["Set absolute", lambda: self.set_countdown(absolute=True)])
contents.append(["Set relative", self.set_countdown])
return contents
Menu([], self.i, self.o, "Countdown settings menu", contents_hook=gmc).activate()

def set_countdown(self, absolute=False):
if absolute: raise NotImplementedError # Needs a TimePicker or something like that
rel_start = 0
message = "After (in minutes):"
if self.countdown:
# A countdown is already active
# Using it as a starting point
h, m, s, _ = self.get_countdown_time_left()
rel_start = h*60+m
offset = IntegerAdjustInput(rel_start, self.i, self.o, message=message).activate()
if offset is not None:
countdown = {"time": datetime.now()+timedelta(minutes=offset)}
self.countdown = countdown

def draw_analog_clock(self, c, time, radius="min(*c.size) / 3", clock_x = "center_x+32", clock_y = "center_y+5", h_len = "radius / 2", m_len = "radius - 5", s_len = "radius - 3", **kwargs):
"""Draws the analog clock, with parameters configurable through config.json."""
center_x, center_y = c.get_center()
clock_x = eval(clock_x)
clock_y = eval(clock_y)
radius = eval(radius)
draw.ellipse((clock_x - radius, clock_y - radius, clock_x + radius, clock_y + radius), fill=False, outline="white")
self.draw_needle(draw, 60 - time.second / 60, eval(s_len), clock_x, clock_y, 1)
self.draw_needle(draw, 60 - time.minute / 60, eval(m_len), clock_x, clock_y, 1)
self.draw_needle(draw, 24 - time.hour / 24, eval(h_len), clock_x, clock_y, 1)
c.ellipse((clock_x - radius, clock_y - radius, clock_x + radius, clock_y + radius), fill=False, outline="white")
self.draw_needle(c, 60 - time.second / 60, eval(s_len), clock_x, clock_y, 1)
self.draw_needle(c, 60 - time.minute / 60, eval(m_len), clock_x, clock_y, 1)
self.draw_needle(c, 24 - time.hour / 24, eval(h_len), clock_x, clock_y, 1)

def draw_countdown(self, c, countdown_x="(center_x/2)-10", countdown_y="center_y/2*3", **kwargs):
"""Draws the digital clock, with parameters configurable through config.json."""
h, m, s, sign = self.get_countdown_time_left()
hz, mz, sz = map(lambda x:str(x).zfill(2), (h, m, s))
string = "{}:{}".format(mz, sz)
if h: string = hz+":"+string
if sign: string = sign+string
center_x, center_y = c.get_center()
centered_coords = c.get_centered_text_bounds(string)
x = eval(countdown_x)
y = eval(countdown_y)
c.text((x, y), string, fill="white")

def draw_text(self, draw, time, text_x="10", text_y="center_y-5", time_format = "%H:%M:%S", **kwargs):
"""Draws the digital clock, with parameters configurable through config.txt."""
def draw_text(self, c, time, text_x="10", text_y="center_y-5", time_format = "%H:%M:%S", **kwargs):
"""Draws the digital clock, with parameters configurable through config.json."""
time_str = time.strftime(time_format)
center_x, center_y = self.get_center(self.o.device.size)
bounds = self.get_centered_text_bounds(draw, time_str, self.o.device.size)
center_x, center_y = c.get_center()
centered_coords = c.get_centered_text_bounds(time_str)
x = eval(text_x)
y = eval(text_y)
draw.text((x, y), time_str, fill="white")
c.text(time_str, (x, y))

def on_refresh(self):
current_time = datetime.now()
return self.render_clock(current_time, **self.config)

def render_clock(self, time, **kwargs):
c = canvas(self.o.device)
c.__enter__()
width, height = c.device.size
draw = c.draw
self.draw_text(draw, time, **kwargs)
self.draw_analog_clock(draw, time, **kwargs)
return c.image

def draw_needle(self, draw, progress, radius, x, y, width):
# type: (ImageDraw, float, float, float, float, int) -> None
c = Canvas(self.o)
width, height = c.size
self.draw_text(c, time, **kwargs)
self.draw_analog_clock(c, time, **kwargs)
if self.countdown:
self.draw_countdown(c, **kwargs)
return c.get_image()

def draw_needle(self, c, progress, radius, x, y, width):
# type: (Canvas, float, float, float, float, int) -> None
hour_angle = math.pi * 2 * progress + math.pi
draw.line(
c.line(
(
x,
y,
x + radius * math.sin(hour_angle),
y + radius * math.cos(hour_angle)
int(x),
int(y),
int(x + radius * math.sin(hour_angle)),
int(y + radius * math.cos(hour_angle))
),
width=width,
fill=True
Expand Down
4 changes: 2 additions & 2 deletions apps/example_apps/loadingbar_test_app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,11 @@ def on_start(self):
with self.bar_choice_listbox.activate() as chosen_loading_bar:
if hasattr(chosen_loading_bar, "progress"):
for i in range(101):
chosen_loading_bar.progress = float(i) / 100
chosen_loading_bar.progress = i
sleep(0.01)
sleep(1)
for i in range(101)[::-1]:
chosen_loading_bar.progress = float(i) / 100
chosen_loading_bar.progress = i
sleep(0.1)
else:
sleep(3)
File renamed without changes.
18 changes: 18 additions & 0 deletions apps/example_apps/menu_arrow_test/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from __future__ import print_function

menu_name = "Menu arrow testing"

from ui import Menu

#Some globals for us
i = None #Input device
o = None #Output device

def callback():
contents = [["Arrow test", lambda: print("Enter"), lambda: print("Right")]]
Menu(contents, i, o, "Menu arrow test menu").activate()

def init_app(input, output):
global i, o
i = input; o = output

2 changes: 1 addition & 1 deletion apps/example_apps/refresher/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,6 @@ def init_app(input, output):
global callback, i, o
i = input; o = output #Getting references to output and input device objects and saving them as globals
time_refresher = Refresher(show_time, i, o, 1, name="Timer")
counter_refresher = Refresher(count, i, o, 1, keymap={"KEY_KPENTER":time_refresher.activate}, name="Counter")
counter_refresher = Refresher(count, i, o, 1, keymap={"KEY_ENTER":time_refresher.activate}, name="Counter")
callback = counter_refresher.activate

17 changes: 9 additions & 8 deletions apps/example_apps/sandbox/main.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
menu_name = "Python sandbox"

from ui import Printer

#Some globals for us
i = None
o = None

def callback():
import code
code.interact(local=dict(globals(), **locals()))
context = None

def init_app(input, output):
global callback, i, o
global i, o
i = input; o = output

def set_context(new_context):
global context
context = new_context

def callback():
import code as __code__
__code__.interact(local=dict(globals(), **locals()))
2 changes: 1 addition & 1 deletion apps/games/g_2048/logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from random import randint, choice
from helpers import flatten

class GameOf2048():
class GameOf2048(object):

matrix = None

Expand Down

0 comments on commit 76de273

Please sign in to comment.