The 2026 Reality: NetworkManager Replaced wpa_supplicant

If you are copying a wpa_supplicant.conf file into the /boot/firmware directory of a fresh Raspberry Pi OS image, your Raspberry Pi WiFi config will fail silently. As of the Bookworm release and continuing into current 2026 builds (including Trixie edge), Raspberry Pi OS has completely deprecated wpa_supplicant and dhcpcd in favor of NetworkManager and systemd-networkd.

This shift breaks years of legacy headless provisioning tutorials. NetworkManager handles WiFi security, roaming, and IP assignment differently, requiring interaction via the nmcli (command line) or nmtui (text UI) tools. For embedded deployments, automated Python scripts wrapping nmcli are now the gold standard for fleet provisioning.

Safety & Hardware Warning: The Raspberry Pi 5 requires the official 27W USB-C PD power supply to maintain stable WiFi under load. Using a standard 15W phone charger will cause brownouts that drop the wlan0 interface randomly during high-throughput MQTT or camera streaming tasks.

Hardware Spec Sheet & UART Debug Pinout

When WiFi configuration fails headless, you cannot SSH in to fix it. You must fall back to the UART serial console. Below is the exact hardware list and pin mapping required to debug a Pi that refuses to join a network.

Target Hardware & Debug Components
ComponentExact Variant / ModelRole in Build
MicrocontrollerRaspberry Pi 5 (8GB) or Pi 4 Model B (4GB)Target board (Code targets Bookworm/Trixie OS)
WiFi SoCInfineon CYW43455 (Pi 5) / Cypress CYW43455 (Pi 4)Internal 802.11ac dual-band radio
Power SupplyOfficial Raspberry Pi 27W USB-C PDPrevents PCIe/SDIO bus brownouts
Debug AdapterFTDI FT232RL USB-to-Serial (3.3V logic)Headless UART console access

UART0 Pin Mapping for Serial Debug

Before applying power, wire the FTDI adapter to the Pi's GPIO header. Ensure your FTDI adapter is physically jumpered to 3.3V. Sending 5V logic into GPIO 15 will permanently destroy the Pi's UART controller.

UART0 Debug Wiring Map
Pi GPIO PinFunctionFTDI FT232RL PinWire Color (Standard)
Pin 8 (GPIO 14)TXD (Transmit)RXD (Receive)Yellow
Pin 10 (GPIO 15)RXD (Receive)TXD (Transmit)Orange
Pin 6GNDGNDBlack

Note: You must add enable_uart=1 to config.txt on the boot partition to activate UART0 on Pi 4/5, as the primary UART is often mapped to the Bluetooth module by default.

Configuration Decision Tree

How you configure the WiFi depends entirely on your deployment environment. Use this decision path to select the correct method. For automated embedded projects, we terminate on nmcli via Python.

Raspberry Pi WiFi Config Decision Matrix
ScenarioMethodPros / ConsVerdict
Single Pi, Monitor & Keyboard attachednmtui or Desktop GUIEasy visual setup / Not scriptableUse for prototyping only
Headless First-Boot (No network)custom.toml in boot partitionNative to Pi Imager / Complex syntaxUse for initial factory flash
Fleet Provisioning / Runtime Updatesnmcli via Python ScriptRobust, scriptable, handles errors / Requires base OS bootDEFAULT PICK: Use for all embedded deployments

Automated Python NetworkManager Provisioning

The following Python 3 script uses subprocess to interact with nmcli. It creates a persistent WiFi connection, forces the interface to connect, and verifies IP assignment. This code explicitly targets Raspberry Pi OS Bookworm/Trixie on Pi 4B and Pi 5, utilizing the wlan0 interface.

import subprocess
import sys
import time

# --- Configuration Variables ---
SSID = 'FactoryFloor_IoT'
PSK = 'SuperSecretWPA3Pass!'
CONN_NAME = 'iot-wifi-primary'
INTERFACE = 'wlan0'
TIMEOUT_SEC = 15

def run_nmcli(command: list) -> subprocess.CompletedProcess:
    """Execute nmcli command and capture output."""
    full_cmd = ['nmcli'] + command
    return subprocess.run(full_cmd, capture_output=True, text=True, check=False)

