Introduction

Having used various incarnations of SMC over the years we decided to try it again and discovered it didn’t have specific support for Kotlin. Instead of creating a generator for Kotlin we decided to attempt a Kotlin DSL along with a simple implementation.

To learn more about Finite State Machines visit:

The Tutorial provides examples of how to compose a DSL and FSM.

Features

This Finite state machine implementation has the following features:

  • Event driven state machine.

  • External and internal transitions

  • State entry and exit actions.

  • Default state actions.

  • Default entry and exit actions.

  • Determine allowed events for current or given state.

  • Multiple state maps with push / pop transitions

  • Automatic transitions

  • Externalisation of state.

  • Simple Visualization.

  • Detailed Visualization.

  • Visualization Gradle Plugin

  • Coroutines

  • Timeout Transitions

Tutorial

Simple turnstile example

Assume we and to manage the state on a simple lock. We want to ensure that the lock() function is only called when the lock is not locked and we want unlock() to be called when locked.

Then we use the DSL to declare a definition of a statemachine matching the diagram:

State Diagram

LockStateDiagram

State Table

Start State Event End State Action

LOCKED

PASS

LOCKED

alarm

LOCKED

COIN

UNLOCKED

unlock

UNLOCKED

PASS

LOCKED

lock

UNLOCKED

COIN

UNLOCKED

returnCoin

Context class

class Turnstile(locked: Boolean = true) {
    var locked: Boolean = locked
        private set

    fun unlock() {
        require(locked) { "Cannot unlock when not locked" }
        println("Unlock")
        locked = false
    }

    fun lock() {
        require(!locked) { "Cannot lock when locked" }
        println("Lock")
        locked = true
    }

    fun alarm() {
        println("Alarm")
    }

    fun returnCoin() {
        println("Return Coin")
    }

    override fun toString(): String {
        return "Turnstile(locked=$locked)"
    }
}

Enums for States and Events

We declare 2 enums, one for the possible states and one for the possible events.

enum class TurnstileStates {
    LOCKED,
    UNLOCKED
}

/**
 * @suppress
 */
enum class TurnstileEvents {
    COIN,
    PASS
}

Packaged definition and execution

class TurnstileFSM(turnstile: Turnstile, savedState: TurnstileStates? = null) {
    private val fsm = definition.create(turnstile, savedState)

    fun externalState() = fsm.currentState
    fun coin() = fsm.sendEvent(TurnstileEvents.COIN)
    fun pass() = fsm.sendEvent(TurnstileEvents.PASS)
    fun allowedEvents() = fsm.allowed().map { it.name.toLowerCase() }.toSet()

    companion object {
        val definition =
            stateMachine(
                TurnstileStates.values().toSet(),
                TurnstileEvents.values().toSet(),
                Turnstile::class
            ) {
                defaultInitialState = TurnstileStates.LOCKED
                initialState {
                    if (locked)
                        TurnstileStates.LOCKED
                    else
                        TurnstileStates.UNLOCKED
                }
                default {
                    onEntry { startState, targetState, _ ->
                        println("entering:$startState -> $targetState for $this")
                    }
                    action { state, event, _ ->
                        println("Default action for state($state) -> on($event) for $this")
                        alarm()
                    }
                    onExit { startState, targetState, _ ->
                        println("exiting:$startState -> $targetState for $this")
                    }
                }
                whenState(TurnstileStates.LOCKED) {
                    onEvent(TurnstileEvents.COIN to TurnstileStates.UNLOCKED) {
                        unlock()
                    }
                }
                whenState(TurnstileStates.UNLOCKED) {
                    onEvent(TurnstileEvents.COIN) {
                        returnCoin()
                    }
                    onEvent(TurnstileEvents.PASS to TurnstileStates.LOCKED) {
                        lock()
                    }
                }
            }.build()

        fun possibleEvents(state: TurnstileStates, includeDefault: Boolean = false) =
            definition.possibleEvents(state, includeDefault)
    }
}

Usage

Then we instantiate the FSM and provide a context to operate on:

val turnstile = Turnstile()
val fsm = TurnstileFSM(turnstile)

Now we have a context that is independent of the FSM.

Sending events may invoke actions:

// State state is LOCKED
fsm.coin()
// Expect unlock action end state is UNLOCKED
fsm.pass()
// Expect lock() action and end state is LOCKED
fsm.pass()
// Expect alarm() action and end state is LOCKED
fsm.coin()
// Expect unlock() and end state is UNLOCKED
fsm.coin()
// Expect returnCoin() and end state is UNLOCKED

Advanced Features

We can add an argument to events and use named state maps with push / pop and automatic transitions.

The argument is sent as follows:

fsm.sendEvent(EVENT1, arg)

The argument is references in the action as follows:

onEvent(EVENT1) { arg ->
    val value = arg as Int
}

If we update the turnstile to include the value of the coin in the coin event we could implement the following: A named state where decisions regarding coins are made. We push to coins with COINS state and then the automatic states will be triggered if the guards are met.

Paying Turnstile

State Table

Start State Event Guard Expression End State Action

LOCKED

PASS

LOCKED

alarm

LOCKED

COIN

COINS

coin(value)

UNLOCKED

PASS

LOCKED

lock

COINS

COIN

COINS

coin(value)

COINS

coins == requiredCoins

UNLOCKED

unlock

COINS

coins > requiredCoins

UNLOCKED

returnCoin(coins-requiredCoins), unlock

When event is empty it is an automatic transition. We will further place COINS state in a named state map to illustrate how these can be composes to isolate or group behaviour.

Context class

The context class doesn’t make decisions. The context class stores values and will update value in very specific ways.

class PayingTurnstile(
    val requiredCoins: Int,
    locked: Boolean = true,
    coins: Int = 0
) {
    var coins: Int = coins
        private set
    var locked: Boolean = locked
        private set

    fun unlock() {
        require(locked) { "Cannot unlock when not locked" }
        require(coins >= requiredCoins) { "Not enough coins. ${requiredCoins - coins} required" }
        println("Unlock")
        locked = false
    }

    fun lock() {
        require(!locked) { "Cannot lock when locked" }
        require(coins == 0) { "Coins $coins must be returned" }
        println("Lock")
        locked = true
    }

    fun alarm() {
        println("Alarm")
    }

    fun coin(value: Int): Int {
        coins += value
        println("Coin received=$value, Total=$coins")
        return coins
    }

    fun returnCoin(returnCoins: Int) {
        println("Return Coin:$returnCoins")
        coins -= returnCoins
    }

    fun reset() {
        coins = 0
        println("Reset coins=$coins")
    }

    override fun toString(): String {
        return "Turnstile(locked=$locked,coins=$coins)"
    }
}

States and Events

enum class PayingTurnstileStates {
    LOCKED,
    COINS,
    UNLOCKED
}

/**
 * @suppress
 */
enum class PayingTurnstileEvents {
    COIN,
    PASS
}

We add a stateMap named coins with the state COINS. The statemap will be entered when there are coins. The automatic transitions will be triggered based on the guard expressions.

State machine definition packaged

This includes an example of externalizing the state and creating an instance using a previously externalized state. The externalState method provides a list of states for the case where there are nested statemaps.

class PayingTurnstileFSM(
    requiredCoins: Int,
    savedState: PayingTurnstileFSMExternalState? = null
) {
    // not private for visibility for tests
    val turnstile: PayingTurnstile = PayingTurnstile(requiredCoins, savedState?.locked ?: true, savedState?.coins ?: 0)
    val fsm = if (savedState != null) {
        definition.create(turnstile, savedState.initialState)
    } else {
        definition.create(turnstile)
    }

    fun coin(value: Int) {
        println("sendEvent:COIN:$value")
        fsm.sendEvent(PayingTurnstileEvents.COIN, value)
    }

    fun pass() {
        println("sendEvent:PASS")
        fsm.sendEvent(PayingTurnstileEvents.PASS)
    }

    fun allowedEvents() = fsm.allowed().map { it.name.toLowerCase() }.toSet()
    fun externalState(): PayingTurnstileFSMExternalState {
        return PayingTurnstileFSMExternalState(turnstile.coins, turnstile.locked, fsm.externalState())
    }

    companion object {
        val definition = stateMachine(
            setOf(PayingTurnstileStates.LOCKED, PayingTurnstileStates.UNLOCKED),
            PayingTurnstileEvents.values().toSet(),
            PayingTurnstile::class,
            Int::class
        ) {
            defaultInitialState = PayingTurnstileStates.LOCKED
            default {
                onEntry { _, targetState, arg ->
                    if (arg != null) {
                        println("entering:$targetState ($arg) for $this")
                    } else {
                        println("entering:$targetState for $this")
                    }
                }
                action { state, event, arg ->
                    if (arg != null) {
                        println("Default action for state($state) -> on($event, $arg) for $this")
                    } else {
                        println("Default action for state($state) -> on($event) for $this")
                    }
                    alarm()
                }
                onExit { startState, _, arg ->
                    if (arg != null) {
                        println("exiting:$startState ($arg) for $this")
                    } else {
                        println("exiting:$startState for $this")
                    }
                }
            }
            stateMap("coins", setOf(PayingTurnstileStates.COINS)) {
                whenState(PayingTurnstileStates.COINS) {
                    automaticPop(PayingTurnstileStates.UNLOCKED, guard = { coins > requiredCoins }) {
                        println("automaticPop:returnCoin")
                        returnCoin(coins - requiredCoins)
                        unlock()
                        reset()
                    }
                    automaticPop(PayingTurnstileStates.UNLOCKED, guard = { coins == requiredCoins }) {
                        println("automaticPop")
                        unlock()
                        reset()
                    }
                    onEvent(PayingTurnstileEvents.COIN) { value ->
                        require(value != null) { "argument required for COIN" }
                        coin(value)
                        println("Coins=$coins")
                        if (coins < requiredCoins) {
                            println("Please add ${requiredCoins - coins}")
                        }
                    }
                }
            }
            whenState(PayingTurnstileStates.LOCKED) {
                // The coin brings amount to exact amount
                onEventPush(PayingTurnstileEvents.COIN, "coins", PayingTurnstileStates.COINS) { value ->
                    require(value != null) { "argument required for COIN" }
                    coin(value)
                    unlock()
                    reset()
                }
                // The coins add up to more than required
                onEventPush(PayingTurnstileEvents.COIN, "coins", PayingTurnstileStates.COINS,
                    guard = { value ->
                        require(value != null) { "argument required for COIN" }
                        value + coins < requiredCoins
                    }) { value ->
                    require(value != null) { "argument required for COIN" }
                    println("PUSH TRANSITION")
                    coin(value)
                    println("Coins=$coins, Please add ${requiredCoins - coins}")
                }
            }
            whenState(PayingTurnstileStates.UNLOCKED) {
                onEvent(PayingTurnstileEvents.COIN) { value ->
                    require(value != null) { "argument required for COIN" }
                    returnCoin(coin(value))
                }
                onEvent(PayingTurnstileEvents.PASS to PayingTurnstileStates.LOCKED) {
                    lock()
                }
            }
        }.build()
    }
}

Test

        val fsm = PayingTurnstileFSM(50)
        assertTrue(fsm.turnstile.locked)
        println("External:${fsm.externalState()}")
        println("--coin1")
        fsm.coin(10)
        assertTrue(fsm.turnstile.locked)
        assertTrue(fsm.turnstile.coins == 10)
        println("--coin2")
        println("External:${fsm.externalState()}")
        val externalState = fsm.externalState()
        PayingTurnstileFSM(50, externalState).apply {
            coin(60)
            assertTrue(turnstile.coins == 0)
            assertTrue(!turnstile.locked)
            println("External:${externalState()}")
            println("--pass1")
            pass()
            assertTrue(turnstile.locked)
            println("--pass2")
            pass()
            println("--pass3")
            pass()
            println("--coin3")
            coin(40)
            assertTrue(turnstile.coins == 40)
            println("--coin4")
            coin(10)
            assertTrue(turnstile.coins == 0)
            assertTrue(!turnstile.locked)
        }

Output

--coin1
sendEvent:COIN:10
entering:LOCKED ([10]) for Turnstile(locked=true,coins=0)
PUSH TRANSITION
Coin received=10, Total=10
Coins=10, Please add 40
--coin2
sendEvent:COIN:60
Coin received=60, Total=70
Return Coin:20
Unlock
Reset coins=0
entering:UNLOCKED ([60]) for Turnstile(locked=false,coins=0)
--pass1
sendEvent:PASS
exiting:UNLOCKED for Turnstile(locked=false,coins=0)
Lock
entering:LOCKED for Turnstile(locked=true,coins=0)
--pass2
sendEvent:PASS
Default action for state(LOCKED) -> on(PASS) for Turnstile(locked=true,coins=0)
Alarm
--pass3
sendEvent:PASS
Default action for state(LOCKED) -> on(PASS) for Turnstile(locked=true,coins=0)
Alarm
--coin3
sendEvent:COIN:40
entering:LOCKED ([40]) for Turnstile(locked=true,coins=0)
PUSH TRANSITION
Coin received=40, Total=40
Coins=40, Please add 10
--coin4
sendEvent:COIN:10
Coin received=10, Total=50
Unlock
Reset coins=0
entering:UNLOCKED ([10]) for Turnstile(locked=false,coins=0)

Secure Turnstile Example

Unless you grew up in a city with a subway you may not be familiar with a turnstile accepting counts. You may be familiar with turnstile that responds to RFID or NFC cards.

We would add a complexity that allows for an override card to allow cards to pass even if they are invalid.

We will use guard expressions to identify the override card and the validity of the card. When the override card is tapped a 2nd time it will cancel the override or lock the turnstile depending on the state.

