ESP8266 WROOM-02とArduinoで構成する簡易httpd

先週はESP-WROOM-02を使ってArduinoにwifi機能を追加し、wifiステーションとして動作することを確認した。今回はwifiのAP(アクセスポイント)およびTCPのサーバー(Listen側)として構成し、ブラウザからのリクエストに応答させてみた。

追記 : 本稿では、WROOM-02出荷時のファームウェアがもつATコマンドベースでhttpdを実装している。ESP8266 Communityが用意してくれているライブラリベースのhttpdの実装については、別稿に記載した。

ESP-WROOM-02

アクセスポイントとするための初期化

以下の一連のATコマンドを送ることで、WROOM-02はwifiアクセスポイント兼 http(tcp/80) のサーバーとなる。なお、サーバーといってもtcp/80の接続要求をaccept()するだけで、接続してきたクライアントとのやり取りは別途書いてやる必要があり、今回はArduinoで動かすスケッチとして書いている。

WROOM-02 初期化

前回と同じように、Arduinoのシリアルモニタからの入力がWROOM-02に送信され、WROOM-02からの出力がシリアルモニタ上に表示されるようにしている。
ATコマンドについては、ESP8266 AT Instruction Set Version 1.4 を参考にした。

AT+RST

WROOM-02をリスタートさせる。完了すると、”ready\r\n”が帰ってくる。

AT+CWMODE_CUR=2

アクセスポイント(softAP)モードに設定。”_CUR”で終わるコマンドの場合、設定内容がフラッシュメモリに記憶されない。

AT+CWDHCP_CUR=0,1

APモードのときDHCPを許可する。

AT+CWSAP_CUR=”ESP8266_AP”,”password”,2,3,1

APモードの設定。各パラメータの内容は、softAPのSSID、AP接続用のパスワード(8~64文字)、チャネルID、暗号化方式、同時接続数となっている。文字列は””(ダブルクォーテーション)で括る必要がある。
暗号化方式の3はWPA2_PSK(詳細は上記のマニュアルを参照のこと)、今回の用途からして同時接続数は1とした(同時4接続まで受け入れ可能)。

AT+CIPMUX=1

複数接続モードの許可。TCPサーバーとして動作させるためには、複数接続モードを許可しておく必要がある。

AT+CIPSERVER=1,80

TCPサーバーとしての動作を許可し、ポート80(http)でのlisten()を開始させる。このコマンドを与える前に、AT+CIPMUX=1を実行しておくこと。

ここまでの一連のコマンドによって、WROOM-02はwifiアクセスポイントとして動作を開始し、httpでの接続を待ち受けるようになる。

PCやスマホからの接続とHTTPリクエスト

ノートPCやスマホから接続する際には、上記の初期化が終わった時点でWROOM-02がブロードキャストしているssid(ESP8266_AP)を一覧から探し、事前共有するパスワードとして password を入力すればよくて、まったくふつう通りの手順。

ホストとしてのWROOM-02は、192.168.4.1 というアドレスを持つ。wifi接続してきたPCやスマホには、DHCPによって192.168.4.2以降のアドレスが与えられる。
たとえばPCのブラウザ(IE10)から、”http://192.168.4.1/” をリクエストしてやると、Arduinoのシリアルモニタには、

