Arduino、いざEthernetへ。

前回の記録: Ethernetシールドの登場!!
Arduino記録群。今回はEthernetシールドを使ってArduino上のLEDを遠隔操作できるようにするためのスケッチについてです。

前回の記録で決めた条件

  • Arduinoのスケッチだけで実現する。
  • iPhoneでリモート操作できるようにする。
  • リモート操作でLEDの点灯/消灯を制御する。
この条件を満たす方法をいろいろと考えましたが、どうやらHTTP通信を利用した方法が1番手っ取り早いようです。
なぜHTTPなのか?の問いに答えると、
  • ArduinoはWebサーバーとして動かすに十分な機能を備えている。
  • iPhoneにはHTTP通信アプリ(Safari)がプリインストールされている。
  • HTTP通信のGETメソッドを使えば、LEDの制御くらいできそうだったから。
HTTPならウマく行きそうです。
Arduino Ethernet シールド [改訂増分版]   本記録で紹介しているArduinoプログラムの改訂版を公開しています. ぜひ上のリンク先に用意したコードを試して見てください. ちなみに,コードの詳細な説明は省いてありますが,このページに載っている内容とあなたのイマジネーションを働かせればきっとすべてを理解出来るはずです. Qapla'!

1, ArduinoをWebサーバーにする

HTTP通信を利用するためには、"サーバークライアント"という関係を築かなければいけません(参考:HTTP通信)。
  • サービスを提供する側がサーバー
  • サービスを利用する側がクライアント
Arduinoがサーバーに、iPhoneがクライアントになればいいのです。便利なことに、ArduinoIDEのExamplesには"WebServer"というスケッチがあります。これをちょこっと書き換えて今回の目的に合わせたスケッチを作成しました。紹介します。
#include <SPI.h>
#include <Ethernet.h>

boolean skip = false;
boolean catchGET = false;

int oPin = 9;

byte ip [] = {192, 168, 11, 5};
byte mac[] = {0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54};

EthernetServer server = EthernetServer(80);

void setup() {
  pinMode(oPin, OUTPUT);
  digitalWrite(oPin, LOW);
  Ethernet.begin(mac, ip);
  Serial.begin(9600);
  Serial.println(Ethernet.localIP());
  server.begin();
}

void loop() {
  EthernetClient client = server.available();
  
  if (client) {
    while (client.connected()) {
      
      if (client.available()) {
        char c = client.read();
        Serial.write(c);
        
        if (!skip && catchGET) {
          switch (c) {
            case '1' : digitalWrite(oPin, HIGH); break;
            case '0' : digitalWrite(oPin, LOW);  break;
            default  : continue;
          }
          skip = true;
        }
        
        if (c == '?') catchGET = true;
        
      }else{
        
        returnHTML(client);
        break;
        
      }
    }
  }
  
  skip = false;
  catchGET = false;
  client.stop();
}

void returnHTML(EthernetClient client) {

  //---HTTP HEADER---
  client.println("HTTP/1.1 200 OK");
  client.println("Content-Type: text/html");
  client.println();
  
  //---HTML DOC------
  client.println("<!DOCTYPE HTML>");
  client.println("<html>");
  client.println("<head>");
  client.println("<meta name=viewport content=\"width=80px, initial-scale=4, maximum-scale=4, user-scalable=no\" />");
  client.println("<title>A Remote</title>");
  client.println("</head>");
  client.println("<body style=\"color:rgb(205,205,205);background-color:rgb(96,96,96);text-align:center;\">");
  client.println("Remote<br />");
  client.println("<form method=GET>");
  client.println("<input type=submit name=1 value=ON /><br />");
  client.println("<input type=submit name=0 value=OFF />");
  client.println("</form>");
  client.println("</body>");
  client.println("</html>");
  
}
今から、このスケッチについて詳しく説明して行きます(説明をスキップ)。

まずは変数宣言部です。

#include <SPI.h>
#include <Ethernet.h>

boolean skip = false;
boolean catchGET = false;

int oPin = 9;

byte ip [] = {192, 168, 11, 5};
byte mac[] = {0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54};

