M5Stack

M5Stack Atomで赤外線リモコンを作る(4)学習リモコン化

M5Stack Atomシリーズを使って、赤外線リモコン信号を受信する方法と送信する方法を解説してきました。

この2つのプログラムを合体させて、受信した信号を送信ができるようにしてみます。

受信した信号データを送信側に渡す方法

何を受け渡すか

受信編のプログラムは、リモコン信号の解析結果をシリアルコンソールに文字列として出力していました。

出力されたこの信号データを送信プログラムのソースコードに手動でコピペして使っていました。このコピペ作業をプログラム上で行えば学習リモコンが作れます。

受け渡すデータは、2通り考えられます。

  • uint16_t rawData[]を受け渡す
  • プロトコルに応じたデータを受け渡す(上記の例ではNECuint64_t data)

rawData[ ]の場合は、次のような利点と欠点があります。

  • 利点:リモコン信号がどんなプロトコルでもデータを受け渡せる。
  • 欠点:受け渡すデータ量が多くなる。(上の例では67*2byte=134byte)

プロトコルに応じたデータを受け渡す場合は、これと逆の利点と欠点があります。

  • 欠点:プロトコルに応じて送信関数を使い分ける必要があり、送信関数がないプロトコルには対応できない。
  • 利点:受け渡すデータ量が少ない。

学習リモコンの用途を考えると、どんなリモコンの信号を学習しなければいけないか事前にはわからないので、rawData[]を受け渡す方法が適していると言えます。

受け渡すデータの作成

受信プログラムはrawData[ ]を文字列として出力できているので、内部にそのデータを持っているはずです。どうやって文字列を生成しているかをライブラリのソースコードを読んで調べれば、受け渡すデータの取り出し方がわかるはずです。

受信プログラムの中で文字列を出力しているのはこの部分です。

IRremoteESP8266ライブラリで定義されている resultToSourceCode() を調べればいいわけです。この関数の定義はgithubのここにあるIRutils.cppに記載されています。resultToSourceCode()の中でrawData[]を出力しいる部分は次のようなコードです。

想像していたよりも複雑です。元データは results->rawbuf[]に入っていて、その値をkRawTick倍した値を出力しています。ただしkRawTick倍した時にuint16_t型の最大値UINT16_MAXを超えた場合は、その部分を切り出して別の配列要素としています。そのコードが214〜221行目です。rawData[]をデータとして取り出して送信側のプログラムに渡す場合、これと同じ処理を行わなければなりません。

214〜221行目のコードをそのままコピペして使えば動くのですが、IRremoteESP8266がバージョンアップした時、この部分のロジックが変わる可能性があります。できればコピペは避けたいところです。何か便利な関数がないかなと思いながらIRutils.cppのコードを眺めていたら、まさにその関数がありました。uint16_t* resultToRawArray()がそれです。

この関数は、rawData[]配列用のメモリ領域を自分で確保してくれるので便利です。でも関数の戻り値は配列へのポインターだけなんで、要素が何個あるかわかりません。困ったなと思ってソースコードをよく見たら、357行目にgetCorrectedRawLength(decode)という関数が使われていて、これが配列要素数を計算してくれていることがわかりました。

ここまでの調査結果を踏まえると、リモコン信号を受信して、rawData[]配列を作成するコードの主要部分はこうなります。

if (irrecv.decode(&results)) {
  rawData = resultToRawArray(&results);
  dataLength = getCorrectedRawLength(&results);
}

 

受信と送信のボタン操作

ここまでの検討で、

  • リモコン信号の受信
  • 信号データの受け渡し
  • リモコン信号の送信

ができるようになりました。次に考えることは、受信と送信のユーザインタフェースをどうするかです。

Atomには、中央に大きなボタンと側面に小さなボタンが付いています。しかし、側面のボタンはリセットボタンなのでプログラムから使うのは難しそうですし、できたとしても誤操作してうっかりリセットしてしまう可能性が高いです。そこで大きなボタンのみを使って、シングルクリック、ダブルクリック、長押しなどで受信と送信を使い分けるのが良さそうです。

ボタンの長押しで信号を受信し、シングルクリックで送信するプログラムにしてみましょう。単純に考えると

