302 lines
13 KiB
Python
302 lines
13 KiB
Python
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()
|