一、前言
我们了解BLE的GATT之前需要了解一些基本的概念:
(1)Profile,字面意思简介、概述、形象印象、轮廓、配置文件,在BLE中,我们可能把它理解成配置文件较好,Profile有一些是BLE SIG规定的,有些可以是我们自定义的。
(2)Service,字面意思服务,在GATT中可以有多个服务,同样地,服务有些是BLE SIG定义的,有些是我们自定义的,习惯性叫这些服务为GATT服务。
(3)Characteristic, 字面意思特征,它存在于Service里面,就好比房间是一个服务,里面的装饰配置之类的是特征,而这些特征决定了房间的用处,也就是服务的用处。
(4)Attribute,字面意思是属性,可以这么理解,它描述的是特征的细节,比如描述特征的读写权限,特征的内容(值),特征的简介描述,还有特征的配置。
二、GGS相关的API
下面列举几个我们GGS常用的协议栈API:
- bStatus_t GGS_SetParameter( uint8 param, uint8 len, void *value )
描述:设置GAP GATT服务参数
参数:
param: Profile参数ID, 取的值可以参考gapgattserver.h的宏定义,其实就是下面表格列出的值:
GGS_DEVICE_NAME_ATT |
---|
GGS_APPEARANCE_ATT |
GGS_DEVICE_NAME_ATT |
GGS_APPEARANCE_ATT |
GGS_PERI_CONN_PARAM_ATT |
GGS_CENT_ADDR_RES_ATT |
GGS_RESOLVABLE_PRIVATE_ADDRESS_ONLY_ATT |
len: 要写入的字节个数
value: 指向要写的数据
比如我们在APP的初始化处初始蓝牙设备的名称,可以这么写:
uint8_t Ble_deviceName[] = "BoBo.cn";
GGS_SetParameter(GGS_DEVICE_NAME_ATT, strlen(Ble_deviceName), Ble_deviceName);
- bStatus_t GAP_SetParamValue(uint16_t paramID, uint16_t paramValue)
描述:设置GAP参数值
参数:
paramID, 这个参数可以参考Gap_ParamIDs_t枚举
paramValue, 值
bStatus_t GGS_AddService( uint32 services )
描述:添加一个功能到GGS上
参数:services,该参数是32位,每个位代表一个功能服务,较常用的是bit1,也就是GAP_SERVICE。 - bStatus_t GATTServApp_AddService( uint32 services )
描述:添加一个功能到GATT服务上,我们将一个服务注册到GATT服务上之后,在蓝牙建立连接过程中客户端就能发现该服务,如果注册的回调函数功能正常的话,就能够正常的使用该服务。
参数:
services,该参数是32位,每个位代表一个功能服务,大多数情况下推荐0xFFFFFFFF,即GATT_ALL_SERVICES - bStatus_t GATTServApp_RegisterService( gattAttribute_t *pAttrs, uint16 numAttrs, uint8 encKeySize, CONST gattServiceCBs_t *pServiceCBs )
描述:向GATT服务应用注册服务的属性列表和回调函数
参数:
pAttrs, 指向属性列表的指针
numAttrs, 属性单元个数
encKeySize, 服务所需的最小加密钥匙字节大小(7~16个字节)
pServiceCBs, 指向服务回调函数的指针
这个API很重要,我们有必要看看结构体gattAttribute_t和gattServiceCBs_t的原型:
typedef struct attAttribute_t
{
/// @brief GATT Attribute Type format.
struct
{
uint8 len; //!< UUID的长度
const uint8 *uuid; //!< 指向UUID
} type; //!< 属性类型 (2或者16字节UUIDs)
uint8 permissions; //!< 属性的权限
uint16 handle; //!< 属性的句柄 - 由属性服务器内部分配
uint8* const pValue; //!< 属性值 - 字节个数不超过512
} gattAttribute_t;
typedef struct
{
pfnGATTReadAttrCB_t pfnReadAttrCB; //!< 指向读ATT函数
pfnGATTWriteAttrCB_t pfnWriteAttrCB; //!< 指向写ATT函数
pfnGATTAuthorizeAttrCB_t pfnAuthorizeAttrCB; //!< 指向认证函数
} gattServiceCBs_t;
上面三个函数类型如下:
typedef bStatus_t (*pfnGATTReadAttrCB_t)( uint16 connHandle, gattAttribute_t *pAttr,
uint8 *pValue, uint16 *pLen, uint16 offset,
uint16 maxLen, uint8 method );
typedef bStatus_t (*pfnGATTWriteAttrCB_t)( uint16 connHandle, gattAttribute_t *pAttr,
uint8 *pValue, uint16 len, uint16 offset,
uint8 method );
typedef bStatus_t (*pfnGATTAuthorizeAttrCB_t)( uint16 connHandle, gattAttribute_t *pAttr,
uint8 opcode );
- bStatus_t GATTServApp_ProcessCharCfg( gattCharCfg_t *charCfgTbl, uint8 *pValue, uint8 authenticated, gattAttribute_t *attrTbl, uint16 numAttrs, uint8 taskId, pfnGATTReadAttrCB_t pfnReadAttrCB )
描述:处理客户端特征配置的改变
参数:
charCfgTbl,特征配置表
pValue, 指向属性内容
authenticated, 是否需要认证
attrTbl, 服务属性列表
numAttrs, 服务属性列表单元个数
taskId, 确认通知的任务ID
pfnReadAttrCB, Att读回调函数,GATTServApp_ProcessCharCfg最后会通过该函数给客户端发送数据 - bStatus_t GATTServApp_ProcessCCCWriteReq( uint16 connHandle, gattAttribute_t *pAttr, uint8 *pValue, uint16 len, uint16 offset, uint16 validCfg )
描述:处理客户端特征配置写请求,这个函数配合UUID 0x2902使用
参数:
connHandle, 客户端的连接句柄
pAttr, 指向属性表
pValue, 指向写入的数据
Len, 待写的字节数
offset,偏移字节写入
validCfg, 配置选项,值可以是GATT_CLIENT_CFG_NOTIFY或者GATT_CLIENT_CFG_INDICATE,或者两者都选(或逻辑)。
三、实现一个数据透传服务
下面写一个服务,两个特征,一个支持写,一个支持读和notify
直接上代码:
- icce_service.c
/*
* icce_service.c
*
* Created on: 2023年4月15日
* Author: 28596
*/
/*********************************************************************
* INCLUDES
*/
#include <string.h>
//#include <xdc/runtime/Log.h> // Comment this in to use xdc.runtime.Log
#include <ti/common/cc26xx/uartlog/UartLog.h> // Comment out if using xdc Log
#include <icall.h>
/* This Header file contains all BLE API and icall structure definition */
#include "icall_ble_api.h"
#include "icce_service.h"
#define DataServiceUUID 0xFFF5
#define DataWrite_UUID 0x0001
#define DataRead_UUID 0x0002
#define ATT_BT_UUID_SIZE 2
#define u8DATALEN 247
#define DATA_SERVICE_SERV_UUID_BASE16(uuid) LO_UINT16(uuid), HI_UINT16(uuid)
#define DS_UUID_BASE16(uuid) LO_UINT16(uuid), HI_UINT16(uuid)
static uint8_t wCharact_Props = GATT_PROP_WRITE;
static uint8_t rnCharact_Props = GATT_PROP_NOTIFY;
static uint8_t ds_icall_rsp_task_id = INVALID_TASK_ID;
// Write Characteristic Value
static uint8_t LOC_InputVal[u8DATALEN] = {0};
static uint8_t LOC_InputValLen = 0;
static uint8_t LOC_OutputVal[u8DATALEN] = {0};
static uint8_t LOC_OutputValLen = 0;
static ICCEDataServiceCBs_t *pAppCBs = NULL;
// Notify Characteristic 客户端特征配置描述符
static gattCharCfg_t *NotifyCharactConfig;//占用字节大小由可连接设备个数决定
// 数据服务的服务UUID Service UUID
CONST uint8_t icce_DataServiceUUID[ATT_BT_UUID_SIZE] =
{
DATA_SERVICE_SERV_UUID_BASE16(DataServiceUUID)
};
// 数据服务之下的写UUID
CONST uint8_t wUserCharact_UUID[ATT_BT_UUID_SIZE] =
{
DS_UUID_BASE16(DataWrite_UUID)
};
// 数据服务之下的读&Notify UUID
CONST uint8_t rnUserCharact_UUID[ATT_BT_UUID_SIZE] =
{
DS_UUID_BASE16(DataRead_UUID)
};
/* 属性类型的定义(UUID的字节长度,UUID)*/
static CONST gattAttrType_t LOC_DataServiceDecl = { ATT_BT_UUID_SIZE, icce_DataServiceUUID };
/* 服务列表 */
static gattAttribute_t ICCE_Data_ServiceAttrTbl[] =
{
// 数据服务服务什么
{
{ ATT_BT_UUID_SIZE, primaryServiceUUID },//2800
GATT_PERMIT_READ,
0,
(uint8_t *)&LOC_DataServiceDecl
},
// 写特征的申明
{
{ ATT_BT_UUID_SIZE, characterUUID },//2803
GATT_PERMIT_READ,
0,
&wCharact_Props
},
// 写特征的内容
{
{ ATT_BT_UUID_SIZE, wUserCharact_UUID },
GATT_PERMIT_WRITE,
0,
LOC_InputVal
},
// 读/通知特征的声明
{
{ ATT_BT_UUID_SIZE, characterUUID },
GATT_PERMIT_READ,
0,
&rnCharact_Props
},
// 读/通知特征的内容
{
{ ATT_BT_UUID_SIZE, rnUserCharact_UUID },
GATT_PERMIT_WRITE,//写权限允许,数组可以更改
0,
LOC_OutputVal
},
// 通知特征的 CCCD
{
{ ATT_BT_UUID_SIZE, clientCharCfgUUID },//0x2902
GATT_PERMIT_READ | GATT_PERMIT_WRITE,
0,
(uint8_t *)&NotifyCharactConfig
},
};
/*********************************************************************
* 静态函数声明
*/
static bStatus_t ICCE_Data_Service_ReadAttrCB(uint16_t connHandle,
gattAttribute_t *pAttr,
uint8_t *pValue,
uint16_t *pLen,
uint16_t offset,
uint16_t maxLen,
uint8_t method);
static bStatus_t ICCE_Data_Service_WriteAttrCB(uint16_t connHandle,
gattAttribute_t *pAttr,
uint8_t *pValue,
uint16_t len,
uint16_t offset,
uint8_t method);
/*********************************************************************
* 配置文件的回调
*/
CONST gattServiceCBs_t LOC_ICCE_Data_ServiceCBs =
{
ICCE_Data_Service_ReadAttrCB,
ICCE_Data_Service_WriteAttrCB,
NULL
};
/*
* ICCE数据服务添加函数 - 向Gatt服务器里注册Gatt属性
* 传参:rspTaskId - ICALL任务ID
*/
extern bStatus_t ICCE_DataService_AddService(uint8_t rspTaskId)
{
uint8_t status;
// 初始化,为客户端特征配置表动态分配一块内存
NotifyCharactConfig = (gattCharCfg_t *)ICall_malloc( sizeof(gattCharCfg_t) * linkDBNumConns);
if(NotifyCharactConfig == NULL)
{
return(bleMemAllocError);
}
// 初始化客户端特征配置属性,默认设置为无效
GATTServApp_InitCharCfg(LINKDB_CONNHANDLE_INVALID, NotifyCharactConfig);
// 向GATT服务器添加我们写好的服务
status = GATTServApp_RegisterService(ICCE_Data_ServiceAttrTbl,
GATT_NUM_ATTRS(ICCE_Data_ServiceAttrTbl),
GATT_MAX_ENCRYPT_KEY_SIZE,
&LOC_ICCE_Data_ServiceCBs);
Log_info1("Registered service, %d attributes",
GATT_NUM_ATTRS(ICCE_Data_ServiceAttrTbl));
ds_icall_rsp_task_id = rspTaskId;
return(status);
}
/*
* 数据服务设置参数 - 设置服务里面的属性内容.
* 传参:param -配置文件参数ID,目前我们ICCE数据服务里面有两个参数,我们把ID的范围取0:1
* len - 带写入的数据长度
* value - 指向带写入的数据.
*/
bStatus_t ICCEDataService_SetParameter(uint8_t param, uint16_t len, void *value)
{
bStatus_t ret = SUCCESS;
uint8_t *pAttrVal;
uint8_t *pValLen;
uint16_t valMinLen;
uint16_t valMaxLen;
uint8_t sendNotiInd = FALSE;
gattCharCfg_t *attrConfig;
uint8_t needAuth;
switch(param)
{
case wCharact_PARAM_ID:
pAttrVal = LOC_InputVal;
pValLen = &LOC_InputValLen;
valMinLen = 0;
valMaxLen = sizeof(LOC_InputVal);
break;
case rnCharact_PARAM_ID:
pAttrVal = LOC_OutputVal;
pValLen = &LOC_OutputValLen;
valMinLen = 0;
valMaxLen = sizeof(LOC_OutputVal);
sendNotiInd = TRUE;
attrConfig = NotifyCharactConfig;
needAuth = FALSE; // 这里发送设置成不需要认证
break;
default:
Log_error1("SetParameter: Parameter #%d not valid.", param);
return(INVALIDPARAMETER);
}
// 更新数据和发送notification或者indication
if(len <= valMaxLen && len >= valMinLen)
{
memcpy(pAttrVal, value, len);
*pValLen = len;
if(sendNotiInd)
{
Log_info2("Trying to send noti/ind: connHandle %x, %s",
attrConfig[0].connHandle,
(uintptr_t)((attrConfig[0].value ==
0) ? "\x1b[33mNoti/ind disabled\x1b[0m" :
(attrConfig[0].value ==
1) ? "Notification enabled" :
"Indication enabled"));
// 尝试发送Notification
GATTServApp_ProcessCharCfg(attrConfig, pAttrVal, needAuth,
ICCE_Data_ServiceAttrTbl,
GATT_NUM_ATTRS(ICCE_Data_ServiceAttrTbl),
ds_icall_rsp_task_id,
ICCE_Data_Service_ReadAttrCB);
}
}
else
{
Log_error3("Length outside bounds: Len: %d MinLen: %d MaxLen: %d.", len,
valMinLen,
valMaxLen);
ret = bleInvalidRange;
}
return(ret);
}
static uint8_t ICCEData_Service_findCharParamId(gattAttribute_t *pAttr)
{
// Is this a Client Characteristic Configuration Descriptor?
if(ATT_BT_UUID_SIZE == pAttr->type.len && GATT_CLIENT_CHAR_CFG_UUID ==
*(uint16_t *)pAttr->type.uuid)
{
return(ICCEData_Service_findCharParamId(pAttr - 1));
}
// 判断是否是wCharact的UUID
else if(ATT_BT_UUID_SIZE == pAttr->type.len && !memcmp(pAttr->type.uuid, wUserCharact_UUID, pAttr->type.len))
{
return(wCharact_PARAM_ID);
}
// 判断是否是rnCharact的UUID
else if(ATT_BT_UUID_SIZE == pAttr->type.len && !memcmp(pAttr->type.uuid, rnUserCharact_UUID, pAttr->type.len))
{
return(rnCharact_PARAM_ID);
}
else
{
return(0xFF);
}
}
/*********************************************************************
* @fn ICCE_Data_Service_ReadAttrCB
* @brief 读取一个属性
* @param connHandle - 连接句柄,客户端和服务器建立连接之后的句柄
* @param pAttr - 指向属性的指针
* @param pValue - 指向待读的数据
* @param pLen - 指向读取的字节个数
* @param offset - 偏移字节读取
* @param maxLen - 读取的最大长度
* @param method - 读取的方式
* @return SUCCESS, blePending or Failure
*/
static bStatus_t ICCE_Data_Service_ReadAttrCB(uint16_t connHandle,
gattAttribute_t *pAttr,
uint8_t *pValue, uint16_t *pLen,
uint16_t offset,
uint16_t maxLen,
uint8_t method)
{
bStatus_t status = SUCCESS;
uint16_t valueLen;
uint8_t paramID = 0xFF;
paramID = ICCEData_Service_findCharParamId(pAttr);
switch(paramID)
{
case rnCharact_PARAM_ID:
valueLen = LOC_OutputValLen;
break;
default:
Log_error0("Attribute was not found.");
return(ATT_ERR_ATTR_NOT_FOUND);
}
if(offset > valueLen)
{
Log_error0("An invalid offset was requested.");
status = ATT_ERR_INVALID_OFFSET;
}
else
{
*pLen = MIN(maxLen, valueLen - offset);
memcpy(pValue, pAttr->pValue + offset, *pLen);
}
return(status);
}
/*********************************************************************
* @fn ICCE_Data_Service_WriteAttrCB
* @brief 在写操作之前验证属性的数据
* @param connHandle - 连接句柄
* @param pAttr - 指向属性的指针
* @param pValue - 指向待写的数据
* @param len - 待写入的数据长度
* @param offset - 偏移字节写入
* @param method - 写类型
* @return SUCCESS, blePending or Failure
*/
static bStatus_t ICCE_Data_Service_WriteAttrCB(uint16_t connHandle,
gattAttribute_t *pAttr,
uint8_t *pValue, uint16_t len,
uint16_t offset,
uint8_t method)
{
bStatus_t status = SUCCESS;
uint8_t paramID = 0xFF;
uint8_t changeParamID = 0xFF;
uint16_t writeLenMin;
uint16_t writeLenMax;
uint8_t *pValueLenVar;
if(ATT_BT_UUID_SIZE == pAttr->type.len && GATT_CLIENT_CHAR_CFG_UUID == *(uint16_t *)pAttr->type.uuid)
{
// Allow notification and indication, but do not check if really allowed per CCCD.
status = GATTServApp_ProcessCCCWriteReq(
connHandle, pAttr, pValue, len,
offset,
GATT_CLIENT_CFG_NOTIFY | GATT_CLIENT_CFG_INDICATE);
if(SUCCESS == status && pAppCBs && pAppCBs->pfnCfgChangeCb)
{
pAppCBs->pfnCfgChangeCb(connHandle,
ICCEData_Service_findCharParamId(pAttr), len, pValue);
}
return(status);
}
paramID = ICCEData_Service_findCharParamId(pAttr);
switch(paramID)
{
case wCharact_PARAM_ID:
writeLenMin = 0;
writeLenMax = sizeof(LOC_InputVal);
pValueLenVar = &LOC_InputValLen;
Log_info5(
"WriteAttrCB : %s connHandle(%d) len(%d) offset(%d) method(0x%02x)",
(uintptr_t)"String",
connHandle,
len,
offset,
method);
break;
default:
Log_error0("Attribute was not found.");
return(ATT_ERR_ATTR_NOT_FOUND);
}
if(offset >= writeLenMax)
{
Log_error0("An invalid offset was requested.");
status = ATT_ERR_INVALID_OFFSET;
}
else if(offset + len > writeLenMax)
{
Log_error0("Invalid value length was received.");
status = ATT_ERR_INVALID_VALUE_SIZE;
}
else if(offset + len < writeLenMin &&
(method == ATT_EXECUTE_WRITE_REQ || method == ATT_WRITE_REQ))
{
Log_error0("Invalid value length was received.");
status = ATT_ERR_INVALID_VALUE_SIZE;
}
else
{
memcpy(pAttr->pValue + offset, pValue, len);
if(offset + len >= writeLenMin)
{
changeParamID = paramID;
*pValueLenVar = offset + len; // Update data length.
}
}
#if 0
if(changeParamID != 0xFF)
{
if(pAppCBs && pAppCBs->pfnChangeCb)
{
pAppCBs->pfnChangeCb(connHandle, paramID, len + offset, pValue); // Call app function from stack task context.
}
}
#endif
return(status);
}
extern void ICCE_test(void)
{
uint8_t value[10] = {0};
static uint64_t vl = 0;
vl++;
memcpy(value, (uint8_t *)&vl, sizeof(vl));
ICCEDataService_SetParameter(rnCharact_PARAM_ID, 10, value);
}
- icce_service.h
/*
* icce_service.h
*
* Created on: 2023年4月15日
* Author: 28596xx
*/
#ifndef PROFILES_ICCE_SERVICE_H_
#define PROFILES_ICCE_SERVICE_H_
#ifdef __cplusplus
extern "C"
{
#endif
/*********************************************************************
* INCLUDES
*/
#include <bcomdef.h>
#define wCharact_PARAM_ID 0x00
#define rnCharact_PARAM_ID 0x01
extern bStatus_t ICCE_DataService_AddService(uint8_t rspTaskId);
extern bStatus_t ICCEDataService_SetParameter(uint8_t param, uint16_t len, void *value);
extern void ICCE_test(void);//用于测试notify是否正常
#endif /* PROFILES_ICCE_SERVICE_H_ */
将上面这两个文件添加到工程,ICCE_test() 用一个定时器每5s执行一次。编译下载到CC2642,运行之后用手机就能发现我们这个服务了。
color=gray