让Python直连蓝牙设备:Bleak异步BLE客户端完整实战指南

让Python直连蓝牙设备:Bleak异步BLE客户端完整实战指南

【免费下载链接】bleakA cross platform Bluetooth Low Energy Client for Python using asyncio项目地址: https://gitcode.com/gh_mirrors/bl/bleak

想象这样一个场景:你手头有一个温湿度传感器、一个智能手环,或者一台支持BLE的心率带,它们都在不停广播数据,而你只想用Python把它们接进自己的程序里。翻遍资料发现,Windows、macOS、Linux上的蓝牙API各不相同,写一套代码到处改,实在让人头疼。

Bleak(Bluetooth Low Energy platform Agnostic Klient)正是为了解决这个痛点而生的Python异步BLE客户端库。它基于asyncio构建,把底层蓝牙协议封装成统一API,让你在Windows、macOS、Linux和Android上都能用同一套代码完成设备扫描、连接、读写和订阅通知。本文将带你从环境搭建出发,一步步完成"扫描→连接→读取→通知"的完整实战,并分享排障经验与常见陷阱,帮助你快速上手Bleak进行BLE开发。

一、为什么你的项目需要一个统一的BLE接入层

在Bleak出现之前,Python生态里接入BLE设备通常要走几条岔路:Linux依赖BlueZ的dbus接口,Windows依赖WinRT API,macOS则必须走CoreBluetooth。三者协议不同、回调风格不同、连UUID的表示都有差异,跨平台意味着三份代码、三倍的维护成本。

Bleak把这一切抽象为几个直观的核心对象:

对象职责
BleakScanner扫描周边设备、解析广播数据
BleakClient连接设备、读写特征、订阅通知
BLEDevice描述一个被发现的设备(地址、名称、RSSI等)
BleakGATTService/BleakGATTCharacteristic描述设备暴露的服务与特征结构

基于asyncio的设计让Bleak天然适合高并发场景:你可以同时维护多个设备连接,也可以把扫描与业务逻辑并行起来,而无需引入额外线程。这些能力都来自官方源码中的bleak/backends/目录——每个操作系统对应一个子目录(如bluezdbuscorebluetoothwinrtp4android),接口统一、实现各异,这正是"平台无关"承诺的落地之处。

平台支持一览:Windows 11(版本22000及以上)、Linux(需BlueZ ≥ 5.55)、macOS(10.15及以上,走CoreBluetooth)、Android(兼容python-for-android)。

二、三步完成环境配置

Bleak对Python版本要求为3.10及以上,安装方式非常直接。

第一步:确认Python版本

$ python --version

如果版本低于3.10,请先升级解释器,否则无法安装。

第二步:通过pip安装Bleak

$ pip install bleak

这是官方推荐的安装方式,会自动拉取最新的稳定版本。若你在iOS的Pythonista环境中使用,请改用以下命令(会一并安装bleak-pythonista配套包):

$ pip install bleak[pythonista]

第三步:验证安装是否成功

$ python -c "import bleak; print(bleak.__version__)"

看到版本号输出即表示环境就绪。想要尝试尚未发布的最新开发特性,也可以直接从项目的develop分支安装,体验先行但稳定性略逊于稳定版。

三、真实场景实战:让传感器数据流动起来

3.1 第一次扫描:看清你周围有哪些BLE设备

拿到新库,第一件事自然是"看看周围有什么"。BleakScanner提供了最简洁的扫描入口:

import asyncio from bleak import BleakScanner async def main(): # 扫描5秒,返回发现的所有设备 devices = await BleakScanner.discover(timeout=5.0) for d in devices: print(f"{d.address} -> {d.name}") asyncio.run(main())

如果你还想拿到广播数据(RSSI信号强度、厂商数据、广播的服务UUID等),可以把return_adv打开:

devices = await BleakScanner.discover(timeout=5.0, return_adv=True) for d, adv in devices.values(): print(d.address, d.name, adv.rssi, adv.service_uuids)

这里advAdvertisementData对象,常用字段包括local_name(广播名)、rssi(信号强度)、manufacturer_data(厂商自定义数据)和service_uuids(广播中携带的服务UUID)。这些信息对后续"按条件筛选设备"非常关键。

3.2 精准定位:按地址、名称或服务UUID找设备

真实项目中设备往往不止一台,盲目连接很容易连错对象。BleakScanner提供了三种定位方式:

from bleak import BleakScanner # 方式一:按蓝牙地址精确定位 device = await BleakScanner.find_device_by_address("24:71:89:CC:09:05") # 方式二:按广播名称模糊查找(最多等10秒) device = await BleakScanner.find_device_by_name("MySensor", timeout=10.0) # 方式三:自定义过滤函数(最灵活) def is_uart(device, adv): return "6E400001-B5A3-F393-E0A9-E50E24DCCA9E".lower() in adv.service_uuids device = await BleakScanner.find_device_by_filter(is_uart)

