// """
// Animasi Mata & Mulut untuk OLED 0.92" (128x64) pada ESP32
// File ini dibuat untuk MicroPython (ESP32).
// Fitur:
// - Mata berkedip
// - Bola mata bergerak mengikuti pola sederhana
// - Mulut bergerak (berbicara / tersenyum)
// Asumsi:
// - OLED 0.92" = 128x64 (SSD1306 umum). I2C address 0x3C.
// - SDA -> GPIO21, SCL -> GPIO22 (ubah sesuai wiring Anda)
// - Jika modul Anda menggunakan SH1106, driver harus diganti.
// Cara pakai:
// 1. Salin file ini ke ESP32 (mis. sebagai main.py) menggunakan ampy/rshell/Thonny.
// 2. Nyalakan ESP32. Animasi otomatis berjalan.
// Catatan: jika modul MicroPython Anda tidak punya modul `ssd1306`, upload driver ssd1306.py atau gunakan driver yang sesuai.
// """
import uasyncio as asyncio
from machine import I2C, Pin
import time
# Coba import driver ssd1306 bawaan; jika tidak ada, user harus upload driver ssd1306.py
try:
from ssd1306 import SSD1306_I2C
except Exception as e:
raise ImportError("Library ssd1306 tidak ditemukan. Upload ssd1306.py ke board Anda atau gunakan firmware yang menyertakan modul ini.\nAsumsi: SSD1306 128x64 via I2C.")
# ====== Konfigurasi ======
WIDTH = 128
HEIGHT = 64
SDA_PIN = 21
SCL_PIN = 22
I2C_FREQ = 400000
I2C_ADDR = 0x3C # biasanya 0x3C atau 0x3D
# Inisialisasi I2C dan display
i2c = I2C(0, scl=Pin(SCL_PIN), sda=Pin(SDA_PIN), freq=I2C_FREQ)
disp = SSD1306_I2C(WIDTH, HEIGHT, i2c, addr=I2C_ADDR)
# ====== Helper Gambar ======
# Kita akan menggunakan koordinat dan bentuk sederhana: lingkaran mata dengan bola mata (di-draw sebagai kotak kecil)
def clear():
disp.fill(0)
def show():
disp.show()
# Karena SSD1306 framebuf tidak punya fungsi lingkaran, kita gunakan algoritma Midpoint circle ringan
# atau menggambar elips dengan titik-titik.
def circle(x0, y0, r):
x = r
y = 0
err = 0
while x >= y:
pixs = [
(x0 + x, y0 + y), (x0 + y, y0 + x),
(x0 - y, y0 + x), (x0 - x, y0 + y),
(x0 - x, y0 - y), (x0 - y, y0 - x),
(x0 + y, y0 - x), (x0 + x, y0 - y)
]
for px, py in pixs:
if 0 <= px < WIDTH and 0 <= py < HEIGHT:
disp.pixel(int(px), int(py), 1)
y += 1
if err <= 0:
err += 2*y + 1
if err > 0:
x -= 1
err -= 2*x + 1
# Mata penuh (outline + pupil)
def draw_eye(x, y, r, pupil_offset_x=0, open_ratio=1.0):
# open_ratio 1.0 = sepenuhnya buka; 0.0 = tertutup
# gambar outline lingkaran
if open_ratio <= 0:
# tutup mata: garis horizontal sebagai kelopak
disp.hline(x - r, y, 2*r, 1)
return
circle(x, y, r)
# isi pupil sebagai lingkaran kecil (pakai kotak agar cepat)
pr = max(1, r//3)
px = x + int(pupil_offset_x)
py = y
# ketika mata "setengah buka", kecilkan pupil dan hapus beberapa titik di atas/bawah
for i in range(-pr, pr+1):
for j in range(-pr, pr+1):
if (px+i) >= 0 and (py+j) >= 0 and (px+i) < WIDTH and (py+j) < HEIGHT:
disp.pixel(px+i, py+j, 1)
# Mulut sederhana sebagai kurva (bezier sederhana lewat 3 titik) atau garis bergelombang
def draw_mouth(x, y, width, height, smile=0.0):
# smile dari -1.0 (masam) sampai +1.0 (tersenyum)
# kita gambar beberapa titik pada rentang x -w..+w
steps = width
for i in range(-steps//2, steps//2):
t = (i + steps//2) / steps # 0..1
# kurva quad: y = a*(t-0.5)^2 + offset; a negatif -> smile
a = -4 * smile # factor pengaruh smile
yy = int(y + a * (t - 0.5)**2 * height)
dx = x + i
if 0 <= dx < WIDTH and 0 <= yy < HEIGHT:
disp.pixel(dx, yy, 1)
# ====== Animasi menggunakan uasyncio ======
async def eye_blink_task(left_pos, right_pos, r):
# loop animasi kedip dan gerak bola mata
pupil_dir = 1
pupil_x = 0
blink = False
blink_timer = 0
while True:
clear()
# update pupil pos (berpindah pelan)
pupil_x += pupil_dir
if abs(pupil_x) > 6:
pupil_dir *= -1
# acak blink setiap beberapa detik
blink_timer += 1
if blink_timer > 30:
# mulai kedip
blink = True
blink_timer = 0
# jika blink aktif, tampilkan frame tertutup beberapa siklus
if blink:
# 3 frame tertutup lalu buka
draw_eye(left_pos[0], left_pos[1], r, pupil_offset_x=0, open_ratio=0.0)
draw_eye(right_pos[0], right_pos[1], r, pupil_offset_x=0, open_ratio=0.0)
draw_mouth(WIDTH//2, 48, 40, 8, smile=0.6) # ekspresi saat kedip
show()
await asyncio.sleep(0.12)
blink = False
continue
# gambar mata normal
draw_eye(left_pos[0], left_pos[1], r, pupil_offset_x=pupil_x)
draw_eye(right_pos[0], right_pos[1], r, pupil_offset_x=pupil_x)
# gambar alis (opsional)
disp.hline(left_pos[0]-r, left_pos[1]-r-6, 2*r, 1)
disp.hline(right_pos[0]-r, right_pos[1]-r-6, 2*r, 1)
# mouth bergerak sinkron dengan pupil untuk ilusi berbicara
mouth_smile = (pupil_x / 6.0) # -1..1 kira2
draw_mouth(WIDTH//2, 50, 48, 12, smile=mouth_smile)
show()
await asyncio.sleep(0.07)
async def idle_breath_task():
# tugas tambahan: membuat background subtle "breath" effect (opsional)
while True:
# untuk sekarang tidak melakukan apa-apa, tapi bisa dipakai untuk blink acak
await asyncio.sleep(1)
async def main():
# Posisi mata (tweak bila perlu)
left = (40, 24)
right = (88, 24)
r = 12
# Clear dan jalankan tugas
clear()
show()
await asyncio.gather(eye_blink_task(left, right, r), idle_breath_task())
# ====== Jalankan ======
try:
asyncio.run(main())
except Exception as e:
# pada MicroPython, jika terjadi error, tampilkan di REPL
print('Error animasi:', e)
# Reset display agar tidak meninggalkan artefak
clear()
show()
# Jika anda ingin menghentikan uasyncio pada MicroPython tertentu, gunakan: asyncio.new_event_loop()