initial commit
commit
6e4e87e6c4
|
@ -0,0 +1 @@
|
|||
.DS_Store
|
|
@ -0,0 +1,67 @@
|
|||
"""switches between app libraries, updates and installs apps.
|
||||
|
||||
To publish apps use https://badge.emfcamp.org"""
|
||||
|
||||
___license___ = "MIT"
|
||||
___name___ = "App Library"
|
||||
___dependencies___ = ["wifi", "dialogs"]
|
||||
___bootstrapped___ = True
|
||||
|
||||
import pyb
|
||||
import ugfx
|
||||
import os
|
||||
#import http_client
|
||||
import wifi
|
||||
import dialogs
|
||||
#from app import App, get_local_apps, get_public_apps, get_public_app_categories, empty_local_app_cache
|
||||
#import filesystem
|
||||
|
||||
TEMP_FILE = ".temp_download"
|
||||
|
||||
ugfx.init()
|
||||
|
||||
### VIEWS ###
|
||||
|
||||
def clear():
|
||||
ugfx.clear(ugfx.html_color(0x7c1143))
|
||||
|
||||
def store():
|
||||
None
|
||||
|
||||
def update():
|
||||
None
|
||||
|
||||
def remove():
|
||||
None
|
||||
|
||||
def settings():
|
||||
None
|
||||
|
||||
def main_menu():
|
||||
while True:
|
||||
clear()
|
||||
|
||||
print()
|
||||
|
||||
menu_items = [
|
||||
{"title": "Install Apps", "function": store},
|
||||
{"title": "Update", "function": update},
|
||||
{"title": "Manage Apps", "function": remove},
|
||||
{"title": "Settings", "function": settings}
|
||||
]
|
||||
|
||||
option = dialogs.prompt_option(menu_items, none_text="Exit", text="What do you want to do?", title="TiLDA App Library")
|
||||
|
||||
if option:
|
||||
option["function"]()
|
||||
else:
|
||||
return
|
||||
|
||||
main_menu()
|
||||
|
||||
#if App("home").loadable:
|
||||
# main_menu()
|
||||
#else:
|
||||
# for app_name in ["changename", "snake", "alistair~selectwifi", "sponsors", "home"]:
|
||||
# install(App(app_name))
|
||||
# pyb.hard_reset()
|
|
@ -0,0 +1,29 @@
|
|||
import pyb, os, micropython
|
||||
|
||||
micropython.alloc_emergency_exception_buf(100)
|
||||
|
||||
root = os.listdir()
|
||||
|
||||
def app(a):
|
||||
if (a in root) and ("main.py" in os.listdir(a)):
|
||||
return a + "/main.py"
|
||||
|
||||
def file(file, remove):
|
||||
try:
|
||||
with open(file, 'r') as f:
|
||||
a = f.read().strip()
|
||||
if remove:
|
||||
os.remove(file)
|
||||
return app(a)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
def any_home():
|
||||
return app(next(a for a in root if a.startswith("home")))
|
||||
|
||||
start = None
|
||||
if "main.py" in root:
|
||||
start = "main.py"
|
||||
start = file("once.txt", True) or file("default_app.txt", False) or any_home() or "bootstrap.py"
|
||||
|
||||
pyb.main(start)
|
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
|
@ -0,0 +1,20 @@
|
|||
"""Default homescreen
|
||||
|
||||
This is the default homescreen for the Tilda Mk4.
|
||||
It gets automatically installed when a badge is
|
||||
newly activated or reset.
|
||||
"""
|
||||
|
||||
___name___ = "Homescreen (Default)"
|
||||
___license___ = "GPL"
|
||||
___categories___ = ["homescreen"]
|
||||
___launchable___ = False
|
||||
___bootstrapped___ = True
|
||||
|
||||
print("there")
|
||||
import ugfx, homescreen
|
||||
|
||||
homescreen.init(color = 0xe4ffdb)
|
||||
|
||||
ugfx.display_image(0, 0, "home_default/bg.gif")
|
||||
ugfx.text(20, 20, homescreen.name(), ugfx.BLACK)
|
|
@ -0,0 +1,9 @@
|
|||
"""Launcher for apps currently installed"""
|
||||
|
||||
___name___ = "Launcher"
|
||||
___license___ = "GPL"
|
||||
___categories___ = ["System"]
|
||||
___launchable___ = False
|
||||
___bootstrapped___ = True
|
||||
|
||||
print("launcher")
|
|
@ -0,0 +1,179 @@
|
|||
"""Model and Helpers for TiLDA apps and the App Library API"""
|
||||
|
||||
___license___ = "MIT"
|
||||
___dependencies___ = ["http"]
|
||||
|
||||
import os
|
||||
import ure
|
||||
import http_client
|
||||
import filesystem
|
||||
import gc
|
||||
|
||||
ATTRIBUTE_MATCHER = ure.compile("^\s*###\s*([^:]*?)\s*:\s*(.*)\s*$") # Yeah, regex!
|
||||
CATEGORY_ALL = "all"
|
||||
CATEGORY_NOT_SET = "uncategorised"
|
||||
|
||||
class App:
|
||||
"""Models an app and provides some helper functions"""
|
||||
def __init__(self, folder_name, api_information = None):
|
||||
self.folder_name = self.name = folder_name.lower()
|
||||
self.user = EMF_USER
|
||||
if USER_NAME_SEPARATOR in folder_name:
|
||||
[self.user, self.name] = folder_name.split(USER_NAME_SEPARATOR, 1)
|
||||
self.user = self.user.lower()
|
||||
self.name = self.name.lower()
|
||||
|
||||
self._attributes = None # Load lazily
|
||||
self.api_information = api_information
|
||||
|
||||
@property
|
||||
def folder_path(self):
|
||||
return "apps/" + self.folder_name
|
||||
|
||||
@property
|
||||
def main_path(self):
|
||||
return self.folder_path + "/main.py"
|
||||
|
||||
@property
|
||||
def loadable(self):
|
||||
return filesystem.is_file(self.main_path) and os.stat(self.main_path)[6] > 0
|
||||
|
||||
@property
|
||||
def description(self):
|
||||
"""either returns a local attribute or uses api_information"""
|
||||
if self.api_information and "description" in self.api_information:
|
||||
return self.api_information["description"]
|
||||
return self.get_attribute("description") or ""
|
||||
|
||||
@property
|
||||
def files(self):
|
||||
"""returns a list of file dicts or returns False if the information is not available"""
|
||||
if self.api_information and "files" in self.api_information:
|
||||
return self.api_information["files"]
|
||||
return False
|
||||
|
||||
@property
|
||||
def category(self):
|
||||
return self.get_attribute("Category", CATEGORY_NOT_SET).lower()
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
return self.get_attribute("appname") or self.name
|
||||
|
||||
@property
|
||||
def user_and_title(self):
|
||||
if self.user == EMF_USER:
|
||||
return self.name
|
||||
else:
|
||||
return "%s by %s" % (self.title, self.user)
|
||||
|
||||
def matches_category(self, category):
|
||||
"""returns True if provided category matches the category of this app"""
|
||||
category = category.lower()
|
||||
return category == CATEGORY_ALL or category == self.category
|
||||
|
||||
@property
|
||||
def attributes(self):
|
||||
"""Returns all attribues of this app
|
||||
|
||||
The result is cached for the lifetime of this object
|
||||
"""
|
||||
if self._attributes == None:
|
||||
self._attributes = {}
|
||||
if self.loadable:
|
||||
with open(self.main_path) as file:
|
||||
for line in file:
|
||||
match = ATTRIBUTE_MATCHER.match(line)
|
||||
if match:
|
||||
self._attributes[match.group(1).strip().lower()] = match.group(2).strip()
|
||||
else:
|
||||
break
|
||||
return self._attributes
|
||||
|
||||
def get_attribute(self, attribute, default=None):
|
||||
"""Returns the value of an attribute, or a specific default value if attribute is not found"""
|
||||
attribute = attribute.lower() # attributes are case insensitive
|
||||
if attribute in self.attributes:
|
||||
return self.attributes[attribute]
|
||||
else:
|
||||
return default
|
||||
|
||||
def fetch_api_information(self):
|
||||
"""Queries the API for information about this app, returns False if app is not publicly listed"""
|
||||
with http_client.get("http://api.badge.emfcamp.org/api/app/%s/%s" % (self.user, self.name)) as response:
|
||||
if response.status == 404:
|
||||
return False
|
||||
self.api_information = response.raise_for_status().json()
|
||||
return self.api_information
|
||||
|
||||
def __str__(self):
|
||||
return self.user_and_title
|
||||
|
||||
def __repr__(self):
|
||||
return "<App %s>" % (self.folder_name)
|
||||
|
||||
|
||||
def app_by_name_and_user(name, user):
|
||||
"""Returns an user object"""
|
||||
if user.lower() == EMF_USER:
|
||||
return App(name)
|
||||
else:
|
||||
return App(user + USER_NAME_SEPARATOR + name)
|
||||
|
||||
def app_by_api_response(response):
|
||||
if response["user"].lower() == EMF_USER:
|
||||
return App(response["name"], response)
|
||||
else:
|
||||
return App(response["user"] + USER_NAME_SEPARATOR + response["name"], response)
|
||||
|
||||
def get_local_apps(category=CATEGORY_ALL):
|
||||
"""Returns a list of apps that can be found in the apps folder"""
|
||||
apps = [App(folder_name) for folder_name in os.listdir("apps") if filesystem.is_dir("apps/" + folder_name)]
|
||||
return [app for app in apps if app.matches_category(category)]
|
||||
|
||||
_public_apps_cache = None
|
||||
def fetch_public_app_api_information(uncached=False):
|
||||
"""Returns a dict category => list of apps
|
||||
|
||||
Uses cached version unless the uncached parameter is set
|
||||
"""
|
||||
global _public_apps_cache
|
||||
if not _public_apps_cache or uncached:
|
||||
response = {}
|
||||
for category, apps in http_client.get("http://api.badge.emfcamp.org/api/apps").raise_for_status().json().items():
|
||||
response[category] = [app_by_api_response(app) for app in apps]
|
||||
|
||||
_public_apps_cache = response
|
||||
return _public_apps_cache
|
||||
|
||||
def get_public_app_categories(uncached=False):
|
||||
"""Returns a list of all categories used on the app library"""
|
||||
return list(fetch_public_app_api_information(uncached).keys())
|
||||
|
||||
def get_public_apps(category=CATEGORY_ALL, uncached=False):
|
||||
"""Returns a list of all public apps in one category"""
|
||||
category = category.lower()
|
||||
api_information = fetch_public_app_api_information(uncached)
|
||||
return api_information[category] if category in api_information else []
|
||||
|
||||
_category_cache = None
|
||||
def get_local_app_categories(uncached=False):
|
||||
"""Returns a list of all app categories the user's apps are currently using
|
||||
|
||||
Uses cached version unless the uncached parameter is set
|
||||
"""
|
||||
global _category_cache
|
||||
if not _category_cache or uncached:
|
||||
_category_cache = ["all"]
|
||||
for app in get_local_apps():
|
||||
if app.category not in _category_cache:
|
||||
_category_cache.append(app.category)
|
||||
|
||||
return _category_cache
|
||||
|
||||
def empty_local_app_cache():
|
||||
"""If you're tight on memory you can clean up the local cache"""
|
||||
global _public_apps_cache, _category_cache
|
||||
_public_apps_cache = None
|
||||
_category_cache = None
|
||||
gc.collect()
|
|
@ -0,0 +1,127 @@
|
|||
"""Convenience methods for dealing with the TiLDA buttons"""
|
||||
|
||||
___license___ = "MIT"
|
||||
|
||||
import pyb
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
_tilda_pins = {}
|
||||
_tilda_interrupts = {}
|
||||
_tilda_bounce = {}
|
||||
|
||||
def _get_pin(button):
|
||||
if button not in _tilda_pins:
|
||||
raise ValueError("Please call button.init() first before using any other button functions")
|
||||
return _tilda_pins[button]
|
||||
|
||||
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 is_pressed(button):
|
||||
pin = _get_pin(button)
|
||||
if pin.pull() == pyb.Pin.PULL_DOWN:
|
||||
return pin.value() > 0
|
||||
else:
|
||||
return pin.value() == 0
|
||||
|
||||
def is_triggered(button, interval = 30):
|
||||
"""Use this function if you want buttons as a trigger for something in a loop
|
||||
|
||||
It blocks for a while before returning a True and ignores trailing edge highs
|
||||
for a certain time to filter out bounce on both edges
|
||||
"""
|
||||
global _tilda_bounce
|
||||
if is_pressed(button):
|
||||
if button in _tilda_bounce:
|
||||
if pyb.millis() > _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)
|
||||
|
||||
# Wait until button is released again
|
||||
while is_pressed(button):
|
||||
pyb.wfi()
|
||||
|
||||
_tilda_bounce[button] = pyb.millis() + interval
|
||||
return True
|
||||
|
||||
def has_interrupt(button):
|
||||
global _tilda_interrupts
|
||||
_get_pin(button)
|
||||
if button in _tilda_interrupts:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def enable_interrupt(button, interrupt, on_press = True, on_release = False):
|
||||
"""Attaches an interrupt to a button
|
||||
|
||||
on_press defines whether it should be called when the button is pressed
|
||||
on_release defines whether it should be called when the button is releaseed
|
||||
|
||||
The callback function must accept exactly 1 argument, which is the line that
|
||||
triggered the interrupt.
|
||||
"""
|
||||
global _tilda_interrupts
|
||||
pin = _get_pin(button)
|
||||
if button in _tilda_interrupts:
|
||||
# If someone tries to set an interrupt on a pin that already
|
||||
# has one that's totally ok, but we need to remove the old one
|
||||
# first
|
||||
disable_interrupt(button)
|
||||
|
||||
if not (on_press or on_release):
|
||||
return
|
||||
|
||||
mode = None;
|
||||
if on_press and on_release:
|
||||
mode = pyb.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
|
||||
else:
|
||||
mode = pyb.ExtInt.IRQ_FALLING if on_press else pyb.ExtInt.IRQ_RISING
|
||||
|
||||
_tilda_interrupts[button] = {
|
||||
"interrupt": pyb.ExtInt(pin, mode, pin.pull(), interrupt),
|
||||
"mode": mode,
|
||||
"pin": pin
|
||||
}
|
||||
|
||||
def disable_interrupt(button):
|
||||
global _tilda_interrupts
|
||||
if button in _tilda_interrupts:
|
||||
interrupt = _tilda_interrupts[button]
|
||||
pyb.ExtInt(interrupt["pin"], interrupt["mode"], interrupt["pin"].pull(), None)
|
||||
del _tilda_interrupts[button]
|
||||
init([button])
|
||||
|
||||
def disable_all_interrupt():
|
||||
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")
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
"""A simple key/value store backed by a json file
|
||||
|
||||
Keys need to be convertable to str
|
||||
Values can be anything json can store, including a dict
|
||||
|
||||
Usage:
|
||||
|
||||
import database
|
||||
with database.open() as db:
|
||||
print(db.get("hello", "default"))
|
||||
db.set("foo", "world")
|
||||
db.delete("bar")
|
||||
|
||||
Or, to make things even easier, there are three static function:
|
||||
|
||||
import database
|
||||
print(database.get("hello", "default"))
|
||||
database.set("foo", "world")
|
||||
database.delete("bar")
|
||||
"""
|
||||
|
||||
___license___ = "MIT"
|
||||
|
||||
import os, json
|
||||
|
||||
class Database:
|
||||
def __init__(self, filename = "config.json"):
|
||||
self.filename = filename
|
||||
self.dirty = False
|
||||
try:
|
||||
with open(filename, "rt") as file:
|
||||
self.data = json.loads(file.read())
|
||||
except (OSError, ValueError):
|
||||
print("Database %s doesn't exists or is invalid, creating new" % (filename))
|
||||
self.data = {}
|
||||
self.dirty = True
|
||||
self.flush()
|
||||
|
||||
def set(self, key, value):
|
||||
"""Sets a value for a given key.
|
||||
|
||||
'key' gets converted into a string
|
||||
'value' can be anything that json can store, including a dict
|
||||
"""
|
||||
self.data[key] = value
|
||||
self.dirty = True
|
||||
|
||||
def get(self, key, default_value = None):
|
||||
"""Returns the value for a given key.
|
||||
|
||||
If key is not found 'default_value' will be returned
|
||||
"""
|
||||
return self.data[key] if key in self.data else default_value
|
||||
|
||||
def delete(self, key):
|
||||
"""Deletes a key/value pair"""
|
||||
if key in self.data:
|
||||
del self.data[key]
|
||||
self.dirty = True
|
||||
|
||||
|
||||
def flush(self):
|
||||
"""Writes changes to flash"""
|
||||
if self.dirty:
|
||||
with open(self.filename, "wt") as file:
|
||||
file.write(json.dumps(self.data))
|
||||
file.flush()
|
||||
os.sync()
|
||||
self.dirty = False
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
self.flush()
|
||||
|
||||
|
||||
def get(key, default_value = None, *args):
|
||||
with Database(*args) as db:
|
||||
return db.get(key, default_value)
|
||||
|
||||
def set(key, value, *args):
|
||||
with Database(*args) as db:
|
||||
return db.set(key, value)
|
||||
|
||||
def delete(key, *args):
|
||||
with Database(*args) as db:
|
||||
return db.delete(key)
|
|
@ -0,0 +1,207 @@
|
|||
"""Some basic UGFX powered dialogs"""
|
||||
|
||||
___license___ = "MIT"
|
||||
___dependencies___ = ["buttons"]
|
||||
|
||||
import ugfx
|
||||
import buttons
|
||||
import pyb
|
||||
|
||||
default_style_badge = ugfx.Style()
|
||||
default_style_badge.set_focus(ugfx.RED)
|
||||
default_style_badge.set_enabled([ugfx.WHITE, ugfx.html_color(0x3C0246), ugfx.GREY, ugfx.RED])
|
||||
default_style_badge.set_background(ugfx.html_color(0x3C0246))
|
||||
|
||||
default_style_dialog = ugfx.Style()
|
||||
default_style_dialog.set_enabled([ugfx.BLACK, ugfx.html_color(0xA66FB0), ugfx.html_color(0xdedede), ugfx.RED])
|
||||
default_style_dialog.set_background(ugfx.html_color(0xFFFFFF))
|
||||
|
||||
|
||||
TILDA_COLOR = ugfx.html_color(0x7c1143);
|
||||
|
||||
def notice(text, title="TiLDA", close_text="Close", width = 260, height = 180, font=ugfx.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):
|
||||
"""A simple one and two-options dialog
|
||||
|
||||
if 'false_text' is set to None only one button is displayed.
|
||||
If both 'true_text' and 'false_text' are given a boolean is returned
|
||||
"""
|
||||
global default_style_dialog
|
||||
if style == None:
|
||||
style = default_style_dialog
|
||||
ugfx.set_default_font(ugfx.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)
|
||||
window.text(5, 10, title, TILDA_COLOR)
|
||||
window.line(0, 30, width, 30, ugfx.BLACK)
|
||||
|
||||
if false_text:
|
||||
true_text = "A: " + true_text
|
||||
false_text = "B: " + false_text
|
||||
|
||||
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)
|
||||
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
|
||||
|
||||
try:
|
||||
buttons.init()
|
||||
|
||||
button_yes.attach_input(ugfx.BTN_A,0)
|
||||
if button_no: button_no.attach_input(ugfx.BTN_B,0)
|
||||
|
||||
window.show()
|
||||
|
||||
while True:
|
||||
pyb.wfi()
|
||||
if buttons.is_triggered("BTN_A"): return True
|
||||
if buttons.is_triggered("BTN_B"): return False
|
||||
|
||||
finally:
|
||||
window.hide()
|
||||
window.destroy()
|
||||
button_yes.destroy()
|
||||
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):
|
||||
"""Shows a dialog and keyboard that allows the user to input/change a string
|
||||
|
||||
Returns None if user aborts with button B
|
||||
"""
|
||||
|
||||
window = ugfx.Container(int((ugfx.width()-width)/2), int((ugfx.height()-height)/2), width, height, style=style)
|
||||
|
||||
if false_text:
|
||||
true_text = "M: " + true_text
|
||||
false_text = "B: " + false_text
|
||||
|
||||
if buttons.has_interrupt("BTN_MENU"):
|
||||
buttons.disable_interrupt("BTN_MENU")
|
||||
|
||||
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)
|
||||
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)
|
||||
label = ugfx.Label(int(width/10), int(height/10), int(width*4/5), int(height*2/5)-60, description, parent=window)
|
||||
|
||||
try:
|
||||
buttons.init()
|
||||
|
||||
button_yes.attach_input(ugfx.BTN_MENU,0)
|
||||
if button_no: button_no.attach_input(ugfx.BTN_B,0)
|
||||
|
||||
window.show()
|
||||
edit.set_focus()
|
||||
while True:
|
||||
pyb.wfi()
|
||||
ugfx.poll()
|
||||
#if buttons.is_triggered("BTN_A"): return edit.text()
|
||||
if buttons.is_triggered("BTN_B"): return None
|
||||
if buttons.is_triggered("BTN_MENU"): return edit.text()
|
||||
|
||||
finally:
|
||||
window.hide()
|
||||
window.destroy()
|
||||
button_yes.destroy()
|
||||
if button_no: button_no.destroy()
|
||||
label.destroy()
|
||||
kb.destroy()
|
||||
edit.destroy();
|
||||
return
|
||||
|
||||
def prompt_option(options, index=0, text = "Please select one of the following:", title=None, select_text="OK", none_text=None):
|
||||
"""Shows a dialog prompting for one of multiple options
|
||||
|
||||
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)
|
||||
window = ugfx.Container(5, 5, ugfx.width() - 10, ugfx.height() - 10)
|
||||
window.show()
|
||||
|
||||
list_y = 30
|
||||
if title:
|
||||
window.text(5, 10, title, TILDA_COLOR)
|
||||
window.line(0, 25, ugfx.width() - 10, 25, ugfx.BLACK)
|
||||
window.text(5, 30, text, ugfx.BLACK)
|
||||
list_y = 50
|
||||
else:
|
||||
window.text(5, 10, text, ugfx.BLACK)
|
||||
|
||||
options_list = ugfx.List(5, list_y, ugfx.width() - 25, 180 - list_y, parent = window)
|
||||
|
||||
for option in options:
|
||||
if isinstance(option, dict) and option["title"]:
|
||||
options_list.add_item(option["title"])
|
||||
else:
|
||||
options_list.add_item(str(option))
|
||||
options_list.selected_index(index)
|
||||
|
||||
select_text = "A: " + select_text
|
||||
if none_text:
|
||||
none_text = "B: " + none_text
|
||||
|
||||
button_select = ugfx.Button(5, ugfx.height() - 50, 140 if none_text else ugfx.width() - 25, 30 , select_text, parent=window)
|
||||
button_none = ugfx.Button(ugfx.width() - 160, ugfx.height() - 50, 140, 30 , none_text, parent=window) if none_text else None
|
||||
|
||||
try:
|
||||
buttons.init()
|
||||
|
||||
while True:
|
||||
pyb.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
|
||||
if button_none and buttons.is_triggered("BTN_MENU"): return None
|
||||
|
||||
finally:
|
||||
window.hide()
|
||||
window.destroy()
|
||||
options_list.destroy()
|
||||
button_select.destroy()
|
||||
if button_none: button_none.destroy()
|
||||
ugfx.poll()
|
||||
|
||||
class WaitingMessage:
|
||||
"""Shows a dialog with a certain message that can not be dismissed by the user"""
|
||||
def __init__(self, text = "Please Wait...", title="TiLDA"):
|
||||
self.window = ugfx.Container(30, 30, ugfx.width() - 60, ugfx.height() - 60)
|
||||
self.window.show()
|
||||
self.window.text(5, 10, title, TILDA_COLOR)
|
||||
self.window.line(0, 30, ugfx.width() - 60, 30, ugfx.BLACK)
|
||||
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()))
|
||||
|
||||
def destroy(self):
|
||||
self.timer.deinit()
|
||||
self.label.destroy()
|
||||
self.indicator.destroy()
|
||||
self.window.destroy()
|
||||
|
||||
@property
|
||||
def text(self):
|
||||
return self.label.text()
|
||||
|
||||
@text.setter
|
||||
def text(self, value):
|
||||
self.label.text(value)
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
self.destroy()
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
"""Shared functionality for home screen apps
|
||||
|
||||
Apps in the "homescreen" should behave in a similar manner to not confuse users.
|
||||
|
||||
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.
|
||||
* Not use
|
||||
|
||||
They also *may*:
|
||||
|
||||
* Display a name, returned by "homescreen.name()"
|
||||
* Display network strength "homescreen.mobile_strength()" (0-1, might return "None" if no SIM card found)
|
||||
* Display network strength "homescreen.wifi_strength()" (0-1, might return "None" if not connected)
|
||||
* Display remaining battery "homescreen.battery()" (0-1)
|
||||
"""
|
||||
|
||||
__license___ = "MIT"
|
||||
__dependencies___ = ["database", "buttons"]
|
||||
|
||||
import database, ugfx, buttons
|
||||
|
||||
def init(color = 0xFFFFFF):
|
||||
ugfx.init()
|
||||
ugfx.clear(ugfx.html_color(color))
|
||||
buttons.init()
|
||||
#buttons.enable_interrupt()
|
||||
|
||||
def menu():
|
||||
ugfx.clear()
|
||||
|
||||
def name():
|
||||
return database.get("homescreen.name", "Marek")
|
||||
|
||||
def mobile_strength():
|
||||
return 0.75
|
||||
|
||||
def wifi_strength():
|
||||
return 0.65
|
||||
|
||||
def battery():
|
||||
return 0.65
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
"""HTTP library specially tied to TiLDAs functionality"""
|
||||
|
||||
___license___ = "MIT"
|
|
@ -0,0 +1,153 @@
|
|||
"""Handles connecting to a wifi access point based on a valid wifi.json file"""
|
||||
|
||||
___license___ = "MIT"
|
||||
___dependencies___ = ["dialogs"]
|
||||
|
||||
import network
|
||||
import os
|
||||
import json
|
||||
import pyb
|
||||
import dialogs
|
||||
|
||||
_nic = None
|
||||
|
||||
def nic():
|
||||
global _nic
|
||||
if not _nic:
|
||||
_nic = network.CC3100()
|
||||
return _nic
|
||||
|
||||
def connection_details():
|
||||
data = None
|
||||
try:
|
||||
if "wifi.json" in os.listdir():
|
||||
with open("wifi.json") as f:
|
||||
data = json.loads(f.read())
|
||||
if 'ssid' not in data or not data['ssid']:
|
||||
data = None
|
||||
except ValueError as e:
|
||||
print(e)
|
||||
|
||||
return data
|
||||
|
||||
def ssid():
|
||||
return connection_details()["ssid"]
|
||||
|
||||
def connect(wait=True, timeout=10, show_wait_message=False, prompt_on_fail=True, dialog_title='TiLDA'):
|
||||
retry_connect = True
|
||||
|
||||
while retry_connect:
|
||||
if nic().is_connected():
|
||||
return
|
||||
|
||||
details = connection_details()
|
||||
if not details:
|
||||
if prompt_on_fail:
|
||||
choose_wifi(dialog_title=dialog_title)
|
||||
else:
|
||||
raise OSError("No valid wifi configuration")
|
||||
|
||||
if not wait:
|
||||
connect_wifi(details, timeout=None, wait=False)
|
||||
return
|
||||
else:
|
||||
try:
|
||||
if show_wait_message:
|
||||
with dialogs.WaitingMessage(text="Connecting to '%s'...\n(%ss timeout)" % (details['ssid'], timeout), title=dialog_title):
|
||||
connect_wifi(details, timeout=timeout, wait=True)
|
||||
else:
|
||||
connect_wifi(details, timeout=timeout, wait=True)
|
||||
except OSError:
|
||||
if prompt_on_fail:
|
||||
retry_connect = dialogs.prompt_boolean(
|
||||
text="Failed to connect to '%s'" % details['ssid'],
|
||||
title=dialog_title,
|
||||
true_text="Try again",
|
||||
false_text="Forget it",
|
||||
)
|
||||
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)
|
||||
else:
|
||||
nic().connect(details['ssid'], timeout=timeout)
|
||||
|
||||
if wait:
|
||||
while not nic().is_connected():
|
||||
nic().update()
|
||||
pyb.delay(100)
|
||||
|
||||
def is_connected():
|
||||
return nic().is_connected()
|
||||
|
||||
def get_security_level(ap):
|
||||
n = nic()
|
||||
levels = {}
|
||||
try:
|
||||
levels = {
|
||||
n.SCAN_SEC_OPEN: 0, # I am awful
|
||||
n.SCAN_SEC_WEP: 'WEP',
|
||||
n.SCAN_SEC_WPA: 'WPA',
|
||||
n.SCAN_SEC_WPA2: 'WPA2',
|
||||
}
|
||||
except AttributeError:
|
||||
print("Firmware too old to query wifi security level, please upgrade.")
|
||||
return None
|
||||
|
||||
return levels.get(ap.get('security', None), None)
|
||||
|
||||
def choose_wifi(dialog_title='TiLDA'):
|
||||
filtered_aps = []
|
||||
with dialogs.WaitingMessage(text='Scanning for networks...', title=dialog_title):
|
||||
visible_aps = nic().list_aps()
|
||||
visible_aps.sort(key=lambda x:x['rssi'], reverse=True)
|
||||
# We'll get one result for each AP, so filter dupes
|
||||
for ap in visible_aps:
|
||||
title = ap['ssid']
|
||||
security = get_security_level(ap)
|
||||
if security:
|
||||
title = title + ' (%s)' % security
|
||||
ap = {
|
||||
'title': title,
|
||||
'ssid': ap['ssid'],
|
||||
'security': security,
|
||||
}
|
||||
if ap['ssid'] not in [ a['ssid'] for a in filtered_aps ]:
|
||||
filtered_aps.append(ap)
|
||||
del visible_aps
|
||||
|
||||
ap = dialogs.prompt_option(
|
||||
filtered_aps,
|
||||
text='Choose wifi network',
|
||||
title=dialog_title
|
||||
)
|
||||
if ap:
|
||||
key = None
|
||||
if ap['security'] != 0:
|
||||
# Backward compat
|
||||
if ap['security'] == None:
|
||||
ap['security'] = 'wifi'
|
||||
|
||||
key = dialogs.prompt_text(
|
||||
"Enter %s key" % ap['security'],
|
||||
width = 310,
|
||||
height = 220
|
||||
)
|
||||
with open("wifi.json", "wt") as file:
|
||||
if key:
|
||||
conn_details = {"ssid": ap['ssid'], "pw": key}
|
||||
else:
|
||||
conn_details = {"ssid": ap['ssid']}
|
||||
|
||||
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()
|
|
@ -0,0 +1,9 @@
|
|||
"""Generic setting used by different apps"""
|
||||
|
||||
___name___ = "Settings"
|
||||
___license___ = "GPL"
|
||||
___categories___ = ["System"]
|
||||
___launchable___ = True
|
||||
___bootstrapped___ = True
|
||||
|
||||
print("settings")
|
Loading…
Reference in New Issue