sonntam / node-red-contrib-xstate-machine Goto Github PK
View Code? Open in Web Editor NEWA xstate-based state machine implementation using state-machine-cat visualization for node red.
License: MIT License
A xstate-based state machine implementation using state-machine-cat visualization for node red.
License: MIT License
Hi, is it possible to import from a graphics editor stately.ai? If yes, how? I'm far from programming and couldn't figure it out.
Sorry for bad english
Hello - Just getting into trying out this module. I am stuck on trying to get a guard condition to work. The below setup doesn't work. Could use some help to tweak this. thx.
In the return state, I have the condition setup like this:
state_one: {target: 'system_off', cond: "myEvent"}
And
config: {
guards: {myEvent: (_, event) => event.payload = "incoming_payload_topic"}
}
edit; Does xstate support the cond: setup like above?
It can happen that a misplaced config section will produce misleading output. For example, actions may seem to work, but activities will produce No implementation found warnings.
For obviously missing sections, some sort of warning should be possible.
I am running Node-RED (0.20.7) on my Raspberry Pi in Docker (19.03.8). (The reason for the specific Node-RED version is an incompatibility with IKEA Tradfri of builds above 1.0).
When I want to install node-red-contrib-xstate-machine using the palette, the following happens:
However, when I want to deploy the changes, I get the following error message:
Flows stopped due to missing node types.
smxstate
Checking Node-RED log, the dependencies failed to install:
12 Apr 23:59:56 - [info] Waiting for missing types to be registered:,
12 Apr 23:59:56 - [info] - smxstate
The tail of "npm install node-red-contrib-xstate-machine" in the Docker container console output is:
npm info lifecycle [email protected]
postinstall: [email protected]postinstall: [email protected]
npm info lifecycle [email protected]
npm info lifecycle @types/[email protected]postinstall: @types/[email protected]preshrinkwrap: undefined
npm info lifecycle undefined
npm info lifecycle undefinedshrinkwrap: undefinedpostshrinkwrap: undefined
npm info lifecycle undefined
npm WARN [email protected] requires a peer of canvas@^2.5.0 but none was installed.
npm WARN [email protected] requires a peer of bufferutil@^4.0.1 but none was installed.
npm WARN [email protected] requires a peer of utf-8-validate@^5.0.2 but none was installed.
- [email protected]
added 103 packages in 38.402s
npm info ok
Any idea why these dependencies do not install?
Thx
I have a flow that was started from the default flow in the smxstate node. I added more states and actions, an renamed the dostuff activity, but now I can't get the activity implementation to be found. All I see in the console log is the Warning that is the subject of this issue.
Is there some way I can get more verbose logging? Or some way to trace into what exactly is (not) happening in my machine config?
If the implementation is not found, how is it that the flow able to be deployed?
Here's the complete payload from the top output when transitioning to the state with the activity:
{"state":{"enabled":{"ticket":{"call":"running"}}},"changed":true,"done":false,"activities":{"check":{"type":"xstate.start","activity":{"type":"check"}}},"actions":[{"type":"xstate.start","activity":{"type":"check"}},{"type":"xstate.send","event":{"type":"xstate.after(30000)#c5791bba.c55e88.enabled.ticket.call.running"},"delay":30000,"id":"xstate.after(30000)#c5791bba.c55e88.enabled.ticket.call.running","_event":{"name":"xstate.after(30000)#c5791bba.c55e88.enabled.ticket.call.running","data":{"type":"xstate.after(30000)#c5791bba.c55e88.enabled.ticket.call.running"},"$$type":"scxml","type":"external"}}],"event":{"type":"RUNNING","payload":1628623713161},"context":{"startLevel":0,"stopLevel":0,"runTime":0}}
Here's the flow:
const { assign } = xstate;
/**
* Guards
*/
const maxRunTimeElapsed = (context, event) => {
return context.runTime >= maxRunTime;
};
/**
* Actions
*/
const ticketSend = (context, event) => {
node.send({ payload: "Ticket Complete" });
console.log('ticket complete time:', Date.now());
};
const incrementCounter = assign({
counter: (context, event) => context.counter + 1
});
const pumpStop = (context, event) => {
node.send({ payload: "Pump Stop" });
console.log('pump stop time:', Date.now());
};
const pumpStart = (context, event) => {
node.send({ payload: "Pump Start" });
console.log('pump start time:', Date.now());
};
const ticketStateSet = (context, event) => {
node.send({ payload: "Set Ticket State" });
console.log('set ticket state time:', Date.now());
};
/**
* Activities
*/
//See https://xstate.js.org/docs/guides/activities.html
const check = () => {
const interval = setInterval(() => {
node.send({ payload: 'Check Level' });
}, 2000);
return () => clearInterval(interval);
};
const doStuff = () => {
// See https://xstate.js.org/docs/guides/activities.html
const interval = setInterval(() => {
node.send({ payload: 'BEEP' });
}, 1000);
return () => clearInterval(interval);
};
return {
machine: {
// Local context for entire machine
context: {
startLevel: 0,
stopLevel: 0,
dispenseQty: global.get('dispenseQty'),
runTime: 0,
maxRunTime: global.get('startLevel')
},
// Initial state
initial: 'off',
// State definitions
states: {
off: {
/* Platform secret switch is open */
on: {
ENABLE: 'enabled'
}
},
enabled: {
/* Platform secret switch is closed */
//type: 'compound',
initial: 'idle',
on: {
DISABLE: 'off'
},
states: {
idle: {
on: {
TICKET: 'ticket'
}
},
ticket: {
//type: 'compound',
initial: 'idle',
onDone: 'idle',
states: {
idle: {
entry: ticketStateSet,
on: {
CALL: 'call',
COMPLETE: 'complete'
}
},
call: {
//type: 'compound',
initial: 'starting',
entry: ticketStateSet,
onDone: 'complete',
states: {
starting: {
entry: pumpStart,
on: {
RUNNING: 'running'
}
},
running: {
on: {
STOP: 'stopping',
STOPPED: 'stopped'
},
after: {
30000: { target: 'stopping' }
},
activities: check
},
stopping: {
entry: pumpStop,
on: {
STOPPED: 'stopped'
}
},
stopped: {
type: 'final'
}
}
},
complete: {
type: 'final',
entry: [ticketSend, ticketStateSet]
}
}
}
}
}
},
config: {
activities: {check},
actions: {
ticketSend,
pumpStart,
pumpStop,
ticketStateSet
},
guards: {
maxRunTimeElapsed
}
//delays: {
// /* ... */
//},
//services: {
/* ... */
//}
}
}
};
hello,
the actual implmentation can support many instances of the same state machine.
my idea is of passing the state and the data for every message and in this mode i can expose the state machine via an api rest and each session user have is state and data but the same implmentation of the state machine
Many thanks for the work on this.
I notice that xstate 5.x is out and I wonder if you have had time to look at what may be involved ?
Based on what I see here, there are some rendering controls that are not exposed. It would be very useful to control the render direction and the fi to width option. For the latter, It might be sufficient if fit to width was just always on, as in my case, a long skinny chart is difficult to see what's happening here.
When renaming a flow that contains a smxstate node, the change is not reflected within the visualization panel's drop-down list.
had an issue where an action was misspelled/missing which resulted in the node throwing an exception and permanently crashing Node Red ie. Node Red could no longer be restarted since it crashes again on the same issue
Had to manually edit the flows.json file and remove the xstate entry.
I don't think a misconfigured node should be able to crash Node Red that hard...
Node-Red running as a service on ubuntu 22.04:
nov. 29 17:36:54 cyclops Node-RED[2576060]: 29 Nov 17:36:54 - [info] Node-RED version: v3.1.0
nov. 29 17:36:54 cyclops Node-RED[2576060]: 29 Nov 17:36:54 - [info] Node.js version: v20.9.0
nov. 29 17:36:54 cyclops Node-RED[2576060]: 29 Nov 17:36:54 - [info] Linux 5.15.0-88-generic x64 LE
Observ that this will most proably crash NodeRed so make backup of .node-red/projects/Home/flows.json
nov. 29 17:36:41 cyclops Node-RED[2575948]: 29 Nov 17:36:41 - [info] Started flows
nov. 29 17:36:41 cyclops Node-RED[2575948]: 29 Nov 17:36:41 - [red] Uncaught Exception:
nov. 29 17:36:41 cyclops Node-RED[2575948]: 29 Nov 17:36:41 - [error] ReferenceError: incrementCounter is not defined
nov. 29 17:36:41 cyclops Node-RED[2575948]: at SMXSTATE node:def654576503dc8a:126:16
nov. 29 17:36:41 cyclops Node-RED[2575948]: at SMXSTATE node:def654576503dc8a:135:3
nov. 29 17:36:41 cyclops Node-RED[2575948]: at Script.runInContext (node:vm:134:12)
nov. 29 17:36:41 cyclops Node-RED[2575948]: at new StateMachineNode (/home/node-red/.node-red/node_modules/node-red-contrib-xstate-machine/dist/smxstate-node.js:435:34)
nov. 29 17:36:41 cyclops Node-RED[2575948]: at Object.createNode (/usr/lib/node_modules/node-red/node_modules/@node-red/runtime/lib/flows/util.js:157:27)
nov. 29 17:36:41 cyclops Node-RED[2575948]: at Flow.start (/usr/lib/node_modules/node-red/node_modules/@node-red/runtime/lib/flows/Flow.js:260:54)
nov. 29 17:36:41 cyclops Node-RED[2575948]: at async Object.start [as startFlows] (/usr/lib/node_modules/node-red/node_modules/@node-red/runtime/lib/flows/index.js:398:17)
nov. 29 17:36:41 cyclops systemd[1]: nodered.service: Main process exited, code=exited, status=1/FAILURE
nov. 29 17:36:41 cyclops systemd[1]: nodered.service: Failed with result 'exit-code'.
All instances of smxstate nodes should be able to create an animated state-chart on the node-red dashboard if installed.
This is a much wanted feature.
See here.
The currently hard-coded render timeout of 10 seconds is too short. Maybe change the rendering so that no hard-coded timeout is necessary by observing the spawned process.
I'm receiving a strange syntax error when trying to render my state machine using smcat. Only the rendering is affected, and the machine node works perfectly otherwise. Since the error message doesn't seem to relate in any meaningful way to my machine definition code, my guess is that the error has something to do with the way that the code is converted into the right format for smcat, hence why I am posting the issue here rather than smcat's repo. Here is my code:
// Import shorthands from xstate object
const { assign } = xstate;
return {
machine: {
context: {
door: "open",
},
initial: "Unoccupied",
states: {
Unoccupied: {
on: {
buzz: [
{
target: "Occupied",
cond: "door_is_closed",
},
{
target: "Tentatively Occupied",
},
],
door_opened: {
actions: {
type: "mark_door_open",
params: {}
},
internal: true,
},
door_closed: {
actions: {
type: "mark_door_closed",
params: {}
},
internal: true,
}
}
},
Occupied: {
on: {
door_opened: {
target: "Tentatively Occupied",
actions: {
type: "mark_door_open",
params: {}
}
}
}
},
"Tentatively Occupied": {
after: {
"60000": [
{
target: "Unoccupied",
actions: []
},
{
internal: false,
}
],
},
on: {
buzz: [
{
target: "Occupied",
cond: "door_is_closed",
},
{
internal: true,
}
],
door_closed: {
actions: {
type: "mark_door_closed",
params: {}
},
internal: true,
},
door_opened: {
actions: {
type: "mark_door_open",
params: {}
},
internal: true,
}
}
}
},
predictableActionArguments: true,
preserveActionOrder: true
},
config: {
actions: {
mark_door_open: assign({door: (context, event) => 'open'}),
mark_door_closed: assign({door: (context, event) => 'closed'}),
},
services: { },
guards: { door_is_closed: (context, event) => context.door == 'closed' },
delays: { }
}
};
And here is the error message from the console log:
Rendering of state machine failed: Render process returned error code 1: file:///data/node_modules/state-machine-cat/src/parse/smcat/smcat-parser.mjs:14
var self = Error.call(this, message);
^
peg$SyntaxError: Expected comment, end of input, line end, statemachine, or whitespace but "e" found.
at new peg$SyntaxError (file:///data/node_modules/state-machine-cat/src/parse/smcat/smcat-parser.mjs:14:20)
at peg$buildStructuredError (file:///data/node_modules/state-machine-cat/src/parse/smcat/smcat-parser.mjs:556:12)
at peg$parse (file:///data/node_modules/state-machine-cat/src/parse/smcat/smcat-parser.mjs:2686:11)
at Object.getAST (file:///data/node_modules/state-machine-cat/src/parse/index.mjs:22:22)
at Object.render (file:///data/node_modules/state-machine-cat/src/index-node.mjs:58:24)
at file:///data/node_modules/state-machine-cat/src/cli/actions.mjs:37:29
at processTicksAndRejections (node:internal/process/task_queues:96:5) {
expected:...
Any help in solving this issue would be greatly appreciated!
Hi there,
I am using "node-red-contrib-xstate-machine" plugin for a while now. It is very enjoyable, thanks for your effort.
My flow will be used on our production line and the state of the stations on the line is managed by the plugin.
Now I have a need to to presist the xstate nodes, as the flow might be restarted from time to time.
Is there a way to persist the state of the node like mentioned in https://xstate.js.org/docs/guides/states.html#persisting-state ?
Thanks in advance,
Ferhat
Hello,
I am trying to create a dynamic flow that calls an xstate machine to load a particular machine JSON based on the scenario. Right now, I have to create more than 20 xstate nodes and I need to manage those nodes statically. Is there a way that we can parse the machine JSON dynamically using node or any workaround in order to achieve that functionality? I would really appreciate your help in achieving that
Thanks
Hi, I've just installed the node fromNR palette manager, and it shows errors after the installation:
27 Aug 20:16:11 - [info] Installed module: node-red-contrib-xstate-machine
27 Aug 20:16:11 - [info] Added node types:
27 Aug 20:16:11 - [info] - node-red-contrib-xstate-machine:smxstate : SyntaxError: Unexpected token {
When I add a node to the flow and restart flows it stops with notification below:
27 Aug 20:16:26 - [info] Waiting for missing types to be registered:
27 Aug 20:16:26 - [info] - smxstate
Any suggestions?
Maybe nodejs v 8.11.3 is to old?
How can I display the state node's context?
There is a Node-RED Context Data panel that (should) display Node, Flow, and Global context.
The Node context does not display the xstate Node context.
I have noticed behavior where the entry
action does not get invoked upon deploying a NodeRed flow. This is demonstrated in the below flow 'test1'. The workaround is contained in 'test2' (image) where an intermediate, short-lived state is introduced.
[
{
"id": "ca58ed842a99f191",
"type": "tab",
"label": "xstate:unsettled",
"disabled": false,
"info": "",
"env": []
},
{
"id": "dcbedfe0f28bc94d",
"type": "smxstate",
"z": "ca58ed842a99f191",
"name": "",
"xstateDefinition": "// @ts-nocheck\nconst { assign } = xstate;\n\nconst sendInfo = (context, event, mta) => {\n console.warn('sendinfo enter')\n\n var metas = {\n ...mta.state.meta[Object.keys(mta.state.meta)[0]],\n ...mta.state.meta[Object.keys(mta.state.meta)[1]]\n }\n\n node.send({\n topic: `${metas.topic}`,\n payload: {\n ts: Date.now(),\n ...metas\n }\n });\n};\n\nreturn {\n machine: {\n id: 'test1',\n meta: {\n topic: 'test1'\n },\n context: {},\n initial: 'unavailable',\n states: {\n unavailable: {\n meta: {\n value: 'UNAVAILABLE'\n },\n entry: ['sendInfo']\n },\n available: {\n meta: {\n value: 'AVAILABLE'\n },\n entry: ['sendInfo']\n }\n },\n on: {\n DEVICE_AVAILABILITY_AVAILABLE: '.available',\n DEVICE_AVAILABILITY_UNAVAILABLE: '.unavailable'\n }\n },\n config: {\n guards: {\n\n },\n actions: {\n sendInfo\n },\n activities: {\n\n },\n services: {\n\n },\n delays: {\n\n }\n }\n};",
"noerr": 0,
"x": 520,
"y": 140,
"wires": [
[],
[
"3ef068dc37da38dc"
]
]
},
{
"id": "b7fa3f877942edf3",
"type": "inject",
"z": "ca58ed842a99f191",
"name": "DEVICE_AVAILABILITY_UNAVAILABLE",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "DEVICE_AVAILABILITY_UNAVAILABLE",
"payload": "",
"payloadType": "date",
"x": 240,
"y": 180,
"wires": [
[
"dcbedfe0f28bc94d"
]
]
},
{
"id": "8ee77e29746217f8",
"type": "inject",
"z": "ca58ed842a99f191",
"name": "DEVICE_AVAILABILITY_AVAILABLE",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "DEVICE_AVAILABILITY_AVAILABLE",
"payload": "",
"payloadType": "date",
"x": 230,
"y": 120,
"wires": [
[
"dcbedfe0f28bc94d"
]
]
},
{
"id": "3ef068dc37da38dc",
"type": "debug",
"z": "ca58ed842a99f191",
"name": "debug 64",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "true",
"targetType": "full",
"statusVal": "",
"statusType": "auto",
"x": 760,
"y": 140,
"wires": []
},
{
"id": "f81e76fe16d7f35e",
"type": "comment",
"z": "ca58ed842a99f191",
"name": "test 1",
"info": "",
"x": 110,
"y": 60,
"wires": []
},
{
"id": "f816d55d355a07e9",
"type": "smxstate",
"z": "ca58ed842a99f191",
"name": "",
"xstateDefinition": "// @ts-nocheck\nconst { assign } = xstate;\n\nconst sendInfo = (context, event, mta) => {\n console.warn('sendinfo enter')\n\n var metas = {\n ...mta.state.meta[Object.keys(mta.state.meta)[0]],\n ...mta.state.meta[Object.keys(mta.state.meta)[1]]\n }\n\n node.send({\n topic: `${metas.topic}`,\n payload: {\n ts: Date.now(),\n ...metas\n }\n });\n};\n\nreturn {\n machine: {\n id: 'test2',\n meta: {\n topic: 'test2'\n },\n context: {},\n initial: 'unsettled',\n states: {\n unsettled: {\n after: {\n DELAY_SETTLE: {\n target: 'unavailable'\n }\n }\n },\n unavailable: {\n meta: {\n value: 'UNAVAILABLE'\n },\n entry: ['sendInfo']\n },\n available: {\n meta: {\n value: 'AVAILABLE'\n },\n entry: ['sendInfo']\n }\n },\n on: {\n DEVICE_AVAILABILITY_AVAILABLE: '.available',\n DEVICE_AVAILABILITY_UNAVAILABLE: '.unavailable'\n }\n },\n config: {\n guards: {\n\n },\n actions: {\n sendInfo\n },\n activities: {\n\n },\n services: {\n\n },\n delays: {\n DELAY_SETTLE: 1\n }\n }\n};",
"noerr": 0,
"x": 520,
"y": 340,
"wires": [
[],
[
"9fbbb592c06d1d34"
]
]
},
{
"id": "6a097a371c31dc50",
"type": "inject",
"z": "ca58ed842a99f191",
"name": "DEVICE_AVAILABILITY_UNAVAILABLE",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "DEVICE_AVAILABILITY_UNAVAILABLE",
"payload": "",
"payloadType": "date",
"x": 240,
"y": 380,
"wires": [
[
"f816d55d355a07e9"
]
]
},
{
"id": "8ed17527886ce6b2",
"type": "inject",
"z": "ca58ed842a99f191",
"name": "DEVICE_AVAILABILITY_AVAILABLE",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "DEVICE_AVAILABILITY_AVAILABLE",
"payload": "",
"payloadType": "date",
"x": 230,
"y": 320,
"wires": [
[
"f816d55d355a07e9"
]
]
},
{
"id": "9fbbb592c06d1d34",
"type": "debug",
"z": "ca58ed842a99f191",
"name": "debug 65",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "true",
"targetType": "full",
"statusVal": "",
"statusType": "auto",
"x": 760,
"y": 340,
"wires": []
},
{
"id": "8f13198dfdc9668b",
"type": "comment",
"z": "ca58ed842a99f191",
"name": "test 2",
"info": "",
"x": 110,
"y": 260,
"wires": []
}
]
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.