diff --git a/app/widgets/connection_tab.py b/app/widgets/connection_tab.py
index 029ccfe..c355eec 100644
--- a/app/widgets/connection_tab.py
+++ b/app/widgets/connection_tab.py
@@ -269,6 +269,7 @@ class ConnectionTab(QWidget):
self._button_backlight_section = ButtonBacklightSection(self._connection)
self._input_section = InputTestSection(self._connection)
self._payment_section = PaymentSection(
+ connection=self._connection,
send_raw=lambda cmd: self._input_section._send_raw(cmd),
)
@@ -310,6 +311,7 @@ class ConnectionTab(QWidget):
("Тест силовых выходов", "fa5s.bolt", self._open_output_test),
("Обновление прошивки", "fa5s.upload", self._open_firmware),
("Задание роли", "fa5s.user-tag", self._open_role),
+ ("Витрина призов", "fa5s.gift", self._open_prize_showcase),
)
self._pro_buttons: list[tuple[QPushButton, str]] = []
for title, icon_name, handler in pro_items:
@@ -605,6 +607,13 @@ class ConnectionTab(QWidget):
lambda: RoleDialog(self._connection, parent=self),
)
+ def _open_prize_showcase(self) -> None:
+ from .dialogs.prize_showcase_dialog import PrizeShowcaseDialog
+ self._exec_dialog(
+ "prize_showcase",
+ lambda: PrizeShowcaseDialog(self._connection, parent=self),
+ )
+
def _open_terminal(self) -> None:
from .dialogs.command_terminal_dialog import CommandTerminalDialog
self._exec_dialog(
diff --git a/app/widgets/dialogs/led_test_dialog.py b/app/widgets/dialogs/led_test_dialog.py
index a8c5016..09ffbd0 100644
--- a/app/widgets/dialogs/led_test_dialog.py
+++ b/app/widgets/dialogs/led_test_dialog.py
@@ -16,6 +16,7 @@ import sys
from PySide6.QtCore import Qt, QTimer
from PySide6.QtWidgets import (
QCheckBox,
+ QColorDialog,
QFormLayout,
QGroupBox,
QHBoxLayout,
@@ -198,6 +199,12 @@ class LedTestDialog(StyledDialog):
gamma_layout.addWidget(apply_gamma_btn)
color_form.addRow("Гамма-коррекция:", gamma_layout)
+ # Пользовательский цвет
+ self._custom_color_btn = QPushButton("Выбрать и отправить цвет")
+ self._custom_color_btn.setIcon(icon("fa5s.palette", color=get_icon_color()))
+ self._custom_color_btn.clicked.connect(self._send_custom_color)
+ color_form.addRow("Свой цвет (08):", self._custom_color_btn)
+
# Тест цвета
self._color_test_btn = QPushButton("Тест цвета")
self._color_test_btn.setProperty("role", "primary")
@@ -212,6 +219,159 @@ class LedTestDialog(StyledDialog):
root.addWidget(color_group)
+ # ── Бродяга морей (cmd 81) ──
+ wanderer_group = QGroupBox("Бродяга морей (cmd 81)")
+ wanderer_form = QFormLayout(wanderer_group)
+ wanderer_form.setSpacing(6)
+
+ # Объект 1
+ self._w1_level = QSpinBox()
+ self._w1_level.setRange(0, 21)
+ self._w1_level.setValue(5)
+
+ w1_apply_level = QPushButton("Задать уровень")
+ w1_apply_level.setIcon(icon("fa5s.angle-double-right", color=get_icon_color()))
+ w1_apply_level.clicked.connect(self._send_w1_level)
+
+ w1_level_layout = QHBoxLayout()
+ w1_level_layout.addWidget(self._w1_level, 1)
+ w1_level_layout.addWidget(w1_apply_level)
+ wanderer_form.addRow("Шкала 1 (уровень 0..21):", w1_level_layout)
+
+ self._w1_effect = NoWheelComboBox()
+ self._w1_effect.addItem("Эффект 1 (синий/фиолетовый)", 1)
+ self._w1_effect.addItem("Эффект 2 (фиолетовый/голубой)", 2)
+ self._w1_effect.addItem("Эффект 3 (мигание белым)", 3)
+
+ w1_apply_eff = QPushButton("Запустить эффект")
+ w1_apply_eff.setProperty("role", "primary")
+ w1_apply_eff.setIcon(icon("fa5s.play", color="#ecfdf5"))
+ w1_apply_eff.clicked.connect(self._send_w1_effect)
+
+ w1_eff_layout = QHBoxLayout()
+ w1_eff_layout.addWidget(self._w1_effect, 1)
+ w1_eff_layout.addWidget(w1_apply_eff)
+ wanderer_form.addRow("Эффекты шкалы 1:", w1_eff_layout)
+
+ # Объект 2
+ self._w2_level = QSpinBox()
+ self._w2_level.setRange(0, 44)
+ self._w2_level.setValue(5)
+
+ w2_apply_level = QPushButton("Задать уровень")
+ w2_apply_level.setIcon(icon("fa5s.angle-double-right", color=get_icon_color()))
+ w2_apply_level.clicked.connect(self._send_w2_level)
+
+ w2_level_layout = QHBoxLayout()
+ w2_level_layout.addWidget(self._w2_level, 1)
+ w2_level_layout.addWidget(w2_apply_level)
+ wanderer_form.addRow("Шкала 2 (уровень 0..44):", w2_level_layout)
+
+ self._w2_effect = NoWheelComboBox()
+ self._w2_effect.addItem("Эффект 1 (синий/фиолетовый)", 1)
+ self._w2_effect.addItem("Эффект 2 (фиолетовый/голубой)", 2)
+ self._w2_effect.addItem("Эффект 3 (мигание белым)", 3)
+
+ w2_apply_eff = QPushButton("Запустить эффект")
+ w2_apply_eff.setProperty("role", "primary")
+ w2_apply_eff.setIcon(icon("fa5s.play", color="#ecfdf5"))
+ w2_apply_eff.clicked.connect(self._send_w2_effect)
+
+ w2_eff_layout = QHBoxLayout()
+ w2_eff_layout.addWidget(self._w2_effect, 1)
+ w2_eff_layout.addWidget(w2_apply_eff)
+ wanderer_form.addRow("Эффекты шкалы 2:", w2_eff_layout)
+
+ # Сброс обоих
+ w_clear_btn = QPushButton("Очистить обе шкалы (8130)")
+ w_clear_btn.setIcon(icon("fa5s.eraser", color=get_icon_color()))
+ w_clear_btn.clicked.connect(self._clear_wanderer)
+ wanderer_form.addRow(w_clear_btn)
+
+ root.addWidget(wanderer_group)
+
+ # ── Управление виртуальной матрицей (cmd 85/86/87) ──
+ matrix_group = QGroupBox("Виртуальная матрица (cmd 85/86/87)")
+ matrix_form = QFormLayout(matrix_group)
+ matrix_form.setSpacing(6)
+
+ # Автокарусель / Рендеринг
+ carousel_layout = QHBoxLayout()
+
+ self._mc_on_btn = QPushButton("Карусель Вкл")
+ self._mc_on_btn.setIcon(icon("fa5s.play-circle", color=get_icon_color()))
+ self._mc_on_btn.clicked.connect(self._send_carousel_on)
+ carousel_layout.addWidget(self._mc_on_btn)
+
+ self._mc_off_btn = QPushButton("Карусель Выкл")
+ self._mc_off_btn.setIcon(icon("fa5s.stop-circle", color=get_icon_color()))
+ self._mc_off_btn.clicked.connect(self._send_carousel_off)
+ carousel_layout.addWidget(self._mc_off_btn)
+
+ self._mc_stop_btn = QPushButton("Rendering OFF")
+ self._mc_stop_btn.setProperty("role", "primary")
+ self._mc_stop_btn.setIcon(icon("fa5s.power-off", color="#ecfdf5"))
+ self._mc_stop_btn.clicked.connect(self._send_rendering_off)
+ carousel_layout.addWidget(self._mc_stop_btn)
+
+ matrix_form.addRow("Режимы рендера:", carousel_layout)
+
+ # Тесты матриц (8525 / 8625 / 8725)
+ test_type_layout = QHBoxLayout()
+ self._m_test_type = NoWheelComboBox()
+ self._m_test_type.addItem("Выключить тест (0)", 0)
+ self._m_test_type.addItem("Общий тест / Змейка (1)", 1)
+ self._m_test_type.addItem("Тест строки (2)", 2)
+ self._m_test_type.addItem("Тест столбца (3)", 3)
+ self._m_test_type.addItem("Тест пикселя (4)", 4)
+ self._m_test_type.currentIndexChanged.connect(self._on_matrix_test_type_changed)
+ test_type_layout.addWidget(self._m_test_type, 1)
+
+ self._m_test_param = QSpinBox()
+ self._m_test_param.setRange(0, 999)
+ self._m_test_param.setValue(0)
+ self._m_test_param.setVisible(False)
+ test_type_layout.addWidget(self._m_test_param)
+
+ m_test_btn = QPushButton("Запустить")
+ m_test_btn.setIcon(icon("fa5s.vial", color=get_icon_color()))
+ m_test_btn.clicked.connect(self._send_matrix_test)
+ test_type_layout.addWidget(m_test_btn)
+
+ matrix_form.addRow("Аппаратный тест:", test_type_layout)
+
+ # Direct-шкалы (только для s_b)
+ self._scales_container = QWidget()
+ scales_layout = QVBoxLayout(self._scales_container)
+ scales_layout.setContentsMargins(0, 0, 0, 0)
+ scales_layout.setSpacing(4)
+
+ scales_title = QLabel("Direct-шкалы (только для Морского боя s_b)")
+ scales_layout.addWidget(scales_title)
+
+ self._m_scales: list[QSpinBox] = []
+ for i in range(4):
+ scale_row = QHBoxLayout()
+ spin = QSpinBox()
+ spin.setRange(0, 22)
+ spin.setValue(0)
+ self._m_scales.append(spin)
+ scale_row.addWidget(spin, 1)
+
+ btn = QPushButton(f"Задать шкалу {i+1}")
+ btn.setIcon(icon("fa5s.level-up-alt", color=get_icon_color()))
+ btn.clicked.connect(lambda checked=False, idx=i: self._send_matrix_scale(idx))
+ scale_row.addWidget(btn)
+ scales_layout.addLayout(scale_row)
+
+ clear_scales_btn = QPushButton("Очистить direct-шкалы (8530)")
+ clear_scales_btn.setIcon(icon("fa5s.trash-alt", color=get_icon_color()))
+ clear_scales_btn.clicked.connect(self._clear_matrix_scales)
+ scales_layout.addWidget(clear_scales_btn)
+
+ matrix_form.addRow(self._scales_container)
+ root.addWidget(matrix_group)
+
# ── Лог ──
self._log = QLabel("")
self._log.setObjectName("LogLabel")
@@ -257,6 +417,10 @@ class LedTestDialog(StyledDialog):
# Кнопка «выключить» — только для has_duration
self._stop_btn.setVisible(mode.has_duration)
+ # Показывать direct-шкалы только для Морского боя s_b (где base_command == "85")
+ is_sb = (mode.base_command == "85")
+ self._scales_container.setVisible(is_sb)
+
def _reload_modes(self) -> None:
"""Перечитать .lm файлы из папки."""
current_name = self._project_combo.currentText()
@@ -338,6 +502,16 @@ class LedTestDialog(StyledDialog):
cmd = f"89{ch}2{val}"
self._send(cmd)
+ def _send_custom_color(self) -> None:
+ color = QColorDialog.getColor(Qt.GlobalColor.white, self, "Выберите цвет")
+ if color.isValid():
+ ch = self._cmd89_channel_combo.currentData()
+ r = color.red()
+ g = color.green()
+ b = color.blue()
+ cmd = f"8{ch}08{r:03d}{g:03d}{b:03d}"
+ self._send(cmd)
+
def _run_color_test(self) -> None:
"""Последовательно отправить 8003→8004→8005→8002→8000 с паузой 300 мс."""
self._color_test_cmds = ["8003", "8004", "8005", "8002", "8000"]
@@ -359,6 +533,71 @@ class LedTestDialog(StyledDialog):
self._color_test_idx += 1
self._send(cmd)
+ def _send_w1_level(self) -> None:
+ val = self._w1_level.value()
+ cmd = f"8131{val:02d}"
+ self._send(cmd)
+
+ def _send_w1_effect(self) -> None:
+ val = self._w1_effect.currentData()
+ cmd = f"8131{val}"
+ self._send(cmd)
+
+ def _send_w2_level(self) -> None:
+ val = self._w2_level.value()
+ cmd = f"8132{val:02d}"
+ self._send(cmd)
+
+ def _send_w2_effect(self) -> None:
+ val = self._w2_effect.currentData()
+ cmd = f"8132{val}"
+ self._send(cmd)
+
+ def _clear_wanderer(self) -> None:
+ self._send("8130")
+
+ def _send_carousel_on(self) -> None:
+ mode = self._current_mode()
+ if mode:
+ self._send(f"{mode.base_command}01")
+
+ def _send_carousel_off(self) -> None:
+ mode = self._current_mode()
+ if mode:
+ self._send(f"{mode.base_command}00")
+
+ def _send_rendering_off(self) -> None:
+ mode = self._current_mode()
+ if mode:
+ self._send(mode.base_command)
+
+ def _on_matrix_test_type_changed(self, index: int) -> None:
+ test_type = self._m_test_type.itemData(index)
+ self._m_test_param.setVisible(test_type >= 2)
+
+ def _send_matrix_test(self) -> None:
+ mode = self._current_mode()
+ if not mode:
+ return
+ test_type = self._m_test_type.currentData()
+ if test_type == 0 or test_type == 1:
+ cmd = f"{mode.base_command}25{test_type}"
+ elif test_type == 2 or test_type == 3:
+ val = self._m_test_param.value()
+ cmd = f"{mode.base_command}25{test_type}{val:02d}"
+ elif test_type == 4:
+ val = self._m_test_param.value()
+ cmd = f"{mode.base_command}25{test_type}{val:03d}"
+ self._send(cmd)
+
+ def _send_matrix_scale(self, index: int) -> None:
+ val = self._m_scales[index].value()
+ cmd = f"853{index+1}{val:02d}"
+ self._send(cmd)
+
+ def _clear_matrix_scales(self) -> None:
+ self._send("8530")
+
def _send(self, cmd: str) -> None:
if not cmd:
return
diff --git a/app/widgets/dialogs/output_test_dialog.py b/app/widgets/dialogs/output_test_dialog.py
index 5c87f9f..4094e2a 100644
--- a/app/widgets/dialogs/output_test_dialog.py
+++ b/app/widgets/dialogs/output_test_dialog.py
@@ -9,6 +9,7 @@ from __future__ import annotations
from PySide6.QtCore import Qt
from PySide6.QtWidgets import (
+ QFormLayout,
QGridLayout,
QGroupBox,
QHBoxLayout,
@@ -22,6 +23,7 @@ from ...services.com_service import ComConnection
from ...services import settings_service
from ...ui.icons import get_icon_color, get_icon_muted, icon
from ...ui.theme import get_palette
+from ..no_wheel_combo_box import NoWheelComboBox
from .styled_dialog import StyledDialog
_NUM_OUTPUTS = 8
@@ -122,6 +124,67 @@ class OutputTestDialog(StyledDialog):
root.addLayout(bulk_layout)
+ # ── Дополнительные GPIO25/GPIO26 (cmd 50) ──
+ gpio_group = QGroupBox("Дополнительные GPIO25/GPIO26 (cmd 50)")
+ gpio_form = QFormLayout(gpio_group)
+ gpio_form.setSpacing(6)
+
+ # Выбор режимов
+ self._gpio25_mode = NoWheelComboBox()
+ self._gpio25_mode.addItem("Вход (Input)", 0)
+ self._gpio25_mode.addItem("Выход (Output)", 1)
+
+ self._gpio26_mode = NoWheelComboBox()
+ self._gpio26_mode.addItem("Вход (Input)", 0)
+ self._gpio26_mode.addItem("Выход (Output)", 1)
+
+ mode_layout = QHBoxLayout()
+ mode_layout.addWidget(self._gpio25_mode, 1)
+ mode_layout.addWidget(QLabel("GPIO26:"))
+ mode_layout.addWidget(self._gpio26_mode, 1)
+
+ apply_mode_btn = QPushButton("Задать режим")
+ apply_mode_btn.setIcon(icon("fa5s.cog", color=get_icon_color()))
+ apply_mode_btn.clicked.connect(self._send_gpio_modes)
+ mode_layout.addWidget(apply_mode_btn)
+ gpio_form.addRow("GPIO25 Режим:", mode_layout)
+
+ # Установка уровней
+ self._gpio25_level = NoWheelComboBox()
+ self._gpio25_level.addItem("Низкий (Low)", 0)
+ self._gpio25_level.addItem("Высокий (High)", 1)
+
+ self._gpio26_level = NoWheelComboBox()
+ self._gpio26_level.addItem("Низкий (Low)", 0)
+ self._gpio26_level.addItem("Высокий (High)", 1)
+
+ level_layout = QHBoxLayout()
+ level_layout.addWidget(self._gpio25_level, 1)
+ level_layout.addWidget(QLabel("GPIO26:"))
+ level_layout.addWidget(self._gpio26_level, 1)
+
+ apply_level_btn = QPushButton("Задать уровни")
+ apply_level_btn.setIcon(icon("fa5s.bolt", color=get_icon_color()))
+ apply_level_btn.clicked.connect(self._send_gpio_levels)
+ level_layout.addWidget(apply_level_btn)
+ gpio_form.addRow("GPIO25 Уровень:", level_layout)
+
+ # Запрос состояния
+ status_layout = QHBoxLayout()
+ read_state_btn = QPushButton("Запросить состояние (cmd 50)")
+ read_state_btn.setProperty("role", "primary")
+ read_state_btn.setIcon(icon("fa5s.sync-alt", color="#ecfdf5"))
+ read_state_btn.clicked.connect(self._read_gpio_state)
+ status_layout.addWidget(read_state_btn)
+
+ read_short_btn = QPushButton("Краткое состояние (cmd 5005)")
+ read_short_btn.setIcon(icon("fa5s.info-circle", color=get_icon_color()))
+ read_short_btn.clicked.connect(self._read_gpio_short)
+ status_layout.addWidget(read_short_btn)
+
+ gpio_form.addRow(status_layout)
+ root.addWidget(gpio_group)
+
# ── Лог ──
self._log = QLabel("")
self._log.setObjectName("LogLabel")
@@ -156,6 +219,26 @@ class OutputTestDialog(StyledDialog):
cmd = "200"
self._send(cmd)
+ def _send_gpio_modes(self) -> None:
+ g25 = self._gpio25_mode.currentData()
+ g26 = self._gpio26_mode.currentData()
+ p_val = g25 * 10 + g26
+ cmd = f"5001{p_val:02d}"
+ self._send(cmd)
+
+ def _send_gpio_levels(self) -> None:
+ g25 = self._gpio25_level.currentData()
+ g26 = self._gpio26_level.currentData()
+ p_val = g25 * 10 + g26
+ cmd = f"5010{p_val:02d}"
+ self._send(cmd)
+
+ def _read_gpio_state(self) -> None:
+ self._send("50")
+
+ def _read_gpio_short(self) -> None:
+ self._send("50050")
+
def _send(self, cmd: str) -> None:
if not cmd:
return
diff --git a/app/widgets/dialogs/prize_showcase_dialog.py b/app/widgets/dialogs/prize_showcase_dialog.py
new file mode 100644
index 0000000..3198c33
--- /dev/null
+++ b/app/widgets/dialogs/prize_showcase_dialog.py
@@ -0,0 +1,195 @@
+"""Диалог «Управление призовой витриной» — ячейки призовой стойки (cmd 25).
+
+Управляет количеством ячеек от 1 до 20 (по умолчанию 5).
+Команды:
+ - 25: Показать состояние всех ячеек
+ - 2501B: Выдать приз из ячейки B
+ - 2511B: Открыть ячейку B без списания приза
+ - 25991/25990: Включить/выключить автозапуск и перезагрузить
+"""
+
+from __future__ import annotations
+
+from PySide6.QtCore import Qt
+from PySide6.QtWidgets import (
+ QFormLayout,
+ QGridLayout,
+ QGroupBox,
+ QHBoxLayout,
+ QLabel,
+ QMessageBox,
+ QPushButton,
+ QScrollArea,
+ QSpinBox,
+ QVBoxLayout,
+ QWidget,
+)
+
+from ...services.com_service import ComConnection
+from ...ui.icons import get_icon_color, get_icon_muted, icon
+from .styled_dialog import StyledDialog
+
+
+class PrizeShowcaseDialog(StyledDialog):
+ """Диалог управления призовой витриной (cmd 25)."""
+
+ def __init__(self, connection: ComConnection, *, parent=None) -> None:
+ super().__init__(parent)
+ self._conn = connection
+ self.setWindowTitle("Призовая витрина")
+ self.setMinimumWidth(550)
+ self.setMinimumHeight(450)
+ self._build_ui()
+
+ def _build_ui(self) -> None:
+ root = self.containerLayout()
+ root.setSpacing(12)
+
+ # ── Описание ──
+ desc = QLabel(
+ "Управление ячейками витрины призов (команда 25).\n"
+ "Поддерживается выдача призов, разблокировка ячеек и управление авторежимом."
+ )
+ desc.setWordWrap(True)
+ desc.setStyleSheet("color: #9ca3af; font-size: 12px; margin-bottom: 4px;")
+ root.addWidget(desc)
+
+ # ── Общие настройки авторежима ──
+ global_group = QGroupBox("Общие параметры витрины")
+ global_layout = QHBoxLayout(global_group)
+
+ query_btn = QPushButton("Состояние ячеек (cmd 25)")
+ query_btn.setProperty("role", "primary")
+ query_btn.setIcon(icon("fa5s.sync-alt", color="#ecfdf5"))
+ query_btn.clicked.connect(self._query_state)
+ global_layout.addWidget(query_btn)
+
+ auto_on_btn = QPushButton("Авторежим Вкл")
+ auto_on_btn.setIcon(icon("fa5s.play-circle", color=get_icon_color()))
+ auto_on_btn.clicked.connect(self._send_auto_on)
+ global_layout.addWidget(auto_on_btn)
+
+ auto_off_btn = QPushButton("Авторежим Выкл")
+ auto_off_btn.setIcon(icon("fa5s.stop-circle", color=get_icon_color()))
+ auto_off_btn.clicked.connect(self._send_auto_off)
+ global_layout.addWidget(auto_off_btn)
+
+ root.addWidget(global_group)
+
+ # ── Выбор количества ячеек ──
+ cfg_layout = QHBoxLayout()
+ cfg_layout.addWidget(QLabel("Количество ячеек витрины (1-20):"))
+ self._cells_count_spin = QSpinBox()
+ self._cells_count_spin.setRange(1, 20)
+ self._cells_count_spin.setValue(5)
+ self._cells_count_spin.valueChanged.connect(self._rebuild_grid)
+ cfg_layout.addWidget(self._cells_count_spin)
+ cfg_layout.addStretch()
+ root.addLayout(cfg_layout)
+
+ # ── Область прокрутки для ячеек ──
+ scroll = QScrollArea()
+ scroll.setWidgetResizable(True)
+ scroll.setStyleSheet("QScrollArea { background: transparent; border: none; }")
+
+ self._grid_widget = QWidget()
+ self._grid_layout = QGridLayout(self._grid_widget)
+ self._grid_layout.setSpacing(8)
+ self._grid_layout.setContentsMargins(0, 0, 0, 0)
+ scroll.setWidget(self._grid_widget)
+ root.addWidget(scroll, 1)
+
+ # ── Лог ──
+ self._log = QLabel("")
+ self._log.setObjectName("LogLabel")
+ self._log.setWordWrap(True)
+ root.addWidget(self._log)
+
+ # ── Закрыть ──
+ btn_layout = QHBoxLayout()
+ btn_layout.addStretch()
+ close_btn = QPushButton("Закрыть")
+ close_btn.setIcon(icon("fa5s.times", color=get_icon_muted()))
+ close_btn.clicked.connect(self.close)
+ btn_layout.addWidget(close_btn)
+ root.addLayout(btn_layout)
+
+ # Создаем сетку ячеек при старте
+ self._rebuild_grid()
+
+ def _rebuild_grid(self) -> None:
+ """Перестроить сетку ячеек на основе значения spinbox."""
+ # Очищаем старые элементы из макета
+ while self._grid_layout.count():
+ item = self._grid_layout.takeAt(0)
+ widget = item.widget()
+ if widget:
+ widget.deleteLater()
+
+ count = self._cells_count_spin.value()
+
+ # Заголовки столбцов
+ headers = ["Ячейка", "Выдать приз (01)", "Открыть ячейку (11)"]
+ for col, h in enumerate(headers):
+ lbl = QLabel(f"{h}")
+ lbl.setAlignment(Qt.AlignmentFlag.AlignCenter)
+ self._grid_layout.addWidget(lbl, 0, col)
+
+ for i in range(count):
+ row = i + 1
+ cell_idx = i + 1
+
+ lbl_cell = QLabel(f"Ячейка {cell_idx}")
+ lbl_cell.setAlignment(Qt.AlignmentFlag.AlignCenter)
+ lbl_cell.setStyleSheet(
+ "background: #1f2937; border: 1px solid #374151; "
+ "border-radius: 4px; padding: 4px; color: #f3f4f6;"
+ )
+ self._grid_layout.addWidget(lbl_cell, row, 0)
+
+ # Выдать приз (2501B)
+ btn_dispense = QPushButton("Выдать")
+ btn_dispense.setProperty("role", "primary")
+ btn_dispense.setIcon(icon("fa5s.gift", color="#ecfdf5"))
+ btn_dispense.clicked.connect(lambda checked=False, idx=cell_idx: self._dispense_prize(idx))
+ self._grid_layout.addWidget(btn_dispense, row, 1)
+
+ # Открыть ячейку (2511B)
+ btn_open = QPushButton("Открыть")
+ btn_open.setIcon(icon("fa5s.unlock", color=get_icon_color()))
+ btn_open.clicked.connect(lambda checked=False, idx=cell_idx: self._open_cell(idx))
+ self._grid_layout.addWidget(btn_open, row, 2)
+
+ # ── Команды витрины ──
+
+ def _query_state(self) -> None:
+ self._send("25")
+
+ def _send_auto_on(self) -> None:
+ self._send("25991")
+
+ def _send_auto_off(self) -> None:
+ self._send("25990")
+
+ def _dispense_prize(self, cell: int) -> None:
+ self._send(f"2501{cell}")
+
+ def _open_cell(self, cell: int) -> None:
+ self._send(f"2511{cell}")
+
+ def _send(self, cmd: str) -> None:
+ if not cmd:
+ return
+ if not self._conn.is_connected:
+ QMessageBox.warning(self, "Ошибка", "Порт не подключён.")
+ return
+ try:
+ self._conn.send_command(cmd)
+ self._log.setText(f"→ {cmd}")
+ resp = self._conn.read_response(timeout=0.4)
+ self._conn.release()
+ if resp:
+ self._log.setText(f"→ {cmd}\n← {resp}")
+ except Exception as exc:
+ self._conn.release()
+ self._log.setText(f"Ошибка: {exc}")
diff --git a/app/widgets/sections/led_test_section.py b/app/widgets/sections/led_test_section.py
index 21d8fdb..dcd1b81 100644
--- a/app/widgets/sections/led_test_section.py
+++ b/app/widgets/sections/led_test_section.py
@@ -9,6 +9,7 @@ from __future__ import annotations
from PySide6.QtCore import QTimer
from PySide6.QtWidgets import (
QCheckBox,
+ QColorDialog,
QFormLayout,
QGroupBox,
QHBoxLayout,
@@ -112,6 +113,12 @@ class LedTestSection(QWidget):
gamma_layout.addWidget(apply_gamma_btn)
color_form.addRow("Гамма-коррекция:", gamma_layout)
+ # Пользовательский цвет
+ self._custom_color_btn = QPushButton("Выбрать и отправить цвет")
+ self._custom_color_btn.setIcon(icon("fa5s.palette", color=get_icon_color()))
+ self._custom_color_btn.clicked.connect(self._send_custom_color)
+ color_form.addRow("Свой цвет (08):", self._custom_color_btn)
+
# Тест цвета
self._color_test_btn = QPushButton("Тест цвета")
self._color_test_btn.setProperty("role", "primary")
@@ -157,6 +164,16 @@ class LedTestSection(QWidget):
cmd = f"89{ch}2{val}"
self._send(cmd)
+ def _send_custom_color(self) -> None:
+ color = QColorDialog.getColor(Qt.GlobalColor.white, self, "Выберите цвет")
+ if color.isValid():
+ ch = self._cmd89_channel_combo.currentData()
+ r = color.red()
+ g = color.green()
+ b = color.blue()
+ cmd = f"8{ch}08{r:03d}{g:03d}{b:03d}"
+ self._send(cmd)
+
def _run_color_test(self) -> None:
self._color_test_cmds = ["8003", "8004", "8005", "8002", "8000"]
self._color_test_idx = 0
diff --git a/app/widgets/sections/payment_section.py b/app/widgets/sections/payment_section.py
index 8b1782f..515c09d 100644
--- a/app/widgets/sections/payment_section.py
+++ b/app/widgets/sections/payment_section.py
@@ -19,6 +19,7 @@ from PySide6.QtWidgets import (
QWidget,
)
+from ...services.com_service import ComConnection
from ...ui.icons import get_icon_color, icon
@@ -27,10 +28,12 @@ class PaymentSection(QWidget):
def __init__(
self,
+ connection: ComConnection,
send_raw: Callable[[str], None],
parent: QWidget | None = None,
) -> None:
super().__init__(parent)
+ self._conn = connection
self._send_raw = send_raw
self._auto_dispense_hopper: int = 0
self._auto_dispense_timer: QTimer | None = None
@@ -56,16 +59,32 @@ class PaymentSection(QWidget):
# ── Монетоприёмник ──
coin_group = QGroupBox("Монетоприёмник")
- coin_layout = QHBoxLayout(coin_group)
+ coin_root_layout = QVBoxLayout(coin_group)
+
+ coin_row = QHBoxLayout()
self._coin_label = QLabel("Монеты: —")
self._coin_label.setMinimumWidth(120)
- coin_layout.addWidget(self._coin_label)
+ coin_row.addWidget(self._coin_label)
self._coin_confirm_btn = QPushButton("Подтвердить (10)")
self._coin_confirm_btn.setIcon(icon("fa5s.check-circle", color=get_icon_color()))
self._coin_confirm_btn.setToolTip("Отправить подтверждение COIN (команда 10)")
self._coin_confirm_btn.clicked.connect(lambda: self._send_raw("10"))
- coin_layout.addWidget(self._coin_confirm_btn)
- coin_layout.addStretch()
+ coin_row.addWidget(self._coin_confirm_btn)
+ coin_row.addStretch()
+ coin_root_layout.addLayout(coin_row)
+
+ global_row = QHBoxLayout()
+ self._global_summa_label = QLabel("Накопительный счетчик (cmd 93): —")
+ global_row.addWidget(self._global_summa_label)
+
+ self._global_summa_refresh_btn = QPushButton("Запросить")
+ self._global_summa_refresh_btn.setIcon(icon("fa5s.sync-alt", color=get_icon_color()))
+ self._global_summa_refresh_btn.setToolTip("Запросить global_summa")
+ self._global_summa_refresh_btn.clicked.connect(self._refresh_global_summa)
+ global_row.addWidget(self._global_summa_refresh_btn)
+ global_row.addStretch()
+ coin_root_layout.addLayout(global_row)
+
root.addWidget(coin_group)
# ── Хопперы ──
@@ -133,6 +152,7 @@ class PaymentSection(QWidget):
def refresh_icons(self) -> None:
clr = get_icon_color()
self._coin_confirm_btn.setIcon(icon("fa5s.check-circle", color=clr))
+ self._global_summa_refresh_btn.setIcon(icon("fa5s.sync-alt", color=clr))
self._h1_on.setIcon(icon("fa5s.play", color=clr))
self._h1_off.setIcon(icon("fa5s.stop", color=clr))
self._h1_auto_btn.setIcon(icon("fa5s.cog", color=clr))
@@ -215,3 +235,20 @@ class PaymentSection(QWidget):
f"Хоппер {hopper}",
f"Ошибка выдачи: датчик хоппера {hopper} не сработал за 10 секунд.",
)
+
+ def _refresh_global_summa(self) -> None:
+ if not self._conn.is_connected:
+ QMessageBox.warning(self, "Ошибка", "Порт не подключён.")
+ return
+ try:
+ self._conn.send_command("93")
+ resp = self._conn.read_response(timeout=0.4)
+ self._conn.release()
+ if resp:
+ self._global_summa_label.setText(f"Накопительный счетчик (cmd 93): {resp}")
+ self._event_log.setText(f"← {resp}")
+ else:
+ self._global_summa_label.setText("Накопительный счетчик (cmd 93): нет ответа")
+ except Exception as exc:
+ self._conn.release()
+ QMessageBox.critical(self, "Ошибка", f"Не удалось прочитать счетчик: {exc}")