Raspberry Pi 4 の Node-RED から KORG nanoKEY2 キーボードの MIDI データを取得し M5Stack と MQTT 経由で連携するメモです。
この記事は 東京テクニカルカレッジ Advent Calendar 2022 の 15 日目の記事です。
MIDI 機器は楽器でつよい
MIDI デバイスは、キーボードやミキサーといった楽器です。楽器ですので、演奏するわけですから繰り返し押されても使い続けられる耐久性がありますし、なにより遅れがなく音を出すためのレスポンスの良さがあります。このあたり、自作の IoT デバイスで作ったスイッチで実現する強度ではたどり着けない強さがあります。
MIDI の規格からみても演奏情報を遅延なく送るための長年培った知見が詰まっているので安心感があります。こちらも、同様のレスポンスの良さを自分で実現するのは厳しいです。 MIDI すごい。
というわけで、これを IoT と連携してみるわけです。
このように動きます
#RaspberryPi の Node-RED から #KORG nanoKEY2 キーボードの #MIDI データを取得し #MQTT 経由で #M5Stack と連携できましたー。 #nodered #nodered pic.twitter.com/ArOEzMHZXB
— Tanaka Seigo (@1ft_seabass) December 13, 2022
このように 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 につなぐ
このように、mini USB – USB A のケーブルで Raspberry Pi 4 とつなぎます。
Node-RED のフロー
Raspberry Pi の Node-RED のフローはこのようになっています。
node-red-contrib-midi ノードの一つである midi in ノードは USB で nanoKEY2 をつなぐと以下のように認識します。
nanoKEY2 を選択することで、すんなり認識します。
これで nanoKEY2 を弾いてあげるとこのようなデータが来ます。1 番目のデータが押された音階としてのキー番号です。2 番目は、押された強さがきていて、指を離したときに 64 の固定値が来るようです。
今回は switch ノードで 2 番目の値で 64 を除外することで、押されたときのみ反応するようにしました。
このような設定ですね。
あとはこの change ノードで 1 番目の値を MQTT に加工して送るようにしています。
このように設定していて、
message 値に、数字を文字列に変換した音階としてのキー番号を送って MQTT につながった M5Stack にデータを送るようにしています。
MQTT ノードは今回は CloudMQTT で建てた MQTT ブローカを使っています。
このようにつないでいます。
この設定を行ってデプロイしてます。
このフローの 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 のソースコード
さて 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 に書き込みます。
動かしてみる
あとは、冒頭のツイートのように Raspberry Pi につながった nanoKEY2 で弾いてみて、M5Stack のディスプレイでキー番号が反応するようになります!
やっぱり MIDI デバイスも MIDI 規格いいですね反応もいいしリアルタイムで伝える MQTT とも相性が良いです。ボタンもたくさんついているので、これを M5Stack 経由でテープ LED やサーボなどにリアルタイム気味にデータを送って連動させると、面白い仕掛けが作れそうです!
しかも、耐久性が高いので、長時間デモをするような展示のような場でも、安心感が増しそうに思えます!