add bt
This commit is contained in:
parent
29ba12f018
commit
4dc76de17c
2
qqqq
2
qqqq
@ -1,3 +1,5 @@
|
|||||||
export DISPLAY=:0
|
export DISPLAY=:0
|
||||||
|
|
||||||
./.venv/bin/python3 ./main.py
|
./.venv/bin/python3 ./main.py
|
||||||
|
|
||||||
|
sudo usermod -aG bluetooth cheykrym
|
||||||
|
|||||||
257
screens/bluetooth.py
Normal file
257
screens/bluetooth.py
Normal file
@ -0,0 +1,257 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime
|
||||||
|
import re
|
||||||
|
|
||||||
|
from PySide6.QtCore import Qt, QTimer, QSettings
|
||||||
|
from PySide6.QtGui import QFont
|
||||||
|
from PySide6.QtWidgets import (
|
||||||
|
QWidget,
|
||||||
|
QLabel,
|
||||||
|
QVBoxLayout,
|
||||||
|
QHBoxLayout,
|
||||||
|
QPushButton,
|
||||||
|
QListWidget,
|
||||||
|
QListWidgetItem,
|
||||||
|
QScroller,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class BluetoothDevice:
|
||||||
|
mac: str
|
||||||
|
name: str
|
||||||
|
|
||||||
|
|
||||||
|
class BluetoothScreen(QWidget):
|
||||||
|
def __init__(self, on_back):
|
||||||
|
super().__init__()
|
||||||
|
self._on_back = on_back
|
||||||
|
self._settings = QSettings("car_ui", "ui")
|
||||||
|
self._log_path = os.path.expanduser("~/.cache/car_ui/bluetooth.log")
|
||||||
|
self._last_error = ""
|
||||||
|
|
||||||
|
root = QVBoxLayout(self)
|
||||||
|
root.setContentsMargins(0, 0, 0, 0)
|
||||||
|
root.setSpacing(12)
|
||||||
|
|
||||||
|
hdr = QHBoxLayout()
|
||||||
|
hdr.setContentsMargins(0, 0, 0, 0)
|
||||||
|
hdr.setSpacing(12)
|
||||||
|
|
||||||
|
back_btn = QPushButton("Назад")
|
||||||
|
back_btn.setObjectName("SettingsBackBtn")
|
||||||
|
back_btn.setMinimumSize(120, 44)
|
||||||
|
back_btn.clicked.connect(self._on_back)
|
||||||
|
|
||||||
|
title = QLabel("Bluetooth")
|
||||||
|
title.setFont(QFont("", 22, 700))
|
||||||
|
|
||||||
|
hdr.addWidget(back_btn)
|
||||||
|
hdr.addWidget(title)
|
||||||
|
hdr.addStretch(1)
|
||||||
|
|
||||||
|
self.status = QLabel("Статус: —")
|
||||||
|
self.status.setObjectName("BluetoothStatus")
|
||||||
|
self.status.setFont(QFont("", 12))
|
||||||
|
|
||||||
|
self.list = QListWidget()
|
||||||
|
self.list.setObjectName("BluetoothList")
|
||||||
|
self.list.setSpacing(6)
|
||||||
|
self.list.setSelectionMode(QListWidget.SingleSelection)
|
||||||
|
self.list.itemSelectionChanged.connect(self._on_select)
|
||||||
|
|
||||||
|
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_refresh = QPushButton("Обновить список")
|
||||||
|
self.btn_refresh.setObjectName("BluetoothActionBtn")
|
||||||
|
self.btn_refresh.setMinimumHeight(56)
|
||||||
|
self.btn_refresh.clicked.connect(self.refresh_list)
|
||||||
|
|
||||||
|
self.btn_connect = QPushButton("Подключить")
|
||||||
|
self.btn_connect.setObjectName("BluetoothActionBtnPrimary")
|
||||||
|
self.btn_connect.setMinimumHeight(56)
|
||||||
|
self.btn_connect.setEnabled(False)
|
||||||
|
self.btn_connect.clicked.connect(self._connect_selected)
|
||||||
|
|
||||||
|
self.btn_disconnect = QPushButton("Отключить")
|
||||||
|
self.btn_disconnect.setObjectName("BluetoothActionBtn")
|
||||||
|
self.btn_disconnect.setMinimumHeight(56)
|
||||||
|
self.btn_disconnect.setEnabled(False)
|
||||||
|
self.btn_disconnect.clicked.connect(self._disconnect_selected)
|
||||||
|
|
||||||
|
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.addLayout(hdr)
|
||||||
|
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._paired_devices()
|
||||||
|
self.list.clear()
|
||||||
|
|
||||||
|
for dev in devices:
|
||||||
|
label = f"{dev.name} ({dev.mac})" if dev.name else dev.mac
|
||||||
|
item = QListWidgetItem(label)
|
||||||
|
item.setData(Qt.UserRole, dev.mac)
|
||||||
|
self.list.addItem(item)
|
||||||
|
|
||||||
|
self._update_status()
|
||||||
|
|
||||||
|
def _on_select(self):
|
||||||
|
has_selection = self._selected_mac() is not None
|
||||||
|
self.btn_connect.setEnabled(has_selection)
|
||||||
|
self.btn_disconnect.setEnabled(has_selection)
|
||||||
|
self._update_status()
|
||||||
|
|
||||||
|
def _selected_mac(self) -> str | None:
|
||||||
|
items = self.list.selectedItems()
|
||||||
|
if not items:
|
||||||
|
return None
|
||||||
|
return items[0].data(Qt.UserRole)
|
||||||
|
|
||||||
|
def _auto_connect_last(self):
|
||||||
|
last_mac = self._settings.value("bluetooth/last_mac", "")
|
||||||
|
if not last_mac:
|
||||||
|
return
|
||||||
|
self._connect_device(last_mac, silent=True)
|
||||||
|
|
||||||
|
def _make_visible(self):
|
||||||
|
self._last_error = ""
|
||||||
|
self._run_cmd(["bluetoothctl", "power", "on"])
|
||||||
|
if self._last_error:
|
||||||
|
self.status.setText("Статус: питание BT выключено (проверьте rfkill)")
|
||||||
|
return
|
||||||
|
self._run_cmd(["bluetoothctl", "discoverable-timeout", "0"])
|
||||||
|
self._run_cmd(["bluetoothctl", "discoverable", "on"])
|
||||||
|
self._run_cmd(["bluetoothctl", "pairable", "on"])
|
||||||
|
self._run_cmd(["bluetoothctl", "agent", "NoInputNoOutput"])
|
||||||
|
self._run_cmd(["bluetoothctl", "default-agent"])
|
||||||
|
if self._last_error:
|
||||||
|
self.status.setText(f"Статус: ошибка ({self._last_error})")
|
||||||
|
else:
|
||||||
|
self.status.setText("Статус: видим для сопряжения")
|
||||||
|
|
||||||
|
def _connect_selected(self):
|
||||||
|
mac = self._selected_mac()
|
||||||
|
if not mac:
|
||||||
|
return
|
||||||
|
self._connect_device(mac, silent=False)
|
||||||
|
|
||||||
|
def _connect_device(self, mac: str, silent: bool):
|
||||||
|
self._last_error = ""
|
||||||
|
self._run_cmd(["bluetoothctl", "trust", mac])
|
||||||
|
self._run_cmd(["bluetoothctl", "connect", mac])
|
||||||
|
if not silent:
|
||||||
|
if self._last_error:
|
||||||
|
self.status.setText(f"Статус: ошибка ({self._last_error})")
|
||||||
|
else:
|
||||||
|
self.status.setText(f"Статус: подключаемся к {mac}")
|
||||||
|
self._settings.setValue("bluetooth/last_mac", mac)
|
||||||
|
QTimer.singleShot(300, self._update_status)
|
||||||
|
|
||||||
|
def _disconnect_selected(self):
|
||||||
|
mac = self._selected_mac()
|
||||||
|
if not mac:
|
||||||
|
return
|
||||||
|
self._last_error = ""
|
||||||
|
self._run_cmd(["bluetoothctl", "disconnect", mac])
|
||||||
|
if self._last_error:
|
||||||
|
self.status.setText(f"Статус: ошибка ({self._last_error})")
|
||||||
|
else:
|
||||||
|
self.status.setText(f"Статус: отключено от {mac}")
|
||||||
|
QTimer.singleShot(300, self._update_status)
|
||||||
|
|
||||||
|
def _update_status(self):
|
||||||
|
mac = self._selected_mac()
|
||||||
|
if not mac:
|
||||||
|
self.status.setText("Статус: выберите устройство")
|
||||||
|
return
|
||||||
|
info = self._device_info(mac)
|
||||||
|
connected = info.get("Connected", "no") == "yes"
|
||||||
|
name = info.get("Name", mac)
|
||||||
|
state = "подключено" if connected else "не подключено"
|
||||||
|
self.status.setText(f"Статус: {name} — {state}")
|
||||||
|
|
||||||
|
def _device_info(self, mac: str) -> dict[str, str]:
|
||||||
|
out = self._run_cmd(["bluetoothctl", "info", mac])
|
||||||
|
info: dict[str, str] = {}
|
||||||
|
for line in out.splitlines():
|
||||||
|
if ":" not in line:
|
||||||
|
continue
|
||||||
|
key, value = line.split(":", 1)
|
||||||
|
info[key.strip()] = value.strip()
|
||||||
|
return info
|
||||||
|
|
||||||
|
def _paired_devices(self) -> list[BluetoothDevice]:
|
||||||
|
out = self._run_cmd(["bluetoothctl", "devices", "Paired"])
|
||||||
|
if "Invalid command" in out or not out:
|
||||||
|
out = self._run_cmd(["bluetoothctl", "paired-devices"])
|
||||||
|
if "Invalid command" in out or not out:
|
||||||
|
out = self._run_cmd(["bluetoothctl", "devices"])
|
||||||
|
devices: list[BluetoothDevice] = []
|
||||||
|
for line in self._strip_ansi(out).splitlines():
|
||||||
|
parts = line.split()
|
||||||
|
if len(parts) >= 2 and parts[0] == "Device":
|
||||||
|
mac = parts[1]
|
||||||
|
name = " ".join(parts[2:]) if len(parts) > 2 else ""
|
||||||
|
devices.append(BluetoothDevice(mac=mac, name=name))
|
||||||
|
return devices
|
||||||
|
|
||||||
|
def _run_cmd(self, args: list[str]) -> str:
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
args,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=2,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
except (subprocess.SubprocessError, OSError):
|
||||||
|
self._last_error = "run-failed"
|
||||||
|
self._log(args, "", "run-failed", 1)
|
||||||
|
return ""
|
||||||
|
stdout = result.stdout.strip()
|
||||||
|
stderr = result.stderr.strip()
|
||||||
|
if result.returncode != 0 or stderr:
|
||||||
|
self._last_error = stderr or f"exit={result.returncode}"
|
||||||
|
self._log(args, stdout, stderr, result.returncode)
|
||||||
|
return stdout
|
||||||
|
|
||||||
|
def _log(self, args: list[str], stdout: str, stderr: str, code: int):
|
||||||
|
try:
|
||||||
|
os.makedirs(os.path.dirname(self._log_path), exist_ok=True)
|
||||||
|
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
line = (
|
||||||
|
f"[{ts}] cmd={' '.join(args)} code={code} "
|
||||||
|
f"stdout={stdout!r} stderr={stderr!r}\n"
|
||||||
|
)
|
||||||
|
with open(self._log_path, "a", encoding="utf-8") as f:
|
||||||
|
f.write(line)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _strip_ansi(self, text: str) -> str:
|
||||||
|
return re.sub(r"\x1b\[[0-9;]*m", "", text)
|
||||||
@ -12,6 +12,7 @@ from PySide6.QtWidgets import (
|
|||||||
from PySide6.QtCore import Qt
|
from PySide6.QtCore import Qt
|
||||||
from PySide6.QtGui import QFont
|
from PySide6.QtGui import QFont
|
||||||
from PySide6.QtWidgets import QScroller
|
from PySide6.QtWidgets import QScroller
|
||||||
|
from screens.bluetooth import BluetoothScreen
|
||||||
|
|
||||||
|
|
||||||
class SettingsRow(QPushButton):
|
class SettingsRow(QPushButton):
|
||||||
@ -90,13 +91,14 @@ class SettingsScreen(QWidget):
|
|||||||
content_layout.setContentsMargins(0, 0, 0, 0)
|
content_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
content_layout.setSpacing(12)
|
content_layout.setSpacing(12)
|
||||||
|
|
||||||
self._add_section(
|
bt_row = self._add_section(
|
||||||
content_layout,
|
content_layout,
|
||||||
"Сеть",
|
"Сеть",
|
||||||
[
|
[
|
||||||
("Wi-Fi", "Доступные сети и подключение"),
|
("Wi-Fi", "Доступные сети и подключение"),
|
||||||
("Bluetooth", "Сопряжение и устройства"),
|
("Bluetooth", "Сопряжение и устройства"),
|
||||||
],
|
],
|
||||||
|
return_row_title="Bluetooth",
|
||||||
)
|
)
|
||||||
|
|
||||||
dev_row = self._add_section(
|
dev_row = self._add_section(
|
||||||
@ -123,11 +125,15 @@ class SettingsScreen(QWidget):
|
|||||||
list_layout.addWidget(scroll, 1)
|
list_layout.addWidget(scroll, 1)
|
||||||
|
|
||||||
self._dev_screen = self._build_dev_screen()
|
self._dev_screen = self._build_dev_screen()
|
||||||
|
self._bt_screen = BluetoothScreen(self._show_list)
|
||||||
self.stack.addWidget(self._list_screen)
|
self.stack.addWidget(self._list_screen)
|
||||||
self.stack.addWidget(self._dev_screen)
|
self.stack.addWidget(self._dev_screen)
|
||||||
|
self.stack.addWidget(self._bt_screen)
|
||||||
|
|
||||||
if dev_row is not None:
|
if dev_row is not None:
|
||||||
dev_row.clicked.connect(self._show_dev)
|
dev_row.clicked.connect(self._show_dev)
|
||||||
|
if bt_row is not None:
|
||||||
|
bt_row.clicked.connect(self._show_bluetooth)
|
||||||
|
|
||||||
def _build_dev_screen(self) -> QWidget:
|
def _build_dev_screen(self) -> QWidget:
|
||||||
screen = QWidget()
|
screen = QWidget()
|
||||||
@ -188,6 +194,9 @@ class SettingsScreen(QWidget):
|
|||||||
def _show_list(self):
|
def _show_list(self):
|
||||||
self.stack.setCurrentWidget(self._list_screen)
|
self.stack.setCurrentWidget(self._list_screen)
|
||||||
|
|
||||||
|
def _show_bluetooth(self):
|
||||||
|
self.stack.setCurrentWidget(self._bt_screen)
|
||||||
|
|
||||||
def _exit_app(self):
|
def _exit_app(self):
|
||||||
app = QApplication.instance()
|
app = QApplication.instance()
|
||||||
if app is not None:
|
if app is not None:
|
||||||
|
|||||||
@ -29,4 +29,33 @@ QWidget { background: #F4F6F8; color: #111827; }
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
#DevExitBtn:hover { background: #E5E7EB; }
|
#DevExitBtn:hover { background: #E5E7EB; }
|
||||||
|
|
||||||
|
#BluetoothStatus { color: rgba(107,114,128,0.95); }
|
||||||
|
#BluetoothList {
|
||||||
|
background: #FFFFFF;
|
||||||
|
border: 1px solid #E5E7EB;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
#BluetoothList::item {
|
||||||
|
padding: 12px 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
#BluetoothList::item:selected { background: #F3F4F6; }
|
||||||
|
#BluetoothActionBtn {
|
||||||
|
background: #FFFFFF;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid #E5E7EB;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
#BluetoothActionBtn:hover { background: #F9FAFB; }
|
||||||
|
#BluetoothActionBtnPrimary {
|
||||||
|
background: #E5E7EB;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid #D1D5DB;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
#BluetoothActionBtnPrimary:hover { background: #D1D5DB; }
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -26,4 +26,31 @@ QWidget { background: #0B0E11; color: #E6EAF0; }
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
#DevExitBtn:hover { background: #263142; }
|
#DevExitBtn:hover { background: #263142; }
|
||||||
|
|
||||||
|
#BluetoothStatus { color: rgba(138,147,166,0.95); }
|
||||||
|
#BluetoothList {
|
||||||
|
background: #0F1318;
|
||||||
|
border: 1px solid #1B2330;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
#BluetoothList::item {
|
||||||
|
padding: 12px 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
#BluetoothList::item:selected { background: #1B2330; }
|
||||||
|
#BluetoothActionBtn {
|
||||||
|
background: #141A22;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
#BluetoothActionBtn:hover { background: #1B2330; }
|
||||||
|
#BluetoothActionBtnPrimary {
|
||||||
|
background: #2A3A52;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
#BluetoothActionBtnPrimary:hover { background: #344968; }
|
||||||
"""
|
"""
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user