Chapter 10 — Reading Hardware from Python

The Python Hardware Toolkit

After nine chapters of theory, let’s consolidate the practical patterns. Python has several mechanisms for hardware interaction, from high-level to low-level:

Mechanism When to Use Example
Read sysfs file Read hardware attributes CPU temp, battery level
Write sysfs file Control hardware LED brightness, power state
Open /dev file Stream I/O with a device Serial port, GPIO
ioctl() Device-specific control commands Terminal size, disk info
mmap() Bulk or memory-mapped access Hardware registers, shared memory
ctypes Interpret raw binary data Struct parsing, C library calls

Pattern 1: Reading Sysfs Attributes

The simplest and safest pattern. No special permissions needed for most attributes.

from pathlib import Path

def sysfs_read(path):
    return Path(path).read_text().strip()

# CPU temperature
temp_mc = int(sysfs_read("/sys/class/thermal/thermal_zone0/temp"))
print(f"CPU: {temp_mc / 1000:.1f}°C")

# Battery
bat = Path("/sys/class/power_supply/BAT0")
if bat.exists():
    print(f"Battery: {sysfs_read(bat / 'capacity')}%")
    print(f"Status:  {sysfs_read(bat / 'status')}")

# Network interface
print(f"eth0: {sysfs_read('/sys/class/net/eth0/operstate')}")

Pattern 2: Writing Sysfs Attributes (with caution)

Some sysfs attributes are writable. These require appropriate permissions (often root).

def sysfs_write(path, value):
    Path(path).write_text(str(value))

