some features added

This commit is contained in:
ayzen
2025-10-16 19:25:55 +03:00
parent 99200aad83
commit 6c363433d6
18 changed files with 4525 additions and 31 deletions

File diff suppressed because it is too large Load Diff

View File

@ -12,8 +12,15 @@ import serial
from vna_system.core import config as cfg from vna_system.core import config as cfg
from vna_system.core.acquisition.port_manager import VNAPortLocator from vna_system.core.acquisition.port_manager import VNAPortLocator
from vna_system.core.acquisition.sweep_buffer import SweepBuffer from vna_system.core.acquisition.sweep_buffer import SweepBuffer
from vna_system.core.acquisition.sound_player import SoundPlayer
from vna_system.core.logging.logger import get_component_logger from vna_system.core.logging.logger import get_component_logger
# Import simulator if enabled
if cfg.USE_SIMULATOR:
from vna_system.core.acquisition.simulator import VNASimulator
else:
VNASimulator = None # type: ignore
logger = get_component_logger(__file__) logger = get_component_logger(__file__)
@ -30,6 +37,16 @@ class VNADataAcquisition:
self.vna_port_locator = VNAPortLocator() self.vna_port_locator = VNAPortLocator()
self._sweep_buffer = SweepBuffer() self._sweep_buffer = SweepBuffer()
# Sound player for sweep notifications
sound_file = Path(cfg.BASE_DIR) / "core" / "sound_sample.mp3"
self._sound_player = SoundPlayer(sound_file)
# Simulator mode
self._simulator = None
if cfg.USE_SIMULATOR:
self._simulator = VNASimulator()
logger.info("VNA Simulator mode enabled")
# Control flags # Control flags
self._running: bool = False self._running: bool = False
self._thread: threading.Thread | None = None self._thread: threading.Thread | None = None
@ -45,7 +62,7 @@ class VNADataAcquisition:
self._collected_rx_payloads: list[bytes] = [] self._collected_rx_payloads: list[bytes] = []
self._meas_cmds_in_sweep: int = 0 self._meas_cmds_in_sweep: int = 0
logger.debug("VNADataAcquisition initialized", baud=self.baud) logger.debug("VNADataAcquisition initialized", baud=self.baud, simulator_mode=cfg.USE_SIMULATOR)
def _get_current_bin_path(self) -> Path | None: def _get_current_bin_path(self) -> Path | None:
"""Get the path to the current binary input file from JSON config.""" """Get the path to the current binary input file from JSON config."""
@ -175,8 +192,55 @@ class VNADataAcquisition:
# --------------------------------------------------------------------- # # --------------------------------------------------------------------- #
# Acquisition loop # Acquisition loop
# --------------------------------------------------------------------- # # --------------------------------------------------------------------- #
def _simulator_acquisition_loop(self) -> None:
"""Simplified acquisition loop for simulator mode."""
logger.info("Starting simulator acquisition loop")
while self._running and not self._stop_event.is_set():
try:
# Honor pause
if self._paused:
time.sleep(0.1)
continue
# Get simulated sweep with noise
points, timestamp = self._simulator.get_sweep()
# Add sweep to buffer
sweep_number = self._sweep_buffer.add_sweep(points, timestamp=timestamp)
logger.info(
"Simulated sweep collected",
sweep_number=sweep_number,
points=len(points),
timestamp=timestamp
)
# Play sweep notification sound
self._sound_player.play()
# Handle single-sweep mode transitions
if not self._continuous_mode:
if self._single_sweep_requested:
self._single_sweep_requested = False
logger.info("Single simulated sweep completed; pausing acquisition")
self.pause()
else:
self.pause()
else:
# In continuous mode, add a small delay between sweeps
time.sleep(0.1)
except Exception as exc: # noqa: BLE001
logger.error("Simulator acquisition loop error", error=repr(exc))
time.sleep(1.0)
def _acquisition_loop(self) -> None: def _acquisition_loop(self) -> None:
"""Main acquisition loop executed by the background thread.""" """Main acquisition loop executed by the background thread."""
# Use simulator loop if simulator is enabled
if self._simulator is not None:
self._simulator_acquisition_loop()
return
while self._running and not self._stop_event.is_set(): while self._running and not self._stop_event.is_set():
try: try:
# Honor pause # Honor pause
@ -305,6 +369,9 @@ class VNADataAcquisition:
expected=cfg.EXPECTED_POINTS_PER_SWEEP, expected=cfg.EXPECTED_POINTS_PER_SWEEP,
actual=len(all_points), actual=len(all_points),
) )
# Play sweep notification sound
self._sound_player.play()
else: else:
logger.warning("No points parsed for sweep") logger.warning("No points parsed for sweep")

