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.
| 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.
| 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
- Interface State: Run
nmcli device status. Ifwlan0showsdisconnected, the radio is up but not associated. If it showsunmanaged, NetworkManager is ignoring the interface (check/etc/NetworkManager/NetworkManager.conf). - DNS Resolution: Run
resolvectl status. If the DNS Servers list is empty, yournmclistatic IP setup missed theipv4.dnsparameter, and the Pi cannot resolve local mDNS (.local) or external domains. - Power Throttling: Run
vcgencmd get_throttled. If it returns0x50005or 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.localmDNS, ensureavahi-daemonis 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
eth0while the Ethernet cable is unplugged. Verify theifnamein your profile usingnmcli 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.






