nRF52840 搭建GATT Service传送门
本文基于高通平台QCC5121
GATT的服务(service)是为了实现设备的某些功能或特征,是一系列数据和行为的集合。如下图所示,一个服务通常由特征(characteristic)或其他服务的引用组成,每一个特征都包含一个值和关于这个值的信息。在这里实现一个服务,即指定该服务的相关参数,在工程中添加相关变量和函数以处理信息。
芯片:QCC5121
调试工具:TRB2000,
库:ADK6.3
由于高通提供了gatt service的注册工具,方便我们使用,所以我们首先根据规则注册一个服务到database中;之后在appGattInit初始化的时候,调用本服务的初始化函数;最后根据设计的服务的各功能实现其对应的句柄,实现各功能,最后指向其库函数,完成相应的动作。
在工程编译之前,编译器首先会调用gattdbgen.exe来生成gatt database,这是为了方便用户无需手动添加service的相关特性到工程中,并自动分配service编号,方便管理。这里用户只需新建一个.dbi文件,并将该文件包含在av_headset_db.db文件,gattdbgen.exe会根据其内容其生成.h和.c文件,添加到工程里。
测试时,我们建立两个特征,一个read特征,包含了read和notify的属性,另一个write特征,包含write属性。由于服务和特征均需要UUID,为避免重复,这里从0xEEE0开始指定UUID,新建一个gatt_sean_server_uuids.h,写入以下内容:
#ifndef __SEAN_UUIDS_H__
#define __SEAN_UUIDS_H__
#define UUID_SEAN_SERVICE 0xEEE0
#define UUID_SEAN_READ 0xEEE1
#define UUID_SEAN_WRITE 0xEEE2
#endif /* __SEAN_UUIDS_H__ */
这里我们新建一个gatt_sean_server_db.dbi, 打开后写以下内容:
#ifndef __GATT_SEAN_SERVER_DB_DBI__
#define __GATT_SEAN_SERVER_DB_DBI__
#include "gatt_sean_server_uuids.h"
primary_service {
uuid : UUID_SEAN_SERVICE,
name : "SEAN_SERVICE",
characteristic {
uuid : UUID_SEAN_READ,
name : "SEAN_READ",
flags : [ FLAG_IRQ , FLAG_DYNLEN ],
properties : [ read , notify ],
value : 0x0,
client_config {
flags : [ FLAG_IRQ , FLAG_DYNLEN ],
name : "SEAN_NOTIFICATION"
}
},
characteristic {
uuid : UUID_SEAN_WRITE,
name : "SEAN_WRITE",
flags : [ FLAG_IRQ , FLAG_DYNLEN ],
properties : [ write ],
value : 0x0
}
},
#endif /* __GATT_SEAN_SERVER_DB_DBI__ */
这里flags用到了”FLAG_IRQ”和“FLAG_DYNLEN”,其含义如图 4‑1所示。
保存后关闭,在av_headset_db.db中添加
#include "gatt_sean_server_db.dbi"
即可,之后工程重新编译,会发现在av_headset_db.h中多出了这一段,这些宏在注册gatt服务时会用到。
#define HANDLE_SEAN_SERVICE (0x0019)
#define HANDLE_SEAN_SERVICE_END (0x001e)
#define HANDLE_SEAN_READ (0x001b)
#define HANDLE_SEAN_NOTIFICATION (0x001c)
#define HANDLE_SEAN_WRITE (0x001e)
注意这里primary_service使用到了JSON格式,其中的语法要严格遵循,否则会在漫长的编译过程后会迎来报错。
工程重新编译后,本应当自动生成各个特征的句柄,但我试了多次也没找到自动生成的文件,既然没有那就自己添加吧,在.h文件中添加如下定义:
#define HANDLE_SEAN_SERVICE_MESSAGE (0x0001)
#define HANDLE_SEAN_READ_MESSAGE (0x0003)
#define HANDLE_SEAN_WRITE_MESSAGE (0x0006)
#define HANDLE_SEAN_CLIENT_CONFIG_MESSAGE (0x0004)
首先要新建两个文件,gatt_sean_server_message.c 和 gatt_sean_server_message.h ,在.c文件中,我们首先添加服务的初始化函数,在其中指定服务的任务和函数,并在gatt_server中注册,如下所示:
bool GattSeanServerInit(SEANS *sean_server,
Task app_task,
const gatt_sean_server_init_params_t *init_params,
uint16 start_handle,
uint16 end_handle)
{
DEBUG_LOG("SEAN: GattSeanServerInit\n");
gatt_manager_server_registration_params_t registration_params;
if ((app_task == NULL) || (sean_server == NULL))
{
GATT_SEAN_SERVER_PANIC(("SEANS: Invalid Initialisation parameters"));
return FALSE;
}
/* Set up library handler for external messages */
sean_server->lib_task.handler = seanServerMsgHandler;
/* Store the Task function parameter.
All library messages need to be sent here */
sean_server->app_task = app_task;
/* Check optional initialisation parameters */
/* When GATT_MANAGER_USE_CONST_DB is enabled then it is the callers responsibility
* to register the appropriate GATT battery server configuration when the
* const database is registered.
*/
if (init_params)
{
/* Store notifications enable flag */
sean_server->notifications_enabled = init_params->enable_notifications;
}
else
{
sean_server->notifications_enabled = FALSE;
}
/* Setup data required for Battery Service to be registered with the GATT Manager */
registration_params.task = &sean_server->lib_task;
registration_params.start_handle = start_handle;
registration_params.end_handle = end_handle;
/* Register with the GATT Manager and verify the result */
return (GattManagerRegisterServer(®istration_params) == gatt_manager_status_success);
}
在函数的参数中,我们使用到了SEANS这个结构体,该结构体用来保存任务的句柄,在.h文件中,我们添加:
typedef struct __SEANS
{
TaskData lib_task;
Task app_task;
bool notifications_enabled;
} SEANS
之后在appGattInit中调用函数GattSeanServerInit,完成初始化:
GattSeanServerInit(appGetGattSeanServer(gatt_instance),
appGetGattSeanTask() ,
&sean_server_params,
HANDLE_SEAN_SERVICE,
HANDLE_SEAN_SERVICE_END);
由于在初始化中指定了message的句柄为seanServerMsgHandler,如图 4‑2所示,所以所有的message都会指向到这个函数中去解析
图 4‑2
所以我们在.c文件中添加该函数的实现:
void seanServerMsgHandler(Task task, MessageId id, Message payload)
{
DEBUG_LOG("SEAN: seanServerMsgHandler, id: %d\n", id);
SEANS *sean_server = (SEANS *)task;
switch (id)
{
case GATT_MANAGER_SERVER_ACCESS_IND:
{
/* Read/write access to characteristic */
handleSeanAccess(sean_server, (GATT_MANAGER_SERVER_ACCESS_IND_T *)payload);
}
break;
case GATT_MANAGER_REMOTE_CLIENT_NOTIFICATION_CFM:
{
/* Library just absorbs confirmation messages */
}
break;
default:
{
/* Unrecognised GATT Manager message */
GATT_SEAN_SERVER_DEBUG_PANIC(("SEAN: GATT Manager Server Msg not handled\n"));
}
break;
}
}
其中,所有“FLAG_IRQ”的信息都进入case GATT_MANAGER_SERVER_ACCESS_IND 中去解析,将payload重新构建成GATT_MANAGER_SERVER_ACCESS_IND_T的message。当信息传入到handleSeanAccess之后,我们再分开解析是读数据,写数据还是读config_value,在.c文件中继续添加handleSeanAccess的实现:
void handleSeanAccess(SEANS *sean_server, const GATT_MANAGER_SERVER_ACCESS_IND_T *access_ind)
{
DEBUG_LOG("SEAN: handleSeanAccess, \t cid: %d\n", access_ind->cid);
DEBUG_LOG("SEAN: handleSeanAccess, \t handle: %d\n", access_ind->handle);
DEBUG_LOG("SEAN: handleSeanAccess, \t flags: %d\n", access_ind->flags);
DEBUG_LOG("SEAN: handleSeanAccess, \t offset: %d\n", access_ind->offset);
DEBUG_LOG("SEAN: handleSeanAccess, \t size_value: %d\n", access_ind->size_value);
for(uint8 i=0; isize_value; i++)
{
DEBUG_LOG("SEAN: handleSeanAccess, \t value: %02X\n", access_ind->value[i]);
}
switch (access_ind->handle)
{
case HANDLE_SEAN_SERVICE_MESSAGE:
{
DEBUG_LOG("SEAN: handleSeanAccess: HANDLE_SEAN_SERVICE_MESSAGE\n");
seanServiceAccess(sean_server, access_ind);
}
break;
case HANDLE_SEAN_READ_MESSAGE:
{
DEBUG_LOG("SEAN: handleSeanAccess: HANDLE_SEAN_READ_MESSAGE\n");
readSthAccess(sean_server, access_ind);
}
break;
case HANDLE_SEAN_WRITE_MESSAGE:
{
DEBUG_LOG("SEAN: handleSeanAccess: HANDLE_SEAN_WRITE_MESSAGE\n");
readSthAccess(sean_server, access_ind);
}
break;
case HANDLE_SEAN_CLIENT_CONFIG_MESSAGE:
{
DEBUG_LOG("SEAN: handleSeanAccess: HANDLE_SEAN_CLIENT_CONFIG_MESSAGE\n");
seanConfigAccess(sean_server, access_ind);
}
break;
default:
{
/* Respond to invalid handles */
sendSeanAccessErrorRsp(sean_server, access_ind, gatt_status_invalid_handle);
}
break;
}
}
其中,读写数据的部分进入readSthAccess去处理,client_config的部分进入seanConfigAccess去处理。
在.c文件中我们先添加下述代码:
static void readSthAccess(SEANS *sean_server, const GATT_MANAGER_SERVER_ACCESS_IND_T *access_ind)
{
DEBUG_LOG("SEAN: readSthAccess\n");
if (access_ind->flags & ATT_ACCESS_READ)
{
DEBUG_LOG("SEAN: readSthAccess: ATT_ACCESS_READ\n");
/* Send read level message to app_task so it can return the current level */
MAKE_SEAN_MESSAGE(GATT_SEAN_SERVER_READ_STH_IND);
message->sean_server = sean_server; /* Pass the instance which can be returned in the response */
message->cid = access_ind->cid; /* Pass the CID which can be returned in the response */
MessageSend(sean_server->app_task, GATT_SEAN_SERVER_READ_STH_IND, message);
}
else if (access_ind->flags & ATT_ACCESS_WRITE)
{
DEBUG_LOG("SEAN: readSthAccess: ATT_ACCESS_WRITE\n");
/* Send read level message to app_task so it can return the current level */
MAKE_SEAN_MESSAGE(GATT_SEAN_SERVER_WRITE_STH_IND);
message->sean_server = sean_server; /* Pass the instance which can be returned in the response */
message->cid = access_ind->cid; /* Pass the CID which can be returned in the response */
message->write_value = (access_ind->value[0]) + (access_ind->value[1]<<8);
// sendSeanNotificationRsp((Task)&sean_server->lib_task, access_ind->cid, HANDLE_SEAN_CLIENT_CONFIG_MESSAGE, GATT_CLIENT_CONFIG_OCTET_SIZE, access_ind->value);
MessageSend(sean_server->app_task, GATT_SEAN_SERVER_WRITE_STH_IND, message);
// sendSeanAccessRsp((Task)&sean_server->lib_task, access_ind->cid, access_ind->handle, gatt_status_success, 0, NULL);
DEBUG_LOG("SEAN: WRITE VALUE: %02x%02x\n", access_ind->value[0], access_ind->value[1]);
}
else
{
/* Reject access requests that aren't read/write, which shouldn't happen. */
sendSeanAccessErrorRsp(sean_server, access_ind, gatt_status_request_not_supported);
}
}
读取数据用到了第一个if,将传递进来的message重新构建一个GATT_SEAN_SERVER_READ_STH_IND_T格式的message,将cid发送过去,所以这里还要再.h文件中添加该message结构体的定义:
typedef struct __GATT_SEAN_SERVER_READ_STH_IND
{
const SEANS *sean_server;
uint16 cid;
} GATT_SEAN_SERVER_READ_STH_IND_T;
在appGattSeanMessageHandler中会接收并解析app的消息,这里在.c文件中添加其实现:
void appGattSeanMessageHandler(Task task, MessageId id, Message message)
{
UNUSED(task);
DEBUG_LOG("SEAN: appGattSeanMessageHandler id:%d 0x%x", id, id);
switch (id)
{
case GATT_SEAN_SERVER_READ_STH_IND:
//TODO: DO STH HERE!
appGattHandleSeanServerReadSthInd((const GATT_SEAN_SERVER_READ_STH_IND_T *) message);
break;
case GATT_SEAN_SERVER_WRITE_STH_IND:
appGattHandleSeanServerWriteSth((const GATT_SEAN_SERVER_WRITE_STH_IND_T *)message);
break;
case GATT_SEAN_SERVER_READ_CLIENT_CONFIG_IND:
DEBUG_LOG("SEAN: appGattSeanMessageHandler:GATT_SEAN_SERVER_READ_CLIENT_CONFIG_IND");
break;
case GATT_SEAN_SERVER_WRITE_CLIENT_CONFIG_IND:
DEBUG_LOG("SEAN: appGattSeanMessageHandler:GATT_SEAN_SERVER_WRITE_CLIENT_CONFIG_IND");
appGattHandleSeanServerReadClientConfig((const GATT_SEAN_SERVER_READ_CLIENT_CONFIG_IND_T *)message);
break;
default:
DEBUG_LOG("SEAN: appGattSeanMessageHandler. Unhandled message id:0x%x", id);
break;
}
}
对于GATT_SEAN_SERVER_READ_STH_IND消息,会进入appGattHandleSeanServerReadSthInd去解析。接下来实现appGattHandleSeanServerReadSthInd,该函数指定要读取的参数,这里测试就只定义一个0xAA,将该值传递给GattSeanServerReadSthResponse。在.c文件中添加该函数实现:
static void appGattHandleSeanServerReadSthInd(const GATT_SEAN_SERVER_READ_STH_IND_T * ind)
{
uint8 return_value = 0xAA;
DEBUG_LOG("SEAN: appGattHandleSeanServerReadSthInd bas=[0x%p] cid=[0x%x]\n", (void *)ind->sean_server, ind->cid);
DEBUG_LOG("SEAN: Return value =[%u]\n", return_value);
/* Return requested battery level */
GattSeanServerReadSthResponse(ind->sean_server, ind->cid, return_value);
}
在GattSeanServerReadSthResponse中可以对服务、值或其他参数进行判断,根据判断结果添加access的状态。在.c文件中添加GattSeanServerReadSthResponse函数的实现:
bool GattSeanServerReadSthResponse(const SEANS *sean_server, uint16 cid, uint8 sean_level)
{
DEBUG_LOG("SEAN: GattSeanServerReadSthResponse: sean_level:%d\n", sean_level);
gatt_status_t status = gatt_status_failure;
if (sean_server == NULL)
{
return FALSE;
}
if (sean_level <= 0xff)
{
status = gatt_status_success;
}
else
{
status = gatt_status_insufficient_resources;
}
sendSeanLevelAccessRsp(sean_server, cid, sean_level, status);
return TRUE;
}
在sendSeanLevelAccessRsp中准备好其他各参数,最重要的是handle这里必须是在db中注册了read属性的特征,所以这里填HANDLE_SEAN_READ_MESSAGE,如下所示:
void sendSeanLevelAccessRsp(const SEANS *sean_server, uint16 cid, uint8 sean_level, uint16 result)
{
DEBUG_LOG("SEAN: sendSeanLevelAccessRsp: result:%d\n", result);
sendSeanAccessRsp((Task)&sean_server->lib_task, cid, HANDLE_SEAN_READ_MESSAGE, result, 1, &sean_level);
}
之后,message被传递到sendSeanAccessRsp中,在该函数中,已准备好的参数传递给库函数GattManagerServerAccessResponse,该函数用于返回GATT_MANAGER_SERVER_ACCESS_IND消息给远程设备。到此,读数据的逻辑流程结束。
当收到一条写数据的message后,会进入readSthAccess的else if中进行处理,在这里可以读取到远程设备写过来的值,即message->write_value。write_value是在.h文件中定义的GATT_SEAN_SERVER_WRITE_STH_IND_T结构体中的参数,在.h文件中添加其定义:
typedef struct __GATT_SEAN_SERVER_WRITE_STH_IND
{
const SEANS *sean_server;
uint16 cid;
uint16 write_value;
} GATT_SEAN_SERVER_WRITE_STH_IND_T;
为了测试方便,这里在收到write的值后,重新写到notify中,返回给手机。
在readSthAccess中,将值打包到message中,发送一个GATT_SEAN_SERVER_WRITE_STH_IND的message到队列中,在appGattSeanMessageHandler中会进入appGattHandleSeanServerWriteSth函数中去解析。
接下来在.c文件中添加appGattHandleSeanServerWriteSth的实现,该函数将要通知的值提取并传递给GattSeanServerWriteToNofityResponse:
static void appGattHandleSeanServerWriteSth(const GATT_SEAN_SERVER_WRITE_STH_IND_T * ind)
{
uint16 client_config = 0;
client_config = ind->write_value;
DEBUG_LOG("SEAN: appGattHandleSeanServerWriteSth, write_value=[0x%x]\n", client_config);
GattSeanServerWriteToNofityResponse(ind->sean_server, ind->cid, client_config);
}
同样GattSeanServerWriteToNofityResponse也是对服务的状态进行判断,看是否有错误发生,这里为了测试只判断了服务是否为空。
static bool GattSeanServerReadClientConfigResponse(const SEANS *sean_server, uint16 cid, uint16 client_config)
{
DEBUG_LOG("SEAN: GattSeanServerReadClientConfigResponse\n");
if (sean_server == NULL)
{
return FALSE;
}
sendSeanConfigAccessRsp(sean_server, cid, client_config);
return TRUE;
}
随后在sendSeanConfigNotificationRsp中将数据准备好,拆分成uint8的格式,并岁其他参数一起发给sendSeanNotificationRsp。注意这里发送的handle也必须是在database中注册过notify的特征,即HANDLE_SEAN_READ_MESSAGE。在.c文件中添加其实现:
static void sendSeanConfigNotificationRsp(const SEANS *sean_server, uint16 cid, uint16 client_config)
{
uint8 config_resp[GATT_CLIENT_CONFIG_OCTET_SIZE];
config_resp[0] = client_config & 0xFF;
config_resp[1] = (client_config >> 8) & 0xFF;
DEBUG_LOG("SEAN: sendSeanConfigNotificationRsp, config_resp:0x%x%x\n", config_resp[0],config_resp[1]);
sendSeanNotificationRsp((Task)&sean_server->lib_task, cid, HANDLE_SEAN_READ_MESSAGE, GATT_CLIENT_CONFIG_OCTET_SIZE, config_resp);
}
在sendSeanNotificationRsp函数中将所有参数准备好,转发给GattManagerRemoteClientNotify,该函数是专用于发送NOTIFICATION的库函数。
static void sendSeanNotificationRsp(Task task,
uint16 cid,
uint16 handle,
uint16 size_value,
const uint8 *value)
{
DEBUG_LOG("SEAN: sendSeanNotificationRsp\n");
GattManagerRemoteClientNotify(task, cid, handle, size_value, value);
}
到此,通知过程实现完毕。
3. 通知