To set a static IP on Raspberry Pi OS Bookworm or newer, you must use NetworkManager via the nmcli command line tool, as the legacy dhcpcd daemon is deprecated. For a headless Pi 5 on a wired connection, the exact command to assign 192.168.1.50 is: nmcli con mod "Wired connection 1" 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 followed by nmcli con up "Wired connection 1". This guide covers the exact hardware integration, verification code, and debugging steps for a permanent embedded deployment.

The Decision Path: DHCP Reservation vs. True Static IP

Before locking in your network configuration, you must decide where the IP assignment lives. In embedded IoT deployments, making the wrong choice leads to IP conflicts or unreachable nodes after a power outage. Use this decision tree to terminate on the correct method for your build.

Deployment Scenario Network Environment Recommended Method
Roaming test bench / Laptop Multiple unknown routers DHCP (Auto)
Home Lab / Media Server Single router, full admin access Router-side DHCP Reservation (MAC binding)
Industrial IoT / Headless Sensor Node Managed switch, strict VLANs, no router access Local Static IP via NetworkManager (Pick This)

Default Recommendation: For any Raspberry Pi deployed inside an enclosure with a PoE HAT acting as a fixed sensor or MQTT gateway, configure a Local Static IP on the Pi itself. Do not rely on router reservations; if the router is replaced or reset, your headless Pi becomes unreachable without a monitor and keyboard.

Hardware Spec Sheet & Pin Mapping

This guide assumes a wired, industrial-style deployment. We are using Power over Ethernet (PoE) to eliminate the need for a separate 5V USB-C power supply, which is a common point of failure in field deployments due to connector vibration and voltage drop over long USB cables.

Parts List

  • Board: Raspberry Pi 5 (8GB variant) - Target for all code and OS commands below.
  • OS: Raspberry Pi OS Bookworm (64-bit, Lite)
  • HAT: Official Raspberry Pi PoE+ HAT (Supports 802.3at, delivers up to 20W to the board)
  • Networking: Cat6 solid-core Ethernet cable (run under 90 meters)
  • Indicators: 2x 3mm LEDs with 330Ω current-limiting resistors for status feedback

Pin Mapping Table

The PoE+ HAT handles power delivery via the 40-pin header's 5V/GND rails, but it also requires I2C for thermal management. Furthermore, we are mapping two GPIO pins for physical network status LEDs, which our Python script will drive.

Component Pi 5 GPIO / Pin Function Notes
PoE+ HAT I2C SDA GPIO 2 (Pin 3) Fan controller comms Do not use for other I2C sensors without a multiplexer
PoE+ HAT I2C SCL GPIO 3 (Pin 5) Fan controller comms Pull-ups are provided on the HAT
PoE+ HAT 5V Power Pins 2, 4 Main 5V rail injection Ensure HAT standoffs are tight to prevent arcing
Status LED 1 (Green) GPIO 17 (Pin 11) Static IP Verified Connect anode to GPIO 17, cathode to GND via 330Ω
Status LED 2 (Red) GPIO 27 (Pin 13) Network / IP Error Connect anode to GPIO 27, cathode to GND via 330Ω
Safety Callout: PoE injectors and switches output 48V-57V DC. While this is considered low voltage, a short circuit on the Ethernet line can melt Cat6 jackets and cause fires. Always use a PoE switch with active negotiation (802.3af/at) rather than passive 24V/48V injectors, which will fry the Pi's Ethernet PHY if plugged in directly.

Step-by-Step: Configuring NetworkManager for a Static IP

With the hardware assembled and the Pi booted into Bookworm, SSH into the device (it will have a DHCP address initially). We will use nmcli (NetworkManager Command Line Interface) to overwrite the default wired profile. For deeper reading on NetworkManager's architecture, refer to the official NetworkManager nmcli documentation.

  1. Identify the active connection name:
    nmcli con show --active
    Look for the wired Ethernet connection. On a fresh Pi OS install, it is usually named Wired connection 1 or eth0. We will use Wired connection 1 for this guide.
  2. Assign the static IP and Subnet Mask:
    nmcli con mod "Wired connection 1" ipv4.addresses 192.168.1.50/24
    The /24 is critical. It defines the subnet mask (255.255.255.0). Omitting it will cause routing failures.
  3. Set the Default Gateway:
    nmcli con mod "Wired connection 1" ipv4.gateway 192.168.1.1
    This must match your router or Layer 3 switch's VLAN interface IP.
  4. Define DNS Servers:
    nmcli con mod "Wired connection 1" ipv4.dns "1.1.1.1 8.8.8.8"
    Space-separated list. Required if your Pi needs to resolve external hostnames for MQTT brokers or NTP sync.
  5. Switch the IPv4 method from auto to manual:
    nmcli con mod "Wired connection 1" ipv4.method manual
    This tells NetworkManager to stop sending DHCPDISCOVER broadcasts on boot.
  6. Apply and restart the connection:
    nmcli con up "Wired connection 1"
    Warning: If you are connected via SSH over Ethernet, your session will drop immediately upon executing this command. Reconnect using the new static IP (192.168.1.50).

Verification Code: Python Network & PoE HAT Monitor

In headless embedded systems, you cannot rely on a monitor to verify network status. The following Python script targets the Raspberry Pi 5 (8GB). It verifies that the assigned IP matches our target static IP, checks the PoE HAT's I2C bus presence, and drives the physical GPIO LEDs mapped in our pin table.

Prerequisites: Install the required libraries via sudo apt install python3-gpiozero python3-smbus2.

import socket
import time
import sys
from gpiozero import LED
from smbus2 import SMBus

