#!/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.")