First pass at making libs work with Mk4

philcrump-phil-add-ntp
Marek Ventur 2018-08-18 22:18:22 +01:00
parent 80e1e03446
commit 42c27b9ce2
19 changed files with 219 additions and 132 deletions

View File

@ -1,7 +1,7 @@
import os, shutil, sys, fnmatch
def sync(storage, patterns, resources, verbose):
root = get_root()
root = get_root(verbose)
# Add all paths that are already files
paths = set([p for p in (patterns or []) if os.path.isfile(os.path.join(root, p))])
@ -30,7 +30,6 @@ def sync(storage, patterns, resources, verbose):
paths.add(path)
if not found:
print("WARN: No resources to copy found for pattern %s" % patterns)
if not verbose:
print("Copying %s files: " % len(paths), end="")
for path in paths:
@ -46,9 +45,7 @@ def sync(storage, patterns, resources, verbose):
target = os.path.join(storage, rel_path)
target_dir = os.path.dirname(target)
if os.path.isfile(target_dir):
# micropython has the tendency to sometimes corrupt directories into files
os.remove(target_dir)
ensure_dir(target_dir, storage)
if not os.path.exists(target_dir):
os.makedirs(target_dir)
shutil.copy2(path, target)
@ -59,6 +56,14 @@ def sync(storage, patterns, resources, verbose):
print(" DONE")
return synced_resources
def ensure_dir(path, storage):
# micropython has a tendecy
if not path or path == storage:
return
if os.path.isfile(path):
os.remove(path)
ensure_dir(os.path.dirname(path), storage)
def set_boot_app(storage, app_to_boot):
path = os.path.join(storage, 'once.txt')
try:
@ -79,8 +84,7 @@ def set_no_boot(storage):
with open(path, 'w') as f:
f.write("\n")
def get_root():
def get_root(verbose=False):
root = os.path.abspath(os.path.join(os.path.dirname( __file__ ), '..'))
if not os.path.isfile(os.path.join(root, "boot.py")):
print("Path %s doesn't contain a boot.py, aborting. Something is probably wrong with your setup.")

View File

@ -138,7 +138,7 @@ def main():
def find_storage():
# todo: find solution for windows and linux
for pattern in ['/Volumes/PYBFLASH', '/Volumes/NO NAME']:
for pattern in ['/Volumes/TILDAMK4', '/Volumes/PYBFLASH', '/Volumes/NO NAME']:
for path in glob.glob(pattern):
return path
print("Couldn't find badge storage - Please make it's plugged in and reset it if necessary")

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
.DS_Store
__pycache__
wifi.json
wifi*.json

View File

@ -1,8 +1,6 @@
import pyb, os, micropython, sys
import os, micropython, sys
micropython.alloc_emergency_exception_buf(100)
sys.path.append('/flash/upip')
# micropython.alloc_emergency_exception_buf(100) # doesn't exist in TiLDA Mk4 yet
os.sync()
root = os.listdir()
@ -34,4 +32,4 @@ else:
start = "main.py"
start = file("once.txt", True) or file("default_app.txt", False) or any_home() or "bootstrap.py"
pyb.main(start)
#todo: something like tilda.main(start)

View File

@ -66,7 +66,7 @@ class App:
def boot(self):
write_launch_file(self.name)
machine.reset() # used to be pyb.hard_reset()
machine.reset()
def __str__(self):
return self.title
@ -116,5 +116,5 @@ def write_launch_file(app, file = "once.txt"):
def restart_to_default():
write_launch_file("")
machine.reset() # used to be pyb.hard_reset()
machine.reset()

View File

