Compare commits

...

No commits in common. "master" and "CDN" have entirely different histories.
master ... CDN

345 changed files with 2 additions and 46909 deletions

View File

@ -1,81 +0,0 @@
# This is meant to run on the badge
import hashlib, binascii, os
def split(path):
if path == "":
return ("", "")
r = path.rsplit("/", 1)
if len(r) == 1:
return ("", path)
head = r[0]
if not head:
head = "/"
return (head, r[1])
def dirname(path):
return split(path)[0]
def exists(path):
try:
os.stat(path)[0]
return True
except OSError:
return False
def makedirs(path):
sub_path = split(path)[0]
if sub_path and (not exists(sub_path)):
makedirs(sub_path)
if not exists(path):
os.mkdir(path)
def isdir(path):
try:
return os.stat(path)[0] & 0o170000 == 0o040000
except OSError:
return False
def h(p):
try:
with open(p, "rb") as f:
h = hashlib.sha256()
h.update(f.read())
print(str(binascii.hexlify(h.digest()), "utf8")[:10])
return
except:
pass
print("nooooooooo")
def w(p, c):
try:
print("file", p)
makedirs(dirname(p))
with open(p, "wb") as f:
f.write(binascii.a2b_base64(c))
os.sync()
print("OK")
except Exception as e:
import sys
print("Error while writing file %s" % p)
sys.print_exception(e)
pass
def clean(path=""):
for s in os.listdir(path):
full = "/".join([path, s]) if path else s
try:
if isdir(full):
try:
clean(full)
except:
pass
os.rmdir(full)
else:
os.remove(full)
except Exception as e:
print("Error while trying to clean '%s'" % full)
try:
os.remove("bootstrap.py")
except:
pass

View File

@ -1,58 +0,0 @@
import urllib.request, tempfile, os, shutil, subprocess
def firmware_update(verbose):
global __verbose
__verbose = verbose
temp_path = tempfile.mktemp("firmware.dfu")
url = "https://s3.amazonaws.com/tilda-badge/mk4/firmware.dfu"
device = "1cbe:00ff"
print("Hello - Welcome to the automated TiLDA Mk4 firmware updater")
try:
response = subprocess.run(["dfu-util", "--list"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if response.returncode != 0:
print(response)
return
if ("Found DFU: [%s]" % device) not in response.stdout.decode('utf-8'):
print(response.stdout.decode('utf-8'))
print("We couldn't find a DFU enabled badge. Please check the following:")
print("")
print("1) Your badge is plugged into this computer via USB")
print("2) The switch underneath the screen at the back of the badge is set to 'on'")
print("3) Your badge is in DFU mode. You can tell by a small, red flashing light at the back")
print("")
print("To put your badge into DFU mode (or if you're unsure whether it really is) you need to")
print("press the joystick centrally down while pressing the reset button at the back.")
print("")
print("After that, please try this script again.")
return
print("Downloading newest firmware: ", end="", flush=True)
with urllib.request.urlopen(url) as response:
with open(temp_path, 'wb') as tmp_file:
shutil.copyfileobj(response, tmp_file)
print("DONE")
response = subprocess.run(["dfu-util", "--download", temp_path])
if response.returncode != 0:
print("Something went wrong during DFU updload :(")
print("")
print(response)
return
print("")
print("You can now restart your badge by pressing the reset button on the back. Please follow the instructions on the screen to finish the setup")
print("Have a nice day!")
except FileNotFoundError as e:
if "No such file or directory: 'dfu-utils'" in str(e):
print("We couldn't find dfu-util. You might have to install it.")
print("You can find instructions here: http://dfu-util.sourceforge.net/")
print("Please try again after you've installed dfu-util.")
else:
raise e
finally:
if os.path.isfile(temp_path): os.remove(temp_path)

View File

@ -1,138 +0,0 @@
""" Consumes a stream and returns a dict
However, the dict won't contain "__doc__", "___version___" etc, but
the shortened versions without underscores: "doc", "version".
Currently not supported:
* Dicts
* Floating points
* ints in list
* Strings in any other format then "x" or 'y'
* Docstrings with any other delimiter other than triple-" or triple='
* Comments
Feel free to expand if necessary
"""
class ParseException(Exception):
"""Indicates a parsing exception"""
def __init__(self, message = ""):
super().__init__(message)
def read_metadata(s):
result = {}
result["doc"] = _read_docstring(s)
while True:
key = _read_key(s)
if key:
result[key] = _read_value(s)
else:
break
return result
def _read_docstring(s):
delimiter = _read_non_whitespace(s, 3)
if delimiter not in ["'''", '"""']:
raise ParseException("Docstring delimiter expected")
result = _read(s, 3);
while result[-3:] != delimiter:
result += _read(s)
return result[:-3]
def _read_value(s):
char = _read_non_whitespace(s)
return _read_value_given_first_char(s, char)
def _read_value_given_first_char(s, first_char):
if first_char in ["'", '"']:
return _read_string(s, first_char)
if first_char in "0123456789":
return _read_int(s, first_char)
if first_char in "TF":
return _read_bool(s, first_char)
if first_char == "[":
return _read_list(s)
raise ParseException("Invalid character %s found" % first_char)
def _read_string(s, delimiter):
result = _read(s)
try:
while result[-1:] != delimiter:
result += _read(s)
except ParseException:
raise ParseException("Invalid string or not terminated: %s" % result)
return result[:-1]
def _read_int(s, char):
result = char
while not char.isspace():
char = s.read(1)
if not char:
break
result += char
if not char in "0123456789":
raise ParseException("Invalid int: %s" % result)
return int(result)
def _read_bool(s, char):
if char == "T":
_assert(char + _read(s, 3), "True", "Invalid boolean")
return True
else:
_assert(char + _read(s, 4), "False", "Invalid boolean")
return False
def _read_list(s):
result = []
while True:
char = _read_non_whitespace(s)
if char == "]":
break
if result:
if char != ",":
raise ParseException("Expected comma, got '%s'" % char)
result.append(_read_value(s))
else:
result.append(_read_value_given_first_char(s, char))
return result
def _read_key(s):
delimiter = _read_non_whitespace(s, 3)
if delimiter != "___":
return None
try:
result = _read(s, 3);
while result[-3:] != delimiter:
char = _read(s)
if char in [" ", "="]:
raise ParseException()
result += char
except ParseException:
raise ParseException("Invalid key: ___%s" % result)
_assert(_read_non_whitespace(s), "=", "Expected equals")
return result[:-3]
def _read(s, l=1):
result = s.read(l)
if len(result)<l:
raise ParseException("Expected to read at least %s characters, got '%s'" % (l, result))
return result
def _assert(input, expected, message):
if not input == expected:
raise ParseException(message + " ('%s' expected, '%s' found)" % (expected, input))
def _read_non_whitespace(s, l=1):
result = s.read(1)
while result.isspace():
result = s.read(1)
if l == 1:
return result
else:
return result + s.read(l - 1)

View File

@ -1,467 +0,0 @@
#!/usr/bin/env python
#
# This file is part of the MicroPython project, http://micropython.org/
#
# The MIT License (MIT)
#
# Copyright (c) 2014-2016 Damien P. George
# Copyright (c) 2017 Paul Sokolovsky
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
"""
pyboard interface
This module provides the Pyboard class, used to communicate with and
control a MicroPython device over a communication channel. Both real
boards and emulated devices (e.g. running in QEMU) are supported.
Various communication channels are supported, including a serial
connection, telnet-style network connection, external process
connection.
Example usage:
import pyboard
pyb = pyboard.Pyboard('/dev/ttyACM0')
Or:
pyb = pyboard.Pyboard('192.168.1.1')
Then:
pyb.enter_raw_repl()
pyb.exec('pyb.LED(1).on()')
pyb.exit_raw_repl()
Note: if using Python2 then pyb.exec must be written as pyb.exec_.
To run a script from the local machine on the board and print out the results:
import pyboard
pyboard.execfile('test.py', device='/dev/ttyACM0')
This script can also be run directly. To execute a local script, use:
./pyboard.py test.py
Or:
python pyboard.py test.py
"""
import sys
import time
import os
try:
stdout = sys.stdout.buffer
except AttributeError:
# Python2 doesn't have buffer attr
stdout = sys.stdout
def stdout_write_bytes(b):
b = b.replace(b"\x04", b"")
stdout.write(b)
stdout.flush()
class PyboardError(BaseException):
pass
class TelnetToSerial:
def __init__(self, ip, user, password, read_timeout=None):
import telnetlib
self.tn = telnetlib.Telnet(ip, timeout=15)
self.read_timeout = read_timeout
if b'Login as:' in self.tn.read_until(b'Login as:', timeout=read_timeout):
self.tn.write(bytes(user, 'ascii') + b"\r\n")
if b'Password:' in self.tn.read_until(b'Password:', timeout=read_timeout):
# needed because of internal implementation details of the telnet server
time.sleep(0.2)
self.tn.write(bytes(password, 'ascii') + b"\r\n")
if b'for more information.' in self.tn.read_until(b'Type "help()" for more information.', timeout=read_timeout):
# login successful
from collections import deque
self.fifo = deque()
return
raise PyboardError('Failed to establish a telnet connection with the board')
def __del__(self):
self.close()
def close(self):
try:
self.tn.close()
except:
# the telnet object might not exist yet, so ignore this one
pass
def read(self, size=1):
while len(self.fifo) < size:
timeout_count = 0
data = self.tn.read_eager()
if len(data):
self.fifo.extend(data)
timeout_count = 0
else:
time.sleep(0.25)
if self.read_timeout is not None and timeout_count > 4 * self.read_timeout:
break
timeout_count += 1
data = b''
while len(data) < size and len(self.fifo) > 0:
data += bytes([self.fifo.popleft()])
return data
def write(self, data):
self.tn.write(data)
return len(data)
def inWaiting(self):
n_waiting = len(self.fifo)
if not n_waiting:
data = self.tn.read_eager()
self.fifo.extend(data)
return len(data)
else:
return n_waiting
class ProcessToSerial:
"Execute a process and emulate serial connection using its stdin/stdout."
def __init__(self, cmd):
import subprocess
self.subp = subprocess.Popen(cmd.split(), bufsize=0, shell=True, preexec_fn=os.setsid,
stdin=subprocess.PIPE, stdout=subprocess.PIPE)
# Initially was implemented with selectors, but that adds Python3
# dependency. However, there can be race conditions communicating
# with a particular child process (like QEMU), and selectors may
# still work better in that case, so left inplace for now.
#
#import selectors
#self.sel = selectors.DefaultSelector()
#self.sel.register(self.subp.stdout, selectors.EVENT_READ)
import select
self.poll = select.poll()
self.poll.register(self.subp.stdout.fileno())
def close(self):
import signal
os.killpg(os.getpgid(self.subp.pid), signal.SIGTERM)
def read(self, size=1):
data = b""
while len(data) < size:
data += self.subp.stdout.read(size - len(data))
return data
def write(self, data):
self.subp.stdin.write(data)
return len(data)
def inWaiting(self):
#res = self.sel.select(0)
res = self.poll.poll(0)
if res:
return 1
return 0
class ProcessPtyToTerminal:
"""Execute a process which creates a PTY and prints slave PTY as
first line of its output, and emulate serial connection using
this PTY."""
def __init__(self, cmd):
import subprocess
import re
import serial
self.subp = subprocess.Popen(cmd.split(), bufsize=0, shell=False, preexec_fn=os.setsid,
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
pty_line = self.subp.stderr.readline().decode("utf-8")
m = re.search(r"/dev/pts/[0-9]+", pty_line)
if not m:
print("Error: unable to find PTY device in startup line:", pty_line)
self.close()
sys.exit(1)
pty = m.group()
# rtscts, dsrdtr params are to workaround pyserial bug:
# http://stackoverflow.com/questions/34831131/pyserial-does-not-play-well-with-virtual-port
self.ser = serial.Serial(pty, interCharTimeout=1, rtscts=True, dsrdtr=True)
def close(self):
import signal
os.killpg(os.getpgid(self.subp.pid), signal.SIGTERM)
def read(self, size=1):
return self.ser.read(size)
def write(self, data):
return self.ser.write(data)
def inWaiting(self):
return self.ser.inWaiting()
class Pyboard:
def __init__(self, device, baudrate=115200, user='micro', password='python', wait=0):
if device.startswith("exec:"):
self.serial = ProcessToSerial(device[len("exec:"):])
elif device.startswith("execpty:"):
self.serial = ProcessPtyToTerminal(device[len("qemupty:"):])
elif device and device[0].isdigit() and device[-1].isdigit() and device.count('.') == 3:
# device looks like an IP address
self.serial = TelnetToSerial(device, user, password, read_timeout=10)
else:
import serial
delayed = False
for attempt in range(wait + 1):
try:
self.serial = serial.Serial(device, baudrate=baudrate, interCharTimeout=1, timeout=1, write_timeout=1)
break
except (OSError, IOError): # Py2 and Py3 have different errors
if wait == 0:
continue
if attempt == 0:
sys.stdout.write('Waiting {} seconds for pyboard '.format(wait))
delayed = True
time.sleep(1)
sys.stdout.write('.')
sys.stdout.flush()
else:
if delayed:
print('')
raise PyboardError('failed to access ' + device)
if delayed:
print('')
def close(self):
self.serial.close()
def read_until(self, min_num_bytes, ending, timeout=10, data_consumer=None):
data = self.serial.read(min_num_bytes)
if data_consumer:
data_consumer(data)
timeout_count = 0
while True:
if data.endswith(ending):
break
elif self.serial.inWaiting() > 0:
new_data = self.serial.read(1)
data = data + new_data
if data_consumer:
data_consumer(new_data)
timeout_count = 0
else:
timeout_count += 1
if timeout is not None and timeout_count >= 100 * timeout:
break
time.sleep(0.01)
return data
def enter_raw_repl(self, retry_count = 2):
self.serial.write(b'\r\x03\x03') # ctrl-C twice: interrupt any running program
# flush input (without relying on serial.flushInput())
n = self.serial.inWaiting()
while n > 0:
self.serial.read(n)
n = self.serial.inWaiting()
self.serial.write(b'\r\x01') # ctrl-A: enter raw REPL
data = self.read_until(1, b'raw REPL; CTRL-B to exit\r\n>')
if not data.endswith(b'raw REPL; CTRL-B to exit\r\n>'):
print(data)
if retry_count:
self.enter_raw_repl(retry_count - 1)
raise PyboardError('could not enter raw repl')
self.serial.write(b'\x04') # ctrl-D: soft reset
data = self.read_until(1, b'soft reboot\r\n')
if not data.endswith(b'soft reboot\r\n'):
print(data)
raise PyboardError('could not enter raw repl')
# By splitting this into 2 reads, it allows boot.py to print stuff,
# which will show up after the soft reboot and before the raw REPL.
data = self.read_until(1, b'raw REPL; CTRL-B to exit\r\n')
if not data.endswith(b'raw REPL; CTRL-B to exit\r\n'):
print(data)
raise PyboardError('could not enter raw repl')
def exit_raw_repl(self):\
self.serial.write(b'\r\x02') # ctrl-B: enter friendly REPL
def follow(self, timeout, data_consumer=None):
# wait for normal output
data = self.read_until(1, b'\x04', timeout=timeout, data_consumer=data_consumer)
if not data.endswith(b'\x04'):
raise PyboardError('timeout waiting for first EOF reception')
data = data[:-1]
# wait for error output
data_err = self.read_until(1, b'\x04', timeout=timeout)
if not data_err.endswith(b'\x04'):
raise PyboardError('timeout waiting for second EOF reception')
data_err = data_err[:-1]
# return normal and error output
return data, data_err
def exec_raw_no_follow(self, command):
if isinstance(command, bytes):
command_bytes = command
else:
command_bytes = bytes(command, encoding='utf8')
# check we have a prompt
data = self.read_until(1, b'>')
if not data.endswith(b'>'):
raise PyboardError('could not enter raw repl')
# write command
for i in range(0, len(command_bytes), 256):
self.serial.write(command_bytes[i:min(i + 256, len(command_bytes))])
time.sleep(0.01)
self.serial.write(b'\x04')
# check if we could exec command
data = self.serial.read(2)
if data != b'OK':
raise PyboardError('could not exec command (response: %r)' % data)
def exec_raw(self, command, timeout=10, data_consumer=None):
self.exec_raw_no_follow(command);
return self.follow(timeout, data_consumer)
def eval(self, expression):
ret = self.exec_('print({})'.format(expression))
ret = ret.strip()
return ret
def exec_(self, command):
ret, ret_err = self.exec_raw(command)
if ret_err:
raise PyboardError('exception', ret, ret_err)
return ret
def execfile(self, filename):
with open(filename, 'rb') as f:
pyfile = f.read()
return self.exec_(pyfile)
def get_time(self):
t = str(self.eval('pyb.RTC().datetime()'), encoding='utf8')[1:-1].split(', ')
return int(t[4]) * 3600 + int(t[5]) * 60 + int(t[6])
# in Python2 exec is a keyword so one must use "exec_"
# but for Python3 we want to provide the nicer version "exec"
setattr(Pyboard, "exec", Pyboard.exec_)
def execfile(filename, device='/dev/ttyACM0', baudrate=115200, user='micro', password='python'):
pyb = Pyboard(device, baudrate, user, password)
pyb.enter_raw_repl()
output = pyb.execfile(filename)
stdout_write_bytes(output)
pyb.exit_raw_repl()
pyb.close()
def main():
import argparse
cmd_parser = argparse.ArgumentParser(description='Run scripts on the pyboard.')
cmd_parser.add_argument('--device', default='/dev/ttyACM0', help='the serial device or the IP address of the pyboard')
cmd_parser.add_argument('-b', '--baudrate', default=115200, help='the baud rate of the serial device')
cmd_parser.add_argument('-u', '--user', default='micro', help='the telnet login username')
cmd_parser.add_argument('-p', '--password', default='python', help='the telnet login password')
cmd_parser.add_argument('-c', '--command', help='program passed in as string')
cmd_parser.add_argument('-w', '--wait', default=0, type=int, help='seconds to wait for USB connected board to become available')
cmd_parser.add_argument('--follow', action='store_true', help='follow the output after running the scripts [default if no scripts given]')
cmd_parser.add_argument('files', nargs='*', help='input files')
args = cmd_parser.parse_args()
# open the connection to the pyboard
try:
pyb = Pyboard(args.device, args.baudrate, args.user, args.password, args.wait)
except PyboardError as er:
print(er)
sys.exit(1)
# run any command or file(s)
if args.command is not None or len(args.files):
# we must enter raw-REPL mode to execute commands
# this will do a soft-reset of the board
try:
pyb.enter_raw_repl()
except PyboardError as er:
print(er)
pyb.close()
sys.exit(1)
def execbuffer(buf):
try:
ret, ret_err = pyb.exec_raw(buf, timeout=None, data_consumer=stdout_write_bytes)
except PyboardError as er:
print(er)
pyb.close()
sys.exit(1)
except KeyboardInterrupt:
sys.exit(1)
if ret_err:
pyb.exit_raw_repl()
pyb.close()
stdout_write_bytes(ret_err)
sys.exit(1)
# run the command, if given
if args.command is not None:
execbuffer(args.command.encode('utf-8'))
# run any files
for filename in args.files:
with open(filename, 'rb') as f:
pyfile = f.read()
execbuffer(pyfile)
# exiting raw-REPL just drops to friendly-REPL mode
pyb.exit_raw_repl()
# if asked explicitly, or no files given, then follow the output
if args.follow or (args.command is None and len(args.files) == 0):
try:
ret, ret_err = pyb.follow(timeout=None, data_consumer=stdout_write_bytes)
except PyboardError as er:
print(er)
sys.exit(1)
except KeyboardInterrupt:
sys.exit(1)
if ret_err:
pyb.close()
stdout_write_bytes(ret_err)
sys.exit(1)
# close the connection to the pyboard
pyb.close()
if __name__ == "__main__":
main()

View File

@ -1,237 +0,0 @@
from pyboard import Pyboard, PyboardError
import glob, sys, pyboard, json, binascii, os, hashlib
_pyb = None
def get_pyb(args):
global _pyb
if not _pyb:
print("Connected to badge:", end="", flush=True)
if not args.device:
args.device = find_tty()
# open the connection to the pyboard
try:
_pyb = Pyboard(args.device, args.baudrate, None, None, args.wait)
except PyboardError as er:
print(" FAIL")
print(er)
sys.exit(1)
print(" DONE")
return _pyb
def close_pyb():
global _pyb
if _pyb:
_pyb.close()
def stop_badge(args, verbose):
pyb = get_pyb(args)
print("Stopping running app:", end="", flush=True)
write_command(pyb, b'\r\x03\x03') # ctrl-C twice: interrupt any running program
n = pyb.serial.inWaiting()
while n > 0:
pyb.serial.read(n)
n = pyb.serial.inWaiting()
print(" DONE")
def write_command(pyb, command):
flush_input(pyb)
pyb.serial.write(command)
flush_input(pyb)
def flush_input(pyb):
n = pyb.serial.inWaiting()
while n > 0:
pyb.serial.read(n)
n = pyb.serial.inWaiting()
def soft_reset(args, verbose = True):
pyb = get_pyb(args)
if verbose:
print("Soft reboot:", end="", flush=True)
write_command(pyb, b'\x04') # ctrl-D: soft reset
data = pyb.read_until(1, b'soft reboot\r\n')
if data.endswith(b'soft reboot\r\n'):
if verbose:
print(" DONE")
else:
if verbose:
print(" FAIL")
raise PyboardError('could not soft reboot')
def find_tty():
# Todo: test in linux, let user pick if multiple ports are available
for pattern in ['/dev/ttyACM*', '/dev/tty.usbmodemTiLDA*', '/dev/tty.usbmodem*']:
for path in glob.glob(pattern):
return path
if sys.platform.startswith('win'):
import serial
for port in ['COM%s' % (i + 1) for i in range(256)]:
try:
s = serial.Serial(port)
s.close()
return port
except (OSError, serial.SerialException):
pass
print("Couldn't find badge tty - Please make it's plugged in and reset it if necessary")
sys.exit(1)
def check_run(paths):
for filename in paths:
with open(filename, 'r', encoding='utf8') as f:
pyfile = f.read()
compile(pyfile + '\n', filename, 'exec')
def execbuffer(pyb, buf):
try:
ret, ret_err = pyb.exec_raw(buf, timeout=None, data_consumer=pyboard.stdout_write_bytes)
except PyboardError as er:
print(er)
pyb.close()
sys.exit(1)
except KeyboardInterrupt:
sys.exit(1)
if ret_err:
pyb.exit_raw_repl()
pyb.close()
pyboard.stdout_write_bytes(ret_err)
sys.exit(1)
def returnbuffer(pyb, buf):
res = b''
def add_to_res(b):
nonlocal res
res += b
try:
ret, ret_err = pyb.exec_raw(buf, timeout=None, data_consumer=add_to_res)
except PyboardError as er:
print(er)
pyb.close()
sys.exit(1)
except KeyboardInterrupt:
sys.exit(1)
if ret_err:
pyb.exit_raw_repl()
pyb.close()
pyboard.stdout_write_bytes(ret_err)
sys.exit(1)
return res.decode('ascii').strip()
def run(args, paths, verbose=True):
pyb = get_pyb(args)
if verbose:
print("Preparing execution:", end=" ", flush=True)
# run any command or file(s) - this is mostly a copy from pyboard.py
if len(paths):
# we must enter raw-REPL mode to execute commands
# this will do a soft-reset of the board
try:
pyb.enter_raw_repl()
except PyboardError as er:
if verbose:
print(" FAIL")
print(er)
pyb.close()
sys.exit(1)
if verbose:
print(" DONE")
try:
# run any files
for filename in paths:
with open(filename, 'rb') as f:
print("-------- %s --------" % filename)
pyfile = f.read()
execbuffer(pyb, pyfile)
# exiting raw-REPL just drops to friendly-REPL mode
pyb.exit_raw_repl()
except OSError as e:
if "Device not configured" in str(e):
print("Connection to badge lost") # This can happen on a hard rest
else:
raise e
# Please don't judge me too harshly for this hack, I had lots of problems with the
# USB mass storage protocol and at some point it looked simpler to just avoid it
# altogether. This _seems_ to work, so maybe it isn't that terrible after all.
def init_copy_via_repl(args):
pyb = get_pyb(args)
print("Init copy via repl:", end=" ", flush=True)
try:
pyb.enter_raw_repl()
with open(os.path.join(os.path.dirname(__file__), "copy_via_repl_header.py"), "rt") as f:
execbuffer(pyb, f.read())
except PyboardError as er:
print("FAIL")
print(er)
pyb.close()
sys.exit(1)
print("DONE")
def copy_via_repl(args, path, rel_path):
with open(path, "rb") as f:
return write_via_repl(args, f.read(), rel_path)
def write_via_repl(args, content, rel_path):
pyb = get_pyb(args)
h = hashlib.sha256()
h.update(content)
content = binascii.b2a_base64(content).decode('ascii').strip()
if os.sep != '/':
rel_path = rel_path.replace(os.sep, '/')
rel_path_as_string = json.dumps(rel_path) # make sure quotes are escaped
cmd = "h(%s)" % rel_path_as_string
badge_hash = returnbuffer(pyb,cmd).splitlines()[0]
local_hash = str(binascii.hexlify(h.digest()), "utf8")[:10]
if badge_hash == local_hash:
# we don't need to update those files
return False
cmd = "w(%s, \"%s\")\n" % (rel_path_as_string, content)
result = returnbuffer(pyb,cmd)
if "OK" in result:
return True
raise Exception("Couldn't write %s to badge: %s" % (rel_path, result))
def end_copy_via_repl(args):
# do we need to do anything?
pass
def clean_via_repl(args):
init_copy_via_repl(args)
print("Cleaning:", end=" ", flush=True)
try:
execbuffer(get_pyb(args), "clean()")
except PyboardError as er:
print("FAIL")
print(er)
pyb.close()
sys.exit(1)
print("DONE")
def hard_reset(args):
pyb = get_pyb(args)
print("Hard reset:", end=" ", flush=True)
try:
pyb.enter_raw_repl()
execbuffer(pyb, "import machine\nmachine.reset()\n")
print("UNEXPECTED")
except PyboardError as er:
print("FAIL")
print(er)
pyb.close()
sys.exit(1)
except Exception as e:
if "Errno 6" in str(e):
print("DONE")
else:
raise e

View File

@ -1,250 +0,0 @@
import os, hashlib, binascii
from metadata_reader import read_metadata, ParseException
"""
Resources are the basic components of any given library.
supported the following types:
* lib (files in lib/)
* shared (file in shared/)
* app (all other folders in root)
* root (files in root)
A resource has the following form:
{
"app1": {"type": "app", dependencies: ["lib/lib1.py"], "files": {"app1/main.py": "abcdef1234", "app1/nested/text.txt": "abcdef1234"}},
"app2": {"type": "app", dependencies: ["lib/lib2.py"], "files": {"app2/main.py": "abcdef1234", "app2/other_content.txt": "abcdef1234", "app2/some_binary_file.gif": "abcdef1234"}},
"lib/lib1.py": {"type": "lib", , dependencies: ["lib/lib3.py"], "files": {"lib/lib1.py": "abcdef1234"}},
"lib/lib2.py": {"type": "lib", "files": {"lib/lib2.py": "abcdef1234"}},
"lib/lib3.py": {"type": "lib", "files": {"lib/lib3.py": "abcdef1234"}},
"lib/lib4.py": {"type": "lib", "files": {"lib/lib4.py": "abcdef1234"}},
"lib/lib5.py": {"type": "lib", "files": {"lib/lib5.py": "abcdef1234"}},
"shared/foo.txt": {"type": "shared", "files": {"shared/foo.txt": "abcdef1234"}},
"boot.py": {"type": "root", "files": {"boot.py": "abcdef1234"}}
}
Every resource can also contain other metadata fields which are extracted from the body
of its main python class (in case of lib or app).
This module has the following operations:
resources = get_resources(path) # Gets resources for a given path
add_hashes(path, resources) # Adds hashes to the file dict - not needed for testing
add_metadata(path, resources) # Adds metadata
validate(resources) # Runs basic validation
resolve_dependencies(resources) # Merges all dependencies into each resource's file dict
remove_upip(resources) # Remove upip resources from dict again
This module encapsulates all the main operations the app library is expect to
perform on a given checkout. It's intentionally kept in one file to make it easier
to share between repositories. The only exception to this rule it metadata_reader
(because it's rather complex and I didn't want to make this file impossible to read)
Please make sure this file can be executes on any operating system running python3.
Don't include any external dependencies. It forms part of the local toolchain.
"""
"""
scan(path)
A resource scanner for the Tilda filesystem. Returns a {path: {type:<type>, files:{...}}}
ignored are the following:
* dotfiles
* __pycache__
"""
def _scan_files(path, rel_path = ""):
result = []
for element in os.listdir(path):
if element.startswith(".") or element == "__pycache__":
continue
element_path = os.path.join(path, element)
element_rel_path = os.path.join(rel_path, element)
if os.path.isdir(element_path):
result.extend(_scan_files(element_path, element_rel_path))
else:
result.append(element_rel_path)
return result
def get_resources(path):
result = {}
for sub_path in os.listdir(path):
if sub_path.startswith(".") or sub_path == "__pycache__":
continue
full_path = os.path.join(path, sub_path)
if os.path.islink(full_path):
continue
if os.path.isfile(full_path):
result[sub_path] = {"type": "root", "files": {sub_path: None}}
continue
if sub_path in ["lib", "shared"]:
files = _scan_files(full_path, sub_path)
for rel_path in files:
result[rel_path] = {"type": sub_path, "files": {rel_path: None}}
elif sub_path == "upip":
for upip_lib in os.listdir(full_path):
if upip_lib.startswith(".") or upip_lib == "__pycache__":
continue
full_lib_path = os.path.join(full_path, upip_lib)
rel_lib_path = os.path.join(sub_path, upip_lib)
files = {}
if os.path.isfile(full_lib_path):
files = {rel_lib_path: None}
upip_lib = upip_lib.rsplit('.', 1)[0]
else:
for rel_path in _scan_files(full_lib_path, os.path.join(sub_path, upip_lib)):
files[rel_path] = None
result["upip:%s" % upip_lib] = {"type": sub_path, "files": files}
else:
files = _scan_files(full_path, sub_path)
result[sub_path] = {"type": "app", "files": {}}
for rel_path in files:
result[sub_path]["files"][rel_path] = None
return result
"""
add_hashes(path, resource)
Adds the first 10 characters of SHA256 hashes to all elements in "files".
The hash is calcuated on the file content, not the file name.
"""
def add_hashes(path, resources):
for resource in resources.values():
for file_path in resource["files"]:
resource["files"][file_path] = _hash_file(os.path.join(path, file_path))
def _hash_file(filename):
"""Calculates the SHA256 hash of a file."""
with open(filename, "rb") as file:
sha256 = hashlib.sha256()
buf = file.read(128)
while len(buf) > 0:
sha256.update(buf)
buf = file.read(128)
return str(binascii.hexlify(sha256.digest()), "utf8")[:10]
"""
add_metadata(path, resource)
Reads primary files for app and lib resources and extracts metadata information from its header
"""
def add_metadata(path, resources):
for resource in resources.values():
file = None
if resource['type'] == "app":
file = next(f for f in resource['files'] if os.path.basename(f) == "main.py")
elif resource['type'] == "lib":
file = next(iter(resource['files'].keys()))
if file:
try:
with open(os.path.join(path, file), "r", encoding='utf8') as stream:
resource.update(_normalize_metadata(read_metadata(stream)))
except ParseException as e:
resource.setdefault("errors", []).append(file + ": " + str(e))
def _normalize_metadata(metadata):
metadata['description'] = metadata.pop('doc')
if 'dependencies' in metadata:
metadata['dependencies'] = [normalize_dependency(l) for l in metadata.pop('dependencies')]
return metadata
"""
resolve_dependencies(resources)
merges files from dependent resources into the original files dict
"""
def resolve_dependencies(resources):
for file, resource in resources.items():
if 'dependencies' in resource:
already_added = [file]
to_add = resource['dependencies'].copy()
while len(to_add):
r = to_add.pop()
r = os.path.normpath(r)
if r in already_added:
continue
if r not in resources:
resource.setdefault("errors", []).append("Dependency %s not found" % r)
continue
already_added.append(r)
to_add.extend(resources[r].get("dependencies", []))
resource['files'].update(resources[r]['files'])
"""
validate(path, resources)
does basic verification:
* Is it valid python?
* Are metadata fields missing
* TBD: Does it have imports that are not dependencies?
"""
def validate(path, resources):
for resource in resources.values():
if resource['type'] == "upip":
continue
_validate_resource(path, resource)
def _validate_resource(path, resource):
# Compile
for file in resource['files'].keys():
if file.endswith(".py"):
try:
filename = os.path.join(path, file)
with open(filename, 'r', encoding='utf8') as s:
compile(s.read() + '\n', filename, 'exec')
except Exception as e:
resource.setdefault("errors", []).append(str(e))
# Metadata check
if resource['type'] in ["app", "lib"]:
pass #todo: should we make license required?
if resource['type'] == "app":
if 'categories' not in resource or (not isinstance(resource['categories'], list)) or len(resource['categories']) == 0:
resource.setdefault("errors", []).append("___categories___ list is required in main.py but not found")
"""
remove_upip(resources)
upip adds over a 100 resources to the list. Some of them have broken validation as well, so it's
useful to remove them after resolving dependencies.
"""
def remove_upip(resources):
to_delete = []
for key, resource in resources.items():
if resource['type'] == "upip":
to_delete.append(key)
for key in to_delete:
del resources[key]
"""
helpers
"""
def get_error_summary(resources):
summary = ""
for key, resource in resources.items():
if "errors" in resource:
summary += "--- %s ---\n" % key
for error in resource['errors']:
summary += error + "\n"
summary += "\n"
return summary.strip()
def pretty_print_resources(resources):
import json
return json.dumps(resources, indent=4)
def normalize_dependency(dependency):
"""lib dependencies can be shortened to just their module name"""
if "." in dependency or os.pathsep in dependency or "upip:" in dependency:
return dependency
return os.path.join("lib", "%s.py" % dependency)

View File

@ -1,77 +0,0 @@
import os, shutil, sys, fnmatch, glob, pyboard_util
def sync(args, patterns, resources, verbose, skip_wifi):
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))])
# Always copy boot.py
paths.add("boot.py")
# wifi.json
if not skip_wifi:
wifi_path = os.path.join(root, "wifi.json")
if os.path.isfile(wifi_path):
paths.add(wifi_path)
if not patterns:
patterns = ["*"]
synced_resources = []
for pattern in patterns:
found = False
for key, resource in resources.items():
if fnmatch.fnmatch(key, pattern):
found = True
synced_resources.append(key)
if verbose:
print("Resource %s is going to be synced" % key)
for path in resource['files'].keys():
paths.add(path)
if not found and (pattern not in paths):
print("WARN: No resources to copy found for pattern %s" % patterns)
pyboard_util.init_copy_via_repl(args)
if not verbose:
print("Copying %s files: " % len(paths), end="", flush=True)
for path in paths:
if not path:
continue
rel_path = os.path.relpath(path, root)
if rel_path.startswith(".") or os.path.isdir(path) or os.path.islink(path):
continue
updated = pyboard_util.copy_via_repl(args, path, rel_path)
if verbose:
print("Copied %s, updated: %s" % (rel_path, updated))
else:
if updated:
print("+", end="", flush=True)
else:
print("=", end="", flush=True)
pyboard_util.end_copy_via_repl(args)
if verbose:
print("Files copied successfully")
else:
print(" DONE")
return synced_resources
def clean(args):
pyboard_util.clean_via_repl(args)
def set_boot_app(args, app_to_boot):
content = app_to_boot + "\n"
pyboard_util.write_via_repl(args, content.encode("utf8"), 'once.txt')
if app_to_boot:
print("setting next boot to %s" % app_to_boot)
def set_no_boot(args):
pyboard_util.write_via_repl(args, b"\n", 'no_boot')
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.")
sys.exit(1)
return root

