Compare commits
No commits in common. "65f5f1fee304807ff327bd32d2859d732ca3874a" and "ac7fd5fd4b331554163fdce3bba8f74564da653d" have entirely different histories.
65f5f1fee3
...
ac7fd5fd4b
2
app.py
2
app.py
@ -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():
|
||||||
|
|||||||
@ -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(
|
||||||
"Подтверждение",
|
"Подтверждение",
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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":
|
||||||
|
|||||||
@ -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)
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user