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

250 fsm state diagram mermaid generator #251

Merged
merged 56 commits into from
Feb 24, 2023
Merged
Show file tree
Hide file tree
Changes from 50 commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
9f1a33d
Add mermaid FSM diagram class and function
quekyj Jan 27, 2023
ad81881
Add the mermaid generation via function
quekyj Jan 30, 2023
4fa61e7
Add doc comment to the public function
quekyj Jan 30, 2023
17134f8
Add output path to the mermaid state diagram
quekyj Jan 30, 2023
25b3164
Initialized outputpath variable
quekyj Jan 30, 2023
9b0a524
Add documentation to indicate what is outputPath
quekyj Jan 30, 2023
2a0c64f
Added oven fsm example
quekyj Jan 30, 2023
5d69d0d
Add output path for oven fsm
quekyj Jan 30, 2023
c3aa3ff
Add documentation on asynchronous await
quekyj Jan 30, 2023
4e1b800
Fix write file to synchronous instead of asynchronous
quekyj Jan 30, 2023
c1a143e
Add unit test for verifyMermaidStateDiagram
quekyj Jan 30, 2023
5689fdd
Remove async in documentation
quekyj Jan 30, 2023
2dfc2d6
Fix oven fsm implementation
quekyj Jan 31, 2023
3bef194
Update the dependent
quekyj Feb 13, 2023
27ea1d0
Add naming to the Logic to make sure the FSM have better label
quekyj Feb 13, 2023
dbbebc6
Add logic to counter time complete as well
quekyj Feb 13, 2023
d4bf700
Call generate diagram from main method instead
quekyj Feb 13, 2023
f31b7f3
Add description to the oven fsm
quekyj Feb 13, 2023
f3ba327
Change the MermaidStateDiagram to private class.
quekyj Feb 13, 2023
4b76c1c
Fix code style by dart analyze
quekyj Feb 13, 2023
a99ba4d
Change string to string buffer to avoid keep rewriting diagram var
quekyj Feb 13, 2023
a67eb38
Test bug on dart format cannot pass
quekyj Feb 13, 2023
61bf850
Remove the bracket
quekyj Feb 13, 2023
5a44b2b
Fix the inconsistent of the comment
quekyj Feb 13, 2023
ee7832c
Add mermaid details to the comment
quekyj Feb 13, 2023
7c866c9
Do not save fsm markdown
quekyj Feb 14, 2023
bdc826d
Change the fsm diagram newline and -v2
quekyj Feb 14, 2023
c70fcab
Add indentation to the results
quekyj Feb 14, 2023
63aad95
Fix typo flexibility
quekyj Feb 14, 2023
97a38c0
Added fullstop to all comment
quekyj Feb 14, 2023
c7a619c
Use singular to name enum
quekyj Feb 14, 2023
17c3dc7
Add a fullstop to comment
quekyj Feb 14, 2023
c9e7531
Change counter input and outputs signals to local signals in comment
quekyj Feb 14, 2023
33a4f4f
Make events: and action: as a title
quekyj Feb 14, 2023
5daf6eb
Make main function to return futures of void instead of just void.
quekyj Feb 14, 2023
a1345dc
Update the vcd generated path
quekyj Feb 14, 2023
d93747f
Use Button value instead of hardcoded string
quekyj Feb 14, 2023
329f7c4
Add gitignore to ignore all fsm state diagram
quekyj Feb 15, 2023
cb39ac6
Fix punctuation and snake case
quekyj Feb 15, 2023
9f5873b
Update the variable name to currentState
quekyj Feb 15, 2023
46c3dee
Remove add State function
quekyj Feb 15, 2023
1eaad47
Make diagram a private variable
quekyj Feb 15, 2023
1b60583
make identation a private property as well
quekyj Feb 15, 2023
0f4a6bc
rename to follow conventions
quekyj Feb 15, 2023
86f5262
tighten check the test by verify for identation
quekyj Feb 15, 2023
cb3a5d9
Merge branch 'main' into 250-fsm-state-diagram-mermaid-generator
quekyj Feb 15, 2023
24dd8b6
Allow delete sync to delete the filePath
quekyj Feb 15, 2023
8dfe36b
Use enum for Button and LEDLight
quekyj Feb 20, 2023
ba2181d
Update the variable to private instead
quekyj Feb 20, 2023
3b6cbfd
Added fsm test to example test
quekyj Feb 20, 2023
39e6f85
update the variable to private instead
quekyj Feb 21, 2023
9678690
Add the listener to listen to the dart stream for led change
quekyj Feb 22, 2023
c8843de
Update the fsm state machine
quekyj Feb 22, 2023
79c39eb
Merge branch 'main' into 250-fsm-state-diagram-mermaid-generator
quekyj Feb 22, 2023
84b617a
Merge branch 'main' into 250-fsm-state-diagram-mermaid-generator
quekyj Feb 22, 2023
c96d071
update type and gitignore rules
quekyj Feb 22, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ doc/api/
tmp_test/
*.vcd
.vscode/
quekyj marked this conversation as resolved.
Show resolved Hide resolved
*_fsm.md

