1. 程式人生 > >藍芽:GATT,屬性,特性,服務

藍芽:GATT,屬性,特性,服務

接著上一篇。

通用屬性配置檔案(Generic Attribute Profile)

1.GATT簡介

通用屬性配置檔案Generic Attribute Profile簡稱GATT。
GATT定義了屬性型別並規定了如何使用,包括了一個數據傳輸和儲存的框架和一些基本操作。中間包含了一些概念如特性characteristics,服務services等,在後面介紹。同時還定義了發現服務,特性和服務間的連線的處理過程,也包括讀寫特性值。

1.1.GATT的角色

GATT中定義了2種角色:伺服器server和客戶端client.
GATT的伺服器是指提供資料的裝置,而GATT的客戶端是指通過GATT的伺服器獲取資料的裝置。

1.2.GATT的層次

GATT伺服器通過一個屬性表來組織傳送的資料。這裡的關係大概是這樣:
這裡寫圖片描述
一個Profile中可包含一個或者多個服務;一個服務可包含一個或者多個特性(邏輯上的集合);一個特性至少包含兩個屬性:一個用於宣告,其他用於儲存屬性值。

2.屬性(Attribute)

一個屬性包括控制代碼,UUID,值。
控制代碼是屬性在GATT表中的索引,在一個裝置中每個屬性的控制代碼是唯一的。UUID包括屬性表中的資料型別資訊,它是理解屬性表中值的每一個位元組的關鍵資訊。GATT表中可能有多個屬性擁有相同的UUID值。
屬性相關的資料結構:

/**@brief 屬性元資料. */
typedef struct { ble_gap_conn_sec_mode_t read_perm; /**< 讀許可權 */ ble_gap_conn_sec_mode_t write_perm; /**< 寫許可權 */ uint8_t vlen :1; /**< 屬性長度. */ uint8_t vloc :2; /**< 值存放的位置, 詳情參照BLE_GATTS_VLOCS.*/ uint8_t rd_auth :1
; /**< 讀許可,該值會在每次讀操作後向應用重新發起請求*/ uint8_t wr_auth :1; /**< 寫許可會在每次讀操作後向應用重新發起請求(但不寫命令). */ } ble_gatts_attr_md_t; /**@brief GATT 屬性. */ typedef struct { ble_uuid_t* p_uuid; /**< 指向屬性的UUID */ ble_gatts_attr_md_t* p_attr_md; /**< 指向屬性元資料的資料結構*/ uint16_t init_len; /**< 初始化屬性長度 */ uint16_t init_offs; /**< 初始化屬性偏移 */ uint16_t max_len; /**< 最大屬性長度,參照BLE_GATTS_ATTR_LENS_MAX*/ uint8_t* p_value; /**< 指向屬性資料。請注意如果 BLE_GATTS_VLOC_USER值的位置被選在attribute metadata結構體中,則需指向有效生命週期緩衝區。協議棧可能在沒有得到應用程式許可的情況下直接操作 */ } ble_gatts_attr_t; /**@brief GATT 屬性內容. */ typedef struct { ble_uuid_t srvc_uuid; /**< 服務UUID */ ble_uuid_t char_uuid; /**< 特性的UUID(BLE_UUID_TYPE_UNKNOWN無效). */ ble_uuid_t desc_uuid; /**< 描述UUID(BLE_UUID_TYPE_UNKNOWN無效). */ uint16_t srvc_handle; /**< 服務控制代碼 */ uint16_t value_handle; /**< 特性的處理控制代碼(BLE_GATT_HANDLE_INVALID 無效). */ uint8_t type; /**< 屬性型別, 參照BLE_GATTS_ATTR_TYPES. */ } ble_gatts_attr_context_t;

描述符:
在任何特性中的屬性不是定義為屬性值就是描述符。描述符是一個額外的屬性,以提供更多特性的資訊。這裡有個特殊的描述符:客戶端特性配置描述符cccd。這個描述符是給任意支援通知或指示功能的特性額外新增的。在cccd中寫入1為使能通知功能,寫入2使能指示功能,寫0禁止通知和指示功能。

