家庭でIoTを実現しよう(Rasberry Pi3 & Jetson Nano)

IoTの恩恵を受けたいけど家庭用はなかなか出てこない。なら作ろうというブログ

Rasberry Pi 3でリモコン信号を解析する (10/1リモコン信号解析結果更新)

目的

多くのサイトで送られた信号を記録してそのまま出力するといったものが見られますがこれだと多くのパターンを記録させなければならないので非効率です。
今回、エアコンに設定したい状態があった場合にその状態に変更するための信号を生成できるようにリモコンの信号生成ルールを明らかにします。

リモコンについて

今回使用したリモコンについて紹介します

概要

NationalエアコンのA75C3026
パナソニック ナショナル エアコンリモコン A75C3026
部屋に備え付けのエアコンのリモコンです。

ボタン

ボタン名 動作
運転ON/OFF 電源ON/OFF状態のトグル
温度UP 設定温度を1度あげる
温度DOWN 設定温度を1度下げる
運転切替 自動/暖房/冷房/除湿の順に切り替え
風量 自動/静か/弱~強4段階で順番に切り替え
風向 自動/下~上を4段階で順番に切り替え
におい除去 長押しでにおい除去モードに切り替え
切タイマ 電源ON時に0.5,1,2,3,4,5,6,7,8,9,10,11,12hの順でタイマの時間を変更
入タイマ 電源OFF時に0.5,1,2,3,4,5,6,7,8,9,10,11,12hの順でタイマの時間を変更
予約 切タイマ,入タイマで設定した時間を送信する
取消 予約の取消
パワフル パワフルモードに切り替え
メニュー エアコンへの信号は特になし

信号解析

次にこのリモコンの信号を解析します

信号フォーマット

リモコンの信号は以下のサイトで詳しく解説されています。
赤外線リモコンの通信フォーマット
大きく分けて以下の3つのフォーマットに分類されるそうです。

今回使用するNational(Panasonic)は2番目の家製協フォーマットを使用しています。
家製協フォーマットについて詳しく説明していきます。

赤外線の信号は一定時間T(数100μsec)を1単位としてON/OFFが何単位時間の長さかで情報を表現しています。
前回紹介したリモコンの信号を取得する"mode2"の出力におけるpulseがON spaceがOFFに対応しています。

家製協フォーマットではON→OFFの長さの組み合わせで以下のような意味を持ちます。

ON OFF 意味 詳細
8T 4T Leader Frameの始めを意味する
1T 1T 0(bit) bitにおける0を意味する(DataBit)
1T 3T 1(bit) bitにおける1を意味する(DataBit)
1T >=800ms Trailer Frameの終了を意味する

つまり1Tの長さのOnの後にOFFの信号が3T続くと"1"を意味するということです。

さらに、信号の構成について見ていきます。以下は信号を含んだFrameと呼ばれるブロックをしめしています。

Leader Customer Code1 Customer Code2 Parity Code Data0 Data1 ・・・ DataN Trailer
8bit 8bit 4bit 4bit 8bit ・・・ 8bit

さきほど出てきたLeaderというのはFrameの開始を示す信号です。これによって受信側はこれからデータが来ることがわかります。
この後にDataBitフォーマットで4bitもしくは8bitのデータが順番に二進数で表現されて続きます。
一般的に最初の二つの8bit信号はカスタマーコードと呼ばれ固有のコードが格納されています。その後4bitがparityコードと呼ばれるカスタマーコードのエラーチェック部になるようです。
次に4bitのデータ0のデータコードが格納された後、8bit単位でデータコードが格納されます。
そしてFrameの最後にはTrailerと呼ばれる信号が格納されて終了となります。

リモコンによってキーを押している間、ずっとframeが繰り返されたりリピート信号が送信されたりするそうなのでこのあたりは実際に信号を確認してみないとわかりません。

以上が信号フォーマットについてになります。

信号データの保存

次に受信した信号を4bitもしくは8bitのコードに変換して人間が読めるような形式に変換します。

まず信号をファイルに保存します。

$mode2 -d /dev/lirc0 | tee ファイル名

このコマンドを実行した後に、センサにリモコンを向けて信号を送信します。
いくつかの信号を送ったらctri+cで処理から抜けます。
すると指定したファイルに以下のように信号が記録されます。

space 4121433 
pulse 142 
space 2075 
pulse 145 
space 111986 
pulse 136 
space 12832 
pulse 142 
space 190883 
pulse 139 
space 1998 
pulse 287
space 272314
pulse 142
space 2031
pulse 235

