ESP32-C3-WROOM-02-N4 を使ったフルカラーLEDチューブ(NeoPixel)のコントローラーとKORG MicroKEY AirをBLE-MIDIで接続する

# はじめに
本記事は、下記のハードウェアを用いて、BLE-MIDIに準拠したMIDIキーボードとBLE接続によりLEDを光らせるソフトウェアの解説です。ハードウェアや開発環境の基本設定方法等については、本記事では詳細には説明致しませんので、必ず下記の記事をお読みください。

https://qiita.com/kaz19610303/items/14c51b291682ff07c770

なお、このハードウェアは、この記事の執筆者である**上野和昭氏**が代表を務める**合同会社Next Step**の**ECサイト**から購入可能です。

なお、今回使用したBLE-MIDIキーボードは、**KORG MicroKEY AIR-25**となります。筆者は、これ以外のBLE-MIDIキーボードを所有しておりませんので、これ以外のBLE-MIDIキーボードで動作するかは確認しておりませんので、ご了承願います。
#  事前準備
1. KORG MicroKEY AIRのファームウェアを最新版Ver1.05(2023.2.10)にアップデートする。
Ver1.04では動作しなかったので、必ずアップデートしてください。
2. Arduinoのライブラリ**Adafruit NeoPixel**をインストールしてください。（前記、上野氏の記事中にもインストールするように書かれていますので、そちらの記事を読んでインストール済の方は改めてインストールする必要はありません。上野氏の記事には画像付きで詳しくインストール方法が書かれていますので、ライブラリのインストール方法の詳細はそちらをご覧ください。）
2. 同じく、Arduinoのライブラリ**BLE-MIDI by lathoub**をインストールしてください。
# スケッチ例
~~~
#include <Arduino.h>
#include <BLEMIDI_Transport.h>

#include <hardware/BLEMIDI_Client_ESP32.h>

#include <Adafruit_NeoPixel.h>

#define PIN 5 //GPIO5
#define SW 4  //GPIO4
#define LED_BUILTIN 3 //GPIO3 
#define DIPSW_0  6  //GPIO6
#define DIPSW_1  7 //GPIO7
#define DIPSW_2  8 //GPIO8
#define DIPSW_3  10 //GPIO10
#define NUM_LED 60 //number of LEDs

#define KEY_OFFSET -12 //KEY OFFSET

#define WIPE  72 + KEY_OFFSET
#define TH_CHASE  74 + KEY_OFFSET
#define RAINBOW 76 + KEY_OFFSET
#define TH_CH_RAIN  77 + KEY_OFFSET
#define TH_CHASE1 71 + KEY_OFFSET

#define TH_C3 60 + KEY_OFFSET
#define TH_D3 62 + KEY_OFFSET
#define TH_E3 64 + KEY_OFFSET
#define TH_F3 65 + KEY_OFFSET
#define TH_G3 67 + KEY_OFFSET
#define TH_A3 69 + KEY_OFFSET
#define TH_A31 70 + KEY_OFFSET
#define TH_B3 71 + KEY_OFFSET

#define NOTE_OFF  0
#define NOTE_NULL  255

unsigned char Red[3] = {255, 0, 0};
unsigned char Orange[3] = {255, 127, 0};
unsigned char Kon[3] = {32, 64, 150};
unsigned char Blue[3] = {0, 0, 255};
unsigned char White[3] = {240, 240, 240};
unsigned char Black[3] = {0, 0, 0};
unsigned char Cyan[3] = {0, 255, 255};
unsigned char Green[3] = {0, 255, 0};
unsigned char Pink[3] = {250, 0, 250};

unsigned char *Col_ptr;

void ReadCB(void *parameter);       //Continuos Read function (See FreeRTOS multitasks)

unsigned long t0 = millis();
bool isConnected = false;

Adafruit_NeoPixel strip = Adafruit_NeoPixel(NUM_LED, PIN, NEO_GRB + NEO_KHZ800);

unsigned char midi_event_flag = NOTE_OFF;
unsigned char pre_event = NOTE_OFF;
bool restart_pat = true;
byte g_velo;


BLEMIDI_CREATE_DEFAULT_INSTANCE(); //Connect to first server found
//BLEMIDI_CREATE_INSTANCE("",MIDI)                  //Connect to the first server found
//BLEMIDI_CREATE_INSTANCE("d0:17:69:bc:bf:dd",MIDI) //Connect to a specific BLE address server
//BLEMIDI_CREATE_INSTANCE("MyBLEserver",MIDI)       //Connect to a specific name server

// -----------------------------------------------------------------------------
//
// -----------------------------------------------------------------------------
void setup()
{
  Serial.begin(115200);
  Serial.setTxTimeoutMs(0); 
  pinMode(SW, INPUT);  
  pinMode(LED_BUILTIN, OUTPUT);
  digitalWrite(LED_BUILTIN, LOW);

  BLEMIDI.setHandleConnected([]()
                             {
                               Serial.println("---------CONNECTED !---------");
                               isConnected = true;
                               digitalWrite(LED_BUILTIN, HIGH);
                             });

  BLEMIDI.setHandleDisconnected([]()
                                {
                                  Serial.println("---------NOT CONNECTED !---------");
                                  isConnected = false;
                                  digitalWrite(LED_BUILTIN, LOW);
                                });

  MIDI.setHandleNoteOn(OnAppleMidiNoteOn);
  
  MIDI.setHandleNoteOff(OnAppleMidiNoteOff);                   

  xTaskCreatePinnedToCore(ReadCB,           //See FreeRTOS for more multitask info  
                          "MIDI-READ",
                          3000,
                          NULL,
                          1,
                          NULL,
                          0); //Core0 or Core1

  // LED init
  Col_ptr = Black;

  strip.begin();
  strip.show(); // Initialize all pixels to 'off' 

  MIDI.begin();
}

// -----------------------------------------------------------------------------
//
// -----------------------------------------------------------------------------
void loop()
{
  unsigned long interval;
  //MIDI.read();  // This function is called in the other task

  //midi_event_flag = TH_CH_RAIN;//TEST
  //Col_ptr = Cyan;

  if (midi_event_flag == RAINBOW)
  {
    interval = 20;
  }
  else if ((midi_event_flag == TH_CHASE1) || (midi_event_flag == TH_CH_RAIN) || (midi_event_flag == WIPE))
  {
    if (g_velo < 100)
    {
      interval = 50;
    }
    else
    {
      interval = 30;
      if (midi_event_flag == WIPE)
      {
        interval = 20;
      }
    }
  }
  else
  {
    interval = 50;
  }
  if ((millis() - t0) > interval) //50ms
  {
    t0 = millis();
    //Serial.print(".");
    if (pre_event != midi_event_flag) //バカ避け NOTE OFFが来なくて、別のパターンの場合
    {
      restart_pat = true;
    }
    switch (midi_event_flag)
    {
      case WIPE:
        Wipe();
        break;
      case TH_CHASE:
        TheaterChase();
        break;
      case TH_CHASE1:
        TheaterChase();
        break;
      case RAINBOW:
        Rainbow();
        break;
      case TH_CH_RAIN:
        theaterChaseRainbow();
        break;
      case NOTE_OFF:
        All_off();
        break;
      default:
        break;
    }
    pre_event = midi_event_flag;
  }
}

/**
 * This function is called by xTaskCreatePinnedToCore() to perform a multitask execution.
 * In this task, read() is called every millisecond (approx.).
 * read() function performs connection, reconnection and scan-BLE functions.
 * Call read() method repeatedly to perform a successfull connection with the server 
 * in case connection is lost.
*/
void ReadCB(void *parameter)
{
  //Serial.print("READ Task is started on core: ");
  //Serial.println(xPortGetCoreID());
  for (;;)
  {
    MIDI.read(); 
    vTaskDelay(1 / portTICK_PERIOD_MS); //Feed the watchdog of FreeRTOS.
    //Serial.println(uxTaskGetStackHighWaterMark(NULL)); //Only for debug. You can see the watermark of the free resources assigned by the xTaskCreatePinnedToCore() function.
  }
  vTaskDelay(1);
}
// ====================================================================================
// Event handlers for incoming MIDI messages
// ====================================================================================

// -----------------------------------------------------------------------------
//
// -----------------------------------------------------------------------------
void OnAppleMidiNoteOn(byte channel, byte note, byte velocity) {
  
  digitalWrite(LED_BUILTIN, LOW);
  g_velo = velocity;

  switch (note)
  {
    case  TH_C3:
      midi_event_flag = TH_CHASE1;
      Col_ptr = Red;
      break;
    case  TH_D3:
      midi_event_flag = TH_CHASE1;
      Col_ptr = Orange;
      break;
    case  TH_E3:
      midi_event_flag = TH_CHASE1;
      Col_ptr = Kon;
      break;
    case  TH_F3:
      midi_event_flag = TH_CHASE1;
      Col_ptr = Blue;
      break;
    case  TH_G3:
      midi_event_flag = TH_CHASE1;
      Col_ptr = White;
      break;
    case  TH_A3:
      midi_event_flag = TH_CHASE1;
      Col_ptr = Cyan;
      break;
    case  TH_A31:
      midi_event_flag = TH_CHASE1;
      Col_ptr = Pink;
      break;
    case  TH_B3:
      midi_event_flag = TH_CHASE1;
      Col_ptr = Green;
      break;

    case WIPE:  //C4
      midi_event_flag = WIPE;
      break;
    case TH_CHASE:  //D4
      midi_event_flag = TH_CHASE;
      if (velocity < 13) //10%未満
      {
        Col_ptr = Black;
      }
      else if (velocity < 26)
      {
        Col_ptr = Red;
      }
      else if (velocity < 39)
      {
        Col_ptr = Orange;
      }
      else if (velocity < 51)
      {
        Col_ptr = Kon;
      }
      else if (velocity < 64)
      {
        Col_ptr = Blue;
      }
      else if (velocity < 77) //60%未満
      {
        Col_ptr = White;
      }
      else //if (velocity <89)
      {
        Col_ptr = Cyan;
      }
      break;
    case RAINBOW:  //E4
      midi_event_flag = RAINBOW;
      break;
    case TH_CH_RAIN: //F4
      midi_event_flag = TH_CH_RAIN;
      break;
    default:
      break;
  }
}

