🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
203 lines
6.2 KiB
Python
203 lines
6.2 KiB
Python
"""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
|