Raspberry Piでエアコンの赤外線リモコンを解析する

この記事は「Raspberry Piでスマートルームをつくる」の一部です。

  1. 設計・部品調達編
  2. 動作テスト編
  3. エアコンのリモコン信号解析編←イマココ
  4. 基板実装編
  5. API/chatbot実装編

今回は自室にあるエアコンを操作するリモコンの信号を丸裸にしていきます!

なんでそんなことするの?

自分で赤外線信号つくって操作したいからです!おうちハックしたいからです!!!

我が家のエアコンを紹介するぜ!

今回リバースエンジニアリングの犠牲対象になるのはこちらのリモコンでございます。

A/C

2004年製造のダイキン製AN22EDS-Wに対応したリモコンです。

赤外線リモコンの信号仕様

赤外線受信モジュールの組み立てで製作したモジュールを使って実際に信号を解析していきます。

赤外線を用いたリモコンでは、制御信号を01のパルスに変換してこれを高周波の搬送波で変調したものを送信しています。
詳しくはChaN氏による赤外線リモコンの通信フォーマットが大変参考になります。

この文献によれば、エアコンのリモコン信号は大きく

  • NECフォーマット
  • 家製協(AEHA)フォーマット
  • SONYフォーマット

の3つに分けられます。
このうちNEC, AEHAフォーマットでは各ビットのデータは以下の図に示すようなフォーマットになっています。

file
リモコン信号のデータ表現
赤外線リモコンの通信フォーマットより引用

パルスが立っている間は無駄な電力消費を抑えるため変調された信号が出力されていることがわかります。
以下はAEHAフォーマットの信号定義ですが、開始を示すLeaderに続いて各種データが定義されています。

file
AEHAフォーマットによる制御信号
赤外線リモコンの通信フォーマットより引用

実際に信号を解析する

信号を可視化する

まずは、irrp.pyで学習した信号を可視化してみます。
このスクリプトが出力するファイルには信号がON(Mark)の時間とOFF(Space)の時間が交互に記録されているので、ひとまずこれを波形として表示させます。

最初に以下のコマンドでデータ用の一時ファイルを生成します。

cat commands | jq '."air:off" | .[]' | awk 'BEGIN {sum=0; print 0; print 0}; {sum = sum + $1; for (i=0;i<2;i++) print sum;}' > /tmp/plot

続いて生成したファイルからgnuplotを使ってグラフを作ります。

 $ gnuplot
plot '/tmp/plot' using 1:(floor(($0 + 1) / 2) % 2) with lines title "pulse"

以上のスクリプトを実行すると以下のような結果が得られます。

file

この結果より、このリモコンは1回の操作につき3つのフレームを送っていることがわかります。さらに拡大してみると、フレームの先頭にはきちんとLeaderが存在していることも確認できます。

file

なんか頭にも信号あるんじゃない?
そこなんだけど、すっ飛ばした信号を送っても動いたから大丈夫みたいなんだよね
大丈夫なんだ……

先頭にはフレームではないパルスが出力されていますが、信号を実際にエミュレートしたところこの部分を省いてフレーム部分から送信しても動作したので必ずしも必要な信号ではないっぽいです。

フォーマットを調べる

それでは信号の詳細な解析を進めていきたいと思いますが、まずはこのリモコンが先の3つのフォーマットのうちどれに対応しているものか調べる必要があります。

以下のようにしてMarkの時間の出現頻度を調べてみると、Markの時間の最頻値として483の値が得られます。

 $ cat commands | jq '."mode:cool" | .[]' | awk 'NR % 2 != 0' | sort | uniq -c | sort -nk1
      3 3530
    289 483

これが引用した図における単位周期Tであると仮定すると、LeaderのMark部分は3583であることよりおよそTの8倍にあたることがわかります。

\displaystyle \frac{3530}{483} = 7.308... \approx 8
意外とぴったり8倍じゃないんだね
ずれが大きめに出てるけど、
NECフォーマットとAEHAフォーマットを区別できるから問題ないかな

以上でこのリモコンはAEHAフォーマットに基づいた信号を送っていることが判明しました。

単位周期ですが、Spaceのヒストグラムを調べると最頻値として386の値が得られます。

 $ cat commands | jq '."mode:cool" | .[]' | awk 'NR % 2 == 0' | sort | uniq -c | sort -nk1
      1 25319
      2 34688
      3 1680
     67 1253
    218 386

検出の誤差を埋めるため、ここでは1回のMarkとSpaceの時間の平均を単位周期Tとします。

T = \displaystyle \frac{483 + 386}{2}  \approx 435

送っているデータを調べる

