Skip to content

Latest commit

History

7 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

robot-python

The Robot logo, with green background.

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.

Why Finite State Machines / StateCharts?

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 🥲

Useful resources

Changes from original library

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.
  • 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.

Examples

Minimal example:

fromrobotimportcreateMachine, state, transition, interpretmachine=createMachine('off', {
'off': state(
transition('toggle', 'on')
),
'on': state(
transition('toggle', 'off')
)
})
service=interpret(machine, lambdax: print(x))
print(service.machine.current) # offservice.send('toggle')
print(service.machine.current) # on

Nearly all features:

fromrobotimportcreateMachine, guard, immediate, invoke, state, transition, reduce, action, stateasfinal, interpret, Serviceimportrobot.debugimportrobot.loggingdeftitleIsValid(ctx, ev):
returnlen(ctx['title']) >5asyncdefsaveTitle():
id=awaitdo_db_stuff()
returnidchildMachine=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(lambdactx: ctx| {'oldTitle': ctx['title']}),
action(lambda: print('side effect action'))
)
),
'editMode': state(
transition('input', 'editMode',
reduce(lambdactx, ev: ctx| {'title': ev.target.value})
),
transition('cancel', 'cancel'),
transition('child', 'child'),
transition('save', 'validate')
),
'cancel': state(
immediate('preview',
# Reset the title back to oldTitlereduce(lambdactx: ctx| {'title': ctx['oldTitle']})
)
),
'validate': state(
# Check if the title is valid. If so go# to the save state, otherwise go back to editModeimmediate('save', guard(titleIsValid), action(
lambdactx: 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...?
)
}, lambdactx: {'title': 'example title'})
defservice_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')

Testing

Tests are located in the tests/ folder, using unittest standard library. Run with this command or equivalent:

$ python -m unittest -v tests/*

License

BSD-2-Clause, same of the original library :D

About

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.

Topics

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages