2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

エアコンのリモコンの押し間違いを防ぎたい…… そうだ!💡

2
Posted at

押し間違いに気がつけるようにすればいいんだ💡

きっかけ

今夏、こんなニュースを見かけました。

熱中症で亡くなった方の中には 冷房と間違えて暖房を入れてしまった という方もいらっしゃったということでした。

私もエアコンのリモコンを押し間違えたことがあります。エアコンの運転音がいつもと違うと思い、幸いすぐに気がつくことができました。

夏なのに暖房をつけてしまう、冬なのに冷房をつけてしまう…
ボタンを間違えるだけで命取りになってしまうと気がついたのです。

なぜ間違えるのか

古いエアコン用リモコン
photo by Takanashi-Photo

  • 純粋な押し間違い
  • リモコンの構造によるもの
  • エアコン本体に運転モードを示す表示がない
  • リモコンを押した時のブザー音がどの運転モードでも同じ

アクセルとブレーキの踏み間違いのように純粋な押し間違いの可能性もあります。しかし写真のような古いタイプのリモコンではどの運転モードなのかがそもそも見づらく、運転モードの切り替えが1ボタンになっています。ボタンの数も多く、ボタンの色も同じですね。

エアコン本体にも問題があると考えています。
本体には運転モードがわかるLED表示はなく、運転している・していないしかわかりません。また、ボタンを押しても「ピッピッ」としか鳴らないのでこれも運転モードを区別できる要素にはなりません。

どうすれば間違えなくなるか

  • リモコンを改良する
  • エアコン本体に運転モードを示す表示をつける
  • 運転モードに応じてブザー音を変える

最近のリモコンも改良が進んでいます。写真は私が使っているリモコンですが、かなりわかりやすくなっています。

  • 液晶表示が大きい
  • ボタンにも色がつけられている
  • リモコンと文字の色のコントラスト比が高い
  • 冷房ボタン・停止ボタンを示す突起がつけられている
  • 相反する運転モードのボタンが離されている
  • 普段使わないボタンはフタで隠されている

しかし結局、ボタンを押し間違えれば誤った運転モードでエアコンが動いてしまいます。そのため、エアコン側の改良も必要になります。ただし、エアコン本体の改造を行うのは危険が伴う上、一般のご家庭で気楽に実行できることではありません。

そこで考えました

エアコンのリモコンには運転モードを示す信号が含まれているはずです。
リモコンの信号は赤外線ですから、エアコンの横に受信ユニットを設置し、光や音で運転モードを知らせるモノを増設します。こうしてリモコンはそのまま、エアコンも無改造で運転モードの確認ができるのではないかと考えました。

やってみよう

用意したもの

PXL_20251212_123915696-EDIT.jpg

必要物はこれだけです。赤外線送受信ユニットはM5Stack用とありますがAtomS3Rにも接続できます。

受信した信号は G1 (GPIO = 1) ピンに入力されます。送信は G2 ピンですが今回は扱いません。

リモコン信号の解析

リモコン信号の仕組み

今回対象にするエアコンのリモコンは富士通ゼネラル製です。
リモコンの信号は家製協(AEHA)フォーマットで送られているようです。

  • キャリア: 赤外線 ($λ_p$ = 950nm)
  • サブキャリア: $f_{sc}$ = 33-40 kHz (38 kHz typ.), $\frac{1}{3}$ duty
  • $T$ = 350-500 μs (425 μs typ.)
  • 16bit のカスタマーコード + 4bit のパリティ
  • 可変長フレーム (48 bit typ.)、トレーラーで終端
    ir_aeha.png
  • カスタマーコード: 16bit のメーカー識別コード。
  • パリティ: カスタマーコードを4ビット単位でXORをとったもので、これによりカスタマーコードのエラーをチェックする。
  • データ: 可変長(28bit typ.)。データ部のエラー制御は実装依存で、通常は DataN がBCC(XOR値、補数、CRCなど)になる。

ChaN氏の解説サイト より要点部を引用、数式部分を補筆)

キャリア は通信に使う光のことです。人間の目にはおよそ 380-750nm の範囲の波長の光 しか見えないので、当然ながら 950nm の赤外線は見えません。

サブキャリア とは上図の Data bit にある櫛形の細かい波(副搬送波)のことです。さらに $\frac{1}{3}$ duty とは波の高い時間が 1、低い時間が 3 の割合ということになります。

