StateMachine icon indicating copy to clipboard operation
StateMachine copied to clipboard

Is there a wildcard state syntax to signify all states?

Open pmi123 opened this issue 5 years ago • 4 comments

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

pmi123 avatar Feb 04 '20 05:02 pmi123

state(StateMachine.Matcher.any()) {
    on<Event.reset> {
        transitionTo(State.Start)
    }
}

kz89 avatar Nov 05 '20 11:11 kz89

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!

pmi123 avatar Sep 04 '21 00:09 pmi123

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 avatar Sep 05 '21 16:09 kz89

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

pmi123 avatar Sep 05 '21 18:09 pmi123