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) |
Project Build: Hardwired PoE MQTT Gateway
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
- De-energize: Ensure the PoE injector is unplugged from mains power. Never hot-plug PoE cables while the HAT is unseated.
- Mount Standoffs: Screw the M2.5 brass standoffs into the four mounting holes on the Pi 5 PCB.
- 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).
- 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.
- Connect Fan: Plug the 27mm fan into the Pi 5's dedicated
FANJST connector. Route the wire over the USB ports to avoid pinching. - 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 forstate UP. If it saysDOWN, your cable is bad, or the switch port is administratively disabled. - Fix: Add
Wants=network-online.targetandAfter=network-online.targetto 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. Ifeth0shows asunmanaged, this is your culprit. - Fix: Force NetworkManager to take control:
sudo nmcli device set eth0 managed yes, thensudo 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 oneth0(check viaip 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.
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.






