ESP32で百葉箱IoT

#ESP32で百葉箱IoTを作った
近年の暑さは異常で、それに伴い豪雨なども各地で発生しており、気温というものに関心が向いてしまいます。
私の場合、自室の温度が32℃を超えたあたりからクーラーをつけようかなという基準を設けており、暑いと感じたらまず気温を確認し、基準値以上であればクーラー、それ以下なら扇風機を付けるように心がけています。

この記事では、昨今のIoTブームに乗っかり、マイコンESP32と温湿度センサーDHT11を用いた百葉箱IoTを作る手順を示します。
もしあなたが夏にこの記事を読んでいるなら、熱中症対策にもうってつけです。

基本的に初心者向けに、詳しめに画像を多く用いて説明を行います。

## 全体構成
- 温湿度センサー：DHT11
- マイコン：ESP32
- 記録：Google スプレッドシート（GAS経由）

以上が使用部品です。

ざっくり言えば、温湿度センサーの情報をマイコンが拾って、それをGASに投げることで、スプレッドシートに書き込んでいく感じです。
今回は、30分おきに計測・記録します。
具体的には次のようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/317044/24172c9b-f4b3-c4de-a712-d98c272a77b2.png)

手順は2つで、GASの準備と、ESP32の準備となります。

### ESP32について
マイコンです。
電子工作を行う人にとって最も入門者に提示されるマイコンは、おそらくArduinoと呼ばれるものです。
Amazonなんかでもクローン品が大量に出回り、かなり安価に購入することができ、Raspberry Piの初期コストに苦心する必要がないのがうれしいです。

ESP32は、Arduinoと似たようなもので、特徴はWiFI、Bluetooth、タッチセンサーが初めから搭載されていることです。

Arduinoの場合、Ethernetシールドと呼ばれる、いうなれば拡張パーツみたいなものをドッキングさせたりして、インターネットへ接続することが多いと思いますが、ESP32ではWiFiを用いることができるので、IoTなどインターネットへの接続を行う場合はESP32の方が簡単に実装できます。

ESP32を基板実装して、開発ボードとして販売しているものを購入するのが良いと思います。
実装には種類があり、ピンアサインも異なってきますから、購入時にどの種類なのかを確認しましょう。
私の場合はAliExpressで、確か800円程度で購入しましたが、海外から購入する場合は技適マークの有無を確認するとよいでしょう。

### DHT11について
安価な温湿度センサーの代表格です。
精度があまりよろしくありませんが、まとめて買えば一つ100円未満で購入することもできるかと思います。
私は複数いらないので、近くの電子パーツ店で300円くらいで売ってるのを見て驚きながら買いました。
[秋月電子](https://akizukidenshi.com/catalog/g/gM-07003/)でも同様の値段ですね。

ピンアサインなどは上記の秋月電子のサイトからも見れるデータシートなどを見るとわかるかと思います。

足が4本生えていますが、電源、データ、グランドの3本のみしか使用せず、一つはあまりだそうです。
モジュールとして足が3つだけになってるやつもありますが、中身としてはほぼ同じで、違いとしてはプルアップ抵抗が内蔵されているようです。
データ出力はディジタルでシリアル通信によって行われます。
計算してもいいかもしれませんが、ライブラリが出てるのでそちらを使うのが賢明でしょう。

### GASについて
GASはGoogle Apps Scriptの略で、Googleのサービスの拡張・自動化や、サービス間連携などをJavaScriptで記述することができるプラットフォームです。

今回、百葉箱と銘打ったものの、記録にはロールペーパーの記録計を用いるわけにもいきませんから、グーグルのスプレッドシートに書き込んでいこうかと思います。
後で気温の推移などをグラフにしたいときなどにも役立つかと思います。

## 手順1：GAS側の準備
[Google スプレッドシート](https://docs.google.com/spreadsheets/u/0/)から、空白のブックを作成します。
このとき、URLが「 https://docs.google.com/spreadsheets/d/ユニークなID/edit#gid=0 」となっていると思いますので、コピーしておきます。

最上部の「無題のスプレッドシート」から、適当に名前を変えて、「ツール＞スクリプトエディタ」と辿ります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/317044/2660fb5c-e048-a4ea-1db5-4bdec34e5907.png)

myFunctionという名前の関数があらかじめ作成されていると思いますが、そんなナンセンスな関数いらないので、サクッと全部消します。
そこへ、次のdoGet関数を記述します。

```javascript:コード.gs
function doGet(e) {
  const url = "さっきコピーしたURL";
  const ss = SpreadsheetApp.openByUrl(url);
  const sheet = ss.getSheets()[0];
  const params = {
    "timestamp": new Date(),
    "temperature": e.parameter.temperature,
    "humidity": e.parameter.humidity
  };
  sheet.appendRow(Object.values(params));
  return ContentService.createTextOutput('sccess');
}
```

その後、保存し、プロジェクトの名前を適当に決めます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/317044/120cef06-9bb4-8bdc-3f1d-f82a140d45cc.png)

