Chapter 11 — Practical Examples: PCI, GPIO, USB, Serial from Python

Overview

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.


Example 1: Enumerate and Decode PCI Devices

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

Example 2: System Hardware Monitor

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()

Example 3: GPIO Control via the Character Device

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.


Example 4: USB Device Detection and Serial Communication

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}")

Example 5: Real-Time Input Event Monitor

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.


Where to Go From Here

The examples above use the kernel interfaces directly. For production code, consider:

Understanding 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

Back to Table of Contents