car_ui/screens/setting/bluetooth_screen.py
2026-04-01 00:48:13 +03:00

302 lines
13 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
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.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 _on_remove(self):
"""Обработчик нажатия кнопки удаления."""
self.remove_clicked.emit(self._device.mac)
def mousePressEvent(self, event):
"""Обработка клика по карточке."""
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) # убираем выделение, т.к. карточки кликабельны
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
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(300, lambda: self._update_card_status(mac))
def _disconnect_device(self, mac: str):
"""Отключить устройство по MAC."""
if mac not in self._cards:
return
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(300, lambda: self._update_card_status(mac))
def _remove_device(self, mac: str):
"""Удалить устройство из списка сопряженных."""
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})")
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()