ESP-WROOM-32のwebserverでLED点灯

前回の続きになるが、ESP32開発ボードを使ってWiFiアクセスポイントとwebサーバーを動作させ、クライアント(スマホ)からの操作でLEDを点けたり消したりするようなスケッチを書いてみた。ついでにベースとなる簡易Webサーバーのクラスも用意した。

以前、ESP-WROOM-02でも似たようなことをやっていたので、すぐに出来上がったのだけど、何も新しいことをやっていないとも言える。まだ梅雨も明けていないはず(梅雨はなかった?)なのに、外に出る気がしないほど暑くあまり難しいことは考えられない。

回路

回路はGPIO13に10KΩの抵抗を介して赤色LEDを接続しただけ。電源はPCから接続するUSBケーブルからもらう。
ESP32のデータシートによれば、各ピンの出力電流の絶対最大定格(Imax)は当初12mAだったものが40mAに変更されている(ESP32 Datasheet V1.4での変更。リリースノートの記述には80mAに変更とあるが、V1.6の本文では40mAになっている)。また、各IOピンの駆動能力は設定可能であり、初期値は20mAと書いてある。10mAまでと思っておけば、壊すことはないだろう。LEDに10mA流すとすれば330Ωの直列抵抗を使うことになる。今回は、点灯することが目で確認できればいいので10KΩの抵抗を使った。

ESP32-HTTPD-LED1
ESP32-HTTPD-LED1, カメラ内深度合成

スケッチ

ESP-WROOM-02(ESP8266)のときには、ESP8266WebServerクラスという便利なスケッチが一緒に付いてきたのだけど、今回導入したArduino Core for ESP32 には見当たらなかったので、簡易的なWebサーバー機能をもつ SimpleWebServer クラスというのをまずこしらえた。このクラスを使う実装側では、自分が処理したいリクエスト文字列と、そのリクエスト文字列に対応したハンドラ(関数ポインタ)を登録しておけばよいので、実装側のスケッチがちょっとすっきりする。もしかしたら、Arduino IDEにESP8266の開発環境も追加してやればESP8266用のサンプルが使えるのかもしれないが、混乱のもとになるのでまだやっていない。

SimpleWebServer.h

以下の内容を SimpleWebServer.h という名前で保存し、実装するスケッチと同じディレクトリに置いてincludeすれば、Arduino IDEにソースタブが増えるから参照や手直しも容易になる。

#include <WiFi.h>
#include <WiFiServer.h>
#include <map>
using namespace std; 

typedef void (*Handler_t)(void) ;
typedef void (*DefaultHandler_t)(String) ;
typedef struct {
  int status;
  const char* cpDesc;
} response_t;

const response_t responses[] = {
  {200, "200 OK"},
  {404, "404 Not Found"},
  {500, "500 Server Error"},
  {0, 0}
};

class SimpleWebServer : public WiFiServer {
  IPAddress ap_addr;
  IPAddress ap_netmask;
  String ssid;
  String password;
  std::map<String, Handler_t> request_handlers;
  DefaultHandler_t default_handler = 0;
  WiFiClient* client_connected;

public:
  SimpleWebServer(const char* ssid, const char* password, const IPAddress ap_addr, const IPAddress ap_netmask, int port = 80) : WiFiServer(port) {
    this->ap_addr = ap_addr;
    this->ap_netmask = ap_netmask;
    this->ssid = ssid;
    this->password = password;
    client_connected = NULL;
  }

  void begin() {
    WiFi.softAP(ssid.c_str(), password.c_str());
    WiFi.softAPConfig(ap_addr, ap_addr, ap_netmask);
    Serial.print("SoftAP : "); 
    Serial.print(WiFi.softAPIP());
    Serial.println(" " + ssid + String(" starts.")); 
    WiFiServer::begin();
    Serial.println("SimpleWebServer begin()");
  }

  void add_handler(String uri, Handler_t func) {
    request_handlers[uri] = func;
  }

  void add_default_handler(DefaultHandler_t func) {
    default_handler = func;
  }

  void send_response(const char* cp)
  {
    if (client_connected)
      client_connected->print(cp);
  }

