近期对BLE(蓝牙低功耗)很感兴趣,我在玩几个支持BLE的教育产品:

  • microbit
  • wedo2
  • BB8
  • 树莓派

就连接的简易性和续航方面,体验都非常好,传输距离更是超过了100米。就智能硬件在教育这个领域的应用而言,我相信BLE是未来,尤其是web bluetooth的标准化推进,使得基于web来做硬件编程教育变得简易,如此一来跨平台变得简易而开发成本也大为降低

本文在前半部分整理了BLE相关的知识和概念(多数摘自维基百科),后半部分记录了我学完理论之后,做的GATT相关实验,介绍了一些方便做实验开源硬件和工具,以及它们的使用方法

BLE(蓝牙低功耗)

引维基百科的介绍:

蓝牙低功耗(Bluetooth Low Energy,或称Bluetooth LE、BLE,旧商标Bluetooth Smart)也称蓝牙低能耗、低功耗蓝牙,是蓝牙技术联盟设计和销售的一种个人局域网技术,旨在用于医疗保健、运动健身、信标、安防、家庭娱乐等领域的新兴应用。相较经典蓝牙,低功耗蓝牙旨在保持同等通信范围的同时显著降低功耗和成本。
包括iOS、Android、Windows Phone和BlackBerry在内的大多数移动操作系统,以及macOS、Linux、Windows 8与Windows 10等在内的桌面操作系统,均已原生支持低功耗蓝牙。蓝牙技术联盟(SIG)预测,到2018年超过90%有蓝牙的智能手机将支持低功耗蓝牙。

从这些信息来看,BLE已获得了各大平台的支持,具体的支持情况为:

  • iOS 5及更高版本
  • Windows 8及更高版本
  • Android 4.3及更高版本
  • Linux 3.4及更高版本,通过BlueZ 5.0

大家可以放心在产品中使用。

主要优点

  • 低功耗,使用纽扣电池就可运行数月至数年
  • 小体积、低成本
  • 与现有的大部分手机、平板电脑和计算机兼容

GATT

GATT (Generic Attribute Profile) 的中文翻译是通用属性规范。

维基百科里提到:

当前所有低功耗应用profile都基于通用属性规范(GATT)。

那么什么是profile呢

蓝牙技术联盟沿用经典蓝牙的规范内容,为蓝牙低功耗定义了一些profile,这些profile定义了一个设备在特定应用情景下如何工作。制造商应通过在实现中遵循特定的profile以确保兼容性。一台设备可以使用多个profile。

说回GATT

GATT定义了属性(Attribute),作为通用的封装数据的单位,并定义了如何通过蓝牙连接传输属性从而达到传输数据的目的

所以本文技术方面的很多内容都与GATT相关。

GAP

介绍 GATT 之前,我们先来了解一下 GAP(Generic Access Profile). GAP用来控制设备连接和广播。 它使你的设备被其他设备可见,并决定了你的设备是否可以以及怎样与合同设备进行交互。例如 Beacon 设备就只是向外广播,不支持连接,小米手环就等设备就可以与中心设备连接。

GAP 给设备定义了若干角色,其中主要的两个是:外围设备(Peripheral)和中心设备(Central)。

  • 外围设备:这一般就是非常小或者简单的低功耗设备,用来提供数据,并连接到一个更加相对强大的中心设备。例如小米手环。
  • 中心设备:中心设备相对比较强大,用来连接其他外围设备。例如手机等。

一旦两个设备经过 GAP 协议建立起了连接,GATT 就开始起作用了,我们就开始跳到GATT部分

关于GAP的更多内容,可以参考:

软件模型

GATT相关术语

由于我自己目前正在学习BLE,对这个领域的知识没有全局的认识,所积累的知识多是片段式的,所以这篇文章会很枯燥,它更像一个零碎的学习笔记,而不是教程。概念相关的部分基本摘自维基百科,如果大家学习ble的相关概念,建议直接读维基百科,而不是我这这部分二道贩子文章。

