建立一个新的profile必须要先熟悉和理解好qpps的profile的实现方式。就是大概理解好qpps.c/qpps_task.c/app_qpps.c/app_qpps_task.c实现的函数基本作用是什么。
qpps.c/qpps_task.c
qpps.c 主要是初始化服务、特征,把任务注册进系统内核,开启或者关闭qpps
qpps_task.c qpps任务状态机的具体内容,包括一些profile底层的操作,与协议栈的直接交互。
app_qpps.c/qpp_qpps_task.c
app_qpps.c 主要是app层对qpps的初始化和一些协议栈请求操作。
app_qpps_task.c app层一些请求操作的消息响应。
app下所有的消息都是属于app_task.c的,应该是用户直接调用的。
profile下所有的消息都是属于其本身任务状态机的,原则上只被app下的qpps相关文件调用。
1.qpps的init
init部分,qpps仅仅初始化了环境变量,注册任务状态机到内核中。
2.enable qpps
初始化profile中,会调用到app_qpps_enable_req来enable qpps,那到底enable做了什么呢?可以看到app_qpps_enable_req最后只是填入参数发送了一个QPPS_ENABLE_REQ的任务消息,体现了app端和profile位于不同 的层次,分工十分的清晰,profile中的文件才是直接和gatt层打交道的,app层是严禁和profile打交道的,平时我们开发也几乎都是使用app中 的api就可以完成所有的任务了。
2.1.为qpps_env获取到连接信息。
2.2.为qpps建立一个数据库,提供服务的定义和特征的定义声明及特征值的声明。
服务是一个可以组合多个特征值的单位,通常会完整的体现一个小完整的功能。服务只需要声明服务UUID和其包含的特征声明。profile中一般由一个或多个服务组成,例如proxr由3个服务组成,而qpps中只包含有一个服务。服务中包含特征声明,我们可以把特征声明分为四种结构:
2.2.1.特征名声明 包含特征的UUID,用于指示某一特征,只能读取。
2.2.2.特征值声明 该值可以有五种属性:Read(可读) Write(可靠的可写,带响应) Write without resp(不可靠的可写,不带响应) Indicate(可靠通知,带响应) Notify(不可靠通知,不带响应)
2.2.3.特征描述名 描述特征的作用、使用方法等等,只能读取。
2.2.4.特征描述符 描述Notify和Indicate特征值的状态,处于打开或者关闭状态,可写可读。
一个服务可以包含多个特征,qpps服务中包含一个可写(不可靠)特征和N个notify特征。
2.3.开启所有服务。此时qpps才算可以正常运行。
3.具体的数据发送操作。
app下的api都是直接被用户调用 的,所以应该考虑到用户所有的需求,对api进行封装,其归根结底是归类用户的参数,然后以合适的方式向profile下的任务机发送消息。一般来说,带task.c的是请求的响应,不带的是请求操作和初始化、使能或者禁止等功能操作。
profile下是app与GATT的交互,涉及到小部分的GATT操作,这里需要相对比较深入的理解协议栈。最主要的是处理app发送到ble的消息和ble向app发送消息。
和app下一样,一般带task的管响应操作,不带的管初始化、使能和禁止等操作。
了解了一个profile的构成,那么接下来就是对自定义的服务来做一些定义了。大部分都是可以参考qpps的方式来做的,其中qpps的特征数是可变的,所以,一些不可变的服务定义可以参考proxr去创建。(这两个例程几乎最简单)。
4.128位的UUID声明
在蓝牙中,每个服务和服务属性都唯一地由"全球唯一标识符" (UUID)来校验。正如它的名字所暗示的,每一个这样的标识符都要在时空上保证唯一。
UUID类可表现为短整形(16或32位)和长整形(128 位)UUID。
UUID值被固定分配在某个范围,该范围的第一个数值称为蓝牙UUID基数(Bluetooth_Base_UUID),其值为00000000—0000—1000—8000—00805F9B34FB。在此范围中,UUID用一个 16位或32位的二进制数表示,经常被称作16位或32位UUID。它的实际值代表一个 128位数。
128位的UUID值与16位或32位的UUID值之间的换算关系如下:
128_bit_value=16_bit_value*2^96+Bluetooth_Base_UUID
128_bit_value=32_bit_value*2^96+BIuetooth_Base_UUID
16位的UUID可以通过扩展16个0转换成32位的UUID。如果两个UUID值位数相同,则可以直接比较,如果位数不同,则需按照上述关系,将短UUID转换成长UUID,位数相同后再比较。
参考qpps的服务声明,首先定义128UUID如下:
#define QPP_SVC_PRIVATE_UUID "\xFB\x34\x9B\x5F\x80\x00\x00\x80\x00\x10\x00\x00\xE9\xFE\x00\x00"
先用一个数组把UUID存放起来。
/// Server Service
uint8_t qpps_svc[ATT_UUID_128_LEN] = QPP_SVC_PRIVATE_UUID;
别说我的方法老土,看了16位UUID的定义,其实它也高尚不到哪里去:
/// Service Value Descriptor - 16-bit
typedef uint16_t atts_svc_desc_t;
接下来就是放入到数据库中
// Service Declaration
[QPPS_IDX_SVC] = {{ATT_UUID_16_LEN, (uint8_t *)"\x00\x28"}, PERM(RD, ENABLE), sizeof(qpps_svc),
sizeof(qpps_svc), (uint8_t *)qpps_svc},
很多人疑惑此处的{ATT_UUID_16_LEN, (uint8_t *)"\x00\x28"}中为什么出现ATT_UUID_16_LEN,其实这一个参数是用来声明服务的类型分的,因为服务分为主要服务和次要服务
(
/// Primary service Declaration
ATT_DECL_PRIMARY_SERVICE = 0x2800,
/// Secondary service Declaration
ATT_DECL_SECONDARY_SERVICE,
)
,所以这一个结构体的作用并不是声明qpps服务的UUID的,qpps服务的UUID包含在qpps_svc中,那么第一个参数是一个16位UUID,但是并不是服务UUID,对于qpps服务来说,此UUID是指示qpps服务类型的。第二个参数该声明中值的读写权限,值中存放的就是qpps_svc,即服务的UUID,第三个参数就是该结构体中值的最大的长度,第四个参数是当前值的长度,最后一个参数就是qpps服务的UUID了。此条服务的声明结构体解析完毕。后面还会有其他的声明结构体,但是参数的含义会有一些差异,并不是完全一致的。
// Received data Characteristic Declaration
[QPPS_IDX_RX_DATA_CHAR] = {{ATT_UUID_16_LEN, (uint8_t *)"\x03\x28"}, PERM(RD, ENABLE), sizeof(qpps_char_rx_data),
sizeof(qpps_char_rx_data), (uint8_t *)&qpps_char_rx_data},
// Received data Characteristic Value
[QPPS_IDX_RX_DATA_VAL] = {{ATT_UUID_128_LEN, (uint8_t *)QPPS_RX_CHAR_UUID}, PERM(WR, ENABLE), QPP_DATA_MAX_LEN,
0, NULL},
// User Descriptor
[QPPS_IDX_RX_DATA_USER_DESP] = {{ATT_UUID_16_LEN, (uint8_t *)"\x01\x29"}, PERM(RD, ENABLE), sizeof("1.0"),
sizeof("1.0"), (uint8_t *)"1.0"},
这是一个特征的声明部分,此特征由三部分组成,第一部分是特征属性声明,说明特征组成的开始,跟服务的声明参数含义类似,不做详解。第二部分是特征值的声明。特征值很重要,是数据收发的时候读写的一个缓存区,所以第一个参数就是该值对应的特征的UUID,第二个参数就是其读写属性(上文介绍的五个属性),第三个参数为可存放最大值,最大为20,第四个参数是当前长度,因为是可写的,所以刚开始是不应该有数据的,为0,另外最后值也为空。第三部分是一个用户对特征的描述,第一个参数是声明该结构体的作用的,其他参数不解析了,值中包含的数据会显示为比较大的位置,例如上例的显示为:
大部分服务的特征是静态的,就是特征数目是确定的,可以放在profile文件夹下的xxx.c中,在声明的同时初始化。如果服务中特征值是可以动态调整的,那么可以把数据库的初始化数据放到xxx_task.c中的qpps_create_db_req_handler中去初始化。
qpps就是一个动态声明和静态声明结合的例程,刚才上面介绍的服务和特征的声明方式是静态的,就是说固定好了就这么多的服务和特征,接下来,在qpps_task.c中,qpps又动态的增加了QPPS_NOTIFY_NUM-1个notify的特征。
uint64_t cfg_flag = QPPS_MANDATORY_MASK;
uint8_t idx_nb = 4;
这是标识每一条声明的掩码,比如qpps.c中一共有7条记录,如果不动态增加nofity特征的话,那么掩码就应该为0x7f,一共7个位为1,但是由于后面的notify是动态增加的,所以qpps只初始化为0x0f,如果声明为5个notify,最后的值应该会0x7ffff,一共19个位为1.而idx_nb显然就是19,就是统计声明的个数的。这两个数一定要计算准确,否则服务和特征的读取会失败。
while (tx_char_num--)
{
cfg_flag = (cfg_flag << 3) | 0x07;
idx_nb += 3;
}
接下来是qpps_db和char_desc_def ,qpps_db是新的数据库,分配的空间由QPPS_NOTIFY_NUM决定,到时候把qppc的静态数据库copy,再动态的添加一些特征,那么就实现了qpps的profile了。char_desc_def 是作为添加特征用的。
struct atts_desc_ext *qpps_db = NULL;
struct atts_char128_desc *char_desc_def = NULL;
qpps_db = (struct atts_desc_ext *)ke_malloc(sizeof(qpps_att_db) + 3 * (tx_char_num) * sizeof(struct atts_desc_ext));
char_desc_def = (struct atts_char128_desc *)ke_malloc((tx_char_num) * sizeof(struct atts_char128_desc));
动态添加norify
for (uint8_t i = 0; i < tx_char_num; i++)
{
struct atts_char128_desc value_char = ATTS_CHAR128(ATT_CHAR_PROP_NTF,
0,
QPPS_FIRST_TX_CHAR_UUID);
value_char.attr_type[0] += i;
char_desc_def = value_char;
}
memcpy(qpps_db, qpps_att_db, sizeof(qpps_att_db));
for (uint8_t i = 0; i < tx_char_num; i++)
{
qpps_db[QPPS_IDX_VAL_CHAR + i * 3] = qpps_db[QPPS_IDX_VAL_CHAR];
qpps_db[QPPS_IDX_VAL_CHAR + i * 3].value = (uint8_t*)&char_desc_def;
qpps_db[QPPS_IDX_VAL_CHAR + i * 3 + 1] = qpps_db[QPPS_IDX_VAL];
qpps_db[QPPS_IDX_VAL_CHAR + i * 3 + 1].type.uuid = qpp_uuid_list;
qpps_db[QPPS_IDX_VAL_CHAR + i * 3 + 2] = qpps_db[QPPS_IDX_VAL + 1];
}
添加的方式其实很简单,就是先复制一份原来的notify,然后再需要改动 的地方稍加改动就可以了。改动的地方为特征值和uuid。