updated acquisition
This commit is contained in:
@ -32,12 +32,7 @@ async def health_check():
|
|||||||
async def device_status():
|
async def device_status():
|
||||||
"""Get device connection status and details."""
|
"""Get device connection status and details."""
|
||||||
acquisition = singletons.vna_data_acquisition_instance
|
acquisition = singletons.vna_data_acquisition_instance
|
||||||
device_info = acquisition.get_device_info()
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"device_info": device_info,
|
|
||||||
"acquisition_running": acquisition.is_running,
|
"acquisition_running": acquisition.is_running,
|
||||||
"last_sweep": acquisition.sweep_buffer.get_latest_sweep_number(),
|
|
||||||
"total_sweeps": len(acquisition.sweep_buffer.get_all_sweeps()),
|
|
||||||
"buffer_stats": acquisition.sweep_buffer.get_stats(),
|
|
||||||
}
|
}
|
||||||
@ -13,7 +13,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
import vna_system.core.singletons as singletons
|
import vna_system.core.singletons as singletons
|
||||||
from vna_system.core.processing.sweep_processor import SweepProcessingManager
|
from vna_system.core.processing.sweep_processor import SweepProcessingManager
|
||||||
from vna_system.api.websockets.websocket_handler import WebSocketManager
|
from vna_system.core.processing.websocket_handler import WebSocketManager
|
||||||
from vna_system.api.endpoints import health, processing, web_ui
|
from vna_system.api.endpoints import health, processing, web_ui
|
||||||
from vna_system.api.websockets import processing as ws_processing
|
from vna_system.api.websockets import processing as ws_processing
|
||||||
|
|
||||||
@ -66,10 +66,10 @@ async def lifespan(app: FastAPI):
|
|||||||
|
|
||||||
# Start acquisition
|
# Start acquisition
|
||||||
logger.info("Starting data acquisition...")
|
logger.info("Starting data acquisition...")
|
||||||
singletons.vna_data_acquisition_instance.start()
|
|
||||||
|
|
||||||
# Connect processing to acquisition
|
# Connect processing to acquisition
|
||||||
singletons.processing_manager.set_sweep_buffer(singletons.vna_data_acquisition_instance.sweep_buffer)
|
singletons.processing_manager.set_sweep_buffer(singletons.vna_data_acquisition_instance.sweep_buffer)
|
||||||
|
singletons.vna_data_acquisition_instance.start()
|
||||||
|
|
||||||
singletons.processing_manager.start()
|
singletons.processing_manager.start()
|
||||||
logger.info("Sweep processing started")
|
logger.info("Sweep processing started")
|
||||||
|
|
||||||
|
|||||||
@ -5,8 +5,12 @@ Configuration file for VNA data acquisition system
|
|||||||
|
|
||||||
import glob
|
import glob
|
||||||
import logging
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
import serial.tools.list_ports
|
import serial.tools.list_ports
|
||||||
|
|
||||||
|
# Base directory for VNA system
|
||||||
|
BASE_DIR = Path(__file__).parent.parent
|
||||||
|
|
||||||
# Serial communication settings
|
# Serial communication settings
|
||||||
DEFAULT_BAUD_RATE = 115200
|
DEFAULT_BAUD_RATE = 115200
|
||||||
DEFAULT_PORT = "/dev/ttyACM0"
|
DEFAULT_PORT = "/dev/ttyACM0"
|
||||||
@ -46,7 +50,7 @@ SERIAL_PEEK_SIZE = 32
|
|||||||
SERIAL_IDLE_TIMEOUT = 0.5
|
SERIAL_IDLE_TIMEOUT = 0.5
|
||||||
SERIAL_DRAIN_DELAY = 0.05
|
SERIAL_DRAIN_DELAY = 0.05
|
||||||
SERIAL_DRAIN_CHECK_DELAY = 0.01
|
SERIAL_DRAIN_CHECK_DELAY = 0.01
|
||||||
SERIAL_CONNECT_DELAY = 0.5
|
SERIAL_CONNECT_DELAY = 0.01
|
||||||
|
|
||||||
|
|
||||||
def find_vna_port():
|
def find_vna_port():
|
||||||
@ -61,21 +65,21 @@ def find_vna_port():
|
|||||||
# Method 1: Use pyserial port detection by VID/PID
|
# Method 1: Use pyserial port detection by VID/PID
|
||||||
try:
|
try:
|
||||||
ports = list(serial.tools.list_ports.comports())
|
ports = list(serial.tools.list_ports.comports())
|
||||||
|
|
||||||
logger.debug(f"Found {len(ports)} serial ports")
|
logger.debug(f"Found {len(ports)} serial ports")
|
||||||
|
|
||||||
for port in ports:
|
for port in ports:
|
||||||
logger.debug(f"Checking port {port.device}: VID={port.vid:04X} PID={port.pid:04X} "
|
logger.debug(f"Checking port {port.device}")
|
||||||
f"Manufacturer='{port.manufacturer}' Product='{port.description}'")
|
|
||||||
|
|
||||||
# Check by VID/PID
|
# Check by VID/PID
|
||||||
if port.vid == VNA_VID and port.pid == VNA_PID:
|
if port.vid == VNA_VID and port.pid == VNA_PID:
|
||||||
logger.info(f"Found VNA device by VID/PID at {port.device}")
|
logger.debug(f"Found VNA device by VID/PID at {port.device}")
|
||||||
return port.device
|
return port.device
|
||||||
|
|
||||||
# Fallback: Check by manufacturer/product strings
|
# Fallback: Check by manufacturer/product strings
|
||||||
if (port.manufacturer and VNA_MANUFACTURER.lower() in port.manufacturer.lower() and
|
if (port.manufacturer and VNA_MANUFACTURER.lower() in port.manufacturer.lower() and
|
||||||
port.description and VNA_PRODUCT.lower() in port.description.lower()):
|
port.description and VNA_PRODUCT.lower() in port.description.lower()):
|
||||||
logger.info(f"Found VNA device by description at {port.device}")
|
logger.debug(f"Found VNA device by description at {port.device}")
|
||||||
return port.device
|
return port.device
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@ -6,11 +6,10 @@ import os
|
|||||||
import struct
|
import struct
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from typing import BinaryIO, List, Optional, Tuple
|
from typing import BinaryIO, List, Tuple
|
||||||
|
|
||||||
import serial
|
import serial
|
||||||
|
|
||||||
# Avoid wildcard imports: make config access explicit and discoverable.
|
|
||||||
from vna_system.config import config as cfg
|
from vna_system.config import config as cfg
|
||||||
from vna_system.core.acquisition.sweep_buffer import SweepBuffer
|
from vna_system.core.acquisition.sweep_buffer import SweepBuffer
|
||||||
|
|
||||||
@ -21,25 +20,16 @@ logger = logging.getLogger(__name__)
|
|||||||
class VNADataAcquisition:
|
class VNADataAcquisition:
|
||||||
"""Main data acquisition class with asynchronous sweep collection."""
|
"""Main data acquisition class with asynchronous sweep collection."""
|
||||||
|
|
||||||
def __init__(self, port: str = None, baud: int = cfg.DEFAULT_BAUD_RATE) -> None:
|
def __init__(self) -> None:
|
||||||
self.bin_log_path: str = cfg.BIN_LOG_FILE_PATH
|
self.bin_log_path: str = cfg.BIN_LOG_FILE_PATH
|
||||||
|
|
||||||
# Auto-detect port if not specified
|
self.baud: int = cfg.DEFAULT_BAUD_RATE
|
||||||
if port is None:
|
|
||||||
self.port: str = cfg.get_vna_port()
|
|
||||||
logger.info(f"Using auto-detected port: {self.port}")
|
|
||||||
else:
|
|
||||||
self.port: str = port
|
|
||||||
logger.info(f"Using specified port: {self.port}")
|
|
||||||
|
|
||||||
self.baud: int = baud
|
|
||||||
|
|
||||||
self._sweep_buffer = SweepBuffer()
|
self._sweep_buffer = SweepBuffer()
|
||||||
self.ser: Optional[serial.SerialBase] = None
|
|
||||||
|
|
||||||
# Control flags
|
# Control flags
|
||||||
self._running: bool = False
|
self._running: bool = False
|
||||||
self._thread: Optional[threading.Thread] = None
|
self._thread: threading.Thread | None = None
|
||||||
self._stop_event: threading.Event = threading.Event()
|
self._stop_event: threading.Event = threading.Event()
|
||||||
|
|
||||||
# Sweep collection state
|
# Sweep collection state
|
||||||
@ -76,7 +66,6 @@ class VNADataAcquisition:
|
|||||||
self._thread.join(timeout=5.0)
|
self._thread.join(timeout=5.0)
|
||||||
logger.info("Acquisition thread joined.")
|
logger.info("Acquisition thread joined.")
|
||||||
|
|
||||||
self._close_serial()
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_running(self) -> bool:
|
def is_running(self) -> bool:
|
||||||
@ -88,99 +77,25 @@ class VNADataAcquisition:
|
|||||||
"""Return a reference to the sweep buffer."""
|
"""Return a reference to the sweep buffer."""
|
||||||
return self._sweep_buffer
|
return self._sweep_buffer
|
||||||
|
|
||||||
def get_device_info(self) -> dict:
|
|
||||||
"""Get information about the connected VNA device."""
|
|
||||||
info = {
|
|
||||||
'port': self.port,
|
|
||||||
'baud_rate': self.baud,
|
|
||||||
'is_connected': self.ser is not None and self.ser.is_open if self.ser else False,
|
|
||||||
'is_running': self.is_running,
|
|
||||||
'device_detected': False,
|
|
||||||
'device_details': None
|
|
||||||
}
|
|
||||||
|
|
||||||
# Try to get device details from port detection
|
|
||||||
try:
|
|
||||||
from serial.tools.list_ports import comports
|
|
||||||
for port in comports():
|
|
||||||
if port.device == self.port:
|
|
||||||
info['device_detected'] = True
|
|
||||||
info['device_details'] = {
|
|
||||||
'vid': f"0x{port.vid:04X}" if port.vid else None,
|
|
||||||
'pid': f"0x{port.pid:04X}" if port.pid else None,
|
|
||||||
'manufacturer': port.manufacturer,
|
|
||||||
'product': port.description,
|
|
||||||
'serial_number': port.serial_number
|
|
||||||
}
|
|
||||||
break
|
|
||||||
except Exception as e:
|
|
||||||
logger.debug(f"Could not get device details: {e}")
|
|
||||||
|
|
||||||
return info
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------- #
|
# --------------------------------------------------------------------- #
|
||||||
# Serial management
|
# Serial management
|
||||||
# --------------------------------------------------------------------- #
|
# --------------------------------------------------------------------- #
|
||||||
|
|
||||||
def _connect_serial(self) -> bool:
|
|
||||||
"""Establish the serial connection."""
|
|
||||||
try:
|
|
||||||
logger.info(f"Attempting to connect to VNA device at {self.port} @ {self.baud} baud")
|
|
||||||
|
|
||||||
# Check if port exists
|
def _drain_serial_input(self, ser: serial.Serial) -> None:
|
||||||
import os
|
|
||||||
if not os.path.exists(self.port):
|
|
||||||
logger.error(f"Port {self.port} does not exist. Device not connected?")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Check permissions
|
|
||||||
if not os.access(self.port, os.R_OK | os.W_OK):
|
|
||||||
logger.error(f"No read/write permissions for {self.port}. Try: sudo chmod 666 {self.port}")
|
|
||||||
logger.error("Or add your user to the dialout group: sudo usermod -a -G dialout $USER")
|
|
||||||
return False
|
|
||||||
|
|
||||||
self.ser = serial.Serial(self.port, baudrate=self.baud, timeout=0)
|
|
||||||
time.sleep(cfg.SERIAL_CONNECT_DELAY)
|
|
||||||
self._drain_serial_input()
|
|
||||||
logger.info(f"Serial connection established to {self.port}")
|
|
||||||
return True
|
|
||||||
except PermissionError as exc:
|
|
||||||
logger.error(f"Permission denied for {self.port}: {exc}")
|
|
||||||
logger.error("Try: sudo chmod 666 {self.port} or add user to dialout group")
|
|
||||||
return False
|
|
||||||
except serial.SerialException as exc:
|
|
||||||
logger.error(f"Serial connection failed for {self.port}: {exc}")
|
|
||||||
return False
|
|
||||||
except Exception as exc: # noqa: BLE001 (keep broad to preserve behavior)
|
|
||||||
logger.error(f"Unexpected error connecting to {self.port}: {exc}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _close_serial(self) -> None:
|
|
||||||
"""Close the serial connection if open."""
|
|
||||||
if self.ser:
|
|
||||||
try:
|
|
||||||
self.ser.close()
|
|
||||||
logger.debug("Serial connection closed.")
|
|
||||||
except Exception: # noqa: BLE001
|
|
||||||
# Preserve original behavior: swallow any serial close errors.
|
|
||||||
logger.debug("Ignoring error while closing serial.", exc_info=True)
|
|
||||||
finally:
|
|
||||||
self.ser = None
|
|
||||||
|
|
||||||
def _drain_serial_input(self) -> None:
|
|
||||||
"""Drain any pending bytes from the serial input buffer."""
|
"""Drain any pending bytes from the serial input buffer."""
|
||||||
if not self.ser:
|
if not ser:
|
||||||
return
|
return
|
||||||
|
|
||||||
drained = 0
|
drained = 0
|
||||||
while True:
|
while True:
|
||||||
bytes_waiting = getattr(self.ser, "in_waiting", 0)
|
bytes_waiting = getattr(ser, "in_waiting", 0)
|
||||||
if bytes_waiting <= 0:
|
if bytes_waiting <= 0:
|
||||||
break
|
break
|
||||||
drained += len(self.ser.read(bytes_waiting))
|
drained += len(ser.read(bytes_waiting))
|
||||||
time.sleep(cfg.SERIAL_DRAIN_CHECK_DELAY)
|
time.sleep(cfg.SERIAL_DRAIN_CHECK_DELAY)
|
||||||
if drained:
|
if drained:
|
||||||
logger.debug("Drained %d pending byte(s) from serial input.", drained)
|
logger.warning("Drained %d pending byte(s) from serial input.", drained)
|
||||||
|
|
||||||
# --------------------------------------------------------------------- #
|
# --------------------------------------------------------------------- #
|
||||||
# Acquisition loop
|
# Acquisition loop
|
||||||
@ -190,28 +105,26 @@ class VNADataAcquisition:
|
|||||||
"""Main acquisition loop executed by the background thread."""
|
"""Main acquisition loop executed by the background thread."""
|
||||||
while self._running and not self._stop_event.is_set():
|
while self._running and not self._stop_event.is_set():
|
||||||
try:
|
try:
|
||||||
if not self._connect_serial():
|
# Auto-detect port
|
||||||
time.sleep(1.0)
|
self.port: str = cfg.get_vna_port()
|
||||||
continue
|
logger.info(f"Using auto-detected port: {self.port}")
|
||||||
|
|
||||||
|
with serial.Serial(self.port, self.baud) as ser:
|
||||||
|
self._drain_serial_input(ser)
|
||||||
with open(self.bin_log_path, "rb") as raw:
|
with open(self.bin_log_path, "rb") as raw:
|
||||||
buffered = io.BufferedReader(raw, buffer_size=cfg.SERIAL_BUFFER_SIZE)
|
buffered = io.BufferedReader(raw, buffer_size=cfg.SERIAL_BUFFER_SIZE)
|
||||||
self._drain_serial_input()
|
self._process_sweep_data(buffered, ser)
|
||||||
self._process_sweep_data(buffered)
|
|
||||||
|
|
||||||
except Exception as exc: # noqa: BLE001
|
except Exception as exc: # noqa: BLE001
|
||||||
logger.error("Acquisition error: %s", exc)
|
logger.error("Acquisition error: %s", exc)
|
||||||
time.sleep(1.0)
|
time.sleep(1.0)
|
||||||
finally:
|
|
||||||
self._close_serial()
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------- #
|
# --------------------------------------------------------------------- #
|
||||||
# Log processing
|
# Log processing
|
||||||
# --------------------------------------------------------------------- #
|
# --------------------------------------------------------------------- #
|
||||||
|
|
||||||
def _process_sweep_data(self, f: BinaryIO) -> None:
|
def _process_sweep_data(self, f: BinaryIO, ser: serial.Serial) -> None:
|
||||||
"""Process the binary log file and collect sweep data one sweep at a time."""
|
"""Process the binary log file and collect sweep data one sweep at a time."""
|
||||||
while self._running and not self._stop_event.is_set():
|
|
||||||
try:
|
try:
|
||||||
# Start from beginning of file for each sweep
|
# Start from beginning of file for each sweep
|
||||||
f.seek(0)
|
f.seek(0)
|
||||||
@ -239,7 +152,7 @@ class VNADataAcquisition:
|
|||||||
|
|
||||||
if direction == cfg.DIR_TO_DEV:
|
if direction == cfg.DIR_TO_DEV:
|
||||||
# TX path: stream to device and inspect for sweep start
|
# TX path: stream to device and inspect for sweep start
|
||||||
first = self._serial_write_from_file(f, length)
|
first = self._serial_write_from_file(f, length, ser)
|
||||||
|
|
||||||
if not self._collecting and self._is_sweep_start_command(length, first):
|
if not self._collecting and self._is_sweep_start_command(length, first):
|
||||||
self._collecting = True
|
self._collecting = True
|
||||||
@ -249,7 +162,7 @@ class VNADataAcquisition:
|
|||||||
|
|
||||||
elif direction == cfg.DIR_FROM_DEV:
|
elif direction == cfg.DIR_FROM_DEV:
|
||||||
# RX path: read exact number of bytes from device
|
# RX path: read exact number of bytes from device
|
||||||
rx_bytes = self._serial_read_exact(length, capture=self._collecting)
|
rx_bytes = self._serial_read_exact(length, ser, capture=self._collecting)
|
||||||
self._skip_bytes(f, length) # Keep log file pointer in sync
|
self._skip_bytes(f, length) # Keep log file pointer in sync
|
||||||
|
|
||||||
if self._collecting:
|
if self._collecting:
|
||||||
@ -320,7 +233,7 @@ class VNADataAcquisition:
|
|||||||
raise EOFError(f"Unexpected EOF while skipping {n} bytes.")
|
raise EOFError(f"Unexpected EOF while skipping {n} bytes.")
|
||||||
remaining -= len(chunk)
|
remaining -= len(chunk)
|
||||||
|
|
||||||
def _serial_write_from_file(self, f: BinaryIO, nbytes: int) -> bytes:
|
def _serial_write_from_file(self, f: BinaryIO, nbytes: int, ser: serial.Serial) -> bytes:
|
||||||
"""
|
"""
|
||||||
Stream *nbytes* from a file-like object to the serial port.
|
Stream *nbytes* from a file-like object to the serial port.
|
||||||
|
|
||||||
@ -343,9 +256,7 @@ class VNADataAcquisition:
|
|||||||
# Write to serial
|
# Write to serial
|
||||||
written = 0
|
written = 0
|
||||||
while written < len(chunk):
|
while written < len(chunk):
|
||||||
if not self.ser:
|
n = ser.write(chunk[written:])
|
||||||
break
|
|
||||||
n = self.ser.write(chunk[written:])
|
|
||||||
if n is None:
|
if n is None:
|
||||||
n = 0
|
n = 0
|
||||||
written += n
|
written += n
|
||||||
@ -354,29 +265,27 @@ class VNADataAcquisition:
|
|||||||
|
|
||||||
return bytes(first)
|
return bytes(first)
|
||||||
|
|
||||||
def _serial_read_exact(self, nbytes: int, capture: bool = False) -> bytes:
|
def _serial_read_exact(self, nbytes: int, ser: serial.Serial, capture: bool = False) -> bytes:
|
||||||
"""Read exactly *nbytes* from the serial port; optionally capture and return them."""
|
"""Read exactly *nbytes* from the serial port; optionally capture and return them."""
|
||||||
if not self.ser:
|
|
||||||
return b""
|
|
||||||
|
|
||||||
deadline = time.monotonic() + cfg.RX_TIMEOUT
|
deadline = time.monotonic() + cfg.RX_TIMEOUT
|
||||||
total = 0
|
total = 0
|
||||||
out = bytearray() if capture else None
|
out = bytearray() if capture else None
|
||||||
old_timeout = self.ser.timeout
|
old_timeout = ser.timeout
|
||||||
self.ser.timeout = min(cfg.SERIAL_IDLE_TIMEOUT, cfg.RX_TIMEOUT)
|
ser.timeout = min(cfg.SERIAL_IDLE_TIMEOUT, cfg.RX_TIMEOUT)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
while total < nbytes:
|
while total < nbytes:
|
||||||
if time.monotonic() >= deadline:
|
if time.monotonic() >= deadline:
|
||||||
raise TimeoutError(f"Timeout while waiting for {nbytes} bytes.")
|
raise TimeoutError(f"Timeout while waiting for {nbytes} bytes.")
|
||||||
chunk = self.ser.read(nbytes - total)
|
chunk = ser.read(nbytes - total)
|
||||||
if chunk:
|
if chunk:
|
||||||
total += len(chunk)
|
total += len(chunk)
|
||||||
if capture and out is not None:
|
if capture and out is not None:
|
||||||
out.extend(chunk)
|
out.extend(chunk)
|
||||||
return bytes(out) if (capture and out is not None) else b""
|
return bytes(out) if (capture and out is not None) else b""
|
||||||
finally:
|
finally:
|
||||||
self.ser.timeout = old_timeout
|
ser.timeout = old_timeout
|
||||||
|
|
||||||
# --------------------------------------------------------------------- #
|
# --------------------------------------------------------------------- #
|
||||||
# Parsing & detection
|
# Parsing & detection
|
||||||
|
|||||||
@ -1,16 +1,9 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Sweep data structures and buffer management for VNA data acquisition.
|
|
||||||
|
|
||||||
This module contains classes for storing and managing sweep data in a thread-safe manner.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import math
|
import math
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from collections import deque
|
from collections import deque
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import List, Tuple, Optional, Dict, Any
|
from typing import List, Tuple
|
||||||
|
|
||||||
from vna_system.config.config import SWEEP_BUFFER_MAX_SIZE
|
from vna_system.config.config import SWEEP_BUFFER_MAX_SIZE
|
||||||
|
|
||||||
@ -55,35 +48,11 @@ class SweepBuffer:
|
|||||||
self._buffer.append(sweep)
|
self._buffer.append(sweep)
|
||||||
return self._sweep_counter
|
return self._sweep_counter
|
||||||
|
|
||||||
def get_latest_sweep(self) -> Optional[SweepData]:
|
def get_latest_sweep(self) -> SweepData | None:
|
||||||
"""Get the most recent sweep"""
|
"""Get the most recent sweep"""
|
||||||
with self._lock:
|
with self._lock:
|
||||||
return self._buffer[-1] if self._buffer else None
|
return self._buffer[-1] if self._buffer else None
|
||||||
|
|
||||||
def get_sweep_by_number(self, sweep_number: int) -> Optional[SweepData]:
|
|
||||||
"""Get a specific sweep by its number"""
|
|
||||||
with self._lock:
|
|
||||||
for sweep in reversed(self._buffer):
|
|
||||||
if sweep.sweep_number == sweep_number:
|
|
||||||
return sweep
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_all_sweeps(self) -> List[SweepData]:
|
|
||||||
"""Get all sweeps currently in buffer"""
|
|
||||||
with self._lock:
|
|
||||||
return list(self._buffer)
|
|
||||||
|
|
||||||
def get_buffer_info(self) -> Dict[str, Any]:
|
|
||||||
"""Get buffer status information"""
|
|
||||||
with self._lock:
|
|
||||||
return {
|
|
||||||
'current_size': len(self._buffer),
|
|
||||||
'max_size': self._buffer.maxlen,
|
|
||||||
'total_sweeps_processed': self._sweep_counter,
|
|
||||||
'latest_sweep_number': self._buffer[-1].sweep_number if self._buffer else None,
|
|
||||||
'oldest_sweep_number': self._buffer[0].sweep_number if self._buffer else None,
|
|
||||||
'next_sweep_number': self._sweep_counter + 1
|
|
||||||
}
|
|
||||||
|
|
||||||
def set_sweep_counter(self, sweep_number: int) -> None:
|
def set_sweep_counter(self, sweep_number: int) -> None:
|
||||||
"""Set the sweep counter to continue from a specific number."""
|
"""Set the sweep counter to continue from a specific number."""
|
||||||
|
|||||||
@ -6,7 +6,7 @@ Base sweep processor interface and utilities.
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import abc
|
import abc
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict
|
||||||
|
|
||||||
from vna_system.core.acquisition.sweep_buffer import SweepData
|
from vna_system.core.acquisition.sweep_buffer import SweepData
|
||||||
|
|
||||||
@ -26,7 +26,7 @@ class BaseSweepProcessor(abc.ABC):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def process(self, sweep: SweepData) -> Optional[ProcessingResult]:
|
def process(self, sweep: SweepData) -> ProcessingResult | None:
|
||||||
"""Process a sweep and return results."""
|
"""Process a sweep and return results."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -43,8 +43,8 @@ class ProcessingResult:
|
|||||||
processor_name: str,
|
processor_name: str,
|
||||||
sweep_number: int,
|
sweep_number: int,
|
||||||
data: Dict[str, Any],
|
data: Dict[str, Any],
|
||||||
plotly_figure: Optional[Dict[str, Any]] = None,
|
plotly_figure: Dict[str, Any] | None = None,
|
||||||
file_path: Optional[str] = None,
|
file_path: str | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.processor_name = processor_name
|
self.processor_name = processor_name
|
||||||
self.sweep_number = sweep_number
|
self.sweep_number = sweep_number
|
||||||
|
|||||||
@ -7,7 +7,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List, Optional, Tuple
|
from typing import Any, Dict, List, Tuple
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
@ -38,7 +38,7 @@ class MagnitudePlotProcessor(BaseSweepProcessor):
|
|||||||
def name(self) -> str:
|
def name(self) -> str:
|
||||||
return "magnitude_plot"
|
return "magnitude_plot"
|
||||||
|
|
||||||
def process(self, sweep: SweepData) -> Optional[ProcessingResult]:
|
def process(self, sweep: SweepData) -> ProcessingResult | None:
|
||||||
"""Process sweep data and create magnitude plot."""
|
"""Process sweep data and create magnitude plot."""
|
||||||
if not self.should_process(sweep):
|
if not self.should_process(sweep):
|
||||||
return None
|
return None
|
||||||
@ -107,7 +107,7 @@ class MagnitudePlotProcessor(BaseSweepProcessor):
|
|||||||
|
|
||||||
return fig
|
return fig
|
||||||
|
|
||||||
def _save_matplotlib_image(self, sweep: SweepData, magnitude_data: List[Tuple[float, float]]) -> Optional[Path]:
|
def _save_matplotlib_image(self, sweep: SweepData, magnitude_data: List[Tuple[float, float]]) -> Path | None:
|
||||||
"""Save plot as image file using matplotlib."""
|
"""Save plot as image file using matplotlib."""
|
||||||
try:
|
try:
|
||||||
filename = f"magnitude_sweep_{sweep.sweep_number:06d}.{self.image_format}"
|
filename = f"magnitude_sweep_{sweep.sweep_number:06d}.{self.image_format}"
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
import threading
|
import threading
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
from vna_system.core.processing.base_processor import ProcessingResult
|
from vna_system.core.processing.base_processor import ProcessingResult
|
||||||
|
|
||||||
@ -169,7 +169,7 @@ class ResultsStorage:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to remove sweep {sweep_number}: {e}")
|
logger.error(f"Failed to remove sweep {sweep_number}: {e}")
|
||||||
|
|
||||||
def get_latest_results(self, processor_name: Optional[str] = None, limit: int = 10) -> List[ProcessingResult]:
|
def get_latest_results(self, processor_name: str | None = None, limit: int = 10) -> List[ProcessingResult]:
|
||||||
"""Get latest processing results."""
|
"""Get latest processing results."""
|
||||||
with self._lock:
|
with self._lock:
|
||||||
results = []
|
results = []
|
||||||
@ -186,7 +186,7 @@ class ResultsStorage:
|
|||||||
|
|
||||||
return results[:limit]
|
return results[:limit]
|
||||||
|
|
||||||
def get_result_by_sweep(self, sweep_number: int, processor_name: Optional[str] = None) -> List[ProcessingResult]:
|
def get_result_by_sweep(self, sweep_number: int, processor_name: str | None = None) -> List[ProcessingResult]:
|
||||||
"""Get processing results for specific sweep number."""
|
"""Get processing results for specific sweep number."""
|
||||||
with self._lock:
|
with self._lock:
|
||||||
results = []
|
results = []
|
||||||
@ -227,7 +227,7 @@ class ResultsStorage:
|
|||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
def get_sweep_metadata(self, sweep_number: int) -> Optional[Dict[str, Any]]:
|
def get_sweep_metadata(self, sweep_number: int) -> Dict[str, Any] | None:
|
||||||
"""Get metadata for specific sweep."""
|
"""Get metadata for specific sweep."""
|
||||||
with self._lock:
|
with self._lock:
|
||||||
if sweep_number not in self._cache:
|
if sweep_number not in self._cache:
|
||||||
@ -243,7 +243,7 @@ class ResultsStorage:
|
|||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_available_sweeps(self, limit: Optional[int] = None) -> List[int]:
|
def get_available_sweeps(self, limit: int | None = None) -> List[int]:
|
||||||
"""Get list of available sweep numbers."""
|
"""Get list of available sweep numbers."""
|
||||||
with self._lock:
|
with self._lock:
|
||||||
sorted_sweeps = sorted(self._cache.keys(), reverse=True)
|
sorted_sweeps = sorted(self._cache.keys(), reverse=True)
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import logging
|
|||||||
import queue
|
import queue
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
from vna_system.core.acquisition.sweep_buffer import SweepBuffer, SweepData
|
from vna_system.core.acquisition.sweep_buffer import SweepBuffer, SweepData
|
||||||
from vna_system.core.processing.base_processor import BaseSweepProcessor, ProcessingResult
|
from vna_system.core.processing.base_processor import BaseSweepProcessor, ProcessingResult
|
||||||
@ -22,11 +22,11 @@ class SweepProcessingManager:
|
|||||||
def __init__(self, config_path: str = "vna_system/core/processing/config.json") -> None:
|
def __init__(self, config_path: str = "vna_system/core/processing/config.json") -> None:
|
||||||
self.processors: List[BaseSweepProcessor] = []
|
self.processors: List[BaseSweepProcessor] = []
|
||||||
self.config: Dict[str, Any] = {}
|
self.config: Dict[str, Any] = {}
|
||||||
self.sweep_buffer: Optional[SweepBuffer] = None
|
self.sweep_buffer: SweepBuffer | None = None
|
||||||
|
|
||||||
# Processing control
|
# Processing control
|
||||||
self._running = False
|
self._running = False
|
||||||
self._thread: Optional[threading.Thread] = None
|
self._thread: threading.Thread | None = None
|
||||||
self._stop_event = threading.Event()
|
self._stop_event = threading.Event()
|
||||||
self._last_processed_sweep = 0
|
self._last_processed_sweep = 0
|
||||||
|
|
||||||
@ -174,7 +174,7 @@ class SweepProcessingManager:
|
|||||||
except queue.Empty:
|
except queue.Empty:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def get_next_result(self, timeout: Optional[float] = None) -> Optional[ProcessingResult]:
|
def get_next_result(self, timeout: float | None = None) -> ProcessingResult | None:
|
||||||
"""Get next processing result from queue. Used by websocket handler."""
|
"""Get next processing result from queue. Used by websocket handler."""
|
||||||
try:
|
try:
|
||||||
return self.results_queue.get(timeout=timeout)
|
return self.results_queue.get(timeout=timeout)
|
||||||
@ -196,11 +196,11 @@ class SweepProcessingManager:
|
|||||||
break
|
break
|
||||||
return results
|
return results
|
||||||
|
|
||||||
def get_latest_results(self, processor_name: Optional[str] = None, limit: int = 10) -> List[ProcessingResult]:
|
def get_latest_results(self, processor_name: str | None = None, limit: int = 10) -> List[ProcessingResult]:
|
||||||
"""Get latest processing results from storage, optionally filtered by processor name."""
|
"""Get latest processing results from storage, optionally filtered by processor name."""
|
||||||
return self.results_storage.get_latest_results(processor_name, limit)
|
return self.results_storage.get_latest_results(processor_name, limit)
|
||||||
|
|
||||||
def get_result_by_sweep(self, sweep_number: int, processor_name: Optional[str] = None) -> List[ProcessingResult]:
|
def get_result_by_sweep(self, sweep_number: int, processor_name: str | None = None) -> List[ProcessingResult]:
|
||||||
"""Get processing results for a specific sweep number from storage."""
|
"""Get processing results for a specific sweep number from storage."""
|
||||||
return self.results_storage.get_result_by_sweep(sweep_number, processor_name)
|
return self.results_storage.get_result_by_sweep(sweep_number, processor_name)
|
||||||
|
|
||||||
|
|||||||
352
vna_system/core/settings/settings_manager.py
Normal file
352
vna_system/core/settings/settings_manager.py
Normal file
@ -0,0 +1,352 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime
|
||||||
|
from enum import Enum
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, List, Tuple
|
||||||
|
|
||||||
|
from vna_system.config import config as cfg
|
||||||
|
from vna_system.core.processing.results_storage import ResultsStorage
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------- Minimal enums & dataclass -----------------------
|
||||||
|
|
||||||
|
class VNAMode(Enum):
|
||||||
|
S11 = "S11"
|
||||||
|
S21 = "S21"
|
||||||
|
|
||||||
|
|
||||||
|
class CalibrationStandard(Enum):
|
||||||
|
OPEN = "open"
|
||||||
|
SHORT = "short"
|
||||||
|
LOAD = "load"
|
||||||
|
THROUGH = "through"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class LogConfig:
|
||||||
|
"""Parsed configuration from a selected config file."""
|
||||||
|
mode: VNAMode
|
||||||
|
file_path: Path
|
||||||
|
stem: str
|
||||||
|
start_hz: float | None
|
||||||
|
stop_hz: float | None
|
||||||
|
points: int | None
|
||||||
|
bw_hz: float | None
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------- Filename parsing helpers --------------------------
|
||||||
|
|
||||||
|
_UNIT_MULT = {
|
||||||
|
"hz": 1.0,
|
||||||
|
"khz": 1e3,
|
||||||
|
"mhz": 1e6,
|
||||||
|
"ghz": 1e9,
|
||||||
|
}
|
||||||
|
_PARAM_RE = re.compile(r"^(str|stp|pnts|bw)(?P<val>[0-9]+(?:\.[0-9]+)?)(?P<unit>[a-zA-Z]+)?$")
|
||||||
|
|
||||||
|
|
||||||
|
def _to_hz(val: float, unit: str | None, default_hz: float) -> float:
|
||||||
|
if unit:
|
||||||
|
m = _UNIT_MULT.get(unit.lower())
|
||||||
|
if m:
|
||||||
|
return float(val) * m
|
||||||
|
return float(val) * default_hz
|
||||||
|
|
||||||
|
|
||||||
|
def parse_config_filename(name: str, assume_mhz_for_freq: bool = True) -> Tuple[float | None, float | None, int | None, float] | None:
|
||||||
|
"""
|
||||||
|
Parse tokens like: str100_stp8800_pnts1000_bw1khz.[bin]
|
||||||
|
- str/stp default to MHz if no unit (configurable)
|
||||||
|
- bw defaults to Hz if no unit
|
||||||
|
"""
|
||||||
|
base = Path(name).stem
|
||||||
|
tokens = base.split("_")
|
||||||
|
|
||||||
|
start_hz = stop_hz = bw_hz = None
|
||||||
|
points: int | None = None
|
||||||
|
|
||||||
|
for t in tokens:
|
||||||
|
m = _PARAM_RE.match(t)
|
||||||
|
if not m:
|
||||||
|
continue
|
||||||
|
key = t[:3]
|
||||||
|
val = float(m.group("val"))
|
||||||
|
unit = m.group("unit")
|
||||||
|
|
||||||
|
if key == "str":
|
||||||
|
start_hz = _to_hz(val, unit, 1e6 if assume_mhz_for_freq else 1.0)
|
||||||
|
elif key == "stp":
|
||||||
|
stop_hz = _to_hz(val, unit, 1e6 if assume_mhz_for_freq else 1.0)
|
||||||
|
elif key == "pnt": # token 'pnts'
|
||||||
|
points = int(val)
|
||||||
|
elif key == "bw":
|
||||||
|
bw_hz = _to_hz(val, unit, 1.0)
|
||||||
|
|
||||||
|
return start_hz, stop_hz, points, bw_hz
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------- VNA Settings Manager ------------------------------
|
||||||
|
|
||||||
|
class VNASettingsManager:
|
||||||
|
"""
|
||||||
|
- Scans config_logs/{S11,S21}/ for available configs
|
||||||
|
- Controls current_log.bin symlink (must be a real symlink)
|
||||||
|
- Parses config params from filename
|
||||||
|
- Stores per-config calibration in:
|
||||||
|
calibration/<MODE>/<STEM>/<STANDARD>/<timestamp>_sweepNNNNNN/
|
||||||
|
- copies ALL processor result JSON files for that sweep (and metadata.json if present)
|
||||||
|
- UI helpers: select S11/S21, calibrate (through/open/short/load) by sweep number
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
base_dir: Path | None = None,
|
||||||
|
config_logs_subdir: str = "binary_logs/config_logs",
|
||||||
|
current_log_name: str = "current_log.bin",
|
||||||
|
calibration_subdir: str = "calibration",
|
||||||
|
assume_mhz_for_freq: bool = True,
|
||||||
|
results_storage: ResultsStorage | None = None,
|
||||||
|
):
|
||||||
|
self.base_dir = Path(base_dir or cfg.BASE_DIR)
|
||||||
|
self.cfg_logs_dir = self.base_dir / config_logs_subdir
|
||||||
|
self.current_log = self.cfg_logs_dir / current_log_name
|
||||||
|
self.calib_root = self.base_dir / calibration_subdir
|
||||||
|
self.assume_mhz_for_freq = assume_mhz_for_freq
|
||||||
|
|
||||||
|
# Ensure directory structure exists
|
||||||
|
(self.cfg_logs_dir / "S11").mkdir(parents=True, exist_ok=True)
|
||||||
|
(self.cfg_logs_dir / "S21").mkdir(parents=True, exist_ok=True)
|
||||||
|
self.calib_root.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# Results storage
|
||||||
|
self.results = results_storage or ResultsStorage(
|
||||||
|
storage_dir=str(self.base_dir / "processing_results")
|
||||||
|
)
|
||||||
|
|
||||||
|
# ---------- configuration selection & discovery ----------
|
||||||
|
|
||||||
|
def list_configs(self, mode: VNAMode | None = None) -> List[LogConfig]:
|
||||||
|
modes = [mode] if mode else [VNAMode.S11, VNAMode.S21]
|
||||||
|
out: List[LogConfig] = []
|
||||||
|
for m in modes:
|
||||||
|
d = self.cfg_logs_dir / m.value
|
||||||
|
if not d.exists():
|
||||||
|
continue
|
||||||
|
for fp in sorted(d.glob("*.bin")):
|
||||||
|
s, e, n, bw = parse_config_filename(fp.name, self.assume_mhz_for_freq)
|
||||||
|
out.append(LogConfig(
|
||||||
|
mode=m,
|
||||||
|
file_path=fp.resolve(),
|
||||||
|
stem=fp.stem,
|
||||||
|
start_hz=s,
|
||||||
|
stop_hz=e,
|
||||||
|
points=n,
|
||||||
|
bw_hz=bw,
|
||||||
|
))
|
||||||
|
return out
|
||||||
|
|
||||||
|
def set_current_config(self, mode: VNAMode, filename: str) -> LogConfig:
|
||||||
|
"""
|
||||||
|
Update current_log.bin symlink to point to config_logs/<MODE>/<filename>.
|
||||||
|
Real symlink only; will raise if not supported.
|
||||||
|
"""
|
||||||
|
target = (self.cfg_logs_dir / mode.value / filename).resolve()
|
||||||
|
if not target.exists():
|
||||||
|
raise FileNotFoundError(f"Config not found: {target}")
|
||||||
|
|
||||||
|
if self.current_log.exists() or self.current_log.is_symlink():
|
||||||
|
self.current_log.unlink()
|
||||||
|
|
||||||
|
# relative link if possible, else absolute (still a symlink)
|
||||||
|
try:
|
||||||
|
rel = target.relative_to(self.current_log.parent)
|
||||||
|
except ValueError:
|
||||||
|
rel = target
|
||||||
|
|
||||||
|
self.current_log.symlink_to(rel)
|
||||||
|
return self.get_current_config()
|
||||||
|
|
||||||
|
def get_current_config(self) -> LogConfig:
|
||||||
|
if not self.current_log.exists():
|
||||||
|
raise FileNotFoundError(f"{self.current_log} does not exist")
|
||||||
|
|
||||||
|
tgt = self.current_log.resolve()
|
||||||
|
mode = VNAMode(tgt.parent.name) # expects .../config_logs/<MODE>/<file>
|
||||||
|
s, e, n, bw = parse_config_filename(tgt.name, self.assume_mhz_for_freq)
|
||||||
|
return LogConfig(
|
||||||
|
mode=mode,
|
||||||
|
file_path=tgt,
|
||||||
|
stem=tgt.stem,
|
||||||
|
start_hz=s, stop_hz=e, points=n, bw_hz=bw
|
||||||
|
)
|
||||||
|
|
||||||
|
# ---------- calibration capture (ALL processors) ----------
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def required_standards(mode: VNAMode) -> List[CalibrationStandard]:
|
||||||
|
return (
|
||||||
|
[CalibrationStandard.OPEN, CalibrationStandard.SHORT, CalibrationStandard.LOAD]
|
||||||
|
if mode == VNAMode.S11
|
||||||
|
else [CalibrationStandard.THROUGH]
|
||||||
|
)
|
||||||
|
|
||||||
|
def _calib_dir(self, cfg: LogConfig, standard: CalibrationStandard | None = None) -> Path:
|
||||||
|
base = self.calib_root / cfg.mode.value / cfg.stem
|
||||||
|
return base / standard.value if standard else base
|
||||||
|
|
||||||
|
def _calib_sweep_dir(self, cfg: LogConfig, standard: CalibrationStandard, sweep_number: int, ts: str | None = None) -> Path:
|
||||||
|
"""
|
||||||
|
calibration/<MODE>/<STEM>/<STANDARD>/<timestamp>_sweepNNNNNN/
|
||||||
|
"""
|
||||||
|
ts = ts or datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
|
d = self._calib_dir(cfg, standard) / f"{ts}_sweep{sweep_number:06d}"
|
||||||
|
d.mkdir(parents=True, exist_ok=True)
|
||||||
|
return d
|
||||||
|
|
||||||
|
def record_calibration_from_sweep(
|
||||||
|
self,
|
||||||
|
standard: CalibrationStandard,
|
||||||
|
sweep_number: int,
|
||||||
|
*,
|
||||||
|
cfg: LogConfig | None = None
|
||||||
|
) -> Path:
|
||||||
|
"""
|
||||||
|
Capture ALL processor JSON results for the given sweep and save under:
|
||||||
|
calibration/<MODE>/<STEM>/<STANDARD>/<timestamp>_sweepNNNNNN/
|
||||||
|
Also copy metadata.json if available.
|
||||||
|
Returns the created sweep calibration directory.
|
||||||
|
"""
|
||||||
|
cfg = cfg or self.get_current_config()
|
||||||
|
|
||||||
|
# Get ALL results for the sweep
|
||||||
|
results = self.results.get_result_by_sweep(sweep_number, processor_name=None)
|
||||||
|
if not results:
|
||||||
|
raise FileNotFoundError(f"No processor results found for sweep {sweep_number}")
|
||||||
|
|
||||||
|
# Determine destination dir
|
||||||
|
dst_dir = self._calib_sweep_dir(cfg, standard, sweep_number)
|
||||||
|
|
||||||
|
# Save processor files (re-serialize what ResultsStorage returns)
|
||||||
|
count = 0
|
||||||
|
for r in results:
|
||||||
|
try:
|
||||||
|
dst_file = dst_dir / f"{r.processor_name}.json"
|
||||||
|
payload = {
|
||||||
|
"processor_name": r.processor_name,
|
||||||
|
"sweep_number": r.sweep_number,
|
||||||
|
"data": r.data,
|
||||||
|
}
|
||||||
|
# keep optional fields if present
|
||||||
|
if getattr(r, "plotly_figure", None) is not None:
|
||||||
|
payload["plotly_figure"] = r.plotly_figure
|
||||||
|
if getattr(r, "file_path", None) is not None:
|
||||||
|
payload["file_path"] = r.file_path
|
||||||
|
|
||||||
|
with open(dst_file, "w") as f:
|
||||||
|
json.dump(payload, f, indent=2)
|
||||||
|
count += 1
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to store processor '{r.processor_name}' for sweep {sweep_number}: {e}")
|
||||||
|
|
||||||
|
# Save metadata if available
|
||||||
|
try:
|
||||||
|
meta = self.results.get_sweep_metadata(sweep_number)
|
||||||
|
if meta:
|
||||||
|
with open(dst_dir / "metadata.json", "w") as f:
|
||||||
|
json.dump(meta, f, indent=2)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to write metadata for sweep {sweep_number}: {e}")
|
||||||
|
|
||||||
|
if count == 0:
|
||||||
|
raise RuntimeError(f"Nothing was written for sweep {sweep_number}")
|
||||||
|
|
||||||
|
logger.info(f"Stored calibration (standard={standard.value}) from sweep {sweep_number} into {dst_dir}")
|
||||||
|
return dst_dir
|
||||||
|
|
||||||
|
def latest_calibration(self, cfg: LogConfig | None = None) -> Dict[CalibrationStandard, Path] | None:
|
||||||
|
"""
|
||||||
|
Returns the latest sweep directory per required standard for the current (or provided) config.
|
||||||
|
"""
|
||||||
|
cfg = cfg or self.get_current_config()
|
||||||
|
out: Dict[CalibrationStandard, Path] | None = {}
|
||||||
|
for std in self.required_standards(cfg.mode):
|
||||||
|
d = self._calib_dir(cfg, std)
|
||||||
|
if not d.exists():
|
||||||
|
out[std] = None
|
||||||
|
continue
|
||||||
|
subdirs = sorted([p for p in d.iterdir() if p.is_dir()])
|
||||||
|
out[std] = subdirs[-1] if subdirs else None
|
||||||
|
return out
|
||||||
|
|
||||||
|
def calibration_status(self, cfg: LogConfig | None = None) -> Dict[str, bool]:
|
||||||
|
cfg = cfg or self.get_current_config()
|
||||||
|
latest = self.latest_calibration(cfg)
|
||||||
|
return {std.value: (p is not None and p.exists()) for std, p in latest.items()}
|
||||||
|
|
||||||
|
def is_fully_calibrated(self, cfg: LogConfig | None = None) -> bool:
|
||||||
|
return all(self.calibration_status(cfg).values())
|
||||||
|
|
||||||
|
# ---------- UI helpers ----------
|
||||||
|
|
||||||
|
def summary(self) -> Dict[str, object]:
|
||||||
|
cfg = self.get_current_config()
|
||||||
|
latest = self.latest_calibration(cfg)
|
||||||
|
return {
|
||||||
|
"mode": cfg.mode.value,
|
||||||
|
"current_log": str(self.current_log),
|
||||||
|
"selected_file": str(cfg.file_path),
|
||||||
|
"stem": cfg.stem,
|
||||||
|
"params": {
|
||||||
|
"start_hz": cfg.start_hz,
|
||||||
|
"stop_hz": cfg.stop_hz,
|
||||||
|
"points": cfg.points,
|
||||||
|
"bw_hz": cfg.bw_hz,
|
||||||
|
},
|
||||||
|
"required_standards": [s.value for s in self.required_standards(cfg.mode)],
|
||||||
|
"calibration_latest": {k.value: (str(v) if v else None) for k, v in latest.items()},
|
||||||
|
"is_fully_calibrated": self.is_fully_calibrated(cfg),
|
||||||
|
}
|
||||||
|
|
||||||
|
def ui_select_S11(self, filename: str) -> Dict[str, object]:
|
||||||
|
self.set_current_config(VNAMode.S11, filename)
|
||||||
|
return self.summary()
|
||||||
|
|
||||||
|
def ui_select_S21(self, filename: str) -> Dict[str, object]:
|
||||||
|
self.set_current_config(VNAMode.S21, filename)
|
||||||
|
return self.summary()
|
||||||
|
|
||||||
|
# Calibration triggers (buttons)
|
||||||
|
def ui_calibrate_through(self, sweep_number: int) -> Dict[str, object]:
|
||||||
|
cfg = self.get_current_config()
|
||||||
|
if cfg.mode != VNAMode.S21:
|
||||||
|
raise RuntimeError("THROUGH is only valid in S21 mode")
|
||||||
|
self.record_calibration_from_sweep(CalibrationStandard.THROUGH, sweep_number)
|
||||||
|
return self.summary()
|
||||||
|
|
||||||
|
def ui_calibrate_open(self, sweep_number: int) -> Dict[str, object]:
|
||||||
|
cfg = self.get_current_config()
|
||||||
|
if cfg.mode != VNAMode.S11:
|
||||||
|
raise RuntimeError("OPEN is only valid in S11 mode")
|
||||||
|
self.record_calibration_from_sweep(CalibrationStandard.OPEN, sweep_number)
|
||||||
|
return self.summary()
|
||||||
|
|
||||||
|
def ui_calibrate_short(self, sweep_number: int) -> Dict[str, object]:
|
||||||
|
cfg = self.get_current_config()
|
||||||
|
if cfg.mode != VNAMode.S11:
|
||||||
|
raise RuntimeError("SHORT is only valid in S11 mode")
|
||||||
|
self.record_calibration_from_sweep(CalibrationStandard.SHORT, sweep_number)
|
||||||
|
return self.summary()
|
||||||
|
|
||||||
|
def ui_calibrate_load(self, sweep_number: int) -> Dict[str, object]:
|
||||||
|
cfg = self.get_current_config()
|
||||||
|
if cfg.mode != VNAMode.S11:
|
||||||
|
raise RuntimeError("LOAD is only valid in S11 mode")
|
||||||
|
self.record_calibration_from_sweep(CalibrationStandard.LOAD, sweep_number)
|
||||||
|
return self.summary()
|
||||||
@ -7,9 +7,11 @@ scattered throughout the codebase.
|
|||||||
"""
|
"""
|
||||||
from vna_system.core.acquisition.data_acquisition import VNADataAcquisition
|
from vna_system.core.acquisition.data_acquisition import VNADataAcquisition
|
||||||
from vna_system.core.processing.sweep_processor import SweepProcessingManager
|
from vna_system.core.processing.sweep_processor import SweepProcessingManager
|
||||||
from vna_system.api.websockets.websocket_handler import WebSocketManager
|
from vna_system.core.processing.websocket_handler import WebSocketManager
|
||||||
|
from vna_system.core.settings.settings_manager import VNASettingsManager
|
||||||
|
|
||||||
# Global singleton instances
|
# Global singleton instances
|
||||||
vna_data_acquisition_instance: VNADataAcquisition = VNADataAcquisition()
|
vna_data_acquisition_instance: VNADataAcquisition = VNADataAcquisition()
|
||||||
processing_manager: SweepProcessingManager = SweepProcessingManager()
|
processing_manager: SweepProcessingManager = SweepProcessingManager()
|
||||||
websocket_manager: WebSocketManager = WebSocketManager(processing_manager)
|
websocket_manager: WebSocketManager = WebSocketManager(processing_manager)
|
||||||
|
settings_manager: VNASettingsManager = VNASettingsManager()
|
||||||
@ -1 +0,0 @@
|
|||||||
"""VNA System scripts module."""
|
|
||||||
Reference in New Issue
Block a user