オムツを注文しながら照明を消す方法

新居の照明が赤外線リモコン式で,寝るときにロフトに上がって布団に入ってからリモコンを忘れたことに気が付いてキレたので,Dash Buttonで操作できるようにした。

この記事はなに

電子工作でよく用いられるWi-Fiモジュール ESP8266(ESP-WROOM-02) とAmazon Dash Buttonを使って,赤外線リモコンで操作可能な照明を操作します。 ESP8266側でDash Buttonのブロードキャストを検出し,赤外線LEDを駆動します。

In this article, I am using ESP8266 (ESP-WROOM-02) Wi-Fi module and Amazon Dash Button to control IR LED. ESP8266 observes broadcasts sent by Dash Button and send 38kHz IR signal.

github.com

使うもの

  • ESP-WROOM-02
  • Amazon Dash Button (moony)
  • 抵抗やFETなどの電子部品
  • 5V 2AのACアダプタ
  • USBシリアル変換モジュール(3.3V対応のもの)

電子部品が揃っている人はESP-WROOM-02Amazon Dash Buttonで1,000円強で作れます。 家に転がってるRaspberry Piで作ろうと思ったけど,寝るときに照明消すためだけにLinux動かすことのアホらしさに気がついた。

つくりかた

Amazon Dash Buttonのセットアップ

Wi-Fiの設定が終わったら商品を選ばずにキャンセルすることで,何も注文しないDash Buttonになります。でもおねしょが心配な人は商品が届くようにしておいたほうが良いと思います。

ルータの設定

Wi-Fiの接続情報を見てDash ButtonのMACアドレスを調べます。機種によってはデフォルトの設定だとSnoopingなどによりESP8266にブロードキャストが届かない可能性があるので,確認してください。また,Amazon Dash Buttonは商品を注文しなくてもAmazon.comと通信するので,これを遮断するのも良いかもしれません。

ハードウェアの作成

ESP8266電源のGNDの他にEN,RST,IO0,IO2,IO15の値を適切に設定する必要があります。以下に回路図の例を示しますが,詳細はデータシートを確認してください。なお,赤外線LEDのドライバ回路は以下の記事で紹介されている の作例を(勝手に)使わせていただきました。

赤外線LEDドライブ回路の決定版 | 電脳伝説

赤外線LEDの光を遠くまで届けるためには大きな電流が必要です。定格では100mAですが,パルスで駆動するときは500mAとか1A流しても大丈夫な場合が多いので,データシートや雰囲気で電流を調整してください(LEDの前に挿入されている抵抗の値を変える)。抵抗の計算方法は,秋月電子のLEDの通販ページに解説が掲載されているため,参考にしてください。赤外線LEDは指向性が強いので,向きを変えやすいように足を切らずにソケットに挿しています。また,運用時には受光モジュールやシリアル通信モジュールは不要になるため,こちらも取り外しできるようにしました。

この回路ではESP8266を書き込みモードにしたときにLEDが点灯しっぱなしになって壊れることがあります。LEDを着脱式にしたり,スイッチやリレーなどを設置したり,何らかの対策をしてください。ぼくは外すのを忘れて2つ壊しました。

回路図
回路図
回路図に書き忘れましたが,受光モジュールのVoutにはデータシートに記載されているRCフィルタを実装しています(なくても動くと思う)。

ソフトウェアの作成

ブロードキャストの検出

ESP8266 Arduinoライブラリで使用されているTCP/IPスタックlwIPのコールバック関数を自作のものに置き換えることで,Dash Buttonのブロードキャストを検出します*1Junxiao Shi氏の作例を参考にさせていただきました。

今回は,送信元がDash Buttonであるイーサネットフレームを検出したら赤外線信号を発します。また,1回操作した後5秒間(記号定数INTERVAL)はブロードキャストを検出しても無視します。

err_t netifInput(pbuf* p, netif* inp)
{
  if (p->len < sizeof(eth_hdr)) {
    return (*originalNetifInput)(p, inp);
  }

  auto eth = reinterpret_cast<const eth_hdr*>(reinterpret_cast<const uint8_t *>(p->payload));
  for (int i = 0; i < 6; i++) {
    if (eth->src.addr[i] != dashButtonAddr[i]) {
      break;
    }
    if (i == 5 && (millis() > lastPushed + INTERVAL || millis() < lastPushed)) {
      Serial.println("\nSending...");
      led.sendPattern(irPattern, irPatternLength);
      Serial.println("Finish");
      lastPushed = millis();
    }
  }
  
  return (*originalNetifInput)(p, inp);
}


void innoculateCallbackFn()
{
  assert(netif_list != nullptr);
  assert(netif_list->next == nullptr);
  originalNetifInput = netif_list->input;
  netif_list->input = netifInput;
}


