Input debounce and Rotary Encoder issues on Raspberry Pi

Hi all,

I have a rotary encoder connected to my Raspberry Pi running (recently-updated) Node Red (v2.2), nodejs (v14 LTS), Raspberry Pi OS Bullseye, etc.

If you haven't used a rotary encoder before, here's how it works: As the encoder turns around, there are a certain number of discrete "positions", even if there are no palpable "clicks". For example, the "volume" or "tuning" knobs on your car radio are probably rotary encoders (if it turns continuously and doesn't come to a hard stop).

There are two pins which are pulled low or high depending on the rotation of the spindle on the rotary encoder. As the position changes, the status of the two pins changes like so: "00", then "01", then "11", then "10", then back to "00". Or, if turning to opposite way, the pins would go "00", "10", "11", "01", and back to "00". This way, you can determine not only motion, but the direction of motion, clockwise or counterclockwise.

Here's my problem: Sometimes my encoder turns quickly. When that happens, the discrete "positions" advance faster than the minimum "Debounce" in the Raspberry Pi input node, such that I get, for example "00", then "11" (non-adjacent positions that don't indicate direction)..

I tried setting the "Debounce" number to zero in the RPi Input node, but that stops the node - it needs a 1ms minimum debounce. Or am I missing something? It does accept decimals over 1ms (e.g. 1.1ms), but not below 1ms. Is there a higher-speed way to do this reading that could cut it down to maybe .5ms or even .3ms (which would be more than sufficient, I believe)? For example, some sort of more native GPIO interface that could report into Node Red? Would it be faster to read with python and pass values into Node Red every so often?

Thank you! Here's a sample flow demonstrating how the reading works:

