Ⅴ. ESP32WebサーバーのLEDの輝度をスマホから調整

 前回はブラウザーのON/OFFボタンで計測用デバイスの黄色LEDを点灯/消灯させましたが、今回はそれにスライダーを追加して、監視用デバイスの赤色LEDの輝度を調整できるようにしてみましょう。
 Webサーバーはスライダーの状態を保持していて、ブラウザー画面のONボタンをクリックすると指定されている輝度でLEDを点灯します。スライダーを操作するとそれにしたがってLEDの輝度を変更し、スライダーの現在値をOLEDディスプレイに表示させます。
 技術面では、jQueryとAjaxを使った非同期通信の方法と、アナログ出力メソッドがないESP32でいかにLEDの調光を制御するかがポイントになります。


 ※スケッチのダウンロードは右側の[Download]ボタンをクリックしてください。    


1.HTMLコードの作成

 今回もまずテキストエディターなどでHTMLコードを書いて、Webブラウザーで表示させながらデザインを整えます。test4.htmlというファイルに以下のようなコードを書いてドキュメントルートに保存しました。
 ボタンにはONとOFFがあり、それぞれにアイドルとクリック(active)の状態があるので、CSSのスタイルで3つのクラス属性を定義しています。続いてスライダーが示す輝度の値とスライダー本体を配置します。スライダーの範囲は0~100、刻み幅は5に設定しました(24行目)。初期状態でスライダーのツマミが中央に位置するよう、valueで50を指定しています。なおスライダーの幅は、CSSスタイルで200pxとしています(14行目)。

