Skip to main content

rpi-hwid — Raspberry Pi hardware identity

PyPI CI License

What is this Raspberry Pi wearing, what powers it, and what is soldered to it? rpi-hwid answers from the Pi itself, using evidence the firmware and kernel already expose but nothing collects: HAT ID EEPROMs (including the ones the firmware never reads), the Pi 5's own verdict on its USB-C supply, the PMIC's input and RTC-cell voltages, the fan header, the USB tree, and which network interfaces are soldered down.

Around that probe:

  • a separate FPGA probe for a NeTV2, Acorn or Arty board attached to the Pi,
  • a separate Tiny Tapeout probe for a demo board on the Pi's USB: which shuttle's chip is on it (read from the chip's own ROM over the board's REPL), ASIC or FPGA breakout, and which demo board revision,
  • a collector that runs both over ssh across a fleet, one JSON document per host,
  • names derived from each board's immutable identifier,
  • a label generator that turns the documents into sticker sheets carrying only what cannot change: serial numbers, MAC addresses, Device DNA.

Contents

Install

uv tool install 'rpi-hwid[labels]'     # everything, including the label generator
pip install rpi-hwid                   # probe, collector, names: no dependencies at all

Or as a Debian package, on Raspberry Pi OS or Debian bookworm, trixie or sid, from the signed apt repository at https://mith.ro/rpi-hwid/ (the page has the three-line setup for each suite):

sudo apt install python3-rpi-hwid      # provides the rpi-hwid command

The probe itself is one dependency-free file that runs on any Pi with python3 3.5 or later, so nothing needs installing on the Pi. Send it over ssh with the source on stdin:

ssh pi@host 'python3 -' < src/rpi_hwid/probe.py
ssh pi@host 'python3 - --json' < src/rpi_hwid/probe.py

On the Pi it wants i2c-tools and passwordless sudo (for i2cdetect, i2ctransfer, dtparam and vcgencmd). Without them it still reports what it can.

Quick start

On a Pi 5 powered through a PoE splitter, with a USB Ethernet adapter:

$ rpi-hwid probe
Raspberry Pi 5 Model B Rev 1.0  serial d88100008543dc30  rev c04170
  header : nothing identifiable on the header
  signal : USB-C as the firmware sees it: max_current 900 mA, no PD contract; 5 V input 4.83 V
  signal : fan header: disabled
  signal : RTC battery: none (0.00 V)
  signal : power port: throttled=0x0
  power  : external supply on USB-C advertising 900 mA by resistor: a PoE splitter or a USB-A lead
  onboard: eth    2c:cf:67:16:bd:98  macb
  onboard: wlan   2c:cf:67:16:bd:99  brcmfmac
  usb net: 0b95:1790 ASIX Elec. Corp. AX88179  00:0e:c6:82:b5:e1  ethernet

On a Pi Zero W wearing Waveshare's PoE-ETH-USB-HUB-HAT bonnet, which has no ID EEPROM and is recognised from the USB tree instead:

$ rpi-hwid probe
Raspberry Pi Zero W Rev 1.1  serial 000000005157f671  rev 9000c1
  header : Waveshare PoE-ETH-USB-HUB-HAT (1a40:0101 hub with RTL8152 on port 4)
  signal : power port: throttled=0x0
  power  : PoE through the Waveshare PoE-ETH-USB-HUB-HAT bonnet
  onboard: wlan   b8:27:eb:02:a3:24  brcmfmac
  usb net: 0bda:8152 Realtek USB 10/100 LAN  00:e0:4c:36:0b:0a  ethernet

The whole command line:

rpi-hwid probe [--json] [--fpga] [--jtag] [--flash] [--tinytapeout]
                                                      on a Pi: what is this?
rpi-hwid fpga [--json] [--jtag] [--flash]             on a Pi: which FPGA board?
rpi-hwid tinytapeout [--json] [--no-repl]             on a Pi: which Tiny Tapeout board?
rpi-hwid collect --out DIR [-J JUMP] [--fpga] [--tinytapeout] HOST…
                                                      over ssh: one JSON per host
