sonntam / node-red-contrib-xstate-machine

A xstate-based state machine implementation using state-machine-cat visualization for node red.
MIT License
23 stars 9 forks source link

Node Red XState-Machine

Computing machines are like the wizards in fairy tales. They give you what you wish for, but do not tell you what that wish should be. \ - Norbert Wiener, founder of cybernetics

The goal of this package is to provide an easy yet flexible and customizable way to define state-machines within node-red and effortlessly observe their state.

Changelog

See the separate changelog file for an overview over the available versions and changes.

Introduction

The idea for this package is based on node-red-contrib-finite-statemachine which was a very good starting point for me, but lacked some functionality that I wanted. If you only need to model simple state machines without guards, actions, time-based transitions, compund or parallel states this is the library you should go to!

This package provides state-machine functionality for node-red by utilizing the excellent xstate library as basis. It allows for very complex systems, e.g. nested machines, nested states, parallel states, history, deep history etc. For visualiztaion the library state-machine-cat comes into play.

The rest of the implementation is based on node-red's function and debug nodes.

Installation

Usage

Once this package is installed in node-red you will find two new items:

In the following the usage of both will be explained to get you started.

The smxstate node

Node image

Within this node you will find a javascript editor similar to the function-node. Here you can define your state-machine in a xstate-fashion. The javascript code must end with a return that returns an object of the following form:

return {
  machine: {
    ...
  },
  config: {
    ...
  }
}

The machine object is an xstate machine. For details on how to model or formulate your machine see the excellent xstate docs.

The config object contains all the named actions, activities, guards and services used in the machine object, see here.

See the node's help within node-red for further details: Node settings dialog

State-machines sidebar

The side bar allows for visualizing a machine instance's current data context and its state.

Visualization of example machine

There are a number of buttons available to control the editor view/control the machine:

Also there are some settings that affect how the state graph is rendered.

Example flows

Simple statemachine with data object

The follwing example is based on the example machine from state-machine-cat 😺

In the image below you can see the node-red setup for the example.

Example flow

It tries to model a cat with 4 states

The cat has an internal value-context containing the belly filling level.

There are several state-transitions with guards, e.g. [belly empty] or [belly full] as well as timed transitions (see the image below).

Cat visualization

Enter the following code snippet into a smxstate node or load the example flow from here into node-red to get the example started:

// Import shorthands from xstate object
const { assign } = xstate; 

// First define names guards, actions, ...

/**
 * Guards
 */
const bellyFull = (context, event) => {
  return context.belly >= 100;
};

const bellyEmpty = (context, event) => {
    return context.belly <= 0;
}

const bellyNotEmpty = (context, event) => {
    return context.belly > 0;
}

/**
 * Actions
 */
const updateBelly = assign({
    belly: (context, ev) => Math.max(
        Math.min(
            context.belly+ev.value,100
        ), 0)
});

/**
 * Activities
 */
const meow = () => {
  const interval = setInterval(() => node.send({payload: "MEOW!"}), 2000);
  return () => clearInterval(interval);
};

/**
 * Services
 * see https://xstate.js.org/docs/guides/communication.html#invoking-callbacks
 */
const eat = (ctx, ev) => (cb) => {
    const id = setInterval(() => cb({
      type: 'eaten',
      value: 5
    }),500);
    return () => clearInterval(id);
};

const digest = (ctx, ev) => (cb) => {
    const id = setInterval(() => cb({
      type: 'digested',
      value: -5
    }),500);
    return () => clearInterval(id);
}

/***************************
 * Main machine definition * 
 ***************************/
 return {
  machine: {
    context: {
        belly: 0    // Belly state, 100 means full, 0 means empty
    },
    initial: 'sleep',
    states: {
      sleep: {
          on: {
              'wake up': 'meow',
          }
      },
      meow: {
          invoke: { src: 'digest' },
          on: {
              'given toy': { target: 'play', cond: 'belly not empty' },
              'given food': 'eat',
              'digested': { actions: 'updateBelly' }
          },
          activities: ['meow']
      },
      play: {
          invoke: { src: 'digest' },
          on: {
              tired: 'sleep',
              bored: 'sleep',
              'digested': { actions: 'updateBelly' },
              '': { target: 'meow', cond: 'belly empty' }
          },
          after: {
              5000: 'sleep',
          }
      },
      eat: {
          invoke: { src: 'eat' },
          on: {
              '': { target: 'sleep', cond: 'belly full' },
              'no more food': { target: 'meow' },
              'eaten': { actions: 'updateBelly' }
          }
      }
    }
  },
  // Configuration containing guards, actions, activities, ...
  // see above
  config: {
      guards: { "belly full": bellyFull, "belly not empty": bellyNotEmpty, "belly empty": bellyEmpty },
      activities: { meow },
      actions: { updateBelly },
      services: { eat, digest }
  }
}

This will give you a state machine to play with. It incorporates actions, delayed events, services, guards and internal data context. Observe the state-machine's state in the visualization panel while you inject events.

Development

Acknowledgements

Caveats