Compare commits

..

No commits in common. "65f5f1fee304807ff327bd32d2859d732ca3874a" and "ac7fd5fd4b331554163fdce3bba8f74564da653d" have entirely different histories.

5 changed files with 46 additions and 163 deletions

2
app.py
View File

@ -69,8 +69,6 @@ def _apply_startup_display_defaults():
settings.setValue("display/theme", "night") settings.setValue("display/theme", "night")
if not settings.contains("media/source_mode"): if not settings.contains("media/source_mode"):
settings.setValue("media/source_mode", "bluetooth") settings.setValue("media/source_mode", "bluetooth")
if not settings.contains("ir_remote/enabled"):
settings.setValue("ir_remote/enabled", False)
def _apply_reset_if_requested(): def _apply_reset_if_requested():

View File

@ -3,13 +3,13 @@ import subprocess
from PySide6.QtCore import Qt, QSettings from PySide6.QtCore import Qt, QSettings
from PySide6.QtGui import QFont from PySide6.QtGui import QFont
from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QFrame from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel
import build_info import build_info
from ui.confirm_dialog import ConfirmDialog from ui.confirm_dialog import ConfirmDialog
def build_dev_screen(on_exit, on_ir_toggle=None) -> QWidget: def build_dev_screen(on_exit) -> QWidget:
screen = QWidget() screen = QWidget()
layout = QVBoxLayout(screen) layout = QVBoxLayout(screen)
layout.setContentsMargins(0, 0, 0, 0) layout.setContentsMargins(0, 0, 0, 0)
@ -18,7 +18,6 @@ def build_dev_screen(on_exit, on_ir_toggle=None) -> QWidget:
layout.addWidget(_build_persist_toggle()) layout.addWidget(_build_persist_toggle())
layout.addWidget(_build_sound_toggles()) layout.addWidget(_build_sound_toggles())
layout.addWidget(_build_mock_devices_toggle()) layout.addWidget(_build_mock_devices_toggle())
layout.addWidget(_build_ir_remote_toggle(on_ir_toggle))
hdr = QHBoxLayout() hdr = QHBoxLayout()
hdr.setContentsMargins(0, 0, 0, 0) hdr.setContentsMargins(0, 0, 0, 0)
@ -185,30 +184,6 @@ def _build_mock_devices_toggle() -> QWidget:
return container return container
def _build_ir_remote_toggle(on_ir_toggle=None) -> QWidget:
"""Переключатель ИК-пульта."""
settings = QSettings("car_ui", "ui")
container = QWidget()
layout = QVBoxLayout(container)
layout.setContentsMargins(12, 6, 12, 6)
layout.setSpacing(8)
row = _toggle_row(
"ИК-пульт (требуется перезапуск)",
settings,
"ir_remote/enabled",
False,
)
layout.addWidget(row)
# Подключаем callback
if on_ir_toggle:
row.findChild(QPushButton).toggled.connect(on_ir_toggle)
return container
def _confirm_exit(on_exit): def _confirm_exit(on_exit):
dialog = ConfirmDialog( dialog = ConfirmDialog(
"Подтверждение", "Подтверждение",

View File

@ -153,7 +153,7 @@ class SettingsScreen(QWidget):
scroll.setWidget(content) scroll.setWidget(content)
list_layout.addWidget(scroll, 1) list_layout.addWidget(scroll, 1)
self._dev_screen = build_dev_screen(self._exit_app, self._on_ir_toggle) self._dev_screen = build_dev_screen(self._exit_app)
self._about_screen = AboutScreen() self._about_screen = AboutScreen()
self._about_screen.dev_unlocked.connect(self._enable_dev_mode) self._about_screen.dev_unlocked.connect(self._enable_dev_mode)
self._bt_screen = BluetoothScreen(self._show_list) self._bt_screen = BluetoothScreen(self._show_list)
@ -211,16 +211,6 @@ class SettingsScreen(QWidget):
self.stack.setCurrentWidget(self._list_screen) self.stack.setCurrentWidget(self._list_screen)
self.view_changed.emit("Настройки", False) self.view_changed.emit("Настройки", False)
def _on_ir_toggle(self, enabled: bool):
"""Обработчик переключения ИК-пульта."""
# Находим main_window и вызываем метод
from ui.main_window_new import MainWindowNew
main_window = self.parent()
while main_window and not isinstance(main_window, MainWindowNew):
main_window = main_window.parent()
if main_window:
main_window.set_ir_remote_enabled(enabled)
def _show_bluetooth(self): def _show_bluetooth(self):
self.stack.setCurrentWidget(self._bt_screen) self.stack.setCurrentWidget(self._bt_screen)
self.view_changed.emit("Bluetooth", True) self.view_changed.emit("Bluetooth", True)

View File

@ -1,11 +1,10 @@
from __future__ import annotations from __future__ import annotations
import os import os
import subprocess import select
import re import struct
import threading import threading
import logging import logging
import time
from pathlib import Path from pathlib import Path
from typing import Callable from typing import Callable
@ -71,12 +70,6 @@ class IrRemoteService(QObject):
def _find_ir_device(self) -> str | None: def _find_ir_device(self) -> str | None:
"""Найти устройство ИК-пульта в /dev/input.""" """Найти устройство ИК-пульта в /dev/input."""
# Сначала пробуем lirc устройства
for path in Path("/dev").glob("lirc*"):
logger.info(f"Found lirc device: {path}")
return str(path)
# Затем ищем event устройства с ir/rc в имени
for path in Path("/dev/input").glob("event*"): for path in Path("/dev/input").glob("event*"):
try: try:
# Проверяем, поддерживает ли устройство MSC_SCAN # Проверяем, поддерживает ли устройство MSC_SCAN
@ -85,19 +78,13 @@ class IrRemoteService(QObject):
EVIOCGNAME = 0x80000000 | (16 << 8) | ord('E') << 24 | 6 EVIOCGNAME = 0x80000000 | (16 << 8) | ord('E') << 24 | 6
import fcntl import fcntl
name = fcntl.ioctl(f.fileno(), EVIOCGNAME, b'\x00' * 256) name = fcntl.ioctl(f.fileno(), EVIOCGNAME, b'\x00' * 256)
name_str = name.decode('utf-8', errors='ignore').lower() if b"ir" in name.lower() or b"rc" in name.lower():
if "ir" in name_str or "rc" in name_str:
logger.info(f"Found IR device by name: {path} ({name_str})")
return str(path) return str(path)
except (OSError, IOError): except (OSError, IOError):
continue continue
# Если не нашли по имени, пробуем первое event
# Если не нашли по имени, пробуем все event устройства for path in Path("/dev/input").glob("event*"):
for path in sorted(Path("/dev/input").glob("event*")):
logger.info(f"Trying event device: {path}")
return str(path) return str(path)
logger.warning("No IR device found")
return None return None
def start(self) -> bool: def start(self) -> bool:
@ -123,110 +110,59 @@ class IrRemoteService(QObject):
self._thread = None self._thread = None
def _event_loop(self) -> None: def _event_loop(self) -> None:
"""Цикл обработки событий через 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: while self._running:
try: try:
# Запускаем ir-keytable -t для чтения событий with open(self._device_path, "rb") as f:
# Пробуем без sudo, если не работает - добавь правило в sudoers while self._running:
proc = subprocess.Popen( # Используем select для неблокирующего чтения
["ir-keytable", "-t"], ready, _, _ = select.select([f], [], [], 0.1)
stdout=subprocess.PIPE, if not ready:
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 continue
# Проверяем задержку между разными нажатиями # Читаем событие (24 байта: tv_sec, tv_usec, type, code, value)
if scancode != last_scancode and (current_time - last_time) < min_delay: data = f.read(24)
logger.debug(f"Skipping too fast: {current_time - last_time:.3f}s < {min_delay}s") if len(data) < 24:
continue continue
logger.debug(f"Processing scancode: {scancode}, is_repeat: {is_repeat}") tv_sec, tv_usec, ev_type, ev_code, ev_value = struct.unpack("llHHI", data)
self._handle_scancode(scancode, is_repeat)
last_scancode = scancode # Обрабатываем MSC_SCAN события (скан-код)
last_time = current_time if ev_type == EV_MSC and ev_code == MSC_SCAN:
scancode = f"0x{ev_value:02x}"
proc.terminate() self._handle_scancode(scancode)
proc.wait(timeout=1)
# Обрабатываем EV_KEY события (отпускание кнопки)
except Exception as e: elif ev_type == EV_KEY and ev_value == KEY_UP:
logger.warning(f"ir-keytable error: {e}, retrying in 1s...") # Кнопка отпущена - сбрасываем состояние
self.reset_button_state()
except (OSError, IOError):
# Устройство недоступно, ждём и пробуем снова
import time
time.sleep(1.0) time.sleep(1.0)
def _handle_scancode(self, scancode: str, is_repeat: bool = False) -> None: def _handle_scancode(self, scancode: str) -> None:
"""Обработать скан-код кнопки.""" """Обработать скан-код кнопки."""
action = self._scancode_to_action.get(scancode) action = self._scancode_to_action.get(scancode)
if not action: if not action:
logger.debug(f"Unknown scancode: {scancode}") logger.debug(f"Unknown scancode: {scancode}")
return return
repeat_count = self._repeat_counts.get(scancode, 1) logger.debug(f"Scancode: {scancode}, action: {action}")
# Увеличиваем счётчик повторений
self._repeat_counts[scancode] = self._repeat_counts.get(scancode, 0) + 1
repeat_count = self._repeat_counts[scancode]
# Проверяем, было ли зажатие уже обработано
hold_triggered = self._hold_triggered.get(scancode, False) hold_triggered = self._hold_triggered.get(scancode, False)
# Определяем тип события
is_repeat = repeat_count > 1
is_hold = repeat_count >= build_info.IR_REPEAT_COUNT_FOR_HOLD is_hold = repeat_count >= build_info.IR_REPEAT_COUNT_FOR_HOLD
logger.debug(f"Scancode: {scancode}, action: {action}, repeat_count={repeat_count}, is_hold={is_hold}") logger.debug(f" repeat_count={repeat_count}, is_repeat={is_repeat}, is_hold={is_hold}")
# Отправляем сигналы # Отправляем сигналы
if action == "play_pause": if action == "play_pause":

View File

@ -108,12 +108,9 @@ class MainWindowNew(QMainWindow):
self._bt_service = BluetoothService(self) self._bt_service = BluetoothService(self)
self._media_controller = MediaController(self._bt_service, self) self._media_controller = MediaController(self._bt_service, self)
# ИК-пульт (только если включено в настройках) # ИК-пульт
self._ir_service = IrRemoteService(self) self._ir_service = IrRemoteService(self)
self._ir_enabled = self._settings.value("ir_remote/enabled", False) self._connect_ir_remote()
if self._ir_enabled:
self._connect_ir_remote()
self._ir_service.start()
self.media_screen = MediaScreen(self._media_controller) self.media_screen = MediaScreen(self._media_controller)
self.stack.addWidget(self.media_screen) # 0 self.stack.addWidget(self.media_screen) # 0
@ -224,19 +221,6 @@ class MainWindowNew(QMainWindow):
# TODO: реализовать быстрое уменьшение громкости # TODO: реализовать быстрое уменьшение громкости
pass pass
def set_ir_remote_enabled(self, enabled: bool):
"""Включить или выключить ИК-пульт."""
self._settings.setValue("ir_remote/enabled", enabled)
self._ir_enabled = enabled
if enabled:
self._connect_ir_remote()
self._ir_service.start()
logger.info("IR remote enabled")
else:
self._ir_service.stop()
logger.info("IR remote disabled")
def apply_theme(self): def apply_theme(self):
self.app.setStyleSheet(THEME_NIGHT if self.is_night else THEME_DAY) self.app.setStyleSheet(THEME_NIGHT if self.is_night else THEME_DAY)