ESP32-S3-BLE

一、GAP(Geneirc Access Profile)

1.GAP的作用

2.一个可扫描可连接的定向蓝牙连接过程

 二、GATT(Generic Attribute Profile)

1.GATT作用 

  • GATT是基于ATT的上层协议;

  • 数据是存储在服务器Server上的,数据格式是以一种Profile配置文件方式来存储的;
  • Profile下会有不同的Service服务,Service底下又有各种各样的Characteristic特征值;

2.Service的构成

  • 首先是Service的声明;
  • 然后是Serivice Include用于引用其他服务;
  • Characteristic特征;

2.1 Service的声明
  • Service声明的数据格式:(从左到右)
    • 属性的句柄;
    • 属性的类别;
      • 当客户端读到属性类型为2800或者2801就知道这一串数据就是服务声明的数据;
      • 2800指主服务PrimaryService它是可以被发现的;2801指次要服务Secondary Service是不可以被发现只能被引用;
    • 属性值;
      • 当知道属性类别是2800或者2801后就知道这里的属性值是代表当前Service UUID
    • 属性权限;

2.2 Service的包含
  • 表明当前的服务可以包含其他的Service;
  • 数据格式从左到右;
    • 属性的句柄;
    • 属性的类别;
      • 读到的属性类型为2802表明当前服务包含其他Service;
    • 属性值;
      • 当属性类型为2802时表示的是其他服务的信息;
        • 首先是包含其他servic起始句柄位置;
        • 然后是结束句柄位置;
        • 所包含服务的UUID
    • 属性权限;

2.3 Service的特征值
2.3.1特征值的构成
  • 2.3.1.1 特征声明
    • 属性的句柄;
    • 属性的类别;
      • 当读到的值为2803时表示当前这串数据描述的是特征值;
    • 属性值;
      • 当属性类别为2803时属性值表;
        • 第一个是它的属性;
        • 第二是特征值的句柄;
        • 第三是特征值的UUID;
    • 属性权限;

  • 2.3.1.2 特征值声明
    • 属性的句柄;
    • 属性的类别;
      • 对应的是特征声明中属性值中第三个成员的UUID
    • 属性值;
      • 即这个特征的特征值
    • 属性权限;

2.3.1.3 特征描述描述声明
  • 这个是用于针对特征值的可选补充信息声明,下面6条这些是可选配置;
    • 扩展属性:识别码2900;
    • 用户属性:识别码2901;
    • 客户端配置属性:识别码2902,客户端可以配置service端是否能发通知的行为;
    • service配置属性:识别码2903;
    • 显示属性:识别码2904;代表了如何解析我们的特征值
      • Attrubute Value从左到右
        • Format:特征值用什么格式表示
        • Exponent:特征值是否有指数的形式
        • Unit:特征值的单位;
    • 集合属性:识别码2905;

2.4 统一格式

  • Attribute Handle:属性句柄;
  • 属性类别;
  • 属性值;
  • 属性权限;
  • 上述格式即ATT属性协议规定的;
  • GATT赋予了属性不同的Attribute Type类别,根据这个类别来判断当前是哪一种类型的数据,从而继续Attribute Value的格式和内以及还可以读取下一条数据;

  • GATT不单单是ATT协议来来存储数据,还使用ATT协议来发送命令和数据
    • 命令的代码
    • 发送命令的输入参数
    • 发送命令的授权

三、GATT服务端工作流程

  •  初始化Flash

  • 释放控制器中经典蓝牙的内存空间

  • 初始化控制器的缺省配置

  • 初始化底层的蓝牙控制器

  • 使能控制器

  • 初始化bluedroid协议栈

  • 使能bluedroid协议栈

  • 调用gatts_register_callback,注册GATT_EVENT回调处理函数处理所有的GATT事件

  • 调用gap_register_callback,注册GAP_EVENT回调处理函数处理所有的GAP事件

  • 调用ggatts_app_register,注册一个API,ESP_APP_ID是底层协议栈用来区分profile配置文件,每一个配置文件对应不同的ID

  • 设置最大可传输单元MTU

  •  gap_event_handler中会处理客户端的扫描、连接等等GAP事件,确保客户端连接到服务器;
  • 在客户端连接成功后底层协议栈会上发GATT事件到gatts_event_handler中,在这里处理不同的GATT事件,完成客户端对服务器数据的读写修改操作;

 1.1 app_main

void app_main(void)
{
    esp_err_t ret;

    /* Initialize NVS. */
    ret = nvs_flash_init();     //初始化Flash
    if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
        ESP_ERROR_CHECK(nvs_flash_erase());
        ret = nvs_flash_init();
    }
    ESP_ERROR_CHECK( ret );

    ESP_ERROR_CHECK(esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT)); //释放控制器中经典蓝牙的内存空间

    esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();    //初始化控制器的缺省配置
    ret = esp_bt_controller_init(&bt_cfg);                                      //初始化底层的蓝牙控制器
    if (ret) {
        ESP_LOGE(GATTS_TABLE_TAG, "%s enable controller failed: %s", __func__, esp_err_to_name(ret));
        return;
    }

    ret = esp_bt_controller_enable(ESP_BT_MODE_BLE);                            //使能控制器
    if (ret) {
        ESP_LOGE(GATTS_TABLE_TAG, "%s enable controller failed: %s", __func__, esp_err_to_name(ret));
        return;
    }

    ret = esp_bluedroid_init();         //初始化bluedroid协议栈
    if (ret) {
        ESP_LOGE(GATTS_TABLE_TAG, "%s init bluetooth failed: %s", __func__, esp_err_to_name(ret));
        return;
    }

    ret = esp_bluedroid_enable();       //使能bluedroid协议栈
    if (ret) {
        ESP_LOGE(GATTS_TABLE_TAG, "%s enable bluetooth failed: %s", __func__, esp_err_to_name(ret));
        return;
    }

    ret = esp_ble_gatts_register_callback(gatts_event_handler); //调用gatts_register_callback,注册GATT_EVENT回调处理函数处理所有的GATT事件
    if (ret){
        ESP_LOGE(GATTS_TABLE_TAG, "gatts register error, error code = %x", ret);
        return;
    }

    ret = esp_ble_gap_register_callback(gap_event_handler);//调用gap_register_callback,注册GAP_EVENT回调处理函数处理所有的GAP事件
    if (ret){
        ESP_LOGE(GATTS_TABLE_TAG, "gap register error, error code = %x", ret);
        return;
    }

    ret = esp_ble_gatts_app_register(ESP_APP_ID);//调用ggatts_app_register,注册一个API,ESP_APP_ID是底层协议栈用来区分profile配置文件,每一个配置文件对应不同的ID
    if (ret){
        ESP_LOGE(GATTS_TABLE_TAG, "gatts app register error, error code = %x", ret);
        return;
    }

    esp_err_t local_mtu_ret = esp_ble_gatt_set_local_mtu(500);//设置最大可传输单元MTU
    if (local_mtu_ret){
        ESP_LOGE(GATTS_TABLE_TAG, "set local  MTU failed, error code = %x", local_mtu_ret);
    }
}

1.2 gatts_event_handler

static void gatts_profile_event_handler(esp_gatts_cb_event_t event,
					esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param);

/* One gatt-based profile one app_id and one gatts_if, this array will store the gatts_if returned by ESP_GATTS_REG_EVT */
//设置每一个profile对应的事件处理函数
static struct gatts_profile_inst heart_rate_profile_tab[PROFILE_NUM] = {
    [PROFILE_APP_IDX] = {
        .gatts_cb = gatts_profile_event_handler,
        .gatts_if = ESP_GATT_IF_NONE,       /* Not get the gatt_if, so initial is ESP_GATT_IF_NONE */
    },
};

static void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param)
{

    /* If event is register event, store the gatts_if for each profile */
    if (event == ESP_GATTS_REG_EVT) {                       //GATT注册事件
        if (param->reg.status == ESP_GATT_OK) {
            heart_rate_profile_tab[PROFILE_APP_IDX].gatts_if = gatts_if; //取得gatts接口,这个接口是服务端分配给每一个客户端的不同接口,底层协议返回给上层
        } else {
            ESP_LOGE(GATTS_TABLE_TAG, "reg app failed, app_id %04x, status %d",
                    param->reg.app_id,
                    param->reg.status);
            return;
        }
    }
    do {
        int idx;
        for (idx = 0; idx < PROFILE_NUM; idx++) {
            /* ESP_GATT_IF_NONE, not specify a certain gatt_if, need to call every profile cb function */
            if (gatts_if == ESP_GATT_IF_NONE || gatts_if == heart_rate_profile_tab[idx].gatts_if) { //当设置值没有连接或者取得gatt接口后调用每一个profile对应每一个的回调函数进一步处理事件
                if (heart_rate_profile_tab[idx].gatts_cb) {
                    heart_rate_profile_tab[idx].gatts_cb(event, gatts_if, param);
                }
            }
        }
    } while (0);
}

1.3 gap_event_handler

static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param)
{
    switch (event) {
    #ifdef CONFIG_SET_RAW_ADV_DATA  //原始广播数据宏
        case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT: //广播数据设置成功事件
            adv_config_done &= (~ADV_CONFIG_FLAG);
            if (adv_config_done == 0){
                esp_ble_gap_start_advertising(&adv_params);
            }
            break;
        case ESP_GAP_BLE_SCAN_RSP_DATA_RAW_SET_COMPLETE_EVT:    //扫描应答成功事件
            adv_config_done &= (~SCAN_RSP_CONFIG_FLAG);
            if (adv_config_done == 0){
                esp_ble_gap_start_advertising(&adv_params);
            }
            break;
    #else
        case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT:    
            adv_config_done &= (~ADV_CONFIG_FLAG);
            if (adv_config_done == 0){
                esp_ble_gap_start_advertising(&adv_params);
            }
            break;
        case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT:
            adv_config_done &= (~SCAN_RSP_CONFIG_FLAG);
            if (adv_config_done == 0){
                esp_ble_gap_start_advertising(&adv_params);
            }
            break;
    #endif
        case ESP_GAP_BLE_ADV_START_COMPLETE_EVT:     //广播开始完成事件
            /* advertising start complete event to indicate advertising start successfully or failed */
            if (param->adv_start_cmpl.status != ESP_BT_STATUS_SUCCESS) {
                ESP_LOGE(GATTS_TABLE_TAG, "advertising start failed");
            }else{
                ESP_LOGI(GATTS_TABLE_TAG, "advertising start successfully");
            }
            break;
        case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT:     //停止完成事件
            if (param->adv_stop_cmpl.status != ESP_BT_STATUS_SUCCESS) {
                ESP_LOGE(GATTS_TABLE_TAG, "Advertising stop failed");
            }
            else {
                ESP_LOGI(GATTS_TABLE_TAG, "Stop adv successfully\n");
            }
            break;
        case ESP_GAP_BLE_UPDATE_CONN_PARAMS_EVT:    //更新连接参数事件
            ESP_LOGI(GATTS_TABLE_TAG, "update connection params status = %d, min_int = %d, max_int = %d,conn_int = %d,latency = %d, timeout = %d",
                  param->update_conn_params.status,
                  param->update_conn_params.min_int,
                  param->update_conn_params.max_int,
                  param->update_conn_params.conn_int,
                  param->update_conn_params.latency,
                  param->update_conn_params.timeout);
            break;
        default:
            break;
    }
}

四、GATT服务端广播流程

1. 非定向广播的广播方

  • 主机的上层向下层的链路层发如下送命令
    • LE Set Advertising Parameters 设置广播参数
    • Read Advertising Channel Tx Power 读取广播通道的power
    • Set Advertising Data 设置广播数据
    • Set Scan Response Data 设置扫描应答数据
    • Set Advertising Enable  使能广播                                              
  • 链路成执行相应的命令后会返回Command Complete命令完成告知上层
  • 完成以上动作链路层开始广播