mode2コマンドの出力がそのままファイルに保存されています。
これで信号出力を保存できました。

単位時間Tの取得

しかしこのままでは人間が理解できる形式ではないので分析できません。

分析するためにはまず単位時間Tを調べる必要があります。
家製協フォーマットはPuiseの長さが1Tと8Tしかありません。1TがFrameに大量に現れますが8TはFrameの最初1回しか現れません。
なので頻度分布を調べて最も頻度が高い値が単位時間Tになるはずです。

なのでまず保存したファイルの中のpulseで始まる行に書かれた値のヒストグラムをとってその最大となる範囲の値を平均化してTを求めます。

今回ヒストグラムは100μsec単位で0~10msecのレンジまでを解析するようにします。

hist = [0] * 100
sum = [0] * 100
for row in reader:
	#make histgram
	length = int(row[1])
	if row[0] == 'pulse' and length <= 10000:
		hist[length/100] = hist[length/100] + 1
sum[length/100] = sum[length/100] + length

hist配列に頻度、sum配列に平均値演算用の和を格納していきます。indexは2列目/100とすることで200から299まではindex=2というように簡単に振り分けができます。

index = hist.index(max(hist))
T = sum[index]/hist[index]

配列作成が終わったら最大頻度のindexを探してそのindexでの平均値を出力します。
この時得られたTは今回476となりました。

tmp_hist=[]
for x in range((index-1)*8,(index+1)*8+1):
	tmp_hist.append(hist[x])
tmp_index = tmp_hist.index(max(tmp_hist))
index = hist.index(tmp_hist[tmp_index])
T8 = sum[index]/hist[index]

次に8Tを探します。
8Tがあるであろうindex周辺でもっとも頻度が高いindexを探してそのindexの平均値を出力します。
この時得られた8Tは3531になりました。

Tをそのまま8倍にしても8Tと同じ時間になりません(476*8 = 3808 > 3531)
これは信号の立上がりと立下りに検出の遅れが発生しているためと推定されます。
なので今回検出されたTと8Tは真の単位時間{T_{true}}と検出遅れ{\alpha}を用いて表すと
{ 
 T = T_{true} + \alpha \\
 8T = 8T_{true} + \alpha
}
よって{T_{true}}はこの二つの差から求められます。

true_T = (T8-T)/7

これで単位時間Tが取得できます。
このコードでリモコンの出力する単位時間Tは436となりました。

以下に検出プログラムを公開しています。家製協フォーマットであれば以上のロジックで単位時間Tを検出できると思います。
https://github.com/kagemomiji/raspi3/blob/master/ir_detect_T.py
使用方法は以下になります。

 $python ir_detect_T.py ファイル名

信号のデコード

単位時間Tがわかりましたので次はpulseとspaceの信号を人間が読めるようにデコードします。

処理の流れとしては以下のようになります。

  1. mode2コマンドで取得したファイルを一行ずつ読み込む
  2. 各信号の長さを単位長さの何倍か計算する
  3. Leaderを検出する
  4. 1TのPulseを検出してその後のSpaceの長さで信号を判定する
  5. データ毎にビットを格納してビット反転を行う
  6. Frame配列にデータを格納する
  7. Frame配列に格納されたデータを16進数に変換して出力する。

順番に見ていきます

#detect pulse length
length = int(round(int(row[1])/T))

まず単位時間に対して何倍の長さかを検出します。
row[1]はmode2の出力の2列目(数値の部分)です。
最後のintが冗長な気もしますがこれでlengthに何倍という情報が格納されます。

if row[0] == 'pulse' and length == 8:
		leader_flag = 1	#8length pulse detect
	if row[0] == 'space' and length == 4 and leader_flag == 1:
		leader_flag = 2 #leader_detect
		data_counter = 0
		frame = []
		datum = 0
		pulse_flag = 0

次にleaderを検出します.pulseの長さが8Tの後にspaceの長さが4Tであればleaderと検出して以降の信号をFrameの信号という判定leader_flag = 2としています。
また、leaderの時に各変数を初期化しています。

if row[0] == 'pulse' and length == 1:
	data_counter = data_counter + 1
	pulse_flag = 1
if row[0] == 'space' and pulse_flag:
	#print "%d,%d" % (data_counter, length)
	#pulse_flag off
	pulse_flag = 0
	
	#detect bit on/off/trailer
	if length == 1:
		bit = 0
	elif length == 3:
		bit = 1

datum = datum << 1 | bit