Secure Turnstile FSM

State Table

Start State Event Guard Expression End State Action

LOCKED

CARD(id)

isOverrideCard(id) and overrideActive

cancelOverride()

LOCKED

CARD(id)

isOverrideCard(id)

activateOverride()

LOCKED

CARD(id)

overrideActive or isValidCard(id)

UNLOCKED

unlock()

LOCKED

CARD(id)

not isValidVard(id)

invalidCard()

UNLOCKED

PASS

LOCKED

lock()

default

buzzer()

Context class

The context class doesn’t make decisions about the behaviour of the turnstile. The context will provide information about the state of turnstile and validity of cards The context class stores values and will update value in very specific ways.

class SecureTurnstile {
    var locked: Boolean = true
        private set
    var overrideActive: Boolean = false
        private set

    fun activateOverride() {
        overrideActive = true
        println("override activated")
    }

    fun cancelOverride() {
        overrideActive = false
        println("override canceled")
    }

    fun lock() {
        println("lock")
        locked = true
        overrideActive = false
    }

    fun unlock() {
        println("unlock")
        locked = false
        overrideActive = false
    }

    fun buzzer() {
        println("BUZZER")
    }

    fun invalidCard(cardId: Int) {
        println("Invalid card $cardId")
    }

    fun isOverrideCard(cardId: Int): Boolean {
        return cardId == 42
    }

    fun isValidCard(cardId: Int): Boolean {
        return cardId % 2 == 1
    }
}

States and Events

enum class SecureTurnstileEvents {
    CARD,
    PASS
}

State machine definition packaged

class SecureTurnstileFSM(private val secureTurnstile: SecureTurnstile) {
    companion object {
        val definition = stateMachine(
            SecureTurnstileStates.values().toSet(),
            SecureTurnstileEvents.values().toSet(),
            SecureTurnstile::class,
            Int::class
        ) {
            defaultInitialState = SecureTurnstileStates.LOCKED
            initialState { if (locked) SecureTurnstileStates.LOCKED else SecureTurnstileStates.UNLOCKED }
            default {
                action { _, _, _ ->
                    buzzer()
                }
            }
            whenState(SecureTurnstileStates.LOCKED) {
                onEvent(SecureTurnstileEvents.CARD, guard = { cardId -> requireNotNull(cardId)
                    isOverrideCard(cardId) && overrideActive
                }) {
                    cancelOverride()
                }
                onEvent(SecureTurnstileEvents.CARD, guard = { cardId -> requireNotNull(cardId)
                    isOverrideCard(cardId)
                }) {
                    activateOverride()
                }
                onEvent(SecureTurnstileEvents.CARD to SecureTurnstileStates.UNLOCKED,
                    guard = { cardId -> requireNotNull(cardId)
                        overrideActive || isValidCard(cardId)
                    }) {
                    unlock()
                }
                onEvent(SecureTurnstileEvents.CARD, guard = { cardId ->
                    requireNotNull(cardId) { "cardId is required" }
                    !isValidCard(cardId)
                }) { cardId -> requireNotNull(cardId)
                    invalidCard(cardId)
                }
            }
            whenState(SecureTurnstileStates.UNLOCKED) {
                onEvent(SecureTurnstileEvents.CARD to SecureTurnstileStates.LOCKED, guard = { cardId -> requireNotNull(cardId)
                    isOverrideCard(cardId)
                }) {
                    lock()
                }
                onEvent(SecureTurnstileEvents.PASS to SecureTurnstileStates.LOCKED) {
                    lock()
                }
            }
        }.build()
    }

    private val fsm = definition.create(secureTurnstile)
    fun card(cardId: Int) = fsm.sendEvent(SecureTurnstileEvents.CARD, cardId)
    fun pass() = fsm.sendEvent(SecureTurnstileEvents.PASS)
    fun allowEvent(): Set<String> = fsm.allowed().map { it.name.toLowerCase() }.toSet()
}

Test

        val fsm = PayingTurnstileFSM(50)
        assertTrue(fsm.turnstile.locked)
        println("External:${fsm.externalState()}")
        println("--coin1")
        fsm.coin(10)
        assertTrue(fsm.turnstile.locked)
        assertTrue(fsm.turnstile.coins == 10)
        println("--coin2")
        println("External:${fsm.externalState()}")
        val externalState = fsm.externalState()
        PayingTurnstileFSM(50, externalState).apply {
            coin(60)
            assertTrue(turnstile.coins == 0)
            assertTrue(!turnstile.locked)
            println("External:${externalState()}")
            println("--pass1")
            pass()
            assertTrue(turnstile.locked)
            println("--pass2")
            pass()
            println("--pass3")
            pass()
            println("--coin3")
            coin(40)
            assertTrue(turnstile.coins == 40)
            println("--coin4")
            coin(10)
            assertTrue(turnstile.coins == 0)
            assertTrue(!turnstile.locked)
        }

Packet Reader Example

packet reader fsm

In real terms the above example combines multiple transitions into one for brevity. It is better when all control characters are one type of event with guard expressions. All events have a parameter which is the byte received and the event type is either BYTE, CTRL or ESC where:

  • ESC is 0x1B

  • CTRL is for SOH,STX,ETX,EOT, ACK, NAK

  • BYTE all other characters

packet reader detail

Context Classes

The current checksum is a trivial implementation for the demonstration where the checksum has a character that matches the first character of each field.

class Block {
    val byteArrayOutputStream = ByteArrayOutputStream(32)
    fun addByte(byte: Int) {
        byteArrayOutputStream.write(byte)
    }
}

interface ProtocolHandler {
    fun sendNACK()
    fun sendACK()
}

interface PacketHandler : ProtocolHandler {
    val checksumValid: Boolean
    fun print()
    fun addField()
    fun endField()
    fun addByte(byte: Int)
    fun addChecksum(byte: Int)
    fun checksum()
}

class ProtocolSender : ProtocolHandler {
    override fun sendNACK() {
        println("NACK")
    }

    override fun sendACK() {
        println("ACK")
    }
}

class Packet(private val protocolHandler: ProtocolHandler) : PacketHandler,
    ProtocolHandler by protocolHandler {
    val fields = mutableListOf<ByteArray>()
    private var currentField: Block? = null
    private var _checksumValid: Boolean = false

    override val checksumValid: Boolean
        get() = _checksumValid
    private val checkSum = Block()

    override fun print() {
        println("Checksum:$checksumValid:Fields:${fields.size}")
        fields.forEachIndexed { index, bytes ->
            print("FLD:$index:")
            bytes.forEach { byte ->
                val hex = byte.toString(16).padStart(2, '0')
                print(" $hex")
            }
            println()
        }
        println()
    }

    override fun addField() {
        currentField = Block()
    }

    override fun endField() {
        val field = currentField
        require(field != null) { "expected currentField to have a value" }
        fields.add(field.byteArrayOutputStream.toByteArray())
        currentField = null
    }

    override fun addByte(byte: Int) {
        val field = currentField
        require(field != null) { "expected currentField to have a value" }
        field.addByte(byte)
    }

    override fun addChecksum(byte: Int) {
        checkSum.addByte(byte)
    }

    override fun checksum() {
        require(checkSum.byteArrayOutputStream.size() > 0)
        val checksumBytes = checkSum.byteArrayOutputStream.toByteArray()
        _checksumValid = if (checksumBytes.size == fields.size) {
            checksumBytes.mapIndexed { index, cs ->
                cs == fields[index][0]
            }.reduce { a, b -> a && b }
        } else {
            false
        }
    }
}

