added generator mode
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
"""Hardware-PWM backend for Raspberry Pi 5 generator sweep gating."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
_PWM_CHANNEL_BY_PIN = {
|
||||
12: 0,
|
||||
13: 1,
|
||||
18: 2,
|
||||
19: 3,
|
||||
}
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class HardwarePwmGate:
|
||||
"""Thin wrapper around Raspberry Pi hardware PWM output."""
|
||||
|
||||
pin: int
|
||||
frequency_hz: int
|
||||
duty_cycle: float
|
||||
_pwm: object | None = field(init=False, default=None, repr=False)
|
||||
_running: bool = field(init=False, default=False, repr=False)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
"""Validate static PWM configuration."""
|
||||
if self.pin not in _PWM_CHANNEL_BY_PIN:
|
||||
raise ValueError(
|
||||
"generator_sweep.pwm_pin must be one of GPIO12, GPIO13, GPIO18, or GPIO19 on Raspberry Pi 5"
|
||||
)
|
||||
if self.frequency_hz <= 0:
|
||||
raise ValueError("generator_sweep.pwm_frequency_hz must be > 0")
|
||||
if not (0.0 < self.duty_cycle <= 1.0):
|
||||
raise ValueError("generator_sweep.pwm_duty_cycle must be within (0, 1]")
|
||||
|
||||
def open(self) -> None:
|
||||
"""Initialize hardware PWM handle."""
|
||||
if self._pwm is not None:
|
||||
return
|
||||
|
||||
try:
|
||||
from rpi_hardware_pwm import HardwarePWM
|
||||
except ImportError as exc:
|
||||
raise RuntimeError(
|
||||
"rpi_hardware_pwm is required for generator_sweep PWM output. "
|
||||
"Install project dependencies on the Raspberry Pi before running this script."
|
||||
) from exc
|
||||
|
||||
channel = _PWM_CHANNEL_BY_PIN[self.pin]
|
||||
try:
|
||||
self._pwm = HardwarePWM(pwm_channel=channel, hz=self.frequency_hz, chip=0)
|
||||
except Exception as exc:
|
||||
config_hint = _boot_config_hint()
|
||||
raise RuntimeError(
|
||||
"Failed to initialize Raspberry Pi hardware PWM. "
|
||||
f"Enable the PWM overlay in {config_hint} by adding "
|
||||
"'dtoverlay=pwm-2chan', then reboot the Raspberry Pi."
|
||||
) from exc
|
||||
self._running = False
|
||||
|
||||
def enable(self) -> None:
|
||||
"""Start PWM output when not already running."""
|
||||
if self._pwm is None:
|
||||
raise RuntimeError("Hardware PWM gate is not open")
|
||||
if self._running:
|
||||
return
|
||||
|
||||
duty_percent = self.duty_cycle * 100.0
|
||||
self._pwm.start(duty_percent)
|
||||
self._running = True
|
||||
|
||||
def disable(self) -> None:
|
||||
"""Stop PWM output when active."""
|
||||
if self._pwm is None or not self._running:
|
||||
return
|
||||
self._pwm.stop()
|
||||
self._running = False
|
||||
|
||||
def close(self) -> None:
|
||||
"""Stop PWM and release runtime state."""
|
||||
self.disable()
|
||||
self._pwm = None
|
||||
|
||||
|
||||
def _boot_config_hint() -> str:
|
||||
"""Return the most likely active Raspberry Pi boot config path."""
|
||||
firmware_config = Path("/boot/firmware/config.txt")
|
||||
if firmware_config.exists():
|
||||
return str(firmware_config)
|
||||
return "/boot/config.txt"
|
||||
Reference in New Issue
Block a user