ESP32で読んだJoystick値をBluetoothで飛ばし、別のESP32でサーボモーターを動かしてみました

#はじめに
ネットの先人たちの情報を、あれもこれも使わせて頂き、まだまだ初心者ですが、色々と遊ばせて頂いています。既出の情報ばかりかとは思いますが、自分のメモとして投稿させて頂きます。ご容赦くださいませ。

#構成
ESP32同士をBluetoothで接続し、片方のESP32に接続されたMCP3208 (ADコンバーター）から読んだXYジョイスティックの値を、もう一方のESP32に飛ばして、XY軸でサーボモーターを動かしてみました。Bluetoothの接続は、MACアドレスを直接指定する形で実現しましたので、このままでは、汎用性がありませんが、実験としては上手くいきましたのでメモとして残しておきます。ジョイスティックを動かすと、スマイリー君がうなづいたり、顔を回転させるだけですが・・・・
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266665/feb99237-5b24-f72e-2303-b3acc2f9435b.png)![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266665/b0b6a5ec-bd1e-ba3e-bbfc-c459ce7c37d5.png)

#マスタ側
##アナログジョイスティックの読み取り
[別の投稿](https://qiita.com/yoroyasu/items/3d15563947589801d34a)にあげたやり方と同じですが、12bit ADCのMCP3208（安価で12bit x 8CHあって便利。差動が擬似なのが残念）と、ESP32を 4MHzのSPI(HSPI）で接続して、約20msec毎に値を読取ります。20msecにしたのは、サーボモータのPWM周期が20msecなので、それよりも高速で送っても意味が無いと思ったからです。そもそも、Bluetoothがどのくらいの周期でパケットを送れるのかよく知らない（調べればいいのですが・・・）

SPIライブラリは一般的な SPI.hを使い、一般的なピン配列で Mode 0 をつかって 4MMzのクロックで通信しました。
MCP3208が 5bitコマンド+1bitダミー+1bit NULL + 12bitデータという信号フォーマットなのですが、SPI.Hの8bit単位のSPI通信を使うために、先頭に5bitダミーのゼロを送り、5bit+ 19bitで 24ビット(3byte）にして通信しました。

```c:
uint16_t adc_read(uint8_t channel) {
  digitalWrite(SPI1_SS, LOW);
  SPI1.transfer(0x06 | channel>>2);//bit7-3:dummy, bit2:start, bit1:single,bit0:D2
  uint8_t dh = SPI1.transfer((channel & 0x03) << 6);//D1,D0,X,Null,B11-B8
  uint8_t dl = SPI1.transfer(0x00);//B7-B0
  digitalWrite(SPI1_SS, HIGH);
  return ((dh & 0xF) << 8 | dl); //get 12bits data
}
```

ジョイスティックで読んだ X, Yの二つの数値 (0～4095）を、コンマ区切りの文字列でスレーブ側に送ります。また後述しますが、スレーブ側でのバッファ読取りで、SerialBT.readStringUntil('\0')を使い、文字列の最後に'\0' がある事を前提としているため、XとYの値を、以下のように連結して送るようにしました。

```c:
void loop() {
  uint16_t xval = adc_read(0); //Channel-0
  uint16_t yval = adc_read(1); //Channel-1
  String str = String(xval) + ',' +String(yval) + '\0';
  Serial.println(str);
  SerialBT.print(str);
  delay(20);
}
```



#Bluetooth接続
Bluetoothについて勉強不足なので、ネットの先人たちの情報を大変参考にさせて頂きました。基本は、ESP32をボードマネジャーで Arduino IDEにインストールした時に入ってきたサンプルコードをほぼ使った形です。

##スレーブ側のMACアドレスを調べる　
（すみません。ネットの先人のサイトから頂きましたが、リンクが見つかりません）
このコードをスレーブ側のESP32に書き込み、実行すると、MACアドレスを取得する事ができるので、メモしておきます。(6バイトのデータです）

```c:
void setup(void) {
  Serial.begin(115200);
  Serial.println("-----------------");
  uint8_t macBT[6];
  esp_read_mac(macBT, ESP_MAC_BT);
  Serial.printf("%02X:%02X:%02X:%02X:%02X:%02X\r\n", macBT[0], macBT[1], macBT[2], macBT[3], macBT[4], macBT[5]);
}

void loop() {
  delay(1000);
}
```

MACアドレスを指定しなくても、名前で検索して接続するのが正しいやり方だと思いますが、接続に時間がかかるという話もあったので、まず実験としては、MACアドレス直接指定で進めました。

##マスター側のコードでスレーブ側のアドレスを直接指定
SerialToSerialBTMのコードをほぼ、そのまま利用させて頂きました。（スレーブ側は、SerialToSerialBTを参考にしました）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266665/d7a7c240-b9de-8a03-8b14-d9b0031cc9aa.png)
このコードを基に、MACアドレスを接続相手のスレーブの値に合わせて修正し、また、nameを使わないで MAC Addressを使うようにコメントアウトやアンコメントした結果が以下のコードです。

```c:
BluetoothSerial SerialBT;
String MACadd = "**:**:**:**:**:**";
uint8_t address[6] = {0x**, 0x**, 0x**, 0x**, 0x**, 0x**};
bool connected;
void init_BT() { // Setup Bluetooth connection
  SerialBT.begin("ESP32_MASTER", true);
  Serial.println("Master Device:");
  connected = SerialBT.connect(address);
  if (connected) {
    Serial.println("Connected Succesfully!");
  } else {
    while (!SerialBT.connected(10000)) {
      Serial.println("Failed to connect. Make sure remote device is available and in range, then restart app.");
    }
  }
  if (SerialBT.disconnect()) {
    Serial.println("Disconnected Succesfully!");
  }
  SerialBT.connect();//reconnect
}
```

##マスター側のコード全容

```c:
#include <SPI.h>
#include "BluetoothSerial.h"

BluetoothSerial SerialBT;
String MACadd = "**:**:**:**:**:**";
uint8_t address[6] = {0x**, 0x**, 0x**, 0x**, 0x**, 0x**};
bool connected;
void init_BT() { // Setup Bluetooth connection
  SerialBT.begin("ESP32_MASTER", true);
  Serial.println("Master Device:");
  connected = SerialBT.connect(address); 
  if (connected) {
    Serial.println("Connected Succesfully!");
  } else {
    while (!SerialBT.connected(10000)) {
      Serial.println("Failed to connect. Make sure remote device is available and in range, then restart app.");
    }
  }
  if (SerialBT.disconnect()) {
    Serial.println("Disconnected Succesfully!");
  }
  SerialBT.connect();//reconnect
}

#define SPI1_CLK  32
#define SPI1_MISO 33
#define SPI1_MOSI 25
#define SPI1_SS   26
#define SPI_CLK 4000000
SPIClass SPI1(HSPI);
SPISettings spiSettings = SPISettings(SPI_CLK, SPI_MSBFIRST, SPI_MODE0);//0,0
void init_SPI() { // Setup SPI connection
  SPI1.begin(SPI1_CLK, SPI1_MISO, SPI1_MOSI, SPI1_SS);
  pinMode(SPI1_SS, OUTPUT);
  pinMode(SPI1_CLK, OUTPUT);
  pinMode(SPI1_MISO, INPUT);
  pinMode(SPI1_MOSI, OUTPUT);
  digitalWrite(SPI1_SS, HIGH) ;
  SPI1.beginTransaction(spiSettings);
}
void setup() {
  Serial.begin(115200);
  init_BT();
  init_SPI();
}
void loop() {
  uint16_t xval = adc_read(0); //Channel-0
  uint16_t yval = adc_read(1); //Channel-1
  String str = String(xval) + ',' +String(yval) + '\0';
  Serial.println(str);
  SerialBT.print(str);
  delay(20);
}
uint16_t adc_read(uint8_t channel) {
  digitalWrite(SPI1_SS, LOW);
  SPI1.transfer(0x06 | channel>>2);//bit7-3:dummy, bit2:start, bit1:single,bit0:D2
  uint8_t dh = SPI1.transfer((channel & 0x03) << 6);//D1,D0,X,Null,B11-B8
  uint8_t dl = SPI1.transfer(0x00);//B7-B0
  digitalWrite(SPI1_SS, HIGH);
  return ((dh & 0xF) << 8 | dl); //get 12bits data
}
```

#スレーブ側
スレーブ側は、サンプルコードを参考にして、setup()の中で初期化し、loop()の中では、readStringUntil()をつかって、マスターから送られてくる文字列の終端文字まで読み、文字列として処理に使います。一文字ずつ読むよりも効率が良いのと、今回のように、マスターも自分で作る場合には、送られてくる文字列の終端を、自分で決めることが出来るのでこの方法が良いと思われます。今回は、'\0'のNULL文字をマスターから送る文字列の終端文字に指定しています。

```c:
  if (SerialBT.available()) {
    String buf = SerialBT.readStringUntil('\0');
```

あとは、読み取ったバッファから必要なデータを取り出して、サーボモータを制御すればよい事になります。残念ながら、Arduinoの開発環境でコンマで文字列を区切る関数 (split()みたいな関数）が無いみたいなので、コンマの位置を探して、部分文字列に分割してから、整数化し、最後に、サーボモーターに送る数字範囲へのマッピングを行ってから、PWM出力しています。

```c:
  if (SerialBT.available()) {
    String buf = SerialBT.readStringUntil('\0');
    Serial.println(buf);
    int pos = buf.indexOf(','); //最初のコンマを探し、その文字位置を得る
    int xval = buf.substring(0, pos).toInt(); //頭から、最初のコンマの1文字前まで得てから整数化
    int yval = buf.substring(pos + 1).toInt(); //コンマの次から最後まで得てから整数化
    int x = (int)map(xval, 0, 4095, 60, 230); // <- 0～4095の値を 50～230へマッピング。便利！
    int y = (int)map(yval, 0, 4095, 50, 230);
    ledcWrite(1, x); //PWM出力してサーボを動かす
    ledcWrite(2, y);
  }
```

ちなみに、ESP32のPWM制御には、今回 ledcライブラリを使っています。GPIO25と GPIO26に、それぞれ X軸用 (Motor1）と、Y軸（Motor2)のPWM出力を割り当て、ledcライブラリの中でつかうために、ledcAttachPinでESP32の内部PWMチャンネルを連結させています。

```c:
#include "esp32-hal-ledc.h"

#define Motor1 25
#define Motor2 26

void setup() {
  Serial.begin(115200);
  ledcSetup(1, 50, 11);//CH:1,Freq:50Hz,Res:11bit
  ledcAttachPin(Motor1, 1);
  ledcAttachPin(Motor2, 2);
```

##スレーブ側のコード全容

```c:
#include "esp32-hal-ledc.h"
#include "BluetoothSerial.h"
BluetoothSerial SerialBT;

//COM17 in my environment
#define Motor1 25
#define Motor2 26

void setup() {
  Serial.begin(115200);
  ledcSetup(1, 50, 11);//CH:1,Freq:50Hz,Res:11bit
  ledcAttachPin(Motor1, 1);
  ledcAttachPin(Motor2, 2);
  SerialBT.begin("ESP32_SLAVE"); //Bluetooth device name
  Serial.println("Slave Device:");
}

int xmin = 60; //for 0.586ms
int xmax = 230; //for 2.246ms
void loop() {
  if (SerialBT.available()) {
    String buf = SerialBT.readStringUntil('\0');
    Serial.println(buf);
    int pos = buf.indexOf(',');
    int xval = buf.substring(0, pos).toInt();
    int yval = buf.substring(pos + 1).toInt();
    int x = (int)map(xval, 0, 4095, 60, 230);
    int y = (int)map(yval, 0, 4095, 50, 230);
    ledcWrite(1, x);
    ledcWrite(2, y);
  }
  delay(1);
}
```


#追記
ESP32での実験をする前に、ESP8266でWiFiをつかって、Node-REDでMQTTメッセージを使い、同じような事を試してみたのですが、あちこちでバッファ処理が入り、いくつかの数値が集められたパケットがスレーブ側に届くため、動きが全くスムーズで無く諦めました。 Bluetoothを使ったのは今回が初めてでしたが、ESP32のBluetoothが簡単につながる事が確認でき、レイテンシーも全く問題なく、非常にすっきりとスマイリー君が動きました。本当に昨今の開発環境の整備具合や、ネット情報の豊富さには驚くばかりです。　

今回作ったプロトを、3Dプリンタで作ったちょっとした箱に入れて、今度、孫が遊びに来た時に、ちょっと試してみようかな・・・と思ってます。喜んでくれないかな・・・・。