To establish a reliable WiFi connection on a Raspberry Pi 5 running Raspberry Pi OS (Bookworm), you must use the nmcli command-line tool via NetworkManager. The legacy wpa_supplicant and dhcpcd methods are deprecated and will fail on current images. This guide walks through the exact hardware selection, NetworkManager configuration, and Python IoT implementation for a WiFi-connected sensor node, followed by a bench-tested debugging playbook for when the connection drops.
The Decision Path: Which Pi and WiFi Method to Choose?
Before flashing an SD card, match your project requirements to the correct board and network band. Raspberry Pi boards have vastly different WiFi silicon and power profiles.
| Project Requirement | Recommended Board | WiFi Band | Configuration Method |
|---|---|---|---|
| High-bandwidth video streaming / Edge AI | Raspberry Pi 5 (8GB) | 5 GHz (802.11ac) | NetworkManager (nmcli) |
| Remote, battery-powered sensor node | Raspberry Pi Zero 2 W | 2.4 GHz (802.11n) | NetworkManager (nmcli) |
| Mission-critical industrial logging | Raspberry Pi 5 (4GB) + PoE HAT | Disable WiFi, use Ethernet | Static IP via nmcli |
| Standard indoor IoT telemetry (Default Pick) | Raspberry Pi 5 (4GB) | 2.4 GHz | NetworkManager (nmcli) |
Parts List and Hardware Pin Mapping
This build implements a WiFi-connected temperature, humidity, and barometric pressure logger. We are using the Bosch BME280 sensor over the I2C bus.
Bill of Materials
- Compute: Raspberry Pi 5 (4GB or 8GB variant)
- OS: Raspberry Pi OS (Bookworm, 64-bit, Lite or Desktop)
- Sensor: Adafruit BME280 I2C Breakout (Product ID: 2652) or generic equivalent
- Wiring: 4x Female-to-Female silicone jumper wires (26 AWG)
- Power: Official Raspberry Pi 27W USB-C Power Supply
GPIO Pin Mapping Table
The Raspberry Pi 5 maintains the standard 40-pin header layout for I2C. Ensure your BME280 breakout is configured for I2C (not SPI) by checking the solder jumpers on the back of the PCB.
| BME280 Pin | Raspberry Pi 5 GPIO | Physical Pin # | Wire Color (Standard) |
|---|---|---|---|
| VIN / VCC | 3V3 Power | 1 | Red |
| GND | Ground | 6 | Black |
| SCK / SCL | GPIO 3 (SCL) | 5 | Yellow |
| SDI / SDA | GPIO 2 (SDA) | 3 | Blue |
Configuring the WiFi Connection on Raspberry Pi (Bookworm)
Bookworm replaced dhcpcd with NetworkManager. If you try to edit /etc/wpa_supplicant/wpa_supplicant.conf, your Pi will not connect. Use the nmcli (NetworkManager Command Line Interface) tool instead.
- Scan for available networks:
nmcli dev wifi list
Look for your SSID and note the signal strength (bars) and channel. Ensure you are connecting to the 2.4 GHz SSID if your router splits bands. - Connect to the network:
sudo nmcli dev wifi connect "Your_SSID_Name" password "Your_WiFi_Password"
Replace the placeholders with your exact credentials. Keep the quotes if your SSID contains spaces. - Verify the connection and IP assignment:
nmcli con show --active
You should see your SSID listed under the NAME column, with 'wifi' in the TYPE column. - Force a persistent DHCP lease (Optional but recommended for IoT):
If you need a static IP for your webhook target to reliably reach the Pi, set it via NetworkManager:
sudo nmcli con mod "Your_SSID_Name" 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
sudo nmcli con up "Your_SSID_Name"
Python IoT Code: Sending Sensor Data Over WiFi
This script targets the Raspberry Pi 5 running Bookworm. It reads the BME280 over I2C and POSTs the JSON payload to a local HTTP webhook over WiFi. It includes robust error handling for both hardware I2C faults and network timeouts.
Prerequisites: Install dependencies via sudo apt install python3-pip i2c-tools and pip3 install adafruit-circuitpython-bme280 requests. Enable I2C via sudo raspi-config (Interface Options -> I2C -> Enable).
import time
import board
import busio
import adafruit_bme280
import requests
import json
from requests.exceptions import ConnectionError, Timeout
# --- PIN & CONFIG DEFINITIONS ---
# I2C Pins: SDA = GPIO 2 (Pin 3), SCL = GPIO 3 (Pin 5)
I2C_SDA = board.SDA
I2C_SCL = board.SCL
# BME280 I2C Address: Default is 0x77. Adafruit breakouts often use 0x77,
# while generic Amazon/eBay clones frequently use 0x76.
BME_ADDRESS = 0x77
WEBHOOK_URL = 'http://192.168.1.100:8080/api/telemetry'
TX_INTERVAL_SEC = 60
HTTP_TIMEOUT_SEC = 5
def init_sensor():
"""Initialize I2C bus and BME280 sensor with error handling."""
try:
i2c = busio.I2C(I2C_SCL, I2C_SDA)
# Attempt to find the sensor at the defined address
sensor = adafruit_bme280.Adafruit_BME280_I2C(i2c, address=BME_ADDRESS)
sensor.sea_level_pressure = 1013.25
print(f"[OK] BME280 initialized at I2C address {hex(BME_ADDRESS)}")
return sensor
except ValueError as e:
print(f"[FATAL] I2C Device not found. Check wiring and address. Error: {e}")
raise
def send_telemetry(sensor):
"""Read sensor data and POST to webhook over WiFi."""
payload = {
'device_id': 'pi5-node-01',
'temperature_c': round(sensor.temperature, 2),
'humidity_pct': round(sensor.relative_humidity, 2),
'pressure_hpa': round(sensor.pressure, 2),
'altitude_m': round(sensor.altitude, 2)
}
try:
response = requests.post(
WEBHOOK_URL,
json=payload,
timeout=HTTP_TIMEOUT_SEC,
headers={'Content-Type': 'application/json'}
)
response.raise_for_status()
print(f"[TX] Success: {response.status_code} | Temp: {payload['temperature_c']}C")
except Timeout:
print(f"[ERR] HTTP Timeout: Server at {WEBHOOK_URL} did not respond in {HTTP_TIMEOUT_SEC}s.")
except ConnectionError as e:
print(f"[ERR] Network Unreachable: WiFi may be down or AP isolation is enabled. {e}")
except requests.exceptions.HTTPError as e:
print(f"[ERR] HTTP Error: Server rejected payload. {e}")
if __name__ == '__main__':
bme_sensor = init_sensor()
print(f"[INFO] Starting telemetry loop. Posting every {TX_INTERVAL_SEC}s.")
while True:
try:
send_telemetry(bme_sensor)
except KeyboardInterrupt:
print("\n[INFO] Script terminated by user.")
break
except Exception as e:
print(f"[ERR] Unexpected runtime error: {e}")
time.sleep(TX_INTERVAL_SEC)
Debugging: Exact Error Strings and Ranked Causes
When a headless Pi drops off the network or fails to read sensors, you need a systematic approach. Here are the exact error strings you will encounter, ranked by probability, and the first three things to check when any failure occurs.
- Is the WiFi interface actually up? Run
nmcli device status. Ifwlan0says 'disconnected', runsudo nmcli con up "Your_SSID_Name". - Do you have a valid route to the internet/gateway? Run
ip route get 1.1.1.1. If it returns 'Network is unreachable', your DHCP lease failed or the router dropped the client. - Is the I2C bus locked up? Run
i2cdetect -y 1. If you see all dashes or the script hangs, the Pi 5's I2C bus has crashed due to a loose SDA line. Reboot and check physical connections.
Error String 1: requests.exceptions.ConnectionError: ... [Errno 101] Network is unreachable
This means the Python requests library cannot find a network interface with a valid route to the destination IP.
- Cause 1 (Most Likely): The WiFi interface dropped due to power management. Bookworm sometimes aggressively suspends
wlan0. Fix: Disable WiFi power save viasudo nmcli con mod "Your_SSID_Name" 802-11-wireless.powersave 2. - Cause 2: You are trying to reach a local IP (e.g.,
192.168.1.100) but the Pi is connected to a guest network or a subnet with AP (Access Point) Isolation enabled. Fix: Move the Pi to the primary LAN VLAN. - Cause 3: The DHCP server assigned an APIPA address (169.254.x.x). Fix: Reboot the router and the Pi, or assign a static IP as shown in the configuration steps.
Error String 2: ValueError: No I2C device at address: 0x77
The adafruit-circuitpython-bme280 library scanned the bus and found nothing at the specified hex address.
- Cause 1 (Most Likely): You are using a generic BME280 clone that defaults to address
0x76. Fix: ChangeBME_ADDRESS = 0x77to0x76in the Python script. - Cause 2: SDA and SCL wires are swapped. The Pi 5 will not auto-correct this. Fix: Verify against the pin mapping table above.
- Cause 3: I2C is disabled in the OS. Fix: Run
sudo raspi-config, navigate to Interface Options -> I2C, and enable it. Reboot.
Error String 3: TimeoutError: The read operation timed out
The Pi successfully sent the TCP SYN packet, but the target server never replied with a SYN-ACK within the HTTP_TIMEOUT_SEC window.
- Cause 1 (Most Likely): The target webhook server (e.g., your local Node-RED or Flask instance) is crashed or not listening on port 8080. Fix: Check the server logs.
- Cause 2: DNS resolution failure if using a hostname instead of an IP. Fix: Ping the hostname. If it fails, add a local DNS entry or switch to a raw IP address in
WEBHOOK_URL.
Extending and Simplifying the Build
Once the baseline WiFi connection and I2C telemetry are stable, you will inevitably need to adapt the node for production or reduce its footprint.
How to Extend the Build
- Switch to MQTT: HTTP POST is heavy for simple telemetry. Replace the
requestsblock with thepaho-mqttlibrary. MQTT maintains a persistent TCP connection over WiFi, drastically reducing the latency and overhead of repeated HTTP handshakes. - Add a Watchdog Timer: The Raspberry Pi 5 includes a hardware watchdog. Enable it via
systemdto automatically reboot the Pi if the Python script hangs or the WiFi driver kernel-panics. AddRuntimeWatchdogSec=60to/etc/systemd/system.conf. - Implement TLS/SSL: If sending data to a cloud broker (like Adafruit IO or AWS IoT), update the
requests.postcall to use anhttps://URL and pass averify='/path/to/cert.pem'argument to enforce certificate validation.
How to Simplify the Build
If you realize you do not need a full Linux kernel, desktop environment, or multi-threading capabilities, downgrade to the Raspberry Pi Pico W. The Pico W uses the same RP2040 silicon but includes an Infineon CYW43439 WiFi/BT chip. It draws roughly 1/10th the idle power of a Pi 5, making it viable for solar or battery-powered outdoor enclosures. You would rewrite the logic in MicroPython using the network and urequests modules, stripping away the overhead of NetworkManager and full Python 3.