States and Events

class CharacterConstants {
    companion object {
        const val SOH = 0x01
        const val STX = 0x02
        const val ETX = 0x03
        const val EOT = 0x04
        const val ACK = 0x06
        const val NAK = 0x15
        const val ESC = 0x1b
    }
}

/**
 * CTRL :
 * BYTE everything else
 */
enum class ReaderEvents {
    BYTE, // everything else
    CTRL, // SOH, EOT, STX, ETX, ACK, NAK
    ESC // ESC = 0x1B
}

enum class ReaderStates {
    START,
    RCVPCKT,
    RCVDATA,
    RCVESC,
    RCVCHK,
    RCVCHKESC,
    CHKSUM,
    END
}

Packaged FSM

class PacketReaderFSM(private val packetHandler: PacketHandler) {
    companion object {

        val definition = stateMachine(
            ReaderStates.values().toSet(),
            ReaderEvents.values().toSet(),
            PacketHandler::class,
            Int::class
        ) {
            defaultInitialState = ReaderStates.START
            default {
                onEvent(ReaderEvents.BYTE to ReaderStates.END) {
                    sendNACK()
                }
                onEvent(ReaderEvents.CTRL to ReaderStates.END) {
                    sendNACK()
                }
                onEvent(ReaderEvents.ESC to ReaderStates.END) {
                    sendNACK()
                }
            }
            whenState(ReaderStates.START) {
                onEvent(
                    ReaderEvents.CTRL to ReaderStates.RCVPCKT,
                    guard = { byte -> byte == CharacterConstants.SOH }) {}
            }
            whenState(ReaderStates.RCVPCKT) {
                onEvent(ReaderEvents.CTRL to ReaderStates.RCVDATA, guard = { byte -> byte == CharacterConstants.STX }) {
                    addField()
                }
                onEvent(ReaderEvents.BYTE to ReaderStates.RCVCHK) { byte ->
                    require(byte != null)
                    addChecksum(byte)
                }
            }
            whenState(ReaderStates.RCVDATA) {
                onEvent(ReaderEvents.BYTE) { byte ->
                    require(byte != null)
                    addByte(byte)
                }
                onEvent(ReaderEvents.CTRL to ReaderStates.RCVPCKT, guard = { byte -> byte == CharacterConstants.ETX }) {
                    endField()
                }
                onEvent(ReaderEvents.ESC to ReaderStates.RCVESC) {}
            }
            whenState(ReaderStates.RCVESC) {
                onEvent(ReaderEvents.ESC to ReaderStates.RCVDATA) {
                    addByte(CharacterConstants.ESC)
                }
                onEvent(ReaderEvents.CTRL to ReaderStates.RCVDATA) { byte ->
                    require(byte != null)
                    addByte(byte)
                }
            }
            whenState(ReaderStates.RCVCHK) {
                onEvent(ReaderEvents.BYTE) { byte ->
                    require(byte != null)
                    addChecksum(byte)
                }
                onEvent(ReaderEvents.ESC to ReaderStates.RCVCHKESC) {}
                onEvent(ReaderEvents.CTRL to ReaderStates.CHKSUM, guard = { byte -> byte == CharacterConstants.EOT }) {
                    checksum()
                }
            }
            whenState(ReaderStates.CHKSUM) {
                automatic(ReaderStates.END, guard = { !checksumValid }) {
                    sendNACK()
                }
                automatic(ReaderStates.END, guard = { checksumValid }) {
                    sendACK()
                }
            }
            whenState(ReaderStates.RCVCHKESC) {
                onEvent(ReaderEvents.ESC to ReaderStates.RCVCHK) {
                    addChecksum(CharacterConstants.ESC)
                }
                onEvent(ReaderEvents.CTRL to ReaderStates.RCVCHK) { byte ->
                    require(byte != null)
                    addChecksum(byte)
                }
            }
        }.build()
    }

    private val fsm = definition.create(packetHandler)
    fun receiveByte(byte: Int) {
        when (byte) {
            CharacterConstants.ESC -> fsm.sendEvent(ReaderEvents.ESC, CharacterConstants.ESC)
            CharacterConstants.SOH,
            CharacterConstants.EOT,
            CharacterConstants.ETX,
            CharacterConstants.STX,
            CharacterConstants.ACK,
            CharacterConstants.NAK -> fsm.sendEvent(ReaderEvents.CTRL, byte)
            else -> fsm.sendEvent(ReaderEvents.BYTE, byte)
        }
    }
}

Tests

class PacketReaderTests {
    @Test
    fun `test reader expect ACK`() {
        val protocolHandler = mockk<ProtocolHandler>()
        every { protocolHandler.sendACK() } just Runs
        val packetReader = Packet(protocolHandler)
        val fsm = PacketReaderFSM(packetReader)
        val stream =
            listOf(
                CharacterConstants.SOH,
                CharacterConstants.STX,
                'A'.toInt(),
                'B'.toInt(),
                'C'.toInt(),
                CharacterConstants.ETX,
                'A'.toInt(),
                CharacterConstants.EOT
            )
        stream.forEach { byte ->
            fsm.receiveByte(byte)
        }
        packetReader.print()
        verify { protocolHandler.sendACK() }
        assertTrue { packetReader.checksumValid }
    }

    @Test
    fun `test reader ESC expect ACK`() {
        val protocolHandler = mockk<ProtocolHandler>()
        every { protocolHandler.sendACK() } just Runs
        val packetReader = Packet(protocolHandler)
        val fsm = PacketReaderFSM(packetReader)
        val stream =
            listOf(
                CharacterConstants.SOH,
                CharacterConstants.STX,
                'A'.toInt(),
                CharacterConstants.ESC,
                CharacterConstants.EOT,
                'C'.toInt(),
                CharacterConstants.ETX,
                'A'.toInt(),
                CharacterConstants.EOT
            )
        stream.forEach { byte ->
            fsm.receiveByte(byte)
        }
        packetReader.print()
        verify { protocolHandler.sendACK() }
        assertTrue { packetReader.checksumValid }
        assertTrue { packetReader.fields.size == 1 }
        assertTrue { packetReader.fields[0].size == 3 }
        assertTrue { packetReader.fields[0][1].toInt() == CharacterConstants.EOT }
    }