# Control a backlight (requires membership in 'video' group or root)
max_brightness = int(sysfs_read("/sys/class/backlight/intel_backlight/max_brightness"))
sysfs_write("/sys/class/backlight/intel_backlight/brightness", max_brightness // 2)

# Toggle a LED
sysfs_write("/sys/class/leds/input3::capslock/brightness", "1")

Pattern 3: File I/O on /dev Devices

Character devices behave like files. Open them, read/write bytes.

import os, select

# Read from /dev/input/event0 (raw input events)
# Each event is a 24-byte struct: time, type, code, value
fd = os.open("/dev/input/event0", os.O_RDONLY | os.O_NONBLOCK)

# Poll for events (non-blocking)
ready, _, _ = select.select([fd], [], [], 1.0)  # 1s timeout
if ready:
    raw = os.read(fd, 24)
    import struct
    tv_sec, tv_usec, ev_type, code, value = struct.unpack("llHHi", raw)
    print(f"Event: type={ev_type} code={code} value={value}")

os.close(fd)

Pattern 4: ioctl for Device Control

ioctl sends device-specific control commands. The request codes are defined in kernel headers or Python constants.

import fcntl, struct, os

# Get terminal size
TIOCGWINSZ = 0x5413
with open("/dev/tty") as tty:
    buf = struct.pack("4H", 0, 0, 0, 0)
    result = fcntl.ioctl(tty.fileno(), TIOCGWINSZ, buf)
rows, cols, _, _ = struct.unpack("4H", result)
print(f"Terminal: {cols} cols × {rows} rows")
# Get disk size via ioctl (BLKGETSIZE64 = 0x80081272)
BLKGETSIZE64 = 0x80081272
with open("/dev/sda", "rb") as f:
    buf = struct.pack("Q", 0)
    result = fcntl.ioctl(f.fileno(), BLKGETSIZE64, buf)
    size = struct.unpack("Q", result)[0]
    print(f"Disk size: {size / 1e9:.1f} GB")

Pattern 5: mmap for Bulk/Memory-Mapped Access

Use mmap when you need fast access to large regions or hardware memory.

import mmap

# Fast file access with mmap
with open("large_data.bin", "r+b") as f:
    with mmap.mmap(f.fileno(), 0) as mm:
        # Random access without seeking
        header = mm[0:16]
        # Slice assignment writes through to file
        mm[1024:1028] = b"\xFF\xFF\xFF\xFF"
# Read a hardware register region (PCI BAR, requires root)
import mmap, os, struct

def read_pci_bar(pci_addr, bar_number=0):
    resource = f"/sys/bus/pci/devices/{pci_addr}/resource{bar_number}"
    size = os.path.getsize(resource)
    if size == 0:
        return None
    with open(resource, "rb") as f:
        mm = mmap.mmap(f.fileno(), size, mmap.MAP_SHARED, mmap.PROT_READ)
        # Read first 4 registers (32-bit each)
        regs = [struct.unpack_from("<I", mm, i*4)[0] for i in range(4)]
        mm.close()
    return regs

Pattern 6: ctypes for Binary Data and C Libraries

ctypes lets you work with C data structures and call C libraries directly.

import ctypes, struct

# Define a C struct in Python
class InputEvent(ctypes.Structure):
    _fields_ = [
        ("tv_sec",  ctypes.c_long),
        ("tv_usec", ctypes.c_long),
        ("type",    ctypes.c_uint16),
        ("code",    ctypes.c_uint16),
        ("value",   ctypes.c_int32),
    ]

# Read and parse directly
import os
fd = os.open("/dev/input/event0", os.O_RDONLY)
event = InputEvent()
nbytes = ctypes.sizeof(event)
data = os.read(fd, nbytes)
ctypes.memmove(ctypes.addressof(event), data, nbytes)
print(f"type={event.type} code={event.code} value={event.value}")
os.close(fd)

Pattern 7: Enumerating Hardware Programmatically

from pathlib import Path

def list_usb_devices():
    """List all USB devices with vendor/product info."""
    usb_path = Path("/sys/bus/usb/devices")
    for device in sorted(usb_path.iterdir()):
        vid_file = device / "idVendor"
        pid_file = device / "idProduct"
        if vid_file.exists() and pid_file.exists():
            vid = vid_file.read_text().strip()
            pid = pid_file.read_text().strip()
            mfg = (device / "manufacturer").read_text().strip() \
                  if (device / "manufacturer").exists() else "?"
            prod = (device / "product").read_text().strip() \
                   if (device / "product").exists() else "?"
            print(f"{vid}:{pid}  {mfg} {prod}")

list_usb_devices()
def list_network_interfaces():
    """List all network interfaces with their state and MAC."""
    net = Path("/sys/class/net")
    for iface in sorted(net.iterdir()):
        state = (iface / "operstate").read_text().strip()
        try:
            mac = (iface / "address").read_text().strip()
        except:
            mac = "N/A"
        print(f"{iface.name:<12} {state:<10} {mac}")

list_network_interfaces()

Error Handling

Hardware I/O raises specific exceptions:

import errno

try:
    with open("/dev/ttyUSB0", "rb") as f:
        data = f.read(64)
except FileNotFoundError:
    print("Device not connected")
except PermissionError:
    print("Need to be in 'dialout' group: sudo usermod -aG dialout $USER")
except OSError as e:
    if e.errno == errno.EBUSY:
        print("Device busy — another process has it open")
    elif e.errno == errno.EIO:
        print("I/O error — hardware problem")
    else:
        raise

Using /proc from Python

def system_stats():
    # Uptime
    with open("/proc/uptime") as f:
        uptime_sec = float(f.read().split()[0])

    # Memory
    meminfo = {}
    with open("/proc/meminfo") as f:
        for line in f:
            k, _, v = line.partition(":")
            meminfo[k.strip()] = v.strip()

    mem_total = int(meminfo["MemTotal"].split()[0]) * 1024
    mem_avail = int(meminfo["MemAvailable"].split()[0]) * 1024

    print(f"Uptime: {uptime_sec/3600:.1f} hours")
    print(f"Memory: {mem_avail//1024//1024} MB free / {mem_total//1024//1024} MB total")

system_stats()

All these patterns are assembled into complete scripts in the code/ directory.


Previous: Chapter 9 — udev and Hotplug

Next: Chapter 11 — Practical Examples

Back to Table of Contents