Raspberry Pi 4 の Node-RED から KORG nanoKEY2 キーボードの MIDI データを取得し MQTT 経由で M5Stack と連携するメモ

Raspberry Pi 4 の Node-RED から KORG nanoKEY2 キーボードの MIDI データを取得し M5Stack と MQTT 経由で連携するメモです。

この記事は 東京テクニカルカレッジ Advent Calendar 2022 の 15 日目の記事です。

MIDI 機器は楽器でつよい

image

MIDI デバイスは、キーボードやミキサーといった楽器です。楽器ですので、演奏するわけですから繰り返し押されても使い続けられる耐久性がありますし、なにより遅れがなく音を出すためのレスポンスの良さがあります。このあたり、自作の IoT デバイスで作ったスイッチで実現する強度ではたどり着けない強さがあります。

MIDI の規格からみても演奏情報を遅延なく送るための長年培った知見が詰まっているので安心感があります。こちらも、同様のレスポンスの良さを自分で実現するのは厳しいです。 MIDI すごい。

というわけで、これを IoT と連携してみるわけです。

このように動きます

このように Raspberry Pi 4 につながった KORG nanoKEY2 キーボードを弾いてみると Node-RED から MIDI データを取得して MQTT 経由で M5Stack に弾かれたキー番号を送ってディスプレイに表示されます。

Raspberry Pi OS のバージョン

Raspberry Pi OS バージョンは以下の通りです。

uname -a

こちらのコマンドでは、

Linux raspberrypi 5.15.61-v7l+ #1579 SMP Fri Aug 26 11:13:03 BST 2022 armv7l GNU/Linux

となり、

lsb_release -a

こちらのコマンドでは、

No LSB modules are available.
Distributor ID: Raspbian
Description:    Raspbian GNU/Linux 11 (bullseye)
Release:        11
Codename:       bullseye

こちらで、Raspberry Pi 4 に Node-RED はインストール済みの前提で進めます。

Raspberry Pi の Node-RED で node-red-contrib-midi インストールの注意点

Raspberry Pi の Node-RED で MIDI データをやりとりするには node-red-contrib-midi が便利ですが、インストールするときには注意点があります、別途まとめたので以下を参考にインストールしてください。

最近 2022 年の Raspberry Pi OS で Node-RED で node-red-contrib-midi を使おうとしたら node-gyp エラーになったのを解決したメモ

nanoKEY 2 を Raspberry Pi 4 につなぐ

image

このように、mini USB – USB A のケーブルで Raspberry Pi 4 とつなぎます。

Node-RED のフロー

image

Raspberry Pi の Node-RED のフローはこのようになっています。

image

node-red-contrib-midi ノードの一つである midi in ノードは USB で nanoKEY2 をつなぐと以下のように認識します。

image

nanoKEY2 を選択することで、すんなり認識します。

image

これで nanoKEY2 を弾いてあげるとこのようなデータが来ます。1 番目のデータが押された音階としてのキー番号です。2 番目は、押された強さがきていて、指を離したときに 64 の固定値が来るようです。

image

今回は switch ノードで 2 番目の値で 64 を除外することで、押されたときのみ反応するようにしました。

image

このような設定ですね。

image

あとはこの change ノードで 1 番目の値を MQTT に加工して送るようにしています。

image

このように設定していて、

image

message 値に、数字を文字列に変換した音階としてのキー番号を送って MQTT につながった M5Stack にデータを送るようにしています。

image

MQTT ノードは今回は CloudMQTT で建てた MQTT ブローカを使っています。

image

このようにつないでいます。

この設定を行ってデプロイしてます。

このフローの JSON ファイル

このフローの JSON ファイルも置いておきます。

