【蓝牙】如何新建一个BLE GATT SERVICE

nRF52840 搭建GATT Service传送门 

本文基于高通平台QCC5121


1.BLE GATT SERVICE 结构

GATT的服务(service)是为了实现设备的某些功能或特征,是一系列数据和行为的集合。如下图所示,一个服务通常由特征(characteristic)或其他服务的引用组成,每一个特征都包含一个值和关于这个值的信息。在这里实现一个服务,即指定该服务的相关参数,在工程中添加相关变量和函数以处理信息。

【蓝牙】如何新建一个BLE GATT SERVICE_第1张图片

 

2. 实现环境

芯片:QCC5121

调试工具:TRB2000,

库:ADK6.3

 

3.添加服务思路

由于高通提供了gatt service的注册工具,方便我们使用,所以我们首先根据规则注册一个服务到database中;之后在appGattInit初始化的时候,调用本服务的初始化函数;最后根据设计的服务的各功能实现其对应的句柄,实现各功能,最后指向其库函数,完成相应的动作。

【蓝牙】如何新建一个BLE GATT SERVICE_第2张图片

 

4. 具体实现

4.1 添加gatt_server_database

在工程编译之前,编译器首先会调用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所示。

【蓝牙】如何新建一个BLE GATT SERVICE_第3张图片

 保存后关闭,在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)

4.2 实现初始化过程

首先要新建两个文件,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);

4.3 实现message调用的句柄

由于在初始化中指定了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去处理。

4.2 实现数据读取

在.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消息给远程设备。到此,读数据的逻辑流程结束。

4.5 实现数据的写入

当收到一条写数据的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;

4.6 实现通知

为了测试方便,这里在收到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);
}

到此,通知过程实现完毕。

5. 实现效果

1. 读数据

【蓝牙】如何新建一个BLE GATT SERVICE_第4张图片

2. 写数据

【蓝牙】如何新建一个BLE GATT SERVICE_第5张图片

3. 通知

【蓝牙】如何新建一个BLE GATT SERVICE_第6张图片

 

 

你可能感兴趣的:(蓝牙)