Arduino-ESP32で、Bluetooth Classic で SPP の Master (initiator) を

:::note warn
2022/8/23 現在、公式の BluetoothSerial サンプルで Master もできるようになってるみたいです。

https://github.com/espressif/arduino-esp32/blob/master/libraries/BluetoothSerial/

（2019年頃には対応してたみたい？）

以下は昔話ということで！
:::



2018/12/8 時点の情報です。 [秋月で買ったＥＳＰ３２－ＤｅｖＫｉｔＣ　ＥＳＰ－ＷＲＯＯＭ－３２開発ボード](http://akizukidenshi.com/catalog/g/gM-11819/) 使ってます。

# 概要
タイトルのとおりの条件の先駆者の記事どんぴしゃのが見つからない。(Slave というか、Acceptor なら ArduinoIDE についてる BluetoothSerial のサンプルで大丈夫なのだが）
↓
どうにか espressif のサンプルコード(ESP-IDF用）を見つけ、Initiator Sample を ArduinoIDE で走らせようとする。
[Acceptor Sample](https://github.com/espressif/esp-idf/blob/master/examples/bluetooth/bt_spp_acceptor/main/example_spp_acceptor_demo.c) と [Initiator Sample](https://github.com/espressif/esp-idf/blob/master/examples/bluetooth/bt_spp_initiator/main/example_spp_initiator_demo.c)
↓
そもそも、Arduino-ESP32 の 1.0.0 のSDK古すぎて定数の定義すら足りてないので開発バージョンの1.0.1-rc1 に変える
（そして、1.0.0のフォルダが完全に消えてなくてハマる）
↓
微妙にうまくいかないので、ArduinoIDE側のbluetooth 初期化をBluetoothSerial のソース見ながら Arduino-ESP32 流に直してく
↓
できた！

# espressif のサンプルコードを Arduino-ESP32 で使うには
サンプルにある app_main を ArduinoIDE では setup にして、loop を空っぽ(もしくは任意の処理)にすればいけるんちゃう？どうせ裏には sdk おるんやし……というもくろみ。躓いたら都度直していこうとお気楽に着手。

（後で判明したことだが、AppData\Local\Arduino15\packages\esp32\hardware\esp32\1.0.1-rc1\cores\esp32\main.cpp みれば
app_main でinitArduino() して、xTaskCreatePinnedToCoreで setup と loop の task を(ARDUINO_RUNNING_COREで)走らせているわけで、initArduino (これは AppData\Local\Arduino15\packages\esp32\hardware\esp32\1.0.1-rc1\cores\esp32\esp32-hal-misc.c に書いてある) でやってる処理を重複してやらないようにする必要がある。そして、initArduino では esp_bt_controller_mem_release しているから、setup でもこれやってたらダメだった）

# Arduino-ESP32 1.0.0ではダメ、1.0.1-rc1ならOK (2018/12/8時点)
たとえば、サンプルに出てくる値で ESP_BT_GAP_PIN_REQ_EVT なんかは 1.0.1-rc1 では sdk\include\bt\esp_gap_bt_api.h ってファイルに定義されているのだが、これが、1.0.0 の sdk\include\bluedroid\api\esp_gap_bt_api.h （や他のファイル）には無い。espressif のサンプルはそういう新しい定数いっぱい使っているので、1.0.0 でコンパイルしたら当然エラーになった。そういうエラーがわらわら出た。

なので、ArduinoIDE の設定で追加ボードマネージャのURLは https://dl.espressif.com/dl/package_esp32_dev_index.json にして、 1.0.1-rc1 を入れてみた。
入れる時に、 1.0.0 を削除してくれるはずなんだけど、エディタでソース開いてたせいか1.0.0のフォルダが完全に消えなかった。
具体的には C:\Users\username\AppData\Local\Arduino15\packages\esp32\hardware\esp32\1.0.0 の中に空フォルダが残っていた。そしたらコンパイル時に

**ボードesp32 (プラットフォームesp32、パッケージesp32)は不明です**
**ボードESP32 Dev Moduleに対するコンパイル時にエラーが発生しました。**

というエラーが出る。エラーメッセージでぐぐって調べて 1.0.0 のフォルダ消したら解決。 [参考サイト](https://dotstud.io/docs/nefrybt-error-handling-1/)

# bluetooth の初期化がー
さっき書いた通り、espressif のサンプルの中でやってる

    ESP_ERROR_CHECK(esp_bt_controller_mem_release(ESP_BT_MODE_BLE));

なんかは、initArduino の中の

    if(!btInUse()){
        esp_bt_controller_mem_release(ESP_BT_MODE_BTDM);
    }

とかと重複するみたいで、bluetooth の初期化で躓くので、ここで Arduino-ESP32 の BluetoothSerial のライブラリのソースを見て初期化を真似してみた。どこ変えたかあんまり覚えてないけどとにかく出来た。

# コードなど
（元のサンプルにあったコードで不要なものが残っているかもです）

下記は、ELM327 使った OBD2 bluetooth アダプタ(PIN="1234"な奴)の bluetooth device address を定数 peer_bd_addr で決めておいて、一致する Bluetooth device が見つかったら SPP 接続に行き、繋がったら ""ATZ\rATE0\rATL0\r" をまず送り、その後は loop の中にあるように3種類を1秒毎に送る。
あっちのデバイスからの受信内容は SPP のコールバックの ESP_SPP_DATA_IND_EVT のところで Serial に print している。
その他状況報告も Serial.print する。


```c:Arduino-ESP32-initiator.ino
#define CONFIG_CLASSIC_BT_ENABLED

#include <stdint.h>
#include <string.h>
#include <stdbool.h>
#include <stdio.h>
#include "nvs.h"
#include "nvs_flash.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "esp_bt.h"
#include "esp_bt_main.h"
#include "esp_gap_bt_api.h"
#include "esp_bt_device.h"
#include "esp_spp_api.h"

#include "time.h"
#include "sys/time.h"

#define EXCAMPLE_DEVICE_NAME "ESP_SPP_INITIATOR"

static const esp_spp_mode_t esp_spp_mode = ESP_SPP_MODE_CB;
static const esp_spp_sec_t sec_mask = ESP_SPP_SEC_AUTHENTICATE;
static const esp_spp_role_t role_master = ESP_SPP_ROLE_MASTER;

static uint8_t peer_bdname_len;
static char peer_bdname[ESP_BT_GAP_MAX_BDNAME_LEN + 1];
static esp_bd_addr_t peer_bd_addr = {0x00,0x11,0x22,0x33,0x44,0x55}; // 適宜変更してください
static const esp_bt_inq_mode_t inq_mode = ESP_BT_INQ_MODE_GENERAL_INQUIRY;
static const uint8_t inq_len = 30;
static const uint8_t inq_num_rsps = 0;


uint16_t myhandle;


static bool get_name_from_eir(uint8_t *eir, char *bdname, uint8_t *bdname_len)
{
    uint8_t *rmt_bdname = NULL;
    uint8_t rmt_bdname_len = 0;

    if (!eir) {
        return false;
    }

    rmt_bdname = esp_bt_gap_resolve_eir_data(eir, ESP_BT_EIR_TYPE_CMPL_LOCAL_NAME, &rmt_bdname_len);
    if (!rmt_bdname) {
        rmt_bdname = esp_bt_gap_resolve_eir_data(eir, ESP_BT_EIR_TYPE_SHORT_LOCAL_NAME, &rmt_bdname_len);
    }

    if (rmt_bdname) {
        if (rmt_bdname_len > ESP_BT_GAP_MAX_BDNAME_LEN) {
            rmt_bdname_len = ESP_BT_GAP_MAX_BDNAME_LEN;
        }

        if (bdname) {
            memcpy(bdname, rmt_bdname, rmt_bdname_len);
            bdname[rmt_bdname_len] = '\0';
        }
        if (bdname_len) {
            *bdname_len = rmt_bdname_len;
        }
        return true;
    }

    return false;
}

static void esp_spp_cb(esp_spp_cb_event_t event, esp_spp_cb_param_t *param)
{
    switch (event) {
    case ESP_SPP_INIT_EVT:
        Serial.println("ESP_SPP_INIT_EVT: Starting GAP discovery");
        esp_bt_dev_set_device_name(EXCAMPLE_DEVICE_NAME);
        esp_bt_gap_set_scan_mode(ESP_BT_SCAN_MODE_CONNECTABLE_DISCOVERABLE);
        esp_bt_gap_start_discovery(inq_mode, inq_len, inq_num_rsps);
        break;
    case ESP_SPP_DISCOVERY_COMP_EVT:
        Serial.println("ESP_SPP_DISCOVERY_COMP_EVT: Connecting");
        if (param->disc_comp.status == ESP_SPP_SUCCESS) {
            esp_spp_connect(sec_mask, role_master, param->disc_comp.scn[0], peer_bd_addr);
        }
        break;
    case ESP_SPP_OPEN_EVT:
        Serial.println("ESP_SPP_OPEN_EVT");
        esp_spp_write(param->srv_open.handle, 14, (uint8_t*)"ATZ\rATE0\rATL0\r");
        break;
    case ESP_SPP_CLOSE_EVT:
        Serial.println("ESP_SPP_CLOSE_EVT");
        break;
    case ESP_SPP_START_EVT:
        Serial.println("ESP_SPP_START_EVT");
        break;
    case ESP_SPP_CL_INIT_EVT:
        Serial.println("ESP_SPP_CL_INIT_EVT");
        break;
    case ESP_SPP_DATA_IND_EVT:
//        Serial.printf( "ESP_SPP_DATA_IND_EVT len=%d handle=%d\n", param->data_ind.len, param->data_ind.handle);
        {
          for (int i = 0; i < param->data_ind.len; i++){
              if ( (char)(param->data_ind.data[i]) == '\r' || (char)(param->data_ind.data[i]) == '\n') {
                 Serial.println("");
              }
              else {
                Serial.print( (char)(param->data_ind.data[i]) );
             }
          }
        }
        break;
    case ESP_SPP_CONG_EVT:
        Serial.print("ESP_SPP_CONG_EVT cong= ");
        Serial.println( param->cong.cong );
        break;
    case ESP_SPP_WRITE_EVT:
        myhandle = param->write.handle;
//        Serial.printf( "ESP_SPP_WRITE_EVT len=%d cong=%d\n", param->write.len , param->write.cong);
        break;
    case ESP_SPP_SRV_OPEN_EVT:
        Serial.println("ESP_SPP_SRV_OPEN_EVT");
        break;
    default:
        Serial.println("ESP_SPP_EVENT OTHER");
        break;
    }
}

static void esp_bt_gap_cb(esp_bt_gap_cb_event_t event, esp_bt_gap_cb_param_t *param)
{
    switch(event){
    case ESP_BT_GAP_DISC_RES_EVT:
        Serial.print( "ESP_BT_GAP_DISC_RES_EVT: num = ");
        Serial.println( param->disc_res.num_prop );
        for (int i = 0; i < param->disc_res.num_prop; i++){
            if ( param->disc_res.prop[i].type == ESP_BT_GAP_DEV_PROP_COD && memcmp( peer_bd_addr, param->disc_res.bda, ESP_BD_ADDR_LEN ) == 0 ) {
              Serial.println( "device found." );                
              esp_spp_start_discovery(peer_bd_addr);
              esp_bt_gap_cancel_discovery();
            }
            if (param->disc_res.prop[i].type == ESP_BT_GAP_DEV_PROP_COD ) {//class of device
              if ( get_name_from_eir((uint8_t*)(param->disc_res.prop[i]).val, peer_bdname, &peer_bdname_len) ) {
                peer_bdname[peer_bdname_len] = 0;
                Serial.print( "PEER CLASS OF DEVICE: " );             
                Serial.println( peer_bdname );             
              }
            }
            else if (param->disc_res.prop[i].type == ESP_BT_GAP_DEV_PROP_RSSI ) {
              Serial.print( "param->disc_res.prop[i].type: ");
              Serial.println( param->disc_res.prop[i].type );
            }
            else if (param->disc_res.prop[i].type == ESP_BT_GAP_DEV_PROP_BDNAME ) {
              Serial.print( "param->disc_res.prop[i].type: ");
              Serial.println( param->disc_res.prop[i].type );
            }
            else {
              Serial.print( "param->disc_res.prop[i].type: ");
              Serial.println( param->disc_res.prop[i].type );
            }
        }
        break;
    case ESP_BT_GAP_DISC_STATE_CHANGED_EVT:
        break;
    case ESP_BT_GAP_RMT_SRVCS_EVT:
        break;
    case ESP_BT_GAP_RMT_SRVC_REC_EVT:
        break;
    case ESP_BT_GAP_AUTH_CMPL_EVT:{
        break;
    }
    case ESP_BT_GAP_PIN_REQ_EVT:{
        if (param->pin_req.min_16_digit) {
            esp_bt_pin_code_t pin_code = {0};
            esp_bt_gap_pin_reply(param->pin_req.bda, true, 16, pin_code);
        } else {
            esp_bt_pin_code_t pin_code;
            pin_code[0] = '1';
            pin_code[1] = '2';
            pin_code[2] = '3';
            pin_code[3] = '4';
            esp_bt_gap_pin_reply(param->pin_req.bda, true, 4, pin_code);
        }
        break;
    }
    case ESP_BT_GAP_CFM_REQ_EVT:
        esp_bt_gap_ssp_confirm_reply(param->cfm_req.bda, true);
        break;
    case ESP_BT_GAP_KEY_NOTIF_EVT:
        break;
    case ESP_BT_GAP_KEY_REQ_EVT:
        break;
    default:
        break;
    }
}

void setup()
{
    Serial.begin(115200);  
    Serial.print("Start\n");

    esp_err_t ret = nvs_flash_init();
    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_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
    if (!btStart()) {
        Serial.printf( "btStart() failed\n");
        return;
    }

    esp_bluedroid_status_t bt_state = esp_bluedroid_get_status();
    if (bt_state == ESP_BLUEDROID_STATUS_UNINITIALIZED){
        if (esp_bluedroid_init()) {
            Serial.println("initialize bluedroid failed");
            return;
        }
    }
    
    if (bt_state != ESP_BLUEDROID_STATUS_ENABLED){
        if (esp_bluedroid_enable()) {
            Serial.println("enable bluedroid failed");
            return;
        }
    }

    if ((ret = esp_bt_gap_register_callback(esp_bt_gap_cb)) != ESP_OK) {
        return;
    }

    if ((ret = esp_spp_register_callback(esp_spp_cb)) != ESP_OK) {
        return;
    }

    if ((ret = esp_spp_init(esp_spp_mode)) != ESP_OK) {
        return;
    }

    /* Set default parameters for Secure Simple Pairing */
    esp_bt_sp_param_t param_type = ESP_BT_SP_IOCAP_MODE;
    esp_bt_io_cap_t iocap = ESP_BT_IO_CAP_IO;
    esp_bt_gap_set_security_param(param_type, &iocap, sizeof(uint8_t));

    /*
     * Set default parameters for Legacy Pairing
     * Use variable pin, input pin code when pairing
     */
    esp_bt_pin_type_t pin_type = ESP_BT_PIN_TYPE_VARIABLE;
    esp_bt_pin_code_t pin_code;
    esp_bt_gap_set_pin(pin_type, 0, pin_code);

    Serial.println("bluetooth initialize finished");
   
}


void loop()
{
  delay(1000);
  esp_spp_write(myhandle , 6, (uint8_t*)"01 0D\r");
  delay(1000);
  esp_spp_write(myhandle , 6, (uint8_t*)"01 0C\r" );
  delay(1000);
  esp_spp_write(myhandle , 6, (uint8_t*)"01 46\r" );
}

```


実行結果は

---------------------------------------
Start
bluetooth initialize finished
ESP_SPP_INIT_EVT: Starting GAP discovery
ESP_BT_GAP_DISC_RES_EVT: num = 2
device found.
param->disc_res.prop[i].type: 3
ESP_SPP_DISCOVERY_COMP_EVT: Connecting
ESP_SPP_CL_INIT_EVT
ESP_SPP_OPEN_EVT
01 46
41 46 41 

\>01 0D
41 0D 00 
以下同様

---------------------------------------


setup の途中で esp_spp_init しているので、ESP_SPP_INIT_EVT が最初に来るので、GAP discovery を始める。
ESP_BT_GAP_DISC_RES_EVT が来て、device が見つかったら esp_spp_start_discovery して、esp_bt_gap_cancel_discovery する。つまり、GAP discovery をとめて、BDADDRESSを指定したSPP discovery を始める。
ESP_SPP_DISCOVERY_COMP_EVT が来るので、esp_spp_connect する。
そしたら、ESP_SPP_CL_INIT_EVT, ESP_SPP_OPEN_EVT が来るので、OPENのところで最初の AT コマンド送信。
あとは、loop の中で ELM327 宛コマンド3種類を順番に。

ほんとうは ESP_SPP_CONG_EVT で CONGESTION すなわち通信混雑の状態を監視して送信したらダメなときは控えるべきなのだけど、この程度だと混雑しないので端折っている。

# おわりに
qiita 使うのはじめてなんだけど、とりあえず自分向け備忘録。コードの書き方（自分が書き直したところ）色々いまいちですんません。長いのに折りたたみもうまくいかん。