"""Встраиваемая секция «Тест оплаты и призов» — монетоприёмник и хопперы. Виджет не управляет потоком опроса самостоятельно — принимает 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}")