気の向くままに辿るIT/ICT/IoT
IoT・電子工作

ESP-01/12/ESP32でリモコン付き扇風機をWiFi操作

ホーム前へ次へ
ESP8266って?

ESP-01/12/ESP32でリモコン付き扇風機をWiFi操作

ESP-01/12/ESP32でリモコン付き扇風機をWiFi操作

自作スマートリモコンで操作する扇風機
2019/04/14

 Wi-Fi(wifi)モジュールESP8266/ESP32開発ボードを使ってAC100V含む赤外線リモコン対応家電を操作する、いわゆるスマートリモコンの自作や非IRリモコン家電を無線で遠隔操作できる、いわゆるスマートコンセント・スマートプラグを自作してみるシリーズ。

 ESP8266/ESP-WROOM-32チップ単体やピッチ変換モジュールとの併用はより省スペースではありますが、ESP8266/ESP32開発ボードを使う方が、何かと手間もなく、無難です。

 今回は、某社製リモコン扇風機用のスマートリモコンを作りました。

 実は、家にあるリモコン対応扇風機は、4台あり、若干機種が異なるものもあるものの、全て同メーカー品で首振り含め、リモコン操作可能な機能は基本、同じです。

 他に非リモコンの小型扇風機が4台、やはり、非リモコンのサーキュレーターが6台ほどあり、非リモコン家電については、自作スマートコンセントで無線操作という選択肢もあります。

 当初、パソコンやタブレット、スマホのブラウザから、これら家電を遠隔操作することを想定していましたが、今となっては、自身は、自作スマートスピーカーメインPCにも搭載の自作スマートスピーカー機能を使って音声で操作するのがメインとなっています。

 よってブラウザからの無線操作のみならず、ラズパイスマートスピーカーで空気清浄機を音声操作可能にします。

操作メニュー

 対象となる扇風機の操作については、とりあえず、電源、オンタイマー、オフタイマー、首振り、風量アップ、風量ダウン、モード選択あたりを実装することにしました。

 尤もモード選択は要らないかもしれません。

事前準備

 スマートリモコンを作るにあたっては、全ては「操作(ボタン・メニュー)に対して、どんな並びの赤外線信号を送信するか」であり、基本的に、これら以外の違いはなく、家電による差もない為、ハードウェアもソフトウェアも共通。

 よって作り方の詳細は、冒頭の自作スマートリモコンのリンク先に譲ります。

 事前準備としては、markszabo/IRremoteESP8266などESP8266用の任意のIR信号送受信ライブラリを使い、ESP8266で送受信回路を作って機能させたい家電のリモコンから受信機に信号を送信、これを解析して(読み取って)おき、操作ボタンと信号のリストを作っておきます。

 尚、今回の家電は扇風機ですが、IRremoteESP8266のIRrecvDump/IRrecvDumpV2では(信号方式としては)、UNKNOWNだったので、sendRaw()関数を使って生(raw)データを送ることにしました。

回路とスケッチ・プログラム

#include <ESP8266WiFi.h>
#include <WiFiClient.h>
#include <ESP8266WebServer.h>
#include <ESP8266mDNS.h>
#include <IRremoteESP8266.h>
#include <IRsend.h>
#include <Arduino.h>
#include <FS.h>
 
const char* path_root   = "/index.html";
 
  const char *ssid = "ssid";
  const char *password = "password";
 
//#define BUFFER_SIZE 16384
//uint8_t buf[BUFFER_SIZE];
 
uint16_t len;
uint16_t freq = 38;
 
ESP8266WebServer server ( 80 );
IRsend irsend(4);
#define SOFTAP_SSID "ESPCOOLFAN"
#define SOFTAP_PW "esp8266coolfan"
 
boolean readHTML() {
  File htmlFile = SPIFFS.open(path_root, "r");
  if (!htmlFile) {
    Serial.println("Failed to open index.html");
    return false;
  }
  size_t size = htmlFile.size();
  if (size >= BUFFER_SIZE) {
    Serial.print("File Size Error:");
    Serial.println((int)size);
  } else {
    Serial.print("File Size OK:");
    Serial.println((int)size);
  }
//  htmlFile.read(buf, size);
  htmlFile.close();
  return true;
}
 
void handleRoot() {
  Serial.println("Access");
  char temp[100];
  int sec = millis() / 1000;
  int min = sec / 60;
  int hr = min / 60;
 
  snprintf ( temp, 100, "", hr, min % 60, sec % 60 );
  server.send(200, "text/html", (char *)buf);
}
 
uint16_t auto_drive[] = {
//120134564, 3820, 1868, 496, 420, 524, 1388, 496, 424, 520, 1388, 500, 420, 552, 1384, 500, 424, 520, 1392, 496, 420, 524, 1388, 496, 424, 520, 1388, 500, 1388, 500, 420, 548, 1392, 496, 420, 524, 1388, 496, 1392, 496, 1392, 496, 1388, 500, 420, 524, 420, 524, 1416, 492, 1396, 496, 420, 520, 420, 524, 424, 520, 420, 524, 1388, 500, 420, 520, 424, 524, 420, 520, 424, 524, 444, 524, 424, 516, 424, 524, 420, 520, 424, 520, 420, 524, 424, 520, 1388, 500, 424, 520, 420, 520, 420, 528, 1388, 524, 424, 516, 424, 524, 420, 524, 416, 524, 424, 520, 420, 524, 424, 524, 416, 524, 1388, 500, 420, 520, 424, 524, 1388, 524, 1388, 496, 1392, 496, 1388, 508, 1380, 500, 1388, 500, 420, 520, 424, 520, 420, 524, 448, 520, 424, 524, 1388, 496, 424, 520, 420, 524, 420, 524, 424, 520, 420, 520, 424, 524, 420, 520, 420, 524, 420, 524, 424, 520, 448, 524, 1388, 496, 424, 520, 420, 524, 424, 520, 420, 524, 420, 520, 424, 520, 424, 524, 416, 524, 420, 524, 1392, 520, 420, 524, 424, 520, 1388, 508, 1384, 492, 1392, 500, 1384, 500, 1388, 496, 424, 524, 424, 544, 420, 524, 1388, 496, 1396, 492, 1392, 500, 1384, 500,
 
3820, 1868, 496, 420, 524, 1388, 496, 424, 520, 1388, 500, 420, 552, 1384, 500, 424, 520, 1392, 496, 420, 524, 1388, 496, 424, 520, 1388, 500, 1388, 500, 420, 548, 1392, 496, 420, 524, 1388, 496, 1392, 496, 1392, 496, 1388, 500, 420, 524, 420, 524, 1416, 492, 1396, 496, 420, 520, 420, 524, 424, 520, 420, 524, 1388, 500, 420, 520, 424, 524, 420, 520, 424, 524, 444, 524, 424, 516, 424, 524, 420, 520, 424, 520, 420, 524, 424, 520, 1388, 500, 424, 520, 420, 520, 420, 528, 1388, 524, 424, 516, 424, 524, 420, 524, 416, 524, 424, 520, 420, 524, 424, 524, 416, 524, 1388, 500, 420, 520, 424, 524, 1388, 524, 1388, 496, 1392, 496, 1388, 508, 1380, 500, 1388, 500, 420, 520, 424, 520, 420, 524, 448, 520, 424, 524, 1388, 496, 424, 520, 420, 524, 420, 524, 424, 520, 420, 520, 424, 524, 420, 520, 420, 524, 420, 524, 424, 520, 448, 524, 1388, 496, 424, 520, 420, 524, 424, 520, 420, 524, 420, 520, 424, 520, 424, 524, 416, 524, 420, 524, 1392, 520, 420, 524, 424, 520, 1388, 508, 1384, 492, 1392, 500, 1384, 500, 1388, 496, 424, 524, 424, 544, 420, 524, 1388, 496, 1396, 492, 1392, 500, 1384, 500,
};
uint16_t cooler[] = {
...
};
uint16_t heater[] = {
...
};
uint16_t dry[] = {
...
};
...
 
void Power() {
  Serial.println("power");
  len = sizeof(power) / sizeof(uint16_t);
  irsend.sendRaw(power, len, freq);
  delay(10);
  irsend.sendRaw(power, len, freq);
  delay(2000);
  server.send(200, "text/html", "power");
}
void Off_Timer() {
  Serial.println("off_timer");
  len = sizeof(off_timer) / sizeof(uint16_t);
  irsend.sendRaw(off_timer, len, freq);
  delay(10);
  irsend.sendRaw(off_timer, len, freq);
  delay(2000);
  server.send(200, "text/html", "off_timer");
}
void On_Timer() {
  Serial.println("on_timer");
  len = sizeof(on_timer) / sizeof(uint16_t);
  irsend.sendRaw(on_timer, len, freq);
  delay(10);
  irsend.sendRaw(on_timer, len, freq);
  delay(2000);
  server.send(200, "text/html", "on_timer");
}
void Neck_Swing() {
  Serial.println("neck_swing");
  len = sizeof(neck_swing) / sizeof(uint16_t);
  irsend.sendRaw(neck_swing, len, freq);
  delay(10);
  irsend.sendRaw(neck_swing, len, freq);
  delay(2000);
  server.send(200, "text/html", "DRY");
}
...
void handleNotFound() {
 
  String message = "File Not Found\n\n";
  message += "URI: ";
  message += server.uri();
  message += "\nMethod: ";
  message += ( server.method() == HTTP_GET ) ? "GET" : "POST";
  message += "\nArguments: ";
  message += server.args();
  message += "\n";
 
  for ( uint8_t i = 0; i < server.args(); i++ ) {
    message += " " + server.argName ( i ) + ": " + server.arg ( i ) + "\n";
  }
  server.send ( 404, "text/plain", message );
}
 
void setup() {
  Serial.begin(115200);
 
  SPIFFS.begin();
  if (!readHTML()) {
    Serial.println("Read HTML error!!");
  }
 
  WiFi.begin(ssid, password);
  irsend.begin();
  Serial.println("");
  // AP+STAモードの設定
  WiFi.mode(WIFI_AP_STA);
  //  WiFi.mode(WIFI_STA);
  // APとして振る舞うためのSSIDとPW情報
  WiFi.softAP(SOFTAP_SSID, SOFTAP_PW);
  Serial.print("Connecting to ");
  Serial.println(SOFTAP_SSID);
  Serial.println("----------");
 
  //wait for connection
  while ( WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.print("Connected to ");
  Serial.println(ssid);
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());
 
  if (!MDNS.begin("espcoolfan")) {
    Serial.println("Error setting up MDNS responder!");
    while (1) {
      delay(1000);
    }
  }
  Serial.println("mDNS responder started");
 
  server.on("/", handleRoot);
  server.on("/power", Power);
  server.on("/off_timer", Off_Timer);
  server.on("/on_timer", On_Timer);
  server.on("/neck_swing", Neck_Swing);
  ...
  server.onNotFound(handleNotFound);
 
  server.begin();
  Serial.println("HTTP server started");
 
  // Add service to MDNS-SD
  MDNS.addService("http", "tcp", 80);
}
 
void loop() {
  server.handleClient();
}

 ライブラリには、IRremoteESP8266を使わせて頂きました。

 自身もそうしましたが、この手のESP8266のスケッチ・プログラム概要としては、Webサーバを立てSPIFFSによりESP8266のメモリ上にトップページに各種ボタンを配置した操作画面となるHTMLファイルを置き、他に操作ごとのページ(URLだけあればよくHTMLファイルは不要)を作り、そこにアクセスするとそれぞれの操作信号を送信するという作りにするのが一般的でしょう。

 ESP8266によるアクセスポイントは、仮にESPCOOLFANとしたので無線AP一覧にもこれが出てくることになります。

 ただ、家電の数だけSOFT_APを立てると1軒でも結構な数になり、帯域を消費してしまうとしたら、微妙かなと...。

 mDNSは、仮にespcoolfanとしたのでespcoolfan.localでPCブラウザなどからアクセスでき、SPIFFSでHTMLファイルをアップロードしていれば、例えば、操作画面が表示され、espcoolfan.local/powerにアクセスすると個別に電源ON/OFF操作できるようになっています。

 今回、扇風機で使うことになったirsend.sendRaw()の引数は、uint16_tだったため、IRrecvDump/IRrecvDumpV2ではunsigned intだったrawデータの型、データ長格納用変数もuint16_t型とし、定数であるリモコンで多く使われるという周波数38(kHz)もuint16_t型の変数に代入しました。

 ただ、htmlFile.read(buf, sizeでつまづき、SPIFFについては、(試してないのでなんですが、たぶん)未解決です。

 それでもブラウザからは操作できているので、とりあえず、よしとしました。

[2019/04/30 訂正・追記:]
 勘違い...、APモードにする(Soft_APを立てる)必要はありませんでした。

#include <ESP8266WiFi.h>
#include <WiFiClient.h>
#include <ESP8266WebServer.h>
#include <ESP8266mDNS.h>
#include <IRremoteESP8266.h>
#include <IRsend.h>
#include <Arduino.h>
#include <FS.h>
 
const char* path_root   = "/index.html";
 
  const char *ssid = "ssid";
  const char *password = "password";
 
#define BUFFER_SIZE 16384
uint8_t buf[BUFFER_SIZE];
 
uint16_t len;
uint16_t freq = 38;
 
ESP8266WebServer server ( 80 );
IRsend irsend(4);
 
boolean readHTML() {
  File htmlFile = SPIFFS.open(path_root, "r");
  if (!htmlFile) {
    Serial.println("Failed to open index.html");
    return false;
  }
  size_t size = htmlFile.size();
  if (size >= BUFFER_SIZE) {
    Serial.print("File Size Error:");
    Serial.println((int)size);
  } else {
    Serial.print("File Size OK:");
    Serial.println((int)size);
  }
  htmlFile.read(buf, size);
  htmlFile.close();
  return true;
}
 
void handleRoot() {
  Serial.println("Access");
 
  server.send(200, "text/html", (char *)buf);
 
  char message[20];
  String(server.arg(0)).toCharArray(message,20);
 
  if(server.arg(0).indexOf("power") != -1){
    Serial.println("power");
    power();
  }
  else if(server.arg(0).indexOf("off_timer") != -1){
    Serial.println("off_timer");
    off_timer();
  }
 ...
}
 
uint16_t auto_drive[] = {
3820, 1868, 496, 420, 524, 1388, 496, 424, 520,..., 500, 1384, 500,
};
uint16_t cooler[] = {
...
};
uint16_t heater[] = {
...
};
uint16_t dry[] = {
...
};
...
 
void Power() {
  Serial.println("power");
  len = sizeof(power) / sizeof(uint16_t);
  irsend.sendRaw(power, len, freq);
  delay(10);
  irsend.sendRaw(power, len, freq);
  delay(2000);
  server.send(200, "text/html", "power");
}
void Off_Timer() {
  Serial.println("off_timer");
  len = sizeof(off_timer) / sizeof(uint16_t);
  irsend.sendRaw(off_timer, len, freq);
  delay(10);
  irsend.sendRaw(off_timer, len, freq);
  delay(2000);
  server.send(200, "text/html", "off_timer");
}
void On_Timer() {
  Serial.println("on_timer");
  len = sizeof(on_timer) / sizeof(uint16_t);
  irsend.sendRaw(on_timer, len, freq);
  delay(10);
  irsend.sendRaw(on_timer, len, freq);
  delay(2000);
  server.send(200, "text/html", "on_timer");
}
void Neck_Swing() {
  Serial.println("neck_swing");
  len = sizeof(neck_swing) / sizeof(uint16_t);
  irsend.sendRaw(neck_swing, len, freq);
  delay(10);
  irsend.sendRaw(neck_swing, len, freq);
  delay(2000);
  server.send(200, "text/html", "DRY");
}
...
void handleNotFound() {
 
  String message = "File Not Found\n\n";
  message += "URI: ";
  message += server.uri();
  message += "\nMethod: ";
  message += ( server.method() == HTTP_GET ) ? "GET" : "POST";
  message += "\nArguments: ";
  message += server.args();
  message += "\n";
 
  for ( uint8_t i = 0; i < server.args(); i++ ) {
    message += " " + server.argName ( i ) + ": " + server.arg ( i ) + "\n";
  }
  server.send ( 404, "text/plain", message );
}
 
void setup() {
  Serial.begin(115200);
 
  SPIFFS.begin();
  if (!readHTML()) {
    Serial.println("Read HTML error!!");
  }
 
  WiFi.begin(ssid, password);
  irsend.begin();
  Serial.println("");
  // AP+STAモードの設定
  WiFi.mode(WIFI_AP_STA);
  //  WiFi.mode(WIFI_STA);
 
  //wait for connection
  while ( WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.print("Connected to ");
  Serial.println(ssid);
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());
 
  if (!MDNS.begin("espcoolfan")) {
    Serial.println("Error setting up MDNS responder!");
    while (1) {
      delay(1000);
    }
  }
  Serial.println("mDNS responder started");
 
  server.on("/", handleRoot);
  server.on("/power", Power);
  server.on("/off_timer", Off_Timer);
  server.on("/on_timer", On_Timer);
  server.on("/neck_swing", Neck_Swing);
  ...
  server.onNotFound(handleNotFound);
 
  server.begin();
  Serial.println("HTTP server started");
 
  // Add service to MDNS-SD
  MDNS.addService("http", "tcp", 80);
}
 
void loop() {
  server.handleClient();
}

[2019/05/06 訂正・追記:]
 勘違い...、太字で強調しましたが、こんな風にしたら、SPIFFSで操作パネルを表示する恰好でいけました。
 元々あった2箇所3行のコメント行をやっぱり有効にする。
 handleRoot()内にコマンドの数だけ条件分岐を追記(これを忘れてたのが元凶)。
 これは前段のスケッチでも修正済みですが、void Power()など各関数のデータ長計算時の分母の方のsizeof()の引数をuint8_tからuint16_tに修正。
 あと前掲のスケッチは、bufやBUFFER_SIZEをコメントアウトしただけで代替がないのでserver.send(200, "text/html", (char *)buf);行でエラーになります...ね、すみません。

操作パネル例

 SPIFFSを利用する場合、例えば、メインメニューは、メイン操作パネル、エアコンの場合、自作スマートリモコンで東芝エアコン大清快を遠隔操作の例のようになります。

リモコンとしてのESP8266

 ESP8266とリモコン参照。

備考

 この扇風機のリモコン受信部は、操作パネル付近にあるので自作赤外線発信機(スマートリモコン)も台座上に直置きでいけます。

 ただ、赤外線LEDにもよるでしょうし、LED先端付近において光線が拡散しない工夫の有無にもよるでしょうが、自身が今回使ったものは、指向性が高い(向きが重要な)ものであり、赤外線LEDは、ブレッドボードに挿した状態で使用しました。

 エアコンと違って直置きできるためか、テレビ同様、結構な範囲で受信できますが、赤外線LEDの向きは意外と重要です。

ホーム前へ次へ