mirror of
https://github.com/apirrone/Open_Duck_Mini_Runtime.git
synced 2025-09-02 19:23:54 +00:00
control refactor + sounds attributing to color (todo, map)
This commit is contained in:
parent
ebf88f92d7
commit
cef6c0a2ea
12 changed files with 176 additions and 20 deletions
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue