気温・湿度・気圧センサーのBME280と気温・湿度センサーのAHT25を使って、室内と室外の環境をモニターするシステムを作りました。 センサーで測定したデータはESP32で定期的にサーバーに送られて、Webブラウザで表示します。
昔購入したBME280が部品箱で埃をかぶっていましたが、ESP32が電池で思いのほか長時間駆動できることが分かったので作ってみました。 室内と室外の両方で測定したいと思いましたが、昔送料込みで$3.2だったBME280が値上がりしていたので、気圧は測定できないけれど廉価なAHT25を買い足しました。 このような環境モニター(weather station)はESP32の工作例としてありがちであまり興味がなかったのですが、実際に使ってみるとエアコンの効き具合や就寝時の気温の変化などが分かっておもしろいと思いました。
センサーから収集したデータを蓄積して表示するにはサーバーが必要になります。 いくつかそれに適したクラウドサービスもあるようですが、柔軟性などを考慮して自宅で24時間稼働しているLinuxサーバーを使うことにしました。
前述のとおりマイコンにはESP32を使用し、センサーにはBME280とAHT25を使用しました。 マイコンもセンサーも動作可能電圧が広いので(BME280は1.8-3.6V、AHT25は2.2-5.5V)、単三電池2本で直接駆動します。 回路はとても簡単なので、回路図は描かずに下記の配線対応表を用意しました。 ESPはESP32で、SENはBME280とAHT25を表します(どちらのセンサーもVCC、GND、SCL、SDAの4つの端子があります)。 PINは、ファームウェア書き込み時にテストクリップを挟むために短いリード線を取り付けます。 センサーのVCCは直接電源につながずに、ESP32のIO23から供給します。 配線表には書かれていませんが、ESP32のVCCには0.1μFのパスコンを付け、ENは10kΩでプルアップします。 またESP32のピーク電流に対応するために、電池ボックスには1000μFの電解コンデンサーを取り付けます。
ESP:IO23 | SEN:VCC |
ESP:GND | SEN:GND |
ESP:IO22 | SEN:SCL |
ESP:IO21 | SEN:SDA |
ESP:GND | GND |
ESP:3V3 | VCC |
ESP:EN | PIN:EN |
ESP:IO0 | PIN:IO0 |
ESP:TXD | PIN:TXD |
ESP:RXD | PIN:RXD |
野外に置くセンサーは防水ケースに入れました。 センサーの部分だけ穴を開けて、防水のためにまわりをホットメルトで固めました。
ケースに入れて完成した状態を下に示します。
ファームウェアはArduino IDEで開発しました。 起動すると、まずセンサーで計測を行い、Wi-Fiで結果をサーバーに送って、10分間ディープスリープします(スリープ時間は設定用モードで変更可能)。 バッテリー電圧も、以前行ったのと同様に非公開関数のrom_phy_get_vdd33()を使って測定します。 10分毎に動作するようにしたので、単三電池2本だと1年も持たないかもしれません。 気温が低い屋外で使用した場合アルカリ電池の容量は3割ほど低下するようなので、動作時間はさらに短くなるかもしれません。 低電圧で動作させるため、ファームウェアの書き込み時にFlash Frequencyは40MHzとしています。
ESP32は磁気センサーを内蔵しています。 磁石を近づけて本機をリセットすると設定用モードで起動します。 SoftAPでアクセスポイントになるため、SSIDを"ESP32"、パスワードを"12345678"としてスマホやタブレットで接続して、ブラウザで"http://192.168.0.1/"にアクセスします。 するとフォーム画面が表示されて、任意のkeyとvalueをESP32に登録することができます。 本機では計測したデータをサーバーにアップロードするために、下記のkeyを設定する必要があります。
key | 内容 |
SSID | Wi-Fi接続のSSID |
PASS | Wi-Fi接続のパスワード |
CHAN | Wi-Fi接続のチャンネル |
ADRS | 本機のIPアドレス(例: 192.168.1.50) |
GATE | 本機のゲートウェイ(例: 192.168.1.1) |
MASK | 本機のサブネットマスク(例: 255.255.255.0) |
HOST | データ送信先ホスト(例: 192.168.1.2) |
WAIT | 測定間隔(単位は分で実数) |
ESP32が搭載しているULPコプロセッサにも興味があり、スリープ中にULPでセンサーを動かすことも考えましたが、今回はサンプリングの頻度は必要ないので使いませんでした。
BME280を使った方は、1年ほどして突然-143℃という気温を常に返すようになりました。 海外のサイトで似たような報告があったので、どうやらBME280の不具合のようです。 とりあえずセンサーを新しい物に交換して修理しました。 また押入れの湿度も監視したいと思い、AHT25を使ったセンサーを後日もう一つ作りました。
その不具合とは別に、低電圧で動かし続けていたところESP32が1つ壊れてしまいました。 プログラムの先頭で"WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0)"として低電圧を検出してリセットする保護機能を無効にしていますが、それにより低電圧での動作中に暴走してフラッシュメモリ等が書き換えられた可能性があります。 そこで、電池の電圧が2.30Vを下回ったらDeep-sleepモードよりもさらに消費電流の少ないHibernationモードで永遠にスリープさせることにしました。 rom_phy_get_vdd33()関数による電圧測定はノイズが多いので、連続で10回2.30Vを下回った場合にスリープさせています。
センサーで収集したデータを蓄積したり、またそれらをWebブラウザで表示するためのCGIをPythonで書きました。 1つのCGIスクリプトで、収集と表示のどちらにも対応します。 つまり、POSTメソッドでデータが送信された場合はデータの蓄積を行い、それ以外の場合は計測結果をグラフにしてWebページを返します。 Apache2では、デフォルトでCGIスクリプトは/usr/bin/cgi-bin/に置くことになっているので、このwst.cgiというスクリプトをそこに置きます。 さらに、wst.lck (ロックファイル)、wst.log (ログファイル)、wst.dat (データファイル)という3つの空ファイルをtouchコマンドなどで同じディレクトリに作成して、CGIスクリプトから変更できるように所有者をwww-dataにしておきます。 ログファイルには過去の全ての計測結果が含まれており、データファイルにはWebブラウザにグラフを表示するのに必要なだけの計測結果が含まれています。 これらの2つのファイルに複数のプロセスから同時に書き込みを行わないために、ロックファイルを使って排他制御を行います。
グラフの描画にはmatplotlibを使いましたが、かなり遅くてページの表示に4秒程度かかります。 表示する情報は、1日と1週間における気温・湿度・気圧・電池電圧の変化で、合計8つのグラフを生成します。 これらのグラフはbase64エンコーディングでHTMLページに埋め込むため、1回のHTTPアクセスでページ描画が完了します。
数ヶ月使用しましたが、電池が1ヶ月も持たずに何度か交換が必要になりました。 そこで電池の持ちをよくするためにいくつかの改善を行いました。 ESP32は無線使用時の消費電流が大きく、センサーの消費電流などはそれに比べれば無視できるほど少ないので、WiFiの使用時間をいかに減らすかが重要になります。 色々試した結果、以下の変更により大きな効果が見られました:
現状で、無線LANの接続に200ms、HTTP転送に500ms程度で、WiFiの使用時間は700ms程度となっています。 HTTPの転送が一番のボトルネックなので色々と試しましたが改善できませんでした。 データの転送自体ではなく、WiFiClient::connect()内でネットワークソケットを作りselect()するあたりで400ms程度の時間がかかっているようです。 なお、時間がかかるのはWiFi接続後の最初のHTTPセッションだけで、複数回HTTPによる転送を行っても2回目以降は30ms程度しかかかりません。
これ以上電池の持ちをよくするには、手っ取り早いのはデータの送信間隔を10分よりも長くすることです。 DHCPサーバーやDNSサーバーを使わない場合も試しましたが、大きな効果は見られませんでした。 今回は全て自宅内のサーバーと通信していますが、外部サーバーとの通信だとさらに遅延が大きくなる可能性があります。
上のスクリーンショットは、Webブラウザで測定結果を表示した例です。 測定したのは冬場ですが、エアコンを消して寝ると屋内の気温が指数関数的減衰をして、湿度は逆に増加していく様子が分かります。 屋内の測定は、センサーを置く場所によっても結果がかなり変わります。 サーキュレーターの置き場所を変えるとその効果が分かり役に立ちました。 屋外のセンサーは、雨が降り始めると湿度が急に上がります。 2つのセンサーを同じ場所に置いた場合は、気温の差は最大で0.2ポイントでとても近い値が得られましたが、湿度は2ポイント程度の差が生じました。
前述のとおり電池を長持ちさせる対策をいくつか行いましたが、やはりHTTPでの通信に時間がかかることが気になっていました。 そこでハンドシェイクが必要でオーバーヘッドの大きいTCPではなくUDPで通信することにして、WiFi使用時間をさらに減らすことにしました。
まず、自宅のLinuxサーバーでrsyslogdが動いているので、/etc/rsyslog.confを以下のように編集してUDPの514番ポートで受けとったデータを/var/log/wst.logというファイルに保存するようにします。
module(load="imudp") input(type="imudp" port="514" ruleset="wst") ruleset(name="wst") { action(type="omfile" file="/var/log/wst.log") }
サーバーへ送信するUDPのメッセージはとてもシンプルで、以下のASCII文字列です:
wst (ステーションID),(温度),(湿度),(気圧),(電池の電圧),(前回のWiFi通信時間)
wstというのはログファイルに記録されるホスト名で、測定結果の数値をカンマで区切ったデータを送信します。 syslogで使われるBSD形式のプロトコルはRFC3164で規定されていますが、ここでは省略可能なフィールドを省いています。 例えば送信時刻は省略しているので、ログ出力時にサーバー側でタイムスタンプが追加されます。
ESP32からUDPパケットを送信してもサーバー側になぜか届かないという問題に悩まされて、対策に苦労しました。 ESP32のネットワーク処理に使われているライブラリのlwIPには欠陥があり、UDP送信用の関数を呼ぶと即座に成功のステータスが返されて、実際にパケットが送信されたのかどうかを確認する手段がありません。 そのためendPacket()関数を呼び出した後ですぐにネットワークを切断してスリープするとデータが送信されません。 UDP通信ではパケットが途中で失われる可能性がありますが、この場合はそもそもパケットが生成すらされません。 endPacket()の後に50msほどのdelay()関数を入れるとほぼ問題が起こらなくなりますが、パケット送信に要する時間はARP応答が返る時間などに依存するため、待つ時間が短すぎると送信に失敗しますが余裕を持たせて長くするとWiFi使用時間が無駄に長くなります。 lwIPの低レベル関数やハードウェアのレジスタを使うなど色々試したけれど確実な方法はなく、とりあえずUDPの送信前にARP要求を行いARP応答を待った上で、UDP送信後に10msの待ち時間を入れるという対策を行いました。
もう一つ悩まされたのがArduino Core for ESP32のバージョンです。 最新版の2.0.9を試した場合にWiFi接続の確立に2,000ms程度要して遅すぎて話にならなかったので、1.0.6に戻しましたが相変わらず遅いままでした。 そこで1.0.5にしたら高速に接続できるようになりました。 前述のとおり、WiFi接続時間を短縮するためにまずWiFi.persistent(true)とWiFi.begin(SSID, PASS, CHANNEL)で最初に接続して、以降は"WiFi.begin()"で接続しますが、最初のSSID等を指定した接続を1.0.5で行うと後の引数を省略した接続を1.0.6で行っても高速に接続できるため、1.0.6で問題ないと思い込んでいたようでした。 なお1.0.5のような古いバージョンでホールセンサーを使う場合はreadHall()関数は正常に動作しないため、代わりにhall_sensor_read()関数を使う必要があります。
少しでも通信を削減するために、DHCPもDNSも使わないでIPアドレスを直接指定しています。 今回の改善により、WiFi使用時間は平均で150〜300ms程度と以前の半分以下になりました。 バッテリーの消耗を減らすことができたため、データの送信間隔を10分から5分に縮めました。
CGIを動かしているLinuxサーバーが非力なこともありますが、CGIのページがアクセスされてからHTMLを生成するのに3秒以上かかっていたので高速化することにしました。 CGIが遅い一番の理由はグラフ描画のためにmatplotlibを使用していることで、パッケージのimportだけで1秒以上かかっています。 そのため、Pillowを直接使ってグラフ描画を行うライブラリを自作しました。
グラフの軸にラベルを描画するにはフォントが必要ですが、汎用的なフォントデータを読み込むだけでも時間がかかります。 そのため必要最小限のコンパクトなTrueTypeフォントを用意しました。 SIL Open Font Licenseで公開されているLiberation Sansフォントから、FontForgeを使って数字といくつかの記号のフォントを抽出し、base64でエンコードしてCGIスクリプトに埋め込んでin-memoryファイルとして使っています(変換用のコード)。
この改良の結果、HTMLの生成が0.4秒以内で終わるようになりました。