<!DOCTYPE HTML>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <style>
      html { font-family: Helvetica; display: inline-block; margin: 0px auto;text-align: center;} 
      h1 {font-size:28px;} 
      .btn_on { padding:12px 30px; text-decoration:none; font-size:24px; background-color:
        #668ad8; color: #FFF; border-bottom: solid 4px #627295; border-radius: 2px;}
      .btn_on:active { -webkit-transform: translateY(0px); transform: translateY(0px);
        border-bottom: none;}
      .btn_off { background-color: #555555; border-bottom: solid 4px #333333;}
      .slider { width: 200px;}
    </style>
  </head>

  <body><h1>Web Server</h1>
    <p>LED State : OFF</p>
    <p><a href="/ON"><button class="btn_on"> ON </button></a></p>
    <p><a href="/OFF"><button class="btn_on btn_off">OFF</button></a></p>
    <p>Brightness (50)</p>
    <input type="range" min="0" max="100" step="5" class="slider" id="slideBar" value="50" />
  </body>
</html>


 ブラウザーを立ち上げてアドレスバーから「http://local/test4.html」を入力すると、次のような画面が表示されます。デザインを確認し、必要なら修正をしてHTMLを手早く完成させます。


 完成したHTMLコードから、スケッチのHTML部分を「生の文字リテラル」で記述します。まずは、HTMLのコードをそのままコピーしてページ・ヘッダー部を定義します。
 続いて、Ajaxによる非同期通信を行うためにjQueryの準備をします。jQueryはもっとも利用されているJavaScriptライブラリーの一種ですが、ここではGoogleにホスティングされているjQueryを使用することにします。CSSスタイル記述の後に、src属性でGoogleスクリプトファイルのURLを指定します(17行目)。

const String strHtmlHeader = R"rawliteral(
<!DOCTYPE HTML>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <style>
      html { font-family: Helvetica; display: inline-block; margin: 0px auto;text-align: center;} 
      h1 {font-size:28px;} 
      .btn_on { padding:12px 30px; text-decoration:none; font-size:24px; background-color:
        #668ad8; color: #FFF; border-bottom: solid 4px #627295; border-radius: 2px;}
      .btn_on:active { -webkit-transform: translateY(0px); transform: translateY(0px);
        border-bottom: none;}
      .btn_off { background-color: #555555; border-bottom: solid 4px #333333;}
      .slider { width: 200px;}
    </style>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
  </head>
)rawliteral";

 次にボディー部を定義しますが、これは次節「スライダーとAjax」で説明します。そして、前回と同様に、ONとOFFそれぞれのボタンを定義しておきます。

const String strButtonOn = R"rawliteral(
    <a href="/ON"><button class="btn_on"> ON </button></a> )rawliteral";
const String strButtonOff = R"rawliteral(
    <a href="/OFF"><button class="btn_on btn_off">OFF</button></a> )rawliteral";


2.スライダーとAjax

 HTMLのボディー部の記述は、ページタイトルや表示のためのプレースホルダーとスライダーで構成されています。プレースホルダーの編集(値の設定)はHTTPレスポンス処理で行うことになりますが、スライダーの操作に伴うクライアント側(Webブラウザ側)の内部処理や「$.get」メソッドによる非同期通信は、すべてこのボディー部にJavaScriptで記述します。以下で、少し細かく内容を見ておきましょう。

・5行目 スライダーの輝度を編集するために、固有識別名として「brightValue」を与えています。
・6~7行目 スライダーに固有識別名として「slideBar」を与えています。このオブジェクトの値はプレースホルダー
 「%SLIDER_VALUE%」に値を設定されることを予定していて、値が変更されて発生するonchangeイベントをきっ
 かけにして関数slideValue(this.value)を実行するよう指定しています。
・8行目 変数objにスライダーオブジェクトを取り込みます。
・9行目 変数targetに輝度表示オブジェクトを取り込みます。
・10行目 輝度表示オブジェクトtargetの値設定プロパティinnerHTMLに、スライダーオブジェクトの値属性valueの
 内容を代入します。この値が輝度表示オブジェクト上に表示されます。
・11行目 スライダーオブジェクトの値が変更された時に実行させる関数です。自身の値属性と輝度表示オブジェクトの
 値設定プロパティに変更された値を代入します。
・12行目 スライダーの値変更によって実行される関数slideValueです。値を伴って呼ばれ、$.getメソッドで非同期
 通信を実行します。この時に引数として「/?value=値&」を伴います。例えばスライダーの値を30に変更すると、
 次のようなパラメータが送信されます。
     GET /?value=30& HTTP/1.1
 "/"はアクセス先のURL、"?value=30&"はクエリー情報です。HTTPリクエスト処理でこのデータを受信したら、
 "="と"&"で挟まれた数値を取り出せば(ここでは30)、スライダー操作で指示された値を取り出すことができます。

 これからわかるように、スライダーの操作によって呼び出されるこの関数が処理のカナメであり、$.getメソッドこそが、jQueryを使ったAjax通信、つまり非同期通信を実現しているわけです。
const String strHtmlBody = R"rawliteral(
  <body><h1>%PAGE_TITLE%</h1>
    <p>LED State : %LED_STATE%</p>
    <p>%BUTTON_STATE%</p>
    <p>Brightness (<span id="brightValue"></span>)</p>
    <input type="range" min="0" max="100" step="5" class="slider" id="slideBar" onchange="slideValue(this.value)" 
    value="%SLIDER_VALUE%" />
    <script> var obj = document.getElementById("slideBar");
    var target = document.getElementById("brightValue");
    target.innerHTML = obj.value;
    obj.oninput = function() { obj.value = this.value; target.innerHTML = this.value; }
    function slideValue(val) { $.get("/?value=" + val + '&'); { Connection: close}; }
    </script>
  </body>
</html>
)rawliteral";


3.LEDの明るさ制御

 現時点で、ESP32にはArduinoUNOなどで実行できるanalogWrite()のようなメソッドがありません。しかし、ESP32にはDAC(Digital Analog Converter)の機能があり、GPIOの25,26ピンからアナログ出力をすることができます。監視用デバイスのLEDはGPIOの25ピン(DAC1)に接続しているので、そのままでDACを使用できます。
 もしGPIO25と26が他の目的で使用されている場合は、PWM(Pulse Width Modulation)で同様の働きをさせることができます。GPIO25,26を含むアナログピンに対して有効ですが、GPIO34以上のピンでは動作しないようです。以下のPWMの実験は、LEDをGPIO25に接続した状態、つまり最初に作成した監視用デバイスそのままの状態で両方の制御を試しています。

