If you are trying to configure WiFi on Raspberry Pi boards running the modern Bookworm OS (Debian 12), the legacy wpa_supplicant.conf boot-partition trick is dead. Raspberry Pi OS now uses NetworkManager by default. To configure WiFi on a headless Raspberry Pi 5 or Pi Zero 2 W, you must use the nmcli command-line tool via SSH, or pre-configure it using the official Raspberry Pi Imager before flashing the SD card.

This guide targets the Raspberry Pi 5 (4GB variant) and the Raspberry Pi Zero 2 W. We will walk through the exact NetworkManager commands for headless WiFi setup, wire up a BME280 I2C environmental sensor, and deploy a robust Python datalogger with full error handling.

Bench Note: The Pi Zero 2 W only has a 2.4GHz WiFi radio. If your router is set to a 5GHz-only SSID or uses WPA3-Enterprise, the Zero 2 W will fail to associate. The Pi 4 and Pi 5 support both 2.4GHz and 5GHz bands.

Hardware Spec Sheet & Pin Mapping

Before we configure the network, let's establish the physical layer. This build uses the Broadcom BCM2712 (Pi 5) or BCM2710A1 (Zero 2 W) SoC. We are reading environmental data over the I2C bus to send over our newly configured WiFi connection.

Project Spec Sheet
Component Variant / Details Operating Voltage
Microcontroller/SBC Raspberry Pi 5 (4GB) or Pi Zero 2 W 5V (USB-C), 3.3V Logic
Sensor Bosch BME280 (I2C breakout board) 1.71V to 3.6V
OS Version Raspberry Pi OS Bookworm (64-bit, Lite) N/A
Network Protocol HTTP POST (JSON payload) N/A

I2C Pin Mapping Table

The Raspberry Pi GPIO header uses 3.3V logic. Never connect 5V I2C devices directly without a logic level converter, or you will fry the BCM SoC's I2C peripheral. The BME280 is natively 3.3V.

Raspberry Pi GPIO (BCM) Physical Pin # BME280 Breakout Pin Wire Color (Standard)
GPIO 2 (SDA1) 3 SDI / SDA Yellow
GPIO 3 (SCL1) 5 SCK / SCL Orange
3V3 Power 1 VCC / VIN Red
GND 6 GND Black

Step-by-Step: Configure WiFi on Raspberry Pi (Headless)