@ -2,25 +2,17 @@
___license___ = "MIT"
import pyb
import machine, time
CONFIG = {
"JOY_UP": pyb.Pin.PULL_DOWN,
"JOY_DOWN": pyb.Pin.PULL_DOWN,
"JOY_RIGHT": pyb.Pin.PULL_DOWN,
"JOY_LEFT": pyb.Pin.PULL_DOWN,
"JOY_CENTER": pyb.Pin.PULL_DOWN,
"BTN_MENU": pyb.Pin.PULL_UP,
"BTN_A": pyb.Pin.PULL_UP,
"BTN_B": pyb.Pin.PULL_UP
}
ROTATION_MAP = {
"JOY_UP": "JOY_LEFT",
"JOY_LEFT": "JOY_DOWN",
"JOY_DOWN": "JOY_RIGHT",
"JOY_RIGHT": "JOY_UP",
"JOY_UP": [1, machine.Pin.PULL_DOWN],
"JOY_DOWN": [2, machine.Pin.PULL_DOWN],
"JOY_RIGHT": [4, machine.Pin.PULL_DOWN],
"JOY_LEFT": [3, machine.Pin.PULL_DOWN],
"JOY_CENTER": [0, machine.Pin.PULL_DOWN],
"BTN_MENU": [5, machine.Pin.PULL_UP]
}
# todo: port expander
_tilda_pins = {}
_tilda_interrupts = {}
@ -35,16 +27,12 @@ def init(buttons = CONFIG.keys()):
"""Inits all pins used by the TiLDA badge"""
global _tilda_pins
for button in buttons:
_tilda_pins[button] = pyb.Pin(button, pyb.Pin.IN)
_tilda_pins[button].init(pyb.Pin.IN, CONFIG[button])
def rotate(button):
"""remaps names of buttons to rotated values"""
return ROTATION_MAP[button]
_tilda_pins[button] = machine.Pin(CONFIG[button][0], machine.Pin.IN)
_tilda_pins[button].init(machine.Pin.IN, CONFIG[button][1])
def is_pressed(button):
pin = _get_pin(button)
if pin.pull() == pyb.Pin.PULL_DOWN:
if pin.pull() == machine.Pin.PULL_DOWN:
return pin.value() > 0
else:
return pin.value() == 0
@ -58,19 +46,19 @@ def is_triggered(button, interval = 30):
global _tilda_bounce
if is_pressed(button):
if button in _tilda_bounce:
if pyb.millis() > _tilda_bounce[button]:
if time.ticks_ms() > _tilda_bounce[button]:
del _tilda_bounce[button]
else:
return False # The button might have bounced back to high
# Wait for a while to avoid bounces to low
pyb.delay(interval)
machine.sleep_ms(interval)
# Wait until button is released again
while is_pressed(button):
pyb.wfi()
machine.sleep_ms(1)
_tilda_bounce[button] = pyb.millis() + interval
_tilda_bounce[button] = time.ticks_ms() + interval
return True
def has_interrupt(button):
@ -83,6 +71,7 @@ def has_interrupt(button):
def enable_interrupt(button, interrupt, on_press = True, on_release = False):
raise Exception("interrupts don't work yet")
"""Attaches an interrupt to a button
on_press defines whether it should be called when the button is pressed
@ -104,35 +93,30 @@ def enable_interrupt(button, interrupt, on_press = True, on_release = False):
mode = None;
if on_press and on_release:
mode = pyb.ExtInt.IRQ_RISING_FALLING
mode = machine.ExtInt.IRQ_RISING_FALLING
else:
if pin.pull() == pyb.Pin.PULL_DOWN:
mode = pyb.ExtInt.IRQ_RISING if on_press else pyb.ExtInt.IRQ_FALLING
if pin.pull() == machine.Pin.PULL_DOWN:
mode = machine.ExtInt.IRQ_RISING if on_press else machine.ExtInt.IRQ_FALLING
else:
mode = pyb.ExtInt.IRQ_FALLING if on_press else pyb.ExtInt.IRQ_RISING
mode = machine.ExtInt.IRQ_FALLING if on_press else machine.ExtInt.IRQ_RISING
_tilda_interrupts[button] = {
"interrupt": pyb.ExtInt(pin, mode, pin.pull(), interrupt),
"interrupt": machine.ExtInt(pin, mode, pin.pull(), interrupt),
"mode": mode,
"pin": pin
}
def disable_interrupt(button):
raise Exception("interrupts don't work yet")
global _tilda_interrupts
if button in _tilda_interrupts:
interrupt = _tilda_interrupts[button]
pyb.ExtInt(interrupt["pin"], interrupt["mode"], interrupt["pin"].pull(), None)
machine.ExtInt(interrupt["pin"], interrupt["mode"], interrupt["pin"].pull(), None)
del _tilda_interrupts[button]
init([button])
def disable_all_interrupt():
raise Exception("interrupts don't work yet")
for interrupt in _tilda_interrupts:
disable_interrupt(interrupt)
def enable_menu_reset():
import onboard
enable_interrupt("BTN_MENU", lambda t:onboard.semihard_reset(), on_release = True)
def disable_menu_reset():
disable_interrupt("BTN_MENU")

View File

@ -1,11 +1,9 @@
"""Some basic UGFX powered dialogs"""
___license___ = "MIT"
___dependencies___ = ["buttons"]
___dependencies___ = ["buttons", "sleep"]
import ugfx
import buttons
import pyb
import ugfx, buttons, sleep
default_style_badge = ugfx.Style()
default_style_badge.set_focus(ugfx.RED)
@ -18,11 +16,13 @@ default_style_dialog.set_background(ugfx.html_color(0xFFFFFF))
TILDA_COLOR = ugfx.html_color(0x7c1143);
FONT_SMALL = 0 #todo: find correct values
FONT_MEDIUM_BOLD = 0
def notice(text, title="TiLDA", close_text="Close", width = 260, height = 180, font=ugfx.FONT_SMALL, style=None):
def notice(text, title="TiLDA", close_text="Close", width = 260, height = 180, font=FONT_SMALL, style=None):
prompt_boolean(text, title = title, true_text = close_text, false_text = None, width = width, height = height, font=font, style=style)
def prompt_boolean(text, title="TiLDA", true_text="Yes", false_text="No", width = 260, height = 180, font=ugfx.FONT_SMALL, style=None):
def prompt_boolean(text, title="TiLDA", true_text="Yes", false_text="No", width = 260, height = 180, font=FONT_SMALL, style=None):
"""A simple one and two-options dialog
if 'false_text' is set to None only one button is displayed.
@ -31,7 +31,7 @@ def prompt_boolean(text, title="TiLDA", true_text="Yes", false_text="No", width
global default_style_dialog
if style == None:
style = default_style_dialog
ugfx.set_default_font(ugfx.FONT_MEDIUM_BOLD)
ugfx.set_default_font(FONT_MEDIUM_BOLD)
window = ugfx.Container((ugfx.width() - width) // 2, (ugfx.height() - height) // 2, width, height, style=style)
window.show()
ugfx.set_default_font(font)
@ -44,7 +44,7 @@ def prompt_boolean(text, title="TiLDA", true_text="Yes", false_text="No", width
ugfx.set_default_font(font)
label = ugfx.Label(5, 30, width - 10, height - 80, text = text, parent=window)
ugfx.set_default_font(ugfx.FONT_MEDIUM_BOLD)
ugfx.set_default_font(FONT_MEDIUM_BOLD)
button_yes = ugfx.Button(5, height - 40, width // 2 - 15 if false_text else width - 15, 30 , true_text, parent=window)
button_no = ugfx.Button(width // 2 + 5, height - 40, width // 2 - 15, 30 , false_text, parent=window) if false_text else None
@ -57,7 +57,7 @@ def prompt_boolean(text, title="TiLDA", true_text="Yes", false_text="No", width
window.show()
while True:
pyb.wfi()
sleep.wfi()
if buttons.is_triggered("BTN_A"): return True
if buttons.is_triggered("BTN_B"): return False
@ -68,7 +68,7 @@ def prompt_boolean(text, title="TiLDA", true_text="Yes", false_text="No", width
if button_no: button_no.destroy()
label.destroy()
def prompt_text(description, init_text = "", true_text="OK", false_text="Back", width = 300, height = 200, font=ugfx.FONT_MEDIUM_BOLD, style=default_style_badge):
def prompt_text(description, init_text = "", true_text="OK", false_text="Back", width = 300, height = 200, font=FONT_MEDIUM_BOLD, style=default_style_badge):
"""Shows a dialog and keyboard that allows the user to input/change a string
Returns None if user aborts with button B
@ -86,7 +86,7 @@ def prompt_text(description, init_text = "", true_text="OK", false_text="Back",
ugfx.set_default_font(ugfx.FONT_MEDIUM)
kb = ugfx.Keyboard(0, int(height/2), width, int(height/2), parent=window)
edit = ugfx.Textbox(5, int(height/2)-30, int(width*4/5)-10, 25, text = init_text, parent=window)
ugfx.set_default_font(ugfx.FONT_SMALL)
ugfx.set_default_font(FONT_SMALL)
button_yes = ugfx.Button(int(width*4/5), int(height/2)-30, int(width*1/5)-3, 25 , true_text, parent=window)
button_no = ugfx.Button(int(width*4/5), int(height/2)-30-30, int(width/5)-3, 25 , false_text, parent=window) if false_text else None
ugfx.set_default_font(font)
@ -101,7 +101,7 @@ def prompt_text(description, init_text = "", true_text="OK", false_text="Back",
window.show()
edit.set_focus()
while True:
pyb.wfi()
sleep.wfi()
ugfx.poll()
#if buttons.is_triggered("BTN_A"): return edit.text()
if buttons.is_triggered("BTN_B"): return None
@ -123,7 +123,7 @@ def prompt_option(options, index=0, text = "Please select one of the following:"
If none_text is specified the user can use the B or Menu button to skip the selection
if title is specified a blue title will be displayed about the text
"""
ugfx.set_default_font(ugfx.FONT_SMALL)
ugfx.set_default_font(FONT_SMALL)
window = ugfx.Container(5, 5, ugfx.width() - 10, ugfx.height() - 10)
window.show()
@ -156,7 +156,7 @@ def prompt_option(options, index=0, text = "Please select one of the following:"
buttons.init()
while True:
pyb.wfi()
sleep.wfi()
ugfx.poll()
if buttons.is_triggered("BTN_A"): return options[options_list.selected_index()]
if button_none and buttons.is_triggered("BTN_B"): return None
@ -180,15 +180,16 @@ class WaitingMessage:
self.label = ugfx.Label(5, 40, self.window.width() - 10, ugfx.height() - 40, text = text, parent=self.window)
# Indicator to show something is going on
self.indicator = ugfx.Label(ugfx.width() - 100, 0, 20, 20, text = "...", parent=self.window)
self.timer = pyb.Timer(3)
self.timer.init(freq=3)
self.timer.callback(lambda t: self.indicator.visible(not self.indicator.visible()))
#self.indicator = ugfx.Label(ugfx.width() - 100, 0, 20, 20, text = "...", parent=self.window)
#self.timer = machine.Timer(3)
#self.timer.init(freq=3)
#self.timer.callback(lambda t: self.indicator.visible(not self.indicator.visible()))
# todo: enable this once we have a timer somewhere
def destroy(self):
self.timer.deinit()
#self.timer.deinit()
self.label.destroy()
self.indicator.destroy()
#self.indicator.destroy()
self.window.destroy()
@property

16
lib/hall_effect.py Normal file
View File

@ -0,0 +1,16 @@
"""Library for sleep related functions"""
import machine
_adc = None
def hall_effect_adc():
global _adc
if not _adc:
_adc = machine.ADC(machine.ADC.ADC_HALLEFFECT)
return _adc
def get_flux():
return hall_effect_adc().convert() # todo: convert this into something meaningful

View File

@ -6,7 +6,7 @@ In particular, they *should*:
* Call "homescreen.init()" at the beginning. This will initiate ugfx, clear the screen and
initiate button handline.
* Use "pyb.wfi()" as much as possible to avoid draining the battery.
* Use "sleep.wfi()" as much as possible to avoid draining the battery.
* Not use
They also *may*:
@ -17,9 +17,9 @@ They also *may*:
"""
___license___ = "MIT"
___dependencies___ = ["database", "buttons", "random", "app"]
___dependencies___ = ["database", "buttons", "random", "app", "sleep"]
import database, ugfx, random, buttons, time, select
import database, ugfx, random, select, buttons
from app import App
_state = None
@ -27,13 +27,10 @@ def init(enable_menu_button = True):
global _state
_state = {"menu": False}
ugfx.init()
ugfx.orientation(90)
if enable_menu_button:
buttons.init()
buttons.enable_interrupt("BTN_MENU", lambda t: set_state("menu"), on_release = True)
#buttons.enable_interrupt("BTN_MENU", lambda t: set_state("menu"), on_release = True)
def set_state(key, value = True):
# we can't allocate memory in interrupts, so make sure all keys are set beforehand and
@ -42,18 +39,13 @@ def set_state(key, value = True):
_state[key] = value
def clean_up():
buttons.disable_all_interrupt()
pass
def check():
global _state
if _state["menu"]:
def sleep(interval = 500):
if button.is_triggered("BTN_MENU", interval=interval):
clean_up()
App("launcher").boot()
def sleep(interval = 500):
check()
time.sleep_ms(interval) # todo: deep sleep
check()
def name(default = None):
return database.get("homescreen.name", default)

View File

@ -8,7 +8,7 @@ Current known issues:
"""
___license___ = "MIT"
___dependencies___ = ["urlencode"]
___dependencies___ = ["urlencode", "wifi"]
import usocket, ujson, os, time, gc, wifi
from urlencode import urlencode
@ -144,6 +144,7 @@ def open_http_socket(method, url, json=None, timeout=None, headers=None, data=No
if proto == 'http:':
port = 80
elif proto == 'https:':
#todo make this work
raise OSError("HTTPS is currently not supported")
port = 443
else:

View File

@ -10,10 +10,10 @@ import os
sep = "/"
R_OK = const(4)
W_OK = const(2)
X_OK = const(1)
F_OK = const(0)
R_OK = 4
W_OK = 2
X_OK = 1
F_OK = 0
def join(*args):
# TODO: this is non-compliant

View File

@ -5,6 +5,7 @@ Warning! Don't use this for anything important, it's probably biased
___license___ = "MIT"
# todo: simplify this by using "urandom"
import os
_bigrand_bytes = 10

11
lib/sleep.py Normal file
View File

@ -0,0 +1,11 @@
"""Library for sleep related functions"""
import time
def sleep_ms(duration):
# todo: deepsleep?
time.sleep_ms(duration)
def wfi():
# todo: this is fake
sleep_ms(1)

34
lib/test_dialogs.py Normal file
View File

@ -0,0 +1,34 @@
"""Tests for app lib
Very limited at the moment since we can't test the main input dialogs"""
___license___ = "MIT"
___dependencies___ = ["upip:unittest", "dialogs", "sleep"]
import unittest, ugfx
from machine import Pin
from dialogs import *
from sleep import *
class TestDialogs(unittest.TestCase):
def setUpClass(self):
ugfx.init()
Pin(Pin.PWM_LCD_BLIGHT).on()
def tearDownClass(self):
Pin(Pin.PWM_LCD_BLIGHT).off()
def test_app_object(self):
count_max = 10
with WaitingMessage("Testing...", "Foo") as c:
for i in range(1, count_max):
sleep_ms(100)
c.text = "%d/%d" % (i, count_max)
print("done")
if __name__ == '__main__':
unittest.main()

17
lib/test_hall_effect.py Normal file
View File

@ -0,0 +1,17 @@
"""Tests for hall effect sensor"""
___license___ = "MIT"
___dependencies___ = ["upip:unittest", "hall_effect"]
import unittest, hall_effect
class TestHallEffect(unittest.TestCase):
def test_hall(self):
flux = hall_effect.get_flux()
self.assertTrue(flux > 0)
self.assertTrue(flux < 4000)
if __name__ == '__main__':
unittest.main()

View File

@ -9,29 +9,29 @@ from icons import *
class TestIcons(unittest.TestCase):
# incomplete!
# todo: fix me
def setUp(self):
ugfx.init()
ugfx.orientation(90)
ugfx.clear()
def test_icon(self):
icon = Icon(44, 40, "Badge Store with", "badge_store/icon.gif")
icon.show()
for s in [True, False, True]:
icon.selected = s
time.sleep(0.1)
icon.__del__()
def test_icon_grid(self):
items = []
for i in range(50):
items.append({
"title": "App %s" % i
})
icon_grid = IconGrid(5, 5, items, None)
# def test_icon(self):
# icon = Icon(44, 40, "Badge Store with", "badge_store/icon.gif")
# icon.show()
#
# for s in [True, False, True]:
# icon.selected = s
# time.sleep(0.1)
#
# icon.__del__()
#
# def test_icon_grid(self):
# items = []
# for i in range(50):
# items.append({
# "title": "App %s" % i
# })
# icon_grid = IconGrid(5, 5, items, None)

14
lib/test_sleep.py Normal file
View File

@ -0,0 +1,14 @@
"""Tests for http"""
___license___ = "MIT"
___dependencies___ = ["upip:unittest", "sleep"]
import unittest, sleep
class TestSleep(unittest.TestCase):
def test_sleep(self):
sleep.sleep_ms(100)
if __name__ == '__main__':
unittest.main()

15
lib/test_wifi.py Normal file
View File

@ -0,0 +1,15 @@
"""Tests for wifi"""
___license___ = "MIT"
___dependencies___ = ["upip:unittest", "wifi"]
import unittest, wifi
class TestWifi(unittest.TestCase):
def test_connect(self):
wifi.connect()
self.assertTrue(wifi.is_connected())
if __name__ == '__main__':
unittest.main()

View File

@ -1,20 +1,17 @@
"""Handles connecting to a wifi access point based on a valid wifi.json file"""
___license___ = "MIT"
___dependencies___ = ["dialogs"]
___dependencies___ = ["dialogs", "sleep"]
import network
import os
import json
import pyb
import dialogs
import network, os, json, dialogs, sleep, time
_nic = None
def nic():
global _nic
if not _nic:
_nic = network.CC3100()
_nic = network.WLAN()
_nic.active(True)
return _nic
def connection_details():
@ -37,7 +34,7 @@ def connect(wait=True, timeout=10, show_wait_message=False, prompt_on_fail=True,
retry_connect = True
while retry_connect:
if nic().is_connected():
if nic().isconnected():
return
details = connection_details()
@ -68,25 +65,27 @@ def connect(wait=True, timeout=10, show_wait_message=False, prompt_on_fail=True,
if not retry_connect:
os.remove('wifi.json')
os.sync()
# We would rather let you choose a new network here, but
# scanning doesn't work after a connect at the moment
pyb.hard_reset()
else:
raise
def connect_wifi(details, timeout, wait=False):
if 'pw' in details:
nic().connect(details['ssid'], details['pw'], timeout=timeout)
nic().connect(details['ssid'], details['pw'])
else:
nic().connect(details['ssid'], timeout=timeout)
nic().connect(details['ssid'])
if wait:
while not nic().is_connected():
nic().update()
pyb.delay(100)
wait_until = time.ticks_ms() + 2000
while not nic().isconnected():
#nic().update() # todo: do we need this?
if (time.ticks_ms() > wait_until):
raise Exception("Timeout while trying to connect to wifi")
sleep.sleep_ms(100)
def is_connected():
return nic().is_connected()
return nic().isconnected()
def get_security_level(ap):
n = nic()
@ -149,5 +148,4 @@ def choose_wifi(dialog_title='TiLDA'):
file.write(json.dumps(conn_details))
os.sync()
# We can't connect after scanning for some bizarre reason, so we reset instead
pyb.hard_reset()
# todo: last time we had to hard reset here, is that still the case?