    @Test
    fun `test reader ESC expect NACK`() {
        val protocolHandler = mockk<ProtocolHandler>()
        every { protocolHandler.sendNACK() } just Runs
        val packetReader = Packet(protocolHandler)
        val fsm = PacketReaderFSM(packetReader)
        val stream =
            listOf(
                CharacterConstants.SOH,
                CharacterConstants.STX,
                'A'.toInt(),
                CharacterConstants.ESC,
                'C'.toInt(),
                CharacterConstants.ETX,
                'A'.toInt(),
                CharacterConstants.EOT
            )
        stream.forEach { byte ->
            fsm.receiveByte(byte)
        }
        packetReader.print()
        verify { protocolHandler.sendNACK() }
        assertFalse { packetReader.checksumValid }
        assertTrue { packetReader.fields.isEmpty() }
    }

    @Test
    fun `test reader expect NACK`() {
        val protocolHandler = mockk<ProtocolHandler>()
        every { protocolHandler.sendNACK() } just Runs
        val packetReader = Packet(protocolHandler)
        val fsm = PacketReaderFSM(packetReader)
        val stream =
            listOf(
                CharacterConstants.SOH,
                CharacterConstants.STX,
                'A'.toInt(),
                'B'.toInt(),
                'C'.toInt(),
                CharacterConstants.ETX,
                'B'.toInt(),
                CharacterConstants.EOT
            )
        stream.forEach { byte ->
            fsm.receiveByte(byte)
        }
        packetReader.print()
        verify { protocolHandler.sendNACK() }
        assertFalse { packetReader.checksumValid }
    }

    @Test
    fun `test reader multiple fields expect ACK`() {
        val protocolHandler = mockk<ProtocolHandler>()
        every { protocolHandler.sendACK() } just Runs
        val packetReader = Packet(protocolHandler)
        val fsm = PacketReaderFSM(packetReader)
        val stream =
            listOf(
                CharacterConstants.SOH,
                CharacterConstants.STX,
                'A'.toInt(),
                'B'.toInt(),
                'C'.toInt(),
                CharacterConstants.ETX,
                CharacterConstants.STX,
                'D'.toInt(),
                'E'.toInt(),
                'F'.toInt(),
                CharacterConstants.ETX,
                'A'.toInt(),
                'D'.toInt(),
                CharacterConstants.EOT
            )
        stream.forEach { byte ->
            fsm.receiveByte(byte)
        }
        packetReader.print()
        verify { protocolHandler.sendACK() }
        assertTrue { packetReader.checksumValid }
        assertTrue { packetReader.fields.size == 2 }
    }
}

Immutable Context Example

In the case where you want a pure functional context that is immutable the following example will provide what you need:

Immutable Context

The immutable context doesn’t modify a variable. It returns a copy containing the new context

data class ImmutableLock(val locked: Int = 1) {

    fun lock(): ImmutableLock {
        require(locked == 0)
        println("Lock")
        return copy(locked + 1)
    }

    fun doubleLock(): ImmutableLock {
        require(locked == 1)
        println("DoubleLock")
        return copy(locked + 1)
    }

    fun unlock(): ImmutableLock {
        require(locked == 1)
        println("Unlock")
        return copy(locked - 1)
    }

    fun doubleUnlock(): ImmutableLock {
        require(locked == 2)
        println("DoubleUnlock")
        return copy(locked - 1)
    }

    override fun toString(): String {
        return "Lock(locked=$locked)"
    }
}

Definition

The definition only exposes one method to handle an event given a context and return the new context.

class ImmutableLockFSM() {

    companion object {
        fun handleEvent(context: ImmutableLock, event: LockEvents): ImmutableLock {
            val fsm = definition.create(context)
            return fsm.sendEvent(event, context) ?: error("Expected context not null")
        }

        private val definition = functionalStateMachine(
            LockStates.values().toSet(),
            LockEvents.values().toSet(),
            ImmutableLock::class
        ) {
            defaultInitialState = LockStates.LOCKED
            initialState {
                when (locked) {
                    0 -> LockStates.UNLOCKED
                    1 -> LockStates.LOCKED
                    2 -> LockStates.DOUBLE_LOCKED
                    else -> error("Invalid state locked=$locked")
                }
            }
            default {
                action { state, event, _ ->
                    println("Default action for state($state) -> on($event) for $this")
                    this
                }
                onEntry { startState, targetState, _ ->
                    println("entering:$startState -> $targetState for $this")
                }
                onExit { startState, targetState, _ ->
                    println("exiting:$startState -> $targetState for $this")
                }
            }
            whenState(LockStates.LOCKED) {
                onEvent(LockEvents.LOCK to LockStates.DOUBLE_LOCKED) {
                    doubleLock()
                }
                onEvent(LockEvents.UNLOCK to LockStates.UNLOCKED) {
                    unlock()
                }
            }
            whenState(LockStates.DOUBLE_LOCKED) {
                onEvent(LockEvents.UNLOCK to LockStates.LOCKED) {
                    doubleUnlock()
                }
            }
            whenState(LockStates.UNLOCKED) {
                onEvent(LockEvents.LOCK to LockStates.LOCKED) {
                    lock()
                }
            }
        }.build()
    }
}

operator fun ImmutableLock.plus(event: LockEvents): ImmutableLock {
    return ImmutableLockFSM.handleEvent(this, event)
}

Usage

We also added an overloaded plus operator. The provides for a way of signaling the application of event to context.

        val lock = ImmutableLock(0)
        val locked = lock + LockEvents.LOCK
        assertEquals(locked.locked, 1)
        val doubleLocked = locked + LockEvents.LOCK
        assertEquals(doubleLocked.locked, 2)

In the case where the argument to the state action will be something other than the context you can use a Pair and even when the state is needed separately:

    val locked = ImmutablePayingTurnstile(50)
    val (state, newTurnStile) = locked + (COIN to 10)

This assumes the argument is defined as type Int and the return as Pair<LockStates,ImmutablePayingTurnstile> The handleEventMethod will be:

    fun handleEvent(context: ImmutablePayingTurnstile, event: Pair<LockEvents, Int>): Pair<LockStates,ImmutablePayingTurnstile> {
        val fsm = definition.create(context)
        val result = fsm.sendEvent(event.first, event.second) ?: error("Expected context not null")
        return Pair(fsm.currentState, result)
    }

Secure Turnstile with Timeout Example

We updated the Secure Turnstile with a timeout condition.

Timeout Secure Turnstile FSM

State Table

TimerSecureTurnstileFSM State Map

Start Event[Guard] Target Action

LOCKED

CARD [{cardId→requireNotNull(cardId);isOverrideCard(cardId)&&overrideActive;}]

LOCKED

{
cancelOverride()
}

LOCKED

CARD [{cardId→requireNotNull(cardId);isOverrideCard(cardId);}]

LOCKED

{
activateOverride()
}

LOCKED

