赤外線リモコン スマートリモコン Smart Remote Controller Aruduino PlatformIO ESP32
本項では全画面表示が可能な図番に * を付けています。
ESP32を使って学習リモコンを作る(4) で説明した「赤外線リモコン送信( irSend )」に引き続きソフトウエアの要素技術「 webserver 」について説明します。
はじめに
学習リモコンを製作して家電製品を制御するシステム構成を 図1 に示します。本機 ESP32-DevKitC-32E を使ったブレッドボードは赤外線リモコン波形学習(波形取込)と赤外線リモコン波形の送信機能を備えています。そして PC やスマートフォンの画面から GUI として Web ブラウザでこれらの機能を制御します。
ESP32-DevKitC-32E に webserver を実装して web ブラウザを実現します。
「Marchan」さんの 1-1. 非同期サーバーをクライアントから制御する① からコードを参照して webserver の確認を行います。コードはほぼそのまま使わせていただきますが、ブラウザ表示に必要な HTML、Java Script、CSS は独立させて別フォルダーに格納しました。
Arduino IDE 環境で作成されていますが、ここでは Visual Studio Code PlatformIO 拡張環境に適合させています。
ハードウエア
webserver は ESP32-DevkitC-32E のみで動作するのでこれまで使っていたハードウエアをそのまま使用することができます。 ESP32を使って学習リモコンを作る(4) で使ったブレッドボードをそのまま使います。(図2)
プロジェクトを作成する
Visula Studio Code ( VSCode ) を開いて画面左端縦の「アクティビティバー」からアリさんマーク(赤丸)をクリックします。(図3)
プライマリサイドバーが表示されるので Create New Project をクリックします。(図4)
PIO Home 画面が表示されます。(図5)
画面中央から右寄りの Quick Access から + New Project をクリックすると、Project Wizard が表示されます。(図6)
Project Wizard に従って入力します。(図7)
Name: PIO_WiFiAsyncTest_3 (何でもいいので任意のプロジェクト名を入力)
Board: Espressif ESP32 Dev Module (プルダウンメニューから選択します)
Framework: Arduino (プルダウンメニューから選択します)
ライブラリ
ESP Async WebServer と AsysncTCP の 2 つのライブラリが必要になります。ESP Async WebServer と AsysncTCP は Built-in で組み込まれているので何もする必要はありません。
webserver プロジェクト
自動生成されたプロジェクトに追加、変更を加えて webserver に対応させます。
platformio.ini ファイルに以下の 2 行を追加します。(図9)
upload_port = COM3 (使用する PC に合わせます)
monitor_speed = 115200
main.cpp ファイルをクリックして自動生成されたプログラムを表示します。(図10)
main.cpp の内容を全て削除して代わりに次のコードを使います。(図11)
#include <asynceventsource.h>
#include <asyncwebsocket.h>
#include <asyncwebsynchronization.h>
#include <espasyncwebserver.h>
#include <spiffseditor.h>
#include <stringarray.h>
#include <webauthentication.h>
#include <webhandlerimpl.h>
#include <webresponseimpl.h>
#include <httpupdate.h>
#include <spiffs.h>
#include <arduino.h>
#include <wifi.h>
#include <espasyncwebserver.h>
/* Function Prototype */
void doInitialize();
void connectToWifi();
String editPlaceHolder(const String&);
String getMessage();
String getCurTime();
// ルーター接続情報
#define WIFI_SSID "xxxxxxxx"
#define WIFI_PASSWORD "yyyyyyyy"
// NTPサーバー時刻設定用
const char* ntpServer = "pool.ntp.org";
const int gmtOffset_sec = 9 * 3600;
/* 基本属性定義 */
#define SPI_SPEED 115200
#define CST_ON "ON"
#define CST_OFF "OFF"
// Webサーバーオブジェクト
#define HTTP_PORT 80
AsyncWebServer server(HTTP_PORT);
/* LEDピン */
const int ledPin = 25; // LED出力接続ピン
String ledState = ""; // 出力ピンの状態
/*****************************************************************************
* Predetermined Sequence *
*****************************************************************************/
void setup(){
doInitialize(); // 初期化処理をして
connectToWifi(); // Wi-Fiルーターに接続する
configTime(gmtOffset_sec, 0, ntpServer); // まず時刻を合わせる
// SPIFFSのセットアップを追加
if (!SPIFFS.begin(true)) {
Serial.println("An Error has occurred while mounting SPIFFS");
return;
}
// GETリクエストに対するハンドラーを登録して
// index.htmlにアクセスされた時のレスポンス
server.on("/", HTTP_GET, [](AsyncWebServerRequest * request) {
request->send(SPIFFS, "/index.html", String(), false, editPlaceHolder);
});
// style.cssにアクセスされた時のレスポンス
server.on("/style.css", HTTP_GET, [](AsyncWebServerRequest * request) {
request->send(SPIFFS, "/style.css", "text/css");
});
// js.jsにアクセスされた時のレスポンス
server.on("/js.js", HTTP_GET, [](AsyncWebServerRequest * request) {
request->send(SPIFFS, "/js.js", "js/js");
});
server.on("/curtime", HTTP_GET, [](AsyncWebServerRequest *request){
request->send_P(200, "text/plain", getCurTime().c_str());
});
server.on("/message", HTTP_GET, [](AsyncWebServerRequest *request){
request->send_P(200, "text/plain", getMessage().c_str());
});
server.on("/update", HTTP_GET, [](AsyncWebServerRequest *request){
if (ledState == CST_ON) {
ledState = CST_OFF;
digitalWrite(ledPin, LOW);
} else {
ledState = CST_ON;
digitalWrite(ledPin, HIGH);
}
Serial.print("LED changed : ");
Serial.println(ledState);
request->send_P(200, "text/plain", getMessage().c_str());
});
// サーバーを開始する
server.begin();
}
void loop(){
}
/*****************************************************************************
* 'GET' Request Handlers *
*****************************************************************************/
/* プレースホルダー処理 */
String editPlaceHolder(const String& var) {
if (var == "MESSAGE") {
return getMessage();
}
else if (var == "CURTIME") {
return getCurTime();
}
}
/* 現在の日時を取得する */
String getCurTime() {
struct tm timeinfo;
char buf[64];
if(getLocalTime(&timeinfo)) {
sprintf(buf,"%04d/%02d/%02d %02d:%02d:%02d",
timeinfo.tm_year+1900, timeinfo.tm_mon+1, timeinfo.tm_mday,
timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec);
//Serial.println(buf); //20240714
return buf;
}
}
/* LEDの状態を通知する */
String getMessage() {
return ledState;
}
/*****************************< Other functions >*****************************/
/* 初期化処理 */
void doInitialize() {
Serial.begin(SPI_SPEED);
pinMode(ledPin, OUTPUT); // GPIO設定:LED
digitalWrite(ledPin, LOW);
ledState = CST_OFF;
}
/****************************< Connect functions >****************************/
/* Wi-Fiルーターに接続する */
void connectToWifi() {
Serial.print("Connecting to Wi-Fi ");
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("");
// モニターにローカル IPアドレスを表示する
Serial.println("WiFi connected.");
Serial.print(" *IP address: ");
Serial.println(WiFi.localIP());
server.begin();
}
図11 main.cpp に使うコード
このコードをコピーして main.cpp にペーストした結果です。(図12)
SSID “xxxxxxxx” と PASSWORD “yyyyyyyy” は Wi-Fi ルーターに合わせて変更します。
画面左端縦の「アクティビティバー」から「エクスプローラー」をクリックしてプロジェクトフォルダーの階層を表示させます。
プロジェクトフォルダー名の右のアイコンから「新しいフォルダー」をクリックします。
プロジェクトフォルダーの下位に入力窓が表示されるので「data」と入力します。(図13)
次にカーソルを「data 」フォルダに合わせて、プロジェクトフォルダー名の右の「新しいファイル」アイコンをクリックすると data フォルダーの下位に入力窓が表示されるので「index.html」と入力します。(図14)
同様にして data フォルダーの下位に「js.js」ファイルを作ります。(図15)
同様にして data フォルダーの下位に style.css ファイルを作ります。(図16)
図14 で作成した index.html ファイルに次のコードをコピー/ペーストします。(図17)
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
p{ text-align:center; }
</style>
<link rel="stylesheet" href="./style.css">
<script src="js.js"></script>
</head>
<body>
<h1>Asynchronous Server</h1>
<p><table>
<tr><th>ELEMENT</th><th width="100">VALUE</th></tr>
<tr><td>LED status</td><td><span id="message" class="value">%MESSAGE%</span></td></tr>
<tr><td>Current time</td><td><span id="curtime" class="value">%CURTIME%</span></td></tr>
</table></p>
<br><button class='btn_on' onclick='btnClicked(this)' id='btn'>CHANGE</button>
<div class="button_on">
<input type="checkbox" id="btn1" name="button_on"onclick="btn1Click();">
<label id= "btn1_label" for="btn1">電源 ON</label>
</div>
</body>
</html>
図17 index.html のコード
このコードをコピーして index.html ファイルにペーストした結果です。(図18)
図15 で作成した js.js ファイルに次のコードをコピー/ペーストします。(図19)
var getCurTime = function () {
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
document.getElementById("curtime").innerHTML = this.responseText;
}
};
xhr.open("GET", "/curtime", true);
xhr.send(null);
}
var getMessage = function () {
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
document.getElementById("message").innerHTML = this.responseText;
}
};
xhr.open("GET", "/message", true);
xhr.send(null);
}
function btnClicked(element) {
var xhr = new XMLHttpRequest();
xhr.open("GET", "/update", true);
xhr.send(null);
}
setInterval(getCurTime, 500);
setInterval(getMessage, 200);
function btn1Click(){
const button1 = document.getElementById('btn1');
button1.checked= true; //電源 ONボタンが押されたら、電源 ONボタンを継続してtrueにする。
}
図19 js.js のコード
このコードをコピーして js.js ファイルにペーストした結果です。(図20)
図16 で作成した style.css ファイルに次のコードをコピー/ペーストします。(図21)
html {font-family: Helvetica; display: inline-block; margin: 0px auto;text-align: center;
}
h1 {font-size:28px;
}
body {text-align: center;
}
table {border-collapse: collapse; margin-left:auto; margin-right:auto;
}
th {padding: 12px; background-color: #0000cd; color: white; border: solid 2px #c0c0c0;
}
tr {border: solid 2px #c0c0c0; padding: 12px;
}
td {border: solid 2px #c0c0c0; padding: 12px;
}
.value {color:blue; font-weight: bold; padding: 1px;
}
.btn_on {width:100px; height:43px; padding:1px 1px; vertical-align: middle;
text-decoration:none; font-size:16px; background-color:
#668ad8; color: #FFF; border-bottom: solid 4px #627295; border-radius: 4px;
}
.btn_on:active {-webkit-transform: translateY(0px); transform: translateY(0px);
border-bottom: none;
}
.button_on{display: flex;
}
.button_on label{display: block;
width: 150px;
background:silver; /*silvergray*/
color: black;
padding: 10px;
margin: 0 auto;
box-sizing: border-box;
text-align: center;
text-decoration: none;
border-radius: 5px;
cursor: pointer;
}
.button_on input:checked+label{background:tomato;
color: white;
}
.button_on input{display: none;
}
図21 style.css のコード
このコードをコピーして style.css ファイルにペーストした結果です。(図22)
以上で webserver プロジェクトが完成です。
webserver プロジェクトの勘所
①WiFi
WiFi が使えるようにヘッダーファイルをインクルードします。(main.cpp 16行目)
27 #define WIFI_SSID “xxxxxxxx”
28 #define WIFI_PASSWORD “yyyyyyyy”
ルーター接続情報として SSID と PASSWORD を定義します。(main.cpp 26~28 行目)SSID “xxxxxxxx” と PASSWORD “yyyyyyyy” は Wi-Fi ルーターに合わせて変更します。
WiFiルーターに接続してモニターにローカル IP アドレスを表示するセットアップシーケンスを記述します。(main.cpp 145~159行目)
②Async Web Server
非同期 Web サーバーを使えるようにヘッダーファイルをインクルードします。(main.cpp 17行目)
97 #define HTTP_PORT 80
98 AsyncWebServer server(HTTP_PORT);
HTTP ポートを定義して非同期 Web サーバーオブジェクトをインスタンス化します。(main.cpp 39~41行目)
97 server.begin();
セットアップにサーバー開始を記述します。(main.cpp 96~97行目)
③SPIFFS
ブラウザの表示は main.cpp から独立させて index.html、js.js および style.css の3つのファイルに記述して外部のフラッシュメモリーに保存します。これらの 3 ファイルを外部フラッシュメモリーから呼び出すために SPIFFS( Serial Peripheral Interface Flash File System )を使用します。
SPIFFS が使えるようにヘッダーをインクルードします。(main.cpp 13行目)
外部メモリーと SPI 接続する場合のスピードを定義します。(main.cpp 35行目)
SPI 接続エラーが発生した時にターミナルにメッセージを出力するセットアップを追加しました。(main.cpp 56~60行目)
④Webページ
WiFi、Async Web Server、SPIFFS が動作するとブラウザに学習リモコンの制御ページが表示できます。 IP アドレスを入力すると、フラッシュメモリーから index.html、js.js および style.css の情報を読込んでブラウザに表示します。
65 request->send(SPIFFS, “/index.html“, String(), false, editPlaceHolder);
66 });
ブラウザから IP アドレスを入力するとトップページ “/” にアクセスします。
GET リクエスト “/” に対して SPIFFS を経由した /index.html ファイルをレスポンスとしてクライアントに通知します。このときサーバーからクライアントへ情報を伝えることができる内部変数を使うために別途定義されている editplaceHolder を参照しています。(main.cpp 64~66行目)
スタイルを定義している style.css ファイルを参照しています。(index.html 9行目)
69 server.on(“/style.css”, HTTP_GET, [](AsyncWebServerRequest * request) {
70 request->send(SPIFFS, “/style.css“, “text/css”);
71 });
GET リクエスト “/style.css” に対して SPIFFS を経由した /style.css ファイルをレスポンスとしてクライアントに通知します。(main.cpp 68~71行目)
⑤現在時刻表示
Java Script を使って現在時刻を更新する制御を行っています。
ページ全体を更新せずにサーバーからデータを受け取ることができる XMLHttpRequest() オブジェクトを使用します。ドキュメントの状態が変化した場合はコールバック関数 onreadystatechange で “curtime” の内容を更新します。そして “GET” リクエストメソッドを使って URL “/curtime” へ非同期通信リクエスト true をサーバーへ送信します。(js.js 1~10行目)
GET リクエスト “/curtime に対して “text/plain” 形式で getCurTime().c_str() をレスポンスとしてクライアントに通知します。(main.cpp 78~80行目)
117 String getCurTime() {
118 struct tm timeinfo;
119 char buf[64];
120
121 if(getLocalTime(&timeinfo)) {
122 sprintf(buf,”%04d/%02d/%02d %02d:%02d:%02d”,
123 timeinfo.tm_year+1900, timeinfo.tm_mon+1, timeinfo.tm_mday,
124 timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec);
125 //Serial.println(buf); //20240714
126 return buf;
127 }
128 }
getCurTime() 関数の結果を内部変数として参照できるようにプレースホルダーで定義します。変数が “CURTIME” の場合戻り値は getCurTime() になります。(main.cpp 106~114行目)
65 request->send(SPIFFS, “/index.html“, String(), false, editPlaceHolder);
66 });
24 var xhr = new XMLHttpRequest();
25 xhr.open(“GET”, “/update”, true);
26 xhr.send(null);
27 }
GET リクエスト “/update” に対する main.cpp 内の処理が定義されています。ここでは LED の状態が CST_ON の場合は 端子 ledpin に LOW を出力して LED を OFF にします。そうでなければ LED の状態を CST_ON にして端子 ledpin に HIGH を出力して LED を ON にします。
ターミナルに ”LED cahnged :” + ledState(LED の状態)を出力します。
最後にリクエストに対するレスポンスとして “/text/plain” で getMessage().c_str() をクライアントに通知します。(main.cpp 84~95行目)
LED の状態を通知するために ledstate を戻り値にして getMessage() 関数を定義しています。(main.cpp 130~133行目)
getMessage() 関数の結果を内部変数として参照できるようにプレースホルダーで定義します。変数が “MESSAGE” の場合戻り値は getMessage() になります。(main.cpp 106~114行目)
=================================================
Async Web Server を使った情報伝達を簡単に整理すると次のようになります。(図23)
クライアント → サーバー : リクエスト(状態の変化)を通知
クライアント ← サーバー : レスポンス(任意の文字列)を通知
ファームウエアビルド
PROJECT TASKS から Build をクリックするとビルドが開始されて [SUCCESS] と表示されると完了です。(図24)
ファームウエア書込み
ESP32-DevKitC-32E を USB ケーブルで PC に接続します。 PROJECT TASKS から Upload をクリックするとファームウェアの書込みが開始されて [SUCCESS] と表示されると完了です。(図25)
ファイルシステムイメージビルド
Web サーバーに必要な HTML、Java Script、CSS の各ファイルは data フォルダーに格納されています。これらのファイルをビルドするにはファイルシステムイメージビルドを使用します。
PROJECT TASKS から Build Filesystem Image をクリックするとビルドが開始されて [SUCCESS] と表示されると完了です。(図26)
ファイルシステムイメージ書込み
PROJECT TASKS から Upload Filesystem Image をクリックするとファイルシステムイメージの書込みが開始されて [SUCCESS] と表示されると完了です。(図27)
ファームウエア実行
PROJECT TASKS から Monitor をクリックするとファームウェアの実行が開始されて、ターミナルウインドウにメッセージが表示されます。(図28)
Connecting to Wi-Fi
WiFi Connected
*IP address: 192.168.xx.xx
ここで表示された IP アドレスをメモしておきます。
(なお IP address は Wi-Fi ルータの環境によって変化します。)
ブラウザアプリを開いて先ほどメモした IP アドレスを入力すると画面が表示されます。(図29)
Current time の表示がリアルタイムで更新されていれば正常に動作しています。
ブラウザからの制御
ブラウザ画面で「change」ボタンをクリックすると LED status が OFF から ON に変化します。(図30)
同時にターミナル出力に「 LED cahnged : ON 」のメッセージが出力されます。(図31)
ブラウザから制御した時の流れは次のようになります。
クライアント CHANGE ボタン押下
→ クライアント “/update” リクエスト
→ サーバー ledstate “ON”
→ サーバー ”LED changed : ON” のメッセージをターミナル出力
→ サーバー ledstate “ON” をクライアントに文字列でレスポンス
(参考)ブラウザ画面で「電源 ON」ボタンをクリックするとボタンの色がグレーからオレンジに変わります。これは本機で使うための機能を先行して JavaScript で動作確認しました。
次回は フラッシュメモリ読書 について説明します。
リンク
ESP32を使って学習リモコンを作る(1) 学習リモコンでどんなことができるの?
ESP32を使って学習リモコンを作る(2) ハードウエア
ESP32を使って学習リモコンを作る(3) ソフトウエア 赤外線リモコン受信( irReceive )
ESP32を使って学習リモコンを作る(4) ソフトウエア 赤外線リモコン送信( irSend )
ESP32を使って学習リモコンを作る(5) ソフトウエア webserver
ESP32を使って学習リモコンを作る(6) ソフトウエア FLASH 読書き(SPIFFS)
ESP32を使って学習リモコンを作る(7) ソフトウエア 本機のプロジェクト
ESP32を使って学習リモコンを作る(8) ソフトウエア 本機プロジェクトの勘所
ESP32を使って学習リモコンを作る(9) ハードウエア設計の勘所
ESP32を使って学習リモコンを作る(10) 学習リモコンを操作した時のターミナル出力
Smart_Remote_Controller プロジェクト GitHub
(YI)
コメント