ESP32で『LチカWebサーバ』を作る

ESP32

Webブラウザからハードウェアを制御する

『ネットワーク経由』のアクセス手段としてもっとも手軽なのはWebブラウザを利用する方法でしょう。WebブラウザならPCやスマートフォンならまず間違いなくインストールされており、比較的簡単にボタンなどのGUI部品を利用することが出来ます。

そこで今回は、PCやスマートフォンのWebブラウザから、ESP32に接続されたLEDを点滅させる実験を行います。名付けて『LチカWebサーバ』です。

HTTPでLED点滅制御

概要

例によって可能な限り簡単にするため、またここまでの記事のソースを使い回すため(苦笑)、以下の仕様にします。

  • 一般のWebサーバと同じく、TCP 80番ポートで待ち受けする
  • 対応するメソッドは GET のみ
  • /on に対するリクエストがあったら、LEDを点灯する
  • /off に対するリクエストがあったら、LEDを消灯する
  • リクエスト内容にかかわらず、レスポンスとして /on および /off に対するリンクを含んだHTMLドキュメントを送信する
  • レスポンスを送信したらクライアントとの通信を切断する

というわけで、『なんちゃってWebサーバ』にほんのちょっと機能を追加しただけの仕様です。

HTTPリクエストの書式

さてここで、HTTPのリクエストの書式に触れておきましょう。

Webブラウザ(Webクライアント)がサーバにリクエストとして送信するデータは、実はテキスト形式です。そして最初の行は必ず

HTML
メソッド リソースのパス HTTPバージョン

のような形式になっています。これを『リクエスト行』といいます。
各項目の区切りは半角スペースです。

では、各項目について説明します。

メソッド

リクエストの一番最初に記述される『メソッド』は、クライアント(Webブラウザ)がサーバに『何をしてほしいのか』を表します。

メソッド意味
GET指定されたURIのリソースを取り出す。
『あなたが持っている○○.htmlというファイルを下さい』というような意味。
HTTPのもっとも基本的な動作で、最初期バージョン(HTTP/0.9)から存在する唯一のメソッド。
POST指定されたURIにデータを送る。Webフォームなどで利用される。HTTP/1.0から定義された。
『このデータをサーバ上で処理してください』というような意味。
HEADGETと似ているが、データ本体ではなくヘッダのみを要求している。HTTP/1.0から定義された。
『あなたが持っている○○.htmlというファイルについての情報を下さい』というような意味。
PUT指定されたURIにデータを保存する。HTTP/1.0から定義された。
DELETE指定されたURIのデータを削除する。HTTP/1.0から定義された。
OPTIONSサーバを調査する。HTTP/1.1から定義された。
TRACEサーバまでの経路を調査する。HTTP/1.1から定義された。
CONNECTTCPトンネルを接続する。HTTP/1.1から定義された。

このうち、憶えておく必要があるのは GETPOST です。

大ざっぱに言うと、

GETメソッドでのリクエストが発生する場合

  • ブラウザのURL欄に直接アドレスを入力した
  • ブックマークから呼び出した
  • リンクをクリックした

POSTメソッドでのリクエストが発生する場合

  • Webページ上のフォームをサブミットした

となります。大ざっぱな話なので例外もありますが。

今回のプログラムでは、ブラウザ上のフォームからデータを送ることはないので、GETメソッドのみの対応でも問題ないわけです。

リソースのパス

リソースのパスは、サーバ上のリクエストの対象を表します。

GETメソッドならばクライアントが要求するファイルなど、
POSTメソッドならばクライアントが送信するデータを処理するプログラムなど

を指します。

たとえば、このブログの「ESP32で『HelloWorldサーバ』を作る」という記事のURLは

HTML
https://workshop.aaa-plaza.net/archives/2460

ですが、このうち /archives/2460 という部分がリソースのパスです。
つまり、ブラウザで「ESP32で『HelloWorldサーバ』を作る」という記事を表示しようとした場合、

HTML
GET /archives/2460 HTTP/1.0

というリクエストが送信されています(バージョン番号は違うことがあります)。

なお、このパスはサーバ上のファイルパスと対応していることが多いですが、サーブレットなど実際のディレクトリやファイル名とはまったく違うパスも存在します。

HTTPバージョン

クライアント(Webブラウザ)が対応するHTTPのバージョンが表されます。HTTP/0.9、HTTP/1.0、HTTP/1.1、HTTP/2、HTTP/3 があります。各バージョンでできることが違ってくるのですが、今回のプログラムでは無視しています。なにしろサーバ側が最初期バージョンのHTTP/0.9の、そのまた一部の機能にしか対応していないので…。

製作

回路

