M5Stack 赤外線リモコンデコーダ

# M5Stack用赤外線リモコンデコーダ

かなり出遅れた話題かもしれませんが、M5Stack　Arduinoの赤外線リモコンデコーダを紹介させていただきます。あるアプリで赤外線リモコンを使いたかったのですが、M5Stackでうまく使えるものが見つけられなかったため、諸々参考させていただき自作しました。
NECフォーマット、家電協フォーマット、SONYフォーマット、ユニデンフォーマットなどがデコードできます。正確なデコードよりもコードを区別できることを主眼にしていますので、若干トリッキーなデコード方法になっています。
赤外線リモコンのコマンドはフォーマットによって長さが違うのでデコード結果はString形式で得られるようにしました。

##赤外線センサ接続方法
M5Stackの天面のコネクタに赤外線センサを接続します。+3.3V,GND,GPIO21を使用しています。
![M5Stack_IRreceiver.jpg](https://qiita-image-store.s3.amazonaws.com/0/331423/553f167a-e613-bc34-3882-db6211351723.jpeg)

##ソースコード
### M5StackIRdecode.h

``` Arduino:M5StackIRdecode.h
#ifndef _M5StackIRdecpde_h
#define _M5StackIRdecode_h

#include <M5Stack.h>
#include "driver/rmt.h"
#include "driver/periph_ctrl.h"
#include "soc/rmt_reg.h"

#define NEC_TIC           560
#define NEC_LEADER_HIGH   NEC_TIC*16
#define NEC_LEADER_LOW    NEC_TIC*8
#define NEC_LEADER_LOWR   NEC_TIC*4
#define NEC_MARGIN        100
#define IGNORE_REPEAT

#define KDN_TIC           400
#define KDN_LEADER_HIGH   KDN_TIC*8
#define KDN_LEADER_LOW    KDN_TIC*4
#define KDN_MARGIN        100

#define SNY_TIC           600
#define SNY_LEADER_HIGH   SNY_TIC*4
#define SNY_MARGIN        100

#define UNI_TIC           560
#define UNI_LEADER_HIGH   UNI_TIC*4
#define UNI_LEADER_LOW    UNI_TIC*2
#define UNI_MARGIN        100

#define MB_TIC           300
#define MB_LEADER_HIGH   MB_TIC
#define MB_LEADER_LOW    MB_TIC*2
#define MB_MARGIN        100

#define TIC_MAX          (NEC_LEADER_HIGH*2)
#define TIC_MIN          (MB_TIC/2)

class M5StackIRdecode{
  public:
    M5StackIRdecode();
    boolean parseData(rmt_item32_t *item, size_t rxSize);
    String _ircode;
  private:
    String checkLeader(rmt_item32_t item);
    int16_t decodeBit(int16_t d0,int16_t d1, boolean dm);
    String rmttmp;
};

#endif
```
###M5StackIRdecode.cpp

``` Arduino:M5StackIRdecode.cpp
#include "M5StackIRdecode.h"

M5StackIRdecode::M5StackIRdecode(){};

String M5StackIRdecode::checkLeader(rmt_item32_t item){

  if ((item.level0 != 0) || (item.level1 != 1)) {
    return "";
  }
  if( item.duration0 > (NEC_LEADER_HIGH-NEC_MARGIN)){
    if( item.duration1> (NEC_LEADER_LOW-NEC_MARGIN)){
      return "N"; //NEC
    } else if( item.duration1> (NEC_LEADER_LOWR-NEC_MARGIN)){
#ifdef IGNORE_REPEAT
      return "";
#else
      return "R";
#endif      
    } else {
      return "";
    }   
  } else if((item.duration0 > (KDN_LEADER_HIGH-KDN_MARGIN))
         && (item.duration1 > (KDN_LEADER_LOW-KDN_MARGIN ))){
      return "K"; // KADEN
  } else if( item.duration0 > (SNY_LEADER_HIGH-SNY_MARGIN)){
      return "S"; // SONY
  } else if((item.duration0 > (UNI_LEADER_HIGH-UNI_MARGIN))
         && (item.duration1 > (UNI_LEADER_LOW-UNI_MARGIN ))){
      return "U"; //UNIDEN
  } else if(item.duration0 > (NEC_TIC-NEC_MARGIN)){ // Ignore Victor repeat
      return "" ;
  }else if((item.duration0 > (MB_LEADER_HIGH-MB_MARGIN))
         && (item.duration1 > (MB_LEADER_LOW-MB_MARGIN ))){
      return "M"; // Mitsubishi
  }
  return "";
 }

int16_t M5StackIRdecode::decodeBit(int16_t d0,int16_t d1, boolean dm){
  int16_t tl,ts;

  if((d0<TIC_MIN)||(d1<TIC_MIN)||(d0>TIC_MAX)){
    return -1;
  }

  ts=(d0>d1) ? d1 : d0; 
  tl=(d0>d1) ? d0 : d1; 
  if(dm){ // mitsubishi
    return (tl-ts)>(ts*4) ? 1 : 0;
  }else{ //  nec kaden sony
    return (tl-ts)>(ts/2) ? 1 : 0;
  }
}

boolean M5StackIRdecode::parseData(rmt_item32_t *item, size_t rxSize) {
  int16_t index = 0;
  int16_t dbit,i;
  uint8_t dbyte;
  boolean dmode;
  
  rmttmp=checkLeader(item[0]);
  if(rmttmp.equals("")){
    return false;
  }
  if(rmttmp.equals("M")){
     dmode=true;
     index=0;
  }else{
    dmode=false;    
    index=1;
  }
  for(i=0, dbyte=0; item[index].duration1>0;index++){
      dbit=decodeBit(item[index].duration0,item[index].duration1,dmode);
      if(dbit<0){
       return false;
      }
      dbyte=dbyte<<1|dbit;
      i++;
      if(i>7){
        if(dbyte<0x10){
          rmttmp.concat('0');
        }
        rmttmp.concat(String(dbyte,HEX));
        dbyte=0;
        i=0;
      }
  }
  if(index==1){ // no data 
    return false;
  }
  if(i>2){ // for SONY format
      rmttmp.concat(String(dbyte,HEX));
  }
  _ircode=rmttmp;
  return true;
}
```

##サンプルアプリ

###rmt_sample1.ino

``` Arduino:rmt_sample1.ino
#include <M5Stack.h>

#include "freertos/task.h"
#include "time.h"

#include "M5stackIRdecode.h"

const rmt_channel_t channel = RMT_CHANNEL_0;
const gpio_num_t irPin = GPIO_NUM_21;
RingbufHandle_t buffer = NULL;

M5StackIRdecode  irDec;

const String up    =  "N00ff18e7";
const String down  =  "N00ff4ab5";
const String left  =  "N00ff10ef";
const String right =  "N00ff5aa5";
const String home  =  "N00ff38c7";

String ircode = "";
int16_t x=160, y=120;
uint16_t t=0;

void init_rmt(rmt_channel_t channel, gpio_num_t irPin){
  rmt_config_t rmtConfig;
 
  rmtConfig.rmt_mode = RMT_MODE_RX;
  rmtConfig.channel = channel;
  rmtConfig.clk_div = 80;
  rmtConfig.gpio_num = irPin;
  rmtConfig.mem_block_num = 1;
 
  rmtConfig.rx_config.filter_en = 1;
  rmtConfig.rx_config.filter_ticks_thresh = 255;
  rmtConfig.rx_config.idle_threshold = 10000;
 
 
  rmt_config(&rmtConfig);
  rmt_driver_install(rmtConfig.channel, 2048, 0);
 
  rmt_get_ringbuf_handle(channel, &buffer);
  rmt_rx_start(channel, 1);
}

void setup() {
  // put your setup code here, to run once:
  M5.begin(true, false, true);
  M5.Lcd.clear(BLACK);
  M5.Lcd.setTextSize(2);
  M5.Lcd.fillCircle(x, y, 10, WHITE); 

  init_rmt(channel, irPin);
  xTaskCreate(rmt_task, "rmt_task", 4096, NULL, 10, NULL);

  
}
 
void loop() {
  // put your main code here, to run repeatedly:
  
  M5.update();
  M5.Lcd.setCursor(0, 0);
  M5.Lcd.printf("%05d",t++);
  
  if(ircode.length()){
    Serial.println(ircode);
    M5.Lcd.fillCircle(x, y, 10, BLACK);     
    if(ircode.equals(up)){
      y=(y>10) ? y-10 : y;
    } else if(ircode.equals(down)){
      y=(y<220) ? y+10 : y;
    } else if(ircode.equals(left)){
      x=(x>10) ? x-10 : x;
    } else if(ircode.equals(right)){
      x=(x<310) ? x+10 : x;
    } else if(ircode.equals(home)){
      x=160;
      y=120;
      t=0;
    }
    M5.Lcd.fillCircle(x, y, 10, WHITE);       
    ircode="";
  }
}

void rmt_task(void *arg){
  size_t rxSize = 0;
  rmt_item32_t * item;
  while(1){
    item = (rmt_item32_t *)xRingbufferReceive(buffer, &rxSize, 10000);
    if (item) {
      if((irDec.parseData(item, rxSize))&&ircode==""){
        ircode=irDec._ircode;
      }
      
      vRingbufferReturnItem(buffer, (void*) item);
    }
  }
  
}
```

サンプルアプリは、赤外線リモコンでLCDに表示された白丸を上下左右に移動させるというものです。
メインのタスクとは別タスクで赤外線受信タスクを動作させています。
画面左上の数字はメインタスクで動作しているループカウンタです。赤外線受信中でもカウンタは止まらないことが分かると思います。テストでは [こちらのリモコン](https://www.akiba-led.jp/product/1489)を使用しています。
コードはNECタイプです。
![IMG_20181228_114523.jpg](https://qiita-image-store.s3.amazonaws.com/0/331423/d8679175-83d9-dcc2-6a01-6d1e756949e4.jpeg)

| ボタン |コード | 動作 |
|:-:|:-:|:-:|
| 1 |N00ff18e7|↑ |
| 8 |N00ff4ab5|↓ |
| 4 |N00ff10ef|← |
| 6 |N00ff5aa5|→ |
| 5 |N00ff38c7|Home|

シリアルポートには受信した赤外線リモコンのコマンドをデコード結果を出力しています。
他のリモコンで使用するときはにコードを確認することができます。
![serialmonitor.png](https://qiita-image-store.s3.amazonaws.com/0/331423/50c2ec5c-e075-1c5c-743b-a67906933d44.png)

##注記
- Arduino初心者ですので見よう見まねで作ったところもあり、変なコードかもしれません。ご指摘いただければと思います。
- エアコン用のリモコンはデータ長がとても長く、受信しきれないようです。

##参考にさせていただいたサイト

- [【ESP32】NEC方式で赤外線通信できるプログラムを作ってみた](http://rikoubou.hatenablog.com/entry/2017/06/27/174558)
- [赤外線リモコンの通信フォーマット](http://elm-chan.org/docs/ir_format.html)
- [PIC16F84Aで作るＡＱＵＯＳのリモコンの製作](http://milky.geocities.jp/waewax/PAGE12.html)
- [M5Stackでマルチタスク処理を実行する](https://qiita.com/nnn_anoken/items/733b5f52957980bb9fb1)
- その他いろいろ