본문 바로가기

MCU/Bluetooth

Bluetooth: ESP32 - examples - gatts_table_create_demo

728x90
반응형

GATT Server

 

app_main

  - nvs_flash_init(),

    esp_bt_controller_init(), esp_bt_controller_enable(),

    esp_bluedroid_init(), esp_bluedroid_enable()

    esp_ble_gap_register_callback(gap_event_handler);

    까지는 일반적인 bluetooth초기화 과정과 동일

 

  gatts example이므로 esp_ble_gatts_register_callback(gatts_event_handler); 있음

  

  esp_ble_gatts_app_register(ESP_APP_ID);
  esp_ble_gatt_set_local_mtu(500);

  잘 못보던 두가지 함수

  

esp_ble_gatts_app_register(ESP_APP_ID)

  - 블루투스 스택에 gatt server로 사용할 것임을 알리며, 서비스 ID를 등록한다.

  - 여러가지 서비스를 등록하여 관리할 수 있다.

  * 항상 App 이라는 말이 헷갈린다.

    Bluetooth에서 App 이라 함은 '특정한 기능이나 서비스를 제공하는 블루투스 스택 안의 논리적인 결정체이다'

    더 쉽게 표현하면, '특정 서비스를 제공하는 블루투스 내의 객체'  정도가 될 것 같다.

 

  * 다양한 App을 등록해서, 하나의 블루투스 스택으로 다양한 서비스를 제공할 수 있다.

   - 심장박동 측정 서비스와 배터리 레벨 서비스 등.

   - 여러가지 서비스를 등록 할 수 있어서, 멀티 페어링 기능을 gatts로 구현할 수 있을까? 라는 생각을 했는데.

     멀티 페어링을 할 때 client가 server를 인식하는 것은 물리적인 레벨이지 서비스(앱) 레벨이 아니기 때문에

     다양한 서비스를 이용한다고 해서 여러 기기처럼 인식하게 만들 수는 없다.

 

esp_ble_gatt_set_local_mtu(500);

  MTU:   Maximum Transmission Unit

  한번에 전송할 수 있는 패킷의 단위를 의미한다.

  BLE에서 기본으로 적용되어 있는 MTU는 23 (byte)라고 한다.

  이를 500 (byte)까지 올리는 설정인데,

  올린다고 무조건 이렇게 보내는 것이 아니라 client 설정과 비교하여 둘 중 작은 maximum 값으로 정해진다고 보면 된다.

 

 

* gatts의 event를 handling하는 gatts_event_handler를 살펴보자

void gatts_event_handler()

gatts_event_handler에는 3개의 paramerter가 있다.

- esp_gatts_cb_event_t event,

- esp_gatt_if_t gatts_if,

- esp_ble_gatts_cb_param_t *param

 

event는 말그대로 event, param*에는 event 별로 특징적인 값들을 담아서 넘겨준다.

gatts_if 의 의미가 항상 잘 와닿지가 않았다. 자세히 한번 살펴보자

 

gatts_if

GATT Server interface number 를 의미한다.

  - 양식상으로는 number (0 이상의 정수)이고

  - 의미상으로는 interface index를 가리키는 것이다.

* interface란?

  인터페이스란 용어는 항상 헷갈리는데.

  다른 사물(이나 시스템) 간의 의사소통이 가능하도록 해주는 매개체이다.

  다른 사물간은 매개체 없이 의사소통이 불가능하다.

  예를 들어 사람-컴퓨터는 인간의 언어, 또는 전기신호 만으로 의사소통을 할 수 없다.

  사람의 언어를 표현하는 입력장치가 컴퓨터의 전기신호로 변환될 수 있는 매개체가 있어야 한다.

  마찬가지로 컴퓨터의 전기신호가 사람의 언어로 표현될 수 있어야 상호 소통이 가능해진다.

  예를 들어 컴퓨터의 전기신호 -> binary 10 -> 십진수 2 와 같은 과정을 거쳐야 인간이 이해할 수 있는 방식으로 전달된다.

  이를 해석해주는 것은 당연히 정해진 '규칙'이다. 정해진 '규칙'은 '언어'와도 같다고 볼 수 있다.

  결국, 인터페이스는 '의사소통 매개체'라고 표현을 할 수 있고,

  '매개체'는 '규칙'을 통해서 매개의 역할을 하는 것이며,

  규칙을 통해서 매개의 역할을 하므로, 일종의 '언어'와도 같다고 볼수  있는 것이다.

   

