Major refactoring:

- New Storyboard class now has execution logic included
- Implemented dummy storyboard for testing
- Language selection is a state in Statemachine
- Added test class for instantiating a Statemachine
This commit is contained in:
jpunkt 2022-01-17 20:39:36 +01:00
parent a64fd6460a
commit d837134db5
9 changed files with 444 additions and 307 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

Binary file not shown.

View file

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

View file

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

View file

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