diff --git a/services/ir_remote_service.py b/services/ir_remote_service.py index 7866813..a252192 100644 --- a/services/ir_remote_service.py +++ b/services/ir_remote_service.py @@ -1,10 +1,11 @@ from __future__ import annotations import os -import select -import struct +import subprocess +import re import threading import logging +import time from pathlib import Path from typing import Callable @@ -122,70 +123,110 @@ class IrRemoteService(QObject): self._thread = None def _event_loop(self) -> None: - """Цикл обработки событий.""" - logger.info(f"Event loop started for {self._device_path}") + """Цикл обработки событий через ir-keytable.""" + logger.info(f"Starting ir-keytable event loop") + + last_scancode = None + last_time = 0 + min_delay = 0.3 # Минимальная задержка между нажатиями (сек) + key_held = False # Флаг: кнопка зажата + while self._running: try: - with open(self._device_path, "rb") as f: - logger.info(f"Opened {self._device_path} successfully") - while self._running: - # Используем select для неблокирующего чтения - ready, _, _ = select.select([f], [], [], 0.5) - if not ready: + # Запускаем ir-keytable -t для чтения событий + # Пробуем без sudo, если не работает - добавь правило в sudoers + proc = subprocess.Popen( + ["ir-keytable", "-t"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + bufsize=1 + ) + + logger.info("ir-keytable started") + + while self._running and proc.poll() is None: + line = proc.stdout.readline() + if not line: + break + + line = line.strip() + + # Игнорируем EV_SYN события (это просто разделители) + if "EV_SYN" in line: + continue + + logger.debug(f"ir-keytable output: {line}") + + # Проверяем отпускание кнопки (EV_KEY с value=0) + if "EV_KEY" in line and "value=0" in line: + logger.debug("Key release detected") + key_held = False + last_scancode = None + self.reset_button_state() + continue + + # Парсим строку вида: + # 23567.604034: lirc protocol(nec): scancode = 0x19 + # 23567.604034: event type EV_MSC(0x04): scancode = 0x19 + match = re.search(r'scancode\s*=\s*(0x[0-9a-fA-F]+)', line) + if match: + current_time = time.time() + scancode = match.group(1).lower() + + # Проверяем есть ли "repeat" в строке + is_repeat = "repeat" in line.lower() + + # Если пришла другая кнопка — сбрасываем состояние предыдущей + if last_scancode and scancode != last_scancode and not is_repeat: + logger.debug(f"New button pressed, resetting previous: {last_scancode} -> {scancode}") + key_held = False + self._repeat_counts.pop(last_scancode, None) + self._hold_triggered.pop(last_scancode, None) + + # Если кнопка была отпущена и пришло новое нажатие + if not key_held and not is_repeat: + key_held = True + self._repeat_counts[scancode] = 1 + logger.debug(f"New key press: {scancode}") + # Если кнопка зажата и пришло repeat + elif key_held and is_repeat: + self._repeat_counts[scancode] = self._repeat_counts.get(scancode, 1) + 1 + logger.debug(f"Key repeat: {scancode}, count={self._repeat_counts[scancode]}") + # Игнорируем дубли + else: + logger.debug(f"Skipping: key_held={key_held}, is_repeat={is_repeat}") continue - # Читаем событие (24 байта: tv_sec, tv_usec, type, code, value) - data = f.read(24) - if len(data) < 24: - logger.warning(f"Read incomplete data: {len(data)} bytes") + # Проверяем задержку между разными нажатиями + if scancode != last_scancode and (current_time - last_time) < min_delay: + logger.debug(f"Skipping too fast: {current_time - last_time:.3f}s < {min_delay}s") continue - # Логируем сырые байты для отладки - logger.debug(f"Raw bytes: {data.hex()}") - - tv_sec, tv_usec, ev_type, ev_code, ev_value = struct.unpack("llHHI", data) - - logger.debug(f"Raw event: type={ev_type} (0x{ev_type:02x}), code={ev_code} (0x{ev_code:02x}), value={ev_value} (0x{ev_value:02x})") - - # Обрабатываем MSC_SCAN события (скан-код) - if ev_type == EV_MSC and ev_code == MSC_SCAN: - scancode = f"0x{ev_value:02x}" - logger.debug(f"MSC_SCAN event: scancode={scancode}") - self._handle_scancode(scancode) - - # Обрабатываем EV_KEY события (отпускание кнопки) - elif ev_type == EV_KEY and ev_value == KEY_UP: - logger.debug(f"KEY_UP event: code={ev_code}") - # Кнопка отпущена - сбрасываем состояние - self.reset_button_state() - - except (OSError, IOError) as e: - # Устройство недоступно, ждём и пробуем снова - logger.warning(f"Device error: {e}, retrying...") - import time + logger.debug(f"Processing scancode: {scancode}, is_repeat: {is_repeat}") + self._handle_scancode(scancode, is_repeat) + last_scancode = scancode + last_time = current_time + + proc.terminate() + proc.wait(timeout=1) + + except Exception as e: + logger.warning(f"ir-keytable error: {e}, retrying in 1s...") time.sleep(1.0) - def _handle_scancode(self, scancode: str) -> None: + def _handle_scancode(self, scancode: str, is_repeat: bool = False) -> None: """Обработать скан-код кнопки.""" action = self._scancode_to_action.get(scancode) if not action: logger.debug(f"Unknown scancode: {scancode}") return - logger.debug(f"Scancode: {scancode}, action: {action}") - - # Увеличиваем счётчик повторений - self._repeat_counts[scancode] = self._repeat_counts.get(scancode, 0) + 1 - repeat_count = self._repeat_counts[scancode] - - # Проверяем, было ли зажатие уже обработано + repeat_count = self._repeat_counts.get(scancode, 1) hold_triggered = self._hold_triggered.get(scancode, False) - - # Определяем тип события - is_repeat = repeat_count > 1 is_hold = repeat_count >= build_info.IR_REPEAT_COUNT_FOR_HOLD - logger.debug(f" repeat_count={repeat_count}, is_repeat={is_repeat}, is_hold={is_hold}") + logger.debug(f"Scancode: {scancode}, action: {action}, repeat_count={repeat_count}, is_hold={is_hold}") # Отправляем сигналы if action == "play_pause":