  void send_status(int status, const char* cp = NULL)
  {
    if (!client_connected)
      return;
    const response_t* p = responses;
    while(p->status != 0) {
      if (p->status == status) {
        String s = "HTTP/1.1" + String(status) + String(" ") + String(p->cpDesc) + String("\r\n\r\n");
        const char* cpDesc = cp ? cp : p->cpDesc;
        s = s + String("<HTML><BODY style='font-size:48px;'>") + String(cpDesc) + String("</BODY></HTML>\r\n");
        client_connected->print(s);
        return;        
      }
      p++;
    }
    send_status(200);
  }

  void handle_request() {
    WiFiClient client = available();
    if (!client)
      return;

    String request_line = "";
    while (client) {
      client_connected = &client;
      while(client.available()) {
          String line = client.readStringUntil('\r');
          line.trim();
          if (line.startsWith("GET")) { // GETしかやってない
            int index = line.indexOf(" ", 5);
            if (index > 0)
              request_line = line.substring(4, index);
            else
              request_line = line.substring(4);
            request_line.trim();
            continue;
          }
          if (line.length() == 0) { // end of request headers.
            Serial.println("request_line = " + request_line);
            Handler_t f = request_handlers[request_line];
            if (!f) {
              if (default_handler)
                default_handler(request_line);
              else
                send_status(404);
            } else
              f();
            client.flush();
          }
      }
      client_connected = NULL;
      client.stop();
    }
  }
};
概要

WiFiのアクセスポイントおよびtcpサーバーとして機能させるため、ライブラリにあるWiFiServerクラスを派生している。インクルードしているライブラリファイルは、前回の導入時にArduino のソース格納用ディレクトリ内に置いた、”…\hardware\espressif\esp32\libraries\WiFi\src\” あたりに入っている。

コンストラクタとbegin()

softAPを構成するために必要なssidやパスワード、tcpサーバーのサーバーアドレスやListenするポートをもらっておく。基本クラスのWiFiServer()にはListenポートを渡しておく。

WiFi.softAP() によってESP32をsoftAPモードとし、WiFi.softAPConfig()の呼び出しにより、サーバーアドレスやネットワーク・アドレスを設定するとともに、dhcpの動作も開始させる。リスタート時にステーションモード(WIFI_STA)になっている場合、このメソッドを呼ばないとdhcpが開始しないので、dhcpを頼りにしているスマホから接続できない。

ハンドラ追加メソッド

実装側のスケッチでは、

  server.add_handler("/", led_menu);

といった記述で、URIのスキームとホスト名以降の部分(ここでは、リクエスト文字列と表記)と、そのリクエスト文字列を受け取ったときに処理する関数ポインタを渡す。ハンドラ追加メソッドは、リクエスト文字列をキーとしたmap内に格納する。mapおよび関数ポインタの宣言は以下のとおり。

typedef void (*Handler_t)(void) ;
typedef void (*DefaultHandler_t)(String) ;
std::map<String, Handler_t> request_handlers;

Handler_tは実装内で特別に処理する仕組みのための関数用で、今回の例ではLEDのオンとオフに使う。DefaultHandler_t は、リクエスト文字列がmapに格納されていなかったときに使うハンドラ用で、例えばSPIフラッシュやSDカードからファイルを読み出して返すようなときに使う予定にしており、引数としてリクエスト文字列をとる。

リクエストを処理するメソッド

void handle_request(); では、WiFiServerがクライアントからのtcp接続を受け入れたかどうかを判定し、接続済ならばGETリクエストのリクエスト文字列を取り出し、request_handlers に登録があれば対応する関数を呼び出し、なければdefault_handler として登録されている関数を呼ぶ。GETリクエスト以外への対応や、リクエストヘッダの格納、リクエスト文字列からのクエリの切り出しなどは、必要に応じて追加していくことになる。

WiFiClient clientオブジェクトに対して、if (client) {… } とかwhile (client) {…} とか書いてあるのはちょっと気になるところだが、WiFiClientクラス内に、接続中ならばtrueを返す boolのオペレータが定義されているから、接続中かどうかを判断していることがわかる。

リクエスト元への応答は、各ハンドラ内で行う必要がある。

実装側 ESP32_HTTPD_LED1.ino

#include "SimpleWebServer.h"

// クライアントから/ledonをもらったら点灯し、ledoffなら消灯
// GPIO13にLEDを接続。