def configure_wifi():
    print(f'[*] Checking interface {INTERFACE} status...')
    
    # 1. Ensure WiFi radio is not hard-blocked
    rfkill_check = run_nmcli(['radio', 'wifi'])
    if 'disabled' in rfkill_check.stdout.lower():
        print('[!] WiFi radio is disabled. Enabling...')
        run_nmcli(['radio', 'wifi', 'on'])

    # 2. Delete existing connection if it exists to prevent duplicate UUID errors
    run_nmcli(['connection', 'delete', CONN_NAME])

    # 3. Add new connection with WPA-PSK security
    print(f'[*] Provisioning connection: {CONN_NAME}')
    add_cmd = [
        'connection', 'add', 'type', 'wifi', 'con-name', CONN_NAME,
        'ssid', SSID, 'ifname', INTERFACE,
        'wifi-sec.key-mgmt', 'wpa-psk', 'wifi-sec.psk', PSK,
        'connection.autoconnect', 'yes', 'ipv4.method', 'auto'
    ]
    
    result = run_nmcli(add_cmd)
    if result.returncode != 0:
        print(f'[ERROR] Failed to add connection: {result.stderr.strip()}')
        sys.exit(1)

    # 4. Activate connection and wait for IP
    print('[*] Activating connection...')
    up_cmd = ['connection', 'up', CONN_NAME, 'ifname', INTERFACE]
    result = run_nmcli(up_cmd)
    
    if result.returncode != 0:
        print(f'[ERROR] Activation failed: {result.stderr.strip()}')
        sys.exit(1)

    # 5. Verify IP Assignment
    start_time = time.time()
    while time.time() - start_time < TIMEOUT_SEC:
        ip_check = run_nmcli(['-g', 'IP4.ADDRESS', 'device', 'show', INTERFACE])
        if ip_check.stdout.strip() and '192.' in ip_check.stdout or '10.' in ip_check.stdout:
            print(f'[SUCCESS] Connected! IP: {ip_check.stdout.strip()}')
            return True
        time.sleep(2)
        
    print('[ERROR] Timeout waiting for DHCP IP assignment.')
    sys.exit(1)

if __name__ == '__main__':
    try:
        configure_wifi()
    except KeyboardInterrupt:
        print('\n[!] Provisioning aborted by user.')
        sys.exit(130)
    except Exception as e:
        print(f'[FATAL] Unexpected error: {e}')
        sys.exit(1)

Debugging: Exact Error Strings and Ranked Fixes

When the script above fails, nmcli returns specific error strings. Here are the exact errors, what they mean, and the ranked fixes. If your build fails, check these first three things in order:

  1. Power Supply Brownout: Check dmesg | grep -i voltage. If you see under-voltage warnings, the WiFi chip is resetting. Upgrade to the 27W PD supply.
  2. 5GHz DFS Channel Blocking: If connecting to a 5GHz network, the Pi must pass Dynamic Frequency Selection (DFS) radar checks. This delays connection by up to 60 seconds. Force your router to use a non-DFS channel (e.g., 36, 40, 44, 48) or use 2.4GHz for initial provisioning.
  3. Legacy Config Interference: Ensure no wpa_supplicant.conf exists in /boot/firmware/. NetworkManager will ignore it, but legacy startup scripts might try to parse it and hang the boot sequence.

Exact Error String Dictionary

Pro-Tip: Always run nmcli device wifi list before debugging. If the list is empty, your issue is physical (firmware missing, rfkill blocked, or antenna disconnected), not a password issue.
Ranked nmcli Error Strings and Solutions
Exact Error StringRoot CauseFix / Command
Error: Connection activation failed: (7) Secrets were required, but not provided. Incorrect PSK, or missing wifi-sec.key-mgmt flag in the add command. Verify password. Ensure wifi-sec.key-mgmt wpa-psk is explicitly passed in the nmcli connection add array.
Error: No suitable device found for this connection (reason: device-not-found). wlan0 is hard-blocked by rfkill, or the brcmfmac firmware failed to load. Run sudo rfkill unblock wifi. If that fails, check dmesg for missing /lib/firmware/brcm/brcmfmac43455-sdio.bin.
Error: Connection activation failed: (5) IP configuration could not be reserved. DHCP server unreachable, or static IP conflict on the network. Check router DHCP pool. Assign a static IP via nmcli: nmcli con mod iot-wifi-primary ipv4.addresses 192.168.1.50/24 ipv4.gateway 192.168.1.1 ipv4.method manual.

Extending the Build: Static IPs and Fleet Scaling

Once your baseline Raspberry Pi WiFi config is stable via the Python script, you will likely need to harden it for production environments. Here is how to extend and simplify the build for fleet scaling.

1. Disable MAC Address Randomization

NetworkManager randomizes the WiFi MAC address on every scan and connection by default to prevent tracking. In an embedded IoT fleet where your router uses MAC filtering or static DHCP leases, this will break your network. Disable it globally:

sudo nano /etc/NetworkManager/conf.d/100-disable-wifi-mac-randomization.conf

Add the following block:

[device]
wifi.scan-rand-mac-address=no

[connection]
wifi.cloned-mac-address=preserve

Restart NetworkManager with sudo systemctl restart NetworkManager.

2. Fallback to Ethernet on WiFi Failure

For critical infrastructure (like greenhouse controllers or CNC monitors), WiFi is inherently unreliable due to RF interference. You can configure NetworkManager to assign a higher route metric to WiFi, ensuring that if an Ethernet cable is plugged in, traffic automatically routes over copper without dropping the active SSH session.

nmcli connection modify iot-wifi-primary ipv4.route-metric 600
nmcli connection modify 'Wired connection 1' ipv4.route-metric 100

By standardizing on nmcli and understanding the underlying shift away from wpa_supplicant, you eliminate the most common point of failure in modern Raspberry Pi embedded deployments. For deeper architectural references on connection profiles, consult the official NetworkManager nmcli documentation and the Raspberry Pi OS configuration guides.