diff --git a/micropython/examples/inky_frame/inkylauncher/daily_activity.py b/micropython/examples/inky_frame/inkylauncher/daily_activity.py
new file mode 100644
index 00000000..07e0cbdc
--- /dev/null
+++ b/micropython/examples/inky_frame/inkylauncher/daily_activity.py
@@ -0,0 +1,121 @@
+import gc
+import ujson
+from urllib import urequest
+
+# Length of time between updates in Seconds.
+# Frequent updates will reduce battery life!
+UPDATE_INTERVAL = 2
+
+# API URL
+URL = "https://www.boredapi.com/api/activity"
+
+graphics = None
+text = None
+
+gc.collect()
+
+
+def display_quote(text, ox, oy, scale, wordwrap):
+ # Processing text is memory intensive
+ # so we'll do it one char at a time as we draw to the screen
+ line_height = 8 * scale
+ html = False
+ html_tag = ""
+ word = ""
+ space_width = graphics.measure_text(" ", scale=scale)
+ x = ox
+ y = oy
+ for char in text:
+ if char in "[]":
+ continue
+ if char == "<":
+ html = True
+ html_tag = ""
+ continue
+ if char == ">":
+ html = False
+ continue
+ if html:
+ if char in "/ ":
+ continue
+ html_tag += char
+ continue
+ if char in (" ", "\n") or html_tag == "br":
+ w = graphics.measure_text(word, scale=scale)
+ if x + w > wordwrap or char == "\n" or html_tag == "br":
+ x = ox
+ y += line_height
+
+ graphics.text(word, x, y, scale=scale)
+ word = ""
+ html_tag = ""
+ x += w + space_width
+ continue
+
+ word += char
+
+ # Last word
+ w = graphics.measure_text(word, scale=scale)
+ if x + w > wordwrap:
+ x = ox
+ y += line_height
+
+ graphics.text(word, x, y, scale=scale)
+
+
+def update():
+ global text
+
+ gc.collect()
+
+ # Grab the data
+ socket = urequest.urlopen(URL)
+ j = ujson.load(socket)
+ socket.close()
+
+ text = [j['activity'], j['type'], j['participants']]
+
+ gc.collect()
+
+
+def draw():
+ global text
+
+ WIDTH, HEIGHT = graphics.get_bounds()
+
+ # Clear the screen
+ graphics.set_pen(1)
+ graphics.clear()
+ graphics.set_pen(0)
+
+ # Page lines!
+ graphics.set_pen(3)
+ graphics.line(0, 65, WIDTH, 65)
+ for i in range(2, 13):
+ graphics.line(0, i * 35, WIDTH, i * 35)
+
+ gc.collect()
+
+ # Page margin
+ graphics.set_pen(4)
+ graphics.line(50, 0, 50, HEIGHT)
+ graphics.set_pen(0)
+
+ # Main text
+ graphics.set_font("cursive")
+ graphics.set_pen(4)
+ graphics.set_font("cursive")
+ graphics.text("Activity Idea", 55, 30, WIDTH - 20, 2)
+ graphics.set_pen(0)
+ graphics.set_font("bitmap8")
+ display_quote(text[0], 55, 170, 5, WIDTH - 20)
+
+ gc.collect()
+
+ graphics.set_pen(2)
+ graphics.text("Activity Type: " + text[1], 55, HEIGHT - 45, WIDTH - 20, 2)
+ graphics.text("Participants: " + str(text[2]), 400, HEIGHT - 45, WIDTH - 20, 2)
+
+ graphics.update()
+
+ gc.collect()
diff --git a/micropython/examples/inky_frame/inkylauncher/inky_helper.py b/micropython/examples/inky_frame/inkylauncher/inky_helper.py
new file mode 100644
index 00000000..0a5f5fbc
--- /dev/null
+++ b/micropython/examples/inky_frame/inkylauncher/inky_helper.py
@@ -0,0 +1,175 @@
+from pimoroni_i2c import PimoroniI2C
+from pcf85063a import PCF85063A
+import math
+from machine import Pin, PWM, Timer
+import time
+import inky_frame
+import json
+import network
+import os
+
+# Pin setup for VSYS_HOLD needed to sleep and wake.
+HOLD_VSYS_EN_PIN = 2
+hold_vsys_en_pin = Pin(HOLD_VSYS_EN_PIN, Pin.OUT)
+
+# intialise the pcf85063a real time clock chip
+I2C_SDA_PIN = 4
+I2C_SCL_PIN = 5
+i2c = PimoroniI2C(I2C_SDA_PIN, I2C_SCL_PIN, 100000)
+rtc = PCF85063A(i2c)
+
+# set up the button LEDs
+button_a_led = Pin(11, Pin.OUT)
+button_b_led = Pin(12, Pin.OUT)
+button_c_led = Pin(13, Pin.OUT)
+button_d_led = Pin(14, Pin.OUT)
+button_e_led = Pin(15, Pin.OUT)
+led_warn = Pin(6, Pin.OUT)
+
+# set up for the network LED
+network_led_pwm = PWM(Pin(7))
+network_led_pwm.freq(1000)
+network_led_pwm.duty_u16(0)
+
+
+# set the brightness of the network led
+def network_led(brightness):
+ brightness = max(0, min(100, brightness)) # clamp to range
+ # gamma correct the brightness (gamma 2.8)
+ value = int(pow(brightness / 100.0, 2.8) * 65535.0 + 0.5)
+ network_led_pwm.duty_u16(value)
+
+
+network_led_timer = Timer(-1)
+network_led_pulse_speed_hz = 1
+
+
+def network_led_callback(t):
+ # updates the network led brightness based on a sinusoid seeded by the current time
+ brightness = (math.sin(time.ticks_ms() * math.pi * 2 / (1000 / network_led_pulse_speed_hz)) * 40) + 60
+ value = int(pow(brightness / 100.0, 2.8) * 65535.0 + 0.5)
+ network_led_pwm.duty_u16(value)
+
+
+# set the network led into pulsing mode
+def pulse_network_led(speed_hz=1):
+ global network_led_timer, network_led_pulse_speed_hz
+ network_led_pulse_speed_hz = speed_hz
+ network_led_timer.deinit()
+ network_led_timer.init(period=50, mode=Timer.PERIODIC, callback=network_led_callback)
+
+
+# turn off the network led and disable any pulsing animation that's running
+def stop_network_led():
+ global network_led_timer
+ network_led_timer.deinit()
+ network_led_pwm.duty_u16(0)
+
+
+# returns the id of the button that is currently pressed or
+# None if none are
+def pressed():
+ if inky_frame.button_a.read():
+ return inky_frame.button_a
+ if inky_frame.button_b.read():
+ return inky_frame.button_b
+ if inky_frame.button_c.read():
+ return inky_frame.button_c
+ if inky_frame.button_d.read():
+ return inky_frame.button_d
+ if inky_frame.button_e.read():
+ return inky_frame.button_e
+ return None
+
+
+def sleep(t):
+ # Time to have a little nap until the next update
+ rtc.clear_timer_flag()
+ rtc.set_timer(t, ttp=rtc.TIMER_TICK_1_OVER_60HZ)
+ rtc.enable_timer_interrupt(True)
+
+ # Set the HOLD VSYS pin to an input
+ # this allows the device to go into sleep mode when on battery power.
+ hold_vsys_en_pin.init(Pin.IN)
+
+ # Regular time.sleep for those powering from USB
+ time.sleep(60 * t)
+
+
+# Turns off the button LEDs
+def clear_button_leds():
+ button_a_led.off()
+ button_b_led.off()
+ button_c_led.off()
+ button_d_led.off()
+ button_e_led.off()
+
+
+def network_connect(SSID, PSK):
+ # Enable the Wireless
+ wlan = network.WLAN(network.STA_IF)
+ wlan.active(True)
+
+ # Number of attempts to make before timeout
+ max_wait = 10
+
+ # Sets the Wireless LED pulsing and attempts to connect to your local network.
+ pulse_network_led()
+ wlan.connect(SSID, PSK)
+
+ while max_wait > 0:
+ if wlan.status() < 0 or wlan.status() >= 3:
+ break
+ max_wait -= 1
+ print('waiting for connection...')
+ time.sleep(1)
+
+ stop_network_led()
+ network_led_pwm.duty_u16(30000)
+
+ # Handle connection error. Switches the Warn LED on.
+ if wlan.status() != 3:
+ stop_network_led()
+ led_warn.on()
+
+
+state = {"run": None}
+app = None
+
+
+def file_exists(filename):
+ try:
+ return (os.stat(filename)[0] & 0x4000) == 0
+ except OSError:
+ return False
+
+
+def clear_state():
+ if file_exists("state.json"):
+ os.remove("state.json")
+
+
+def save_state(data):
+ with open("/state.json", "w") as f:
+ f.write(json.dumps(data))
+ f.flush()
+
+
+def load_state():
+ global state
+ data = json.loads(open("/state.json", "r").read())
+ if type(data) is dict:
+ state = data
+
+
+def update_state(running):
+ global state
+ state['run'] = running
+ save_state(state)
+
+
+def launch_app(app_name):
+ global app
+ app = __import__(app_name)
+ print(app)
+ update_state(app_name)
diff --git a/micropython/examples/inky_frame/inkylauncher/lib/logging.mpy b/micropython/examples/inky_frame/inkylauncher/lib/logging.mpy
new file mode 100644
index 00000000..fc00426e
Binary files /dev/null and b/micropython/examples/inky_frame/inkylauncher/lib/logging.mpy differ
diff --git a/micropython/examples/inky_frame/inkylauncher/lib/sdcard.mpy b/micropython/examples/inky_frame/inkylauncher/lib/sdcard.mpy
new file mode 100644
index 00000000..7f3fd6bd
Binary files /dev/null and b/micropython/examples/inky_frame/inkylauncher/lib/sdcard.mpy differ
diff --git a/micropython/examples/inky_frame/inkylauncher/lib/tinyweb/server.mpy b/micropython/examples/inky_frame/inkylauncher/lib/tinyweb/server.mpy
new file mode 100644
index 00000000..a8416347
Binary files /dev/null and b/micropython/examples/inky_frame/inkylauncher/lib/tinyweb/server.mpy differ
diff --git a/micropython/examples/inky_frame/inkylauncher/lib/tinyweb/server.py b/micropython/examples/inky_frame/inkylauncher/lib/tinyweb/server.py
new file mode 100644
index 00000000..c274a072
--- /dev/null
+++ b/micropython/examples/inky_frame/inkylauncher/lib/tinyweb/server.py
@@ -0,0 +1,662 @@
+"""
+Tiny Web - pretty simple and powerful web server for tiny platforms like ESP8266 / ESP32
+MIT license
+(C) Konstantin Belyalov 2017-2018
+"""
+import logging
+import uasyncio as asyncio
+import uasyncio.core
+import ujson as json
+import gc
+import uos as os
+import sys
+import uerrno as errno
+import usocket as socket
+
+
+log = logging.getLogger('WEB')
+
+type_gen = type((lambda: (yield))()) # noqa: E275
+
+# uasyncio v3 is shipped with MicroPython 1.13, and contains some subtle
+# but breaking changes. See also https://github.com/peterhinch/micropython-async/blob/master/v3/README.md
+IS_UASYNCIO_V3 = hasattr(asyncio, "__version__") and asyncio.__version__ >= (3,)
+
+
+def urldecode_plus(s):
+ """Decode urlencoded string (including '+' char).
+ Returns decoded string
+ """
+ s = s.replace('+', ' ')
+ arr = s.split('%')
+ res = arr[0]
+ for it in arr[1:]:
+ if len(it) >= 2:
+ res += chr(int(it[:2], 16)) + it[2:]
+ elif len(it) == 0:
+ res += '%'
+ else:
+ res += it
+ return res
+
+
+def parse_query_string(s):
+ """Parse urlencoded string into dict.
+ Returns dict
+ """
+ res = {}
+ pairs = s.split('&')
+ for p in pairs:
+ vals = [urldecode_plus(x) for x in p.split('=', 1)]
+ if len(vals) == 1:
+ res[vals[0]] = ''
+ else:
+ res[vals[0]] = vals[1]
+ return res
+
+
+class HTTPException(Exception):
+ """HTTP protocol exceptions"""
+
+ def __init__(self, code=400):
+ self.code = code
+
+
+class request:
+ """HTTP Request class"""
+
+ def __init__(self, _reader):
+ self.reader = _reader
+ self.headers = {}
+ self.method = b''
+ self.path = b''
+ self.query_string = b''
+
+ async def read_request_line(self):
+ """Read and parse first line (AKA HTTP Request Line).
+ Function is generator.
+ Request line is something like:
+ GET /something/script?param1=val1 HTTP/1.1
+ """
+ while True:
+ rl = await self.reader.readline()
+ # skip empty lines
+ if rl == b'\r\n' or rl == b'\n':
+ continue
+ break
+ rl_frags = rl.split()
+ if len(rl_frags) != 3:
+ raise HTTPException(400)
+ self.method = rl_frags[0]
+ url_frags = rl_frags[1].split(b'?', 1)
+ self.path = url_frags[0]
+ if len(url_frags) > 1:
+ self.query_string = url_frags[1]
+
+ async def read_headers(self, save_headers=[]):
+ """Read and parse HTTP headers until \r\n\r\n:
+ Optional argument 'save_headers' controls which headers to save.
+ This is done mostly to deal with memory constrains.
+ Function is generator.
+ HTTP headers could be like:
+ Host: google.com
+ Content-Type: blah
+ \r\n
+ """
+ while True:
+ gc.collect()
+ line = await self.reader.readline()
+ if line == b'\r\n':
+ break
+ frags = line.split(b':', 1)
+ if len(frags) != 2:
+ raise HTTPException(400)
+ if frags[0] in save_headers:
+ self.headers[frags[0]] = frags[1].strip()
+
+ async def read_parse_form_data(self):
+ """Read HTTP form data (payload), if any.
+ Function is generator.
+ Returns:
+ - dict of key / value pairs
+ - None in case of no form data present
+ """
+ # TODO: Probably there is better solution how to handle
+ # request body, at least for simple urlencoded forms - by processing
+ # chunks instead of accumulating payload.
+ gc.collect()
+ if b'Content-Length' not in self.headers:
+ return {}
+ # Parse payload depending on content type
+ if b'Content-Type' not in self.headers:
+ # Unknown content type, return unparsed, raw data
+ return {}
+ size = int(self.headers[b'Content-Length'])
+ if size > self.params['max_body_size'] or size < 0:
+ raise HTTPException(413)
+ data = await self.reader.readexactly(size)
+ # Use only string before ';', e.g:
+ # application/x-www-form-urlencoded; charset=UTF-8
+ ct = self.headers[b'Content-Type'].split(b';', 1)[0]
+ try:
+ if ct == b'application/json':
+ return json.loads(data)
+ elif ct == b'application/x-www-form-urlencoded':
+ return parse_query_string(data.decode())
+ except ValueError:
+ # Re-generate exception for malformed form data
+ raise HTTPException(400)
+
+
+class response:
+ """HTTP Response class"""
+
+ def __init__(self, _writer):
+ self.writer = _writer
+ self.send = _writer.awrite
+ self.code = 200
+ self.version = '1.0'
+ self.headers = {}
+
+ async def _send_headers(self):
+ """Compose and send:
+ - HTTP request line
+ - HTTP headers following by \r\n.
+ This function is generator.
+ P.S.
+ Because of usually we have only a few HTTP headers (2-5) it doesn't make sense
+ to send them separately - sometimes it could increase latency.
+ So combining headers together and send them as single "packet".
+ """
+ # Request line
+ hdrs = 'HTTP/{} {} MSG\r\n'.format(self.version, self.code)
+ # Headers
+ for k, v in self.headers.items():
+ hdrs += '{}: {}\r\n'.format(k, v)
+ hdrs += '\r\n'
+ # Collect garbage after small mallocs
+ gc.collect()
+ await self.send(hdrs)
+
+ async def error(self, code, msg=None):
+ """Generate HTTP error response
+ This function is generator.
+ Arguments:
+ code - HTTP response code
+ Example:
+ # Not enough permissions. Send HTTP 403 - Forbidden
+ await resp.error(403)
+ """
+ self.code = code
+ if msg:
+ self.add_header('Content-Length', len(msg))
+ await self._send_headers()
+ if msg:
+ await self.send(msg)
+
+ async def redirect(self, location, msg=None):
+ """Generate HTTP redirect response to 'location'.
+ Basically it will generate HTTP 302 with 'Location' header
+ Arguments:
+ location - URL to redirect to
+ Example:
+ # Redirect to /something
+ await resp.redirect('/something')
+ """
+ self.code = 302
+ self.add_header('Location', location)
+ if msg:
+ self.add_header('Content-Length', len(msg))
+ await self._send_headers()
+ if msg:
+ await self.send(msg)
+
+ def add_header(self, key, value):
+ """Add HTTP response header
+ Arguments:
+ key - header name
+ value - header value
+ Example:
+ resp.add_header('Content-Encoding', 'gzip')
+ """
+ self.headers[key] = value
+
+ def add_access_control_headers(self):
+ """Add Access Control related HTTP response headers.
+ This is required when working with RestApi (JSON requests)
+ """
+ self.add_header('Access-Control-Allow-Origin', self.params['allowed_access_control_origins'])
+ self.add_header('Access-Control-Allow-Methods', self.params['allowed_access_control_methods'])
+ self.add_header('Access-Control-Allow-Headers', self.params['allowed_access_control_headers'])
+
+ async def start_html(self):
+ """Start response with HTML content type.
+ This function is generator.
+ Example:
+ await resp.start_html()
+ await resp.send('
Hello, world!
')
+ """
+ self.add_header('Content-Type', 'text/html')
+ await self._send_headers()
+
+ async def send_file(self, filename, content_type=None, content_encoding=None, max_age=2592000, buf_size=128):
+ """Send local file as HTTP response.
+ This function is generator.
+ Arguments:
+ filename - Name of file which exists in local filesystem
+ Keyword arguments:
+ content_type - Filetype. By default - None means auto-detect.
+ max_age - Cache control. How long browser can keep this file on disk.
+ By default - 30 days
+ Set to 0 - to disable caching.
+ Example 1: Default use case:
+ await resp.send_file('images/cat.jpg')
+ Example 2: Disable caching:
+ await resp.send_file('static/index.html', max_age=0)
+ Example 3: Override content type:
+ await resp.send_file('static/file.bin', content_type='application/octet-stream')
+ """
+ try:
+ # Get file size
+ stat = os.stat(filename)
+ slen = str(stat[6])
+ self.add_header('Content-Length', slen)
+ # Find content type
+ if content_type:
+ self.add_header('Content-Type', content_type)
+ # Add content-encoding, if any
+ if content_encoding:
+ self.add_header('Content-Encoding', content_encoding)
+ # Since this is static content is totally make sense
+ # to tell browser to cache it, however, you can always
+ # override it by setting max_age to zero
+ self.add_header('Cache-Control', 'max-age={}, public'.format(max_age))
+ with open(filename) as f:
+ await self._send_headers()
+ gc.collect()
+ buf = bytearray(min(stat[6], buf_size))
+ while True:
+ size = f.readinto(buf)
+ if size == 0:
+ break
+ await self.send(buf, sz=size)
+ except OSError as e:
+ # special handling for ENOENT / EACCESS
+ if e.args[0] in (errno.ENOENT, errno.EACCES):
+ raise HTTPException(404)
+ else:
+ raise
+
+
+async def restful_resource_handler(req, resp, param=None):
+ """Handler for RESTful API endpoins"""
+ # Gather data - query string, JSON in request body...
+ data = await req.read_parse_form_data()
+ # Add parameters from URI query string as well
+ # This one is actually for simply development of RestAPI
+ if req.query_string != b'':
+ data.update(parse_query_string(req.query_string.decode()))
+ # Call actual handler
+ _handler, _kwargs = req.params['_callmap'][req.method]
+ # Collect garbage before / after handler execution
+ gc.collect()
+ if param:
+ res = _handler(data, param, **_kwargs)
+ else:
+ res = _handler(data, **_kwargs)
+ gc.collect()
+ # Handler result could be:
+ # 1. generator - in case of large payload
+ # 2. string - just string :)
+ # 2. dict - meaning client what tinyweb to convert it to JSON
+ # it can also return error code together with str / dict
+ # res = {'blah': 'blah'}
+ # res = {'blah': 'blah'}, 201
+ if isinstance(res, type_gen):
+ # Result is generator, use chunked response
+ # NOTICE: HTTP 1.0 by itself does not support chunked responses, so, making workaround:
+ # Response is HTTP/1.1 with Connection: close
+ resp.version = '1.1'
+ resp.add_header('Connection', 'close')
+ resp.add_header('Content-Type', 'application/json')
+ resp.add_header('Transfer-Encoding', 'chunked')
+ resp.add_access_control_headers()
+ await resp._send_headers()
+ # Drain generator
+ for chunk in res:
+ chunk_len = len(chunk.encode('utf-8'))
+ await resp.send('{:x}\r\n'.format(chunk_len))
+ await resp.send(chunk)
+ await resp.send('\r\n')
+ gc.collect()
+ await resp.send('0\r\n\r\n')
+ else:
+ if type(res) == tuple:
+ resp.code = res[1]
+ res = res[0]
+ elif res is None:
+ raise Exception('Result expected')
+ # Send response
+ if type(res) is dict:
+ res_str = json.dumps(res)
+ else:
+ res_str = res
+ resp.add_header('Content-Type', 'application/json')
+ resp.add_header('Content-Length', str(len(res_str)))
+ resp.add_access_control_headers()
+ await resp._send_headers()
+ await resp.send(res_str)
+
+
+class webserver:
+
+ def __init__(self, request_timeout=3, max_concurrency=3, backlog=16, debug=False):
+ """Tiny Web Server class.
+ Keyword arguments:
+ request_timeout - Time for client to send complete request
+ after that connection will be closed.
+ max_concurrency - How many connections can be processed concurrently.
+ It is very important to limit this number because of
+ memory constrain.
+ Default value depends on platform
+ backlog - Parameter to socket.listen() function. Defines size of
+ pending to be accepted connections queue.
+ Must be greater than max_concurrency
+ debug - Whether send exception info (text + backtrace)
+ to client together with HTTP 500 or not.
+ """
+ self.loop = asyncio.get_event_loop()
+ self.request_timeout = request_timeout
+ self.max_concurrency = max_concurrency
+ self.backlog = backlog
+ self.debug = debug
+ self.explicit_url_map = {}
+ self.catch_all_handler = None
+ self.parameterized_url_map = {}
+ # Currently opened connections
+ self.conns = {}
+ # Statistics
+ self.processed_connections = 0
+
+ def _find_url_handler(self, req):
+ """Helper to find URL handler.
+ Returns tuple of (function, opts, param) or (None, None) if not found.
+ """
+ # First try - lookup in explicit (non parameterized URLs)
+ if req.path in self.explicit_url_map:
+ return self.explicit_url_map[req.path]
+ # Second try - strip last path segment and lookup in another map
+ idx = req.path.rfind(b'/') + 1
+ path2 = req.path[:idx]
+ if len(path2) > 0 and path2 in self.parameterized_url_map:
+ # Save parameter into request
+ req._param = req.path[idx:].decode()
+ return self.parameterized_url_map[path2]
+
+ if self.catch_all_handler:
+ return self.catch_all_handler
+
+ # No handler found
+ return (None, None)
+
+ async def _handle_request(self, req, resp):
+ await req.read_request_line()
+ # Find URL handler
+ req.handler, req.params = self._find_url_handler(req)
+ if not req.handler:
+ # No URL handler found - read response and issue HTTP 404
+ await req.read_headers()
+ raise HTTPException(404)
+ # req.params = params
+ # req.handler = han
+ resp.params = req.params
+ # Read / parse headers
+ await req.read_headers(req.params['save_headers'])
+
+ async def _handler(self, reader, writer):
+ """Handler for TCP connection with
+ HTTP/1.0 protocol implementation
+ """
+ gc.collect()
+
+ try:
+ req = request(reader)
+ resp = response(writer)
+ # Read HTTP Request with timeout
+ await asyncio.wait_for(self._handle_request(req, resp),
+ self.request_timeout)
+
+ # OPTIONS method is handled automatically
+ if req.method == b'OPTIONS':
+ resp.add_access_control_headers()
+ # Since we support only HTTP 1.0 - it is important
+ # to tell browser that there is no payload expected
+ # otherwise some webkit based browsers (Chrome)
+ # treat this behavior as an error
+ resp.add_header('Content-Length', '0')
+ await resp._send_headers()
+ return
+
+ # Ensure that HTTP method is allowed for this path
+ if req.method not in req.params['methods']:
+ raise HTTPException(405)
+
+ # Handle URL
+ gc.collect()
+ if hasattr(req, '_param'):
+ await req.handler(req, resp, req._param)
+ else:
+ await req.handler(req, resp)
+ # Done here
+ except (asyncio.CancelledError, asyncio.TimeoutError):
+ pass
+ except OSError as e:
+ # Do not send response for connection related errors - too late :)
+ # P.S. code 32 - is possible BROKEN PIPE error (TODO: is it true?)
+ if e.args[0] not in (errno.ECONNABORTED, errno.ECONNRESET, 32):
+ try:
+ await resp.error(500)
+ except Exception as e:
+ log.exc(e, "")
+ except HTTPException as e:
+ try:
+ await resp.error(e.code)
+ except Exception as e:
+ log.exc(e)
+ except Exception as e:
+ # Unhandled expection in user's method
+ log.error(req.path.decode())
+ log.exc(e, "")
+ try:
+ await resp.error(500)
+ # Send exception info if desired
+ if self.debug:
+ sys.print_exception(e, resp.writer.s)
+ except Exception:
+ pass
+ finally:
+ await writer.aclose()
+ # Max concurrency support -
+ # if queue is full schedule resume of TCP server task
+ if len(self.conns) == self.max_concurrency:
+ self.loop.create_task(self._server_coro)
+ # Delete connection, using socket as a key
+ del self.conns[id(writer.s)]
+
+ def add_route(self, url, f, **kwargs):
+ """Add URL to function mapping.
+ Arguments:
+ url - url to map function with
+ f - function to map
+ Keyword arguments:
+ methods - list of allowed methods. Defaults to ['GET', 'POST']
+ save_headers - contains list of HTTP headers to be saved. Case sensitive. Default - empty.
+ max_body_size - Max HTTP body size (e.g. POST form data). Defaults to 1024
+ allowed_access_control_headers - Default value for the same name header. Defaults to *
+ allowed_access_control_origins - Default value for the same name header. Defaults to *
+ """
+ if url == '' or '?' in url:
+ raise ValueError('Invalid URL')
+ # Initial params for route
+ params = {'methods': ['GET'],
+ 'save_headers': [],
+ 'max_body_size': 1024,
+ 'allowed_access_control_headers': '*',
+ 'allowed_access_control_origins': '*',
+ }
+ params.update(kwargs)
+ params['allowed_access_control_methods'] = ', '.join(params['methods'])
+ # Convert methods/headers to bytestring
+ params['methods'] = [x.encode() for x in params['methods']]
+ params['save_headers'] = [x.encode() for x in params['save_headers']]
+ # If URL has a parameter
+ if url.endswith('>'):
+ idx = url.rfind('<')
+ path = url[:idx]
+ idx += 1
+ param = url[idx:-1]
+ if path.encode() in self.parameterized_url_map:
+ raise ValueError('URL exists')
+ params['_param_name'] = param
+ self.parameterized_url_map[path.encode()] = (f, params)
+
+ if url.encode() in self.explicit_url_map:
+ raise ValueError('URL exists')
+ self.explicit_url_map[url.encode()] = (f, params)
+
+ def add_resource(self, cls, url, **kwargs):
+ """Map resource (RestAPI) to URL
+ Arguments:
+ cls - Resource class to map to
+ url - url to map to class
+ kwargs - User defined key args to pass to the handler.
+ Example:
+ class myres():
+ def get(self, data):
+ return {'hello': 'world'}
+ app.add_resource(myres, '/api/myres')
+ """
+ methods = []
+ callmap = {}
+ # Create instance of resource handler, if passed as just class (not instance)
+ try:
+ obj = cls()
+ except TypeError:
+ obj = cls
+ # Get all implemented HTTP methods and make callmap
+ for m in ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']:
+ fn = m.lower()
+ if hasattr(obj, fn):
+ methods.append(m)
+ callmap[m.encode()] = (getattr(obj, fn), kwargs)
+ self.add_route(url, restful_resource_handler,
+ methods=methods,
+ save_headers=['Content-Length', 'Content-Type'],
+ _callmap=callmap)
+
+ def catchall(self):
+ """Decorator for catchall()
+ Example:
+ @app.catchall()
+ def catchall_handler(req, resp):
+ response.code = 404
+ await response.start_html()
+ await response.send('My custom 404!
\n')
+ """
+ params = {'methods': [b'GET'], 'save_headers': [], 'max_body_size': 1024, 'allowed_access_control_headers': '*', 'allowed_access_control_origins': '*'}
+
+ def _route(f):
+ self.catch_all_handler = (f, params)
+ return f
+ return _route
+
+ def route(self, url, **kwargs):
+ """Decorator for add_route()
+ Example:
+ @app.route('/')
+ def index(req, resp):
+ await resp.start_html()
+ await resp.send('Hello, world!
\n')
+ """
+ def _route(f):
+ self.add_route(url, f, **kwargs)
+ return f
+ return _route
+
+ def resource(self, url, method='GET', **kwargs):
+ """Decorator for add_resource() method
+ Examples:
+ @app.resource('/users')
+ def users(data):
+ return {'a': 1}
+ @app.resource('/messages/')
+ async def index(data, topic_id):
+ yield '{'
+ yield '"topic_id": "{}",'.format(topic_id)
+ yield '"message": "test",'
+ yield '}'
+ """
+ def _resource(f):
+ self.add_route(url, restful_resource_handler,
+ methods=[method],
+ save_headers=['Content-Length', 'Content-Type'],
+ _callmap={method.encode(): (f, kwargs)})
+ return f
+ return _resource
+
+ async def _tcp_server(self, host, port, backlog):
+ """TCP Server implementation.
+ Opens socket for accepting connection and
+ creates task for every new accepted connection
+ """
+ addr = socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM)[0][-1]
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ sock.setblocking(False)
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ sock.bind(addr)
+ sock.listen(backlog)
+ try:
+ while True:
+ if IS_UASYNCIO_V3:
+ yield uasyncio.core._io_queue.queue_read(sock)
+ else:
+ yield asyncio.IORead(sock)
+ csock, caddr = sock.accept()
+ csock.setblocking(False)
+ # Start handler / keep it in the map - to be able to
+ # shutdown gracefully - by close all connections
+ self.processed_connections += 1
+ hid = id(csock)
+ handler = self._handler(asyncio.StreamReader(csock),
+ asyncio.StreamWriter(csock, {}))
+ self.conns[hid] = handler
+ self.loop.create_task(handler)
+ # In case of max concurrency reached - temporary pause server:
+ # 1. backlog must be greater than max_concurrency, otherwise
+ # client will got "Connection Reset"
+ # 2. Server task will be resumed whenever one active connection finished
+ if len(self.conns) == self.max_concurrency:
+ # Pause
+ yield False
+ except asyncio.CancelledError:
+ return
+ finally:
+ sock.close()
+
+ def run(self, host="127.0.0.1", port=8081, loop_forever=True):
+ """Run Web Server. By default it runs forever.
+ Keyword arguments:
+ host - host to listen on. By default - localhost (127.0.0.1)
+ port - port to listen on. By default - 8081
+ loop_forever - run loo.loop_forever(), otherwise caller must run it by itself.
+ """
+ self._server_coro = self._tcp_server(host, port, self.backlog)
+ self.loop.create_task(self._server_coro)
+ if loop_forever:
+ self.loop.run_forever()
+
+ def shutdown(self):
+ """Gracefully shutdown Web Server"""
+ asyncio.cancel(self._server_coro)
+ for hid, coro in self.conns.items():
+ asyncio.cancel(coro)
diff --git a/micropython/examples/inky_frame/inkylauncher/lib/urllib/urequest.mpy b/micropython/examples/inky_frame/inkylauncher/lib/urllib/urequest.mpy
new file mode 100644
index 00000000..8cf56fb4
Binary files /dev/null and b/micropython/examples/inky_frame/inkylauncher/lib/urllib/urequest.mpy differ
diff --git a/micropython/examples/inky_frame/inkylauncher/main.py b/micropython/examples/inky_frame/inkylauncher/main.py
new file mode 100644
index 00000000..48e2f8a6
--- /dev/null
+++ b/micropython/examples/inky_frame/inkylauncher/main.py
@@ -0,0 +1,111 @@
+from picographics import PicoGraphics, DISPLAY_INKY_FRAME_4 as DISPLAY # 4.0"
+import gc
+import time
+from machine import reset
+import inky_helper as ih
+
+# Create a secrets.py with your Wifi details to be able to get the time
+#
+# secrets.py should contain:
+# WIFI_SSID = "Your WiFi SSID"
+# WIFI_PASSWORD = "Your WiFi password"
+
+# Setup for the display.
+graphics = PicoGraphics(DISPLAY)
+WIDTH, HEIGHT = graphics.get_bounds()
+graphics.set_font("bitmap8")
+
+
+def launcher():
+ # Draws the menu
+ graphics.set_pen(1)
+ graphics.clear()
+ graphics.set_pen(0)
+
+ graphics.text("Inky Launcher", 5, 5, 350, 5)
+ graphics.text("Menu", 5, 50, 350, 3)
+
+ graphics.text("A. NASA Picture Of the Day", 5, 90, 350, 3)
+ graphics.text("B. Word Clock", 5, 130, 350, 3)
+ graphics.text("C. Daily Activity", 5, 170, 350, 3)
+ graphics.text("D. Headlines", 5, 210, 350, 3)
+ graphics.text("E. Random Joke", 5, 250, 350, 3)
+
+ graphics.update()
+
+ # Now we've drawn the menu to the screen, we wait here for the user to select an app.
+ # Then once an app is selected, we set that as the current app and reset the device and load into it.
+
+ # You can replace any of the included examples with one of your own,
+ # just replace the name of the app in the line "ih.update_last_app("nasa_apod")"
+
+ while True:
+ if ih.inky_frame.button_a.read():
+ ih.button_a_led.on()
+ ih.update_state("nasa_apod")
+ time.sleep(0.5)
+ reset()
+ if ih.inky_frame.button_b.read():
+ ih.button_b_led.on()
+ ih.update_state("word_clock")
+ time.sleep(0.5)
+ reset()
+ if ih.inky_frame.button_c.read():
+ ih.button_c_led.on()
+ ih.update_state("daily_activity")
+ time.sleep(0.5)
+ reset()
+ if ih.inky_frame.button_d.read():
+ ih.button_d_led.on()
+ ih.update_state("news_headlines")
+ time.sleep(0.5)
+ reset()
+ if ih.inky_frame.button_e.read():
+ ih.button_e_led.on()
+ ih.update_state("random_joke")
+ time.sleep(0.5)
+ reset()
+
+
+# Turn any LEDs off that may still be on from last run.
+ih.clear_button_leds()
+ih.led_warn.off()
+
+if ih.pressed() == ih.inky_frame.button_a and ih.pressed() == ih.inky_frame.button_e:
+ launcher()
+
+ih.clear_button_leds()
+
+if ih.file_exists("state.json"):
+ # Loads the JSON and launches the app
+ ih.load_state()
+ ih.launch_app(ih.state['run'])
+
+ # Passes the the graphics object from the launcher to the app
+ ih.app.graphics = graphics
+ ih.app.WIDTH = WIDTH
+ ih.app.HEIGHT = HEIGHT
+
+else:
+ launcher()
+
+try:
+ from secrets import WIFI_SSID, WIFI_PASSWORD
+ ih.network_connect(WIFI_SSID, WIFI_PASSWORD)
+except ImportError:
+ print("Create secrets.py with your WiFi credentials")
+
+# Get some memory back, we really need it!
+gc.collect()
+
+# The main loop executes the update and draw function from the imported app,
+# and then goes to sleep ZzzzZZz
+
+file = ih.file_exists("state.json")
+
+print(file)
+
+while True:
+ ih.app.update()
+ ih.app.draw()
+ ih.sleep(ih.app.UPDATE_INTERVAL)
diff --git a/micropython/examples/inky_frame/inkylauncher/nasa_apod.py b/micropython/examples/inky_frame/inkylauncher/nasa_apod.py
new file mode 100644
index 00000000..4bf93f5a
--- /dev/null
+++ b/micropython/examples/inky_frame/inkylauncher/nasa_apod.py
@@ -0,0 +1,90 @@
+import gc
+import jpegdec
+from urllib import urequest
+from ujson import load
+
+gc.collect()
+
+graphics = None
+
+WIDTH, HEIGHT = 0, 0
+
+FILENAME = "nasa-apod-640x400-daily"
+IMG_URL = "https://pimoroni.github.io/feed2image/nasa-apod-640x400-daily.jpg"
+# A Demo Key is used in this example and is IP rate limited. You can get your own API Key from https://api.nasa.gov/
+API_URL = "https://api.nasa.gov/planetary/apod?api_key=CgQGiTiyzQWEfkPgZ4btNM1FTLZQP5DeSfEwbVr7"
+
+# Length of time between updates in minutes.
+# Frequent updates will reduce battery life!
+UPDATE_INTERVAL = 1
+
+# Variable for storing the NASA APOD Title
+apod_title = None
+
+
+def show_error(text):
+ graphics.set_pen(4)
+ graphics.rectangle(0, 10, 640, 35)
+ graphics.set_pen(1)
+ graphics.text(text, 5, 16, 400, 2)
+
+
+def update():
+ global apod_title
+
+ try:
+ # Grab the data
+ socket = urequest.urlopen(API_URL)
+ gc.collect()
+ j = load(socket)
+ socket.close()
+ apod_title = j['title']
+ gc.collect()
+ except OSError as e:
+ print(e)
+ apod_title = "Image Title Unavailable"
+
+ try:
+ # Grab the image
+ socket = urequest.urlopen(IMG_URL)
+
+ gc.collect()
+
+ data = bytearray(1024)
+ with open(FILENAME, "wb") as f:
+ while True:
+ if socket.readinto(data) == 0:
+ break
+ f.write(data)
+ socket.close()
+ del data
+ gc.collect()
+ except OSError as e:
+ print(e)
+ show_error("Unable to download image")
+
+
+def draw():
+ jpeg = jpegdec.JPEG(graphics)
+ gc.collect() # For good measure...
+
+ graphics.set_pen(1)
+ graphics.clear()
+
+ try:
+ jpeg.open_file(FILENAME)
+ jpeg.decode()
+ except OSError:
+ graphics.set_pen(4)
+ graphics.rectangle(0, 170, 640, 25)
+ graphics.set_pen(1)
+ graphics.text("Unable to display image! :(", 5, 175, 400, 2)
+
+ graphics.set_pen(0)
+ graphics.rectangle(0, 375, 640, 25)
+ graphics.set_pen(1)
+ graphics.text(apod_title, 5, 380, 400, 2)
+
+ gc.collect()
+
+ graphics.update()
diff --git a/micropython/examples/inky_frame/inkylauncher/news_headlines.py b/micropython/examples/inky_frame/inkylauncher/news_headlines.py
new file mode 100644
index 00000000..ea670958
--- /dev/null
+++ b/micropython/examples/inky_frame/inkylauncher/news_headlines.py
@@ -0,0 +1,160 @@
+from urllib import urequest
+import gc
+import qrcode
+
+# Uncomment one URL to use (Top Stories, World News and technology)
+# URL = "http://feeds.bbci.co.uk/news/rss.xml"
+# URL = "http://feeds.bbci.co.uk/news/world/rss.xml"
+URL = "http://feeds.bbci.co.uk/news/technology/rss.xml"
+
+# Length of time between updates in minutes.
+# Frequent updates will reduce battery life!
+UPDATE_INTERVAL = 30
+
+graphics = None
+code = qrcode.QRCode()
+
+
+def read_until(stream, char):
+ result = b""
+ while True:
+ c = stream.read(1)
+ if c == char:
+ return result
+ result += c
+
+
+def discard_until(stream, c):
+ while stream.read(1) != c:
+ pass
+
+
+def parse_xml_stream(s, accept_tags, group_by, max_items=3):
+ tag = []
+ text = b""
+ count = 0
+ current = {}
+ while True:
+ char = s.read(1)
+ if len(char) == 0:
+ break
+
+ if char == b"<":
+ next_char = s.read(1)
+
+ # Discard stuff like ")
+ continue
+
+ # Detect ") # Discard ]>
+ gc.collect()
+
+ elif next_char == b"/":
+ current_tag = read_until(s, b">")
+ top_tag = tag[-1]
+
+ # Populate our result dict
+ if top_tag in accept_tags:
+ current[top_tag.decode("utf-8")] = text.decode("utf-8")
+
+ # If we've found a group of items, yield the dict
+ elif top_tag == group_by:
+ yield current
+ current = {}
+ count += 1
+ if count == max_items:
+ return
+ tag.pop()
+ text = b""
+ gc.collect()
+ continue
+
+ else:
+ current_tag = read_until(s, b">")
+ tag += [next_char + current_tag.split(b" ")[0]]
+ text = b""
+ gc.collect()
+
+ else:
+ text += char
+
+
+def measure_qr_code(size, code):
+ w, h = code.get_size()
+ module_size = int(size / w)
+ return module_size * w, module_size
+
+
+def draw_qr_code(ox, oy, size, code):
+ size, module_size = measure_qr_code(size, code)
+ graphics.set_pen(1)
+ graphics.rectangle(ox, oy, size, size)
+ graphics.set_pen(0)
+ for x in range(size):
+ for y in range(size):
+ if code.get_module(x, y):
+ graphics.rectangle(ox + x * module_size, oy + y * module_size, module_size, module_size)
+
+
+# A function to get the data from an RSS Feed, this in case BBC News.
+def get_rss():
+ try:
+ stream = urequest.urlopen(URL)
+ output = list(parse_xml_stream(stream, [b"title", b"description", b"guid", b"pubDate"], b"item"))
+ return output
+
+ except OSError as e:
+ print(e)
+ return False
+
+
+feed = None
+
+
+def update():
+ global feed
+ # Gets Feed Data
+ feed = get_rss()
+
+
+def draw():
+ global feed
+ WIDTH, HEIGHT = graphics.get_bounds()
+ graphics.set_font("bitmap8")
+
+ # Clear the screen
+ graphics.set_pen(1)
+ graphics.clear()
+ graphics.set_pen(0)
+
+ # Title
+ graphics.text("Headlines from BBC News:", 10, 10, 320, 3)
+
+ # Draws 2 articles from the feed if they're available.
+ if feed:
+ graphics.set_pen(4)
+ graphics.text(feed[0]["title"], 10, 70, WIDTH - 150, 3 if graphics.measure_text(feed[0]["title"]) < 600 else 2)
+ graphics.text(feed[1]["title"], 130, 260, WIDTH - 140, 3 if graphics.measure_text(feed[1]["title"]) < 600 else 2)
+
+ graphics.set_pen(3)
+ graphics.text(feed[0]["description"], 10, 135 if graphics.measure_text(feed[0]["title"]) < 650 else 90, WIDTH - 150, 2)
+ graphics.text(feed[1]["description"], 130, 320 if graphics.measure_text(feed[1]["title"]) < 650 else 230, WIDTH - 145, 2)
+
+ graphics.line(10, 215, 620, 215)
+
+ code.set_text(feed[0]["guid"])
+ draw_qr_code(530, 65, 100, code)
+ code.set_text(feed[1]["guid"])
+ draw_qr_code(10, 265, 100, code)
+
+ else:
+ graphics.set_pen(4)
+ graphics.text("Error: Unable to get feed :(", 10, 40, WIDTH - 150, 4)
+
+ graphics.update()
diff --git a/micropython/examples/inky_frame/inkylauncher/ntp.py b/micropython/examples/inky_frame/inkylauncher/ntp.py
new file mode 100644
index 00000000..f12d2a64
--- /dev/null
+++ b/micropython/examples/inky_frame/inkylauncher/ntp.py
@@ -0,0 +1,32 @@
+import machine
+import time
+import usocket
+import struct
+
+
+def fetch(synch_with_rtc=True, timeout=10):
+ ntp_host = "pool.ntp.org"
+
+ timestamp = None
+ try:
+ query = bytearray(48)
+ query[0] = 0x1b
+ address = usocket.getaddrinfo(ntp_host, 123)[0][-1]
+ socket = usocket.socket(usocket.AF_INET, usocket.SOCK_DGRAM)
+ socket.settimeout(timeout)
+ socket.sendto(query, address)
+ data = socket.recv(48)
+ socket.close()
+ local_epoch = 2208988800 # selected by Chris - blame him. :-D
+ timestamp = struct.unpack("!I", data[40:44])[0] - local_epoch
+ timestamp = time.gmtime(timestamp)
+ except Exception:
+ return None
+
+ # if requested set the machines RTC to the fetched timestamp
+ if synch_with_rtc:
+ machine.RTC().datetime((
+ timestamp[0], timestamp[1], timestamp[2], timestamp[6],
+ timestamp[3], timestamp[4], timestamp[5], 0))
+
+ return timestamp
diff --git a/micropython/examples/inky_frame/inkylauncher/random_joke.py b/micropython/examples/inky_frame/inkylauncher/random_joke.py
new file mode 100644
index 00000000..04f48d4e
--- /dev/null
+++ b/micropython/examples/inky_frame/inkylauncher/random_joke.py
@@ -0,0 +1,80 @@
+import gc
+import random
+import jpegdec
+from urllib import urequest
+
+gc.collect() # We're really gonna need that RAM!
+
+graphics = None
+
+WIDTH = 0
+HEIGHT = 0
+
+FILENAME = "random-joke.jpg"
+
+JOKE_IDS = "https://pimoroni.github.io/feed2image/jokeapi-ids.txt"
+JOKE_IMG = "https://pimoroni.github.io/feed2image/jokeapi-{}-{}x{}.jpg"
+
+gc.collect() # Claw back some RAM!
+
+
+# We don't have the RAM to store the list of Joke IDs in memory.
+# the first line of `jokeapi-ids.txt` is a COUNT of IDs.
+# Grab it, then pick a random line between 0 and COUNT.
+# Seek to that line and ...y'know... there's our totally random joke ID
+
+def update():
+
+ try:
+ socket = urequest.urlopen(JOKE_IDS)
+ except OSError:
+ return
+
+ # Get the first line, which is a count of the joke IDs
+ number_of_lines = int(socket.readline().decode("ascii"))
+ print("Total jokes {}".format(number_of_lines))
+
+ # Pick a random joke (by its line number)
+ line = random.randint(0, number_of_lines)
+ print("Getting ID from line {}".format(line))
+
+ for x in range(line): # Throw away lines to get where we need
+ socket.readline()
+
+ # Read our chosen joke ID!
+ random_joke_id = int(socket.readline().decode("ascii"))
+ socket.close()
+
+ print("Random joke ID: {}".format(random_joke_id))
+
+ url = JOKE_IMG.format(random_joke_id, WIDTH, HEIGHT)
+
+ socket = urequest.urlopen(url)
+
+ # Stream the image data from the socket onto disk in 1024 byte chunks
+ # the 600x448-ish jpeg will be roughly ~24k, we really don't have the RAM!
+ data = bytearray(1024)
+ with open(FILENAME, "wb") as f:
+ while True:
+ if socket.readinto(data) == 0:
+ break
+ f.write(data)
+ socket.close()
+ del data
+ gc.collect() # We really are tight on RAM!
+
+
+def draw():
+ jpeg = jpegdec.JPEG(graphics)
+ gc.collect() # For good measure...
+
+ graphics.set_pen(1)
+ graphics.clear()
+
+ try:
+ jpeg.open_file(FILENAME)
+ jpeg.decode()
+ except OSError:
+ return
+
+ graphics.update()
diff --git a/micropython/examples/inky_frame/inkylauncher/word_clock.py b/micropython/examples/inky_frame/inkylauncher/word_clock.py
new file mode 100644
index 00000000..c7cea80f
--- /dev/null
+++ b/micropython/examples/inky_frame/inkylauncher/word_clock.py
@@ -0,0 +1,80 @@
+import ntp
+import machine
+
+# Length of time between updates in minutes.
+UPDATE_INTERVAL = 15
+graphics = None
+
+rtc = machine.RTC()
+time_string = None
+words = ["it", "dx", "is", "mn", "about", "lve", "quarter", "c", "half", "to", "ou", "past", "n", "one",
+ "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven", "twelve", "r", "O'Clock"]
+
+
+def approx_time(hours, minutes):
+ nums = {0: "twelve", 1: "one", 2: "two",
+ 3: "three", 4: "four", 5: "five", 6: "six",
+ 7: "seven", 8: "eight", 9: "nine", 10: "ten",
+ 11: "eleven", 12: "twelve"}
+
+ if hours == 12:
+ hours = 0
+ if minutes > 0 and minutes < 8:
+ return "it is about " + nums[hours] + " O'Clock"
+ elif minutes >= 8 and minutes < 23:
+ return "it is about quarter past " + nums[hours]
+ elif minutes >= 23 and minutes < 38:
+ return "Tit is about half past " + nums[hours]
+ elif minutes >= 38 and minutes < 53:
+ return "it is about quarter to " + nums[hours + 1]
+ else:
+ return "it is about " + nums[hours + 1] + " O'Clock"
+
+
+def update():
+ global time_string
+ # grab the current time from the ntp server and update the Pico RTC
+ ntp.fetch()
+
+ current_t = rtc.datetime()
+ time_string = approx_time(current_t[4] - 12 if current_t[4] > 12 else current_t[4], current_t[5])
+
+ # Splits the string into an array of words for displaying later
+ time_string = time_string.split()
+
+ print(time_string)
+
+
+def draw():
+ global time_string
+ graphics.set_font("bitmap8")
+
+ WIDTH, HEIGHT = graphics.get_bounds()
+
+ # Clear the screen
+ graphics.set_pen(1)
+ graphics.clear()
+ graphics.set_pen(6)
+
+ x = 10
+ y = 10
+ scale = 5
+ spacing = 2
+
+ for word in words:
+
+ if word in time_string:
+ graphics.set_pen(0)
+ else:
+ graphics.set_pen(7)
+
+ for letter in word:
+ text_length = graphics.measure_text(letter, scale, spacing)
+ if not x + text_length <= WIDTH:
+ y += 70
+ x = 5
+
+ graphics.text(letter.upper(), x, y, 640, scale, spacing)
+ x += 40
+
+ graphics.update()