Implemented all commands:
- Play sound with pygame (sounddevice is too flaky) - Play sound during user interaction (interruptable) - Blink LEDs during user interaction - Flash LEDs during POST
This commit is contained in:
parent
77eb772776
commit
2c85f7ed46
1 changed files with 93 additions and 58 deletions
|
@ -1,5 +1,6 @@
|
||||||
import logging
|
import logging
|
||||||
import functools
|
import functools
|
||||||
|
import threading
|
||||||
from time import sleep
|
from time import sleep
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
|
@ -9,6 +10,8 @@ from scipy.io.wavfile import write as writewav
|
||||||
import sounddevice as sd
|
import sounddevice as sd
|
||||||
import soundfile as sf
|
import soundfile as sf
|
||||||
|
|
||||||
|
import pygame.mixer as mx
|
||||||
|
|
||||||
from . import gpio_pins
|
from . import gpio_pins
|
||||||
|
|
||||||
from picamera import PiCamera
|
from picamera import PiCamera
|
||||||
|
@ -43,11 +46,6 @@ class SerialCommands(Enum):
|
||||||
|
|
||||||
USER_INTERACT = b'U'
|
USER_INTERACT = b'U'
|
||||||
|
|
||||||
RESP_BLUE = b'X'
|
|
||||||
RESP_RED = b'O'
|
|
||||||
RESP_YELLOW = b'Y'
|
|
||||||
RESP_GREEN = b'N'
|
|
||||||
|
|
||||||
RECORD = b'C'
|
RECORD = b'C'
|
||||||
REWIND = b'R'
|
REWIND = b'R'
|
||||||
|
|
||||||
|
@ -104,6 +102,38 @@ class PizzaHAL:
|
||||||
raise SerialCommunicationError(f'Serial Connection received invalid response to HELLO: {resp}')
|
raise SerialCommunicationError(f'Serial Connection received invalid response to HELLO: {resp}')
|
||||||
self.connected = True
|
self.connected = True
|
||||||
|
|
||||||
|
def init_sounds(self, sounds: List=None):
|
||||||
|
"""
|
||||||
|
Load prerecorded Sounds into memory
|
||||||
|
|
||||||
|
:param hal:
|
||||||
|
:param sounds: A list of sound files
|
||||||
|
"""
|
||||||
|
if self.soundcache is None:
|
||||||
|
self.soundcache = {}
|
||||||
|
|
||||||
|
if not mx.get_init():
|
||||||
|
mx.init()
|
||||||
|
|
||||||
|
if sounds is not None:
|
||||||
|
for sound in sounds:
|
||||||
|
# Extract data and sampling rate from file
|
||||||
|
# data, fs = sf.read(str(sound), dtype='float32')
|
||||||
|
# self.soundcache[str(sound)] = (data, fs)
|
||||||
|
self.soundcache[str(sound)] = mx.Sound(sound)
|
||||||
|
|
||||||
|
def init_camera(self):
|
||||||
|
if self.camera is None:
|
||||||
|
self.camera = PiCamera(sensor_mode=5)
|
||||||
|
|
||||||
|
def play_sound(self, sound: str):
|
||||||
|
s = self.soundcache.get(sound, mx.Sound(sound))
|
||||||
|
s.play()
|
||||||
|
|
||||||
|
def stop_sound(self):
|
||||||
|
if mx.get_busy():
|
||||||
|
mx.stop()
|
||||||
|
|
||||||
def send_cmd(self, command: SerialCommands, *options):
|
def send_cmd(self, command: SerialCommands, *options):
|
||||||
"""
|
"""
|
||||||
Send a command and optional options. Options need to be encoded as bytes before passing.
|
Send a command and optional options. Options need to be encoded as bytes before passing.
|
||||||
|
@ -115,7 +145,15 @@ class PizzaHAL:
|
||||||
self.serialcon.write(o)
|
self.serialcon.write(o)
|
||||||
self.serialcon.write(SerialCommands.EOT.value)
|
self.serialcon.write(SerialCommands.EOT.value)
|
||||||
resp = self.serialcon.read_until()
|
resp = self.serialcon.read_until()
|
||||||
# TODO handle errors in response
|
|
||||||
|
while resp == b'':
|
||||||
|
# If serial communication timeout occurs, response is empty.
|
||||||
|
# Read again to allow for longer waiting times
|
||||||
|
resp = self.serialcon.read_until()
|
||||||
|
|
||||||
|
if not resp.startswith(SerialCommands.RECEIVED.value):
|
||||||
|
raise SerialCommunicationError(f'Serial Communication received unexpected response: {resp}')
|
||||||
|
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
|
||||||
|
@ -152,44 +190,60 @@ def turn_off(hal: PizzaHAL):
|
||||||
|
|
||||||
|
|
||||||
def wait_for_input(hal: PizzaHAL,
|
def wait_for_input(hal: PizzaHAL,
|
||||||
blue_callback: Any = None,
|
blue_cb: Any = None,
|
||||||
red_callback: Any = None,
|
red_cb: Any = None,
|
||||||
yellow_callback: Any = None,
|
yellow_cb: Any = None,
|
||||||
green_callback: Any = None,
|
green_cb: Any = None,
|
||||||
timeout_callback: Any = None,
|
timeout_cb: Any = None,
|
||||||
|
sound: Any = None,
|
||||||
timeout=120, **kwargs):
|
timeout=120, **kwargs):
|
||||||
"""
|
"""
|
||||||
Blink leds on buttons. Wait until the user presses a button, then execute
|
Blink leds on buttons. Wait until the user presses a button, then execute
|
||||||
the appropriate callback
|
the appropriate callback. If a callback is not defined, the button is not
|
||||||
|
used.
|
||||||
|
Optionally plays sound which can be interrupted by user input.
|
||||||
|
|
||||||
:param hal: The hardware abstraction object
|
:param hal: The hardware abstraction object
|
||||||
:param go_callback: called when button 'go' is pressed
|
:param blue_cb: Callback for blue button press
|
||||||
:param back_callback: called whan button 'back' is pressed
|
:param red_cb: Callback for red button press
|
||||||
:param to_callback: called on timeout
|
:param yellow_cb: Callback for yellow button press
|
||||||
:param timeout: inactivity timeout in seconds (default 120)
|
:param green_cb: Callback for green button press
|
||||||
|
:param timeout_cb: Callback for no button press
|
||||||
|
:param sound: Name of sound file to play until user presses a button
|
||||||
|
:param timeout: Time to wait before abort. 0 to wait forever
|
||||||
"""
|
"""
|
||||||
timeout *= 1000
|
if timeout is not None:
|
||||||
|
timeout *= 1000
|
||||||
|
else:
|
||||||
|
timeout = 0
|
||||||
|
|
||||||
bitmask = (1 if blue_callback else 0) | \
|
bitmask = (1 if blue_cb else 0) | \
|
||||||
(2 if red_callback else 0) | \
|
(2 if red_cb else 0) | \
|
||||||
(4 if yellow_callback else 0) | \
|
(4 if yellow_cb else 0) | \
|
||||||
(8 if green_callback else 0)
|
(8 if green_cb else 0)
|
||||||
|
|
||||||
|
if sound is not None:
|
||||||
|
hal.play_sound(sound)
|
||||||
|
|
||||||
resp = hal.send_cmd(SerialCommands.USER_INTERACT, bitmask.to_bytes(1, 'little', signed=False), timeout.to_bytes(4, 'little', signed=False))
|
resp = hal.send_cmd(SerialCommands.USER_INTERACT, bitmask.to_bytes(1, 'little', signed=False), timeout.to_bytes(4, 'little', signed=False))
|
||||||
if (not resp.startswith(SerialCommands.RECEIVED.value)) or (len(resp) != 3):
|
|
||||||
raise SerialCommunicationError(f'USER_INTERACTION received {resp} (expected 3 bytes, starting with 0x03)')
|
if sound is not None:
|
||||||
|
hal.stop_sound()
|
||||||
|
|
||||||
|
if len(resp) != 3:
|
||||||
|
raise SerialCommunicationError(f'USER_INTERACTION expects 3 bytes, received {resp}')
|
||||||
|
|
||||||
resp = resp[1]
|
resp = resp[1]
|
||||||
if resp == 1:
|
if resp == 1:
|
||||||
blue_callback(**kwargs)
|
blue_cb(**kwargs)
|
||||||
elif resp == 2:
|
elif resp == 2:
|
||||||
red_callback(**kwargs)
|
red_cb(**kwargs)
|
||||||
elif resp == 4:
|
elif resp == 4:
|
||||||
yellow_callback(**kwargs)
|
yellow_cb(**kwargs)
|
||||||
elif resp == 8:
|
elif resp == 8:
|
||||||
green_callback(**kwargs)
|
green_cb(**kwargs)
|
||||||
elif timeout_callback is not None:
|
elif timeout_cb is not None:
|
||||||
timeout_callback(**kwargs)
|
timeout_cb(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
def light_layer(hal: PizzaHAL, r: float, g: float, b: float, w: float, fade: float = 0.0, **kwargs):
|
def light_layer(hal: PizzaHAL, r: float, g: float, b: float, w: float, fade: float = 0.0, **kwargs):
|
||||||
|
@ -232,25 +286,25 @@ def backlight(hal: PizzaHAL, r: float, g: float, b: float, w: float, fade: float
|
||||||
int(fade * 1000).to_bytes(4, 'little'))
|
int(fade * 1000).to_bytes(4, 'little'))
|
||||||
|
|
||||||
|
|
||||||
def play_sound(hal: PizzaHAL, sound: Any, interruptable: bool = False, **kwargs):
|
def play_sound(hal: PizzaHAL, sound: Any, **kwargs):
|
||||||
"""
|
"""
|
||||||
Play a sound.
|
Play a sound (blocking).
|
||||||
|
|
||||||
:param hal: The hardware abstraction object
|
:param hal: The hardware abstraction object
|
||||||
:param sound: The sound to be played
|
:param sound: The sound to be played
|
||||||
"""
|
"""
|
||||||
# Extract data and sampling rate from file
|
# Extract data and sampling rate from file
|
||||||
try:
|
try:
|
||||||
# TODO implement interruption
|
hal.play_sound(sound)
|
||||||
data, fs = hal.soundcache.get(str(sound), sf.read(str(sound), dtype='float32'))
|
while mx.get_busy():
|
||||||
sd.play(data, fs)
|
pass
|
||||||
sd.wait() # Wait until file is done playing
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
|
mx.stop()
|
||||||
logger.debug('skipped playback')
|
logger.debug('skipped playback')
|
||||||
# sd.stop()
|
|
||||||
|
|
||||||
|
|
||||||
def record_sound(hal: PizzaHAL, filename: Any, duration: float,
|
def record_sound(hal: PizzaHAL, filename: Any,
|
||||||
|
duration: float,
|
||||||
cache: bool = False, **kwargs):
|
cache: bool = False, **kwargs):
|
||||||
"""
|
"""
|
||||||
Record sound using the microphone
|
Record sound using the microphone
|
||||||
|
@ -264,11 +318,12 @@ def record_sound(hal: PizzaHAL, filename: Any, duration: float,
|
||||||
samplerate=AUDIO_REC_SR,
|
samplerate=AUDIO_REC_SR,
|
||||||
channels=2)
|
channels=2)
|
||||||
|
|
||||||
hal.send_cmd(SerialCommands.RECORD, int(duration).to_bytes(4, 'little', signed=False))
|
hal.send_cmd(SerialCommands.RECORD, int(duration*1000).to_bytes(4, 'little', signed=False))
|
||||||
|
|
||||||
sd.stop()
|
sd.stop()
|
||||||
|
|
||||||
writewav(str(filename), AUDIO_REC_SR, myrecording)
|
writewav(str(filename), AUDIO_REC_SR, myrecording)
|
||||||
|
|
||||||
if cache:
|
if cache:
|
||||||
hal.soundcache[str(filename)] = (myrecording, AUDIO_REC_SR)
|
hal.soundcache[str(filename)] = (myrecording, AUDIO_REC_SR)
|
||||||
|
|
||||||
|
@ -297,23 +352,3 @@ def take_photo(hal: PizzaHAL, filename: Any, **kwargs):
|
||||||
hal.camera.resolution = PHOTO_RES
|
hal.camera.resolution = PHOTO_RES
|
||||||
hal.camera.capture(str(filename))
|
hal.camera.capture(str(filename))
|
||||||
|
|
||||||
|
|
||||||
def init_sounds(hal: PizzaHAL, sounds: List):
|
|
||||||
"""
|
|
||||||
Load prerecorded Sounds into memory
|
|
||||||
|
|
||||||
:param hal:
|
|
||||||
:param sounds: A list of sound files
|
|
||||||
"""
|
|
||||||
if hal.soundcache is None:
|
|
||||||
hal.soundcache = {}
|
|
||||||
|
|
||||||
for sound in sounds:
|
|
||||||
# Extract data and sampling rate from file
|
|
||||||
data, fs = sf.read(str(sound), dtype='float32')
|
|
||||||
hal.soundcache[str(sound)] = (data, fs)
|
|
||||||
|
|
||||||
|
|
||||||
def init_camera(hal: PizzaHAL):
|
|
||||||
if hal.camera is None:
|
|
||||||
hal.camera = PiCamera(sensor_mode=5)
|
|
||||||
|
|
Loading…
Reference in a new issue