190 lines
5.5 KiB
Python
190 lines
5.5 KiB
Python
#!/usr/bin/env python
|
|
from lib import LCD_2inch
|
|
import lgpio
|
|
from PIL import Image, ImageDraw
|
|
import time
|
|
import numpy as np
|
|
import random
|
|
import spidev
|
|
import usb.core
|
|
import usb.util
|
|
from tuning import Tuning
|
|
|
|
# === Display Configuration ===
|
|
RST1 = 27
|
|
DC1 = 25
|
|
BL1 = 18
|
|
RST2 = 22
|
|
DC2 = 23
|
|
BL2 = 24
|
|
bus = 0
|
|
|
|
# === Initialize GPIO Handle ===
|
|
h = lgpio.gpiochip_open(0)
|
|
|
|
# === Initialize Displays ===
|
|
disp1 = LCD_2inch.LCD_2inch(spi=spidev.SpiDev(bus, 0), rst=RST1, dc=DC1, bl=BL1)
|
|
disp1.Init()
|
|
disp1.clear()
|
|
disp1.bl_DutyCycle(50)
|
|
|
|
disp2 = LCD_2inch.LCD_2inch(spi=spidev.SpiDev(bus, 1), rst=RST2, dc=DC2, bl=BL2)
|
|
disp2.Init()
|
|
disp2.clear()
|
|
disp2.bl_DutyCycle(50)
|
|
|
|
# === Image and Drawing Setup ===
|
|
WIDTH = disp1.width
|
|
HEIGHT = disp1.height
|
|
CENTER = (WIDTH // 2, HEIGHT // 2)
|
|
|
|
# === Animation Parameters ===
|
|
segment_count = 12
|
|
segment_width = 8
|
|
segment_length = 4
|
|
base_iris_radius = 30
|
|
pulse_range = 5
|
|
rotation_speed = 0.002
|
|
sleep_time = 0.05
|
|
|
|
angle_offset = 0
|
|
fixed_outer_radius = 75
|
|
|
|
# === State Management ===
|
|
class EyeState:
|
|
IDLE = "idle"
|
|
LISTENING = "listening"
|
|
RESPONDING = "responding"
|
|
PLEASURE = "pleasure"
|
|
|
|
state = EyeState.IDLE
|
|
state_timer = time.time()
|
|
|
|
# === Idle Color Pulse Setup ===
|
|
pulse_timer = 0
|
|
pulse_duration = 8.0
|
|
|
|
# === DoA Detection Setup ===
|
|
dev = usb.core.find(idVendor=0x2886, idProduct=0x0018)
|
|
mic_tuning = Tuning(dev) if dev else None
|
|
|
|
def get_doa_angle():
|
|
if mic_tuning:
|
|
try:
|
|
return mic_tuning.direction
|
|
except Exception as e:
|
|
print(f"DoA read error: {e}")
|
|
return None
|
|
return None
|
|
|
|
last_doa_poll = 0
|
|
iris_offset = (0, 0)
|
|
doa_angle = 180
|
|
|
|
# === Flick Timer ===
|
|
last_flick_time = time.time()
|
|
flick_interval = random.uniform(15, 30)
|
|
flick_active = False
|
|
flick_duration = 0.2
|
|
flick_start = 0
|
|
flick_offset = (0, 0)
|
|
|
|
try:
|
|
while True:
|
|
current_time = time.time()
|
|
elapsed = current_time - pulse_timer
|
|
|
|
# === Flick logic ===
|
|
if not flick_active and current_time - last_flick_time > flick_interval:
|
|
flick_active = True
|
|
flick_start = current_time
|
|
flick_offset = (random.randint(-8, 8), random.randint(-4, 4))
|
|
last_flick_time = current_time
|
|
flick_interval = random.uniform(15, 30)
|
|
|
|
if flick_active:
|
|
if current_time - flick_start < flick_duration:
|
|
current_offset = flick_offset
|
|
else:
|
|
flick_active = False
|
|
current_offset = iris_offset
|
|
else:
|
|
current_offset = iris_offset
|
|
|
|
# === Poll DoA every 0.5s ===
|
|
if current_time - last_doa_poll > 0.5:
|
|
angle = get_doa_angle()
|
|
if angle is not None:
|
|
doa_angle = angle
|
|
offset_x = int(-20 * np.sin(np.radians(doa_angle)))
|
|
offset_y = int(-10 * np.cos(np.radians(doa_angle)))
|
|
iris_offset = (offset_x, offset_y)
|
|
last_doa_poll = current_time
|
|
|
|
# === State-Driven Properties ===
|
|
if state == EyeState.IDLE:
|
|
t = (np.sin(2 * np.pi * elapsed / pulse_duration) + 1) / 2
|
|
iris_color = (
|
|
int((1 - t) * 192 + t * 0),
|
|
int((1 - t) * 192 + t * 255),
|
|
int((1 - t) * 192 + t * 255)
|
|
)
|
|
pulse = np.sin(current_time * 0.5) * pulse_range
|
|
elif state == EyeState.LISTENING:
|
|
iris_color = (160, 255, 255)
|
|
pulse = np.sin(current_time * 4) * (pulse_range * 0.5)
|
|
elif state == EyeState.RESPONDING:
|
|
iris_color = (100, 220, 255)
|
|
pulse = np.sin(current_time * 5) * (pulse_range * 1.5)
|
|
elif state == EyeState.PLEASURE:
|
|
iris_color = (180, 180, 255)
|
|
pulse = np.sin(current_time * 2) * (pulse_range * 0.5)
|
|
else:
|
|
iris_color = (192, 192, 192)
|
|
pulse = 0
|
|
|
|
iris_radius = base_iris_radius + pulse
|
|
center_offset = (CENTER[0] + current_offset[0], CENTER[1] + current_offset[1])
|
|
|
|
img = Image.new("RGB", (WIDTH, HEIGHT), (10, 0, 20))
|
|
draw = ImageDraw.Draw(img)
|
|
|
|
for r in range(int(fixed_outer_radius), int(iris_radius), -2):
|
|
alpha = int(255 * (1 - (r - iris_radius) / (fixed_outer_radius - iris_radius)))
|
|
color = tuple((c * alpha // 255) for c in iris_color)
|
|
draw.ellipse(
|
|
[center_offset[0] - r, center_offset[1] - r, center_offset[0] + r, center_offset[1] + r],
|
|
fill=color
|
|
)
|
|
|
|
draw.ellipse(
|
|
[center_offset[0] - iris_radius, center_offset[1] - iris_radius,
|
|
center_offset[0] + iris_radius, center_offset[1] + iris_radius],
|
|
fill=iris_color
|
|
)
|
|
|
|
# Draw segmented outer ring as small arcs (platinum, fixed size)
|
|
platinum = (192, 192, 192)
|
|
arc_extent = 360 / segment_count * 0.6
|
|
for i in range(segment_count):
|
|
start_angle = np.degrees(angle_offset + 2 * np.pi * i / segment_count)
|
|
bbox = [
|
|
center_offset[0] - fixed_outer_radius,
|
|
center_offset[1] - fixed_outer_radius,
|
|
center_offset[0] + fixed_outer_radius,
|
|
center_offset[1] + fixed_outer_radius
|
|
]
|
|
draw.arc(bbox, start=start_angle, end=start_angle + arc_extent, fill=platinum, width=segment_width)
|
|
|
|
angle_offset += rotation_speed
|
|
|
|
disp1.ShowImage(img)
|
|
disp2.ShowImage(img.transpose(Image.ROTATE_180))
|
|
time.sleep(sleep_time)
|
|
|
|
except KeyboardInterrupt:
|
|
disp1.module_exit()
|
|
disp2.module_exit()
|
|
lgpio.gpiochip_close(h)
|
|
print("Animation stopped.")
|