Assuming you have flashed your SD card using the Raspberry Pi Imager (and ideally enabled SSH and set your username/password in the Imager's advanced settings), boot the Pi and SSH into it. If you didn't pre-configure WiFi in the Imager, follow these steps to configure it via NetworkManager.

  1. Verify the WiFi interface is present:
    nmcli device status
    Look for wlan0 in the output. It should say disconnected or unavailable.
  2. Scan for available networks:
    sudo nmcli device wifi rescan
    nmcli device wifi list
    Identify your SSID and ensure the signal strength is adequate (greater than -70 dBm for reliable IoT telemetry).
  3. Connect to the WiFi network:
    sudo nmcli device wifi connect "YOUR_SSID" password "YOUR_PASSWORD"
    Note: If your SSID has spaces, wrap it in quotes. NetworkManager will automatically generate and save the connection profile in /etc/NetworkManager/system-connections/.
  4. Verify the IP assignment:
    ip -4 addr show wlan0
    You should see an inet address assigned by your router's DHCP server.
  5. Set the connection to auto-connect on boot:
    NetworkManager does this by default, but you can enforce it:
    sudo nmcli connection modify "YOUR_SSID" connection.autoconnect yes
Deprecation Warning: Do not waste time creating a wpa_supplicant.conf file in the /boot/firmware/ partition. While this worked on Bullseye and earlier, Bookworm's NetworkManager ignores it. See the official Raspberry Pi networking documentation for the migration details.

Complete Python IoT Datalogger Code

With WiFi configured and the BME280 wired to the I2C bus, we need code to read the sensor and POST the data to a local server (like Home Assistant, Node-RED, or a custom Flask API).

Prerequisites: Install the required libraries via terminal:
sudo apt update && sudo apt install python3-smbus2 python3-pip -y
pip3 install bme280 requests --break-system-packages

Save the following script as wifi_datalogger.py. This code includes explicit pin definitions, I2C error handling, and network timeout management.


import time
import sys
import requests
from smbus2 import SMBus
import bme280

# ==========================================
# PIN & CONFIGURATION DEFINITIONS
# ==========================================
# BCM GPIO Pin Definitions for I2C1
PIN_I2C_SDA = 2
PIN_I2C_SCL = 3
I2C_BUS_ID = 1

# BME280 I2C Address (0x76 if SDO is tied to GND, 0x77 if tied to VCC)
BME280_I2C_ADDR = 0x76

# Network Configuration
API_ENDPOINT = "http://192.168.1.100:8080/api/telemetry"
WIFI_TIMEOUT_SEC = 10
READ_INTERVAL_SEC = 60

def read_sensor_data(bus, address, calibration_params):
    """Reads BME280 data with hardware error handling."""
    try:
        data = bme280.sample(bus, address, calibration_params)
        return {
            "temperature_c": round(data.temperature, 2),
            "pressure_hpa": round(data.pressure, 2),
            "humidity_pct": round(data.humidity, 2),
            "timestamp": time.time()
        }
    except IOError as e:
        print(f"[HARDWARE ERROR] I2C Communication Failed: {e}")
        return None

def send_telemetry(payload):
    """Sends JSON payload over WiFi with network error handling."""
    headers = {'Content-Type': 'application/json'}
    try:
        response = requests.post(
            API_ENDPOINT, 
            json=payload, 
            headers=headers, 
            timeout=WIFI_TIMEOUT_SEC
        )
        response.raise_for_status()
        print(f"[SUCCESS] Data sent. HTTP Status: {response.status_code}")
    except requests.exceptions.ConnectionError as e:
        print(f"[NETWORK ERROR] Connection refused or WiFi down: {e}")
    except requests.exceptions.Timeout:
        print(f"[NETWORK ERROR] Request timed out after {WIFI_TIMEOUT_SEC}s. Check router.")
    except requests.exceptions.RequestException as e:
        print(f"[NETWORK ERROR] General request failure: {e}")

def main():
    print(f"Initializing I2C Bus {I2C_BUS_ID} (SDA: GPIO {PIN_I2C_SDA}, SCL: GPIO {PIN_I2C_SCL})...")
    
    try:
        bus = SMBus(I2C_BUS_ID)
        # Load calibration parameters from the sensor's non-volatile memory
        calibration_params = bme280.load_calibration_params(bus, BME280_I2C_ADDR)
        print("BME280 calibration loaded successfully.")
    except FileNotFoundError:
        print(f"[FATAL] I2C Bus {I2C_BUS_ID} not found. Is I2C enabled in raspi-config?")
        sys.exit(1)
    except Exception as e:
        print(f"[FATAL] Could not initialize BME280 at address 0x{BME280_I2C_ADDR:02X}. Check wiring. Error: {e}")
        sys.exit(1)

    print(f"Starting telemetry loop. Posting to {API_ENDPOINT} every {READ_INTERVAL_SEC}s.")
    
    while True:
        sensor_data = read_sensor_data(bus, BME280_I2C_ADDR, calibration_params)
        
        if sensor_data:
            print(f"Sensor Read: {sensor_data['temperature_c']}C | {sensor_data['humidity_pct']}%")
            send_telemetry(sensor_data)
        else:
            print("Skipping transmission due to sensor read failure.")
            
        time.sleep(READ_INTERVAL_SEC)

if __name__ == "__main__":
    main()

Debugging: First Three Things to Check When It Fails

When embedding SBCs in the field, things break. Here are the exact error strings you will see and the ranked causes for each.

1. Error: OSError: [Errno 101] Network is unreachable

This means the Python requests library cannot find a valid route to the destination IP.

  • Cause A (Most Likely): The WiFi interface (wlan0) dropped the connection or failed to obtain a DHCP lease. Run ip route to see if a default gateway exists.
  • Cause B: You are trying to reach a local IP address, but the Pi connected to a guest network or a VLAN that enforces client isolation (AP isolation).
  • Cause C: The API endpoint IP address in the code is incorrect or the target server is offline.

2. Error: smbus2.smbus.IOError: [Errno 121] Remote I/O error

This is a physical layer failure on the I2C bus.

  • Cause A (Most Likely): The BME280 I2C address is wrong. Run i2cdetect -y 1 in the terminal. If the sensor shows up at 0x77 instead of 0x76, update the BME280_I2C_ADDR variable in the code.
  • Cause B: Missing pull-up resistors. While many Adafruit/SparkFun breakouts include 4.7kΩ pull-ups on the SDA/SCL lines, cheap generic Amazon breakouts often omit them. If the bus is floating, add 4.7kΩ resistors between SDA/SCL and 3.3V.
  • Cause C: I2C is disabled in the OS. Run sudo raspi-config, navigate to Interface Options > I2C, and enable it.

3. Error: nmcli error: No network with SSID 'MyNetwork' found

This happens during the initial WiFi configuration step.

  • Cause A (Most Likely): You are using a Pi Zero 2 W and trying to connect to a 5GHz-only network. The Zero 2 W hardware physically lacks a 5GHz radio.
  • Cause B: The SSID is hidden. NetworkManager's wifi connect command struggles with hidden SSIDs unless you explicitly pass the hidden yes flag: sudo nmcli device wifi connect "SSID" password "PASS" hidden yes.

Extending and Simplifying the Build

To Simplify: If you just need a WiFi-connected relay or LED and don't need environmental data, drop the BME280 and the smbus2 dependencies. Use the Pi Zero 2 W to save $30+ on hardware costs, and rely purely on the requests library to poll a web API for on/off states.

To Extend: For industrial or outdoor enclosures, HTTP POST over WiFi is fragile. Extend this build by swapping the requests library for paho-mqtt to publish telemetry to an MQTT broker (like Mosquitto). Furthermore, if you are deploying the Pi 5 in a location where WiFi is unreliable, upgrade to a PoE+ HAT (Power over Ethernet) to get hardwired network connectivity and power over a single Cat6 cable, eliminating the need for WiFi configuration entirely.

Frequently Asked Questions

How do I configure WiFi on Raspberry Pi without a monitor before first boot?

Use the official Raspberry Pi Imager on your PC or Mac. When selecting your OS and storage, click the "Gear" icon (or press Ctrl+Shift+X) to open the Advanced Options. Check "Configure Wireless LAN", enter your SSID and password, and ensure you select the correct WiFi country code (this dictates which RF channels the radio is legally allowed to use). The Imager will inject the NetworkManager configuration files directly onto the SD card before you insert it into the Pi.

Why is my wpa_supplicant.conf file not working on Bookworm?

Starting with Raspberry Pi OS Bookworm (released late 2023), the underlying network stack shifted from dhcpcd and wpa_supplicant to NetworkManager. Dropping a wpa_supplicant.conf file into the boot partition is no longer parsed by the OS on first boot. You must either use the Raspberry Pi Imager to pre-configure the network, or boot the Pi, connect it via Ethernet (or a USB serial console), and use nmcli to configure the WiFi profile manually.

Can I configure WiFi on Raspberry Pi Pico W the same way?

No. The Raspberry Pi Pico W is a microcontroller (RP2040) running bare-metal code or MicroPython, not a Linux SBC. It does not use NetworkManager or nmcli. To configure WiFi on a Pico W, you must hardcode or load your SSID and password into your MicroPython script using the network module (e.g., wlan = network.WLAN(network.STA_IF); wlan.connect('SSID', 'PASS')). The Pico W also only supports 2.4GHz 802.11n WiFi and lacks the cryptographic hardware acceleration found on the Pi 4/5, making TLS/SSL handshake operations significantly slower.