(1)DACによる輝度制御

 DACでは、dacWrite(pin, value) メソッドでvalueの値を0~255の範囲で指定して、アナログ電圧を直接ピンに出力できます。ところが、LEDは電圧を下げると、ある時点から明るさを制御できなく(点灯しなく)なります。試行の結果、valueが125~255の範囲で消灯から最高輝度までを再現できそうなので、スライダーの値範囲0~100をこれに対応させて、次式でスライダーの値Xからvalueへ変換させることにしました。
   value = 13 * ( X / 10 ) + 125


(2)PWMによる輝度制御

 PWM(Pulse Width Modulation:パルス幅変調)は、目では識別できない高い周波数で点灯/消灯を繰り返し、各サイクルでの点灯時間の幅、つまりパルス幅を変えることにより点灯時間の比率(デュ-ティ比)を調整することで輝度を制御します。
 LEDの輝度調整についての好例がサンプルスケッチにあるので、ここではそれを流用することにします。IDEを開いて、次の手順でサンプルスケッチを参照することができます。
     [ファイル]→[スケッチ例]→ESP32→AnalogOut→LEDCSoftwareFade
 このスケッチ、LEDCSoftwareFade.inoでは次の3つのメソッドが使用されています(これらメソッドの実体はPWM用ライブラリ\cores\esp32\esp32-hal-led.cに記述されています)。
  double ledcSetup(uint8_t chan, double freq, uint8_t bit_num)
    chan:     利用するチャンネル(0~15)
    freq:     PWMの周波数
    bit_num:  デューティー比を表すビット数

  void ledcAttachPin(uint8_t pin, uint8_t chan)
    pin:    利用するピン
    chan:   利用するチャンネル

  void ledcWrite(uint8_t chan, uint32_t duty)
    chan:   利用するチャンネル
    duty:   デューティー比 0~2^bit_num
 さらに、PWM制御の基本情報が次のように定義されています。
  #define LEDC_CHANNEL_0		0		// 使用するチャンネル
  #define LEDC_TIMER_13_BIT	13		// デューティー比を表すビット数
  #define LEDC_BASE_FREQ		5000	// PWMの周波数


4.コードの解説

①ヘッダーファイルとデータ等の定義

・36行目 PWM制御の基本情報です。
・42~43行 環境に合った情報を設定してください!
・47~48行 どちらかをコメントアウトしてLEDの輝度制御方式を宣言してください!
・54行目 スライダーの値を保持します。初期値は中央値の50にしています。
・69行目 次の行以降に、すでに解説済みのHTMLコードを記述します。
#include <Arduino.h>
#include <WiFi.h>
#include <Wire.h>                   // I2C interface
#include <Adafruit_SSD1306.h>       // SSD1306 display
#include <Adafruit_GFX.h>           // SSD1306 display

/* Function Prototype */
void doInitialize();
void doMeasurement();
void httpListen();
void httpRequestProccess(String*);
void httpSendResponse(WiFiClient*);
void connectToWifi();
void ledcAnalogWrite(uint8_t, uint32_t, uint32_t);
void displayValues(String);

/* 基本属性定義  */
#define SPI_SPEED   115200          // SPI通信速度
// (For software fade LED)
#define LEDC_CHANNEL_0     0        // use first channel of 16 channels (started from zero)
#define LEDC_BASE_FREQ     5000     // use 5000 Hz as a LEDC base frequency
#define LEDC_TIMER_13_BIT  13       // use 13 bit precission for LEDC timer

// ルーター接続情報
#define WIFI_SSID "your_ssid"
#define WIFI_PASSWORD "your_password"
#define HTTP_PORT 80

