Difficulty: Intermediate | Time: 45 Minutes | Board Target: Raspberry Pi Zero 2 W (Bookworm Lite)

If you are searching for dhcpcd raspberry pi because you just encountered a sudo: dhcpcd: command not found error on a fresh install, here is the direct answer: Raspberry Pi OS Bookworm completely removed dhcpcd and wpa_supplicant, replacing them with NetworkManager. Any tutorial written before late 2023 that tells you to edit /etc/dhcpcd.conf will fail on current systems.

In this guide, we will cover exactly how to debug network failures under the new NetworkManager regime, and then apply that knowledge to build a robust, headless BME280 environmental sensor node that verifies its network state via nmcli before pushing data over MQTT.

The dhcpcd vs NetworkManager Shift (Why Your Tutorial Failed)

For nearly a decade, the Raspberry Pi ecosystem relied on dhcpcd for DHCP client duties and wpa_supplicant for Wi-Fi management. With the release of Raspberry Pi OS Bookworm, the foundation aligned with mainstream enterprise Linux distributions by adopting NetworkManager as the single source of truth for all networking.

Networking Daemon Comparison: Bullseye vs Bookworm
FeatureOS Bullseye (Legacy)OS Bookworm (Current)
DHCP ClientdhcpcdNetworkManager (dhclient/internal)
Wi-Fi Authwpa_supplicantNetworkManager (wpa_supplicant/iwd backend)
Static IP Config/etc/dhcpcd.confnmcli or /etc/NetworkManager/system-connections/
CLI Toolifconfig / iwconfignmcli / resolvectl

If you attempt to run legacy commands on Bookworm, you will hit one of these exact error strings:

  • sudo: dhcpcd: command not found
  • Failed to start dhcpcd.service: Unit dhcpcd.service not found.
  • wpa_cli: command not found

First Three Things to Check When Networking Fails

When your headless Pi drops off the network or refuses to grab an IP address on Bookworm, do not waste time looking for dhcpcd logs. Follow this ranked diagnostic path:

1. Verify NetworkManager Interface State

The most common failure is an interface stuck in a 'disconnected' or 'unmanaged' state. Run:

nmcli device status

If your wlan0 or eth0 shows as unmanaged, NetworkManager is ignoring it, often due to a leftover configuration file in /etc/network/interfaces. Delete or empty that file and reboot.

2. Check Wi-Fi Connection Profiles

Unlike the old wpa_supplicant.conf method, NetworkManager stores Wi-Fi credentials as individual connection profiles. If your Pi isn't connecting to Wi-Fi on boot, list the saved profiles:

nmcli connection show

If your network isn't listed, add it via CLI:

sudo nmcli device wifi connect "YourSSID" password "YourPassword"

3. Inspect DNS and Routing via resolvectl

If you can ping 8.8.8.8 but cannot resolve domain names, dhcpcd isn't the culprit. Bookworm uses systemd-resolved. Check your DNS assignment with:

resolvectl status

Ensure your active interface has a valid DNS server assigned by your router's DHCP.

Project Build: Headless BME280 MQTT Sensor Node

Let's build a practical IoT node. This script reads temperature and humidity from a BME280 sensor over I2C, but crucially, it uses nmcli to verify the network is actually connected via NetworkManager before attempting an MQTT publish. This prevents the script from hanging on socket timeouts during boot-up network delays.

Parts List

  • Board: Raspberry Pi Zero 2 W (Running Raspberry Pi OS Bookworm Lite, 64-bit)
  • Sensor: Adafruit BME280 I2C/SPI Breakout (Product ID: 2652)
  • Power: 5V 2.5A USB-C Power Supply (Official Raspberry Pi)
  • Wiring: 4x Silicone jumper wires (Female-to-Female)

Pin Mapping Table

BME280 PinRaspberry Pi Zero 2 W GPIOPhysical Pin #Function
VIN3V3 Power1Power (3.3V)
GNDGround6Ground
SCK (SCL)GPIO 3 (SCL)5I2C Clock
SDI (SDA)GPIO 2 (SDA)3I2C Data
Bench Tip: Ensure I2C is enabled via sudo raspi-config (Interface Options > I2C). On Bookworm Lite, the I2C kernel module isn't loaded by default until you explicitly enable it.

Complete Python Implementation

Install the required dependencies first: sudo apt install python3-pip network-manager followed by pip3 install paho-mqtt smbus2 RPi.bme280.

import time
import subprocess
import json
import paho.mqtt.client as mqtt
from smbus2 import SMBus
import bme280

# --- PIN & BUS DEFINITIONS ---
# I2C Bus 1 is the default on Pi Zero 2 W (GPIO 2/SDA, GPIO 3/SCL)
I2C_BUS_ID = 1
BME280_I2C_ADDR = 0x77  # Adafruit breakout default; use 0x76 for generic clones

# --- NETWORK & MQTT CONFIG ---
MQTT_BROKER_IP = "192.168.1.50"
MQTT_PORT = 1883
MQTT_TOPIC = "home/office/environment"

