Compare commits

...

3 Commits

Author SHA1 Message Date
65f5f1fee3 add params for ir 2026-04-01 05:35:28 +03:00
9bb3c1b2df mda 2026-04-01 05:28:03 +03:00
ae558f1362 logger 2026-04-01 05:05:21 +03:00
5 changed files with 163 additions and 46 deletions

2
app.py
View File

@ -69,6 +69,8 @@ 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 from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QFrame
import build_info import build_info
from ui.confirm_dialog import ConfirmDialog from ui.confirm_dialog import ConfirmDialog
def build_dev_screen(on_exit) -> QWidget: def build_dev_screen(on_exit, on_ir_toggle=None) -> QWidget:
screen = QWidget() screen = QWidget()
layout = QVBoxLayout(screen) layout = QVBoxLayout(screen)
layout.setContentsMargins(0, 0, 0, 0) layout.setContentsMargins(0, 0, 0, 0)
@ -18,6 +18,7 @@ def build_dev_screen(on_exit) -> 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)
@ -184,6 +185,30 @@ 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._dev_screen = build_dev_screen(self._exit_app, self._on_ir_toggle)
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,6 +211,16 @@ 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,10 +1,11 @@
from __future__ import annotations from __future__ import annotations
import os import os
import select import subprocess
import struct import re
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
@ -70,6 +71,12 @@ 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
@ -78,13 +85,19 @@ 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)
if b"ir" in name.lower() or b"rc" in name.lower(): name_str = name.decode('utf-8', errors='ignore').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
for path in Path("/dev/input").glob("event*"): # Если не нашли по имени, пробуем все 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:
@ -110,59 +123,110 @@ 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:
with open(self._device_path, "rb") as f: # Запускаем ir-keytable -t для чтения событий
while self._running: # Пробуем без sudo, если не работает - добавь правило в sudoers
# Используем select для неблокирующего чтения proc = subprocess.Popen(
ready, _, _ = select.select([f], [], [], 0.1) ["ir-keytable", "-t"],
if not ready: 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 continue
# Читаем событие (24 байта: tv_sec, tv_usec, type, code, value) # Проверяем задержку между разными нажатиями
data = f.read(24) if scancode != last_scancode and (current_time - last_time) < min_delay:
if len(data) < 24: logger.debug(f"Skipping too fast: {current_time - last_time:.3f}s < {min_delay}s")
continue continue
tv_sec, tv_usec, ev_type, ev_code, ev_value = struct.unpack("llHHI", data) logger.debug(f"Processing scancode: {scancode}, is_repeat: {is_repeat}")
self._handle_scancode(scancode, is_repeat)
last_scancode = scancode
last_time = current_time
# Обрабатываем MSC_SCAN события (скан-код) proc.terminate()
if ev_type == EV_MSC and ev_code == MSC_SCAN: proc.wait(timeout=1)
scancode = f"0x{ev_value:02x}"
self._handle_scancode(scancode)
# Обрабатываем 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) -> None: def _handle_scancode(self, scancode: str, is_repeat: bool = False) -> 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
logger.debug(f"Scancode: {scancode}, action: {action}") repeat_count = self._repeat_counts.get(scancode, 1)
# Увеличиваем счётчик повторений
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" 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": if action == "play_pause":

View File

@ -108,9 +108,12 @@ 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._connect_ir_remote() self._ir_enabled = self._settings.value("ir_remote/enabled", False)
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
@ -221,6 +224,19 @@ 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)