UUID:
在GATT規範中定義所有的屬性都必須要有一個UUID值,UUID是全球唯一的128位的資料,用來識別不同的特性。
藍芽核心規範制定了兩種不同的UUID,一種是基本的UUID,一種是代替基本UUID的16為UUID。
我們協議棧的後面那裡使用的就是代替基本UUID的16為UUID:先增加一個特定的基本UUID,在定義一個十六位的UUID。載入到基本UUID之上。原始碼中UUID如下:

#define LBS_UUID_BASE {0x23, 0xD1, 0xBC, 0xEA, 0x5F, 0x78, 0x23, 0x15, 0xDE, 0xEF, 0x12, 0x12, 0x00, 0x00, 0x00, 0x00}
#define LBS_UUID_SERVICE 0x1523
#define LBS_UUID_LED_CHAR 0x1525
#define LBS_UUID_BUTTON_CHAR 0x1524
//新增一個特定的基本UUID
ble_uuid128_t base_uuid = {LBS_UUID_BASE};
err_code = sd_ble_uuid_vs_add(&base_uuid, &p_lbs->uuid_type);

UUID資料結構:

/** @brief 128 bit UUID values. */
typedef struct
{ 
    unsigned char uuid128[16];     
} ble_uuid128_t;

/** @brief  Bluetooth Low Energy UUID type, encapsulates both 16-bit and 128-bit UUIDs. */
typedef struct
{
    uint16_t    uuid; /**< 16-bit UUID value or octets 12-13 of 128-bit UUID. */
    uint8_t     type; /**< UUID type, see @ref BLE_UUID_TYPES. If type is BLE_UUID_TYPE_UNKNOWN, the value of uuid is undefined. */
} ble_uuid_t;

3.特性(Characteristics)

一個特性至少包含2個屬性:一個用於宣告,一個用於儲存屬性值。通過GATT服務傳輸的資料必須對映成一系列的特性。可以把特性中的資料看成是一個個捆綁起來的資料,每個特性就是一個數據點。原始碼中特性的資料結構如下:

/**@brief GATT 特性表示格式 */
typedef struct
{
  uint8_t          format;      /**< 值的格式,參照BLE_GATT_CPF_FORMATS. */
  int8_t           exponent;    /**< 整數型別的指數 */
  uint16_t         unit;        /**< 藍芽中分配的UUID*/
  uint8_t          name_space;  /**< 從藍芽分配的名稱空間,參見BLE_GATT_CPF_NAMESPACES.*/
  uint16_t         desc;        /**< 從藍芽分配的名稱空間描述,參見BLE_GATT_CPF_NAMESPACES.*/
} ble_gatts_char_pf_t;

/**@brief GATT 特性性質 */
typedef struct
{
  /* 標準性質 */
  uint8_t broadcast       :1; /**< 是否允許廣播 */
  uint8_t read            :1; /**< 是否允許讀操作 */
  uint8_t write_wo_resp   :1; /**< 是否允許命令寫修改操作 */
  uint8_t write           :1; /**< 是否允許請求寫修改操作*/
  uint8_t notify          :1; /**< 是否允許通知修改 */
  uint8_t indicate        :1; /**< 是否允許指示修改 */
  uint8_t auth_signed_wr  :1; /**< 是否允許簽名寫命令寫入*/
} ble_gatt_char_props_t;

/**@brief GATT 特性元資料. */
typedef struct
{
  ble_gatt_char_props_t       char_props;               /**< 特性性質 */
  ble_gatt_char_ext_props_t   char_ext_props;           /**< 特性拓展性質 */
  uint8_t*                    p_char_user_desc;         /**< 指向UTF-8, NULL則無要求 */
  uint16_t                    char_user_desc_max_size;  /**< 使用者描述符的最大位元組長 */
  uint16_t                    char_user_desc_size;      /**< 使用者描述位元組, 必須小於等於char_user_desc_max_size */ 
  ble_gatts_char_pf_t*        p_char_pf;                /**< 指向現存的結構體格式,NULL則對描述符不做要求 */
  ble_gatts_attr_md_t*        p_user_desc_md;           /**< 使用者描述符的Attribute元資料,NULL為預設值 */
  ble_gatts_attr_md_t*        p_cccd_md;                /**< cccd的Attribute元資料,NULL為預設值 */
  ble_gatts_attr_md_t*        p_sccd_md;                /**< sccd的Attribute元資料,NULL為預設值  */
} ble_gatts_char_md_t;