EthernetServer server = EthernetServer(80);
ヘッダファイルには、SPIEthernetの2つ使います。boolean型の変数2つは、HTTPリクエストヘッダの中身を解析するときに使います。oPinには出力ピン、つまりLEDをつなげるピン番号を格納します。今回はデジタル9番にしたので、配線図は次のようになります。
byte型の配列ipには、Ethernetシールドに割り当てたいIPアドレスを入力します。
IPアドレスは同じネットワークにつながっている機器とかぶってはいけませんので注意しましょう。もし、DHCP(IPアドレス自動割り当て機能)が使えるネットワーク環境ならば、このip宣言をまるまる一行消して、setup()内にあるEthernet.begin(mac, ip);というステートメントをEthernet.begin(mac);に変更しましょう。またこの宣言はあくまでも配列なので、区切り文字はドット(192.168.11.5)ではなくカンマ(192,168,11,5)です。
例では"192.168.11.5"というアドレスを割り当てる設定となっています。配列macも重要です。ここに自分のEthernetシールドのMACアドレスを入力します。0xに続けて2ケタづつ入力してゆきます。例では"FE:DC:BA:98:76:54"というMACアドレスになります。ちなみに0xとは16進数を意味します。EthernetServer型の変数は、その名の通りイーサネットサーバーのオブジェクトです。HTTP通信はポート80番を使用するので、80で初期化します。

次にsetup()です。

void setup() {
  pinMode(oPin, OUTPUT);
  digitalWrite(oPin, LOW);
  Ethernet.begin(mac, ip);
  Serial.begin(9600);
  Serial.println(Ethernet.localIP());
  server.begin();
}
setup()では次の処理を順に行っています。
  1. oPinを出力に設定する。
  2. 念のためその出力を0V(LOW)にする。
  3. イーサネット通信を開始する。
  4. シリアル通信を開始する。
  5. 割り当てられたIPアドレスを表示する(DHCPのときに便利)。
  6. サーバーを開始する。

loop()、各行の意味を説明します。

void loop() {
  
  //サーバーに接続してくるクライアントを取得
  EthernetClient client = server.available();
  
  //接続してきたクライアントがある場合true
  if (client) {
    
    //クライアントが接続している間ずっとループ
    while (client.connected()) {
      
      //クライアントから読み取れるデータがある場合true
      if (client.available()) {
        
        /*  true
        クライアントからデータを受信している時
        (HTTPリクエストを受信中に行う処理などを書く)*/
        
      }else{
        
        /*  false
        クライアントからのデータをすべて受け取った時
        (HTTPレスポンスを返す処理などを書く)*/
        
      }
    }
  }
  
  //初期化処理を書く。
  
}
以下の説明を読む前にHTTPについてを読むことをおすすめします。loop()は分岐が要な関数です(今回の場合"サーバー"をArduino、"クライアント"をiPhone、"読み取れるデータ"をHTTPリクエストと読み換えることができます)。

