Comprehensive guide for Python development on Raspberry Pi Zero W with hardware integration. Use this skill when developing, debugging, or improving Python code that interacts with GPIO, sensors, or other hardware components.
This skill provides comprehensive guidance for developing Python applications that interact with Raspberry Pi Zero W hardware, including GPIO programming, code structure, testing strategies, error handling, and deployment practices.
Use this skill when:
Current Project Configuration:
#!/usr/bin/python (points to Python 2.x on Raspberry Pi OS)python3Recommendations for New Code:
#!/usr/bin/env python3 for new scriptsCore GPIO Library:
# Install RPi.GPIO for Python 3
sudo apt-get install python3-rpi.gpio
# Install RPi.GPIO for Python 2
sudo apt-get install python-rpi.gpio
Common Additional Libraries:
# For I2C/SMBus devices
sudo apt-get install python3-smbus i2c-tools
# For SPI devices
sudo apt-get install python3-spidev
# For system utilities
sudo apt-get install python3-psutil
# For configuration files
sudo apt-get install python3-yaml python3-configparser
project-name/
├── project_name.py # Main application script
├── config.py # Configuration constants
├── hardware/ # Hardware abstraction modules
│ ├── __init__.py
│ ├── gpio_manager.py # GPIO setup and management
│ └── sensors.py # Sensor interfaces
├── utils/ # Utility functions
│ ├── __init__.py
│ ├── logging_config.py # Logging setup
│ └── file_utils.py # File operations
├── tests/ # Test scripts (syntax validation)
│ └── test_syntax.py
└── README.md # Documentation
Luigi modules MUST use configuration files in /etc/luigi/{module-path}/:
The configuration path follows the repository structure. For a module at motion-detection/mario/, the config file is /etc/luigi/motion-detection/mario/mario.conf.
Configuration File Format (INI-style):
# /etc/luigi/motion-detection/mario/mario.conf
# Mario Motion Detection Configuration
[GPIO]
# GPIO pin for PIR sensor (BCM numbering)
SENSOR_PIN=23
[Timing]
# Cooldown period in seconds (30 minutes = 1800)
COOLDOWN_SECONDS=1800
# Main loop sleep interval
MAIN_LOOP_SLEEP=100
[Files]
# Sound directory
SOUND_DIR=/usr/share/sounds/mario/
# Timer file location
TIMER_FILE=/tmp/mario_timer
# Log file location
LOG_FILE=/var/log/motion.log
[Logging]
# Log level (DEBUG, INFO, WARNING, ERROR)
LOG_LEVEL=INFO
# Maximum log file size in bytes
LOG_MAX_BYTES=10485760
# Number of backup log files
LOG_BACKUP_COUNT=5
Python Config Loader Pattern:
#!/usr/bin/env python3
"""Configuration loader for Luigi modules."""
import os
import configparser
from pathlib import Path
class Config:
"""Load configuration from file with fallback to defaults."""
# Default configuration values
DEFAULT_SENSOR_PIN = 23
DEFAULT_COOLDOWN_SECONDS = 1800
DEFAULT_SOUND_DIR = "/usr/share/sounds/mario/"
DEFAULT_LOG_FILE = "/var/log/motion.log"
DEFAULT_LOG_LEVEL = "INFO"
def __init__(self, module_path="motion-detection/mario"):
"""
Initialize configuration.
Args:
module_path: Module path matching repository structure
(e.g., "motion-detection/mario", "sensors/temp")
"""
self.module_path = module_path
self.config_file = f"/etc/luigi/{module_path}/config.conf"
self._load_config()
def _load_config(self):
"""Load configuration from file or use defaults."""
parser = configparser.ConfigParser()
if os.path.exists(self.config_file):
try:
parser.read(self.config_file)
self.SENSOR_PIN = parser.getint('GPIO', 'SENSOR_PIN',
fallback=self.DEFAULT_SENSOR_PIN)
self.COOLDOWN_SECONDS = parser.getint('Timing', 'COOLDOWN_SECONDS',
fallback=self.DEFAULT_COOLDOWN_SECONDS)
self.SOUND_DIR = parser.get('Files', 'SOUND_DIR',
fallback=self.DEFAULT_SOUND_DIR)
self.LOG_FILE = parser.get('Files', 'LOG_FILE',
fallback=self.DEFAULT_LOG_FILE)
self.LOG_LEVEL = parser.get('Logging', 'LOG_LEVEL',
fallback=self.DEFAULT_LOG_LEVEL)
print(f"Configuration loaded from {self.config_file}")
except Exception as e:
print(f"Warning: Error reading config file: {e}")
print("Using default configuration")
self._use_defaults()
else:
print(f"Config file not found: {self.config_file}")
print("Using default configuration")
self._use_defaults()
def _use_defaults(self):
"""Set all values to defaults."""
self.SENSOR_PIN = self.DEFAULT_SENSOR_PIN
self.COOLDOWN_SECONDS = self.DEFAULT_COOLDOWN_SECONDS
self.SOUND_DIR = self.DEFAULT_SOUND_DIR
self.LOG_FILE = self.DEFAULT_LOG_FILE
self.LOG_LEVEL = self.DEFAULT_LOG_LEVEL
# Usage in main code
config = Config(module_path="motion-detection/mario")
GPIO.setup(config.SENSOR_PIN, GPIO.IN)
Alternative: Simple Key=Value Parser (No Dependencies):
def load_config(config_file, defaults):
"""
Load simple key=value config file.
Args:
config_file: Path to config file
defaults: Dictionary of default values
Returns:
Dictionary of configuration values
"""
config = defaults.copy()
if not os.path.exists(config_file):
return config
try:
with open(config_file, 'r') as f:
for line in f:
line = line.strip()
# Skip comments and empty lines
if not line or line.startswith('#'):
continue
# Parse key=value
if '=' in line:
key, value = line.split('=', 1)
key = key.strip()
value = value.strip()
# Convert types
if value.isdigit():
config[key] = int(value)
elif value.lower() in ('true', 'false'):
config[key] = value.lower() == 'true'
else:
config[key] = value
except Exception as e:
print(f"Warning: Error reading config: {e}")
return config
# Usage
defaults = {
'SENSOR_PIN': 23,
'COOLDOWN_SECONDS': 1800,
'SOUND_DIR': '/usr/share/sounds/mario/',
'LOG_FILE': '/var/log/motion.log'
}
config = load_config('/etc/luigi/motion-detection/mario/mario.conf', defaults)
GPIO.setup(config['SENSOR_PIN'], GPIO.IN)
Create abstraction layer for hardware:
# hardware/gpio_manager.py
"""GPIO management and abstraction."""
import RPi.GPIO as GPIO
import logging
class GPIOManager:
"""Manages GPIO setup and cleanup."""
def __init__(self, mode=GPIO.BCM):
"""Initialize GPIO with specified mode."""
self.mode = mode
self.initialized = False
self.pins_in_use = []
def initialize(self):
"""Set up GPIO mode."""
try:
GPIO.setmode(self.mode)
self.initialized = True
logging.info(f"GPIO initialized with mode: {self.mode}")
except RuntimeError as e:
logging.error(f"Failed to initialize GPIO: {e}")
raise
def setup_input(self, pin, pull_up_down=None):
"""Configure pin as input."""
if not self.initialized:
raise RuntimeError("GPIO not initialized")
kwargs = {}
if pull_up_down is not None:
kwargs['pull_up_down'] = pull_up_down
GPIO.setup(pin, GPIO.IN, **kwargs)
self.pins_in_use.append(pin)
logging.debug(f"Pin {pin} configured as input")
def setup_output(self, pin, initial=GPIO.LOW):
"""Configure pin as output."""
if not self.initialized:
raise RuntimeError("GPIO not initialized")
GPIO.setup(pin, GPIO.OUT, initial=initial)
self.pins_in_use.append(pin)
logging.debug(f"Pin {pin} configured as output")
def cleanup(self):
"""Clean up GPIO resources."""
if self.initialized:
GPIO.cleanup()
self.initialized = False
logging.info("GPIO cleaned up")
Sensor abstraction example:
# hardware/sensors.py
"""Sensor interface classes."""
import RPi.GPIO as GPIO
import logging
class PIRSensor:
"""PIR motion sensor interface."""
def __init__(self, pin, callback=None):
"""
Initialize PIR sensor.
Args:
pin: GPIO pin number (BCM mode)
callback: Function to call on motion detection
"""
self.pin = pin
self.callback = callback
self._event_registered = False
def start(self):
"""Start monitoring for motion."""
if self.callback is None:
raise ValueError("Callback function required")
try:
GPIO.add_event_detect(
self.pin,
GPIO.RISING,
callback=self.callback
)
self._event_registered = True
logging.info(f"PIR sensor started on pin {self.pin}")
except RuntimeError as e:
logging.error(f"Failed to start PIR sensor: {e}")
raise
def stop(self):
"""Stop monitoring for motion."""
if self._event_registered:
GPIO.remove_event_detect(self.pin)
self._event_registered = False
logging.info(f"PIR sensor stopped on pin {self.pin}")
@staticmethod
def read_state(pin):
"""Read current sensor state."""
return GPIO.input(pin)
Always handle permission errors:
import sys
import RPi.GPIO as GPIO