このコードはシンプルなので、新しい機能などを追加したり、エラーハンドリングしたい場合があると思いますので、初心者向けに最後にちょっとした解説をいれておきますので、興味があれば一読ください。

次に、「公開＞ウェブアプリケーションとして導入...」と辿り、何も考えないでそのまま「Deploy」ボタンを押します。
初めは権限がないので、許可を求められますから、「許可を確認」から認めてあげます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/317044/abf23296-72ae-1fe4-2b17-85876dc4a2e4.png)

自分が作ったプロジェクトなので、あなたがGoogleに認められた開発者でない場合は「このアプリは確認されていません」とでます。左下の「詳細」をクリックし、「[プロジェクト名] （安全はでないページ）に移動」をクリックして、ようやく「許可」ボタンをクリックできます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/317044/8d291e07-2c5d-a127-adb3-09104b406b81.png)

許可の後、スクリプトエディタに「Deploy as web app」というモーダルウィンドウがでます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/317044/fea70bcc-ec2e-68c2-fd17-17820b866a64.png)
そのなかの「Current web app URL:」の下にある
「 https://script.google.com/macros/s/プロジェクトのid/exec 」というのが、今回公開したウェブアプリケーションのURLです。これをコピーしメモ帳などにメモっておきます。

試しに別のタブなどから「 https://script.google.com/macros/s/プロジェクトのid/exec?temperature=28&humidity=50 」へアクセスしてみてください。先ほどコピーしたURLの末尾にクエリを付加したものです。 
successと殺風景なページが出れば成功です。
このとき元のスプレッドシートの1行目には、日付、28、50と追加されています。

分かりにくいので、一行目の内容を消して、「日付」、「気温」、「湿度」などとしておけば分かり易いと思います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/317044/24172c9b-f4b3-c4de-a712-d98c272a77b2.png)

末尾の行にどんどん追記していくプログラムなので、1行目にこんなものがあってもちゃんと動きます。

これで、GAS側の準備は完了です。
「 https://script.google.com/macros/s/プロジェクトのid/exec 」のメモを忘れずに。

## 手順2：ESP32側の準備
さて、ESP32側の準備に入ります。

### 回路構成
回路図と、写真で確認してください。
温湿度センサーとのシリアル通信にはGPIO32を用いています。GPIO34だとなぜか動かなかったという経緯があります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/317044/06c61fe1-bf88-a74a-00b8-4c9f2f3d3453.png)

![exif-removed.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/317044/54749ae0-1a84-e2dd-7e0c-9dcf7b9b4e2c.jpeg)

DHT11周りは、正面左から$\rm V_{cc}=5[\rm V]$、$\rm data$、なし、$\rm GND$の順につなげています。
モジュール版でない場合、プルアップ抵抗を接続しておくと安定するそうなので、$\rm V_{cc}$と$\rm data$
の間に$10[\rm k\Omega]$の抵抗をつないでいます。