HTTPリクエスト受信中にすべき処理(true)

  char c = client.read();
  Serial.write(c);
  
  if (!skip && catchGET) {
    switch (c) {
      case 1' : digitalWrite(oPin, HIGH); break;
      case 0' : digitalWrite(oPin, LOW); break;
      default : continue;
    }
    skip = true;
  }
  
  if (c == ?') catchGET = true;
今後この処理を"スイッチャー部"と呼びます。なぜならHTTPリクエストの内容に応じて、処理の切り替え(スイッチング)をするからです。HTTPリクエストの内容とは、GETメソッドに関係してくる事柄なので、これについては後述します。

HTTPリクエスト受信中は、ひたすらその内容をシリアルモニターへ表示します。char型の変数cに、client.read()でクライアントからの受信データを一文字だけ格納しています。LEDを制御する上でポイントとなるのがこの、受信データ(HTTPリクエスト)を"一文字だけ"格納している、という点です。while文の中なので、一文字づつ順番に代入されていきます。
余談ですがHTTPリクエストには特殊文字が含まれています。特殊文字はエスケープとも呼びます。バックスラッシュ(日本では¥マーク)と英字一文字のセットで表します。例:行を次に移す(改行する)ときは"\n"と表します。行頭に移る(リターンする)ときには"\r"と表します。HTTPリクエストの終わりは、\r\n\r\n(リターンと改行が2回連続で続く)です。これを受信したらHTTPリクエストは受信し終えた、と考えていいです。
この"一文字ずつ"という言葉は、このスイッチャー部に無くてはならないものです。GETメソッドについては後述します。

すべて受信し終えた時の処理(false)

 returnHTML(client);
 break; 

void returnHTML(EthernetClient client) {
  
  //---HTTP HEADER---
  client.println("HTTP/1.1 200 OK");
  client.println("Content-Type: text/html");
  client.println();
  
  //---HTML DOC------
  client.println("<!DOCTYPE HTML>");
  client.println("<html>");
  client.println("<head>");
  client.println("<meta name=viewport content=\"width=80px, initial-scale=4, maximum-scale=4, user-scalable=no\" />");
  client.println("<title>A Remote</title>");
  client.println("</head>");
  client.println("<body style=\"color:rgb(205,205,205);background-color:rgb(96,96,96);text-align:center;\">");
  client.println("Remote<br />");
  client.println("<form method=GET>");
  client.println("<input type=submit name=1 value=ON /><br />");
  client.println("<input type=submit name=0 value=OFF />");
  client.println("</form>");
  client.println("</body>");
  client.println("</html>");
  
}
一行目で関数returnHTMLを呼んでいます。線から下がreturnHTML()です。この関数の役目は、HTTPレスポンスとHTML文章をクライアントへ返信することです。HTTP HEADERから下の部分がレスポンスで、HTML DOCから下の部分がHTML文書を返信します。つまり、これがHTTPレスポンすであり、iPhoneに表示されるページということです。

<input type=submit name=1 value=ON />という部分がGETメソッドの送信を司る部分です。nameがキーの値で、valueが文字列の値です(キーと文字列(値)については後述します)。

返信し終え、処理がloop()へ戻ると、breakでwhile文から抜け出します。抜けだすと初期化処理が待っています。この処理は、スイッチャー部分の処理で使ったboolean型の変数をすべて初期値に戻し、client.stop()でクライアントから切断する、という重要な処理を行います。

次はHTTPのGETメソッドについてです。スイッチャー部分でどんな処理をしているのかを説明します。

2, HTTPのGETメソッドを利用する

HTTPのGETメソッドを説明する前に、URLについてお話しましょう。URLは皆さんご存知の通り、インターネット上の資源(リソース)のありかを示すものです。ここでは資源=Webページです。例えば"http://www.storange.jp/"とかがURLです。
ではGETメソッドの話。これはデータの送信方法の一種です。このGETメソッドを使うと、送り先へ文字列を送ることができます。ふつうはキーと値(文字列)をセットで送ります。
キー
Key=Value
こんな感じにキーと値をセットで送る。
受け取った側は、キーを参照して値を読み取ります。つまりキーは変数とも言えます。で、このセットをどうやって送るかというと、URLにくっつけて送るのです。こんな感じに…
送り先GETメソッド
http://storange.com/ ?Key=Value
送り先URLに?がついてたら、それはGETメソッド
送り先URLに"?"をつけます。受け取った側は、この?を見て"GETメソッドで値が送られてきている。どんな値がくっついているのだろう?"と気づくことができます。?に続けてキーと値のセットをつけます。受け取り側はこの値を読み取ることができます。
GETメソッドはHTTPリクエストからも参照できます。追記:GETメソッドで送信可能な文字数は環境によってまちまちですが、もしたくさんの文字列を一度に送りたい場合はPOSTメソッドを使ったほうがいいかもしれません。


Arduinoの話に戻ります。
実は、ArduinoにはGETメソッドを取得する方法はありません。しかし、HTTPリクエストの中身を調べれば取得できるようになります。ではそのHTTPリクエストの中身とやらを見てみましょう。

HTTPリクエストの一例

GET /?1=ON HTTP/1.1
Host: 192.168.11.5
User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 5_1 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/5.1 Mobile/9B179 Safari/7534.48.3
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Referer: http://192.168.11.5/
Accept-Language: ja-jp
Accept-Encoding: gzip, deflate
Connection: keep-alive
これはiPhoneがArduinoへ送ったHTTPリクエストの例です。GETメソッドの部分は一行目の、
GET /?1=ON
です。どうやらiPhoneは、1というキーとONという値のセットをArduinoへ送ったようです。そうです。これはArduinoへ"LEDをONにしてくれ"というお願いなのです。
今回はArduinoの動作を必要最小限にしました。というのも、スイッチャー部の処理は、GETメソッドのキーだけを取得するように設計しました。なので値は無視します。なんと邪道!と思われても仕方ありませんが、今回のスケッチでは、GETメソッドの値はまったく無意味なのです。値は、プログラミングでのコメントアウト程度に考えてください。
ではもう一度、スイッチャー部を見てみましょう。

スイッチャー部

  char c = client.read();
  Serial.write(c);
  
  if (!skip && catchGET) {
    switch (c) {
      case '1' : digitalWrite(oPin, HIGH); break;
      case '0' : digitalWrite(oPin, LOW); break;
      default : continue;
    }
    skip = true;
  }
  
  if (c == '?') catchGET = true;
HTTPリクエストをclient.read()で取得します。で、もしその文字が"?"だった場合、catchGETがtrueになります。初期値でskipはfalseですので、次にHTTPリクエストを一文字取得した時にはif(!skip&&catchGET)がtrueになります。中にはswitch(c)があります。ここではGETメソッドのキーによって処理を振り分けています(スイッチング)。1ならoPinにHIGH。0ならoPinにLOWです。一度ここの処理を通るとskipがtrueになります。こうなると、switch(client.connected)を抜けるまでif(!skip&&catchGET)はfalseに(スキップと)なります。

なぜスキップする必要があるのか?

 実はHTTPリクエストにはRefererというものがあります。これはリンク前のURLを示すものです。GETメソッドの値を変更すると、それはリンクとみなされます。そのとき、HTTPリクエストには2つの"?"が現れることになります(変更前のURLについていたGETメソッドの"?"と、変更後の"?")。変更後の?は先に現れるので、?を一度取得したら次に?が現れても取得しないようにskipして、RefererのGET(?)を無視しているのです。
このスケッチなら、switch文に変更を加えればあらゆることができるようになります。柔軟なカスタマイズってやつです。では最後に、iPhoneの話をしましょう。今までの内容を理解して、実際に遠隔地からArduinoを操作をしてみましょう!

3, iPhoneのブラウザから操作する


接続図
スケッチをArduinoへアップロードする前に、スケッチを自分の環境に合わせて変更する必要があります。変更する部分をもう一度確かめてみます。
  • 配列ipmacを自分のものに変更する。
  • DHCPが使える場合はスケッチからipの部分を消す。
  • 出力ピン(LEDをつなぐピン)の番号を変数oPinに格納する。
以上の項目を確認したら、Arduinoへサクッとアップ。コンパイルエラーが出なかったら、シリアルモニターを開いてみましょう。すると、Arduinoに割り当てられたIPアドレスが表示されるはずです。そのIPアドレスをiPhoneのブラウザSafariのアドレスバーに打ち込みます。

例ではArduinoに"192.168.11.5"番地を割り当てた。
本来ならばアドレスの前に"http://"をつけるべきだが、
最近のブラウザなら省略しても何ら問題はない。
そして"Go"! 次のようなページが表示されれば、ArduinoなWebサーバーは正常に動作しています。

操作性を考慮してViewportを4倍に設定してあります。
ページ"A Remote"が表示されました。さぁ、もうあなたはArduinoを操作するためのリモコンを手に入れました!ぜひONをタップしてみてください。ArduinoのシリアルモニターにはiPhoneが送ったGETメソッド入りのHTTPリクエストが表示されるはずです。そしてまもなく、LEDは点灯するはずです。

HTTP通信で制御しているわりに動作速度が速い!
もしArduinoのIP割り当てに、DHCPでなく固定IPアドレスを設定できるだけの余裕がある人は、次の手順でブックマークしておくと今後便利になるでしょう。ページ"A Remote"でオプションを開いて、"ホーム画面に追加"をタップします。

Safari下部の真ん中のアイコンをタップすると、上のようなオプションが開く。
すると、ホーム画面にアイコンが現れます。
ページのスクリーンショットがそのままアイコンになります。まるで良くできたアプリのようです!

Appleにこんな画像があっても違和感ありません!
茶番はこのくらいにして…。

これをタップすれば、いつでもArduinoにアクセスできます。Arduinoライフです。追記:お気づきでしょうが、もちろんPCのブラウザからでもArduinoへアクセスできます。

付録, GETメソッドを実感する

Googleのサーバー宛にGETメソッドを使って値を送ってみる。

www.google.com/searchというサーバーにGETメソッドを使って値を渡してみましょう。このサーバーにqというキーの値を渡すとその値を検索クエリとした検索結果をWebページとして返してくれます。
例:Arduinoという値で検索してもらう。
https://www.google.com/search?q=arduino
日本語の値を送る場合にはURLエンコードなる処理が必要です。面倒なので英語の値を送ってみてください。(←最近はエンコードなしに日本語を直接送ってみても平気らしい...)
4985115007036559040 http://www.storange.jp/2012/04/arduinoethernet.html http://www.storange.jp/2012/04/arduinoethernet.html Arduino、いざEthernetへ。 2012-04-22T12:30:00+09:00 http://www.storange.jp/2012/04/arduinoethernet.html Hideyuki Tabata 200 200 72 72