Files
quibot/raspi/eyes.py
2026-06-18 13:45:32 +02:00

274 lines
7.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
eyes.py — Control de les matrius LED 8x8 RGB WS2811 (ulls del robot).
Equivalent a eyes.cpp del codi Arduino/ESP32.
FastLED → pigpio waveforms (GPIO26, qualsevol pin).
REQUISIT: iniciar el dimoni pigpio amb resolució d'1µs:
sudo pigpiod -s 1
Si s'inicia sense -s 1 (defecte 5µs), els LEDs no funcionaran correctament.
Si en el futur es fa una modificació hardware (GPIO26 → GPIO18 o GPIO21),
es pot substituir _send_ws2811() per rpi_ws281x sense canviar cap altra funció.
"""
import time
import threading
import pigpio
from pins import LED_DATA
# ==================
# Constants
# ==================
ROW_NUM = 8
COL_NUM = 8
NUM_LEDS = ROW_NUM * COL_NUM * 2 # 128 LEDs (2 matrius 8x8)
MAX_BR = 170 # Brillantor màxima del parpelleig (0255)
MIN_BR = 80 # Brillantor mínima del parpelleig
# Colors predefinits (R, G, B)
WHITE = (255, 255, 255)
RED = (255, 0, 0)
GREEN = ( 0, 255, 0)
BLUE = ( 0, 0, 255)
YELLOW = (255, 200, 0)
ORANGE = (255, 80, 0)
PURPLE = (180, 0, 255)
CYAN = ( 0, 255, 255) # Color del mode gestos
BLACK = ( 0, 0, 0)
# ==================
# Formes dels ulls (índexs dels LEDs actius)
# ==================
class EyeShape:
"""Conjunt de LEDs que formen una expressió dels ulls."""
def __init__(self, leds: list):
self.leds = leds
self.len = len(leds)
EYES_OPEN = EyeShape([
102, 89, 38, 25, 106, 101, 90, 85, 42, 37, 26, 21,
107, 100, 91, 84, 43, 36, 27, 20, 108, 99, 92, 83,
44, 35, 28, 19, 109, 98, 93, 82, 45, 34, 29, 18,
97, 94, 33, 30
])
EYES_FW = EyeShape([
103, 88, 39, 24, 105, 102, 89, 86, 41, 38, 25, 22,
117, 106, 101, 90, 85, 74, 53, 42, 37, 26, 21, 10,
123, 116, 100, 91, 75, 68, 59, 52, 36, 27, 11, 4,
99, 92, 35, 28, 98, 93, 34, 29, 97, 94, 33, 30, 96,
95, 32, 31
])
EYES_DOWN = EyeShape([97, 94, 33, 30])
# Nova forma per al mode gestos: marc extern dels dos ulls (expressió "atenta")
EYES_GESTURE = EyeShape([
96, 97, 98, 99, 100, 101, 102, 103,
104, 111, 112, 119, 120, 127,
31, 32, 33, 34, 35, 36, 37, 38, 39,
0, 7, 8, 15, 16, 23
])
# ==================
# Estat global
# ==================
_pi: pigpio.pi = None
_leds = [[0, 0, 0] for _ in range(NUM_LEDS)]
_brightness = MAX_BR
_leds_lock = threading.Lock()
_update_stop = threading.Event()
_update_thread: threading.Thread = None
# Màscara GPIO per a les waveforms de pigpio
_GPIO_MASK: int = 0
# ==================
# WS2811 via pigpio waveforms
# ==================
def _send_ws2811(data: bytes):
"""
Envia dades RGB als LEDs WS2811 via pigpio waveforms.
Ordre de color: GRB (igual que FastLED amb WS2811, GRB).
Timing a 1µs de resolució (requereix sudo pigpiod -s 1):
- Bit 0: 1µs HIGH + 2µs LOW (spec: 0.5µs + 2.0µs)
- Bit 1: 2µs HIGH + 1µs LOW (spec: 1.2µs + 1.3µs)
- Reset: 80µs LOW
"""
pulses = []
for byte_val in data:
for bit in range(7, -1, -1):
if byte_val & (1 << bit):
pulses.append(pigpio.pulse(_GPIO_MASK, 0, 2))
pulses.append(pigpio.pulse(0, _GPIO_MASK, 1))
else:
pulses.append(pigpio.pulse(_GPIO_MASK, 0, 1))
pulses.append(pigpio.pulse(0, _GPIO_MASK, 2))
pulses.append(pigpio.pulse(0, _GPIO_MASK, 80)) # reset
_pi.wave_add_new()
_pi.wave_add_generic(pulses)
wid = _pi.wave_create()
if wid >= 0:
_pi.wave_send_once(wid)
while _pi.wave_tx_busy():
pass
_pi.wave_delete(wid)
def _eyes_show(brightness: int):
"""Renderitza l'estat actual de _leds amb la brillantor indicada."""
data = bytearray(NUM_LEDS * 3)
scale = brightness / 255
for i, (r, g, b) in enumerate(_leds):
data[i * 3 + 0] = int(g * scale) # WS2811 GRB: primer G
data[i * 3 + 1] = int(r * scale)
data[i * 3 + 2] = int(b * scale)
_send_ws2811(bytes(data))
# ==================
# Setup i cleanup
# ==================
def eyes_setup(pi: pigpio.pi):
"""Inicialitza el GPIO i arrenca el thread de parpelleig."""
global _pi, _GPIO_MASK, _update_thread
_pi = pi
_GPIO_MASK = 1 << LED_DATA
pi.set_mode(LED_DATA, pigpio.OUTPUT)
pi.write(LED_DATA, 0)
_update_stop.clear()
_update_thread = threading.Thread(
target=_task_update_leds, daemon=True, name="eyes"
)
_update_thread.start()
def eyes_cleanup():
"""Atura el thread de parpelleig i apaga els LEDs."""
_update_stop.set()
if _update_thread:
_update_thread.join(timeout=1.0)
with _leds_lock:
for i in range(NUM_LEDS):
_leds[i] = [0, 0, 0]
_eyes_show(255)
# ==================
# Thread de parpelleig (equivalent a task_update_leds del FreeRTOS)
# ==================
def _task_update_leds():
"""
Bucle continu que fa respirar la brillantor dels ulls.
Equivalent a task_update_leds() del FreeRTOS.
"""
global _brightness
going_up = False # Al C++ comença a MAX_BR i baixa
_brightness = MAX_BR
while not _update_stop.is_set():
if going_up:
if _brightness < MAX_BR:
_brightness += 2
else:
going_up = False
else:
if _brightness > MIN_BR:
_brightness -= 2
else:
going_up = True
with _leds_lock:
_eyes_show(_brightness)
time.sleep(0.05)
# ==================
# Animacions (equivalent a les funcions de eyes.cpp)
# ==================
def eyes_turn_off():
"""Apaga tots els LEDs amb un fos progressiu."""
for _ in range(50):
with _leds_lock:
for i in range(NUM_LEDS):
r, g, b = _leds[i]
_leds[i] = [int(r * 245 / 255),
int(g * 245 / 255),
int(b * 245 / 255)]
time.sleep(0.01)
with _leds_lock:
for i in range(NUM_LEDS):
_leds[i] = [0, 0, 0]
def eyes_turn_on(shape: EyeShape, color: tuple,
repeat: int = 1, forward: bool = True):
"""
Encén els LEDs d'una forma un per un, amb animació.
shape: forma a dibuixar (EYES_OPEN, EYES_FW, EYES_DOWN…)
color: color RGB com a tupla (r, g, b)
repeat: nombre de vegades que es repeteix l'animació
forward: True = ordre normal, False = ordre invers
"""
r, g, b = color
for rep in range(repeat):
eyes_turn_off()
for i in range(shape.len):
idx = shape.leds[i if forward else (shape.len - 1 - i)]
with _leds_lock:
_leds[idx] = [r, g, b]
time.sleep(0.008)
if rep < repeat - 1:
for i in range(shape.len):
idx = shape.leds[i if forward else (shape.len - 1 - i)]
with _leds_lock:
_leds[idx] = [0, 0, 0]
time.sleep(0.008)
# ==================
# Animacions noves — mode gestos (TFG)
# ==================
def eyes_gesture_mode_on():
"""
Animació d'activació del mode gestos.
Parpelleig doble en cian per indicar que el robot escolta gestos.
"""
eyes_turn_on(EYES_OPEN, CYAN, repeat=2)
def eyes_gesture_mode_off():
"""
Animació de desactivació del mode gestos.
Torna als ulls oberts en blanc.
"""
eyes_turn_off()
eyes_turn_on(EYES_OPEN, WHITE)
def eyes_listening():
"""
Expressió "escoltant": marc extern dels ulls en cian.
Es mostra mentre el robot espera un gest.
"""
eyes_turn_on(EYES_GESTURE, CYAN)