some improvements

This commit is contained in:
Ayzen
2025-09-30 19:02:11 +03:00
parent facff2b4f8
commit 6b30e8a6fd
26 changed files with 12385 additions and 12088 deletions

View File

@ -588,6 +588,88 @@ async def clear_current_reference() -> dict[str, Any]:
raise HTTPException(status_code=500, detail=str(exc)) raise HTTPException(status_code=500, detail=str(exc))
@router.get("/reference/{reference_name}", response_model=ReferenceModel)
async def get_reference(reference_name: str, preset_filename: str | None = None) -> ReferenceModel:
"""Get information about a specific reference."""
try:
settings_manager = singletons.settings_manager
if preset_filename:
preset = next(
(p for p in settings_manager.get_available_presets() if p.filename == preset_filename),
None
)
if not preset:
raise HTTPException(status_code=404, detail=f"Preset '{preset_filename}' not found")
else:
preset = settings_manager.get_current_preset()
if not preset:
raise HTTPException(status_code=400, detail="No current preset set and no preset_filename provided")
info = settings_manager.get_reference_info(reference_name, preset)
if not info:
raise HTTPException(status_code=404, detail=f"Reference '{reference_name}' not found")
return ReferenceModel(
name=info.name,
timestamp=info.timestamp.isoformat(),
preset_filename=info.preset_filename,
description=info.description,
metadata=info.metadata,
)
except HTTPException:
raise
except Exception as exc: # noqa: BLE001
logger.error("Failed to get reference info", name=reference_name, error=repr(exc))
raise HTTPException(status_code=500, detail=str(exc))
@router.get("/reference/{reference_name}/plot")
async def get_reference_plot(reference_name: str, preset_filename: str | None = None) -> dict[str, Any]:
"""Get magnitude plot for a reference measurement."""
try:
settings_manager = singletons.settings_manager
if preset_filename:
preset = next(
(p for p in settings_manager.get_available_presets() if p.filename == preset_filename),
None
)
if not preset:
raise HTTPException(status_code=404, detail=f"Preset '{preset_filename}' not found")
else:
preset = settings_manager.get_current_preset()
if not preset:
raise HTTPException(status_code=400, detail="No current preset set and no preset_filename provided")
reference_info = settings_manager.get_reference_info(reference_name, preset)
if not reference_info:
raise HTTPException(status_code=404, detail=f"Reference '{reference_name}' not found")
sweep_data = settings_manager.reference_manager.get_reference_sweep(reference_name, preset)
if not sweep_data:
raise HTTPException(status_code=404, detail=f"Reference data not found for '{reference_name}'")
from vna_system.core.visualization.magnitude_chart import generate_magnitude_plot_from_sweep_data
plot_fig = generate_magnitude_plot_from_sweep_data(sweep_data, preset)
if "error" in plot_fig:
raise HTTPException(status_code=500, detail=plot_fig["error"])
return {
"reference_name": reference_name,
"preset": {"filename": preset.filename, "mode": preset.mode.value},
"plot": plot_fig,
"timestamp": reference_info.timestamp.isoformat(),
"description": reference_info.description
}
except HTTPException:
raise
except Exception as exc: # noqa: BLE001
logger.error("Failed to get reference plot", name=reference_name, error=repr(exc))
raise HTTPException(status_code=500, detail=str(exc))
@router.delete("/reference/{reference_name}") @router.delete("/reference/{reference_name}")
async def delete_reference(reference_name: str, preset_filename: str | None = None) -> dict[str, Any]: async def delete_reference(reference_name: str, preset_filename: str | None = None) -> dict[str, Any]:
"""Delete a reference.""" """Delete a reference."""
@ -622,6 +704,17 @@ async def delete_reference(reference_name: str, preset_filename: str | None = No
raise HTTPException(status_code=500, detail=str(exc)) raise HTTPException(status_code=500, detail=str(exc))
@router.delete("/calibration/current")
async def clear_current_calibration() -> dict[str, Any]:
"""Clear the current calibration selection."""
try:
singletons.settings_manager.calibration_manager.clear_current_calibration()
return {"success": True, "message": "Current calibration cleared"}
except Exception as exc: # noqa: BLE001
logger.error("Failed to clear current calibration", error=repr(exc))
raise HTTPException(status_code=500, detail=str(exc))
@router.delete("/calibration/{calibration_name}") @router.delete("/calibration/{calibration_name}")
async def delete_calibration(calibration_name: str, preset_filename: str | None = None) -> dict[str, Any]: async def delete_calibration(calibration_name: str, preset_filename: str | None = None) -> dict[str, Any]:
"""Delete a calibration set.""" """Delete a calibration set."""

View File

@ -23,6 +23,7 @@ class SettingsStatusModel(BaseModel):
working_calibration: Dict[str, Any] | None working_calibration: Dict[str, Any] | None
available_presets: int available_presets: int
available_calibrations: int available_calibrations: int
available_references: int
class SetPresetRequest(BaseModel): class SetPresetRequest(BaseModel):
@ -77,4 +78,4 @@ class CreateReferenceRequest(BaseModel):
class SetReferenceRequest(BaseModel): class SetReferenceRequest(BaseModel):
name: str name: str
preset_filename: str | None = None preset_filename: str | None = None

View File

@ -1 +1 @@
s11_start100_stop8800_points1000_bw1khz/еуыеуые s11_start100_stop8800_points1000_bw1khz/вфыввф

View File

@ -7,12 +7,12 @@
"points": 1000, "points": 1000,
"bandwidth": 1000.0 "bandwidth": 1000.0
}, },
"calibration_name": "tuncTuncTuncSahur", "calibration_name": "вфыввф",
"standards": [ "standards": [
"open", "open",
"short", "short",
"load" "load"
], ],
"created_timestamp": "2025-09-24T17:55:12.053850", "created_timestamp": "2025-09-30T18:55:17.618157",
"is_complete": true "is_complete": true
} }

