ESP32でWebサーバーをたててAPIを作った話

#はじめに
こんにちは
RHEMS技術研究室バーチャルエンジニアの荒木です。
嘘です。実在してます。

#背景
**なぜか室長が業務用冷蔵庫をオフィスに持ってきました。そしてかなりでかい。**
何言ってるかわからないと思います。安心してください、僕もなぜオフィスに業務用冷蔵庫があるのかわからないです。
で、その冷蔵庫がかなり古いもので取り付けされている温度計が信用ならないということで冷蔵庫内の温度を定期測定+後の活用できるようにデータ溜めを行い故障してないかを確かめる事になりました。
さらに、手元でも現在の庫内の温度が知りたくなったのでESP32でWebサーバーを立てて、現在の庫内温度を返すAPIを作りました。
今回では冷蔵庫側に取り付けるのが構造上困難だったので冷凍庫に設置して測定しました。

#使用した技術・サービス・マイコン等
主に測定にはESP32+[K型熱電対温度センサ](https://www.switch-science.com/catalog/864/)で行っています。
測定データの可視化にはAmbientを用いて、AWSのDynamoDBでデータを貯めています。

今回は主にESP32で温度測定+APIの作成をまとめます。
次回(更新未定)でAmbientやDynamoDBを用いた記事を書きます。


#測定してみる
K型熱電対温度センサはSPIとなっていて通常のシリアル通信、I/Oを用いての測定はできません。
ですがすでにライブラリが公開されているので使わせていただきましょう。
今回はした２つを使用します。
https://github.com/adafruit/Adafruit_BusIO
https://github.com/adafruit/Adafruit-MAX31855-library

これらのライブラリを導入したらAdafruit MAX31855 libraryのserialthermocoupleというサンプルを元にESP32用に書き換えていきます。
25行目~の定義をESP32用に以下に書き換えます。

```c:
#define MAXDO   19
#define MAXCS   5
#define MAXCLK  18
```
ESP32ではSPI接続をする際には使用するピン番号がArduinoとは違うので注意が必要です。

![IMG_2933.HEIX.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269207/05797441-1a9c-5e44-aa22-e72e0c7b41b3.jpeg)

ピンはこんな感じ。

書き換えたら測定できるようになっているはずなので確かめてみてください。

#APIを作ってみる
では実際に測定した温度を返してくれるAPIを作ってみましょう。

```c:ReizoukoAPI
//ライブラリを準備
#include <WiFiManager.h>
#include <WiFiClientSecure.h>
#include <ESPmDNS.h>
#include <SPI.h>
#include "Adafruit_MAX31855.h"

//#define DEBUG

WiFiClientSecure client;
WiFiManager wifiManager;
WiFiServer server(80);

#define MAXDO   19
#define MAXCS   5
#define MAXCLK  18
Adafruit_MAX31855 thermocouple(MAXCLK, MAXCS, MAXDO);

void setup() {
  Serial.begin(115200);

  //3.3Vピンが使えなかったので電源用
  pinMode(22, OUTPUT);
  digitalWrite(22, HIGH);

  //不揮発性メモリを読みに行って設定があればそのままWiFiに接続する。
  //なければアクセスポイントが立ち上がり，設定をしなければならない。
  Serial.println(wifiManager.autoConnect());

  IPAddress ipadr = WiFi.localIP();
  Serial.println(WiFi.SSID());

  if (!MDNS.begin("ESP32Temp")) {
    while (1) {
      delay(500);
    }
  }
  server.begin();
  MDNS.addService("http", "tcp", 80);

  Serial.println("End Setup");
}

void loop() {
  bool flag = 0;
  String buff = "";
  char c;

  Serial.print("Internal Temp = ");
  Serial.println(thermocouple.readInternal());
  double temperature = thermocouple.readCelsius();

#ifdef  DEBUG
  if (isnan(temp)) {
    Serial.println("Something wrong with thermocouple!");
  } else {
    Serial.print("C = ");
    Serial.println(temp);
  }
#endif

  WiFiClient client = server.available();
  if (client) {
    while (client.connected()) {
      //アクセスがあった場合リクエスト内容を1文字ずつ読む
      if (client.available()) {
        while (1) {
          c = client.read();
          buff += c;
          if (c == '\n') break;
        }
        //HTTPのバージョンは気にしないので空白で置き換えて削除
        buff.replace("HTTP/1.1", "");
        buff.trim();

        //メソッドがGETorPOSTのときだけ"200 OK"ないし現在の状態を返す
        //それ以外のメソッドは405を返す
        if (buff.startsWith("GET") or buff.startsWith("POST")) {
          client.println("HTTP/1.1 200 OK");
          client.println("Content-Type: application/json");
          client.println();
          client.println("{\"Temp\":" + (String)temperature + "}");

          client.println();

          buff = "";
          break;
        } else {
          client.println("HTTP/1.1 405 Method Not Allowed");
          buff = "";
          break;
        }
      }
    }
    client.stop();
  }
  delay(1);
}
```

ではコードの説明します。

```c
WiFiServer server(80);
```
これは見ての通り80番(HTTP)ポートを開放しています。
現在ESP32もしくはESP8266ではHTTPしか使えないので仕方なくHTTPを使います。

```c++
Serial.println(wifiManager.autoConnect());
```
ESP32が起動したタイミングでアクセスポイント(以下AP)が立ち上がりWiFiの設定ができます。
なのでプログラム内にSSIDもパスワードを直書きする必要がなくなります。
設定はPCでもスマホでもできて、立ち上がったESP32〇〇~というAPにつなげて少し待つと設定画面が出るはずなので順序どおりに設定します。
また、これは初期起動時もしくは、設定してあったWiFiに接続できなかった場合のみ動きます。
これはWiFiの情報がESP32の不揮発メモリに書き込まれるからで、基本的には一回設定するとその後は設定しなくても大丈夫です。


```c
if (!MDNS.begin("ESP32Temp")) {
    while (1) {
      delay(500);
    }
  }
  server.begin();
  MDNS.addService("http", "tcp", 80);
```
上のif文では名前解決用の設定です。`MDNS.begin()`の引数がサーバーにアクセスする際の名前解決に使われます。
`MDNS.addService("http", "tcp", 80);`これはWebサーバーを立ち上げる際のテンプレになります。


```c
  WiFiClient client = server.available();
  if (client) {
    while (client.connected()) {
      //アクセスがあった場合リクエスト内容を1文字ずつ読む
      if (client.available()) {
        while (1) {
          c = client.read();
          buff += c;
          if (c == '\n') break;
        }
        //HTTPのバージョンは気にしないので空白で置き換えて削除
        buff.replace("HTTP/1.1", "");
        buff.trim();

        //メソッドがGETorPOSTのときだけ"200 OK"ないし現在の状態を返す
        //それ以外のメソッドは405を返す
        if (buff.startsWith("GET") or buff.startsWith("POST")) {
          client.println("HTTP/1.1 200 OK");
          client.println("Content-Type: application/json");
          client.println();
          client.println("{\"Temp\":" + (String)temperature + "}");

          client.println();

          buff = "";
          break;
        } else {
          client.println("HTTP/1.1 405 Method Not Allowed");
          buff = "";
          break;
        }
      }
    }
    client.stop();
  }
```
ここではHTTPリクエストを処理しています

```c
if (client.available()) {
        while (1) {
          c = client.read();
          buff += c;
          if (c == '\n') break;
        }
        //HTTPのバージョンは気にしないので空白で置き換えて削除
        buff.replace("HTTP/1.1", "");
        buff.trim();

```
このへんでリクエスト分を読みます。
今回はヘッダーの最初の一文だけが必要になるので改行が来るまで読んで、メソッドのみを取り出します。
本来ならGETリクエストで返す必要はないのですが通常のブラウザでも呼び出せるようにGETとPOSTでのリクエストで返すようにしています。
また、GET,POST以外のメソッド(DELET,PUTとか)は405(Method Not Allowed)を返すようにしています。



あとはコメントの通りになります。

では実際に動かして見ましょう。

<img width="1092" alt="スクリーンショット 2020-02-29 19.01.12.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269207/d524d913-52cc-002d-bc33-c786f7c2a6bc.png">


うん、いい感じですね。


#さいごに
センサーの部分もWiFiの自動接続もWebサーバーの部分もそれだけ抜き取って使えるものになっています。

これらが参考になると僕は嬉しいです。
~~(参考になったらいいねしてほしいなぁ)~~