다시 gatts_if로 넘어오자.

esp_ble_gatts_app_register(ESP_APP_ID);를 통해서 gatt server가 블루투스 스택에 app 등록을 할 때,

'블루투스 스택'과 'app' 사이에 interface (의사소통 매개체)가 생긴다.

블루투스 스택은 app을 등록하면서 gatts_if를 app에 부여한다. 1, 2, 3 등과 같이 말이다.

이는 id라고 볼 수 있다.

 

왜 필요할까?

gatt server app은 여러가지가 될 수 있다.

하나의 블루투스 스택이 여러가지 gatt server app 을 관리하게 되는 것인데,

특정 gatts event가 발생했을 때, 여러 gatts app 중에 어떤 것에서 발생한 event인지를 gatts_if를 통해서 알려준다.

 

그럼 APP_ID는?

esp_ble_gatts_app_register(ESP_APP_ID) 함수를 통해서

사용자가 직접 지정해준 ESP_APP_ID를 가지고 앱등록을 했다.

따라서 app은 사용자가 지정해준 ID를 갖는다.

그리고 블루투스 스택은 interface를 생성하면서 gatts_if를 app에 부여한다.

따라서 사용자 지정 APP_ID와 gatts_if는 1:1의 관계가 생기게 된다. (같은 것은 아니다.)

 

위에서 언급한 esp_ble_gatts_app_register(ESP_APP_ID) 이벤트가  발생하면,

ESP_GATTS_REG_EVT의 event로 들어오게 되고,

여기서 gatt server로 app을 등록한 profile에 gatts_if를 받아와서 값을 넣어주면 된다.

/* One gatt-based profile one app_id and one gatts_if, this array will store the gatts_if returned by ESP_GATTS_REG_EVT */
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) {
        if (param->reg.status == ESP_GATT_OK) {
            heart_rate_profile_tab[PROFILE_APP_IDX].gatts_if = gatts_if;  // 이 부분이다.
        } else {
            ESP_LOGE(GATTS_TABLE_TAG, "reg app failed, app_id %04x, status %d",
                    param->reg.app_id,
                    param->reg.status);
            return;
        }
    }

    ...

 

 

Read, Write, Notify, Indicate

gatts와 gattc간에 등록된 service(app)을 통해 상호작용이 가능하다.

이때, client는 service에서 정의한 characteristics (쉽게 생각하면 data를 요청하는 것)에 대해서 정보를 얻거나, 업데이트를 요청할 수 있다.

server는 characteristics가 업데이트가 됐음을 알릴 수 있다.

Ble에서 gatts와 gattc 간의 이런 상호작용을 Read, Write, Notify, Indicate로 나눌 수 있다.

 

Client -> Server

  Read: client가 server에게 characteristics를 요청하는 것 (정보를 얻어온다)

  Write: client가 server에게 characterisctics의 업데이트를 요청하는 것

 

Server -> Client

  Notify: server가 client에게 characterstics의 업데이트를 알리는 것, 단, client쪽의 인지가 필요없이 일방적으로 알린다.

             (Write 요청이 없어도 업데이트를 알릴 때를 의미)

  Indicate: server가 client에게 characteristics의 업데이트를 알리는데, client쪽의 인지를 필요로 한다.

 

이런 행위들에 대해서 server는 service별로 client의 접근 권한을 설정할 수 있다. (아래 두 코드 참조)

  1.

static const uint8_t char_prop_read_write_notify = 
    ESP_GATT_CHAR_PROP_BIT_READ | 
    ESP_GATT_CHAR_PROP_BIT_WRITE | 
    ESP_GATT_CHAR_PROP_BIT_NOTIFY;

 

  2.

