control refactor + sounds attributing to color (todo, map)

This commit is contained in:
Aronnaxx 2025-08-16 14:17:57 -07:00
parent ebf88f92d7
commit cef6c0a2ea
12 changed files with 176 additions and 20 deletions

View file

@ -9,7 +9,7 @@ Design:
from __future__ import annotations
from threading import Lock
from typing import Tuple, Optional
from typing import Tuple, Optional, Union, Dict
# Direct hardware imports (we assume we're running on-device)
import board
@ -38,42 +38,85 @@ class LedController:
# Use the module-level NeoPixel configured above
self._pixels = pixels
# Cache simple color tuples
self.OFF = (0, 0, 0)
self.WHITE = (255, 255, 255)
# Cache simple color tuples (use 4-tuple for RGBW strips)
# Colors are stored in logical (R, G, B, W) regardless of ORDER.
self.OFF = (0, 0, 0, 0)
self.WHITE = (0, 0, 0, 255)
self.RED = (255, 0, 0, 0)
self.GREEN = (0, 255, 0, 0)
self.BLUE = (0, 0, 255, 0)
self._named_colors = {
"off": self.OFF,
"white": self.WHITE,
"red": self.RED,
"green": self.GREEN,
"blue": self.BLUE,
}
# Track on/off states (for toggling and consistent show)
self.eye_left_on = True
self.eye_right_on = True
self.projector_on = False
# Track current colors for each pixel (default WHITE)
self.left_color = self.WHITE
self.right_color = self.WHITE
self.proj_color = self.WHITE
# Ensure an initial known state
self._apply()
# Internal helper to apply current states to pixels
def _apply(
self,
left_color: Optional[Tuple[int, int, int]] = None,
right_color: Optional[Tuple[int, int, int]] = None,
proj_color: Optional[Tuple[int, int, int]] = None,
left_color: Optional[Tuple[int, int, int, int]] = None,
right_color: Optional[Tuple[int, int, int, int]] = None,
proj_color: Optional[Tuple[int, int, int, int]] = None,
) -> None:
if self._pixels is None or self._deinited:
return
with self._lock:
# Determine colors from boolean states when not explicitly specified
if left_color is None:
left_color = self.WHITE if self.eye_left_on else self.OFF
left_color = self.left_color if self.eye_left_on else self.OFF
if right_color is None:
right_color = self.WHITE if self.eye_right_on else self.OFF
right_color = self.right_color if self.eye_right_on else self.OFF
if proj_color is None:
proj_color = self.WHITE if self.projector_on else self.OFF
proj_color = self.proj_color if self.projector_on else self.OFF
# Assign indices: 2-left, 1-right, 0-projector
self._pixels[0] = proj_color
self._pixels[1] = right_color
self._pixels[2] = left_color
self._pixels[0] = self._to_order(proj_color)
self._pixels[1] = self._to_order(right_color)
self._pixels[2] = self._to_order(left_color)
self._pixels.show()
def _to_order(self, color_rgba: Tuple[int, int, int, int]):
"""
Convert logical (R,G,B,W) into the configured NeoPixel ORDER tuple.
For RGB strips (no W), we will drop the W channel.
"""
r, g, b, w = color_rgba
try:
# neopixel library generally accepts (r,g,b) or (r,g,b,w) depending on pixel type
# We map to the declared ORDER for clarity when direct indexing is used.
if ORDER in (neopixel.RGB, neopixel.GRB):
mapping = {
neopixel.RGB: (r, g, b),
neopixel.GRB: (g, r, b),
}
return mapping[ORDER]
else:
# RGBW variants
mapping = {
neopixel.RGBW: (r, g, b, w),
neopixel.GRBW: (g, r, b, w),
}
return mapping.get(ORDER, (r, g, b, w))
except Exception:
# Fallback: return RGBA or RGB as-is
return (r, g, b, w)
# Eyes API
def set_eyes(self, on: bool) -> None:
self.eye_left_on = on
@ -93,6 +136,47 @@ class LedController:
self.projector_on = on
self._apply()
# Color API (accepts name or tuple)
def _norm_color(self, color: Union[str, Tuple[int, int, int], Tuple[int, int, int, int]]
) -> Tuple[int, int, int, int]:
if isinstance(color, str):
c = self._named_colors.get(color.lower())
if c is None:
raise ValueError(f"Unknown color name: {color}")
return c
# If 3-tuple provided, assume RGB on RGBW strip -> map to (r,g,b,0)
if isinstance(color, tuple) and len(color) == 3:
r, g, b = color
return (r, g, b, 0)
if isinstance(color, tuple) and len(color) == 4:
return color # already RGBA(W)
raise ValueError("Color must be a name or RGB/RGBW tuple")
def set_left_eye_color(self, color: Union[str, Tuple[int, int, int], Tuple[int, int, int, int]]):
self.left_color = self._norm_color(color)
self._apply()
def set_right_eye_color(self, color: Union[str, Tuple[int, int, int], Tuple[int, int, int, int]]):
self.right_color = self._norm_color(color)
self._apply()
def set_projector_color(self, color: Union[str, Tuple[int, int, int], Tuple[int, int, int, int]]):
self.proj_color = self._norm_color(color)
self._apply()
def set_eyes_color(self, color: Union[str, Tuple[int, int, int], Tuple[int, int, int, int]]):
norm = self._norm_color(color)
self.left_color = norm
self.right_color = norm
self._apply()
def set_all_color(self, color: Union[str, Tuple[int, int, int], Tuple[int, int, int, int]]):
norm = self._norm_color(color)
self.left_color = norm
self.right_color = norm
self.proj_color = norm
self._apply()
# Utilities
def all_off(self) -> None:
self.eye_left_on = False

