4
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

updated at

ESP-WROOM-32でWebサーバーを作ってHello World!をする

ESP-WROOM-32でWebサーバーを作ってHello World!をする

Webサーバーとは何なのか

WEBサーバーというあちこちでよく聞く謎の存在

現代の情報化社会において、Webサーバーに関わらずに生きている可能性はほとんどないはずです。

なんせこのQiitaにアクセスしている時点で、Webサーバーにアクセスしているのだがら当然でしょう。

この界隈にWebサーバーは当たり前のようにあり、日常的に使われています。

しかし、私はWebサーバーが実際どのようなものなのか、実は分かっていない。分かっていないけど使っている。

果たしてこれは何なのか。それが気になり始めたので調べてみることにしました。

しかし、いろいろ調べましたが、Webサービスを作るチュートリアル等では、Webサーバーを動かしてWebページを表示するところまですっとんでしまいます。

インストールしてコマンドを打ち込めば起動するWebサーバーの仕組みを知りたいのですが、そこら辺に関する資料というのがなかなか見つかりません。

そうやってあちこち調べてみると、最近のIoT方面ではネットに接続できるマイコンをWebサーバーにすることで、様々な情報をWifi経由で取得するのが主流になっていることがわかってきました。

マイコンならWebサーバーが何をやっているのか、自分で書くことでしっかりと理解できるはずです。

幸い手元にESP-WROOM-32の開発キットが転がっていたので、やってみることにします。

まずは作ってみる

とりあえずこんな作例を見つけたので、まずは完全コピーで作ってみます。
ESP32 Web Server – Arduino IDE

回路はGPIOの配置がボードごとに異なることを考慮して、ブレッドボード上に配線します。
AMZN_2020-11-10_12_49_53.jpg

スケッチをコピーして書き込み、シリアルモニタに表示されるIPアドレスにWebブラウザからアクセスすると、このような画面が表示されました。成功です。
espserver.png
ボタンをクリックすると、それに合わせてLEDが点灯、消灯し、ブラウザの表示も変わります。

これで、wifi経由でマイコンに対して信号を送受信することができるようになりました。
ブラウザからセンサーにしたり、ラジコンにしたりと、様々なことに活用できそうです。

また、ArduinoにZigbee等の通信用のシステムを外付けする場合に比べて、シンプルかつコンパクトに仕上がりました。

シリアルモニタからではなく、ブラウザからアクセスできることも利点です。ブラウザにIPアドレスを打ち込めば応答するので、スマートフォン等からでも容易にアクセスできます。

コードを読んでみる

早速コードを読んでいきます

ヘッダファイルの定義やグローバル変数の定義

#include <WiFi.h>

Wifiのヘッダファイルをインクルードしています。
この辺はまあ簡単です。

const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";

ssidとパスワードの文字列をcharポインタで定義しています。

後で関数等に渡すことを考えると、大きさが変動するcharの配列よりも、アドレスとして一定の範囲の値になるポインタを渡すのは合理的ですね。
文字列であるStringを使わないのは、関数がchar*で作られているのと、String型が持つ文字列の編集機能がSSIDやパスワード等では不要なためでしょう。

WiFiServer server(80);
String header;

WiFiServerオブジェクトとしてインスタンスのserverをポート80で作成しています。

これでポート80番宛てのパケットを受信するサーバが作成されています。
ちなみに作成されただけで、動作はしていません。

そしてヘッダー情報を格納するString型の変数headerが宣言されています。
ヘッダー情報とかよく聞きますけど、マイコンでの扱いは文字列型だったんですね。

String output26State = "off";
String output27State = "off";

マイコン内部でのLEDの状態を保持する変数を定義しています。

True Falseのbooleanではなく、String型で定義することで、可読性を向上させています。

const int output26 = 26;
const int output27 = 27;

ピン番号の定義ですので、そこまで気にする必要は無いですね。

unsigned long currentTime = millis();
unsigned long previousTime = 0; 
const long timeoutTime = 2000;

現在の時間と前の時間とタイムアウトを定義

これはサーバに負荷が加わったときに、時間内に「応答がありませんでした」という関連の処理に必要な変数ですね。

setup()関数

Serial.begin(115200);

pinMode(output26, OUTPUT);
pinMode(output27, OUTPUT);

digitalWrite(output26, LOW);
digitalWrite(output27, LOW);

まずシリアル通信の開始と、GPIOの設定を行っています。

Serial.print("Connecting to ");
Serial.println(ssid);
WiFi.begin(ssid, password);

シリアル通信にメッセージを表示してから、WiFi通信を開始しています。シリアル通信の開始時には通信速度を入力しますが、WiFiの場合はssidとパスワードを入力して開始します。

while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}

WiFi.status()がWL_CONNECTEDにならない限り0.5秒ごとにシリアル通信にピリオドを出力し続けます。

このとき、ssidとパスワードが正しければ、通信が短時間で確立するはずという視点で書かれているようです。
通信が確立しない限り、次のコードに進むことはありません。