# Exceptions
!test/example_icarus_waves.vcd
252 changes: 252 additions & 0 deletions example/oven_fsm.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
/// Copyright (C) 2023 Intel Corporation
quekyj marked this conversation as resolved.
Show resolved Hide resolved
/// SPDX-License-Identifier: BSD-3-Clause
///
/// oven_fsm.dart
/// A simple oven FSM implementation using ROHD.
///
/// 2023 February 13
/// Author: Yao Jing Quek <yao.jing.quek@intel.com>
///

// ignore_for_file: avoid_print
quekyj marked this conversation as resolved.
Show resolved Hide resolved

// Import the ROHD package.
import 'package:rohd/rohd.dart';

// Import the counter module implement in example.dart.
import './example.dart';

// Enumerated type named `OvenState` with four possible states:
// `standby`, `cooking`,`paused`, and `completed`.
enum OvenState { standby, cooking, paused, completed }

// One-hot encoded `Button` using dart enhanced enums.
// Represent start, pause, and resume as integer value 0, 1,
// and 2 respectively.
enum Button {
start(value: 0),
pause(value: 1),
resume(value: 2);

const Button({required this.value});

final int value;
}

// One-hot encoded `LEDLight` using dart enhanced enums.
// Represent yellow, blue, red, and green as integer value 0, 1,
// 2, and 3 respectively.
enum LEDLight {
yellow(value: 0),
blue(value: 1),
red(value: 2),
green(value: 3);

const LEDLight({required this.value});

final int value;
}