$T$ とはリモコン信号の単位時間のことです。この $T$ の長さで信号が送られているかどうかで 0 と 1 のビット値が決まります。
Data bit にあるとおり、信号の ON/OFF の割合が 1:1 ならば 0、1:3 ならば 1 を表します。425 μs typ. とありますが、実際どれくらいの時間であるかは幅があるため、最初に送られてくる Leader 部の $8T$ を基準にして $T$ の値を決めます(後述)。

ただし、実際のリモコンの信号では上記の記述に当てはまらず、既成のデコーダでは解析に失敗しました。そこで、実際に受信した信号にどのようなデータ・パターンが含まれているかを自分で解析しなければなりません。

信号解析

IRremoteESP8266 というライブラリを使います。含まれている デコーダのヘッダファイル を見ると、今回対象の富士通のエアコンはライブラリサポートの対象外のようです。そこでこのライブラリが保持している生データ rawbuf の情報を利用します。

rawbuf(抜粋)
#include <IRrecv.h>
#include <IRtext.h>
#include <IRutils.h>

const uint16_t kRecvPin           = 1;
const uint16_t kCaptureBufferSize = 1024;
const uint8_t kTimeout            = 50;

IRrecv irrecv(kRecvPin, kCaptureBufferSize, kTimeout, true);
decode_results results;

void setup() {
  irrecv.enableIRIn();
}

void loop() {
  M5.update();

  if (irrecv.decode(&results)) {
    uint16_t* data = results->rawbuf;  // これで取り出せる
    irrecv.resume();
  }

  delay(100);
}

IRremoteESP8266 の仕様上、一度 decode 関数を通さなければ rawbuf に生データが格納されないようです。リモコンのボタンを押して信号を受信し、この生データの中身を確認すると次のようになっています。

1,   1644, 817, 209, 200, 207, 200, 207, 607, 209, 200, 207, 608, 208, 200, 208, 200, 207, 200, 208, 607, 208, 607,
208, 212,  196, 212, 196, 212, 196, 608, 208, 608, 208, 201, 207, 212, 196, 212, 195, 201, 207, 212, 196, 201, 207,
212, 196,  212, 196, 212, 196, 212, 196, 212, 196, 212, 196, 212, 196, 608, 208, 212, 196, 212, 196, 212, 196, 212,
196, 212,  196, 212, 196, 212, 196, 608, 208, 212, 196, 212, 196, 212, 196, 212, 196, 608, 208, 608, 207, 608, 207,
608, 207,  608, 208, 608, 207, 608, 208, 608, 207, 608, 207, 212, 196, 609, 207, 212, 196, 212, 196, 211, 196, 212,
196, 609,  207, 212, 196, 212, 196, 212, 196, 212, 196, 212, 196, 620, 196, 212, 196, 620, 196, 212, 196, 212, 196,
212, 196,  212, 196, 620, 195, 620, 196, 212, 196, 212, 196, 212, 196, 620, 196, 212, 196, 212, 195, 212, 195, 212,
196, 212,  196, 212, 196, 212, 195, 212, 195, 212, 196, 212, 196, 212, 196, 212, 196, 212, 195, 212, 196, 212, 196,
212, 196,  212, 196, 212, 195, 212, 196, 212, 196, 212, 196, 212, 195, 212, 195, 212, 196, 212, 196, 212, 196, 212,
195, 212,  195, 212, 196, 212, 196, 212, 196, 212, 196, 212, 196, 212, 196, 212, 196, 212, 196, 212, 195, 212, 195,
212, 196,  212, 196, 212, 196, 212, 195, 212, 196, 212, 196, 212, 196, 212, 196, 620, 196, 212, 196, 212, 196, 620,
195, 212,  196, 212, 196, 212, 196, 212, 196, 212, 196, 212, 196, 212, 196, 212, 195, 212, 196, 212, 196, 212, 196,
212, 195,  212, 196, 212, 196, 620, 196, 212, 195, 212, 196, 620, 196, 212, 195

最初の 1 は信号と関係ないため無視してください。
すると冒頭に 1644, 817, ... と他よりも何倍か多い数値が見えます。これが前述した Leader で、それぞれ $8T, 4T$ の時間を表します。

