diff --git a/pizzactrl/fs_names.py b/pizzactrl/fs_names.py index 1eb8ab7..f8176be 100644 --- a/pizzactrl/fs_names.py +++ b/pizzactrl/fs_names.py @@ -98,4 +98,4 @@ SFX_SHUTTER = SfxFile('done') SFX_REC_AUDIO = SfxFile('countdown') SFX_STOP_REC = SfxFile('done') -SND_SELECT_LANG = StoryFile(name='IC-SIBI-00') +SND_SELECT_LANG = SfxFile('lang-select') diff --git a/pizzactrl/hal_serial.py b/pizzactrl/hal_serial.py index 0234e4b..9627949 100644 --- a/pizzactrl/hal_serial.py +++ b/pizzactrl/hal_serial.py @@ -118,18 +118,18 @@ class PizzaHAL: :param hal: :param sounds: A list of sound files """ - if self.soundcache is None: - self.soundcache = {} + # 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(str(sound)) + # 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(str(sound)) def init_camera(self): if self.camera is None: @@ -192,7 +192,6 @@ def turn_off(hal: PizzaHAL, **kwargs): """ Turn off the lights. """ - set_light(hal, Lights.BACKLIGHT, 0, 0, 0, 0, 0) set_light(hal, Lights.FRONTLIGHT, 0, 0, 0, 0, 0) do_it(hal) @@ -232,6 +231,7 @@ def wait_for_input(hal: PizzaHAL, (8 if green_cb else 0) if sound is not None: + logger.debug(f'Waiting for user, playing 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)) diff --git a/pizzactrl/sb_de.py b/pizzactrl/sb_de.py index e35f438..c896b5a 100644 --- a/pizzactrl/sb_de.py +++ b/pizzactrl/sb_de.py @@ -112,7 +112,7 @@ STORYBOARD = [ storyboard.Do(storyboard.Activity.ADVANCE_UP) # Bild 9 ), storyboard.Chapter( # X9 - storyboard.Do(storyboard.Activity.LIGHT_LAYER, + storyboard.Do(storyboard.Activity.LIGHT_FRONT, intensity=1., fade=0.5), storyboard.Do(storyboard.Activity.LIGHT_BACK, intensity=1., fade=0.5), @@ -122,7 +122,7 @@ STORYBOARD = [ sound=fs_names.StoryFile('23de')), storyboard.Do(storyboard.Activity.PLAY_SOUND, sound=fs_names.StoryFile('24de')), - storyboard.Do(storyboard.Activity.LIGHT_LAYER, + storyboard.Do(storyboard.Activity.LIGHT_FRONT, intensity=0., fade=1.), storyboard.Do(storyboard.Activity.LIGHT_BACK, intensity=0., fade=1.), diff --git a/pizzactrl/sb_dummy.py b/pizzactrl/sb_dummy.py index 7c37871..d8b60a4 100644 --- a/pizzactrl/sb_dummy.py +++ b/pizzactrl/sb_dummy.py @@ -1,42 +1,47 @@ from pizzactrl import fs_names from pizzactrl.storyboard import * -STORYBOARD = [ - Chapter( - Do(Activity.ADVANCE_UP, - steps=50), - Do(Activity.LIGHT_BACK, # Bild 1 - intensity=1.0, fade=1.0), +STORYBOARD = Storyboard( + Chapter( # Chapter 0 + Do(Activity.PARALLEL, + activities = [ + Do(Activity.ADVANCE_UP, steps=30), + Do(Activity.LIGHT_BACK, r=0, g=0, b=0, w=1.0, fade=1.0), + ]), Do(Activity.WAIT_FOR_INPUT, on_blue=Select(Option.CONTINUE), - on_red=Select(Option.REPEAT)) + on_red=Select(Option.REPEAT)), + Do(Activity.LIGHT_BACK) # Fade out ), - Chapter( - Do(Activity.ADVANCE_LEFT, - steps=100), - Do(Activity.ADVANCE_UP, - steps=50), + Chapter( # Chapter 1 + Do(Activity.PARALLEL, + activities = [ + Do(Activity.LIGHT_FRONT, r=1.0, fade=2.0), + Do(Activity.ADVANCE_LEFT, steps=50), + Do(Activity.ADVANCE_UP, steps=25) + ]), 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)) + on_green=Select(Option.QUIT)), + Do(Activity.LIGHT_FRONT) # Fade out ), - Chapter( - Do(Activity.ADVANCE_LEFT, - steps=-50), - Do(Activity.ADVANCE_UP, - steps=-20) + Chapter( # Chapter 2 + Do(Activity.LIGHT_BACK, b=1., fade=2.0), + Do(Activity.ADVANCE_LEFT, steps=-50), + Do(Activity.ADVANCE_UP, steps=-20), + Do(Activity.LIGHT_BACK) ), - Chapter( - Do(Activity.ADVANCE_LEFT, - steps=100), - Do(Activity.ADVANCE_UP, - steps=50), + Chapter( # Chapter 3 + Do(Activity.LIGHT_FRONT, r=1., g=1., fade=2.0), + Do(Activity.ADVANCE_LEFT, steps=50), + Do(Activity.ADVANCE_UP, steps=50), + Do(Activity.LIGHT_FRONT, fade=5.0), 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/sb_en.py b/pizzactrl/sb_en.py index 2a9986d..18e9a1e 100644 --- a/pizzactrl/sb_en.py +++ b/pizzactrl/sb_en.py @@ -2,7 +2,7 @@ from pizzactrl import storyboard, fs_names STORYBOARD = [ storyboard.Chapter( - storyboard.Do(storyboard.Activity.LIGHT_LAYER, # VERT01 + storyboard.Do(storyboard.Activity.LIGHT_FRONT, # VERT01 intensity=1.0, fade=1.0) ), storyboard.Chapter( @@ -19,7 +19,7 @@ STORYBOARD = [ # storyboard.Do(storyboard.Activity.ADVANCE_UP, steps=90), # VERT02down storyboard.Do(storyboard.Activity.LIGHT_BACK, intensity=1.0, fade=0.5), - storyboard.Do(storyboard.Activity.LIGHT_LAYER, + storyboard.Do(storyboard.Activity.LIGHT_FRONT, intensity=0.0, fade=0.5), storyboard.Do(storyboard.Activity.PLAY_SOUND, sound=fs_names.StoryFile('IC-SIBI-05')), @@ -32,7 +32,7 @@ STORYBOARD = [ sound=fs_names.StoryFile('IC-SIBI-07')), storyboard.Do(storyboard.Activity.WAIT_FOR_INPUT), # storyboard.Do(storyboard.Activity.ADVANCE_UP), # VERT04 - storyboard.Do(storyboard.Activity.LIGHT_LAYER, + storyboard.Do(storyboard.Activity.LIGHT_FRONT, intensity=1.0, fade=1.0), ), storyboard.Chapter( @@ -41,7 +41,7 @@ STORYBOARD = [ # storyboard.Do(storyboard.Activity.ADVANCE_LEFT), # HOR02 storyboard.Do(storyboard.Activity.LIGHT_BACK, intensity=1.0, fade=0.5), - storyboard.Do(storyboard.Activity.LIGHT_LAYER, + storyboard.Do(storyboard.Activity.LIGHT_FRONT, intensity=0., fade=0.5), storyboard.Do(storyboard.Activity.PLAY_SOUND, sound=fs_names.StoryFile('IC-SIBI-09')), @@ -71,14 +71,14 @@ STORYBOARD = [ storyboard.Do(storyboard.Activity.PLAY_SOUND, sound=fs_names.StoryFile('IC-SIBI-13')), # storyboard.Do(storyboard.Activity.ADVANCE_UP), # VERT05 - storyboard.Do(storyboard.Activity.LIGHT_LAYER, + storyboard.Do(storyboard.Activity.LIGHT_FRONT, intensity=1.0, fade=1.0), storyboard.Do(storyboard.Activity.PLAY_SOUND, sound=fs_names.StoryFile('IC-SIBI-14')), # storyboard.Do(storyboard.Activity.ADVANCE_LEFT), # HOR03 storyboard.Do(storyboard.Activity.LIGHT_BACK, intensity=1.0, fade=0.5), - storyboard.Do(storyboard.Activity.LIGHT_LAYER, + storyboard.Do(storyboard.Activity.LIGHT_FRONT, intensity=0., fade=0.5), storyboard.Do(storyboard.Activity.PLAY_SOUND, sound=fs_names.StoryFile('IC-SIBI-15')), @@ -91,11 +91,11 @@ STORYBOARD = [ storyboard.Chapter( storyboard.Do(storyboard.Activity.PLAY_SOUND, sound=fs_names.StoryFile('IC-SIBI-17')), - storyboard.Do(storyboard.Activity.LIGHT_LAYER, + storyboard.Do(storyboard.Activity.LIGHT_FRONT, intensity=1.0, fade=1.0), storyboard.Do(storyboard.Activity.PLAY_SOUND, sound=fs_names.StoryFile('IC-SIBI-18')), - storyboard.Do(storyboard.Activity.LIGHT_LAYER, + storyboard.Do(storyboard.Activity.LIGHT_FRONT, intensity=0.0, fade=1.0), storyboard.Do(storyboard.Activity.PLAY_SOUND, sound=fs_names.StoryFile('IC-SIBI-19')), @@ -104,11 +104,11 @@ STORYBOARD = [ storyboard.Chapter( storyboard.Do(storyboard.Activity.PLAY_SOUND, sound=fs_names.StoryFile('IC-SIBI-20')), - storyboard.Do(storyboard.Activity.LIGHT_LAYER, + storyboard.Do(storyboard.Activity.LIGHT_FRONT, intensity=1.0, fade=1.0), storyboard.Do(storyboard.Activity.PLAY_SOUND, sound=fs_names.StoryFile('IC-SIBI-21')), - storyboard.Do(storyboard.Activity.LIGHT_LAYER, + storyboard.Do(storyboard.Activity.LIGHT_FRONT, intensity=0.0, fade=1.0), storyboard.Do(storyboard.Activity.PLAY_SOUND, sound=fs_names.StoryFile('IC-SIBI-22')), @@ -117,16 +117,16 @@ STORYBOARD = [ storyboard.Chapter( storyboard.Do(storyboard.Activity.PLAY_SOUND, sound=fs_names.StoryFile('IC-SIBI-23')), - storyboard.Do(storyboard.Activity.LIGHT_LAYER, + storyboard.Do(storyboard.Activity.LIGHT_FRONT, intensity=1.0, fade=1.0), storyboard.Do(storyboard.Activity.PLAY_SOUND, sound=fs_names.StoryFile('IC-SIBI-24')), - storyboard.Do(storyboard.Activity.LIGHT_LAYER, + storyboard.Do(storyboard.Activity.LIGHT_FRONT, intensity=0.0, fade=1.0), storyboard.Do(storyboard.Activity.PLAY_SOUND, sound=fs_names.StoryFile('IC-SIBI-25')), storyboard.Do(storyboard.Activity.WAIT_FOR_INPUT), - storyboard.Do(storyboard.Activity.LIGHT_LAYER, + storyboard.Do(storyboard.Activity.LIGHT_FRONT, intensity=1.0, fade=1.0), ), storyboard.Chapter( @@ -143,7 +143,7 @@ STORYBOARD = [ cache=False), storyboard.Do(storyboard.Activity.PLAY_SOUND, sound=fs_names.SFX_STOP_REC), - storyboard.Do(storyboard.Activity.LIGHT_LAYER, + storyboard.Do(storyboard.Activity.LIGHT_FRONT, intensity=0.0, fade=0.5), storyboard.Do(storyboard.Activity.LIGHT_BACK, intensity=1.0, fade=0.5), diff --git a/pizzactrl/sounds/lang-select.wav b/pizzactrl/sounds/lang-select.wav new file mode 100644 index 0000000..2a6ddab Binary files /dev/null and b/pizzactrl/sounds/lang-select.wav differ diff --git a/pizzactrl/statemachine.py b/pizzactrl/statemachine.py index d8dacc0..4fc4746 100644 --- a/pizzactrl/statemachine.py +++ b/pizzactrl/statemachine.py @@ -8,11 +8,10 @@ from time import sleep from enum import Enum, auto from subprocess import call -from pizzactrl import fs_names, sb_dummy +from pizzactrl import SOUNDS_PATH, fs_names 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, \ - set_light, set_movement, rewind +from .storyboard import Language, Activity, Select, Option, Storyboard +from .hal_serial import SerialCommunicationError, PizzaHAL, wait_for_input, play_sound, turn_off from pizzactrl import storyboard logger = logging.getLogger(__name__) @@ -22,88 +21,54 @@ class State(Enum): POWER_ON = auto() POST = auto() IDLE_START = auto() + LANGUAGE_SELECT = auto() PLAY = auto() PAUSE = auto() + POST_PROCESS = auto() REWIND = auto() IDLE_END = auto() SHUTDOWN = auto() ERROR = -1 -class Language(Enum): - NOT_SET = auto() - DE = auto() - EN = auto() - - -def load_sounds(): - """ - Load all prerecorded Sounds from the cache - - :returns a list of sound file names - """ - soundcache = [ - fs_names.SFX_SHUTTER, - fs_names.SFX_ERROR, - fs_names.SFX_POST_OK, - fs_names.SND_SELECT_LANG - ] - 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: set_light, - Activity.LIGHT_BACK: set_light, - Activity.ADVANCE_UP: set_movement, - Activity.ADVANCE_LEFT: set_movement - } - - class Statemachine: def __init__(self, - story_de: Any=None, - story_en: Any=None, + hal: PizzaHAL, + story: Storyboard, + default_lang=Language.NOT_SET, move: bool = False, loop: bool = True, test: bool = False): - self.state = State.POWER_ON - self.hal = PizzaHAL() + self.hal = hal - self.chapter = 0 # The storyboard index of the current chapter to play - self.next_chapter = 0 # The storyboard index of the next chapter to play - self.chapter_set = False # `True` if the next chapter has been set + self.LANG = default_lang + self.lang = None + + self.story = story + self.story.MOVE = move - self.story = None - self.story_de = story_de - self.story_en = story_en - - self.lang = Language.NOT_SET - - self.MOVE = move # self.move is reset to this value - self.move = self.MOVE - self.test = test self.loop = loop + + self.state = State.POWER_ON def run(self): - logger.debug(f'Run(state={self.state})') + logger.debug(f'Starting Statemachine...') choice = { State.POWER_ON: self._power_on, State.POST: self._post, State.IDLE_START: self._idle_start, + State.LANGUAGE_SELECT: self._lang_select, State.PLAY: self._play, + State.POST_PROCESS: self._post_process, State.REWIND: self._rewind, State.IDLE_END: self._idle_end } while (self.state is not State.ERROR) and \ (self.state is not State.SHUTDOWN): + logger.debug(f'Run(state={self.state})') choice[self.state]() if self.state is State.ERROR: @@ -117,27 +82,15 @@ class Statemachine: self._shutdown() - def _lang_de(self, **kwargs): - logger.info(f'select language german') - self.lang = Language.DE - self.story = self.story_de - - def _lang_en(self, **kwargs): - logger.info(f'select language english') - self.lang = Language.EN - self.story = self.story_en - def _power_on(self): """ 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 - self.hal.init_sounds(load_sounds()) + self.hal.init_sounds() self.hal.init_camera() self.state = State.POST @@ -146,9 +99,6 @@ class Statemachine: """ Power on self test. """ - - 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 @@ -171,11 +121,9 @@ class Statemachine: play_sound(self.hal, fs_names.SFX_POST_OK) if self.test: - self.state = State.PLAY - logger.debug('play') + self.state = State.LANGUAGE_SELECT else: self.state = State.IDLE_START - logger.debug('idle_start') def _idle_start(self): @@ -184,164 +132,62 @@ class Statemachine: """ pass + def _lang_select(self): + """ + Select language + """ + def _select_de(): + self.lang = Language.DE + + def _select_en(): + self.lang = Language.EN + + def _select_default(): + self.lang = self.LANG + + #sound = self.hal.soundcache.get(fs_names.SND_SELECT_LANG) + #logger.debug(f'got sound {sound} from soundcache.') + + wait_for_input(self.hal, + blue_cb=_select_de, + red_cb=_select_en, + sound=fs_names.SND_SELECT_LANG, + timeout_cb=_select_default) + + self.story.language = self.lang + + logger.debug(f'User selected language={self.lang}') + self.state = State.PLAY + def _play(self): """ Select language, then run the storyboard """ - logger.debug(f'play') - if self.test: - self.story = sb_dummy.STORYBOARD - else: - # TODO reenable language selection - self.story = self.story_en + self.story.hal = self.hal - while self.chapter is not None: - self._play_chapter() - self._advance_chapter() + while self.story.hasnext(): + self.story.play_chapter() + self.story.advance_chapter() - self.state = State.REWIND + self.state = State.POST_PROCESS - def _option_callback(self, selection: Select): + def _post_process(self): """ - 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 - self.chapter_set = True - 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.chapter_set = True - self.move = rewind - self.next_chapter = self.chapter - - def _goto(**kwargs): - """ - Jump to a specified chapter. - """ - self.chapter_set = True - self.move = self.MOVE - self.next_chapter = next_chapter - - def _quit(**kwargs): - self.chapter_set = True - 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, - 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_SELECTOR[act.activity](self.hal, **act.values) - except KeyError: - logger.exception('Caught KeyError, ignoring...') - pass - - if not self.chapter_set: - self.chapter_set = True - 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.chapter_set and (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'] - - 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: - set_movement(self.hal, h_steps, True) - set_movement(self.hal, v_steps, False) - - logger.debug(f'Setting chapter (cur: {self.chapter}) to {self.next_chapter}.') - self.chapter = self.next_chapter - self.chapter_set = False - - def _rewind(self): - """ - Rewind all scrolls, post-process videos + Post-processing """ # 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) - - logger.debug('Rewinding...') - if self.move: - rewind(self.hal) - for chapter in self.story: - chapter.rewind() + self.state = State.REWIND + + def _rewind(self): + """ + Rewind all scrolls, post-process videos + """ + turn_off(self.hal) + self.story.rewind() if self.loop: self.state = State.IDLE_START @@ -358,8 +204,4 @@ class Statemachine: """ Clean up, end execution """ - logger.debug('shutdown') - - turn_off(self.hal) - del self.hal diff --git a/pizzactrl/statemachine_test.py b/pizzactrl/statemachine_test.py index 3304a57..4fe8db1 100644 --- a/pizzactrl/statemachine_test.py +++ b/pizzactrl/statemachine_test.py @@ -4,6 +4,9 @@ import logging logging.basicConfig(level=logging.DEBUG, stream=sys.stdout) from pizzactrl.statemachine import Statemachine +from pizzactrl.sb_dummy import STORYBOARD +from pizzactrl.hal_serial import PizzaHAL, rewind, turn_off -sm = Statemachine(move=True, loop=False, test=True) +hal = PizzaHAL() +sm = Statemachine(hal, STORYBOARD, move=True, loop=False, test=True) diff --git a/pizzactrl/storyboard.py b/pizzactrl/storyboard.py index 2d206e0..e934b9f 100644 --- a/pizzactrl/storyboard.py +++ b/pizzactrl/storyboard.py @@ -1,7 +1,24 @@ +import logging from enum import Enum, auto -from typing import List +from threading import active_count +from typing import List, Any -from pizzactrl.hal_serial import SerialCommands +from pizzactrl.hal_serial import Lights, Scrolls, SerialCommands, PizzaHAL, \ + do_it, play_sound, take_photo, record_video, \ + record_sound, turn_off, wait_for_input, \ + set_light, set_movement, rewind + +logger = logging.getLogger(__name__) + + +class ConfigurationException(Exception): + pass + + +class Language(Enum): + NOT_SET = 'NA' + DE = 'DE' + EN = 'EN' class Option(Enum): @@ -11,7 +28,7 @@ class Option(Enum): 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 + QUIT = {'quit': True} # End playback. class Select: @@ -35,33 +52,37 @@ class Activity(Enum): 'on_yellow': Select(None), 'on_green': Select(None), 'on_timeout': Select(Option.QUIT), - 'sound': None, + Language.NOT_SET.value: None, + Language.DE.value: None, + Language.EN.value: None, 'timeout': 0} - PLAY_SOUND = {'sound': None} + PLAY_SOUND = {Language.NOT_SET.value: None, + Language.DE.value: None, + Language.EN.value: 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, + ADVANCE_UP = {'steps': 100, + 'scroll': Scrolls.VERTICAL} + ADVANCE_LEFT = {'steps': 200, + 'scroll': Scrolls.HORIZONTAL} + LIGHT_FRONT = {'r': 0, 'g': 0, 'b': 0, - 'w': 1.0, + 'w': 0, 'fade': 1.0, - 'light': [ SerialCommands.FRONTLIGHT ]} + 'light': Lights.FRONTLIGHT} LIGHT_BACK = {'r': 0, 'g': 0, 'b': 0, - 'w': 1.0, + 'w': 0, 'fade': 1.0, - 'light': [ SerialCommands.BACKLIGHT ]} + 'light': Lights.BACKLIGHT} + PARALLEL = {'activities': []} + GOTO = {'index': 0} class Do: @@ -74,21 +95,29 @@ class Do: for key, value in self.activity.value.items(): self.values[key] = kwargs.get(key, value) - def execute(): - # TODO implement - pass + def __repr__(self) -> str: + return f'{self.activity.name}({self.values})' + def __str__(self) -> str: + return f'{self.activity.name}({self.values})' -class Do_Parallel: - """ - A list of activities which should be done at the same time - """ - def __init__(self, *activities: List[Do]) -> None: - self.act_list = activities - - def execute(): - for act in self.act_list: - act.execute() + def get_steps(self): + """ + Returns the number of steps this activity makes. + returns: h_steps, v_steps + """ + h_steps = 0 + v_steps = 0 + if self.activity is Activity.ADVANCE_UP: + v_steps += self.values['steps'] + elif self.activity is Activity.ADVANCE_LEFT: + h_steps += self.values['steps'] + elif self.activity is Activity.PARALLEL: + for act in self.values['activities']: + h, v = act.get_steps() + h_steps += h + v_steps += v + return h_steps, v_steps class Chapter: @@ -115,16 +144,15 @@ class Chapter: self._update_pos(act) return act - def _update_pos(self, act: Activity): + def _update_pos(self, act: Do): """ 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) + h, v = act.get_steps() + self.h_pos += h + self.v_pos += v def hasnext(self): """ @@ -153,3 +181,262 @@ class Chapter: self._update_pos(act) return {'h_steps': self.h_pos - h_pos, 'v_steps': self.v_pos - v_pos} + +def _get_sound(language, **kwargs): + """ + Select the right sound depending on the language + """ + sound = kwargs.get(language, kwargs.get(Language.NOT_SET.value, None)) + if sound is None: + logger.debug(f'_get_sound(language={language}) Could not find sound, returning None.') + + return sound + + +class Storyboard: + def __init__(self, *story: List[Do]) -> None: + self.story = story + self.hal = None + + self._index = 0 # The storyboard index of the current chapter to play + self._next_chapter = 0 # The storyboard index of the next chapter to play + self._chapter_set = False # `True` if the next chapter has been set + + self.MOVE = False # self.move is reset to this value + self._move = self.MOVE + + self._lang = Language.NOT_SET + + self.ACTIVITY_SELECTOR = None + + @property + def move(self) -> bool: + return self._move + + @move.setter + def move(self, move: bool): + if move is None: + self._move = self.MOVE + else: + self._move = move + + @property + def language(self) -> Language: + return self._lang + + @language.setter + def language(self, language: Language): + self._lang = language + + @property + def next_chapter(self): + return self._next_chapter + + @next_chapter.setter + def next_chapter(self, next_chapter): + self._chapter_set = True + self.move = None # Reset to default value + self._next_chapter = next_chapter + + def hasnext(self): + return self._index is not None + + 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. + """ + logger.debug('User selected continue') + if len(self.story) > (self._index + 1): + self.next_chapter = self._index + 1 + else: + self.next_chapter = None + + def _repeat(rewind: bool=None, **kwargs): + """ + Repeat the current chapter. Do not rewind if the selection says so. + """ + logger.debug('User selected repeat') + self.next_chapter = self._index + self.move = rewind + + def _goto(next_chapter: int=None, **kwargs): + """ + Jump to a specified chapter. + """ + logger.debug(f'User selected goto {next_chapter}') + self.next_chapter = next_chapter + + def _quit(**kwargs): + logger.debug('User selected quit') + 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._index}') + + if self.hal is None: + raise ConfigurationException('Set Storyboard.hal before calling Storyboard.play_chapter()') + + if self._index is None: + # Reached end of story + return + + def _play_sound(hal, **kwargs): + """ + Handle Activity.PLAY_SOUND + """ + logger.debug(f'Storyboard._play_sound({kwargs})') + play_sound(hal, sound=_get_sound(language=self.language, **kwargs), **kwargs) + + def _wait_for_input(hal, sound=None, **kwargs): + """ + Handle Activity.WAIT_FOR_INPUT + """ + logger.debug(f'Storyboard._wait_for_input({kwargs})') + wait_for_input(hal=hal, + blue_cb = self._option_callback(kwargs['on_blue']), + red_cb = self._option_callback(kwargs['on_red']), + yellow_cb = self._option_callback(kwargs['on_yellow']), + green_cb = self._option_callback(kwargs['on_green']), + timeout_cb = self._option_callback(kwargs['on_timeout']), + sound = _get_sound(language=self.language, **kwargs), + **kwargs) + + def _parallel(hal, activities: List[Do], **kwargs): + """ + Handle Activity.PARALLEL + """ + logger.debug(f'Storyboard._parallel({activities})') + for paract in activities: + self.ACTIVITY_SELECTOR[paract.activity](hal, do_now=False, **paract.values) + + do_it(self.hal) + + def _move(hal, do_now=True, **kwargs): + logger.debug(f'Storyboard._move({kwargs})') + set_movement(hal, **kwargs) + if do_now: + do_it(hal) + + def _light(hal, do_now=True, **kwargs): + logger.debug(f'Storyboard._light({kwargs})') + set_light(hal, **kwargs) + if do_now: + do_it(hal) + + def _goto(hal, index:int, **kwargs): + """ + Set the next chapter + """ + logger.debug(f'Storyboard._goto({kwargs})') + self.next_chapter = index + + self.ACTIVITY_SELECTOR = { + Activity.PLAY_SOUND: _play_sound, + Activity.WAIT_FOR_INPUT: _wait_for_input, + Activity.PARALLEL: _parallel, + Activity.GOTO: _goto, + Activity.RECORD_SOUND: record_sound, + Activity.RECORD_VIDEO: record_video, + Activity.TAKE_PHOTO: take_photo, + Activity.LIGHT_FRONT: _light, + Activity.LIGHT_BACK: _light, + Activity.ADVANCE_UP: _move, + Activity.ADVANCE_LEFT: _move, + } + + if self._index < len(self.story): + chapter = self.story[self._index] + + while chapter.hasnext(): + act = next(chapter) + logger.debug(f'next activity {act.activity}') + try: + self.ACTIVITY_SELECTOR[act.activity](self.hal, **act.values) + except KeyError as e: + raise ConfigurationException('Missing handler for {act.activity}', e) + + if not self._chapter_set: + self._chapter_set = True + self._next_chapter = self._index + 1 + + else: + self._next_chapter = None + + def advance_chapter(self): + """ + Update chapters and move the scrolls. + Update self.chapter to self.next_chapter + """ + if not self._chapter_set: + return + elif self._index is None: + return + elif self._next_chapter is not None: + diff = self._next_chapter - self._index + h_steps = 0 + v_steps = 0 + if diff < 0: + """ + Rewind all chapters up to target + """ + for ch in self.story[self._next_chapter:self._index]: + steps = ch.rewind() + h_steps += steps['h_steps'] + v_steps += steps['v_steps'] + + elif diff > 0: + """ + Skip all chapters up to target + """ + for ch in self.story[self._index:self._next_chapter]: + steps = ch.skip() + h_steps += steps['h_steps'] + v_steps += steps['v_steps'] + else: + """ + Rewind current chapter + """ + steps = self.story[self._index].rewind() + h_steps = steps['h_steps'] + v_steps = steps['v_steps'] + + if self.move: + set_movement(self.hal, scroll=Scrolls.HORIZONTAL, steps=h_steps) + set_movement(self.hal, scroll=Scrolls.VERTICAL, steps=v_steps) + do_it(self.hal) + + logger.debug(f'Setting chapter (cur: {self._index}) to {self._next_chapter}.') + self._index = self._next_chapter + self._chapter_set = False + + def rewind(self): + if self.hal is None: + raise ConfigurationException('Set Storyboard.hal before calling Storyboard.rewind()') + + if self.move: + rewind(self.hal) + + for chapter in self.story: + chapter.rewind() + + self._index = self._next_chapter = 0