第三种方式在对接Nordic UART服务这类设备时特别好用——你不需要关心设备叫什么名字,只要它广播的服务UUID匹配,就直接锁定目标。

3.3 建立连接并读取设备数据

定位到设备后,就可以建立连接读取数据了。官方推荐的写法是异步上下文管理器,它能自动处理连接与断连:

import asyncio from bleak import BleakClient # 设备地址与"型号"特征的UUID(蓝牙SIG标准) ADDRESS = "24:71:89:CC:09:05" MODEL_NBR_UUID = "2A24" async def main(): async with BleakClient(ADDRESS) as client: # 读取特征值,返回bytearray raw = await client.read_gatt_char(MODEL_NBR_UUID) print(f"设备型号: {raw.decode()}") asyncio.run(main())

如果你需要精细控制连接生命周期(比如记录异常、手动决定何时断开),也可以不使用上下文管理器:

async def main(): client = BleakClient(ADDRESS) try: await client.connect() raw = await client.read_gatt_char(MODEL_NBR_UUID) print(f"设备型号: {raw.decode()}") except Exception as e: print(f"操作失败: {e}") finally: await client.disconnect()

读取之外,写入同样简单:await client.write_gatt_char(uuid, data)。部分特征还支持"无响应写入"(write-without-response),吞吐量更高,适用于大量数据下行场景——具体支持哪些属性,可以通过特征对象的properties查看。

3.4 订阅通知:被动接收设备推送的数据

很多传感器(心率带、温湿度计)并不会等你来读,而是持续主动推送数据。这时需要用start_notify注册回调:

import asyncio from bleak import BleakClient from bleak.backends.characteristic import BleakGATTCharacteristic HEART_RATE_UUID = "2A37" # 心率测量特征 def on_data(characteristic: BleakGATTCharacteristic, data: bytearray): """设备每推送一次数据,就会回调一次。""" print(f"来自 {characteristic.description}: {data.hex()}") async def main(): async with BleakClient("24:71:89:CC:09:05") as client: # 开启通知订阅 await client.start_notify(HEART_RATE_UUID, on_data) # 持续接收5秒 await asyncio.sleep(5.0) # 记得关闭订阅 await client.stop_notify(HEART_RATE_UUID) asyncio.run(main())

💡经验之谈start_notify的回调运行在事件循环里,回调内不要做耗时操作(如写文件、发HTTP请求),否则会阻塞整个循环。需要耗时处理时,把数据丢进asyncio.Queue,另起协程消费即可——参考仓库中的examples/async_callback_with_queue.py示例。

3.5 进阶:同时管理多台设备

Bleak的异步特性让"多设备并发"几乎零成本。下面的代码扫描周边设备后,逐个连接并打印其服务结构:

import asyncio from bleak import BleakClient, BleakScanner async def survey_all(): devices = await BleakScanner.discover(timeout=5.0) for d in devices: print(f"\n===== {d.name} ({d.address}) =====") try: async with BleakClient(d) as client: for service in client.services: print(f"[服务] {service}") for char in service.characteristics: print(f" [特征] {char.uuid} 属性: {','.join(char.properties)}") except Exception as e: print(f"连接失败: {e}") asyncio.run(survey_all())

这段代码等价于一个极简的"服务浏览器"——仓库中的examples/service_explorer.py提供了带参数解析、支持配对和调试日志的完整版本,值得直接阅读。当你想了解一个陌生设备内部到底长什么样时,跑一遍它准没错。

四、跨平台注意事项:权限与系统差异

4.1 macOS:先给终端蓝牙权限

在macOS上,应用首次访问蓝牙时会触发权限弹窗;如果错过了,或想检查已授权的应用,需要进入系统偏好设置的"安全性与隐私 → 隐私 → 蓝牙"页面确认。关键点:授权对象不是你的Python脚本,而是启动它的宿主程序——终端、iTerm、PyCharm等。

如果你在macOS上通过PyCharm运行代码却始终扫描不到设备,大概率就是PyCharm本身没被勾选。此外,macOS上设备地址有时会以UUID形式返回,需要按地址匹配时记得在扫描参数中处理use_bdaddr选项。

4.2 Windows:管理操作需提权

日常读写通常无需特殊权限,但如果要执行蓝牙数据包捕获等底层操作,必须以管理员身份运行命令提示符或PowerShell

在Windows上,推荐使用WinRT后端(Bleak默认自动选择),同时建议保持系统版本在Windows 11 22000以上,以获得最稳定的行为。

