For headless hardware control, the Raspberry Pi Zero 2 W running Raspberry Pi OS Lite (64-bit) and Python's 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 CaseBoard VariantRAMIdle PowerVerdict
Dedicated Sensor/Relay NodePi Zero 2 W (SC0020)512MB~1.2WDefault Pick
Local MQTT Broker + NodePi 4 Model B2GB~2.7WChoose if hosting Mosquitto locally
Edge Vision / AI InferencePi 58GB~3.8WOverkill 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

Bench Note: The Pi's 3.3V GPIO pins can source a maximum of 16mA each, with a total bank limit of 50mA. A standard 5V Songle relay coil requires ~70mA to latch. Driving it directly from the Pi will cause a brownout and permanently damage the SoC's GPIO pad. We use a 2N2222A NPN transistor as a low-side switch to isolate the Pi from the relay coil.
ComponentExact Variant / Part NumberEst. Cost
MicrocontrollerRaspberry Pi Zero 2 W (SC0020)$15.00
Temp/Humidity SensorDHT22 / AM2302 (with 10k pull-up)$6.50
Relay ModuleSongle SRD-05VDC-SL-C (5V Coil)$3.00
Switching Transistor2N2222A NPN (TO-92 package)$0.15
Base Resistor1kΩ Carbon Film (1/4W)$0.02
Flyback Diode1N4007 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 PinBCM GPIOComponent TargetWire Color
Pin 25V PowerRelay VCC & DHT22 VCCRed
Pin 6GNDRelay GND, DHT22 GND, 2N2222 EmitterBlack
Pin 7GPIO 4DHT22 Data (add 10k pull-up to 5V)Yellow
Pin 11GPIO 171kΩ Resistor -> 2N2222 BaseOrange

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.

  1. 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 configures NetworkManager automatically on first boot.
  2. SSH and Update: Connect via ssh user@sensor-node.local. Run sudo apt update && sudo apt upgrade -y.
  3. Install Modern GPIO Backends: Bookworm requires the lgpio C-library for Python to access /dev/gpiochip0. Run:
    sudo apt install python3-gpiozero python3-rpi-lgpio python3-pip
  4. 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/activate
    pip 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:

RankCauseCLI Fix
1Missing Bookworm lgpio backendRun sudo apt install python3-rpi-lgpio
2Running inside Docker without device mappingAdd --device /dev/gpiomem:/dev/gpiomem to your docker run command
3Executing via sudo in a venv incorrectlyNever use sudo with Python venvs; fix group permissions instead: sudo usermod -aG gpio $USER

The First Three Things to Check When It Fails

  1. Verify the backend is installed: Run dpkg -l | grep lgpio. If it returns nothing, your OS is missing the hardware translation layer.
  2. Check character device permissions: Run ls -l /dev/gpiochip0. Your user must be in the gpio group, or the script must be run as root (not recommended).
  3. Confirm you are in the virtual environment: If you installed adafruit-circuitpython-dht in a venv but run the script with the system Python, it will fail to import. Always source ~/env/bin/activate first.

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.