Bluetooth 初心者が,ESP-IDF の GATT SERVER SPP demo のコードを読み解いてみたので,分かったことを紹介します.
はじめに
少し前に,ESP-IDF の github にGATT SERVER SPP demoが登録されました.これを使うと,Bluetooth Low Energy (BLE) を使って UART 通信を離れた場所に伝送することができます.
機能としてはシンプルなものですが,独自の BLE プロファイルを定義するサンプルとして良さそうだったので,読み解いてみました.
前提知識として,下記の本に目を通しておくのがオススメです.
私の場合,BLE に関してほぼ知識ゼロから始めましたが,あらかじめこの本を読んでおくとで BLE の全体像をつかむことができ,さらに詳しい内容を調べるにあたって助けられました.
モジュール分割
コードを読んでいくと,本質的には 3 つほどのモジュールに分割できそうな事が見えてきましたので,ソースコードを次の 3 つに分割してみることにしました.
- 一般的な BLE サーバー処理
- デバイスの GATT サービス定義や,GATT 関係のイベント処理を担います.
- 文字列バッファ管理
- 細切れになった文字列バッファの管理を担います.
- UART 伝送特有の処理
- 今回のアプリケーションに特化した処理を担います.
分割の過程で自分が理解しやすくなるように一部コードの書き換えも行いました.
結果は github にアップしてあります.
以降では,それぞれのモジュールについてコードの中身を順に追っていきます.説明をスムーズにするため,一部ソースコードと順番を入れ替えています.
一般的な BLE サーバー処理
コード全体はこちらです.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
static const uint8_t SPP_ADV_DATA[23] = { 0x02, // Length 0x01, // AD Type (Flags) 0x06, // Flags (LE General Discoverable Mode, BR/EDR Not Supported) 0x03, // Length 0x03, // AD Type (Complete List of 16-bit Service Class UUIDs) 0xF0, 0xAB, 0x0F, // Length 0x09, // AD Type (Complete Local Name) // E S P _ S P P _ S E R V E R 0x45,0x53,0x50,0x5f,0x53,0x50,0x50,0x5f,0x53,0x45,0x52,0x56,0x45,0x52 }; |
Advertising パケットとして送信するデータを定義しています.元のコードは値のみが呪文のように記載されていたので,コメントで意味を追記しました.Advertising パケットの構造については「Advertisement packet format」で検索すると分かりやすい解説が見つかると思います.
1 2 3 4 5 6 7 8 |
static esp_ble_adv_params_t spp_adv_params = { .adv_int_min = 0x20, .adv_int_max = 0x40, .adv_type = ADV_TYPE_IND, .own_addr_type = BLE_ADDR_TYPE_PUBLIC, .channel_map = ADV_CHNL_ALL, .adv_filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY, }; |
Advertising パケットの送信設定を行います.各項目の意味は追ってません.
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 |
const esp_gatts_attr_db_t SPP_GATT_DB[SPP_IDX_NB] = { // Service Declaration [SPP_IDX_SVC] = { { ESP_GATT_AUTO_RSP }, { ESP_UUID_LEN_16, (uint8_t *)&PRIM_SERVICE_UUID, ESP_GATT_PERM_READ, sizeof(SPP_SERVICE_UUID), sizeof(SPP_SERVICE_UUID), (uint8_t *)&SPP_SERVICE_UUID } }, // Data Receive: characteristic declaration [SPP_IDX_SPP_DATA_RECV_CHAR] = { { ESP_GATT_AUTO_RSP }, { ESP_UUID_LEN_16, (uint8_t *)&CHAR_DECL_UUID, ESP_GATT_PERM_READ, CHAR_DECLARATION_SIZE, CHAR_DECLARATION_SIZE, (uint8_t *)&CHAR_PROP_READ_WRITE } }, // Data Receive: characteristic value [SPP_IDX_SPP_DATA_RECV_VAL] = { { ESP_GATT_AUTO_RSP }, { ESP_UUID_LEN_16, (uint8_t *)&SPP_DATA_RECV_UUID, ESP_GATT_PERM_WRITE, SPP_DATA_MAX_LEN, sizeof(SPP_DATA_RECV_VAL), (uint8_t *)SPP_DATA_RECV_VAL } }, // Data Notify: characteristic declaration [SPP_IDX_SPP_DATA_NOTIFY_CHAR] = { { ESP_GATT_AUTO_RSP }, { ESP_UUID_LEN_16, (uint8_t *)&CHAR_DECL_UUID, ESP_GATT_PERM_READ, CHAR_DECLARATION_SIZE, CHAR_DECLARATION_SIZE, (uint8_t *)&CHAR_PROP_READ_NOTIFY } }, // Data notify: characteristic value [SPP_IDX_SPP_DATA_NOTIFY_VAL] = { { ESP_GATT_AUTO_RSP }, { ESP_UUID_LEN_16, (uint8_t *)&SPP_DATA_NOTIFY_UUID, ESP_GATT_PERM_READ, SPP_DATA_MAX_LEN, sizeof(SPP_DATA_NOTIFY_VAL), (uint8_t *)SPP_DATA_NOTIFY_VAL } }, // Data notify: client characteristic configuration descriptor [SPP_IDX_SPP_DATA_NOTIFY_CFG] = { { ESP_GATT_AUTO_RSP }, { ESP_UUID_LEN_16, (uint8_t *)&CHAR_CLIENT_CONFIG_UUID, ESP_GATT_PERM_READ|ESP_GATT_PERM_WRITE, sizeof(uint16_t), sizeof(SPP_DATA_NOTIFY_CCC), (uint8_t *)SPP_DATA_NOTIFY_CCC } }, (中略) }; |
GATT のサービス定義です.デバイスは Characteristic の形で状態を公開し,クライアントはこれらの状態に対して読み書きを行うことで,アプリケーションとしての機能を実現します.
今回のアプリケーションの場合,以下のような Characteristic が宣言されています.
SPP_IDX_SPP_DATA_RECV_*
- リモートから UART データを受信する Characteristic.
SPP_IDX_SPP_DATA_NOTIFY_*
- リモートに UART データを送信する Characteristic.
SPP_IDX_SPP_DATA_NOTIFY_CFG
によって送信を ON/OFF 設定できるようになっています.
1 2 3 4 5 6 7 |
gatts_profile_inst_t spp_profile_tab[SPP_PROFILE_NUM] = { [SPP_PROFILE_APP_IDX] = { .gatts_cb = gatts_profile_event_handler, .gatts_if = ESP_GATT_IF_NONE, .mtu_size = 23, }, }; |
アプリケーションにおける GATT サーバの状態を管理する構造体です.元のコードは構造体のものは使わずグローバル変数を使っていましたが,書き直す際に全てこの構造体の変数を使う形に直しました.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
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 == ESP_GATTS_REG_EVT) { if (param->reg.status == ESP_GATT_OK) { gatts_profile()->gatts_if = gatts_if; } else { ESP_LOGE(TAG, "Failed to regist application."); return; } } for (int i = 0; i < SPP_PROFILE_NUM; i++) { if ((gatts_if == ESP_GATT_IF_NONE) || (gatts_if == spp_profile_tab[i].gatts_if)) { if (spp_profile_tab[i].gatts_cb != NULL) { spp_profile_tab[i].gatts_cb(event, gatts_if, param); } } } } |
GATT に関するイベントハンドラです.ESP-IDF のライブラリに登録して使います.今のところ,深追いできていません.SPP_PROFILE_NUM
を 2 以上にしたくなったら,ちゃんと理解しとく必要がありそうです.
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 |
void gatts_profile_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param) { esp_ble_gatts_cb_param_t *p_data = (esp_ble_gatts_cb_param_t *) param; uint8_t res = find_gatts_index(p_data->read.handle); switch (event) { case ESP_GATTS_REG_EVT: esp_ble_gap_set_device_name(DEVICE_NAME); esp_ble_gap_config_adv_data_raw((uint8_t *)SPP_ADV_DATA, sizeof(SPP_ADV_DATA)); esp_ble_gatts_create_attr_tab(SPP_GATT_DB, gatts_if, SPP_IDX_NB, SPP_SVC_INST_ID); break; case ESP_GATTS_READ_EVT: if (res == SPP_IDX_SPP_STATUS_VAL) { // TODO: client read the status characteristic } break; case ESP_GATTS_WRITE_EVT: handle_gatts_write_event(res, p_data); break; case ESP_GATTS_EXEC_WRITE_EVT: handle_gatts_exec_write_event(p_data); break; case ESP_GATTS_MTU_EVT: gatts_profile()->mtu_size = p_data->mtu.mtu; break; case ESP_GATTS_CONNECT_EVT: gatts_profile()->connection_id = p_data->connect.conn_id; gatts_profile()->gatts_if = gatts_if; gatts_profile()->is_connected = true; break; case ESP_GATTS_DISCONNECT_EVT: gatts_profile()->is_connected = false; gatts_profile()->is_notify_enabled = false; esp_ble_gap_start_advertising(&spp_adv_params); break; case ESP_GATTS_CREAT_ATTR_TAB_EVT: if (param->add_attr_tab.status != ESP_GATT_OK){ ESP_LOGE(TAG, "Failed to create attribute table."); break; } memcpy(spp_handle_table, param->add_attr_tab.handles, sizeof(spp_handle_table)); esp_ble_gatts_start_service(spp_handle_table[SPP_IDX_SVC]); break; default: break; } } |
先ほどの gatts_event_handler
から spp_profile_tab
経由で呼ばれる関数です.下記のイベントについては,定型処理を書いておけば良さそうです.
- ESP_GATTS_REG_EVT
- ESP_GATTS_MTU_EVT
- ESP_GATTS_CONNECT_EVT
- ESP_GATTS_DISCONNECT_EVT
- ESP_GATTS_CREAT_ATTR_TAB_EVT
これ以外の,ESP_GATTS_READ_EVT
や ESP_GATTS_*WRITE_EVT
にはアプリケーション特有の処理を書くことになります.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { switch (event) { case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT: esp_ble_gap_start_advertising(&spp_adv_params); break; case ESP_GAP_BLE_ADV_START_COMPLETE_EVT: if (param->adv_start_cmpl.status != ESP_BT_STATUS_SUCCESS) { ESP_LOGE(TAG, "Failed to start advertising."); } break; default: break; } } |
Advertising に関するイベントハンドラです.ESP-IDF のライブラリに登録して使います.定型処理を書いておけば良さそうです.
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 |
void handle_gatts_write_event(uint8_t res, esp_ble_gatts_cb_param_t *param) { if (param->write.is_prep == true) { switch (res) { case SPP_IDX_SPP_DATA_RECV_VAL: handle_uart_remote_data_prep(param->write.value, param->write.len); break; default: break; } } else { switch (res) { case SPP_IDX_SPP_COMMAND_VAL: handle_command(param->write.value, param->write.len); break; case SPP_IDX_SPP_DATA_NOTIFY_CFG: if (param->write.len != 2) { break; } if ((param->write.value[0] == 0x01) && (param->write.value[1] == 0x00)){ gatts_profile()->is_notify_enabled = true; } else if ((param->write.value[0] == 0x00) && (param->write.value[1] == 0x00)) { gatts_profile()->is_notify_enabled = false; } break; case SPP_IDX_SPP_DATA_RECV_VAL: handle_uart_remote_data(param->write.value, param->write.len); break; default: break; } } } |
GATT のイベントハンドラの WRITE の処理をする部分を抜き出した関数です.対象が SPP_IDX_SPP_DATA_RECV_VAL
の場合は,リモートからの UART データの受信処理を行っています.データサイズが大きいとパケット分割されて送られてくるので,最初に param->write.is_prep == true
で場合分けしています.ローカル UART に対する書き出し処理内容は別ファイルに定義しています.
今読み返すと,まず最初に switch (res)
がきた方が自然な気がしますが,元のコードの構造がこうなっていたので,とりあえず踏襲しています.
対象が SPP_IDX_SPP_DATA_NOTIFY_CFG
の場合は,送信の ON/OFF 設定を行っています.
1 2 3 4 5 6 |
void handle_gatts_exec_write_event(esp_ble_gatts_cb_param_t *param) { if (param->exec_write.exec_write_flag) { handle_uart_remote_data_exec(); } } |
GATT のイベントハンドラの EXEC WRITE の処理をする部分を抜き出した関数です.分割して送信されてきた書き込み内容を反映する処理を行います.ローカル UART に対する書き出し処理内容は別ファイルに定義しています.
文字列バッファ管理
コード全体はこちらです.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
typedef struct str_buf_node { uint32_t len; uint8_t *str; struct str_buf_node *next; } str_buf_node_t; typedef struct str_buf { uint32_t node_count; uint32_t buff_size; str_buf_node_t *first_node; str_buf_node_t *last_node; } str_buf_t; static str_buf_t str_buf = { .node_count = 0, .buff_size = 0, .first_node = NULL, .last_node = NULL, }; |
文字列バッファをリンクトリストで管理するための構造体です.あとのコード内容は,ここの構造体から推測できると思いますので,説明は割愛します.
元のコードの場合,このバッファ管理に関する複数のグローバル変数があったため,コードの見通しを悪くしているように感じます.追っていけばやっていることはシンプルです.
UART 伝送特有の処理
コード全体はこちらです.
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 49 50 51 52 53 54 55 |
/////////////////////////////////////////////////////////////////////////////// // UART handler: Local to Remote // Read UART data and send it to remote via BLE. void handle_uart_local_data(uint8_t *str, uint32_t len) { uint8_t *buf; uint32_t mtu_size = gatts_profile()->mtu_size; if (len <= (mtu_size - 3)) { esp_ble_gatts_send_indicate(gatts_profile()->gatts_if, gatts_profile()->connection_id, gatts_handle(SPP_IDX_SPP_DATA_NOTIFY_VAL), len, str, false); } else { uint32_t current_num = 0; uint32_t max_data_size = mtu_size - 7; uint32_t total_num = (len - 1) / max_data_size + 1; buf = (uint8_t *)malloc((mtu_size-3)*sizeof(uint8_t)); if (buf == NULL) { ESP_LOGE(TAG, "Failed to malloc at %s.", __func__); return; } buf[0] = '#'; buf[1] = '#'; buf[2] = total_num; while (current_num < total_num) { uint32_t data_size; buf[3] = current_num + 1; if (current_num == (total_num - 1)) { data_size = len % max_data_size; } else { data_size = max_data_size; } memcpy(buf+4, str, data_size); esp_ble_gatts_send_indicate(gatts_profile()->gatts_if, gatts_profile()->connection_id, gatts_handle(SPP_IDX_SPP_DATA_NOTIFY_VAL), data_size + 4, buf, false); vTaskDelay(20 / portTICK_PERIOD_MS); current_num++; str += max_data_size; } free(buf); } } |
ローカルの UART データをリモートに送信する処理を行っています.一つのパケットに収まらない場合,複数のパケットに分割して送信しています.
その際,先頭の 4 バイトに分割に関する情報を埋め込みます.この処理はこのサンプル特有の処理だと思いますので,接続相手のクライアントはこのデータ形式に対応している必要がありそうです.
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 |
void uart_task(void *pvParameters) { static QueueHandle_t uart_queue = NULL; uart_event_t event; uart_driver_install(UART_NUM, 4096, 8192, 10,&uart_queue,0); while (1) { uint8_t *buf; uint32_t len; if (xQueueReceive(uart_queue, (void * )&event, (portTickType)portMAX_DELAY) == pdFALSE) { continue; } if ((event.type != UART_DATA) || (event.size == 0)) { continue; } len = event.size; buf = (uint8_t *)malloc(sizeof(uint8_t)*len); if (buf == NULL) { ESP_LOGE(TAG, "Failed to malloc at %s.", __func__); break; } uart_read(buf, len); do { if (!gatts_profile()->is_connected) { ESP_LOGI(TAG, "BLE is NOT connected."); break; } if (!gatts_profile()->is_notify_enabled) { ESP_LOGI(TAG, "Data Notify is NOT enabled."); break; } handle_uart_local_data(buf, len); } while (0); free(buf); } vTaskDelete(NULL); } |
ローカルの UART データを受け取って,先ほどのデータ送信関数を呼び出すタスクです.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
//////////////////////////////////////////////////////////////////////////////// // UART handler: Remote to Local // Receive UART data via BLE and write data. void handle_uart_remote_data(uint8_t *str, uint32_t len) { uart_write(str, len); } void handle_uart_remote_data_prep(uint8_t *str, uint32_t len) { str_buf_store(str, len); } void handle_uart_remote_data_exec() { str_buf_iter(uart_write); str_buf_clear(); } |
リモートから届いた UART データをローカルにに書き出します.パケットが分割されている場合は,少し前に出てきたリンクトリスト使って,一端溜め込んだ後,一気に書き込むようになっています.
リモートに分割送信する場合は先頭 4 バイトに分割情報を埋め込んでいましたが,受信するデータは特にそういったことは行われていないようです.この非対称性が何からきているのかは理解できていません.
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 |
//////////////////////////////////////////////////////////////////////////////// // Command handler void handle_command(uint8_t *str, uint32_t len) { uint8_t *spp_cmd_buff; spp_cmd_buff = (uint8_t *)malloc(sizeof(uint8_t)*(len)); if(spp_cmd_buff == NULL){ ESP_LOGE(TAG, "Failed to malloc at %s.", __func__); return; } memcpy(spp_cmd_buff, str, len); spp_cmd_buff[len-1] = '\0'; // NOTE: a measures if there is a bug on client xQueueSend(cmd_queue, &spp_cmd_buff, 10/portTICK_PERIOD_MS); } void command_task(void * arg) { uint8_t * cmd_id; cmd_queue = xQueueCreate(10, sizeof(uint32_t)); while (1) { vTaskDelay(50 / portTICK_PERIOD_MS); if (xQueueReceive(cmd_queue, &cmd_id, portMAX_DELAY) == pdFALSE) { continue; } esp_log_buffer_char(TAG,(char *)(cmd_id),strlen((char *)cmd_id)); free(cmd_id); } vTaskDelete(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 |
void app_main() { esp_err_t ret; esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT(); ret = nvs_flash_init(); if (ret == ESP_ERR_NVS_NO_FREE_PAGES) { ESP_ERROR_CHECK(nvs_flash_erase()); ESP_ERROR_CHECK(nvs_flash_init()); } ESP_ERROR_CHECK(esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT)); 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()); esp_ble_gatts_register_callback(gatts_event_handler); esp_ble_gap_register_callback(gap_event_handler); esp_ble_gatts_app_register(ESP_SPP_APP_ID); ESP_LOGI(TAG, "BLE intialization is done."); uart_init(); spp_task_init(); ESP_LOGI(TAG, "Task is started."); return; } |
メイン関数です.BLE のコントローラやホストを初期化して,イベントハンドラを登録しています.
まとめ
BLE は他のネットワークプロトコルでは見慣れない用語体系がバックにあることもあって,いきなりコードを見ると一見さんお断り的な敷居の高さを感じます.ただ,やっていることはシンプルなので,コードを整理しながら巷にある BLE の解説文書を読んでいくけば,割と理解しやすいように感じます.
私の場合,BLE の各種用語になれることが一番大変でした.そこさえ乗り越えれば,あとは見通し良かったです.
参考になれば幸いです.
コメント
[…] 前回,SERVER 側を読み解いたのに続き,今回は CLIENT 側です.通常,CLIENT はパソコンやスマホが担当しますが,今回のものは SERVER と同じく,ESP32 で動作します. […]