nRF52840 搭建GATT Service传送门

本文基于高通平台QCC5121


1.BLE GATT SERVICE 结构

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

2. 实现环境

芯片:QCC5121

调试工具:TRB2000,

库:ADK6.3

3.添加服务思路

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

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所示。

保存后关闭,在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(&registration_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; i<access_ind->size_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. 读数据

2. 写数据

3. 通知

【蓝牙】如何新建一个BLE GATT SERVICE相关推荐

  1. CSR8675学习笔记:新建一个GATT server

    为了让CSR867x的开发更容易,现与思度科技联合推出CSR867x学习板[淘宝链接:思度科技CSR开发板]. 技术交流QQ群号:743434463 开发板会员QQ群号:725398389(凭订单号入 ...

  2. 蓝牙BLE GATT完全分析和运用

    很多人都做过蓝牙开发,很多人也能够通过仿照GATT例程的方式添加一个属性服务,但是很多人都未必能够清晰地理解BLE的属性profile,也很容易被属性Attribute和特性characteristi ...

  3. java web service_怎样新建一个Java的Web Service

    展开全部 1.web  service分客户端和服务端. 2.新建服务端:在这里我教你用MyEclipse自带的工具62616964757a686964616fe4b893e5b19e31333337 ...

  4. 【Nordic】52840 搭建GATT Service

    1 NRF52840 SDK 基础 1.1 GATT Service 蓝牙协议中定义 GATT service 为: "A service is a collection of data a ...

  5. 【BlueZ】【蓝牙】跨平台实现Ble MasterSlaveMesh 之Linux篇-1

    一,前述 1,上一篇讲述了如何通过Windows实现蓝牙应用,该方式可适用于Windows/Linux/Macos等场景.[https://blog.csdn.net/u014028690/artic ...

  6. 系统性简述蓝牙以及ESP32对BLE蓝牙的使用(一)

    这里写自定义目录标题 1.蓝牙简介 1.1 蓝牙分类 1.2 蓝牙技术 1.3 蓝牙协议框架 2.BLE 低功耗蓝牙协议 2.1 BLE协议栈框架 2.2 Link Layer States 2.2. ...

  7. nimble 蓝牙开发一:BLE 蓝牙 Host 规范概述

    目录 nimble 蓝牙开发一:概述 一.BLE 简介 二.BLE 基础知识 蓝牙设备地址 BLE 广播类型 标准广播数据 三.BLE 工作概述 BLE 常见的操作有: BLE 常见的工作流程: 四. ...

  8. android ble 蓝牙绑定流程,android BLE蓝牙开发

    蓝牙BLE设备是目前比较热门的设备.由于BLE有低功耗等特点,被广泛应用到身边的电子产品上.如智能手表.手环.防丢器等各种产品上.最近研究一下android上的ble应用开发.跟大家分享一下相关的内容 ...

  9. ESP32学习笔记(30)——BLE GATT服务端自定义服务和特征

    一.简介 1.1 低功耗蓝牙(BLE)协议栈 链路层(LL) 控制设备的射频状态,有五个设备状态:待机.广播.扫描.初始化和连接. 广播 为广播数据包,而 扫描 则是监听广播. GAP通信中角色,中心 ...

最新文章

  1. 抽象类(abstract class)和接口(Interface)的区别
  2. 京东把 Elasticsearch 用的真牛逼!
  3. mysql对库授权alter_mysql 数据库授权(给某个用户授权某个数据库)
  4. JavaScript立即执行函数学习
  5. 引入宽字符error: converting to execution character set: Invalid or incomplete multibyte or wide character
  6. [译]Javascript中的闭包(closures)
  7. linux路由内核实现分析(一)----邻居子节点(1)
  8. Python入门--元组的遍历,获取元组元素
  9. JavaScript正则表达式使用详解
  10. Linux - Centos7 查询系统安装时间以及硬盘序列号命令
  11. ISO/IEC 27017:2015 标准信息安全策略
  12. 正好股票开户有色金属应声大涨
  13. 一个WEB应用的开发流程 供学习用!
  14. 解决百度云离线文件因含有违规内容被系统屏蔽无法下载问题
  15. Linux -- 代理服务器(Squid Server)的配置与应用1
  16. 电脑常用快捷键及组合键
  17. 客户端在线更新-QT
  18. docfx 做一个和微软一样的文档平台
  19. 查看word的版本型号
  20. 2020年终总结,少壮工夫老始成

热门文章

  1. Flux Pattern
  2. SSM电竞陪玩服务平台(包安装调试)
  3. S7-300 400 PLC 系统软件冗余调试的常见问题
  4. Microsoft JScript 运行时错误: 对象不支持此属性或方法
  5. htcvive 经常黑屏 steamvr 提示 dhcp err
  6. 简述人工泛音与自然泛音
  7. 账号没违规扣分,视频推荐播放量突然减少,这两个错误别再犯
  8. 千万级数据导出Excel
  9. displaytag的Excel导出实践
  10. gyp ERR find Python 解决方案