1Tのpulseの後にspaceがあればそれを信号と判定してspaceの長さによってbitの値を判定します。
1Tなら0、3Tなら1です。
最後にdatumにbitシフトさせながら格納していきます。

			 
#detect 4bit datum(parity, data0)
if data_counter == 20 or data_counter == 24:
	frame.append(rev_bit.reverse_bit4(datum))	
	datum = 0
#detect 8bit datum(customer, data1~)	
elif data_counter % 8 == 0:
	frame.append(rev_bit.reverse_bit8(datum))
	datum=0

datumに格納されたデータをparityコードやdata0コードでは4bit毎にその他コードでは8bit毎にLSB/MSB反転を行います。
なぜならリモコン信号のデータ並び順はD0,D1,D2,D3....D7と下の桁から順番となっています。しかし数値処理する場合それは逆でD7,D6,...D1,D0というように並んでいなければなりません。
そのためLSB/MSBを反転する処理が必要です。
各コードごとに反転したコードをframe配列に順番に格納し行きます。

data_str = [hex(x) for x in frame]
writer.writerow(data_str)

最後にcsvに対してframeに格納されたデータを16進数表記に変換して信号を出力します。

以下に私の書いたサンプルコードがありますのでよければそちらをご確認ください。
https://github.com/kagemomiji/raspi3/blob/master/decode_ir.py

使い方は

$ python decode_ir.py 入力ファイル名

とすると "ファイル名_format.csv"として出力されます
このコードによってデコードされた我が家のエアコンのリモコン信号が以下になります。

0x2,0x20,0x0,0xe,0x4,0x0,0x31,0x3a,0x80,0xaf,0x0,0x0,0x6,0x60,0x0,0x0,0x80,0x0,0x16,0x9c
trailer
0x2,0x20,0x0,0xe,0x4,0x0,0x0,0x0,0x6
0x2,0x20,0x0,0xe,0x4,0x0,0x31,0x3c,0x80,0xaf,0x0,0x0,0x6,0x60,0x0,0x0,0x80,0x0,0x16,0x9e
trailer
0x2,0x20,0x0,0xe,0x4,0x0,0x0,0x0,0x6
0x2,0x20,0x0,0xe,0x4,0x0,0x31,0x3c,0x80,0xaf,0x0,0x0,0x6,0x60,0x0,0x0,0x80,0x0,0x16,0x9e
trailer
0x2,0x20,0x0,0xe,0x4,0x0,0x0,0x0,0x6

一つのボタンを押すと3行ずつがセットで出力されているようです。
状態信号+trailer+識別信号のような順番で出力されているようです。

信号対応付け

信号を分析できる形式で見ることができるようになりました。
ここですべてのボタンを押していきそれぞれの状態がどこにどのように割り当てられているのか分析していきました。
短い式別信号のような信号は基本的に固定されていたので長いほうの状態信号を調べました。
Dataコードの結果は以下のようになりました。

0 固定(4bit)
1 固定
2 固定
3 モード(上位4bit) 電源ステータス(下位4bit)
4 温度の2倍(8bit)
5 固定
6 風量(上位4bit) 風向(下位4bit)
7 固定
8 ONタイマの時間上位8bit
9 ONタイマの時間下位8bit/OFFタイマの時間上位8bit
10 OFFタイマの時間下位8bit
11 乾燥機能ON/OFF(上位7bit目)/静モード(上位6bit目)/パワフルモード/通常(下位4bit)
12 固定
13 固定
14 固定
15 におい防止 ON/OFF
16 カスタマーコードからデータコードを8bit単位にしたものの総和のうち下位8bit

最後のデータビットの解析に手間取りました。CRCチェックやXORなどを考えたのですが一番単純な総和でした。
データコードの12~14はもしかするとためしていないボタンがいくつかあるのでそれに割り振られているかもしれませんが
今回は解析をここで終了したいと思います。

10/1追記分
11バイト目と15バイト目について修正加筆しました。
メニューから設定される以下の設定信号について追加しました。
11バイト目は冷房、除湿機能時に有効になる乾燥機能
15バイト目はON/OFF機能だと考えていましたが、実際は冷房・除湿機能時に有効になるにおい防止
になります。
なかなかリバースエンジニアリングは奥が深いです。

まとめ

今回は家にあるエアコンのリモコン信号を取得して、家製協フォーマットの信号を解析しました。
そして信号の単位時間Tを求めて、人間が読める形に信号を変換して分析を行いました。
次はこの解析した信号をもとに信号を生成して出力することを目指します。