car_ui/screens/setting/bluetooth_screen.py
2026-04-01 02:07:43 +03:00

388 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from __future__ import annotations
from PySide6.QtCore import Qt, QTimer, QSettings, QEvent, Signal
from PySide6.QtGui import QFont
from PySide6.QtWidgets import (
QWidget,
QLabel,
QVBoxLayout,
QHBoxLayout,
QPushButton,
QListWidget,
QListWidgetItem,
QScroller,
QFrame,
)
import build_info
from services.bluetooth_service import BluetoothService, BluetoothDevice
from ui.confirm_dialog import ConfirmDialog
class BluetoothDeviceCard(QFrame):
"""Карточка Bluetooth устройства."""
connect_clicked = Signal(str) # mac
disconnect_clicked = Signal(str) # mac
remove_clicked = Signal(str) # mac
def __init__(self, device: BluetoothDevice, connected: bool, parent=None):
super().__init__(parent)
self._device = device
self._connected = connected
self._busy = False
self.setObjectName("BluetoothDeviceCard")
self.setFrameShape(QFrame.StyledPanel)
self.setCursor(Qt.PointingHandCursor)
root = QHBoxLayout(self)
root.setContentsMargins(14, 10, 14, 10)
root.setSpacing(12)
# Индикатор статуса (цветная точка)
self.status_indicator = QLabel("")
self.status_indicator.setObjectName("BluetoothStatusIndicator")
self.status_indicator.setFont(QFont("", 18))
self.status_indicator.setAlignment(Qt.AlignCenter)
self.status_indicator.setFixedSize(24, 24)
# Информация об устройстве
info_col = QVBoxLayout()
info_col.setContentsMargins(0, 0, 0, 0)
info_col.setSpacing(2)
self.name_label = QLabel(device.name if device.name else "Неизвестное устройство")
self.name_label.setObjectName("BluetoothDeviceName")
self.name_label.setFont(QFont("", 15, 600))
self.mac_label = QLabel(device.mac)
self.mac_label.setObjectName("BluetoothDeviceMac")
self.mac_label.setFont(QFont("", 11))
self.status_label = QLabel("Подключено" if connected else "Не подключено")
self.status_label.setObjectName("BluetoothDeviceStatus")
self.status_label.setFont(QFont("", 12))
info_col.addWidget(self.name_label)
info_col.addWidget(self.mac_label)
info_col.addWidget(self.status_label)
# Кнопка удаления (крестик)
self.remove_btn = QPushButton("")
self.remove_btn.setObjectName("BluetoothRemoveBtn")
self.remove_btn.setToolTip("Удалить устройство")
self.remove_btn.setFixedSize(40, 40)
self.remove_btn.setCursor(Qt.PointingHandCursor)
self.remove_btn.clicked.connect(self._on_remove)
root.addWidget(self.status_indicator)
root.addLayout(info_col, 1)
root.addWidget(self.remove_btn)
self._update_status_indicator()
def _update_status_indicator(self):
"""Обновить цвет индикатора."""
if self._connected:
self.status_indicator.setStyleSheet("color: #4ade80;") # зелёный
self.status_label.setStyleSheet("color: #4ade80;")
self.status_label.setText("Подключено")
else:
self.status_indicator.setStyleSheet("color: #9ca3af;") # серый
self.status_label.setStyleSheet("color: #9ca3af;")
self.status_label.setText("Не подключено")
def update_connected(self, connected: bool):
"""Обновить статус подключения."""
self._connected = connected
self._update_status_indicator()
def is_busy(self) -> bool:
"""Проверить, выполняется ли действие."""
return self._busy
def _on_remove(self):
"""Обработчик нажатия кнопки удаления."""
if self._busy:
return
self.remove_clicked.emit(self._device.mac)
def mousePressEvent(self, event):
"""Обработка клика по карточке."""
if self._busy:
return # Игнорируем клики во время выполнения действия
if self._connected:
self.disconnect_clicked.emit(self._device.mac)
else:
self.connect_clicked.emit(self._device.mac)
super().mousePressEvent(event)
class BluetoothScreen(QWidget):
def __init__(self, on_back):
super().__init__()
self._on_back = on_back
self._settings = QSettings("car_ui", "ui")
self._bt_service = BluetoothService(self)
self._discoverable_timer = QTimer(self)
self._discoverable_timer.timeout.connect(self._refresh_discoverable)
self._list_refresh_timer = QTimer(self)
self._list_refresh_timer.timeout.connect(self.refresh_list)
root = QVBoxLayout(self)
root.setContentsMargins(0, 0, 0, 0)
root.setSpacing(12)
self.status = QLabel("Статус: —")
self.status.setObjectName("BluetoothStatus")
self.status.setFont(QFont("", 12))
self.list = QListWidget()
self.list.setObjectName("BluetoothList")
self.list.setSpacing(8)
self.list.setSelectionMode(QListWidget.NoSelection)
self.list.setFocusPolicy(Qt.NoFocus)
self.list.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.list.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
QScroller.scroller(self.list.viewport()).grabGesture(
self.list.viewport(),
QScroller.LeftMouseButtonGesture,
)
actions = QHBoxLayout()
actions.setContentsMargins(0, 0, 0, 0)
actions.setSpacing(12)
# Кнопка скрыта, видимость включается автоматически при входе в меню
self.btn_visible = QPushButton("Сделать видимым")
self.btn_visible.setObjectName("BluetoothActionBtn")
self.btn_visible.setMinimumHeight(56)
self.btn_visible.clicked.connect(self._make_visible)
self.btn_visible.setVisible(False)
self.btn_refresh = QPushButton("Обновить список")
self.btn_refresh.setObjectName("BluetoothActionBtn")
self.btn_refresh.setMinimumHeight(56)
self.btn_refresh.clicked.connect(self.refresh_list)
self.btn_refresh.setVisible(False) # скрываем, обновляемся автоматически
# Кнопки подключить/отключить скрыты — управление через клик по карточке
self.btn_connect = QPushButton("Подключить")
self.btn_connect.setObjectName("BluetoothActionBtnPrimary")
self.btn_connect.setMinimumHeight(56)
self.btn_connect.clicked.connect(self._connect_last_selected)
self.btn_connect.setVisible(False)
self.btn_disconnect = QPushButton("Отключить")
self.btn_disconnect.setObjectName("BluetoothActionBtn")
self.btn_disconnect.setMinimumHeight(56)
self.btn_disconnect.clicked.connect(self._disconnect_last_selected)
self.btn_disconnect.setVisible(False)
actions.addWidget(self.btn_visible, 1)
actions.addWidget(self.btn_refresh, 1)
actions.addWidget(self.btn_connect, 1)
actions.addWidget(self.btn_disconnect, 1)
root.addWidget(self.status)
root.addWidget(self.list, 1)
root.addLayout(actions)
self.refresh_list()
QTimer.singleShot(200, self._auto_connect_last)
def refresh_list(self):
"""Обновить список сопряженных устройств."""
devices = self._bt_service.get_paired_devices()
self.list.clear()
self._cards: dict[str, BluetoothDeviceCard] = {}
for dev in devices:
connected = self._bt_service.is_connected(dev.mac)
card = BluetoothDeviceCard(dev, connected, self)
card.connect_clicked.connect(self._connect_device)
card.disconnect_clicked.connect(self._disconnect_device)
card.remove_clicked.connect(self._remove_device)
item = QListWidgetItem()
item.setSizeHint(card.sizeHint())
self.list.addItem(item)
self.list.setItemWidget(item, card)
self._cards[dev.mac] = card
self._update_status()
def _auto_connect_last(self):
"""Автоподключение к последнему устройству."""
last_mac = self._settings.value("bluetooth/last_mac", "")
if not last_mac:
return
self._connect_device(last_mac)
def _connect_last_selected(self):
"""Подключить последнее выбранное устройство."""
if self._cards:
last_mac = self._settings.value("bluetooth/last_mac", "")
if last_mac and last_mac in self._cards:
self._connect_device(last_mac)
def _disconnect_last_selected(self):
"""Отключить последнее выбранное устройство."""
if self._cards:
last_mac = self._settings.value("bluetooth/last_mac", "")
if last_mac and last_mac in self._cards:
self._disconnect_device(last_mac)
def _connect_device(self, mac: str):
"""Подключиться к устройству по MAC."""
if mac not in self._cards:
return
card = self._cards[mac]
if card.is_busy():
return
# Блокируем все карточки (только клики)
self._set_all_cards_busy(True)
# Выделяем только активную карточку
self._set_card_active(mac, True, "Подключение...")
def do_connect():
success = self._bt_service.connect_device(mac)
if not success:
self.status.setText(f"Статус: ошибка ({self._bt_service.last_error})")
self._settings.setValue("bluetooth/last_mac", mac)
QTimer.singleShot(500, lambda: self._finish_action(mac))
QTimer.singleShot(0, do_connect)
def _disconnect_device(self, mac: str):
"""Отключить устройство по MAC."""
if mac not in self._cards:
return
card = self._cards[mac]
if card.is_busy():
return
# Блокируем все карточки (только клики)
self._set_all_cards_busy(True)
# Выделяем только активную карточку
self._set_card_active(mac, True, "Отключение...")
def do_disconnect():
success = self._bt_service.disconnect_device(mac)
if not success:
self.status.setText(f"Статус: ошибка ({self._bt_service.last_error})")
else:
self.status.setText(f"Статус: отключено от {mac}")
QTimer.singleShot(500, lambda: self._finish_action(mac))
QTimer.singleShot(0, do_disconnect)
def _remove_device(self, mac: str):
"""Удалить устройство из списка сопряженных."""
device = self._cards.get(mac)
if not device:
return
name = device._device.name if device._device.name else mac
dialog = ConfirmDialog(
"Подтверждение",
f"Удалить устройство \"{name}\" из списка сопряженных?",
"Удалить",
ok_object_name="ConfirmOkDanger",
)
if dialog.exec() == ConfirmDialog.Accepted:
# Блокируем все карточки (только клики)
self._set_all_cards_busy(True)
# Выделяем только активную карточку
self._set_card_active(mac, True, "Удаление...")
success = self._bt_service.remove_device(mac)
if success:
self.status.setText(f"Статус: устройство {mac} удалено")
QTimer.singleShot(300, self.refresh_list)
else:
self.status.setText(f"Статус: ошибка удаления ({self._bt_service.last_error})")
self._set_all_cards_busy(False)
self._set_card_active(mac, False)
def _finish_action(self, mac: str):
"""Завершить действие и обновить статус."""
self._set_all_cards_busy(False)
self._set_card_active(mac, False)
self.refresh_list()
def _set_all_cards_busy(self, busy: bool):
"""Установить состояние busy для всех карточек (только блокировка кликов)."""
for card in self._cards.values():
card._busy = busy
if busy:
card.setCursor(Qt.WaitCursor)
else:
card.setCursor(Qt.PointingHandCursor)
def _set_card_active(self, mac: str, active: bool, action_text: str = ""):
"""Установить активное состояние для конкретной карточки (визуальное выделение)."""
if mac not in self._cards:
return
card = self._cards[mac]
card.setProperty("selected", active)
card.style().unpolish(card)
card.style().polish(card)
if active and action_text:
card.status_label.setText(action_text)
card.status_label.setStyleSheet("color: #fbbf24;")
elif not active:
card._update_status_indicator()
def _update_card_status(self, mac: str):
"""Обновить статус на карточке устройства."""
if mac in self._cards:
connected = self._bt_service.is_connected(mac)
self._cards[mac].update_connected(connected)
self._update_status()
def _make_visible(self):
"""Сделать устройство видимым для сопряжения."""
success = self._bt_service.make_discoverable()
if self._bt_service.last_error == "power_off":
self.status.setText("Статус: питание BT выключено (проверьте rfkill)")
elif self._bt_service.last_error == "max_devices":
self.status.setText(
f"Статус: достигнуто макс. число устройств ({build_info.BLUETOOTH_MAX_PAIRED_DEVICES})"
)
elif not success:
self.status.setText(f"Статус: ошибка ({self._bt_service.last_error})")
else:
self.status.setText("Статус: видим для сопряжения (10 сек)")
self._discoverable_timer.start(9000) # 9 секунд
def _refresh_discoverable(self):
"""Продлить режим сопряжения."""
self._bt_service.make_discoverable()
self._discoverable_timer.start(9000) # 9 секунд
def _update_status(self):
"""Обновить текстовый статус."""
last_mac = self._settings.value("bluetooth/last_mac", "")
if not last_mac or last_mac not in self._cards:
self.status.setText("Статус: —")
return
self.status.setText(self._bt_service.get_status_text(last_mac))
def showEvent(self, event):
"""Экран показан — запускаем режим сопряжения и обновление списка."""
super().showEvent(event)
self._refresh_discoverable()
# Обновляем список каждые 2 секунды для отслеживания новых устройств
self._list_refresh_timer.start(2000)
def hideEvent(self, event):
"""Экран скрыт — останавливаем таймеры."""
super().hideEvent(event)
self._discoverable_timer.stop()
self._list_refresh_timer.stop()