added calibration
This commit is contained in:
301
vna_system/api/endpoints/settings.py
Normal file
301
vna_system/api/endpoints/settings.py
Normal file
@ -0,0 +1,301 @@
|
|||||||
|
from fastapi import APIRouter, HTTPException
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
import vna_system.core.singletons as singletons
|
||||||
|
from vna_system.core.settings.calibration_manager import CalibrationStandard
|
||||||
|
from vna_system.api.models.settings import (
|
||||||
|
PresetModel,
|
||||||
|
CalibrationModel,
|
||||||
|
SettingsStatusModel,
|
||||||
|
SetPresetRequest,
|
||||||
|
StartCalibrationRequest,
|
||||||
|
CalibrateStandardRequest,
|
||||||
|
SaveCalibrationRequest,
|
||||||
|
SetCalibrationRequest,
|
||||||
|
RemoveStandardRequest,
|
||||||
|
WorkingCalibrationModel
|
||||||
|
)
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/api/v1/settings", tags=["settings"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/status", response_model=SettingsStatusModel)
|
||||||
|
async def get_status():
|
||||||
|
"""Get current settings status"""
|
||||||
|
try:
|
||||||
|
status = singletons.settings_manager.get_status_summary()
|
||||||
|
return status
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/presets", response_model=List[PresetModel])
|
||||||
|
async def get_presets(mode: str | None = None):
|
||||||
|
"""Get all available configuration presets, optionally filtered by mode"""
|
||||||
|
try:
|
||||||
|
if mode:
|
||||||
|
from vna_system.core.settings.preset_manager import VNAMode
|
||||||
|
try:
|
||||||
|
vna_mode = VNAMode(mode.lower())
|
||||||
|
presets = singletons.settings_manager.get_presets_by_mode(vna_mode)
|
||||||
|
except ValueError:
|
||||||
|
raise HTTPException(status_code=400, detail=f"Invalid mode: {mode}")
|
||||||
|
else:
|
||||||
|
presets = singletons.settings_manager.get_available_presets()
|
||||||
|
|
||||||
|
return [
|
||||||
|
PresetModel(
|
||||||
|
filename=preset.filename,
|
||||||
|
mode=preset.mode.value,
|
||||||
|
start_freq=preset.start_freq,
|
||||||
|
stop_freq=preset.stop_freq,
|
||||||
|
points=preset.points,
|
||||||
|
bandwidth=preset.bandwidth
|
||||||
|
)
|
||||||
|
for preset in presets
|
||||||
|
]
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/preset/set")
|
||||||
|
async def set_preset(request: SetPresetRequest):
|
||||||
|
"""Set current configuration preset"""
|
||||||
|
try:
|
||||||
|
# Find preset by filename
|
||||||
|
presets = singletons.settings_manager.get_available_presets()
|
||||||
|
preset = next((p for p in presets if p.filename == request.filename), None)
|
||||||
|
|
||||||
|
if not preset:
|
||||||
|
raise HTTPException(status_code=404, detail=f"Preset not found: {request.filename}")
|
||||||
|
|
||||||
|
# Clear current calibration when changing preset
|
||||||
|
singletons.settings_manager.calibration_manager.clear_current_calibration()
|
||||||
|
|
||||||
|
singletons.settings_manager.set_current_preset(preset)
|
||||||
|
return {"success": True, "message": f"Preset set to {request.filename}"}
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/preset/current", response_model=PresetModel | None)
|
||||||
|
async def get_current_preset():
|
||||||
|
"""Get currently selected configuration preset"""
|
||||||
|
try:
|
||||||
|
preset = singletons.settings_manager.get_current_preset()
|
||||||
|
if not preset:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return PresetModel(
|
||||||
|
filename=preset.filename,
|
||||||
|
mode=preset.mode.value,
|
||||||
|
start_freq=preset.start_freq,
|
||||||
|
stop_freq=preset.stop_freq,
|
||||||
|
points=preset.points,
|
||||||
|
bandwidth=preset.bandwidth
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/calibrations", response_model=List[CalibrationModel])
|
||||||
|
async def get_calibrations(preset_filename: str | None = None):
|
||||||
|
"""Get available calibrations for current or specified preset"""
|
||||||
|
try:
|
||||||
|
preset = None
|
||||||
|
if preset_filename:
|
||||||
|
presets = singletons.settings_manager.get_available_presets()
|
||||||
|
preset = next((p for p in presets if p.filename == preset_filename), None)
|
||||||
|
if not preset:
|
||||||
|
raise HTTPException(status_code=404, detail=f"Preset not found: {preset_filename}")
|
||||||
|
|
||||||
|
calibrations = singletons.settings_manager.get_available_calibrations(preset)
|
||||||
|
|
||||||
|
# Get detailed info for each calibration
|
||||||
|
calibration_details = []
|
||||||
|
current_preset = preset or singletons.settings_manager.get_current_preset()
|
||||||
|
|
||||||
|
if current_preset:
|
||||||
|
for calib_name in calibrations:
|
||||||
|
info = singletons.settings_manager.get_calibration_info(calib_name, current_preset)
|
||||||
|
|
||||||
|
# Convert standards format if needed
|
||||||
|
standards = info.get('standards', {})
|
||||||
|
if isinstance(standards, list):
|
||||||
|
# If standards is a list (from complete calibration), convert to dict
|
||||||
|
required_standards = singletons.settings_manager.get_required_standards(current_preset.mode)
|
||||||
|
standards = {std.value: std.value in standards for std in required_standards}
|
||||||
|
|
||||||
|
calibration_details.append(CalibrationModel(
|
||||||
|
name=calib_name,
|
||||||
|
is_complete=info.get('is_complete', False),
|
||||||
|
standards=standards
|
||||||
|
))
|
||||||
|
|
||||||
|
return calibration_details
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/calibration/start")
|
||||||
|
async def start_calibration(request: StartCalibrationRequest):
|
||||||
|
"""Start new calibration for current or specified preset"""
|
||||||
|
try:
|
||||||
|
preset = None
|
||||||
|
if request.preset_filename:
|
||||||
|
presets = singletons.settings_manager.get_available_presets()
|
||||||
|
preset = next((p for p in presets if p.filename == request.preset_filename), None)
|
||||||
|
if not preset:
|
||||||
|
raise HTTPException(status_code=404, detail=f"Preset not found: {request.preset_filename}")
|
||||||
|
|
||||||
|
calibration_set = singletons.settings_manager.start_new_calibration(preset)
|
||||||
|
required_standards = singletons.settings_manager.get_required_standards(calibration_set.preset.mode)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": "Calibration started",
|
||||||
|
"preset": calibration_set.preset.filename,
|
||||||
|
"required_standards": [s.value for s in required_standards]
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/calibration/add-standard")
|
||||||
|
async def add_calibration_standard(request: CalibrateStandardRequest):
|
||||||
|
"""Add calibration standard from latest sweep"""
|
||||||
|
try:
|
||||||
|
# Validate standard
|
||||||
|
try:
|
||||||
|
standard = CalibrationStandard(request.standard)
|
||||||
|
except ValueError:
|
||||||
|
raise HTTPException(status_code=400, detail=f"Invalid calibration standard: {request.standard}")
|
||||||
|
|
||||||
|
# Capture from data acquisition
|
||||||
|
sweep_number = singletons.settings_manager.capture_calibration_standard_from_acquisition(
|
||||||
|
standard, singletons.vna_data_acquisition_instance
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get current working calibration status
|
||||||
|
working_calib = singletons.settings_manager.get_current_working_calibration()
|
||||||
|
progress = working_calib.get_progress() if working_calib else (0, 0)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": f"Added {standard.value} standard from sweep {sweep_number}",
|
||||||
|
"sweep_number": sweep_number,
|
||||||
|
"progress": f"{progress[0]}/{progress[1]}",
|
||||||
|
"is_complete": working_calib.is_complete() if working_calib else False
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/calibration/save")
|
||||||
|
async def save_calibration(request: SaveCalibrationRequest):
|
||||||
|
"""Save current working calibration set"""
|
||||||
|
try:
|
||||||
|
calibration_set = singletons.settings_manager.save_calibration_set(request.name)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": f"Calibration '{request.name}' saved successfully",
|
||||||
|
"preset": calibration_set.preset.filename,
|
||||||
|
"standards": list(calibration_set.standards.keys())
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/calibration/set")
|
||||||
|
async def set_calibration(request: SetCalibrationRequest):
|
||||||
|
"""Set current active calibration"""
|
||||||
|
try:
|
||||||
|
preset = None
|
||||||
|
if request.preset_filename:
|
||||||
|
presets = singletons.settings_manager.get_available_presets()
|
||||||
|
preset = next((p for p in presets if p.filename == request.preset_filename), None)
|
||||||
|
if not preset:
|
||||||
|
raise HTTPException(status_code=404, detail=f"Preset not found: {request.preset_filename}")
|
||||||
|
|
||||||
|
singletons.settings_manager.set_current_calibration(request.name, preset)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": f"Calibration set to '{request.name}'"
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/working-calibration", response_model=WorkingCalibrationModel)
|
||||||
|
async def get_working_calibration():
|
||||||
|
"""Get current working calibration status"""
|
||||||
|
try:
|
||||||
|
working_calib = singletons.settings_manager.get_current_working_calibration()
|
||||||
|
|
||||||
|
if not working_calib:
|
||||||
|
return WorkingCalibrationModel(active=False)
|
||||||
|
|
||||||
|
completed, total = working_calib.get_progress()
|
||||||
|
missing_standards = working_calib.get_missing_standards()
|
||||||
|
|
||||||
|
return WorkingCalibrationModel(
|
||||||
|
active=True,
|
||||||
|
preset=working_calib.preset.filename,
|
||||||
|
progress=f"{completed}/{total}",
|
||||||
|
is_complete=working_calib.is_complete(),
|
||||||
|
completed_standards=[s.value for s in working_calib.standards.keys()],
|
||||||
|
missing_standards=[s.value for s in missing_standards]
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/calibration/remove-standard")
|
||||||
|
async def remove_calibration_standard(request: RemoveStandardRequest):
|
||||||
|
"""Remove calibration standard from current working set"""
|
||||||
|
try:
|
||||||
|
# Validate standard
|
||||||
|
try:
|
||||||
|
standard = CalibrationStandard(request.standard)
|
||||||
|
except ValueError:
|
||||||
|
raise HTTPException(status_code=400, detail=f"Invalid calibration standard: {request.standard}")
|
||||||
|
|
||||||
|
singletons.settings_manager.remove_calibration_standard(standard)
|
||||||
|
|
||||||
|
# Get current working calibration status
|
||||||
|
working_calib = singletons.settings_manager.get_current_working_calibration()
|
||||||
|
progress = working_calib.get_progress() if working_calib else (0, 0)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": f"Removed {standard.value} standard",
|
||||||
|
"progress": f"{progress[0]}/{progress[1]}",
|
||||||
|
"is_complete": working_calib.is_complete() if working_calib else False
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/calibration/current")
|
||||||
|
async def get_current_calibration():
|
||||||
|
"""Get currently selected calibration details"""
|
||||||
|
try:
|
||||||
|
current_calib = singletons.settings_manager.get_current_calibration()
|
||||||
|
|
||||||
|
if not current_calib:
|
||||||
|
return {"active": False}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"active": True,
|
||||||
|
"preset": {
|
||||||
|
"filename": current_calib.preset.filename,
|
||||||
|
"mode": current_calib.preset.mode.value
|
||||||
|
},
|
||||||
|
"calibration_name": current_calib.name,
|
||||||
|
"standards": [s.value for s in current_calib.standards.keys()],
|
||||||
|
"is_complete": current_calib.is_complete()
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
@ -14,7 +14,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.core.processing.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, settings, web_ui
|
||||||
from vna_system.api.websockets import processing as ws_processing
|
from vna_system.api.websockets import processing as ws_processing
|
||||||
|
|
||||||
|
|
||||||
@ -117,6 +117,7 @@ else:
|
|||||||
app.include_router(web_ui.router) # Web UI should be first for root path
|
app.include_router(web_ui.router) # Web UI should be first for root path
|
||||||
app.include_router(health.router)
|
app.include_router(health.router)
|
||||||
app.include_router(processing.router)
|
app.include_router(processing.router)
|
||||||
|
app.include_router(settings.router)
|
||||||
app.include_router(ws_processing.router)
|
app.include_router(ws_processing.router)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
59
vna_system/api/models/settings.py
Normal file
59
vna_system/api/models/settings.py
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
from typing import List, Dict, Any
|
||||||
|
|
||||||
|
|
||||||
|
class PresetModel(BaseModel):
|
||||||
|
filename: str
|
||||||
|
mode: str
|
||||||
|
start_freq: float | None
|
||||||
|
stop_freq: float | None
|
||||||
|
points: int | None
|
||||||
|
bandwidth: float | None
|
||||||
|
|
||||||
|
|
||||||
|
class CalibrationModel(BaseModel):
|
||||||
|
name: str
|
||||||
|
is_complete: bool
|
||||||
|
standards: Dict[str, bool]
|
||||||
|
|
||||||
|
|
||||||
|
class SettingsStatusModel(BaseModel):
|
||||||
|
current_preset: PresetModel | None
|
||||||
|
current_calibration: Dict[str, Any] | None
|
||||||
|
working_calibration: Dict[str, Any] | None
|
||||||
|
available_presets: int
|
||||||
|
available_calibrations: int
|
||||||
|
|
||||||
|
|
||||||
|
class SetPresetRequest(BaseModel):
|
||||||
|
filename: str
|
||||||
|
|
||||||
|
|
||||||
|
class StartCalibrationRequest(BaseModel):
|
||||||
|
preset_filename: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class CalibrateStandardRequest(BaseModel):
|
||||||
|
standard: str
|
||||||
|
|
||||||
|
|
||||||
|
class SaveCalibrationRequest(BaseModel):
|
||||||
|
name: str
|
||||||
|
|
||||||
|
|
||||||
|
class SetCalibrationRequest(BaseModel):
|
||||||
|
name: str
|
||||||
|
preset_filename: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class RemoveStandardRequest(BaseModel):
|
||||||
|
standard: str
|
||||||
|
|
||||||
|
|
||||||
|
class WorkingCalibrationModel(BaseModel):
|
||||||
|
active: bool
|
||||||
|
preset: str | None = None
|
||||||
|
progress: str | None = None
|
||||||
|
is_complete: bool | None = None
|
||||||
|
completed_standards: List[str] | None = None
|
||||||
|
missing_standards: List[str] | None = None
|
||||||
Binary file not shown.
1
vna_system/binary_input/current_input.bin
Symbolic link
1
vna_system/binary_input/current_input.bin
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
config_inputs/s11_start100_stop8800_points1000_bw1khz.bin
|
||||||
1
vna_system/calibration/current_calibration
Symbolic link
1
vna_system/calibration/current_calibration
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
s11_start100_stop8800_points1000_bw1khz/tuncTuncTuncSahur
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"preset": {
|
||||||
|
"filename": "s11_start100_stop8800_points1000_bw1khz.bin",
|
||||||
|
"mode": "s11",
|
||||||
|
"start_freq": 100000000.0,
|
||||||
|
"stop_freq": 8800000000.0,
|
||||||
|
"points": 1000,
|
||||||
|
"bandwidth": 1000.0
|
||||||
|
},
|
||||||
|
"calibration_name": "lol",
|
||||||
|
"standards": [
|
||||||
|
"open",
|
||||||
|
"short",
|
||||||
|
"load"
|
||||||
|
],
|
||||||
|
"created_timestamp": "2025-09-24T17:28:43.780211",
|
||||||
|
"is_complete": true
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"preset": {
|
||||||
|
"filename": "s11_start100_stop8800_points1000_bw1khz.bin",
|
||||||
|
"mode": "s11",
|
||||||
|
"start_freq": 100000000.0,
|
||||||
|
"stop_freq": 8800000000.0,
|
||||||
|
"points": 1000,
|
||||||
|
"bandwidth": 1000.0
|
||||||
|
},
|
||||||
|
"calibration_name": "lol",
|
||||||
|
"standard": "load",
|
||||||
|
"sweep_number": 736,
|
||||||
|
"sweep_timestamp": 1758724118.5653427,
|
||||||
|
"created_timestamp": "2025-09-24T17:28:43.780150",
|
||||||
|
"total_points": 1000
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"preset": {
|
||||||
|
"filename": "s11_start100_stop8800_points1000_bw1khz.bin",
|
||||||
|
"mode": "s11",
|
||||||
|
"start_freq": 100000000.0,
|
||||||
|
"stop_freq": 8800000000.0,
|
||||||
|
"points": 1000,
|
||||||
|
"bandwidth": 1000.0
|
||||||
|
},
|
||||||
|
"calibration_name": "lol",
|
||||||
|
"standard": "open",
|
||||||
|
"sweep_number": 731,
|
||||||
|
"sweep_timestamp": 1758724108.0550592,
|
||||||
|
"created_timestamp": "2025-09-24T17:28:43.775117",
|
||||||
|
"total_points": 1000
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"preset": {
|
||||||
|
"filename": "s11_start100_stop8800_points1000_bw1khz.bin",
|
||||||
|
"mode": "s11",
|
||||||
|
"start_freq": 100000000.0,
|
||||||
|
"stop_freq": 8800000000.0,
|
||||||
|
"points": 1000,
|
||||||
|
"bandwidth": 1000.0
|
||||||
|
},
|
||||||
|
"calibration_name": "lol",
|
||||||
|
"standard": "short",
|
||||||
|
"sweep_number": 733,
|
||||||
|
"sweep_timestamp": 1758724112.282575,
|
||||||
|
"created_timestamp": "2025-09-24T17:28:43.777473",
|
||||||
|
"total_points": 1000
|
||||||
|
}
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"preset": {
|
||||||
|
"filename": "s11_start100_stop8800_points1000_bw1khz.bin",
|
||||||
|
"mode": "s11",
|
||||||
|
"start_freq": 100000000.0,
|
||||||
|
"stop_freq": 8800000000.0,
|
||||||
|
"points": 1000,
|
||||||
|
"bandwidth": 1000.0
|
||||||
|
},
|
||||||
|
"calibration_name": "tuncTuncTuncSahur",
|
||||||
|
"standards": [
|
||||||
|
"open",
|
||||||
|
"short",
|
||||||
|
"load"
|
||||||
|
],
|
||||||
|
"created_timestamp": "2025-09-24T17:55:12.053850",
|
||||||
|
"is_complete": true
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"preset": {
|
||||||
|
"filename": "s11_start100_stop8800_points1000_bw1khz.bin",
|
||||||
|
"mode": "s11",
|
||||||
|
"start_freq": 100000000.0,
|
||||||
|
"stop_freq": 8800000000.0,
|
||||||
|
"points": 1000,
|
||||||
|
"bandwidth": 1000.0
|
||||||
|
},
|
||||||
|
"calibration_name": "tuncTuncTuncSahur",
|
||||||
|
"standard": "load",
|
||||||
|
"sweep_number": 1221,
|
||||||
|
"sweep_timestamp": 1758725709.366605,
|
||||||
|
"created_timestamp": "2025-09-24T17:55:12.053789",
|
||||||
|
"total_points": 1000
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"preset": {
|
||||||
|
"filename": "s11_start100_stop8800_points1000_bw1khz.bin",
|
||||||
|
"mode": "s11",
|
||||||
|
"start_freq": 100000000.0,
|
||||||
|
"stop_freq": 8800000000.0,
|
||||||
|
"points": 1000,
|
||||||
|
"bandwidth": 1000.0
|
||||||
|
},
|
||||||
|
"calibration_name": "tuncTuncTuncSahur",
|
||||||
|
"standard": "open",
|
||||||
|
"sweep_number": 1217,
|
||||||
|
"sweep_timestamp": 1758725700.971026,
|
||||||
|
"created_timestamp": "2025-09-24T17:55:12.049478",
|
||||||
|
"total_points": 1000
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"preset": {
|
||||||
|
"filename": "s11_start100_stop8800_points1000_bw1khz.bin",
|
||||||
|
"mode": "s11",
|
||||||
|
"start_freq": 100000000.0,
|
||||||
|
"stop_freq": 8800000000.0,
|
||||||
|
"points": 1000,
|
||||||
|
"bandwidth": 1000.0
|
||||||
|
},
|
||||||
|
"calibration_name": "tuncTuncTuncSahur",
|
||||||
|
"standard": "short",
|
||||||
|
"sweep_number": 1219,
|
||||||
|
"sweep_timestamp": 1758725705.1671622,
|
||||||
|
"created_timestamp": "2025-09-24T17:55:12.051603",
|
||||||
|
"total_points": 1000
|
||||||
|
}
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"preset": {
|
||||||
|
"filename": "s21_start100_stop8800_points1000_bw1khz.bin",
|
||||||
|
"mode": "s21",
|
||||||
|
"start_freq": 100000000.0,
|
||||||
|
"stop_freq": 8800000000.0,
|
||||||
|
"points": 1000,
|
||||||
|
"bandwidth": 1000.0
|
||||||
|
},
|
||||||
|
"calibration_name": "bimBimPatapim",
|
||||||
|
"standards": [
|
||||||
|
"through"
|
||||||
|
],
|
||||||
|
"created_timestamp": "2025-09-24T17:50:35.610853",
|
||||||
|
"is_complete": true
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"preset": {
|
||||||
|
"filename": "s21_start100_stop8800_points1000_bw1khz.bin",
|
||||||
|
"mode": "s21",
|
||||||
|
"start_freq": 100000000.0,
|
||||||
|
"stop_freq": 8800000000.0,
|
||||||
|
"points": 1000,
|
||||||
|
"bandwidth": 1000.0
|
||||||
|
},
|
||||||
|
"calibration_name": "bimBimPatapim",
|
||||||
|
"standard": "through",
|
||||||
|
"sweep_number": 1186,
|
||||||
|
"sweep_timestamp": 1758725431.370608,
|
||||||
|
"created_timestamp": "2025-09-24T17:50:35.610460",
|
||||||
|
"total_points": 1000
|
||||||
|
}
|
||||||
@ -35,7 +35,7 @@ SWEEP_BUFFER_MAX_SIZE = 100 # Maximum number of sweeps to store in circular buf
|
|||||||
SERIAL_BUFFER_SIZE = 512 * 1024
|
SERIAL_BUFFER_SIZE = 512 * 1024
|
||||||
|
|
||||||
# Log file settings
|
# Log file settings
|
||||||
BIN_LOG_FILE_PATH = "./vna_system/binary_logs/current_log.bin" # Symbolic link to the current log file
|
BIN_INPUT_FILE_PATH = "./vna_system/binary_input/current_input.bin" # Symbolic link to the current log file
|
||||||
|
|
||||||
# Binary log format constants
|
# Binary log format constants
|
||||||
MAGIC = b"VNALOG1\n"
|
MAGIC = b"VNALOG1\n"
|
||||||
|
|||||||
@ -21,7 +21,7 @@ class VNADataAcquisition:
|
|||||||
"""Main data acquisition class with asynchronous sweep collection."""
|
"""Main data acquisition class with asynchronous sweep collection."""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.bin_log_path: str = cfg.BIN_LOG_FILE_PATH
|
self.bin_log_path: str = cfg.BIN_INPUT_FILE_PATH
|
||||||
|
|
||||||
self.baud: int = cfg.DEFAULT_BAUD_RATE
|
self.baud: int = cfg.DEFAULT_BAUD_RATE
|
||||||
|
|
||||||
@ -81,7 +81,6 @@ class VNADataAcquisition:
|
|||||||
# Serial management
|
# Serial management
|
||||||
# --------------------------------------------------------------------- #
|
# --------------------------------------------------------------------- #
|
||||||
|
|
||||||
|
|
||||||
def _drain_serial_input(self, ser: serial.Serial) -> None:
|
def _drain_serial_input(self, ser: serial.Serial) -> None:
|
||||||
"""Drain any pending bytes from the serial input buffer."""
|
"""Drain any pending bytes from the serial input buffer."""
|
||||||
if not ser:
|
if not ser:
|
||||||
|
|||||||
134
vna_system/core/processing/calibration_processor.py
Normal file
134
vna_system/core/processing/calibration_processor.py
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
"""
|
||||||
|
Calibration Processor Module
|
||||||
|
|
||||||
|
Applies VNA calibrations to sweep data using stored calibration standards.
|
||||||
|
Supports both S11 and S21 measurement modes with appropriate correction algorithms.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from vna_system.core.acquisition.sweep_buffer import SweepData
|
||||||
|
from vna_system.core.settings.preset_manager import VNAMode
|
||||||
|
from vna_system.core.settings.calibration_manager import CalibrationSet
|
||||||
|
from vna_system.core.settings.calibration_manager import CalibrationStandard
|
||||||
|
|
||||||
|
|
||||||
|
class CalibrationProcessor:
|
||||||
|
"""
|
||||||
|
Processes sweep data by applying VNA calibrations.
|
||||||
|
|
||||||
|
For S11 mode: Uses OSL (Open-Short-Load) calibration
|
||||||
|
For S21 mode: Uses Through calibration
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def apply_calibration(self, sweep_data: SweepData, calibration_set: CalibrationSet) -> np.ndarray:
|
||||||
|
"""
|
||||||
|
Apply calibration to sweep data and return corrected complex array.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sweep_data: Raw sweep data from VNA
|
||||||
|
calibration_set: Calibration standards data
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Complex array with calibration applied
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If calibration is incomplete or mode not supported
|
||||||
|
"""
|
||||||
|
if not calibration_set.is_complete():
|
||||||
|
raise ValueError("Calibration set is incomplete")
|
||||||
|
|
||||||
|
# Convert sweep data to complex array
|
||||||
|
raw_signal = self._sweep_to_complex_array(sweep_data)
|
||||||
|
|
||||||
|
# Apply calibration based on measurement mode
|
||||||
|
if calibration_set.preset.mode == VNAMode.S21:
|
||||||
|
return self._apply_s21_calibration(raw_signal, calibration_set)
|
||||||
|
elif calibration_set.preset.mode == VNAMode.S11:
|
||||||
|
return self._apply_s11_calibration(raw_signal, calibration_set)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unsupported measurement mode: {calibration_set.preset.mode}")
|
||||||
|
|
||||||
|
def _sweep_to_complex_array(self, sweep_data: SweepData) -> np.ndarray:
|
||||||
|
"""Convert SweepData to complex numpy array."""
|
||||||
|
complex_data = []
|
||||||
|
for real, imag in sweep_data.points:
|
||||||
|
complex_data.append(complex(real, imag))
|
||||||
|
return np.array(complex_data)
|
||||||
|
|
||||||
|
def _apply_s21_calibration(self, raw_signal: np.ndarray, calibration_set: CalibrationSet) -> np.ndarray:
|
||||||
|
"""
|
||||||
|
Apply S21 (transmission) calibration using through standard.
|
||||||
|
|
||||||
|
Calibrated_S21 = Raw_Signal / Through_Reference
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Get through calibration data
|
||||||
|
through_sweep = calibration_set.standards[CalibrationStandard.THROUGH]
|
||||||
|
through_reference = self._sweep_to_complex_array(through_sweep)
|
||||||
|
|
||||||
|
# Validate array sizes
|
||||||
|
if len(raw_signal) != len(through_reference):
|
||||||
|
raise ValueError("Signal and calibration data have different lengths")
|
||||||
|
|
||||||
|
# Avoid division by zero
|
||||||
|
through_reference = np.where(through_reference == 0, 1e-12, through_reference)
|
||||||
|
|
||||||
|
# Apply through calibration
|
||||||
|
calibrated_signal = raw_signal / through_reference
|
||||||
|
|
||||||
|
return calibrated_signal
|
||||||
|
|
||||||
|
def _apply_s11_calibration(self, raw_signal: np.ndarray, calibration_set: CalibrationSet) -> np.ndarray:
|
||||||
|
"""
|
||||||
|
Apply S11 (reflection) calibration using OSL (Open-Short-Load) method.
|
||||||
|
|
||||||
|
This implements the standard OSL error correction:
|
||||||
|
- Ed (Directivity): Load standard
|
||||||
|
- Es (Source Match): Calculated from Open, Short, Load
|
||||||
|
- Er (Reflection Tracking): Calculated from Open, Short, Load
|
||||||
|
|
||||||
|
Final correction: S11 = (Raw - Ed) / (Er + Es * (Raw - Ed))
|
||||||
|
"""
|
||||||
|
from vna_system.core.settings.calibration_manager import CalibrationStandard
|
||||||
|
|
||||||
|
# Get calibration standards
|
||||||
|
open_sweep = calibration_set.standards[CalibrationStandard.OPEN]
|
||||||
|
short_sweep = calibration_set.standards[CalibrationStandard.SHORT]
|
||||||
|
load_sweep = calibration_set.standards[CalibrationStandard.LOAD]
|
||||||
|
|
||||||
|
# Convert to complex arrays
|
||||||
|
open_cal = self._sweep_to_complex_array(open_sweep)
|
||||||
|
short_cal = self._sweep_to_complex_array(short_sweep)
|
||||||
|
load_cal = self._sweep_to_complex_array(load_sweep)
|
||||||
|
|
||||||
|
# Validate array sizes
|
||||||
|
if not (len(raw_signal) == len(open_cal) == len(short_cal) == len(load_cal)):
|
||||||
|
raise ValueError("Signal and calibration data have different lengths")
|
||||||
|
|
||||||
|
# Calculate error terms
|
||||||
|
directivity = load_cal.copy() # Ed = Load
|
||||||
|
|
||||||
|
# Source match: Es = (Open + Short - 2*Load) / (Open - Short)
|
||||||
|
denominator = open_cal - short_cal
|
||||||
|
denominator = np.where(np.abs(denominator) < 1e-12, 1e-12, denominator)
|
||||||
|
source_match = (open_cal + short_cal - 2 * load_cal) / denominator
|
||||||
|
|
||||||
|
# Reflection tracking: Er = -2 * (Open - Load) * (Short - Load) / (Open - Short)
|
||||||
|
reflection_tracking = -2 * (open_cal - load_cal) * (short_cal - load_cal) / denominator
|
||||||
|
|
||||||
|
# Apply OSL correction
|
||||||
|
corrected_numerator = raw_signal - directivity
|
||||||
|
corrected_denominator = reflection_tracking + source_match * corrected_numerator
|
||||||
|
|
||||||
|
# Avoid division by zero
|
||||||
|
corrected_denominator = np.where(np.abs(corrected_denominator) < 1e-12, 1e-12, corrected_denominator)
|
||||||
|
|
||||||
|
calibrated_signal = corrected_numerator / corrected_denominator
|
||||||
|
|
||||||
|
return calibrated_signal
|
||||||
@ -5,7 +5,6 @@ Magnitude plot processor for sweep data.
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List, Tuple
|
from typing import Any, Dict, List, Tuple
|
||||||
|
|
||||||
@ -17,6 +16,8 @@ import plotly.graph_objects as go
|
|||||||
|
|
||||||
from vna_system.core.acquisition.sweep_buffer import SweepData
|
from vna_system.core.acquisition.sweep_buffer import SweepData
|
||||||
from vna_system.core.processing.base_processor import BaseSweepProcessor, ProcessingResult
|
from vna_system.core.processing.base_processor import BaseSweepProcessor, ProcessingResult
|
||||||
|
from vna_system.core.processing.calibration_processor import CalibrationProcessor
|
||||||
|
import vna_system.core.singletons as singletons
|
||||||
|
|
||||||
|
|
||||||
class MagnitudePlotProcessor(BaseSweepProcessor):
|
class MagnitudePlotProcessor(BaseSweepProcessor):
|
||||||
@ -30,6 +31,10 @@ class MagnitudePlotProcessor(BaseSweepProcessor):
|
|||||||
self.width = config.get("width", 800)
|
self.width = config.get("width", 800)
|
||||||
self.height = config.get("height", 600)
|
self.height = config.get("height", 600)
|
||||||
|
|
||||||
|
# Calibration support
|
||||||
|
self.apply_calibration = config.get("apply_calibration", True)
|
||||||
|
self.calibration_processor = CalibrationProcessor()
|
||||||
|
|
||||||
# Create output directory if it doesn't exist
|
# Create output directory if it doesn't exist
|
||||||
if self.save_image:
|
if self.save_image:
|
||||||
self.output_dir.mkdir(parents=True, exist_ok=True)
|
self.output_dir.mkdir(parents=True, exist_ok=True)
|
||||||
@ -43,18 +48,24 @@ class MagnitudePlotProcessor(BaseSweepProcessor):
|
|||||||
if not self.should_process(sweep):
|
if not self.should_process(sweep):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
# Get current calibration from settings manager
|
||||||
|
current_calibration = singletons.settings_manager.get_current_calibration()
|
||||||
|
|
||||||
|
# Apply calibration if available and enabled
|
||||||
|
processed_sweep = self._apply_calibration_if_available(sweep, current_calibration)
|
||||||
|
|
||||||
# Extract magnitude data
|
# Extract magnitude data
|
||||||
magnitude_data = self._extract_magnitude_data(sweep)
|
magnitude_data = self._extract_magnitude_data(processed_sweep)
|
||||||
if not magnitude_data:
|
if not magnitude_data:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Create plotly figure for websocket/API consumption
|
# Create plotly figure for websocket/API consumption
|
||||||
plotly_fig = self._create_plotly_figure(sweep, magnitude_data)
|
plotly_fig = self._create_plotly_figure(sweep, magnitude_data, current_calibration is not None)
|
||||||
|
|
||||||
# Save image if requested (using matplotlib)
|
# Save image if requested (using matplotlib)
|
||||||
file_path = None
|
file_path = None
|
||||||
if self.save_image:
|
if self.save_image:
|
||||||
file_path = self._save_matplotlib_image(sweep, magnitude_data)
|
file_path = self._save_matplotlib_image(sweep, magnitude_data, current_calibration is not None)
|
||||||
|
|
||||||
# Prepare result data
|
# Prepare result data
|
||||||
result_data = {
|
result_data = {
|
||||||
@ -62,6 +73,8 @@ class MagnitudePlotProcessor(BaseSweepProcessor):
|
|||||||
"timestamp": sweep.timestamp,
|
"timestamp": sweep.timestamp,
|
||||||
"total_points": sweep.total_points,
|
"total_points": sweep.total_points,
|
||||||
"magnitude_stats": self._calculate_magnitude_stats(magnitude_data),
|
"magnitude_stats": self._calculate_magnitude_stats(magnitude_data),
|
||||||
|
"calibration_applied": current_calibration is not None and self.apply_calibration,
|
||||||
|
"calibration_name": current_calibration.name if current_calibration else None,
|
||||||
}
|
}
|
||||||
|
|
||||||
return ProcessingResult(
|
return ProcessingResult(
|
||||||
@ -72,6 +85,29 @@ class MagnitudePlotProcessor(BaseSweepProcessor):
|
|||||||
file_path=str(file_path) if file_path else None,
|
file_path=str(file_path) if file_path else None,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _apply_calibration_if_available(self, sweep: SweepData, calibration_set) -> SweepData:
|
||||||
|
"""Apply calibration to sweep data if calibration is available and enabled."""
|
||||||
|
if not self.apply_calibration or not calibration_set:
|
||||||
|
return sweep
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Apply calibration and get corrected complex array
|
||||||
|
calibrated_complex = self.calibration_processor.apply_calibration(sweep, calibration_set)
|
||||||
|
|
||||||
|
# Convert back to (real, imag) tuples for SweepData
|
||||||
|
calibrated_points = [(complex_val.real, complex_val.imag) for complex_val in calibrated_complex]
|
||||||
|
|
||||||
|
# Create new SweepData with calibrated points
|
||||||
|
return SweepData(
|
||||||
|
sweep_number=sweep.sweep_number,
|
||||||
|
timestamp=sweep.timestamp,
|
||||||
|
points=calibrated_points,
|
||||||
|
total_points=len(calibrated_points)
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Failed to apply calibration: {e}")
|
||||||
|
return sweep
|
||||||
|
|
||||||
def _extract_magnitude_data(self, sweep: SweepData) -> List[Tuple[float, float]]:
|
def _extract_magnitude_data(self, sweep: SweepData) -> List[Tuple[float, float]]:
|
||||||
"""Extract magnitude data from sweep points."""
|
"""Extract magnitude data from sweep points."""
|
||||||
magnitude_data = []
|
magnitude_data = []
|
||||||
@ -80,23 +116,29 @@ class MagnitudePlotProcessor(BaseSweepProcessor):
|
|||||||
magnitude_data.append((i, magnitude))
|
magnitude_data.append((i, magnitude))
|
||||||
return magnitude_data
|
return magnitude_data
|
||||||
|
|
||||||
def _create_plotly_figure(self, sweep: SweepData, magnitude_data: List[Tuple[float, float]]) -> go.Figure:
|
def _create_plotly_figure(self, sweep: SweepData, magnitude_data: List[Tuple[float, float]], calibrated: bool = False) -> go.Figure:
|
||||||
"""Create plotly figure for magnitude plot."""
|
"""Create plotly figure for magnitude plot."""
|
||||||
indices = [point[0] for point in magnitude_data]
|
indices = [point[0] for point in magnitude_data]
|
||||||
magnitudes = [point[1] for point in magnitude_data]
|
magnitudes = [point[1] for point in magnitude_data]
|
||||||
|
|
||||||
fig = go.Figure()
|
fig = go.Figure()
|
||||||
|
|
||||||
|
# Choose color based on calibration status
|
||||||
|
line_color = 'green' if calibrated else 'blue'
|
||||||
|
trace_name = 'Magnitude (Calibrated)' if calibrated else 'Magnitude (Raw)'
|
||||||
|
|
||||||
fig.add_trace(go.Scatter(
|
fig.add_trace(go.Scatter(
|
||||||
x=indices,
|
x=indices,
|
||||||
y=magnitudes,
|
y=magnitudes,
|
||||||
mode='lines',
|
mode='lines',
|
||||||
name='Magnitude',
|
name=trace_name,
|
||||||
line=dict(color='blue', width=2)
|
line=dict(color=line_color, width=2)
|
||||||
))
|
))
|
||||||
|
|
||||||
|
# Add calibration indicator to title
|
||||||
|
title_suffix = ' (Calibrated)' if calibrated else ' (Raw)'
|
||||||
fig.update_layout(
|
fig.update_layout(
|
||||||
title=f'Magnitude Plot - Sweep #{sweep.sweep_number}',
|
title=f'Magnitude Plot - Sweep #{sweep.sweep_number}{title_suffix}',
|
||||||
xaxis_title='Point Index',
|
xaxis_title='Point Index',
|
||||||
yaxis_title='Magnitude',
|
yaxis_title='Magnitude',
|
||||||
width=self.width,
|
width=self.width,
|
||||||
@ -107,10 +149,12 @@ class MagnitudePlotProcessor(BaseSweepProcessor):
|
|||||||
|
|
||||||
return fig
|
return fig
|
||||||
|
|
||||||
def _save_matplotlib_image(self, sweep: SweepData, magnitude_data: List[Tuple[float, float]]) -> Path | None:
|
def _save_matplotlib_image(self, sweep: SweepData, magnitude_data: List[Tuple[float, float]], calibrated: bool = False) -> 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}"
|
# Add calibration indicator to filename
|
||||||
|
cal_suffix = "_cal" if calibrated else ""
|
||||||
|
filename = f"magnitude_sweep_{sweep.sweep_number:06d}{cal_suffix}.{self.image_format}"
|
||||||
file_path = self.output_dir / filename
|
file_path = self.output_dir / filename
|
||||||
|
|
||||||
# Extract data for plotting
|
# Extract data for plotting
|
||||||
@ -119,9 +163,16 @@ class MagnitudePlotProcessor(BaseSweepProcessor):
|
|||||||
|
|
||||||
# Create matplotlib figure
|
# Create matplotlib figure
|
||||||
fig, ax = plt.subplots(figsize=(self.width/100, self.height/100), dpi=100)
|
fig, ax = plt.subplots(figsize=(self.width/100, self.height/100), dpi=100)
|
||||||
ax.plot(indices, magnitudes, 'b-', linewidth=2, label='Magnitude')
|
|
||||||
|
|
||||||
ax.set_title(f'Magnitude Plot - Sweep #{sweep.sweep_number}')
|
# Choose color and label based on calibration status
|
||||||
|
line_color = 'green' if calibrated else 'blue'
|
||||||
|
label = 'Magnitude (Calibrated)' if calibrated else 'Magnitude (Raw)'
|
||||||
|
|
||||||
|
ax.plot(indices, magnitudes, color=line_color, linewidth=2, label=label)
|
||||||
|
|
||||||
|
# Add calibration indicator to title
|
||||||
|
title_suffix = ' (Calibrated)' if calibrated else ' (Raw)'
|
||||||
|
ax.set_title(f'Magnitude Plot - Sweep #{sweep.sweep_number}{title_suffix}')
|
||||||
ax.set_xlabel('Point Index')
|
ax.set_xlabel('Point Index')
|
||||||
ax.set_ylabel('Magnitude')
|
ax.set_ylabel('Magnitude')
|
||||||
ax.legend()
|
ax.legend()
|
||||||
|
|||||||
315
vna_system/core/settings/calibration_manager.py
Normal file
315
vna_system/core/settings/calibration_manager.py
Normal file
@ -0,0 +1,315 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import shutil
|
||||||
|
from datetime import datetime
|
||||||
|
from enum import Enum
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, List
|
||||||
|
|
||||||
|
from vna_system.config import config as cfg
|
||||||
|
from vna_system.core.acquisition.sweep_buffer import SweepData
|
||||||
|
from .preset_manager import ConfigPreset, VNAMode, PresetManager
|
||||||
|
|
||||||
|
|
||||||
|
class CalibrationStandard(Enum):
|
||||||
|
OPEN = "open"
|
||||||
|
SHORT = "short"
|
||||||
|
LOAD = "load"
|
||||||
|
THROUGH = "through"
|
||||||
|
|
||||||
|
|
||||||
|
class CalibrationSet:
|
||||||
|
def __init__(self, preset: ConfigPreset, name: str = ""):
|
||||||
|
self.preset = preset
|
||||||
|
self.name = name
|
||||||
|
self.standards: Dict[CalibrationStandard, SweepData] = {}
|
||||||
|
|
||||||
|
def add_standard(self, standard: CalibrationStandard, sweep_data: SweepData):
|
||||||
|
"""Add calibration data for specific standard"""
|
||||||
|
self.standards[standard] = sweep_data
|
||||||
|
|
||||||
|
def remove_standard(self, standard: CalibrationStandard):
|
||||||
|
"""Remove calibration data for specific standard"""
|
||||||
|
if standard in self.standards:
|
||||||
|
del self.standards[standard]
|
||||||
|
|
||||||
|
def has_standard(self, standard: CalibrationStandard) -> bool:
|
||||||
|
"""Check if standard is present in calibration set"""
|
||||||
|
return standard in self.standards
|
||||||
|
|
||||||
|
def is_complete(self) -> bool:
|
||||||
|
"""Check if all required standards are present"""
|
||||||
|
required_standards = self._get_required_standards()
|
||||||
|
return all(std in self.standards for std in required_standards)
|
||||||
|
|
||||||
|
def get_missing_standards(self) -> List[CalibrationStandard]:
|
||||||
|
"""Get list of missing required standards"""
|
||||||
|
required_standards = self._get_required_standards()
|
||||||
|
return [std for std in required_standards if std not in self.standards]
|
||||||
|
|
||||||
|
def get_progress(self) -> tuple[int, int]:
|
||||||
|
"""Get calibration progress as (completed, total)"""
|
||||||
|
required_standards = self._get_required_standards()
|
||||||
|
completed = len([std for std in required_standards if std in self.standards])
|
||||||
|
return completed, len(required_standards)
|
||||||
|
|
||||||
|
def _get_required_standards(self) -> List[CalibrationStandard]:
|
||||||
|
"""Get required calibration standards for preset mode"""
|
||||||
|
if self.preset.mode == VNAMode.S11:
|
||||||
|
return [CalibrationStandard.OPEN, CalibrationStandard.SHORT, CalibrationStandard.LOAD]
|
||||||
|
elif self.preset.mode == VNAMode.S21:
|
||||||
|
return [CalibrationStandard.THROUGH]
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
class CalibrationManager:
|
||||||
|
def __init__(self, preset_manager: PresetManager, base_dir: Path | None = None):
|
||||||
|
self.base_dir = Path(base_dir or cfg.BASE_DIR)
|
||||||
|
self.calibration_dir = self.base_dir / "calibration"
|
||||||
|
self.current_calibration_symlink = self.calibration_dir / "current_calibration"
|
||||||
|
|
||||||
|
self.calibration_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# Current working calibration set
|
||||||
|
self._current_working_set: CalibrationSet | None = None
|
||||||
|
|
||||||
|
# Preset manager for parsing filenames
|
||||||
|
self.preset_manager = preset_manager
|
||||||
|
|
||||||
|
def start_new_calibration(self, preset: ConfigPreset) -> CalibrationSet:
|
||||||
|
"""Start new calibration set for preset"""
|
||||||
|
self._current_working_set = CalibrationSet(preset)
|
||||||
|
return self._current_working_set
|
||||||
|
|
||||||
|
def get_current_working_set(self) -> CalibrationSet | None:
|
||||||
|
"""Get current working calibration set"""
|
||||||
|
return self._current_working_set
|
||||||
|
|
||||||
|
def add_calibration_standard(self, standard: CalibrationStandard, sweep_data: SweepData):
|
||||||
|
"""Add calibration standard to current working set"""
|
||||||
|
if self._current_working_set is None:
|
||||||
|
raise RuntimeError("No active calibration set. Call start_new_calibration first.")
|
||||||
|
|
||||||
|
self._current_working_set.add_standard(standard, sweep_data)
|
||||||
|
|
||||||
|
def remove_calibration_standard(self, standard: CalibrationStandard):
|
||||||
|
"""Remove calibration standard from current working set"""
|
||||||
|
if self._current_working_set is None:
|
||||||
|
raise RuntimeError("No active calibration set.")
|
||||||
|
|
||||||
|
self._current_working_set.remove_standard(standard)
|
||||||
|
|
||||||
|
def save_calibration_set(self, calibration_name: str) -> CalibrationSet:
|
||||||
|
"""Save current working calibration set to disk"""
|
||||||
|
if self._current_working_set is None:
|
||||||
|
raise RuntimeError("No active calibration set to save.")
|
||||||
|
|
||||||
|
if not self._current_working_set.is_complete():
|
||||||
|
missing = self._current_working_set.get_missing_standards()
|
||||||
|
raise ValueError(f"Calibration incomplete. Missing standards: {[s.value for s in missing]}")
|
||||||
|
|
||||||
|
preset = self._current_working_set.preset
|
||||||
|
preset_dir = self._get_preset_calibration_dir(preset)
|
||||||
|
calib_dir = preset_dir / calibration_name
|
||||||
|
calib_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# Save each standard
|
||||||
|
for standard, sweep_data in self._current_working_set.standards.items():
|
||||||
|
# Save sweep data as JSON
|
||||||
|
sweep_json = {
|
||||||
|
'sweep_number': sweep_data.sweep_number,
|
||||||
|
'timestamp': sweep_data.timestamp,
|
||||||
|
'points': sweep_data.points,
|
||||||
|
'total_points': sweep_data.total_points
|
||||||
|
}
|
||||||
|
|
||||||
|
file_path = calib_dir / f"{standard.value}.json"
|
||||||
|
with open(file_path, 'w') as f:
|
||||||
|
json.dump(sweep_json, f, indent=2)
|
||||||
|
|
||||||
|
# Save metadata for each standard
|
||||||
|
metadata = {
|
||||||
|
'preset': {
|
||||||
|
'filename': preset.filename,
|
||||||
|
'mode': preset.mode.value,
|
||||||
|
'start_freq': preset.start_freq,
|
||||||
|
'stop_freq': preset.stop_freq,
|
||||||
|
'points': preset.points,
|
||||||
|
'bandwidth': preset.bandwidth
|
||||||
|
},
|
||||||
|
'calibration_name': calibration_name,
|
||||||
|
'standard': standard.value,
|
||||||
|
'sweep_number': sweep_data.sweep_number,
|
||||||
|
'sweep_timestamp': sweep_data.timestamp,
|
||||||
|
'created_timestamp': datetime.now().isoformat(),
|
||||||
|
'total_points': sweep_data.total_points
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata_path = calib_dir / f"{standard.value}_metadata.json"
|
||||||
|
with open(metadata_path, 'w') as f:
|
||||||
|
json.dump(metadata, f, indent=2)
|
||||||
|
|
||||||
|
# Save calibration set metadata
|
||||||
|
set_metadata = {
|
||||||
|
'preset': {
|
||||||
|
'filename': preset.filename,
|
||||||
|
'mode': preset.mode.value,
|
||||||
|
'start_freq': preset.start_freq,
|
||||||
|
'stop_freq': preset.stop_freq,
|
||||||
|
'points': preset.points,
|
||||||
|
'bandwidth': preset.bandwidth
|
||||||
|
},
|
||||||
|
'calibration_name': calibration_name,
|
||||||
|
'standards': [std.value for std in self._current_working_set.standards.keys()],
|
||||||
|
'created_timestamp': datetime.now().isoformat(),
|
||||||
|
'is_complete': True
|
||||||
|
}
|
||||||
|
|
||||||
|
set_metadata_path = calib_dir / "calibration_info.json"
|
||||||
|
with open(set_metadata_path, 'w') as f:
|
||||||
|
json.dump(set_metadata, f, indent=2)
|
||||||
|
|
||||||
|
# Set name for the working set
|
||||||
|
self._current_working_set.name = calibration_name
|
||||||
|
|
||||||
|
return self._current_working_set
|
||||||
|
|
||||||
|
def load_calibration_set(self, preset: ConfigPreset, calibration_name: str) -> CalibrationSet:
|
||||||
|
"""Load existing calibration set from disk"""
|
||||||
|
preset_dir = self._get_preset_calibration_dir(preset)
|
||||||
|
calib_dir = preset_dir / calibration_name
|
||||||
|
|
||||||
|
if not calib_dir.exists():
|
||||||
|
raise FileNotFoundError(f"Calibration not found: {calibration_name}")
|
||||||
|
|
||||||
|
calib_set = CalibrationSet(preset, calibration_name)
|
||||||
|
|
||||||
|
# Load all standard files
|
||||||
|
for standard in CalibrationStandard:
|
||||||
|
file_path = calib_dir / f"{standard.value}.json"
|
||||||
|
if file_path.exists():
|
||||||
|
with open(file_path, 'r') as f:
|
||||||
|
sweep_json = json.load(f)
|
||||||
|
|
||||||
|
sweep_data = SweepData(
|
||||||
|
sweep_number=sweep_json['sweep_number'],
|
||||||
|
timestamp=sweep_json['timestamp'],
|
||||||
|
points=sweep_json['points'],
|
||||||
|
total_points=sweep_json['total_points']
|
||||||
|
)
|
||||||
|
|
||||||
|
calib_set.add_standard(standard, sweep_data)
|
||||||
|
|
||||||
|
return calib_set
|
||||||
|
|
||||||
|
def get_available_calibrations(self, preset: ConfigPreset) -> List[str]:
|
||||||
|
"""Get list of available calibration sets for preset"""
|
||||||
|
preset_dir = self._get_preset_calibration_dir(preset)
|
||||||
|
|
||||||
|
if not preset_dir.exists():
|
||||||
|
return []
|
||||||
|
|
||||||
|
calibrations = []
|
||||||
|
for item in preset_dir.iterdir():
|
||||||
|
if item.is_dir():
|
||||||
|
calibrations.append(item.name)
|
||||||
|
|
||||||
|
return sorted(calibrations)
|
||||||
|
|
||||||
|
def get_calibration_info(self, preset: ConfigPreset, calibration_name: str) -> Dict:
|
||||||
|
"""Get information about specific calibration"""
|
||||||
|
preset_dir = self._get_preset_calibration_dir(preset)
|
||||||
|
calib_dir = preset_dir / calibration_name
|
||||||
|
info_file = calib_dir / "calibration_info.json"
|
||||||
|
|
||||||
|
if info_file.exists():
|
||||||
|
with open(info_file, 'r') as f:
|
||||||
|
return json.load(f)
|
||||||
|
|
||||||
|
# Fallback: scan files
|
||||||
|
standards = {}
|
||||||
|
required_standards = self._get_required_standards(preset.mode)
|
||||||
|
|
||||||
|
for standard in required_standards:
|
||||||
|
file_path = calib_dir / f"{standard.value}.json"
|
||||||
|
standards[standard.value] = file_path.exists()
|
||||||
|
|
||||||
|
return {
|
||||||
|
'calibration_name': calibration_name,
|
||||||
|
'standards': standards,
|
||||||
|
'is_complete': all(standards.values())
|
||||||
|
}
|
||||||
|
|
||||||
|
def set_current_calibration(self, preset: ConfigPreset, calibration_name: str):
|
||||||
|
"""Set current calibration by creating symlink"""
|
||||||
|
preset_dir = self._get_preset_calibration_dir(preset)
|
||||||
|
calib_dir = preset_dir / calibration_name
|
||||||
|
|
||||||
|
if not calib_dir.exists():
|
||||||
|
raise FileNotFoundError(f"Calibration not found: {calibration_name}")
|
||||||
|
|
||||||
|
# Check if calibration is complete
|
||||||
|
info = self.get_calibration_info(preset, calibration_name)
|
||||||
|
if not info.get('is_complete', False):
|
||||||
|
raise ValueError(f"Calibration {calibration_name} is incomplete")
|
||||||
|
|
||||||
|
# Remove existing symlink if present
|
||||||
|
if self.current_calibration_symlink.exists() or self.current_calibration_symlink.is_symlink():
|
||||||
|
self.current_calibration_symlink.unlink()
|
||||||
|
|
||||||
|
# Create new symlink
|
||||||
|
try:
|
||||||
|
relative_path = calib_dir.relative_to(self.calibration_dir)
|
||||||
|
except ValueError:
|
||||||
|
relative_path = calib_dir
|
||||||
|
|
||||||
|
self.current_calibration_symlink.symlink_to(relative_path)
|
||||||
|
|
||||||
|
def get_current_calibration(self) -> CalibrationSet | None:
|
||||||
|
"""Get currently selected calibration as CalibrationSet"""
|
||||||
|
if not self.current_calibration_symlink.exists():
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
target = self.current_calibration_symlink.resolve()
|
||||||
|
calibration_name = target.name
|
||||||
|
preset_name = target.parent.name
|
||||||
|
|
||||||
|
# Parse preset from filename
|
||||||
|
preset = self.preset_manager._parse_filename(f"{preset_name}.bin")
|
||||||
|
|
||||||
|
if preset is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Load and return the calibration set
|
||||||
|
return self.load_calibration_set(preset, calibration_name)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def clear_current_calibration(self):
|
||||||
|
"""Clear current calibration symlink"""
|
||||||
|
if self.current_calibration_symlink.exists() or self.current_calibration_symlink.is_symlink():
|
||||||
|
self.current_calibration_symlink.unlink()
|
||||||
|
|
||||||
|
def delete_calibration(self, preset: ConfigPreset, calibration_name: str):
|
||||||
|
"""Delete calibration set"""
|
||||||
|
preset_dir = self._get_preset_calibration_dir(preset)
|
||||||
|
calib_dir = preset_dir / calibration_name
|
||||||
|
|
||||||
|
if calib_dir.exists():
|
||||||
|
shutil.rmtree(calib_dir)
|
||||||
|
|
||||||
|
def _get_preset_calibration_dir(self, preset: ConfigPreset) -> Path:
|
||||||
|
"""Get calibration directory for specific preset"""
|
||||||
|
preset_dir = self.calibration_dir / preset.filename.replace('.bin', '')
|
||||||
|
preset_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
return preset_dir
|
||||||
|
|
||||||
|
def _get_required_standards(self, mode: VNAMode) -> List[CalibrationStandard]:
|
||||||
|
"""Get required calibration standards for VNA mode"""
|
||||||
|
if mode == VNAMode.S11:
|
||||||
|
return [CalibrationStandard.OPEN, CalibrationStandard.SHORT, CalibrationStandard.LOAD]
|
||||||
|
elif mode == VNAMode.S21:
|
||||||
|
return [CalibrationStandard.THROUGH]
|
||||||
|
return []
|
||||||
129
vna_system/core/settings/preset_manager.py
Normal file
129
vna_system/core/settings/preset_manager.py
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from enum import Enum
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from vna_system.config import config as cfg
|
||||||
|
|
||||||
|
|
||||||
|
class VNAMode(Enum):
|
||||||
|
S11 = "s11"
|
||||||
|
S21 = "s21"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ConfigPreset:
|
||||||
|
filename: str
|
||||||
|
mode: VNAMode
|
||||||
|
start_freq: float | None = None
|
||||||
|
stop_freq: float | None = None
|
||||||
|
points: int | None = None
|
||||||
|
bandwidth: float | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class PresetManager:
|
||||||
|
def __init__(self, binary_input_dir: Path | None = None):
|
||||||
|
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.current_input_symlink = self.binary_input_dir / "current_input.bin"
|
||||||
|
|
||||||
|
self.config_inputs_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
def _parse_filename(self, filename: str) -> ConfigPreset | None:
|
||||||
|
"""Parse configuration parameters from filename like s11_start100_stop8800_points1000_bw1khz.bin"""
|
||||||
|
base_name = Path(filename).stem.lower()
|
||||||
|
|
||||||
|
# Extract mode - must be at the beginning
|
||||||
|
mode = None
|
||||||
|
if base_name.startswith('s11'):
|
||||||
|
mode = VNAMode.S11
|
||||||
|
elif base_name.startswith('s21'):
|
||||||
|
mode = VNAMode.S21
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
preset = ConfigPreset(filename=filename, mode=mode)
|
||||||
|
|
||||||
|
# Extract parameters using regex
|
||||||
|
patterns = {
|
||||||
|
'start': r'start(\d+(?:\.\d+)?)',
|
||||||
|
'stop': r'stop(\d+(?:\.\d+)?)',
|
||||||
|
'points': r'points?(\d+)',
|
||||||
|
'bw': r'bw(\d+(?:\.\d+)?)(hz|khz|mhz)?'
|
||||||
|
}
|
||||||
|
|
||||||
|
for param, pattern in patterns.items():
|
||||||
|
match = re.search(pattern, base_name)
|
||||||
|
if match:
|
||||||
|
value = float(match.group(1))
|
||||||
|
|
||||||
|
if param == 'start':
|
||||||
|
# Assume MHz if no unit specified
|
||||||
|
preset.start_freq = value * 1e6
|
||||||
|
elif param == 'stop':
|
||||||
|
# Assume MHz if no unit specified
|
||||||
|
preset.stop_freq = value * 1e6
|
||||||
|
elif param == 'points':
|
||||||
|
preset.points = int(value)
|
||||||
|
elif param == 'bw':
|
||||||
|
unit = match.group(2) if len(match.groups()) > 1 and match.group(2) else 'hz'
|
||||||
|
if unit == 'khz':
|
||||||
|
value *= 1e3
|
||||||
|
elif unit == 'mhz':
|
||||||
|
value *= 1e6
|
||||||
|
# hz is base unit, no multiplication needed
|
||||||
|
preset.bandwidth = value
|
||||||
|
|
||||||
|
return preset
|
||||||
|
|
||||||
|
def get_available_presets(self) -> List[ConfigPreset]:
|
||||||
|
"""Return list of all available configuration presets"""
|
||||||
|
presets = []
|
||||||
|
|
||||||
|
if not self.config_inputs_dir.exists():
|
||||||
|
return presets
|
||||||
|
for file_path in self.config_inputs_dir.glob("*.bin"):
|
||||||
|
preset = self._parse_filename(file_path.name)
|
||||||
|
if preset is not None:
|
||||||
|
presets.append(preset)
|
||||||
|
|
||||||
|
return sorted(presets, key=lambda x: x.filename)
|
||||||
|
|
||||||
|
def set_current_preset(self, preset: ConfigPreset) -> ConfigPreset:
|
||||||
|
"""Set current configuration by creating symlink to specified preset"""
|
||||||
|
preset_path = self.config_inputs_dir / preset.filename
|
||||||
|
|
||||||
|
if not preset_path.exists():
|
||||||
|
raise FileNotFoundError(f"Preset file not found: {preset.filename}")
|
||||||
|
|
||||||
|
# Remove existing symlink if present
|
||||||
|
if self.current_input_symlink.exists() or self.current_input_symlink.is_symlink():
|
||||||
|
self.current_input_symlink.unlink()
|
||||||
|
|
||||||
|
# Create new symlink
|
||||||
|
try:
|
||||||
|
relative_path = preset_path.relative_to(self.binary_input_dir)
|
||||||
|
except ValueError:
|
||||||
|
relative_path = preset_path
|
||||||
|
|
||||||
|
self.current_input_symlink.symlink_to(relative_path)
|
||||||
|
|
||||||
|
return preset
|
||||||
|
|
||||||
|
def get_current_preset(self) -> ConfigPreset | None:
|
||||||
|
"""Get currently selected configuration preset"""
|
||||||
|
if not self.current_input_symlink.exists():
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
target = self.current_input_symlink.resolve()
|
||||||
|
return self._parse_filename(target.name)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def preset_exists(self, preset: ConfigPreset) -> bool:
|
||||||
|
"""Check if preset file exists"""
|
||||||
|
return (self.config_inputs_dir / preset.filename).exists()
|
||||||
@ -1,352 +1,176 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
|
||||||
import logging
|
import logging
|
||||||
import re
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from datetime import datetime
|
|
||||||
from enum import Enum
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, List, Tuple
|
from typing import Dict, List
|
||||||
|
|
||||||
from vna_system.config import config as cfg
|
from vna_system.config import config as cfg
|
||||||
from vna_system.core.processing.results_storage import ResultsStorage
|
from vna_system.core.acquisition.sweep_buffer import SweepData
|
||||||
|
from .preset_manager import PresetManager, ConfigPreset, VNAMode
|
||||||
|
from .calibration_manager import CalibrationManager, CalibrationSet, CalibrationStandard
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
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:
|
class VNASettingsManager:
|
||||||
"""
|
"""
|
||||||
- Scans config_logs/{S11,S21}/ for available configs
|
Main settings manager that coordinates preset and calibration management.
|
||||||
- Controls current_log.bin symlink (must be a real symlink)
|
|
||||||
- Parses config params from filename
|
Provides high-level interface for:
|
||||||
- Stores per-config calibration in:
|
- Managing configuration presets
|
||||||
calibration/<MODE>/<STEM>/<STANDARD>/<timestamp>_sweepNNNNNN/
|
- Managing calibration data
|
||||||
- copies ALL processor result JSON files for that sweep (and metadata.json if present)
|
- Coordinating between preset selection and calibration
|
||||||
- UI helpers: select S11/S21, calibrate (through/open/short/load) by sweep number
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, base_dir: Path | None = None):
|
||||||
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.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
|
# Initialize sub-managers
|
||||||
(self.cfg_logs_dir / "S11").mkdir(parents=True, exist_ok=True)
|
self.preset_manager = PresetManager(self.base_dir / "binary_input")
|
||||||
(self.cfg_logs_dir / "S21").mkdir(parents=True, exist_ok=True)
|
self.calibration_manager = CalibrationManager(self.preset_manager, self.base_dir)
|
||||||
self.calib_root.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
# Results storage
|
# ---------- Preset Management ----------
|
||||||
self.results = results_storage or ResultsStorage(
|
|
||||||
storage_dir=str(self.base_dir / "processing_results")
|
|
||||||
)
|
|
||||||
|
|
||||||
# ---------- configuration selection & discovery ----------
|
def get_available_presets(self) -> List[ConfigPreset]:
|
||||||
|
"""Get all available configuration presets"""
|
||||||
|
return self.preset_manager.get_available_presets()
|
||||||
|
|
||||||
def list_configs(self, mode: VNAMode | None = None) -> List[LogConfig]:
|
def get_presets_by_mode(self, mode: VNAMode) -> List[ConfigPreset]:
|
||||||
modes = [mode] if mode else [VNAMode.S11, VNAMode.S21]
|
"""Get presets filtered by VNA mode"""
|
||||||
out: List[LogConfig] = []
|
all_presets = self.get_available_presets()
|
||||||
for m in modes:
|
return [p for p in all_presets if p.mode == mode]
|
||||||
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:
|
def set_current_preset(self, preset: ConfigPreset) -> ConfigPreset:
|
||||||
"""
|
"""Set current configuration preset"""
|
||||||
Update current_log.bin symlink to point to config_logs/<MODE>/<filename>.
|
return self.preset_manager.set_current_preset(preset)
|
||||||
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():
|
def get_current_preset(self) -> ConfigPreset | None:
|
||||||
self.current_log.unlink()
|
"""Get currently selected preset"""
|
||||||
|
return self.preset_manager.get_current_preset()
|
||||||
|
|
||||||
# relative link if possible, else absolute (still a symlink)
|
# ---------- Calibration Management ----------
|
||||||
try:
|
|
||||||
rel = target.relative_to(self.current_log.parent)
|
|
||||||
except ValueError:
|
|
||||||
rel = target
|
|
||||||
|
|
||||||
self.current_log.symlink_to(rel)
|
def start_new_calibration(self, preset: ConfigPreset | None = None) -> CalibrationSet:
|
||||||
return self.get_current_config()
|
"""Start new calibration for current or specified preset"""
|
||||||
|
if preset is None:
|
||||||
|
preset = self.get_current_preset()
|
||||||
|
if preset is None:
|
||||||
|
raise RuntimeError("No current preset selected")
|
||||||
|
|
||||||
def get_current_config(self) -> LogConfig:
|
return self.calibration_manager.start_new_calibration(preset)
|
||||||
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) ----------
|
def get_current_working_calibration(self) -> CalibrationSet | None:
|
||||||
|
"""Get current working calibration set (in-progress, not yet saved)"""
|
||||||
|
return self.calibration_manager.get_current_working_set()
|
||||||
|
|
||||||
|
def add_calibration_standard(self, standard: CalibrationStandard, sweep_data: SweepData):
|
||||||
|
"""Add calibration standard to current working set"""
|
||||||
|
self.calibration_manager.add_calibration_standard(standard, sweep_data)
|
||||||
|
|
||||||
|
def remove_calibration_standard(self, standard: CalibrationStandard):
|
||||||
|
"""Remove calibration standard from current working set"""
|
||||||
|
self.calibration_manager.remove_calibration_standard(standard)
|
||||||
|
|
||||||
|
def save_calibration_set(self, calibration_name: str) -> CalibrationSet:
|
||||||
|
"""Save current working calibration set"""
|
||||||
|
return self.calibration_manager.save_calibration_set(calibration_name)
|
||||||
|
|
||||||
|
def get_available_calibrations(self, preset: ConfigPreset | None = None) -> List[str]:
|
||||||
|
"""Get available calibrations for current or specified preset"""
|
||||||
|
if preset is None:
|
||||||
|
preset = self.get_current_preset()
|
||||||
|
if preset is None:
|
||||||
|
return []
|
||||||
|
|
||||||
|
return self.calibration_manager.get_available_calibrations(preset)
|
||||||
|
|
||||||
|
def set_current_calibration(self, calibration_name: str, preset: ConfigPreset | None = None):
|
||||||
|
"""Set current calibration"""
|
||||||
|
if preset is None:
|
||||||
|
preset = self.get_current_preset()
|
||||||
|
if preset is None:
|
||||||
|
raise RuntimeError("No current preset selected")
|
||||||
|
|
||||||
|
self.calibration_manager.set_current_calibration(preset, calibration_name)
|
||||||
|
|
||||||
|
def get_current_calibration(self) -> CalibrationSet | None:
|
||||||
|
"""Get currently selected calibration set (saved and active via symlink)"""
|
||||||
|
return self.calibration_manager.get_current_calibration()
|
||||||
|
|
||||||
|
def get_calibration_info(self, calibration_name: str, preset: ConfigPreset | None = None) -> Dict:
|
||||||
|
"""Get calibration information"""
|
||||||
|
if preset is None:
|
||||||
|
preset = self.get_current_preset()
|
||||||
|
if preset is None:
|
||||||
|
raise RuntimeError("No current preset selected")
|
||||||
|
|
||||||
|
return self.calibration_manager.get_calibration_info(preset, calibration_name)
|
||||||
|
|
||||||
|
# ---------- Combined Status and UI helpers ----------
|
||||||
|
|
||||||
|
def get_status_summary(self) -> Dict[str, object]:
|
||||||
|
"""Get comprehensive status of current configuration and calibration"""
|
||||||
|
current_preset = self.get_current_preset()
|
||||||
|
current_calibration = self.get_current_calibration()
|
||||||
|
working_calibration = self.get_current_working_calibration()
|
||||||
|
|
||||||
|
summary = {
|
||||||
|
"current_preset": None,
|
||||||
|
"current_calibration": None,
|
||||||
|
"working_calibration": None,
|
||||||
|
"available_presets": len(self.get_available_presets()),
|
||||||
|
"available_calibrations": 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if current_preset:
|
||||||
|
summary["current_preset"] = {
|
||||||
|
"filename": current_preset.filename,
|
||||||
|
"mode": current_preset.mode.value,
|
||||||
|
"start_freq": current_preset.start_freq,
|
||||||
|
"stop_freq": current_preset.stop_freq,
|
||||||
|
"points": current_preset.points,
|
||||||
|
"bandwidth": current_preset.bandwidth
|
||||||
|
}
|
||||||
|
summary["available_calibrations"] = len(self.get_available_calibrations(current_preset))
|
||||||
|
|
||||||
|
if current_calibration:
|
||||||
|
summary["current_calibration"] = {
|
||||||
|
"preset_filename": current_calibration.preset.filename,
|
||||||
|
"calibration_name": current_calibration.name
|
||||||
|
}
|
||||||
|
|
||||||
|
if working_calibration:
|
||||||
|
completed, total = working_calibration.get_progress()
|
||||||
|
summary["working_calibration"] = {
|
||||||
|
"preset_filename": working_calibration.preset.filename,
|
||||||
|
"progress": f"{completed}/{total}",
|
||||||
|
"is_complete": working_calibration.is_complete(),
|
||||||
|
"missing_standards": [s.value for s in working_calibration.get_missing_standards()]
|
||||||
|
}
|
||||||
|
|
||||||
|
return summary
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def required_standards(mode: VNAMode) -> List[CalibrationStandard]:
|
def get_required_standards(mode: VNAMode) -> List[CalibrationStandard]:
|
||||||
return (
|
"""Get required calibration standards for VNA mode"""
|
||||||
[CalibrationStandard.OPEN, CalibrationStandard.SHORT, CalibrationStandard.LOAD]
|
if mode == VNAMode.S11:
|
||||||
if mode == VNAMode.S11
|
return [CalibrationStandard.OPEN, CalibrationStandard.SHORT, CalibrationStandard.LOAD]
|
||||||
else [CalibrationStandard.THROUGH]
|
elif mode == VNAMode.S21:
|
||||||
)
|
return [CalibrationStandard.THROUGH]
|
||||||
|
return []
|
||||||
|
|
||||||
def _calib_dir(self, cfg: LogConfig, standard: CalibrationStandard | None = None) -> Path:
|
# ---------- Integration with VNADataAcquisition ----------
|
||||||
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:
|
def capture_calibration_standard_from_acquisition(self, standard: CalibrationStandard, data_acquisition):
|
||||||
"""
|
"""Capture calibration standard from VNADataAcquisition instance"""
|
||||||
calibration/<MODE>/<STEM>/<STANDARD>/<timestamp>_sweepNNNNNN/
|
# Get latest sweep from acquisition
|
||||||
"""
|
latest_sweep = data_acquisition._sweep_buffer.get_latest_sweep()
|
||||||
ts = ts or datetime.now().strftime("%Y%m%d_%H%M%S")
|
if latest_sweep is None:
|
||||||
d = self._calib_dir(cfg, standard) / f"{ts}_sweep{sweep_number:06d}"
|
raise RuntimeError("No sweep data available in acquisition buffer")
|
||||||
d.mkdir(parents=True, exist_ok=True)
|
|
||||||
return d
|
|
||||||
|
|
||||||
def record_calibration_from_sweep(
|
# Add to current working calibration
|
||||||
self,
|
self.add_calibration_standard(standard, latest_sweep)
|
||||||
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
|
logger.info(f"Captured {standard.value} calibration standard from sweep {latest_sweep.sweep_number}")
|
||||||
results = self.results.get_result_by_sweep(sweep_number, processor_name=None)
|
return latest_sweep.sweep_number
|
||||||
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()
|
|
||||||
492
vna_system/web_ui/static/css/settings.css
Normal file
492
vna_system/web_ui/static/css/settings.css
Normal file
@ -0,0 +1,492 @@
|
|||||||
|
/* ===================================================================
|
||||||
|
Settings Page Styles - Professional Design
|
||||||
|
=================================================================== */
|
||||||
|
|
||||||
|
/* Settings Container */
|
||||||
|
.settings-container {
|
||||||
|
max-width: var(--max-content-width);
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: var(--space-8);
|
||||||
|
background: var(--bg-primary);
|
||||||
|
min-height: calc(100vh - var(--header-height));
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: var(--space-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-4);
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: var(--font-size-3xl);
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
margin: 0 0 var(--space-8) 0;
|
||||||
|
text-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-icon {
|
||||||
|
width: 2.5rem;
|
||||||
|
height: 2.5rem;
|
||||||
|
color: var(--color-primary-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Professional Cards */
|
||||||
|
.settings-card {
|
||||||
|
background: var(--bg-surface);
|
||||||
|
border: 1px solid var(--border-primary);
|
||||||
|
border-radius: var(--radius-xl);
|
||||||
|
padding: var(--space-8);
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
position: relative;
|
||||||
|
transition: all var(--transition-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-card::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 4px;
|
||||||
|
background: linear-gradient(90deg, var(--color-primary-500), var(--color-primary-600));
|
||||||
|
border-radius: var(--radius-xl) var(--radius-xl) 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: var(--shadow-xl);
|
||||||
|
border-color: var(--border-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-card-title {
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: var(--font-size-xl);
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
margin: 0 0 var(--space-3) 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-card-title::before {
|
||||||
|
content: '';
|
||||||
|
width: 6px;
|
||||||
|
height: 24px;
|
||||||
|
background: linear-gradient(135deg, var(--color-primary-500), var(--color-primary-600));
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-card-description {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
margin: 0 0 var(--space-8) 0;
|
||||||
|
line-height: var(--line-height-relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Enhanced Preset Controls */
|
||||||
|
.preset-controls {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-group {
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
border: 1px solid var(--border-primary);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: var(--space-6);
|
||||||
|
transition: all var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-group:hover {
|
||||||
|
border-color: var(--border-secondary);
|
||||||
|
background: var(--bg-surface-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-label {
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
margin-bottom: var(--space-4);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-label::before {
|
||||||
|
content: '';
|
||||||
|
width: 4px;
|
||||||
|
height: 4px;
|
||||||
|
background: var(--color-primary-500);
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preset-selector {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-4);
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preset-dropdown,
|
||||||
|
.calibration-dropdown {
|
||||||
|
flex: 1;
|
||||||
|
padding: var(--space-3) var(--space-4);
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border: 1px solid var(--border-primary);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
min-height: 3rem;
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
transition: all var(--transition-fast);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preset-dropdown:focus,
|
||||||
|
.calibration-dropdown:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--color-primary-500);
|
||||||
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||||
|
background: var(--bg-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preset-dropdown:hover,
|
||||||
|
.calibration-dropdown:hover {
|
||||||
|
border-color: var(--color-primary-500);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preset-dropdown:disabled,
|
||||||
|
.calibration-dropdown:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preset-info {
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
border: 1px solid var(--border-primary);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preset-details {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preset-param {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-label {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-value {
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Calibration Status */
|
||||||
|
.calibration-status {
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
border: 1px solid var(--border-primary);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: var(--space-4);
|
||||||
|
margin-bottom: var(--space-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: var(--space-2) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-label {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-value {
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Calibration Workflow */
|
||||||
|
.calibration-workflow {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-section {
|
||||||
|
border: 1px solid var(--border-primary);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: var(--space-6);
|
||||||
|
background: var(--bg-surface);
|
||||||
|
transition: all var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-section:hover {
|
||||||
|
border-color: var(--border-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-title {
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
margin: 0 0 var(--space-4) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-3);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Calibration Progress */
|
||||||
|
.calibration-progress {
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
border: 1px solid var(--border-primary);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: var(--space-4);
|
||||||
|
margin-bottom: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-info {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-text {
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Calibration Standards */
|
||||||
|
.calibration-standards {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--space-3);
|
||||||
|
margin-bottom: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calibration-standard-btn {
|
||||||
|
min-width: 120px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calibration-standard-btn i {
|
||||||
|
margin-right: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calibration-standard-btn:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Calibration Actions */
|
||||||
|
.calibration-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-3);
|
||||||
|
align-items: stretch;
|
||||||
|
margin-top: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calibration-name-input {
|
||||||
|
flex: 1;
|
||||||
|
padding: var(--space-3) var(--space-4);
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border: 1px solid var(--border-primary);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
min-height: 2.5rem;
|
||||||
|
transition: all var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calibration-name-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--color-primary-500);
|
||||||
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calibration-name-input:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calibration-name-input::placeholder {
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Existing Calibrations */
|
||||||
|
.existing-calibrations {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-3);
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status Grid */
|
||||||
|
.status-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-grid .status-item {
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
border: 1px solid var(--border-primary);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: var(--space-4);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-1);
|
||||||
|
transition: all var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-grid .status-item:hover {
|
||||||
|
background: var(--bg-surface-hover);
|
||||||
|
border-color: var(--border-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-grid .status-label {
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-grid .status-value {
|
||||||
|
font-size: var(--font-size-lg);
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Design */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.settings-container {
|
||||||
|
padding: var(--spacing-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preset-selector {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calibration-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.existing-calibrations {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calibration-standards {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calibration-standard-btn {
|
||||||
|
min-width: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animation and Transitions */
|
||||||
|
.settings-card {
|
||||||
|
transition: box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-card:hover {
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-section {
|
||||||
|
transition: border-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-section:hover {
|
||||||
|
border-color: var(--color-primary-alpha);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading States */
|
||||||
|
.btn:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.loading {
|
||||||
|
position: relative;
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.loading::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
margin: -8px 0 0 -8px;
|
||||||
|
border: 2px solid currentColor;
|
||||||
|
border-right-color: transparent;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Success/Error States */
|
||||||
|
.preset-param .param-value.success {
|
||||||
|
color: var(--color-success-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preset-param .param-value.error {
|
||||||
|
color: var(--color-error-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preset-param .param-value.warning {
|
||||||
|
color: var(--color-warning-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Calibration standard button states */
|
||||||
|
.calibration-standard-btn.btn--success {
|
||||||
|
background-color: var(--color-success-500);
|
||||||
|
border-color: var(--color-success-500);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calibration-standard-btn.btn--success:hover:not(:disabled) {
|
||||||
|
background-color: var(--color-success-600);
|
||||||
|
border-color: var(--color-success-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calibration-standard-btn.btn--warning {
|
||||||
|
background-color: var(--color-warning-500);
|
||||||
|
border-color: var(--color-warning-500);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calibration-standard-btn.btn--warning:hover:not(:disabled) {
|
||||||
|
background-color: var(--color-warning-600);
|
||||||
|
border-color: var(--color-warning-600);
|
||||||
|
}
|
||||||
@ -8,6 +8,7 @@ import { ChartManager } from './modules/charts.js';
|
|||||||
import { UIManager } from './modules/ui.js';
|
import { UIManager } from './modules/ui.js';
|
||||||
import { NotificationManager } from './modules/notifications.js';
|
import { NotificationManager } from './modules/notifications.js';
|
||||||
import { StorageManager } from './modules/storage.js';
|
import { StorageManager } from './modules/storage.js';
|
||||||
|
import { SettingsManager } from './modules/settings.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main Application Class
|
* Main Application Class
|
||||||
@ -40,6 +41,7 @@ class VNADashboard {
|
|||||||
this.ui = new UIManager(this.notifications);
|
this.ui = new UIManager(this.notifications);
|
||||||
this.charts = new ChartManager(this.config.charts, this.notifications);
|
this.charts = new ChartManager(this.config.charts, this.notifications);
|
||||||
this.websocket = new WebSocketManager(this.config.websocket, this.notifications);
|
this.websocket = new WebSocketManager(this.config.websocket, this.notifications);
|
||||||
|
this.settings = new SettingsManager(this.notifications);
|
||||||
|
|
||||||
// Bind methods
|
// Bind methods
|
||||||
this.handleWebSocketData = this.handleWebSocketData.bind(this);
|
this.handleWebSocketData = this.handleWebSocketData.bind(this);
|
||||||
@ -98,6 +100,9 @@ class VNADashboard {
|
|||||||
// Initialize chart manager
|
// Initialize chart manager
|
||||||
await this.charts.init();
|
await this.charts.init();
|
||||||
|
|
||||||
|
// Initialize settings manager
|
||||||
|
await this.settings.init();
|
||||||
|
|
||||||
// Set up UI event handlers
|
// Set up UI event handlers
|
||||||
this.setupUIHandlers();
|
this.setupUIHandlers();
|
||||||
|
|
||||||
@ -144,6 +149,11 @@ class VNADashboard {
|
|||||||
// Navigation
|
// Navigation
|
||||||
this.ui.onViewChange((view) => {
|
this.ui.onViewChange((view) => {
|
||||||
console.log(`📱 Switched to view: ${view}`);
|
console.log(`📱 Switched to view: ${view}`);
|
||||||
|
|
||||||
|
// Refresh settings when switching to settings view
|
||||||
|
if (view === 'settings') {
|
||||||
|
this.settings.refresh();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Processor toggles
|
// Processor toggles
|
||||||
@ -347,6 +357,7 @@ class VNADashboard {
|
|||||||
|
|
||||||
// Cleanup managers
|
// Cleanup managers
|
||||||
this.charts.destroy();
|
this.charts.destroy();
|
||||||
|
this.settings.destroy();
|
||||||
this.ui.destroy();
|
this.ui.destroy();
|
||||||
this.notifications.destroy();
|
this.notifications.destroy();
|
||||||
|
|
||||||
@ -389,6 +400,7 @@ if (typeof process !== 'undefined' && process?.env?.NODE_ENV === 'development')
|
|||||||
dashboard: () => window.vnaDashboard,
|
dashboard: () => window.vnaDashboard,
|
||||||
websocket: () => window.vnaDashboard?.websocket,
|
websocket: () => window.vnaDashboard?.websocket,
|
||||||
charts: () => window.vnaDashboard?.charts,
|
charts: () => window.vnaDashboard?.charts,
|
||||||
ui: () => window.vnaDashboard?.ui
|
ui: () => window.vnaDashboard?.ui,
|
||||||
|
settings: () => window.vnaDashboard?.settings
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
585
vna_system/web_ui/static/js/modules/settings.js
Normal file
585
vna_system/web_ui/static/js/modules/settings.js
Normal file
@ -0,0 +1,585 @@
|
|||||||
|
/**
|
||||||
|
* Settings Manager Module
|
||||||
|
* Handles VNA configuration presets and calibration management
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class SettingsManager {
|
||||||
|
constructor(notifications) {
|
||||||
|
this.notifications = notifications;
|
||||||
|
this.isInitialized = false;
|
||||||
|
this.currentPreset = null;
|
||||||
|
this.currentCalibration = null;
|
||||||
|
this.workingCalibration = null;
|
||||||
|
|
||||||
|
// DOM elements will be populated during init
|
||||||
|
this.elements = {};
|
||||||
|
|
||||||
|
// Bind methods
|
||||||
|
this.handlePresetChange = this.handlePresetChange.bind(this);
|
||||||
|
this.handleSetPreset = this.handleSetPreset.bind(this);
|
||||||
|
this.handleStartCalibration = this.handleStartCalibration.bind(this);
|
||||||
|
this.handleCalibrateStandard = this.handleCalibrateStandard.bind(this);
|
||||||
|
this.handleSaveCalibration = this.handleSaveCalibration.bind(this);
|
||||||
|
this.handleSetCalibration = this.handleSetCalibration.bind(this);
|
||||||
|
this.handleCalibrationChange = this.handleCalibrationChange.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
try {
|
||||||
|
console.log('🔧 Initializing Settings Manager...');
|
||||||
|
|
||||||
|
// Get DOM elements
|
||||||
|
this.initializeElements();
|
||||||
|
|
||||||
|
// Set up event listeners
|
||||||
|
this.setupEventListeners();
|
||||||
|
|
||||||
|
// Load initial data
|
||||||
|
await this.loadInitialData();
|
||||||
|
|
||||||
|
this.isInitialized = true;
|
||||||
|
console.log('✅ Settings Manager initialized');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Settings Manager initialization failed:', error);
|
||||||
|
this.notifications.show({
|
||||||
|
type: 'error',
|
||||||
|
title: 'Settings Error',
|
||||||
|
message: 'Failed to initialize settings'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeElements() {
|
||||||
|
this.elements = {
|
||||||
|
// Preset elements
|
||||||
|
presetDropdown: document.getElementById('presetDropdown'),
|
||||||
|
setPresetBtn: document.getElementById('setPresetBtn'),
|
||||||
|
currentPreset: document.getElementById('currentPreset'),
|
||||||
|
|
||||||
|
// Calibration elements
|
||||||
|
currentCalibration: document.getElementById('currentCalibration'),
|
||||||
|
startCalibrationBtn: document.getElementById('startCalibrationBtn'),
|
||||||
|
calibrationSteps: document.getElementById('calibrationSteps'),
|
||||||
|
calibrationStandards: document.getElementById('calibrationStandards'),
|
||||||
|
progressText: document.getElementById('progressText'),
|
||||||
|
calibrationNameInput: document.getElementById('calibrationNameInput'),
|
||||||
|
saveCalibrationBtn: document.getElementById('saveCalibrationBtn'),
|
||||||
|
calibrationDropdown: document.getElementById('calibrationDropdown'),
|
||||||
|
setCalibrationBtn: document.getElementById('setCalibrationBtn'),
|
||||||
|
|
||||||
|
// Status elements
|
||||||
|
presetCount: document.getElementById('presetCount'),
|
||||||
|
calibrationCount: document.getElementById('calibrationCount'),
|
||||||
|
systemStatus: document.getElementById('systemStatus')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
setupEventListeners() {
|
||||||
|
// Preset controls
|
||||||
|
this.elements.presetDropdown?.addEventListener('change', this.handlePresetChange);
|
||||||
|
this.elements.setPresetBtn?.addEventListener('click', this.handleSetPreset);
|
||||||
|
|
||||||
|
// Calibration controls
|
||||||
|
this.elements.startCalibrationBtn?.addEventListener('click', this.handleStartCalibration);
|
||||||
|
this.elements.saveCalibrationBtn?.addEventListener('click', this.handleSaveCalibration);
|
||||||
|
this.elements.calibrationDropdown?.addEventListener('change', this.handleCalibrationChange);
|
||||||
|
this.elements.setCalibrationBtn?.addEventListener('click', this.handleSetCalibration);
|
||||||
|
|
||||||
|
// Calibration name input
|
||||||
|
this.elements.calibrationNameInput?.addEventListener('input', () => {
|
||||||
|
const hasName = this.elements.calibrationNameInput.value.trim().length > 0;
|
||||||
|
const isComplete = this.workingCalibration && this.workingCalibration.is_complete;
|
||||||
|
this.elements.saveCalibrationBtn.disabled = !hasName || !isComplete;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadInitialData() {
|
||||||
|
await Promise.all([
|
||||||
|
this.loadPresets(),
|
||||||
|
this.loadStatus(),
|
||||||
|
this.loadWorkingCalibration()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadPresets() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/v1/settings/presets');
|
||||||
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||||
|
|
||||||
|
const presets = await response.json();
|
||||||
|
this.populatePresetDropdown(presets);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load presets:', error);
|
||||||
|
this.notifications.show({
|
||||||
|
type: 'error',
|
||||||
|
title: 'Load Error',
|
||||||
|
message: 'Failed to load configuration presets'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadStatus() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/v1/settings/status');
|
||||||
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||||
|
|
||||||
|
const status = await response.json();
|
||||||
|
this.updateStatusDisplay(status);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load status:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadWorkingCalibration() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/v1/settings/working-calibration');
|
||||||
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||||
|
|
||||||
|
const workingCalibration = await response.json();
|
||||||
|
this.updateWorkingCalibration(workingCalibration);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load working calibration:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadCalibrations() {
|
||||||
|
if (!this.currentPreset) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/v1/settings/calibrations?preset_filename=${this.currentPreset.filename}`);
|
||||||
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||||
|
|
||||||
|
const calibrations = await response.json();
|
||||||
|
this.populateCalibrationDropdown(calibrations);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load calibrations:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
populatePresetDropdown(presets) {
|
||||||
|
const dropdown = this.elements.presetDropdown;
|
||||||
|
dropdown.innerHTML = '';
|
||||||
|
|
||||||
|
if (presets.length === 0) {
|
||||||
|
dropdown.innerHTML = '<option value="">No presets available</option>';
|
||||||
|
dropdown.disabled = true;
|
||||||
|
this.elements.setPresetBtn.disabled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dropdown.innerHTML = '<option value="">Select preset...</option>';
|
||||||
|
presets.forEach(preset => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = preset.filename;
|
||||||
|
option.textContent = this.formatPresetDisplay(preset);
|
||||||
|
dropdown.appendChild(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
dropdown.disabled = false;
|
||||||
|
this.elements.setPresetBtn.disabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
populateCalibrationDropdown(calibrations) {
|
||||||
|
const dropdown = this.elements.calibrationDropdown;
|
||||||
|
dropdown.innerHTML = '';
|
||||||
|
|
||||||
|
if (calibrations.length === 0) {
|
||||||
|
dropdown.innerHTML = '<option value="">No calibrations available</option>';
|
||||||
|
dropdown.disabled = true;
|
||||||
|
this.elements.setCalibrationBtn.disabled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dropdown.innerHTML = '<option value="">Select calibration...</option>';
|
||||||
|
calibrations.forEach(calibration => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = calibration.name;
|
||||||
|
option.textContent = `${calibration.name} ${calibration.is_complete ? '✓' : '⚠'}`;
|
||||||
|
dropdown.appendChild(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
dropdown.disabled = false;
|
||||||
|
this.elements.setCalibrationBtn.disabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
formatPresetDisplay(preset) {
|
||||||
|
let display = `${preset.filename} (${preset.mode})`;
|
||||||
|
|
||||||
|
if (preset.start_freq && preset.stop_freq) {
|
||||||
|
const startMHz = (preset.start_freq / 1e6).toFixed(0);
|
||||||
|
const stopMHz = (preset.stop_freq / 1e6).toFixed(0);
|
||||||
|
display += ` - ${startMHz}-${stopMHz}MHz`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preset.points) {
|
||||||
|
display += `, ${preset.points}pts`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return display;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStatusDisplay(status) {
|
||||||
|
// Update current preset
|
||||||
|
if (status.current_preset) {
|
||||||
|
this.currentPreset = status.current_preset;
|
||||||
|
this.elements.currentPreset.textContent = status.current_preset.filename;
|
||||||
|
this.elements.startCalibrationBtn.disabled = false;
|
||||||
|
|
||||||
|
// Load calibrations for current preset
|
||||||
|
this.loadCalibrations();
|
||||||
|
} else {
|
||||||
|
this.currentPreset = null;
|
||||||
|
this.elements.currentPreset.textContent = 'None';
|
||||||
|
this.elements.startCalibrationBtn.disabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update current calibration
|
||||||
|
if (status.current_calibration) {
|
||||||
|
this.currentCalibration = status.current_calibration;
|
||||||
|
this.elements.currentCalibration.textContent = status.current_calibration.calibration_name;
|
||||||
|
} else {
|
||||||
|
this.currentCalibration = null;
|
||||||
|
this.elements.currentCalibration.textContent = 'None';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update counts
|
||||||
|
this.elements.presetCount.textContent = status.available_presets || 0;
|
||||||
|
this.elements.calibrationCount.textContent = status.available_calibrations || 0;
|
||||||
|
this.elements.systemStatus.textContent = 'Ready';
|
||||||
|
}
|
||||||
|
|
||||||
|
updateWorkingCalibration(workingCalibration) {
|
||||||
|
this.workingCalibration = workingCalibration;
|
||||||
|
|
||||||
|
if (workingCalibration.active) {
|
||||||
|
this.showCalibrationSteps(workingCalibration);
|
||||||
|
} else {
|
||||||
|
this.hideCalibrationSteps();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showCalibrationSteps(workingCalibration) {
|
||||||
|
this.elements.calibrationSteps.style.display = 'block';
|
||||||
|
this.elements.progressText.textContent = workingCalibration.progress || '0/0';
|
||||||
|
|
||||||
|
// Generate standard buttons
|
||||||
|
this.generateStandardButtons(workingCalibration);
|
||||||
|
|
||||||
|
// Update save button state
|
||||||
|
const hasName = this.elements.calibrationNameInput.value.trim().length > 0;
|
||||||
|
this.elements.saveCalibrationBtn.disabled = !hasName || !workingCalibration.is_complete;
|
||||||
|
this.elements.calibrationNameInput.disabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
hideCalibrationSteps() {
|
||||||
|
this.elements.calibrationSteps.style.display = 'none';
|
||||||
|
this.elements.calibrationStandards.innerHTML = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
generateStandardButtons(workingCalibration) {
|
||||||
|
const container = this.elements.calibrationStandards;
|
||||||
|
container.innerHTML = '';
|
||||||
|
|
||||||
|
const allStandards = this.getAllStandardsForMode();
|
||||||
|
const completedStandards = workingCalibration.completed_standards || [];
|
||||||
|
const missingStandards = workingCalibration.missing_standards || [];
|
||||||
|
|
||||||
|
allStandards.forEach(standard => {
|
||||||
|
const button = document.createElement('button');
|
||||||
|
button.className = 'btn calibration-standard-btn';
|
||||||
|
button.dataset.standard = standard;
|
||||||
|
|
||||||
|
const isCompleted = completedStandards.includes(standard);
|
||||||
|
const isMissing = missingStandards.includes(standard);
|
||||||
|
|
||||||
|
if (isCompleted) {
|
||||||
|
button.classList.add('btn--success');
|
||||||
|
button.innerHTML = `<i data-lucide="check"></i> ${standard.toUpperCase()} ✓`;
|
||||||
|
button.disabled = false;
|
||||||
|
button.title = 'Click to recapture this standard';
|
||||||
|
} else if (isMissing) {
|
||||||
|
button.classList.add('btn--primary');
|
||||||
|
button.innerHTML = `<i data-lucide="radio"></i> Capture ${standard.toUpperCase()}`;
|
||||||
|
button.disabled = false;
|
||||||
|
button.title = 'Click to capture this standard';
|
||||||
|
} else {
|
||||||
|
button.classList.add('btn--secondary');
|
||||||
|
button.innerHTML = `${standard.toUpperCase()}`;
|
||||||
|
button.disabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.addEventListener('click', () => this.handleCalibrateStandard(standard));
|
||||||
|
container.appendChild(button);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Re-initialize lucide icons for new buttons
|
||||||
|
if (typeof lucide !== 'undefined') {
|
||||||
|
lucide.createIcons();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getAllStandardsForMode() {
|
||||||
|
if (!this.currentPreset) return [];
|
||||||
|
|
||||||
|
if (this.currentPreset.mode === 's11') {
|
||||||
|
return ['open', 'short', 'load'];
|
||||||
|
} else if (this.currentPreset.mode === 's21') {
|
||||||
|
return ['through'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event handlers
|
||||||
|
handlePresetChange() {
|
||||||
|
const selectedValue = this.elements.presetDropdown.value;
|
||||||
|
this.elements.setPresetBtn.disabled = !selectedValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleSetPreset() {
|
||||||
|
const filename = this.elements.presetDropdown.value;
|
||||||
|
if (!filename) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.elements.setPresetBtn.disabled = true;
|
||||||
|
this.elements.setPresetBtn.textContent = 'Setting...';
|
||||||
|
|
||||||
|
const response = await fetch('/api/v1/settings/preset/set', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ filename })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
this.notifications.show({
|
||||||
|
type: 'success',
|
||||||
|
title: 'Preset Set',
|
||||||
|
message: result.message
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reload status
|
||||||
|
await this.loadStatus();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to set preset:', error);
|
||||||
|
this.notifications.show({
|
||||||
|
type: 'error',
|
||||||
|
title: 'Preset Error',
|
||||||
|
message: 'Failed to set configuration preset'
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
this.elements.setPresetBtn.disabled = false;
|
||||||
|
this.elements.setPresetBtn.innerHTML = '<i data-lucide="check"></i> Set Active';
|
||||||
|
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleStartCalibration() {
|
||||||
|
if (!this.currentPreset) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.elements.startCalibrationBtn.disabled = true;
|
||||||
|
this.elements.startCalibrationBtn.textContent = 'Starting...';
|
||||||
|
|
||||||
|
const response = await fetch('/api/v1/settings/calibration/start', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ preset_filename: this.currentPreset.filename })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
this.notifications.show({
|
||||||
|
type: 'info',
|
||||||
|
title: 'Calibration Started',
|
||||||
|
message: `Started calibration for ${result.preset}`
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reload working calibration
|
||||||
|
await this.loadWorkingCalibration();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to start calibration:', error);
|
||||||
|
this.notifications.show({
|
||||||
|
type: 'error',
|
||||||
|
title: 'Calibration Error',
|
||||||
|
message: 'Failed to start calibration'
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
this.elements.startCalibrationBtn.disabled = false;
|
||||||
|
this.elements.startCalibrationBtn.innerHTML = '<i data-lucide="play"></i> Start Calibration';
|
||||||
|
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleCalibrateStandard(standard) {
|
||||||
|
try {
|
||||||
|
const button = document.querySelector(`[data-standard="${standard}"]`);
|
||||||
|
if (button) {
|
||||||
|
button.disabled = true;
|
||||||
|
button.textContent = 'Capturing...';
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch('/api/v1/settings/calibration/add-standard', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ standard })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
this.notifications.show({
|
||||||
|
type: 'success',
|
||||||
|
title: 'Standard Captured',
|
||||||
|
message: result.message
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reload working calibration
|
||||||
|
await this.loadWorkingCalibration();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to capture standard:', error);
|
||||||
|
this.notifications.show({
|
||||||
|
type: 'error',
|
||||||
|
title: 'Calibration Error',
|
||||||
|
message: 'Failed to capture calibration standard'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Re-enable button
|
||||||
|
const button = document.querySelector(`[data-standard="${standard}"]`);
|
||||||
|
if (button) {
|
||||||
|
button.disabled = false;
|
||||||
|
this.generateStandardButtons(this.workingCalibration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleSaveCalibration() {
|
||||||
|
const name = this.elements.calibrationNameInput.value.trim();
|
||||||
|
if (!name) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.elements.saveCalibrationBtn.disabled = true;
|
||||||
|
this.elements.saveCalibrationBtn.textContent = 'Saving...';
|
||||||
|
|
||||||
|
const response = await fetch('/api/v1/settings/calibration/save', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ name })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
this.notifications.show({
|
||||||
|
type: 'success',
|
||||||
|
title: 'Calibration Saved',
|
||||||
|
message: result.message
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear working calibration
|
||||||
|
this.hideCalibrationSteps();
|
||||||
|
this.elements.calibrationNameInput.value = '';
|
||||||
|
|
||||||
|
// Reload data
|
||||||
|
await Promise.all([
|
||||||
|
this.loadStatus(),
|
||||||
|
this.loadWorkingCalibration(),
|
||||||
|
this.loadCalibrations()
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to save calibration:', error);
|
||||||
|
this.notifications.show({
|
||||||
|
type: 'error',
|
||||||
|
title: 'Calibration Error',
|
||||||
|
message: 'Failed to save calibration'
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
this.elements.saveCalibrationBtn.disabled = true;
|
||||||
|
this.elements.saveCalibrationBtn.innerHTML = '<i data-lucide="save"></i> Save Calibration';
|
||||||
|
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCalibrationChange() {
|
||||||
|
const selectedValue = this.elements.calibrationDropdown.value;
|
||||||
|
this.elements.setCalibrationBtn.disabled = !selectedValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleSetCalibration() {
|
||||||
|
const name = this.elements.calibrationDropdown.value;
|
||||||
|
if (!name || !this.currentPreset) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.elements.setCalibrationBtn.disabled = true;
|
||||||
|
this.elements.setCalibrationBtn.textContent = 'Setting...';
|
||||||
|
|
||||||
|
const response = await fetch('/api/v1/settings/calibration/set', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
name,
|
||||||
|
preset_filename: this.currentPreset.filename
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
this.notifications.show({
|
||||||
|
type: 'success',
|
||||||
|
title: 'Calibration Set',
|
||||||
|
message: result.message
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reload status
|
||||||
|
await this.loadStatus();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to set calibration:', error);
|
||||||
|
this.notifications.show({
|
||||||
|
type: 'error',
|
||||||
|
title: 'Calibration Error',
|
||||||
|
message: 'Failed to set active calibration'
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
this.elements.setCalibrationBtn.disabled = false;
|
||||||
|
this.elements.setCalibrationBtn.innerHTML = '<i data-lucide="check"></i> Set Active';
|
||||||
|
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public methods for external use
|
||||||
|
async refresh() {
|
||||||
|
if (!this.isInitialized) return;
|
||||||
|
|
||||||
|
await this.loadInitialData();
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
// Remove event listeners
|
||||||
|
this.elements.presetDropdown?.removeEventListener('change', this.handlePresetChange);
|
||||||
|
this.elements.setPresetBtn?.removeEventListener('click', this.handleSetPreset);
|
||||||
|
this.elements.startCalibrationBtn?.removeEventListener('click', this.handleStartCalibration);
|
||||||
|
this.elements.saveCalibrationBtn?.removeEventListener('click', this.handleSaveCalibration);
|
||||||
|
this.elements.calibrationDropdown?.removeEventListener('change', this.handleCalibrationChange);
|
||||||
|
this.elements.setCalibrationBtn?.removeEventListener('click', this.handleSetCalibration);
|
||||||
|
|
||||||
|
this.isInitialized = false;
|
||||||
|
console.log('🧹 Settings Manager destroyed');
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -31,6 +31,7 @@
|
|||||||
<link rel="stylesheet" href="/static/css/layout.css">
|
<link rel="stylesheet" href="/static/css/layout.css">
|
||||||
<link rel="stylesheet" href="/static/css/components.css">
|
<link rel="stylesheet" href="/static/css/components.css">
|
||||||
<link rel="stylesheet" href="/static/css/charts.css">
|
<link rel="stylesheet" href="/static/css/charts.css">
|
||||||
|
<link rel="stylesheet" href="/static/css/settings.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
@ -123,8 +124,125 @@
|
|||||||
<!-- Settings View -->
|
<!-- Settings View -->
|
||||||
<div class="view" id="settingsView">
|
<div class="view" id="settingsView">
|
||||||
<div class="settings-container">
|
<div class="settings-container">
|
||||||
<h2>Settings</h2>
|
<div class="settings-section">
|
||||||
<p>Settings panel will be implemented in future versions.</p>
|
<h2 class="settings-title">
|
||||||
|
<i data-lucide="settings" class="settings-icon"></i>
|
||||||
|
VNA Settings
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<!-- Configuration Presets Section -->
|
||||||
|
<div class="settings-card">
|
||||||
|
<h3 class="settings-card-title">Configuration Presets</h3>
|
||||||
|
<p class="settings-card-description">Select measurement configuration from available presets</p>
|
||||||
|
|
||||||
|
<div class="preset-controls">
|
||||||
|
<div class="control-group">
|
||||||
|
<label class="control-label">Available Presets:</label>
|
||||||
|
<div class="preset-selector" id="presetSelector">
|
||||||
|
<select class="preset-dropdown" id="presetDropdown" disabled>
|
||||||
|
<option value="">Loading presets...</option>
|
||||||
|
</select>
|
||||||
|
<button class="btn btn--primary" id="setPresetBtn" disabled>
|
||||||
|
<i data-lucide="check"></i>
|
||||||
|
Set Active
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="preset-info" id="presetInfo">
|
||||||
|
<div class="preset-details">
|
||||||
|
<div class="preset-param">
|
||||||
|
<span class="param-label">Current:</span>
|
||||||
|
<span class="param-value" id="currentPreset">None</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Calibration Section -->
|
||||||
|
<div class="settings-card">
|
||||||
|
<h3 class="settings-card-title">Calibration Management</h3>
|
||||||
|
<p class="settings-card-description">Manage calibration data for accurate measurements</p>
|
||||||
|
|
||||||
|
<!-- Current Calibration Status -->
|
||||||
|
<div class="calibration-status" id="calibrationStatus">
|
||||||
|
<div class="status-item">
|
||||||
|
<span class="status-label">Current Calibration:</span>
|
||||||
|
<span class="status-value" id="currentCalibration">None</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Calibration Workflow -->
|
||||||
|
<div class="calibration-workflow">
|
||||||
|
<!-- Start New Calibration -->
|
||||||
|
<div class="workflow-section">
|
||||||
|
<h4 class="workflow-title">New Calibration</h4>
|
||||||
|
<div class="workflow-controls">
|
||||||
|
<button class="btn btn--primary" id="startCalibrationBtn" disabled>
|
||||||
|
<i data-lucide="play"></i>
|
||||||
|
Start Calibration
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Calibration Steps -->
|
||||||
|
<div class="workflow-section" id="calibrationSteps" style="display: none;">
|
||||||
|
<h4 class="workflow-title">Calibration Steps</h4>
|
||||||
|
<div class="calibration-progress" id="calibrationProgress">
|
||||||
|
<div class="progress-info">
|
||||||
|
<span class="progress-text" id="progressText">0/0</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="calibration-standards" id="calibrationStandards">
|
||||||
|
<!-- Standards buttons will be dynamically generated -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="calibration-actions">
|
||||||
|
<input type="text" class="calibration-name-input" id="calibrationNameInput" placeholder="Enter calibration name" disabled>
|
||||||
|
<button class="btn btn--success" id="saveCalibrationBtn" disabled>
|
||||||
|
<i data-lucide="save"></i>
|
||||||
|
Save Calibration
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Existing Calibrations -->
|
||||||
|
<div class="workflow-section">
|
||||||
|
<h4 class="workflow-title">Existing Calibrations</h4>
|
||||||
|
<div class="existing-calibrations" id="existingCalibrations">
|
||||||
|
<select class="calibration-dropdown" id="calibrationDropdown" disabled>
|
||||||
|
<option value="">No calibrations available</option>
|
||||||
|
</select>
|
||||||
|
<button class="btn btn--primary" id="setCalibrationBtn" disabled>
|
||||||
|
<i data-lucide="check"></i>
|
||||||
|
Set Active
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Status Summary -->
|
||||||
|
<div class="settings-card">
|
||||||
|
<h3 class="settings-card-title">Status Summary</h3>
|
||||||
|
<div class="status-grid" id="statusSummary">
|
||||||
|
<div class="status-item">
|
||||||
|
<span class="status-label">Available Presets:</span>
|
||||||
|
<span class="status-value" id="presetCount">-</span>
|
||||||
|
</div>
|
||||||
|
<div class="status-item">
|
||||||
|
<span class="status-label">Available Calibrations:</span>
|
||||||
|
<span class="status-value" id="calibrationCount">-</span>
|
||||||
|
</div>
|
||||||
|
<div class="status-item">
|
||||||
|
<span class="status-label">System Status:</span>
|
||||||
|
<span class="status-value" id="systemStatus">Checking...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
Reference in New Issue
Block a user