sweep feature added

This commit is contained in:
Ayzen
2026-10-09 19:45:30 +03:00
parent 62e0430668
commit a1386e2588
8 changed files with 241 additions and 6 deletions
+1
View File
@@ -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-карту устройства
+19 -1
View File
@@ -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
+28 -1
View File
@@ -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):
+120 -1
View File
@@ -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)
+8 -1
View File
@@ -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)
+24
View File
@@ -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
View File
@@ -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."""
+20 -1
View File
@@ -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.