# --- Pin & Hardware Definitions ---
TARGET_STATIC_IP = '192.168.1.50'
I2C_BUS_ID = 1  # Pi 5 uses I2C bus 1 on GPIO 2/3
POE_HAT_I2C_ADDR = 0x45  # Default address for PoE+ HAT fan controller

# GPIO Pin Mapping for Status LEDs
led_ip_ok = LED(17)   # Green LED on GPIO 17
led_ip_err = LED(27)  # Red LED on GPIO 27

def get_local_ip():
    """Fetches the primary IPv4 address of the eth0 interface."""
    try:
        # Create a UDP socket to a public DNS to force OS to select the primary route
        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        s.settimeout(2.0)
        s.connect(('8.8.8.8', 80))
        ip = s.getsockname()[0]
        s.close()
        return ip
    except Exception as e:
        print(f'Network Error: {e}')
        return None

def check_poe_hat_i2c():
    """Verifies the PoE HAT is seated and responding on the I2C bus."""
    try:
        with SMBus(I2C_BUS_ID) as bus:
            # Attempt to read a single byte from the HAT controller
            bus.read_byte(POE_HAT_I2C_ADDR)
            return True
    except OSError as e:
        print(f'I2C Hardware Fault: PoE HAT not detected on bus {I2C_BUS_ID}. Error: {e}')
        return False

def main():
    print('Starting Embedded Network Monitor...')
    
    # 1. Verify Hardware Seating
    if not check_poe_hat_i2c():
        print('WARNING: PoE HAT I2C communication failed. Check physical seating.')
    else:
        print('PoE HAT I2C handshake successful.')

    # 2. Verify Network Configuration
    current_ip = get_local_ip()
    
    if current_ip is None:
        print('CRITICAL: No network interface active.')
        led_ip_err.blink(on_time=0.2, off_time=0.2) # Fast blink = No Link
        sys.exit(1)
        
    if current_ip == TARGET_STATIC_IP:
        print(f'SUCCESS: Static IP verified ({current_ip}).')
        led_ip_ok.on()
        led_ip_err.off()
    else:
        print(f'ERROR: IP Mismatch. Expected {TARGET_STATIC_IP}, got {current_ip}.')
        print('Fallback to DHCP detected. Check nmcli configuration.')
        led_ip_err.on()
        led_ip_ok.off()

if __name__ == '__main__':
    try:
        main()
    except KeyboardInterrupt:
        print('\nMonitor stopped by user.')
        led_ip_ok.off()
        led_ip_err.off()
        sys.exit(0)

Debugging: "Connection Failed" and IP Assignment Errors

When configuring static IPs via the command line, typos in subnet math or interface names will lock you out of the device. If you lose SSH access, plug in a HDMI monitor and USB keyboard to debug locally. For more on Raspberry Pi network troubleshooting, consult the Raspberry Pi OS Configuration Documentation.

Exact Error Strings and Ranked Causes

Error 1: Error: Connection activation failed: No suitable device found for this connection

  1. Cause A (Most Likely): The connection name is wrong. You typed eth0 but NetworkManager named it Wired connection 1. Run nmcli con show to get the exact string.
  2. Cause B: The Ethernet cable is unplugged or the PoE switch port is administratively down. NetworkManager will not activate a wired profile if the physical link layer (carrier) is down.

Error 2: RTNETLINK answers: File exists

  1. Cause A (Most Likely): You are trying to add a static IP route or address that is already assigned to another active interface (e.g., you left a static IP on wlan0 that overlaps with your eth0 subnet).
  2. Cause B: Duplicate IP conflict on the LAN. Another device is already holding 192.168.1.50, and the kernel is rejecting the route addition.

The First Three Things to Check When It Fails

  1. Check Interface State: Run ip link show. Ensure your Ethernet interface (usually eth0 or end0 on Pi 5) shows state UP. If it says DOWN, you have a physical layer issue (cable, switch port, or PoE failure).
  2. Check Subnet Math: Run ip route. Ensure your gateway (e.g., 192.168.1.1) falls within the subnet range defined by your CIDR mask (e.g., /24). If your IP is 192.168.1.50/24, but your gateway is 192.168.2.1, routing will fail silently.
  3. Check for Rogue DHCP: Run cat /etc/resolv.conf. If you see a local router IP instead of the DNS servers you specified, a DHCP client daemon (like a lingering dhcpcd service) is overriding NetworkManager. Disable it via sudo systemctl disable dhcpcd.

Extending and Simplifying the Build

How to Extend: Add 802.1Q VLAN Tagging

In industrial environments, IoT sensors are rarely placed on the primary corporate LAN. You can extend this NetworkManager configuration to tag traffic for a specific VLAN (e.g., VLAN 40 for IoT devices) without touching the router. Run:

nmcli con add type vlan con-name 'IoT-VLAN40' dev eth0 id 40 ip4 10.0.40.50/24 gw4 10.0.40.1 ipv4.method manual

This creates a virtual interface (vlan40) that isolates your Pi's MQTT traffic from the main network, drastically improving security.

How to Simplify: The Emergency Fallback

If you misconfigure the static IP and lock yourself out of a headless Pi 5 mounted inside a sealed NEMA enclosure, you do not need to disassemble the hardware to fix it. Simplify your recovery by relying on Router-Side MAC Binding as a backup.

If the Pi fails to boot with its local static IP (e.g., due to a corrupted NetworkManager config file), it will eventually time out and fallback to a DHCP request if you configure the connection with ipv4.may-fail yes. Your router will then assign it a temporary IP based on its MAC address, allowing you to SSH back in and fix the nmcli syntax without opening the enclosure.