This chapter walks through five complete, runnable examples. Each example exercises a real hardware interaction scenario that Python developers commonly encounter. Full code is in the code/ directory.
Goal: List all PCI devices with vendor name, device name, and current driver.
The PCI ID database maps vendor:device pairs to human-readable names. The pciutils package includes it at /usr/share/misc/pci.ids or /usr/share/hwdata/pci.ids.
# See code/pci_enumerate.py for the full version
from pathlib import Path
def parse_pci_ids(ids_file="/usr/share/misc/pci.ids"):
"""Build a lookup dict from PCI IDs file."""
vendors = {}
current_vendor = None
try:
with open(ids_file, errors="replace") as f:
for line in f:
if line.startswith("#") or line.strip() == "":
continue
if not line.startswith("\t"):
parts = line.split(None, 1)
if len(parts) == 2:
current_vendor = parts[0]
vendors[current_vendor] = {"name": parts[1].strip(), "devices": {}}
elif line.startswith("\t") and not line.startswith("\t\t"):
parts = line.strip().split(None, 1)
if len(parts) == 2 and current_vendor:
vendors[current_vendor]["devices"][parts[0]] = parts[1].strip()
except FileNotFoundError:
pass
return vendors
def list_pci_devices():
pci_ids = parse_pci_ids()
base = Path("/sys/bus/pci/devices")
for dev in sorted(base.iterdir()):
vendor_id = (dev / "vendor").read_text().strip()[2:] # strip 0x
device_id = (dev / "device").read_text().strip()[2:]
vendor_info = pci_ids.get(vendor_id, {})
vendor_name = vendor_info.get("name", vendor_id)
device_name = vendor_info.get("devices", {}).get(device_id, device_id)
driver_link = dev / "driver"
driver = driver_link.resolve().name if driver_link.exists() else "no driver"
print(f"{dev.name} [{vendor_name}] {device_name} driver={driver}")
list_pci_devices()
Sample output:
0000:00:02.0 [Intel Corporation] UHD Graphics 620 driver=i915
0000:00:1f.3 [Intel Corporation] Cannon Point-LP HD Audio driver=snd_hda_intel
0000:01:00.0 [NVIDIA Corporation] GP107M [GTX 1050 Ti] driver=nouveau
Goal: Read CPU temperature, fan speeds, battery, and memory from sysfs/proc.
# See code/hw_monitor.py for the full version
from pathlib import Path
def read(path, default="N/A"):
try:
return Path(path).read_text().strip()
except (FileNotFoundError, PermissionError):
return default
def cpu_temperatures():
base = Path("/sys/class/hwmon")
for hwmon in sorted(base.iterdir()):
name = read(hwmon / "name")
if name in ("coretemp", "k10temp", "zenpower"):
for temp_file in sorted(hwmon.glob("temp*_input")):
label_file = temp_file.with_name(temp_file.name.replace("input", "label"))
label = read(label_file, temp_file.stem)
temp_mc = int(read(temp_file, "0"))
print(f" {label}: {temp_mc/1000:.1f}°C")
def battery():
bat = Path("/sys/class/power_supply/BAT0")
if not bat.exists():
print(" No battery")
return
capacity = read(bat / "capacity")
status = read(bat / "status")
energy_now = int(read(bat / "energy_now", "0"))
energy_full = int(read(bat / "energy_full", "1"))
print(f" {capacity}% ({status}), {energy_now/1e6:.1f}/{energy_full/1e6:.1f} Wh")
print("=== CPU Temperatures ===")
cpu_temperatures()
print("\n=== Battery ===")
battery()
Goal: Read and write GPIO pins using the modern gpiod character device interface (kernel 4.8+).
The modern GPIO interface uses /dev/gpiochipN with ioctl calls, or the gpiod library.
# Install the Python binding
pip install gpiod
import gpiod
# Open chip (usually gpiochip0 on Raspberry Pi)
chip = gpiod.Chip("gpiochip0")
# Configure pin 17 as output
line = chip.get_line(17)
config = gpiod.LineRequest()
config.consumer = "my-app"
config.request_type = gpiod.LineRequest.DIRECTION_OUTPUT
line.request(config)
# Toggle the pin
line.set_value(1) # HIGH
import time
time.sleep(0.5)
line.set_value(0) # LOW
line.release()
chip.close()
# Read a button on pin 27
line_in = chip.get_line(27)
config = gpiod.LineRequest()
config.request_type = gpiod.LineRequest.DIRECTION_INPUT
line_in.request(config)
state = line_in.get_value()
print(f"Button: {'pressed' if state == 0 else 'released'}")
The sysfs GPIO interface (/sys/class/gpio/) is the legacy method — still works but deprecated. The character device is the correct modern approach.
Goal: Find a connected USB serial adapter by VID/PID and communicate with it.
# See code/usb_serial.py for the full version
from pathlib import Path
import glob, os, termios, struct
def find_serial_by_vidpid(vid, pid):
"""Return /dev/ttyUSBx for a USB device with given vendor:product IDs."""
vid = vid.lower().lstrip("0x")
pid = pid.lower().lstrip("0x")
for tty_path in Path("/sys/class/tty").iterdir():
device = tty_path / "device"
if not device.exists():
continue
# Walk up to find USB parent
parent = device.resolve()
while parent != parent.parent:
id_vendor = parent / "idVendor"
id_product = parent / "idProduct"
if id_vendor.exists():
if id_vendor.read_text().strip() == vid and \
id_product.read_text().strip() == pid:
dev_name = (tty_path / "dev").read_text().strip()
return f"/dev/{tty_path.name}"
parent = parent.parent
return None
port = find_serial_by_vidpid("10c4", "ea60")
print(f"CP2102 found at: {port}")
For actual serial communication, pyserial is the right tool:
import serial
with serial.Serial(port, baudrate=9600, timeout=1) as ser:
ser.write(b"AT\r\n")
response = ser.readline()
print(f"Response: {response}")
Goal: Read raw input events from a keyboard or mouse using /dev/input/eventN.
Linux input devices expose raw events through /dev/input/event*. Each event is a 24-byte struct: (timeval_sec, timeval_usec, type, code, value).
# See code/input_monitor.py for the full version
import struct, os, select
from pathlib import Path
EVENT_SIZE = struct.calcsize("llHHi")
EVENT_FMT = "llHHi"
EV_TYPES = {0: "SYN", 1: "KEY", 2: "REL", 3: "ABS", 4: "MSC"}
def find_keyboard():
"""Find the first device that generates KEY events."""
for event_file in sorted(Path("/sys/class/input").iterdir()):
name_file = event_file / "device" / "name"
if name_file.exists():
name = name_file.read_text().strip()
if "keyboard" in name.lower():
return f"/dev/input/{event_file.name}"
return "/dev/input/event0" # fallback
def monitor_input(device_path, duration=5):
fd = os.open(device_path, os.O_RDONLY | os.O_NONBLOCK)
print(f"Monitoring {device_path} for {duration}s...")
import time
deadline = time.time() + duration
while time.time() < deadline:
ready, _, _ = select.select([fd], [], [], 0.1)
if not ready:
continue
data = os.read(fd, EVENT_SIZE)
if len(data) < EVENT_SIZE:
continue
tv_sec, tv_usec, ev_type, code, value = struct.unpack(EVENT_FMT, data)
type_name = EV_TYPES.get(ev_type, f"?{ev_type}")
print(f"{type_name:4s} code={code:4d} value={value}")
os.close(fd)
monitor_input(find_keyboard())
This works without any external library — just the kernel’s event interface.
The examples above use the kernel interfaces directly. For production code, consider:
psutil (wraps /proc and sysfs)pyserialpyusb (libusb backend) or device-specific driversgpiod (uses character device), RPi.GPIO (Raspberry Pi specific)smbus2spidevevdev (wraps the kernel input subsystem)pyudevUnderstanding the kernel interfaces (what this book covers) makes you a better user of all these libraries — you know what they’re actually doing and how to debug when they fail.
Previous: Chapter 10 — Reading Hardware from Python
Next: Chapter 12 — Conclusion