If you need deterministic latency, zero Wi-Fi dropout risk, and a single-cable deployment for an industrial or outdoor IoT node, you need a hardwired connection. While most hobbyists default to Wi-Fi, building a reliable gateway using a Raspberry Pi with Ethernet port capabilities is the standard for commercial edge deployments. In this guide, we are building a hardwired, Power-over-Ethernet (PoE) MQTT network monitor. We will target the Raspberry Pi 5, leverage its dedicated PoE header, and write robust Python code to handle physical link drops gracefully.

Raspberry Pi Ethernet Options: Built-in vs. HATs vs. USB

Not all Ethernet implementations on the Pi are created equal. Before ordering parts, you need to understand the bus architecture and power delivery limits of your chosen board. The table below breaks down the real-world performance of the most common Raspberry Pi Ethernet configurations available today.

Interface Variant Max Throughput Bus Architecture PoE Support Approx Cost (Board + Adapter)
Pi 5 Built-in (BCM2712) 1000 Mbps (Gigabit) Dedicated PCIe / RGMII No (Requires HAT) $80
Pi 4 Built-in (BCM2711) 1000 Mbps (Gigabit) Dedicated RGMII No (Requires HAT) $55
Pi 5 + Official PoE+ HAT 1000 Mbps (Gigabit) Dedicated PCIe / RGMII Yes (802.3at, up to 25W) $100
Pi Zero 2 W + Waveshare ETH HAT ~40 Mbps (Effective) SPI (Bottlenecked) No $45
Any Pi + USB 3.0 to Gigabit 800 Mbps (Real-world) USB 3.0 Bus No $25 (Adapter only)
Bench Note: Avoid USB-to-Ethernet adapters for mission-critical MQTT brokers. The USB bus polling overhead introduces micro-stutters that can cause TCP window scaling issues under heavy sensor payload loads. Always prefer the native RJ45 jack on the Pi 4/5.

Project Build: Hardwired PoE MQTT Gateway

Difficulty: Intermediate | Time: 45 Minutes | Target Board: Raspberry Pi 5 (4GB)

Parts List

  • Compute: Raspberry Pi 5 (4GB or 8GB variant)
  • Power/Network: Official Raspberry Pi PoE+ HAT (802.3at compliant)
  • Cooling: 27mm PWM JST cooling fan (included with official HAT)
  • Network Injection: 802.3at PoE+ Injector (e.g., TP-Link TL-POE150S) or PoE-enabled switch
  • Cabling: Cat6 solid copper UTP patch cable
  • Hardware: M2.5 brass standoffs (usually included with Pi case/HAT)

Pin Mapping & Physical Connections

The Raspberry Pi 5 changed how PoE power is routed. Unlike the Pi 4, which routed PoE 5V through the 40-pin GPIO header (often causing thermal throttling on the 5V traces), the Pi 5 uses a dedicated 4-pin JST connector for high-current power delivery. Do not attempt to power the Pi 5 via the GPIO 5V pins if using a high-draw HAT.

Pi 5 Connection Point PoE+ HAT Connection Function
40-Pin Header (Pins 1-12) 12-Pin Receptacle (Probes) I2C Data/Clock for fan control & 3.3V logic
4-Pin PoE JST Header 4-Pin JST Cable Main 5V/5A Power Delivery from HAT to Pi
4-Pin FAN JST Header 27mm PWM Fan Cable Tachometer and PWM speed control
RJ45 Jack Cat6 Ethernet Cable Gigabit Data + 48V PoE Input

Assembly Steps

  1. De-energize: Ensure the PoE injector is unplugged from mains power. Never hot-plug PoE cables while the HAT is unseated.
  2. Mount Standoffs: Screw the M2.5 brass standoffs into the four mounting holes on the Pi 5 PCB.
  3. Connect Power JST: Plug the 4-pin PoE power cable from the HAT into the dedicated 4-pin PoE header on the Pi 5 (located near the USB-C port).
  4. Seat the HAT: Align the 12 spring probes on the HAT with the top-left pins of the 40-pin GPIO header. Press down firmly and evenly until the HAT rests on the standoffs. Secure with M2.5 screws.
  5. Connect Fan: Plug the 27mm fan into the Pi 5's dedicated FAN JST connector. Route the wire over the USB ports to avoid pinching.
  6. Verify: Plug the Cat6 cable into the Pi, then plug the PoE injector into the wall. The Pi 5 power LED should turn solid green within 3 seconds.

The Code: Network Monitor with Auto-Reconnect

This Python script targets Raspberry Pi OS (Bookworm) and uses psutil to read eth0 byte counts, publishing them to an MQTT broker. Crucially, it implements the paho-mqtt v2.0 callback API, which requires explicit version declaration to avoid runtime crashes common in older tutorials.

Prerequisites: sudo apt install python3-pip python3-psutil and pip3 install paho-mqtt --break-system-packages

import paho.mqtt.client as mqtt
import psutil
import time
import socket
import sys

# --- CONFIGURATION ---
BROKER_IP = "192.168.1.100"
BROKER_PORT = 1883
TOPIC = "gateway/pi5/network_stats"
INTERFACE = "eth0"
INTERVAL = 10  # Seconds

# --- PAHO MQTT v2.0 CALLBACKS ---
def on_connect(client, userdata, flags, reason_code, properties):
    if reason_code == 0:
        print(f"Connected to MQTT Broker at {BROKER_IP}")
    else:
        print(f"MQTT Connection failed with reason code: {reason_code}")

