plate-config-app/app/widgets/sections/payment_section.py

209 lines
9.5 KiB
Python

"""Встраиваемая секция «Тест оплаты и призов» — монетоприёмник и хопперы.
Виджет не управляет потоком опроса самостоятельно — принимает 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 ...ui.icons import get_icon_color, icon
class PaymentSection(QWidget):
"""Inline-секция оплаты и призов (для базового режима)."""
def __init__(
self,
send_raw: Callable[[str], None],
parent: QWidget | None = None,
) -> None:
super().__init__(parent)
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)
# ── Монетоприёмник ──
coin_group = QGroupBox("Монетоприёмник")
coin_layout = QHBoxLayout(coin_group)
self._coin_label = QLabel("Монеты: —")
self._coin_label.setMinimumWidth(120)
coin_layout.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()
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._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 секунд.",
)