最終更新2018年9月20日
IoTを語る上で、Arduinoの制御をブラウザから出来るようにしておくことは重要だと考えます。
Ajax
とは「Asynchronous JavaScript + XML」の略。つまり、「JavaScriptとXMLを使って非同期にサーバとの間の通信を行うこと。」としています。
ブラウザ表示はHTMLで記述し、表示内容を変化/遷移するにはページ全体をリロードするのが一般的です。Pageサイズがしれていればそんなに問題視しないのですが、変化が頻繁になると、これは大きな負荷になります。
実際、Arduino のEthernetスケッチですとWebServerのコードをご覧ください。この例だと6ch分のAD変換値をHTMLに記載したREFRESH時間毎に更新して更新表示しています。単に数値表示6ch分の更新ですが、サーバ側(Arduino)この処理はREFRESH毎に新規HTMLを作成して要求に応じています。
不特定多数のクライアントに対して対応するには、この方法はリーズナブルかと思うのですが、1対1の接続で遣り取りしている場合、文字列部分だけ書き直してもらえれば通信量が少なくて済みます。背景に画像なんか貼り付けてあるページでは当然です。
或いは要求処理なんかでも、https://ホスト名:ポート番号/パス?クエリー と云うようなフォームで処理したりします。
もっと効率的な方法論として、
Ajax が挙がります。Ajaxを支える仕組み として、
- XMLHttpRequest
- JavaScript
- DOM
- XML
という要素の組み合わせで実現しています。
以下の記事が正しく示しています。
https://arduinomylifeup.com/arduino-web-server/スケッチは以下の通りです
/* * 20180903 * このコードはWiznet社が提供するExampleコードをレタッチしています。 * CPU:MEGA2560 * EthernetShield WizNet5100搭載のEthernetShield cs=10固定 * LED8個を42〜49pinに割付る */ #include <SPI.h> #include <Ethernet.h> #include <SD.h> #define SD_CS 4 #define EH_CS 10 // Enter a MAC address はスイッチサイエンスが提供した正規のモノ const byte mac[] = {0x00,0x50,0xC2,0x97,0x2C,0x3D}; const byte ip[] = {192, 168, 0, 198}; const byte subnet[] = {255,255,255,0}; const byte gateway[] = {192,168,0,1}; // Initialize the Ethernet server library // (port 80 is default for HTTP): EthernetServer server(80); File webPage; int RED1 = 42; //LED to turn on/off int RED2 = 43; //LED to turn on/off int RED3 = 44; //LED to turn on/off int RED4 = 45; //LED to turn on/off int RED5 = 46; //LED to turn on/off int RED6 = 47; //LED to turn on/off int RED7 = 48; //LED to turn on/off int RED8 = 49; //LED to turn on/off String HTTP_req; // stores the HTTP request void setup() { Serial.begin(115200); // for debugging Ethernet.begin(mac, ip); // initialize Ethernet device server.begin(); // start to listen for clients //while (!Serial) { // ; // wait for serial port to connect. Needed for native USB port only //} // initialize SD card Serial.println(F("Checking SD card is accessible...")); if (!SD.begin(SD_CS)) { Serial.println(F("ERROR - SD card initialization failed!")); return; // init failed } Serial.println(F("SUCCESS - SD card initialized.")); Serial.print(F("Server is at ")); Serial.print(Ethernet.localIP()); HTTP_req = ""; //Set LED to output pinMode(RED1, OUTPUT);digitalWrite(RED1, HIGH); pinMode(RED2, OUTPUT);digitalWrite(RED2, HIGH); pinMode(RED3, OUTPUT);digitalWrite(RED3, HIGH); pinMode(RED4, OUTPUT);digitalWrite(RED4, HIGH); pinMode(RED5, OUTPUT);digitalWrite(RED5, HIGH); pinMode(RED6, OUTPUT);digitalWrite(RED6, HIGH); pinMode(RED7, OUTPUT);digitalWrite(RED7, HIGH); pinMode(RED8, OUTPUT);digitalWrite(RED8, HIGH); } void loop() { // listen for incoming clients EthernetClient client = server.available(); if (client) { Serial.println(F("new client")); // an http request ends with a blank line boolean currentLineIsBlank = true; while (client.connected()) { if (client.available()) { char c = client.read(); // read 1 byte (character) from client // if you've gotten to the end of the line (received a newline // character) and the line is blank, the http request has ended, // so you can send a reply if ( HTTP_req.length() < 80) HTTP_req += c; // save the HTTP request 1 char at a time if (c == '\n' && currentLineIsBlank) { // send a standard http response header client.println(F("HTTP/1.1 200 OK")); client.println(F("Content-Type: text/html")); client.println(F("Connection: close")); client.println(); // send web page if (HTTP_req.indexOf("ajaxrefresh") >= 0 ) { Serial.println(F("ajaxRequest.")); ajaxRequest(client); //update the analog values break; }else if (HTTP_req.indexOf("ajax_inputs") >= 0 ) { Serial.print(F("ledChangeStatus."));Serial.print(HTTP_req); ledChangeStatus(client); //change the LED state break; }else{ webPage = SD.open("index.htm"); // open web page file if (webPage) { while (webPage.available()) { client.write(webPage.read()); // send web page to client } webPage.close(); Serial.println(F("WriteWebPage.")); } break; } if (c == '\n') { // you're starting a new line currentLineIsBlank = true; } else if (c != '\r') { // you've gotten a character on the current line currentLineIsBlank = false; } } } } // give the web browser time to receive the data delay(1); // close the connection: client.stop(); HTTP_req = ""; Serial.println(F("client disconnected")); } // end if (client) } // send the state of the switch to the web browser void ajaxRequest(EthernetClient client) { for (int analogChannel = 0; analogChannel < 6; analogChannel++) { int sensorReading = analogRead(analogChannel); client.print(F("analog input ")); client.print(analogChannel); client.print(F(" is ")); client.print(sensorReading); client.println(F("<br />")); } } void ledChangeStatus(EthernetClient client) { int LEDIndex,LEDState; LEDIndex = HTTP_req.charAt(20) - 48; LEDState = HTTP_req.charAt(22) - 48; //sscanf(HTTP_req,"ajax_inputs&LED%d=%d",LEDIndex,LEDState); Serial.print(F("LEDIndex="));Serial.print(LEDIndex); Serial.print(F("LEDState="));Serial.println(LEDState); if (LEDState == 1) { digitalWrite(LEDIndex+41, LOW); client.print(F("OFF")); }else if(LEDState == 0){ digitalWrite(LEDIndex+41, HIGH); client.print(F("ON")); }else{ ; } }
HTMLの中身を示します。
赤字の数字を変更して更新時間を変更できます。
<HTML> <HEAD> <META NAME="GENERATOR" CONTENT="Adobe PageMill 3.0J Win"> <TITLE>Arduino Ajax LED Button Control</TITLE> <LINK REL="shortcut icon" HREF="favicon.ico"> <SCRIPT> window.setInterval(function(){ nocache = "&nocache=" + Math.random() * 1000000; var request = new XMLHttpRequest(); request.onreadystatechange = function() { if (this.readyState == 4) { if (this.status == 200) { if (this.responseText != null) { document.getElementById("analoge_data").innerHTML = this.responseText; } } } } request.open("GET", "ajaxrefresh" + nocache, true); request.send(null); }, 1000); strLED = ""; //var LED2_state = 0; function GetArduinoIO() { nocache = "&nocache=" + Math.random() * 1000000; var request = new XMLHttpRequest(); request.onreadystatechange = function() { if (this.readyState == 4) { if (this.status == 200) { if (this.responseXML != null) { // XML file received - contains LED states var re; var num_LEDs; var i; var ledstr = ""; re = this.responseXML.getElementsByTagName('LED'); num_LEDs = re.length; for (i = 0; i < num_LEDs; i++) { ledstr = "LED" + (i + 1); if (re[i].childNodes[0].nodeValue === "checked") { document.getElementsByName(ledstr)[0].checked = true; } else { document.getElementsByName(ledstr)[0].checked = false; } } } } } } // send HTTP GET request with LEDs to switch on/off if any request.open("GET", "ajax_inputs" + strLED + nocache, true); request.send(null); setTimeout('GetArduinoIO()', 200); strLED = ""; } // service LEDs when checkbox checked/unchecked function GetCheck(led_num_str, cb) { if (cb.checked) { strLED += ("&" + led_num_str + "=1"); } else { strLED += ("&" + led_num_str + "=0"); } } </SCRIPT> <STYLE> .IO_box { float: left; margin: 0 20px 20px 0; border: 1px solid blue; padding: 0 5px 0 5px; width: 120px; } h1 { font-size: 120%; color: blue; margin: 0 0 10px 0; } h2 { font-size: 85%; color: #5734E6; margin: 5px 0 5px 0; } p, form, button { font-size: 80%; color: #252525; } .small_text { font-size: 70%; color: #737373; } </STYLE> </HEAD> <BODY onload="GetArduinoIO()"> <FORM ID="check_LEDs" NAME="LED_form1"> <H1><TABLE WIDTH="1149" BORDER="1" CELLSPACING="0" CELLPADDING="0"> <TR> <TD WIDTH="100%" BGCOLOR="#00646e"> </TD> </TR> </TABLE></H1> <BLOCKQUOTE> <H1>Analogue Values</H1> <P><div id="analoge_data">Arduino analog input values loading.....</div></P> <H1>Arduino Ajax MEGA 8 Output</H1> <div class="IO_box"> <H2>LED Using Checkbox</H2> <P><INPUT TYPE="checkbox" NAME="LED1" VALUE="0" onclick="GetCheck('LED1', this)" />LED 1 (D42)<BR / CLEAR="ALL"><BR / CLEAR="ALL"></div><div class="IO_box"></P> <H2>LED Using Checkbox</H2> <P></FORM> <FORM ID="check_LEDs" NAME="LED_form2"> <INPUT TYPE="checkbox" NAME="LED2" VALUE="0" onclick="GetCheck('LED2', this)" />LED 2 (D43)<BR / CLEAR="ALL"><BR / CLEAR="ALL"></div><div class="IO_box"></P> <H2>LED Using Checkbox</H2> <P></FORM> <FORM ID="check_LEDs" NAME="LED_form3"> <INPUT TYPE="checkbox" NAME="LED3" VALUE="0" onclick="GetCheck('LED3', this)" />LED 3 (D44)<BR / CLEAR="ALL"><BR / CLEAR="ALL"></div><div class="IO_box"></P> <H2>LED Using Checkbox</H2> <P></FORM> <FORM ID="check_LEDs" NAME="LED_form4"> <INPUT TYPE="checkbox" NAME="LED4" VALUE="0" onclick="GetCheck('LED4', this)" />LED 4 (D45)<BR / CLEAR="ALL"><BR / CLEAR="ALL"></div><div class="IO_box"></P> <H2>LED Using Checkbox</H2> <P></FORM> <FORM ID="check_LEDs" NAME="LED_form5"> <INPUT TYPE="checkbox" NAME="LED5" VALUE="0" onclick="GetCheck('LED5', this)" />LED 5 (D46)<BR / CLEAR="ALL"><BR / CLEAR="ALL"></div><div class="IO_box"></P> <H2>LED Using Checkbox</H2> <P></FORM> <FORM ID="check_LEDs" NAME="LED_form6"> <INPUT TYPE="checkbox" NAME="LED6" VALUE="0" onclick="GetCheck('LED6', this)" />LED 6 (D47)<BR / CLEAR="ALL"><BR / CLEAR="ALL"></div><div class="IO_box"></P> <H2>LED Using Checkbox</H2> <P></FORM> <FORM ID="check_LEDs" NAME="LED_form7"> <INPUT TYPE="checkbox" NAME="LED7" VALUE="0" onclick="GetCheck('LED7', this)" />LED 7 (D48)<BR / CLEAR="ALL"><BR / CLEAR="ALL"></div><div class="IO_box"></P> <H2>LED Using Checkbox</H2> <P></FORM> <FORM ID="check_LEDs" NAME="LED_form8"> <INPUT TYPE="checkbox" NAME="LED8" VALUE="0" onclick="GetCheck('LED8', this)" />LED 8 (D49)<BR / CLEAR="ALL"><BR / CLEAR="ALL"></div></BLOCKQUOTE> </FORM> </BODY> </HTML>
テンプレートをレタッチして問題となるケースはHTTPの遣り取りの中身をきちんと理解しないといけないことかと思います。
Arduinoはデバッグがなかなか難しいところがありますが、Print文でシリアルモニタに書き込むのがリーズナブルです。今回の例では、
Serial.print(F("ledChangeStatus."));Serial.print(HTTP_req);という扱いをして、ブラウザのEventがHTTP_req内にどのような文字列になっているかを確認します。
後は文字列を一生懸命弄るだけです。処理しやすい文字列にしなければいけないことも実際にやってみると判断出来ます。上記コードですが、複数のクライアントが同じサーバにアクセスした場合の挙動を確認しました。
- 当然ですが、複数のクライアントデバイスに対し、WebPageを表示します。
- しかしアクセス権は最後に接続したクライアントのみになります。この点はコードを再検証すればその通りかと思います。
WebPageをReloadすることでアクセス権は復権できるようです。これはこれで正しいのかと思います。- クライアントがアクセス権を持っているかどうかを確認出来る術が出来ないか検討したいところです。
新たなクライアントが接続した時点で、それまでのクライアントに対してdisconnetのメッセージを送れないか、と云う事です。- このスケッチですが、UNOでもビルド出来るのですが実行出来ない事が判りました。実行時にSDカード内に目的のファイルがある事は確認しますがそれを読み出そうとしてエラーになります。詳しいエラー等のチェックはライブラリに任せる必要がありますが、RAM700Byteでは不足のようです。
SDカードが搭載されているのでこのカードを使うのは理に適っていますが、UNO(AVR328p)では制限が多いです。
フラッシュに書き残す方向で作り込むにも今度はフラッシュのサイズ32KBが小さい事に気付きます。
プロジェクトの規模と使うべきCPUの選択は重要ですね。iPhoneからのアクセスでももちろん制御出来ました。Localの無線LAN接続ですが。LTE経由となるとちょっと工夫が要りますね。
免責事項
本ソフトウエアは、あなたに対して何も保証しません。本ソフトウエアの関係者(他の利用者も含む)は、あなたに対して一切責任を負いません。
あなたが、本ソフトウエアを利用(コンパイル後の再利用など全てを含む)する場合は、自己責任で行う必要があります。本ソフトウエアの著作権はToolsBoxに帰属します。
本ソフトウエアをご利用の結果生じた損害について、ToolsBoxは一切責任を負いません。
ToolsBoxはコンテンツとして提供する全ての文章、画像等について、内容の合法性・正確性・安全性等、において最善の注意をし、作成していますが、保証するものではありません。
ToolsBoxはリンクをしている外部サイトについては、何ら保証しません。
ToolsBoxは事前の予告無く、本ソフトウエアの開発・提供を中止する可能性があります。
商標・登録商標
Microsoft、Windows、WindowsNTは米国Microsoft Corporationの米国およびその他の国における登録商標です。
Windows Vista、Windows XPは、米国Microsoft Corporation.の商品名称です。
LabVIEW、National Instruments、NI、ni.comはNational Instrumentsの登録商標です。
I2Cは、NXP Semiconductors社の登録商標です。
その他の企業名ならびに製品名は、それぞれの会社の商標もしくは登録商標です。
すべての商標および登録商標は、それぞれの所有者に帰属します。