def on_disconnect(client, userdata, flags, reason_code, properties):
    print(f"Disconnected from broker (Code: {reason_code}). Auto-reconnect enabled.")

# --- SETUP CLIENT ---
# CRITICAL: paho-mqtt v2.0 requires explicit API version declaration
client = mqtt.Client(callback_api_version=mqtt.CallbackAPIVersion.VERSION2)
client.on_connect = on_connect
client.on_disconnect = on_disconnect
client.reconnect_delay_set(min_delay=1, max_delay=60)

def get_eth0_stats():
    try:
        net_io = psutil.net_io_counters(pernic=True)
        if INTERFACE in net_io:
            stats = net_io[INTERFACE]
            return {
                "bytes_sent": stats.bytes_sent,
                "bytes_recv": stats.bytes_recv,
                "packets_dropped": stats.dropin + stats.dropout
            }
        else:
            return {"error": f"Interface {INTERFACE} not found"}
    except Exception as e:
        return {"error": str(e)}

def main():
    print(f"Starting {INTERFACE} monitor...")
    try:
        client.connect(BROKER_IP, BROKER_PORT, keepalive=60)
        client.loop_start()
    except socket.gaierror:
        print(f"DNS/Resolution error for {BROKER_IP}. Check broker IP.")
        sys.exit(1)
    except ConnectionRefusedError:
        print(f"Connection refused on {BROKER_IP}:{BROKER_PORT}. Is Mosquitto running?")
        sys.exit(1)

    while True:
        try:
            data = get_eth0_stats()
            if client.is_connected():
                # Publish as raw string payload for simplicity; use json.dumps for production
                client.publish(TOPIC, str(data))
                print(f"Published: {data}")
            else:
                print("MQTT disconnected. Waiting for auto-reconnect...")
            
            time.sleep(INTERVAL)
            
        except KeyboardInterrupt:
            print("Shutting down monitor...")
            client.loop_stop()
            client.disconnect()
            break
        except OSError as e:
            print(f"OS Level Network Error: {e}")
            time.sleep(INTERVAL)

if __name__ == "__main__":
    main()

Debugging: "Network is unreachable" and Ethernet Drops

When working with headless Pis over Ethernet, the most common fatal exception you will encounter in your Python logs is:

OSError: [Errno 101] Network is unreachable

This means the kernel has no valid route to the destination IP in its routing table. Here are the first three things to check when this error crashes your script, ranked by probability:

1. The Physical Link is Down or DHCP Timed Out

PoE switches often take 15-30 seconds to negotiate power and bring the data link up. If your Python script runs via systemd on boot before the link is ready, it will throw Errno 101.

  • Verify: Run ip link show eth0. Look for state UP. If it says DOWN, your cable is bad, or the switch port is administratively disabled.
  • Fix: Add Wants=network-online.target and After=network-online.target to your systemd service file to delay execution until the interface has an IP.

2. NetworkManager Ignored eth0 (Bookworm Specific)

Raspberry Pi OS Bookworm ditched dhcpcd for NetworkManager. If you flashed an older config file or manually edited /etc/network/interfaces, NetworkManager will ignore eth0, leaving it without an IP.

  • Verify: Run nmcli device status. If eth0 shows as unmanaged, this is your culprit.
  • Fix: Force NetworkManager to take control: sudo nmcli device set eth0 managed yes, then sudo nmcli connection up "Wired connection 1".

3. Switch Port VLAN Mismatch or 802.1X Blocking

If you are plugging into a corporate or managed switch, the port might be assigned to a VLAN that doesn't route to your MQTT broker's subnet, or 802.1X port security is dropping packets until MAC authentication occurs.

  • Verify: Run ip route show. If you have an IP on eth0 (check via ip a) but no default gateway is listed, the DHCP server on that VLAN isn't handing out routes.
  • Fix: Move the Cat6 cable to an unmanaged switch or an access port configured for the correct data VLAN.

For deeper MQTT-specific debugging and callback error codes, refer to the official Eclipse Paho Python documentation.

Extending and Simplifying the Build

How to Extend: Add RS485 Modbus Polling

The primary advantage of the official Pi 5 PoE+ HAT is that it leaves the rest of the 40-pin GPIO header completely exposed. You can stack an RS485 CAN HAT (like the Waveshare RS485 CAN HAT) on top of the PoE HAT. This allows your Pi to act as a bridge: polling industrial Modbus RTU sensors over RS485, and publishing the telemetry to your MQTT broker over the hardwired Gigabit Ethernet port. Ensure you use the uart0 pins (GPIO 14/15) and disable the serial console in raspi-config.

How to Simplify: Drop the PoE Requirement

If you are deploying this gateway inside a standard server rack or a location with easy access to AC outlets, you can save $20 and reduce thermal output by skipping the PoE HAT entirely. Simply use the native RJ45 jack on the Pi 5 for data, and power the board using an official Raspberry Pi 27W USB-C PD Power Supply. The Python code and network interface (eth0) remain exactly the same, but you eliminate the 48V-to-5V step-down heat generation on the PCB. For more details on Pi 5 power delivery specifications, consult the Raspberry Pi 5 hardware documentation.

Final Code Caveat: Always wrap your network socket calls in try/except OSError blocks. In embedded environments, Ethernet cables get yanked, switches reboot, and PoE injectors fail. Your code must expect the network to disappear and recover without requiring a manual reboot.