Sakura VPSでnginxを使いIoTダッシュボードを作成する方法 その2

Node-REDのインストール

Node-REDのインストールを行います。

bash <(curl -sL https://raw.githubusercontent.com/node-red/linux-installers/master/deb/update-nodejs-and-nodered)

Sakura VPSのパケットフィルターで1880をTCPで開けます。

そのままだとUFWでひっかかるので、

$ sudo ufw allow 1880

これで、1880もあけます。

無事にNode-REDがインストールされました。

Node-REDとダッシュボードの連携

WebSocketを使ってNode-REDからデータを送り、ダッシュボードに表示させます。

まずは、温度のデータをNode-REDから受信します。

index.html側

下記のようなコードにします。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>リアルタイムセンサーデータ表示</title>
    <!-- BootstrapのCSSをCDNから読み込み -->
    <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet">
    <!-- Chart.jsのCDNを読み込み -->
    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
    <style>
        body {
            background-color: #000;
            color: #fff;
        }
        .card {
            background-color: #222;
            border: none;
        }
        .card-header {
            background-color: #333;
        }
        .chart-container {
            width: 80%;
            margin: 20px auto;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1 class="text-center my-4">設備情報</h1>
        <div class="row">
            <div class="col-md-3 mb-3">
                <div class="card text-center">
                    <div class="card-header">発電電力</div>
                    <div class="card-body">
                        <p>本日の合計: <span id="daily-power">24.3</span> kWh</p>
                        <p>今月の合計: <span id="monthly-power">153.5</span> kWh</p>
                    </div>
                </div>
            </div>
            <div class="col-md-3 mb-3">
                <div class="card text-center">
                    <div class="card-header">CO<sub>2</sub>削減量</div>
                    <div class="card-body">
                        <p>本日の合計: <span id="daily-co2">96</span> kg-CO<sub>2</sub></p>
                        <p>今月の合計: <span id="monthly-co2">2466</span> kg-CO<sub>2</sub></p>
                    </div>
                </div>
            </div>
            <div class="col-md-3 mb-3">
                <div class="card text-center">
                    <div class="card-header">温度</div>
                    <div class="card-body">
                        <p><span id="temperature">25</span> °C</p>
                    </div>
                </div>
            </div>
            <div class="col-md-3 mb-3">
                <div class="card text-center">
                    <div class="card-header">湿度</div>
                    <div class="card-body">
                        <p><span id="humidity">31</span> %</p>
                    </div>
                </div>
            </div>
            <div class="col-md-3 mb-3">
                <div class="card text-center">
                    <div class="card-header">日射強度</div>
                    <div class="card-body">
                        <p><span id="solar-intensity">0.86</span> kW/m<sup>2</sup></p>
                    </div>
                </div>
            </div>
        </div>

        <div class="chart-container">
            <canvas id="powerChart"></canvas>
        </div>
    </div>

    <!-- jQueryとBootstrapのJSをCDNから読み込み -->
    <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@4.5.2/dist/js/bootstrap.bundle.min.js"></script>
    
    <script>
        // WebSocket接続を作成
        const ws = new WebSocket('ws://NODERED-SERVER-IP:1880/ws/temperature');
      
        // メッセージ送信関数(非同期)
        function sendMessage(ws, message) {
            return new Promise((resolve, reject) => {
                ws.onopen = () => {
                    ws.send(message);
                    resolve("メッセージが送信されました: " + message);
                };
                ws.onerror = (error) => {
                    reject("WebSocketエラー: " + error);
                };
            });
        }

        // WebSocketでメッセージを受信した時の処理
        ws.onmessage = function(event) {
            const data = JSON.parse(event.data);
            document.getElementById('temperature').innerText = data.temperature;
            console.log("受信したデータ: ", data);
        };

        // 接続が開かれたときの処理
        ws.onopen = function() {
            console.log("WebSocket接続が開かれました");

            // サーバーへのメッセージ送信
            sendMessage(ws, JSON.stringify({ temperature: 25 }))
                .then(response => console.log(response))
                .catch(error => console.error(error));
        };

        // 接続が閉じられたときの処理
        ws.onclose = function() {
            console.log("WebSocket接続が閉じられました");
        };

        // データを更新する関数
        function updateSensorData() {
            // 他のデータはランダムに生成(例示のため)
            document.getElementById('daily-power').innerText = (Math.random() * 30).toFixed(1);
            document.getElementById('monthly-power').innerText = (Math.random() * 200).toFixed(1);
            document.getElementById('daily-co2').innerText = Math.floor(Math.random() * 100);
            document.getElementById('monthly-co2').innerText = Math.floor(Math.random() * 3000);
            document.getElementById('humidity').innerText = Math.floor(20 + Math.random() * 60);
            document.getElementById('solar-intensity').innerText = (Math.random() * 1).toFixed(2);
        }

        // 5秒ごとにデータを更新
        setInterval(updateSensorData, 5000);

        // グラフの設定
        const ctx = document.getElementById('powerChart').getContext('2d');
        const powerChart = new Chart(ctx, {
            type: 'bar',
            data: {
                labels: ['0時', '3時', '6時', '9時', '12時', '15時', '18時', '21時'],
                datasets: [{
                    label: '本日の発電量',
                    data: [0, 0, 5, 15, 25, 30, 20, 5],
                    backgroundColor: 'rgba(255, 165, 0, 0.7)',
                    borderColor: 'rgba(255, 165, 0, 1)',
                    borderWidth: 1
                }]
            },
            options: {
                scales: {
                    y: {
                        beginAtZero: true,
                        max: 30
                    }
                }
            }
        });
    </script>
</body>
</html>

WebSocketの接続にはNode-REDサーバーのIPアドレスを使用します。

Node-RED側

[{"id":"a1f7d9c0.7b45d8","type":"inject","z":"1c01e343d026d694","name":"5秒ごとに実行","props":[],"repeat":"5","crontab":"","once":true,"onceDelay":0.1,"topic":"","x":240,"y":240,"wires":[["f983bc45.957b"]]},{"id":"f983bc45.957b","type":"function","z":"1c01e343d026d694","name":"温度データ生成","func":"msg.payload = {\n    temperature: (20 + Math.random() * 10).toFixed(1)\n};\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":460,"y":300,"wires":[["1e6d1296.f233be"]]},{"id":"1e6d1296.f233be","type":"websocket out","z":"1c01e343d026d694","name":"","server":"d44f4b57.6b7198","client":"","x":660,"y":380,"wires":[]},{"id":"d44f4b57.6b7198","type":"websocket-listener","path":"/ws/temperature","wholemsg":"false"}]

このようなフローを作成し、functionノードで作成した温度データをWebSocketOutノードで取得できるようにします。

これで、index.html側で受け取れるようになるのですが、Node-RED側を修正すると

WebSocketが切断されてしまいます。

一度閉じられたWebSocket通信を復活させる方法

一度閉じられたWebSocket通信を再接続させるためには、WebSocketのoncloseonerrorイベントを使用して、接続が切れた際に再接続を試みるロジックを実装する必要があります。

下記のコードをindex.htmlに追加します。

 // WebSocket接続の初期化
        function initWebSocket() {
            ws = new WebSocket('ws://NODERED-SERVER-IP:1880/ws/temperature');

            // WebSocketが開いたときの処理
            ws.onopen = function() {
                console.log("WebSocket接続が開かれました");
            };

            // WebSocketでメッセージを受信した時の処理
            ws.onmessage = function(event) {
                const data = JSON.parse(event.data);
                document.getElementById('temperature').innerText = data.temperature;
                console.log("受信したデータ: ", data);
            };

            // エラー時の処理
            ws.onerror = function(error) {
                console.error("WebSocketエラー: ", error);
            };

            // 接続が閉じられた時の再接続処理
            ws.onclose = function() {
                console.log("WebSocket接続が閉じられました。再接続を試みます...");
                setTimeout(initWebSocket, reconnectInterval); // 再接続を5秒後に試みる
            };
        }

こうすることで

このように、WebSocket接続が閉じられたら再接続をするようになりました。

他のデータもNode-REDから受信できるようにする

発電電力、CO2削減量、湿度、日射強度もWebSocketでNode-REDから受信できるようにします。また、WebSocketのEndpointがtemperatureだったのでこれをdataに修正します。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>リアルタイムセンサーデータ表示</title>
    <!-- BootstrapのCSSをCDNから読み込み -->
    <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet">
    <!-- Chart.jsのCDNを読み込み -->
    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
    <style>
        body {
            background-color: #000;
            color: #fff;
        }
        .card {
            background-color: #222;
            border: none;
        }
        .card-header {
            background-color: #333;
        }
        .chart-container {
            width: 80%;
            margin: 20px auto;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1 class="text-center my-4">設備情報</h1>
        <div class="row">
            <div class="col-md-3 mb-3">
                <div class="card text-center">
                    <div class="card-header">発電電力</div>
                    <div class="card-body">
                        <p>本日の合計: <span id="daily-power">24.3</span> kWh</p>
                        <p>今月の合計: <span id="monthly-power">153.5</span> kWh</p>
                    </div>
                </div>
            </div>
            <div class="col-md-3 mb-3">
                <div class="card text-center">
                    <div class="card-header">CO<sub>2</sub>削減量</div>
                    <div class="card-body">
                        <p>本日の合計: <span id="daily-co2">96</span> kg-CO<sub>2</sub></p>
                        <p>今月の合計: <span id="monthly-co2">2466</span> kg-CO<sub>2</sub></p>
                    </div>
                </div>
            </div>
            <div class="col-md-3 mb-3">
                <div class="card text-center">
                    <div class="card-header">気温</div>
                    <div class="card-body">
                        <p><span id="temperature">25</span> °C</p>
                    </div>
                </div>
            </div>
            <div class="col-md-3 mb-3">
                <div class="card text-center">
                    <div class="card-header">湿度</div>
                    <div class="card-body">
                        <p><span id="humidity">31</span> %</p>
                    </div>
                </div>
            </div>
            <div class="col-md-3 mb-3">
                <div class="card text-center">
                    <div class="card-header">日射強度</div>
                    <div class="card-body">
                        <p><span id="solar-intensity">0.86</span> kW/m<sup>2</sup></p>
                    </div>
                </div>
            </div>
        </div>

        <div class="chart-container">
            <canvas id="powerChart"></canvas>
        </div>
    </div>

    <!-- jQueryとBootstrapのJSをCDNから読み込み -->
    <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@4.5.2/dist/js/bootstrap.bundle.min.js"></script>
    
    <script>
        let ws;
        const reconnectInterval = 5000; // 5秒間隔で再接続

        // WebSocket接続の初期化
        function initWebSocket() {
            ws = new WebSocket('ws://NODERED-SERVER-IP:1880/ws/data');

            // WebSocketが開いたときの処理
            ws.onopen = function() {
                console.log("WebSocket接続が開かれました");
            };

            // WebSocketでメッセージを受信した時の処理
            ws.onmessage = function(event) {
                const data = JSON.parse(event.data);

                // 受信データをそれぞれの要素に反映
                if (data.temperature !== undefined) {
                    document.getElementById('temperature').innerText = data.temperature;
                }
                if (data.dailyPower !== undefined) {
                    document.getElementById('daily-power').innerText = data.dailyPower;
                }
                if (data.monthlyPower !== undefined) {
                    document.getElementById('monthly-power').innerText = data.monthlyPower;
                }
                if (data.dailyCO2 !== undefined) {
                    document.getElementById('daily-co2').innerText = data.dailyCO2;
                }
                if (data.monthlyCO2 !== undefined) {
                    document.getElementById('monthly-co2').innerText = data.monthlyCO2;
                }
                if (data.humidity !== undefined) {
                    document.getElementById('humidity').innerText = data.humidity;
                }
                if (data.solarIntensity !== undefined) {
                    document.getElementById('solar-intensity').innerText = data.solarIntensity;
                }

                console.log("受信したデータ: ", data);
            };

            // エラー時の処理
            ws.onerror = function(error) {
                console.error("WebSocketエラー: ", error);
            };

            // 接続が閉じられた時の再接続処理
            ws.onclose = function() {
                console.log("WebSocket接続が閉じられました。再接続を試みます...");
                setTimeout(initWebSocket, reconnectInterval); // 再接続を5秒後に試みる
            };
        }

        // WebSocket接続の開始
        initWebSocket();

        // グラフの設定
        const ctx = document.getElementById('powerChart').getContext('2d');
        const powerChart = new Chart(ctx, {
            type: 'bar',
            data: {
                labels: ['0時', '3時', '6時', '9時', '12時', '15時', '18時', '21時'],
                datasets: [{
                    label: '本日の発電量',
                    data: [0, 0, 5, 15, 25, 30, 20, 5],
                    backgroundColor: 'rgba(255, 165, 0, 0.7)',
                    borderColor: 'rgba(255, 165, 0, 1)',
                    borderWidth: 1
                }]
            },
            options: {
                scales: {
                    y: {
                        beginAtZero: true,
                        max: 30
                    }
                }
            }
        });
    </script>
</body>
</html>

Node-RED側も修正します

msg.payload = {
    temperature: (20 + Math.random() * 10).toFixed(1), // 気温 20°C〜30°C
    dailyPower: (Math.random() * 50).toFixed(1),       // 本日の発電電力 0〜50 kWh
    monthlyPower: (Math.random() * 200).toFixed(1),    // 今月の発電電力 0〜200 kWh
    dailyCO2: Math.floor(Math.random() * 300),         // 本日のCO2削減量 0〜300 kg-CO2
    monthlyCO2: Math.floor(Math.random() * 3000),      // 今月のCO2削減量 0〜3000 kg-CO2
    humidity: Math.floor(20 + Math.random() * 40),     // 湿度 20〜60%
    solarIntensity: (Math.random()).toFixed(2)         // 日射強度 0〜1 kW/m²
};
return msg;

functionノードをこのように修正

結果

これで、Node-RED側からデータを受け取ることができました。

まとめ

以上、Sakura VPSでnginxを使いIoTダッシュボードを作成する方法 その2を紹介しました。

次回はNode-RED側でグラフの値を生成して、index.htmlで表示する方法を紹介します。