2. 非定向广播的被动扫描方

  • 主机的上层向下层的链路层发如下送命令
    • Set Scan Parameters 设置被动扫描的扫描参数
    • Set Scan Enable 设置扫描运行
  • 链路成执行相应的命令后会返回Command Complete命令完成告知上层
    • Advertising Report 上报设备B的广播数据到上层应用层

3. 非定向广播的主动扫描方

  • 主机的上层向下层的链路层发如下送命令
    • Set Scan Parameters 设置被动扫描的扫描参数
    • Set Scan Enable 设置扫描运行
  • 链路成执行相应的命令后会返回Command Complete命令完成告知上层
    • Advertising Report 上报设备B的广播数据到上层应用层
  • 链接层会向对端发送
    • SCAN_REQ 扫描请求命令
    • SCAN_RSP 对端广播设备返回扫描请求应答数据(广播数据携带最大31个字节,更多的信息如果在扫描的阶段知道就放在描请求应答数据中)

4 .代码上的实现

  • 首先注册了来区分profile配置文件APP ID(ESP_APP_ID) 后触发ESP_GATTS_REG_EVT事件在gatts_event_handler中注册的回调函数中处理

注册的回调函数 ESP_GATTS_REG_EVT事件代码中做了如下事情

  • esp_ble_gap_set_device_name 设置设备名

  • esp_ble_gap_config_adv_data_raw 设置广播数据内容:设置完后会触发 GAP中的ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT事件,判断广播配置是否完成对应一个Command Complete,然后开始广播esp_ble_gap_start_advertising

  • esp_ble_gap_config_scan_rsp_data_raw 配置扫描应答数据内容:设置完后会触发GAP中的 ESP_GAP_BLE_SCAN_RSP_DATA_RAW_SET_COMPLETE_EVT事件,判断广播配置是否完成对应一个Command Complete,然后开始广播

  • esp_ble_gap_start_advertising 触发GAP中ESP_GAP_BLE_ADV_START_COMPLETE_EVT 对应流程图最后的Command Complete是链路层发送回来广播是否成功

图中的流程为首先注册了APP ID对应当前的profile,然后触发gatt中注册的profile中的回调函数中的注册事件执行配置广播和描应答数据数据,这两个事件会触发GAP中的ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT和ESP_GAP_BLE_SCAN_RSP_DATA_RAW_SET_COMPLETE_EVT 再等待链路层返回Command Complete后开始广播esp_ble_gap_start_advertising 这个函数又会触发ESP_GAP_BLE_ADV_START_COMPLETE_EVT来返回广播状态。

五、 GATT服务器广播数据

1. 广播数据数据以及格式

  • 有效数据部分用AD Structure形式,无效数据部分用0填充;
  • AD structure结构组成部分
    • 第一部分是1个字节用来表示后面数据的长度,包含这个structure后面所有字节的长度:Length
    • Data第二部分为数据:Data
      • 第一小部分为数据类型:AD Type
      •  第二小部分为实际数据内容:AD Data

2. 代码实现 

static uint8_t raw_adv_data[] = {
        /* flags */
        0x02, 0x01, 0x06,
        /* tx power*/
        0x02, 0x0a, 0xeb,
        /* service uuid */
        0x03, 0x03, 0xFF, 0x00,
        /* device name */
        0x0f, 0x09, 'E', 'S', 'P', '_', 'G', 'A', 'T', 'T', 'S', '_', 'D','E', 'M', 'O'
};
static uint8_t raw_scan_rsp_data[] = {
        /* flags */
        0x02, 0x01, 0x06,
        /* tx power */
        0x02, 0x0a, 0xeb,
        /* service uuid */
        0x03, 0x03, 0xFF,0x00
};
  •  广播数据中设置的设备名/* device name */和程序在gatt_profile中设置的设备名不一样;后者是用来设备扫描后显示的名字,前者是用着广播数据中广播的名字;

  • 修改gatt_profile中设置的名字

  • 修改广播数据中的名字

  • legacy advertisingPDUs广播数据最大31个字节,extended advertisingPDUs最大254个字节;

六、 GATT客户端工作流程

  • 1