View File

@ -1,164 +0,0 @@
#!/usr/bin/env python3
"""Toolchain for working with the TiLDA Mk4
Usage
------------------------------------
Reboot badge
$ tilda_tools reset
Soft reboot badge and start specific app
$ tilda_tools reset --boot my_app
Update files in folder(s) to match current local version
$ tilda_tools sync my_game shared
$ tilda_tools sync <pattern1> <pattern2> ...
Sync (as above), but execute my_app after reboot
$ tilda_tools sync --boot my_app [<other sync parameter>]
Sync (as above), but execute a single file afterwards without copying it to the badge
$ tilda_tools sync --run some_other_file.py
Sync a given app and execute it
$ tilda_tools app home_default
Executes a single file on the badge without copying anything (Using pyboard.py)
$ tilda_tools run my_app/main.py
Runs local validation (doesn't require a badge, but doesn't run unit tests)
$ tilda_tools validate
Runs local validation and badge-side tests
$ tilda_tools test
Update firmware on badge (warning, this will delete all settings etc. stored on the badge!)
$ tilda_tools firmware-update
Setup wifi.json to be copied to the badge on every sync
$ tilda_tools wifi
Common parameters
-----------------
-d --device : serial interface (default: auto)
-s --storage : path to flash storage
"""
import glob
import sync, firmware_update, wifi, pyboard_util, sys
from resources import *
def main():
import argparse
cmd_parser = argparse.ArgumentParser(description='Toolchain for working with the TiLDA Mk4')
cmd_parser.add_argument('command', nargs=1, help='command [test|reset|sync|run|validate|wifi|firmware-update|app|bootstrap]', choices=['test', 'reset', 'sync', 'validate', 'run', 'wifi', 'firmware-update', 'app', 'bootstrap'])
cmd_parser.add_argument('-c', '--clean', action='store_true', help='clean mass storage before writing')
cmd_parser.add_argument('-d', '--device', help='the serial device of the badge')
cmd_parser.add_argument('-s', '--storage', help='the usb mass storage path of the badge')
cmd_parser.add_argument('-b', '--baudrate', default=115200, help='the baud rate of the serial device')
cmd_parser.add_argument('-v', '--verbose', action='store_true', help='adds more output')
cmd_parser.add_argument('--skip-wifi', action='store_true', help='does not sync wifi.json')
cmd_parser.add_argument('--bootstrapped-apps', action='store_true', help='[Sync] only bootstrapped apps by default')
cmd_parser.add_argument('--print_resources', action='store_true', help='prints resources in json')
cmd_parser.add_argument('--boot', help='defines which app to boot into after reboot')
cmd_parser.add_argument('--run', help='like run, but after a sync')
cmd_parser.add_argument('-w', '--wait', default=0, type=int, help='seconds to wait for USB connected board to become available')
cmd_parser.add_argument('paths', nargs='*', help='input files')
args = cmd_parser.parse_args()
command = args.command[0]
path = sync.get_root()
run_tests = command == "test"
if command not in ["validate"]:
try:
import serial
except Exception as e:
print("Please install pyserial first: https://pyserial.readthedocs.io/en/latest/pyserial.html")
sys.exit(1)
if command == "firmware-update":
firmware_update.firmware_update(args.verbose)
if command == "wifi":
wifi.select_wifi()
if command == "app":
command = "sync"
args.run = "%s/main.py" % args.paths[0]
#args.boot = args.paths[0]
if command in ["test", "validate", "sync", "bootstrap"]:
resources = get_resources(path)
add_metadata(path, resources)
validate(path, resources)
resolve_dependencies(resources)
remove_upip(resources)
if args.print_resources:
print(pretty_print_resources(resources))
errors = get_error_summary(resources)
if errors:
print("Problems found:\n")
print(errors)
sys.exit(1)
print("Local Test: PASS")
if command == "test":
command = "sync"
if len(args.paths) == 0:
print("Please define an app or lib to sync: tilda_tools sync my_app\n")
sys.exit(1)
else:
args.paths = ["lib/test_%s.py" % p for p in args.paths]
if command in ["reset", "sync", "bootstrap"]:
pyboard_util.stop_badge(args, args.verbose)
if command == "bootstrap":
sync.clean(args)
sync.sync(args, ["bootstrap.py"], {}, args.verbose, args.skip_wifi)
pyboard_util.hard_reset(args)
if command == "sync":
paths = args.paths if len(args.paths) else None
if args.bootstrapped_apps:
for k,val in list(resources.items()):
requested = paths and k in paths
bootstrapped = val.get("bootstrapped", False)
if val.get("type", None) == "app":
if not (bootstrapped or (paths and requested)):
# App is not in the bootstrap list, and isn't explicitly requested
if args.verbose:
print("Removing app '{0}' from sync list".format(k))
del resources[k]
if args.clean:
sync.clean(args)
synced_resources = sync.sync(args, paths, resources, args.verbose, args.skip_wifi)
if (command in ["reset", "sync"]) or run_tests:
sync.set_boot_app(args, args.boot or "")
if args.run:
command = "run"
args.paths = [args.run]
sync.set_no_boot(args)
pyboard_util.soft_reset(args)
if command == "run":
pyboard_util.check_run(args.paths)
pyboard_util.run(args, args.paths)
if run_tests:
for resource in synced_resources:
pyboard_util.check_run([resource])
pyboard_util.run(args, [resource], False)
pyboard_util.soft_reset(args, False)
pyboard_util.close_pyb()
if __name__ == "__main__":
main()

View File

@ -1,14 +0,0 @@
#!/usr/bin/env bash
TARGET=$(dirname `pwd`)"/upip"
rm -rf "/tmp/upip.zip"
curl -L "https://github.com/micropython/micropython-lib/archive/master.zip" -o "/tmp/upip.zip"
rm -rf "/tmp/upip"
unzip -q -a "/tmp/upip.zip" -d "/tmp/upip"
cd "/tmp/upip/micropython-lib-master"
rm -rf "$TARGET/*"
for d in `find . -maxdepth 1 -type d ! -name ".*"`; do
echo $d;
find "$d" -maxdepth 1 -mindepth 1 \( -name '*.py' -not -name 'test_*' -not -name 'example_*' -not -name 'setup.py' -size +10c \) -or \( -type d -not -name 'dist' -not -name '*.egg-info' -not -name '__pycache__' \) | xargs -I{} bash -c -- 'ditto {} "'"$TARGET"'/"`echo "{}" | sed -e "s/\.\/[^\/]*\///"`';
done

View File

@ -1,13 +0,0 @@
import os, sync, json
def select_wifi():
ssid = input('Enter wifi name (SSID): ')
pw = input('Enter wifi password, leave empty for open network: ')
with open(os.path.join(sync.get_root(), "wifi.json"), "wt") as file:
if pw:
conn_details = {"ssid": ssid, "pw": pw}
else:
conn_details = {"ssid": ssid}
file.write(json.dumps(conn_details))
print("wifi.json created - It will be transfered to the badge on the next sync")

5
.gitignore vendored
View File

@ -1,6 +1 @@
.DS_Store
__pycache__
wifi*.json
config.json
cmd.exe.lnk
tilda_tools.bat

View File

@ -1,2 +0,0 @@
script:
./tilda_tools validate

View File

@ -1,50 +0,0 @@
# Exported from Wings 3D 1.5.4
mtllib coriolis.mtl
o Cube1
#12 vertices, 14 faces
v -1.00000000 -1.00000000 0.0000000e+0
v -1.00000000 0.0000000e+0 -1.00000000
v 0.0000000e+0 -1.00000000 -1.00000000
v -1.00000000 0.0000000e+0 1.00000000
v 0.0000000e+0 -1.00000000 1.00000000
v 0.0000000e+0 1.00000000 -1.00000000
v -1.00000000 1.00000000 0.0000000e+0
v 0.0000000e+0 1.00000000 1.00000000
v 1.00000000 -1.00000000 0.0000000e+0
v 1.00000000 0.0000000e+0 -1.00000000
v 1.00000000 0.0000000e+0 1.00000000
v 1.00000000 1.00000000 0.0000000e+0
v 0.2 0.1 1.01
v 0.2 -0.1 1.01
v -0.2 -0.1 1.01
v -0.2 0.1 1.01
vn -0.70710678 -0.70710678 0.0000000e+0
vn -0.70710678 0.0000000e+0 -0.70710678
vn 0.0000000e+0 -0.70710678 -0.70710678
vn -0.70710678 0.0000000e+0 0.70710678
vn 0.0000000e+0 -0.70710678 0.70710678
vn 0.0000000e+0 0.70710678 -0.70710678
vn -0.70710678 0.70710678 0.0000000e+0
vn 0.0000000e+0 0.70710678 0.70710678
vn 0.70710678 -0.70710678 0.0000000e+0
vn 0.70710678 0.0000000e+0 -0.70710678
vn 0.70710678 0.0000000e+0 0.70710678
vn 0.70710678 0.70710678 0.0000000e+0
g Cube1_default
usemtl default
s 1
f 1//1 2//2 3//3
f 1//1 4//4 7//7 2//2
f 1//1 5//5 4//4
f 2//2 6//6 10//10 3//3
f 2//2 7//7 6//6
f 3//3 9//9 5//5 1//1
f 3//3 10//10 9//9
f 4//4 8//8 7//7
f 5//5 9//9 11//11
f 5//5 11//11 8//8 4//4
f 6//6 12//12 10//10
f 7//7 8//8 12//12 6//6
f 8//8 11//11 12//12
f 10//10 12//12 11//11 9//9
f 16 15 14 13

View File

@ -1,29 +0,0 @@
# Exported from Wings 3D 1.5.4
mtllib cube.mtl
o Cube1
#8 vertices, 6 faces
v -1.00000000 -1.00000000 -1.00000000
v -1.00000000 -1.00000000 1.00000000
v -1.00000000 1.00000000 -1.00000000
v -1.00000000 1.00000000 1.00000000
v 1.00000000 -1.00000000 -1.00000000
v 1.00000000 -1.00000000 1.00000000
v 1.00000000 1.00000000 -1.00000000
v 1.00000000 1.00000000 1.00000000
vn -0.57735027 -0.57735027 -0.57735027
vn -0.57735027 -0.57735027 0.57735027
vn -0.57735027 0.57735027 -0.57735027
vn -0.57735027 0.57735027 0.57735027
vn 0.57735027 -0.57735027 -0.57735027
vn 0.57735027 -0.57735027 0.57735027
vn 0.57735027 0.57735027 -0.57735027
vn 0.57735027 0.57735027 0.57735027
g Cube1_default
usemtl default
s 1
f 1//1 5//5 6//6 2//2
f 2//2 4//4 3//3 1//1
f 2//2 6//6 8//8 4//4
f 3//3 7//7 5//5 1//1
f 4//4 8//8 7//7 3//3
f 5//5 7//7 8//8 6//6

View File

@ -1,59 +0,0 @@
# Exported from Wings 3D 1.5.4
mtllib dodecahedron.mtl
o dodecahedron1
#20 vertices, 12 faces
v -0.50000000 0.0000000e+0 1.30901699
v 0.50000000 0.0000000e+0 1.30901699
v -0.80901699 -0.80901699 -0.80901699
v -0.80901699 -0.80901699 0.80901699
v -0.80901699 0.80901699 -0.80901699
v -0.80901699 0.80901699 0.80901699
v 0.80901699 -0.80901699 -0.80901699
v 0.80901699 -0.80901699 0.80901699
v 0.80901699 0.80901699 -0.80901699
v 0.80901699 0.80901699 0.80901699
v 1.30901699 0.50000000 0.0000000e+0
v 1.30901699 -0.50000000 0.0000000e+0
v -1.30901699 0.50000000 0.0000000e+0
v -1.30901699 -0.50000000 0.0000000e+0
v -0.50000000 0.0000000e+0 -1.30901699
v 0.50000000 0.0000000e+0 -1.30901699
v 0.0000000e+0 1.30901699 0.50000000
v 0.0000000e+0 1.30901699 -0.50000000
v 0.0000000e+0 -1.30901699 0.50000000
v 0.0000000e+0 -1.30901699 -0.50000000
vn -0.35682209 0.0000000e+0 0.93417236
vn 0.35682209 0.0000000e+0 0.93417236
vn -0.57735027 -0.57735027 -0.57735027
vn -0.57735027 -0.57735027 0.57735027
vn -0.57735027 0.57735027 -0.57735027
vn -0.57735027 0.57735027 0.57735027
vn 0.57735027 -0.57735027 -0.57735027
vn 0.57735027 -0.57735027 0.57735027
vn 0.57735027 0.57735027 -0.57735027
vn 0.57735027 0.57735027 0.57735027
vn 0.93417236 0.35682209 0.0000000e+0
vn 0.93417236 -0.35682209 0.0000000e+0
vn -0.93417236 0.35682209 0.0000000e+0
vn -0.93417236 -0.35682209 0.0000000e+0
vn -0.35682209 0.0000000e+0 -0.93417236
vn 0.35682209 0.0000000e+0 -0.93417236
vn 0.0000000e+0 0.93417236 0.35682209
vn 0.0000000e+0 0.93417236 -0.35682209
vn 0.0000000e+0 -0.93417236 0.35682209
vn 0.0000000e+0 -0.93417236 -0.35682209
g dodecahedron1_default
usemtl default
s 1
f 1//1 4//4 19//19 8//8 2//2
f 1//1 6//6 13//13 14//14 4//4
f 2//2 10//10 17//17 6//6 1//1
f 3//3 20//20 19//19 4//4 14//14
f 5//5 18//18 9//9 16//16 15//15
f 7//7 16//16 9//9 11//11 12//12
f 8//8 12//12 11//11 10//10 2//2
f 9//9 18//18 17//17 10//10 11//11
f 12//12 8//8 19//19 20//20 7//7
f 13//13 6//6 17//17 18//18 5//5
f 14//14 13//13 5//5 15//15 3//3
f 15//15 16//16 7//7 20//20 3//3

View File

@ -1,51 +0,0 @@
# Exported from Wings 3D 1.5.4
mtllib icosahedron.mtl
o icosahedron1
#12 vertices, 20 faces
v 0.0000000e+0 1.90211303 0.0000000e+0
v 1.70130162 0.85065081 0.0000000e+0
v 0.52573111 0.85065081 1.61803399
v -1.37638192 0.85065081 1.00000000
v -1.37638192 0.85065081 -1.00000000
v 0.52573111 0.85065081 -1.61803399
v -1.70130162 -0.85065081 0.0000000e+0
v -0.52573111 -0.85065081 -1.61803399
v 1.37638192 -0.85065081 -1.00000000
v 1.37638192 -0.85065081 1.00000000
v -0.52573111 -0.85065081 1.61803399
v 0.0000000e+0 -1.90211303 0.0000000e+0
vn 2.7942283e-17 1.00000000 -1.3971142e-17
vn 0.89442719 0.44721360 0.0000000e+0
vn 0.27639320 0.44721360 0.85065081
vn -0.72360680 0.44721360 0.52573111
vn -0.72360680 0.44721360 -0.52573111
vn 0.27639320 0.44721360 -0.85065081
vn -0.89442719 -0.44721360 0.0000000e+0
vn -0.27639320 -0.44721360 -0.85065081
vn 0.72360680 -0.44721360 -0.52573111
vn 0.72360680 -0.44721360 0.52573111
vn -0.27639320 -0.44721360 0.85065081
vn -4.1913425e-17 -1.00000000 0.0000000e+0
g icosahedron1_default
usemtl default
s 1
f 1//1 3//3 2//2
f 1//1 4//4 3//3
f 1//1 5//5 4//4
f 1//1 6//6 5//5
f 2//2 6//6 1//1
f 2//2 9//9 6//6
f 2//2 10//10 9//9
f 3//3 10//10 2//2
f 3//3 11//11 10//10
f 4//4 11//11 3//3
f 5//5 7//7 4//4
f 5//5 8//8 7//7
f 6//6 8//8 5//5
f 6//6 9//9 8//8
f 7//7 11//11 4//4
f 7//7 12//12 11//11
f 8//8 12//12 7//7
f 9//9 12//12 8//8
f 10//10 12//12 9//9
f 11//11 12//12 10//10

View File

@ -1,361 +0,0 @@
"""3d rotating polyhedra. 2016 badge competition winner, ported for 2018!"""
___title___ = "3D Spin"
___license___ = "MIT"
___categories___ = ["Demo"]
___dependencies___ = ["app", "ugfx_helper", "sleep", "buttons"]
import ugfx
from tilda import Buttons
import math
from uos import listdir
import time
# from imu import IMU
import gc
# import pyb
import app
app_path = './3dspin'
from math import sqrt
class Vector3D:
def __init__(self, x=0.0, y=0.0, z=0.0):
self.x = x
self.y = y
self.z = z
def magnitude(self):
return sqrt(self.x*self.x+self.y*self.y+self.z*self.z)
def __sub__(self, v):
return Vector3D(self.x-v.x, self.y-v.y, self.z-v.z)
def normalize(self):
mag = self.magnitude()
if (mag > 0.0):
self.x /= mag
self.y /= mag
self.z /= mag
else:
raise Exception('*** Vector: error, normalizing zero vector! ***')
def cross(self, v): #cross product
return Vector3D(self.y*v.z-self.z*v.y, self.z*v.x-self.x*v.z, self.x*v.y-self.y*v.x)
#The layout of the matrix (row- or column-major) matters only when the user reads from or writes to the matrix (indexing). For example in the multiplication function we know that the first components of the Matrix-vectors need to be multiplied by the vector. The memory-layout is not important
class Matrix:
''' Column-major order '''
def __init__(self, createidentity=True):# (2,2) creates a 2*2 Matrix
# if rows < 2 or cols < 2:
# raise Exception('*** Matrix: error, getitem((row, col)), row, col problem! ***')
self.rows = 4
self.cols = 4
self.m = [[0.0]*self.rows for x in range(self.cols)]
#If quadratic matrix then create identity one
if createidentity:
for i in range(self.rows):
self.m[i][i] = 1.0
def mul(self, right):
if isinstance(right, Matrix):
r = Matrix(False)
for i in range(self.rows):
for j in range(right.cols):
for k in range(self.cols):
r.m[i][j] += self.m[i][k]*right.m[k][j]
return r
elif isinstance(right, Vector3D): #Translation: the last column of the matrix. Remains unchanged due to the the fourth coord of the vector (1).
# if self.cols == 4:
r = Vector3D()
addx = addy = addz = 0.0
if self.rows == self.cols == 4:
addx = self.m[0][3]
addy = self.m[1][3]
addz = self.m[2][3]
r.x = self.m[0][0]*right.x+self.m[0][1]*right.y+self.m[0][2]*right.z+addx
r.y = self.m[1][0]*right.x+self.m[1][1]*right.y+self.m[1][2]*right.z+addy
r.z = self.m[2][0]*right.x+self.m[2][1]*right.y+self.m[2][2]*right.z+addz
#In 3D game programming we use homogenous coordinates instead of cartesian ones in case of Vectors in order to be able to use them with a 4*4 Matrix. The 4th coord (w) is not included in the Vector-class but gets computed on the fly
w = self.m[3][0]*right.x+self.m[3][1]*right.y+self.m[3][2]*right.z+self.m[3][3]
if (w != 1 and w != 0):
r.x = r.x/w;
r.y = r.y/w;
r.z = r.z/w;
return r
else:
raise Exception('*** Matrix: error, matrix multiply with not matrix, vector or int or float! ***')
def loadObject(filename):
print(filename)
if (".obj" in filename):
loadObj(filename)
if (".dat" in filename):
loadDat(filename)
def loadDat(filename):
global obj_vertices
global obj_faces
obj_vertices = []
obj_faces = []
f = open(app_path + "/" + filename)
for line in f:
if line[:2] == "v ":
parts = line.split(" ")
obj_vertices.append(
Vector3D(
float(parts[1]),
float(parts[2]),
float(parts[3])
)
)
gc.collect()
elif line[:2] == "f ":
parts = line.split(" ")
face = []
for part in parts[1:]:
face.append(int(part.split("/",1)[0])-1)
obj_faces.append(face)
gc.collect()
f.close()
def loadObj(filename):
global obj_vertices
global obj_faces
obj_vertices = []
obj_faces = []
f = open(app_path + "/" + filename)
for line in f:
if line[:2] == "v ":
parts = line.split(" ")
obj_vertices.append(
Vector3D(
float(parts[1]),
float(parts[2]),
float(parts[3])
)
)
gc.collect()
elif line[:2] == "f ":
parts = line.split(" ")
face = []
for part in parts[1:]:
face.append(int(part.split("/",1)[0])-1)
obj_faces.append(face)
gc.collect()
f.close()
def toScreenCoords(pv):
px = int((pv.x+1)*0.5*240)
py = int((1-(pv.y+1)*0.5)*320)
return [px, py]
def createCameraMatrix(x,y,z):
camera_transform = Matrix()
camera_transform.m[0][3] = x
camera_transform.m[1][3] = y
camera_transform.m[2][3] = z
return camera_transform
def createProjectionMatrix(horizontal_fov, zfar, znear):
s = 1/(math.tan(math.radians(horizontal_fov/2)))
proj = Matrix()
proj.m[0][0] = s * (320/240) # inverse aspect ratio
proj.m[1][1] = s
proj.m[2][2] = -zfar/(zfar-znear)
proj.m[3][2] = -1.0
proj.m[2][3] = -(zfar*znear)/(zfar-znear)
return proj
def createRotationMatrix(x_rotation, y_rotation, z_rotation):
rot_x = Matrix()
rot_x.m[1][1] = rot_x.m[2][2] = math.cos(x_rotation)
rot_x.m[2][1] = math.sin(x_rotation)
rot_x.m[1][2] = -rot_x.m[2][1]
rot_y = Matrix()
rot_y.m[0][0] = rot_y.m[2][2] = math.cos(y_rotation)
rot_y.m[0][2] = math.sin(y_rotation)
rot_y.m[2][0] = -rot_y.m[0][2]
rot_z = Matrix()
rot_z.m[0][0] = rot_z.m[1][1] = math.cos(z_rotation)
rot_z.m[1][0] = math.sin(z_rotation)
rot_z.m[0][1] = -rot_z.m[1][0]
return rot_z.mul(rot_x).mul(rot_y)
def normal(face, vertices, normalize = True):
# Work out the face normal for lighting
normal = (vertices[face[1]]-vertices[face[0]]).cross(vertices[face[2]]-vertices[face[0]])
if normalize == True:
normal.normalize()
return normal
def clear_screen():
# Selectively clear the screen by re-rendering the previous frame in black
global last_polygons
global last_mode
for poly in last_polygons:
if last_mode == FLAT:
ugfx.fill_polygon(0,0, poly, ugfx.BLACK)
ugfx.polygon(0,0, poly, ugfx.BLACK)
def render(mode, rotation):
# Rotate all the vertices in one go
vertices = [rotation.mul(vertex) for vertex in obj_vertices]
# Calculate normal for each face (for lighting)
if mode == FLAT:
face_normal_zs = [normal(face, vertices).z for face in obj_faces]
# Project (with camera) all the vertices in one go as well
vertices = [camera_projection.mul(vertex) for vertex in vertices]
# Calculate projected normals for each face
if mode != WIREFRAME:
proj_normal_zs = [normal(face, vertices, False).z for face in obj_faces]
# Convert to screen coordinates all at once
# We could do this faster by only converting vertices that are
# in faces that will be need rendered, but it's likely that test
# would take longer.
vertices = [toScreenCoords(v) for v in vertices]
# Render the faces to the screen
vsync()
clear_screen()
global last_polygons
global last_mode
last_polygons = []
last_mode = mode
for index in range(len(obj_faces)):
# Only render things facing towards us (unless we're in wireframe mode)
if (mode == WIREFRAME) or (proj_normal_zs[index] > 0):
# Convert polygon
poly = [vertices[v] for v in obj_faces[index]]
# Calculate colour and render
ugcol = ugfx.WHITE
if mode == FLAT:
# Simple lighting calculation
colour5 = int(face_normal_zs[index] * 31)
colour6 = int(face_normal_zs[index] * 63)
# Create a 5-6-5 grey
ugcol = (colour5 << 11) | (colour6 << 5) | colour5
# Render polygon
ugfx.fill_polygon(0,0, poly, ugcol)
# Always draw the wireframe in the same colour to fill gaps left by the
# fill_polygon method
ugfx.polygon(0,0, poly, ugcol)
last_polygons.append(poly)
def vsync():
None
# while(tear.value() == 0):
# pass
# while(tear.value()):
# pass
def calculateRotation(smoothing, accelerometer):
# Keep a list of recent rotations to smooth things out
global x_rotation
global z_rotation
# First, pop off the oldest rotation
# if len(x_rotations) >= smoothing:
# x_rotations = x_rotations[1:]
# if len(z_rotations) >= smoothing:
# z_rotations = z_rotations[1:]
# Now append a new rotation
pi_2 = math.pi / 2
#x_rotations.append(-accelerometer['z'] * pi_2)
#z_rotations.append(accelerometer['x'] * pi_2)
# Calculate rotation matrix
return createRotationMatrix(
# this averaging isn't correct in the first <smoothing> frames, but who cares
math.radians(x_rotation),
math.radians(y_rotation),
math.radians(z_rotation)
)
print("Hello 3DSpin")
# Initialise hardware
ugfx.init()
ugfx.clear(ugfx.BLACK)
# imu=IMU()
# buttons.init()
# Enable tear detection for vsync
# ugfx.enable_tear()
# tear = pyb.Pin("TEAR", pyb.Pin.IN)
#ugfx.set_tear_line(1)
print("Graphics initalised")
# Set up static rendering matrices
camera_transform = createCameraMatrix(0, 0, -5.0)
proj = createProjectionMatrix(45.0, 100.0, 0.1)
camera_projection = proj.mul(camera_transform)
print("Camera initalised")
# Get the list of available objects, and load the first one
obj_vertices = []
obj_faces = []
print("available objects: {}", listdir(app_path))
objects = [x for x in listdir(app_path) if (((".obj" in x) | (".dat" in x)) & (x[0] != "."))]
selected = 0
loadObject(objects[selected])
print("loaded object {}", objects[selected])
# Set up rotation tracking arrays
x_rotation = 0
z_rotation = 0
y_rotation = 0
# Smooth rotations over 5 frames
smoothing = 5
# Rendering modes
BACKFACECULL = 1
FLAT = 2
WIREFRAME = 3
# Start with backface culling mode
mode = BACKFACECULL
last_polygons = []
last_mode = WIREFRAME
# Main loop
run = True
while run:
gc.collect()
# Render the scene
render(
mode,
calculateRotation(smoothing, None)
)
# Button presses
y_rotation += 5
x_rotation += 3
z_rotation += 1
if Buttons.is_pressed(Buttons.JOY_Left):
y_rotation -= 5
if Buttons.is_pressed(Buttons.JOY_Right):
y_rotation += 5
if Buttons.is_pressed(Buttons.JOY_Center):
y_rotation = 0
if Buttons.is_pressed(Buttons.BTN_B):
selected += 1
if selected >= len(objects):
selected = 0
loadObject(objects[selected])
time.sleep_ms(500) # Wait a while to avoid skipping ahead if the user still has the button down
if Buttons.is_pressed(Buttons.BTN_A):
mode += 1
if mode > 3:
mode = 1
time.sleep_ms(500) # Wait a while to avoid skipping ahead if the user still has the button down
if Buttons.is_pressed(Buttons.BTN_Menu):
run = False
app.restart_to_default()

View File

@ -1,27 +0,0 @@
# Exported from Wings 3D 2.0.5
mtllib octohedron.mtl
o octahedron1
#6 vertices, 8 faces
v 2.00000000 0.0000000e+0 0.0000000e+0
v -2.00000000 0.0000000e+0 0.0000000e+0
v 0.0000000e+0 2.00000000 0.0000000e+0
v 0.0000000e+0 -2.00000000 0.0000000e+0
v 0.0000000e+0 0.0000000e+0 2.00000000
v 0.0000000e+0 0.0000000e+0 -2.00000000
vn 1.00000000 0.0000000e+0 0.0000000e+0
vn -1.00000000 0.0000000e+0 0.0000000e+0
vn 0.0000000e+0 1.00000000 0.0000000e+0
vn 0.0000000e+0 -1.00000000 0.0000000e+0
vn 0.0000000e+0 0.0000000e+0 1.00000000
vn 0.0000000e+0 0.0000000e+0 -1.00000000
g octahedron1_default
usemtl default
s 1
f 1//1 5//5 4//4
f 1//1 6//6 3//3
f 2//2 5//5 3//3
f 2//2 6//6 4//4
f 3//3 5//5 1//1
f 3//3 6//6 2//2
f 4//4 5//5 2//2
f 4//4 6//6 1//1

View File

@ -1,81 +0,0 @@
# Exported from Wings 3D 2.0.5
mtllib octoad.mtl
o octotoad1
#24 vertices, 26 faces
v 1.66800000 0.55600000 0.55600000
v 1.66800000 0.55600000 -0.55600000
v 1.66800000 -0.55600000 0.55600000
v 1.66800000 -0.55600000 -0.55600000
v -1.66800000 0.55600000 0.55600000
v -1.66800000 0.55600000 -0.55600000
v -1.66800000 -0.55600000 0.55600000
v -1.66800000 -0.55600000 -0.55600000
v 0.55600000 1.66800000 0.55600000
v 0.55600000 1.66800000 -0.55600000
v 0.55600000 -1.66800000 0.55600000
v 0.55600000 -1.66800000 -0.55600000
v 0.55600000 0.55600000 1.66800000
v 0.55600000 0.55600000 -1.66800000
v 0.55600000 -0.55600000 1.66800000
v 0.55600000 -0.55600000 -1.66800000
v -0.55600000 1.66800000 0.55600000
v -0.55600000 1.66800000 -0.55600000
v -0.55600000 -1.66800000 0.55600000
v -0.55600000 -1.66800000 -0.55600000
v -0.55600000 0.55600000 1.66800000
v -0.55600000 0.55600000 -1.66800000
v -0.55600000 -0.55600000 1.66800000
v -0.55600000 -0.55600000 -1.66800000
vn 0.85476344 0.36700100 0.36700100
vn 0.85476344 0.36700100 -0.36700100
vn 0.85476344 -0.36700100 0.36700100
vn 0.85476344 -0.36700100 -0.36700100
vn -0.85476344 0.36700100 0.36700100
vn -0.85476344 0.36700100 -0.36700100
vn -0.85476344 -0.36700100 0.36700100
vn -0.85476344 -0.36700100 -0.36700100
vn 0.36700100 0.85476344 0.36700100
vn 0.36700100 0.85476344 -0.36700100
vn 0.36700100 -0.85476344 0.36700100
vn 0.36700100 -0.85476344 -0.36700100
vn 0.36700100 0.36700100 0.85476344
vn 0.36700100 0.36700100 -0.85476344
vn 0.36700100 -0.36700100 0.85476344
vn 0.36700100 -0.36700100 -0.85476344
vn -0.36700100 0.85476344 0.36700100
vn -0.36700100 0.85476344 -0.36700100
vn -0.36700100 -0.85476344 0.36700100
vn -0.36700100 -0.85476344 -0.36700100
vn -0.36700100 0.36700100 0.85476344
vn -0.36700100 0.36700100 -0.85476344
vn -0.36700100 -0.36700100 0.85476344
vn -0.36700100 -0.36700100 -0.85476344
g octotoad1_default
usemtl default
s 1
f 1//1 3//3 4//4 2//2
f 1//1 13//13 15//15 3//3
f 2//2 10//10 9//9 1//1
f 2//2 14//14 10//10
f 3//3 11//11 12//12 4//4
f 3//3 15//15 11//11
f 4//4 16//16 14//14 2//2
f 5//5 17//17 18//18 6//6
f 5//5 21//21 17//17
f 6//6 8//8 7//7 5//5
f 6//6 22//22 24//24 8//8
f 7//7 23//23 21//21 5//5
f 8//8 20//20 19//19 7//7
f 8//8 24//24 20//20
f 9//9 13//13 1//1
f 9//9 17//17 21//21 13//13
f 10//10 18//18 17//17 9//9
f 11//11 19//19 20//20 12//12
f 12//12 16//16 4//4
f 12//12 20//20 24//24 16//16
f 13//13 21//21 23//23 15//15
f 14//14 22//22 18//18 10//10
f 15//15 23//23 19//19 11//11
f 16//16 24//24 22//22 14//14
f 18//18 22//22 6//6
f 19//19 23//23 7//7