なお、図と写真では電気的には同じですが、配線やボードの種類がわずかに違います（同じボードの図がなかったので）。

### プログラム作成
USBなどでPCとESP32を接続します。
Arduino IDEをインストールしていない場合はインストールしてください。している前提で話を進めます。
[Arduino core for ESP32](https://github.com/espressif/arduino-esp32)の導入も、長くなるのでここでは説明しないので、「ESP32 ArduinoIDE」などで検索し、各自設定を終えてください。
「ツール＞ボード」から、自分の購入した種類の開発ボードが選択できない人は上記手順を行っていません。

DHT11を使用するので、そのライブラリを「スケッチ＞ライブラリをインクルード＞ライブラリを管理...」またはWindowsなら「Ctrl + Shift + I」でライブラリマネージャを開き、「dht」と検索し「DHT sensor library」をインストールします。（場合によっては「DHT sensor library for ESPx」の方がいい場合があると思いますので、適宜読み替えてください。）
使用するコードは以下になります。

```c:DHT11_log.ino
#include <SPI.h>
#include <WiFi.h>
#include <DHT.h>
#include <HTTPClient.h>

const int PIN_DHT = 32;
DHT dht(PIN_DHT, DHT11);
hw_timer_t * timer = NULL;
WiFiServer server(80);
const String host = "https://script.google.com/macros/s/プロジェクトのID/exec";
TaskHandle_t taskHandle;
 
void responseHTTP (WiFiClient client, float temperature, float humidity) {
  boolean currentLineIsBlank = true;
  
  while (client.connected()) {
    if (client.available()) {
      char c = client.read();
      Serial.write(c);
      if (c == '\n' && currentLineIsBlank) {
        client.println("HTTP/1.1 200 OK");
        client.println("Content-Type: text/html");
        client.println("Connection: close");
        client.println();
        client.println("<!DOCTYPE HTML>");
        client.println("<html>");        
        client.println("<h1>ESP32 is running</h1>");
        client.println("<p>Measuring temperature and humidity with DHT11 now...</p>");
        client.print("<p>temperature: "); client.print(temperature); client.print(" *C, ");
        client.print("humidity: "); client.print(humidity); client.print(" %</p>");
        client.println("</html>");
        break;
      }
      if (c == '\n') {
        currentLineIsBlank = true;
      } else if (c != '\r') {
        currentLineIsBlank = false;
      }
    }
  }
}

void recordDHT11(float temperature, float humidity) {
  HTTPClient http;
  http.begin(host + "?temperature=" + temperature + "&humidity=" + humidity);
  int status_code = http.GET();
  Serial.printf("get request: status code = %d\r\n", status_code);
  http.end();
}

void task1(void *pvParameters) {
  while (1) {
    if (xTaskNotifyWait(0, 0, NULL, pdMS_TO_TICKS(0)) == pdTRUE) {
      float temperature = dht.readTemperature(false);
      float humidity = dht.readHumidity();
      if (isnan(temperature)) {
        delay(2000);
        temperature = dht.readTemperature(false);
        humidity = dht.readHumidity();
      }
      recordDHT11(temperature, humidity);
    }
    delay(1);
  }
}

void onTimer(){
  BaseType_t taskWoken;
  xTaskNotifyFromISR(taskHandle, 0, eIncrement, &taskWoken);
}

void setup() {  
  Serial.begin(115200);
  dht.begin();
  pinMode(2, OUTPUT); // on-board LED
  
  // Connect to WiFi
  const char* ssid = "ssid";
  const char* pass = "password";
  digitalWrite(2, LOW);
  Serial.print("Connecting to ");
  Serial.print(ssid);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("connected");
  digitalWrite(2, HIGH);
  
  server.begin();
  Serial.print("server: ");
  Serial.println(WiFi.localIP());
  
  xTaskCreateUniversal(
    task1,
    "task1",
    8192,
    NULL,
    1,
    &taskHandle,
    PRO_CPU_NUM
  );
  
  timer = timerBegin(0, 80, true); // 1us
  timerAttachInterrupt(timer, &onTimer, true);
  timerAlarmWrite(timer, 18e8, true);
  timerAlarmEnable(timer);
}


void loop() {
  WiFiClient client = server.available();
  if (client) {
    Serial.println("client connected");
    float temperature = dht.readTemperature(false);
    float humidity = dht.readHumidity();
    responseHTTP(client, temperature, humidity);
    recordDHT11(temperature, humidity);
    delay(1);
    client.stop();
    Serial.println("client disconnected");
  }
  delay(2000);
}
```

上記コードをそのままコピペしても動きません。
あなたの環境に合わせて、メモってあるはずの、GASのウェブアプリケーションURLの部分、setup関数内のWiFiのssidとパスワード、の合計3か所を書き換えてください。
それを記述し、転送してください。
私の開発ボードでは、転送するときに、「BOOT」ボタンを書き込み開始まで長押ししなければ書き込みが始まらないので、参考にしてください。

こちらのコードも最後に解説をつけますから、興味があればご一読ください。

もし、上記コードでコンパイルが通らない場合、ESP32の使用環境が整っているか、ボードはあっているか、ピンアサインはどうか、DHT11のライブラリをインストールしたか、などを確認してください。
コンパイルは通るが、動作しない場合は、主に回路を見直すのがいいと思いますが、シリアルポートを選択し忘れたりしてないか確認してください。特に、データ通信のできないUSBを使っていたりしたりして、かなりの時間を無駄にしがちなので気を付けてください。

モジュール版のDHT11の場合、ピンアサインが異なる場合もあります（裏面を見て結線を確認するといい）し、開発ボードの種類によっては「DHT sensor library for ESPx」でないと動かない場合もあり、そのときは、上記コードを多少変更してください。DHTesp.hをインクルードしたりセットアップが違ったり、温度と湿度を同時に読んだりする違いがあったはずです。

以上でESP32側も準備完了です。

## 実際に動作させる
といっても、以上の手順を踏んだ段階で、動作は始まっているはずです。
シリアルモニタから接続状況などを確認できます。また、WiFiの接続成功時に開発ボード上の青色のLEDを点灯させていますから、PCなどに接続していない場合でもそこで確認できます。

`timerAlarmWrite(timer, 18e8, true);`の`18e8`を`1e7`などにすれば、10秒おきに記録を始めるはずなので、スプレッドシートを開きながら動作させると分かり易いと思います。
なお、スプレッドシートはリアルタイムで更新されます。

DHT11では上下に2℃までの誤差が出ます。正直かなり誤差が大きい部類なので、初心者向けにちょっとしたIoT体験をするには構いませんが、誤差のことを理解していてください。
もう少し正確なのが欲しければ、他の（例えばDHT22なんかは同じように使えます）温湿度センサーを検討しましょう。

シリアルモニタに、現在ESP32の割り当てられたIPアドレスを表示していますので、ブラウザからそのIPアドレスで表示すると、気温と湿度が見られるはずです。非同期サーバではないので、気温と湿度の情報は勝手には更新されません。自分でブラウザから更新して下さい。
そのとき、同時にスプレッドシートにも追記するようにしてありますから、更新を連打するとスプレッドシートが大変なことになると思います。

正常に動作していることを確認したら、もし変更した部分があれば元に戻して、気温の測りたいところに放置しましょう。
ただし、火事には気を付けてください。消費電力はそんなに高くはないはずですが、燃えるものを近くに置かないように（とくに埃とか、、、）

その後は、例えばある一定の温度以上でどこかに通知してみたり、LEDを点灯させてみたりできると思います。

お疲れ様でした。

## 補足：プログラム解説
### GAS
```javascript:コード.gs
function doGet(e) {
  const url = "さっきコピーしたURL";
  const ss = SpreadsheetApp.openByUrl(url);
  const sheet = ss.getSheets()[0];
  const params = {
    "timestamp": new Date(),
    "temperature": e.parameter.temperature,
    "humidity": e.parameter.humidity
  };
  sheet.appendRow(Object.values(params));
  return ContentService.createTextOutput('sccess');
}
```
まず、関数`doGet`とは、ウェブアプリケーションのURLに対してhttpのgetリクエストが来たときに実行される関数です、この名前自体はGAS側が決めているので特に深くは考えなくてよいでしょう。postリクエストであれば`doPost`になります。

3行目

```javascript
const ss = SpreadsheetApp.openByUrl(url);
```
では、スプレッドシートを開いています。
GASからスプレッドシートを操作するためには、GAS側にスプレッドシートの情報を与えないといけません。今回の方法で作成したコードは、スプレッドシートと紐づいていないため、URLからその情報を与えています。
urlの一部分がidとなっていて、そこだけを取り出して、`SpreadsheetApp.openById(id)`とすることもできます。

4行目

```javascript
const sheet = ss.getSheets()[0];
```
では、3行目で開いたスプレッドシートのブックから、該当するシート（下部にあるシート1というやつ）を取得するものです。
今回は、すべてのシートを配列として取得し、一番初めのものを添え字0で取り出しています。
名前から取得するときは、`getSheetByName(name)`となります。

詳しい情報は当然Googleがリファレンスを出していて、 https://developers.google.com/apps-script/reference/spreadsheet/ から確認できます。

5～9行目

```javascript
const params = {
  "timestamp": new Date(),
  "temperature": e.parameter.temperature,
  "humidity": e.parameter.humidity
};
```
は、追加するデータを、オブジェクトとして作成しています。
Dateは時刻を扱うコンストラクタですが、文字列に暗黙の変換が可能です。
温湿度については、クエリ（ ...?temperature=xx&humidity=xx　の部分）から取得することになっていましたので、それを取り出しています。クエリで与えられた変数の情報は、`doGet`の引数に与えられ、今回はそれを`e`という変数名で受けています。その中の`parameter`以下にクエリの変数の情報が格納されています。

10行目
```javascript
sheet.appendRow(Object.values(params));
```
では、末尾の行に先ほどのデータの、値の部分だけを追加しています。
`Object.values()`は、与えられたオブジェクトの値のみを配列として取り出すものです。

11行目はgetしたときに返す情報を決定しています。今回は文字列`"sccesss"`です。

### ESP32
```c:DHT11_log.ino
#include <SPI.h>
#include <WiFi.h>
#include <DHT.h>
#include <HTTPClient.h>

const int PIN_DHT = 32;
DHT dht(PIN_DHT, DHT11);
hw_timer_t * timer = NULL;
WiFiServer server(80);
const String host = "https://script.google.com/macros/s/プロジェクトのID/exec";
TaskHandle_t taskHandle;
 
void responseHTTP (WiFiClient client, float temperature, float humidity) {
  boolean currentLineIsBlank = true;
  
  while (client.connected()) {
    if (client.available()) {
      char c = client.read();
      Serial.write(c);
      if (c == '\n' && currentLineIsBlank) {
        client.println("HTTP/1.1 200 OK");
        client.println("Content-Type: text/html");
        client.println("Connection: close");
        client.println();
        client.println("<!DOCTYPE HTML>");
        client.println("<html>");        
        client.println("<h1>ESP32 is running</h1>");
        client.println("<p>Measuring temperature and humidity with DHT11 now...</p>");
        client.print("<p>temperature: "); client.print(temperature); client.print(" *C, ");
        client.print("humidity: "); client.print(humidity); client.print(" %</p>");
        client.println("</html>");
        break;
      }
      if (c == '\n') {
        currentLineIsBlank = true;
      } else if (c != '\r') {
        currentLineIsBlank = false;
      }
    }
  }
}

void recordDHT11(float temperature, float humidity) {
  HTTPClient http;
  http.begin(host + "?temperature=" + temperature + "&humidity=" + humidity);
  int status_code = http.GET();
  Serial.printf("get request: status code = %d\r\n", status_code);
  http.end();
}

void task1(void *pvParameters) {
  while (1) {
    if (xTaskNotifyWait(0, 0, NULL, pdMS_TO_TICKS(0)) == pdTRUE) {
      float temperature = dht.readTemperature(false);
      float humidity = dht.readHumidity();
      if (isnan(temperature)) {
        delay(2000);
        temperature = dht.readTemperature(false);
        humidity = dht.readHumidity();
      }
      recordDHT11(temperature, humidity);
    }
    delay(1);
  }
}

void onTimer(){
  BaseType_t taskWoken;
  xTaskNotifyFromISR(taskHandle, 0, eIncrement, &taskWoken);
}

void setup() {  
  Serial.begin(115200);
  dht.begin();
  pinMode(2, OUTPUT); // on-board LED
  
  // Connect to WiFi
  const char* ssid = "ssid";
  const char* pass = "password";
  digitalWrite(2, LOW);
  Serial.print("Connecting to ");
  Serial.print(ssid);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("connected");
  digitalWrite(2, HIGH);
  
  server.begin();
  Serial.print("server: ");
  Serial.println(WiFi.localIP());
  
  xTaskCreateUniversal(
    task1,
    "task1",
    8192,
    NULL,
    1,
    &taskHandle,
    PRO_CPU_NUM
  );
  
  timer = timerBegin(0, 80, true); // 1us
  timerAttachInterrupt(timer, &onTimer, true);
  timerAlarmWrite(timer, 18e8, true);
  timerAlarmEnable(timer);
}


void loop() {
  WiFiClient client = server.available();
  if (client) {
    Serial.println("client connected");
    float temperature = dht.readTemperature(false);
    float humidity = dht.readHumidity();
    responseHTTP(client, temperature, humidity);
    recordDHT11(temperature, humidity);
    delay(1);
    client.stop();
    Serial.println("client disconnected");
  }
  delay(2000);
}
```

長いので大事なところだけ説明します。
IPアドレスをhttpから叩いたときに、温湿度情報を表示するため、`responseHTTP`という関数を作成し、`loop`で実行しています。
通常であれば`loop`内でdelay(30分)にすればいいのですが、その場合httpから叩くときに応答できなくなるので、マルチスレッド処理とタイマーによる通知によって実現しています。

タイマーを使った割り込みだと、温湿度の読み取りからgetリクエストなどの処理が比較的重いため、パニックになります。しかし、ESP32はデュアルコアのため、スレッドを分け、タイマーからの通知によって実行させています。

`timerBegin`によって、タイマーの間隔は、動作周波数80MHzと、分周比80から1$\rm \mu s$になっており、通知は、$18 \times 10^8$回後ということになります。これによって30分間隔でタイマーによる割り込みが発生し、そのたびに別スレッドで待機している`task1`に通知され、通知を受けて測定・記録を実行しています。

肝心な記録部分は`recordDHT11`という関数名で作ってあります。
`HTTPClient`にurlを`begin`で登録し、`GET`メソッドで実際にgetリクエストを行っています。
一応、シリアルモニタにステータスコードを表示していますが、リダイレクトになるため、302ばかり帰ってくると思います。

簡単ですが説明を終えさせていただきます。
似たような感じで他のセンサーなども使えると思います。ぜひ電子工作やIoTを始めてみてはいかがでしょうか。

また、開発ボードは基盤むき出しですので、くれぐれも火事だけは気を付けてください。
埃が少ない綺麗な場所かつ、WiFiの電波が届くところで使用してください。