Intern

Minecraft統合版(BE)にRaspberry PiのCPU温度を表示させ、Ambientにデータを蓄積してみた

今回はMinecraftの画面でNode-REDを実行しているRaspberry PiのCPU温度をリアルタイム表示し、それをグラフ化させてみたので紹介します。

開発環境

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" } ]