4-3.スマホで家電操作(屋内)
スマホで操作する家電リモコン概要
スマホで家電を操作ができる電子工作を行います。ただし、屋外からの家電操作はできません。(屋外からのアクセス方法は5-2で説明しています。)。
今まで学習してきた赤外線リモコンの送受信やWebサーバ機能などを活かし実現していきます。
電気回路と電子工作
今回利用するのは、LED制御とリモコン送受信を実施するために今までに学習した2-1,2-5,2-6の回路を、一つにまとめた回路となります。まとめた回路図とブレッドボード配線図を以下に示します。
【ESP32-DevKitCの入出力端子はこちらを参照下さい】
関連フォルダ及びスケッチ、HTML関連ファイル
利用するファイル構成を以下に示します。
スケッチは「4_3_remocon.ino」と「irRecvSend.ino」です。また、dataフォルダ内はSPIFFSデータとして本体に書き込み、WebサーバでHTMLファイル等を送信することでWebブラウザの画面表示に利用しています。
そのため、関連ファイルは以下の大きく2種類となります。
1)スケッチ(Arduino制御プログラム)
ファイル名:4_3_remocon.ino, irRecvSend.ino
2)HTML関連(HTMLとJavascriptファイル)
ファイル名:top.html, set.html, rem.js
スケッチでLED制御やリモコンの送受信、また、データ保存や読出などの制御を行います。スマホへの画面表示や制御に関してHTML関連ファイルで実現しています。
各ディレクトリとファイルについて説明します。
(1)4_3_remocon(ディレクトリ)
スケッチのディレクトリです。本電子工作で利用するスケッチが格納されています。
(2)data(ディレクトリ)
SPIFFSアップローダーで利用するディレクトリです。
HTML関連ファイルが格納されており、ディレクトリ内のファイルが全て本体に書き込まれ、SPIFFSで読み込みできます。
(3)4_3_remocon.ino
スケッチの全体・共通部分のプログラムで内容は以下となります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 |
// IoT電子工作キット // 4-3.スマホで操作する家電リモコン(屋内) // ①ライブラリを読み込み #include <WiFi.h> #include <EEPROM.h> #include <DHT.h> #include <SPIFFS.h> #include <ESPAsyncWebServer.h> // ②ピン配置 const byte LED_PIN = 25; // LED緑 const byte IR_R_PIN = 36; // リモコン受信 const byte IR_S_PIN = 32; // リモコン送信 bool ledFlag = true; // LED制御フラグ // ③EEPROMで利用する型(構造体)を宣言(ボタン名を保存する型) struct st_remocon { char remo_name[65]; }; // ④Wi-Fi設定 const char *ssid = "AirPort55874"; const char *password = "6daa0a8a475badc3284509bbbd7a7eeb65ab0d2f3c28147066f969360df4a4c7"; //const char *ssid = "##### SSID #####"; //const char *password = "### PASSWORD ###"; IPAddress ip(192, 168, 1, 123); // IPアドレス(本機が利用するIP) IPAddress gateway(192, 168, 1, 1); // デフォルトゲートウェイ IPAddress subnet(255, 255, 255, 0); // サブネットマスク // ⑤WebServerを定義 ポート番号:80 AsyncWebServer webServer ( 80 ); // ⑥リモコンデータ保存用 (unsigned short型の配列1500個) unsigned short irData[1500]; void setup(void) { // ⑦Serial設定 Serial.begin(115200); Serial.println ( ); // ⑧SPIFFS開始 SPIFFS.begin(); // ⑨EEPROM開始(サイズ指定:ボタン名10個分[65*10]) EEPROM.begin(650); // ⑩ピンモード設定 pinMode ( LED_PIN, OUTPUT ); pinMode ( IR_R_PIN, INPUT ); pinMode ( IR_S_PIN, OUTPUT ); // ⑪無線Wi-Fi接続 WiFi.config( ip, gateway, subnet ); WiFi.begin ( ssid, password ); // ⑫Wi-Fi接続処理(接続するまで無限ループ) while ( WiFi.status() != WL_CONNECTED ) { // ⑬LEDを1秒毎に点滅する ledFlag = !ledFlag; digitalWrite(LED_PIN, ledFlag); delay ( 1000 ); Serial.print ( "." ); } // ⑭Wi-Fi接続できたのでシリアルモニターにIPアドレス表示 Serial.print ( "Wi-Fi Connected! IP address: " ); Serial.println ( WiFi.localIP() ); // ⑮LED点灯(Wi-Fi接続状態) digitalWrite ( LED_PIN, true ); // ⑯WebServer処理設定(制御画面を送信) webServer.on("/", HTTP_GET, [](AsyncWebServerRequest *request){ Serial.println ( "Top" ); request->send(SPIFFS, "/top.html", "text/html"); }); // ⑰WebServer処理設定(制画面のJavascriptを送信) webServer.on("/rem.js", HTTP_GET, [](AsyncWebServerRequest *request){ Serial.println ( "js" ); request->send(SPIFFS, "/rem.js", "text/html"); }); // ⑱WebServer処理設定(設定画面を送信) webServer.on("/set", HTTP_GET, [](AsyncWebServerRequest *request){ Serial.println ( "set" ); request->send(SPIFFS, "/set.html", "text/html"); }); // ⑲WebServer処理設定(ボタン情報送信) webServer.on("/getrem", HTTP_GET, [](AsyncWebServerRequest *request){ Serial.println ( "getrem" ); getRemocon(request); }); // ⑳WebServer処理設定(リモコン受信設定) webServer.on("/setrem", HTTP_GET, [](AsyncWebServerRequest *request){ Serial.println ( "setrem" ); setRemocon(request); }); // ㉑WebServer処理設定(リモコン送信) webServer.on("/cntrem", HTTP_GET, [](AsyncWebServerRequest *request){ Serial.println ( "cntrem" ); contRemocon(request); }); // ㉒WebServer開始処理 webServer.begin(); Serial.println ( "Web server started" ); } // ㉓loop処理 void loop(void){ } // ㉔ボタン情報送信関数 void getRemocon(AsyncWebServerRequest *request) { // ㉕送信データを作成(JSON形式) String senddata = "{"; // ㉖EEPROMデータを保存する変数を宣言 st_remocon remRom; // ㉗ボタン情報10個分を読み出し返答する for (byte i = 0; i < 10; i++) { // ㉘EEPROMメモリ位置を計算 int memPos = (65 * i); // ㉙前回の値'O:'が残らないように消す remRom.remo_name[0] = 'n'; // ㉚EEPROMからデータ取得 EEPROM.get<st_remocon>(memPos, remRom); // ㉛データが保存されているかCheck if (remRom.remo_name[0] == 'O' && remRom.remo_name[1] == ':' ) { // ㉜返答文字列長が1を超える場合は","をつける(2つ目以降との区切り) if (senddata.length() > 1) { senddata += ","; } // ㉝返答する値を一度String型に置き換える("O:"を削除するため) String getirname = String(remRom.remo_name); // ㉞返答文字列作成("O:"を削除するため2から最後まで) senddata += "\"" + (String)i + "\":\"" + getirname.substring(2,getirname.length()) + "\""; } } // ㉟最後にJSONデータを閉じるため"}"をつける senddata += "}"; // ㊱作成した返答(JSON)データをWebサーバから返答する request->send(200, "text", senddata); Serial.println( "getRemocon:" + senddata); } |
3-2,4-2で学習したWebサーバとSPIFFS利用の内容と基本は同じですので、変更したところを中心に説明します。
⑯〜⑱でアクセスされるURLを設定しています。以下の3つは関数を呼びたしdataフォルダにあるファイルを送信する処理を実施しています。
webServer.on ( “/”, HTTP_GET, … // 制御画面を送信
webServer.on ( “/rem.js”, HTTP_GET, … // 制画面のJavascriptを送信
webServer.on ( “/set”, HTTP_GET, … // 設定画面を送信
⑲はEEPROM内にあるボタン名をJSON形式で送信しています。
webServer.on ( “/getrem”, HTTP_GET, … // 画面のボタン情報送信
⑳、㉑の2つはリモコン送受信処理をするための関数を呼び出しています。
webServer.on ( “/setrem”, HTTP_GET, … // リモコン受信
webServer.on ( “/cntrem”, HTTP_GET, … // リモコン送信
次にgetRemocon関数はEEPROM内のボタン名情報をJSON形式で送信する処理を実施しています。JSON形式は多くのプログラム言語で容易に扱うことができる機能が提供されてため多くのシステムで利用されています。{}内に名前に対する値をコロンで区切り表記し、データの間をカンマで区切り、以下のように表記することができます。配列などの表記も可能ですが今回は以下のシンプルな表記のみ利用しています。
{“name1”:”value1”,”name2”:”value2”}
㉕で送信データを保存する変数senddataを宣言し最初の”{”を代入しています。㉗0から9までのボタン名を順次処理し、㉘でEEPROMの保存しているメモリ位置が0から9で異なるため計算し㉚でEEPROMからメモリ位置のデータを取得しています。
㉛でデータの最初の2文字が”O:”の場合のみ情報があるとしています(データ保存時に設定)ので、チェックし存在すればsenddataに追加します。㉜は2つ目以降のデータは”,”を追加しデータ間には”,”で区切ります。㉞のgetirname.substring(2,getirname.length())はString型のメソッドsubstringを使い、0から始まるので0と1文字”O:”を含めず、2文字目から最後までの文字列を切り取っています。
㉟でデータの最後に”}”を追加し、㊱で作成したJSONデータを返送します。
(4)irRecvSend.ino
リモコン送受信を処理する関数についてファイルを分けて作成します。同じファイルで作成しても特に何も変わりませんが、プログラムを見やすく、また開発しやすくするためにファイルを分けて作成します。
ArduinoIDEでタブを分けることでファイルを分けることができます。下図のように一番右上の「▼」マークをクリックし「新規タブ」を作成しファイル名を入力することでファイルが分かれて作成されます。
作成したファイル(タブ)にリモコン送受信に関するプログラムを記載します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 |
// ①赤外線受信(信号受信 or 30秒間を処理) void setRemocon (AsyncWebServerRequest *request) { // ②利用する変数を宣言 unsigned short irCount = 0; // HIGH,LOWの信号数 uint32_t lastt = 0; // 1つ前の経過時間を保持 uint32_t deltt = 0; // 1つ前の経過時間を保持 uint32_t sMilli; // 本処理の開始時間 uint32_t wMilli; // 赤外線の待ち開始時間 uint32_t sMicro; // 処理の開始時間 uint32_t cMicro; // 現在時間 bool rState = 0; // 赤外線受信モジュールの状態 0:LOW,1:HIGH // ③現在の時間をミリ秒で取得 sMilli = millis(); // ④特定条件(信号受信 or 30秒経過)するまで無限ループ while(1) { // ⑤Ir受信を待つ開始時間を取得 wMilli = millis(); // ⑥反転信号の受信待ち while (digitalRead(IR_R_PIN) == rState) { // ⑦待ち始めて0.5秒以上経過したら if (millis() - wMilli > 500) { // ⑧待ち始めて0.5秒以上経過したら if ( irCount > 10 ) { Serial.println(""); Serial.println("recvOK!"); delay(1); // ⑨ボタン名をEEPROMへ、リモコンデータをファイルへ保存 //xTaskCreatePinnedToCore(saveIr,"saveIr", 4096, ((irCount - 1), request), 1, NULL, 1); //Task1実行 if (saveIr( (irCount - 1), request)) { // ⑩正常に終了したため、WebサーバからOKを返答 request->send(200, "text", "OK"); Serial.println("setRemocon OK"); } else { // ⑪信号保存に失敗したためNGを返答 request->send(200, "text", "NG"); Serial.println("setRemocon NG"); } return; // ⑪正常に完了 } // ⑫0,1信号が10個以上ない場合は雑音のため再度ゼロから受信 irCount = 0; } // ⑬処理が15秒以上経過したらタイムアウト if ( millis() - sMilli > 15000 ) { request->send(200, "text", "NG");// ⑭タイムアウトしたのでNGを送信 Serial.println("setRemocon NG"); return; // ⑮15秒経過で処理終了(受信なし) } } // ⑯信号受信開始時の現在の時間や経過時間を取得 if ( irCount == 0 ) { sMicro = micros(); lastt = 0; irCount++; Serial.print("ir:"); // ⑰信号受信処理開始後の処理(irCountが1以上) } else { // ⑱赤外線受信部の状態変化が最後に変化した時間からの経過時間を計算 cMicro = micros(); deltt = ( (cMicro - sMicro)/ 10 ) - lastt; irData[(irCount - 1 )] = deltt; // ⑲次回経過時間計算のため最後に変化した経過時間を保存 lastt = lastt + deltt; irCount++; Serial.print( deltt ); Serial.print(","); } // ⑳次回While内で状態変化を検知する値を変更 rState = !rState; } } // ㉑ボタン名をEEPROMへ、リモコンデータをファイルへ保存 bool saveIr(unsigned short irLength, AsyncWebServerRequest *request){ String setirname = ""; String setNumStr = ""; // ㉒ボタン番号の取得とチェック(HTTP GETリクエストのパラメータ) if (request->hasParam("n")) { setNumStr = request->getParam("n")->value(); } else { return false; } // ㉓ボタン名の取得とチェック(HTTP GETリクエストのパラメータ) if (request->hasParam("a")) { setirname = request->getParam("a")->value(); } else { return false; } // ㉔ボタン番号をString型をint型へ変換 int setNum = setNumStr.toInt(); // ㉕ボタン名に識別用文字"O:"を先頭に付与 setirname = "O:" + setirname; // ㉖EEPROMに保存用の型が一致した変数を定義 st_remocon remRom; // ㉗Stringからchar型に変換(終了文字を付与するためLengthは+1) setirname.toCharArray(remRom.remo_name, setirname.length()+1); // ㉘メモリ位置を計算しEEPROMへ書き込み int memPos = (65 * setNum); EEPROM.put<st_remocon>(memPos, remRom); EEPROM.commit(); Serial.println("setIr:" + String(setNum) + ":" + setirname); // ㉙リモコン信号を保存するためファイル名を作成(ファイル名はボタン番号) String t_file = "/" + setNumStr; Serial.println( "recvFile:" + t_file); // ㉚ファイルを書き込みモードで開く File fw = SPIFFS.open(t_file.c_str(), "w"); // ㉛リモコン信号の長さを最初に書き込み(最初の行) fw.println( String( irLength, HEX ) ); // ㉜リモコン信号の0,1の時間長さを書き込み(2行目以降) for (int i = 0; i < irLength; i++) { fw.println( String( irData[i], HEX ) ); } // ㉝書き込みが完了したのでファイルを閉じる fw.close(); // ㉞正常に処理が完了したのでtrueを返す return true; } // ㉟赤外線送信処理 void contRemocon (AsyncWebServerRequest *request) { // ㊱変数宣言 unsigned short irCount = 0; // HIGH,LOWの信号数 unsigned long l_now = 0; // 送信開始時間を保持 unsigned long sndt = 0; // 送信開始からの経過時間 String setNumStr = ""; // 送信番号保存 // ㊲ボタン番号を取得とチェック(HTTP GETリクエストのパラメータ) if (request->hasParam("n")) { setNumStr = request->getParam("n")->value(); } else { Serial.println("send NG"); request->send(200, "text", "NG"); return; } // ㊳リモコン信号をファイルから取得(ファイル名はボタン番号) String t_file = "/" + setNumStr; Serial.println( "sendFile:" + t_file); // ㊴ファイルを開く File fr = SPIFFS.open(t_file.c_str(), "r"); if(!fr || fr.isDirectory()){ Serial.println("FileOpen NG"); request->send(200, "text", "NG"); return; } // ㊵最初の1行のみ読み出し信号長さ(0,1の数)を取得 String snum = fr.readStringUntil('\n'); // ㊶1行目の文字列を数値型に変換 irCount = strtol(snum.c_str(), NULL, 16); Serial.print( "sendData:" + String(irCount)); // ㊷一旦ファイルから変数irDataに読み出し保存 for (int i = 0; i < irCount; i++) { snum = fr.readStringUntil('\n'); irData[i] = strtol(snum.c_str(), NULL, 16); Serial.print("," + String(irData[i])); } fr.close(); // ㊸送信開始時間を取得 l_now = micros(); // ㊹0,1の信号回数分をFor文でループ for (int i = 0; i < irCount; i++) { // ㊺送信開始からの信号終了時間を計算 sndt += irData[i]; do { // ㊻iが偶数なら赤外線ON、奇数ならOFFのまま // キャリア周波数38kHz(約26μSec周期の半分)でON時間で送信 digitalWrite(IR_S_PIN, !(i&1)); microWait(13); // ㊼キャリア周波数38kHz(約26μSec周期の半分)でOFF時間で送信 digitalWrite(IR_S_PIN, 0); microWait(13); // ㊽送信開始からの信号終了時間が超えるまでループ } while (long(l_now + (sndt * 10) - micros()) > 0); } // ㊾正常に終了したので、WebサーバよりOKを返答 request->send(200, "text", "OK"); Serial.println("send OK"); } // ㊿マイクロ秒単位で待つ void microWait(signed long waitTime) { unsigned long waitStartMicros = micros(); while (micros() - waitStartMicros < waitTime) {}; } |
setRemocon関数は2-5のリモコン受信処理で学習した内容と基本は同じですが、完了時にボタン名とリモコン信号を受信する処理⑨などを追加しています。
saveIr関数でボタン名をEEPROMへ書き込み、リモコンデータをファイルへ保存しています。EEPROMの書き込みは㉒から㉘で処理しボタン名を保存しています。また、SPIFSを利用してリモコン信号のファイルへの書き込みは㉙から㉝で処理します。
㉒はHTTPのGETで送信された値を取得しています。送信される値はアクセスURLに「?name1=value1&name2=value2」のようにデータが付加されて要求されます。”?”のあとはデータとなりデータ間は”&”で繋がれます。データ(value)は「webServer.arg(“name1″)」のように取得することができます。
㉕でボタン名が存在していることを明示するため”O:”を追加し㉘で該当ボタンのメモリ位置で保存します。ボタン名のByte数は65なのでメモリ位置も65Byte毎にボタン名を0から保存しています。例えば、ボタン番号が2の場合は65*2=130Byteの位置に保存することになります。
リモコン信号のファイル保存は㉙でボタン番号をファイル名で作成し㉛で最初の行に信号長さ(0,1の信号数)を保存し㉜で2行目から0,1の信号長さを書き込みます。また、「fw.println( String( irLength, HEX ) );」のHEXは扱うデータを小さくするため16進数で保存しています。
リモコン送信処理のcontRemocon関数は2-6で学習した内容と同じですが、ファイルから読み込み送信する処理として㊲から㊷までを追加して変更しています。㊵は0,1の信号数の最初の1行のみ読み込んでいます。㊶は数値型にしていますが16進数で保存していたので10進数に戻すため「strtol(snum.c_str(), NULL, 16);」で16(進数)を指定しています。
㊷から2行目以降を同様にファイルから読み出しirDataに保存し、㊸から読み出したirDataデータの通りリモコン信号を送信しています。
(5)top.html
top.htmlはWebアクセス時に表示される制御画面です。HTMLファイルの内容は以下の通りです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
<!doctype html> <html> <!-- ①headタグ --> <head> <meta charset='UTF-8'/> <meta name='viewport' content='width=device-width'/> <!-- ②StyleSheet --> <style type='text/css'><!-- #contents { width: 100%; max-width: 320px; } #menu{ color: #fff; background: #222; } .autumn { background-image: -webkit-radial-gradient(27% 185%, #F9F6F1 0%, #D7D0C5 100%); background-image: radial-gradient(27% 185%, #F9F6F1 0%, #D7D0C5 100%); } button { width:155px; height:35px } #dispStatus{ color: #f00; } footer { text-align: right; } --></style> <!-- ③Javascript --> <script type='text/javascript' src='rem.js'></script> </head> <!-- ④bodyタグ --> <body class='autumn'><center><div id='contents'> <!-- ⑤headerタグ --> <header>< h3>IoT電子工作キット<font size=-1> [ver1]</font></h3></header> <!-- ⑥menu名 --> <div id='menu'>リモコン制御画面</div> <div align=right><a href='/set'>[設定]</a></div> <!-- ⑦tableタグ --> <table> <tr> <!-- ⑧buttonタグ --> <td><button id='btn0' class='cntbtn' onClick="snd(0)"> <font size=+1><span id='spn0'>-</span></font></button></td> <td><button id='btn1' class='cntbtn' onClick="snd(1)"> <font size=+1><span id='spn1'>-</span></font></button></td> </tr> <tr> <td><button id='btn2' class='cntbtn' onClick="snd(2)"> <font size=+1><span id='spn2'>-</span></font></button></td> <td><button id='btn3' class='cntbtn' onClick="snd(3)"> <font size=+1><span id='spn3'>-</span></font></button></td> </tr> <tr> <td><button id='btn4' class='cntbtn' onClick="snd(4)"> <font size=+1><span id='spn4'>-</span></font></button></td> <td><button id='btn5' class='cntbtn' onClick="snd(5)"> <font size=+1><span id='spn5'>-</span></font></button></td> </tr> <tr> <td><button id='btn6' class='cntbtn' onClick="snd(6)"> <font size=+1><span id='spn6'>-</span></font></button></td> <td><button id='btn7' class='cntbtn' onClick="snd(7)"> <font size=+1><span id='spn7'>-</span></font></button></td> </tr> <tr> <td><button id='btn8' class='cntbtn' onClick="snd(8)"> <font size=+1><span id='spn8'>-</span></font></button></td> <td><button id='btn9' class='cntbtn' onClick="snd(9)"> <font size=+1><span id='spn9'>-</span></font></button></td> </tr> </table> <!-- ⑨状態表示 --> <div id='dispStatus'><br></div> <!-- ⑩footerタグ --> <footer><font size=-1>2019 IoT電子工作キット [SOCINNO Inc.]</font></footer> </div></center></body> </html> |
画面表示を綺麗にするため②Styleタグでカスケーディング・スタイルシート:CSS(Cascading Style Sheet)を設定しています。CSSにより画面の見栄えを細かく指定しています。
CSSはセレクタで該当の文字列を指定し{}でプロパティと値を指定します。セレクタは先頭文字でHTMLの指定する属性が変更され以下のようになっています。
・ “半角英数字” : HTMLタグ
・ ”#” : HTMLタグのid属性
・ ”.” : HTMLタグのclass属性
例えば「#contents { width: 100%; max-width: 320px; }」HTMLタグ属性のidが”contents”の場合にこの規定に従い、Width(画面幅)は100%で表示し、max-width(最大画面幅)を320pxと指定しています。
セレクタ「.autumn」はHTMLタグのclass属性が”automn”の場合に適用され、バックグランドのグラデーション表記をプロパティと値で指定しています。
③でJavascriptのソール(元)ファイルを指定しており、”/rem.js” にアクセスしてJavascriptファイルをダウンロードします。
⑧のbuttonタグはクリックした場合にonclick属性でJavascriptのsend関数を呼び出しますが、Javascriptはダウンロードして読み込むため、「top.html」にJavascriptは記載されていません。
⑨のdivタグのIDが”dipsstatus”はJavascriptで状態が動的に書き込まれます。
top.htmlをWebブラウザで画面表示すると以下のように表示されます。
(6)set.html
設定画面を表示するHTMLファイルで内容は以下の通りです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
<!doctype html> <html> <!-- ①headタグ --> <head> <meta charset='UTF-8'/> <meta name='viewport' content='width=device-width'/> <!-- ②StyleSheet --> <style type='text/css'><!-- #contents { width: 100%; max-width: 320px; } #menu{ color: #fff; background: #222; } .autumn { background-image: -webkit-radial-gradient(27% 185%, #F9F6F1 0%, #D7D0C5 100%); background-image: radial-gradient(27% 185%, #F9F6F1 0%, #D7D0C5 100%); } input { width:80px; height:15px; } .setbtn { width:47px; height:22px; padding:0; } #dispStatus{ color: #f00; } footer { text-align: right; } --></style> <!-- ③Javascript --> <script type='text/javascript' src='rem.js'></script> </head> <!-- ④bodyタグ --> <body class='autumn'><center><div id='contents'> <!-- ⑤headerタグ --> <header>< h3>IoT電子工作キット<font size=-1> [ver1]</font></h3></header> <!-- ⑥menu名 --> <div id='menu'>リモコン設定画面</div> <div align=right><a href='/'>[制御]</a></div> <!-- ⑦tableタグ --> <table> <tr> <!-- ⑧inputタグ --> <td align=left >1<input id='ir0'/> <button class='setbtn' onClick="rcv(0)">設定</button></td> <td align=right>2<input id='ir1'/> <button class='setbtn' onClick="rcv(1)">設定</button></td> </tr> <tr> <td align=left >3<input id='ir2'/> <button class='setbtn' onClick="rcv(2)">設定</button></td> <td align=right>4<input id='ir3'/> <button class='setbtn' onClick="rcv(3)">設定</button></td> </tr> <tr> <td align=left >5<input id='ir4'/> <button class='setbtn' onClick="rcv(4)">設定</button></td> <td align=right>6<input id='ir5'/> <button class='setbtn' onClick="rcv(5)">設定</button></td> </tr> <tr> <td align=left >7<input id='ir6'/> <button class='setbtn' onClick="rcv(6)">設定</button></td> <td align=right>8<input id='ir7'/> <button class='setbtn' onClick="rcv(7)">設定</button></td> </tr> <tr> <td align=left >9<input id='ir8'/> <button class='setbtn' onClick="rcv(8)">設定</button></td> <td align=right>10<input id='ir9'/> <button class='setbtn' onClick="rcv(9)">設定</button></td> </tr> </table> <!-- ⑨状態表示 --> <div id='dispStatus'><br></div> <!-- ⑩footerタグ --> <footer><font size=-1>2019 IoT電子工作キット [SOCINNO Inc.]</font></footer> </div></center></body> </html> |
top.html(制御画面)と同じ内容が多くありますので、異なる内容を中心に説明します。
⑧の部分で、inputタグでボタン名を入力し設定することができるようになっています。buttonタグはクリックした場合にonclick属性でJavascriptのrcv関数を呼び出し、inputタグ内の入力値も取得し設定しています。
set.htmlをWebブラウザで画面表示すると下図のように表示されます。
(7)rem.js
制御・設定画面では操作性を向上させるためJavascriptで通信したりHTML画面を動的に変更したりします。HTMLでは実現できないページを遷移せずにHTMLを書き換えルことができるので操作性の高いUIを実現できます。
具体的にJavascriptの内容についてプログラムを用いて説明していきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 |
// ①onloadは画面が読み込まれた時に実行 window.onload = function () { // ②リモコンボタン情報の更新処理実行 updateIr(); } // ③リモコンボタン情報取得・表示 function updateIr() { var xhr = new XMLHttpRequest(); var url = window.location.href; var urlarr = url.split("/"); // ④アクセスURL作成(例:http://192.168.1.123:12193/getrem) url = "http://" + urlarr[2] + "/getrem"; xhr.timeout = 5000; xhr.ontimeout = function(e){ document.getElementById('dispStatus').innerHTML = "<b>本体へのアクセスが失敗</b>"; }; xhr.open("GET", url); xhr.send(""); xhr.addEventListener("load",function(ev){ var resGtStr = xhr.responseText; var gtRecv = JSON.parse(resGtStr); // ⑤ボタン10個分のデータを確認 for ( i=0 ; i<10 ; i++) { // ⑥受信データ(ボタン名)が存在するか確認 if ( gtRecv[i] != "" && typeof gtRecv[i] !== "undefined" ) { var idname = "spn" + i; if ( document.getElementById(idname) != null ) { // ⑦"spn0-9"が存在した場合(制御画面)にボタン名を入力 document.getElementById(idname).innerHTML = gtRecv[i]; } else { // ⑧"spn0-9"が存在しない場合(設定画面)にボタン名を"ir0-9"に入力 idname = "ir" + i; document.getElementById(idname).value = gtRecv[i]; } } else { // ⑨"btn0-9"が存在した場合(制御画面)でボタン名がなければボタンを無効化 var idname = "btn" + i; if ( document.getElementById(idname) != null ) { document.getElementById(idname).disabled = true; } } } }); } // ⑩グローバル変数を定義(送受信の関数で利用) irFlg=false; // 受信処理中フラグ(true:処理中、false:処理可能) flgRed=true; // 状態表示の表示・非表示フラグ count=0; // 1秒毎にカウントしタイムアウト判定用 rcvTimer=15; // タイムアウト秒数 // ⑪リモコン信号処理 function snd(setNum) { // ⑫処置中判定 if (irFlg) { // ⑬処置中の場合は処理中表示し終了 document.getElementById('dispStatus').innerHTML = "<b>処理中です</b>"; return; } // ⑭処置フラグを処理中とし、受信中表示処理実施 irFlg=true; document.getElementById('dispStatus').innerHTML = "<b>リモコン送信中</b>"; var xhr = new XMLHttpRequest(); var url = window.location.href; var urlarr = url.split("/"); // ⑮アクセスURL作成(例:http://192.168.1.123:12193/cntrem?n=1) url = "http://" + urlarr[2] + "/cntrem?n=" + setNum; xhr.timeout = 5000; xhr.ontimeout = function(e){ dispfail(); }; xhr.open("GET", url); xhr.send(""); xhr.addEventListener("load",function(ev){ var resStr = xhr.responseText; // ⑯OKを受信したらif文内を状態表示。それ以外はelse内を状態表示 if ( resStr.indexOf("OK") != -1 ) { document.getElementById('dispStatus').innerHTML = "<b>リモコン送信しました。</b>"; } else { document.getElementById('dispStatus').innerHTML = "<b>リモコン送信に失敗しました。</b>"; } // ⑰処置フラグを戻す irFlg=false; }); } // ⑱リモコン受信処理 function rcv(setNum){ // ⑲処理カウンタリセット count = 0; // ⑳処置中判定 if (irFlg) { // ㉑処置中の場合は処理中表示し終了 document.getElementById('dispStatus').innerHTML = "<b>処理中です</b>"; return; } // ㉒処置フラグを処理中とし、受信中表示処理実施 irFlg=true; setMsgTenmetu(); // ㉓入力したボタン名を取得 var idname = "ir" + setNum; var setName = document.getElementById(idname).value; // ㉔受信設定処理へ本体へアクセス var xhr = new XMLHttpRequest(); var url = window.location.href; var urlarr = url.split("/"); url = "http://" + urlarr[2] + "/setrem?n=" + setNum + "&a=" + setName; xhr.timeout = rcvTimer * 1000; xhr.ontimeout = function(e){ dispfail(); }; xhr.open("GET", url); xhr.send(""); xhr.addEventListener("load",function(ev){ var resStr = xhr.responseText; // ㉕OKを受信したらif文内を状態表示。それ以外はelse内を状態表示 if ( resStr.indexOf("OK") != -1 ) { // ㉖フラグを受信完了へ。状態表示に完了表示 irFlg=false; document.getElementById('dispStatus').innerHTML = "<b>リモコン設定が完了しました。</b>"; } else { // ㉗失敗表示 dispfail(); } }); } // ㉘状態表示の点滅処理(リモコン受信中) function setMsgTenmetu(){ // ㉙受信完了していない。かつ、タイムアウト前 if (irFlg == true && count < rcvTimer ) { // 受信完了してなければ // ㉚flgRedで1秒毎にIF文内とelse文内を交互に表示(受信中を点滅) if(flgRed){ document.getElementById('dispStatus').innerHTML = "<b>リモコン受信中(" + rcvTimer + "秒間)</b>"; }else{ document.getElementById('dispStatus').innerHTML = "<br>"; } // ㉛状態表示状態を反転 flgRed=!flgRed; // ㉜1秒後に、再度setMsgTenmetu()を実行 setTimeout("setMsgTenmetu()",1000); count++; // ㉝タイムアウトしていた場合は失敗処理へ } else if (count >= rcvTimer) { dispfail(); } } // ㉞失敗時の表示 function dispfail(){ // ㉟点滅しないようにタイマーアウトにカウントを合わせる count=rcvTimer; irFlg=false; // ㊱状態に失敗を表示 document.getElementById('dispStatus').innerHTML = "<b>リモコン受信に失敗しました。</b>"; } |
最初の「window.onload = function () {}」は画面が読み込まれた後に{}内が実行される関数となります。もう少し詳細に説明するとDOMツリー構造及び関連リソースが読み込まれた後となります。DOMとはDocument Object Modelの略でJavasriptがHTMLを扱うために各タグやid、nameなどを識別することと理解してもらえればと思います。
①のwindow.onloadは画面が読み込まれた時に実施する処理になります。②でリモコンボタン情報の更新「updateIr()」を呼び出しています。③updateIr()関数で本体よりボタン情報を取得し画面に表示する処理を行っています。最初に XMLHttpRequestを利用できるように「new XMLHttpRequest()」を宣言し変数xhrに格納しています。XMLHttpRequestはJavascriptでHTTPアクセスを実現できるための機能となります。利用方法はプログラムの通り、④でアクセスするURLを作成しOpenで設定、SendすることでWebアクセスします。アクセスURLは④でアクセスしているURLをベースにパスのみ変更(”/getrem”を追加)して作成しています。
「xhr.ontimeout」でアクセス時に応答がない場合のタイムアウトについて処理を記載しています。「addEventListener」で応答があった場合の処理について記載し「var gtRecv = JSON.parse(resGtStr);」で受信データをJSONデータとして扱えるように変換し変数gtRecvに格納しています。
⑥についてはボタンデータが存在する場合はif文内を処理しデータがない場合はelse文内を処理します。ボタンデータが存在した場合において”spn”のidをチェックし存在する場合は制御画面のため⑦でボタン名を表示し、存在しない場合は設定画面のため⑧でボタン名を表示しています。また、ボタンデータが存在しない場合は⑨でbuttonタグを無効化します。
次にリモコン送受信処理ですが、⑩でグローバル変数を定義しています。これは文字を点滅させるためsetTimeout関数でをsetMsgTenmetuを呼び出して実現していますがグローバル変数で値を保持することで、現在のカウント数(繰り返し回数)や状態を管理しています。
⑪リモコン送信処理は制御画面でbuttonをクリック(buttonタグのonclickで呼び出される)した場合に実行されます。同様にXMLHttpRequestを利用し本体のリモコン送信用URLにアクセスすることでリモコン送信を実現します。⑮で送信用URLを作成しますがどのボタンを送信するかのボタン番号を「”/cntrem?n=” + setNum」で設定しています。送信完了の可否は⑯の本体からの応答が文字列”OK”を確認し正常完了としています。
⑱の受信処理ですが同様にXMLHttpRequestで本体のリモコン受信用URLにアクセスしますが、タイマーを15秒に設定しています。また、㉒でsetMsgTenmetu関数を呼び出して、「リモコン受信中」の赤文字表示を点滅させています。setMsgTenmetu関数では㉙で受信完了していない。かつ、タイムアウト前の場合に、flgRedの変数の値に応じて1秒毎にIf文内とelse文内を交互に表示し㉝でsetTimeoutを1秒後に再度読み出すことで点滅を実現しています。タイムアウトしている場合は㉞dispfail関数を呼び出します。
㉞dispfail関数ではタイムアウトしている状態に変数を設定し、画面に赤字で失敗表示を行い処理を終了しています。
本体への書き込みと実行
本体の配線が完了し、スケッチなどプログラムファイルの作成が完了したら本体への書き込みを実施してください。
書き込むデータは以下の通り2種類あります。書き込み方法については既に説明していますので不明な場合は以下のリンクから参照ください。
(1)SPIFFSデータの書き込み(4-2参照)
(2)スケッチの書き込み(1-3参照)
上記2つを書き込みが完了し、本体LEDが点灯(無線接続完了)したら、Webブラウザで「http://192.168.1.123」(設定を変更している場合は変更内容に修正して下さい)をアクセスし制御画面が表示されることを確認して下さい。
屋外からの利用
本工作のWebアクセスを屋外から利用できるようにするには、宅内ルータにポート開放設定を行えば技術的には可能です。
ただし、セキュリティ対策を行わずにポート開放を行うと外部から誰でもアクセス可能になってしまいますので本サイトでは紹介していません。
ポート開放を行う場合は適切なセキュリティ対策(ID,パスワード認証やポート番号変更など)を実施する必要があります。
近年のIoT端末はクラウドに常時接続し端末を管理し屋外からもアクセスすることが主流となっています。そのため、屋外からアクセスする方法は5-3にて紹介していますので、そちらを参照して下さい。