注册gatt_app ID时触发gatt中注册的profile中的回调函数中的ESP_GATTC_REG_EVT事件调用esp_ble_gap_set_scan_params设置扫描参数;

esp_ble_gap_set_scan_params再触发gap的cb的设置扫描参数完成事件ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT调用esp_ble_gap_start_scanning开始扫描;

esp_ble_gap_start_scanning再触发ESP_GAP_BLE_SCAN_START_COMPLETE_EVT扫描开始完成事件打印出开始扫描的结果是失败还是成功;

esp_ble_gap_start_scanning再触发ESP_GAP_BLE_SCAN_RESULT_EVT扫描结果事件,在这个事件中有两个子事件ESP_GAP_SEARCH_INQ_RES_EVT :用于扫描到每一个蓝牙服务器时在广播会触发该事件;ESP_GAP_SEARCH_INQ_CMPL_EVT:扫描完成所以的蓝牙主机事件;每一个扫描结果会通过ESP_GAP_SEARCH_INQ_RES_EVT上报到客户端这个事件中会调用esp_ble_resolve_adv_data解析每一个广播数据;如果扫描的设备是我们所需要的设备就会调用esp_ble_gap_stop_scanning停止扫描然后调用esp_ble_gap_gattc_open打开gatt的连接;

  • 2

esp_ble_gap_gattc_open再触发gatt中注册的profile中的回调函数中的ESP_GATTC_CONNECT_EVT以及ESP_GATTC_OPEN_EVT;ESP_GATTC_CONNECT_EVT会打印出连接和接口以及调用esp_ble_gattc_send_mtu_req再次设置MTU最大传输单元在初始化时已经设置过一次;ESP_GATT_OPEN_EVT主要是判断GATT打开是否失败还是成功并打印信息;

esp_ble_gattc_send_mtu_req再触发ESP_GATTC_DIS_SRVC_CMPL_EVT和ESP_GATTC_CFG_MTU_EVT;ESP_GATTC_DIS_SRVC_CMPL_EVT是发现服务是否成功寻找成功调用esp_ble_gattc_search_service去找服务器中的servic;ESP_GATTC_CFG_MTU_EVT是配置MTU是否成功;

esp_ble_gattc_search_service再触发ESP_GATTC_SEARCH_RES_EVT和ESP_GATT_SEARCH_CMPL_EVT;每找到一个service触发ESP_GATTC_SEARCH_RES_EVT找完所有service触发ESP_GATT_SEARCH_CMPL_EVT;ESP_GATTC_SEARCH_RES_EVT会保存每一个service的start_handle和end_handler;ESP_GATT_SEARCH_CMPL_EVT中调用esp_ble_gattc_get_attr_count去找属性的个数再用esp_ble_gattc_get_char_by_uuid通过UUID找属性如果这个属性有通知属性再用esp_ble_gattc_register_for_notify注册notify;

esp_ble_gattc_register_for_notify会触发ESP_GATTC_REG_FOR_NOTIFY_EVT调用esp_ble_gattc_write_char_descr写属性的描述符通过写这个值服务端才会主动发送数据到客户端;esp_ble_gattc_write_char_descr触发ESP_GATTC_WRITE_DESCR_EVT这个事件中调用esp_ble_gattc_write_char写一个特性值;

esp_ble_gattc_write_char触发ESP_GATTC_WRITE_CHAR_EVT中打印写是否成功失败; 在次之后如果服务端有发送通知来的数据就会触发ESP_GATTC_NOTIFY_EVT事件esp_log_buffer_hex把值打印出来;