/**@brief GATT 特性定義控制代碼 */
typedef struct
{
  uint16_t          value_handle;       /**< 處理特徵值控制代碼 */
  uint16_t          user_desc_handle;   /**< 處理使用者描述符的控制代碼,BLE_GATT_HANDLE_INVALID為不存在 */
  uint16_t          cccd_handle;        /**< 處理CCCD的控制代碼,BLE_GATT_HANDLE_INVALID 為不存在*/
  uint16_t          sccd_handle;        /**< 處理SCCD的控制代碼,BLE_GATT_HANDLE_INVALID 為不存在. */
} ble_gatts_char_handles_t;

ble_gatts_char_md_t結構體中的ble_gatt_char_props_t和ble_gatt_char_ext_props_t型別中記錄了GATT的標準和拓展特性。典型的特性包括:
讀:GATT客戶端可以從GATT伺服器中讀取特性值
寫和沒有迴應的寫:允許GATT客戶端寫入一個值到GATT伺服器的特性中。沒有迴應的寫沒有任何應用層上的確認或迴應。
通知:允許GATT伺服器在某個特性該改變的時候對GATT客戶端進行提醒。
指示:允許GATT伺服器在某個特性該改變的時候對GATT客戶端進行提醒,並在應用層上確認。
廣播。

4.服務(serive)

一個服務包括一個或者多個特性,這些特性是邏輯上的集合體。

5.原始碼部分分析

主程式中這些部分主要是對GATT相關內容的操作。這裡的藍芽在GATT中是作為伺服器的角色。

5.1.連線引數

conn_params_init();     //初始化連線引數

static void conn_params_init(void)
{
    uint32_t               err_code;
    ble_conn_params_init_t cp_init;
    memset(&cp_init, 0, sizeof(cp_init));
    cp_init.p_conn_params                  = NULL;
    cp_init.first_conn_params_update_delay = FIRST_CONN_PARAMS_UPDATE_DELAY;
    cp_init.next_conn_params_update_delay  = NEXT_CONN_PARAMS_UPDATE_DELAY;
    cp_init.max_conn_params_update_count   = MAX_CONN_PARAMS_UPDATE_COUNT;
    cp_init.start_on_notify_cccd_handle    = BLE_GATT_HANDLE_INVALID;
    cp_init.disconnect_on_fail             = false;
    cp_init.evt_handler                    = on_conn_params_evt;
    cp_init.error_handler                  = conn_params_error_handler;

    err_code = ble_conn_params_init(&cp_init);   //初始化
    APP_ERROR_CHECK(err_code);
}

uint32_t ble_conn_params_init(const ble_conn_params_init_t * p_init)
{
    uint32_t err_code;
    m_conn_params_config = *p_init;
    m_change_param = false;
    if (p_init->p_conn_params != NULL)
    {
        m_preferred_conn_params = *p_init->p_conn_params;   
        // 設定棧的連線引數
        err_code = sd_ble_gap_ppcp_set(&m_preferred_conn_params);
        if (err_code != NRF_SUCCESS)
        {
            return err_code;
        }
    }
    else
    {
        // 從棧上獲取連線引數
        err_code = sd_ble_gap_ppcp_get(&m_preferred_conn_params);
        if (err_code != NRF_SUCCESS)
        {
            return err_code;
        }
    }
    m_conn_handle  = BLE_CONN_HANDLE_INVALID;
    m_update_count = 0;
    return app_timer_create(&m_conn_params_timer_id,
                            APP_TIMER_MODE_SINGLE_SHOT,
                            update_timeout_handler);
}

該函式中定義了一個連線引數結構體資料cp_init,用來儲存連線引數。再呼叫ble_conn_params_init函式進行初始化,同時建立一個定時器。

5.2.自定義服務

該程式實現一個按鍵通知LED點亮的功能。
該部分程式是在services_init函式中新增的

typedef struct
{
    ble_lbs_led_write_handler_t led_write_handler;  /* 當LED的特性被寫時呼叫的回撥函式 */
} ble_lbs_init_t;