なお、実際には IRremoteESP8266 では 0.5 μs の解像度で信号を読み取っているようですので、実際の ON/OFF 時間はこの半分(822 μs, 408.5μs, ...)になります。

これで基準となる $T$ の時間を求められるので、$8T = 1644$ から $T = 205.5$ とわかります。上記の生データのすべてのデータを 205.5 で割って四捨五入を行うと、

8, 4,                                            // Leader
1, 1, 1, 1, 1, 3, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1,  // 1 バイト目
1, 3, 1, 3, 1, 1, 1, 1, 1, 1, 1, 3, 1, 3, 1, 1,  // 2 バイト目
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,  // ...
1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1,
1, 1, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3,
1, 3, 1, 3, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1,
1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1, 1,
1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1, 3, 1, 1,
1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 3, 1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,  // 17 バイト目
1, 1, 1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 3, 1, 1,  // 18 バイト目
1                                                // Trailer (終端) を表す 1

という結果になります。

Leader の後に続く信号も ON, OFF, ON, OFF, ... と続いています。

信号の ON/OFF の時間が 1:1 のときビットは 0、1:3 のときビットは 1 です。冒頭の Leader と終端の Trailer を除いて、1, 10, 1, 31 というルールで置換を行うと、

0, 0, 1, 0, 1, 0, 0, 0,  // 1 バイト目
1, 1, 0, 0, 0, 1, 1, 0,  // 2 バイト目
0, 0, 0, 0, 0, 0, 0, 0,  // ...
0, 0, 0, 0, 1, 0, 0, 0,
0, 0, 0, 0, 1, 0, 0, 0,
0, 1, 1, 1, 1, 1, 1, 1,
1, 1, 0, 1, 0, 0, 0, 0,
1, 0, 0, 0, 0, 0, 1, 0,
1, 0, 0, 0, 0, 1, 1, 0,
0, 0, 1, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 1, 0, 0, 1, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 1, 0, 0, 1, 0

となります。
なお Frame 部分のビットは LSB first(最下位ビットが先) ですので、それを考慮して16進数表現に直すと

14 63 00 10 10 FE 0B 41 61 04 00 00 00 00 00 12 00 48

という結果になります。

判明した信号パターン

この解析方法でリモコンのボタンを一つずつ押していき、どのような状態のときにどのような信号が送られているかをまとめました。

14 63 00 10 10 FE 0B 41 61 04 00 00 00 00 00 12 00 48  // 暖房ボタン押下: 暖房, 20℃, 風量自動, スイングなし
14 63 00 10 10 FE 0B 41 58 04 00 00 00 00 00 12 00 51  // 温度下げボタン押下: 暖房, 19℃, 風量自動, スイングなし
14 63 00 10 10 FE 0B 41 59 04 00 00 00 00 00 12 00 50  // 暖房ボタン押下: 暖房, 19℃, 風量自動, スイングなし
14 63 00 10 10 FE 0B 41 59 04 00 00 00 00 00 12 00 50  // 暖房ボタン押下: 暖房, 19℃, 風量自動, スイングなし
14 63 00 10 10 FE 0B 41 60 04 00 00 00 00 00 12 00 49  // 温度上げボタン押下: 暖房, 20℃, 風量自動, スイングなし
14 63 00 10 10 FE 0B 41 61 04 00 00 00 00 00 12 00 48  // 暖房ボタン押下: 暖房, 20℃, 風量自動, スイングなし

14 63 00 10 10 FE 0B 41 89 01 00 00 00 00 00 12 00 23  // 冷房ボタン押下: 冷房, 25℃, 風量自動, スイングなし
14 63 00 10 10 FE 0B 41 60 01 00 00 00 00 00 12 00 4C  // 温度下げボタン押下: 冷房, 20℃, 風量自動, スイングなし
14 63 00 10 10 FE 0B 41 58 01 00 00 00 00 00 12 00 54  // 温度下げボタン押下: 冷房, 19℃, 風量自動, スイングなし

14 63 00 10 10 FE 0B 41 89 05 00 00 00 00 00 12 00 1F  // 除湿ボタン押下: 除湿, 25℃, 風量自動, スイングなし

14 63 00 10 10 FE 0B 41 89 00 00 00 00 00 00 12 00 24  // 自動ボタン押下: 自動, 標準温度±0℃, 風量自動, スイングなし