// -----------------------------------------------------------------------------
//
// -----------------------------------------------------------------------------
void OnAppleMidiNoteOff(byte channel, byte note, byte velocity) {
  
  digitalWrite(LED_BUILTIN, HIGH);
  midi_event_flag = NOTE_OFF;
  restart_pat = true;
}

// ====================================================================================
// Subroutine for LED
// Fill the dots one after the other with a color
// ====================================================================================
void All_off()
{
  int i;
  uint32_t c;

  c = strip.Color(0, 0, 0);
  for (i = 0; i < strip.numPixels(); i++)
  {
    strip.setPixelColor(i, c);
  }
  strip.show();
  midi_event_flag = NOTE_NULL;
}

void Wipe(void)
{
  static int i = 0;
  static int j = 0;
  static int loop_end;
  static uint32_t c;

  if (restart_pat == true)
  {
    i = 0;
    loop_end = strip.numPixels();
    switch (j)
    {
      case 0:
        c = strip.Color(255, 0, 0);
        break;
      case 1:
        c = strip.Color( 0, 255, 0);
        break;
      case 2:
        c = strip.Color( 0, 0, 255);
        break;
      default:
        break;
    }
    j++;
    if (j >= 3) j = 0;
    restart_pat = false;
  }
  strip.setPixelColor(i, c);
  strip.show();
  i++;
  if (i >= loop_end)
  {
    restart_pat = true;
  }
}

void Rainbow(void)
{
  static int j = 0;

  if (restart_pat == true)
  {
    j = 0;
    restart_pat = false;
  }
  for (int i = 0; i < strip.numPixels(); i++) {
    strip.setPixelColor(i, Wheel((i + j) & 255));
  }
  strip.show();
  j++;
  if (j >= 256) j = 0;
}

void TheaterChase(void)
{
  static int j = 0;
  static int k = 0;
  static int q = 0;
  static bool even = true;
  static uint32_t c;

  if (restart_pat == true)
  {
    j = 0;
    q = 0;
    even = true;
    switch (k)
    {
      case 0:
        c = strip.Color(Col_ptr[0], Col_ptr[1], Col_ptr[2]);
        break;
      case 1:
        c = strip.Color(Col_ptr[0], Col_ptr[1], Col_ptr[2]);
        //c = strip.Color(Col_ptr[0], 0, 0);
        break;
      case 2:
        c = strip.Color(Col_ptr[0], Col_ptr[1], Col_ptr[2]);
        //c = strip.Color(0, 0, Col_ptr[2]);
        break;
      default:
        break;
    }
    k++;
    if (k >= 3) k = 0;
    restart_pat = false;
  }
  if (even == true)
  {
    for (int i = 0; i < strip.numPixels(); i = i + 3) {
      strip.setPixelColor(i + q, c);  //turn every third pixel on
    }
    strip.show();
    even = false;
  }
  else
  {
    for (int i = 0; i < strip.numPixels(); i = i + 3) {
      strip.setPixelColor(i + q, 0);      //turn every third pixel off
    }
    even = true;
    q++;
    if (q >= 3)
    {
      q = 0;
      j++;
      if (j >= 10)
      {
        j = 0;
      }
    }
  }
}

void theaterChaseRainbow(void)
{
  static int j = 0;
  static int q = 0;
  static bool even = true;

  if (restart_pat == true)
  {
    j = 0;
    q = 0;
    even = true;
    restart_pat = false;
  }
  if (even == true)
  {
    for (int i = 0; i < strip.numPixels(); i = i + 3) {
      strip.setPixelColor(i + q, Wheel( (i + j) % 255)); //turn every third pixel on
    }
    strip.show();
    even = false;
  }
  else
  {
    for (int i = 0; i < strip.numPixels(); i = i + 3) {
      strip.setPixelColor(i + q, 0);      //turn every third pixel off
    }
    even = true;
    q++;
    if (q >= 3)
    {
      q = 0;
      j++;
      if (j >= 256)
      {
        j = 0;
      }
    }
  }
}

