ESP32をWebサーバにしてGroveセンサーを制御してみる

最近、「[ESP32 & Arduino 電子工作プログラミング入門](https://www.amazon.co.jp/dp/4297112051)」という本を購入したため、復習の意味を兼ねて、ESP32をWebサーバにして、Groveセンサーを制御するプログラムを作ってみます。

本の中では、「第5章 各種のライブラリを使う」のところに「ESP32をWebサーバとして動作させる」という章があり、ここを参考にしながら進めていきます。
ここではスピーカーと接続して音を鳴らすサンプルになっていますが、今回はGroveセンサーの[LED](https://wiki.seeedstudio.com/Grove-Red_LED/)をON/OFFしたいと思います。

# 環境
今回は、[ESP32-DevKitC](https://www.espressif.com/en/products/modules/esp-wroom-32/)を使用します。
開発環境は[Arduino IDE](https://www.arduino.cc/en/main/software)を使用します。

# ハードウェア
まず、LEDのピンは以下のようになっています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130771/96cc5ae4-208b-9f63-f15f-dcb3a0c843e3.png)

[こちら](https://ht-deko.com/arduino/esp-wroom-32.html)を参考にして、それぞれを、ESP32-DevKitCのピンに割り当てます。

| Seeeduino | Grove-Red Led | ESP32-DevKitC |
|:---:|:---:|:---:|
| 5V | Red | 5V |
| GND | Black | GND | 
| Not Conencted | White | Not Connect |
| D2 | Yellow | IO4(No.26) |

これらをブレッドボード経由で接続します。

![DSC_0013.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130771/4c28bf27-a6df-14e6-f4d5-e647a65dbaa7.jpeg)

ケーブルの色を合わせると、接続するところを間違えずに済みます。
（ESP32-DevKitCの文字が小さいのが辛いですが ^^;）

# ソフトウェア
プログラムは以下のようになります。

## 宣言
まず、ヘッダファイルを呼び出します。
Webサーバとして作成するときには「WebServer」クラスを使用します。

```c
#include <WiFi.h>
#include <WebServer.h>
```

WebServerクラスのインスタンスを、外部変数として作成します。
ポート番号は80を指定します。

```c
WebServer server(80)
```

今回はWiFiでネットに接続するため、アクセスポイントのSSIDとパスワードを指定します。
（下では仮の文字列にしてあります）

```c
const char* ssid = "wifi_ssid";
const char* pass = "wifi_pass";
```

## 初期化
初期化関数「setup()」内で、以下の処理を行います。

- シリアルポートの初期化
- WiFiのアクセスポイントに接続
- Groveセンサーの初期化
- Webサーバの起動

まず、シリアルポートとの速度を指定して開始します。

```c
    // シリアルポートの初期化
    Serial.begin(115200);
```

続いて、WiFiのアクセスポイントの接続します。

```c
    // WiFiのアクセスポイントに接続
    WiFi.begin(ssid, pass);
    while (WiFi.status() != WL_CONNECTED) {
        delay(500);
    }
    // ESP32のIPアドレスを出力
    Serial.println("WiFi Connected.");
    Serial.print("IP = ");
    Serial.println(WiFi.localIP());
```

動作させるとき、WebサーバのIPアドレスがわからないとアクセスできませんので、シリアルポートに出力します。

次に、今回使用するLEDの初期化を行います。

```c
    pinMode(4, OUTPUT);
```

今回は、IO4から出力しますので、そのように設定しています。

最後にWebサーバを起動します。
起動する際、アドレスごとにコールバック関数を定義しておきます。

```c
    // 処理するアドレスを定義
    server.on("/", handleRoot);
    server.on("/led", handleLED);
    server.onNotFound(handleNotFound);
    // Webサーバーを起動
    server.begin();
```

それぞれ、「/」「/led」「それ以外」にアクセスされたときの処理を行う関数を定義します。

## 処理ループ
処理ループ関数「loop()」内では、「WebServer.handleClient()」を呼び出すだけになっており、実際の処理は、初期化時に指定したコールバック関数内で記述します。

```c
void loop()
{
    server.handleClient();
}
```

## コールバック関数
初期化時に指定した、以下の関数に処理を記述します。

- 初期画面
- LEDをON/OFFする
- 存在しないアドレスが指定された時

まず、初期画面を作成します。

```c
void handleRoot(void)
{
    String html;

    // HTMLを組み立てる
    html = "<!DOCTYPE html>";
    html += "<html>";
    html += "<head>";
    html += "<meta charset=\"utf-8\">";
    html += "<title>LEDをON/OFFする</title>";
    html += "</head>";
    html += "<body>";
    html += "<p>リンクをクリックするとLEDがON/OFFします</p>";
    html += "<ul>";
    html += "<li><a href=\"/led?s=on\">ON</a></li>";
    html += "<li><a href=\"/led?s=off\">OFF</a></li>";
    html += "</ul>";
    html += "</body>";
    html += "</html>";

    // HTMLを出力する
    server.send(200, "text/html", html);
}
```

この画面では、LEDをON/OFFするためのURLへのリンクをHTMLで記述しています。
この程度の内容なので短いですが、もっと長くなるようであれば、もうちょっと記述方法を工夫したほうがよいかもしれません。

次に、LEDをON/OFFするコールバック関数を作成します。

```c
void handleLED(void)
{
    String msg;

    // 「/led?s=○」のパラメータが指定されているかどうかを確認
    if (server.hasArg("s"))
    {
        // 「○」の値に応じて、LEDをON/OFFする
        if (server.arg("s").equals("on"))
        {
            digitalWrite(4, HIGH);
            msg = "LEDをONにしました";
        }
        else if (server.arg("s").equals("off"))
        {
            digitalWrite(4, LOW);
            msg = "LEDをOFFにしました";
        }
        else
        {
            msg = "パラメータが正しく指定されていません";
        }
    }

    // 変数msgの文字列を送信する
    server.send(200, "text/plain; charset=utf-8", msg);
}
```

「/led」が呼ばれた際のパラメータで、LEDをON/OFFするようにしています。
まずパラメータ名が「s」であるかを確認し、その値が「on」「off」であるかを確認しています。
それぞれ、IO4に対してHIGH/LOWの値を指定して、LEDのON/OFFを実現し、画面にもその結果を表示するようにしています。
（HTMLで書くほどではないので、ただのテキストで出力しています）

最後に、存在しないアドレスが指定された時の処理を作成します。

```c
void handleNotFound(void)
{
    server.send(404, "text/plain", "Not Found.");
}
```

ただ「Not Found」と出力しているだけです。

# 実行
実行してみると、シリアルモニタにWebサーバのIPアドレスが表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130771/b368e0ad-9b85-0413-38aa-12d57162839e.png)

そのIPアドレスに、Webブラウザでアクセスすると、以下のような画面が表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130771/344f8d02-4042-9ea2-9b85-c39c897d583f.png)

「ON」をクリックすると、LEDが光ります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130771/716bee9c-17cb-e704-d872-59b28a19305b.png)
![DSC_0015.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130771/5a6cfe6a-9d1e-d14a-4587-97eeca9321d0.jpeg)

「OFF」をクリックすると、LEDが消えます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130771/7ffc2816-6a93-2b37-9602-6482c47e28a8.png)
![DSC_0017.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130771/2fcfe0ea-c93d-f1cb-af36-f6e57213b293.jpeg)

