家電を声で操作してスマートホーム化したいけど、アレクサ(Amazon echo)みたいなスマートスピーカーはうちにはない、そんな人もいらっしゃるのではないでしょうか。
本記事では、音声認識部はiPhoneに任せて、iPhoneからマイコンへ命令を送って赤外線LEDを光らせることで、まるでスマートスピーカー&リモコンのような使い方ができるプロダクトの作り方記録となります。
※本記事の回路・プログラムの確実な動作保証は致しかねます。
・概要
全体の流れは以下の通り。
① Wi-Fi対応マイコンをWebサーバーとして稼働させる。
② ①のサーバーにはあらかじめ家電用リモコンの赤外線発光パターンを覚えさせておく。
③ Siriの音声認識機能を使って、iPhoneが①のサーバーにアクセスする。
④ アクセスされたサーバー(マイコン)は赤外線LEDでリモコンの信号を発信する。
⑤ 家電が動く。(完成)
まず、単品のESP32に電源やプログラム書き込み用の線をつなぎ、使える状態にします。
電源には5VのACアダプタを使ったので、3端子レギュレータで5Vから3.3Vを作り、ESP32の電源ピンに繋いています。回路図中では、ESP32が消費する大電流を補うために70mFとかいうバカでかいコンデンサをレギュレータ部に繋いでいますが、ここまでする必要はないと思います(たまたま手持ちにあって、他に使いみちがないので使っただけ)。
5V電源ではなく、最初から電圧3.3VのACアダプタをお持ちでそれを使うのであれば、レギュレータは不要です。
ESP32にはGNDピンは複数あるので、全部電源のGNDに繋ぎましょう。
ESP32のシリアル通信に使われるTx, Rxピン(WROVERではそれぞれ37, 38ピン)を、あとでUSB-シリアル変換モジュールのRx, Txと繋げられるように、ピンソケットか何かと配線で繋いでおきます。
ESP32のENピンは、回路図のように1kΩ抵抗と0.1uFのコンデンサを繋ぎ、タクトスイッチを押したらGNDと繋がるように組みます。
このスイッチは、ESP32を再起動させたいときに押します。
あ、ENピンもプログラム書き込み時にUSB-シリアル変換器と繋がないといけないので、ピンソケットか何かで外部と繋がるようにしておいてください。
同様に、IO0ピンにも押すとGNDと繋がるようスイッチを付けます。
これは、ESP32へのプログラム書き込み時に使います。ESP32にリセットがかかった時、IO0がGNDに落ちているとプログラム書き込みモードとなるので、書き込み時にはこのスイッチを押しっぱなしにします(書き込み開始されたら離してOK)。
赤外線LEDの駆動には、今回はIO4ピンを使いました。特に理由はないので4番じゃなくても良いです。赤外線信号の到達距離を稼ぐためになるべく大きな電流を赤外線LEDに流したいので、LEDは直接駆動するのではなくトランジスタで増幅した電流で光らせるようにします。
あとは、動作確認用に光らせるためのLEDをIO2ピンに繋ぎました。これは必須ではないです。
裏面にもGNDのパッドがあります。ここははんだ付けしなくても動くかもしれませんが、一応データシート的にはちゃんとはんだ付けするように言われてますので、線をつないでGNDと繋ぎました。
端面スルーホールに一本づつ配線をつけていった様子。1.27mmピッチなので、集中すれば手作業でも結構いけます。
赤外線はいろんな角度に飛ばしたいので、使用した赤外線LEDは1個ではなく3個使い、まとめて設置したあと、上から焦電センサ用のフレネルレンズをキャップのようにかぶせました。
これで回路は完成。
・Webサーバーの設置(ファームウェア編)
ESP32を、こちらが指定した名称のWebサーバーとして起動させ、他機器からのアクセスを受け付けるようにするため、下記のコードをESP32に書き込みます。
ESP32スマートリモコン
#include <Arduino.h>
#include <WebServer.h>
#include <ESPmDNS.h>
#include <IRremoteESP8266.h>
#include <IRsend.h>
#include <WiFiUdp.h>
#include <ArduinoOTA.h>
WebServer server(80); //ポートは80番を使用
const char *serverName = "espremo";
//IRremoteに関する設定
const uint16_t IR_pin = 4;//IO4ピンで赤外線LEDを使う。
IRsend irsend(IR_pin);
const char *ssid = "自宅の無線LANルーターのSSID";
const char *password = "自宅の無線LANのパスワード";
const char *FWver = "Ver. 1.0";
void living_on();
void living_off();
void rootConnect();
void info();
void error();
void setup()
{
irsend.begin();
Serial.begin(115200);
//WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) //WiFiに接続完了していない間…
{
//何もせず接続待ち待機
}
Serial.println("WiFi connected!");
Serial.print("My IP address is ");
Serial.println(WiFi.localIP());
Serial.println(" ");
//↓このあとのOTAに関する処理でサーバー名を勝手に変えられてしまうので、先に所望の名前でOTAの設定をしておく
ArduinoOTA.setHostname(serverName);
if (!MDNS.begin(serverName))
{
Serial.println("mDNS Failed");
while (1);
}
MDNS.addService("http", "tcp", 80);//Webサーバーを開始
Serial.print("My server Name is,");
Serial.println(serverName);
server.on("/", rootConnect);
server.on("/livingon", living_on);
server.on("/livingoff", living_off);
server.on("/info", info);
server.onNotFound(error);
server.begin();
//OTAに関する初期設定
ArduinoOTA
.onStart([]() {
String type;
if (ArduinoOTA.getCommand() == U_FLASH)
type = "sketch";
else // U_SPIFFS
type = "filesystem"
// NOTE: if updating SPIFFS this would be the place to unmount SPIFFS using SPIFFS.end()
Serial.println("Start updating " + type);
})
.onEnd([]() {
Serial.println("\nEnd");
})
.onProgress([](unsigned int progress, unsigned int total) {
Serial.printf("Progress: %u%%\r", (progress / (total / 100)));
})
.onError([](ota_error_t error) {
Serial.printf("Error[%u]: ", error);
if (error == OTA_AUTH_ERROR)
Serial.println("Auth Failed");
else if (error == OTA_BEGIN_ERROR)
Serial.println("Begin Failed");
else if (error == OTA_CONNECT_ERROR)
Serial.println("Connect Failed");
else if (error == OTA_RECEIVE_ERROR)
Serial.println("Receive Failed");
else if (error == OTA_END_ERROR)
Serial.println("End Failed");
});
ArduinoOTA.begin();
//ここから下にその他の初期設定
pinMode(2, OUTPUT);
for (int i =0;i<6;i++)//設定終了をLED点滅で知らせる
{
digitalWrite(2,HIGH);
delay(50);
digitalWrite(2,LOW);
delay(50);
}
Serial.println("Setup finished");
}
void loop()
{
server.handleClient(); //Webサーバーへのアクセスを確認する処理
ArduinoOTA.handle();//OTAのための待受処理
//その他の普通のloop処理があれば以下に書く
}
void living_on()//リビングの照明(アイリスオーヤマ、リモコン1)のオン信号
{
server.send(200, "text/plain", "OK");//GETリクエストへの返答
uint16_t rawCh1On[85] = {2250, 600, 5650, 750, 1700, 250, 1700, 250, 700, 300, 700, 250, 700, 300, 700, 300, 650, 350, 650, 300,
650, 350, 1600, 350, 1600, 400, 600, 350, 650, 350, 650, 350, 600, 400, 1600, 350, 600, 350, 1600, 400, 600, 350, 650, 350, 650, 350,
600, 400, 600, 350, 650, 350, 650, 350, 600, 400, 600, 350, 650, 350, 650, 350, 600, 400, 600, 350, 650, 350, 1600, 350, 650, 350,
1600, 350, 1600, 350, 1600, 400, 600, 350, 1600, 350, 1600, 350, 600}; // Protocol=UNKNOWN Data=0xF36E6C18
irsend.sendRaw(rawCh1On, 85, 38);//(rawData, データ数, IR変調周波数)
}
void living_off()//リビングの照明(アイリスオーヤマ、リモコン1)のオフ信号
{
server.send(200, "text/plain", "OK");//GETリクエストへの返答
uint16_t rawCh1Off[85] = {2250, 650, 5600, 750, 1700, 250, 1700, 300, 650, 300, 700, 300, 700, 300, 650, 350,
650, 300, 700, 300, 650, 350, 1600, 350, 1650, 300, 650, 350, 650, 350, 650, 300, 700, 300, 1650, 300, 1650, 350, 650, 300,
650, 350, 650, 350, 650, 350, 650, 300, 650, 350, 650, 350, 650, 350, 650, 300, 650, 350, 650, 350, 650, 350, 650, 300,
700, 300, 650, 350, 650, 350, 1600, 350, 1600, 350, 1550, 400, 1600, 350, 600, 450, 1500, 400, 1600, 350, 600}; // Protocol=UNKNOWN Data=0xFB0B9DE5
irsend.sendRaw(rawCh1Off, 85, 38);//(rawData, データ数, IR変調周波数)
}
void info()
{
IPAddress myIP = WiFi.localIP();
String myIPString = String(myIP[0]) + "." + String(myIP[1]) + "." + String(myIP[2]) + "." + String(myIP[3]);
String infoHTML = "<!DOCTYPE html>";
infoHTML += "<HTML><HEAD><meta charset ='UTF-8'><TITLE>スマートリモコン 情報</TITLE></HEAD>";
infoHTML += "<BODY><p><B>IPアドレス</p></B>" + myIPString;
infoHTML += "<BR><BR><p><B>FWバージョン情報</p></B>" + String(FWver);
infoHTML += "</BODY></HTML>";
server.send(200, "text/html", infoHTML);
}
void rootConnect()
{
server.send(200, "text/plain", "ROOT connected");
Serial.println("ROOT connected");
}
void error()
{
server.send(404, "text/plain", "Error");
Serial.println("Error");
}
コードの中身に特殊なものはなく、既存のライブラリをふんだんに活用しているだけです。
冒頭のSSIDやパスワードの部分には、自宅でお使いの無線LANルーターのSSIDとパスワードを入れてください。
なお、コード中にはOTA機能を入れています。OTAとはWi-Fi経由で無線でESP32にプログラムを書き込む機能のことですが、これについては、もしプログラムを書き換えたくなったときにいちいちUSB-シリアル変換モジュールを繋ぐのがめんどくさいので入れただけです。
OTA機能を使わなくても、今回のスマートリモコンとしての使用には全く問題有りません。
ESP32への書き込み方は、USB-シリアル変換モジュールを使って、モジュールのTx, Rx, DTRピンの3つをさっき作った回路に繋ぐだけで良いです。具体的には、シリアル変換モジュールのRxピンをESP32のTxピンに、モジュールのTxピンをESP32のRxピンに、モジュールのDTRピンをESP32のENピンに繋ぐだけ。
(Arduino IDEでのESP32開発環境は既に準備されているものとしますので、本記事では扱いません。)
Arduino IDE上で書き込みボタンを押してから実際に書き込み開始されるまでの間、ESP32のIO0ピンに取り付けたスイッチは押しっぱなしにしておきます。
書き込み終了したら、電源をつなぐとESP32がWi-Fiに接続し、こちらが決めた名前のサーバーとして動作を始めるはず。今回は上記コード内にある通り”espremo”という名前に設定しました。このとき設定する名前は、LAN内に存在する他の端末に設定されている名称とかぶらないようにすること。
サーバーが問題なく稼働しているとき、同じルーターに接続している手持ちのパソコンやスマホ(もちろん他のマイコンからでも良いけど)から、http://サーバー名.local にアクセスすれば、あらかじめESP32に仕込んだ動作をさせることができます。先程のコードではサーバー名はespremoとしたので、マイコンにアクセスするためのURLはhttp://espremo.localとなります。このURLにアクセスした時、ちゃんと接続できていれば”ROOT connected”ってメッセージが返ってくるはず。
リモコンとしての動作は、http://espremo.local/livingon および http://espremo.local/livingoff にアクセスしたときに呼び出されます。
これらのURLにアクセスした時、それぞれliving_on()、living_off()という関数が呼び出されます。
この関数内において、IRremoteライブラリ(厳密には、ESP32でも使えるようにされたIRremoteESP8266ライブラリ)によって、赤外線通信のプロトコルに従ってLEDを点滅させます。
上のコードでは赤外線信号をRawDataで送っていますが、これは私の家で使っているアイリスオーヤマの照明用リモコンが、謎の通信プロトコルだったため。もしお使いの家電のリモコンの通信規格がよく使われているNECフォーマットとかなら、RawDataの配列をまるまる送ったりせず、IRremoteライブラリのsendNECとかを使ってもう少しシンプルに通信をすることができます。
スマートリモコンの機能そのものではないですが、送信する赤外線信号を読み取るための回路は別途必要なので、一応その作り方も載せます。
赤外線受光モジュールとArduinoを繋ぎます。電源、GND、信号アウトプットの3本だけです。(不安定な電源の場合はリップルフィルタが要るかも。お使いの受光モジュールのデータシートをご確認ください。)
次にこのArduinoに赤外線信号を解析するプログラムを書き込みます。このプログラムはわざわざ自分で作る必要は無くて、IRremoteライブラリをArduino IDEに導入していれば、そのとき一緒に入るサンプルスケッチ「IRreceiveDumpV2」を開いて、そのままArduinoに書き込めばOKです。
書き込めたらシリアルモニタを開き、赤外線受光モジュールにリモコンの赤外線を送ると、信号の解析を行い、その内容(送信データと送信プロトコル)をPC画面に表示してくれます。
今回のアイリスオーヤマのようにプロトコルが解析できなかった場合は、プロトコル部分はunknownと表示されてしまいますので、とりあえず信号パルスのオンオフ時間が配列でずらーーっと出てきているのをそのまま送信してやればいいです。
living_on()やliving_off()関数の中にある、要素数85個の数字の集合はそれ。
シリアルモニタに表示されてる配列の中身をコピペして、それをsendRawで送ればいいです。
プロトコルがNECフォーマットやPANASONICフォーマットであると判明した場合でも、同じやり方でsendRawで送ることは一応可能です。
もっとも、プロトコルがちゃんとわかっている場合は、専用機能を使ってもっと簡単な記述で送信できますが……
回路の方の作業はこれで終わりなので、次はiPhoneの方の設定をします。
・iPhoneのショートカット設定
iPhone側で使うのは「ショートカット」というアプリ。iOS13以降ならプリインストールのはずなので、App storeからわざわざダウンロードしなくても既に入ってるはず。
こちらで好きなように動作を組み合わせて設定しておくと、ボタンを押したりSiriから呼び出すだけでその動作を実行できます。
というわけで、いちいちブラウザを立ち上げてURLを「http://espremo~~~」と入力しなくても、音声でここにアクセスできるようにショートカットを組みます。
使うコマンドはたったの2つ。
「アクションを追加」→「Web」→「URL」を選び、URL部にさっきのマイコンサーバーURL「http://espremo.local/livingon」を設定する。
次に、「Web」→「URLの内容を取得」というアクションを追加する。
これで、iPhoneは設定したURLの中身が何なのかを取得するため、そのURLにアクセスします。それによってliving_on()関数が呼び出されるため、赤外線LEDが発光し、家電を操作することができます。
最後にこのショートカットを「電気つけて」という名前で保存します。
すると、Siriに「電気つけて」と言うと、iPhoneはHomeKitではなくこちらのショートカットの方の呼び出しを行ってくれます(最初の1回はWebへのアクセス許可のための確認が出るかもしれないので、そのときはOKを選択)。
全く同様にして、照明をオフにするためのURLにアクセスするショートカットを「電気消して」という名前で保存しましょう。
照明以外の家電に関しても、リモコンの赤外線信号を解析してマイコンプログラムに追加&それに対応したiPhoneショートカットの追加、という流れで(マイコンのメモリが許す限り)いくらでも機能を増やすことができる。現時点で使用中の我が家のマイコンには、とりあえず上記コードからさらに機能追加して、ファンヒーターの設定温度を上げる/下げる、ヒーター停止という3つの動作も付け足しました。
もちろんこれらは単独で実行させるだけでなく、照明の消灯用URLとヒーターオフ用URLの両方にアクセスするショートカットを組んで、複数の家電を全部オフにするといった使い方もできます。
・まとめ
以上で、「iPhoneに話しかける」→「Siriがショートカットを実行」→「指定したURLにアクセスが行われる」→「アクセスを受けたマイコンが赤外線LEDによる信号送信を実施」という流れが完成しました。
これにより、音声で家電のコントロールができるようになります。
(↑Siriに話しかけて部屋の電気を消している様子をgifで動画にしたんだけど、elchikaに画像投稿したら動いてない?僕の環境だけ?)
今回使った材料は以下の通り。
品目 小計
マイコン:ESP32-WROVER-32 530円
3.3Vレギュレータ:NJU7223F33 50円
タクトスイッチ 2個 20円
フレネルレンズ S9013 40円
LED (赤×1個, 赤外×3個) 80円
XHコネクタ(電源ケーブル接続用) 10円
電気2重層コンデンサ 50円
XHコネクタ(電源ケーブル接続用) 10円
線材、抵抗、コンデンサ等(安すぎて価格算定できないのでざっくり) 50円
ユニバーサル基板 50円
部材と価格(金額は秋月電子等の電子部品通販サイトより。2021年1月5日時点)
合計金額は890円。マイコンにWROVERじゃなくてWROOMの方を使っていれば720円。WROVERを使った理由はたまたま手持ちに余ってたというだけなので、今から材料揃えるなら安いWROOMにしたほうが良いですね。
次にスマートリモコンを作るときは回路に温度センサーを付け足して、Siriに室温を尋ねて答えさせたり、指定の時間が来たらマイコンが自動で照明オン/オフして目覚ましや睡眠を促す機能とかを増やそうかな。
あとは……このリモコンには赤外線信号の学習機能をもたせていないので、もし追加で新しくこのスマートリモコンで制御したい家電を増やしたいということになったら、プログラムの書き換えを行わないといけないのでとても面倒。そのあたりも改良したいな……。
おわり
※この記事は私のブログに掲載した内容をベースとしています。(iPhoneとESP32で作る1000円スマートリモコン)
投稿者の人気記事
-
K.Shj
さんが
2021/02/20
に
編集
をしました。
(メッセージ: 初版)
-
K.Shj
さんが
2021/02/20
に
編集
をしました。
ログインしてコメントを投稿する