def check_network_nmcli():
    """Checks NetworkManager state via nmcli to ensure we are connected."""
    try:
        result = subprocess.run(
            ["nmcli", "-t", "-f", "STATE", "g"],
            capture_output=True, text=True, check=True, timeout=5
        )
        # nmcli returns 'connected' when an active route exists
        return "connected" in result.stdout.strip()
    except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError):
        return False

def on_connect(client, userdata, flags, rc, properties=None):
    if rc == 0:
        print("[MQTT] Connected to broker.")
    else:
        print(f"[MQTT] Connection failed with code {rc}")

def main():
    # Initialize I2C and Sensor
    try:
        bus = SMBus(I2C_BUS_ID)
        sensor = bme280.BME280(i2c_dev=bus, i2c_addr=BME280_I2C_ADDR)
        print("[SENSOR] BME280 initialized successfully.")
    except Exception as e:
        print(f"[FATAL] I2C Sensor initialization failed: {e}")
        return

    # Initialize MQTT
    client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="pi_zero_2_env")
    client.on_connect = on_connect

    print("[SYSTEM] Waiting for NetworkManager to establish connection...")
    
    # Network verification loop (replaces legacy dhcpcd wait scripts)
    retry_count = 0
    while not check_network_nmcli():
        retry_count += 1
        if retry_count > 12:
            print("[ERROR] Network not connected after 60 seconds. Exiting.")
            return
        time.sleep(5)

    print("[NETWORK] Connection verified via nmcli.")
    
    try:
        client.connect(MQTT_BROKER_IP, MQTT_PORT, 60)
        client.loop_start()
    except Exception as e:
        print(f"[MQTT] Broker connection error: {e}")
        return

    # Main telemetry loop
    try:
        while True:
            temp_c = round(sensor.get_temperature(), 2)
            humidity = round(sensor.get_humidity(), 2)
            pressure = round(sensor.get_pressure(), 2)

            payload = json.dumps({
                "temp_c": temp_c,
                "humidity_pct": humidity,
                "pressure_hpa": pressure
            })

            result = client.publish(MQTT_TOPIC, payload, qos=1)
            if result.rc == mqtt.MQTT_ERR_SUCCESS:
                print(f"[TX] Published: {payload}")
            else:
                print(f"[TX] Publish failed: {result.rc}")

            time.sleep(60)
            
    except KeyboardInterrupt:
        print("\n[SYSTEM] Shutting down gracefully.")
    finally:
        client.loop_stop()
        client.disconnect()
        bus.close()

if __name__ == "__main__":
    main()

Extending or Simplifying the Build

Depending on your deployment environment, you may need to alter the scope of this project.

To Simplify (Local Data Logging): If you don't have an MQTT broker or reliable Wi-Fi, strip out the paho-mqtt and nmcli logic. Mount a USB thumb drive to /mnt/usb via /etc/fstab, and use Python's native csv module to append a row every 60 seconds. This turns the node into a rugged, offline data logger that requires zero network configuration.

To Extend (Cellular Off-Grid): If deploying to a remote greenhouse or shed, swap the Wi-Fi dependency for a cellular HAT like the Waveshare SIM7600G-H 4G HAT. NetworkManager natively supports ModemManager. You can configure the cellular APN using nmcli connection add type gsm ifname '*' con-name cell autoconnect yes apn 'your.apn.com'. The Python script above will work without modification, as nmcli -t -f STATE g will still report 'connected' once the cellular modem establishes a PPP or MBIM data session.

Frequently Asked Questions

How do I set a static IP without dhcpcd on Raspberry Pi?

On Bookworm, you assign a static IP using nmcli. For an Ethernet connection (eth0), the command is:

sudo nmcli con mod "Wired connection 1" ipv4.addresses 192.168.1.50/24 ipv4.gateway 192.168.1.1 ipv4.dns "8.8.8.8,1.1.1.1" ipv4.method manual
sudo nmcli con up "Wired connection 1"

Replace "Wired connection 1" with your actual connection name found via nmcli con show.

Can I reinstall dhcpcd on Raspberry Pi OS Bookworm?

Technically, yes, via sudo apt install dhcpcd5, but do not do this. Installing it will conflict with NetworkManager, resulting in race conditions where both daemons fight for control of the routing table and DNS resolution. You will experience random network drops and intermittent SSH timeouts. Stick to NetworkManager.

Why is my Raspberry Pi dropping Wi-Fi connection on Bookworm?

The most common cause on the Pi Zero 2 W and Pi 4 is aggressive power management on the Wi-Fi chip. NetworkManager doesn't disable power saving by default. To fix this, create a NetworkManager dispatcher script or disable Wi-Fi power save globally by creating /etc/NetworkManager/conf.d/default-wifi-powersave-on.conf with the following contents:

[connection]
wifi.powersave = 2

(Note: 2 means disable power save; 3 means enable).

How do I enable Wi-Fi on headless Raspberry Pi without wpa_supplicant?

The old method of dropping a wpa_supplicant.conf file into the /boot/ partition no longer works on Bookworm. Instead, use the official Raspberry Pi Imager on your PC/Mac. Click the gear icon (Advanced Options) before flashing the OS, and enter your Wi-Fi SSID and password there. The Imager securely injects the NetworkManager configuration into the image before the first boot.