【蓝牙】BLE 数据收发实战指南(手机 App ↔ 嵌入式 Linux/BlueZ)
BLE 数据收发实战指南(手机 App ↔ 嵌入式 Linux/BlueZ)
本文面向在手机 App(Android/iOS)与嵌入式 Linux(BlueZ)之间实现 BLE(Bluetooth Low Energy)数据收发的工程实践,涵盖基础概念、接口映射、两类典型拓扑的完整流程、命令与验证、常见问题与排查,以及上线建议。
1. 基础概念与方向
- 角色与职责
Central(中心):负责扫描、连接、发现服务;常发起读写与订阅通知。Peripheral(外设):暴露 GATT 服务/特征/描述符;常被读写并主动上报通知/指示。
- GATT/ATT 数据通道
- 订阅上报:
notify/indicate由外设主动上报;由CCCD(0x2902)控制。 - 主动交互:
read/write由中心主动发起;外设读写回调中处理。
- 订阅上报:
- 关键对象(BlueZ D-Bus)
org.bluez.GattService1、org.bluez.GattCharacteristic1、org.bluez.GattDescriptor1- 特征属性:
UUID(s)、Service(o)、Value(ay)、Notifying(b)、Flags(as) - 特征方法:
ReadValue(a{sv} → ay)、WriteValue(ay, a{sv})、StartNotify()、StopNotify()
- CCCD(Client Characteristic Configuration Descriptor)
- 订阅通知:中心向 CCCD 写入
0x0001(notify)或0x0002(indicate)。 - 取消订阅:写入
0x0000。
- 订阅通知:中心向 CCCD 写入
2. BlueZ 接口与信号格式(Linux 外设)
- 特征 Value 类型:
ay(array of bytes)。 - 通知事件通过
PropertiesChanged信号上报,签名:s a{sv} as- 正确结构示例:
string "org.bluez.GattCharacteristic1"array [ dict entry( string "Value" variant array of bytes [ 12 34 56 78 ] ) ]array [ ]
- 正确结构示例:
- 重要实践:构造
a{sv}时应直接以ay作为"{sv}"的值,由 D-Bus 框架封装成单层variant。不要手动g_variant_new_variant(ay)再传入,否则会出现variant-of-variant(双层封装),导致上位机解析异常。- 参考实现路径:
vendor/linkric/acs-device-sdk/BluetoothImplementations/BlueZ/src/BlueZLEGattCharacteristic.cpp - 方法:
emitPropertiesChanged()将Value以ay直接放入"{sv}"。
- 参考实现路径:
2.1 从发现到数据收发的流程图(端到端)

