This work is licensed under Apache License 2.0

This project will explore many ways to visualize KFSM definitions.

The Kotlin source containing the FSM is parsed with an ANTLR parser generated at build time from the Kotlin grammar (src/main/antlr, taken from the develop branch of kotlin-spec, see KOTLIN_SPEC_COMMIT). No external grammar tools need to be built or installed. Run ./download-kotlin-spec-tools.sh to update the grammar from kotlin-spec develop and rebuild.

Getting Started

repositories {
    maven {
        url 'https://oss.sonatype.org/content/groups/public/'
    }
    mavenCentral()
}
dependencies {
    testImplementation 'org.infosmid:kfsm-viz:2.0.0'
}

You can implement a test method to generate the required statemachine model artifacts, or you can configure the kfsm-viz-plugin for Gradle to produce visualizations.

PlantUML Statemachine Diagram

By parsing the source code we can extract the details of the guard expressions and actions. This requires that you place one definition inside of one class. There is a chance for the parser based solution to misbehave. We request that you log an issue with the problematic FSM definition.

@Test
fun produceVisualizationTurnstileFSM() {
    println("== TurnStile")
    val visualisation = parseStateMachine("TurnstileFSM", File("src/test/kotlin/Turnstile.kt"))

    File("generated-parsed", "turnstile.plantuml").writeText(plantUml(visualisation, true)) (1)

    File("generated-parsed", "turnstile-simple.plantuml").writeText(plantUml(visualisation, false)) (2)
}
  1. Detailed Included in State Diagram

  2. Detail like actions and guards ignored

AsciiDoctor State Table from VisualStatemachineDefinition

@Test
fun produceVisualizationTurnstileFSM() {
    println("== TurnStile")
    val visualisation = parseStateMachine("TurnstileFSM", File("src/test/kotlin/Turnstile.kt"))
    File("generated-parsed", "turnstile.adoc").writeText(asciiDoc(visualisation))
}

Generated from parsed VisualStateDefinition

We generated an asciidoc table as well as a plantuml statemachine model.

Simple Turnstile FSM

Simple State Diagram
simple turnstile simple
Detailed State Diagram
simple turnstile

SimpleTurnstileFSM State Chart

SimpleTurnstileFSM State Map

Start Event[Guard] Target Action

[start]

UNLOCKED

LOCKED

COIN

UNLOCKED

{
unlock()
}

UNLOCKED

PASS

LOCKED

{
lock()
}

UNLOCKED

COIN

UNLOCKED

{
returnCoin()
}

Paying Turnstile FSM

Simple State Diagram
paying turnstile simple
Detailed State Diagram
paying turnstile

PayingTurnstileFSM State Chart

PayingTurnstileFSM State Map

Start Event[Guard] Target Action

[start]

UNLOCKED

LOCKED

COIN

COINS

{
coin(it)
unlock()
reset()
}

LOCKED

COIN [{requiredCoins >= coins+(it?:error("argument required"))}]

COINS

{
coin(it)
println("Coins=$coins, Please add ${requiredCoins-coins}")
}

UNLOCKED

COIN

UNLOCKED

{
returnCoin(coin(it))
}

UNLOCKED

PASS

LOCKED

{
lock()
}

State Map coins

Start Event[Guard] Target Action

COINS

<<automatic>> [{coins > requiredCoins}]

UNLOCKED

{
returnCoin(coins-requiredCoins)
unlock()
reset()
}

COINS

<<automatic>> [{coins == requiredCoins}]

UNLOCKED

{
unlock()
reset()
}

COINS

COIN

COINS

{
coin(it)
if(coins < requiredCoins){
println("Please add ${requiredCoins-coins}")
}
}

Secure Turnstile FSM

Simple State Diagram
secure turnstile simple
Detailed State Diagram
secure turnstile

SecureTurnstileFSM State Chart

SecureTurnstileFSM State Map

Start Event[Guard] Target Action

[start]

UNLOCKED

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

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

LOCKED

{
lock()
}

UNLOCKED

PASS

LOCKED

{
lock()
}

Packer Reader FSM

Simple State Diagram
packet reader simple
Detailed State Diagram
packet reader

PacketReaderFSM State Chart

PacketReaderFSM State Map

Start Event[Guard] Target Action

START

CTRL [{it == CharacterConstants.SOH}]

RCVPCKT

RCVPCKT

CTRL [{it == CharacterConstants.STX}]

RCVDATA

{
addField()
}

RCVPCKT

BYTE

RCVCHK

{
addChecksum(it)
}

RCVDATA

BYTE

RCVDATA

{
addByte(it)
}

RCVDATA

CTRL [{it == CharacterConstants.ETX}]

RCVPCKT

{
endField()
}

RCVDATA

ESC

RCVESC

RCVESC

ESC

RCVDATA

{
addByte(CharacterConstants.ESC)
}

RCVESC

CTRL

RCVDATA

{
addByte(it)
}

RCVCHK

BYTE

RCVCHK

{
addChecksum(it)
}

RCVCHK

ESC

RCVCHKESC

RCVCHK

CTRL [{it == CharacterConstants.EOT}]

CHKSUM

{
checksum()
}

CHKSUM

<<automatic>> [{!checksumValid}]

[*]

{
sendNACK()
}

CHKSUM

<<automatic>> [{checksumValid}]

[*]

{
sendACK()
}

RCVCHKESC

ESC

RCVCHK

{
addChecksum(CharacterConstants.ESC)
}

RCVCHKESC

CTRL

RCVCHK

{byte->
kotlin.require(byte != null)
addChecksum(byte)
}

FAQ

What is with the semi-colons?

For guard expression we have decided the keep them on one line and we made the decision to print the implicit semi-colons to remove ambiguity.