rpi-hwid labels --data DIR --out labels.pdf           print-ready labels from that data
rpi-hwid name --netv2 DNA… | --arty SERIAL…           the derived board names
rpi-hwid revision CODE…                               decode Pi revision codes
$ rpi-hwid revision c04170 9000c1 a020d3
c04170: Raspberry Pi 5, 4 GB, Rev 1.0, BCM2712
9000c1: Raspberry Pi Zero W, 512 MB, Rev 1.1, BCM2835
a020d3: Raspberry Pi 3 Model B+, 1 GB, Rev 1.3, BCM2837

The probe: what each signal proves

signal tells
HAT ID EEPROM at 0x50 what the firmware read: /proc/device-tree/hat (official PoE HATs, Digilent Pmod HAT Adaptor, Google VoiceBonnet…)
HAT ID EEPROM at 0x510x57, read off the ID bus boards the firmware never reads: Waveshare's PoE M.2 HAT+ (B) puts a well-formed HAT+ EEPROM at 0x52 (product string, pid 0x6d87, a DT atom naming pciex1)
devices on I2C bus 1 Waveshare PoE HAT (B): SSD1306 at 0x3c and PCF8574 at 0x20
USB tree Waveshare PoE-ETH-USB-HUB-HAT on a Zero: a Terminus 1a40:0101 hub on the root port with an RTL8152 on its port 4. That RTL8152 is reported as the Zero's wired port, not as a removable adapter
Pi 5 max_current the firmware's USB-C verdict: 5000 after a PD contract, 3000 both for a 3 A resistor source and for no USB-C source at all (a HAT on the GPIO 5 V pins), 1500 or 900 for a resistor source advertising that much, so 900/1500 proves an external USB-C supply
Pi 5 PMIC ADC 5 V input (GPIO-fed HATs 5.1–5.4 V, splitters 4.8–5.0 V) and the RTC cell (about 3 V fitted, under 0.01 V not)
Pi 5 cooling_fan node a fan on the Pi's own header
interface drivers soldered-down (SoC Ethernet, SDIO radio, the 3B+'s LAN7800) versus removable USB adapters, which are listed with their descriptors
throttle flags under-voltage now or since boot: all a 3B+, Zero or Pi 4 can say about its supply

The verdict names the power source where the evidence allows, and says so where it does not:

power_class meaning
gpio-poe-hat a PoE HAT feeding the GPIO 5 V pins, identified by its EEPROM or I2C devices
bonnet-poe the Waveshare PoE-ETH-USB-HUB-HAT bonnet
usbc-supply an external supply on USB-C advertising 900 or 1500 mA: a PoE splitter or a USB-A lead
usbc-pd-supply a USB-C supply with a PD contract
ambiguous two sources read identically: an EEPROM-less GPIO PoE HAT on a Pi 5 and a 3 A USB-C splitter, or an EEPROM-less, I2C-less HAT on a 3B+ and any splitter
undetermined nothing on the Pi distinguishes the source

The switch-side 802.3af class narrows the ambiguous cases (the bonnet is class 3, the M.2 HAT+ (B) class 4, an af-only HAT is never class 4), but that is read from the switch, not the Pi, so it is outside this package.

FPGA boards

rpi_hwid.fpga is kept apart from the Pi probe because few people have an FPGA board on their Pi. rpi-hwid probe --fpga appends it; rpi-hwid fpga runs it alone.

From what the Pi sees without touching the FPGA: a NeTV2 running LitePCIe is PCIe 10ee:7024 with one 1 MiB BAR; an SQRL Acorn CLE-215+ is 1e24:021f (or 10ee:7011 under other gateware) with 128 KiB + 64 KiB BARs; a Digilent Arty is its own FT2232 with a 210319… serial. BAR sizes come from sysfs and a BAR is never mapped, because that wedges a host.

With --jtag, openFPGALoader reads the IDCODE and Device DNA over the Arty's FT2232 or the host's GPIO harness (libgpiod, pins 27:22:4:17). A GPIO chain that answers on a host with no Arty is taken to be a NeTV2. With --flash, an Arty's SPI flash is identified by JEDEC id, which reloads the FPGA with openFPGALoader's bridge bitstream. The S25FL128S and S25FL127S both answer 0x012018, so the label says S25FL128S/127S.

$ rpi-hwid fpga --jtag          # a Pi 4 with an Arty A7-35T on USB
  fpga   : arty (Digilent FT2232 210319B301DE; FT2232 JTAG idcode 0x362d093 artix a7 35t), DNA 0x00628502251ea85c
$ rpi-hwid fpga                 # a Pi 5 with an Acorn on its PCIe connector
  fpga   : acorn (PCIe 1e24:021f, 128 KiB + 64 KiB BARs (SQRL Acorn CLE-215+))

Tiny Tapeout boards

rpi_hwid.tinytapeout is the same kind of stand-alone module for a Tiny Tapeout demo board plugged into the Pi's USB. rpi-hwid probe --tinytapeout appends it; rpi-hwid tinytapeout runs it alone.

From the USB tree alone the board is only a candidate: the demo board's RP2040 (TT04 to TT08 boards) or RP2350 (the DBv3 "ETR" boards) runs the Tiny Tapeout MicroPython SDK, which is stock MicroPython as far as USB is concerned, 2e8a:0005 "MicroPython" "Board in FS mode" with the RP2's flash unique id as its serial. What makes it a Tiny Tapeout board is the SDK, so the module then drives the board's raw REPL over /dev/ttyACM* (opened with os.open and termios, no pyserial needed) and asks the SDK what it already holds: the copy of the chip ROM that the boot cached (shuttle=, repo=, commit=, present on every chip since TT05; FPGA on the FPGA breakout), the demo board it detected (TT04/TT05, TT06+, TTDBv3 [3.2]) and its own version. Reading the ROM afresh would drive the chip's pins, so the probe never does: when the boot did not read it (a custom main.py, say) the ROM is reported as not cached, with the reason. Asking interrupts whatever the board is running (at boot, nothing), never resets it and touches no pin; every read and write has a deadline, and a board that cannot be reached stays a candidate. --no-repl stops at the USB tree.

$ rpi-hwid tinytapeout             # a Pi 4 with a TT06 dev kit on USB
  tt     : TT06 on demo board TT06+ (Tiny Tapeout SDK 2.0.4 on Raspberry Pi Pico with RP2040 (USB 1-1.2); chip ROM shuttle=tt06; demo board TT06+)

The module also carries a table of what the board cannot say: the chip carrier and demo board colours per shuttle, the demo board revision that shipped with each kit, and the chip's page on tinytapeout.com, for the label.

Collecting a fleet

rpi-hwid collect pushes the probe source to each host over ssh (nothing is installed on the Pi), in parallel, and writes <host>.json per host. The FPGA module is appended with --fpga for every host, or with --jtag HOST and --flash HOST for the hosts that should drive JTAG; the Tiny Tapeout module with --tinytapeout. A login banner before the JSON is skipped.

$ rpi-hwid collect --out data/ -J jump.example.org --fpga --jtag pi@10.21.2.16 \
      rpi5-netv2 rpiz-serial pi@10.21.2.16 pi@10.21.2.47
  rpi5-netv2: Raspberry Pi 5 Model B Rev 1.0; header bare; power usbc-supply
  rpiz-serial: Raspberry Pi Zero W Rev 1.1; header ['Waveshare PoE-ETH-USB-HUB-HAT']; power bonnet-poe
  pi@10.21.2.16: Raspberry Pi 4 Model B Rev 1.5; header ['Pmod HAT Adaptor']; power undetermined; fpga 0x00628502251ea85c
  pi@10.21.2.47: Raspberry Pi 5 Model B Rev 1.1; header ['Waveshare PoE M.2 HAT+ (B)']; power gpio-poe-hat; fpga acorn
4 of 4 host(s) written to data

--users lists the login names to try in order (default: you, then pi); the one that worked is recorded in the document. A host that cannot be reached is reported and skipped, and the exit status says so.

The document

rpi-hwid probe --json, and every file the collector writes, is one JSON object: the raw evidence as it came off the Pi, plus a verdict block with a fixed-shape summary that everything else in the package consumes.

{
  "model": "Raspberry Pi Zero W Rev 1.1",
  "serial": "000000005157f671",
  "revision": "9000c1",
  "hat_fw": null,
  "hat_eeproms": {},
  "i2c1": [],
  "usb": {"1-1": "1a40:0101", "1-1.4": "0bda:8152"},
  "interfaces": ["…"],
  "usb_net": ["…"],
  "throttled": "0x0",
  "verdict": {
    "header": ["Waveshare PoE-ETH-USB-HUB-HAT (1a40:0101 hub with RTL8152 on port 4)"],
    "power": "PoE through the Waveshare PoE-ETH-USB-HUB-HAT bonnet",
    "evidence": ["power port: throttled=0x0"],
    "summary": {
      "model": "Raspberry Pi Zero W Rev 1.1",
      "serial": "000000005157f671",
      "revision": "9000c1",
      "header": ["Waveshare PoE-ETH-USB-HUB-HAT"],
      "hat_uuid": null,
      "power_class": "bonnet-poe",
      "macs": [{"kind": "eth", "mac": "00:e0:4c:36:0b:0a"},
               {"kind": "wlan", "mac": "b8:27:eb:02:a3:24"}],
      "usb_net": [],
      "rtc_battery": null, "fan": null, "max_current_ma": null, "ext5v_v": null,
      "fpga": []
    }
  }
}

(Lists shortened.) The summary is the contract. header is what sits on the 40-pin header, macs the soldered-down interfaces (eth first), usb_net the removable adapters with their descriptors, fpga the boards the FPGA module found, tinytapeout the demo boards the Tiny Tapeout module found (present only when that module ran), and hat_uuid the EEPROM's UUID when one was read. The Pi 5-only fields are null elsewhere. Everything outside verdict is evidence, kept so a wrong verdict can be argued with.

Names

Raw identifiers come in near-identical clusters (Device DNAs sharing most of their digits, Digilent serials differing in the last byte), so rpi-hwid name hashes them into short, distinct words:

$ rpi-hwid name --netv2 0x00742c4e63b9085c --arty 210319B301DE 210319B0C238
netv2-grove  00742c4e63b9085c
arty-hawk  210319B301DE
arty-serin  210319B0C238

A NeTV2's name is a pure function of its DNA. Arty names are a hash chain resolved against a registry, so two serials never share a word and adding a board never renames an old one. Keep the registry as a JSON object of serial to name and pass it with --names registry.json to both name and labels.

Labels

rpi-hwid labels lays out 63.5 × 38.1 mm labels, 21 to an A4 sheet (the Avery L7160 grid), from a directory of collected documents. Print at 100 %. --outline draws the die-cut edges for an alignment print on plain paper; --start N skips N positions on the first sheet so a partly used sheet can be finished; --only rpi|fpga|tt|usb limits the kinds; --list prints what would be generated and where.

$ rpi-hwid labels --data data/ --list
sheet 1 row 1 col 1  arty   arty-hawk
sheet 1 row 1 col 2  acorn  Acorn CLE-215+
sheet 1 row 1 col 3  tt     TT06 E6614C311B7A7A37
sheet 1 row 2 col 1  rpi    Pi 3 Model B+ 1 GB 000000004fe3e7e4
sheet 1 row 2 col 2  rpi    Pi 4 Model B 2 GB 10000000ce8e3593
sheet 1 row 2 col 3  rpi    Pi 5 1 GB c36b093f773d46b8
sheet 1 row 3 col 1  rpi    Pi 5 4 GB d88100008543dc30
sheet 1 row 3 col 2  rpi    Pi Zero W 512 MB 000000005157f671
sheet 1 row 3 col 3  usb    ASIX AX88179A f8:e4:3b:0f:c1:e6
sheet 1 row 4 col 1  usb    ASIX Elec. Corp. AX88179 00:0e:c6:82:b5:e1
$ rpi-hwid labels --data data/ --out labels.pdf
10 labels on 1 sheet -> labels.pdf

Every label carries only what cannot change, and every identifier that might otherwise be typed is also a QR code. The four layouts, cropped from a rendered sheet:

Raspberry Pi. The MACs are what people look for, so they are the largest thing on the label, each with its own QR. Model, memory and revision are decoded from the revision code, and the HAT band names what the probe found on the header (and the EEPROM UUID when there is one). The serial is a cross-check rather than the identity anyone uses, so it runs up the left edge with a small QR of its own at the top. The layout is always the same, so a stack of them reads at a glance.

Pi 5, bare header Pi 5 wearing a Waveshare PoE M.2 HAT+ (B); its radio is disabled so the wlan MAC cannot be read

Pi 4 with a Digilent Pmod HAT Adaptor Pi 3B+; the wlan MAC is derived from the eth MAC

Pi Zero W with the Waveshare PoE-ETH-USB-HUB-HAT; the bonnet's RTL8152 is its eth MAC

The Zero W's wired port comes from the bonnet, so its MAC is printed as the eth MAC. On a 3B+ or a Zero the wlan MAC follows from the eth MAC (the Broadcom-OUI rule: same serial digits, XOR 55:55:55), so it is printed even when the radio is off. On a Pi 4 or 5 it cannot be derived, so a disabled radio is stated as such.

FPGA boards. The maker and the derived name, the die, and the immutable identifier full width with a QR: Device DNA where read, the Digilent serial and flash part on an Arty, and a line to write the DNA on when it has not been read yet.

NeTV2 Arty A7-35T Acorn CLE-215+, DNA not yet read

Tiny Tapeout boards. The shuttle is the headline, with ASIC or FPGA breakout and the PDK under it; then the demo board as the SDK detected it with the revision that shipped in that kit, and the chip ROM's commit. The two colour boxes are the chip carrier's and the demo board's colours with the colour's name beside each, so the right board is picked out of a drawer (an empty box marked n/a where the colour is not recorded). The large QR opens the chip's page on tinytapeout.com; the demo board's RP2 unique id, its USB serial, runs along the bottom with its own small QR at the end of the row.

TT06 chip on a TT06+ demo board TTIHP25a chip on a DBv3 demo board; no colours recorded for that shuttle yet

USB network adapters. The descriptors (USB version and speed, driver, VID:PID) beside the MAC's QR, and the MAC itself full width along the foot, so a dongle can be matched to a DHCP lease from across the room.

ASIX AX88179 USB 3.0 gigabit adapter

The package ships the Raspberry Pi raspberry, the Alphamax, Digilent and Tiny Tapeout marks (each its owner's trademark, drawn only on that maker's own hardware to identify it) and the public-domain USB trident; see src/rpi_hwid/artwork/README.md for the sources. A --artwork DIR overrides any of them and may add a netv2.svg. A board whose maker has no mark (SQRL) gets the name in type. The images are regenerated from the test fixtures by docs/examples/render.py.

Using it from Python

Everything on the collecting side is a frozen dataclass (rpi_hwid.model); the probes emit JSON because they run on a Pi's Python 3.5.

from pathlib import Path

from rpi_hwid.collect import collect, load_collected
from rpi_hwid.names import netv2_name
from rpi_hwid.revision import decode_revision

collect(["rpi5-netv2", "pi@10.21.2.47"], Path("data"), jump="jump.example.org", fpga=True)

for host, doc in load_collected(Path("data")).items():
    s = doc.summary
    rev = decode_revision(s.revision)
    print(host, rev.model, rev.memory, s.power_class, [m.mac for m in s.macs])
    for board in s.fpga:
        name = netv2_name(board.dna) if board.kind == "netv2" and board.dna else ""
        print("  ", board.kind, board.identity, name)
    for tt in s.tinytapeout:
        print("  ", tt.shuttle, tt.chip, tt.demoboard, tt.usb_serial)

Summary.from_dict refuses a field it does not know, so a probe that has grown a field is noticed when its document is loaded, not silently dropped. ProbeDocument.evidence keeps the whole raw document for anything the summary leaves out.

Development

uv sync --all-extras --group dev
uv run ruff check && uv run mypy && uv run pytest

The test suite renders a sheet from the fixture documents and decodes every QR on it with zxing (it needs pdftoppm from poppler and a monospace font). The three probe files must stay Python 3.5-clean: the tests refuse f-strings in them, and CI byte-compiles them with a real 3.5.

Releases are automatic: every green push to main publishes to PyPI and rebuilds the apt repository. See RELEASING.md. To build the Debian package locally:

docker run --rm -v "$PWD:/src" -w /src debian:bookworm sh packaging/ci-build.sh

The repository's social preview, docs/social-preview.png, is regenerated by uv run docs/social_preview.py from the same fixture documents as the README images, so it tracks the generator. The PNG is byte-stable only for a given set of fonts and poppler version, so a regeneration on another machine may differ by a few pixels. GitHub has no API for it: after regenerating, upload the file by hand in Settings → Social preview.

Origin

Worked out on a fleet of Pi Zero W, 3B+, 4 and 5 hosts carrying NeTV2, Acorn and Arty boards, powered by a mix of Waveshare PoE HATs and external PoE splitters, in September 2026. The rules above are what those boards showed; a board that behaves differently is a bug report. The Tiny Tapeout module was written from the SDK's public sources (tt-micropython-firmware, tt-demo-pcb, tt-support-tools) and tested against an emulated board; a report from a real demo board is welcome.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

rpi_hwid-0.0.post40.tar.gz (781.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

rpi_hwid-0.0.post40-py3-none-any.whl (90.7 kB view details)

Uploaded Python 3

File details

Details for the file rpi_hwid-0.0.post40.tar.gz.

File metadata

  • Download URL: rpi_hwid-0.0.post40.tar.gz
  • Upload date:
  • Size: 781.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for rpi_hwid-0.0.post40.tar.gz
Algorithm Hash digest
SHA256 e9566ca52d819b556b0822607ee87240914933f9d23f3cd7c07e329c795866c3
MD5 15bb3b2641ef4f74eb6da29bcb31bf80
BLAKE2b-256 aa55cfbdd61a889800182d0c65eccc96bdd28d714bae013e121104309f16b7f9

See more details on using hashes here.

Provenance

The following attestation bundles were made for rpi_hwid-0.0.post40.tar.gz:

Publisher: publish-pypi.yml on mithro/rpi-hwid

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file rpi_hwid-0.0.post40-py3-none-any.whl.

File metadata

  • Download URL: rpi_hwid-0.0.post40-py3-none-any.whl
  • Upload date:
  • Size: 90.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for rpi_hwid-0.0.post40-py3-none-any.whl
Algorithm Hash digest
SHA256 23e2b0ef208c13295b84f3ebb150dae889c5be916dbd1058e5fef8dc51151711
MD5 0cdc3cbeab3bc13d59762fb38df4b4ba
BLAKE2b-256 831ebc24271f6e30988014affd37aa6eb3d4263f710756d94f881cedc15c214e

See more details on using hashes here.

Provenance

The following attestation bundles were made for rpi_hwid-0.0.post40-py3-none-any.whl:

Publisher: publish-pypi.yml on mithro/rpi-hwid

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.0.post66

2 files

0.0.post64

2 files

0.0.post62

2 files

0.0.post60

2 files

0.0.post55

2 files

0.0.post53

2 files

0.0.post51

2 files

0.0.post49

2 files

0.0.post47

2 files

This release

0.0.post40 This release

2 files

0.0.post30

2 files

0.0.post23

2 files

0.0.post19

2 files

0.0.post18

2 files

0.0.post17

2 files

0.0.post16

2 files

0.0.post13

2 files

0.0.post11

2 files

0.0.post9

2 files

0.0.post5

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page