Danil Iarosh 448b9bb07a Add board v2.x commands support to UI dialogs and sections
- Добавлена поддержка выбора произвольного цвета (QColorDialog) с отправкой команды 8008RRRGGGBBB.
- Реализована панель управления GPIO25/GPIO26 (команда 50) в output_test_dialog.py.
- Добавлена поддержка direct-объектов и эффектов Бродяги морей (команда 81) в led_test_dialog.py.
- Реализованы тесты матриц и direct-шкалы (команда 85) в led_test_dialog.py.
- Создан новый диалог prize_showcase_dialog.py для ячеек призовой витрины (команда 25).
- Добавлена кнопка «Витрина призов» в PRO-кнопки connection_tab.py.
- В payment_section.py добавлен показ накопительного счетчика монет global_summa (команда 93).
2026-06-30 09:22:36 +03:00

255 lines
12 KiB
Python
Raw Permalink 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.

"""Встраиваемая секция «Тест оплаты и призов» — монетоприёмник и хопперы.
Виджет не управляет потоком опроса самостоятельно — принимает callable
для отправки команд и вызывается снаружи при поступлении info-строк.
"""
from __future__ import annotations
from collections.abc import Callable
from PySide6.QtCore import QTimer
from PySide6.QtWidgets import (
QGroupBox,
QHBoxLayout,
QLabel,
QMessageBox,
QPushButton,
QVBoxLayout,
QWidget,
)
from ...services.com_service import ComConnection
from ...ui.icons import get_icon_color, icon
class PaymentSection(QWidget):
"""Inline-секция оплаты и призов (для базового режима)."""
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
# Нужен доступ к digital-маске для автовыдачи — будет передаваться снаружи
self._last_digital_val: int = 0xFFFFFFFF
self._setup_ui()
# ── 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)
# ── Монетоприёмник ──
coin_group = QGroupBox("Монетоприёмник")
coin_root_layout = QVBoxLayout(coin_group)
coin_row = QHBoxLayout()
self._coin_label = QLabel("Монеты: —")
self._coin_label.setMinimumWidth(120)
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_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)
# ── Хопперы ──
hopper_group = QGroupBox("Хопперы")
hopper_layout = QVBoxLayout(hopper_group)
# Хоппер 1
h1_row = QHBoxLayout()
h1_row.addWidget(QLabel("Хоппер 1:"))
self._hopper1_label = QLabel("—")
self._hopper1_label.setMinimumWidth(100)
h1_row.addWidget(self._hopper1_label)
self._h1_on = QPushButton("Вкл")
self._h1_on.setIcon(icon("fa5s.play", color=get_icon_color()))
self._h1_on.setToolTip("Включить мотор (301)")
self._h1_on.clicked.connect(lambda: self._send_raw("301"))
h1_row.addWidget(self._h1_on)
self._h1_off = QPushButton("Выкл")
self._h1_off.setIcon(icon("fa5s.stop", color=get_icon_color()))
self._h1_off.setToolTip("Выключить мотор (300)")
self._h1_off.clicked.connect(lambda: self._send_raw("300"))
h1_row.addWidget(self._h1_off)
self._h1_auto_btn = QPushButton("Автовыдача")
self._h1_auto_btn.setIcon(icon("fa5s.cog", color=get_icon_color()))
self._h1_auto_btn.setToolTip("Программная автовыдача: вкл мотор → ждёт датчик (10 сек)")
self._h1_auto_btn.clicked.connect(lambda: self._start_auto_dispense(1))
h1_row.addWidget(self._h1_auto_btn)
h1_row.addStretch()
hopper_layout.addLayout(h1_row)
# Хоппер 2
h2_row = QHBoxLayout()
h2_row.addWidget(QLabel("Хоппер 2:"))
self._hopper2_label = QLabel("—")
self._hopper2_label.setMinimumWidth(100)
h2_row.addWidget(self._hopper2_label)
self._h2_on = QPushButton("Вкл")
self._h2_on.setIcon(icon("fa5s.play", color=get_icon_color()))
self._h2_on.setToolTip("Включить мотор (401)")
self._h2_on.clicked.connect(lambda: self._send_raw("401"))
h2_row.addWidget(self._h2_on)
self._h2_off = QPushButton("Выкл")
self._h2_off.setIcon(icon("fa5s.stop", color=get_icon_color()))
self._h2_off.setToolTip("Выключить мотор (400)")
self._h2_off.clicked.connect(lambda: self._send_raw("400"))
h2_row.addWidget(self._h2_off)
self._h2_auto_btn = QPushButton("Автовыдача")
self._h2_auto_btn.setIcon(icon("fa5s.cog", color=get_icon_color()))
self._h2_auto_btn.setToolTip("Программная автовыдача: вкл мотор → ждёт датчик (10 сек)")
self._h2_auto_btn.clicked.connect(lambda: self._start_auto_dispense(2))
h2_row.addWidget(self._h2_auto_btn)
h2_row.addStretch()
hopper_layout.addLayout(h2_row)
root.addWidget(hopper_group)
# ── Лог событий ──
self._event_log = QLabel("")
self._event_log.setWordWrap(True)
self._event_log.setStyleSheet("color: #888; font-size: 11px;")
root.addWidget(self._event_log)
# ── Public ────────────────────────────────────────────────────────
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))
self._h2_on.setIcon(icon("fa5s.play", color=clr))
self._h2_off.setIcon(icon("fa5s.stop", color=clr))
self._h2_auto_btn.setIcon(icon("fa5s.cog", color=clr))
def on_info_line(self, line: str) -> None:
"""Обработать информационную строку (COINS, HOPPER, PRIZES)."""
upper = line.upper()
if upper.startswith("COINS"):
self._coin_label.setText(f"Монеты: {line}")
elif upper.startswith("HOPPER1"):
self._hopper1_label.setText(line)
elif upper.startswith("HOPPER2"):
self._hopper2_label.setText(line)
elif upper.startswith("HOPPER"):
self._hopper1_label.setText(line)
elif upper.startswith("PRIZES"):
self._coin_label.setText(f"Монеты: {line}")
self._event_log.setText(f"← {line}")
def on_digital(self, val: int) -> None:
"""Получить текущее значение цифровых входов (для автовыдачи)."""
self._last_digital_val = val
self._check_auto_dispense(val)
# ── Автовыдача ────────────────────────────────────────────────────
def _start_auto_dispense(self, hopper: int) -> None:
if self._auto_dispense_hopper != 0:
return
self._auto_dispense_hopper = hopper
label = self._hopper1_label if hopper == 1 else self._hopper2_label
btn = self._h1_auto_btn if hopper == 1 else self._h2_auto_btn
label.setText("Автовыдача…")
btn.setEnabled(False)
on_cmd = "301" if hopper == 1 else "401"
self._send_raw(on_cmd)
self._auto_dispense_timer = QTimer(self)
self._auto_dispense_timer.setSingleShot(True)
self._auto_dispense_timer.timeout.connect(self._auto_dispense_timeout)
self._auto_dispense_timer.start(10_000)
def _check_auto_dispense(self, val: int) -> None:
hopper = self._auto_dispense_hopper
if hopper == 0:
return
bit_index = 23 + hopper
sensor_active = not bool(val & (1 << bit_index))
if sensor_active:
self._finish_auto_dispense(success=True)
def _auto_dispense_timeout(self) -> None:
if self._auto_dispense_hopper != 0:
self._finish_auto_dispense(success=False)
def _finish_auto_dispense(self, *, success: bool) -> None:
hopper = self._auto_dispense_hopper
if hopper == 0:
return
if self._auto_dispense_timer is not None:
self._auto_dispense_timer.stop()
self._auto_dispense_timer.deleteLater()
self._auto_dispense_timer = None
off_cmd = "300" if hopper == 1 else "400"
self._send_raw(off_cmd)
label = self._hopper1_label if hopper == 1 else self._hopper2_label
btn = self._h1_auto_btn if hopper == 1 else self._h2_auto_btn
self._auto_dispense_hopper = 0
btn.setEnabled(True)
if success:
label.setText("Автовыдача: OK ✓")
self._event_log.setText(f"← Хоппер {hopper}: приз выдан")
else:
label.setText("Автовыдача: ОШИБКА ✗")
self._event_log.setText(f"← Хоппер {hopper}: таймаут 10 сек")
QMessageBox.warning(
self,
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}")