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) # onNearly 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
