M5StickC を TOTO ウォッシュレットのリモコンにする

#概要

ウォッシュレットのリモコンの一部ボタン（大事な「大」と「小」）が不調で時々反応しなくなるので、予備として M5StickC でリモコンとして使えるようにしました。（まぁ、オート機能もあるし、本体側のボタンもあるので実際に使うことはなさそうですが）
リモコンの赤外線信号の情報が無いので M5GO 付属の IR REMOTE ユニットを接続し、ライブラリ IRremoteESP8266 付属のスケッチ例 IRrecvDumpV2 で実際のリモコンの赤外線信号データを取得、そのデータを M5StickC から送信する仕組みです。取得した生データをそのまま sendRaw() で送っても動作しましたが、生データを解析し、コード指定で送信するようにしています。

##環境

- M5StickC
 - [Arduino 1.8.10](https://www.arduino.cc/en/Main/Software)
 - ESP32 Arduino 1.0.4
 - M5StickC 0.1.0
 - IRremoteESP8266 2.6.6
- ウォッシュレット
 - TOTO ネオレストＡＨ３  
![2019-10-14-1 TOTO Washlet.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/133064/69cfc60c-abf6-6e84-2b02-eb634cb2875a.jpeg)   
![2019-10-14-2 TOTO Washlet.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/133064/effd5001-6392-1232-707e-881c8dae2d0e.jpeg)

##リモコン信号

以下の信号データは実際のリモコン信号を解析したものです。ボタンを押すと各信号が３回繰り返して送信されていました。

| ボタン | 信号１ | 信号２ | 注 | 
|:-----:|:-----:|:-----:|:--:|
| 便ふた開閉 | 0x2008000E0E | - | |
| 便座開閉   | 0x200800F6F6 | - | |
| 大        | 0x200800B0B0 | - | |
| 小        | 0x2008008888 | - | |
| eco 小    | 0x200800B6B6 | - | |
| おしり     | 0x2008006060 | 0x2008AC802C | 注１、２ |
| やわらか   | 0x2008006060 | 0x2008ACA804 | 注１、２ |
| ビデ      | 0x2008001010 | 0x2008AC40EC | 注１、２ |
| 止        | 0x2008000000 | 0x2008AADA70 | 注１ |
| マッサージ | 0x200800E0E0 | - | |
| 乾燥      | 0x200800C0C0 | - | 注３ |
| パワー脱臭 | 0x2008007C7C | - | |

（ 今回の実装では「マッサージ/乾燥/パワー脱臭」は画面に入りきらないので省略 )

- 注１ 「おりし/やわらか/ビデ/止」ボタンは「ムーブ入/切」と「洗う」の２種類の信号が出力される ）

- 注２ 実際のリモコンは状態を持っていて、洗浄停止中に「おりし/やわらか/ビデ」を押すと信号２（洗浄）のみが送信され、洗浄中に押すと信号１（「ムーブ入/切）と信号２（洗浄）の両方が送信される。今回の実装は実用上問題が無いので常に信号１と信号２の両方を送信する。
- 注３ 乾燥に関してもリモコンは状態を持っていて、乾燥中に再度「乾燥」ボタンを押すと「止」と同じ信号が送信される。

##実行例

リモコンの赤外線信号解析用に M5GO 付属の IR REMOTE ユニットを接続し、受信用のピン番号は 33 で IRrecvDumpV2 を実行しました。

![2019-10-14-4 TOTO Washlet.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/133064/41d0b793-9ac2-8aa1-793b-df3168a7ccc4.jpeg)

リモコンプログラムは画面に送信信号をメニュー表示し、ボタン操作で選択、送信します。

- ボタンＢ・電源ボタンでメニュー項目を進む・戻る
- ボタンＡで選択中の信号を送信

![2019-10-14-3 TOTO Washlet.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/133064/d2589814-9163-0c5e-67b4-2e0052bb6dc5.jpeg)

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">実装を公開しました。<br>「M5StickC を TOTO ウォッシュレットのリモコンにする」<a href="https://t.co/7A7RBnZdIo">https://t.co/7A7RBnZdIo</a><a href="https://twitter.com/hashtag/M5StickC?src=hash&amp;ref_src=twsrc%5Etfw">#M5StickC</a> <a href="https://twitter.com/hashtag/M5Stack?src=hash&amp;ref_src=twsrc%5Etfw">#M5Stack</a> <a href="https://t.co/mT5zuUVi9C">pic.twitter.com/mT5zuUVi9C</a></p>&mdash; 稲澤祐一 (@inasawa) <a href="https://twitter.com/inasawa/status/1183945148071202816?ref_src=twsrc%5Etfw">October 15, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

