基于Zephyr实现BLE Peripheral

Creative Commons
本作品采用知识共享署名

本文以环境传感器为例说明如何使用zephyr gatt来实现一个BLE Peripheral

硬件

包含DHT11,可以测试温度和湿度

Spec

实现第一步先了解BLE环境传感器的spec,BLE中基于GATT的spec有4个层次:

  • Profile: 配置文件:定义一个实际的应用场景,由一组Service组成
  • Service:服务:由一组Characteristic组成
  • Characteristic:有自己的读写Nodify属性, 并由一组Descriptor描述其它属性
  • Descriptor:用于描述Characteristic的属性

Profile

读Profile找到要实现的service
环境传感器的profile是ESP:ENVIRONMENTAL SENSING PROFILE,找到包含的services
profile
可以看到Environmental Sensing Service必须实现

Service

我们只实现Environmental Sensing Service,读ESS的spec,并下载 org.bluetooth.service.environmental_sensing可以得到如下信息:

Declare

推荐为Primary service
declar

Characteristic

ESS可选的Characteristic有很多种,ESS spec要求至少实现一种(Page 10, Table 3.1),这里选择温度和湿度两种。
char

Descriptor

温度和湿度支援的描述,ESS spec未强制要求一定要实现
dest
desh

实现

一个Profile可以包含多个service,在Zephyr的实现中Profile是个逻辑概念,分别对Service进行定义即可。对于ESP我只实现了ESS,如下:

1
2
3
4
5
6
7
8
9
10
11
Profile:ESP
`-- Service:BT_UUID_ESS
|-- Characteristic:BT_UUID_TEMPERATURE
| |-- Descriptor:BT_UUID_ES_MEASUREMENT
| |-- Descriptor:BT_UUID_GATT_CUD
| |-- Descriptor:BT_UUID_VALID_RANGE
| |-- Descriptor:BT_UUID_ES_TRIGGER_SETTING
| `-- Descriptor:BT_UUID_GATT_CCC
`-- Characteristic:BT_UUID_HUMIDITY
|-- Descriptor:BT_UUID_GATT_CCC
`-- Descriptor:BT_UUID_GATT_CUD

Service定义

Service

下面代码是ess service的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//定义attr数组
static struct bt_gatt_attr ess_attrs[] = {
/* Primary service declare */
BT_GATT_PRIMARY_SERVICE(BT_UUID_ESS), //声明Primary service

/* Temperature Sensor*/
BT_GATT_CHARACTERISTIC(BT_UUID_TEMPERATURE,
BT_GATT_CHRC_READ | BT_GATT_CHRC_NOTIFY,
BT_GATT_PERM_READ,
read_u16, NULL, &sensor_temp.temp_value), //定义Temperature characteristic,提供温度
BT_GATT_DESCRIPTOR(BT_UUID_ES_MEASUREMENT, BT_GATT_PERM_READ,
read_es_measurement, NULL, &sensor_temp.meas), //Environmental Sensing Measurement Descriptor,提供温度传感器的测量参数,例如更新时间,测量精度等等
BT_GATT_CUD(SENSOR_T_NAME, BT_GATT_PERM_READ), //Characteristic User Description,用户自定义,这里用于提供传感器的名字
BT_GATT_DESCRIPTOR(BT_UUID_VALID_RANGE, BT_GATT_PERM_READ,
read_temp_valid_range, NULL, &sensor_temp), //Valid Range Descriptor传感器有效值范围
BT_GATT_DESCRIPTOR(BT_UUID_ES_TRIGGER_SETTING,
BT_GATT_PERM_READ | BT_GATT_PERM_WRITE, read_temp_trigger_setting,
write_temp_trigger_setting, &sensor_temp), // Environmental Sensing Trigger Setting Descriptor 设置和读区传感器触发方式,例如温度变化才nodify
BT_GATT_CCC(sensor_temp.ccc_cfg, temp_ccc_cfg_changed), //Client Characteristic Configuration Descriptor, 配置Characteristic的描述符,例如可以配置让该Characteristic自动或者停止Nodify

/* Humidity Sensor */
BT_GATT_CHARACTERISTIC(BT_UUID_HUMIDITY, BT_GATT_CHRC_READ,
BT_GATT_PERM_READ,
read_u16, NULL, &sensor_hum.humid_value), //湿度描述符
BT_GATT_CUD(SENSOR_H_NAME, BT_GATT_PERM_READ), //湿度传感器的名称
BT_GATT_DESCRIPTOR(BT_UUID_ES_MEASUREMENT, BT_GATT_PERM_READ,
read_es_measurement, NULL, &sensor_hum.meas), //湿度传感器的测量参数
};

