To set up WiFi on a Raspberry Pi running Pi OS Bookworm or newer, you must use NetworkManager via the nmcli command-line tool. The legacy wpa_supplicant.conf method is deprecated and will fail on modern images. For a resilient, headless IoT deployment, the most reliable approach is to pair a Raspberry Pi 5 with an I2C OLED for visual IP feedback and a Python watchdog script that programmatically manages connections and handles dropouts.
Hardware Decision Path: Which Pi for WiFi IoT?
Choosing the right board variant dictates your WiFi capabilities, power envelope, and physical footprint. Below is the decision matrix for embedded WiFi nodes in 2026.
| Criteria | Raspberry Pi 5 (8GB) | Raspberry Pi Zero 2 W | Pi 4 Model B (4GB) |
|---|---|---|---|
| WiFi Standard | Dual-band 802.11ac (WiFi 5) | Single-band 802.11n (WiFi 4, 2.4GHz) | Dual-band 802.11ac (WiFi 5) |
| Power Requirement | 5V/5A (27W USB-C PD) | 5V/2.5A (Micro-USB or GPIO) | 5V/3A (USB-C) |
| PCIe / Ethernet | PCIe 2.0, Gigabit Ethernet | None (USB OTG only) | Gigabit Ethernet (shared USB) |
| Best Use Case | Edge AI, local MQTT brokers, heavy telemetry | Battery-powered sensors, simple HTTP polling | Legacy retro-fits, basic IP cameras |
BOM and GPIO Pin Mapping
This build targets the Raspberry Pi 5 (8GB) running Pi OS Bookworm (64-bit). We are adding an SSD1306 I2C OLED to display the WiFi IP address and connection state locally, eliminating the need to scan your router's DHCP table when debugging headless nodes.
Parts List
- Board: Raspberry Pi 5 (8GB)
- Power: Official Raspberry Pi 27W USB-C PD Power Supply (Critical: third-party 5V/3A phone chargers will trigger brownout warnings and disable the WiFi chip).
- Display: 0.96-inch SSD1306 I2C OLED (128x64, 4-pin)
- Wiring: 4x Female-to-Female Dupont jumper wires
GPIO Pin Mapping Table
| OLED Pin | Pi 5 GPIO / Function | Physical Pin # | Notes |
|---|---|---|---|
| GND | Ground | 6 | Common ground reference |
| VCC | 3V3 Power | 1 | Do NOT use 5V; SSD1306 logic is 3.3V |
| SCL | GPIO 3 (I2C1 SCL) | 5 | Hardware I2C clock line |
| SDA | GPIO 2 (I2C1 SDA) | 3 | Hardware I2C data line |
Step-by-Step Headless WiFi Provisioning
Before writing custom code, the OS must be provisioned with baseline network access. Because wpa_supplicant is deprecated, use the Raspberry Pi Imager's advanced settings or a direct nmcli command over a serial console.
- Flash the OS: Use Raspberry Pi Imager to flash Pi OS Bookworm (64-bit) Lite. In the 'OS Customisation' menu, set your hostname, enable SSH (password or key), and enter your WiFi SSID and password. The Imager now writes a
NetworkManagerconfiguration file directly to the boot partition. - Boot and Verify: Power the Pi 5 with the 27W supply. Wait 60 seconds for the first boot resize and NetworkManager initialization.
- SSH and Check State: Connect via SSH. Run
nmcli device status. You should seewlan0listed asconnected. - Set the Regulatory Domain: This is the most missed step. If your country code isn't set, the WiFi chip restricts channels and transmit power. Run
sudo raspi-config→ Localisation Options → WLAN Country, and select your region. Reboot. - Install Python Dependencies: Run
sudo apt update && sudo apt install python3-pip python3-smbus i2c-tools -y. Then install the OLED library:pip3 install luma.oled.
Python IoT WiFi Manager with Auto-Recovery
This script targets the Pi 5 hardware I2C bus. It uses subprocess to call nmcli, checks connection health, attempts a reconnect if the network drops, and renders the IP address to the SSD1306 OLED. It includes robust error handling for missing networks and I2C bus faults.
import subprocess
import time
import socket
from luma.core.interface.serial import i2c
from luma.oled.device import ssd1306
from PIL import ImageFont, ImageDraw, Image
# --- Hardware & Network Definitions ---
I2C_PORT = 1
I2C_ADDRESS = 0x3C
TARGET_SSID = 'MyIoTNetwork'
TARGET_PASS = 'SuperSecretPassword123'
# --- Initialize OLED Display ---
try:
serial = i2c(port=I2C_PORT, address=I2C_ADDRESS)
device = ssd1306(serial)
display_available = True
except Exception as e:
print(f'[WARN] OLED not found on I2C bus {I2C_PORT}: {e}')
display_available = False
def get_ip_address():
"""Fetches the current IPv4 address of wlan0."""
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.settimeout(0)
# Connect to a public DNS to determine the active routing interface IP
s.connect(('8.8.8.8', 80))
ip = s.getsockname()[0]
s.close()
return ip
except Exception:
return 'No IP'
def check_wifi_status():
"""Returns True if wlan0 is connected to the target SSID."""
try:
result = subprocess.run(
['nmcli', '-t', '-f', 'ACTIVE,SSID', 'dev', 'wifi'],
capture_output=True, text=True, check=True
)
for line in result.stdout.strip().split('\n'):
if line.startswith('yes:') and TARGET_SSID in line:
return True
return False
except subprocess.CalledProcessError:
return False
def connect_wifi():
"""Attempts to connect using nmcli with explicit error handling."""
print(f'[INFO] Attempting connection to {TARGET_SSID}...')
try:
subprocess.run(
['nmcli', 'device', 'wifi', 'connect', TARGET_SSID, 'password', TARGET_PASS],
capture_output=True, text=True, check=True
)
print('[INFO] Connection command succeeded.')
except subprocess.CalledProcessError as e:
error_msg = e.stderr.strip()
print(f'[ERROR] nmcli failed: {error_msg}')
# Handle specific known failure modes
if 'No network with SSID' in error_msg:
print('[FATAL] SSID not found in scan. Check RF block or 5GHz DFS channels.')
elif 'No suitable device found' in error_msg:
print('[FATAL] wlan0 is missing or hard-blocked. Run rfkill unblock wifi.')
def update_display(ip, status):
"""Renders text to the SSD1306 OLED."""
if not display_available:
return
image = Image.new('1', (device.width, device.height))
draw = ImageDraw.Draw(image)
font = ImageFont.load_default()
draw.text((0, 0), f'SSID: {TARGET_SSID}', font=font, fill=255)
draw.text((0, 15), f'IP: {ip}', font=font, fill=255)
draw.text((0, 30), f'Status: {status}', font=font, fill=255)
device.display(image)
# --- Main Execution Loop ---
if __name__ == '__main__':
print('Starting WiFi Watchdog...')
while True:
is_connected = check_wifi_status()
current_ip = get_ip_address() if is_connected else 'Disconnected'
status_text = 'ONLINE' if is_connected else 'RECONNECTING'
update_display(current_ip, status_text)
if not is_connected:
connect_wifi()
time.sleep(10) # Wait for DHCP lease
else:
time.sleep(30) # Poll every 30 seconds when stable
Debugging: Exact Errors and the First 3 Checks
When deploying headless, you will inevitably hit RF or configuration walls. If your script hangs or nmcli throws an error, follow this diagnostic path.
- Power Supply Brownout: Run
vcgencmd get_throttled. If it returns0x50000or similar, your USB-C cable or power brick is dropping below 4.65V under load. The Pi 5 will aggressively disable the WiFi module to save the CPU. Swap to the official 27W PD supply. - RF Kill State: Run
rfkill list. If 'Soft blocked' or 'Hard blocked' says 'yes' for Wireless LAN, runsudo rfkill unblock wifi. This often happens if the regulatory domain was changed without a reboot. - 5GHz DFS Channel Timeout: If your router is on a DFS (Dynamic Frequency Selection) channel (e.g., 52-144), the Pi must passively listen for 60 seconds before transmitting. During this window,
nmcli device wifi listwill return empty. Move your router to a non-DFS channel like 36 or 149 for instant IoT handshakes.
Exact Error Strings and Ranked Causes
| Exact Error String | Most Likely Cause | Fix |
|---|---|---|
Error: Connection activation failed: No suitable device found for this connection |
The wlan0 interface is either hard-blocked by the kernel, or the country code is unset, causing the firmware to disable the radio to comply with international law. |
Run sudo rfkill unblock wifi and verify country code in raspi-config. |
Error: No network with SSID 'MyIoTNetwork' found. |
SSID is hidden, or the router is broadcasting exclusively on a 5GHz DFS channel that the Pi hasn't scanned long enough to detect. | Unhide the SSID for IoT devices, or force the router to use 2.4GHz (Ch 1, 6, 11) or 5GHz non-DFS (Ch 36). |
Secrets were required, but not provided. |
The password in your nmcli command or NetworkManager profile is incorrect, or the router is enforcing WPA3-Enterprise which requires a certificate. |
Verify the PSK. If using WPA3, ensure Pi OS is fully updated, as older wpa_supplicant backends struggle with SAE (WPA3) handshakes. |
Extending and Simplifying the Build
Depending on your deployment environment, you may need to scale this design up for industrial reliability or down for cost-savings.
How to Simplify (Cost & Power Reduction)
- Drop the OLED: If the node is deployed in an accessible location or you rely entirely on MQTT telemetry, remove the SSD1306 and the
luma.oleddependencies. Rely onnmclilogging tojournaldinstead. - Switch to Pi Zero 2 W: If your payload is just a BME280 sensor reading pushed via HTTP every 5 minutes, the Pi 5 is overkill. The Zero 2 W draws ~1.2W at idle compared to the Pi 5's ~2.5W. Just remember to constrain your router to 2.4GHz.
How to Extend (Resilience & Edge Routing)
- Add Ethernet Fallback: Modify the Python script to ping the gateway via
eth0ifwlan0fails 3 consecutive times. NetworkManager can be configured with route metrics to prefer Ethernet (ipv4.route-metric 10) over WiFi (ipv4.route-metric 20). - Deploy an External Antenna: The Pi 5's onboard PCB antenna is excellent for open rooms, but fails inside metal enclosures (NEMA boxes). Use a USB WiFi adapter with an RP-SMA connector (like the Panda PAU09) and configure NetworkManager to bind specifically to the
wlxMAC-based interface name.
For deeper reading on modern Linux networking on single-board computers, refer to the official Raspberry Pi Network Configuration documentation and the NetworkManager nmcli reference manual.