フォーマットが分かればあとはどんなデータがどこに埋め込まれているのかを調べていく作業になります。データビットの仕様は参考リンクで示した通りなので、まずはこれをデコードするスクリプトを以下のように作成しました。

時間のシーケンスを単位周期Tで割ったものを順次見ていく形のスクリプトです。

これを使って標準入力から受け取った信号のON/OFFの時間データをデコードするスクリプトがこちらです。

ここからは地道に状態を変化させた信号のバイナリとのにらめっこになります。
適当にやってもらちが明かないので、ざっくりと分かりやすいところからこんな順番であたりをつけていきます。

  1. 電源ON/OFF
  2. 単一モードで温度切り替え
  3. 運転モード切替
  4. 風向風量フラグ

以下の結果は電源のON/OFFを示す制御信号をデコードした結果を並べたものです。
Frame2のData3において、下位4ビットが電源OFF時は1000、電源ON時は1001へ変化していることがわかります。

 $ paste <(cat commands | jq '."air:off" | .[]' | python3 aehadump.py -t 435 -v) <(cat
commands | jq '."air:on" | .[]' | python3 aehadump.py -t 435 -v)
Frame 0 Frame 0
Customer Code: 1101101000010001 Customer Code: 1101101000010001
Parity: 0111    Parity: 0111
Data00: 0010    Data00: 0010
Data01: 00000000        Data01: 00000000
Data02: 11000101        Data02: 11000101
Data03: 00000000        Data03: 00000000
Data04: 01000000        Data04: 01000000
Data05: 00010111        Data05: 00010111
Frame 1 Frame 1
Customer Code: 1101101000010001 Customer Code: 1101101000010001
Parity: 0111    Parity: 0111
Data00: 0010    Data00: 0010
Data01: 00000000        Data01: 00000000
Data02: 01000010        Data02: 01000010
Data03: 00000000        Data03: 00000000
Data04: 00000000        Data04: 00000000
Data05: 01010100        Data05: 01010100
Frame 2 Frame 2
Customer Code: 1101101000010001 Customer Code: 1101101000010001
Parity: 0111    Parity: 0111
Data00: 0010    Data00: 0010
Data01: 00000000        Data01: 00000000
Data02: 00000000        Data02: 00000000
Data03: 01001000        Data03: 01001001
Data04: 00101010        Data04: 00101010
Data05: 00000000        Data05: 00000000
Data06: 10101111        Data06: 10101111
Data07: 00000000        Data07: 00000000
Data08: 00000000        Data08: 00000000
Data09: 00000000        Data09: 00000000
Data10: 00000000        Data10: 00000000
Data11: 00000000        Data11: 00000000
Data12: 00000000        Data12: 00000000
Data13: 11000000        Data13: 11000000
Data14: 00000000        Data14: 00000000
Data15: 00000000        Data15: 00000000
Data16: 11110011        Data16: 11110100

このような形で最低限制御に必要な部分だけ抽出していくと以下のようになりました。

Frame0

基本的に固定

Frame1

基本的に固定

Frame2

Data 内容
00 固定(0010)
01 固定(0)
02 固定(0)
03 上位4bits: 運転モード, 下位4bits: 電源
04 温度情報
05 固定(0)
06 上位4bits: 風量 / 下位4bits: 風向なし:0000, 風向上下:1111
07 固定(0)
08 入タイマー
09 上位4bits: 入りタイマー, 下位4bits: 切りタイマー
10 切りタイマー
11 固定(0)
12 固定(0)
13 固定(11000000)
14 固定(0)
15 固定(0)
16 チェックサム

チェックサムを調べる

さて、最後にチェックサムがどのように算出されているかを調べてフィニッシュです。
ここは規格で決まっているわけではないので完全に実装依存なのが憎たらしいですが仕方ないですね。
結論から言うと今回のリモコンではLeaderに続くデータを8bitごとに分割したものの総和の下位8ビットがチェックサムになっていました。

以下のコードを上記のaehadump.pyの末尾に追記して実行することで確認を行いました。

print(f"sum: {bin(sum([datum(c) for c in list(chunks(frame[24:], 8))[:-1]]))}")

総和を算出するところまではよかったんですが、途中でチェックサムも総和に加えてしまったおかげで「チェックサム全然わからなくない??」と言いながらすっかりハマってしまいましたが……。

まとめ

エアコンのリモコン信号をリバースエンジニアリングしました。

ここで解析した結果から信号を組み立てていけば、自身でエアコンのリモコンのような振る舞いをさせるものを作れますね。
以上の解析結果に基づいて実際にエアコンを制御するAPIを作成しました。作成したAPIはこちらの記事で紹介していますのでご覧ください。

Leave a Comment

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です