#define LED 13
SimpleWebServer server("ESP32AP", "password", IPAddress(192,168,4,1), IPAddress(255,255,255,0), 80);
void led_menu() {
  const char* cur = digitalRead(LED) ? "ON" : "OFF";
  String s =  "HTTP/1.1 200 OK\r\nContent-type:text/html\r\n\r\n"
              "<HTML><BODY style='font-size:48px;'>ESP32_HTTPD_LED1<br/>"
              "<br/><a href=/ledon>ON</a><br/><a href=/ledoff>OFF</a><br/><br/>"
              "Current Status = ";
  s = s + String(cur) + "</BODY></HTML>\r\n";
  server.send_response(s.c_str());
}

void led_on()
{
  digitalWrite(LED, 1);
  led_menu();
}

void led_off()
{
  digitalWrite(LED, 0);
  led_menu();
}

void led_500()
{
  server.send_status(500, "Internal Server Error");
}

void default_handler(String request_line)
{
  Serial.println("default_handler : " + request_line);  
  String s = request_line + " is not found";
  server.send_status(404, s.c_str());
}

void setup() {
  Serial.begin(115200);
  delay(10);
  Serial.println("");
  pinMode(LED, OUTPUT);
  digitalWrite(LED, 0);

  server.add_handler("/", led_menu);
  server.add_handler("/ledon", led_on);
  server.add_handler("/ledoff", led_off);
  server.add_handler("/led500", led_500);

  server.add_default_handler(default_handler);
  server.begin();
}

void loop() {
  server.handle_request();
  delay(10);
}

面倒なことは先に書いたクラス側がやってくれるので、実装側で本来やるべきことだけ書けばよくなっている。今回はLEDを点けたり消したりするだけなので、digitalWrite()を使ってオン/オフしている。

SimpleWebServer クラスのインスタンスを必要なパラメータを並べて作成し、LEDをオンにしたりオフにしたりするハンドラを書き、setup()内でそれらをリクエスト文字列とともに登録している。led_500() や default_handler() といった関数は無くても困らないのだが、SimpleWebServerクラスが意図通りに動いていることを確認するために用意した。void loop() では、server.handle_request() を繰り返し呼ぶだけ。

ビルドと実行

スケッチのコンパイルやESP32開発ボードへのダウンロード(マイコンボードへの書込み)は特に問題なく、開発ボードのボタンを操作することもなく、Arduino IDEの操作(「マイコンボードに書き込む」を選ぶ)だけですんなりとできた。シリアルモニタを開いたままでも書込みに問題はなかった。コンパイル時に表示されたメッセージは以下のとおり。

最大1310720バイトのフラッシュメモリのうち、スケッチが435839バイト(33%)を使っています。
最大294912バイトのRAMのうち、グローバル変数が33040バイト(11%)を使っていて、ローカル変数で261872バイト使うことができます。

プログラムを格納できるフラッシュメモリサイズは1280Kバイトで、今回のスケッチをコンパイルしたら435,839バイトのバイナリができましたということのようだ。ライブラリをいろいろと抱えているので、これくらいのバイナリサイズになるのだろう (後述するが、この数字はちょっと矛盾している)。

WROOM-02のときは使えるRAMの最大サイズが81,920バイトだったものが、294,912バイトになったのは嬉しい。

フラッシュにはパーティションごとに書き込まれる

スケッチメニューから「マイコンボードに書き込む」を選ぶと、以下のように表示された。

esptool.py v2.0-beta3
Connecting......
Uploading stub...
Running stub...
Stub running...
Configuring flash size...
Flash params set to 0x0220
Compressed 11120 bytes to 7193...

Writing at 0x00001000... (100 %)
Wrote 11120 bytes (7193 compressed) at 0x00001000 in 0.6 seconds (effective 141.0 kbit/s)...
Hash of data verified.
Compressed 3072 bytes to 105...

Writing at 0x00008000... (100 %)
Wrote 3072 bytes (105 compressed) at 0x00008000 in 0.0 seconds (effective 1536.0 kbit/s)...
Hash of data verified.
Compressed 8192 bytes to 47...

Writing at 0x0000e000... (100 %)
Wrote 8192 bytes (47 compressed) at 0x0000e000 in 0.0 seconds (effective 3640.9 kbit/s)...
Hash of data verified.
Compressed 505984 bytes to 289002...

Writing at 0x00010000... (5 %)
Writing at 0x00014000... (11 %)
Writing at 0x00018000... (16 %)
... (一部省略) ...
Wrote 505984 bytes (289002 compressed) at 0x00010000 in 25.5 seconds (effective 158.6 kbit/s)...
Hash of data verified.

