どんなサーバを作るか
概要
今度は Arduino + ESP-WROOM-02U をサーバにします。サンプルとして、HTTP(Web)サーバを作ってみます。
Webサーバというと大げさに思えるかもしれませんが、
- HTTPは比較的単純なプロトコルで、『とりあえずブラウザに文章を表示したい』という程度ならプロトコルに関して憶えなければならないことが少ない
- クライアント/サーバ間で通信が一往復したら通信終了するので、サーバ側でステータス制御を行う(情報を保存しておく)必要が無い
- クライアント側のプログラムを用意する必要が無い(一般的なWebブラウザでいい)
というわけで、実はサーバの学習・実験には適しているのです。
仕様
まず、製作するサーバの仕様を決めます。
Arduino はメモリが少なく、あまり大きなデータを扱うことができません(変数領域がグローバル変数/ローカル変数あわせて2kBしかありません)。そこで、思い切ってごく単純な仕様にしました。
・一般のWebサーバと同じく、TCP80番ポートで待ち受けする
・対応するメソッドはGETのみ
・というか、リクエストの内容に関わらず、リクエストを送信してきたクライアントに対して固定のHTMLドキュメントをレスポンスとして送信する
・レスポンスを送信したらTCP切断
というものです。HTMLドキュメントのレスポンスを求めないタイプのリクエストが来てもHTMLドキュメントを返してしまうという『なんちゃってWebサーバ』ですが、まぁブラウザでちょっと試してみる分には問題ありません。
製作
使用部品・回路
使用する回路はATコマンドによるテストやクライアントで用いたものと同じです。
スケッチ
今回のスケッチは以下の通りです。Apache などインターネットのサービスに使用されているWebサーバは非常に大きなプログラムですが、この超簡単仕様のなんちゃってWebサーバはコメントや空行を含めても120行もありません。しかも大半はWifiインターフェイスの設定で、Webサーバとして動作しているのは loop() 関数の中だけです。
#include "ESP8266.h"
#include <SoftwareSerial.h>
// 以下は各自の環境に合わせて下さい
#define SSID "NETWORK"
#define PASSWORD "password"
#define HOST_PORT 80
SoftwareSerial wifiSerial(2, 3); // RxD=2, TxD=3
ESP8266 wifi(wifiSerial);
void setup(void)
{
Serial.begin(9600);
wifiSerial.begin(9600);
// wifi restart
while(!wifi.restart()){
delay(1000);
}
Serial.println("wifi restart...ok.");
// kick (check alive)
while(!wifi.kick()){
delay(1000);
}
Serial.println("check alive...ok.");
// set station mode
while(!wifi.setOprToStation()){
delay(1000);
}
Serial.println("set station mode...ok.");
// join to AP(SSID,PASSWORD)
while(!wifi.joinAP(SSID, PASSWORD)){
delay(1000);
}
Serial.println("join to AP...ok.");
Serial.print("IP:");
Serial.println(wifi.getLocalIP());
// set multiple mode
while(!wifi.enableMUX()){
delay(1000);
}
Serial.println("set multiple mode...ok.");
// TCPサーバを起動
while(!wifi.startTCPServer(HOST_PORT)){
delay(1000);
}
Serial.println("start TCP server...ok.");
// TCPサーバのタイムアウトを設定
while(!wifi.setTCPServerTimeout(600)){
delay(1000);
}
Serial.println("set timeout...ok.");
Serial.println("setup complete.");
}
void loop(void)
{
char responseHeader[128];
char responseBody[256];
char recvBuffer[128];
uint8_t mux_id;
uint32_t len;
if((len=wifi.recv(&mux_id, recvBuffer, sizeof(recvBuffer)-1, 10000))>0){
// クライアントからのリクエストを受信したら
Serial.print("client connected:");
Serial.println(mux_id);
recvBuffer[len]='\0';
Serial.println(recvBuffer);
// レスポンスを組み立てる
sprintf(responseBody, "<html><head><title>test page</title></head><body><h1>Arduino</h1>this is test page.</body></html>\r\n");
sprintf(responseHeader, "HTTP/1.0 200 ok\r\nContent-type: text/html\r\nContent-length: %d\r\n\r\n", strlen(responseBody));
wifi.send(mux_id, responseHeader, strlen(responseHeader));
wifi.send(mux_id, responseBody, strlen(responseBody));
// TCP接続を解放
wifi.releaseTCP(mux_id);
}
}
コードの解説
Wifiの設定
restart()(ESP8266の再起動)
kick()(起動確認)
setOprToStation()(ステーションモードに設定)
joinAP()(アクセスポイントに接続)
まではクライアント接続のときの例とまったく同じなので、そちらを参照して下さい。ただし動作確認用の Serial.println() は減らしてあります。
IPアドレスの表示
クライアントから接続するために、ESP-WROOM-02U に割り当てられているIPアドレスを知る必要があります。アドレスを知るためには ESP8266::getLocalIP() 関数を使います。書式は以下の通りです。
String ESP8266::getLocalIP(void)
引数
なし
返却値
返却値は、IPアドレスおよびMACアドレスを表す文字列です。
getLocalIP() は、内部的には『AT+CIFSR』コマンドをESP8266に送信し、レスポンスメッセージからコマンドのエコーバックと『OK』を取り除いて出力するようになっています。
よって、返却値の文字列は
+CIFSR:STAIP,"192.168.0.19"
+CIFSR:STAMAC,"12:34:56:78:9a:bc"
という型式になります。
と、ここまで説明してきたのですが、実は手元の環境ではgetLocalIP()が動作しません。
もしかしたら ESP-WROOM-02U と無印では微妙に仕様が違うのかもしれませんが、ESP8266.cpp を修正すれば対処できます。
ライブラリをインストールしたディレクトリにある ESP8266.cpp をテキストエディタで開き、703行目(ESP8266::eATCIFSR() 関数内)を以下のように修正して下さい。”\r\r\n” を “\r\n” にするだけです。
修正前
↓ ↓ ↓ ↓ ↓
修正後
マルチプル(複数接続)モードに設定
サーバとして動作させる場合、複数のクライアントからの接続が可能となるように、『マルチプルモード』に設定します。
この設定には、ESP8266::enableMUX() 関数を使用します。書式は以下の通りです。
bool ESP8266::enableMUX(void)
引数
なし
返却値
値 | 意味 |
---|---|
true | モードの設定に成功 |
false | モードの設定に失敗 |
サーバの起動
TCPサーバを起動します。起動には ESP8266::startTCPServer() 関数を使用します。この関数の書式は以下の通りです。
bool ESP8266::startTCPServer( [ uint32_t port = 333 ] )
引数
名前 | 型 | 意味 |
---|---|---|
port | uint32_t | Listenポートの番号。省略すると333が使用される |
返却値
値 | 意味 |
---|---|
true | サーバ起動に成功 |
false | サーバ起動に失敗 |
今回は『なんちゃってWebサーバ』のため、ポート番号は80番を指定しています(あらかじめ #define HOST_PORT 80 として定数を設定しています)。
サーバのタイムアウト時間を設定する
サーバがタイムアウトするまでの時間を設定します。設定には ESP8266::setTCPServerTimeout()関数を使用します。この関数の書式は以下の通りです。
bool ESP8266::setTCPServerTimeout( [ uint32_t timeout = 180 ] )
引数
名前 | 型 | 意味 |
---|---|---|
timeout | uint32_t | タイムアウトするまでの時間を秒単位で指定します。 0~28800秒(=3時間)の範囲が指定可能です。省略した場合は180秒になります。 |
返却値
値 | 意味 |
---|---|
true | 設定に成功 |
false | 設定に失敗 |
タイムアウト値はデフォルトのままでもよいのですが、まぁ関数の説明のために入れておきました。
クライアントから送られてきたリクエストメッセージを受信
if((len=wifi.recv(&mux_id, recvBuffer, sizeof(recvBuffer)-1, 10000))>0){
この部分では、クライアントからのリクエストを待ち受け&クライアントから送られてきたメッセージの受信を行っています。使用している関数は ESP8266::recv() 関数です。この関数の書式は以下の通りです。
uint32_t ESP8266::recv(uint8_t mux_id, uint8_t *buffer, uint32_t buffer_size [ , uint32_t timeout = 1000 ] )
シングル(単一接続)モードでも recv() 関数を使用しましたが、マルチプルモードでは引数が異なります。
引数
名前 | 型 | 意味 |
---|---|---|
mux_id | uint8_t* | 接続中の複数のクライアントを識別する値(参照渡しする) |
buffer | uint8_t* | 受信バッファへのポインタ |
size | uint32_t | 受信バッファのサイズ |
timeout | uint32_t | タイムアウトまでの時間をms単位で指定します。 省略すると1000ms になります。 |
返却値
値 | 意味 |
---|---|
1以上の値 | 受信したデータの長さ(バイト数) |
0 | タイムアウト(制限時間内に受信データがなかった) |
マルチプルモードでは、recv() 関数が待ち受けとメッセージの受信を行います。
recv() 関数は、メッセージを受信するか、またはタイムアウト時間に達するまで待ちます。返却値は受信したデータのバイト数なので、メッセージを受信した場合は返却値が1以上、タイムアウトした場合は返却値が0となり、この2つの場合を区別することができます。
マルチプルモードでは複数のクライアントと同時に接続するため、受信したメッセージがどのクライアントからのものかを区別をする必要があります。このための値が mux_id です。同時に接続可能なクライアント最大数は5なので、これらを区別するため mux_id は0~4の整数値をとります。
mux_id は新規にクライアントが接続した時に決まるので、recv() 関数には mux_id をプログラムからパラメータとして与えるのではなく、接続時に決定した値を関数からプログラムに通知しなければなりません。しかし関数自体の返却値は受信したデータ長であるため、第一引数に変数を参照渡しすることで、関数内でこの変数に mux_id の値を代入するようになっています。
※C言語で、1つの関数から2種類以上の値を得たい場合によく使うテクニックです
受信したメッセージの処理
クライアントからのメッセージを受信したとき(recv()>0のとき)、受信したメッセージの処理を行います。といっても、今回はメッセージの内容を解析などせず、Arduino IDE のシリアルモニタで確認できるように PC に送信しているだけです。
Serial.print("client connected:");
Serial.println(mux_id);
recvBuffer[len]='\0';
Serial.println(recvBuffer);
HTTPの場合、送信されているメッセージはテキストですが、recv() は受信したテキストの末尾に’\0’(ヌル文字)を付加しません。そのまま表示しようとすると不具合があるので、 recvBuffer[len]=’\0′ としてヌル文字を追加しています。
※C言語の多くの文字列処理関数では、『先頭からヌル文字の1つ前までが一続きの文字列』として扱います
クライアントに送信するデータの組み立て
次はサーバからクライアントへ送信するメッセージの組立です。本プログラムはWebサーバとして動作します。本当はクライアントから送られてきたリクエストを分析して、それに応じた処理をしなければいけないのですが、今回は極限まで簡略化したナンチャッテWebサーバとして、クライアントからどんなリクエストが送られてこようが固定のhtmlドキュメントをレスポンスとして送信するようになっています。
sprintf(responseBody, "<html><head><title>test page</title></head><body><h1>Arduino</h1>this is test page.</body></html>\r\n");
sprintf(responseHeader, "HTTP/1.0 200 ok\r\nContent-type: text/html\r\nContent-length: %d\r\n\r\n", strlen(responseBody));
送信するのはヘッダが先ですが、ヘッダ内にボディ長のフィールドがあるので、先にボディ(データ本体、この例ではhtmlドキュメント部分)を組み立ててからヘッダ(付加情報)を組み立てるようにしています。
実際に送信されるデータは以下のようになります。
HTTP/1.0 200 ok
Content-type: text/html
Content-length: 99
<html><head><title>test page</title></head><body><h1>Arduino</h1>this is test page.</body></html>
ヘッダはブラウザで正常に表示するために最低限必要なものだけです。
1行目は、ステータス行といい、リクエストが正常に処理されたかどうかを表します。書式は
version status_code status_message
です。3つのフィールドは半角空白1文字で区切られています。
version はプロトコルのバージョンを表す文字列で、『HTTP/1.0』『HTTP/1.1』『HTTP/2』などです。
status_code および status_message は処理結果を表す数値です。『200 ok』(status_code=200、status_message=”ok”)は処理が正常終了したことを表します。このサンプルプログラムは処理を簡単にするため、リクエスト内容によらず固定の html ドキュメントを返信するので、処理結果としても常に固定の文字列『200 ok』を返します。
2行目、3行目はヘッダです。このサンプルプログラムではブラウザが正常にhtmlドキュメントを表示するのに最小限必要な情報だけを記述しています。
『Content-type: 』は、レスポンスで送信されたデータの型式を表します。『text/html』は内容がhtml文書であることを表します。他に、画像の場合は『image/jpeg』や『image/png』、pdf文書なら『application/pdf』…などがあります。
『Content-Length: 』は、ボディ(データ本体、この例ではhtmlドキュメント部分)のバイト数を表します。
4行目の空行は、ヘッダとボディの区切りを表します。サンプルプログラムでは、ヘッダ(変数responseHeader)の最後で “\r\n\r\n” と改行文字を2回繰り返すことで空行を表現しています。
5行目がデータ本体(htmlドキュメント)です。この例ではバイト数を減らすために改行を入れていません。
responseBody の内容を変更すればブラウザに表示される内容も変更できますが、Arduino は変数領域がグローバル変数/ローカル変数あわせて2kBしかないので、あまり長いドキュメントを送信することはできません。レスポンスボディ格納用の変数 responseBody は256バイト分用意してありますが、ここを大きくしすぎるとコンパイルを通っても実行時に不具合が発生します。たぶん Arduino はメモリ領域管理があまり上手くなく、配列変数の領域が他のデータ領域を破壊してしまうのだと思います。
データの送信
wifi.send(mux_id, responseHeader, strlen(responseHeader));
wifi.send(mux_id, responseBody, strlen(responseBody));
ヘッダ(変数responseHeader)、ボディ(変数responseBody)の順にデータを送信します。
データの送信には、ESP8266::send() 関数を使用します。この関数の書式は以下の通りです。
bool ESP8266::send(uint8_t mux_id, const uint8_t *buffer, uint32_t len)
引数
名前 | 型 | 意味 |
---|---|---|
mux_id | uint8_t | 接続中の複数のクライアントから送信先を識別する値(0~4) |
buffer | const uint8_t | 送信するデータバッファ(uint8_tのデータ列)へのポインタ |
len | uint32_t | 送信するデータの長さ |
返却値
値 | 意味 |
---|---|
true | 送信に成功 |
false | 送信に失敗 |
send() 関数はシングルモードにもありましたが、マルチプルモードでは引数が増えています。第一引数の mux_id は接続中の複数のクライアントの中からデータを送信する相手を指定するためのもので、このサンプルでは直前に recv() 関数で取得した値をそのまま使っています。
TCPの切断
HTTPでは、リクエスト~レスポンスとデータが一往復したらTCP接続を切断することになっています。TCP接続の切断には、ESP8266::releaseTCP() 関数を使用します。この関数の書式は以下の通りです。
bool ESP8266::releaseTCP(uint8_t mux_id)
引数
名前 | 型 | 意味 |
---|---|---|
mux_id | uint8_t | 切断するクライアントを識別する値(0~4) |
返却値
値 | 意味 |
---|---|
true | 切断に成功 |
false | 切断に失敗 |
実行
Arduino と PC をUSBケーブルで接続し、Arduino IDE でシリアルモニタを開いた状態でスケッチを実行してみましょう。
初期化にはしばらく時間がかかります。Arduino のシリアルモニタに、
wifi restart...ok.
check alive...ok.
set station mode...ok.
join to AP...ok.
IP:
+CIFSR:STAIP,"192.168.0.19"
+CIFSR:STAMAC,"b4:e6:2d:46:98:a3"
set multiple mode...ok.
start TCP server...ok.
set timeout...ok.
setup complete.
と表示されたら準備完了です。途中、『IP:』の後にこのサーバのIPアドレス(この例では192.168.0.19)が表示されています。
PC上でブラウザを起動し、URL欄に『http://サーバのIPアドレス/』(この例では『http://192.168.0.19』)と入力してみましょう。
ブラウザにこのように表示されれば成功です。このとき、Arduino IDE のシリアルモニタには、
このように、クライアントからのリクエストメッセージの内容が表示されています(GET / HTTP/1.1の行から下)。
以上で、Arduino + ESP-WROOM-02U をWebサーバとして動作させる実験は終了です。
まとめ
- ESP-WROOM-02 をTCPサーバとして動作させることができる
- サーバとして動作させるには startTCPServer() 関数を使用する
コメント