園芸×IoT(前編)環境センサ「お花大好きボックス」(Maker Faire Tokyo 2019)
2019年08月02日 金曜日
CONTENTS
はじめに
前編では、Machinist 普及活動の名目のもと、趣味で作成した植物向け温湿度計を対応させたので作成方法を小学生にも分かるようまとめてみました。是非夏休みの自由研究やサボテン育成にご利用ください。
後編では、身の回りのメトリクスを収集・整理・活用するMachinistについて紹介します。
お花大好きボックスについて
「お花大好きボックス」とは趣味でサボテンを育てているデジタルネイティブな製作者が少しでも育成環境向上を図るため作成したデジタルなIoTな百葉箱のことです。
具体的な機能としては指定した間隔で「温度」「湿度」「気圧」を取得し Wi-Fi 経由でインターネット上のSaaS(Machinist)にデータを保存しつづけるありきたりな機能です。
あえて特徴を言うのであれば「電池駆動」「Wi-Fi(SSID/Presharedkey)は任意で設定可能」「外部サービス(Machinist)の設定が任意に可能」を目指しました。
これらの意図は Arduino の組み込みにおいて固定化されやすいデータを可変にすることで共有しやすくしたかったからです。
使ったもの
以下を秋葉原にある秋月電子通商で購入しました。
2019年7月25日時点でのリンクを用意しましたが代用は可能なのでお好きに好きな部品を選んでください。
品目 | 個数 | 価格 |
---|---|---|
電解コンデンサー OS-CON 1000μF 16V | 1 | ¥140 |
Wi-Fi モジュール ESP-WROOM-02 DIP化キット | 1 | ¥650 |
低損失CMOS三端子レギュレータ 3.3V 500mA NJU7223F33 | 1 | ¥50 |
タクトスイッチ(黒) | 1 | ¥120 |
カーボン抵抗(炭素皮膜抵抗)1/4W 10kΩ | 3 | ¥100 |
カーボン抵抗(炭素皮膜抵抗)1/4W 6.3Ω | 1 | ¥100 |
分割ロングピンソケット(メス) 1×40(40P) | 1 | ¥80 |
BME280使用 温湿度・気圧センサモジュールキット | 1 | ¥1,080 |
絶縁ラジアルリード型積層セラミックコンデンサー0.1μF50V2.54mm | 1 | ¥100 |
片面ユニバーサル基板(ブレッドボード配線パターンタイプ) Dタイプ(47x36mm) ガラスコンポジット | 1 | ¥40 |
3mm緑色LED | 1 | ¥20 |
電池ボックス 単3×4本 Bスナップ | 1 | ¥60 |
バッテリースナップ(電池スナップ・Bスナップ) 縦型 カバー付 | 1 | ¥20 |
耐熱電子ワイヤー 1m×10色 導体外径0.54mm(AWG24相当) | 数本 | ¥300 |
利用しているプログラム
Arduino(EPS8266) への書き込み方法はインターネット中にあるのでここでは説明を省きます。
一点だけヒントというかポイントとして、おそらく EPS8266-Generic ボードを選択すると思いますが「Flash Size」を「512K (32K SPIFFS)」にしてください。これでWi-Fi等の情報を ESP8266 内に保存できるようになります。
プログラムのソース。
// 環境セットアップ // BME280 のライブラリを入手 // https://github.com/embeddedadventures/BME280 // IDE に esp8266 を登録 // https://github.com/esp8266/Arduino/releases/download/2.5.2/package_esp8266com_index.json // IIJMachinistClient // https://github.com/nara256/IIJMachinistClient // ////////////////////////////////////////////////////////////////////// // 20190611.a : 保存先を IIJ Machinistに変更 // 20171225.a : First edition() #include <BME280_MOD-1022.h> #include <Wire.h> #include <ESP8266WiFi.h> #include <ESP8266WebServer.h> #include <FS.h> #include <IIJMachinistClient.h> // ESP8266 // 任意設定の方針へ変更により元ネタ値 const unsigned long DEEPSLEEP_INTERVAL = 60 * 1000 * 1000; ESP8266WebServer server(8888); // サーバーモード時の IP Address IPAddress ip( 192, 168, 10, 1 ); IPAddress subnet( 255, 255, 255, 0 ); boolean modestatus; // サーバモード LED 用の PIN const int LED_PIN = 13; // リセット用 PIN const int MODE_PIN = 12; // Wi-Fi設定保存ファイル const char* ConfTXT = "/conf.txt"; // IIJ Machinist WiFiClient client; // ============================================================================================ // // リセットボタンを押しながら起動するとサーバモードで実行される。 void FirestStartWIFI() { // ssid は MAC アドレスとする。 byte mac[6]; WiFi.macAddress(mac); String ssid = ""; for (int i = 0; i < 6; i++) { ssid += String(mac[i], HEX); } Serial.println("SSID: " + ssid); WiFi.softAPConfig(ip, ip, subnet); WiFi.softAP(ssid.c_str()); server.on("/", HTTP_GET, handleFirestHTML); server.on("/", HTTP_POST, handlePostHTML); server.begin(); Serial.println("HTTP server started."); } // ================================== // void handleFirestHTML() { Serial.println("hendleRootGet Started."); String html = ""; html += "<center>"; html += "<h1>WiFi & IIJ Machinist Settings Page.</h1>"; html += "<form method='POST'>"; html += " <input type='text' name='ssid' placeholder='ssid'><br>"; html += " <input type='text' name='wifipassword' placeholder='wifipassword'><br>"; html += " <input type='text' name='APIKEY' placeholder='IIJ Machinist API KEY'><br>"; html += " <input type='text' name='AgentName' placeholder='IIJ Machinist Agent Name'><br>"; html += " <b>Post Interval</b><select name='Tempinterbal'>"; html += " <option value='1'>1</option>"; html += " <option value='3'>3</option>"; html += " <option value='5'>5</option>"; html += " <option value='10'>10</option>"; html += " <option value='15'>15</option>"; html += " <option value='20' selected>20</option>"; html += " <option value='30'>30</option>"; html += " <option value='40'>40</option>"; html += " <option value='50'>50</option>"; html += " <option value='60'>60</option>"; html += " </select><br>"; html += " <b>LED ON</b><input type='radio' name='LEDlove' value='on'><br>"; html += " <b>LED OFF</b><input type='radio' name='LEDlove' value='off' checked='checked'><br>"; html += " <input type='submit'><br>"; html += "</form>"; server.send(200, "text/html", html); Serial.println("handleFirestHTML END."); } // ================================== // void handlePostHTML() { Serial.println("handlePostHTML Started."); String ssid = server.arg("ssid"); String wifipassword = server.arg("wifipassword"); String APIKEY = server.arg("APIKEY"); String AgentName = server.arg("AgentName"); String Tempinterbal = server.arg("Tempinterbal"); String LEDlove = server.arg("LEDlove"); File f = SPIFFS.open(ConfTXT, "w"); f.println(ssid); f.println(wifipassword); f.println(APIKEY); f.println(AgentName); f.println(Tempinterbal); f.println(LEDlove); f.close(); String html = ""; html += "<h1>Now Setting.</h1>"; html += ssid + "<br>"; html += wifipassword + "<br>"; html += APIKEY + "<br>"; html += AgentName + "<br>"; html += Tempinterbal + "<br>"; html += LEDlove + "<br>"; html += "reboot plz...<br>"; server.send(200, "text/html", html); Serial.println("handlePostHTML END."); delay(5000); } // ================================== // void setup() { // 開始時のお約束 Serial.begin(115200); Serial.println(" "); Serial.println("setup!"); // ファイルシステムの初期化 SPIFFS.begin(); // リセット確認 pinMode(MODE_PIN, INPUT); // ボタンが ON 状態はサーバモードと処理する if (digitalRead(MODE_PIN) == LOW) { Serial.println("MODE PIN LOW"); // サーバモード用フラグ modestatus = true; // サーバモードで実行中はLEDが光り続ける pinMode(LED_PIN, OUTPUT); digitalWrite(LED_PIN, HIGH); // サーバモード実行 FirestStartWIFI(); } else { Serial.println("MODE PIN HIGH"); // 明示的にサーバモードFlagを落とす modestatus = false; // 念のためOFFにする // digitalWrite(LED_PIN, LOW); } // BME280 用 i2c I/F 定義 Wire.begin(); delay(500); // BME280 初期化 BME280.readCompensationParams(); delay(10); BME280.writeStandbyTime(tsb_500ms); // tsb = 500ms // オーバーサンプリング設定(全部16回) BME280.writeFilterCoefficient(fc_16); // IIR Filter coefficient 16 BME280.writeOversamplingPressure(os16x); // pressure x16 BME280.writeOversamplingTemperature(os2x); // temperature x2 BME280.writeOversamplingHumidity(os1x); // humidity x1 BME280.readMeasurements delay(500); Serial.println("setup end."); } // ------------------------------------------------ main loop void loop() { if (modestatus){ server.handleClient(); } else { float temp,humidity,pressure; float tempMostAccurate, humidityMostAccurate, pressureMostAccurate; int n = 1; // 計測開始 BME280.writeMode(smForced); while (BME280.isMeasuring()) { Serial.print("."); delay(100); } // 自動でsleepモードに移行するが念のため Sleep に移行させる BME280.writeMode(smSleep); Serial.println(" BME280 Done!"); BME280.readMeasurements(); tempMostAccurate = BME280.getTemperatureMostAccurate(); // 温度 humidityMostAccurate = BME280.getHumidityMostAccurate(); // 湿度 pressureMostAccurate = BME280.getPressureMostAccurate(); // 気圧 temp=BME280.getTemperature(); humidity=BME280.getHumidity(); pressure=BME280.getPressure(); // 設定ファイル読み込み Serial.println("FileOpen(Read) started."); File f = SPIFFS.open(ConfTXT, "r"); String ssid = f.readStringUntil('\n'); String wifipassword = f.readStringUntil('\n'); String APIKEY = f.readStringUntil('\n'); String AgentName = f.readStringUntil('\n'); String Tempinterbal = f.readStringUntil('\n'); String LEDlove = f.readStringUntil('\n'); f.close(); ssid.trim(); wifipassword.trim(); APIKEY.trim(); AgentName.trim(); Tempinterbal.trim(); LEDlove.trim(); // writeFields には int で指定する必要がある Serial.println("ssid: " + ssid); // Serial.println("wifipassword: " + wifipassword); // Serial.println("APIKEY: " + APIKEY); Serial.println("AgentName: " + AgentName); Serial.println("LEDlove: " + LEDlove); Serial.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); int TITnumber; TITnumber = Tempinterbal.toInt(); Serial.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); // 無駄モードは徐々にLDEを光らせる1 if(LEDlove.equals("on")){ pinMode(LED_PIN, OUTPUT); for ( int L = 0; L < 50; L++){ analogWrite(LED_PIN, L); delay(5);} } else { pinMode(LED_PIN, INPUT); // digitalWrite(LED_PIN, LOW); } // WIFI 接続 Serial.println("WIFI Connect Start"); WiFi.begin(ssid.c_str(), wifipassword.c_str()); while (WiFi.status() != WL_CONNECTED) { if ( n > 20 ){ // WIFI が20回チャレンジしてもつながらない場合はLEDがチカチカする digitalWrite(LED_PIN, HIGH); delay(1000); digitalWrite(LED_PIN, LOW); } else { n = n + 1; delay(2000); } Serial.println("WIFI Connected!"); } // IP 情報を表示 Serial.println(""); Serial.print("IP Address: "); Serial.println(WiFi.localIP()); // 無駄モードは徐々にLDEを光らせる2 if(LEDlove.equals("on")){ for ( int L = 50 ; L < 100; L++){ analogWrite(LED_PIN, L); delay(5);} } // BME280 output print Serial.println(""); Serial.print("getTemperatureMostAccurate:"); Serial.println(tempMostAccurate); Serial.print("getHumidityMostAccurate:"); Serial.println(humidityMostAccurate); Serial.print("getPressureMostAccurate:"); Serial.println(pressureMostAccurate); Serial.println("----------"); Serial.println(temp); Serial.println(humidity); Serial.println(pressure); // 無駄モードは徐々にLDEを光らせる3 if(LEDlove.equals("on")){ for ( int L = 100 ; L < 150; L++){ analogWrite(LED_PIN, L); delay(5);} } // iij machinist 準備 IIJMachinistClient iijmc(APIKEY); iijmc.init(); // IIJ Machinist へ POST Serial.println("Post Date IIJMachinist..."); iijmc.setDebugSerial(Serial); int httpstatus1 = iijmc.post(AgentName, "temperature", "data_point", tempMostAccurate); if (httpstatus1 == 200) { Serial.println("POST OK"); } else { Serial.println("NG status=" + String(httpstatus1)); } int httpstatus2 = iijmc.post(AgentName, "humidity", "data_point", humidityMostAccurate); if (httpstatus2 == 200) { Serial.println("POST OK"); } else { Serial.println("NG status=" + String(httpstatus2)); } int httpstatus3 = iijmc.post(AgentName, "pressure", "data_point", pressure); if (httpstatus3 == 200) { Serial.println("POST OK"); } else { Serial.println("NG status=" + String(httpstatus3)); } // 無駄モードは徐々にLDEを光らせる4 if(LEDlove.equals("on")){ for ( int L = 150 ; L < 200; L++){ analogWrite(LED_PIN, L); delay(5);} } // 念のためお行儀よく切断しておく WiFi.disconnect(); Serial.println("Zzz..."); Serial.println("---------------------------------------------------"); // 無駄モードは徐々にLEDを消す if(LEDlove.equals("on")){ for (int L=255; L > 0; L--){ analogWrite(LED_PIN, L); delay(8); } } // インターバル時間を修正 ESP.deepSleep(TITnumber * DEEPSLEEP_INTERVAL, WAKE_RF_DEFAULT); //deepsleepモード移行までのダミー命令 delay(1000); } }
作成した基板
回路図と作成した基板の写真を載せておきます。
利用方法
設定と起動方法は以下の通り。
本機は機器の設定を実施する「サーバモード」と実際にデータを取得する「クライアントモード」が存在します。サーバモードへは起動時にIO12をLOWにすることで移行します。
Step.1 Machinist を準備する
- Machinistのアカウントを取得する。
- 自分好みのエージェントを作成する。(作成したエージェント名をセットアップで利用します)
- 「アカウント設定」の画面から「APIキー」を用意する。
Machinistの利用方法については、後のパートをご覧ください。
お花大好きボックスに必要な情報は「APIキー」と「エージェント名」です。
Step.2 お花大好きボックスを設定する
- 機器のボタンスイッチを押しながら電源投入(電池セット)しサーバモードで起動する。
- LED ランプが光り続けていることを確認する。
- iPhone から Wi-Fi のAPを検索する。
- お花大好きボックスの MAC アドレス(英数字6文字程度の羅列)を見つけ出し接続する。(この時のパスワードは不要です。)
- iPhone のブラウザで http://192.168.10.1:8888 にアクセスする。
- ブラウザに表示された画面(※)に従い利用する Wi-Fi の SSID とパスワードを入力する。
- Machinist の API KEY とエージェント名を入力する。
- ブラウザに表示されている submit ボタンを押して保存する。
- 画面が遷移し入力内容を確認後。本製品を再起動(電池の抜き差し)を実施すると環境設定が完了。
※)画面内には「LED ON」と「LED OFF」があります。
「LED ON」にチェックをつけると処理途中にLEDが点滅します。
「ベランダで光ってると可愛くね?」というふざけた理由でつけた機能であり、電池の無駄遣いであり本製品のコンセプトを覆す動作でありおすすめしません。
Step.3 利用する
- 電源を投入し Machinist の画面が更新されるのを待つ。
- 初めて利用する場合は温度環境が安定するまで1日程度かかることがあるので気長に待つ。
なお、設定は再起動後も残ります。
投入した設定は明示的に削除しなければ消えないのでご注意ください。
QA
Q1. LED ランプが光り続けています。
A1. 機器設定を実施する「サーバモード」で起動しています。
機器のボタンが押されている状態で電源が投入されたと考えられます。
Q2. クライアントモードで実行中LEDランプが激しく点滅します。
A2. おそらく Wi-Fi への接続に失敗しています。
点滅が続くようであればサーバモードで起動しなおして再設定をおすすめします。
Q3. データはどの程度正確ですか?
A3. たぶん。誤差は以下の通りです。
Q4. 設定を初期化したいです。
A4. サーバモードで起動して空白でPOSTしてください。
Q5. 取得データが0だったり、取得に失敗したりします。
A5. 電池切れです。電池を交換してください。
Q6. 気圧は不要です。
A6. 湿度も不要であればそれなりにコスト削減が出来るようになると思います。
Q7. 1分間隔で利用してたら電池一ヶ月持ちませんでした。
A7. 当然です。15分間隔であれば半年以上持ちます。
Q8. 正しく設定を入力したはずなのに上手く動きません。
A8. 入力されたデータのチェックは実施していません。空白が入っていないか確かめてください。
参考イメージ画像
お花大好きボックスの設定画面
基板の写真
上に載せている部品と一部違いますが参考画像として。
黄色は3.3V、黒はGNDで青はBME280のデータ通信用です。
後編へ続く
前編の仕組みで収集したデータを、整理して活用するMachinistについて紹介します。
こちらも合わせてご覧ください。
Maker Faire Tokyo 2019 展示作品:園芸×IoT(後編)可視化ツール「Machinist」