让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/目录——每个操作系统对应一个子目录(如bluezdbus、corebluetooth、winrt、p4android),接口统一、实现各异,这正是"平台无关"承诺的落地之处。
✅平台支持一览: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)这里adv是AdvertisementData对象,常用字段包括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-mqtt、InfluxDB客户端等组合成完整的链路。由于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),仅供参考