// Define a class OvenModule that extends ROHD's abstract Module class.
class OvenModule extends Module {
// A private variable with type StateMachine<OvenState> `oven`.
quekyj marked this conversation as resolved.
Show resolved Hide resolved
//
// Use `late` to indicate that the value will not be null
// and will be assign in the later section.
late StateMachine<OvenState> _oven;

// This oven module receives a `button` and a `reset` input from runtime.
OvenModule(Logic button, Logic reset) : super(name: 'OvenModule') {
// Register inputs and outputs of the module in the constructor.
// Module logic must consume registered inputs and output to registered
// outputs. `led` output also added as the output port.
button = addInput('button', button, width: button.width);
reset = addInput('reset', reset);
final led = addOutput('led', width: button.width);

// An internal clock generator.
final clk = SimpleClockGenerator(10).clk;

// Register local signals, `counterReset` and `en`
// for Counter module.
final counterReset = Logic(name: 'counter_reset');
final en = Logic(name: 'counter_en');

// An internal counter module that will be used to time the cooking state.
// Receive `en`, `counterReset` and `clk` as input.
final counter = Counter(en, counterReset, clk, name: 'counter_module');

// A list of `OvenState` that describe the FSM. Note that
// `OvenState` consists of identifier, events and actions. We
// can think of `identifier` as the state name, `events` is a map of event
// that trigger next state. `actions` is the behaviour of current state,
// like what is the actions need to be shown separate current state with
// other state. Represented as List of conditionals to be executed.
final states = [
// identifier: standby state, represent by `OvenState.standby`.
State<OvenState>(OvenState.standby,
// events:
// When the button `start` is pressed during standby state,
// OvenState will changed to `OvenState.cooking` state.
events: {
Logic(name: 'button_start')
..gets(button
.eq(Const(Button.start.value, width: button.width))):
OvenState.cooking,
},
// actions:
// During the standby state, `led` is change to blue; timer's
// `counterReset` is set to 1 (Reset the timer);
// timer's `en` is set to 0 (Disable value update).
actions: [
led < LEDLight.blue.value,
counterReset < 1,
en < 0,
]),

// identifier: cooking state, represent by `OvenState.cooking`.
State<OvenState>(OvenState.cooking,
// events:
// When the button `paused` is pressed during cooking state,
// OvenState will changed to `OvenState.paused` state.
//
// When the button `counter` time is elapsed during cooking state,
// OvenState will changed to `OvenState.completed` state.
events: {
Logic(name: 'button_pause')
..gets(button
.eq(Const(Button.pause.value, width: button.width))):
OvenState.paused,
Logic(name: 'counter_time_complete')..gets(counter.val.eq(4)):
OvenState.completed
},
// actions:
// During the cooking state, `led` is change to yellow; timer's
// `counterReset` is set to 0 (Do not reset);
// timer's `en` is set to 1 (Enable value update).
actions: [
led < LEDLight.yellow.value,
counterReset < 0,
en < 1,
]),

// identifier: paused state, represent by `OvenState.paused`.
State<OvenState>(OvenState.paused,
// events:
// When the button `resume` is pressed during paused state,
// OvenState will changed to `OvenState.cooking` state.
events: {
Logic(name: 'button_resume')
..gets(button
.eq(Const(Button.resume.value, width: button.width))):
OvenState.cooking
},
// actions:
// During the paused state, `led` is change to red; timer's
// `counterReset` is set to 0 (Do not reset);
// timer's `en` is set to 0 (Disable value update).
actions: [
led < LEDLight.red.value,
counterReset < 0,
en < 0,
]),

// identifier: completed state, represent by `OvenState.completed`.
State<OvenState>(OvenState.completed,
// events:
// When the button `start` is pressed during completed state,
// OvenState will changed to `OvenState.cooking` state.
events: {
Logic(name: 'button_start')
..gets(button
.eq(Const(Button.start.value, width: button.width))):
OvenState.cooking
},
// actions:
// During the start state, `led` is change to green; timer's
// `counterReset` is set to 1 (Reset value);
// timer's `en` is set to 0 (Disable value update).
actions: [
led < LEDLight.green.value,
counterReset < 1,
en < 0,
])
];

// Assign the oven StateMachine object to public variable declared.
quekyj marked this conversation as resolved.
Show resolved Hide resolved
_oven = StateMachine<OvenState>(clk, reset, OvenState.standby, states);
}

// An ovenStateMachine that represent in getter.
StateMachine<OvenState> get ovenStateMachine => _oven;
}

Future<void> main({bool noPrint = false}) async {
// Signals `button` and `reset` that mimic user's behaviour of button pressed
// and reset.
//
// Width of button is 2 because button is represent by 2-bits signal.
final button = Logic(name: 'button', width: 2);
final reset = Logic(name: 'reset');

// Build an Oven Module and passed the `button` and `reset`.
final oven = OvenModule(button, reset);

// Generate a Mermaid FSM diagram and save as the name `oven_fsm.md`.
// Note that the extension of the files is recommend as .md or .mmd.
//
// Check on https://mermaid.js.org/intro/ to view the diagram generated.
// If you are using vscode, you can download the mermaid extension.
oven.ovenStateMachine.generateDiagram(outputPath: 'oven_fsm.md');
quekyj marked this conversation as resolved.
Show resolved Hide resolved

// Before we can simulate or generate code with the counter, we need
// to build it.
await oven.build();

// Now let's try simulating!

// Let's start off with asserting reset to Oven.
reset.inject(1);

// Attach a waveform dumper so we can see what happens.
if (!noPrint) {
WaveDumper(oven, outputPath: 'oven.vcd');
}

quekyj marked this conversation as resolved.
Show resolved Hide resolved
// Drop reset at time 25.
Simulator.registerAction(25, () => reset.put(0));

// Press button start => `00` at time 25.
Simulator.registerAction(25, () {
button.put(Button.start.value);
});

// Press button pause => `01` at time 50.
Simulator.registerAction(50, () {
button.put(Button.pause.value);
});

// Press button resume => `10` at time 70.
Simulator.registerAction(70, () {
button.put(Button.resume.value);
});

// Print a message when we're done with the simulation!
Simulator.registerAction(120, () {
if (!noPrint) {
print('Simulation completed!');
}
});

// Set a maximum time for the simulation so it doesn't keep running forever.
Simulator.setMaxSimTime(120);

quekyj marked this conversation as resolved.
Show resolved Hide resolved
// Kick off the simulation.
await Simulator.run();

// We can take a look at the waves now
if (!noPrint) {
print('To view waves, check out waves.vcd with a'
' waveform viewer (e.g. `gtkwave waves.vcd`).');
}
}
69 changes: 68 additions & 1 deletion lib/src/state_machine.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
/// 2022 April 22
/// Author: Shubham Kumar <shubham.kumar@intel.com>
///

