2017年4月8日土曜日

FlashAir+Arduino+Raspberry Piで温湿度計(4):Raspberry Pi で見える化する〜完成

[This report is also available in English.]

もくじ
  1. 準備〜センサから値を読む
  2. iSDIOを使いながらSDカードを読む
  3. Arduino から Raspberry Pi に測定値を送る
  4. Raspberry Pi で見える化する〜完成

ようやく最終回です。
Arduino から送られてきたデータを Raspberry Pi で受け取り、
Java Script で見える化します。

今回作成する PHP, JavaScript は Github に置きました

おさらいですが、全体の通信シーケンスは次のようになります。


データ収集APIでArduinoからの送信を受け付けます。
受け取ったデータはログファイルに書き込んでおき、
タブレット用表示ページのJavaScriptから参照させる流れです。

Raspberry PiでWeb APIを動かす

Apache と PHP で Web API を作ります。
単にパッケージをインストールするだけです。
ググればたくさん情報が見つかりますので詳細は省略します。
pi@raspberrypi ~ $ sudo apt-get install apache2
pi@raspberrypi ~ $ sudo apt-get install php
pi@raspberrypi ~ $ sudo systemctl start apache2
インストール後、/var/www/html/ に置いたファイルを HTTP 経由で参照できるようになります。

データ収集API

Arduino から HTTP 経由でデータを受け取ってログに書き込みます。
厳密に言うと、同時アクセスを考慮したファイルロックや、
ログが肥大化した時の対応を考えるべきですが、
今回はArduinoの数が少ないので、とりあえず動くレベルで済ませます。

ポイントは次のあたりでしょうか。
  • Arduino は起動時刻からの経過時間しか取得できないので、
    ログのタイムスタンプは Raspberry Pi 側の時刻を書き込む。
  • 場所IDごとにログファイルを切り替える。
  • Arduinoから異常な値を送信された場合にログに書き込まない。

まずはログ書き込み先フォルダを作ります。

pi@raspberrypi ~ $ mkdir /var/www/html/data/
pi@raspberrypi ~ $ chmod 777 /var/www/html/data/

次の PHP を /var/www/html/api.php に保存し、
Arduino 側で「http://{IPアドレス}/api.php」にデータを送ると
/var/www/data/log_{場所ID}.txt に計測値が書き込まれます。

OK

表示用ページ

表示用の方もあまり汎用化を考えずに、とりあえず動くレベルで作りました。

グラフ表示には Chart.js のバブルチャートを使います。
ググってもバブルチャートの説明があまり見つかりませんが、
使い方は棒グラフなどとあまり違いはありません。

まずは枠の HTML を置きます。
JavaScript を読み込んで canvas を作っているだけです。
canvas の中身は全て「mychart.js」で描画します。







Temperature



と言うわけで、mychart.js という名前でバブルチャート本体を作ります。

onLoad で呼び出される main メソッドではログファイルのリストを作って描画用関数に渡します。
それを setInterval で10分おきに呼び出して再描画します。

readyChart メソッドはバブルチャートの座標軸や凡例など書き変わらない部分を定義しています。
詳細は Chart.js のマニュアルで説明されています。
全部ハードコーディングにしてしまったので、測定場所が増えると書き換えが必要です。

function main() {

  var filePath = [
        './data/log_1.txt?'+new Date().getTime(),
        './data/log_2.txt?'+new Date().getTime(),
        './data/log_3.txt?'+new Date().getTime(),
        './data/log_4.txt?'+new Date().getTime(),
        './data/log_5.txt?'+new Date().getTime(),
        './data/log_6.txt?'+new Date().getTime()
        ];

  var myChart = readyChart();

  updateChart(filePath, myChart,0);

  setInterval(updateChart(filePath,myChart),60000);
}
function readyChart() {
  var data = [{},{},{},{},{},{}] ;

  var ctx = document.getElementById("myChart").getContext("2d");
  var myChart = new Chart(ctx, {
    type: 'bubble',
    data: {
      datasets: [
        { 
          label: "リビング",
          data: data[0],
          borderColor: '#FF0000',
          borderWidth: 3,
          showLine: true
        },
             :(中略)
        {
          label: "洗面所収納",
          data: data[5],
          borderColor: '#666666',
          borderWidth: 3,
          showLine: true
        }


      ]
    },
    options: {
      scales: {
        xAxes: [{
          scaleLabel: {
            display: true,
            labelString: '温度 [℃]'
          }
        }],
        yAxes: [{
          scaleLabel: {
            display: true,
            labelString: '湿度 [%]'
          }
        }]
      }
    }
  });

  return myChart;
}

続いて、チャートの描画です。
渡されたファイルリストから1個ずつログファイルを取得してJSON形式に変換します。
JavaScript が結構煩雑になるので、ログを JSON-P 形式にするPHPを作っておき、
HTML から script タグでロードした方が楽だったかもしれません。

とは言え、今回は JavaScript で CSV をパースするように作ってしまったのでこのまま進めます。
XMLHttpRequest を連続で呼び出すと非同期で再描画が走って表示が崩れるので、
XMLHttpRequest の onLoad の末尾で再帰呼び出しすることで、シーケンシャルに
各ログを読み込みました。
そのため、途中のログでエラーが発生すると再描画されなくなります。。。

バブルチャートの注意点としては、単純にデータ点数をバブルサイズにしてしまうと、
測定時間が長かった場所だけバブルが巨大になってしまいます。
そこで、今回は測定値を整数に丸め込んで(DHT11の場合は元から整数です)おき、
同一測定値のデータ点数最大値で全体を割り算しています。
簡易ではありますが、この方法でバブルの最大サイズを固定しました。

function updateChart(filePath, myChart, idx) {

  if ( idx >= filePath.length ){ return; }
  var req = new XMLHttpRequest();
  req.open("GET", filePath[idx], true);
  req.onload = function() {
    var data = convertData(csv2Array(req.responseText));
    myChart.data.datasets[idx].data = data;
    myChart.update();
    updateChart(filePath, myChart, idx+1);
  }
  req.send(null);
}

function convertData(source) {
  var data = [];
  var max = 1;
  var buf = {};
  for (var row in source) {
    y = String(parseInt(parseFloat(source[row][1]) + 0.5));
    x = String(parseInt(parseFloat(source[row][2]) + 0.5));
    if ( buf[x] == undefined ){
      buf[x] = {};
      buf[x][y] = 1;
    } else if ( buf[x][y] == undefined ){
      buf[x][y] = 1;
    }else{ buf[x][y] ++; }

    if ( buf[x][y] > max ){ max = buf[x][y]; }
  };

  for (var i in buf) {
    for (var j in buf[i]) {
      data.push( { x:i, y:j, r:20*buf[i][j]/max } );
    }
  }
  return data;
}

function csv2Array(str) {
  var csvData = [];
  var lines = str.split("\n");
  for (var i = 0; i < lines.length; ++i) {
    var cells = lines[i].split(",");
    csvData.push(cells);
  }
  return csvData;
}

完成

そんなこんなで Raspberry Pi 側は色々とやっつけ仕事ですが一応完成。
iPad などで表示用ページにアクセスすると次のように表示されます。
寝室だけ夜間に測定したので寒いです。
洗面所よりも湿度の高い子供部屋。

0 件のコメント:

コメントを投稿