Raspberry Pi PINAS LEDs

40-pin header (Pi 1 B+ / 2 / 3 / 4 / 5 / Zero). All GPIO pins are 3.3 V logic. Per-pin source/sink limit 16 mA, total across the header 50 mA. Target 10 mA per LED to stay well inside those budgets.

Pinout map

              3V3  (1) (2)  5V
   GPIO2  SDA  (3) (4)  5V
   GPIO3  SCL  (5) (6)  GND
   GPIO4       (7) (8)  GPIO14  TXD
          GND  (9)(10)  GPIO15  RXD
   GPIO17     (11)(12)  GPIO18  PWM0
   GPIO27     (13)(14)  GND
   GPIO22     (15)(16)  GPIO23
          3V3 (17)(18)  GPIO24
   GPIO10 MOSI(19)(20)  GND
   GPIO9  MISO(21)(22)  GPIO25
   GPIO11 SCLK(23)(24)  GPIO8   CE0
          GND (25)(26)  GPIO7   CE1
   ID_SD      (27)(28)  ID_SC          <- reserved (HAT EEPROM)
   GPIO5      (29)(30)  GND
   GPIO6      (31)(32)  GPIO12  PWM0
   GPIO13 PWM1(33)(34)  GND
   GPIO19 PWM1(35)(36)  GPIO16
   GPIO26     (37)(38)  GPIO20
          GND (39)(40)  GPIO21

Pin map for PiNAS for LEDs

GPIO Header pin Notes Led Use Lead Colour
2 3 i2c to X1009 Hat
3 5 i2c to X1009 Hat
4 7 Power LED Yellow
5 29 Ethernet Activity LED White
6 31 Led 01 - Disk Space Green
12 32 hardware PWM0 Led XX -
13 33 hardware PWM1 Led XX -
16 36 Led 02 - Disk Space
17 11 Led 03 - Disk Space
18 12 hardware PWM0 Led XX -
19 35 hardware PWM1 Led XX -
20 38 Led 04 - Disk Space
21 40 Led 05 - Disk Space
22 15 Led 06 - Disk Space
23 16 Led 07 - Disk Space
24 18 Led 08 - Disk Space
25 22 Led 09 - Disk Space
26 37 Led 10 - Disk Space
27 13

Wiring

  GPIOxx ──┬── [ R ] ──▶|── GND
           │           LED

Resistor on either side of the LED is fine. Anode ( +ve ) is the long leg, connect it to GPIO. The cathode ( -ve, short leg ) connects toward GND.

Resistor values (3.3 V supply)

R = (3.3 − Vf) / I. Picking the next-larger standard E12 value.

LED colour Vf (typ.) 10 mA → calc Use 5 mA → calc Use
Red 2.0 V 130 Ω 150 Ω 260 Ω 330 Ω
Yellow 2.1 V 120 Ω 150 Ω 240 Ω 270 Ω
Orange 2.0 V 130 Ω 150 Ω 260 Ω 330 Ω
Green (std) 2.1 V 120 Ω 150 Ω 240 Ω 270 Ω
Green (hi-b) 3.0 V 30 Ω 47 Ω 60 Ω 100 Ω
Blue 3.0 V 30 Ω 47 Ω 60 Ω 100 Ω
White 3.0 V 30 Ω 47 Ω 60 Ω 100 Ω

A 220 Ω resistor is a safe universal default for any colour from a 3.3 V GPIO — gives roughly 6 mA on a red, ~1.5 mA on a blue (dim but visible). Use 330 Ω if you only need an indicator.

Quick test (Python, gpiozero)

from gpiozero import LED
from time import sleep

led = LED(17)          # BCM numbering — header pin 11
while True:
    led.on();  sleep(0.5)
    led.off(); sleep(0.5)

For PWM brightness use PWMLED(12) etc. on one of the hardware-PWM-capable pins.

Reading a PC fan tach (3rd pin) on GPIO

A standard 3-pin PC fan has: GND, +12 V, TACH. (A 4-pin fan adds a PWM control input — same tach pin.) The tach output is an open-collector pulse — the fan pulls the line low through an internal transistor, and an external pull-up supplies the high level. Most PC fans emit 2 pulses per revolution, so:

RPM = (pulses_counted / seconds) * 60 / 2

Wiring

The fan must be powered from 12 V (its own supply, not the Pi). Only the tach line goes to the Pi, with a pull-up to 3.3 V so the high level is GPIO-safe:

  +12V ──────────────────── fan red (+12V)
  GND  ──┬───────────────── fan black (GND)
         │
         └───────────────── Pi GND  (common ground is mandatory)

  Pi 3V3 ── [ 10 kΩ ] ──┬── fan yellow/green (TACH)
                        └── Pi GPIOxx  (input)

10 kΩ to 3.3 V is the standard value. Do not pull up to 5 V or 12 V — the fan's open-collector output will happily sit there, but it puts an over-voltage on the Pi input and will damage it.

If the fan turns out to have an internal pull-up to 12 V (uncommon but it happens on some server fans — measure the tach pin to GND with the fan running, before connecting to the Pi), the line will swing 0 → 12 V and you need a level shifter or a simple divider (e.g. 10 kΩ from tach to GPIO, 3.9 kΩ from GPIO to GND gives ~3.3 V at the GPIO from a 12 V high).

Pin choice

Any free GPIO from the "safe for LEDs" table above works as an input — there's no special hardware capture peripheral needed at PC-fan speeds (a 3000 RPM fan is only 100 Hz on the tach line). GPIO 4, 17, 27, 22, 5, 6, 16, 26 are all fine.

Quick test (Python, gpiozero)

from gpiozero import Button   # convenient edge-counting wrapper
from time import sleep

tach = Button(4, pull_up=False)   # external pull-up to 3V3 is on the wire
pulses = 0

def _tick():
    global pulses
    pulses += 1

tach.when_pressed = _tick     # counts falling edges

while True:
    pulses = 0
    sleep(1.0)
    print(f"{pulses * 30} RPM")   # pulses/sec * 60 / 2

For better accuracy at low RPM, measure the period between two edges instead of counting over a fixed window.

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 LED section above (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.