Network Activity LED (GPIO 5)

The kernel doesn't drive an activity LED on a header GPIO directly, so a tiny userspace daemon polls the interface's byte counter from /proc/net/dev and blinks the LED on every change. Resistor + wiring follow the Pi LEDS section (220 Ω from GPIO 5 to LED anode, cathode to GND).

One script, parameterised by interface and LED pin via CLI args — the same binary drives an eth0 LED, a wlan0 LED, or both at once on different GPIOs.

#!/usr/bin/env python3
"""Blink an LED briefly whenever <iface> has RX/TX traffic.

Usage: net-activity-led.py <iface> [gpio] [min_bytes]
  iface     - e.g. eth0, end0, wlan0
  gpio      - BCM pin number (default 5)
  min_bytes - ignore counter deltas below this (default 0; use ~1500 on wlan0
              to filter beacons / mDNS / background chatter)
"""
import sys
from gpiozero import LED
from time import sleep

IFACE     = sys.argv[1]
LED_PIN   = int(sys.argv[2]) if len(sys.argv) > 2 else 5
MIN_BYTES = int(sys.argv[3]) if len(sys.argv) > 3 else 0
POLL      = 0.05    # seconds between counter reads
BLINK     = 0.03    # LED on-time per detected activity tick

led = LED(LED_PIN)

def iface_bytes(name):
    with open("/proc/net/dev") as f:
        for line in f:
            n, _, rest = line.partition(":")
            if n.strip() == name:
                cols = rest.split()
                return int(cols[0]) + int(cols[8])   # rx_bytes + tx_bytes
    return 0

prev = iface_bytes(IFACE)
while True:
    sleep(POLL)
    now = iface_bytes(IFACE)
    if now - prev > MIN_BYTES:
        led.on(); sleep(BLINK); led.off()
    prev = now

Install it:

sudo install -m 0755 net-activity-led.py /usr/local/bin/net-activity-led.py

Interface name on Pi 4/5 may be end0 rather than eth0 for wired — check with ip -br link. The wireless interface is wlan0 on all current Pi OS releases.

Setup A — eth0 on GPIO 5

/etc/systemd/system/net-activity-led-eth.service:

[Unit]
Description=Ethernet activity LED (eth0 → GPIO 5)
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
ExecStart=/usr/bin/python3 /usr/local/bin/net-activity-led.py eth0 5
Restart=on-failure
RestartSec=2

User=pi
Group=gpio

NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes

[Install]
WantedBy=multi-user.target

Setup B — wlan0 on GPIO 5

/etc/systemd/system/net-activity-led-wlan.service:

[Unit]
Description=WLAN activity LED (wlan0 → GPIO 5)
Requires=sys-subsystem-net-devices-wlan0.device
After=sys-subsystem-net-devices-wlan0.device network-online.target
Wants=network-online.target

[Service]
Type=simple
# 3rd arg = min bytes per tick; ~1500 filters Wi-Fi beacons / mDNS chatter.
ExecStart=/usr/bin/python3 /usr/local/bin/net-activity-led.py wlan0 5 1500
Restart=on-failure
RestartSec=2

User=pi
Group=gpio

NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes

[Install]
WantedBy=multi-user.target

Two differences from setup A:

  1. Requires=/After=sys-subsystem-net-devices-wlan0.device — udev creates this unit automatically once the kernel publishes wlan0, so the script doesn't start before the interface exists.
  2. The extra 1500 argument enables the noise filter so background beacons / ARP / mDNS don't keep the LED on solid.

Enable

Pick whichever setup you want (or run both on different GPIOs by giving each its own pin in ExecStart):

sudo systemctl daemon-reload
sudo systemctl enable --now net-activity-led-eth.service     # or -wlan
systemctl status net-activity-led-eth.service
journalctl -u net-activity-led-eth.service -f

The same polling pattern works for other activity sources — swap iface_bytes() for a reader of /proc/diskstats (disk), /proc/stat (CPU) or /sys/class/thermal/thermal_zone0/temp (thermal threshold) and blink on change.