# main.py — cycle through all Expressions on SSD1306 (ESP32 + MicroPython)
from machine import Pin, I2C
import utime
import ssd1306
WHITE = 1
BLACK = 0
class Expressions:
SCREEN_WIDTH = 128
SCREEN_HEIGHT = 64
def __init__(self, display):
self.display = display
# State
self.prevBlinkTime = utime.ticks_ms()
self.happy_state = False
self.q = -20 # upset() slider
# MicroPython ssd1306 has fixed 8x8-ish font (Arduino text size 1)
# ---------- low-level helpers (MicroPython ssd1306 lacks these) ----------
def _pixel(self, x, y, c=WHITE):
if 0 <= x < self.SCREEN_WIDTH and 0 <= y < self.SCREEN_HEIGHT:
self.display.pixel(x, y, c)
def _hline(self, x, y, w, c=WHITE):
if w <= 0: return
self.display.hline(x, y, w, c)
def _vline(self, x, y, h, c=WHITE):
if h <= 0: return
self.display.vline(x, y, h, c)
def _rect(self, x, y, w, h, c=WHITE):
self.display.rect(x, y, w, h, c)
def _fill_rect(self, x, y, w, h, c=WHITE):
# clamp
if w <= 0 or h <= 0: return
self.display.fill_rect(x, y, w, h, c)
def _draw_circle(self, x0, y0, r, c=WHITE):
# midpoint circle (8-way symmetry)
f = 1 - r
ddF_x = 1
ddF_y = -2 * r
x = 0
y = r
self._pixel(x0, y0 + r, c)
self._pixel(x0, y0 - r, c)
self._pixel(x0 + r, y0, c)
self._pixel(x0 - r, y0, c)
while x < y:
if f >= 0:
y -= 1
ddF_y += 2
f += ddF_y
x += 1
ddF_x += 2
f += ddF_x
self._pixel(x0 + x, y0 + y, c)
self._pixel(x0 - x, y0 + y, c)
self._pixel(x0 + x, y0 - y, c)
self._pixel(x0 - x, y0 - y, c)
self._pixel(x0 + y, y0 + x, c)
self._pixel(x0 - y, y0 + x, c)
self._pixel(x0 + y, y0 - x, c)
self._pixel(x0 - y, y0 - x, c)
def _fill_circle(self, x0, y0, r, c=WHITE):
for y in range(-r, r + 1):
# horizontal span length at this y
dx = int((r*r - y*y) ** 0.5)
self._hline(x0 - dx, y0 + y, 2*dx + 1, c)
def _fill_triangle(self, x0, y0, x1, y1, x2, y2, c=WHITE):
# scanline triangle fill
# sort by y
if y0 > y1: x0, x1, y0, y1 = x1, x0, y1, y0
if y1 > y2: x1, x2, y1, y2 = x2, x1, y2, y1
if y0 > y1: x0, x1, y0, y1 = x1, x0, y1, y0
def interp(xa, ya, xb, yb, y):
if yb == ya: return xa
return int(xa + (xb - xa) * (y - ya) / (yb - ya))
# top to middle
for y in range(y0, y1 + 1):
xa = interp(x0, y0, x2, y2, y)
xb = interp(x0, y0, x1, y1, y)
if xa > xb: xa, xb = xb, xa
self._hline(xa, y, xb - xa + 1, c)
# middle to bottom
for y in range(y1, y2 + 1):
xa = interp(x0, y0, x2, y2, y)
xb = interp(x1, y1, x2, y2, y)
if xa > xb: xa, xb = xb, xa
self._hline(xa, y, xb - xa + 1, c)
def _fill_round_rect(self, x, y, w, h, r, c=WHITE):
# body
self._fill_rect(x + r, y, w - 2*r, h, c)
self._fill_rect(x, y + r, r, h - 2*r, c)
self._fill_rect(x + w - r, y + r, r, h - 2*r, c)
# corners
self._fill_circle(x + r, y + r, r, c)
self._fill_circle(x + w - r - 1, y + r, r, c)
self._fill_circle(x + r, y + h - r - 1, r, c)
self._fill_circle(x + w - r - 1, y + h - r - 1, r, c)
def _draw_line(self, x0, y0, x1, y1, c=WHITE):
self.display.line(x0, y0, x1, y1, c)
def _text(self, s, x, y, c=WHITE):
self.display.text(s, x, y, c)
def _clear(self):
self.display.fill(0)
def _show(self):
self.display.show()
def _delay(self, ms):
utime.sleep_ms(ms)
# ------------------------- expressions (ported) --------------------------
def close(self):
self._clear()
# closed eyes as filled rounded rectangles
self._fill_round_rect(5, 19, 55, 18, 6, WHITE)
self._fill_round_rect(67, 19, 55, 18, 6, WHITE)
# clear top portions
self._fill_rect(5, 1, 55, 18, BLACK)
self._fill_rect(67, 1, 55, 18, BLACK)
self._show()
def normal(self):
self._clear()
self._fill_round_rect(8, 12, 50, 35, 9, WHITE)
self._fill_round_rect(70, 12, 50, 35, 9, WHITE)
self._show()
self.happy_state = False
def blink(self):
now = utime.ticks_ms()
if utime.ticks_diff(now, self.prevBlinkTime) > 2000:
self.close()
self._delay(50)
self.normal()
self.prevBlinkTime = now
def sad(self):
for i in range(0, 16, 3):
self._clear()
self._fill_round_rect(8, 18, 50, 29, 9, WHITE)
self._fill_round_rect(70, 18, 50, 29, 9, WHITE)
# eyebrows as triangles
self._fill_triangle(3, 14, 64, 14, 3, 21+i, BLACK)
self._fill_triangle(68, 14, 124, 21+i, 124, 14, BLACK)
self._show()
def upset(self):
if not self.happy_state:
self._clear()
self._fill_round_rect(8, 12, 50, 35, 9, WHITE)
self._fill_round_rect(70, 12, 50, 35, 9, WHITE)
# clear top portion to “crop” eyes
self._fill_rect(8, self.q, 50, 35, BLACK)
self._fill_rect(70, self.q, 50, 35, BLACK)
self._show()
if self.q <= -7:
self.q += 3
def happy(self):
for i in range(62, 58, -3):
self._clear()
self._fill_round_rect(8, 12, 50, 35, 11, WHITE)
self._fill_round_rect(70, 12, 50, 35, 11, WHITE)
self._fill_circle(33, i, 38, BLACK)
self._fill_circle(95, i, 38, BLACK)
self._show()
self.happy_state = True
def cute(self):
for i in range(0, 3, 2):
self._clear()
self._fill_round_rect(8, 12, 50, 35, 12, WHITE)
self._fill_round_rect(70, 12, 50, 35, 12, WHITE)
self._fill_circle(30, 66 - i, 40, BLACK)
self._fill_circle(98, 66 - i, 40, BLACK)
self._show()
self.happy_state = True
def angry(self):
for i in range(0, 16, 3):
self._clear()
self._fill_round_rect(8, 18, 50, 29, 9, WHITE)
self._fill_round_rect(70, 18, 50, 29, 9, WHITE)
self._fill_triangle(3, 14, 64, 18 + i, 124, 14, BLACK)
self._show()
def sleepy(self):
for i in range(0, 12, 2):
self._clear()
self._fill_round_rect(8, 12 + i, 50, 25, 9, WHITE)
self._fill_round_rect(70, 12 + i, 50, 25, 9, WHITE)
self._text("z", 100 - i, 40 - i, WHITE)
self._text("z", 110 - i, 30 - i, WHITE)
self._text("z", 120 - i, 20 - i, WHITE)
self._show()
self._delay(100)
def wink(self):
self._clear()
# right eye normal
self._fill_round_rect(70, 12, 50, 35, 9, WHITE)
# left eye closed
self._fill_round_rect(5, 19, 55, 18, 6, WHITE)
self._fill_rect(5, 1, 55, 18, BLACK)
self._show()
self._delay(300)
self.normal()
def surprised(self):
self._clear()
self._fill_circle(33, 30, 20, WHITE)
self._fill_circle(95, 30, 20, WHITE)
self._draw_line(13, 5, 53, 5, WHITE)
self._draw_line(75, 5, 115, 5, WHITE)
self._show()
def confused(self):
self._clear()
self._fill_round_rect(8, 12, 50, 35, 9, WHITE)
self._fill_circle(95, 30, 20, WHITE)
self._draw_line(75, 5, 115, 15, WHITE)
self._show()
def love(self):
self._clear()
for x in (33, 95):
self._fill_circle(x - 7, 25, 10, WHITE)
self._fill_circle(x + 7, 25, 10, WHITE)
self._fill_triangle(x - 15, 30, x + 15, 30, x, 45, WHITE)
self._show()
def dizzy(self):
self._clear()
for centerX in (33, 95):
centerY = 30
for r in range(0, 15, 3):
self._draw_circle(centerX, centerY, r, WHITE)
self._draw_line(centerX - r, centerY - r, centerX + r, centerY + r, WHITE)
self._draw_line(centerX - r, centerY + r, centerX + r, centerY - r, WHITE)
self._show()
def thinking(self):
self._clear()
self._fill_round_rect(8, 15, 50, 25, 9, WHITE)
self._fill_round_rect(70, 15, 50, 25, 9, WHITE)
self._fill_circle(110, 15, 3, WHITE)
self._fill_circle(115, 10, 4, WHITE)
self._fill_circle(122, 5, 5, WHITE)
self._show()
def mischievous(self):
self._clear()
self._fill_round_rect(8, 12, 50, 25, 9, WHITE)
self._fill_round_rect(70, 18, 50, 25, 9, WHITE)
self._draw_line(8, 5, 58, 15, WHITE)
self._draw_line(70, 15, 120, 5, WHITE)
self._show()
def crying(self):
for i in range(0, 15, 3):
self._clear()
self._fill_round_rect(8, 18, 50, 29, 9, WHITE)
self._fill_round_rect(70, 18, 50, 29, 9, WHITE)
self._fill_round_rect(20, 45 + i, 3, 5, 1, WHITE)
self._fill_round_rect(82, 45 + i, 3, 5, 1, WHITE)
self._show()
self._delay(100)
def nervous(self):
for i in range(0, 4):
self._clear()
dx = (i % 2) * 2
self._fill_round_rect(8 + dx, 12, 50, 35, 9, WHITE)
self._fill_round_rect(70 + dx, 12, 50, 35, 9, WHITE)
self._fill_round_rect(115, 20 + i*2, 3, 5, 1, WHITE)
self._show()
self._delay(100)
# --- OLED setup (ESP32 default pins for Wokwi/most dev boards) ---
i2c = I2C(0, scl=Pin(22), sda=Pin(21))
oled = ssd1306.SSD1306_I2C(128, 64, i2c)
expr = Expressions(oled)
# --- tiny helpers for pacing ---
def pause(ms=700):
utime.sleep_ms(ms)
def animate(func, frames=6, delay_ms=120):
# call an expression a few times to let its internal state move
for _ in range(frames):
func()
utime.sleep_ms(delay_ms)
# --- sequence of faces to show, in a loop ---
# Methods that already animate internally (e.g., sleepy, crying) are given
# a longer pause afterwards so you can see them finish before switching.
sequence = [
("normal", lambda: (expr.normal(),)),
("happy", lambda: (expr.happy(),)),
("cute", lambda: (expr.cute(),)),
("angry", lambda: (expr.angry(), pause(250))),
("sad", lambda: (expr.sad(), pause(250))),
("sleepy", lambda: (expr.sleepy(), pause(350))),
("wink", lambda: (expr.wink(), pause(200))),
("surprised", lambda: (expr.surprised(), pause(600))),
("confused", lambda: (expr.confused(), pause(600))),
("love", lambda: (expr.love(), pause(800))),
("dizzy", lambda: (expr.dizzy(), pause(800))),
("thinking", lambda: (expr.thinking(), pause(800))),
("mischievous", lambda: (expr.mischievous(), pause(800))),
("crying", lambda: (expr.crying(), pause(300))),
("nervous", lambda: (expr.nervous(), pause(300))),
# upset() uses an internal slider q; call multiple times to reveal motion
("upset", lambda: (animate(expr.upset, frames=6, delay_ms=140), pause(400))),
# a neat palate cleanser:
("close", lambda: (expr.close(), pause(400))),
]
# --- run forever, blinking occasionally between faces ---
try:
# show something immediately
expr.normal()
pause(500)
while True:
for name, action in sequence:
# optional: a quick natural blink between some faces
start = utime.ticks_ms()
# do the face/animation
action()
# opportunistic blink every ~2s of real time
while utime.ticks_diff(utime.ticks_ms(), start) < 2000:
expr.blink()
utime.sleep_ms(80)
# loop back and do them again
except KeyboardInterrupt:
# tidy exit if you Ctrl+C from a REPL
oled.fill(0)
oled.show()