M5Stick-Cで赤外線リモコンを作る

M5StickCで赤外線リモコンを作ります。
赤外線を受信してエンコードしたり、エンコードしたデータを送信したりします。

Arduinoのライブラリを活用します。
割と定番のようで、多数の有志の方々が、同様の記事を書かれていまして、それらを参考にさせていただきました。

また、ブラウザから、エンコードした信号を複数保存したり、保存した信号のうち1つを選択して送信したり、複数の信号をまとめて送信したりします。
ちなみに、テレビのリモコンだけでなく、エアコンのリモコンのようにリモコン信号が長すぎても大丈夫です。

# 全体構成

構成する各要素について説明しておきます。

## M5Stick-C

今回の主題であるM5Stick-Cです。CPUとしてESP32を搭載しています。
最初から、赤外線送信機を搭載しています。
赤外線受信の方は、M5StackのUnit IRを使いました。M5StickCには、Grove端子がついているので、これがちょうどよかったです。

M5Stack ： Unit IR
　https://docs.m5stack.com/#/en/unit/ir 

あと、やってみてわかったのですが、M5Stick-Cに搭載されている赤外線送信機の通信距離は非常に短いです。環境によって違うのかも知れませんが、数十cmしか飛びませんでした。
そこで、以下の赤外線送信機を使うようにしました。

Grove - Inframed Emitter
　http://wiki.seeedstudio.com/Grove-Infrared_Emitter/ 

ちなみに、M5Stick-CのGrove端子は赤外線受信機で使ってしまっているので、M5Stick-Cの反対側の2.54mmピン端子を使いました。

また、赤外線送受信の操作は、外部からのGET呼び出しで行うようにしています。
Arduinoで動作するaRestという簡易なRESTサーバを使わせていただきました。

marcoschwartz/aREST
　https://github.com/marcoschwartz/aREST 

## Webページ

WebブラウザからM5Stick-Cに対して、赤外線送信や受信を要求するためのWebページです。

エンコード化された赤外線信号は、ブラウザのHTML5の機能を使ってローカルストレージに保存しておきます。
また、M5Stick-Cに対する赤外線送信要求や赤外線受信要求は、ブラウザのJavascriptから直接要求するようにしました。
（とりあえず、今回は、手っ取り早く動かすことを優先しましたが、次回はWebサーバを立ち上げて、サーバ側で赤外線信号を覚えておくようにする予定です）

# M5Stick-Cの構成


使うのは以下の通りです。

Arduino IDEのボードマネージャからインストール
　・M5Stick-C

ライブラリマネージャからインストール
　・IRremoteESP8266 
　・aRest

利用するGPIO
　・LED(デジタル出力)     GPIO_NUM_10
　・赤外線受信(デジタル入力)  GPIO_NUM_33
　・赤外線送信(デジタル出力)  GPIO_NUM_26 または GPIO_NUM_9

赤外線受信(デジタル入力)のGPIO_NUM_33 は、M5Stick-CのGrove端子に割り当たっているピンです。
赤外線送信(デジタル出力)は2つの選択肢があります。M5Stick-C内蔵の赤外線送信機であれば9を、2.54mmピン端子を使う場合は26を選択します。

ここでちょっとずるいことをしています。
aRESTを改造しています。
aRESTは、GET呼び出しを待ち受けることができるのですが、戻り値をnumber型しか返せません。
いろんな値を返したいので、String型を返すようにしています。
ちょっと強引でしたが、これしか思いつきませんでした。

以下のファイルを改造します。
　[ユーザのDocuments]\Arduino\libraries\aREST\aREST.h

```c:aREST.h
$ diff aREST.h aREST-original.h
1490c1490
<     String result = functions[value](arguments);
---
>     int result = functions[value](arguments);
1495c1495
<       addStringToBuffer(result.c_str(), true);
---
>       addToBuffer(result, true);
1578c1578
< void function(char * function_name, String (*f)(String)){
---
> void function(char * function_name, int (*f)(String)){
2015c2015
<   String (*functions[NUMBER_FUNCTIONS])(String);
---
>   int (*functions[NUMBER_FUNCTIONS])(String);
```

