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