gpiozero library is the definitive raspberry pi command line setup. You get a $15 board with enough processing headroom for TLS-encrypted MQTT payloads, while the OS Lite image strips out the desktop environment to save 200MB of RAM and eliminate GUI-related boot delays.
This guide walks through building a headless, CLI-managed environmental monitor that reads a DHT22 sensor and triggers a 5V cooling fan via a transistor-driven relay. We will cover the exact hardware driver circuit (never wire a relay directly to a GPIO pin), the modern Bookworm OS network configuration, and how to debug the most common CLI pin factory errors.
Decision Tree: Picking the Right Pi for CLI Headless Builds
Not every project needs a flagship processor. When your primary interface is the raspberry pi command line via SSH, you are optimizing for low idle power draw, small physical footprint, and sufficient RAM for background daemons.
| Use Case | Board Variant | RAM | Idle Power | Verdict |
|---|---|---|---|---|
| Dedicated Sensor/Relay Node | Pi Zero 2 W (SC0020) | 512MB | ~1.2W | Default Pick |
| Local MQTT Broker + Node | Pi 4 Model B | 2GB | ~2.7W | Choose if hosting Mosquitto locally |
| Edge Vision / AI Inference | Pi 5 | 8GB | ~3.8W | Overkill for basic GPIO polling |
The Decision: If your build strictly polls sensors and toggles relays via CLI scripts, terminate your search and buy the Pi Zero 2 W. It handles Python threading and cron jobs effortlessly while drawing less than 2 watts at the wall.
Parts List and Hardware Spec Sheet
| Component | Exact Variant / Part Number | Est. Cost |
|---|---|---|
| Microcontroller | Raspberry Pi Zero 2 W (SC0020) | $15.00 |
| Temp/Humidity Sensor | DHT22 / AM2302 (with 10k pull-up) | $6.50 |
| Relay Module | Songle SRD-05VDC-SL-C (5V Coil) | $3.00 |
| Switching Transistor | 2N2222A NPN (TO-92 package) | $0.15 |
| Base Resistor | 1kΩ Carbon Film (1/4W) | $0.02 |
| Flyback Diode | 1N4007 Rectifier | $0.10 |
GPIO Pin Mapping and Driver Circuit
Wire the components according to this physical mapping. The 1N4007 flyback diode must be placed in reverse bias across the relay coil (cathode to 5V, anode to the transistor collector) to absorb the inductive kickback when the coil de-energizes.
| Pi Physical Pin | BCM GPIO | Component Target | Wire Color |
|---|---|---|---|
| Pin 2 | 5V Power | Relay VCC & DHT22 VCC | Red |
| Pin 6 | GND | Relay GND, DHT22 GND, 2N2222 Emitter | Black |
| Pin 7 | GPIO 4 | DHT22 Data (add 10k pull-up to 5V) | Yellow |
| Pin 11 | GPIO 17 | 1kΩ Resistor -> 2N2222 Base | Orange |
Step-by-Step CLI Setup and Compilable Code
Raspberry Pi OS 'Bookworm' fundamentally changed headless networking and GPIO backends. The old wpa_supplicant.conf drop-in method is deprecated in favor of NetworkManager, and RPi.GPIO has been replaced by lgpio.
- Flash and Configure Headless Access: Use Raspberry Pi Imager. In the advanced settings (Ctrl+Shift+X), enable SSH, set your hostname to
sensor-node, and input your WiFi credentials. This configuresNetworkManagerautomatically on first boot. - SSH and Update: Connect via
ssh user@sensor-node.local. Runsudo apt update && sudo apt upgrade -y. - Install Modern GPIO Backends: Bookworm requires the
lgpioC-library for Python to access/dev/gpiochip0. Run:sudo apt install python3-gpiozero python3-rpi-lgpio python3-pip - Install DHT Library: Install the Adafruit CircuitPython DHT library in a virtual environment (PEP 668 compliance in Bookworm prevents global pip installs):
python3 -m venv ~/env && source ~/env/bin/activatepip install adafruit-circuitpython-dht
Below is the complete, compilable Python script. It targets the Pi Zero 2 W, reads the DHT22 on GPIO 4, and engages the relay on GPIO 17 if the temperature exceeds 28.0°C.
#!/usr/bin/env python3
import time
import sys
import board
import adafruit_dht
from gpiozero import OutputDevice
# --- PIN DEFINITIONS ---
RELAY_PIN = 17
DHT_PIN = board.D4
# --- HARDWARE INITIALIZATION ---
# active_high=False because we are using an NPN transistor low-side switch
# When GPIO goes HIGH, transistor saturates, pulling relay IN to GND
relay = OutputDevice(RELAY_PIN, active_high=True, initial_value=False)
dht_device = adafruit_dht.DHT22(DHT_PIN)
def read_sensor():
# DHT sensors frequently throw checksum errors; retry logic is mandatory
for attempt in range(5):
try:
temp_c = dht_device.temperature
humidity = dht_device.humidity
if temp_c is not None and humidity is not None:
return temp_c, humidity
except RuntimeError as err:
# Common DHT checksum/timing error, wait and retry
time.sleep(0.5)
continue
except Exception as err:
print(f'Fatal sensor error: {err}')
sys.exit(1)
return None, None
def main():
print('Starting headless environmental monitor...')
try:
while True:
temp, hum = read_sensor()
if temp is not None:
print(f'Temp: {temp:.1f}C | Humidity: {hum:.1f}%')
if temp > 28.0 and not relay.is_active:
relay.on()
print('RELAY ENGAGED: Cooling fan ON')
elif temp <= 27.0 and relay.is_active:
relay.off()
print('RELAY DISENGAGED: Cooling fan OFF')
else:
print('Failed to retrieve stable sensor data.')
time.sleep(10)
except KeyboardInterrupt:
print('\nInterrupt received. Cleaning up GPIO...')
finally:
relay.off()
relay.close()
dht_device.exit()
print('Hardware safely de-energized.')
if __name__ == '__main__':
main()
Debugging: Fixing `gpiozero.exc.BadPinFactory`
When transitioning to Bookworm or running headless CLI scripts, the most common roadblock is the pin factory error. If your script crashes immediately with the following exact string:
gpiozero.exc.BadPinFactory: Unable to load any default pin factory!
This means gpiozero cannot find a valid backend to translate BCM pin numbers to the Linux lgpio character device. Here are the ranked causes and fixes:
| Rank | Cause | CLI Fix |
|---|---|---|
| 1 | Missing Bookworm lgpio backend | Run sudo apt install python3-rpi-lgpio |
| 2 | Running inside Docker without device mapping | Add --device /dev/gpiomem:/dev/gpiomem to your docker run command |
| 3 | Executing via sudo in a venv incorrectly | Never use sudo with Python venvs; fix group permissions instead: sudo usermod -aG gpio $USER |
The First Three Things to Check When It Fails
- Verify the backend is installed: Run
dpkg -l | grep lgpio. If it returns nothing, your OS is missing the hardware translation layer. - Check character device permissions: Run
ls -l /dev/gpiochip0. Your user must be in thegpiogroup, or the script must be run as root (not recommended). - Confirm you are in the virtual environment: If you installed
adafruit-circuitpython-dhtin a venv but run the script with the system Python, it will fail to import. Alwayssource ~/env/bin/activatefirst.
Extending and Simplifying the CLI Build
Once the base script is stable, you have two paths depending on your project scope.
How to Extend: Systemd and Cron
For a production headless node, do not rely on screen or tmux. Wrap the Python script in a systemd service so it survives reboots and automatically restarts on crash. Create /etc/systemd/system/climate-monitor.service:
[Unit]
Description=Headless Climate Monitor
After=network.target
[Service]
ExecStart=/home/pi/env/bin/python /home/pi/monitor.py
WorkingDirectory=/home/pi
Restart=always
User=pi
[Install]
WantedBy=multi-user.target
Enable it via the raspberry pi command line with sudo systemctl enable --now climate-monitor.service.
How to Simplify: Native Bash with `pinctrl`
If you don't need Python and just want to toggle a pin from a bash script, skip gpiozero entirely. Bookworm includes the pinctrl utility natively. To set GPIO 17 as an output and drive it high directly from the terminal:
pinctrl set 17 op dh
This is the ultimate simplification for basic CI/CD pipeline hardware triggers or bash-based cron jobs where spinning up a Python interpreter is unnecessary overhead.






