M5StickCで ESPNowトランシーバーを作ってみる話


#1. はじめに

M5StickC には マイクロフォンがついており
HAT として スピーカーに簡単接続できる

https://www.switch-science.com/catalog/5754/

最近ESPNowっぽいものでセンサを繋ぎまくった経緯があり
どれくらいの大きいデータを送信できるのかの検証を兼ねて
作ってみる

* 近くのホームセンターで ESPNow で 33m の通信が出来感動したことは言うまでもありません
* 最初は なんちゃってBle通信だったんですけど 10m くらいしか出来なかった


#2. M5StickC I2Sの扱い方

色々とWEBをさまよったあげく
lang-shipさんの素敵なサイトに辿りつく

https://lang-ship.com/blog/work/m5stickc-mic-3/

なるほど..
録音は I2S Mic -> soundBuffer -> soundStorage
再生は　soundStorage -> soundBuffer -> I2S Speaker
って感じで食わせるという事なんですね

#3. HATを使ってI2Sで再生もできちゃうのね

2個の M5StickC は持っているのだが
HAT は 1個しかない... 100円ガチャで入手したスピーカーとか
繋いでも大丈夫なんだろうか...
という事で半田コテで GND/G26 に繋がるように製作する

![IMG_3765.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2063214/45542d0e-e9c1-8304-5607-e2f6af4380dd.jpeg)

![IMG_3766.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2063214/d165369f-c561-41a4-ef0b-6e957b0e11a5.jpeg)


#4. 仕様

二つの M5StickC に同じプログラムを転送して
録音が終わったら soundStorage 全体を ESPNow で転送して
ブロードキャストしてあげれば MacAddress に関係なく 
受信した側は soundStorage の中身を再生すれば良い
これでトランシーバーっぽいのが出来上がるという事になる
転送の開始と転送の最後に1Byteのフラグ(STX/ETX)を送り
旨い具合に同期を取る感じで....

* ESPNOW_SEND_DELAY が3になってますが delay を入れないと大きいデータは上手い具合に同期が取れないようで resurt を見てちょっと愕然としました:frowning2:

``` myESPNow.h
//============================================> myESPNow.h
//
// M5Stick-mic-3.ino
// +-----+
// |     |
// |     | Rec:
// |     | I2S Mic -> soundBuffer -> soundStorage
// |     |
// |     | Play:
// |M5   | soundStorage -> soundBuffer -> I2S Speaker
// |Stick|
// +-----+
//
// M5WalkyTalky.ino
// +-----+                                            +-----+
// |     |                                            |     |
// |     | Rec:                                       |     |
// |     | I2S Mic -> soundBuffer -> soundStorage     |     |
// |     |                                            |     |
// |     |                                      :Play |     |
// |Talk | soundStorage -> soundBuffer -> I2S Speaker |Listn|
// |   er|                                            |   er|
// +-----+                                            +-----+
//

#include <esp_now.h>
#include <WiFi.h>
esp_now_peer_info_t slave;
#define ESPNOW_MAXSEND (250)        // ESPNow送信最大値
#define ESPNOW_SEND_DELAY (3)       // 最低の待ち時間
#define STX (0x02) // 転送開始
#define ETX (0x03) // 転送終了

// MacAdrsを表示する
void dispAdrs(const uint8_t *mac_addr)
{
  char macStr[18];
  snprintf(macStr, sizeof(macStr), "%02X:%02X:%02X:%02X:%02X:%02X",
           mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]);
  M5.Lcd.println(macStr);
}

void prePlay()
{
  // 初期雑音を消す
  memset(soundStorage, 0, sizeof(soundStorage));
  recPos = 128;
  i2sPlay();
}

// タイトル表示
void titleDisp()
{
  M5.Lcd.print(" ");
  M5.Lcd.setTextColor(WHITE, BLACK);
  M5.Lcd.println(" M5WalkyTalky ");
  M5.Lcd.setTextColor(BLACK, WHITE);
  M5.Lcd.println(" BtnA Rec/Speak" );
  M5.Lcd.println(" BtnB Play" );
}

// データ送信コールバック
void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
  //  dispAdrs(mac_addr);
  //  M5.Lcd.println(status == ESP_NOW_SEND_SUCCESS ? "Success" : "Fail");
}

// データ受信コールバック
void OnDataRecv(const uint8_t *mac_addr, const uint8_t *recv, int recvcnt) {
  static int row_ = 0;
  //  dispAdrs(mac_addr);
  Serial.printf("[recv] recvcnt:%d\n", recvcnt);

  if (recvcnt == 1) {
    // 転送された長さが1Byte?
    if (recv[0] == STX) {
      // STX？
      row_ = 0;
      recPos = 0;
      memset(soundStorage, 0x0, sizeof(soundStorage));
    } else {
      // ETX？
      Serial.printf("[recv] row:%d recPos:%d\n", row_, recPos);
      digitalWrite(10, !HIGH);
      M5.Lcd.setCursor(0, 24);
      M5.Lcd.println(" Play...   ");
      M5.Lcd.setCursor(0, 32);
      M5.Lcd.printf(" Recv <= %5d Byte", recPos);
      i2sPlay();
      M5.Lcd.setCursor(0, 24);
      M5.Lcd.println("           ");
      digitalWrite(10, !LOW);
    }
  } else {
    // recvcntが1以外
    memcpy(&soundStorage[recPos], recv, recvcnt);
    recPos += recvcnt;
    row_ ++;
  }
}

void resultCheck(int cnt, esp_err_t result)
{
  switch (result) {
    case ESP_OK:
      break;
    case ESP_ERR_ESPNOW_NOT_INIT:
      Serial.printf("%d:ESPNOW not Init.\n", cnt);
      break;
    case ESP_ERR_ESPNOW_ARG:
      Serial.printf("%d:Invalid Argument.\n", cnt);
      break;
    case ESP_ERR_ESPNOW_INTERNAL:
      Serial.printf("%d:Internal Error.\n", cnt);
      break;
    case ESP_ERR_ESPNOW_NO_MEM:
      Serial.printf("%d:ESP_ERR_ESPNOW_NO_MEM.\n", cnt);
      break;
    case ESP_ERR_ESPNOW_NOT_FOUND:
      Serial.printf("%d:Peer not found.\n", cnt);
      break;
    default:
      Serial.printf("%d:Not sure what happened.\n", cnt);
      break;
  }
}

void sendESPNow()
{
  uint8_t dt[1] = {0};
  size_t sendPos = 0;
  int mod = recPos % ESPNOW_MAXSEND;
  int row = recPos / ESPNOW_MAXSEND;

  esp_err_t result;
  Serial.printf("[send] row:%d mod:%d\n", row, mod);
  M5.Lcd.setCursor(0, 32);
  M5.Lcd.printf(" Send => %5d Byte", recPos);

  dt[0] = STX;
  result = esp_now_send(slave.peer_addr, dt, 1);
  delay(ESPNOW_SEND_DELAY);
  resultCheck(-1, result);

  int i;
  for (i = 0; i < row; i++) {
    result = esp_now_send(slave.peer_addr, &soundStorage[sendPos], ESPNOW_MAXSEND);
    delay(ESPNOW_SEND_DELAY);
    resultCheck(i, result);
    sendPos += ESPNOW_MAXSEND;
  }
  if (mod) {
    result = esp_now_send(slave.peer_addr, &soundStorage[sendPos], mod);
    delay(ESPNOW_SEND_DELAY);
    resultCheck(i, result);
    sendPos += mod;
  }
  dt[0] = ETX;
  result = esp_now_send(slave.peer_addr, dt, 1);
  delay(ESPNOW_SEND_DELAY);
  resultCheck(-1, result);
  Serial.printf("[send] sendPos %d\n", sendPos);
}

void setupESPNow()
{
  WiFi.mode(WIFI_STA);
  WiFi.disconnect();
  if (esp_now_init() == ESP_OK) {
    Serial.print("ESPNow Init Success\n");
  } else {
    Serial.print("ESPNow Init Failed\n");
    ESP.restart();
  }
  pinMode(10, OUTPUT);
  digitalWrite(10, !LOW);
  memset(&slave, 0x0, sizeof(slave));
  for (int i = 0; i < sizeof(slave.peer_addr); ++i) {
    slave.peer_addr[i] = (uint8_t)0xff;
  }
  esp_err_t addStatus = esp_now_add_peer(&slave);
  esp_now_register_send_cb(OnDataSent);
  esp_now_register_recv_cb(OnDataRecv);
}

//<============================================ myESPNow.h

```
#5. lang-shipさんの ino に組み込む

* M5WalkyTalky.ino myESPNow.h は同じフォルダに入れて下さい
* 立ち上がりに スピーカーから雑音が出ちゃうので prePlay(recPos = 128 で短い再生)を実行
* `100KByte` を確保しようとすると謎のエラー しかたなく`80KByte`に調整
* サンプリングレートは 44100 で 3秒くらいしか音が出ない、それだと話にならないので 16384 で調整、これで5秒は話せる
* 録音は `BtnA`、 離すと自動的にブロードキャストします
* 話したあとに `BtnB` で再生出来ます
* 受信した側も `BtnB` で再度再生が出来ます
* 修正部分には `//edit` がついてます