View File

@ -1,19 +0,0 @@
# Exported from Wings 3D 1.5.4
mtllib tetrahedron.mtl
o tetrahedron1
#4 vertices, 4 faces
v 0.0000000e+0 1.08866211 0.0000000e+0
v 0.0000000e+0 -0.54433105 1.15470054
v -1.00000000 -0.54433105 -0.57735027
v 1.00000000 -0.54433105 -0.57735027
vn 0.0000000e+0 1.00000000 -1.1102230e-16
vn 0.0000000e+0 -0.33333333 0.94280904
vn -0.81649658 -0.33333333 -0.47140452
vn 0.81649658 -0.33333333 -0.47140452
g tetrahedron1_default
usemtl default
s 1
f 1//1 3//3 2//2
f 1//1 4//4 3//3
f 2//2 4//4 1//1
f 3//3 4//4 2//2

View File

@ -1,67 +0,0 @@
# Rules for apps in the official badge store
See [Rules for apps in the official badge store](https://badge.emfcamp.org/wiki/TiLDA_MK4/Badge_Store_Submissions#Rules_for_apps_in_the_official_badge_store)
# Packaging up your badge app
See [Packaging up your badge app for submission to the store](https://badge.emfcamp.org/wiki/TiLDA_MK4/Badge_Store_Submissions#Packaging_up_your_badge_app_for_submission_to_the_store)
# Using git and GitHub to submit your badge app
Please ensure:
1. You have a [GitHub account](https://github.com/join)
2. You have [git installed](https://git-scm.com/downloads) on your local computer
3. You have written a badge app [packaged it up](https://badge.emfcamp.org/wiki/TiLDA_MK4/Badge_Store_Submissions#Packaging_up_your_badge_app_for_submission_to_the_store) and [validated](https://badge.emfcamp.org/wiki/TiLDA_MK4/Badge_Store_Submissions#Packaging_up_your_badge_app_for_submission_to_the_store) it
These instructions are tailored around submitting your Badge App but the general principle can be used to raise any pull request.
Please keep pull requests focused on one thing only (like submission of your app), since this makes it easier to merge and test
in a timely manner.
## Setting up your git username and email address
Using the command line/terminal on your computer type:
```
git config --global user.name “username”
git config --global user.email “username@users.noreply.github.com”
```
Note that the username is the username you use to log into GitHub, not your profile “Name”
## Main flow for contributing
It's important to follow these steps to make changes to your own copy of the emfcamp repo and then raise a pull request that will be merged into the emfcamp repo.
1. Login to GitHub, go to the [emfcamp/Mk4-Apps](https://github.com/emfcamp/Mk4-Apps) repository and click ```Fork``` in
the top right
2. Using the command line/terminal on your computer type: `git clone <url to YOUR fork>`
3. `cd Mk4-Apps`
4. `git checkout master`
5. `git checkout -b my-app-name` to create a branch with your apps name
6. Copy your app files in their uniquely named folder into the Mk4-Apps/ directory (repo root directory)
7. `git add .` to add all of your app files and directory to your local repo
8. `git commit -m "my-app-name badge app"` note that you can put any message in the quotes
9. `git push origin my-app-name` to update *your* GitHub fork with the change
10. Create pull request using the GitHub UI to merge your changes from your new branch into `emfcamp/Mk4-Apps/master`
11. Repeat from step 4 for new other changes.
The primary thing to remember is that separate pull requests should be created for separate branches. Never create a pull request from your `master` branch.
Once you have created the pull request, every new commit/push in your branch will propagate from your
fork into the pull reqests in the main github/emfcamp/Mk4-Apps repo.
## Updating your GitHub and local git repo
Later, you can get the changes from the emfcamp/Mk4-Apps repo into your `master` branch by adding emfcamp as a git remote and
merging from it as follows:
1. `git remote add emfcamp https://github.com/emfcamp/Mk4-Apps.git`
2. `git checkout master`
3. `git fetch emfcamp`
4. `git merge emfcamp/master` will update your local repo
5. `git push origin master` will update your fork on GitHub
## Useful links
The GitHub workflow: https://guides.github.com/introduction/flow/index.html
If you need help with pull requests there are guides on GitHub here: https://help.github.com/articles/creating-a-pull-request/

View File

@ -1,82 +0,0 @@
"""DevRant Client for TiLDA-MK4
"""
___name___ = "DevRant"
___license___ = "MIT"
___dependencies___ = ["app", "wifi", "http", "ugfx_helper"]
___categories___ = ["Other"]
___launchable___ = True
import ugfx, wifi, http, json, utime, ugfx_helper, dialogs, app
char_ln = 25
ln_pg = 19
def loop():
skip = 0
while True:
ugfx.clear(ugfx.html_color(0x544c6d))
data= json.loads(http.get("https://devrant.com/api/devrant/rants?app=3&sort=top&range=day&limit=1&skip="+str(skip)).raise_for_status().content)["rants"][0]
text=data["text"].split(" ")
screens = [[]]
line = ""
screen = 0
for word in text:
if len(line+word)+1 >= char_ln:
if len(screens[screen]) >= ln_pg:
screen+=1
screens.append([])
screens[screen].append(line)
line=word
else:
line = line + " " + word
if len(screens[screen]) < ln_pg:
screens[screen].append(line)
else:
screens.append([line])
hold=True
page = 0
while hold:
ugfx.clear(ugfx.html_color(0x544c6d))
ugfx.area(0,0,240,35,ugfx.html_color(0x41476d))
ugfx.text(5,5,str(data["score"])+"++ " + data["user_username"] + ":",ugfx.BLACK)
ugfx.text(5,20,"Page: " + str(page+1) + "/" + str(len(screens)),ugfx.BLACK)
count = 0
for line in screens[page]:
ugfx.text(5,35+count*15,line,ugfx.BLACK)
count+=1
hold_btn = True
while hold_btn:
if tilda.Buttons.is_pressed(tilda.Buttons.BTN_Menu):
return
if tilda.Buttons.is_pressed(tilda.Buttons.BTN_A):
skip += 1
hold_btn = False
hold = False
while tilda.Buttons.is_pressed(tilda.Buttons.BTN_A):
utime.sleep_ms(10)
if tilda.Buttons.is_pressed(tilda.Buttons.JOY_Right):
if page < len(screens)-1:
page += 1
hold_btn = False
while tilda.Buttons.is_pressed(tilda.Buttons.JOY_Right):
utime.sleep_ms(10)
if tilda.Buttons.is_pressed(tilda.Buttons.JOY_Left):
if page > 0:
page -= 1
hold_btn = False
while tilda.Buttons.is_pressed(tilda.Buttons.JOY_Left):
utime.sleep_ms(10)
ugfx_helper.init()
ugfx.clear()
ugfx.text(5,5, "DevRant for the TiLDA Mk4", ugfx.BLACK)
ugfx.text(5, 40, "Connecting To WIFI", ugfx.BLACK)
wifi.connect()
ugfx.text(5, 40, "Connecting To WIFI", ugfx.WHITE)
loop()
app.restart_to_default()

File diff suppressed because one or more lines are too long

18
README
View File

@ -1,17 +1,3 @@
TiLDA Mk4 App Library
-------------------
This is a special branch used for storing static content that you want to access from the badge.
Check out the wiki: https://badge.emfcamp.org/wiki/TiLDA_MK4
Please use one of these categories:
* System
* Homescreens
* Games
* Sound
* EMF
* Villages
* Phone
* LEDs
* Sensors
* Demo
* Other
Stuff stored in here is explicitly excempt from the "no sideloading" rule for badge apps since it goes through the same approval process.

View File

@ -1,180 +0,0 @@
"""Accidentally created etcher sketch...\nThen made it awesome"""
___name___ = "Sketchy-Etch"
___title___ = "Sketchy-Etch"
___license___ = "MIT"
___dependencies___ = ["ugfx_helper", "dialogs"]
___categories___ = ["Games"]
import ugfx, ugfx_helper, app, dialogs
from tilda import Buttons
from time import sleep
def reset():
global i
global j
global maxHeight
i = int(ugfx.width() / 2)
j = int(maxHeight / 2)
ugfx.area(0, 0, ugfx.width(), maxHeight, ugfx.BLACK)
ugfx.area((i - 1) if i > 0 else 0, (j - 1) if j > 0 else 0, 3 if (i > 0 and i < (ugfx.width() - 1)) else 2, 3 if (j > 0 and j < (maxHeight - 1)) else 2, ugfx.GREY)
def getColour(intensity, angle):
intensity *= 2
if angle < (1 / 6):
return (intensity, intensity * (angle * 6), 0) if intensity < 1 else (1, (angle * 6) + ((1 - (angle * 6)) * (intensity - 1)), (intensity - 1))
elif angle < (2 / 6):
return (intensity * (2 - (6 * angle)), intensity, 0) if intensity < 1 else ((2 - (6 * angle)) + ((1 - (2 - (6 * angle))) * (intensity - 1)), 1, (intensity - 1))
elif angle < (3 / 6):
return (0, intensity, intensity * ((6 * angle) - 2)) if intensity < 1 else ((intensity - 1), 1, ((6 * angle) - 2) + ((1 - ((6 * angle) - 2)) * (intensity - 1)))
elif angle < (4 / 6):
return (0, intensity * (4 - (6 * angle)), intensity) if intensity < 1 else ((intensity - 1), (4 - (6 * angle)) + ((1 - (4 - (6 * angle))) * (intensity - 1)), 1)
elif angle < (5 / 6):
return (intensity * ((6 * angle) - 4), 0, intensity) if intensity < 1 else (((6 * angle) - 4) + ((1 - ((6 * angle) - 4)) * (intensity - 1)), (intensity - 1), 1)
else:
return (intensity, 0, intensity * 6 * (1 - angle)) if intensity < 1 else (1, (intensity - 1), (6 * (1 - angle)) + ((1 - (6 * (1 - angle))) * (intensity - 1)))
shades = 16
hues = 20
scroll = 0
huesToShow = 2
colourI = 0
colourJ = 0
def showColourChangeMenu():
global shades
global hues
global scroll
global huesToShow
global maxHeight
boxHeight = int((ugfx.height() - maxHeight) / huesToShow)
boxWidth = int(ugfx.width() / shades)
for x in range(shades):
for y in range(scroll, scroll + huesToShow):
(r, g, b) = getColour(x / shades, y / hues)
ugfx.area(x * boxWidth, maxHeight + int((y - scroll) * boxHeight), boxWidth, boxHeight, (int(31 * r) << 11) + (int(63 * g) << 5) + int(31 * b))
def selectColour():
global shades
global hues
global scroll
global huesToShow
global colourI
global colourJ
global maxHeight
boxHeight = int((ugfx.height() - maxHeight) / huesToShow)
boxWidth = int(ugfx.width() / shades)
(r, g, b) = getColour(colourI / shades, colourJ / hues)
ugfx.box(colourI * boxWidth, maxHeight + ((colourJ - scroll) * boxHeight), boxWidth, boxHeight, (int(31 * (1 - r)) << 11) + (int(63 * (1 - g)) << 5) + int(31 * (1 - b)))
while not Buttons.is_pressed(Buttons.JOY_Center):
positionChanged = False
scrollChanged = False
oldI = colourI
oldJ = colourJ
if Buttons.is_pressed(Buttons.JOY_Right) and (colourI < (shades - 1)):
colourI += 1
positionChanged = True
while Buttons.is_pressed(Buttons.JOY_Right):
pass
elif Buttons.is_pressed(Buttons.JOY_Left) and (colourI > 0):
colourI -= 1
positionChanged = True
while Buttons.is_pressed(Buttons.JOY_Left):
pass
if Buttons.is_pressed(Buttons.JOY_Down) and (colourJ < (hues - 1)):
if (colourJ - scroll) == 1:
scroll += 1
scrollChanged = True
colourJ += 1
positionChanged = True
while Buttons.is_pressed(Buttons.JOY_Down):
pass
elif Buttons.is_pressed(Buttons.JOY_Up) and (colourJ > 0):
if (colourJ - scroll) == 0:
scroll -= 1
scrollChanged = True
colourJ -= 1
positionChanged = True
while Buttons.is_pressed(Buttons.JOY_Up):
pass
if scrollChanged or positionChanged:
if scrollChanged:
showColourChangeMenu()
elif positionChanged:
(r, g, b) = getColour(oldI / shades, oldJ / hues)
ugfx.box(oldI * boxWidth, maxHeight + ((oldJ - scroll) * boxHeight), boxWidth, boxHeight, (int(31 * r) << 11) + (int(63 * g) << 5) + int(31 * b))
(r, g, b) = getColour(colourI / shades, colourJ / hues)
ugfx.box(colourI * boxWidth, maxHeight + ((colourJ - scroll) * boxHeight), boxWidth, boxHeight, (int(31 * (1 - r)) << 11) + (int(63 * (1 - g)) << 5) + int(31 * (1 - b)))
sleep(0.05)
while Buttons.is_pressed(Buttons.JOY_Center):
pass
(r, g, b) = getColour(colourI / shades, colourJ / hues)
ugfx.box(colourI * boxWidth, maxHeight + ((colourJ - scroll) * boxHeight), boxWidth, boxHeight, (int(31 * r) << 11) + (int(63 * g) << 5) + int(31 * b))
return (int(31 * r) << 11) + (int(63 * g) << 5) + int(31 * b)
ugfx_helper.init()
maxHeight = int(ugfx.height() * 0.9)
i = 0
j = 0
ugfx.clear()
dialogs.notice("Draw with joystick arrows\nHold joystick centre for circle\nA to clear\nMENU to choose colour\nB to exit", title="Sketchy-Etch")
ugfx.area(0, 0, ugfx.width(), maxHeight, ugfx.BLACK)
showColourChangeMenu()
circleSize = 3
reset()
colour = ugfx.WHITE
while not Buttons.is_pressed(Buttons.BTN_B):
changed = False
oldI = i
oldJ = j
if Buttons.is_pressed(Buttons.JOY_Right) and (i < (ugfx.width() - 1)):
i += 1
changed = True
elif Buttons.is_pressed(Buttons.JOY_Left) and (i > 0):
i -= 1
changed = True
if Buttons.is_pressed(Buttons.JOY_Down) and (j < (maxHeight - 1)):
j += 1
changed = True
elif Buttons.is_pressed(Buttons.JOY_Up) and (j > 0):
j -= 1
changed = True
if Buttons.is_pressed(Buttons.JOY_Center):
circleSize += 1
ugfx.fill_circle(i, j, circleSize, colour)
showColourChangeMenu()
if Buttons.is_pressed(Buttons.BTN_A):
circleSize = 3
reset()
if Buttons.is_pressed(Buttons.BTN_Menu):
colour = selectColour()
circleSize = 3
if changed:
circleSize = 3
ugfx.area((oldI - 1) if oldI > 0 else 0, (oldJ - 1) if oldJ > 0 else 0, 3 if (oldI > 0 and oldI < (ugfx.width() - 1)) else 2, 3 if (oldJ > 0 and oldJ < (maxHeight - 1)) else 2, colour)
ugfx.area((i - 1) if i > 0 else 0, (j - 1) if j > 0 else 0, 3 if (i > 0 and i < (ugfx.width() - 1)) else 2, 3 if (j > 0 and j < (maxHeight - 1)) else 2, ugfx.GREY)
sleep(0.05)
ugfx.clear()
app.restart_to_default()

View File

@ -1,8 +0,0 @@
# Fancy a quick experiment? No problem, just code it in here and run
# "./tilda_tools run adhoc.py"
import ugfx, ugfx_helper, dialogs
ugfx_helper.init()
ugfx.clear()

View File

@ -1,198 +0,0 @@
"""This app needs an SDS011 sensor attacthed to UART 4 """
___name___ = "Air Quality"
___license___ = "MIT"
___dependencies___ = ["sleep", "app", "ugfx_helper", "buttons", "homescreen"]
___categories___ = ["EMF"]
___bootstrapped___ = False # Whether or not apps get downloaded on first install. Defaults to "False", mostly likely you won't have to use this at all.
import app
import ugfx, os, time, sleep
from tilda import Buttons
from tilda import Sensors
from machine import Pin
from machine import UART
from machine import Neopix
import random
class DustSensorTester(object):
verbose = False
def contains_sequence (self,data, test):
""" Checks to see if the data sequence contains the test sewquence
Args:
data: sequence of data items
test: test sequence
Returns:
True if the test sequence is in the data sequence
"""
if len(data)<len(test): return False
for pos in range(0,len(data)-len(test)+1):
if test == data[pos:pos+len(test)]: return True
return False
class DustSensor(object):
verbose = False
fake_sensor = False
AWAITING_START = 0
READING_BLOCK = 1
def pump_byte(self, b):
""" Pump a byte into the block decode. Calls the decode method
when the block is complete
Args:
b: byte to pump
"""
if self.state==self.AWAITING_START:
if self.verbose: print(self, "Awaiting start:", b)
if b==self.start_sequence[self.start_pos]:
# got a match - move to next byte in start sequence
self.start_pos = self.start_pos+1
if self.start_pos == len(self.start_sequence):
# matched the start sequence
self.block=self.start_sequence.copy()
self.state=self.READING_BLOCK
elif self.state==self.READING_BLOCK:
if self.verbose: print("Reading block:", b)
self.block.append(b)
if len(self.block) == self.block_size:
self.state=self.AWAITING_START
self.start_pos=0
self.process_block()
def __init__(self, display):
self.display = display
self.state=self.AWAITING_START
self.start_pos = 0
class sds011_sensor(DustSensor):
start_sequence = [0xaa,0xc0]
block_size = 10
def process_block(self):
""" Process a block of data obtained from the sensor
calls the new_reading method on the display to
deliver a new reading or the error method
on the display to indicate an error
"""
if self.verbose: print("sds011 process block")
if self.verbose: print([hex(x) for x in self.block])
check_sum = 0
for i in range(2,8):
check_sum = check_sum + self.block[i]
check_sum = check_sum & 0xff
if self.verbose: print("Checksum:",hex(check_sum))
if check_sum!=self.block[8]:
message = "Rcv:" + hex(self.block[8]) + " Cal:" + hex(check_sum)
self.display.error(message)
return
ppm10 = (self.block[4]+256*self.block[5])/10
ppm2_5 = (self.block[2]+256*self.block[3])/10
self.display.new_readings(ppm10,ppm2_5)
class Air_Quality_Display():
def setup_screen(self):
""" Set up the screen and the labels that display
values on it.
"""
ugfx.init()
width=ugfx.width()
height=ugfx.height()
ugfx.clear(ugfx.html_color(0x800080))
style = ugfx.Style()
style.set_enabled([ugfx.WHITE, ugfx.html_color(0x800080), ugfx.html_color(0x800080), ugfx.html_color(0x800080)])
style.set_background(ugfx.html_color(0x800080))
ugfx.set_default_style(style)
ugfx.orientation(90)
ugfx.set_default_font(ugfx.FONT_TITLE)
ugfx.Label(0, 0, width, 60,"Air Quality", justification=ugfx.Label.CENTER)
label_height=45
self.ppm10_label = ugfx.Label(0, label_height, width, label_height,"PPM 10: starting", justification=ugfx.Label.CENTER)
self.ppm25_label = ugfx.Label(0, label_height*2, width, label_height,"PPM 2.5: starting", justification=ugfx.Label.CENTER)
self.temp_label = ugfx.Label(0, label_height*3, width, label_height,"Temp: starting", justification=ugfx.Label.CENTER)
self.humid_label = ugfx.Label(0, label_height*4, width, label_height,"Humid: starting", justification=ugfx.Label.CENTER)
self.error_label = ugfx.Label(0, label_height*5, width, label_height,"", justification=ugfx.Label.CENTER)
self.message_label = ugfx.Label(0, label_height*6, width, label_height,"", justification=ugfx.Label.CENTER)
self.error_count = 0
self.error_message = ""
self.neopix = Neopix()
self.p10_decode = ((100,0x00ff00),(250,0xffff00),(350,0xff8000),(430,0xff0000),(-1,0xcc6600))
self.p25_decode = ((60, 0x00ff00),(91, 0xffff00), (121,0xff8000),(251,0xff0000),(-1,0xcc6600))
def get_reading_color(self, value, decode):
for item in decode:
if item[0] < 0:
# reached the upper limit - return
return item[1]
if value < item[0]:
return item[1]
def new_readings(self,ppm10_value, ppm25_value):
""" Called by the sensor to deliver new values to the screen.
Will also trigger the reading of the temperature and humidity
values.
"""
self.ppm10_label.text("PPM 10: "+str(ppm10_value))
self.ppm25_label.text("PPM 2.5: "+str(ppm25_value))
temp = Sensors.get_hdc_temperature()
temp_string = "Temp: {0:2.1f}".format(temp)
self.temp_label.text(temp_string)
humid = Sensors.get_hdc_humidity()
humid_string = "Humidity: {0:2.1f}".format(humid)
self.humid_label.text(humid_string)
# Calculate some colours
self.neopix.display((self.get_reading_color(ppm25_value, self.p25_decode),self.get_reading_color(ppm10_value, self.p10_decode)))
def error(self, error_message):
""" Called by the sensor to deliver an error message.
Args:
error_message: error message string
"""
self.error_count = self.error_count + 1
self.error_label.text( "Errors: " +str(self.error_count))
self.message_label.text(str(error_message))
display = Air_Quality_Display()
display.setup_screen()
sensor_port = UART(2,9600, bits=8, mode=UART.BINARY, parity=None, stop=1)
sensor = sds011_sensor(display)
def test_sensor():
""" Can be called to pump some test sequences into the sensor
"""
test_sequences = [ ['0xaa', '0xc0', '0xf', '0x0', '0x22', '0x0', '0xe1', '0xdb', '0xed', '0xab'],
['0xaa', '0xc0', '0x13', '0x0', '0x3e', '0x0', '0xe1', '0xdb', '0xb', '0xab'], # bad checksum
['0xaa', '0xc0', '0x13', '0x0', '0x3e', '0x0', '0xe1', '0xdb', '0xa', '0xab'],
['0xaa', '0xc0', '0x13', '0x0', '0x3e', '0x0', '0xe1', '0xdb', '0xd', '0xab'] ]
for test_sequence in test_sequences:
for ch in test_sequence:
sensor.pump_byte(int(ch))
# test_sensor()
buffer = bytearray([0])
while (not Buttons.is_pressed(Buttons.BTN_A)) and (not Buttons.is_pressed(Buttons.BTN_B)) and (not Buttons.is_pressed(Buttons.BTN_Menu)):
while sensor_port.any() > 0:
sensor_port.readinto(buffer,1)
sensor.pump_byte(buffer[0])
sleep.wfi()
ugfx.clear()
app.restart_to_default()

View File

@ -1,160 +0,0 @@
"""A simple homescreen diplaying an avatar from an url and the user's name"""
___title___ = "Avatar Homescreen"
___license___ = "WTFPL"
___categories___ = ["Homescreens"]
___dependencies___ = ["homescreen", "wifi", "http", "sleep", "app", "buttons"]
___bootstrapped___ = False
___launchable___ = True
import ugfx_helper, uos, wifi, ugfx, http, time, sleep, app, sys, database, buttons
from tilda import Buttons
from homescreen import *
from dialogs import *
# Constants
intro_height = 30
name_height = 60
status_height = 20
info_height = 30
max_name = 8
avatar_file_name='shared/avatar.png'
avatar_db_key="avatar_url"
# Local variables
db = database.Database()
### START OF WRITING STUFF ###
def write_instructions():
ugfx.clear(ugfx.html_color(0x000000))
ugfx.orientation(270)
ugfx.text(5, 5, "Press A to refresh", ugfx.WHITE)
ugfx.text(5, 25, "Press B to change the url", ugfx.WHITE)
ugfx.text(5, 45, "Press Menu to exit", ugfx.WHITE)
def write_hot_instructions():
ugfx.orientation(270)
ugfx.text(3, 85, "Press A to refresh or press B", ugfx.WHITE)
ugfx.text(3, 105, "to change the url or check", ugfx.WHITE)
ugfx.text(3, 125, "your wifi settings...", ugfx.WHITE)
def write_loading():
ugfx.clear(ugfx.html_color(0x000000))
ugfx.orientation(90)
ugfx.text(5, 5, "Loading...", ugfx.WHITE)
ugfx.orientation(270)
ugfx.text(5, 5, "Loading...", ugfx.WHITE)
def write_name():
name_setting = name("Set your name in the settings app")
if len(name_setting) <= max_name:
ugfx.set_default_font(ugfx.FONT_NAME)
else:
ugfx.set_default_font(ugfx.FONT_MEDIUM_BOLD)
# Draw name
ugfx.orientation(90)
ugfx.Label(0, ugfx.height() - name_height, ugfx.width(), name_height, name_setting, justification=ugfx.Label.CENTER, style=style)
### END OF WRITING STUFF ###
### START OF AVATAR HANDLING STUFF ###
def avatar_exists():
ret = True
try:
f = open(avatar_file_name, 'r')
except:
ret = False
return ret
def load_avatar():
#Load the avatar from the local storage
try:
f = open(avatar_file_name, 'r')
avatar_file = f.read()
ugfx.orientation(90)
ugfx.display_image(0,0,bytearray(avatar_file))
f.close()
return True
except:
ugfx.clear(ugfx.html_color(0x000000))
ugfx.orientation(270)
ugfx.text(3, 65, "No local avatar.", ugfx.RED)
return False
def download_avatar():
avatar_url=db.get("avatar_url", "")
if avatar_url:
if (avatar_url.endswith(".png") or avatar_url.startswith("http")):
try:
image = http.get(avatar_url).raise_for_status().content
ugfx.orientation(90)
ugfx.display_image(0,0,bytearray(image))
#f = open(avatar_file_name, 'w')
#f.write(image)
#f.close()
#ugfx.display_image(0,0,bytearray(image))
except:
ugfx.clear(ugfx.html_color(0x000000))
ugfx.orientation(270)
ugfx.text(3, 65, "Couldn't download the avatar.", ugfx.RED)
return False
else:
ugfx.clear(ugfx.html_color(0x000000))
ugfx.orientation(270)
ugfx.text(3, 65, "Invalid avatar url.", ugfx.RED)
return False
else:
ugfx.clear(ugfx.html_color(0x000000))
ugfx.orientation(270)
ugfx.text(3, 65, "No avatar url.", ugfx.RED)
return True
### END OF AVATAR HANDLING STUFF ###
### START OF MAIN ###
def start():
write_name()
#if not avatar_exists():
if not download_avatar():
write_hot_instructions()
#if not load_avatar():
#write_hot_instructions()
init()
ugfx.clear(ugfx.html_color(0x000000))
style = ugfx.Style()
style.set_enabled([ugfx.WHITE, ugfx.html_color(0x000000), ugfx.html_color(0x000000), ugfx.html_color(0x000000)])
style.set_background(ugfx.html_color(0x000000))
ugfx.set_default_style(style)
write_instructions()
wait_until = time.ticks_ms() + 3000
while time.ticks_ms() < wait_until:
time.sleep(0.1)
if Buttons.is_pressed(Buttons.BTN_A) or Buttons.is_pressed(Buttons.BTN_B) or Buttons.is_pressed(Buttons.BTN_Menu):
break
start()
while True:
if buttons.is_triggered(Buttons.BTN_B):
ugfx.orientation(270)
avatar_url = prompt_text("Avatar url:", init_text=db.get(avatar_db_key, ""))
db.set(avatar_db_key, avatar_url)
db.flush()
ugfx.orientation(90)
start()
if buttons.is_triggered(Buttons.BTN_Menu):
break
app.restart_to_default()
### END OF MAIN ###

View File

@ -1,134 +0,0 @@
"""Official TiLDA MK4 Badge Store App
switches between app libraries, updates and installs apps.
To publish apps use https://badge.emfcamp.org"""
___license___ = "MIT"
___title___ = "Badge Store"
___dependencies___ = ["badge_store", "dialogs", "ugfx_helper", "app", "database", "ospath"]
___categories___ = ["System"]
___bootstrapped___ = True
import ugfx_helper, os, database, wifi, app, ospath
from dialogs import *
from lib.badge_store import BadgeStore
from app import *
### VIEWS ###
ugfx_helper.init()
url = database.get("badge_store.url", "http://badgeserver.emfcamp.org/2018")
repo = database.get("badge_store.repo", "emfcamp/Mk4-Apps")
ref = database.get("badge_store.ref", "master")
store = BadgeStore(url=url, repo=repo, ref=ref)
title = "TiLDA Badge Store"
def clear():
ugfx.clear()
def show_categories():
clear()
with WaitingMessage(title=title, text="Loading categories..."):
menu_items = [{"title": c, "category": c} for c in store.get_categories()]
option = prompt_option(menu_items, none_text="Back", title="Install: Categories")
if option:
show_apps(option["category"])
else:
return
def show_apps(c):
clear()
menu_items = [{"title": a, "app": a} for a in store.get_apps(c)]
option = prompt_option(menu_items, none_text="Back", title="Install: " + c)
if option:
show_app(option["app"],c)
else:
show_categories()
def show_app(a,c):
clear()
with WaitingMessage(title=title, text="Loading app description..."):
app_info = store.get_app(a)
# Try to get the 'title' key from app_info, falling back to the value of a if not present
name = app_info.get("title", a)
desc = app_info["description"].strip()
app_text = """App:\n{}\n\nDescription:\n{}""".format(name, desc)
install = prompt_boolean(app_text , title="Install App", true_text="Install", false_text="Back")
if install:
app_text = "App:\n{}\n\n".format(name)
with WaitingMessage(title="Installing App...", text="%sGetting ready..." % app_text) as message:
installers = store.install([a])
n = len(installers)
for i, installer in enumerate(installers):
message.text = "%s%s (%s/%s)" % (app_text + "Downloading files...\n\n", installer.path, i + 1, n)
installer.download()
app.uncache_apps()
launch = prompt_boolean(
"%sSuccessfully installed.\n\nPress A to launch the app.\n\nPress B to list more \"%s\" apps." % (app_text, c), title="Install Success!", true_text="Launch", false_text="Back")
if (launch):
for app_obj in get_apps():
if app_obj.name == a:
app_obj.boot()
else:
show_apps(c)
else:
show_apps(c)
def show_update():
clear()
update = prompt_boolean("Do you want to update all apps on this badge?", title="Update all Apps", true_text="OK", false_text="Back")
if update:
clear()
with WaitingMessage(title=title, text="Getting updates...") as message:
update_text = "Downloading files:"
installers = store.install(_get_current_apps())
n = len(installers)
for i, installer in enumerate(installers):
message.text = "%s\n\n%s (%s/%s)" % (update_text, installer.path, i + 1, n)
installer.download()
notice("Your badge has been successfully updated.", title="Update Success!", close_text="Back")
def show_remove():
clear()
app_to_remove = prompt_option(_get_current_apps(), title="Remove App...", none_text="Back", text="Select an App to remove.")
if app_to_remove:
ospath.recursive_rmdir(app_to_remove)
app.uncache_apps()
app_text = """App:\n{}""".format(app_to_remove)
notice("\"%s\"\n\nThe app has now been removed." % app_text, title="Remove Success!", close_text="Back")
def main_menu():
while True:
clear()
menu_items = [
{"title": "Install Apps", "function": show_categories},
{"title": "Update all Apps", "function": show_update},
{"title": "Remove App", "function": show_remove}
]
option = prompt_option(menu_items, none_text="Exit", text="What do you want to do?", title=title)
if option:
option["function"]()
else:
break
def _get_current_apps():
return [a.name for a in app.get_apps()]
wifi.connect(show_wait_message=True)
main_menu()
app.restart_to_default()

View File

@ -1,25 +0,0 @@
"""This app creates a real EMF badge experience"""
___title___ = "EMF 2018 badge simulator"
___license___ = "MIT"
___categories___ = ["EMF"]
___dependencies___ = ["sleep", "app"]
import ugfx, app
from time import sleep
from tilda import Buttons
ugfx.init()
ugfx.clear()
ugfx.set_default_font(ugfx.FONT_MEDIUM_BOLD)
ugfx.Label(10, 10, 240, 15, "EMF2018")
ugfx.Label(10, 40, 240, 15, "TiLDA Mk4")
ugfx.Label(10, 80, 240, 15, "Error")
ugfx.Label(10, 110, 240, 15, "Something went wrong :(")
while (not Buttons.is_pressed(Buttons.BTN_A)) and (not Buttons.is_pressed(Buttons.BTN_B)) and (not Buttons.is_pressed(Buttons.BTN_Menu)):
sleep(2)
ugfx.clear()
app.restart_to_default()

View File

@ -1,47 +0,0 @@
"""An NTP time app"""
___title___ = "NTP time"
___license___ = "MIT"
___dependencies___ = ["ntp", "wifi", "app"]
___categories___ = ["EMF"]
# borrowed from https://github.com/micropython/micropython/blob/master/esp8266/scripts/ntptime.py
import ugfx, ntp, wifi, utime, machine, app
from tilda import Buttons
# initialize screen
ugfx.init()
ugfx.clear()
# set the RTC using time from ntp
# print out RTC datetime
if not wifi.is_connected():
wifi.connect(show_wait_message=True)
ntp.set_NTP_time()
rtc = machine.RTC()
ugfx.orientation(270)
count = 0
last = None
while 1:
now = rtc.now()[:6]
year = now[0]
month = now[1]
day = now[2]
hour = now[3]
minute = now[4]
second = now[5]
if now != last:
last = now
ugfx.clear()
ugfx.text(5, 5, "current time", ugfx.BLACK)
time_str = "%02i:%02i:%02i %i/%i/%4i" % (hour, minute, second, day, month, year)
ugfx.text(5, 20, time_str, ugfx.BLACK)
if Buttons.is_pressed(Buttons.BTN_A) or Buttons.is_pressed(Buttons.BTN_B) or Buttons.is_pressed(Buttons.BTN_Menu):
break
utime.sleep_ms(10)
ugfx.clear()
app.restart_to_default()

View File

@ -1,89 +0,0 @@
"""What's on tap?!
Get up to date information on what's in stock at The Robot Arms!
"""
___title___ = "beer"
___license___ = "MIT"
___dependencies___ = ["app", "sleep", "wifi", "http", "ugfx_helper"]
___categories___ = ["EMF"]
import wifi, ugfx, http, ujson, app, sleep
from tilda import Buttons, LED
orientation = 270
def get_beer():
global bar, stock
LED(LED.RED).on()
try:
bar_json = http.get("https://bar.emf.camp/location/Bar.json").raise_for_status().content
stock_json = http.get("https://bar.emf.camp/stock.json").raise_for_status().content
bar = ujson.loads(bar_json)
stock = ujson.loads(stock_json)
except:
print('oh poop')
LED(LED.RED).off()
draw_screen()
def draw_screen():
global bar, stock
ugfx.clear(ugfx.BLACK)
ugfx.text(65, 5, "what's on tap?", ugfx.RED)
ugfx.line(5, 20, ugfx.width(), 20, ugfx.GREY)
for idx, beer in enumerate(bar['location']):
remaining = 0
for item in stock['stock']:
if item['description'] == beer['description']:
remaining = float(item['remaining'])
ugfx.text(5, 22 + idx*15, beer['description'][:28], ugfx.WHITE)
ugfx.text(202, 22 + idx*15, '!' if (remaining < 30) else ' ', ugfx.RED)
ugfx.text(210, 22 + idx*15, "{:>4}".format(beer['price']), ugfx.WHITE)
def toggle_orientation():
global orientation
if orientation == 90:
ugfx.orientation(270)
orientation = 270
draw_screen()
else:
ugfx.orientation(90)
orientation = 90
draw_screen()
ugfx.init()
ugfx.clear(ugfx.BLACK)
ugfx.set_default_font(ugfx.FONT_FIXED)
s=ugfx.Style()
s.set_enabled([ugfx.WHITE, ugfx.BLACK, ugfx.BLACK, ugfx.GREY])
s.set_background(ugfx.BLACK)
ugfx.set_default_style(s)
Buttons.enable_interrupt(Buttons.BTN_A, lambda button_id:get_beer(), on_press=True, on_release=False)
Buttons.enable_interrupt(Buttons.BTN_B, lambda button_id:toggle_orientation(), on_press=True, on_release=False)
Buttons.enable_interrupt(Buttons.BTN_Menu, lambda button_id:app.restart_to_default(), on_press=True, on_release=False)
ugfx.text(5, 10, "Instructions:", ugfx.WHITE)
ugfx.text(5, 30, "Press the A button to refresh", ugfx.WHITE)
ugfx.text(5, 45, "Press the B button to rotate", ugfx.WHITE)
ugfx.text(5, 60, "Press the Menu button to exit", ugfx.WHITE)
ugfx.text(5, 90, "!", ugfx.RED)
ugfx.text(15, 90, "means the stock is low", ugfx.WHITE)
ugfx.text(5, 120, "Loading data from the bar...", ugfx.WHITE)
get_beer()
while True:
sleep.wfi()
ugfx.clear()
app.restart_to_default()

View File

@ -1,208 +0,0 @@
"""Simple brainfuck (an esoteric programming language) interpreter.
Runs very slowly... prints sierpinski triangle"""
___name___ = "bf interpreter"
___license___ = "MIT"
___dependencies___ = ["sleep", "app"]
___categories___ = ["Other"]
import ugfx, os, time, sleep, app
from tilda import Buttons
from time import sleep_ms
# initialize screen
ugfx.init()
ugfx.clear()
ugfx.set_default_font(ugfx.FONT_TITLE)
Prog="""
+>-[>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>++[-]->>>>+>>>+>>>+>>>+>>>+>
>>+>>>+>>>+>>>++[-<+]-<<<<+<<++++[->++++++++<]<++[------>+<]>++<<+[--->++<]>++
<<-[--->+<]>------<+[-<+]-<[>>+[->+]-<[-]<<[-]+++[>[-]++++++++++.[-]+++[>+[>>+<<
-]>>[<<++[-<+]->++[->+]->-]<<+[-<+]->-[-[-[-[-[-[-[-[->>>]>>>]>>>]>>>]>>>]>>>]>>
>]>>>]>>>>>>>>>>>>>>>>>>>>> > > > > > > > > > > > > >>>>>>>>>>[+[-<+]-<<<<<<.>>>
>>>>]>[+[-<+]-<<<< <<<.>>>>>>>>]>[+[-
<+]-<<<<<<<<.>>> tic tac toe >>>>>>]+[-<+]-<<
<<<.>>>-]<-]+++ to play: type a number (1 to 9) to +++++++.[-]<<<<
<<[<<<<<<<<<<<+ place an X at that grid location [--->++<]>+++.[
->+++++++<]>.++ ++++.-[---->+<]
>+++.---[->+++<] [ http://mitxela.com/ ] >.+++[->++++<]>+
.+++++.-[->+++++<] >.[--->+<]>-.+[-<+
]-<[-]>>>>]<[<<<<<<<++++[++++>---<]>+.[++++>---<]>-.+++[->+++<]>++.+[--->+<]>+.+
[---->+<]>+++.[--->+<]>-.[-]+[-<+]-<[-]>>>>]<[<<<<<<<<<<+[--->++<]>+++.[->++++++
+<]>.++++++.-[---->+<]>+++.++++++[->++<]>.+[--->+<]>.++++.++++[->+++<]>.--[--->+
<]>.[--->+<]>-.+[-<+]-<[-]>>>>]<+[-<+]-<[>>->>>>>>+[-<<<<[-]<<[-]>>>>-[>>[-]+<<+
<[-]<[-]<[-]<[-]-[----->+<]>---<,>[-<->]<[>>+>+<<<-]>>[<<+>>-]+++++++++[->-[<<]>
]>>-]<<<<[-]>>>>[-]+<<<<<<[>>+>+<<<-]>>[<<+>>-]>>]>>-<<<[-]<<[<->-]<-[-[-[-[-[-[
-[-[->>>]>>>]>>>]>>>]>>>]>>>]>>>]>>>]]>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
>[->++[-<+]->>>>>[>>>[>>>[+[-<+]-<<<<<<<<<[-]++[->+]->]]]+[-<+]->>>>>>>>>>>>>>[>
>>[>>>[+[-<+]-<<<<<<<<<[-]++[->+]->]]]+[-<+]->>>>>>>>>>>>>>>>>>>>>>>[>>>[>>>[+[-
<+]-<<<<<<<<<[-]++[->+]->]]]+[-<+]->>>>>[>>>>>>>>>[>>>>>>>>>[+[-<+]-<<<<<<<<<[-]
++[->+]->]]]+[-<+]->>>>>>>>[>>>>>>>>>[>>>>>>>>>[+[-<+]-<<<<<<<<<[-]++[->+]->]]]+
[-<+]->>>>>>>>>>>[>>>>>>>>>[>>>>>>>>>[+[-<+]-<<<<<<<<<[-]++[->+]->]]]+[-<+]->>>>
>[>>>>>>>>>>>>[>>>>>>>>>>>>[+[-<+]-<<<<<<<<<[-]++[->+]->]]]+[-<+]->>>>>>>>>>>[>>
>>>>[>>>>>>[+[-<+]-<<<<<<<<<[-]++[->+]->]]]+[-<+]-<<<<<<<<<-[++[->+]-<<<<<<<<<<[
-]++[->+]->>>>[+[-<+]-<<<<<<<<<<[-]+[->+]->]+[-<+]->>>>>>>[+[-<+]-<<<<<<<<<<[-]+
[->+]->]+[-<+]->>>>>>>>>>[+[-<+]-<<<<<<<<<<[-]+[->+]->]+[-<+]->>>>>>>>>>>>>[+[-<
+]-<<<<<<<<<<[-]+[->+]->]+[-<+]->>>>>>>>>>>>>>>>[+[-<+]-<<<<<<<<<<[-]+[->+]->]+[
-<+]->>>>>>>>>>>>>>>>>>>[+[-<+]-<<<<<<<<<<[-]+[->+]->]+[-<+]->>>>>>>>>>>>>>>>>>>
>>>[+[-<+]-<<<<<<<<<<[-]+[->+]->]+[-<+]->>>>>>>>>>>>>>>>>>>>>>>>>[+[-<+]-<<<<<<<
<<<[-]+[->+]->]+[-<+]->>>>>>>>>>>>>>>>>>>>>>>>>>>>[+[-<+]-<<<<<<<<<<[-]+[->+]->]
+[-<+]-<<[-]>[-]+>>>>>>[>>>[>>[+[-<+]-<[-]<[-]++++[->+]->]]]>+[-<+]->>>>>[>>[>>>
>[+[-<+]-<[-]<[-]+++[->+]->]]]>+[-<+]->>>>[>>>>[>>>[+[-<+]-<[-]<[-]++[->+]->]]]>
+[-<+]->>>>>>>>>>>>>>[>>>[>>[+[-<+]-<[-]<[-]+++++++[->+]->]]]>+[-<+]->>>>>>>>>>>
>>>[>>[>>>>[+[-<+]-<[-]<[-]++++++[->+]->]]]>+[-<+]->>>>>>>>>>>>>[>>>>[>>>[+[-<+]
-<[-]<[-]+++++[->+]->]]]>+[-<+]->>>>>>>>>>>>>>>>>>>>>>>[>>>[>>[+[-<+]-<[-]<[-]++
++++++++[->+]->]]]>+[-<+]->>>>>>>>>>>>>>>>>>>>>>>[>>[>>>>[+[-<+]-<[-]<[-]+++++++
++[->+]->]]]>+[-<+]->>>>>>>>>>>>>>>>>>>>>>[>>>>[>>>[+[-<+]-<[-]<[-]++++++++[->+]
->]]]>+[-<+]->>>>>[>>>>>>>>>[>>>>>>>>[+[-<+]-<[-]<[-]++++++++[->+]->]]]>+[-<+]->
>>>>[>>>>>>>>[>>>>>>>>>>[+[-<+]-<[-]<[-]+++++[->+]->]]]>+[-<+]->>>>[>>>>>>>>>>[>
>>>>>>>>[+[-<+]-<[-]<[-]++[->+]->]]]>+[-<+]->>>>>>>>[>>>>>>>>>[>>>>>>>>[+[-<+]-<
[-]<[-]+++++++++[->+]->]]]>+[-<+]->>>>>>>>[>>>>>>>>[>>>>>>>>>>[+[-<+]-<[-]<[-]++
++++[->+]->]]]>+[-<+]->>>>>>>[>>>>>>>>>>[>>>>>>>>>[+[-<+]-<[-]<[-]+++[->+]->]]]>
+[-<+]->>>>>>>>>>>[>>>>>>>>>[>>>>>>>>[+[-<+]-<[-]<[-]++++++++++[->+]->]]]>+[-<+]
->>>>>>>>>>>[>>>>>>>>[>>>>>>>>>>[+[-<+]-<[-]<[-]+++++++[->+]->]]]>+[-<+]->>>>>>>
>>>[>>>>>>>>>>[>>>>>>>>>[+[-<+]-<[-]<[-]++++[->+]->]]]>+[-<+]->>>>>[>>>>>>>>>>>>
[>>>>>>>>>>>[+[-<+]-<[-]<[-]++++++++++[->+]->]]]>+[-<+]->>>>[>>>>>>>>>>>>>[>>>>>
>>>>>>>[+[-<+]-<[-]<[-]++[->+]->]]]>+[-<+]->>>>>>>>>>>[>>>>>>[>>>>>[+[-<+]-<[-]<
[-]++++++++[->+]->]]]>+[-<+]->>>>>>>>>>[>>>>>>>[>>>>>>[+[-<+]-<[-]<[-]++++[->+]-
>]]]>+[-<+]->>>>>>[>>>[>[+[-<+]-<[-]<[-]++++[->+]->]]]>+[-<+]->>>>>>[>[>>>>>[+[-
<+]-<[-]<[-]+++[->+]->]]]>+[-<+]->>>>[>>>>>[>>>[+[-<+]-<[-]<[-]++[->+]->]]]>+[-<
+]->>>>>>>>>>>>>>>[>>>[>[+[-<+]-<[-]<[-]+++++++[->+]->]]]>+[-<+]->>>>>>>>>>>>>>>
[>[>>>>>[+[-<+]-<[-]<[-]++++++[->+]->]]]>+[-<+]->>>>>>>>>>>>>[>>>>>[>>>[+[-<+]-<
[-]<[-]+++++[->+]->]]]>+[-<+]->>>>>>>>>>>>>>>>>>>>>>>>[>>>[>[+[-<+]-<[-]<[-]++++
++++++[->+]->]]]>+[-<+]->>>>>>>>>>>>>>>>>>>>>>>>[>[>>>>>[+[-<+]-<[-]<[-]++++++++
+[->+]->]]]>+[-<+]->>>>>>>>>>>>>>>>>>>>>>[>>>>>[>>>[+[-<+]-<[-]<[-]++++++++[->+]
->]]]>+[-<+]->>>>>>[>>>>>>>>>[>>>>>>>[+[-<+]-<[-]<[-]++++++++[->+]->]]]>+[-<+]->
>>>>>[>>>>>>>[>>>>>>>>>>>[+[-<+]-<[-]<[-]+++++[->+]->]]]>+[-<+]->>>>[>>>>>>>>>>>
[>>>>>>>>>[+[-<+]-<[-]<[-]++[->+]->]]]>+[-<+]->>>>>>>>>[>>>>>>>>>[>>>>>>>[+[-<+]
-<[-]<[-]+++++++++[->+]->]]]>+[-<+]->>>>>>>>>[>>>>>>>[>>>>>>>>>>>[+[-<+]-<[-]<[-
]++++++[->+]->]]]>+[-<+]->>>>>>>[>>>>>>>>>>>[>>>>>>>>>[+[-<+]-<[-]<[-]+++[->+]->
]]]>+[-<+]->>>>>>>>>>>>[>>>>>>>>>[>>>>>>>[+[-<+]-<[-]<[-]++++++++++[->+]->]]]>+[
-<+]->>>>>>>>>>>>[>>>>>>>[>>>>>>>>>>>[+[-<+]-<[-]<[-]+++++++[->+]->]]]>+[-<+]->>
>>>>>>>>[>>>>>>>>>>>[>>>>>>>>>[+[-<+]-<[-]<[-]++++[->+]->]]]>+[-<+]->>>>>>[>>>>>
>>>>>>>[>>>>>>>>>>[+[-<+]-<[-]<[-]++++++++++[->+]->]]]>+[-<+]->>>>[>>>>>>>>>>>>>
>[>>>>>>>>>>>>[+[-<+]-<[-]<[-]++[->+]->]]]>+[-<+]->>>>>>>>>>>>[>>>>>>[>>>>[+[-<+
]-<[-]<[-]++++++++[->+]->]]]>+[-<+]->>>>>>>>>>[>>>>>>>>[>>>>>>[+[-<+]-<[-]<[-]++
++[->+]->]]]>+[-<+]-<[>>+[-<+]->>>>>>>>>>>>>>>>>>>>>>>>>>>>[+[-<+]-<[-]<[-]+++++
+++++[->+]->]+[-<+]->>>>>>>>>>>>>>>>>>>>>>[+[-<+]-<[-]<[-]++++++++[->+]->]+[-<+]
->>>>>>>>>>[+[-<+]-<[-]<[-]++++[->+]->]+[-<+]->>>>[+[-<+]-<[-]<[-]++[->+]->]+[-<
+]->>>>>>>>>>>>>>>>>>>>>>>>>[+[-<+]-<[-]<[-]+++++++++[->+]->]+[-<+]->>>>>>>>>>>>
>>>>>>>[+[-<+]-<[-]<[-]+++++++[->+]->]+[-<+]->>>>>>>>>>>>>[+[-<+]-<[-]<[-]+++++[
->+]->]+[-<+]->>>>>>>[+[-<+]-<[-]<[-]+++[->+]->]+[-<+]->>>>>>>>>>>>>>>>[+[-<+]-<
[-]<[-]++++++[->+]->]+[-<+]->]>>+[-<+]-<<<<[+[->+]->>>>>>>>>>>>>>>>>[+[-<+]-<[-]
<[-]++[->+]->]+[-<+]->]>>>>+[-<+]-<<[>>>+[-<+]-<[-]<[+[-<+]->++[->+]-<<-]+[-<+]-
>-[-[-[-[-[-[-[-[->>>]>>>]>>>]>>>]>>>]>>>]>>>]>>>]>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>->>++[-<+]->]>>>>+[-<+]-<<[-]>>>+[-<+]-<<<<[-]>>>>>+[-<+]->>>>>>[>>
>[>>>[+[-<+]-<<<<<<<<<<<[-]++[->+]->]]]+[-<+]->>>>>>>>>>>>>>>[>>>[>>>[+[-<+]-<<<
<<<<<<<<[-]++[->+]->]]]+[-<+]->>>>>>>>>>>>>>>>>>>>>>>>[>>>[>>>[+[-<+]-<<<<<<<<<<
<[-]++[->+]->]]]+[-<+]->>>>>>[>>>>>>>>>[>>>>>>>>>[+[-<+]-<<<<<<<<<<<[-]++[->+]->
]]]+[-<+]->>>>>>>>>[>>>>>>>>>[>>>>>>>>>[+[-<+]-<<<<<<<<<<<[-]++[->+]->]]]+[-<+]-
>>>>>>>>>>>>[>>>>>>>>>[>>>>>>>>>[+[-<+]-<<<<<<<<<<<[-]++[->+]->]]]+[-<+]->>>>>>[
>>>>>>>>>>>>[>>>>>>>>>>>>[+[-<+]-<<<<<<<<<<<[-]++[->+]->]]]+[-<+]->>>>>>>>>>>>[>
>>>>>[>>>>>>[+[-<+]-<<<<<<<<<<<[-]++[->+]->]]]+[-<+]-<[-]]++[->+]->]+[-<+]-<+[
-<+]-<]>>+[->+]->>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>+[-[-]<+]-<+[-[-]<+]-<+>]
"""
# Hello World
#Prog="++++++++[>++++[>++>+++>+++>+<<<<-]>+>+>->>+[<]<-]>>.>---.+++++++..+++.>>.<-.<.+++.------.--------.>>+.>++."
# Sierpinski
Prog="""
++++++++[>+>++++<<-]>++>>+<[-[>>+<<-]+>>]>+[
-<<<[
->[+[-]+>++>>>-<<]<[<]>>++++++[<<+++++>>-]+<<++.[-]<<
]>.>+[>>]>+
]"""
buf=""
def output(t):
global buf
buf+=t
buf=buf[-(16*80):]
ugfx.clear()
lines=buf.split("\n")
lines=lines[-16:]
for i,v in enumerate(lines):
ugfx.text(5,i*20+5, v+" ", ugfx.BLACK)
lastpushed=0
def pushed(n):
global Tape, TP, waiting
if (waiting):
output(n+" \n")
Tape[TP]=ord(n)
waiting=False
Buttons.enable_interrupt(Buttons.BTN_1, lambda button_id:pushed("1"), on_press=True, on_release=False)
Buttons.enable_interrupt(Buttons.BTN_2, lambda button_id:pushed("2"), on_press=True, on_release=False)
Buttons.enable_interrupt(Buttons.BTN_3, lambda button_id:pushed("3"), on_press=True, on_release=False)
Buttons.enable_interrupt(Buttons.BTN_4, lambda button_id:pushed("4"), on_press=True, on_release=False)
Buttons.enable_interrupt(Buttons.BTN_5, lambda button_id:pushed("5"), on_press=True, on_release=False)
Buttons.enable_interrupt(Buttons.BTN_6, lambda button_id:pushed("6"), on_press=True, on_release=False)
Buttons.enable_interrupt(Buttons.BTN_7, lambda button_id:pushed("7"), on_press=True, on_release=False)
Buttons.enable_interrupt(Buttons.BTN_8, lambda button_id:pushed("8"), on_press=True, on_release=False)
Buttons.enable_interrupt(Buttons.BTN_9, lambda button_id:pushed("9"), on_press=True, on_release=False)
Buttons.enable_interrupt(Buttons.BTN_0, lambda button_id:pushed("0"), on_press=True, on_release=False)
output("Loading...")
waiting=False
Prog+='\0'
Tape=[0]*256
PP=-1
TP=0
while True:
if (waiting):
sleep_ms(200)
else:
PP=PP+1
if (PP>=len(Prog)):
waiting=True
output("END!")
elif (Prog[PP]=="+"):
Tape[TP]=Tape[TP]+1
elif (Prog[PP] =="-"):
Tape[TP]=Tape[TP]-1
elif (Prog[PP] ==">"):
TP=TP+1
elif (Prog[PP] =="<"):
TP=TP-1
elif (Prog[PP] =="."):
output(chr(Tape[TP]))
elif (Prog[PP] ==","):
waiting=True
elif (Prog[PP] =="["):
if (Tape[TP]==0):
depth=1
while (depth>0):
PP=PP+1
if (Prog[PP]=="]"):
depth = depth - 1
if (Prog[PP]=="["):
depth = depth + 1
elif (Prog[PP] =="]"):
if (Tape[TP]!=0):
depth=1
while (depth>0):
PP=PP-1
if (Prog[PP]=="]"):
depth = depth + 1
if (Prog[PP]=="["):
depth = depth - 1

View File

@ -1,141 +0,0 @@
"""App to use the badge as a (handset profile only) bluetooth speaker"""
___name___ = "Bluetooth Speaker"
___license___ = "MIT"
___dependencies___ = ["ugfx_helper", "sim800", "dialogs", "buttons", "app"]
___categories___ = ["Sound"]
import ugfx_helper, ugfx
import app
import sim800
from dialogs import *
import buttons
BLUETOOTH_NAME = "BadgeSpeaker"
g_paired = False
def pairing_dialog(scan_timeout_s=10):
''' Show BLE devices to pair with and connect. Returns True if paired, False if failed '''
waiting_message = WaitingMessage("Scanning for bluetooth devices for %s seconds"%scan_timeout_s, "Scanning")
devices = sim800.btscan(int(scan_timeout_s * 1000))
waiting_message.destroy()
# List format is [id, name, addr, rssi]. FIXME: Only returns 1 item?
try:
devices_prompts = [{'title': v[1], 'id': v[0]} for v in devices]
except TypeError: #Only one device found. #TODO: Not very neat
devices_prompts = [{'title':devices[1] ,'id':devices[0]},]
#TODO: Fix non printable chars in device names
option = prompt_option(devices_prompts, title="Devices Found", select_text="Select", none_text="Rescan")
if option:
sim800.btpair(option['id'])
passcode = sim800.btparingpasscode()
correct_passcode = prompt_boolean(passcode, title="Started connection from other device?", font=FONT_MEDIUM_BOLD)
if correct_passcode:
sim800.btpairconfirm() #TODO: 4 number passcodes?
return True
else:
sim800.btpairreject()
return False
else:
return False
def pairing_callback(param):
''' Callback for incoming pairing request '''
global g_paired
accept = prompt_boolean("Accept pairing request from %s"%param, title="Incoming pairing")
if accept:
sim800.btpairconfirm(0000)
# Check if we did pair
if len(sim800.btpaired()) > 1:
g_paired = True
else:
sim800.btpairreject()
def set_simple_pairing():
''' Set pairing mode to 4 digit pin, default 0000 '''
sim800.command("AT+BTPAIRCFG=1,0000", 1000, "OK") # TODO: Error checking?
#Initialise
ugfx_helper.init()
ugfx.init()
ugfx.clear()
ugfx.text(5,5, "Powering Up SIM800", ugfx.BLACK)
sim800.poweron()
ugfx.clear()
ugfx.text(5,5, "Enabling Bluetooth", ugfx.BLACK)
sim800.btpoweron()
sim800.btname(BLUETOOTH_NAME)
sim800.poweroff()
sim800.poweron()
sim800.btpoweron() # Needs a full cycle to have an effect
sim800.btvisible(True)
# Set pairing mode
set_simple_pairing()
ugfx.text(5,20, "Addr: %s " % sim800.btaddress(), ugfx.BLACK)
ugfx.text(5,35, "Name: %s " % sim800.btname(), ugfx.BLACK)
ugfx.clear()
# Register pairings callback
sim800.registercallback("+BTPAIRING:", pairing_callback)
clear_pairing = prompt_boolean("Delete all bluetooth pairings?",title="Clear Pairings?", true_text="Yes", false_text="No")
if clear_pairing:
sim800.btunpair(0) #0 = clear every pairing
# Start main loop
ugfx.clear()
ugfx.Label(5,5, 220, 200, "Connect to %s \n Passcode = 0000 \n Press menu to exit" % BLUETOOTH_NAME)
connected = True
while(True):
# Check for pairing button
if (buttons.is_triggered(buttons.Buttons.BTN_1)):
pairing_dialog()
# Check for exit button
if (buttons.is_triggered(buttons.Buttons.BTN_Menu)):
sim800.btpoweroff()
app.restart_to_default()
num_connections = len(sim800.btconnected())
if (connected == False) and (num_connections > 0): # Gained connection
ugfx.area(0,220,240,320, ugfx.BLACK) #Blank bottom of screen
print(sim800.btconnected())
sim800.speakervolume(100)
sim800.btvoicevolume(100)
ugfx.set_default_font(ugfx.FONT_TITLE)
ugfx.text(5,230,"CONNECTED!", ugfx.GREEN)
ugfx.set_default_font(ugfx.FONT_SMALL)
connected = True
elif (connected == True) and (num_connections == 0): # Lost connection
ugfx.area(0,220,240,320, ugfx.BLACK) #Blank bottom of screen
ugfx.set_default_font(ugfx.FONT_TITLE)
ugfx.text(5,230,"DISCONNECTED", ugfx.RED)
ugfx.set_default_font(ugfx.FONT_SMALL)
connected = False
sleep.wfi()

46
boot.py
View File

@ -1,46 +0,0 @@
import os, tilda
from machine import Neopix
n=Neopix()
n.display([0,0,0])
n.display([0,0,0])
print("EMF: boot.py")
os.sync()
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:
a = None
with open(file, 'r') as f:
a = f.read().strip()
if remove:
os.remove(file)
return app(a)
except Exception as e:
print("Not found: %s" % file)
def any_home():
h = [a for a in root if a.startswith("home")]
return h[0] if len(h) else False
if "no_boot" in root:
os.remove("no_boot")
print("no_boot found, aborting boot sequence")
elif "bootstrap.py" in root:
print("Bootstrapping...")
tilda.main("bootstrap.py")
else:
start = None
if "main.py" in root:
start = "main.py"
start = start or file("once.txt", True) or file("default_app.txt", False) or any_home()
if ".py" not in start:
start += "/main.py"
print("Booting into %s" % start)
tilda.main(start)

View File

@ -1,199 +0,0 @@
"""Bootstraps the badge by downloading the base software"""
import ugfx, machine, network, json, time, usocket, os, gc
from tilda import Buttons
HOST = "badgeserver.emfcamp.org"
wifi = network.WLAN()
wifi.active(True)
# Helpers
def msg(text):
ugfx.clear()
ugfx.text(5, 5, "EMF 2018", ugfx.BLACK)
ugfx.text(5, 30, "TiLDA Mk4", ugfx.BLACK)
lines = text.split("\n")
print(lines[0])
for i, line in enumerate(lines):
ugfx.text(5, 65 + i * 20, line, ugfx.BLACK)
def wifi_select():
msg("Please select your wifi\nConfirm with button A")
sl = ugfx.List(5, 110, 228, 204)
aps = {}
while not Buttons.is_pressed(Buttons.BTN_A):
for s in (wifi.scan() or []):
if s[0] not in aps:
sl.add_item(s[0])
aps[s[0]] = s
time.sleep(0.01)
ugfx.poll()
ssid = sl.selected_text()
sl.destroy()
msg("Wifi: %s\nPlease enter your password\nConfirm with button A" % ssid)
kb = ugfx.Keyboard(0, 160, 240, 170)
e = ugfx.Textbox(5, 130, 228, 25, text="")
while not Buttons.is_pressed(Buttons.BTN_A):
time.sleep(0.01)
ugfx.poll()
pw = e.text()
e.destroy()
kb.destroy()
result = {"ssid":ssid,"pw":pw}
with open("wifi.json", "wt") as file:
file.write(json.dumps(result))
file.flush()
os.sync()
return result
def wifi_details():
try:
with open("wifi.json") as f:
return json.loads(f.read())
except Exception as e:
print(str(e))
return wifi_select()
def connect():
details = wifi_details()
if 'user' in details:
wifi.connect(details['ssid'], details['pw'], enterprise=True, entuser=details['user'], entmethod=wifi.EAP_METHOD_PEAP0_MSCHAPv2, entserverauth=False)
elif 'pw' in details:
wifi.connect(details['ssid'], details['pw'])
else:
wifi.connect(details['ssid'])
wait_until = time.ticks_ms() + 10000
while not wifi.isconnected():
if (time.ticks_ms() > wait_until):
os.remove("wifi.json")
raise OSError("Wifi timeout");
time.sleep(0.1)
def addrinfo(host, port, retries_left = 20):
try:
return usocket.getaddrinfo(host, port)[0][4]
except OSError as e:
if ("-15" in str(e)) and retries_left:
# [addrinfo error -15]
# This tends to happen after startup and goes away after a while
time.sleep_ms(200)
return addrinfo(host, port, retries_left - 1)
else:
raise e
def get(path):
s = usocket.socket()
s.connect(addrinfo(HOST, 80))
body = b""
status = None
try:
s.send('GET /2018/%s HTTP/1.0\r\nHost: %s\r\n\r\n' % (path, HOST))
state = 1
hbuf = b""
clen = 9999999
headers = {}
while len(body) < clen:
buf = s.recv(1024)
if state == 1: # Status
nl = buf.find(b"\n")
if nl > -1:
hbuf += buf[:nl - 1]
status = int(hbuf.split(b' ')[1])
state = 2
hbuf = b"";
buf = buf[nl + 1:]
else:
hbuf += buf
if state == 2: # Headers
hbuf += buf
nl = hbuf.find(b"\n")
while nl > -1:
if nl < 2:
buf = hbuf[2:]
hbuf = None
state = 3
clen = int(headers["content-length"])
break
header = hbuf[:nl - 1].decode("utf8").split(':', 3)
headers[header[0].strip().lower()] = header[1].strip()
hbuf = hbuf[nl + 1:]
nl = hbuf.find(b"\n")
if state == 3: # Content
body += buf
finally:
s.close()
if status != 200:
raise Exception("HTTP %d for %s" % (status, path))
return body
# os.path bits
def split(path):
if path == "":
return ("", "")
r = path.rsplit("/", 1)
if len(r) == 1:
return ("", path)
head = r[0]
if not head:
head = "/"
return (head, r[1])
def dirname(path):
return split(path)[0]
def exists(path):
try:
os.stat(path)[0]
return True
except OSError:
return False
def makedirs(path):
sub_path = split(path)[0]
if sub_path and (not exists(sub_path)):
makedirs(sub_path)
if not exists(path):
os.mkdir(path)
# Steps
def step_wifi():
while not wifi.isconnected():
msg("Connecting to wifi...");
try:
connect()
except Exception as e:
print(str(e))
msg("Couldn't connect\nPlease check wifi details")
time.sleep(1)
def step_download():
msg("Connecting to server...")
files = list(json.loads(get("bootstrap")).keys())
for i, file in enumerate(files):
msg("Downloading - %d%%\n%s" % (100 * i // len(files), file))
makedirs(dirname(file))
with open(file, 'wb') as f:
f.write(get("download?repo=emfcamp/Mk4-Apps&path=%s" % file))
os.sync()
def step_goodbye():
msg("All done!\n\nRestarting badge...")
os.remove("bootstrap.py")
time.sleep(2)
machine.reset()
ugfx.init()
machine.Pin(machine.Pin.PWM_LCD_BLIGHT).on()
try:
step_wifi()
step_download()
step_goodbye()
except Exception as e:
msg("Error\nSomething went wrong :(\n\n" + str(e))
raise e

View File

@ -1,235 +0,0 @@
"""Breakout!"""
___title___ = "Breakout"
___license___ = "MIT"
___categories___ = ["Games"]
___dependencies___ = ["app", "ugfx_helper", "buttons"]
from tilda import Buttons
import ugfx, ugfx_helper, dialogs
import time
import app
import random
import math
background_colour = ugfx.BLACK
framerate = 60
SCREEN_WIDTH = 240
SCREEN_HEIGHT = 320
class Ball:
def __init__(self, x = 5.0, y = 5.0, dx = 2, dy = 2):
self.colour = ugfx.WHITE
self.diameter = 4
self.x = x
self.y = y
self.dy = dx
self.dx = dy
def centerX(self):
return self.x + self.diameter / 2
def centerY(self):
return self.y + self.diameter / 2
def left(self):
return self.x
def right(self):
return self.x + self.diameter
def top(self):
return self.y
def bottom(self):
return self.y + self.diameter
def draw(self):
ugfx.fill_ellipse(int(self.x), int(self.y), self.diameter, self.diameter, self.colour)
def clear(self):
ugfx.fill_ellipse(int(self.x), int(self.y), self.diameter, self.diameter, background_colour)
def bounceX(self):
self.dx *= -1
def bounceY(self):
self.dy *= -1
def bounceUpwards(self, ratioFromMiddle):
speed = math.sqrt(self.dx * self.dx + self.dy * self.dy)
self.dx = math.sin(ratioFromMiddle) * speed
self.dy = math.cos(ratioFromMiddle) * speed * -1
def tick(self):
self.x += self.dx
self.y += self.dy
if self.x < 0 or self.x + self.diameter > SCREEN_WIDTH:
self.bounceX()
if self.y < 0 or self.y + self.diameter > SCREEN_HEIGHT:
self.bounceY()
def hasCollidedWith(self, item):
return self.right() >= item.left() and self.left() <= item.right() and self.top() <= item.bottom() and self.bottom() >= item.top()
def isHorizontalCollision(self, item):
return self.centerY() >= item.top() and self.centerY() <= item.bottom()
def isVerticalCollision(self, item):
return self.centerX() >= item.left() and self.centerX() <= item.right()
def hasHitTop(self, item):
return self.y + self.diameter >= item.top()
def horizontalPositionFromMiddle(self, item):
return min(1, max(0, (self.centerX() - item.left()) / (item.right() - item.left()))) - 1
class Paddle:
def __init__(self, x = SCREEN_WIDTH / 2, width = SCREEN_WIDTH // 4, dx = 10):
self.x = x
self.dx = dx
self.width = width
self.height = 4
self.colour = ugfx.WHITE
def left(self):
return self.x - self.width / 2
def right(self):
return self.x + self.width / 2
def top(self):
return self.bottom() - self.height
def bottom(self):
return SCREEN_HEIGHT
def draw(self):
ugfx.area(int(self.left()), int(self.top()), self.width, self.height, self.colour)
def clear(self):
ugfx.area(int(self.left()), int(self.top()), self.width, self.height, background_colour)
def tick(self):
if Buttons.is_pressed(Buttons.JOY_Right) and self.right() < SCREEN_WIDTH:
self.x += self.dx
if Buttons.is_pressed(Buttons.JOY_Left) and self.left() > 0:
self.x -= self.dx
class Block:
def __init__(self, x, y, width, height, colour = ugfx.WHITE):
self.x = x
self.y = y
self.width = width
self.height = height
self.colour = colour
self.visible = True
def left(self):
return self.x
def right(self):
return self.x + self.width
def top(self):
return self.y
def bottom(self):
return self.y + self.height
def draw(self):
colour = self.colour if self.visible else background_colour
ugfx.area(int(self.left()), int(self.top()), self.width, self.height, colour)
def clear(self):
ugfx.area(int(self.left()), int(self.top()), self.width, self.height, background_colour)
def hide(self):
self.visible = False
self.clear()
# Clear LEDs
leds = Neopix()
leds.display([0,0,0])
leds.display([0,0,0])
ugfx_helper.init()
ugfx.clear(background_colour)
def randomColour():
return random.randint(0, 0xffffff)
def gameEnd(score):
ugfx.text(5, 5, str(score) + ' POINTS!!!', ugfx.WHITE)
for i in range(0, 10):
leds.display([randomColour(), 0])
time.sleep(0.1)
leds.display([0, randomColour()])
time.sleep(0.1)
leds.display([0, 0])
time.sleep(1)
def gameOver(score):
ugfx.text(5, 5, 'GAME OVER', ugfx.WHITE)
ugfx.text(5, 30, str(score) + ' points', ugfx.WHITE)
for i in range(0, 5):
leds.display([0xff0000, 0])
time.sleep(0.2)
leds.display([0, 0xff0000])
time.sleep(0.2)
leds.display([0, 0])
time.sleep(1)
def runGame():
paddle = Paddle()
direction = random.random() - 0.5
initial_speed_up = 4
ball = Ball(x = SCREEN_WIDTH / 2, y = SCREEN_HEIGHT / 2, dx = math.cos(direction) * initial_speed_up, dy = math.sin(direction) * initial_speed_up)
blocks = \
[Block(x = x, y = 30, width = 36, height = 10, colour = ugfx.RED) for x in range(24, SCREEN_WIDTH - 24, 40)] + \
[Block(x = x, y = 44, width = 36, height = 10, colour = ugfx.GREEN) for x in range(24, SCREEN_WIDTH - 24, 40)] + \
[Block(x = x, y = 58, width = 36, height = 10, colour = ugfx.BLUE) for x in range(24, SCREEN_WIDTH - 24, 40)] + \
[Block(x = x, y = 72, width = 36, height = 10, colour = ugfx.YELLOW) for x in range(24, SCREEN_WIDTH - 24, 40)] + \
[Block(x = x, y = 86, width = 36, height = 10, colour = ugfx.ORANGE) for x in range(24, SCREEN_WIDTH - 24, 40)]
def invisibleBlocks():
return [block for block in blocks if not(block.visible)]
for block in blocks:
block.draw()
while True:
paddle.draw()
ball.draw()
time.sleep(1.0 / framerate)
paddle.clear()
ball.clear()
paddle.tick()
ball.tick()
if Buttons.is_pressed(Buttons.BTN_Menu):
gameRunning = False
if all([not(block.visible) for block in blocks]):
gameEnd(score = 50 + len(invisibleBlocks()))
break
if ball.hasHitTop(paddle):
if ball.hasCollidedWith(paddle):
ball.bounceUpwards(ball.horizontalPositionFromMiddle(paddle))
else:
gameOver(score = len(invisibleBlocks()))
break
for block in blocks:
if block.visible and ball.hasCollidedWith(block):
block.hide()
if ball.isHorizontalCollision(block):
ball.bounceX()
if ball.isVerticalCollision(block):
ball.bounceY()
runGame()
app.restart_to_default()

View File

@ -1,76 +0,0 @@
"""Scan for and display nearby bluetooth devices"""
___title___ = "Bluetooth Scan"
___license___ = "MIT"
___dependencies___ = ["sleep", "app", "sim800"]
___categories___ = ["Other", "System"]
import ugfx, app
from machine import Neopix
np = Neopix()
import sim800
from tilda import Buttons
from time import sleep
btrestore = False
duration = 10
status_height = 20
ugfx.init()
ugfx.clear()
ugfx.set_default_font(ugfx.FONT_FIXED)
def instructions(duration):
ugfx.Label(5, 180, 240, 30, "Press A to start, B to change scan length or MENU to exit")
ugfx.Label(5, 210, 240, 15, "Scan requires ~{0} seconds".format(duration))
if not sim800.btison():
sim800.btpoweron()
btrestore = True
instructions(duration)
# while (not Buttons.is_pressed(Buttons.BTN_A)) and (not Buttons.is_pressed(Buttons.BTN_B)) and (not Buttons.is_pressed(Buttons.BTN_Menu)):
while not Buttons.is_pressed(Buttons.BTN_Menu):
a = Buttons.is_pressed(Buttons.BTN_A)
b = Buttons.is_pressed(Buttons.BTN_B)
if not a and not b:
ugfx.poll()
continue
if b:
duration = duration + 5
if duration > 60:
duration = 5
ugfx.clear()
instructions(duration)
continue
ugfx.clear()
np.display([0,0])
np.display([0x000099, 0x000099])
devs = sim800.btscan(duration*1000)
np.display([0x00, 0x00])
if len(devs) == 0:
ugfx.Label(0, 0, 240, 25, "No devices found")
np.display([0x110000,0x110000])
sleep(1)
np.display([0,0])
else:
if type(devs[0]) == int:
devs = [devs]
y = 0
for dev in devs[:20]:
ugfx.Label(0, y, 240, 25, "{3}dB {1}".format(*dev))
y += status_height
instructions(duration)
## App quitting...
if btrestore:
sim800.btpoweroff()
ugfx.clear()
app.restart_to_default()

File diff suppressed because it is too large Load Diff

View File

@ -1,58 +0,0 @@
''' Random card generator, includes Base Set, The First Expansion, The Second Expansion, The Third Expansion, The Fourth Expansion, The Fifth Expansion, The Sixth Expansion, Green Box Expansion, 90s Nostalgia Pack, Box Expansion, Fantasy Pack, Food Pack, Science Pack and World Wide Web Pack '''
___name___ = "Cards Against EMF"
___license___ = ["MIT"]
___dependencies___ = ["ugfx_helper", "sleep"]
___categories___ = ["Games"]
___bootstrapped___ = False # Whether or not apps get downloaded on first install. Defaults to "False", mostly likely you won't have to use this at all.
import ugfx, json, random
from tilda import Buttons
from app import restart_to_default
ugfx.init()
ugfx.clear()
ugfx.text(10, 10, "CARDS AGAINST EMF", ugfx.BLACK)
ugfx.text(10, 40, "A for a question", ugfx.BLACK)
ugfx.text(10, 60, "B for an answer", ugfx.BLACK)
ugfx.text(10, 80, "MENU to exit", ugfx.BLACK)
b=ugfx.Style()
b.set_background(ugfx.BLACK)
b.set_enabled([ugfx.WHITE, ugfx.BLACK, ugfx.BLACK, ugfx.BLACK]) # sets the style for when something is enabled
w=ugfx.Style()
w.set_background(ugfx.WHITE)
with open("cards_against_emf/cards.json") as data:
d = json.load(data)
def get_black():
x = random.randint(1, 320)
ugfx.clear(ugfx.html_color(0x000000))
text = str(d["blackCards"][x]["text"])
ugfx.Label(0, 0, 240, 400, text, style=b)
def get_white():
y = random.randint(1, 1271)
ugfx.clear(ugfx.html_color(0xffffff))
text = str(d["whiteCards"][y])
ugfx.Label(0, 0, 240, 400, text, style=w)
Buttons.enable_interrupt(
Buttons.BTN_A,
lambda button_id:get_black(),
on_press=True,
on_release=False)
Buttons.enable_interrupt(
Buttons.BTN_B,
lambda button_id:get_white(),
on_press=True,
on_release=False)
Buttons.enable_interrupt(
Buttons.BTN_Menu,
lambda button_id:restart_to_default(),
on_press=True,
on_release=False)

Binary file not shown.

View File

@ -1,86 +0,0 @@
"""Colour picker to show on neopixels"""
___name___ = "ColourPicker"
___title___ = "Colour Picker"
___license___ = "MIT"
___dependencies___ = ["ugfx_helper"]
___categories___ = ["LEDs"]
import ugfx, ugfx_helper, app
from tilda import Buttons
from time import sleep
from machine import Neopix
def getColour(intensity, angle):
intensity *= 2
if angle < (1 / 6):
return (intensity, intensity * (angle * 6), 0) if intensity < 1 else (1, (angle * 6) + ((1 - (angle * 6)) * (intensity - 1)), (intensity - 1))
elif angle < (2 / 6):
return (intensity * (2 - (6 * angle)), intensity, 0) if intensity < 1 else ((2 - (6 * angle)) + ((1 - (2 - (6 * angle))) * (intensity - 1)), 1, (intensity - 1))
elif angle < (3 / 6):
return (0, intensity, intensity * ((6 * angle) - 2)) if intensity < 1 else ((intensity - 1), 1, ((6 * angle) - 2) + ((1 - ((6 * angle) - 2)) * (intensity - 1)))
elif angle < (4 / 6):
return (0, intensity * (4 - (6 * angle)), intensity) if intensity < 1 else ((intensity - 1), (4 - (6 * angle)) + ((1 - (4 - (6 * angle))) * (intensity - 1)), 1)
elif angle < (5 / 6):
return (intensity * ((6 * angle) - 4), 0, intensity) if intensity < 1 else (((6 * angle) - 4) + ((1 - ((6 * angle) - 4)) * (intensity - 1)), (intensity - 1), 1)
else:
return (intensity, 0, intensity * 6 * (1 - angle)) if intensity < 1 else (1, (intensity - 1), (6 * (1 - angle)) + ((1 - (6 * (1 - angle))) * (intensity - 1)))
ugfx_helper.init()
ugfx.clear()
maxHeight = ugfx.height()
n = Neopix()
# Draw colour swatch
for x in range(ugfx.width()):
intensity = x / ugfx.width()
for y in range(maxHeight):
(r, g, b) = getColour(intensity, y / ugfx.height())
colour = (int(31 * r) << 11) + (int(63 * g) << 5) + int(31 * b)
ugfx.area(x, y, 1, 1, colour)
i = 0
j = 0
ugfx.area((i - 1) if i > 0 else 0, (j - 1) if j > 0 else 0, 3 if (i > 0 and i < (ugfx.width() - 1)) else 2, 3 if (j > 0 and j < (maxHeight - 1)) else 2, ugfx.WHITE)
while (not Buttons.is_pressed(Buttons.BTN_A)) and (not Buttons.is_pressed(Buttons.BTN_B)) and (not Buttons.is_pressed(Buttons.BTN_Menu)):
changed = False
oldI = i
oldJ = j
if Buttons.is_pressed(Buttons.JOY_Right) and (i < (ugfx.width() - 1)):
i += 1
changed = True
elif Buttons.is_pressed(Buttons.JOY_Left) and (i > 0):
i -= 1
changed = True
if Buttons.is_pressed(Buttons.JOY_Down) and (j < (maxHeight - 1)):
j += 1
changed = True
elif Buttons.is_pressed(Buttons.JOY_Up) and (j > 0):
j -= 1
changed = True
if changed:
(r, g, b) = getColour(i / ugfx.width(), j / ugfx.height())
colour = (int(255 * r) << 16) + (int(255 * g) << 8) + int(255 * b)
n.display([colour, colour])
for xx in range((oldI - 1) if (oldI > 0) else 0, 1 + ((oldI + 1) if (oldI < (ugfx.width() - 2)) else (ugfx.width() - 1))):
intensity = xx / ugfx.width()
for yy in range((oldJ - 1) if (oldJ > 0) else 0, 1 + ((oldJ + 1) if (oldJ < (maxHeight - 2)) else (maxHeight - 1))):
(rr, gg, bb) = getColour(intensity, yy / ugfx.height())
colour = (int(31 * rr) << 11) + (int(63 * gg) << 5) + int(31 * bb)
ugfx.area(xx, yy, 1, 1, colour)
ugfx.area((i - 1) if i > 0 else 0, (j - 1) if j > 0 else 0, 3 if (i > 0 and i < (ugfx.width() - 1)) else 2, 3 if (j > 0 and j < (maxHeight - 1)) else 2, ugfx.WHITE)
sleep(0.05)
ugfx.clear()
app.restart_to_default()

View File

@ -1,124 +0,0 @@
"""
Clone of the default homescreen for the Tilda Mk4.
Shows the EMF homescreen and a picture loaded on the badge alternately.
"""
___title___ = "Custom Image Home"
___license___ = "MIT"
___categories___ = ["Homescreens"]
___dependencies___ = ["homescreen", "shared/logo.png", "shared/sponsors.png"]
import ugfx
from homescreen import *
import time
import os
# We ❤️ our sponsors
ugfx.display_image(0, 0, "shared/sponsors.png")
wait = 5
while wait:
wait -= 1
sleep_or_exit(0.5)
def drawEMFscreen():
# Padding for name
intro_height = 30
intro_text = "Hi! I'm"
name_height = 60
status_height = 20
info_height = 30
logo_path = "shared/logo.png"
logo_height = 150
logo_width = 56
# Maximum length of name before downscaling
max_name = 8
# Background stuff
init()
ugfx.clear(ugfx.html_color(0x800080))
# Colour stuff
style = ugfx.Style()
style.set_enabled([ugfx.WHITE, ugfx.html_color(0x800080), ugfx.html_color(0x800080), ugfx.html_color(0x800080)])
style.set_background(ugfx.html_color(0x800080))
ugfx.set_default_style(style)
# Logo stuff
ugfx.display_image(
int((ugfx.width() - logo_width) / 2),
int((ugfx.height() - logo_height) / 2),
logo_path
)
# Draw for people to see
ugfx.orientation(90)
# Draw introduction
ugfx.set_default_font(ugfx.FONT_TITLE)
ugfx.Label(0, ugfx.height() - name_height - intro_height, ugfx.width(), intro_height, intro_text,
justification=ugfx.Label.CENTER)
# Process name
name_setting = name("Set your name in the settings app")
if len(name_setting) <= max_name:
ugfx.set_default_font(ugfx.FONT_NAME)
else:
ugfx.set_default_font(ugfx.FONT_MEDIUM_BOLD)
# Draw name
ugfx.Label(0, ugfx.height() - name_height, ugfx.width(), name_height, name_setting, justification=ugfx.Label.CENTER)
# Draw for wearer to see
ugfx.orientation(270)
# Title
ugfx.set_default_font(ugfx.FONT_TITLE)
ugfx.Label(0, ugfx.height() - info_height * 2, ugfx.width(), info_height, "TiLDA Mk4",
justification=ugfx.Label.CENTER)
# info
ugfx.Label(0, ugfx.height() - info_height, ugfx.width(), info_height, "Press MENU", justification=ugfx.Label.CENTER)
ugfx.set_default_font(ugfx.FONT_SMALL)
status = ugfx.Label(0, ugfx.height() - info_height * 2 - status_height, ugfx.width(), status_height, "",
justification=ugfx.Label.CENTER)
text = ""
value_wifi_strength = wifi_strength()
value_battery = battery()
if value_wifi_strength:
text += "Wi-Fi: %s%%, " % int(value_wifi_strength)
if value_battery:
text += "Battery: %s%%" % int(value_battery)
status.text(text)
def drawCustomImage():
ugfx.clear()
ugfx.orientation(90)
ugfx.display_image(0, 0, 'customImage.png')
def drawHelpText():
ugfx.clear()
ugfx. Label(0, 110, ugfx.width(), 100, "Copy an image named\ncustomImage.png with a\n240x320 resolution to the\nbadge root directory\nand it will appear!")
try:
f = open('customImage.png')
customImage = True
f.close()
except OSError:
customImage = False
# update loop
while True:
drawEMFscreen()
wait = 20
while wait:
wait -= 1
sleep_or_exit(0.5)
if customImage:
drawCustomImage()
else:
drawHelpText()
wait = 20
while wait:
wait -= 1
sleep_or_exit(0.5)

View File

@ -1,60 +0,0 @@
"""This is a dowsing rod for WiFi APs"""
___title___ = "Dowsing Rod"
___license___ = "MIT"
___dependencies___ = ["sleep", "app", "wifi", "sim800"]
___categories___ = ["EMF", "System"]
import ugfx, wifi, app
from tilda import Buttons
from time import sleep
status_height = 20
ssid = 'emfcamp-legacy18'
ugfx.init()
ugfx.clear()
ugfx.set_default_font(ugfx.FONT_FIXED)
ugfx.Label(5, 180, 240, 15, "Press A to scan, MENU to exit")
# while (not Buttons.is_pressed(Buttons.BTN_A)) and (not Buttons.is_pressed(Buttons.BTN_B)) and (not Buttons.is_pressed(Buttons.BTN_Menu)):
while not Buttons.is_pressed(Buttons.BTN_Menu):
if not Buttons.is_pressed(Buttons.BTN_A) and not Buttons.is_pressed(Buttons.BTN_B):
ugfx.poll()
continue
if Buttons.is_pressed(Buttons.BTN_B):
ugfx.clear()
ugfx.Label(0, 0, 240, 25, "SSID:")
ssid_box = ugfx.Textbox(0, 25, 240, 25, text=ssid)
ugfx.Keyboard(0, ugfx.height()//2, ugfx.width(), ugfx.height()//2)
ssid_box.set_focus()
while not Buttons.is_pressed(Buttons.BTN_A):
ugfx.poll()
continue
ssid = ssid_box.text()
ugfx.clear()
wifi.nic().active(False)
wifi.nic().active(True)
# networks = [{ "ssid": ap[0], "mac": ap[1], "channel": ap[2], "signal": ap[3] } for ap in wifi.nic().scan()]
networks = sorted([net for net in wifi.nic().scan() if net[0] == ssid], key=lambda n: n[3], reverse=True)
aps = []
for ap in [(net[1], net[3]) for net in networks]:
if ap[0] not in [ap[0] for ap in aps]:
aps.append(ap)
y = 0
for ap in aps[:20]:
ugfx.Label(0, y, 240, 25, "{1}dB {0}".format(*ap))
y += status_height
if len(aps) == 0:
ugfx.Label(0, y, 240, 25, "No %s APs found" % ssid)
ugfx.clear()
app.restart_to_default()

View File

@ -1,102 +0,0 @@
"""
emfcampqueer theme by ganbariley
"""
___title___ = "EMFCamp Rainbow Homescreen"
___license___ = "MIT"
___categories___ = ["Homescreens"]
___dependencies___ = ["homescreen"]
___launchable___ = False
___bootstrapped___ = False
import ugfx
from homescreen import *
import time
from tilda import Buttons
from machine import Pin
from machine import Neopix
torch = Pin(Pin.GPIO_FET)
neo = Neopix()
# Padding for name
intro_height = 30
intro_text = "Hi! I'm"
name_height = 60
status_height = 20
info_height = 30
logo_path = "emfcampqueer_home/pridelogo.png"
logo_height = 150
logo_width = 56
# Maximum length of name before downscaling
max_name = 8
torch_on = False
# Background stuff
init()
ugfx.clear(ugfx.html_color(0x800080))
# Colour stuff
style = ugfx.Style()
style.set_enabled([ugfx.WHITE, ugfx.html_color(0x800080), ugfx.html_color(0x800080), ugfx.html_color(0x800080)])
style.set_background(ugfx.html_color(0x800080))
ugfx.set_default_style(style)
# Logo stuff
ugfx.display_image(
int((ugfx.width() - logo_width) / 2),
int((ugfx.height() - logo_height) / 2),
logo_path
)
# Draw for people to see
ugfx.orientation(90)
# Draw introduction
ugfx.set_default_font(ugfx.FONT_TITLE)
ugfx.Label(0, ugfx.height() - name_height - intro_height, ugfx.width(), intro_height, intro_text, justification=ugfx.Label.CENTER)
# Process name
name_setting = name("Set your name in the settings app")
if len(name_setting) <= max_name:
ugfx.set_default_font(ugfx.FONT_NAME)
else:
ugfx.set_default_font(ugfx.FONT_MEDIUM_BOLD)
# Draw name
ugfx.Label(0, ugfx.height() - name_height, ugfx.width(), name_height, name_setting, justification=ugfx.Label.CENTER)
# Draw for wearer to see
ugfx.orientation(270)
# Title
ugfx.set_default_font(ugfx.FONT_TITLE)
ugfx.Label(0, ugfx.height() - info_height * 2, ugfx.width(), info_height, "TiLDA Mk4", justification=ugfx.Label.CENTER)
# info
ugfx.Label(0, ugfx.height() - info_height, ugfx.width(), info_height, "Press MENU", justification=ugfx.Label.CENTER)
ugfx.set_default_font(ugfx.FONT_SMALL)
status = ugfx.Label(0, ugfx.height() - info_height * 2 - status_height, ugfx.width(), status_height, "", justification=ugfx.Label.CENTER)
# update loop
while True:
text = "";
value_wifi_strength = wifi_strength()
value_battery = battery()
if value_wifi_strength:
text += "Wi-Fi: %s%%, " % int(value_wifi_strength)
if value_battery:
text += "Battery: %s%%" % int(value_battery)
status.text(text)
if Buttons.is_pressed(Buttons.BTN_Star):
if torch_on:
torch_on = False
torch.off()
neo.display([0,0])
else:
torch_on = True
torch.on()
neo.display([0xffffff,0xffffff])
sleep_or_exit(0.5)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -1,75 +0,0 @@
"""enby flag homescreen
Similar to the default homescreen, but the
background is the enby flag. Based on Pride Flag Homescreen by marekventur
"""
___title___ = "Enby"
___license___ = "MIT"
___categories___ = ["Homescreens"]
___dependencies___ = ["homescreen", "app"]
from app import restart_to_default
import ugfx
import homescreen
homescreen.init()
ugfx.clear(ugfx.html_color(0xFF0000))
# Used for placement around text
name_height = 55
info_height = 20
# Maximum length of name before downscaling
max_name = 8
# Orientation for other people to see
ugfx.orientation(90)
# enby flag colours
colours = [0xfff433, 0xffffff, 0x9b59d0, 0x000000]
# Draw each "band" of colour in the flag
colour_width = ugfx.width() / len(colours)
for num, colour in enumerate(colours):
width_loc = int(num * colour_width)
ugfx.area(width_loc, 0, int(colour_width), 320, ugfx.html_color(colour))
ugfx.set_default_font(ugfx.FONT_NAME)
# Calc center of screen
center = (int(ugfx.width() / 2), int(ugfx.height() / 2))
# Process name
given_name = homescreen.name("Set your name in the settings app")
if len(given_name) <= max_name:
ugfx.set_default_font(ugfx.FONT_NAME)
else:
ugfx.set_default_font(ugfx.FONT_MEDIUM_BOLD)
# Draw name
ugfx.Label(0, ugfx.height() - name_height, ugfx.width(), name_height, given_name, justification=ugfx.Label.CENTER)
# Draw for the user to see
ugfx.orientation(270)
ugfx.set_default_font(ugfx.FONT_SMALL)
# WiFi/Battery update loop
while True:
ugfx.area(0, ugfx.height() - info_height, ugfx.width(), info_height, ugfx.WHITE)
wifi_strength_value = homescreen.wifi_strength()
if wifi_strength_value:
wifi_message = 'WiFi: %s%%' % int(wifi_strength_value)
wifi_text = ugfx.text(center[0], ugfx.height() - info_height, wifi_message, ugfx.BLACK)
battery_value = homescreen.battery()
if battery_value:
battery_message = 'Battery: %s%%' % int(battery_value)
battery_text = ugfx.text(0, ugfx.height() - info_height, battery_message, ugfx.BLACK)
homescreen.sleep_or_exit(1.5)
restart_to_default()

View File

@ -1,106 +0,0 @@
"""Game of Life"""
___title___ = "Conway game of life"
___license___ = "MIT"
___categories___ = ["Games"]
___dependencies___ = ["app", "ugfx_helper", "sleep", "buttons"]
import app, ugfx, ugfx_helper, buttons, sleep, time, random
from tilda import Buttons
# the game of life logic
class Board:
def __init__(self, width, height):
self.width = width
self.height = height
self.data = [random.randint(0,1) for x in range(width * height)]
def __str__(self):
res = "w: {} h: {}".format(self.width, self.height)
for j in range(0, self.height):
row = [self.value(i, j) for i in range(self.width)]
res = res + "\n" + row
return res
def value(self, x, y):
return self.data[x * self.width + y]
def neighbours(self, x, y):
neighbCoords = [(i, j)
for i in range(x - 1, x + 2) if i >= 0 and i < self.width
for j in range(y - 1, y + 2) if j >= 0 and j < self.height
]
return [self.value(neighbCoord[0], neighbCoord[1])
for neighbCoord in neighbCoords if neighbCoord != (x, y) ]
# returns the new value of a given cell
def nextValue(self, x, y):
neighbsArr = self.neighbours(x, y)
liveNeighbs = 0
for neighb in neighbsArr:
if (neighb):
liveNeighbs = liveNeighbs + 1
if(self.value(x, y)):
if (liveNeighbs <= 1):
return 0 # underpopulation
else:
if (liveNeighbs <= 3):
return 1 # lives
else:
return 0 # overpopulation
else:
if (liveNeighbs == 3):
return 1 # reproduction
else:
return 0 # dies
# update the board data in place
def step(self):
self.data = [self.nextValue(x, y) for x in range(self.width) for y in range(self.height)]
# now the displaying part
ugfx_helper.init()
ugfx.clear()
grid_size = 5
grid_width = round(ugfx.width() / grid_size)
grid_height = round(ugfx.height() / grid_size)
alive_colours = [ugfx.WHITE, ugfx.GRAY, ugfx.BLUE, ugfx.RED, ugfx.GREEN, ugfx.YELLOW, ugfx.ORANGE]
dead_colour = ugfx.BLACK
def displayCell(x, y, alive):
if(alive):
colour = alive_colours[random.randrange(len(alive_colours))]
else:
colour = dead_colour
ugfx.area(x*grid_size, y*grid_size, grid_size, grid_size, colour)
def displayBoard(board):
coords = [(x, y) for x in range(board.width) for y in range(board.height)]
for (x, y) in coords:
displayCell(x, y, board.value(x, y))
board = Board(grid_width, grid_height)
while True:
displayBoard(board)
board.step()
#time.sleep(1)
sleep.wfi()
if buttons.is_triggered(Buttons.BTN_Menu):
break
ugfx.clear()
app.restart_to_default()

View File

@ -1,27 +0,0 @@
"""This is a simple hello world app"""
___title___ = "Hello World"
___license___ = "MIT"
___dependencies___ = ["sleep", "app"]
___categories___ = ["EMF"]
import ugfx, sleep, app
from tilda import Buttons
# initialize screen
ugfx.init()
ugfx.clear()
# show text
ugfx.text(5, 5, "Hello World!!", ugfx.BLACK)
# waiting until a button has been pressed
while (not Buttons.is_pressed(Buttons.BTN_A)) and (not Buttons.is_pressed(Buttons.BTN_B)) and (not Buttons.is_pressed(Buttons.BTN_Menu)):
sleep.wfi()
# closing
ugfx.clear()
app.restart_to_default()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

View File

@ -1,266 +0,0 @@
"""Camp Holland app"""
___title___ = "Holland"
___license___ = "MIT"
___dependencies___ = ["app", "sim800", "ugfx_helper"]
___categories___ = ["Villages"]
___bootstrapped___ = False
from app import *
from dialogs import *
import ugfx
import ugfx_helper
from machine import Neopix
def show_screen(color1, color2, text, text2="", flip=False):
if flip:
ugfx.orientation(90)
ugfx.clear(ugfx.html_color(color1))
ugfx.set_default_font(ugfx.FONT_NAME)
ugfx.text(0, 100, text, ugfx.html_color(color2))
ugfx.set_default_font(ugfx.FONT_SMALL)
ugfx.text(0, 200, text2, ugfx.html_color(color2))
if flip:
ugfx.orientation(270)
def show_vip(inv):
if (inv):
show_screen(0xFFFFFF, 0xFFA400, "Dutch VIP", "", True)
else:
show_screen(0xFFA400, 0xFFFFFF, "Dutch VIP", "", True)
def show_flag():
ugfx.display_image(0, 0, "holland/nederland.png")
def show_boot():
ugfx.display_image(0, 0, "holland/start.png")
ugfx_helper.init()
ugfx.clear()
show_boot()
import sim800
import time
from tilda import Buttons
sim800.poweron()
n = Neopix()
vip = False
vip_inv = False
strobe = False
def cbButtonA(button_id):
global vip
vip = False
show_flag()
def cbButtonB(button_id):
global vip
vip = True
show_vip(vip_inv)
def load():
global vip
vip = False
show_screen(0x000000, 0xFFFFFF, "LOADING")
print("Copy AMR")
sim800.fscreate("REC\\1.AMR")
f = open('holland/wilhelmus.amr', 'r')
data = f.read(256)
c = len(data)
sim800.fswrite("REC\\1.AMR", data, True)
pr = c
while (c>0):
data = f.read(256)
c = len(data)
sim800.fswrite("REC\\1.AMR", data, False)
pr = pr + c
show_screen(0x000000, 0xFFFFFF, "LOADING", str(pr))
print(str(pr))
f.close()
show_screen(0x000000, 0xFFFFFF, "DONE")
wilhelmus = (
("D", 300), ("G", 300), ("G", 300), ("A", 300), ("B", 300), ("C2", 300), ("A", 300), ("B", 300), ("A", 300), ("B", 300), ("C2", 300), ("B", 300), ("A", 300), ("G", 300), ("A", 600), ("G", 600), ("D", 300),
("G", 300), ("G", 300), ("A", 300), ("B", 300), ("C2", 300), ("A", 300), ("B", 300), ("A", 300), ("B", 300), ("C", 300), ("B", 300), ("A", 600), ("G", 600), ("A", 600), ("G", 600), ("B", 300), ("C", 300),
)
freq = {
"C": 2616,
"D": 2936,
"E": 3296,
"F": 3492,
"G": 3920,
"A": 4400,
"B": 4938,
"C2": 5322,
}
def cbButtonMenu(button_id):
restart_to_default()
def cbButtonCall(button_id):
sim800.speakervolume(100)
show_screen(0x000000, 0xFFFFFF, "TONE")
for note, length in wilhelmus:
sim800.playtone(freq.get(note, 9000), length, False)
def cbButton1(button_id):
global vip
vip = False
ugfx.display_image(0, 0, "holland/eu.png")
def cbButton2(button_id):
sim800.speakervolume(100)
sim800.stopplayback()
show_screen(0x000000, 0xFFFFFF, "PLAY")
a = sim800.startplayback(1,0,100)
if not a:
sim800.fsrm("REC\\1.AMR")
sim800.fsrm("REC\\2.AMR")
sim800.fsrm("REC\\3.AMR")
load()
show_screen(0x000000, 0xFFFFFF, "PLAY")
sim800.startplayback(1,0,100)
def cbButton3(button_id):
show_screen(0x000000, 0xFFFFFF, "STOP")
sim800.stopplayback()
def cbButton4(button_id):
global vip
vip = False
ugfx.display_image(0, 0, "holland/otter.png")
def cbButton5(button_id):
n.display([0xFFFFFF, 0xFFFFFF])
def cbButton6(button_id):
n.display([0x000000, 0x000000])
def cbButton7(button_id):
global vip
vip = False
show_boot()
def cbButton8(button_id):
global strobe
strobe = True
def cbButton9(button_id):
global strobe
strobe = False
def cbButtonHash(button_id):
global vip
vip = False
ugfx.display_image(0, 0, "holland/brenno.png")
Buttons.enable_interrupt(
Buttons.BTN_Menu,
cbButtonMenu,
on_press=True,
on_release=False);
Buttons.enable_interrupt(
Buttons.BTN_Call,
cbButtonCall,
on_press=True,
on_release=False);
Buttons.enable_interrupt(
Buttons.BTN_A,
cbButtonA,
on_press=True,
on_release=False);
Buttons.enable_interrupt(
Buttons.BTN_B,
cbButtonB,
on_press=True,
on_release=False);
Buttons.enable_interrupt(
Buttons.BTN_1,
cbButton1,
on_press=True,
on_release=False);
Buttons.enable_interrupt(
Buttons.BTN_2,
cbButton2,
on_press=True,
on_release=False);
Buttons.enable_interrupt(
Buttons.BTN_3,
cbButton3,
on_press=True,
on_release=False);
Buttons.enable_interrupt(
Buttons.BTN_4,
cbButton4,
on_press=True,
on_release=False);
Buttons.enable_interrupt(
Buttons.BTN_5,
cbButton5,
on_press=True,
on_release=False);
Buttons.enable_interrupt(
Buttons.BTN_6,
cbButton6,
on_press=True,
on_release=False);
Buttons.enable_interrupt(
Buttons.BTN_7,
cbButton7,
on_press=True,
on_release=False);
Buttons.enable_interrupt(
Buttons.BTN_8,
cbButton8,
on_press=True,
on_release=False);
Buttons.enable_interrupt(
Buttons.BTN_9,
cbButton9,
on_press=True,
on_release=False);
Buttons.enable_interrupt(
Buttons.BTN_Hash,
cbButtonHash,
on_press=True,
on_release=False);
vip = True
aaa = False
while True:
if vip_inv:
vip_inv = False
else:
vip_inv = True
if vip:
show_vip(vip_inv)
if strobe:
if aaa:
n.display([0xFFA500, 0xFFA500])
aaa = False
else:
n.display([0x000000, 0x000000])
aaa = True
if not vip:
time.sleep(0.1)
else:
time.sleep(0.1)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

View File

@ -1,51 +0,0 @@
"""This app connects to the Hologram service via GPRS displays recieved data on the screen and sets the neopixles"""
___title___ = "Hologram Demo"
___license___ = "MIT"
___dependencies___ = ["app", "sim800"]
___categories___ = ["EMF", "System"]
___bootstrapped___ = False
#import ugfx, os, time, sleep, app, sim800
import ugfx, app, sim800
import os
from tilda import Buttons
from time import sleep
from machine import Neopix
n = Neopix()
ugfx.init()
ugfx.clear()
ugfx.set_default_font(ugfx.FONT_FIXED)
def callback(data):
payload=data.decode("utf-8")
ugfx.Label(5, 100, 240, 15, payload)
colour = int(payload)
n.display([colour,colour])
print('Launching Hologram Demo')
ugfx.Label(5, 20, 240, 15, "Starting....")
sim800.setup_gprs()
ugfx.Label(5, 20, 240, 15, "GPRS Ready")
sim800.connect_gprs('hologram')
ugfx.Label(5, 40, 240, 15, "GPRS Connected")
sim800.start_server(4010, callback)
ugfx.Label(5, 60, 240, 15, "Server Started")
ugfx.Label(5, 300, 240, 15, "** Hold A or B or MENU to exit **")
while (not Buttons.is_pressed(Buttons.BTN_A)) and (not Buttons.is_pressed(Buttons.BTN_B)) and (not Buttons.is_pressed(Buttons.BTN_Menu)):
sleep(2)
ugfx.clear()
ugfx.Label(5, 20, 240, 15, "Stopping....")
sim800.stop_server()
sim800.stop_gprs()
app.restart_to_default()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

View File

@ -1,125 +0,0 @@
"""Default homescreen
Hackedup awful code for a london aerospace themed badge
"""
___name___ = "Aerospace Badge"
___license___ = "MIT"
___categories___ = ["Homescreens"]
___dependencies___ = ["homescreen", "wifi", "http", "ugfx_helper", "sleep"]
___launchable___ = False
import ugfx, random, time, wifi, http, math
from tilda import LED, Buttons
from machine import Neopix
from homescreen import *
import time
cycle = 0
#colourList = [0xff0000,0x00ff00]
colourList = [0xFF0000, 0xFFFFFF, 0x00FF00, 0x0000FF, 0xFFF000, 0xD800FF, 0xFF008F, 0x00FFF7]
n = Neopix()
# We ❤️ our sponsors
ugfx.display_image(0, 0, "home_aerospace/aerospace-logo.png")
wait = 5
while wait:
wait-=1
sleep_or_exit(0.5)
def ledChange():
colourNum1 = colourList[random.randint(0,len(colourList)-1)]
colourNum2 = colourList[random.randint(0,len(colourList)-1)]
while colourNum1 == colourNum2:
colourNum2 = colourList[random.randint(0,len(colourList)-1)]
n.display([colourNum1,colourNum2])
# Padding for name
intro_height = 30
intro_text = "London Aerospace"
intro_width = 200
intro_position_left = 0
name_height = 60
status_height = 30
info_height = 30
tick = 0
logo_path = "home_aerospace/aerospace-logo.png"
logo_height = 250
logo_width = 250
aerospace_text = "London Aerospace Yo"
# Maximum length of name before downscaling
max_name = 8
# Background stuff
init()
ugfx.clear(ugfx.html_color(0xFFFFFF))
# Colour stuff
style = ugfx.Style()
style.set_enabled([ugfx.BLACK, ugfx.html_color(0xFFFFFF), ugfx.html_color(0xFFFFFF), ugfx.html_color(0xFFFFFF)])
style.set_background(ugfx.html_color(0xFFFFFF))
ugfx.set_default_style(style)
# Draw for people to see
ugfx.orientation(90)
# Logo stuff
ugfx.display_image(
int((ugfx.width() - logo_width) / 2),
int((ugfx.height() - logo_height) / 2 - 20),
logo_path
)
# Draw introduction
ugfx.set_default_font(ugfx.FONT_TITLE)
intro_object = ugfx.Label(0, ugfx.height() - name_height - intro_height, ugfx.width(), intro_height, intro_text, justification=ugfx.Label.CENTER)
# Process name
name_setting = name("Set your name in the settings app")
if len(name_setting) <= max_name:
ugfx.set_default_font(ugfx.FONT_NAME)
else:
ugfx.set_default_font(ugfx.FONT_MEDIUM_BOLD)
# Draw name
ugfx.Label(0, ugfx.height() - name_height, ugfx.width(), name_height, name_setting, justification=ugfx.Label.CENTER)
# Draw for wearer to see
ugfx.orientation(270)
# Title
ugfx.set_default_font(ugfx.FONT_TITLE)
# info
ugfx.set_default_font(ugfx.FONT_SMALL)
status = ugfx.Label(0, ugfx.height() - 30, ugfx.width(), status_height, "", justification=ugfx.Label.CENTER)
status.text('BATTERY INCOMING')
# update loop
while True:
text = "";
if math.fmod(tick, 100) == 0:
value_wifi_strength = wifi_strength()
value_battery = battery()
if value_wifi_strength:
text += "Wi-Fi: %s%%, " % int(value_wifi_strength)
if value_battery:
text += "Battery: %s%%" % int(value_battery)
status.text(text)
tick +=1
# if intro_position_left > -intro_width:
# intro_position_left -= 1
# intro_object.x(
# intro_position_left
# )
# else:
# intro_object.x(0)
# intro_position_left = 0
ledChange()
sleep_or_exit(0.05)

View File

@ -1,98 +0,0 @@
"""Default homescreen
This is the default homescreen for the Tilda Mk4.
It gets automatically installed when a badge is
newly activated or reset.
"""
___title___ = "Homescreen (Default)"
___license___ = "MIT"
___categories___ = ["Homescreens"]
___dependencies___ = ["homescreen", "shared/logo.png", "shared/sponsors.png"]
___launchable___ = False
___bootstrapped___ = True
import ugfx
from homescreen import *
import time
from tilda import Buttons
# We ❤️ our sponsors
init()
ugfx.display_image(0, 0, "shared/sponsors.png")
wait_until = time.ticks_ms() + 3000
while time.ticks_ms() < wait_until:
time.sleep(0.1)
if Buttons.is_pressed(Buttons.BTN_A) or Buttons.is_pressed(Buttons.BTN_B) or Buttons.is_pressed(Buttons.BTN_Menu):
break
# Padding for name
intro_height = 30
intro_text = "Hi! I'm"
name_height = 60
status_height = 20
info_height = 30
logo_path = "shared/logo.png"
logo_height = 150
logo_width = 56
# Maximum length of name before downscaling
max_name = 8
# Background stuff
ugfx.clear(ugfx.html_color(0x800080))
# Colour stuff
style = ugfx.Style()
style.set_enabled([ugfx.WHITE, ugfx.html_color(0x800080), ugfx.html_color(0x800080), ugfx.html_color(0x800080)])
style.set_background(ugfx.html_color(0x800080))
ugfx.set_default_style(style)
# Logo stuff
ugfx.display_image(
int((ugfx.width() - logo_width) / 2),
int((ugfx.height() - logo_height) / 2),
logo_path
)
# Draw for people to see
ugfx.orientation(90)
# Draw introduction
ugfx.set_default_font(ugfx.FONT_TITLE)
ugfx.Label(0, ugfx.height() - name_height - intro_height, ugfx.width(), intro_height, intro_text, justification=ugfx.Label.CENTER)
# Process name
name_setting = name("Set your name in the settings app")
if len(name_setting) <= max_name:
ugfx.set_default_font(ugfx.FONT_NAME)
else:
ugfx.set_default_font(ugfx.FONT_MEDIUM_BOLD)
# Draw name
ugfx.Label(0, ugfx.height() - name_height, ugfx.width(), name_height, name_setting, justification=ugfx.Label.CENTER)
# Draw for wearer to see
ugfx.orientation(270)
# Title
ugfx.set_default_font(ugfx.FONT_TITLE)
ugfx.Label(0, ugfx.height() - info_height * 2, ugfx.width(), info_height, "TiLDA Mk4", justification=ugfx.Label.CENTER)
# info
ugfx.Label(0, ugfx.height() - info_height, ugfx.width(), info_height, "Long Press MENU", justification=ugfx.Label.CENTER)
ugfx.set_default_font(ugfx.FONT_SMALL)
status = ugfx.Label(0, ugfx.height() - info_height * 2 - status_height, ugfx.width(), status_height, "", justification=ugfx.Label.CENTER)
# update loop
while True:
text = "";
value_wifi_strength = wifi_strength()
value_battery = battery()
if value_wifi_strength:
text += "Wi-Fi: %s%%, " % int(value_wifi_strength)
if value_battery:
text += "Battery: %s%%" % int(value_battery)
status.text(text)
sleep_or_exit(0.5)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

View File

@ -1,90 +0,0 @@
"""Amateur Radio homescreen
This is a modified version of the default homescreen that allows you to set a callsign
"""
___title___ = "Amateur Radio Homescreen"
___license___ = "MIT"
___categories___ = ["Homescreens"]
___dependencies___ = ["homescreen"]
import ugfx
from homescreen import *
import time
from tilda import Buttons
init()
# Padding for name
intro_height = 30
name_height = 60
status_height = 20
callsign_height = 50
info_height = 30
logo_path = "home_ham/emf_ham.png"
logo_width = 200
# Maximum length of name before downscaling
max_name = 8
# Background stuff
ugfx.clear(ugfx.html_color(0xffffff))
# Colour stuff
style = ugfx.Style()
style.set_enabled([ugfx.BLACK, ugfx.html_color(0xffffff), ugfx.html_color(0xffffff), ugfx.html_color(0xffffff)])
style.set_background(ugfx.html_color(0xffffff))
ugfx.set_default_style(style)
ugfx.orientation(90)
# Logo stuff
ugfx.display_image(
int((ugfx.width() - logo_width) / 2),
30,
logo_path
)
# Draw for people to see
# Draw introduction
ugfx.set_default_font(ugfx.FONT_TITLE)
# Process name
name_setting = name("Set your name in the settings app")
callsign_setting = callsign("Set your callsign in the settings app")
if len(name_setting) <= max_name:
ugfx.set_default_font(ugfx.FONT_NAME)
else:
ugfx.set_default_font(ugfx.FONT_MEDIUM_BOLD)
# Draw name
ugfx.Label(0, 220 ,ugfx.width(), name_height, name_setting, justification=ugfx.Label.CENTER)
# Title
if len(callsign_setting) <= max_name:
ugfx.set_default_font(ugfx.FONT_NAME)
else:
ugfx.set_default_font(ugfx.FONT_MEDIUM_BOLD)
# Draw callsign
ugfx.Label(0, 270, ugfx.width(), callsign_height, callsign_setting, justification=ugfx.Label.CENTER)
ugfx.set_default_font(ugfx.FONT_SMALL)
# Draw for wearer to see
ugfx.orientation(270)
status = ugfx.Label(0, 300, ugfx.width(), status_height, "", justification=ugfx.Label.CENTER)
# update loop
while True:
text = "";
value_wifi_strength = wifi_strength()
value_battery = battery()
if value_wifi_strength:
text += "Wi-Fi: %s%%, " % int(value_wifi_strength)
if value_battery:
text += "Battery: %s%%" % int(value_battery)
status.text(text)
sleep_or_exit(0.5)
app.restart_to_default()

View File

@ -1,93 +0,0 @@
"""PyCon homescreen
This is the default homescreen for the Tilda Mk4.
It gets automatically installed when a badge is
newly activated or reset.
"""
___title___ = "Homescreen (PyCon)"
___license___ = "MIT"
___categories___ = ["Homescreens"]
___dependencies___ = ["homescreen"]
___launchable___ = False
___bootstrapped___ = False
import ugfx
from homescreen import *
import time
from tilda import Buttons
init()
# Padding for name
intro_height = 30
intro_text = "Hi! I'm"
name_height = 60
status_height = 20
info_height = 30
logo_path = "home_pycon/python_single.png"
logo_height = 82
logo_width = 55
# Maximum length of name before downscaling
max_name = 8
# Background stuff
bg_color = 0xfecb2f
ugfx.clear(ugfx.html_color(bg_color))
# Colour stuff
style = ugfx.Style()
style.set_enabled([ugfx.BLACK, ugfx.html_color(bg_color), ugfx.html_color(bg_color), ugfx.html_color(bg_color)])
style.set_background(ugfx.html_color(bg_color))
ugfx.set_default_style(style)
# Draw for people to see
ugfx.orientation(90)
# Logo stuff
ugfx.display_image(
int((ugfx.width() - logo_width) / 2),
int((ugfx.height() - logo_height) / 2),
logo_path
)
# Draw introduction
ugfx.set_default_font(ugfx.FONT_TITLE)
ugfx.Label(0, ugfx.height() - name_height - intro_height, ugfx.width(), intro_height, intro_text, justification=ugfx.Label.CENTER)
# Process name
name_setting = name("Set your name in the settings app")
if len(name_setting) <= max_name:
ugfx.set_default_font(ugfx.FONT_NAME)
else:
ugfx.set_default_font(ugfx.FONT_MEDIUM_BOLD)
# Draw name
ugfx.Label(0, ugfx.height() - name_height, ugfx.width(), name_height, name_setting, justification=ugfx.Label.CENTER)
# Draw for wearer to see
ugfx.orientation(270)
# Title
ugfx.set_default_font(ugfx.FONT_TITLE)
ugfx.Label(0, ugfx.height() - info_height * 2, ugfx.width(), info_height, "TiLDA Mk4", justification=ugfx.Label.CENTER)
# info
ugfx.Label(0, ugfx.height() - info_height, ugfx.width(), info_height, "Long Press MENU", justification=ugfx.Label.CENTER)
ugfx.set_default_font(ugfx.FONT_SMALL)
status = ugfx.Label(0, ugfx.height() - info_height * 2 - status_height, ugfx.width(), status_height, "", justification=ugfx.Label.CENTER)
# update loop
while True:
text = "";
value_wifi_strength = wifi_strength()
value_battery = battery()
if value_wifi_strength:
text += "Wi-Fi: %s%%, " % int(value_wifi_strength)
if value_battery:
text += "Battery: %s%%" % int(value_battery)
status.text(text)
sleep_or_exit(0.5)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 963 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

View File

@ -1,88 +0,0 @@
"""Stratum 0 homescreen
This is the Stratum 0 flavored homescreen for the Tilda Mk4.
"""
___title___ = "Homescreen (Stratum 0)"
___license___ = "MIT"
___categories___ = ["Homescreens"]
___dependencies___ = ["homescreen"]
import ugfx
from homescreen import *
import time
from tilda import Buttons
# Init Homescreen
init()
# Padding for name
intro_height = 30
intro_text = "Moin! I'm"
name_height = 60
status_height = 20
info_height = 30
logo_path = "home_stratum0/logo.png"
logo_height = 106
logo_width = 58
# Maximum length of name before downscaling
max_name = 8
# Background stuff
ugfx.clear(ugfx.html_color(0x000000))
# Colour stuff
style = ugfx.Style()
style.set_enabled([ugfx.WHITE, ugfx.html_color(0x000000), ugfx.html_color(0x000000), ugfx.html_color(0x000000)])
style.set_background(ugfx.html_color(0x000000))
ugfx.set_default_style(style)
# Logo stuff
ugfx.orientation(90)
ugfx.display_image(
int((ugfx.width() - logo_width) / 2),
int((ugfx.height() - logo_height) / 2),
logo_path
)
# Draw for people to see
ugfx.orientation(90)
# Draw introduction
ugfx.set_default_font(ugfx.FONT_TITLE)
ugfx.Label(0, ugfx.height() - name_height - intro_height, ugfx.width(), intro_height, intro_text, justification=ugfx.Label.CENTER)
# Process name
name_setting = name("Set your name in the settings app")
if len(name_setting) <= max_name:
ugfx.set_default_font(ugfx.FONT_NAME)
else:
ugfx.set_default_font(ugfx.FONT_MEDIUM_BOLD)
# Draw name
ugfx.Label(0, ugfx.height() - name_height, ugfx.width(), name_height, name_setting, justification=ugfx.Label.CENTER)
# Draw for wearer to see
ugfx.orientation(270)
# Title
ugfx.set_default_font(ugfx.FONT_TITLE)
ugfx.Label(0, ugfx.height() - info_height * 2, ugfx.width(), info_height, "TiLDA Mk4", justification=ugfx.Label.CENTER)
# info
ugfx.Label(0, ugfx.height() - info_height, ugfx.width(), info_height, "Long Press MENU", justification=ugfx.Label.CENTER)
ugfx.set_default_font(ugfx.FONT_SMALL)
status = ugfx.Label(0, ugfx.height() - info_height * 2 - status_height, ugfx.width(), status_height, "", justification=ugfx.Label.CENTER)
# WiFi/Battery update loop
while True:
text = "";
value_wifi_strength = wifi_strength()
value_battery = battery()
if value_wifi_strength:
text += "Wi-Fi: %s%%, " % int(value_wifi_strength)
if value_battery:
text += "Battery: %s%%" % int(value_battery)
status.text(text)
sleep_or_exit(0.5)
app.restart_to_default()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

View File

@ -1,175 +0,0 @@
"""Trans homescreen
A version of the home screen that has a trans flag.
Press 0 to go back to normal or 8 to show the flag.
Hold * to activate all LEDs for use as a torch.
"""
___title___ = "Homescreen (Trans)"
___license___ = "MIT"
___categories___ = ["Homescreens"]
___dependencies___ = ["homescreen", "shared/logo.png"]
___launchable___ = False
___bootstrapped___ = False
import ugfx
from homescreen import *
import time
from tilda import Buttons
from machine import Pin
from machine import Neopix
torch = Pin(Pin.GPIO_FET)
neo = Neopix()
init()
# Padding for name
intro_height = 30
intro_text = "Hi! I'm"
name_height = 64
status_height = 20
info_height = 30
logo_path = "shared/logo.png"
trans_logo_path = "home_trans/logo.png"
logo_height = 150
logo_width = 56
# Maximum length of name before downscaling
max_name = 8
torch_on = False
# Background stuff
ugfx.clear(ugfx.html_color(0x55cdfc))
# Colour stuff
style = ugfx.Style()
style.set_enabled([ugfx.BLACK, ugfx.html_color(0x55cdfc), ugfx.html_color(0x55cdfc), ugfx.html_color(0x55cdfc)])
style.set_background(ugfx.html_color(0x55cdfc))
ugfx.set_default_style(style)
ugfx.display_image(0, 0, "home_trans/trans.png")
# Logo stuff
ugfx.display_image(
int((ugfx.width() - logo_width) / 2),
int((ugfx.height() - logo_height) / 2)+9,
trans_logo_path
)
# Draw for people to see
ugfx.orientation(90)
# Draw introduction
style.set_enabled([ugfx.BLACK, ugfx.html_color(0xf8b0be), ugfx.html_color(0xf8b0be), ugfx.html_color(0xf8b0be)])
style.set_background(ugfx.html_color(0xf8b0be))
ugfx.set_default_style(style)
ugfx.set_default_font(ugfx.FONT_TITLE)
ugfx.Label(0, ugfx.height() - name_height - intro_height, ugfx.width(), intro_height, intro_text, justification=ugfx.Label.CENTER)
# Prepare to draw name
style.set_enabled([ugfx.BLACK, ugfx.html_color(0x55cdfc), ugfx.html_color(0x55cdfc), ugfx.html_color(0x55cdfc)])
style.set_background(ugfx.html_color(0x55cdfc))
ugfx.set_default_style(style)
# Process name
name_setting = name("Set your name in the settings app")
if len(name_setting) <= max_name:
ugfx.set_default_font(ugfx.FONT_NAME)
else:
ugfx.set_default_font(ugfx.FONT_MEDIUM_BOLD)
# Draw name
ugfx.Label(0, ugfx.height() - name_height, ugfx.width(), name_height, name_setting, justification=ugfx.Label.CENTER)
# Draw for wearer to see
ugfx.orientation(270)
ugfx.set_default_font(ugfx.FONT_SMALL)
status = ugfx.Label(0, ugfx.height() - status_height, ugfx.width(), status_height, "", justification=ugfx.Label.LEFT)
def draw_badge():
style.set_enabled([ugfx.WHITE, ugfx.html_color(0x800080), ugfx.html_color(0x800080), ugfx.html_color(0x800080)])
style.set_background(ugfx.html_color(0x800080))
ugfx.clear(ugfx.html_color(0x800080))
ugfx.set_default_style(style)
# Logo stuff
ugfx.display_image(
int((ugfx.width() - logo_width) / 2),
int((ugfx.height() - logo_height) / 2),
logo_path
)
# Draw for people to see
ugfx.orientation(90)
# Draw introduction
ugfx.set_default_font(ugfx.FONT_TITLE)
ugfx.Label(0, ugfx.height() - name_height - intro_height, ugfx.width(), intro_height, intro_text, justification=ugfx.Label.CENTER)
# Process name
name_setting = name("Set your name in the settings app")
if len(name_setting) <= max_name:
ugfx.set_default_font(ugfx.FONT_NAME)
else:
ugfx.set_default_font(ugfx.FONT_MEDIUM_BOLD)
# Draw name
ugfx.Label(0, ugfx.height() - name_height, ugfx.width(), name_height, name_setting, justification=ugfx.Label.CENTER)
# Draw for wearer to see
ugfx.orientation(270)
ugfx.set_default_font(ugfx.FONT_SMALL)
status = ugfx.Label(0, ugfx.height() - status_height, ugfx.width(), status_height, "", justification=ugfx.Label.LEFT)
def draw_trans():
style.set_enabled([ugfx.BLACK, ugfx.html_color(0x55cdfc), ugfx.html_color(0x55cdfc), ugfx.html_color(0x55cdfc)])
style.set_background(ugfx.html_color(0x55cdfc))
ugfx.set_default_style(style)
ugfx.display_image(0, 0, "home_trans/trans.png")
# Logo stuff
ugfx.display_image(
int((ugfx.width() - logo_width) / 2),
int((ugfx.height() - logo_height) / 2)+9,
trans_logo_path
)
# Draw for people to see
ugfx.orientation(90)
# Draw introduction
style.set_enabled([ugfx.BLACK, ugfx.html_color(0xf8b0be), ugfx.html_color(0xf8b0be), ugfx.html_color(0xf8b0be)])
style.set_background(ugfx.html_color(0xf8b0be))
ugfx.set_default_style(style)
ugfx.set_default_font(ugfx.FONT_TITLE)
ugfx.Label(0, ugfx.height() - name_height - intro_height, ugfx.width(), intro_height, intro_text, justification=ugfx.Label.CENTER)
# Prepare to draw name
style.set_enabled([ugfx.BLACK, ugfx.html_color(0x55cdfc), ugfx.html_color(0x55cdfc), ugfx.html_color(0x55cdfc)])
style.set_background(ugfx.html_color(0x55cdfc))
ugfx.set_default_style(style)
# Process name
name_setting = name("Set your name in the settings app")
if len(name_setting) <= max_name:
ugfx.set_default_font(ugfx.FONT_NAME)
else:
ugfx.set_default_font(ugfx.FONT_MEDIUM_BOLD)
# Draw name
ugfx.Label(0, ugfx.height() - name_height, ugfx.width(), name_height, name_setting, justification=ugfx.Label.CENTER)
# Draw for wearer to see
ugfx.orientation(270)
ugfx.set_default_font(ugfx.FONT_SMALL)
status = ugfx.Label(0, ugfx.height() - status_height, ugfx.width(), status_height, "", justification=ugfx.Label.LEFT)
# update loop
while True:
text = "";
value_battery = battery()
if value_battery:
text += "%s%%" % int(value_battery)
if Buttons.is_pressed(Buttons.BTN_Star):
if torch_on:
torch_on = False
torch.off()
neo.display([0,0])
else:
torch_on = True
torch.on()
neo.display([0xffffff,0xffffff])
if Buttons.is_pressed(Buttons.BTN_8):
draw_trans()
if Buttons.is_pressed(Buttons.BTN_0):
draw_badge()
status.text(text)
sleep_or_exit(0.5)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -1,23 +0,0 @@
"""Launcher for apps currently installed"""
___title___ = "Launcher"
___license___ = "MIT"
___categories___ = ["System", "Launcher"]
___dependencies___ = ["dialogs", "app", "ugfx_helper"]
___launchable___ = False
___bootstrapped___ = True
import ugfx_helper, ugfx
from app import *
from dialogs import *
ugfx_helper.init()
ugfx.clear()
options = [{"title": a.title, "app": a} for a in get_apps()]
option = prompt_option(options, none_text="Home", text="Select App to start")
if not option:
restart_to_default()
else:
option["app"].boot()

View File

@ -1,118 +0,0 @@
"""Model and Helpers for local apps
This is useful for the launcher and other apps.
"""
___license___ = "MIT"
___dependencies___ = ["metadata_reader", "ospath"]
from ospath import *
import os, machine
from metadata_reader import read_metadata
class App:
"""Models an app and provides some helper functions"""
def __init__(self, name):
self.name = name
self._attributes = None # Load lazily
@property
def folder_path(self):
return self.name
@property
def main_path(self):
return self.folder_path + "/main.py"
@property
def loadable(self):
return isfile(self.main_path)
@property
def description(self):
return self.get_attribute("doc")
@property
def title(self):
return self.get_attribute("title", self.name)
@property
def categories(self):
return self.get_attribute("categories")
def matches_category(self, target):
return target in self.categories
@property
def attributes(self):
if self._attributes == None:
try:
with open(self.main_path) as file:
self._attributes = read_metadata(file)
except OSError:
raise Exception("File %s not found in on badge" % self.main_path)
return self._attributes
def get_attribute(self, attribute, default=None):
if attribute in self.attributes:
return self.attributes[attribute]
return default
def boot(self):
write_launch_file(self.name)
machine.reset()
def __str__(self):
return self.title
def __repr__(self):
return "<App %s>" % (self.name)
def __eq__(self, other):
if isinstance(other, App):
return self.name == other.name
return False
def __hash__(self):
return hash(self.name)
_apps = None
def get_apps(category=None):
global _apps
if _apps == None:
_apps = []
for path in os.listdir():
if path.startswith(".") or (not isdir(path)) or path in ["lib", "shared", "upip"]:
continue
app = App(path)
if app.loadable:
_apps.append(app)
if category:
return [app for app in _apps if app.matches_category(category)]
return _apps
def uncache_apps():
global _apps
_apps = None
_categories = None
def get_categories():
global _categories
if _categories == None:
_categories = set()
for app in get_apps():
_categories.update(app.categories)
return _categories
def write_launch_file(app, file = "once.txt"):
with open(file, "wt") as file:
file.write(app)
file.flush()
os.sync()
def restart_to_default():
write_launch_file("")
machine.reset()

View File

@ -1,106 +0,0 @@
"""Library to interact with the badge store"""
___license___ = "MIT"
___dependencies___ = ["http", "ospath"]
from ospath import *
from http import *
import hashlib, binascii
class BadgeStore:
def __init__(self, url = "http://badgeserver.emfcamp.org/2018", repo="emfcamp/Mk4-Apps", ref="master"):
self.url = url
self.repo = repo
self.ref = ref
self._apps = None
def get_all_apps(self):
if not self._apps:
self._apps = self._call("apps")
return self._apps
def get_apps(self, category):
return self.get_all_apps()[category]
def get_categories(self):
return self.get_all_apps().keys()
def get_app(self, app):
return self._call("app", {"app": app})
def get_prs(self):
return self._call("prs")
def install(self, apps):
return self._create_installers(self._call("install", {"apps": ",".join(apps)}))
def update(self, apps):
return self._create_installers(self._call("update", {"apps": ",".join(apps)}))
def bootstrap(self):
return self._create_installers(self._call("bootstrap"))
def _call(self, command, params = {}):
params["repo"] = self.repo
params["ref"] = self.ref
with get("%s/%s" % (self.url, command), params=params).raise_for_status() as response:
return response.json() # todo: error handling
def _create_installers(self, files):
installers = []
url = "%s/download" % (self.url)
for path, hash in files.items():
if hash == get_hash(path):
continue
params = {"repo": self.repo, "ref": self.ref, "path": path}
installers.append(Installer(path, url, params, hash))
return installers
def _is_file_up_to_date(self, path, hash):
return hash == _get_hash(path)
TEMP_FILE = ".tmp.download"
class Installer:
def __init__(self, path, url, params, hash):
self.path = path
self.url = url
self.params = params
self.hash = hash
def download(self):
count = 0
while get_hash(TEMP_FILE) != self.hash:
count += 1
if count > 5:
try:
os.remove(TEMP_FILE)
except:
pass
raise OSError("Aborting download of %s after 5 unsuccessful attempts" % self.path)
try:
get(self.url, params=self.params).raise_for_status().download_to(TEMP_FILE)
except OSError as e:
if "404" in str(e):
raise e
pass
try:
os.remove(self.path)
except OSError:
pass
makedirs(dirname(self.path))
os.rename(TEMP_FILE, self.path)
def get_hash(path):
if not isfile(path):
return None
with open(path, "rb") as file:
sha256 = hashlib.sha256()
buf = file.read(128)
while len(buf) > 0:
sha256.update(buf)
buf = file.read(128)
return str(binascii.hexlify(sha256.digest()), "utf8")[:10]

View File

@ -1,113 +0,0 @@
"""Convenience methods for dealing with the TiLDA buttons
Pins are decined in tilda.Buttons.BTN_XYZ:
BTN_0 - BTN_9, BTN_Hash, BTN_Star
BTN_A, BTN_B
BTN_Call, BTN_End
BTN_Menu
JOY_Center, JOY_Down, JOY_Left, JOY_Right, JOY_Up
"""
___license___ = "MIT"
import machine, time, tilda
# Convenience
Buttons = tilda.Buttons
_tilda_bounce = {}
def is_pressed(button):
return tilda.Buttons.is_pressed(button)
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 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
time.sleep_ms(interval)
# Wait until button is released again
while is_pressed(button):
time.sleep_ms(1)
_tilda_bounce[button] = time.ticks_ms() + interval
return True
# The following functions might not work
def has_interrupt(button):
return False;
# todo: re-enable
#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):
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
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 = machine.ExtInt.IRQ_RISING_FALLING
else:
if pin.pull() == machine.Pin.PULL_DOWN:
mode = machine.ExtInt.IRQ_RISING if on_press else machine.ExtInt.IRQ_FALLING
else:
mode = machine.ExtInt.IRQ_FALLING if on_press else machine.ExtInt.IRQ_RISING
_tilda_interrupts[button] = {
"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]
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)

View File

@ -1,88 +0,0 @@
"""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.Database() 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, **kwargs):
with Database(*args, **kwargs) as db:
return db.get(key, default_value)
def set(key, value, *args, **kwargs):
with Database(*args, **kwargs) as db:
return db.set(key, value)
def delete(key, *args, **kwargs):
with Database(*args, **kwargs) as db:
return db.delete(key)

View File

@ -1,325 +0,0 @@
"""Some basic UGFX powered dialogs"""
___license___ = "MIT"
___dependencies___ = ["buttons", "sleep"]
import ugfx, buttons, sleep
from buttons import Buttons
import time
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);
FONT_SMALL = 0 #todo: find correct values
FONT_MEDIUM_BOLD = 0
def notice(text, title="TiLDA", close_text="Close", font=FONT_SMALL, style=None):
prompt_boolean(text, title = title, true_text = close_text, false_text = None, font=font, style=style)
def prompt_boolean(text, title="TiLDA", true_text="Yes", false_text="No", font=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(FONT_MEDIUM_BOLD)
width = ugfx.width() - 10
height = ugfx.height() - 10
window = ugfx.Container(5, 5, width, height)
window.show()
ugfx.set_default_font(font)
window.text(5, 5, title, TILDA_COLOR)
window.line(0, 25, width, 25, 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, justification=4)
ugfx.set_default_font(FONT_MEDIUM_BOLD)
button_yes = ugfx.Button(5, height - 40, width // 2 - 10 if false_text else width - 15, 30 , true_text, parent=window)
button_no = ugfx.Button(width // 2, height - 40, width // 2 - 10, 30 , false_text, parent=window) if false_text else None
# Find newlines in label text to scroll.
def find_all(a_str, sub):
start = 0
while True:
start = a_str.find(sub, start)
if start == -1: return
yield start + 1 # Trap: \n becomes a single character, not 2.
start += len(sub) # use start += 1 to find overlapping matches
new_line_pos = [0] + list(find_all(text, '\n'))
text_scroll_offset = 0
try:
#button_yes.attach_input(ugfx.BTN_A,0) # todo: re-enable once working
#if button_no: button_no.attach_input(ugfx.BTN_B,0)
window.show()
while True:
sleep.wfi()
if buttons.is_triggered(buttons.Buttons.BTN_A): return True
if buttons.is_triggered(buttons.Buttons.BTN_B): return False
# Allow scrolling by new lines.
if buttons.is_triggered(buttons.Buttons.JOY_Down):
if text_scroll_offset < len(new_line_pos)-1:
text_scroll_offset = text_scroll_offset + 1
label.text(text[new_line_pos[text_scroll_offset]:])
if buttons.is_triggered(buttons.Buttons.JOY_Up):
if (text_scroll_offset > 0):
text_scroll_offset=text_scroll_offset - 1
label.text(text[new_line_pos[text_scroll_offset]:])
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", font=FONT_MEDIUM_BOLD, style=default_style_badge, numeric=False):
"""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(0, 0, ugfx.width(), ugfx.height())
if false_text:
true_text = "A: " + true_text
false_text = "B: " + false_text
ugfx.set_default_font(FONT_MEDIUM_BOLD)
kb = ugfx.Keyboard(0, ugfx.height()//2, ugfx.width(), ugfx.height()//2, parent=window)
edit = ugfx.Textbox(2, ugfx.height()//2-60, ugfx.width()-7, 25, text = init_text, parent=window)
ugfx.set_default_font(FONT_SMALL)
button_yes = ugfx.Button(2, ugfx.height()//2-30, ugfx.width()//2-6, 25 , true_text, parent=window)
button_no = ugfx.Button(ugfx.width()//2+2, ugfx.height()//2-30, ugfx.width()//2-6, 25 , false_text, parent=window) if false_text else None
ugfx.set_default_font(font)
label = ugfx.Label(ugfx.width()//10, ugfx.height()//10, ugfx.width()*4//5, ugfx.height()*2//5-90, description, parent=window)
try:
window.show()
# edit.set_focus() todo: do we need this?
while True:
sleep.wfi()
ugfx.poll()
if buttons.is_triggered(buttons.Buttons.BTN_A): return edit.text()
if buttons.is_triggered(buttons.Buttons.BTN_B): return None
if buttons.is_triggered(buttons.Buttons.BTN_Menu): return edit.text()
handle_keypad(edit, numeric)
finally:
window.hide()
window.destroy()
button_yes.destroy()
if button_no: button_no.destroy()
label.destroy()
kb.destroy()
edit.destroy();
return
last_key = None
last_keytime = None
def handle_keypad(edit, numeric):
global last_key, last_keytime
threshold = 1000
keymap = {
buttons.Buttons.BTN_0: [" ", "0"],
buttons.Buttons.BTN_1: ["1"],
buttons.Buttons.BTN_2: ["a", "b", "c", "2"],
buttons.Buttons.BTN_3: ["d", "e", "f", "3"],
buttons.Buttons.BTN_4: ["g", "h", "i", "4"],
buttons.Buttons.BTN_5: ["j", "k", "l", "5"],
buttons.Buttons.BTN_6: ["m", "n", "o", "6"],
buttons.Buttons.BTN_7: ["p", "q", "r", "s", "7"],
buttons.Buttons.BTN_8: ["t", "u", "v", "8"],
buttons.Buttons.BTN_9: ["w", "x", "y", "z", "9"],
buttons.Buttons.BTN_Hash: ["#"],
buttons.Buttons.BTN_Star: ["*", "+"],
}
for key, chars in keymap.items():
if buttons.is_triggered(key):
if numeric:
edit.text(edit.text() + chars[-1])
elif key != last_key:
edit.text(edit.text() + chars[0])
else:
if last_keytime is None or (time.ticks_ms() - last_keytime) > threshold:
edit.text(edit.text() + chars[0])
else:
last_char = edit.text()[-1]
try:
last_index = chars.index(last_char)
except ValueError:
# not sure how we get here...
return
next_index = (last_index+1) % len(chars)
edit.text(edit.text()[:-1] + chars[next_index])
last_key = key
last_keytime = time.ticks_ms()
def prompt_option(options, index=0, text = None, 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(FONT_SMALL)
window = ugfx.Container(5, 5, ugfx.width() - 10, ugfx.height() - 10)
window.show()
list_y = 30
if title:
window.text(5, 5, title, TILDA_COLOR)
window.line(0, 25, ugfx.width() - 10, 25, ugfx.BLACK)
list_y = 30
if text:
list_y += 20
window.text(5, 30, text, ugfx.BLACK)
else:
window.text(5, 10, text, ugfx.BLACK)
options_list = ugfx.List(5, list_y, ugfx.width() - 24, 265 - list_y, parent = window)
options_list.disable_draw()
optnum = 1
for option in options:
if isinstance(option, dict) and option["title"]:
title = option["title"]
else:
title = str(option)
if optnum < 11:
# mod 10 to make 10th item numbered 0
options_list.add_item("{}: {}".format((optnum % 10),title))
else:
options_list.add_item(" {}".format(title))
optnum = optnum + 1
options_list.enable_draw()
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, 105 if none_text else 200, 30 , select_text, parent=window)
button_none = ugfx.Button(116, ugfx.height() - 50, 105, 30 , none_text, parent=window) if none_text else None
try:
while True:
sleep.wfi()
ugfx.poll()
# todo: temporary hack
#if (buttons.is_triggered(buttons.Buttons.JOY_Up)):
# index = max(index - 1, 0)
# options_list.selected_index(index)
#if (buttons.is_triggered(buttons.Buttons.JOY_Down)):
# index = min(index + 1, len(options) - 1)
# options_list.selected_index(index)
if buttons.is_triggered(buttons.Buttons.BTN_A) or buttons.is_triggered(buttons.Buttons.JOY_Center):
return options[options_list.selected_index()]
if button_none and buttons.is_triggered(buttons.Buttons.BTN_B): return None
if button_none and buttons.is_triggered(buttons.Buttons.BTN_Menu): return None
# These are indexes for selected_index, 1 means "First item", ie index 0. 0 is treated as if it were 10
button_nums = {
Buttons.BTN_1: 0,
Buttons.BTN_2: 1,
Buttons.BTN_3: 2,
Buttons.BTN_4: 3,
Buttons.BTN_5: 4,
Buttons.BTN_6: 5,
Buttons.BTN_7: 6,
Buttons.BTN_8: 7,
Buttons.BTN_9: 8,
Buttons.BTN_0: 9,
}
for key, num in button_nums.items():
if buttons.is_triggered(key):
# No need to check for too large an index; gwinListSetSelected validates this.
options_list.selected_index(num)
break
if buttons.is_triggered(Buttons.BTN_Hash):
# Page down
idx = options_list.selected_index() + 10
cnt = options_list.count()
if idx >= cnt:
idx = cnt - 1
options_list.selected_index(idx)
continue
if buttons.is_triggered(Buttons.BTN_Star):
# Page up
idx = options_list.selected_index() - 10
if idx < 0:
idx = 0
options_list.selected_index(idx)
continue
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, 5, title, TILDA_COLOR)
self.window.line(0, 25, ugfx.width() - 60, 25, ugfx.BLACK)
self.label = ugfx.Label(5, 40, self.window.width() - 15, 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 = 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.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()

View File

@ -1,16 +0,0 @@
"""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

@ -1,79 +0,0 @@
"""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 "sleep.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.wifi_strength()" (0-1, might return "None" if not connected)
* Display remaining battery "homescreen.battery()" (0-1)
"""
___license___ = "MIT"
___dependencies___ = ["database", "buttons", "app", "sleep", "ugfx_helper", "wifi", "sim800"]
import database, ugfx, random, buttons, tilda, sleep, ugfx_helper, wifi, time, sim800
from app import App
_state = None
def init(enable_menu_button = True):
global _state
_state = {"menu": False}
ugfx_helper.init()
if enable_menu_button:
pass
#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
# you're only using numbers and booleans
global _state
_state[key] = value
def clean_up():
pass
def time_as_string(seconds=False):
t = time.localtime()
if seconds:
return "%d:%02d:%02d" % (t[3], t[4], t[5])
return "%d:%02d" % (t[3], t[4]) #todo: add a setting for AM/PM mode
def sleep_or_exit(interval = 0.5):
# todo: do this better - check button multiple times and sleep for only a short while
if buttons.is_triggered(tilda.Buttons.BTN_Menu):
clean_up()
launcher = "launcher"
try:
with open("default_launcher.txt", "r") as dl:
launcher=dl.readline()
except OSError:
pass
App(launcher).boot()
sleep.sleep(interval)
def name(default = None):
return database.get("homescreen.name", default)
def callsign(default = None):
return database.get("homescreen.callsign", default)
# Strength in %, None if unavailable
def wifi_strength():
return wifi.get_strength()
# Charge in %, None if unavailable
def battery():
return sim800.batterycharge() # todo: fix me, we can get this from the sim800

View File

@ -1,267 +0,0 @@
"""HTTP library specially tied to TiLDAs functionality
Somewhat inspired by "request".
Current known issues:
* HTTPS is not supported
"""
___license___ = "MIT"
___dependencies___ = ["urlencode", "wifi"]
import usocket, ujson, os, time, gc, wifi, ussl
from urlencode import urlencode
"""Usage
from http_client import *
print(get("http://example.com").raise_for_status().content)
post("http://mydomain.co.uk/api/post", data="SOMETHING").raise_for_status().close() # If response is not consumed you need to close manually
# Or, if you prefer the with syntax:
with post("http://mydomain.co.uk/api/post", urlencoded="SOMETHING") as response:
response.raise_for_error() # No manual close needed
"""
SUPPORT_TIMEOUT = hasattr(usocket.socket, 'settimeout')
CONTENT_TYPE_JSON = 'application/json'
BUFFER_SIZE = 1024
class Response(object):
def __init__(self):
self.encoding = 'utf-8'
self.headers = {}
self.status = None
self.socket = None
self._content = None
# Hands the responsibility for a socket over to this reponse. This needs to happen
# before any content can be inspected
def add_socket(self, socket, content_so_far):
self.content_so_far = content_so_far
self.socket = socket
@property
def content(self, timeout=90):
start_time = time.time()
if not self._content:
if not self.socket:
raise OSError("Invalid response socket state. Has the content been downloaded instead?")
try:
if "Content-Length" in self.headers:
content_length = int(self.headers["Content-Length"])
elif "content-length" in self.headers:
content_length = int(self.headers["content-length"])
else:
raise Exception("No Content-Length")
self._content = self.content_so_far
del self.content_so_far
while len(self._content) < content_length:
buf = self.socket.recv(BUFFER_SIZE)
self._content += buf
if (time.time() - start_time) > timeout:
raise Exception("HTTP request timeout")
finally:
self.close()
return self._content;
@property
def text(self):
return str(self.content, self.encoding) if self.content else ''
# If you don't use the content of a Response at all you need to manually close it
def close(self):
if self.socket is not None:
self.socket.close()
self.socket = None
def json(self):
try:
return ujson.loads(self.text)
except ValueError as e:
print("Invalid JSON: %s" % self.text)
raise(e)
# Writes content into a file. This function will write while receiving, which avoids
# having to load all content into memory
def download_to(self, target, timeout=90):
start_time = time.time()
if not self.socket:
raise OSError("Invalid response socket state. Has the content already been consumed?")
try:
if "Content-Length" in self.headers:
remaining = int(self.headers["Content-Length"])
elif "content-length" in self.headers:
remaining = int(self.headers["content-length"])
else:
raise Exception("No Content-Length")
with open(target, 'wb') as f:
f.write(self.content_so_far)
remaining -= len(self.content_so_far)
del self.content_so_far
while remaining > 0:
buf = self.socket.recv(BUFFER_SIZE)
f.write(buf)
remaining -= len(buf)
if (time.time() - start_time) > timeout:
raise Exception("HTTP request timeout")
f.flush()
os.sync()
finally:
self.close()
def raise_for_status(self):
if 400 <= self.status < 500:
raise OSError('Client error: %s' % self.status)
if 500 <= self.status < 600:
raise OSError('Server error: %s' % self.status)
return self
# In case you want to use "with"
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, traceback):
self.close()
def open_http_socket(method, url, json=None, timeout=None, headers=None, data=None, params=None):
# This will immediately return if we're already connected, otherwise
# it'll attempt to connect or prompt for a new network. Proceeding
# without an active network connection will cause the getaddrinfo to
# fail.
wifi.connect(
wait=True,
show_wait_message=False,
prompt_on_fail=True,
dialog_title='TiLDA Wifi'
)
urlparts = url.split('/', 3)
proto = urlparts[0]
host = urlparts[2]
urlpath = '' if len(urlparts) < 4 else urlparts[3]
if proto == 'http:':
port = 80
elif proto == 'https:':
port = 443
else:
raise OSError('Unsupported protocol: %s' % proto[:-1])
if ':' in host:
host, port = host.split(':')
port = int(port)
if data is not None:
if isinstance(data, str):
content = data
content_type = "text/plain; charset=UTF-8"
else:
content = urlencode(data)
content_type = "application/x-www-form-urlencoded"
elif json is not None:
content = ujson.dumps(json)
content_type = CONTENT_TYPE_JSON
else:
content = None
# ToDo: Handle IPv6 addresses
addr = get_address_info(host, port)
sock = usocket.socket(usocket.AF_INET, usocket.SOCK_STREAM)
if params:
urlpath += "?" + urlencode(params)
sock.connect(addr)
if proto == 'https:':
sock = ussl.wrap_socket(sock, ca_certs="DST Root CA X3", cert_reqs=ussl.CERT_OPTIONAL)
sock.send('%s /%s HTTP/1.0\r\nHost: %s\r\n' % (method, urlpath, host))
if headers is not None:
for header in headers.items():
sock.send('%s: %s\r\n' % header)
if content is not None:
sock.send('content-length: %s\r\n' % len(content))
sock.send('content-type: %s\r\n' % content_type)
sock.send('\r\n')
sock.send(content)
else:
sock.send('\r\n')
return sock
def get_address_info(host, port, retries_left = 20):
try:
return usocket.getaddrinfo(host, port)[0][4]
except OSError as e:
if ("-15" in str(e)) and retries_left:
# [addrinfo error -15]
# This tends to happen after startup and goes away after a while
time.sleep_ms(200)
return get_address_info(host, port, retries_left - 1)
else:
raise e
# Adapted from upip
def request(method, url, json=None, timeout=None, headers=None, data=None, params=None):
sock = open_http_socket(method, url, json, timeout, headers, data, params)
try:
response = Response()
state = 1
hbuf = b""
while True:
buf = sock.recv(BUFFER_SIZE)
if state == 1: # Status
nl = buf.find(b"\n")
if nl > -1:
hbuf += buf[:nl - 1]
response.status = int(hbuf.split(b' ')[1])
state = 2
hbuf = b"";
buf = buf[nl + 1:]
else:
hbuf += buf
if state == 2: # Headers
hbuf += buf
nl = hbuf.find(b"\n")
while nl > -1:
if nl < 2:
buf = hbuf[2:]
hbuf = None
state = 3
break
header = hbuf[:nl - 1].decode("utf8").split(':', 3)
response.headers[header[0].strip()] = header[1].strip()
hbuf = hbuf[nl + 1:]
nl = hbuf.find(b"\n")
if state == 3: # Content
response.add_socket(sock, buf)
sock = None # It's not our responsibility to close the socket anymore
return response
finally:
if sock: sock.close()
gc.collect()
def get(url, **kwargs):
return request('GET', url, **kwargs)
def post(url, **kwargs):
return request('POST', url, **kwargs)
def is_ipv4_address(address):
octets = address.split('.')
try:
valid_octets = [x for x in octets if 0 <= int(x) and int(x) <= 255]
return len(valid_octets) == 4
except Exception:
return False

View File

@ -1,109 +0,0 @@
""" library to display icons
http://glyph.smarticons.co/
"""
___license___ = "MIT"
___dependencies___ = ["ospath", "shared/icons/unknown.gif", "buttons"]
import ugfx, ospath
_icon_size = 50;
_half_icon_size = _icon_size // 2
_padding = 5
_padded_size = _icon_size + _padding * 2
_text_height = 30
_icon_container_style = ugfx.Style()
_icon_container_style.set_disabled([ugfx.BLACK, ugfx.WHITE, ugfx.WHITE, ugfx.RED])
_icon_container_style.set_enabled([ugfx.BLACK, ugfx.RED, ugfx.WHITE, ugfx.RED])
#_icon_container_style.set_background(ugfx.html_color(ugfx.WHITE))
class Icon:
def __init__(self, x, y, title, path_to_icon = None):
if path_to_icon == None or not ospath.isfile(path_to_icon):
path_to_icon = "shared/icons/unknown.gif"
self._selected = False
self._init_container(x, y, title, path_to_icon)
def _init_container(self, x, y, title, path_to_icon):
self.container = ugfx.Container(
x - _half_icon_size - _padding, y - _half_icon_size - _padding,
_padded_size, _padded_size + _text_height,
style=_icon_container_style
)
#This doesn't work reliably at the moment
#ugfx.Imagebox(
# _padding - 2, _padding - 2,
# _icon_size, _icon_size,
# parent=self.container, text=path_to_icon
#)
self.label = ugfx.Label(
0, _padded_size,
_padded_size, _text_height,
title, parent=self.container, justification=ugfx.Label.CENTERTOP
)
self.container.enabled(self._selected)
def show(self):
self.container.show()
self.refresh_image()
def refresh_image(self):
self.container.area(_padding, _padding, _icon_size, _icon_size, ugfx.BLACK)
@property
def selected(self):
return self._selected
@selected.setter
def selected(self, value):
self._selected = value
self.container.enabled(value)
self.refresh_image()
def __del__(self):
self.label.destroy()
self.container.destroy()
def chunks(l, n):
"""Yield successive n-sized chunks from l."""
for i in range(0, len(l), n):
yield l[i:i + n]
class IconGrid:
def __init__(self, x, y, items, controls):
self._x = x
self._y = y
self._pages = list(chunks(items, 9))
self._current_page_index = 0
self._current_cursor_x = 0
self._current_cursor_y = 0
self._last_state = None
self._refresh_page()
def _refresh_page(self):
state = (self._current_page_index, self._current_cursor_x, self._current_cursor_y)
if self._last_state == state:
return # nothing to do
self._last_state = state
self._current_icons = []
for i, item in enumerate(self._pages[self._current_page_index]):
x = i % 3
y = i // 3
icon = Icon(
self._x + x * 60 + 30, self._y + y * 90 + 30,
item['title'], item.get('icon', None)
)
icon.selected = (x == self._current_cursor_x) and (y == self._current_cursor_y)
icon.show()
self._current_icons.append(icon)
def __del__(self):
del self._current_icons

View File

@ -1,138 +0,0 @@
""" Consumes a stream and returns a dict
However, the dict won't contain "__doc__", "___version___" etc, but
the shortened versions without underscores: "doc", "version".
Currently not supported:
* Dicts
* Floating points
* ints in list
* Strings in any other format then "x" or 'y'
* Docstrings with any other delimiter other than triple-" or triple='
* Comments
Feel free to expand if necessary
"""
class ParseException(Exception):
"""Indicates a parsing exception"""
def __init__(self, message = ""):
super().__init__(message)
def read_metadata(s):
result = {}
result["doc"] = _read_docstring(s)
while True:
key = _read_key(s)
if key:
result[key] = _read_value(s)
else:
break
return result
def _read_docstring(s):
delimiter = _read_non_whitespace(s, 3)
if delimiter not in ["'''", '"""']:
raise ParseException("Docstring delimiter expected")
result = _read(s, 3);
while result[-3:] != delimiter:
result += _read(s)
return result[:-3]
def _read_value(s):
char = _read_non_whitespace(s)
return _read_value_given_first_char(s, char)
def _read_value_given_first_char(s, first_char):
if first_char in ["'", '"']:
return _read_string(s, first_char)
if first_char in "0123456789":
return _read_int(s, first_char)
if first_char in "TF":
return _read_bool(s, first_char)
if first_char == "[":
return _read_list(s)
raise ParseException("Invalid character %s found" % first_char)
def _read_string(s, delimiter):
result = _read(s)
try:
while result[-1:] != delimiter:
result += _read(s)
except ParseException:
raise ParseException("Invalid string or not terminated: %s" % result)
return result[:-1]
def _read_int(s, char):
result = char
while not char.isspace():
char = s.read(1)
if not char:
break
result += char
if not char in "0123456789":
raise ParseException("Invalid int: %s" % result)
return int(result)
def _read_bool(s, char):
if char == "T":
_assert(char + _read(s, 3), "True", "Invalid boolean")
return True
else:
_assert(char + _read(s, 4), "False", "Invalid boolean")
return False
def _read_list(s):
result = []
while True:
char = _read_non_whitespace(s)
if char == "]":
break
if result:
if char != ",":
raise ParseException("Expected comma, got '%s'" % char)
result.append(_read_value(s))
else:
result.append(_read_value_given_first_char(s, char))
return result
def _read_key(s):
delimiter = _read_non_whitespace(s, 3)
if delimiter != "___":
return None
try:
result = _read(s, 3);
while result[-3:] != delimiter:
char = _read(s)
if char in [" ", "="]:
raise ParseException()
result += char
except ParseException:
raise ParseException("Invalid key: ___%s" % result)
_assert(_read_non_whitespace(s), "=", "Expected equals")
return result[:-3]
def _read(s, l=1):
result = s.read(l)
if len(result)<l:
raise ParseException("Expected to read at least %s characters, got '%s'" % (l, result))
return result
def _assert(input, expected, message):
if not input == expected:
raise ParseException(message + " ('%s' expected, '%s' found)" % (expected, input))
def _read_non_whitespace(s, l=1):
result = s.read(1)
while result.isspace():
result = s.read(1)
if l == 1:
return result
else:
return result + s.read(l - 1)

View File

@ -1,86 +0,0 @@
"""Functions to sync the hardware clock to internet network time servers
Derived from the 2016 implementation.
"""
___license___ = "MIT"
import database
import usocket
import machine
# (date(2000, 1, 1) - date(1900, 1, 1)).days * 24*60*60
NTP_DELTA = 3155673600
# With Mk3 Firmware an IP address string works 5%, hangs at socket.socket(..) 95%, could be a bug in 2016 upython?
NTP_HOSTS = ["0.emfbadge.pool.ntp.org", "1.emfbadge.pool.ntp.org", "2.emfbadge.pool.ntp.org", "3.emfbadge.pool.ntp.org"]
NTP_PORT = 123
def get_NTP_time():
for NTP_HOST in NTP_HOSTS:
res = query_NTP_host(NTP_HOST)
if res is not None:
return res
return None
def query_NTP_host(_NTP_HOST):
NTP_QUERY = bytearray(48)
NTP_QUERY[0] = 0x1b
# Catch exception when run on a network without working DNS
try:
addr = usocket.getaddrinfo(_NTP_HOST, NTP_PORT)[0][-1]
except OSError:
return None
s = usocket.socket(usocket.AF_INET, usocket.SOCK_DGRAM)
s.sendto(NTP_QUERY, addr)
# Setting timeout for receiving data. Because we're using UDP,
# there's no need for a timeout on send.
msg = None
try:
msg = s.recv(48)
except OSError:
pass
finally:
s.close()
if msg is None:
return None
import struct
stratum = int(msg[1])
if stratum == 0:
# KoD, reason doesn't matter, failover to next host
return None
val = struct.unpack("!I", msg[40:44])[0]
print("Using NTP Host: %s, Stratum: %d" % (_NTP_HOST, stratum))
return val - NTP_DELTA
def set_NTP_time():
import time
print("Setting time from NTP")
t = get_NTP_time()
if t is None:
print("Could not set time from NTP")
return False
tz = 0
with database.Database() as db:
tz = db.get("timezone", 0)
tz_minutes = int(abs(tz) % 100) * (1 if tz >= 0 else -1)
tz_hours = int(tz / 100)
t += (tz_hours * 3600) + (tz_minutes * 60)
tm = time.localtime(t)
tm = tm[0:3] + tm[3:6]
rtc = machine.RTC()
rtc.init(tm)
return True

View File

@ -1,89 +0,0 @@
""" A TiLDA optimized implementation of os.path
The one in upip requires a modified version of "os" that I don't want to include
"""
___dependencies___ = ["upip:stat"]
from stat import *
import os
sep = "/"
R_OK = 4
W_OK = 2
X_OK = 1
F_OK = 0
def join(*args):
# TODO: this is non-compliant
if not args:
return ""
if len(args) == 1:
return args[0]
if type(args[0]) is bytes:
return b"/".join(args)
else:
return sep.join(args)
def split(path):
if path == "":
return ("", "")
r = path.rsplit(sep, 1)
if len(r) == 1:
return ("", path)
head = r[0] #.rstrip(sep)
if not head:
head = sep
return (head, r[1])
def dirname(path):
return split(path)[0]
def basename(path):
return split(path)[1]
def exists(path):
try:
os.stat(path)[0]
return True
except OSError:
return False
def isdir(path):
import stat
try:
mode = os.stat(path)[0]
return stat.S_ISDIR(mode)
except OSError:
return False
def isfile(path):
import stat
try:
mode = os.stat(path)[0]
return stat.S_ISREG(mode)
except OSError:
return False
# not normally in os.path
def makedirs(path):
"""recursively creates a given path"""
sub_path = dirname(path)
if sub_path and (not exists(sub_path)):
makedirs(sub_path)
if not exists(path):
os.mkdir(path)
def recursive_rmdir(path=""):
for s in os.listdir(path):
full = join(path, s)
if isdir(full):
try:
recursive_rmdir(full)
except:
pass
os.rmdir(full)
else:
os.remove(full)

View File

@ -1,897 +0,0 @@
"""SIM800 library for the TiLDA MK4"""
___license___ = "MIT"
___dependencies___ = []
import machine
import time
import micropython
import tilda
uart_port = 1
uart_default_baud = 115200
uart_timeout = 28
default_response_timeout = 2000
status_pin = machine.Pin(machine.Pin.GPIO_SIM_STATUS, machine.Pin.IN)
ringer_pin = machine.Pin(machine.Pin.GPIO_SIM_RI, machine.Pin.IN)
pwr_key_pin = machine.Pin(machine.Pin.GPIO_SIM_PWR_KEY, machine.Pin.OUT)
amp_pin = machine.Pin(machine.Pin.GPIO_AMP_SHUTDOWN, machine.Pin.OUT)
netlight_pin = machine.Pin(machine.Pin.GPIO_SIM_NETLIGHT, machine.Pin.IN)
# Open the UART
uart = machine.UART(uart_port, uart_default_baud, mode=machine.UART.BINARY, timeout=uart_timeout)
dirtybuffer = False # Flag if the buffer could have residual end of reresponsesponces line in it?
# A list of callback functions
callbacks = []
server_callback = None
# Globals for remembering callback data
clip = ""
btpairing = ''
holdoffirq=False
# Check if the SIM800 is powered up
def ison():
return status_pin.value()==1
# Check if the SIM800 is ringing
def isringing():
return ringer_pin.value()==0
# Register for a callback
def registercallback(call, function):
callbacks.append([call, function])
# Deregitser for a callback
def deregistercallback(function):
for entry in callbacks:
if entry[1]==function:
callbacks.remove(entry)
# Identify if this was a positive response
def ispositive(response):
return (response=="OK") or response.startswith("CONNECT") or response.startswith("SEND OK")
# Identify if this was a negative response
def isnegative(response):
return (response=="NO CARRIER") or (response=="ERROR") or (response=="NO DIALTONE") or (response=="BUSY") or (response=="NO ANSWER") or (response=="SEND FAIL") or (response=="TIMEOUT") or (response=="TimeOut")
# Identify if this is the completion of a response
def isdefinitive(response, custom=None):
if custom is not None:
return ispositive(response) or isnegative(response) or response.startswith(custom)
else:
return ispositive(response) or isnegative(response)
# Extract the [first/only] parameter from a response
def extractval(parameter, response, default=""):
for entry in response:
if entry.startswith(parameter):
return (entry[len(parameter):]).strip()
return default
# Extract all parameter from a response
def extractvals(parameter, response):
result = []
for entry in response:
if entry.startswith(parameter):
result.append((entry[len(parameter):]).strip())
return result
# Read a lines of response from the UART
def readline():
stringin = ""
while (True):
charin = uart.read(1)
# If we time out we are at the end of a line
if charin is None:
return stringin
# We this the end of the line?
elif (charin == b'\r'):
if (stringin!=""):
return stringin
# This will be part of the string then
elif not (charin == b'\n'):
stringin += chr(ord(charin))
# Check if we have a callback hook for this line
def processcallbacks(line):
global clip
global btpairing
# Check for the caller line information
if line.startswith("+CLIP:"):
clip = line[6:].strip()
# Check for Bluetooth pairing request
if line.startswith("+BTPAIRING:"):
btpairing = line[11:].strip()
# Handle TCP Server Data
if line.startswith("+RECEIVE"):
dlen = int(line.split(",")[2].rstrip(":"))+1
payload = uart.read(dlen)
if server_callback:
micropython.schedule(server_callback, payload[1:])
# Check for app callbacks
for entry in callbacks:
if line.startswith(entry[0]):
micropython.schedule(entry[1], line[len(entry[0]):].strip())
# Process the buffer for unsolicited result codes
def processbuffer():
while uart.any()>0:
line = readline()
processcallbacks(line)
# Execute a command on the module
# The same interface as and called by command() but without power so can be called from power()
def command_internal(command="AT", response_timeout=default_response_timeout, required_response=None, custom_endofdata=None):
global dirtybuffer
global holdoffirq
# Don't let the interupt process the buffer mid command
holdoffirq = True
# Process anything remaining in the buffer
processbuffer()
# Send the command
uart.write(command + "\r")
# Read the results
result = []
complete = False
customcomplete = required_response is None
timeouttime = time.time()+(response_timeout/1000)
while (time.time()<timeouttime):
line = readline()
processcallbacks(line)
# Remember the line if not empty
if (len(line)>0):
result.append(line)
# Check if we have a standard end of response
if isdefinitive(line, custom_endofdata):
complete = True
# Check if we have the data we are looking for
if (required_response is not None) and (line.startswith(required_response)):
customcomplete = True
# Check if we are done
if complete and customcomplete:
holdoffirq = False
return result
# We ran out of time
# set the dirty buffer flag is an out of date end of responcs cound end up in the buffer
if required_response is None:
dirtybuffer = True
result.append("TIMEOUT")
holdoffirq = False
return result
# Send the command to set the default configuration to the SIM800
def senddefaultconfig():
# Send a command to autonigotiate UART speed
command_internal("AT")
# Turn on new SMS notificationn
command_internal("AT+CNMI=1,2")
# Turn on calling line identification notification
command_internal("AT+CLIP=1")
# Swith to text mode
command("AT+CMGF=1")
# Switch to ASCII(ish)
command("AT+CSCS=\"8859-1\"")
# Enable DTMF detection
command("AT+DDET=1,0,1")
# Set up multichannel Bluetooth without transparent transmition
command("AT+BTSPPCFG=\"MC\",1")
command("AT+BTSPPCFG=\"TT\",0")
# Power on the SIM800 (True=on, False=off, returns true when on)
def power(onoroff, asyncro):
# Get to a stable state if not async
if not asyncro and pwr_key_pin.value():
pwr_key_pin.off()
time.sleep(3)
# Press the virtual power key if we are off
if not (ison()==onoroff):
pwr_key_pin.on()
if not asyncro:
time.sleep(3)
# Have we just turned on?
isonnow = ison()
if (isonnow==onoroff) and pwr_key_pin.value():
# Stop pressing the virtual key
pwr_key_pin.off()
# Clear the buffer
processbuffer()
dirtybuffer = False
# We are now live
if isonnow:
# Set the deault configuration
senddefaultconfig()
return isonnow
# Power on the SIM800 (returns true when on)
def poweron(asyncro=False):
return power(True, asyncro)
# Power off the SIM800 (returns true when off)
def poweroff(asyncro=False):
return not power(False, asyncro)
# Change the speed on the communication
def uartspeed(newbaud):
global uart
command("AT+IPR=" + str(newbaud))
uart.deinit()
if (newbaud==0):
uart = machine.UART(uart_port, uart_default_baud, mode=UART.BINARY, timeout=uart_timeout)
else:
uart = machine.UART(uart_port, newbaud, mode=UART.BINARY, timeout=uart_timeout)
# Netlight sheduled (called for polling uart)
def netlightscheduled_internal(pinstate):
# Complete the setup procedure if needed
if pwr_key_pin.value() and ison():
poweron()
# Check for incomming commands
if holdoffirq==False:
processbuffer()
# Netlight IRQ (called for polling uart)
def netlightirq_internal(pinstate):
micropython.schedule(netlightscheduled_internal, pinstate)
# Command is the AT command without the AT or CR/LF, response_timeout (in ms) is how long to wait for completion, required_response is to wait for a non standard response, custom_endofdata will finish when found
def command(command="AT", response_timeout=default_response_timeout, required_response=None, custom_endofdata=None):
# Check we are powered on and set up
poweron(False)
# Call the internal command() function
return command_internal(command, response_timeout, required_response, custom_endofdata)
# Make a voice call
def call(number):
command("ATD" + str(number) + ";", 20000)
# Answer a voice call
def answer():
command("ATA", 20000)
# End/reject a voice call
def hangup():
command("ATH")
# Redial the last number
def redial():
command("ATDL")
# Get the current/latest number to call
def latestnumber():
if ison():
processbuffer()
return clip.split(",")[0].strip("\"")
# Play DTMF tone(s) on a call
def dtmf(number):
validdigits = '1234567890#*ABCD'
for digit in str(number).upper():
if (digit in validdigits):
command("AT+VTS=" + digit)
elif (digit==','):
time.sleep(1)
# Send an SMS message
def sendsms(number, message):
# Swith to text mode
command("AT+CMGF=1")
# Switch to ASCII(ish)
command("AT+CSCS=\"8859-1\"")
# Send the message
command("AT+CMGS=\"" + str(number) + "\"", 2000, None, "> ")
return command(message + "\x1a", 60000)
# List the summery of SMS messages (0=unread,1=read,2=saved unread,3=saved sent, 4=all)
def listsms(stat=0):
statvals = ["REC UNREAD", "REC READ", "STO UNSENT", "STO SENT", "ALL"]
# Swith to text mode
command("AT+CMGF=1")
# Retrieve the list
return extractvals("+CMGL:", command("AT+CMGL=\"" + statvals[stat] + "\",1", 8000))
# Check if we have recived a new unread SMS message
def newsms():
return len(listsms(stat=0))>0
# Read an SMS message
def readsms(index, leaveunread=False):
# Swith to text mode
command("AT+CMGF=1")
# Switch to ASCII(ish)
command("AT+CSCS=\"8859-1\"")
# Retrieve the message
response = command("AT+CMGR=" + str(index) + "," + str(int(leaveunread)), 5000)
if (len(response)>=3):
return response[-2]
else:
return ""
# Delete an SMS message
def deletesms(index):
command("AT+CMGD=" + str(index), 5000)
# Get the IMEI number of the SIM800
def imei():
response = command("AT+GSN")
if (len(response)>=2):
return response[-2]
else:
return ""
# Get the IMSI number of the Sim Card
def imsi():
response = command("AT+CIMI")
if (len(response)>=2):
return response[-2]
else:
return ""
# Get the ICCID of the Sim Card
def iccid():
response = command("AT+ICCID")
return extractval("+ICCID:", response)
# Get the received signal strength indication
def rssi():
response = command("AT+CSQ")
return int(extractval("+CSQ:", response, "0,0").split(",")[0])
# Get the bit error rate
def ber():
response = command("AT+CSQ")
return int(extractval("+CSQ:", response, "0,0").split(",")[1])
# Get the cell engineering information (True to include neighboring cell id)
def engineeringinfo(neighbor=False):
command("AT+CENG=1," + str(int(neighbor)))
response = command("AT+CENG?")
command("AT+CENG=0")
responselist = extractvals("+CENG:", response)[1:]
results = []
for entry in responselist:
results.append([entry[0], entry.replace("\"", "").split(",")[1:]])
return results
# Get the cell id of the currently connected cell
def cellid():
return engineeringinfo()[0][1][6]
# Get/Set ringer volume (0-100)
def ringervolume(level=None):
# Set the new leve if we have one to set
if level is not None:
command("AT+CRSL=" + str(level))
# Retieve the set level to report back
response = command("AT+CRSL?")
return int(extractval("+CRSL:", response, 0))
# Get/Set speaker volume (0-100)
def speakervolume(level=None):
# Set the new leve if we have one to set
if level is not None:
command("AT+CLVL=" + str(level))
# Retieve the set level to report back
response = command("AT+CLVL?")
return int(extractval("+CLVL:", response, 0))
# Get/Set/Preview and set the ringtone (alert is 0-19)
def ringtone(alert=None,preview=False):
# Set/preview the new ringtone if we have one to set
if alert is not None:
command("AT+CALS=" + str(alert) + "," + str(int(preview)))
# Retieve the current/new setting
response = command("AT+CALS?")
current = extractval("+CALS:", response, 0).split(",")[0]
# Stop the preview unless we started it
if alert is None:
command("AT+CALS=" + current + ",0")
# Return the surrent setting
return int(current)
# Play a tone though the SIM800 (MHz and ms)
def playtone(freq=0,duration=2000,asyncro=True):
if freq>0:
command("AT+SIMTONE=1," + str(freq) + "," + str(duration) + ",0," + str(duration))
if not asyncro:
time.sleep(duration/1000)
else:
command("AT+SIMTONE=0")
# Record audio (id=1-10)
def startrecording(id=1, length=None):
if length is None:
return ispositive(command("AT+CREC=1," + str(id) + ",0")[-1])
else:
return ispositive(command("AT+CREC=1," + str(id) + ",0," + str(int(length/1000)))[-1])
# Stop recording audio
def stoprecording():
return ispositive(command("AT+CREC=2")[-1])
# Delete recording
def deleterecording(id=1):
return ispositive(command("AT+CREC=3," + str(id))[-1])
# Play recording
def startplayback(id=1, channel=0, level=100, repeat=False):
return ispositive(command("AT+CREC=4," + str(id) + "," + str(channel) + "," + str(level) + "," + str(int(repeat)))[-1])
# Stop playback
def stopplayback():
return ispositive(command("AT+CREC=5")[-1])
# List recordings (returns a list of ids and size)
def listrecordings():
response = command("AT+CREC=7")
responselist = extractvals("+CREC:", response)
result = []
for entry in responselist:
splitentry = entry.split(",")
result.append([splitentry[1], splitentry[2]])
return result
# Is the battery charging (0=no, 1=yes, 2=full)
def batterycharging():
response = command("AT+CBC")
vals = extractval("+CBC:", response, "0,0,0").split(",")
return int(vals[0])
# How full is the battery (1-100)
def batterycharge():
response = command("AT+CBC")
vals = extractval("+CBC:", response, "0,0,0").split(",")
return int(vals[1])
# List the available operator (returns list of [0=?,1=available,2=current,3=forbidden], 0=long name, 1=short name, 2=GSMLAI )
def listoperators(available_only=True):
delim = "||||"
response = command("AT+COPS=?", 45000)
responsedata = extractval("+COPS:", response, "").split(",,")[0]
responselist = responsedata.replace("),(",delim)[1:-1].split(delim)
results = []
for entry in responselist:
subresults = []
for subentry in entry.split(","):
subresults.append(subentry.strip("\""))
if (not available_only) or (subresults[0]=="1") or (subresults[0]=="2"):
results.append(subresults)
return results
# Get the current operator (format 0=long name, 1=short name, 2=GSMLAI)
def currentoperator(format=0):
command("AT+COPS=3," + str(format))
response = command("AT+COPS?")
responsedata = extractval("+COPS:", response, "").split(",")
if (len(responsedata)>=3):
return responsedata[2].strip("\"")
else:
return ""
# Set the operator selection ([0=automatic,1=Manual,2=deregister,4=try manual then automatic])
def setoperator(mode, format=None, operator=None):
params = ""
if format is not None:
params += "," + str(format)
if operator is not None:
params += ",\"" + str(operator) + "\""
command("AT+COPS=" + str(mode) + params, 120000)
# Get the activity status (returns 0=ready, 2=unknown, 3=ringing, 4=call in progress)
def getstatus():
response = command("AT+CPAS")
return int(extractval("+CPAS:", response, "2"))
# Get the firmware revision
def getfirmwarever():
response = command("AT+CGMR")
if (len(response)>=3):
return response[-2]
else:
return ""
# Request Unstructured Supplementary Service Data from network
def ussd(ussdstring, timeout=8000):
response = command("AT+CUSD=1,\"" + ussdstring + "\"", timeout, "+CUSD:")
return extractval("+CUSD:", response, "")
# Get my number (only works on some networks)
def getmynumber():
responsedata = ussd("*#100#", 8000).split(",")
if (len(responsedata)>=2):
num = responsedata[1].strip().strip("\"")
return num
else:
return ""
# Turn on or off Bluetooth
def btpower(onoroff=True):
command("AT+BTPOWER=" + str(int(onoroff)), 8000)
# Turn on Bluetooth
def btpoweron():
btpower(True);
# Turn off Bluetooth
def btpoweroff():
btpower(False);
# Get the current status of Bluetooth (0=off,5=idel, other values docuemtned for "AT+BTSTATUS")
def btstatus():
response = command("AT+BTSTATUS?")
return int(extractval("+BTSTATUS:", response, "0"))
# Is Bluetooth on?
def btison():
return btstatus()>=5
# Get/Set the Bluetooth host device name
def btname(name=None):
if name is not None:
response = command("AT+BTHOST=" + str(name))
# Retrieve the current name
response = command("AT+BTHOST?")
responsedata = extractval("+BTHOST:", response, "").split(",")
return responsedata[0]
# Get the Bluetooth address
def btaddress():
response = command("AT+BTHOST?")
responsedata = extractval("+BTHOST:", response, "").split(",")
if (len(responsedata)>=2):
return responsedata[-1]
else:
return ""
# Get/Set Bluetooth visibility (True for on, False for off)
def btvisible(visible=None):
# Power on if we want to be visible
if visible:
btpoweron()
# Set the new leve if we have one to set
if visible is not None:
command("AT+BTVIS=" + str(int(visible)))
# Retieve the set gain to report back
response = command("AT+BTVIS?")
return int(extractval("+BTVIS:", response, 0))
# Get the Bluetooth address (timeout in ms from 10000 to 60000, returnd device ID, name, address, rssi)
def btscan(timeout=30000):
btpoweron()
result = []
response = command("AT+BTSCAN=1," + str(int(timeout/1000)), timeout+8000, "+BTSCAN: 1")
for entry in extractvals("+BTSCAN: 0,", response):
splitentry = entry.split(",")
result.append([int(splitentry[0]), splitentry[1].strip("\""), splitentry[2], int(splitentry[3])])
return result
# Get the requesting paring device name
def btparingname():
if ison():
processbuffer()
return btpairing.split(",")[0].strip("\"")
# Get the requesting paring passcode
def btparingpasscode():
if ison():
processbuffer()
splitdata = btpairing.split(",")
if (len(splitdata)>=3):
return splitdata[2]
else:
return ""
# Pair a Bluetooth device
def btpair(device):
global btpairing
btpairing = ''
response = command("AT+BTPAIR=0," + str(device), 8000, "+BTPAIRING:")
return extractval("+BTPAIRING:", response, "").split(",")
# Confirm the pairing of a Bluetooth device
def btpairconfirm(passkey=None):
global btpairing
btpairing = ''
if passkey is None:
return command("AT+BTPAIR=1,1", 8000)
else:
return command("AT+BTPAIR=2," + str(passkey), 8000)
# Cancel/reject the pairing of a Bluetooth device
def btpairreject():
global btpairing
btpairing = ''
return command("AT+BTPAIR=1,0", 8000)
# Unpair a Bluetooth device (unpair everything when device is 0)
def btunpair(device=0):
return command("AT+BTUNPAIR=" + str(device), 8000)
# List the paired Bluetooth devices
def btpaired():
result = []
response = command("AT+BTSTATUS?")
for entry in extractvals("P:", response):
splitentry = entry.split(",")
result = [int(splitentry[0]), splitentry[1].strip("\""), splitentry[2]]
return result
# List profiles supported by a paired device
def btgetprofiles(device):
response = command("AT+BTGETPROF=" + str(device), 8000)
responselist = extractvals("+BTGETPROF:", response)
results = []
for entry in responselist:
splitentry = entry.split(",")
subresults = [int(splitentry[0]), splitentry[1].strip("\"")]
results.append(subresults)
return results
# Is a paticula profile supported (usename=False for id, True for name)
def btprofilesupported(device, usename):
profiles = btgetprofiles(device)
for entry in profiles:
if (type(usename)==int) and (entry[0]==usename):
return True
if (type(usename)==str) and (entry[1]==usename):
return True
return False
# Connect a Bluetooth device
def btconnect(device, profile):
response = command("AT+BTCONNECT=" + str(device) + "," + str(profile), 8000, "+BTCONNECT:")
return extractvals("+BTCONNECT:", response)
# Disconnect a Bluetooth device
def btdisconnect(device):
response = command("AT+BTDISCONN=" + str(device), 8000, "+BTDISCONN:")
return extractvals("+BTDISCONN:", response)
# List the Bluetooth connections
def btconnected():
result = []
response = command("AT+BTSTATUS?")
for entry in extractvals("C:", response):
splitentry = entry.split(",")
result = [int(splitentry[0]), splitentry[1].strip("\""), splitentry[2]]
return result
# Push an OPP object/file over Bluetooth (must be paired for OPP, monitor +BTOPPPUSH: for sucsess / fail / server issue)
def btopppush(device, filename):
response = command("AT+BTOPPPUSH=" + str(device) + "," + filename, 45000, "+BTOPPPUSH:")
responce2 = extractval("+BTOPPPUSH:", response, "")
return responce2=="1"
# Accept an OPP object/file from Bluetooth (monitor +BTOPPPUSHING: for offering, files stored in "\\User\\BtReceived")
def btoppaccept():
response = command("AT+BTOPPACPT=1", 45000, "+BTOPPPUSH:")
responce2 = extractval("+BTOPPPUSH:", response, 0)
return responce2=="1"
# Send data over a Bluetooth serial connection
def btsppwrite(connection, data):
response = command("AT+BTSPPSEND=" + str(connection) + "," + str(len(data)), 8000, None, ">")
if response[-1].startswith(">"):
return ispositive(command(data)[-1])
else:
return False
# Receive data from a Bluetooth serial connection
def btsppread(connection):
command()
global holdoffirq
# Don't let the interupt process the buffer mid command
holdoffirq = True
request = "AT+BTSPPGET=3," + str(connection) + "\n"
uart.write(request)
data = uart.read()
while True:
time.sleep(uart_timeout/1000)
if uart.any()==0:
break
data += uart.read()
holdoffirq = False
if not data.endswith("ERROR\r\n"):
return data[len(request)+2:-6]
else:
return None
# Reject an OPP object/file transfer
def btoppreject():
command("AT+BTOPPACPT=0")
# Make a voice call
def btcall(number):
btpoweron()
command("AT+BTATD" + str(number), 20000)
# Answer a voice call
def btanswer():
btpoweron()
command("AT+BTATA", 20000)
# End a voice call
def bthangup():
btpoweron()
command("AT+BTATH")
# Redial the last number
def btredial():
btpoweron()
command("AT+BTATDL")
# Play DTMF tone(s) on a Bluetooth call
def btdtmf(number):
validdigits = '1234567890#*ABCD'
for digit in str(number).upper():
if (digit in validdigits):
command("AT+BTVTS=" + digit)
elif (digit==','):
time.sleep(1)
# Get/Set Bluetooth voice gain (0-15)
def btvoicevolume(gain=None):
# Set the new leve if we have one to set
if gain is not None:
command("AT+BTVGS=" + str(gain))
# Retieve the set gain to report back
response = command("AT+BTVGS?")
return int(extractval("+BTVGS:", response, 0))
# Get/Set microphone gain volume (0-15)
def btvoicevolume(gain=None):
# Set the new leve if we have one to set
if gain is not None:
command("AT+BTVGM=" + str(gain))
# Retieve the set gain to report back
response = command("AT+BTVGM?")
return int(extractval("+BTVGM:", response, 0))
# Get the Bluetooth signal quality for a device (-127-0)
def btrssi(device):
response = command("AT+BTRSSI=" + str(device))
return int(extractval("+BTRSSI:", response, 0))
# Get available space on the flash storage
def fsfree():
response = command("AT+FSMEM")
return extractval("+FSMEM:", response, "?:0bytes").split(",")[0].split(":")[1][:-5]
# List the entries in directory on flash storage (returned directories end with "\\")
def fsls(directory=""):
if not directory.endswith("\\"):
directory += "\\"
return command("AT+FSLS=" + str(directory))[1:-1]
# Get the size of a file on the flash storage
def fssize(filename):
response = command("AT+FSFLSIZE=" + str(filename))
return int(extractval("+FSFLSIZE:", response, "-1"))
# Create a directory on flash storage
def fsmkdir(directory):
return ispositive(command("AT+FSMKDIR=" + str(directory))[-1])
# Remove a directory on flash storage
def fsrmdir(directory):
return ispositive(command("AT+FSRMDIR=" + str(directory))[-1])
# Create a file on flash storage
def fscreate(filename):
return ispositive(command("AT+FSCREATE=" + str(filename))[-1])
# Read a chunk of data from a file on the flash storage
def fsreadpart(filename, size=256, start=0):
global holdoffirq
mode=int(start>0)
command()
# Don't let the interupt process the buffer mid command
holdoffirq = True
request = "AT+FSREAD=" + str(filename) + "," + str(mode) + "," + str(size) + "," + str(start) + "\n"
uart.write(request)
data = uart.read()
while True:
time.sleep(uart_timeout/1000)
if uart.any()==0:
break
data += uart.read()
holdoffirq = False
if not data.endswith("ERROR\r\n"):
return data[len(request)+2:-6]
else:
return None
# Read data from a file on the flash storage
def fsread(filename):
result = bytearray(0)
while True:
chunk = fsreadpart(filename, 256, len(result))
if chunk is not None:
result += chunk
else:
return result
# Append a small chunk data to a file on the flash storage, you should use sfwrite
def fswritepart(filename, data):
response = command("AT+FSWRITE=" + str(filename) + ",1," + str(len(data)) + ",8", 2000, None, ">")
if response[-1].startswith(">"):
return ispositive(command(data)[-1])
else:
return False
# Write data to a file on the flash storage
def fswrite(filename, data, truncate=False):
length = len(data)
pointer = 0
chunksize = 256
# Create a file if needed
if truncate or (fssize(filename)<0):
fscreate(filename)
# Loop through the data in small chunks
while pointer<length:
result = fswritepart(filename, data[pointer:min(pointer+chunksize,length)])
if not result:
return False
pointer += chunksize
return True
# Delete a file from flash storage
def fsrm(filename):
return ispositive(command("AT+FSDEL=" + str(filename))[-1])
# Rename a file on the flash storage
def fsmv(filenamefrom, filenameto):
return ispositive(command("AT+FSRENAME=" + str(filenamefrom) + "," + str(filenameto))[-1])
# Callback for call buton being pressed
def callbuttonpressed_internal(nullparam=None):
answer()
# Callback for end buton being pressed
def endbuttonpressed_internal(nullparam=None):
hangup()
#GPRS and TCP server functions
def setup_gprs():
command("AT+CIPSHUT", response_timeout=60000, custom_endofdata="SHUT OK")
command("AT+CGATT?", response_timeout=10000)
command("AT+CIPMUX=1", response_timeout=10000)
def connect_gprs(apn):
command("AT+CSTT=\""+apn+"\"", response_timeout=10000)
command("AT+CIICR", response_timeout=10000)
command("AT+CIFSR")
def stop_gprs():
command("AT+CIPSHUT", response_timeout=60000, custom_endofdata="SHUT OK")
def start_server(port, callback):
global server_callback
server_callback = callback
command("AT+CIPSERVER=1,"+str(port), response_timeout=10000)
def stop_server():
command("AT+CIPSERVER=0", response_timeout=10000)
# Startup...
# Start turning on the SIM800 asynchronously
onatstart = poweron(True)
# Reset SIM800 configuration if hardware is still on from before
if onatstart:
senddefaultconfig()
# Turn on the audio amp
amp_pin.on()
# Hook in the Call / End buttons
tilda.Buttons.enable_interrupt(tilda.Buttons.BTN_Call, callbuttonpressed_internal)
tilda.Buttons.enable_interrupt(tilda.Buttons.BTN_End, endbuttonpressed_internal)
# Enable the interupts on network light to poll uart
netlight_pin.irq(netlightirq_internal)

View File

@ -1,21 +0,0 @@
"""Library for sleep related functions"""
___license___ = "MIT"
import time
def sleep_ms(duration):
start_time = time.ticks_ms()
end_time = start_time + duration
while time.ticks_ms() < end_time:
wfi()
def sleep(duration):
sleep_ms(duration * 1000)
def wfi():
# todo: this is fake
time.sleep_ms(1)

View File

@ -1,71 +0,0 @@
"""Library for speaker related functions"""
from machine import Pin, PWM
from math import log, pow
from time import sleep_ms
_enabled = True
_amp_pin = Pin(Pin.GPIO_AMP_SHUTDOWN)
_speaker_pwm = None
def enabled(value=None):
"""Sets the internal amplifier. This is entirely independent from the PWM"""
global _enabled
if value == None:
return _enabled
else:
_enabled = value
_set_amp()
def _set_amp():
if enabled():
_amp_pin.on()
else:
_amp_pin.off()
def speaker_pwm():
global _speaker_pwm
if not _speaker_pwm:
_speaker_pwm = PWM(PWM.PWM_SPEAKER)
return _speaker_pwm
_frequency = None
def frequency(value = None):
global _frequency
if value == None:
return _frequency
_frequency = value
_set_amp()
speaker_pwm().init(duty=1, freq=_frequency)
def stop():
global _frequency
# todo: maybe we should deinit the PWM?
_frequency = None
_set_amp()
speaker_pwm().duty(0)
# Music
NOTES = {"C":0, "C#":1, "D":2, "D#":3, "E":4, "F":5, "F#":6, "G":7, "G#":8, "A":9, "A#":10, "B":11}
def note_to_frequency(note):
if (len(note) == 2) and note[1].isdigit():
octave = int(note[1])
note = note[0].upper()
elif len(note) == 3:
octave = int(note[2])
note = note[0:2].upper()
else:
octave = 4
note = note.upper()
if note not in NOTES:
raise Exception("%s it not a valid note" % note)
halftones = NOTES[note] + 12 * (octave - 4)
freq = 440 * pow(1.059, halftones)
return round(freq)
def note(note):
frequency(note_to_frequency(note))

View File

@ -1,63 +0,0 @@
"""A stack-based naviation system.
Maintains a stack of states through which a user can navigate.
Think Android's back button model.
Every screen is modeled as a function that takes one argument "state".
It can modify state and should either return
* None (which means go back one level)
* A new function (which adds to the stack)
* A keyword like NEXT_EXIT (exit nav) or NEXT_TOP (go to top of stack)
Children get a a shallow clone of the current state, going back will
lead to a screen with the original state.
"""
___dependencies___ = ["dialogs", "database"]
___license___ = "MIT"
import dialogs, database
NEXT_EXIT = "exit" # Leave navigation
NEXT_TOP = "init" # Go to top of stack
def nav(init_fn, init_state={}):
stack = [(init_fn, init_state)]
while stack:
(fn, state) = stack[-1] # peek
next_state = state.copy()
result = fn(next_state)
if callable(result):
stack.append((result, next_state))
elif result == NEXT_TOP:
stack = [(init_fn, init_state)]
elif result == NEXT_EXIT:
break
else:
stack.pop()
# A simple menu. Format of items is [{"foo":fn}, ...]
def selection(items, title="Settings", none_text="Back"):
items = [{"title": t, "function": fn} for (t, fn) in items.items()]
selection = dialogs.prompt_option(items, none_text=none_text, title=title)
if selection:
return selection["function"]
else:
return None
# A wrapper for a simple string menu
def change_string(title, getter_setter_function):
def inner(state):
value = getter_setter_function()
value = dialogs.prompt_text(title, init_text=value, true_text="Change", false_text="Back")
if value:
getter_setter_function(value)
return inner
# A wrapper for a database key
def change_database_string(title, key, default=""):
def inner(state):
value = database.get(key, default)
value = dialogs.prompt_text(title, init_text=value, true_text="Change", false_text="Back")
if value:
database.set(key, value)
return inner

View File

@ -1,44 +0,0 @@
"""Tests for app lib"""
___license___ = "MIT"
___dependencies___ = ["upip:unittest", "app"]
import unittest
from app import *
class TestApp(unittest.TestCase):
def test_app_object(self):
app = App("badge_store")
self.assertEqual(app, App("badge_store"))
self.assertEqual(app.folder_path, "badge_store")
self.assertEqual(app.main_path, "badge_store/main.py")
self.assertEqual(app.loadable, True)
self.assertIn("TiLDA MK4", app.description)
self.assertEqual(app.title, "Badge Store")
self.assertTrue(app.matches_category("System"))
self.assertFalse(app.matches_category("Something"))
self.assertTrue(app.attributes["bootstrapped"], True)
self.assertTrue(app.get_attribute("bootstrapped"), True)
self.assertTrue(app.get_attribute("foobar", "default"), "default")
def test_app_object_with_non_existent_app(self):
app = App("asdfghj")
self.assertEqual(app.folder_path, "asdfghj")
self.assertEqual(app.loadable, False)
with self.assertRaises(Exception) as context:
app.title
self.assertIn("File asdfghj/main.py not found in on badge", str(context.exception))
def test_get_categories(self):
categories = get_categories()
self.assertIn("System", categories)
def test_get_apps(self):
apps = get_apps()
self.assertIn(App("badge_store"), apps)
if __name__ == '__main__':
unittest.main()

View File

@ -1,61 +0,0 @@
"""Tests for badge_store lib"""
___license___ = "MIT"
___dependencies___ = ["upip:unittest", "badge_store", "shared/test/file.txt"]
import unittest, os
from lib.badge_store import *
from ospath import *
class TestBadgeStore(unittest.TestCase):
def setUpClass(self):
self.store = BadgeStore(repo="emfcamp/Mk4-Apps", ref="ee144e8")
self.download_file = "shared/test/download.txt"
def tearDownClass(self):
self._remove_download_file()
def test_get_all_apps(self):
response = self.store.get_all_apps()
self.assertEqual(response["System"], ['badge_store', 'launcher', 'settings'])
def test_categories(self):
categories = self.store.get_categories()
self.assertEqual(set(categories), set(["System", "homescreen"]))
def test_get_apps(self):
apps = self.store.get_apps("System")
self.assertEqual(set(apps), set(['badge_store', 'settings', 'launcher']))
def test_get_app(self):
response = self.store.get_app("launcher")
self.assertEqual(response["description"], "Launcher for apps currently installed")
def test_get_hash(self):
self.assertEqual(get_hash("shared/test/file.txt"), "182d04f8ee")
self.assertEqual(get_hash("does/not/exist.txt"), None)
def test_install_integration(self):
self._remove_download_file()
store = BadgeStore(url="http://badge.marekventur.com", repo="emfcamp/Mk4-Apps", ref="dont-delete-test-download-branch")
for installer in store.install(["launcher"]):
if installer.path == "shared/test/download.txt":
installer.download()
with open(self.download_file, "rt") as response:
self.assertIn("I'm a download test", response.read())
def test_bootstrap_integration(self):
self._remove_download_file()
store = BadgeStore(url="http://badge.marekventur.com")
installers = store.bootstrap()
self.assertTrue(len(installers) > 0)
def _remove_download_file(self):
if isdir(self.download_file) or isfile(self.download_file):
os.remove(self.download_file)
if __name__ == '__main__':
unittest.main()

View File

@ -1,20 +0,0 @@
"""Tests for icons lib"""
___license___ = "MIT"
___dependencies___ = ["upip:unittest", "buttons"]
import unittest, ugfx, time
from buttons import *
class TestButtons(unittest.TestCase):
def test_is_pressed(self):
self.assertFalse(is_pressed(Buttons.BTN_9))
def test_is_triggered(self):
self.assertFalse(is_triggered(Buttons.BTN_9))
if __name__ == '__main__':
unittest.main()

View File

@ -1,54 +0,0 @@
"""Tests for database"""
___license___ = "MIT"
___dependencies___ = ["upip:unittest", "database"]
import database, unittest
class TestDatabase(unittest.TestCase):
def setUp(self):
self.filename = "tmp.testdb.json"
self.database = database.Database(filename = self.filename)
self._remove_test_db()
def tearDown(self):
self._remove_test_db();
def test_convenience_get_default(self):
self.assertEqual(
database.get("does_not_exist", "default_value", filename=self.filename),
"default_value"
)
def test_convenience_set_and_get(self):
database.set("foo", "bar", filename=self.filename)
self.assertEqual(database.get("foo", filename=self.filename), "bar")
def test_convenience_delete(self):
database.set("foo", "bar", filename=self.filename)
database.delete("foo", filename=self.filename)
self.assertEqual(database.get("foo", filename=self.filename), None)
def test_get_default(self):
self.assertEqual(
self.database.get("does_not_exist", "default_value"),
"default_value"
)
def test_set_and_get(self):
self.database.set("foo", "bar")
self.assertEqual(self.database.get("foo"), "bar")
def test_delete(self):
self.database.set("foo", "bar")
self.database.delete("foo")
self.assertEqual(self.database.get("foo"), None)
def _remove_test_db(self):
try:
os.remove(self.filename)
except Exception as e:
pass
if __name__ == '__main__':
unittest.main()

View File

@ -1,38 +0,0 @@
"""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", "ugfx_helper"]
import unittest, ugfx, ugfx_helper
from machine import Pin
from dialogs import *
from sleep import *
class TestDialogs(unittest.TestCase):
def setUpClass(self):
ugfx_helper.init()
def tearDownClass(self):
ugfx_helper.deinit()
def test_waiting(self):
count_max = 3
with WaitingMessage("Testing...", "Foo") as c:
for i in range(1, count_max):
c.text = "%d/%d" % (i, count_max)
print("done")
def test_text(self):
prompt_text("description")
def test_option(self):
print(prompt_option(["foo", "bar", "baz"]))
if __name__ == '__main__':
unittest.main()

View File

@ -1,16 +0,0 @@
"""Tests for hall effect sensor"""
___license___ = "MIT"
___dependencies___ = ["upip:unittest", "hall_effect"]
import unittest, hall_effect, speaker
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

@ -1,27 +0,0 @@
"""Tests for hall effect sensor"""
___license___ = "MIT"
___dependencies___ = ["upip:unittest", "homescreen", "database"]
import unittest, homescreen, database
class TestHomescreen(unittest.TestCase):
def test_name(self):
o = database.get("homescreen.name")
database.delete("homescreen.name")
self.assertEqual(homescreen.name("default"), "default")
database.set("homescreen.name", "foo")
self.assertEqual(homescreen.name("default"), "foo")
database.set("homescreen.name", o)
def test_time(self):
self.assertIn(len(homescreen.time_as_string()), [4, 5])
self.assertIn(len(homescreen.time_as_string(True)), [7, 8])
def test_wifi_strength(self):
# test that it doesn't throw an exception
homescreen.wifi_strength()
if __name__ == '__main__':
unittest.main()

View File

@ -1,48 +0,0 @@
"""Tests for http"""
___license___ = "MIT"
___dependencies___ = ["upip:unittest", "http", "wifi"]
import unittest
from http import *
import wifi
class TestHttp(unittest.TestCase):
def setUpClass(self):
wifi.connect()
def test_get_with_https(self):
with get("https://httpbin.org/get") as response:
self.assertEqual(response.status, 200)
print(response.text)
def test_get(self):
with get("http://httpbin.org/get", params={"foo": "bar"}, headers={"accept": "application/json"}) as response:
self.assertEqual(response.headers["Content-Type"], "application/json")
self.assertEqual(response.status, 200)
content = response.json()
self.assertEqual(content["headers"]["Accept"], "application/json")
self.assertEqual(content["args"], {"foo":"bar"})
def test_post_form(self):
with post("http://httpbin.org/post", data={"foo": "bar"}).raise_for_status() as response:
content = response.json()
self.assertEqual(content["headers"]["Content-Type"], "application/x-www-form-urlencoded")
self.assertEqual(content["form"], {"foo":"bar"})
def test_post_string(self):
with post("http://httpbin.org/post", data="foobar").raise_for_status() as response:
content = response.json()
self.assertEqual(content["headers"]["Content-Type"], "text/plain; charset=UTF-8")
self.assertEqual(content["data"], "foobar")
def test_post_json(self):
with post("http://httpbin.org/post", json={"foo":"bar"}).raise_for_status() as response:
content = response.json()
self.assertEqual(content["headers"]["Content-Type"], "application/json")
self.assertEqual(content["json"], {"foo":"bar"})
if __name__ == '__main__':
unittest.main()

View File

@ -1,39 +0,0 @@
"""Tests for icons lib"""
___license___ = "MIT"
___dependencies___ = ["upip:unittest", "icons"]
import unittest, ugfx, time
from icons import *
class TestIcons(unittest.TestCase):
# incomplete!
# todo: fix me
def setUp(self):
ugfx.init()
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)
if __name__ == '__main__':
unittest.main()

View File

@ -1,23 +0,0 @@
"""Tests for metadata_reader"""
___license___ = "MIT"
___dependencies___ = ["upip:unittest", "metadata_reader"]
___foo___ = "bar"
___flag___ = True
___list___ = ["a", "b", "c"]
import unittest
from metadata_reader import read_metadata
class TestMetadataReader(unittest.TestCase):
def test_reader(self):
with open("lib/test_metadata_reader.py", "rt") as file:
data = read_metadata(file)
self.assertIn("Tests for", data["doc"])
self.assertEqual(data["foo"], "bar")
self.assertEqual(data["flag"], True)
self.assertEqual(data["list"], ["a", "b", "c"])
if __name__ == '__main__':
unittest.main()

View File

@ -1,29 +0,0 @@
"""Tests for ntp"""
___license___ = "MIT"
___dependencies___ = ["upip:unittest", "ntp", "wifi", "ugfx_helper"]
import unittest, wifi, ntp, machine, ugfx_helper
class TestWifi(unittest.TestCase):
def setUpClass(self):
ugfx_helper.init()
wifi.connect()
def tearDownClass(self):
ugfx_helper.deinit()
def test_get_time(self):
t = ntp.get_NTP_time()
self.assertTrue(t > 588699276)
self.assertTrue(t < 1851003302) # 27 August 2028
def test_set_time(self):
ntp.set_NTP_time()
rtc = machine.RTC()
self.assertTrue(rtc.now()[0] >= 2018)
self.assertTrue(rtc.now()[0] <= 2028)
if __name__ == '__main__':
unittest.main()

View File

@ -1,29 +0,0 @@
"""Tests for app lib"""
___license___ = "MIT"
___dependencies___ = ["upip:unittest", "ospath"]
import unittest
from ospath import *
class TestOsPath(unittest.TestCase):
# todo: write more tests!
def test_isdir(self):
self.assertTrue(isdir("lib"))
self.assertFalse(isdir("lib/ospath.py"))
self.assertFalse(isdir("foo/bar/zzz"))
def test_isfile(self):
self.assertFalse(isfile("lib"))
self.assertTrue(isfile("lib/ospath.py"))
self.assertFalse(isfile("foo/bar/zzz"))
def test_exists(self):
self.assertTrue(exists("lib"))
self.assertTrue(exists("lib/ospath.py"))
self.assertFalse(exists("foo/bar/zzz"))
if __name__ == '__main__':
unittest.main()

View File

@ -1,39 +0,0 @@
"""Tests for random lib"""
___license___ = "MIT"
___dependencies___ = ["upip:unittest"]
import unittest
from random import *
class TestRandom(unittest.TestCase):
def test_random(self):
for i in range(1, 100):
r = random()
self.assertTrue(r>=0)
self.assertTrue(r<=1)
def test_randint(self):
for i in range(1, 100):
r = randint(500, 1000)
self.assertTrue(r>=500)
self.assertTrue(r<=1000)
def test_randrange(self):
for i in range(1, 100):
r = randrange(10)
self.assertTrue(r>=0)
self.assertTrue(r<10)
def test_shuffle(self):
for i in range(1, 100):
r = list(range(1, 10))
shuffle(r)
self.assertEqual(sum(r), 45)
self.assertEqual(set(r), set(range(1, 10)))
self.assertNotEqual(r, list(range(1, 10)))
if __name__ == '__main__':
unittest.main()

View File

@ -1,26 +0,0 @@
"""Tests for http"""
___license___ = "MIT"
___dependencies___ = ["upip:unittest", "sleep"]
import unittest, sleep, time
class TestSleep(unittest.TestCase):
def test_sleep(self):
sleep_secs = 5
time_before = time.ticks_ms()
time_after = time_before + 1000 * sleep_secs
sleep.sleep(sleep_secs)
self.assertTrue(time.ticks_ms() >= time_after)
def test_sleep_ms(self):
sleep_ms = 3000
time_before = time.ticks_ms()
time_after = time_before + sleep_ms
sleep.sleep_ms(sleep_ms)
self.assertTrue(time.ticks_ms() >= time_after)
if __name__ == '__main__':
unittest.main()

Some files were not shown because too many files have changed in this diff Show More