Arduinoのソースコードです。

```c
#include <IRrecv.h>
#include <IRsend.h>
#include <IRac.h>
#include <IRutils.h>

#include <WiFi.h>
#include <WiFiServer.h>

#include <aREST.h>

#include <M5StickC.h>

// Create aREST instance
aREST rest = aREST();

#define LED_PIN     GPIO_NUM_10
#define IR_RECV_PIN  GPIO_NUM_33
//#define IR_SEND_PIN   GPIO_NUM_9
#define IR_SEND_PIN   GPIO_NUM_26

IRrecv irrecv(IR_RECV_PIN, 1024, 50, true);
decode_results results;
IRsend irsend(IR_SEND_PIN);

#define IR_MAX_SEND_DATA 300
uint16_t send_data[IR_MAX_SEND_DATA];
uint16_t send_len = 0;

unsigned long recvStartTime;
unsigned long recvDuration = 0;

// WiFi parameters
const char* ssid = "【WiFiのSSID】";
const char* password = "【WiFiのパスワード】";

WiFiServer server(80);

void print_screen(String message, int font_size = 2){
  M5.Lcd.fillScreen(BLACK);
  M5.Lcd.setCursor(0, 0);
  M5.Lcd.setTextSize(font_size);

  M5.Lcd.println(message);
}

void print_screen_next(String message){
  M5.Lcd.println(message);
}
  
void setup()
{
  M5.begin();
  M5.IMU.Init();

  M5.Axp.ScreenBreath(9);
  M5.Lcd.setRotation(3);
  M5.Lcd.fillScreen(BLACK);
  M5.Lcd.setTextSize(2);

  M5.Lcd.println("[M5StickC]");
  delay(500);

  pinMode(LED_PIN, OUTPUT);
  digitalWrite(LED_PIN, LOW);
  
  irsend.begin();

  // Start Serial
  Serial.begin(9600);

  // Init variables and expose them to REST API

  // Function to be exposed
  rest.function("led",ledControl);
  rest.function("start", irStart);
  rest.function("stop", irStop);
  rest.function("send", irSend);
  rest.function("get", irGet);

  // Give name & ID to the device (ID should be 6 characters long)
  rest.set_id("0001");
  rest.set_name("esp32");

  // Connect to WiFi
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.println("WiFi connected");

  // Start the server
  server.begin();
  Serial.println("Server started");

  // Print the IP address
  Serial.println(WiFi.localIP());
  M5.Lcd.println(WiFi.localIP());
}

void loop() {
  M5.update();
  
  if ( M5.BtnA.wasReleased() ) {
    // M5Stick-Cのボタンが押下されたら、直近のリモコン信号を送信
    Serial.println("BtnA.released");
    print_screen("BtnA.released start", 2);
    if( send_len > 0 )
      irsend.sendRaw((uint16_t*)send_data, send_len, 38);
    print_screen_next("BtnA.released end");
  }

  if (irrecv.decode(&results)) {
    // リモコン信号の受信が受信された
    irrecv.resume(); // Receive the next value

    if( results.rawlen <= IR_MAX_SEND_DATA ){
      ir_stop();

      // 受信したリモコン信号をバッファに格納
      uint16_t * result = resultToRawArray(&results);
      send_len = getCorrectedRawLength(&results);
      for( int i = 0 ; i < send_len ; i++ )
        send_data[i] = result[i];
      delete[] result;
      
      Serial.println("IR received");
      print_screen_next("IR received");
    }else{
      Serial.println("IR size over");
      print_screen_next("IR size over");
    }

    Serial.print(resultToHumanReadableBasic(&results));
    String description = IRAcUtils::resultAcToString(&results);
    if (description.length())
      Serial.println("Mesg Desc.: " + description);
//    Serial.println(resultToTimingInfo(&results));
    Serial.println(resultToSourceCode(&results));

    delay(100);
  }
  
  if( recvDuration > 0 ){
    // リモコン受信待ち時間タイムアウト
    unsigned long elapsed = millis() - recvStartTime;
    if( elapsed >= recvDuration ){
      Serial.println("Expired");
      ir_stop();
    }
  }
  
  // Handle REST calls
  WiFiClient client = server.available();
  if (client) {
    // GET呼び出しを検知
    for( int i = 0 ; i < 10000; i += 10 ){
      if(client.available()){
        // GET呼び出しのコールバック呼び出し
        rest.handle(client);
        return;
      }
      delay(10);
    }
    // まれにGET呼び出し受付に失敗するようです。
    Serial.println("timeout");
  }
}

// Custom function accessible by the API

String ledControl(String command) {
  Serial.println("ledControl called");

  // Get state from command
  int state = command.toInt();

  digitalWrite(LED_PIN, state);

  return "OK";
}

String irSend(String command) {
  Serial.println("irSend called");

  send_len = hexstr2array(command, send_data, IR_MAX_SEND_DATA);
  if( send_len > 0 ){
    print_screen("IR send", 2);

    Serial.println(send_data[0]);
    Serial.println("...");
    Serial.println(send_data[send_len - 1]);

    irsend.sendRaw((uint16_t*)send_data, send_len, 38);

    return "OK";
  }else{
    return "NG";
  }
}

void ir_start(unsigned long duration){
  if( duration == 0 ){
    ir_stop();
  }else{
    if( recvDuration == 0 ){
      irrecv.enableIRIn();
    }

    send_len = 0;
    recvStartTime = millis();
    recvDuration = duration;

    print_screen("IR scan start", 2);
  }
}

void ir_stop(void){
  if( recvDuration > 0 ){
    pinMode(IR_RECV_PIN, OUTPUT);
    recvDuration = 0;

    print_screen_next("IR scan stopped");
  }
}

String irStart(String command){
  Serial.println("irStart called");

  unsigned long duration = command.toInt();

  ir_start(duration);
  
  return "OK";
}

String irStop(String command){
  Serial.println("irStop called");

  ir_stop();
  
  return "OK";
}

String irGet(String command){
  Serial.println("irGet called");

  if( send_len > 0 ){
    String hexstr = array2hexstr(send_data, send_len);
    return hexstr;
  }else{
    return "NG";
  }
}

int char2int(char c){
  if( c >= '0' && c <= '9' )
    return c - '0';
  if( c >= 'a' && c <= 'f' )
    return c - 'a' + 10;
  if( c >= 'A' && c <= 'F' )
    return c- 'A' + 10;

  return 0;
}

char int2char(int i){
  if( i >= 0 && i <= 9 )
    return '0' + i;
  if( i >= 10 && i <= 15 )
    return 'a' + (i - 10);

  return '0';
}

int hexstr2array(String str, uint16_t *array, int maxlen){
  int len = str.length();
  if( (len % 4) != 0 )
    return -1;
  if( len / 4 > maxlen )
    return -1;
    
  for( int i = 0 ; i < len ; i += 4 ){
    uint16_t value = char2int(str.charAt(i)) << 12;
    value += char2int(str.charAt(i + 1)) << 8;
    value += char2int(str.charAt(i + 2)) << 4;
    value += char2int(str.charAt(i + 3));
    array[i / 4] = value;
  }

  return len / 4;
}

String array2hexstr(uint16_t *array, int len){
  String str = "";
  for( int i = 0 ; i < len * 4 ; i++ )
    str.concat("0");
    
  for( int i = 0 ; i < len ; i++ ){
      str.setCharAt(i * 4, int2char((array[i] >> 12) & 0x0f));
      str.setCharAt(i * 4 + 1, int2char((array[i] >> 8) & 0x0f));
      str.setCharAt(i * 4 + 2, int2char((array[i] >> 4) & 0x0f));
      str.setCharAt(i * 4 + 3, int2char((array[i]) & 0x0f));
  }

  return str;
}
```

