diff --git a/main.py b/main.py index 9855367..b41ffc4 100644 --- a/main.py +++ b/main.py @@ -1,16 +1,19 @@ import os import re +import io import sys import ctypes import logging +import threading import time +import json import hashlib import tempfile import warnings import subprocess import win32com.client from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, QDialog, QLabel, QComboBox, QCheckBox, QLineEdit, QAction, QMenu, QMessageBox, QPushButton, QTableWidget, QVBoxLayout, QHBoxLayout, QTableWidgetItem -from PyQt5.QtCore import Qt, QCoreApplication, QTimer +from PyQt5.QtCore import Qt, QCoreApplication, QPropertyAnimation, QTimer from PyQt5.QtGui import QFont @@ -257,7 +260,7 @@ class BtSdk: return self.dll.Btsdk_IsBluetoothReady() def enableDeviceDiscovery(self): - device_class, max_dev_num, max_durations = 0X000400, 30, 7 + device_class, max_dev_num, max_durations = 0X000400, 30, 10 code = self.dll.Btsdk_StartDeviceDiscovery(device_class, max_dev_num, max_durations) code != 0 and warnings.warn(self.errs[code]) return not code @@ -376,6 +379,18 @@ class BtSdk: self.dll.Btsdk_DeleteUnpairedDevicesByClass(0) return True + def isBluetoothActive(self): + data = [] + enum_handle = self.dll.Btsdk_StartEnumConnection() + while True: + conn = self.dll.Btsdk_EnumConnection(enum_handle, 0) + if (not conn) == 1: + self.dll.Btsdk_EndEnumConnection(enum_handle) + break + data.append(conn) + return True if len(data) > 0 else False + + def _fd(f): return f.fileno() if hasattr(f, 'fileno') else f @@ -473,6 +488,23 @@ else: return fcntl.flock(_fd(f), flags) == 0 +def getJson(file, data=None): + if os.path.exists(file): + try: + return json.loads(open(file=file, mode='r', encoding='utf-8').read()) + except json.decoder.JSONDecodeError: + return data + return data + + +def putJson(file, data=None): + with open(file=file, mode='w', encoding='utf-8') as f: + flock(f, LOCK_EX) + res = f.write(json.dumps(data, indent=4, ensure_ascii=True)) + flock(f, LOCK_UN) + return res + + class LoggerFileHandler: def __init__(self, log_file: str, mode: str = 'a', level: str = None, fmt: str = None): self.log, self.mod = log_file, mode @@ -549,6 +581,22 @@ class Logger: self.c = self.logger.critical +class Setting(dict): + def __init__(self, setting_file: str, setting_default: dict): + self.data_file = os.path.abspath(setting_file) + super().__init__(getJson(self.data_file, setting_default)) + + def __getitem__(self, item): + if item in self: + return super().__getitem__(item) + else: + return None + + def __setitem__(self, key, value): + super().__setitem__(key, value) + putJson(self.data_file, self) + + class CustomLineEdit(QLineEdit): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -612,11 +660,14 @@ class MainWindow(QMainWindow): def __init__(self, logger: Logger): super().__init__() self.app_name = '蓝牙音频连接' - self.app_version = ('1.0.3', '20240815', 'zhaoyafan', 'zhaoyafan@foxmail.com', 'https://www.fanscloud.net/') + self.app_version = ('1.1.0', '20241030', 'zhaoyafan', 'zhaoyafan@foxmail.com', 'https://www.fanscloud.net/') self.app_text = {'search': '正在搜索', 'connecting': '连接中...'} self.logger = logger + self.toast = ToastNotification() + self.setting = Setting(os.path.abspath(os.path.join(tempfile.gettempdir(), '%s.config' % self.md5(__file__)[:16])), {}) self.bluesoleil = None self.currentDevHandle = None + self.currentDevRssi = 0 self.setWindowTitle(self.app_name) self.setGeometry(0, 0, 600, 400) self.statusBar() @@ -652,6 +703,12 @@ class MainWindow(QMainWindow): self.edtBtName.setFixedSize(250, 36) m_layout_1.addWidget(self.edtBtName) + # 信号强度 + self.edtBtRssi = CustomLineEditNoPopup('') + self.edtBtRssi.setReadOnly(True) + self.edtBtRssi.setFixedSize(38, 36) + m_layout_1.addWidget(self.edtBtRssi) + # 断开连接 self.butDisconnect = CustomPushButton('断开连接') self.butDisconnect.setFixedSize(64, 36) @@ -673,7 +730,7 @@ class MainWindow(QMainWindow): # 地址输入 self.edtBtAddr = CustomLineEdit('') self.edtBtAddr.setStyleSheet('QLineEdit {font-size: 24px; font-family: \'Microsoft YaHei\'; color: #000000; background-color: #FFFFEE;}') - self.edtBtAddr.setFixedSize(249, 36) + self.edtBtAddr.setFixedSize(294, 36) self.edtBtAddr.returnPressed.connect(self.on_connect_bt_address) m_layout_3.addWidget(self.edtBtAddr) @@ -694,6 +751,15 @@ class MainWindow(QMainWindow): self.show() self.init_bluesoleil() + self.connect_status_update_timer = QTimer() + self.connect_status_update_timer.timeout.connect(self.connect_status_update) + + if (self.bluesoleil.isBluetoothActive() and self.setting['deviceHandle'] > 0) == 1: + self.in_connect_process() + self.currentDevHandle = self.setting['deviceHandle'] or 0 + self.currentDevRssi = self.setting['deviceRssi'] or 0 + self.connect_action_ui_update() + def init_bluesoleil(self): try: self.bluesoleil = BtSdk() @@ -724,6 +790,20 @@ class MainWindow(QMainWindow): if (not _c) == 0: QMessageBox.information(self, '关于', "" 'version: %s, build: %s, author: %s, email: %s, site: %s' % (_c[0], _c[1], _c[2], _c[3], '%s' % (_c[4], _c[4]))) + @staticmethod + def md5(input_data): + if isinstance(input_data, bytes): + return hashlib.md5(input_data).hexdigest() + if isinstance(input_data, str): + return hashlib.md5(bytes(input_data, encoding='utf-8')).hexdigest() + md5_object = hashlib.md5() + while True: + data = input_data.read(io.DEFAULT_BUFFER_SIZE) + if data: + md5_object.update(data) + else: + return md5_object.hexdigest() + @staticmethod def path_expandvars(path): resolve = os.path.expandvars(path) @@ -770,6 +850,8 @@ class MainWindow(QMainWindow): def in_connect_process(self): self.edtBtName.setStyleSheet('font-size: 16px; font-family: \'Microsoft YaHei\'; color: #303030; border: 1px solid #808080; font-weight: bold; background-color: #7BD136') self.edtBtName.setText('') + self.edtBtRssi.setStyleSheet('font-size: 16px; font-family: \'Microsoft YaHei\'; color: #303030; border: 1px solid #808080; font-weight: bold; background-color: #7BD136') + self.edtBtRssi.setText('') self.edtBtAddr.setText('') self.edtBtAddr.setEnabled(False) self.edtBtAddr.setFocus() @@ -779,6 +861,8 @@ class MainWindow(QMainWindow): def un_connect_process(self): self.edtBtName.setStyleSheet('font-size: 16px; font-family: \'Microsoft YaHei\'; color: #303030; border: 1px solid #808080; font-weight: bold; background-color: #FDD391') self.edtBtName.setText('') + self.edtBtRssi.setStyleSheet('font-size: 16px; font-family: \'Microsoft YaHei\'; color: #303030; border: 1px solid #808080; font-weight: bold; background-color: #F5F5F5') + self.edtBtRssi.setText('') self.edtBtAddr.setText('') self.edtBtAddr.setEnabled(True) self.edtBtAddr.setFocus() @@ -786,8 +870,13 @@ class MainWindow(QMainWindow): self.butManuSearch.setEnabled(True) def on_disconnect(self): + self.connect_status_update_timer.stop() self.bluesoleil.cancelDeviceDiscovery() self.bluesoleil.removeAllDevices() + self.currentDevHandle = 0 + self.currentDevRssi = 0 + self.setting['deviceHandle'] = 0 + self.setting['deviceRssi'] = 0 self.un_connect_process() def on_autosearch(self): @@ -826,22 +915,41 @@ class MainWindow(QMainWindow): return None return self.connect_action_ui(text) + def connect_action_ui_update(self): + self.connect_status_update_timer.start(1500) + self.edtBtAddr.setText(str(self.bluesoleil.getDeviceAddr(self.currentDevHandle)).replace(':', '')) + self.edtBtName.setText(str(self.bluesoleil.getDeviceName(self.currentDevHandle))) + self.edtBtRssi.setText(str(self.currentDevRssi if self.currentDevRssi != -3 else '')) + if -3 == self.currentDevRssi: + self.edtBtRssi.setStyleSheet('font-size: 16px; font-family: \'Microsoft YaHei\'; color: #303030; border: 1px solid #808080; font-weight: bold; background-color: #FFF3F3') + if -65 < self.currentDevRssi <= -5: + self.edtBtRssi.setStyleSheet('font-size: 16px; font-family: \'Microsoft YaHei\'; color: #303030; border: 1px solid #808080; font-weight: bold; background-color: #73CD2A') + if -72 < self.currentDevRssi <= -65: + self.edtBtRssi.setStyleSheet('font-size: 16px; font-family: \'Microsoft YaHei\'; color: #303030; border: 1px solid #808080; font-weight: bold; background-color: #F2B32D') + if -99 < self.currentDevRssi <= -72: + self.edtBtRssi.setStyleSheet('font-size: 16px; font-family: \'Microsoft YaHei\'; color: #303030; border: 1px solid #808080; font-weight: bold; background-color: #F25555') + def connect_action_ui(self, handle): if (handle != 0) == 1: self.in_connect_process() self.setWindowTitle(self.app_text['connecting']) - if (not self.connect(handle)) == 1: + self.currentDevRssi = self.connect(handle) or 0 + else: + self.currentDevRssi = 0 + if (not self.currentDevRssi) == 1: self.un_connect_process() self.edtBtAddr.selectAll() self.setWindowTitle(self.app_name) QMessageBox.critical(self, '提示', '连接失败') return None else: - self.edtBtAddr.setText(str(self.bluesoleil.getDeviceAddr(self.currentDevHandle)).replace(':', '')) - self.edtBtName.setText(str(self.bluesoleil.getDeviceName(self.currentDevHandle))) + self.connect_action_ui_update() self.setWindowTitle(self.app_name) return True + def connect_status_update(self): + self.bluesoleil.isBluetoothActive() or self.on_disconnect() + def connect(self, data): devHandle = data if (isinstance(data, str)) == 1: @@ -853,11 +961,23 @@ class MainWindow(QMainWindow): break if (not devHandle) == 1: return None + rssi = 0 + for i in range(8): + time.sleep(0.25) + rssi = self.bluesoleil.getDeviceRSSI(devHandle) + if (rssi != 0) == 1: + break code = self.bluesoleil.connectAudioService(devHandle) if (not code) == 1: return None self.currentDevHandle = devHandle - return True + self.setting['deviceHandle'] = devHandle + self.setting['deviceRssi'] = rssi + if (not rssi and code > 0) == 1: + return -3 + else: + return rssi + class ManuWindow(QDialog): def __init__(self, mainwindow): @@ -938,10 +1058,11 @@ class ManuWindow(QDialog): self.mainwindow.bluesoleil.enableDeviceDiscovery() except AttributeError: self.close() + self.mainwindow.toast.show_toast('正在搜索设备') timer = QTimer(self) timer.timeout.connect(self.task_end_search) timer.setSingleShot(True) - timer.start(7000) + timer.start(9999) def on_connect(self, data): self.mainwindow.bluesoleil.cancelDeviceDiscovery() @@ -956,6 +1077,46 @@ class ManuWindow(QDialog): pass +class ToastNotification(QDialog): + def __init__(self): + super().__init__() + self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.WindowType.Tool) + layout = QVBoxLayout() + layout.setContentsMargins(5, 705, 5, 5) + + self.label = QLabel("") + self.label.setStyleSheet("font-family: 'Microsoft YaHei'; font-size: 24px; color: #ffffff;") + layout.addWidget(self.label) + self.setLayout(layout) + self.setAttribute(Qt.WA_TranslucentBackground) + self.setStyleSheet("background-color: rgba(65, 65, 65, 180); border-radius: 5px; padding: 10px 16px 10px 16px") + + self.adjustSize() + + self.timer = QTimer(self) + self.timer.setInterval(2000) + self.timer.timeout.connect(self.close) + + self.fade_animation = QPropertyAnimation(self, b"windowOpacity") + self.fade_animation.setDuration(500) + + def show_toast(self, message): + self.close() + self.label.setText(message) + self.timer.isActive() and self.timer.stop() + self.timer.start() + self.adjustSize() + self.fade_animation.setStartValue(0.0) + self.fade_animation.setEndValue(1.0) + self.show() + self.fade_animation.start() + + def closeEvent(self, event): + if self.timer.isActive(): + self.timer.stop() + super().closeEvent(event) + + if __name__ == '__main__': if (os.path.basename(__file__).lower().endswith('.int')) == 1: QCoreApplication.addLibraryPath(os.path.abspath(os.path.join(os.path.dirname(__file__), 'site-packages/PyQt5/Qt5/plugins')))