View File

@ -0,0 +1,150 @@
"""
VNA Simulator Module
This module provides a simulator mode that loads sweep data from a JSON file
and adds configurable Gaussian noise to simulate real device behavior.
"""
import json
import logging
import time
from pathlib import Path
from typing import List, Tuple, Optional
import numpy as np
from vna_system.core import config
logger = logging.getLogger(__name__)
class VNASimulator:
"""
Simulator for VNA device that loads sweep data from file and adds noise.
This simulator reads a reference sweep from a JSON file and adds Gaussian
noise to the real and imaginary components to simulate measurement variations.
"""
def __init__(
self,
sweep_file: Optional[Path] = None,
noise_level: Optional[float] = None
):
"""
Initialize the VNA simulator.
Args:
sweep_file: Path to JSON file containing sweep data.
If None, uses config.SIMULATOR_SWEEP_FILE
noise_level: Standard deviation of Gaussian noise to add.
If None, uses config.SIMULATOR_NOISE_LEVEL
"""
self.sweep_file = sweep_file or config.SIMULATOR_SWEEP_FILE
self.noise_level = noise_level if noise_level is not None else config.SIMULATOR_NOISE_LEVEL
self._reference_points: List[Tuple[float, float]] = []
self._sweep_counter = 0
self._load_reference_sweep()
logger.info(
f"VNA Simulator initialized with file: {self.sweep_file}, "
f"noise level: {self.noise_level}"
)
def _load_reference_sweep(self) -> None:
"""Load reference sweep data from JSON file."""
try:
with open(self.sweep_file, 'r') as f:
data = json.load(f)
# Extract points from JSON structure
# Expected format: {"sweep_number": N, "timestamp": T, "points": [[real, imag], ...]}
if 'points' not in data:
raise ValueError(f"JSON file missing 'points' field: {self.sweep_file}")
self._reference_points = [
(float(point[0]), float(point[1]))
for point in data['points']
]
logger.info(
f"Loaded {len(self._reference_points)} points from reference sweep "
f"(sweep_number: {data.get('sweep_number', 'N/A')})"
)
except FileNotFoundError:
logger.error(f"Simulator sweep file not found: {self.sweep_file}")
raise
except json.JSONDecodeError as e:
logger.error(f"Failed to parse JSON file {self.sweep_file}: {e}")
raise
except Exception as e:
logger.error(f"Failed to load reference sweep: {e}")
raise
def get_sweep(self) -> Tuple[List[Tuple[float, float]], float]:
"""
Generate a simulated sweep with added noise.
Returns:
Tuple of (points, timestamp) where:
- points: List of (real, imag) tuples with added noise
- timestamp: Current Unix timestamp
"""
time.sleep(1)
timestamp = time.time()
self._sweep_counter += 1
# Add Gaussian noise to each point
noisy_points = []
for real, imag in self._reference_points:
# Add independent noise to real and imaginary parts
noise_real = np.random.normal(0, self.noise_level)
noise_imag = np.random.normal(0, self.noise_level)
noisy_real = real + noise_real
noisy_imag = imag + noise_imag
noisy_points.append((noisy_real, noisy_imag))
# Log statistics about the noise added
if len(noisy_points) > 0:
first_orig = self._reference_points[0]
first_noisy = noisy_points[0]
logger.debug(
f"Simulated sweep #{self._sweep_counter}: "
f"noise_level={self.noise_level:.2e}, "
f"first_point: orig=({first_orig[0]:.6e}, {first_orig[1]:.6e}), "
f"noisy=({first_noisy[0]:.6e}, {first_noisy[1]:.6e})"
)
logger.debug(
f"Generated simulated sweep #{self._sweep_counter} with "
f"{len(noisy_points)} points at timestamp {timestamp:.6f}"
)
return noisy_points, timestamp
def get_point_count(self) -> int:
"""
Get the number of points in the reference sweep.
Returns:
Number of points in each simulated sweep
"""
return len(self._reference_points)
def reset_counter(self) -> None:
"""Reset the internal sweep counter."""
self._sweep_counter = 0
logger.debug("Simulator sweep counter reset")
def create_simulator() -> VNASimulator:
"""
Factory function to create a VNA simulator instance.
Uses configuration from config module.
Returns:
Configured VNASimulator instance
"""
return VNASimulator()

