Initial commit - Phase 3/4

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
michael
2026-01-06 13:45:29 -08:00
parent 1da6730735
commit 4f35df1781
323 changed files with 98287 additions and 1195 deletions

View File

@@ -0,0 +1,202 @@
"""Hardware service for plugin hardware access.
Provides controlled access to hardware interfaces (I2C, SPI, GPIO, Serial)
with permission checking and logging.
"""
import logging
from typing import Optional, Any
logger = logging.getLogger(__name__)
class HardwareService:
"""Service for controlled hardware access.
Provides safe wrappers for hardware interfaces that:
- Log all access attempts
- Handle import errors gracefully (for non-Pi systems)
- Return None if hardware is unavailable
Permission checking is done by PluginBase before calling these methods.
"""
# Track which plugins have accessed which hardware
_access_log: dict[str, list[str]] = {}
@classmethod
def _log_access(cls, plugin_id: str, hardware_type: str) -> None:
"""Log hardware access for auditing.
Args:
plugin_id: Plugin requesting access
hardware_type: Type of hardware (i2c, gpio, spi, serial)
"""
if plugin_id not in cls._access_log:
cls._access_log[plugin_id] = []
if hardware_type not in cls._access_log[plugin_id]:
cls._access_log[plugin_id].append(hardware_type)
logger.info(f"Plugin '{plugin_id}' accessed {hardware_type}")
@classmethod
def get_access_log(cls) -> dict[str, list[str]]:
"""Get the hardware access log.
Returns:
Dictionary mapping plugin IDs to list of accessed hardware types
"""
return cls._access_log.copy()
@staticmethod
def get_i2c_bus(plugin_id: str, bus: int = 1) -> Optional[Any]:
"""Get I2C bus for a plugin.
Args:
plugin_id: Plugin requesting access (for logging)
bus: I2C bus number (default: 1 for Raspberry Pi)
Returns:
SMBus instance or None if unavailable
"""
HardwareService._log_access(plugin_id, "i2c")
try:
from smbus2 import SMBus
return SMBus(bus)
except ImportError:
logger.warning("smbus2 not available - I2C access disabled")
return None
except Exception as e:
logger.error(f"Failed to open I2C bus {bus}: {e}")
return None
@staticmethod
def get_gpio(plugin_id: str) -> Optional[Any]:
"""Get GPIO access for a plugin.
Returns the RPi.GPIO module if available.
Plugin is responsible for setup/cleanup.
Args:
plugin_id: Plugin requesting access (for logging)
Returns:
GPIO module or None if unavailable
"""
HardwareService._log_access(plugin_id, "gpio")
try:
import RPi.GPIO as GPIO
return GPIO
except ImportError:
# Try gpiozero as fallback
try:
import gpiozero
logger.info("Using gpiozero instead of RPi.GPIO")
return gpiozero
except ImportError:
logger.warning("No GPIO library available (RPi.GPIO or gpiozero)")
return None
except Exception as e:
logger.error(f"Failed to access GPIO: {e}")
return None
@staticmethod
def get_spi(plugin_id: str, bus: int = 0, device: int = 0) -> Optional[Any]:
"""Get SPI device for a plugin.
Args:
plugin_id: Plugin requesting access (for logging)
bus: SPI bus number (default: 0)
device: SPI device/chip select (default: 0)
Returns:
SpiDev instance or None if unavailable
"""
HardwareService._log_access(plugin_id, "spi")
try:
import spidev
spi = spidev.SpiDev()
spi.open(bus, device)
return spi
except ImportError:
logger.warning("spidev not available - SPI access disabled")
return None
except Exception as e:
logger.error(f"Failed to open SPI bus {bus} device {device}: {e}")
return None
@staticmethod
def get_serial(
plugin_id: str,
port: str,
baudrate: int = 9600,
timeout: float = 1.0
) -> Optional[Any]:
"""Get serial port for a plugin.
Args:
plugin_id: Plugin requesting access (for logging)
port: Serial port path (e.g., '/dev/ttyUSB0')
baudrate: Baud rate (default: 9600)
timeout: Read timeout in seconds (default: 1.0)
Returns:
Serial instance or None if unavailable
"""
HardwareService._log_access(plugin_id, "serial")
try:
import serial
return serial.Serial(port, baudrate, timeout=timeout)
except ImportError:
logger.warning("pyserial not available - serial access disabled")
return None
except Exception as e:
logger.error(f"Failed to open serial port {port}: {e}")
return None
@staticmethod
def is_hardware_available(hardware_type: str) -> bool:
"""Check if a hardware type is available on this system.
Args:
hardware_type: One of 'i2c', 'gpio', 'spi', 'serial'
Returns:
True if the hardware library is importable
"""
try:
if hardware_type == "i2c":
from smbus2 import SMBus
return True
elif hardware_type == "gpio":
try:
import RPi.GPIO
return True
except ImportError:
import gpiozero
return True
elif hardware_type == "spi":
import spidev
return True
elif hardware_type == "serial":
import serial
return True
else:
return False
except ImportError:
return False
@staticmethod
def get_available_hardware() -> list[str]:
"""Get list of available hardware types.
Returns:
List of available hardware type names
"""
available = []
for hw_type in ["i2c", "gpio", "spi", "serial"]:
if HardwareService.is_hardware_available(hw_type):
available.append(hw_type)
return available