StateMachine
StateMachine copied to clipboard
Is there a wildcard state syntax to signify all states?
Is there a way to express all states as the start of a transition? In other words, if there is a transition that causes all states to go to one single state, how can that be expressed in the transition code? For example, consider a 'reset' transition that makes the state machine go to a Start state regardless of the current state I can write a transition rule from each state to Start using the event reset, but it would be more readable to have something like
state<State.*> {
on<Event.reset> {
transitionTo(State.Start)
}
}
Is there a syntax for something like this? Thanks! Mark
state(StateMachine.Matcher.any()) {
on<Event.reset> {
transitionTo(State.Start)
}
}
I tried the above syntax for transitioning from any state, and it does not work. I have a state machine defined and I am building up a set of test cases for each transition. All the state transitions work except the wildcard state matching above.
My state machine definition:
sealed class RLState {
object Start: RLState()
object Connected: RLState()
object ContinuityOK: RLState()
object ContinuityFailed: RLState()
object Armed: RLState()
object Launch: RLState()
object Ignition: RLState()
}
sealed class RLEvent {
object SecureBLEOK: RLEvent()
object SecureBLELost: RLEvent()
object SearchBLE: RLEvent()
object InvalidKey: RLEvent()
data class ValidKey(val continuity_detected: Boolean) : RLEvent()
//object ValidKey: RLEvent()
//object ContinuityOK: RLEvent()
object ContinuityFailed: RLEvent()
object RemoveKey: RLEvent()
object ArmEngines: RLEvent()
object Launch: RLEvent()
object StartLaunchTimer: RLEvent()
object LaunchTimerRunning: RLEvent()
object LaunchTimerExpired: RLEvent()
object Reset: RLEvent()
}
sealed class RLSideEffect {
object LogStart: RLSideEffect()
object LogConnected: RLSideEffect()
object LogContinuityOK: RLSideEffect()
object LogContinuityFailed: RLSideEffect()
object LogArmed: RLSideEffect()
object LogLaunch: RLSideEffect()
object LogIgnition: RLSideEffect()
}
class LauncherStateMachine {
companion object {
val LOG = Logger.getLogger(LauncherStateMachine::class.java.name)
}
val launcherStateMachine = StateMachine.create<RLState, RLEvent, RLSideEffect> {
initialState(RLState.Start)
state<RLState.Start> {
on<RLEvent.SecureBLEOK> {
transitionTo(RLState.Connected, RLSideEffect.LogConnected)
}
}
state<RLState.Connected> {
on<RLEvent.ValidKey> {
if (it.continuity_detected) {
transitionTo(RLState.ContinuityOK, RLSideEffect.LogContinuityOK)
} else {
transitionTo(RLState.ContinuityFailed, RLSideEffect.LogContinuityFailed)
}
}
}
state<RLState.ContinuityOK> {
on<RLEvent.ArmEngines> {
transitionTo(RLState.Armed, RLSideEffect.LogArmed)
}
on<RLEvent.RemoveKey> {
transitionTo(RLState.Connected, RLSideEffect.LogConnected)
}
}
state<RLState.ContinuityFailed> {
on<RLEvent.RemoveKey> {
transitionTo(RLState.Connected, RLSideEffect.LogConnected)
}
}
state<RLState.Armed> {
on<RLEvent.Launch> {
transitionTo(RLState.Launch, RLSideEffect.LogLaunch)
}
on<RLEvent.RemoveKey> {
transitionTo(RLState.Connected, RLSideEffect.LogConnected)
}
on<RLEvent.ContinuityFailed> {
transitionTo(RLState.ContinuityFailed, RLSideEffect.LogContinuityFailed)
}
}
state<RLState.Launch> {
on<RLEvent.StartLaunchTimer> {
transitionTo(RLState.Ignition, RLSideEffect.LogIgnition)
}
on<RLEvent.ContinuityFailed> {
transitionTo(RLState.ContinuityFailed, RLSideEffect.LogContinuityFailed)
}
}
state<RLState.Ignition> {
on<RLEvent.LaunchTimerExpired> {
transitionTo(RLState.Connected, RLSideEffect.LogConnected)
}
}
state(StateMachine.Matcher.any()) {
// from any state, if the BLE is lost, then reset to the start state
on<RLEvent.SecureBLELost> {
transitionTo(RLState.Start)
}
// hook for resetting the state machine
on<RLEvent.Reset> {
transitionTo(RLState.Start)
}
}
onTransition{
val validTransition = it as? StateMachine.Transition.Valid ?: return@onTransition
when(validTransition.sideEffect){
RLSideEffect.LogStart -> LOG.info("Entered Start state")
RLSideEffect.LogConnected -> LOG.info("Entered Connected state")
RLSideEffect.LogContinuityOK -> LOG.info("Entered ContinuityOK state")
RLSideEffect.LogContinuityFailed -> LOG.info("Entered ContinuityFailed state")
RLSideEffect.LogArmed -> LOG.info("Entered Armed state")
RLSideEffect.LogLaunch -> LOG.info("Entered Launch state")
RLSideEffect.LogIgnition -> LOG.info("Entered Ignition state")
}
}
}
}
This test passes:
@Test
fun transitionIgnition() {
println("Test: transitionIgnition")
stateMachine.launcherStateMachine.transition(RLEvent.SecureBLEOK)
stateMachine.launcherStateMachine.transition(RLEvent.ValidKey(continuity_detected = true))
stateMachine.launcherStateMachine.transition(RLEvent.ArmEngines)
stateMachine.launcherStateMachine.transition(RLEvent.Launch)
stateMachine.launcherStateMachine.transition(RLEvent.StartLaunchTimer)
assertEquals(RLState.Ignition, stateMachine.launcherStateMachine.state)
}
This test fails:
@Test
fun transitionIgnitionSecureBLELost() {
println("Test: transitionIgnitionSecureBLELost")
stateMachine.launcherStateMachine.transition(RLEvent.SecureBLEOK)
stateMachine.launcherStateMachine.transition(RLEvent.ValidKey(continuity_detected = true))
stateMachine.launcherStateMachine.transition(RLEvent.ArmEngines)
stateMachine.launcherStateMachine.transition(RLEvent.Launch)
stateMachine.launcherStateMachine.transition(RLEvent.StartLaunchTimer)
stateMachine.launcherStateMachine.transition(RLEvent.SecureBLELost)
assertEquals(RLState.Start, stateMachine.launcherStateMachine.state)
}
The error message says the expected state is RLState.Ignition
, instead of RLStateStart
. It appears that the matching to any state is not working. It must be a syntax error, but AndroidStudio doesn't indicate there is one.
Thanks for any guidance you can give me. I am transitioning from Java to Kotlin, and the learning curve is steep!
This doesn't work because if there are multiple definitions for the same state then the first one is used. This is from StateMachine.kt
:
private fun STATE.getDefinition() = graph.stateDefinitions
.filter { it.key.matches(this) }
.map { it.value }
.firstOrNull() ?: error("Missing definition for state ${this.javaClass.simpleName}!")
You have two definitions for RLState.Ignition
right now.
First:
state<RLState.Ignition> {
on<RLEvent.LaunchTimerExpired> {
transitionTo(RLState.Connected, RLSideEffect.LogConnected)
}
}
and second:
state(StateMachine.Matcher.any()) {
// from any state, if the BLE is lost, then reset to the start state
on<RLEvent.SecureBLELost> {
transitionTo(RLState.Start)
}
// hook for resetting the state machine
on<RLEvent.Reset> {
transitionTo(RLState.Start)
}
}
and only the first one is used.
You could move state(StateMachine.Matcher.any())
definition above the state<RLState.Ignition>
definition but then the latter one won't ever be used...
I guess the only way to do this would be to add the default transitions to every state unfortunately.
You can define it like:
private fun <State : RLState> StateMachine.GraphBuilder<RLState, RLEvent, RLSideEffect>.StateDefinitionBuilder<State>.defaultTransitions() {
// from any state, if the BLE is lost, then reset to the start state
on<RLEvent.SecureBLELost> {
transitionTo(RLState.Start)
}
// hook for resetting the state machine
on<RLEvent.Reset> {
transitionTo(RLState.Start)
}
}
And then call if for every state like:
state<RLState.Start> {
defaultTransitions()
on<RLEvent.SecureBLEOK> {
transitionTo(RLState.Connected, RLSideEffect.LogConnected)
}
}
@kz89 Thanks for your quick reply! Your solution worked like a charm.
Another solution, since the state machine is so small, is to add the following at the end of every state transition definition:
on<RLEvent.SecureBLELost> {
transitionTo(RLState.Start, RLSideEffect.LogStart)
}
This seems to work as well, as all tests that I wrote passed, too.