この記事は「Raspberry Piでスマートルームをつくる」の一部です。
- 設計・部品調達編
- 動作テスト編
- エアコンのリモコン信号解析編←イマココ
- 基板実装編
- API/chatbot実装編
今回は自室にあるエアコンを操作するリモコンの信号を丸裸にしていきます!
なんでそんなことするの?
自分で赤外線信号つくって操作したいからです!おうちハックしたいからです!!!
我が家のエアコンを紹介するぜ!
今回リバースエンジニアリングの犠牲対象になるのはこちらのリモコンでございます。
2004年製造のダイキン製AN22EDS-Wに対応したリモコンです。
赤外線リモコンの信号仕様
赤外線受信モジュールの組み立てで製作したモジュールを使って実際に信号を解析していきます。
赤外線を用いたリモコンでは、制御信号を0
と1
のパルスに変換してこれを高周波の搬送波で変調したものを送信しています。
詳しくはChaN氏による赤外線リモコンの通信フォーマットが大変参考になります。
この文献によれば、エアコンのリモコン信号は大きく
- NECフォーマット
- 家製協(AEHA)フォーマット
- SONYフォーマット
の3つに分けられます。
このうちNEC, AEHAフォーマットでは各ビットのデータは以下の図に示すようなフォーマットになっています。
リモコン信号のデータ表現
赤外線リモコンの通信フォーマットより引用
パルスが立っている間は無駄な電力消費を抑えるため変調された信号が出力されていることがわかります。
以下はAEHAフォーマットの信号定義ですが、開始を示すLeaderに続いて各種データが定義されています。
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"
以上のスクリプトを実行すると以下のような結果が得られます。
この結果より、このリモコンは1回の操作につき3つのフレームを送っていることがわかります。さらに拡大してみると、フレームの先頭にはきちんとLeaderが存在していることも確認できます。
先頭にはフレームではないパルスが出力されていますが、信号を実際にエミュレートしたところこの部分を省いてフレーム部分から送信しても動作したので必ずしも必要な信号ではないっぽいです。
フォーマットを調べる
それでは信号の詳細な解析を進めていきたいと思いますが、まずはこのリモコンが先の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
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の時間データをデコードするスクリプトがこちらです。
ここからは地道に状態を変化させた信号のバイナリとのにらめっこになります。
適当にやってもらちが明かないので、ざっくりと分かりやすいところからこんな順番であたりをつけていきます。
- 電源ON/OFF
- 単一モードで温度切り替え
- 運転モード切替
- 風向風量フラグ
以下の結果は電源の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はこちらの記事で紹介していますのでご覧ください。