/* Full Database Description - Used to add attributes into the database */
static const esp_gatts_attr_db_t gatt_db[HRS_IDX_NB] =
{
    // Service Declaration
    [IDX_SVC]        =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&primary_service_uuid, ESP_GATT_PERM_READ,
      sizeof(uint16_t), sizeof(GATTS_SERVICE_UUID_TEST), (uint8_t *)&GATTS_SERVICE_UUID_TEST}},

    /* Characteristic Declaration */
    [IDX_CHAR_A]     =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&character_declaration_uuid, ESP_GATT_PERM_READ,
      CHAR_DECLARATION_SIZE, CHAR_DECLARATION_SIZE, (uint8_t *)&char_prop_read_write_notify}},

    /* Characteristic Value */
    [IDX_CHAR_VAL_A] =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&GATTS_CHAR_UUID_TEST_A, ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE,
      GATTS_DEMO_CHAR_VAL_LEN_MAX, sizeof(char_value), (uint8_t *)char_value}},

    /* Client Characteristic Configuration Descriptor */
    [IDX_CHAR_CFG_A]  =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&character_client_config_uuid, ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE,
      sizeof(uint16_t), sizeof(heart_measurement_ccc), (uint8_t *)heart_measurement_ccc}},

    /* Characteristic Declaration */
    [IDX_CHAR_B]      =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&character_declaration_uuid, ESP_GATT_PERM_READ,
      CHAR_DECLARATION_SIZE, CHAR_DECLARATION_SIZE, (uint8_t *)&char_prop_read}},

    /* Characteristic Value */
    [IDX_CHAR_VAL_B]  =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&GATTS_CHAR_UUID_TEST_B, ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE,
      GATTS_DEMO_CHAR_VAL_LEN_MAX, sizeof(char_value), (uint8_t *)char_value}},

    /* Characteristic Declaration */
    [IDX_CHAR_C]      =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&character_declaration_uuid, ESP_GATT_PERM_READ,
      CHAR_DECLARATION_SIZE, CHAR_DECLARATION_SIZE, (uint8_t *)&char_prop_write}},

    /* Characteristic Value */
    [IDX_CHAR_VAL_C]  =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&GATTS_CHAR_UUID_TEST_C, ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE,
      GATTS_DEMO_CHAR_VAL_LEN_MAX, sizeof(char_value), (uint8_t *)char_value}},

};

 

 

* 블루투스 키보드에 대해서 Read, Write, Notify, Indicate를 매칭시켜보면 다음과 같다.

- 일반적으로 블루투스 키보드에서 키를 입력해서 연결된 PC에 report를 보내는 것은 notify 이다

(notify, indicate 중 하나를 선택할 수 있겠지만, 일반적으로 notify 방식을 사용한다 - 이유는 아래 추가)

- PC쪽에서 Capslock이나 Numlock 등의 상태 변경이 있을 때,

 PC -> 키보드 쪽으로 상태변경을 요청하고, 이는 Write가 된다.

 

- Claude 대화 첨부

Q.

When I make bluetooth keyboard, keyboard is gatts, right? and when sending key report, is it notify? or indicate? and when capslock event occur, keyboard got write from client (pc)?

A.
You've got a great grasp of the concept! Let's break down the Bluetooth Low Energy (BLE) keyboard scenario:

1. Keyboard as GATTS:
  - Yes, you're correct. The keyboard acts as a GATT Server (GATTS).
  - It provides services and characteristics that a client (like a PC) can interact with.

 

2. Sending Key Reports:
  - For sending key presses to the client (PC), the keyboard typically uses Notifications.
  - Notifications are preferred over Indications for keyboard input because:

    a) They're faster (no need to wait for acknowledgment).

    b) It's okay if an occasional keypress is missed.

        (less critical than losing a keypress due to waiting for an acknowledgment)

 

3. Capslock Event:
  - You're right again! When the Capslock state changes on the PC,

    it's communicated back to the keyboard via a Write operation.
  - The client (PC) writes to a specific characteristic on the keyboard to update its Capslock state.

 

Here's a more detailed breakdown:

 

1. Key Input Service:
  - Characteristic for key reports (Notify)
  - Characteristic for LED states (Write) - for Capslock, Numlock, etc.

 

2. Key Report Flow:
  - Keyboard -> PC: Notification of key report characteristic
  - No need for the PC to acknowledge each keypress

 

3. LED State Flow:
  - PC -> Keyboard: Write to LED state characteristic
  - Keyboard updates its internal state and physical LEDs if present

 

4. Configuration:
  - The PC enables notifications for the key report characteristic during initial setup
  - The keyboard handles write requests to the LED state characteristic

 

This setup allows for efficient, low-latency transmission of keypresses from the keyboard to the PC, while also allowing the PC to update the keyboard about state changes like Capslock.

728x90
반응형

'MCU > Bluetooth' 카테고리의 다른 글

Bluetooth - HID: Little Endian, Big Endian  (1) 2024.05.01
Bluetooth: GAP, GATT # BLE  (0) 2024.04.14
Bluetooth: Protocol vs Profile  (0) 2024.04.14