[{"id":"0beb6b0848a6cb93","type":"switch","z":"9146dfa0.ce79f","name":"error","property":"error","propertyType":"msg","rules":[{"t":"true"}],"checkall":"true","repair":false,"outputs":1,"x":950,"y":1040,"wires":[["1ec6dce18e3b682c"]]},{"id":"18ab5da0c5bdf9d0","type":"function","z":"9146dfa0.ce79f","name":"encoder counter","func":"var main = global.get(\"rotary.main\");\nvar oldMain = main;\n\nvar Apin = \"\";\nvar Bpin = \"\";\n\n\nif (msg.topic == \"A\") {   \n    Apin = String(msg.payload);\n    Bpin = String(global.get(\"rotary.B.state\"));\n}\n\nif (msg.topic == \"B\") {   \n    Bpin = String(msg.payload);\n    Apin = String(global.get(\"rotary.A.state\"));\n}\n\nvar newPins = Apin + Bpin;\nglobal.set(\"rotary.pins\",newPins);\nvar lastPins = global.get(\"rotary.lastPins\");\n\nif (newPins == lastPins)\n{return null;} // or maybe this should report +4 or -4 depending on the direction of movement..?\n\nif (newPins == \"00\")\n{ if        (lastPins == \"10\")\n                {main++;\n                global.set(\"rotary.ifErrorMovement\",2)}\n  else if   (lastPins == \"01\") \n                {main--;\n                global.set(\"rotary.ifErrorMovement\",-2)}\n  else {msg.error = true;\n        main = main + global.get(\"rotary.ifErrorMovement\");\n        }\n}\n  \nif (newPins == \"01\")\n{ if        (lastPins == \"00\")\n                {main++;\n                global.set(\"rotary.ifErrorMovement\",2)}\n  else if   (lastPins == \"11\")\n                {main--;\n                global.set(\"rotary.ifErrorMovement\",-2)}\n  else {msg.error = true;\n        main = main + global.get(\"rotary.ifErrorMovement\");\n        }\n}\n\nif (newPins == \"11\")\n{ if        (lastPins == \"01\")\n                {main++;\n                global.set(\"rotary.ifErrorMovement\",2)}\n  else if   (lastPins == \"10\")\n                {main--;\n                global.set(\"rotary.ifErrorMovement\",-2)}\n  else {msg.error = true;\n        main = main + global.get(\"rotary.ifErrorMovement\");\n        }\n}\n\nif (newPins == \"10\")\n{ if        (lastPins == \"11\")\n                {main++;\n                global.set(\"rotary.ifErrorMovement\",2)}\n  else if   (lastPins == \"00\")\n                {main--;\n                global.set(\"rotary.ifErrorMovement\",-2)}\n  else {msg.error = true;\n        main = main + global.get(\"rotary.ifErrorMovement\");\n        }\n}\n\nglobal.set(\"rotary.main\",main);\nglobal.set(\"rotary.lastPins\",newPins);\n\nmsg.change = main - oldMain;\nmsg.payload = main;\nmsg.pins = newPins;\n\nreturn msg;","outputs":1,"noerr":0,"x":740,"y":1080,"wires":[["d54264bc5d48db8a","0beb6b0848a6cb93","afbd717f5bfc578d","5af03fa76b149da3"]]},{"id":"1ec6dce18e3b682c","type":"template","z":"9146dfa0.ce79f","name":"","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"There has been an error with the rotary encoder - a non-adjacent pin combination was received.\n\nValue: {{payload}}","output":"str","x":1100,"y":1040,"wires":[["7d9a8e6a6bc993e4"]]},{"id":"c0bb5128fe89eb64","type":"delay","z":"9146dfa0.ce79f","name":"","pauseType":"rate","timeout":"5","timeoutUnits":"seconds","rate":"2000","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"outputs":1,"x":520,"y":1080,"wires":[["18ab5da0c5bdf9d0"]]},{"id":"d54264bc5d48db8a","type":"trigger","z":"9146dfa0.ce79f","name":"","op1":"","op2":"","op1type":"nul","op2type":"payl","duration":"1","extend":false,"units":"s","reset":"","bytopic":"all","outputs":1,"x":960,"y":1080,"wires":[["4bfd216b55c1e53a"]]},{"id":"afbd717f5bfc578d","type":"debug","z":"9146dfa0.ce79f","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"pins","targetType":"msg","x":770,"y":1120,"wires":[]},{"id":"5af03fa76b149da3","type":"debug","z":"9146dfa0.ce79f","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":770,"y":1040,"wires":[]},{"id":"7d9a8e6a6bc993e4","type":"debug","z":"9146dfa0.ce79f","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1250,"y":1020,"wires":[]},{"id":"ce3f0bf12eef3285","type":"change","z":"9146dfa0.ce79f","name":"A settings","rules":[{"t":"set","p":"rotary.A.state","pt":"global","to":"payload","tot":"msg"},{"t":"set","p":"topic","pt":"msg","to":"A","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":300,"y":1060,"wires":[["c0bb5128fe89eb64"]]},{"id":"f79669d9654793e5","type":"change","z":"9146dfa0.ce79f","name":"B settings","rules":[{"t":"set","p":"rotary.B.state","pt":"global","to":"payload","tot":"msg"},{"t":"set","p":"topic","pt":"msg","to":"B","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":300,"y":1120,"wires":[["c0bb5128fe89eb64"]]},{"id":"4bfd216b55c1e53a","type":"mqtt out","z":"9146dfa0.ce79f","name":"","topic":"rotaryRawCount","qos":"","retain":"","broker":"cbcfb059.a48c1","x":1140,"y":1080,"wires":[]},{"id":"9046c6bd061e9993","type":"rpi-gpio in","z":"9146dfa0.ce79f","name":"","pin":"23","intype":"up","debounce":"1","read":true,"bcm":true,"x":100,"y":1060,"wires":[["ce3f0bf12eef3285"]]},{"id":"26146eb6e3506c63","type":"rpi-gpio in","z":"9146dfa0.ce79f","name":"","pin":"24","intype":"up","debounce":"1","read":true,"bcm":true,"x":100,"y":1120,"wires":[["f79669d9654793e5"]]},{"id":"cbcfb059.a48c1","type":"mqtt-broker","name":"localhost","broker":"localhost","port":"1883","clientid":"","usetls":false,"compatmode":true,"keepalive":"60","cleansession":true,"birthTopic":"","birthQos":"0","birthPayload":"","closeTopic":"","closeQos":"0","closePayload":"","willTopic":"","willQos":"0","willPayload":""}]

Not sure you will get truly fast speeds working. Node red doesn't really read pins fast enough depending on your needs. I wound up using a arduino to read my encoder then sending the info over to node red. Sorta kludgey but it works.

Yeah... I have a lot of ESP32s and ESP8266s around and was considering using one of those. That might solve the problem.

Did you aggregate readings somehow (like, send the most recent value over once per second, etc.)? Or something else? And when you do that, I assume you're using serial? I'll have other options with the ESPs (like MQTT or TCP) but maybe serial is the best way to do it.

Sorry, for the delay gone for a while. So.. I send everything over MQTT for the position. Now, TBH the rotary is on my garage door and it was a just because thing. I had the ESP and some encoders laying around and thought what can I use these on? But I do get a 1 inch position on the door which is fun. Back to the question, I send the MQTT message every time the position changes and if the door is moving the MQTT gets back seat to the counting so the reading is real jumpy but as soon as the door stops the reading is stable. And the position is very reproducible so I'm not dropping pulses any where. But I tried multiple things hooking to a PI and never got close. The ESP was quick, easy, reliable.

1 Like