#M5StickCプログラム
```cpp:TOTO_Washlet.ino
#include <M5StickC.h>
#include <Arduino.h>
#include <IRremoteESP8266.h>
#include <IRsend.h>

const uint16_t kIrLed = 9;  // M5StickC GPIO pin to use.
IRsend irsend(kIrLed);  // Set the GPIO to be used to sending the message.

// IR Data for TOTO Washlet Neorest AH3
struct {
  std::string name;
  uint64_t data1;
  uint64_t data2;
} ir_data[] = {
    {"Op/Cl Lid",  0x2008000E0E, 0},            // Open Close Lid
    {"Op/Cl Seat", 0x200800F6F6, 0},            // Open Close Seat
    {"Full Flush", 0x200800B0B0, 0},            // Full Flush
    {"Half Flush", 0x2008008888, 0},            // Half Flush
    {"eco Flush",  0x200800B6B6, 0},            // eco Half Flush
    {"Wash Rear",  0x2008006060, 0x2008AC802C}, // move on/off + Wash Rear
    {"Soft Rear",  0x2008006060, 0x2008ACA804}, // move on/off + Soft Rear
    {"Bidet",      0x2008001010, 0x2008AC40EC}, // move on/off + Bidet
    {"Stop",       0x2008000000, 0x2008AADA70}  // move off + Stop Wash
  };

int data_count = sizeof(ir_data) / sizeof(ir_data[0]);
int ix = 0;
int x_offset0 =  3;
int x_offset1 =  5;
int x_offset2 = 15;
int y_offset1 = 12;
int y_offset2 = 30;
int height    = 14;

void sendToto(uint64_t data) {
  irsend.sendGeneric(
          6000,  3000,      // headermark, headerspace,
          600,   1600,      // onemark,    onespace,
          600,    600,      // zeromark,   zerospace,
          600,  40000,      // footermark, gap,
          data,    39,      // data,       nbits,
          38,    true,      // frequency,  MSBfirst,
          2, kDutyDefault); // repeat,     dutycycle (kDutyDefault = 50 %)
}

void setup() {
  M5.begin();
  irsend.begin();

  M5.Axp.ScreenBreath(9); // 画面輝度 (7-15)
  M5.Lcd.setTextColor(TFT_WHITE, TFT_BLACK);
  M5.Lcd.drawString("<< Washlet >>", x_offset0, y_offset1);
  M5.Lcd.setTextColor(TFT_ORANGE, TFT_BLACK);
  M5.Lcd.drawString(">", x_offset1, y_offset2);
  M5.Lcd.drawString(ir_data[0].name.c_str(), x_offset2, y_offset2);
  M5.Lcd.setTextColor(TFT_WHITE, TFT_BLACK);
  for (int i = 1; i < data_count; i ++) {
    M5.Lcd.drawString(ir_data[i].name.c_str(), x_offset2, y_offset2 + i * height);    
  }
}

void loop() {
  M5.update();
  
  if (M5.BtnA.wasPressed()) {
    Serial.println(ir_data[ix].name.c_str());
    sendToto(ir_data[ix].data1);
    if (ir_data[ix].data2 != 0) {
      delay(40);
      sendToto(ir_data[ix].data2);
    }
  }
  
  if (M5.BtnB.wasPressed() || M5.Axp.GetBtnPress() > 0) {
    M5.Lcd.setTextColor(TFT_BLACK, TFT_BLACK);
    M5.Lcd.drawString(">",  x_offset1, y_offset2 + ix * height);
    M5.Lcd.setTextColor(TFT_WHITE, TFT_BLACK);
    M5.Lcd.drawString(ir_data[ix].name.c_str(), x_offset2, y_offset2 + ix * height);
    if (M5.BtnB.wasPressed()) {
      ix ++;
      if (ix == data_count) {
        ix = 0;
      }
    } else {
      ix --;
      if (ix < 0) {
        ix = data_count - 1;
      }      
    }
    M5.Lcd.setTextColor(TFT_ORANGE, TFT_BLACK);
    M5.Lcd.drawString(">",  x_offset1, y_offset2 + ix * height);
    M5.Lcd.drawString(ir_data[ix].name.c_str(), x_offset2, y_offset2 + ix * height);    
  }
}
```

