Going through TODOs for SIBI.
Implemented, untested: - start button - record sounds as mp3 (using pydub) - video and audio converted to mp4 - added new state REWIND
This commit is contained in:
parent
16165b61bd
commit
711454a06c
7 changed files with 86 additions and 71 deletions
|
@ -80,14 +80,15 @@ class StoryFile(FileHandle):
|
||||||
FileHandle.__init__(self, name, FileType.STORY)
|
FileHandle.__init__(self, name, FileType.STORY)
|
||||||
|
|
||||||
|
|
||||||
REC_NAME = RecFile('name.wav')
|
REC_NAME = RecFile('name.mp3')
|
||||||
REC_MY_IBK = RecFile('my_ibk.wav')
|
REC_MY_IBK = RecFile('my_ibk.mp3')
|
||||||
REC_PORTRAIT = RecFile('portrait.jpg')
|
REC_PORTRAIT = RecFile('portrait.jpg')
|
||||||
REC_CITY_NAME = RecFile('city_name.wav')
|
REC_CITY_NAME = RecFile('city_name.mp3')
|
||||||
REC_CITY_DESC = RecFile('city_description.wav')
|
REC_CITY_DESC = RecFile('city_description.mp3')
|
||||||
REC_CITY_SOUND = RecFile('city_sound.wav')
|
REC_CITY_SOUND = RecFile('city_sound.mp3')
|
||||||
REC_DRAW_CITY = RecFile('city_video.h264')
|
REC_DRAW_CITY = RecFile('city_video.h264')
|
||||||
REC_CITY_PHOTO = RecFile('city_drawing.jpg')
|
REC_CITY_PHOTO = RecFile('city_drawing.jpg')
|
||||||
|
REC_MERGED_VIDEO = RecFile('video.mp4')
|
||||||
|
|
||||||
SFX_ERROR = SfxFile('error')
|
SFX_ERROR = SfxFile('error')
|
||||||
SFX_ERROR_DE = SfxFile('error-de')
|
SFX_ERROR_DE = SfxFile('error-de')
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
# GPIO pin definitions
|
# GPIO pin definitions
|
||||||
|
|
||||||
|
BTN_START = 26 # "Start" button (begin the performance) TODO review
|
||||||
|
|
||||||
BTN_BACK_GPIO = 14 # "Back" button (user input)
|
BTN_BACK_GPIO = 14 # "Back" button (user input)
|
||||||
BTN_FORWARD_GPIO = 18 # "Forward" button (user input)
|
BTN_FORWARD_GPIO = 18 # "Forward" button (user input)
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,8 @@ from time import sleep
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
from typing import Any, List
|
from typing import Any, List
|
||||||
from scipy.io.wavfile import write as writewav
|
# from scipy.io.wavfile import write as writewav
|
||||||
|
import pydub
|
||||||
|
|
||||||
import sounddevice as sd
|
import sounddevice as sd
|
||||||
import soundfile as sf
|
import soundfile as sf
|
||||||
|
@ -13,7 +14,7 @@ import numpy as np
|
||||||
from . import gpio_pins
|
from . import gpio_pins
|
||||||
|
|
||||||
from picamera import PiCamera
|
from picamera import PiCamera
|
||||||
from gpiozero import Button, OutputDevice, PWMOutputDevice, PWMLED
|
from gpiozero import Button
|
||||||
|
|
||||||
import serial
|
import serial
|
||||||
|
|
||||||
|
@ -53,6 +54,8 @@ class PizzaHAL:
|
||||||
def __init__(self, serialdev: str = SERIAL_DEV, baudrate: int = SERIAL_BAUDRATE):
|
def __init__(self, serialdev: str = SERIAL_DEV, baudrate: int = SERIAL_BAUDRATE):
|
||||||
self.serialcon = serial.Serial(serialdev, baudrate=baudrate, timeout=None)
|
self.serialcon = serial.Serial(serialdev, baudrate=baudrate, timeout=None)
|
||||||
|
|
||||||
|
self.btn_start = Button(gpio_pins.BTN_START)
|
||||||
|
|
||||||
self.camera = None
|
self.camera = None
|
||||||
self.soundcache = {}
|
self.soundcache = {}
|
||||||
|
|
||||||
|
@ -120,7 +123,8 @@ def turn_off(hal: PizzaHAL):
|
||||||
|
|
||||||
|
|
||||||
def wait_for_input(hal: PizzaHAL, go_callback: Any,
|
def wait_for_input(hal: PizzaHAL, go_callback: Any,
|
||||||
back_callback: Any, **kwargs):
|
back_callback: Any, to_callback: Any,
|
||||||
|
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
|
||||||
|
@ -128,12 +132,16 @@ def wait_for_input(hal: PizzaHAL, go_callback: Any,
|
||||||
:param hal: The hardware abstraction object
|
:param hal: The hardware abstraction object
|
||||||
:param go_callback: called when button 'go' is pressed
|
:param go_callback: called when button 'go' is pressed
|
||||||
:param back_callback: called whan button 'back' is pressed
|
:param back_callback: called whan button 'back' is pressed
|
||||||
|
:param to_callback: called on timeout
|
||||||
|
:param timeout: inactivity timeout in seconds (default 120)
|
||||||
"""
|
"""
|
||||||
resp = hal.send_cmd(SerialCommands.USER_INTERACTION)
|
resp = hal.send_cmd(SerialCommands.USER_INTERACTION, timeout)
|
||||||
if resp == 'B':
|
if resp == 'B':
|
||||||
go_callback(**kwargs)
|
go_callback(**kwargs)
|
||||||
elif resp == 'R':
|
elif resp == 'R':
|
||||||
back_callback(**kwargs)
|
back_callback(**kwargs)
|
||||||
|
else:
|
||||||
|
to_callback(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
@blocking
|
@blocking
|
||||||
|
@ -200,8 +208,13 @@ def record_sound(hal: PizzaHAL, filename: Any, duration: int,
|
||||||
myrecording = sd.rec(int(duration * AUDIO_REC_SR),
|
myrecording = sd.rec(int(duration * AUDIO_REC_SR),
|
||||||
samplerate=AUDIO_REC_SR,
|
samplerate=AUDIO_REC_SR,
|
||||||
channels=2)
|
channels=2)
|
||||||
|
# TODO user interaction instead
|
||||||
sd.wait() # Wait until recording is finished
|
sd.wait() # Wait until recording is finished
|
||||||
writewav(str(filename), AUDIO_REC_SR, myrecording)
|
# TODO test
|
||||||
|
myrecording = np.int16(myrecording)
|
||||||
|
song = pydub.AudioSegment(myrecording.tobytes(), frame_rate=AUDIO_REC_SR, sample_width=2, channels=2)
|
||||||
|
song.export(str(filename), format="mp3", bitrate="320k")
|
||||||
|
# ALTERNATIVE 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)
|
||||||
|
|
||||||
|
|
|
@ -1,16 +1,15 @@
|
||||||
import logging
|
import logging
|
||||||
import os.path
|
import os.path
|
||||||
import threading
|
|
||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from time import sleep
|
from time import sleep
|
||||||
|
|
||||||
from enum import Enum, auto
|
from enum import Enum, auto
|
||||||
|
from subprocess import call
|
||||||
|
|
||||||
from pizzactrl import fs_names, sb_dummy, sb_de_alt
|
from pizzactrl import fs_names, sb_dummy
|
||||||
from .storyboard import Activity
|
from .storyboard import Activity
|
||||||
|
|
||||||
from .hal_serial import play_sound, take_photo, record_video, record_sound, turn_off, \
|
from .hal_serial import play_sound, take_photo, record_video, record_sound, turn_off, \
|
||||||
PizzaHAL, init_camera, init_sounds, wait_for_input, \
|
PizzaHAL, init_camera, init_sounds, wait_for_input, \
|
||||||
light_layer, backlight, move_vert, move_hor, rewind
|
light_layer, backlight, move_vert, move_hor, rewind
|
||||||
|
@ -24,6 +23,7 @@ class State(Enum):
|
||||||
IDLE_START = auto()
|
IDLE_START = auto()
|
||||||
PLAY = auto()
|
PLAY = auto()
|
||||||
PAUSE = auto()
|
PAUSE = auto()
|
||||||
|
REWIND = auto()
|
||||||
IDLE_END = auto()
|
IDLE_END = auto()
|
||||||
SHUTDOWN = auto()
|
SHUTDOWN = auto()
|
||||||
ERROR = -1
|
ERROR = -1
|
||||||
|
@ -54,7 +54,8 @@ class Statemachine:
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
story_de: Any=None,
|
story_de: Any=None,
|
||||||
story_en: Any=None,
|
story_en: Any=None,
|
||||||
move: bool = False):
|
move: bool = False,
|
||||||
|
loop: bool = True):
|
||||||
self.state = State.POWER_ON
|
self.state = State.POWER_ON
|
||||||
self.hal = PizzaHAL()
|
self.hal = PizzaHAL()
|
||||||
self.story = None
|
self.story = None
|
||||||
|
@ -64,6 +65,7 @@ class Statemachine:
|
||||||
self.lang = Language.NOT_SET
|
self.lang = Language.NOT_SET
|
||||||
self.move = move
|
self.move = move
|
||||||
self.test = False
|
self.test = False
|
||||||
|
self.loop = loop
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
logger.debug(f'Run(state={self.state})')
|
logger.debug(f'Run(state={self.state})')
|
||||||
|
@ -118,66 +120,50 @@ class Statemachine:
|
||||||
# check scroll positions and rewind if necessary
|
# check scroll positions and rewind if necessary
|
||||||
turn_off(self.hal)
|
turn_off(self.hal)
|
||||||
|
|
||||||
#rewind(self.hal.motor_ud, self.hal.ud_sensor)
|
|
||||||
|
|
||||||
if not os.path.exists(fs_names.USB_STICK):
|
if not os.path.exists(fs_names.USB_STICK):
|
||||||
logger.warning('USB-Stick not found.')
|
logger.warning('USB-Stick not found.')
|
||||||
self.state = State.ERROR
|
self.state = State.ERROR
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Callback for start when blue button is held
|
||||||
|
self.hal.btn_start.when_activated = self._start_or_rewind
|
||||||
|
|
||||||
# play a sound if everything is alright
|
# play a sound if everything is alright
|
||||||
play_sound(self.hal, fs_names.SFX_POST_OK)
|
play_sound(self.hal, fs_names.SFX_POST_OK)
|
||||||
|
|
||||||
# Callback for start when blue button is held
|
|
||||||
# TODO add start button
|
|
||||||
#self.hal.btn_forward.when_deactivated = self._start
|
|
||||||
|
|
||||||
self.state = State.IDLE_START
|
self.state = State.IDLE_START
|
||||||
|
|
||||||
def _idle_start(self):
|
def _idle_start(self):
|
||||||
"""
|
"""
|
||||||
Device is armed. Wait for user to hold blue button to start
|
Device is armed. Wait for user to press start button
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _start(self):
|
def _start_or_rewind(self):
|
||||||
"""
|
"""
|
||||||
Start playback when blue button is held for 3s
|
Callback function.
|
||||||
"""
|
|
||||||
t = 0.
|
|
||||||
while (self.hal.btn_forward.inactive_time < 3.0) and \
|
|
||||||
not self.hal.btn_forward.is_active:
|
|
||||||
t = self.hal.btn_forward.inactive_time
|
|
||||||
|
|
||||||
if t > 3.0:
|
If statemachine is in idle state, start playback when start
|
||||||
self.hal.btn_forward.when_deactivated = None
|
button is pressed (released).
|
||||||
if not self.hal.btn_back.is_active:
|
|
||||||
self.alt = True
|
If statemachine is playing, trigger rewind and start fresh
|
||||||
|
"""
|
||||||
|
if self.state == State.IDLE_START:
|
||||||
self.state = State.PLAY
|
self.state = State.PLAY
|
||||||
|
return
|
||||||
|
if self.state == State.PLAY:
|
||||||
|
self.state = State.REWIND
|
||||||
|
|
||||||
def _play(self):
|
def _play(self):
|
||||||
"""
|
"""
|
||||||
Run the storyboard
|
Run the storyboard
|
||||||
"""
|
"""
|
||||||
logger.debug(f'play')
|
logger.debug(f'play')
|
||||||
if not self.alt:
|
if self.test:
|
||||||
play_sound(self.hal, fs_names.SND_SELECT_LANG)
|
self.story = sb_dummy.STORYBOARD
|
||||||
wait_for_input(self.hal,
|
|
||||||
self._lang_de,
|
|
||||||
self._lang_en)
|
|
||||||
|
|
||||||
sleep(0.5)
|
|
||||||
else:
|
else:
|
||||||
self.story = sb_de_alt.STORYBOARD
|
# TODO reenable language selection
|
||||||
|
self.story = self.story_en
|
||||||
try:
|
|
||||||
if self.story is None:
|
|
||||||
self.story = sb_dummy.STORYBOARD
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
finally:
|
|
||||||
if self.test:
|
|
||||||
self.story = sb_dummy.STORYBOARD
|
|
||||||
|
|
||||||
for chapter in iter(self.story):
|
for chapter in iter(self.story):
|
||||||
logger.debug(f'playing chapter {chapter}')
|
logger.debug(f'playing chapter {chapter}')
|
||||||
|
@ -187,16 +173,17 @@ class Statemachine:
|
||||||
if act.activity is Activity.WAIT_FOR_INPUT:
|
if act.activity is Activity.WAIT_FOR_INPUT:
|
||||||
wait_for_input(hal=self.hal,
|
wait_for_input(hal=self.hal,
|
||||||
go_callback=chapter.mobilize,
|
go_callback=chapter.mobilize,
|
||||||
back_callback=chapter.rewind)
|
back_callback=chapter.rewind,
|
||||||
elif act.activity is Activity.ADVANCE_UP:
|
to_callback=self._start_or_rewind)
|
||||||
if chapter.move and self.move:
|
# elif act.activity is Activity.ADVANCE_UP:
|
||||||
logger.debug(
|
# if chapter.move and self.move:
|
||||||
f'advance({self.hal.motor_ud}, '
|
# logger.debug(
|
||||||
f'{self.hal.ud_sensor})')
|
# f'advance({self.hal.motor_ud}, '
|
||||||
advance(motor=self.hal.motor_ud,
|
# f'{self.hal.ud_sensor})')
|
||||||
sensor=self.hal.ud_sensor)
|
# advance(motor=self.hal.motor_ud,
|
||||||
elif not self.move:
|
# sensor=self.hal.ud_sensor)
|
||||||
play_sound(self.hal, fs_names.StoryFile('stop'))
|
# elif not self.move:
|
||||||
|
# play_sound(self.hal, fs_names.StoryFile('stop'))
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
{
|
{
|
||||||
|
@ -206,22 +193,35 @@ class Statemachine:
|
||||||
Activity.TAKE_PHOTO: take_photo,
|
Activity.TAKE_PHOTO: take_photo,
|
||||||
Activity.LIGHT_LAYER: light_layer,
|
Activity.LIGHT_LAYER: light_layer,
|
||||||
Activity.LIGHT_BACK: backlight,
|
Activity.LIGHT_BACK: backlight,
|
||||||
# Activity.ADVANCE_UP: noop
|
Activity.ADVANCE_UP: move_vert,
|
||||||
|
Activity.ADVANCE_LEFT: move_hor
|
||||||
}[act.activity](self.hal, **act.values)
|
}[act.activity](self.hal, **act.values)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
logger.exception('Caught KeyError, ignoring...')
|
logger.exception('Caught KeyError, ignoring...')
|
||||||
pass
|
pass
|
||||||
|
|
||||||
self.state = State.IDLE_END
|
self.state = State.REWIND
|
||||||
|
|
||||||
|
def _rewind(self):
|
||||||
|
"""
|
||||||
|
Rewind all scrolls, post-process videos
|
||||||
|
"""
|
||||||
|
# postprocessing
|
||||||
|
cmdstring = f'MP4Box -add {fs_names.REC_CITY_DESC} -add{fs_names.REC_DRAW_CITY} {fs_names.REC_MERGED_VIDEO}'
|
||||||
|
call([cmdstring], shell=True)
|
||||||
|
|
||||||
|
if self.move:
|
||||||
|
rewind(self.hal)
|
||||||
|
|
||||||
|
if self.loop:
|
||||||
|
self.state = State.IDLE_START
|
||||||
|
else:
|
||||||
|
self.state = State.IDLE_END
|
||||||
|
|
||||||
def _idle_end(self):
|
def _idle_end(self):
|
||||||
"""
|
"""
|
||||||
Initialize shutdown
|
Initialize shutdown
|
||||||
"""
|
"""
|
||||||
if self.move:
|
|
||||||
rewind(self.hal.motor_ud, self.hal.ud_sensor)
|
|
||||||
|
|
||||||
self.state = State.SHUTDOWN
|
self.state = State.SHUTDOWN
|
||||||
|
|
||||||
def _shutdown(self):
|
def _shutdown(self):
|
||||||
|
|
|
@ -7,7 +7,7 @@ class Activity(Enum):
|
||||||
RECORD_SOUND = {'duration': 0.0, 'filename': '', 'cache': False}
|
RECORD_SOUND = {'duration': 0.0, 'filename': '', 'cache': False}
|
||||||
RECORD_VIDEO = {'duration': 0.0, 'filename': ''}
|
RECORD_VIDEO = {'duration': 0.0, 'filename': ''}
|
||||||
TAKE_PHOTO = {'filename': ''}
|
TAKE_PHOTO = {'filename': ''}
|
||||||
ADVANCE_UP = {'speed': 0.3, 'direction': True}
|
ADVANCE_UP = {'speed': 0.3, 'direction': True, 'steps': 100}
|
||||||
ADVANCE_LEFT = {'speed': 0.3, 'direction': True, 'steps': 180} # TODO set right number of steps
|
ADVANCE_LEFT = {'speed': 0.3, 'direction': True, 'steps': 180} # TODO set right number of steps
|
||||||
LIGHT_LAYER = {'intensity': 1.0, 'fade': 0.0, 'layer': True}
|
LIGHT_LAYER = {'intensity': 1.0, 'fade': 0.0, 'layer': True}
|
||||||
LIGHT_BACK = {'intensity': 1.0, 'fade': 0.0}
|
LIGHT_BACK = {'intensity': 1.0, 'fade': 0.0}
|
||||||
|
@ -40,8 +40,6 @@ class Chapter:
|
||||||
if self.pos >= len(self.activities):
|
if self.pos >= len(self.activities):
|
||||||
raise StopIteration
|
raise StopIteration
|
||||||
act = self.activities[self.pos]
|
act = self.activities[self.pos]
|
||||||
if act.activity is Activity.ADVANCE_UP: # TODO add ADVANCE_LEFT
|
|
||||||
self.move_ud += 1
|
|
||||||
self.pos += 1
|
self.pos += 1
|
||||||
return act
|
return act
|
||||||
|
|
||||||
|
@ -50,7 +48,6 @@ class Chapter:
|
||||||
|
|
||||||
def rewind(self, **kwargs):
|
def rewind(self, **kwargs):
|
||||||
self.move = False
|
self.move = False
|
||||||
self.move_ud = 0
|
|
||||||
self.pos = 0
|
self.pos = 0
|
||||||
|
|
||||||
def mobilize(self, **kwargs):
|
def mobilize(self, **kwargs):
|
||||||
|
|
|
@ -6,3 +6,4 @@ sounddevice
|
||||||
soundfile
|
soundfile
|
||||||
scipy
|
scipy
|
||||||
pyserial
|
pyserial
|
||||||
|
pydub
|
3
setup.py
3
setup.py
|
@ -30,7 +30,8 @@ with open('pizzactrl/__init__.py', 'rb') as f:
|
||||||
'sounddevice',
|
'sounddevice',
|
||||||
'soundfile',
|
'soundfile',
|
||||||
'scipy',
|
'scipy',
|
||||||
'pyserial'
|
'pyserial',
|
||||||
|
'pydub'
|
||||||
],
|
],
|
||||||
|
|
||||||
entry_points='''
|
entry_points='''
|
||||||
|
|
Loading…
Reference in a new issue