removed symlinks to support windows

This commit is contained in:
Ayzen
2025-09-30 20:15:35 +03:00
parent 0ef860111e
commit c16cf2ba8a
11 changed files with 149 additions and 101 deletions

5
.gitignore vendored
View File

@ -228,3 +228,8 @@ test*
processing_results* processing_results*
plots* plots*
# Runtime state files - managed by application
vna_system/binary_input/current_input.json
vna_system/calibration/current_calibration.json
vna_system/references/current_reference.json

View File

@ -1 +0,0 @@
config_inputs/s11_start100_stop8800_points1000_bw1khz.bin

View File

@ -1 +0,0 @@
s11_start100_stop8800_points1000_bw1khz/вфыввф

View File

@ -1,8 +1,10 @@
import io import io
import json
import os import os
import struct import struct
import threading import threading
import time import time
from pathlib import Path
from typing import BinaryIO from typing import BinaryIO
import serial import serial
@ -20,7 +22,8 @@ class VNADataAcquisition:
def __init__(self) -> None: def __init__(self) -> None:
# Configuration # Configuration
self.bin_log_path: str = cfg.BIN_INPUT_FILE_PATH self.current_input_json = Path(cfg.BASE_DIR) / "binary_input" / "current_input.json"
self.config_inputs_dir = Path(cfg.BASE_DIR) / "binary_input" / "config_inputs"
self.baud: int = cfg.DEFAULT_BAUD_RATE self.baud: int = cfg.DEFAULT_BAUD_RATE
# Dependencies # Dependencies
@ -42,7 +45,32 @@ 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, bin_log_path=self.bin_log_path) logger.debug("VNADataAcquisition initialized", baud=self.baud)
def _get_current_bin_path(self) -> Path | None:
"""Get the path to the current binary input file from JSON config."""
if not self.current_input_json.exists():
logger.warning("Current input JSON not found", path=str(self.current_input_json))
return None
try:
with self.current_input_json.open("r", encoding="utf-8") as f:
current_info = json.load(f)
filename = current_info.get("filename")
if not filename:
logger.warning("Invalid current input JSON format")
return None
bin_path = self.config_inputs_dir / filename
if not bin_path.exists():
logger.warning("Current input binary file not found", path=str(bin_path))
return None
return bin_path
except Exception as exc: # noqa: BLE001
logger.error("Failed to load current input config", error=repr(exc))
return None
# --------------------------------------------------------------------- # # --------------------------------------------------------------------- #
# Lifecycle # Lifecycle
@ -165,12 +193,19 @@ class VNADataAcquisition:
logger.debug("Using port", device=port, baud=self.baud) logger.debug("Using port", device=port, baud=self.baud)
# Get current binary input file path
bin_path = self._get_current_bin_path()
if not bin_path:
logger.error("No current binary input configured, waiting...")
time.sleep(1.0)
continue
# Open serial + process one sweep from the binary log # Open serial + process one sweep from the binary log
with serial.Serial(port, self.baud) as ser: with serial.Serial(port, self.baud) as ser:
self._drain_serial_input(ser) self._drain_serial_input(ser)
# Open the log file each iteration to read the next sweep from start # Open the log file each iteration to read the next sweep from start
with open(self.bin_log_path, "rb") as raw: with open(bin_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._process_sweep_data(buffered, ser) self._process_sweep_data(buffered, ser)

View File

@ -47,10 +47,6 @@ EXPECTED_POINTS_PER_SWEEP = 1000
SWEEP_BUFFER_MAX_SIZE = 100 # Maximum number of sweeps to store in circular buffer SWEEP_BUFFER_MAX_SIZE = 100 # Maximum number of sweeps to store in circular buffer
SERIAL_BUFFER_SIZE = 512 * 1024 SERIAL_BUFFER_SIZE = 512 * 1024
# -----------------------------------------------------------------------------
# Log file settings (binary input path, not to be confused with text logs)
# -----------------------------------------------------------------------------
BIN_INPUT_FILE_PATH = "./vna_system/binary_input/current_input.bin" # Symlink to current binary input
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Binary log format constants # Binary log format constants

View File

@ -1,5 +1,5 @@
{ {
"open_air": false, "open_air": true,
"axis": "real", "axis": "real",
"data_limitation": "ph_only_1", "data_limitation": "ph_only_1",
"cut": 0.816, "cut": 0.816,

View File

@ -1,5 +1,5 @@
{ {
"y_min": -80, "y_min": -80,
"y_max": 40, "y_max": 40,
"show_phase": false "show_phase": true
} }

View File

@ -86,7 +86,7 @@ class CalibrationManager:
Layout Layout
------ ------
<BASE_DIR>/calibration/ <BASE_DIR>/calibration/
├─ current_calibration -> <preset_dir>/<calibration_name> ├─ current_calibration.json # Current active calibration metadata
├─ <preset_name>/ ├─ <preset_name>/
│ └─ <calibration_name>/ │ └─ <calibration_name>/
│ ├─ open.json / short.json / load.json / through.json │ ├─ open.json / short.json / load.json / through.json
@ -97,7 +97,7 @@ class CalibrationManager:
def __init__(self, base_dir: Path | None = None) -> None: def __init__(self, base_dir: Path | None = None) -> None:
self.base_dir = Path(base_dir or cfg.BASE_DIR) self.base_dir = Path(base_dir or cfg.BASE_DIR)
self.calibration_dir = self.base_dir / "calibration" self.calibration_dir = self.base_dir / "calibration"
self.current_calibration_symlink = self.calibration_dir / "current_calibration" self.current_calibration_file = self.calibration_dir / "current_calibration.json"
self.calibration_dir.mkdir(parents=True, exist_ok=True) self.calibration_dir.mkdir(parents=True, exist_ok=True)
self._current_working_set: CalibrationSet | None = None self._current_working_set: CalibrationSet | None = None
@ -268,10 +268,10 @@ class CalibrationManager:
} }
# ------------------------------------------------------------------ # # ------------------------------------------------------------------ #
# Current calibration (symlink) # Current calibration (JSON file)
# ------------------------------------------------------------------ # # ------------------------------------------------------------------ #
def set_current_calibration(self, preset: ConfigPreset, calibration_name: str) -> None: def set_current_calibration(self, preset: ConfigPreset, calibration_name: str) -> None:
"""Point the `current_calibration` symlink to the chosen calibration dir.""" """Save the current calibration selection to JSON file."""
preset_dir = self._get_preset_calibration_dir(preset) preset_dir = self._get_preset_calibration_dir(preset)
calib_dir = preset_dir / calibration_name calib_dir = preset_dir / calibration_name
if not calib_dir.exists(): if not calib_dir.exists():
@ -281,60 +281,59 @@ class CalibrationManager:
if not info.get("is_complete", False): if not info.get("is_complete", False):
raise ValueError(f"Calibration {calibration_name} is incomplete") raise ValueError(f"Calibration {calibration_name} is incomplete")
# Refresh symlink # Write current calibration info to JSON file
try: try:
if self.current_calibration_symlink.exists() or self.current_calibration_symlink.is_symlink(): current_info = {
self.current_calibration_symlink.unlink() "preset_filename": preset.filename,
except Exception as exc: # noqa: BLE001 "calibration_name": calibration_name,
logger.warning("Failed to remove existing current_calibration link", error=repr(exc)) "preset_dir": preset.filename.replace(".bin", ""),
"updated_at": datetime.now().isoformat()
try: }
# Create a relative link when possible to keep the tree portable self._atomic_json_write(self.current_calibration_file, current_info)
relative = calib_dir
try:
relative = calib_dir.relative_to(self.calibration_dir)
except ValueError:
pass
self.current_calibration_symlink.symlink_to(relative)
logger.info("Current calibration set", preset=preset.filename, name=calibration_name) logger.info("Current calibration set", preset=preset.filename, name=calibration_name)
except Exception as exc: # noqa: BLE001 except Exception as exc: # noqa: BLE001
logger.error("Failed to create current_calibration symlink", error=repr(exc)) logger.error("Failed to set current calibration", error=repr(exc))
raise raise
def get_current_calibration(self, current_preset: ConfigPreset) -> CalibrationSet | None: def get_current_calibration(self, current_preset: ConfigPreset) -> CalibrationSet | None:
""" """
Resolve and load the calibration currently pointed to by the symlink. Load the calibration currently saved in JSON file.
Returns None if the link doesn't exist, points to an invalid location, Returns None if the file doesn't exist, is invalid,
or targets a different preset. or targets a different preset.
""" """
if not self.current_calibration_symlink.exists(): if not self.current_calibration_file.exists():
return None return None
try: try:
target = self.current_calibration_symlink.resolve() with self.current_calibration_file.open("r", encoding="utf-8") as f:
calibration_name = target.name current_info = json.load(f)
preset_dir_name = target.parent.name # <preset_name> (without .bin)
expected_preset_name = current_preset.filename.replace(".bin", "") calibration_name = current_info.get("calibration_name")
if preset_dir_name != expected_preset_name: preset_filename = current_info.get("preset_filename")
if not calibration_name or not preset_filename:
logger.warning("Invalid current calibration file format")
return None
if preset_filename != current_preset.filename:
logger.warning( logger.warning(
"Current calibration preset mismatch", "Current calibration preset mismatch",
expected=expected_preset_name, expected=current_preset.filename,
actual=preset_dir_name, actual=preset_filename,
) )
raise RuntimeError("Current calibration belongs to a different preset") return None
return self.load_calibration_set(current_preset, calibration_name) return self.load_calibration_set(current_preset, calibration_name)
except Exception as exc: # noqa: BLE001 except Exception as exc: # noqa: BLE001
logger.warning("Failed to resolve current calibration", error=repr(exc)) logger.warning("Failed to load current calibration", error=repr(exc))
return None return None
def clear_current_calibration(self) -> None: def clear_current_calibration(self) -> None:
"""Remove the current calibration symlink.""" """Remove the current calibration JSON file."""
if self.current_calibration_symlink.exists() or self.current_calibration_symlink.is_symlink(): if self.current_calibration_file.exists():
try: try:
self.current_calibration_symlink.unlink() self.current_calibration_file.unlink()
logger.info("Current calibration cleared") logger.info("Current calibration cleared")
except Exception as exc: # noqa: BLE001 except Exception as exc: # noqa: BLE001
logger.warning("Failed to clear current calibration", error=repr(exc)) logger.warning("Failed to clear current calibration", error=repr(exc))

View File

@ -1,3 +1,4 @@
import json
import re import re
from dataclasses import dataclass from dataclasses import dataclass
from enum import Enum from enum import Enum
@ -52,7 +53,7 @@ class PresetManager:
<BASE_DIR>/vna_system/binary_input/ <BASE_DIR>/vna_system/binary_input/
├─ config_inputs/ ├─ config_inputs/
│ └─ *.bin (preset files; configuration encoded in filename) │ └─ *.bin (preset files; configuration encoded in filename)
└─ current_input.bin -> config_inputs/<chosen>.bin └─ current_input.json # Current active preset metadata
Filenames encode parameters, e.g.: Filenames encode parameters, e.g.:
s11_start100_stop8800_points1000_bw1khz.bin s11_start100_stop8800_points1000_bw1khz.bin
@ -69,7 +70,7 @@ class PresetManager:
def __init__(self, binary_input_dir: Path | None = None) -> None: def __init__(self, binary_input_dir: Path | None = None) -> None:
self.binary_input_dir = Path(binary_input_dir or (cfg.BASE_DIR / "vna_system" / "binary_input")) self.binary_input_dir = Path(binary_input_dir or (cfg.BASE_DIR / "vna_system" / "binary_input"))
self.config_inputs_dir = self.binary_input_dir / "config_inputs" self.config_inputs_dir = self.binary_input_dir / "config_inputs"
self.current_input_symlink = self.binary_input_dir / "current_input.bin" self.current_input_file = self.binary_input_dir / "current_input.json"
self.config_inputs_dir.mkdir(parents=True, exist_ok=True) self.config_inputs_dir.mkdir(parents=True, exist_ok=True)
logger.debug( logger.debug(
@ -184,43 +185,54 @@ class PresetManager:
def set_current_preset(self, preset: ConfigPreset) -> ConfigPreset: def set_current_preset(self, preset: ConfigPreset) -> ConfigPreset:
""" """
Select a preset by (re)pointing `current_input.bin` to the chosen file. Select a preset by saving it to JSON file.
""" """
src = self.config_inputs_dir / preset.filename src = self.config_inputs_dir / preset.filename
if not src.exists(): if not src.exists():
raise FileNotFoundError(f"Preset file not found: {preset.filename}") raise FileNotFoundError(f"Preset file not found: {preset.filename}")
# Remove any existing link/file # Write current preset info to JSON file
if self.current_input_symlink.exists() or self.current_input_symlink.is_symlink(): current_info = {
try: "filename": preset.filename,
self.current_input_symlink.unlink() "mode": preset.mode.value,
except Exception as exc: # noqa: BLE001 "start_freq": preset.start_freq,
logger.warning("Failed to remove existing current_input.bin", error=repr(exc)) "stop_freq": preset.stop_freq,
"points": preset.points,
"bandwidth": preset.bandwidth
}
# Prefer a relative symlink for portability with self.current_input_file.open("w", encoding="utf-8") as f:
try: json.dump(current_info, f, indent=2, ensure_ascii=False)
target = src.relative_to(self.binary_input_dir)
except ValueError:
target = src
self.current_input_symlink.symlink_to(target)
logger.info("Current preset set", filename=preset.filename) logger.info("Current preset set", filename=preset.filename)
return preset return preset
def get_current_preset(self) -> ConfigPreset | None: def get_current_preset(self) -> ConfigPreset | None:
""" """
Resolve the `current_input.bin` symlink and parse the underlying preset. Load current preset from JSON file.
Returns None if the symlink is missing or the filename cannot be parsed. Returns None if the file is missing or invalid.
""" """
if not self.current_input_symlink.exists(): if not self.current_input_file.exists():
return None return None
try: try:
target = self.current_input_symlink.resolve() with self.current_input_file.open("r", encoding="utf-8") as f:
return self._parse_filename(target.name) current_info = json.load(f)
filename = current_info.get("filename")
if not filename:
logger.warning("Invalid current preset file format")
return None
# Verify the file still exists
if not (self.config_inputs_dir / filename).exists():
logger.warning("Current preset file no longer exists", filename=filename)
return None
return self._parse_filename(filename)
except Exception as exc: # noqa: BLE001 except Exception as exc: # noqa: BLE001
logger.warning("Failed to resolve current preset", error=repr(exc)) logger.warning("Failed to load current preset", error=repr(exc))
return None return None
def preset_exists(self, preset: ConfigPreset) -> bool: def preset_exists(self, preset: ConfigPreset) -> bool:

View File

@ -63,7 +63,7 @@ class ReferenceManager:
References are stored in the following structure: References are stored in the following structure:
references/ references/
├── current_reference -> symlink to active reference ├── current_reference.json # Current active reference metadata
├── <preset_name>/ ├── <preset_name>/
│ ├── <reference_name>/ │ ├── <reference_name>/
│ │ ├── info.json # Reference metadata │ │ ├── info.json # Reference metadata
@ -74,7 +74,7 @@ class ReferenceManager:
def __init__(self): def __init__(self):
self.references_dir = Path(cfg.BASE_DIR) / "references" self.references_dir = Path(cfg.BASE_DIR) / "references"
self.current_reference_symlink = self.references_dir / "current_reference" self.current_reference_file = self.references_dir / "current_reference.json"
# Ensure directory structure exists # Ensure directory structure exists
self.references_dir.mkdir(exist_ok=True) self.references_dir.mkdir(exist_ok=True)
@ -219,17 +219,16 @@ class ReferenceManager:
raise ValueError(f"Reference '{name}' not found for preset {preset.filename}") raise ValueError(f"Reference '{name}' not found for preset {preset.filename}")
try: try:
# Remove existing symlink # Write current reference info to JSON file
if self.current_reference_symlink.exists() or self.current_reference_symlink.is_symlink(): current_info = {
self.current_reference_symlink.unlink() "reference_name": name,
"preset_filename": preset.filename,
"preset_dir": Path(preset.filename).stem,
"updated_at": datetime.now().isoformat()
}
# Create relative symlink for portability with self.current_reference_file.open("w", encoding="utf-8") as f:
try: json.dump(current_info, f, indent=2, ensure_ascii=False)
relative_target = reference_dir.relative_to(self.references_dir)
except ValueError:
relative_target = reference_dir
self.current_reference_symlink.symlink_to(relative_target)
logger.info("Set current reference", name=name, preset=preset.filename) logger.info("Set current reference", name=name, preset=preset.filename)
return True return True
@ -249,37 +248,42 @@ class ReferenceManager:
ReferenceInfo | None: Current reference info, or None if no reference is set ReferenceInfo | None: Current reference info, or None if no reference is set
or if the current reference doesn't match the preset or if the current reference doesn't match the preset
""" """
if not self.current_reference_symlink.exists(): if not self.current_reference_file.exists():
return None return None
try: try:
target_dir = self.current_reference_symlink.resolve() with self.current_reference_file.open("r", encoding="utf-8") as f:
current_info = json.load(f)
if not target_dir.exists(): reference_name = current_info.get("reference_name")
logger.warning("Current reference symlink points to non-existent directory") preset_filename = current_info.get("preset_filename")
if not reference_name or not preset_filename:
logger.warning("Invalid current reference file format")
return None return None
# Load reference info # Check if reference matches current preset
info_file = target_dir / "info.json" if preset_filename != preset.filename:
logger.debug(
"Current reference is for different preset",
reference_preset=preset_filename,
current_preset=preset.filename
)
return None
# Load full reference info
preset_dir = self._get_preset_dir(preset)
reference_dir = preset_dir / reference_name
info_file = reference_dir / "info.json"
if not info_file.exists(): if not info_file.exists():
logger.warning("Current reference missing info.json") logger.warning("Current reference missing info.json", name=reference_name)
return None return None
with info_file.open("r", encoding="utf-8") as f: with info_file.open("r", encoding="utf-8") as f:
info_data = json.load(f) info_data = json.load(f)
reference_info = ReferenceInfo.from_dict(info_data) return ReferenceInfo.from_dict(info_data)
# Check if reference matches current preset
if reference_info.preset_filename != preset.filename:
logger.debug(
"Current reference is for different preset",
reference_preset=reference_info.preset_filename,
current_preset=preset.filename
)
return None
return reference_info
except Exception as exc: except Exception as exc:
logger.warning("Failed to get current reference", error=repr(exc)) logger.warning("Failed to get current reference", error=repr(exc))
@ -293,8 +297,8 @@ class ReferenceManager:
bool: True if cleared successfully bool: True if cleared successfully
""" """
try: try:
if self.current_reference_symlink.exists() or self.current_reference_symlink.is_symlink(): if self.current_reference_file.exists():
self.current_reference_symlink.unlink() self.current_reference_file.unlink()
logger.info("Cleared current reference") logger.info("Cleared current reference")
return True return True
except Exception as exc: except Exception as exc:

View File

@ -1 +0,0 @@
s11_start100_stop8800_points1000_bw1khz/asd