从头开始学习:如何根据规范文件编写设备代码
名词解释
开始之前先简单了解一下蓝牙规范中的一些名词。
-
UUID 全称:Universally Unique Identifier,即通用唯一识别码。
它的标准型式包含 32 个十六进制数,以连字号分为五段,形式为
8-4-4-4-12的 32 个字符,如:9c80ace6-a25e-42c3-9ff4-fb43426c41dd。UUID 有 16bit 的,或者 128bit 的。 16bit 的 UUID 是官方通过认证的,需要花钱购买,128bit 是自定义的,这个就可以自己随便设置。
配置文件并不是实际存在于蓝牙设备上的,它只是一个被 Bluetooth SIG 预先定义的服务的集合。例如 Heart Rate Profile 就是结合了 Heart Rate 和 Device Information 两个服务。
服务把数据分成一个个的独立逻辑项,它包含一个或者多个特征。每个服务都有一个 UUID。
特征是最小的逻辑数据单元。与服务类似,每个特征也有一个 UUID。
你可以使用 Bluetooth SIG 官方定义的标准特征,这样可以确保软件和硬件能相互理解。
实际上,和蓝牙外设打交道的主要使用的是特征,你可以通过它读写数据,这样就实现了双向通信。
暂无。
本文中蓝牙设备指的是可以实现配置文件中指定的功能,并且可以和其它蓝牙设备进行通信的设备。
软件方面,蓝牙设备主要包括以下两个部分:
-
配置文件类模块:
-
配置文件类:包含服务和特征集合的类模块,它定义了设备应该具备的功能
-
特征值类:用于设置和获取特征值的数据
-
-
设备代码类模块:包含服务和特征的实现代码,提供设备广播和数据交换等功能
-
网页和文档
提前打开下列网页和文档,方便后续查阅。
-
Specifications and Documents 网站:用于查找官方定义好的配置文件
-
Assigned Numbers:用于查找服务、特征和描述符的 UUID ,以及所有定义好的常量值
-
GATT Specification Supplement:用于查找特征值的数据类型
编写配置文件类 (CTSProfile)
我们以 Current Time Service (CTS) 为例开始编写代码。
提示
在开始之前我们需要在/profiles目录下新建cts.py文件,并导入必要的模块。
View Code
1. 下载配置文件文档
在 Specifications and Documents 网站搜索CTS,点开链接下载 Current Time Service v1.1 文档。
2. 查找服务包含的特征
在文档第 9 页 Service Characteristics 章节中,可以看到 CTS 服务包含的特征,如下表所示:
| 特征 | 必选 / 可选 |
|---|---|
| Current Time | 必选 |
| Local Time Information | 可选 |
| Reference Time Information | 可选 |
提示
CTS 服务不添加可选特征也可以正常使用,这里我们选择添加前两个特征。
到这里需要先暂停一下,等收集完必要的 UUID 之后再继续。
3. 查找服务和特征的 UUID
在 Assigned Numbers 文档中找到以下内容并记录它们的 UUID:
- Current Time Service @page_66
- Current Time @page_85
- Local Time Information @page_89
View Code
4. 编写服务和特征类
返回刚刚暂停的文档处,继续查看各个特征的属性,如下表所示:
| 特征 | 读 | 写 | 通知 |
|---|---|---|---|
| Current Time | 必选 | 可选 | 必选 |
| Local Time Information | 必选 | 可选 | 无关 |
根据以上获取到的信息就可以开始编写 CTS Profile 所需的服务和特征类了。需要注意的是,这里只给特征添加了必选属性。
View Code
5. 编写配置文件类
View Code
在__make_profile()函数中,我们可以使用较为直观的方式添加配置文件的服务和特征。
编写特征值类 (CTSValues)
特征值类用于设置特征的值并返回符合要求的字节数据。
在开始之前我们需要在cts.py文件中增加 CTSValues 类定义。
View Code
在 GATT Specification Supplement 文档中找到特征值说明,并记录它们的数据格式,包括字段名称、数据类型和数据大小等。
1. 编写 Current Time 特征值代码
-
字段 数据类型 大小 内容描述 Exact Time 256 struct 9 参考 Exact Time 256 特征。 Adjust Reason boolean[8] 1 此字段表示调整时间的原因:
0:手动时间更新
1:外部参考时间更新
2:时区变更
3:夏令时变更
4–7:保留以备将来使用-
Exact Time 256 字段是一个结构体,需要参考 Exact Time 256 特征的数据格式,暂时跳过。
-
Adjust Reason 字段是一个布尔数组,列出了 4 种调整原因,要将它们作为常量进行记录,并增加类属性代码。
View Code
继续查看 Exact Time 256 特征值说明。
字段 数据类型 大小 内容描述 Day Date Time struct 8 参考 Day Date Time 特征。 Fractions256 uint8 1 1/256 秒的分数,有效范围 0–255。 Fractions256
个人理解这个字段表示当前时间的毫秒数,为了节省空间所以采用 1/256 计数。
假设当前时间毫秒数为 900,那么对应 Fractions256 的值应为 900 / 1000 * 255,取整为 229。
- Day Date Time 字段需要参考 Day Date Time 特征的数据格式,暂时跳过。
-
Fractions256 字段取值范围 0~255,给它增加类属性代码。
View Code
/profiles/cts.py
继续查看 Day Date Time 特征值说明。
字段 数据类型 大小 内容描述 Date Time struct 7 参考 Date Time 特征。 Day of Week struct 1 参考 Day of Week 特征。 Day Date Time 特征的两个字段都是结构体,直接查看它们的特征值说明。
字段 数据类型 大小 内容描述 Year uint16 2 公历年份,有效范围 1582 到 9999,0 表示年份未知。 Month uint8 1 公历年份中的某一月份,有效范围 1(月)到 12(月),0 表示月份未知。 Day uint8 1 公历月份中的某一天,有效范围 1 到 31,0 表示日期未知。 Hours uint8 1 午夜过后的小时数,有效范围 0 到 23。 Minutes uint8 1 自整点开始以来的分钟数,有效范围 0 到 59。 Seconds uint8 1 自分钟开始以来的秒数,有效范围 0 到 59。 字段 数据类型 大小 内容描述 Day of Week uint8 1 0:未知
1:星期一
2:星期二
3:星期三
4:星期四
5:星期五
6:星期六
7:星期日
8–255:保留以备将来使用Date Time 和 Day of Week 特征值所有字段的值都可以使用
time.localtime()获取。 -
至此我们已经得到 Current Time 特征全部的特征值。
根据上述特征值信息可以得到如下分解公式:
| Current Time | = | Exact Time 256 | + | Adjust Reason | ||||||||||||||
| = | Day Date Time | + | Fractions256 | + | Adjust Reason | |||||||||||||
| = | Date Time | + | Day of Week | + | Fractions256 | + | Adjust Reason | |||||||||||
| = | Year | + | Month | + | Day | + | Hours | + | Minutes | + | Seconds | + | Day of Week | + | Fractions256 | + | Adjust Reason |
整理后得到数组示意表格:
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
|---|---|---|---|---|---|---|---|---|---|
| Year | Year | Month | Day | Hours | Minutes | Seconds | Day of Week | Fractions256 | Adjust Reason |
根据表格内容增加类属性代码。
View Code
2. 编写 Local Time Information 特征值代码
有了上面编写 Current Time 特征值代码的经验,下面都是大同小异,也就一带而过了。
-
字段 数据类型 大小 内容描述 Time Zone struct 1 参考 Time Zone 特征。 DST Offset struct 1 参考 DST Offset 特征。 字段 数据类型 大小 内容描述 Time Zone sint8 1 此字段表示与 UTC 的偏移量,以 15 分钟为增量,有效范围从 -48 到 +56,-128 表示时区偏移量未知。
无论夏令时是否生效,此特性中定义的偏移量都是恒定的。UTC 偏移量
-48 表示:UTC-12:00
+56 表示:UTC+14:00
为 Time Zone 字段增加类属性代码。
View Code
/profiles/cts.py 字段 数据类型 大小 内容描述 DST Offset uint8 1 0:标准时间
2:半小时夏令时(+0.5h)
4:夏令时(+1h)
8:双夏令时(+2h)
255:DST 偏移未知
其它:保留以备将来使用为 DST Offset 字段增加类常量和类属性代码。
View Code
至此我们已经得到 Local Time Information 特征全部的特征值,为其增加类属性代码。
View Code
编写设备类 (CTSServer)
提示
在开始之前我们需要在/devices/cts目录下新建ctsserver.py文件。
编写 CTS Server 设备类,大致分为以下几个步骤,每一步中只给出关键示例(伪)代码。
1. 导入 CTSProfile 及相关模块
View Code
2. 生成 CTSProfile 实例
新建一个 CTSServer 类,在其中初始化蓝牙 BLE 外设,并实例化 CTSProfile 和 CTSValues 类。
View Code
3. 注册服务,初始化特征值
注册服务并获取特征操作句柄。
View Code
4. 生成广播数据并启动广播
View Code
| /devices/cts/ctsserver.py | |
|---|---|
5. 监听中心设备读取请求
一旦有中心设备成功连接,就可以读取 CTS Server 的日期时间等数据,需要在读取请求回调中进行响应处理。
View Code
获取完整代码
附录1:特征值数据类型和大小
-
数据大小单位是 Octet,即 8 位字节。
-
数据类型:
数据类型 大小 说明 struct 不定 表示一个可变长度的字节数组,其长度和内部格式与值单独指定。 boolean[8] 1 表示一个由 8 个布尔值组成的数组。 uint8 1 表示一个 8 位无符号整数。 sint8 1 表示一个 8 位有符号整数。