A small, blazing fast, functional zero-dependency and immutable Finite State Machine (StateCharts) library implemented in Python. Using state machines for your components brings the declarative programming approach to application state.
This is a python port of the popular JavaScript library robot with nearly identical API, still in optimization and with emphasis in general Python and MicroPython support. Tasks:
- Python port, tested in MicroPython and python 3.6 as minimal version, older versions may don't work because ordered dicts requirement (see below for a workaround)
- Same tests of JavaScript ported
- Test passed
- MicroPython support (RP2040 and Unix platform tested)
- Used in a DIY Raspberry Pi Pico W project for a energy meter for a business 😉
- Extensive documentation (meanwhile check oficial robot documentation, has the same API)
- General optimizations
- MicroPython optimizations
- More python tests
- Create native machine code (.mpy) for MicroPython
- (maybe) less dynamic, more performant API for constrained devices in MicroPython
- ...
See thisrobot.life for documentation, but take in account that is in JavaScript.
It is a robust paradigm for general purpose programming, but also recommended for high availability, performance and modeling sw/hw applications, is in use in so many applications such as software, embedded applications, hardware, electronics and many things that keep us alive. From an 8-bit microcontroller to a large application, the use of FSM/StateCharts can be useful to understand, model (and implement) solutions for complex logic and interactions environments.
Historically StateCharts were associated with a Graphical Modeling, but StateCharts don't limit to modeling and fancy drawings, libraries like this can be used to implement fsm/statechart as it in code! Even you don’t need to draw something when you can start to program a FSM (see examples).
If only the Apollo 11 assembler programmers (1969) had known this paradigm (1984) before designing their electronic and user interface systems 🥲
- Welcome to the world of StateCharts
- Highly recommended conference (UI conference, but the explanations can be applied in general): State of the Art Web User Interfaces with State Machines - David Khourshid and his slides
- Another conference from the same author, I like the analogy of Pacman: David Khourshid - Infinitely Better UIs with Finite Automata and his slides
- Wikipedia article about StateCharts and FSMs (Mathematical Model)
- Original paper: STATECHARTS: A VISUAL FORMALISM FOR COMPLEX SYSTEMS
- StateChart Autocoding for Curiosity Rover
- Spanish conference: This is how Apollo 11 was programmed in 1969 about curiosities and complexities of Apollo 11 (and its errors), English subtitles with auto CC
- Apollo Guidance Computer (AGC) and github code, will be interesting if all AGC can be programmed in StateCharts 😛
The API is nearly the same of the JS library, with some changes/gotchas:
- JS objects are replaced with Python equivalents:
- state definitions need to be dictionaries or objects with
__getitem__
method - events can be strings (equal as in the original library), objects with property type, dictionaries or objects with
__getitem__
method and type key - context doesn't has restrictions.
- state definitions need to be dictionaries or objects with
- Some helpers were implemented as classes, more robust in type checking and with exact API that JS functions
- JS Promises are implemented with async/await Python feature
- Debug and logging helpers work as expected importing them
- In MicroPython, you need to install typing stub package to support type annotations (zero runtime overhead)
- In MicroPython or python version prior 3.6, you must provide initialState (first argument) in createMachine, because un-ordered dicts doesn't guarantee deduction of first state as initialState.
Minimal example:
from robot import createMachine, state, transition, interpret
machine = createMachine('off', {
'off': state(
transition('toggle', 'on')
),
'on': state(
transition('toggle', 'off')
)
})
service = interpret(machine, lambda x: print(x))
print(service.machine.current) # off
service.send('toggle')
print(service.machine.current) # on
Nearly all features:
from robot import createMachine, guard, immediate, invoke, state, transition, reduce, action, state as final, interpret, Service
import robot.debug
import robot.logging
def titleIsValid(ctx, ev):
return len(ctx['title']) > 5
async def saveTitle():
id = await do_db_stuff()
return id
childMachine = createMachine('idle', {
'idle': state(transition('toggle', 'end', action(lambda: print('in child machine!')))),
'end': final()
})
machine = createMachine('preview', {
'preview': state(
transition('edit', 'editMode',
# Save the current title as oldTitle so we can reset later.
reduce(lambda ctx: ctx | {'oldTitle': ctx['title']}),
action(lambda: print('side effect action'))
)
),
'editMode': state(
transition('input', 'editMode',
reduce(lambda ctx, ev: ctx | {'title': ev.target.value})
),
transition('cancel', 'cancel'),
transition('child', 'child'),
transition('save', 'validate')
),
'cancel': state(
immediate('preview',
# Reset the title back to oldTitle
reduce(lambda ctx: ctx | {'title': ctx['oldTitle']})
)
),
'validate': state(
# Check if the title is valid. If so go
# to the save state, otherwise go back to editMode
immediate('save', guard(titleIsValid), action(
lambda ctx: print(ctx['title'], ' is in validation'))),
immediate('editMode')
),
'save': invoke(saveTitle,
transition('done', 'preview', action(
lambda: print('side effect action'))),
transition('error', 'error')
),
'child': invoke(childMachine,
transition('done', 'preview'),
),
'error': state(
# Should we provide a retry or...?
)
}, lambda ctx: {'title': 'example title'})
def service_log(service: Service):
print('send event! current state: ', service.machine.current)
service = interpret(machine, service_log)
print(service.machine.current)
service.send('edit')
service.send('child')
service.child.send('toggle')
- Please star the repository on GitHub.
- File an issue if you find a bug. Or better yet...
- Submit a pull request to contribute.
Tests are located in the tests/
folder, using unittest standard library.
Run with this command or equivalent:
$ python -m unittest -v tests/*
BSD-2-Clause, same of the original library :D