Experimental libgpiod GPIO nodes for Node-RED on Raspberry Pi Zero 2 W / Debian Trixie
Before anything else, I want to make the intention of this project very clear.
This is not a criticism of node-red-node-pi-gpio, its author, its maintainers, Node-RED, DietPi, or anyone involved in those projects.
Quite the opposite.
I have used the original Node-RED Raspberry Pi GPIO nodes for years, and I have personally corresponded with @dceejay in the past. I have a great deal of respect for his work and for the Node-RED project as a whole.
I would also like to explicitly acknowledge @MichaIng, who leads the DietPi project.
An earlier discussion about Raspberry Pi GPIO on DietPi/Trixie was an important part of the path that eventually led to this experiment:
That discussion solved an immediate DietPi/lgpio problem, but it also raised a broader question.
If the I/O is physically connected to the Raspberry Pi anyway, is moving it to an external microcontroller necessarily the best solution, or is that sometimes simply another workaround?
That question stayed with me.
Later, when we encountered continuous CPU overhead on GPIO inputs, it brought us back to the same idea:
keep the GPIO on the Raspberry Pi, but try using the modern Linux GPIO interface directly.
That is how this project started.
Who developed this?
This project was developed collaboratively by two participants: myself and ChatGPT from OpenAI.
I provided:
- the real Raspberry Pi hardware;
- the original problem;
- the relay and input board;
- measurements;
- engineering requirements;
- all physical testing;
- acceptance criteria.
ChatGPT contributed substantially to:
- investigation of the GPIO software stack;
- analysis of the CPU behaviour;
- architecture;
- implementation;
- code review;
- lifecycle design;
- fault analysis;
- test planning.
I want to say this explicitly because I do not want to claim sole credit for work that was genuinely done together.
Without ChatGPT's contribution, this particular implementation in its current form would not have been possible for me.
At the same time, this is not being presented as a finished or perfect solution.
We are almost certainly missing something.
That is one of the main reasons for publishing it.
Very important: hardware scope
Everything described below has currently been developed and tested on one Raspberry Pi model only:
Raspberry Pi Zero 2 W
Test environment:
- Raspberry Pi Zero 2 W
- DietPi 10.6 RC2
- Debian 13.6 Trixie
- Linux
6.18.39+rpt-rpi-v8 - Node-RED 5.0.6
- Node.js 26.8.1
python3-libgpiod2.2.0/dev/gpiochip0
We are not claiming compatibility with Raspberry Pi 3, Raspberry Pi 4, Raspberry Pi 5, Compute Modules, or any other model at this stage.
This is especially important for Raspberry Pi 5, where the GPIO architecture involves RP1 and the gpiochip topology may differ.
The current implementation assumes the arrangement that we have actually verified on our Raspberry Pi Zero 2 W.
Support for other Raspberry Pi generations should be established through real testing rather than assumptions.
Testing from people with Pi 3, Pi 4, Pi 5 or Compute Modules would therefore be extremely valuable.
Why did we start this?
After moving a Raspberry Pi Zero 2 W installation to DietPi / Debian Trixie, the standard Node-RED GPIO nodes worked functionally after the earlier DietPi/lgpio issues had been addressed.
However, we later noticed another behaviour.
With a GPIO input active, one lgpio notification thread was consuming approximately 4–4.5% CPU continuously on our system.
We investigated this rather than assuming that the Node-RED node itself was responsible.
On this particular Trixie installation, the compatibility path was approximately:
node-red-node-pi-gpio
↓
Python RPi.GPIO
↓
python3-rpi-lgpio
↓
lgpio
The Python nrgpio.py process itself was not busy-looping.
We traced most of the CPU usage to one notification thread lower in the compatibility stack.
This distinction is important:
we are not saying that node-red-node-pi-gpio itself is badly designed.
The behaviour we observed appears to be associated with the compatibility stack used on this particular modern Debian Trixie installation.
Direct libgpiod test
We then tested the same GPIO directly with gpiomon.
The continuous CPU usage essentially disappeared.
That led us to try a completely separate approach using:
python3-libgpiod
directly.
The result became:
node-red-contrib-dietpi-gpiod
Current tested candidate:
v0.1.3
Source repository:
Design goals
The project deliberately does very little.
Currently it supports:
- Digital GPIO IN
- Digital GPIO OUT
That is all we needed.
There is currently no:
- PWM
- servo support
- analogue I/O
- keyboard/mouse functionality
- attempt to reproduce every feature of
node-red-node-pi-gpio
The goal is not feature parity.
The goal is a small, event-driven digital GPIO implementation for our Debian Trixie environment.
Architecture
Instead of starting a separate Python helper for every GPIO node, all dietpi-gpiod nodes share one Python helper process:
gpiod_helper.py
Conceptually:
Node-RED GPIO nodes
↓
one shared gpiod_helper.py
↓
python3-libgpiod
↓
Linux GPIO character device
Input GPIO request file descriptors are registered with a blocking selector.
There is no GPIO polling loop.
Inputs use libgpiod edge events.
Debounce is configured through libgpiod LineSettings.
CPU results
With real hardware connected and multiple GPIO inputs active, we measured load averages such as:
0.02 / 0.02 / 0.00
The shared helper itself was typically around:
0.0–0.2% CPU
during our tests on the Raspberry Pi Zero 2 W.
The important result for us was not the exact percentage.
It was that adding multiple GPIO inputs did not recreate the previous continuous per-input CPU overhead.
Real hardware testing
Our test board has:
- 8 digital inputs
- 8 relay outputs
All eight inputs were tested physically.
Inputs
| Function | Physical pin | BCM GPIO |
|---|---|---|
| Input 1 | 29 | GPIO5 |
| Input 2 | 22 | GPIO25 |
| Input 3 | 18 | GPIO24 |
| Input 4 | 16 | GPIO23 |
| Input 5 | 15 | GPIO22 |
| Input 6 | 13 | GPIO27 |
| Input 7 | 12 | GPIO18 |
| Input 8 | 11 | GPIO17 |
Relay outputs
| Function | Physical pin | BCM GPIO |
|---|---|---|
| Relay 1 | 31 | GPIO6 |
| Relay 2 | 32 | GPIO12 |
| Relay 3 | 33 | GPIO13 |
| Relay 4 | 36 | GPIO16 |
| Relay 5 | 35 | GPIO19 |
| Relay 6 | 38 | GPIO20 |
| Relay 7 | 37 | GPIO26 |
| Relay 8 | 40 | GPIO21 |
The runtime uses BCM/libgpiod line numbers.
Output state
For an output, the helper does not simply assume that the last command represents the current GPIO state.
After:
set_value()
it performs:
get_value()
and reports the logical GPIO state back to the Node-RED node.
The node status therefore shows, for example:
GPIO6 = 0
or:
GPIO6 = 1
This is logical GPIO readback only.
It must not be confused with physical feedback from a relay, contactor, valve, pump, etc.
Applications requiring confirmation that the external device really operated still need a separate physical feedback input.
GPIO ownership
Only one dietpi-gpiod node may own a BCM GPIO.
For example, if GPIO5 is already configured as an input and another dietpi-gpiod node attempts to use GPIO5 as an output, the second node is rejected:
GPIO5: already used by another dietpi-gpiod node
The original owner continues operating.
The rejected node remains unclaimed/error instead of pretending to control the GPIO.
An output node does not execute 0 or 1 commands until its GPIO has been successfully claimed.
Fault testing
We deliberately tested failure cases rather than only normal operation.
Forced helper crash
We deliberately killed the shared helper with:
kill -9 <helper-pid>
Observed behaviour:
- the helper terminated;
- an active test relay dropped out;
- Node-RED automatically started a new helper;
- only one helper process remained;
- GPIO inputs were reclaimed automatically;
- input events resumed;
- outputs returned to their configured
Initialstate.
The last point is intentional.
After a helper failure we do not restore the previous commanded output state.
Outputs return to their configured safe Initial state.
For example:
GPIO6 = 1
helper crashes
new helper starts
Initial = 0
GPIO6 returns to 0
Helper lifecycle
The JavaScript side uses explicit lifecycle states:
STOPPED
STARTING
RUNNING
STOPPING
The Python helper sends:
READY
before GPIO claims are issued.
Once shutdown has started, no new GPIO commands are sent to the old helper.
If new GPIO nodes require a helper while the old process is stopping, the code waits for the old helper to actually exit before starting another one.
This was added after we reproduced a real Deploy race in an earlier development version.
Node-RED Deploy test
We tested Deploy with an output active.
Observed behaviour:
- only one helper remained;
- no new kernel/libgpiod
Device or resource busyerror occurred; - all eight inputs recovered;
- the active relay returned to its configured
Initial = 0state.
Node-RED service restart
We also tested:
systemctl restart node-red
with an active relay.
The relay dropped out and returned to the configured safe Initial state.
Cold boot / power cycle
The Raspberry Pi Zero 2 W was tested from a complete power-off condition with the real relay board connected.
We did not observe any relay turning on unexpectedly during boot.
This is important for our application, although this behaviour obviously depends on both the Raspberry Pi and the external relay circuitry.
What we still do not know
A lot.
This project has been tested intensively on one Raspberry Pi Zero 2 W.
That does not make it universally correct.
Areas where community testing would be especially useful include:
- Raspberry Pi 3
- Raspberry Pi 4
- Raspberry Pi 5
- Compute Modules
- different Debian releases
- Raspberry Pi OS
- different libgpiod versions
- different gpiochip layouts
- long-duration operation
- different relay/input hardware
- unusual GPIO configurations
- failure scenarios we have not considered
We are almost certainly missing something.
Please treat this project as something to review and test, not as a claim that every possible case has been solved.
Relationship to the existing Node-RED GPIO project
Again:
this project is not intended to replace, criticise or attack node-red-node-pi-gpio.
The original project is much more mature and supports considerably more functionality.
It has also been an important reference for expected Node-RED behaviour and user-interface ideas.
Our implementation takes a different architectural path because we were investigating a specific behaviour on Debian Trixie.
Where ideas or behaviour have been informed by the existing project, we want that influence acknowledged openly and correctly.
We intend to comply strictly with all applicable open-source licence and attribution requirements.
If anyone sees anything in the source, attribution, naming, packaging or licensing that should be corrected, please tell us.
We will correct it.
Acknowledgements
I would especially like to thank:
@dceejay
for his work on the original Node-RED Raspberry Pi GPIO implementation and for everything he has contributed to Node-RED over the years.
And:
@MichaIng
for his work leading DietPi and for the earlier DietPi/Trixie GPIO discussion.
The question raised in that discussion — whether GPIO should really need to be moved away from the Raspberry Pi or whether a modern direct API could be used instead — was an important part of the path that eventually led us to experiment with libgpiod.
Open for review and contribution
This project is being published specifically so other people can inspect it.
Code review, criticism, fault testing, corrections and improvements are welcome.
If the architecture is wrong somewhere, please tell us.
If there is a race we missed, please try to break it.
If our assumptions about libgpiod are wrong on another Raspberry Pi generation, we want to know.
If the Node-RED packaging or naming should be changed, we would appreciate guidance.
If we have misunderstood or missed an open-source licensing requirement, please tell us and we will correct it.
And if this approach proves useful, contributions to make it more robust across Raspberry Pi generations would be very welcome.
@dceejay and @MichaIng — if you have the time, I would especially value your technical opinion on what we have built and on anything important that we may have overlooked.
The objective is simple:
we encountered a real problem, investigated it, built and tested one possible solution, and are now opening that work to the community so it can be checked, corrected and improved.
Source repository: