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