Updated storyboard mechanics

Updated statemachine
Implemented advanced choice mechanics
Still testing.
This commit is contained in:
jpunkt 2022-01-14 22:47:19 +01:00
parent 2c85f7ed46
commit 06cbde4179
4 changed files with 333 additions and 368 deletions

View file

@ -120,7 +120,7 @@ class PizzaHAL:
# Extract data and sampling rate from file # Extract data and sampling rate from file
# data, fs = sf.read(str(sound), dtype='float32') # data, fs = sf.read(str(sound), dtype='float32')
# self.soundcache[str(sound)] = (data, fs) # 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): def init_camera(self):
if self.camera is None: if self.camera is None:
@ -157,23 +157,19 @@ class PizzaHAL:
return resp 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. 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): def rewind(hal: PizzaHAL, **kwargs):
"""
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):
""" """
Rewind both scrolls. Rewind both scrolls.
@ -181,9 +177,9 @@ def rewind(hal: PizzaHAL):
hal.send_cmd(SerialCommands.REWIND) 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.BACKLIGHT, 0)
hal.send_cmd(SerialCommands.FRONTLIGHT, 0) hal.send_cmd(SerialCommands.FRONTLIGHT, 0)
@ -223,7 +219,7 @@ def wait_for_input(hal: PizzaHAL,
(8 if green_cb else 0) (8 if green_cb else 0)
if sound is not None: 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)) 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) 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 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 :param steps: int
How many steps for the fade (default: 100) How many steps for the fade (default: 100)
""" """
hal.send_cmd(SerialCommands.FRONTLIGHT, hal.send_cmd(SerialCommands.BACKLIGHT if backlight else SerialCommands.FRONTLIGHT,
int(r * 255).to_bytes(1, 'little'),
int(g * 255).to_bytes(1, 'little'),
int(b * 255).to_bytes(1, 'little'), int(b * 255).to_bytes(1, 'little'),
int(w * 255).to_bytes(1, 'little'),
int(fade * 1000).to_bytes(4, 'little'))
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(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(w * 255).to_bytes(1, 'little'),
int(fade * 1000).to_bytes(4, '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 # Extract data and sampling rate from file
try: try:
hal.play_sound(sound) hal.play_sound(str(sound))
while mx.get_busy(): while mx.get_busy():
pass pass
except KeyboardInterrupt: except KeyboardInterrupt:

View file

@ -1,248 +1,42 @@
from pizzactrl import storyboard, fs_names from pizzactrl import fs_names
from pizzactrl.storyboard import *
STORYBOARD = [ STORYBOARD = [
storyboard.Chapter( Chapter(
# storyboard.Do(storyboard.Activity.ADVANCE_UP), Do(Activity.ADVANCE_UP,
storyboard.Do(storyboard.Activity.LIGHT_BACK, # Bild 1 steps=50),
intensity=1.0, fade=1.0) 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( Chapter(
storyboard.Do(storyboard.Activity.PLAY_SOUND, Do(Activity.ADVANCE_LEFT,
sound=fs_names.StoryFile('01dummy')), steps=100),
storyboard.Do(storyboard.Activity.WAIT_FOR_INPUT), Do(Activity.ADVANCE_UP,
storyboard.Do(storyboard.Activity.ADVANCE_UP) # Bild 2 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( Chapter(
storyboard.Do(storyboard.Activity.PLAY_SOUND, Do(Activity.ADVANCE_LEFT,
sound=fs_names.StoryFile('01dummy')), steps=-50),
storyboard.Do(storyboard.Activity.ADVANCE_UP), # Bild 3 Do(Activity.ADVANCE_UP,
storyboard.Do(storyboard.Activity.PLAY_SOUND, steps=-20)
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
), ),
storyboard.Chapter( Chapter(
storyboard.Do(storyboard.Activity.LIGHT_BACK, Do(Activity.ADVANCE_LEFT,
intensity=1.0), steps=100),
storyboard.Do(storyboard.Activity.PLAY_SOUND, Do(Activity.ADVANCE_UP,
sound=fs_names.StoryFile('01dummy')), steps=50),
storyboard.Do(storyboard.Activity.PLAY_SOUND, Do(Activity.WAIT_FOR_INPUT,
sound=fs_names.StoryFile('01dummy')), on_blue=Select(Option.CONTINUE),
storyboard.Do(storyboard.Activity.PLAY_SOUND, on_red=Select(Option.REPEAT),
sound=fs_names.SFX_REC_AUDIO), on_yellow=Select(Option.GOTO, chapter=0),
storyboard.Do(storyboard.Activity.PLAY_SOUND, on_green=Select(Option.QUIT))
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.)
) )
] ]

View file

@ -9,10 +9,11 @@ from enum import Enum, auto
from subprocess import call from subprocess import call
from pizzactrl import fs_names, sb_dummy from pizzactrl import fs_names, sb_dummy
from .storyboard import Activity from pizzactrl.hal import ScrollSensor
from .hal_serial import SerialCommunicationError, play_sound, take_photo, record_video, record_sound, turn_off, \ from .storyboard import Activity, Select, Option
PizzaHAL, init_camera, init_sounds, wait_for_input, \ from .hal_serial import SerialCommunicationError, PizzaHAL, play_sound, take_photo, record_video, record_sound, turn_off, wait_for_input, \
light_layer, backlight, move_vert, move_hor, rewind light, move, rewind
from pizzactrl import storyboard
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -50,25 +51,47 @@ def load_sounds():
return soundcache 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: 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): loop: bool = True,
test: bool = False):
self.state = State.POWER_ON self.state = State.POWER_ON
self.hal = PizzaHAL() self.hal = PizzaHAL()
self.chapter = 0
self.next_chapter = 0
self.story = None self.story = None
self.story_de = story_de self.story_de = story_de
self.story_en = story_en self.story_en = story_en
self.alt = False
self.lang = Language.NOT_SET 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 self.loop = loop
def run(self): def run(self):
logger.debug(f'Run(state={self.state})') logger.debug(f'Run(state={self.state})')
choice = { choice = {
State.POWER_ON: self._power_on, State.POWER_ON: self._power_on,
State.POST: self._post, State.POST: self._post,
@ -77,6 +100,7 @@ class Statemachine:
State.REWIND: self._rewind, State.REWIND: self._rewind,
State.IDLE_END: self._idle_end State.IDLE_END: self._idle_end
} }
while (self.state is not State.ERROR) and \ while (self.state is not State.ERROR) and \
(self.state is not State.SHUTDOWN): (self.state is not State.SHUTDOWN):
choice[self.state]() choice[self.state]()
@ -107,40 +131,51 @@ class Statemachine:
Initialize hal callbacks, load sounds Initialize hal callbacks, load sounds
""" """
logger.debug(f'power on') logger.debug(f'power on')
# TODO enable lid sensor
# self.hal.lid_sensor.when_pressed = self._lid_open # self.hal.lid_sensor.when_pressed = self._lid_open
# self.hal.lid_sensor.when_released = self._lid_closed # self.hal.lid_sensor.when_released = self._lid_closed
try:
self.hal.init_connection() self.hal.init_sounds(load_sounds())
except SerialCommunicationError as e: self.hal.init_camera()
self.state = State.ERROR
logger.exception(e)
return
init_sounds(self.hal, load_sounds())
init_camera(self.hal)
self.state = State.POST self.state = State.POST
def _post(self): def _post(self):
""" """
Power on self test. 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.') logger.warning('USB-Stick not found.')
self.state = State.ERROR self.state = State.ERROR
return return
# TODO set RPi_HELO pins, wait for response
# Callback for start when blue button is held # Callback for start when blue button is held
self.hal.btn_start.when_activated = self._start_or_rewind # self.hal.btn_start.when_activated = self._start_or_rewind
logger.debug('start button callback activated') # 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 a sound if everything is alright
play_sound(self.hal, fs_names.SFX_POST_OK) play_sound(self.hal, fs_names.SFX_POST_OK)
self.state = State.IDLE_START if self.test:
logger.debug('idle_start') self.state = State.PLAY
logger.debug('play')
else:
self.state = State.IDLE_START
logger.debug('idle_start')
def _idle_start(self): def _idle_start(self):
""" """
@ -148,24 +183,9 @@ class Statemachine:
""" """
pass 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): def _play(self):
""" """
Run the storyboard Select language, then run the storyboard
""" """
logger.debug(f'play') logger.debug(f'play')
if self.test: if self.test:
@ -174,48 +194,134 @@ class Statemachine:
# TODO reenable language selection # TODO reenable language selection
self.story = self.story_en self.story = self.story_en
for chapter in iter(self.story): while self.chapter is not None:
logger.debug(f'playing chapter {chapter}') 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(): while chapter.hasnext():
act = next(chapter) act = next(chapter)
logger.debug(f'next activity {act.activity}') logger.debug(f'next activity {act.activity}')
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, blue_cb = self._option_callback(act.values['on_blue']),
back_callback=chapter.rewind, red_cb = self._option_callback(act.values['on_red']),
to_callback=self._start_or_rewind) yellow_cb = self._option_callback(act.values['on_yellow']),
# elif act.activity is Activity.ADVANCE_UP: green_cb = self._option_callback(act.values['on_green']),
# if chapter.move and self.move: timeout_cb = self._option_callback(act.values['on_timeout']),
# logger.debug( **act.values)
# 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'))
else: else:
try: try:
{ ACTIVITY_SELECTOR[act.activity](self.hal, **act.values)
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)
except KeyError: except KeyError:
logger.exception('Caught KeyError, ignoring...') logger.exception('Caught KeyError, ignoring...')
pass 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): def _rewind(self):
""" """
Rewind all scrolls, post-process videos Rewind all scrolls, post-process videos
""" """
# postprocessing # TODO postprocessing - add sound
logger.debug('Converting video...') logger.debug('Converting video...')
cmdstring = f'MP4Box -add {fs_names.REC_DRAW_CITY} {fs_names.REC_MERGED_VIDEO}' cmdstring = f'MP4Box -add {fs_names.REC_DRAW_CITY} {fs_names.REC_MERGED_VIDEO}'
call([cmdstring], shell=True) call([cmdstring], shell=True)
@ -223,6 +329,7 @@ class Statemachine:
logger.debug('Rewinding...') logger.debug('Rewinding...')
if self.move: if self.move:
rewind(self.hal) rewind(self.hal)
for chapter in self.story: for chapter in self.story:
chapter.rewind() chapter.rewind()

View file

@ -1,19 +1,70 @@
from enum import Enum, auto 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): class Activity(Enum):
WAIT_FOR_INPUT = {'steps': 0} """
PLAY_SOUND = {'sound': None} Things the box can do
RECORD_SOUND = {'duration': 0.0, 'filename': '', 'cache': False} """
RECORD_VIDEO = {'duration': 0.0, 'filename': ''} WAIT_FOR_INPUT = {'on_blue': Select(Option.CONTINUE),
TAKE_PHOTO = {'filename': ''} 'on_red': Select(Option.REPEAT),
ADVANCE_UP = {'speed': 0.3, 'direction': True, 'steps': 100} 'on_yellow': Select(None),
ADVANCE_LEFT = {'speed': 0.3, 'direction': True, 'steps': 200} # TODO set right number of steps 'on_green': Select(None),
LIGHT_LAYER = {'intensity': 1.0, 'fade': 0.0, 'layer': True} 'on_timeout': Select(Option.QUIT),
LIGHT_BACK = {'intensity': 1.0, 'fade': 0.0} '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: class Do:
"""
An activity instance. Can override the default settings from `Activity`s
"""
def __init__(self, activity: Activity, **kwargs): def __init__(self, activity: Activity, **kwargs):
self.activity = activity self.activity = activity
self.values = {} self.values = {}
@ -23,33 +74,63 @@ class Do:
class Chapter: 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. Keeps track of advanced steps on the scrolls.
""" """
def __init__(self, *activities): def __init__(self, *activities):
self.activities = activities self.activities = activities
self.pos = 0 self.index = 0
self.move = True self.h_pos = 0
self.move_ud = 0 self.v_pos = 0
def __iter__(self): def __iter__(self):
return self return self
def __next__(self): def __next__(self):
if self.pos >= len(self.activities): if self.index >= len(self.activities):
raise StopIteration raise StopIteration
act = self.activities[self.pos]
self.pos += 1 act = self.activities[self.index]
self._update_pos(act)
return 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): 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): 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): def skip(self, **kwargs):
self.move = True """
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}