罗列一下GATT相关术语

  • 客户端(Client): 一个发出GATT命令和请求的设备,然后接受响应。例如一个计算机或智能手机。
  • 服务器(Server): 一个接受GATT命令和请求的设备,然后返回响应。例如一个温度传感器。
  • 特征(Characteristic): 在客户端与服务器间传递的数据值,例如当前的电池电压。GATT 事务中的最低界别的是 Characteristic,Characteristic 是最小的逻辑数据单元。和 BLE 外设打交道,主要是通过 Characteristic
  • 服务(Service): 有关特征的收集,具有一系列操作来执行特定功能。例如,“体温计”服务包括一个温度测量值,以及测量的时间间隔。
  • 描述符(Descriptor: 描述符提供有关特征的其他信息。例如指示一个温度值特征的单位(如摄氏度),以及传感器可以测量的最大值和最小值。描述符是可选的——每个特征可以有任何数量的描述符。

值得注意的是:某些服务和特征用于管理目的——例如,“通用访问”服务中的型号名称和序列号可作为标准特征读取。设备的主要功能被称为主(primary)服务

你可以自己实现一个类似串口(UART)的 Sevice,这个 Service 中包含两个 Characteristic,一个被配置只读的通道(RX),另一个配置为只写的通道(TX)。(TX表示发射,RX表示接收)

标识符(Identifiers)

服务、特征和描述符被统称为属性(attributes),并以UUID标识。蓝牙技术联盟已预留一系列UUID供标准属性使用:蓝牙分配号码

16 bit 的 UUID 是官方通过认证的,需要花钱购买,128 bit 是自定义的,可以自己随便设置

GATT操作

GATT协议提供了大量用于客户端的命令以发现有关服务器的信息。这包括:

  • 发现所有主要服务的UUID
  • 使用指定UUID查找一个服务
  • 查找指定主服务的辅助服务
  • 发现指定服务的所有特征
  • 查找匹配指定UUID的特征
  • 读取特定特征的所有描述符

除此之外,也提供读(从服务器传输到客户端)和写(客户端传给服务器)特征值的命令:

  • 可以指定特征的UUID或句柄(handle)值(由上面的发现命令返回)来读取值。
  • 写操作始终需要以句柄来标识特征,但可选是否需要服务器返回响应。(Q:写操作不能使用特征的UUID吗?)

GATT有提供通知(notifications)和指示(indications)。客户端可以请求服务器通知一项特征。服务器可以在其变为可用时将该值发送给客户端。例如,温度传感器的服务器可以在每次测量时通知其客户端。这得以避免客户端轮询服务器,造成服务器的无线电路保持运行。

指示(indication)与通知类似,不同之处是它需要客户端响应已收到该消息。

GATT 的连接与网络拓扑

GATT 连接网络拓扑结构:

GATT 连接是独占的。也就是一个 BLE 外设同时只能被一个中心设备连接(中心设备可以连接多个BLE外设)。一旦外设被连接,它就会马上停止广播,这样它就对其他设备不可见了。当设备断开,它又开始广播。

GATT 通信事务

GATT 通信的双方是 C/S 关系。外设作为 GATT 服务端(Server),它维持了 ATT 的查找表以及 service 和 characteristic 的定义。中心设备是 GATT 客户端(Client),它向 Server 发起请求

体验

上边的这些背景知识,已经足够我们开发BLE相关的项目了。

但要体验BLE,则无需任何背景知识,如果你有一块micro:bit,你就可以快速来体验一下BLE的好处:

两篇教程任选其一跟着操作就行

上边两个项目的源码都完全开放,所以你可以拿到service和characteristic的uuid,如果你有兴趣,可以翻阅源码拿到这些数据,自己写gatt client去和microbit交互

GATT实验笔记

在学完GATT的相关知识后,我准备动手写写client和server

下文是我做实验的笔记。

工具

我准备使用树莓派(3B)和micro:bit来做实验

可能对你有用的软件工具有:

  • hcitool/gatttool(运行在树莓派上)
  • BLE Scanner(运行在安卓手机上)
  • LightBlue(运行在macOS下)

在树莓派中做实验

做实验之前,我们先来了解一下bluez,bluez是官方Linux蓝牙协议栈,是一个基于GPL协议发布的开源项目,当前的最新版本是5.50,稍后我们在树莓派里做蓝牙相关的实验,我们都得依赖它

bluez安装玩之后,会得到hcitool与gattool,我们可以根据这些工具来轻松的调试我们的蓝牙设备。 (需要注意的是,在调试BLE设备时,需要获得root权限)

我在树莓派刷完raspbian stretch镜像后,好像自带了bluez

查看bluez版本:bluetoothctl -v,我的当前版本是5.43

hcitool

hcitool可以对蓝牙进行控制,参数分为两部分,一为正常的蓝牙设备调试,二为BLE设备调试

为了开始我们的实验,我们得有一个GATT server,我们先实验micro:bit当GATT server

你需要现在micro:bit烧入一个固件:microbit-bluetooth

ps: pxt允许你设置蓝牙连接是否需要密码

之后我们就能够在树莓派中搜到micro:bit了:

1
2
3
$ sudo hcitool lescan
LE Scan ...
DF:48:87:86:93:20 BBC micro:bit [zuzop]

从中我们可以拿到microbit的地址:DF:48:87:86:93:20

但我在查看设备附加信息时却失败了:sudo hcitool leinfo DF:48:87:86:93:20,reddit里有人说可能是树莓派里蓝牙与wifi冲突(公用一个芯片)

但我使用手机工具:BLE Scanner和电脑工具:LightBlue都没问题

这些我们从LightBlue里都可以看到:

我们从lancaster给出的[micro:bit蓝牙文档](Bluetooth Developer Studio Level 3 Profile Report)里,可以查到:

gatttool

关于gatttool的介绍我们引用蓝牙工具hcitool和gatttool的使用:

使用hcitool主要是为了对设备的连接进行管理,对BLE数据进行精细化管理的话,就需要用到gattool。使用gattool对蓝牙设备发送指令的操作上要比hcitool的cmd齐全很多。

关于gattool的使用分为两种,一种直接使用参数对蓝牙设备进行控制;二就是使用-I参数进入gattool的interactive模式对蓝牙设备进行控制

接下来我们使用gatttool来做些实验, 进入交互模式

1
2
3
4
pi@raspberrypi:~ $ gatttool -I -b DF:48:87:86:93:20 -t random
[DF:48:87:86:93:20][LE]> connect
Attempting to connect to DF:48:87:86:93:20
Connection successful

micro:bit将显示连接成功的图案

接着我们来看一下UART service的相关信息:

1
2
3
4
5
6
7
8
9
[DF:48:87:86:93:20][LE]> primary 6e400001-b5a3-f393-e0a9-e50e24dcca9e
Starting handle: 0x0021 Ending handle: 0xffff
[DF:48:87:86:93:20][LE]> char-desc 0x0021 0xffff
handle: 0x0021, uuid: 00002800-0000-1000-8000-00805f9b34fb
handle: 0x0022, uuid: 00002803-0000-1000-8000-00805f9b34fb
handle: 0x0023, uuid: 6e400002-b5a3-f393-e0a9-e50e24dcca9e
handle: 0x0024, uuid: 00002902-0000-1000-8000-00805f9b34fb
handle: 0x0025, uuid: 00002803-0000-1000-8000-00805f9b34fb
handle: 0x0026, uuid: 6e400003-b5a3-f393-e0a9-e50e24dcca9e

Q:service里同时包含Characteristic和Descriptor?

我们在前头提到:

可以指定特征的UUID或句柄(handle)值(由上面的发现命令返回)来读取值

我看来看看这些uuid,从lancaster给出的micro:bit蓝牙文档里,可以查到

  • UART RX CHARACTERISTIC UUID: 6E400002B5A3F393E0A9E50E24DCCA9E
  • UART TX CHARACTERISTIC UUID:6E400003B5A3F393E0A9E50E24DCCA9E

(TX表示发射,RX表示接收)

Descriptor 00002902-0000-1000-8000-00805f9b34fb (handle: 0x0024)是 Client Characteristic Configuration, 允许你从service中订阅信息

其他三个descriptors是 Primary Service 和 Characteristic Declaration,暂时跳过

设置 Client Characteristic Configuration为0x0200 将订阅 TX events.

1
2
[DF:48:87:86:93:20][LE]> char-write-req 0x0024 0200
Characteristic value was written successfully

每次按下micro:bit上的A按钮,我将收到通知

1
2
3
4
5
6
7
8
[DF:48:87:86:93:20][LE]> char-write-req 0x0024 0200
Characteristic value was written successfully
Indication   handle = 0x0023 value: 01
Indication   handle = 0x0023 value: 02
Indication   handle = 0x0023 value: 03
Indication   handle = 0x0023 value: 04
Indication   handle = 0x0023 value: 05
Indication   handle = 0x0023 value: 06

我们也可以直接读取:

1
2
[DF:48:87:86:93:20][LE]> char-read-hnd 0x0023
Characteristic value/descriptor: 06

接下来,让我们往micro:bit的UART中写数据,前头我们会有提到

写操作始终需要以句柄来标识特征

写操作对应的句柄(handle)为0x0026

我们在bleserver 固件里写的逻辑是遇:到换行,现实数据通信图标. 而换行的十六进制为: 0x0a

1
char-write-cmd 0x0026 0a

可以看到数据通信图标在micro:bit中显示

至此gatt client与gatt server的读写操作已经订阅消息,我们都做了演示

gatt server

我们可以通过在makecode中拖延积木为micro:bit搭出所需的gatt server,但事实上我们实在组合已有的服务

下来我们实验树莓派来运行一个简单的gatt server,我们将使用python-bluezero提供的一个demo来讲解如何写gatt server

python-bluezero致力于帮助新手使用Bluez,为使用蓝牙功能的用户提供简化的API。该库底层封装了对Bluez、D-Bus API的调用,并使用“合理”的默认值来实现简化。它旨在支持创建有趣的STEM活动,而无需深入理解Bluez API或编写事件循环。

该库假定你的BlueZ版本为5.43(Raspbian Stretch目前是这个版本)

安装

安装过程参考文档

使用

安装完系统依赖之后,就可以安装python-bluezero了,我选择在python3中使用,将项目克隆到本地之后,进入目录: python3 setup.py install

下边我们来看一下GATT Server (Peripheral role)

这个例子实现了一个报告cpu温度的characteristic,允许client请求服务器通知温度变化。

源码为cpu_temperature.py

运行之后,可以看到:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
pi@cozmo1:~/python-bluezero/examples $ python3 cpu_temperature.py
cpu_temp: temp=57.5'C

CPU temperature is 57.5C
cpu_temp: temp=58.0'C

sint16: b'\xa8\x16'
b'\xa8\x16'
cpu_temp: temp=58.0'C

Advertisement registered

如果你分不清那个是你的树莓派,如果树莓派离你很近,就接着选择信号好的外围设备(Peripheral),连接它,就可以读取树莓派的cpu温度了

我在界面点击R(read),可以看到温度值的十六进制表示为: 0xBA13

我们在树莓派里看到当前温度为:

1
2
cpu_temp: temp=50.5'C
sint16: b'\xba\x13'

这两者是怎么联系起来的呢,我们翻下源码,跟踪一下当gatt client读取温度时,发生了什么

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
def ReadValue(self, options):
        reading = [get_cpu_temperature()]
        self.props[constants.GATT_CHRC_IFACE]['Value'] = reading
        return dbus.Array(
            cpu_temp_sint16(self.props[constants.GATT_CHRC_IFACE]['Value'])
        )

def cpu_temp_sint16(value):
    answer = []
    value_int16 = sint16(value[0])
    for bytes in value_int16:
        answer.append(dbus.Byte(bytes))

    return answer

def sint16(value):
    return int(value * 100).to_bytes(2, byteorder='little', signed=True)

可见温度被sint16函数做了转化,我们来看下十进制的50.5被转完之后是什么:

1
2
3
4
5
6
7
8
9
In [1]: def sint16(value):
   ...:     return int(value * 100).to_bytes(2, byteorder='little', signed=True)
   ...:
   ...:

In [2]: t = 50.5

In [3]: sint16(50.5)
Out[3]: b'\xba\x13'

前导\x转义序列意味着接下来的两个字符被解释为字符代码的十六进制数字,所以\xaa等于chr(0xaa). 和手机里显示的正好对起来啦

我们如何把它逆向转化呢,毕竟我们在client只看到它的16进制表示:

1
2
In [4]: int.from_bytes(b'\xba\x13', byteorder='little')
Out[4]: 5050  #50.5 * 100

源码的其他部分,语义十分清晰,如果你有GATT的基础知识(前头我们整理的那些),读起来应该很轻松

更多好玩的

BB8

BB8基于ble基于与外部通信,允许GATT client读取它的状态信息并控制它,我们使用LightBlue对其做个了解

如果你想了解BB8提供的所有服务,可以参考bluetooth-toy-bb8

这里有几个项目实现了手写gatt client与BB8通信:

协议相关的部分参考:Sphero_API_1.50

wedo2

乐高为教育推出的wedo2套件,通信部分也是基于ble

社区里有人将其封装为一个python库: LEGO-WeDo-2.0-Python-SDK, 基于pygatt, 由于pygatt实现了BGAPI,所以可以使用BLED112,直接在mac下使用

如果你想在其他平台上与wedo2交互,可以看官方的例子:wedo-2/developer-kits

可以一直接看社区小伙伴提取出的协议细节:WeDo2-BLE-Protocol

渗透工具

对于关注安全的小伙伴,可以参考这篇文章:神器分享:物联网安全测试工具包

参考