209 lines
7.3 KiB
Python
209 lines
7.3 KiB
Python
"""Orchestration for standalone LibreVNA signal-generator sweeps."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import replace
|
|
import logging
|
|
import time
|
|
|
|
from python_app.generator_sweep.config import (
|
|
GeneratorSweepConfig,
|
|
ResolvedGeneratorSweepConfig,
|
|
resolve_generator_sweep_config,
|
|
)
|
|
from python_app.generator_sweep.pwm import HardwarePwmGate
|
|
from python_app.hardware_full.librevna_driver import GeneratorSettings, LibreVNADevice
|
|
from python_app.hardware_full.switch_drivers.gpio_uapi import GpioOutputLines
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class _MarkerOutputs:
|
|
"""Own two GPIO marker signals used during generator sweeps."""
|
|
|
|
def __init__(
|
|
self,
|
|
*,
|
|
gpio_chip: str,
|
|
sweep_start_pin: int,
|
|
curr_step_pin: int,
|
|
curr_step_initial_level: int,
|
|
) -> None:
|
|
"""Store GPIO metadata and initialize marker state."""
|
|
self._lines = GpioOutputLines(
|
|
chip=gpio_chip,
|
|
offsets=[sweep_start_pin, curr_step_pin],
|
|
consumer="generator_sweep",
|
|
)
|
|
self._curr_step_initial_level = curr_step_initial_level
|
|
self._sweep_start_level = 0
|
|
self._curr_step_level = curr_step_initial_level
|
|
self._is_open = False
|
|
|
|
def open(self) -> None:
|
|
"""Request GPIO lines and apply idle levels."""
|
|
self._lines.open()
|
|
self._is_open = True
|
|
self.reset_to_idle()
|
|
|
|
def close(self) -> None:
|
|
"""Release GPIO lines."""
|
|
self._is_open = False
|
|
self._lines.close()
|
|
|
|
def begin_sweep(self) -> None:
|
|
"""Raise sweep marker while preserving current step level."""
|
|
time.sleep(0.001)
|
|
self._sweep_start_level = 1
|
|
self._apply()
|
|
|
|
def end_sweep(self) -> None:
|
|
"""Drop sweep marker after a completed pass."""
|
|
self._sweep_start_level = 0
|
|
self._apply()
|
|
|
|
def toggle_step(self) -> None:
|
|
"""Invert current-step marker."""
|
|
self._curr_step_level = 1 - self._curr_step_level
|
|
self._apply()
|
|
|
|
def reset_to_idle(self) -> None:
|
|
"""Return both markers to their configured idle state."""
|
|
self._sweep_start_level = 0
|
|
self._curr_step_level = self._curr_step_initial_level
|
|
self._apply()
|
|
|
|
def _apply(self) -> None:
|
|
"""Write current marker levels to GPIO."""
|
|
if not self._is_open:
|
|
return
|
|
self._lines.set_values([self._sweep_start_level, self._curr_step_level])
|
|
|
|
|
|
class GeneratorSweepRunner:
|
|
"""Execute a signal-generator frequency sweep defined in run config."""
|
|
|
|
def __init__(self, config: GeneratorSweepConfig) -> None:
|
|
"""Resolve configuration and prepare runtime helpers."""
|
|
self._config: ResolvedGeneratorSweepConfig = resolve_generator_sweep_config(config)
|
|
self._device: LibreVNADevice | None = None
|
|
self._markers = _MarkerOutputs(
|
|
gpio_chip=self._config.gpio_chip,
|
|
sweep_start_pin=self._config.sweep_start_pin,
|
|
curr_step_pin=self._config.curr_step_pin,
|
|
curr_step_initial_level=self._config.curr_step_initial_level,
|
|
)
|
|
self._pwm = HardwarePwmGate(
|
|
pin=self._config.pwm_pin,
|
|
frequency_hz=self._config.pwm_frequency_hz,
|
|
duty_cycle=self._config.pwm_duty_cycle,
|
|
)
|
|
self._base_generator_settings = GeneratorSettings(
|
|
frequency_hz=float(self._config.frequencies_hz[0]),
|
|
power_dbm=self._config.power_dbm,
|
|
active_port=self._config.port,
|
|
apply_amplitude_correction=self._config.amplitude_correction,
|
|
)
|
|
|
|
def run(self) -> None:
|
|
"""Open resources and execute one-shot or looping sweeps."""
|
|
try:
|
|
self._open()
|
|
while True:
|
|
self._run_single_sweep()
|
|
if not self._config.loop:
|
|
break
|
|
except KeyboardInterrupt:
|
|
logger.info("Generator sweep interrupted by user")
|
|
finally:
|
|
self._close()
|
|
|
|
def _open(self) -> None:
|
|
"""Open GPIO, PWM, and LibreVNA device connection."""
|
|
self._markers.open()
|
|
self._pwm.open()
|
|
self._device = LibreVNADevice()
|
|
self._device.connect(
|
|
serial=self._config.serial,
|
|
strict_protocol_version=self._config.strict_protocol_version,
|
|
timeout_s=2.0,
|
|
)
|
|
self._device.generator.set_idle(timeout_s=1.0)
|
|
|
|
logger.info(
|
|
"Generator sweep ready: points=%d start=%dHz stop=%dHz loop=%s",
|
|
len(self._config.frequencies_hz),
|
|
self._config.frequencies_hz[0],
|
|
self._config.frequencies_hz[-1],
|
|
self._config.loop,
|
|
)
|
|
|
|
def _close(self) -> None:
|
|
"""Best-effort cleanup of PWM, GPIO, and device state."""
|
|
self._pwm.disable()
|
|
|
|
device = self._device
|
|
if device is not None:
|
|
try:
|
|
if device.is_connected:
|
|
device.generator.set_idle(timeout_s=1.0)
|
|
except Exception as exc: # noqa: BLE001
|
|
logger.warning("Failed to set LibreVNA idle mode during shutdown: %s", exc)
|
|
finally:
|
|
device.disconnect()
|
|
self._device = None
|
|
|
|
try:
|
|
self._markers.reset_to_idle()
|
|
finally:
|
|
self._markers.close()
|
|
self._pwm.close()
|
|
|
|
def _run_single_sweep(self) -> None:
|
|
"""Execute one pass from the first configured frequency to the last."""
|
|
self._markers.begin_sweep()
|
|
try:
|
|
for index, frequency_hz in enumerate(self._config.frequencies_hz):
|
|
self._pwm.disable()
|
|
self._set_generator_frequency(frequency_hz)
|
|
if index > 0:
|
|
self._markers.toggle_step()
|
|
self._wait_for_generator_ready()
|
|
self._emit_pwm_window()
|
|
finally:
|
|
self._pwm.disable()
|
|
self._markers.end_sweep()
|
|
|
|
def _set_generator_frequency(self, frequency_hz: int) -> None:
|
|
"""Transmit next generator frequency to LibreVNA."""
|
|
device = self._require_device()
|
|
settings = replace(self._base_generator_settings, frequency_hz=float(frequency_hz))
|
|
device.generator.configure(settings, timeout_s=max(self._config.settle_timeout_s, 1.0))
|
|
logger.debug("Generator frequency set to %d Hz", frequency_hz)
|
|
|
|
def _wait_for_generator_ready(self) -> None:
|
|
"""Wait until generator lock telemetry reports ready."""
|
|
device = self._require_device()
|
|
device.generator.wait_until_ready(
|
|
timeout_s=self._config.settle_timeout_s,
|
|
poll_interval_s=self._config.status_poll_s,
|
|
)
|
|
if self._config.post_lock_delay_s > 0.0:
|
|
time.sleep(self._config.post_lock_delay_s)
|
|
|
|
def _emit_pwm_window(self) -> None:
|
|
"""Enable PWM only during the configured hold window."""
|
|
if self._config.hold_time_s <= 0.0:
|
|
return
|
|
self._pwm.enable()
|
|
try:
|
|
time.sleep(self._config.hold_time_s)
|
|
finally:
|
|
self._pwm.disable()
|
|
|
|
def _require_device(self) -> LibreVNADevice:
|
|
"""Return connected device or fail fast if runner is not open."""
|
|
if self._device is None:
|
|
raise RuntimeError("Generator sweep runner is not open")
|
|
return self._device
|