If you are setting up a headless Raspberry Pi 5 for an IoT sensor node, the direct answer for modern network configuration is to abandon legacy dhcpcd.conf and wpa_supplicant.conf files. Since the release of Pi OS Bookworm, the OS relies entirely on NetworkManager via the nmcli command-line tool. To assign a static IP, connect to WiFi, and ensure reliable MQTT telemetry, you must configure connection profiles directly through nmcli.

This guide walks through a complete headless network config for a Raspberry Pi 5 reading a BME280 environmental sensor and pushing data over MQTT, including the exact debugging steps when the network stack inevitably drops.

The Bookworm Shift: NetworkManager vs Legacy Config

The single biggest point of failure for makers returning to Raspberry Pi projects after a multi-year hiatus is attempting to edit /etc/wpa_supplicant/wpa_supplicant.conf or /etc/dhcpcd.conf. Those daemons are disabled by default in modern Pi OS. Understanding the architectural shift is critical before touching the terminal.

Table 1: Raspberry Pi Network Configuration Methods (2024-2026)
Method / Tool Status in Pi OS Bookworm+ Configuration Interface Best Use Case
NetworkManager (nmcli) Default & Fully Supported CLI (nmcli) or nmtui Headless nodes, static IPs, enterprise WPA2/3
dhcpcd Deprecated / Removed /etc/dhcpcd.conf Legacy Buster/Bullseye migrations only
wpa_supplicant Deprecated (Handled by NM) /boot/firmware/wpa_supplicant.conf None on modern OS; use Pi Imager advanced settings instead
systemd-networkd Available but not default /etc/systemd/network/*.network Custom Yocto builds or strict systemd environments

For production IoT nodes, nmcli is the only path forward. It handles interface state, DNS resolution via systemd-resolved, and connection persistence across reboots without requiring file-level edits. For a deeper dive into the underlying daemon changes, refer to the official Raspberry Pi NetworkManager documentation.

Hardware BOM & GPIO Pin Mapping

This build targets the Raspberry Pi 5 (8GB variant, SKU: SC1112). We pair it with the official 27W USB-C PD power supply (SKU: SC1095). Bench note: Do not use a standard 5V/3A phone charger. The Pi 5 will negotiate a lower power envelope and throttle the PCIe and USB controllers, which can cause intermittent drops on USB-to-Ethernet adapters or WiFi chipsets under load.

Parts List

  • Raspberry Pi 5 (8GB) with Official Active Cooler
  • Official 27W USB-C PD Power Supply
  • Adafruit BME280 I2C Temperature/Humidity/Pressure Sensor (Product ID: 2652)
  • 4-pin JST-SH or standard 0.1' header breakaway
  • 22 AWG silicone stranded wire (Red, Black, Blue, Yellow)

Pin Mapping Table

The BME280 operates on 3.3V logic. The Pi 5's GPIO header remains 3.3V tolerant, but the I2C pull-up resistors on the Pi 5 are slightly weaker than the Pi 4. Keep I2C wire runs under 30cm to avoid bus capacitance issues.

Table 2: Pi 5 GPIO to BME280 I2C Wiring
Pi 5 GPIO Pin Function Wire Color BME280 Pin
Pin 1 3V3 Power Red VIN / 3V3
Pin 6 Ground Black GND
Pin 3 (GPIO 2) I2C1 SDA Blue SDA
Pin 5 (GPIO 3) I2C1 SCL Yellow SCL

Step-by-Step Headless Network Config

Flash Raspberry Pi OS Lite (64-bit) using the Raspberry Pi Imager. In the Imager's 'Advanced Options' (Ctrl+Shift+X), set your hostname, enable SSH, and create a local user. Do not configure WiFi here if you intend to use a static IP on a specific interface; we will do it post-boot for precise control.

Boot the Pi, connect it temporarily via Ethernet or rely on the Imager's initial WiFi setup to get your first SSH session, then execute the following nmcli commands to lock in a static IP.

1. Create the WiFi Connection Profile

nmcli connection add type wifi con-name 'IoT-WiFi' ifname wlan0 ssid 'YourNetworkSSID' wifi-sec.key-mgmt wpa-psk wifi-sec.psk 'YourPassword'

2. Assign Static IPv4 and DNS

Assuming your subnet is 192.168.1.0/24 and gateway is .1, and you want the Pi locked to .50:

nmcli connection modify 'IoT-WiFi' ipv4.addresses 192.168.1.50/24 ipv4.gateway 192.168.1.1 ipv4.dns '1.1.1.1,8.8.8.8' ipv4.method manual

3. Configure Auto-Connect and Bring Up

nmcli connection modify 'IoT-WiFi' connection.autoconnect yes
nmcli connection up 'IoT-WiFi'

Verify the assignment with ip -4 addr show wlan0. You should see inet 192.168.1.50/24. You can now disconnect the Ethernet cable and SSH in via the static IP.

Python MQTT Sensor Code with Network Error Handling

Headless IoT nodes live and die by their error handling. A script that crashes on a momentary WiFi dropout is useless. The following Python script targets the Pi 5, reads the BME280 via smbus2, and publishes to an MQTT broker using the Eclipse Paho MQTT library. It includes explicit try/except blocks for the exact network errors that plague embedded Linux deployments.

Prerequisites: Run sudo apt install python3-pip python3-smbus2 and pip3 install RPi.bme280 paho-mqtt.

import time
import socket
import smbus2
import bme280
import paho.mqtt.client as mqtt

# --- Hardware & Network Definitions ---
I2C_BUS = 1
I2C_ADDRESS = 0x76  # BME280 default (check with i2cdetect -y 1 if 0x77)

MQTT_BROKER = 'mqtt.local.lan'
MQTT_PORT = 1883
MQTT_TOPIC = 'pi5/node01/environment'

# Initialize I2C and Sensor
bus = smbus2.SMBus(I2C_BUS)
calibration_params = bme280.load_calibration_params(bus, I2C_ADDRESS)

# Initialize MQTT Client
client = mqtt.Client(client_id='Pi5_Node01', protocol=mqtt.MQTTv311)

def publish_telemetry():
    while True:
        try:
            # Read Sensor
            data = bme280.sample(bus, I2C_ADDRESS, calibration_params)
            temp_c = round(data.temperature, 2)
            humidity = round(data.humidity, 1)
            pressure = round(data.pressure, 1)
            
            payload = f'{{"temp": {temp_c}, "hum": {humidity}, "pres": {pressure}}}'
            
            # Network Publish
            client.connect(MQTT_BROKER, MQTT_PORT, keepalive=60)
            client.publish(MQTT_TOPIC, payload, qos=1)
            client.disconnect()
            print(f'Published: {payload}')
            
        except socket.gaierror as e:
            # DNS Resolution Failure
            print(f'Network Error: DNS failure for {MQTT_BROKER}. Check nmcli DNS. ({e})')
        except ConnectionRefusedError as e:
            # Broker is down or port blocked
            print(f'Network Error: MQTT Broker refused connection. ({e})')
        except OSError as e:
            # I2C bus disconnected or general network socket timeout
            print(f'Hardware/Socket Error: {e}')
        except Exception as e:
            print(f'Unhandled Exception: {e}')
            
        # Sleep before next read
        time.sleep(10)

if __name__ == '__main__':
    publish_telemetry()

Debugging Network Failures: Exact Errors and Fixes

When the script above throws an exception, or the Pi drops off the network entirely, do not guess. Run through this decision path.

The First Three Things to Check

  1. Interface State: Run nmcli device status. If wlan0 shows disconnected, the radio is up but not associated. If it shows unmanaged, NetworkManager is ignoring the interface (check /etc/NetworkManager/NetworkManager.conf).
  2. DNS Resolution: Run resolvectl status. If the DNS Servers list is empty, your nmcli static IP setup missed the ipv4.dns parameter, and the Pi cannot resolve local mDNS (.local) or external domains.
  3. Power Throttling: Run vcgencmd get_throttled. If it returns 0x50005 or similar, the Pi is experiencing undervoltage. The WiFi chip is the first peripheral to brown out when the power supply sags, causing silent network drops.

Ranked Causes for Exact Error Strings

Error 1: socket.gaierror: [Errno -3] Temporary failure in name resolution

  • Cause A (Most Likely): The MQTT broker hostname (e.g., mqtt.local.lan) cannot be resolved. If using .local mDNS, ensure avahi-daemon is running on the broker, or switch to a static IP for the broker in your Python code.
  • Cause B: The Pi's DNS servers were not applied. Fix: nmcli con modify 'IoT-WiFi' ipv4.dns '192.168.1.1' && nmcli con up 'IoT-WiFi'.

Error 2: nmcli outputs Error: Connection activation failed: (2) Active connection could not be found

  • Cause A: The WiFi password is incorrect, or the SSID is out of range. The handshake timed out.
  • Cause B: You are trying to bring up a profile bound to eth0 while the Ethernet cable is unplugged. Verify the ifname in your profile using nmcli con show 'IoT-WiFi' | grep ifname.

Error 3: OSError: [Errno 121] Remote I/O error (Hardware, but often confused with network timeouts)

  • Cause: The BME280 sensor dropped off the I2C bus. On the Pi 5, this happens if the 3.3V rail sags during a WiFi transmission burst. Add a 100µF decoupling capacitor across the BME280 VIN and GND pins on the breadboard to stabilize the local power envelope.

Extending and Simplifying the Build

To Simplify: If you do not have an MQTT broker (like Mosquitto) running on your network, strip the Paho library out entirely. Replace the MQTT block with a lightweight Flask or FastAPI web server. Expose a single /api/sensor GET endpoint. This eliminates DNS resolution requirements for outbound connections, as you only need to know the Pi's static IP to poll it from a dashboard like Grafana or Home Assistant.

To Extend: For remote agricultural or off-grid nodes where WiFi is unreliable, add a LoRaWAN concentrator via the Pi 5's PCIe HAT (such as the Waveshare CoreRPi LoRaWAN Gateway). You will need to configure NetworkManager to treat the Ethernet bridge as the primary uplink while routing LoRa packets through the local SPI interface. The Pi 5's PCIe Gen 2 lane makes it the only Pi variant capable of handling high-throughput LoRaWAN packet forwarding without dropping I2C sensor reads.