IoT試作の例・大気圧自動観測システム

システム概要

  1. 弊社に大気圧測定装置を設置し、WiFi経由でレンタルサーバー内のMySQLデータベースへ定期的に測定値を送り、記録する
  2. ブラウザから、データベースへアクセスしをデータ取得し表示する
  3. 定期的に最新データを取得し、グラフを再表示する
  4. 装置全体
  5. 表示パネル

主要部品

  1. WiFiモジュール ESP32 購入元:HiLetgo 購入時価格@1,248円
  2. 気圧温度センサー bmp180 購入元:HK-JP-ShopStar 購入時価格@207円
  3. 表示装置 LCDキャラクタディスプレイモジュール 16×2行 バックライト付白抜き 購入元:秋月電子 購入時価格@800円

システム仕様

  1. WiFiモジュール ESP32のプログラム作成はarduino IDEを使用
    ARDUINOホームページ

    arduinoソースコード クリックで展開
                
    // include the library code:
    #include <Wire.h>
    #include <WiFi.h>
    #include <KanaLiquidCrystal.h>
    #include <LiquidCrystal.h>
    #include <Adafruit_BMP085.h>
    Adafruit_BMP085 bmp;
    
    const char* ssid     = "親機のSSID";
    const char* password = "親機のパスワード";
    
    // データを書き込むサーバーのホスト名
    #define HOST_NAME     "yose.co.jp"
    // PHPファイルが格納されている場所のファイルパス
    #define PHP_PATH      "/arduino/kisyo/sensorSetDb.php"
    
    // ポート番号
    #define PORT_NUMBER       80
    
    // these constants won't change.  But you can change the size of
    // your LCD using them 行列:
    const int numRows = 2;
    const int numCols = 16;
    
    // initialize the library with the numbers of the interface pins
    KanaLiquidCrystal lcd(17, 23, 5, 18, 19, 16);
    
    //気圧
    double ruikeiVal = 0;
    double heikinVal = 0;
    double val = 0;
    double valHosei = -320;
    int kiatu = 0;
    
    //気温
    double ruikeiTemp = 0;
    double heikinTemp = 0;
    double temp = 0;
    double tempHosei = -2.1;
    
    //カウンター
    int    cntVal;
    
    void setup() {
      // 転送レート115200bpsでパソコンとのシリアル通信のポートを開ける
      //*************************************************************************
      // パソコンと接続しないときここで止まるので、この行はコメントにしておく
      //Serial.begin(115200);
      // *************************************************************************
    
      // set up the LCD's number of columns and rows:
      lcd.begin(numCols, numRows);
      lcd.kanaOn(); // 半角カナの表示を許可する
    
      // Wi-Fi設定
      setupWiFi();
      if (!bmp.begin()) {
        Serial.println("Could not find a valid BMP085 sensor, check wiring!");
      }
    }
    
    void setupWiFi() {
      Serial.print("Connecting to ");
      Serial.println(ssid);
    
      // シリアルポートの指定
      WiFi.begin(ssid, password);
      while (WiFi.status() != WL_CONNECTED) {
        delay(3000);
        -    Serial.print(".");
        if ( 10 < retryKaisu) {
          delay(10000);
          retryKaisu = 0;
          ESP.restart();
          retryKaisu = 0;
        }
        retryKaisu += 1;
      }
    
      Serial.println("");
      Serial.println("WiFi connected");
      Serial.println("IP address: ");
      Serial.println(WiFi.localIP());
      delay(3000);
    }
    
    void loop() {
      char msg[17];
      char s[17];
      Serial.print("Pressure = ");
      Serial.print(bmp.readPressure());
      Serial.println(" Pa");
    
      // set the cursor to column 0, line 1
      // (note: line 1 is the second row, since counting begins with 0):
      val = bmp.readPressure() + valHosei;
      temp = bmp.readTemperature() + tempHosei;
    
      lcd.setCursor(0, 0);
      sprintf(msg, " キオン :%s ドC", dtostrf( temp, 6, 1, s));
      lcd.print(msg);
    
      lcd.setCursor(0, 1);
      sprintf(msg, " キアツ :%s  pa", dtostrf(val, 6, 0, s));
      lcd.print(msg);
    
      WiFiClient client;
      const int httpPort = 80;
      if (10 >= cntVal) {
        //10ごとにデータを送る
        Serial.println("10回ごとにデータを送る");
        if (!client.connect(HOST_NAME, PORT_NUMBER)) {
          Serial.println("connection failed");
          Serial.println("wait 5 sec...");
          delay(5000);
          if ( 10 < retryKaisu) {
            delay(10000);
            retryKaisu = 0;
            ESP.restart();
          }
          retryKaisu += 1;
          return;
        }
        heikinVal = ruikeiVal / cntVal + valHosei;
        kiatu = ruikeiVal / cntVal;
        heikinTemp = ruikeiTemp / cntVal + tempHosei;
        char sendData[256] = "";
    
      //気温、気圧両方送る
        sprintf(sendData, "GET %s?value=%lf&temp=%lf HTTP/1.1\r\nHost: %s\r\nConnection: close\r\n\r\n", PHP_PATH, heikinVal, heikinTemp, HOST_NAME);
        // サーバーへデータを送信
        client.print(sendData);
    
        Serial.println(sendData);
        Serial.println(" ");
    
    
        ruikeiTemp = 0;
        ruikeiVal = 0;
        cntVal = 0;
      }
      else {
        cntVal += 1;
        ruikeiVal += bmp.readPressure();
        ruikeiTemp += bmp.readTemperature();
      }
    
      delay(60000);
    
    }
    
                
                

  2. データベースはMySQL、読み書きはPHPを使用

    データベースへ書き込むときのコード クリックで展開
                
    <?php
      $value = $_GET['value'];
      $temp = $_GET['temp'];
    // データベースに接続するために必要なデータソースを変数に格納
    // mysql:host=ホスト名;dbname=データベース名;charset=文字エンコード
    $dsn = 'mysql:host=mysqlのサーバー名;dbname=データベース名;charset=utf8';
    
      // データベースのユーザー名
    $user = 'ユーザー名';
    
      // データベースのパスワード
    $password = 'パスワード';
    
    // tryにPDOの処理を記述
    try {
    
      // PDOインスタンスを生成
      $dbh = new PDO($dsn, $user, $password);
    
    // エラー(例外)が発生した時の処理を記述
    } catch (PDOException $e) {
    
      // エラーメッセージを表示させる
      echo 'データベースにアクセスできません!' . $e->getMessage();
    
      // 強制終了
      exit;
    
    }
    // INSERT INTO文を変数に格納
    $sql = "INSERT INTO kisyo (jikoku, ondo, ppm) VALUES (NOW(), :ondo, :ppm)";
    
    // 値と該当のIDは空のまま、SQL実行の準備をする
    $stmt = $dbh->prepare($sql);
    
    // 値を配列に格納する
    $params = array(':ppm' => $value, ':ondo' => $temp);
    
    // 値と該当のIDが入った変数をexecuteにセットしてSQLを実行
    $stmt->execute($params);
    
    // 完了のメッセージ
    echo '挿入完了しました';
    
    // データベースとの接続を閉じる
    $dbh = null;
    ?>
    
                
                

    データベースから読み込むときのコード クリックで展開
                
    <?php
    // mysql:host=ホスト名;dbname=データベース名;charset=文字エンコード
    $dsn = 'mysql:host=mysqlのサーバー名;dbname=データベース名;charset=utf8';
    
      // データベースのユーザー名
    $user = 'ユーザー名';
    
      // データベースのパスワード
    $password = 'パスワード';
    
    // tryにPDOの処理を記述
    try {
      // PDOインスタンスを生成
      $dbh = new PDO($dsn, $user, $password);
    
      // エラー(例外)が発生した時の処理を記述
    } catch (PDOException $e) {
    
      // エラーメッセージを表示させる
      echo 'データベースにアクセスできません!' . $e->getMessage();
    
      // 強制終了
      exit;
    
    }
    // SELECT文を変数に格納
    $sql = "SELECT * FROM kisyo  ORDER BY jikoku DESC LIMIT 1 ";
    
    // SQLステートメントを実行し、結果を変数に格納
    $stmt = $dbh->query($sql);
    
    // foreach文で配列の中身を一行ずつ出力
    foreach ($stmt as $row) {
      //$arere[] = $row['ppm'];
      //$arere[] = $row['dt'];
      // データベースのフィールド名で出力
        //list($id, $ppm, $dt) = $row;
        //echo "id= ",$id, " ppm= " ,$ppm, " dt= ", $dt,"
    "; // echo $row['ppm'], $row['dt']; echo json_encode($row); } //echo json_encode($stmt); // データベースとの接続を閉じる $dbh = null; ?>

  3. グラフはオープンソースの Chart.jsを使用
    Chart.jsを利用したコード クリックで展開
                
    // センサーの値
    var sensorValue = 0;
    // サーバーにアクセス中かどうか
    var isAjax = false;
    var sensorTime = new Date();
    var in_box = document.getElementById("id_kensu");
    var in_max = document.getElementById("id_max");
    var inkensu = in_box.value * 132;
    
    var config = {
        type: 'line',
        data: {
            //    labels: ['1', '2', '3', '4', '5'],
            labels: [],
            datasets: [{
                label: '気圧[Pa]',
                backgroundColor: window.chartColors.blue,
                borderColor: window.chartColors.blue,
                //      data: [100, 200, 300, 400, 500],
                data: [],
                fill: false,
        }]
        },
        options: {
            // アニメーションの速さ
            animationSteps: 10,
            responsive: true,
            title: {
                display: true,
                text: '亀田本町の気圧 件数指定可 自動更新あり'
            },
            tooltips: {
                mode: 'index',
                intersect: false,
            },
            hover: {
                mode: 'nearest',
                intersect: true
            },
            scales: {
                xAxes: [{
                    display: true,
                    scaleLabel: {
                        display: true,
                        labelString: '測定日時'
                    }
          }],
                yAxes: [{
                    display: true,
                    scaleLabel: {
                        display: true,
                        labelString: '気圧[Pa]'
                    }
          }]
            }
        }
    };
    
    
    var ctx = document.getElementById('canvas').getContext('2d');
    window.myLine = new Chart(ctx, config);
    
    window.onload = function () {
        if (!isAjax) getSensorValueIni();
    };
    
    var bkaisi = document.getElementById('kaisi');
    
    bkaisi.addEventListener('click', function () {
    //    inkensu = in_box.value;
        inkensu = (in_box.value) * 132;
        
        console.log("in_box.value : " + in_box.value);
    
        if (!isAjax) getSensorValueIni();
        //  window.myLine.update();
    });
    
    var bkaisi100 = document.getElementById('kaisi100');
    
    bkaisi100.addEventListener('click', function () {
    
        inkensu = 132;
        console.log("in_box.value : " + 132);
    
        if (!isAjax) getSensorValueIni();
        //  window.myLine.update();
    });
    
    function getSensorValueIni() {
        // フラグをtrue
        isAjax = true;
    
        //console.log("inkensu : " + inkensu);
    
        console.log("inkensu : " + inkensu);
        var urlRange = "range.php?kensu=" + inkensu;
        //var PHP_URL = "range.php";
    
        $.ajax({
    
            url: urlRange,
            type: "post",
            //dataType: "json",
            dataType: "text",
            success: function (avalue) {
    
                var len = config.data.labels.length;
                config.data.labels.splice(0, len); // remove the label
                config.data.datasets[0].data.splice(0, len); // remove the
    
    
                console.log("avalue : " + avalue);
                rvalue = avalue.slice(1);
                console.log("rvalue slice(1): " + rvalue);
                rvalue = rvalue.slice(0, -1);
                console.log("rvalue slice(-1): " + rvalue);
                rvalue = rvalue.replace(/},/g, '}},');
                rvalue = rvalue.split('},');
                console.log("rvalue split : " + rvalue);
                for (i = rvalue.length - 1; 0  <= i; i--) {
                    console.log("avalue[" + i + "] : " + rvalue[i]);
                    var sData = JSON.parse(rvalue[i]);
                    console.log("ppm : " + sData.ppm);
                    console.log("jikoku : " + sData.jikoku);
                    //var sData = eval('(' + value + ')');
                    //sensorValue = value;
                    sensorValue = sData.ppm;
                    sensorTime = sData.jikoku;
                    console.log("Sensor Value : " + sensorValue);
                    console.log("Sensor time : " + sensorTime);
                    var dtstr = sensorTime.toString();
                    console.log("dtstr : " + dtstr);
                    console.log("dtstr len : " + dtstr.length);
                    var format_str = dtstr.slice(0, 16);
                    //        var format_str = dtstr.slice(11);
                    console.log("format_str : " + format_str);
                    //var minute_str = dtstr.substring(15,16);
                    //var second_str = dtstr.substring(18,19);
    
                    //format_str = hour_str + ":" + minute_str + ":" + second_str;
                    //format_str = hour_str + ":" + minute_str + ":" + second_str;
    
                    config.data.labels.push(format_str);
                    //dataset.data.push(999);
                    config.data.datasets[0].data.push(sensorValue);
                    //window.myLine.update();
                }
                // フラグをfalse
                isAjax = false;
                window.myLine.update();
    
            },
            error: function () {
                console.log("センサーの値を取得できませんでした。");
                isAjax = false;
            }
        })
    }
    
    setInterval(function () {
    
        // サーバーへアクセス中でなければ
        if (!isAjax) getSensorValueAdd();
    
        //var date = new Date();
        var dtstr = sensorTime.toString();
        console.log("dtstr : " + dtstr);
        console.log("dtstr len : " + dtstr.length);
        var format_str = dtstr.slice(0, 16);
        console.log("format_str : " + format_str);
        //var minute_str = dtstr.substring(15,16);
        //var second_str = dtstr.substring(18,19);
    
        //format_str = hour_str + ":" + minute_str + ":" + second_str;
        //format_str = hour_str + ":" + minute_str + ":" + second_str;
    
        var len = config.data.datasets[0].data.length;
        if (len > inkensu) {
            config.data.labels.shift(); // remove the label first
            //				config.data.labels[0].pop();
            config.data.datasets[0].data.shift();
        } else {
            config.data.labels.push(format_str);
            //dataset.data.push(999);
            config.data.datasets[0].data.push(sensorValue);
        }
    
        window.myLine.update();
    }, 600000);
    
    function getSensorValueAdd() {
        var urlOne = "sensorGetDb.php";
        // フラグをtrue
        isAjax = true;
    
        $.ajax({
            //url: PHP_URL + "?p=" + new Date().getTime(),
            url: urlOne,
            type: "post",
            //dataType: "json",
            dataType: "text",
            success: function (value) {
                console.log("value : " + value);
                var sData = JSON.parse(value);
                console.log("ppm : " + sData.ppm);
                console.log("jikoku : " + sData.jikoku);
                //var sData = eval('(' + value + ')');
                //sensorValue = value;
                sensorValue = sData.ppm;
                sensorTime = sData.jikoku;
                console.log("Sensor Value : " + sensorValue);
                console.log("Sensor time : " + sensorTime);
    
                // フラグをfalse
                isAjax = false;
            },
            error: function () {
                console.log("センサーの値を取得できませんでした。");
                isAjax = false;
            }
        })
    }
    
                
                

  4. 気圧の単位はパスカルです、100で割るとヘクトパスカルになります
  5. 約10分おきに自動的に再表示します
  6. 表示日数を指定できます。

  • こちらの説明では簡単すぎて再現困難だと思いますが、興味を持たれた方は、最新情報をネット上で探してみてください。
  • 電子工作とインターネットを組み合わせた、実験ですが、最初はこの本がきっかけで興味をもちました
  • また、こちらのics.mediaさんのサイトも参考になりました。