// LED fade方式(以下のいずれかをコメントアウト)
const String FADE_CONTROL = "DAC";
//const String FADE_CONTROL = "PWM";

// Webサーバーオブジェクト
WiFiServer server(HTTP_PORT);

// スライダーの値と初期値
String strSlider = "50";

/* LEDピン */
const int ledPin = 25;              // LED出力接続ピン
String ledState = "OFF";            // 出力ピンの状態

/* OLEDディスプレイの設定 */
#define SCREEN_WIDTH 128            // 幅 (単位:ピクセル)
#define SCREEN_HEIGHT 64            // 高さ(単位:ピクセル)
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire);

/* HTMLレスポンスヘッダー */
const String strResponseHeader = "HTTP/1.1 200 OK\r\nContent-Type:text/html\r\n"
        "Connection:close\r\n\r\n";

/* HTMLページ構成要素 */
// (ページヘッダー部)
const String strHtmlHeader = R"rawliteral(
<!DOCTYPE HTML>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <style>
      html { font-family: Helvetica; display: inline-block; margin: 0px auto;text-align: center;} 
      h1 {font-size:28px;} 
      .btn_on { padding:12px 30px; text-decoration:none; font-size:24px; background-color:
        #668ad8; color: #FFF; border-bottom: solid 4px #627295; border-radius: 2px;}
      .btn_on:active { -webkit-transform: translateY(0px); transform: translateY(0px);
        border-bottom: none;}
      .btn_off { background-color: #555555; border-bottom: solid 4px #333333;}
      .slider { width: 200px;}
    </style>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
  </head>
)rawliteral";

// (ページボディー部)
const String strHtmlBody = R"rawliteral(
  <body><h1>%PAGE_TITLE%</h1>
    <p>LED State : %LED_STATE%</p>
    <p>%BUTTON_STATE%</p>
    <p>Brightness (<span id="brightValue"></span>)</p>
    <input type="range" min="0" max="100" step="5" class="slider" id="slideBar" onchange="slideValue(this.value)" 
    value="%SLIDER_VALUE%" />
    <script> var obj = document.getElementById("slideBar");
    var target = document.getElementById("brightValue");
    target.innerHTML = obj.value;
    obj.oninput = function() { obj.value = this.value; target.innerHTML = this.value; }
    function slideValue(val) { $.get("/?value=" + val + '&'); { Connection: close}; }
    </script>
  </body>
</html>
)rawliteral";

// (ON/OFFボタン)
const String strButtonOn = R"rawliteral(
    <a href="/ON"><button class="btn_on"> ON </button></a> )rawliteral";
const String strButtonOff = R"rawliteral(
    <a href="/OFF"><button class="btn_on btn_off">OFF</button></a> )rawliteral";


②定型のsetup()とloop()

・setup()では初期化処理とルーター接続の関数を実行します。
・loop()では「HTTPリスン処理」を実行し続けます。
void setup() {
    doInitialize();             // 初期化処理をして
    connectToWifi();            // Wi-Fiルーターに接続する
}

void loop() {
    httpListen();               // HTTP手順を制御する
}


③初期化処理

・シリアルポートを設定し、LEDをLOWにしてOLEDディスプレイを初期化します。
・134~137行 LEDの輝度制御にPWMを使用する場合は、使用チャンネルの設定と使用ピンの関連付けをします。
void doInitialize() {
    Serial.begin(SPI_SPEED);
    pinMode(ledPin, OUTPUT);           // GPIO設定:LED
    digitalWrite(ledPin, LOW);

    // LED FadeがPWMの場合の初期設定
    if (FADE_CONTROL == "PWM") {
        ledcSetup(LEDC_CHANNEL_0, LEDC_BASE_FREQ, LEDC_TIMER_13_BIT);
        ledcAttachPin(ledPin, LEDC_CHANNEL_0);
    }
    // OLEDディスプレイの初期化処理
    if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
        Serial.println(F("*SSD1306 allocation failed!"));
        for(;;); // 失敗したら処理を進ませない!
    }
    displayValues(strSlider);         // OLEDディスプレイに初期値を表示する
}