Leaving...
Hard resetting...

esptoolによって、4つのブロックがESP-WROOM-32内のSPIフラッシュに書き込まれているようである。表示内容の、” Writing at 0x00001000… ” の 数値部分がフラッシュ内での書込み先アドレスに相当していて、以下のような内容を書き込んでいるようである。

  • 0x01000 :  2ndブートローダー (…/esp32/tools/sdk/bin/bootloader.bin) 11120バイト。1stブートローダーから呼ばれ、フラッシュ内プログラムを開始する。
  • 0x08000 :  パーティションテーブル (ESP32_HTTPD_LED1.ino.partitions.bin) 3072バイト 。フラッシュ内の領域情報。なおかつ0x09000から20Kバイトはnvs( non volatile storage ) 領域のようで、何も書き込まれないから内容が保持される。
  • 0x0e000:  (…/esp32/tools/partitions/boot_app0.bin) otaデータ領域 8192バイト。boot_app0.binの中身はほとんど0xff。この領域は、スケッチのダウンロードごとに初期化されることになる。
  • 0x10000 : 今回のスケッチ (ESP32_HTTPD_LED1.ino.bin)  505984バイト。さきほどのコンパイル結果とはサイズが大きく異なっている。コンパイル時のデバッグオプション( Core Debug Level ) を変えても、一致した数字にはならなかった。

パーティションテーブルとスケッチは、Arduino IDEでビルドすると、ビルド先ディレクトリ ( c:\Users\ユーザー名\AppData\Local\Temp\arduino_build_xxxxxx) 内に作成されている。パーティションテーブルのファイルをダンプしてみると、…/tools/partitions/default.csvと同じ内容だった。このcsvファイルを書き換えることで、パーティションテーブルも変更可能なのだろう。
なお、default.csvをみるとSPIフラッシュの後ろの方の1472Kバイトがspiffs領域のようなので、そのうちファイルシステム領域として利用したい。

実行

スケッチの転送が終了すると、USBを介してリセット指示が送られ開発ボードがリスタートしシリアルモニタには以下のような内容が表示される。なお、コンパイル時にはCore Debug Level : Debug としてビルドしているので、WiFiGeneric.cpp のDebugメッセージ (log_d() の出力) が表示されていて、[D] で開始する行がそれにあたる。

ets Jun  8 2016 00:22:57

rst:0x1 (POWERON_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
ets Jun  8 2016 00:22:57

rst:0x10 (RTCWDT_RTC_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
configsip: 0, SPIWP:0x00
clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00
mode:DIO, clock div:2
load:0x3fff0008,len:8
load:0x3fff0010,len:160
load:0x40078000,len:10632
load:0x40080000,len:252
entry 0x40080034

[D][WiFiGeneric.cpp:182] _eventCallback(): Event: 12 - AP_START
SoftAP : 192.168.4.1 ESP32AP starts.
SimpleWebServer begin()
----------- 開始時はここまで
[D][WiFiGeneric.cpp:182] _eventCallback(): Event: 14 - AP_STACONNECTED
----------- スマホからのWiFi接続を検出
----------- 以下、スマホのブラウザからのリクエスト文字列を表示
request_line = /
request_line = /ledon
request_line = /ledoff

スマホのブラウザを操作することで、ブレッドボード上のLEDが点灯/消灯する様子を動画にしてYoutubeにおいた。4Kではありません。

動画は、”/”をリクエストしてメニュー( led_menu() による)を表示しているところから始まり、ONおよびOFFのリンクを触ることで、ブレッドボード手前の赤いLEDが点いたり消えたりする。
また、ハンドリングしていないURIを指定することで、実装側の default_handler() が応答している様子なども含まれている。

きょうのまとめ

ESP-WROOM-02のときと同じように動かすことができた。ただ、この程度ならWROOM-02で十分であるとも言える。まぁ、しばらく離れていたのでリヒハビリテーションのつもり。次はSPIフラッシュにファイルを置く方法を調べてみる予定。

ESP32モジュールはけっこう熱をもつ。WROOM-02がほんのり暖かという感じだったのに対し、熱いの一歩手前くらいの感じ。リスタート時はWiFi機能などをオフにしておき、必要に応じて必要な機能だけをオンにするような設定方法も考えておいた方がよさそうである。