├── examples ├── snake.py ├── imu.py ├── read_light.py ├── dactest.py ├── name.py ├── buzzer.py ├── http.py ├── external_test │ ├── external.py │ └── main.py ├── read_uid.py ├── party_mode.py ├── uidemo.py ├── ledtest.py ├── network.py ├── fonts.py ├── adc.py ├── party_mode_vsync.py ├── set_rtc_from_ntp.py ├── clock.py ├── clock_vsync.py └── buttons.py ├── main.py ├── apps ├── sponsors │ ├── sponsors.gif │ └── main.py ├── changename │ └── main.py ├── home │ ├── main.py │ ├── draw_name.py │ ├── quick_launch.py │ ├── file_loader.py │ └── home.py ├── logger │ ├── external.py │ └── main.py ├── changetz │ └── main.py ├── snake │ └── main.py ├── ball_demo │ └── main.py └── app_library │ └── main.py ├── .gitignore ├── boot.py ├── LICENSE ├── lib ├── run_app.py ├── ntp.py ├── imu.py ├── onboard.py ├── filesystem.py ├── database.py ├── buttons.py ├── wifi.py ├── mqtt.py ├── app.py ├── dialogs.py └── http_client.py └── bootstrap.py /examples/snake.py: -------------------------------------------------------------------------------- 1 | ../apps/snake/main.py -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | # main.py -- put your code here! 2 | import ugfx 3 | ugfx.init() 4 | import apps.home.main -------------------------------------------------------------------------------- /apps/sponsors/sponsors.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emfcamp/Mk3-Firmware/HEAD/apps/sponsors/sponsors.gif -------------------------------------------------------------------------------- /examples/imu.py: -------------------------------------------------------------------------------- 1 | from imu import IMU 2 | import pyb 3 | 4 | imu = IMU() 5 | 6 | while True: 7 | print(imu.get_acceleration()) 8 | pyb.delay(1000); 9 | -------------------------------------------------------------------------------- /examples/read_light.py: -------------------------------------------------------------------------------- 1 | import pyb 2 | 3 | #light sensor is on PA3 (note change this) 4 | a = pyb.ADC('PA3') 5 | 6 | while True: 7 | print(str(a.read())) 8 | pyb.delay(1000) 9 | -------------------------------------------------------------------------------- /examples/dactest.py: -------------------------------------------------------------------------------- 1 | import pyb 2 | dac = pyb.DAC(2) 3 | 4 | countdown = 10 5 | 6 | while (countdown): 7 | dac.write(countdown*20) 8 | 9 | pyb.delay(300) 10 | 11 | countdown -= 10 12 | -------------------------------------------------------------------------------- /examples/name.py: -------------------------------------------------------------------------------- 1 | import ugfx 2 | 3 | def display_name(): 4 | ugfx.area(0,0,ugfx.width(),ugfx.height(),0xFFFF) 5 | ugfx.set_default_font("D*") 6 | ugfx.text(40,120,"MATT",ugfx.YELLOW) 7 | ugfx.circle(160,200,40,ugfx.GREEN) 8 | 9 | ugfx.init() 10 | display_name() 11 | -------------------------------------------------------------------------------- /examples/buzzer.py: -------------------------------------------------------------------------------- 1 | import pyb 2 | 3 | t4 = pyb.Timer(4, freq=100, mode=pyb.Timer.CENTER) 4 | 5 | for x in range(1,90): 6 | 7 | t4.freq(x*100) 8 | ch1 = t4.channel(1, pyb.Timer.PWM, pin=pyb.Pin("BUZZ"), pulse_width=(t4.period() + 1) // 2) 9 | 10 | pyb.delay(100) 11 | 12 | pyb.Pin("BUZZ",pyb.Pin.OUT).low() 13 | -------------------------------------------------------------------------------- /examples/http.py: -------------------------------------------------------------------------------- 1 | import wifi 2 | from http_client import get 3 | 4 | wifi.connect() 5 | 6 | try: 7 | if wifi.nic().is_connected(): 8 | ip = get('http://httpbin.org/ip').raise_for_status().json()["origin"] 9 | print("My public IP is %s" %(ip)) 10 | except OSError as e: 11 | print("Query failed " + str(e)) 12 | -------------------------------------------------------------------------------- /examples/external_test/external.py: -------------------------------------------------------------------------------- 1 | import ugfx 2 | 3 | period = 1 * 1000 4 | needs_icon = True 5 | 6 | i = 0 7 | def tick(icon): 8 | global i 9 | i += 1 10 | icon.show() 11 | ugfx.set_default_font("c*") 12 | icon.area(0, 0, icon.width(), icon.height(), 0xFFFF) 13 | icon.text(0, 0, str(i), 0) 14 | 15 | return "Test: %d"% i 16 | -------------------------------------------------------------------------------- /examples/read_uid.py: -------------------------------------------------------------------------------- 1 | import stm 2 | 3 | r1 = stm.mem32[0x1FFF7590] 4 | r2 = stm.mem32[0x1FFF7594] 5 | r3 = stm.mem32[0x1FFF7598] 6 | 7 | print("X coord: " + str(r1 & 0xFFFF) + " Y coord: " + str(r1>>16)) 8 | print("Wafer: " + str(r2 & 0xFF)) 9 | print("Lot: " + chr(r3>>24) + chr((r3>>16)&0xFF) + chr((r3>>8)&0xFF) + chr(r3&0xFF) + chr(r2>>24) + chr((r2>>16)&0xFF) + chr((r2>>8)&0xFF)) 10 | -------------------------------------------------------------------------------- /examples/external_test/main.py: -------------------------------------------------------------------------------- 1 | ### Author: EMF Badge team 2 | ### Description: Test app for the hook into the home screen 3 | ### Category: Example 4 | ### License: MIT 5 | ### Appname : Home Callback Test 6 | 7 | import ugfx, buttons, pyb 8 | 9 | ugfx.init() 10 | buttons.init() 11 | ugfx.clear() 12 | 13 | ugfx.Label(5, 5, ugfx.width(), ugfx.height(), "Nothing to see here") 14 | 15 | while True: 16 | pyb.wfi() -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Object files 2 | *.o 3 | *.ko 4 | *.obj 5 | *.elf 6 | 7 | # Precompiled Headers 8 | *.gch 9 | *.pch 10 | 11 | # Libraries 12 | *.lib 13 | *.a 14 | *.la 15 | *.lo 16 | 17 | # Shared objects (inc. Windows DLLs) 18 | *.dll 19 | *.so 20 | *.so.* 21 | *.dylib 22 | 23 | # Executables 24 | *.exe 25 | *.out 26 | *.app 27 | *.i*86 28 | *.x86_64 29 | *.hex 30 | 31 | # Debug files 32 | *.dSYM/ 33 | 34 | .DS_Store 35 | 36 | config.json 37 | wifi.json 38 | -------------------------------------------------------------------------------- /examples/party_mode.py: -------------------------------------------------------------------------------- 1 | import ugfx 2 | import pyb 3 | 4 | 5 | keepgoing = 1 6 | tgl_menu = pyb.Pin("BTN_MENU", pyb.Pin.IN) 7 | tgl_menu.init(pyb.Pin.IN, pyb.Pin.PULL_UP) 8 | while(keepgoing): 9 | ugfx.area(0,0,320,240,ugfx.RED) 10 | pyb.delay(60) 11 | ugfx.area(0,0,320,240,ugfx.GREEN) 12 | pyb.delay(60) 13 | ugfx.area(0,0,320,240,ugfx.YELLOW) 14 | pyb.delay(60) 15 | ugfx.area(0,0,320,240,ugfx.WHITE) 16 | pyb.delay(60) 17 | ugfx.area(0,0,320,240,ugfx.BLUE) 18 | pyb.delay(60) 19 | if tgl_menu.value() == 0: 20 | keepgoing = 0 -------------------------------------------------------------------------------- /apps/changename/main.py: -------------------------------------------------------------------------------- 1 | ### Author: EMF Badge team 2 | ### Description: Change your name 3 | ### Category: Settings 4 | ### License: MIT 5 | ### Appname : Change name 6 | 7 | import dialogs 8 | from database import Database 9 | import buttons 10 | import ugfx 11 | 12 | ugfx.init() 13 | buttons.init() 14 | 15 | with Database() as db: 16 | name = db.get("display-name", "") 17 | name_new = dialogs.prompt_text("Enter your name", init_text=name, width = 310, height = 220) 18 | if name_new: 19 | db.set("display-name", name_new) 20 | -------------------------------------------------------------------------------- /examples/uidemo.py: -------------------------------------------------------------------------------- 1 | import ugfx 2 | import os 3 | 4 | #options.destroy() 5 | #btn_ok.destroy() 6 | #btn_menu.destroy() 7 | 8 | ugfx.init() 9 | 10 | ugfx.set_default_font("D*") 11 | 12 | ugfx.text(40, 0, "EMF BADGE 2016", ugfx.PURPLE) 13 | 14 | ugfx.set_default_font("C*") 15 | 16 | options = ugfx.List(0,0,160,150) 17 | btn_ok = ugfx.Button(200,50,70,30,"A: Run") 18 | btn_menu = ugfx.Button(200,90,70,30,"M: Menu") 19 | 20 | files = os.listdir() 21 | 22 | for f in files: 23 | options.add_item(f) 24 | 25 | btn_menu.attach_input(ugfx.BTN_MENU) 26 | btn_ok.attach_input(ugfx.BTN_A) 27 | -------------------------------------------------------------------------------- /examples/ledtest.py: -------------------------------------------------------------------------------- 1 | import pyb 2 | ledr = pyb.Pin("LED_RED",pyb.Pin.OUT) 3 | ledg = pyb.Pin("LED_GREEN",pyb.Pin.OUT) 4 | ledt = pyb.Pin("LED_TORCH",pyb.Pin.OUT) 5 | ledb = pyb.Pin("LED_BACKLIGHT",pyb.Pin.OUT) 6 | 7 | timer = pyb.Timer(17, freq=1000) 8 | ch1 = timer.channel(1, pyb.Timer.PWM, pin=ledb, pulse_width=0) 9 | 10 | ledt.high() 11 | 12 | countdown = 10; 13 | while (countdown): 14 | 15 | ch1.pulse_width_percent(100-(countdown*10)) 16 | ledr.high() 17 | ledg.low() 18 | pyb.delay(200) 19 | 20 | ledr.low() 21 | ledg.high() 22 | pyb.delay(200) 23 | 24 | countdown-=1; 25 | 26 | 27 | -------------------------------------------------------------------------------- /examples/network.py: -------------------------------------------------------------------------------- 1 | import network 2 | import gc 3 | import socket 4 | import pyb 5 | 6 | nic = network.CC3100() 7 | nic.connect("ssid","password") 8 | 9 | i=0 10 | while (i <= 20): 11 | i = i + 1 12 | print("Loop:") 13 | print(i) 14 | s=socket.socket() 15 | s.connect(socket.getaddrinfo('baconipsum.com',80)[0][4]) 16 | s.send("GET /api/?type=meat-and-filler¶s=100 HTTP/1.1\r\nHost:baconipsum.com\r\nConnection: close\r\n\r\n") 17 | 18 | buf = s.recv(1024) 19 | while len(buf) > 0: 20 | print(buf) 21 | buf = s.recv(1024) 22 | pyb.delay(5) 23 | gc.collect() 24 | s.close() 25 | -------------------------------------------------------------------------------- /apps/home/main.py: -------------------------------------------------------------------------------- 1 | ### Author: EMF Badge team 2 | ### Description: Main app 3 | ### Category: Other 4 | ### License: MIT 5 | ### Appname: Home 6 | ### Built-in: hide 7 | 8 | import os 9 | import json 10 | 11 | if 'main.json' in os.listdir(): 12 | m = None 13 | try: 14 | with open('main.json', 'r') as f: 15 | main_dict = json.loads(f.read()) 16 | m = main_dict['main'] 17 | if not main_dict.get('perm', False): 18 | os.remove('main.json') 19 | except Exception as e: 20 | print(e) 21 | 22 | if m: 23 | import run_app 24 | run_app.run_app(m) 25 | 26 | 27 | execfile("apps/home/home.py") 28 | -------------------------------------------------------------------------------- /examples/fonts.py: -------------------------------------------------------------------------------- 1 | import ugfx 2 | import pyb 3 | 4 | ugfx.clear() 5 | 6 | ugfx.set_default_font(ugfx.FONT_SMALL) 7 | ugfx.text(20,30,"Tiny AbCdEfGhiJkLmNoPqRsTuVwXyZ 1\"3$5^7*9) ",0) 8 | ugfx.set_default_font(ugfx.FONT_TITLE) 9 | ugfx.text(20,50,"Title AbCdEfGhiJkLmNoPqRsTuVwXyZ 1\"3$5^7*9) ",0) 10 | ugfx.set_default_font(ugfx.FONT_NAME) 11 | ugfx.text(20,80,"Name AbCdEfGhiJkLmNoPqRsTuVwXyZ 1\"3$5^7*9) ",0) 12 | ugfx.set_default_font(ugfx.FONT_MEDIUM) 13 | ugfx.text(20,120,"Medium AbCdEfGhiJkLmNoPqRsTuVwXyZ 1\"3$5^7*9) ",0) 14 | ugfx.set_default_font(ugfx.FONT_MEDIUM_BOLD) 15 | ugfx.text(20,150,"Medium-Bold AbCdEfGhiJkLmNoPqRsTuVwXyZ 1\"3$5^7*9) ",0) 16 | 17 | while True: 18 | pyb.wfi() -------------------------------------------------------------------------------- /boot.py: -------------------------------------------------------------------------------- 1 | # boot.py -- run on boot-up 2 | # can run arbitrary Python, but best to keep it minimal 3 | # this is a special version for EMF 4 | #pyb.usb_mode('CDC+MSC') # act as a serial and a storage device 5 | #pyb.usb_mode('CDC+HID') # act as a serial device and a mouse 6 | 7 | import pyb 8 | import os 9 | import micropython 10 | 11 | micropython.alloc_emergency_exception_buf(100) 12 | 13 | m = "bootstrap.py" 14 | if "main.py" in os.listdir(): 15 | m = "main.py" 16 | elif "apps" in os.listdir(): 17 | apps = os.listdir("apps") 18 | if ("home" in apps) and ("main.py" in os.listdir("apps/home")): 19 | m = "apps/home/main.py" 20 | elif ("app_library" in apps) and ("main.py" in os.listdir("apps/app_library")): 21 | m = "apps/app_library/main.py" 22 | pyb.main(m) 23 | -------------------------------------------------------------------------------- /examples/adc.py: -------------------------------------------------------------------------------- 1 | #This example shows how to use the ADC, and how to 2 | # measure the internal reference to get a more accurate 3 | # ADC reading 4 | import pyb 5 | 6 | # set adc resolution to 12 bits 7 | adca = pyb.ADCAll(12) 8 | 9 | # channel 17 is the internal 1.21V reference 10 | ref_reading = adca.read_channel(17) 11 | 12 | # channel 0 (PA0) is the Vbus/2 connection 13 | usb_reading = adca.read_channel(0) 14 | 15 | # Use the internal reference to calculate the supply voltage 16 | # The supply voltage is used as the ADC reference and is not exactly 3.3V 17 | supply_voltage = 4095/ref_reading*1.21 18 | 19 | print("supply voltage: " + str(supply_voltage) + "\n") 20 | 21 | # now calculate the USB voltage 22 | usb_voltage = usb_reading/4095*supply_voltage*2 23 | 24 | print("usb_voltage: " + str(usb_voltage) + "\n") 25 | -------------------------------------------------------------------------------- /examples/party_mode_vsync.py: -------------------------------------------------------------------------------- 1 | import ugfx 2 | import pyb 3 | 4 | 5 | keepgoing = 1 6 | tgl_menu = pyb.Pin("BTN_MENU", pyb.Pin.IN) 7 | tgl_menu.init(pyb.Pin.IN, pyb.Pin.PULL_UP) 8 | ugfx.enable_tear() 9 | tear = pyb.Pin("TEAR", pyb.Pin.IN) 10 | wi = ugfx.width() 11 | hi = ugfx.height() 12 | while(keepgoing): 13 | 14 | while(tear.value() == 0): 15 | 2+2 16 | while(tear.value()): 17 | 2+2 18 | ugfx.area(0,0,wi,hi,ugfx.RED) 19 | pyb.delay(60) 20 | 21 | while(tear.value() == 0): 22 | 2+2 23 | while(tear.value()): 24 | 2+2 25 | ugfx.area(0,0,wi,hi,ugfx.GREEN) 26 | pyb.delay(60) 27 | 28 | while(tear.value() == 0): 29 | 2+2 30 | while(tear.value()): 31 | 2+2 32 | ugfx.area(0,0,wi,hi,ugfx.YELLOW) 33 | pyb.delay(60) 34 | 35 | while(tear.value() == 0): 36 | 2+2 37 | while(tear.value()): 38 | 2+2 39 | ugfx.area(0,0,wi,hi,ugfx.WHITE) 40 | pyb.delay(60) 41 | 42 | while(tear.value() == 0): 43 | 2+2 44 | while(tear.value()): 45 | 2+2 46 | ugfx.area(0,0,wi,hi,ugfx.BLUE) 47 | pyb.delay(60) 48 | 49 | if tgl_menu.value() == 0: 50 | keepgoing = 0 51 | ugfx.disable_tear() -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Electromagnetic Field 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /apps/sponsors/main.py: -------------------------------------------------------------------------------- 1 | ### Author: EMF Badge team 2 | ### Description: Shows all the sponsors that have helped make this badge possible 3 | ### Category: Other 4 | ### License: MIT 5 | 6 | import ugfx, pyb, buttons 7 | 8 | ugfx.init() 9 | ugfx.clear() 10 | buttons.init() 11 | ugfx.set_default_font(ugfx.FONT_NAME) 12 | 13 | def screen_1(): 14 | ugfx.display_image(0, 0, "splash1.bmp") 15 | 16 | def screen_2(): 17 | ugfx.display_image(0, 0, "apps/sponsors/sponsors.gif") 18 | 19 | def screen_3(): 20 | ugfx.clear(ugfx.html_color(0x7c1143)) 21 | ugfx.text(27, 90, "Thank you!", ugfx.WHITE) 22 | 23 | SCREENS = [screen_1, screen_2, screen_3] 24 | SCREEN_DURATION = 2000 25 | 26 | screen_index = -1 27 | next_change = 0; 28 | while True: 29 | if pyb.millis() > next_change: 30 | screen_index = (screen_index + 1) % len(SCREENS) 31 | SCREENS[screen_index]() 32 | next_change = pyb.millis() + SCREEN_DURATION 33 | pyb.wfi() 34 | if buttons.is_triggered("BTN_MENU") or buttons.is_triggered("BTN_A") or buttons.is_triggered("BTN_B") or buttons.is_triggered("JOY_CENTER"): 35 | break; 36 | 37 | ugfx.clear() 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /apps/home/draw_name.py: -------------------------------------------------------------------------------- 1 | import ugfx 2 | from database import database_get 3 | 4 | obj = [] 5 | sty = None 6 | 7 | def draw(x,y,window): 8 | global obj 9 | global sty 10 | if len(obj) == 0: 11 | 12 | sty = ugfx.Style() 13 | sty.set_enabled([ugfx.RED, ugfx.BLACK, ugfx.GREY, ugfx.GREY]) 14 | 15 | 16 | #ugfx.Imagebox(0,0,window.width(),window.height(),"apps/home/back.bmp",0, win2) 17 | ugfx.set_default_font(ugfx.FONT_NAME) 18 | l=ugfx.Label(5,20,310,window.height()-20,database_get("display-name", ""),parent=window) 19 | obj.append(l) 20 | ugfx.set_default_font(ugfx.FONT_MEDIUM_BOLD) 21 | obj.append(ugfx.Label(5,0,310,20,"My name is...",parent=window,style=sty)) 22 | #ugfx.text(40,80,database_get("display-name", ""),ugfx.BLUE) 23 | #ugfx.circle(140,150,40,ugfx.GREEN) 24 | #ugfx.circle(160,150,40,ugfx.GREEN) 25 | #ugfx.circle(180,150,40,ugfx.GREEN) 26 | window.show() 27 | else: 28 | window.hide() 29 | window.show() 30 | 31 | 32 | def draw_destroy(obj_name): 33 | #there may be some .destroy() functions that could be wanted to be called 34 | global obj 35 | for o in obj: 36 | o.destroy() 37 | obj = [] 38 | -------------------------------------------------------------------------------- /examples/set_rtc_from_ntp.py: -------------------------------------------------------------------------------- 1 | # borrowed from https://github.com/micropython/micropython/blob/master/esp8266/scripts/ntptime.py 2 | import socket 3 | import pyb 4 | import network 5 | 6 | # (date(2000, 1, 1) - date(1900, 1, 1)).days * 24*60*60 7 | NTP_DELTA = 3155673600 8 | 9 | host = "pool.ntp.org" 10 | 11 | def getntptime(): 12 | NTP_QUERY = bytearray(48) 13 | NTP_QUERY[0] = 0x1b 14 | addr = socket.getaddrinfo(host, 123)[0][-1] 15 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 16 | s.sendto(NTP_QUERY, addr) 17 | msg = s.recv(48) 18 | s.close() 19 | import struct 20 | val = struct.unpack("!I", msg[40:44])[0] 21 | return val - NTP_DELTA 22 | 23 | def settime(): 24 | import time 25 | from pyb import RTC 26 | t = getntptime() 27 | tm = time.localtime(t) 28 | tm = tm[0:3] + (0,) + tm[3:6] + (0,) 29 | rtc = RTC() 30 | rtc.init() 31 | rtc.datetime(tm) 32 | 33 | nic = network.CC3100() 34 | nic.connect("","") 35 | while (not nic.is_connected()): 36 | nic.update() 37 | pyb.delay(100) 38 | 39 | 40 | # set the RTC using time from ntp 41 | settime() 42 | # print out RTC datetime 43 | pyb.RTC().datetime() 44 | 45 | -------------------------------------------------------------------------------- /lib/run_app.py: -------------------------------------------------------------------------------- 1 | 2 | def reset_and_run(path): 3 | import pyb 4 | # if stm.mem8[0x40002850] == 0: # this battery backed RAM section is set to 0 when the name screen runs 5 | with open('main.json', 'w') as f: 6 | f.write('{"main":"' + path + '"}') 7 | # stm.mem8[0x40002850] = 2 #set this address to != 0 so this if statement doesnt run next time 8 | pyb.hard_reset() 9 | 10 | def run_app(path): 11 | import buttons 12 | import ugfx 13 | import sys 14 | 15 | buttons.init() 16 | ugfx.init() 17 | ugfx.clear() 18 | 19 | if not buttons.has_interrupt("BTN_MENU"): 20 | buttons.enable_menu_reset() 21 | 22 | try: 23 | # Make libraries shipped by the app importable 24 | app_path = '/flash/' + '/'.join(path.split('/')[:-1]) 25 | sys.path.append(app_path) 26 | 27 | mod = __import__(path) 28 | if "main" in dir(mod): 29 | mod.main() 30 | except Exception as e: 31 | import sys 32 | import uio 33 | import ugfx 34 | s = uio.StringIO() 35 | sys.print_exception(e, s) 36 | ugfx.clear() 37 | ugfx.set_default_font(ugfx.FONT_SMALL) 38 | w=ugfx.Container(0,0,ugfx.width(),ugfx.height()) 39 | ugfx.Label(0,0,ugfx.width(),ugfx.height(),s.getvalue(),parent=w) 40 | w.show() 41 | raise(e) 42 | import stm 43 | stm.mem8[0x40002850] = 0x9C 44 | import pyb 45 | pyb.hard_reset() 46 | -------------------------------------------------------------------------------- /lib/ntp.py: -------------------------------------------------------------------------------- 1 | ### Description: Update the badge's time via NTP 2 | ### License: MIT 3 | 4 | import database 5 | import socket 6 | 7 | 8 | # (date(2000, 1, 1) - date(1900, 1, 1)).days * 24*60*60 9 | NTP_DELTA = 3155673600 10 | NTP_HOST = "pool.ntp.org" 11 | NTP_PORT = 123 12 | 13 | 14 | def get_NTP_time(): 15 | NTP_QUERY = bytearray(48) 16 | NTP_QUERY[0] = 0x1b 17 | addr = socket.getaddrinfo(NTP_HOST, NTP_PORT)[0][-1] 18 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 19 | s.sendto(NTP_QUERY, addr) 20 | 21 | # Setting timeout for receiving data. Because we're using UDP, 22 | # there's no need for a timeout on send. 23 | s.settimeout(2) 24 | msg = None 25 | try: 26 | msg = s.recv(48) 27 | except OSError: 28 | pass 29 | finally: 30 | s.close() 31 | 32 | if msg is None: 33 | return None 34 | 35 | import struct 36 | val = struct.unpack("!I", msg[40:44])[0] 37 | return val - NTP_DELTA 38 | 39 | 40 | def set_NTP_time(): 41 | import time 42 | from pyb import RTC 43 | print("Setting time from NTP") 44 | 45 | t = get_NTP_time() 46 | if t is None: 47 | print("Could not set time from NTP") 48 | return False 49 | 50 | tz = 0 51 | with database.Database() as db: 52 | tz = db.get("timezone", 0) 53 | 54 | tz_minutes = int(abs(tz) % 100) * (1 if tz >= 0 else -1) 55 | tz_hours = int(tz / 100) 56 | t += (tz_hours * 3600) + (tz_minutes * 60) 57 | 58 | tm = time.localtime(t) 59 | tm = tm[0:3] + (0,) + tm[3:6] + (0,) 60 | 61 | rtc = RTC() 62 | rtc.init() 63 | rtc.datetime(tm) 64 | 65 | return True 66 | -------------------------------------------------------------------------------- /examples/clock.py: -------------------------------------------------------------------------------- 1 | import pyb 2 | import math 3 | import ugfx 4 | 5 | # Example of how a simple animation can be done 6 | # ToDo: This is quite flickery. It would work a lot better with 7 | # Pixmaps, but I couldn't get them to work :( 8 | 9 | ugfx.init() 10 | 11 | sec = 0; 12 | 13 | def draw_hand(cx, cy, angle, length, thickness, color): 14 | x = int(math.cos(angle) * length + cx); 15 | y = int(math.sin(angle) * length + cy); 16 | ugfx.thickline(cx, cy, x, y, color, thickness, 1) 17 | 18 | while True: 19 | ugfx.area(0, 0, ugfx.width(), ugfx.height(), 0) 20 | 21 | # Center 22 | cx = int(ugfx.width() / 2); 23 | cy = int(ugfx.height() / 2); 24 | 25 | # Clock face 26 | ugfx.circle(cx, cy, 70, ugfx.WHITE) 27 | for i in range(0, 12): 28 | a = math.pi * 2 * i / 12 29 | x1 = int(math.cos(a) * 55 + cx); 30 | y1 = int(math.sin(a) * 55 + cy); 31 | x2 = int(math.cos(a) * 60 + cx); 32 | y2 = int(math.sin(a) * 60 + cy); 33 | ugfx.line(x1, y1, x2, y2, ugfx.WHITE) 34 | 35 | # Hand: hours 36 | angel_hour = math.pi * 2 * sec / 60 / 60 / 12 - math.pi / 2 37 | draw_hand(cx, cy, angel_hour, 35, 4, ugfx.YELLOW) 38 | 39 | # Hand: minutes 40 | angel_min = math.pi * 2 * sec / 60 / 60 - math.pi / 2 41 | draw_hand(cx, cy, angel_min, 40, 2, ugfx.WHITE) 42 | 43 | # Hand: seconds 44 | angel_seconds = math.pi * 2 * sec / 60 - math.pi / 2 45 | draw_hand(cx, cy, angel_seconds, 50, 1, ugfx.RED) 46 | 47 | # Wait 48 | pyb.delay(10) 49 | 50 | sec += 1; -------------------------------------------------------------------------------- /apps/logger/external.py: -------------------------------------------------------------------------------- 1 | from filesystem import is_file 2 | from database import database_get 3 | import stm 4 | import http_client 5 | import wifi 6 | import onboard 7 | import binascii 8 | 9 | needs_wifi = True 10 | period = 120 * 1000 11 | 12 | def tick(): 13 | bv = str(onboard.get_battery_voltage()) 14 | uv = str(onboard.get_unreg_voltage()) 15 | li = str(onboard.get_light()) 16 | wifi.nic().get_rssi() 17 | 18 | aps = wifi.nic().list_aps() 19 | highest_rssi = -200 20 | nearestbssid = "" 21 | for a in aps: 22 | if (a['rssi'] > highest_rssi) and (a['rssi'] < 0): 23 | highest_rssi = a['rssi'] 24 | nearestbssid = binascii.hexlify(a['bssid']) 25 | 26 | logfile = "log.txt" 27 | if not highest_rssi > -200: 28 | rssis = "," 29 | json={"vbat" : bv, "vunreg" : uv, "light" : li} 30 | else: 31 | rssis = str(highest_rssi) + "," + str(nearestbssid) 32 | r1 = stm.mem32[0x1FFF7590] 33 | r1 |= (stm.mem32[0x1FFF7594]<<32) 34 | r1 |= (stm.mem32[0x1FFF7598]<<64) 35 | json={"vbat" : bv, "vunreg" : uv, "light" : li, "rssi" : str(highest_rssi), "bssid" : str(nearestbssid), "uuid":"%x" % r1} 36 | 37 | if database_get("stats_upload"): 38 | try: 39 | if wifi.nic().is_connected(): 40 | with http_client.post('http://api.badge.emfcamp.org/api/barms', json=json): 41 | pass 42 | except OSError as e: 43 | print("Upload failed " + str(e)) 44 | 45 | try: 46 | if not is_file(logfile): 47 | with open(logfile, "w") as f: 48 | f.write("vbat, vunreg, light, rssi, bssid \r\n") 49 | 50 | with open(logfile, "a") as f: 51 | f.write(bv + ", " + uv + ", " + li + ", " + rssis + "\r\n") 52 | except OSError as e: 53 | print("Logging failed: " + str(e)) 54 | return "Logging failed" 55 | -------------------------------------------------------------------------------- /lib/imu.py: -------------------------------------------------------------------------------- 1 | ### Author: EMF Badge team 2 | ### Description: Allows access to IMU on the TiLDA 3 | ### License: MIT 4 | import pyb, ustruct 5 | 6 | IMU_ADDRESS = 0x6A 7 | IMU_REG_WHO_AM_I = 0x0F 8 | IMU_REG_ACCEL_DATA = 0X28 9 | 10 | class IMU: 11 | """Simple IMU interface 12 | 13 | Usage: 14 | imu = IMU() 15 | while True: 16 | print(imu.get_acceleration()) 17 | pyb.delay(1000); 18 | """ 19 | def __init__(self): 20 | self.accuracy = 8 21 | 22 | self.i2c = pyb.I2C(3, pyb.I2C.MASTER) 23 | self.i2c.init(pyb.I2C.MASTER) 24 | 25 | pyb.delay(20) 26 | 27 | count = 0 28 | while not self.i2c.is_ready(IMU_ADDRESS): 29 | pyb.delay(10) 30 | count += 1 31 | if count > 100: 32 | raise OSError("Can't connect to IMU") 33 | 34 | self.self_check() 35 | 36 | settings_acceleration = 0x00 | 0x04 | 0x40 # ToDo: make this configurable 37 | self.i2c.mem_write(settings_acceleration, IMU_ADDRESS, 0x10) 38 | # ToDo: Add Gyro 39 | 40 | self.self_check() 41 | 42 | def self_check(self): 43 | if self.i2c.mem_read(1, IMU_ADDRESS, IMU_REG_WHO_AM_I)[0] != 0x69: 44 | raise OSError("IMU self check failed") 45 | 46 | def _acceleration_raw_to_float(self, data, offset): 47 | input = ustruct.unpack_from("h", data, offset)[0]; 48 | return input * 0.061 * self.accuracy / 1000 49 | 50 | def get_acceleration(self): 51 | data = self.i2c.mem_read(6, IMU_ADDRESS, IMU_REG_ACCEL_DATA) 52 | return { 53 | 'x': self._acceleration_raw_to_float(data, 0), 54 | 'y': self._acceleration_raw_to_float(data, 2), 55 | 'z': self._acceleration_raw_to_float(data, 4) 56 | } 57 | 58 | # ToDo: Add way to de-init i2c 59 | -------------------------------------------------------------------------------- /lib/onboard.py: -------------------------------------------------------------------------------- 1 | import pyb 2 | import stm 3 | 4 | def get_temperature(): 5 | global adc_obj, ref_obj 6 | tval = adc_obj.read() 7 | ref_reading = ref_obj.read() 8 | factory_reading = stm.mem16[0x1FFF75AA] 9 | reference_voltage = factory_reading/4095*3 10 | supply_voltage = 4095/ref_reading*reference_voltage 11 | adc30_3v = stm.mem16[0x1FFF75A8] 12 | adc110_3v = stm.mem16[0x1FFF75CA] 13 | grad = (adc110_3v - adc30_3v)/(110-30) 14 | tval_3v = tval/3*supply_voltage 15 | diff = (adc30_3v - tval_3v)/grad 16 | return 30 - diff 17 | 18 | 19 | def get_unreg_voltage(): 20 | global adc_obj, ref_obj 21 | vin = adc_obj.read() 22 | ref_reading = ref_obj.read() 23 | factory_reading = stm.mem16[0x1FFF75AA] 24 | reference_voltage = factory_reading/4095*3 25 | supply_voltage = 4095/ref_reading*reference_voltage 26 | return 2 * vin / 4095 * supply_voltage 27 | 28 | def get_battery_voltage(): 29 | global vbat_obj, ref_obj 30 | vin = vbat_obj.read() 31 | ref_reading = ref_obj.read() 32 | factory_reading = stm.mem16[0x1FFF75AA] 33 | reference_voltage = factory_reading/4095*3 34 | supply_voltage = 4095/ref_reading*reference_voltage 35 | return 6 * vin / 4095 * supply_voltage 36 | 37 | def get_battery_percentage(): 38 | v = get_unreg_voltage() 39 | return int( (v-3.7) / (4.15-3.7) * 100) 40 | 41 | def get_light(): 42 | global light_obj 43 | return light_obj.read() 44 | 45 | adc_obj = pyb.ADC(pyb.Pin("ADC_UNREG")) 46 | ref_obj = pyb.ADC(0) 47 | temp_obj = pyb.ADC(17) 48 | vbat_obj = pyb.ADC(18) 49 | light_obj = pyb.ADC(16) 50 | 51 | def hide_splash_on_next_boot(hide=True): 52 | if hide: 53 | stm.mem8[0x40002850] = 0x9C 54 | else: 55 | stm.mem8[0x40002850] = 0x00 56 | 57 | def is_splash_hidden(): 58 | return stm.mem8[0x40002850] == 0x9C 59 | 60 | def semihard_reset(): 61 | hide_splash_on_next_boot() 62 | pyb.hard_reset() 63 | -------------------------------------------------------------------------------- /examples/clock_vsync.py: -------------------------------------------------------------------------------- 1 | import pyb 2 | import math 3 | import ugfx 4 | 5 | # Example of how a simple animation can be done 6 | # ToDo: This is quite flickery. It would work a lot better with 7 | # Pixmaps, but I couldn't get them to work :( 8 | 9 | ugfx.init() 10 | ugfx.enable_tear() 11 | tear = pyb.Pin("TEAR", pyb.Pin.IN) 12 | ugfx.set_tear_line((int(320/2)+0)) 13 | ugfx.area(0,0,ugfx.width(), ugfx.height(), 0) 14 | sec = 0; 15 | 16 | def draw_hand(cx, cy, angle, length, thickness, color): 17 | x = int(math.cos(angle) * length + cx); 18 | y = int(math.sin(angle) * length + cy); 19 | ugfx.thickline(cx, cy, x, y, color, thickness, 1) 20 | 21 | while True: 22 | 23 | 24 | # Center 25 | cx = int(ugfx.width() / 2); 26 | cy = int(ugfx.height() / 2); 27 | 28 | 29 | # Hand: hours 30 | angel_hour = math.pi * 2 * sec / 60 / 60 / 12 - math.pi / 2 31 | 32 | # Hand: minutes 33 | angel_min = math.pi * 2 * sec / 60 / 60 - math.pi / 2 34 | 35 | # Hand: seconds 36 | angel_seconds = math.pi * 2 * sec / 60 - math.pi / 2 37 | 38 | # wait for vsync 39 | while(tear.value() == 0): 40 | pass 41 | while(tear.value()): 42 | pass 43 | #Do all the drawing at once 44 | ugfx.area(cx-71, cy-71, 141, 141, 0) 45 | # Hands 46 | draw_hand(cx, cy, angel_hour, 35, 4, ugfx.YELLOW) 47 | draw_hand(cx, cy, angel_seconds, 50, 1, ugfx.RED) 48 | draw_hand(cx, cy, angel_min, 40, 2, ugfx.WHITE) 49 | # Clock face 50 | ugfx.circle(cx, cy, 70, ugfx.WHITE) 51 | for i in range(0, 12): 52 | a = math.pi * 2 * i / 12 53 | x1 = int(math.cos(a) * 55 + cx); 54 | y1 = int(math.sin(a) * 55 + cy); 55 | x2 = int(math.cos(a) * 60 + cx); 56 | y2 = int(math.sin(a) * 60 + cy); 57 | ugfx.line(x1, y1, x2, y2, ugfx.WHITE) 58 | 59 | 60 | # Wait 61 | pyb.delay(10) 62 | 63 | sec += 1; 64 | 65 | ugfx.disable_tear() -------------------------------------------------------------------------------- /apps/changetz/main.py: -------------------------------------------------------------------------------- 1 | ### Author: Thibault ML 2 | ### Description: Change timezone settings 3 | ### Category: Settings 4 | ### License: MIT 5 | ### Appname : Change Timezone 6 | 7 | import pyb 8 | import dialogs 9 | import database 10 | import ugfx 11 | 12 | class Timezone: 13 | def __init__(self, name, value): 14 | self.name = name 15 | self.value = value 16 | 17 | def __str__(self): 18 | return self.name 19 | 20 | timezone_list = [ 21 | Timezone("UTC-12:00", -1200), 22 | Timezone("UTC-11:00", -1100), 23 | Timezone("UTC-10:00", -1000), 24 | Timezone("UTC-09:30", -0930), 25 | Timezone("UTC-09:00", -0900), 26 | Timezone("UTC-08:00", -0800), 27 | Timezone("UTC-07:00", -0700), 28 | Timezone("UTC-06:00", -0600), 29 | Timezone("UTC-05:00", -0500), 30 | Timezone("UTC-04:00", -0400), 31 | Timezone("UTC-03:30", -0330), 32 | Timezone("UTC-03:00", -0300), 33 | Timezone("UTC-02:00", -0200), 34 | Timezone("UTC-01:00", -0100), 35 | Timezone("UTC+00:00", +0000), 36 | Timezone("UTC+01:00", +0100), 37 | Timezone("UTC+02:00", +0200), 38 | Timezone("UTC+03:00", +0300), 39 | Timezone("UTC+03:30", +0330), 40 | Timezone("UTC+04:00", +0400), 41 | Timezone("UTC+04:30", +0430), 42 | Timezone("UTC+05:00", +0500), 43 | Timezone("UTC+05:30", +0530), 44 | Timezone("UTC+05:45", +0545), 45 | Timezone("UTC+06:00", +0600), 46 | Timezone("UTC+06:30", +0630), 47 | Timezone("UTC+07:00", +0700), 48 | Timezone("UTC+08:00", +0800), 49 | Timezone("UTC+08:30", +0830), 50 | Timezone("UTC+08:45", +0845), 51 | Timezone("UTC+09:00", +0900), 52 | Timezone("UTC+09:30", +0930), 53 | Timezone("UTC+10:00", +1000), 54 | Timezone("UTC+10:30", +1030), 55 | Timezone("UTC+11:00", +1100), 56 | Timezone("UTC+12:00", +1200), 57 | Timezone("UTC+12:45", +1245), 58 | Timezone("UTC+13:00", +1300), 59 | Timezone("UTC+14:00", +1400) 60 | ] 61 | 62 | ugfx.init() 63 | 64 | tz = dialogs.prompt_option(timezone_list, text="Select your timezone:", index=14) 65 | with database.Database() as db: 66 | db.set("timezone", int(tz.value)) 67 | -------------------------------------------------------------------------------- /examples/buttons.py: -------------------------------------------------------------------------------- 1 | import buttons 2 | import ugfx 3 | 4 | up = 0 5 | down = 0 6 | left = 0 7 | right = 0 8 | 9 | def callback_arrow_up(line): 10 | global up 11 | up = 1 12 | 13 | def callback_arrow_down(line): 14 | global down 15 | down = 1 16 | 17 | def callback_arrow_right(line): 18 | global left 19 | left = 1 20 | 21 | def callback_arrow_left(line): 22 | global right 23 | right = 1 24 | 25 | buttons.init() 26 | buttons.enable_interrupt("JOY_UP", callback_arrow_up) 27 | buttons.enable_interrupt("JOY_DOWN", callback_arrow_down) 28 | buttons.enable_interrupt("JOY_LEFT", callback_arrow_left) 29 | buttons.enable_interrupt("JOY_RIGHT", callback_arrow_right) 30 | 31 | while True: 32 | if up: 33 | up = 0 34 | ugfx.area(40,0,20,20,0) 35 | #else: 36 | # ugfx.area(40,0,20,20,0xFFFF) 37 | # up = 0 38 | 39 | if down: 40 | down = 0 41 | ugfx.area(40,50,20,20,0) 42 | #else: 43 | # down = 0 44 | # ugfx.area(40,50,20,20,0xFFFF) 45 | 46 | if right: 47 | right = 0 48 | ugfx.area(70,25,20,20,0) 49 | # else: 50 | # right = 0 51 | # ugfx.area(70,25,20,20,0xFFFF) 52 | 53 | if left: 54 | left = 0 55 | ugfx.area(10,25,20,20,0) 56 | # else: 57 | # left = 0 58 | # ugfx.area(10,25,20,20,0xFFFF) 59 | 60 | 61 | if buttons.is_pressed("JOY_UP"): 62 | ugfx.area(140,0,20,20,0) 63 | else: 64 | ugfx.area(140,0,20,20,0xFFFF) 65 | #ugfx.area(40,0,20,20,0xFFFF) 66 | 67 | if buttons.is_pressed("JOY_DOWN"): 68 | ugfx.area(140,50,20,20,0) 69 | else: 70 | ugfx.area(140,50,20,20,0xFFFF) 71 | #ugfx.area(40,50,20,20,0xFFFF) 72 | 73 | if buttons.is_pressed("JOY_RIGHT"): 74 | ugfx.area(170,25,20,20,0) 75 | else: 76 | ugfx.area(170,25,20,20,0xFFFF) 77 | #ugfx.area(70,25,20,20,0xFFFF) 78 | 79 | if buttons.is_pressed("JOY_LEFT"): 80 | ugfx.area(110,25,20,20,0) 81 | else: 82 | ugfx.area(110,25,20,20,0xFFFF) 83 | #ugfx.area(10,25,20,20,0xFFFF) 84 | -------------------------------------------------------------------------------- /lib/filesystem.py: -------------------------------------------------------------------------------- 1 | ### Author: EMF Badge team 2 | ### Description: Small set of micropython specific filesystem helpers 3 | ### License: MIT 4 | 5 | import os 6 | import hashlib 7 | import binascii 8 | 9 | def get_app_foldername(path): 10 | """Gets the app name based on a path""" 11 | if not is_file(path): 12 | return "" 13 | 14 | s = path.split("/") 15 | if not (len(s) >= 2): 16 | return "" 17 | 18 | if s[0] == "examples": 19 | if s[-1].endswith(".py"): 20 | return ((s[-1])[:-3]) 21 | else: 22 | return "" 23 | else: 24 | return s[-2] 25 | 26 | def get_app_attribute(path, attribute): 27 | if not is_file(path): 28 | return "" 29 | rv = "" 30 | attribute = attribute.lower() 31 | try: 32 | with open(path) as f: 33 | while True: ## ToDo: set the max lines to loop over to be 20 or so 34 | l = f.readline() 35 | if l.startswith("### "): 36 | kv = l[4:].split(":",1) 37 | if len(kv) >= 2: 38 | if (kv[0].strip().lower() == attribute): 39 | rv = kv[1].strip() 40 | break; 41 | else: 42 | break 43 | 44 | except OSError: 45 | return "" 46 | return rv 47 | 48 | def is_dir(path): 49 | """Checks whether a path exists and is a director""" 50 | try: 51 | return os.stat(path)[0] & 61440 == 16384 52 | except OSError as e: 53 | if e.args[0] == 2: 54 | return False 55 | else: 56 | raise e 57 | 58 | def is_file(path): 59 | """Checks whether a path exists and is a regular file""" 60 | try: 61 | return os.stat(path)[0] & 61440 == 32768 62 | except OSError as e: 63 | if e.args[0] == 2: 64 | return False 65 | else: 66 | raise e 67 | 68 | def exists(path): 69 | """Checks whether a path exists""" 70 | try: 71 | os.stat(path) 72 | return True 73 | except OSError as e: 74 | if e.args[0] == 2: 75 | return False 76 | else: 77 | raise e 78 | 79 | def calculate_hash(filename, raise_on_not_found = False): 80 | """Calculates the SHA256 hash of a file. 81 | 82 | Unless raise_on_not_found is set returns 'NOTFOUND' if the file can't be found 83 | """ 84 | if not is_file(filename) and not raise_on_not_found: 85 | return "NOTFOUND" 86 | 87 | with open(filename, "rb") as file: 88 | sha256 = hashlib.sha256() 89 | buf = file.read(128) 90 | while len(buf) > 0: 91 | sha256.update(buf) 92 | buf = file.read(128) 93 | return str(binascii.hexlify(sha256.digest()), "utf8") 94 | -------------------------------------------------------------------------------- /lib/database.py: -------------------------------------------------------------------------------- 1 | ### Author: EMF Badge team 2 | ### Description: A simple json backed key/value store 3 | ### License: MIT 4 | 5 | import os 6 | import json 7 | 8 | class Database: 9 | """A simple key/value store backed by a json file 10 | 11 | Keys need to be convertable to str 12 | Values can be anything json can store, including a dict 13 | 14 | Usage: 15 | import database 16 | with database.open() as db: 17 | print(db.get("hello", "default")) 18 | db.set("foo", "world") 19 | db.delete("bar") 20 | """ 21 | 22 | def __init__(self, filename = "config.json"): 23 | self.filename = filename 24 | self.dirty = False 25 | try: 26 | with open(filename, "rt") as file: 27 | self.data = json.loads(file.read()) 28 | except (OSError, ValueError): 29 | print("Database %s doesn't exists or is invalid, creating new" % (filename)) 30 | self.data = {} 31 | self.dirty = True 32 | self.flush() 33 | 34 | def set(self, key, value): 35 | """Sets a value for a given key. 36 | 37 | 'key' gets converted into a string 38 | 'value' can be anything that json can store, including a dict 39 | """ 40 | self.data[key] = value 41 | self.dirty = True 42 | 43 | def get(self, key, default_value = None): 44 | """Returns the value for a given key. 45 | 46 | If key is not found 'default_value' will be returned 47 | """ 48 | return self.data[key] if key in self.data else default_value 49 | 50 | def delete(self, key): 51 | """Deletes a key/value pair""" 52 | if key in self.data: 53 | del self.data[key] 54 | self.dirty = True 55 | 56 | 57 | def flush(self): 58 | """Writes changes to flash""" 59 | if self.dirty: 60 | with open(self.filename, "wt") as file: 61 | file.write(json.dumps(self.data)) 62 | file.flush() 63 | os.sync() 64 | self.dirty = False 65 | 66 | def __enter__(self): 67 | return self 68 | 69 | def __exit__(self, exc_type, exc_value, traceback): 70 | self.flush() 71 | 72 | 73 | def database_get(key, default_value = None, *args): 74 | with Database(*args) as db: 75 | return db.get(key, default_value) 76 | 77 | def database_set(key, value, *args): 78 | with Database(*args) as db: 79 | return db.set(key, value) 80 | 81 | def database_delete(key, *args): 82 | with Database(*args) as db: 83 | return db.delete(key) 84 | -------------------------------------------------------------------------------- /bootstrap.py: -------------------------------------------------------------------------------- 1 | # TiLDA Badge Bootstrap script 2 | # Automatically downloads the app library to the badge via wifi 3 | import pyb 4 | import machine 5 | import os 6 | import ugfx 7 | import hashlib 8 | import binascii 9 | import uio 10 | import sys 11 | import buttons 12 | import dialogs 13 | import wifi 14 | from http_client import get 15 | 16 | def calculate_hash(filename): 17 | try: 18 | with open(filename, "rb") as file: 19 | sha256 = hashlib.sha256() 20 | buf = file.read(128) 21 | while len(buf) > 0: 22 | sha256.update(buf) 23 | buf = file.read(128) 24 | return str(binascii.hexlify(sha256.digest()), "utf8") 25 | except: 26 | return "ERR" 27 | 28 | def download(url, target, expected_hash): 29 | while True: 30 | get(url).raise_for_status().download_to(target) 31 | if calculate_hash(target) == expected_hash: 32 | break 33 | 34 | ugfx.init() 35 | buttons.init() 36 | 37 | wifi.connect( 38 | wait=True, 39 | show_wait_message=True, 40 | prompt_on_fail=True, 41 | dialog_title='TiLDA Setup' 42 | ) 43 | 44 | addendum = "\n\n\n\nIf stalled for 2 minutes please press the reset button on the back" 45 | with dialogs.WaitingMessage(text="Please wait" + addendum, title="Downloading TiLDA software") as message: 46 | 47 | success = False 48 | failure_counter = 0 49 | URL = "http://api.badge.emfcamp.org/firmware" 50 | 51 | while not success: 52 | for d in ["apps", "apps/app_library", "lib"]: 53 | try: 54 | os.remove(d) # Sometimes FS corruption leads to files instead of folders 55 | except OSError as e: 56 | pass 57 | try: 58 | os.mkdir(d) 59 | except OSError as e: 60 | print(e) 61 | 62 | try: 63 | message.text = "Downloading list of libraries" + addendum 64 | master = get(URL + "/master.json").raise_for_status().json() 65 | libs_to_update = [] 66 | for i, (lib, expected_hash) in enumerate(master["lib"].items()): 67 | message.text ="Downloading library: %s (%d/%d)%s" % (lib, i + 1, len(master["lib"]), addendum) 68 | download(URL + "/master/lib/%s" % lib, "lib/%s" % lib, expected_hash) 69 | 70 | message.text = "Downloading app library" + addendum 71 | download(URL + "/master/apps/app_library/main.py", "apps/app_library/main.py", master["apps"]["app_library"]["main.py"]) 72 | success = True 73 | 74 | except Exception as e: 75 | error_string = uio.StringIO() 76 | sys.print_exception(e, error_string) 77 | error_string = error_string.getvalue() 78 | 79 | failure_counter += 1 80 | print("Error:") 81 | print(error_string) 82 | 83 | if failure_counter > 5: 84 | message.text = "Something went wrong for the 5th time, giving up :(\nError:\n%s" % error_string 85 | while True: 86 | pyb.wfi() 87 | 88 | message.text = "Something went wrong, trying again..." 89 | pyb.delay(1000) 90 | 91 | os.sync() 92 | machine.reset() 93 | -------------------------------------------------------------------------------- /apps/snake/main.py: -------------------------------------------------------------------------------- 1 | ### Author: EMF Badge team 2 | ### Description: Snake! 3 | ### Category: Games 4 | ### License: MIT 5 | ### Appname: Snake! 6 | ### Built-in: yes 7 | 8 | import pyb 9 | import math 10 | import ugfx 11 | import buttons 12 | 13 | ugfx.init() 14 | buttons.init() 15 | buttons.disable_menu_reset() 16 | 17 | def one_round(): 18 | grid_size = 8; 19 | body_colour = ugfx.RED 20 | back_colour = 0; 21 | food_colour = ugfx.YELLOW 22 | wall_colour = ugfx.BLUE 23 | score = 0; 24 | edge_x = math.floor(ugfx.width()/grid_size)-2; 25 | edge_y = math.floor(ugfx.height()/grid_size)-2; 26 | 27 | def disp_square(x,y,colour): 28 | ugfx.area((x+1)*grid_size, (y+1)*grid_size, grid_size, grid_size, colour) 29 | 30 | def disp_body_straight(x,y,rotation,colour): 31 | if (rotation == 0): 32 | ugfx.area((x+1)*grid_size+1, (y+1)*grid_size+1, grid_size-2, grid_size, colour) 33 | elif (rotation == 90): 34 | ugfx.area((x+1)*grid_size+1, (y+1)*grid_size+1, grid_size, grid_size-2, colour) 35 | elif (rotation == 180): 36 | ugfx.area((x+1)*grid_size+1, (y+1)*grid_size-1, grid_size-2, grid_size, colour) 37 | else: 38 | ugfx.area((x+1)*grid_size-1, (y+1)*grid_size+1, grid_size, grid_size-2, colour) 39 | 40 | def disp_eaten_food(x,y,colour): 41 | ugfx.area((x+1)*grid_size, (y+1)*grid_size, grid_size, grid_size, colour) 42 | 43 | def randn_square(): 44 | return [pyb.rng()%edge_x, pyb.rng()%edge_y] 45 | 46 | body_x = [12,13,14,15,16] 47 | body_y = [2,2,2,2,2] 48 | 49 | ugfx.area(0,0,ugfx.width(),ugfx.height(),0) 50 | 51 | ugfx.area(0,0,grid_size*(edge_x+1),grid_size,wall_colour) 52 | ugfx.area(0,0,grid_size,grid_size*(edge_y+1),wall_colour) 53 | ugfx.area(grid_size*(edge_x+1),0,grid_size,grid_size*(edge_y+1),wall_colour) 54 | ugfx.area(0,grid_size*(edge_y+1),grid_size*(edge_x+2),grid_size,wall_colour) 55 | 56 | keepgoing = 1; 57 | 58 | food = [20,20] 59 | disp_square(food[0],food[1],food_colour) 60 | 61 | dir_x = 1 62 | dir_y = 0 63 | orient = 270 64 | 65 | #for i in range(0,len(body_x)): 66 | # disp_body_straight(body_x[i],body_y[i],orient,body_colour) 67 | 68 | while keepgoing: 69 | if buttons.is_pressed("JOY_RIGHT"): 70 | dir_x = 1; 71 | dir_y = 0; 72 | orient = 270 73 | elif buttons.is_pressed("JOY_LEFT"): 74 | dir_x = -1; 75 | dir_y = 0; 76 | orient = 90 77 | elif buttons.is_pressed("JOY_DOWN"): 78 | dir_y = 1; 79 | dir_x = 0; 80 | orient = 180 81 | elif buttons.is_pressed("JOY_UP"): 82 | dir_y = -1; 83 | dir_x = 0; 84 | orient = 0 85 | 86 | body_x.append(body_x[-1]+dir_x) 87 | body_y.append(body_y[-1]+dir_y) 88 | 89 | for i in range(0,len(body_x)-1): 90 | if (body_x[i] == body_x[-1]) and (body_y[i] == body_y[-1]): 91 | keepgoing = 0 92 | 93 | if not((body_x[-1] == food[0]) and (body_y[-1] == food[1])): 94 | x_del = body_x.pop(0) 95 | y_del = body_y.pop(0) 96 | disp_eaten_food(x_del,y_del,back_colour) 97 | else: 98 | disp_eaten_food(food[0],food[1],body_colour) 99 | food = randn_square() 100 | disp_square(food[0],food[1],food_colour) 101 | score = score + 1 102 | 103 | disp_body_straight(body_x[-1],body_y[-1],orient,body_colour) 104 | 105 | 106 | if ((body_x[-1] >= edge_x) or (body_x[-1] < 0) or (body_y[-1] >= edge_y) or (body_y[-1] < 0)): 107 | break 108 | 109 | pyb.delay(100) 110 | return score 111 | 112 | playing = 1 113 | while playing: 114 | score = one_round() 115 | ugfx.area(0,0,ugfx.width(),ugfx.height(),0) 116 | ugfx.text(30, 30, "GAME OVER Score: %d" % (score), 0xFFFF) 117 | ugfx.text(30, 60, "Press A to play again", 0xFFFF) 118 | ugfx.text(30, 90, "Press MENU to quit" , 0xFFFF) 119 | while True: 120 | pyb.wfi() 121 | if buttons.is_triggered("BTN_A"): 122 | break 123 | 124 | if buttons.is_triggered("BTN_MENU"): 125 | playing = 0 #pyb.hard_reset() 126 | break 127 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /lib/buttons.py: -------------------------------------------------------------------------------- 1 | ### Author: EMF Badge team 2 | ### Description: Convenience methods for dealing with the TiLDA buttons 3 | ### License: MIT 4 | 5 | import pyb 6 | import onboard 7 | 8 | CONFIG = { 9 | "JOY_UP": pyb.Pin.PULL_DOWN, 10 | "JOY_DOWN": pyb.Pin.PULL_DOWN, 11 | "JOY_RIGHT": pyb.Pin.PULL_DOWN, 12 | "JOY_LEFT": pyb.Pin.PULL_DOWN, 13 | "JOY_CENTER": pyb.Pin.PULL_DOWN, 14 | "BTN_MENU": pyb.Pin.PULL_UP, 15 | "BTN_A": pyb.Pin.PULL_UP, 16 | "BTN_B": pyb.Pin.PULL_UP 17 | } 18 | 19 | _tilda_pins = {} 20 | _tilda_interrupts = {} 21 | _tilda_bounce = {} 22 | 23 | def _get_pin(button): 24 | if button not in _tilda_pins: 25 | raise ValueError("Please call button.init() first before using any other button functions") 26 | return _tilda_pins[button] 27 | 28 | def init(buttons = CONFIG.keys()): 29 | """Inits all pins used by the TiLDA badge""" 30 | global _tilda_pins 31 | for button in buttons: 32 | _tilda_pins[button] = pyb.Pin(button, pyb.Pin.IN) 33 | _tilda_pins[button].init(pyb.Pin.IN, CONFIG[button]) 34 | 35 | def is_pressed(button): 36 | pin = _get_pin(button) 37 | if pin.pull() == pyb.Pin.PULL_DOWN: 38 | return pin.value() > 0 39 | else: 40 | return pin.value() == 0 41 | 42 | def is_triggered(button, interval = 30): 43 | """Use this function if you want buttons as a trigger for something in a loop 44 | 45 | It blocks for a while before returning a True and ignores trailing edge highs 46 | for a certain time to filter out bounce on both edges 47 | """ 48 | global _tilda_bounce 49 | if is_pressed(button): 50 | if button in _tilda_bounce: 51 | if pyb.millis() > _tilda_bounce[button]: 52 | del _tilda_bounce[button] 53 | else: 54 | return False # The button might have bounced back to high 55 | 56 | # Wait for a while to avoid bounces to low 57 | pyb.delay(interval) 58 | 59 | # Wait until button is released again 60 | while is_pressed(button): 61 | pyb.wfi() 62 | 63 | _tilda_bounce[button] = pyb.millis() + interval 64 | return True 65 | 66 | def has_interrupt(button): 67 | global _tilda_interrupts 68 | _get_pin(button) 69 | if button in _tilda_interrupts: 70 | return True 71 | else: 72 | return False 73 | 74 | 75 | def enable_interrupt(button, interrupt, on_press = True, on_release = False): 76 | """Attaches an interrupt to a button 77 | 78 | on_press defines whether it should be called when the button is pressed 79 | on_release defines whether it should be called when the button is releaseed 80 | 81 | The callback function must accept exactly 1 argument, which is the line that 82 | triggered the interrupt. 83 | """ 84 | global _tilda_interrupts 85 | pin = _get_pin(button) 86 | if button in _tilda_interrupts: 87 | # If someone tries to set an interrupt on a pin that already 88 | # has one that's totally ok, but we need to remove the old one 89 | # first 90 | disable_interrupt(button) 91 | 92 | if not (on_press or on_release): 93 | return 94 | 95 | mode = None; 96 | if on_press and on_release: 97 | mode = pyb.ExtInt.IRQ_RISING_FALLING 98 | else: 99 | if pin.pull() == pyb.Pin.PULL_DOWN: 100 | mode = pyb.ExtInt.IRQ_RISING if on_press else pyb.ExtInt.IRQ_FALLING 101 | else: 102 | mode = pyb.ExtInt.IRQ_FALLING if on_press else pyb.ExtInt.IRQ_RISING 103 | 104 | _tilda_interrupts[button] = { 105 | "interrupt": pyb.ExtInt(pin, mode, pin.pull(), interrupt), 106 | "mode": mode, 107 | "pin": pin 108 | } 109 | 110 | def disable_interrupt(button): 111 | global _tilda_interrupts 112 | if button in _tilda_interrupts: 113 | interrupt = _tilda_interrupts[button] 114 | pyb.ExtInt(interrupt["pin"], interrupt["mode"], interrupt["pin"].pull(), None) 115 | del _tilda_interrupts[button] 116 | init([button]) 117 | 118 | def disable_all_interrupt(): 119 | for interrupt in _tilda_interrupts: 120 | disable_interrupt(interrupt) 121 | 122 | def enable_menu_reset(): 123 | enable_interrupt("BTN_MENU", lambda t:onboard.semihard_reset(), on_release = True) 124 | 125 | def disable_menu_reset(): 126 | disable_interrupt("BTN_MENU") 127 | 128 | -------------------------------------------------------------------------------- /apps/home/quick_launch.py: -------------------------------------------------------------------------------- 1 | import ugfx 2 | import pyb 3 | import buttons 4 | import dialogs 5 | from database import Database, database_get, database_set 6 | import gc 7 | from app import App, empty_local_app_cache 8 | 9 | ugfx.init() 10 | ugfx.set_default_style(dialogs.default_style_badge) 11 | ugfx.clear(ugfx.html_color(dialogs.default_style_badge.background())) 12 | 13 | def _draw_cursor (x, y, color, win_quick): 14 | win_quick.fill_polygon(10 + x * 155, 15 + y * 40, [[0,0],[20,7],[0,14],[4,7]], color) 15 | 16 | def quick_launch_screen(): 17 | wi = ugfx.width() 18 | hi = ugfx.height() 19 | 20 | win_header = ugfx.Container(0,0,wi,30) 21 | win_quick = ugfx.Container(0,33,wi,hi-33-33) 22 | win_help = ugfx.Container(0,hi-30,wi,30) 23 | 24 | DEFAULT_APPS = ["app_library", "changename", "alistair~selectwifi", "snake"] 25 | with Database() as db: 26 | pinned = [App(a) for a in db.get("pinned_apps", DEFAULT_APPS)] 27 | pinned = [app for app in pinned if app.loadable] # Filter out deleted apps 28 | pinned = pinned[:7] # Limit to 7 29 | db.set("pinned_apps", [app.folder_name for app in pinned]) 30 | 31 | ugfx.set_default_font(ugfx.FONT_TITLE) 32 | title = ugfx.Label(3,3,wi-10,45,"EMF Camp 2016",parent=win_header) 33 | 34 | ugfx.set_default_font(ugfx.FONT_MEDIUM_BOLD) 35 | 36 | pinned_buttons = [] 37 | for i in range(0, 8): 38 | x = i % 2 39 | y = i // 2 40 | button_title = "Installed Apps" if i == 7 else "" 41 | if i < len(pinned): 42 | button_title = pinned[i].title 43 | pinned_buttons.append(ugfx.Button(35 + 155 * x, 5 + 40 * y, 120, 35, button_title, parent=win_quick)) 44 | 45 | btn_ok = ugfx.Button(10,5,20,20,"A",parent=win_help,shape=ugfx.Button.ELLIPSE) 46 | l_ok = ugfx.Label(40,5,100,20,"Run",parent=win_help) 47 | 48 | btn_back = ugfx.Button(100,5,20,20,"B",parent=win_help,shape=ugfx.Button.ELLIPSE) 49 | l_back = ugfx.Label(130,5,100,20,"Back",parent=win_help) 50 | 51 | btn_menu = ugfx.Button(200,5,20,20,"M",parent=win_help,shape=ugfx.Button.ROUNDED) 52 | l_menu = ugfx.Label(230,5,100,20,"Menu",parent=win_help) 53 | 54 | win_header.show() 55 | win_quick.show() 56 | win_help.show() 57 | 58 | buttons.init() 59 | cursor = {"x": 0, "y": 0} 60 | last_cursor = cursor.copy() 61 | _draw_cursor(0, 0, ugfx.RED, win_quick) 62 | 63 | if not database_get("quicklaunch_firstrun"): 64 | dialogs.notice("""This screen displays the most commonly used apps. 65 | Apps pinned here can also interact with the name screen. 66 | To view all apps, pin and un-pin, select 'Installed Apps' 67 | """, title="TiLDA - Quick Launch", close_text="Close") 68 | database_set("quicklaunch_firstrun", True) 69 | 70 | try: 71 | while True: 72 | pyb.wfi() 73 | 74 | if buttons.is_triggered("JOY_UP"): 75 | cursor["y"] = max(0, cursor["y"] - 1) 76 | if buttons.is_triggered("JOY_DOWN"): 77 | cursor["y"] = min(3, cursor["y"] + 1) 78 | if buttons.is_triggered("JOY_RIGHT"): 79 | cursor["x"] = 1 80 | if buttons.is_triggered("JOY_LEFT"): 81 | cursor["x"] = 0 82 | 83 | if cursor["x"] != last_cursor["x"] or cursor["y"] != last_cursor["y"]: # Has the cursor moved? 84 | _draw_cursor(last_cursor["x"], last_cursor["y"], dialogs.default_style_badge.background(), win_quick) 85 | _draw_cursor(cursor["x"], cursor["y"], ugfx.RED, win_quick) 86 | last_cursor = cursor.copy() 87 | 88 | if buttons.is_triggered("BTN_B"): 89 | return None 90 | 91 | #if buttons.is_triggered("BTN_MENU"): 92 | # open unpin dialog 93 | # break; 94 | 95 | if buttons.is_triggered("BTN_A"): 96 | index = cursor["x"] + cursor["y"] * 2 97 | if index == 7: 98 | return "file_loader" 99 | if index < len(pinned): 100 | return pinned[index] 101 | finally: 102 | buttons.disable_all_interrupt() 103 | 104 | win_header.hide() 105 | win_quick.hide() 106 | win_help.hide() 107 | for b in pinned_buttons: 108 | b.destroy() 109 | btn_ok.destroy() 110 | l_ok.destroy() 111 | btn_back.destroy() 112 | l_back.destroy() 113 | btn_menu.destroy() 114 | l_menu.destroy() 115 | win_header.destroy() 116 | win_quick.destroy() 117 | win_help.destroy() 118 | title.destroy() 119 | 120 | torun = quick_launch_screen() 121 | if torun: 122 | print("Running: %s" % torun) 123 | empty_local_app_cache() 124 | buttons.enable_menu_reset() 125 | gc.collect() 126 | pyb.info() 127 | 128 | import run_app 129 | if torun == "file_loader": 130 | run_app.run_app("apps/home/file_loader") 131 | else: 132 | rbr = torun.get_attribute("reboot-before-run") 133 | if type(rbr) == str and rbr.lower() == "false": 134 | run_app.run_app(torun.main_path[:-3]) 135 | run_app.reset_and_run(torun.main_path[:-3]) 136 | 137 | #ugfx.area(0,0,ugfx.width(),ugfx.height(),0) 138 | 139 | #deinit ugfx here 140 | #could hard reset here too 141 | 142 | # execfile("apps/%s/main.py" % (app_to_load)) 143 | -------------------------------------------------------------------------------- /lib/wifi.py: -------------------------------------------------------------------------------- 1 | ### Author: EMF Badge team 2 | ### Description: Handles connecting to a wifi access point based on a valid wifi.json file 3 | ### License: MIT 4 | 5 | import network 6 | import os 7 | import json 8 | import pyb 9 | import dialogs 10 | 11 | _nic = None 12 | 13 | def nic(): 14 | global _nic 15 | if not _nic: 16 | _nic = network.CC3100() 17 | return _nic 18 | 19 | def connection_details(): 20 | data = None 21 | try: 22 | if "wifi.json" in os.listdir(): 23 | with open("wifi.json") as f: 24 | data = json.loads(f.read()) 25 | if 'ssid' not in data or not data['ssid']: 26 | data = None 27 | except ValueError as e: 28 | print(e) 29 | 30 | return data 31 | 32 | def ssid(): 33 | return connection_details()["ssid"] 34 | 35 | def connect(wait=True, timeout=10, show_wait_message=False, prompt_on_fail=True, dialog_title='TiLDA'): 36 | retry_connect = True 37 | 38 | while retry_connect: 39 | if nic().is_connected(): 40 | return 41 | 42 | details = connection_details() 43 | if not details: 44 | if prompt_on_fail: 45 | choose_wifi(dialog_title=dialog_title) 46 | else: 47 | raise OSError("No valid wifi configuration") 48 | 49 | if not wait: 50 | connect_wifi(details, timeout=None, wait=False) 51 | return 52 | else: 53 | try: 54 | if show_wait_message: 55 | with dialogs.WaitingMessage(text="Connecting to '%s'...\n(%ss timeout)" % (details['ssid'], timeout), title=dialog_title): 56 | connect_wifi(details, timeout=timeout, wait=True) 57 | else: 58 | connect_wifi(details, timeout=timeout, wait=True) 59 | except OSError: 60 | if prompt_on_fail: 61 | retry_connect = dialogs.prompt_boolean( 62 | text="Failed to connect to '%s'" % details['ssid'], 63 | title=dialog_title, 64 | true_text="Try again", 65 | false_text="Forget it", 66 | ) 67 | if not retry_connect: 68 | os.remove('wifi.json') 69 | os.sync() 70 | # We would rather let you choose a new network here, but 71 | # scanning doesn't work after a connect at the moment 72 | pyb.hard_reset() 73 | else: 74 | raise 75 | 76 | def connect_wifi(details, timeout, wait=False): 77 | if 'pw' in details: 78 | nic().connect(details['ssid'], details['pw'], timeout=timeout) 79 | else: 80 | nic().connect(details['ssid'], timeout=timeout) 81 | 82 | if wait: 83 | while not nic().is_connected(): 84 | nic().update() 85 | pyb.delay(100) 86 | 87 | def is_connected(): 88 | return nic().is_connected() 89 | 90 | def get_security_level(ap): 91 | n = nic() 92 | levels = {} 93 | try: 94 | levels = { 95 | n.SCAN_SEC_OPEN: 0, # I am awful 96 | n.SCAN_SEC_WEP: 'WEP', 97 | n.SCAN_SEC_WPA: 'WPA', 98 | n.SCAN_SEC_WPA2: 'WPA2', 99 | } 100 | except AttributeError: 101 | print("Firmware too old to query wifi security level, please upgrade.") 102 | return None 103 | 104 | return levels.get(ap.get('security', None), None) 105 | 106 | def choose_wifi(dialog_title='TiLDA'): 107 | filtered_aps = [] 108 | with dialogs.WaitingMessage(text='Scanning for networks...', title=dialog_title): 109 | visible_aps = nic().list_aps() 110 | visible_aps.sort(key=lambda x:x['rssi'], reverse=True) 111 | # We'll get one result for each AP, so filter dupes 112 | for ap in visible_aps: 113 | title = ap['ssid'] 114 | security = get_security_level(ap) 115 | if security: 116 | title = title + ' (%s)' % security 117 | ap = { 118 | 'title': title, 119 | 'ssid': ap['ssid'], 120 | 'security': security, 121 | } 122 | if ap['ssid'] not in [ a['ssid'] for a in filtered_aps ]: 123 | filtered_aps.append(ap) 124 | del visible_aps 125 | 126 | ap = dialogs.prompt_option( 127 | filtered_aps, 128 | text='Choose wifi network', 129 | title=dialog_title 130 | ) 131 | if ap: 132 | key = None 133 | if ap['security'] != 0: 134 | # Backward compat 135 | if ap['security'] == None: 136 | ap['security'] = 'wifi' 137 | 138 | key = dialogs.prompt_text( 139 | "Enter %s key" % ap['security'], 140 | width = 310, 141 | height = 220 142 | ) 143 | with open("wifi.json", "wt") as file: 144 | if key: 145 | conn_details = {"ssid": ap['ssid'], "pw": key} 146 | else: 147 | conn_details = {"ssid": ap['ssid']} 148 | 149 | file.write(json.dumps(conn_details)) 150 | os.sync() 151 | # We can't connect after scanning for some bizarre reason, so we reset instead 152 | pyb.hard_reset() 153 | -------------------------------------------------------------------------------- /apps/ball_demo/main.py: -------------------------------------------------------------------------------- 1 | ### Author: Joel Bodenmann aka Tectu , Andrew Hannam aka inmarket 2 | ### Description: A python port of the UGFX ball demo 3 | ### Category: Examples 4 | ### License: BSD 5 | 6 | # Copyright (c) 2012, 2013, Joel Bodenmann aka Tectu 7 | # Copyright (c) 2012, 2013, Andrew Hannam aka inmarket 8 | # 9 | # All rights reserved. 10 | # 11 | # Redistribution and use in source and binary forms, with or without 12 | # modification, are permitted provided that the following conditions are met: 13 | # * Redistributions of source code must retain the above copyright 14 | # notice, this list of conditions and the following disclaimer. 15 | # * Redistributions in binary form must reproduce the above copyright 16 | # notice, this list of conditions and the following disclaimer in the 17 | # documentation and/or other materials provided with the distribution. 18 | # * Neither the name of the nor the 19 | # names of its contributors may be used to endorse or promote products 20 | # derived from this software without specific prior written permission. 21 | # 22 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 23 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 24 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 25 | # DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 26 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 27 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 28 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 29 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 30 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 31 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | 33 | import pyb 34 | import ugfx 35 | import buttons 36 | 37 | ugfx.init() 38 | buttons.init() 39 | 40 | BALLCOLOR1 = ugfx.RED 41 | BALLCOLOR2 = ugfx.YELLOW 42 | WALLCOLOR = ugfx.GREEN 43 | BACKCOLOR = ugfx.BLUE 44 | FLOORCOLOR = ugfx.PURPLE 45 | SHADOWALPHA = (255-255*0.2) 46 | 47 | width = ugfx.width() 48 | height = ugfx.height() 49 | 50 | radius=height/5+height%2+1 # The ball radius 51 | ii = 1.0/radius # radius as easy math 52 | floor=height/5-1 # floor position 53 | spin=0.0 # current spin angle on the ball 54 | spinspeed=0.1 # current spin speed of the ball 55 | ballx=width/2 # ball x position (relative to the ball center) 56 | bally=height/4 # ball y position (relative to the ball center) 57 | dx=.01*width # motion in the x axis 58 | dy=0.0 # motion in the y axis 59 | ballcx = 12*radius/5 # ball x diameter including the shadow 60 | ballcy = 21*radius/10 # ball y diameter including the shadow 61 | 62 | # The clipping window for this frame. 63 | minx = miny = 0 64 | maxx = width 65 | maxy = height 66 | 67 | def invsqrt(x): 68 | return x**-1/2 69 | 70 | while not buttons.is_triggered("BTN_MENU"): 71 | # Draw one frame 72 | ugfx.stream_start(minx, miny, maxx-minx, maxy-miny) 73 | for x in range(minx, maxx): 74 | g = (ballx-x)*ii 75 | for y in range(miny, maxy): 76 | h = (bally-y)*ii 77 | f=-.3*g+.954*h 78 | if g*g < 1-h*h: 79 | # The inside of the ball 80 | if ((int((9-spin+(.954*g+.3*h)*invsqrt(1-f*f)))+int((2+f*2))&1)): 81 | colour = BALLCOLOR1 82 | else: 83 | colour = BALLCOLOR2 84 | else: 85 | # The background (walls and floor) 86 | if y > height-floor: 87 | if x < height-y or height-y > width-x: 88 | colour = WALLCOLOR 89 | else: 90 | colour = FLOORCOLOR 91 | elif xwidth-floor: 92 | colour = WALLCOLOR 93 | else: 94 | colour = BACKCOLOR 95 | 96 | # The ball shadow is darker 97 | #if (g*(g+.4)+h*(h+.1) < 1) 98 | # colour = gdispBlendColor(colour, Black, SHADOWALPHA); 99 | 100 | ugfx.stream_color(colour) # pixel to the LCD 101 | ugfx.stream_stop() 102 | 103 | # Calculate the new frame size (note this is a drawing optimisation only) 104 | minx = ballx - radius 105 | miny = bally - radius 106 | maxx = minx + ballcx 107 | maxy = miny + ballcy 108 | 109 | if dx > 0: 110 | maxx += dx 111 | else: 112 | minx += dx 113 | 114 | if dy > 0: 115 | maxy += dy 116 | else: 117 | miny += dy 118 | 119 | if minx < 0: 120 | minx = 0 121 | 122 | if maxx > width: 123 | maxx = width 124 | 125 | if miny < 0: 126 | miny = 0 127 | 128 | if maxy > height: 129 | maxy = height 130 | 131 | minx = int(minx); 132 | miny = int(miny); 133 | maxx = int(maxx); 134 | maxy = int(maxy); 135 | 136 | # Motion 137 | spin += spinspeed 138 | ballx += dx 139 | bally += dy 140 | 141 | if ballx < radius or ballx > width-radius: 142 | spinspeed = -spinspeed 143 | dx = -dx 144 | 145 | if bally > height-1.75*floor: 146 | dy = -.04*height 147 | else: 148 | dy = dy+.002*height; 149 | 150 | pyb.hard_reset() 151 | -------------------------------------------------------------------------------- /lib/mqtt.py: -------------------------------------------------------------------------------- 1 | import wifi 2 | import usocket as socket 3 | import ustruct as struct 4 | 5 | class MQTTException(Exception): 6 | pass 7 | 8 | class MQTTClient: 9 | 10 | def __init__(self, client_id, server, port=1883): 11 | # This will immediately return if we're already connected, otherwise 12 | # it'll attempt to connect or prompt for a new network. Proceeding 13 | # without an active network connection will cause the getaddrinfo to 14 | # fail. 15 | wifi.connect( 16 | wait=True, 17 | show_wait_message=False, 18 | prompt_on_fail=True, 19 | dialog_title='TiLDA Wifi' 20 | ) 21 | 22 | self.client_id = client_id 23 | self.sock = None 24 | self.addr = socket.getaddrinfo(server, port)[0][-1] 25 | self.pid = 0 26 | self.cb = None 27 | 28 | def _send_str(self, s): 29 | self.sock.send(struct.pack("!H", len(s))) 30 | self.sock.send(s) 31 | 32 | def _recv_len(self): 33 | n = 0 34 | sh = 0 35 | while 1: 36 | b = self.sock.recv(1)[0] 37 | n |= (b & 0x7f) << sh 38 | if not b & 0x80: 39 | return n 40 | sh += 7 41 | 42 | def set_callback(self, f): 43 | self.cb = f 44 | 45 | def connect(self, clean_session=True): 46 | self.sock = socket.socket() 47 | self.sock.connect(self.addr) 48 | msg = bytearray(b"\x10\0\0\x04MQTT\x04\x02\0\0") 49 | msg[1] = 10 + 2 + len(self.client_id) 50 | msg[9] = clean_session << 1 51 | self.sock.send(msg) 52 | #print(hex(len(msg)), hexlify(msg, ":")) 53 | self._send_str(self.client_id) 54 | resp = self.sock.recv(4) 55 | assert resp[0] == 0x20 and resp[1] == 0x02 56 | if resp[3] != 0: 57 | raise MQTTException(resp[3]) 58 | return resp[2] & 1 59 | 60 | def disconnect(self): 61 | self.sock.send(b"\xe0\0") 62 | self.sock.close() 63 | 64 | def ping(self): 65 | self.sock.send(b"\xc0\0") 66 | self.sock.close() 67 | 68 | def publish(self, topic, msg, retain=False, qos=0): 69 | pkt = bytearray(b"\x30\0\0") 70 | pkt[0] |= qos << 1 | retain 71 | sz = 2 + len(topic) + len(msg) 72 | if qos > 0: 73 | sz += 2 74 | assert sz <= 16383 75 | pkt[1] = (sz & 0x7f) | 0x80 76 | pkt[2] = sz >> 7 77 | #print(hex(len(pkt)), hexlify(pkt, ":")) 78 | self.sock.send(pkt) 79 | self._send_str(topic) 80 | if qos > 0: 81 | self.pid += 1 82 | pid = self.pid 83 | buf = bytearray(b"\0\0") 84 | struct.pack_into("!H", buf, 0, pid) 85 | self.sock.send(buf) 86 | self.sock.send(msg) 87 | if qos == 1: 88 | while 1: 89 | op = self.wait_msg() 90 | if op == 0x40: 91 | sz = self.sock.recv(1) 92 | assert sz == b"\x02" 93 | rcv_pid = self.sock.recv(2) 94 | rcv_pid = rcv_pid[0] << 8 | rcv_pid[1] 95 | if pid == rcv_pid: 96 | return 97 | elif qos == 2: 98 | assert 0 99 | 100 | def subscribe(self, topic, qos=0): 101 | assert self.cb is not None, "Subscribe callback is not set" 102 | pkt = bytearray(b"\x82\0\0\0") 103 | self.pid += 1 104 | struct.pack_into("!BH", pkt, 1, 2 + 2 + len(topic) + 1, self.pid) 105 | #print(hex(len(pkt)), hexlify(pkt, ":")) 106 | self.sock.send(pkt) 107 | self._send_str(topic) 108 | self.sock.send(qos.to_bytes(1)) 109 | resp = self.sock.recv(5) 110 | #print(resp) 111 | assert resp[0] == 0x90 112 | assert resp[2] == pkt[2] and resp[3] == pkt[3] 113 | if resp[4] == 0x80: 114 | raise MQTTException(resp[4]) 115 | 116 | # Wait for a single incoming MQTT message and process it. 117 | # Subscribed messages are delivered to a callback previously 118 | # set by .set_callback() method. Other (internal) MQTT 119 | # messages processed internally. 120 | def wait_msg(self): 121 | res = self.sock.recv(1) 122 | if res is None: 123 | return None 124 | self.sock.setblocking(True) 125 | if res == b"": 126 | raise OSError(-1) 127 | if res == b"\xd0": # PINGRESP 128 | sz = self.sock.recv(1)[0] 129 | assert sz == 0 130 | return None 131 | op = res[0] 132 | if op & 0xf0 != 0x30: 133 | return op 134 | sz = self._recv_len() 135 | topic_len = self.sock.recv(2) 136 | topic_len = (topic_len[0] << 8) | topic_len[1] 137 | topic = self.sock.recv(topic_len) 138 | sz -= topic_len + 2 139 | if op & 6: 140 | pid = self.sock.recv(2) 141 | pid = pid[0] << 8 | pid[1] 142 | sz -= 2 143 | msg = self.sock.recv(sz) 144 | self.cb(topic, msg) 145 | if op & 6 == 2: 146 | pkt = bytearray(b"\x40\x02\0\0") 147 | struct.pack_into("!H", pkt, 2, pid) 148 | self.sock.send(pkt) 149 | elif op & 6 == 4: 150 | assert 0 151 | 152 | # Checks whether a pending message from server is available. 153 | # If not, returns immediately with None. Otherwise, does 154 | # the same processing as wait_msg. 155 | def check_msg(self): 156 | self.sock.setblocking(False) 157 | return self.wait_msg() 158 | -------------------------------------------------------------------------------- /apps/home/file_loader.py: -------------------------------------------------------------------------------- 1 | import ugfx 2 | import os 3 | import pyb 4 | import buttons 5 | import dialogs 6 | from database import database_get, database_set 7 | from filesystem import is_dir, is_file 8 | import gc 9 | from app import get_local_apps, get_local_app_categories 10 | 11 | ugfx.init() 12 | buttons.init() 13 | ugfx.set_default_style(dialogs.default_style_badge) 14 | ugfx.clear(ugfx.html_color(dialogs.default_style_badge.background())) 15 | 16 | def update_options(options, category, pinned): 17 | options.disable_draw() 18 | apps = get_local_apps(category) 19 | out = [] 20 | while options.count(): 21 | options.remove_item(0) 22 | 23 | for app in apps: 24 | if app.get_attribute("built-in") == "hide": 25 | continue # No need to show the home app 26 | 27 | if app.folder_name in pinned: 28 | options.add_item("*%s" % app.title) 29 | else: 30 | options.add_item(app.title) 31 | out.append(app) 32 | 33 | options.selected_index(0) 34 | options.enable_draw() 35 | return out 36 | 37 | def file_loader(): 38 | width = ugfx.width() 39 | height = ugfx.height() 40 | buttons.disable_menu_reset() 41 | 42 | # Create visual elements 43 | win_header = ugfx.Container(0,0,width,30) 44 | win_files = ugfx.Container(0,33,int(width/2),height-33) 45 | win_preview = ugfx.Container(int(width/2)+2,33,int(width/2)-2,height-33) 46 | components = [win_header, win_files, win_preview] 47 | ugfx.set_default_font(ugfx.FONT_TITLE) 48 | components.append(ugfx.Label(3,3,width-10,29,"Choose App",parent=win_header)) 49 | ugfx.set_default_font(ugfx.FONT_MEDIUM) 50 | options = ugfx.List(0,30,win_files.width(),win_files.height()-30,parent=win_files) 51 | btnl = ugfx.Button(5,3,20,20,"<",parent=win_files) 52 | btnr = ugfx.Button(win_files.width()-7-20,3,20,20,">",parent=win_files) 53 | btnr.attach_input(ugfx.JOY_RIGHT,0) 54 | btnl.attach_input(ugfx.JOY_LEFT,0) 55 | components.append(options) 56 | components.append(btnr) 57 | components.append(btnl) 58 | ugfx.set_default_font(ugfx.FONT_MEDIUM_BOLD) 59 | l_cat = ugfx.Label(30,3,100,20,"Built-in",parent=win_files) 60 | components.append(l_cat) 61 | components.append(ugfx.Button(10,win_preview.height()-25,20,20,"A",parent=win_preview)) 62 | components.append(ugfx.Label(35,win_preview.height()-25,50,20,"Run",parent=win_preview)) 63 | components.append(ugfx.Button(80,win_preview.height()-25,20,20,"B",parent=win_preview)) 64 | components.append(ugfx.Label(105,win_preview.height()-25,100,20,"Back",parent=win_preview)) 65 | components.append(ugfx.Button(10,win_preview.height()-50,20,20,"M",parent=win_preview)) 66 | components.append(ugfx.Label(35,win_preview.height()-50,100,20,"Pin/Unpin",parent=win_preview)) 67 | ugfx.set_default_font(ugfx.FONT_SMALL) 68 | author = ugfx.Label(1,win_preview.height()-78,win_preview.width()-3,20,"by: ",parent=win_preview) 69 | desc = ugfx.Label(3,1,win_preview.width()-10,win_preview.height()-83,"",parent=win_preview,justification=ugfx.Label.LEFTTOP) 70 | components.append(author) 71 | components.append(desc) 72 | 73 | pinned = database_get("pinned_apps", []) 74 | catergories = get_local_app_categories() 75 | c_ptr = 0 76 | 77 | try: 78 | win_header.show() 79 | win_files.show() 80 | win_preview.show() 81 | 82 | pinned = database_get("pinned_apps", []) 83 | # apps = [] 84 | apps_path = [] 85 | 86 | if is_dir("apps"): 87 | for app in os.listdir("apps"): 88 | path = "apps/" + app 89 | if is_dir(path) and is_file(path + "/main.py"): 90 | apps_path.append(path + "/main.py") 91 | if is_dir("examples"): 92 | for app in os.listdir("examples"): 93 | path = "examples/" + app 94 | if is_file(path) and path.endswith(".py"): 95 | apps_path.append(path) 96 | 97 | displayed_apps = update_options(options, catergories[c_ptr], pinned) 98 | 99 | index_prev = -1; 100 | 101 | while True: 102 | pyb.wfi() 103 | ugfx.poll() 104 | 105 | if index_prev != options.selected_index(): 106 | if options.selected_index() < len(displayed_apps): 107 | author.text("by: %s" % displayed_apps[options.selected_index()].user) 108 | desc.text(displayed_apps[options.selected_index()].description) 109 | index_prev = options.selected_index() 110 | 111 | if buttons.is_triggered("JOY_LEFT"): 112 | if c_ptr > 0: 113 | c_ptr -= 1 114 | btnl.set_focus() 115 | l_cat.text(catergories[c_ptr]) 116 | displayed_apps = update_options(options, catergories[c_ptr], pinned) 117 | index_prev = -1 118 | 119 | if buttons.is_triggered("JOY_RIGHT"): 120 | if c_ptr < len(catergories)-1: 121 | c_ptr += 1 122 | btnr.set_focus() 123 | l_cat.text(catergories[c_ptr]) 124 | displayed_apps = update_options(options, catergories[c_ptr], pinned) 125 | index_prev = -1 126 | 127 | if buttons.is_triggered("BTN_MENU"): 128 | app = displayed_apps[options.selected_index()] 129 | if app.folder_name in pinned: 130 | pinned.remove(app.folder_name) 131 | else: 132 | pinned.append(app.folder_name) 133 | update_options(options, catergories[c_ptr], pinned) 134 | database_set("pinned_apps", pinned) 135 | 136 | if buttons.is_triggered("BTN_B"): 137 | return None 138 | 139 | if buttons.is_triggered("BTN_A"): 140 | return displayed_apps[options.selected_index()] 141 | 142 | finally: 143 | for component in components: 144 | component.destroy() 145 | 146 | app_to_load = file_loader() 147 | if app_to_load: 148 | gc.collect() 149 | buttons.enable_menu_reset() 150 | import run_app 151 | rbr = app_to_load.get_attribute("reboot-before-run") 152 | if type(rbr) == str and rbr.lower() == "false": 153 | run_app.run_app(app_to_load.main_path[:-3]) 154 | run_app.reset_and_run(app_to_load.main_path[:-3]) 155 | 156 | 157 | -------------------------------------------------------------------------------- /apps/logger/main.py: -------------------------------------------------------------------------------- 1 | ### Author: EMF Badge team 2 | ### Description: Log stuff to memory 3 | ### Category: Comms 4 | ### License: MIT 5 | ### Appname : BARMS_Logger 6 | ### Built-in: yes 7 | 8 | import ugfx 9 | from filesystem import is_file 10 | from database import database_get, database_set 11 | import pyb 12 | import math 13 | import buttons 14 | 15 | ugfx.init() 16 | buttons.init() 17 | 18 | def wait_for_exit(): 19 | global chk_upload 20 | buttons.init() 21 | while True: 22 | pyb.wfi() 23 | if buttons.is_triggered("BTN_B"): 24 | break; 25 | 26 | database_set("stats_upload", chk_upload.checked()) 27 | 28 | wi = ugfx.width() 29 | hi = ugfx.height() 30 | 31 | ugfx.clear() 32 | 33 | s = ugfx.Style() 34 | 35 | s.set_enabled([ugfx.BLACK, ugfx.html_color(0xA66FB0), ugfx.html_color(0x5e5e5e), ugfx.RED]) 36 | s.set_background(ugfx.html_color(0xFFFFFF)) 37 | 38 | ugfx.set_default_style(s) 39 | 40 | win_header = ugfx.Container(0,0,wi,33,style=s) 41 | win_legend = ugfx.Container(0,hi-30,wi,30,style=s) 42 | 43 | 44 | toplot = ['vbat','vunreg','light','rssi'] 45 | # scale to fit on the y scale (range 0->150) 46 | scale_m = [75, 75, 0.4, 1] 47 | scale_c = [-255, -255, 0, 100] 48 | colour = [ugfx.RED, ugfx.ORANGE, ugfx.YELLOW, ugfx.BLUE] 49 | 50 | buttons.disable_menu_reset() 51 | 52 | ugfx.set_default_font(ugfx.FONT_TITLE) 53 | title = ugfx.Label(3,3,wi-10,45,"Log Viewer",parent=win_header) 54 | ugfx.set_default_font(ugfx.FONT_SMALL) 55 | chk_upload = ugfx.Checkbox(190,3,130,20,"M: Enable uplink",parent=win_header) 56 | chk_upload.attach_input(ugfx.BTN_MENU,0) 57 | if database_get("stats_upload"): 58 | chk_upload.checked(1) 59 | 60 | win_header.show() 61 | win_legend.show() 62 | 63 | ugfx.set_default_font(ugfx.FONT_MEDIUM) 64 | ugfx.set_default_style(s) 65 | 66 | graph = ugfx.Graph(0,33,wi,hi-33-33,3,3) 67 | graph.appearance(ugfx.Graph.STYLE_POINT, ugfx.Graph.POINT_NONE, 0, 0) 68 | wi_g = wi - 3 69 | graph.show() 70 | ugfx.set_default_font(ugfx.FONT_SMALL) 71 | win_zoom = ugfx.Container(1,33,92,25) 72 | btnl = ugfx.Button(3,3,20,20,"<",parent=win_zoom) 73 | btnr = ugfx.Button(68,3,20,20,">",parent=win_zoom) 74 | l_cat = ugfx.Label(28,3,35,20,"1x",parent=win_zoom) 75 | btnr.attach_input(ugfx.JOY_RIGHT,0) 76 | btnl.attach_input(ugfx.JOY_LEFT,0) 77 | win_zoom.show() 78 | 79 | scaling = int((hi-33-33-30)/2) 80 | 81 | lines = 0 82 | names = [] 83 | seek = -1 84 | if not is_file("log.txt"): 85 | ugfx.text(20,100,"Log file not found",0) 86 | wait_for_exit() 87 | pyb.hard_reset() 88 | 89 | 90 | #open the file and see how long it is 91 | with open("log.txt","r") as f: 92 | l = f.readline() 93 | lines += 1; 94 | names = l.split(",") 95 | while len(f.readline()): 96 | lines += 1; 97 | 98 | 99 | cl = 0 100 | x_index = 0 101 | 102 | names=[n.strip() for n in names] 103 | 104 | xscale = int(max(math.floor(wi/lines),1)) 105 | 106 | zoom = [1, 2, 4, 8, 16] 107 | lines_z = [] 108 | for z in zoom: 109 | lines_z.append(lines-(z*wi_g)) 110 | seeks = [0, 0, 0, 0, 0] 111 | 112 | with open("log.txt","r") as f: 113 | #now we know how long the file is, look for the index of the start of the plotting area 114 | l=f.readline() #ignore the title 115 | ra = range(1,len(zoom)) 116 | while True: 117 | seek = f.tell() 118 | l=f.readline() 119 | if len(l) == 0: 120 | break 121 | for r in ra: 122 | if (cl == lines_z[r]): 123 | seeks[r] = seek 124 | if (cl >= lines-wi_g): 125 | seeks[0] = seek 126 | break 127 | cl += 1 128 | 129 | 130 | 131 | def plot(start,file_step,xscale): 132 | global names 133 | global toplot 134 | global scale_m 135 | global scale_c 136 | global graph 137 | print("drawing from index " + str(start) + " in steps of " + str(file_step) + " " + str(xscale)) 138 | seek = start 139 | with open("log.txt","r") as f: 140 | #plot each line 141 | col = 0 142 | for n in names: 143 | if n in toplot: 144 | f.seek(seek) 145 | graph.appearance(ugfx.Graph.STYLE_LINE, ugfx.Graph.LINE_SOLID, 3, colour[ toplot.index(n) ]) 146 | x_index = 0 147 | m = scale_m[ toplot.index(n) ] 148 | c = scale_c[ toplot.index(n) ] 149 | new_series = 1 150 | while True: 151 | rs = file_step 152 | while rs: 153 | l=f.readline() 154 | rs -= 1 155 | if len(l) == 0: 156 | break 157 | s = l.strip().split(",") 158 | if len(s) > col: 159 | try: 160 | data_y = int((float(s[col])*m)+c) 161 | graph.plot(x_index, data_y, new_series) 162 | new_series = 0 163 | except ValueError: 164 | pass 165 | x_index += xscale 166 | col += 1 167 | 168 | #plot the legend 169 | x = 0 170 | i=0 171 | ugfx.set_default_font(ugfx.FONT_SMALL) 172 | for p in toplot: 173 | ugfx.Label(x+13,0,50,25,p,parent=win_legend) 174 | win_legend.thickline(x,13,x+10,13,colour[ i ],3,1,) 175 | i += 1 176 | x += 75 177 | 178 | plot(seeks[0],zoom[0],xscale) 179 | 180 | plot_index = 0 181 | buttons.init() 182 | while True: 183 | pyb.wfi() 184 | ugfx.poll() 185 | inc = 0 186 | if buttons.is_triggered("JOY_RIGHT"): 187 | inc = -1 188 | if buttons.is_triggered("JOY_LEFT"): 189 | inc = 1 190 | if buttons.is_triggered("BTN_B"): 191 | break; 192 | 193 | if not inc == 0: 194 | inc += plot_index 195 | if inc < 0: 196 | pass 197 | elif inc >= len(zoom): 198 | pass 199 | elif seeks[0] == 0: ## dont allow zoom out if we dont have enough data 200 | pass 201 | else: 202 | plot_index = inc 203 | graph.destroy() 204 | graph = ugfx.Graph(0,33,wi,hi-33-33,3,3) 205 | graph.appearance(ugfx.Graph.STYLE_POINT, ugfx.Graph.POINT_NONE, 0, 0) 206 | graph.show() 207 | win_zoom.hide(); win_zoom.show() 208 | if plot_index == 0: 209 | l_cat.text("1x") 210 | else: 211 | l_cat.text("1/" + str(zoom[plot_index])+"x") 212 | plot(seeks[plot_index],zoom[plot_index],1) 213 | 214 | 215 | database_set("stats_upload", chk_upload.checked()) 216 | 217 | -------------------------------------------------------------------------------- /lib/app.py: -------------------------------------------------------------------------------- 1 | ### Author: EMF Badge team 2 | ### Description: Model and Helpers for TiLDA apps and the App Library API 3 | ### License: MIT 4 | import os 5 | import ure 6 | import http_client 7 | import filesystem 8 | import gc 9 | 10 | EMF_USER = "emf" 11 | USER_NAME_SEPARATOR = "~" 12 | ATTRIBUTE_MATCHER = ure.compile("^\s*###\s*([^:]*?)\s*:\s*(.*)\s*$") # Yeah, regex! 13 | CATEGORY_ALL = "all" 14 | CATEGORY_NOT_SET = "uncategorised" 15 | 16 | class App: 17 | """Models an app and provides some helper functions""" 18 | def __init__(self, folder_name, api_information = None): 19 | self.folder_name = self.name = folder_name.lower() 20 | self.user = EMF_USER 21 | if USER_NAME_SEPARATOR in folder_name: 22 | [self.user, self.name] = folder_name.split(USER_NAME_SEPARATOR, 1) 23 | self.user = self.user.lower() 24 | self.name = self.name.lower() 25 | 26 | self._attributes = None # Load lazily 27 | self.api_information = api_information 28 | 29 | @property 30 | def folder_path(self): 31 | return "apps/" + self.folder_name 32 | 33 | @property 34 | def main_path(self): 35 | return self.folder_path + "/main.py" 36 | 37 | @property 38 | def loadable(self): 39 | return filesystem.is_file(self.main_path) and os.stat(self.main_path)[6] > 0 40 | 41 | @property 42 | def description(self): 43 | """either returns a local attribute or uses api_information""" 44 | if self.api_information and "description" in self.api_information: 45 | return self.api_information["description"] 46 | return self.get_attribute("description") or "" 47 | 48 | @property 49 | def files(self): 50 | """returns a list of file dicts or returns False if the information is not available""" 51 | if self.api_information and "files" in self.api_information: 52 | return self.api_information["files"] 53 | return False 54 | 55 | @property 56 | def category(self): 57 | return self.get_attribute("Category", CATEGORY_NOT_SET).lower() 58 | 59 | @property 60 | def title(self): 61 | return self.get_attribute("appname") or self.name 62 | 63 | @property 64 | def user_and_title(self): 65 | if self.user == EMF_USER: 66 | return self.name 67 | else: 68 | return "%s by %s" % (self.title, self.user) 69 | 70 | def matches_category(self, category): 71 | """returns True if provided category matches the category of this app""" 72 | category = category.lower() 73 | return category == CATEGORY_ALL or category == self.category 74 | 75 | @property 76 | def attributes(self): 77 | """Returns all attribues of this app 78 | 79 | The result is cached for the lifetime of this object 80 | """ 81 | if self._attributes == None: 82 | self._attributes = {} 83 | if self.loadable: 84 | with open(self.main_path) as file: 85 | for line in file: 86 | match = ATTRIBUTE_MATCHER.match(line) 87 | if match: 88 | self._attributes[match.group(1).strip().lower()] = match.group(2).strip() 89 | else: 90 | break 91 | return self._attributes 92 | 93 | def get_attribute(self, attribute, default=None): 94 | """Returns the value of an attribute, or a specific default value if attribute is not found""" 95 | attribute = attribute.lower() # attributes are case insensitive 96 | if attribute in self.attributes: 97 | return self.attributes[attribute] 98 | else: 99 | return default 100 | 101 | def fetch_api_information(self): 102 | """Queries the API for information about this app, returns False if app is not publicly listed""" 103 | with http_client.get("http://api.badge.emfcamp.org/api/app/%s/%s" % (self.user, self.name)) as response: 104 | if response.status == 404: 105 | return False 106 | self.api_information = response.raise_for_status().json() 107 | return self.api_information 108 | 109 | def __str__(self): 110 | return self.user_and_title 111 | 112 | def __repr__(self): 113 | return "" % (self.folder_name) 114 | 115 | 116 | def app_by_name_and_user(name, user): 117 | """Returns an user object""" 118 | if user.lower() == EMF_USER: 119 | return App(name) 120 | else: 121 | return App(user + USER_NAME_SEPARATOR + name) 122 | 123 | def app_by_api_response(response): 124 | if response["user"].lower() == EMF_USER: 125 | return App(response["name"], response) 126 | else: 127 | return App(response["user"] + USER_NAME_SEPARATOR + response["name"], response) 128 | 129 | def get_local_apps(category=CATEGORY_ALL): 130 | """Returns a list of apps that can be found in the apps folder""" 131 | apps = [App(folder_name) for folder_name in os.listdir("apps") if filesystem.is_dir("apps/" + folder_name)] 132 | return [app for app in apps if app.matches_category(category)] 133 | 134 | _public_apps_cache = None 135 | def fetch_public_app_api_information(uncached=False): 136 | """Returns a dict category => list of apps 137 | 138 | Uses cached version unless the uncached parameter is set 139 | """ 140 | global _public_apps_cache 141 | if not _public_apps_cache or uncached: 142 | response = {} 143 | for category, apps in http_client.get("http://api.badge.emfcamp.org/api/apps").raise_for_status().json().items(): 144 | response[category] = [app_by_api_response(app) for app in apps] 145 | 146 | _public_apps_cache = response 147 | return _public_apps_cache 148 | 149 | def get_public_app_categories(uncached=False): 150 | """Returns a list of all categories used on the app library""" 151 | return list(fetch_public_app_api_information(uncached).keys()) 152 | 153 | def get_public_apps(category=CATEGORY_ALL, uncached=False): 154 | """Returns a list of all public apps in one category""" 155 | category = category.lower() 156 | api_information = fetch_public_app_api_information(uncached) 157 | return api_information[category] if category in api_information else [] 158 | 159 | _category_cache = None 160 | def get_local_app_categories(uncached=False): 161 | """Returns a list of all app categories the user's apps are currently using 162 | 163 | Uses cached version unless the uncached parameter is set 164 | """ 165 | global _category_cache 166 | if not _category_cache or uncached: 167 | _category_cache = ["all"] 168 | for app in get_local_apps(): 169 | if app.category not in _category_cache: 170 | _category_cache.append(app.category) 171 | 172 | return _category_cache 173 | 174 | def empty_local_app_cache(): 175 | """If you're tight on memory you can clean up the local cache""" 176 | global _public_apps_cache, _category_cache 177 | _public_apps_cache = None 178 | _category_cache = None 179 | gc.collect() 180 | -------------------------------------------------------------------------------- /apps/app_library/main.py: -------------------------------------------------------------------------------- 1 | ### author: emf badge team 2 | ### description: updates and installs apps. To publish apps use https://badge.emfcamp.org 3 | ### license: MIT 4 | ### reboot-before-run: True 5 | ### Appname: App Library 6 | 7 | import pyb 8 | import ugfx 9 | import os 10 | import http_client 11 | import wifi 12 | import dialogs 13 | from app import App, get_local_apps, get_public_apps, get_public_app_categories, empty_local_app_cache 14 | import filesystem 15 | 16 | TEMP_FILE = ".temp_download" 17 | 18 | ugfx.init() 19 | 20 | def clear(): 21 | ugfx.clear(ugfx.html_color(0x7c1143)) 22 | 23 | def download(url, target, expected_hash): 24 | if filesystem.calculate_hash(target) == expected_hash: 25 | return 26 | count = 0 27 | 28 | while filesystem.calculate_hash(TEMP_FILE) != expected_hash: 29 | count += 1 30 | if count > 5: 31 | os.remove(TEMP_FILE) 32 | raise OSError("Aborting download of %s after 5 unsuccessful attempts" % url) 33 | try: 34 | http_client.get(url).raise_for_status().download_to(TEMP_FILE) 35 | except OSError: 36 | pass 37 | 38 | # If it already exists the rename will fail 39 | try: 40 | os.remove(target) 41 | except OSError: 42 | pass 43 | os.rename(TEMP_FILE, target) 44 | 45 | def download_list(items, message_dialog): 46 | for i, item in enumerate(items): 47 | message_dialog.text = "Downloading %s (%d/%d)" % (item["title"], i + 1, len(items)) 48 | download(item["url"], item["target"], item["expected_hash"]) 49 | 50 | def download_app(app, message_dialog): 51 | files_to_update = [] 52 | for file in app.files: 53 | file_path = "%s/%s" % (app.folder_path, file["file"]) 54 | if file["hash"] != filesystem.calculate_hash(file_path): 55 | data = { 56 | "url": file["link"], 57 | "target": file_path, 58 | "expected_hash": file["hash"], 59 | "title": app.folder_name + "/" + file["file"] 60 | } 61 | 62 | if file["file"] == "main.py": # Make sure the main.py is the last file we load 63 | files_to_update.append(data) 64 | else: 65 | files_to_update.insert(0, data) 66 | 67 | download_list(files_to_update, message_dialog) 68 | 69 | def connect(): 70 | wifi.connect( 71 | wait=True, 72 | show_wait_message=True, 73 | prompt_on_fail=True, 74 | dialog_title='TiLDA App Library' 75 | ) 76 | 77 | ### VIEWS ### 78 | 79 | def main_menu(): 80 | while True: 81 | clear() 82 | 83 | menu_items = [ 84 | {"title": "Browse app library", "function": store}, 85 | {"title": "Update apps and libs", "function": update}, 86 | {"title": "Remove app", "function": remove} 87 | ] 88 | 89 | option = dialogs.prompt_option(menu_items, none_text="Exit", text="What do you want to do?", title="TiLDA App Library") 90 | 91 | if option: 92 | option["function"]() 93 | else: 94 | return 95 | 96 | def update(): 97 | clear() 98 | connect() 99 | 100 | with dialogs.WaitingMessage(text="Downloading full list of library files", title="TiLDA App Library") as message: 101 | message.text="Downloading full list of library files" 102 | master = http_client.get("http://api.badge.emfcamp.org/firmware/master-lib.json").raise_for_status().json() 103 | libs_to_update = [] 104 | for lib, expected_hash in master.items(): 105 | if expected_hash != filesystem.calculate_hash("lib/" + lib): 106 | libs_to_update.append({ 107 | "url": "http://api.badge.emfcamp.org/firmware/master/lib/" + lib, 108 | "target": "lib/" + lib, 109 | "expected_hash": expected_hash, 110 | "title": lib 111 | }) 112 | download_list(libs_to_update, message) 113 | 114 | apps = get_local_apps() 115 | for i, app in enumerate(apps): 116 | message.text = "Updating app %s" % app 117 | if app.fetch_api_information(): 118 | download_app(app, message) 119 | 120 | dialogs.notice("Everything is up-to-date") 121 | 122 | def store(): 123 | global apps_by_category 124 | 125 | while True: 126 | empty_local_app_cache() 127 | clear() 128 | connect() 129 | 130 | with dialogs.WaitingMessage(text="Fetching app library...", title="TiLDA App Library"): 131 | categories = get_public_app_categories() 132 | 133 | category = dialogs.prompt_option(categories, text="Please select a category", select_text="Browse", none_text="Back") 134 | if category: 135 | store_category(category) 136 | else: 137 | return 138 | 139 | def store_category(category): 140 | while True: 141 | clear() 142 | app = dialogs.prompt_option(get_public_apps(category), text="Please select an app", select_text="Details / Install", none_text="Back") 143 | if app: 144 | store_details(category, app) 145 | empty_local_app_cache() 146 | else: 147 | return 148 | 149 | def store_details(category, app): 150 | clear() 151 | empty_local_app_cache() 152 | with dialogs.WaitingMessage(text="Fetching app information...", title="TiLDA App Library"): 153 | app.fetch_api_information() 154 | 155 | clear() 156 | if dialogs.prompt_boolean(app.description, title = str(app), true_text = "Install", false_text="Back"): 157 | install(app) 158 | dialogs.notice("%s has been successfully installed" % app) 159 | 160 | def install(app): 161 | clear() 162 | connect() 163 | 164 | with dialogs.WaitingMessage(text="Installing %s" % app, title="TiLDA App Library") as message: 165 | if not app.files: 166 | app.fetch_api_information() 167 | 168 | if not filesystem.is_dir(app.folder_path): 169 | os.mkdir(app.folder_path) 170 | 171 | download_app(app, message) 172 | 173 | def remove(): 174 | clear() 175 | 176 | app = dialogs.prompt_option(get_local_apps(), title="TiLDA App Library", text="Please select an app to remove", select_text="Remove", none_text="Back") 177 | 178 | if app: 179 | clear() 180 | with dialogs.WaitingMessage(text="Removing %s\nPlease wait..." % app, title="TiLDA App Library"): 181 | for file in os.listdir(app.folder_path): 182 | os.remove(app.folder_path + "/" + file) 183 | os.remove(app.folder_path) 184 | remove() 185 | 186 | if App("home").loadable: 187 | main_menu() 188 | else: 189 | for app_name in ["changename", "snake", "alistair~selectwifi", "sponsors", "home"]: 190 | install(App(app_name)) 191 | pyb.hard_reset() 192 | -------------------------------------------------------------------------------- /lib/dialogs.py: -------------------------------------------------------------------------------- 1 | ### Author: EMF Badge team 2 | ### Description: Some basic UGFX powered dialogs 3 | ### License: MIT 4 | 5 | import ugfx 6 | import buttons 7 | import pyb 8 | 9 | default_style_badge = ugfx.Style() 10 | default_style_badge.set_focus(ugfx.RED) 11 | default_style_badge.set_enabled([ugfx.WHITE, ugfx.html_color(0x3C0246), ugfx.GREY, ugfx.RED]) 12 | default_style_badge.set_background(ugfx.html_color(0x3C0246)) 13 | 14 | default_style_dialog = ugfx.Style() 15 | default_style_dialog.set_enabled([ugfx.BLACK, ugfx.html_color(0xA66FB0), ugfx.html_color(0xdedede), ugfx.RED]) 16 | default_style_dialog.set_background(ugfx.html_color(0xFFFFFF)) 17 | 18 | 19 | TILDA_COLOR = ugfx.html_color(0x7c1143); 20 | 21 | def notice(text, title="TiLDA", close_text="Close", width = 260, height = 180, font=ugfx.FONT_SMALL, style=None): 22 | prompt_boolean(text, title = title, true_text = close_text, false_text = None, width = width, height = height, font=font, style=style) 23 | 24 | def prompt_boolean(text, title="TiLDA", true_text="Yes", false_text="No", width = 260, height = 180, font=ugfx.FONT_SMALL, style=None): 25 | """A simple one and two-options dialog 26 | 27 | if 'false_text' is set to None only one button is displayed. 28 | If both 'true_text' and 'false_text' are given a boolean is returned 29 | """ 30 | global default_style_dialog 31 | if style == None: 32 | style = default_style_dialog 33 | ugfx.set_default_font(ugfx.FONT_MEDIUM_BOLD) 34 | window = ugfx.Container((ugfx.width() - width) // 2, (ugfx.height() - height) // 2, width, height, style=style) 35 | window.show() 36 | ugfx.set_default_font(font) 37 | window.text(5, 10, title, TILDA_COLOR) 38 | window.line(0, 30, width, 30, ugfx.BLACK) 39 | 40 | if false_text: 41 | true_text = "A: " + true_text 42 | false_text = "B: " + false_text 43 | 44 | ugfx.set_default_font(font) 45 | label = ugfx.Label(5, 30, width - 10, height - 80, text = text, parent=window) 46 | ugfx.set_default_font(ugfx.FONT_MEDIUM_BOLD) 47 | button_yes = ugfx.Button(5, height - 40, width // 2 - 15 if false_text else width - 15, 30 , true_text, parent=window) 48 | button_no = ugfx.Button(width // 2 + 5, height - 40, width // 2 - 15, 30 , false_text, parent=window) if false_text else None 49 | 50 | try: 51 | buttons.init() 52 | 53 | button_yes.attach_input(ugfx.BTN_A,0) 54 | if button_no: button_no.attach_input(ugfx.BTN_B,0) 55 | 56 | window.show() 57 | 58 | while True: 59 | pyb.wfi() 60 | if buttons.is_triggered("BTN_A"): return True 61 | if buttons.is_triggered("BTN_B"): return False 62 | 63 | finally: 64 | window.hide() 65 | window.destroy() 66 | button_yes.destroy() 67 | if button_no: button_no.destroy() 68 | label.destroy() 69 | 70 | 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): 71 | """Shows a dialog and keyboard that allows the user to input/change a string 72 | 73 | Returns None if user aborts with button B 74 | """ 75 | 76 | window = ugfx.Container(int((ugfx.width()-width)/2), int((ugfx.height()-height)/2), width, height, style=style) 77 | 78 | if false_text: 79 | true_text = "M: " + true_text 80 | false_text = "B: " + false_text 81 | 82 | if buttons.has_interrupt("BTN_MENU"): 83 | buttons.disable_interrupt("BTN_MENU") 84 | 85 | ugfx.set_default_font(ugfx.FONT_MEDIUM) 86 | kb = ugfx.Keyboard(0, int(height/2), width, int(height/2), parent=window) 87 | edit = ugfx.Textbox(5, int(height/2)-30, int(width*4/5)-10, 25, text = init_text, parent=window) 88 | ugfx.set_default_font(ugfx.FONT_SMALL) 89 | button_yes = ugfx.Button(int(width*4/5), int(height/2)-30, int(width*1/5)-3, 25 , true_text, parent=window) 90 | 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 91 | ugfx.set_default_font(font) 92 | label = ugfx.Label(int(width/10), int(height/10), int(width*4/5), int(height*2/5)-60, description, parent=window) 93 | 94 | try: 95 | buttons.init() 96 | 97 | button_yes.attach_input(ugfx.BTN_MENU,0) 98 | if button_no: button_no.attach_input(ugfx.BTN_B,0) 99 | 100 | window.show() 101 | edit.set_focus() 102 | while True: 103 | pyb.wfi() 104 | ugfx.poll() 105 | #if buttons.is_triggered("BTN_A"): return edit.text() 106 | if buttons.is_triggered("BTN_B"): return None 107 | if buttons.is_triggered("BTN_MENU"): return edit.text() 108 | 109 | finally: 110 | window.hide() 111 | window.destroy() 112 | button_yes.destroy() 113 | if button_no: button_no.destroy() 114 | label.destroy() 115 | kb.destroy() 116 | edit.destroy(); 117 | return 118 | 119 | def prompt_option(options, index=0, text = "Please select one of the following:", title=None, select_text="OK", none_text=None): 120 | """Shows a dialog prompting for one of multiple options 121 | 122 | If none_text is specified the user can use the B or Menu button to skip the selection 123 | if title is specified a blue title will be displayed about the text 124 | """ 125 | ugfx.set_default_font(ugfx.FONT_SMALL) 126 | window = ugfx.Container(5, 5, ugfx.width() - 10, ugfx.height() - 10) 127 | window.show() 128 | 129 | list_y = 30 130 | if title: 131 | window.text(5, 10, title, TILDA_COLOR) 132 | window.line(0, 25, ugfx.width() - 10, 25, ugfx.BLACK) 133 | window.text(5, 30, text, ugfx.BLACK) 134 | list_y = 50 135 | else: 136 | window.text(5, 10, text, ugfx.BLACK) 137 | 138 | options_list = ugfx.List(5, list_y, ugfx.width() - 25, 180 - list_y, parent = window) 139 | 140 | for option in options: 141 | if isinstance(option, dict) and option["title"]: 142 | options_list.add_item(option["title"]) 143 | else: 144 | options_list.add_item(str(option)) 145 | options_list.selected_index(index) 146 | 147 | select_text = "A: " + select_text 148 | if none_text: 149 | none_text = "B: " + none_text 150 | 151 | button_select = ugfx.Button(5, ugfx.height() - 50, 140 if none_text else ugfx.width() - 25, 30 , select_text, parent=window) 152 | button_none = ugfx.Button(ugfx.width() - 160, ugfx.height() - 50, 140, 30 , none_text, parent=window) if none_text else None 153 | 154 | try: 155 | buttons.init() 156 | 157 | while True: 158 | pyb.wfi() 159 | ugfx.poll() 160 | if buttons.is_triggered("BTN_A"): return options[options_list.selected_index()] 161 | if button_none and buttons.is_triggered("BTN_B"): return None 162 | if button_none and buttons.is_triggered("BTN_MENU"): return None 163 | 164 | finally: 165 | window.hide() 166 | window.destroy() 167 | options_list.destroy() 168 | button_select.destroy() 169 | if button_none: button_none.destroy() 170 | ugfx.poll() 171 | 172 | class WaitingMessage: 173 | """Shows a dialog with a certain message that can not be dismissed by the user""" 174 | def __init__(self, text = "Please Wait...", title="TiLDA"): 175 | self.window = ugfx.Container(30, 30, ugfx.width() - 60, ugfx.height() - 60) 176 | self.window.show() 177 | self.window.text(5, 10, title, TILDA_COLOR) 178 | self.window.line(0, 30, ugfx.width() - 60, 30, ugfx.BLACK) 179 | self.label = ugfx.Label(5, 40, self.window.width() - 10, ugfx.height() - 40, text = text, parent=self.window) 180 | 181 | # Indicator to show something is going on 182 | self.indicator = ugfx.Label(ugfx.width() - 100, 0, 20, 20, text = "...", parent=self.window) 183 | self.timer = pyb.Timer(3) 184 | self.timer.init(freq=3) 185 | self.timer.callback(lambda t: self.indicator.visible(not self.indicator.visible())) 186 | 187 | def destroy(self): 188 | self.timer.deinit() 189 | self.label.destroy() 190 | self.indicator.destroy() 191 | self.window.destroy() 192 | 193 | @property 194 | def text(self): 195 | return self.label.text() 196 | 197 | @text.setter 198 | def text(self, value): 199 | self.label.text(value) 200 | 201 | def __enter__(self): 202 | return self 203 | 204 | def __exit__(self, exc_type, exc_value, traceback): 205 | self.destroy() 206 | 207 | -------------------------------------------------------------------------------- /lib/http_client.py: -------------------------------------------------------------------------------- 1 | ### Author: EMF Badge team 2 | ### Description: A basic HTTP library, based on https://github.com/balloob/micropython-http-client 3 | ### License: MIT 4 | 5 | import usocket 6 | import ujson 7 | import os 8 | import time 9 | import gc 10 | import wifi 11 | 12 | """Usage 13 | from http_client import * 14 | 15 | print(get("http://example.com").raise_for_status().content) 16 | post("http://mydomain.co.uk/api/post", urlencoded="SOMETHING").raise_for_status().close() # If response is not consumed you need to close manually 17 | # Or, if you prefer the with syntax: 18 | with post("http://mydomain.co.uk/api/post", urlencoded="SOMETHING") as response: 19 | response.raise_for_error() # No manual close needed 20 | """ 21 | 22 | SUPPORT_TIMEOUT = hasattr(usocket.socket, 'settimeout') 23 | CONTENT_TYPE_JSON = 'application/json' 24 | BUFFER_SIZE = 1024 25 | 26 | class Response(object): 27 | def __init__(self): 28 | self.encoding = 'utf-8' 29 | self.headers = {} 30 | self.status = None 31 | self.socket = None 32 | self._content = None 33 | 34 | # Hands the responsibility for a socket over to this reponse. This needs to happen 35 | # before any content can be inspected 36 | def add_socket(self, socket, content_so_far): 37 | self.content_so_far = content_so_far 38 | self.socket = socket 39 | 40 | @property 41 | def content(self, timeout=90): 42 | start_time = time.time() 43 | if not self._content: 44 | if not self.socket: 45 | raise OSError("Invalid response socket state. Has the content been downloaded instead?") 46 | try: 47 | if "Content-Length" in self.headers: 48 | content_length = int(self.headers["Content-Length"]) 49 | elif "content-length" in self.headers: 50 | content_length = int(self.headers["content-length"]) 51 | else: 52 | raise Exception("No Content-Length") 53 | self._content = self.content_so_far 54 | del self.content_so_far 55 | while len(self._content) < content_length: 56 | buf = self.socket.recv(BUFFER_SIZE) 57 | self._content += buf 58 | if (time.time() - start_time) > timeout: 59 | raise Exception("HTTP request timeout") 60 | 61 | finally: 62 | self.close() 63 | return self._content; 64 | 65 | @property 66 | def text(self): 67 | return str(self.content, self.encoding) if self.content else '' 68 | 69 | # If you don't use the content of a Response at all you need to manually close it 70 | def close(self): 71 | if self.socket is not None: 72 | self.socket.close() 73 | self.socket = None 74 | 75 | def json(self): 76 | return ujson.loads(self.text) 77 | 78 | # Writes content into a file. This function will write while receiving, which avoids 79 | # having to load all content into memory 80 | def download_to(self, target, timeout=90): 81 | start_time = time.time() 82 | if not self.socket: 83 | raise OSError("Invalid response socket state. Has the content already been consumed?") 84 | try: 85 | if "Content-Length" in self.headers: 86 | remaining = int(self.headers["Content-Length"]) 87 | elif "content-length" in self.headers: 88 | remaining = int(self.headers["content-length"]) 89 | else: 90 | raise Exception("No Content-Length") 91 | 92 | with open(target, 'wb') as f: 93 | f.write(self.content_so_far) 94 | remaining -= len(self.content_so_far) 95 | del self.content_so_far 96 | while remaining > 0: 97 | buf = self.socket.recv(BUFFER_SIZE) 98 | f.write(buf) 99 | remaining -= len(buf) 100 | 101 | if (time.time() - start_time) > timeout: 102 | raise Exception("HTTP request timeout") 103 | 104 | f.flush() 105 | os.sync() 106 | 107 | finally: 108 | self.close() 109 | 110 | def raise_for_status(self): 111 | if 400 <= self.status < 500: 112 | raise OSError('Client error: %s' % self.status) 113 | if 500 <= self.status < 600: 114 | raise OSError('Server error: %s' % self.status) 115 | return self 116 | 117 | # In case you want to use "with" 118 | def __enter__(self): 119 | return self 120 | 121 | def __exit__(self, exc_type, exc_value, traceback): 122 | self.close() 123 | 124 | def open_http_socket(method, url, json=None, timeout=None, headers=None, urlencoded = None): 125 | # This will immediately return if we're already connected, otherwise 126 | # it'll attempt to connect or prompt for a new network. Proceeding 127 | # without an active network connection will cause the getaddrinfo to 128 | # fail. 129 | wifi.connect( 130 | wait=True, 131 | show_wait_message=False, 132 | prompt_on_fail=True, 133 | dialog_title='TiLDA Wifi' 134 | ) 135 | 136 | urlparts = url.split('/', 3) 137 | proto = urlparts[0] 138 | host = urlparts[2] 139 | urlpath = '' if len(urlparts) < 4 else urlparts[3] 140 | 141 | if proto == 'http:': 142 | port = 80 143 | elif proto == 'https:': 144 | port = 443 145 | else: 146 | raise OSError('Unsupported protocol: %s' % proto[:-1]) 147 | 148 | if ':' in host: 149 | host, port = host.split(':') 150 | port = int(port) 151 | 152 | if json is not None: 153 | content = ujson.dumps(json) 154 | content_type = CONTENT_TYPE_JSON 155 | elif urlencoded is not None: 156 | content = urlencoded 157 | content_type = "application/x-www-form-urlencoded" 158 | else: 159 | content = None 160 | 161 | # ToDo: Handle IPv6 addresses 162 | if is_ipv4_address(host): 163 | addr = (host, port) 164 | else: 165 | ai = usocket.getaddrinfo(host, port) 166 | addr = ai[0][4] 167 | 168 | sock = None 169 | if proto == 'https:': 170 | sock = usocket.socket(usocket.AF_INET, usocket.SOCK_STREAM, usocket.SEC_SOCKET) 171 | else: 172 | sock = usocket.socket() 173 | 174 | sock.connect(addr) 175 | if proto == 'https:': 176 | sock.settimeout(0) # Actually make timeouts working properly with ssl 177 | 178 | sock.send('%s /%s HTTP/1.0\r\nHost: %s\r\n' % (method, urlpath, host)) 179 | 180 | if headers is not None: 181 | for header in headers.items(): 182 | sock.send('%s: %s\r\n' % header) 183 | 184 | if content is not None: 185 | sock.send('content-length: %s\r\n' % len(content)) 186 | sock.send('content-type: %s\r\n' % content_type) 187 | sock.send('\r\n') 188 | sock.send(content) 189 | else: 190 | sock.send('\r\n') 191 | 192 | return sock 193 | 194 | # Adapted from upip 195 | def request(method, url, json=None, timeout=None, headers=None, urlencoded=None): 196 | sock = open_http_socket(method, url, json, timeout, headers, urlencoded) 197 | try: 198 | response = Response() 199 | state = 1 200 | hbuf = b"" 201 | while True: 202 | buf = sock.recv(BUFFER_SIZE) 203 | if state == 1: # Status 204 | nl = buf.find(b"\n") 205 | if nl > -1: 206 | hbuf += buf[:nl - 1] 207 | response.status = int(hbuf.split(b' ')[1]) 208 | state = 2 209 | hbuf = b""; 210 | buf = buf[nl + 1:] 211 | else: 212 | hbuf += buf 213 | 214 | if state == 2: # Headers 215 | hbuf += buf 216 | nl = hbuf.find(b"\n") 217 | while nl > -1: 218 | if nl < 2: 219 | buf = hbuf[2:] 220 | hbuf = None 221 | state = 3 222 | break 223 | 224 | header = hbuf[:nl - 1].decode("utf8").split(':', 3) 225 | response.headers[header[0].strip()] = header[1].strip() 226 | hbuf = hbuf[nl + 1:] 227 | nl = hbuf.find(b"\n") 228 | 229 | if state == 3: # Content 230 | response.add_socket(sock, buf) 231 | sock = None # It's not our responsibility to close the socket anymore 232 | return response 233 | finally: 234 | if sock: sock.close() 235 | gc.collect() 236 | 237 | def get(url, **kwargs): 238 | attempts = 0 239 | while attempts < 5: 240 | try: 241 | return request('GET', url, **kwargs) 242 | except OSError: 243 | attempts += 1 244 | time.sleep(1) 245 | raise OSError('GET Failed') 246 | 247 | def post(url, **kwargs): 248 | return request('POST', url, **kwargs) 249 | 250 | def is_ipv4_address(address): 251 | octets = address.split('.') 252 | try: 253 | valid_octets = [x for x in octets if 0 <= int(x) and int(x) <= 255] 254 | return len(valid_octets) == 4 255 | except Exception: 256 | return False 257 | -------------------------------------------------------------------------------- /apps/home/home.py: -------------------------------------------------------------------------------- 1 | ### Author: EMF Badge team 2 | ### Description: Main app 3 | ### Category: Other 4 | ### License: MIT 5 | ### Appname: Home 6 | ### Built-in: hide 7 | 8 | 9 | import ugfx 10 | import pyb 11 | from database import Database 12 | from filesystem import is_file 13 | import buttons 14 | import gc 15 | import apps.home.draw_name 16 | import wifi 17 | from imu import IMU 18 | import onboard 19 | import dialogs 20 | from app import get_local_apps 21 | import sys 22 | import ntp 23 | 24 | def draw_battery(back_colour,percent, win_bv): 25 | percent = max(0,percent) 26 | ugfx.set_default_font("c*") 27 | main_c = ugfx.WHITE #back_colour^0xFFFF 28 | x=3 29 | y=3 30 | win_bv.area(x+35,y,40,19,back_colour) 31 | if percent <= 120: 32 | win_bv.text(x+35,y,str(int(min(percent,100))),main_c) 33 | y += 2 34 | win_bv.area(x,y,30,11,main_c) 35 | win_bv.area(x+30,y+3,3,5,main_c) 36 | 37 | if percent > 120: 38 | win_bv.area(x+2,y+2,26,7,ugfx.YELLOW) 39 | elif percent > 2: 40 | win_bv.area(x+2,y+2,26,7,back_colour) 41 | win_bv.area(x+2,y+2,int(min(percent,100)*26/100),7,main_c) 42 | else: 43 | win_bv.area(x+2,y+2,26,7,ugfx.RED) 44 | 45 | def draw_wifi(back_colour, rssi, connected, connecting, win_wifi): 46 | 47 | x = int((rssi+100)/14) 48 | x = min(5,x) 49 | x = max(1,x) 50 | y = x*4 51 | x = x*5 52 | 53 | outline = [[0,20],[25,20],[25,0]] 54 | outline_rssi = [[0,20],[x,20],[x,20-y]] 55 | 56 | #win_wifi.fill_polygon(0, 0, outline, back_colour^0xFFFF) 57 | 58 | if connected: 59 | win_wifi.fill_polygon(0, 0, outline, ugfx.html_color(0xC4C4C4)) 60 | win_wifi.fill_polygon(0, 0, outline_rssi, ugfx.WHITE) 61 | elif connecting: 62 | win_wifi.fill_polygon(0, 0, outline, ugfx.YELLOW) 63 | else: 64 | win_wifi.fill_polygon(0, 0, outline, ugfx.RED) 65 | 66 | 67 | def draw_time(bg_colour, datetime, win_clock): 68 | win_clock.area(0, 0, win_clock.width(), win_clock.height(), bg_colour) 69 | 70 | digit_width = 9 71 | right_padding = 5 72 | time_text = "%02d:%02d" % (datetime[4], datetime[5]) 73 | start_x = win_clock.width() - (digit_width * len(time_text)) - right_padding 74 | start_y = 5 75 | win_clock.text(start_x, start_y, time_text, ugfx.WHITE) 76 | 77 | 78 | next_tick = 0 79 | tick = True 80 | 81 | def backlight_adjust(): 82 | if ugfx.backlight() == 0: 83 | ugfx.power_mode(ugfx.POWER_ON) 84 | l = pyb.ADC(16).read() 85 | if (l > 90): 86 | ugfx.backlight(100) 87 | elif (l > 20): 88 | ugfx.backlight(70) 89 | else: 90 | ugfx.backlight(30) 91 | 92 | # Finds all locally installed apps that have an external.py 93 | def get_external_hook_paths(): 94 | return ["%s/external" % app.folder_path for app in get_local_apps() if is_file("%s/external.py" % app.folder_path)] 95 | 96 | def low_power(): 97 | ugfx.backlight(0) 98 | ugfx.power_mode(ugfx.POWER_OFF) 99 | 100 | 101 | ugfx.init() 102 | imu=IMU() 103 | neo = pyb.Neopix(pyb.Pin("PB13")) 104 | neo.display(0x04040404) 105 | ledg = pyb.LED(2) 106 | ival = imu.get_acceleration() 107 | if ival['y'] < 0: 108 | ugfx.orientation(0) 109 | else: 110 | ugfx.orientation(180) 111 | 112 | 113 | buttons.init() 114 | if not onboard.is_splash_hidden(): 115 | splashes = ["splash1.bmp"] 116 | for s in splashes: 117 | ugfx.display_image(0,0,s) 118 | delay = 2000 119 | while delay: 120 | delay -= 1 121 | if buttons.is_triggered("BTN_MENU"): 122 | break; 123 | if buttons.is_triggered("BTN_A"): 124 | break; 125 | if buttons.is_triggered("BTN_B"): 126 | break; 127 | if buttons.is_triggered("JOY_CENTER"): 128 | break; 129 | pyb.delay(1) 130 | 131 | 132 | onboard.hide_splash_on_next_boot(False) 133 | 134 | ugfx.set_default_style(dialogs.default_style_badge) 135 | 136 | sty_tb = ugfx.Style(dialogs.default_style_badge) 137 | sty_tb.set_enabled([ugfx.WHITE, ugfx.html_color(0xA66FB0), ugfx.html_color(0x5e5e5e), ugfx.RED]) 138 | sty_tb.set_background(ugfx.html_color(0xA66FB0)) 139 | 140 | orientation = ugfx.orientation() 141 | 142 | with Database() as db: 143 | if not db.get("home_firstrun"): 144 | stats_upload = dialogs.prompt_boolean("""Press menu to see all the available apps and download more.""", title="Welcome to the EMF camp badge!", true_text="A: OK", false_text = None, width = 320, height = 240) 145 | db.set("home_firstrun", True) 146 | db.set("stats_upload", stats_upload) 147 | 148 | def home_main(): 149 | global orientation, next_tick, tick 150 | 151 | ugfx.area(0,0,320,240,sty_tb.background()) 152 | 153 | ugfx.set_default_font(ugfx.FONT_MEDIUM) 154 | win_bv = ugfx.Container(0,0,80,25, style=sty_tb) 155 | win_wifi = ugfx.Container(82,0,60,25, style=sty_tb) 156 | win_name = ugfx.Container(0,25,320,240-25-60, style=dialogs.default_style_badge) 157 | win_text = ugfx.Container(0,240-60,320,60, style=sty_tb) 158 | win_clock = ugfx.Container(250, 0, 70, 25, style=sty_tb) 159 | 160 | windows = [win_bv, win_wifi, win_clock, win_text] 161 | 162 | obj_name = apps.home.draw_name.draw(0,25,win_name) 163 | 164 | buttons.init() 165 | 166 | gc.collect() 167 | ugfx.set_default_font(ugfx.FONT_MEDIUM_BOLD) 168 | hook_feeback = ugfx.List(0, 0, win_text.width(), win_text.height(), parent=win_text) 169 | 170 | win_bv.show() 171 | win_text.show() 172 | win_wifi.show() 173 | win_clock.show() 174 | 175 | # Create external hooks so other apps can run code in the context of 176 | # the home screen. 177 | # To do so an app needs to have an external.py with a tick() function. 178 | # The tick period will default to 60 sec, unless you define something 179 | # else via a "period" variable in the module context (use milliseconds) 180 | # If you set a variable "needs_wifi" in the module context tick() will 181 | # only be called if wifi is available 182 | # If you set a variable "needs_icon" in the module context tick() will 183 | # be called with a reference to a 25x25 pixel ugfx container that you 184 | # can modify 185 | external_hooks = [] 186 | icon_x = 150 187 | for path in get_external_hook_paths(): 188 | try: 189 | module = __import__(path) 190 | if not hasattr(module, "tick"): 191 | raise Exception("%s must have a tick function" % path) 192 | 193 | hook = { 194 | "name": path[5:-9], 195 | "tick": module.tick, 196 | "needs_wifi": hasattr(module, "needs_wifi"), 197 | "period": module.period if hasattr(module, "period") else 60 * 1000, 198 | "next_tick_at": 0 199 | } 200 | 201 | if hasattr(module, "needs_icon"): 202 | hook["icon"] = ugfx.Container(icon_x, 0, 25, 25) 203 | icon_x += 27 204 | 205 | external_hooks.append(hook) 206 | except Exception as e: # Since we dont know what exception we're looking for, we cant do much 207 | print ("%s while parsing background task %s. This task will not run! " % (type(e).__name__, path[5:-9])) 208 | sys.print_exception(e) 209 | continue # If the module fails to load or dies during the setup, skip it 210 | 211 | backlight_adjust() 212 | 213 | inactivity = 0 214 | last_rssi = 0 215 | 216 | ## start connecting to wifi in the background 217 | wifi_timeout = 30 #seconds 218 | wifi_reconnect_timeout = 0 219 | wifi_did_connect = 0 220 | try: 221 | wifi.connect(wait = False, prompt_on_fail = False) 222 | except OSError: 223 | print("Connect failed") 224 | 225 | while True: 226 | pyb.wfi() 227 | ugfx.poll() 228 | 229 | if (next_tick <= pyb.millis()): 230 | tick = True 231 | next_tick = pyb.millis() + 1000 232 | 233 | #if wifi still needs poking 234 | if (wifi_timeout > 0): 235 | if wifi.nic().is_connected(): 236 | wifi_timeout = -1 237 | #wifi is connected, but if becomes disconnected, reconnect after 5 sec 238 | wifi_reconnect_timeout = 5 239 | else: 240 | wifi.nic().update() 241 | 242 | 243 | if tick: 244 | tick = False 245 | 246 | ledg.on() 247 | 248 | if (wifi_timeout > 0): 249 | wifi_timeout -= 1; 250 | 251 | # change screen orientation 252 | ival = imu.get_acceleration() 253 | if ival['y'] < -0.5: 254 | if orientation != 0: 255 | ugfx.orientation(0) 256 | elif ival['y'] > 0.5: 257 | if orientation != 180: 258 | ugfx.orientation(180) 259 | if orientation != ugfx.orientation(): 260 | inactivity = 0 261 | ugfx.area(0,0,320,240,sty_tb.background()) 262 | orientation = ugfx.orientation() 263 | for w in windows: 264 | w.hide(); w.show() 265 | apps.home.draw_name.draw(0,25,win_name) 266 | 267 | 268 | #if wifi timeout has occured and wifi isnt connected in time 269 | if (wifi_timeout == 0) and not (wifi.nic().is_connected()): 270 | print("Giving up on Wifi connect") 271 | wifi_timeout = -1 272 | wifi.nic().disconnect() #give up 273 | wifi_reconnect_timeout = 30 #try again in 30sec 274 | 275 | wifi_is_connected = wifi.nic().is_connected() 276 | 277 | #if not connected, see if we should try again 278 | if not wifi_is_connected: 279 | if wifi_did_connect > 0: 280 | wifi_did_connect = 0 281 | if wifi_reconnect_timeout>0: 282 | wifi_reconnect_timeout -= 1 283 | if wifi_reconnect_timeout == 0: 284 | wifi_timeout = 60 #seconds 285 | wifi.connect(wait = False) 286 | else: 287 | # If we've just connected, set NTP time 288 | if wifi_did_connect == 0: 289 | wifi_did_connect = 1 290 | ntp.set_NTP_time() 291 | 292 | ledg.on() 293 | 294 | # display the wifi logo 295 | rssi = wifi.nic().get_rssi() 296 | if rssi == 0: 297 | rssi = last_rssi 298 | else: 299 | last_rssi = rssi 300 | 301 | time = pyb.RTC().datetime() 302 | if time[0] >= 2016: 303 | draw_time(sty_tb.background(), pyb.RTC().datetime(), win_clock) 304 | 305 | draw_wifi(sty_tb.background(),rssi, wifi_is_connected,wifi_timeout>0,win_wifi) 306 | 307 | battery_percent = onboard.get_battery_percentage() 308 | draw_battery(sty_tb.background(),battery_percent,win_bv) 309 | 310 | inactivity += 1 311 | 312 | # turn off after some period 313 | # takes longer to turn off in the 'used' position 314 | if ugfx.orientation() == 180: 315 | inactivity_limit = 120 316 | else: 317 | inactivity_limit = 30 318 | if battery_percent > 120: #if charger plugged in 319 | if ugfx.backlight() == 0: 320 | ugfx.power_mode(ugfx.POWER_ON) 321 | ugfx.backlight(100) 322 | elif inactivity > inactivity_limit: 323 | low_power() 324 | else: 325 | backlight_adjust() 326 | 327 | ledg.off() 328 | 329 | for hook in external_hooks: 330 | try: 331 | if hook["needs_wifi"] and not wifi.nic().is_connected(): 332 | continue; 333 | 334 | if hook["next_tick_at"] < pyb.millis(): 335 | text = None 336 | if "icon" in hook: 337 | text = hook["tick"](hook["icon"]) 338 | else: 339 | text = hook["tick"]() 340 | hook["next_tick_at"] = pyb.millis() + hook["period"] 341 | if text: 342 | if hook_feeback.count() > 10: 343 | hook_feeback.remove_item(0) 344 | hook_feeback.add_item(text) 345 | if hook_feeback.selected_index() >= (hook_feeback.count()-2): 346 | hook_feeback.selected_index(hook_feeback.count()-1) 347 | except Exception as e: # if anything in the hook fails to work, we need to skip it 348 | print ("%s in %s background task. Not running again until next reboot! " % (type(e).__name__, hook['name'])) 349 | sys.print_exception(e) 350 | external_hooks.remove(hook) 351 | continue 352 | 353 | if buttons.is_pressed("BTN_MENU"): 354 | pyb.delay(20) 355 | break 356 | if buttons.is_pressed("BTN_A"): 357 | inactivity = 0 358 | tick = True 359 | if buttons.is_pressed("BTN_B"): 360 | inactivity = 0 361 | tick = True 362 | 363 | 364 | for hook in external_hooks: 365 | if "icon" in hook: 366 | hook["icon"].destroy() 367 | for w in windows: 368 | w.destroy() 369 | apps.home.draw_name.draw_destroy(obj_name) 370 | win_name.destroy() 371 | hook_feeback.destroy() 372 | if ugfx.backlight() == 0: 373 | ugfx.power_mode(ugfx.POWER_ON) 374 | ugfx.backlight(100) 375 | ugfx.orientation(180) 376 | 377 | #if we havnt connected yet then give up since the periodic function wont be poked 378 | if wifi_timeout >= 0: # not (wifi.nic().is_connected()): 379 | wifi.nic().disconnect() 380 | 381 | while True: 382 | # By separating most of the work in a function we save about 1,6kb memory 383 | home_main() 384 | gc.collect() 385 | execfile("apps/home/quick_launch.py") 386 | --------------------------------------------------------------------------------