[{"id":"3d6e40ebac7fd7ab","type":"midi in","z":"011eb05152755699","name":"","midiport":"1","x":370,"y":420,"wires":[["052634620b6260c4","03a971288218ba5b"]]},{"id":"d28ad6ac54f3fb4b","type":"mqtt out","z":"011eb05152755699","name":"","topic":"top/subscribe","qos":"","retain":"","respTopic":"","contentType":"","userProps":"","correl":"","expiry":"","broker":"02daaaab19d0d7f6","x":1050,"y":480,"wires":[]},{"id":"ecb19286dfb1c8a7","type":"change","z":"011eb05152755699","name":"1番目の値をmessage","rules":[{"t":"set","p":"midiPayload","pt":"msg","to":"payload","tot":"msg"},{"t":"set","p":"payload","pt":"msg","to":"{}","tot":"json"},{"t":"set","p":"payload.message","pt":"msg","to":"$string(midiPayload[0])","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":820,"y":480,"wires":[["d28ad6ac54f3fb4b","1e846404cba6f371"]]},{"id":"052634620b6260c4","type":"debug","z":"011eb05152755699","name":"debug 8","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":560,"y":420,"wires":[]},{"id":"03a971288218ba5b","type":"switch","z":"011eb05152755699","name":"2番目の値が64以外","property":"payload[1]","propertyType":"msg","rules":[{"t":"neq","v":"64","vt":"num"}],"checkall":"true","repair":false,"outputs":1,"x":590,"y":480,"wires":[["ecb19286dfb1c8a7"]]},{"id":"1e846404cba6f371","type":"debug","z":"011eb05152755699","name":"debug 10","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1040,"y":420,"wires":[]},{"id":"c82a98180106b15d","type":"comment","z":"011eb05152755699","name":"nanoKEY2 からデータが来る","info":"","x":440,"y":380,"wires":[]},{"id":"02daaaab19d0d7f6","type":"mqtt-broker","name":"","broker":"driver.cloudmqtt.com","port":"1880","clientid":"","autoConnect":true,"usetls":false,"protocolVersion":"4","keepalive":"60","cleansession":true,"birthTopic":"","birthQos":"0","birthPayload":"","birthMsg":{},"closeTopic":"","closeQos":"0","closePayload":"","closeMsg":{},"willTopic":"","willQos":"0","willPayload":"","willMsg":{},"userProps":"","sessionExpiry":"","credentials":{}}]

M5Stack のソースコード

image

さて MQTT からデータを受け取ってキー番号を表示する M5Stack 側のソースコードです。M5Stack が書き出せる前提です。

M5Stack で MQTT から Node-RED 経由で JSON データでまとめてシナリオを渡してビジュアルが変わる仕組みを作ったメモ を参考に、PubSubClient や ArduinoJson を事前にライブラリインストールしておきましょう。

#include <M5Stack.h>

// Wi-Fi をつなぐためのライブラリ
// 今回は MQTT のため
#include <WiFiClient.h>
#include <WiFi.h>

// MQTT をつなぎためのライブラリ
// 今回追加インストールする
#include <PubSubClient.h>  // インストールすれば色がつく
// JSON を扱いやすくするライブラリ
#include <ArduinoJson.h> // こちらは色がついてなくてOK

// Wi-FiのSSID
const char *ssid = "Wi-FiのSSID";
// Wi-Fiのパスワード
const char *password = "Wi-Fiのパスワード";

// 今回使いたい CloudMQTT のブローカーのアドレス
const char *mqttEndpoint = "driver.cloudmqtt.com";
// 今回使いたい CloudMQTT のポート
const int mqttPort = 1880;
// 今回使いたい CloudMQTT のユーザー名
const char *mqttUsername = "CloudMQTT のユーザー名";
// 今回使いたい CloudMQTT のパスワード
const char *mqttPassword = "CloudMQTT のパスワード";

// デバイスID
// デバイスIDは機器ごとにユニークにします
// YOURNAME を自分の名前の英数字に変更します
// デバイスIDは同じMQTTブローカー内で重複すると大変なので、後の処理でさらにランダム値を付与してますが、名前を変えるのが確実なので、ちゃんと変更しましょう。
const char *deviceID = "M5Stack-YOURNAME";

// MQTT メッセージを LINE BOT に知らせるトピック
// YOURNAME を自分の名前の英数字に変更します
const char *pubTopic = "top/publish";

// MQTT メッセージを LINE BOT から待つトピック
// YOURNAME を自分の名前の英数字に変更します
char *subTopic = "top/subscribe";

// JSON 送信時に使う buffer
char pubJson[255];

// PubSubClient まわりの準備
WiFiClient httpClient;
PubSubClient mqttClient(httpClient);

void setup() {
  // init lcd, serial, but don't init sd card
  // LCD ディスプレイとシリアルは動かして、SDカードは動かさない設定
  M5.begin(true, false, true);

  // スタート
  M5.Lcd.fillScreen(BLACK);
  M5.Lcd.setCursor(0, 0);
  M5.Lcd.setTextColor(WHITE);
  M5.Lcd.setTextSize(2);

  // Arduino のシリアルモニタ・M5Stack LCDディスプレイ両方にメッセージを出す
  Serial.print("START");  // Arduino のシリアルモニタにメッセージを出す
  M5.Lcd.print("START");  // M5Stack LCDディスプレイにメッセージを出す(英語のみ)

  // WiFi 接続開始
  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED) {
      delay(500);

      // Arduino のシリアルモニタ・M5Stack LCDディスプレイ両方にメッセージを出す
      Serial.print(".");
      M5.Lcd.print(".");
  }

  // WiFi Connected
  // WiFi 接続完了
  M5.Lcd.setCursor(10, 40);
  M5.Lcd.setTextColor(WHITE);
  M5.Lcd.setTextSize(2);

  // Arduino のシリアルモニタ・M5Stack LCDディスプレイ両方にメッセージを出す
  // 前のメッセージが print で改行入っていないので println で一つ入れる
  Serial.println("");  // Arduino のシリアルモニタにメッセージを出し改行が最後に入る
  M5.Lcd.println("");  // M5Stack LCDディスプレイにメッセージを出す改行が最後に入る(英語のみ) 

  // Arduino のシリアルモニタ・M5Stack LCDディスプレイ両方にメッセージを出す
  Serial.println("WiFi Connected.");  // Arduino のシリアルモニタにメッセージを出す
  M5.Lcd.println("WiFi Connected.");  // M5Stack LCDディスプレイにメッセージを出す(英語のみ)

  // ちゃんとつながったと分かるために 2 秒待ってから MQTT の処理に行く
  delay(2000);

  // MQTT の接続先設定
  mqttClient.setServer(mqttEndpoint, mqttPort);
  // MQTT のデータを受け取った時(購読時)の動作を設定
  mqttClient.setCallback(mqttCallback);
  // MQTT の接続
  mqttConnect();
}



void mqttConnect() {

  M5.Lcd.fillScreen(BLACK);
  M5.Lcd.setCursor(0, 0);
  M5.Lcd.setTextColor(WHITE);
  M5.Lcd.setTextSize(2);
  
  // MQTT clientID のランダム化(名称重複対策)
  char clientID[40] = "clientID";
  String rndNum = String(random(0xffffff), HEX);
  String deviceIDRandStr = String(deviceID);
  deviceIDRandStr.concat("-");
  deviceIDRandStr.concat(rndNum);
  deviceIDRandStr.toCharArray(clientID, 40);
  M5.Lcd.println("[MQTT]");
  M5.Lcd.println("");
  M5.Lcd.printf("- clientID ");
  M5.Lcd.println("");
  M5.Lcd.println(clientID);

  // 接続されるまで待ちます
  while (!mqttClient.connected()) {
    if (mqttClient.connect(clientID,mqttUsername,mqttPassword)) {
      Serial.println("Connected.");
      M5.Lcd.println("");
      M5.Lcd.println("- MQTT Connected.");
      
      // subTopic 変数で指定されたトピックに向けてデータを送ります
      int qos = 0;
      mqttClient.subscribe(subTopic, qos);
      Serial.println("Subscribe start.");
      M5.Lcd.println("");
      M5.Lcd.println("- MQTT Subscribe start.");
      M5.Lcd.println(subTopic);

      // 初回データ送信 publish ///////////
      // データ送信のための JSON をつくる
      DynamicJsonDocument doc(1024);
      doc["message"] = "Connected";
      // pubJson という変数に JSON 文字列化されたものが入る
      serializeJson(doc, pubJson);
      // pubTopic 変数で指定されたトピックに向けてデータを送ります
      mqttClient.publish(pubTopic, pubJson);
    } else {
      // MQTT 接続エラーの場合はつながるまで 5 秒ごとに繰り返します
      Serial.print("Failed. Error state=");
      Serial.println(mqttClient.state());
      // Wait 5 seconds before retrying
      delay(5000);
    }
  }
}

// JSON を格納する StaticJsonDocument を準備
StaticJsonDocument<255> jsonData;

// MQTT のデータを受け取った時(購読時)の動作を設定
void mqttCallback (char* topic, byte* payload, unsigned int length) {
  
  // データ取得
  String str = "";
  Serial.print("Received. topic=");
  Serial.println(topic);
  for (int i = 0; i < length; i++) {
      Serial.print((char)payload[i]);
      str += (char)payload[i];
  }
  Serial.print("\n");

  // 来た文字列を JSON 化して扱いやすくする
  // 変換する対象は jsonData という変数
  DeserializationError error = deserializeJson(jsonData, str);

  // JSON パースのテスト
  if (error) {
    Serial.print(F("deserializeJson() failed: "));
    Serial.println(error.f_str());
    return;
  }

  // 以下 jsonData 内が JSON として呼び出せる
  M5.Lcd.fillScreen(BLACK);

  // データの取り出し
  // https://arduinojson.org/v6/example/parser/
  const char* message = jsonData["message"];

  // データの表示
  M5.Lcd.setCursor(0, 80);
  M5.Lcd.setTextSize(7);
  M5.Lcd.println(message);
  
}

// 常にチェックして切断されたら復帰できるようにする対応
void mqttLoop() {
  if (!mqttClient.connected()) {
      mqttConnect();
  }
  mqttClient.loop();
}
 
void loop() {

  M5.update();

  // 常にチェックして切断されたら復帰できるようにする対応
  mqttLoop();
}

このソースコードを Arduino IDE にコピーペーストします。

// Wi-FiのSSID
const char *ssid = "Wi-FiのSSID";
// Wi-Fiのパスワード
const char *password = "Wi-Fiのパスワード";

// 今回使いたい CloudMQTT のブローカーのアドレス
const char *mqttEndpoint = "driver.cloudmqtt.com";
// 今回使いたい CloudMQTT のポート
const int mqttPort = 1880;
// 今回使いたい CloudMQTT のユーザー名
const char *mqttUsername = "CloudMQTT のユーザー名";
// 今回使いたい CloudMQTT のパスワード
const char *mqttPassword = "CloudMQTT のパスワード";

このあたりで Wi-Fi の設定と、今回つなぐ CloudMQTT の設定を反映して M5Stack に書き込みます。

動かしてみる

image

あとは、冒頭のツイートのように Raspberry Pi につながった nanoKEY2 で弾いてみて、M5Stack のディスプレイでキー番号が反応するようになります!

やっぱり MIDI デバイスも MIDI 規格いいですね反応もいいしリアルタイムで伝える MQTT とも相性が良いです。ボタンもたくさんついているので、これを M5Stack 経由でテープ LED やサーボなどにリアルタイム気味にデータを送って連動させると、面白い仕掛けが作れそうです!

しかも、耐久性が高いので、長時間デモをするような展示のような場でも、安心感が増しそうに思えます!