Compare commits
14 Commits
4dddc46bd8
...
radiophoto
| Author | SHA1 | Date | |
|---|---|---|---|
|
e43ce26fdf
|
|||
|
bfc3949c4d
|
|||
|
2742cfe856
|
|||
|
c64f2a4d6b
|
|||
|
8599e3cb55
|
|||
|
327baee051
|
|||
|
88ef718f31
|
|||
|
5aea37ac63
|
|||
| c1f5d6580e | |||
| 23bc4bdd24 | |||
| 2b2e323fbf | |||
| 20666e894f | |||
| 333ec5d196 | |||
| 174ab59004 |
0
__init__.py
Normal file
0
__init__.py
Normal file
@ -4,4 +4,5 @@ fastapi==0.117.1
|
|||||||
numpy==2.3.3
|
numpy==2.3.3
|
||||||
matplotlib==3.10.6
|
matplotlib==3.10.6
|
||||||
plotly==6.3.0
|
plotly==6.3.0
|
||||||
|
scipy==1.16.3
|
||||||
# ??? kaleido==1.1.0
|
# ??? kaleido==1.1.0
|
||||||
0
vna_system/__init__.py
Normal file
0
vna_system/__init__.py
Normal file
215
vna_system/api/endpoints/laser.py
Normal file
215
vna_system/api/endpoints/laser.py
Normal file
@ -0,0 +1,215 @@
|
|||||||
|
from fastapi import APIRouter, HTTPException, Depends
|
||||||
|
from typing import Dict, Any
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from ...api.models.laser import (
|
||||||
|
LaserParameters,
|
||||||
|
LaserStatus,
|
||||||
|
LaserStartResponse,
|
||||||
|
LaserStopResponse,
|
||||||
|
ManualLaserParameters,
|
||||||
|
ManualLaserStartResponse
|
||||||
|
)
|
||||||
|
from ...core.laser.laser_controller import LaserController
|
||||||
|
from ...core import singletons
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/api/v1/laser", tags=["laser"])
|
||||||
|
|
||||||
|
|
||||||
|
def get_laser_controller():
|
||||||
|
"""Dependency to get laser controller instance from singletons"""
|
||||||
|
return singletons.laser_controller_instance
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/start", response_model=LaserStartResponse)
|
||||||
|
async def start_laser_cycle(
|
||||||
|
parameters: LaserParameters,
|
||||||
|
controller: LaserController = Depends(get_laser_controller)
|
||||||
|
) -> LaserStartResponse:
|
||||||
|
"""
|
||||||
|
Start laser control cycle with specified parameters.
|
||||||
|
|
||||||
|
This endpoint accepts laser control parameters and starts the measurement cycle.
|
||||||
|
All parameters are validated and logged.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
logger.info("Received request to start laser cycle")
|
||||||
|
|
||||||
|
# Validate that only one scan mode is enabled at a time
|
||||||
|
# (Manual mode = all disabled, Scan mode = one enabled)
|
||||||
|
enabled_modes = sum([
|
||||||
|
parameters.enable_t1,
|
||||||
|
parameters.enable_t2,
|
||||||
|
parameters.enable_c1,
|
||||||
|
parameters.enable_c2
|
||||||
|
])
|
||||||
|
if enabled_modes > 1:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail="Можно включить только один режим сканирования одновременно"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Start the cycle
|
||||||
|
result = controller.start_cycle(parameters)
|
||||||
|
|
||||||
|
if not result["success"]:
|
||||||
|
raise HTTPException(status_code=500, detail=result["message"])
|
||||||
|
|
||||||
|
return LaserStartResponse(
|
||||||
|
success=True,
|
||||||
|
message=result["message"],
|
||||||
|
parameters=parameters
|
||||||
|
)
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Unexpected error starting laser cycle: {e}", exc_info=True)
|
||||||
|
raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/start-manual", response_model=ManualLaserStartResponse)
|
||||||
|
async def start_manual_laser_control(
|
||||||
|
parameters: ManualLaserParameters,
|
||||||
|
controller: LaserController = Depends(get_laser_controller)
|
||||||
|
) -> ManualLaserStartResponse:
|
||||||
|
"""
|
||||||
|
Start manual laser control with simplified parameters (t1, t2, i1, i2).
|
||||||
|
|
||||||
|
This endpoint is designed for direct manual control without scan modes.
|
||||||
|
It accepts only 4 parameters and immediately applies them to the device.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
parameters: Manual control parameters with t1, t2, i1, i2
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ManualLaserStartResponse with success status and message
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
logger.info("Received request to start manual laser control")
|
||||||
|
logger.info(f"Parameters: T1={parameters.t1}°C, T2={parameters.t2}°C, I1={parameters.i1}mA, I2={parameters.i2}mA")
|
||||||
|
|
||||||
|
# Call the simplified manual control method
|
||||||
|
result = controller.start_manual_direct(
|
||||||
|
t1=parameters.t1,
|
||||||
|
t2=parameters.t2,
|
||||||
|
i1=parameters.i1,
|
||||||
|
i2=parameters.i2
|
||||||
|
)
|
||||||
|
|
||||||
|
if not result["success"]:
|
||||||
|
raise HTTPException(status_code=500, detail=result["message"])
|
||||||
|
|
||||||
|
return ManualLaserStartResponse(
|
||||||
|
success=True,
|
||||||
|
message=result["message"],
|
||||||
|
parameters=parameters
|
||||||
|
)
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Unexpected error starting manual laser control: {e}", exc_info=True)
|
||||||
|
raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/stop", response_model=LaserStopResponse)
|
||||||
|
async def stop_laser_cycle(
|
||||||
|
controller: LaserController = Depends(get_laser_controller)
|
||||||
|
) -> LaserStopResponse:
|
||||||
|
"""
|
||||||
|
Stop the current laser control cycle.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
logger.info("Received request to stop laser cycle")
|
||||||
|
|
||||||
|
result = controller.stop_cycle()
|
||||||
|
|
||||||
|
if not result["success"]:
|
||||||
|
raise HTTPException(status_code=500, detail=result["message"])
|
||||||
|
|
||||||
|
return LaserStopResponse(
|
||||||
|
success=True,
|
||||||
|
message=result["message"]
|
||||||
|
)
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Unexpected error stopping laser cycle: {e}", exc_info=True)
|
||||||
|
raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/status", response_model=LaserStatus)
|
||||||
|
async def get_laser_status(
|
||||||
|
controller: LaserController = Depends(get_laser_controller)
|
||||||
|
) -> LaserStatus:
|
||||||
|
"""
|
||||||
|
Get current laser status including temperature, current, and voltage readings.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
status = controller.get_status()
|
||||||
|
return status
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error getting laser status: {e}", exc_info=True)
|
||||||
|
raise HTTPException(status_code=500, detail=f"Ошибка получения статуса: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/connect")
|
||||||
|
async def connect_laser(
|
||||||
|
port: str = None,
|
||||||
|
controller: LaserController = Depends(get_laser_controller)
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Connect to laser control hardware on specified port.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
port: Serial port (e.g., '/dev/ttyUSB0'). If not specified, auto-detect.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
logger.info(f"Received request to connect to laser hardware on port: {port or 'auto-detect'}")
|
||||||
|
|
||||||
|
# If already connected, disconnect first
|
||||||
|
if controller.is_connected:
|
||||||
|
logger.info("Already connected, disconnecting first...")
|
||||||
|
controller.disconnect()
|
||||||
|
|
||||||
|
result = controller.connect(port)
|
||||||
|
|
||||||
|
if not result["success"]:
|
||||||
|
raise HTTPException(status_code=500, detail=result["message"])
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error connecting to laser: {e}", exc_info=True)
|
||||||
|
raise HTTPException(status_code=500, detail=f"Ошибка подключения: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/disconnect")
|
||||||
|
async def disconnect_laser(
|
||||||
|
controller: LaserController = Depends(get_laser_controller)
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Disconnect from laser control hardware.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
logger.info("Received request to disconnect from laser hardware")
|
||||||
|
|
||||||
|
result = controller.disconnect()
|
||||||
|
|
||||||
|
if not result["success"]:
|
||||||
|
raise HTTPException(status_code=500, detail=result["message"])
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error disconnecting from laser: {e}", exc_info=True)
|
||||||
|
raise HTTPException(status_code=500, detail=f"Ошибка отключения: {str(e)}")
|
||||||
120
vna_system/api/models/laser.py
Normal file
120
vna_system/api/models/laser.py
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
from pydantic import BaseModel, Field, field_validator
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
class LaserParameters(BaseModel):
|
||||||
|
"""Model for laser control parameters"""
|
||||||
|
|
||||||
|
# Laser 1 Temperature parameters
|
||||||
|
min_temp_1: float = Field(..., ge=-1, le=45, description="Минимальная температура лазера 1 (C)")
|
||||||
|
max_temp_1: float = Field(..., ge=-1, le=45, description="Максимальная температура лазера 1 (C)")
|
||||||
|
delta_temp_1: float = Field(..., ge=0.05, le=1.0, description="Шаг дискретизации температуры лазера 1 (C)")
|
||||||
|
|
||||||
|
# Laser 1 Current parameters
|
||||||
|
min_current_1: float = Field(..., ge=15, le=70, description="Минимальный ток лазера 1 (мА)")
|
||||||
|
max_current_1: float = Field(..., ge=15, le=70, description="Максимальный ток лазера 1 (мА)")
|
||||||
|
delta_current_1: float = Field(..., ge=0.002, le=0.5, description="Шаг дискретизации тока лазера 1 (мА)")
|
||||||
|
|
||||||
|
# Laser 2 Temperature parameters
|
||||||
|
min_temp_2: float = Field(..., ge=-1, le=45, description="Минимальная температура лазера 2 (C)")
|
||||||
|
max_temp_2: float = Field(..., ge=-1, le=45, description="Максимальная температура лазера 2 (C)")
|
||||||
|
delta_temp_2: float = Field(..., ge=0.05, le=1.0, description="Шаг дискретизации температуры лазера 2 (C)")
|
||||||
|
|
||||||
|
# Laser 2 Current parameters
|
||||||
|
min_current_2: float = Field(..., ge=15, le=60, description="Минимальный ток лазера 2 (мА)")
|
||||||
|
max_current_2: float = Field(..., ge=15, le=60, description="Максимальный ток лазера 2 (мА)")
|
||||||
|
delta_current_2: float = Field(..., ge=0.002, le=0.5, description="Шаг дискретизации тока лазера 2 (мА)")
|
||||||
|
|
||||||
|
# Time parameters
|
||||||
|
delta_time: int = Field(..., ge=20, le=100, description="Шаг дискретизации времени (мкс), шаг 10")
|
||||||
|
tau: int = Field(..., ge=3, le=10, description="Время задержки (мс)")
|
||||||
|
|
||||||
|
# Enable flags for different control modes
|
||||||
|
enable_t1: bool = Field(False, description="Включить изменение температуры лазера 1")
|
||||||
|
enable_t2: bool = Field(False, description="Включить изменение температуры лазера 2")
|
||||||
|
enable_c1: bool = Field(False, description="Включить изменение тока лазера 1")
|
||||||
|
enable_c2: bool = Field(False, description="Включить изменение тока лазера 2")
|
||||||
|
|
||||||
|
@field_validator('delta_time')
|
||||||
|
@classmethod
|
||||||
|
def validate_delta_time(cls, v):
|
||||||
|
if v % 10 != 0:
|
||||||
|
raise ValueError('delta_time должен быть кратен 10 мкс')
|
||||||
|
return v
|
||||||
|
|
||||||
|
@field_validator('max_temp_1')
|
||||||
|
@classmethod
|
||||||
|
def validate_temp_1_range(cls, v, info):
|
||||||
|
if 'min_temp_1' in info.data and v < info.data['min_temp_1']:
|
||||||
|
raise ValueError('max_temp_1 должен быть больше min_temp_1')
|
||||||
|
return v
|
||||||
|
|
||||||
|
@field_validator('max_temp_2')
|
||||||
|
@classmethod
|
||||||
|
def validate_temp_2_range(cls, v, info):
|
||||||
|
if 'min_temp_2' in info.data and v < info.data['min_temp_2']:
|
||||||
|
raise ValueError('max_temp_2 должен быть больше min_temp_2')
|
||||||
|
return v
|
||||||
|
|
||||||
|
@field_validator('max_current_1')
|
||||||
|
@classmethod
|
||||||
|
def validate_current_1_range(cls, v, info):
|
||||||
|
if 'min_current_1' in info.data and v < info.data['min_current_1']:
|
||||||
|
raise ValueError('max_current_1 должен быть больше min_current_1')
|
||||||
|
return v
|
||||||
|
|
||||||
|
@field_validator('max_current_2')
|
||||||
|
@classmethod
|
||||||
|
def validate_current_2_range(cls, v, info):
|
||||||
|
if 'min_current_2' in info.data and v < info.data['min_current_2']:
|
||||||
|
raise ValueError('max_current_2 должен быть больше min_current_2')
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
class LaserStatus(BaseModel):
|
||||||
|
"""Model for laser status response"""
|
||||||
|
|
||||||
|
temp_1: Optional[float] = Field(None, description="Текущая температура лазера 1 (C)")
|
||||||
|
temp_2: Optional[float] = Field(None, description="Текущая температура лазера 2 (C)")
|
||||||
|
current_1: Optional[float] = Field(None, description="Текущий ток фотодиода 1 (мА)")
|
||||||
|
current_2: Optional[float] = Field(None, description="Текущий ток фотодиода 2 (мА)")
|
||||||
|
temp_ext_1: Optional[float] = Field(None, description="Температура внешнего термистора 1 (C)")
|
||||||
|
temp_ext_2: Optional[float] = Field(None, description="Температура внешнего термистора 2 (C)")
|
||||||
|
voltage_3v3: Optional[float] = Field(None, description="Напряжение 3V3 (В)")
|
||||||
|
voltage_5v1: Optional[float] = Field(None, description="Напряжение 5V1 (В)")
|
||||||
|
voltage_5v2: Optional[float] = Field(None, description="Напряжение 5V2 (В)")
|
||||||
|
voltage_7v0: Optional[float] = Field(None, description="Напряжение 7V0 (В)")
|
||||||
|
is_running: bool = Field(False, description="Запущен ли цикл измерений")
|
||||||
|
connected: bool = Field(False, description="Подключено ли устройство")
|
||||||
|
|
||||||
|
|
||||||
|
class LaserStartResponse(BaseModel):
|
||||||
|
"""Response model for laser start endpoint"""
|
||||||
|
|
||||||
|
success: bool = Field(..., description="Успешность операции")
|
||||||
|
message: str = Field(..., description="Сообщение о результате")
|
||||||
|
parameters: Optional[LaserParameters] = Field(None, description="Примененные параметры")
|
||||||
|
|
||||||
|
|
||||||
|
class LaserStopResponse(BaseModel):
|
||||||
|
"""Response model for laser stop endpoint"""
|
||||||
|
|
||||||
|
success: bool = Field(..., description="Успешность операции")
|
||||||
|
message: str = Field(..., description="Сообщение о результате")
|
||||||
|
|
||||||
|
|
||||||
|
class ManualLaserParameters(BaseModel):
|
||||||
|
"""Model for manual laser control with simple parameters"""
|
||||||
|
|
||||||
|
t1: float = Field(..., ge=-1, le=45, description="Температура лазера 1 (°C)")
|
||||||
|
t2: float = Field(..., ge=-1, le=45, description="Температура лазера 2 (°C)")
|
||||||
|
i1: float = Field(..., ge=15, le=70, description="Ток лазера 1 (мА)")
|
||||||
|
i2: float = Field(..., ge=15, le=60, description="Ток лазера 2 (мА)")
|
||||||
|
|
||||||
|
|
||||||
|
class ManualLaserStartResponse(BaseModel):
|
||||||
|
"""Response model for manual laser start endpoint"""
|
||||||
|
|
||||||
|
success: bool = Field(..., description="Успешность операции")
|
||||||
|
message: str = Field(..., description="Сообщение о результате")
|
||||||
|
parameters: Optional[ManualLaserParameters] = Field(None, description="Примененные параметры")
|
||||||
0
vna_system/core/__init__.py
Normal file
0
vna_system/core/__init__.py
Normal file
@ -1,5 +1,6 @@
|
|||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
|
import math
|
||||||
import os
|
import os
|
||||||
import struct
|
import struct
|
||||||
import threading
|
import threading
|
||||||
@ -192,6 +193,67 @@ class VNADataAcquisition:
|
|||||||
# --------------------------------------------------------------------- #
|
# --------------------------------------------------------------------- #
|
||||||
# Acquisition loop
|
# Acquisition loop
|
||||||
# --------------------------------------------------------------------- #
|
# --------------------------------------------------------------------- #
|
||||||
|
def _radar_pipe_acquisition_loop(self) -> None:
|
||||||
|
"""Acquisition loop for reading radar data from named pipe."""
|
||||||
|
logger.info("Starting radar pipe acquisition loop", pipe_path=cfg.RADAR_PIPE_PATH)
|
||||||
|
|
||||||
|
while self._running and not self._stop_event.is_set():
|
||||||
|
try:
|
||||||
|
# Honor pause
|
||||||
|
if self._paused:
|
||||||
|
time.sleep(0.1)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Open named pipe for reading
|
||||||
|
pipe_path = Path(cfg.RADAR_PIPE_PATH)
|
||||||
|
|
||||||
|
# Check if pipe exists
|
||||||
|
if not pipe_path.exists():
|
||||||
|
logger.warning("Radar pipe not found, waiting...", path=str(pipe_path))
|
||||||
|
time.sleep(1.0)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Open pipe in non-blocking mode to avoid hanging
|
||||||
|
with open(pipe_path, "rb") as pipe:
|
||||||
|
# Read data from pipe (adjust buffer size as needed)
|
||||||
|
data = pipe.read(cfg.EXPECTED_POINTS_PER_SWEEP * cfg.RADAR_BYTES_PER_SAMPLE)
|
||||||
|
|
||||||
|
if not data:
|
||||||
|
time.sleep(0.01)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Parse radar data
|
||||||
|
points = self._parse_radar_data(data)
|
||||||
|
|
||||||
|
if points:
|
||||||
|
timestamp = time.time()
|
||||||
|
sweep_number = self._sweep_buffer.add_sweep(points, timestamp=timestamp)
|
||||||
|
logger.info(
|
||||||
|
"Radar sweep collected from pipe",
|
||||||
|
sweep_number=sweep_number,
|
||||||
|
points=len(points),
|
||||||
|
timestamp=timestamp
|
||||||
|
)
|
||||||
|
|
||||||
|
# # Play sweep notification sound
|
||||||
|
# self._sound_player.play()
|
||||||
|
|
||||||
|
# Handle single-sweep mode transitions
|
||||||
|
if not self._continuous_mode:
|
||||||
|
if self._single_sweep_requested:
|
||||||
|
self._single_sweep_requested = False
|
||||||
|
logger.info("Single radar sweep completed; pausing acquisition")
|
||||||
|
self.pause()
|
||||||
|
else:
|
||||||
|
self.pause()
|
||||||
|
|
||||||
|
except FileNotFoundError:
|
||||||
|
logger.warning("Radar pipe not found, retrying...", pipe_path=cfg.RADAR_PIPE_PATH)
|
||||||
|
time.sleep(1.0)
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
logger.error("Radar pipe acquisition loop error", error=repr(exc))
|
||||||
|
time.sleep(1.0)
|
||||||
|
|
||||||
def _simulator_acquisition_loop(self) -> None:
|
def _simulator_acquisition_loop(self) -> None:
|
||||||
"""Simplified acquisition loop for simulator mode."""
|
"""Simplified acquisition loop for simulator mode."""
|
||||||
logger.info("Starting simulator acquisition loop")
|
logger.info("Starting simulator acquisition loop")
|
||||||
@ -236,6 +298,11 @@ class VNADataAcquisition:
|
|||||||
|
|
||||||
def _acquisition_loop(self) -> None:
|
def _acquisition_loop(self) -> None:
|
||||||
"""Main acquisition loop executed by the background thread."""
|
"""Main acquisition loop executed by the background thread."""
|
||||||
|
# Use radar pipe loop if enabled
|
||||||
|
if cfg.USE_RADAR_PIPE:
|
||||||
|
self._radar_pipe_acquisition_loop()
|
||||||
|
return
|
||||||
|
|
||||||
# Use simulator loop if simulator is enabled
|
# Use simulator loop if simulator is enabled
|
||||||
if self._simulator is not None:
|
if self._simulator is not None:
|
||||||
self._simulator_acquisition_loop()
|
self._simulator_acquisition_loop()
|
||||||
@ -468,6 +535,74 @@ class VNADataAcquisition:
|
|||||||
# --------------------------------------------------------------------- #
|
# --------------------------------------------------------------------- #
|
||||||
# Parsing & detection
|
# Parsing & detection
|
||||||
# --------------------------------------------------------------------- #
|
# --------------------------------------------------------------------- #
|
||||||
|
|
||||||
|
def _parse_radar_data(self, data: bytes) -> list[tuple[float, float]]:
|
||||||
|
"""
|
||||||
|
Parse radar data from named pipe format.
|
||||||
|
|
||||||
|
Expected format: 4 bytes per 32-bit word (big-endian)
|
||||||
|
- Word format: 0xF0XXXXXX where:
|
||||||
|
- F0 is the marker byte (bits 31-24)
|
||||||
|
- XXXXXX is the 24-bit data value (bits 23-0)
|
||||||
|
|
||||||
|
Returns list of (real, imag) tuples where:
|
||||||
|
- real: dB value converted from raw data
|
||||||
|
- imag: always 0.0
|
||||||
|
"""
|
||||||
|
points: list[tuple[float, float]] = []
|
||||||
|
|
||||||
|
# Process data in 4-byte chunks as 32-bit words (big-endian)
|
||||||
|
num_words = len(data) // cfg.RADAR_BYTES_PER_SAMPLE
|
||||||
|
|
||||||
|
for i in range(num_words):
|
||||||
|
|
||||||
|
offset = i * cfg.RADAR_BYTES_PER_SAMPLE
|
||||||
|
chunk = data[offset : offset + cfg.RADAR_BYTES_PER_SAMPLE]
|
||||||
|
|
||||||
|
# Unpack as big-endian 32-bit unsigned integer
|
||||||
|
word = struct.unpack("<I", chunk)[0]
|
||||||
|
|
||||||
|
# Extract marker (top 8 bits)
|
||||||
|
marker = (word >> 24) & 0xFF
|
||||||
|
|
||||||
|
# Extract 24-bit data value (lower 24 bits)
|
||||||
|
raw_value = word & 0xFFFFFF
|
||||||
|
|
||||||
|
# Check marker byte - log warning but continue processing
|
||||||
|
if marker != cfg.RADAR_DATA_MARKER:
|
||||||
|
# Only log occasionally to avoid spam
|
||||||
|
if i % 100 == 0:
|
||||||
|
logger.debug(
|
||||||
|
"Non-F0 marker detected",
|
||||||
|
marker=hex(marker),
|
||||||
|
word=hex(word),
|
||||||
|
offset=offset
|
||||||
|
)
|
||||||
|
# Still process the data if it has valid bits
|
||||||
|
|
||||||
|
# Convert unsigned 24-bit to signed 24-bit (as in main.py to_int24)
|
||||||
|
# If MSB is set, treat as negative (two's complement)
|
||||||
|
if raw_value & 0x800000:
|
||||||
|
raw_value -= 0x1000000 # Convert to signed: -8388608 to +8388607
|
||||||
|
|
||||||
|
points.append((float(raw_value), 0.))
|
||||||
|
if i == 0 or i == 100:
|
||||||
|
logger.debug(f"raw_value: {raw_value}, marker: {marker}, word= {word}" )
|
||||||
|
|
||||||
|
# Log statistics about parsed data
|
||||||
|
if points:
|
||||||
|
values = [p[0] for p in points]
|
||||||
|
logger.info(
|
||||||
|
"📊 Radar data parsed from pipe",
|
||||||
|
total_points=len(points),
|
||||||
|
min_value=min(values),
|
||||||
|
max_value=max(values),
|
||||||
|
mean_value=sum(values) / len(values) if values else 0,
|
||||||
|
non_zero=sum(1 for v in values if v != 0)
|
||||||
|
)
|
||||||
|
|
||||||
|
return points
|
||||||
|
|
||||||
def _parse_measurement_data(self, payload: bytes) -> list[tuple[float, float]]:
|
def _parse_measurement_data(self, payload: bytes) -> list[tuple[float, float]]:
|
||||||
"""Parse complex measurement samples (float32 pairs) from a payload."""
|
"""Parse complex measurement samples (float32 pairs) from a payload."""
|
||||||
if len(payload) <= cfg.MEAS_HEADER_LEN:
|
if len(payload) <= cfg.MEAS_HEADER_LEN:
|
||||||
|
|||||||
@ -35,10 +35,18 @@ VNA_PID = 0x5740 # STM32 Virtual ComPort
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Simulator mode settings
|
# Simulator mode settings
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
USE_SIMULATOR = True # Set to True to use simulator instead of real device
|
USE_SIMULATOR = False # Set to True to use simulator instead of real device
|
||||||
SIMULATOR_SWEEP_FILE = BASE_DIR / "binary_input" / "sweep_example" / "example.json"
|
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
|
SIMULATOR_NOISE_LEVEL = 100 # Standard deviation of Gaussian noise to add to real and imaginary parts
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Radar pipe mode settings
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
USE_RADAR_PIPE = True # Set to True to read radar data from named pipe
|
||||||
|
RADAR_PIPE_PATH = "/tmp/radar_data_pipe" # Path to the named pipe
|
||||||
|
RADAR_DATA_MARKER = 0xF0 # First byte marker for radar data packets
|
||||||
|
RADAR_BYTES_PER_SAMPLE = 4 # Total bytes per radar sample (1 marker + 3 data)
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Sweep detection and parsing constants
|
# Sweep detection and parsing constants
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@ -46,7 +54,7 @@ SWEEP_CMD_LEN = 515
|
|||||||
SWEEP_CMD_PREFIX = bytes([0xAA, 0x00, 0xDA])
|
SWEEP_CMD_PREFIX = bytes([0xAA, 0x00, 0xDA])
|
||||||
MEAS_HEADER_LEN = 21
|
MEAS_HEADER_LEN = 21
|
||||||
MEAS_CMDS_PER_SWEEP = 17
|
MEAS_CMDS_PER_SWEEP = 17
|
||||||
EXPECTED_POINTS_PER_SWEEP = 1000
|
EXPECTED_POINTS_PER_SWEEP = 1024
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Buffer settings
|
# Buffer settings
|
||||||
|
|||||||
3
vna_system/core/laser/__init__.py
Normal file
3
vna_system/core/laser/__init__.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from .laser_controller import LaserController
|
||||||
|
|
||||||
|
__all__ = ['LaserController']
|
||||||
606
vna_system/core/laser/laser_controller.py
Normal file
606
vna_system/core/laser/laser_controller.py
Normal file
@ -0,0 +1,606 @@
|
|||||||
|
import logging
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional, Dict, Any
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from ...api.models.laser import LaserParameters, LaserStatus
|
||||||
|
from .RFG_PCB_PC_controller_supersimple import device_interaction as dev
|
||||||
|
from .RFG_PCB_PC_controller_supersimple import device_commands as cmd
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class LaserController:
|
||||||
|
"""
|
||||||
|
Controller for laser control system.
|
||||||
|
|
||||||
|
Uses RFG_PCB_PC_controller_supersimple module for device communication.
|
||||||
|
Communicates with RadioPhotonic board via serial port (115200 baud).
|
||||||
|
Supports both manual control and automated scanning modes.
|
||||||
|
|
||||||
|
Control Logic (RFG implementation):
|
||||||
|
- Manual mode: send_control_parameters() to set steady current/temperature
|
||||||
|
- Scan mode: send_control_parameters() THEN send_task_command() for scanning
|
||||||
|
- Stop: send_control_parameters() to return to steady state (not reset)
|
||||||
|
|
||||||
|
TaskType format: String-based ("TT_CHANGE_CURR_1", "TT_CHANGE_CURR_2")
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, config_path: Optional[str] = None):
|
||||||
|
self.prt = None # Serial port object
|
||||||
|
self.is_connected = False
|
||||||
|
self.is_running = False
|
||||||
|
self.current_parameters: Optional[LaserParameters] = None
|
||||||
|
self.current_status = LaserStatus()
|
||||||
|
self.last_data: Optional[Dict[str, Any]] = None
|
||||||
|
|
||||||
|
# Load default parameters from JSON
|
||||||
|
self._load_default_parameters(config_path)
|
||||||
|
|
||||||
|
logger.info("LaserController initialized")
|
||||||
|
|
||||||
|
def _load_default_parameters(self, config_path: Optional[str] = None) -> None:
|
||||||
|
"""
|
||||||
|
Load default parameters from JSON configuration file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config_path: Path to JSON config file. If None, uses default location.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Determine config file path
|
||||||
|
if config_path is None:
|
||||||
|
# Default path relative to this file
|
||||||
|
current_dir = Path(__file__).parent
|
||||||
|
config_path = current_dir / "RadioPhotonic_PCB_PC_software" / "init_params.json"
|
||||||
|
else:
|
||||||
|
config_path = Path(config_path)
|
||||||
|
|
||||||
|
# Load JSON file
|
||||||
|
if not config_path.exists():
|
||||||
|
logger.warning(f"Config file not found: {config_path}, using hardcoded defaults")
|
||||||
|
self._set_hardcoded_defaults()
|
||||||
|
return
|
||||||
|
|
||||||
|
with open(config_path, 'r') as f:
|
||||||
|
config = json.load(f)
|
||||||
|
|
||||||
|
# Set parameters from config
|
||||||
|
self.initial_temperature_1 = config.get("INITIAL_TEMPERATURE_1", 28)
|
||||||
|
self.initial_temperature_2 = config.get("INITIAL_TEMPERATURE_2", 28.9)
|
||||||
|
self.initial_current_1 = config.get("INITIAL_CURRENT_1", 33)
|
||||||
|
self.initial_current_2 = config.get("INITIAL_CURRENT_2", 35)
|
||||||
|
|
||||||
|
# PI coefficients (multiplied by 256 as per device protocol)
|
||||||
|
self.proportional_coeff_1 = int(config.get("PROPORTIONAL_COEFF_1", 10) * 256)
|
||||||
|
self.proportional_coeff_2 = int(config.get("PROPORTIONAL_COEFF_2", 10) * 256)
|
||||||
|
self.integral_coeff_1 = int(config.get("INTEGRAL_COEFF_1", 0.5) * 256)
|
||||||
|
self.integral_coeff_2 = int(config.get("INTEGRAL_COEFF_2", 0.5) * 256)
|
||||||
|
self.message_id = config.get("MESSAGE_ID", "00FF")
|
||||||
|
|
||||||
|
# Additional parameters
|
||||||
|
self.gui_timeout_interval = config.get("GUI_TIMEOUT_INTERVAL", 5)
|
||||||
|
self.save_points_number = config.get("SAVE_POINTS_NUMBER", 1000)
|
||||||
|
|
||||||
|
logger.info(f"Default parameters loaded from {config_path}")
|
||||||
|
logger.info(f" Initial T1: {self.initial_temperature_1}°C, T2: {self.initial_temperature_2}°C")
|
||||||
|
logger.info(f" Initial I1: {self.initial_current_1} mA, I2: {self.initial_current_2} mA")
|
||||||
|
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
logger.error(f"Invalid JSON in config file: {e}")
|
||||||
|
self._set_hardcoded_defaults()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error loading config file: {e}")
|
||||||
|
self._set_hardcoded_defaults()
|
||||||
|
|
||||||
|
def _set_hardcoded_defaults(self) -> None:
|
||||||
|
"""Set hardcoded default parameters as fallback."""
|
||||||
|
self.initial_temperature_1 = 28
|
||||||
|
self.initial_temperature_2 = 28.9
|
||||||
|
self.initial_current_1 = 33
|
||||||
|
self.initial_current_2 = 35
|
||||||
|
self.proportional_coeff_1 = int(10 * 256)
|
||||||
|
self.proportional_coeff_2 = int(10 * 256)
|
||||||
|
self.integral_coeff_1 = int(0.5 * 256)
|
||||||
|
self.integral_coeff_2 = int(0.5 * 256)
|
||||||
|
self.message_id = "00FF"
|
||||||
|
self.gui_timeout_interval = 5
|
||||||
|
self.save_points_number = 1000
|
||||||
|
logger.info("Using hardcoded default parameters")
|
||||||
|
|
||||||
|
def start_cycle(self, parameters: LaserParameters) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Start laser control cycle with given parameters.
|
||||||
|
|
||||||
|
Determines mode (manual vs scan) based on enable flags:
|
||||||
|
- Manual mode: all enable_* flags are False
|
||||||
|
- Scan mode: one enable_* flag is True
|
||||||
|
|
||||||
|
Args:
|
||||||
|
parameters: LaserParameters object with control settings
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with success status and message
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if not self.is_connected or self.prt is None:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": "Устройство не подключено. Сначала выполните подключение.",
|
||||||
|
"parameters": None
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("=" * 60)
|
||||||
|
logger.info("LASER CONTROL: START CYCLE")
|
||||||
|
logger.info(f"Timestamp: {datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')}")
|
||||||
|
logger.info("-" * 60)
|
||||||
|
|
||||||
|
# Determine operation mode
|
||||||
|
is_manual = not any([
|
||||||
|
parameters.enable_t1,
|
||||||
|
parameters.enable_t2,
|
||||||
|
parameters.enable_c1,
|
||||||
|
parameters.enable_c2
|
||||||
|
])
|
||||||
|
|
||||||
|
if is_manual:
|
||||||
|
logger.info("Mode: MANUAL CONTROL")
|
||||||
|
result = self._start_manual_mode(parameters)
|
||||||
|
else:
|
||||||
|
logger.info("Mode: AUTOMATED SCAN")
|
||||||
|
result = self._start_scan_mode(parameters)
|
||||||
|
|
||||||
|
if result["success"]:
|
||||||
|
self.current_parameters = parameters
|
||||||
|
self.is_running = True
|
||||||
|
self.current_status.is_running = True
|
||||||
|
|
||||||
|
logger.info("=" * 60)
|
||||||
|
return result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error starting laser cycle: {e}", exc_info=True)
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": f"Ошибка при запуске цикла: {str(e)}",
|
||||||
|
"parameters": None
|
||||||
|
}
|
||||||
|
|
||||||
|
def _start_manual_mode(self, parameters: LaserParameters) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Start manual control mode with fixed T1, T2, I1, I2 values.
|
||||||
|
Uses DECODE_ENABLE (0x1111) command - simplified version from RFG example.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Prepare control parameters (simplified - direct send)
|
||||||
|
ctrl_params = {
|
||||||
|
'Temp_1': parameters.min_temp_1,
|
||||||
|
'Temp_2': parameters.min_temp_2,
|
||||||
|
'Iset_1': parameters.min_current_1,
|
||||||
|
'Iset_2': parameters.min_current_2,
|
||||||
|
'ProportionalCoeff_1': self.proportional_coeff_1,
|
||||||
|
'ProportionalCoeff_2': self.proportional_coeff_2,
|
||||||
|
'IntegralCoeff_1': self.integral_coeff_1,
|
||||||
|
'IntegralCoeff_2': self.integral_coeff_2,
|
||||||
|
'Message_ID': self.message_id
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(f"Sending manual control parameters:")
|
||||||
|
logger.info(f" T1: {ctrl_params['Temp_1']}°C, T2: {ctrl_params['Temp_2']}°C")
|
||||||
|
logger.info(f" I1: {ctrl_params['Iset_1']} mA, I2: {ctrl_params['Iset_2']} mA")
|
||||||
|
|
||||||
|
# Send control parameters to device (steady current mode)
|
||||||
|
dev.send_control_parameters(self.prt, ctrl_params)
|
||||||
|
logger.info(dev.request_data(self.prt))
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": "Ручное управление запущено",
|
||||||
|
"parameters": parameters.model_dump()
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in manual mode: {e}", exc_info=True)
|
||||||
|
raise
|
||||||
|
|
||||||
|
def _start_scan_mode(self, parameters: LaserParameters) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Start automated scan mode - simplified version from RFG example.
|
||||||
|
RFG flow: send_control_parameters() THEN send_task_command()
|
||||||
|
This initializes the device before starting the scan.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Step 1: Start lasers with initial control parameters (from RFG example lines 72-75)
|
||||||
|
logger.info("Step 1: Starting lasers with control parameters...")
|
||||||
|
ctrl_params = {
|
||||||
|
'Temp_1': parameters.min_temp_1,
|
||||||
|
'Temp_2': parameters.min_temp_2,
|
||||||
|
'Iset_1': parameters.min_current_1,
|
||||||
|
'Iset_2': parameters.min_current_2,
|
||||||
|
'ProportionalCoeff_1': self.proportional_coeff_1,
|
||||||
|
'ProportionalCoeff_2': self.proportional_coeff_2,
|
||||||
|
'IntegralCoeff_1': self.integral_coeff_1,
|
||||||
|
'IntegralCoeff_2': self.integral_coeff_2,
|
||||||
|
'Message_ID': self.message_id
|
||||||
|
}
|
||||||
|
|
||||||
|
dev.send_control_parameters(self.prt, ctrl_params)
|
||||||
|
logger.info(dev.request_data(self.prt))
|
||||||
|
logger.info("Control parameters sent - lasers started")
|
||||||
|
|
||||||
|
# Small delay as in RFG example (line 75: sleep(2))
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
# Step 2: Determine which parameter to scan
|
||||||
|
if parameters.enable_c1:
|
||||||
|
task_type = "TT_CHANGE_CURR_1" # RFG uses string format
|
||||||
|
scan_param = "Current Laser 1"
|
||||||
|
logger.info(f"Scanning Current Laser 1: {parameters.min_current_1} to {parameters.max_current_1} mA")
|
||||||
|
elif parameters.enable_c2:
|
||||||
|
task_type = "TT_CHANGE_CURR_2" # RFG uses string format
|
||||||
|
scan_param = "Current Laser 2"
|
||||||
|
logger.info(f"Scanning Current Laser 2: {parameters.min_current_2} to {parameters.max_current_2} mA")
|
||||||
|
elif parameters.enable_t1:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": "Сканирование температуры Laser 1 не поддерживается устройством",
|
||||||
|
"parameters": None
|
||||||
|
}
|
||||||
|
elif parameters.enable_t2:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": "Сканирование температуры Laser 2 не поддерживается устройством",
|
||||||
|
"parameters": None
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": "Не выбран параметр для сканирования",
|
||||||
|
"parameters": None
|
||||||
|
}
|
||||||
|
|
||||||
|
# Step 3: Build task parameters for scan (from RFG example lines 78-82)
|
||||||
|
logger.info("Step 2: Switching to current sweep mode...")
|
||||||
|
task_params = {
|
||||||
|
'TaskType': task_type,
|
||||||
|
'Dt': parameters.delta_time ,
|
||||||
|
'Tau': parameters.tau,
|
||||||
|
'ProportionalCoeff_1': self.proportional_coeff_1,
|
||||||
|
'ProportionalCoeff_2': self.proportional_coeff_2,
|
||||||
|
'IntegralCoeff_1': self.integral_coeff_1,
|
||||||
|
'IntegralCoeff_2': self.integral_coeff_2
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add scan-specific parameters
|
||||||
|
if task_type == "TT_CHANGE_CURR_1":
|
||||||
|
task_params.update({
|
||||||
|
'MinC1': parameters.min_current_1,
|
||||||
|
'MaxC1': parameters.max_current_1,
|
||||||
|
'DeltaC1': parameters.delta_current_1,
|
||||||
|
'T1': parameters.min_temp_1, # Fixed
|
||||||
|
'I2': parameters.min_current_2, # Fixed
|
||||||
|
'T2': parameters.min_temp_2 # Fixed
|
||||||
|
})
|
||||||
|
elif task_type == "TT_CHANGE_CURR_2":
|
||||||
|
task_params.update({
|
||||||
|
'MinC2': parameters.min_current_2,
|
||||||
|
'MaxC2': parameters.max_current_2,
|
||||||
|
'DeltaC2': parameters.delta_current_2,
|
||||||
|
'T2': parameters.min_temp_2, # Fixed
|
||||||
|
'I1': parameters.min_current_1, # Fixed
|
||||||
|
'T1': parameters.min_temp_1 # Fixed
|
||||||
|
})
|
||||||
|
|
||||||
|
# Send task command to device (start current variation)
|
||||||
|
dev.send_task_command(self.prt, task_params)
|
||||||
|
# print(dev.request_data(self.prt))
|
||||||
|
logger.info("Task command sent successfully - scan started")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": f"Сканирование запущено: {scan_param}",
|
||||||
|
"parameters": parameters.model_dump()
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in scan mode: {e}", exc_info=True)
|
||||||
|
raise
|
||||||
|
|
||||||
|
def stop_cycle(self) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Stop current laser control cycle.
|
||||||
|
Uses send_control_parameters to return to steady current mode (RFG example logic).
|
||||||
|
This stops any ongoing scan and maintains the last known parameters.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with success status and message
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
logger.info("=" * 60)
|
||||||
|
logger.info("LASER CONTROL: STOP CYCLE")
|
||||||
|
logger.info(f"Timestamp: {datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')}")
|
||||||
|
logger.info("=" * 60)
|
||||||
|
|
||||||
|
if self.is_connected and self.prt is not None:
|
||||||
|
# Stop current variation - go to steady current mode
|
||||||
|
# Use last known parameters or defaults
|
||||||
|
if self.current_parameters:
|
||||||
|
ctrl_params = {
|
||||||
|
'Temp_1': self.current_parameters.min_temp_1,
|
||||||
|
'Temp_2': self.current_parameters.min_temp_2,
|
||||||
|
'Iset_1': self.current_parameters.min_current_1,
|
||||||
|
'Iset_2': self.current_parameters.min_current_2,
|
||||||
|
'ProportionalCoeff_1': self.proportional_coeff_1,
|
||||||
|
'ProportionalCoeff_2': self.proportional_coeff_2,
|
||||||
|
'IntegralCoeff_1': self.integral_coeff_1,
|
||||||
|
'IntegralCoeff_2': self.integral_coeff_2,
|
||||||
|
'Message_ID': self.message_id
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(f"Stopping scan - returning to steady current mode:")
|
||||||
|
logger.info(f" T1: {ctrl_params['Temp_1']}°C, T2: {ctrl_params['Temp_2']}°C")
|
||||||
|
logger.info(f" I1: {ctrl_params['Iset_1']} mA, I2: {ctrl_params['Iset_2']} mA")
|
||||||
|
|
||||||
|
try:
|
||||||
|
dev.send_control_parameters(self.prt, ctrl_params)
|
||||||
|
logger.info(dev.request_data(self.prt))
|
||||||
|
logger.info("Control parameters sent - laser in steady state")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to send control parameters: {e}")
|
||||||
|
else:
|
||||||
|
logger.warning("No current parameters stored, cannot send steady state command")
|
||||||
|
|
||||||
|
self.is_running = False
|
||||||
|
self.current_status.is_running = False
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": "Цикл управления лазером остановлен (steady current mode)"
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error stopping laser cycle: {e}", exc_info=True)
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": f"Ошибка при остановке цикла: {str(e)}"
|
||||||
|
}
|
||||||
|
|
||||||
|
def start_manual_direct(self, t1: float, t2: float, i1: float, i2: float) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Start manual control mode with direct T1, T2, I1, I2 parameters.
|
||||||
|
Simplified interface for manual control.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
t1: Temperature for Laser 1 in Celsius
|
||||||
|
t2: Temperature for Laser 2 in Celsius
|
||||||
|
i1: Current for Laser 1 in mA
|
||||||
|
i2: Current for Laser 2 in mA
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with success status and message
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Check connection status
|
||||||
|
if not self.is_connected:
|
||||||
|
logger.error("Device not connected")
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": "Устройство не подключено. Нажмите кнопку 'Подключить' в настройках.",
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.prt is None:
|
||||||
|
logger.error("Serial port is None")
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": "Порт не инициализирован. Переподключите устройство.",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if port is actually open
|
||||||
|
if not self.prt.is_open:
|
||||||
|
logger.error("Serial port is not open")
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": "Порт не открыт. Переподключите устройство.",
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("=" * 60)
|
||||||
|
logger.info("LASER CONTROL: START MANUAL MODE (Direct)")
|
||||||
|
logger.info(f"Timestamp: {datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')}")
|
||||||
|
logger.info(f"Port: {self.prt.port}, Open: {self.prt.is_open}")
|
||||||
|
logger.info(f" T1: {t1}°C, T2: {t2}°C")
|
||||||
|
logger.info(f" I1: {i1} mA, I2: {i2} mA")
|
||||||
|
logger.info("=" * 60)
|
||||||
|
|
||||||
|
# Prepare control parameters
|
||||||
|
params = {
|
||||||
|
'Temp_1': t1,
|
||||||
|
'Temp_2': t2,
|
||||||
|
'Iset_1': i1,
|
||||||
|
'Iset_2': i2,
|
||||||
|
'ProportionalCoeff_1': self.proportional_coeff_1,
|
||||||
|
'ProportionalCoeff_2': self.proportional_coeff_2,
|
||||||
|
'IntegralCoeff_1': self.integral_coeff_1,
|
||||||
|
'IntegralCoeff_2': self.integral_coeff_2,
|
||||||
|
'Message_ID': self.message_id
|
||||||
|
}
|
||||||
|
|
||||||
|
# Send control parameters to device
|
||||||
|
logger.info("Sending control parameters to device...")
|
||||||
|
dev.send_control_parameters(self.prt, params)
|
||||||
|
logger.info("Control parameters sent successfully")
|
||||||
|
|
||||||
|
self.is_running = True
|
||||||
|
self.current_status.is_running = True
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": "Ручное управление запущено",
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error starting manual control: {e}", exc_info=True)
|
||||||
|
# Reset connection status if error
|
||||||
|
self.is_connected = False
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": f"Ошибка при запуске: {str(e)}. Попробуйте переподключить устройство.",
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_status(self) -> LaserStatus:
|
||||||
|
"""
|
||||||
|
Get current laser status by requesting data from device.
|
||||||
|
Uses TRANS_ENABLE (0x4444) command.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
LaserStatus object with current state
|
||||||
|
"""
|
||||||
|
self.current_status.connected = self.is_connected
|
||||||
|
self.current_status.is_running = self.is_running
|
||||||
|
|
||||||
|
if self.is_connected and self.prt is not None:
|
||||||
|
try:
|
||||||
|
# Request data from device
|
||||||
|
data = dev.request_data(self.prt)
|
||||||
|
|
||||||
|
if data and isinstance(data, dict):
|
||||||
|
# Update status from device data
|
||||||
|
self.last_data = data
|
||||||
|
self.current_status.temp_1 = data.get('Temp_1', 0.0)
|
||||||
|
self.current_status.temp_2 = data.get('Temp_2', 0.0)
|
||||||
|
self.current_status.current_1 = data.get('I1', 0.0)
|
||||||
|
self.current_status.current_2 = data.get('I2', 0.0)
|
||||||
|
self.current_status.temp_ext_1 = data.get('Temp_Ext_1', 0.0)
|
||||||
|
self.current_status.temp_ext_2 = data.get('Temp_Ext_2', 0.0)
|
||||||
|
self.current_status.voltage_3v3 = data.get('MON_3V3', 0.0)
|
||||||
|
self.current_status.voltage_5v1 = data.get('MON_5V1', 0.0)
|
||||||
|
self.current_status.voltage_5v2 = data.get('MON_5V2', 0.0)
|
||||||
|
self.current_status.voltage_7v0 = data.get('MON_7V0', 0.0)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Error requesting status from device: {e}")
|
||||||
|
# Keep previous status values on error
|
||||||
|
|
||||||
|
return self.current_status
|
||||||
|
|
||||||
|
def connect(self, port: Optional[str] = None) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Connect to laser control hardware via serial port.
|
||||||
|
Auto-detects USB serial ports if port not specified.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
port: Serial port to connect to (e.g., '/dev/ttyUSB0')
|
||||||
|
If None, will auto-detect USB ports
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with success status and message
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
logger.info(f"Attempting to connect to laser hardware on port: {port or 'auto-detect'}")
|
||||||
|
|
||||||
|
if self.is_connected:
|
||||||
|
logger.warning("Already connected to device")
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": "Уже подключено к устройству",
|
||||||
|
"port": str(self.prt.port) if self.prt else "unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create port connection (auto-detect if port not specified)
|
||||||
|
if port:
|
||||||
|
# Manual port specification
|
||||||
|
try:
|
||||||
|
self.prt = cmd.setup_port_connection(port=port, baudrate=115200, timeout_sec=1)
|
||||||
|
cmd.open_port(self.prt)
|
||||||
|
dev.reset_port_settings(self.prt)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to connect to specified port {port}: {e}")
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": f"Не удалось подключиться к порту {port}: {str(e)}",
|
||||||
|
"port": port
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
# Auto-detect USB ports
|
||||||
|
self.prt = dev.create_port_connection()
|
||||||
|
|
||||||
|
if self.prt is None:
|
||||||
|
logger.error("Failed to create port connection")
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": "Не удалось найти устройство. Проверьте подключение USB.",
|
||||||
|
"port": None
|
||||||
|
}
|
||||||
|
|
||||||
|
self.is_connected = True
|
||||||
|
self.current_status.connected = True
|
||||||
|
|
||||||
|
port_name = self.prt.port if hasattr(self.prt, 'port') else "unknown"
|
||||||
|
logger.info(f"Successfully connected to laser hardware on {port_name}")
|
||||||
|
|
||||||
|
# Request initial status
|
||||||
|
try:
|
||||||
|
time.sleep(0.2) # Give device time to initialize
|
||||||
|
self.get_status()
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to get initial status: {e}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": f"Подключено к устройству на порту {port_name}",
|
||||||
|
"port": port_name
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error connecting to laser hardware: {e}", exc_info=True)
|
||||||
|
self.is_connected = False
|
||||||
|
self.prt = None
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": f"Ошибка подключения: {str(e)}",
|
||||||
|
"port": port
|
||||||
|
}
|
||||||
|
|
||||||
|
def disconnect(self) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Disconnect from laser control hardware.
|
||||||
|
Stops any running cycle and closes serial port.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with success status and message
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
logger.info("Disconnecting from laser hardware")
|
||||||
|
|
||||||
|
# Stop any running cycle first
|
||||||
|
if self.is_running:
|
||||||
|
self.stop_cycle()
|
||||||
|
|
||||||
|
# Close serial port using RFG close_connection
|
||||||
|
if self.prt is not None:
|
||||||
|
try:
|
||||||
|
dev.close_connection(self.prt)
|
||||||
|
logger.info("Serial port closed")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Error closing serial port: {e}")
|
||||||
|
# Fallback: just set to None
|
||||||
|
self.prt = None
|
||||||
|
else:
|
||||||
|
self.prt = None
|
||||||
|
|
||||||
|
self.is_connected = False
|
||||||
|
self.current_status.connected = False
|
||||||
|
self.last_data = None
|
||||||
|
|
||||||
|
logger.info("Successfully disconnected from laser hardware")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": "Отключено от устройства"
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error disconnecting from laser hardware: {e}", exc_info=True)
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": f"Ошибка отключения: {str(e)}"
|
||||||
|
}
|
||||||
@ -1,11 +1,16 @@
|
|||||||
{
|
{
|
||||||
"open_air": false,
|
"open_air": false,
|
||||||
"axis": "abs",
|
"axis": "abs",
|
||||||
"cut": 0.244,
|
"cut": 0.23,
|
||||||
"max": 1.0,
|
"max": 1.5,
|
||||||
"gain": 1.0,
|
"gain": 1.0,
|
||||||
"start_freq": 600.0,
|
"start_freq": 470.0,
|
||||||
"stop_freq": 6080.0,
|
"stop_freq": 8800.0,
|
||||||
"clear_history": false,
|
"clear_history": false,
|
||||||
|
"sigma": 1.44,
|
||||||
|
"border_border_m": 0.41,
|
||||||
|
"if_normalize": false,
|
||||||
|
"if_draw_level": false,
|
||||||
|
"detection_level": 3.0,
|
||||||
"data_limit": 500
|
"data_limit": 500
|
||||||
}
|
}
|
||||||
@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"y_min": -50,
|
"y_min": -80,
|
||||||
"y_max": 40,
|
"y_max": 40,
|
||||||
"autoscale": true,
|
"autoscale": false,
|
||||||
"show_magnitude": true,
|
"show_magnitude": true,
|
||||||
"show_phase": true,
|
"show_phase": false,
|
||||||
"open_air": false
|
"open_air": false
|
||||||
}
|
}
|
||||||
19
vna_system/core/processors/configs/rfg_radar_config.json
Normal file
19
vna_system/core/processors/configs/rfg_radar_config.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"data_type": "SYNC_DET",
|
||||||
|
"pont_in_one_fq_change": 86,
|
||||||
|
"gaussian_sigma": 5.0,
|
||||||
|
"fft0_delta": 5,
|
||||||
|
"standard_raw_size": 64000,
|
||||||
|
"standard_sync_size": 1000,
|
||||||
|
"freq_start_ghz": 3.0,
|
||||||
|
"freq_stop_ghz": 13.67,
|
||||||
|
"fq_end": 512,
|
||||||
|
"show_processed": true,
|
||||||
|
"show_fourier": true,
|
||||||
|
"normalize_signal": false,
|
||||||
|
"log_scale": false,
|
||||||
|
"disable_interpolation": false,
|
||||||
|
"y_max_processed": 900000.0,
|
||||||
|
"y_max_fourier": 1000.0,
|
||||||
|
"auto_scale": false
|
||||||
|
}
|
||||||
@ -0,0 +1,197 @@
|
|||||||
|
# RFG Processor - Радиофотонный радар
|
||||||
|
|
||||||
|
## Описание
|
||||||
|
|
||||||
|
RFGProcessor - новый процессор для обработки данных радиофотонного радара, основанный на алгоритмах из `RFG_Receiver_GUI/main.py`.
|
||||||
|
|
||||||
|
## Основные возможности
|
||||||
|
|
||||||
|
### Типы данных
|
||||||
|
- **SYNC_DET (0xF0)** - Данные синхронного детектирования (~1000 сэмплов)
|
||||||
|
- **RAW (0xD0)** - Сырые ADC данные с меандровой модуляцией (~64000 сэмплов)
|
||||||
|
|
||||||
|
### Обработка данных
|
||||||
|
|
||||||
|
#### Для SYNC_DET (по умолчанию):
|
||||||
|
1. Интерполяция до 1000 точек
|
||||||
|
2. Прямая FFT обработка (данные уже демодулированы)
|
||||||
|
3. Гауссово сглаживание
|
||||||
|
4. Обнуление центра FFT
|
||||||
|
|
||||||
|
#### Для RAW:
|
||||||
|
1. Интерполяция до 64000 точек
|
||||||
|
2. Генерация меандрового сигнала
|
||||||
|
3. Синхронное детектирование (умножение на меандр)
|
||||||
|
4. Частотная сегментация (по 86 точек)
|
||||||
|
5. FFT обработка с гауссовым сглаживанием
|
||||||
|
6. Обнуление центра FFT
|
||||||
|
|
||||||
|
### Выходные графики
|
||||||
|
|
||||||
|
1. **Processed Signal** - Обработанный сигнал в частотной области (3-13.67 ГГц)
|
||||||
|
2. **Fourier Transform** - Спектр FFT с гауссовым сглаживанием
|
||||||
|
|
||||||
|
## Конфигурация
|
||||||
|
|
||||||
|
### Файл: `configs/rfg_config.json`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data_type": "SYNC_DET", // Тип данных: "RAW" или "SYNC_DET"
|
||||||
|
"pont_in_one_fq_change": 86, // Размер сегмента частоты
|
||||||
|
"gaussian_sigma": 5.0, // Параметр сглаживания (σ)
|
||||||
|
"fft0_delta": 5, // Смещение обнуления центра FFT
|
||||||
|
"standard_raw_size": 64000, // Целевой размер для RAW
|
||||||
|
"standard_sync_size": 1000, // Целевой размер для SYNC_DET
|
||||||
|
"freq_start_ghz": 3.0, // Начало частотного диапазона
|
||||||
|
"freq_stop_ghz": 13.67, // Конец частотного диапазона
|
||||||
|
"fq_end": 512, // Точка отсечки FFT
|
||||||
|
"show_processed": true, // Показать Processed Signal
|
||||||
|
"show_fourier": true // Показать Fourier Transform
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### UI параметры
|
||||||
|
|
||||||
|
Процессор предоставляет следующие настройки через веб-интерфейс:
|
||||||
|
|
||||||
|
- **Тип данных** - Выбор между RAW и SYNC_DET
|
||||||
|
- **Гауссово сглаживание (σ)** - Слайдер 0.1-20.0
|
||||||
|
- **Размер сегмента частоты** - Слайдер 50-200
|
||||||
|
- **Смещение центра FFT** - Слайдер 0-20
|
||||||
|
- **Точка отсечки FFT** - Слайдер 100-1000
|
||||||
|
- **Частота начала (ГГц)** - Слайдер 1.0-15.0
|
||||||
|
- **Частота конца (ГГц)** - Слайдер 1.0-15.0
|
||||||
|
- **Переключатели** - Показать/скрыть графики
|
||||||
|
|
||||||
|
## Использование
|
||||||
|
|
||||||
|
### Автоматическая регистрация
|
||||||
|
|
||||||
|
Процессор автоматически регистрируется в `ProcessorManager` при запуске системы.
|
||||||
|
|
||||||
|
### Входные данные
|
||||||
|
|
||||||
|
Процессор ожидает данные из pipe в формате `SweepData`:
|
||||||
|
- `points`: список пар `[(real, imag), ...]`
|
||||||
|
- Для ADC данных используется только `real` часть
|
||||||
|
- Данные должны соответствовать формату 0xF0 (SYNC_DET) или 0xD0 (RAW)
|
||||||
|
|
||||||
|
### Пример структуры данных
|
||||||
|
|
||||||
|
```python
|
||||||
|
sweep_data = SweepData(
|
||||||
|
sweep_number=1,
|
||||||
|
timestamp=1234567890.0,
|
||||||
|
points=[(adc_sample_1, 0), (adc_sample_2, 0), ...],
|
||||||
|
total_points=1000 # или 64000 для RAW
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Алгоритмы обработки
|
||||||
|
|
||||||
|
### 1. Интерполяция данных
|
||||||
|
```python
|
||||||
|
# Линейная интерполяция scipy.interpolate.interp1d
|
||||||
|
old_indices = np.linspace(0, 1, len(data))
|
||||||
|
new_indices = np.linspace(0, 1, target_size)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Меандровая демодуляция (только RAW)
|
||||||
|
```python
|
||||||
|
# Генерация квадратного сигнала
|
||||||
|
time_idx = np.arange(1, size + 1)
|
||||||
|
meander = square(time_idx * np.pi)
|
||||||
|
demodulated = data * meander
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Частотная сегментация (только RAW)
|
||||||
|
```python
|
||||||
|
# Разделение на сегменты и суммирование
|
||||||
|
segment_size = 86
|
||||||
|
for segment in segments:
|
||||||
|
signal.append(np.sum(segment))
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. FFT обработка
|
||||||
|
```python
|
||||||
|
# Обрезка + FFT + сдвиг
|
||||||
|
sig_cut = np.sqrt(np.abs(signal[:512]))
|
||||||
|
F = np.fft.fft(sig_cut)
|
||||||
|
Fshift = np.abs(np.fft.fftshift(F))
|
||||||
|
|
||||||
|
# Обнуление центра
|
||||||
|
center = len(sig_cut) // 2
|
||||||
|
Fshift[center:center+1] = 0
|
||||||
|
|
||||||
|
# Гауссово сглаживание
|
||||||
|
FshiftS = gaussian_filter1d(Fshift, sigma=5.0)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Отличия от оригинального main.py
|
||||||
|
|
||||||
|
### Изменено:
|
||||||
|
1. **Вход данных**: Вместо CSV файлов - pipe через `SweepData`
|
||||||
|
2. **Без накопления**: Обработка каждой развёртки отдельно (убран `PeriodIntegrate=2`)
|
||||||
|
3. **Без B-scan**: Только Processed Signal + Fourier Transform
|
||||||
|
4. **Plotly вместо Matplotlib**: Веб-визуализация
|
||||||
|
5. **JSON конфигурация**: Вместо хардкода констант
|
||||||
|
6. **Один sweep**: Нет потокового чтения файлов
|
||||||
|
|
||||||
|
### Сохранено:
|
||||||
|
- ✅ Алгоритмы обработки (интерполяция, меандр, сегментация, FFT)
|
||||||
|
- ✅ Гауссово сглаживание
|
||||||
|
- ✅ Обнуление центра FFT
|
||||||
|
- ✅ Частотный диапазон 3-13.67 ГГц
|
||||||
|
- ✅ Параметры обработки (sigma=5, segment=86, delta=5)
|
||||||
|
|
||||||
|
## Файлы
|
||||||
|
|
||||||
|
```
|
||||||
|
vna_system/core/processors/
|
||||||
|
├── implementations/
|
||||||
|
│ ├── rfg_processor.py # Основной класс
|
||||||
|
│ └── RFG_PROCESSOR_README.md # Эта документация
|
||||||
|
├── configs/
|
||||||
|
│ └── rfg_config.json # Конфигурация по умолчанию
|
||||||
|
└── manager.py # Регистрация процессора
|
||||||
|
```
|
||||||
|
|
||||||
|
## Разработка и отладка
|
||||||
|
|
||||||
|
### Логирование
|
||||||
|
Процессор использует стандартную систему логирования:
|
||||||
|
```python
|
||||||
|
from vna_system.core.logging.logger import get_component_logger
|
||||||
|
logger = get_component_logger(__file__)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Тестирование
|
||||||
|
Для тестирования создайте `SweepData` с тестовыми ADC данными:
|
||||||
|
```python
|
||||||
|
# Тест SYNC_DET (1000 точек)
|
||||||
|
test_data = [(float(i), 0.0) for i in range(1000)]
|
||||||
|
|
||||||
|
# Тест RAW (64000 точек)
|
||||||
|
test_data = [(float(i), 0.0) for i in range(64000)]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Зависимости
|
||||||
|
|
||||||
|
- `numpy` - Численные операции
|
||||||
|
- `scipy.signal.square` - Генерация меандра
|
||||||
|
- `scipy.ndimage.gaussian_filter1d` - Гауссово сглаживание
|
||||||
|
- `scipy.interpolate.interp1d` - Интерполяция
|
||||||
|
- `plotly` - Визуализация
|
||||||
|
|
||||||
|
## Авторство
|
||||||
|
|
||||||
|
Создан на основе алгоритмов из `/home/awe/Documents/RFG_Receiver_GUI/main.py`
|
||||||
|
Адаптирован для VNA System architecture.
|
||||||
|
|
||||||
|
## Версия
|
||||||
|
|
||||||
|
v1.0 - Первая реализация (2025-11-28)
|
||||||
|
- Поддержка SYNC_DET и RAW форматов
|
||||||
|
- Два графика: Processed Signal + Fourier Transform
|
||||||
|
- Полная интеграция с VNA System
|
||||||
@ -6,6 +6,7 @@ from typing import Any
|
|||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from numpy.typing import NDArray
|
from numpy.typing import NDArray
|
||||||
|
from scipy.ndimage import gaussian_filter1d
|
||||||
|
|
||||||
from vna_system.core.logging.logger import get_component_logger
|
from vna_system.core.logging.logger import get_component_logger
|
||||||
from vna_system.core.processors.base_processor import BaseProcessor, UIParameter, ProcessedResult
|
from vna_system.core.processors.base_processor import BaseProcessor, UIParameter, ProcessedResult
|
||||||
@ -56,6 +57,11 @@ class BScanProcessor(BaseProcessor):
|
|||||||
"start_freq": 100.0, # Start frequency (MHz)
|
"start_freq": 100.0, # Start frequency (MHz)
|
||||||
"stop_freq": 8800.0, # Stop frequency (MHz)
|
"stop_freq": 8800.0, # Stop frequency (MHz)
|
||||||
"clear_history": False, # UI button; not persisted
|
"clear_history": False, # UI button; not persisted
|
||||||
|
"sigma" : 0.01,
|
||||||
|
"border_border_m" : 0.5,
|
||||||
|
"if_normalize" : False,
|
||||||
|
"if_draw_level" : False,
|
||||||
|
"detection_level" : 5,
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_ui_parameters(self) -> list[UIParameter]:
|
def get_ui_parameters(self) -> list[UIParameter]:
|
||||||
@ -74,7 +80,7 @@ class BScanProcessor(BaseProcessor):
|
|||||||
label="Ось",
|
label="Ось",
|
||||||
type="select",
|
type="select",
|
||||||
value=cfg["axis"],
|
value=cfg["axis"],
|
||||||
options={"choices": ["real", "abs", "phase"]},
|
options={"choices": ["real", "imag", "abs", "phase"]},
|
||||||
),
|
),
|
||||||
# UIParameter(
|
# UIParameter(
|
||||||
# name="data_limitation",
|
# name="data_limitation",
|
||||||
@ -118,6 +124,43 @@ class BScanProcessor(BaseProcessor):
|
|||||||
value=cfg["stop_freq"],
|
value=cfg["stop_freq"],
|
||||||
options={"min": 100.0, "max": 8800.0, "step": 10.0, "dtype": "float"},
|
options={"min": 100.0, "max": 8800.0, "step": 10.0, "dtype": "float"},
|
||||||
),
|
),
|
||||||
|
|
||||||
|
# Modern features
|
||||||
|
UIParameter(
|
||||||
|
name="sigma",
|
||||||
|
label="Степень сглаживания в abs режиме",
|
||||||
|
type="slider",
|
||||||
|
value=0.01,
|
||||||
|
options={"min": 0.01, "max": 5.0, "step": 0.01, "dtype": "float"},
|
||||||
|
),
|
||||||
|
UIParameter(
|
||||||
|
name="if_normalize",
|
||||||
|
label="Нормировка",
|
||||||
|
type="toggle",
|
||||||
|
value=False,
|
||||||
|
),
|
||||||
|
UIParameter(
|
||||||
|
name="border_border_m",
|
||||||
|
label="Глубина границы",
|
||||||
|
type="slider",
|
||||||
|
value=0.5,
|
||||||
|
options={"min": 0.0, "max": 2.5, "step": 0.01, "dtype": "float"},
|
||||||
|
),
|
||||||
|
UIParameter(
|
||||||
|
name="if_draw_level",
|
||||||
|
label="Детекция",
|
||||||
|
type="toggle",
|
||||||
|
value=False,
|
||||||
|
),
|
||||||
|
UIParameter(
|
||||||
|
name="detection_level",
|
||||||
|
label="Порог детекции,%",
|
||||||
|
type="slider",
|
||||||
|
value=5,
|
||||||
|
options={"min": 0.0, "max": 100.0, "step": 0.1, "dtype": "float"},
|
||||||
|
),
|
||||||
|
|
||||||
|
# Big botton
|
||||||
UIParameter(
|
UIParameter(
|
||||||
name="clear_history",
|
name="clear_history",
|
||||||
label="Очистить историю",
|
label="Очистить историю",
|
||||||
@ -291,112 +334,167 @@ class BScanProcessor(BaseProcessor):
|
|||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
def generate_plotly_config(
|
def generate_plotly_config(
|
||||||
self,
|
self,
|
||||||
processed_data: dict[str, Any],
|
processed_data: dict[str, Any],
|
||||||
vna_config: dict[str, Any], # noqa: ARG002 - reserved for future layout tweaks
|
vna_config: dict[str, Any], # noqa: ARG002 - reserved for future layout tweaks
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Produce a Plotly-compatible heatmap configuration from accumulated sweeps.
|
Produce a Plotly-compatible heatmap configuration from accumulated sweeps.
|
||||||
"""
|
"""
|
||||||
if "error" in processed_data:
|
if "error" in processed_data:
|
||||||
return {
|
return {
|
||||||
"data": [],
|
"data": [],
|
||||||
"layout": {
|
"layout": {
|
||||||
"title": "B-Scan анализ - Ошибка",
|
"title": "B-Scan анализ - Ошибка",
|
||||||
"annotations": [
|
"annotations": [
|
||||||
{
|
{
|
||||||
"text": f"Ошибка: {processed_data['error']}",
|
"text": f"Ошибка: {processed_data['error']}",
|
||||||
"x": 0.5,
|
"x": 0.5,
|
||||||
"y": 0.5,
|
"y": 0.5,
|
||||||
"xref": "paper",
|
"xref": "paper",
|
||||||
"yref": "paper",
|
"yref": "paper",
|
||||||
"showarrow": False,
|
"showarrow": False,
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"template": "plotly_dark",
|
"template": "plotly_dark",
|
||||||
},
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
with self._lock:
|
||||||
|
history = list(self._plot_history)
|
||||||
|
|
||||||
|
if not history:
|
||||||
|
return {
|
||||||
|
"data": [],
|
||||||
|
"layout": {
|
||||||
|
"title": "B-Scan анализ - Нет данных",
|
||||||
|
"xaxis": {"title": "Номер развертки"},
|
||||||
|
"yaxis": {"title": "Глубина (м)"},
|
||||||
|
"template": "plotly_dark",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Y_VALUE = self._config["border_border_m"]
|
||||||
|
|
||||||
|
# Build scatter-like heatmap (irregular grid) from history
|
||||||
|
x_coords: list[int] = []
|
||||||
|
y_coords: list[float] = []
|
||||||
|
z_values: list[float] = []
|
||||||
|
|
||||||
|
z_values_square = np.zeros((len(history[0]["distance_data"]),len(history)),dtype=float)
|
||||||
|
for sweep_index, item in enumerate(history, start=1):
|
||||||
|
depths = item["distance_data"]
|
||||||
|
amps = item["time_domain_data"]
|
||||||
|
|
||||||
|
if self._config['if_normalize']:
|
||||||
|
depth_mask = np.array(depths) < Y_VALUE
|
||||||
|
normalized_ampls = np.array(amps) / np.max(np.array(amps)[depth_mask])
|
||||||
|
else:
|
||||||
|
normalized_ampls = np.array(amps)
|
||||||
|
z_values_square[:,sweep_index-1] = normalized_ampls
|
||||||
|
|
||||||
|
for d, a in zip(depths, normalized_ampls, strict=False):
|
||||||
|
x_coords.append(sweep_index)
|
||||||
|
y_coords.append(d)
|
||||||
|
z_values.append(float(a))
|
||||||
|
|
||||||
|
if self._config["if_draw_level"]:
|
||||||
|
detection_level_abs = np.percentile(z_values, 100 - self._config["detection_level"])
|
||||||
|
detected_points_mask = (np.array(z_values) > detection_level_abs) & (np.array(y_coords) > Y_VALUE)
|
||||||
|
detected_points_x = list([float(x) for x in np.array(x_coords)[detected_points_mask]])
|
||||||
|
detected_points_y = list([float(y) for y in np.array(y_coords)[detected_points_mask]])
|
||||||
|
detected_trace = {
|
||||||
|
"type": "scatter",
|
||||||
|
"mode": "markers",
|
||||||
|
"x": detected_points_x,
|
||||||
|
"y": detected_points_y,
|
||||||
|
"marker": {
|
||||||
|
"size": 8,
|
||||||
|
"color": "red",
|
||||||
|
"symbol": "circle",
|
||||||
|
},
|
||||||
|
"name": "Обнаруженные точки",
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
detected_trace = None
|
||||||
|
|
||||||
|
|
||||||
|
# Colorscale selection
|
||||||
|
if self._config["axis"] == "abs":
|
||||||
|
colorscale = "Viridis"
|
||||||
|
heatmap_kwargs: dict[str, Any] = {}
|
||||||
|
else:
|
||||||
|
colorscale = "RdBu"
|
||||||
|
heatmap_kwargs = {"zmid": 0}
|
||||||
|
|
||||||
|
heatmap_trace = {
|
||||||
|
"type": "heatmap",
|
||||||
|
"x": x_coords,
|
||||||
|
"y": y_coords,
|
||||||
|
"z": z_values,
|
||||||
|
"colorscale": colorscale,
|
||||||
|
"colorbar": {"title": "Амплитуда"},
|
||||||
|
"hovertemplate": (
|
||||||
|
"Развертка: %{x}<br>"
|
||||||
|
"Глубина: %{y:.3f} м<br>"
|
||||||
|
"Амплитуда: %{z:.3f}<br>"
|
||||||
|
"<extra></extra>"
|
||||||
|
),
|
||||||
|
**heatmap_kwargs,
|
||||||
}
|
}
|
||||||
|
|
||||||
with self._lock:
|
freq_start, freq_stop = processed_data.get("frequency_range", [0.0, 0.0])
|
||||||
history = list(self._plot_history)
|
config_info = (
|
||||||
|
f"Частота: {freq_start/1e6:.1f}-{freq_stop/1e6:.1f} МГц | "
|
||||||
|
f"Усиление: {self._config['gain']:.1f} | "
|
||||||
|
f"Отсечка: {self._config['cut']:.3f} м | "
|
||||||
|
f"Макс глубина: {self._config['max']:.1f} м | "
|
||||||
|
f"Ось: {self._config['axis']} | "
|
||||||
|
f"Разверток: {len(history)}"
|
||||||
|
)
|
||||||
|
|
||||||
if not history:
|
if processed_data.get("reference_used", False):
|
||||||
return {
|
config_info += " | Открытый воздух: ВКЛ"
|
||||||
"data": [],
|
|
||||||
"layout": {
|
# if self._config["data_limitation"]:
|
||||||
"title": "B-Scan анализ - Нет данных",
|
# config_info += f" | Limit: {self._config['data_limitation']}"
|
||||||
"xaxis": {"title": "Номер развертки"},
|
|
||||||
"yaxis": {"title": "Глубина (м)"},
|
layout = {
|
||||||
"template": "plotly_dark",
|
"title": f"B-Scan тепловая карта - {config_info}",
|
||||||
},
|
"xaxis": {"title": "Номер развертки", "side": "bottom"},
|
||||||
|
"yaxis": {"title": "Глубина (м)", "autorange": "reversed"},
|
||||||
|
"hovermode": "closest",
|
||||||
|
"height": 546,
|
||||||
|
"template": "plotly_dark",
|
||||||
|
"margin": {"t": 40, "r": 50, "b": 110, "l": 50},
|
||||||
|
"autosize": True,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Build scatter-like heatmap (irregular grid) from history
|
print(self._config['if_normalize'], self._config['if_draw_level'])
|
||||||
x_coords: list[int] = []
|
if self._config['if_normalize'] or self._config['if_draw_level']:
|
||||||
y_coords: list[float] = []
|
layout["shapes"] = layout.get("shapes", []) + [
|
||||||
z_values: list[float] = []
|
{
|
||||||
|
"type": "line",
|
||||||
|
# по X — координаты "бумаги" (0–1 по всей ширине графика)
|
||||||
|
"xref": "paper",
|
||||||
|
# по Y — координаты данных (в метрах глубины)
|
||||||
|
"yref": "y",
|
||||||
|
"x0": 0,
|
||||||
|
"x1": 1,
|
||||||
|
"y0": Y_VALUE,
|
||||||
|
"y1": Y_VALUE,
|
||||||
|
"line": {
|
||||||
|
"width": 2,
|
||||||
|
"dash": "dash",
|
||||||
|
"color": "white",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
for sweep_index, item in enumerate(history, start=1):
|
if detected_trace is not None:
|
||||||
depths = item["distance_data"]
|
return {"data": [heatmap_trace,detected_trace], "layout": layout}
|
||||||
amps = item["time_domain_data"]
|
return {"data": [heatmap_trace], "layout": layout}
|
||||||
|
|
||||||
for d, a in zip(depths, amps, strict=False):
|
|
||||||
x_coords.append(sweep_index)
|
|
||||||
y_coords.append(d)
|
|
||||||
z_values.append(a)
|
|
||||||
|
|
||||||
# Colorscale selection
|
|
||||||
if self._config["axis"] == "abs":
|
|
||||||
colorscale = "Viridis"
|
|
||||||
heatmap_kwargs: dict[str, Any] = {}
|
|
||||||
else:
|
|
||||||
colorscale = "RdBu"
|
|
||||||
heatmap_kwargs = {"zmid": 0}
|
|
||||||
|
|
||||||
heatmap_trace = {
|
|
||||||
"type": "heatmap",
|
|
||||||
"x": x_coords,
|
|
||||||
"y": y_coords,
|
|
||||||
"z": z_values,
|
|
||||||
"colorscale": colorscale,
|
|
||||||
"colorbar": {"title": "Амплитуда"},
|
|
||||||
"hovertemplate": (
|
|
||||||
"Развертка: %{x}<br>"
|
|
||||||
"Глубина: %{y:.3f} м<br>"
|
|
||||||
"Амплитуда: %{z:.3f}<br>"
|
|
||||||
"<extra></extra>"
|
|
||||||
),
|
|
||||||
**heatmap_kwargs,
|
|
||||||
}
|
|
||||||
|
|
||||||
freq_start, freq_stop = processed_data.get("frequency_range", [0.0, 0.0])
|
|
||||||
config_info = (
|
|
||||||
f"Частота: {freq_start/1e6:.1f}-{freq_stop/1e6:.1f} МГц | "
|
|
||||||
f"Усиление: {self._config['gain']:.1f} | "
|
|
||||||
f"Отсечка: {self._config['cut']:.3f} м | "
|
|
||||||
f"Макс глубина: {self._config['max']:.1f} м | "
|
|
||||||
f"Ось: {self._config['axis']} | "
|
|
||||||
f"Разверток: {len(history)}"
|
|
||||||
)
|
|
||||||
|
|
||||||
if processed_data.get("reference_used", False):
|
|
||||||
config_info += " | Открытый воздух: ВКЛ"
|
|
||||||
|
|
||||||
# if self._config["data_limitation"]:
|
|
||||||
# config_info += f" | Limit: {self._config['data_limitation']}"
|
|
||||||
|
|
||||||
layout = {
|
|
||||||
"title": f"B-Scan тепловая карта - {config_info}",
|
|
||||||
"xaxis": {"title": "Номер развертки", "side": "bottom"},
|
|
||||||
"yaxis": {"title": "Глубина (м)", "autorange": "reversed"},
|
|
||||||
"hovermode": "closest",
|
|
||||||
"height": 546,
|
|
||||||
"template": "plotly_dark",
|
|
||||||
"margin": {"t": 40, "r": 50, "b": 110, "l": 50},
|
|
||||||
"autosize": True
|
|
||||||
}
|
|
||||||
|
|
||||||
return {"data": [heatmap_trace], "layout": layout}
|
|
||||||
|
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
# Recalculation override
|
# Recalculation override
|
||||||
@ -557,6 +655,14 @@ class BScanProcessor(BaseProcessor):
|
|||||||
freq_start = self._config["start_freq"] * 1e6
|
freq_start = self._config["start_freq"] * 1e6
|
||||||
freq_stop = self._config["stop_freq"] * 1e6
|
freq_stop = self._config["stop_freq"] * 1e6
|
||||||
|
|
||||||
|
# Determine sigma for smoothing
|
||||||
|
if vna_config:
|
||||||
|
calc_type = self._config["axis"]
|
||||||
|
sigma = self._config["sigma"]
|
||||||
|
else:
|
||||||
|
calc_type = self._config["axis"]
|
||||||
|
sigma = self._config["sigma"]
|
||||||
|
|
||||||
# Frequency vector over current data length
|
# Frequency vector over current data length
|
||||||
freq_axis = np.linspace(freq_start, freq_stop, complex_data.size, dtype=float)
|
freq_axis = np.linspace(freq_start, freq_stop, complex_data.size, dtype=float)
|
||||||
|
|
||||||
@ -571,8 +677,13 @@ class BScanProcessor(BaseProcessor):
|
|||||||
# Depth windowing and gain shaping
|
# Depth windowing and gain shaping
|
||||||
depth_out, time_out = self._apply_depth_processing(depth_m, time_response)
|
depth_out, time_out = self._apply_depth_processing(depth_m, time_response)
|
||||||
|
|
||||||
|
if calc_type == 'abs':
|
||||||
|
filtered_time_out = gaussian_filter1d(time_out, sigma=sigma)
|
||||||
|
else:
|
||||||
|
filtered_time_out = time_out
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"time_data": time_out,
|
"time_data": filtered_time_out,
|
||||||
"distance": depth_out,
|
"distance": depth_out,
|
||||||
"freq_range": [freq_start, freq_stop],
|
"freq_range": [freq_start, freq_stop],
|
||||||
"complex_time": complex_data,
|
"complex_time": complex_data,
|
||||||
@ -659,6 +770,8 @@ class BScanProcessor(BaseProcessor):
|
|||||||
y_fin = np.abs(y)
|
y_fin = np.abs(y)
|
||||||
elif axis == "real":
|
elif axis == "real":
|
||||||
y_fin = np.real(y)
|
y_fin = np.real(y)
|
||||||
|
elif axis == "imag":
|
||||||
|
y_fin = np.imag(y)
|
||||||
elif axis == "phase":
|
elif axis == "phase":
|
||||||
y_fin = np.angle(y)
|
y_fin = np.angle(y)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@ -94,7 +94,7 @@ class MagnitudeProcessor(BaseProcessor):
|
|||||||
real_points.append(float(real))
|
real_points.append(float(real))
|
||||||
imag_points.append(float(imag))
|
imag_points.append(float(imag))
|
||||||
mag = abs(complex_val)
|
mag = abs(complex_val)
|
||||||
mags_db.append(20.0 * np.log10(mag) if mag > 0.0 else -120.0)
|
mags_db.append( 20*np.log10(mag) if mag > 0.0 else -120.0)
|
||||||
phases_deg.append(np.degrees(np.angle(complex_val)))
|
phases_deg.append(np.degrees(np.angle(complex_val)))
|
||||||
|
|
||||||
result = {
|
result = {
|
||||||
|
|||||||
643
vna_system/core/processors/implementations/rfg_processor.py
Normal file
643
vna_system/core/processors/implementations/rfg_processor.py
Normal file
@ -0,0 +1,643 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
from numpy.typing import NDArray
|
||||||
|
from scipy.signal import square
|
||||||
|
from scipy.ndimage import gaussian_filter1d
|
||||||
|
from scipy.interpolate import interp1d
|
||||||
|
|
||||||
|
from vna_system.core.logging.logger import get_component_logger
|
||||||
|
from vna_system.core.processors.base_processor import BaseProcessor, UIParameter, ProcessedResult
|
||||||
|
from vna_system.core.acquisition.sweep_buffer import SweepData
|
||||||
|
|
||||||
|
logger = get_component_logger(__file__)
|
||||||
|
|
||||||
|
|
||||||
|
class RFGProcessor(BaseProcessor):
|
||||||
|
"""
|
||||||
|
Radiophotonic radar processor based on RFG_Receiver_GUI algorithm.
|
||||||
|
|
||||||
|
Processes ADC data with synchronous detection (0xF0 format) or RAW data (0xD0 format).
|
||||||
|
Outputs two graphs:
|
||||||
|
- Processed Signal: Frequency domain signal (3-13.67 GHz range)
|
||||||
|
- Fourier Transform: FFT magnitude spectrum with Gaussian smoothing
|
||||||
|
|
||||||
|
Features
|
||||||
|
--------
|
||||||
|
- Data interpolation to standard size
|
||||||
|
- Meander demodulation (for RAW data)
|
||||||
|
- Frequency segmentation
|
||||||
|
- FFT processing with Gaussian smoothing
|
||||||
|
- Center zeroing for artifact reduction
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, config_dir: Path) -> None:
|
||||||
|
super().__init__("rfg_radar", config_dir)
|
||||||
|
|
||||||
|
# No history accumulation needed (process single sweeps)
|
||||||
|
self._max_history = 1
|
||||||
|
|
||||||
|
# Pre-computed meander signal (will be initialized on first use)
|
||||||
|
self._meandr: NDArray[np.floating] | None = None
|
||||||
|
self._last_size: int = 0
|
||||||
|
|
||||||
|
logger.info("RFGProcessor initialized", processor_id=self.processor_id)
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# Configuration
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _get_default_config(self) -> dict[str, Any]:
|
||||||
|
"""Return default configuration values."""
|
||||||
|
return {
|
||||||
|
"data_type": "SYNC_DET", # "RAW" or "SYNC_DET"
|
||||||
|
"pont_in_one_fq_change": 86, # Frequency segment size
|
||||||
|
"gaussian_sigma": 5.0, # FFT smoothing sigma
|
||||||
|
"fft0_delta": 5, # Center zero offset
|
||||||
|
"standard_raw_size": 64000, # Target size for RAW data
|
||||||
|
"standard_sync_size": 1000, # Target size for SYNC_DET data
|
||||||
|
"freq_start_ghz": 3.0, # Display frequency start (GHz)
|
||||||
|
"freq_stop_ghz": 13.67, # Display frequency stop (GHz)
|
||||||
|
"fq_end": 512, # FFT cutoff point
|
||||||
|
"show_processed": True, # Show processed signal graph
|
||||||
|
"show_fourier": True, # Show Fourier transform graph
|
||||||
|
"normalize_signal": False, # Normalize processed signal to [0, 1]
|
||||||
|
"log_scale": False, # Use logarithmic scale for signal
|
||||||
|
"disable_interpolation": False, # Skip interpolation (use raw data size)
|
||||||
|
"y_max_processed": 900000.0, # Max Y-axis value for processed signal
|
||||||
|
"y_max_fourier": 1000.0, # Max Y-axis value for Fourier spectrum
|
||||||
|
"auto_scale": False, # Auto-scale Y-axis (ignore y_max if True)
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_ui_parameters(self) -> list[UIParameter]:
|
||||||
|
"""Return UI parameter schema for configuration."""
|
||||||
|
cfg = self._config
|
||||||
|
|
||||||
|
return [
|
||||||
|
UIParameter(
|
||||||
|
name="data_type",
|
||||||
|
label="Тип данных",
|
||||||
|
type="select",
|
||||||
|
value=cfg["data_type"],
|
||||||
|
options={"choices": ["RAW", "SYNC_DET"]},
|
||||||
|
),
|
||||||
|
UIParameter(
|
||||||
|
name="gaussian_sigma",
|
||||||
|
label="Гауссово сглаживание (σ)",
|
||||||
|
type="slider",
|
||||||
|
value=cfg["gaussian_sigma"],
|
||||||
|
options={"min": 0.1, "max": 20.0, "step": 0.1, "dtype": "float"},
|
||||||
|
),
|
||||||
|
UIParameter(
|
||||||
|
name="pont_in_one_fq_change",
|
||||||
|
label="Размер сегмента частоты",
|
||||||
|
type="slider",
|
||||||
|
value=cfg["pont_in_one_fq_change"],
|
||||||
|
options={"min": 50, "max": 200, "step": 1, "dtype": "int"},
|
||||||
|
),
|
||||||
|
UIParameter(
|
||||||
|
name="fft0_delta",
|
||||||
|
label="Смещение центра FFT",
|
||||||
|
type="slider",
|
||||||
|
value=cfg["fft0_delta"],
|
||||||
|
options={"min": 0, "max": 20, "step": 1, "dtype": "int"},
|
||||||
|
),
|
||||||
|
UIParameter(
|
||||||
|
name="fq_end",
|
||||||
|
label="Точка отсечки FFT",
|
||||||
|
type="slider",
|
||||||
|
value=cfg["fq_end"],
|
||||||
|
options={"min": 100, "max": 1000, "step": 10, "dtype": "int"},
|
||||||
|
),
|
||||||
|
UIParameter(
|
||||||
|
name="freq_start_ghz",
|
||||||
|
label="Частота начала (ГГц)",
|
||||||
|
type="slider",
|
||||||
|
value=cfg["freq_start_ghz"],
|
||||||
|
options={"min": 1.0, "max": 15.0, "step": 0.1, "dtype": "float"},
|
||||||
|
),
|
||||||
|
UIParameter(
|
||||||
|
name="freq_stop_ghz",
|
||||||
|
label="Частота конца (ГГц)",
|
||||||
|
type="slider",
|
||||||
|
value=cfg["freq_stop_ghz"],
|
||||||
|
options={"min": 1.0, "max": 15.0, "step": 0.1, "dtype": "float"},
|
||||||
|
),
|
||||||
|
UIParameter(
|
||||||
|
name="show_processed",
|
||||||
|
label="Показать обработанный сигнал",
|
||||||
|
type="toggle",
|
||||||
|
value=cfg["show_processed"],
|
||||||
|
),
|
||||||
|
UIParameter(
|
||||||
|
name="show_fourier",
|
||||||
|
label="Показать Фурье образ",
|
||||||
|
type="toggle",
|
||||||
|
value=cfg["show_fourier"],
|
||||||
|
),
|
||||||
|
UIParameter(
|
||||||
|
name="normalize_signal",
|
||||||
|
label="Нормализовать сигнал",
|
||||||
|
type="toggle",
|
||||||
|
value=cfg["normalize_signal"],
|
||||||
|
),
|
||||||
|
UIParameter(
|
||||||
|
name="log_scale",
|
||||||
|
label="Логарифмическая шкала",
|
||||||
|
type="toggle",
|
||||||
|
value=cfg["log_scale"],
|
||||||
|
),
|
||||||
|
UIParameter(
|
||||||
|
name="disable_interpolation",
|
||||||
|
label="Без интерполяции (как FOURIER)",
|
||||||
|
type="toggle",
|
||||||
|
value=cfg["disable_interpolation"],
|
||||||
|
),
|
||||||
|
UIParameter(
|
||||||
|
name="auto_scale",
|
||||||
|
label="Автоматический масштаб Y",
|
||||||
|
type="toggle",
|
||||||
|
value=cfg["auto_scale"],
|
||||||
|
),
|
||||||
|
UIParameter(
|
||||||
|
name="y_max_processed",
|
||||||
|
label="Макс. амплитуда (Processed)",
|
||||||
|
type="slider",
|
||||||
|
value=cfg["y_max_processed"],
|
||||||
|
options={"min": 1000.0, "max": 200000.0, "step": 1000.0, "dtype": "float"},
|
||||||
|
),
|
||||||
|
UIParameter(
|
||||||
|
name="y_max_fourier",
|
||||||
|
label="Макс. амплитуда (Fourier)",
|
||||||
|
type="slider",
|
||||||
|
value=cfg["y_max_fourier"],
|
||||||
|
options={"min": 1000.0, "max": 200000.0, "step": 1000.0, "dtype": "float"},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# Processing
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def process_sweep(
|
||||||
|
self,
|
||||||
|
sweep_data: SweepData,
|
||||||
|
calibrated_data: SweepData | None,
|
||||||
|
vna_config: dict[str, Any],
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Process a single sweep of ADC data.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
dict
|
||||||
|
Keys: processed_signal, fourier_spectrum, freq_axis, data_type, points_processed
|
||||||
|
Or: {"error": "..."} on failure.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Extract raw ADC data from sweep (use real part only)
|
||||||
|
adc_data = self._extract_adc_data(sweep_data)
|
||||||
|
if adc_data is None or adc_data.size == 0:
|
||||||
|
logger.warning("No valid ADC data for RFG processing")
|
||||||
|
return {"error": "No valid ADC data"}
|
||||||
|
|
||||||
|
data_type = self._config["data_type"]
|
||||||
|
|
||||||
|
if data_type == "RAW":
|
||||||
|
# Full processing with meander demodulation
|
||||||
|
processed_signal, fourier_spectrum = self._process_raw_data(adc_data)
|
||||||
|
elif data_type == "SYNC_DET":
|
||||||
|
# Direct FFT processing (data already demodulated)
|
||||||
|
processed_signal, fourier_spectrum = self._process_sync_det_data(adc_data)
|
||||||
|
else:
|
||||||
|
return {"error": f"Unknown data type: {data_type}"}
|
||||||
|
|
||||||
|
# Generate frequency axis for visualization
|
||||||
|
freq_axis = self._generate_frequency_axis(processed_signal)
|
||||||
|
fourier_spectrum /= (2*3.14)
|
||||||
|
return {
|
||||||
|
"processed_signal": processed_signal.tolist(),
|
||||||
|
"fourier_spectrum": fourier_spectrum.tolist(),
|
||||||
|
"freq_axis": freq_axis.tolist(),
|
||||||
|
"data_type": data_type,
|
||||||
|
"points_processed": int(adc_data.size),
|
||||||
|
"original_size": int(adc_data.size),
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
logger.error("RFG processing failed", error=repr(exc))
|
||||||
|
return {"error": str(exc)}
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# Visualization
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def generate_plotly_config(
|
||||||
|
self,
|
||||||
|
processed_data: dict[str, Any],
|
||||||
|
vna_config: dict[str, Any], # noqa: ARG002
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Produce Plotly configuration for two subplots: Processed Signal + Fourier Transform.
|
||||||
|
"""
|
||||||
|
if "error" in processed_data:
|
||||||
|
return {
|
||||||
|
"data": [],
|
||||||
|
"layout": {
|
||||||
|
"title": "RFG Радар - Ошибка",
|
||||||
|
"annotations": [
|
||||||
|
{
|
||||||
|
"text": f"Ошибка: {processed_data['error']}",
|
||||||
|
"x": 0.5,
|
||||||
|
"y": 0.5,
|
||||||
|
"xref": "paper",
|
||||||
|
"yref": "paper",
|
||||||
|
"showarrow": False,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"template": "plotly_dark",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
processed_signal = processed_data.get("processed_signal", [])
|
||||||
|
fourier_spectrum = processed_data.get("fourier_spectrum", [])
|
||||||
|
freq_axis = processed_data.get("freq_axis", [])
|
||||||
|
|
||||||
|
# Create subplot configuration
|
||||||
|
traces = []
|
||||||
|
|
||||||
|
if self._config["show_processed"] and processed_signal:
|
||||||
|
# Processed Signal trace - use absolute value as in main.py line 1043
|
||||||
|
import numpy as np
|
||||||
|
processed_signal_abs = np.abs(np.array(processed_signal))
|
||||||
|
|
||||||
|
# Apply normalization if enabled
|
||||||
|
if self._config.get("normalize_signal", False):
|
||||||
|
max_val = np.max(processed_signal_abs)
|
||||||
|
if max_val > 0:
|
||||||
|
processed_signal_abs = processed_signal_abs / max_val
|
||||||
|
|
||||||
|
# Apply log scale if enabled
|
||||||
|
if self._config.get("log_scale", False):
|
||||||
|
processed_signal_abs = np.log10(processed_signal_abs + 1e-12) # Add small epsilon to avoid log(0)
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
"Processed signal stats",
|
||||||
|
min=float(np.min(processed_signal_abs)),
|
||||||
|
max=float(np.max(processed_signal_abs)),
|
||||||
|
mean=float(np.mean(processed_signal_abs)),
|
||||||
|
size=len(processed_signal_abs)
|
||||||
|
)
|
||||||
|
|
||||||
|
traces.append({
|
||||||
|
"type": "scatter",
|
||||||
|
"mode": "lines",
|
||||||
|
"x": freq_axis,
|
||||||
|
"y": processed_signal_abs.tolist(),
|
||||||
|
"name": "Обработанный сигнал",
|
||||||
|
"line": {"width": 1, "color": "cyan"},
|
||||||
|
"xaxis": "x",
|
||||||
|
"yaxis": "y",
|
||||||
|
})
|
||||||
|
|
||||||
|
if self._config["show_fourier"] and fourier_spectrum:
|
||||||
|
# Fourier Transform trace
|
||||||
|
fft_x = list(range(len(fourier_spectrum)))
|
||||||
|
traces.append({
|
||||||
|
"type": "scatter",
|
||||||
|
"mode": "lines",
|
||||||
|
"x": fft_x,
|
||||||
|
"y": fourier_spectrum,
|
||||||
|
"name": "Фурье образ",
|
||||||
|
"line": {"width": 1, "color": "yellow"},
|
||||||
|
"xaxis": "x2",
|
||||||
|
"yaxis": "y2",
|
||||||
|
})
|
||||||
|
|
||||||
|
# Build layout with two subplots
|
||||||
|
layout = {
|
||||||
|
"title": (
|
||||||
|
f"RFG Радар - {processed_data.get('data_type', 'N/A')} | "
|
||||||
|
f"Точек: {processed_data.get('points_processed', 0)}"
|
||||||
|
),
|
||||||
|
"grid": {"rows": 2, "columns": 1, "pattern": "independent"},
|
||||||
|
"xaxis": {
|
||||||
|
"title": "Частота (ГГц)",
|
||||||
|
"domain": [0, 1],
|
||||||
|
"anchor": "y",
|
||||||
|
},
|
||||||
|
"yaxis": {
|
||||||
|
"title": "Амплитуда",
|
||||||
|
"domain": [0.55, 1],
|
||||||
|
"anchor": "x",
|
||||||
|
},
|
||||||
|
"xaxis2": {
|
||||||
|
"title": "Индекс",
|
||||||
|
"domain": [0, 1],
|
||||||
|
"anchor": "y2",
|
||||||
|
},
|
||||||
|
"yaxis2": {
|
||||||
|
"title": "Магнитуда FFT",
|
||||||
|
"domain": [0, 0.45],
|
||||||
|
"anchor": "x2",
|
||||||
|
},
|
||||||
|
"template": "plotly_dark",
|
||||||
|
"height": 700,
|
||||||
|
"showlegend": True,
|
||||||
|
"hovermode": "closest",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Apply Y-axis limits if not auto-scaling
|
||||||
|
if not self._config.get("auto_scale", False):
|
||||||
|
y_max_processed = float(self._config.get("y_max_processed", 90000.0))
|
||||||
|
y_max_fourier = float(self._config.get("y_max_fourier", 60000.0))
|
||||||
|
|
||||||
|
layout["yaxis"]["range"] = [0, y_max_processed]
|
||||||
|
layout["yaxis2"]["range"] = [0, y_max_fourier]
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
"Y-axis limits applied",
|
||||||
|
processed_max=y_max_processed,
|
||||||
|
fourier_max=y_max_fourier
|
||||||
|
)
|
||||||
|
|
||||||
|
return {"data": traces, "layout": layout}
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# Data Processing Helpers
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _extract_adc_data(self, sweep_data: SweepData) -> NDArray[np.floating] | None:
|
||||||
|
"""
|
||||||
|
Extract ADC data from SweepData.
|
||||||
|
|
||||||
|
Assumes sweep_data.points contains [(real, imag), ...] pairs.
|
||||||
|
For ADC data, we take only the real part.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if not sweep_data.points:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Extract real part (ADC samples)
|
||||||
|
arr = np.asarray(sweep_data.points, dtype=float)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"🔍 RAW INPUT DATA SHAPE",
|
||||||
|
shape=arr.shape,
|
||||||
|
ndim=arr.ndim,
|
||||||
|
total_points=sweep_data.total_points,
|
||||||
|
dtype=arr.dtype
|
||||||
|
)
|
||||||
|
|
||||||
|
if arr.ndim == 2 and arr.shape[1] >= 1:
|
||||||
|
# Take first column (real part)
|
||||||
|
adc_data = arr[:, 0]
|
||||||
|
elif arr.ndim == 1:
|
||||||
|
# Already 1D array
|
||||||
|
adc_data = arr
|
||||||
|
else:
|
||||||
|
raise ValueError("Unexpected data shape for ADC extraction")
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"📊 EXTRACTED ADC DATA",
|
||||||
|
size=adc_data.size,
|
||||||
|
min=float(np.min(adc_data)),
|
||||||
|
max=float(np.max(adc_data)),
|
||||||
|
mean=float(np.mean(adc_data)),
|
||||||
|
non_zero_count=int(np.count_nonzero(adc_data))
|
||||||
|
)
|
||||||
|
|
||||||
|
return adc_data.astype(float, copy=False)
|
||||||
|
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
logger.error("Failed to extract ADC data", error=repr(exc))
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _resize_1d_interpolate(
|
||||||
|
self,
|
||||||
|
data: NDArray[np.floating],
|
||||||
|
target_size: int,
|
||||||
|
) -> NDArray[np.floating]:
|
||||||
|
"""
|
||||||
|
Resize 1D array using linear interpolation.
|
||||||
|
|
||||||
|
Based on main.py resize_1d_interpolate() function.
|
||||||
|
"""
|
||||||
|
if len(data) == target_size:
|
||||||
|
return data
|
||||||
|
|
||||||
|
old_indices = np.linspace(0, 1, len(data))
|
||||||
|
new_indices = np.linspace(0, 1, target_size)
|
||||||
|
|
||||||
|
f = interp1d(old_indices, data, kind='linear', fill_value='extrapolate')
|
||||||
|
return f(new_indices)
|
||||||
|
|
||||||
|
def _process_raw_data(
|
||||||
|
self,
|
||||||
|
adc_data: NDArray[np.floating],
|
||||||
|
) -> tuple[NDArray[np.floating], NDArray[np.floating]]:
|
||||||
|
"""
|
||||||
|
Process RAW ADC data (0xD0 format).
|
||||||
|
|
||||||
|
Pipeline (based on main.py lines 824-896):
|
||||||
|
1. Interpolate to standard size (64000)
|
||||||
|
2. Generate meander signal for demodulation
|
||||||
|
3. Synchronous detection (multiply by meander)
|
||||||
|
4. Frequency segmentation
|
||||||
|
5. FFT processing with Gaussian smoothing
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
tuple
|
||||||
|
(processed_signal, fourier_spectrum)
|
||||||
|
"""
|
||||||
|
# Step 1: Resize to standard RAW size
|
||||||
|
target_size = int(self._config["standard_raw_size"])
|
||||||
|
data_resized = self._resize_1d_interpolate(adc_data, target_size)
|
||||||
|
|
||||||
|
# Step 2: Generate meander signal (square wave)
|
||||||
|
if self._meandr is None or self._last_size != target_size:
|
||||||
|
time_idx = np.arange(1, target_size + 1)
|
||||||
|
self._meandr = square(time_idx * np.pi)
|
||||||
|
self._last_size = target_size
|
||||||
|
logger.debug("Meander signal regenerated", size=target_size)
|
||||||
|
|
||||||
|
# Step 3: Meander demodulation (synchronous detection)
|
||||||
|
demodulated = data_resized * self._meandr
|
||||||
|
|
||||||
|
# Step 4: Frequency segmentation
|
||||||
|
processed_signal = self._frequency_segmentation(demodulated)
|
||||||
|
|
||||||
|
# Step 5: FFT processing
|
||||||
|
fourier_spectrum = self._compute_fft_spectrum(processed_signal) / (2*3.14)
|
||||||
|
|
||||||
|
return processed_signal, fourier_spectrum
|
||||||
|
|
||||||
|
def _process_sync_det_data(
|
||||||
|
self,
|
||||||
|
adc_data: NDArray[np.floating],
|
||||||
|
) -> tuple[NDArray[np.floating], NDArray[np.floating]]:
|
||||||
|
"""
|
||||||
|
Process SYNC_DET data (0xF0 format).
|
||||||
|
|
||||||
|
Pipeline (based on main.py lines 898-917):
|
||||||
|
1. Interpolate to standard size (1000)
|
||||||
|
2. Use data directly as signal (already demodulated)
|
||||||
|
3. FFT processing with Gaussian smoothing
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
tuple
|
||||||
|
(processed_signal, fourier_spectrum)
|
||||||
|
"""
|
||||||
|
# Step 1: Optionally resize to standard SYNC_DET size
|
||||||
|
if self._config.get("disable_interpolation", False):
|
||||||
|
# FOURIER mode: no interpolation, use raw data size
|
||||||
|
data_resized = adc_data
|
||||||
|
logger.info("🚫 Interpolation disabled - using raw data size", size=adc_data.size)
|
||||||
|
else:
|
||||||
|
# SYNC_DET mode: interpolate to standard size
|
||||||
|
target_size = int(self._config["standard_sync_size"])
|
||||||
|
data_resized = self._resize_1d_interpolate(adc_data, target_size)
|
||||||
|
logger.debug(
|
||||||
|
"SYNC_DET data resized",
|
||||||
|
original_size=adc_data.size,
|
||||||
|
target_size=target_size,
|
||||||
|
min_val=float(np.min(data_resized)),
|
||||||
|
max_val=float(np.max(data_resized)),
|
||||||
|
mean_val=float(np.mean(data_resized))
|
||||||
|
)
|
||||||
|
|
||||||
|
# Step 2: Data is already demodulated, use directly
|
||||||
|
processed_signal = data_resized
|
||||||
|
|
||||||
|
# Step 3: FFT processing
|
||||||
|
fourier_spectrum = self._compute_fft_spectrum(processed_signal)
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
"SYNC_DET processing complete",
|
||||||
|
signal_size=processed_signal.size,
|
||||||
|
fft_size=fourier_spectrum.size
|
||||||
|
)
|
||||||
|
|
||||||
|
return processed_signal, fourier_spectrum
|
||||||
|
|
||||||
|
def _frequency_segmentation(
|
||||||
|
self,
|
||||||
|
data: NDArray[np.floating],
|
||||||
|
) -> NDArray[np.floating]:
|
||||||
|
"""
|
||||||
|
Divide data into frequency segments and sum each segment.
|
||||||
|
|
||||||
|
Based on main.py lines 866-884.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
data : ndarray
|
||||||
|
Demodulated signal
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
ndarray
|
||||||
|
Segmented and summed signal
|
||||||
|
"""
|
||||||
|
pont_in_one_fq = int(self._config["pont_in_one_fq_change"])
|
||||||
|
|
||||||
|
signal_list = []
|
||||||
|
start = 0
|
||||||
|
segment_start_idx = 0
|
||||||
|
|
||||||
|
for idx in range(len(data)):
|
||||||
|
if (idx - start) > pont_in_one_fq:
|
||||||
|
segment = data[segment_start_idx:idx]
|
||||||
|
if segment.size > 0:
|
||||||
|
# Sum the segment to extract signal
|
||||||
|
signal_list.append(np.sum(segment))
|
||||||
|
start = idx
|
||||||
|
segment_start_idx = idx
|
||||||
|
|
||||||
|
return np.array(signal_list, dtype=float)
|
||||||
|
|
||||||
|
def _compute_fft_spectrum(
|
||||||
|
self,
|
||||||
|
signal: NDArray[np.floating],
|
||||||
|
) -> NDArray[np.floating]:
|
||||||
|
"""
|
||||||
|
Compute FFT spectrum with Gaussian smoothing.
|
||||||
|
|
||||||
|
Based on main.py lines 984-994.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
signal : ndarray
|
||||||
|
Input signal (processed or raw)
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
ndarray
|
||||||
|
Smoothed FFT magnitude spectrum
|
||||||
|
"""
|
||||||
|
fq_end = int(self._config["fq_end"])
|
||||||
|
gaussian_sigma = float(self._config["gaussian_sigma"])
|
||||||
|
fft0_delta = int(self._config["fft0_delta"])
|
||||||
|
|
||||||
|
# Cut signal to FFT length
|
||||||
|
sig_cut = signal[:fq_end] if len(signal) >= fq_end else signal
|
||||||
|
|
||||||
|
# Take square root of absolute value (as in main.py)
|
||||||
|
sig_cut = np.sqrt(np.abs(sig_cut))
|
||||||
|
|
||||||
|
# Compute FFT
|
||||||
|
F = np.fft.fft(sig_cut)
|
||||||
|
Fshift = np.abs(np.fft.fftshift(F))
|
||||||
|
|
||||||
|
# Zero out the center (remove DC component and nearby artifacts)
|
||||||
|
center = len(sig_cut) // 2
|
||||||
|
if center < len(Fshift):
|
||||||
|
zero_start = max(center - 0, 0)
|
||||||
|
zero_end = min(center + 1, len(Fshift))
|
||||||
|
Fshift[zero_start:zero_end] = 0
|
||||||
|
|
||||||
|
# Apply Gaussian smoothing
|
||||||
|
FshiftS = gaussian_filter1d(Fshift, gaussian_sigma)
|
||||||
|
|
||||||
|
return FshiftS
|
||||||
|
|
||||||
|
def _generate_frequency_axis(
|
||||||
|
self,
|
||||||
|
signal: NDArray[np.floating],
|
||||||
|
) -> NDArray[np.floating]:
|
||||||
|
"""
|
||||||
|
Generate frequency axis for visualization.
|
||||||
|
|
||||||
|
Based on main.py lines 1041-1042: maps signal indices to 3-13.67 GHz range.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
signal : ndarray
|
||||||
|
Processed signal
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
ndarray
|
||||||
|
Frequency axis in GHz
|
||||||
|
"""
|
||||||
|
freq_start = float(self._config["freq_start_ghz"])
|
||||||
|
freq_stop = float(self._config["freq_stop_ghz"])
|
||||||
|
|
||||||
|
signal_size = signal.size
|
||||||
|
if signal_size == 0:
|
||||||
|
return np.array([])
|
||||||
|
|
||||||
|
# Calculate frequency per point
|
||||||
|
freq_range = freq_stop - freq_start
|
||||||
|
per_point_fq = freq_range / signal_size
|
||||||
|
|
||||||
|
# Generate frequency axis: start + (index * per_point_fq)
|
||||||
|
freq_axis = freq_start + (np.arange(1, signal_size + 1) * per_point_fq)
|
||||||
|
|
||||||
|
return freq_axis
|
||||||
@ -397,11 +397,13 @@ class ProcessorManager:
|
|||||||
try:
|
try:
|
||||||
from .implementations.magnitude_processor import MagnitudeProcessor
|
from .implementations.magnitude_processor import MagnitudeProcessor
|
||||||
from .implementations.bscan_processor import BScanProcessor
|
from .implementations.bscan_processor import BScanProcessor
|
||||||
|
from .implementations.rfg_processor import RFGProcessor
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# self.register_processor(PhaseProcessor(self.config_dir))
|
# self.register_processor(PhaseProcessor(self.config_dir))
|
||||||
self.register_processor(BScanProcessor(self.config_dir))
|
self.register_processor(BScanProcessor(self.config_dir))
|
||||||
self.register_processor(MagnitudeProcessor(self.config_dir))
|
self.register_processor(MagnitudeProcessor(self.config_dir))
|
||||||
|
self.register_processor(RFGProcessor(self.config_dir))
|
||||||
# self.register_processor(SmithChartProcessor(self.config_dir))
|
# self.register_processor(SmithChartProcessor(self.config_dir))
|
||||||
|
|
||||||
logger.info("Default processors registered", count=len(self._processors))
|
logger.info("Default processors registered", count=len(self._processors))
|
||||||
|
|||||||
@ -11,6 +11,7 @@ from vna_system.core.processors.storage.data_storage import DataStorage
|
|||||||
from vna_system.core.settings.settings_manager import VNASettingsManager
|
from vna_system.core.settings.settings_manager import VNASettingsManager
|
||||||
from vna_system.core.processors.manager import ProcessorManager
|
from vna_system.core.processors.manager import ProcessorManager
|
||||||
from vna_system.core.processors.websocket_handler import ProcessorWebSocketHandler
|
from vna_system.core.processors.websocket_handler import ProcessorWebSocketHandler
|
||||||
|
from vna_system.core.laser.laser_controller import LaserController
|
||||||
from vna_system.core.config import PROCESSORS_CONFIG_DIR_PATH
|
from vna_system.core.config import PROCESSORS_CONFIG_DIR_PATH
|
||||||
|
|
||||||
# Global singleton instances
|
# Global singleton instances
|
||||||
@ -22,4 +23,7 @@ processor_manager: ProcessorManager = ProcessorManager(vna_data_acquisition_inst
|
|||||||
data_storage = DataStorage()
|
data_storage = DataStorage()
|
||||||
processor_websocket_handler: ProcessorWebSocketHandler = ProcessorWebSocketHandler(
|
processor_websocket_handler: ProcessorWebSocketHandler = ProcessorWebSocketHandler(
|
||||||
processor_manager, data_storage
|
processor_manager, data_storage
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Laser control system
|
||||||
|
laser_controller_instance: LaserController = LaserController()
|
||||||
@ -8,7 +8,7 @@ from fastapi import FastAPI
|
|||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
|
||||||
import vna_system.core.singletons as singletons
|
import vna_system.core.singletons as singletons
|
||||||
from vna_system.api.endpoints import acquisition, health, settings, web_ui
|
from vna_system.api.endpoints import acquisition, health, settings, web_ui, laser
|
||||||
from vna_system.api.websockets import processing as ws_processing
|
from vna_system.api.websockets import processing as ws_processing
|
||||||
from vna_system.core.config import API_HOST, API_PORT
|
from vna_system.core.config import API_HOST, API_PORT
|
||||||
from vna_system.core.logging.logger import get_component_logger, setup_logging
|
from vna_system.core.logging.logger import get_component_logger, setup_logging
|
||||||
@ -40,6 +40,17 @@ async def lifespan(app: FastAPI):
|
|||||||
processors=singletons.processor_manager.list_processors(),
|
processors=singletons.processor_manager.list_processors(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Try to connect to laser controller (optional, non-blocking)
|
||||||
|
logger.info("Attempting to connect to laser control hardware")
|
||||||
|
try:
|
||||||
|
result = singletons.laser_controller_instance.connect()
|
||||||
|
if result["success"]:
|
||||||
|
logger.info("Laser controller connected", port=result.get("port"))
|
||||||
|
else:
|
||||||
|
logger.warning("Laser controller connection failed (will retry on demand)", message=result.get("message"))
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("Failed to connect to laser controller on startup (will retry on demand)", error=str(e))
|
||||||
|
|
||||||
logger.info("VNA API Server started successfully")
|
logger.info("VNA API Server started successfully")
|
||||||
yield
|
yield
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
@ -47,6 +58,14 @@ async def lifespan(app: FastAPI):
|
|||||||
raise
|
raise
|
||||||
logger.info("Shutting down VNA API Server")
|
logger.info("Shutting down VNA API Server")
|
||||||
|
|
||||||
|
# Disconnect laser controller
|
||||||
|
if singletons.laser_controller_instance and singletons.laser_controller_instance.is_connected:
|
||||||
|
try:
|
||||||
|
singletons.laser_controller_instance.disconnect()
|
||||||
|
logger.info("Laser controller disconnected")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("Error disconnecting laser controller", error=str(e))
|
||||||
|
|
||||||
if singletons.processor_manager:
|
if singletons.processor_manager:
|
||||||
singletons.processor_manager.stop_processing()
|
singletons.processor_manager.stop_processing()
|
||||||
logger.info("Processor system stopped")
|
logger.info("Processor system stopped")
|
||||||
@ -77,6 +96,7 @@ app.include_router(web_ui.router)
|
|||||||
app.include_router(health.router)
|
app.include_router(health.router)
|
||||||
app.include_router(acquisition.router)
|
app.include_router(acquisition.router)
|
||||||
app.include_router(settings.router)
|
app.include_router(settings.router)
|
||||||
|
app.include_router(laser.router)
|
||||||
app.include_router(ws_processing.router)
|
app.include_router(ws_processing.router)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -574,3 +574,67 @@
|
|||||||
min-width: unset;
|
min-width: unset;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ========================================
|
||||||
|
Laser Parameters Table Styles
|
||||||
|
======================================== */
|
||||||
|
|
||||||
|
.laser-params-table {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: var(--space-3);
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
border: 1px solid var(--border-primary);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.laser-param-cell {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.laser-param-header {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
text-align: center;
|
||||||
|
padding: var(--space-3);
|
||||||
|
background: var(--bg-surface);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
border: 1px solid var(--border-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.laser-param-input {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.laser-param-input .settings-input {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive layout for smaller screens */
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.laser-params-table {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.laser-params-table {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.laser-param-header {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.laser-param-input .settings-input {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
* Handles Plotly.js chart creation, updates, and management
|
* Handles Plotly.js chart creation, updates, and management
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { formatProcessorName, safeClone, downloadJSON } from './utils.js';
|
import { formatProcessorName, safeClone, downloadJSON, showConfirmDialog } from './utils.js';
|
||||||
import { renderIcons } from './icons.js';
|
import { renderIcons } from './icons.js';
|
||||||
import { ChartSettingsManager } from './charts/chart-settings.js';
|
import { ChartSettingsManager } from './charts/chart-settings.js';
|
||||||
import { BScanClickHandler } from './charts/bscan-click-handler.js';
|
import { BScanClickHandler } from './charts/bscan-click-handler.js';
|
||||||
@ -374,12 +374,6 @@ export class ChartManager {
|
|||||||
if (processorData) {
|
if (processorData) {
|
||||||
downloadJSON(processorData, `${baseFilename}_data.json`);
|
downloadJSON(processorData, `${baseFilename}_data.json`);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.notifications?.show?.({
|
|
||||||
type: 'success',
|
|
||||||
title: 'Скачивание завершено',
|
|
||||||
message: `Скачаны график и данные ${formatProcessorName(id)}`
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -396,12 +390,6 @@ export class ChartManager {
|
|||||||
if (processorData) {
|
if (processorData) {
|
||||||
downloadJSON(processorData, `${baseFilename}_data.json`);
|
downloadJSON(processorData, `${baseFilename}_data.json`);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.notifications?.show?.({
|
|
||||||
type: 'warning',
|
|
||||||
title: 'Скачивание завершено',
|
|
||||||
message: `График скачан. Данные ограничены (нет подключения к серверу)`
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Chart download failed:', e);
|
console.error('Chart download failed:', e);
|
||||||
@ -592,12 +580,6 @@ export class ChartManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log('Total exported files:', exportedCount);
|
console.log('Total exported files:', exportedCount);
|
||||||
|
|
||||||
this.notifications?.show?.({
|
|
||||||
type: 'success',
|
|
||||||
title: 'Экспорт завершён',
|
|
||||||
message: `Данные свипов экспортированы для ${processorId}`
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
exportPointsToTSV(points, vnaConfig, filename) {
|
exportPointsToTSV(points, vnaConfig, filename) {
|
||||||
@ -723,6 +705,25 @@ export class ChartManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show dialog to choose whether to update settings or not
|
||||||
|
const choice = await showConfirmDialog({
|
||||||
|
title: 'Загрузка истории',
|
||||||
|
message: 'Обновить параметры процессора из файла или сохранить текущие настройки?',
|
||||||
|
buttons: [
|
||||||
|
{ value: 'cancel', text: 'Отмена', class: 'btn--secondary' },
|
||||||
|
{ value: 'data_only', text: 'Только данные', class: 'btn--primary' },
|
||||||
|
{ value: 'with_settings', text: 'Данные и настройки', class: 'btn--primary' }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
// User cancelled
|
||||||
|
if (!choice || choice === 'cancel') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine whether to include config
|
||||||
|
const configToSend = (choice === 'with_settings' && processorConfig) ? processorConfig : null;
|
||||||
|
|
||||||
// Send load_history message via WebSocket
|
// Send load_history message via WebSocket
|
||||||
const websocket = window.vnaDashboard?.websocket;
|
const websocket = window.vnaDashboard?.websocket;
|
||||||
if (websocket && websocket.ws && websocket.ws.readyState === WebSocket.OPEN) {
|
if (websocket && websocket.ws && websocket.ws.readyState === WebSocket.OPEN) {
|
||||||
@ -730,13 +731,14 @@ export class ChartManager {
|
|||||||
type: 'load_history',
|
type: 'load_history',
|
||||||
processor_id: processorId,
|
processor_id: processorId,
|
||||||
history_data: sweepHistory,
|
history_data: sweepHistory,
|
||||||
config: processorConfig
|
config: configToSend
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const settingsMsg = configToSend ? ' (с обновлением настроек)' : ' (без изменения настроек)';
|
||||||
this.notifications?.show?.({
|
this.notifications?.show?.({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
title: 'История загружена',
|
title: 'История загружена',
|
||||||
message: `Загружено ${sweepHistory.length} записей для ${formatProcessorName(processorId)}`
|
message: `Загружено ${sweepHistory.length} записей для ${formatProcessorName(processorId)}${settingsMsg}`
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.notifications?.show?.({
|
this.notifications?.show?.({
|
||||||
|
|||||||
@ -89,6 +89,15 @@ export const API = {
|
|||||||
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`
|
REFERENCE_PLOT: (name) => `${API_BASE}/settings/reference/${encodeURIComponent(name)}/plot`
|
||||||
|
},
|
||||||
|
|
||||||
|
LASER: {
|
||||||
|
START: `${API_BASE}/laser/start`,
|
||||||
|
START_MANUAL: `${API_BASE}/laser/start-manual`,
|
||||||
|
STOP: `${API_BASE}/laser/stop`,
|
||||||
|
STATUS: `${API_BASE}/laser/status`,
|
||||||
|
CONNECT: `${API_BASE}/laser/connect`,
|
||||||
|
DISCONNECT: `${API_BASE}/laser/disconnect`
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
import { PresetManager } from './settings/preset-manager.js';
|
import { PresetManager } from './settings/preset-manager.js';
|
||||||
import { CalibrationManager } from './settings/calibration-manager.js';
|
import { CalibrationManager } from './settings/calibration-manager.js';
|
||||||
import { ReferenceManager } from './settings/reference-manager.js';
|
import { ReferenceManager } from './settings/reference-manager.js';
|
||||||
|
import { LaserManager } from './settings/laser-manager.js';
|
||||||
import { Debouncer, ButtonState, downloadJSON } from './utils.js';
|
import { Debouncer, ButtonState, downloadJSON } from './utils.js';
|
||||||
import { renderIcons } from './icons.js';
|
import { renderIcons } from './icons.js';
|
||||||
import {
|
import {
|
||||||
@ -34,6 +35,7 @@ export class SettingsManager {
|
|||||||
this.presetManager = new PresetManager(notifications);
|
this.presetManager = new PresetManager(notifications);
|
||||||
this.calibrationManager = new CalibrationManager(notifications);
|
this.calibrationManager = new CalibrationManager(notifications);
|
||||||
this.referenceManager = new ReferenceManager(notifications);
|
this.referenceManager = new ReferenceManager(notifications);
|
||||||
|
this.laserManager = new LaserManager(notifications);
|
||||||
|
|
||||||
// Plots modal state
|
// Plots modal state
|
||||||
this.currentPlotsData = null;
|
this.currentPlotsData = null;
|
||||||
@ -100,6 +102,23 @@ export class SettingsManager {
|
|||||||
currentReferenceDescription: document.getElementById('currentReferenceDescription'),
|
currentReferenceDescription: document.getElementById('currentReferenceDescription'),
|
||||||
currentReferenceCalibration: document.getElementById('currentReferenceCalibration'),
|
currentReferenceCalibration: document.getElementById('currentReferenceCalibration'),
|
||||||
|
|
||||||
|
// Laser controls
|
||||||
|
laserManualMode: document.getElementById('laserManualMode'),
|
||||||
|
laserTemp1: document.getElementById('laserTemp1'),
|
||||||
|
laserTemp2: document.getElementById('laserTemp2'),
|
||||||
|
laserCurrent1: document.getElementById('laserCurrent1'),
|
||||||
|
laserCurrent2: document.getElementById('laserCurrent2'),
|
||||||
|
laserMinCurrent1: document.getElementById('laserMinCurrent1'),
|
||||||
|
laserMaxCurrent1: document.getElementById('laserMaxCurrent1'),
|
||||||
|
laserDeltaCurrent1: document.getElementById('laserDeltaCurrent1'),
|
||||||
|
laserScanTemp1: document.getElementById('laserScanTemp1'),
|
||||||
|
laserScanTemp2: document.getElementById('laserScanTemp2'),
|
||||||
|
laserScanCurrent2: document.getElementById('laserScanCurrent2'),
|
||||||
|
laserDeltaTime: document.getElementById('laserDeltaTime'),
|
||||||
|
laserTau: document.getElementById('laserTau'),
|
||||||
|
laserStartBtn: document.getElementById('laserStartBtn'),
|
||||||
|
laserStopBtn: document.getElementById('laserStopBtn'),
|
||||||
|
|
||||||
// Status
|
// Status
|
||||||
presetCount: document.getElementById('presetCount'),
|
presetCount: document.getElementById('presetCount'),
|
||||||
calibrationCount: document.getElementById('calibrationCount'),
|
calibrationCount: document.getElementById('calibrationCount'),
|
||||||
@ -118,6 +137,23 @@ export class SettingsManager {
|
|||||||
this.presetManager.init(this.elements);
|
this.presetManager.init(this.elements);
|
||||||
this.calibrationManager.init(this.elements);
|
this.calibrationManager.init(this.elements);
|
||||||
this.referenceManager.init(this.elements);
|
this.referenceManager.init(this.elements);
|
||||||
|
this.laserManager.init({
|
||||||
|
manualMode: this.elements.laserManualMode,
|
||||||
|
temp1: this.elements.laserTemp1,
|
||||||
|
temp2: this.elements.laserTemp2,
|
||||||
|
current1: this.elements.laserCurrent1,
|
||||||
|
current2: this.elements.laserCurrent2,
|
||||||
|
minCurrent1: this.elements.laserMinCurrent1,
|
||||||
|
maxCurrent1: this.elements.laserMaxCurrent1,
|
||||||
|
deltaCurrent1: this.elements.laserDeltaCurrent1,
|
||||||
|
scanTemp1: this.elements.laserScanTemp1,
|
||||||
|
scanTemp2: this.elements.laserScanTemp2,
|
||||||
|
scanCurrent2: this.elements.laserScanCurrent2,
|
||||||
|
deltaTime: this.elements.laserDeltaTime,
|
||||||
|
tau: this.elements.laserTau,
|
||||||
|
startBtn: this.elements.laserStartBtn,
|
||||||
|
stopBtn: this.elements.laserStopBtn
|
||||||
|
});
|
||||||
|
|
||||||
// Setup callbacks
|
// Setup callbacks
|
||||||
this.presetManager.onPresetChanged = async () => {
|
this.presetManager.onPresetChanged = async () => {
|
||||||
@ -569,8 +605,6 @@ export class SettingsManager {
|
|||||||
plot_data: plotsData.plot
|
plot_data: plotsData.plot
|
||||||
};
|
};
|
||||||
downloadJSON(data, `${base}_data.json`);
|
downloadJSON(data, `${base}_data.json`);
|
||||||
|
|
||||||
this.notify(SUCCESS, 'Скачивание завершено', `Скачаны график и данные эталона ${plotsData.reference_name}`);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Download reference failed:', e);
|
console.error('Download reference failed:', e);
|
||||||
this.notify(ERROR, 'Ошибка скачивания', 'Не удалось скачать данные эталона');
|
this.notify(ERROR, 'Ошибка скачивания', 'Не удалось скачать данные эталона');
|
||||||
@ -589,8 +623,6 @@ export class SettingsManager {
|
|||||||
|
|
||||||
const data = this.prepareCalibrationDownloadData(standardName);
|
const data = this.prepareCalibrationDownloadData(standardName);
|
||||||
downloadJSON(data, `${base}_data.json`);
|
downloadJSON(data, `${base}_data.json`);
|
||||||
|
|
||||||
this.notify(SUCCESS, 'Скачивание завершено', `Скачаны график и данные стандарта ${standardName.toUpperCase()}`);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Download standard failed:', e);
|
console.error('Download standard failed:', e);
|
||||||
this.notify(ERROR, 'Ошибка скачивания', 'Не удалось скачать данные калибровки');
|
this.notify(ERROR, 'Ошибка скачивания', 'Не удалось скачать данные калибровки');
|
||||||
|
|||||||
406
vna_system/web_ui/static/js/modules/settings/laser-manager.js
Normal file
406
vna_system/web_ui/static/js/modules/settings/laser-manager.js
Normal file
@ -0,0 +1,406 @@
|
|||||||
|
/**
|
||||||
|
* Laser Manager
|
||||||
|
* Handles laser control interface with two modes: manual and scan
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { apiPost } from '../api-client.js';
|
||||||
|
import { API, NOTIFICATION_TYPES } from '../constants.js';
|
||||||
|
|
||||||
|
const { SUCCESS, ERROR } = NOTIFICATION_TYPES;
|
||||||
|
|
||||||
|
export class LaserManager {
|
||||||
|
constructor(notifications) {
|
||||||
|
this.notifications = notifications;
|
||||||
|
this.elements = {};
|
||||||
|
this.isRunning = false;
|
||||||
|
this.isManualMode = false;
|
||||||
|
|
||||||
|
// Bind methods
|
||||||
|
this.handleModeChange = this.handleModeChange.bind(this);
|
||||||
|
this.handleStartClick = this.handleStartClick.bind(this);
|
||||||
|
this.handleStopClick = this.handleStopClick.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
init(elements) {
|
||||||
|
this.elements = elements;
|
||||||
|
|
||||||
|
// Add event listeners
|
||||||
|
this.elements.manualMode?.addEventListener('change', this.handleModeChange);
|
||||||
|
this.elements.startBtn?.addEventListener('click', this.handleStartClick);
|
||||||
|
this.elements.stopBtn?.addEventListener('click', this.handleStopClick);
|
||||||
|
|
||||||
|
// Initialize UI state
|
||||||
|
this.updateUIState();
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this.elements.manualMode?.removeEventListener('change', this.handleModeChange);
|
||||||
|
this.elements.startBtn?.removeEventListener('click', this.handleStartClick);
|
||||||
|
this.elements.stopBtn?.removeEventListener('click', this.handleStopClick);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleModeChange() {
|
||||||
|
this.isManualMode = this.elements.manualMode?.checked || false;
|
||||||
|
this.updateUIState();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateUIState() {
|
||||||
|
const manualSection = document.getElementById('laserManualSection');
|
||||||
|
const scanSection = document.getElementById('laserScanSection');
|
||||||
|
|
||||||
|
if (this.isManualMode) {
|
||||||
|
// Manual mode: show manual controls, hide scan controls
|
||||||
|
if (manualSection) manualSection.style.display = 'block';
|
||||||
|
if (scanSection) scanSection.style.display = 'none';
|
||||||
|
|
||||||
|
// Enable manual mode inputs only if not running
|
||||||
|
if (!this.isRunning) {
|
||||||
|
this.elements.temp1.disabled = false;
|
||||||
|
this.elements.temp2.disabled = false;
|
||||||
|
this.elements.current1.disabled = false;
|
||||||
|
this.elements.current2.disabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable scan inputs
|
||||||
|
this.elements.minCurrent1.disabled = true;
|
||||||
|
this.elements.maxCurrent1.disabled = true;
|
||||||
|
this.elements.deltaCurrent1.disabled = true;
|
||||||
|
this.elements.scanTemp1.disabled = true;
|
||||||
|
this.elements.scanTemp2.disabled = true;
|
||||||
|
this.elements.scanCurrent2.disabled = true;
|
||||||
|
this.elements.deltaTime.disabled = true;
|
||||||
|
this.elements.tau.disabled = true;
|
||||||
|
} else {
|
||||||
|
// Scan mode: hide manual controls, show scan controls
|
||||||
|
if (manualSection) manualSection.style.display = 'none';
|
||||||
|
if (scanSection) scanSection.style.display = 'block';
|
||||||
|
|
||||||
|
// Disable manual mode inputs
|
||||||
|
this.elements.temp1.disabled = true;
|
||||||
|
this.elements.temp2.disabled = true;
|
||||||
|
this.elements.current1.disabled = true;
|
||||||
|
this.elements.current2.disabled = true;
|
||||||
|
|
||||||
|
// Enable scan inputs only if not running
|
||||||
|
if (!this.isRunning) {
|
||||||
|
this.elements.minCurrent1.disabled = false;
|
||||||
|
this.elements.maxCurrent1.disabled = false;
|
||||||
|
this.elements.deltaCurrent1.disabled = false;
|
||||||
|
this.elements.scanTemp1.disabled = false;
|
||||||
|
this.elements.scanTemp2.disabled = false;
|
||||||
|
this.elements.scanCurrent2.disabled = false;
|
||||||
|
this.elements.deltaTime.disabled = false;
|
||||||
|
this.elements.tau.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable/disable start button based on running state
|
||||||
|
if (this.elements.startBtn) {
|
||||||
|
this.elements.startBtn.disabled = this.isRunning;
|
||||||
|
}
|
||||||
|
if (this.elements.stopBtn) {
|
||||||
|
this.elements.stopBtn.disabled = !this.isRunning;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleStartClick() {
|
||||||
|
if (this.isRunning) {
|
||||||
|
this.notify(ERROR, 'Ошибка', 'Цикл уже запущен');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Disable start button during request
|
||||||
|
this.elements.startBtn.disabled = true;
|
||||||
|
|
||||||
|
let parameters;
|
||||||
|
let endpoint;
|
||||||
|
|
||||||
|
if (this.isManualMode) {
|
||||||
|
// Manual mode - use simplified endpoint with only t1, t2, i1, i2
|
||||||
|
parameters = this.collectManualParametersSimple();
|
||||||
|
endpoint = API.LASER.START_MANUAL;
|
||||||
|
} else {
|
||||||
|
// Scan mode - use full endpoint with scan parameters
|
||||||
|
parameters = this.collectScanParameters();
|
||||||
|
endpoint = API.LASER.START;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate parameters
|
||||||
|
if (!this.validateParameters(parameters)) {
|
||||||
|
this.elements.startBtn.disabled = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send start request to appropriate endpoint
|
||||||
|
const response = await apiPost(endpoint, parameters);
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
this.isRunning = true;
|
||||||
|
this.notify(SUCCESS, 'Успешно', response.message);
|
||||||
|
|
||||||
|
// Disable all controls during operation
|
||||||
|
this.disableAllControls();
|
||||||
|
|
||||||
|
// Update button states
|
||||||
|
this.elements.startBtn.disabled = true;
|
||||||
|
this.elements.stopBtn.disabled = false;
|
||||||
|
|
||||||
|
console.log('Laser cycle started:', parameters);
|
||||||
|
} else {
|
||||||
|
this.notify(ERROR, 'Ошибка', response.message);
|
||||||
|
this.elements.startBtn.disabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to start laser cycle:', error);
|
||||||
|
this.notify(ERROR, 'Ошибка', `Не удалось запустить цикл: ${error.message}`);
|
||||||
|
this.elements.startBtn.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleStopClick() {
|
||||||
|
if (!this.isRunning) {
|
||||||
|
this.notify(ERROR, 'Ошибка', 'Цикл не запущен');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Disable stop button during request
|
||||||
|
this.elements.stopBtn.disabled = true;
|
||||||
|
|
||||||
|
const response = await apiPost(API.LASER.STOP, {});
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
this.isRunning = false;
|
||||||
|
this.notify(SUCCESS, 'Успешно', response.message);
|
||||||
|
|
||||||
|
// Re-enable controls
|
||||||
|
this.enableAllControls();
|
||||||
|
this.updateUIState();
|
||||||
|
|
||||||
|
console.log('Laser cycle stopped');
|
||||||
|
} else {
|
||||||
|
this.notify(ERROR, 'Ошибка', response.message);
|
||||||
|
this.elements.stopBtn.disabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to stop laser cycle:', error);
|
||||||
|
this.notify(ERROR, 'Ошибка', `Не удалось остановить цикл: ${error.message}`);
|
||||||
|
this.elements.stopBtn.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
collectManualParameters() {
|
||||||
|
// Manual mode: use fixed temperature and current values
|
||||||
|
// Set min=max to indicate manual mode
|
||||||
|
const temp1 = parseFloat(this.elements.temp1.value);
|
||||||
|
const temp2 = parseFloat(this.elements.temp2.value);
|
||||||
|
const current1 = parseFloat(this.elements.current1.value);
|
||||||
|
const current2 = parseFloat(this.elements.current2.value);
|
||||||
|
|
||||||
|
return {
|
||||||
|
// Manual mode indicated by setting enable_c1 = false and all ranges equal
|
||||||
|
enable_c1: false,
|
||||||
|
enable_t1: false,
|
||||||
|
enable_t2: false,
|
||||||
|
enable_c2: false,
|
||||||
|
|
||||||
|
// Temperature values (same for min/max in manual mode)
|
||||||
|
min_temp_1: temp1,
|
||||||
|
max_temp_1: temp1,
|
||||||
|
delta_temp_1: 0.05,
|
||||||
|
min_temp_2: temp2,
|
||||||
|
max_temp_2: temp2,
|
||||||
|
delta_temp_2: 0.05,
|
||||||
|
|
||||||
|
// Current values (same for min/max in manual mode)
|
||||||
|
min_current_1: current1,
|
||||||
|
max_current_1: current1,
|
||||||
|
delta_current_1: 0.05,
|
||||||
|
min_current_2: current2,
|
||||||
|
max_current_2: current2,
|
||||||
|
delta_current_2: 0.05,
|
||||||
|
|
||||||
|
// Time parameters (not used in manual mode but required)
|
||||||
|
delta_time: 50,
|
||||||
|
tau: 10
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
collectManualParametersSimple() {
|
||||||
|
// Simplified manual mode: only 4 parameters (t1, t2, i1, i2)
|
||||||
|
return {
|
||||||
|
t1: parseFloat(this.elements.temp1.value),
|
||||||
|
t2: parseFloat(this.elements.temp2.value),
|
||||||
|
i1: parseFloat(this.elements.current1.value),
|
||||||
|
i2: parseFloat(this.elements.current2.value)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
collectScanParameters() {
|
||||||
|
// Scan mode: scan current 1 while keeping other parameters fixed
|
||||||
|
return {
|
||||||
|
enable_c1: true, // Scanning current 1
|
||||||
|
enable_t1: false,
|
||||||
|
enable_t2: false,
|
||||||
|
enable_c2: false,
|
||||||
|
|
||||||
|
// Temperature 1 (fixed)
|
||||||
|
min_temp_1: parseFloat(this.elements.scanTemp1.value),
|
||||||
|
max_temp_1: parseFloat(this.elements.scanTemp1.value),
|
||||||
|
delta_temp_1: 0.05,
|
||||||
|
|
||||||
|
// Temperature 2 (fixed)
|
||||||
|
min_temp_2: parseFloat(this.elements.scanTemp2.value),
|
||||||
|
max_temp_2: parseFloat(this.elements.scanTemp2.value),
|
||||||
|
delta_temp_2: 0.05,
|
||||||
|
|
||||||
|
// Current 1 (scanning range)
|
||||||
|
min_current_1: parseFloat(this.elements.minCurrent1.value),
|
||||||
|
max_current_1: parseFloat(this.elements.maxCurrent1.value),
|
||||||
|
delta_current_1: parseFloat(this.elements.deltaCurrent1.value),
|
||||||
|
|
||||||
|
// Current 2 (fixed)
|
||||||
|
min_current_2: parseFloat(this.elements.scanCurrent2.value),
|
||||||
|
max_current_2: parseFloat(this.elements.scanCurrent2.value),
|
||||||
|
delta_current_2: 0.05,
|
||||||
|
|
||||||
|
// Time parameters
|
||||||
|
delta_time: parseInt(this.elements.deltaTime.value),
|
||||||
|
tau: parseInt(this.elements.tau.value)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
validateParameters(params) {
|
||||||
|
// Check if simplified format (t1, t2, i1, i2)
|
||||||
|
if ('t1' in params && 't2' in params && 'i1' in params && 'i2' in params) {
|
||||||
|
// Simplified format validation
|
||||||
|
const values = [params.t1, params.t2, params.i1, params.i2];
|
||||||
|
|
||||||
|
if (values.some(v => isNaN(v))) {
|
||||||
|
this.notify(ERROR, 'Ошибка валидации', 'Все поля должны быть заполнены корректными числами');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check temperature ranges
|
||||||
|
if (params.t1 < -1 || params.t1 > 45 || params.t2 < -1 || params.t2 > 45) {
|
||||||
|
this.notify(ERROR, 'Ошибка валидации', 'Температура должна быть от -1 до 45°C');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check current ranges
|
||||||
|
if (params.i1 < 15 || params.i1 > 70 || params.i2 < 15 || params.i2 > 60) {
|
||||||
|
this.notify(ERROR, 'Ошибка валидации', 'Ток лазера 1: 15-70мА, лазера 2: 15-60мА');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Full format validation (original)
|
||||||
|
// Check for NaN values
|
||||||
|
const values = [
|
||||||
|
params.min_temp_1, params.max_temp_1,
|
||||||
|
params.min_temp_2, params.max_temp_2,
|
||||||
|
params.min_current_1, params.max_current_1,
|
||||||
|
params.min_current_2, params.max_current_2,
|
||||||
|
params.delta_time, params.tau
|
||||||
|
];
|
||||||
|
|
||||||
|
if (values.some(v => isNaN(v))) {
|
||||||
|
this.notify(ERROR, 'Ошибка валидации', 'Все поля должны быть заполнены корректными числами');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check temperature ranges
|
||||||
|
if (params.min_temp_1 < -1 || params.min_temp_1 > 45 ||
|
||||||
|
params.max_temp_1 < -1 || params.max_temp_1 > 45 ||
|
||||||
|
params.min_temp_2 < -1 || params.min_temp_2 > 45 ||
|
||||||
|
params.max_temp_2 < -1 || params.max_temp_2 > 45) {
|
||||||
|
this.notify(ERROR, 'Ошибка валидации', 'Температура должна быть от -1 до 45°C');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check current ranges
|
||||||
|
if (params.min_current_1 < 15 || params.max_current_1 > 70 ||
|
||||||
|
params.min_current_2 < 15 || params.max_current_2 > 60) {
|
||||||
|
this.notify(ERROR, 'Ошибка валидации', 'Ток должен быть в допустимых пределах');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// In scan mode, check min < max for current 1
|
||||||
|
if (!this.isManualMode) {
|
||||||
|
if (params.min_current_1 >= params.max_current_1) {
|
||||||
|
this.notify(ERROR, 'Ошибка валидации',
|
||||||
|
'Минимальный ток лазера 1 должен быть меньше максимального');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check delta_time is multiple of 10
|
||||||
|
if (params.delta_time % 10 !== 0) {
|
||||||
|
this.notify(ERROR, 'Ошибка валидации',
|
||||||
|
'Шаг дискретизации времени должен быть кратен 10 мкс');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check time parameters
|
||||||
|
if (params.delta_time < 20 || params.delta_time > 100) {
|
||||||
|
this.notify(ERROR, 'Ошибка валидации',
|
||||||
|
'Шаг дискретизации времени должен быть от 20 до 100 мкс');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.tau < 3 || params.tau > 10) {
|
||||||
|
this.notify(ERROR, 'Ошибка валидации',
|
||||||
|
'Время задержки должно быть от 3 до 10 мс');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
disableAllControls() {
|
||||||
|
// Disable mode checkbox
|
||||||
|
if (this.elements.manualMode) {
|
||||||
|
this.elements.manualMode.disabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable all input fields
|
||||||
|
const allInputs = [
|
||||||
|
this.elements.temp1,
|
||||||
|
this.elements.temp2,
|
||||||
|
this.elements.current1,
|
||||||
|
this.elements.current2,
|
||||||
|
this.elements.minCurrent1,
|
||||||
|
this.elements.maxCurrent1,
|
||||||
|
this.elements.deltaCurrent1,
|
||||||
|
this.elements.scanTemp1,
|
||||||
|
this.elements.scanTemp2,
|
||||||
|
this.elements.scanCurrent2,
|
||||||
|
this.elements.deltaTime,
|
||||||
|
this.elements.tau
|
||||||
|
];
|
||||||
|
|
||||||
|
allInputs.forEach(input => {
|
||||||
|
if (input) input.disabled = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
enableAllControls() {
|
||||||
|
// Enable mode checkbox
|
||||||
|
if (this.elements.manualMode) {
|
||||||
|
this.elements.manualMode.disabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// UI state will be updated by updateUIState() call after this
|
||||||
|
}
|
||||||
|
|
||||||
|
notify(type, title, message) {
|
||||||
|
if (this.notifications) {
|
||||||
|
this.notifications.show(type, title, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -311,3 +311,77 @@ export function formatBytes(bytes) {
|
|||||||
|
|
||||||
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
|
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show a confirmation dialog with custom options
|
||||||
|
* @param {Object} options - Dialog options
|
||||||
|
* @param {string} options.title - Dialog title
|
||||||
|
* @param {string} options.message - Dialog message
|
||||||
|
* @param {Array<Object>} options.buttons - Array of button configurations
|
||||||
|
* @returns {Promise<string>} Resolves with the button value that was clicked
|
||||||
|
*/
|
||||||
|
export function showConfirmDialog({ title, message, buttons }) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
// Create modal structure
|
||||||
|
const modal = document.createElement('div');
|
||||||
|
modal.className = 'modal modal--active';
|
||||||
|
|
||||||
|
const buttonsHtml = buttons.map(btn =>
|
||||||
|
`<button class="btn ${btn.class || 'btn--secondary'}" data-value="${btn.value}">${btn.text}</button>`
|
||||||
|
).join('');
|
||||||
|
|
||||||
|
modal.innerHTML = `
|
||||||
|
<div class="modal__backdrop" style="background-color: rgba(0, 0, 0, 0.85);"></div>
|
||||||
|
<div class="modal__content" style="background-color: #1e293b;">
|
||||||
|
<div class="modal__header">
|
||||||
|
<h3 class="modal__title">${escapeHtml(title)}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="modal__body" style="padding: var(--space-6);">
|
||||||
|
<p style="margin: 0; color: var(--color-text-secondary);">${escapeHtml(message)}</p>
|
||||||
|
</div>
|
||||||
|
<div style="padding: var(--space-6); padding-top: 0; display: flex; gap: var(--space-3); justify-content: flex-end;">
|
||||||
|
${buttonsHtml}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(modal);
|
||||||
|
|
||||||
|
// Handle button clicks
|
||||||
|
const handleClick = (e) => {
|
||||||
|
const button = e.target.closest('[data-value]');
|
||||||
|
if (button) {
|
||||||
|
const value = button.dataset.value;
|
||||||
|
cleanup();
|
||||||
|
resolve(value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle backdrop click
|
||||||
|
const handleBackdrop = (e) => {
|
||||||
|
if (e.target.classList.contains('modal__backdrop')) {
|
||||||
|
cleanup();
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle escape key
|
||||||
|
const handleEscape = (e) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
cleanup();
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
modal.removeEventListener('click', handleClick);
|
||||||
|
modal.removeEventListener('click', handleBackdrop);
|
||||||
|
document.removeEventListener('keydown', handleEscape);
|
||||||
|
modal.remove();
|
||||||
|
};
|
||||||
|
|
||||||
|
modal.addEventListener('click', handleClick);
|
||||||
|
modal.addEventListener('click', handleBackdrop);
|
||||||
|
document.addEventListener('keydown', handleEscape);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@ -327,6 +327,121 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Laser Control Section -->
|
||||||
|
<div class="settings-card">
|
||||||
|
<h3 class="settings-card-title">Управление лазером</h3>
|
||||||
|
<p class="settings-card-description">Контроль параметров лазерной схемы</p>
|
||||||
|
|
||||||
|
<div class="laser-controls">
|
||||||
|
<!-- Mode Selection -->
|
||||||
|
<div class="control-section">
|
||||||
|
<h4 class="control-section-title">Режим работы</h4>
|
||||||
|
<div class="control-group">
|
||||||
|
<label class="control-label control-label--checkbox">
|
||||||
|
<input type="checkbox" id="laserManualMode">
|
||||||
|
<span>Ручной режим ввода</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Manual Mode Controls -->
|
||||||
|
<div class="control-section" id="laserManualSection">
|
||||||
|
<h4 class="control-section-title">Параметры ручного режима</h4>
|
||||||
|
<div class="laser-params-table">
|
||||||
|
<div class="laser-param-cell laser-param-header">T1 (°C)</div>
|
||||||
|
<div class="laser-param-cell laser-param-header">T2 (°C)</div>
|
||||||
|
<div class="laser-param-cell laser-param-header">Ток L1 (мА)</div>
|
||||||
|
<div class="laser-param-cell laser-param-header">Ток L2 (мА)</div>
|
||||||
|
|
||||||
|
<div class="laser-param-cell laser-param-input">
|
||||||
|
<input type="number" class="settings-input" id="laserTemp1"
|
||||||
|
min="-1" max="45" step="0.1" value="28">
|
||||||
|
</div>
|
||||||
|
<div class="laser-param-cell laser-param-input">
|
||||||
|
<input type="number" class="settings-input" id="laserTemp2"
|
||||||
|
min="-1" max="45" step="0.1" value="28.9">
|
||||||
|
</div>
|
||||||
|
<div class="laser-param-cell laser-param-input">
|
||||||
|
<input type="number" class="settings-input" id="laserCurrent1"
|
||||||
|
min="15" max="60" step="0.1" value="33">
|
||||||
|
</div>
|
||||||
|
<div class="laser-param-cell laser-param-input">
|
||||||
|
<input type="number" class="settings-input" id="laserCurrent2"
|
||||||
|
min="15" max="60" step="0.1" value="35">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Scan Mode Controls -->
|
||||||
|
<div class="control-section" id="laserScanSection" style="display: none;">
|
||||||
|
<h4 class="control-section-title">Параметры сканирования тока лазера 1</h4>
|
||||||
|
<div class="laser-params-table">
|
||||||
|
<!-- Row 1: Headers -->
|
||||||
|
<div class="laser-param-cell laser-param-header">Мин ток L1 (мА)</div>
|
||||||
|
<div class="laser-param-cell laser-param-header">Макс ток L1 (мА)</div>
|
||||||
|
<div class="laser-param-cell laser-param-header">Шаг тока L1 (мА)</div>
|
||||||
|
<div class="laser-param-cell laser-param-header">Δt (мкс)</div>
|
||||||
|
|
||||||
|
<!-- Row 2: Inputs -->
|
||||||
|
<div class="laser-param-cell laser-param-input">
|
||||||
|
<input type="number" class="settings-input" id="laserMinCurrent1"
|
||||||
|
min="15" max="70" step="0.1" value="33" disabled>
|
||||||
|
</div>
|
||||||
|
<div class="laser-param-cell laser-param-input">
|
||||||
|
<input type="number" class="settings-input" id="laserMaxCurrent1"
|
||||||
|
min="15" max="70" step="0.1" value="70" disabled>
|
||||||
|
</div>
|
||||||
|
<div class="laser-param-cell laser-param-input">
|
||||||
|
<input type="number" class="settings-input" id="laserDeltaCurrent1"
|
||||||
|
min="0.002" max="0.5" step="0.001" value="0.05" disabled>
|
||||||
|
</div>
|
||||||
|
<div class="laser-param-cell laser-param-input">
|
||||||
|
<input type="number" class="settings-input" id="laserDeltaTime"
|
||||||
|
min="20" max="100" step="10" value="50" disabled>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Row 3: Fixed params headers -->
|
||||||
|
<div class="laser-param-cell laser-param-header">T1 (°C)</div>
|
||||||
|
<div class="laser-param-cell laser-param-header">T2 (°C)</div>
|
||||||
|
<div class="laser-param-cell laser-param-header">Ток L2 (мА)</div>
|
||||||
|
<div class="laser-param-cell laser-param-header">Tau (мс)</div>
|
||||||
|
|
||||||
|
<!-- Row 4: Fixed params inputs -->
|
||||||
|
<div class="laser-param-cell laser-param-input">
|
||||||
|
<input type="number" class="settings-input" id="laserScanTemp1"
|
||||||
|
min="-1" max="45" step="0.1" value="28" disabled>
|
||||||
|
</div>
|
||||||
|
<div class="laser-param-cell laser-param-input">
|
||||||
|
<input type="number" class="settings-input" id="laserScanTemp2"
|
||||||
|
min="-1" max="45" step="0.1" value="28.9" disabled>
|
||||||
|
</div>
|
||||||
|
<div class="laser-param-cell laser-param-input">
|
||||||
|
<input type="number" class="settings-input" id="laserScanCurrent2"
|
||||||
|
min="15" max="60" step="0.1" value="35" disabled>
|
||||||
|
</div>
|
||||||
|
<div class="laser-param-cell laser-param-input">
|
||||||
|
<input type="number" class="settings-input" id="laserTau"
|
||||||
|
min="3" max="10" step="1" value="10" disabled>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Control Buttons -->
|
||||||
|
<div class="control-section">
|
||||||
|
<div class="control-group control-group--buttons">
|
||||||
|
<button class="btn btn--primary" id="laserStartBtn">
|
||||||
|
<span data-icon="play"></span>
|
||||||
|
Пуск
|
||||||
|
</button>
|
||||||
|
<button class="btn btn--secondary" id="laserStopBtn" disabled>
|
||||||
|
<span data-icon="square"></span>
|
||||||
|
Стоп
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- System Summary -->
|
<!-- System Summary -->
|
||||||
<div class="settings-card">
|
<div class="settings-card">
|
||||||
<h3 class="settings-card-title">Сводка системы</h3>
|
<h3 class="settings-card-title">Сводка системы</h3>
|
||||||
|
|||||||
Reference in New Issue
Block a user