View File

@ -7,10 +7,10 @@
"points": 1000, "points": 1000,
"bandwidth": 1000.0 "bandwidth": 1000.0
}, },
"calibration_name": "tuncTuncTuncSahur", "calibration_name": "вфыввф",
"standard": "load", "standard": "load",
"sweep_number": 1221, "sweep_number": 33,
"sweep_timestamp": 1758725709.366605, "sweep_timestamp": 1759247715.1613321,
"created_timestamp": "2025-09-24T17:55:12.053789", "created_timestamp": "2025-09-30T18:55:17.618031",
"total_points": 1000 "total_points": 1000
} }

View File

@ -7,10 +7,10 @@
"points": 1000, "points": 1000,
"bandwidth": 1000.0 "bandwidth": 1000.0
}, },
"calibration_name": "tuncTuncTuncSahur", "calibration_name": "вфыввф",
"standard": "open", "standard": "open",
"sweep_number": 1217, "sweep_number": 18,
"sweep_timestamp": 1758725700.971026, "sweep_timestamp": 1759247683.8793015,
"created_timestamp": "2025-09-24T17:55:12.049478", "created_timestamp": "2025-09-30T18:55:17.612864",
"total_points": 1000 "total_points": 1000
} }

View File

@ -7,10 +7,10 @@
"points": 1000, "points": 1000,
"bandwidth": 1000.0 "bandwidth": 1000.0
}, },
"calibration_name": "tuncTuncTuncSahur", "calibration_name": "вфыввф",
"standard": "short", "standard": "short",
"sweep_number": 1219, "sweep_number": 19,
"sweep_timestamp": 1758725705.1671622, "sweep_timestamp": 1759247685.9623504,
"created_timestamp": "2025-09-24T17:55:12.051603", "created_timestamp": "2025-09-30T18:55:17.615536",
"total_points": 1000 "total_points": 1000
} }

View File

@ -4,7 +4,7 @@
"data_limitation": "ph_only_1", "data_limitation": "ph_only_1",
"cut": 2.0, "cut": 2.0,
"max": 5.0, "max": 5.0,
"gain": 2.2, "gain": 0.0,
"start_freq": 100.0, "start_freq": 100.0,
"stop_freq": 8800.0, "stop_freq": 8800.0,
"clear_history": false, "clear_history": false,

View File

@ -183,9 +183,9 @@ class BScanProcessor(BaseProcessor):
latest_history = self._sweep_history[-1] latest_history = self._sweep_history[-1]
reference_data = latest_history.get("reference_data") reference_data = latest_history.get("reference_data")
if reference_data is None: if reference_data is None:
logger.warning(f"Open air substraction cannot be done: reference_data is None") logger.warning(f"Open air substraction cannot be done: reference_data is None")
self._config["open_air"] = False self._config["open_air"] = False
if self._config["open_air"] and reference_data is not None: if self._config["open_air"] and reference_data is not None:
reference_complex = self._get_complex_s11(reference_data) reference_complex = self._get_complex_s11(reference_data)

View File

