2021-10-24 16:17:36 +00:00
|
|
|
import logging
|
|
|
|
import functools
|
|
|
|
from time import sleep
|
|
|
|
from enum import Enum
|
|
|
|
|
|
|
|
from typing import Any, List
|
2021-11-04 14:04:12 +00:00
|
|
|
from scipy.io.wavfile import write as writewav
|
2021-10-24 16:17:36 +00:00
|
|
|
|
|
|
|
import sounddevice as sd
|
|
|
|
import soundfile as sf
|
|
|
|
|
|
|
|
from . import gpio_pins
|
|
|
|
|
|
|
|
from picamera import PiCamera
|
2021-11-01 16:00:40 +00:00
|
|
|
from gpiozero import Button
|
2021-10-24 16:17:36 +00:00
|
|
|
|
|
|
|
import serial
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
# Constants
|
|
|
|
VIDEO_RES = (1920, 1080) # Video Resolution
|
|
|
|
PHOTO_RES = (2592, 1944) # Photo Resolution
|
|
|
|
AUDIO_REC_SR = 44100 # Audio Recording Samplerate
|
2022-01-05 18:09:31 +00:00
|
|
|
|
|
|
|
SERIAL_DEV = '/dev/serial0' # Serial port to use
|
|
|
|
SERIAL_BAUDRATE = 115200 # Serial connection baud rate
|
|
|
|
SERIAL_CONN_TIMEOUT = 60 # Serial connection read timeout
|
2021-10-24 16:17:36 +00:00
|
|
|
|
|
|
|
|
|
|
|
class SerialCommands(Enum):
|
2021-12-21 18:13:28 +00:00
|
|
|
HELLO = b'\x00'
|
|
|
|
ALREADY_CONNECTED = b'\x01'
|
|
|
|
ERROR = b'\x02'
|
|
|
|
RECEIVED = b'\x03'
|
|
|
|
|
|
|
|
MOTOR_H = b'H'
|
|
|
|
MOTOR_V = b'V'
|
|
|
|
|
|
|
|
BACKLIGHT = b'B'
|
|
|
|
FRONTLIGHT = b'F'
|
|
|
|
|
|
|
|
USER_INTERACT = b'U'
|
2022-01-05 18:09:31 +00:00
|
|
|
|
|
|
|
RESP_BLUE = b'X'
|
|
|
|
RESP_RED = b'O'
|
|
|
|
RESP_YELLOW = b'Y'
|
|
|
|
RESP_GREEN = b'N'
|
2021-12-21 18:13:28 +00:00
|
|
|
|
|
|
|
RECORD = b'C'
|
|
|
|
REWIND = b'R'
|
|
|
|
|
2022-01-11 19:21:44 +00:00
|
|
|
DEBUG_SCROLL = b'S'
|
|
|
|
DEBUG_SENSORS = b'Z'
|
|
|
|
|
2021-12-21 18:13:28 +00:00
|
|
|
EOT = b'\n'
|
2021-10-24 16:17:36 +00:00
|
|
|
|
|
|
|
|
2022-01-05 18:09:31 +00:00
|
|
|
class SerialCommunicationError(Exception):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
2021-10-24 16:17:36 +00:00
|
|
|
class PizzaHAL:
|
|
|
|
"""
|
|
|
|
This class holds a represenation of the pizza box hardware and provides
|
|
|
|
methods to interact with it.
|
|
|
|
|
|
|
|
- lights upper/lower on/off
|
|
|
|
- motor up-down/left-right speed distance
|
|
|
|
- scroll up-down/left-right positions
|
|
|
|
- lid open/closed detectors
|
|
|
|
- user interface buttons
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
2022-01-05 18:09:31 +00:00
|
|
|
def __init__(self, serialdev: str = SERIAL_DEV, baudrate: int = SERIAL_BAUDRATE, timeout: float = SERIAL_CONN_TIMEOUT):
|
|
|
|
self.serialcon = serial.Serial(serialdev, baudrate=baudrate, timeout=timeout)
|
2021-10-24 16:17:36 +00:00
|
|
|
|
2021-11-01 16:00:40 +00:00
|
|
|
self.btn_start = Button(gpio_pins.BTN_START)
|
|
|
|
|
2021-10-24 16:17:36 +00:00
|
|
|
self.camera = None
|
|
|
|
self.soundcache = {}
|
|
|
|
|
2022-01-05 18:09:31 +00:00
|
|
|
self.connected = False
|
|
|
|
|
|
|
|
def init_connection(self):
|
|
|
|
self.serialcon.write(SerialCommands.HELLO.value + SerialCommands.EOT.value)
|
|
|
|
resp = self.serialcon.read_until()
|
|
|
|
if resp == (SerialCommands.HELLO.value + SerialCommands.EOT.value):
|
|
|
|
self.serialcon.write(SerialCommands.ALREADY_CONNECTED.value + SerialCommands.EOT.value)
|
2022-01-11 19:21:44 +00:00
|
|
|
resp = self.serialcon.read_until()
|
|
|
|
if resp == (SerialCommands.ALREADY_CONNECTED.value + SerialCommands.EOT.value):
|
|
|
|
logger.info('Serial Connection established')
|
|
|
|
elif resp == b'':
|
|
|
|
raise SerialCommunicationError('Timeout on initializing connection.')
|
|
|
|
else:
|
|
|
|
raise SerialCommunicationError(f'Serial Connection received invalid response to ALREADY CONNECTED: {resp}')
|
2022-01-05 18:09:31 +00:00
|
|
|
elif resp == (SerialCommands.ALREADY_CONNECTED.value + SerialCommands.EOT.value):
|
|
|
|
logger.warn('Serial Connection received ALREADY CONNECTED as response to HELLO. Assuming connection ok.')
|
|
|
|
elif resp == b'':
|
|
|
|
raise SerialCommunicationError('Timeout on initializing connection.')
|
|
|
|
else:
|
|
|
|
raise SerialCommunicationError(f'Serial Connection received invalid response to HELLO: {resp}')
|
2022-01-11 19:21:44 +00:00
|
|
|
self.connected = True
|
2022-01-05 18:09:31 +00:00
|
|
|
|
2022-01-11 19:21:44 +00:00
|
|
|
def send_cmd(self, command: SerialCommands, *options):
|
2021-12-21 18:13:28 +00:00
|
|
|
"""
|
|
|
|
Send a command and optional options. Options need to be encoded as bytes before passing.
|
|
|
|
"""
|
2022-01-05 18:09:31 +00:00
|
|
|
if not self.connected:
|
|
|
|
raise SerialCommunicationError("Serial Communication not initialized. Call `init_connection()` before `send_cmd()`.")
|
2021-12-21 18:13:28 +00:00
|
|
|
self.serialcon.write(command.value)
|
|
|
|
for o in options:
|
|
|
|
self.serialcon.write(o)
|
|
|
|
self.serialcon.write(SerialCommands.EOT.value)
|
|
|
|
resp = self.serialcon.read_until()
|
|
|
|
# TODO handle errors in response
|
2021-10-24 16:17:36 +00:00
|
|
|
return resp
|
|
|
|
|
|
|
|
|
|
|
|
def move_vert(hal: PizzaHAL, steps: int):
|
|
|
|
"""
|
|
|
|
Move the motor controlling the vertical scroll a given distance.
|
|
|
|
|
|
|
|
"""
|
2021-12-21 18:13:28 +00:00
|
|
|
hal.send_cmd(SerialCommands.MOTOR_V, steps.to_bytes(2, 'little', signed=True))
|
2021-10-24 16:17:36 +00:00
|
|
|
|
|
|
|
|
|
|
|
def move_hor(hal: PizzaHAL, steps: int):
|
|
|
|
"""
|
|
|
|
Move the motor controlling the horizontal scroll a given distance.
|
|
|
|
|
|
|
|
"""
|
2021-12-21 18:13:28 +00:00
|
|
|
hal.send_cmd(SerialCommands.MOTOR_H, steps.to_bytes(2, 'little', signed=True))
|
2021-10-24 16:17:36 +00:00
|
|
|
|
|
|
|
|
|
|
|
def rewind(hal: PizzaHAL):
|
|
|
|
"""
|
|
|
|
Rewind both scrolls.
|
|
|
|
|
|
|
|
"""
|
|
|
|
hal.send_cmd(SerialCommands.REWIND)
|
|
|
|
|
|
|
|
|
|
|
|
def turn_off(hal: PizzaHAL):
|
|
|
|
"""
|
|
|
|
Turn off everything.
|
|
|
|
"""
|
|
|
|
hal.send_cmd(SerialCommands.BACKLIGHT, 0)
|
|
|
|
hal.send_cmd(SerialCommands.FRONTLIGHT, 0)
|
|
|
|
|
|
|
|
|
|
|
|
def wait_for_input(hal: PizzaHAL, go_callback: Any,
|
2021-11-01 16:00:40 +00:00
|
|
|
back_callback: Any, to_callback: Any,
|
|
|
|
timeout=120, **kwargs):
|
2021-10-24 16:17:36 +00:00
|
|
|
"""
|
|
|
|
Blink leds on buttons. Wait until the user presses a button, then execute
|
|
|
|
the appropriate callback
|
|
|
|
|
|
|
|
:param hal: The hardware abstraction object
|
|
|
|
:param go_callback: called when button 'go' is pressed
|
|
|
|
:param back_callback: called whan button 'back' is pressed
|
2021-11-01 16:00:40 +00:00
|
|
|
:param to_callback: called on timeout
|
|
|
|
:param timeout: inactivity timeout in seconds (default 120)
|
2021-10-24 16:17:36 +00:00
|
|
|
"""
|
2021-11-04 13:21:33 +00:00
|
|
|
resp = hal.send_cmd(SerialCommands.USER_INTERACTION, timeout).strip()
|
2021-11-04 13:33:31 +00:00
|
|
|
if resp == b'B':
|
2021-10-24 16:17:36 +00:00
|
|
|
go_callback(**kwargs)
|
2021-11-04 13:33:31 +00:00
|
|
|
elif resp == b'R':
|
2021-10-24 16:17:36 +00:00
|
|
|
back_callback(**kwargs)
|
2021-11-01 16:00:40 +00:00
|
|
|
else:
|
|
|
|
to_callback(**kwargs)
|
2021-10-24 16:17:36 +00:00
|
|
|
|
|
|
|
|
2021-12-21 18:13:28 +00:00
|
|
|
def light_layer(hal: PizzaHAL, r: float, g: float, b: float, w: float, fade: float = 0.0, **kwargs):
|
2021-10-24 16:17:36 +00:00
|
|
|
"""
|
|
|
|
Turn on the light to illuminate the upper scroll
|
|
|
|
|
|
|
|
:param hal: The hardware abstraction object
|
|
|
|
:param fade: float
|
|
|
|
Default 0, time in seconds to fade in or out
|
|
|
|
:param intensity: float
|
|
|
|
Intensity of the light in percent
|
2021-10-29 16:40:27 +00:00
|
|
|
:param steps: int
|
|
|
|
How many steps for the fade (default: 100)
|
2021-10-24 16:17:36 +00:00
|
|
|
"""
|
2021-12-21 18:13:28 +00:00
|
|
|
hal.send_cmd(SerialCommands.FRONTLIGHT,
|
|
|
|
int(r * 255).to_bytes(1, 'little'),
|
|
|
|
int(g * 255).to_bytes(1, 'little'),
|
|
|
|
int(b * 255).to_bytes(1, 'little'),
|
|
|
|
int(w * 255).to_bytes(1, 'little'),
|
|
|
|
int(fade * 1000).to_bytes(4, 'little'))
|
2021-10-24 16:17:36 +00:00
|
|
|
|
|
|
|
|
2021-12-21 18:13:28 +00:00
|
|
|
def backlight(hal: PizzaHAL, r: float, g: float, b: float, w: float, fade: float = 0.0, **kwargs):
|
2021-10-24 16:17:36 +00:00
|
|
|
"""
|
|
|
|
Turn on the backlight
|
|
|
|
|
|
|
|
:param hal: The hardware abstraction object
|
|
|
|
:param fade: float
|
|
|
|
Default 0, time in seconds to fade in or out
|
|
|
|
:param intensity: float
|
|
|
|
Intensity of the light in percent
|
|
|
|
:param steps: int
|
2021-10-29 16:40:27 +00:00
|
|
|
How many steps for the fade (default: 100)
|
2021-10-24 16:17:36 +00:00
|
|
|
"""
|
2021-12-21 18:13:28 +00:00
|
|
|
hal.send_cmd(SerialCommands.BACKLIGHT,
|
|
|
|
int(r * 255).to_bytes(1, 'little'),
|
|
|
|
int(g * 255).to_bytes(1, 'little'),
|
|
|
|
int(b * 255).to_bytes(1, 'little'),
|
|
|
|
int(w * 255).to_bytes(1, 'little'),
|
|
|
|
int(fade * 1000).to_bytes(4, 'little'))
|
2021-10-24 16:17:36 +00:00
|
|
|
|
2022-01-05 18:09:31 +00:00
|
|
|
|
2021-10-24 16:17:36 +00:00
|
|
|
def play_sound(hal: PizzaHAL, sound: Any, **kwargs):
|
|
|
|
"""
|
|
|
|
Play a sound.
|
|
|
|
|
|
|
|
:param hal: The hardware abstraction object
|
|
|
|
:param sound: The sound to be played
|
|
|
|
"""
|
|
|
|
# Extract data and sampling rate from file
|
|
|
|
try:
|
|
|
|
data, fs = hal.soundcache.get(str(sound), sf.read(str(sound), dtype='float32'))
|
|
|
|
sd.play(data, fs)
|
|
|
|
sd.wait() # Wait until file is done playing
|
|
|
|
except KeyboardInterrupt:
|
|
|
|
logger.debug('skipped playback')
|
|
|
|
# sd.stop()
|
|
|
|
|
|
|
|
|
2021-11-19 15:09:49 +00:00
|
|
|
def record_sound(hal: PizzaHAL, filename: Any, duration: float,
|
2021-10-24 16:17:36 +00:00
|
|
|
cache: bool = False, **kwargs):
|
|
|
|
"""
|
|
|
|
Record sound using the microphone
|
|
|
|
|
|
|
|
:param hal: The hardware abstraction object
|
|
|
|
:param filename: The path of the file to record to
|
|
|
|
:param duration: The time to record in seconds
|
|
|
|
:param cache: `True` to save recording to cache. Default is `False`
|
|
|
|
"""
|
|
|
|
myrecording = sd.rec(int(duration * AUDIO_REC_SR),
|
|
|
|
samplerate=AUDIO_REC_SR,
|
|
|
|
channels=2)
|
2021-11-19 15:09:49 +00:00
|
|
|
resp = hal.send_cmd(SerialCommands.RECORD, int(duration)).strip()
|
2021-11-04 13:35:15 +00:00
|
|
|
if resp == b'I':
|
2021-11-04 13:21:33 +00:00
|
|
|
sd.stop()
|
|
|
|
else:
|
|
|
|
sd.wait() # Wait until recording is finished
|
2021-11-04 14:04:12 +00:00
|
|
|
writewav(str(filename), AUDIO_REC_SR, myrecording)
|
2021-10-24 16:17:36 +00:00
|
|
|
if cache:
|
|
|
|
hal.soundcache[str(filename)] = (myrecording, AUDIO_REC_SR)
|
|
|
|
|
|
|
|
|
|
|
|
def record_video(hal: PizzaHAL, filename: Any, duration: float, **kwargs):
|
|
|
|
"""
|
|
|
|
Record video using the camera
|
|
|
|
|
|
|
|
:param hal: The hardware abstraction object
|
|
|
|
:param filename: The path of the file to record to
|
|
|
|
:param duration: The time to record in seconds
|
|
|
|
"""
|
|
|
|
hal.camera.resolution = VIDEO_RES
|
|
|
|
hal.camera.start_recording(str(filename))
|
|
|
|
hal.camera.wait_recording(duration)
|
|
|
|
hal.camera.stop_recording()
|
|
|
|
|
|
|
|
|
|
|
|
def take_photo(hal: PizzaHAL, filename: Any, **kwargs):
|
|
|
|
"""
|
|
|
|
Take a foto with the camera
|
|
|
|
|
|
|
|
:param hal: The hardware abstraction object
|
|
|
|
:param filename: The path of the filename for the foto
|
|
|
|
"""
|
|
|
|
hal.camera.resolution = PHOTO_RES
|
|
|
|
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:
|
2021-11-04 14:52:09 +00:00
|
|
|
hal.camera = PiCamera(sensor_mode=5)
|