203 lines
7.7 KiB
Python
203 lines
7.7 KiB
Python
"""Configuration resolution for the standalone generator sweep."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
|
|
@dataclass(slots=True, frozen=True)
|
|
class GeneratorSweepConfig:
|
|
"""User-edited generator sweep configuration stored next to the script."""
|
|
|
|
log_level: str = "INFO"
|
|
serial: str | None = None
|
|
strict_protocol_version: int = 14
|
|
start_hz: float = 1_000_000.0
|
|
stop_hz: float = 6_000_000.0
|
|
step_hz: float = 0.0
|
|
points: int = 0
|
|
hold_time_ms: float = 50.0
|
|
loop: bool = True
|
|
port: int = 1
|
|
power_dbm: float = -10.0
|
|
amplitude_correction: bool = False
|
|
settle_timeout_ms: int = 1_000
|
|
status_poll_ms: float = 10.0
|
|
post_lock_delay_us: int = 0
|
|
gpio_chip: str = "/dev/gpiochip0"
|
|
sweep_start_pin: int = 5
|
|
curr_step_pin: int = 6
|
|
curr_step_initial_level: int = 0
|
|
pwm_pin: int = 12
|
|
pwm_frequency_hz: int = 2_000_000
|
|
pwm_duty_cycle: float = 0.5
|
|
|
|
|
|
@dataclass(slots=True, frozen=True)
|
|
class ResolvedGeneratorSweepConfig:
|
|
"""Fully validated generator sweep configuration."""
|
|
|
|
log_level: int
|
|
serial: str | None
|
|
strict_protocol_version: int
|
|
frequencies_hz: tuple[int, ...]
|
|
hold_time_s: float
|
|
loop: bool
|
|
port: int
|
|
power_dbm: float
|
|
amplitude_correction: bool
|
|
settle_timeout_s: float
|
|
status_poll_s: float
|
|
post_lock_delay_s: float
|
|
gpio_chip: str
|
|
sweep_start_pin: int
|
|
curr_step_pin: int
|
|
curr_step_initial_level: int
|
|
pwm_pin: int
|
|
pwm_frequency_hz: int
|
|
pwm_duty_cycle: float
|
|
|
|
|
|
def _require(condition: bool, message: str) -> None:
|
|
"""Raise ``ValueError`` when a configuration invariant is violated."""
|
|
if not condition:
|
|
raise ValueError(message)
|
|
|
|
|
|
def _validate_positive_int(value: int, label: str) -> None:
|
|
"""Validate positive integer setting."""
|
|
_require(value > 0, f"{label} must be > 0")
|
|
|
|
|
|
def _validate_non_negative_int(value: int, label: str) -> None:
|
|
"""Validate non-negative integer setting."""
|
|
_require(value >= 0, f"{label} must be >= 0")
|
|
|
|
|
|
def _validate_non_negative_float(value: float, label: str) -> None:
|
|
"""Validate non-negative float setting."""
|
|
_require(value >= 0.0, f"{label} must be >= 0")
|
|
|
|
|
|
def _validate_positive_float(value: float, label: str) -> None:
|
|
"""Validate positive float setting."""
|
|
_require(value > 0.0, f"{label} must be > 0")
|
|
|
|
|
|
def _resolve_log_level(value: str) -> int:
|
|
"""Normalize logging level name to ``logging`` module integer constant."""
|
|
import logging
|
|
|
|
normalized = value.strip().upper()
|
|
if not normalized:
|
|
raise ValueError("generator_sweep.log_level must not be empty")
|
|
|
|
level = logging.getLevelName(normalized)
|
|
if isinstance(level, int):
|
|
return level
|
|
raise ValueError("generator_sweep.log_level must be one of DEBUG, INFO, WARNING, ERROR, CRITICAL")
|
|
|
|
|
|
def _build_frequency_grid(model: GeneratorSweepConfig) -> tuple[int, ...]:
|
|
"""Build the ordered frequency grid with inclusive stop frequency."""
|
|
start_hz = int(round(model.start_hz))
|
|
stop_hz = int(round(model.stop_hz))
|
|
step_hz = int(round(model.step_hz))
|
|
points = int(model.points)
|
|
|
|
_validate_positive_int(start_hz, "generator_sweep.start_hz")
|
|
_validate_positive_int(stop_hz, "generator_sweep.stop_hz")
|
|
_require(stop_hz >= start_hz, "generator_sweep.stop_hz must be >= generator_sweep.start_hz")
|
|
|
|
has_step = step_hz > 0
|
|
has_points = points > 0
|
|
_require(has_step != has_points, "generator_sweep must define exactly one of step_hz or points")
|
|
|
|
if start_hz == stop_hz:
|
|
if has_points:
|
|
_require(points == 1, "generator_sweep.points must be 1 when start_hz == stop_hz")
|
|
return (start_hz,)
|
|
|
|
if has_step:
|
|
_validate_positive_int(step_hz, "generator_sweep.step_hz")
|
|
frequencies: list[int] = []
|
|
current_hz = start_hz
|
|
while current_hz < stop_hz:
|
|
frequencies.append(current_hz)
|
|
current_hz += step_hz
|
|
if not frequencies or frequencies[-1] != stop_hz:
|
|
frequencies.append(stop_hz)
|
|
return tuple(frequencies)
|
|
|
|
_validate_positive_int(points, "generator_sweep.points")
|
|
_require(points >= 2, "generator_sweep.points must be >= 2 when start_hz != stop_hz")
|
|
span_hz = stop_hz - start_hz
|
|
frequencies = tuple(
|
|
int(round(start_hz + (span_hz * index) / float(points - 1)))
|
|
for index in range(points)
|
|
)
|
|
_require(frequencies[0] == start_hz, "generator_sweep.points grid does not start at start_hz")
|
|
_require(frequencies[-1] == stop_hz, "generator_sweep.points grid does not end at stop_hz")
|
|
_require(
|
|
all(left < right for left, right in zip(frequencies, frequencies[1:])),
|
|
"generator sweep grid contains duplicate or non-monotonic frequencies",
|
|
)
|
|
return frequencies
|
|
|
|
|
|
def resolve_generator_sweep_config(model: GeneratorSweepConfig) -> ResolvedGeneratorSweepConfig:
|
|
"""Validate and resolve runtime configuration for generator sweep mode."""
|
|
log_level = _resolve_log_level(model.log_level)
|
|
frequencies_hz = _build_frequency_grid(model)
|
|
hold_time_ms = float(model.hold_time_ms)
|
|
settle_timeout_ms = int(model.settle_timeout_ms)
|
|
status_poll_ms = float(model.status_poll_ms)
|
|
post_lock_delay_us = int(model.post_lock_delay_us)
|
|
pwm_frequency_hz = int(model.pwm_frequency_hz)
|
|
sweep_start_pin = int(model.sweep_start_pin)
|
|
curr_step_pin = int(model.curr_step_pin)
|
|
pwm_pin = int(model.pwm_pin)
|
|
curr_step_initial_level = int(model.curr_step_initial_level)
|
|
|
|
_validate_non_negative_float(hold_time_ms, "generator_sweep.hold_time_ms")
|
|
_validate_positive_int(settle_timeout_ms, "generator_sweep.settle_timeout_ms")
|
|
_validate_positive_float(status_poll_ms, "generator_sweep.status_poll_ms")
|
|
_validate_non_negative_int(post_lock_delay_us, "generator_sweep.post_lock_delay_us")
|
|
_validate_positive_int(pwm_frequency_hz, "generator_sweep.pwm_frequency_hz")
|
|
_validate_positive_int(int(model.strict_protocol_version), "generator_sweep.strict_protocol_version")
|
|
_require(model.port in {1, 2}, "generator_sweep.port must be 1 or 2")
|
|
_require(curr_step_initial_level in {0, 1}, "generator_sweep.curr_step_initial_level must be 0 or 1")
|
|
_require(bool(model.gpio_chip.strip()), "generator_sweep.gpio_chip must not be empty")
|
|
_validate_non_negative_int(sweep_start_pin, "generator_sweep.sweep_start_pin")
|
|
_validate_non_negative_int(curr_step_pin, "generator_sweep.curr_step_pin")
|
|
_validate_non_negative_int(pwm_pin, "generator_sweep.pwm_pin")
|
|
_require(
|
|
len({sweep_start_pin, curr_step_pin, pwm_pin}) == 3,
|
|
"generator_sweep sweep_start_pin, curr_step_pin, and pwm_pin must be distinct",
|
|
)
|
|
_require(
|
|
0.0 < float(model.pwm_duty_cycle) <= 1.0,
|
|
"generator_sweep.pwm_duty_cycle must be within (0, 1]",
|
|
)
|
|
|
|
return ResolvedGeneratorSweepConfig(
|
|
log_level=log_level,
|
|
serial=(model.serial.strip() or None) if isinstance(model.serial, str) else model.serial,
|
|
strict_protocol_version=int(model.strict_protocol_version),
|
|
frequencies_hz=frequencies_hz,
|
|
hold_time_s=hold_time_ms / 1000.0,
|
|
loop=bool(model.loop),
|
|
port=int(model.port),
|
|
power_dbm=float(model.power_dbm),
|
|
amplitude_correction=bool(model.amplitude_correction),
|
|
settle_timeout_s=settle_timeout_ms / 1000.0,
|
|
status_poll_s=status_poll_ms / 1000.0,
|
|
post_lock_delay_s=post_lock_delay_us / 1_000_000.0,
|
|
gpio_chip=model.gpio_chip.strip(),
|
|
sweep_start_pin=sweep_start_pin,
|
|
curr_step_pin=curr_step_pin,
|
|
curr_step_initial_level=curr_step_initial_level,
|
|
pwm_pin=pwm_pin,
|
|
pwm_frequency_hz=pwm_frequency_hz,
|
|
pwm_duty_cycle=float(model.pwm_duty_cycle),
|
|
)
|