diff --git a/vna_system/api/endpoints/laser.py b/vna_system/api/endpoints/laser.py new file mode 100644 index 0000000..49b48ef --- /dev/null +++ b/vna_system/api/endpoints/laser.py @@ -0,0 +1,179 @@ +from fastapi import APIRouter, HTTPException, Depends +from typing import Dict, Any +import logging + +from ...api.models.laser import ( + LaserParameters, + LaserStatus, + LaserStartResponse, + LaserStopResponse +) +from ...core.laser import LaserController + +logger = logging.getLogger(__name__) + +router = APIRouter(prefix="/api/v1/laser", tags=["laser"]) + +# Singleton laser controller instance +_laser_controller: LaserController = None + + +def get_laser_controller() -> LaserController: + """Dependency to get laser controller instance""" + global _laser_controller + if _laser_controller is None: + _laser_controller = LaserController() + return _laser_controller + + +@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 at least one control mode is enabled + if not any([ + parameters.enable_t1, + parameters.enable_t2, + parameters.enable_c1, + parameters.enable_c2 + ]): + raise HTTPException( + status_code=400, + detail="Необходимо включить хотя бы один режим управления (температура или ток)" + ) + + # Validate that only one mode is enabled at a time + 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("/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}") + + 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)}") diff --git a/vna_system/api/models/laser.py b/vna_system/api/models/laser.py new file mode 100644 index 0000000..5a1c835 --- /dev/null +++ b/vna_system/api/models/laser.py @@ -0,0 +1,103 @@ +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="Сообщение о результате") diff --git a/vna_system/core/laser/__init__.py b/vna_system/core/laser/__init__.py new file mode 100644 index 0000000..d7bdce4 --- /dev/null +++ b/vna_system/core/laser/__init__.py @@ -0,0 +1,3 @@ +from .laser_controller import LaserController + +__all__ = ['LaserController'] diff --git a/vna_system/core/laser/laser_controller.py b/vna_system/core/laser/laser_controller.py new file mode 100644 index 0000000..3f85ef3 --- /dev/null +++ b/vna_system/core/laser/laser_controller.py @@ -0,0 +1,213 @@ +import logging +from typing import Optional, Dict, Any +from datetime import datetime + +from ...api.models.laser import LaserParameters, LaserStatus + +logger = logging.getLogger(__name__) + + +class LaserController: + """ + Controller for laser control system. + + This is a stub implementation that logs all actions. + Future integration with actual hardware would use serial communication + similar to the RadioPhotonic_PCB_PC_software project. + """ + + def __init__(self): + self.is_connected = False + self.is_running = False + self.current_parameters: Optional[LaserParameters] = None + self.current_status = LaserStatus() + logger.info("LaserController initialized") + + def start_cycle(self, parameters: LaserParameters) -> Dict[str, Any]: + """ + Start laser control cycle with given parameters. + + Args: + parameters: LaserParameters object with control settings + + Returns: + Dictionary with success status and message + """ + try: + 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) + + # Log all parameters + logger.info("Laser 1 Temperature Parameters:") + logger.info(f" Min Temperature: {parameters.min_temp_1}°C") + logger.info(f" Max Temperature: {parameters.max_temp_1}°C") + logger.info(f" Delta Temperature: {parameters.delta_temp_1}°C") + + logger.info("Laser 1 Current Parameters:") + logger.info(f" Min Current: {parameters.min_current_1} mA") + logger.info(f" Max Current: {parameters.max_current_1} mA") + logger.info(f" Delta Current: {parameters.delta_current_1} mA") + + logger.info("Laser 2 Temperature Parameters:") + logger.info(f" Min Temperature: {parameters.min_temp_2}°C") + logger.info(f" Max Temperature: {parameters.max_temp_2}°C") + logger.info(f" Delta Temperature: {parameters.delta_temp_2}°C") + + logger.info("Laser 2 Current Parameters:") + logger.info(f" Min Current: {parameters.min_current_2} mA") + logger.info(f" Max Current: {parameters.max_current_2} mA") + logger.info(f" Delta Current: {parameters.delta_current_2} mA") + + logger.info("Time Parameters:") + logger.info(f" Delta Time: {parameters.delta_time} μs") + logger.info(f" Tau (Delay): {parameters.tau} ms") + + logger.info("Control Mode Flags:") + logger.info(f" Enable Temperature Control Laser 1: {parameters.enable_t1}") + logger.info(f" Enable Temperature Control Laser 2: {parameters.enable_t2}") + logger.info(f" Enable Current Control Laser 1: {parameters.enable_c1}") + logger.info(f" Enable Current Control Laser 2: {parameters.enable_c2}") + + logger.info("=" * 60) + + # Store current parameters + self.current_parameters = parameters + self.is_running = True + self.current_status.is_running = True + + return { + "success": True, + "message": "Цикл управления лазером успешно запущен (stub mode - без реального устройства)", + "parameters": parameters.model_dump() + } + + 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 stop_cycle(self) -> Dict[str, Any]: + """ + Stop current laser control cycle. + + 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) + + self.is_running = False + self.current_status.is_running = False + self.current_parameters = None + + return { + "success": True, + "message": "Цикл управления лазером остановлен" + } + + except Exception as e: + logger.error(f"Error stopping laser cycle: {e}", exc_info=True) + return { + "success": False, + "message": f"Ошибка при остановке цикла: {str(e)}" + } + + def get_status(self) -> LaserStatus: + """ + Get current laser status. + + Returns: + LaserStatus object with current state + """ + # In real implementation, this would query the hardware + # For now, return stub data + self.current_status.connected = self.is_connected + self.current_status.is_running = self.is_running + + # Stub values (would come from actual hardware) + if self.is_connected: + self.current_status.temp_1 = 28.0 + self.current_status.temp_2 = 28.9 + self.current_status.current_1 = -0.02 + self.current_status.current_2 = -0.02 + self.current_status.temp_ext_1 = 30.95 + self.current_status.temp_ext_2 = 29.58 + self.current_status.voltage_3v3 = 3.30 + self.current_status.voltage_5v1 = 4.92 + self.current_status.voltage_5v2 = 4.96 + self.current_status.voltage_7v0 = 7.57 + + return self.current_status + + def connect(self, port: Optional[str] = None) -> Dict[str, Any]: + """ + Connect to laser control hardware. + + Args: + port: Serial port to connect to (e.g., '/dev/ttyUSB0') + + Returns: + Dictionary with success status and message + """ + try: + logger.info(f"Attempting to connect to laser hardware on port: {port or 'auto-detect'}") + + # In real implementation, would use serial communication here + # For now, just simulate connection + self.is_connected = True + self.current_status.connected = True + + logger.info("Successfully connected to laser hardware (stub mode)") + + return { + "success": True, + "message": f"Подключено к устройству (stub mode)", + "port": port or "auto-detected" + } + + except Exception as e: + logger.error(f"Error connecting to laser hardware: {e}", exc_info=True) + return { + "success": False, + "message": f"Ошибка подключения: {str(e)}", + "port": port + } + + def disconnect(self) -> Dict[str, Any]: + """ + Disconnect from laser control hardware. + + 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() + + self.is_connected = False + self.current_status.connected = False + + 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)}" + } diff --git a/vna_system/core/processors/configs/bscan_config.json b/vna_system/core/processors/configs/bscan_config.json index 6696aa4..e211909 100644 --- a/vna_system/core/processors/configs/bscan_config.json +++ b/vna_system/core/processors/configs/bscan_config.json @@ -1,5 +1,5 @@ { - "open_air": true, + "open_air": false, "axis": "abs", "cut": 0.23, "max": 1.5, @@ -7,8 +7,8 @@ "start_freq": 470.0, "stop_freq": 8800.0, "clear_history": false, - "sigma": 3.42, - "border_border_m": 0.26, + "sigma": 1.44, + "border_border_m": 0.41, "if_normalize": false, "if_draw_level": false, "detection_level": 3.0, diff --git a/vna_system/main.py b/vna_system/main.py index fba59d7..fedc157 100644 --- a/vna_system/main.py +++ b/vna_system/main.py @@ -8,7 +8,7 @@ from fastapi import FastAPI from fastapi.staticfiles import StaticFiles 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.core.config import API_HOST, API_PORT from vna_system.core.logging.logger import get_component_logger, setup_logging @@ -77,6 +77,7 @@ app.include_router(web_ui.router) app.include_router(health.router) app.include_router(acquisition.router) app.include_router(settings.router) +app.include_router(laser.router) app.include_router(ws_processing.router) diff --git a/vna_system/web_ui/static/css/settings.css b/vna_system/web_ui/static/css/settings.css index 96e2292..c632dce 100644 --- a/vna_system/web_ui/static/css/settings.css +++ b/vna_system/web_ui/static/css/settings.css @@ -574,3 +574,67 @@ 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; + } +} diff --git a/vna_system/web_ui/static/js/modules/constants.js b/vna_system/web_ui/static/js/modules/constants.js index 8d20058..6817ef9 100644 --- a/vna_system/web_ui/static/js/modules/constants.js +++ b/vna_system/web_ui/static/js/modules/constants.js @@ -89,6 +89,14 @@ export const API = { REFERENCE_CURRENT: `${API_BASE}/settings/reference/current`, REFERENCE_ITEM: (name) => `${API_BASE}/settings/reference/${encodeURIComponent(name)}`, REFERENCE_PLOT: (name) => `${API_BASE}/settings/reference/${encodeURIComponent(name)}/plot` + }, + + LASER: { + START: `${API_BASE}/laser/start`, + STOP: `${API_BASE}/laser/stop`, + STATUS: `${API_BASE}/laser/status`, + CONNECT: `${API_BASE}/laser/connect`, + DISCONNECT: `${API_BASE}/laser/disconnect` } }; diff --git a/vna_system/web_ui/static/js/modules/settings.js b/vna_system/web_ui/static/js/modules/settings.js index 4eb1aa3..4381de6 100644 --- a/vna_system/web_ui/static/js/modules/settings.js +++ b/vna_system/web_ui/static/js/modules/settings.js @@ -6,6 +6,7 @@ import { PresetManager } from './settings/preset-manager.js'; import { CalibrationManager } from './settings/calibration-manager.js'; import { ReferenceManager } from './settings/reference-manager.js'; +import { LaserManager } from './settings/laser-manager.js'; import { Debouncer, ButtonState, downloadJSON } from './utils.js'; import { renderIcons } from './icons.js'; import { @@ -34,6 +35,7 @@ export class SettingsManager { this.presetManager = new PresetManager(notifications); this.calibrationManager = new CalibrationManager(notifications); this.referenceManager = new ReferenceManager(notifications); + this.laserManager = new LaserManager(notifications); // Plots modal state this.currentPlotsData = null; @@ -100,6 +102,23 @@ export class SettingsManager { currentReferenceDescription: document.getElementById('currentReferenceDescription'), 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 presetCount: document.getElementById('presetCount'), calibrationCount: document.getElementById('calibrationCount'), @@ -118,6 +137,23 @@ export class SettingsManager { this.presetManager.init(this.elements); this.calibrationManager.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 this.presetManager.onPresetChanged = async () => { diff --git a/vna_system/web_ui/static/js/modules/settings/laser-manager.js b/vna_system/web_ui/static/js/modules/settings/laser-manager.js new file mode 100644 index 0000000..3210f13 --- /dev/null +++ b/vna_system/web_ui/static/js/modules/settings/laser-manager.js @@ -0,0 +1,366 @@ +/** + * Laser Manager + * Handles laser control interface with two modes: manual and scan + */ + +import { ButtonState } from '../utils.js'; +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 { + ButtonState.disable(this.elements.startBtn); + + let parameters; + + if (this.isManualMode) { + // Manual mode - set fixed values + parameters = this.collectManualParameters(); + } else { + // Scan mode - set scan parameters + parameters = this.collectScanParameters(); + } + + // Validate parameters + if (!this.validateParameters(parameters)) { + ButtonState.enable(this.elements.startBtn); + return; + } + + // Send start request + const response = await apiPost(API.LASER.START, 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); + ButtonState.enable(this.elements.startBtn); + } + + } catch (error) { + console.error('Failed to start laser cycle:', error); + this.notify(ERROR, 'Ошибка', `Не удалось запустить цикл: ${error.message}`); + ButtonState.enable(this.elements.startBtn); + } + } + + async handleStopClick() { + if (!this.isRunning) { + this.notify(ERROR, 'Ошибка', 'Цикл не запущен'); + return; + } + + try { + ButtonState.disable(this.elements.stopBtn); + + 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); + ButtonState.enable(this.elements.stopBtn); + } + + } catch (error) { + console.error('Failed to stop laser cycle:', error); + this.notify(ERROR, 'Ошибка', `Не удалось остановить цикл: ${error.message}`); + ButtonState.enable(this.elements.stopBtn); + } + } + + 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 + }; + } + + 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 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); + } + } +} diff --git a/vna_system/web_ui/templates/index.html b/vna_system/web_ui/templates/index.html index b571be1..597e899 100644 --- a/vna_system/web_ui/templates/index.html +++ b/vna_system/web_ui/templates/index.html @@ -327,6 +327,130 @@ + +
+

Управление лазером

+

Контроль параметров лазерной схемы оптического смесителя

+ +
+ +
+

Режим работы

+
+ +
+
+ + +
+

Параметры ручного режима

+
+
Температура лазера 1 (°C)
+
Температура лазера 2 (°C)
+
Управляющий ток лазера 1 (15-60 мА)
+
Управляющий ток лазера 2 (15-60 мА)
+ +
+ +
+
+ +
+
+ +
+
+ +
+
+
+ + + + + +
+
+ + +
+
+
+
+

Сводка системы