七、 服务器端读取数据

  • 实例代码gatt_server_service_table
  • esp_gatts_attr_db_t gatt_db[HRS_IDX_NB]数组使用表格形式组织了蓝牙的数据;

  • 通过ESP_GATTS_READ_EVT来处理读事件,但是代码中该事件不做任何操作原因见下;

  • 数组数据中的ESP_GATT_AUTO_RSP:表示ESP32会自动回复读写的操作
  • 客户端读取服务器的值时自动回复特征值11223344,在这个例子中客户端读取数据时协议栈会自动回复上面数据,因为在数据中设置的是ESP_GATT_AUTO_RSP;

  • 将数组中所有的ESP_GATT_AUTO_RSP替换为ESP_GATT_RSP_BY_APP(编程返回)其他不做修改会导致读取数据失败然后断开连接;

  • 解决办法在ESP_GATTS_READ_EVT中添加返回数据给客户端 
    case ESP_GATTS_READ_EVT:
            ESP_LOGI(GATTS_TABLE_TAG, "ESP_GATTS_READ_EVT");
            esp_gatt_rsp_t rsp;                             //定义变量
            memset(&rsp, 0, sizeof(esp_gatt_rsp_t));        //初始化变量
            rsp.attr_value.handle = param->read.handle;     //设置读写句柄
            rsp.attr_value.len = 4;                         //数据长度
            rsp.attr_value.value[0] = 0xde;                 //数据值
            rsp.attr_value.value[1] = 0xed;
            rsp.attr_value.value[2] = 0xbe;
            rsp.attr_value.value[3] = 0xef;
            esp_ble_gatts_send_response(gatts_if, param->read.conn_id, param->read.trans_id,
                                        ESP_GATT_OK, &rsp);
            break;

八、 客户端读取数据处理

  • 实例代码gatt_client(实验需要两个开发板一个做服务端另外一个是客户端)
  • 基于GATT客户端的广播流程;先取消掉客户端对服务设置的Notify功能,然后添加读取指定服务器端特定特征值的功能;这里指定读取服务端FF02的特征值
 esp_ble_gatts_read_char(gattc_if,   //gattc 的接口
                    p_data->search_cmpl.conn_id,        //连接的ID
                    char_elem_result[0].char_handle,    //特征值的句柄
                    ESP_GATT_AUTH_REQ_NONE);            //授权模式

  • 服务器上的特征配置:给FF02特征值配置的值是cha_value_b[4]

  • 在esp_ble_gatts_read_char读取数据的时候ESP32客户端会触发读取特征值的事件处理;自己添加一个读取特征值的ESP_GATTC_READ_CHAR_EVT事件代码;即读取到特征值后蓝牙协议栈会触发ESP_GATTC_READ_CHAR_EVT事件;
case ESP_GATTC_READ_CHAR_EVT:
        ESP_LOGI(GATTC_TAG, "ESP_GATTC_READ_CHAE_EVT");
        ESP_LOGI(GATTC_TAG, "p_data->read.value_len=%d", p_data->read.value_len);
        esp_log_buffer_hex(GATTC_TAG, p_data->read.value, p_data->read.value_len);
        break;

  • 上面是服务器,下面是客户端读取回服务端的数据;

九、服务端写数据处理

  • 即服务端处理客户端写过来的数据
    • 对于服务端如果写入的数据小于或等于MTU-3时,服务端触发ESP_GATTS_WRITE_EVT在这个事件中接收处理数据;
    • 大于MTU-3会先把所有数据放入ESP_GATTS_WRITE_EVT事件在的prepare_buf接收完所以的数据后再触发ESP_GATTS_EXEC_WRITE_EVT,中处理prepare_buf数据;

  •  手机端向服务端写入数据会触发ESP_GATTS_WRITE_EVT事件
  • 在事件中
    • 首先判断是写入哪一个特征值的句柄这里指定是CHAR_VAL_A
    • 然后判断写入数据的长短是不是为1,写的数据为1开启LED,为2关闭LED;
case ESP_GATTS_WRITE_EVT:
        configure_led(); //配置IO
        //判断写入的是否为VAL_A特征值的特征值句柄
        if(heart_rate_handle_table[IDX_CHAR_VAL_A] == param->write.handle)
        {
            //判读写的数据长度是否是1个bit和所写数据的内容
            if(param->write.len == 1 && param->write.value[0] == 1)
            {
                //等于1打开LED
                blink_led(1);
            }else if (param->write.len == 1 && param->write.value[0] == 2)
            {
                //等于2关闭LED
                blink_led(0);
            }
            else
            {
                ESP_LOGI(GATTS_TABLE_TAG, "wrong led command!");
            }
        }

  • 26
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值