【蓝牙】如何新建一个BLE GATT SERVICE
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(®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; 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相关推荐
- CSR8675学习笔记:新建一个GATT server
为了让CSR867x的开发更容易,现与思度科技联合推出CSR867x学习板[淘宝链接:思度科技CSR开发板]. 技术交流QQ群号:743434463 开发板会员QQ群号:725398389(凭订单号入 ...
- 蓝牙BLE GATT完全分析和运用
很多人都做过蓝牙开发,很多人也能够通过仿照GATT例程的方式添加一个属性服务,但是很多人都未必能够清晰地理解BLE的属性profile,也很容易被属性Attribute和特性characteristi ...
- java web service_怎样新建一个Java的Web Service
展开全部 1.web service分客户端和服务端. 2.新建服务端:在这里我教你用MyEclipse自带的工具62616964757a686964616fe4b893e5b19e31333337 ...
- 【Nordic】52840 搭建GATT Service
1 NRF52840 SDK 基础 1.1 GATT Service 蓝牙协议中定义 GATT service 为: "A service is a collection of data a ...
- 【BlueZ】【蓝牙】跨平台实现Ble MasterSlaveMesh 之Linux篇-1
一,前述 1,上一篇讲述了如何通过Windows实现蓝牙应用,该方式可适用于Windows/Linux/Macos等场景.[https://blog.csdn.net/u014028690/artic ...
- 系统性简述蓝牙以及ESP32对BLE蓝牙的使用(一)
这里写自定义目录标题 1.蓝牙简介 1.1 蓝牙分类 1.2 蓝牙技术 1.3 蓝牙协议框架 2.BLE 低功耗蓝牙协议 2.1 BLE协议栈框架 2.2 Link Layer States 2.2. ...
- nimble 蓝牙开发一:BLE 蓝牙 Host 规范概述
目录 nimble 蓝牙开发一:概述 一.BLE 简介 二.BLE 基础知识 蓝牙设备地址 BLE 广播类型 标准广播数据 三.BLE 工作概述 BLE 常见的操作有: BLE 常见的工作流程: 四. ...
- android ble 蓝牙绑定流程,android BLE蓝牙开发
蓝牙BLE设备是目前比较热门的设备.由于BLE有低功耗等特点,被广泛应用到身边的电子产品上.如智能手表.手环.防丢器等各种产品上.最近研究一下android上的ble应用开发.跟大家分享一下相关的内容 ...
- ESP32学习笔记(30)——BLE GATT服务端自定义服务和特征
一.简介 1.1 低功耗蓝牙(BLE)协议栈 链路层(LL) 控制设备的射频状态,有五个设备状态:待机.广播.扫描.初始化和连接. 广播 为广播数据包,而 扫描 则是监听广播. GAP通信中角色,中心 ...
最新文章
- 抽象类(abstract class)和接口(Interface)的区别
- 京东把 Elasticsearch 用的真牛逼!
- mysql对库授权alter_mysql 数据库授权(给某个用户授权某个数据库)
- JavaScript立即执行函数学习
- 引入宽字符error: converting to execution character set: Invalid or incomplete multibyte or wide character
- [译]Javascript中的闭包(closures)
- linux路由内核实现分析(一)----邻居子节点(1)
- Python入门--元组的遍历,获取元组元素
- JavaScript正则表达式使用详解
- Linux - Centos7 查询系统安装时间以及硬盘序列号命令
- ISO/IEC 27017:2015 标准信息安全策略
- 正好股票开户有色金属应声大涨
- 一个WEB应用的开发流程 供学习用!
- 解决百度云离线文件因含有违规内容被系统屏蔽无法下载问题
- Linux -- 代理服务器(Squid Server)的配置与应用1
- 电脑常用快捷键及组合键
- 客户端在线更新-QT
- docfx 做一个和微软一样的文档平台
- 查看word的版本型号
- 2020年终总结,少壮工夫老始成