├── img ├── hello_font.jpg └── hello_world.jpg ├── .gitignore ├── esp32spiram-idf4-20191220-v1.12.bin ├── boot.py ├── path.py ├── exampleSd.py ├── scripts.txt ├── config_template.py ├── text.py ├── LICENSE ├── images.py ├── playground.py ├── exampleNetwork.py ├── example.py ├── calendar_api.py ├── mcp23017.py ├── README.md ├── utils.py ├── gfx_standard_font_01.py ├── ui.py ├── sdcard.py ├── app.py ├── device.py ├── layout.py ├── gfx.py ├── pyboard.py ├── image.py └── inkplate.py /img/hello_font.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikurahul/Inkplate-6-micropython/HEAD/img/hello_font.jpg -------------------------------------------------------------------------------- /img/hello_world.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikurahul/Inkplate-6-micropython/HEAD/img/hello_world.jpg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | board_config.py 2 | board_config_*.py 3 | config.py 4 | init.mpf 5 | lib 6 | __pycache__ 7 | .vscode/.ropeproject/objectdb 8 | -------------------------------------------------------------------------------- /esp32spiram-idf4-20191220-v1.12.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikurahul/Inkplate-6-micropython/HEAD/esp32spiram-idf4-20191220-v1.12.bin -------------------------------------------------------------------------------- /boot.py: -------------------------------------------------------------------------------- 1 | import machine 2 | 3 | from app import App 4 | 5 | if machine.reset_cause() == machine.DEEPSLEEP_RESET: 6 | print('Waking up.') 7 | app = App() 8 | app.on_wakeup() 9 | -------------------------------------------------------------------------------- /path.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | def exists(path): 5 | ''' 6 | Return True if the path exists. 7 | ''' 8 | try: 9 | os.stat(path) 10 | return True 11 | except OSError: 12 | return False 13 | -------------------------------------------------------------------------------- /exampleSd.py: -------------------------------------------------------------------------------- 1 | import os 2 | from inkplate import Inkplate 3 | 4 | display = Inkplate(Inkplate.INKPLATE_2BIT) 5 | display.begin() 6 | 7 | # This prints all the files on card 8 | # print(os.listdir("/sd")) 9 | 10 | f = open("sd/text.txt", "r") 11 | 12 | # Print file contents 13 | print(f.read()) 14 | f.close() 15 | 16 | # Utterly slow, can take minutes :( 17 | display.drawImageFile(0, 0, "sd/32bit.bmp") 18 | 19 | display.display() 20 | -------------------------------------------------------------------------------- /scripts.txt: -------------------------------------------------------------------------------- 1 | erase: 2 | esptool.py --port /dev/cu.usbserial-1420 erase_flash 3 | 4 | flash: 5 | esptool.py --chip esp32 --port /dev/cu.usbserial-1420 write_flash -z 0x1000 esp32spiram-idf4-20191220-v1.12.bin 6 | 7 | copy all: 8 | python3 pyboard.py --device /dev/cu.usbserial-1420 -f cp inkplate.py gfx.py gfx_standard_font_01.py mcp23017.py image.py sdcard.py : 9 | 10 | run: 11 | python3 pyboard.py --device /dev/cu.usbserial-1420 -f cp inkplate.py : && python3 pyboard.py --device /dev/cu.usbserial-1420 example.py -------------------------------------------------------------------------------- /config_template.py: -------------------------------------------------------------------------------- 1 | # Copy this file and rename it to `config.py` 2 | 3 | # OAuth2 Configuration 4 | # Look at: https://developers.google.com/identity/protocols/oauth2/limited-input-device 5 | 6 | API_KEY = ' 10) else text_size 39 | self.text_size = text_size 40 | self.padding = padding 41 | 42 | def measured_width(self): 43 | return self.length * _WIDTHS[self.text_size] + 2 * self.padding 44 | 45 | def measured_height(self): 46 | return _HEIGHTS[self.text_size] + 2 * self.padding 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Thorsten von Eicken 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 | -------------------------------------------------------------------------------- /images.py: -------------------------------------------------------------------------------- 1 | 2 | # https://javl.github.io/image2cpp/ 3 | 4 | # 40x40 5 | CALENDAR_40_40 = bytearray([ 6 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x1c, 0x00, 0x00, 7 | 0x38, 0x00, 0x1c, 0x00, 0x00, 0x3c, 0x00, 0x1c, 0x00, 0x01, 0xff, 0xff, 0xff, 0x80, 0x03, 0xff, 8 | 0xff, 0xff, 0xc0, 0x07, 0xff, 0xff, 0xff, 0xe0, 0x07, 0xff, 0xff, 0xff, 0xe0, 0x07, 0xff, 0xff, 9 | 0xff, 0xe0, 0x07, 0xff, 0xff, 0xff, 0xe0, 0x07, 0xff, 0xff, 0xff, 0xe0, 0x07, 0xff, 0xff, 0xff, 10 | 0xe0, 0x07, 0x80, 0x00, 0x01, 0xe0, 0x07, 0x00, 0x00, 0x00, 0xe0, 0x07, 0x00, 0x00, 0x00, 0xe0, 11 | 0x07, 0x00, 0x00, 0x00, 0xe0, 0x07, 0x00, 0x00, 0x00, 0xe0, 0x07, 0x00, 0x00, 0x00, 0xe0, 0x07, 12 | 0x00, 0x00, 0x00, 0xe0, 0x07, 0x00, 0x0f, 0xf0, 0xe0, 0x07, 0x00, 0x0f, 0xf0, 0xe0, 0x07, 0x00, 13 | 0x0f, 0xf0, 0xe0, 0x07, 0x00, 0x0f, 0xf0, 0xe0, 0x07, 0x00, 0x0f, 0xf0, 0xe0, 0x07, 0x00, 0x0f, 14 | 0xf0, 0xe0, 0x07, 0x00, 0x0f, 0xf0, 0xe0, 0x07, 0x00, 0x0f, 0xf0, 0xe0, 0x07, 0x00, 0x00, 0x00, 15 | 0xe0, 0x07, 0x00, 0x00, 0x00, 0xe0, 0x07, 0x00, 0x00, 0x00, 0xe0, 0x07, 0x80, 0x00, 0x01, 0xe0, 16 | 0x07, 0xff, 0xff, 0xff, 0xe0, 0x03, 0xff, 0xff, 0xff, 0xc0, 0x01, 0xff, 0xff, 0xff, 0x80, 0x00, 17 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 18 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 19 | ]) 20 | -------------------------------------------------------------------------------- /playground.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | import machine 4 | import network 5 | import ntptime 6 | import utime 7 | 8 | from config import WLAN_PASSWORD, WLAN_SSID 9 | from inkplate import Inkplate 10 | 11 | display = Inkplate(Inkplate.INKPLATE_1BIT) 12 | width = display.width() 13 | height = display.height() 14 | 15 | 16 | def initialize(): 17 | # Must be called before using, line in Arduino 18 | display.begin() 19 | wlan = network.WLAN(network.STA_IF) 20 | wlan.active(True) 21 | wlan.connect(WLAN_SSID, WLAN_PASSWORD) 22 | delay = 0 23 | while not wlan.isconnected() and delay < 50: 24 | time.sleep_ms(200) 25 | delay += 1 26 | 27 | ntptime.settime() 28 | 29 | 30 | def draw_grid(): 31 | for i in range(0, height, 10): 32 | display.drawFastHLine(0, i, width, display.BLACK) 33 | 34 | for i in range(0, width, 10): 35 | display.drawFastVLine(i, 0, height, display.BLACK) 36 | 37 | y = 10 38 | for i in range(1, 11, 1): 39 | display.setTextSize(i) 40 | display.printText(10, y, 'Hello World'.upper()) 41 | y += 9 * i 42 | 43 | display.display() 44 | 45 | def deep_sleep(): 46 | print('About to deep sleep') 47 | machine.deepsleep(10 * 1000) 48 | 49 | 50 | if __name__ == "__main__": 51 | if machine.reset_cause() == machine.DEEPSLEEP_RESET: 52 | print('Waking up.') 53 | 54 | deep_sleep() 55 | 56 | 57 | -------------------------------------------------------------------------------- /exampleNetwork.py: -------------------------------------------------------------------------------- 1 | import network 2 | import time 3 | from inkplate import Inkplate 4 | 5 | ssid = "" 6 | password = "" 7 | 8 | # More info here: https://docs.micropython.org/en/latest/esp8266/tutorial/network_basics.html 9 | def do_connect(): 10 | import network 11 | 12 | sta_if = network.WLAN(network.STA_IF) 13 | if not sta_if.isconnected(): 14 | print("connecting to network...") 15 | sta_if.active(True) 16 | sta_if.connect(ssid, password) 17 | while not sta_if.isconnected(): 18 | pass 19 | print("network config:", sta_if.ifconfig()) 20 | 21 | 22 | # More info here: https://docs.micropython.org/en/latest/esp8266/tutorial/network_tcp.html 23 | def http_get(url): 24 | import socket 25 | 26 | res = "" 27 | _, _, host, path = url.split("/", 3) 28 | addr = socket.getaddrinfo(host, 80)[0][-1] 29 | s = socket.socket() 30 | s.connect(addr) 31 | s.send(bytes("GET /%s HTTP/1.0\r\nHost: %s\r\n\r\n" % (path, host), "utf8")) 32 | while True: 33 | data = s.recv(100) 34 | if data: 35 | res += str(data, "utf8") 36 | else: 37 | break 38 | s.close() 39 | 40 | return res 41 | 42 | 43 | # Calling functions defined above 44 | do_connect() 45 | response = http_get("http://micropython.org/ks/test.html") 46 | 47 | # Initialise our Inkplate object 48 | display = Inkplate(Inkplate.INKPLATE_1BIT) 49 | display.begin() 50 | 51 | # Print response in lines 52 | cnt = 0 53 | for x in response.split("\n"): 54 | display.printText( 55 | 10, 10 + cnt, x.upper() 56 | ) # Default font has only upper case letters 57 | cnt += 10 58 | 59 | # Display image from buffer 60 | display.display() 61 | -------------------------------------------------------------------------------- /example.py: -------------------------------------------------------------------------------- 1 | from inkplate import Inkplate 2 | from image import * 3 | import time 4 | 5 | display = Inkplate(Inkplate.INKPLATE_1BIT) 6 | 7 | if __name__ == "__main__": 8 | # Must be called before using, line in Arduino 9 | display.begin() 10 | 11 | for r in range(4): 12 | # Sets the screen rotation 13 | display.setRotation(r) 14 | 15 | # All drawing functions 16 | display.drawPixel(100, 100, display.BLACK) 17 | display.drawRect(50, 50, 75, 75, display.BLACK) 18 | display.drawCircle(200, 200, 30, display.BLACK) 19 | display.fillCircle(300, 300, 30, display.BLACK) 20 | display.drawFastHLine(20, 100, 50, display.BLACK) 21 | display.drawFastVLine(100, 20, 50, display.BLACK) 22 | display.drawLine(100, 100, 400, 400, display.BLACK) 23 | display.drawRoundRect(100, 10, 100, 100, 10, display.BLACK) 24 | display.fillRoundRect(10, 100, 100, 100, 10, display.BLACK) 25 | display.drawTriangle(300, 100, 400, 150, 400, 100, display.BLACK) 26 | 27 | if display.rotation % 2 == 0: 28 | display.fillTriangle(500, 101, 400, 150, 400, 100, display.BLACK) 29 | 30 | # Draws image from bytearray 31 | display.setRotation(0) 32 | display.drawBitmap(120, 200, image, 576, 100) 33 | display.display() 34 | 35 | time.sleep(5) 36 | 37 | # You can switch display modes anytime 38 | display.selectDisplayMode(display.INKPLATE_2BIT) 39 | 40 | display.clearDisplay() 41 | for r in range(4): 42 | display.setRotation(r) 43 | 44 | # All drawing functions 45 | display.drawPixel(100, 100, 0) 46 | display.drawRect(50, 50, 75, 75, 1) 47 | display.drawCircle(200, 200, 30, 1) 48 | display.fillCircle(300, 300, 30, 1) 49 | display.drawFastHLine(20, 100, 50, 1) 50 | display.drawFastVLine(100, 20, 50, 1) 51 | display.drawLine(100, 100, 400, 400, 0) 52 | display.drawRoundRect(100, 10, 100, 100, 10, 0) 53 | display.fillRoundRect(10, 100, 100, 100, 10, 1) 54 | display.drawTriangle(300, 100, 400, 150, 400, 100, 1) 55 | 56 | if display.rotation % 2 == 0: 57 | display.fillTriangle(500, 101, 400, 150, 400, 100, 1) 58 | 59 | display.setRotation(0) 60 | 61 | # Displays content from buffer 62 | display.display() 63 | -------------------------------------------------------------------------------- /calendar_api.py: -------------------------------------------------------------------------------- 1 | import urequests as requests 2 | 3 | from config import API_KEY 4 | from utils import DateTime, today_rfc3339, urlencode 5 | 6 | 7 | class Calendar: 8 | ''' 9 | A Google Calendar API Client 10 | ''' 11 | 12 | def __init__(self, device_auth): 13 | self.device_auth = device_auth 14 | if not self.device_auth.authorized: 15 | raise RuntimeError('Unauthorized.') 16 | 17 | def events(self, limit=5): 18 | # Calendar id is part of the endpoint iself. 19 | endpoint = 'https://www.googleapis.com/calendar/v3/calendars/primary/events' 20 | start_time = today_rfc3339(hours_offset=-1) 21 | token = self.device_auth.token() 22 | authorization = 'Bearer %s' % (token) 23 | headers = { 24 | 'Authorization': authorization, 25 | 'Accept': 'application/json' 26 | } 27 | payload = { 28 | 'maxResults': limit, 29 | 'orderBy': 'startTime', 30 | 'singleEvents': 'true', 31 | 'timeMin': start_time, 32 | 'key': API_KEY 33 | } 34 | encoded = urlencode(payload) 35 | full_url = '%s?%s' % (endpoint, encoded) 36 | r = requests.request( 37 | 'GET', 38 | full_url, 39 | headers=headers 40 | ) 41 | j = r.json() 42 | r.close() 43 | 44 | if 'error' in j: 45 | raise RuntimeError(j) 46 | 47 | return self._parse_calendar_events(j) 48 | 49 | def _parse_calendar_events(self, j): 50 | events = list() 51 | if not 'items' in j: 52 | return events 53 | 54 | items = j['items'] 55 | for i in range(len(items)): 56 | item = items[i] 57 | event = Event(item) 58 | events.append(event) 59 | 60 | return events 61 | 62 | 63 | class Event: 64 | ''' 65 | A Calendar Event 66 | ''' 67 | 68 | def __init__(self, j): 69 | self.summary = j['summary'] 70 | 71 | self.description = None 72 | self.start_at = None 73 | self.end_at = None 74 | 75 | if 'description' in j: 76 | self.description = j['description'] 77 | if 'start' in j and 'dateTime' in j['start']: 78 | # 2020-12-24T18:30:00-08:00 79 | formatted = j['start']['dateTime'] 80 | self.start_at = DateTime.from_str(formatted) 81 | if 'end' in j and 'date' in j['end']: 82 | # '2021-01-02' 83 | formatted = j['end']['date'] 84 | self.end_at = DateTime.from_str(formatted) 85 | -------------------------------------------------------------------------------- /mcp23017.py: -------------------------------------------------------------------------------- 1 | # Copyright © 2020 by Thorsten von Eicken. 2 | from machine import Pin as mPin 3 | from micropython import const 4 | 5 | # MCP23017 registers - defined as const(), which makes them module-global 6 | IODIR = const(0) 7 | IOCON = const(0xA) 8 | GPPU = const(0xC) 9 | GPIO = const(0x12) 10 | OLAT = const(0x14) 11 | 12 | 13 | # MCP23017 is a minimal driver for an 16-bit I2C I/O expander 14 | class MCP23017: 15 | def __init__(self, i2c, addr=0x20): 16 | self.i2c = i2c 17 | self.addr = addr 18 | self.write(IOCON, 0x00) 19 | self.write2(IODIR, 0xFF, 0xFF) # all inputs 20 | self.gpio0 = 0 21 | self.gpio1 = 0 22 | self.write2(GPIO, 0, 0) 23 | 24 | # read an 8-bit register, internal method 25 | def read(self, reg): 26 | return self.i2c.readfrom_mem(self.addr, reg, 1)[0] 27 | 28 | # write an 8-bit register, internal method 29 | def write(self, reg, v): 30 | self.i2c.writeto_mem(self.addr, reg, bytes((v,))) 31 | 32 | # write two 8-bit registers, internal method 33 | def write2(self, reg, v1, v2): 34 | self.i2c.writeto_mem(self.addr, reg, bytes((v1, v2))) 35 | 36 | # writebuf writes multiple bytes to the same register 37 | def writebuf(self, reg, v): 38 | self.i2c.writeto_mem(self.addr, reg, v) 39 | 40 | # bit reads or sets a bit in a register, caching the gpio register for performance 41 | def bit(self, reg, num, v=None): 42 | if v is None: 43 | data = self.read(reg) 44 | if reg == GPIO: 45 | self.gpio0 = data 46 | elif reg == GPIO + 1: 47 | self.gpio1 = data 48 | return (data >> num) & 1 49 | else: 50 | mask = 0xFF ^ (1 << num) 51 | if reg == GPIO: 52 | self.gpio0 = (self.gpio0 & mask) | ((v & 1) << num) 53 | self.write(reg, self.gpio0) 54 | elif reg == GPIO + 1: 55 | self.gpio1 = (self.gpio1 & mask) | ((v & 1) << num) 56 | self.write(reg, self.gpio1) 57 | else: 58 | data = (self.read(reg) & mask) | ((v & 1) << num) 59 | self.write(reg, data) 60 | 61 | def pin(self, num, mode=mPin.IN, pull=None, value=None): 62 | return Pin(self, num, mode, pull, value) 63 | 64 | 65 | # Pin implements a minimal machine.Pin look-alike for pins on the MCP23017 66 | class Pin: 67 | def __init__(self, mcp23017, num, mode=mPin.IN, pull=None, value=None): 68 | self.bit = mcp23017.bit 69 | incr = num >> 3 # bank selector 70 | self.gpio = GPIO + incr 71 | self.num = num = num & 0x7 72 | if value is not None: 73 | self.bit(self.gpio, num, value) 74 | self.bit(IODIR + incr, num, 1 if mode == mPin.IN else 0) 75 | self.bit(GPPU + incr, num, 1 if pull == mPin.PULL_UP else 0) 76 | 77 | # value reads or write a pin value (0 or 1) 78 | def value(self, v=None): 79 | if v is None: 80 | return self.bit(self.gpio, self.num) 81 | else: 82 | self.bit(self.gpio, self.num, v) 83 | 84 | __call__ = value 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Inkplate 6 micropython module 2 | 3 | ![](https://www.crowdsupply.com/img/040a/inkplate-6-angle-01_png_project-main.jpg) 4 | 5 | Micropython for all-in-one e-paper display Inkplate 6 can be found in this repo. Inkplate 6 is a powerful, Wi-Fi enabled ESP32 based six-inch e-paper display – recycled from a Kindle e-reader. Its main feature is simplicity. Just plug in a USB cable, open Arduino IDE, and change the contents of the screen with few lines of code. Learn more about Inkplate 6 on [official website](https://inkplate.io/). Inkplate was crowdfunded on [Crowd Supply](https://www.crowdsupply.com/e-radionica/inkplate-6). 6 | 7 | Original effort done by [tve](https://github.com/tve/micropython-inkplate6). 8 | 9 | ### Features 10 | 11 | - Simple graphics class for monochrome use of the e-paper display 12 | - Simple graphics class for 2 bits per pixel greyscale use of the e-paper display 13 | - Support for partial updates (currently only on the monochrome display) 14 | - Access to touch sensors 15 | - Everything in pure python with screen updates virtually as fast as the Arduino C driver 16 | - Bitmap drawing, although really slow one 17 | 18 | ### Getting started with micropython on Inkplate 6 19 | 20 | 21 | - Flash MicroPython firmware supplied, or from http://micropython.org/download/esp32/ . 22 | - Run 23 | ``` 24 | esptool.py --port /dev/cu.usbserial-1420 erase_flash 25 | ``` 26 | to erase esp32 flash and then 27 | ``` 28 | esptool.py --chip esp32 --port /dev/cu.usbserial-1420 write_flash -z 0x1000 esp32spiram-idf4-20191220-v1.12.bin 29 | ``` 30 | to flash supplied firmware. 31 | 32 | If you don't have esptool.py installed, install it from here: https://github.com/espressif/esptool. 33 | 34 | - Copy library files to your board, something like: 35 | ``` 36 | python3 pyboard.py --device /dev/ttyUSB0 -f cp mcp23017.py sdcard.py inkplate.py image.py gfx.py gfx_standard_font_01.py : 37 | ``` 38 | (You can find `pyboard.py` in the MicroPython tools directory or just download it from 39 | GitHub: https://raw.githubusercontent.com/micropython/micropython/master/tools/pyboard.py) 40 | 41 | - Run `example.py`: 42 | ``` 43 | python3 pyboard.py --device /dev/ttyUSB0 example.py 44 | ``` 45 | - You can run our other 2 examples, showing how to use the Sd card and network class. 46 | 47 | ### Examples 48 | 49 | The repo contains many examples which can demonstrate the Inkplate capabilites. 50 | - example.py -> demonstrates basic drawing capabilites, as well as drawing some images 51 | - exampleNetwork.py -> demonstrates connection to WiFi network while drawing the HTTP request response on the screen 52 | - exampleSd.py -> demonstrates reading files and images from SD card 53 | 54 | ### Battery power 55 | 56 | Inkplate 6 has two options for powering it. First one is obvious - USB port at side of the board. Just plug any micro USB cable and you are good to go. Second option is battery. Supported batteries are standard Li-Ion/Li-Poly batteries with 3.7V nominal voltage. Connector for the battery is standard 2.00mm pitch JST connector. The onboard charger will charge the battery with 500mA when USB is plugged at the same time. You can use battery of any size or capacity if you don't have a enclosure. If you are using our enclosure, battery size shouldn't exceed 90mm x 40mm (3.5 x 1.57 inch) and 5mm (0.19 inch) in height. [This battery](https://e-radionica.com/en/li-ion-baterija-1200mah.html) is good fit for the Inkplate. 57 | 58 | ### Arduino? 59 | 60 | Looking for Arduino library? Look [here](https://github.com/e-radionicacom/Inkplate-6-Arduino-library)! 61 | 62 | ### Open-source 63 | 64 | All of Inkplate-related development is open-sourced: 65 | - [Arduino library](https://github.com/e-radionicacom/Inkplate-6-Arduino-library) 66 | - [Inkplate 6 hardware](https://github.com/e-radionicacom/Inkplate-6-hardware) 67 | - [micropython Inkplate](https://github.com/e-radionicacom/Inkplate-6-micropython) 68 | - [OSHWA certificate](https://certification.oshwa.org/hr000003.html) 69 | 70 | ### Where to buy & other 71 | 72 | Inkplate 6 is available for purchase via: 73 | 74 | - [e-radionica.com](https://e-radionica.com/en/inkplate.html) 75 | - [Crowd Supply](https://www.crowdsupply.com/e-radionica/inkplate-6) 76 | - [Mouser](https://hr.mouser.com/Search/Refine?Keyword=inkplate) 77 | - [Sparkfun](https://www.sparkfun.com/search/results?term=inkplate) 78 | - [Pimoroni](https://shop.pimoroni.com/products/inkplate-6) 79 | 80 | Inkplate 6 is open-source. If you are looking for hardware design of the board, check the [Hardware repo](https://github.com/e-radionicacom/Inkplate-6-hardware). You will find 3D printable [enclosure](https://github.com/e-radionicacom/Inkplate-6-hardware/tree/master/3D%20printable%20case) there, as well as [detailed dimensions](https://github.com/e-radionicacom/Inkplate-6-hardware/tree/master/Technical%20drawings). In this repo you will find code for driving the ED060SC7 e-paper display used by Inkplate. 81 | 82 | For all questions and issues, please use our [forum](http://forum.e-radionica.com/en) to ask an question. 83 | For sales & collaboration, please reach us via [e-mail](mailto:kontakt@e-radionica.com). 84 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | import utime 4 | 5 | from config import UTC_OFFSET 6 | 7 | always_safe = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_.-' 8 | 9 | 10 | def quote(s): 11 | res = [] 12 | for c in s: 13 | if c in always_safe: 14 | res.append(c) 15 | continue 16 | res.append('%%%x' % ord(c)) 17 | return ''.join(res) 18 | 19 | 20 | def urlencode(query): 21 | if isinstance(query, dict): 22 | query = query.items() 23 | l = [] 24 | for k, v in query: 25 | if not isinstance(v, list): 26 | v = [v] 27 | for value in v: 28 | k = quote(str(k)) 29 | v = quote(str(value)) 30 | l.append(k + '=' + v) 31 | return '&'.join(l) 32 | 33 | 34 | def time_as_utime(offset=0): 35 | ''' 36 | Returns the number of seconds since UTC epoch adjusted to the 37 | calendar timezone. 38 | ''' 39 | 40 | t = utime.mktime(utime.localtime()) # Seconds since UTC Epoch 41 | t += int(offset * 3600) # 3600 = 1 hr in seconds 42 | # Convert it back to epoch for roll over 43 | return utime.mktime(utime.localtime(t)) 44 | 45 | 46 | def today_rfc3339(hours_offset=0): 47 | ''' 48 | Return `today`s date 49 | ''' 50 | 51 | now = time_as_utime(offset=UTC_OFFSET) 52 | (year, month, day, hours, _, _, _, _) = utime.localtime(now) 53 | hours += hours_offset 54 | t = utime.mktime((year, month, day, hours, 0, 0, 0, 0)) 55 | return format_time(t, tz=UTC_OFFSET) 56 | 57 | 58 | def format_time(t, tz=0.): 59 | ''' 60 | Formats a timestamp based on RFC 3339 61 | ''' 62 | year, month, day, hours, minutes, seconds, _, _ = utime.localtime(t) 63 | f_tz = format_tz(tz) 64 | # RFC 3339 Timestamp 65 | # 2011-06-03T10:00:00-07:00 66 | # 2011-06-03T10:00:00Z 67 | return '{:04d}-{:02d}-{:02d}T{:02d}:{:02d}:{:02d}{:s}'.format(year, month, day, hours, minutes, seconds, f_tz) 68 | 69 | 70 | def format_tz(tz): 71 | ''' 72 | Formats the `tz` offset to a string. 73 | ''' 74 | a = abs(tz) 75 | f = math.floor(a) 76 | d = a - f 77 | 78 | hours = int(f) 79 | minutes = int(d * 60) 80 | 81 | if tz == 0: 82 | return 'Z' 83 | 84 | sign = '+' if tz > 0 else '-' 85 | return '{:s}{:02d}:{:02d}'.format(sign, hours, minutes) 86 | 87 | 88 | class DateTime: 89 | ''' 90 | Represents a date and time with a UTF offset. 91 | ''' 92 | 93 | def __init__(self, epoch_s, tz=0.): 94 | self.epoch_s = epoch_s 95 | self.tz = tz 96 | 97 | def is_today(self): 98 | today = time_as_utime(self.tz) 99 | year1, month1, day1, _, _, _, _, _ = utime.localtime(today) 100 | year2, month2, day2, _, _, _, _, _ = utime.localtime(self.epoch_s) 101 | return ( 102 | year1 == year2 and 103 | month1 == month2 and 104 | day1 == day2 105 | ) 106 | 107 | def formatted(self, include_day=False, include_time=True): 108 | year, month, day, hours, minutes, _, _, _ = utime.localtime( 109 | self.epoch_s 110 | ) 111 | y_m_d = '{:04d}-{:02d}-{:02d} '.format(year, month, day) if include_day == True else '' 112 | suffix = 'PM' if hours >= 12 else 'AM' 113 | f_hours = hours % 12 114 | h_m = '{:02d}:{:02d} {:s}'.format(f_hours, minutes, suffix) if include_time else '' 115 | return '%s%s' % (y_m_d, h_m) 116 | 117 | @classmethod 118 | def from_str(cls, formatted): 119 | ''' 120 | Creates an instance of DateTime using a formatted string. 121 | ''' 122 | # 0123456790123456789012345 123 | # 2020-12-24T18:30:00-08:00 124 | 125 | if len(formatted) < 10: 126 | raise RuntimeError('Invalid formatted string') 127 | 128 | year = int(formatted[0:4]) 129 | month = int(formatted[5:7]) 130 | day = int(formatted[8:10]) 131 | 132 | hours = 0 133 | minutes = 0 134 | seconds = 0 135 | 136 | if len(formatted) > 10: 137 | hours = int(formatted[11:13]) 138 | minutes = int(formatted[14:16]) 139 | seconds = int(formatted[17:19]) 140 | 141 | epoch_s = utime.mktime( 142 | (year, month, day, hours, minutes, seconds, 0, 0) 143 | ) 144 | 145 | tz = 0. 146 | sign = formatted[19] if len(formatted) >= 20 else 'Z' 147 | # Multiplicative factor 148 | m = 1. 149 | hour_offset = 0 150 | minutes_offset = 0 151 | if sign != 'Z' and len(formatted[19:]) == 6: 152 | m = -1. if sign == '-' else 1 153 | hour_offset = int(formatted[20:22]) 154 | minutes_offset = int(formatted[23:]) 155 | minutes_f = minutes_offset / 60. 156 | tz = m * (hour_offset + minutes_f) 157 | 158 | return DateTime(epoch_s, tz) 159 | 160 | @classmethod 161 | def today(cls, tz=UTC_OFFSET): 162 | epoch_s = time_as_utime(tz) 163 | return DateTime(epoch_s=epoch_s) 164 | -------------------------------------------------------------------------------- /gfx_standard_font_01.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # 3 | # Copyright (c) 2018 Jonah Yolles-Murphy for Makers Anywhere! 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 13 | # all 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 21 | # THE SOFTWARE. 22 | """ 23 | `gfx_standard_font_01` 24 | ==================================================== 25 | 26 | CircuitPython pixel graphics drawing library. 27 | 28 | * Author(s): Jonah Yolles-Murphy 29 | 30 | Implementation Notes 31 | -------------------- 32 | 33 | **Hardware:** 34 | 35 | **Software and Dependencies:** 36 | 37 | * Adafruit CircuitPython firmware for the supported boards: 38 | https://github.com/adafruit/circuitpython/releases 39 | 40 | * letter format: 41 | { 'character_here' : bytearray(WIDTH,HEIGHT,right-most-data,more-bytes-here,left-most-data')} 42 | the right most bit is the top most bit of each vertical stripe of a char 43 | * Key format: 44 | keys of one length only represent one character. longer then one is either 45 | extended characters or special characters like the degree sign. 46 | all extended or special charaters have all capitalized keys. 47 | "?CHAR?" is used when an input character is not in the font dictionary 48 | 49 | """ 50 | # pylint: disable=invalid-name 51 | text_dict = { 52 | "A": bytearray(b"\x05\x07?DDD?"), 53 | "B": bytearray(b"\x05\x07\x7fAII6"), 54 | "C": bytearray(b'\x05\x07>AAA"'), 55 | "D": bytearray(b'\x05\x07\x7fAA"\x1c'), 56 | "E": bytearray(b"\x05\x07\x7fIIIA"), 57 | "F": bytearray(b"\x05\x07\x7fHH@@"), 58 | "G": bytearray(b"\x05\x07>AII."), 59 | "H": bytearray(b"\x05\x07\x7f\x08\x08\x08\x7f"), 60 | "I": bytearray(b"\x05\x07AA\x7fAA"), 61 | "J": bytearray(b"\x05\x07FA~@@"), 62 | "K": bytearray(b"\x05\x07\x7f\x08\x08t\x03"), 63 | "L": bytearray(b"\x05\x07\x7f\x01\x01\x01\x01"), 64 | "M": bytearray(b"\x05\x07\x7f \x10 \x7f"), 65 | "N": bytearray(b"\x05\x07\x7f \x1c\x02\x7f"), 66 | "O": bytearray(b"\x05\x07>AAA>"), 67 | "P": bytearray(b"\x05\x07\x7fHHH0"), 68 | "Q": bytearray(b"\x05\x07>AEB="), 69 | "R": bytearray(b"\x05\x07\x7fHLJ1"), 70 | "S": bytearray(b"\x05\x072III&"), 71 | "T": bytearray(b"\x05\x07@@\x7f@@"), 72 | "U": bytearray(b"\x05\x07~\x01\x01\x01~"), 73 | "V": bytearray(b"\x05\x07p\x0e\x01\x0ep"), 74 | "W": bytearray(b"\x05\x07|\x03\x04\x03|"), 75 | "X": bytearray(b"\x05\x07c\x14\x08\x14c"), 76 | "Y": bytearray(b"\x05\x07`\x10\x0f\x10`"), 77 | "Z": bytearray(b"\x05\x07CEIQa"), 78 | "0": bytearray(b"\x05\x07>EIQ>"), 79 | "1": bytearray(b"\x05\x07\x11!\x7f\x01\x01"), 80 | "2": bytearray(b"\x05\x07!CEI1"), 81 | "3": bytearray(b"\x05\x07FAQiF"), 82 | "4": bytearray(b"\x05\x07x\x08\x08\x08\x7f"), 83 | "5": bytearray(b"\x05\x07rQQQN"), 84 | "6": bytearray(b"\x05\x07\x1e)II\x06"), 85 | "7": bytearray(b"\x05\x07@GHP`"), 86 | "8": bytearray(b"\x05\x076III6"), 87 | "9": bytearray(b"\x05\x070IIJ<"), 88 | ")": bytearray(b"\x05\x07\x00A>\x00\x00"), 89 | "(": bytearray(b"\x05\x07\x00\x00>A\x00"), 90 | "[": bytearray(b"\x05\x07\x00\x00\x7fA\x00"), 91 | "]": bytearray(b"\x05\x07\x00A\x7f\x00\x00"), 92 | ".": bytearray(b"\x05\x07\x00\x03\x03\x00\x00"), 93 | "'": bytearray(b"\x05\x07\x00\x000\x00\x00"), 94 | ":": bytearray(b"\x05\x07\x00\x0066\x00"), 95 | "?CHAR?": bytearray(b"\x05\x07\x7f_RG\x7f"), 96 | "!": bytearray(b"\x05\x07\x00{{\x00\x00"), 97 | "?": bytearray(b"\x05\x07 @EH0"), 98 | ",": bytearray(b"\x05\x07\x00\x05\x06\x00\x00"), 99 | ";": bytearray(b"\x05\x07\x0056\x00\x00"), 100 | "/": bytearray(b"\x05\x07\x01\x06\x080@"), 101 | ">": bytearray(b"\x05\x07Ac6\x1c\x08"), 102 | "<": bytearray(b"\x05\x07\x08\x1c6cA"), 103 | "%": bytearray(b"\x05\x07af\x083C"), 104 | "@": bytearray(b"\x05\x07&IOA>"), 105 | "#": bytearray(b"\x05\x07\x14\x7f\x14\x7f\x14"), 106 | "$": bytearray(b"\x05\x072I\x7fI&"), 107 | "&": bytearray(b'\x05\x076IU"\x05'), 108 | "*": bytearray(b"\x05\x07(\x10|\x10("), 109 | "-": bytearray(b"\x05\x07\x00\x08\x08\x08\x00"), 110 | "_": bytearray(b"\x05\x07\x01\x01\x01\x01\x01"), 111 | "+": bytearray(b"\x05\x07\x08\x08>\x08\x08"), 112 | "=": bytearray(b"\x05\x07\x00\x14\x14\x14\x00"), 113 | '"': bytearray(b"\x05\x07\x00p\x00p\x00"), 114 | "`": bytearray(b"\x05\x07\x00\x00 \x10\x00"), 115 | "~": bytearray(b"\x05\x07\x08\x10\x08\x04\x08"), 116 | " ": bytearray(b"\x05\x07\x00\x00\x00\x00\x00"), 117 | "^": bytearray(b"\x05\x07\x10 @ \x10"), 118 | "NONE": bytearray(b"\x00\x07"), 119 | "BLANK": bytearray(b"\x05\x07\x00\x00\x00\x00\x00"), 120 | "BATA0": bytearray(b"\x0b\x07\x7fAAAAAAAA\x7f\x1c"), 121 | "BATA1": bytearray(b"\x0b\x07\x7fA]AAAAAA\x7f\x1c"), 122 | "BATA2": bytearray(b"\x0b\x07\x7fA]]AAAAA\x7f\x1c"), 123 | "BATA3": bytearray(b"\x0b\x07\x7fA]]]AAAA\x7f\x1c"), 124 | "BATA4": bytearray(b"\x0b\x07\x7fA]]]]AAA\x7f\x1c"), 125 | "BATA5": bytearray(b"\x0b\x07\x7fA]]]]]AA\x7f\x1c"), 126 | "BATA6": bytearray(b"\x0b\x07\x7fA]]]]]]A\x7f\x1c"), 127 | "BATACHRG": bytearray(b"\x07\x08\x7fAIYyOMIA\x7f\x1c"), 128 | "BATB0": bytearray(b"\x0b\x07\x7fAAAAAAAA\x7f\x1c"), 129 | "FULL": bytearray(b"\x05\x07\x7f\x7f\x7f\x7f\x7f"), 130 | "\n": bytearray(b"\x05\x07\x00\x00\x00\x00\x00"), 131 | "DEGREESIGN": bytearray(b"\x05\x07\x18$$\x18\x00"), 132 | } 133 | -------------------------------------------------------------------------------- /ui.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from images import CALENDAR_40_40 4 | from inkplate import Inkplate 5 | from layout import ALIGN_CENTER, ALIGN_LEFT, ALIGN_RIGHT, Column, Row 6 | 7 | ''' 8 | python pyboard.py --device /dev/ttyUSB0 -f cp layout.py text.py images.py : 9 | python pyboard.py --device /dev/ttyUSB0 ui.py 10 | ''' 11 | 12 | 13 | class UI: 14 | ''' 15 | This helps with layout of the InkPlate UI. 16 | Everything is rendered via immediate mode & everything is being re-rendered 17 | via partial updates. 18 | ''' 19 | 20 | def __init__(self): 21 | self.display = Inkplate(Inkplate.INKPLATE_1BIT) 22 | self.display.begin() 23 | self.width = self.display.width() 24 | self.height = self.display.height() 25 | self._columnar_interface() 26 | 27 | def _build_textual_interface(self): 28 | self.root = Column( 29 | layout_width=self.width, 30 | layout_height=self.height, 31 | padding=20 32 | ) 33 | # Nested Column 34 | column = Column(parent=self.root, padding=0) 35 | column.add_spacer(20) 36 | self.root.add_node(column) 37 | # Nested Row 38 | row = Row(parent=self.root, padding=10, align=ALIGN_CENTER) 39 | row.add_text_content('Test 1') 40 | row.add_text_content('Test 2') 41 | row.add_text_content('Test 3') 42 | row.add_text_content('Test 4') 43 | self.root.add_node(row) 44 | # Other text nodes 45 | self.root.add_text_content( 46 | 'Good Morning, Rahul', 47 | text_size=3 48 | ) 49 | self.root.add_text_content('Some text') 50 | self.root.add_text_content('Some more text') 51 | self.root.add_text_content( 52 | 'Medium Text', 53 | text_size=5, 54 | align=ALIGN_CENTER 55 | ) 56 | self.root.add_text_content( 57 | 'More Medium Text', 58 | text_size=5, 59 | align=ALIGN_RIGHT 60 | ) 61 | self.root.add_text_content( 62 | 'Large Text', 63 | text_size=9, 64 | align=ALIGN_CENTER 65 | ) 66 | self.root.add_text_content( 67 | 'Largest Text', 68 | text_size=10, 69 | align=ALIGN_CENTER 70 | ) 71 | 72 | def _columnar_interface(self): 73 | self.root = Row( 74 | layout_width=self.width, 75 | layout_height=self.height, 76 | padding=5, 77 | align=ALIGN_CENTER 78 | ) 79 | 80 | count = 3 81 | columns = [ 82 | Column( 83 | self.root, 84 | layout_width=self.width // count, 85 | wrap_content=False, # Fill parent 86 | padding=10, 87 | outline=True) 88 | for _ in range(count) 89 | ] 90 | for column in columns: 91 | column.add_text_content('Line 1 happens to be long', align=ALIGN_CENTER) 92 | column.add_text_content('Line 2', align=ALIGN_CENTER) 93 | column.add_text_content('Line 3', align=ALIGN_CENTER) 94 | column.add_text_content('Line 4', align=ALIGN_CENTER) 95 | column.add_text_content('Line 5', align=ALIGN_RIGHT) 96 | column.add_text_content('Line 6', align=ALIGN_LEFT) 97 | column.add_image(CALENDAR_40_40, 40, 40, align=ALIGN_CENTER) 98 | self.root.add_node(column) 99 | 100 | def _build_calendar(self): 101 | self.root = Column( 102 | layout_width=self.width, 103 | layout_height=self.height, 104 | padding=20 105 | ) 106 | header = Row( 107 | parent=self.root, 108 | layout_height=40, 109 | wrap_content=False 110 | ) 111 | header.add_text_content('Calendar', text_size=4) 112 | header.add_image(CALENDAR_40_40, 40, 40, align=ALIGN_RIGHT) 113 | content_root = Row( 114 | parent=self.root, 115 | layout_height=440, 116 | wrap_content=False, 117 | outline=True 118 | ) 119 | content = Column( 120 | parent=content_root, 121 | wrap_content=False 122 | ) 123 | content.add_spacer(10, outline=True) 124 | content_root.add_node(content) 125 | status = Row( 126 | parent=self.root, 127 | layout_height=40, 128 | wrap_content=False, 129 | outline=True 130 | ) 131 | status.add_text_content('Last updated at <>', align=ALIGN_RIGHT) 132 | self.root.add_node(header) 133 | self.root.add_node(content_root) 134 | self.root.add_node(status) 135 | 136 | def _build_auth(self): 137 | self.root = Column( 138 | layout_width=self.width, 139 | layout_height=self.height, 140 | padding=10 141 | ) 142 | header = Row( 143 | parent=self.root, 144 | layout_height=40, 145 | wrap_content=False 146 | ) 147 | header.add_text_content('Calendar', text_size=4) 148 | header.add_image(CALENDAR_40_40, 40, 40, align=ALIGN_RIGHT) 149 | content_root = Row( 150 | parent=self.root, 151 | layout_height=520, 152 | wrap_content=False, 153 | outline=True 154 | ) 155 | content = Column( 156 | parent=content_root, 157 | wrap_content=False 158 | ) 159 | content.add_spacer(10, outline=True) 160 | content.add_spacer(self.width // 4) 161 | content.add_text_content('ABCD-EFGH', text_size=6, align=ALIGN_CENTER) 162 | content.add_text_content( 163 | 'google.com/auth/code to continue', 164 | align=ALIGN_CENTER 165 | ) 166 | content_root.add_node(content) 167 | self.root.add_node(header) 168 | self.root.add_node(content_root) 169 | 170 | def draw(self): 171 | self.display.clearDisplay() 172 | self.root.draw(self.display, 0, 0) 173 | self.display.display() 174 | 175 | 176 | if __name__ == '__main__': 177 | ui = UI() 178 | ui.draw() 179 | time.sleep(5) 180 | ui._build_auth() 181 | ui.draw() 182 | time.sleep(5) 183 | ui._build_calendar() 184 | ui.draw() 185 | time.sleep(5) 186 | -------------------------------------------------------------------------------- /sdcard.py: -------------------------------------------------------------------------------- 1 | """ 2 | MicroPython driver for SD cards using SPI bus. 3 | 4 | Requires an SPI bus and a CS pin. Provides readblocks and writeblocks 5 | methods so the device can be mounted as a filesystem. 6 | 7 | Example usage on pyboard: 8 | 9 | import pyb, sdcard, os 10 | sd = sdcard.SDCard(pyb.SPI(1), pyb.Pin.board.X5) 11 | pyb.mount(sd, '/sd2') 12 | os.listdir('/') 13 | 14 | Example usage on ESP8266: 15 | 16 | import machine, sdcard, os 17 | sd = sdcard.SDCard(machine.SPI(1), machine.Pin(15)) 18 | os.mount(sd, '/sd') 19 | os.listdir('/') 20 | 21 | """ 22 | 23 | from micropython import const 24 | import time 25 | 26 | 27 | _CMD_TIMEOUT = const(100) 28 | 29 | _R1_IDLE_STATE = const(1 << 0) 30 | # R1_ERASE_RESET = const(1 << 1) 31 | _R1_ILLEGAL_COMMAND = const(1 << 2) 32 | # R1_COM_CRC_ERROR = const(1 << 3) 33 | # R1_ERASE_SEQUENCE_ERROR = const(1 << 4) 34 | # R1_ADDRESS_ERROR = const(1 << 5) 35 | # R1_PARAMETER_ERROR = const(1 << 6) 36 | _TOKEN_CMD25 = const(0xFC) 37 | _TOKEN_STOP_TRAN = const(0xFD) 38 | _TOKEN_DATA = const(0xFE) 39 | 40 | 41 | class SDCard: 42 | def __init__(self, spi, cs): 43 | self.spi = spi 44 | self.cs = cs 45 | 46 | self.cmdbuf = bytearray(6) 47 | self.dummybuf = bytearray(512) 48 | self.tokenbuf = bytearray(1) 49 | for i in range(512): 50 | self.dummybuf[i] = 0xFF 51 | self.dummybuf_memoryview = memoryview(self.dummybuf) 52 | 53 | # initialise the card 54 | self.init_card() 55 | 56 | def init_spi(self, baudrate): 57 | try: 58 | master = self.spi.MASTER 59 | except AttributeError: 60 | # on ESP8266 61 | self.spi.init(baudrate=baudrate, phase=0, polarity=0) 62 | else: 63 | # on pyboard 64 | self.spi.init(master, baudrate=baudrate, phase=0, polarity=0) 65 | 66 | def init_card(self): 67 | # init CS pin 68 | self.cs.init(self.cs.OUT, value=1) 69 | 70 | # init SPI bus; use low data rate for initialisation 71 | self.init_spi(100000) 72 | 73 | # clock card at least 100 cycles with cs high 74 | for i in range(16): 75 | self.spi.write(b"\xff") 76 | 77 | # CMD0: init card; should return _R1_IDLE_STATE (allow 5 attempts) 78 | for _ in range(5): 79 | if self.cmd(0, 0, 0x95) == _R1_IDLE_STATE: 80 | break 81 | else: 82 | raise OSError("no SD card") 83 | 84 | # CMD8: determine card version 85 | r = self.cmd(8, 0x01AA, 0x87, 4) 86 | if r == _R1_IDLE_STATE: 87 | self.init_card_v2() 88 | elif r == (_R1_IDLE_STATE | _R1_ILLEGAL_COMMAND): 89 | self.init_card_v1() 90 | else: 91 | raise OSError("couldn't determine SD card version") 92 | 93 | # get the number of sectors 94 | # CMD9: response R2 (R1 byte + 16-byte block read) 95 | if self.cmd(9, 0, 0, 0, False) != 0: 96 | raise OSError("no response from SD card") 97 | csd = bytearray(16) 98 | self.readinto(csd) 99 | if csd[0] & 0xC0 == 0x40: # CSD version 2.0 100 | self.sectors = ((csd[8] << 8 | csd[9]) + 1) * 1024 101 | elif csd[0] & 0xC0 == 0x00: # CSD version 1.0 (old, <=2GB) 102 | c_size = csd[6] & 0b11 | csd[7] << 2 | (csd[8] & 0b11000000) << 4 103 | c_size_mult = ((csd[9] & 0b11) << 1) | csd[10] >> 7 104 | self.sectors = (c_size + 1) * (2 ** (c_size_mult + 2)) 105 | else: 106 | raise OSError("SD card CSD format not supported") 107 | # print('sectors', self.sectors) 108 | 109 | # CMD16: set block length to 512 bytes 110 | if self.cmd(16, 512, 0) != 0: 111 | raise OSError("can't set 512 block size") 112 | 113 | # set to high data rate now that it's initialised 114 | self.init_spi(1320000) 115 | 116 | def init_card_v1(self): 117 | for i in range(_CMD_TIMEOUT): 118 | self.cmd(55, 0, 0) 119 | if self.cmd(41, 0, 0) == 0: 120 | self.cdv = 512 121 | # print("[SDCard] v1 card") 122 | return 123 | raise OSError("timeout waiting for v1 card") 124 | 125 | def init_card_v2(self): 126 | for i in range(_CMD_TIMEOUT): 127 | time.sleep_ms(50) 128 | self.cmd(58, 0, 0, 4) 129 | self.cmd(55, 0, 0) 130 | if self.cmd(41, 0x40000000, 0) == 0: 131 | self.cmd(58, 0, 0, 4) 132 | self.cdv = 1 133 | # print("[SDCard] v2 card") 134 | return 135 | raise OSError("timeout waiting for v2 card") 136 | 137 | def cmd(self, cmd, arg, crc, final=0, release=True, skip1=False): 138 | self.cs(0) 139 | 140 | # create and send the command 141 | buf = self.cmdbuf 142 | buf[0] = 0x40 | cmd 143 | buf[1] = arg >> 24 144 | buf[2] = arg >> 16 145 | buf[3] = arg >> 8 146 | buf[4] = arg 147 | buf[5] = crc 148 | self.spi.write(buf) 149 | 150 | if skip1: 151 | self.spi.readinto(self.tokenbuf, 0xFF) 152 | 153 | # wait for the response (response[7] == 0) 154 | for i in range(_CMD_TIMEOUT): 155 | self.spi.readinto(self.tokenbuf, 0xFF) 156 | response = self.tokenbuf[0] 157 | if not (response & 0x80): 158 | # this could be a big-endian integer that we are getting here 159 | for j in range(final): 160 | self.spi.write(b"\xff") 161 | if release: 162 | self.cs(1) 163 | self.spi.write(b"\xff") 164 | return response 165 | 166 | # timeout 167 | self.cs(1) 168 | self.spi.write(b"\xff") 169 | return -1 170 | 171 | def readinto(self, buf): 172 | self.cs(0) 173 | 174 | # read until start byte (0xff) 175 | for i in range(_CMD_TIMEOUT): 176 | self.spi.readinto(self.tokenbuf, 0xFF) 177 | if self.tokenbuf[0] == _TOKEN_DATA: 178 | break 179 | else: 180 | self.cs(1) 181 | raise OSError("timeout waiting for response") 182 | 183 | # read data 184 | mv = self.dummybuf_memoryview 185 | if len(buf) != len(mv): 186 | mv = mv[: len(buf)] 187 | self.spi.write_readinto(mv, buf) 188 | 189 | # read checksum 190 | self.spi.write(b"\xff") 191 | self.spi.write(b"\xff") 192 | 193 | self.cs(1) 194 | self.spi.write(b"\xff") 195 | 196 | def write(self, token, buf): 197 | self.cs(0) 198 | 199 | # send: start of block, data, checksum 200 | self.spi.read(1, token) 201 | self.spi.write(buf) 202 | self.spi.write(b"\xff") 203 | self.spi.write(b"\xff") 204 | 205 | # check the response 206 | if (self.spi.read(1, 0xFF)[0] & 0x1F) != 0x05: 207 | self.cs(1) 208 | self.spi.write(b"\xff") 209 | return 210 | 211 | # wait for write to finish 212 | while self.spi.read(1, 0xFF)[0] == 0: 213 | pass 214 | 215 | self.cs(1) 216 | self.spi.write(b"\xff") 217 | 218 | def write_token(self, token): 219 | self.cs(0) 220 | self.spi.read(1, token) 221 | self.spi.write(b"\xff") 222 | # wait for write to finish 223 | while self.spi.read(1, 0xFF)[0] == 0x00: 224 | pass 225 | 226 | self.cs(1) 227 | self.spi.write(b"\xff") 228 | 229 | def readblocks(self, block_num, buf): 230 | nblocks = len(buf) // 512 231 | assert nblocks and not len(buf) % 512, "Buffer length is invalid" 232 | if nblocks == 1: 233 | # CMD17: set read address for single block 234 | if self.cmd(17, block_num * self.cdv, 0, release=False) != 0: 235 | # release the card 236 | self.cs(1) 237 | raise OSError(5) # EIO 238 | # receive the data and release card 239 | self.readinto(buf) 240 | else: 241 | # CMD18: set read address for multiple blocks 242 | if self.cmd(18, block_num * self.cdv, 0, release=False) != 0: 243 | # release the card 244 | self.cs(1) 245 | raise OSError(5) # EIO 246 | offset = 0 247 | mv = memoryview(buf) 248 | while nblocks: 249 | # receive the data and release card 250 | self.readinto(mv[offset : offset + 512]) 251 | offset += 512 252 | nblocks -= 1 253 | if self.cmd(12, 0, 0xFF, skip1=True): 254 | raise OSError(5) # EIO 255 | 256 | def writeblocks(self, block_num, buf): 257 | nblocks, err = divmod(len(buf), 512) 258 | assert nblocks and not err, "Buffer length is invalid" 259 | if nblocks == 1: 260 | # CMD24: set write address for single block 261 | if self.cmd(24, block_num * self.cdv, 0) != 0: 262 | raise OSError(5) # EIO 263 | 264 | # send the data 265 | self.write(_TOKEN_DATA, buf) 266 | else: 267 | # CMD25: set write address for first block 268 | if self.cmd(25, block_num * self.cdv, 0) != 0: 269 | raise OSError(5) # EIO 270 | # send the data 271 | offset = 0 272 | mv = memoryview(buf) 273 | while nblocks: 274 | self.write(_TOKEN_CMD25, mv[offset : offset + 512]) 275 | offset += 512 276 | nblocks -= 1 277 | self.write_token(_TOKEN_STOP_TRAN) 278 | 279 | def ioctl(self, op, arg): 280 | if op == 4: # get number of blocks 281 | return self.sectors 282 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | import machine 4 | import network 5 | import ntptime 6 | 7 | from calendar_api import Calendar 8 | from config import ( 9 | CLIENT_ID, 10 | CLIENT_SECRET, 11 | DISCOVERY_ENDPOINT, 12 | SAVED_LOCATION, 13 | SCOPES, 14 | REFRESH_INTERVAL, 15 | WLAN_PASSWORD, 16 | WLAN_SSID 17 | ) 18 | from device import DeviceAuth 19 | from images import CALENDAR_40_40 20 | from inkplate import Inkplate 21 | from layout import ALIGN_CENTER, ALIGN_RIGHT, Column, Row 22 | from utils import DateTime 23 | 24 | # Shell 25 | ''' 26 | picocom /dev/ttyUSB0 -b115200 27 | ''' 28 | 29 | # Copy files. 30 | ''' 31 | python pyboard.py --device /dev/ttyUSB0 -f cp app.py boot.py config.py calendar_api.py device.py images.py layout.py path.py text.py utils.py : 32 | python pyboard.py --device /dev/ttyUSB0 app.py 33 | ''' 34 | 35 | 36 | class App: 37 | ''' 38 | The Calendar App. 39 | ''' 40 | 41 | def __init__(self): 42 | self.display = Inkplate(Inkplate.INKPLATE_1BIT) 43 | self.display.begin() 44 | self.width = self.display.width() 45 | self.height = self.display.height() 46 | # Connection state. 47 | self.connecting = False 48 | self.connected = False 49 | # Auth state 50 | self.authorizing = False 51 | self.authorized = False 52 | # Calender 53 | self.calendar = None 54 | 55 | def connect_to_network(self, notify=True): 56 | ''' 57 | Connects to WiFi, and sync-s Network time 58 | ''' 59 | 60 | if notify: 61 | self._notify('Initializing', messages=[ 62 | 'Connecting to %s' % (WLAN_SSID) 63 | ]) 64 | 65 | wlan = network.WLAN(network.STA_IF) 66 | wlan.active(True) 67 | delay = 0 68 | if not wlan.isconnected(): 69 | self.connecting = True 70 | wlan.connect(WLAN_SSID, WLAN_PASSWORD) 71 | while not wlan.isconnected() and delay < 50: 72 | time.sleep_ms(200) 73 | delay += 1 74 | 75 | if not wlan.isconnected(): 76 | message = 'Cannot connect to WiFi SSID %s' % (WLAN_SSID) 77 | self._error(message) 78 | return 79 | 80 | config = wlan.ifconfig() 81 | print('Connected with config', config) 82 | 83 | if notify: 84 | self._notify('Initializing', messages=[ 85 | 'Sync-ing real time clocks with Network' 86 | ]) 87 | 88 | print('Sync-ing network time.') 89 | delay = 0 90 | time_set = False 91 | while wlan.isconnected() and not time_set and delay < 10: 92 | try: 93 | ntptime.settime() 94 | time_set = True 95 | except Exception: 96 | time.sleep_ms(200) 97 | delay += 1 98 | 99 | self.connecting = False 100 | self.connected = wlan.isconnected() and time_set 101 | 102 | def initialize(self, notify=True): 103 | ''' 104 | Initialize App. 105 | ''' 106 | self.connect_to_network(notify=notify) 107 | if not self.connected: 108 | return 109 | 110 | self.device_auth = DeviceAuth.from_file(SAVED_LOCATION) 111 | if not self.device_auth or self.device_auth.authorized == False: 112 | # Initialize auth 113 | self.device_auth = DeviceAuth( 114 | client_id=CLIENT_ID, 115 | client_secret=CLIENT_SECRET, 116 | discovery_endpoint=DISCOVERY_ENDPOINT, 117 | scopes=SCOPES, 118 | saved_location=SAVED_LOCATION 119 | ) 120 | 121 | self.device_auth.discover() 122 | self.authorizing = True 123 | self.device_auth.authorize() 124 | user_code = self.device_auth.user_code 125 | verification_url = self.device_auth.verification_url 126 | current_attempt = 0 127 | max_attempts = 20 128 | while not self.device_auth.authorized and current_attempt < max_attempts: 129 | messages = [ 130 | '%s to continue' % (verification_url), 131 | 'Attempt %s of %s' % (current_attempt + 1, max_attempts) 132 | ] 133 | self._notify(user_code, messages=messages) 134 | self.device_auth.check_authorization_complete(max_attempts=1) 135 | time.sleep(5) # Sleep duration in seconds. 136 | current_attempt += 1 137 | 138 | if not self.device_auth.authorized: 139 | message = 'Unable to authorize the application.' 140 | self._error(message=message) 141 | return 142 | 143 | if notify: 144 | self._notify('Syncing', messages=[ 145 | 'Updating calendar events' 146 | ]) 147 | 148 | self.build_calendar_ui() 149 | print('Entering deep sleep.') 150 | machine.deepsleep(REFRESH_INTERVAL * 60 * 1000) 151 | 152 | def on_wakeup(self): 153 | ''' 154 | Execute this method after waking up from deep sleep. 155 | ''' 156 | self.initialize(notify=False) 157 | 158 | 159 | def build_calendar_ui(self): 160 | ''' 161 | Builds the actual Calendar UI after making the RESTful request. 162 | ''' 163 | if not self.device_auth.authorized: 164 | self._error('Need to authorize first.') 165 | 166 | if not self.calendar: 167 | self.calendar = Calendar(self.device_auth) 168 | 169 | events = self.calendar.events() 170 | date_today = DateTime.today() 171 | sync_at = date_today.formatted() 172 | sync_at_message = 'Last updated at %s' % (sync_at) 173 | if len(events) <= 0: 174 | messages = [ 175 | sync_at_message 176 | ] 177 | self._notify('No events.', messages=messages) 178 | else: 179 | root = Column( 180 | layout_width=self.width, 181 | layout_height=self.height, 182 | padding=10 183 | ) 184 | header = Row( 185 | parent=root, 186 | layout_height=40, 187 | wrap_content=False 188 | ) 189 | f_date_today = date_today.formatted(include_day=True, include_time=False) 190 | header.add_text_content('Today - %s' % (f_date_today), text_size=4) 191 | header.add_image(CALENDAR_40_40, 40, 40, align=ALIGN_RIGHT) 192 | content_root = Row( 193 | parent=root, 194 | layout_height=480, 195 | wrap_content=False, 196 | outline=True 197 | ) 198 | content = Column( 199 | parent=content_root, 200 | wrap_content=False 201 | ) 202 | content.add_spacer(10, outline=True) 203 | content.add_spacer(20) 204 | for event in events: 205 | summary = event.summary 206 | duration_info = None 207 | if event.start_at: 208 | include_day = not event.start_at.is_today() 209 | duration_info = 'At %s' % ( 210 | event.start_at.formatted(include_day=include_day) 211 | ) 212 | elif event.end_at: 213 | duration_info = 'Ends at %s' % ( 214 | event.end_at.formatted(include_day=True, include_time=False) 215 | ) 216 | 217 | content.add_text_content(summary) 218 | if duration_info: 219 | content.add_text_content(duration_info) 220 | content.add_spacer(height=15) 221 | content_root.add_node(content) 222 | status = Row( 223 | parent=root, 224 | layout_height=40, 225 | wrap_content=False, 226 | outline=True 227 | ) 228 | status.add_text_content(sync_at_message, align=ALIGN_RIGHT) 229 | root.add_node(header) 230 | root.add_node(content_root) 231 | root.add_node(status) 232 | self._draw(root) 233 | 234 | def _error(self, message): 235 | messages = [message] 236 | print(message) 237 | self._notify('Error', messages=messages) 238 | 239 | def _notify(self, title, messages=list()): 240 | root = Column( 241 | layout_width=self.width, 242 | layout_height=self.height, 243 | padding=10 244 | ) 245 | header = Row( 246 | parent=root, 247 | layout_height=40, 248 | wrap_content=False 249 | ) 250 | header.add_text_content('Calendar', text_size=4) 251 | header.add_image(CALENDAR_40_40, 40, 40, align=ALIGN_RIGHT) 252 | content_root = Row( 253 | parent=root, 254 | layout_height=520, 255 | wrap_content=False, 256 | outline=True 257 | ) 258 | content = Column( 259 | parent=content_root, 260 | wrap_content=False 261 | ) 262 | content.add_spacer(10, outline=True) 263 | content.add_spacer(self.width // 4) 264 | content.add_text_content(title, text_size=6, align=ALIGN_CENTER) 265 | for message in messages: 266 | content.add_text_content(message, align=ALIGN_CENTER) 267 | content_root.add_node(content) 268 | root.add_node(header) 269 | root.add_node(content_root) 270 | self._draw(root) 271 | 272 | def _draw(self, node): 273 | self.display.clearDisplay() 274 | node.draw(self.display, 0, 0) 275 | self.display.display() 276 | 277 | 278 | if __name__ == '__main__': 279 | app = App() 280 | print('Initializing app.') 281 | app.initialize() 282 | -------------------------------------------------------------------------------- /device.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import time 4 | 5 | import urequests as requests 6 | 7 | from path import exists 8 | from utils import urlencode 9 | 10 | 11 | class DeviceAuth: 12 | ''' 13 | Helps with authenticating devices with limited input capabilities 14 | per the OAuth2 device flow specification. 15 | ''' 16 | 17 | def __init__( 18 | self, 19 | client_id, 20 | client_secret, 21 | discovery_endpoint, 22 | scopes=list(), 23 | saved_location=None 24 | ): 25 | self.client_id = client_id 26 | self.client_secret = client_secret 27 | self.discovery_endpoint = discovery_endpoint 28 | self.scopes = scopes 29 | self.saved_location = saved_location 30 | 31 | self.user_code = None 32 | self.verification_url = None 33 | 34 | self._discovered = False 35 | self._authorization_started = False 36 | self._authorization_completed = False 37 | 38 | self._device_auth_endpoint = None 39 | self._token_endpoint = None 40 | self._device_code = None 41 | self._interval = None 42 | self._code_expires_in = None 43 | 44 | self._access_token = None 45 | self._token_acquired_at = None 46 | self._token_expires_in = None 47 | self._token_scope = None 48 | self._token_type = None 49 | self._refresh_token = None 50 | 51 | def discover(self): 52 | ''' 53 | Performs OAuth2 device endpoint discovery. 54 | ''' 55 | 56 | if not self._discovered: 57 | r = requests.request('GET', self.discovery_endpoint) 58 | j = r.json() 59 | self._device_auth_endpoint = j['device_authorization_endpoint'] 60 | self._token_endpoint = j['token_endpoint'] 61 | self._discovered = True 62 | r.close() 63 | 64 | saved = self.save() 65 | if not saved: 66 | print('Unable to save auth state.') 67 | 68 | 69 | def authorize(self): 70 | ''' 71 | Makes an authorization request. 72 | ''' 73 | 74 | if not self._discovered: 75 | print('Need to discover authorization and token endpoints.') 76 | return 77 | 78 | headers = {'Content-Type': 'application/x-www-form-urlencoded'} 79 | payload = { 80 | 'client_id': self.client_id, 81 | 'scope': ' '.join(self.scopes) 82 | } 83 | encoded = urlencode(payload) 84 | r = requests.request( 85 | 'POST', 86 | self._device_auth_endpoint, 87 | data=encoded, 88 | headers=headers 89 | ) 90 | j = r.json() 91 | r.close() 92 | 93 | if 'error' in j: 94 | raise RuntimeError(j['error']) 95 | 96 | self._device_code = j['device_code'] 97 | self.user_code = j['user_code'] 98 | self.verification_url = j['verification_url'] 99 | self._interval = j['interval'] 100 | self._code_expires_in = j['expires_in'] 101 | self._authorization_started = True 102 | message = 'Use code %s at %s to authorize the device.' % ( 103 | self.user_code, 104 | self.verification_url 105 | ) 106 | print(message) 107 | 108 | def check_authorization_complete(self, sleep_duration_seconds=5, max_attempts=10): 109 | ''' 110 | Polls until completion of an authorization request. 111 | ''' 112 | 113 | if not self._authorization_started: 114 | print('Start an authorization request.') 115 | return 116 | 117 | headers = {'Content-Type': 'application/x-www-form-urlencoded'} 118 | payload = { 119 | 'client_id': self.client_id, 120 | 'client_secret': self.client_secret, 121 | 'device_code': self._device_code, 122 | 'grant_type': 'urn:ietf:params:oauth:grant-type:device_code' 123 | } 124 | encoded = urlencode(payload) 125 | 126 | current_attempt = 0 127 | while not self.authorized and current_attempt < max_attempts: 128 | current_attempt = current_attempt + 1 129 | r = requests.request( 130 | 'POST', 131 | self._token_endpoint, 132 | data=encoded, 133 | headers=headers 134 | ) 135 | j = r.json() 136 | r.close() 137 | if 'error' in j: 138 | if j['error'] == 'authorization_pending': 139 | print('Pending authorization. ') 140 | time.sleep(sleep_duration_seconds) 141 | elif j['error'] == 'access_denied': 142 | print('Access denied') 143 | raise RuntimeError(j['error']) 144 | else: 145 | self._access_token = j['access_token'] 146 | self._token_acquired_at = int(time.time()) 147 | self._token_expires_in = j['expires_in'] 148 | self._token_scope = j['scope'] 149 | self._token_type = j['token_type'] 150 | self._refresh_token = j['refresh_token'] 151 | print('Completed authorization') 152 | self._authorization_completed = True 153 | saved = self.save() 154 | if not saved: 155 | print('Unable to save auth state.') 156 | 157 | @property 158 | def authorized(self): 159 | return self._authorization_completed 160 | 161 | def token(self, force_refresh=False): 162 | ''' 163 | Fetches a valid access token. 164 | ''' 165 | 166 | if not self._authorization_completed: 167 | print('Complete an authorization request') 168 | return 169 | 170 | buffer = 10 * 60 * -1 # 10 min in seconds 171 | now = int(time.time()) 172 | is_valid = now < ( 173 | self._token_acquired_at + 174 | self._token_expires_in + 175 | buffer 176 | ) 177 | if not is_valid or force_refresh: 178 | print('Token expired. Refreshing access tokens.') 179 | headers = {'Content-Type': 'application/x-www-form-urlencoded'} 180 | payload = { 181 | 'client_id': self.client_id, 182 | 'client_secret': self.client_secret, 183 | 'refresh_token': self._refresh_token, 184 | 'grant_type': 'refresh_token' 185 | } 186 | encoded = urlencode(payload) 187 | r = requests.request( 188 | 'POST', 189 | self._token_endpoint, 190 | data=encoded, 191 | headers=headers 192 | ) 193 | status_code = r.status_code 194 | j = r.json() 195 | r.close() 196 | 197 | if status_code == 400: 198 | print('Unable to refresh tokens.') 199 | raise(RuntimeError('Unable to refresh tokens.')) 200 | 201 | print('Updated access tokens.') 202 | self._access_token = j['access_token'] 203 | self._token_acquired_at = int(time.time()) 204 | self._token_expires_in = j['expires_in'] 205 | self._token_scope = j['scope'] 206 | self._token_type = j['token_type'] 207 | saved = self.save() 208 | if not saved: 209 | print('Unable to store auth state.') 210 | 211 | return self._access_token 212 | 213 | def save(self): 214 | ''' 215 | Serializes the auth state to a JSON payload and saves it in `location`. 216 | ''' 217 | 218 | if not self.saved_location: 219 | return True 220 | 221 | payload = { 222 | 'client_id': self.client_id, 223 | 'client_secret': self.client_secret, 224 | 'discovery_endpoint': self.discovery_endpoint, 225 | 'scopes': self.scopes 226 | } 227 | 228 | if self.saved_location: 229 | payload['saved_location'] = self.saved_location 230 | 231 | if self._discovered: 232 | payload['discovered'] = True 233 | payload['device_auth_endpoint'] = self._device_auth_endpoint 234 | payload['token_endpoint'] = self._token_endpoint 235 | 236 | if self.authorized: 237 | payload['authorized'] = True 238 | payload['refresh_token'] = self._refresh_token 239 | payload['access_token'] = self._access_token 240 | payload['token_acquired_at'] = self._token_acquired_at 241 | payload['token_expires_in'] = self._token_expires_in 242 | 243 | try: 244 | with open(self.saved_location, 'w') as handle: 245 | json.dump(payload, handle) 246 | print('Saved auth state.') 247 | 248 | return True 249 | except OSError as error: 250 | print('Error saving authentication state.', error) 251 | return False 252 | 253 | @classmethod 254 | def from_file(cls, location): 255 | ''' 256 | Loads authentication state from a given location. 257 | ''' 258 | if not exists(location): 259 | print('No serialized state.') 260 | return None 261 | 262 | try: 263 | with open(location, 'r') as handle: 264 | payload = json.load(handle) 265 | client_id = payload['client_id'] 266 | client_secret = payload['client_secret'] 267 | discovery_endpoint = payload['discovery_endpoint'] 268 | scopes = payload['scopes'] 269 | device_auth = DeviceAuth( 270 | client_id=client_id, 271 | client_secret=client_secret, 272 | discovery_endpoint=discovery_endpoint, 273 | scopes=scopes 274 | ) 275 | 276 | if 'saved_location' in payload: 277 | saved_location = payload['saved_location'] 278 | device_auth.saved_location = saved_location 279 | 280 | if 'discovered' in payload: 281 | device_auth_endpoint = payload['device_auth_endpoint'] 282 | token_endpoint = payload['token_endpoint'] 283 | device_auth._discovered = True 284 | device_auth._device_auth_endpoint = device_auth_endpoint 285 | device_auth._token_endpoint = token_endpoint 286 | 287 | if 'authorized' in payload: 288 | refresh_token = payload['refresh_token'] 289 | access_token = payload['access_token'] 290 | token_acquired_at = payload['token_acquired_at'] 291 | token_expires_in = payload['token_expires_in'] 292 | device_auth._authorization_completed = True 293 | device_auth._refresh_token = refresh_token 294 | device_auth._access_token = access_token 295 | device_auth._token_acquired_at = token_acquired_at 296 | device_auth._token_expires_in = token_expires_in 297 | 298 | return device_auth 299 | except Exception as error: 300 | print('Unable to create an instance of DeviceAuth.', error) 301 | try: 302 | os.remove(location) 303 | except OSError as error: 304 | # Do nothing 305 | pass 306 | 307 | return None 308 | -------------------------------------------------------------------------------- /layout.py: -------------------------------------------------------------------------------- 1 | from text import _WIDTHS, Text 2 | 3 | # Text alignments 4 | 5 | ALIGN_LEFT = 0 6 | ALIGN_CENTER = 1 7 | ALIGN_RIGHT = 2 8 | 9 | 10 | class Node: 11 | ''' 12 | A layout node. 13 | ''' 14 | 15 | def __init__( 16 | self, 17 | parent=None, 18 | layout_width=0, 19 | layout_height=0, 20 | wrap_content=False, 21 | align=ALIGN_LEFT, 22 | padding=0): 23 | 24 | self.parent = parent 25 | self.padding = padding 26 | self.wrap_content = wrap_content 27 | self.align = align 28 | 29 | if not self.parent and (layout_width == 0 or layout_height == 0): 30 | raise RuntimeError( 31 | 'Invalid constraints. Must specify parent or a size.') 32 | 33 | if self.parent and layout_width == 0: 34 | self.layout_width = self.parent.layout_width 35 | else: 36 | self.layout_width = layout_width 37 | 38 | if self.parent and layout_height == 0: 39 | self.layout_height = self.parent.layout_height 40 | else: 41 | self.layout_height = layout_height 42 | 43 | self.layout_width -= 2 * self.padding 44 | self.layout_height -= 2 * self.padding 45 | 46 | def measure(self): 47 | ''' 48 | Return the measured dimensions. 49 | ''' 50 | return None, None 51 | 52 | def draw(self, display, x, y): 53 | pass 54 | 55 | 56 | class Column(Node): 57 | ''' 58 | A simple FlowLayout. 59 | ''' 60 | 61 | def __init__( 62 | self, 63 | parent=None, 64 | layout_width=0, 65 | layout_height=0, 66 | wrap_content=True, 67 | align=ALIGN_LEFT, 68 | padding=0, 69 | outline=False 70 | ): 71 | super().__init__( 72 | parent=parent, 73 | layout_width=layout_width, 74 | layout_height=layout_height, 75 | wrap_content=wrap_content, 76 | align=align, 77 | padding=padding 78 | ) 79 | self.outline = outline 80 | self.children = list() 81 | 82 | def add_node(self, node): 83 | if isinstance(node, Node): 84 | self.children.append(node) 85 | 86 | def add_spacer(self, height, outline=False): 87 | node = Spacer(self, height, outline=outline) 88 | self.add_node(node) 89 | 90 | def add_text_content(self, content, text_size=3, padding=5, align=ALIGN_LEFT): 91 | node = TextNode.overflow( 92 | TextNode( 93 | parent=self, 94 | content=content, 95 | text_size=text_size, 96 | padding=padding, 97 | align=align 98 | ), 99 | self.layout_width 100 | ) 101 | self.add_node(node) 102 | 103 | def add_image( 104 | self, 105 | image, 106 | width, 107 | height, 108 | wrap_content=True, 109 | align=ALIGN_LEFT 110 | ): 111 | node = ImageNode( 112 | parent=self, 113 | image=image, 114 | width=width, 115 | height=height, 116 | wrap_content=wrap_content, 117 | align=align 118 | ) 119 | self.add_node(node) 120 | 121 | def measure(self): 122 | width = 0 123 | height = 0 124 | if not self.wrap_content: 125 | width = self.layout_width 126 | height = self.layout_height 127 | else: 128 | for child in self.children: 129 | w, h = child.measure() 130 | w_p = w + self.padding 131 | width = w_p if width < w_p else width 132 | height += h + self.padding 133 | 134 | return width, height 135 | 136 | def draw(self, display, x, y): 137 | # Measure once 138 | measurements = list() 139 | for child in self.children: 140 | measurements.append(child.measure()) 141 | # Adjust x, y coordinates for alignment 142 | d_x = x 143 | if self.align == ALIGN_CENTER or self.align == ALIGN_RIGHT: 144 | # Measure the children in the container 145 | # to compute the actual intrinsic widths. 146 | intrinsic_width = 0 147 | for m in measurements: 148 | w, _ = m 149 | intrinsic_width += w 150 | 151 | center_x = int(self.layout_width / 2) 152 | h_w = int(intrinsic_width / 2) 153 | if self.align == ALIGN_CENTER: 154 | d_x += (center_x - h_w) 155 | else: 156 | d_x += (self.layout_width - intrinsic_width) 157 | else: 158 | d_x += self.padding 159 | d_y = y + self.padding 160 | 161 | idx = 0 162 | for child in self.children: 163 | w, h = measurements[idx] 164 | outline = getattr(child, 'outline', False) 165 | if outline: 166 | display.drawRect(d_x, d_y, w, h, display.BLACK) 167 | child.draw(display, d_x, d_y) 168 | d_y += h + self.padding 169 | idx += 1 170 | 171 | 172 | class TextNode(Node): 173 | ''' 174 | A Text Node. 175 | ''' 176 | 177 | def __init__( 178 | self, 179 | parent, 180 | content, 181 | text_size=3, 182 | padding=5, 183 | align=ALIGN_LEFT): 184 | 185 | super().__init__(parent=parent, padding=padding, align=align) 186 | self.content = content 187 | self.text_size = text_size 188 | self.text = Text( 189 | self.content, 190 | text_size=self.text_size, 191 | padding=self.padding 192 | ) 193 | 194 | def measure(self): 195 | return self.text.measured_width(), self.text.measured_height() 196 | 197 | def draw(self, display, x, y): 198 | d_x = x 199 | d_y = y + self.padding 200 | if self.align == ALIGN_CENTER or self.align == ALIGN_RIGHT: 201 | width = self.text.measured_width() 202 | h_w = int(width / 2) 203 | center_w = int(self.layout_width / 2) 204 | if self.align == ALIGN_CENTER: 205 | d_x = x + (center_w - h_w) 206 | else: 207 | d_x = x + (self.layout_width - width) 208 | else: 209 | d_x = x + self.padding 210 | display.setTextSize(self.text_size) 211 | display.printText( 212 | d_x, 213 | d_y, 214 | self.text.content 215 | ) 216 | 217 | @classmethod 218 | def overflow(cls, node, target_width): 219 | ''' 220 | This can use a lot of improvement. 221 | ''' 222 | 223 | mw = node.text.measured_width() 224 | if mw < target_width: 225 | return node 226 | 227 | index = (target_width // _WIDTHS[node.text_size]) - 4 228 | ellipsized = node.text.content[:index] 229 | return TextNode( 230 | content='%s...' % ellipsized, 231 | parent=node.parent, 232 | text_size=node.text_size, 233 | padding=node.padding 234 | ) 235 | 236 | 237 | class Row(Node): 238 | ''' 239 | A Row. (Flow layout in horizontal direction) 240 | ''' 241 | 242 | def __init__( 243 | self, 244 | parent=None, 245 | layout_width=0, 246 | layout_height=0, 247 | wrap_content=True, 248 | align=ALIGN_LEFT, 249 | padding=0, 250 | outline=False): 251 | 252 | super().__init__( 253 | parent=parent, 254 | layout_width=layout_width, 255 | layout_height=layout_height, 256 | wrap_content=wrap_content, 257 | align=align, 258 | padding=padding 259 | ) 260 | self.outline = outline 261 | self.children = list() 262 | 263 | def measure(self): 264 | width = 0 265 | height = 0 266 | if not self.wrap_content: 267 | width = self.layout_width 268 | height = self.layout_height 269 | else: 270 | for child in self.children: 271 | w, h = child.measure() 272 | h_p = h + self.padding 273 | width += w + self.padding 274 | height = h_p if height < h_p else height 275 | 276 | return width, height 277 | 278 | def add_node(self, node): 279 | if isinstance(node, Node): 280 | self.children.append(node) 281 | 282 | def add_spacer(self, height, outline=False): 283 | node = Spacer(self, height, padding=self.padding, outline=outline) 284 | self.add_node(node) 285 | 286 | def add_text_content(self, content, text_size=3, align=ALIGN_LEFT): 287 | node = TextNode.overflow( 288 | TextNode( 289 | parent=self, 290 | content=content, 291 | text_size=text_size, 292 | align=align 293 | ), 294 | self.layout_width 295 | ) 296 | self.add_node(node) 297 | 298 | def add_image( 299 | self, 300 | image, 301 | width, 302 | height, 303 | wrap_content=True, 304 | align=ALIGN_LEFT 305 | ): 306 | node = ImageNode( 307 | parent=self, 308 | image=image, 309 | width=width, 310 | height=height, 311 | wrap_content=wrap_content, 312 | align=align 313 | ) 314 | self.add_node(node) 315 | 316 | def draw(self, display, x, y): 317 | # Measure once 318 | measurements = list() 319 | for child in self.children: 320 | measurements.append(child.measure()) 321 | 322 | # Adjust x, y coordinates for alignment 323 | d_x = x 324 | if self.align == ALIGN_CENTER or self.align == ALIGN_RIGHT: 325 | # Measure the children in the container 326 | # to compute the actual intrinsic widths. 327 | intrinsic_width = 0 328 | for m in measurements: 329 | w, _ = m 330 | intrinsic_width += w 331 | 332 | center_x = int(self.layout_width / 2) 333 | h_w = int(intrinsic_width / 2) 334 | if self.align == ALIGN_CENTER: 335 | d_x += (center_x - h_w) 336 | else: 337 | d_x += (self.layout_width - intrinsic_width) 338 | else: 339 | d_x += self.padding 340 | d_y = y + self.padding 341 | idx = 0 342 | for child in self.children: 343 | w, h = measurements[idx] 344 | outline = getattr(child, 'outline', False) 345 | if outline: 346 | display.drawRect(d_x, d_y, w, h, display.BLACK) 347 | if child.align == ALIGN_CENTER or child.align == ALIGN_RIGHT: 348 | # Alignments are always with respect to parent 349 | d_x = x 350 | child.draw(display, d_x, d_y) 351 | d_x += w + self.padding 352 | idx += 1 353 | 354 | 355 | class Spacer(Node): 356 | ''' 357 | A Spacer that represents an empty space. 358 | ''' 359 | 360 | def __init__(self, parent, height, padding=0, outline=False): 361 | # Note: This should not wrap content. 362 | super().__init__(parent=parent, padding=padding) 363 | self.width = self.layout_width 364 | self.height = height 365 | self.outline = outline 366 | 367 | def measure(self): 368 | return self.width, self.height 369 | 370 | def draw(self, display, x, y): 371 | # Does nothing 372 | pass 373 | 374 | 375 | class ImageNode(Node): 376 | ''' 377 | An Image Node 378 | ''' 379 | 380 | def __init__( 381 | self, 382 | parent, 383 | image, 384 | width, 385 | height, 386 | padding=0, 387 | wrap_content=True, 388 | align=ALIGN_LEFT): 389 | 390 | super().__init__( 391 | parent=parent, 392 | padding=padding, 393 | wrap_content=wrap_content, 394 | align=align 395 | ) 396 | self.image = image 397 | self.width = width 398 | self.height = height 399 | 400 | def measure(self): 401 | if self.wrap_content: 402 | return self.layout_width, self.layout_height 403 | else: 404 | w = self.width + self.padding 405 | h = self.height + self.padding 406 | return w, h 407 | 408 | def draw(self, display, x, y): 409 | d_x = x 410 | d_y = y + self.padding 411 | if self.align == ALIGN_CENTER or self.align == ALIGN_RIGHT: 412 | w = self.width 413 | h_w = int(w / 2) 414 | center_w = int(self.layout_width / 2) 415 | if self.align == ALIGN_CENTER: 416 | d_x += (center_w - h_w) 417 | else: 418 | d_x += (self.layout_width - w) 419 | else: 420 | d_x += self.padding 421 | display.drawBitmap( 422 | d_x, 423 | d_y, 424 | self.image, 425 | self.width, 426 | self.height 427 | ) 428 | -------------------------------------------------------------------------------- /gfx.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # 3 | # Copyright (c) 2018 Kattni Rembor for Adafruit Industries 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 13 | # all 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 21 | # THE SOFTWARE. 22 | """ 23 | `gfx` 24 | ==================================================== 25 | CircuitPython pixel graphics drawing library. 26 | * Author(s): Kattni Rembor, Tony DiCola, Jonah Yolles-Murphy, based on code by Phil Burgess 27 | Implementation Notes 28 | -------------------- 29 | **Hardware:** 30 | **Software and Dependencies:** 31 | * Adafruit CircuitPython firmware for the supported boards: 32 | https://github.com/adafruit/circuitpython/releases 33 | """ 34 | 35 | __version__ = "0.0.0-auto.0" 36 | __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_GFX.git" 37 | 38 | # pylint: disable=invalid-name 39 | class GFX: 40 | # pylint: disable=too-many-instance-attributes 41 | """Create an instance of the GFX drawing class. 42 | :param width: The width of the drawing area in pixels. 43 | :param height: The height of the drawing area in pixels. 44 | :param pixel: A function to call when a pixel is drawn on the display. This function 45 | should take at least an x and y position and then any number of optional 46 | color or other parameters. 47 | :param hline: A function to quickly draw a horizontal line on the display. 48 | This should take at least an x, y, and width parameter and 49 | any number of optional color or other parameters. 50 | :param vline: A function to quickly draw a vertical line on the display. 51 | This should take at least an x, y, and height paraemter and 52 | any number of optional color or other parameters. 53 | :param fill_rect: A funtion to quickly draw a solid rectangle with four 54 | input parameters: x,y, width, and height. Any number of other 55 | parameters for color or screen specific data. 56 | :param text: A function to quickly place text on the screen. The inputs include: 57 | x, y data(top left as starting point). 58 | :param font: An optional input to augment the default text method with a new font. 59 | The input shoudl be a properly formatted dict. 60 | """ 61 | # pylint: disable=too-many-arguments 62 | def __init__( 63 | self, 64 | width, 65 | height, 66 | pixel, 67 | hline=None, 68 | vline=None, 69 | fill_rect=None, 70 | text=None, 71 | font=None, 72 | ): 73 | # pylint: disable=too-many-instance-attributes 74 | self.width = width 75 | self.height = height 76 | self._pixel = pixel 77 | # Default to slow horizontal & vertical line implementations if no 78 | # faster versions are provided. 79 | if hline is None: 80 | self.hline = self._slow_hline 81 | else: 82 | self.hline = hline 83 | if vline is None: 84 | self.vline = self._slow_vline 85 | else: 86 | self.vline = vline 87 | if fill_rect is None: 88 | self.fill_rect = self._fill_rect 89 | else: 90 | self.fill_rect = fill_rect 91 | if text is None: 92 | self.text = self._very_slow_text 93 | # if no supplied font set to std 94 | if font is None: 95 | from gfx_standard_font_01 import ( # pylint: disable=import-outside-toplevel # changed 96 | text_dict as std_font, 97 | ) 98 | 99 | self.font = std_font 100 | self.set_text_background() 101 | else: 102 | self.font = font 103 | if not isinstance(self.font, dict): 104 | raise ValueError( 105 | "Font definitions must be contained in a dictionary object." 106 | ) 107 | del self.set_text_background 108 | 109 | else: 110 | self.text = text 111 | 112 | def pixel(self, x0, y0, *args, **kwargs): 113 | """A function to pass through in input pixel functionality.""" 114 | # This was added to mainitatn the abstrtion between gfx and the dislay library 115 | self._pixel(x0, y0, *args, **kwargs) 116 | 117 | def _slow_hline(self, x0, y0, width, *args, **kwargs): 118 | """Slow implementation of a horizontal line using pixel drawing. 119 | This is used as the default horizontal line if no faster override 120 | is provided.""" 121 | if y0 < 0 or y0 > self.height or x0 < -width or x0 > self.width: 122 | return 123 | for i in range(width): 124 | self._pixel(x0 + i, y0, *args, **kwargs) 125 | 126 | def _slow_vline(self, x0, y0, height, *args, **kwargs): 127 | """Slow implementation of a vertical line using pixel drawing. 128 | This is used as the default vertical line if no faster override 129 | is provided.""" 130 | if y0 < -height or y0 > self.height or x0 < 0 or x0 > self.width: 131 | return 132 | for i in range(height): 133 | self._pixel(x0, y0 + i, *args, **kwargs) 134 | 135 | def rect(self, x0, y0, width, height, *args, **kwargs): 136 | """Rectangle drawing function. Will draw a single pixel wide rectangle 137 | starting in the upper left x0, y0 position and width, height pixels in 138 | size.""" 139 | if y0 < -height or y0 > self.height or x0 < -width or x0 > self.width: 140 | return 141 | self.hline(x0, y0, width, *args, **kwargs) 142 | self.hline(x0, y0 + height - 1, width, *args, **kwargs) 143 | self.vline(x0, y0, height, *args, **kwargs) 144 | self.vline(x0 + width - 1, y0, height, *args, **kwargs) 145 | 146 | def _fill_rect(self, x0, y0, width, height, *args, **kwargs): 147 | """Filled rectangle drawing function. Will draw a single pixel wide 148 | rectangle starting in the upper left x0, y0 position and width, height 149 | pixels in size.""" 150 | if y0 < -height or y0 > self.height or x0 < -width or x0 > self.width: 151 | return 152 | for i in range(x0, x0 + width): 153 | self.vline(i, y0, height, *args, **kwargs) 154 | 155 | def line(self, x0, y0, x1, y1, *args, **kwargs): 156 | """Line drawing function. Will draw a single pixel wide line starting at 157 | x0, y0 and ending at x1, y1.""" 158 | steep = abs(y1 - y0) > abs(x1 - x0) 159 | if steep: 160 | x0, y0 = y0, x0 161 | x1, y1 = y1, x1 162 | if x0 > x1: 163 | x0, x1 = x1, x0 164 | y0, y1 = y1, y0 165 | dx = x1 - x0 166 | dy = abs(y1 - y0) 167 | err = dx // 2 168 | ystep = 0 169 | if y0 < y1: 170 | ystep = 1 171 | else: 172 | ystep = -1 173 | while x0 <= x1: 174 | if steep: 175 | self._pixel(y0, x0, *args, **kwargs) 176 | else: 177 | self._pixel(x0, y0, *args, **kwargs) 178 | err -= dy 179 | if err < 0: 180 | y0 += ystep 181 | err += dx 182 | x0 += 1 183 | 184 | def circle(self, x0, y0, radius, *args, **kwargs): 185 | """Circle drawing function. Will draw a single pixel wide circle with 186 | center at x0, y0 and the specified radius.""" 187 | f = 1 - radius 188 | ddF_x = 1 189 | ddF_y = -2 * radius 190 | x = 0 191 | y = radius 192 | self._pixel(x0, y0 + radius, *args, **kwargs) # bottom 193 | self._pixel(x0, y0 - radius, *args, **kwargs) # top 194 | self._pixel(x0 + radius, y0, *args, **kwargs) # right 195 | self._pixel(x0 - radius, y0, *args, **kwargs) # left 196 | while x < y: 197 | if f >= 0: 198 | y -= 1 199 | ddF_y += 2 200 | f += ddF_y 201 | x += 1 202 | ddF_x += 2 203 | f += ddF_x 204 | # angle notations are based on the unit circle and in diection of being drawn 205 | self._pixel(x0 + x, y0 + y, *args, **kwargs) # 270 to 315 206 | self._pixel(x0 - x, y0 + y, *args, **kwargs) # 270 to 255 207 | self._pixel(x0 + x, y0 - y, *args, **kwargs) # 90 to 45 208 | self._pixel(x0 - x, y0 - y, *args, **kwargs) # 90 to 135 209 | self._pixel(x0 + y, y0 + x, *args, **kwargs) # 0 to 315 210 | self._pixel(x0 - y, y0 + x, *args, **kwargs) # 180 to 225 211 | self._pixel(x0 + y, y0 - x, *args, **kwargs) # 0 to 45 212 | self._pixel(x0 - y, y0 - x, *args, **kwargs) # 180 to 135 213 | 214 | def fill_circle(self, x0, y0, radius, *args, **kwargs): 215 | """Filled circle drawing function. Will draw a filled circule with 216 | center at x0, y0 and the specified radius.""" 217 | self.vline(x0, y0 - radius, 2 * radius + 1, *args, **kwargs) 218 | f = 1 - radius 219 | ddF_x = 1 220 | ddF_y = -2 * radius 221 | x = 0 222 | y = radius 223 | while x < y: 224 | if f >= 0: 225 | y -= 1 226 | ddF_y += 2 227 | f += ddF_y 228 | x += 1 229 | ddF_x += 2 230 | f += ddF_x 231 | self.vline(x0 + x, y0 - y, 2 * y + 1, *args, **kwargs) 232 | self.vline(x0 + y, y0 - x, 2 * x + 1, *args, **kwargs) 233 | self.vline(x0 - x, y0 - y, 2 * y + 1, *args, **kwargs) 234 | self.vline(x0 - y, y0 - x, 2 * x + 1, *args, **kwargs) 235 | 236 | def triangle(self, x0, y0, x1, y1, x2, y2, *args, **kwargs): 237 | # pylint: disable=too-many-arguments 238 | """Triangle drawing function. Will draw a single pixel wide triangle 239 | around the points (x0, y0), (x1, y1), and (x2, y2).""" 240 | self.line(x0, y0, x1, y1, *args, **kwargs) 241 | self.line(x1, y1, x2, y2, *args, **kwargs) 242 | self.line(x2, y2, x0, y0, *args, **kwargs) 243 | 244 | def fill_triangle(self, x0, y0, x1, y1, x2, y2, *args, **kwargs): 245 | # pylint: disable=too-many-arguments, too-many-locals, too-many-statements, too-many-branches 246 | """Filled triangle drawing function. Will draw a filled triangle around 247 | the points (x0, y0), (x1, y1), and (x2, y2).""" 248 | if y0 > y1: 249 | y0, y1 = y1, y0 250 | x0, x1 = x1, x0 251 | if y1 > y2: 252 | y2, y1 = y1, y2 253 | x2, x1 = x1, x2 254 | if y0 > y1: 255 | y0, y1 = y1, y0 256 | x0, x1 = x1, x0 257 | a = 0 258 | b = 0 259 | y = 0 260 | last = 0 261 | if y0 == y2: 262 | a = x0 263 | b = x0 264 | if x1 < a: 265 | a = x1 266 | elif x1 > b: 267 | b = x1 268 | if x2 < a: 269 | a = x2 270 | elif x2 > b: 271 | b = x2 272 | self.hline(a, y0, b - a + 1, *args, **kwargs) 273 | return 274 | dx01 = x1 - x0 275 | dy01 = y1 - y0 276 | dx02 = x2 - x0 277 | dy02 = y2 - y0 278 | dx12 = x2 - x1 279 | dy12 = y2 - y1 280 | if dy01 == 0: 281 | dy01 = 1 282 | if dy02 == 0: 283 | dy02 = 1 284 | if dy12 == 0: 285 | dy12 = 1 286 | sa = 0 287 | sb = 0 288 | if y1 == y2: 289 | last = y1 290 | else: 291 | last = y1 - 1 292 | for y in range(y0, last + 1): 293 | a = x0 + sa // dy01 294 | b = x0 + sb // dy02 295 | sa += dx01 296 | sb += dx02 297 | if a > b: 298 | a, b = b, a 299 | self.hline(a, y, b - a + 1, *args, **kwargs) 300 | sa = dx12 * (y - y1) 301 | sb = dx02 * (y - y0) 302 | while y <= y2: 303 | a = x1 + sa // dy12 304 | b = x0 + sb // dy02 305 | sa += dx12 306 | sb += dx02 307 | if a > b: 308 | a, b = b, a 309 | self.hline(a, y, b - a + 1, *args, **kwargs) 310 | y += 1 311 | 312 | def round_rect(self, x0, y0, width, height, radius, *args, **kwargs): 313 | """Rectangle with rounded corners drawing function. 314 | This works like a regular rect though! if radius = 0 315 | Will draw the outline of a rextabgle with rounded corners with (x0,y0) at the top left""" 316 | # shift to correct for start point location 317 | x0 += radius 318 | y0 += radius 319 | 320 | # ensure that the radius will only ever half of the shortest side or less 321 | radius = int(min(radius, width / 2, height / 2)) 322 | 323 | if radius: 324 | f = 1 - radius 325 | ddF_x = 1 326 | ddF_y = -2 * radius 327 | x = 0 328 | y = radius 329 | self.vline( 330 | x0 - radius, y0, height - 2 * radius + 1, *args, **kwargs 331 | ) # left 332 | self.vline( 333 | x0 + width - radius, y0, height - 2 * radius + 1, *args, **kwargs 334 | ) # right 335 | self.hline( 336 | x0, y0 + height - radius + 1, width - 2 * radius + 1, *args, **kwargs 337 | ) # bottom 338 | self.hline(x0, y0 - radius, width - 2 * radius + 1, *args, **kwargs) # top 339 | while x < y: 340 | if f >= 0: 341 | y -= 1 342 | ddF_y += 2 343 | f += ddF_y 344 | x += 1 345 | ddF_x += 2 346 | f += ddF_x 347 | # angle notations are based on the unit circle and in diection of being drawn 348 | 349 | # top left 350 | self._pixel(x0 - y, y0 - x, *args, **kwargs) # 180 to 135 351 | self._pixel(x0 - x, y0 - y, *args, **kwargs) # 90 to 135 352 | # top right 353 | self._pixel( 354 | x0 + x + width - 2 * radius, y0 - y, *args, **kwargs 355 | ) # 90 to 45 356 | self._pixel( 357 | x0 + y + width - 2 * radius, y0 - x, *args, **kwargs 358 | ) # 0 to 45 359 | # bottom right 360 | self._pixel( 361 | x0 + y + width - 2 * radius, 362 | y0 + x + height - 2 * radius, 363 | *args, 364 | **kwargs, 365 | ) # 0 to 315 366 | self._pixel( 367 | x0 + x + width - 2 * radius, 368 | y0 + y + height - 2 * radius, 369 | *args, 370 | **kwargs, 371 | ) # 270 to 315 372 | # bottom left 373 | self._pixel( 374 | x0 - x, y0 + y + height - 2 * radius, *args, **kwargs 375 | ) # 270 to 255 376 | self._pixel( 377 | x0 - y, y0 + x + height - 2 * radius, *args, **kwargs 378 | ) # 180 to 225 379 | 380 | def fill_round_rect(self, x0, y0, width, height, radius, *args, **kwargs): 381 | """Filled circle drawing function. Will draw a filled circule with 382 | center at x0, y0 and the specified radius.""" 383 | # shift to correct for start point location 384 | x0 += radius 385 | y0 += radius 386 | 387 | # ensure that the radius will only ever half of the shortest side or less 388 | radius = int(min(radius, width / 2, height / 2)) 389 | 390 | self.fill_rect( 391 | x0, y0 - radius, width - 2 * radius + 2, height + 2, *args, **kwargs 392 | ) 393 | 394 | if radius: 395 | f = 1 - radius 396 | ddF_x = 1 397 | ddF_y = -2 * radius 398 | x = 0 399 | y = radius 400 | while x < y: 401 | if f >= 0: 402 | y -= 1 403 | ddF_y += 2 404 | f += ddF_y 405 | x += 1 406 | ddF_x += 2 407 | f += ddF_x 408 | # part notation starts with 0 on left and 1 on right, and direction is noted 409 | # top left 410 | self.vline( 411 | x0 - y, y0 - x, 2 * x + 1 + height - 2 * radius, *args, **kwargs 412 | ) # 0 to .25 413 | self.vline( 414 | x0 - x, y0 - y, 2 * y + 1 + height - 2 * radius, *args, **kwargs 415 | ) # .5 to .25 416 | # top right 417 | self.vline( 418 | x0 + x + width - 2 * radius, 419 | y0 - y, 420 | 2 * y + 1 + height - 2 * radius, 421 | *args, 422 | **kwargs, 423 | ) # .5 to .75 424 | self.vline( 425 | x0 + y + width - 2 * radius, 426 | y0 - x, 427 | 2 * x + 1 + height - 2 * radius, 428 | *args, 429 | **kwargs, 430 | ) # 1 to .75 431 | 432 | def _place_char(self, x0, y0, char, size, *args, **kwargs): 433 | """A sub class used for placing a single character on the screen""" 434 | # pylint: disable=undefined-loop-variable 435 | arr = self.font[char] 436 | width = arr[0] 437 | height = arr[1] 438 | # extract the char section of the data 439 | data = arr[2:] 440 | for x in range(width): 441 | for y in range(height): 442 | bit = bool(data[x] & 2 ** y) 443 | # char pixel 444 | if bit: 445 | self.fill_rect( 446 | size * x + x0, 447 | size * (height - y - 1) + y0, 448 | size, 449 | size, 450 | *args, 451 | **kwargs, 452 | ) 453 | # else background pixel 454 | else: 455 | try: 456 | self.fill_rect( 457 | size * x + x0, 458 | size * (height - y - 1) + y0, 459 | size, 460 | size, 461 | *self.text_bkgnd_args, 462 | **self.text_bkgnd_kwargs, 463 | ) 464 | except TypeError: 465 | pass 466 | del arr, width, height, data, x, y, x0, y0, char, size 467 | 468 | def _very_slow_text(self, x0, y0, string, size, *args, **kwargs): 469 | """a function to place text on the display.(temporary) 470 | to use special characters put "__" on either side of the desired characters. 471 | letter format: 472 | {'character_here' : bytearray(b',WIDTH,HEIGHT,right-most-data, 473 | more-bytes-here,left-most-data') ,} 474 | (replace the "," with backslashes!!) 475 | each byte: 476 | | lower most bit(lowest on display) 477 | V 478 | x0110100 479 | ^c 480 | | top most bit (highest on display)""" 481 | 482 | x_roll = x0 # rolling x 483 | y_roll = y0 # rolling y 484 | 485 | # highest_height = 0#wrap 486 | sep_string = string.split("__") 487 | 488 | for chunk in sep_string: 489 | # print(chunk) 490 | try: 491 | self._place_char(x_roll, y_roll, chunk, size, *args, **kwargs) 492 | x_roll += size * self.font[chunk][0] + size 493 | # highest_height = max(highest_height, size*self.font[chunk][1] + 1) #wrap 494 | except KeyError: 495 | while chunk: 496 | char = chunk[0] 497 | 498 | # make sure something is sent even if not in font dict 499 | try: 500 | self._place_char(x_roll, y_roll, char, size, *args, **kwargs) 501 | except KeyError: 502 | self._place_char( 503 | x_roll, y_roll, "?CHAR?", size, *args, **kwargs 504 | ) 505 | char = "?CHAR?" 506 | 507 | x_roll += size * self.font[char][0] 508 | 509 | # gap between letters 510 | try: 511 | self.fill_rect( 512 | x_roll, 513 | y_roll, 514 | size, 515 | size * self.font[char][1], 516 | *self.text_bkgnd_args, 517 | **self.text_bkgnd_kwargs, 518 | ) 519 | except TypeError: 520 | pass 521 | 522 | x_roll += size 523 | # highest_height = max(highest_height, size*self.font[char][1] + 1) #wrap 524 | chunk = chunk[1:] # wrap 525 | # if (x_roll >= self.width) or (chunk[0:2] == """\n"""): #wrap 526 | # self._text(x0,y0+highest_height,"__".join(sep_string),size) #wrap 527 | # print(highest_height) #wrap 528 | 529 | def set_text_background(self, *args, **kwargs): 530 | """A method to change the background color of text, input any and all color paramsself. 531 | run without any inputs to return to "clear" background 532 | """ 533 | self.text_bkgnd_args = args 534 | self.text_bkgnd_kwargs = kwargs 535 | 536 | # pylint: enable=too-many-arguments 537 | -------------------------------------------------------------------------------- /pyboard.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # This file is part of the MicroPython project, http://micropython.org/ 4 | # 5 | # The MIT License (MIT) 6 | # 7 | # Copyright (c) 2014-2019 Damien P. George 8 | # Copyright (c) 2017 Paul Sokolovsky 9 | # 10 | # Permission is hereby granted, free of charge, to any person obtaining a copy 11 | # of this software and associated documentation files (the "Software"), to deal 12 | # in the Software without restriction, including without limitation the rights 13 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | # copies of the Software, and to permit persons to whom the Software is 15 | # furnished to do so, subject to the following conditions: 16 | # 17 | # The above copyright notice and this permission notice shall be included in 18 | # all copies or substantial portions of the Software. 19 | # 20 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 26 | # THE SOFTWARE. 27 | 28 | """ 29 | pyboard interface 30 | 31 | This module provides the Pyboard class, used to communicate with and 32 | control a MicroPython device over a communication channel. Both real 33 | boards and emulated devices (e.g. running in QEMU) are supported. 34 | Various communication channels are supported, including a serial 35 | connection, telnet-style network connection, external process 36 | connection. 37 | 38 | Example usage: 39 | 40 | import pyboard 41 | pyb = pyboard.Pyboard('/dev/ttyACM0') 42 | 43 | Or: 44 | 45 | pyb = pyboard.Pyboard('192.168.1.1') 46 | 47 | Then: 48 | 49 | pyb.enter_raw_repl() 50 | pyb.exec('import pyb') 51 | pyb.exec('pyb.LED(1).on()') 52 | pyb.exit_raw_repl() 53 | 54 | Note: if using Python2 then pyb.exec must be written as pyb.exec_. 55 | To run a script from the local machine on the board and print out the results: 56 | 57 | import pyboard 58 | pyboard.execfile('test.py', device='/dev/ttyACM0') 59 | 60 | This script can also be run directly. To execute a local script, use: 61 | 62 | ./pyboard.py test.py 63 | 64 | Or: 65 | 66 | python pyboard.py test.py 67 | 68 | """ 69 | 70 | import sys 71 | import time 72 | import os 73 | import ast 74 | 75 | try: 76 | stdout = sys.stdout.buffer 77 | except AttributeError: 78 | # Python2 doesn't have buffer attr 79 | stdout = sys.stdout 80 | 81 | 82 | def stdout_write_bytes(b): 83 | b = b.replace(b"\x04", b"") 84 | stdout.write(b) 85 | stdout.flush() 86 | 87 | 88 | class PyboardError(Exception): 89 | pass 90 | 91 | 92 | class TelnetToSerial: 93 | def __init__(self, ip, user, password, read_timeout=None): 94 | self.tn = None 95 | import telnetlib 96 | 97 | self.tn = telnetlib.Telnet(ip, timeout=15) 98 | self.read_timeout = read_timeout 99 | if b"Login as:" in self.tn.read_until(b"Login as:", timeout=read_timeout): 100 | self.tn.write(bytes(user, "ascii") + b"\r\n") 101 | 102 | if b"Password:" in self.tn.read_until(b"Password:", timeout=read_timeout): 103 | # needed because of internal implementation details of the telnet server 104 | time.sleep(0.2) 105 | self.tn.write(bytes(password, "ascii") + b"\r\n") 106 | 107 | if b"for more information." in self.tn.read_until( 108 | b'Type "help()" for more information.', timeout=read_timeout 109 | ): 110 | # login successful 111 | from collections import deque 112 | 113 | self.fifo = deque() 114 | return 115 | 116 | raise PyboardError("Failed to establish a telnet connection with the board") 117 | 118 | def __del__(self): 119 | self.close() 120 | 121 | def close(self): 122 | if self.tn: 123 | self.tn.close() 124 | 125 | def read(self, size=1): 126 | while len(self.fifo) < size: 127 | timeout_count = 0 128 | data = self.tn.read_eager() 129 | if len(data): 130 | self.fifo.extend(data) 131 | timeout_count = 0 132 | else: 133 | time.sleep(0.25) 134 | if ( 135 | self.read_timeout is not None 136 | and timeout_count > 4 * self.read_timeout 137 | ): 138 | break 139 | timeout_count += 1 140 | 141 | data = b"" 142 | while len(data) < size and len(self.fifo) > 0: 143 | data += bytes([self.fifo.popleft()]) 144 | return data 145 | 146 | def write(self, data): 147 | self.tn.write(data) 148 | return len(data) 149 | 150 | def inWaiting(self): 151 | n_waiting = len(self.fifo) 152 | if not n_waiting: 153 | data = self.tn.read_eager() 154 | self.fifo.extend(data) 155 | return len(data) 156 | else: 157 | return n_waiting 158 | 159 | 160 | class ProcessToSerial: 161 | "Execute a process and emulate serial connection using its stdin/stdout." 162 | 163 | def __init__(self, cmd): 164 | import subprocess 165 | 166 | self.subp = subprocess.Popen( 167 | cmd, 168 | bufsize=0, 169 | shell=True, 170 | preexec_fn=os.setsid, 171 | stdin=subprocess.PIPE, 172 | stdout=subprocess.PIPE, 173 | ) 174 | 175 | # Initially was implemented with selectors, but that adds Python3 176 | # dependency. However, there can be race conditions communicating 177 | # with a particular child process (like QEMU), and selectors may 178 | # still work better in that case, so left inplace for now. 179 | # 180 | # import selectors 181 | # self.sel = selectors.DefaultSelector() 182 | # self.sel.register(self.subp.stdout, selectors.EVENT_READ) 183 | 184 | import select 185 | 186 | self.poll = select.poll() 187 | self.poll.register(self.subp.stdout.fileno()) 188 | 189 | def close(self): 190 | import signal 191 | 192 | os.killpg(os.getpgid(self.subp.pid), signal.SIGTERM) 193 | 194 | def read(self, size=1): 195 | data = b"" 196 | while len(data) < size: 197 | data += self.subp.stdout.read(size - len(data)) 198 | return data 199 | 200 | def write(self, data): 201 | self.subp.stdin.write(data) 202 | return len(data) 203 | 204 | def inWaiting(self): 205 | # res = self.sel.select(0) 206 | res = self.poll.poll(0) 207 | if res: 208 | return 1 209 | return 0 210 | 211 | 212 | class ProcessPtyToTerminal: 213 | """Execute a process which creates a PTY and prints slave PTY as 214 | first line of its output, and emulate serial connection using 215 | this PTY.""" 216 | 217 | def __init__(self, cmd): 218 | import subprocess 219 | import re 220 | import serial 221 | 222 | self.subp = subprocess.Popen( 223 | cmd.split(), 224 | bufsize=0, 225 | shell=False, 226 | preexec_fn=os.setsid, 227 | stdin=subprocess.PIPE, 228 | stdout=subprocess.PIPE, 229 | stderr=subprocess.PIPE, 230 | ) 231 | pty_line = self.subp.stderr.readline().decode("utf-8") 232 | m = re.search(r"/dev/pts/[0-9]+", pty_line) 233 | if not m: 234 | print("Error: unable to find PTY device in startup line:", pty_line) 235 | self.close() 236 | sys.exit(1) 237 | pty = m.group() 238 | # rtscts, dsrdtr params are to workaround pyserial bug: 239 | # http://stackoverflow.com/questions/34831131/pyserial-does-not-play-well-with-virtual-port 240 | self.ser = serial.Serial(pty, interCharTimeout=1, rtscts=True, dsrdtr=True) 241 | 242 | def close(self): 243 | import signal 244 | 245 | os.killpg(os.getpgid(self.subp.pid), signal.SIGTERM) 246 | 247 | def read(self, size=1): 248 | return self.ser.read(size) 249 | 250 | def write(self, data): 251 | return self.ser.write(data) 252 | 253 | def inWaiting(self): 254 | return self.ser.inWaiting() 255 | 256 | 257 | class Pyboard: 258 | def __init__( 259 | self, device, baudrate=115200, user="micro", password="python", wait=0 260 | ): 261 | if device.startswith("exec:"): 262 | self.serial = ProcessToSerial(device[len("exec:") :]) 263 | elif device.startswith("execpty:"): 264 | self.serial = ProcessPtyToTerminal(device[len("qemupty:") :]) 265 | elif ( 266 | device 267 | and device[0].isdigit() 268 | and device[-1].isdigit() 269 | and device.count(".") == 3 270 | ): 271 | # device looks like an IP address 272 | self.serial = TelnetToSerial(device, user, password, read_timeout=10) 273 | else: 274 | import serial 275 | 276 | delayed = False 277 | for attempt in range(wait + 1): 278 | try: 279 | self.serial = serial.Serial( 280 | device, baudrate=baudrate, interCharTimeout=1 281 | ) 282 | break 283 | except (OSError, IOError): # Py2 and Py3 have different errors 284 | if wait == 0: 285 | continue 286 | if attempt == 0: 287 | sys.stdout.write("Waiting {} seconds for pyboard ".format(wait)) 288 | delayed = True 289 | time.sleep(1) 290 | sys.stdout.write(".") 291 | sys.stdout.flush() 292 | else: 293 | if delayed: 294 | print("") 295 | raise PyboardError("failed to access " + device) 296 | if delayed: 297 | print("") 298 | 299 | def close(self): 300 | self.serial.close() 301 | 302 | def read_until(self, min_num_bytes, ending, timeout=10, data_consumer=None): 303 | # if data_consumer is used then data is not accumulated and the ending must be 1 byte long 304 | assert data_consumer is None or len(ending) == 1 305 | 306 | data = self.serial.read(min_num_bytes) 307 | if data_consumer: 308 | data_consumer(data) 309 | timeout_count = 0 310 | while True: 311 | if data.endswith(ending): 312 | break 313 | elif self.serial.inWaiting() > 0: 314 | new_data = self.serial.read(1) 315 | if data_consumer: 316 | data_consumer(new_data) 317 | data = new_data 318 | else: 319 | data = data + new_data 320 | timeout_count = 0 321 | else: 322 | timeout_count += 1 323 | if timeout is not None and timeout_count >= 100 * timeout: 324 | break 325 | time.sleep(0.01) 326 | return data 327 | 328 | def enter_raw_repl(self): 329 | self.serial.write(b"\r\x03\x03") # ctrl-C twice: interrupt any running program 330 | 331 | # flush input (without relying on serial.flushInput()) 332 | n = self.serial.inWaiting() 333 | while n > 0: 334 | self.serial.read(n) 335 | n = self.serial.inWaiting() 336 | 337 | self.serial.write(b"\r\x01") # ctrl-A: enter raw REPL 338 | data = self.read_until(1, b"raw REPL; CTRL-B to exit\r\n>") 339 | if not data.endswith(b"raw REPL; CTRL-B to exit\r\n>"): 340 | print(data) 341 | raise PyboardError("could not enter raw repl") 342 | 343 | self.serial.write(b"\x04") # ctrl-D: soft reset 344 | data = self.read_until(1, b"soft reboot\r\n") 345 | if not data.endswith(b"soft reboot\r\n"): 346 | print(data) 347 | raise PyboardError("could not enter raw repl") 348 | # By splitting this into 2 reads, it allows boot.py to print stuff, 349 | # which will show up after the soft reboot and before the raw REPL. 350 | data = self.read_until(1, b"raw REPL; CTRL-B to exit\r\n") 351 | if not data.endswith(b"raw REPL; CTRL-B to exit\r\n"): 352 | print(data) 353 | raise PyboardError("could not enter raw repl") 354 | 355 | def exit_raw_repl(self): 356 | self.serial.write(b"\r\x02") # ctrl-B: enter friendly REPL 357 | 358 | def follow(self, timeout, data_consumer=None): 359 | # wait for normal output 360 | data = self.read_until(1, b"\x04", timeout=timeout, data_consumer=data_consumer) 361 | if not data.endswith(b"\x04"): 362 | raise PyboardError("timeout waiting for first EOF reception") 363 | data = data[:-1] 364 | 365 | # wait for error output 366 | data_err = self.read_until(1, b"\x04", timeout=timeout) 367 | if not data_err.endswith(b"\x04"): 368 | raise PyboardError("timeout waiting for second EOF reception") 369 | data_err = data_err[:-1] 370 | 371 | # return normal and error output 372 | return data, data_err 373 | 374 | def exec_raw_no_follow(self, command): 375 | if isinstance(command, bytes): 376 | command_bytes = command 377 | else: 378 | command_bytes = bytes(command, encoding="utf8") 379 | 380 | # check we have a prompt 381 | data = self.read_until(1, b">") 382 | if not data.endswith(b">"): 383 | raise PyboardError("could not enter raw repl") 384 | 385 | # write command 386 | for i in range(0, len(command_bytes), 256): 387 | self.serial.write(command_bytes[i : min(i + 256, len(command_bytes))]) 388 | time.sleep(0.01) 389 | self.serial.write(b"\x04") 390 | 391 | # check if we could exec command 392 | data = self.serial.read(2) 393 | if data != b"OK": 394 | raise PyboardError("could not exec command (response: %r)" % data) 395 | 396 | def exec_raw(self, command, timeout=10, data_consumer=None): 397 | self.exec_raw_no_follow(command) 398 | return self.follow(timeout, data_consumer) 399 | 400 | def eval(self, expression): 401 | ret = self.exec_("print({})".format(expression)) 402 | ret = ret.strip() 403 | return ret 404 | 405 | def exec_(self, command, data_consumer=None): 406 | ret, ret_err = self.exec_raw(command, data_consumer=data_consumer) 407 | if ret_err: 408 | raise PyboardError("exception", ret, ret_err) 409 | return ret 410 | 411 | def execfile(self, filename): 412 | with open(filename, "rb") as f: 413 | pyfile = f.read() 414 | return self.exec_(pyfile) 415 | 416 | def get_time(self): 417 | t = str(self.eval("pyb.RTC().datetime()"), encoding="utf8")[1:-1].split(", ") 418 | return int(t[4]) * 3600 + int(t[5]) * 60 + int(t[6]) 419 | 420 | def fs_ls(self, src): 421 | cmd = ( 422 | "import uos\nfor f in uos.ilistdir(%s):\n" 423 | " print('{:12} {}{}'.format(f[3]if len(f)>3 else 0,f[0],'/'if f[1]&0x4000 else ''))" 424 | % (("'%s'" % src) if src else "") 425 | ) 426 | self.exec_(cmd, data_consumer=stdout_write_bytes) 427 | 428 | def fs_cat(self, src, chunk_size=256): 429 | cmd = ( 430 | "with open('%s') as f:\n while 1:\n" 431 | " b=f.read(%u)\n if not b:break\n print(b,end='')" % (src, chunk_size) 432 | ) 433 | self.exec_(cmd, data_consumer=stdout_write_bytes) 434 | 435 | def fs_get(self, src, dest, chunk_size=256): 436 | self.exec_("f=open('%s','rb')\nr=f.read" % src) 437 | with open(dest, "wb") as f: 438 | while True: 439 | data = bytearray() 440 | self.exec_( 441 | "print(r(%u))" % chunk_size, data_consumer=lambda d: data.extend(d) 442 | ) 443 | assert data.endswith(b"\r\n\x04") 444 | try: 445 | data = ast.literal_eval(str(data[:-3], "ascii")) 446 | if not isinstance(data, bytes): 447 | raise ValueError("Not bytes") 448 | except (UnicodeError, ValueError) as e: 449 | raise PyboardError( 450 | "fs_get: Could not interpret received data: %s" % str(e) 451 | ) 452 | if not data: 453 | break 454 | f.write(data) 455 | self.exec_("f.close()") 456 | 457 | def fs_put(self, src, dest, chunk_size=256): 458 | self.exec_("f=open('%s','wb')\nw=f.write" % dest) 459 | with open(src, "rb") as f: 460 | while True: 461 | data = f.read(chunk_size) 462 | if not data: 463 | break 464 | if sys.version_info < (3,): 465 | self.exec_("w(b" + repr(data) + ")") 466 | else: 467 | self.exec_("w(" + repr(data) + ")") 468 | self.exec_("f.close()") 469 | 470 | def fs_mkdir(self, dir): 471 | self.exec_("import uos\nuos.mkdir('%s')" % dir) 472 | 473 | def fs_rmdir(self, dir): 474 | self.exec_("import uos\nuos.rmdir('%s')" % dir) 475 | 476 | def fs_rm(self, src): 477 | self.exec_("import uos\nuos.remove('%s')" % src) 478 | 479 | 480 | # in Python2 exec is a keyword so one must use "exec_" 481 | # but for Python3 we want to provide the nicer version "exec" 482 | setattr(Pyboard, "exec", Pyboard.exec_) 483 | 484 | 485 | def execfile( 486 | filename, device="/dev/ttyACM0", baudrate=115200, user="micro", password="python" 487 | ): 488 | pyb = Pyboard(device, baudrate, user, password) 489 | pyb.enter_raw_repl() 490 | output = pyb.execfile(filename) 491 | stdout_write_bytes(output) 492 | pyb.exit_raw_repl() 493 | pyb.close() 494 | 495 | 496 | def filesystem_command(pyb, args): 497 | def fname_remote(src): 498 | if src.startswith(":"): 499 | src = src[1:] 500 | return src 501 | 502 | def fname_cp_dest(src, dest): 503 | src = src.rsplit("/", 1)[-1] 504 | if dest is None or dest == "": 505 | dest = src 506 | elif dest == ".": 507 | dest = "./" + src 508 | elif dest.endswith("/"): 509 | dest += src 510 | return dest 511 | 512 | cmd = args[0] 513 | args = args[1:] 514 | try: 515 | if cmd == "cp": 516 | srcs = args[:-1] 517 | dest = args[-1] 518 | if srcs[0].startswith("./") or dest.startswith(":"): 519 | op = pyb.fs_put 520 | fmt = "cp %s :%s" 521 | dest = fname_remote(dest) 522 | else: 523 | op = pyb.fs_get 524 | fmt = "cp :%s %s" 525 | for src in srcs: 526 | src = fname_remote(src) 527 | dest2 = fname_cp_dest(src, dest) 528 | print(fmt % (src, dest2)) 529 | op(src, dest2) 530 | else: 531 | op = { 532 | "ls": pyb.fs_ls, 533 | "cat": pyb.fs_cat, 534 | "mkdir": pyb.fs_mkdir, 535 | "rmdir": pyb.fs_rmdir, 536 | "rm": pyb.fs_rm, 537 | }[cmd] 538 | if cmd == "ls" and not args: 539 | args = [""] 540 | for src in args: 541 | src = fname_remote(src) 542 | print("%s :%s" % (cmd, src)) 543 | op(src) 544 | except PyboardError as er: 545 | print(str(er.args[2], "ascii")) 546 | pyb.exit_raw_repl() 547 | pyb.close() 548 | sys.exit(1) 549 | 550 | 551 | _injected_import_hook_code = """\ 552 | import uos, uio 553 | class _FS: 554 | class File(uio.IOBase): 555 | def __init__(self): 556 | self.off = 0 557 | def ioctl(self, request, arg): 558 | return 0 559 | def readinto(self, buf): 560 | buf[:] = memoryview(_injected_buf)[self.off:self.off + len(buf)] 561 | self.off += len(buf) 562 | return len(buf) 563 | mount = umount = chdir = lambda *args: None 564 | def stat(self, path): 565 | if path == '_injected.mpy': 566 | return tuple(0 for _ in range(10)) 567 | else: 568 | raise OSError(-2) # ENOENT 569 | def open(self, path, mode): 570 | return self.File() 571 | uos.mount(_FS(), '/_') 572 | uos.chdir('/_') 573 | from _injected import * 574 | uos.umount('/_') 575 | del _injected_buf, _FS 576 | """ 577 | 578 | 579 | def main(): 580 | import argparse 581 | 582 | cmd_parser = argparse.ArgumentParser(description="Run scripts on the pyboard.") 583 | cmd_parser.add_argument( 584 | "-d", 585 | "--device", 586 | default=os.environ.get("PYBOARD_DEVICE", "/dev/ttyACM0"), 587 | help="the serial device or the IP address of the pyboard", 588 | ) 589 | cmd_parser.add_argument( 590 | "-b", 591 | "--baudrate", 592 | default=os.environ.get("PYBOARD_BAUDRATE", "115200"), 593 | help="the baud rate of the serial device", 594 | ) 595 | cmd_parser.add_argument( 596 | "-u", "--user", default="micro", help="the telnet login username" 597 | ) 598 | cmd_parser.add_argument( 599 | "-p", "--password", default="python", help="the telnet login password" 600 | ) 601 | cmd_parser.add_argument("-c", "--command", help="program passed in as string") 602 | cmd_parser.add_argument( 603 | "-w", 604 | "--wait", 605 | default=0, 606 | type=int, 607 | help="seconds to wait for USB connected board to become available", 608 | ) 609 | group = cmd_parser.add_mutually_exclusive_group() 610 | group.add_argument( 611 | "--follow", 612 | action="store_true", 613 | help="follow the output after running the scripts [default if no scripts given]", 614 | ) 615 | group.add_argument( 616 | "--no-follow", 617 | action="store_true", 618 | help="Do not follow the output after running the scripts.", 619 | ) 620 | cmd_parser.add_argument( 621 | "-f", "--filesystem", action="store_true", help="perform a filesystem action" 622 | ) 623 | cmd_parser.add_argument("files", nargs="*", help="input files") 624 | args = cmd_parser.parse_args() 625 | 626 | # open the connection to the pyboard 627 | try: 628 | pyb = Pyboard(args.device, args.baudrate, args.user, args.password, args.wait) 629 | except PyboardError as er: 630 | print(er) 631 | sys.exit(1) 632 | 633 | # run any command or file(s) 634 | if args.command is not None or args.filesystem or len(args.files): 635 | # we must enter raw-REPL mode to execute commands 636 | # this will do a soft-reset of the board 637 | try: 638 | pyb.enter_raw_repl() 639 | except PyboardError as er: 640 | print(er) 641 | pyb.close() 642 | sys.exit(1) 643 | 644 | def execbuffer(buf): 645 | try: 646 | if args.no_follow: 647 | pyb.exec_raw_no_follow(buf) 648 | ret_err = None 649 | else: 650 | ret, ret_err = pyb.exec_raw( 651 | buf, timeout=None, data_consumer=stdout_write_bytes 652 | ) 653 | except PyboardError as er: 654 | print(er) 655 | pyb.close() 656 | sys.exit(1) 657 | except KeyboardInterrupt: 658 | sys.exit(1) 659 | if ret_err: 660 | pyb.exit_raw_repl() 661 | pyb.close() 662 | stdout_write_bytes(ret_err) 663 | sys.exit(1) 664 | 665 | # do filesystem commands, if given 666 | if args.filesystem: 667 | filesystem_command(pyb, args.files) 668 | del args.files[:] 669 | 670 | # run the command, if given 671 | if args.command is not None: 672 | execbuffer(args.command.encode("utf-8")) 673 | 674 | # run any files 675 | for filename in args.files: 676 | with open(filename, "rb") as f: 677 | pyfile = f.read() 678 | if filename.endswith(".mpy") and pyfile[0] == ord("M"): 679 | pyb.exec_("_injected_buf=" + repr(pyfile)) 680 | pyfile = _injected_import_hook_code 681 | execbuffer(pyfile) 682 | 683 | # exiting raw-REPL just drops to friendly-REPL mode 684 | pyb.exit_raw_repl() 685 | 686 | # if asked explicitly, or no files given, then follow the output 687 | if args.follow or ( 688 | args.command is None and not args.filesystem and len(args.files) == 0 689 | ): 690 | try: 691 | ret, ret_err = pyb.follow(timeout=None, data_consumer=stdout_write_bytes) 692 | except PyboardError as er: 693 | print(er) 694 | sys.exit(1) 695 | except KeyboardInterrupt: 696 | sys.exit(1) 697 | if ret_err: 698 | pyb.close() 699 | stdout_write_bytes(ret_err) 700 | sys.exit(1) 701 | 702 | # close the connection to the pyboard 703 | pyb.close() 704 | 705 | 706 | if __name__ == "__main__": 707 | main() 708 | -------------------------------------------------------------------------------- /image.py: -------------------------------------------------------------------------------- 1 | image = bytearray( 2 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0f\xff\xff\xf8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x3f\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\xff\xff\xff\xff\xe0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\xff\xff\xff\xff\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x3f\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xff\xff\xff\xff\xff\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\xff\xff\xff\xff\xff\xff\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0f\xff\xff\x00\x00\x3f\xff\xf8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1f\xff\xf8\x00\x00\x0f\xff\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xff\x80\x00\x00\x00\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\x00\x00\x00\x00\x7f\xff\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xff\xf8\x00\x00\x00\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\xff\xf0\x00\x00\x00\x00\x07\xff\xe0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\xff\xc0\x00\x00\x00\x00\x01\xff\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0f\xff\x80\x00\x00\x00\x00\x00\xff\xf8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1f\xfe\x00\x00\x00\x00\x00\x00\x3f\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1f\xfc\x00\x00\x00\x00\x00\x00\x1f\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x3f\xf8\x00\x00\x00\x00\x00\x00\x0f\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\x00\x00\x00\x00\x00\x07\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xe0\x00\x00\x00\x00\x00\x00\x03\xff\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xc0\x00\x00\x00\x00\x00\x00\x01\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xff\x80\x00\x00\x00\x00\x00\x00\x00\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\xff\x80\x00\x00\x00\x00\x00\x00\x00\x7f\xe0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\xff\x00\x00\x00\x00\x00\x00\x00\x00\x3f\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x3f\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x1f\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0f\xfc\x00\x00\x7f\xff\xff\xf8\x00\x00\x0f\xf8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0f\xf8\x00\x07\xff\xff\xff\xfc\x00\x00\x0f\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\xc3\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x01\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0f\xf8\x00\x0f\xff\xff\xff\xfc\x00\x00\x0f\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\xc3\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x01\xf8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1f\xf0\x00\x3f\xff\xff\xff\xfc\x00\x00\x07\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\xc3\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x03\xf8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1f\xf0\x00\x7f\xff\xff\xff\xfc\x00\x00\x03\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\xc3\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x03\xf8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x3f\xe0\x01\xff\xff\xff\xff\xff\xff\x80\x03\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\xc3\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x01\xf8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x3f\xe0\x03\xff\xfc\x00\x7d\xff\xff\x80\x03\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\xc3\xe0\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x3f\xc0\x07\xfe\x30\x00\x30\xff\xff\x80\x01\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x3f\xc0\x0f\xfc\x30\x00\x30\xff\xff\x80\x01\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xc0\x1f\xf0\x38\x70\x20\xfe\x1f\x00\x00\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xc0\x1f\xe0\x3f\xf8\x78\xfc\x00\x00\x00\xff\x00\x00\x00\x00\xff\xff\x00\x00\x00\x00\x00\x1f\xff\x80\x01\xff\xfc\x00\x00\x1f\xff\xff\xc1\xc0\x00\xff\xfe\x00\x0f\xff\xfe\x00\x00\xe0\x00\x7f\xff\x00\x00\x07\xff\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\x80\x3f\xc0\x3f\xf8\xf8\xfc\x00\x00\x00\xff\x00\x00\x00\x0f\xff\xff\xf0\x00\x00\x00\x03\xff\xff\xc0\x1f\xff\xff\xc0\x01\xff\xff\xff\xc3\xe0\x0f\xff\xff\xe0\x1f\xff\xff\xf0\x00\xf0\x07\xff\xff\xf0\x00\x7f\xff\xff\x00\x00\x00\x7f\xff\xfe\x00\x07\xff\xff\xe0\x0f\xff\xff\x07\xff\xfc\x00\xff\x80\x3f\x80\x3f\xf8\xf8\xfc\x00\x00\x00\xff\x80\x00\x00\x1f\xff\xff\xf8\x00\x00\x00\x0f\xff\xff\xc0\x3f\xff\xff\xe0\x03\xff\xff\xff\xc3\xf0\x1f\xff\xff\xf0\x1f\xff\xff\xfc\x01\xf0\x0f\xff\xff\xf8\x00\xff\xff\xff\x80\x00\x00\xff\xff\xff\x00\x0f\xff\xff\xf0\x0f\xff\xff\x8f\xff\xff\x00\xff\x00\x7f\x00\x00\xf0\x78\xfc\x00\x00\x00\xff\x80\x00\x00\x3f\xff\xff\xfc\x00\x00\x00\x1f\xff\xff\xc0\xff\xff\xff\xf0\x0f\xff\xff\xff\xc3\xf0\x3f\xff\xff\xf8\x1f\xff\xff\xfe\x01\xf0\x3f\xff\xff\xfc\x03\xff\xff\xff\xc0\x00\x03\xff\xff\xff\xc0\x1f\xff\xff\xfc\x0f\xff\xff\xff\xff\xff\xc0\xff\x00\xff\x00\x00\x00\x30\xfc\x00\x00\x00\x7f\x80\x00\x00\x3f\xff\xff\xfe\x00\x00\x00\x3f\xff\xff\xc0\xff\xff\xff\xf8\x0f\xff\xff\xff\xc3\xf0\x7f\xff\xff\xfc\x1f\xff\xff\xff\x01\xf0\x3f\xff\xff\xfe\x03\xff\xff\xff\xe0\x00\x03\xff\xff\xff\xc0\x3f\xff\xff\xfc\x0f\xff\xff\xff\xff\xff\xc0\xff\x00\xfe\x00\x00\x00\x00\xfc\x00\x00\x00\x7f\xc0\x00\x00\x7f\x80\x01\xff\x00\x00\x00\x7f\xe0\x00\x01\xfe\x00\x07\xf8\x1f\xe0\x00\x0f\xc3\xf0\xff\x00\x03\xfc\x1f\x80\x01\xff\x81\xf0\x7f\x80\x01\xff\x07\xf8\x00\x1f\xf0\x00\x03\xf0\x00\x0f\xe0\x3f\x00\x00\xfe\x0f\xc0\x07\xff\x00\x1f\xf0\xff\x80\xfc\x00\x00\x00\x00\xfc\x00\x00\x00\x7f\xc0\x00\x00\xfe\x00\x00\x7f\x00\x00\x00\xff\x80\x00\x01\xfc\x00\x01\xfc\x1f\xc0\x00\x07\xc3\xf0\xfe\x00\x00\xfe\x1f\x00\x00\x7f\xc1\xf0\x7f\x00\x00\x7f\x07\xf0\x00\x07\xf0\x00\x07\xe0\x00\x07\xf0\x7e\x00\x00\x7f\x0f\x80\x03\xff\x00\x07\xf0\xff\xfc\xfc\xfc\x00\x00\x00\xfc\x00\x00\x00\x7f\xc0\x00\x00\xfc\x00\x00\x3f\x00\x00\x00\xfe\x00\x00\x03\xf0\x00\x00\xfc\x3f\x00\x00\x07\xc3\xf0\xfc\x00\x00\x7e\x1f\x00\x00\x1f\xc1\xf0\xfe\x00\x00\x3f\x0f\xe0\x00\x03\xf0\x00\x07\xc0\x00\x03\xf0\x7c\x00\x00\x3f\x0f\x80\x00\xff\x00\x03\xf0\xff\xff\xfd\xfe\x00\x00\x00\xfe\x00\x00\x00\x3f\xc0\x00\x00\xfc\x00\x00\x3f\x00\x00\x00\xfc\x00\x00\x03\xf0\x00\x00\xfc\x3f\x00\x00\x07\xc3\xf1\xf8\x00\x00\x3e\x1f\x00\x00\x0f\xc1\xf0\xfc\x00\x00\x1f\x0f\xc0\x00\x01\xf0\x00\x0f\xc0\x00\x01\xf0\xfc\x00\x00\x1f\x0f\x80\x00\x7f\x00\x01\xf8\xff\xff\xff\xff\x01\xc0\x00\xff\xfc\x00\x00\x3f\xc0\x00\x00\xf8\x00\x00\x1f\x00\x00\x01\xf8\x00\x00\x03\xf0\x00\x00\x7c\x3f\x00\x00\x07\xc3\xf1\xf0\x00\x00\x3f\x1f\x00\x00\x07\xe1\xf0\xfc\x00\x00\x1f\x0f\xc0\x00\x01\xf0\x00\x0f\x80\x00\x01\xf0\xf8\x00\x00\x1f\x0f\x80\x00\x3f\x00\x00\xfc\xff\xff\xff\xff\x83\xe0\x00\xff\xfc\x00\x00\x3f\xc0\x00\x00\xf8\x00\x00\x1f\x00\x00\x01\xf0\x00\x00\x03\xe0\x00\x00\xfc\x3f\x00\x00\x07\xc3\xf1\xf0\x00\x00\x3f\x1f\x00\x00\x03\xe1\xf0\xfc\x00\x00\x0f\x0f\xc0\x00\x03\xf0\x00\x0f\x80\x00\x00\xf0\xf8\x00\x00\x0f\x0f\x80\x00\x3f\x00\x00\xfc\xff\xff\xff\xff\xff\xe0\x00\xff\xfc\x00\x00\x3f\xc0\x00\x00\xf8\x00\x00\x3f\x00\x00\x01\xf0\x00\x00\x00\x00\x00\x3f\xfc\x3f\x00\x00\x07\xc3\xf1\xf0\x00\x00\x3f\x1f\x00\x00\x03\xe1\xf0\xfc\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\x0f\x80\x00\x00\x00\xf8\x00\x00\x0f\x0f\x80\x00\x3f\x00\x00\x7e\xff\xff\xff\xff\xff\xe0\x00\xff\xfc\x00\x00\x3f\xc0\x00\x00\xf8\x00\x00\x7f\x00\x00\x03\xf0\x00\x00\x00\x00\x00\xff\xfc\x3f\x00\x00\x07\xc3\xf1\xf0\x00\x00\x3f\x1f\x00\x00\x03\xf1\xf0\xfc\x00\x00\x00\x00\x00\x03\xff\xf0\x00\x0f\x80\x00\x00\x00\xf8\x00\x00\x0f\x0f\x80\x00\x3f\x00\x00\x7e\x3f\xfd\xfd\xfe\x03\xc0\x00\xfe\x00\x00\x00\x3f\xc0\x00\x00\xf8\x00\x0f\xff\x00\x00\x03\xf0\x00\x00\x00\x00\x3f\xff\xfc\x3f\x00\x00\x07\xc3\xf1\xf0\x00\x00\x3f\x1f\x00\x00\x03\xf1\xf0\xfc\x00\x00\x00\x00\x00\xff\xff\xf0\x00\x0f\x80\x00\x00\x00\xf8\x00\x00\x0f\x0f\x80\x00\x1f\x00\x00\x3f\x3f\xfc\xfc\xfc\x00\x80\x00\xfc\x00\x00\x00\x3f\xc0\x00\x00\xf8\x00\x7f\xfe\x00\x00\x03\xf0\x00\x00\x00\x01\xff\xff\xfc\x3f\x00\x00\x07\xc3\xf1\xf0\x00\x00\x3f\x1f\x00\x00\x03\xf1\xf0\xfc\x00\x00\x00\x00\x07\xff\xff\xf0\x00\x0f\x80\x00\x00\x00\xf8\x00\x00\x0f\x0f\x80\x00\x1f\x00\x00\x3f\x00\x00\xfc\x00\x00\x00\x00\xfc\x00\x00\x00\x7f\xc0\x00\x00\xf8\x1f\xff\xfc\x07\xff\xc3\xf0\x00\x00\x00\x3f\xff\xff\xfc\x3f\x00\x00\x07\xc3\xf1\xf0\x00\x00\x3f\x1f\x00\x00\x03\xf1\xf0\xfc\x00\x00\x00\x00\xff\xff\xff\xf0\x00\x0f\x80\x00\x00\x00\xf8\x00\x00\x0f\x0f\x80\x00\x1f\x00\x00\x3f\x00\x00\xfe\x00\x00\x00\x00\xfc\x00\x00\x00\x7f\xc0\x00\x00\xfc\xff\xff\xfc\x0f\xff\xc3\xf0\x00\x00\x00\xff\xff\xfc\x7c\x3f\x00\x00\x07\xc3\xf1\xf0\x00\x00\x3f\x1f\x00\x00\x03\xf1\xf0\xfc\x00\x00\x00\x01\xff\xff\xf1\xf0\x00\x0f\x80\x00\x00\x00\xf8\x00\x00\x0f\x0f\x80\x00\x1f\x00\x00\x3f\x00\x00\xff\x00\x70\x00\x00\xfc\x00\x00\x00\x7f\xc0\x00\x00\xff\xff\xff\xc0\x0f\xff\xc3\xf0\x00\x00\x00\xff\xff\x00\x7c\x3f\x00\x00\x07\xc3\xf1\xf0\x00\x00\x3f\x1f\x00\x00\x03\xf1\xf0\xfc\x00\x00\x00\x03\xff\xfc\x00\xf0\x00\x0f\x80\x00\x00\x00\xf8\x00\x00\x0f\x0f\x80\x00\x1f\x00\x00\x3f\x00\x00\x7f\x00\xf8\x02\x00\xfc\x00\x00\x00\x7f\x80\x00\x00\xff\xff\xfe\x00\x0f\xff\xc3\xf0\x00\x00\x01\xff\xf8\x00\x7c\x3f\x00\x00\x07\xc3\xf1\xf0\x00\x00\x3f\x1f\x00\x00\x03\xf1\xf0\xfc\x00\x00\x00\x07\xff\xe0\x00\xf0\x00\x0f\x80\x00\x00\x00\xf8\x00\x00\x0f\x0f\x80\x00\x1f\x00\x00\x3f\x00\x00\x3f\x80\xf8\x0f\x00\xfc\x00\x00\x00\xff\x80\x00\x00\xff\xff\xc0\x00\x07\xff\xc3\xf0\x00\x00\x03\xff\x00\x00\x7c\x3f\x00\x00\x07\xc3\xf1\xf0\x00\x00\x3f\x1f\x00\x00\x03\xf1\xf0\xfc\x00\x00\x00\x07\xfc\x00\x00\xf0\x00\x0f\x80\x00\x00\x00\xf8\x00\x00\x0f\x0f\x80\x00\x1f\x00\x00\x3f\x00\x00\x3f\xc0\xf8\x0f\x80\xfc\x00\x00\x00\xff\x80\x00\x00\xff\xfe\x00\x00\x00\x00\x03\xf0\x00\x00\x03\xf8\x00\x00\x7c\x3f\x00\x00\x07\xc3\xf1\xf0\x00\x00\x3f\x1f\x00\x00\x03\xf1\xf0\xfc\x00\x00\x00\x0f\xe0\x00\x00\xf0\x00\x0f\x80\x00\x00\x00\xf8\x00\x00\x0f\x0f\x80\x00\x1f\x00\x00\x3f\x00\x00\x1f\xe0\xf0\x0f\x80\xfc\x00\x00\x00\xff\x80\x00\x00\xff\x80\x00\x0f\x00\x00\x03\xf0\x00\x00\x03\xf0\x00\x00\x7c\x3f\x00\x00\x07\xc3\xf1\xf0\x00\x00\x3f\x1f\x00\x00\x03\xf1\xf0\xfc\x00\x00\x0f\x0f\xc0\x00\x00\xf0\x00\x0f\x80\x00\x00\xe0\xf8\x00\x00\x0f\x0f\x80\x00\x1f\x00\x00\x3f\x00\x00\x1f\xf0\x60\x0f\x80\xfc\x00\x00\x00\xff\x00\x00\x00\xfc\x00\x00\x1f\x00\x00\x03\xf0\x00\x00\x03\xe0\x00\x00\x7c\x3f\x00\x00\x07\xc3\xf1\xf0\x00\x00\x3f\x1f\x00\x00\x03\xf1\xf0\xfc\x00\x00\x0f\x0f\xc0\x00\x01\xf0\x00\x0f\x80\x00\x00\xf0\xf8\x00\x00\x0f\x0f\x80\x00\x1f\x00\x00\x3f\x00\x00\x0f\xfc\x20\x06\x00\xff\xff\x80\x00\xff\x00\x00\x00\xfc\x00\x00\x3f\x00\x00\x03\xf0\x00\x00\x03\xf0\x00\x00\x7c\x3f\x00\x00\x07\xc3\xf1\xf0\x00\x00\x3f\x1f\x00\x00\x03\xf1\xf0\xfc\x00\x00\x1f\x0f\xc0\x00\x01\xf0\x00\x0f\x80\x00\x01\xf0\xf8\x00\x00\x1f\x0f\x80\x00\x1f\x00\x00\x3f\x00\x00\x07\xfe\x70\x0e\x00\xff\xff\x80\x00\xff\x00\x00\x00\xfc\x00\x00\x3f\x00\x00\x03\xf0\x00\x00\x03\xf0\x00\x00\xfc\x3f\x00\x00\x0f\xc3\xf0\xf8\x00\x00\x3e\x1f\x00\x00\x03\xf1\xf0\xfc\x00\x00\x1f\x0f\xc0\x00\x01\xf0\x00\x0f\xc0\x00\x01\xf0\xfc\x00\x00\x1f\x0f\x80\x00\x1f\x00\x00\x3f\x07\x80\x03\xff\xf8\x3e\x01\xff\xff\x80\x01\xff\x00\x00\x00\xfc\x00\x00\x3f\x00\x00\x03\xf0\x00\x00\x03\xf8\x00\x00\xfc\x3f\x80\x00\x0f\xc3\xf0\xfc\x00\x00\x7e\x1f\x00\x00\x03\xf1\xf0\x7e\x00\x00\x3f\x0f\xe0\x00\x03\xf0\x00\x07\xc0\x00\x03\xf0\x7c\x00\x00\x3f\x0f\x80\x00\x1f\x00\x00\x3f\x0f\xc0\x01\xff\xff\xff\xff\xff\xff\x80\x03\xff\x00\x00\x00\xfe\x00\x00\xfe\x00\x00\x03\xf0\x00\x00\x03\xfc\x00\x01\xfc\x1f\xc0\x00\x1f\xc3\xf0\xfe\x00\x00\xfe\x1f\x00\x00\x03\xf1\xf0\x7f\x00\x00\x7f\x07\xf0\x00\x07\xf0\x0e\x07\xe0\x00\x07\xe0\x7e\x00\x00\x7e\x0f\x80\x00\x1f\x00\x00\x3f\x0f\xe0\x00\xff\xff\xff\xff\xfe\xff\x00\x03\xfe\x00\x00\x00\x7f\xff\xff\xfe\x00\x00\x03\xf0\x00\x00\x01\xff\xff\xff\xf8\x1f\xff\xff\xff\x83\xf0\xff\xff\xff\xfc\x1f\x00\x00\x03\xf1\xf0\x3f\xff\xff\xfe\x07\xff\xff\xff\xe0\x1f\x03\xff\xff\xff\xe0\x3f\xff\xff\xfe\x0f\x80\x00\x1f\x00\x00\x3f\x1f\xf0\x00\x7f\xff\xff\xff\xfc\x00\x00\x03\xfe\x00\x00\x00\x3f\xff\xff\xfc\x00\x00\x03\xf0\x00\x00\x00\xff\xff\xff\xf0\x0f\xff\xff\xff\x83\xf0\x7f\xff\xff\xfc\x1f\x00\x00\x03\xf1\xf0\x3f\xff\xff\xfe\x03\xff\xff\xff\xe0\x3f\x03\xff\xff\xff\xc0\x3f\xff\xff\xfc\x0f\x80\x00\x1f\x00\x00\x3f\x1f\xf0\x00\x1f\xff\xff\xff\xfc\x00\x00\x03\xfc\x00\x00\x00\x3f\xff\xff\xf8\x00\x00\x03\xf0\x00\x00\x00\xff\xff\xff\xf0\x07\xff\xff\xff\x03\xf0\x3f\xff\xff\xf8\x1f\x00\x00\x03\xf1\xf0\x1f\xff\xff\xfc\x01\xff\xff\xff\xc0\x3f\x01\xff\xff\xff\x80\x1f\xff\xff\xf8\x0f\x80\x00\x1f\x00\x00\x3f\x0f\xf0\x00\x07\xff\xff\xff\xfc\x00\x00\x07\xfc\x00\x00\x00\x0f\xff\xff\xf0\x00\x00\x03\xf0\x00\x00\x00\x7f\xff\xff\xe0\x03\xff\xff\xfe\x03\xf0\x1f\xff\xff\xf0\x1f\x00\x00\x03\xf1\xf0\x0f\xff\xff\xf8\x00\xff\xff\xff\x80\x3f\x00\xff\xff\xff\x00\x0f\xff\xff\xf0\x0f\x80\x00\x1f\x00\x00\x3f\x0f\xf8\x00\x00\x7f\xff\xff\xf8\x00\x00\x0f\xfc\x00\x00\x00\x03\xff\xff\xc0\x00\x00\x01\xe0\x00\x00\x00\x0f\xff\xff\x00\x00\xff\xff\xf0\x03\xe0\x07\xff\xff\xc0\x0f\x00\x00\x03\xe0\xf0\x03\xff\xff\xe0\x00\x3f\xff\xfe\x00\x0e\x00\x1f\xff\xf8\x00\x01\xff\xff\x80\x07\x00\x00\x0e\x00\x00\x3c\x0f\xfc\x00\x00\x0f\xf8\x00\x00\x00\x00\x0f\xf8\x00\x00\x00\x00\xff\xfe\x00\x00\x00\x00\xc0\x00\x00\x00\x01\xff\xfc\x00\x00\x1f\xff\xc0\x00\xc0\x00\xff\xfe\x00\x06\x00\x00\x00\xc0\x60\x00\x7f\xff\x00\x00\x07\xff\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x1f\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x3f\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\xff\x00\x00\x00\x00\x00\x00\x00\x00\x3f\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\xff\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xe0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xff\x80\x00\x00\x00\x00\x00\x00\x00\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xc0\x00\x00\x00\x00\x00\x00\x00\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xe0\x00\x00\x00\x00\x00\x00\x03\xff\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\x00\x00\x00\x00\x00\x03\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x3f\xf8\x00\x00\x00\x00\x00\x00\x0f\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x3f\xfc\x00\x00\x00\x00\x00\x00\x0f\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0f\xfe\x00\x00\x00\x00\x00\x00\x3f\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0f\xff\x00\x00\x00\x00\x00\x00\x7f\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\xff\xc0\x00\x00\x00\x00\x00\xff\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\xff\xe0\x00\x00\x00\x00\x03\xff\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xff\xf8\x00\x00\x00\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xfc\x00\x00\x00\x00\x1f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x3f\xff\xc0\x00\x00\x00\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x3f\xff\xe0\x00\x00\x03\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0f\xff\xff\x00\x00\x3f\xff\xf8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\xff\xff\xe0\x03\xff\xff\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xff\xff\xff\xff\xff\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1f\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0f\xff\xff\xff\xff\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xff\xff\xff\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xff\xff\xff\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\xff\xff\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xff\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 3 | ) 4 | -------------------------------------------------------------------------------- /inkplate.py: -------------------------------------------------------------------------------- 1 | # Copyright © 2020 by Thorsten von Eicken. 2 | import time 3 | import micropython 4 | import framebuf 5 | import machine, sdcard, os 6 | from machine import Pin, I2C 7 | from uarray import array 8 | from mcp23017 import MCP23017 9 | from micropython import const 10 | 11 | from gfx import GFX 12 | from gfx_standard_font_01 import text_dict as std_font 13 | 14 | TPS65186_addr = const(0x48) # I2C address 15 | 16 | # ESP32 GPIO set and clear registers to twiddle 32 gpio bits at once 17 | # from esp-idf: 18 | # define DR_REG_GPIO_BASE 0x3ff44000 19 | # define GPIO_OUT_W1TS_REG (DR_REG_GPIO_BASE + 0x0008) 20 | # define GPIO_OUT_W1TC_REG (DR_REG_GPIO_BASE + 0x000c) 21 | ESP32_GPIO = const(0x3FF44000) # ESP32 GPIO base 22 | # register offsets from ESP32_GPIO 23 | W1TS0 = const(2) # offset for "write one to set" register for gpios 0..31 24 | W1TC0 = const(3) # offset for "write one to clear" register for gpios 0..31 25 | W1TS1 = const(5) # offset for "write one to set" register for gpios 32..39 26 | W1TC1 = const(6) # offset for "write one to clear" register for gpios 32..39 27 | # bit masks in W1TS/W1TC registers 28 | EPD_DATA = const(0x0E8C0030) # EPD_D0..EPD_D7 29 | EPD_CL = const(0x00000001) # in W1Tx0 30 | EPD_LE = const(0x00000004) # in W1Tx0 31 | EPD_CKV = const(0x00000001) # in W1Tx1 32 | EPD_SPH = const(0x00000002) # in W1Tx1 33 | 34 | # Raw display constants 35 | D_ROWS = const(600) 36 | D_COLS = const(800) 37 | 38 | # Inkplate provides access to the pins of the Inkplate 6 as well as to low-level display 39 | # functions. 40 | class _Inkplate: 41 | @classmethod 42 | def init(cls, i2c): 43 | cls._i2c = i2c 44 | cls._mcp23017 = MCP23017(i2c) 45 | # Display control lines 46 | cls.EPD_CL = Pin(0, Pin.OUT, value=0) 47 | cls.EPD_LE = Pin(2, Pin.OUT, value=0) 48 | cls.EPD_CKV = Pin(32, Pin.OUT, value=0) 49 | cls.EPD_SPH = Pin(33, Pin.OUT, value=1) 50 | cls.EPD_OE = cls._mcp23017.pin(0, Pin.OUT, value=0) 51 | cls.EPD_GMODE = cls._mcp23017.pin(1, Pin.OUT, value=0) 52 | cls.EPD_SPV = cls._mcp23017.pin(2, Pin.OUT, value=1) 53 | # Display data lines - we only use the Pin class to init the pins 54 | Pin(4, Pin.OUT) 55 | Pin(5, Pin.OUT) 56 | Pin(18, Pin.OUT) 57 | Pin(19, Pin.OUT) 58 | Pin(23, Pin.OUT) 59 | Pin(25, Pin.OUT) 60 | Pin(26, Pin.OUT) 61 | Pin(27, Pin.OUT) 62 | # TPS65186 power regulator control 63 | cls.TPS_WAKEUP = cls._mcp23017.pin(3, Pin.OUT, value=0) 64 | cls.TPS_PWRUP = cls._mcp23017.pin(4, Pin.OUT, value=0) 65 | cls.TPS_VCOM = cls._mcp23017.pin(5, Pin.OUT, value=0) 66 | cls.TPS_INT = cls._mcp23017.pin(6, Pin.IN) 67 | cls.TPS_PWR_GOOD = cls._mcp23017.pin(7, Pin.IN) 68 | # Misc 69 | cls.GPIO0_PUP = cls._mcp23017.pin(8, Pin.OUT, value=0) 70 | cls.VBAT_EN = cls._mcp23017.pin(9, Pin.OUT, value=1) 71 | # Touch sensors 72 | cls.TOUCH1 = cls._mcp23017.pin(10, Pin.IN) 73 | cls.TOUCH2 = cls._mcp23017.pin(11, Pin.IN) 74 | cls.TOUCH3 = cls._mcp23017.pin(12, Pin.IN) 75 | 76 | cls._on = False # whether panel is powered on or not 77 | 78 | if len(_Inkplate.byte2gpio) == 0: 79 | _Inkplate.gen_byte2gpio() 80 | 81 | # _tps65186_write writes an 8-bit value to a register 82 | @classmethod 83 | def _tps65186_write(cls, reg, v): 84 | cls._i2c.writeto_mem(TPS65186_addr, reg, bytes((v,))) 85 | 86 | # _tps65186_read reads an 8-bit value from a register 87 | @classmethod 88 | def _tps65186_read(cls, reg): 89 | cls._i2c.readfrom_mem(TPS65186_addr, reg, 1)[0] 90 | 91 | # power_on turns the voltage regulator on and wakes up the display (GMODE and OE) 92 | @classmethod 93 | def power_on(cls): 94 | if cls._on: 95 | return 96 | cls._on = True 97 | # turn on power regulator 98 | cls.TPS_WAKEUP(1) 99 | cls.TPS_PWRUP(1) 100 | cls.TPS_VCOM(1) 101 | # enable all rails 102 | cls._tps65186_write(0x01, 0x3F) # ??? 103 | time.sleep_ms(40) 104 | cls._tps65186_write(0x0D, 0x80) # ??? 105 | time.sleep_ms(2) 106 | cls._temperature = cls._tps65186_read(1) 107 | # wake-up display 108 | cls.EPD_GMODE(1) 109 | cls.EPD_OE(1) 110 | 111 | # power_off puts the display to sleep and cuts the power 112 | # TODO: also tri-state gpio pins to avoid current leakage during deep-sleep 113 | @classmethod 114 | def power_off(cls): 115 | if not cls._on: 116 | return 117 | cls._on = False 118 | # put display to sleep 119 | cls.EPD_GMODE(0) 120 | cls.EPD_OE(0) 121 | # turn off power regulator 122 | cls.TPS_PWRUP(0) 123 | cls.TPS_WAKEUP(0) 124 | cls.TPS_VCOM(0) 125 | 126 | # ===== Methods that are independent of pixel bit depth 127 | 128 | # vscan_start begins a vertical scan by toggling CKV and SPV 129 | # sleep_us calls are commented out 'cause MP is slow enough... 130 | @classmethod 131 | def vscan_start(cls): 132 | def ckv_pulse(): 133 | cls.EPD_CKV(0) 134 | cls.EPD_CKV(1) 135 | 136 | # start a vertical scan pulse 137 | cls.EPD_CKV(1) # time.sleep_us(7) 138 | cls.EPD_SPV(0) # time.sleep_us(10) 139 | ckv_pulse() # time.sleep_us(8) 140 | cls.EPD_SPV(1) # time.sleep_us(10) 141 | # pulse through 3 scan lines that end up being invisible 142 | ckv_pulse() # time.sleep_us(18) 143 | ckv_pulse() # time.sleep_us(18) 144 | ckv_pulse() 145 | 146 | # vscan_write latches the row into the display pixels and moves to the next row 147 | @micropython.viper 148 | @staticmethod 149 | def vscan_write(): 150 | w1ts0 = ptr32(int(ESP32_GPIO + 4 * W1TS0)) 151 | w1tc0 = ptr32(int(ESP32_GPIO + 4 * W1TC0)) 152 | w1tc0[W1TC1 - W1TC0] = EPD_CKV # remove gate drive 153 | w1ts0[0] = EPD_LE # pulse to latch row -- 154 | w1ts0[0] = EPD_LE # delay a tiny bit 155 | w1tc0[0] = EPD_LE 156 | w1tc0[0] = EPD_LE # delay a tiny bit 157 | w1ts0[W1TS1 - W1TS0] = EPD_CKV # apply gate drive to next row 158 | 159 | # byte2gpio converts a byte of data for the screen to 32 bits of gpio0..31 160 | # (oh, e-radionica, why didn't you group the gpios better?!) 161 | byte2gpio = [] 162 | 163 | @classmethod 164 | def gen_byte2gpio(cls): 165 | cls.byte2gpio = array("L", bytes(4 * 256)) 166 | for b in range(256): 167 | cls.byte2gpio[b] = ( 168 | (b & 0x3) << 4 | (b & 0xC) << 16 | (b & 0x10) << 19 | (b & 0xE0) << 20 169 | ) 170 | # sanity check that all EPD_DATA bits got set at some point and no more 171 | union = 0 172 | for i in range(256): 173 | union |= cls.byte2gpio[i] 174 | assert union == EPD_DATA 175 | 176 | # fill_screen writes the same value to all bytes of the screen, it is used for cleaning 177 | @micropython.viper 178 | @staticmethod 179 | def fill_screen(data: int): 180 | w1ts0 = ptr32(int(ESP32_GPIO + 4 * W1TS0)) 181 | w1tc0 = ptr32(int(ESP32_GPIO + 4 * W1TC0)) 182 | # set the data output gpios 183 | w1tc0[0] = EPD_DATA | EPD_CL 184 | w1ts0[0] = data 185 | vscan_write = _Inkplate.vscan_write 186 | epd_cl = EPD_CL 187 | 188 | # send all rows 189 | for r in range(D_ROWS): 190 | # send first byte of row with start-row signal 191 | w1tc0[W1TC1 - W1TC0] = EPD_SPH 192 | w1ts0[0] = epd_cl 193 | w1tc0[0] = epd_cl 194 | w1ts0[W1TS1 - W1TS0] = EPD_SPH 195 | 196 | # send remaining bytes (we overshoot by one, which is OK) 197 | i = int(D_COLS >> 3) 198 | while i > 0: 199 | w1ts0[0] = epd_cl 200 | w1tc0[0] = epd_cl 201 | w1ts0[0] = epd_cl 202 | w1tc0[0] = epd_cl 203 | i -= 1 204 | 205 | # latch row and increment to next 206 | # inlined vscan_write() 207 | w1tc0[W1TC1 - W1TC0] = EPD_CKV # remove gate drive 208 | w1ts0[0] = EPD_LE # pulse to latch row -- 209 | w1ts0[0] = EPD_LE # delay a tiny bit 210 | w1tc0[0] = EPD_LE 211 | w1tc0[0] = EPD_LE # delay a tiny bit 212 | w1ts0[W1TS1 - W1TS0] = EPD_CKV # apply gate drive to next row 213 | 214 | # clean fills the screen with one of the four possible pixel patterns 215 | @classmethod 216 | def clean(cls, patt, rep): 217 | c = [0xAA, 0x55, 0x00, 0xFF][patt] 218 | data = _Inkplate.byte2gpio[c] & ~EPD_CL 219 | for i in range(rep): 220 | cls.vscan_start() 221 | cls.fill_screen(data) 222 | 223 | 224 | class InkplateMono(framebuf.FrameBuffer): 225 | def __init__(self): 226 | self._framebuf = bytearray(D_ROWS * D_COLS // 8) 227 | super().__init__(self._framebuf, D_COLS, D_ROWS, framebuf.MONO_HMSB) 228 | ip = InkplateMono 229 | ip._gen_luts() 230 | ip._wave = [ 231 | ip.lut_blk, 232 | ip.lut_blk, 233 | ip.lut_blk, 234 | ip.lut_blk, 235 | ip.lut_blk, 236 | ip.lut_bw, 237 | ] 238 | 239 | # gen_luts generates the look-up tables to convert a nibble (4 bits) of pixels to the 240 | # 32-bits that need to be pushed into the gpio port. 241 | # The LUTs used here were copied from the e-Radionica Inkplate-6-Arduino-library. 242 | @classmethod 243 | def _gen_luts(cls): 244 | b16 = bytes(4 * 16) # is there a better way to init an array with 16 words??? 245 | cls.lut_wht = array("L", b16) # bits to ship to gpio to make pixels white 246 | cls.lut_blk = array("L", b16) # bits to ship to gpio to make pixels black 247 | cls.lut_bw = array( 248 | "L", b16 249 | ) # bits to ship to gpio to make pixels black and white 250 | for i in range(16): 251 | wht = 0 252 | blk = 0 253 | bw = 0 254 | # display uses 2 bits per pixel: 00=discharge, 01=black, 10=white, 11=skip 255 | for bit in range(4): 256 | wht = wht | ((2 if (i >> bit) & 1 == 0 else 3) << (2 * bit)) 257 | blk = blk | ((1 if (i >> bit) & 1 == 1 else 3) << (2 * bit)) 258 | bw = bw | ((1 if (i >> bit) & 1 == 1 else 2) << (2 * bit)) 259 | cls.lut_wht[i] = _Inkplate.byte2gpio[wht] | EPD_CL 260 | cls.lut_blk[i] = _Inkplate.byte2gpio[blk] | EPD_CL 261 | cls.lut_bw[i] = _Inkplate.byte2gpio[bw] | EPD_CL 262 | # print("Black: %08x, White:%08x Data:%08x" % (cls.lut_bw[0xF], cls.lut_bw[0], EPD_DATA)) 263 | 264 | # _send_row writes a row of data to the display 265 | @micropython.viper 266 | @staticmethod 267 | def _send_row(lut_in, framebuf, row: int): 268 | # cache vars into locals 269 | w1ts0 = ptr32(int(ESP32_GPIO + 4 * W1TS0)) 270 | w1tc0 = ptr32(int(ESP32_GPIO + 4 * W1TC0)) 271 | off = int(EPD_DATA | EPD_CL) # mask with all data bits and clock bit 272 | fb = ptr8(framebuf) 273 | ix = int(row * (D_COLS >> 3) + 99) # index into framebuffer 274 | lut = ptr32(lut_in) 275 | # send first byte 276 | data = int(fb[ix]) 277 | ix -= 1 278 | w1tc0[0] = off 279 | w1tc0[W1TC1 - W1TC0] = EPD_SPH 280 | w1ts0[0] = lut[data >> 4] # set data bits and assert clock 281 | # w1tc0[0] = EPD_CL # clear clock, leaving data bits (unreliable if data also cleared) 282 | w1tc0[0] = off # clear data bits as well ready for next byte 283 | w1ts0[W1TS1 - W1TS0] = EPD_SPH 284 | w1ts0[0] = lut[data & 0xF] 285 | # w1tc0[0] = EPD_CL 286 | w1tc0[0] = off 287 | # send the remaining bytes (792 pixels) 288 | for c in range(99): 289 | data = int(fb[ix]) 290 | ix -= 1 291 | w1ts0[0] = lut[data >> 4] 292 | # w1tc0[0] = EPD_CL 293 | w1tc0[0] = off 294 | w1ts0[0] = lut[data & 0xF] 295 | # w1tc0[0] = EPD_CL 296 | w1tc0[0] = off 297 | 298 | # display_mono sends the monochrome buffer to the display, clearing it first 299 | def display(self): 300 | ip = _Inkplate 301 | ip.power_on() 302 | 303 | # clean the display 304 | t0 = time.ticks_ms() 305 | ip.clean(0, 1) 306 | ip.clean(1, 12) 307 | ip.clean(2, 1) 308 | ip.clean(0, 11) 309 | ip.clean(2, 1) 310 | ip.clean(1, 12) 311 | ip.clean(2, 1) 312 | ip.clean(0, 11) 313 | 314 | # the display gets written N times 315 | t1 = time.ticks_ms() 316 | n = 0 317 | send_row = InkplateMono._send_row 318 | vscan_write = ip.vscan_write 319 | fb = self._framebuf 320 | for lut in self._wave: 321 | ip.vscan_start() 322 | # write all rows 323 | r = D_ROWS - 1 324 | while r >= 0: 325 | send_row(lut, fb, r) 326 | vscan_write() 327 | r -= 1 328 | n += 1 329 | 330 | t2 = time.ticks_ms() 331 | tc = time.ticks_diff(t1, t0) 332 | td = time.ticks_diff(t2, t1) 333 | tt = time.ticks_diff(t2, t0) 334 | print( 335 | "Mono: clean %dms (%dms ea), draw %dms (%dms ea), total %dms" 336 | % (tc, tc // (4 + 22 + 24), td, td // len(self._wave), tt) 337 | ) 338 | 339 | ip.clean(2, 2) 340 | ip.clean(3, 1) 341 | ip.power_off() 342 | 343 | # @micropython.viper 344 | def clear(self): 345 | self.fill(0) 346 | # fb = ptr8(self._framebuf) 347 | # for ix in range(D_ROWS * D_COLS // 8): 348 | # fb[ix] = 0 349 | 350 | 351 | # Inkplate display with 2 bits of gray scale (4 levels) 352 | class InkplateGS2(framebuf.FrameBuffer): 353 | _wave = None 354 | 355 | def __init__(self): 356 | self._framebuf = bytearray(D_ROWS * D_COLS // 4) 357 | super().__init__(self._framebuf, D_COLS, D_ROWS, framebuf.GS2_HMSB) 358 | if not InkplateGS2._wave: 359 | InkplateGS2._gen_wave() 360 | 361 | # _gen_wave generates the waveform table. The table consists of N phases or steps during 362 | # each of which the entire display gets written. The array in each phase gets indexed with 363 | # a nibble of data and contains the 32-bits that need to be pushed into the gpio port. 364 | # The waveform used here was adapted from the e-Radionica Inkplate-6-Arduino-library 365 | # by taking colors 0 (black), 3, 5, and 7 (white) from "waveform3Bit[8][7]". 366 | @classmethod 367 | def _gen_wave(cls): 368 | # genlut generates the lookup table that maps a nibble (2 pixels, 4 bits) to a 32-bit 369 | # word to push into the GPIO port 370 | def genlut(op): 371 | return bytes([op[j] | op[i] << 2 for i in range(4) for j in range(4)]) 372 | 373 | cls._wave = [ 374 | genlut([0, 0, 0, 0]), # order: blk, dk-grey, light-grey, white 375 | genlut([0, 0, 0, 0]), # value: 0=dischg, 1=black, 2=white, 3=skip 376 | genlut([0, 1, 1, 0]), 377 | genlut([0, 1, 1, 0]), 378 | genlut([1, 2, 1, 0]), 379 | genlut([1, 1, 2, 0]), 380 | genlut([1, 2, 2, 2]), 381 | ] 382 | 383 | # _send_row writes a row of data to the display 384 | @micropython.viper 385 | @staticmethod 386 | def _send_row(lut_in, framebuf, row: int): 387 | # cache vars into locals 388 | w1ts0 = ptr32(int(ESP32_GPIO + 4 * W1TS0)) 389 | w1tc0 = ptr32(int(ESP32_GPIO + 4 * W1TC0)) 390 | off = int(EPD_DATA | EPD_CL) # mask with all data bits and clock bit 391 | fb = ptr8(framebuf) 392 | ix = int(row * 200 + 199) # index into framebuffer 393 | lut = ptr8(lut_in) 394 | b2g = ptr32(_Inkplate.byte2gpio) 395 | # send first byte 396 | data = int(fb[ix]) 397 | ix -= 1 398 | w1tc0[0] = off 399 | w1tc0[W1TC1 - W1TC0] = EPD_SPH 400 | w1ts0[0] = ( 401 | b2g[lut[data >> 4] << 4 | lut[data & 0xF]] | EPD_CL 402 | ) # set data bits and clock 403 | # w1tc0[0] = EPD_CL # clear clock, leaving data bits (unreliable if data also cleared) 404 | w1tc0[0] = off # clear data bits as well ready for next byte 405 | w1ts0[W1TS1 - W1TS0] = EPD_SPH 406 | # send the remaining bytes (792 pixels) 407 | for c in range(199): 408 | data = int(fb[ix]) 409 | ix -= 1 410 | w1ts0[0] = b2g[lut[data >> 4] << 4 | lut[data & 0xF]] | EPD_CL 411 | # w1tc0[0] = EPD_CL 412 | w1tc0[0] = off 413 | 414 | # display_mono sends the monochrome buffer to the display, clearing it first 415 | def display(self): 416 | ip = _Inkplate 417 | ip.power_on() 418 | 419 | # clean the display 420 | t0 = time.ticks_ms() 421 | ip.clean(0, 1) 422 | ip.clean(1, 12) 423 | ip.clean(2, 1) 424 | ip.clean(0, 11) 425 | ip.clean(2, 1) 426 | ip.clean(1, 12) 427 | ip.clean(2, 1) 428 | ip.clean(0, 11) 429 | 430 | # the display gets written N times 431 | t1 = time.ticks_ms() 432 | n = 0 433 | send_row = InkplateGS2._send_row 434 | vscan_write = ip.vscan_write 435 | fb = self._framebuf 436 | for lut in InkplateGS2._wave: 437 | ip.vscan_start() 438 | # write all rows 439 | r = D_ROWS - 1 440 | while r >= 0: 441 | send_row(lut, fb, r) 442 | vscan_write() 443 | r -= 1 444 | n += 1 445 | 446 | t2 = time.ticks_ms() 447 | tc = time.ticks_diff(t1, t0) 448 | td = time.ticks_diff(t2, t1) 449 | tt = time.ticks_diff(t2, t0) 450 | print( 451 | "GS2: clean %dms (%dms ea), draw %dms (%dms ea), total %dms" 452 | % (tc, tc // (4 + 22 + 24), td, td // len(InkplateGS2._wave), tt) 453 | ) 454 | 455 | ip.clean(2, 1) # ?? 456 | ip.clean(3, 1) 457 | ip.power_off() 458 | 459 | # @micropython.viper 460 | def clear(self): 461 | self.fill(3) 462 | # fb = ptr8(self._framebuf) 463 | # for ix in range(int(len(self._framebuf))): 464 | # fb[ix] = 0xFF 465 | 466 | 467 | # InkplatePartial managed partial updates. It starts by making a copy of the current framebuffer 468 | # and then when asked to draw it renders the differences between the copy and the new framebuffer 469 | # state. The constructor needs a reference to the current/main display object (InkplateMono). 470 | # Only InkplateMono is supported at the moment. 471 | class InkplatePartial: 472 | def __init__(self, base): 473 | self._base = base 474 | self._framebuf = bytearray(len(base._framebuf)) 475 | InkplatePartial._gen_lut_mono() 476 | 477 | # start makes a reference copy of the current framebuffer 478 | def start(self): 479 | self._framebuf[:] = self._base._framebuf[:] 480 | 481 | # display the changes between our reference copy and the current framebuffer contents 482 | def display(self, x=0, y=0, w=D_COLS, h=D_ROWS): 483 | ip = _Inkplate 484 | ip.power_on() 485 | 486 | # the display gets written a couple of times 487 | t0 = time.ticks_ms() 488 | n = 0 489 | send_row = InkplatePartial._send_row 490 | skip_rows = InkplatePartial._skip_rows 491 | vscan_write = ip.vscan_write 492 | nfb = self._base._framebuf # new framebuffer 493 | ofb = self._framebuf # old framebuffer 494 | lut = InkplatePartial._lut_mono 495 | h -= 1 496 | for _ in range(5): 497 | ip.vscan_start() 498 | r = D_ROWS - 1 499 | # skip rows that supposedly have no change 500 | if r > y + h: 501 | skip_rows(r - (y + h)) 502 | r = y + h 503 | # write changed rows 504 | while r >= y: 505 | send_row(lut, ofb, nfb, r) 506 | vscan_write() 507 | r -= 1 508 | # skip remaining rows (doesn't seem to be necessary) 509 | # if r > 0: 510 | # skip_rows(r) 511 | n += 1 512 | 513 | t1 = time.ticks_ms() 514 | td = time.ticks_diff(t1, t0) 515 | print( 516 | "Partial: draw %dms (%dms/frame %dus/row) (y=%d..%d)" 517 | % (td, td // n, td * 1000 // n // (D_ROWS - y), y, y + h + 1) 518 | ) 519 | 520 | ip.clean(2, 2) 521 | ip.clean(3, 1) 522 | ip.power_off() 523 | 524 | # gen_lut_mono generates a look-up tables to change the display from a nibble of old 525 | # pixels (4 bits = 4 pixels) to a nibble of new pixels. The LUT contains the 526 | # 32-bits that need to be pushed into the gpio port to effect the change. 527 | @classmethod 528 | def _gen_lut_mono(cls): 529 | lut = cls._lut_mono = array("L", bytes(4 * 256)) 530 | for o in range(16): # iterate through all old-pixels combos 531 | for n in range(16): # iterate through all new-pixels combos 532 | bw = 0 533 | for bit in range(4): 534 | # value to send to display: turns out that if we juxtapose the old and new 535 | # bits we get the right value except for the 00 combination... 536 | val = (((o >> bit) << 1) & 2) | ((n >> bit) & 1) 537 | if val == 0: 538 | val = 3 539 | bw = bw | (val << (2 * bit)) 540 | lut[o * 16 + n] = _Inkplate.byte2gpio[bw] | EPD_CL 541 | # print("Black: %08x, White:%08x Data:%08x" % (cls.lut_bw[0xF], cls.lut_bw[0], EPD_DATA)) 542 | 543 | # _skip_rows skips N rows 544 | @micropython.viper 545 | @staticmethod 546 | def _skip_rows(rows: int): 547 | if rows <= 0: 548 | return 549 | # cache vars into locals 550 | w1ts0 = ptr32(int(ESP32_GPIO + 4 * W1TS0)) 551 | w1tc0 = ptr32(int(ESP32_GPIO + 4 * W1TC0)) 552 | 553 | # need to fill the column latches with "no-change" values (all ones) 554 | epd_cl = EPD_CL 555 | w1tc0[0] = epd_cl 556 | w1ts0[0] = EPD_DATA 557 | # send first byte of row with start-row signal 558 | w1tc0[W1TC1 - W1TC0] = EPD_SPH 559 | w1ts0[0] = epd_cl 560 | w1tc0[0] = epd_cl 561 | w1ts0[W1TS1 - W1TS0] = EPD_SPH 562 | # send remaining bytes 563 | i = int(D_COLS >> 3) 564 | while i > 0: 565 | w1ts0[0] = epd_cl 566 | w1tc0[0] = epd_cl 567 | w1ts0[0] = epd_cl 568 | w1tc0[0] = epd_cl 569 | i -= 1 570 | 571 | # write the same row over and over, weird thing is that we need the sleep otherwise 572 | # the rows we subsequently draw don't draw proper whites leaving ghosts behind - hard to 573 | # understand why the speed at which we "skip" rows affects rows that are drawn later... 574 | while rows > 0: 575 | _Inkplate.vscan_write() 576 | rows -= 1 577 | time.sleep_us(50) 578 | 579 | # _send_row writes a row of data to the display 580 | @micropython.viper 581 | @staticmethod 582 | def _send_row(lut_in, old_framebuf, new_framebuf, row: int): 583 | # cache vars into locals 584 | w1ts0 = ptr32(int(ESP32_GPIO + 4 * W1TS0)) 585 | w1tc0 = ptr32(int(ESP32_GPIO + 4 * W1TC0)) 586 | off = int(EPD_DATA | EPD_CL) # mask with all data bits and clock bit 587 | ofb = ptr8(old_framebuf) 588 | nfb = ptr8(new_framebuf) 589 | ix = int(row * (D_COLS >> 3) + 99) # index into framebuffer 590 | lut = ptr32(lut_in) 591 | # send first byte 592 | odata = int(ofb[ix]) 593 | ndata = int(nfb[ix]) 594 | ix -= 1 595 | w1tc0[0] = off 596 | w1tc0[W1TC1 - W1TC0] = EPD_SPH 597 | if odata == ndata: 598 | w1ts0[0] = off # send all-ones: no change to any of the pixels 599 | w1tc0[0] = EPD_CL 600 | w1ts0[W1TS1 - W1TS0] = EPD_SPH 601 | w1ts0[0] = EPD_CL 602 | w1tc0[0] = off 603 | else: 604 | w1ts0[0] = lut[(odata & 0xF0) + (ndata >> 4)] 605 | w1tc0[0] = off # clear data bits as well ready for next byte 606 | w1ts0[W1TS1 - W1TS0] = EPD_SPH 607 | w1ts0[0] = lut[((odata & 0xF) << 4) + (ndata & 0xF)] 608 | w1tc0[0] = off 609 | # send the remaining bytes (792 pixels) 610 | for c in range(99): 611 | odata = int(ofb[ix]) 612 | ndata = int(nfb[ix]) 613 | ix -= 1 614 | if odata == ndata: 615 | w1ts0[0] = off # send all-ones: no change to any of the pixels 616 | w1tc0[0] = EPD_CL 617 | w1ts0[0] = EPD_CL 618 | w1tc0[0] = off 619 | else: 620 | w1ts0[0] = lut[(odata & 0xF0) + ((ndata >> 4) & 0xF)] 621 | w1tc0[0] = off 622 | w1ts0[0] = lut[((odata & 0xF) << 4) + (ndata & 0xF)] 623 | w1tc0[0] = off 624 | 625 | 626 | class Inkplate: 627 | INKPLATE_1BIT = 0 628 | INKPLATE_2BIT = 1 629 | 630 | BLACK = 1 631 | WHITE = 0 632 | 633 | _width = D_COLS 634 | _height = D_ROWS 635 | 636 | rotation = 0 637 | displayMode = 0 638 | textSize = 1 639 | 640 | def __init__(self, mode): 641 | self.displayMode = mode 642 | try: 643 | os.mount( 644 | sdcard.SDCard( 645 | machine.SPI( 646 | 1, 647 | baudrate=80000000, 648 | polarity=0, 649 | phase=0, 650 | bits=8, 651 | firstbit=0, 652 | sck=Pin(14), 653 | mosi=Pin(13), 654 | miso=Pin(12), 655 | ), 656 | machine.Pin(15), 657 | ), 658 | "/sd", 659 | ) 660 | except: 661 | pass 662 | 663 | def begin(self): 664 | _Inkplate.init(I2C(0, scl=Pin(22), sda=Pin(21))) 665 | 666 | self.ipg = InkplateGS2() 667 | self.ipm = InkplateMono() 668 | self.ipp = InkplatePartial(self.ipm) 669 | 670 | self.GFX = GFX( 671 | D_COLS, 672 | D_ROWS, 673 | self.writePixel, 674 | self.writeFastHLine, 675 | self.writeFastVLine, 676 | self.writeFillRect, 677 | None, 678 | None, 679 | ) 680 | 681 | def clearDisplay(self): 682 | self.ipg.clear() 683 | self.ipm.clear() 684 | 685 | def display(self): 686 | if self.displayMode == 0: 687 | self.ipm.display() 688 | elif self.displayMode == 1: 689 | self.ipg.display() 690 | 691 | def partialUpdate(self): 692 | if self.displayMode == 1: 693 | return 694 | self.ipp.display() 695 | 696 | def clean(self): 697 | self.einkOn() 698 | _Inkplate.clean(0, 1) 699 | _Inkplate.clean(1, 12) 700 | _Inkplate.clean(2, 1) 701 | _Inkplate.clean(0, 11) 702 | _Inkplate.clean(2, 1) 703 | _Inkplate.clean(1, 12) 704 | _Inkplate.clean(2, 1) 705 | _Inkplate.clean(0, 11) 706 | self.einkOff() 707 | 708 | def einkOn(self): 709 | _Inkplate.power_on() 710 | 711 | def einkOff(self): 712 | _Inkplate.power_off() 713 | 714 | def width(self): 715 | return self._width 716 | 717 | def height(self): 718 | return self._height 719 | 720 | # Arduino compatibility functions 721 | def setRotation(self, x): 722 | self.rotation = x % 4 723 | if self.rotation == 0 or self.rotation == 2: 724 | self._width = D_COLS 725 | self._height = D_ROWS 726 | elif self.rotation == 1 or self.rotation == 3: 727 | self._width = D_ROWS 728 | self._height = D_COLS 729 | 730 | def getRotation(self): 731 | return self.rotation 732 | 733 | def drawPixel(self, x, y, c): 734 | self.startWrite() 735 | self.writePixel(x, y, c) 736 | self.endWrite() 737 | 738 | def startWrite(self): 739 | pass 740 | 741 | def writePixel(self, x, y, c): 742 | if x > self.width() - 1 or y > self.height() - 1 or x < 0 or y < 0: 743 | return 744 | if self.rotation == 1: 745 | x, y = y, x 746 | x = self.height() - x - 1 747 | elif self.rotation == 2: 748 | x = self.width() - x - 1 749 | y = self.height() - y - 1 750 | elif self.rotation == 3: 751 | x, y = y, x 752 | y = self.width() - y - 1 753 | (self.ipm.pixel if self.displayMode == self.INKPLATE_1BIT else self.ipg.pixel)( 754 | x, y, c 755 | ) 756 | 757 | def writeFillRect(self, x, y, w, h, c): 758 | for j in range(w): 759 | for i in range(h): 760 | self.writePixel(x + j, y + i, c) 761 | 762 | def writeFastVLine(self, x, y, h, c): 763 | for i in range(h): 764 | self.writePixel(x, y + i, c) 765 | 766 | def writeFastHLine(self, x, y, w, c): 767 | for i in range(w): 768 | self.writePixel(x + i, y, c) 769 | 770 | def writeLine(self, x0, y0, x1, y1, c): 771 | self.GFX.line(x0, y0, x1, y1, c) 772 | 773 | def endWrite(self): 774 | pass 775 | 776 | def drawFastVLine(self, x, y, h, c): 777 | self.startWrite() 778 | self.writeFastVLine(x, y, h, c) 779 | self.endWrite() 780 | 781 | def drawFastHLine(self, x, y, w, c): 782 | self.startWrite() 783 | self.writeFastHLine(x, y, w, c) 784 | self.endWrite() 785 | 786 | def fillRect(self, x, y, w, h, c): 787 | self.startWrite() 788 | self.writeFillRect(x, y, w, h, c) 789 | self.endWrite() 790 | 791 | def fillScreen(self, c): 792 | self.fillRect(0, 0, self.width(), self.height()) 793 | 794 | def drawLine(self, x0, y0, x1, y1, c): 795 | self.startWrite() 796 | self.writeLine(x0, y0, x1, y1, c) 797 | self.endWrite() 798 | 799 | def drawRect(self, x, y, w, h, c): 800 | self.GFX.rect(x, y, w, h, c) 801 | 802 | def drawCircle(self, x, y, r, c): 803 | self.GFX.circle(x, y, r, c) 804 | 805 | def fillCircle(self, x, y, r, c): 806 | self.GFX.fill_circle(x, y, r, c) 807 | 808 | def drawTriangle(self, x0, y0, x1, y1, x2, y2, c): 809 | self.GFX.triangle(x0, y0, x1, y1, x2, y2, c) 810 | 811 | def fillTriangle(self, x0, y0, x1, y1, x2, y2, c): 812 | self.GFX.fill_triangle(x0, y0, x1, y1, x2, y2, c) 813 | 814 | def drawRoundRect(self, x, y, q, h, r, c): 815 | self.GFX.round_rect(x, y, q, h, r, c) 816 | 817 | def fillRoundRect(self, x, y, q, h, r, c): 818 | self.GFX.fill_round_rect(x, y, q, h, r, c) 819 | 820 | def setDisplayMode(self, mode): 821 | self.displayMode = mode 822 | 823 | def selectDisplayMode(self, mode): 824 | self.displayMode = mode 825 | 826 | def getDisplayMode(self): 827 | return self.displayMode 828 | 829 | def setTextSize(self, s): 830 | self.textSize = s 831 | 832 | def setFont(self, f): 833 | self.GFX.font = f 834 | 835 | def printText(self, x, y, s): 836 | self.GFX._very_slow_text(x, y, s, self.textSize, 1) 837 | 838 | def drawBitmap(self, x, y, data, w, h): 839 | byteWidth = (w + 7) // 8 840 | byte = 0 841 | self.startWrite() 842 | for j in range(h): 843 | for i in range(w): 844 | if i & 7: 845 | byte <<= 1 846 | else: 847 | byte = data[j * byteWidth + i // 8] 848 | if byte & 0x80: 849 | self.writePixel(x + i, y + j, 1) 850 | self.endWrite() 851 | 852 | def drawImageFile(self, x, y, path, invert=False): 853 | with open(path, "rb") as f: 854 | header14 = f.read(14) 855 | if header14[0] != 0x42 or header14[1] != 0x4D: 856 | return 0 857 | header40 = f.read(40) 858 | 859 | w = int( 860 | (header40[7] << 24) 861 | + (header40[6] << 16) 862 | + (header40[5] << 8) 863 | + header40[4] 864 | ) 865 | h = int( 866 | (header40[11] << 24) 867 | + (header40[10] << 16) 868 | + (header40[9] << 8) 869 | + header40[8] 870 | ) 871 | dataStart = int((header14[11] << 8) + header14[10]) 872 | 873 | depth = int((header40[15] << 8) + header40[14]) 874 | totalColors = int((header40[33] << 8) + header40[32]) 875 | 876 | rowSize = 4 * ((depth * w + 31) // 32) 877 | 878 | if totalColors == 0: 879 | totalColors = 1 << depth 880 | 881 | palette = None 882 | 883 | if depth <= 8: 884 | palette = [0 for i in range(totalColors)] 885 | p = f.read(totalColors * 4) 886 | for i in range(totalColors): 887 | palette[i] = ( 888 | 54 * p[i * 4] + 183 * p[i * 4 + 1] + 19 * p[i * 4 + 2] 889 | ) >> 14 890 | # print(palette) 891 | f.seek(dataStart) 892 | for j in range(h): 893 | # print(100 * j // h, "% complete") 894 | buffer = f.read(rowSize) 895 | for i in range(w): 896 | val = 0 897 | if depth == 1: 898 | px = int( 899 | invert 900 | ^ (palette[0] < palette[1]) 901 | ^ bool(buffer[i >> 3] & (1 << (7 - i & 7))) 902 | ) 903 | val = palette[px] 904 | elif depth == 4: 905 | px = (buffer[i >> 1] & (0x0F if i & 1 == 1 else 0xF0)) >> ( 906 | 0 if i & 1 else 4 907 | ) 908 | val = palette[px] 909 | if invert: 910 | val = 3 - val 911 | elif depth == 8: 912 | px = buffer[i] 913 | val = palette[px] 914 | if invert: 915 | val = 3 - val 916 | elif depth == 16: 917 | px = (buffer[(i << 1) | 1] << 8) | buffer[(i << 1)] 918 | 919 | r = (px & 0x7C00) >> 7 920 | g = (px & 0x3E0) >> 2 921 | b = (px & 0x1F) << 3 922 | 923 | val = (54 * r + 183 * g + 19 * b) >> 14 924 | 925 | if invert: 926 | val = 3 - val 927 | elif depth == 24: 928 | r = buffer[i * 3] 929 | g = buffer[i * 3 + 1] 930 | b = buffer[i * 3 + 2] 931 | 932 | val = (54 * r + 183 * g + 19 * b) >> 14 933 | 934 | if invert: 935 | val = 3 - val 936 | elif depth == 32: 937 | r = buffer[i * 4] 938 | g = buffer[i * 4 + 1] 939 | b = buffer[i * 4 + 2] 940 | 941 | val = (54 * r + 183 * g + 19 * b) >> 14 942 | 943 | if invert: 944 | val = 3 - val 945 | 946 | if self.getDisplayMode() == self.INKPLATE_1BIT: 947 | val >>= 1 948 | 949 | self.drawPixel(x + i, y + h - j, val) 950 | 951 | --------------------------------------------------------------------------------