View file

@ -2,14 +2,22 @@ import pygame
import time
import os
import random
from threading import Thread, Lock
from open_duck_mini_runtime.led_controller import get_controller
class Sounds:
def __init__(self, volume=1.0, sound_directory="./"):
def __init__(self, volume=1.0, sound_directory="", default_color: str = "white", sound_color_map: dict | None = None):
pygame.mixer.init()
pygame.mixer.music.set_volume(volume)
self.sounds = {}
self.ok = True
self._ctrl = get_controller()
self._default_color = default_color
self._sound_color_map = sound_color_map or {}
self._color_token = 0
self._lock = Lock()
try:
for file in os.listdir(sound_directory):
if file.endswith(".wav"):
@ -26,13 +34,64 @@ class Sounds:
print("No sound files found in the directory.")
self.ok = False
# Initialize color map defaults (cycle through a limited palette)
if self.ok and not self._sound_color_map:
palette = ["red", "green", "blue", "white"]
for idx, name in enumerate(sorted(self.sounds.keys())):
self._sound_color_map[name] = palette[idx % len(palette)]
# Ensure LEDs are default color initially (without changing on/off state)
try:
self._ctrl.set_all_color(self._default_color)
except Exception:
pass
def play(self, sound_name):
if not self.ok:
print("Sounds not initialized properly.")
return
if sound_name in self.sounds:
self.sounds[sound_name].play()
chan = self.sounds[sound_name].play()
print(f"Playing: {sound_name}")
# Change LEDs to mapped color (supports string for all or dict per pixel)
mapping_val = self._sound_color_map.get(sound_name, self._default_color)
try:
if isinstance(mapping_val, dict):
left = mapping_val.get("left", self._default_color)
right = mapping_val.get("right", self._default_color)
proj = mapping_val.get("projector", self._default_color)
self._ctrl.set_left_eye_color(left)
self._ctrl.set_right_eye_color(right)
self._ctrl.set_projector_color(proj)
else:
self._ctrl.set_all_color(mapping_val)
except Exception as e:
print(f"LED color set failed: {e}")
# Schedule reset to default when the sound finishes
with self._lock:
self._color_token += 1
token = self._color_token
def _reset_when_done(channel, expected_token):
try:
# If no channel returned, fallback to sleep by length
if channel is None:
duration = self.sounds[sound_name].get_length()
time.sleep(duration)
else:
# Wait until the channel is no longer busy
while channel.get_busy():
time.sleep(0.02)
# Only reset if no newer sound changed the color
with self._lock:
if expected_token == self._color_token:
self._ctrl.set_all_color(self._default_color)
except Exception:
pass
Thread(target=_reset_when_done, args=(chan, token), daemon=True).start()
else:
print(f"Sound '{sound_name}' not found!")
@ -41,10 +100,24 @@ class Sounds:
print("Sounds not initialized properly.")
return
sound_name = random.choice(list(self.sounds.keys()))
self.sounds[sound_name].play()
self.play(sound_name)
def play_happy(self):
self.sounds["happy1.wav"].play()
self.play("happy1.wav")
# API helpers
def set_default_color(self, color: str):
self._default_color = color
try:
self._ctrl.set_all_color(self._default_color)
except Exception:
pass
def set_sound_color(self, sound_name: str, color: str):
self._sound_color_map[sound_name] = color
def set_sound_color_map(self, mapping: dict):
self._sound_color_map.update(mapping)
@ -53,6 +126,5 @@ if __name__ == "__main__":
sound_player = Sounds(1.0, "../assets/")
time.sleep(1)
while True:
# sound_player.play_random_sound()
sound_player.play_happy()
time.sleep(3)
sound_player.play_random_sound()
time.sleep(5)