以下の部分は環境に合わせて変更してください。
　const char* ssid = "【WiFiのSSID】";
　const char* password = "【WiFiのパスワード】";

GPIOはここで指定しています。

```
#define LED_PIN     GPIO_NUM_10
#define IR_RECV_PIN  GPIO_NUM_33
//#define IR_SEND_PIN   GPIO_NUM_9
#define IR_SEND_PIN   GPIO_NUM_26
```

以下のところで、GET呼び出しを受け付けるエンドポイントを指定しています。
GET呼び出しを受け付けると、指定されたコールバック関数が呼ばれます。

  rest.function("led",ledControl);
  rest.function("start", irStart);
  rest.function("stop", irStop);
  rest.function("send", irSend);
  rest.function("get", irGet);

ledが簡単ですね。

```c
String ledControl(String command) {
  Serial.println("ledControl called");

  // Get state from command
  int state = command.toInt();

  digitalWrite(LED_PIN, state);

  return "OK";
}
```

GET呼び出し時のパラメータが、commandに入ってきます。
その値をint型にパースして、その値をLEDのGPIOに設定しています。

GET呼び出しエンドポイントの仕様を整理しておきます。

led
　M5StickCに内蔵されたLEDをOn/Offします。
　params："0"：点灯、"1"：消灯
　return_value："OK"(成功時)