# まとめ
ゴリゴリHTMLを書くのが面倒ですが、コールバックの仕組みを使えば、簡単にセンサーの制御ができます。
またGroveセンサーも問題なくつなぐことができます。

# ソースコード
```c
#include <WiFi.h>
#include <WebServer.h>

WebServer server(80);

// WiFi情報
const char* ssid = "wifi_ssid";
const char* pass = "wifi_pass";

// 初期画面
void handleRoot(void)
{
    String html;

    // HTMLを組み立てる
    html = "<!DOCTYPE html>";
    html += "<html>";
    html += "<head>";
    html += "<meta charset=\"utf-8\">";
    html += "<title>LEDをON/OFFする</title>";
    html += "</head>";
    html += "<body>";
    html += "<p>リンクをクリックするとLEDがON/OFFします</p>";
    html += "<ul>";
    html += "<li><a href=\"/led?s=on\">ON</a></li>";
    html += "<li><a href=\"/led?s=off\">OFF</a></li>";
    html += "</ul>";
    html += "</body>";
    html += "</html>";

    // HTMLを出力する
    server.send(200, "text/html", html);
}

// LEDのON/OFF
void handleLED(void)
{
    String msg;

    // 「/led?s=○」のパラメータが指定されているかどうかを確認
    if (server.hasArg("s"))
    {
        // 「○」の値に応じて、LEDをON/OFFする
        if (server.arg("s").equals("on"))
        {
            digitalWrite(4, HIGH);
            msg = "LEDをONにしました";
        }
        else if (server.arg("s").equals("off"))
        {
            digitalWrite(4, LOW);
            msg = "LEDをOFFにしました";
        }
        else
        {
            msg = "パラメータが正しく指定されていません";
        }
    }

    // 変数msgの文字列を送信する
    server.send(200, "text/plain; charset=utf-8", msg);
}

// 存在しないアドレスが指定された時の処理
void handleNotFound(void)
{
    server.send(404, "text/plain", "Not Found.");
}

// 初期化
void setup()
{
    // シリアルポートの初期化
    Serial.begin(115200);

    // WiFiのアクセスポイントに接続
    WiFi.begin(ssid, pass);
    while (WiFi.status() != WL_CONNECTED) {
        delay(500);
    }
    // ESP32のIPアドレスを出力
    Serial.println("WiFi Connected.");
    Serial.print("IP = ");
    Serial.println(WiFi.localIP());
    
    // Groveセンサーの初期化
    pinMode(4, OUTPUT);

    // 処理するアドレスを定義
    server.on("/", handleRoot);
    server.on("/led", handleLED);
    server.onNotFound(handleNotFound);
    // Webサーバーを起動
    server.begin();
}

// 処理ループ
void loop()
{
    server.handleClient();
}
```