Files
vna_system/vna_system/api/endpoints/settings.py
T

418 lines
16 KiB
Python

from fastapi import APIRouter, HTTPException
from typing import List
from pathlib import Path
import vna_system.core.singletons as singletons
from vna_system.core.settings.calibration_manager import CalibrationStandard
from vna_system.core.visualization.magnitude_chart import generate_standards_magnitude_plots, generate_combined_standards_plot
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))
@router.get("/calibration/{calibration_name}/standards-plots")
async def get_calibration_standards_plots(calibration_name: str, preset_filename: str = None):
"""Get magnitude plots for all standards in a calibration set"""
try:
# Get preset
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}")
else:
preset = singletons.settings_manager.get_current_preset()
if not preset:
raise HTTPException(status_code=400, detail="No current preset selected")
# Get calibration directory
calibration_manager = singletons.settings_manager.calibration_manager
calibration_dir = calibration_manager._get_preset_calibration_dir(preset) / calibration_name
if not calibration_dir.exists():
raise HTTPException(status_code=404, detail=f"Calibration not found: {calibration_name}")
# Generate plots for each standard
individual_plots = generate_standards_magnitude_plots(calibration_dir, preset)
return {
"calibration_name": calibration_name,
"preset": {
"filename": preset.filename,
"mode": preset.mode.value
},
"individual_plots": individual_plots
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/working-calibration/standards-plots")
async def get_working_calibration_standards_plots():
"""Get magnitude plots for standards in current working calibration"""
try:
working_calib = singletons.settings_manager.get_current_working_calibration()
if not working_calib:
raise HTTPException(status_code=404, detail="No working calibration active")
# Check if there are any standards captured
if not working_calib.standards:
raise HTTPException(status_code=404, detail="No standards captured in working calibration")
# Generate plots directly from in-memory sweep data
from vna_system.core.visualization.magnitude_chart import generate_magnitude_plot_from_sweep_data
individual_plots = {}
standard_colors = {
'open': '#2ca02c', # Green
'short': '#d62728', # Red
'load': '#ff7f0e', # Orange
'through': '#1f77b4' # Blue
}
for standard, sweep_data in working_calib.standards.items():
try:
# Generate plot for this standard
plot_config = generate_magnitude_plot_from_sweep_data(sweep_data, working_calib.preset)
if 'error' not in plot_config:
# Customize color and title for this standard
if plot_config.get('data'):
plot_config['data'][0]['line']['color'] = standard_colors.get(standard.value, '#1f77b4')
plot_config['data'][0]['name'] = f'{standard.value.upper()} Standard'
plot_config['layout']['title'] = f'{standard.value.upper()} Standard Magnitude (Working)'
# Include raw sweep data for download
plot_config['raw_sweep_data'] = {
'sweep_number': sweep_data.sweep_number,
'timestamp': sweep_data.timestamp,
'total_points': sweep_data.total_points,
'points': sweep_data.points, # Raw complex data points
'file_path': None # No file path for working calibration
}
# Add frequency information
plot_config['frequency_info'] = {
'start_freq': working_calib.preset.start_freq,
'stop_freq': working_calib.preset.stop_freq,
'points': working_calib.preset.points,
'bandwidth': working_calib.preset.bandwidth
}
individual_plots[standard.value] = plot_config
else:
individual_plots[standard.value] = plot_config
except Exception as e:
individual_plots[standard.value] = {'error': f'Failed to generate plot for {standard.value}: {str(e)}'}
if not individual_plots:
raise HTTPException(status_code=404, detail="No valid plots generated for working calibration")
return {
"calibration_name": "Working Calibration",
"preset": {
"filename": working_calib.preset.filename,
"mode": working_calib.preset.mode.value
},
"individual_plots": individual_plots,
"is_working": True,
"is_complete": working_calib.is_complete()
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))