start
　M5StickCに接続した赤外線受信機で、リモコン信号の受信を待ち受けます。
　params：受信待ち時間の10進数文字列
　return_value："OK"(成功時)

end
　赤外線受信待ち受けを停止します。
　return_value："OK"(成功時)

get
　受信したリモコン信号を取得します。
　return_value：エンコード化されたリモコン信号の16進数文字列(成功時)

send
　M5Stick-Cに接続したまたは内蔵されている赤外線送信機で、リモコン信号を送信します。
　params：エンコード化されたリモコン信号の16進数文字列
　return_value："OK"(成功時)

paramsはこんな風に指定します。
　例：LED点灯
　　http://XXX.XXX.XXX.XXX/led?params=0
　XXX.XXX.XXX.XXX には、M5Stick-Cに割り当てられたIPアドレスを指定してください。

ledでのreturn_valueの例です。
　{"return_value": "OK", "id": "0001", "name": "esp32", "hardware": "esp32", "connected": true}

getのretun_valueやsendのparamsでの16進数文字列は、2バイトのunsigned shortをBig Endianで並べたものです。
　例： [1,2,3]の場合、"000100020003" となります。


# 動作確認

起動させると以下のようにコンソールに表示されます。

```
..
WiFi connected
Server started
XXX.XXX.XXX.XXX
```

　http://XXX.XXX.XXX.XXX/start?params=10000

と呼び出すと、以下のように表示されます。

```
irStart called
```

すかさず、手持ちのリモコンから信号を送信すると以下のように表示されます。

```
IR received
Encoding  : UNKNOWN
Code      : E88A71EA (58 bits)
uint16_t rawData[115] = {3298, 1596,  446, 414,  370, 444,  368, 1228,  422, 390,  418, 1204,  396, 470,  290,                                                           442,  444, 418,  292, 1280,  418, 1230,  392, 416,  422, 392,  418, 392,  392, 1230,  422, 1202,  418, 416,                                                            368, 420,  418, 470,  294, 440,  420, 442,  290, 442,  450, 390,  392, 418,  292, 520,  410, 430,  364, 442,                                                            200, 614,  366, 444,  292, 1278,  416, 424,  418, 418,  368, 416,  262, 548,  398, 440,  292, 520,  366, 418,                                                            446, 1176,  394, 472,  292, 464,  394, 442,  368, 600,  196, 1272,  292, 518,  368, 444,  366, 444,  366, 418                                                          ,  416, 500,  290, 416,  392, 1230,  394, 418,  420, 1202,  418, 1232,  288, 1306,  394, 1230,  392, 1228,  39                                                          6, 1228,  394};  // UNKNOWN E88A71EA
```