4.3 Linux:检查BlueZ版本

Linux后端依赖BlueZ守护进程,版本过低会导致连接异常。可用以下命令检查:

$ bluetoothctl --version

低于5.55请先升级BlueZ。多数发行版更新后即可满足要求。

五、新手最容易踩的5个坑

坑1:把脚本命名为bleak.py🚫

这是官方README里特别强调过的陷阱。脚本一旦命名为bleak.py,Python导入时会把自己当成库包,引发循环导入错误。请务必换个名字,比如demo_ble.py

坑2:多次调用asyncio.run()

Bleak要求整个程序只调用一次asyncio.run(),因为后端需要保持同一个事件循环。下面的写法虽然语法上没问题,却会运行时报错:

# ❌ 错误示范 device = asyncio.run(scan()) asyncio.run(connect(device))

正确做法是把所有逻辑收进一个入口协程:

# ✅ 正确示范 async def main(): device = await scan() await connect(device) asyncio.run(main())

坑3:忽视UUID的大小写

蓝牙UUID不区分大小写,但部分后端在比较时做了小写归一化,而你自己写的过滤条件可能混入了大写。建议统一用.lower()处理后再比较,避免"明明在广播却匹配不上"的怪问题。

坑4:Wi-Fi与蓝牙互相干扰

在树莓派等同时集成Wi-Fi和蓝牙的设备上,两者共用天线,扫描或连接可能频繁失败。可先尝试关闭Wi-Fi验证是否为干扰问题:

$ sudo rfkill block wlan

如果确认是干扰,改用USB蓝牙适配器是最稳妥的方案。

坑5:被操作系统缓存的老服务信息误导

开发自己的BLE固件时,如果改了服务结构却发现Python侧读到的还是旧数据,很可能是操作系统缓存了旧GATT信息。Linux上清除方式如下:

$ bluetoothctl -- remove XX:XX:XX:XX:XX:XX # 若BlueZ低于5.62,还需手动删除GATT缓存 $ sudo rm "/var/lib/bluetooth/YY:YY:YY:YY:YY:YY/cache/XX:XX:XX:XX:XX:XX"

其中XX:XX:XX:XX:XX:XX是设备地址,YY:YY:YY:YY:YY:YY是本机适配器地址。清除后重新扫描连接即可。

六、生态与延伸:从示例到生产级应用

Bleak官方仓库的examples/目录是一份被低估的学习宝藏,建议按以下顺序精读:

示例文件核心知识点
examples/discover.py扫描与广播数据解析
examples/service_explorer.py遍历服务/特征/描述符,理解设备结构
examples/enable_notifications.py通知订阅的标准写法
examples/uart_service.py与Nordic UART设备实现双向通信
examples/two_devices.py多设备并发管理
examples/async_callback_with_queue.py回调+队列的异步模式

其中uart_service.py尤其值得研读:它用find_device_by_filter按服务UUID锁定设备、用disconnected_callback感知断线、用start_notify接收下行数据,几乎涵盖了BLE串口通信的全部要点,是"读一行、懂一行"的典型范例。

在实际项目中,Bleak通常不会单独出现:物联网网关里,它承担数据采集,再通过MQTT把数据转发到云端;在自动化脚本里,它可以与paho-mqttInfluxDB客户端等组合成完整的链路。由于Bleak是纯asyncio实现,整条链路都可以保持异步风格,避免线程切换带来的心智负担。

七、现在就开始你的BLE之旅

回看整篇文章,Bleak的核心价值可以浓缩为一句话:一套异步API,四个主流平台。从安装、扫描、连接到读写与通知,你需要的代码不超过二十行;从简单demo到多设备并发的生产场景,它的生态示例也能帮你少走很多弯路。

如果你正准备把传感器接入Python,或正在为跨平台蓝牙开发发愁,不妨现在就动手:

$ pip install bleak

然后运行下面这段代码,看看你能发现多少台周边设备:

import asyncio from bleak import BleakScanner async def main(): devices = await BleakScanner.discover(timeout=5.0) print(f"发现 {len(devices)} 台设备:") for d in devices: print(f" - {d.name or '(未命名)'} @ {d.address}") asyncio.run(main())

如果扫描结果里有你熟悉的设备,下一步就是连接它、读取它的特征值、订阅它的通知——你会发现,蓝牙低功耗开发从未如此简单。去试试吧,把那些孤零零飘在空中的数据,变成你程序里流动的信息流。🚀

【免费下载链接】bleakA cross platform Bluetooth Low Energy Client for Python using asyncio项目地址: https://gitcode.com/gh_mirrors/bl/bleak

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考