『ESP32でLEDを点滅させる』『ESP32で『LED点滅サーバ』を作る』とまったく同じ回路です。

配線

配線も『ESP32でLEDを点滅させる』『ESP32で『LED点滅サーバ』を作る』とまったく同じで大丈夫です。

今回は片側6穴のブレッドボードを使用しました。それに伴い、写真の配線例ではGNDのピンを変更していますが、以前の例のママでももちろん動きます。

プログラム

ソースコード

メインプログラム LEDSwitchingWebServer
C++
#include "WiFi.h"
#include "wifisecret.h"

// LED端子の設定
#define LED_PIN 13
bool led;

// リスンポートを23番としてサーバのインスタンスを作成
IPAddress serverIP(192,168,0,101);
IPAddress gatewayIP(192,168,0,1);
IPAddress subnetMask(255,255,255,0);
uint16_t  listenPort=80;
WiFiServer server(listenPort);

void setup(){
  // LED端子の設定
  pinMode(LED_PIN, OUTPUT);
  digitalWrite(LED_PIN, LOW);
  led=false;

  // 無線LANへの接続
  WiFi.config(serverIP, gatewayIP, subnetMask);
  WiFi.begin(_SSID, _PASS);
  while(true){
    if(WiFi.status() == WL_CONNECTED)break;
    delay(1000);
  }
  // サーバの起動
  server.begin();
}

void loop(){
  WiFiClient client = server.available();
  if(client){
    // クライアントと接続した時の処理
    // 1行目のみマジメに受信する
    String currentLine = "";
    while(true){
      if(!client.connected()){
        currentLine = "";
        break;
      }
      if(client.available()){
        // 受信した文字がある場合
        char c = client.read();
        if(c == '\n'){
          break;
        }
        if(c != '\r'){
          currentLine += c;
        }
      }
    }
    if(currentLine!=""){
      // 2行目以降は読み飛ばす
      while(client.available()){
        char c=client.read();
      }
      if(currentLine.startsWith("GET ")){
        if(currentLine.substring(4,7).equals("/on")){
          digitalWrite(LED_PIN, HIGH);
          led=true;
        }else if(currentLine.substring(4,8).equals("/off")){
          digitalWrite(LED_PIN, LOW);
          led=false;
        }
        client.print(getHTMLDocument());
      }
    }
    client.stop();
    while(client.connected());  // 完全に切断されるまで待たないと、loop()の頭に戻ったとき接続ありと誤認してしまう
  }
}

// HTMLドキュメントの送信
String getHTMLDocument(){
    String responseBody = "";
    responseBody += "<!DOCTYPE html>\r\n";
    responseBody += "<html><head>\r\n";
    responseBody += "<meta charset=\"utf-8\">\r\n";
    responseBody += "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">";
    responseBody += "<title>test page</title></head>\r\n";
    responseBody += "<body style=\"text-align:center;\">\r\n";
    responseBody += "<h1>ESP32-DevkitC</h1>\r\n";
    responseBody += "<h2>** LED Switching Web-Server **</h2>\r\n";
    if(led){
      responseBody += "<p style=\"color:orange;\">LEDは点灯しています</p>\r\n";
    }else{
      responseBody += "<p style=\"color:blue;\">LEDは消灯しています</p>\r\n";
    }
    responseBody += "<p><button onClick=\"location.href='/on'\" style=\"padding:10px;\">LEDを点灯します</button></p>\r\n";
    responseBody += "<p><button onClick=\"location.href='/off'\" style=\"padding:10px;\">LEDを消灯します</button></p>\r\n";
    responseBody += "</body></html>\r\n";
    String responseHeader = "";
    responseHeader += "HTTP/1.0 200 ok\r\n";
    responseHeader += "Content-type: text/html\r\n";
    responseHeader += "Content-length: " + String(responseBody.length()) + "\r\n";
    responseHeader += "\r\n";
    return responseHeader+responseBody;
}
wifisecret.h
C++
#define _SSID "xxxxxxxx"
#define _PASS "yyyyyyyy"

※ご利用の無線LAN環境にあわせて、”xxxxxxxx” の部分には SSID、”yyyyyyyy” の部分にはパスワードを記述してください。

プログラムの解説

大部分は『なんちゃってWebサーバ』と同じです。異なる部分のみ解説します。

出力端子の定義
C++
// LED点滅関係
#define LED_PIN 12
bool led;

Lチカのプログラムにもあった、LEDを接続した出力端子を表す定数 LED_PIN の定義です。プログラム中に直接番号を記述してもいいのですが、このように定数として定義しておいた方が後々変更があったときに間違いが起こりにくくなります。

もう一つ、クライアント画面でLEDの状態を表示した方が判りやすいかな、と思ったので、LEDの点灯/消灯をtrue/falseで表す変数ledを用意しました。

