From 06cbde417950c0801f2bd02f213d44f921e41739 Mon Sep 17 00:00:00 2001 From: jpunkt Date: Fri, 14 Jan 2022 22:47:19 +0100 Subject: [PATCH] Updated storyboard mechanics Updated statemachine Implemented advanced choice mechanics Still testing. --- pizzactrl/hal_serial.py | 61 +++------ pizzactrl/sb_dummy.py | 276 +++++--------------------------------- pizzactrl/statemachine.py | 239 ++++++++++++++++++++++++--------- pizzactrl/storyboard.py | 125 ++++++++++++++--- 4 files changed, 333 insertions(+), 368 deletions(-) diff --git a/pizzactrl/hal_serial.py b/pizzactrl/hal_serial.py index 440c2c7..410f64e 100644 --- a/pizzactrl/hal_serial.py +++ b/pizzactrl/hal_serial.py @@ -120,7 +120,7 @@ class PizzaHAL: # 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) + self.soundcache[str(sound)] = mx.Sound(str(sound)) def init_camera(self): if self.camera is None: @@ -157,23 +157,19 @@ class PizzaHAL: return resp -def move_vert(hal: PizzaHAL, steps: int): +def move(hal: PizzaHAL, + steps: int, + horizontal: bool, + **kwargs): """ Move the motor controlling the vertical scroll a given distance. """ - hal.send_cmd(SerialCommands.MOTOR_V, steps.to_bytes(2, 'little', signed=True)) + hal.send_cmd(SerialCommands.MOTOR_H if horizontal else SerialCommands.MOTOR_V, + steps.to_bytes(2, 'little', signed=True)) -def move_hor(hal: PizzaHAL, steps: int): - """ - Move the motor controlling the horizontal scroll a given distance. - - """ - hal.send_cmd(SerialCommands.MOTOR_H, steps.to_bytes(2, 'little', signed=True)) - - -def rewind(hal: PizzaHAL): +def rewind(hal: PizzaHAL, **kwargs): """ Rewind both scrolls. @@ -181,9 +177,9 @@ def rewind(hal: PizzaHAL): hal.send_cmd(SerialCommands.REWIND) -def turn_off(hal: PizzaHAL): +def turn_off(hal: PizzaHAL, **kwargs): """ - Turn off everything. + Turn off the lights. """ hal.send_cmd(SerialCommands.BACKLIGHT, 0) hal.send_cmd(SerialCommands.FRONTLIGHT, 0) @@ -223,7 +219,7 @@ def wait_for_input(hal: PizzaHAL, (8 if green_cb else 0) if sound is not None: - hal.play_sound(sound) + hal.play_sound(str(sound)) resp = hal.send_cmd(SerialCommands.USER_INTERACT, bitmask.to_bytes(1, 'little', signed=False), timeout.to_bytes(4, 'little', signed=False)) @@ -246,7 +242,14 @@ def wait_for_input(hal: PizzaHAL, timeout_cb(**kwargs) -def light_layer(hal: PizzaHAL, r: float, g: float, b: float, w: float, fade: float = 0.0, **kwargs): +def light(hal: PizzaHAL, + r: float, + g: float, + b: float, + w: float, + fade: float = 0.0, + backlight: bool = False, + **kwargs): """ Turn on the light to illuminate the upper scroll @@ -258,30 +261,10 @@ def light_layer(hal: PizzaHAL, r: float, g: float, b: float, w: float, fade: flo :param steps: int How many steps for the fade (default: 100) """ - hal.send_cmd(SerialCommands.FRONTLIGHT, - int(r * 255).to_bytes(1, 'little'), - int(g * 255).to_bytes(1, 'little'), + hal.send_cmd(SerialCommands.BACKLIGHT if backlight else SerialCommands.FRONTLIGHT, int(b * 255).to_bytes(1, 'little'), - int(w * 255).to_bytes(1, 'little'), - int(fade * 1000).to_bytes(4, 'little')) - - -def backlight(hal: PizzaHAL, r: float, g: float, b: float, w: float, fade: float = 0.0, **kwargs): - """ - 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 - How many steps for the fade (default: 100) - """ - 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(r * 255).to_bytes(1, 'little'), int(w * 255).to_bytes(1, 'little'), int(fade * 1000).to_bytes(4, 'little')) @@ -295,7 +278,7 @@ def play_sound(hal: PizzaHAL, sound: Any, **kwargs): """ # Extract data and sampling rate from file try: - hal.play_sound(sound) + hal.play_sound(str(sound)) while mx.get_busy(): pass except KeyboardInterrupt: diff --git a/pizzactrl/sb_dummy.py b/pizzactrl/sb_dummy.py index c9198d8..7c37871 100644 --- a/pizzactrl/sb_dummy.py +++ b/pizzactrl/sb_dummy.py @@ -1,248 +1,42 @@ -from pizzactrl import storyboard, fs_names +from pizzactrl import fs_names +from pizzactrl.storyboard import * STORYBOARD = [ - storyboard.Chapter( - # storyboard.Do(storyboard.Activity.ADVANCE_UP), - storyboard.Do(storyboard.Activity.LIGHT_BACK, # Bild 1 - intensity=1.0, fade=1.0) + Chapter( + Do(Activity.ADVANCE_UP, + steps=50), + Do(Activity.LIGHT_BACK, # Bild 1 + intensity=1.0, fade=1.0), + Do(Activity.WAIT_FOR_INPUT, + on_blue=Select(Option.CONTINUE), + on_red=Select(Option.REPEAT)) ), - storyboard.Chapter( - storyboard.Do(storyboard.Activity.PLAY_SOUND, - sound=fs_names.StoryFile('01dummy')), - storyboard.Do(storyboard.Activity.WAIT_FOR_INPUT), - storyboard.Do(storyboard.Activity.ADVANCE_UP) # Bild 2 + Chapter( + Do(Activity.ADVANCE_LEFT, + steps=100), + Do(Activity.ADVANCE_UP, + steps=50), + Do(Activity.WAIT_FOR_INPUT, + on_blue=Select(Option.CONTINUE), + on_red=Select(Option.REPEAT), + on_yellow=Select(Option.GOTO, chapter=0), + on_green=Select(Option.QUIT)) ), - storyboard.Chapter( - storyboard.Do(storyboard.Activity.PLAY_SOUND, - sound=fs_names.StoryFile('01dummy')), - storyboard.Do(storyboard.Activity.ADVANCE_UP), # Bild 3 - storyboard.Do(storyboard.Activity.PLAY_SOUND, - sound=fs_names.StoryFile('01dummy')), - storyboard.Do(storyboard.Activity.PLAY_SOUND, - sound=fs_names.StoryFile('01dummy')), - storyboard.Do(storyboard.Activity.PLAY_SOUND, - sound=fs_names.StoryFile('01dummy')), - storyboard.Do(storyboard.Activity.ADVANCE_UP), # Bild 4 - storyboard.Do(storyboard.Activity.PLAY_SOUND, - sound=fs_names.StoryFile('01dummy')), - storyboard.Do(storyboard.Activity.PLAY_SOUND, - sound=fs_names.StoryFile('01dummy')), - storyboard.Do(storyboard.Activity.LIGHT_BACK, - intensity=0.0), - storyboard.Do(storyboard.Activity.WAIT_FOR_INPUT), - storyboard.Do(storyboard.Activity.ADVANCE_UP), # Bild 5 + Chapter( + Do(Activity.ADVANCE_LEFT, + steps=-50), + Do(Activity.ADVANCE_UP, + steps=-20) ), - storyboard.Chapter( - storyboard.Do(storyboard.Activity.LIGHT_BACK, - intensity=1.0), - storyboard.Do(storyboard.Activity.PLAY_SOUND, - sound=fs_names.StoryFile('01dummy')), - storyboard.Do(storyboard.Activity.PLAY_SOUND, - sound=fs_names.StoryFile('01dummy')), - storyboard.Do(storyboard.Activity.PLAY_SOUND, - sound=fs_names.SFX_REC_AUDIO), - storyboard.Do(storyboard.Activity.PLAY_SOUND, - sound=fs_names.StoryFile('01dummy')), - storyboard.Do(storyboard.Activity.WAIT_FOR_INPUT), - storyboard.Do(storyboard.Activity.PLAY_SOUND, - sound=fs_names.SFX_REC_AUDIO), - storyboard.Do(storyboard.Activity.RECORD_SOUND, - filename=fs_names.REC_NAME, - duration=7.0, - cache=True), - storyboard.Do(storyboard.Activity.PLAY_SOUND, - sound=fs_names.SFX_STOP_REC), - ), - storyboard.Chapter( - storyboard.Do(storyboard.Activity.PLAY_SOUND, - sound=fs_names.StoryFile('01dummy')), - storyboard.Do(storyboard.Activity.PLAY_SOUND, - sound=fs_names.REC_NAME), - storyboard.Do(storyboard.Activity.PLAY_SOUND, - sound=fs_names.StoryFile('01dummy')), - storyboard.Do(storyboard.Activity.ADVANCE_UP), # Bild 6 - storyboard.Do(storyboard.Activity.PLAY_SOUND, - sound=fs_names.StoryFile('01dummy')), - storyboard.Do(storyboard.Activity.WAIT_FOR_INPUT), - storyboard.Do(storyboard.Activity.PLAY_SOUND, - sound=fs_names.SFX_REC_AUDIO), - storyboard.Do(storyboard.Activity.RECORD_SOUND, - filename=fs_names.REC_CITY_DESC, - duration=60.0), - storyboard.Do(storyboard.Activity.PLAY_SOUND, - sound=fs_names.SFX_STOP_REC), - storyboard.Do(storyboard.Activity.ADVANCE_UP) # Bild 7 - ), - storyboard.Chapter( - storyboard.Do(storyboard.Activity.PLAY_SOUND, - sound=fs_names.StoryFile('01dummy')), - storyboard.Do(storyboard.Activity.LIGHT_BACK, - intensity=0.0), - storyboard.Do(storyboard.Activity.WAIT_FOR_INPUT), - storyboard.Do(storyboard.Activity.TAKE_PHOTO, - filename=fs_names.REC_PORTRAIT), - storyboard.Do(storyboard.Activity.PLAY_SOUND, - sound=fs_names.SFX_SHUTTER) - ), - storyboard.Chapter( - storyboard.Do(storyboard.Activity.PLAY_SOUND, - sound=fs_names.StoryFile('01dummy')), - storyboard.Do(storyboard.Activity.WAIT_FOR_INPUT), - storyboard.Do(storyboard.Activity.ADVANCE_UP) - ), - storyboard.Chapter( - storyboard.Do(storyboard.Activity.PLAY_SOUND, - sound=fs_names.StoryFile('01dummy')), - storyboard.Do(storyboard.Activity.LIGHT_BACK, - intensity=1.0), - storyboard.Do(storyboard.Activity.PLAY_SOUND, - sound=fs_names.StoryFile('01dummy')), - storyboard.Do(storyboard.Activity.PLAY_SOUND, - sound=fs_names.StoryFile('01dummy')), - storyboard.Do(storyboard.Activity.LIGHT_BACK, - intensity=0., fade=2.0), - storyboard.Do(storyboard.Activity.PLAY_SOUND, - sound=fs_names.StoryFile('01dummy')), - storyboard.Do(storyboard.Activity.WAIT_FOR_INPUT), - storyboard.Do(storyboard.Activity.ADVANCE_UP) # Bild 9 - ), - storyboard.Chapter( - storyboard.Do(storyboard.Activity.LIGHT_LAYER, - intensity=1., fade=0.5), - storyboard.Do(storyboard.Activity.LIGHT_BACK, - intensity=1., fade=0.5), - storyboard.Do(storyboard.Activity.PLAY_SOUND, - sound=fs_names.StoryFile('01dummy')), - storyboard.Do(storyboard.Activity.PLAY_SOUND, - sound=fs_names.StoryFile('01dummy')), - storyboard.Do(storyboard.Activity.PLAY_SOUND, - sound=fs_names.StoryFile('01dummy')), - storyboard.Do(storyboard.Activity.LIGHT_LAYER, - intensity=0., fade=1.), - storyboard.Do(storyboard.Activity.LIGHT_BACK, - intensity=0., fade=1.), - storyboard.Do(storyboard.Activity.PLAY_SOUND, - sound=fs_names.StoryFile('01dummy')), - storyboard.Do(storyboard.Activity.WAIT_FOR_INPUT), - storyboard.Do(storyboard.Activity.ADVANCE_UP) # Bild 10 - ), - storyboard.Chapter( - storyboard.Do(storyboard.Activity.LIGHT_BACK, - intensity=1., fade=1.), - storyboard.Do(storyboard.Activity.PLAY_SOUND, - sound=fs_names.StoryFile('01dummy')), - storyboard.Do(storyboard.Activity.PLAY_SOUND, - sound=fs_names.StoryFile('01dummy')), - storyboard.Do(storyboard.Activity.PLAY_SOUND, - sound=fs_names.StoryFile('01dummy')), - storyboard.Do(storyboard.Activity.LIGHT_BACK, - intensity=0., fade=1.), - storyboard.Do(storyboard.Activity.PLAY_SOUND, - sound=fs_names.StoryFile('01dummy')), - storyboard.Do(storyboard.Activity.WAIT_FOR_INPUT), - storyboard.Do(storyboard.Activity.ADVANCE_UP) # Bild 11 - ), - storyboard.Chapter( - storyboard.Do(storyboard.Activity.LIGHT_BACK, - intensity=1., fade=1.), - storyboard.Do(storyboard.Activity.PLAY_SOUND, - sound=fs_names.StoryFile('01dummy')), - storyboard.Do(storyboard.Activity.PLAY_SOUND, - sound=fs_names.StoryFile('01dummy')), - storyboard.Do(storyboard.Activity.LIGHT_BACK, - intensity=0., fade=1.), - storyboard.Do(storyboard.Activity.PLAY_SOUND, - sound=fs_names.StoryFile('01dummy')), - storyboard.Do(storyboard.Activity.WAIT_FOR_INPUT), - storyboard.Do(storyboard.Activity.ADVANCE_UP) # Bild 12 - ), - storyboard.Chapter( - storyboard.Do(storyboard.Activity.LIGHT_BACK, - intensity=1., fade=.5), - storyboard.Do(storyboard.Activity.PLAY_SOUND, - sound=fs_names.StoryFile('01dummy')), - storyboard.Do(storyboard.Activity.WAIT_FOR_INPUT), - storyboard.Do(storyboard.Activity.ADVANCE_UP) # Bild 13 - ), - storyboard.Chapter( - storyboard.Do(storyboard.Activity.PLAY_SOUND, - sound=fs_names.StoryFile('01dummy')), - storyboard.Do(storyboard.Activity.LIGHT_BACK, - intensity=0., fade=1.), - storyboard.Do(storyboard.Activity.WAIT_FOR_INPUT), - storyboard.Do(storyboard.Activity.PLAY_SOUND, - sound=fs_names.SFX_REC_AUDIO), - storyboard.Do(storyboard.Activity.RECORD_SOUND, - filename=fs_names.REC_CITY_NAME, - duration=7.0), - storyboard.Do(storyboard.Activity.PLAY_SOUND, - sound=fs_names.SFX_STOP_REC), - storyboard.Do(storyboard.Activity.ADVANCE_UP) # Bild 14 - ), - storyboard.Chapter( - storyboard.Do(storyboard.Activity.LIGHT_BACK, - intensity=1., fade=1.), - storyboard.Do(storyboard.Activity.PLAY_SOUND, - sound=fs_names.StoryFile('01dummy')), - storyboard.Do(storyboard.Activity.WAIT_FOR_INPUT), - storyboard.Do(storyboard.Activity.PLAY_SOUND, - sound=fs_names.SFX_REC_AUDIO), - storyboard.Do(storyboard.Activity.RECORD_SOUND, - filename=fs_names.REC_CITY_DESC, - duration=60.0), - storyboard.Do(storyboard.Activity.PLAY_SOUND, - sound=fs_names.SFX_STOP_REC), - storyboard.Do(storyboard.Activity.ADVANCE_UP) # Bild 15 - ), - storyboard.Chapter( - storyboard.Do(storyboard.Activity.PLAY_SOUND, - sound=fs_names.StoryFile('01dummy')), - storyboard.Do(storyboard.Activity.WAIT_FOR_INPUT), - storyboard.Do(storyboard.Activity.PLAY_SOUND, - sound=fs_names.SFX_REC_AUDIO), - storyboard.Do(storyboard.Activity.RECORD_SOUND, - filename=fs_names.REC_CITY_DESC, - duration=60.0), - storyboard.Do(storyboard.Activity.PLAY_SOUND, - sound=fs_names.SFX_STOP_REC), - storyboard.Do(storyboard.Activity.LIGHT_BACK, - intensity=0., fade=1.), - storyboard.Do(storyboard.Activity.ADVANCE_UP) # Bild 16 - ), - storyboard.Chapter( - storyboard.Do(storyboard.Activity.PLAY_SOUND, - sound=fs_names.StoryFile('01dummy')), - storyboard.Do(storyboard.Activity.LIGHT_BACK, - intensity=1., fade=1.), - storyboard.Do(storyboard.Activity.PLAY_SOUND, - sound=fs_names.StoryFile('01dummy')), - storyboard.Do(storyboard.Activity.ADVANCE_UP) # Bild 17 - ), - storyboard.Chapter( - storyboard.Do(storyboard.Activity.PLAY_SOUND, - sound=fs_names.StoryFile('01dummy')), - storyboard.Do(storyboard.Activity.LIGHT_BACK, - intensity=0., fade=1.), - storyboard.Do(storyboard.Activity.WAIT_FOR_INPUT), - storyboard.Do(storyboard.Activity.PLAY_SOUND, - sound=fs_names.SFX_REC_AUDIO), - storyboard.Do(storyboard.Activity.RECORD_VIDEO, - filename=fs_names.REC_DRAW_CITY, - duration=60.0), - storyboard.Do(storyboard.Activity.PLAY_SOUND, - sound=fs_names.SFX_STOP_REC), - - storyboard.Do(storyboard.Activity.ADVANCE_UP) # Bild 18 - ), - storyboard.Chapter( - storyboard.Do(storyboard.Activity.LIGHT_BACK, - intensity=1., fade=1.), - storyboard.Do(storyboard.Activity.PLAY_SOUND, - sound=fs_names.StoryFile('01dummy')), - storyboard.Do(storyboard.Activity.ADVANCE_UP), # Bild 19 - storyboard.Do(storyboard.Activity.PLAY_SOUND, - sound=fs_names.StoryFile('01dummy')), - storyboard.Do(storyboard.Activity.LIGHT_BACK, - intensity=0., fade=2.) + Chapter( + Do(Activity.ADVANCE_LEFT, + steps=100), + Do(Activity.ADVANCE_UP, + steps=50), + Do(Activity.WAIT_FOR_INPUT, + on_blue=Select(Option.CONTINUE), + on_red=Select(Option.REPEAT), + on_yellow=Select(Option.GOTO, chapter=0), + on_green=Select(Option.QUIT)) ) ] diff --git a/pizzactrl/statemachine.py b/pizzactrl/statemachine.py index e71d69c..a7ae7c6 100644 --- a/pizzactrl/statemachine.py +++ b/pizzactrl/statemachine.py @@ -9,10 +9,11 @@ from enum import Enum, auto from subprocess import call from pizzactrl import fs_names, sb_dummy -from .storyboard import Activity -from .hal_serial import SerialCommunicationError, play_sound, take_photo, record_video, record_sound, turn_off, \ - PizzaHAL, init_camera, init_sounds, wait_for_input, \ - light_layer, backlight, move_vert, move_hor, rewind +from pizzactrl.hal import ScrollSensor +from .storyboard import Activity, Select, Option +from .hal_serial import SerialCommunicationError, PizzaHAL, play_sound, take_photo, record_video, record_sound, turn_off, wait_for_input, \ + light, move, rewind +from pizzactrl import storyboard logger = logging.getLogger(__name__) @@ -50,25 +51,47 @@ def load_sounds(): return soundcache +# Map Activities to function calls +ACTIVITY_SELECTOR = { + Activity.PLAY_SOUND: play_sound, + Activity.RECORD_SOUND: record_sound, + Activity.RECORD_VIDEO: record_video, + Activity.TAKE_PHOTO: take_photo, + Activity.LIGHT_LAYER: light, + Activity.LIGHT_BACK: light, + Activity.ADVANCE_UP: move, + Activity.ADVANCE_LEFT: move + } + + class Statemachine: def __init__(self, story_de: Any=None, story_en: Any=None, move: bool = False, - loop: bool = True): + loop: bool = True, + test: bool = False): self.state = State.POWER_ON self.hal = PizzaHAL() + + self.chapter = 0 + self.next_chapter = 0 + self.story = None self.story_de = story_de self.story_en = story_en - self.alt = False + self.lang = Language.NOT_SET - self.move = move - self.test = False + + self.MOVE = move # self.move is reset to this value + self.move = self.MOVE + + self.test = test self.loop = loop def run(self): logger.debug(f'Run(state={self.state})') + choice = { State.POWER_ON: self._power_on, State.POST: self._post, @@ -77,6 +100,7 @@ class Statemachine: State.REWIND: self._rewind, State.IDLE_END: self._idle_end } + while (self.state is not State.ERROR) and \ (self.state is not State.SHUTDOWN): choice[self.state]() @@ -107,40 +131,51 @@ class Statemachine: Initialize hal callbacks, load sounds """ logger.debug(f'power on') + + # TODO enable lid sensor # self.hal.lid_sensor.when_pressed = self._lid_open # self.hal.lid_sensor.when_released = self._lid_closed - try: - self.hal.init_connection() - except SerialCommunicationError as e: - self.state = State.ERROR - logger.exception(e) - return - init_sounds(self.hal, load_sounds()) - init_camera(self.hal) + + self.hal.init_sounds(load_sounds()) + self.hal.init_camera() + self.state = State.POST def _post(self): """ Power on self test. """ - logger.debug(f'post') - # check scroll positions and rewind if necessary - turn_off(self.hal) - if not os.path.exists(fs_names.USB_STICK): + logger.debug(f'post') + + if (not self.test) and (not os.path.exists(fs_names.USB_STICK)): logger.warning('USB-Stick not found.') self.state = State.ERROR return + # TODO set RPi_HELO pins, wait for response + # Callback for start when blue button is held - self.hal.btn_start.when_activated = self._start_or_rewind - logger.debug('start button callback activated') + # self.hal.btn_start.when_activated = self._start_or_rewind + # logger.debug('start button callback activated') + + try: + self.hal.init_connection() + except SerialCommunicationError as e: + self.state = State.ERROR + logger.exception(e) + return # play a sound if everything is alright play_sound(self.hal, fs_names.SFX_POST_OK) - self.state = State.IDLE_START - logger.debug('idle_start') + if self.test: + self.state = State.PLAY + logger.debug('play') + else: + self.state = State.IDLE_START + logger.debug('idle_start') + def _idle_start(self): """ @@ -148,24 +183,9 @@ class Statemachine: """ pass - def _start_or_rewind(self): - """ - Callback function. - - If statemachine is in idle state, start playback when start - button is pressed (released). - - If statemachine is playing, trigger rewind and start fresh - """ - if self.state == State.IDLE_START: - self.state = State.PLAY - return - if self.state == State.PLAY: - self.state = State.REWIND - def _play(self): """ - Run the storyboard + Select language, then run the storyboard """ logger.debug(f'play') if self.test: @@ -174,48 +194,134 @@ class Statemachine: # TODO reenable language selection self.story = self.story_en - for chapter in iter(self.story): - logger.debug(f'playing chapter {chapter}') + while self.chapter is not None: + self._play_chapter() + self._advance_chapter() + + self.state = State.REWIND + + def _option_callback(self, selection: Select): + """ + Return a callback for the appropriate option and parameters. + Callbacks set the properties of `Statemachine` to determine it's behaviour. + """ + rewind = selection.values.get('rewind', Option.REPEAT.value['rewind']) + next_chapter = selection.values.get('chapter', Option.GOTO.value['chapter']) + shutdown = selection.values.get('shutdown', Option.QUIT.value['shutdown']) + + def _continue(**kwargs): + """ + Continue in the Storyboard. Prepare advancing to the next chapter. + """ + self.move = self.MOVE + if len(self.story) > (self.chapter + 1): + self.next_chapter = self.chapter + 1 + else: + self.next_chapter = None + + def _repeat(**kwargs): + """ + Repeat the current chapter. Do not rewind if the selection says so. + """ + self.move = rewind + self.next_chapter = self.chapter + + def _goto(**kwargs): + """ + Jump to a specified chapter. + """ + self.move = self.MOVE + self.next_chapter = next_chapter + + def _quit(**kwargs): + self.move = self.MOVE + self.loop = not shutdown + self.next_chapter = None + + return { + Option.CONTINUE: _continue, + Option.REPEAT: _repeat, + Option.GOTO: _goto, + Option.QUIT: _quit, + None: None + }[selection.option] + + def _play_chapter(self): + """ + Play the chapter specified by self.chapter + """ + logger.debug(f'playing chapter {self.chapter}') + + if self.chapter < len(self.story): + chapter = self.story[self.chapter] + while chapter.hasnext(): act = next(chapter) logger.debug(f'next activity {act.activity}') if act.activity is Activity.WAIT_FOR_INPUT: wait_for_input(hal=self.hal, - go_callback=chapter.mobilize, - back_callback=chapter.rewind, - to_callback=self._start_or_rewind) - # elif act.activity is Activity.ADVANCE_UP: - # if chapter.move and self.move: - # logger.debug( - # f'advance({self.hal.motor_ud}, ' - # f'{self.hal.ud_sensor})') - # advance(motor=self.hal.motor_ud, - # sensor=self.hal.ud_sensor) - # elif not self.move: - # play_sound(self.hal, fs_names.StoryFile('stop')) + blue_cb = self._option_callback(act.values['on_blue']), + red_cb = self._option_callback(act.values['on_red']), + yellow_cb = self._option_callback(act.values['on_yellow']), + green_cb = self._option_callback(act.values['on_green']), + timeout_cb = self._option_callback(act.values['on_timeout']), + **act.values) else: try: - { - Activity.PLAY_SOUND: play_sound, - Activity.RECORD_SOUND: record_sound, - Activity.RECORD_VIDEO: record_video, - Activity.TAKE_PHOTO: take_photo, - Activity.LIGHT_LAYER: light_layer, - Activity.LIGHT_BACK: backlight, - # Activity.ADVANCE_UP: move_vert, - # Activity.ADVANCE_LEFT: move_hor - }[act.activity](self.hal, **act.values) + ACTIVITY_SELECTOR[act.activity](self.hal, **act.values) except KeyError: logger.exception('Caught KeyError, ignoring...') pass + + self.next_chapter = self.chapter + 1 + else: + self.next_chapter = None + + def _advance_chapter(self): + """ + Update chapters and move the scrolls. + Update self.chapter to self.next_chapter + """ + if self.next_chapter is not None: + diff = self.next_chapter - self.chapter + h_steps = 0 + v_steps = 0 + if diff < 0: + """ + Rewind all chapters up to target + """ + for ch in self.story[self.next_chapter:self.chapter]: + steps = ch.rewind() + h_steps += steps['h_steps'] + v_steps += steps['v_steps'] - self.state = State.REWIND + elif diff > 0: + """ + Skip all chapters up to target + """ + for ch in self.story[self.chapter:self.next_chapter]: + steps = ch.skip() + h_steps += steps['h_steps'] + v_steps += steps['v_steps'] + else: + """ + Rewind current chapter + """ + steps = self.story[self.chapter].rewind() + h_steps = steps['h_steps'] + v_steps = steps['v_steps'] + + if self.move: + move(self.hal, h_steps, True) + move(self.hal, v_steps, False) + + self.chapter = self.next_chapter def _rewind(self): """ Rewind all scrolls, post-process videos """ - # postprocessing + # TODO postprocessing - add sound logger.debug('Converting video...') cmdstring = f'MP4Box -add {fs_names.REC_DRAW_CITY} {fs_names.REC_MERGED_VIDEO}' call([cmdstring], shell=True) @@ -223,6 +329,7 @@ class Statemachine: logger.debug('Rewinding...') if self.move: rewind(self.hal) + for chapter in self.story: chapter.rewind() diff --git a/pizzactrl/storyboard.py b/pizzactrl/storyboard.py index c3fe930..a028aab 100644 --- a/pizzactrl/storyboard.py +++ b/pizzactrl/storyboard.py @@ -1,19 +1,70 @@ from enum import Enum, auto +class Option(Enum): + """ + Options can be chosen by the user in WAIT_FOR_INPUT + """ + CONTINUE = {} # Continue with chapter + REPEAT = {'rewind': True} # Repeat chapter from beginning. `rewind=True`: reset scrolls to starting position + GOTO = {'chapter': 0} # Jump to chapter number + QUIT = {'shutdown': True} # End playback. `shutdown=True` also powers off box + + +class Select: + """ + An option instance. Can override the default settings from `Option`s + """ + def __init__(self, option: Option, **kwargs): + self.option = option + self.values = {} + if option is not None: + for key, value in self.option.value.items(): + self.values[key] = kwargs.get(key, value) + + class Activity(Enum): - WAIT_FOR_INPUT = {'steps': 0} - PLAY_SOUND = {'sound': None} - RECORD_SOUND = {'duration': 0.0, 'filename': '', 'cache': False} - RECORD_VIDEO = {'duration': 0.0, 'filename': ''} - TAKE_PHOTO = {'filename': ''} - ADVANCE_UP = {'speed': 0.3, 'direction': True, 'steps': 100} - ADVANCE_LEFT = {'speed': 0.3, 'direction': True, 'steps': 200} # TODO set right number of steps - LIGHT_LAYER = {'intensity': 1.0, 'fade': 0.0, 'layer': True} - LIGHT_BACK = {'intensity': 1.0, 'fade': 0.0} + """ + Things the box can do + """ + WAIT_FOR_INPUT = {'on_blue': Select(Option.CONTINUE), + 'on_red': Select(Option.REPEAT), + 'on_yellow': Select(None), + 'on_green': Select(None), + 'on_timeout': Select(Option.QUIT), + 'sound': None, + 'timeout': 0} + PLAY_SOUND = {'sound': None} + RECORD_SOUND = {'duration': 10.0, + 'filename': '', + 'cache': False} + RECORD_VIDEO = {'duration': 60.0, + 'filename': ''} + TAKE_PHOTO = {'filename': ''} + ADVANCE_UP = {'steps': 100, # TODO set right number of steps + 'direction': True, + 'horizontal': False} + ADVANCE_LEFT = {'steps': 200, # TODO set right number of steps + 'direction': True, + 'horizontal': True} + LIGHT_LAYER = {'r': 0, + 'g': 0, + 'b': 0, + 'w': 1.0, + 'fade': 1.0, + 'backlight': False} + LIGHT_BACK = {'r': 0, + 'g': 0, + 'b': 0, + 'w': 1.0, + 'fade': 1.0, + 'backlight': True} class Do: + """ + An activity instance. Can override the default settings from `Activity`s + """ def __init__(self, activity: Activity, **kwargs): self.activity = activity self.values = {} @@ -23,33 +74,63 @@ class Do: class Chapter: """ - A logical storyboard entity, which can be replayed (rewind to start). + A logical storyboard entity, which can be replayed (rewind to start) + or skipped (do all movements at once). Keeps track of advanced steps on the scrolls. """ def __init__(self, *activities): self.activities = activities - self.pos = 0 - self.move = True - self.move_ud = 0 - + self.index = 0 + self.h_pos = 0 + self.v_pos = 0 + def __iter__(self): return self def __next__(self): - if self.pos >= len(self.activities): + if self.index >= len(self.activities): raise StopIteration - act = self.activities[self.pos] - self.pos += 1 + + act = self.activities[self.index] + self._update_pos(act) return act + def _update_pos(self, act: Activity): + """ + Update the positions from the activity. + Implicitly increments the index. + """ + self.index += 1 + if act.activity is Activity.ADVANCE_UP: + self.v_pos += act.values.get('steps', 0) + elif act.activity is Activity.ADVANCE_LEFT: + self.h_pos += act.values.get('steps', 0) + def hasnext(self): - return self.pos < len(self.activities) + """ + Returns True if the chapter has more activities + """ + return self.index < len(self.activities) def rewind(self, **kwargs): - self.move = False - self.pos = 0 + """ + Reset the position to zero. Return how many steps are needed to rewind the scrolls + """ + self.index = 0 + h_pos = self.h_pos + v_pos = self.v_pos + self.h_pos = 0 + self.v_pos = 0 + return {'h_steps': -h_pos, 'v_steps': -v_pos} - def mobilize(self, **kwargs): - self.move = True + def skip(self, **kwargs): + """ + Skip chapter. Returns all movements necessary to advance scrolls. + """ + h_pos = self.h_pos + v_pos = self.v_pos + for act in self.activities[self.index:]: + self._update_pos(act) + return {'h_steps': self.h_pos - h_pos, 'v_steps': self.v_pos - v_pos}