void setup() {
  innoculateCallbackFn();
}

赤外線パターンの取得

赤外線受信モジュールとしてGP1UXC41QSを使用しています。このモジュールは赤外線の信号がオンのときは0V付近を,オフのときはVcc-0.5V(今回の場合Vcc 3.3Vなので約2.8V)を出力します(赤外線LEDの点灯・消灯とは異なり,意味上のオン・オフですが,これについては後に述べます)。この値をディジタル値として取得して,0の時間と1の時間をマイクロ秒単位で記録し,シリアルで送信します。PCでこれを受信して,コードに配列として埋め込んで送信します。

int ESP8266DashIR::IRLED::readPattern(unsigned int *data, int bufferSize) {
  unsigned int startTime = 0;
  unsigned int currentTime = 0;
  unsigned int receivedVal = 0;
  int bufferIndex = 0;
  
  while(digitalRead(receiverPin) == 1) {
    delay(0);
  }

  startTime = micros();
  currentTime = startTime;
  while(currentTime - startTime < IR_RECEIVER_TIMEOUT) {
    currentTime = micros();
    if (digitalRead(receiverPin) != receivedVal) {
      data[bufferIndex++] = currentTime - startTime;
      startTime = currentTime;
      receivedVal = (~receivedVal) & 0x01;

      if (bufferIndex >= bufferSize) {
        return -1;
      }
    }
  }

  return bufferIndex;
}

赤外線LEDの駆動

赤外線受信モジュールが出力するオン・オフは,赤外線LEDの点灯・消灯とは異なり,意味上のオン・オフでした。これは,赤外線の信号に「オンのとき常に点灯しているわけではない」という決まりがあるためです。具体的には,26μsを1単位としてオンのときは最初の8.7μsだけ点灯します*2。詳しく知りたい方は「38kHz変調」などのキーワードで検索をしてください。

今回の実装では,オン・オフの時間を記録した配列の偶数番目がオフ,奇数番目がオンとして,オンの場合9μsの間LEDを点灯し,13μs消灯します。オフの場合は,はじめの9μsも消灯しています。26μsには4μsほど足りませんが,whileの条件判定やdigitalWriteなどの実行時間があるため,実際にはだいたい26μsになります。

void ESP8266DashIR::IRLED::sendPattern(const unsigned int *pattern, const unsigned int patternLength) {
  unsigned int startTime = 0;
  int pinState = LOW;

  for (int patternIndex = 0; patternIndex < patternLength; patternIndex++) {
    startTime = micros();
    while (micros() - startTime < pattern[patternIndex]) {
      digitalWrite(ledPin, patternIndex & 0x01 ? LOW : HIGH);
      delayMicroseconds(9);
      digitalWrite(ledPin, LOW);
      delayMicroseconds(13);
    }
  }
  digitalWrite(ledPin, LOW);
}

動かしてみる

これらを実装した以下のスケッチを使用します。Arduino core for ESP8266をインストールし,ボードを"Generic ESP8266 Module"に,lwIPを"1.4 Higher Bandwidth"に変更します。 ESP8266DashIR.inoの定数や変数は以下のように設定してください。

  • LED_OUT : LEDをつないでいるピン
  • IR_RECEIVER : 受光モジュールをつないでいるピン
  • WIFI_SSID : Wi-Fi SSID
  • WIFI_PASSWORD : Wi-Fi Password
  • irPatternLength : 赤外線信号パターンの長さ
  • irPattern : 赤外線パターンの配列(マイクロ秒単位)
  • dashButtonAddr : Dash ButtonのMACアドレス

github.com

このスケッチはシリアル通信で'r'を送信すると受信モードになり,赤外線リモコンが発する信号を受信するとシリアルコンソールに出力します。 これをメモして,irPatternLengthとirPatternを書き換えます。 シリアルで's'を送ると記録されたパターンを送信するので,機器が正しく操作できることを確認して下さい。 赤外線LEDは指向性が強いので,受光部の位置が分かりづらい照明機器などはいろいろな設置場所を試してうまくいく位置をみつけてください。

あとはDash Buttonを押せばパターンが送信されるので,Dash Buttonを常に枕元に置いておけばリモコンを忘れても静かに眠ることができます。

おわりに

蓋閉まらなくてキレそう f:id:suna_pan:20180326223745j:plain

*1:コールバック関数を付け替えるタイミングを変えれば,他の通信を行うアプリケーションと同居できそうな気がします。

*2:さらに,ディジタル値0/1のそれぞれのデータの送信も,オン・オフを規定の時間で操作することで実現されています。受信したデータを見ると,オン・オフの時間が規則的に並んでいることがわかりますね。