#リモコン信号解析プログラム

スケッチ例 IRrecvDumpV2 で取得した Raw Data を解析して信号データに変換するプログラムです。Python で書いて Mac上で実行しましたが、この程度なので IRrecvDumpV2 に組み込んでもよさそうです。値の範囲の判定値は仕様などは確認していなくて実際の値に合わせて調整したものです。

```python:TOTO_Washlet.py
import re
import fileinput

HDR_MARK   =  6000
HDR_SPACE  =  3000
BIT_MARK   =   600
ZERO_SPACE =   600
ONE_SPACE  =  1600
GAP        = 40000

for line in fileinput.input():
    line = line.strip()
    if len(line) == 0:
        continue

    m = re.match('uint16_t rawData\[\d+\] = {([^}]+)};', line)
    if m:
        print(line)
        print()
        raw = list(map(lambda x: int(x), m.group(1).split(',')))
        while len(raw) >= 2:
            mark  = raw.pop(0) # Skip Header Mark
            space = raw.pop(0) # Skip Header Space
            data = 0
            bit_length = 0
            while len(raw) >= 2:
                mark  = raw.pop(0)
                space = raw.pop(0)
                if (BIT_MARK-200) <= mark <= (BIT_MARK+200):
                    if (ZERO_SPACE-200) <= space <= (ZERO_SPACE+200):
                        print('0', end='')
                        data = data << 1
                        bit_length += 1
                    elif (ONE_SPACE-200) <= space <= (ONE_SPACE+200):
                        print('1', end='')
                        data = data << 1 | 1
                        bit_length += 1
                    elif (GAP-10000) < space < (GAP+10000):
                        break
                    else:
                        print()
                        print('Space Error:', space)
                else:
                    print()
                    print('BitMark Error:', mark)
            print('  0x{:X}  {:d} bit'.format(data, bit_length))
        print()
```

「便ふた開閉」ボタンの Raw Data を上記プログラムで処理して 0x2008000E0E の信号が３回出力されていることを確認した例です。

```
$ python TOTO_Washlet.py Raw_Data.txt
uint16_t rawData[245] = {5984, 2958,  606, 544,  580, 1658,  582, 544,  574, 544,  576, 544,  582, 540,  576, 544,  576, 544,  576, 544,  578, 516,  604, 546,  576, 1662,  576, 544,  578, 520,  600, 546,  578, 544,  578, 544,  578, 542,  578, 546,  578, 542,  580, 542,  576, 520,  600, 544,  576, 548,  578, 544,  576, 544,  578, 520,  602, 1658,  578, 1636,  602, 1636,  600, 546,  578, 544,  576, 544,  578, 544,  576, 544,  578, 1662,  576, 1636,  602, 1660,  578, 546,  578, 39116,  5960, 2960,  602, 546,  578, 1636,  600, 522,  604, 540,  580, 518,  602, 544,  578, 544,  578, 544,  576, 544,  576, 544,  578, 522,  600, 1634,  604, 518,  602, 518,  602, 548,  574, 548,  574, 544,  578, 544,  576, 546,  576, 544,  578, 544,  580, 516,  602, 548,  576, 544,  578, 546,  576, 544,  578, 542,  578, 1660,  580, 1632,  602, 1660,  576, 544,  578, 546,  576, 518,  604, 544,  578, 546,  574, 1658,  582, 1656,  578, 1662,  576, 544,  578, 38872,  5984, 2958,  604, 520,  604, 1636,  602, 542,  578, 544,  576, 546,  576, 520,  602, 542,  578, 522,  604, 542,  578, 544,  576, 550,  572, 1634,  604, 562,  576, 544,  578, 544,  578, 546,  576, 548,  578, 544,  576, 546,  574, 544,  576, 544,  578, 546,  578, 542,  578, 544,  576, 522,  600, 546,  578, 544,  578, 1634,  602, 1660,  578, 1660,  576, 544,  576, 548,  572, 544,  576, 546,  578, 544,  576, 1660,  578, 1636,  602, 1658,  578, 544,  578};  // UNKNOWN 450EA3E

010000000001000000000000000111000001110  0x2008000E0E  39 bit
010000000001000000000000000111000001110  0x2008000E0E  39 bit
010000000001000000000000000111000001110  0x2008000E0E  39 bit
```