0,CONNECT
+IPD,0,257:GET / HTTP/1.1
Accept: text/html, application/xhtml+xml, */*
Accept-Language: ja-JP
User-Agent: Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; WOW64; Trident/6.0)
Accept-Encoding: gzip, deflate
Host: 192.168.4.1
DNT: 1
Connection: Keep-Alive
    

と表示され、ブラウザは応答待ち状態となる。
“0, CONNECT”は、端末側からの接続を受け入れてLinkID 0番を与えたことを示しており、”+IPD,0,257:”は、LinkID=0の端末から257バイトのデータを受信したことを示している。引き続く “GET / HTTP/1.1 ……. “は、IE10が送ってきたHTTPリクエストそのものである。
ブラウザは応答待ちのままなので、×をクリックしてやると、

0,CLOSED

と表示され、LinkID 0番の接続がclose()されたことが分かる。

適当なhttpdの実装

あとはArduinoのスケッチでATコマンドを出して初期化したり、ブラウザからのリクエストに応答したりするだけである。
100行ほどなのでそのまま掲載することにした。

void setup() {
  Serial.begin(115200);
  Serial.print("\r\nStart\r\n");
  Serial1.begin(115200);
  delay(1000);
  Serial1.print("AT\r\n");
}
static int r_count = 0;
#define SZ_BUFFER  260
char buffer[SZ_BUFFER + 2];
void loop() {
  if (Serial1.available() > 0) {  // esp8266から
    byte c = Serial1.read();
    buffer[r_count++] = c;
    buffer[r_count] = 0;
    if (r_count == 1 && c == '>') // AT+CIPSENDに対するプロンプト
      execute();      
    else if (c == '\n' || r_count >= SZ_BUFFER)
      execute();
  }
  while (Serial.available() > 0) {  // arduino IDEから
    Serial1.write(Serial.read());
  }
}

static const char* cpResponse200 = 
    "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n"
    "<HTML><BODY>OK</BODY></HTML>\r\n";

static const char* cpResponse404 = 
    "HTTP/1.1 404 Not Found\r\nConnection: close\r\n\r\n"
    "<HTML><BODY>Not Found</BODY></HTML>\r\n";

enum { initESP = 0, wait_wifi_connect, wait_request , got_request , prepare_response , response_sent } state = initESP;

String linkID = "";
boolean result = false;
void execute() {
  static int init_index = 0;
  static const char* cpInitCommands[] = {
    "AT+RST\r\n",
    "AT+CWMODE_CUR=2\r\n",
    "AT+CWDHCP_CUR=0,1\r\n",
    "AT+CWSAP_CUR=\"ESP8266_AP\",\"password\",2,3,1\r\n",
    "AT+CIPMUX=1\r\n",
    "AT+CIPSERVER=1,80\r\n",
    NULL
  };
  String s = String(buffer);
  s.trim();
  switch(state) {
  case initESP:
    if (s == "ready" || s == "OK") {
      if (s == "OK" && init_index == 1)
        break;  // AT+RSTのOKは無視させる。
      const char *cp = cpInitCommands[init_index++];
      if (cp) {
        delay(50);
        Serial1.print(cp);
      } else
        state = wait_request;
    }
    break;
  case wait_request:
    if (s.startsWith("+IPD") && s.indexOf("HTTP/1") > 0 ) {
      linkID = s.substring(5, 6);
      int n = s.indexOf("GET");  // GETしか扱わない。
      if (n < 0)
         break;
      int m = s.indexOf(" ", n + 4);
      String uri = s.substring(n + 4, m);  // "GET_" の次から、URIの次の空白まで。
      result = uri.startsWith("/cmd");
      state = got_request;
    }
    break;
  case got_request:
    if (s.length() == 0) { 
      delay(10);
      int n = strlen(result ? cpResponse200 : cpResponse404);
      Serial1.print("AT+CIPSEND=" + linkID + "," + n + "\r\n");
      state = prepare_response;
    } else if (s.endsWith("CLOSED"))
      state = wait_request;
    break;
  case prepare_response:
    if (s.startsWith(">")) {
        delay(10);
        Serial1.print(result ? cpResponse200 : cpResponse404);
        state = response_sent;
    }
    break;  
  case response_sent:    
    if (s.startsWith("SEND OK")) { 
      delay(10);
      Serial1.print("AT+CIPCLOSE=" + linkID + "\r\n");
      linkID = "";
      state = wait_request;
    }
    break;    
  }
  if (s != "!" && s.length() > 1)
    Serial.print(s + "\r\n");
  r_count = 0;  
}

WROOM-02からの受信文字列によって駆動される適当なステートマシンになっている。同時に異なるLinkIDが生じることへの配慮をしておらず、同時複数の接続は想定していない。

WROOM-02の初期化の後は、リクエストの待ち受けと応答を繰り返す。対話時にエラーが起きることを想定していないので、このコードでは”ERROR”とかかえってくるとストールするだろうし、リクエスト途上での接続断でも状態を見失う。
なにやら、WROOM-02からの文字列受信後すぐにATコマンドを送ると具合が悪かったので、送信の前にはdelay(10); を挟んでいる。
httpdとしては、”/cmd”で始まるURIがリクエストされた場合には 200を応答し、それ以外は404を応答している。PCやスマホのブラウザから叩いてみたところ、予定通りに動いてくれた。

ゆっくり使いましょう

※ ブラウザでF5キーやCtrl-Rによるリロードを素早く繰り返すと、スケッチによるレスポンス送信のタイミングでブラウザからの切断と再接続が起きたりしてしまって、”AT+CIPSEND=”によるレスポンスの送信要求自体がエラーとなることがある。

...
Accept-Encoding: gzip, deflate, sdch
Accept-Language: ja,en-US;q=0.8,en;q=0.6
(ユ餝�=Mャ0,CLOSED
0,CONNECT
+IPD,0,433:GET /cmd=1?param=123&aaa&fjsakldfsj HTTP/1.1
Host: 192.168.4.1
...

リクエストヘッダ受信完了と同時に、切断(0, CLOSED)を検出しているが、スケッチからAT+CIPSEND=… とか送信しようとして、おかしなことになっている。

クライアント側からは、あるリクエストに対するレスポンスを見てから次を送るようにすれば(つまりゆっくり使えば)、さほど問題はない。

次のステップ

200を応答する前に、パラメータを解析してステッピングモーターやサーボを動かせば、前にADKのAPIを使ってUSB経由でコマンドをもらったときと同様な機能はすぐに実現できるだろう。AIR A01からどんどん離れているが、そろそろカメラの話に戻りたいところ。