- обновлён простой режим: тест цвета и проверка подсветки кнопок объединены в раздел «Тест подсветки» - добавлена проверка подсветки кнопок через 8-й силовой выход с автоматическим коротким тестом - упрощены описания пользовательских проверок и убраны лишние технические подробности из простого режима - изменена раскладка теста цвета, увеличена длительность шага до 1 секунды и убраны описания из PRO-блока - добавлена защита выпадающих списков от случайного изменения колесом мыши - улучшены немодальные PRO-диалоги, настройки соединения и обработка событий COM-порта - обновлены визуальные детали приложения, пользовательские ресурсы, инструкции AGENTS и Release Notes 1.3.0
589 lines
26 KiB
Python
589 lines
26 KiB
Python
"""Встраиваемая секция «Проверка ввода» — цифровые/аналоговые входы.
|
||
|
||
Повторяет функциональность InputTestDialog, но как обычный QWidget.
|
||
Подписывается на единый поток чтения ComConnection при start_polling()
|
||
и отписывается при stop_polling() или скрытии виджета.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import os
|
||
import subprocess
|
||
import sys as _sys
|
||
|
||
from PySide6.QtCore import Qt, QTimer, QPointF
|
||
from PySide6.QtGui import QPainter, QPen, QBrush, QColor
|
||
from PySide6.QtWidgets import (
|
||
QGridLayout,
|
||
QGroupBox,
|
||
QHBoxLayout,
|
||
QLabel,
|
||
QPushButton,
|
||
QSizePolicy,
|
||
QSpinBox,
|
||
QVBoxLayout,
|
||
QWidget,
|
||
)
|
||
|
||
from ...services.com_service import ComConnection
|
||
from ...services.input_mappings_service import (
|
||
InputMapping,
|
||
_MAX_Y,
|
||
get_input_mappings_dir,
|
||
load_all_input_mappings,
|
||
)
|
||
from ...ui.icons import get_icon_color, get_icon_muted, icon
|
||
from ..no_wheel_combo_box import NoWheelComboBox
|
||
|
||
_NUM_DIGITAL = 26 # 24 входа + 2 хоппера
|
||
|
||
_JOY_SIZE = 120 # размер виджета джойстика
|
||
|
||
|
||
class _JoystickWidget(QWidget):
|
||
"""Визуальный круг джойстика — показывает позицию по двум осям (0‥4095).
|
||
|
||
Поддерживает калибровку: при включённом режиме калибровки запоминает
|
||
min/max по каждой оси; нормализация ведётся по откалиброванному диапазону.
|
||
"""
|
||
|
||
_ADC_MAX = 4095.0
|
||
|
||
def __init__(self, parent: QWidget | None = None) -> None:
|
||
super().__init__(parent)
|
||
self.setFixedSize(_JOY_SIZE, _JOY_SIZE)
|
||
self._x = 0.5 # нормализованные 0..1
|
||
self._y = 0.5
|
||
# Калибровка: min/max для каждой оси
|
||
self._calibrating = False
|
||
self._cal_x_min: int | None = None
|
||
self._cal_x_max: int | None = None
|
||
self._cal_y_min: int | None = None
|
||
self._cal_y_max: int | None = None
|
||
|
||
# ── Калибровка ────────────────────────────────────────────────────
|
||
|
||
def start_calibration(self) -> None:
|
||
"""Начать калибровку: сбросить min/max и начать запоминать."""
|
||
self._calibrating = True
|
||
self._cal_x_min = None
|
||
self._cal_x_max = None
|
||
self._cal_y_min = None
|
||
self._cal_y_max = None
|
||
|
||
def finish_calibration(self) -> None:
|
||
"""Завершить калибровку — min/max зафиксированы."""
|
||
self._calibrating = False
|
||
|
||
def reset_calibration(self) -> None:
|
||
"""Сбросить калибровку — вернуться к диапазону 0‥4095."""
|
||
self._calibrating = False
|
||
self._cal_x_min = None
|
||
self._cal_x_max = None
|
||
self._cal_y_min = None
|
||
self._cal_y_max = None
|
||
|
||
@property
|
||
def is_calibrated(self) -> bool:
|
||
return self._cal_x_min is not None and self._cal_x_max is not None
|
||
|
||
def calibration_text(self) -> str:
|
||
"""Текстовое представление калибровки."""
|
||
if not self.is_calibrated:
|
||
return "Нет"
|
||
return (
|
||
f"X: {self._cal_x_min}–{self._cal_x_max} "
|
||
f"Y: {self._cal_y_min}–{self._cal_y_max}"
|
||
)
|
||
|
||
# ── Данные ────────────────────────────────────────────────────────
|
||
|
||
def _normalize(self, raw: int, cal_min: int | None, cal_max: int | None) -> float:
|
||
"""Нормализовать значение в 0..1 с учётом калибровки."""
|
||
if cal_min is not None and cal_max is not None and cal_max > cal_min:
|
||
return max(0.0, min((raw - cal_min) / (cal_max - cal_min), 1.0))
|
||
return max(0.0, min(raw / self._ADC_MAX, 1.0))
|
||
|
||
def set_values(self, raw_x: int, raw_y: int) -> None:
|
||
"""Принять сырые ADC-значения (0‥4095) и перерисовать."""
|
||
# Обновляем калибровку
|
||
if self._calibrating:
|
||
if self._cal_x_min is None or raw_x < self._cal_x_min:
|
||
self._cal_x_min = raw_x
|
||
if self._cal_x_max is None or raw_x > self._cal_x_max:
|
||
self._cal_x_max = raw_x
|
||
if self._cal_y_min is None or raw_y < self._cal_y_min:
|
||
self._cal_y_min = raw_y
|
||
if self._cal_y_max is None or raw_y > self._cal_y_max:
|
||
self._cal_y_max = raw_y
|
||
|
||
self._x = self._normalize(raw_x, self._cal_x_min, self._cal_x_max)
|
||
self._y = self._normalize(raw_y, self._cal_y_min, self._cal_y_max)
|
||
self.update()
|
||
|
||
# ── Рисование ─────────────────────────────────────────────────────
|
||
|
||
def paintEvent(self, event) -> None: # noqa: N802
|
||
p = QPainter(self)
|
||
p.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||
w, h = self.width(), self.height()
|
||
cx, cy = w / 2.0, h / 2.0
|
||
radius = min(w, h) / 2.0 - 4
|
||
|
||
# Фон — круг
|
||
p.setPen(QPen(QColor("#555555"), 1.5))
|
||
p.setBrush(QBrush(QColor(40, 40, 40, 80)))
|
||
p.drawEllipse(QPointF(cx, cy), radius, radius)
|
||
|
||
# Крест
|
||
p.setPen(QPen(QColor("#444444"), 0.8))
|
||
p.drawLine(QPointF(cx - radius, cy), QPointF(cx + radius, cy))
|
||
p.drawLine(QPointF(cx, cy - radius), QPointF(cx, cy + radius))
|
||
|
||
# Точка — положение джойстика
|
||
px = cx + (self._x - 0.5) * 2.0 * radius
|
||
py = cy + (self._y - 0.5) * 2.0 * radius
|
||
dot_r = 6.0
|
||
p.setPen(Qt.PenStyle.NoPen)
|
||
p.setBrush(QBrush(QColor("#10b981")))
|
||
p.drawEllipse(QPointF(px, py), dot_r, dot_r)
|
||
|
||
p.end()
|
||
|
||
|
||
class InputTestSection(QWidget):
|
||
"""Inline-секция проверки ввода (для базового режима)."""
|
||
|
||
def __init__(self, connection: ComConnection, parent: QWidget | None = None) -> None:
|
||
super().__init__(parent)
|
||
self._conn = connection
|
||
self._closed = False
|
||
self._polling = False
|
||
|
||
self._mappings = load_all_input_mappings()
|
||
self._current_mapping: InputMapping | None = None
|
||
|
||
self._setup_ui()
|
||
self._apply_mapping()
|
||
|
||
# ── UI ────────────────────────────────────────────────────────────
|
||
|
||
def _setup_ui(self) -> None:
|
||
root = QVBoxLayout(self)
|
||
root.setContentsMargins(0, 0, 0, 0)
|
||
root.setSpacing(10)
|
||
|
||
desc = QLabel(
|
||
"Проверяет кнопки, датчики и джойстики. "
|
||
"Нажимайте элементы на панели и следите, чтобы индикаторы менялись сразу. "
|
||
"Для джойстиков проверьте плавное движение точки и при необходимости выполните калибровку."
|
||
)
|
||
desc.setWordWrap(True)
|
||
desc.setObjectName("HelpText")
|
||
root.addWidget(desc)
|
||
|
||
# ── Выбор маппинга ──
|
||
mapping_row = QHBoxLayout()
|
||
mapping_row.addWidget(QLabel("Раскладка:"))
|
||
self._mapping_combo = NoWheelComboBox()
|
||
self._mapping_combo.addItem("— Без раскладки —")
|
||
for m in self._mappings:
|
||
self._mapping_combo.addItem(m.project_name)
|
||
self._mapping_combo.currentIndexChanged.connect(self._on_mapping_changed)
|
||
mapping_row.addWidget(self._mapping_combo, 1)
|
||
|
||
self._reload_btn = QPushButton()
|
||
self._reload_btn.setIcon(icon("fa5s.sync-alt", color=get_icon_color()))
|
||
self._reload_btn.setToolTip("Перечитать файлы раскладок")
|
||
self._reload_btn.setFixedWidth(36)
|
||
self._reload_btn.clicked.connect(self._reload_mappings)
|
||
mapping_row.addWidget(self._reload_btn)
|
||
|
||
self._open_dir_btn = QPushButton()
|
||
self._open_dir_btn.setIcon(icon("fa5s.folder-open", color=get_icon_color()))
|
||
self._open_dir_btn.setToolTip("Открыть папку input_mappings")
|
||
self._open_dir_btn.setFixedWidth(36)
|
||
self._open_dir_btn.clicked.connect(self._open_mappings_dir)
|
||
mapping_row.addWidget(self._open_dir_btn)
|
||
|
||
root.addLayout(mapping_row)
|
||
|
||
# ── Холст кнопок (по координатам из маппинга) ──
|
||
self._canvas_group = QGroupBox("Кнопки")
|
||
self._canvas_grid = QGridLayout(self._canvas_group)
|
||
self._canvas_grid.setSpacing(4)
|
||
self._canvas_labels: dict[int, QLabel] = {}
|
||
root.addWidget(self._canvas_group)
|
||
|
||
# ── Цифровые входы (классическая сетка) ──
|
||
self._digital_group = QGroupBox("Цифровые входы (24 + 2 хоппера)")
|
||
digital_grid = QGridLayout(self._digital_group)
|
||
digital_grid.setSpacing(4)
|
||
|
||
self._digital_labels: list[QLabel] = []
|
||
for i in range(_NUM_DIGITAL):
|
||
lbl = QLabel(f"IN{i}")
|
||
lbl.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||
lbl.setFixedSize(52, 28)
|
||
lbl.setObjectName("InputIndicator")
|
||
lbl.setProperty("active", False)
|
||
self._digital_labels.append(lbl)
|
||
row, col = divmod(i, 8)
|
||
digital_grid.addWidget(lbl, row, col)
|
||
|
||
root.addWidget(self._digital_group)
|
||
|
||
# ── Аналоговые входы (джойстики) ──
|
||
analog_group = QGroupBox("Аналоговые входы — Джойстики")
|
||
analog_outer = QVBoxLayout(analog_group)
|
||
|
||
# Два круга — джойстик 1 и джойстик 2
|
||
joysticks_row = QHBoxLayout()
|
||
|
||
# Джойстик 1 (ADC0, ADC1)
|
||
joy1_frame = QGroupBox("Джойстик 1")
|
||
joy1_layout = QVBoxLayout(joy1_frame)
|
||
self._joy1_canvas = _JoystickWidget()
|
||
joy1_layout.addWidget(self._joy1_canvas, alignment=Qt.AlignmentFlag.AlignCenter)
|
||
self._joy1_label = QLabel("X: — Y: —")
|
||
self._joy1_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||
joy1_layout.addWidget(self._joy1_label)
|
||
self._joy1_cal_label = QLabel("Калибровка: Нет")
|
||
self._joy1_cal_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||
self._joy1_cal_label.setStyleSheet("color: #888; font-size: 10px;")
|
||
joy1_layout.addWidget(self._joy1_cal_label)
|
||
joy1_btn_row = QHBoxLayout()
|
||
self._joy1_cal_btn = QPushButton("Калибровать")
|
||
self._joy1_cal_btn.setIcon(icon("fa5s.crosshairs", color=get_icon_color()))
|
||
self._joy1_cal_btn.setToolTip("Покрутите джойстик по кругу, затем нажмите 'Готово'")
|
||
self._joy1_cal_btn.clicked.connect(lambda: self._toggle_calibration(1))
|
||
joy1_btn_row.addWidget(self._joy1_cal_btn)
|
||
self._joy1_reset_btn = QPushButton("Сброс")
|
||
self._joy1_reset_btn.setIcon(icon("fa5s.undo", color=get_icon_color()))
|
||
self._joy1_reset_btn.setToolTip("Сбросить калибровку джойстика 1")
|
||
self._joy1_reset_btn.clicked.connect(lambda: self._reset_calibration(1))
|
||
joy1_btn_row.addWidget(self._joy1_reset_btn)
|
||
joy1_layout.addLayout(joy1_btn_row)
|
||
joysticks_row.addWidget(joy1_frame)
|
||
|
||
# Джойстик 2 (ADC2, ADC3)
|
||
joy2_frame = QGroupBox("Джойстик 2")
|
||
joy2_layout = QVBoxLayout(joy2_frame)
|
||
self._joy2_canvas = _JoystickWidget()
|
||
joy2_layout.addWidget(self._joy2_canvas, alignment=Qt.AlignmentFlag.AlignCenter)
|
||
self._joy2_label = QLabel("X: — Y: —")
|
||
self._joy2_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||
joy2_layout.addWidget(self._joy2_label)
|
||
self._joy2_cal_label = QLabel("Калибровка: Нет")
|
||
self._joy2_cal_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||
self._joy2_cal_label.setStyleSheet("color: #888; font-size: 10px;")
|
||
joy2_layout.addWidget(self._joy2_cal_label)
|
||
joy2_btn_row = QHBoxLayout()
|
||
self._joy2_cal_btn = QPushButton("Калибровать")
|
||
self._joy2_cal_btn.setIcon(icon("fa5s.crosshairs", color=get_icon_color()))
|
||
self._joy2_cal_btn.setToolTip("Покрутите джойстик по кругу, затем нажмите 'Готово'")
|
||
self._joy2_cal_btn.clicked.connect(lambda: self._toggle_calibration(2))
|
||
joy2_btn_row.addWidget(self._joy2_cal_btn)
|
||
self._joy2_reset_btn = QPushButton("Сброс")
|
||
self._joy2_reset_btn.setIcon(icon("fa5s.undo", color=get_icon_color()))
|
||
self._joy2_reset_btn.setToolTip("Сбросить калибровку джойстика 2")
|
||
self._joy2_reset_btn.clicked.connect(lambda: self._reset_calibration(2))
|
||
joy2_btn_row.addWidget(self._joy2_reset_btn)
|
||
joy2_layout.addLayout(joy2_btn_row)
|
||
joysticks_row.addWidget(joy2_frame)
|
||
|
||
analog_outer.addLayout(joysticks_row)
|
||
|
||
# Управление опросом
|
||
adc_ctrl_row = QHBoxLayout()
|
||
adc_ctrl_row.addWidget(QLabel("Интервал (мс):"))
|
||
self._adc_interval_spin = QSpinBox()
|
||
self._adc_interval_spin.setRange(50, 5000)
|
||
self._adc_interval_spin.setValue(100)
|
||
self._adc_interval_spin.setSingleStep(50)
|
||
adc_ctrl_row.addWidget(self._adc_interval_spin)
|
||
|
||
self._adc_toggle_btn = QPushButton("Начать тест")
|
||
self._adc_toggle_btn.setIcon(icon("fa5s.play", color=get_icon_color()))
|
||
self._adc_toggle_btn.clicked.connect(self._toggle_adc_polling)
|
||
adc_ctrl_row.addWidget(self._adc_toggle_btn)
|
||
adc_ctrl_row.addStretch()
|
||
analog_outer.addLayout(adc_ctrl_row)
|
||
|
||
root.addWidget(analog_group)
|
||
self._adc_polling_active = False
|
||
|
||
# ── Лог событий ──
|
||
self._event_log = QLabel("")
|
||
self._event_log.setWordWrap(True)
|
||
self._event_log.setStyleSheet("color: #888; font-size: 11px;")
|
||
root.addWidget(self._event_log)
|
||
|
||
# ── Антидребезг ──
|
||
debounce_layout = QHBoxLayout()
|
||
debounce_layout.addWidget(QLabel("Антидребезг (мс):"))
|
||
self._debounce_spin = QSpinBox()
|
||
self._debounce_spin.setRange(10, 100)
|
||
self._debounce_spin.setValue(50)
|
||
debounce_layout.addWidget(self._debounce_spin)
|
||
self._set_debounce_btn = QPushButton("Применить")
|
||
self._set_debounce_btn.setIcon(icon("fa5s.check", color=get_icon_color()))
|
||
self._set_debounce_btn.clicked.connect(self._set_debounce)
|
||
debounce_layout.addWidget(self._set_debounce_btn)
|
||
debounce_layout.addStretch()
|
||
root.addLayout(debounce_layout)
|
||
|
||
# ── Public API ────────────────────────────────────────────────────
|
||
|
||
def start_polling(self) -> None:
|
||
"""Подписаться на события входов. Вызывать после подключения."""
|
||
if self._polling:
|
||
return
|
||
if not self._conn.is_connected:
|
||
return
|
||
self._closed = False
|
||
self._conn.input_received.connect(
|
||
self._on_digital, Qt.ConnectionType.QueuedConnection
|
||
)
|
||
self._conn.adc_received.connect(
|
||
self._on_analog, Qt.ConnectionType.QueuedConnection
|
||
)
|
||
self._conn.info_received.connect(
|
||
self._on_info_line, Qt.ConnectionType.QueuedConnection
|
||
)
|
||
self._polling = True
|
||
|
||
def stop_polling(self) -> None:
|
||
"""Отписаться от событий входов."""
|
||
if not self._polling:
|
||
return
|
||
# Если ADC автовывод был включён — выключить
|
||
if self._adc_polling_active:
|
||
self._adc_polling_active = False
|
||
self._adc_toggle_btn.setText("Начать тест")
|
||
self._adc_toggle_btn.setIcon(icon("fa5s.play", color=get_icon_color()))
|
||
self._adc_interval_spin.setEnabled(True)
|
||
self._send_raw("92")
|
||
self._closed = True
|
||
try:
|
||
self._conn.input_received.disconnect(self._on_digital)
|
||
except (RuntimeError, TypeError):
|
||
pass
|
||
try:
|
||
self._conn.adc_received.disconnect(self._on_analog)
|
||
except (RuntimeError, TypeError):
|
||
pass
|
||
try:
|
||
self._conn.info_received.disconnect(self._on_info_line)
|
||
except (RuntimeError, TypeError):
|
||
pass
|
||
self._polling = False
|
||
|
||
def refresh_icons(self) -> None:
|
||
clr = get_icon_color()
|
||
self._reload_btn.setIcon(icon("fa5s.sync-alt", color=clr))
|
||
self._open_dir_btn.setIcon(icon("fa5s.folder-open", color=clr))
|
||
if not self._adc_polling_active:
|
||
self._adc_toggle_btn.setIcon(icon("fa5s.play", color=clr))
|
||
else:
|
||
self._adc_toggle_btn.setIcon(icon("fa5s.stop", color=clr))
|
||
self._set_debounce_btn.setIcon(icon("fa5s.check", color=clr))
|
||
self._joy1_cal_btn.setIcon(icon("fa5s.crosshairs", color=clr))
|
||
self._joy1_reset_btn.setIcon(icon("fa5s.undo", color=clr))
|
||
self._joy2_cal_btn.setIcon(icon("fa5s.crosshairs", color=clr))
|
||
self._joy2_reset_btn.setIcon(icon("fa5s.undo", color=clr))
|
||
|
||
# ── Маппинги ──────────────────────────────────────────────────────
|
||
|
||
def _on_mapping_changed(self, index: int) -> None:
|
||
if index <= 0:
|
||
self._current_mapping = None
|
||
else:
|
||
self._current_mapping = self._mappings[index - 1]
|
||
self._apply_mapping()
|
||
|
||
def _apply_mapping(self) -> None:
|
||
for lbl in self._canvas_labels.values():
|
||
self._canvas_grid.removeWidget(lbl)
|
||
lbl.deleteLater()
|
||
self._canvas_labels.clear()
|
||
|
||
for c in range(self._canvas_grid.columnCount()):
|
||
self._canvas_grid.setColumnStretch(c, 0)
|
||
for r in range(self._canvas_grid.rowCount()):
|
||
self._canvas_grid.setRowStretch(r, 0)
|
||
|
||
mapping = self._current_mapping
|
||
if mapping is None or not mapping.buttons:
|
||
self._canvas_group.hide()
|
||
self._digital_group.show()
|
||
return
|
||
|
||
self._canvas_group.show()
|
||
self._canvas_group.setTitle(f"Кнопки — {mapping.project_name}")
|
||
self._digital_group.hide()
|
||
|
||
used_cols: set[int] = set()
|
||
used_rows: set[int] = set()
|
||
|
||
for btn in mapping.buttons:
|
||
lbl = QLabel(btn.name)
|
||
lbl.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||
lbl.setObjectName("InputIndicator")
|
||
lbl.setProperty("active", False)
|
||
lbl.setSizePolicy(
|
||
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred
|
||
)
|
||
lbl.setMinimumHeight(28)
|
||
grid_row = _MAX_Y - btn.y
|
||
self._canvas_grid.addWidget(lbl, grid_row, btn.x)
|
||
self._canvas_labels[btn.input_index] = lbl
|
||
used_cols.add(btn.x)
|
||
used_rows.add(grid_row)
|
||
|
||
for c in used_cols:
|
||
self._canvas_grid.setColumnStretch(c, 1)
|
||
for r in used_rows:
|
||
self._canvas_grid.setRowStretch(r, 1)
|
||
|
||
# ── Обновление индикаторов ────────────────────────────────────────
|
||
|
||
def _on_digital(self, val: int) -> None:
|
||
if self._closed:
|
||
return
|
||
for i, lbl in enumerate(self._digital_labels):
|
||
active = bool(val & (1 << i))
|
||
pressed = not active
|
||
if lbl.property("active") != pressed:
|
||
lbl.setProperty("active", pressed)
|
||
lbl.style().unpolish(lbl)
|
||
lbl.style().polish(lbl)
|
||
lbl.update()
|
||
for input_idx, lbl in self._canvas_labels.items():
|
||
if input_idx < _NUM_DIGITAL:
|
||
active = bool(val & (1 << input_idx))
|
||
pressed = not active
|
||
if lbl.property("active") != pressed:
|
||
lbl.setProperty("active", pressed)
|
||
lbl.style().unpolish(lbl)
|
||
lbl.style().polish(lbl)
|
||
lbl.update()
|
||
|
||
def _on_analog(self, values: list[str]) -> None:
|
||
if self._closed:
|
||
return
|
||
# values = ["v0", "v1", "v2", "v3"]
|
||
vals = [0, 0, 0, 0]
|
||
for i, v in enumerate(values[:4]):
|
||
try:
|
||
vals[i] = int(v)
|
||
except ValueError:
|
||
vals[i] = 0
|
||
# Джойстик 1: ADC0 (X), ADC1 (Y)
|
||
self._joy1_canvas.set_values(vals[0], vals[1])
|
||
self._joy1_label.setText(f"X: {vals[0]} Y: {vals[1]}")
|
||
if self._joy1_canvas._calibrating:
|
||
self._joy1_cal_label.setText("Калибровка: идёт… покрутите джойстик")
|
||
# Джойстик 2: ADC2 (X), ADC3 (Y)
|
||
self._joy2_canvas.set_values(vals[2], vals[3])
|
||
self._joy2_label.setText(f"X: {vals[2]} Y: {vals[3]}")
|
||
if self._joy2_canvas._calibrating:
|
||
self._joy2_cal_label.setText("Калибровка: идёт… покрутите джойстик")
|
||
|
||
# ── Прочее ────────────────────────────────────────────────────────
|
||
|
||
def _send_raw(self, cmd: str) -> None:
|
||
if not self._conn.is_connected or self._closed:
|
||
return
|
||
try:
|
||
self._conn.send_async(cmd)
|
||
except Exception:
|
||
pass
|
||
|
||
def _on_info_line(self, line: str) -> None:
|
||
if self._closed:
|
||
return
|
||
self._event_log.setText(f"← {line}")
|
||
|
||
def _toggle_adc_polling(self) -> None:
|
||
"""Начать или прекратить периодический опрос ADC."""
|
||
if not self._conn.is_connected or self._closed:
|
||
return
|
||
if self._adc_polling_active:
|
||
# Прекратить — отправить «92» (без параметра)
|
||
self._send_raw("92")
|
||
self._adc_polling_active = False
|
||
self._adc_toggle_btn.setText("Начать тест")
|
||
self._adc_toggle_btn.setIcon(icon("fa5s.play", color=get_icon_color()))
|
||
self._adc_interval_spin.setEnabled(True)
|
||
else:
|
||
# Начать — отправить «92<interval>»
|
||
interval = self._adc_interval_spin.value()
|
||
self._send_raw(f"92{interval}")
|
||
self._adc_polling_active = True
|
||
self._adc_toggle_btn.setText("Прекратить тест")
|
||
self._adc_toggle_btn.setIcon(icon("fa5s.stop", color=get_icon_color()))
|
||
self._adc_interval_spin.setEnabled(False)
|
||
|
||
# ── Калибровка джойстиков ─────────────────────────────────────────
|
||
|
||
def _toggle_calibration(self, joy: int) -> None:
|
||
"""Начать/завершить калибровку джойстика 1 или 2."""
|
||
canvas = self._joy1_canvas if joy == 1 else self._joy2_canvas
|
||
btn = self._joy1_cal_btn if joy == 1 else self._joy2_cal_btn
|
||
cal_label = self._joy1_cal_label if joy == 1 else self._joy2_cal_label
|
||
|
||
if canvas._calibrating:
|
||
# Завершить калибровку
|
||
canvas.finish_calibration()
|
||
btn.setText("Калибровать")
|
||
btn.setIcon(icon("fa5s.crosshairs", color=get_icon_color()))
|
||
cal_label.setText(f"Калибровка: {canvas.calibration_text()}")
|
||
else:
|
||
# Начать калибровку
|
||
canvas.start_calibration()
|
||
btn.setText("Готово")
|
||
btn.setIcon(icon("fa5s.check", color=get_icon_color()))
|
||
cal_label.setText("Калибровка: идёт… покрутите джойстик")
|
||
|
||
def _reset_calibration(self, joy: int) -> None:
|
||
"""Сбросить калибровку джойстика."""
|
||
canvas = self._joy1_canvas if joy == 1 else self._joy2_canvas
|
||
btn = self._joy1_cal_btn if joy == 1 else self._joy2_cal_btn
|
||
cal_label = self._joy1_cal_label if joy == 1 else self._joy2_cal_label
|
||
canvas.reset_calibration()
|
||
btn.setText("Калибровать")
|
||
btn.setIcon(icon("fa5s.crosshairs", color=get_icon_color()))
|
||
cal_label.setText("Калибровка: Нет")
|
||
|
||
def _set_debounce(self) -> None:
|
||
if not self._conn.is_connected or self._closed:
|
||
return
|
||
val = self._debounce_spin.value()
|
||
self._send_raw(f"91{val}")
|
||
|
||
def _reload_mappings(self) -> None:
|
||
current_name = self._mapping_combo.currentText()
|
||
self._mappings = load_all_input_mappings()
|
||
self._mapping_combo.blockSignals(True)
|
||
self._mapping_combo.clear()
|
||
self._mapping_combo.addItem("— Без раскладки —")
|
||
for m in self._mappings:
|
||
self._mapping_combo.addItem(m.project_name)
|
||
idx = self._mapping_combo.findText(current_name)
|
||
if idx >= 0:
|
||
self._mapping_combo.setCurrentIndex(idx)
|
||
self._mapping_combo.blockSignals(False)
|
||
self._on_mapping_changed(self._mapping_combo.currentIndex())
|
||
|
||
def _open_mappings_dir(self) -> None:
|
||
mappings_dir = get_input_mappings_dir()
|
||
if not mappings_dir.exists():
|
||
mappings_dir.mkdir(parents=True, exist_ok=True)
|
||
if _sys.platform == "win32":
|
||
os.startfile(str(mappings_dir)) # noqa: S606
|
||
elif _sys.platform == "darwin":
|
||
subprocess.Popen(["open", str(mappings_dir)]) # noqa: S603, S607
|
||
else:
|
||
subprocess.Popen(["xdg-open", str(mappings_dir)]) # noqa: S603, S607
|