Skip to content

Latest commit

 

History

History
175 lines (141 loc) · 8.42 KB

readme.md

File metadata and controls

175 lines (141 loc) · 8.42 KB

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