蓝牙广播只能单向发送数据且数据可以被任何其它设备接收,无法实现设备接收数据且不安全。而我的项目中不仅需要蓝牙设备发送数据给微信小程序而且需要微信小程序给蓝牙设备发送数据,因此只能放弃使用广播方式通信。
通过查阅资料得知,要实现蓝牙设备与微信小程序的双向数据收发,需要通过自定义服务,然后读写特征值。首先学习一下后面程序中经常使用到的概念。
在 GAP 中外围设备通过两种方式向外广播数据: Advertising Data Payload(广播数据)和 Scan Response Data Payload(扫描回复),每种数据最长可以包含 31 byte。这里广播数据是必需的,因为外围设备必需不停的向外广播,让中央设备知道它的存在。外围设备会设定一个广播间隔,每个广播间隔中,它会重新发送自己的广播数据。大部分情况下,外围设备通过广播自己来让中心设备发现自己,并建立GATT连接,从而进行更多的数据交换。GATT 连接是独占的,也就是一个外围设备同时只能被一个中央设备连接。一旦外围设备被连接,外围设备就会马上停止广播,这样外围设备就对其它中央设备不可见了。当连接断开,外围设备又开始广播。中央设备和外围设备需要双向通信的话,唯一的方式就是建立 GATT 连接。
GATT通信的双方是C/S关系。外围设备(peripheral device)作为GATT服务端(Server),它维持了ATT的查找表以及service和characteristic的定义。中央设备(central device)是GATT客户端(Client),它向Server发起请求。需要注意的是,所有的通信事件,都是由客户端(也叫主设备,Master)发起,并且接收服务端(也叫从设备,Slava)的响应。
一个蓝牙设备可以有多个服务,一个服务有可以有多个特征值,特征值都有他的属性,例如长度(size),权限(permission),值(value),描述(descriptor)。如下图:
下面是从一些权威的资料中摘抄出来的,翻译的不容易理解。
Connections “Connections” provides more information about connections at the lower level, and “Roles” discusses the corresponding GAP roles.Connections involve two separate roles:
Repeatedly scans the preset frequencies for connectable advertising packets and, when suitable, initiates a connection. Once the connection is established, the central manages the timing and initiates the periodical data exchanges.
A device that sends connectable advertising packets periodically and accepts incoming connections. Once in an active connection, the peripheral follows the central’s timing and exchanges data regularly with it.
To initiate a connection, a central device picks up the connectable advertising packets from a peripheral and then sends a request to the peripheral to establish an exclusive connection between the two devices. Once the connection is established, the peripheral stops advertising and the two devices can begin exchanging data in both directions.
A connection is therefore nothing more than the periodical exchange of data at certain specific points in time (connection events) between the two peers involved in it.
Beginning with version 4.1 of the specification, any restrictions on role combinations have been removed, and the following are all possible:
A device can act as a central and a peripheral at the same time.
A central can be connected to multiple peripherals.
A peripheral can be connected to multiple centrals.
Previous versions of the specification limited the peripheral to a single central connection (although not conversely) and limited the role combinations.(ps:网很多中文资料有点老,说一个外围设备只能连接一个中央设备)
Generic profiles are defined by the specification, and it’s important to understand how two of them are fundamental to ensuring interoperability between BLE devices from different vendors:
Covering the usage model of the lower-level radio protocols to define roles, procedures, and modes that allow devices to broadcast data, discover devices, establish connections, manage connections, and negotiate security levels, GAP is, in essence, the topmost control layer of BLE. This profile is mandatory for all BLE devices, and all must comply with it.
Dealing with data exchange in BLE, GATT defines a basic data model and procedures to allow devices to discover, read, write, and push data elements between them. It is, in essence, the topmost data layer of BLE.
ble_std.h中设置蓝牙设备名字为中文。
可以看到第一条日志是__GAPM_PROFILE_ADDED_IND,在ble_std.c文件中找到了
可以看到程序先判断当前app状态是APPM_CREATE_DB的话就调用Service_Add函数添加服务,添加完毕将app状态置为APPM_READY,所有服务添加完毕后就开始广播,
Service_Add函数中最后是通过app.h中的宏完成添加服务的:
/* List of functions used to create the database */
#define SERVICE_ADD_FUNCTION_LIST \
DEFINE_SERVICE_ADD_FUNCTION(Batt_ServiceAdd_Server), \
DEFINE_SERVICE_ADD_FUNCTION(CustomService_ServiceAdd)
//DEFINE_SERVICE_ADD_FUNCTION(CustomService_Service2Add)
若想加入自己定义的服务,只需要照葫芦画瓢实现自己的CustomService_ServiceAdd函数即可,CustomService_ServiceAdd函数在ble_custom.c中实现:
/* ----------------------------------------------------------------------------
* Function : void CustomService_ServiceAdd(void)
* ----------------------------------------------------------------------------
* Description : Send request to add custom profile into the attribute
* database.Defines the different access functions
* (setter/getter commands to access the different
* characteristic attributes).
* Inputs : None
* Outputs : None
* Assumptions : None
* ------------------------------------------------------------------------- */
void CustomService_ServiceAdd(void)
{
struct gattm_add_svc_req *req =
KE_MSG_ALLOC_DYN(GATTM_ADD_SVC_REQ,
TASK_GATTM, TASK_APP,
gattm_add_svc_req,
CS_IDX_NB * sizeof(struct gattm_att_desc));
const uint8_t svc_uuid[ATT_UUID_128_LEN] = CS_SVC_UUID;
const struct gattm_att_desc att[CS_IDX_NB] =
{
/* Attribute Index = Attribute properties: UUID,
* Permissions,
* Max size,
* Extra permissions */
/* TX Characteristic */
[CS_IDX_TX_VALUE_CHAR] = ATT_DECL_CHAR(),
[CS_IDX_TX_VALUE_VAL] = ATT_DECL_CHAR_UUID_128(CS_CHARACTERISTIC_TX_UUID,
PERM(RD, ENABLE) | PERM(NTF, ENABLE),
CS_TX_VALUE_MAX_LENGTH),
[CS_IDX_TX_VALUE_CCC] = ATT_DECL_CHAR_CCC(),
[CS_IDX_TX_VALUE_USR_DSCP] = ATT_DECL_CHAR_USER_DESC(CS_USER_DESCRIPTION_MAX_LENGTH),
/* RX Characteristic */
[CS_IDX_RX_VALUE_CHAR] = ATT_DECL_CHAR(),
[CS_IDX_RX_VALUE_VAL] = ATT_DECL_CHAR_UUID_128(CS_CHARACTERISTIC_RX_UUID,
PERM(RD, ENABLE) | PERM(WRITE_REQ, ENABLE)
| PERM(WRITE_COMMAND, ENABLE),
CS_RX_VALUE_MAX_LENGTH),
[CS_IDX_RX_VALUE_CCC] = ATT_DECL_CHAR_CCC(),
[CS_IDX_RX_VALUE_USR_DSCP] = ATT_DECL_CHAR_USER_DESC(CS_USER_DESCRIPTION_MAX_LENGTH),
};
/* Fill the add custom service message */
req->svc_desc.start_hdl = 0;
req->svc_desc.task_id = TASK_APP;
req->svc_desc.perm = PERM(SVC_UUID_LEN, UUID_128);
req->svc_desc.nb_att = CS_IDX_NB;
memcpy(&req->svc_desc.uuid[0], &svc_uuid[0], ATT_UUID_128_LEN);
for (unsigned int i = 0; i < CS_IDX_NB; i++)
{
memcpy(&req->svc_desc.atts[i], &att[i],
sizeof(struct gattm_att_desc));
}
/* Send the message */
ke_msg_send(req);
}
在ble_custom.h中定义了服务和特征值的uuid(uuid遵循一定规范):
/* Custom service UUIDs */
#define CS_SVC_UUID { 0x24, 0xdc, 0x0e, 0x6e, 0x01, 0x40, \
0xca, 0x9e, 0xe5, 0xa9, 0xa3, 0x00, \
0xb5, 0xf3, 0x93, 0xe0 }
#define CS_CHARACTERISTIC_TX_UUID { 0x24, 0xdc, 0x0e, 0x6e, 0x02, 0x40, \
0xca, 0x9e, 0xe5, 0xa9, 0xa3, 0x00, \
0xb5, 0xf3, 0x93, 0xe0 }
#define CS_CHARACTERISTIC_RX_UUID { 0x24, 0xdc, 0x0e, 0x6e, 0x03, 0x40, \
0xca, 0x9e, 0xe5, 0xa9, 0xa3, 0x00, \
0xb5, 0xf3, 0x93, 0xe0 }
为了表述方便,这里我将主动请求数据的手机蓝牙调试app称为主机,被动接受的蓝牙设备RSL10板卡称为从机。若主机请求读特征值,则会调用ble_custom.c中的回调函数GATTC_ReadReqInd。
我们就可以在GATTC_ReadReqInd函数中填充需要发送主机的数据,将数据填充到valptr即可,程序中我将“eeworld”发送给主机。在实际应用中,我们在主循环或者其它函数中将数据填充到cs_env[device_indx].tx_value即可。
int GATTC_ReadReqInd(ke_msg_id_t const msg_id,
struct gattc_read_req_ind const *param,
ke_task_id_t const dest_id,
ke_task_id_t const src_id)
{
uint8_t length = 0;
uint8_t status = GAP_ERR_NO_ERROR;
uint16_t attnum;
uint8_t *valptr = NULL;
PRINTF("===================read\n");
/* Retrieve the index of environment structure representing peer device */
signed int device_indx = Find_Connected_Device_Index(KE_IDX_GET(src_id));
if (device_indx == INVALID_DEV_IDX)
{
return (KE_MSG_CONSUMED);
}
struct gattc_read_cfm *cfm;
/* Set the attribute handle using the attribute index
* in the custom service */
if (param->handle > cs_env[device_indx].start_hdl)
{
attnum = (param->handle - cs_env[device_indx].start_hdl - 1);
}
else
{
status = ATT_ERR_INVALID_HANDLE;
}
PRINTF("==== attnum=0x%02x param->handle=0x%04x ====\n",attnum,param->handle);
/* If there is no error, send back the requested attribute value */
if (status == GAP_ERR_NO_ERROR)
{
switch (attnum)
{
/* RX characteristic*/
case CS_IDX_RX_VALUE_VAL:
{
length = CS_RX_VALUE_MAX_LENGTH;
valptr = (uint8_t *)&cs_env[device_indx].rx_value;
}
break;
case CS_IDX_RX_VALUE_CCC:
{
length = 2;
valptr = (uint8_t *)&cs_env[device_indx].rx_cccd_value;
}
break;
case CS_IDX_RX_VALUE_USR_DSCP:
{
length = strlen(CS_RX_CHARACTERISTIC_NAME);
valptr = (uint8_t *)CS_RX_CHARACTERISTIC_NAME;
}
break;
/* TX characteristic */
case CS_IDX_TX_VALUE_VAL:
{
length = CS_TX_VALUE_MAX_LENGTH;
valptr = (uint8_t *)&cs_env[device_indx].tx_value;
for(int n=0;n<5;n++)
{
PRINTF("====Read=== tx_value[%d]=0x%02x\n",n,cs_env[device_indx].tx_value[n]);
}
}
break;
case CS_IDX_TX_VALUE_CCC:
{
length = 2;
valptr = (uint8_t *)&cs_env[device_indx].tx_cccd_value;
PRINTF("====Read=== tx_cccd_value=0x%04x\n",cs_env[device_indx].tx_cccd_value);
}
break;
case CS_IDX_TX_VALUE_USR_DSCP:
{
length = strlen(CS_TX_CHARACTERISTIC_NAME);
valptr = (uint8_t *)CS_TX_CHARACTERISTIC_NAME;
}
break;
default:
{
status = ATT_ERR_READ_NOT_PERMITTED;
}
break;
}
}
/* Allocate and build message */
cfm = KE_MSG_ALLOC_DYN(GATTC_READ_CFM,
KE_BUILD_ID(TASK_GATTC, ble_env[device_indx].conidx),
TASK_APP,
gattc_read_cfm, length);
length=8;
memcpy(valptr, "eeworld", length);
if (valptr != NULL)
{
memcpy(cfm->value, valptr, length);
}
cfm->handle = param->handle;
cfm->length = length;
cfm->status = status;
/* Send the message */
ke_msg_send(cfm);
return (KE_MSG_CONSUMED);
}
主机开启通知或者关闭通知,发送数据给从机将调用ble_custom.c中的GATTC_WriteReqInd,先看看日志:
int GATTC_WriteReqInd(ke_msg_id_t const msg_id,
struct gattc_write_req_ind const *param,
ke_task_id_t const dest_id,
ke_task_id_t const src_id)
{
/* Retrieve the index of environment structure representing peer device */
signed int device_indx = Find_Connected_Device_Index(KE_IDX_GET(src_id));
if (device_indx == INVALID_DEV_IDX)
{
return (KE_MSG_CONSUMED);
}
struct gattc_write_cfm *cfm = KE_MSG_ALLOC(GATTC_WRITE_CFM,KE_BUILD_ID(TASK_GATTC, ble_env[device_indx].conidx),TASK_APP, gattc_write_cfm);
uint8_t status = GAP_ERR_NO_ERROR;
uint16_t attnum;
uint8_t *valptr = NULL;
/* Check that offset is not zero */
if (param->offset)
{
status = ATT_ERR_INVALID_OFFSET;
}
/* Set the attribute handle using the attribute index
* in the custom service */
if (param->handle > cs_env[device_indx].start_hdl)
{
attnum = (param->handle - cs_env[device_indx].start_hdl - 1);
}
else
{
status = ATT_ERR_INVALID_HANDLE;
}
PRINTF("[%s:%d %s]==== write === attnum=0x%02x param->handle=0x%04x cs_env[device_indx].start_hdl=0x%04x\n",__FILE__,__LINE__,__FUNCTION__,attnum,param->handle,cs_env[device_indx].start_hdl);
/* If there is no error, save the requested attribute value */
if (status == GAP_ERR_NO_ERROR)
{
switch (attnum)
{
case CS_IDX_RX_VALUE_VAL:
{
valptr = (uint8_t *)&cs_env[device_indx].rx_value;
cs_env[device_indx].rx_value_changed = true;
PRINTF("[%s:%d %s]========= rx_value ==========\n",__FILE__,__LINE__,__FUNCTION__);
}
break;
case CS_IDX_RX_VALUE_CCC:
{
valptr = (uint8_t *)&cs_env[device_indx].rx_cccd_value;
PRINTF("[%s:%d %s]=================== rx_cccd_value:0x%04x\n",__FILE__,__LINE__,__FUNCTION__,cs_env[device_indx].rx_cccd_value);
}
break;
case CS_IDX_TX_VALUE_CCC:
{
valptr = (uint8_t *)&cs_env[device_indx].tx_cccd_value;
//这是原来的值
PRINTF("[%s:%d %s]=================== tx_cccd_value:0x%04x\n",__FILE__,__LINE__,__FUNCTION__,cs_env[device_indx].tx_cccd_value);
//要写入的新值
PRINTF("[%s:%d %s]=================== will write 0x",__FILE__,__LINE__,__FUNCTION__);
//大端模式传输
for(int i=param->length-1;i>=0;i--)
{
PRINTF("%02x",param->value[i]);
}
PRINTF(" to it\n");
}
break;
default:
{
status = ATT_ERR_WRITE_NOT_PERMITTED;
}
break;
}
}
if (valptr != NULL)
{
memcpy(valptr, param->value, param->length);
}
cfm->handle = param->handle;
cfm->status = status;
/* Send the message */
ke_msg_send(cfm);
return (KE_MSG_CONSUMED);
}
程序中将valptr = (uint8_t *)&cs_env[device_indx].rx_value,然后主机发来数据(数据存储在 param->value中)attnum==CS_IDX_RX_VALUE_VAL,设置接收标志位cs_env[device_indx].rx_value_changed = true;最后调用memcopy将数据拷贝到valptr指向的内存即cs_env[device_indx].rx_value。我们就可以在app.c中将数据读取到:
#include "app.h"
#include <printf.h>
#include <stdio.h>
int main(void)
{
App_Initialize();
/* Debug/trace initialization. In order to enable UART or RTT trace,
* configure the 'OUTPUT_INTERFACE' macro in printf.h */
printf_init();
PRINTF("__peripheral_server has started!\n");
/* Main application loop:
* - Run the kernel scheduler
* - Send notifications for the battery voltage and RSSI values
* - Refresh the watchdog and wait for an interrupt before continuing */
while (1)
{
Kernel_Schedule();
for (int i = 0; i < NUM_MASTERS; i++)
{
if (ble_env[i].state == APPM_CONNECTED)
{
/* Send battery level if battery service is enabled */
if (app_env.send_batt_ntf[i] && bass_support_env[i].enable)
{
PRINTF("__SEND BATTERY LEVEL %d\n",app_env.batt_lvl);
app_env.send_batt_ntf[i] = false;
Batt_LevelUpdateSend(ble_env[i].conidx,app_env.batt_lvl, 0);
}
/* Update custom service characteristics, send notifications if notification is enabled */
if (cs_env[i].tx_value_changed && cs_env[i].sent_success)
{
cs_env[i].tx_value_changed = false;
(cs_env[i].val_notif)++;
if (cs_env[i].tx_cccd_value & ATT_CCC_START_NTF)
{
PRINTF("[%s:%d %s]==== notifications is enabled ====\n",__FILE__,__LINE__,__FUNCTION__);
//memset(cs_env[i].tx_value, cs_env[i].val_notif,CS_TX_VALUE_MAX_LENGTH);
//CustomService_SendNotification(ble_env[i].conidx,CS_IDX_TX_VALUE_VAL,&cs_env[i].tx_value[0],CS_TX_VALUE_MAX_LENGTH);
memset(cs_env[i].tx_value, cs_env[i].val_notif,1);
CustomService_SendNotification(ble_env[i].conidx,CS_IDX_TX_VALUE_VAL,&cs_env[i].tx_value[0],1);
}
}
if (cs_env[i].rx_value_changed)
{
PRINTF("[%s:%d %s]==== get data from phone start_hdl:0x%04x ====\n",__FILE__,__LINE__,__FUNCTION__,cs_env[i].start_hdl);
for (int j=0;j<5;j++)
{
PRINTF("[%s:%d %s]==== get data from phone %d:0x%02x ====\n",__FILE__,__LINE__,__FUNCTION__,j,cs_env[i].rx_value[j]);
}
cs_env[i].rx_value_changed=false;
}
}
}
/* Refresh the watchdog timer */
Sys_Watchdog_Refresh();
/* Wait for an event before executing the scheduler again */
SYS_WAIT_FOR_EVENT;
}
}
具体如下,首先判断接收标志,然后读取数据:
if (cs_env[i].rx_value_changed)
{
PRINTF("[%s:%d %s]==== get data from phone start_hdl:0x%04x ====\n",__FILE__,__LINE__,__FUNCTION__,cs_env[i].start_hdl);
for (int j=0;j<5;j++)
{
PRINTF("[%s:%d %s]==== get data from phone %d:0x%02x ====\n",__FILE__,__LINE__,__FUNCTION__,j,cs_env[i].rx_value[j]);
}
cs_env[i].rx_value_changed=false;
}
例子中定义的cs_env结构体变量中没有定义一个变量指示接收数据的实际长度,实际使用中可以加上,在接收函数中GATTC_WriteReqInd拷贝的数据的同时赋值数据长度。
开启或者关闭通知的话则会attnum==CS_IDX_TX_VALUE_CCC,开启通知主机向从机发送数据0x0001,关闭通知主机向从机发送数据0x0000,并把这个数据写入cs_env[device_indx].tx_cccd_value。例子中通知数据的改变标志cs_env.tx_value_changed是在app_process.c中APP_Timer函数中定时置位。
总结:有个疑问就是,在不自定义变量的情况,如何在主循环中收发数据的时候区分uuid呢,有没有什么api接口可以根据device_indx获得uuid,因为官方sdk现在还没精力去研究仔细。例程中attnum = (param->handle - cs_env[device_indx].start_hdl - 1);而start_hdl是sdk里面动态申请的(也可以自己设置固定值)。
最后附上一个修改了的例程。