#概要
ウォッシュレットのリモコンの一部ボタン(大事な「大」と「小」)が不調で時々反応しなくなるので、予備として M5StickC でリモコンとして使えるようにしました。(まぁ、オート機能もあるし、本体側のボタンもあるので実際に使うことはなさそうですが)
リモコンの赤外線信号の情報が無いので M5GO 付属の IR REMOTE ユニットを接続し、ライブラリ IRremoteESP8266 付属のスケッチ例 IRrecvDumpV2 で実際のリモコンの赤外線信号データを取得、そのデータを M5StickC から送信する仕組みです。取得した生データをそのまま sendRaw() で送っても動作しましたが、生データを解析し、コード指定で送信するようにしています。
##環境
- M5StickC
- Arduino 1.8.10
- ESP32 Arduino 1.0.4
- M5StickC 0.1.0
- IRremoteESP8266 2.6.6
- ウォッシュレット
- TOTO ネオレストAH3
##リモコン信号
以下の信号データは実際のリモコン信号を解析したものです。ボタンを押すと各信号が3回繰り返して送信されていました。
ボタン | 信号1 | 信号2 | 注 |
---|---|---|---|
便ふた開閉 | 0x2008000E0E | - | |
便座開閉 | 0x200800F6F6 | - | |
大 | 0x200800B0B0 | - | |
小 | 0x2008008888 | - | |
eco 小 | 0x200800B6B6 | - | |
おしり | 0x2008006060 | 0x2008AC802C | 注1、2 |
やわらか | 0x2008006060 | 0x2008ACA804 | 注1、2 |
ビデ | 0x2008001010 | 0x2008AC40EC | 注1、2 |
止 | 0x2008000000 | 0x2008AADA70 | 注1 |
マッサージ | 0x200800E0E0 | - | |
乾燥 | 0x200800C0C0 | - | 注3 |
パワー脱臭 | 0x2008007C7C | - |
( 今回の実装では「マッサージ/乾燥/パワー脱臭」は画面に入りきらないので省略 )
-
注1 「おりし/やわらか/ビデ/止」ボタンは「ムーブ入/切」と「洗う」の2種類の信号が出力される )
-
注2 実際のリモコンは状態を持っていて、洗浄停止中に「おりし/やわらか/ビデ」を押すと信号2(洗浄)のみが送信され、洗浄中に押すと信号1(「ムーブ入/切)と信号2(洗浄)の両方が送信される。今回の実装は実用上問題が無いので常に信号1と信号2の両方を送信する。
-
注3 乾燥に関してもリモコンは状態を持っていて、乾燥中に再度「乾燥」ボタンを押すと「止」と同じ信号が送信される。
##実行例
リモコンの赤外線信号解析用に M5GO 付属の IR REMOTE ユニットを接続し、受信用のピン番号は 33 で IRrecvDumpV2 を実行しました。
リモコンプログラムは画面に送信信号をメニュー表示し、ボタン操作で選択、送信します。
- ボタンB・電源ボタンでメニュー項目を進む・戻る
- ボタンAで選択中の信号を送信
実装を公開しました。
— 稲澤祐一 (@inasawa) October 15, 2019
「M5StickC を TOTO ウォッシュレットのリモコンにする」https://t.co/7A7RBnZdIo#M5StickC #M5Stack pic.twitter.com/mT5zuUVi9C
#M5StickCプログラム
#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 に組み込んでもよさそうです。値の範囲の判定値は仕様などは確認していなくて実際の値に合わせて調整したものです。
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 の信号が3回出力されていることを確認した例です。
$ 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