//定义ess service
static struct bt_gatt_service ess_svc = BT_GATT_SERVICE(ess_attrs);

BLE协议简述一文中说明了GATT Service就是一组Attribute组成,这些Attribute分为Declare/Characteristic/Descriptor,从上也可以看到确实是先定义了一个bt_gatt_attr数组,再以这个数组来定义service,从下面代码可以看到一个service就是存放了attr数组和attr的个数

1
2
3
4
5
6
7
8
9
10
11
12
13
#define BT_GATT_SERVICE(_attrs)						\
{ \
.attrs = _attrs, \
.attr_count = ARRAY_SIZE(_attrs), \
}

struct bt_gatt_service {
/** Service Attributes */
struct bt_gatt_attr *attrs;
/** Service Attribute count */
size_t attr_count;
sys_snode_t node;
};

Declare/Characteristic/Descriptor

将前面代码的几个宏展开,可以看到Declare/Characteristic/Descriptor最后都是Attr, Zephyr为方便使用将一些有共性的Descriptor进行重新封装,例如BT_GATT_CUD,BT_GATT_CCC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#define BT_GATT_PRIMARY_SERVICE(_service)				\
BT_GATT_ATTRIBUTE(BT_UUID_GATT_PRIMARY, BT_GATT_PERM_READ, \
bt_gatt_attr_read_service, NULL, _service)


#define BT_GATT_CHARACTERISTIC(_uuid, _props, _perm, _read, _write, _value) \
BT_GATT_ATTRIBUTE(BT_UUID_GATT_CHRC, BT_GATT_PERM_READ, \
bt_gatt_attr_read_chrc, NULL, \
(&(struct bt_gatt_chrc) { .uuid = _uuid, \
.properties = _props, })), \
BT_GATT_ATTRIBUTE(_uuid, _perm, _read, _write, _value)

#define BT_GATT_DESCRIPTOR(_uuid, _perm, _read, _write, _value) \
BT_GATT_ATTRIBUTE(_uuid, _perm, _read, _write, _value)

#define BT_GATT_CUD(_value, _perm) \
BT_GATT_DESCRIPTOR(BT_UUID_GATT_CUD, _perm, bt_gatt_attr_read_cud, \
NULL, (void *)_value)

#define BT_GATT_CCC(_cfg, _cfg_changed) \
BT_GATT_ATTRIBUTE(BT_UUID_GATT_CCC, \
BT_GATT_PERM_READ | BT_GATT_PERM_WRITE, \
bt_gatt_attr_read_ccc, bt_gatt_attr_write_ccc, \
(&(struct _bt_gatt_ccc) { .cfg = _cfg, \
.cfg_len = ARRAY_SIZE(_cfg), \
.cfg_changed = _cfg_changed, }))

Attribute

Service所有的元素最后都落脚于Attribute,这里也看下Attribute的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#define BT_GATT_ATTRIBUTE(_uuid, _perm, _read, _write, _value)		\
{ \
.uuid = _uuid, \
.perm = _perm, \
.read = _read, \
.write = _write, \
.user_data = _value, \
}

struct bt_gatt_attr {
//UUID
const struct bt_uuid *uuid;
//读回调,Central要求读对应Attr的时候会在Client调用该回调
ssize_t (*read)(struct bt_conn *conn,
const struct bt_gatt_attr *attr,
void *buf, u16_t len,
u16_t offset);
//写回调,Central要求写对应Attr的时候会在Client调用该回调
ssize_t (*write)(struct bt_conn *conn,
const struct bt_gatt_attr *attr,
const void *buf, u16_t len,
u16_t offset, u8_t flags);
//用户自定义数据
void *user_data;
//handle,由BLE Stack分配
u16_t handle;
//ATTR权限,读/写/Nodify
u8_t perm;
};