ちょうど手元にあった富士通製のエアコンのリモコンを受信してみたときのものです。
上記の例では、長さは115でした。最大の受信サイズは以下で指定していますので、もし足りなければ変更してください。

```
#define IR_MAX_SEND_DATA 300
```

次に以下を呼び出します。

　http://XXX.XXX.XXX.XXX/get

すると、以下が返ってきます。

```
{"return_value": "0ce2063c01be019e017201bc017004cc01a6018601a204b4018c01d6012201ba01bc01a20124050001a204ce018801a001a6018801a20188018804ce01a604b201a201a0017001a401a201d6012601b801a401ba012201ba01c20186018801a201240208019a01ae016c01ba00c80266016e01bc012404fe01a001a801a201a2017001a001060224018e01b801240208016e01a201be0498018a01d8012401d0018a01ba0170025800c404f801240206017001bc016e01bc016e01a201a001f4012201a0018804ce018a01a201a404b201a204d00120051a018a04ce018804cc018c04cc018a", "id": "0001", "name": "esp32", "hardware": "esp32", "connected": true}
```

return_valueのところが、エンコード化されたリモコン信号です。

これを以下のparamsに指定して呼び出します。

　http://XXX.XXX.XXX.XXX/send?params=ZZZZZZZZZZZZZZZZZZZZZZZZZZZZ

これで、取得したエンコード化されたリモコン信号をM5Stick-Cに接続した赤外線送信機から送信されます。
繰り返しになりますが、M5Stick-Cに内蔵の赤外線送信機の距離は短い(数十cm)ので注意してください。

# Webページ

以下のようなページです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/e2e99f15-576f-d5c1-b007-69d47821bec0.png)

ソースコードです。

```html:index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <meta http-equiv="Content-Security-Policy" content="default-src * data: gap: https://ssl.gstatic.com 'unsafe-eval' 'unsafe-inline'; style-src * 'unsafe-inline'; media-src *; img-src * data: content: blob:;">
  <meta name="format-detection" content="telephone=no">
  <meta name="msapplication-tap-highlight" content="no">
  <meta name="apple-mobile-web-app-capable" content="yes" />
  <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width">

  <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
  <script src="https://code.jquery.com/jquery-1.12.4.min.js" integrity="sha384-nvAa0+6Qg9clwYCGGPpDQLVpLNn0fRaROjHqs13t4Ggj3Ez50XnGQqc/r8MhnRDZ" crossorigin="anonymous"></script>
  <!-- Latest compiled and minified CSS -->
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css" integrity="sha384-HSMxcRTRxnN+Bdg0JdbxYKrThecOKuH5zCYotlSAcp1+c8xmyTe9GYg1l9a69psu" crossorigin="anonymous">
  <!-- Optional theme -->
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap-theme.min.css" integrity="sha384-6pzBo3FDv/PJ8r2KRkGHifhEocL+1X2rVCTTkUfGk7/0pbek5mMa1upzvWbrUbOZ" crossorigin="anonymous">
  <!-- Latest compiled and minified JavaScript -->
  <script src="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js" integrity="sha384-aJ21OjlMXNL5UyIl/XNwTMqvzeRMZH2w8c5cRVpzpU8Y5bApTppSuUkhZXN0VxHd" crossorigin="anonymous"></script>

  <title>M5Stick-Cリモコン</title>

  <script src="js/methods_utils.js"></script>
  <script src="js/vue_utils.js"></script>

<!--
  <script src="dist/js/vconsole.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/js-cookie@2/src/js.cookie.min.js"></script>
-->
  <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
    <div id="top" class="container">
        <h1>M5Stick-Cリモコン</h1>

        <label>m5stickc url</label> <input class="form-control" v-model="base_url"><br>
        <button class="btn btn-default" v-on:click="ir_start()">リモコン受信開始</button><br>
        <button class="btn btn-default" v-on:click="ir_get()">リモコン信号取得</button><br>

        <table class="table table-striped">
            <thead>
                <tr><th>選択</th><th>名前</th><th>削除</th></tr>
            </thead>
            <tbody>
                <tr v-for="(value, index) in send_data_list">
                    <td><button class="btn btn-default" v-on:click="data_select(index)">選択</button></td>
                    <td>{{value.name}}</td>
                    <td><button class="btn btn-default" v-on:click="data_delete(index)">削除</button></td>
                </tr>
            </tbody>
        </table>

        <label>リモコン信号</label>
        <textarea class="form-control" rows="10" v-model="send_data" readonly></textarea>
        <button class="btn btn-default" v-on:click="ir_send()">リモコン信号送信</button><br>


        <div class="modal fade" id="progress">
            <div class="modal-dialog">
                <div class="modal-content">
                    <div class="modal-header">
                        <h4 class="modal-title">{{progress_title}}</h4>
                    </div>
                    <div class="modal-body">
                        <center><progress max="100" /></center>
                    </div>
                </div>
            </div>
        </div>
    </div>

    <script src="js/start.js"></script>
</body>
```

