Files
RadioPhotonic_PCB_PC_software/laser_control/gui/sections.py
2026-04-26 18:39:55 +03:00

564 lines
24 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Small UI builders used by the main laser-control window."""
from __future__ import annotations
from PyQt6.QtWidgets import (
QCheckBox,
QComboBox,
QDoubleSpinBox,
QFormLayout,
QGridLayout,
QGroupBox,
QHBoxLayout,
QLabel,
QPlainTextEdit,
QPushButton,
QSizePolicy,
QSpinBox,
QTabWidget,
QTextEdit,
QVBoxLayout,
QWidget,
)
from laser_control.constants import (
AD9102_PAT_BASE_MAX,
AD9102_PAT_BASE_MIN,
AD9102_PAT_PERIOD_MAX,
AD9102_PAT_PERIOD_MIN,
AD9102_SAW_STEP_MAX,
AD9102_SAW_STEP_MIN,
AD9102_SRAM_AMPLITUDE_MAX,
AD9102_SRAM_AMPLITUDE_MIN,
AD9102_SRAM_HOLD_MAX,
AD9102_SRAM_HOLD_MIN,
AD9102_SRAM_SAMPLE_MAX,
AD9102_SRAM_SAMPLE_MIN,
CURRENT_MAX_MA,
CURRENT_MIN_MA,
DEFAULT_AD9102_AMPLITUDE,
DEFAULT_AD9102_SAW_FREQUENCY_HZ,
DEFAULT_AD9102_SRAM_FREQUENCY_HZ,
DEFAULT_AD9102_HOLD_CYCLES,
DEFAULT_AD9102_PAT_BASE,
DEFAULT_AD9102_PAT_PERIOD,
DEFAULT_AD9102_SAMPLE_COUNT,
DEFAULT_AD9102_SAW_STEP,
DEFAULT_AD9833_FREQUENCY_HZ,
DEFAULT_CURRENT1_MA,
DEFAULT_CURRENT2_MA,
DEFAULT_DS1809_COUNT,
DEFAULT_DS1809_PROFILE_POSITION,
DEFAULT_DS1809_PULSE_MS,
DEFAULT_STM32_DAC_CODE,
DEFAULT_TEMP1_C,
DEFAULT_TEMP2_C,
DS1809_COUNT_MAX,
DS1809_COUNT_MIN,
DS1809_PROFILE_POSITION_MAX,
DS1809_PROFILE_POSITION_MIN,
DS1809_PULSE_MS_MAX,
DS1809_PULSE_MS_MIN,
AD9833_MCLK_HZ,
AD9833_OUTPUT_FREQ_MAX_HZ,
AD9833_OUTPUT_FREQ_MIN_HZ,
STM32_DAC_CODE_MAX,
STM32_DAC_CODE_MIN,
TEMP_MAX_C,
TEMP_MIN_C,
)
def _double_spinbox(
minimum: float,
maximum: float,
value: float,
*,
decimals: int = 2,
step: float = 0.1,
suffix: str = "",
) -> QDoubleSpinBox:
box = QDoubleSpinBox()
box.setRange(minimum, maximum)
box.setDecimals(decimals)
box.setSingleStep(step)
box.setValue(value)
if suffix:
box.setSuffix(suffix)
return box
def _int_spinbox(minimum: int, maximum: int, value: int, *, suffix: str = "") -> QSpinBox:
box = QSpinBox()
box.setRange(minimum, maximum)
box.setValue(value)
if suffix:
box.setSuffix(suffix)
return box
def _expanding_button(label: str, *, primary: bool = False) -> QPushButton:
button = QPushButton(label)
button.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
if primary:
button.setObjectName("primaryButton")
return button
def build_manual_group(owner) -> QGroupBox:
"""Create manual control inputs."""
group = QGroupBox("Ручной режим")
layout = QFormLayout(group)
layout.setHorizontalSpacing(12)
layout.setVerticalSpacing(8)
owner._manual_temp1 = _double_spinbox(TEMP_MIN_C, TEMP_MAX_C, DEFAULT_TEMP1_C, suffix=" °C")
owner._manual_temp2 = _double_spinbox(TEMP_MIN_C, TEMP_MAX_C, DEFAULT_TEMP2_C, suffix=" °C")
owner._manual_current1 = _double_spinbox(
CURRENT_MIN_MA,
CURRENT_MAX_MA,
DEFAULT_CURRENT1_MA,
decimals=3,
step=0.05,
suffix=" мА",
)
owner._manual_current2 = _double_spinbox(
CURRENT_MIN_MA,
CURRENT_MAX_MA,
DEFAULT_CURRENT2_MA,
decimals=3,
step=0.05,
suffix=" мА",
)
layout.addRow("Температура лазера 1", owner._manual_temp1)
layout.addRow("Температура лазера 2", owner._manual_temp2)
layout.addRow("Ток лазера 1", owner._manual_current1)
layout.addRow("Ток лазера 2", owner._manual_current2)
owner._apply_manual_button = _expanding_button("Применить", primary=True)
owner._apply_manual_button.clicked.connect(owner._on_apply_manual)
layout.addRow(owner._apply_manual_button)
return group
def build_device_group(owner) -> QGroupBox:
"""Create compact tabs for supported peripheral commands."""
group = QGroupBox("Периферия")
layout = QVBoxLayout(group)
tabs = QTabWidget(group)
tabs.addTab(_build_ad9102_tab(owner), "Генератор AD9102")
tabs.addTab(_build_ad9833_tab(owner), "Генератор AD9833")
tabs.addTab(_build_aux_tab(owner), "Выходы и DS1809")
tabs.addTab(_build_wave_tab(owner), "Своя форма")
layout.addWidget(tabs)
return group
def _build_ad9102_tab(owner) -> QWidget:
tab = QWidget()
layout = QVBoxLayout(tab)
layout.setSpacing(10)
note = QLabel(
"AD9102 в этой прошивке умеет два режима. "
"Первый: встроенная пила или треугольник самого AD9102. "
"Второй: STM32 сама строит пилу или треугольник из точек и записывает их в память AD9102. "
"Для упрощённого режима ниже задаются понятные величины, а регистровые поля спрятаны в расширенных настройках."
)
note.setWordWrap(True)
note.setObjectName("captionLabel")
basic_group = QGroupBox("Основные настройки")
basic_layout = QFormLayout(basic_group)
basic_layout.setHorizontalSpacing(12)
basic_layout.setVerticalSpacing(8)
owner._ad9102_enable = QCheckBox("Подать сигнал на выход")
owner._ad9102_enable.setChecked(True)
owner._ad9102_mode = QComboBox()
owner._ad9102_mode.addItem("Встроенная пила/треугольник AD9102", "saw")
owner._ad9102_mode.addItem("Пила/треугольник через память AD9102", "sram")
owner._ad9102_shape = QComboBox()
owner._ad9102_shape.addItem("Пила", "saw")
owner._ad9102_shape.addItem("Треугольник", "triangle")
owner._ad9102_shape.setCurrentIndex(1)
owner._ad9102_frequency_hz = _int_spinbox(
1,
DEFAULT_AD9102_SRAM_FREQUENCY_HZ,
DEFAULT_AD9102_SAW_FREQUENCY_HZ,
suffix=" Гц",
)
owner._ad9102_frequency_hz.setSingleStep(100)
owner._ad9102_frequency_hz.setGroupSeparatorShown(True)
owner._ad9102_basic_hint = QLabel()
owner._ad9102_basic_hint.setWordWrap(True)
owner._ad9102_basic_hint.setObjectName("captionLabel")
owner._ad9102_preview = QLabel("Реальная частота: —")
owner._ad9102_preview.setObjectName("valueLabel")
owner._ad9102_advanced_toggle = QCheckBox("Показать расширенные параметры AD9102")
owner._ad9102_saw_step = _int_spinbox(
AD9102_SAW_STEP_MIN,
AD9102_SAW_STEP_MAX,
DEFAULT_AD9102_SAW_STEP,
)
owner._ad9102_pat_base = _int_spinbox(
AD9102_PAT_BASE_MIN,
AD9102_PAT_BASE_MAX,
DEFAULT_AD9102_PAT_BASE,
)
owner._ad9102_pat_period = _int_spinbox(
AD9102_PAT_PERIOD_MIN,
AD9102_PAT_PERIOD_MAX,
DEFAULT_AD9102_PAT_PERIOD,
)
owner._ad9102_sample_count = _int_spinbox(
AD9102_SRAM_SAMPLE_MIN,
AD9102_SRAM_SAMPLE_MAX,
DEFAULT_AD9102_SAMPLE_COUNT,
)
owner._ad9102_hold_cycles = _int_spinbox(
AD9102_SRAM_HOLD_MIN,
AD9102_SRAM_HOLD_MAX,
DEFAULT_AD9102_HOLD_CYCLES,
)
owner._ad9102_amplitude = _int_spinbox(
AD9102_SRAM_AMPLITUDE_MIN,
AD9102_SRAM_AMPLITUDE_MAX,
DEFAULT_AD9102_AMPLITUDE,
)
owner._ad9102_use_amplitude = QCheckBox(
"Использовать формат \"размах + число точек\" вместо \"пауза + число точек\""
)
owner._ad9102_mode.currentIndexChanged.connect(owner._on_ad9102_mode_changed)
owner._ad9102_shape.currentIndexChanged.connect(owner._update_ad9102_form)
owner._ad9102_frequency_hz.valueChanged.connect(owner._update_ad9102_form)
owner._ad9102_advanced_toggle.toggled.connect(owner._update_ad9102_form)
owner._ad9102_use_amplitude.toggled.connect(owner._update_ad9102_form)
owner._ad9102_sample_count.valueChanged.connect(owner._update_ad9102_form)
owner._ad9102_hold_cycles.valueChanged.connect(owner._update_ad9102_form)
owner._ad9102_saw_step.valueChanged.connect(owner._update_ad9102_form)
owner._ad9102_amplitude.valueChanged.connect(owner._update_ad9102_form)
owner._ad9102_use_amplitude.setChecked(True)
basic_layout.addRow(owner._ad9102_enable)
basic_layout.addRow("Режим генерации", owner._ad9102_mode)
basic_layout.addRow("Форма сигнала", owner._ad9102_shape)
basic_layout.addRow("Частота сигнала", owner._ad9102_frequency_hz)
basic_layout.addRow("Размах сигнала в режиме памяти (0..8191)", owner._ad9102_amplitude)
basic_layout.addRow(owner._ad9102_preview)
basic_layout.addRow(owner._ad9102_basic_hint)
owner._ad9102_advanced_group = QGroupBox("Расширенные параметры")
advanced_layout = QFormLayout(owner._ad9102_advanced_group)
advanced_layout.setHorizontalSpacing(12)
advanced_layout.setVerticalSpacing(8)
advanced_layout.addRow("Скорость нарастания пилы (1..63)", owner._ad9102_saw_step)
advanced_layout.addRow("Масштаб периода (0..15)", owner._ad9102_pat_base)
advanced_layout.addRow("Длина периода (0..65535)", owner._ad9102_pat_period)
advanced_layout.addRow("Число точек формы (2..4096)", owner._ad9102_sample_count)
advanced_layout.addRow("Пауза на каждой точке (0..15)", owner._ad9102_hold_cycles)
advanced_layout.addRow(owner._ad9102_use_amplitude)
layout.addWidget(note)
layout.addWidget(basic_group)
layout.addWidget(owner._ad9102_advanced_toggle)
layout.addWidget(owner._ad9102_advanced_group)
owner._apply_ad9102_button = _expanding_button("Применить настройки генератора", primary=True)
owner._apply_ad9102_button.clicked.connect(owner._on_apply_ad9102)
layout.addWidget(owner._apply_ad9102_button)
layout.addStretch(1)
owner._ad9102_saw_step.setToolTip(
"Код шага нарастания пилообразного сигнала. Диапазон 1..63. "
"Чем больше значение, тем быстрее растёт сигнал внутри одного периода."
)
owner._ad9102_pat_base.setToolTip(
"Грубый масштаб периода повторения. Диапазон 0..15. "
"Используется вместе с длиной периода и задаёт базу времени генератора."
)
owner._ad9102_pat_period.setToolTip(
"Точная длина периода повторения. Диапазон 0..65535. "
"Совместно с масштабом периода определяет, как часто форма начинается заново."
)
owner._ad9102_sample_count.setToolTip(
"Количество отсчётов формы в памяти SRAM. Диапазон 2..4096. "
"Один период в режиме памяти состоит из этого числа точек."
)
owner._ad9102_hold_cycles.setToolTip(
"Количество внутренних циклов удержания одной точки формы. Диапазон 0..15. "
"Чем больше значение, тем дольше каждая точка удерживается перед переходом к следующей."
)
owner._ad9102_amplitude.setToolTip(
"Амплитудный коэффициент для режима, где STM32 сама строит пилу или треугольник "
"и записывает их в память AD9102. Диапазон 0..8191. Чем больше значение, тем больше размах сигнала."
)
owner._ad9102_frequency_hz.setToolTip(
"Желаемая частота сигнала в герцах. "
"Интерфейс автоматически подберёт ближайшие поддерживаемые параметры AD9102."
)
owner._ad9102_preview.setToolTip(
"Показывает, какая реальная частота получится после округления к поддерживаемым параметрам чипа."
)
owner._ad9102_use_amplitude.setToolTip(
"Ограничение текущей короткой STM-команды: в режиме памяти можно передать "
"либо \"размах + число точек\", либо \"пауза + число точек\"."
)
return tab
def _build_ad9833_tab(owner) -> QWidget:
tab = QWidget()
layout = QFormLayout(tab)
layout.setHorizontalSpacing(12)
layout.setVerticalSpacing(8)
owner._ad9833_enable = QCheckBox("Подать сигнал на выход")
owner._ad9833_enable.setChecked(True)
owner._ad9833_shape = QComboBox()
owner._ad9833_shape.addItem("Синус", "sine")
owner._ad9833_shape.addItem("Треугольник", "triangle")
owner._ad9833_shape.setCurrentIndex(1)
owner._ad9833_frequency_hz = _int_spinbox(
AD9833_OUTPUT_FREQ_MIN_HZ,
AD9833_OUTPUT_FREQ_MAX_HZ,
DEFAULT_AD9833_FREQUENCY_HZ,
suffix=" Гц",
)
owner._ad9833_frequency_hz.setSingleStep(1000)
owner._ad9833_frequency_hz.setGroupSeparatorShown(True)
owner._ad9833_frequency_hz.valueChanged.connect(owner._update_ad9833_preview)
owner._ad9833_word_preview = QLabel("Внутренний код: —")
owner._ad9833_word_preview.setObjectName("valueLabel")
note = QLabel(
f"AD9833 тактируется от {AD9833_MCLK_HZ:,} Гц. "
f"Поэтому здесь задаётся сразу частота сигнала в герцах. "
f"Рабочий диапазон интерфейса: {AD9833_OUTPUT_FREQ_MIN_HZ:,}..{AD9833_OUTPUT_FREQ_MAX_HZ:,} Гц "
f"(до половины тактовой частоты). Внутренний код рассчитывается автоматически."
)
note.setWordWrap(True)
note.setObjectName("captionLabel")
layout.addRow(owner._ad9833_enable)
layout.addRow("Форма сигнала", owner._ad9833_shape)
layout.addRow(
f"Частота сигнала ({AD9833_OUTPUT_FREQ_MIN_HZ:,}..{AD9833_OUTPUT_FREQ_MAX_HZ:,} Гц)",
owner._ad9833_frequency_hz,
)
layout.addRow("Внутренний код AD9833", owner._ad9833_word_preview)
layout.addRow(note)
owner._apply_ad9833_button = _expanding_button("Применить настройки генератора", primary=True)
owner._apply_ad9833_button.clicked.connect(owner._on_apply_ad9833)
layout.addRow(owner._apply_ad9833_button)
owner._ad9833_frequency_hz.setToolTip(
"Частота выходного сигнала в герцах. "
"Интерфейс ограничен диапазоном 0..10 МГц для тактовой частоты 20 МГц."
)
owner._ad9833_word_preview.setToolTip(
"28-битный frequency word, который будет автоматически передан в AD9833."
)
return tab
def _build_aux_tab(owner) -> QWidget:
tab = QWidget()
layout = QVBoxLayout(tab)
layout.setSpacing(10)
dac_group = QGroupBox("Аналоговый выход STM32 (PA4)")
dac_layout = QFormLayout(dac_group)
dac_layout.setHorizontalSpacing(12)
dac_layout.setVerticalSpacing(8)
dac_note = QLabel(
"Это встроенный 12-битный ЦАП микроконтроллера STM32. "
"Диапазон кода: 0..4095. Это соответствует примерно 0..Vref+, "
"то есть обычно около 0..3.3 В."
)
dac_note.setWordWrap(True)
dac_note.setObjectName("captionLabel")
owner._stm32_dac_enable = QCheckBox("Подать напряжение на выход")
owner._stm32_dac_enable.setChecked(True)
owner._stm32_dac_code = _int_spinbox(
STM32_DAC_CODE_MIN,
STM32_DAC_CODE_MAX,
DEFAULT_STM32_DAC_CODE,
)
owner._apply_stm32_dac_button = _expanding_button("Применить уровень выхода", primary=True)
owner._apply_stm32_dac_button.clicked.connect(owner._on_apply_stm32_dac)
dac_layout.addRow(dac_note)
dac_layout.addRow(owner._stm32_dac_enable)
dac_layout.addRow("Уровень выхода (0..4095)", owner._stm32_dac_code)
dac_layout.addRow(owner._apply_stm32_dac_button)
owner._stm32_dac_code.setToolTip(
"Код встроенного 12-битного ЦАП STM32. "
"0 = примерно 0 В, 4095 = примерно верхний предел питания ЦАП "
"(обычно около 3.3 В)."
)
ds_group = QGroupBox("Цифровой подстроечный резистор DS1809")
ds_layout = QFormLayout(ds_group)
ds_layout.setHorizontalSpacing(12)
ds_layout.setVerticalSpacing(8)
owner._ds1809_direction = QComboBox()
owner._ds1809_direction.addItem("Увеличить", "inc")
owner._ds1809_direction.addItem("Уменьшить", "dec")
owner._ds1809_count = _int_spinbox(DS1809_COUNT_MIN, DS1809_COUNT_MAX, DEFAULT_DS1809_COUNT)
owner._ds1809_pulse_ms = _int_spinbox(
DS1809_PULSE_MS_MIN,
DS1809_PULSE_MS_MAX,
DEFAULT_DS1809_PULSE_MS,
suffix=" мс",
)
owner._pulse_ds1809_button = _expanding_button("Сделать шаги резистора", primary=True)
owner._pulse_ds1809_button.clicked.connect(owner._on_pulse_ds1809)
owner._ds1809_profile_apply = QCheckBox("Сохранять абсолютную позицию в профиль")
owner._ds1809_profile_apply.setChecked(True)
owner._ds1809_profile_position = _int_spinbox(
DS1809_PROFILE_POSITION_MIN,
DS1809_PROFILE_POSITION_MAX,
DEFAULT_DS1809_PROFILE_POSITION,
)
ds_layout.addRow("Куда менять", owner._ds1809_direction)
ds_layout.addRow("Число шагов", owner._ds1809_count)
ds_layout.addRow("Длительность шага", owner._ds1809_pulse_ms)
ds_layout.addRow(owner._ds1809_profile_apply)
ds_layout.addRow("Позиция для профиля (от минимума)", owner._ds1809_profile_position)
ds_layout.addRow(owner._pulse_ds1809_button)
owner._ds1809_count.setToolTip("На сколько шагов изменить цифровой резистор.")
owner._ds1809_pulse_ms.setToolTip("Сколько миллисекунд длится один управляющий импульс.")
owner._ds1809_profile_apply.setToolTip(
"Если флажок включён, при сохранении профиля прошивка сначала загонит DS1809 в минимум, "
"а затем поднимет его до указанной позиции."
)
owner._ds1809_profile_position.setToolTip(
"Абсолютная позиция DS1809 относительно минимального положения. "
"Используется только при сохранении профиля на SD."
)
layout.addWidget(dac_group)
layout.addWidget(ds_group)
layout.addStretch(1)
return tab
def _build_wave_tab(owner) -> QWidget:
tab = QWidget()
layout = QVBoxLayout(tab)
layout.setSpacing(10)
note = QLabel(
"Здесь можно вручную загрузить свою форму сигнала для AD9102. "
"Каждое число - это одна точка формы. Допустимый диапазон: от -8192 до 8191."
)
note.setWordWrap(True)
note.setObjectName("captionLabel")
layout.addWidget(note)
owner._wave_info_label = QLabel("Отсчётов: 0")
owner._wave_info_label.setObjectName("valueLabel")
layout.addWidget(owner._wave_info_label)
owner._wave_samples_box = QPlainTextEdit()
owner._wave_samples_box.setPlaceholderText("0 1024 2048 1024 0 -1024 -2048 -1024")
owner._wave_samples_box.setMinimumHeight(180)
owner._wave_samples_box.textChanged.connect(owner._on_wave_text_changed)
layout.addWidget(owner._wave_samples_box)
buttons = QWidget()
buttons_layout = QHBoxLayout(buttons)
buttons_layout.setContentsMargins(0, 0, 0, 0)
buttons_layout.setSpacing(8)
owner._load_wave_file_button = _expanding_button("Открыть файл")
owner._upload_wave_button = _expanding_button("Загрузить форму", primary=True)
owner._cancel_wave_button = _expanding_button("Отменить загрузку")
owner._load_wave_file_button.clicked.connect(owner._on_load_wave_file)
owner._upload_wave_button.clicked.connect(owner._on_upload_waveform)
owner._cancel_wave_button.clicked.connect(owner._on_cancel_waveform)
buttons_layout.addWidget(owner._load_wave_file_button)
buttons_layout.addWidget(owner._upload_wave_button)
buttons_layout.addWidget(owner._cancel_wave_button)
layout.addWidget(buttons)
return tab
def build_status_group(owner) -> QGroupBox:
"""Create status and telemetry labels."""
group = QGroupBox("Телеметрия и статус")
layout = QVBoxLayout(group)
layout.setSpacing(10)
owner._status_header = QLabel("Отключено")
owner._status_header.setObjectName("statusError")
layout.addWidget(owner._status_header)
grid = QGridLayout()
grid.setHorizontalSpacing(12)
grid.setVerticalSpacing(6)
rows = [
("Порт", "_port_value"),
("Статус", "_state_value"),
("Доп. код", "_detail_value"),
("ID сообщения", "_message_id_value"),
("Температура 1", "_telemetry_temp1"),
("Температура 2", "_telemetry_temp2"),
("Фотодиод 1", "_telemetry_current1"),
("Фотодиод 2", "_telemetry_current2"),
("Внешняя температура 1", "_telemetry_temp_ext1"),
("Внешняя температура 2", "_telemetry_temp_ext2"),
("Питание 3.3 В", "_telemetry_3v3"),
("Питание 5V1", "_telemetry_5v1"),
("Питание 5V2", "_telemetry_5v2"),
("Питание 7V0", "_telemetry_7v0"),
]
for row_index, (label_text, attr_name) in enumerate(rows):
label = QLabel(label_text)
label.setObjectName("captionLabel")
value = QLabel("")
value.setObjectName("valueLabel")
setattr(owner, attr_name, value)
grid.addWidget(label, row_index, 0)
grid.addWidget(value, row_index, 1)
layout.addLayout(grid)
buttons = QWidget()
buttons_layout = QHBoxLayout(buttons)
buttons_layout.setContentsMargins(0, 0, 0, 0)
buttons_layout.setSpacing(8)
owner._reconnect_button = _expanding_button("Переподключить")
owner._reset_button = _expanding_button("Сброс")
owner._save_profile_button = _expanding_button("Сохранить профиль", primary=True)
owner._reconnect_button.clicked.connect(owner._on_reconnect)
owner._reset_button.clicked.connect(owner._on_reset_device)
owner._save_profile_button.clicked.connect(owner._on_save_profile)
buttons_layout.addWidget(owner._reconnect_button)
buttons_layout.addWidget(owner._reset_button)
layout.addWidget(buttons)
layout.addWidget(owner._save_profile_button)
return group
def build_log_group(owner) -> QGroupBox:
"""Create compact runtime log output."""
group = QGroupBox("Runtime Log")
layout = QVBoxLayout(group)
owner._log_box = QTextEdit()
owner._log_box.setObjectName("logBox")
owner._log_box.setReadOnly(True)
owner._log_box.setMinimumHeight(180)
layout.addWidget(owner._log_box)
return group