Bluetooth 初心者が,ESP-IDF の GATT CLIENT SPP demo のコードを読み解いてみたので,分かったことを紹介します.
はじめに
前回,SERVER 側を読み解いたのに続き,今回は CLIENT 側です.通常,CLIENT はパソコンやスマホが担当しますが,今回のものは SERVER と同じく,ESP32 で動作します.
モジュール分割
前回と同じく,次の 2 つにもモジュール分割を行いました.
- 一般的な BLE クライアント処理
- GAP や GATT 関係のイベント処理を担います.
- UART 伝送特有の処理
- 今回のアプリケーションに特化した処理を担います.
分割の過程で自分が理解しやすくなるように一部コードの書き換えも行いました.結果は github にアップしてあります.
以降では,それぞれのモジュールについてコードの中身を順に追っていきます.説明をスムーズにするため,一部ソースコードと順番を入れ替えています.
一般的な BLE クライアント処理
コード全体はこちらです.
1 2 3 4 5 6 7 8 9 10 11 |
static struct gatts_spp_status spp_status[PROFILE_NUM] = { [PROFILE_APP_ID] = { .gattc_cb = gattc_profile_event_handler, .gattc_if = ESP_GATT_IF_NONE, /* Not get the gatt_if, so initial is ESP_GATT_IF_NONE */ .is_connected = false, .is_scanning = false, .is_registered = false, .mtu_size = 23, .db = NULL, }, }; |
GATT クライアントの状態を管理する構造体です.元のコードは構造体の変数は使わずグローバル変数を使っていましたが,書き直す際に全てこの構造体の変数を使う形に直しました.
オリジナルに対して,is_scanning
と is_registered
を追加しています.これらはサーバ側がリセットした場合に動作を継続するために使用します.詳細は,以降で説明します.
1 2 3 4 |
static esp_bt_uuid_t SPP_SERVICE_UUID = { .len = ESP_UUID_LEN_16, .uuid = {.uuid16 = ESP_GATT_SPP_SERVICE_UUID,}, }; |
接続するサービスの定義.
1 2 3 4 5 6 7 |
static esp_ble_scan_params_t ble_scan_params = { .scan_type = BLE_SCAN_TYPE_ACTIVE, .own_addr_type = BLE_ADDR_TYPE_PUBLIC, .scan_filter_policy = BLE_SCAN_FILTER_ALLOW_ALL, .scan_interval = 0x50, .scan_window = 0x30 }; |
デバイスをスキャンするときの設定を行います.各項目の意味は追ってません.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
static void reset_spp_status(void) { spp_status[PROFILE_APP_ID].is_connected = false; spp_status[PROFILE_APP_ID].connection_id = 0; spp_status[PROFILE_APP_ID].mtu_size = 23; spp_status[PROFILE_APP_ID].reg_cmd = 0; spp_status[PROFILE_APP_ID].service_start_handle = 0; spp_status[PROFILE_APP_ID].service_end_handle = 0; memset(spp_status[PROFILE_APP_ID].remote_bda, 0x00, sizeof(esp_bd_addr_t)); if (spp_status[PROFILE_APP_ID].db) { free(spp_status[PROFILE_APP_ID].db); spp_status[PROFILE_APP_ID].db = NULL; } } |
接続が切れた際の処理です.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { switch (event) { case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: if (param->scan_param_cmpl.status != ESP_BT_STATUS_SUCCESS) { ESP_LOGE(TAG_SPP, "Failed to scan param set."); break; } esp_ble_gap_start_scanning(0xFFFF); gattc_spp_status()->is_scanning = true; break; case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT: if (param->scan_start_cmpl.status != ESP_BT_STATUS_SUCCESS) { ESP_LOGE(TAG_SPP, "Failed to scan start."); break; } break; case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT: if (param->scan_stop_cmpl.status != ESP_BT_STATUS_SUCCESS) { ESP_LOGE(TAG_SPP, "Failed to scan stop."); break; } if (gattc_spp_status()->is_connected == false) { esp_ble_gattc_open(gattc_spp_status()->gattc_if, scan_result.scan_rst.bda, true); } break; case ESP_GAP_BLE_SCAN_RESULT_EVT: handle_scan_result_event(param); break; default: break; } } |
デバイスのスキャンや接続に関するイベントハンドラです.デバイスに接続するまでの処理の流れとしては次のような感じです.
- デバイスが見つかると
ESP_GAP_BLE_SCAN_RESULT_EVT
イベントが発生. - 見つかったデバイスが SPP SERVER の場合,
handle_scan_result_event
の中でesp_ble_gap_stop_scanning
が呼ばれる. ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT
イベントが発生.esp_ble_gattc_open
を呼び,デバイスに接続.- 以降,
gattc_profile_event_handler
の中で処理が進みます.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
static void handle_scan_result_event(esp_ble_gap_cb_param_t *param) { if (param->scan_rst.search_evt == ESP_GAP_SEARCH_INQ_RES_EVT) { uint8_t *adv_name; uint8_t adv_name_len = 0; adv_name = esp_ble_resolve_adv_data(param->scan_rst.ble_adv, ESP_BLE_AD_TYPE_NAME_CMPL, &adv_name_len); if (adv_name != NULL) { if (strncmp((char *)adv_name, spp_device_name, adv_name_len) == 0) { memcpy(&(scan_result), param, sizeof(scan_result)); // NOTE: Add to handle reset of server device. // When server is reset, ESP_GAP_BLE_SCAN_RESULT_EVT event occurs twice. // (I don't know the reason...) if (gattc_spp_status()->is_scanning) { esp_ble_gap_stop_scanning(); gattc_spp_status()->is_scanning = false; } } } } } |
見つかったデバイスが SPP SERVER かチェックして,一致していれば esp_ble_gap_stop_scanning
を呼びます.
オリジナルにはない処理として,gattc_spp_status()->is_scanning
による条件分岐を行っています.詳細は追えていないのですが,デバイスがリセットした場合に ESP_GAP_BLE_SCAN_RESULT_EVT
イベントが 2 回発生するようなので,それへの対策です.ちなみに,これを行わないと,「BT: bta_dm_ble_scan stop scan failed」というエラーが発生します.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
void gattc_profile_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) { switch (event) { case ESP_GATTC_REG_EVT: esp_ble_gap_set_scan_params(&ble_scan_params); break; case ESP_GATTC_CONNECT_EVT: gattc_spp_status()->gattc_if = gattc_if; gattc_spp_status()->is_connected = true; gattc_spp_status()->connection_id = param->connect.conn_id; memcpy(gattc_spp_status()->remote_bda, param->connect.remote_bda, sizeof(esp_bd_addr_t)); esp_ble_gattc_search_service(gattc_spp_status()->gattc_if, gattc_spp_status()->connection_id, &SPP_SERVICE_UUID); break; case ESP_GATTC_DISCONNECT_EVT: reset_spp_status(); gattc_spp_status()->is_scanning = true; esp_ble_gap_start_scanning(0xFFFF); break; case ESP_GATTC_SEARCH_RES_EVT: gattc_spp_status()->service_start_handle = param->search_res.start_handle; gattc_spp_status()->service_end_handle = param->search_res.end_handle; break; case ESP_GATTC_SEARCH_CMPL_EVT: esp_ble_gattc_send_mtu_req(gattc_if, gattc_spp_status()->connection_id); break; case ESP_GATTC_REG_FOR_NOTIFY_EVT: if (param->reg_for_notify.status != ESP_GATT_OK) { ESP_LOGE(TAG_SPP, "Failed to regist for norify."); break; } handle_regist_for_notify_event(param); break; case ESP_GATTC_NOTIFY_EVT: notify_event_handler(param); break; case ESP_GATTC_READ_CHAR_EVT: break; case ESP_GATTC_WRITE_CHAR_EVT: if (param->write.status != ESP_GATT_OK) { ESP_LOGE(TAG_SPP, "Failed to write characteristic."); break; } break; case ESP_GATTC_PREP_WRITE_EVT: break; case ESP_GATTC_EXEC_EVT: break; case ESP_GATTC_WRITE_DESCR_EVT: if (param->write.status != ESP_GATT_OK) { ESP_LOGE(TAG_SPP, "Failed to write descriptor."); break; } handle_write_descriptor_event(param); break; case ESP_GATTC_CFG_MTU_EVT: if(param->cfg_mtu.status != ESP_OK){ ESP_LOGE(TAG_SPP, "Failed to configure mtu."); break; } handle_configure_mtu_event(param); break; case ESP_GATTC_SRVC_CHG_EVT: break; default: break; } } |
デバイスが見つかった後の,GATT の読み書き処理に関するイベントハンドラです.ESP-IDF のライブラリに登録して使います.
イベントの発生順序は下記のような感じです.
- 接続完了後,
ESP_GATTC_CONNECT_EVT
イベントが発生.esp_ble_gattc_search_service
を呼んで,デバイスが提供するサービスを読み出し. ESP_GATTC_SEARCH_RES_EVT
イベントに続いて,ESP_GATTC_SEARCH_CMPL_EVT
イベントが発生したら,esp_ble_gattc_send_mtu_req
を呼んで,デバイスとの通信の MTU を取得.ESP_GATTC_CFG_MTU_EVT
イベントが発生したら,取得済みのサービス内容をgattc_spp_status()->db
にコピー.続いて,サーバーからクライアントへのデータ送信を有効化する書き込みを実施.- データ送信の有効化の過程で
ESP_GATTC_REG_FOR_NOTIFY_EVT
イベントとESP_GATTC_WRITE_DESCR_EVT
イベントが発生.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
static void handle_configure_mtu_event(esp_ble_gattc_cb_param_t *param) { uint16_t count = SPP_IDX_NB; gattc_spp_status()->mtu_size = param->cfg_mtu.mtu; gattc_spp_status()->db = (esp_gattc_db_elem_t *)malloc(count*sizeof(esp_gattc_db_elem_t)); if (gattc_spp_status()->db == NULL) { ESP_LOGE(TAG_SPP, "Failed to malloc at %s.", __func__); return; } if (esp_ble_gattc_get_db(gattc_spp_status()->gattc_if, gattc_spp_status()->connection_id, gattc_spp_status()->service_start_handle, gattc_spp_status()->service_end_handle, gattc_spp_status()->db, &count) != ESP_GATT_OK) { ESP_LOGE(TAG_SPP, "Failed to get db."); return; } if (count != SPP_IDX_NB) { return; } gattc_spp_status()->reg_cmd = SPP_IDX_SPP_DATA_NTY_VAL; // NOTE: Add to handle reset of server device. if (gattc_spp_status()->is_registered) { esp_ble_gattc_unregister_for_notify(gattc_spp_status()->gattc_if, gattc_spp_status()->remote_bda, gattc_spp_status()->db[SPP_IDX_SPP_DATA_NTY_VAL].attribute_handle); esp_ble_gattc_unregister_for_notify(gattc_spp_status()->gattc_if, gattc_spp_status()->remote_bda, gattc_spp_status()->db[SPP_IDX_SPP_STATUS_VAL].attribute_handle); } xQueueSend(cmd_regist_queue, &(gattc_spp_status()->reg_cmd), 10/portTICK_PERIOD_MS); } |
ESP_GATTC_CFG_MTU_EVT
イベントのハンドラです.まず,サービス内容のコピーを行っています.内容からするとこの処理は ESP_GATTC_SEARCH_CMPL_EVT
イベントのハンドラの中の方が自然な気がしますが,オリジナルコードでもここで行われていました.
その後,サーバーからクライアントへのデータ送信を有効化する書き込みを行っていきますが,ここが少しややこしいです.
具体的には,NOTIFY に対するハンドラの登録とサーバーに対する NOFITY の有効化をコールバックを挟みながら順に行っています.しかも,NOTIFY イベントに対するハンドラの登録は,別タスクから実施しています.流れを書くと下記のようになっています.
- NOTIFY イベントハンドラを登録する Characteristic のインデックスを
cmd_regist_queue
に追加. spp_client_regist_task
タスクが NOFITY イベントハンドラを追加.- ハンドラが追加されると
ESP_GATTC_REG_FOR_NOTIFY_EVT
イベントが発生. ESP_GATTC_REG_FOR_NOTIFY_EVT
イベントのハンドラの中で esp_ble_gattc_write_char_descr を呼び,NOTIFY を有効化.- 書き込みが完了すると
ESP_GATTC_WRITE_DESCR_EVT
イベントが発生. ESP_GATTC_WRITE_DESCR_EVT
イベントハンドラの中で,別の Characteristic のインデックスを
cmd_regist_queue
に追加.- 以下同様.
オリジナルにはない処理として,gattc_spp_status()->is_registered
による条件分岐を行っています.デバイスがリセットした場合に,NOTIFY イベントハンドラを二重に登録するのを防止しています.コードを整理して,イベントハンドラの登録処理と NOTIFY を有効化処理を分離すれば,これはなくせると思います.(オリジナルコードでは両者が結びついているので,デバイスリセット時にイベントハンドラを登録しなくすると NOTIFY の有効化も行われなくなってしまいます)
1 2 3 4 5 6 7 8 9 10 11 12 |
static void handle_regist_for_notify_event(esp_ble_gattc_cb_param_t *param) { uint16_t notify_enable = 1; esp_ble_gattc_write_char_descr(gattc_spp_status()->gattc_if, gattc_spp_status()->connection_id, gattc_spp_status()->db[gattc_spp_status()->reg_cmd+1].attribute_handle, sizeof(notify_enable), (uint8_t *)¬ify_enable, ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE); } |
NOTIFY の有効化を行います.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
static void handle_write_descriptor_event(esp_ble_gattc_cb_param_t *param) { switch (gattc_spp_status()->reg_cmd) { case SPP_IDX_SPP_DATA_NTY_VAL: gattc_spp_status()->reg_cmd = SPP_IDX_SPP_STATUS_VAL; xQueueSend(cmd_regist_queue, &(gattc_spp_status()->reg_cmd), 10/portTICK_PERIOD_MS); break; case SPP_IDX_SPP_STATUS_VAL: gattc_spp_status()->is_registered = true; break; default: break; }; } |
SPP_IDX_SPP_DATA_NTY_VAL
の NOTIFY の有効化に続いて,SPP_IDX_SPP_STATUS_VAL
の NOTIFY の有効化を行います.handle_configure_mtu_event
の説明の「6.」に相当します.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) { if (event == ESP_GATTC_REG_EVT) { if (param->reg.status == ESP_GATT_OK) { spp_status[param->reg.app_id].gattc_if = gattc_if; } else { ESP_LOGE(TAG_SPP, "Failed to regist application."); return; } } for (int i = 0; i < PROFILE_NUM; i++) { if ((gattc_if == ESP_GATT_IF_NONE) || (gattc_if == spp_status[i].gattc_if)) { if (spp_status[i].gattc_cb != NULL) { spp_status[i].gattc_cb(event, gattc_if, param); } } } } |
GATT に関するイベントハンドラです.ESP-IDF のライブラリに登録して使います.今のところ,深追いできていません.SPP_PROFILE_NUM を 2 以上にしたくなったら,ちゃんと理解しとく必要がありそうです.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
void spp_client_regist_task(void* arg) { uint16_t reg_cmd; cmd_regist_queue = xQueueCreate(10, sizeof(uint32_t)); while (1) { vTaskDelay(100 / portTICK_PERIOD_MS); if (xQueueReceive(cmd_regist_queue, ®_cmd, portMAX_DELAY) == pdFALSE) { continue; } esp_ble_gattc_register_for_notify(gattc_spp_status()->gattc_if, gattc_spp_status()->remote_bda, gattc_spp_status()->db[reg_cmd].attribute_handle); } } |
キューの内容に基づいて NOTIFY イベントハンドラの登録を行います.
UART 伝送特有の処理
コード全体はこちらです.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
//////////////////////////////////////////////////////////////////////////////// // UART handler: Remote to Local // Receive UART data via BLE and write data. void notify_event_handler(esp_ble_gattc_cb_param_t * p_data) { uint8_t handle = p_data->notify.handle; if (handle == gattc_spp_status()->db[SPP_IDX_SPP_DATA_NTY_VAL].attribute_handle) { uart_write(p_data->notify.value, p_data->notify.value_len); } else if (handle == gattc_spp_status()->db[SPP_IDX_SPP_STATUS_VAL].attribute_handle) { // TODO: server notify status characteristic } else { ESP_LOGE(TAG_SPP, "Notify event occured with unkown handle"); } } |
リモートから届いた UART データをローカルにに書き出します.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
//////////////////////////////////////////////////////////////////////////////// // UART handler: Local to Remote // Read UART data and send it to remote via BLE. void uart_task(void *arg) { uart_event_t event; uart_driver_install(UART_NUM_0, 4096, 8192, 10, &spp_uart_queue, 0); while (1) { uint8_t *buf; uint32_t len; if (xQueueReceive(spp_uart_queue, (void * )&event, (portTickType)portMAX_DELAY) == pdFALSE) { continue; } if ((event.type != UART_DATA) || (event.size == 0)) { continue; } if ((gattc_spp_status()->is_connected != true) || (gattc_spp_status()->db == NULL)) { continue; } if (!(gattc_spp_status()->db[SPP_IDX_SPP_DATA_RECV_VAL].properties & (ESP_GATT_CHAR_PROP_BIT_WRITE_NR | ESP_GATT_CHAR_PROP_BIT_WRITE))) { continue; } len = event.size; buf = (uint8_t *)malloc(sizeof(uint8_t)*len); if (buf == NULL) { ESP_LOGE(TAG_SPP, "Failed to malloc at %s.", __func__); break; } memset(buf, 0x00, len); uart_read(buf, len); esp_ble_gattc_write_char(gattc_spp_status()->gattc_if, gattc_spp_status()->connection_id, gattc_spp_status()->db[SPP_IDX_SPP_DATA_RECV_VAL].attribute_handle, len, buf, ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE); free(buf); } vTaskDelete(NULL); } |
ローカルの UART データを受け取って,リモートに送信するタスクです.オリジナルコードに対して esp_ble_gattc_write_char
を呼ぶまでのチェックを少し強化してあります.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
void app_main() { 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(); nvs_flash_init(); ESP_ERROR_CHECK(esp_bt_controller_init(&bt_cfg)); ESP_ERROR_CHECK(esp_bt_controller_enable(ESP_BT_MODE_BLE)); ESP_ERROR_CHECK(esp_bluedroid_init()); ESP_ERROR_CHECK(esp_bluedroid_enable()); cmd_regist_queue = xQueueCreate(10, sizeof(uint32_t)); ESP_ERROR_CHECK(esp_ble_gap_register_callback(gap_event_handler)); ESP_ERROR_CHECK(esp_ble_gattc_register_callback(gattc_event_handler)); ESP_ERROR_CHECK(esp_ble_gattc_app_register(PROFILE_APP_ID)); ESP_ERROR_CHECK(esp_ble_gatt_set_local_mtu(200)); uart_init(); spp_task_init(); ESP_LOGI(TAG_SPP, "Task is started."); } |
メイン関数です.BLE のコントローラやホストを初期化して,イベントハンドラを登録しています.
まとめ
サーバ側よりもコードはコンパクトで,ESP32 でクライアントを実装するのもそれほど難しくはなさそうです.処理の流れはシンプルなので,個々の処理がイベントハンドラやタスクをまたいで連なっていくのに慣れれば,あとの見通しは良いと思います.
参考になれば幸いです.
コメント