View File

@ -0,0 +1,101 @@
"""
Cross-platform sound player for sweep notifications.
Supports Windows and Linux platforms.
"""
import platform
import threading
from pathlib import Path
from vna_system.core.logging.logger import get_component_logger
logger = get_component_logger(__file__)
class SoundPlayer:
"""Cross-platform sound player using system-specific APIs."""
def __init__(self, sound_file: Path):
"""
Initialize sound player with a sound file.
Args:
sound_file: Path to the sound file (mp3, wav, etc.)
"""
self.sound_file = sound_file
self.system = platform.system()
self._player = None
# Validate sound file exists
if not sound_file.exists():
logger.warning("Sound file not found", path=str(sound_file))
return
# Initialize platform-specific player
if self.system == "Windows":
self._init_windows_player()
elif self.system == "Linux":
self._init_linux_player()
else:
logger.warning("Unsupported platform for sound playback", platform=self.system)
def _init_windows_player(self):
"""Initialize Windows-specific sound player using winsound."""
try:
import winsound
self._player = lambda: winsound.PlaySound(
str(self.sound_file),
winsound.SND_FILENAME | winsound.SND_ASYNC | winsound.SND_NODEFAULT
)
logger.debug("Windows sound player initialized")
except ImportError:
logger.warning("winsound not available on Windows")
self._player = None
def _init_linux_player(self):
"""Initialize Linux-specific sound player using subprocess."""
import subprocess
import shutil
# Try to find available audio players
players = ["paplay", "aplay", "mpg123", "ffplay"]
available_player = None
for player in players:
if shutil.which(player):
available_player = player
break
if available_player:
def linux_play():
try:
# Run player silently in background
subprocess.Popen(
[available_player, str(self.sound_file)],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
except Exception as exc:
logger.error("Failed to play sound on Linux", error=repr(exc))
self._player = linux_play
logger.debug("Linux sound player initialized", player=available_player)
else:
logger.warning("No audio player found on Linux", tried=players)
self._player = None
def play(self):
"""Play the sound asynchronously (non-blocking)."""
if self._player is None:
return
# Play in a background thread to avoid blocking
thread = threading.Thread(target=self._player, daemon=True, name="SoundPlayer")
thread.start()
def play_sync(self):
"""Play the sound synchronously (blocking)."""
if self._player is None:
return
self._player()

View File

@ -65,7 +65,7 @@ class SweepBuffer:
# ------------------------------ # ------------------------------
# Core API # Core API
# ------------------------------ # ------------------------------
def add_sweep(self, points: list[Point]) -> int: def add_sweep(self, points: list[Point], timestamp: float | None = None) -> int:
""" """
Add a new sweep to the buffer. Add a new sweep to the buffer.
@ -73,12 +73,15 @@ class SweepBuffer:
---------- ----------
points: points:
Sequence of (real, imag) tuples representing a sweep. Sequence of (real, imag) tuples representing a sweep.
timestamp:
Optional UNIX timestamp. If None, current time is used.
Returns Returns
------- -------
int int
The assigned sweep number for the newly added sweep. The assigned sweep number for the newly added sweep.
""" """
if timestamp is None:
timestamp = time.time() timestamp = time.time()
with self._lock: with self._lock:
self._sweep_counter += 1 self._sweep_counter += 1

View File

@ -32,6 +32,13 @@ TX_CHUNK_SIZE = 64 * 1024
VNA_VID = 0x0483 # STMicroelectronics VNA_VID = 0x0483 # STMicroelectronics
VNA_PID = 0x5740 # STM32 Virtual ComPort VNA_PID = 0x5740 # STM32 Virtual ComPort
# -----------------------------------------------------------------------------
# Simulator mode settings
# -----------------------------------------------------------------------------
USE_SIMULATOR = True # Set to True to use simulator instead of real device
SIMULATOR_SWEEP_FILE = BASE_DIR / "binary_input" / "sweep_example" / "example.json"
SIMULATOR_NOISE_LEVEL = 100 # Standard deviation of Gaussian noise to add to real and imaginary parts
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Sweep detection and parsing constants # Sweep detection and parsing constants
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------

View File

@ -299,7 +299,7 @@ class BaseProcessor:
# --------------------------------------------------------------------- # # --------------------------------------------------------------------- #
# Data path: accept new sweep, recompute, produce result # Data path: accept new sweep, recompute, produce result
# --------------------------------------------------------------------- # # --------------------------------------------------------------------- #
def add_sweep_data(self, sweep_data: Any, calibrated_data: Any, vna_config: ConfigPreset | None, reference_data: Any = None): def add_sweep_data(self, sweep_data: Any, calibrated_data: Any, vna_config: ConfigPreset | None, reference_data: Any = None, reference_info: Any = None):
""" """
Add the latest sweep to the in-memory history and trigger recalculation. Add the latest sweep to the in-memory history and trigger recalculation.
@ -313,6 +313,8 @@ class BaseProcessor:
Snapshot of VNA settings (dataclass or pydantic model supported). Snapshot of VNA settings (dataclass or pydantic model supported).
reference_data: reference_data:
Open air reference sweep data for background subtraction/normalization. Open air reference sweep data for background subtraction/normalization.
reference_info:
ReferenceInfo object with metadata (name, description, etc.) about the reference.
Returns Returns
------- -------
@ -326,6 +328,7 @@ class BaseProcessor:
"calibrated_data": calibrated_data, "calibrated_data": calibrated_data,
"vna_config": self._snapshot_vna_config(vna_config), "vna_config": self._snapshot_vna_config(vna_config),
"reference_data": reference_data, "reference_data": reference_data,
"reference_info": reference_info,
"timestamp": datetime.now().timestamp(), "timestamp": datetime.now().timestamp(),
} }
) )
@ -614,7 +617,6 @@ class BaseProcessor:
reference_data = entry.get("reference_data") reference_data = entry.get("reference_data")
exported.append({ exported.append({
"sweep_number": sweep_data.sweep_number if sweep_data else None,
"timestamp": float(entry.get("timestamp")) if entry.get("timestamp") is not None else None, "timestamp": float(entry.get("timestamp")) if entry.get("timestamp") is not None else None,
"sweep_points": self._points_to_list(getattr(sweep_data, "points", [])), "sweep_points": self._points_to_list(getattr(sweep_data, "points", [])),
"calibrated_points": self._points_to_list(getattr(calibrated_data, "points", [])), "calibrated_points": self._points_to_list(getattr(calibrated_data, "points", [])),
@ -639,28 +641,29 @@ class BaseProcessor:
with self._lock: with self._lock:
self._sweep_history.clear() self._sweep_history.clear()
for entry in history_data: for idx, entry in enumerate(history_data):
sweep_points = entry.get("sweep_points", []) sweep_points = entry.get("sweep_points", [])
calibrated_points = entry.get("calibrated_points", []) calibrated_points = entry.get("calibrated_points", [])
reference_points = entry.get("reference_points", []) reference_points = entry.get("reference_points", [])
# Reconstruct SweepData objects # Reconstruct SweepData objects
# Use sequential index as sweep_number since it's not stored
sweep_data = SweepData( sweep_data = SweepData(
sweep_number=entry.get("sweep_number", 0), sweep_number=idx,
timestamp=entry.get("timestamp", 0.0), timestamp=entry.get("timestamp", 0.0),
points=sweep_points, points=sweep_points,
total_points=len(sweep_points) total_points=len(sweep_points)
) if sweep_points else None ) if sweep_points else None
calibrated_data = SweepData( calibrated_data = SweepData(
sweep_number=entry.get("sweep_number", 0), sweep_number=idx,
timestamp=entry.get("timestamp", 0.0), timestamp=entry.get("timestamp", 0.0),
points=calibrated_points, points=calibrated_points,
total_points=len(calibrated_points) total_points=len(calibrated_points)
) if calibrated_points else None ) if calibrated_points else None
reference_data = SweepData( reference_data = SweepData(
sweep_number=entry.get("sweep_number", 0), sweep_number=idx,
timestamp=entry.get("timestamp", 0.0), timestamp=entry.get("timestamp", 0.0),
points=reference_points, points=reference_points,
total_points=len(reference_points) total_points=len(reference_points)
@ -731,16 +734,33 @@ class BaseProcessor:
def get_full_state(self) -> dict[str, Any]: def get_full_state(self) -> dict[str, Any]:
""" """
Return complete processor state including sweep history. Return complete processor state including sweep history and active reference.
This should be called explicitly when needed (e.g., for export/download), This should be called explicitly when needed (e.g., for export/download),
not on every broadcast. not on every broadcast.
""" """
with self._lock: with self._lock:
# Get active reference info from the latest history entry
active_reference = None
if self._sweep_history:
latest_entry = self._sweep_history[-1]
reference_info = latest_entry.get("reference_info")
reference_data = latest_entry.get("reference_data")
if reference_info is not None:
# Export full ReferenceInfo metadata using its to_dict() method
active_reference = reference_info.to_dict()
# Also include the sweep data points
if reference_data is not None:
active_reference["points"] = self._points_to_list(getattr(reference_data, "points", []))
active_reference["total_points"] = getattr(reference_data, "total_points", 0)
active_reference["sweep_timestamp"] = getattr(reference_data, "timestamp", None)
return { return {
"processor_id": self.processor_id, "processor_id": self.processor_id,
"config": self._config.copy(), "config": self._config.copy(),
"history_count": len(self._sweep_history), "history_count": len(self._sweep_history),
"max_history": self._max_history, "max_history": self._max_history,
"sweep_history": self.export_history_data(), "sweep_history": self.export_history_data(),
"active_reference": active_reference,
} }

View File

@ -1,6 +1,6 @@
{ {
"open_air": false, "open_air": true,
"axis": "phase", "axis": "abs",
"cut": 0.266, "cut": 0.266,
"max": 2.3, "max": 2.3,
"gain": 0.3, "gain": 0.3,

View File

@ -1,7 +1,7 @@
{ {
"y_min": -80, "y_min": -45,
"y_max": 40, "y_max": 40,
"autoscale": true, "autoscale": true,
"show_magnitude": true, "show_magnitude": true,
"show_phase": true "show_phase": false
} }

View File

@ -138,12 +138,11 @@ class BScanProcessor(BaseProcessor):
super().update_config(updates) super().update_config(updates)
def _clear_plot_history(self) -> None: def _clear_plot_history(self) -> None:
"""Clear the accumulated plot history.""" """Clear the accumulated plot and sweep history completely."""
with self._lock: with self._lock:
latest = self._sweep_history[-1]
self._sweep_history.clear() self._sweep_history.clear()
self._sweep_history.append(latest) self._plot_history.clear()
logger.info("Plot history cleared", processor_id=self.processor_id) logger.info("Plot and sweep history cleared completely", processor_id=self.processor_id)
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
# Processing # Processing
@ -206,7 +205,6 @@ class BScanProcessor(BaseProcessor):
plot_record = { plot_record = {
"time_domain_data": analysis["time_data"].tolist(), "time_domain_data": analysis["time_data"].tolist(),
"distance_data": analysis["distance"].tolist(), "distance_data": analysis["distance"].tolist(),
"sweep_number": sweep_data.sweep_number,
"timestamp": sweep_data.timestamp, "timestamp": sweep_data.timestamp,
"frequency_range": analysis["freq_range"], "frequency_range": analysis["freq_range"],
} }
@ -220,7 +218,8 @@ class BScanProcessor(BaseProcessor):
with self._lock: with self._lock:
all_time_domain = [record["time_domain_data"] for record in self._plot_history] all_time_domain = [record["time_domain_data"] for record in self._plot_history]
all_distance = [record["distance_data"] for record in self._plot_history] all_distance = [record["distance_data"] for record in self._plot_history]
all_sweep_numbers = [record["sweep_number"] for record in self._plot_history] # Generate sequential sweep numbers starting from 1
all_sweep_numbers = list(range(1, len(self._plot_history) + 1))
all_timestamps = [record["timestamp"] for record in self._plot_history] all_timestamps = [record["timestamp"] for record in self._plot_history]
return { return {
@ -292,13 +291,12 @@ class BScanProcessor(BaseProcessor):
y_coords: list[float] = [] y_coords: list[float] = []
z_values: list[float] = [] z_values: list[float] = []
for item in history: for sweep_index, item in enumerate(history, start=1):
sweep_num = item["sweep_number"]
depths = item["distance_data"] depths = item["distance_data"]
amps = item["time_domain_data"] amps = item["time_domain_data"]
for d, a in zip(depths, amps, strict=False): for d, a in zip(depths, amps, strict=False):
x_coords.append(sweep_num) x_coords.append(sweep_index)
y_coords.append(d) y_coords.append(d)
z_values.append(a) z_values.append(a)
@ -368,8 +366,32 @@ class BScanProcessor(BaseProcessor):
""" """
with self._lock: with self._lock:
if not self._sweep_history: if not self._sweep_history:
logger.debug("Recalculate skipped; sweep history empty") logger.debug("Recalculate with empty history; returning empty result")
return None # Return empty result with proper structure
empty_data = {
"time_domain_data": [],
"distance_data": [],
"frequency_range": [0.0, 0.0],
"reference_used": False,
"axis_type": self._config["axis"],
"points_processed": 0,
"plot_history_count": 0,
"all_time_domain_data": [],
"all_distance_data": [],
"all_sweep_numbers": [],
"all_timestamps": [],
}
plotly_conf = self.generate_plotly_config(empty_data, {})
ui_params = self.get_ui_parameters()
return ProcessedResult(
processor_id=self.processor_id,
timestamp=datetime.now().timestamp(),
data=empty_data,
plotly_config=plotly_conf,
ui_parameters=ui_params,
metadata=self._get_metadata(),
)
# Clear existing plot history to rebuild from scratch # Clear existing plot history to rebuild from scratch
self._plot_history.clear() self._plot_history.clear()
@ -657,3 +679,22 @@ class BScanProcessor(BaseProcessor):
except Exception as exc: # noqa: BLE001 except Exception as exc: # noqa: BLE001
logger.error("Depth processing failed", error=repr(exc)) logger.error("Depth processing failed", error=repr(exc))
return depth_m, response return depth_m, response
# -------------------------------------------------------------------------
# State export override
# -------------------------------------------------------------------------
def get_full_state(self) -> dict[str, Any]:
"""
Return complete processor state including BScan-specific data.
Extends base implementation with plot history count.
"""
# Get base state (includes active_reference)
state = super().get_full_state()
# Add BScan-specific information
with self._lock:
state["plot_history_count"] = len(self._plot_history)
return state

View File

@ -59,6 +59,20 @@ class MagnitudeProcessor(BaseProcessor):
logger.warning("Calibrated sweep contains zero points") logger.warning("Calibrated sweep contains zero points")
return {"error": "Empty calibrated sweep"} return {"error": "Empty calibrated sweep"}
# Apply open air reference subtraction if enabled
reference_data = None
if self._config["open_air"] and self._sweep_history:
latest_history = self._sweep_history[-1]
reference_data = latest_history.get("reference_data")
if reference_data is None:
logger.warning("Open air subtraction cannot be done: reference_data is None")
self._config["open_air"] = False
if self._config["open_air"] and reference_data is not None:
reference_points = getattr(reference_data, "points", None)
if reference_points:
points = self._subtract_reference(points, reference_points)
# Frequency axis from VNA config (defaults if not provided) # Frequency axis from VNA config (defaults if not provided)
start_freq = float(vna_config.get("start_freq", 100e6)) start_freq = float(vna_config.get("start_freq", 100e6))
stop_freq = float(vna_config.get("stop_freq", 8.8e9)) stop_freq = float(vna_config.get("stop_freq", 8.8e9))
@ -228,6 +242,12 @@ class MagnitudeProcessor(BaseProcessor):
type="toggle", type="toggle",
value=self._config.get("show_phase", False), value=self._config.get("show_phase", False),
), ),
UIParameter(
name="open_air",
label="Вычесть Open Air",
type="toggle",
value=self._config.get("open_air", False),
),
UIParameter( UIParameter(
name="autoscale", name="autoscale",
label="Автомасштаб оси Y", label="Автомасштаб оси Y",
@ -258,6 +278,7 @@ class MagnitudeProcessor(BaseProcessor):
"autoscale": False, "autoscale": False,
"show_magnitude": True, "show_magnitude": True,
"show_phase": False, "show_phase": False,
"open_air": False,
} }
def update_config(self, updates: dict[str, Any]) -> None: def update_config(self, updates: dict[str, Any]) -> None:
@ -287,3 +308,35 @@ class MagnitudeProcessor(BaseProcessor):
logger.info("Adjusted y_max to maintain y_max > y_min", logger.info("Adjusted y_max to maintain y_max > y_min",
y_min=self._config["y_min"], y_min=self._config["y_min"],
y_max=self._config["y_max"]) y_max=self._config["y_max"])
def _subtract_reference(
self,
signal: list[tuple[float, float]],
reference: list[tuple[float, float]]
) -> list[tuple[float, float]]:
"""
Subtract reference from signal (complex subtraction).
Args:
signal: List of (real, imag) tuples
reference: List of (real, imag) tuples
Returns:
List of (real, imag) tuples after subtraction
"""
try:
n = min(len(signal), len(reference))
if n == 0:
return signal
result = []
for i in range(n):
sig_real, sig_imag = signal[i]
ref_real, ref_imag = reference[i]
result.append((sig_real - ref_real, sig_imag - ref_imag))
logger.debug("Reference subtraction completed", points=n)
return result
except Exception as exc:
logger.error("Reference subtraction failed", error=repr(exc))
return signal

View File

@ -100,7 +100,7 @@ class ProcessorManager:
# --------------------------------------------------------------------- # # --------------------------------------------------------------------- #
# Main processing actions # Main processing actions
# --------------------------------------------------------------------- # # --------------------------------------------------------------------- #
def process_sweep(self, sweep_data: SweepData, calibrated_data: Any, vna_config: ConfigPreset | None, reference_data: Any = None) -> dict[str, ProcessedResult]: def process_sweep(self, sweep_data: SweepData, calibrated_data: Any, vna_config: ConfigPreset | None, reference_data: Any = None, reference_info: Any = None) -> dict[str, ProcessedResult]:
""" """
Feed a sweep into all processors and dispatch results to callbacks. Feed a sweep into all processors and dispatch results to callbacks.
@ -114,7 +114,7 @@ class ProcessorManager:
for processor_id, processor in processors_items: for processor_id, processor in processors_items:
try: try:
result = processor.add_sweep_data(sweep_data, calibrated_data, vna_config, reference_data) result = processor.add_sweep_data(sweep_data, calibrated_data, vna_config, reference_data, reference_info)
if result: if result:
results[processor_id] = result results[processor_id] = result
for cb in callbacks: for cb in callbacks:
@ -280,7 +280,8 @@ class ProcessorManager:
calibrated = self._apply_calibration(latest) calibrated = self._apply_calibration(latest)
vna_cfg = self.settings_manager.get_current_preset() vna_cfg = self.settings_manager.get_current_preset()
reference_data = self._apply_calibration(self.settings_manager.get_current_reference_sweep(vna_cfg)) reference_data = self._apply_calibration(self.settings_manager.get_current_reference_sweep(vna_cfg))
self.process_sweep(latest, calibrated, vna_cfg, reference_data) reference_info = self.settings_manager.get_current_reference(vna_cfg)
self.process_sweep(latest, calibrated, vna_cfg, reference_data, reference_info)
self._last_processed_sweep = latest.sweep_number self._last_processed_sweep = latest.sweep_number
# Light-duty polling to reduce wakeups # Light-duty polling to reduce wakeups

View File

@ -287,12 +287,26 @@ class VNASettingsManager:
if not current_preset: if not current_preset:
raise ValueError("No current preset available") raise ValueError("No current preset available")
# Get current calibration info to store in metadata
current_calibration = self.get_current_calibration()
calibration_info = None
if current_calibration:
calibration_info = {
"calibration_name": current_calibration.name,
"preset_filename": current_calibration.preset.filename
}
# Merge calibration info into metadata
merged_metadata = metadata or {}
if calibration_info:
merged_metadata["calibration"] = calibration_info
reference_info = self.reference_manager.create_reference( reference_info = self.reference_manager.create_reference(
name=reference_name, name=reference_name,
preset=current_preset, preset=current_preset,
sweep_data=latest, sweep_data=latest,
description=description, description=description,
metadata=metadata or {} metadata=merged_metadata
) )
logger.info( logger.info(

Binary file not shown.

View File

@ -380,7 +380,8 @@ export class ChartManager {
plotly_config: safeClone(current_data.plotly_config), plotly_config: safeClone(current_data.plotly_config),
timestamp: current_data.timestamp timestamp: current_data.timestamp
} : null, } : null,
sweep_history: state.sweep_history || [] sweep_history: state.sweep_history || [],
active_reference: state.active_reference || null
}; };
} }

View File

@ -98,6 +98,7 @@ export class SettingsManager {
currentReferenceName: document.getElementById('currentReferenceName'), currentReferenceName: document.getElementById('currentReferenceName'),
currentReferenceTimestamp: document.getElementById('currentReferenceTimestamp'), currentReferenceTimestamp: document.getElementById('currentReferenceTimestamp'),
currentReferenceDescription: document.getElementById('currentReferenceDescription'), currentReferenceDescription: document.getElementById('currentReferenceDescription'),
currentReferenceCalibration: document.getElementById('currentReferenceCalibration'),
// Status // Status
presetCount: document.getElementById('presetCount'), presetCount: document.getElementById('presetCount'),

View File

@ -203,6 +203,18 @@ export class ReferenceManager {
this.elements.currentReferenceDescription.textContent = reference.description || '—'; this.elements.currentReferenceDescription.textContent = reference.description || '—';
} }
// Show calibration info if available
if (this.elements.currentReferenceCalibration) {
const calibrationInfo = reference.metadata?.calibration;
if (calibrationInfo && calibrationInfo.calibration_name) {
this.elements.currentReferenceCalibration.textContent = calibrationInfo.calibration_name;
this.elements.currentReferenceCalibration.parentElement.style.display = 'flex';
} else {
this.elements.currentReferenceCalibration.textContent = '—';
this.elements.currentReferenceCalibration.parentElement.style.display = 'none';
}
}
this.onReferenceUpdated?.(reference); this.onReferenceUpdated?.(reference);
this.updateButtons(); this.updateButtons();
} }
@ -239,8 +251,20 @@ export class ReferenceManager {
); );
} }
handleReferenceChange() { async handleReferenceChange() {
this.updateButtons(); this.updateButtons();
// Show info for selected reference immediately
const selectedName = this.elements.referenceDropdown?.value;
if (selectedName) {
const selectedRef = this.availableReferences.find(r => r.name === selectedName);
if (selectedRef) {
this.updateInfo(selectedRef);
}
} else {
// If no selection, show current reference info
this.updateInfo(this.currentReference);
}
} }
async handlePreviewReference() { async handlePreviewReference() {

View File

@ -316,6 +316,10 @@
<span class="reference-details__label">Описание:</span> <span class="reference-details__label">Описание:</span>
<span class="reference-details__value" id="currentReferenceDescription"></span> <span class="reference-details__value" id="currentReferenceDescription"></span>
</div> </div>
<div class="reference-details__row" style="display: none;">
<span class="reference-details__label">Калибровка:</span>
<span class="reference-details__value" id="currentReferenceCalibration"></span>
</div>
</div> </div>
</div> </div>
</div> </div>