用Python和Bluez 5.50在Ubuntu 20.04上打造可发现的BLE广播设备

在物联网和智能硬件快速发展的今天,蓝牙低功耗(BLE)技术因其低功耗、低成本的特点,成为连接智能设备的首选方案之一。想象一下,你正在开发一个智能温湿度传感器,或者一个可穿戴健康监测设备,如何让用户的手机能够快速发现并识别你的设备?BLE广播正是实现这一功能的核心技术。

本文将带你从零开始,在Ubuntu 20.04系统上使用Python和Bluez 5.50蓝牙协议栈,创建一个能够被手机发现的BLE广播设备。不同于复杂的理论讲解,我们将聚焦于实际操作,确保你能够在30分钟内完成从环境搭建到手机验证的完整流程。无论你是物联网开发者、Python爱好者,还是对BLE技术好奇的硬件工程师,这篇指南都将为你提供清晰的实践路径。

1. 环境准备与工具安装

在开始编码之前,我们需要确保开发环境已经正确配置。Ubuntu 20.04是一个理想的开发平台,因为它提供了对Bluez蓝牙协议栈的良好支持。以下是需要准备的软件和工具:

  • 操作系统:Ubuntu 20.04 LTS(建议使用实体机或虚拟机)
  • 蓝牙适配器:确保你的电脑配备了兼容的蓝牙硬件(USB蓝牙适配器也可)
  • Python环境:Python 3.8或更高版本
  • 必要软件包:Bluez 5.50及相关开发工具

打开终端,执行以下命令安装所需依赖:

sudo apt update
sudo apt install -y bluez libbluetooth-dev python3-dev python3-pip
sudo pip3 install dbus-python pygobject

安装完成后,验证Bluez版本:

bluetoothctl --version

你应该看到类似bluetoothctl: 5.50的输出。如果版本不一致,可能需要手动编译安装Bluez 5.50,但这超出了本文的范围。

提示:如果遇到权限问题,请确保你的用户账户已加入lpbluetooth用户组:

sudo usermod -a -G lp,bluetooth $USER

2. BLE广播基础与工作原理

在深入代码之前,理解BLE广播的基本概念至关重要。BLE设备通过广播通道向外发送数据包,这些数据包可以包含设备名称、服务UUID、制造商特定数据等信息。手机等扫描设备会监听这些广播,从而发现附近的BLE设备。

一个典型的BLE广播包包含以下部分:

字段描述示例
设备名称可读的设备标识符"MySensor"
服务UUID设备提供的服务标识"180A" (设备信息服务)
制造商数据厂商自定义数据0xFFFF + [0x01,0x02]
发射功率信号强度指示-70dBm
连接标志指示设备是否可连接0x01

广播模式主要分为三种:

  • 可连接广播:允许设备建立BLE连接
  • 不可连接广播:仅发送数据,不允许连接
  • 扫描响应:当扫描设备请求更多信息时的额外响应

我们的示例将创建一个不可连接的广播设备,专注于广播数据的发送。

3. 编写Python广播程序

现在,让我们创建一个完整的Python脚本,实现BLE广播功能。我们将使用dbus-python库与Bluez交互。创建一个名为ble_advertiser.py的文件,内容如下:

#!/usr/bin/env python3

import dbus
import dbus.mainloop.glib
from gi.repository import GLib
import sys

# DBus接口定义
BLUEZ_SERVICE_NAME = 'org.bluez'
DBUS_OM_IFACE = 'org.freedesktop.DBus.ObjectManager'
DBUS_PROP_IFACE = 'org.freedesktop.DBus.Properties'
LE_ADVERTISING_MANAGER_IFACE = 'org.bluez.LEAdvertisingManager1'
LE_ADVERTISEMENT_IFACE = 'org.bluez.LEAdvertisement1'