CARD [{cardId→requireNotNull(cardId);overrideActive||isValidCard(cardId);}]

UNLOCKED

{
unlock()
}

LOCKED

CARD [{cardId→requireNotNull(cardId){"cardId is required"};!isValidCard(cardId);}]

LOCKED

{cardId->
requireNotNull(cardId)
invalidCard(cardId)
}

UNLOCKED

<<timeout = 500>>

LOCKED

{
println("Timeout. Locking")
lock()
}

UNLOCKED

CARD [{cardId→requireNotNull(cardId);isOverrideCard(cardId);}]

LOCKED

{
lock()
}

UNLOCKED

PASS

LOCKED

{
lock()
}

Context class

The context class doesn’t make decisions about the behaviour of the turnstile. The context will provide information about the state of turnstile and validity of cards The context class stores values and will update value in very specific ways.

class TimerSecureTurnstile {
    var locked: Boolean = true
        private set
    var overrideActive: Boolean = false
        private set

    fun activateOverride() {
        overrideActive = true
        println("override activated")
    }

    fun cancelOverride() {
        overrideActive = false
        println("override canceled")
    }

    fun lock() {
        println("lock")
        locked = true
        overrideActive = false
    }

    fun unlock() {
        println("unlock")
        locked = false
        overrideActive = false
    }

    fun buzzer() {
        println("BUZZER")
    }

    fun invalidCard(cardId: Int) {
        println("Invalid card $cardId")
    }

    fun isOverrideCard(cardId: Int): Boolean {
        return cardId == 42
    }

    fun isValidCard(cardId: Int): Boolean {
        return cardId % 2 == 1
    }
}

States and Events

enum class SecureTurnstileEvents {
    CARD,
    PASS
}

State machine definition packaged

class TimerSecureTurnstileFSM(private val secureTurnstile: TimerSecureTurnstile) {
    companion object {
        val definition = asyncStateMachine(
            SecureTurnstileStates.values().toSet(),
            SecureTurnstileEvents.values().toSet(),
            TimerSecureTurnstile::class,
            Int::class
        ) {
            defaultInitialState = SecureTurnstileStates.LOCKED
            initialState { if (locked) SecureTurnstileStates.LOCKED else SecureTurnstileStates.UNLOCKED }
            default {
                action { _, _, _ ->
                    buzzer()
                }
            }
            whenState(SecureTurnstileStates.LOCKED) {
                onEvent(SecureTurnstileEvents.CARD, guard = { cardId ->
                    requireNotNull(cardId)
                    isOverrideCard(cardId) && overrideActive
                }) {
                    cancelOverride()
                }
                onEvent(SecureTurnstileEvents.CARD, guard = { cardId ->
                    requireNotNull(cardId)
                    isOverrideCard(cardId)
                }) {
                    activateOverride()
                }
                onEvent(SecureTurnstileEvents.CARD to SecureTurnstileStates.UNLOCKED,
                    guard = { cardId ->
                        requireNotNull(cardId)
                        overrideActive || isValidCard(cardId)
                    }) {
                    unlock()
                }
                onEvent(SecureTurnstileEvents.CARD, guard = { cardId ->
                    requireNotNull(cardId) { "cardId is required" }
                    !isValidCard(cardId)
                }) { cardId ->
                    requireNotNull(cardId)
                    invalidCard(cardId)
                }
            }
            whenState(SecureTurnstileStates.UNLOCKED) {
                timeout(SecureTurnstileStates.LOCKED, 500L) {
                    println("Timeout. Locking")
                    lock()
                }
                onEvent(SecureTurnstileEvents.CARD to SecureTurnstileStates.LOCKED, guard = { cardId ->
                    requireNotNull(cardId)
                    isOverrideCard(cardId)
                }) {
                    lock()
                }
                onEvent(SecureTurnstileEvents.PASS to SecureTurnstileStates.LOCKED) {
                    lock()
                }
            }
        }.build()
    }

    private val fsm = definition.create(secureTurnstile)
    suspend fun card(cardId: Int) = fsm.sendEvent(SecureTurnstileEvents.CARD, cardId)
    suspend fun pass() = fsm.sendEvent(SecureTurnstileEvents.PASS)
    fun allowEvent(): Set<String> = fsm.allowed().map { it.name.toLowerCase() }.toSet()
}

Test

    @Test
    fun `test timeout`() {
        val turnstile = TimerSecureTurnstile()
        val fsm = TimerSecureTurnstileFSM(turnstile)
        assertTrue { turnstile.locked }
        println("Card 1")
        runBlocking {
            fsm.card(1)
            println("Assertion")
            assertTrue { !turnstile.locked }
            println("Delay")
            delay(1000L)
            println("Assertion")
            assertTrue { turnstile.locked }
        }
    }

Further thoughts

The escape handling from the packet reader could be a separate statemap. The current implementation has a problem in that the value should be added to different buffer. It may be worthwhile to have a mechanism for providing a different substate context to take care of this kind of situation. An interface/class exposed to the substate instead of the full context. In this example Block will be exposed to the stateMap. The stateMap can then pop after adding the byte. In the case of an invalid character the statemap will still transition to the end state.

Getting Started

Repository

Use this repository for SNAPSHOT builds. Releases are on Maven Central

repositories {
    maven {
        url 'https://oss.sonatype.org/content/groups/public'
    }
}

Dependencies

Kotlin/JVM Projects

dependencies {
    implementation 'io.jumpco.open:kfsm-jvm:1.0.2'
}

KotlinJS Projects

dependencies {
    implementation 'io.jumpco.open:kfsm-js:1.0.2'
}

Kotlin/Native Projects using WASM

dependencies {
    implementation 'io.jumpco.open:kfsm-wasm32:1.0.2'
}

Kotlin/Native Projects using LinuxX64

dependencies {
    implementation 'io.jumpco.open:kfsm-linuxX64:1.0.2'
}

Kotlin/Native Projects using MinGW64

dependencies {
    implementation 'io.jumpco.open:kfsm-mingwX64:1.0.2'
}

Kotlin/Native Projects using macOS

dependencies {
    implementation 'io.jumpco.open:kfsm-macosX64:1.0.2'
}

Finite state machines.

This section will describe the life-cycle and philosophy behind this implementation of Finite state machines.

Definition

An FSM is defined using a DSL as described further in this document.

During this definition the user describes state types, events types as well as the behaviour of the FSM to events and how it will operate on a provided context.

The state and event types should be a value types.

The events may receive an argument and may return a value. The argument for all event must be the same type. However sealed classes can be used to simplify the representation of different argument types.

The state machine will define one or more state maps each representing a subset of possible states of the same type.

The state machine will receive events, constrained by set of events. The events does not have to be enum, only a value with equals and hashCode implementations.

The state machine will apply actions to a context. The context represents the work that need to be performed.

An event may trigger a state transition which defines actions to apply when the state machine is in a specific state.

