added A scan

This commit is contained in:
ayzen
2025-11-10 13:41:39 +03:00
parent 6b0d1647e6
commit 4dddc46bd8
4 changed files with 82 additions and 111 deletions

View File

@ -35,7 +35,7 @@ VNA_PID = 0x5740 # STM32 Virtual ComPort
# -----------------------------------------------------------------------------
# Simulator mode settings
# -----------------------------------------------------------------------------
USE_SIMULATOR = False # Set to True to use simulator instead of real device
USE_SIMULATOR = True # Set to True to use simulator instead of real device
SIMULATOR_SWEEP_FILE = BASE_DIR / "binary_input" / "sweep_example" / "example.json"
SIMULATOR_NOISE_LEVEL = 100 # Standard deviation of Gaussian noise to add to real and imaginary parts

View File

@ -1,5 +1,5 @@
{
"open_air": true,
"open_air": false,
"axis": "abs",
"cut": 0.244,
"max": 1.0,

View File

@ -733,7 +733,7 @@ class BScanProcessor(BaseProcessor):
"""
Return complete processor state including BScan-specific data.
Extends base implementation with plot history count.
Extends base implementation with plot history count and plot data.
"""
# Get base state (includes active_reference)
state = super().get_full_state()
@ -742,4 +742,17 @@ class BScanProcessor(BaseProcessor):
with self._lock:
state["plot_history_count"] = len(self._plot_history)
# Include plot history data for preview functionality
all_time_domain = [record["time_domain_data"] for record in self._plot_history]
all_distance = [record["distance_data"] for record in self._plot_history]
all_sweep_numbers = list(range(1, len(self._plot_history) + 1))
all_timestamps = [record["timestamp"] for record in self._plot_history]
state["plot_data"] = {
"all_time_domain_data": all_time_domain,
"all_distance_data": all_distance,
"all_sweep_numbers": all_sweep_numbers,
"all_timestamps": all_timestamps,
}
return state

View File

@ -83,7 +83,7 @@ export class BScanClickHandler {
// Add visual highlight by adding a vertical line shape
this.highlightColumn(plotContainer, columnIndex);
console.log(`Column ${columnIndex} selected. Press 'D' to delete.`);
console.log(`Column ${columnIndex} selected. Press 'D' to delete, 'P' to preview depth profile.`);
}
/**
@ -171,10 +171,10 @@ export class BScanClickHandler {
this.showDeleteConfirmation(this.selectedProcessorId, this.selectedColumn);
}
} else if (event.key === 'p' || event.key === 'P') {
// Show sweep preview modal
// Show column depth profile (A-scan)
if (this.selectedColumn !== null && this.selectedProcessorId !== null) {
event.preventDefault();
this.showSweepPreview(this.selectedProcessorId, this.selectedColumn);
this.showColumnProfile(this.selectedProcessorId, this.selectedColumn);
}
} else if (event.key === 'Escape') {
// Clear selection on Escape
@ -183,7 +183,7 @@ export class BScanClickHandler {
};
document.addEventListener('keydown', this.keyboardListener);
console.log('Keyboard shortcuts: D - delete column, P - preview sweep, Escape - deselect');
console.log('Keyboard shortcuts: D - delete column, P - preview column depth profile, Escape - deselect');
}
/**
@ -253,11 +253,11 @@ export class BScanClickHandler {
}
/**
* Show sweep preview modal for selected column
* Show column depth profile (A-scan) modal for selected column
* @param {string} processorId - Processor ID
* @param {number} columnIndex - Column index (1-based)
*/
showSweepPreview(processorId, columnIndex) {
showColumnProfile(processorId, columnIndex) {
// Get websocket from global window object if not available in instance
const websocket = this.websocket || window.vnaDashboard?.websocket;
@ -271,13 +271,13 @@ export class BScanClickHandler {
}
try {
// Request processor state to get sweep data
// Request processor state to get column data
const message = {
type: 'get_processor_state',
processor_id: processorId
};
console.log('Requesting processor state for sweep preview:', message);
console.log('Requesting processor state for column profile:', message);
// Set up one-time listener for the response
const handleResponse = (event) => {
@ -288,8 +288,8 @@ export class BScanClickHandler {
// Remove this listener
websocket.ws.removeEventListener('message', handleResponse);
// Extract sweep data for the selected column
this.displaySweepModal(response, columnIndex);
// Extract column data from plot history
this.displayColumnModal(response, columnIndex);
}
} catch (error) {
console.error('Error parsing processor state response:', error);
@ -300,61 +300,65 @@ export class BScanClickHandler {
websocket.ws.send(JSON.stringify(message));
} catch (error) {
console.error('Failed to request sweep preview:', error);
console.error('Failed to request column profile:', error);
this.notifications?.show?.({
type: 'error',
title: 'Ошибка предпросмотра',
message: 'Не удалось загрузить данные свипа'
message: 'Не удалось загрузить данные столбца'
});
}
}
/**
* Display sweep data in modal window
* Display column depth profile data in modal window
* @param {Object} processorState - Processor state from backend
* @param {number} columnIndex - Column index (1-based, same as sweep number)
* @param {number} columnIndex - Column index (1-based)
*/
displaySweepModal(processorState, columnIndex) {
const sweepHistory = processorState.state?.sweep_history || [];
displayColumnModal(processorState, columnIndex) {
const plotData = processorState.state?.plot_data || {};
const config = processorState.state?.config || {};
// Convert 1-based column index to 0-based array index
const sweepIndex = columnIndex - 1;
// Get plot history data from plot_data
const allTimeData = plotData.all_time_domain_data || [];
const allDistanceData = plotData.all_distance_data || [];
if (sweepIndex < 0 || sweepIndex >= sweepHistory.length) {
console.log('Plot data available:', {
columns: allTimeData.length,
requestedColumn: columnIndex,
allTimeDataLength: allTimeData.length,
allDistanceDataLength: allDistanceData.length
});
// Convert 1-based column index to 0-based array index
const columnArrayIndex = columnIndex - 1;
if (columnArrayIndex < 0 || columnArrayIndex >= allTimeData.length) {
this.notifications?.show?.({
type: 'error',
title: 'Ошибка',
message: `Свип ${columnIndex} не найден в истории`
message: `Столбец ${columnIndex} не найден в данных (доступно столбцов: ${allTimeData.length})`
});
return;
}
const sweep = sweepHistory[sweepIndex];
const amplitudeData = allTimeData[columnArrayIndex];
const depthData = allDistanceData[columnArrayIndex];
// Use same logic as BScanProcessor: calibrated_data or sweep_data
const hasCalibrated = this.hasCalibratedSweepData(sweep);
const dataToProcess = hasCalibrated ? sweep.calibrated_points : (sweep.sweep_points || []);
const referencePoints = sweep.reference_points || [];
const openAirEnabled = config.open_air || false;
if (dataToProcess.length === 0) {
if (!amplitudeData || amplitudeData.length === 0 || !depthData || depthData.length === 0) {
this.notifications?.show?.({
type: 'warning',
title: 'Нет данных',
message: `Свип ${columnIndex} не содержит данных для отображения`
message: `Столбец ${columnIndex} не содержит данных для отображения`
});
return;
}
// Show modal with plot
this.showSweepPlotModal(
// Show modal with depth profile plot
this.showColumnProfileModal(
columnIndex,
dataToProcess,
referencePoints,
openAirEnabled,
hasCalibrated,
sweep.vna_config
amplitudeData,
depthData,
config
);
}
@ -443,81 +447,41 @@ export class BScanClickHandler {
}
/**
* Show modal with sweep plot
* @param {number} sweepNumber - Sweep number (1-based)
* @param {Array} points - Sweep points (calibrated or raw)
* @param {Array} referencePoints - Reference points for open air subtraction
* @param {boolean} openAirEnabled - Whether to subtract reference
* @param {boolean} hasCalibrated - Whether the data is calibrated (true) or raw (false)
* @param {Object} vnaConfig - VNA configuration
* Show modal with column depth profile (A-scan) plot
* @param {number} columnNumber - Column number (1-based)
* @param {Array} amplitudeData - Amplitude values
* @param {Array} depthData - Depth values in meters
* @param {Object} config - Processor configuration
*/
showSweepPlotModal(sweepNumber, points, referencePoints, openAirEnabled, hasCalibrated, vnaConfig) {
showColumnProfileModal(columnNumber, amplitudeData, depthData, config) {
// Get modal element
const modal = document.getElementById('plotsModal');
if (!modal) {
console.error('Plots modal not found');
return;
}
// Prepare data for Plotly
const frequencies = [];
const magnitudes = [];
const phases = [];
points.forEach((point, index) => {
const freq = vnaConfig?.start_freq + (index / (points.length - 1)) * (vnaConfig?.stop_freq - vnaConfig?.start_freq);
frequencies.push(freq / 1e6); // Convert to MHz
let real = point[0] || point.real || point.r || 0;
let imag = point[1] || point.imag || point.i || 0;
// Apply reference subtraction if enabled (same as BScanProcessor)
if (openAirEnabled && referencePoints && referencePoints.length > index) {
const refPoint = referencePoints[index];
const refReal = refPoint[0] || refPoint.real || refPoint.r || 0;
const refImag = refPoint[1] || refPoint.imag || refPoint.i || 0;
// Subtract reference: complex_data - reference_complex
real = real - refReal;
imag = imag - refImag;
}
const magnitude = Math.sqrt(real * real + imag * imag);
const phase = Math.atan2(imag, real) * (180 / Math.PI);
magnitudes.push(magnitude);
phases.push(phase);
});
// Create Plotly traces
// Create Plotly trace - A-scan format (depth on X-axis, amplitude on Y-axis)
const traces = [
{
x: frequencies,
y: magnitudes,
x: depthData,
y: amplitudeData,
type: 'scatter',
mode: 'lines',
name: 'Амплитуда',
line: { color: '#3b82f6', width: 2 }
},
{
x: frequencies,
y: phases,
type: 'scatter',
mode: 'lines',
name: 'Фаза',
yaxis: 'y2',
line: { color: '#f59e0b', width: 2 }
}
];
// Build descriptive title
const dataType = hasCalibrated ? 'откалиброванные' : 'сырые';
const refStatus = openAirEnabled && referencePoints && referencePoints.length > 0 ? 'с вычетом референса' : 'без вычета референса';
const titleText = `Свип ${sweepNumber} (${dataType}, ${refStatus})`;
const axisType = config.axis || 'abs';
const refStatus = config.open_air ? 'с вычетом референса' : 'без вычета референса';
const titleText = `Столбец ${columnNumber} - Профиль глубины (${axisType}, ${refStatus})`;
const layoutOverrides = {
title: { text: titleText, font: { size: 16, color: '#f1f5f9' } },
xaxis: {
title: 'Частота (МГц)',
title: 'Глубина (м)',
gridcolor: '#334155',
zerolinecolor: '#475569',
color: '#cbd5e1',
@ -525,30 +489,19 @@ export class BScanClickHandler {
},
yaxis: {
title: 'Амплитуда',
side: 'left',
gridcolor: '#334155',
zerolinecolor: '#475569',
color: '#cbd5e1',
fixedrange: false
},
yaxis2: {
title: 'Фаза (°)',
overlaying: 'y',
side: 'right',
gridcolor: '#334155',
zerolinecolor: '#475569',
color: '#cbd5e1',
fixedrange: false
},
showlegend: true,
legend: { x: 0.01, y: 0.99 },
showlegend: false,
height: 500
};
const configOverrides = {
toImageButtonOptions: {
format: 'png',
filename: `sweep-${sweepNumber}-preview`,
filename: `column-${columnNumber}-profile`,
height: 600,
width: 800,
scale: 1
@ -577,7 +530,7 @@ export class BScanClickHandler {
<div class="chart-card__title">${titleText}</div>
</div>
<div class="chart-card__content">
<div class="chart-card__plot" id="sweep-preview-plot"></div>
<div class="chart-card__plot" id="column-profile-plot"></div>
</div>
`;
container.appendChild(card);
@ -591,7 +544,7 @@ export class BScanClickHandler {
// Create plot after modal is shown
setTimeout(() => {
const plotElement = document.getElementById('sweep-preview-plot');
const plotElement = document.getElementById('column-profile-plot');
if (plotElement) {
createPlotlyPlot(plotElement, traces, layoutOverrides, configOverrides);
} else {
@ -619,10 +572,15 @@ export class BScanClickHandler {
modal.classList.remove('modal--active');
document.body.style.overflow = '';
// Cleanup Plotly plot
const plotElement = document.getElementById('sweep-preview-plot');
if (plotElement) {
cleanupPlotly(plotElement);
// Cleanup Plotly plot - try both possible plot IDs
const profilePlot = document.getElementById('column-profile-plot');
if (profilePlot) {
cleanupPlotly(profilePlot);
}
const sweepPlot = document.getElementById('sweep-preview-plot');
if (sweepPlot) {
cleanupPlotly(sweepPlot);
}
}