启动BLE Serivce

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
static void bt_ready(int err)
{
if (err) {
printk("Bluetooth init failed (err %d)\n", err);
return;
}

printk("Bluetooth initialized\n");
//注册ess
bt_gatt_service_register(&ess_svc);

//启动client广播
err = bt_le_adv_start(BT_LE_ADV_CONN_NAME, ad, ARRAY_SIZE(ad), NULL, 0);
if (err) {
printk("Advertising failed to start (err %d)\n", err);
return;
}

printk("Advertising successfully started\n");
}

void esp_init(void)
{
int err;
//初始化时enable ble,ready后会调用bt_ready
err = bt_enable(bt_ready);
if (err) {
printk("Bluetooth init failed (err %d)\n", err);
return;
}

while (1) {
k_sleep(MSEC_PER_SEC);
//每秒poll一次温度和湿度
ess_poll();
}
}

GATT操作

CHARACTERISTIC读写

在使用宏BT_GATT_CHARACTERISTIC定义Characteristic时会将Read和Write的函数注册进入,当Central端进行读GATT时Peripheral调用Read函数,响应的Central端进行GATT写的时候就调用Write函数,例如

1
2
3
4
BT_GATT_CHARACTERISTIC(BT_UUID_TEMPERATURE,
BT_GATT_CHRC_READ | BT_GATT_CHRC_NOTIFY,
BT_GATT_PERM_READ,
read_u16, NULL, &sensor_temp.temp_value)

只定义了读函数也就是支持gatt读温度,当Central需要读温度时,Peripheral主动调用该函数

1
2
3
4
5
6
7
8
9
static ssize_t read_u16(struct bt_conn *conn, const struct bt_gatt_attr *attr,
void *buf, u16_t len, u16_t offset)
{
const u16_t *u16 = attr->user_data; //这里的user_data就是前面注册的sensor_temp.temp_value
u16_t value = sys_cpu_to_le16(*u16);
//通过gatt attr read将temp送到Central
return bt_gatt_attr_read(conn, attr, buf, len, offset, &value,
sizeof(value));
}

写类似,本文可参考Descriptor的写

Descriptor读写

在使用宏BT_GATT_DESCRIPTOR定义Descriptor时会将Read和Write的函数注册进入,当Central端进行读GATT时Peripheral调用Read函数,响应的Central端进行GATT写的时候就调用Write函数,例如

1
2
3
BT_GATT_DESCRIPTOR(BT_UUID_ES_TRIGGER_SETTING,
BT_GATT_PERM_READ | BT_GATT_PERM_WRITE, read_temp_trigger_setting,
write_temp_trigger_setting, &sensor_temp)

上面的read_temp_trigger_setting和write_temp_trigger_setting分别用于读取和设置温度传感器的触发方式

特殊Descriptor操作

对于一些特殊的Descriptor操作是固定的,所以读写函数Zephyr已经帮我们将其写好了,只用在定义的时候给出Value即可例如

1
#define BT_GATT_CUD(_value, _perm)

也有一些Descriptor操作是特定的,Zephyr将其操作接口抽象出来由用户实现既可以,例如:

1
#define BT_GATT_CCC(_cfg, _cfg_changed)

传感器Poll

在主thread中每1s poll一次温湿度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static void ess_poll(void)
{
static u8_t i;
u16_t val;

//定期fetch温湿度
if (!(i % SENSOR_UPDATE_IVAL)) {
struct sensor_value temp, humidity;
struct device *dev = device_get_binding("DHT11");
sensor_sample_fetch(dev); //从dh11读取数据
sensor_channel_get(dev, SENSOR_CHAN_AMBIENT_TEMP, &temp); //读取温度
sensor_channel_get(dev, SENSOR_CHAN_HUMIDITY, &humidity); //读取湿度
val = temp.val1*100 + temp.val2/10000;
//将温湿度保存在user_data中,供gatt读取
update_temperature(NULL, &ess_attrs[2], val, &sensor_temp);
sensor_hum.humid_value = humidity.val1*100 + humidity.val2/10000;
}

if (!(i % INT8_MAX)) {
i = 0;
}

i++;
}

演示

终端上直接看温度和湿度
uart
通过手机蓝牙看温度和湿度
sen

参考

https://docs.zephyrproject.org/latest/samples/bluetooth/peripheral_esp/README.html
https://www.bluetooth.com/specifications/gatt