Transitions may have a guard expression that has to evaluate to true to allow execution of transition.

An automatic transition can be defined on a state and has no events. Automatic transitions will be executed if the specific state is current after the exit action.

A timeout transition can be defined on a state and is triggered a configured amount of time after the entry into the state. The transition will be trigger upon timeout unless a configured guard expression doesn’t evaluate to true.

The state can be any value type providing equals and hashCode, even though we have been using enum class most examples.

Instance

The philosophy behind the design is that the finite state machine can be used when needed by creating an instance using the definition and providing it with the context and optionally a previously externalized state. The state machine can also be defined to derive the current state from the provided context.

Then events are sent to the FSM with an argument if needed and this may trigger the defined actions.

The FSM provides for a query to determine which events are allowed for the current or a given state. This is useful when user interaction controls need to be disabled or in the case of a back-end application using HATEOAS can add links limited to allowed events.

If the FSM is configured with timeout transitions the instance will need to remain active. This is typically only used in UI or embedded interactive applications.

If the FSM is used in a stateless environment that is driven by requests or events the state can be externalised after use if it isn’t derived from the provided context. The externalised state is a list of pairs making up the state type and the name of the associated statemap.

Named Maps and Push / Pop Transitions

A named map represents a set of states that is grouped together. Named maps can be visited by using a push transition and return using a pop transition. A push transition requires a map name and a target state along with the normal event and optional guard and action. The current state map will be pushed on a stack and a statemap instance will be created using the named definition. Events will be processed according to the transition rules of the named map.

DSL

The DSL provides a way of configuring the statemachine. The statemachine supports:

  • Transitions: internal and external

    • Transitions are external when the target state is defined even if the target is same as current.

  • Guard expressions

  • Entry and exit actions per state and globally

  • Default actions per state and globally

  • Named statemaps

  • Push and pop transitions

  • Automatic transitions

  • Timeout transitions

  • Actions as suspend functions.

All configuration calls are eventually applied to StateMachineBuilder

stateMachine

The top level element is stateMachine either by using the function

There are 3 overloaded variations on stateMachine for providing Any as the return type and the argument to events/actions in cases where they are not used.

// using global function
val definition = stateMachine(
    State.values().toSet(),
    Event.values().toSet(),
    ContextType::class,
    ArgType::class,
    ReturnType::class
) {
    defaultInitialState = State.S1 // optional start state
    default { // global defaults
    }
    initialState { // initial state expression
    }
    initialStates { // define expression for deriving state stack for nested maps.
    }
    stateMap { // define named statemap
    }
    whenState { // state definition
    }
}.build()

functionalStateMachine

The top level element is functionalStateMachine either by using the function

It provides the same as stateMachine but in this case the Context, argument and return types are all the same.

// using global function
val definition = stateMachine(
    State.values().toSet(),
    Event.values().toSet(),
    ContextType::class
) {
    defaultInitialState = State.S1 // optional start state
    default { // global defaults
    }
    initialState { // initial state expression
    }
    initialStates { // define expression for deriving state stack for nested maps.
    }
    stateMap { // define named statemap
    }
    whenState { // state definition
    }
}.build()

asyncStateMachine

This method provides for creating a statemachine definition and instances that have suspend functions as actions. This also adds the support for timeout on the state map handler.

  • asyncStateMachine

  • link:javadoc/kfsm/io.jumpco.open.kfsm.async/-async-state-machine-builder/state-machine.html

There are 3 overloaded variations on stateMachine for providing Any as the return type and the argument to events/actions in cases where they are not used.

// using global function
val definition = asyncStateMachine(
    State.values().toSet(),
    Event.values().toSet(),
    ContextType::class,
    ArgType::class,
    ReturnType::class
) {
    defaultInitialState = State.S1 // optional
    default { // global defaults
    }
    initialState { // initial state expression
    }
    initialStates { // define expression for deriving state stack for nested maps.
    }
    stateMap { // define named statemap
    }
    whenState { // state definition
    }
}.build()

asyncFunctionalStateMachine

This is the same as asyncStateMachine except that Context, argument and return types are all the same.

// using global function
val definition = asyncFunctionalStateMachine(
    State.values().toSet(),
    Event.values().toSet(),
    ContextType::class
) {
    defaultInitialState = State.S1 // optional
    default { // global defaults
    }
    initialState { // initial state expression
    }
    initialStates { // define expression for deriving state stack for nested maps.
    }
    stateMap { // define named statemap
    }
    whenState { // state definition
    }
}.build()

default

Provide default configuration for entry and exit actions as well as a default action.

Example:

default {
    action { // global action
    }
    onEntry { // global state entry action
    }
    onExit { // global state exit action
    }
    onEvent { // default transitions
    }
}

action

Provide a lambda C.(S, E, A?)→R? that will be invoked when no other transitions are matched.

Example:

action { currentState, event, arg -> // global default action
    contextFunction()
    anotherContextFunction()
}

onEntry

Provide a lambda C.(S,S,A?) → Unit that will be invoked before a change in the state of the FSM. Global entry actions will be called for all external transitions after state specific entry actions.

Example:

onEntry { fromState, targetState, arg ->
    entryAction()
}

onExit

Provide a lambda C.(S,S,A?) → Unit that will be invoked after a change in the state of the FSM. Global exit actions will be called for all external transitions after state specific entry actions.

Example:

onExit { fromState, targetState, arg ->
    exitAction()
}

onEvent

This defines a transition when a specific event is receive and no other transition was matched. There are 2 variations, the first is internal and doesn’t define a target state, the second is external and defines a target state. In both cases the lambda type is C.(A?) → R?

Example:

onEvent(Event.EVENT) { arg -> // default internal state action for given event
    someFunction()
}

onEvent(Event.EVENT to State.STATE) { arg -> // default external state action for given event
    anotherFunction()
}

initialState

Provide a lambda C.() → S that will determine the state of the state machine.

Example:

initialState {
    when(flag) {
        1 -> State.S1
        2 -> State.S2
        else -> error("Invalid state")
    }
}

initialStates

One of initialState or initialStates must be provided. When a state-machine has named maps the initialStates must be provided.

Provide a lambda C.() → StateMapList<S> that will determine the state of the state machine and map names that should be placed on the stack.

Example:

initialStates {
    mutableListOf<StateMapItem<PayingTurnstileStates>>().apply {
        if (locked) {
            this.add(PayingTurnstileStates.LOCKED to "default")
        } else {
            this.add(PayingTurnstileStates.UNLOCKED to "default")
        }
        if (coins > 0) {
            this.add(PayingTurnstileStates.COINS to "coins")
        }
    }.toMap()
}

whenState

Each whenState block decribes the transitions for a given state.

Example:

