Plan 3: state machine (IDLE -> WAKE_PENDING -> LISTENING <-> ASSISTANT_SPEAKING -> IDLE_PENDING -> IDLE)
This commit is contained in:
@@ -0,0 +1,70 @@
|
|||||||
|
"""Assistant client state machine.
|
||||||
|
|
||||||
|
States flow: IDLE -> WAKE_PENDING -> LISTENING <-> ASSISTANT_SPEAKING
|
||||||
|
\\ /
|
||||||
|
session_ended
|
||||||
|
\\
|
||||||
|
IDLE_PENDING -> IDLE
|
||||||
|
"""
|
||||||
|
from enum import Enum, auto
|
||||||
|
|
||||||
|
from client.log import get_logger
|
||||||
|
|
||||||
|
_log = get_logger("state")
|
||||||
|
|
||||||
|
|
||||||
|
class State(Enum):
|
||||||
|
IDLE = auto()
|
||||||
|
WAKE_PENDING = auto()
|
||||||
|
LISTENING = auto()
|
||||||
|
ASSISTANT_SPEAKING = auto()
|
||||||
|
IDLE_PENDING = auto()
|
||||||
|
|
||||||
|
|
||||||
|
class StateMachine:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._state = State.IDLE
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self) -> State:
|
||||||
|
return self._state
|
||||||
|
|
||||||
|
@property
|
||||||
|
def wakeword_enabled(self) -> bool:
|
||||||
|
return self._state is State.IDLE
|
||||||
|
|
||||||
|
@property
|
||||||
|
def uplink_enabled(self) -> bool:
|
||||||
|
return self._state is State.LISTENING
|
||||||
|
|
||||||
|
def _transition(self, target: State) -> None:
|
||||||
|
if target is self._state:
|
||||||
|
return
|
||||||
|
_log.info("state %s -> %s", self._state.name, target.name)
|
||||||
|
self._state = target
|
||||||
|
|
||||||
|
def wake_fired(self) -> bool:
|
||||||
|
if self._state is not State.IDLE:
|
||||||
|
return False
|
||||||
|
self._transition(State.WAKE_PENDING)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def session_started(self) -> None:
|
||||||
|
if self._state in (State.WAKE_PENDING, State.IDLE):
|
||||||
|
self._transition(State.LISTENING)
|
||||||
|
|
||||||
|
def assistant_audio_arrived(self) -> None:
|
||||||
|
if self._state is State.LISTENING:
|
||||||
|
self._transition(State.ASSISTANT_SPEAKING)
|
||||||
|
|
||||||
|
def assistant_done(self) -> None:
|
||||||
|
if self._state is State.ASSISTANT_SPEAKING:
|
||||||
|
self._transition(State.LISTENING)
|
||||||
|
|
||||||
|
def session_ended(self) -> None:
|
||||||
|
if self._state in (State.LISTENING, State.ASSISTANT_SPEAKING, State.WAKE_PENDING):
|
||||||
|
self._transition(State.IDLE_PENDING)
|
||||||
|
|
||||||
|
def sleep_played(self) -> None:
|
||||||
|
if self._state is State.IDLE_PENDING:
|
||||||
|
self._transition(State.IDLE)
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
from client.state import State, StateMachine
|
||||||
|
|
||||||
|
|
||||||
|
def test_starts_idle_with_wakeword_enabled_and_uplink_disabled():
|
||||||
|
sm = StateMachine()
|
||||||
|
assert sm.state is State.IDLE
|
||||||
|
assert sm.wakeword_enabled
|
||||||
|
assert not sm.uplink_enabled
|
||||||
|
|
||||||
|
|
||||||
|
def test_wake_fired_goes_idle_to_wake_pending_and_disables_wakeword():
|
||||||
|
sm = StateMachine()
|
||||||
|
assert sm.wake_fired() is True
|
||||||
|
assert sm.state is State.WAKE_PENDING
|
||||||
|
assert not sm.wakeword_enabled
|
||||||
|
assert not sm.uplink_enabled
|
||||||
|
|
||||||
|
|
||||||
|
def test_wake_fired_is_idempotent_when_not_idle():
|
||||||
|
sm = StateMachine()
|
||||||
|
sm.wake_fired()
|
||||||
|
assert sm.wake_fired() is False
|
||||||
|
assert sm.state is State.WAKE_PENDING
|
||||||
|
|
||||||
|
|
||||||
|
def test_session_started_goes_wake_pending_to_listening_and_unmutes_uplink():
|
||||||
|
sm = StateMachine()
|
||||||
|
sm.wake_fired()
|
||||||
|
sm.session_started()
|
||||||
|
assert sm.state is State.LISTENING
|
||||||
|
assert sm.uplink_enabled
|
||||||
|
assert not sm.wakeword_enabled
|
||||||
|
|
||||||
|
|
||||||
|
def test_assistant_audio_arrived_mutes_uplink():
|
||||||
|
sm = StateMachine()
|
||||||
|
sm.wake_fired()
|
||||||
|
sm.session_started()
|
||||||
|
sm.assistant_audio_arrived()
|
||||||
|
assert sm.state is State.ASSISTANT_SPEAKING
|
||||||
|
assert not sm.uplink_enabled
|
||||||
|
|
||||||
|
|
||||||
|
def test_assistant_audio_arrived_is_noop_outside_listening():
|
||||||
|
sm = StateMachine()
|
||||||
|
sm.assistant_audio_arrived()
|
||||||
|
assert sm.state is State.IDLE # ignored from IDLE
|
||||||
|
sm.wake_fired()
|
||||||
|
sm.assistant_audio_arrived()
|
||||||
|
assert sm.state is State.WAKE_PENDING # ignored from WAKE_PENDING
|
||||||
|
|
||||||
|
|
||||||
|
def test_assistant_done_returns_to_listening_from_speaking():
|
||||||
|
sm = StateMachine()
|
||||||
|
sm.wake_fired()
|
||||||
|
sm.session_started()
|
||||||
|
sm.assistant_audio_arrived()
|
||||||
|
sm.assistant_done()
|
||||||
|
assert sm.state is State.LISTENING
|
||||||
|
assert sm.uplink_enabled
|
||||||
|
|
||||||
|
|
||||||
|
def test_assistant_done_is_noop_when_not_speaking():
|
||||||
|
sm = StateMachine()
|
||||||
|
sm.wake_fired()
|
||||||
|
sm.session_started() # LISTENING; never went to ASSISTANT_SPEAKING
|
||||||
|
sm.assistant_done()
|
||||||
|
assert sm.state is State.LISTENING
|
||||||
|
|
||||||
|
|
||||||
|
def test_session_ended_routes_through_idle_pending_then_idle():
|
||||||
|
sm = StateMachine()
|
||||||
|
sm.wake_fired()
|
||||||
|
sm.session_started()
|
||||||
|
sm.session_ended()
|
||||||
|
assert sm.state is State.IDLE_PENDING
|
||||||
|
assert not sm.wakeword_enabled
|
||||||
|
assert not sm.uplink_enabled
|
||||||
|
sm.sleep_played()
|
||||||
|
assert sm.state is State.IDLE
|
||||||
|
assert sm.wakeword_enabled
|
||||||
|
|
||||||
|
|
||||||
|
def test_session_ended_works_from_assistant_speaking():
|
||||||
|
sm = StateMachine()
|
||||||
|
sm.wake_fired()
|
||||||
|
sm.session_started()
|
||||||
|
sm.assistant_audio_arrived()
|
||||||
|
sm.session_ended()
|
||||||
|
assert sm.state is State.IDLE_PENDING
|
||||||
|
|
||||||
|
|
||||||
|
def test_session_ended_from_idle_is_noop():
|
||||||
|
sm = StateMachine()
|
||||||
|
sm.session_ended()
|
||||||
|
assert sm.state is State.IDLE
|
||||||
|
|
||||||
|
|
||||||
|
def test_sleep_played_outside_idle_pending_is_noop():
|
||||||
|
sm = StateMachine()
|
||||||
|
sm.sleep_played()
|
||||||
|
assert sm.state is State.IDLE
|
||||||
Reference in New Issue
Block a user