``` M5WalkyTalky.ino
//
// M5StickCのマイクを使ってみる その3 録音再生
// https://lang-ship.com/blog/work/m5stickc-mic-3/
//
//

#include <M5StickC.h>
#include <driver/i2s.h>

#define htonl(x) ( ((x)<<24 & 0xFF000000UL) | \
                   ((x)<< 8 & 0x00FF0000UL) | \
                   ((x)>> 8 & 0x0000FF00UL) | \
                   ((x)>>24 & 0x000000FFUL) )

#define PIN_CLK       (0)           // I2S Clock PIN
#define PIN_DATA      (34)          // I2S Data PIN
#define SAMPLING_RATE (16384)       // サンプリングレート(44100, 22050, 16384, 8192, more...) // edit
#define BUFFER_LEN    (1024 *  1)   // バッファサイズ // edit
#define STORAGE_LEN   (1024 * 80)   // 本体保存容量(MAX 100K前後) // edit

#define WAVE_EXPORT   (0)           // WAVEファイルに出力するか


uint8_t soundBuffer[BUFFER_LEN];    // DMA転送バッファ
uint8_t soundStorage[STORAGE_LEN];  // サウンドデータ保存領域

bool recFlag = false;               // 録音状態
int recPos = 0;                     // 録音の長さ

// 録音をする
void i2sRecord() {
  // 録音用設定
  i2s_config_t i2s_config = {
    .mode                 = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_PDM),
    .sample_rate          = SAMPLING_RATE,
    .bits_per_sample      = I2S_BITS_PER_SAMPLE_16BIT,
    .channel_format       = I2S_CHANNEL_FMT_ALL_RIGHT,
    .communication_format = I2S_COMM_FORMAT_I2S,
    .intr_alloc_flags     = ESP_INTR_FLAG_LEVEL1,
    .dma_buf_count        = 2,
    .dma_buf_len          = BUFFER_LEN,
    .use_apll             = false,
    .tx_desc_auto_clear   = true,
    .fixed_mclk           = 0,
  };

  // PIN設定
  i2s_pin_config_t pin_config;
  pin_config.bck_io_num   = I2S_PIN_NO_CHANGE;
  pin_config.ws_io_num    = PIN_CLK;
  pin_config.data_out_num = I2S_PIN_NO_CHANGE;
  pin_config.data_in_num  = PIN_DATA;

  // 録音設定実施
  i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL);
  i2s_set_pin(I2S_NUM_0, &pin_config);
  i2s_set_clk(I2S_NUM_0, SAMPLING_RATE, I2S_BITS_PER_SAMPLE_16BIT, I2S_CHANNEL_MONO);

  // 録音開始
  recFlag = true;
  xTaskCreatePinnedToCore(i2sRecordTask, "i2sRecordTask", 2048, NULL, 1, NULL, 1);
}

// 再生をする
void i2sPlay() {
  // 再生設定
  i2s_config_t i2s_config = {
    .mode                 = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX | I2S_MODE_DAC_BUILT_IN),
    .sample_rate          = SAMPLING_RATE,
    .bits_per_sample      = I2S_BITS_PER_SAMPLE_16BIT,
    .channel_format       = I2S_CHANNEL_FMT_ONLY_LEFT,
    .communication_format = I2S_COMM_FORMAT_I2S_MSB,
    .intr_alloc_flags     = ESP_INTR_FLAG_LEVEL1,
    .dma_buf_count        = 2,
    .dma_buf_len          = BUFFER_LEN,
    .use_apll             = false,
    .tx_desc_auto_clear   = true,
    .fixed_mclk           = 0,
  };

  // 再生設定実施
  i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL);
  i2s_set_pin(I2S_NUM_0, NULL);
  i2s_zero_dma_buffer(I2S_NUM_0);

  // 再生
  size_t transBytes;
  size_t playPos = 0;
  while ( playPos < recPos ) {
    for ( int i = 0 ; i < BUFFER_LEN ; i += 2 ) {
      soundBuffer[i] = 0;                         // 下位8ビットは無視される
      soundBuffer[i + 1] = soundStorage[playPos]; // 上位8ビットにuint8_tのデータを入れる
      playPos++;
    }

    // データ転送
    i2s_write(I2S_NUM_0, (char*)soundBuffer, BUFFER_LEN, &transBytes, (100 / portTICK_RATE_MS));
//    Serial.println(playPos); // edit
  }
  // 後始末
  i2s_zero_dma_buffer(I2S_NUM_0);
  i2s_driver_uninstall(I2S_NUM_0);
}

// 録音用タスク
void i2sRecordTask(void* arg)
{
  // 初期化
  recPos = 0;
  memset(soundStorage, 0, sizeof(soundStorage));

  // 録音処理
  while (recFlag) {
    size_t transBytes;

    // I2Sからデータ取得
    i2s_read(I2S_NUM_0, (char*)soundBuffer, BUFFER_LEN, &transBytes, (100 / portTICK_RATE_MS));

    // int16_t(12bit精度)をuint8_tに変換
    for (int i = 0 ; i < transBytes ; i += 2 ) {
      if ( recPos < STORAGE_LEN ) {
        int16_t* val = (int16_t*)&soundBuffer[i];
        soundStorage[recPos] = ( *val + 32768 ) / 256;
        recPos++;
      } else {
        digitalWrite(10, !LOW); // edit
      }
    }
    Serial.printf("transBytes = %d, STORAGE_LEN=%d, recPos=%d\n", transBytes, STORAGE_LEN, recPos);
    vTaskDelay(1 / portTICK_RATE_MS);
  }

  i2s_driver_uninstall(I2S_NUM_0);

  // タスク削除
  vTaskDelete(NULL);
}

#include "myESPNow.h" // edit

void setup() {
  M5.begin();
  M5.Lcd.setRotation(3);
  M5.Lcd.fillScreen(WHITE);
  M5.Lcd.setTextColor(BLACK, WHITE);
  /* // edit
    M5.Lcd.println("Sound Recorder");
    M5.Lcd.println("BtnA Record");
    M5.Lcd.println("BtnB Play");
  */
  setupESPNow();  // edit
  prePlay(); // edit
  titleDisp();  // edit
}

void loop() {
  M5.update();

  if ( M5.BtnA.wasPressed() ) {
    digitalWrite(10, !HIGH); // edit
    // 録音スタート
    M5.Lcd.setCursor(0, 24);
    M5.Lcd.println(" Rec...    ");
    Serial.println("Record Start");
    i2sRecord();
  } else if ( M5.BtnA.wasReleased() ) {
    digitalWrite(10, !LOW); // edit
    // 録音ストップ
    M5.Lcd.setCursor(0, 24);
    M5.Lcd.println("           ");
    recFlag = false;
    delay(100); // 録音終了まで待つ
    sendESPNow(); // edit
    Serial.println("Record Stop");

    // WAVEファイルをシリアルに出力
    if ( WAVE_EXPORT ) {
      Serial.printf("52494646");                        // RIFFヘッダ
      Serial.printf("%08lx", htonl(recPos + 44 - 8));   // 総データサイズ+44(チャンクサイズ)-8(ヘッダサイズ)
      Serial.printf("57415645");                        // WAVEヘッダ
      Serial.printf("666D7420");                        // フォーマットチャンク
      Serial.printf("10000000");                        // フォーマットサイズ
      Serial.printf("0100");                            // フォーマットコード
      Serial.printf("0100");                            // チャンネル数
      Serial.printf("%08lx", htonl(SAMPLING_RATE));     // サンプリングレート
      Serial.printf("%08lx", htonl(SAMPLING_RATE));     // バイト／秒
      Serial.printf("0200");                            // ブロック境界
      Serial.printf("0800");                            // ビット／サンプル
      Serial.printf("64617461");                        // dataチャンク
      Serial.printf("%08lx", htonl(recPos));            // 総データサイズ

      for (int n = 0; n <= recPos; n++) {
        Serial.printf("%02x", soundStorage[n]);
      }
      Serial.printf("\n");
    }
  } else if ( M5.BtnB.wasReleased() ) {
    // 再生スタート
    M5.Lcd.setCursor(0, 24);
    M5.Lcd.println(" Play...");
    Serial.println("Play Start");
    i2sPlay();
    M5.Lcd.setCursor(0, 24);
    M5.Lcd.println("        ");
    Serial.println("Play Stop");
  }

  delay(10);
}
```

#6. 最後に
ESPNow の転送はセンサーでは随分とやっていたのですが
さすがに 80KByte はきつかったです
でも　これで写真でも映像でも送れる事がわかって一安心です

いつも素晴らしい src を公開してくださっている lang-shipさんに感謝です
今後ともよろしくお願い申し上げます
https://lang-ship.com/blog/