出力端子の設定
C++
  // LED接続端子の設定
  pinMode(LED_PIN, OUTPUT);
  digitalWrite(LED_PIN, LOW);
  led=false;

初期設定では、LED_PIN の端子を出力モードに設定し、すぐにLOWを出力(LED消灯)しています。合わせて変数ledも消灯を表すfalseにしています。

リクエスト内容のチェック
C++
      if(currentLine.startsWith("GET ")){
        if(currentLine.substring(4,7).equals("/on")){
          digitalWrite(LED_PIN, HIGH);
          led=true;
        }else if(currentLine.substring(4,8).equals("/off")){
          digitalWrite(LED_PIN, LOW);
          led=false;
        }
        client.print(getHTMLDocument());

変数currentLine には、クライアントから送信されてきたメッセージの最初の行(リクエスト行)が入っています。このプログラムでは、まずリクエスト行が ”GET ” で始まることをチェックした上で、

リソースのパスが “/on” ならば LED_PIN の出力を HIGH に(LED点灯)
リソースのパスが “/off” ならば LED_PIN の出力を LOW に(LED消灯)

しています。また、それにあわせて変数ledの値も設定しています。

そしてリソースのパスがなんであろうと、最終的には getHTMLDocument()関数をつかってHTMLドキュメントをクライアントに送信します。

getHTMLDocument()関数
C++
// HTMLドキュメントの送信
String getHTMLDocument(bool led){
    String responseBody = "";
    responseBody += "<!DOCTYPE html>\r\n";
    responseBody += "<html><head>\r\n";
    responseBody += "<meta charset=\"utf-8\">\r\n";
    responseBody += "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">";
    responseBody += "<title>test page</title></head>\r\n";
    responseBody += "<body style=\"text-align:center;\">\r\n";
    responseBody += "<h1>ESP32-DevkitC</h1>\r\n";
    responseBody += "<h2>** LED Switching Web-Server **</h2>\r\n";
    if(led){
      responseBody += "<p style=\"color:orange;\">LEDは点灯しています</p>\r\n";
    }else{
      responseBody += "<p style=\"color:blue;\">LEDは消灯しています</p>\r\n";
    }
    responseBody += "<p><button onClick=\"location.href='/on'\" style=\"padding:10px;\">LEDを点灯します</button></p>\r\n";
    responseBody += "<p><button onClick=\"location.href='/off'\" style=\"padding:10px;\">LEDを消灯します</button></p>\r\n";
    responseBody += "</body></html>\r\n";
    String responseHeader = "";
    responseHeader += "HTTP/1.0 200 ok\r\n";
    responseHeader += "Content-type: text/html\r\n";
    responseHeader += "Content-length: " + String(responseBody.length()) + "\r\n";
    responseHeader += "\r\n";
    return responseHeader+responseBody;
}

関数の動作としては以前と変わっていませんが、導出されるHTMLドキュメントが大きく変わり、以下のようになっています。

HTML
<!DOCTYPE html>
<html><head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"><title>test page</title></head>
<body style="text-align:center;">
<h1>ESP32-DevkitC</h1>
<h2>** LED Switching Web-Server **</h2>
<p style="color:blue;">LEDは消灯しています</p>
<p><button onClick="location.href='/on'" style="padding:10px;">LEDを点灯します</button></p>
<p><button onClick="location.href='/off'" style="padding:10px;">LEDを消灯します</button></p>
</body></html>

今回のメインは、

HTML
<p><button onClick="location.href='/on'" style="padding:10px;">LEDを点灯します</button></p>
<p><button onClick="location.href='/off'" style="padding:10px;">LEDを消灯します</button></p>

という部分です。button要素の中で、マウスでクリックしたときに反応する onClick イベントハンドラを定義し、location.href=○○ でページ遷移しています。ページの遷移先が “/on” もしくは “/off” になっているため、

HTML
GET /on HTTP/1.0

または

HTML
GET /off HTTP/1.0

のようなリクエストが送信される、というわけです。

そのほか、スマートフォンで表示しやすいように viewport の設定を追加しています。詳しいことは html や css の書籍などを参考にしてください。

 

実行

ESP32を起動してからWebブラウザでアドレス欄に http://192.168.0.101 (ソースコード通りのIPアドレスの場合)と入力すると、

このような画面が表示されます。2つあるボタンをクリック/タップすると、ESP32に接続されたLEDが点灯・消灯します。

動画もどうぞ。

スマートフォンから操作すると IoT気分が盛り上がります(?)

まとめ

  • ESP32を使えば、Webブラウザから電子回路を制御することが簡単にできる

コメント

タイトルとURLをコピーしました