class Advertisement(dbus.service.Object):
    def __init__(self, bus, index, advertising_type):
        self.path = '/org/bluez/example/advertisement' + str(index)
        self.bus = bus
        self.ad_type = advertising_type
        self.service_uuids = None
        self.manufacturer_data = None
        self.local_name = None
        self.include_tx_power = False
        dbus.service.Object.__init__(self, bus, self.path)

    def add_service_uuid(self, uuid):
        if not self.service_uuids:
            self.service_uuids = []
        self.service_uuids.append(uuid)

    def add_manufacturer_data(self, manufacturer_id, data):
        if not self.manufacturer_data:
            self.manufacturer_data = dbus.Dictionary({}, signature='qv')
        self.manufacturer_data[manufacturer_id] = dbus.Array(data, signature='y')

    def add_local_name(self, name):
        self.local_name = name

    @dbus.service.method(DBUS_PROP_IFACE, in_signature='s', out_signature='a{sv}')
    def GetAll(self, interface):
        if interface != LE_ADVERTISEMENT_IFACE:
            raise InvalidArgsException()
        
        properties = dict()
        properties['Type'] = self.ad_type
        
        if self.service_uuids:
            properties['ServiceUUIDs'] = dbus.Array(self.service_uuids, signature='s')
        if self.manufacturer_data:
            properties['ManufacturerData'] = self.manufacturer_data
        if self.local_name:
            properties['LocalName'] = dbus.String(self.local_name)
        if self.include_tx_power:
            properties['Includes'] = dbus.Array(['tx-power'], signature='s')
        
        return properties

def register_ad_cb():
    print("Advertisement registered successfully")

def register_ad_error_cb(error):
    print("Failed to register advertisement: " + str(error))
    sys.exit(1)

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

def main():
    dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
    bus = dbus.SystemBus()
    
    adapter = find_adapter(bus)
    if not adapter:
        print("找不到支持BLE广播的蓝牙适配器")
        return
    
    # 开启蓝牙适配器
    adapter_props = dbus.Interface(bus.get_object(BLUEZ_SERVICE_NAME, adapter), DBUS_PROP_IFACE)
    adapter_props.Set("org.bluez.Adapter1", "Powered", dbus.Boolean(1))
    
    # 创建广告对象
    ad_manager = dbus.Interface(bus.get_object(BLUEZ_SERVICE_NAME, adapter), LE_ADVERTISING_MANAGER_IFACE)
    
    advertisement = Advertisement(bus, 0, "peripheral")
    advertisement.add_service_uuid("180A")  # 设备信息服务
    advertisement.add_manufacturer_data(0xFFFF, [0x01, 0x02, 0x03, 0x04])
    advertisement.add_local_name("PythonBLE")
    advertisement.include_tx_power = True
    
    # 注册广告
    ad_manager.RegisterAdvertisement(advertisement.get_path(), {},
                                    reply_handler=register_ad_cb,
                                    error_handler=register_ad_error_cb)
    
    try:
        # 启动事件循环
        mainloop = GLib.MainLoop()
        print("BLE广播已启动,按Ctrl+C停止")
        mainloop.run()
    except KeyboardInterrupt:
        mainloop.quit()

if __name__ == "__main__":
    main()

让我们分解这段代码的关键部分:

  1. Advertisement类:封装了BLE广播数据,包括:

    • 服务UUID(add_service_uuid方法)
    • 制造商数据(add_manufacturer_data方法)
    • 设备名称(add_local_name方法)
  2. DBus交互

    • find_adapter函数查找可用的蓝牙适配器
    • adapter_props.Set启用蓝牙适配器
    • ad_manager.RegisterAdvertisement注册广播数据
  3. 主循环

    • GLib.MainLoop()保持程序运行,持续发送广播

注意:运行此脚本需要root权限或蓝牙组权限,建议使用:

sudo python3 ble_advertiser.py

4. 使用手机验证广播设备

程序运行后,我们需要验证手机是否能够发现我们的BLE设备。以下是使用nRF Connect(一款流行的BLE调试工具)的步骤:

  1. 在手机应用商店下载安装nRF Connect
  2. 打开应用,确保手机蓝牙已开启
  3. 点击"Scan"按钮开始扫描附近的BLE设备
  4. 你应该在设备列表中看到名为"PythonBLE"的设备
  5. 点击设备查看详细信息,确认广播数据与代码设置一致

