Arduino記録群。今回はEthernetシールドを使ってArduino上のLEDを遠隔操作できるようにするためのスケッチについてです。
前回の記録で決めた条件
- Arduinoのスケッチだけで実現する。
- iPhoneでリモート操作できるようにする。
- リモート操作でLEDの点灯/消灯を制御する。
- ArduinoはWebサーバーとして動かすに十分な機能を備えている。
- iPhoneにはHTTP通信アプリ(Safari)がプリインストールされている。
- HTTP通信のGETメソッドを使えば、LEDの制御くらいできそうだったから。
Arduino Ethernet シールド [改訂増分版]
本記録で紹介しているArduinoプログラムの改訂版を公開しています. ぜひ上のリンク先に用意したコードを試して見てください. ちなみに,コードの詳細な説明は省いてありますが,このページに載っている内容とあなたのイマジネーションを働かせればきっとすべてを理解出来るはずです. Qapla'!
1, ArduinoをWebサーバーにする
HTTP通信を利用するためには、"サーバーとクライアント"という関係を築かなければいけません(参考:HTTP通信)。
HTTPリクエスト受信中は、ひたすらその内容をシリアルモニターへ表示します。char型の変数cに、client.read()でクライアントからの受信データを一文字だけ格納しています。LEDを制御する上でポイントとなるのがこの、受信データ(HTTPリクエスト)を"一文字だけ"格納している、という点です。while文の中なので、一文字づつ順番に代入されていきます。
<input type=submit name=1 value=ON />という部分がGETメソッドの送信を司る部分です。nameがキーの値で、valueが文字列の値です(キーと文字列(値)については後述します)。
返信し終え、処理がloop()へ戻ると、breakでwhile文から抜け出します。抜けだすと初期化処理が待っています。この処理は、スイッチャー部分の処理で使ったboolean型の変数をすべて初期値に戻し、client.stop()でクライアントから切断する、という重要な処理を行います。
次はHTTPのGETメソッドについてです。スイッチャー部分でどんな処理をしているのかを説明します。
- サービスを提供する側がサーバー
- サービスを利用する側がクライアント
#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 <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);
ヘッダファイルには、SPIとEthernetの2つ使います。boolean型の変数2つは、HTTPリクエストヘッダの中身を解析するときに使います。oPinには出力ピン、つまりLEDをつなげるピン番号を格納します。今回はデジタル9番にしたので、配線図は次のようになります。
byte型の配列ipには、Ethernetシールドに割り当てたいIPアドレスを入力します。
#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);
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()では次の処理を順に行っています。
pinMode(oPin, OUTPUT);
digitalWrite(oPin, LOW);
Ethernet.begin(mac, ip);
Serial.begin(9600);
Serial.println(Ethernet.localIP());
server.begin();
}
- oPinを出力に設定する。
- 念のためその出力を0V(LOW)にする。
- イーサネット通信を開始する。
- シリアル通信を開始する。
- 割り当てられたIPアドレスを表示する(DHCPのときに便利)。
- サーバーを開始する。
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リクエストと読み換えることができます)。
//サーバーに接続してくるクライアントを取得
EthernetClient client = server.available();
//接続してきたクライアントがある場合true
if (client) {
//クライアントが接続している間ずっとループ
while (client.connected()) {
//クライアントから読み取れるデータがある場合true
if (client.available()) {
/* true
クライアントからデータを受信している時
(HTTPリクエストを受信中に行う処理などを書く)*/
}else{
/* false
クライアントからのデータをすべて受け取った時
(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メソッドに関係してくる事柄なので、これについては後述します。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リクエスト受信中は、ひたすらその内容をシリアルモニターへ表示します。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に表示されるページということです。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>");
}
<input type=submit name=1 value=ON />という部分がGETメソッドの送信を司る部分です。nameがキーの値で、valueが文字列の値です(キーと文字列(値)については後述します)。
返信し終え、処理がloop()へ戻ると、breakでwhile文から抜け出します。抜けだすと初期化処理が待っています。この処理は、スイッチャー部分の処理で使ったboolean型の変数をすべて初期値に戻し、client.stop()でクライアントから切断する、という重要な処理を行います。
次はHTTPのGETメソッドについてです。スイッチャー部分でどんな処理をしているのかを説明します。
2, HTTPのGETメソッドを利用する
キーと値
Key=Value
受け取った側は、キーを参照して値を読み取ります。つまりキーは変数とも言えます。で、このセットをどうやって送るかというと、URLにくっつけて送るのです。こんな感じに…
Key=Value
送り先GETメソッド
http://storange.com/ ?Key=Value
送り先URLに"?"をつけます。受け取った側は、この?を見て"GETメソッドで値が送られてきている。どんな値がくっついているのだろう?"と気づくことができます。?に続けてキーと値のセットをつけます。受け取り側はこの値を読み取ることができます。
http://storange.com/ ?Key=Value
GETメソッドはHTTPリクエストからも参照できます。追記:GETメソッドで送信可能な文字数は環境によってまちまちですが、もしたくさんの文字列を一度に送りたい場合はPOSTメソッドを使ったほうがいいかもしれません。
Arduinoの話に戻ります。
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メソッドの部分は一行目の、
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
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に(スキップと)なります。
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リクエストにはRefererというものがあります。これはリンク前のURLを示すものです。GETメソッドの値を変更すると、それはリンクとみなされます。そのとき、HTTPリクエストには2つの"?"が現れることになります(変更前のURLについていたGETメソッドの"?"と、変更後の"?")。変更後の?は先に現れるので、?を一度取得したら次に?が現れても取得しないようにskipして、RefererのGET(?)を無視しているのです。3, iPhoneのブラウザから操作する
スケッチをArduinoへアップロードする前に、スケッチを自分の環境に合わせて変更する必要があります。変更する部分をもう一度確かめてみます。
ページのスクリーンショットがそのままアイコンになります。まるで良くできたアプリのようです!
茶番はこのくらいにして…。
これをタップすれば、いつでもArduinoにアクセスできます。Arduinoライフです。追記:お気づきでしょうが、もちろんPCのブラウザからでもArduinoへアクセスできます。
- 配列ipとmacを自分のものに変更する。
- DHCPが使える場合はスケッチからipの部分を消す。
- 出力ピン(LEDをつなぐピン)の番号を変数oPinに格納する。
これをタップすれば、いつでもArduinoにアクセスできます。Arduinoライフです。追記:お気づきでしょうが、もちろんPCのブラウザからでもArduinoへアクセスできます。
付録, GETメソッドを実感する
Googleのサーバー宛にGETメソッドを使って値を送ってみる。
www.google.com/searchというサーバーにGETメソッドを使って値を渡してみましょう。このサーバーにqというキーの値を渡すとその値を検索クエリとした検索結果をWebページとして返してくれます。
例:Arduinoという値で検索してもらう。
https://www.google.com/search?q=arduino
https://www.google.com/search?q=arduino
日本語の値を送る場合にはURLエンコードなる処理が必要です。面倒なので英語の値を送ってみてください。(←最近はエンコードなしに日本語を直接送ってみても平気らしい...)
4985115007036559040
https://www.storange.jp/2012/04/arduinoethernet.html
https://www.storange.jp/2012/04/arduinoethernet.html
Arduino、いざEthernetへ。
2012-04-22T12:30:00+09:00
https://www.storange.jp/2012/04/arduinoethernet.html
Hideyuki Tabata
Hideyuki Tabata
200
200
72
72