import 'dart:collection';
import 'dart:io';
import 'dart:math';

import 'package:rohd/rohd.dart';
Expand All @@ -21,7 +23,8 @@ class StateMachine<StateIdentifier> {
List<State<StateIdentifier>> get states => UnmodifiableListView(_states);
final List<State<StateIdentifier>> _states;

/// A map to store the state identifier as the key and the object as the value
/// A map to store the state identifier as the key
/// and the object as the value.
final Map<StateIdentifier, State<StateIdentifier>> _stateLookup = {};

/// A map to store the state object as the key and the index of the state in
Expand Down Expand Up @@ -99,6 +102,24 @@ class StateMachine<StateIdentifier> {
)
]);
}

/// Generate a FSM state diagram [_MermaidStateDiagram].
quekyj marked this conversation as resolved.
Show resolved Hide resolved
/// Check on https://mermaid.js.org/intro/ to view the diagram generated.
/// If you are using vscode, you can download the mermaid extension.
///
/// Output to mermaid diagram at [outputPath].
void generateDiagram({String outputPath = 'diagram_fsm.md'}) {
final figure = _MermaidStateDiagram(outputPath: outputPath)
..addStartState(resetState.toString());

for (final state in _states) {
for (final entry in state.events.entries) {
figure.addTransitions(state.identifier.toString(),
entry.value.toString(), entry.key.name);
}
}
figure.writeToFile();
}
}

/// Simple class to initialize each state of the FSM.
Expand All @@ -117,3 +138,49 @@ class State<StateIdentifier> {
/// and [actions] associated with that state.
State(this.identifier, {required this.events, required this.actions});
}

/// A state diagram generator for FSM.
///
/// Outputs to vcd format at [outputPath].
class _MermaidStateDiagram {
/// The diagram to be return as String.
late StringBuffer _diagram;

/// The output filepath of the generated state diagram.
final String outputPath;

/// The file to write dumped output waveform to.
final File _outputFile;

// An empty spaces indentation for state.
final _indentation = ' ' * 4;

/// Generate a [_MermaidStateDiagram] that initialized the diagram of
/// mermaid as `stateDiagram`.
///
/// Passed output path to save in custom directory.
_MermaidStateDiagram({this.outputPath = 'diagram_fsm.md'})
: _outputFile = File(outputPath) {
_diagram = StringBuffer('stateDiagram-v2');
}

/// Register a new transition [event] that point the
/// current state [currentState] to next state [nextState].
void addTransitions(String currentState, String nextState, String event) =>
_diagram.write('\n$_indentation$currentState --> $nextState: $event');

/// Register a start state [startState].
void addStartState(String startState) =>
_diagram.write('\n$_indentation[*] --> $startState');

/// Write the object content to [_outputFile] by enclose it with
/// mermaid identifier.
void writeToFile() {
final outputDiagram = StringBuffer('''
```mermaid
$_diagram
```
''');
_outputFile.writeAsStringSync(outputDiagram.toString());
}
}
5 changes: 5 additions & 0 deletions test/example_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import 'package:test/test.dart';

import '../example/example.dart' as counter;
import '../example/fir_filter.dart' as fir_filter;
import '../example/oven_fsm.dart' as fsm;
import '../example/tree.dart' as tree;

void main() {
Expand All @@ -28,4 +29,8 @@ void main() {
test('fir filter example', () async {
await fir_filter.main(noPrint: true);
});

test('fsm example', () async {
await fsm.main(noPrint: true);
});
}
Loading