14 63 00 10 10 FE 0B 41 60 04 08 00 00 00 00 12 00 41  // 暖房ボタン押下: 暖房, 20℃, 風量4, スイングなし
14 63 00 10 10 FE 0B 41 60 04 06 00 00 00 00 12 00 43  // 暖房ボタン押下: 暖房, 20℃, 風量3, スイングなし
14 63 00 10 10 FE 0B 41 60 04 03 00 00 00 00 12 00 46  // 暖房ボタン押下: 暖房, 20℃, 風量2, スイングなし
14 63 00 10 10 FE 0B 41 60 04 01 00 00 00 00 12 00 48  // 暖房ボタン押下: 暖房, 20℃, 風量1, スイングなし
14 63 00 10 10 FE 0B 41 60 04 00 00 00 00 00 12 00 49  // 暖房ボタン押下: 暖房, 20℃, 風量自動, スイングなし
14 63 00 10 10 FE 0B 41 60 04 10 00 00 00 00 12 00 39  // 暖房ボタン押下: 暖房, 20℃, 風量自動, スイングあり

14 63 00 10 10 FE 0B 41 81 03 00 00 00 00 00 12 00 29  // 送風ボタン押下: 送風, 風量自動, スイングなし

14 63 00 10 10 6C 93                                   // 風向ボタン押下
14 63 00 10 10 39 C6                                   // ハイパワーボタン押下
14 63 00 10 10 FE 09 C1 60 01 00 00 FE 9F 00 41        // 内部クリーンボタン押下
14 63 00 10 10 02 FD                                   // 停止ボタン押下: 停止

明確にパターンが見えてきませんか??

冒頭の 14 63 00 10 10 は絶対に固定で、末尾の1バイトはその変化からチェックサムのように振る舞っているように見えます。

さらに今回は運転モードさえ把握できればいいので、特定の1バイトの数字だけをチェックすれば目的を達成できます。

14 63 00 10 10 FE 0B 41 89 00 00 00 00 00 00 12 00 24  // 自動ボタン押下
14 63 00 10 10 FE 0B 41 89 01 00 00 00 00 00 12 00 23  // 冷房ボタン押下
14 63 00 10 10 FE 0B 41 81 03 00 00 00 00 00 12 00 29  // 送風ボタン押下
14 63 00 10 10 FE 0B 41 61 04 00 00 00 00 00 12 00 48  // 暖房ボタン押下
14 63 00 10 10 FE 0B 41 89 05 00 00 00 00 00 12 00 1F  // 除湿ボタン押下
                           ^
                           このバイトが運転モード

14 63 00 10 10 02 FD  // 停止ボタン押下

これで赤外線の信号のデコード方法、そして必要なプロトコルが把握できました!

プログラム

信号のデコードの全体の流れは以下のようになります。

inline uint8_t convertRawToBytes(const decode_results* results, uint8_t* bytes, uint8_t maxBytes) {
  uint8_t normalized[512];  // 正規化されたタイミングデータ
  uint8_t bitArray[256];    // ビット配列(0 or 1)
  uint8_t numBytes = 0;

  // rawbuf[0]は無関係のデータのためスキップし、rawbuf[1]から処理開始
  const volatile uint16_t* actualData = &results->rawbuf[1];
  uint16_t actualLen                  = results->rawlen - 1;

  if (normalizeTimingData(actualData, actualLen, normalized)) {
    uint16_t numBits = pairsToBits(normalized, actualLen, bitArray, 256);
    numBytes         = bitsToBytes(bitArray, numBits, bytes, maxBytes);
  }

  return numBytes;
}

ON/OFFのタイミングデータを $T$ の値に正規化する処理は次のようになります。正規化に失敗したら false を返します。

inline bool normalizeTimingData(const volatile uint16_t* rawbuf, uint16_t rawlen, uint8_t* normalized) {
  if (rawlen < 4)
    return false;

  // ステップ1: Leader (8T, 4T) からTを算出
  // 最初の要素は8Tを表すため、T = rawbuf[0] / 8
  // 例: rawbuf[0] = 1644 の場合、T = 1644 / 8 = 205.5
  float T = rawbuf[0] / 8.0;

  if (T < 1.0)
    return false;

  // ステップ2: 全要素をTで割って正規化(四捨五入: +0.5)
  // 例: rawbuf[i] = 817, T = 205.5 の場合、normalized[i] = (817 / 205.5 + 0.5) = 4
  for (uint16_t i = 0; i < rawlen; i++) {
    normalized[i] = (uint8_t)(rawbuf[i] / T + 0.5);

    if (normalized[i] > 100)
      return false;
  }

  return true;
}