static void services_init(void)
{
    uint32_t err_code;
    ble_lbs_init_t init;

    init.led_write_handler = led_write_handler;

    err_code = ble_lbs_init(&m_lbs, &init);
    APP_ERROR_CHECK(err_code);
}

在函式職工定義了一個init變數,然後繫結到led_write_handler這個回撥函式上,再呼叫ble_lbs_init進行初始化。在該函式中必須要新增服務,得到服務控制代碼。

uint32_t ble_lbs_init(ble_lbs_t * p_lbs, const ble_lbs_init_t * p_lbs_init)
{
    uint32_t   err_code;
    ble_uuid_t ble_uuid;

    // 初始化服務結構體
    p_lbs->conn_handle       = BLE_CONN_HANDLE_INVALID;
    p_lbs->led_write_handler = p_lbs_init->led_write_handler;

    // 新增服務
    ble_uuid128_t base_uuid = {LBS_UUID_BASE};
    err_code = sd_ble_uuid_vs_add(&base_uuid, &p_lbs->uuid_type); //繫結baseUUID
    if (err_code != NRF_SUCCESS)
    {
        return err_code;
    }

    ble_uuid.type = p_lbs->uuid_type;
    ble_uuid.uuid = LBS_UUID_SERVICE;

    //這裡是新增server服務UUID為0x1523,並得到一個唯一的控制代碼
    err_code = sd_ble_gatts_service_add(BLE_GATTS_SRVC_TYPE_PRIMARY, &ble_uuid, &p_lbs->service_handle);
    if (err_code != NRF_SUCCESS)
    {
        return err_code;
    }

    //這裡是新增button服務 UUID為0x1524
    err_code = button_char_add(p_lbs, p_lbs_init);
    if (err_code != NRF_SUCCESS)
    {
        return err_code;
    }

    //這裡是新增led服務 UUID為0x1525
    err_code = led_char_add(p_lbs, p_lbs_init);
    if (err_code != NRF_SUCCESS)
    {
        return err_code;
    }

    return NRF_SUCCESS;
}

下面舉例button_char_add()。led_char_add函式的過程也與此類似。

static uint32_t button_char_add(ble_lbs_t * p_lbs, const ble_lbs_init_t * p_lbs_init)
{
    ble_gatts_char_md_t char_md;
    ble_gatts_attr_md_t cccd_md;
    ble_gatts_attr_t    attr_char_value;
    ble_uuid_t          ble_uuid;
    ble_gatts_attr_md_t attr_md;

    memset(&cccd_md, 0, sizeof(cccd_md));
    //設定宣告屬性值
    BLE_GAP_CONN_SEC_MODE_SET_OPEN(&cccd_md.read_perm);
    BLE_GAP_CONN_SEC_MODE_SET_OPEN(&cccd_md.write_perm);
    cccd_md.vloc = BLE_GATTS_VLOC_STACK; //屬性值分配在棧記憶體上

    memset(&char_md, 0, sizeof(char_md));
    //設定特性值
    char_md.char_props.read   = 1;
    char_md.char_props.notify = 1;
    char_md.p_char_user_desc  = NULL;
    char_md.p_char_pf         = NULL;
    char_md.p_user_desc_md    = NULL;
    char_md.p_cccd_md         = &cccd_md;
    char_md.p_sccd_md         = NULL;

    //設定按鍵UUID
    ble_uuid.type = p_lbs->uuid_type;
    ble_uuid.uuid = LBS_UUID_BUTTON_CHAR;

    memset(&attr_md, 0, sizeof(attr_md));
    //設定自定義屬性值
    BLE_GAP_CONN_SEC_MODE_SET_OPEN(&attr_md.read_perm);
    BLE_GAP_CONN_SEC_MODE_SET_NO_ACCESS(&attr_md.write_perm);
    attr_md.vloc       = BLE_GATTS_VLOC_STACK;
    attr_md.rd_auth    = 0;
    attr_md.wr_auth    = 0;
    attr_md.vlen       = 0;

    memset(&attr_char_value, 0, sizeof(attr_char_value));
    //設定屬性值
    attr_char_value.p_uuid       = &ble_uuid;
    attr_char_value.p_attr_md    = &attr_md;
    attr_char_value.init_len     = sizeof(uint8_t);
    attr_char_value.init_offs    = 0;
    attr_char_value.max_len      = sizeof(uint8_t);
    attr_char_value.p_value      = NULL;

    //新增按鍵服務
    return sd_ble_gatts_characteristic_add(p_lbs->service_handle, &char_md,
                                               &attr_char_value,
                                               &p_lbs->button_char_handles);
}