详细步骤说明(基础通信过程)
-
发现与连接
- 中心启用控制器:
btmgmt power on、btmgmt le on - 扫描:
bluetoothctl→scan on,观察目标设备的广播(名称/服务 UUID/厂商字段)。 - 连接:
bluetoothctl connect <MAC>,建立 LE 连接(可在btmon中看到LE Connection Complete)。
- 中心启用控制器:
-
服务与特征发现
bluetoothctl→menu gatt→list-attributes查看服务与特征(GATT/ATT 句柄)。- 手机 App(Android/iOS)在连接后自动进行服务发现(
BluetoothGatt#discoverServices/CBPeripheral#discoverServices),并定位目标特征与 CCCD(0x2902)。
-
订阅通知 / 指示
- 中心写入 CCCD:
- Android:
descriptor.setValue(ENABLE_NOTIFICATION_VALUE/ENABLE_INDICATION_VALUE)→writeDescriptor - iOS:
peripheral.setNotifyValue(true, for: characteristic)
- Android:
- 外设(Linux/BlueZ)在
StartNotify设定Notifying=true。随后调用setValue(...)会发出PropertiesChanged,ATT 层表现为 Handle Value Notification/Indication。
- 中心写入 CCCD:
-
数据发送(外设→中心)
- 外设侧将待发字节流放入特征的
Value(ay)并触发PropertiesChanged:- 信号结构:
s a{sv} as,其中Value必须是variant(ay)的单层封装。 - 上层解析期望:
array of bytes [...]。
- 信号结构:
- 中心在回调中接收:
- Android:
onCharacteristicChanged - iOS:
didUpdateValueFor
- Android:
- 外设侧将待发字节流放入特征的
-
数据写入(中心→外设)
- 中心构造要写入的
ay字节流:- Android:
writeCharacteristic - iOS:
writeValue(data, type: .withResponse/.withoutResponse)
- Android:
- 外设在
WriteValue中读取参数并执行业务处理;需要返回结果时,结合通知/指示上报:setValue(result_bytes)。
- 中心构造要写入的
-
诊断与验证
- D-Bus 信号:
dbus-monitor --system "interface='org.freedesktop.DBus.Properties',member='PropertiesChanged'" - 低层抓包:
sudo btmon,观察 ATT 读写与通知时序。 - 常见问题:CCCD 写入失败、
Notifying未更新、variant-of-variant导致解析异常(务必保证Value的ay仅单层 variant)。
- D-Bus 信号:
3. 案例 A:手机 App 为 Central,Linux/BlueZ 为 Peripheral
3.1 Linux 侧(外设)
- 特征声明包含
Flags:notify或indicate。 - 启用通知:在
StartNotify中设置Notifying=true;之后每次调用setValue(...)都会触发PropertiesChanged信号(通知)。 - 验证信号:
dbus-monitor --system "interface='org.freedesktop.DBus.Properties',member='PropertiesChanged'" | grep -A3 /org/bluez/app/service0/chrc1- 低层抓包:
sudo btmon观察ATT Handle Value Notification/Indication。
- 快速命令(bluetoothctl):
scan on→ 发现并connect <MAC>→menu gatt→ 选择特征后notify on。
3.2 手机 App 侧(中心)
- Android(
BluetoothGatt)- 流程:连接→发现服务→查找特征与
CCCD。 - 订阅示例:
gatt.setCharacteristicNotification(characteristic, true)- 写入
CCCD:descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);gatt.writeDescriptor(descriptor)。
- 接收:
onCharacteristicChanged回调中读取字节数组。
- 流程:连接→发现服务→查找特征与
- iOS(
CoreBluetooth)- 流程:连接→
discoverServices/discoverCharacteristics。 - 订阅:
peripheral.setNotifyValue(true, for: characteristic)。 - 接收:
peripheral(_:didUpdateValueFor:error:)回调读取characteristic.value。
- 流程:连接→
3.3 数据帧约定(建议)
- 在字节流首部编码协议版本与命令字,便于扩展与兼容:
- 示例:
[magic=0x5AA5][ver=0x11][cmd=0x01][len=...][payload][CRC]
- 示例:
4. 案例 B:手机 App 写入命令(Central→Peripheral),Linux 回传结果
4.1 手机侧(写入)
- Android:
characteristic.setValue(bytes)→gatt.writeCharacteristic(characteristic);是否需要withResponse视特征Flags而定。 - iOS:
peripheral.writeValue(data, for: characteristic, type: .withResponse/.withoutResponse)。
4.2 Linux 侧(接收与反馈)
- 在特征的
WriteValue中读取ay参数,进行业务处理。 - 需要反馈时:在处理完成后
setValue(result_bytes)并确保已订阅通知(Notifying=true),中心即可收到结果通知。
4.3 验证
dbus-monitor观察PropertiesChanged;btmon观察 ATT 写入与后续通知时序。
5. 案例 C:Linux 为 Central(BlueZ + 工具/bleak),手机/设备为 Peripheral
5.1 Linux 侧(中心)
bluetoothctl快速验证:scan on→connect <MAC>→menu gatt→list-attributes- 选择目标特征后:
notify on订阅、read/write <hex bytes>主动交互。
- Python bleak(如需自动化):
- 订阅:
await client.start_notify(char_uuid, callback) - 写入:
await client.write_gatt_char(char_uuid, data)
- 订阅:
5.2 手机 App(外设)
- 暴露自定义 GATT 服务与特征,按需支持
notify/indicate与write。 - 快速验证可用
nRF Connect/LightBlue等通用 App。
6. 验证与诊断清单
- 控制器启用:
btmgmt power on、btmgmt le on - 扫描与连接:
bluetoothctl→scan on、connect <MAC> - 订阅通知:
menu gatt→ 选择特征 →notify on - 读写测试:
read、write <hex bytes> - D-Bus 信号监控:
dbus-monitor --system "interface='org.freedesktop.DBus.Properties',member='PropertiesChanged'" - 低层抓包:
sudo btmon
7. 常见问题与排查
- 无法订阅通知
- 特征
Flags未包含notify/indicate;CCCD 写入失败;外设未维护Notifying状态。
- 特征
- 收到空数据或长度异常
getValue()返回空数组;写入处理未更新m_value;两端编码约定不一致。
variant-of-variant解析异常- 构造
a{sv}时应直接传入ay,让框架封装单层variant;不要手动g_variant_new_variant(ay)再加入"{sv}"。
- 构造
- 连接稳定性与功耗
- 广播与连接参数(interval/latency/supervision timeout)需与手机端适配;高频上报场景建议限流与降采样。
8. 上线建议
- 建立“金路径”脚本:用
bluetoothctl与btmon固化验证步骤,更新后快速回归。 - 手机侧先用通用 App(
nRF Connect/LightBlue)快速验证,再集成到自研 App。 - 为数据帧定义清晰的协议文档与测试用例,确保两端编码一致。
