用Python和Bluez 5.50在Ubuntu 20.04上,手把手教你做个能被手机发现的BLE小设备
用Python和Bluez 5.50在Ubuntu 20.04上构建可被手机发现的BLE设备
在物联网和嵌入式开发领域,蓝牙低功耗(BLE)技术因其低功耗、低成本的特点而广受欢迎。然而,对于许多开发者来说,传统的BLE开发往往意味着复杂的底层协议和C语言环境,这让不少想要快速验证创意的开发者望而却步。本文将带你使用Python这一更易上手的语言,配合Bluez官方接口,在Ubuntu系统上从零构建一个能发出自定义广播包、并能被手机APP扫描到的虚拟BLE设备。
1. 环境准备与基础概念
在开始编码之前,我们需要确保开发环境配置正确,并理解几个关键概念。Ubuntu 20.04是一个理想的开发平台,因为它提供了稳定的Bluez支持,而Python则能让我们专注于逻辑而非底层细节。
1.1 系统与软件要求
首先确认你的系统满足以下要求:
- Ubuntu 20.04 LTS(物理机或虚拟机均可)
- Python 3.8或更高版本
- Bluez 5.50或更高版本
- D-Bus Python绑定
可以通过以下命令检查已安装的Bluez版本:
bluetoothctl --version
如果尚未安装Bluez,可以使用apt进行安装:
sudo apt update
sudo apt install bluez bluez-tools
1.2 BLE广播基础
BLE设备通过广播(Advertising)来宣告自己的存在。广播数据包可以包含多种信息:
- 设备名称:人类可读的标识符
- 服务UUID:设备提供的服务类型
- 制造商数据:自定义的厂商特定信息
- 发射功率:帮助设备估算距离
理解这些概念对后续构建广播包至关重要。广播包的最大长度为31字节,需要合理规划各字段的使用。
2. 配置D-Bus与Bluez接口
Bluez通过D-Bus接口暴露其功能,我们需要先建立与D-Bus系统的连接。D-Bus是Linux系统上的一种进程间通信机制,Bluez通过它提供了一套标准化的API。
2.1 初始化D-Bus连接
首先获取系统总线并检查Bluez服务是否可用:
import dbus
# 获取系统总线
bus = dbus.SystemBus()
# 检查Bluez服务是否运行
try:
bluez_service = 'org.bluez'
bus.get_object(bluez_service, '/')
print("Bluez服务可用")
except dbus.exceptions.DBusException as e:
print("Bluez服务不可用:", e)
exit(1)
2.2 查找BLE适配器
不是所有的蓝牙适配器都支持BLE,我们需要找到支持BLE的适配器:
BLUEZ_SERVICE_NAME = 'org.bluez'
DBUS_OM_IFACE = 'org.freedesktop.DBus.ObjectManager'
LE_ADVERTISING_MANAGER_IFACE = 'org.bluez.LEAdvertisingManager1'
def find_adapter(bus):
remote_om = dbus.Interface(
bus.get_object(BLUEZ_SERVICE_NAME, '/'),
DBUS_OM_IFACE)
objects = remote_om.GetManagedObjects()
for path, interfaces in objects.items():
if LE_ADVERTISING_MANAGER_IFACE in interfaces:
return path
return None
adapter = find_adapter(bus)
if not adapter:
print("未找到支持BLE广告的适配器")
exit(1)
3. 构建BLE广播包
现在我们可以开始构建自定义的广播包了。Bluez通过org.bluez.LEAdvertisement1接口来定义广播内容。
3.1 创建Advertisement类
我们需要继承Bluez提供的Advertisement基类来实现自定义广播:
from gi.repository import GObject
from dbus.mainloop.glib import DBusGMainLoop
import dbus.service
class CustomAdvertisement(dbus.service.Object):
PATH_BASE = '/org/bluez/example/advertisement'
def __init__(self, bus, index):
self.path = self.PATH_BASE + str(index)
self.bus = bus
self.ad_type = 'peripheral'
self.local_name = 'PythonBLE'
self.service_uuids = ['180A'] # 设备信息服务
self.manufacturer_data = {0xFFFF: [0x01, 0x02, 0x03, 0x04]}
self.include_tx_power = True
dbus.service.Object.__init__(self, bus, self.path)
def get_properties(self):
properties = dict()
properties['Type'] = self.ad_type
if self.local_name is not None:
properties['LocalName'] = dbus.String(self.local_name)
if self.service_uuids is not None:
properties['ServiceUUIDs'] = dbus.Array(self.service_uuids,
signature='s')
if self.manufacturer_data is not None:
properties['ManufacturerData'] = dbus.Dictionary(
self.manufacturer_data, signature='qv')
if self.include_tx_power:
properties['Includes'] = dbus.Array(["tx-power"], signature='s')
return {LE_ADVERTISING_MANAGER_IFACE: properties}
@dbus.service.method(DBUS_PROP_IFACE,
in_signature='s',
out_signature='a{sv}')
def GetAll(self, interface):
if interface != LE_ADVERTISING_MANAGER_IFACE:
raise InvalidArgsException()
return self.get_properties()[LE_ADVERTISING_MANAGER_IFACE]
3.2 广播包字段详解
广播包中可以包含多种类型的字段,每种都有特定用途:
| 字段类型 | 说明 | 示例值 |
|---|---|---|
| LocalName | 设备名称 | "MyBLEDevice" |
| ServiceUUIDs | 提供的服务UUID列表 | ["180A", "180F"] |
| ManufacturerData | 厂商自定义数据 | {0xFFFF: [0x01,0x02]} |
| Includes | 包含的额外信息 | ["tx-power"] |
合理组合这些字段可以让你的设备提供丰富的信息,同时保持广播包在31字节的限制内。
4. 注册并启动广播
有了广播包定义后,我们需要将其注册到Bluez的广告管理器中。
4.1 注册广告
def register_ad_cb():
print("广告注册成功")
def register_ad_error_cb(error):
print("广告注册失败:", str(error))
mainloop.quit()
# 获取广告管理器接口
ad_manager = dbus.Interface(
bus.get_object(BLUEZ_SERVICE_NAME, adapter),
LE_ADVERTISING_MANAGER_IFACE)
# 创建并注册广告
advertisement = CustomAdvertisement(bus, 0)
ad_manager.RegisterAdvertisement(
advertisement.get_path(),
{},
reply_handler=register_ad_cb,
error_handler=register_ad_error_cb)
4.2 启动事件循环
最后,我们需要启动GLib事件循环来处理D-Bus消息:
# 初始化主循环
DBusGMainLoop(set_as_default=True)
mainloop = GLib.MainLoop()
try:
print("BLE广播已启动,使用Ctrl+C停止")
mainloop.run()
except KeyboardInterrupt:
mainloop.quit()
print("\n广播已停止")
5. 验证与调试
完成代码编写后,我们需要验证设备是否能被正确发现。
5.1 使用nRF Connect扫描
在Android设备上安装nRF Connect应用,然后执行以下步骤:
- 打开nRF Connect应用
- 点击"扫描"按钮
- 查找名为"PythonBLE"的设备
- 点击设备查看详细广播数据
你应该能看到我们定义的所有广播字段,包括设备名称、服务UUID和制造商数据。
5.2 常见问题排查
如果设备未被发现,可以检查以下几点:
- 蓝牙适配器状态:确保蓝牙已启用且未被其他进程占用
- 广播权限:某些系统可能需要root权限来注册广告
- D-Bus权限:检查Python进程是否有权限访问系统D-Bus
- 广告间隔:默认间隔可能较长,尝试等待30秒以上
可以通过以下命令检查蓝牙适配器状态:
hciconfig -a
如果遇到权限问题,可以尝试以root身份运行Python脚本:
sudo python3 ble_advertiser.py
6. 进阶自定义选项
基础广播工作后,我们可以进一步定制设备行为。
6.1 调整广播参数
通过org.bluez.LEAdvertisingManager1接口可以设置广播参数:
advertising_properties = {
'Type': 'peripheral',
'Interval': dbus.UInt32(200), # 广播间隔(ms)
'Timeout': dbus.UInt32(0), # 0表示不超时
'Discoverable': dbus.Boolean(True)
}
ad_manager.RegisterAdvertisement(
advertisement.get_path(),
advertising_properties,
reply_handler=register_ad_cb,
error_handler=register_ad_error_cb)
6.2 动态更新广播数据
Advertisement对象创建后,可以动态更新其属性:
def update_advertisement(adv, new_name, new_data):
adv.local_name = new_name
adv.manufacturer_data = new_data
# 需要重新注册广告才能使更改生效
ad_manager.UnregisterAdvertisement(adv.get_path())
ad_manager.RegisterAdvertisement(
adv.get_path(),
{},
reply_handler=register_ad_cb,
error_handler=register_ad_error_cb)
这种技术可以用于实现动态变化的设备名称或数据,而无需重启整个应用。
7. 性能优化与最佳实践
当你的BLE设备需要长时间运行时,考虑以下优化建议:
- 广播间隔平衡:较短的间隔提高发现概率但增加功耗
- 数据精简:确保广播包不超过31字节限制
- 错误处理:妥善处理D-Bus调用可能抛出的异常
- 资源清理:程序退出时注销广告
一个健壮的生产级实现应该包含这些元素:
import signal
def shutdown(signum, frame):
print("\n正在清理资源...")
ad_manager.UnregisterAdvertisement(advertisement.get_path())
mainloop.quit()
signal.signal(signal.SIGINT, shutdown)
signal.signal(signal.SIGTERM, shutdown)
在实际项目中,我发现最常遇到的问题是与权限相关的D-Bus错误。确保你的用户属于bluetooth组可以避免大多数此类问题:
sudo usermod -aG bluetooth $USER
另一个实用技巧是在开发过程中启用Bluez的调试输出,这可以帮助理解底层发生了什么:
sudo systemctl stop bluetooth
sudo /usr/lib/bluetooth/bluetoothd -d
更多推荐


所有评论(0)