dreader-application/setup_rpi.py
Duncan Tourolle 70c0b4a1f2
All checks were successful
Python CI / test (3.12) (push) Successful in 7m1s
Python CI / test (3.13) (push) Successful in 7m10s
HW integratation
2025-11-11 11:57:39 +01:00

446 lines
13 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Raspberry Pi Setup Script for DReader Hardware.
This interactive script helps configure your DReader e-reader hardware by:
1. Detecting connected hardware (I2C devices, SPI, etc.)
2. Creating/editing hardware_config.json
3. Installing required system packages
4. Setting up permissions and services
Usage:
sudo python3 setup_rpi.py
"""
import sys
import os
import json
import subprocess
from pathlib import Path
from typing import Dict, List, Optional, Tuple
# Check if running on Raspberry Pi
try:
with open('/proc/device-tree/model', 'r') as f:
model = f.read()
if 'Raspberry Pi' not in model:
print("⚠️ Warning: This doesn't appear to be a Raspberry Pi")
print(f" Detected: {model.strip()}")
response = input("Continue anyway? (y/N): ")
if response.lower() != 'y':
sys.exit(1)
except:
print("⚠️ Warning: Could not detect Raspberry Pi")
class Colors:
"""ANSI color codes for terminal output."""
HEADER = '\033[95m'
BLUE = '\033[94m'
CYAN = '\033[96m'
GREEN = '\033[92m'
YELLOW = '\033[93m'
RED = '\033[91m'
END = '\033[0m'
BOLD = '\033[1m'
def print_header(text: str):
"""Print a header."""
print(f"\n{Colors.BOLD}{Colors.BLUE}{'='*70}{Colors.END}")
print(f"{Colors.BOLD}{Colors.BLUE}{text:^70}{Colors.END}")
print(f"{Colors.BOLD}{Colors.BLUE}{'='*70}{Colors.END}\n")
def print_step(number: int, text: str):
"""Print a step number."""
print(f"\n{Colors.BOLD}{Colors.CYAN}Step {number}: {text}{Colors.END}")
print(f"{Colors.CYAN}{'-'*70}{Colors.END}")
def print_success(text: str):
"""Print success message."""
print(f"{Colors.GREEN}{text}{Colors.END}")
def print_warning(text: str):
"""Print warning message."""
print(f"{Colors.YELLOW}{text}{Colors.END}")
def print_error(text: str):
"""Print error message."""
print(f"{Colors.RED}{text}{Colors.END}")
def run_command(cmd: str, check: bool = True) -> Tuple[int, str, str]:
"""Run a shell command and return result."""
try:
result = subprocess.run(
cmd,
shell=True,
capture_output=True,
text=True,
check=check
)
return result.returncode, result.stdout, result.stderr
except subprocess.CalledProcessError as e:
return e.returncode, e.stdout, e.stderr
def check_interfaces() -> Dict[str, bool]:
"""Check if I2C and SPI interfaces are enabled."""
print("Checking system interfaces...")
interfaces = {
'i2c': False,
'spi': False,
}
# Check I2C
if os.path.exists('/dev/i2c-1'):
interfaces['i2c'] = True
print_success("I2C interface enabled")
else:
print_warning("I2C interface not enabled")
# Check SPI
if os.path.exists('/dev/spidev0.0'):
interfaces['spi'] = True
print_success("SPI interface enabled")
else:
print_warning("SPI interface not enabled")
return interfaces
def detect_i2c_devices() -> List[str]:
"""Detect I2C devices."""
print("\nScanning I2C bus...")
returncode, stdout, stderr = run_command("i2cdetect -y 1", check=False)
if returncode != 0:
print_warning("Could not scan I2C bus (i2cdetect not found or no permission)")
return []
# Parse i2cdetect output
devices = []
for line in stdout.split('\n'):
if ':' in line:
# Extract hex addresses
parts = line.split(':')[1].split()
for part in parts:
if part != '--' and len(part) == 2:
devices.append(f"0x{part}")
if devices:
print_success(f"Found {len(devices)} I2C device(s): {', '.join(devices)}")
# Identify known devices
device_map = {
'0x38': 'FT5316 Touch Panel',
'0x14': 'BMA400 Accelerometer',
'0x15': 'BMA400 Accelerometer (alt)',
'0x68': 'PCF8523 RTC',
'0x40': 'INA219 Power Monitor',
}
print("\nDetected devices:")
for addr in devices:
device_name = device_map.get(addr, 'Unknown device')
print(f" {addr}: {device_name}")
else:
print_warning("No I2C devices detected")
return devices
def enable_interfaces():
"""Enable I2C and SPI interfaces."""
print("\nEnabling interfaces...")
# Use raspi-config to enable I2C and SPI
print("Enabling I2C...")
run_command("raspi-config nonint do_i2c 0", check=False)
print("Enabling SPI...")
run_command("raspi-config nonint do_spi 0", check=False)
print_success("Interfaces enabled (reboot required to take effect)")
def setup_permissions():
"""Set up user permissions for GPIO, I2C, and SPI."""
print("\nSetting up user permissions...")
user = os.environ.get('SUDO_USER', os.environ.get('USER'))
groups = ['gpio', 'i2c', 'spi']
for group in groups:
print(f"Adding user '{user}' to group '{group}'...")
returncode, _, _ = run_command(f"usermod -a -G {group} {user}", check=False)
if returncode == 0:
print_success(f"Added to {group} group")
else:
print_warning(f"Could not add to {group} group (may not exist)")
print_warning("You must log out and back in for group changes to take effect")
def get_vcom_voltage() -> float:
"""Prompt user for VCOM voltage."""
print("\n" + Colors.BOLD + "VCOM Voltage Configuration" + Colors.END)
print("="*70)
print("Your e-ink display has a VCOM voltage printed on a label.")
print("This is usually on the back of the display.")
print("")
print("Example labels:")
print(" • VCOM = -2.06V")
print(" • VCOM: -1.98V")
print(" • -2.14V")
print("")
print(Colors.RED + Colors.BOLD + "⚠️ IMPORTANT: Using incorrect VCOM can damage your display!" + Colors.END)
print("")
while True:
vcom_str = input("Enter your display's VCOM voltage (e.g., -2.06): ").strip()
try:
vcom = float(vcom_str)
if vcom > 0:
print_warning("VCOM is usually negative. Did you forget the minus sign?")
continue
if vcom < -3.0 or vcom > -1.0:
print_warning(f"VCOM {vcom}V is unusual. Most displays are between -1.5V and -2.5V")
confirm = input("Are you sure this is correct? (y/N): ")
if confirm.lower() != 'y':
continue
return vcom
except ValueError:
print_error("Invalid voltage. Please enter a number (e.g., -2.06)")
def configure_gpio_buttons() -> dict:
"""Configure GPIO buttons interactively."""
print("\n" + Colors.BOLD + "GPIO Button Configuration" + Colors.END)
print("="*70)
print("Configure physical buttons for navigation.")
print("Buttons should be connected between GPIO pin and GND.")
print("")
enable = input("Enable GPIO buttons? (Y/n): ").strip().lower()
if enable == 'n':
return {
"enabled": False,
"pull_up": True,
"bounce_time_ms": 200,
"buttons": []
}
buttons = []
# Common button configurations (based on actual hardware)
button_presets = [
("prev_page", "Previous Page", "swipe_right", 22),
("next_page", "Next Page", "swipe_left", 27),
("power_off", "Power Off", "long_press", 21),
]
print("\nAvailable GPIOs (BCM numbering): 2-27 (avoid 2, 3 if using I2C)")
print("")
for name, description, default_gesture, default_gpio in button_presets:
print(f"\n{Colors.BOLD}{description} Button{Colors.END}")
enable_btn = input(f" Enable {description} button? (Y/n): ").strip().lower()
if enable_btn == 'n':
continue
# Get GPIO pin
while True:
gpio_str = input(f" GPIO pin (default {default_gpio}): ").strip()
if not gpio_str:
gpio = default_gpio
break
try:
gpio = int(gpio_str)
if gpio < 2 or gpio > 27:
print_error(" GPIO must be between 2 and 27")
continue
if gpio in [2, 3]:
print_warning(" GPIO 2/3 are I2C pins (SDA/SCL)")
confirm = input(" Use anyway? (y/N): ")
if confirm.lower() != 'y':
continue
break
except ValueError:
print_error(" Invalid GPIO number")
# Add button
buttons.append({
"name": name,
"gpio": gpio,
"gesture": default_gesture,
"description": description
})
print_success(f" Configured: GPIO {gpio} -> {description}")
return {
"enabled": True,
"pull_up": True,
"bounce_time_ms": 200,
"buttons": buttons
}
def create_hardware_config(vcom: float, gpio_config: dict, i2c_devices: List[str]) -> dict:
"""Create hardware configuration dictionary."""
# Auto-detect which optional components are available
has_touch = '0x38' in i2c_devices
has_accel = '0x14' in i2c_devices or '0x15' in i2c_devices
has_rtc = '0x68' in i2c_devices
has_power = '0x40' in i2c_devices
config = {
"_description": "Hardware configuration for DReader e-ink device",
"_generated": "Generated by setup_rpi.py",
"display": {
"width": 1872,
"height": 1404,
"vcom": vcom,
"spi_hz": 24000000,
"auto_sleep": True
},
"gpio_buttons": gpio_config,
"accelerometer": {
"enabled": has_accel,
"tilt_enabled": False,
"orientation_enabled": has_accel,
"calibration_file": "accelerometer_config.json"
},
"rtc": {
"enabled": has_rtc
},
"power_monitor": {
"enabled": has_power,
"shunt_ohms": 0.1,
"battery_capacity_mah": 3000,
"low_battery_threshold": 20.0,
"show_battery_interval": 100
},
"application": {
"library_path": "/home/pi/Books",
"auto_save_interval": 60,
"force_library_mode": False,
"log_level": "INFO"
}
}
return config
def main():
"""Main setup function."""
print_header("DReader Raspberry Pi Hardware Setup")
# Check if running as root
if os.geteuid() != 0:
print_error("This script must be run with sudo")
print("Usage: sudo python3 setup_rpi.py")
sys.exit(1)
# Step 1: Check interfaces
print_step(1, "Checking System Interfaces")
interfaces = check_interfaces()
if not all(interfaces.values()):
print("\nSome interfaces are not enabled.")
enable = input("Enable I2C and SPI now? (Y/n): ").strip().lower()
if enable != 'n':
enable_interfaces()
print_warning("Reboot required for interface changes to take effect")
# Step 2: Detect hardware
print_step(2, "Detecting I2C Devices")
i2c_devices = detect_i2c_devices()
if not i2c_devices:
print_warning("No I2C devices detected. Check your wiring.")
print("See HARDWARE_SETUP.md for wiring instructions.")
# Step 3: Set up permissions
print_step(3, "Setting Up User Permissions")
setup_permissions()
# Step 4: Configure VCOM
print_step(4, "Display Configuration")
vcom = get_vcom_voltage()
print_success(f"VCOM voltage set to {vcom}V")
# Step 5: Configure GPIO buttons
print_step(5, "GPIO Button Configuration")
gpio_config = configure_gpio_buttons()
if gpio_config["enabled"]:
print_success(f"Configured {len(gpio_config['buttons'])} button(s)")
else:
print("GPIO buttons disabled")
# Step 6: Generate configuration
print_step(6, "Generating Configuration File")
config = create_hardware_config(vcom, gpio_config, i2c_devices)
config_file = Path("hardware_config.json")
with open(config_file, 'w') as f:
json.dump(config, f, indent=2)
print_success(f"Configuration saved to {config_file}")
# Step 7: Summary
print_header("Setup Complete!")
print("Configuration summary:")
print(f" • Display: {config['display']['width']}x{config['display']['height']}, VCOM={config['display']['vcom']}V")
print(f" • GPIO Buttons: {'Enabled' if gpio_config['enabled'] else 'Disabled'}")
if gpio_config['enabled']:
for btn in gpio_config['buttons']:
print(f" - {btn['description']}: GPIO {btn['gpio']}")
print(f" • Accelerometer: {'Enabled' if config['accelerometer']['enabled'] else 'Disabled'}")
print(f" • RTC: {'Enabled' if config['rtc']['enabled'] else 'Disabled'}")
print(f" • Power Monitor: {'Enabled' if config['power_monitor']['enabled'] else 'Disabled'}")
print("\n" + Colors.BOLD + "Next Steps:" + Colors.END)
print("1. Review and edit hardware_config.json if needed")
print("2. Reboot if you enabled I2C/SPI: sudo reboot")
print("3. Log out and back in for permission changes")
print("4. Run: python examples/run_on_hardware_config.py")
print("")
if __name__ == '__main__':
try:
main()
except KeyboardInterrupt:
print("\n\nSetup cancelled by user")
sys.exit(1)
except Exception as e:
print_error(f"Setup failed: {e}")
import traceback
traceback.print_exc()
sys.exit(1)