蓝牙上位机开发指南
蓝牙上位机开发指南
蓝牙技术在现代设备通信中扮演着重要角色,从无线耳机到物联网设备都有广泛应用。本文将介绍如何使用Python开发一个功能完整的蓝牙上位机应用,包括设备扫描、连接和数据传输等核心功能。
一、蓝牙上位机开发
1.1 PyBluez2蓝牙工具使用
PyBluez2是一个跨平台的Python蓝牙库,支持Windows和Linux系统。首先需要安装这个库:
pip install pybluez2
基本导入和初始化:
import bluetooth
from bluetooth import *# 检查蓝牙适配器是否可用
def check_bluetooth():try:devices = bluetooth.discover_devices(lookup_names=True)print("找到 {} 个设备".format(len(devices)))return Trueexcept Exception as e:print("蓝牙适配器不可用:", e)return False
1.2 界面设置
使用Tkinter构建简单的GUI界面:
import tkinter as tk
from tkinter import ttk, scrolledtextclass BluetoothApp:def __init__(self, root):self.root = rootself.root.title("蓝牙上位机")self.root.geometry("600x400")self.setup_ui()def setup_ui(self):# 扫描按钮self.scan_btn = ttk.Button(self.root, text="扫描设备", command=self.scan_devices)self.scan_btn.pack(pady=10)# 设备列表self.device_tree = ttk.Treeview(self.root, columns=("name", "address"), show="headings")self.device_tree.heading("name", text="设备名称")self.device_tree.heading("address", text="MAC地址")self.device_tree.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)# 连接按钮self.connect_btn = ttk.Button(self.root, text="连接设备", command=self.connect_device)self.connect_btn.pack(pady=5)# 数据发送区域self.send_frame = ttk.Frame(self.root)self.send_frame.pack(fill=tk.X, padx=10, pady=5)self.data_entry = ttk.Entry(self.send_frame)self.data_entry.pack(side=tk.LEFT, fill=tk.X, expand=True)self.send_btn = ttk.Button(self.send_frame, text="发送", command=self.send_data)self.send_btn.pack(side=tk.RIGHT)# 接收数据显示区域self.recv_text = scrolledtext.ScrolledText(self.root, height=10)self.recv_text.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
1.3 子线程扫描蓝牙设备
为了避免界面冻结,使用子线程进行蓝牙设备扫描:
import threading
from threading import Threadclass BluetoothApp:# ... 之前的代码 ...def scan_devices(self):self.scan_btn.config(state=tk.DISABLED)self.device_tree.delete(*self.device_tree.get_children())# 在子线程中扫描设备scan_thread = Thread(target=self._scan_thread)scan_thread.daemon = Truescan_thread.start()def _scan_thread(self):try:devices = bluetooth.discover_devices(lookup_names=True, duration=8)# 回到主线程更新UIself.root.after(0, self._update_device_list, devices)except Exception as e:self.root.after(0, self._scan_error, str(e))def _update_device_list(self, devices):for addr, name in devices:self.device_tree.insert("", tk.END, values=(name, addr))self.scan_btn.config(state=tk.NORMAL)def _scan_error(self, error_msg):self.recv_text.insert(tk.END, f"扫描错误: {error_msg}\n")self.scan_btn.config(state=tk.NORMAL)
1.4 连接蓝牙设备
建立蓝牙连接并处理通信:
class BluetoothApp:def __init__(self, root):# ... 之前的初始化代码 ...self.socket = Noneself.connected = Falsedef connect_device(self):selection = self.device_tree.selection()if not selection:returnitem = self.device_tree.item(selection[0])addr = item['values'][1]connect_thread = Thread(target=self._connect_thread, args=(addr,))connect_thread.daemon = Trueconnect_thread.start()def _connect_thread(self, addr):try:self.socket = bluetooth.BluetoothSocket(bluetooth.RFCOMM)self.socket.connect((addr, 1))self.connected = Trueself.root.after(0, self._connection_success)# 启动数据接收线程recv_thread = Thread(target=self._recv_data_thread)recv_thread.daemon = Truerecv_thread.start()except Exception as e:self.root.after(0, self._connection_failed, str(e))def _connection_success(self):self.recv_text.insert(tk.END, "连接成功!\n")self.connect_btn.config(text="断开连接", command=self.disconnect_device)def _connection_failed(self, error_msg):self.recv_text.insert(tk.END, f"连接失败: {error_msg}\n")def disconnect_device(self):if self.socket:self.socket.close()self.socket = Noneself.connected = Falseself.connect_btn.config(text="连接设备", command=self.connect_device)self.recv_text.insert(tk.END, "连接已断开\n")
1.5 发送数据
实现数据发送和接收功能:
class BluetoothApp:# ... 之前的代码 ...def send_data(self):if not self.connected:self.recv_text.insert(tk.END, "未连接设备,无法发送数据\n")returndata = self.data_entry.get()if not data:returntry:self.socket.send(data.encode('utf-8'))self.recv_text.insert(tk.END, f"发送: {data}\n")self.data_entry.delete(0, tk.END)except Exception as e:self.recv_text.insert(tk.END, f"发送失败: {e}\n")self.connected = Falseself.connect_btn.config(text="连接设备", command=self.connect_device)def _recv_data_thread(self):while self.connected:try:data = self.socket.recv(1024)if data:self.root.after(0, self._display_recv_data, data.decode('utf-8'))except:if self.connected:self.root.after(0, self._connection_lost)breakdef _display_recv_data(self, data):self.recv_text.insert(tk.END, f"接收: {data}\n")self.recv_text.see(tk.END)def _connection_lost(self):self.recv_text.insert(tk.END, "连接已断开\n")self.connected = Falseself.connect_btn.config(text="连接设备", command=self.connect_device)
二、PyInstaller打包
将Python应用打包成可执行文件,方便分发和使用:
2.1 安装PyInstaller
pip install pyinstaller
2.2 创建打包配置文件
创建spec
文件或直接使用命令行参数。创建一个build.spec
文件:
# -*- mode: python ; coding: utf-8 -*-block_cipher = Nonea = Analysis(['bluetooth_app.py'],pathex=[],binaries=[],datas=[],hiddenimports=[],hookspath=[],hooksconfig={},runtime_hooks=[],excludes=[],win_no_prefer_redirects=False,win_private_assemblies=False,cipher=block_cipher,noarchive=False,
)
pyz = PYZ(a.pure, a.zipped_files, cipher=block_cipher)exe = EXE(pyz,a.scripts,a.binaries,a.zipfiles,a.datas,[],name='BluetoothApp',debug=False,bootloader_ignore_signals=False,strip=False,upx=True,upx_exclude=[],runtime_tmpdir=None,console=False, # 设置为True可以显示控制台窗口icon='app_icon.ico', # 可选:添加应用图标
)
第二种
import PyInstaller.__main__# 这里的是不需要打包进去的三方模块,可以减少软件包的体积,这里只是举个例子
excluded_modules = ["pytest","selenium",
]append_string = []
for mod in excluded_modules:append_string += [f'--exclude-module={mod}']PyInstaller.__main__.run(['-y', # 如果dist文件夹内已经存在生成文件,则不询问用户,直接覆盖'img_test.py', # 主程序入口# '--onedir', # -D 文件夹'--onefile', # -F 单文件# '--nowindowed', # -c 无窗口'--windowed', # -w 有窗口'-n', 'utils', # -n 生成的文件名,默认是程序的名称'-i', 'res/logo.ico', # -i 生成的图标,默认是程序的图标'--add-data=res;res', # 第一个 res 是当前工作目录下的资源文件夹路径# 第二个 res 是打包后资源文件在应用程序中的相对路径*append_string
])
2.3 执行打包命令
pyinstaller build.spec
或者直接使用命令行:
pyinstaller --onefile --windowed --name BluetoothApp bluetooth_app.py
2.4 常用PyInstaller选项
--onefile
: 打包成单个可执行文件--windowed
: 不显示控制台窗口(GUI应用)--console
: 显示控制台窗口--icon=app.ico
: 设置应用图标--add-data "source;destination"
: 添加额外文件--hidden-import modulename
: 添加隐藏导入的模块