在nRF Connect中,你应该能看到以下信息:

  • 设备名称:PythonBLE
  • 服务UUID:180A
  • 制造商数据:FFFF -> [01,02,03,04]
  • TX Power:显示信号强度值

如果一切正常,恭喜你!你已经成功创建了一个可被手机发现的BLE广播设备。这个基础框架可以扩展为各种实际应用,比如:

  • 智能信标(iBeacon/Eddystone)
  • 设备状态指示灯
  • 简单的数据广播传感器

5. 高级自定义与故障排除

掌握了基础广播功能后,你可能希望进一步定制广播数据或解决可能出现的问题。以下是几个常见场景的解决方案:

自定义广播间隔

广播间隔影响设备被发现的速度和功耗。要修改广播间隔,可以在注册广告后添加:

adapter_props.Set("org.bluez.Adapter1", "AdvertisingInterval", dbus.UInt16(0x0800))

常见间隔值:

  • 0x0020 (20ms) - 高频率,快速发现
  • 0x0800 (1.28s) - 平衡发现速度与功耗
  • 0x4000 (10.24s) - 低功耗

常见问题排查

问题现象可能原因解决方案
手机看不到设备蓝牙未启用/适配器问题检查bluetoothctl状态,确认适配器已启用
广告注册失败权限不足/接口不支持使用sudo运行,或确认适配器支持BLE
数据不完整广播包过长BLE广播包最多31字节,减少数据量
频繁断开信号干扰检查环境,避免2.4GHz频段干扰源

扩展广播数据

BLE 5.0引入了扩展广播功能,支持更大的数据包。要使用扩展广播,需要:

  1. 确认硬件支持BLE 5.0
  2. 修改广告类型为"peripheral"为"broadcast"
  3. 设置扩展广播参数:
advertisement = Advertisement(bus, 0, "broadcast")
adapter_props.Set("org.bluez.Adapter1", "AdvertisingFeatures", dbus.UInt16(0x10))

6. 实际应用案例

让我们看一个实际应用场景:创建一个室内温湿度监测信标。我们可以修改广播数据来包含环境信息:

# 模拟温湿度数据
temperature = 25  # 摄氏度
humidity = 60     # 百分比

# 在Advertisement类中添加
advertisement.add_manufacturer_data(0xFFFF, [
    0x01,          # 数据类型标识
    temperature,   # 温度值
    humidity       # 湿度值
])

这样,手机APP无需连接设备就能获取基本环境数据,大大降低了功耗。实际产品中,这些数据可以来自真实的传感器读取。

另一个常见应用是资产跟踪。通过在广播包中包含设备唯一标识,可以实现简单的物品追踪系统:

device_id = [0x12, 0x34, 0x56, 0x78]  # 4字节设备ID
advertisement.add_manufacturer_data(0xFFFF, device_id)

7. 性能优化与最佳实践

为了确保BLE广播的稳定性和效率,请考虑以下建议:

  1. 广播数据精简

    • 优先使用1字节UUID(如"180A"而非"0000180A-0000-1000-8000-00805F9B34FB")
    • 合并相关数据到制造商特定字段
  2. 功耗优化

    • 根据应用场景调整广播间隔
    • 考虑使用BLE 5.0的周期性广播功能
  3. 兼容性考虑

    • 测试不同手机型号的扫描行为
    • 避免使用保留的UUID和制造商ID
  4. 安全注意事项

    • 广播数据是公开的,不要包含敏感信息
    • 如需安全通信,应建立加密连接
# 示例:优化后的广播设置
advertisement.add_service_uuid("180D")  # 心率服务(2字节表示)
advertisement.add_manufacturer_data(0xFFFF, [
    0x01,       # 数据版本
    0x80,       # 电池电量(80%)
    0x64        # 信号强度
])

通过本文的实践,你已经掌握了使用Python和Bluez创建BLE广播设备的核心技能。在实际项目中,我发现最常遇到的问题通常是权限和版本兼容性,因此建议在部署前在不同硬件上进行充分测试。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