ESP32-WROOM-32EでRGB LEDを操作する

組み込みエンジニア

こんにちは、ENGかぴです。

ESP32でWebサーバーを実装するとクライアント(スマホなど)のリクエストに対応したレスポンスを返信する事ができます。WebサーバーとAdafruit_NeoPixelライブラリを使ってRGB LEDを操作して動作確認しました。

Grove Mech Keycap(Seeed Studio:秋月電子で購入)を使用しています。RGB LEDはMech keycapに実装されているものとRGB LEDはマイコン内蔵RGB LED 5mm PL9823-F5(秋月電子で購入)を使用しています。

ESP32-WROOM-32Eの開発環境はArduino IDEを使用しています。またESP32-WROOM-32E開発ボード(秋月電子)使用しています。

ESP32-WROOM-32Eを使って動作確認したことをまとめています。

ESP32-WROOM-32Eで学べるソフト開発と標準ライブラリの使い方

Adafruit NeoPixelライブラリを追加する

Adafruit_NeoPixelライブラリの追加
Adafruit_NeoPixelライブラリの追加

Arduino IDEでライブラリの追加を行います。ライブラリマネージャーを開いて検索欄に「pixel」(adafruitなど)などライブラリ名の一部を入力するとライブラリの候補が表示されます。候補の中から「Adafruit NeoPixel by Adafruit」をインストールします。

RGB LEDを操作するためにArduinoの標準関数であるdigitalWrite()とタイマを使ってDOを操作する場合ライブラリの処理の遅延の為RGB LEDの操作に必要なタイミング波形を生成することができません。

Adafruit NeoPixelライブラリはArduinoマイコンの操作をアセンブラ(機械語)に近づけることで処理による遅延を可能な限り減らすことで精度の良いタイミング波形を生成するように工夫されています。

Adafruit NeoPixelライブラリは上記のことを意識しなくてもメンバー関数をコールすることで点灯パターンを指定できるようになっています。マイコンのDOでRGB LEDを操作する場合のタイミング波形の考え方を下記記事にまとめています。

PICマイコン(PIC16F1827)でRGB LEDを操作する

すき間時間で資格をゲット【STUDYing(スタディング)】

ライブラリの準備と初期化

#include <Adafruit_NeoPixel.h>

#define NUMPIXELS 1 // Popular NeoPixel ring size
//Adafruit_NeoPixelクラスの型の変数を宣言
Adafruit_NeoPixel pixels(NUMPIXELS, PIXEL_PIN, NEO_GRB  + NEO_KHZ800);
Adafruit_NeoPixel pixels2(NUMPIXELS, PIXEL_PIN2, NEO_RGB  + NEO_KHZ400);

void setup() {
  pixels.begin(); //Mech keycapのLEDの設定
  pixels.clear();
  pixels.show();

  pixels2.begin(); //PL9823のLEDの設定
  pixels2.clear(); //色パターンクリア
  pixels2.show(); //色パターンを表示(点灯)
}

ライブラリを使用するためAdafruit_NeoPixel.hをインクルードします。Adafruit_NeoPixelクラスの型の変数を宣言してインスタンス化します。インスタンス化した変数の第1引数にはRGB LEDを操作する数を指定します。第2引数にはRGB LEDを操作するDOピンを指定します。第3引数にはRGB LEDの色のタイプ及びタイミング波形の周波数を選択します。

Mech keycapを操作する変数をpixelsとしています。第3引数はMech keycapのRGBパターンであるNEO_GRBを指定し、応答速度にNEO_KHZ800(800kHz)を指定して加算しています。

PL9823-F5を操作する変数をpixels2としています。PL9823-F5を2つ使って色のパターンを表示するため第1引数に2を指定しています。第3引数はPL9823-F5の点灯順であるRGBを指定するためNEO_RGBを指定し応答速度を調整するためNEO_KHZ400(400kHz)を指定して加算しています。

PL9823-F5のデータシートには最大800kHzと記載がありますがライブラリのNEO_KHZ800(800KHz)を指定すると動作しないことがあったので速度を落としています。

