今回はMinecraftの画面でNode-REDを実行しているRaspberry PiのCPU温度をリアルタイム表示し、それをグラフ化させてみたので紹介します。
Contents
開発環境
PC:Windows 11
※Raspberry Pi 4 Model B (Linux)
開発ツール:Node.js (v18.19.0)
Node-RED (v.3.1.3)
インターネット:社内の無線LAN
Minecraft:v1.20.51 (執筆時点)
※Node-REDをRaspberry Piで動かし、Raspberry PiのCPU温度を表示させています。
Ambientについて
Ambientとは
Ambientは、IoTデータ可視化サービスで、マイコンなどから送られるセンサデータを受信し、蓄積し、可視化(グラフ化)することができます。ユーザ登録をしてとりあえず触ってみて簡単に使うことのできるサービスです。
Ambientに登録する
Ambient.ioを開き、新規登録を押します。登録するメールアドレスとパスワードを入力し、届いたメールから認証して完了です。
利用するための設定
登録が完了すると、このような画面が開くと思います。

「チャネルを作る」を押下すると、自動でキーが発行されます。
ここで発行されるIDやキーは、他人に知られてしまうと他人も簡単にアクセスできてしまうので、慎重に扱いましょう。
次に、タブから「ボード一覧」を開き、作られたボードをクリックします。
画面上部にある下のようなアイコンをクリックするとチャートが現れます。

チャート設定を開き、下のように表示するデータのプルボタンを設定すると、

このようにチャートが登場します。

つまり、この d1やd2 にデータを送信することで、グラフが作成されていくことになります。
事前準備
上記で紹介したAmbientを準備しておく
先ほど発行した「チャネルID」と「ライトキー」を次に紹介するノードで使用します。
あらかじめ控えておきましょう。
Ambientノードを追加する
パレットの管理から「node-red-contrib-ambient」を追加しておきます。
ノードの編集画面で、用意した「チャネルID」と「ライトキー」を、
指定の場所にペーストしておきます。

Minecraftにスコアボードを設定しておく
今回はMinecraftの機能の一つであるスコアボードを利用してCPU温度を画面に表示します。下記コマンドを順にMinecraftのチャットに打ち込みます。
/scoreboard objectives add test dummy
/scoreboard objectives setdisplay sidebar test
フローの概要

今回、定期的にプログラムを実行してCPU温度を表示させたいので、
injectノードを繰り返し実行に設定しています。
CPU温度のゲーム内表示
execノードでCPU温度取得
execノードを利用して外部コマンドを実行できます。今回はCPU温度の出力ができるLinuxの下記ターミナルコマンドを実行しています。
vcgencmd measure_temp
Minecraftへの送信
今回はスコアボードへ表示させますが、本来主には数値を使うことの多いコマンドです。
しかし今回はスコアボード内での表示名を利用してCPU温度を可視化しています。
したがってCPU温度が変わる度、別名称として登録されてしまうのでリセットが必要です。
今回はテストしやすい3秒間隔でプログラムを実行しています。
delayノードは繰り返しの実行がされる0.5秒前に削除用のコマンドを送信できるよう挿入しています。
functionの中身はこんな感じです。
const uuid = msg.payout;
const output = msg.payload;
const commandRequestMessageJSON = {
"header": {
"version": 1, // プロトコルのバージョン1.18.2時点では1でOK
"requestId": uuid, // UUIDv4を生成して指定
"messageType": "commandRequest", // commandRequestを指定
"messagePurpose": "commandRequest", // commandRequestを指定
},
"body": {
"origin": {
"type": "player" // 誰がコマンドを実行するかを指定(ただし、Player以外にどの値が利用可能かは要調査)
},
"version": 1, // プロトコルのバージョン1.18.2時点では1でOK
"commandLine": `scoreboard players set Temp:${output.substr(5,4)}°C test 0`, // マイクラで実行したいコマンドを指定(ここではニワトリをスポーンさせるコマンドを指定)
}
};
msg.payload = JSON.stringify(commandRequestMessageJSON);
return msg;
削除用のコマンドラインは、setをresetに変更しただけのものです。
Ambientへの送信
Ambientノードのヘルプを参照してみる
ヘルプを見てみると、Ambientノードが受信できる文字列の形が記載されています。
記載の通り、payloadに格納されたデータを{“d1″:データ1,”d2”:データ2}のように書き換える必要があります。
payloadの編集
ヘルプで参照した通り、payloadの形を送信用の文字列に変換します。
execノードから得た温度データの文字列には、改行も含まれているので、javascriptの正規表現を用いて脱しています。
const data = msg.payload;
const dataa = data.replace("temp=","");
const dataaa = dataa.replace("'C","");
const findata = dataaa.replace(/\r?\n/g, "");
msg.payload = '{"d1": '+findata+'}';
return msg;
動作させてみる


ノードデータ
[ { "id": "e31a9344983d4ffb", "type": "websocket out", "z": "458a61ef3f0be4ba", "name": "", "server": "c838b41f45982027", "client": "", "x": 980, "y": 160, "wires": [] }, { "id": "c9f21a66d722bb1a", "type": "exec", "z": "458a61ef3f0be4ba", "command": "vcgencmd measure_temp", "addpay": "", "append": "", "useSpawn": "true", "timer": "", "winHide": false, "oldrc": false, "name": "view temp", "x": 320, "y": 140, "wires": [ [ "44a316b0cfd35c61", "eae2a2251644a4fc", "a31259ecbc726054", "27109d1b3fabf25c" ], [], [] ] }, { "id": "2574015b818a0c87", "type": "inject", "z": "458a61ef3f0be4ba", "name": "", "props": [ { "p": "payload" }, { "p": "topic", "vt": "str" } ], "repeat": "3", "crontab": "", "once": false, "onceDelay": 0.1, "topic": "", "payload": "", "payloadType": "date", "x": 150, "y": 140, "wires": [ [ "c9f21a66d722bb1a" ] ] }, { "id": "cac865acbbf690d4", "type": "function", "z": "458a61ef3f0be4ba", "name": "function 12", "func": "const uuid = msg.payout;\nconst output = msg.payload;\n\nconst commandRequestMessageJSON = {\n \"header\": {\n \"version\": 1, // プロトコルのバージョン1.18.2時点では1でOK\n \"requestId\": uuid, // UUIDv4を生成して指定\n \"messageType\": \"commandRequest\", // commandRequestを指定\n \"messagePurpose\": \"commandRequest\", // commandRequestを指定\n },\n \"body\": {\n \"origin\": {\n \"type\": \"player\" // 誰がコマンドを実行するかを指定(ただし、Player以外にどの値が利用可能かは要調査)\n },\n \"version\": 1, // プロトコルのバージョン1.18.2時点では1でOK\n \"commandLine\": `scoreboard players set Temp:${output.substr(5,4)}°C test 0`, // マイクラで実行したいコマンドを指定(ここではニワトリをスポーンさせるコマンドを指定)\n }\n};\n\nmsg.payload = JSON.stringify(commandRequestMessageJSON);\n\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 810, "y": 140, "wires": [ [ "e31a9344983d4ffb" ] ] }, { "id": "26976f8ef7c30cb0", "type": "function", "z": "458a61ef3f0be4ba", "name": "function 13", "func": "const uuid = msg.payout;\nconst oldtmp = msg.payload;\n\nconst commandRequestMessageJSON = {\n \"header\": {\n \"version\": 1, // プロトコルのバージョン1.18.2時点では1でOK\n \"requestId\": uuid, // UUIDv4を生成して指定\n \"messageType\": \"commandRequest\", // commandRequestを指定\n \"messagePurpose\": \"commandRequest\", // commandRequestを指定\n },\n \"body\": {\n \"origin\": {\n \"type\": \"player\" // 誰がコマンドを実行するかを指定(ただし、Player以外にどの値が利用可能かは要調査)\n },\n \"version\": 1, // プロトコルのバージョン1.18.2時点では1でOK\n \"commandLine\": `scoreboard players reset Temp:${oldtmp.substr(5,4)}°C test`, // マイクラで実行したいコマンドを指定(ここではニワトリをスポーンさせるコマンドを指定)\n }\n};\n\nmsg.payload = JSON.stringify(commandRequestMessageJSON);\n\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 810, "y": 200, "wires": [ [ "e31a9344983d4ffb" ] ] }, { "id": "eae2a2251644a4fc", "type": "delay", "z": "458a61ef3f0be4ba", "name": "", "pauseType": "delay", "timeout": "2.5", "timeoutUnits": "seconds", "rate": "1", "nbRateUnits": "1", "rateUnits": "second", "randomFirst": "1", "randomLast": "5", "randomUnits": "seconds", "drop": false, "allowrate": false, "outputs": 1, "x": 490, "y": 200, "wires": [ [ "b311cc5a69222188" ] ] }, { "id": "a31259ecbc726054", "type": "function", "z": "458a61ef3f0be4ba", "name": "payloadの編集", "func": "const data = msg.payload;\n\nconst dataa = data.replace(\"temp=\",\"\");\nconst dataaa = dataa.replace(\"'C\",\"\");\nconst findata = dataaa.replace(/\\r?\\n/g, \"\");\n\nmsg.payload = '{\"d1\": '+findata+'}';\n\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 220, "y": 280, "wires": [ [ "2f562285125439fa" ] ] }, { "id": "2f562285125439fa", "type": "json", "z": "458a61ef3f0be4ba", "name": "", "property": "payload", "action": "", "pretty": false, "x": 370, "y": 280, "wires": [ [ "92390ea9542f7d98", "f05dcbaeb0b56327" ] ] }, { "id": "92390ea9542f7d98", "type": "debug", "z": "458a61ef3f0be4ba", "name": "debug 21", "active": true, "tosidebar": true, "console": false, "tostatus": false, "complete": "false", "statusVal": "", "statusType": "auto", "x": 540, "y": 340, "wires": [] }, { "id": "44a316b0cfd35c61", "type": "uuid", "z": "458a61ef3f0be4ba", "uuidVersion": "v4", "namespaceType": "", "namespace": "", "namespaceCustom": "", "name": "", "field": "payout", "fieldType": "msg", "x": 650, "y": 140, "wires": [ [ "cac865acbbf690d4" ] ] }, { "id": "b311cc5a69222188", "type": "uuid", "z": "458a61ef3f0be4ba", "uuidVersion": "v4", "namespaceType": "", "namespace": "", "namespaceCustom": "", "name": "", "field": "payout", "fieldType": "msg", "x": 650, "y": 200, "wires": [ [ "26976f8ef7c30cb0" ] ] }, { "id": "27109d1b3fabf25c", "type": "debug", "z": "458a61ef3f0be4ba", "name": "debug 22", "active": true, "tosidebar": true, "console": false, "tostatus": false, "complete": "false", "statusVal": "", "statusType": "auto", "x": 500, "y": 80, "wires": [] }, { "id": "f05dcbaeb0b56327", "type": "Ambient", "z": "458a61ef3f0be4ba", "name": "", "channelId": "", "writeKey": "", "x": 540, "y": 280, "wires": [] }, { "id": "c838b41f45982027", "type": "websocket-listener", "path": "ws", "wholemsg": "false" } ]