$T$ の値からビット配列に変換する処理は以下のようになります。Leader と Trailer に情報はないのでこれを取り除く処理が多いですが、本質はステップ3のペア処理部分です。

inline uint16_t pairsToBits(const uint8_t* normalized, uint16_t normalizedLen, uint8_t* bitArray, uint16_t maxBits) {
  // ステップ1: Leader (8, 4) を確認
  const uint8_t LEADER_HIGH = 8;
  const uint8_t LEADER_LOW  = 4;

  if (normalizedLen < 4 || normalized[0] != LEADER_HIGH || normalized[1] != LEADER_LOW)
    return 0;

  // ステップ2: Leader(2要素)とTrailer(1要素)を除外した信号数を計算
  uint16_t numSignals = normalizedLen - 2;  // Leader除去

  // Trailerが存在する場合は除去
  // 奇数個の信号の場合、最後の1要素が Trailer
  if (numSignals % 2 != 0)
    numSignals--;  // Trailer除去

  uint16_t numBits = numSignals / 2;

  if (numBits > maxBits)
    return 0;

  // ステップ3: ペア処理 [1,1]→ビット0, [1,3]→ビット1
  for (uint16_t i = 0; i < numBits; i++) {
    uint8_t high = normalized[2 + i * 2];  // Leader後のデータから読む
    uint8_t low  = normalized[2 + i * 2 + 1];

    if (high == 1 && low == 1)
      bitArray[i] = 0;
    else if (high == 1 && low == 3)
      bitArray[i] = 1;
    else
      return 0;  // 不正なペア
  }

  return numBits;
}

最後にビット配列をバイト配列に変換する処理です。LSB Firstです。

inline uint8_t bitsToBytes(const uint8_t* bits, uint16_t numBits, uint8_t* bytes, uint8_t maxBytes) {
  if (numBits == 0 || numBits % 8 != 0)
    return 0;

  uint8_t numBytes = numBits / 8;

  if (numBytes > maxBytes)
    return 0;

  for (uint8_t byteIdx = 0; byteIdx < numBytes; byteIdx++) {
    uint8_t byteValue = 0;

    // LSB first!
    for (uint8_t bitIdx = 0; bitIdx < 8; bitIdx++)
      byteValue |= (bits[byteIdx * 8 + bitIdx] << bitIdx);

    bytes[byteIdx] = byteValue;
  }

  return numBytes;
}

バイト配列に変換できたので、あとは受け取ったデータの特定のバイトを読めば運転モードが読み取れます。

完成したプログラムは下記にあります。

できました

リモコンを押すと画面の色が変わり、さらに最初の30秒間は画面が点滅するようにしました。

エアコン動作中にどの運転モードで稼働しているのかが一目瞭然になりました。

PXL_20251212_132130907.jpg

機能としてはこれだけで非常にシンプルです。エアコンのボタンと合わせるため、色は下記の表のようにしてみました。

運転モード 画面の色
自動 🟪 紫
冷房 🟦 水
送風 ⬜️ 白
暖房 🟥 赤
除湿 🟩 黄緑
停止 ⬛️ 消灯

今後の展望

リモコン信号のデコード方法の解析・理解で数日かかってしまいましたが、解読に成功できてからはすぐ実装ができました。

今回はディスプレイの色だけが変わるようにしましたが、音もつけられるとさらに実用度が上がるかもしれません。個人的にはボタンを押すたび騒がしいのは苦手なので、今回ディスプレイ表示だけでも満足しています。

試作ゆえ AtomS3R 特有の機能はほとんど使っていない上に今回は赤外線の送信は行っていないので、基板から作ればもっとコンパクトかつ安く作れそうです。

正直、これだけで押し間違いに気がつけて命も守れるのであれば、市販したらそこそこ需要はありそうです。その場合、各メーカーのエアコンのリモコン信号に対応が必要そうですが…

参考文献

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?