あまり長く通信が確立しないようなら、ssid とパスワードの間違いを疑うという感じになると思います。

Serial.println("");
Serial.println("WiFi connected.");
Serial.println("IP address: ");
Serial.println(WiFi.localIP());
server.begin();

通信が確立した旨と、ネットワーク上で割り当てられたIPアドレスを表示し、サーバーを起動します。

loop()関数

ここからは図にしないとわかりにくくなるので、フローチャートを作成しました。

サーバー図1.png

WiFiClient client = server.available();
if (client) {
}

WiFiClientオブジェクトのclientをserver.available()により取得しています。

シリアル通信が始まるのをマイコン側に待たせる場合は、Serial.available()(データが届いたら1以上、届いていないなら0)でシリアル通信のバッファにデータが届いていないかを常に監視して、データが届いたら動作させるという書き方をよくやります。

if(Serial.available()>0){

}

WiFiClientを作っているだけで、書き方としては同じものです。

オブジェクトがfalseではないならば、WiFiの接続が成立しているので、通信のための処理を開始します。

    currentTime = millis();
    previousTime = currentTime;

millis()でマイコン起動時からの時間をmsで記録していきます。

currentTimeは「現在の時間」でpreviousTimeは「接続を開始した時間」です。この2つの変数を使い、後でタイムアウト判定を行います。

String currentLine = "";

そして空のcurrentLine、つまり「現在の行」を保存する文字列変数を作成します。

