|
| 1 | +# qteye_blink_qualia.py - a stand-alone round LCD "eye" on a ESP32-S3 Qualia board |
| 2 | +# 16 Oct 2023 - @todbot / Tod Kurt |
| 3 | +# Part of circuitpython-tricks/larger-tricks/eyeballs |
| 4 | +# also see: https://todbot.com/blog/2022/05/19/multiple-displays-in-circuitpython-compiling-custom-circuitpython/ |
| 5 | + |
| 6 | +import time, math, random |
| 7 | +import board, busio |
| 8 | +import displayio |
| 9 | +import adafruit_imageload |
| 10 | + |
| 11 | +# config: behaviors |
| 12 | +eye_twitch_time = 2 # bigger is less twitchy |
| 13 | +eye_twitch_amount = 20 # allowable deviation from center for iris |
| 14 | +eye_blink_time = 1.8 # bigger is slower |
| 15 | +eye_rotation = 00 # 0 or 180, don't do 90 or 270 because too slow (on gc9a01) |
| 16 | + |
| 17 | + |
| 18 | +# config: wiring for QT Py, should work on any QT Py or XIAO board, but ESP32-S3 is fastest |
| 19 | +# import gc9a01 |
| 20 | +# spi0 = busio.SPI(clock=tft0_clk, MOSI=tft0_mosi) |
| 21 | +# tft0_clk = board.SCK |
| 22 | +# tft0_mosi = board.MOSI |
| 23 | +# tft_L0_rst = board.MISO |
| 24 | +# tft_L0_dc = board.RX |
| 25 | +# tft_L0_cs = board.TX |
| 26 | +# display_bus = displayio.FourWire(spi, command=dc, chip_select=cs, reset=rst, baudrate=64_000_000) |
| 27 | +# display = gc9a01.GC9A01(display_bus, width=dw, height=dh, rotation=rot) |
| 28 | +# display.auto_refresh = False |
| 29 | +# dw, dh = 240, 240 # display dimensions |
| 30 | + |
| 31 | +# config: for Qualia ESP32S3 board and 480x480 round display |
| 32 | +# followed the instructions here: |
| 33 | +# https://learn.adafruit.com/adafruit-qualia-esp32-s3-for-rgb666-displays/circuitpython-display-setup |
| 34 | +# and cribbed from @dexter/@rsbohn's work: |
| 35 | +# https://gist.github.com/rsbohn/26a8e69c8fe80112a24e7de09177e8d9 |
| 36 | +import dotclockframebuffer |
| 37 | +from framebufferio import FramebufferDisplay |
| 38 | + |
| 39 | +init_sequence_TL021WVC02 = bytes(( |
| 40 | + b'\xff\x05w\x01\x00\x00\x10' |
| 41 | + b'\xc0\x02;\x00' |
| 42 | + b'\xc1\x02\x0b\x02' |
| 43 | + b'\xc2\x02\x00\x02' |
| 44 | + b'\xcc\x01\x10' |
| 45 | + b'\xcd\x01\x08' |
| 46 | + b'\xb0\x10\x02\x13\x1b\r\x10\x05\x08\x07\x07$\x04\x11\x0e,3\x1d' |
| 47 | + b'\xb1\x10\x05\x13\x1b\r\x11\x05\x08\x07\x07$\x04\x11\x0e,3\x1d' |
| 48 | + b'\xff\x05w\x01\x00\x00\x11' |
| 49 | + b'\xb0\x01]' |
| 50 | + b'\xb1\x01C' |
| 51 | + b'\xb2\x01\x81' |
| 52 | + b'\xb3\x01\x80' |
| 53 | + b'\xb5\x01C' |
| 54 | + b'\xb7\x01\x85' |
| 55 | + b'\xb8\x01 ' |
| 56 | + b'\xc1\x01x' |
| 57 | + b'\xc2\x01x' |
| 58 | + b'\xd0\x01\x88' |
| 59 | + b'\xe0\x03\x00\x00\x02' |
| 60 | + b'\xe1\x0b\x03\xa0\x00\x00\x04\xa0\x00\x00\x00 ' |
| 61 | + b'\xe2\r\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' |
| 62 | + b'\xe3\x04\x00\x00\x11\x00' |
| 63 | + b'\xe4\x02"\x00' |
| 64 | + b'\xe5\x10\x05\xec\xa0\xa0\x07\xee\xa0\xa0\x00\x00\x00\x00\x00\x00\x00\x00' |
| 65 | + b'\xe6\x04\x00\x00\x11\x00' |
| 66 | + b'\xe7\x02"\x00' |
| 67 | + b'\xe8\x10\x06\xed\xa0\xa0\x08\xef\xa0\xa0\x00\x00\x00\x00\x00\x00\x00\x00' |
| 68 | + b'\xeb\x07\x00\x00@@\x00\x00\x00' |
| 69 | + b'\xed\x10\xff\xff\xff\xba\n\xbfE\xff\xffT\xfb\xa0\xab\xff\xff\xff' |
| 70 | + b'\xef\x06\x10\r\x04\x08?\x1f' |
| 71 | + b'\xff\x05w\x01\x00\x00\x13' |
| 72 | + b'\xef\x01\x08' |
| 73 | + b'\xff\x05w\x01\x00\x00\x00' |
| 74 | + b'6\x01\x00' |
| 75 | + b':\x01`' |
| 76 | + b'\x11\x80d' |
| 77 | + b')\x802' |
| 78 | +)) |
| 79 | + |
| 80 | +# I'm just guessing at all of this |
| 81 | +tft_timings = { |
| 82 | + "frequency": 6_500_000, |
| 83 | + "width": 480, |
| 84 | + "height": 480, |
| 85 | + "hsync_pulse_width": 20, |
| 86 | + "hsync_front_porch": 40, |
| 87 | + "hsync_back_porch": 40, |
| 88 | + "vsync_pulse_width": 10, |
| 89 | + "vsync_front_porch": 40, |
| 90 | + "vsync_back_porch": 40, |
| 91 | + "hsync_idle_low": False, |
| 92 | + "vsync_idle_low": False, |
| 93 | + "de_idle_high": False, |
| 94 | + "pclk_active_high": False, |
| 95 | + "pclk_idle_high": False, |
| 96 | +} |
| 97 | + |
| 98 | +displayio.release_displays() |
| 99 | +board.I2C().deinit() |
| 100 | +i2c = busio.I2C(board.SCL, board.SDA, frequency=400_000) |
| 101 | +dotclockframebuffer.ioexpander_send_init_sequence( |
| 102 | + i2c, init_sequence_TL021WVC02, **(dict(board.TFT_IO_EXPANDER))) |
| 103 | +i2c.deinit() |
| 104 | + |
| 105 | +fb = dotclockframebuffer.DotClockFramebuffer( |
| 106 | + **(dict(board.TFT_PINS)), **tft_timings) |
| 107 | +display = FramebufferDisplay(fb, rotation=eye_rotation) |
| 108 | +display.auto_refresh=False |
| 109 | + |
| 110 | +dw_real, dh_real = 480, 480 |
| 111 | +dw, dh = 240,240 # for emulation with 240x240 displays |
| 112 | + |
| 113 | +# load our eye and iris bitmaps |
| 114 | +## static so load from disk (also can't have it it RAM and eyelids in RAM too) |
| 115 | +eyeball_bitmap = displayio.OnDiskBitmap(open("/imgs/eye0_ball2.bmp", "rb")) |
| 116 | +eyeball_pal = eyeball_bitmap.pixel_shader |
| 117 | +## moves around, so load into RAM |
| 118 | +iris_bitmap, iris_pal = adafruit_imageload.load("imgs/eye0_iris0.bmp") |
| 119 | +iris_pal.make_transparent(0) |
| 120 | +## also moves, so load into RAM (hopefully) |
| 121 | +try: |
| 122 | + eyelid_bitmap, eyelid_pal = adafruit_imageload.load("/imgs/eyelid_spritesheet2.bmp") |
| 123 | +except Exception as e: |
| 124 | + print("couldn't load",e) |
| 125 | + eyelid_bitmap = displayio.OnDiskBitmap(open("/imgs/eyelid_spritesheet2.bmp", "rb")) |
| 126 | + eyelid_pal = eyelid_bitmap.pixel_shader |
| 127 | +eyelid_sprite_cnt = eyelid_bitmap.width // dw # should be 16 |
| 128 | +eyelid_pal.make_transparent(1) |
| 129 | + |
| 130 | +# compute or declare some useful info about the eyes |
| 131 | +iris_w, iris_h = iris_bitmap.width, iris_bitmap.height # iris is normally 110x110 |
| 132 | +iris_cx, iris_cy = dw//2 - iris_w//2, dh//2 - iris_h//2 |
| 133 | + |
| 134 | + |
| 135 | +# class to help us track eye info (not needed for this use exactly, but I find it interesting) |
| 136 | +class Eye: |
| 137 | + """ |
| 138 | + global variables used: |
| 139 | + - iris_cx, iris_cy |
| 140 | + - eye_twitch_amount |
| 141 | + - eye_twitch_time |
| 142 | + - eye_blink_time |
| 143 | + """ |
| 144 | + def __init__(self, display, eye_speed=0.25): |
| 145 | + # ends up being 80 MHz on ESP32-S3, nice |
| 146 | + main = displayio.Group(scale=2) |
| 147 | + display.root_group = main |
| 148 | + self.display = display |
| 149 | + self.eyeball = displayio.TileGrid(eyeball_bitmap, pixel_shader=eyeball_pal) |
| 150 | + self.iris = displayio.TileGrid(iris_bitmap, pixel_shader=iris_pal, x=iris_cx,y=iris_cy) |
| 151 | + self.lids = displayio.TileGrid(eyelid_bitmap, pixel_shader=eyelid_pal, x=0, y=0, tile_width=dw, tile_height=dw) |
| 152 | + main.append(self.eyeball) |
| 153 | + main.append(self.iris) |
| 154 | + main.append(self.lids) |
| 155 | + self.x, self.y = iris_cx, iris_cy |
| 156 | + self.tx, self.ty = self.x, self.y |
| 157 | + self.next_time = 0 |
| 158 | + self.eye_speed = eye_speed |
| 159 | + self.lidpos = 0 |
| 160 | + self.lidpos_inc = 1 |
| 161 | + self.lid_next_time = 0 |
| 162 | + |
| 163 | + def update(self): |
| 164 | + # make the eye twitch around |
| 165 | + self.x = self.x * (1-self.eye_speed) + self.tx * self.eye_speed # "easing" |
| 166 | + self.y = self.y * (1-self.eye_speed) + self.ty * self.eye_speed |
| 167 | + self.iris.x = int( self.x ) |
| 168 | + self.iris.y = int( self.y ) # + 10 # have it look down a bit FIXME |
| 169 | + if time.monotonic() > self.next_time: |
| 170 | + # pick a new "target" for the eye to look at |
| 171 | + t = random.uniform(0.25, eye_twitch_time) |
| 172 | + self.next_time = time.monotonic() + t |
| 173 | + self.tx = iris_cx + random.uniform(-eye_twitch_amount,eye_twitch_amount) |
| 174 | + self.ty = iris_cy + random.uniform(-eye_twitch_amount,eye_twitch_amount) |
| 175 | + # elif to minimize display changes per update |
| 176 | + elif time.monotonic() > self.lid_next_time: |
| 177 | + # make the eye blink its eyelids |
| 178 | + self.lid_next_time = time.monotonic() + random.uniform( eye_blink_time*0.5, eye_blink_time*1.5) |
| 179 | + self.lidpos = self.lidpos + self.lidpos_inc |
| 180 | + self.lids[0] = self.lidpos |
| 181 | + if self.lidpos == 0 or self.lidpos == eyelid_sprite_cnt-1: |
| 182 | + self.lidpos_inc *= -1 # change direction |
| 183 | + |
| 184 | + self.display.refresh() |
| 185 | + |
| 186 | + |
| 187 | + |
| 188 | +# a list of all the eyes, in this case, only one |
| 189 | +the_eyes = [ |
| 190 | + Eye( display ), |
| 191 | +] |
| 192 | + |
| 193 | +while True: |
| 194 | + for eye in the_eyes: |
| 195 | + eye.update() |
0 commit comments