Skip to content

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.

Notifications You must be signed in to change notification settings

sytabaresa/robot-python

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 
 
 
 
 
 
 
 
 
 
 

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:

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')

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

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages