#はじめに
フューチャー Advent Calendar 2020の9日目の記事です。
Amazon Echoをただ単にセットアップして、「アレクサ、テレビをつけて」と言ったところでテレビがつくことはありません。これは当たり前の話で一般的なテレビはリモコンで赤外線による操作が必要ですが、アレクサはテレビの情報も知らなければ赤外線を送信する機能もないからです。
今時っぽくスマート家電リモコンを利用してもいいのですが、エンジニアたるもの自分で作成したいじゃないですか。ということで本記事ではリモコンによる赤外線操作のところをM5stickCで補うことで「アレクサ、テレビをつけて」を実現させます。
#M5stickCとは
とある界隈では有名なモジュールですが、改めて説明しますと超小型のArduinoモジュールで液晶画面、ボタン、マイク、Wi-Fi、Bluetooth、赤外線送信、LED、6軸センサ、Grove等々を備えた製品で、これだけ入ってお値段1,800円ぐらいです。お買い得すぎる!
拡張も容易で専用のHATやGrove経由での拡張可能です。
Echo Dotも十分小さいですが、比較するとすごい小さい。
#概要図
わざわざ概要図にするまでもないかもしれませんが、概要はこんな感じです。
#利用するもの
- なんらかのAmazon Echo (アレクサで音声操作するのに利用)
- M5stickC (音声操作を受け、赤外線を送信するのに利用)
- M5STACK IR UNIT (リモコンの赤外線を読み取るのに利用、送信にも利用可)
- スマホ (アレクサアプリの操作に利用)
- 一般的なPC (Arduinoを利用してM5stickCにプログラムを書き込むのに利用)
#準備
最終的にはアレクサで音声を操作し、M5stickCから赤外線を送信することになりますが、その過程でもろもろ準備が必要なのでやっていきます。
Arduino IDEのインストール
Arduino IDEインストール方法 – Windows編 がわかりやすかったです。このページを参考にインストールします。
ライブラリのインストール
今回利用するライブラリを先にインストールしておきます。
Arduino IDEの[スケッチ] - [ライブラリをインクルード] - [ライブラリを管理...] でライブラリマネージャを開き、M5StickC, Espalexa, IRremoteESP8266をインストールします。
##テレビリモコンの赤外線を調べる
テレビに向かって赤外線を送信することになりますが、送信する赤外線のパターンがわかりません。M5stickCに赤外線を送信する機能はありますが、残念ながら受信する機能はありません。
でも安心してください。M5stickCはGroveで容易に拡張できます。M5STACK IR UNITを利用しましょう。Grove、つまり半田付け不要でコネクタを挿すだけで簡単に赤外線を読み取ることができます。ちなみにプログラムを書く必要がありますが、サンプルプログラムを少し変更するだけでいけます。ちなみにM5STACK IR UNITは300円程度で入手できます。こちらも実にお安い!
サンプルプログラムを以下で開き、サンプルの一部をM5stickC用に書き換えてマイコンボードに書き込みます。
[ファイル] - [スケッチ例] - [IRremoteESP8266] - [IRrecvDumpV2]
IR UNITのINに白い線が繋がっており、それがG33に繋がっているので以下を書き換えます。
#const uint16_t kRecvPin = 14;
const uint16_t kRecvPin = 33; // M5STACK IR UNITのINに対応するGroveの番号
/*
* IRremoteESP8266: IRrecvDumpV2 - dump details of IR codes with IRrecv
* An IR detector/demodulator must be connected to the input kRecvPin.
*
* Copyright 2009 Ken Shirriff, http://arcfn.com
* Copyright 2017-2019 David Conran
*
* Example circuit diagram:
* https://github.com/crankyoldgit/IRremoteESP8266/wiki#ir-receiving
*
* Changes:
* Version 1.0 October, 2019
* - Internationalisation (i18n) support.
* - Stop displaying the legacy raw timing info.
* Version 0.5 June, 2019
* - Move A/C description to IRac.cpp.
* Version 0.4 July, 2018
* - Minor improvements and more A/C unit support.
* Version 0.3 November, 2017
* - Support for A/C decoding for some protocols.
* Version 0.2 April, 2017
* - Decode from a copy of the data so we can start capturing faster thus
* reduce the likelihood of miscaptures.
* Based on Ken Shirriff's IrsendDemo Version 0.1 July, 2009,
*/
#include <Arduino.h>
#include <assert.h>
#include <IRrecv.h>
#include <IRremoteESP8266.h>
#include <IRac.h>
#include <IRtext.h>
#include <IRutils.h>
// ==================== start of TUNEABLE PARAMETERS ====================
// An IR detector/demodulator is connected to GPIO pin 14
// e.g. D5 on a NodeMCU board.
// Note: GPIO 16 won't work on the ESP8266 as it does not have interrupts.
const uint16_t kRecvPin = 33; // M5STACK IR UNITのINに対応するGroveの番号
// The Serial connection baud rate.
// i.e. Status message will be sent to the PC at this baud rate.
// Try to avoid slow speeds like 9600, as you will miss messages and
// cause other problems. 115200 (or faster) is recommended.
// NOTE: Make sure you set your Serial Monitor to the same speed.
const uint32_t kBaudRate = 115200;
// As this program is a special purpose capture/decoder, let us use a larger
// than normal buffer so we can handle Air Conditioner remote codes.
const uint16_t kCaptureBufferSize = 1024;
// kTimeout is the Nr. of milli-Seconds of no-more-data before we consider a
// message ended.
// This parameter is an interesting trade-off. The longer the timeout, the more
// complex a message it can capture. e.g. Some device protocols will send
// multiple message packets in quick succession, like Air Conditioner remotes.
// Air Coniditioner protocols often have a considerable gap (20-40+ms) between
// packets.
// The downside of a large timeout value is a lot of less complex protocols
// send multiple messages when the remote's button is held down. The gap between
// them is often also around 20+ms. This can result in the raw data be 2-3+
// times larger than needed as it has captured 2-3+ messages in a single
// capture. Setting a low timeout value can resolve this.
// So, choosing the best kTimeout value for your use particular case is
// quite nuanced. Good luck and happy hunting.
// NOTE: Don't exceed kMaxTimeoutMs. Typically 130ms.
#if DECODE_AC
// Some A/C units have gaps in their protocols of ~40ms. e.g. Kelvinator
// A value this large may swallow repeats of some protocols
const uint8_t kTimeout = 50;
#else // DECODE_AC
// Suits most messages, while not swallowing many repeats.
const uint8_t kTimeout = 15;
#endif // DECODE_AC
// Alternatives:
// const uint8_t kTimeout = 90;
// Suits messages with big gaps like XMP-1 & some aircon units, but can
// accidentally swallow repeated messages in the rawData[] output.
//
// const uint8_t kTimeout = kMaxTimeoutMs;
// This will set it to our currently allowed maximum.
// Values this high are problematic because it is roughly the typical boundary
// where most messages repeat.
// e.g. It will stop decoding a message and start sending it to serial at
// precisely the time when the next message is likely to be transmitted,
// and may miss it.
// Set the smallest sized "UNKNOWN" message packets we actually care about.
// This value helps reduce the false-positive detection rate of IR background
// noise as real messages. The chances of background IR noise getting detected
// as a message increases with the length of the kTimeout value. (See above)
// The downside of setting this message too large is you can miss some valid
// short messages for protocols that this library doesn't yet decode.
//
// Set higher if you get lots of random short UNKNOWN messages when nothing
// should be sending a message.
// Set lower if you are sure your setup is working, but it doesn't see messages
// from your device. (e.g. Other IR remotes work.)
// NOTE: Set this value very high to effectively turn off UNKNOWN detection.
const uint16_t kMinUnknownSize = 12;
// Legacy (No longer supported!)
//
// Change to `true` if you miss/need the old "Raw Timing[]" display.
#define LEGACY_TIMING_INFO false
// ==================== end of TUNEABLE PARAMETERS ====================
// Use turn on the save buffer feature for more complete capture coverage.
IRrecv irrecv(kRecvPin, kCaptureBufferSize, kTimeout, true);
decode_results results; // Somewhere to store the results
// This section of code runs only once at start-up.
void setup() {
#if defined(ESP8266)
Serial.begin(kBaudRate, SERIAL_8N1, SERIAL_TX_ONLY);
#else // ESP8266
Serial.begin(kBaudRate, SERIAL_8N1);
#endif // ESP8266
while (!Serial) // Wait for the serial connection to be establised.
delay(50);
// Perform a low level sanity checks that the compiler performs bit field
// packing as we expect and Endianness is as we expect.
assert(irutils::lowLevelSanityCheck() == 0);
Serial.printf("\n" D_STR_IRRECVDUMP_STARTUP "\n", kRecvPin);
#if DECODE_HASH
// Ignore messages with less than minimum on or off pulses.
irrecv.setUnknownThreshold(kMinUnknownSize);
#endif // DECODE_HASH
irrecv.enableIRIn(); // Start the receiver
}
// The repeating section of the code
void loop() {
// Check if the IR code has been received.
if (irrecv.decode(&results)) {
// Display a crude timestamp.
uint32_t now = millis();
Serial.printf(D_STR_TIMESTAMP " : %06u.%03u\n", now / 1000, now % 1000);
// Check if we got an IR message that was to big for our capture buffer.
if (results.overflow)
Serial.printf(D_WARN_BUFFERFULL "\n", kCaptureBufferSize);
// Display the library version the message was captured with.
Serial.println(D_STR_LIBRARY " : v" _IRREMOTEESP8266_VERSION_ "\n");
// Display the basic output of what we found.
Serial.print(resultToHumanReadableBasic(&results));
// Display any extra A/C info if we have it.
String description = IRAcUtils::resultAcToString(&results);
if (description.length()) Serial.println(D_STR_MESGDESC ": " + description);
yield(); // Feed the WDT as the text output can take a while to print.
#if LEGACY_TIMING_INFO
// Output legacy RAW timing info of the result.
Serial.println(resultToTimingInfo(&results));
yield(); // Feed the WDT (again)
#endif // LEGACY_TIMING_INFO
// Output the results as source code
Serial.println(resultToSourceCode(&results));
Serial.println(); // Blank line between entries
yield(); // Feed the WDT (again)
}
}
M5stickCに書き込めたら
[ツール] - [シリアルモニタ] でシリアルモニタを開き、M5STACK IR UNITに向けてテレビリモコンの電源ボタンを押す。
そうするとシリアルモニタに受信した内容が表示されます。文字化けする場合にはシリアル接続のbpsが合ってないので合わせます。
Timestamp : 000043.913
Library : v2.7.11
Protocol : NEC
Code : 0x2FD48B7 (32 Bits)
uint16_t rawData[71] = {9026, 4510, 554, 580, 550, 578, 550, 578, 552, 580, 550, 580, 550, 580, 550, 1694, 554, 584, 550, 1692, 554, 1694, 554, 1692, 554, 1694, 552, 1692, 554, 1692, 554, 580, 550, 1696, 554, 578, 552, 1692, 554, 578, 550, 580, 550, 1694, 552, 580, 550, 578, 552, 584, 550, 1694, 554, 578, 552, 1692, 554, 1692, 554, 578, 552, 1692, 552, 1692, 554, 1692, 554, 40336, 9028, 2252, 554}; // NEC 2FD48B7
uint32_t address = 0x40;
uint32_t command = 0x12;
uint64_t data = 0x2FD48B7;
Timestamp : 000044.021
Library : v2.7.11
Protocol : NEC (Repeat)
Code : 0xFFFFFFFFFFFFFFFF (0 Bits)
uint16_t rawData[3] = {9028, 2252, 554}; // NEC (Repeat) FFFFFFFFFFFFFFFF
uint64_t data = 0xFFFFFFFFFFFFFFFF;
受信した内容を見ると我が家のテレビではNECの0x2FD48B7を利用すればよいことがわかりました。
##サンプルの確認
作成するスケッチは大雑把に言うと以下の処理が必要になります。
- アレクサの命令を受ける
- 赤外線を送信する
- M5stickCに表示する
それぞれのスケッチ例を確認しておきましょう。
アレクサの命令を受ける(EspalexaColor)スケッチ例
/*
* This is a basic example on how to use Espalexa with RGB color devices.
*/
#ifdef ARDUINO_ARCH_ESP32
#include <WiFi.h>
#else
#include <ESP8266WiFi.h>
#endif
#define ESPALEXA_ASYNC
#include <Espalexa.h>
// prototypes
boolean connectWifi();
//callback function prototype
void colorLightChanged(uint8_t brightness, uint32_t rgb);
// Change this!!
const char* ssid = "...";
const char* password = "wifipassword";
boolean wifiConnected = false;
Espalexa espalexa;
void setup()
{
Serial.begin(115200);
// Initialise wifi connection
wifiConnected = connectWifi();
if(wifiConnected){
espalexa.addDevice("Color Light", colorLightChanged);
espalexa.begin();
} else
{
while (1) {
Serial.println("Cannot connect to WiFi. Please check data and reset the ESP.");
delay(2500);
}
}
}
void loop()
{
espalexa.loop();
delay(1);
}
//the color device callback function has two parameters
void colorLightChanged(uint8_t brightness, uint32_t rgb) {
//do what you need to do here, for example control RGB LED strip
Serial.print("Brightness: ");
Serial.print(brightness);
Serial.print(", Red: ");
Serial.print((rgb >> 16) & 0xFF); //get red component
Serial.print(", Green: ");
Serial.print((rgb >> 8) & 0xFF); //get green
Serial.print(", Blue: ");
Serial.println(rgb & 0xFF); //get blue
}
// connect to wifi – returns true if successful or false if not
boolean connectWifi(){
boolean state = true;
int i = 0;
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
Serial.println("");
Serial.println("Connecting to WiFi");
// Wait for connection
Serial.print("Connecting...");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
if (i > 40){
state = false; break;
}
i++;
}
Serial.println("");
if (state){
Serial.print("Connected to ");
Serial.println(ssid);
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
}
else {
Serial.println("Connection failed.");
}
return state;
}
赤外線を送信する(IRsendDemo)スケッチ例
/* IRremoteESP8266: IRsendDemo - demonstrates sending IR codes with IRsend.
*
* Version 1.1 January, 2019
* Based on Ken Shirriff's IrsendDemo Version 0.1 July, 2009,
* Copyright 2009 Ken Shirriff, http://arcfn.com
*
* An IR LED circuit *MUST* be connected to the ESP8266 on a pin
* as specified by kIrLed below.
*
* TL;DR: The IR LED needs to be driven by a transistor for a good result.
*
* Suggested circuit:
* https://github.com/crankyoldgit/IRremoteESP8266/wiki#ir-sending
*
* Common mistakes & tips:
* * Don't just connect the IR LED directly to the pin, it won't
* have enough current to drive the IR LED effectively.
* * Make sure you have the IR LED polarity correct.
* See: https://learn.sparkfun.com/tutorials/polarity/diode-and-led-polarity
* * Typical digital camera/phones can be used to see if the IR LED is flashed.
* Replace the IR LED with a normal LED if you don't have a digital camera
* when debugging.
* * Avoid using the following pins unless you really know what you are doing:
* * Pin 0/D3: Can interfere with the boot/program mode & support circuits.
* * Pin 1/TX/TXD0: Any serial transmissions from the ESP8266 will interfere.
* * Pin 3/RX/RXD0: Any serial transmissions to the ESP8266 will interfere.
* * ESP-01 modules are tricky. We suggest you use a module with more GPIOs
* for your first time. e.g. ESP-12 etc.
*/
#include <Arduino.h>
#include <IRremoteESP8266.h>
#include <IRsend.h>
const uint16_t kIrLed = 4; // ESP8266 GPIO pin to use. Recommended: 4 (D2).
IRsend irsend(kIrLed); // Set the GPIO to be used to sending the message.
// Example of data captured by IRrecvDumpV2.ino
uint16_t rawData[67] = {9000, 4500, 650, 550, 650, 1650, 600, 550, 650, 550,
600, 1650, 650, 550, 600, 1650, 650, 1650, 650, 1650,
600, 550, 650, 1650, 650, 1650, 650, 550, 600, 1650,
650, 1650, 650, 550, 650, 550, 650, 1650, 650, 550,
650, 550, 650, 550, 600, 550, 650, 550, 650, 550,
650, 1650, 600, 550, 650, 1650, 650, 1650, 650, 1650,
650, 1650, 650, 1650, 650, 1650, 600};
// Example Samsung A/C state captured from IRrecvDumpV2.ino
uint8_t samsungState[kSamsungAcStateLength] = {
0x02, 0x92, 0x0F, 0x00, 0x00, 0x00, 0xF0,
0x01, 0xE2, 0xFE, 0x71, 0x40, 0x11, 0xF0};
void setup() {
irsend.begin();
#if ESP8266
Serial.begin(115200, SERIAL_8N1, SERIAL_TX_ONLY);
#else // ESP8266
Serial.begin(115200, SERIAL_8N1);
#endif // ESP8266
}
void loop() {
Serial.println("NEC");
irsend.sendNEC(0x00FFE01FUL);
delay(2000);
Serial.println("Sony");
irsend.sendSony(0xa90, 12, 2); // 12 bits & 2 repeats
delay(2000);
Serial.println("a rawData capture from IRrecvDumpV2");
irsend.sendRaw(rawData, 67, 38); // Send a raw data capture at 38kHz.
delay(2000);
Serial.println("a Samsung A/C state from IRrecvDumpV2");
irsend.sendSamsungAC(samsungState);
delay(2000);
}
M5stickCに表示する(HelloWorld)スケッチ例
#include <M5StickC.h>
// the setup routine runs once when M5StickC starts up
void setup(){
// Initialize the M5StickC object
M5.begin();
// LCD display
M5.Lcd.print("Hello World");
}
// the loop routine runs over and over again forever
void loop() {
}
#実装
おまけにM5stickCのボタン押すことでリモコンになるようにします。
高々100行程度のプログラムなのでマジックナンバーとかDRY原則とかは気にしない。
なお、Espalexaのスケッチ例ではコンパイル時にエラーが発生するのでM5StickCをAlexa連携デバイス化するを参考にし以下を削除する必要があります。また、今回の実装において大変参考にさせていただきました。ありがとうございます。
#define ESPALEXA_ASYNC
できあがったスケッチは以下です。
/*
サンプルソースを切り貼りして作成したアレクサの音声でテレビの電源をつける/消す
*/
#ifdef ARDUINO_ARCH_ESP32
#include <WiFi.h>
#else
#include <ESP8266WiFi.h>
#endif
#include <Espalexa.h>
#include <IRremoteESP8266.h>
#include <IRsend.h>
#include <M5StickC.h>
// prototypes
boolean connectWifi();
//callback function prototype
void tvChanged(EspalexaDevice* dev);
// Change this!!
const char* ssid = "YOUR SSID";
const char* password = "YOUR PASSWORD";
boolean wifiConnected = false;
const uint16_t kIrLed = 32;
IRsend irsend(kIrLed); // Set the GPIO to be used to sending the message.
Espalexa espalexa;
#define LED 10 //M5StickCに内蔵されているLEDを使う
void setup()
{
Serial.begin(115200);
irsend.begin();
pinMode(LED, OUTPUT);
digitalWrite(LED, HIGH);
// Initialise wifi connection
wifiConnected = connectWifi();
if (wifiConnected) {
espalexa.addDevice("tvonoff", tvChanged);
espalexa.begin();
} else
{
while (1) {
Serial.println("Cannot connect to WiFi. Please check data and reset the ESP.");
delay(2500);
}
}
M5.begin();
M5.Lcd.setRotation(1);
M5.Lcd.setTextFont(4);
M5.Lcd.fillScreen(BLACK);
M5.Lcd.setCursor(0, 0);
M5.Lcd.printf("Connected to WiFi");
}
void loop()
{
espalexa.loop();
M5.update();
// M5stickCのAボタン押した際にもテレビに赤外線送信
if ( M5.BtnA.wasReleased() ) {
irsend.sendNEC(0x2FD48B7);
}
delay(1);
}
void tvChanged(EspalexaDevice *d)
{
// LED点灯
digitalWrite(GPIO_NUM_10, LOW);
// テレビに赤外線送信
irsend.sendNEC(0x2FD48B7);
delay(500);
// LED消灯
digitalWrite(GPIO_NUM_10, HIGH);
}
// connect to wifi – returns true if successful or false if not
boolean connectWifi() {
boolean state = true;
int i = 0;
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
Serial.println("");
Serial.println("Connecting to WiFi");
// Wait for connection
Serial.print("Connecting...");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
if (i > 40) {
state = false; break;
}
i++;
}
Serial.println("");
if (state) {
Serial.print("Connected to ");
Serial.println(ssid);
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
digitalWrite(LED, LOW);
delay(3000);
digitalWrite(LED, HIGH);
}
else {
Serial.println("Connection failed.");
for (int i = 0; i < 5; i++) {
digitalWrite(LED, LOW);
delay(500);
digitalWrite(LED, HIGH);
delay(500);
}
}
return state;
}
#アレクサの設定
Amazon Echoを再起動し、スマホのアレクサアプリでM5stickCに接続します。
[デバイス] - [+マーク] - [デバイスを追加] - [その他] - [デバイスを検出]
でデバイスを検出します。
デバイスが文字化けして見つかる場合もありますが、その場合には名前を編集して修正しましょう。
赤外線がテレビに届くようにM5stickCを設置し、「アレクサ、テレビをつけて」と言ってみてテレビが無事につけば完成です。設置にあたりM5stickCのAボタンを押すことで赤外線を送信するようにしているので、これを利用すると便利です。
ちなみにテレビのリモコンは電源ONでも電源OFFでも同じ赤外線のパターンが送信されます。ですのでテレビがついている際に「アレクサ、テレビをつけて」と言うと消えます。
#ハマりどころ
スマホアプリのアレクサでデバイスを検出する際にM5stickCが検出できずドハマりしました。
M5stickCにプログラムを書き込み、スマホアプリのアレクサでデバイスを検出できると思って試しましたができず、M5stickCを再起動してもダメ、スマホを再起動してもダメ、利用するライブラリのバージョンを変更してみてもダメで諦めかけましたが、Echoを再起動することで問題が解消しました。理由は今もわかっていませんが、どうやら M5stickCを先に起動し、その後Echoを起動する必要がある ようです。スマホアプリのアレクサとM5stickCの接続だと思い込んでましたが違うらしい。
全行程で一番時間を費やしたのはこの部分でした。
※最新のEchoだと大丈夫なのかもしれませんが、私が利用したのは第1世代のEcho Dotです。
#さいごに
Arduinoの取り扱いが初めてでしたが、ちょっとしたプログラムであればサンプルを真似て容易に目的のプログラムを作成することができました。家族に小さい子供がいるとテレビのリモコンがしばしば行方不明になります。主電源でOFFにすると録画が出来ず困りますがこれでもう安心。寝る時や出かける時にテレビが消せなくてイライラすることもありましたが、今ではアレクサに頼めば消してくれるので我が家のQOLが向上しました。
#参考にさせていただいたページ
Arduino IDEインストール方法 – Windows編
M5StickCをAlexa連携デバイス化する
M5StickCで赤外線リモコンを作ろう