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:
jpunkt 2022-01-13 19:18:40 +01:00
parent 77eb772776
commit 2c85f7ed46

View file

@ -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)