added generator mode
This commit is contained in:
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user