begin()関数はインスタンス化した変数で指定したDOのピンの設定を行います。clear()関数は点灯のパターンをクリアします。show()関数で指定した点灯のパターンでLEDを点灯します。

ライブラリの使用例

//Mech keycapの点灯パターン
pixels.setPixelColor(0,mred,mgrn,mblu);
pixels.show();
//PL9823-Fの点灯パターン
pixels2.setPixelColor(0,lred,lgrn,lblu);
pixels2.show();

色パターンの指定はsetPixelColor()関数を使用します。第1引数に点灯させるLEDのインデックス番号を指定します。インデックス番号はRGB LEDを連結して点灯する場合の番号です。第2引数は赤色の点灯パターンを0~255で指定します。第3引数は緑色の点灯パターンを0~255で指定します。第4引数は青色の点灯パターンを0~255で指定します。

setPixelColor()関数で点灯パターンを指定した後、show()関数を使用することで任意のパターンで点灯させることができます。

スポンサーリンク

WebServerライブラリを使用する

WiFiライブラリを使用してアクセスポイントを使用する方法とWebServerライブラリの使い方を下記記事にまとめています。

ESP32-WROOM-32EでWiFiのアクセスポイントを実装

ESP32-WROOM-32EでWebServerを実装する

以下ではWebServerの要点のみまとめています。

WebServerの準備

#include <WebServer.h>

WebServer Wserver(80); //WebServer情報に関する変数(インスタンス)

void setup() {
  Wserver.on("/", HTTP_GET, HtmlSet); //URLを指定して処理する関数を指定
  Wserver.on("/", HTTP_POST, HtmlPost); //URLを指定して処理する関数を指定
  Wserver.onNotFound(handleNotFound); //URLが存在しない場合の処理する関数を指定
  Wserver.begin(); //Webサーバーの開始
}
void loop(){
  Wserver.handleClient();
}

WebServerライブラリを使用するためWebServer.hをインクルードします。WebServerの型で変数を宣言しインスタンスを生成します。初期化関数内でWebServerの設定を行います。

on()関数を使用するとブラウザーで指定したURLで表示するページを分岐させることができます。第1引数にURLアドレスの階層を示す文字、第2引数にHTTPの種類を指定、第3引数に接続要求の処理を行う関数を指定します。

onNotFound()関数はページが存在しない時に表示する関数を指定します。on()関数やonNotFound()関数の指定後はbegin()関数でWebサーバーを開始します。

クライアント接続の待機

void loop() {

  Wserver.handleClient(); //接続待ち
}

handleClient()関数でクライアントからのリクエストを常時監視します。クライアントからリクエストがあれば登録したURLに対応する処理が呼び出されます。

HTMLデータの生成

void HtmlSet(void){
  String str = "";

  str += "<!DOCTYPE html>";
  str += "<html lang =\'ja\'>";
  str += "<head>";
  str += "<meta charset =\'UTF-8\'>";
  str += "<title>RGB LED動作確認</title>";
  str += "</head>";
  str += "<body>";
  str += "<h1>RGB LED動作確認</h1>";
  //省略
  str += "</body>";
  str += "</html>";

  Wserver.send(200,"text/html", str); 
  //HTTPレスポンス200でhtmlデータとして送信
}

クライアントからのリクエストに対してHTMLデータで返信するためHtmlSet()を実装します。

HTMLデータの作成方法は様々ですがString型を使って文字列を生成して送信する方法を紹介します。strをString型で宣言しHTMLデータになるように+=でstrに文字列を付け加えていきます。

一文字でも間違うと表示できないことがあるためテキストファイルなどでHTML形式で文章を生成しGoogle Chromeのブラウザーで表示できることを確認しておくとよいでしょう。

ESP32のString型はRAM領域の容量の関係上、65535バイト分(2バイト)までしか対応していないため文字の増やし過ぎには注意が必要です。

HTMLデータを生成した後はWebServerのメンバー関数であるsend()関数を使ってクライアントに返信します。

第1引数ではHTTPのレスポンスでとして200(リクエストOK)を指定しています。第2引数では返信するテキスト文字がHTMLタイプであることを通知しています。第3引数では返信する文字列を指定します。

カラーピッカを使用する

<input type="color" id="colorbox" value="#000000">
選択した値:<label id ="colortxt"></label>

<script>
    let clrbox = document.getElementById('colorbox');
    let clrtxt = document.getElementById('colortxt');

    clrbox.addEventListener('change',function(){
        clrtxt.innerText = this.value;
    });
</script>

HTMLでカラーピッカを使用する場合はinput(フォーム入力)でtypeにcolorを指定します。valueはカラーピッカの初期値を指定します。idを指定することでカラーピッカで設定した値をscriptから取得することができます。

scriptでdocument.getElementById()でカラーピッカのID(colorbox)及びラベルのID(colortxt)を宣言します。カラーピッカとリンクしているclrboxにおいてイベントの発生を待機します。イベントの種別をchangeに指定することでカラーピッカの値が変化したときにイベントが発生します。

イベントではカラーピッカの値(value)をラベルとリンクしているclrtxtにinnerTextでカラーピッカの値をテキストで取得します。

カラーピッカのサンプル:色を選択すると値が表示します。

選択した値:

カラーピッカで色を選択すると選択した値が表示されます。取得した値を使ってRGB LEDの操作に使用します。例ではラベルに表示していますが、RGB LEDを操作する値を手動で変更できるようにソースコードではテキストボックスを使用しています。

カラーピッカの値を取得して設定する

void HtmlPost(void){
  String mtxt;
  uint32_t mrgb;

  mechtxt = Wserver.arg("mechtxt");//mech keycapの値
  //ブラウザーに表示するhtml(省略)

  if( mechtxt.indexOf("#") == 0 ){
    if( strcmp((char*)&mechtxt,(char*)&oldmechtxt) ){
        oldmechtxt = mechtxt;
        mtxt = mechtxt;
        mtxt.replace("#","");
        mrgb = strtol((char*)&mtxt, NULL,16);
        mred = (mrgb & 0xFF0000 ) >> 16; //red
        mgrn = (mrgb & 0x00FF00 ) >> 8; //green
        mblu = (mrgb & 0x0000FF ); //blue
        pixels.setPixelColor(0,mred,mgrn,mblu);//パターンの指定
        pixels.show();
      }
    }
  }
}

HtmlPost()はHTMLページのフォームからデータを取得すると呼び出される関数(on()関数で指定)です。Webサーバーでarg()関数で取得する対象の名前を指定します。例ではテキストボックスの名前であるmechtxtのデータを取得しています。

カラーピッカはMSB順に赤、緑、青の順番で3byteの値で構成されています。取得した文字列からRGB LEDの操作に使用するデータの生成を行います。

カラーピッカから取得したデータは#FFFFFFのように先頭に#が付加されるのでStringのメンバー関数であるindexOf()関数で#が文字列に含まれるかを確認しています。

次に#を含む文字列からreplace()関数で#を削除し、strtol()関数で16進数の文字列を10進数のデータに変換します。変換したデータであるmrgbを使用して各色の値を算出します。

赤の値(mred)はmrgbと0xFF0000の論理積を計算した結果を16ビット分右にシフトすることで得られます。緑の値(mgrn)はmrgbと0x00FF00の論理積を計算した結果を8ビット分右にシフトすることで得られます。青の値(mblu)はmrgbと0x0000FFの論理積の値になります。

これらの値をSetPixelColor()関数で指定し、show()関数によってカラーピッカで選択した値に応じたパターンでLEDを点灯させることができます。

スポンサーリンク

動作確認

動作確認用の回路図
動作確認用の回路図

ESP32-WROOM-32EとSHT35-DISの配線例を示しています。回路図の番号はESP32 -WROOM-32Eの左上を1ピンとした時反時計回りにピンを数えた場合の番号としています。ピン番号横の()内の番号はシルク印刷されているピンの名称です。

PL9823-F5のデータシートの実装例に抵抗が実装されていますが値が記載されていません。構成部品不要とも記載されているため抵抗は不要だと思いますが、電流制限の目的で抵抗を実装しています。抵抗を実装すると各色の輝度に影響するため100Ωの抵抗にしています。

抵抗を実装せず直接配線するとカラーピッカで設定した値に近い色合いになります。PL9823-F5のLED電源は輝度をあげるためにDC5Vを使用しています。

直接配線した場合DC3.3Vにおいても色合いが保てているため抵抗を実装しない方が良いかもしれません。

WiFiのアクセスポイントである「EngKapi1」に接続した状態でGoogle Chrome等のブラウザーからIPアドレス「192.168.11.2」を入力しリクエストを送るとESP32-WROOM-32EからRGB LED動作確認のページを返信します。

WebServerの返信によるページの表示
WebServerの返信によるページの表示
カラーピッカによる色パターンの設定
カラーピッカによる色パターンの設定

Mech keycapのLEDの色の選択とRGB LEDの色の選択をカラーピッカで行います。カラーピッカをタッチするとブラウザーによって表示は異なりますが色のパターンを選択する画面が表示されます。色を選択すると選択した値のテキストにRGBを示す値を表示します。テキストに手動で値を入力して指定することもできます。

色のパターン変更のボタンは対象のLEDのパターン変更しますが、色の変更がない場合は処理を行わないようにしています。カラーピッカの黒色に近くなるとLEDが消灯します。

カラーピッカによるRGB LEDの動作確認
カラーピッカによるRGB LEDの動作確認

Mech keycapの色のパターンとしてカラーピッカで#0090ffを選択すると緑と青を合成した色である水色(青が強め)になります。またRGB LEDの色パターンをカラーピッカで#ffae00を選択すると赤と緑を合成した色である黄色(赤が強め)になっています。

色パターン変更のボタンを押して各LEDの色を確定するとカラーピッカの表示色とほぼ同等の色でLEDが点灯していることが確認できました。

スポンサーリンク

ソースコード全体

以下のソースコードはコンパイルして動作確認をしております。コメントなど細かな部分で間違っていたりやライブラリの更新などにより動作しなくなったりする可能性があります。参考としてお使いいただければと思います。

#include <WebServer.h>
#include <WiFi.h>
#include <Adafruit_NeoPixel.h>

#define TIME_OFF -1 //タイマーを使用しない場合
#define TIME_UP 0 //タイムアップ
#define BASE_CNT 10 //ベースタイマカウント値
#define TIM_CLR 200
#define NUMPIXELS 1 // Popular NeoPixel ring size
#define BUTTON_PIN   5
#define PIXEL_PIN    18
#define PIXEL_PIN2   17

const char *ssid = "EngKapi1"; //SSID
const char *pass = "22223333"; //password
const IPAddress ip(192,168,11,2); //IPアドレス
const IPAddress subnet(255,255,255,0); //サブネットマスク

uint32_t beforetimCnt = millis();
WebServer Wserver(80);
String mechtxt,oldmechtxt;
String ledtxt,oldledtxt;
String btnval;
uint8_t mred;
uint8_t mgrn;
uint8_t mblu;
uint8_t lred;
uint8_t lgrn;
uint8_t lblu;
Adafruit_NeoPixel pixels(NUMPIXELS, PIXEL_PIN, NEO_GRB  + NEO_KHZ800);
Adafruit_NeoPixel pixels2(NUMPIXELS, PIXEL_PIN2, NEO_RGB  + NEO_KHZ400);
int16_t timClear;

/* プロトタイプ宣言 */
void mainTimer(void);
void mainApp(void);
void HtmlSet(void);
void HtmlPost(void);
void handleNotFound(void);

void setup() {

  //Serial.begin(115200);
  pinMode(BUTTON_PIN, INPUT_PULLDOWN);

  WiFi.softAP(ssid, pass); //WiFiのアクセスポイントの設定
  WiFi.softAPConfig(ip, ip, subnet); //アクセスポイントのIP及びサブネットマスク
  Wserver.on("/", HTTP_GET, HtmlSet); //URLを指定して処理する関数を指定
  Wserver.on("/", HTTP_POST, HtmlPost); //URLを指定して処理する関数を指定
  Wserver.onNotFound(handleNotFound); //URLが存在しない場合の処理する関数を指定
  Wserver.begin(); //Webサーバーの開始

  mechtxt ="#FFFFFF";
  ledtxt ="#FFFFFF";
  mred = 0;
  mgrn = 0;
  mblu = 0;
  pixels.begin();
  pixels.clear();
  pixels.show(); // Initialize all pixels to 'off'

  lred = 0;
  lgrn = 0;
  lblu = 0;
  pixels2.begin();
  pixels2.clear();
  pixels2.show(); // Initialize all pixels to 'off'
}

void loop() {

  mainTimer();
  Wserver.handleClient();

  if( digitalRead(BUTTON_PIN)){
    if( timClear == TIME_OFF){
      timClear = TIM_CLR;
    }
  }
  else{
    timClear = TIME_OFF;
  }

  if( timClear == TIME_UP){
    timClear = TIME_OFF;
    oldmechtxt ="#000000";
    oldledtxt ="#000000";
    pixels.clear();
    pixels.show();
    pixels2.clear();
    pixels2.show();
  }
}
/* タイマ管理 */
void mainTimer(void){

  if ( millis() - beforetimCnt > BASE_CNT ){
    beforetimCnt = millis();

    if( timClear > TIME_UP){
      --timClear;
    }
  }
}
/* クライアントに返信するhtmlデータを生成 */
void HtmlSet(void){
  String str = "";

  str += "<!DOCTYPE html>";
  str += "<html lang =\'ja\'>";
  str += "<head>";
  str += "<meta charset =\'UTF-8\'>";
  str += "<title>RGB LED動作確認</title>";
  str += "</head>";
  str += "<body>";
  str += "<h1>RGB LED動作確認</h1>";
  str += "<form method='post'>";
  str += "<table border ='1pt' style='background:lightyellow'>";
  str += "<tr><th>項目</th><th>色パターン変更</th></tr>";
  str += "<td>";
  str += "<label>Mech keycapの色を選択:</label>";
  str += "<br>";
  str += "<input type='color' id='colorbox' value='" + mechtxt +"'>";
  str += "選択した値:<input type='text' id='mechtxt' name='mechtxt' value='" + mechtxt +"'>";
  str += "</td>";
  str += "<td>";
  str += "<br>";
  str += "<button type='submit' name='BtnSel' value='1'";
  str += "style='padding: 5px; min-width: 100px;border-radius: 5px";
  str += "font-family: inherit; background: lightgreen; font-size: 1rem'>";
  str += "Mech keycap</button>";
  str += "<br><br>";
  str += "</td>";
  str += "<tr>";
  str += "<td>";
  str += "<label >RGB LEDの色を選択:</label>";
  str += "<br>";
  str += "<input type='color' id='colorbox1' value='" + ledtxt +"'>";
  str += "選択した値:<input type='text' id='ledtxt' name='ledtxt' value='" + ledtxt +"'>";
  str += "</td>";
  str += "<td>";
  str += "<br>";
  str += "<button type='submit' name='BtnSel' value='2'";
  str += "style='padding: 5px; min-width: 100px;border-radius: 5px";
  str += "font-family: inherit; background: lightgreen; font-size: 1rem'>";
  str += "RGB LED</button>";  
  str += "<br><br>";
  str += "</td>";
  str += "</tr>";
  str += "</table>";
  str += "</form>";
  str += "<script>";
  str += "let clrbox = document.getElementById('colorbox');";
  str += "let clrbox1 = document.getElementById('colorbox1');";
  str += "let mechtxt = document.getElementById('mechtxt');";
  str += "let ledtxt = document.getElementById('ledtxt');";
  str += "clrbox.addEventListener('change',function(){";
  str += "mechtxt.value = this.value;";
  str += "});";
  str += "clrbox1.addEventListener('change',function(){";
  str += "ledtxt.value = this.value;";
  str += "});";
  str += "</script>";
  str += "</body>";
  str += "</html>";

  Wserver.send(200,"text/html", str); 
  //HTTPレスポンス200でhtmlデータとして送信
}

/* 送信を押したときに遷移する */
void HtmlPost(void){
  String str = "";

  mechtxt = Wserver.arg("mechtxt");
  ledtxt = Wserver.arg("ledtxt");
  btnval = Wserver.arg("BtnSel");
  Serial.println(mechtxt);
  Serial.println(ledtxt);

  str += "<html lang=\"ja\">";
  str += "<head>";
  str += "<meta http-equiv='refresh' content='2;'>";
  str += "<meta charset=\"UTF-8\">";
  str += "<title>Sensor graph</title>";
  str += "</head>";
  str += "<body>";
  str += "<h1>設定変更中</h1>";
  str += "</body>";
  str += "</html>";

  Wserver.send(200, "text/html", str);
  mainApp();
}
/* POSTで受けてLEDを操作する */
void mainApp(void){
  String mtxt;
  String ltxt;
  uint32_t mrgb;
  uint32_t lrgb;
  uint8_t btn;

  btn = btnval.toInt();

  switch (btn)
  {
  case 1:
    if( mechtxt.indexOf("#") == 0 ){
      if( strcmp((char*)&mechtxt,(char*)&oldmechtxt) ){
        oldmechtxt = mechtxt;
        mtxt = mechtxt;
        mtxt.replace("#","");
        mrgb = strtol((char*)&mtxt, NULL,16);

        mred = (mrgb & 0xFF0000 ) >> 16;
        mgrn = (mrgb & 0x00FF00 ) >> 8;
        mblu = (mrgb & 0x0000FF );

      //Serial.println(mrgb);
      //Serial.println(mred);
      //Serial.println(mgrn);
      //Serial.println(mblu);
        pixels.setPixelColor(0,mred,mgrn,mblu);
        pixels.show();
      }
    }
    break;
  case 2:
    if( ledtxt.indexOf("#") == 0 ){
      if( strcmp((char*)&ledtxt,(char*)&oldledtxt) ){
        oldledtxt = ledtxt;
        ltxt = ledtxt;
        ltxt.replace("#","");
        lrgb = strtol((char*)&ltxt, NULL,16);
        lred = (lrgb & 0xFF0000 ) >> 16;
        lgrn = (lrgb & 0x00FF00 ) >> 8;
        lblu = (lrgb & 0x0000FF );
      //Serial.println(lrgb);
      //Serial.println(lred);
      //Serial.println(lgrn);
      //Serial.println(lblu);
        pixels2.setPixelColor(0,lred,lgrn,lblu);
        pixels2.show();
      }
    }
    break;  
  default:
    break;
  }
}
/* URLが存在しない場合の処理 */
void handleNotFound(void) {

  String message = "File Not Found\n\n";
  message += "URI: ";
  message += Wserver.uri();
  message += "\nMethod: ";
  message += (Wserver.method() == HTTP_GET) ? "GET" : "POST";
  message += "\nArguments: ";
  message += Wserver.args();
  message += "\n";

  for (uint8_t i = 0; i < Wserver.args(); i++) {
    message += " " + Wserver.argName(i) + ": " + Wserver.arg(i) + "\n";
  }
  Wserver.send(404, "text/plain", message); //テキストファイルであることを示している。
}

関連リンク

Arduinoのライブラリを使って動作確認を行ったことを下記リンクにまとめています。

Arduinoで学べるマイコンのソフト開発と標準ライブラリの使い方

Seeeduino XIAOで学べるソフト開発と標準ライブラリの使い方

ESP32-WROOM-32Eで学べるソフト開発と標準ライブラリの使い方

GEEKJOB-未経験からITエンジニアに【オンライン無料体験】

最後まで、読んでいただきありがとうございました。

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