④HTTPリスン処理

・「HTTPリスン定型処理」です。
・162行目 「HTTPリクエスト処理」を実行します。
・163行目 「HTTPレスポンス処理」を実行します。
void httpListen() {
    String strBuffer = "";
    WiFiClient client = server.available();

    if (client) {                         // クライアントから着信があれば
        Serial.println("New Client.");
        String currentLine = "";
        while (client.connected()) {      // 接続中に以下を繰り返す
            if (client.available()) {     // 着信データがあれば
                char c = client.read();   // 1バイト読み込んで
                strBuffer += c;           // 受信文を形成する
                if (c == '\n') {
                    // 復帰改行文字で受信領域が空なら、リクエスト処理とレスポンス
                    // を送信してループを脱出する
                    if (currentLine.length() == 0) {
                        httpRequestProccess(&strBuffer);
                        httpSendResponse(&client);
                        break;
                    } else {              // 後続文字があればラインバッファをクリア
                        currentLine = "";
                    }
                } else if (c != '\r') {   // 改行以外ならラインバッファに結合する
                    currentLine += c;
                }
            }
        }
        // コネクションを閉じる
        client.stop();
        Serial.println("Client disconnected.\n");
    }
}


⑤HTTPリクエスト処理

・リクエストメッセージを解析してアクションを起こします。
・182行目 ONボタンがクリックされていればLEDを点灯します。
・186行目 OFFボタンがクリックされていればLEDを消灯します。
・192~195行 クライアントからの$.getメソッドのパラメータを受信すると、スライダーの値を取り出します。
・200~203行 DAC制御で点灯状態なら、先に規定した式でスライダー値を変換してdacWrite()で出力します。
・209~212行 PWM制御で点灯状態なら、スライダー値と最大値100でledcAnalogWrite()で出力しています。
       ledcAnalogWrite()はPWM用ライブラリのメソッドを使って記述しており、後述⑨のとおりです。
void httpRequestProccess(String* strbuf) {
    // 受信メッセージを判別してLEDをオン/オフする
    if (strbuf->indexOf("GET /ON") >= 0) {
        Serial.println("GET /ON");
        ledState = "ON";
        digitalWrite(ledPin, HIGH);
    } else if (strbuf->indexOf("GET /OFF") >= 0) {
        Serial.println("GET /OFF");
        ledState = "OFF";
        digitalWrite(ledPin, LOW);
    }
    // スライダーの値 valueを取得して表示する
    if (strbuf->indexOf("GET /?value=") >= 0) {
        int pos1 = strbuf->indexOf('=');
        int pos2 = strbuf->indexOf('&');
        strSlider = strbuf->substring(pos1+1, pos2);
        displayValues(strSlider);
        Serial.print("Value="); Serial.println(strSlider);
    }
    // LED Fade(DAC変換の場合)
    if (FADE_CONTROL == "DAC") {
        if (ledState=="ON") {
            int val = 125 + 13 * std::atoi(strSlider.c_str()) / 10;
            dacWrite(ledPin, val);
        } else {
            dacWrite(ledPin, 0);
        }
    }
    // LED Fade(PWMの場合)
    if (FADE_CONTROL == "PWM") {
        if (ledState=="ON") {
            int val = std::atoi(strSlider.c_str());
            ledcAnalogWrite(LEDC_CHANNEL_0, val, 100);
        } else {
            ledcAnalogWrite(LEDC_CHANNEL_0, 0, 100);
        }
        Serial.println("PWM mode!");
    }
    Serial.print("LedState: "); Serial.println(ledState);
}


⑦HTTPレスポンス処理

・HTTPレスポンスヘッダー、ページヘッダー、ボディー部を順番にクライアントへ送信します。
・230行 編集のために、生の文字リテラルで定義したボディー部を作業エリアに取り込みます。
・234行 クリックされているボタンによって次のボタン表示を設定します。
void httpSendResponse(WiFiClient* client) {
    // HTTPレスポンスヘッダーを送信
    client->println(strResponseHeader);

    // ページヘッダーを送信
    client->println(strHtmlHeader);
    
    // ページボディー部を編集して送信
    String buf = strHtmlBody;
    buf.replace("%PAGE_TITLE%", "Web Server");
    buf.replace("%LED_STATE%", ledState);
    //(ON/OFFボタンの設定)
    if (ledState=="OFF") {
        buf.replace("%BUTTON_STATE%", strButtonOn);
    } else {
        buf.replace("%BUTTON_STATE%", strButtonOff);
    }
    //(スライダー値の設定)
    buf.replace("%SLIDER_VALUE%", strSlider);
    client->println(buf);
    client->println();     // 最後に、HTTP終端の空行を送信
}


⑧コネクション確立用

・以前と同じ定型のロジックです。
/* Wi-Fiルーターに接続する */
void connectToWifi() {
    // Wi-Fi接続して
    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();
}


⑨アナログ出力

・サンプルスケッチのコードです。ArduinoのanalogWrite()を模しています。
・入力した値から13ビットのデューティー比を求めています。0~valueMaxまでの値が0~8191に変換されます。
/* LED FadeのためのArduino風疑似analogWrite */
void ledcAnalogWrite(uint8_t channel, uint32_t value, uint32_t valueMax = 255) {
  // calculate duty, 8191 from 2 ^ 13 - 1
  uint32_t duty = (8191 / valueMax) * min(value, valueMax);

  // write duty to LEDC
  ledcWrite(channel, duty);
}


⑩OLEDディスプレイ表示

・引数で与えられたスライダーの値を表示します。
void displayValues(String value) {
    display.clearDisplay();
    display.setTextSize(2);
    display.setTextColor(WHITE);
    display.setCursor(0,0);    display.print("Brightness");
    display.setCursor(24,20);  display.print("   " + value);
    display.display();
}


5.動作検証

 シリアルモニターを立ち上げて実行を開始します。Wi-Fi接続が完了すると「割り当てられたIPアドレス」が表示されます(ここでは192.168.0.30)。
 パソコンまたはスマートフォンのWebブラウザーを起動して、アドレスバーに「割り当てられたIPアドレス」を入力すると次のような状態の「Web Server」画面が表示されます。
  ・LED State: OFF
  ・[ON]ボタン
  ・Brightness(50)
 この状態でスライダーのつまみは中央に位置しています。これを右にスライドするとBrightnessの値が5ずつ大きくなり、85のところで止めます(右図上)。
 [ON]ボタンをクリックまたはタップすると、図右・中のように赤色LEDが明るく点灯します。今度はつまみを左方向へ20まで移動します。移動中はBrightness表示が5ずつ小さくなりますが、LEDの輝度は変わりません。つまみを離すと、LEDの輝度が変わります。Brightnessが20の状態ではLEDはかなり暗いですが、間違いなく点灯しています(右図下)。
 [OFF]ボタンをタップするとLEDが消灯して、明るさの差が確認できると思います。これらの過程はシリアルモニターに図左のように表示されます。


 定型化したHTTPリスンメソッドをベースにしたシンプルな構造ですが、jQueryとAjaxを使った非同期通信を取り入れることによって、Webブラウザーを操作性が優れたコンソールにできることがわかりました。さらに同様の方法で、モーターの回転速度やサーボ機構の角度などを簡単に制御できます。応用範囲がぐっと広がりそうですね。

 スケッチ全体は、ページ先頭の[Download]ボタンでダウンロードしてください。
 次回は2チャンネルのリレーを使って、独立した電気回路をON/OFF制御する実験に取り組みます。お楽しみに!


 
Copyright (C) 2011-2024 Marchan, All rights reserved.