added generator mode

This commit is contained in:
Ayzen
2026-04-01 13:00:01 +03:00
parent 43d1c226a1
commit 4abc95c372
16 changed files with 936 additions and 0 deletions
@@ -28,6 +28,7 @@ from .models import (
DeviceInfo,
DeviceLimits,
DeviceStatus,
GeneratorSettings,
Packet,
StreamHandle,
SweepResult,
@@ -45,6 +46,7 @@ __all__ = [
"DeviceInfo",
"DeviceLimits",
"DeviceStatus",
"GeneratorSettings",
"HardwareFamily",
"IncompleteSweepError",
"LibreVNADevice",
@@ -1,9 +1,11 @@
"""Public controller classes."""
from .config import ConfigController
from .generator import GeneratorController
from .vna import VNAController
__all__ = [
"ConfigController",
"GeneratorController",
"VNAController",
]
@@ -0,0 +1,65 @@
"""Signal-generator controller for direct protocol packets."""
from __future__ import annotations
import logging
import time
from ..enums import PacketType
from ..exceptions import TimeoutError
from ..models import DeviceStatus, GeneratorSettings, Packet
from ..session import LibreVNASession
logger = logging.getLogger(__name__)
class GeneratorController:
"""Signal-generator operations built on direct packet protocol."""
def __init__(self, session: LibreVNASession) -> None:
"""Bind generator controller to active session."""
self._session = session
self._settings: GeneratorSettings | None = None
def configure(self, settings: GeneratorSettings, *, timeout_s: float = 1.0) -> None:
"""Configure generator mode and active CW output."""
self._settings = settings
self._session.send(Packet(PacketType.GENERATOR, settings), require_ack=True, timeout_s=timeout_s)
logger.info(
"Generator configured: freq=%.3fHz power=%.2fdBm port=%d correction=%s",
settings.frequency_hz,
settings.power_dbm,
settings.active_port,
settings.apply_amplitude_correction,
)
def set_idle(self, *, timeout_s: float = 1.0) -> None:
"""Return device to idle mode and stop generator output."""
logger.info("Setting LibreVNA idle mode")
self._session.send(Packet(PacketType.SET_IDLE), require_ack=True, timeout_s=timeout_s)
def wait_until_ready(
self,
*,
timeout_s: float,
poll_interval_s: float,
) -> DeviceStatus:
"""Wait until available lock flags report the generator is ready."""
deadline = time.monotonic() + timeout_s
while True:
status = self._session.get_device_status(timeout_s=min(timeout_s, 1.0))
lock_values = [value for value in (status.source_locked, status.lo_locked) if value is not None]
if lock_values:
if all(lock_values):
return status
else:
raise RuntimeError(
f"Generator lock telemetry is unavailable for hardware family {status.family.name}"
)
remaining = deadline - time.monotonic()
if remaining <= 0:
raise TimeoutError("Timed out waiting for LibreVNA generator lock")
time.sleep(min(poll_interval_s, remaining))
@@ -6,6 +6,7 @@ import logging
from types import TracebackType
from .api.config import ConfigController
from .api.generator import GeneratorController
from .api.vna import VNAController
from .enums import PacketType
from .models import DeviceInfo, DeviceStatus, Packet, USBDeviceDescriptor
@@ -22,6 +23,7 @@ class LibreVNADevice:
self._session = LibreVNASession()
self.vna = VNAController(self._session)
self.generator = GeneratorController(self._session)
self.config = ConfigController(self._session)
def __enter__(self) -> LibreVNADevice:
@@ -120,6 +120,23 @@ class VNASweepSettings:
raise ValueError("power sweep requires f_start_hz == f_stop_hz")
@dataclass(slots=True)
class GeneratorSettings:
"""Configuration for LibreVNA signal-generator mode."""
frequency_hz: float = 1_000_000.0
power_dbm: float = -10.0
active_port: int = 1
apply_amplitude_correction: bool = False
def __post_init__(self) -> None:
"""Validate generator settings before transmission."""
if self.frequency_hz <= 0:
raise ValueError("frequency_hz must be > 0")
if self.active_port not in {1, 2}:
raise ValueError("active_port must be 1 or 2")
@dataclass(slots=True)
class DeviceConfigVariant:
"""Family-specific device configuration fields."""
@@ -4,6 +4,7 @@ from .codec import (
decode_packet_payload,
decode_vna_datapoint_payload,
encode_device_config_payload,
encode_generator_settings_payload,
encode_packet_payload,
encode_sweep_settings_payload,
ensure_no_payload_types,
@@ -22,6 +23,7 @@ __all__ = [
"decode_vna_datapoint_payload",
"encode_device_config_payload",
"encode_frame",
"encode_generator_settings_payload",
"encode_packet_payload",
"encode_sweep_settings_payload",
"ensure_no_payload_types",
@@ -15,6 +15,7 @@ from ..models import (
DeviceInfo,
DeviceLimits,
DeviceStatus,
GeneratorSettings,
Packet,
VNADatapointPacket,
VNASweepSettings,
@@ -26,6 +27,7 @@ logger = logging.getLogger(__name__)
_DEVICE_INFO_STRUCT = struct.Struct("<HBBBBcQQIIHhhIIBQBH")
_SWEEP_SETTINGS_STRUCT = struct.Struct("<QQHIhBHhH")
_GENERATOR_SETTINGS_STRUCT = struct.Struct("<QhBB")
_DEVICE_CONFIG_V1_STRUCT = struct.Struct("<IBHB")
_DEVICE_CONFIG_VFF_STRUCT = struct.Struct("<IIIBH")
_DEVICE_CONFIG_VFE_STRUCT = struct.Struct("<H")
@@ -275,6 +277,16 @@ def encode_sweep_settings_payload(settings: VNASweepSettings) -> bytes:
)
def encode_generator_settings_payload(settings: GeneratorSettings) -> bytes:
"""Encode ``GeneratorSettings`` payload."""
return _GENERATOR_SETTINGS_STRUCT.pack(
int(round(settings.frequency_hz)),
int(round(settings.power_dbm * 100.0)),
int(settings.active_port),
int(bool(settings.apply_amplitude_correction)),
)
def parse_device_config(payload: bytes, family: HardwareFamily) -> DeviceConfigVariant:
"""Decode family-specific `DeviceConfiguration` payload."""
if len(payload) != 15:
@@ -378,6 +390,8 @@ def encode_packet_payload(packet_type: PacketType, payload: object) -> bytes:
if packet_type == PacketType.SWEEP_SETTINGS and isinstance(payload, VNASweepSettings):
return encode_sweep_settings_payload(payload)
if packet_type == PacketType.GENERATOR and isinstance(payload, GeneratorSettings):
return encode_generator_settings_payload(payload)
if packet_type == PacketType.DEVICE_CONFIGURATION and isinstance(payload, DeviceConfigVariant):
return encode_device_config_payload(payload)
@@ -396,6 +410,7 @@ NO_PAYLOAD_PACKET_TYPES = {
PacketType.REQUEST_DEVICE_CONFIGURATION,
PacketType.REQUEST_DEVICE_STATUS,
PacketType.INITIATE_SWEEP,
PacketType.SET_IDLE,
PacketType.RESET_DEVICE_CONFIGURATION,
}