Initial commit - original eyes2.py from head-lyra
This commit is contained in:
189
eyes2.py
Normal file
189
eyes2.py
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
#!/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.")
|
||||||
197
tuning.py
Executable file
197
tuning.py
Executable file
@@ -0,0 +1,197 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import struct
|
||||||
|
import usb.core
|
||||||
|
import usb.util
|
||||||
|
|
||||||
|
USAGE = """Usage: python {} -h
|
||||||
|
-p show all parameters
|
||||||
|
-r read all parameters
|
||||||
|
NAME get the parameter with the NAME
|
||||||
|
NAME VALUE set the parameter with the NAME and the VALUE
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# parameter list
|
||||||
|
# name: (id, offset, type, max, min , r/w, info)
|
||||||
|
PARAMETERS = {
|
||||||
|
'AECFREEZEONOFF': (18, 7, 'int', 1, 0, 'rw', 'Adaptive Echo Canceler updates inhibit.', '0 = Adaptation enabled', '1 = Freeze adaptation, filter only'),
|
||||||
|
'AECNORM': (18, 19, 'float', 16, 0.25, 'rw', 'Limit on norm of AEC filter coefficients'),
|
||||||
|
'AECPATHCHANGE': (18, 25, 'int', 1, 0, 'ro', 'AEC Path Change Detection.', '0 = false (no path change detected)', '1 = true (path change detected)'),
|
||||||
|
'RT60': (18, 26, 'float', 0.9, 0.25, 'ro', 'Current RT60 estimate in seconds'),
|
||||||
|
'HPFONOFF': (18, 27, 'int', 3, 0, 'rw', 'High-pass Filter on microphone signals.', '0 = OFF', '1 = ON - 70 Hz cut-off', '2 = ON - 125 Hz cut-off', '3 = ON - 180 Hz cut-off'),
|
||||||
|
'RT60ONOFF': (18, 28, 'int', 1, 0, 'rw', 'RT60 Estimation for AES. 0 = OFF 1 = ON'),
|
||||||
|
'AECSILENCELEVEL': (18, 30, 'float', 1, 1e-09, 'rw', 'Threshold for signal detection in AEC [-inf .. 0] dBov (Default: -80dBov = 10log10(1x10-8))'),
|
||||||
|
'AECSILENCEMODE': (18, 31, 'int', 1, 0, 'ro', 'AEC far-end silence detection status. ', '0 = false (signal detected) ', '1 = true (silence detected)'),
|
||||||
|
'AGCONOFF': (19, 0, 'int', 1, 0, 'rw', 'Automatic Gain Control. ', '0 = OFF ', '1 = ON'),
|
||||||
|
'AGCMAXGAIN': (19, 1, 'float', 1000, 1, 'rw', 'Maximum AGC gain factor. ', '[0 .. 60] dB (default 30dB = 20log10(31.6))'),
|
||||||
|
'AGCDESIREDLEVEL': (19, 2, 'float', 0.99, 1e-08, 'rw', 'Target power level of the output signal. ', '[-inf .. 0] dBov (default: -23dBov = 10log10(0.005))'),
|
||||||
|
'AGCGAIN': (19, 3, 'float', 1000, 1, 'rw', 'Current AGC gain factor. ', '[0 .. 60] dB (default: 0.0dB = 20log10(1.0))'),
|
||||||
|
'AGCTIME': (19, 4, 'float', 1, 0.1, 'rw', 'Ramps-up / down time-constant in seconds.'),
|
||||||
|
'CNIONOFF': (19, 5, 'int', 1, 0, 'rw', 'Comfort Noise Insertion.', '0 = OFF', '1 = ON'),
|
||||||
|
'FREEZEONOFF': (19, 6, 'int', 1, 0, 'rw', 'Adaptive beamformer updates.', '0 = Adaptation enabled', '1 = Freeze adaptation, filter only'),
|
||||||
|
'STATNOISEONOFF': (19, 8, 'int', 1, 0, 'rw', 'Stationary noise suppression.', '0 = OFF', '1 = ON'),
|
||||||
|
'GAMMA_NS': (19, 9, 'float', 3, 0, 'rw', 'Over-subtraction factor of stationary noise. min .. max attenuation'),
|
||||||
|
'MIN_NS': (19, 10, 'float', 1, 0, 'rw', 'Gain-floor for stationary noise suppression.', '[-inf .. 0] dB (default: -16dB = 20log10(0.15))'),
|
||||||
|
'NONSTATNOISEONOFF': (19, 11, 'int', 1, 0, 'rw', 'Non-stationary noise suppression.', '0 = OFF', '1 = ON'),
|
||||||
|
'GAMMA_NN': (19, 12, 'float', 3, 0, 'rw', 'Over-subtraction factor of non- stationary noise. min .. max attenuation'),
|
||||||
|
'MIN_NN': (19, 13, 'float', 1, 0, 'rw', 'Gain-floor for non-stationary noise suppression.', '[-inf .. 0] dB (default: -10dB = 20log10(0.3))'),
|
||||||
|
'ECHOONOFF': (19, 14, 'int', 1, 0, 'rw', 'Echo suppression.', '0 = OFF', '1 = ON'),
|
||||||
|
'GAMMA_E': (19, 15, 'float', 3, 0, 'rw', 'Over-subtraction factor of echo (direct and early components). min .. max attenuation'),
|
||||||
|
'GAMMA_ETAIL': (19, 16, 'float', 3, 0, 'rw', 'Over-subtraction factor of echo (tail components). min .. max attenuation'),
|
||||||
|
'GAMMA_ENL': (19, 17, 'float', 5, 0, 'rw', 'Over-subtraction factor of non-linear echo. min .. max attenuation'),
|
||||||
|
'NLATTENONOFF': (19, 18, 'int', 1, 0, 'rw', 'Non-Linear echo attenuation.', '0 = OFF', '1 = ON'),
|
||||||
|
'NLAEC_MODE': (19, 20, 'int', 2, 0, 'rw', 'Non-Linear AEC training mode.', '0 = OFF', '1 = ON - phase 1', '2 = ON - phase 2'),
|
||||||
|
'SPEECHDETECTED': (19, 22, 'int', 1, 0, 'ro', 'Speech detection status.', '0 = false (no speech detected)', '1 = true (speech detected)'),
|
||||||
|
'FSBUPDATED': (19, 23, 'int', 1, 0, 'ro', 'FSB Update Decision.', '0 = false (FSB was not updated)', '1 = true (FSB was updated)'),
|
||||||
|
'FSBPATHCHANGE': (19, 24, 'int', 1, 0, 'ro', 'FSB Path Change Detection.', '0 = false (no path change detected)', '1 = true (path change detected)'),
|
||||||
|
'TRANSIENTONOFF': (19, 29, 'int', 1, 0, 'rw', 'Transient echo suppression.', '0 = OFF', '1 = ON'),
|
||||||
|
'VOICEACTIVITY': (19, 32, 'int', 1, 0, 'ro', 'VAD voice activity status.', '0 = false (no voice activity)', '1 = true (voice activity)'),
|
||||||
|
'STATNOISEONOFF_SR': (19, 33, 'int', 1, 0, 'rw', 'Stationary noise suppression for ASR.', '0 = OFF', '1 = ON'),
|
||||||
|
'NONSTATNOISEONOFF_SR': (19, 34, 'int', 1, 0, 'rw', 'Non-stationary noise suppression for ASR.', '0 = OFF', '1 = ON'),
|
||||||
|
'GAMMA_NS_SR': (19, 35, 'float', 3, 0, 'rw', 'Over-subtraction factor of stationary noise for ASR. ', '[0.0 .. 3.0] (default: 1.0)'),
|
||||||
|
'GAMMA_NN_SR': (19, 36, 'float', 3, 0, 'rw', 'Over-subtraction factor of non-stationary noise for ASR. ', '[0.0 .. 3.0] (default: 1.1)'),
|
||||||
|
'MIN_NS_SR': (19, 37, 'float', 1, 0, 'rw', 'Gain-floor for stationary noise suppression for ASR.', '[-inf .. 0] dB (default: -16dB = 20log10(0.15))'),
|
||||||
|
'MIN_NN_SR': (19, 38, 'float', 1, 0, 'rw', 'Gain-floor for non-stationary noise suppression for ASR.', '[-inf .. 0] dB (default: -10dB = 20log10(0.3))'),
|
||||||
|
'GAMMAVAD_SR': (19, 39, 'float', 1000, 0, 'rw', 'Set the threshold for voice activity detection.', '[-inf .. 60] dB (default: 3.5dB 20log10(1.5))'),
|
||||||
|
# 'KEYWORDDETECT': (20, 0, 'int', 1, 0, 'ro', 'Keyword detected. Current value so needs polling.'),
|
||||||
|
'DOAANGLE': (21, 0, 'int', 359, 0, 'ro', 'DOA angle. Current value. Orientation depends on build configuration.')
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Tuning:
|
||||||
|
TIMEOUT = 100000
|
||||||
|
|
||||||
|
def __init__(self, dev):
|
||||||
|
self.dev = dev
|
||||||
|
|
||||||
|
def write(self, name, value):
|
||||||
|
try:
|
||||||
|
data = PARAMETERS[name]
|
||||||
|
except KeyError:
|
||||||
|
return
|
||||||
|
|
||||||
|
if data[5] == 'ro':
|
||||||
|
raise ValueError('{} is read-only'.format(name))
|
||||||
|
|
||||||
|
id = data[0]
|
||||||
|
|
||||||
|
# 4 bytes offset, 4 bytes value, 4 bytes type
|
||||||
|
if data[2] == 'int':
|
||||||
|
payload = struct.pack(b'iii', data[1], int(value), 1)
|
||||||
|
else:
|
||||||
|
payload = struct.pack(b'ifi', data[1], float(value), 0)
|
||||||
|
|
||||||
|
self.dev.ctrl_transfer(
|
||||||
|
usb.util.CTRL_OUT | usb.util.CTRL_TYPE_VENDOR | usb.util.CTRL_RECIPIENT_DEVICE,
|
||||||
|
0, 0, id, payload, self.TIMEOUT)
|
||||||
|
|
||||||
|
def read(self, name):
|
||||||
|
try:
|
||||||
|
data = PARAMETERS[name]
|
||||||
|
except KeyError:
|
||||||
|
return
|
||||||
|
|
||||||
|
id = data[0]
|
||||||
|
|
||||||
|
cmd = 0x80 | data[1]
|
||||||
|
if data[2] == 'int':
|
||||||
|
cmd |= 0x40
|
||||||
|
|
||||||
|
length = 8
|
||||||
|
|
||||||
|
response = self.dev.ctrl_transfer(
|
||||||
|
usb.util.CTRL_IN | usb.util.CTRL_TYPE_VENDOR | usb.util.CTRL_RECIPIENT_DEVICE,
|
||||||
|
0, cmd, id, length, self.TIMEOUT)
|
||||||
|
|
||||||
|
response = struct.unpack(b'ii', response.tobytes())
|
||||||
|
|
||||||
|
if data[2] == 'int':
|
||||||
|
result = response[0]
|
||||||
|
else:
|
||||||
|
result = response[0] * (2.**response[1])
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def set_vad_threshold(self, db):
|
||||||
|
self.write('GAMMAVAD_SR', db)
|
||||||
|
|
||||||
|
def is_voice(self):
|
||||||
|
return self.read('VOICEACTIVITY')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def direction(self):
|
||||||
|
return self.read('DOAANGLE')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def version(self):
|
||||||
|
return self.dev.ctrl_transfer(
|
||||||
|
usb.util.CTRL_IN | usb.util.CTRL_TYPE_VENDOR | usb.util.CTRL_RECIPIENT_DEVICE,
|
||||||
|
0, 0x80, 0, 1, self.TIMEOUT)[0]
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""
|
||||||
|
close the interface
|
||||||
|
"""
|
||||||
|
usb.util.dispose_resources(self.dev)
|
||||||
|
|
||||||
|
|
||||||
|
def find(vid=0x2886, pid=0x0018):
|
||||||
|
dev = usb.core.find(idVendor=vid, idProduct=pid)
|
||||||
|
if not dev:
|
||||||
|
return
|
||||||
|
|
||||||
|
# configuration = dev.get_active_configuration()
|
||||||
|
|
||||||
|
# interface_number = None
|
||||||
|
# for interface in configuration:
|
||||||
|
# interface_number = interface.bInterfaceNumber
|
||||||
|
|
||||||
|
# if dev.is_kernel_driver_active(interface_number):
|
||||||
|
# dev.detach_kernel_driver(interface_number)
|
||||||
|
|
||||||
|
return Tuning(dev)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
if sys.argv[1] == '-p':
|
||||||
|
print('name\t\t\ttype\tmax\tmin\tr/w\tinfo')
|
||||||
|
print('-------------------------------')
|
||||||
|
for name in sorted(PARAMETERS.keys()):
|
||||||
|
data = PARAMETERS[name]
|
||||||
|
print('{:16}\t{}'.format(name, '\t'.join([str(i) for i in data[2:7]])))
|
||||||
|
for extra in data[7:]:
|
||||||
|
print('{}{}'.format(' '*60, extra))
|
||||||
|
else:
|
||||||
|
dev = find()
|
||||||
|
if not dev:
|
||||||
|
print('No device found')
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# print('version: {}'.format(dev.version))
|
||||||
|
|
||||||
|
if sys.argv[1] == '-r':
|
||||||
|
print('{:24} {}'.format('name', 'value'))
|
||||||
|
print('-------------------------------')
|
||||||
|
for name in sorted(PARAMETERS.keys()):
|
||||||
|
print('{:24} {}'.format(name, dev.read(name)))
|
||||||
|
else:
|
||||||
|
name = sys.argv[1].upper()
|
||||||
|
if name in PARAMETERS:
|
||||||
|
if len(sys.argv) > 2:
|
||||||
|
dev.write(name, sys.argv[2])
|
||||||
|
|
||||||
|
print('{}: {}'.format(name, dev.read(name)))
|
||||||
|
else:
|
||||||
|
print('{} is not a valid name'.format(name))
|
||||||
|
|
||||||
|
dev.close()
|
||||||
|
else:
|
||||||
|
print(USAGE.format(sys.argv[0]))
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user