whenState(State.STATE) {
    default { // default action for State.STATE
    }
    onEntry { // entry action for State.STATE
    }
    onExit { // exit action for State.STATE
    }
    onEvent(Event.EV2 to State.S1, guard = {flag == 1 }) { // external transition with guard expression
    }
    onEventPush(Event.EV2, "mapName", State.S1, gaurd = { flag == 1}) { // push transition to new map with guard expression
    }
    onEventPop(Event.EV3, "newMap", State.S3) { // pop transition leading into new push transition while executing current action only
    }
    automatic(State.S1, guard = { flag == 1}) { // automatic transition to new state when guard is met
    }
    // timeout is limited to AsyncStateMachineBuilder
    timeout(State.S1, timeout, [guard = { expression }]) { // transition to S1 when timeout is triggered and guard is true
    }
    timeoutPop(State.S1, timeout, [guard = { expression }]) { // transition to S1 when timeout is triggered and guard is true
    }
    timeoutPush(State.S1, "mapName",  timeout, [guard = { expression }]) { // transition to S1 when timeout is triggered and guard is true
    }
}

default

A state block may have one default action which is a lambda of type C.(S,E,Array<out Any>) → Unit that is invoked when no other transition is found for the given state and event and guard expressions.

Example:

default { fromState, event, arg -> // default state action
    someDefaultAction()
}

onEntry

This defines a lambda of type C.(S,S,A?) → R? that will be invoked after the transition action for an external transition.

Example:

onEntry { fromState, targetState, arg -> // state entry action
    println("Entering:$targetState from $fromState with $arg")
}

onExit

This defines a lambda of type C.(S,S,A?) → Unit that will be invoked before the transition action for an external transitions.

Example:

onExit { fromState, targetState, arg -> // state exit action
    println("Exiting:$fromState to $targetState with $arg")
}

automatic

There are 2 variations of automatic transitions: Those with and without guards. An automatic transition is exercises after the state machine has completed processing a transition. All automatic transitions attached to a given state will be invoked if their guards are met.

Example:

whenState(State.S1) {
    automatic(State.S1, guard = { flag == 1}) { // automatic transition to new state when guard is met
    }
    automatic(State.S1) { // automatic transition to new state
    }
}

automaticPop

There are 6 variations of automatic transitions: Those with and without guards, those with and without targetMaps which will lead to a new push transition.

Example:

whenState(State.S1) {
    automaticPop { // pop when S1
    }
    automaticPop(guard= { flag == 1 }) { // pop when S1 and guard is true
    }
    automaticPop(State.S2, guard = { flag == 1 }) { // automatic pop transition to new state when guard is met
    }
    automaticPop(State.S2) { // automatic pop transition to new state
    }
    automaticPop("map1", State.S2) { // automatic pop transition to push transition to new state in target map
    }
    automaticPop("map1", State.S2, guard={flag == 1 }) { // automatic pop transition to push transition to new state in target map
    }
}

automaticPush

There are 2 variations of automatic transitions: Those with and without guards

Example:

whenState(State.S1) {
    automaticPush("map1", State.S2) { // automatic push transition to S2 in target map "map1"
    }
    automaticPush("map1", State.S2, guard={flag == 1 }) { // automatic push transition to S2 in target map "map1" if guard is true
    }
}

onEvent

There are 4 variations of transitions: External and internal, with and without a guard expression.

This defines a transition action for a given event. For an external transition a target state must be provided, while an internal transition must have no targetState. An optional guard expression can be provided. The order in which the DSL encounters guard expression determine the evaluation order. The first matching guard expression will determine the transition that will be used. Their may be only one transition without a guard expression.

Examples:

onEvent(Event.EV1, guard = { flag == 1 }) { arg -> // internal transition with guard expression
}
onEvent(Event.EV1 to State.S2, guard = { flag == 2}) { arg -> // external transition with guard expression
}
onEvent(Event.EV1) { arg -> // internal transition
}
onEvent(Event.EV2 to State.S2) { arg -> // external transition
}

onEventPush

There are 2 variations of automatic transitions: Those with and without guards Example:

whenState(State.S1) {
    onEventPush(Event.EV2, "mapName", State.S2) { // push transition to S2 in new map "mapName"
    }
    onEventPush(Event.EV2, "mapName", State.S2, gaurd = { flag == 1}) { // push transition to S2 in new map "mapName" with guard expression
    }
}

onEventPop

There are 6 variations of popTransitions to provide for with and without guards, with and without a new state and with and without a targetMap that will result in a new push transition.

Example:

whenState(State.S1) {
    onEventPop(Event.EV3) { // pop transition without targetState
    }
    onEventPop(Event.EV3, guard={ flag == 1 }) { // pop transition without targetState and guard expression
    }
    onEventPop(Event.EV3 to State.S2) { // pop transition on EV3 changing state to S2
    }
    onEventPop(Event.EV3 to State.S2, guard={ flag == 1 }) { // pop transition on EV3 changing state to S2 with a guard expression
    }
    onEventPop(Event.EV3, "newMap", State.S3) { // pop transition leading into new push transition to S3 in "newMap"
    }
    onEventPop(Event.EV3, "newMap", State.S3, guard={ flag == 1 }) { // pop transition leading into new push transition to S3 in "newMap" with a guard expression
    }
}

timeout

There are 2 versions. One adds the support for a guard expression that will be evaluated and the action will only trigger if the guard evaluates true.

timeoutPop

There are 5 versions. Combinations exist to add the optional guard expression and the targetMap.

timeoutPush

There are 2 variations, the 2nd adds the optional guard expression.

Operation

When the FSM was defined and instance can be created providing a context and optional externalized stated.

When sendEvent is called the state machine applies the event to the current state map. The current state map is usually top-level state map unless you have defined named maps and used a push transition.

FSM sequence

Send Event

The normal operation is to invoke the following actions:

  • sendEvent

    • if(external) exitAction()

    • action()

    • if(external) entryAction()

The exit action is chosen from one of:

  • stateMap/whenState/onExit

  • stateMap/whenState/default/onExit

  • stateMap/default/onExit

The action is chosen from one of:

  • stateMap/whenState/onEvent

  • stateMap/whenState/default/action

  • stateMap/default/action

The entry action is chosen from one of:

  • stateMap/whenState/onEntry

  • stateMap/whenState/default/onEntry

  • stateMap/default/onEntry

The combination of current state and event determines a set of transition rules that have been applied to that combination by one or more definitions. If the transition rules contains guard transitions the guard expressions are evaluated until one evaluates true. The onEntry and onExit action are only invoked for external transitions.

External transitions have an explicit target state defined. If the target state is not defined it is an internal transition.

The currentState determines the exit action. The target state determines the entry action.

Notes on concurrency and coroutines.

The implementations of the AsyncTimer on the different platform work slightly differently depending on the platform.

JVM

The code in the JVM implementation uses CoroutineScope(Dispatchers.Default) to launch the trigger. Your action will be responsible for ensuring proper handling if a UI thread is impacted.

JavaScript

The current code assumes you are executing in the browser it uses window.setTimeout to configure a function that will use `GlobalScope to launch the trigger.