@ -188,6 +188,13 @@ class VNASettingsManager:
raise ValueError("No current preset available") raise ValueError("No current preset available")
return self.reference_manager.delete_reference(reference_name, current_preset) return self.reference_manager.delete_reference(reference_name, current_preset)
def get_reference_info(self, reference_name: str, preset: ConfigPreset | None = None) -> ReferenceInfo | None:
"""Get detailed information about a reference."""
current_preset = preset or self.get_current_preset()
if not current_preset:
raise ValueError("No current preset available")
return self.reference_manager.get_reference_info(reference_name, current_preset)
def delete_calibration(self, preset: ConfigPreset, calibration_name: str) -> None: def delete_calibration(self, preset: ConfigPreset, calibration_name: str) -> None:
"""Delete a saved calibration for the given preset.""" """Delete a saved calibration for the given preset."""
self.calibration_manager.delete_calibration(preset, calibration_name) self.calibration_manager.delete_calibration(preset, calibration_name)
@ -309,7 +316,8 @@ class VNASettingsManager:
"current_calibration": None, "current_calibration": None,
"working_calibration": None, "working_calibration": None,
"available_presets": len(self.get_available_presets()), "available_presets": len(self.get_available_presets()),
"available_calibrations": 0 "available_calibrations": 0,
"available_references": 0,
} }
if current_preset: if current_preset:
@ -322,6 +330,7 @@ class VNASettingsManager:
"bandwidth": current_preset.bandwidth "bandwidth": current_preset.bandwidth
} }
summary["available_calibrations"] = len(self.get_available_calibrations(current_preset)) summary["available_calibrations"] = len(self.get_available_calibrations(current_preset))
summary["available_references"] = len(self.get_available_references(current_preset))
if current_calibration: if current_calibration:
summary["current_calibration"] = { summary["current_calibration"] = {

View File

@ -1 +1 @@
s21_start100_stop8800_points1000_bw1khz/ффывфы s11_start100_stop8800_points1000_bw1khz/asd

View File

@ -431,7 +431,7 @@
/* Notifications */ /* Notifications */
.notifications { .notifications {
position: fixed; position: fixed;
top: var(--space-4); top: calc(var(--header-height) + var(--space-2));
right: var(--space-4); right: var(--space-4);
z-index: var(--z-toast); z-index: var(--z-toast);
display: flex; display: flex;

View File

@ -518,6 +518,11 @@
margin-top: var(--space-3); margin-top: var(--space-3);
} }
#currentReferenceInfo {
display: none;
flex-direction: column;
}
/* Reference details */ /* Reference details */
.reference-details { .reference-details {
margin-top: var(--space-3); margin-top: var(--space-3);

View File

@ -85,7 +85,7 @@ class VNADashboard {
this.notifications.show({ this.notifications.show({
type: 'info', type: 'info',
title: 'Панель готова', title: 'Панель готова',
message: 'Соединение с системой ВНА установлено. Ожидание данных свипа...' message: 'Соединение с системой установлено. Ожидание данных свипа...'
}); });
} catch (error) { } catch (error) {

View File

@ -75,6 +75,7 @@ export const API = {
CALIBRATION_START: `${API_BASE}/settings/calibration/start`, CALIBRATION_START: `${API_BASE}/settings/calibration/start`,
CALIBRATION_SAVE: `${API_BASE}/settings/calibration/save`, CALIBRATION_SAVE: `${API_BASE}/settings/calibration/save`,
CALIBRATION_SET: `${API_BASE}/settings/calibration/set`, CALIBRATION_SET: `${API_BASE}/settings/calibration/set`,
CALIBRATION_CLEAR_CURRENT: `${API_BASE}/settings/calibration/current`,
CALIBRATION_DELETE: (name) => `${API_BASE}/settings/calibration/${encodeURIComponent(name)}`, CALIBRATION_DELETE: (name) => `${API_BASE}/settings/calibration/${encodeURIComponent(name)}`,
CALIBRATION_ADD_STANDARD: `${API_BASE}/settings/calibration/add-standard`, CALIBRATION_ADD_STANDARD: `${API_BASE}/settings/calibration/add-standard`,
CALIBRATION_STANDARDS_PLOTS: (name) => `${API_BASE}/settings/calibration/${encodeURIComponent(name)}/standards-plots`, CALIBRATION_STANDARDS_PLOTS: (name) => `${API_BASE}/settings/calibration/${encodeURIComponent(name)}/standards-plots`,
@ -86,7 +87,8 @@ export const API = {
REFERENCE_CREATE: `${API_BASE}/settings/reference/create`, REFERENCE_CREATE: `${API_BASE}/settings/reference/create`,
REFERENCE_SET: `${API_BASE}/settings/reference/set`, REFERENCE_SET: `${API_BASE}/settings/reference/set`,
REFERENCE_CURRENT: `${API_BASE}/settings/reference/current`, REFERENCE_CURRENT: `${API_BASE}/settings/reference/current`,
REFERENCE_ITEM: (name) => `${API_BASE}/settings/reference/${encodeURIComponent(name)}` REFERENCE_ITEM: (name) => `${API_BASE}/settings/reference/${encodeURIComponent(name)}`,
REFERENCE_PLOT: (name) => `${API_BASE}/settings/reference/${encodeURIComponent(name)}/plot`
} }
}; };

View File

@ -136,10 +136,9 @@ const ICONS = {
'eye-off': { 'eye-off': {
viewBox: '0 0 24 24', viewBox: '0 0 24 24',
elements: [ elements: [
{ type: 'path', attrs: { d: 'M3 3l18 18' } }, { type: 'path', attrs: { d: 'M2 12s3.5-6 10-6 10 6 10 6-3.5 6-10 6S2 12 2 12z' } },
{ type: 'path', attrs: { d: 'M9.5 9.5A4 4 0 0 0 12 16a4 4 0 0 0 3.5-6.5' } }, { type: 'circle', attrs: { cx: 12, cy: 12, r: 3 } },
{ type: 'path', attrs: { d: 'M7.2 7.5C4.8 8.9 3 12 3 12s2.7 5 9 5c1.5 0 2.8-.3 3.9-.8' } }, { type: 'line', attrs: { x1: 4, y1: 4, x2: 20, y2: 20 } }
{ type: 'path', attrs: { d: 'M17.2 10.2C18.7 9.2 21 12 21 12s-2.7 5-9 5' } }
] ]
}, },
'alert-circle': { 'alert-circle': {

View File

@ -77,6 +77,7 @@ export class SettingsManager {
setCalibrationBtn: document.getElementById('setCalibrationBtn'), setCalibrationBtn: document.getElementById('setCalibrationBtn'),
viewPlotsBtn: document.getElementById('viewPlotsBtn'), viewPlotsBtn: document.getElementById('viewPlotsBtn'),
deleteCalibrationBtn: document.getElementById('deleteCalibrationBtn'), deleteCalibrationBtn: document.getElementById('deleteCalibrationBtn'),
clearCalibrationBtn: document.getElementById('clearCalibrationBtn'),
viewCurrentPlotsBtn: document.getElementById('viewCurrentPlotsBtn'), viewCurrentPlotsBtn: document.getElementById('viewCurrentPlotsBtn'),
// Modal // Modal
@ -139,6 +140,10 @@ export class SettingsManager {
this.referenceManager.onReferenceUpdated = (reference) => { this.referenceManager.onReferenceUpdated = (reference) => {
this.updateReferenceSummary(reference); this.updateReferenceSummary(reference);
}; };
this.referenceManager.onShowPlots = (plotData) => {
this.showPlotsModal(plotData);
};
} }
setupEventHandlers() { setupEventHandlers() {
@ -163,6 +168,9 @@ export class SettingsManager {
if (this.elements.systemStatus) { if (this.elements.systemStatus) {
this.elements.systemStatus.textContent = 'Не подключено'; this.elements.systemStatus.textContent = 'Не подключено';
} }
if (this.elements.referenceCount) {
this.elements.referenceCount.textContent = '-';
}
this.notify(ERROR, 'Ошибка статуса', 'Не удалось получить текущее состояние системы'); this.notify(ERROR, 'Ошибка статуса', 'Не удалось получить текущее состояние системы');
return null; return null;
} }
@ -198,8 +206,9 @@ export class SettingsManager {
} }
const effectiveStatus = { const effectiveStatus = {
...(status ?? { available_presets: 0, available_calibrations: 0 }), ...(status ?? { available_presets: 0, available_calibrations: 0, available_references: 0 }),
current_preset: fallbackPreset, current_preset: fallbackPreset,
available_references: this.referenceManager?.availableReferences?.length ?? 0,
}; };
if (!status) { if (!status) {
@ -210,7 +219,7 @@ export class SettingsManager {
this.elements.calibrationCount.textContent = effectiveStatus.available_calibrations ?? '-'; this.elements.calibrationCount.textContent = effectiveStatus.available_calibrations ?? '-';
} }
if (this.elements.referenceCount) { if (this.elements.referenceCount) {
this.elements.referenceCount.textContent = '-'; this.elements.referenceCount.textContent = effectiveStatus.available_references ?? '-';
} }
} }
@ -234,7 +243,9 @@ export class SettingsManager {
this.elements.calibrationCount.textContent = status?.available_calibrations ?? '-'; this.elements.calibrationCount.textContent = status?.available_calibrations ?? '-';
} }
if (this.elements.referenceCount) { if (this.elements.referenceCount) {
const count = this.referenceManager?.availableReferences?.length; const count = typeof status?.available_references === 'number'
? status.available_references
: this.referenceManager?.availableReferences?.length;
this.elements.referenceCount.textContent = typeof count === 'number' ? count : '-'; this.elements.referenceCount.textContent = typeof count === 'number' ? count : '-';
} }
this.updateHeaderSummary(status); this.updateHeaderSummary(status);
@ -294,14 +305,25 @@ export class SettingsManager {
this.currentPlotsData = plotsData; this.currentPlotsData = plotsData;
this.renderCalibrationPlots(plotsData.individual_plots, plotsData.preset); if (plotsData.reference_name) {
this.renderReferencePlot(plotsData);
} else {
this.renderCalibrationPlots(plotsData.individual_plots, plotsData.preset);
}
const title = modal.querySelector('.modal__title'); const title = modal.querySelector('.modal__title');
if (title) { if (title) {
title.innerHTML = ` if (plotsData.reference_name) {
<span data-icon="bar-chart-3"></span> title.innerHTML = `
${plotsData.calibration_name} - ${plotsData.preset.mode.toUpperCase()} Standards <span data-icon="target"></span>
`; ${plotsData.reference_name} - ${plotsData.preset.mode.toUpperCase()}
`;
} else {
title.innerHTML = `
<span data-icon="bar-chart-3"></span>
${plotsData.calibration_name} - ${plotsData.preset.mode.toUpperCase()} Standards
`;
}
renderIcons(title); renderIcons(title);
} }
@ -311,6 +333,60 @@ export class SettingsManager {
document.body.style.overflow = 'hidden'; document.body.style.overflow = 'hidden';
} }
renderReferencePlot(plotsData) {
const container = this.elements.plotsGrid;
if (!container) return;
container.innerHTML = '';
if (!plotsData.plot || plotsData.plot.error) {
container.innerHTML = '<div class="plot-error">Не удалось загрузить график эталона</div>';
return;
}
const card = document.createElement('div');
card.className = 'chart-card';
card.innerHTML = `
<div class="chart-card__header">
<div class="chart-card__title">
<span data-icon="target" class="chart-card__icon"></span>
${plotsData.reference_name}
</div>
<div class="chart-card__actions">
<button class="chart-card__action" data-action="fullscreen" title="На весь экран">
<span data-icon="expand"></span>
</button>
<button class="chart-card__action" data-action="download" title="Скачать">
<span data-icon="download"></span>
</button>
</div>
</div>
<div class="chart-card__content">
<div class="chart-card__plot" id="reference-plot"></div>
</div>
<div class="chart-card__meta">
<div class="chart-card__timestamp">Дата: ${new Date(plotsData.timestamp).toLocaleString()}</div>
<div class="chart-card__sweep">${plotsData.description || 'Эталон открытого пространства'}</div>
</div>
`;
card.addEventListener('click', (e) => {
const action = e.target.closest?.('[data-action]')?.dataset.action;
if (!action) return;
e.stopPropagation();
const plotEl = card.querySelector('.chart-card__plot');
if (action === 'fullscreen') this.toggleFullscreen(card);
if (action === 'download') this.downloadReferencePlot(plotsData, plotEl);
});
container.appendChild(card);
renderIcons(card);
const plotEl = card.querySelector('.chart-card__plot');
this.renderPlotly(plotEl, plotsData.plot, plotsData.reference_name);
}
renderCalibrationPlots(individualPlots, preset) { renderCalibrationPlots(individualPlots, preset) {
const container = this.elements.plotsGrid; const container = this.elements.plotsGrid;
if (!container) return; if (!container) return;
@ -473,6 +549,33 @@ export class SettingsManager {
this.currentPlotsData = null; this.currentPlotsData = null;
} }
async downloadReferencePlot(plotsData, plotContainer) {
try {
const ts = new Date().toISOString().replace(/[:.]/g, '-');
const base = `${plotsData.reference_name}_${ts}`;
if (plotContainer) {
await downloadPlotlyImage(plotContainer, `${base}_plot`);
}
const data = {
reference_info: {
name: plotsData.reference_name,
timestamp: plotsData.timestamp,
description: plotsData.description,
preset: plotsData.preset
},
plot_data: plotsData.plot
};
downloadJSON(data, `${base}_data.json`);
this.notify(SUCCESS, 'Скачивание завершено', `Скачаны график и данные эталона ${plotsData.reference_name}`);
} catch (e) {
console.error('Download reference failed:', e);
this.notify(ERROR, 'Ошибка скачивания', 'Не удалось скачать данные эталона');
}
}
async downloadCalibrationStandard(standardName, plotContainer) { async downloadCalibrationStandard(standardName, plotContainer) {
try { try {
const ts = new Date().toISOString().replace(/[:.]/g, '-'); const ts = new Date().toISOString().replace(/[:.]/g, '-');

View File

@ -26,6 +26,7 @@ export class CalibrationManager {
this.handleSetCalibration = this.handleSetCalibration.bind(this); this.handleSetCalibration = this.handleSetCalibration.bind(this);
this.handleCalibrationChange = this.handleCalibrationChange.bind(this); this.handleCalibrationChange = this.handleCalibrationChange.bind(this);
this.handleDeleteCalibration = this.handleDeleteCalibration.bind(this); this.handleDeleteCalibration = this.handleDeleteCalibration.bind(this);
this.handleClearCalibration = this.handleClearCalibration.bind(this);
} }
init(elements) { init(elements) {
@ -35,6 +36,7 @@ export class CalibrationManager {
this.elements.calibrationDropdown?.addEventListener('change', this.handleCalibrationChange); this.elements.calibrationDropdown?.addEventListener('change', this.handleCalibrationChange);
this.elements.setCalibrationBtn?.addEventListener('click', this.handleSetCalibration); this.elements.setCalibrationBtn?.addEventListener('click', this.handleSetCalibration);
this.elements.deleteCalibrationBtn?.addEventListener('click', this.handleDeleteCalibration); this.elements.deleteCalibrationBtn?.addEventListener('click', this.handleDeleteCalibration);
this.elements.clearCalibrationBtn?.addEventListener('click', this.handleClearCalibration);
this.elements.calibrationNameInput?.addEventListener('input', () => { this.elements.calibrationNameInput?.addEventListener('input', () => {
const hasName = this.elements.calibrationNameInput.value.trim().length > 0; const hasName = this.elements.calibrationNameInput.value.trim().length > 0;
@ -49,6 +51,7 @@ export class CalibrationManager {
this.elements.calibrationDropdown?.removeEventListener('change', this.handleCalibrationChange); this.elements.calibrationDropdown?.removeEventListener('change', this.handleCalibrationChange);
this.elements.setCalibrationBtn?.removeEventListener('click', this.handleSetCalibration); this.elements.setCalibrationBtn?.removeEventListener('click', this.handleSetCalibration);
this.elements.deleteCalibrationBtn?.removeEventListener('click', this.handleDeleteCalibration); this.elements.deleteCalibrationBtn?.removeEventListener('click', this.handleDeleteCalibration);
this.elements.clearCalibrationBtn?.removeEventListener('click', this.handleClearCalibration);
this.resetCaptureState(); this.resetCaptureState();
} }
@ -97,6 +100,9 @@ export class CalibrationManager {
if (this.elements.deleteCalibrationBtn) { if (this.elements.deleteCalibrationBtn) {
this.elements.deleteCalibrationBtn.disabled = true; this.elements.deleteCalibrationBtn.disabled = true;
} }
if (this.elements.clearCalibrationBtn) {
this.elements.clearCalibrationBtn.disabled = true;
}
return; return;
} }
@ -114,6 +120,9 @@ export class CalibrationManager {
if (this.elements.deleteCalibrationBtn) { if (this.elements.deleteCalibrationBtn) {
this.elements.deleteCalibrationBtn.disabled = true; this.elements.deleteCalibrationBtn.disabled = true;
} }
if (this.elements.clearCalibrationBtn) {
this.elements.clearCalibrationBtn.disabled = !this.currentCalibration;
}
} }
updateWorking(working) { updateWorking(working) {
@ -385,6 +394,53 @@ export class CalibrationManager {
); );
} }
async handleClearCalibration() {
if (!this.currentPreset || !this.currentCalibration) {
return;
}
if (!confirm(`Сбросить текущую калибровку «${this.currentCalibration.calibration_name}»? Вы сможете выбрать её снова позже.`)) {
return;
}
this.debouncer.debounce('clear-calibration', () =>
this.reqGuard.runExclusive('clear-calibration', async () => {
try {
if (this.elements.clearCalibrationBtn) {
ButtonState.set(this.elements.clearCalibrationBtn, { state: 'loading', icon: 'loader', text: 'Сброс...' });
}
const result = await apiDelete(API.SETTINGS.CALIBRATION_CLEAR_CURRENT);
this.notify(INFO, 'Калибровка сброшена', result.message);
this.currentCalibration = null;
if (this.elements.currentCalibration) {
this.elements.currentCalibration.textContent = 'Нет';
}
if (this.elements.calibrationDropdown) {
this.elements.calibrationDropdown.value = '';
}
await this.loadCalibrations();
await this.loadWorkingCalibration();
if (this.onCalibrationSet) {
await this.onCalibrationSet();
}
} catch (e) {
console.error('Clear calibration failed:', e);
this.notify(ERROR, 'Ошибка калибровки', 'Не удалось сбросить калибровку');
} finally {
if (this.elements.clearCalibrationBtn) {
ButtonState.set(this.elements.clearCalibrationBtn, { state: 'normal', icon: 'square', text: 'Сбросить' });
}
}
}), TIMING.DEBOUNCE_CALIBRATION
);
}
updateStatus(status) { updateStatus(status) {
if (status.current_calibration) { if (status.current_calibration) {
this.currentCalibration = status.current_calibration; this.currentCalibration = status.current_calibration;
@ -393,6 +449,10 @@ export class CalibrationManager {
this.currentCalibration = null; this.currentCalibration = null;
this.elements.currentCalibration.textContent = 'Нет'; this.elements.currentCalibration.textContent = 'Нет';
} }
if (this.elements.clearCalibrationBtn) {
this.elements.clearCalibrationBtn.disabled = !this.currentCalibration;
}
} }
resetCaptureState(standard = null) { resetCaptureState(standard = null) {
@ -413,6 +473,7 @@ export class CalibrationManager {
} }
if (this.elements.saveCalibrationBtn) this.elements.saveCalibrationBtn.disabled = true; if (this.elements.saveCalibrationBtn) this.elements.saveCalibrationBtn.disabled = true;
if (this.elements.progressText) this.elements.progressText.textContent = '0/0'; if (this.elements.progressText) this.elements.progressText.textContent = '0/0';
if (this.elements.clearCalibrationBtn) this.elements.clearCalibrationBtn.disabled = true;
} }
getWorkingCalibration() { getWorkingCalibration() {

View File

@ -7,7 +7,7 @@ import { Debouncer, RequestGuard, ButtonState } from '../utils.js';
import { apiGet, apiPost, apiDelete, buildUrl } from '../api-client.js'; import { apiGet, apiPost, apiDelete, buildUrl } from '../api-client.js';
import { API, TIMING, NOTIFICATION_TYPES } from '../constants.js'; import { API, TIMING, NOTIFICATION_TYPES } from '../constants.js';
const { SUCCESS, ERROR, WARNING, INFO } = NOTIFICATION_TYPES; const { SUCCESS, ERROR, WARNING } = NOTIFICATION_TYPES;
export class ReferenceManager { export class ReferenceManager {
constructor(notifications) { constructor(notifications) {
@ -15,6 +15,7 @@ export class ReferenceManager {
this.availableReferences = []; this.availableReferences = [];
this.currentReference = null; this.currentReference = null;
this.currentPreset = null; this.currentPreset = null;
this.lastPresetFilename = null;
this.elements = {}; this.elements = {};
this.debouncer = new Debouncer(); this.debouncer = new Debouncer();
this.reqGuard = new RequestGuard(); this.reqGuard = new RequestGuard();
@ -54,12 +55,23 @@ export class ReferenceManager {
} }
async setCurrentPreset(preset) { async setCurrentPreset(preset) {
const previousPreset = this.currentPreset?.filename ?? null;
const newPreset = preset?.filename ?? null;
this.currentPreset = preset; this.currentPreset = preset;
if (preset) { this.lastPresetFilename = newPreset;
await this.loadReferences();
} else { if (!preset) {
await this.clearReferenceCache();
this.reset(); this.reset();
return;
} }
if (previousPreset !== newPreset) {
await this.clearServerCurrentReference();
}
await this.loadReferences();
} }
async loadReferences() { async loadReferences() {
@ -67,6 +79,7 @@ export class ReferenceManager {
if (!this.currentPreset) { if (!this.currentPreset) {
this.renderDropdown([]); this.renderDropdown([]);
this.updateInfo(null); this.updateInfo(null);
this.updateReferenceCount(null);
return; return;
} }
@ -241,34 +254,21 @@ export class ReferenceManager {
this.debouncer.debounce('preview-reference', () => this.debouncer.debounce('preview-reference', () =>
this.reqGuard.runExclusive('preview-reference', async () => { this.reqGuard.runExclusive('preview-reference', async () => {
try { try {
const url = buildUrl(API.SETTINGS.REFERENCE_ITEM(targetName), { ButtonState.set(this.elements.previewReferenceBtn, { state: 'loading', icon: 'loader', text: 'Загрузка...' });
const url = buildUrl(API.SETTINGS.REFERENCE_PLOT(targetName), {
preset_filename: this.currentPreset?.filename preset_filename: this.currentPreset?.filename
}); });
const details = await apiGet(url); const plotData = await apiGet(url);
const timestamp = details?.timestamp if (this.onShowPlots) {
? new Date(details.timestamp).toLocaleString() this.onShowPlots(plotData);
: '—';
const description = details?.description ? details.description : '—';
const lines = [
`Имя: ${details?.name ?? targetName}`,
`Дата: ${timestamp}`,
`Описание: ${description}`
];
if (details?.metadata && Object.keys(details.metadata).length) {
const metaPairs = Object.entries(details.metadata)
.map(([key, value]) => `${key}: ${JSON.stringify(value)}`)
.join('; ');
lines.push(`Метаданные: ${metaPairs}`);
} }
const message = lines.join(' • ');
this.notify(INFO, 'Просмотр эталона', message);
} catch (error) { } catch (error) {
console.error('Preview reference failed:', error); console.error('Preview reference failed:', error);
this.notify(ERROR, 'Ошибка эталона', 'Не удалось загрузить данные эталона'); this.notify(ERROR, 'Ошибка эталона', 'Не удалось загрузить данные эталона');
} finally {
ButtonState.set(this.elements.previewReferenceBtn, { state: 'normal', icon: 'eye', text: 'Просмотр' });
} }
}) })
); );
@ -361,6 +361,23 @@ export class ReferenceManager {
this.updateReferenceCount(0); this.updateReferenceCount(0);
} }
async clearReferenceCache() {
this.availableReferences = [];
this.updateReferenceCount(0);
this.updateInfo(null);
}
async clearServerCurrentReference() {
try {
await apiDelete(API.SETTINGS.REFERENCE_CURRENT);
} catch (error) {
if (!error?.status || error.status !== 404) {
console.error('Failed to clear current reference', error);
}
}
await this.clearReferenceCache();
}
notify(type, title, message) { notify(type, title, message) {
this.notifications?.show?.({ type, title, message }); this.notifications?.show?.({ type, title, message });
} }

View File

@ -3,10 +3,9 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Панель управления системой ВНА для сбора и обработки данных в реальном времени"> <meta name="description" content="Панель управления системой для сбора и обработки данных в реальном времени">
<meta name="keywords" content="ВНА, Vector Network Analyzer, РФ, микроволны, сбор данных, реальное время">
<meta name="author" content="VNA System"> <meta name="author" content="VNA System">
<title>Система ВНА — Панель управления</title> <title>Радар</title>
<!-- Favicon --> <!-- Favicon -->
<link rel="icon" type="image/svg+xml" href="/static/assets/favicon.svg"> <link rel="icon" type="image/svg+xml" href="/static/assets/favicon.svg">
@ -129,7 +128,7 @@
<span data-icon="activity" class="empty-state__icon"></span> <span data-icon="activity" class="empty-state__icon"></span>
<h3 class="empty-state__title">Нет данных</h3> <h3 class="empty-state__title">Нет данных</h3>
<p class="empty-state__description"> <p class="empty-state__description">
Ожидаем данные свипа от системы ВНА... Ожидаем данные свипа от системы...
</p> </p>
<div class="loading-spinner"> <div class="loading-spinner">
<div class="spinner"></div> <div class="spinner"></div>
@ -223,7 +222,7 @@
Графики текущей калибровки Графики текущей калибровки
</button> </button>
<input type="text" class="calibration-name-input settings-input" id="calibrationNameInput" placeholder="Введите имя калибровки" disabled> <input type="text" class="calibration-name-input settings-input" id="calibrationNameInput" placeholder="Введите имя калибровки" disabled>
<button class="btn btn--success" id="saveCalibrationBtn" disabled> <button class="btn btn--primary" id="saveCalibrationBtn" disabled>
<span data-icon="save"></span> <span data-icon="save"></span>
Сохранить калибровку Сохранить калибровку
</button> </button>
@ -242,6 +241,10 @@
<span data-icon="check"></span> <span data-icon="check"></span>
Сделать активной Сделать активной
</button> </button>
<button class="btn btn--secondary" id="clearCalibrationBtn" disabled>
<span data-icon="square"></span>
Сбросить
</button>
<button class="btn btn--secondary" id="viewPlotsBtn" disabled> <button class="btn btn--secondary" id="viewPlotsBtn" disabled>
<span data-icon="bar-chart-3"></span> <span data-icon="bar-chart-3"></span>
Показать графики Показать графики
@ -286,6 +289,10 @@
<span data-icon="check"></span> <span data-icon="check"></span>
Использовать Использовать
</button> </button>
<button class="btn btn--secondary" id="clearReferenceBtn" disabled>
<span data-icon="square"></span>
Сбросить
</button>
<button class="btn btn--secondary" id="previewReferenceBtn" disabled> <button class="btn btn--secondary" id="previewReferenceBtn" disabled>
<span data-icon="bar-chart-3"></span> <span data-icon="bar-chart-3"></span>
Просмотр Просмотр