Compare commits
1 Commits
master
...
dont-delet
Author | SHA1 | Date |
---|---|---|
Marek Ventur | a5c13da725 |
|
@ -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
|
|
@ -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)
|
|
@ -240,7 +240,7 @@ class Pyboard:
|
|||
delayed = False
|
||||
for attempt in range(wait + 1):
|
||||
try:
|
||||
self.serial = serial.Serial(device, baudrate=baudrate, interCharTimeout=1, timeout=1, write_timeout=1)
|
||||
self.serial = serial.Serial(device, baudrate=baudrate, interCharTimeout=1)
|
||||
break
|
||||
except (OSError, IOError): # Py2 and Py3 have different errors
|
||||
if wait == 0:
|
||||
|
@ -282,8 +282,9 @@ class Pyboard:
|
|||
time.sleep(0.01)
|
||||
return data
|
||||
|
||||
def enter_raw_repl(self, retry_count = 2):
|
||||
def enter_raw_repl(self):
|
||||
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:
|
||||
|
@ -294,8 +295,6 @@ class Pyboard:
|
|||
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
|
||||
|
@ -310,7 +309,7 @@ class Pyboard:
|
|||
print(data)
|
||||
raise PyboardError('could not enter raw repl')
|
||||
|
||||
def exit_raw_repl(self):\
|
||||
def exit_raw_repl(self):
|
||||
self.serial.write(b'\r\x02') # ctrl-B: enter friendly REPL
|
||||
|
||||
def follow(self, timeout, data_consumer=None):
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
from pyboard import Pyboard, PyboardError
|
||||
import glob, sys, pyboard, json, binascii, os, hashlib
|
||||
import glob, sys, pyboard
|
||||
|
||||
_pyb = None
|
||||
|
||||
def get_pyb(args):
|
||||
global _pyb
|
||||
if not _pyb:
|
||||
print("Connected to badge:", end="", flush=True)
|
||||
print("Connected to badge:", end="")
|
||||
if not args.device:
|
||||
args.device = find_tty()
|
||||
|
||||
|
@ -27,13 +27,11 @@ def close_pyb():
|
|||
|
||||
def stop_badge(args, verbose):
|
||||
pyb = get_pyb(args)
|
||||
print("Stopping running app:", end="", flush=True)
|
||||
if verbose:
|
||||
print("Stopping running app:", end="")
|
||||
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")
|
||||
if verbose:
|
||||
print(" DONE")
|
||||
|
||||
def write_command(pyb, command):
|
||||
flush_input(pyb)
|
||||
|
@ -49,9 +47,11 @@ def flush_input(pyb):
|
|||
def soft_reset(args, verbose = True):
|
||||
pyb = get_pyb(args)
|
||||
if verbose:
|
||||
print("Soft reboot:", end="", flush=True)
|
||||
print("Soft reboot:", end="")
|
||||
write_command(pyb, b'\x04') # ctrl-D: soft reset
|
||||
#print("1")
|
||||
data = pyb.read_until(1, b'soft reboot\r\n')
|
||||
#print("2")
|
||||
if data.endswith(b'soft reboot\r\n'):
|
||||
if verbose:
|
||||
print(" DONE")
|
||||
|
@ -61,70 +61,24 @@ def soft_reset(args, verbose = True):
|
|||
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*']:
|
||||
# Todo: find solution for windows, test in linux
|
||||
for pattern in ['/dev/ttyACM*', '/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:
|
||||
with open(filename, 'r') 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)
|
||||
print("Preparing execution:", end="")
|
||||
# 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
|
||||
|
@ -140,98 +94,27 @@ def run(args, paths, verbose=True):
|
|||
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)
|
||||
def execbuffer(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)
|
||||
|
||||
# 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
|
||||
# run any files
|
||||
for filename in paths:
|
||||
with open(filename, 'rb') as f:
|
||||
print("-------- %s --------" % filename)
|
||||
pyfile = f.read()
|
||||
execbuffer(pyfile)
|
||||
|
||||
# exiting raw-REPL just drops to friendly-REPL mode
|
||||
pyb.exit_raw_repl()
|
||||
|
|
|
@ -0,0 +1,543 @@
|
|||
#!/usr/bin/env python
|
||||
# This file is part of the OpenMV project.
|
||||
# Copyright (c) 2013/2014 Ibrahim Abdelkader <i.abdalkader@gmail.com>
|
||||
# This work is licensed under the MIT license, see the file LICENSE for
|
||||
# details.
|
||||
|
||||
"""This module implements enough functionality to program the STM32F4xx over
|
||||
DFU, without requiring dfu-util.
|
||||
See app note AN3156 for a description of the DFU protocol.
|
||||
See document UM0391 for a dscription of the DFuse file.
|
||||
"""
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import struct
|
||||
import sys
|
||||
import usb.core
|
||||
import usb.util
|
||||
import zlib
|
||||
|
||||
# VID/PID
|
||||
__VID = 0x0483
|
||||
__PID = 0xdf11
|
||||
|
||||
# USB request __TIMEOUT
|
||||
__TIMEOUT = 4000
|
||||
|
||||
# DFU commands
|
||||
__DFU_DETACH = 0
|
||||
__DFU_DNLOAD = 1
|
||||
__DFU_UPLOAD = 2
|
||||
__DFU_GETSTATUS = 3
|
||||
__DFU_CLRSTATUS = 4
|
||||
__DFU_GETSTATE = 5
|
||||
__DFU_ABORT = 6
|
||||
|
||||
# DFU status
|
||||
__DFU_STATE_APP_IDLE = 0x00
|
||||
__DFU_STATE_APP_DETACH = 0x01
|
||||
__DFU_STATE_DFU_IDLE = 0x02
|
||||
__DFU_STATE_DFU_DOWNLOAD_SYNC = 0x03
|
||||
__DFU_STATE_DFU_DOWNLOAD_BUSY = 0x04
|
||||
__DFU_STATE_DFU_DOWNLOAD_IDLE = 0x05
|
||||
__DFU_STATE_DFU_MANIFEST_SYNC = 0x06
|
||||
__DFU_STATE_DFU_MANIFEST = 0x07
|
||||
__DFU_STATE_DFU_MANIFEST_WAIT_RESET = 0x08
|
||||
__DFU_STATE_DFU_UPLOAD_IDLE = 0x09
|
||||
__DFU_STATE_DFU_ERROR = 0x0a
|
||||
|
||||
_DFU_DESCRIPTOR_TYPE = 0x21
|
||||
|
||||
|
||||
# USB device handle
|
||||
__dev = None
|
||||
|
||||
__verbose = None
|
||||
|
||||
# USB DFU interface
|
||||
__DFU_INTERFACE = 0
|
||||
|
||||
import inspect
|
||||
if 'length' in inspect.getfullargspec(usb.util.get_string).args:
|
||||
# PyUSB 1.0.0.b1 has the length argument
|
||||
def get_string(dev, index):
|
||||
return usb.util.get_string(dev, 255, index)
|
||||
else:
|
||||
# PyUSB 1.0.0.b2 dropped the length argument
|
||||
def get_string(dev, index):
|
||||
return usb.util.get_string(dev, index)
|
||||
|
||||
|
||||
def init():
|
||||
"""Initializes the found DFU device so that we can program it."""
|
||||
global __dev
|
||||
devices = get_dfu_devices(idVendor=__VID, idProduct=__PID)
|
||||
if not devices:
|
||||
raise ValueError('No DFU device found')
|
||||
if len(devices) > 1:
|
||||
raise ValueError("Multiple DFU devices found")
|
||||
__dev = devices[0]
|
||||
__dev.set_configuration()
|
||||
|
||||
# Claim DFU interface
|
||||
usb.util.claim_interface(__dev, __DFU_INTERFACE)
|
||||
|
||||
# Clear status
|
||||
clr_status()
|
||||
|
||||
|
||||
def clr_status():
|
||||
"""Clears any error status (perhaps left over from a previous session)."""
|
||||
__dev.ctrl_transfer(0x21, __DFU_CLRSTATUS, 0, __DFU_INTERFACE,
|
||||
None, __TIMEOUT)
|
||||
|
||||
|
||||
def get_status():
|
||||
"""Get the status of the last operation."""
|
||||
stat = __dev.ctrl_transfer(0xA1, __DFU_GETSTATUS, 0, __DFU_INTERFACE,
|
||||
6, 20000)
|
||||
# print (__DFU_STAT[stat[4]], stat)
|
||||
return stat[4]
|
||||
|
||||
|
||||
def mass_erase():
|
||||
"""Performs a MASS erase (i.e. erases the entire device."""
|
||||
# Send DNLOAD with first byte=0x41
|
||||
__dev.ctrl_transfer(0x21, __DFU_DNLOAD, 0, __DFU_INTERFACE,
|
||||
"\x41", __TIMEOUT)
|
||||
|
||||
# Execute last command
|
||||
if get_status() != __DFU_STATE_DFU_DOWNLOAD_BUSY:
|
||||
raise Exception("DFU: erase failed")
|
||||
|
||||
# Check command state
|
||||
if get_status() != __DFU_STATE_DFU_DOWNLOAD_IDLE:
|
||||
raise Exception("DFU: erase failed")
|
||||
|
||||
|
||||
def page_erase(addr):
|
||||
"""Erases a single page."""
|
||||
if __verbose:
|
||||
print("Erasing page: 0x%x..." % (addr))
|
||||
|
||||
# Send DNLOAD with first byte=0x41 and page address
|
||||
buf = struct.pack("<BI", 0x41, addr)
|
||||
__dev.ctrl_transfer(0x21, __DFU_DNLOAD, 0, __DFU_INTERFACE, buf, __TIMEOUT)
|
||||
|
||||
# Execute last command
|
||||
if get_status() != __DFU_STATE_DFU_DOWNLOAD_BUSY:
|
||||
raise Exception("DFU: erase failed")
|
||||
|
||||
# Check command state
|
||||
if get_status() != __DFU_STATE_DFU_DOWNLOAD_IDLE:
|
||||
|
||||
raise Exception("DFU: erase failed")
|
||||
|
||||
|
||||
def set_address(addr):
|
||||
"""Sets the address for the next operation."""
|
||||
# Send DNLOAD with first byte=0x21 and page address
|
||||
buf = struct.pack("<BI", 0x21, addr)
|
||||
__dev.ctrl_transfer(0x21, __DFU_DNLOAD, 0, __DFU_INTERFACE, buf, __TIMEOUT)
|
||||
|
||||
# Execute last command
|
||||
if get_status() != __DFU_STATE_DFU_DOWNLOAD_BUSY:
|
||||
raise Exception("DFU: set address failed")
|
||||
|
||||
# Check command state
|
||||
if get_status() != __DFU_STATE_DFU_DOWNLOAD_IDLE:
|
||||
raise Exception("DFU: set address failed")
|
||||
|
||||
|
||||
def write_memory(addr, buf, progress=None, progress_addr=0, progress_size=0):
|
||||
"""Writes a buffer into memory. This routine assumes that memory has
|
||||
already been erased.
|
||||
"""
|
||||
|
||||
xfer_count = 0
|
||||
xfer_bytes = 0
|
||||
xfer_total = len(buf)
|
||||
xfer_base = addr
|
||||
|
||||
while xfer_bytes < xfer_total:
|
||||
if __verbose and xfer_count % 512 == 0:
|
||||
print ("Addr 0x%x %dKBs/%dKBs..." % (xfer_base + xfer_bytes,
|
||||
xfer_bytes // 1024,
|
||||
xfer_total // 1024))
|
||||
if progress and xfer_count % 2 == 0:
|
||||
progress(progress_addr, xfer_base + xfer_bytes - progress_addr,
|
||||
progress_size)
|
||||
|
||||
# Set mem write address
|
||||
set_address(xfer_base+xfer_bytes)
|
||||
|
||||
# Send DNLOAD with fw data
|
||||
# the "2048" is the DFU transfer size supported by the ST DFU bootloader
|
||||
# TODO: this number should be extracted from the USB config descriptor
|
||||
chunk = min(2048, xfer_total-xfer_bytes)
|
||||
__dev.ctrl_transfer(0x21, __DFU_DNLOAD, 2, __DFU_INTERFACE,
|
||||
buf[xfer_bytes:xfer_bytes + chunk], __TIMEOUT)
|
||||
|
||||
# Execute last command
|
||||
if get_status() != __DFU_STATE_DFU_DOWNLOAD_BUSY:
|
||||
raise Exception("DFU: write memory failed")
|
||||
|
||||
# Check command state
|
||||
if get_status() != __DFU_STATE_DFU_DOWNLOAD_IDLE:
|
||||
raise Exception("DFU: write memory failed")
|
||||
|
||||
xfer_count += 1
|
||||
xfer_bytes += chunk
|
||||
|
||||
|
||||
def write_page(buf, xfer_offset):
|
||||
"""Writes a single page. This routine assumes that memory has already
|
||||
been erased.
|
||||
"""
|
||||
|
||||
xfer_base = 0x08000000
|
||||
|
||||
# Set mem write address
|
||||
set_address(xfer_base+xfer_offset)
|
||||
|
||||
# Send DNLOAD with fw data
|
||||
__dev.ctrl_transfer(0x21, __DFU_DNLOAD, 2, __DFU_INTERFACE, buf, __TIMEOUT)
|
||||
|
||||
# Execute last command
|
||||
if get_status() != __DFU_STATE_DFU_DOWNLOAD_BUSY:
|
||||
raise Exception("DFU: write memory failed")
|
||||
|
||||
# Check command state
|
||||
if get_status() != __DFU_STATE_DFU_DOWNLOAD_IDLE:
|
||||
raise Exception("DFU: write memory failed")
|
||||
|
||||
if __verbose:
|
||||
print ("Write: 0x%x " % (xfer_base + xfer_offset))
|
||||
|
||||
|
||||
def exit_dfu():
|
||||
"""Exit DFU mode, and start running the program."""
|
||||
|
||||
# set jump address
|
||||
set_address(0x08000000)
|
||||
|
||||
# Send DNLOAD with 0 length to exit DFU
|
||||
__dev.ctrl_transfer(0x21, __DFU_DNLOAD, 0, __DFU_INTERFACE,
|
||||
None, __TIMEOUT)
|
||||
|
||||
try:
|
||||
# Execute last command
|
||||
if get_status() != __DFU_STATE_DFU_MANIFEST:
|
||||
print("Failed to reset device")
|
||||
|
||||
# Release device
|
||||
usb.util.dispose_resources(__dev)
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
def named(values, names):
|
||||
"""Creates a dict with `names` as fields, and `values` as values."""
|
||||
return dict(zip(names.split(), values))
|
||||
|
||||
|
||||
def consume(fmt, data, names):
|
||||
"""Parses the struct defined by `fmt` from `data`, stores the parsed fields
|
||||
into a named tuple using `names`. Returns the named tuple, and the data
|
||||
with the struct stripped off."""
|
||||
size = struct.calcsize(fmt)
|
||||
return named(struct.unpack(fmt, data[:size]), names), data[size:]
|
||||
|
||||
|
||||
def cstring(string):
|
||||
"""Extracts a null-terminated string from a byte array."""
|
||||
return string.decode('utf-8').split('\0', 1)[0]
|
||||
|
||||
|
||||
def compute_crc(data):
|
||||
"""Computes the CRC32 value for the data passed in."""
|
||||
return 0xFFFFFFFF & -zlib.crc32(data) - 1
|
||||
|
||||
|
||||
def read_dfu_file(filename):
|
||||
"""Reads a DFU file, and parses the individual elements from the file.
|
||||
Returns an array of elements. Each element is a dictionary with the
|
||||
following keys:
|
||||
num - The element index
|
||||
address - The address that the element data should be written to.
|
||||
size - The size of the element ddata.
|
||||
data - The element data.
|
||||
If an error occurs while parsing the file, then None is returned.
|
||||
"""
|
||||
|
||||
print("File: {}".format(filename))
|
||||
with open(filename, 'rb') as fin:
|
||||
data = fin.read()
|
||||
crc = compute_crc(data[:-4])
|
||||
elements = []
|
||||
|
||||
# Decode the DFU Prefix
|
||||
#
|
||||
# <5sBIB
|
||||
# < little endian
|
||||
# 5s char[5] signature "DfuSe"
|
||||
# B uint8_t version 1
|
||||
# I uint32_t size Size of the DFU file (not including suffix)
|
||||
# B uint8_t targets Number of targets
|
||||
dfu_prefix, data = consume('<5sBIB', data,
|
||||
'signature version size targets')
|
||||
print (" %(signature)s v%(version)d, image size: %(size)d, "
|
||||
"targets: %(targets)d" % dfu_prefix)
|
||||
for target_idx in range(dfu_prefix['targets']):
|
||||
# Decode the Image Prefix
|
||||
#
|
||||
# <6sBI255s2I
|
||||
# < little endian
|
||||
# 6s char[6] signature "Target"
|
||||
# B uint8_t altsetting
|
||||
# I uint32_t named bool indicating if a name was used
|
||||
# 255s char[255] name name of the target
|
||||
# I uint32_t size size of image (not incl prefix)
|
||||
# I uint32_t elements Number of elements in the image
|
||||
img_prefix, data = consume('<6sBI255s2I', data,
|
||||
'signature altsetting named name '
|
||||
'size elements')
|
||||
img_prefix['num'] = target_idx
|
||||
if img_prefix['named']:
|
||||
img_prefix['name'] = cstring(img_prefix['name'])
|
||||
else:
|
||||
img_prefix['name'] = ''
|
||||
print(' %(signature)s %(num)d, alt setting: %(altsetting)s, '
|
||||
'name: "%(name)s", size: %(size)d, elements: %(elements)d'
|
||||
% img_prefix)
|
||||
|
||||
target_size = img_prefix['size']
|
||||
target_data, data = data[:target_size], data[target_size:]
|
||||
for elem_idx in range(img_prefix['elements']):
|
||||
# Decode target prefix
|
||||
# < little endian
|
||||
# I uint32_t element address
|
||||
# I uint32_t element size
|
||||
elem_prefix, target_data = consume('<2I', target_data, 'addr size')
|
||||
elem_prefix['num'] = elem_idx
|
||||
print(' %(num)d, address: 0x%(addr)08x, size: %(size)d'
|
||||
% elem_prefix)
|
||||
elem_size = elem_prefix['size']
|
||||
elem_data = target_data[:elem_size]
|
||||
target_data = target_data[elem_size:]
|
||||
elem_prefix['data'] = elem_data
|
||||
elements.append(elem_prefix)
|
||||
|
||||
if len(target_data):
|
||||
print("target %d PARSE ERROR" % target_idx)
|
||||
|
||||
# Decode DFU Suffix
|
||||
# < little endian
|
||||
# H uint16_t device Firmware version
|
||||
# H uint16_t product
|
||||
# H uint16_t vendor
|
||||
# H uint16_t dfu 0x11a (DFU file format version)
|
||||
# 3s char[3] ufd 'UFD'
|
||||
# B uint8_t len 16
|
||||
# I uint32_t crc32
|
||||
dfu_suffix = named(struct.unpack('<4H3sBI', data[:16]),
|
||||
'device product vendor dfu ufd len crc')
|
||||
print (' usb: %(vendor)04x:%(product)04x, device: 0x%(device)04x, '
|
||||
'dfu: 0x%(dfu)04x, %(ufd)s, %(len)d, 0x%(crc)08x' % dfu_suffix)
|
||||
if crc != dfu_suffix['crc']:
|
||||
print("CRC ERROR: computed crc32 is 0x%08x" % crc)
|
||||
return
|
||||
data = data[16:]
|
||||
if data:
|
||||
print("PARSE ERROR")
|
||||
return
|
||||
|
||||
return elements
|
||||
|
||||
|
||||
class FilterDFU(object):
|
||||
"""Class for filtering USB devices to identify devices which are in DFU
|
||||
mode.
|
||||
"""
|
||||
|
||||
def __call__(self, device):
|
||||
for cfg in device:
|
||||
for intf in cfg:
|
||||
return (intf.bInterfaceClass == 0xFE and
|
||||
intf.bInterfaceSubClass == 1)
|
||||
|
||||
|
||||
def get_dfu_devices(*args, **kwargs):
|
||||
"""Returns a list of USB device which are currently in DFU mode.
|
||||
Additional filters (like idProduct and idVendor) can be passed in to
|
||||
refine the search.
|
||||
"""
|
||||
# convert to list for compatibility with newer pyusb
|
||||
return list(usb.core.find(*args, find_all=True,
|
||||
custom_match=FilterDFU(), **kwargs))
|
||||
|
||||
|
||||
def get_memory_layout(device):
|
||||
"""Returns an array which identifies the memory layout. Each entry
|
||||
of the array will contain a dictionary with the following keys:
|
||||
addr - Address of this memory segment
|
||||
last_addr - Last address contained within the memory segment.
|
||||
size - size of the segment, in bytes
|
||||
num_pages - number of pages in the segment
|
||||
page_size - size of each page, in bytes
|
||||
"""
|
||||
cfg = device[0]
|
||||
intf = cfg[(0, 0)]
|
||||
mem_layout_str = get_string(device, intf.iInterface)
|
||||
mem_layout = mem_layout_str.split('/')
|
||||
result = []
|
||||
for mem_layout_index in range(1, len(mem_layout), 2):
|
||||
addr = int(mem_layout[mem_layout_index], 0)
|
||||
segments = mem_layout[mem_layout_index + 1].split(',')
|
||||
seg_re = re.compile(r'(\d+)\*(\d+)(.)(.)')
|
||||
for segment in segments:
|
||||
seg_match = seg_re.match(segment)
|
||||
num_pages = int(seg_match.groups()[0], 10)
|
||||
page_size = int(seg_match.groups()[1], 10)
|
||||
multiplier = seg_match.groups()[2]
|
||||
if multiplier == 'K':
|
||||
page_size *= 1024
|
||||
if multiplier == 'M':
|
||||
page_size *= 1024 * 1024
|
||||
size = num_pages * page_size
|
||||
last_addr = addr + size - 1
|
||||
result.append(named((addr, last_addr, size, num_pages, page_size),
|
||||
"addr last_addr size num_pages page_size"))
|
||||
addr += size
|
||||
return result
|
||||
|
||||
|
||||
def list_dfu_devices(*args, **kwargs):
|
||||
"""Prints a lits of devices detected in DFU mode."""
|
||||
devices = get_dfu_devices(*args, **kwargs)
|
||||
if not devices:
|
||||
print("No DFU capable devices found")
|
||||
return
|
||||
for device in devices:
|
||||
print("Bus {} Device {:03d}: ID {:04x}:{:04x}"
|
||||
.format(device.bus, device.address,
|
||||
device.idVendor, device.idProduct))
|
||||
layout = get_memory_layout(device)
|
||||
print("Memory Layout")
|
||||
for entry in layout:
|
||||
print(" 0x{:x} {:2d} pages of {:3d}K bytes"
|
||||
.format(entry['addr'], entry['num_pages'],
|
||||
entry['page_size'] // 1024))
|
||||
|
||||
|
||||
def write_elements(elements, mass_erase_used, progress=None):
|
||||
"""Writes the indicated elements into the target memory,
|
||||
erasing as needed.
|
||||
"""
|
||||
|
||||
mem_layout = get_memory_layout(__dev)
|
||||
for elem in elements:
|
||||
addr = elem['addr']
|
||||
size = elem['size']
|
||||
data = elem['data']
|
||||
elem_size = size
|
||||
elem_addr = addr
|
||||
if progress:
|
||||
progress(elem_addr, 0, elem_size)
|
||||
while size > 0:
|
||||
write_size = size
|
||||
if not mass_erase_used:
|
||||
for segment in mem_layout:
|
||||
if addr >= segment['addr'] and \
|
||||
addr <= segment['last_addr']:
|
||||
# We found the page containing the address we want to
|
||||
# write, erase it
|
||||
page_size = segment['page_size']
|
||||
page_addr = addr & ~(page_size - 1)
|
||||
if addr + write_size > page_addr + page_size:
|
||||
write_size = page_addr + page_size - addr
|
||||
page_erase(page_addr)
|
||||
break
|
||||
write_memory(addr, data[:write_size], progress,
|
||||
elem_addr, elem_size)
|
||||
data = data[write_size:]
|
||||
addr += write_size
|
||||
size -= write_size
|
||||
if progress:
|
||||
progress(elem_addr, addr - elem_addr, elem_size)
|
||||
|
||||
|
||||
def cli_progress(addr, offset, size):
|
||||
"""Prints a progress report suitable for use on the command line."""
|
||||
width = 25
|
||||
done = offset * width // size
|
||||
print("\r0x{:08x} {:7d} [{}{}] {:3d}% "
|
||||
.format(addr, size, '=' * done, ' ' * (width - done),
|
||||
offset * 100 // size), end="")
|
||||
sys.stdout.flush()
|
||||
if offset == size:
|
||||
print("")
|
||||
|
||||
|
||||
def main():
|
||||
"""Test program for verifying this files functionality."""
|
||||
global __verbose
|
||||
# Parse CMD args
|
||||
parser = argparse.ArgumentParser(description='DFU Python Util')
|
||||
#parser.add_argument("path", help="file path")
|
||||
parser.add_argument(
|
||||
"-l", "--list",
|
||||
help="list available DFU devices",
|
||||
action="store_true",
|
||||
default=False
|
||||
)
|
||||
parser.add_argument(
|
||||
"-m", "--mass-erase",
|
||||
help="mass erase device",
|
||||
action="store_true",
|
||||
default=False
|
||||
)
|
||||
parser.add_argument(
|
||||
"-u", "--upload",
|
||||
help="read file from DFU device",
|
||||
dest="path",
|
||||
default=False
|
||||
)
|
||||
parser.add_argument(
|
||||
"-v", "--verbose",
|
||||
help="increase output verbosity",
|
||||
action="store_true",
|
||||
default=False
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
__verbose = args.verbose
|
||||
|
||||
if args.list:
|
||||
list_dfu_devices(idVendor=__VID, idProduct=__PID)
|
||||
return
|
||||
|
||||
init()
|
||||
|
||||
if args.mass_erase:
|
||||
print ("Mass erase...")
|
||||
mass_erase()
|
||||
|
||||
if args.path:
|
||||
elements = read_dfu_file(args.path)
|
||||
if not elements:
|
||||
return
|
||||
print("Writing memory...")
|
||||
write_elements(elements, args.mass_erase, progress=cli_progress)
|
||||
|
||||
print("Exiting DFU...")
|
||||
exit_dfu()
|
||||
return
|
||||
|
||||
print("No command specified")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -0,0 +1,49 @@
|
|||
from pydfu import *
|
||||
import urllib.request, tempfile, os, shutil, ssl
|
||||
|
||||
def firmware_update(verbose):
|
||||
global __verbose
|
||||
__verbose = verbose
|
||||
|
||||
temp_path = tempfile.mktemp("firmware.dfu")
|
||||
url = "https://update.badge.emfcamp.org/firmware.dfu"
|
||||
|
||||
print("Hello - Welcome to the automated TiLDA Mk4 firmware updater")
|
||||
print("Finding badge: ", end="")
|
||||
try:
|
||||
init()
|
||||
print("DONE")
|
||||
|
||||
print("Downloading newest firmware: ", end="")
|
||||
context = ssl._create_unverified_context()
|
||||
with urllib.request.urlopen(url, context=context) as response:
|
||||
with open(temp_path, 'wb') as tmp_file:
|
||||
shutil.copyfileobj(response, tmp_file)
|
||||
print("DONE")
|
||||
|
||||
elements = read_dfu_file(temp_path)
|
||||
if not elements:
|
||||
return
|
||||
|
||||
print("Resetting Badge: ", end="")
|
||||
mass_erase()
|
||||
print("DONE")
|
||||
|
||||
print("Updating...")
|
||||
write_elements(elements, True, progress=cli_progress)
|
||||
exit_dfu()
|
||||
|
||||
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 ValueError as e:
|
||||
print("FAIL")
|
||||
print("")
|
||||
print("We couldn't find your badge. You need to make sure it's plugged in and in DFU mode.")
|
||||
print("To put your badge into DFU mode you need to press the joystick in the middle while pressing the reset button at the back.")
|
||||
print("After that, please try this script again.")
|
||||
print()
|
||||
print("Error: %s" %(e))
|
||||
finally:
|
||||
if os.path.isfile(temp_path): os.remove(temp_path)
|
|
@ -87,10 +87,9 @@ def get_resources(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}
|
||||
files = {full_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)):
|
||||
|
@ -135,13 +134,13 @@ 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")
|
||||
file = next(f for f in resource['files'] if "/main.py" in f)
|
||||
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:
|
||||
with open(os.path.join(path, file), "r") as stream:
|
||||
resource.update(_normalize_metadata(read_metadata(stream)))
|
||||
except ParseException as e:
|
||||
resource.setdefault("errors", []).append(file + ": " + str(e))
|
||||
|
@ -166,7 +165,6 @@ def resolve_dependencies(resources):
|
|||
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:
|
||||
|
@ -197,7 +195,7 @@ def _validate_resource(path, resource):
|
|||
if file.endswith(".py"):
|
||||
try:
|
||||
filename = os.path.join(path, file)
|
||||
with open(filename, 'r', encoding='utf8') as s:
|
||||
with open(filename, 'r') as s:
|
||||
compile(s.read() + '\n', filename, 'exec')
|
||||
except Exception as e:
|
||||
resource.setdefault("errors", []).append(str(e))
|
||||
|
@ -245,6 +243,6 @@ def pretty_print_resources(resources):
|
|||
|
||||
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:
|
||||
if "." in dependency or "/" in dependency or "upip:" in dependency:
|
||||
return dependency
|
||||
return os.path.join("lib", "%s.py" % dependency)
|
||||
return "lib/%s.py" % dependency
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import os, shutil, sys, fnmatch, glob, pyboard_util
|
||||
import os, shutil, sys, fnmatch
|
||||
|
||||
def sync(args, patterns, resources, verbose, skip_wifi):
|
||||
root = get_root(verbose)
|
||||
def sync(storage, patterns, resources, verbose):
|
||||
root = get_root()
|
||||
|
||||
# Add all paths that are already files
|
||||
paths = set([p for p in (patterns or []) if os.path.isfile(os.path.join(root, p))])
|
||||
|
@ -10,10 +10,9 @@ def sync(args, patterns, resources, verbose, skip_wifi):
|
|||
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)
|
||||
wifi_path = os.path.join(root, "wifi.json")
|
||||
if os.path.isfile(wifi_path):
|
||||
paths.add(wifi_path)
|
||||
|
||||
if not patterns:
|
||||
patterns = ["*"]
|
||||
|
@ -29,27 +28,30 @@ def sync(args, patterns, resources, verbose, skip_wifi):
|
|||
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):
|
||||
if not found:
|
||||
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)
|
||||
print("Copying %s files: " % len(paths), end="")
|
||||
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))
|
||||
print("Copying %s..." % rel_path)
|
||||
else:
|
||||
if updated:
|
||||
print("+", end="", flush=True)
|
||||
else:
|
||||
print("=", end="", flush=True)
|
||||
pyboard_util.end_copy_via_repl(args)
|
||||
print(".", end="")
|
||||
|
||||
target = os.path.join(storage, rel_path)
|
||||
target_dir = os.path.dirname(target)
|
||||
if os.path.isfile(target_dir):
|
||||
# micropython has the tendency to sometimes corrupt directories into files
|
||||
os.remove(target_dir)
|
||||
if not os.path.exists(target_dir):
|
||||
os.makedirs(target_dir)
|
||||
shutil.copy2(path, target)
|
||||
|
||||
if verbose:
|
||||
print("Files copied successfully")
|
||||
|
@ -57,19 +59,18 @@ def sync(args, patterns, resources, verbose, skip_wifi):
|
|||
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')
|
||||
def set_boot_app(storage, app_to_boot):
|
||||
path = os.path.join(storage, 'once.txt')
|
||||
try:
|
||||
os.remove(path)
|
||||
except OSError:
|
||||
pass
|
||||
with open(path, 'w') as f:
|
||||
f.write(app_to_boot + "\n")
|
||||
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):
|
||||
def get_root():
|
||||
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.")
|
||||
|
|
|
@ -11,18 +11,18 @@ $ tilda_tools reset
|
|||
Soft reboot badge and start specific app
|
||||
$ tilda_tools reset --boot my_app
|
||||
|
||||
Update files on the badge to match the current local version, restarts afterwards
|
||||
$ tilda_tools sync
|
||||
|
||||
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>]
|
||||
$ tilda_toold.py 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
|
||||
$ tilda_toold.py sync --run some_other_file.py
|
||||
|
||||
Executes a single file on the badge without copying anything (Using pyboard.py)
|
||||
$ tilda_tools run my_app/main.py
|
||||
|
@ -47,21 +47,18 @@ Common parameters
|
|||
|
||||
"""
|
||||
|
||||
import glob
|
||||
import sync, firmware_update, wifi, pyboard_util, sys
|
||||
import sys, glob
|
||||
import sync, pyboard_util, wifi
|
||||
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('command', nargs=1, help='command [test|reset|sync|run|validate|wifi|firmware-update]', choices=['test', 'reset', 'sync', 'validate', 'run', 'wifi', 'firmware-update'])
|
||||
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')
|
||||
|
@ -72,25 +69,14 @@ def main():
|
|||
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)
|
||||
import pydfu_util # to avoid having a "usb" dependency for other calls
|
||||
pydfu_util.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"]:
|
||||
if command in ["test", "validate", "sync"]:
|
||||
resources = get_resources(path)
|
||||
add_metadata(path, resources)
|
||||
validate(path, resources)
|
||||
|
@ -107,44 +93,24 @@ def main():
|
|||
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)
|
||||
args.paths = ["lib/test_*"]
|
||||
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 in ["reset", "sync"]:
|
||||
pyboard_util.stop_badge(args, args.verbose)
|
||||
|
||||
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]
|
||||
synced_resources = sync.sync(get_storage(args), paths, resources, args.verbose)
|
||||
|
||||
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 command in ["reset", "sync"]:
|
||||
sync.set_boot_app(get_storage(args), args.boot or "")
|
||||
pyboard_util.soft_reset(args)
|
||||
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)
|
||||
|
@ -156,8 +122,24 @@ def main():
|
|||
pyboard_util.run(args, [resource], False)
|
||||
pyboard_util.soft_reset(args, False)
|
||||
|
||||
|
||||
|
||||
pyboard_util.close_pyb()
|
||||
|
||||
def find_storage():
|
||||
# todo: find solution for windows and linux
|
||||
for pattern in ['/Volumes/PYBFLASH', '/Volumes/NO NAME']:
|
||||
for path in glob.glob(pattern):
|
||||
return path
|
||||
print("Couldn't find badge storage - Please make it's plugged in and reset it if necessary")
|
||||
sys.exit(1)
|
||||
|
||||
def get_storage(args):
|
||||
if not args.storage:
|
||||
args.storage = find_storage()
|
||||
return args.storage
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
.DS_Store
|
||||
__pycache__
|
||||
wifi*.json
|
||||
config.json
|
||||
cmd.exe.lnk
|
||||
tilda_tools.bat
|
||||
wifi.json
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
361
3dspin/main.py
|
@ -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()
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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/
|
|
@ -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()
|
20
README
|
@ -1,17 +1,9 @@
|
|||
TiLDA Mk4 App Library
|
||||
-------------------
|
||||
|
||||
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
|
||||
* How to use tilda_tools.py
|
||||
* How to transfer this to badge
|
||||
* How to fork
|
||||
* How to make your own changes
|
||||
* How to run tests
|
||||
* How to send a PR
|
||||
|
|
|
@ -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()
|
8
adhoc.py
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
160
avatar/main.py
|
@ -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 ###
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
|
@ -6,129 +6,65 @@ To publish apps use https://badge.emfcamp.org"""
|
|||
|
||||
___license___ = "MIT"
|
||||
___title___ = "Badge Store"
|
||||
___dependencies___ = ["badge_store", "dialogs", "ugfx_helper", "app", "database", "ospath"]
|
||||
___dependencies___ = ["wifi", "dialogs"]
|
||||
___categories___ = ["System"]
|
||||
___bootstrapped___ = True
|
||||
|
||||
import ugfx_helper, os, database, wifi, app, ospath
|
||||
from dialogs import *
|
||||
from lib.badge_store import BadgeStore
|
||||
from app import *
|
||||
import pyb
|
||||
import ugfx
|
||||
import os
|
||||
#import http_client
|
||||
import wifi
|
||||
import dialogs
|
||||
#from app import App, get_local_apps, get_public_apps, get_public_app_categories, empty_local_app_cache
|
||||
#import filesystem
|
||||
|
||||
TEMP_FILE = ".temp_download"
|
||||
|
||||
ugfx.init()
|
||||
|
||||
### VIEWS ###
|
||||
|
||||
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()
|
||||
ugfx.clear(ugfx.html_color(0x7c1143))
|
||||
|
||||
def show_categories():
|
||||
clear()
|
||||
with WaitingMessage(title=title, text="Loading categories..."):
|
||||
menu_items = [{"title": c, "category": c} for c in store.get_categories()]
|
||||
def store():
|
||||
None
|
||||
|
||||
option = prompt_option(menu_items, none_text="Back", title="Install: Categories")
|
||||
def update():
|
||||
None
|
||||
|
||||
if option:
|
||||
show_apps(option["category"])
|
||||
else:
|
||||
return
|
||||
def remove():
|
||||
None
|
||||
|
||||
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 settings():
|
||||
None
|
||||
|
||||
def main_menu():
|
||||
while True:
|
||||
clear()
|
||||
|
||||
print()
|
||||
|
||||
menu_items = [
|
||||
{"title": "Install Apps", "function": show_categories},
|
||||
{"title": "Update all Apps", "function": show_update},
|
||||
{"title": "Remove App", "function": show_remove}
|
||||
{"title": "Install Apps", "function": store},
|
||||
{"title": "Update", "function": update},
|
||||
{"title": "Manage Apps", "function": remove},
|
||||
{"title": "Settings", "function": settings}
|
||||
]
|
||||
|
||||
option = prompt_option(menu_items, none_text="Exit", text="What do you want to do?", title=title)
|
||||
option = dialogs.prompt_option(menu_items, none_text="Exit", text="What do you want to do?", title="TiLDA App Library")
|
||||
|
||||
if option:
|
||||
option["function"]()
|
||||
else:
|
||||
break
|
||||
return
|
||||
|
||||
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()
|
||||
|
||||
#if App("home").loadable:
|
||||
# main_menu()
|
||||
#else:
|
||||
# for app_name in ["changename", "snake", "alistair~selectwifi", "sponsors", "home"]:
|
||||
# install(App(app_name))
|
||||
# pyb.hard_reset()
|
||||
|
|
|
@ -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()
|
|
@ -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()
|
89
beer/main.py
|
@ -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()
|
|
@ -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
|
||||
|
|
@ -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()
|
36
boot.py
|
@ -1,11 +1,9 @@
|
|||
import os, tilda
|
||||
from machine import Neopix
|
||||
import pyb, os, micropython, sys
|
||||
|
||||
n=Neopix()
|
||||
n.display([0,0,0])
|
||||
n.display([0,0,0])
|
||||
micropython.alloc_emergency_exception_buf(100)
|
||||
|
||||
sys.path.append('/flash/upip')
|
||||
|
||||
print("EMF: boot.py")
|
||||
os.sync()
|
||||
root = os.listdir()
|
||||
|
||||
|
@ -14,6 +12,7 @@ def app(a):
|
|||
return a + "/main.py"
|
||||
|
||||
def file(file, remove):
|
||||
print(file)
|
||||
try:
|
||||
a = None
|
||||
with open(file, 'r') as f:
|
||||
|
@ -22,25 +21,14 @@ def file(file, remove):
|
|||
os.remove(file)
|
||||
return app(a)
|
||||
except Exception as e:
|
||||
print("Not found: %s" % file)
|
||||
print(e)
|
||||
|
||||
def any_home():
|
||||
h = [a for a in root if a.startswith("home")]
|
||||
return h[0] if len(h) else False
|
||||
return app(next(a for a in root if a.startswith("home")))
|
||||
|
||||
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)
|
||||
start = None
|
||||
if "main.py" in root:
|
||||
start = "main.py"
|
||||
start = file("once.txt", True) or file("default_app.txt", False) or any_home() or "bootstrap.py"
|
||||
|
||||
pyb.main(start)
|
||||
|
|
199
bootstrap.py
|
@ -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
|
235
breakout/main.py
|
@ -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()
|
|
@ -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()
|
|
@ -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)
|
BIN
cmd.exe.lnk
|
@ -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()
|
|
@ -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)
|
|
@ -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()
|
|
@ -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)
|
Before Width: | Height: | Size: 2.3 KiB |
75
enby/main.py
|
@ -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()
|
|
@ -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()
|
|
@ -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()
|
Before Width: | Height: | Size: 136 KiB |
BIN
holland/eu.png
Before Width: | Height: | Size: 34 KiB |
266
holland/main.py
|
@ -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)
|
Before Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 117 KiB |
Before Width: | Height: | Size: 117 KiB |
|
@ -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()
|
Before Width: | Height: | Size: 14 KiB |
|
@ -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)
|
|
@ -5,94 +5,16 @@ 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"]
|
||||
___name___ = "Homescreen (Default)"
|
||||
___license___ = "GPL"
|
||||
___categories___ = ["homescreen"]
|
||||
___launchable___ = False
|
||||
___bootstrapped___ = True
|
||||
|
||||
import ugfx
|
||||
from homescreen import *
|
||||
import time
|
||||
from tilda import Buttons
|
||||
print("there")
|
||||
import ugfx, homescreen
|
||||
|
||||
# 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
|
||||
homescreen.init(color = 0xe4ffdb)
|
||||
|
||||
# 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)
|
||||
ugfx.display_image(0, 0, "home_default/bg.gif")
|
||||
ugfx.text(20, 20, homescreen.name(), ugfx.BLACK)
|
||||
|
|
Before Width: | Height: | Size: 31 KiB |
|
@ -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()
|
|
@ -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)
|
Before Width: | Height: | Size: 963 B |
Before Width: | Height: | Size: 3.4 KiB |
|
@ -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()
|
Before Width: | Height: | Size: 3.5 KiB |
|
@ -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)
|
Before Width: | Height: | Size: 2.6 KiB |
|
@ -1,23 +1,15 @@
|
|||
"""Launcher for apps currently installed"""
|
||||
|
||||
___title___ = "Launcher"
|
||||
___name___ = "Launcher"
|
||||
___license___ = "MIT"
|
||||
___categories___ = ["System", "Launcher"]
|
||||
___dependencies___ = ["dialogs", "app", "ugfx_helper"]
|
||||
___categories___ = ["System"]
|
||||
___dependencies___ = ["shared/test/download.txt"]
|
||||
___launchable___ = False
|
||||
___bootstrapped___ = True
|
||||
|
||||
import ugfx_helper, ugfx
|
||||
from app import *
|
||||
from dialogs import *
|
||||
import ugfx
|
||||
|
||||
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()
|
||||
print("launcher")
|
||||
|
|
19
lib/app.py
|
@ -7,7 +7,6 @@ ___license___ = "MIT"
|
|||
___dependencies___ = ["metadata_reader", "ospath"]
|
||||
|
||||
from ospath import *
|
||||
import os, machine
|
||||
from metadata_reader import read_metadata
|
||||
|
||||
class App:
|
||||
|
@ -58,10 +57,6 @@ class App:
|
|||
return self.attributes[attribute]
|
||||
return default
|
||||
|
||||
def boot(self):
|
||||
write_launch_file(self.name)
|
||||
machine.reset()
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
|
@ -93,10 +88,6 @@ def get_apps(category=None):
|
|||
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
|
||||
|
@ -106,13 +97,3 @@ def get_categories():
|
|||
_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()
|
||||
|
||||
|
|
|
@ -8,37 +8,33 @@ from http import *
|
|||
import hashlib, binascii
|
||||
|
||||
class BadgeStore:
|
||||
def __init__(self, url = "http://badgeserver.emfcamp.org/2018", repo="emfcamp/Mk4-Apps", ref="master"):
|
||||
def __init__(self, url = "http://badge.marekventur.com", repo="emfcamp/Mk4-Apps", ref="master"):
|
||||
self.url = url
|
||||
self.repo = repo
|
||||
self.ref = ref
|
||||
self._apps = None
|
||||
|
||||
def get_all_apps(self):
|
||||
def get_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()
|
||||
return self.get_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"))
|
||||
files = self._call("install", {"apps": ",".join(apps)})
|
||||
installers = []
|
||||
url = "%s/download" % (self.url)
|
||||
for path, hash in files.items():
|
||||
if self._is_file_up_to_date(path, hash):
|
||||
continue
|
||||
params = {"repo": self.repo, "ref": self.ref, "path": path}
|
||||
installers.append(Installer(path, url, params))
|
||||
return installers
|
||||
|
||||
def _call(self, command, params = {}):
|
||||
params["repo"] = self.repo
|
||||
|
@ -46,61 +42,28 @@ class BadgeStore:
|
|||
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)
|
||||
if not isfile(path):
|
||||
return False
|
||||
|
||||
TEMP_FILE = ".tmp.download"
|
||||
with open(path, "rb") as file:
|
||||
sha256 = hashlib.sha256()
|
||||
buf = file.read(128)
|
||||
while len(buf) > 0:
|
||||
sha256.update(buf)
|
||||
buf = file.read(128)
|
||||
current = str(binascii.hexlify(sha256.digest()), "utf8")[:10]
|
||||
return current == hash
|
||||
|
||||
class Installer:
|
||||
def __init__(self, path, url, params, hash):
|
||||
def __init__(self, path, url, params):
|
||||
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))
|
||||
with get(self.url, params=self.params).raise_for_status() as response:
|
||||
response.download(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]
|
||||
|
||||
|
|
106
lib/buttons.py
|
@ -1,24 +1,54 @@
|
|||
"""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
|
||||
"""
|
||||
"""Convenience methods for dealing with the TiLDA buttons"""
|
||||
|
||||
___license___ = "MIT"
|
||||
|
||||
import machine, time, tilda
|
||||
import pyb
|
||||
|
||||
# Convenience
|
||||
Buttons = tilda.Buttons
|
||||
CONFIG = {
|
||||
"JOY_UP": pyb.Pin.PULL_DOWN,
|
||||
"JOY_DOWN": pyb.Pin.PULL_DOWN,
|
||||
"JOY_RIGHT": pyb.Pin.PULL_DOWN,
|
||||
"JOY_LEFT": pyb.Pin.PULL_DOWN,
|
||||
"JOY_CENTER": pyb.Pin.PULL_DOWN,
|
||||
"BTN_MENU": pyb.Pin.PULL_UP,
|
||||
"BTN_A": pyb.Pin.PULL_UP,
|
||||
"BTN_B": pyb.Pin.PULL_UP
|
||||
}
|
||||
|
||||
ROTATION_MAP = {
|
||||
"JOY_UP": "JOY_LEFT",
|
||||
"JOY_LEFT": "JOY_DOWN",
|
||||
"JOY_DOWN": "JOY_RIGHT",
|
||||
"JOY_RIGHT": "JOY_UP",
|
||||
}
|
||||
|
||||
_tilda_pins = {}
|
||||
_tilda_interrupts = {}
|
||||
_tilda_bounce = {}
|
||||
|
||||
def _get_pin(button):
|
||||
if button not in _tilda_pins:
|
||||
raise ValueError("Please call button.init() first before using any other button functions")
|
||||
return _tilda_pins[button]
|
||||
|
||||
def init(buttons = CONFIG.keys()):
|
||||
"""Inits all pins used by the TiLDA badge"""
|
||||
global _tilda_pins
|
||||
for button in buttons:
|
||||
_tilda_pins[button] = pyb.Pin(button, pyb.Pin.IN)
|
||||
_tilda_pins[button].init(pyb.Pin.IN, CONFIG[button])
|
||||
|
||||
def rotate(button):
|
||||
"""remaps names of buttons to rotated values"""
|
||||
return ROTATION_MAP[button]
|
||||
|
||||
|
||||
def is_pressed(button):
|
||||
return tilda.Buttons.is_pressed(button)
|
||||
pin = _get_pin(button)
|
||||
if pin.pull() == pyb.Pin.PULL_DOWN:
|
||||
return pin.value() > 0
|
||||
else:
|
||||
return pin.value() == 0
|
||||
|
||||
def is_triggered(button, interval = 30):
|
||||
"""Use this function if you want buttons as a trigger for something in a loop
|
||||
|
@ -29,40 +59,31 @@ def is_triggered(button, interval = 30):
|
|||
global _tilda_bounce
|
||||
if is_pressed(button):
|
||||
if button in _tilda_bounce:
|
||||
if time.ticks_ms() > _tilda_bounce[button]:
|
||||
if pyb.millis() > _tilda_bounce[button]:
|
||||
del _tilda_bounce[button]
|
||||
else:
|
||||
return False # The button might have bounced back to high
|
||||
|
||||
# Wait for a while to avoid bounces to low
|
||||
time.sleep_ms(interval)
|
||||
pyb.delay(interval)
|
||||
|
||||
# Wait until button is released again
|
||||
while is_pressed(button):
|
||||
time.sleep_ms(1)
|
||||
pyb.wfi()
|
||||
|
||||
_tilda_bounce[button] = time.ticks_ms() + interval
|
||||
_tilda_bounce[button] = pyb.millis() + 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
|
||||
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
|
||||
|
@ -84,30 +105,35 @@ def enable_interrupt(button, interrupt, on_press = True, on_release = False):
|
|||
|
||||
mode = None;
|
||||
if on_press and on_release:
|
||||
mode = machine.ExtInt.IRQ_RISING_FALLING
|
||||
mode = pyb.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
|
||||
if pin.pull() == pyb.Pin.PULL_DOWN:
|
||||
mode = pyb.ExtInt.IRQ_RISING if on_press else pyb.ExtInt.IRQ_FALLING
|
||||
else:
|
||||
mode = machine.ExtInt.IRQ_FALLING if on_press else machine.ExtInt.IRQ_RISING
|
||||
mode = pyb.ExtInt.IRQ_FALLING if on_press else pyb.ExtInt.IRQ_RISING
|
||||
|
||||
_tilda_interrupts[button] = {
|
||||
"interrupt": machine.ExtInt(pin, mode, pin.pull(), interrupt),
|
||||
"interrupt": pyb.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)
|
||||
pyb.ExtInt(interrupt["pin"], interrupt["mode"], interrupt["pin"].pull(), None)
|
||||
del _tilda_interrupts[button]
|
||||
init([button])
|
||||
|
||||
def disable_all_interrupt():
|
||||
raise Exception("interrupts don't work yet")
|
||||
for interrupt in _tilda_interrupts:
|
||||
disable_interrupt(interrupt)
|
||||
|
||||
def enable_menu_reset():
|
||||
import onboard
|
||||
enable_interrupt("BTN_MENU", lambda t:onboard.semihard_reset(), on_release = True)
|
||||
|
||||
def disable_menu_reset():
|
||||
disable_interrupt("BTN_MENU")
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ Values can be anything json can store, including a dict
|
|||
Usage:
|
||||
|
||||
import database
|
||||
with database.Database() as db:
|
||||
with database.open() as db:
|
||||
print(db.get("hello", "default"))
|
||||
db.set("foo", "world")
|
||||
db.delete("bar")
|
||||
|
|
262
lib/dialogs.py
|
@ -1,11 +1,11 @@
|
|||
"""Some basic UGFX powered dialogs"""
|
||||
|
||||
___license___ = "MIT"
|
||||
___dependencies___ = ["buttons", "sleep"]
|
||||
___dependencies___ = ["buttons"]
|
||||
|
||||
import ugfx, buttons, sleep
|
||||
from buttons import Buttons
|
||||
import time
|
||||
import ugfx
|
||||
import buttons
|
||||
import pyb
|
||||
|
||||
default_style_badge = ugfx.Style()
|
||||
default_style_badge.set_focus(ugfx.RED)
|
||||
|
@ -18,13 +18,11 @@ 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 notice(text, title="TiLDA", close_text="Close", width = 260, height = 180, font=ugfx.FONT_SMALL, style=None):
|
||||
prompt_boolean(text, title = title, true_text = close_text, false_text = None, width = width, height = height, font=font, style=style)
|
||||
|
||||
def prompt_boolean(text, title="TiLDA", true_text="Yes", false_text="No", font=FONT_SMALL, style=None):
|
||||
def prompt_boolean(text, title="TiLDA", true_text="Yes", false_text="No", width = 260, height = 180, font=ugfx.FONT_SMALL, style=None):
|
||||
"""A simple one and two-options dialog
|
||||
|
||||
if 'false_text' is set to None only one button is displayed.
|
||||
|
@ -33,59 +31,35 @@ def prompt_boolean(text, title="TiLDA", true_text="Yes", false_text="No", font=F
|
|||
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)
|
||||
ugfx.set_default_font(ugfx.FONT_MEDIUM_BOLD)
|
||||
window = ugfx.Container((ugfx.width() - width) // 2, (ugfx.height() - height) // 2, width, height, style=style)
|
||||
window.show()
|
||||
ugfx.set_default_font(font)
|
||||
window.text(5, 5, title, TILDA_COLOR)
|
||||
window.line(0, 25, width, 25, ugfx.BLACK)
|
||||
window.text(5, 10, title, TILDA_COLOR)
|
||||
window.line(0, 30, width, 30, ugfx.BLACK)
|
||||
|
||||
if false_text:
|
||||
true_text = "A: " + true_text
|
||||
false_text = "B: " + false_text
|
||||
|
||||
ugfx.set_default_font(font)
|
||||
label = ugfx.Label(5, 30, width - 10, height - 80, text = text, parent=window, 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
|
||||
label = ugfx.Label(5, 30, width - 10, height - 80, text = text, parent=window)
|
||||
ugfx.set_default_font(ugfx.FONT_MEDIUM_BOLD)
|
||||
button_yes = ugfx.Button(5, height - 40, width // 2 - 15 if false_text else width - 15, 30 , true_text, parent=window)
|
||||
button_no = ugfx.Button(width // 2 + 5, height - 40, width // 2 - 15, 30 , false_text, parent=window) if false_text else None
|
||||
|
||||
try:
|
||||
#button_yes.attach_input(ugfx.BTN_A,0) # todo: re-enable once working
|
||||
#if button_no: button_no.attach_input(ugfx.BTN_B,0)
|
||||
buttons.init()
|
||||
|
||||
button_yes.attach_input(ugfx.BTN_A,0)
|
||||
if button_no: button_no.attach_input(ugfx.BTN_B,0)
|
||||
|
||||
window.show()
|
||||
|
||||
while True:
|
||||
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]:])
|
||||
pyb.wfi()
|
||||
if buttons.is_triggered("BTN_A"): return True
|
||||
if buttons.is_triggered("BTN_B"): return False
|
||||
|
||||
finally:
|
||||
window.hide()
|
||||
|
@ -94,37 +68,44 @@ def prompt_boolean(text, title="TiLDA", true_text="Yes", false_text="No", font=F
|
|||
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):
|
||||
def prompt_text(description, init_text = "", true_text="OK", false_text="Back", width = 300, height = 200, font=ugfx.FONT_MEDIUM_BOLD, style=default_style_badge):
|
||||
"""Shows a dialog and keyboard that allows the user to input/change a string
|
||||
|
||||
Returns None if user aborts with button B
|
||||
"""
|
||||
|
||||
window = ugfx.Container(0, 0, ugfx.width(), ugfx.height())
|
||||
window = ugfx.Container(int((ugfx.width()-width)/2), int((ugfx.height()-height)/2), width, height, style=style)
|
||||
|
||||
if false_text:
|
||||
true_text = "A: " + true_text
|
||||
true_text = "M: " + 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
|
||||
if buttons.has_interrupt("BTN_MENU"):
|
||||
buttons.disable_interrupt("BTN_MENU")
|
||||
|
||||
ugfx.set_default_font(ugfx.FONT_MEDIUM)
|
||||
kb = ugfx.Keyboard(0, int(height/2), width, int(height/2), parent=window)
|
||||
edit = ugfx.Textbox(5, int(height/2)-30, int(width*4/5)-10, 25, text = init_text, parent=window)
|
||||
ugfx.set_default_font(ugfx.FONT_SMALL)
|
||||
button_yes = ugfx.Button(int(width*4/5), int(height/2)-30, int(width*1/5)-3, 25 , true_text, parent=window)
|
||||
button_no = ugfx.Button(int(width*4/5), int(height/2)-30-30, int(width/5)-3, 25 , false_text, parent=window) if false_text else None
|
||||
ugfx.set_default_font(font)
|
||||
label = ugfx.Label(ugfx.width()//10, ugfx.height()//10, ugfx.width()*4//5, ugfx.height()*2//5-90, description, parent=window)
|
||||
label = ugfx.Label(int(width/10), int(height/10), int(width*4/5), int(height*2/5)-60, description, parent=window)
|
||||
|
||||
try:
|
||||
buttons.init()
|
||||
|
||||
button_yes.attach_input(ugfx.BTN_MENU,0)
|
||||
if button_no: button_no.attach_input(ugfx.BTN_B,0)
|
||||
|
||||
window.show()
|
||||
# edit.set_focus() todo: do we need this?
|
||||
edit.set_focus()
|
||||
while True:
|
||||
sleep.wfi()
|
||||
pyb.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)
|
||||
#if buttons.is_triggered("BTN_A"): return edit.text()
|
||||
if buttons.is_triggered("BTN_B"): return None
|
||||
if buttons.is_triggered("BTN_MENU"): return edit.text()
|
||||
|
||||
finally:
|
||||
window.hide()
|
||||
|
@ -136,148 +117,50 @@ def prompt_text(description, init_text="", true_text="OK", false_text="Back", fo
|
|||
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):
|
||||
def prompt_option(options, index=0, text = "Please select one of the following:", title=None, select_text="OK", none_text=None):
|
||||
"""Shows a dialog prompting for one of multiple options
|
||||
|
||||
If none_text is specified the user can use the B or Menu button to skip the selection
|
||||
if title is specified a blue title will be displayed about the text
|
||||
"""
|
||||
ugfx.set_default_font(FONT_SMALL)
|
||||
ugfx.set_default_font(ugfx.FONT_SMALL)
|
||||
window = ugfx.Container(5, 5, ugfx.width() - 10, ugfx.height() - 10)
|
||||
window.show()
|
||||
|
||||
|
||||
list_y = 30
|
||||
if title:
|
||||
window.text(5, 5, title, TILDA_COLOR)
|
||||
window.text(5, 10, 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)
|
||||
|
||||
window.text(5, 30, text, ugfx.BLACK)
|
||||
list_y = 50
|
||||
else:
|
||||
window.text(5, 10, text, ugfx.BLACK)
|
||||
|
||||
options_list = ugfx.List(5, list_y, ugfx.width() - 24, 265 - list_y, parent = window)
|
||||
options_list.disable_draw()
|
||||
options_list = ugfx.List(5, list_y, ugfx.width() - 25, 180 - list_y, parent = window)
|
||||
|
||||
optnum = 1
|
||||
for option in options:
|
||||
if isinstance(option, dict) and option["title"]:
|
||||
title = option["title"]
|
||||
options_list.add_item(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.add_item(str(option))
|
||||
options_list.selected_index(index)
|
||||
|
||||
select_text = "A: " + select_text
|
||||
if none_text:
|
||||
none_text = "B: " + none_text
|
||||
|
||||
button_select = ugfx.Button(5, ugfx.height() - 50, 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
|
||||
button_select = ugfx.Button(5, ugfx.height() - 50, 140 if none_text else ugfx.width() - 25, 30 , select_text, parent=window)
|
||||
button_none = ugfx.Button(ugfx.width() - 160, ugfx.height() - 50, 140, 30 , none_text, parent=window) if none_text else None
|
||||
|
||||
try:
|
||||
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)
|
||||
buttons.init()
|
||||
|
||||
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
|
||||
while True:
|
||||
pyb.wfi()
|
||||
ugfx.poll()
|
||||
if buttons.is_triggered("BTN_A"): return options[options_list.selected_index()]
|
||||
if button_none and buttons.is_triggered("BTN_B"): return None
|
||||
if button_none and buttons.is_triggered("BTN_MENU"): return None
|
||||
|
||||
finally:
|
||||
window.hide()
|
||||
|
@ -289,24 +172,23 @@ def prompt_option(options, index=0, text = None, title=None, select_text="OK", n
|
|||
|
||||
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"):
|
||||
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)
|
||||
self.window.text(5, 10, title, TILDA_COLOR)
|
||||
self.window.line(0, 30, ugfx.width() - 60, 30, ugfx.BLACK)
|
||||
self.label = ugfx.Label(5, 40, self.window.width() - 10, ugfx.height() - 40, text = text, parent=self.window)
|
||||
|
||||
# Indicator to show something is going on
|
||||
#self.indicator = ugfx.Label(ugfx.width() - 100, 0, 20, 20, text = "...", parent=self.window)
|
||||
#self.timer = 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
|
||||
self.indicator = ugfx.Label(ugfx.width() - 100, 0, 20, 20, text = "...", parent=self.window)
|
||||
self.timer = pyb.Timer(3)
|
||||
self.timer.init(freq=3)
|
||||
self.timer.callback(lambda t: self.indicator.visible(not self.indicator.visible()))
|
||||
|
||||
def destroy(self):
|
||||
#self.timer.deinit()
|
||||
self.timer.deinit()
|
||||
self.label.destroy()
|
||||
#self.indicator.destroy()
|
||||
self.indicator.destroy()
|
||||
self.window.destroy()
|
||||
|
||||
@property
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
@ -6,7 +6,7 @@ In particular, they *should*:
|
|||
|
||||
* Call "homescreen.init()" at the beginning. This will initiate ugfx, clear the screen and
|
||||
initiate button handline.
|
||||
* Use "sleep.wfi()" as much as possible to avoid draining the battery.
|
||||
* Use "pyb.wfi()" as much as possible to avoid draining the battery.
|
||||
* Not use
|
||||
|
||||
They also *may*:
|
||||
|
@ -16,64 +16,29 @@ They also *may*:
|
|||
* Display remaining battery "homescreen.battery()" (0-1)
|
||||
"""
|
||||
|
||||
___license___ = "MIT"
|
||||
___dependencies___ = ["database", "buttons", "app", "sleep", "ugfx_helper", "wifi", "sim800"]
|
||||
__license___ = "MIT"
|
||||
__dependencies___ = ["database", "buttons"]
|
||||
|
||||
import database, ugfx, random, buttons, tilda, sleep, ugfx_helper, wifi, time, sim800
|
||||
from app import App
|
||||
import database, ugfx, buttons
|
||||
|
||||
_state = None
|
||||
def init(enable_menu_button = True):
|
||||
global _state
|
||||
_state = {"menu": False}
|
||||
ugfx_helper.init()
|
||||
def init(color = 0xFFFFFF):
|
||||
ugfx.init()
|
||||
ugfx.clear(ugfx.html_color(color))
|
||||
buttons.init()
|
||||
#buttons.enable_interrupt()
|
||||
|
||||
if enable_menu_button:
|
||||
pass
|
||||
#buttons.enable_interrupt("BTN_MENU", lambda t: set_state("menu"), on_release = True)
|
||||
def menu():
|
||||
ugfx.clear()
|
||||
|
||||
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 name():
|
||||
return database.get("homescreen.name", "bar")
|
||||
|
||||
def clean_up():
|
||||
pass
|
||||
def mobile_strength():
|
||||
return 0.75
|
||||
|
||||
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()
|
||||
return 0.65
|
||||
|
||||
# Charge in %, None if unavailable
|
||||
def battery():
|
||||
return sim800.batterycharge() # todo: fix me, we can get this from the sim800
|
||||
|
||||
return 0.65
|
||||
|
||||
|
|
41
lib/http.py
|
@ -4,12 +4,13 @@ Somewhat inspired by "request".
|
|||
|
||||
Current known issues:
|
||||
* HTTPS is not supported
|
||||
*
|
||||
"""
|
||||
|
||||
___license___ = "MIT"
|
||||
___dependencies___ = ["urlencode", "wifi"]
|
||||
___dependencies___ = ["urlencode"]
|
||||
|
||||
import usocket, ujson, os, time, gc, wifi, ussl
|
||||
import usocket, ujson, os, time, gc, wifi
|
||||
from urlencode import urlencode
|
||||
|
||||
"""Usage
|
||||
|
@ -75,11 +76,7 @@ class Response(object):
|
|||
self.socket = None
|
||||
|
||||
def json(self):
|
||||
try:
|
||||
return ujson.loads(self.text)
|
||||
except ValueError as e:
|
||||
print("Invalid JSON: %s" % self.text)
|
||||
raise(e)
|
||||
return ujson.loads(self.text)
|
||||
|
||||
# Writes content into a file. This function will write while receiving, which avoids
|
||||
# having to load all content into memory
|
||||
|
@ -147,6 +144,7 @@ def open_http_socket(method, url, json=None, timeout=None, headers=None, data=No
|
|||
if proto == 'http:':
|
||||
port = 80
|
||||
elif proto == 'https:':
|
||||
raise OSError("HTTPS is currently not supported")
|
||||
port = 443
|
||||
else:
|
||||
raise OSError('Unsupported protocol: %s' % proto[:-1])
|
||||
|
@ -169,17 +167,24 @@ def open_http_socket(method, url, json=None, timeout=None, headers=None, data=No
|
|||
content = None
|
||||
|
||||
# ToDo: Handle IPv6 addresses
|
||||
addr = get_address_info(host, port)
|
||||
if is_ipv4_address(host):
|
||||
addr = (host, port)
|
||||
else:
|
||||
ai = usocket.getaddrinfo(host, port)
|
||||
addr = ai[0][4]
|
||||
|
||||
sock = usocket.socket(usocket.AF_INET, usocket.SOCK_STREAM)
|
||||
sock = None
|
||||
if proto == 'https:':
|
||||
sock = usocket.socket(usocket.AF_INET, usocket.SOCK_STREAM, usocket.SEC_SOCKET)
|
||||
else:
|
||||
sock = usocket.socket()
|
||||
|
||||
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.settimeout(0) # Actually make timeouts working properly with ssl
|
||||
|
||||
sock.send('%s /%s HTTP/1.0\r\nHost: %s\r\n' % (method, urlpath, host))
|
||||
|
||||
|
@ -197,18 +202,6 @@ def open_http_socket(method, url, json=None, timeout=None, headers=None, data=No
|
|||
|
||||
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)
|
||||
|
@ -265,3 +258,5 @@ def is_ipv4_address(address):
|
|||
return len(valid_octets) == 4
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
|
|
86
lib/ntp.py
|
@ -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
|
|
@ -10,17 +10,13 @@ import os
|
|||
|
||||
sep = "/"
|
||||
|
||||
R_OK = 4
|
||||
W_OK = 2
|
||||
X_OK = 1
|
||||
F_OK = 0
|
||||
R_OK = const(4)
|
||||
W_OK = const(2)
|
||||
X_OK = const(1)
|
||||
F_OK = const(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:
|
||||
|
@ -65,25 +61,3 @@ def isfile(path):
|
|||
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)
|
||||
|
||||
|
|
897
lib/sim800.py
|
@ -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)
|
21
lib/sleep.py
|
@ -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)
|
|
@ -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))
|
||||
|
|
@ -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
|
|
@ -3,59 +3,34 @@
|
|||
___license___ = "MIT"
|
||||
___dependencies___ = ["upip:unittest", "badge_store", "shared/test/file.txt"]
|
||||
|
||||
import unittest, os
|
||||
import unittest
|
||||
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"
|
||||
self.store = BadgeStore(url="http://badge.marekventur.com", repo="emfcamp/Mk4-Apps", ref="ee144e8")
|
||||
|
||||
def tearDownClass(self):
|
||||
self._remove_download_file()
|
||||
|
||||
def test_get_all_apps(self):
|
||||
response = self.store.get_all_apps()
|
||||
def test_apps(self):
|
||||
response = self.store.get_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):
|
||||
def test_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_is_file_up_to_date(self):
|
||||
self.assertFalse(self.store._is_file_up_to_date("shared/test/file.txt", "1234567890"))
|
||||
self.assertFalse(self.store._is_file_up_to_date("does/not/exist.txt", "1234567890"))
|
||||
self.assertTrue(self.store._is_file_up_to_date("shared/test/file.txt", "182d04f8ee"))
|
||||
|
||||
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()
|
||||
def test_install(self):
|
||||
installers = self.store.install(["launcher", "badge_store"])
|
||||
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()
|
||||
|
|
|
@ -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()
|
|
@ -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()
|
|
@ -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()
|
|
@ -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()
|
|
@ -13,9 +13,9 @@ class TestHttp(unittest.TestCase):
|
|||
wifi.connect()
|
||||
|
||||
def test_get_with_https(self):
|
||||
with get("https://httpbin.org/get") as response:
|
||||
self.assertEqual(response.status, 200)
|
||||
print(response.text)
|
||||
with self.assertRaises(OSError) as context:
|
||||
get("https://httpbin.org/get")
|
||||
self.assertIn("HTTPS is currently not supported", str(context.exception))
|
||||
|
||||
def test_get(self):
|
||||
with get("http://httpbin.org/get", params={"foo": "bar"}, headers={"accept": "application/json"}) as response:
|
||||
|
|
|
@ -9,29 +9,29 @@ from icons import *
|
|||
class TestIcons(unittest.TestCase):
|
||||
|
||||
# incomplete!
|
||||
# todo: fix me
|
||||
|
||||
def setUp(self):
|
||||
ugfx.init()
|
||||
ugfx.orientation(90)
|
||||
ugfx.clear()
|
||||
|
||||
# def test_icon(self):
|
||||
# icon = Icon(44, 40, "Badge Store with", "badge_store/icon.gif")
|
||||
# icon.show()
|
||||
#
|
||||
# for s in [True, False, True]:
|
||||
# icon.selected = s
|
||||
# time.sleep(0.1)
|
||||
#
|
||||
# icon.__del__()
|
||||
#
|
||||
# def test_icon_grid(self):
|
||||
# items = []
|
||||
# for i in range(50):
|
||||
# items.append({
|
||||
# "title": "App %s" % i
|
||||
# })
|
||||
# icon_grid = IconGrid(5, 5, items, None)
|
||||
def test_icon(self):
|
||||
icon = Icon(44, 40, "Badge Store with", "badge_store/icon.gif")
|
||||
icon.show()
|
||||
|
||||
for s in [True, False, True]:
|
||||
icon.selected = s
|
||||
time.sleep(0.1)
|
||||
|
||||
icon.__del__()
|
||||
|
||||
def test_icon_grid(self):
|
||||
items = []
|
||||
for i in range(50):
|
||||
items.append({
|
||||
"title": "App %s" % i
|
||||
})
|
||||
icon_grid = IconGrid(5, 5, items, None)
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -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()
|
|
@ -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()
|
|
@ -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()
|
|
@ -1,34 +0,0 @@
|
|||
"""Tests for http"""
|
||||
|
||||
___license___ = "MIT"
|
||||
___dependencies___ = ["upip:unittest", "speaker", "sleep"]
|
||||
|
||||
import unittest, speaker, ugfx_helper
|
||||
from sleep import *
|
||||
|
||||
class TestSpeaker(unittest.TestCase):
|
||||
|
||||
def tearDown(self):
|
||||
speaker.stop()
|
||||
|
||||
def test_enabled(self):
|
||||
self.assertEqual(speaker.enabled(), True)
|
||||
speaker.enabled(False)
|
||||
self.assertEqual(speaker.enabled(), False)
|
||||
speaker.enabled(True)
|
||||
self.assertEqual(speaker.enabled(), True)
|
||||
|
||||
def test_beep_and_stop(self):
|
||||
for f in range(50, 1000):
|
||||
speaker.frequency(f)
|
||||
self.assertEqual(speaker.frequency(), f)
|
||||
sleep_ms(1)
|
||||
speaker.stop()
|
||||
|
||||
def test_note(self):
|
||||
for n in ["c", "d", "e", "f", "g", "a", "b", "c5"]:
|
||||
speaker.note(n)
|
||||
sleep_ms(100)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
|
@ -1,23 +0,0 @@
|
|||
"""Tests for http"""
|
||||
|
||||
___license___ = "MIT"
|
||||
___dependencies___ = ["upip:unittest", "websockets", "wifi"]
|
||||
|
||||
import unittest
|
||||
from websockets import connect
|
||||
import wifi
|
||||
|
||||
class TestWebsockets(unittest.TestCase):
|
||||
|
||||
def setUpClass(self):
|
||||
wifi.connect()
|
||||
|
||||
def test_echo_service(self):
|
||||
ws = connect("ws://echo.websocket.org")
|
||||
self.assertTrue(ws.open)
|
||||
ws.send("Hello from TiLDA")
|
||||
response = ws.recv()
|
||||
self.assertEqual(response, "Hello from TiLDA")
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
|
@ -1,22 +0,0 @@
|
|||
"""Tests for wifi"""
|
||||
|
||||
___license___ = "MIT"
|
||||
___dependencies___ = ["upip:unittest", "wifi", "ugfx_helper"]
|
||||
|
||||
import unittest, wifi, ugfx_helper
|
||||
from machine import Pin
|
||||
|
||||
class TestWifi(unittest.TestCase):
|
||||
|
||||
def setUpClass(self):
|
||||
ugfx_helper.init()
|
||||
|
||||
def tearDownClass(self):
|
||||
ugfx_helper.deinit()
|
||||
|
||||
def test_connect(self):
|
||||
wifi.connect(show_wait_message=True)
|
||||
self.assertTrue(wifi.is_connected())
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
|
@ -1,13 +0,0 @@
|
|||
"""Helper library for ugfx stuff"""
|
||||
|
||||
___license___ = "MIT"
|
||||
|
||||
import ugfx
|
||||
from machine import Pin
|
||||
|
||||
def init():
|
||||
ugfx.init()
|
||||
Pin(Pin.PWM_LCD_BLIGHT).on()
|
||||
|
||||
def deinit():
|
||||
Pin(Pin.PWM_LCD_BLIGHT).off()
|
|
@ -1,324 +0,0 @@
|
|||
"""
|
||||
Websockets client for micropython
|
||||
|
||||
Based off https://github.com/danni/uwebsockets
|
||||
"""
|
||||
|
||||
|
||||
import ure as re
|
||||
import ubinascii as binascii
|
||||
import ustruct as struct
|
||||
import urandom as random
|
||||
import usocket as socket
|
||||
from ucollections import namedtuple
|
||||
|
||||
# Opcodes
|
||||
OP_CONT = const(0x0)
|
||||
OP_TEXT = const(0x1)
|
||||
OP_BYTES = const(0x2)
|
||||
OP_CLOSE = const(0x8)
|
||||
OP_PING = const(0x9)
|
||||
OP_PONG = const(0xa)
|
||||
|
||||
# Close codes
|
||||
CLOSE_OK = const(1000)
|
||||
CLOSE_GOING_AWAY = const(1001)
|
||||
CLOSE_PROTOCOL_ERROR = const(1002)
|
||||
CLOSE_DATA_NOT_SUPPORTED = const(1003)
|
||||
CLOSE_BAD_DATA = const(1007)
|
||||
CLOSE_POLICY_VIOLATION = const(1008)
|
||||
CLOSE_TOO_BIG = const(1009)
|
||||
CLOSE_MISSING_EXTN = const(1010)
|
||||
CLOSE_BAD_CONDITION = const(1011)
|
||||
|
||||
URL_RE = re.compile(r'ws://([A-Za-z0-9\-\.]+)(?:\:([0-9]+))?(/.+)?')
|
||||
URI = namedtuple('URI', ('hostname', 'port', 'path'))
|
||||
|
||||
DEBUG = False
|
||||
|
||||
# Call this to spew the headers etc
|
||||
def enable_debug():
|
||||
global DEBUG
|
||||
DEBUG = True
|
||||
|
||||
def debug_log(*args, **kwargs):
|
||||
if DEBUG:
|
||||
print(args)
|
||||
|
||||
def urlparse(uri):
|
||||
"""Parse ws:// URLs"""
|
||||
match = URL_RE.match(uri)
|
||||
if match:
|
||||
hostname = match.group(1)
|
||||
port = int(match.group(2)) if match.group(2) else 80
|
||||
path = match.group(3)
|
||||
return URI(hostname, port, path)
|
||||
|
||||
|
||||
class Websocket:
|
||||
"""
|
||||
Basis of the Websocket protocol.
|
||||
|
||||
This can probably be replaced with the C-based websocket module, but
|
||||
this one currently supports more options.
|
||||
"""
|
||||
is_client = False
|
||||
|
||||
def __init__(self, sock):
|
||||
self._sock = sock
|
||||
self.open = True
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc, tb):
|
||||
debug_log("exited context manager")
|
||||
self.close()
|
||||
|
||||
def settimeout(self, timeout):
|
||||
self._sock.settimeout(timeout)
|
||||
|
||||
def read_frame(self, max_size=None):
|
||||
"""
|
||||
Read a frame from the socket.
|
||||
See https://tools.ietf.org/html/rfc6455#section-5.2 for the details.
|
||||
"""
|
||||
|
||||
# Frame header
|
||||
byte1, byte2 = struct.unpack('!BB', self._sock.recv(2))
|
||||
|
||||
debug_log("read a frame header: {} {}".format(byte1, byte2))
|
||||
|
||||
# Byte 1: FIN(1) _(1) _(1) _(1) OPCODE(4)
|
||||
fin = bool(byte1 & 0x80)
|
||||
opcode = byte1 & 0x0f
|
||||
|
||||
|
||||
# Byte 2: MASK(1) LENGTH(7)
|
||||
mask = bool(byte2 & (1 << 7))
|
||||
length = byte2 & 0x7f
|
||||
|
||||
if length == 126: # Magic number, length header is 2 bytes
|
||||
debug_log("length is 126")
|
||||
length, = struct.unpack('!H', self._sock.recv(2))
|
||||
elif length == 127: # Magic number, length header is 8 bytes
|
||||
debug_log("length is 127")
|
||||
length, = struct.unpack('!Q', self._sock.recv(8))
|
||||
else:
|
||||
debug_log("length is something else: {}".format(length))
|
||||
|
||||
if mask: # Mask is 4 bytes
|
||||
mask_bits = self._sock.recv(4)
|
||||
|
||||
if length:
|
||||
debug_log("Trying to receive {} bytes".format(length))
|
||||
try:
|
||||
data = self._sock.recv(length)
|
||||
except MemoryError:
|
||||
# We can't receive this many bytes, close the socket
|
||||
debug_log("Frame of length %s too big. Closing",
|
||||
length)
|
||||
self.close(code=CLOSE_TOO_BIG)
|
||||
return True, OP_CLOSE, None
|
||||
else:
|
||||
data = b''
|
||||
|
||||
if mask:
|
||||
data = bytes(b ^ mask_bits[i % 4]
|
||||
for i, b in enumerate(data))
|
||||
|
||||
return fin, opcode, data
|
||||
|
||||
def write_frame(self, opcode, data=b''):
|
||||
"""
|
||||
Write a frame to the socket.
|
||||
See https://tools.ietf.org/html/rfc6455#section-5.2 for the details.
|
||||
"""
|
||||
fin = True
|
||||
mask = self.is_client # messages sent by client are masked
|
||||
|
||||
length = len(data)
|
||||
|
||||
# Frame header
|
||||
# Byte 1: FIN(1) _(1) _(1) _(1) OPCODE(4)
|
||||
byte1 = 0x80 if fin else 0
|
||||
byte1 |= opcode
|
||||
|
||||
# Byte 2: MASK(1) LENGTH(7)
|
||||
byte2 = 0x80 if mask else 0
|
||||
|
||||
if length < 126: # 126 is magic value to use 2-byte length header
|
||||
byte2 |= length
|
||||
self._sock.send(struct.pack('!BB', byte1, byte2))
|
||||
|
||||
elif length < (1 << 16): # Length fits in 2-bytes
|
||||
byte2 |= 126 # Magic code
|
||||
self._sock.send(struct.pack('!BBH', byte1, byte2, length))
|
||||
|
||||
elif length < (1 << 64):
|
||||
byte2 |= 127 # Magic code
|
||||
self._sock.send(struct.pack('!BBQ', byte1, byte2, length))
|
||||
|
||||
else:
|
||||
raise ValueError()
|
||||
|
||||
if mask: # Mask is 4 bytes
|
||||
mask_bits = struct.pack('!I', random.getrandbits(32))
|
||||
self._sock.send(mask_bits)
|
||||
|
||||
data = bytes(b ^ mask_bits[i % 4]
|
||||
for i, b in enumerate(data))
|
||||
|
||||
self._sock.send(data)
|
||||
|
||||
def recv(self):
|
||||
"""
|
||||
Receive data from the websocket.
|
||||
|
||||
This is slightly different from 'websockets' in that it doesn't
|
||||
fire off a routine to process frames and put the data in a queue.
|
||||
If you don't call recv() sufficiently often you won't process control
|
||||
frames.
|
||||
"""
|
||||
assert self.open
|
||||
|
||||
while self.open:
|
||||
try:
|
||||
fin, opcode, data = self.read_frame()
|
||||
except ValueError:
|
||||
debug_log("Failed to read frame. Socket dead.")
|
||||
self._close()
|
||||
return
|
||||
|
||||
debug_log("Got a frame with opcode {}".format(opcode))
|
||||
if not fin:
|
||||
raise NotImplementedError()
|
||||
|
||||
if opcode == OP_TEXT:
|
||||
return data.decode('utf-8')
|
||||
elif opcode == OP_BYTES:
|
||||
return data
|
||||
elif opcode == OP_CLOSE:
|
||||
debug_log("got a close frame")
|
||||
self._close()
|
||||
return
|
||||
elif opcode == OP_PONG:
|
||||
# Ignore this frame, keep waiting for a data frame
|
||||
continue
|
||||
elif opcode == OP_PING:
|
||||
# We need to send a pong frame
|
||||
debug_log("Sending PONG")
|
||||
self.write_frame(OP_PONG, data)
|
||||
# And then wait to receive
|
||||
continue
|
||||
elif opcode == OP_CONT:
|
||||
# This is a continuation of a previous frame
|
||||
raise NotImplementedError(opcode)
|
||||
else:
|
||||
raise ValueError(opcode)
|
||||
|
||||
def send(self, buf):
|
||||
"""Send data to the websocket."""
|
||||
|
||||
assert self.open
|
||||
|
||||
if isinstance(buf, str):
|
||||
opcode = OP_TEXT
|
||||
buf = buf.encode('utf-8')
|
||||
elif isinstance(buf, bytes):
|
||||
opcode = OP_BYTES
|
||||
else:
|
||||
raise TypeError()
|
||||
|
||||
self.write_frame(opcode, buf)
|
||||
|
||||
def close(self, code=CLOSE_OK, reason=''):
|
||||
"""Close the websocket."""
|
||||
if not self.open:
|
||||
return
|
||||
|
||||
buf = struct.pack('!H', code) + reason.encode('utf-8')
|
||||
|
||||
self.write_frame(OP_CLOSE, buf)
|
||||
self._close()
|
||||
|
||||
def _close(self):
|
||||
debug_log("Connection closed")
|
||||
self.open = False
|
||||
self._sock.close()
|
||||
|
||||
def readline(sock):
|
||||
# Micropython on the Mk3 doesn't have socket.readline
|
||||
try:
|
||||
line = sock.readline()[:-2]
|
||||
debug_log("readline:")
|
||||
debug_log(line)
|
||||
return line
|
||||
except AttributeError:
|
||||
line = b''
|
||||
while True:
|
||||
chunk = sock.recv(2)
|
||||
if chunk == b'\r\n':
|
||||
debug_log("Read line:")
|
||||
debug_log(line)
|
||||
return line
|
||||
else:
|
||||
line += chunk
|
||||
|
||||
|
||||
class WebsocketClient(Websocket):
|
||||
is_client = True
|
||||
|
||||
def connect(uri):
|
||||
"""
|
||||
Connect a websocket.
|
||||
"""
|
||||
|
||||
uri = urlparse(uri)
|
||||
assert uri
|
||||
|
||||
debug_log("open connection %s:%s",
|
||||
uri.hostname, uri.port)
|
||||
|
||||
sock = socket.socket()
|
||||
debug_log("got a socket")
|
||||
try:
|
||||
addr = socket.getaddrinfo(uri.hostname, uri.port)
|
||||
debug_log("getaddrinfo result")
|
||||
debug_log(addr)
|
||||
sock.connect(addr[0][4])
|
||||
debug_log("connected using getaddrinfo method")
|
||||
except OSError:
|
||||
debug_log("getaddrinfo method failed")
|
||||
sock.connect((uri.hostname, uri.port))
|
||||
debug_log("connected using direct method")
|
||||
|
||||
def send_header(header, *args):
|
||||
debug_log(str(header), *args)
|
||||
sock.send(header % args + '\r\n')
|
||||
|
||||
# Sec-WebSocket-Key is 16 bytes of random base64 encoded
|
||||
key = binascii.b2a_base64(bytes(random.getrandbits(8)
|
||||
for _ in range(16)))[:-1]
|
||||
debug_log("set key to:")
|
||||
debug_log(key)
|
||||
|
||||
send_header(b'GET %s HTTP/1.1', uri.path or '/')
|
||||
send_header(b'Host: %s:%s', uri.hostname, uri.port)
|
||||
send_header(b'Connection: Upgrade')
|
||||
send_header(b'Upgrade: websocket')
|
||||
send_header(b'Sec-WebSocket-Key: %s', key)
|
||||
send_header(b'Sec-WebSocket-Version: 13')
|
||||
send_header(b'Origin: http://localhost')
|
||||
send_header(b'')
|
||||
|
||||
header = readline(sock)
|
||||
assert header[:13] == b'HTTP/1.1 101 ', header
|
||||
|
||||
# We don't (currently) need these headers
|
||||
# FIXME: should we check the return key?
|
||||
while header:
|
||||
debug_log(str(header))
|
||||
header = readline(sock)
|
||||
|
||||
return WebsocketClient(sock)
|
98
lib/wifi.py
|
@ -1,17 +1,20 @@
|
|||
"""Handles connecting to a wifi access point based on a valid wifi.json file"""
|
||||
|
||||
___license___ = "MIT"
|
||||
___dependencies___ = ["dialogs", "sleep"]
|
||||
___dependencies___ = ["dialogs"]
|
||||
|
||||
import network, os, json, dialogs, sleep, time
|
||||
import network
|
||||
import os
|
||||
import json
|
||||
import pyb
|
||||
import dialogs
|
||||
|
||||
_nic = None
|
||||
|
||||
def nic():
|
||||
global _nic
|
||||
if not _nic:
|
||||
_nic = network.WLAN()
|
||||
_nic.active(True)
|
||||
_nic = network.CC3100()
|
||||
return _nic
|
||||
|
||||
def connection_details():
|
||||
|
@ -31,8 +34,10 @@ def ssid():
|
|||
return connection_details()["ssid"]
|
||||
|
||||
def connect(wait=True, timeout=10, show_wait_message=False, prompt_on_fail=True, dialog_title='TiLDA'):
|
||||
while True:
|
||||
if nic().isconnected():
|
||||
retry_connect = True
|
||||
|
||||
while retry_connect:
|
||||
if nic().is_connected():
|
||||
return
|
||||
|
||||
details = connection_details()
|
||||
|
@ -58,78 +63,62 @@ def connect(wait=True, timeout=10, show_wait_message=False, prompt_on_fail=True,
|
|||
text="Failed to connect to '%s'" % details['ssid'],
|
||||
title=dialog_title,
|
||||
true_text="Try again",
|
||||
false_text="Change it",
|
||||
false_text="Forget it",
|
||||
)
|
||||
if not retry_connect:
|
||||
os.remove('wifi.json')
|
||||
os.sync()
|
||||
|
||||
# We would rather let you choose a new network here, but
|
||||
# scanning doesn't work after a connect at the moment
|
||||
pyb.hard_reset()
|
||||
else:
|
||||
raise
|
||||
|
||||
def connect_wifi(details, timeout, wait=False):
|
||||
if 'user' in details:
|
||||
nic().connect(details['ssid'], details['pw'], enterprise=True, entuser=details['user'], entmethod=nic().EAP_METHOD_PEAP0_MSCHAPv2, entserverauth=False)
|
||||
elif 'pw' in details:
|
||||
nic().connect(details['ssid'], details['pw'])
|
||||
if 'pw' in details:
|
||||
nic().connect(details['ssid'], details['pw'], timeout=timeout)
|
||||
else:
|
||||
nic().connect(details['ssid'])
|
||||
nic().connect(details['ssid'], timeout=timeout)
|
||||
|
||||
if wait:
|
||||
wait_until = time.ticks_ms() + timeout * 1000
|
||||
while not nic().isconnected():
|
||||
#nic().update() # todo: do we need this?
|
||||
if (time.ticks_ms() > wait_until):
|
||||
raise OSError("Timeout while trying to connect to wifi")
|
||||
sleep.sleep_ms(100)
|
||||
|
||||
while not nic().is_connected():
|
||||
nic().update()
|
||||
pyb.delay(100)
|
||||
|
||||
def is_connected():
|
||||
return nic().isconnected()
|
||||
|
||||
# returns wifi strength in %, None if unavailable
|
||||
def get_strength():
|
||||
n = nic()
|
||||
if n.isconnected():
|
||||
v = n.status("rssi");
|
||||
if v:
|
||||
# linear range: -60 =100%; -100= 20%
|
||||
# todo: it's probably not linear, improve me.
|
||||
return v * 2 + 220
|
||||
return None
|
||||
return nic().is_connected()
|
||||
|
||||
def get_security_level(ap):
|
||||
#todo: fix this
|
||||
n = nic()
|
||||
levels = {
|
||||
n.SCAN_SEC_OPEN: 0, # I am awful
|
||||
n.SCAN_SEC_WEP: 'WEP',
|
||||
n.SCAN_SEC_WPA: 'WPA',
|
||||
n.SCAN_SEC_WPA2: 'WPA2',
|
||||
}
|
||||
levels = {}
|
||||
try:
|
||||
levels = {
|
||||
n.SCAN_SEC_OPEN: 0, # I am awful
|
||||
n.SCAN_SEC_WEP: 'WEP',
|
||||
n.SCAN_SEC_WPA: 'WPA',
|
||||
n.SCAN_SEC_WPA2: 'WPA2',
|
||||
}
|
||||
except AttributeError:
|
||||
print("Firmware too old to query wifi security level, please upgrade.")
|
||||
return None
|
||||
|
||||
return levels.get(ap.get('security', None), None)
|
||||
|
||||
def choose_wifi(dialog_title='TiLDA'):
|
||||
filtered_aps = []
|
||||
with dialogs.WaitingMessage(text='Scanning for networks...', title=dialog_title):
|
||||
visible_aps = None
|
||||
while not visible_aps:
|
||||
visible_aps = nic().scan()
|
||||
print(visible_aps)
|
||||
sleep.sleep_ms(300)
|
||||
#todo: timeout
|
||||
visible_aps.sort(key=lambda x:x[3], reverse=True)
|
||||
visible_aps = nic().list_aps()
|
||||
visible_aps.sort(key=lambda x:x['rssi'], reverse=True)
|
||||
# We'll get one result for each AP, so filter dupes
|
||||
for ap in visible_aps:
|
||||
title = ap[0]
|
||||
security = "?" # todo: re-add get_security_level(ap)
|
||||
title = ap['ssid']
|
||||
security = get_security_level(ap)
|
||||
if security:
|
||||
title = title + ' (%s)' % security
|
||||
ap = {
|
||||
'title': title,
|
||||
'ssid': ap[0],
|
||||
'security': ap[4],
|
||||
'ssid': ap['ssid'],
|
||||
'security': security,
|
||||
}
|
||||
if ap['ssid'] not in [ a['ssid'] for a in filtered_aps ]:
|
||||
filtered_aps.append(ap)
|
||||
|
@ -147,7 +136,11 @@ def choose_wifi(dialog_title='TiLDA'):
|
|||
if ap['security'] == None:
|
||||
ap['security'] = 'wifi'
|
||||
|
||||
key = dialogs.prompt_text("Enter %s key" % ap['security'])
|
||||
key = dialogs.prompt_text(
|
||||
"Enter %s key" % ap['security'],
|
||||
width = 310,
|
||||
height = 220
|
||||
)
|
||||
with open("wifi.json", "wt") as file:
|
||||
if key:
|
||||
conn_details = {"ssid": ap['ssid'], "pw": key}
|
||||
|
@ -156,4 +149,5 @@ def choose_wifi(dialog_title='TiLDA'):
|
|||
|
||||
file.write(json.dumps(conn_details))
|
||||
os.sync()
|
||||
# todo: last time we had to hard reset here, is that still the case?
|
||||
# We can't connect after scanning for some bizarre reason, so we reset instead
|
||||
pyb.hard_reset()
|
||||
|
|