// Input a value 0 to 255 to get a color value.
// The colours are a transition r - g - b - back to r.
uint32_t Wheel(byte WheelPos) {
  WheelPos = 255 - WheelPos;
  if (WheelPos < 85) {
    return strip.Color(255 - WheelPos * 3, 0, WheelPos * 3);
  }
  if (WheelPos < 170) {
    WheelPos -= 85;
    return strip.Color(0, WheelPos * 3, 255 - WheelPos * 3);
  }
  WheelPos -= 170;
  return strip.Color(WheelPos * 3, 255 - WheelPos * 3, 0);
}

// -----------------------------------------------------------------------------
//
// -----------------------------------------------------------------------------
uint8_t device_id(void)
{
  //INPUT_PULLUP Mode except for GPIO16
  pinMode(DIPSW_0, INPUT);
  pinMode(DIPSW_1, INPUT);
  pinMode(DIPSW_2, INPUT);
  pinMode(DIPSW_3, INPUT);

  delay(10);
  return ((digitalRead(DIPSW_3) << 3) | (digitalRead(DIPSW_2) << 2) | (digitalRead(DIPSW_1) << 1) | digitalRead(DIPSW_0)+1);
}
~~~
# スケッチ例の説明
1.オクターブ調整
スケッチ例では、MIDIノートNo.60(C3)～No.77(F4)をONすることにより、LEDがいろいろなパターンで発光します。
MIDIキーボードの鍵盤数やメーカーによって左端の鍵盤がCxに配置されているかはまちまちですので、配置を調整したい場合は、
~~~
#define KEY_OFFSET -12 //KEY OFFSET
~~~
を**オクターブ単位(12の倍数）** で＋－方向に変更してください。
デフォルトの-12は**MicroKEY Air 25** に合うように調整しています。

2.接続するMIDIデバイスの指定
~~~
BLEMIDI_CREATE_DEFAULT_INSTANCE(); //Connect to first server found
//BLEMIDI_CREATE_INSTANCE("",MIDI)                  //Connect to the first server found
//BLEMIDI_CREATE_INSTANCE("d0:17:69:bc:bf:dd",MIDI) //Connect to a specific BLE address server
//BLEMIDI_CREATE_INSTANCE("MyBLEserver",MIDI)       //Connect to a specific name server
~~~
スケッチ例では、スキャンして最初に見つかったBLE-MIDI機器に接続します。大概これで問題ないですが、多数のMIDI機器が混在する環境で明示した機器に接続したい場合は、
~~~
//BLEMIDI_CREATE_DEFAULT_INSTANCE(); //Connect to first server found
//BLEMIDI_CREATE_INSTANCE("",MIDI)                  //Connect to the first server found
BLEMIDI_CREATE_INSTANCE("d0:17:69:bc:bf:dd",MIDI) //Connect to a specific BLE address server
//BLEMIDI_CREATE_INSTANCE("MyBLEserver",MIDI)       //Connect to a specific name server
~~~
に変更します。これにより、指定したBLE addressの機器に接続します。BLE機器はのアドレスは、デフォルトのソースのままスケッチを実行して、機器との接続完了時にシリアルモニタに表示されますので、それを上記ソースの
~~~
"d0:17:69:bc:bf:dd"
~~~
部分を表示されたアドレスに置き換えてください。（シリアルモニタのBLE address表示例は、下記のUSB CDC設定説明のキャプチャー画像の中の一番下の行です。）

![20230302_164059.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3286694/58f1d84c-7c5a-1f5f-39eb-7694c53e8acc.jpeg)
**[動画はここをクリック](https://youtu.be/7exJmwE3PZw)**


# 注意点
この基板は、ESP32C3の内蔵USBをシリアルモニタ用のUARTとして使いますので、必ず**USB CDC On Boot**を**Enabled**にして下さい。![CDC_ENABLE.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3286694/0d77d9a4-ca97-17a6-fc69-b0aa6a2aaff2.jpeg)

# 終わりに

Arduinoのライブラリが非常に充実しており、極めて少ないコードでESP32とBLE-MIDIキーボードを接続することができました。このようなライブラリを提供してくださったlathoub氏に感謝申し上げます。
MIDIライブラリーの使い方は、lathoub氏のGithub等をお読みください。
**コールバック関数**によるデータ受信処理への分岐や**FreeRTOSを使いマルチスレッド**によって**MIDI受信データ**を**ポーリング**して受信する等このライブラリを使うには**リアルタイム処理系によく使われる手法**に対しての慣れが必要かと思います。
機会がありましたら、そのあたりも解説できればよいかと思います。