sweep feature added
This commit is contained in:
@@ -35,6 +35,7 @@ PyQt6-приложение для управления лазерной плат
|
|||||||
- ручной режим: `T1/T2/I1/I2`
|
- ручной режим: `T1/T2/I1/I2`
|
||||||
- live telemetry: `T1/T2`, внешние термисторы, фотодиоды, `3V3/5V1/5V2/7V0`
|
- live telemetry: `T1/T2`, внешние термисторы, фотодиоды, `3V3/5V1/5V2/7V0`
|
||||||
- TEC drive modulation: синусоидальная добавка к выходу TEC PID для выбранного лазера
|
- TEC drive modulation: синусоидальная добавка к выходу TEC PID для выбранного лазера
|
||||||
|
- свипирование тока лазера 2 и STM32 DAC: лесенка min..max с заданным шагом, временем шага и паузой между свипами
|
||||||
- AD9102: saw/SRAM режимы и загрузка custom waveform
|
- AD9102: saw/SRAM режимы и загрузка custom waveform
|
||||||
- AD9833, DS1809 и STM32 DAC через отдельные firmware-команды
|
- AD9833, DS1809 и STM32 DAC через отдельные firmware-команды
|
||||||
- сохранение профиля на SD-карту устройства
|
- сохранение профиля на SD-карту устройства
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ SHORT_CONTROL_TOTAL_LENGTH = 10
|
|||||||
WAVE_DATA_TOTAL_LENGTH = 30
|
WAVE_DATA_TOTAL_LENGTH = 30
|
||||||
PROFILE_SAVE_CONTROL_TOTAL_LENGTH = 30
|
PROFILE_SAVE_CONTROL_TOTAL_LENGTH = 30
|
||||||
PROFILE_SAVE_DATA_TOTAL_LENGTH = 30
|
PROFILE_SAVE_DATA_TOTAL_LENGTH = 30
|
||||||
|
SWEEP_CONTROL_TOTAL_LENGTH = 16
|
||||||
SHORT_COMMAND_LENGTH = 2
|
SHORT_COMMAND_LENGTH = 2
|
||||||
STATUS_RESPONSE_LENGTH = 2
|
STATUS_RESPONSE_LENGTH = 2
|
||||||
|
|
||||||
@@ -35,6 +36,7 @@ CMD_AD9102_WAVE_CONTROL = 0xCCCC
|
|||||||
CMD_AD9102_WAVE_DATA = 0xDDDD
|
CMD_AD9102_WAVE_DATA = 0xDDDD
|
||||||
CMD_PROFILE_SAVE_DATA = 0xEEEE
|
CMD_PROFILE_SAVE_DATA = 0xEEEE
|
||||||
CMD_TEC_MODULATION_CONTROL = 0xF0F0
|
CMD_TEC_MODULATION_CONTROL = 0xF0F0
|
||||||
|
CMD_SWEEP_CONTROL = 0xF1F1
|
||||||
|
|
||||||
# ---- Setup-word bit layout from firmware app_decode_work_packet()
|
# ---- Setup-word bit layout from firmware app_decode_work_packet()
|
||||||
|
|
||||||
@@ -106,6 +108,9 @@ STM32_DAC_FLAG_ENABLE = 0x0001
|
|||||||
TEC_MODULATION_FLAG_ENABLE = 0x0001
|
TEC_MODULATION_FLAG_ENABLE = 0x0001
|
||||||
TEC_MODULATION_FLAG_CHANNEL_2 = 0x0002
|
TEC_MODULATION_FLAG_CHANNEL_2 = 0x0002
|
||||||
|
|
||||||
|
SWEEP_FLAG_ENABLE = 0x0001
|
||||||
|
SWEEP_FLAG_TARGET_STM32_DAC = 0x0002
|
||||||
|
|
||||||
AD9102_WAVE_OPCODE_BEGIN = 0x0001
|
AD9102_WAVE_OPCODE_BEGIN = 0x0001
|
||||||
AD9102_WAVE_OPCODE_COMMIT = 0x0002
|
AD9102_WAVE_OPCODE_COMMIT = 0x0002
|
||||||
AD9102_WAVE_OPCODE_CANCEL = 0x0003
|
AD9102_WAVE_OPCODE_CANCEL = 0x0003
|
||||||
@@ -194,7 +199,14 @@ STM32_DAC_CODE_MAX = 4095
|
|||||||
TEC_MODULATION_FREQUENCY_MIN_HZ = 50
|
TEC_MODULATION_FREQUENCY_MIN_HZ = 50
|
||||||
TEC_MODULATION_FREQUENCY_MAX_HZ = 2_000
|
TEC_MODULATION_FREQUENCY_MAX_HZ = 2_000
|
||||||
TEC_MODULATION_AMPLITUDE_CODE_MIN = 0
|
TEC_MODULATION_AMPLITUDE_CODE_MIN = 0
|
||||||
TEC_MODULATION_AMPLITUDE_CODE_MAX = 4_096
|
TEC_MODULATION_AMPLITUDE_CODE_MAX = 27_900 # Half of the firmware TEC drive range 1000..56800.
|
||||||
|
|
||||||
|
# Firmware sweep tick: step periods are rounded down to a multiple of it.
|
||||||
|
SWEEP_TICK_US = 10
|
||||||
|
SWEEP_STEP_PERIOD_MIN_US = 20
|
||||||
|
SWEEP_STEP_PERIOD_MAX_US = 65_530
|
||||||
|
SWEEP_PAUSE_MAX_MS = 65_535
|
||||||
|
SWEEP_CURRENT_STEP_MIN_MA = 0.002 # One laser DAC code is ~0.001 mA.
|
||||||
|
|
||||||
# ---- Rail tolerances
|
# ---- Rail tolerances
|
||||||
|
|
||||||
@@ -232,6 +244,12 @@ DEFAULT_STM32_DAC_CODE = round(
|
|||||||
)
|
)
|
||||||
DEFAULT_TEC_MODULATION_FREQUENCY_HZ = 1_000
|
DEFAULT_TEC_MODULATION_FREQUENCY_HZ = 1_000
|
||||||
DEFAULT_TEC_MODULATION_AMPLITUDE_CODE = 256
|
DEFAULT_TEC_MODULATION_AMPLITUDE_CODE = 256
|
||||||
|
DEFAULT_SWEEP_CURRENT_MIN_MA = 30.0
|
||||||
|
DEFAULT_SWEEP_CURRENT_MAX_MA = 40.0
|
||||||
|
DEFAULT_SWEEP_CURRENT_STEP_MA = 0.05
|
||||||
|
DEFAULT_SWEEP_DAC_STEP_CODE = 16
|
||||||
|
DEFAULT_SWEEP_STEP_PERIOD_US = 50
|
||||||
|
DEFAULT_SWEEP_PAUSE_MS = 5
|
||||||
|
|
||||||
DEFAULT_PI_P = 2560
|
DEFAULT_PI_P = 2560
|
||||||
DEFAULT_PI_I = 128
|
DEFAULT_PI_I = 128
|
||||||
|
|||||||
@@ -49,6 +49,9 @@ from .constants import (
|
|||||||
PROFILE_SAVE_SECTION_WAVEFORM_TEXT,
|
PROFILE_SAVE_SECTION_WAVEFORM_TEXT,
|
||||||
STM32_DAC_CODE_MAX,
|
STM32_DAC_CODE_MAX,
|
||||||
STM32_DAC_CODE_MIN,
|
STM32_DAC_CODE_MIN,
|
||||||
|
SWEEP_PAUSE_MAX_MS,
|
||||||
|
SWEEP_STEP_PERIOD_MAX_US,
|
||||||
|
SWEEP_STEP_PERIOD_MIN_US,
|
||||||
STATUS_RESPONSE_LENGTH,
|
STATUS_RESPONSE_LENGTH,
|
||||||
WAIT_AFTER_SEND_SEC,
|
WAIT_AFTER_SEND_SEC,
|
||||||
TEC_MODULATION_AMPLITUDE_CODE_MAX,
|
TEC_MODULATION_AMPLITUDE_CODE_MAX,
|
||||||
@@ -62,7 +65,14 @@ from .exceptions import (
|
|||||||
DeviceStateError,
|
DeviceStateError,
|
||||||
InvalidParameterError,
|
InvalidParameterError,
|
||||||
)
|
)
|
||||||
from .models import DeviceState, DeviceStatus, Measurements, ProfileSaveRequest
|
from .models import (
|
||||||
|
DeviceState,
|
||||||
|
DeviceStatus,
|
||||||
|
Measurements,
|
||||||
|
ProfileSaveRequest,
|
||||||
|
SweepSettings,
|
||||||
|
SweepTarget,
|
||||||
|
)
|
||||||
from .protocol import Protocol
|
from .protocol import Protocol
|
||||||
from .transport import SerialTransport
|
from .transport import SerialTransport
|
||||||
from .validators import ParameterValidator
|
from .validators import ParameterValidator
|
||||||
@@ -472,6 +482,23 @@ class LaserController:
|
|||||||
amplitude_code,
|
amplitude_code,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def configure_sweep(self, settings: SweepSettings) -> None:
|
||||||
|
"""Start (or stop, if disabled) a firmware staircase sweep of laser 2 current or the STM32 DAC."""
|
||||||
|
if settings.enabled:
|
||||||
|
code_max = STM32_DAC_CODE_MAX if settings.target == SweepTarget.STM32_DAC else 0xFFFF
|
||||||
|
self._validate_int_range(settings.min_code, "min_code", 0, code_max)
|
||||||
|
self._validate_int_range(settings.max_code, "max_code", settings.min_code, code_max)
|
||||||
|
self._validate_int_range(settings.step_code, "step_code", 1, code_max)
|
||||||
|
self._validate_int_range(
|
||||||
|
settings.step_period_us,
|
||||||
|
"step_period_us",
|
||||||
|
SWEEP_STEP_PERIOD_MIN_US,
|
||||||
|
SWEEP_STEP_PERIOD_MAX_US,
|
||||||
|
)
|
||||||
|
self._validate_int_range(settings.pause_ms, "pause_ms", 0, SWEEP_PAUSE_MAX_MS)
|
||||||
|
self._send_and_expect_ok(Protocol.encode_sweep_control(settings))
|
||||||
|
logger.info("Sweep configured: %s", settings)
|
||||||
|
|
||||||
def save_profile_to_sd(self, request: ProfileSaveRequest) -> None:
|
def save_profile_to_sd(self, request: ProfileSaveRequest) -> None:
|
||||||
"""Stream a rendered profile INI and optional waveform CSV to the device SD card."""
|
"""Stream a rendered profile INI and optional waveform CSV to the device SD card."""
|
||||||
if not isinstance(request, ProfileSaveRequest):
|
if not isinstance(request, ProfileSaveRequest):
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
|
||||||
from PyQt6.QtWidgets import (
|
from PyQt6.QtWidgets import (
|
||||||
QCheckBox,
|
QCheckBox,
|
||||||
QComboBox,
|
QComboBox,
|
||||||
@@ -52,6 +54,12 @@ from laser_control.constants import (
|
|||||||
DEFAULT_DS1809_PROFILE_POSITION,
|
DEFAULT_DS1809_PROFILE_POSITION,
|
||||||
DEFAULT_DS1809_PULSE_MS,
|
DEFAULT_DS1809_PULSE_MS,
|
||||||
DEFAULT_STM32_DAC_CODE,
|
DEFAULT_STM32_DAC_CODE,
|
||||||
|
DEFAULT_SWEEP_CURRENT_MAX_MA,
|
||||||
|
DEFAULT_SWEEP_CURRENT_MIN_MA,
|
||||||
|
DEFAULT_SWEEP_CURRENT_STEP_MA,
|
||||||
|
DEFAULT_SWEEP_DAC_STEP_CODE,
|
||||||
|
DEFAULT_SWEEP_PAUSE_MS,
|
||||||
|
DEFAULT_SWEEP_STEP_PERIOD_US,
|
||||||
DEFAULT_TEC_MODULATION_AMPLITUDE_CODE,
|
DEFAULT_TEC_MODULATION_AMPLITUDE_CODE,
|
||||||
DEFAULT_TEC_MODULATION_FREQUENCY_HZ,
|
DEFAULT_TEC_MODULATION_FREQUENCY_HZ,
|
||||||
DEFAULT_TEMP1_C,
|
DEFAULT_TEMP1_C,
|
||||||
@@ -67,6 +75,11 @@ from laser_control.constants import (
|
|||||||
AD9833_OUTPUT_FREQ_MIN_HZ,
|
AD9833_OUTPUT_FREQ_MIN_HZ,
|
||||||
STM32_DAC_CODE_MAX,
|
STM32_DAC_CODE_MAX,
|
||||||
STM32_DAC_CODE_MIN,
|
STM32_DAC_CODE_MIN,
|
||||||
|
SWEEP_CURRENT_STEP_MIN_MA,
|
||||||
|
SWEEP_PAUSE_MAX_MS,
|
||||||
|
SWEEP_STEP_PERIOD_MAX_US,
|
||||||
|
SWEEP_STEP_PERIOD_MIN_US,
|
||||||
|
SWEEP_TICK_US,
|
||||||
TEC_MODULATION_AMPLITUDE_CODE_MAX,
|
TEC_MODULATION_AMPLITUDE_CODE_MAX,
|
||||||
TEC_MODULATION_AMPLITUDE_CODE_MIN,
|
TEC_MODULATION_AMPLITUDE_CODE_MIN,
|
||||||
TEC_MODULATION_FREQUENCY_MAX_HZ,
|
TEC_MODULATION_FREQUENCY_MAX_HZ,
|
||||||
@@ -74,6 +87,8 @@ from laser_control.constants import (
|
|||||||
TEMP_MAX_C,
|
TEMP_MAX_C,
|
||||||
TEMP_MIN_C,
|
TEMP_MIN_C,
|
||||||
)
|
)
|
||||||
|
from laser_control.conversions import current_ma_to_n
|
||||||
|
from laser_control.models import SweepSettings, SweepTarget
|
||||||
|
|
||||||
|
|
||||||
def _double_spinbox(
|
def _double_spinbox(
|
||||||
@@ -112,6 +127,64 @@ def _expanding_button(label: str, *, primary: bool = False) -> QPushButton:
|
|||||||
return button
|
return button
|
||||||
|
|
||||||
|
|
||||||
|
class SweepControls(QGroupBox):
|
||||||
|
"""Checkbox-enabled firmware staircase sweep settings for one output."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
title: str,
|
||||||
|
target: SweepTarget,
|
||||||
|
*,
|
||||||
|
minimum: QSpinBox | QDoubleSpinBox,
|
||||||
|
maximum: QSpinBox | QDoubleSpinBox,
|
||||||
|
step: QSpinBox | QDoubleSpinBox,
|
||||||
|
to_code: Callable[[float], int],
|
||||||
|
on_apply: Callable[[SweepSettings], None],
|
||||||
|
) -> None:
|
||||||
|
super().__init__(title)
|
||||||
|
self._target = target
|
||||||
|
self._to_code = to_code
|
||||||
|
self._enable = QCheckBox("Включить свипирование")
|
||||||
|
self._minimum = minimum
|
||||||
|
self._maximum = maximum
|
||||||
|
self._step = step
|
||||||
|
self._step_period_us = _int_spinbox(
|
||||||
|
SWEEP_STEP_PERIOD_MIN_US,
|
||||||
|
SWEEP_STEP_PERIOD_MAX_US,
|
||||||
|
DEFAULT_SWEEP_STEP_PERIOD_US,
|
||||||
|
suffix=" мкс",
|
||||||
|
)
|
||||||
|
self._step_period_us.setSingleStep(SWEEP_TICK_US)
|
||||||
|
self._step_period_us.setToolTip(f"Разрешение {SWEEP_TICK_US} мкс, значение округляется вниз.")
|
||||||
|
self._pause_ms = _int_spinbox(0, SWEEP_PAUSE_MAX_MS, DEFAULT_SWEEP_PAUSE_MS, suffix=" мс")
|
||||||
|
self._pause_ms.setToolTip("Выход удерживается на нижней границе перед каждым новым свипом.")
|
||||||
|
self.apply_button = _expanding_button("Применить", primary=True)
|
||||||
|
self.apply_button.clicked.connect(lambda: on_apply(self.settings()))
|
||||||
|
|
||||||
|
layout = QFormLayout(self)
|
||||||
|
layout.setHorizontalSpacing(12)
|
||||||
|
layout.setVerticalSpacing(8)
|
||||||
|
layout.addRow(self._enable)
|
||||||
|
layout.addRow("Нижняя граница", minimum)
|
||||||
|
layout.addRow("Верхняя граница", maximum)
|
||||||
|
layout.addRow("Шаг", step)
|
||||||
|
layout.addRow("Время шага", self._step_period_us)
|
||||||
|
layout.addRow("Пауза между свипами", self._pause_ms)
|
||||||
|
layout.addRow(self.apply_button)
|
||||||
|
|
||||||
|
def settings(self) -> SweepSettings:
|
||||||
|
"""Return the current inputs converted to firmware codes."""
|
||||||
|
return SweepSettings(
|
||||||
|
target=self._target,
|
||||||
|
enabled=self._enable.isChecked(),
|
||||||
|
min_code=self._to_code(self._minimum.value()),
|
||||||
|
max_code=self._to_code(self._maximum.value()),
|
||||||
|
step_code=self._to_code(self._step.value()),
|
||||||
|
step_period_us=self._step_period_us.value(),
|
||||||
|
pause_ms=self._pause_ms.value(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def build_manual_group(owner) -> QGroupBox:
|
def build_manual_group(owner) -> QGroupBox:
|
||||||
"""Create manual control inputs."""
|
"""Create manual control inputs."""
|
||||||
group = QGroupBox("Ручной режим")
|
group = QGroupBox("Ручной режим")
|
||||||
@@ -159,6 +232,7 @@ def build_device_group(owner) -> QGroupBox:
|
|||||||
tabs.addTab(_build_ad9833_tab(owner), "Генератор AD9833")
|
tabs.addTab(_build_ad9833_tab(owner), "Генератор AD9833")
|
||||||
tabs.addTab(_build_aux_tab(owner), "Выходы и DS1809")
|
tabs.addTab(_build_aux_tab(owner), "Выходы и DS1809")
|
||||||
tabs.addTab(_build_tec_modulation_tab(owner), "TEC модуляция")
|
tabs.addTab(_build_tec_modulation_tab(owner), "TEC модуляция")
|
||||||
|
tabs.addTab(_build_sweep_tab(owner), "Свипирование")
|
||||||
tabs.addTab(_build_wave_tab(owner), "Своя форма")
|
tabs.addTab(_build_wave_tab(owner), "Своя форма")
|
||||||
layout.addWidget(tabs)
|
layout.addWidget(tabs)
|
||||||
return group
|
return group
|
||||||
@@ -479,7 +553,7 @@ def _build_tec_modulation_tab(owner) -> QWidget:
|
|||||||
TEC_MODULATION_AMPLITUDE_CODE_MAX,
|
TEC_MODULATION_AMPLITUDE_CODE_MAX,
|
||||||
DEFAULT_TEC_MODULATION_AMPLITUDE_CODE,
|
DEFAULT_TEC_MODULATION_AMPLITUDE_CODE,
|
||||||
)
|
)
|
||||||
owner._tec_mod_amplitude_code.setSingleStep(16)
|
owner._tec_mod_amplitude_code.setSingleStep(256)
|
||||||
owner._tec_mod_amplitude_code.setGroupSeparatorShown(True)
|
owner._tec_mod_amplitude_code.setGroupSeparatorShown(True)
|
||||||
owner._apply_tec_modulation_button = _expanding_button("Применить TEC модуляцию", primary=True)
|
owner._apply_tec_modulation_button = _expanding_button("Применить TEC модуляцию", primary=True)
|
||||||
owner._apply_tec_modulation_button.clicked.connect(owner._on_apply_tec_modulation)
|
owner._apply_tec_modulation_button.clicked.connect(owner._on_apply_tec_modulation)
|
||||||
@@ -504,6 +578,51 @@ def _build_tec_modulation_tab(owner) -> QWidget:
|
|||||||
return tab
|
return tab
|
||||||
|
|
||||||
|
|
||||||
|
def _build_sweep_tab(owner) -> QWidget:
|
||||||
|
tab = QWidget()
|
||||||
|
layout = QHBoxLayout(tab)
|
||||||
|
layout.setSpacing(10)
|
||||||
|
|
||||||
|
owner._laser2_sweep = SweepControls(
|
||||||
|
"Ток лазера 2",
|
||||||
|
SweepTarget.LASER2_CURRENT,
|
||||||
|
minimum=_double_spinbox(
|
||||||
|
CURRENT_MIN_MA, CURRENT_MAX_MA, DEFAULT_SWEEP_CURRENT_MIN_MA, decimals=3, step=0.05, suffix=" мА"
|
||||||
|
),
|
||||||
|
maximum=_double_spinbox(
|
||||||
|
CURRENT_MIN_MA, CURRENT_MAX_MA, DEFAULT_SWEEP_CURRENT_MAX_MA, decimals=3, step=0.05, suffix=" мА"
|
||||||
|
),
|
||||||
|
step=_double_spinbox(
|
||||||
|
SWEEP_CURRENT_STEP_MIN_MA,
|
||||||
|
CURRENT_MAX_MA - CURRENT_MIN_MA,
|
||||||
|
DEFAULT_SWEEP_CURRENT_STEP_MA,
|
||||||
|
decimals=3,
|
||||||
|
step=0.01,
|
||||||
|
suffix=" мА",
|
||||||
|
),
|
||||||
|
to_code=current_ma_to_n,
|
||||||
|
on_apply=owner._on_apply_sweep,
|
||||||
|
)
|
||||||
|
owner._laser2_sweep.setToolTip(
|
||||||
|
"Работает только в рабочем режиме (после «Применить» в ручном режиме); "
|
||||||
|
"после остановки возвращается ручной ток лазера 2."
|
||||||
|
)
|
||||||
|
owner._stm32_dac_sweep = SweepControls(
|
||||||
|
"STM32 DAC (PA4)",
|
||||||
|
SweepTarget.STM32_DAC,
|
||||||
|
minimum=_int_spinbox(STM32_DAC_CODE_MIN, STM32_DAC_CODE_MAX, STM32_DAC_CODE_MIN),
|
||||||
|
maximum=_int_spinbox(STM32_DAC_CODE_MIN, STM32_DAC_CODE_MAX, STM32_DAC_CODE_MAX),
|
||||||
|
step=_int_spinbox(1, STM32_DAC_CODE_MAX, DEFAULT_SWEEP_DAC_STEP_CODE),
|
||||||
|
to_code=int,
|
||||||
|
on_apply=owner._on_apply_sweep,
|
||||||
|
)
|
||||||
|
owner._stm32_dac_sweep.setToolTip("После остановки возвращается ручной уровень выхода STM32 DAC.")
|
||||||
|
|
||||||
|
layout.addWidget(owner._laser2_sweep)
|
||||||
|
layout.addWidget(owner._stm32_dac_sweep)
|
||||||
|
return tab
|
||||||
|
|
||||||
|
|
||||||
def _build_wave_tab(owner) -> QWidget:
|
def _build_wave_tab(owner) -> QWidget:
|
||||||
tab = QWidget()
|
tab = QWidget()
|
||||||
layout = QVBoxLayout(tab)
|
layout = QVBoxLayout(tab)
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ from laser_control.controller import (
|
|||||||
ad9102_sram_frequency_limits_hz,
|
ad9102_sram_frequency_limits_hz,
|
||||||
ad9102_sram_sample_count_from_frequency_hz,
|
ad9102_sram_sample_count_from_frequency_hz,
|
||||||
)
|
)
|
||||||
from laser_control.models import DeviceStatus, Measurements, ProfileSaveRequest
|
from laser_control.models import DeviceStatus, Measurements, ProfileSaveRequest, SweepSettings
|
||||||
|
|
||||||
from .dialogs import ProfileSaveDialog
|
from .dialogs import ProfileSaveDialog
|
||||||
from .sections import (
|
from .sections import (
|
||||||
@@ -68,6 +68,7 @@ class MainWindow(QMainWindow):
|
|||||||
request_pulse_ds1809 = pyqtSignal(bool, int, int)
|
request_pulse_ds1809 = pyqtSignal(bool, int, int)
|
||||||
request_set_stm32_dac = pyqtSignal(bool, int)
|
request_set_stm32_dac = pyqtSignal(bool, int)
|
||||||
request_apply_tec_modulation = pyqtSignal(bool, int, int, int)
|
request_apply_tec_modulation = pyqtSignal(bool, int, int, int)
|
||||||
|
request_apply_sweep = pyqtSignal(object)
|
||||||
request_upload_wave = pyqtSignal(object, int)
|
request_upload_wave = pyqtSignal(object, int)
|
||||||
request_cancel_wave = pyqtSignal()
|
request_cancel_wave = pyqtSignal()
|
||||||
request_save_profile = pyqtSignal(object)
|
request_save_profile = pyqtSignal(object)
|
||||||
@@ -222,6 +223,7 @@ class MainWindow(QMainWindow):
|
|||||||
self.request_pulse_ds1809.connect(self._worker.pulse_ds1809)
|
self.request_pulse_ds1809.connect(self._worker.pulse_ds1809)
|
||||||
self.request_set_stm32_dac.connect(self._worker.set_stm32_dac)
|
self.request_set_stm32_dac.connect(self._worker.set_stm32_dac)
|
||||||
self.request_apply_tec_modulation.connect(self._worker.apply_tec_modulation)
|
self.request_apply_tec_modulation.connect(self._worker.apply_tec_modulation)
|
||||||
|
self.request_apply_sweep.connect(self._worker.apply_sweep)
|
||||||
self.request_upload_wave.connect(self._worker.upload_ad9102_waveform)
|
self.request_upload_wave.connect(self._worker.upload_ad9102_waveform)
|
||||||
self.request_cancel_wave.connect(self._worker.cancel_ad9102_waveform_upload)
|
self.request_cancel_wave.connect(self._worker.cancel_ad9102_waveform_upload)
|
||||||
self.request_save_profile.connect(self._worker.save_profile)
|
self.request_save_profile.connect(self._worker.save_profile)
|
||||||
@@ -333,6 +335,9 @@ class MainWindow(QMainWindow):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _on_apply_sweep(self, settings: SweepSettings) -> None:
|
||||||
|
self._dispatch_command(lambda: self.request_apply_sweep.emit(settings))
|
||||||
|
|
||||||
def _on_save_profile(self) -> None:
|
def _on_save_profile(self) -> None:
|
||||||
dialog = ProfileSaveDialog(
|
dialog = ProfileSaveDialog(
|
||||||
custom_waveform_available=self._custom_waveform_is_available(),
|
custom_waveform_available=self._custom_waveform_is_available(),
|
||||||
@@ -443,6 +448,8 @@ class MainWindow(QMainWindow):
|
|||||||
self._apply_ad9833_button.setEnabled(connected)
|
self._apply_ad9833_button.setEnabled(connected)
|
||||||
self._apply_stm32_dac_button.setEnabled(connected)
|
self._apply_stm32_dac_button.setEnabled(connected)
|
||||||
self._apply_tec_modulation_button.setEnabled(connected)
|
self._apply_tec_modulation_button.setEnabled(connected)
|
||||||
|
self._laser2_sweep.apply_button.setEnabled(connected)
|
||||||
|
self._stm32_dac_sweep.apply_button.setEnabled(connected)
|
||||||
self._pulse_ds1809_button.setEnabled(connected)
|
self._pulse_ds1809_button.setEnabled(connected)
|
||||||
self._upload_wave_button.setEnabled(connected)
|
self._upload_wave_button.setEnabled(connected)
|
||||||
self._cancel_wave_button.setEnabled(connected)
|
self._cancel_wave_button.setEnabled(connected)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from laser_control import (
|
|||||||
DeviceNotRespondingError,
|
DeviceNotRespondingError,
|
||||||
LaserController,
|
LaserController,
|
||||||
)
|
)
|
||||||
|
from laser_control.models import SweepSettings, SweepTarget
|
||||||
|
|
||||||
|
|
||||||
class ControllerWorker(QObject):
|
class ControllerWorker(QObject):
|
||||||
@@ -117,6 +118,16 @@ class ControllerWorker(QObject):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@pyqtSlot(object)
|
||||||
|
def apply_sweep(self, settings: object) -> None:
|
||||||
|
"""Start or stop a firmware staircase sweep."""
|
||||||
|
self._run_command(
|
||||||
|
lambda: (
|
||||||
|
self._ensure_connected(),
|
||||||
|
self._apply_sweep_impl(settings),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
@pyqtSlot(object)
|
@pyqtSlot(object)
|
||||||
def save_profile(self, request: object) -> None:
|
def save_profile(self, request: object) -> None:
|
||||||
"""Save the current GUI configuration to the device SD card."""
|
"""Save the current GUI configuration to the device SD card."""
|
||||||
@@ -292,6 +303,19 @@ class ControllerWorker(QObject):
|
|||||||
)
|
)
|
||||||
self._emit_status()
|
self._emit_status()
|
||||||
|
|
||||||
|
def _apply_sweep_impl(self, settings: SweepSettings) -> None:
|
||||||
|
self._controller.configure_sweep(settings)
|
||||||
|
output = "STM32 DAC" if settings.target == SweepTarget.STM32_DAC else "Laser 2 current"
|
||||||
|
if settings.enabled:
|
||||||
|
self.log_message.emit(
|
||||||
|
"INFO",
|
||||||
|
f"{output} sweep started: codes {settings.min_code}..{settings.max_code} "
|
||||||
|
f"step {settings.step_code}, {settings.step_period_us} us/step, pause {settings.pause_ms} ms",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.log_message.emit("INFO", f"{output} sweep stopped")
|
||||||
|
self._emit_status()
|
||||||
|
|
||||||
def _save_profile_impl(self, request: object) -> None:
|
def _save_profile_impl(self, request: object) -> None:
|
||||||
self._controller.save_profile_to_sd(request)
|
self._controller.save_profile_to_sd(request)
|
||||||
profile_name = getattr(request, "profile_name", "<unnamed>")
|
profile_name = getattr(request, "profile_name", "<unnamed>")
|
||||||
|
|||||||
+21
-1
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from enum import IntFlag
|
from enum import IntEnum, IntFlag
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from .constants import (
|
from .constants import (
|
||||||
@@ -119,6 +119,26 @@ class DeviceStatus:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class SweepTarget(IntEnum):
|
||||||
|
"""Output driven by a firmware staircase sweep."""
|
||||||
|
|
||||||
|
LASER2_CURRENT = 0
|
||||||
|
STM32_DAC = 1
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class SweepSettings:
|
||||||
|
"""Firmware sweep: min..max in fixed steps, then a pause at min before repeating."""
|
||||||
|
|
||||||
|
target: SweepTarget
|
||||||
|
enabled: bool
|
||||||
|
min_code: int
|
||||||
|
max_code: int
|
||||||
|
step_code: int
|
||||||
|
step_period_us: int
|
||||||
|
pause_ms: int
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True)
|
@dataclass(slots=True)
|
||||||
class ProfileSaveRequest:
|
class ProfileSaveRequest:
|
||||||
"""Rendered profile payload that should be persisted on the device SD card."""
|
"""Rendered profile payload that should be persisted on the device SD card."""
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ from .constants import (
|
|||||||
CMD_DS1809_CONTROL,
|
CMD_DS1809_CONTROL,
|
||||||
CMD_STATE,
|
CMD_STATE,
|
||||||
CMD_STM32_DAC_CONTROL,
|
CMD_STM32_DAC_CONTROL,
|
||||||
|
CMD_SWEEP_CONTROL,
|
||||||
CMD_TEC_MODULATION_CONTROL,
|
CMD_TEC_MODULATION_CONTROL,
|
||||||
CMD_TRANS_ENABLE,
|
CMD_TRANS_ENABLE,
|
||||||
DEFAULT_SETUP_WORD,
|
DEFAULT_SETUP_WORD,
|
||||||
@@ -47,6 +48,9 @@ from .constants import (
|
|||||||
SEND_PARAMS_TOTAL_LENGTH,
|
SEND_PARAMS_TOTAL_LENGTH,
|
||||||
SHORT_CONTROL_TOTAL_LENGTH,
|
SHORT_CONTROL_TOTAL_LENGTH,
|
||||||
STM32_DAC_FLAG_ENABLE,
|
STM32_DAC_FLAG_ENABLE,
|
||||||
|
SWEEP_CONTROL_TOTAL_LENGTH,
|
||||||
|
SWEEP_FLAG_ENABLE,
|
||||||
|
SWEEP_FLAG_TARGET_STM32_DAC,
|
||||||
TEC_MODULATION_FLAG_CHANNEL_2,
|
TEC_MODULATION_FLAG_CHANNEL_2,
|
||||||
TEC_MODULATION_FLAG_ENABLE,
|
TEC_MODULATION_FLAG_ENABLE,
|
||||||
STATUS_DESCRIPTIONS,
|
STATUS_DESCRIPTIONS,
|
||||||
@@ -65,7 +69,7 @@ from .conversions import (
|
|||||||
voltage_7v_n_to_v,
|
voltage_7v_n_to_v,
|
||||||
)
|
)
|
||||||
from .exceptions import CRCError, ProtocolError
|
from .exceptions import CRCError, ProtocolError
|
||||||
from .models import DeviceState, Measurements
|
from .models import DeviceState, Measurements, SweepSettings, SweepTarget
|
||||||
|
|
||||||
|
|
||||||
def _int_to_hex4(value: int) -> str:
|
def _int_to_hex4(value: int) -> str:
|
||||||
@@ -293,6 +297,21 @@ class Protocol:
|
|||||||
_ensure_uint(amplitude_code, "amplitude_code", 0, 0xFFFF),
|
_ensure_uint(amplitude_code, "amplitude_code", 0, 0xFFFF),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def encode_sweep_control(settings: SweepSettings) -> bytes:
|
||||||
|
"""Build a laser-2-current or STM32-DAC staircase sweep control packet."""
|
||||||
|
flags = SWEEP_FLAG_ENABLE if settings.enabled else 0
|
||||||
|
if settings.target == SweepTarget.STM32_DAC:
|
||||||
|
flags |= SWEEP_FLAG_TARGET_STM32_DAC
|
||||||
|
payload = [flags] + [
|
||||||
|
_ensure_uint(getattr(settings, name), name, 0, 0xFFFF)
|
||||||
|
for name in ("min_code", "max_code", "step_code", "step_period_us", "pause_ms")
|
||||||
|
]
|
||||||
|
packet = _pack_words([CMD_SWEEP_CONTROL, *payload, _payload_checksum(payload)])
|
||||||
|
if len(packet) != SWEEP_CONTROL_TOTAL_LENGTH:
|
||||||
|
raise ProtocolError(f"Sweep control length mismatch: {len(packet)} bytes")
|
||||||
|
return packet
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def encode_ad9102_wave_begin(sample_count: int, pat_period_us: int = 0) -> bytes:
|
def encode_ad9102_wave_begin(sample_count: int, pat_period_us: int = 0) -> bytes:
|
||||||
"""Build an AD9102 custom-wave upload BEGIN packet.
|
"""Build an AD9102 custom-wave upload BEGIN packet.
|
||||||
|
|||||||
Reference in New Issue
Block a user