while (client.connected() && currentTime - previousTime <= timeoutTime) {

client.connected() つまりclientが接続中、かつ、「現在の時間」から「接続を開始した時間」を引いたものが、タイムアウト時間以下の場合、つまり、接続開始から現在までに経過した時間がタイムアウト時間以下の場合に処理が次に進みます。

currentTime = millis();

「現在の時間」を更新することで処理に要した時間を大きくしていきます。

if (client.available()) {    

clientオブジェクトから文字列を読み取り可能な場合、Trueになるので処理が次に進みます。

char c = client.read();                             
header += c;

clientオブジェクトから1文字を読み取って、header文字列の末尾に追加します。

if (c == '\n') {  
    if (currentLine.length() == 0) {
        client.println("HTTP/1.1 200 OK");
        client.println("Content-type:text/html");
        client.println("Connection: close");
        client.println();
        //HTML省略
        client.println();

clientオブジェクトから読み取った文字が「\n」つまり改行コードかを判定し、改行コードを読み取った際にcurrentLine文字列の長さが0なら、HTTPレスポンスとして、ヘッダとHTMLの送信を開始するという処理になっています。

ヘッダとHTMLは空の改行の送信client.println();によって区切られます。

そしてHTTPレスポンスは空の改行を送信client.println();して終了します。

なぜこのようになっているのかというと、HTTPリクエストとレスポンスは空の改行を送信して区切りや終了をするというルールがあるからです。

このループ部分は要するに、ブラウザからのHTTPリクエストを全て受信したら、マイコン側からブラウザにHTTPレスポンスを送信するという仕組みを構築しています。

HTTPリクエストの解析

このループでは 受信した文字は全てheaderの末尾に追加 されていきます。
つまり、ブラウザから送信されたリクエストは全てheaderに保存されます。
そして、改行コード以外の文字はcurrentLineに一時保存され、「現在の行」という情報を一時的に保持します。

改行が行われると、currentLineの「現在の行」という情報は破棄されます。

実際のHTTPヘッダで見てみると次のようになります。
HTTPヘッダをベタ書きすると

HTTP/1.1 200 OK
Content-type:text/html
Connection: close

一番下は空白行です。

ここで改行コード「\n」を可視化した文字列とすると次のようになります。

HTTP/1.1 200 OK\nContent-type:text/html\nConnection: close\n\n

HTTPの区切りに該当する空白行とは、「現在の行」が空の場合に「\n」を受信した場合であり、「\n」を2回連続で受信した場合です。

ここで問題になるのが 「\r」CR(キャリッジリターン) の取り扱いです。

Unix Linuxや、UnixベースのMacOSでは改行を 「\n」LF(ラインフィード) で表していますが、windowsの場合は歴史的に文字列の改行を「\r\n」の2文字で表しています。

HTTP/1.1 200 OK\r\nContent-type:text/html\r\nConnection: close\r\n\r\n

キャリッジリターンは現在ではほぼwindows特有の改行コードですが、HTTPでは現役です。

HTTPでは「\n」と「\r\n」のどちらにも対応することを定めています。

しかし「\r\n\r\n」だと煩雑になるので、読み取った文字が「\r」の場合はcurrentLineに追加しないことで、「\r」の排除を行っています。

currentLineの長さが0になるのは「\n」を読み取った後です。その後に他の文字を読み取ればcurrentLineの長さは1以上になります。「\n」を読み取った直後にまた「\n」を読み取った場合、currentLineの長さが0の状態で「\n」を読み取ったということはHTTPリクエストの区切りの空白行なので、これで向こうからのリクエストが完了したとみなし、HTTPレスポンスの送信を開始しています。

リクエスト内容の読み取りと、レスポンス内容の変更

ESP32 Web Server – Arduino IDEでは、HTTPレスポンスを送信する際に、リクエスト文字列であるheaderに対して、indexOFメソッドで文字列を検索させることで、HTMLを変更しています。

if (header.indexOf("GET /26/on") >= 0){

indexOfは検索対象の文字列が文字列オブジェクト内に存在するとその位置を返し、見つからなかった場合は-1を返します。

HTMLに/26/onへのリンクを張っておくことで、HTTPのGETメソッドでリンク先情報をヘッダに組み込んで送信し、マイコン側でメソッドとURLを解析して、マイコン自体の動作を変更し、サーバーとして返すHTMLを書き換えています。

これにより、サーバーにしているマイコンに情報を送信することができるのです。インターフェースをHTMLの機能で作ることができるので、ボタンで動作させる他に、HTML5の
スライダー等を使えば、ブラウザ上からサーボモーターやLEDの明るさをブラウザからGUIで変更することができるようになります。
ちなみにスライダーはformタグなので長くなるのでここまでにしておきます。

また、サーバーにデータを送信する方法としてPOSTメソッドというものもありますが、今回は割愛します。

Hello World!を作る

ESP32 Web Server – Arduino IDEではjavascriptを使ったりと、割と高度なことをしていますが、初心者にはそこまで必要ではないかもしれません。
例えば遠隔でセンサの値を確認することができればそれで十分だったりもします。

そこで今回はプログラミングを開始するときに必ず行うHello World!をESP32のWebサーバーでやってみようと思います。

#include <WiFi.h>

//ネットワーク情報
const char* ssid = "ssidを入力";
const char* password = "ネットワークのパスワードを入力";

WiFiServer server(80);//webサーバーをポート80に設定する

String header;//HTTPリクエストのヘッダーを格納する変数

unsigned long currentTime = millis();
unsigned long previousTime = 0; 
const long timeoutTime = 2000;

void setup(void)
{
  Serial.begin(115200);
  Serial.print("Connecting to ");
  Serial.println(ssid);
  //Wi-Fi接続を開始する
  WiFi.begin(ssid, password);
  //接続が開始するのを待つ
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  } 
  //通信ができたら、ESP32に割り当てられたIPアドレスを表示する
  Serial.println("");
  Serial.println("WiFi connected.");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());
  //WiFiServer server(80);で作成したサーバーを開始
  server.begin();
}

void loop(void)
{
  WiFiClient client = server.available();   
  if (client) {                          
    currentTime = millis();
    previousTime = currentTime;
    Serial.println("New Client.");       

    String currentLine = "";              
    while (client.connected() && currentTime - previousTime <= timeoutTime) { 
      currentTime = millis();//ここでcurrentTimeが更新されていくので、処理に時間がかかるとタイムアウトする
      if (client.available()) {             
        char c = client.read();             
        Serial.write(c);                   
        header += c;
        if (c == '\n') {
          if (currentLine.length() == 0) {
            //HTMLヘッダ送信開始
            client.println("HTTP/1.1 200 OK");
            client.println("Content-type:text/html");
            client.println("Connection: close");
            //HTTPヘッダは空白改行で終了する
            client.println();
            //HTMLコード開始
            client.println("<!DOCTYPE html><html><head></head><body>");
            client.println("Hello World!");
            client.println("</body></html>");            
            //HTMLコード終了
            //HTTPレスポンスは空白改行で終了する
            client.println();
            break;
          } else {
            currentLine = "";
          }
        } else if (c != '\r') {  
          currentLine += c;     
        }
      }
    }

    //応答が終了したらヘッダをクリアする
    header = "";
    //クライアントとの接続を終了する
    client.stop();
    Serial.println("Client disconnected.");
    Serial.println("");
  }
}

これをESP32に書き込むとシリアルモニタにESP32に割り当てられたIPアドレスが表示されます。

com01.png

このIPアドレスにブラウザからアクセスするとブラウザに以下のようなHelloWorld!の表示が出ます。
helloworld.png

これでESP32をWebサーバーとして動作させて、任意の文字列を表示させることができるようになりました。
シリアルモニタを見ると、ブラウザからのリクエストを受けていることがわかります
com.png

遠隔のセンサとして使用する場合は、HelloWorld部分を数値等に置き換えてやればいいわけです。

要改善

ただ、この実装とにかくシンプルさを突き詰めただけあって問題が多いです。

サーバーを名乗っていながら、GET HEADといった各種のメソッドに一切対応していません。

アクセスがあればなんであれレスポンスを返すという実装です。

サーバーを名乗るなら、メソッドやステータス番号といった対応も必要でしょう。

結び

ESP32上に簡易的にwebサーバーを実装することで、HTTPの本質が文字列の送受信であることを理解できました。

今後はこれを用いたセンシングデバイス等を作成してみようと思います

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
4
Help us understand the problem. What are the problem?