```js:start.js
'use strict';

//var vConsole = new VConsole();

const base_url = "http://XXX.XXX.XXX.XXX";

var vue_options = {
    el: "#top",
    data: {
        progress_title: '',

        send_data: '',
        send_data_list: [],
        base_url: base_url,
    },
    computed: {
    },
    methods: {
        ir_start: function(){
            return do_get(this.base_url + "/start", { params: "5000" } )
            .then(json =>{
                console.log(json);
            })
            .catch(error =>{
            	alert(error);
            });
        },
        ir_get: function(){
            return do_get(this.base_url + "/get", {} )
            .then(json =>{
                console.log(json);
                if( json.return_value != "NG" ){
                    var ret = prompt('名前を決めてください。');
                    if( ret ){
                        this.send_data_list.push({ name: ret, data: json.return_value });
                        this.send_data = json.return_value;
                        localStorage.setItem('send_data_list', JSON.stringify(this.send_data_list));
                    }
                }else{
                    alert('取得できませんでした。');
                }
            })
            .catch(error =>{
            	alert(error);
            });
        },
        ir_send: function(){
            return do_get(this.base_url + "/send", { params: this.send_data} )
            .then(json =>{
                console.log(json);
            })
            .catch(error =>{
            	alert(error);
            });
        },
        data_select: function(index){
            this.send_data = this.send_data_list[index].data;
        },
        data_delete: function(index){
            this.send_data_list.splice(index, 1);
            this.send_data_list = JSON.parse(JSON.stringify(this.send_data_list));
            localStorage.setItem('send_data_list', JSON.stringify(this.send_data_list));
        },
    },
    created: function(){
    },
    mounted: function(){
        proc_load();

        var list = localStorage.getItem('send_data_list');
        if( list )
            this.send_data_list = JSON.parse(list);
    }
};
vue_add_methods(vue_options, methods_utils);
var vue = new Vue( vue_options );

function do_get(url, qs) {
  var params = new URLSearchParams(qs);
  var url2 = new URL(url);
  url2.search = params;

  return fetch(url2.toString(), {
      method: 'GET',
    })
    .then((response) => {
      if (!response.ok)
        throw 'status is not 200';
      return response.json();
    });
}
```

base_url のところは、M5Stick-Cに割り当てられたIPアドレスを指定して下さい。

以上