From 81d9b2ab81d8eba9a622c36934a33685d1ac0260 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Fri, 25 Nov 2022 16:48:03 +0000 Subject: [PATCH] Badger2040W: Add examples and libs. --- .../examples/badger2040w/WIFI_CONFIG.py | 3 + micropython/examples/badger2040w/clock.py | 160 ++++++++++ micropython/examples/badger2040w/launcher.py | 240 ++++++++++++++ .../examples/badger2040w/launchericons.py | 297 ++++++++++++++++++ .../examples/badger2040w/lib/badger2040w.py | 133 ++++++++ .../examples/badger2040w/lib/badger_os.py | 185 +++++++++++ .../badger2040w/lib/network_manager.py | 107 +++++++ .../badger2040w/lib/urllib/urequest.py | 67 ++++ micropython/examples/badger2040w/main.py | 11 +- .../examples/badger2040w/uf2-manifest.txt | 4 +- 10 files changed, 1205 insertions(+), 2 deletions(-) create mode 100644 micropython/examples/badger2040w/WIFI_CONFIG.py create mode 100644 micropython/examples/badger2040w/clock.py create mode 100644 micropython/examples/badger2040w/launcher.py create mode 100644 micropython/examples/badger2040w/launchericons.py create mode 100644 micropython/examples/badger2040w/lib/badger2040w.py create mode 100644 micropython/examples/badger2040w/lib/badger_os.py create mode 100644 micropython/examples/badger2040w/lib/network_manager.py create mode 100644 micropython/examples/badger2040w/lib/urllib/urequest.py diff --git a/micropython/examples/badger2040w/WIFI_CONFIG.py b/micropython/examples/badger2040w/WIFI_CONFIG.py new file mode 100644 index 00000000..76d4a887 --- /dev/null +++ b/micropython/examples/badger2040w/WIFI_CONFIG.py @@ -0,0 +1,3 @@ +SSID = "" +PSK = "" +COUNTRY = "" diff --git a/micropython/examples/badger2040w/clock.py b/micropython/examples/badger2040w/clock.py new file mode 100644 index 00000000..7d52e4c6 --- /dev/null +++ b/micropython/examples/badger2040w/clock.py @@ -0,0 +1,160 @@ +import time +import machine +import ntptime +import badger2040w + + +display = badger2040w.Badger2040W() + +WIDTH, HEIGHT = display.get_bounds() + +display.connect() + +# We're going to keep the badger on, so slow down the system clock if on battery +display.set_update_speed(3) + +ntptime.settime() +rtc = machine.RTC() + +display.set_font("gothic") + +cursors = ["year", "month", "day", "hour", "minute"] +set_clock = False +cursor = 0 +last = 0 + +# Set up the buttons +button_down = machine.Pin(badger2040w.BUTTON_DOWN, machine.Pin.IN, machine.Pin.PULL_DOWN) +button_up = machine.Pin(badger2040w.BUTTON_UP, machine.Pin.IN, machine.Pin.PULL_DOWN) + +button_a = machine.Pin(badger2040w.BUTTON_A, machine.Pin.IN, machine.Pin.PULL_DOWN) +button_b = machine.Pin(badger2040w.BUTTON_B, machine.Pin.IN, machine.Pin.PULL_DOWN) +button_c = machine.Pin(badger2040w.BUTTON_C, machine.Pin.IN, machine.Pin.PULL_DOWN) + + +def days_in_month(month, year): + if month == 2 and ((year % 4 == 0 and year % 100 != 0) or year % 400 == 0): + return 29 + return (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)[month - 1] + + +# Button handling function +def button(pin): + global last, set_clock, cursor, year, month, day, hour, minute + + time.sleep(0.01) + if not pin.value(): + return + + if button_a.value() and button_c.value(): + machine.reset() + + adjust = 0 + changed = False + + if pin == button_b: + set_clock = not set_clock + changed = True + if not set_clock: + rtc.datetime((year, month, day, 0, hour, minute, second, 0)) + + if set_clock: + if pin == button_c: + cursor += 1 + cursor %= len(cursors) + + if pin == button_a: + cursor -= 1 + cursor %= len(cursors) + + if pin == button_up: + adjust = 1 + + if pin == button_down: + adjust = -1 + + if cursors[cursor] == "year": + year += adjust + year = max(year, 2022) + day = min(day, days_in_month(month, year)) + if cursors[cursor] == "month": + month += adjust + month = min(max(month, 1), 12) + day = min(day, days_in_month(month, year)) + if cursors[cursor] == "day": + day += adjust + day = min(max(day, 1), days_in_month(month, year)) + if cursors[cursor] == "hour": + hour += adjust + hour %= 24 + if cursors[cursor] == "minute": + minute += adjust + minute %= 60 + + if set_clock or changed: + draw_clock() + + +# Register the button handling function with the buttons +button_down.irq(trigger=machine.Pin.IRQ_RISING, handler=button) +button_up.irq(trigger=machine.Pin.IRQ_RISING, handler=button) +button_a.irq(trigger=machine.Pin.IRQ_RISING, handler=button) +button_b.irq(trigger=machine.Pin.IRQ_RISING, handler=button) +button_c.irq(trigger=machine.Pin.IRQ_RISING, handler=button) + + +def draw_clock(): + hms = "{:02}:{:02}:{:02}".format(hour, minute, second) + ymd = "{:04}/{:02}/{:02}".format(year, month, day) + + hms_width = display.measure_text(hms, 1.8) + hms_offset = int((WIDTH / 2) - (hms_width / 2)) + h_width = display.measure_text(hms[0:2], 1.8) + mi_width = display.measure_text(hms[3:5], 1.8) + mi_offset = display.measure_text(hms[0:3], 1.8) + + ymd_width = display.measure_text(ymd, 1.0) + ymd_offset = int((WIDTH / 2) - (ymd_width / 2)) + y_width = display.measure_text(ymd[0:4], 1.0) + m_width = display.measure_text(ymd[5:7], 1.0) + m_offset = display.measure_text(ymd[0:5], 1.0) + d_width = display.measure_text(ymd[8:10], 1.0) + d_offset = display.measure_text(ymd[0:8], 1.0) + + display.set_pen(15) + display.clear() + display.set_pen(0) + + display.text(hms, hms_offset, 40, 0, 1.8) + display.text(ymd, ymd_offset, 100, 0, 1.0) + + if set_clock: + if cursors[cursor] == "year": + display.line(ymd_offset, 120, ymd_offset + y_width, 120) + if cursors[cursor] == "month": + display.line(ymd_offset + m_offset, 120, ymd_offset + m_offset + m_width, 120) + if cursors[cursor] == "day": + display.line(ymd_offset + d_offset, 120, ymd_offset + d_offset + d_width, 120) + + if cursors[cursor] == "hour": + display.line(hms_offset, 70, hms_offset + h_width, 70) + if cursors[cursor] == "minute": + display.line(hms_offset + mi_offset, 70, hms_offset + mi_offset + mi_width, 70) + + display.update() + + +year, month, day, wd, hour, minute, second, _ = rtc.datetime() + +if (year, month, day) == (2021, 1, 1): + rtc.datetime((2022, 2, 28, 0, 12, 0, 0, 0)) + +last_second = second + +while True: + if not set_clock: + year, month, day, wd, hour, minute, second, _ = rtc.datetime() + if second != last_second: + draw_clock() + last_second = second + time.sleep(0.01) diff --git a/micropython/examples/badger2040w/launcher.py b/micropython/examples/badger2040w/launcher.py new file mode 100644 index 00000000..ea28ef02 --- /dev/null +++ b/micropython/examples/badger2040w/launcher.py @@ -0,0 +1,240 @@ +import gc +import time +import math +import badger2040w as badger2040 +import launchericons +import badger_os + +# Reduce clock speed to 48MHz +badger2040.system_speed(badger2040.SYSTEM_NORMAL) + +changed = False +exited_to_launcher = False +woken_by_button = badger2040.woken_by_button() # Must be done before we clear_pressed_to_wake + +if badger2040.pressed_to_wake(badger2040.BUTTON_A) and badger2040.pressed_to_wake(badger2040.BUTTON_C): + # Pressing A and C together at start quits app + exited_to_launcher = badger_os.state_clear_running() +else: + # Otherwise restore previously running app + badger_os.state_launch() + +# for e.g. 2xAAA batteries, try max 3.4 min 3.0 +MAX_BATTERY_VOLTAGE = 4.0 +MIN_BATTERY_VOLTAGE = 3.2 + +display = badger2040.Badger2040W() +display.set_font("bitmap8") +display.led(128) + +state = { + "page": 0, + "font_size": 1, + "inverted": False, + "running": "launcher" +} + +badger_os.state_load("launcher", state) + +display.invert(state["inverted"]) + +icons = bytearray(launchericons.data()) +icons_width = 576 + +examples = [ + ("_clock", 0), + ("_fonts", 1), + ("_ebook", 2), + ("_image", 3), + ("_list", 4), + ("_badge", 5), + ("_qrgen", 8), + ("_info", 6), + ("_help", 7), +] + +font_sizes = (0.5, 0.7, 0.9) + +# Approximate center lines for buttons A, B and C +centers = (41, 147, 253) + +MAX_PAGE = math.ceil(len(examples) / 3) + +WIDTH = 296 + + +def map_value(input, in_min, in_max, out_min, out_max): + return (((input - in_min) * (out_max - out_min)) / (in_max - in_min)) + out_min + + +def draw_battery(level, x, y): + # Outline + display.set_pen(15) + display.rectangle(x, y, 19, 10) + # Terminal + display.rectangle(x + 19, y + 3, 2, 4) + display.set_pen(0) + display.rectangle(x + 1, y + 1, 17, 8) + if level < 1: + display.set_pen(0) + display.line(x + 3, y, x + 3 + 10, y + 10) + display.line(x + 3 + 1, y, x + 3 + 11, y + 10) + display.set_pen(15) + display.line(x + 2 + 2, y - 1, x + 4 + 12, y + 11) + display.line(x + 2 + 3, y - 1, x + 4 + 13, y + 11) + return + # Battery Bars + display.set_pen(15) + for i in range(4): + if level / 4 > (1.0 * i) / 4: + display.rectangle(i * 4 + x + 2, y + 2, 3, 6) + + +def draw_disk_usage(x): + _, f_used, _ = badger_os.get_disk_usage() + + display.set_pen(15) + display.image( + bytearray( + ( + 0b00000000, + 0b00111100, + 0b00111100, + 0b00111100, + 0b00111000, + 0b00000000, + 0b00000000, + 0b00000001, + ) + ), + 8, + 8, + x, + 4, + ) + display.rectangle(x + 10, 3, 80, 10) + display.set_pen(0) + display.rectangle(x + 11, 4, 78, 8) + display.set_pen(15) + display.rectangle(x + 12, 5, int(76 / 100.0 * f_used), 6) + display.text("{:.2f}%".format(f_used), x + 91, 4, WIDTH, 1.0) + + +def render(): + display.set_pen(15) + display.clear() + display.set_pen(0) + + max_icons = min(3, len(examples[(state["page"] * 3):])) + + for i in range(max_icons): + x = centers[i] + label, icon = examples[i + (state["page"] * 3)] + label = label[1:].replace("_", " ") + display.set_pen(0) + display.icon(icons, icon, icons_width, 64, x - 32, 24) + w = display.measure_text(label, font_sizes[state["font_size"]]) + display.text(label, int(x - (w / 2)), 16 + 80, WIDTH, font_sizes[state["font_size"]]) + + for i in range(MAX_PAGE): + x = 286 + y = int((128 / 2) - (MAX_PAGE * 10 / 2) + (i * 10)) + display.set_pen(0) + display.rectangle(x, y, 8, 8) + if state["page"] != i: + display.set_pen(15) + display.rectangle(x + 1, y + 1, 6, 6) + + display.set_pen(0) + display.rectangle(0, 0, WIDTH, 16) + draw_disk_usage(90) + vbat = badger_os.get_battery_level() + bat = int(map_value(vbat, MIN_BATTERY_VOLTAGE, MAX_BATTERY_VOLTAGE, 0, 4)) + draw_battery(bat, WIDTH - 22 - 3, 3) + display.set_pen(15) + display.text("badgerOS", 4, 4, WIDTH, 1.0) + + display.update() + + +def wait_for_user_to_release_buttons(): + pr = display.pressed + while pr(badger2040.BUTTON_A) or pr(badger2040.BUTTON_B) or pr(badger2040.BUTTON_C) or pr(badger2040.BUTTON_UP) or pr(badger2040.BUTTON_DOWN): + time.sleep(0.01) + + +def launch_example(index): + wait_for_user_to_release_buttons() + + file = examples[(state["page"] * 3) + index][0] + + for k in locals().keys(): + if k not in ("gc", "file", "badger_os"): + del locals()[k] + + gc.collect() + + badger_os.launch(file) + + +def button(pin): + global changed + changed = True + + if not display.pressed(badger2040.BUTTON_USER): # User button is NOT held down + if pin == badger2040.BUTTON_A: + launch_example(0) + if pin == badger2040.BUTTON_B: + launch_example(1) + if pin == badger2040.BUTTON_C: + launch_example(2) + if pin == badger2040.BUTTON_UP: + if state["page"] > 0: + state["page"] -= 1 + render() + if pin == badger2040.BUTTON_DOWN: + if state["page"] < MAX_PAGE - 1: + state["page"] += 1 + render() + else: # User button IS held down + if pin == badger2040.BUTTON_UP: + state["font_size"] += 1 + if state["font_size"] == len(font_sizes): + state["font_size"] = 0 + render() + if pin == badger2040.BUTTON_DOWN: + state["font_size"] -= 1 + if state["font_size"] < 0: + state["font_size"] = 0 + render() + if pin == badger2040.BUTTON_A: + state["inverted"] = not state["inverted"] + display.invert(state["inverted"]) + render() + + +if exited_to_launcher or not woken_by_button: + wait_for_user_to_release_buttons() + display.set_update_speed(badger2040.UPDATE_MEDIUM) + render() + +display.set_update_speed(badger2040.UPDATE_FAST) + +while True: + if display.pressed(badger2040.BUTTON_A): + button(badger2040.BUTTON_A) + if display.pressed(badger2040.BUTTON_B): + button(badger2040.BUTTON_B) + if display.pressed(badger2040.BUTTON_C): + button(badger2040.BUTTON_C) + + if display.pressed(badger2040.BUTTON_UP): + button(badger2040.BUTTON_UP) + if display.pressed(badger2040.BUTTON_DOWN): + button(badger2040.BUTTON_DOWN) + + if changed: + badger_os.state_save("launcher", state) + changed = False + + display.halt() diff --git a/micropython/examples/badger2040w/launchericons.py b/micropython/examples/badger2040w/launchericons.py new file mode 100644 index 00000000..831e6884 --- /dev/null +++ b/micropython/examples/badger2040w/launchericons.py @@ -0,0 +1,297 @@ +# Code generated by convert.py. + +_data =\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x3f\xf0\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x0f\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x01\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x3f\xfc\x00\x00\x00'\ +b'\x00\x00\x00\x0f\xfc\x00\x00\x00\x00\x00\x00\x1f\xf8\x00\x00\x00'\ +b'\x03\xff\xf8\x61\x98\x1f\xff\xc0\x00\x00\x0f\xff\xff\xc0\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x7f\xfe\x00\x00\x00\x00\x00\x00\x7f\xff\x80\x00\x00'\ +b'\x00\x00\x00\xff\xff\x00\x00\x00\x03\xff\xf8\x61\x98\x1f\xff\xc0'\ +b'\x00\x00\x3f\xff\xff\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\xff\xff\xff\xff\x80\x00'\ +b'\x00\x1f\xff\xff\xff\xff\xe0\x00\x00\x00\x00\x7f\xfe\x00\x00\x00'\ +b'\x00\x00\x03\xff\xff\xf0\x00\x00\x00\x00\x07\xff\xff\xe0\x00\x00'\ +b'\x03\x00\x18\x01\xf9\x98\x00\xc0\x00\x00\x7f\xff\xff\xf8\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x3f\xff\xff\xff\xff\xf0\x00\x00\xff\xff\xff\xff\xff\xfc\x00'\ +b'\x00\x00\x00\xff\xff\x00\x00\x00\x00\x00\x0f\xff\xff\xfc\x00\x00'\ +b'\x00\x00\x1f\xff\xff\xf8\x00\x00\x03\x00\x18\x01\xf9\x98\x00\xc0'\ +b'\x00\x01\xff\xc0\x0f\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xff\xff\xff\xff\xf8\x00'\ +b'\x01\xff\xff\xff\xff\xff\xfe\x00\x00\x00\x00\xfc\x3f\x00\x00\x00'\ +b'\x00\x00\x1f\xff\xff\xfe\x00\x00\x00\x00\x3f\xff\xff\xfc\x00\x00'\ +b'\x03\x3f\x18\x60\x18\x18\xfc\xc0\x00\x03\xfe\x00\x01\xff\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x7f\xff\xff\xff\xff\xf8\x00\x03\xff\xff\xff\xff\xff\xff\x00'\ +b'\x00\x00\x00\xfc\x3f\x00\x00\x00\x00\x00\x7f\xf0\x03\xff\x80\x00'\ +b'\x00\x00\xff\xe0\x07\xff\x00\x00\x03\x3f\x18\x60\x18\x18\xfc\xc0'\ +b'\x00\x07\xf8\x00\x00\x7f\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xff\xff\xff\xff\xf8\x00'\ +b'\x03\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\xfc\x3f\x00\x00\x00'\ +b'\x00\x00\xff\x80\x00\x7f\xc0\x00\x00\x01\xff\x00\x00\xff\x80\x00'\ +b'\x03\x3f\x19\x98\x61\x98\xfc\xc0\x00\x0f\xf0\x00\x00\x3f\xc0\x00'\ +b'\x00\x00\x03\xff\xff\xff\xfe\x00\x00\x03\xff\x80\x03\xfe\x00\x00'\ +b'\x00\xff\xff\xff\xff\xff\xfc\x00\x03\xff\xff\xff\xff\xff\xff\x00'\ +b'\x00\x00\x00\xfc\x3f\x00\x00\x00\x00\x01\xfe\x00\x00\x1f\xe0\x00'\ +b'\x00\x03\xfc\x00\x00\x3f\xc0\x00\x03\x3f\x19\x98\x61\x98\xfc\xc0'\ +b'\x00\x0f\xc0\x00\x00\x0f\xc0\x00\x00\x00\x03\xff\xff\xff\xfe\x00'\ +b'\x00\x3f\xff\xf0\x1f\xff\xf0\x00\x00\xfc\x00\x00\x00\x00\xfc\x00'\ +b'\x03\xe0\x00\x00\x00\x00\x1f\x00\x00\x00\x00\xfc\x3f\x00\x00\x00'\ +b'\x00\x03\xfc\x00\x00\x0f\xf0\x00\x00\x07\xf8\x00\x00\x1f\xe0\x00'\ +b'\x03\x3f\x19\x87\xe1\x98\xfc\xc0\x00\x1f\x80\x00\x00\x07\xe0\x00'\ +b'\x00\x00\x03\xff\xff\xff\xfe\x00\x00\xff\xff\xfe\x3f\xff\xfc\x00'\ +b'\x00\xfc\x00\x00\x00\x00\xfc\x00\x03\xe0\x00\x00\x00\x00\x1f\x00'\ +b'\x00\x00\x00\xfc\x3f\x00\x00\x00\x00\x03\xf0\x00\x60\x03\xf0\x00'\ +b'\x00\x07\xe0\x00\x00\x07\xe0\x00\x03\x3f\x19\x87\xe1\x98\xfc\xc0'\ +b'\x00\x3f\x00\x00\x00\x03\xf0\x00\x00\x00\x03\xff\xff\xff\xfe\x00'\ +b'\x03\xff\xff\xff\xff\xff\xff\x00\x00\xfc\x00\x00\x00\x00\xfc\x00'\ +b'\x03\xe0\x00\x00\x00\x00\x1f\x00\x00\x00\x00\xfc\x3f\x00\x00\x00'\ +b'\x00\x07\xe0\x01\xf8\x01\xf8\x00\x00\x0f\xc0\x00\x00\x03\xf0\x00'\ +b'\x03\x00\x18\x79\x98\x18\x00\xc0\x00\x7e\x00\x07\x00\x01\xf8\x00'\ +b'\x00\x00\x03\xff\xff\xff\xfe\x00\x03\xff\xff\xff\xff\xff\xff\x80'\ +b'\x00\xfc\x00\x00\x00\x00\xfc\x00\x03\xe0\x00\x00\x00\x00\x1f\x00'\ +b'\x00\x0f\xff\xfc\x3f\xff\xf0\x00\x00\x0f\xc0\x01\xf8\x00\xfc\x00'\ +b'\x00\x1f\x80\x0f\xe0\x01\xf8\x00\x03\x00\x18\x79\x98\x18\x00\xc0'\ +b'\x00\x7e\x00\x07\x00\x01\xf8\x00\x00\x00\x03\xff\xff\xff\xfe\x00'\ +b'\x07\xfe\x00\xff\xff\x03\xff\x80\x00\xfc\x00\x00\x00\x00\xfc\x00'\ +b'\x03\xe3\xff\xc0\x00\x00\x1f\x00\x00\x7f\xff\xfc\x3f\xff\xfe\x00'\ +b'\x00\x1f\x80\x01\xfc\x00\x7e\x00\x00\x3f\x00\x3f\xf0\x00\xfc\x00'\ +b'\x03\x00\x18\x79\x98\x18\x00\xc0\x00\xfc\x00\x07\x00\x00\xfc\x00'\ +b'\x00\x00\x03\xff\xff\xff\xfe\x00\x07\xe0\x00\x1f\xf0\x00\x1f\x80'\ +b'\x00\xfc\x00\x00\x00\x00\xfc\x00\x03\xe7\xff\xc0\x00\x00\x1f\x00'\ +b'\x00\xff\xff\xfc\x3f\xff\xff\x00\x00\x1f\x80\x01\xf8\x00\x7e\x00'\ +b'\x00\x3f\x00\x7f\xf8\x00\xfc\x00\x03\xff\xf9\x99\x99\x9f\xff\xc0'\ +b'\x00\xf8\x00\x07\x00\x00\x7c\x00\x00\x00\x00\x00\x3f\xe0\x00\x00'\ +b'\x07\xe0\x00\x0f\xc0\x00\x1f\x80\x00\xfc\x00\x00\x00\x00\xfc\x00'\ +b'\x03\xe7\xff\xc0\x00\x00\x1f\x00\x01\xff\xff\xfc\x3f\xff\xff\x80'\ +b'\x00\x3f\x00\x01\xf8\x00\x3f\x00\x00\x7e\x00\xff\xfc\x00\x7e\x00'\ +b'\x03\xff\xf9\x99\x99\x9f\xff\xc0\x00\xf8\x00\x07\x00\x00\x7c\x00'\ +b'\x00\x00\x00\x00\x1f\xe0\x00\x00\x07\xe0\x00\x0f\x80\x00\x1f\x80'\ +b'\x00\xfc\x00\x00\x00\x00\xfc\x00\x03\xe7\xff\xc0\x00\x00\x1f\x00'\ +b'\x01\xff\xff\xfc\x3f\xff\xff\x80\x00\x3e\x00\x00\xf0\x00\x1f\x00'\ +b'\x00\x7c\x01\xfc\xfe\x00\x3e\x00\x00\x00\x00\x61\x9f\x80\x00\x00'\ +b'\x01\xf0\x00\x07\x00\x00\x3e\x00\x00\x00\x00\x00\x1f\xe0\x00\x00'\ +b'\x07\xe0\x00\x0f\x80\x00\x1f\x80\x00\xfc\x00\x00\x00\x00\xfc\x00'\ +b'\x03\xe7\xff\xc0\x00\x00\x1f\x00\x01\xf0\x00\xff\xff\x00\x0f\x80'\ +b'\x00\x3e\x00\x00\x00\x00\x1f\x00\x00\x7c\x03\xf0\x3f\x00\x3e\x00'\ +b'\x00\x00\x00\x61\x9f\x80\x00\x00\x01\xf0\x00\x07\x00\x00\x3e\x00'\ +b'\x00\x00\x00\x00\x1f\xe0\x00\x00\x07\xe0\x00\x0f\x80\x00\x1f\x80'\ +b'\x00\xfc\x00\x00\x00\x00\xfc\x00\x03\xe0\x00\x00\x00\x00\x1f\x00'\ +b'\x01\xf0\x00\x7f\xfe\x00\x0f\x80\x00\x7c\x00\x00\x00\x00\x0f\x80'\ +b'\x00\xf8\x03\xe0\x1f\x00\x1f\x00\x03\xc0\xfe\x78\x60\x00\xf0\x00'\ +b'\x01\xf0\x00\x07\x00\x00\x3e\x00\x00\x00\x00\x00\x1f\xe0\x00\x00'\ +b'\x07\xe0\x00\x0f\x80\x00\x1f\x80\x00\xfc\x00\x00\x00\x00\xfc\x00'\ +b'\x03\xe0\x00\x00\x00\x06\x1f\x00\x01\xf0\x00\x7f\xfe\x00\x0f\x80'\ +b'\x00\x7c\x00\x3f\x80\x00\x0f\x80\x00\xf8\x03\xc0\x0f\x00\x1f\x00'\ +b'\x03\xc0\xfe\x78\x60\x00\xf0\x00\x01\xe0\x00\x07\x00\x00\x1e\x00'\ +b'\x00\xff\xff\xfc\x1f\xe0\x00\x00\x07\xe0\x00\x0f\x80\x00\x1f\x80'\ +b'\x00\xfc\x00\x00\x00\x00\xfc\x00\x03\xe0\x00\x00\x00\x0f\x1f\x00'\ +b'\x01\xf0\x00\x3f\xfc\x00\x0f\x80\x00\x7c\x00\x7f\xe0\x00\x0f\x80'\ +b'\x00\xf8\x03\xc0\x0f\x80\x1f\x00\x03\xff\x00\x1f\xff\x87\xff\x00'\ +b'\x03\xe0\x00\x07\x00\x00\x1f\x00\x01\xff\xff\xfc\x1f\xe0\x00\x00'\ +b'\x07\xe0\x00\x0f\x80\x00\x1f\x80\x00\xfc\x00\x00\x00\x00\xfc\x00'\ +b'\x03\xe0\x00\x00\x00\x1f\x9f\x00\x01\xf0\x00\x1f\xf8\x00\x0f\x80'\ +b'\x00\x78\x00\xff\xf0\x00\x07\x80\x00\xf0\x03\xc0\x0f\x80\x0f\x00'\ +b'\x03\xff\x00\x1f\xff\x87\xff\x00\x03\xe0\x00\x07\x00\x00\x1f\x00'\ +b'\x01\xff\xff\xfc\x1f\xe0\x00\x00\x07\xe0\x00\x0f\x80\x00\x1f\x80'\ +b'\x00\xfc\x00\x00\x00\x00\xfc\x00\x03\xe0\x00\x00\x00\x3f\x9f\x00'\ +b'\x01\xf0\x00\x07\xe0\x00\x0f\x80\x00\xf8\x00\x7f\xf0\x00\x07\xc0'\ +b'\x01\xf0\x03\xc0\x0f\x80\x0f\x80\x03\xcf\xf8\x61\xff\xe7\x33\xc0'\ +b'\x03\xe0\x00\x07\x00\x00\x1f\x00\x01\xff\xff\xfc\x1f\xe0\x00\x00'\ +b'\x07\xe0\x00\x0f\x80\x00\x1f\x80\x00\xfc\x00\x00\x00\x00\xfc\x00'\ +b'\x03\xe0\x00\x00\x20\x7f\x1f\x00\x01\xf0\x00\x00\x00\x00\x0f\x80'\ +b'\x00\xf8\x00\x7f\xf0\x00\x07\xc0\x01\xf0\x00\x00\x1f\x00\x0f\x80'\ +b'\x03\xcf\xf8\x61\xff\xe7\x33\xc0\x03\xe0\x00\x07\x00\x00\x1f\x00'\ +b'\x01\xff\xff\xfc\x1f\xe0\x00\x00\x07\xe0\x00\x0f\x80\x00\x1f\x80'\ +b'\x00\xfc\x00\x00\x00\x00\xfc\x00\x03\xe7\xff\xc0\x70\xfe\x1f\x00'\ +b'\x01\xf0\x00\x00\x00\x00\x0f\x80\x00\xf8\x00\x07\xf0\x00\x07\xc0'\ +b'\x01\xf0\x00\x00\x7f\x00\x0f\x80\x00\x33\xe1\x9f\x98\x78\xf0\xc0'\ +b'\x03\xe0\x00\x07\x00\x00\x1f\x00\x01\xff\xff\xfc\x1f\xe0\x00\x00'\ +b'\x07\xe0\x00\x0f\x80\x00\x1f\x80\x00\xfc\x00\x00\x00\x00\xfc\x00'\ +b'\x03\xe7\xff\xc0\xf9\xfc\x1f\x00\x01\xf0\x01\xc0\x07\xfe\x0f\x80'\ +b'\x00\xf8\x00\x07\xf0\x00\x07\xc0\x01\xf0\x00\x00\xfe\x00\x0f\x80'\ +b'\x00\x33\xe1\x9f\x98\x78\xf0\xc0\x03\xe0\x00\x07\xc0\x00\x1f\x00'\ +b'\x01\xff\xff\xfc\x1f\xe0\x00\x00\x07\xe0\x00\x0f\x80\x00\x1f\x80'\ +b'\x00\xfc\x00\x00\x08\x00\xfc\x00\x03\xe7\xff\xc1\xff\xf8\x1f\x00'\ +b'\x01\xf0\x03\xe0\x07\xfe\x0f\x80\x00\xf8\x00\x07\xf0\x00\x07\xc0'\ +b'\x01\xf0\x00\x03\xfe\x00\x0f\x80\x03\x00\x19\xfe\x01\x9f\x00\xc0'\ +b'\x03\xe0\x00\x07\xe0\x00\x1f\x00\x01\xff\xff\xfc\x1f\xe0\x00\x00'\ +b'\x07\xe0\x00\x0f\x80\x00\x1f\x80\x00\xfc\x00\x00\x1c\x00\xfc\x00'\ +b'\x03\xe7\xff\xc0\xff\xf0\x1f\x00\x01\xf0\x07\xf0\x07\xfe\x0f\x80'\ +b'\x00\xf8\x00\x0f\xe0\x00\x07\xc0\x01\xf0\x00\x03\xf8\x00\x0f\x80'\ +b'\x03\x00\x19\xfe\x01\x9f\x00\xc0\x03\xe0\x00\x03\xf8\x00\x1f\x00'\ +b'\x00\x01\xfe\x00\x1f\xe0\x00\x00\x07\xe0\x00\x0f\x80\x00\x1f\x80'\ +b'\x00\xfc\x00\x00\x3e\x00\xfc\x00\x03\xe0\x00\x00\x7f\xe0\x1f\x00'\ +b'\x01\xf0\x07\xf0\x07\xfe\x0f\x80\x00\xf8\x00\x0f\xe0\x00\x07\xc0'\ +b'\x01\xf0\x00\x07\xf0\x00\x0f\x80\x03\xcc\x07\x9f\x87\x87\x03\x00'\ +b'\x01\xe0\x00\x00\xfe\x00\x1e\x00\x00\x01\xfe\x00\x1f\xe0\x00\x00'\ +b'\x07\xe0\x00\x0f\x80\x00\x1f\x80\x00\xfc\x01\x00\x7e\x00\xfc\x00'\ +b'\x03\xe0\x00\x00\x3f\xc0\x1f\x00\x01\xf0\x07\xf0\x00\x00\x0f\x80'\ +b'\x00\xf8\x00\x0f\xe0\x00\x07\xc0\x01\xf0\x00\x07\xc0\x00\x0f\x80'\ +b'\x03\xcc\x07\x9f\x87\x87\x03\x00\x01\xf0\x00\x00\x3f\x00\x3e\x00'\ +b'\x00\x01\xfe\x00\x1f\xe0\x00\x00\x07\xe0\x00\x0f\x80\x00\x1f\x80'\ +b'\x00\xfc\x03\x80\x7f\x00\xfc\x00\x03\xe0\x00\x00\x1f\x80\x1f\x00'\ +b'\x01\xf0\x07\xf0\x00\x00\x0f\x80\x00\x78\x00\x0f\xe0\x00\x07\x80'\ +b'\x00\xf0\x00\x0f\x80\x00\x0f\x00\x03\x3c\xfe\x19\xe6\x60\x33\xc0'\ +b'\x01\xf0\x00\x00\x1f\xc0\x3e\x00\x00\x01\xfe\x00\x1f\xe0\x00\x00'\ +b'\x07\xe0\x00\x0f\x80\x00\x1f\x80\x00\xfc\x07\xc0\xff\x80\xfc\x00'\ +b'\x03\xe0\x00\x00\x0f\x00\x1f\x00\x01\xf0\x03\xe0\x00\x00\x0f\x80'\ +b'\x00\x7c\x00\x0f\xe0\x00\x0f\x80\x00\xf8\x00\x0f\x80\x00\x1f\x00'\ +b'\x03\x3c\xfe\x19\xe6\x60\x33\xc0\x01\xf0\x00\x00\x07\xe0\x3e\x00'\ +b'\x00\x01\xfe\x00\x1f\xe0\x00\x00\x07\xe0\x00\x0f\x80\x00\x1f\x80'\ +b'\x00\xfc\x07\xe1\xff\xc0\xfc\x00\x03\xe0\x00\x00\x06\x00\x1f\x00'\ +b'\x01\xf0\x00\x00\x07\xfe\x0f\x80\x00\x7c\x00\x1f\xe0\x00\x0f\x80'\ +b'\x00\xf8\x00\x0f\x80\x00\x1f\x00\x03\x0f\xe1\x87\x9e\x60\xcc\xc0'\ +b'\x00\xf8\x00\x00\x01\xe0\x7c\x00\x00\x01\xfe\x00\x1f\xe0\x00\x00'\ +b'\x07\xe0\x00\x0f\x80\x00\x1f\x80\x00\xfc\x0f\xe3\xff\xc0\xfc\x00'\ +b'\x03\xe3\xff\xc0\x00\x00\x1f\x00\x01\xf0\x00\x00\x07\xfe\x0f\x80'\ +b'\x00\x7c\x00\x1f\xc0\x00\x0f\x80\x00\xf8\x00\x07\x00\x00\x1f\x00'\ +b'\x03\x0f\xe1\x87\x9e\x60\xcc\xc0\x00\xf8\x00\x00\x00\xc0\x7c\x00'\ +b'\x00\x01\xfe\x00\x1f\xe0\x00\x00\x07\xe0\x00\x0f\x80\x00\x1f\x80'\ +b'\x00\xfc\x1f\xf7\xff\xe0\xfc\x00\x03\xe7\xff\xc0\x00\x00\x1f\x00'\ +b'\x01\xf0\x01\xc0\x07\xfe\x0f\x80\x00\x3e\x00\x1f\xc0\x00\x1f\x00'\ +b'\x00\x7c\x00\x00\x00\x00\x3e\x00\x03\x3c\xfe\x00\x79\xff\xcc\x00'\ +b'\x00\xfc\x00\x00\x00\x00\xfc\x00\x00\x01\xfe\x00\x1f\xe0\x00\x00'\ +b'\x07\xe0\x00\x0f\x80\x00\x1f\x80\x00\xfc\x3f\xff\xff\xf0\xfc\x00'\ +b'\x03\xe7\xff\xc0\x00\x00\x1f\x00\x01\xf0\x1f\xfc\x07\xfe\x0f\x80'\ +b'\x00\x3e\x00\x1f\xc0\x00\x1f\x00\x00\x7c\x00\x00\x00\x00\x3e\x00'\ +b'\x03\x3c\xfe\x00\x79\xff\xcc\x00\x00\x7e\x00\x00\x00\x01\xf8\x00'\ +b'\x00\x01\xfe\x00\x1f\xe0\x00\x00\x07\xe0\x00\x0f\x80\x00\x1f\x80'\ +b'\x00\xfc\x7f\xff\xff\xf8\xfc\x00\x03\xe7\xff\xc0\x00\x00\x1f\x00'\ +b'\x01\xf0\x3f\xfe\x00\x00\x0f\x80\x00\x3f\x00\x1f\xc0\x00\x3f\x00'\ +b'\x00\x7e\x00\x02\x00\x00\x7e\x00\x00\x00\x01\xfe\x01\x80\xcc\x00'\ +b'\x00\x7e\x00\x00\x00\x01\xf8\x00\x00\x01\xfe\x00\x1f\xe0\x00\x00'\ +b'\x07\xe0\x00\x0f\x80\x00\x1f\x80\x00\xfc\x7f\xff\xff\xf8\xfc\x00'\ +b'\x03\xe7\xff\xc0\x00\x00\x1f\x00\x01\xf0\x7f\xff\x00\x00\x0f\x80'\ +b'\x00\x1f\x80\x1f\xc7\x00\x7e\x00\x00\x3f\x00\x0f\x80\x00\xfc\x00'\ +b'\x00\x00\x01\xfe\x01\x80\xcc\x00\x00\x3f\x00\x00\x00\x03\xf0\x00'\ +b'\x00\x01\xfe\x00\x1f\xe0\x00\x00\x07\xe3\xff\x0f\x80\x00\x1f\x80'\ +b'\x00\xfc\xff\xff\xff\xfc\xfc\x00\x03\xe3\xff\xc0\x00\x00\x1f\x00'\ +b'\x01\xf0\x7f\xff\x00\x00\x0f\x80\x00\x1f\x80\x1f\xcf\x00\x7e\x00'\ +b'\x00\x3f\x00\x0f\x80\x00\xfc\x00\x03\xff\xf9\xfe\x79\x98\xf0\xc0'\ +b'\x00\x1f\x80\x00\x00\x07\xe0\x00\x00\x01\xfe\x00\x1f\xe0\x00\x00'\ +b'\x07\xff\xff\xff\x83\xff\x1f\x80\x00\xfc\x00\x00\x00\x00\xfc\x00'\ +b'\x03\xe0\x00\x00\x00\x00\x1f\x00\x01\xf0\x7f\xff\x00\x00\x0f\x80'\ +b'\x00\x0f\xc0\x0f\xff\x00\xfc\x00\x00\x1f\x80\x0f\x80\x01\xf8\x00'\ +b'\x03\xff\xf9\xfe\x79\x98\xf0\xc0\x00\x0f\xc0\x00\x00\x0f\xc0\x00'\ +b'\x00\x01\xfe\x00\x1f\xe0\x00\x00\x07\xff\xff\xff\x8f\xff\xff\x80'\ +b'\x00\xfc\x00\x00\x00\x00\xfc\x00\x03\xe0\x00\x00\x00\x00\x1f\x00'\ +b'\x01\xf0\x00\x00\x00\x00\x0f\x80\x00\x07\xe0\x07\xff\x01\xf8\x00'\ +b'\x00\x0f\xc0\x0f\x80\x03\xf0\x00\x03\x00\x19\x80\x1f\x80\xc0\x00'\ +b'\x00\x0f\xf0\x00\x00\x3f\xc0\x00\x00\x01\xfe\x00\x1f\xe0\x00\x00'\ +b'\x07\xff\xff\xff\xbf\xff\xff\x80\x00\xfc\x00\x00\x00\x00\xfc\x00'\ +b'\x03\xe0\x00\x00\x00\x00\x1f\x00\x01\xf0\x00\x00\x00\x00\x0f\x80'\ +b'\x00\x03\xf0\x03\xfc\x03\xf0\x00\x00\x07\xe0\x07\x00\x07\xe0\x00'\ +b'\x03\x00\x19\x80\x1f\x80\xc0\x00\x00\x07\xf8\x00\x00\x7f\x80\x00'\ +b'\x00\x01\xfe\x00\x1f\xe0\x00\x00\x07\xff\xff\xff\xff\xff\xff\x80'\ +b'\x00\xfc\x00\x00\x00\x00\xfc\x00\x03\xe0\x00\x00\x00\x00\x1f\x00'\ +b'\x01\xf0\x00\x00\x00\x00\x0f\x80\x00\x03\xfc\x00\x70\x0f\xf0\x00'\ +b'\x00\x07\xf8\x00\x00\x1f\xe0\x00\x03\x00\x19\x80\x1f\x80\xc0\x00'\ +b'\x00\x03\xfe\x00\x01\xff\x00\x00\x00\x01\xfe\x00\x1f\xe0\x00\x00'\ +b'\x07\xfe\x01\xff\xff\xff\xff\x80\x00\xff\xff\xff\xff\xff\xfc\x00'\ +b'\x03\xff\xff\xff\xff\xff\xff\x00\x01\xf0\x00\x00\x00\x00\x0f\x80'\ +b'\x00\x01\xfe\x00\x00\x1f\xe0\x00\x00\x03\xfc\x00\x00\x3f\xc0\x00'\ +b'\x03\x3f\x18\x01\xff\xff\xff\x00\x00\x01\xff\xc0\x0f\xfe\x00\x00'\ +b'\x00\x01\xfe\x00\x1f\xe0\x00\x00\x03\xf0\x00\x1f\xfe\x01\xff\x80'\ +b'\x00\x7f\xff\xff\xff\xff\xf8\x00\x03\xff\xff\xff\xff\xff\xff\x00'\ +b'\x01\xf0\x00\x00\x00\x00\x0f\x80\x00\x00\xff\x80\x00\x7f\xc0\x00'\ +b'\x00\x01\xff\x00\x00\xff\x80\x00\x03\x3f\x18\x01\xff\xff\xff\x00'\ +b'\x00\x00\x7f\xff\xff\xf8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x01\x80\x00\x07\xf8\x00\x3f\x00\x00\x7f\xff\xff\xff\xff\xf8\x00'\ +b'\x03\xff\xff\xff\xff\xff\xff\x00\x01\xff\xff\xff\xff\xff\xff\x80'\ +b'\x00\x00\x7f\xf0\x03\xff\x80\x00\x00\x00\xff\xe0\x07\xff\x00\x00'\ +b'\x03\x3f\x18\x66\x1e\x7f\x33\xc0\x00\x00\x3f\xff\xff\xf0\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xe0\x00\x06\x00'\ +b'\x00\x7f\xff\xff\xff\xff\xf8\x00\x01\xff\xff\xff\xff\xff\xfe\x00'\ +b'\x01\xff\xff\xff\xff\xff\xff\x80\x00\x00\x1f\xff\xff\xfe\x00\x00'\ +b'\x00\x00\x3f\xff\xff\xfc\x00\x00\x03\x3f\x18\x66\x1e\x7f\x33\xc0'\ +b'\x00\x00\x0f\xff\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\xc0\x00\x00\x00\x00\x3f\xff\xff\xff\xff\xf0\x00'\ +b'\x00\xff\xff\xff\xff\xff\xfc\x00\x00\xff\xff\xff\xff\xff\xff\x00'\ +b'\x00\x00\x0f\xff\xff\xfc\x00\x00\x00\x00\x1f\xff\xff\xf8\x00\x00'\ +b'\x03\x3f\x18\x18\x19\xe7\x0c\xc0\x00\x00\x01\xff\xfe\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x7f\xff\xff\xff\xff\xfe\x00\x00\x00\x03\xff\xff\xf0\x00\x00'\ +b'\x00\x00\x07\xff\xff\xe0\x00\x00\x03\x3f\x18\x18\x19\xe7\x0c\xc0'\ +b'\x00\x00\x00\x3f\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x3f\xff\xff\xff\xff\xfc\x00'\ +b'\x00\x00\x00\x7f\xff\x80\x00\x00\x00\x00\x00\xff\xff\x00\x00\x00'\ +b'\x03\x00\x19\xe6\x01\x9f\xc0\xc0\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0f\xfc\x00\x00\x00'\ +b'\x00\x00\x00\x1f\xf8\x00\x00\x00\x03\x00\x19\xe6\x01\x9f\xc0\xc0'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x03\xff\xf9\xe7\x81\xe0\x30\xc0\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x03\xff\xf9\xe7\x81\xe0\x30\xc0'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ + +_mvdata = memoryview(_data) + +def data(): + return _mvdata + diff --git a/micropython/examples/badger2040w/lib/badger2040w.py b/micropython/examples/badger2040w/lib/badger2040w.py new file mode 100644 index 00000000..e19d995c --- /dev/null +++ b/micropython/examples/badger2040w/lib/badger2040w.py @@ -0,0 +1,133 @@ +import machine +from picographics import PicoGraphics, DISPLAY_INKY_PACK +from network_manager import NetworkManager +import WIFI_CONFIG +import uasyncio +import gc +import wakeup + + +BUTTON_UP = 15 +BUTTON_DOWN = 11 +BUTTON_A = 12 +BUTTON_B = 13 +BUTTON_C = 14 +BUTTON_USER = 23 + +BUTTON_MASK = 0b11111 << 11 + +SYSTEM_VERY_SLOW = 0 +SYSTEM_SLOW = 1 +SYSTEM_NORMAL = 2 +SYSTEM_FAST = 3 +SYSTEM_TURBO = 4 + +UPDATE_NORMAL = 0 +UPDATE_MEDIUM = 1 +UPDATE_FAST = 2 +UPDATE_TURBO = 3 + +WIDTH = 296 +HEIGHT = 128 + +SYSTEM_FREQS = [ + 4000000, + 12000000, + 48000000, + 133000000, + 250000000 +] + +WAKEUP_GPIO_STATE = wakeup.get_gpio_state() + +BUTTONS = { + 11: machine.Pin(11), + 12: machine.Pin(12), + 13: machine.Pin(13), + 14: machine.Pin(14), + 15: machine.Pin(15), + 23: machine.Pin(23) +} + + +def woken_by_button(): + return WAKEUP_GPIO_STATE & BUTTON_MASK > 0 + + +def pressed_to_wake(button): + return WAKEUP_GPIO_STATE & (1 << button) > 0 + + +def system_speed(speed): + try: + machine.freq(SYSTEM_FREQS[speed]) + except IndexError: + pass + + +class Badger2040W(): + def __init__(self): + self.display = PicoGraphics(DISPLAY_INKY_PACK) + + def __getattr__(self, item): + # Glue to redirect calls to PicoGraphics + if item in dir(self.display): + return getattr(self.display, item) + elif item in self.__dict__.keys(): + return getattr(self, item) + else: + raise AttributeError(f"No attribute '{item}'") + + def led(self, brightness): + pass + + def invert(self, invert): + pass + + def thickness(self, thickness): + print("Thickness!") + + def halt(self): + pass + + def pressed(self, button): + return BUTTONS[button].value() == 1 + + @micropython.native + def icon(self, data, index, data_w, icon_size, x, y): + s_x = (index * icon_size) % data_w + s_y = int((index * icon_size) / data_w) + + for o_y in range(icon_size): + for o_x in range(icon_size): + o = ((o_y + s_y) * data_w) + (o_x + s_x) + bm = 0b10000000 >> (o & 0b111) + if data[o >> 3] & bm: + self.display.pixel(x + o_x, y + o_y) + + def image(self, data, w, h, x, y): + for oy in range(h): + row = data[oy] + for ox in range(w): + if row & 0b1 == 0: + self.display.pixel(x + ox, y + oy) + row >>= 1 + + + def status_handler(self, mode, status, ip): + print(mode, status, ip) + self.display.set_pen(15) + self.display.clear() + self.display.set_pen(0) + if status: + self.display.text("Connected!", 10, 10, 300, 0.5) + self.display.text(ip, 10, 30, 300, 0.5) + else: + self.display.text("Connecting...", 10, 10, 300, 0.5) + self.display.update() + + def connect(self): + self.display.set_update_speed(2) + network_manager = NetworkManager(WIFI_CONFIG.COUNTRY, status_handler=self.status_handler) + uasyncio.get_event_loop().run_until_complete(network_manager.client(WIFI_CONFIG.SSID, WIFI_CONFIG.PSK)) + gc.collect() diff --git a/micropython/examples/badger2040w/lib/badger_os.py b/micropython/examples/badger2040w/lib/badger_os.py new file mode 100644 index 00000000..7effe1cb --- /dev/null +++ b/micropython/examples/badger2040w/lib/badger_os.py @@ -0,0 +1,185 @@ +"""Keep track of app state in persistent flash storage.""" + +import os +import gc +import time +import json +import machine +import badger2040w as badger2040 + + +def get_battery_level(): + return 0 + # Battery measurement + vbat_adc = machine.ADC(badger2040.PIN_BATTERY) + vref_adc = machine.ADC(badger2040.PIN_1V2_REF) + vref_en = machine.Pin(badger2040.PIN_VREF_POWER) + vref_en.init(machine.Pin.OUT) + vref_en.value(0) + + # Enable the onboard voltage reference + vref_en.value(1) + + # Calculate the logic supply voltage, as will be lower that the usual 3.3V when running off low batteries + vdd = 1.24 * (65535 / vref_adc.read_u16()) + vbat = ( + (vbat_adc.read_u16() / 65535) * 3 * vdd + ) # 3 in this is a gain, not rounding of 3.3V + + # Disable the onboard voltage reference + vref_en.value(0) + + # Convert the voltage to a level to display onscreen + return vbat + + +def get_disk_usage(): + # f_bfree and f_bavail should be the same? + # f_files, f_ffree, f_favail and f_flag are unsupported. + f_bsize, f_frsize, f_blocks, f_bfree, _, _, _, _, _, f_namemax = os.statvfs("/") + + f_total_size = f_frsize * f_blocks + f_total_free = f_bsize * f_bfree + f_total_used = f_total_size - f_total_free + + f_used = 100 / f_total_size * f_total_used + f_free = 100 / f_total_size * f_total_free + + return f_total_size, f_used, f_free + + +def state_running(): + state = {"running": "launcher"} + state_load("launcher", state) + return state["running"] + + +def state_clear_running(): + running = state_running() + state_modify("launcher", {"running": "launcher"}) + return running != "launcher" + + +def state_set_running(app): + state_modify("launcher", {"running": app}) + + +def state_launch(): + app = state_running() + if app is not None and app != "launcher": + launch("_" + app) + + +def state_delete(app): + try: + os.remove("/state/{}.json".format(app)) + except OSError: + pass + + +def state_save(app, data): + try: + with open("/state/{}.json".format(app), "w") as f: + f.write(json.dumps(data)) + f.flush() + except OSError: + import os + try: + os.stat("/state") + except OSError: + os.mkdir("/state") + state_save(app, data) + + +def state_modify(app, data): + state = {} + state_load(app, state) + state.update(data) + state_save(app, state) + + +def state_load(app, defaults): + try: + data = json.loads(open("/state/{}.json".format(app), "r").read()) + if type(data) is dict: + defaults.update(data) + return True + except (OSError, ValueError): + pass + + state_save(app, defaults) + return False + + +def launch(file): + state_set_running(file[1:]) + + gc.collect() + + button_a = machine.Pin(badger2040.BUTTON_A, machine.Pin.IN, machine.Pin.PULL_DOWN) + button_c = machine.Pin(badger2040.BUTTON_C, machine.Pin.IN, machine.Pin.PULL_DOWN) + + def quit_to_launcher(pin): + if button_a.value() and button_c.value(): + machine.reset() + + button_a.irq(trigger=machine.Pin.IRQ_RISING, handler=quit_to_launcher) + button_c.irq(trigger=machine.Pin.IRQ_RISING, handler=quit_to_launcher) + + try: + try: + __import__(file[1:]) # Try to import _[file] (drop underscore prefix) + except ImportError: + __import__(file) # Failover to importing [_file] + + except ImportError: + # If the app doesn't exist, notify the user + warning(None, "Could not launch: " + file[1:]) + time.sleep(4.0) + except Exception as e: + # If the app throws an error, catch it and display! + print(e) + warning(None, str(e)) + time.sleep(4.0) + + # If the app exits or errors, do not relaunch! + state_clear_running() + machine.reset() # Exit back to launcher + + +# Draw an overlay box with a given message within it +def warning(display, message, width=badger2040.WIDTH - 40, height=badger2040.HEIGHT - 40, line_spacing=20, text_size=0.6): + print(message) + + if display is None: + display = badger2040.Badger2040W() + display.led(128) + + # Draw a light grey background + display.set_pen(12) + display.rectangle((badger2040.WIDTH - width) // 2, (badger2040.HEIGHT - height) // 2, width, height) + + # Take the provided message and split it up into + # lines that fit within the specified width + words = message.split(" ") + + lines = [] + current_line = "" + for word in words: + if display.measure_text(current_line + word + " ", text_size) < width: + current_line += word + " " + else: + lines.append(current_line.strip()) + current_line = word + " " + lines.append(current_line.strip()) + + display.set_pen(0) + + # Display each line of text from the message, centre-aligned + num_lines = len(lines) + for i in range(num_lines): + length = display.measure_text(lines[i], text_size) + current_line = (i * line_spacing) - ((num_lines - 1) * line_spacing) // 2 + display.text(lines[i], (badger2040.WIDTH - length) // 2, (badger2040.HEIGHT // 2) + current_line, badger2040.WIDTH, text_size) + + display.update() diff --git a/micropython/examples/badger2040w/lib/network_manager.py b/micropython/examples/badger2040w/lib/network_manager.py new file mode 100644 index 00000000..d4d59230 --- /dev/null +++ b/micropython/examples/badger2040w/lib/network_manager.py @@ -0,0 +1,107 @@ +import rp2 +import network +import machine +import uasyncio + + +class NetworkManager: + _ifname = ("Client", "Access Point") + + def __init__(self, country="GB", client_timeout=30, access_point_timeout=5, status_handler=None, error_handler=None): + rp2.country(country) + self._ap_if = network.WLAN(network.AP_IF) + self._sta_if = network.WLAN(network.STA_IF) + + self._mode = network.STA_IF + self._client_timeout = client_timeout + self._access_point_timeout = access_point_timeout + self._status_handler = status_handler + self._error_handler = error_handler + self.UID = ("{:02X}" * 8).format(*machine.unique_id()) + + def isconnected(self): + return self._sta_if.isconnected() or self._ap_if.isconnected() + + def config(self, var): + if self._sta_if.active(): + return self._sta_if.config(var) + else: + if var == "password": + return self.UID + return self._ap_if.config(var) + + def mode(self): + if self._sta_if.isconnected(): + return self._ifname[0] + if self._ap_if.isconnected(): + return self._ifname[1] + return None + + def ifaddress(self): + if self._sta_if.isconnected(): + return self._sta_if.ifconfig()[0] + if self._ap_if.isconnected(): + return self._ap_if.ifconfig()[0] + return '0.0.0.0' + + def disconnect(self): + if self._sta_if.isconnected(): + self._sta_if.disconnect() + if self._ap_if.isconnected(): + self._ap_if.disconnect() + + async def wait(self, mode): + while not self.isconnected(): + self._handle_status(mode, None) + await uasyncio.sleep_ms(1000) + + def _handle_status(self, mode, status): + if callable(self._status_handler): + self._status_handler(self._ifname[mode], status, self.ifaddress()) + + def _handle_error(self, mode, msg): + if callable(self._error_handler): + if self._error_handler(self._ifname[mode], msg): + return + raise RuntimeError(msg) + + async def client(self, ssid, psk): + if self._sta_if.isconnected(): + self._handle_status(network.STA_IF, True) + return + + self._ap_if.disconnect() + self._ap_if.active(False) + + self._sta_if.active(True) + self._sta_if.connect(ssid, psk) + + try: + await uasyncio.wait_for(self.wait(network.STA_IF), self._client_timeout) + self._handle_status(network.STA_IF, True) + + except uasyncio.TimeoutError: + self._sta_if.active(False) + self._handle_status(network.STA_IF, False) + self._handle_error(network.STA_IF, "WIFI Client Failed") + + async def access_point(self): + if self._ap_if.isconnected(): + self._handle_status(network.AP_IF, True) + return + + self._sta_if.disconnect() + self._sta_if.active(False) + + self._ap_if.ifconfig(("10.10.1.1", "255.255.255.0", "10.10.1.1", "10.10.1.1")) + self._ap_if.config(password=self.UID) + self._ap_if.active(True) + + try: + await uasyncio.wait_for(self.wait(network.AP_IF), self._access_point_timeout) + self._handle_status(network.AP_IF, True) + + except uasyncio.TimeoutError: + self._sta_if.active(False) + self._handle_status(network.AP_IF, False) + self._handle_error(network.AP_IF, "WIFI Client Failed") diff --git a/micropython/examples/badger2040w/lib/urllib/urequest.py b/micropython/examples/badger2040w/lib/urllib/urequest.py new file mode 100644 index 00000000..4c654d45 --- /dev/null +++ b/micropython/examples/badger2040w/lib/urllib/urequest.py @@ -0,0 +1,67 @@ +import usocket + + +def urlopen(url, data=None, method="GET"): + if data is not None and method == "GET": + method = "POST" + try: + proto, dummy, host, path = url.split("/", 3) + except ValueError: + proto, dummy, host = url.split("/", 2) + path = "" + if proto == "http:": + port = 80 + elif proto == "https:": + import ussl + + port = 443 + else: + raise ValueError("Unsupported protocol: " + proto) + + if ":" in host: + host, port = host.split(":", 1) + port = int(port) + + ai = usocket.getaddrinfo(host, port, 0, usocket.SOCK_STREAM) + ai = ai[0] + + s = usocket.socket(ai[0], ai[1], ai[2]) + try: + s.connect(ai[-1]) + if proto == "https:": + s = ussl.wrap_socket(s, server_hostname=host) + + s.write(method) + s.write(b" /") + s.write(path) + s.write(b" HTTP/1.0\r\nHost: ") + s.write(host) + s.write(b"\r\n") + + if data: + s.write(b"Content-Length: ") + s.write(str(len(data))) + s.write(b"\r\n") + s.write(b"\r\n") + if data: + s.write(data) + + l = s.readline() + l = l.split(None, 2) + # print(l) + status = int(l[1]) + while True: + l = s.readline() + if not l or l == b"\r\n": + break + # print(l) + if l.startswith(b"Transfer-Encoding:"): + if b"chunked" in l: + raise ValueError("Unsupported " + l) + elif l.startswith(b"Location:"): + raise NotImplementedError("Redirects not yet supported") + except OSError: + s.close() + raise + + return s diff --git a/micropython/examples/badger2040w/main.py b/micropython/examples/badger2040w/main.py index 29658341..39fcdade 100644 --- a/micropython/examples/badger2040w/main.py +++ b/micropython/examples/badger2040w/main.py @@ -1 +1,10 @@ -# Hello World +import WIFI_CONFIG + +if WIFI_CONFIG.SSID == "": + import badger2040w + badger2040w.init() + badger2040w.screen.set_pen(15) + badger2040w.screen.clear() + badger2040w.screen.set_pen(0) + badger2040w.screen.text("Please configure your WiFi in WIFI_CONFIG.py") + badger2040w.screen.update() diff --git a/micropython/examples/badger2040w/uf2-manifest.txt b/micropython/examples/badger2040w/uf2-manifest.txt index 11a5d8e1..02acc631 100644 --- a/micropython/examples/badger2040w/uf2-manifest.txt +++ b/micropython/examples/badger2040w/uf2-manifest.txt @@ -1 +1,3 @@ -main.py \ No newline at end of file +*.py +lib/*.py +lib/urllib/*.py \ No newline at end of file