sd_ble_gatts_characteristic_add函式用來新增特性。第一個引數是要新增到的服務的控制代碼,第二個引數是特性值,在它上面繫結一個cccd的屬性元資料(描述符),第三個引數值屬性的描述,中間綁定了ble按鍵的UUID值和按鍵屬性的元資料,第四個引數是返回的按鍵的特性和描述符的唯一控制代碼。

前面大概講過,當協議棧啟動之後,BLE的事件響應在初始化的時候被繫結到ble_evt_dispatch函式中了。而在這個函式中有一個ble_lbs_on_ble_evt的事件監測函式,程式碼如下:

void ble_lbs_on_ble_evt(ble_lbs_t * p_lbs, ble_evt_t * p_ble_evt)
{
    switch (p_ble_evt->header.evt_id)
    {
        case BLE_GAP_EVT_CONNECTED:
            on_connect(p_lbs, p_ble_evt);
            break;

        case BLE_GAP_EVT_DISCONNECTED:
            on_disconnect(p_lbs, p_ble_evt);
            break;

        case BLE_GATTS_EVT_WRITE:
            on_write(p_lbs, p_ble_evt);
            break;

        default:
            break;
    }
}
static void on_disconnect(ble_lbs_t * p_lbs, ble_evt_t * p_ble_evt)
{
    UNUSED_PARAMETER(p_ble_evt);
    p_lbs->conn_handle = BLE_CONN_HANDLE_INVALID;
}
static void on_connect(ble_lbs_t * p_lbs, ble_evt_t * p_ble_evt)
{
    p_lbs->conn_handle = p_ble_evt->evt.gap_evt.conn_handle;
}
//寫入回撥函式,在初始化的時候別繫結。當有寫入時回撥
static void led_write_handler(ble_lbs_t * p_lbs, uint8_t led_state)
{
    if (led_state)
    {
        nrf_gpio_pin_set(LEDBUTTON_LED_PIN_NO);
    }
    else
    {
        nrf_gpio_pin_clear(LEDBUTTON_LED_PIN_NO);
    }
}

連線事件和斷開事件呼叫函式on_connect和on_disconnect,和官方提供的on_connect和on_disconnect處理差不多。

typedef struct
{
  ble_evt_hdr_t header;                 /**< Event header. */
  union
  {
    ble_common_evt_t  common_evt; /**< Common Event, evt_id in BLE_EVT_* series. */
    ble_gap_evt_t     gap_evt;  /**< GAP originated event, evt_id in BLE_GAP_EVT_* series. */
    ble_l2cap_evt_t   l2cap_evt; /**< L2CAP originated event, evt_id in BLE_L2CAP_EVT* series. */
    ble_gattc_evt_t   gattc_evt; /**< GATT client originated event, evt_id in BLE_GATTC_EVT* series. */
    ble_gatts_evt_t   gatts_evt; /**< GATT server originated event, evt_id in BLE_GATTS_EVT* series. */
  } evt;
} ble_evt_t;


static void on_write(ble_lbs_t * p_lbs, ble_evt_t * p_ble_evt)
{
    ble_gatts_evt_write_t * p_evt_write = &p_ble_evt->evt.gatts_evt.params.write;

    if ((p_evt_write->handle == p_lbs->led_char_handles.value_handle) &&
        (p_evt_write->len == 1) &&
        (p_lbs->led_write_handler != NULL))
    {
        p_lbs->led_write_handler(p_lbs, p_evt_write->data[0]);
    }
}

這裡會從事件結構體ble_evt_t中獲取(強制轉換)GATTS的事件資訊(ble_gatts_evt_write_t結構體),然後根據寫入的資訊完成回撥函式的呼叫。至此為一個服務呼叫的流程。