この記事は IoTLT Advent Calendar 2020 4日目の記事です。
Node-RED から obniz ノードをつかってクリスマスソング&キラキラ&顔文字ダンスさせてみます。
こんな風に動きます
やった!Node-RED で #obniz ノードから直接ジングルベルのメロディ流せたぞー!1音符ごと配列に手打ちして頑張った!Node-RED で完結できる obniz 素敵すぎる。#nodered #noderedjp #iotlt pic.twitter.com/hGH0V92S0b
— Tanaka Seigo (@1ft_seabass) December 3, 2020
obniz ノードのバージョンアップ!
obniz ノードは昔からあったのですが、最近バージョンアップしましてとても使いやすくなっています。これがすごい。
obniz に関わっている @wicket 木戸さんからもバージョンアップをまとめてくれています。
Node-REDのobnizノードがバージョンアップした! - Qiita
- obnizとの通信用のjsonデータを書かなくて良くなった!楽!
- obnizにアウトプットしかできなかった(ディスプレイに文字出すとかLEDつけるとか)のが、インプットもできるようになった!
- JavaScriptSDKでできることが全部そのままできるようになった!
- 互換性なくなった!
とのことで obniz クラウドや Node.js obniz ライブラリで書いていた JavaScript のコードがそのまま移植できるのが魅力です。
今まで、Node-REDとobnizを連携するときは、obniz クラウドでobnizを立ち上げてMQTTやWebSocket経由でNode-REDからデータを受け取っているところが、obnizノードでNode-REDがシンプルに連携できるんです!
obniz に圧電スピーカーとマイコン内蔵RGBLEDを差し込む
obnizの パーツライブラリ で圧電スピーカーとRGBフルカラーLEDを調べて入手しました。
- 圧電スピーカー(圧電サウンダ)(13mm)PKM13EPYH4000-A0: パーツ一般 秋月電子通商-電子部品・ネット通販
- マイコン内蔵RGB 8mmLED PL9823-F8: LED(発光ダイオード) 秋月電子通商-電子部品・ネット通販
以下のようにつなぎました。
- 圧電スピーカー(+-は関係なし)
- 0 番ピン
- 3 番ピン
- マイコン内蔵 RGB LED
- 9 番ピン(一番長いピン)
- 10 番ピン(次に長いピン)
- 11 番ピン(さらに次に長いピン)
マイコン内蔵 RGB LED は、上記の説明だけだと分かりにくいので画像説明も置いておきます。
写真内の番号に合わせて、 9 ~ 11 番ピンを挿します。
メロディを参考にする
ジングルベルってどんなメロディだっけ?しらべたら こちらのサイト が分かりやすかったです。
ミミミミミミミソドレミ ファファファファファミミミミレレドレソ ミミミミミミミソドレミ ファファファファファミミミソソファレド
こちらを配列に変えて1音1音使えるようにします。
{"melody": ["ミ","ミ","ミ","ミ","ミ","ミ","ミ","ミ","ミ","ソ","ド","レ","ミ","ファ","ファ","ファ","ファ","ファ","ミ","ミ","ミ","ミ","レ","レ","ド","レ","ソ","ミ","ミ","ミ","ミ","ミ","ミ","ミ","ミ","ミ","ソ","ド","レ","ミ","ファ","ファ","ファ","ファ","ファ","ミ","ミ","ミ","ソ","ソ","ファ","レ","ド"]}
今回は長音(のばす音)の仕様は入れていないので、のばす部分を連音を多めにするなど調整をしています。
音階の周波数を参考にする
上記の配列を1文字ずつ消費して、その文字に合わせてスピーカーで流す周波数を導き出すようにします。以前、便利に使わせていただいたサイトがYahooジオシティーズの閉鎖に伴って無くなっていたので、以下のサイトを参考にさせていただきました。
いい感じの音階グループを
ド 523.23
レ 587.34
ミ 659.25
ファ 698.45
ソ 783.98
ラ 879.99
シ 987.75
ド 1046.5
このように抽出して
{
"ド":523,
"レ":587,
"ミ":659,
"ファ":698,
"ソ":783,
"ラ":879,
"シ":987,
"ド":1046
}
という形で使えるようにします。
Node-RED のフロー
自分は Node-RED を手元のPCのローカルで立ち上げています。
今回動かしたNode-RED のフローはこのようになっております。
[{"id":"7abda08.31dfe6","type":"change","z":"87a7ba2e.1c34b8","name":"音階準備","rules":[{"t":"set","p":"melodyHz","pt":"msg","to":"{\"ド\":1046,\"レ\":587,\"ミ\":659,\"ファ\":698,\"ソ\":783,\"ラ\":879,\"シ\":987}","tot":"json"}],"action":"","property":"","from":"","to":"","reg":false,"x":820,"y":620,"wires":[["54aa628.e33749c"]]},{"id":"e4c3cedc.cfaf2","type":"inject","z":"87a7ba2e.1c34b8","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":420,"y":620,"wires":[["12cbf12c.f7af3f"]]},{"id":"54aa628.e33749c","type":"change","z":"87a7ba2e.1c34b8","name":"メロディ準備","rules":[{"t":"set","p":"melodyLine","pt":"msg","to":"{\"melody\":[\"ミ\",\"ミ\",\"ミ\",\"ミ\",\"ミ\",\"ミ\",\"ミ\",\"ミ\",\"ミ\",\"ソ\",\"ド\",\"レ\",\"ミ\",\"ファ\",\"ファ\",\"ファ\",\"ファ\",\"ファ\",\"ミ\",\"ミ\",\"ミ\",\"ミ\",\"レ\",\"レ\",\"ド\",\"レ\",\"ソ\",\"ミ\",\"ミ\",\"ミ\",\"ミ\",\"ミ\",\"ミ\",\"ミ\",\"ミ\",\"ミ\",\"ソ\",\"ド\",\"レ\",\"ミ\",\"ファ\",\"ファ\",\"ファ\",\"ファ\",\"ファ\",\"ミ\",\"ミ\",\"ミ\",\"ソ\",\"ソ\",\"ファ\",\"レ\",\"ド\"]}","tot":"json"}],"action":"","property":"","from":"","to":"","reg":false,"x":980,"y":620,"wires":[["d3af031d.364a2"]]},{"id":"55df16b.92748e8","type":"delay","z":"87a7ba2e.1c34b8","name":"","pauseType":"delay","timeout":"100","timeoutUnits":"milliseconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":430,"y":900,"wires":[["d3af031d.364a2"]]},{"id":"12cbf12c.f7af3f","type":"change","z":"87a7ba2e.1c34b8","name":"melodycount 初期化","rules":[{"t":"set","p":"payload","pt":"msg","to":"{\"melodycount\":0}","tot":"json"}],"action":"","property":"","from":"","to":"","reg":false,"x":640,"y":620,"wires":[["7abda08.31dfe6"]]},{"id":"d3af031d.364a2","type":"obniz-function","z":"87a7ba2e.1c34b8","obniz":"e1f5c3f5.3c7f5","name":"メロディ鳴らす&踊る&光る","code":"let melodycount = msg.payload.melodycount;\n\n// メロディが鳴る\nlet currentMelodyName = msg.melodyLine.melody[msg.payload.melodycount];\nlet currentMelodyHz = msg.melodyHz[currentMelodyName];\nobnizParts.speaker.play(currentMelodyHz);\n\n// ランダムで光る\nlet r = Math.floor(Math.random() * 10) * 25;\nlet g = Math.floor(Math.random() * 10) * 25;\nlet b = Math.floor(Math.random() * 10) * 25;\n\nobnizParts.led.rgb(r, g, b);\n\n// テキストで踊る\nobniz.display.clear();\nlet motion = melodycount % 4;\nif(motion == 1){\n obniz.display.print(\" \\\\(o_o)\\\\\\n\");\n obniz.display.print(\" \\\\(^_^)\\\\\\n\");\n obniz.display.print(\" \\\\(^_^)\\\\\\n\");\n} else if(motion == 2){\n obniz.display.print(\" /(^_^)/\\n\");\n obniz.display.print(\" /(o_o)/\\n\");\n obniz.display.print(\" /(^_^)/\\n\");\n} else if(motion == 3){\n obniz.display.print(\" \\\\(^_^)\\\\\\n\");\n obniz.display.print(\" \\\\(^_^)\\\\\\n\");\n obniz.display.print(\" \\\\(o_o)\\\\\\n\");\n} else {\n obniz.display.print(\" /(^_^)/\\n\");\n obniz.display.print(\" /(^_^)/\\n\");\n obniz.display.print(\" /(^_^)/\\n\");\n}\nobniz.display.print(\">> Hz:\" + currentMelodyHz);\n\nreturn msg;","x":460,"y":720,"wires":[["a2b3386f.a6d328"]]},{"id":"b382e09.cf33a2","type":"change","z":"87a7ba2e.1c34b8","name":"melodycount 加算(次へ)","rules":[{"t":"set","p":"payload.melodycount","pt":"msg","to":"payload.melodycount + 1","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":760,"y":820,"wires":[["83b4b3d8.ab7e7"]]},{"id":"83b4b3d8.ab7e7","type":"switch","z":"87a7ba2e.1c34b8","name":"終了判定","property":"payload.melodycount","propertyType":"msg","rules":[{"t":"eq","v":"melodyLine.melody.length","vt":"msg"},{"t":"else"}],"checkall":"true","repair":false,"outputs":2,"x":1000,"y":820,"wires":[["ac46e0fd.b3006"],["55df16b.92748e8","6ef23572.f14f0c"]]},{"id":"6ef23572.f14f0c","type":"debug","z":"87a7ba2e.1c34b8","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1210,"y":840,"wires":[]},{"id":"814155e7.77faa8","type":"obniz-function","z":"87a7ba2e.1c34b8","obniz":"e1f5c3f5.3c7f5","name":"Finish!","code":"obnizParts.speaker.play(0);\nobnizParts.led.rgb(0, 0, 0);\n\nobniz.display.clear();\nobniz.display.print(\" \\\\(o_-)/\\n\");\nobniz.display.print(\" \\\\(-_o)/\\n\");\nobniz.display.print(\" \\\\(o_-)/\\n\");\nobniz.display.print(\"IoTLT Christmas!\");","x":1330,"y":720,"wires":[[]]},{"id":"ac46e0fd.b3006","type":"delay","z":"87a7ba2e.1c34b8","name":"","pauseType":"delay","timeout":"300","timeoutUnits":"milliseconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":1170,"y":720,"wires":[["814155e7.77faa8"]]},{"id":"a2b3386f.a6d328","type":"delay","z":"87a7ba2e.1c34b8","name":"","pauseType":"delay","timeout":"200","timeoutUnits":"milliseconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":670,"y":720,"wires":[["f8c066a4.238fd8"]]},{"id":"f8c066a4.238fd8","type":"obniz-function","z":"87a7ba2e.1c34b8","obniz":"e1f5c3f5.3c7f5","name":"音を一度切る","code":"obnizParts.speaker.stop();\nobnizParts.led.rgb(0, 0, 0);\n\nreturn msg;","x":840,"y":720,"wires":[["b382e09.cf33a2"]]},{"id":"782b6ae2.9d4a24","type":"inject","z":"87a7ba2e.1c34b8","name":"起動時","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":"1","topic":"","payload":"","payloadType":"date","x":400,"y":560,"wires":[["c704cf1f.e04c7"]]},{"id":"c704cf1f.e04c7","type":"obniz-function","z":"87a7ba2e.1c34b8","obniz":"e1f5c3f5.3c7f5","name":"obniz 準備","code":"// スピーカーとLEDの呼び出し\n// obnizPartsに格納して他の obniz ノード使えるようにする\nobnizParts.speaker = obniz.wired(\"Speaker\", {signal:0, gnd:3});\nobnizParts.led = obniz.wired(\"WS2811\", {gnd:9, vcc: 10, din: 11});\n// スピーカーとLEDの初期化\nobnizParts.speaker.play(0);\nobnizParts.led.rgb(255, 0, 0);\n// ディスプレイに表示\nobniz.display.clear();\nobniz.display.print(\"obniz + Node-RED\");\nobniz.display.print(\"IoTLT Christmas 2020!\");\n\nreturn msg;","x":610,"y":560,"wires":[[]]},{"id":"e1f5c3f5.3c7f5","type":"obniz","z":"","obnizId":"0000-0000","deviceType":"obnizboard","name":"","accessToken":"","code":""}]
すぐにインポートして使えるフロー JSON データはこちらです。
obniz IDは 0000-0000
にしてあります。
詳細の設定で、自分の obniz にあわせて obniz ID と device type を合わせましょう。
フロー内のハイライト説明
起動時
ざっくりいうと、起動時
と書いてある inject ノードは起動後1秒後にパーツの呼び出しを行って準備をします。
// スピーカーとLEDの呼び出し
// obnizPartsに格納して他の obniz ノード使えるようにする
obnizParts.speaker = obniz.wired("Speaker", {signal:0, gnd:3});
obnizParts.led = obniz.wired("WS2811", {gnd:9, vcc: 10, din: 11});
// スピーカーとLEDの初期化
obnizParts.speaker.play(0);
obnizParts.led.rgb(255, 0, 0);
// ディスプレイに表示
obniz.display.clear();
obniz.display.print("obniz + Node-RED");
obniz.display.print("IoTLT Christmas 2020!");
return msg;
obniz ノードの中身はこのようになっています。スピーカーとLEDの呼び出し・初期化、ディスプレイ表示、obnizPartsに格納して他の obniz ノード使えるようにする、といった処理を行っています。
メロディを流す部分
タイムスタンプ
と書かれている inject ノードではじめます。上記で触れた、スピーカをドレミで鳴らす周波数や、ジングルベルのメロディ配列を初期設定。melodycount
を1つずつ増やしてジングルベルのメロディ配列を1つ1つ追っています。
実際に、メロディ鳴らす・踊る・光るの処理をしているノードはこちらの obniz ノードです。
中身は以下のようになっています。
let melodycount = msg.payload.melodycount;
// メロディが鳴る
let currentMelodyName = msg.melodyLine.melody[msg.payload.melodycount];
let currentMelodyHz = msg.melodyHz[currentMelodyName];
obnizParts.speaker.play(currentMelodyHz);
// ランダムで光る
let r = Math.floor(Math.random() * 10) * 25;
let g = Math.floor(Math.random() * 10) * 25;
let b = Math.floor(Math.random() * 10) * 25;
obnizParts.led.rgb(r, g, b);
// テキストで踊る
obniz.display.clear();
let motion = melodycount % 4;
if(motion == 1){
obniz.display.print(" \\(o_o)\\\n");
obniz.display.print(" \\(^_^)\\\n");
obniz.display.print(" \\(^_^)\\\n");
} else if(motion == 2){
obniz.display.print(" /(^_^)/\n");
obniz.display.print(" /(o_o)/\n");
obniz.display.print(" /(^_^)/\n");
} else if(motion == 3){
obniz.display.print(" \\(^_^)\\\n");
obniz.display.print(" \\(^_^)\\\n");
obniz.display.print(" \\(o_o)\\\n");
} else {
obniz.display.print(" /(^_^)/\n");
obniz.display.print(" /(^_^)/\n");
obniz.display.print(" /(^_^)/\n");
}
obniz.display.print(">> Hz:" + currentMelodyHz);
return msg;
ジングルベルのメロディ配列を1つ1つ追ってメロディが鳴り、LEDがタイミングに合わせてランダムで光り、顔文字テキストで踊るというメインの処理を行っています。LEDのランダム値は Math.floor(Math.random() * 10) * 25
にすることとで 25 段階で目にわかりやすい階調にしたり、顔文字ではテキストで地味に \
がエスケープしなければならず \\
にしています。
ジングルベルのメロディ配列を1つ1つ追って配列が終わるまで繰り返します。音を鳴らす、delayで少し待って、音を無くすを繰り返しています。音を無くさないと、同じ音が続く場合に変化が分からないんですよね。
締めのアニメーション
ジングルベルのメロディ配列を消費しきったら終了処理に向かいます。
中身は以下のようになっています。
obnizParts.speaker.play(0);
obnizParts.led.rgb(0, 0, 0);
obniz.display.clear();
obniz.display.print(" \\(o_-)/\n");
obniz.display.print(" \\(-_o)/\n");
obniz.display.print(" \\(o_-)/\n");
obniz.display.print("IoTLT Christmas!");
顔文字だけでも結構表現できますね!
3人そろって、ジャジャン!
明日は
明日の IoTLT Advent Calendar 2020 の担当は @yukima77 さんです!
楽しみです!