if (Btn.シングルクリック()) then {
  送信処理
} else if (Btn.長押し() {
  受信処理
  信号データを取り出す
}

という感じでコードを書きたいのですが、Atomのボタンライブラリに用意されている関数はこうなっています。

長押しとかダブルクリックとかありませんね。ライブラリに用意されている関数を組み合わせて実現することになります。誰かが既に実装済みかもしれないと思い、ググッてみましたが見つけられませんでした。諦めて自作することにして、いろいろ試してみた結果、こんなコードでなんとなく動くようになりました。ボタンライブラリが想定している使い方とは異なっていてバグを含んでいるかもしれませんが、とりあえず動いているので良しとします。

void loop() {
  M5.update();
  if (M5.Btn.wasReleased()) {// クリック
    送信処理
  } else  if (M5.Btn.pressedFor(300)) { // 長押し
    受信待ちを有効化
    while (! M5.Btn.wasReleased()) { // ボタンが離されるまで待つ
      delay(50);
      M5.update();
    }
  } else {
    if (受信していたら) {
      信号データを取り出す
    }
  }
}

「信号データを取り出す」が長押し処理の中ではなく最後のelse文の中で行われているのは、受信処理が非同期で行われるためです。

学習リモコンプログラムの全体像

ここまでの検討結果を総合して、学習リモコンプログラムを書いてみました。

#include <M5Atom.h>
#include <IRrecv.h>
#include <IRremoteESP8266.h>
#include <IRac.h>
#include <IRtext.h>
#include <IRutils.h>

// Select IR_LED in M5Atom or M5Stack IR Unit
#define IR_LED 12  // IR LED in M5Atom
//#define IR_LED 26  // IR LED in the M5Stack IR Unit

// ============ IRremoteESP8266 TUNEABLE PARAMETERS ================
const uint16_t kRecvPin = 32;
const uint32_t kBaudRate = 115200;
const uint16_t kCaptureBufferSize = 1024;
#if DECODE_AC
const uint8_t kTimeout = 50;
#else   // DECODE_AC
const uint8_t kTimeout = 15;
#endif  // DECODE_AC
const uint16_t kMinUnknownSize = 12;
#define LEGACY_TIMING_INFO false
// =================================================================

IRrecv irrecv(kRecvPin, kCaptureBufferSize, kTimeout, true);
decode_results results;  // Somewhere to store the results
IRsend irsend(IR_LED);

uint16_t *rawData;        // IR message container
uint16_t dataLength = 0;  // IR message length

void setup() {
  M5.begin(true, false, false); // SerialEnable = true, I2CEnable = false, DisplayEnable = false);
  Serial.printf("\n\n### IR Remote Controller ###\n");
  Serial.printf("IR Sensor Pin Number =%d \n", kRecvPin);
  irrecv.setUnknownThreshold(kMinUnknownSize);
  irsend.begin();
}

void loop() {
  M5.update();
  if (M5.Btn.wasReleased()) {
    Serial.print("Button wasReleased(): ");
    if (dataLength > 0) {
      Serial.printf("send rawData[%d]\n", dataLength);
      irsend.sendRaw(rawData, dataLength, 38);
    } else {
      Serial.println("rawData[] is empty. Skip sending");
    }
  } else if (M5.Btn.pressedFor(300)) { // 長押しなら赤外線信号受信モードに遷移
    irrecv.enableIRIn();
    Serial.println("M5.Btn.pressedFor(300): waiting for IR signal");
    while (! M5.Btn.wasReleased()) { // ボタンが離されるまで待つ
      delay(50);
      M5.update();
    }
  } else { // 赤外線信号を受信していたら、信号を配列に格納
    if (irrecv.decode(&results)) {
      irrecv.disableIRIn(); // 赤外線受信モードを解除
      if (results.overflow)
        Serial.printf(D_WARN_BUFFERFULL "\n", kCaptureBufferSize);
      rawData = resultToRawArray(&results);         // 信号データを抽出
      dataLength = getCorrectedRawLength(&results); // 配列長を抽出
      Serial.printf("rawData[%d] : ", dataLength);
      for (int i = 0; i < dataLength; i++) {
        Serial.printf("%d, ", rawData[i]);
      }
      Serial.println();
    }
  }
}

このプログラム実行すると

  • ボタン長押しで赤外線信号を学習
  • ボタンクリックで学習した赤外線信号を送信

が動作します。