Creating a never-ending listener and type error

dear all
i'm pondering what the right way is to create a node that listens (ideally async) for output from a python script that never ends (it is monitoring GPIO interrupts)? the code, as it stands, is below. This is the first time trying to build a node for node-red and am doing so as I cannot find a pre-rolled node for EC11 rotary encoders.


module.exports = function(RED) {
	'use strict'
	const path = require('path')
	const {exec} = require("child_process");
	const command = "rotaryEncoder.py"

	// the magic to make python print stuff immediately
	process.env.PYTHONUNBUFFERED = 1

	function rotaryEncoder(config) {
		RED.nodes.createNode(this, config)
		this.out = n.out || 'out'
		const node = this
		let out = ''
		this.pos = 0

		node.on('input', listenForRotaryInput);

		node.on('close', function(done) {
			node.status({
				fill: 'grey',
				shape: 'ring',
				text: 'Closed'
			})
			done()
		});

		async function listenForRotaryInput() {
			await execPython()
				.then(data => {
					this.log(data);
					out = parseInt(data);
					if (out != this.pos) {
						const delta = this.pos + out;
						const direction = delta > 0 ? "CW" : "CCW";
						this.pos = out;
						msg.payload = {
							position: this.pos,
							delta: delta,
							direction: direction
						};
						node.send(msg);
						node.status({
							fill: "green",
							shape: "ring",
							text: "pos: " + msg.payload.toString()
						});
					}
				});
		}

		const execPython = async function(){
			return new Promise((resolved, rejected) => {
				exec(`./example ${parameter}`, (error, stdout, stderr) => {
					if (error) {
						this.log(`rotary-encoder: error: ${error.message}`);
						rejected(error);
						return;
					}
					if (stderr) {
						this.log(`rotary-encoder stderr: ${stderr}`);
						return;
					}

					const cData = {
						returnValue: stdout
					};
					resolved(cData);
				});
			});
		};
	}
	RED.nodes.registerType('rotary-encoder', rotaryEncoder)
}

separately I am getting an error when I try to drop the node on a flow. the error is below. Any help on this would be great. It looks like an html issue but the html is just the below; so not much scope for errors.


<script type="text/javascript">
    RED.nodes.registerType('rotary-encoder', {
        category: 'input',
        color: '#a6bbcf',
        defaults: {
        	value:""
        },
        inputs: 1,
        outputs: 1,
        icon: "bridge-dash.svg",
        paletteLabel: "rotary encoder",
        label: function() {
            return this.name || "Rotary Encoder";
        }
    });
</script>

<script type="text/html" data-template-name="rotary-encoder">
	<div class="form-row">
        <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
        <input type="text" id="node-input-name" placeholder="Name">
	</div>
</script>

Would it be simpler to make your python script output to MQTT and then pick that up in node-red?

it's for a rotary encoder so the intention is for instant response from the application layer.
I'd rather not introduce another layer of latency.

Is there no practical way to have an asynchronous listener in nodered?

There is also a Daemon node, very similar to the exec node, which you could use to run and get print() output from your python script.

You could use the built in exec mode itself but you would have to run the script yourself, perhaps as a systemd service.
The Daemon node is designed to manage permanently running services from Node-red.

With either of these options you have to call python -u so that it's output is unbuffered, ie immediately visible to Node-red

@Colin's suggestion of mqtt is probably more elegant but takes a bit of python coding to set up.

1 Like

I don't know the answer to that, which is why I suggested the simpler approach.

How quickly do you need it to respond and what sort of processor are you running on?
Is it disastrous if the application layer misses an event?

thank you both.
The daemon node seems to be close to what I am looking for, if not exact.

as to how quickly it needs to react: the point is to have a knob controlling speed of rotation for a pump motor. the movement clock/anticlock-wise should result in a smooth and responsive change in speed. I'd suspect that any lag may reduce the user experience but I have not yet tested it.

and I think I must pause in further testing as my python skills are not up to scratch. What is working perfectly in python (i.e. from the command line) is not working from within the daemon node.

the error I get, for info, is

daemon:Rotary Encoder] error: Error: spawn python3 -u ./rotaryencoder/rotaryEncoder.py ENOENT

gpio: GPIO Sysfs Interface for Userspace is deprecated (https://www.kernel.org/doc/Documentation/gpio/sysfs.txt).

Function is now useless and empty.

wiringPiISR: unable to open /sys/class/gpio/gpio20/value: No such file or directory

as I said, it's working fine from the command line so I'm not sure what's up. and in the python script I'm using RPi.gpio which should still be ok on a RPi 4.

ENOENT means that it cannot find the file. The reason is probably that ./ is not what you think it is from inside node-red. Always specify the full path to files for such cases.

fair enough.

tried this


[daemon:Rotary Encoder] error: Error: spawn /usr/bin/python3 -u /home/jpadie/dev/rotaryEncoder/rotaryEncoder.py ENOENT

the same invocation works ok on the command line. python and python3 are both in /usr/bin and there is no doubt that the script is where indicated, with -rwxr-xr-x perms

Can you show us the script?

yes, of course. here it is. I've just rewritten it to use gpiozero in case that was the headache.

from threading import Event
from gpiozero import RotaryEncoder, Button

PINA = 20
PINB = 21
PINC = 16

rotor = RotaryEncoder(PINA, PINB, wrap=False, max_steps=0 )
btn = Button(PINC, pull_up=True)
done = Event()

def notify():
  print(rotor.steps)

def notifyButton():
  print("pressed")

btn.when_released = notifyButton
rotor.when_rotated = notify
done.wait()

Show us how you have configured the daemon node please, and the contents of any message that you send to it.

no problem

no input messages as the node autostarts.

this is working now, after a fashion. the -u and the file name needed to be passed as arguments in the config box.

but unfortunately either the daemon node or python is severely buffering the output such that it becomes less than useful

I still think the best way would be to amend the python script to publish to MQTT (which I believe is easy, I am sure there are examples of how to do it available) and pick that up in node red. MQTT is very efficient and will handle hundreds of messages/second if necessary. Use the Mosquitto broker.

i am happy to try that.

coming back here to update on how I resolved this in the end

the python script I used remains simple. Here it is

from threading import Event
from gpiozero import RotaryEncoder, Button

rotor = RotaryEncoder(20, 21, wrap=False, max_steps=0 )
btn = Button(16, pull_up=False)
done = Event()

def notify():
        print(rotor.steps, end='\n', flush=True)

def notifyButton():
        print("btn",end='\n',flush=True)


btn.when_released = notifyButton
rotor.when_rotated = notify

while True:
        done.wait()

in the node-red dashboard I used the pythonshell node (node-red-contrib. the flow looks like this. The switch is to differentiate between the btn and the encoder inputs. the function is to cast to an int as the python script currently is treated by the node as a string and a line break is appended.

this behaviour can be altered in the src file at line 85.