""" 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 (0–255) 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)