Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Is there a wildcard state syntax to signify all states? #27

Open
pmi123 opened this issue Feb 4, 2020 · 4 comments
Open

Is there a wildcard state syntax to signify all states? #27

pmi123 opened this issue Feb 4, 2020 · 4 comments

Comments

@pmi123
Copy link

pmi123 commented Feb 4, 2020

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

@kz89
Copy link

kz89 commented Nov 5, 2020

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

@pmi123
Copy link
Author

pmi123 commented Sep 4, 2021

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!

@kz89
Copy link

kz89 commented Sep 5, 2021

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

@pmi123
Copy link
Author

pmi123 commented Sep 5, 2021

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants