1880|0

1239

帖子

68

TA的资源

纯净的硅(中级)

楼主
 

RSL1蓝牙特征值读写 [复制链接]

  本帖最后由 dql2016 于 2021-6-28 20:07 编辑

蓝牙广播只能单向发送数据且数据可以被任何其它设备接收,无法实现设备接收数据且不安全。而我的项目中不仅需要蓝牙设备发送数据给微信小程序而且需要微信小程序给蓝牙设备发送数据,因此只能放弃使用广播方式通信。

通过查阅资料得知,要实现蓝牙设备与微信小程序的双向数据收发,需要通过自定义服务,然后读写特征值。首先学习一下后面程序中经常使用到的概念。

  • 每个蓝牙设备可以同时扮演一个或者多个GAP角色,角色有2种,分别是中央设备(central device)外围设备(peripheral device) 。GAP角色与建立连接有关,外围设备可以发送广播,以使其它设备知道它的存在,但是只有中央设备可以发送连接请求以建立连接。
  • 一个蓝牙设备可以同时扮演中央设备和外围设备的角色。比如,智能手机在连接了运动手环的时候扮演的是中央设备角色,但与此同时它还扮演外围设备的角色和电脑连接了。安森美官方也提供了多种角色、混合角色的例子:

在 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.下载附件  保存到相册

2021-6-28 15:48 上传

由于sdk里面使用了大量的回调机制,直接从初始化代码看起不容易弄清程序流程,所以先从日志看起:

可以看到第一条日志是__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里面动态申请的(也可以自己设置固定值)。

最后附上一个修改了的例程。

peripheral_server.rar (619.09 KB, 下载次数: 4)

点赞 关注
 
 

回复
举报
您需要登录后才可以回帖 登录 | 注册

随便看看
查找数据手册?

EEWorld Datasheet 技术支持

相关文章 更多>>
关闭
站长推荐上一条 1/9 下一条

 
EEWorld订阅号

 
EEWorld服务号

 
汽车开发圈

About Us 关于我们 客户服务 联系方式 器件索引 网站地图 最新更新 手机版

站点相关: 国产芯 安防电子 汽车电子 手机便携 工业控制 家用电子 医疗电子 测试测量 网络通信 物联网

北京市海淀区中关村大街18号B座15层1530室 电话:(010)82350740 邮编:100190

电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 电信业务审批[2006]字第258号函 京公网安备 11010802033920号 Copyright © 2005-2025 EEWORLD.com.cn, Inc. All rights reserved
快速回复 返回顶部 返回列表