【前言】
参与《NUCLEO-WBA55CG开发板》试用中,我是准备DIY一台蓝牙远程控制的取暖器,蓝牙控制可以使用手机的微信小程,也可以使用web控件,我选择了python的bleak库进行设计,结合PyQt5实现GUI的桌面程序。
【界面设计】
使用QtDesigner进行设计,目前想到的功能,主要是展示从WBA55CG上采集到的温湿度进行展示。执行温度的设计,开关机,还有可以扫描到自己的设备,可指定设备进行连接。界面如下:
【代码设计】
1、首先使用PyUIC把ui文件转化为py文件,转换后实现界面.py文件:
# -*- coding: utf-8 -*- # Form implementation generated from reading ui file 'wbg55.ui' # # Created by: PyQt5 UI code generator 5.15.9 # # WARNING: Any manual changes made to this file will be lost when pyuic5 is # run again. Do not edit this file unless you know what you are doing. from PyQt5 import QtCore, QtGui, QtWidgets class Ui_MainWindow(object): def setupUi(self, MainWindow): MainWindow.setObjectName("MainWindow") MainWindow.resize(800, 600) self.centralwidget = QtWidgets.QWidget(MainWindow) self.centralwidget.setObjectName("centralwidget") self.bntConnect = QtWidgets.QPushButton(self.centralwidget) self.bntConnect.setGeometry(QtCore.QRect(50, 270, 81, 41)) font = QtGui.QFont() font.setPointSize(16) self.bntConnect.setFont(font) self.bntConnect.setObjectName("bntConnect") self.device_list = QtWidgets.QListWidget(self.centralwidget) self.device_list.setGeometry(QtCore.QRect(50, 70, 311, 191)) self.device_list.setObjectName("device_list") self.lcdNumberTemp = QtWidgets.QLCDNumber(self.centralwidget) self.lcdNumberTemp.setGeometry(QtCore.QRect(380, 80, 91, 71)) font = QtGui.QFont() font.setPointSize(36) self.lcdNumberTemp.setFont(font) self.lcdNumberTemp.setObjectName("lcdNumberTemp") self.labelLinkState = QtWidgets.QLabel(self.centralwidget) self.labelLinkState.setGeometry(QtCore.QRect(40, 370, 291, 31)) self.labelLinkState.setObjectName("labelLinkState") self.pushButtonConnect = QtWidgets.QPushButton(self.centralwidget) self.pushButtonConnect.setGeometry(QtCore.QRect(50, 40, 311, 31)) font = QtGui.QFont() font.setPointSize(12) self.pushButtonConnect.setFont(font) self.pushButtonConnect.setObjectName("pushButtonConnect") self.label_2 = QtWidgets.QLabel(self.centralwidget) self.label_2.setGeometry(QtCore.QRect(381, 40, 91, 31)) font = QtGui.QFont() font.setPointSize(16) self.label_2.setFont(font) self.label_2.setObjectName("label_2") self.label_3 = QtWidgets.QLabel(self.centralwidget) self.label_3.setGeometry(QtCore.QRect(530, 40, 81, 31)) font = QtGui.QFont() font.setPointSize(16) self.label_3.setFont(font) self.label_3.setObjectName("label_3") self.bntADD = QtWidgets.QPushButton(self.centralwidget) self.bntADD.setGeometry(QtCore.QRect(530, 230, 75, 41)) font = QtGui.QFont() font.setPointSize(16) self.bntADD.setFont(font) self.bntADD.setObjectName("bntADD") self.bntDec = QtWidgets.QPushButton(self.centralwidget) self.bntDec.setGeometry(QtCore.QRect(530, 290, 75, 41)) font = QtGui.QFont() font.setPointSize(16) self.bntDec.setFont(font) self.bntDec.setObjectName("bntDec") self.lcdNumberSet = QtWidgets.QLCDNumber(self.centralwidget) self.lcdNumberSet.setGeometry(QtCore.QRect(400, 240, 121, 91)) font = QtGui.QFont() font.setPointSize(36) self.lcdNumberSet.setFont(font) self.lcdNumberSet.setObjectName("lcdNumberSet") self.label_4 = QtWidgets.QLabel(self.centralwidget) self.label_4.setGeometry(QtCore.QRect(410, 180, 151, 31)) font = QtGui.QFont() font.setPointSize(16) self.label_4.setFont(font) self.label_4.setObjectName("label_4") self.bntOnOFF = QtWidgets.QPushButton(self.centralwidget) self.bntOnOFF.setGeometry(QtCore.QRect(190, 280, 91, 41)) font = QtGui.QFont() font.setPointSize(16) self.bntOnOFF.setFont(font) self.bntOnOFF.setObjectName("bntOnOFF") self.labelData = QtWidgets.QLabel(self.centralwidget) self.labelData.setGeometry(QtCore.QRect(40, 320, 401, 31)) self.labelData.setObjectName("labelData") self.lcdNumberHum = QtWidgets.QLCDNumber(self.centralwidget) self.lcdNumberHum.setGeometry(QtCore.QRect(520, 80, 81, 71)) font = QtGui.QFont() font.setPointSize(36) self.lcdNumberHum.setFont(font) self.lcdNumberHum.setObjectName("lcdNumberHum") MainWindow.setCentralWidget(self.centralwidget) self.menubar = QtWidgets.QMenuBar(MainWindow) self.menubar.setGeometry(QtCore.QRect(0, 0, 800, 22)) self.menubar.setObjectName("menubar") MainWindow.setMenuBar(self.menubar) self.statusbar = QtWidgets.QStatusBar(MainWindow) self.statusbar.setObjectName("statusbar") MainWindow.setStatusBar(self.statusbar) self.retranslateUi(MainWindow) QtCore.QMetaObject.connectSlotsByName(MainWindow) def retranslateUi(self, MainWindow): _translate = QtCore.QCoreApplication.translate MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow")) self.bntConnect.setText(_translate("MainWindow", "连接")) self.labelLinkState.setText(_translate("MainWindow", "连接状态:")) self.pushButtonConnect.setText(_translate("MainWindow", "扫描蓝牙设备")) self.label_2.setText(_translate("MainWindow", "室温")) self.label_3.setText(_translate("MainWindow", "湿度")) self.bntADD.setText(_translate("MainWindow", "增大")) self.bntDec.setText(_translate("MainWindow", "减小")) self.label_4.setText(_translate("MainWindow", "温度设定")) self.bntOnOFF.setText(_translate("MainWindow", "关机")) self.labelData.setText(_translate("MainWindow", "接收到数据:"))
2、在main.py中,我们首先要安排PyQt5、并安装bleak蓝牙库:
具体的安装方法,大家可以自行百度。
3、主要代码介绍:
第一是蓝牙扫描,我当创建了一个类BluetoothScanner用于扫描蓝牙设备,扫描结束后,使用pyqtSignal异步发送给主函数,用于界面显示。
class BluetoothScanner(QThread): device_found_signal = pyqtSignal(dict) def __init__(self, parent=None): super().__init__(parent) def run(self): asyncio.run(self.scan()) async def scan(self): devices = await BleakScanner.discover() for device in devices: self.device_found_signal.emit({'name': device.name, 'address': device.address}) print({'name': device.name, 'address': device.address})
第二是,处理与蓝牙设备的连接、数据接收与发送:其代码如下:
class BluetoothReceiver(QThread): data_received_signal = pyqtSignal(dict) connection_status_signal = pyqtSignal(str) def __init__(self, device_address): super().__init__() print("init") self.device_address = device_address self.client = None self.receive_task = None self.connected = False print("init end address:" + str(self.device_address)) def run(self): print("recive run") asyncio.run(self.connect()) async def connect(self): try: # 基于MAC地址查找设备 device = await BleakScanner.find_device_by_address( self.device_address, cb=dict(use_bdaddr=False) # use_bdaddr判断是否是MOC系统 ) if device is None: print("could not find device with address '%s'",self.device_address) return # 事件定义 self.client = BleakClient(self.device_address,disconnected_callback=self.disconnect) await self.client.connect() await self.client.start_notify(par_notification_characteristic,self.receive_data) self.connected = True self.connection_status_signal.emit("已连接") while True: await self.client.write_gatt_char(par_write_characteristic, send_str) await asyncio.sleep(1.0) # 每休眠1秒发送一次 await self.client.write_gatt_char(par_write_characteristic, send_str1) await asyncio.sleep(1.0) # self.receive_task = asyncio.create_task(self.receive_data()) except Exception as e: self.connection_status_signal.emit(f"连接失败: {e}") async def receive_data(self,characteristic, data): try: Temp = data[0]*10 + data[1] Hum = data[2]*10 + data[3] self.data_received_signal.emit({'Temp': Temp, 'Hum': Hum}) except Exception as e: self.connection_status_signal.emit(f"接收数据出错: {e}") async def disconnect(self): if self.client: await self.client.disconnect() self.connected = False self.connection_status_signal.emit("已断开连接")
在此个类中,首先基于Mac地址来查找设备,查找到后使用start_notfy来创建异步连接,实现对服务的数据异步接收的recive_data回调函数的注册,如果接收到数据进行数据组装通过emit发送给GUI进行数据展示。
同时每隔1秒向服务端发送LED的开关指令,确保长连接。
3、GUI主程序:
class MyMainForm(QMainWindow, Ui_MainWindow): def __init__(self, parent=None): super(MyMainForm, self).__init__(parent) self.setupUi(self) self.pushButtonConnect.clicked.connect(self.start_async_task) self.bntConnect.clicked.connect(self.connect_device) def start_async_task(self): self.device_list.clear() selected_item = self.device_list.currentItem() self.labelLinkState.setText("正在执行扫描...") async_thread = BluetoothScanner(self) async_thread.finished.connect(self.async_task_finished) async_thread.device_found_signal.connect(self.add_device_to_list) async_thread.start() def async_task_finished(self): self.labelLinkState.setText("扫描完成") def add_device_to_list(self, device_info): self.device_list.addItem(f"{device_info['name']} - {device_info['address']}") def connect_device(self): selected_item = self.device_list.currentItem() if selected_item: device_address = selected_item.text().split(' - ')[1] self.thread1 = BluetoothReceiver(device_address) self.thread1.finished.connect(self.async_task_finished) self.thread1.connection_status_signal.connect(self.update_status_label) self.thread1.data_received_signal.connect(self.update_data_label) self.thread1.start() print(5) def update_data_label(self, data): try: self.labelData.setText(f"接收数据: {data}") self.lcdNumberTemp.display(data['Temp']) self.lcdNumberHum.display(data['Hum']) except Exception as e: print("接收数据解释错误:", str(e)) def update_status_label(self, status): self.labelLinkState.setText(f"状态: {status}") if __name__ == "__main__": # 固定的,PyQt5程序都需要QApplication对象。sys.argv是命令行参数列表,确保程序可以双击运行 app = QApplication(sys.argv) # 初始化 myWin = MyMainForm() # 将窗口控件显示在屏幕上 myWin.show() # 程序运行,sys.exit方法确保程序完整退出。 sys.exit(app.exec_())
在GUI类中,对应按键件事,创建对应的任务,并注册信号接收回调函数,对数据进行解析、展示。
【实现效果】
运行桌面程序后,首先点击扫描,我们的P2S服务会出现在列表框中,选中该行后按连接,在下面的状态提示中会提示连接,室温、湿度的LCD会实现显示开发板的温湿度,同时开发板也会接收到界面发出的指令,第隔一秒闪烁一次。
【总结】
PyQt5+bleak库,可以快速的创建桌面蓝牙应用程序,相关学习资料好找,python也提供强大的跨平台支持。