Pythonでエアコンのリモコン信号をデコードしパースする
この記事は、CAMPHOR- Advent Calendar 2023の4日目の記事です。
TL;DR
- ラズパイを使って、簡単なスマートリモコン[1]を作った
- その際にエアコンのリモコン信号を解析したが、以外と大変だった
- 本記事ではリモコン信号をフォーマットに従いデコードしパースするところまでを説明
はじめに
今年の夏は、めちゃくちゃ暑かったですね。帰宅前にエアコンをつけておきたいと思うことが多かった[2]ので、スマートリモコンを作りました。
製作物の概要としては、SlackからエアコンON
とか温度上げて
みたいなメッセージを送ると、ラズパイに接続された赤外線LEDが信号を送信し、エアコンを操作するというものです。
本記事では、その際にエアコンのリモコン信号を解析した話をしようと思います。(記事が長くなりそうだったので、まずは信号をデコードしパースするところまで)
リモコン信号の読み取り
Pythonで赤外線信号の読み取り・送信を行うためのスクリプトとして、irrp.py
が有名です。
今回は、以下の記事を参考にirrp.py
を他のPythonスクリプトから呼び出しやすいように改変したものを使用しました。[3]
irrp.py
(改変版)を用いると、以下のようなPythonコードでリモコン信号を読み取ることができます。
from irrp import IRRP
JSON_PATH = "code/mitsubishi_aircon.json"
POST_TIME = 130 # 信号が終了したと判断する時間(単位はms)
RECV_PIN = 18 # ラズパイに接続した赤外線受信モジュールのGPIOピン番号
ir = IRRP(file=JSON_PATH, post=POST_TIME, no_confirm=True)
ir.Record(GPIO=RECV_PIN, ID="ac:16")
ir.stop()
このコードを実行し、赤外線受光モジュールに向けてリモコンのボタンを押すと、以下のようなJSONファイルが出力されます。
{"ac:16": [3446, 1652, 477, 1218, 477, 1218, 477, 368, 477, 368, 477, 368, 477, 1218, 477, 368, 477, 368, 477, 1218, 477, 1218, 477, 368, 477, 1218, 477, 368, 477, 368, 477, 1218, 477, 1218, 477, 368, 477, 1218, 477, 1218, 477, 368, 477, 368, 477, 1218, 477, 368, 477, 368, 477, 1218, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 553, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 1218, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 1218, 477, 1218, 477, 368, 477, 1218, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 553, 1218, 477, 1218, 477, 368, 477, 368, 477, 368, 477, 1218, 477, 1218, 477, 368, 477, 368, 477, 368, 477, 368, 553, 368, 477, 368, 477, 1218, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 553, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 553, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 553, 368, 477, 368, 477, 1218, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 1218, 477, 1218, 477, 368, 477, 368, 477, 368, 477, 1218, 477, 368, 477, 1218, 477, 12982, 3446, 1652, 477, 1218, 477, 1218, 477, 368, 477, 368, 553, 368, 477, 1218, 477, 368, 477, 368, 477, 1218, 477, 1218, 477, 368, 477, 1218, 477, 368, 477, 368, 477, 1218, 477, 1218, 477, 368, 477, 1218, 477, 1218, 477, 368, 477, 368, 477, 1218, 477, 368, 477, 368, 477, 1218, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 553, 368, 477, 368, 477, 368, 477, 1218, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 1218, 477, 1218, 477, 368, 477, 1218, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 1218, 477, 1218, 477, 368, 477, 368, 477, 368, 477, 1218, 477, 1218, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 553, 1218, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 1218, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 1218, 477, 1218, 477, 368, 477, 368, 477, 368, 477, 1218, 477, 368, 477, 1218, 477]}
このJSONファイルには、前から順に赤外線LEDの点灯時間と消灯時間 (単位はμs
) が交互に記録されています。
一般に赤外線リモコンでは、赤外線LEDの点灯時間と消灯時間の比によって、送信信号のbitの0/1を表現しています。そのため、読み取ったJSONファイルに記録されているLEDの点灯時間と消灯時間の比を調べ、まずはバイナリのデータにデコードすることが、赤外線リモコン信号の解析の第一歩となります。
赤外線リモコンの通信フォーマット
リモコン信号のバイナリデータを、赤外線LEDの点灯時間・消灯時間としてエンコードするフォーマットとして、主に以下の3種類が用いられています[4]。
- NECフォーマット
- AEHA[5]フォーマット
- SONYフォーマット
リモコン信号を解析するためには、まず解析対象のリモコンがどのフォーマットに準拠しているのかを調べる必要があります。
SONYフォーマットなどの独自フォーマットは、そのメーカーの製品のリモコンにしか使われていないため、すぐに判別できます。一方、NECフォーマットやAEHAフォーマットは、複数メーカーの製品のリモコンで用いられているため、送信信号のLeader部[6]を調べ、どのフォーマットに準拠しているのかを判別する必要があります。
実際に信号をデコードしてみる
それでは、実際にリモコンの信号を解析してみましょう。
今回は、三菱電機の霧ヶ峰のエアコン(型番:NA053)の信号を解読してみようと思います。
まず、リモコンの設定を
- 冷房16℃
- 風速:自動
- 風向(上下):自動
- 風向(左右):スイング
に設定した際の信号を、以下に示します。
{"ac:16": [3446, 1652, 477, 1218, 477, 1218, 477, 368, 477, 368, 477, 368, 477, 1218, 477, 368, 477, 368, 477, 1218, 477, 1218, 477, 368, 477, 1218, 477, 368, 477, 368, 477, 1218, 477, 1218, 477, 368, 477, 1218, 477, 1218, 477, 368, 477, 368, 477, 1218, 477, 368, 477, 368, 477, 1218, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 553, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 1218, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 1218, 477, 1218, 477, 368, 477, 1218, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 553, 1218, 477, 1218, 477, 368, 477, 368, 477, 368, 477, 1218, 477, 1218, 477, 368, 477, 368, 477, 368, 477, 368, 553, 368, 477, 368, 477, 1218, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 553, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 553, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 553, 368, 477, 368, 477, 1218, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 1218, 477, 1218, 477, 368, 477, 368, 477, 368, 477, 1218, 477, 368, 477, 1218, 477, 12982, 3446, 1652, 477, 1218, 477, 1218, 477, 368, 477, 368, 553, 368, 477, 1218, 477, 368, 477, 368, 477, 1218, 477, 1218, 477, 368, 477, 1218, 477, 368, 477, 368, 477, 1218, 477, 1218, 477, 368, 477, 1218, 477, 1218, 477, 368, 477, 368, 477, 1218, 477, 368, 477, 368, 477, 1218, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 553, 368, 477, 368, 477, 368, 477, 1218, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 1218, 477, 1218, 477, 368, 477, 1218, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 1218, 477, 1218, 477, 368, 477, 368, 477, 368, 477, 1218, 477, 1218, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 553, 1218, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 1218, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 368, 477, 1218, 477, 1218, 477, 368, 477, 368, 477, 368, 477, 1218, 477, 368, 477, 1218, 477]}
通信フォーマットの判別
上記の信号において、始めの点灯時間と消灯時間の組である、
[3446, 1652]
の部分がLeader部です。AEHAフォーマットの変調単位[7]
[8.10, 3.89]
となります。この値は、AEHAフォーマットのLeader部の長さである[
バイナリデータにデコードする
Leader部と同様に、Data部についても
[1.12, 2.87, 1.12, 2.87, 1.12, 0.87, ...]
となります。AEHAフォーマットにおいて、
[1, 1, 0, ...]
この作業を、信号全体に対して行うことで、リモコン信号のバイナリデータが得られます。もちろん手作業だと大変なので、以下のようなPythonのコードを書きました。
# 入力された赤外線LEDの点灯・消灯時間を変調単位で割り、正規化する
def normalize(code, T=425):
normalized_code = []
frame = []
for i in range(0, len(code)):
period = round(code[i] / T)
# Trailer(Frame終端)の場合
if period > 8:
normalized_code.append(frame)
frame = []
else:
frame.append(period)
normalized_code.append(frame)
return normalized_code
# AEHAフォーマットに従って、信号をバイナリデータにデコードする
def decode_to_binary(normalized_code):
block = []
data_frame = ""
for frame in normalized_code:
for i in range(0, len(frame) - 1, 2):
# Leader
if frame[i] == 8 and frame[i + 1] == 4:
continue
# 0 (Data bit)
elif frame[i] == 1 and frame[i + 1] == 1:
data_frame += "0"
# 1 (Data bit)
elif frame[i] == 1 and frame[i + 1] == 3:
data_frame += "1"
else:
raise ("Unabled to decode.")
block.append(data_frame)
data_frame = ""
# 全てのフレームが同一であるかを確認
assert all(
df == block[0] for df in block
), "Each data frame is not identical."
return block[0], len(block)
このコードを用いると、上記のリモコン信号は以下のバイナリデータにデコードされます。
110001001101001101100100100000000000000000000100000110100000000001100011000000100000000000000000000000000000000000001000000000000000000011000101
バイナリデータをパースする
AEHAフォーマットにおいて、Data部のバイナリデータは最下位bitから上位bitの順に並んでいます。[8]
AEHAフォーマットの概要 (http://elm-chan.org/docs/ir_format.html より引用)
また、Data部は以下のようなフィールドから構成されています。
名称 | データ長 | 概要 |
---|---|---|
Customer Code1 | 8bit | メーカーの識別に用いられる |
Customer Code2 | 8bit | 同上 |
Parity | 4bit | 誤り訂正用 (Customer Codeを4bitごとにXORした値) |
Data0 | 4bit | |
Data1 | 8bit | |
Data2 | 8bit | |
︙ | ||
DataN | 8bit |
ここで、データ長が4bitのフィールドがあることに注意する必要があります。
バイナリデータのパース結果は、見やすさのために16進表記することが多いですが、単に前から8bitずつパースして、その結果を16進表記すると、ParityフィールドとData0フィールドの順序が逆になってしまいます。[9]
以上の点を考慮すると以下のようなコードを用いることで、バイナリデータをパースすることができます。
def parse_binary_code(binary_code):
customer_code1 = int(binary_code[7::-1], 2)
customer_code2 = int(binary_code[15:7:-1], 2)
parity_code = int(binary_code[19:15:-1], 2)
data0 = int(binary_code[23:19:-1], 2)
binary_code = binary_code[24:]
data = []
while len(binary_code) > 0:
data.append(int(binary_code[7::-1], 2))
binary_code = binary_code[8:]
data.insert(0, data0)
frame = {
"customer_code1": customer_code1,
"customer_code2": customer_code2,
"parity_code": parity_code,
"data": data,
}
return frame
実際に用いたパース方法
先程、バイナリデータを単に前から8bitずつパースすると、ParityフィールドとData0フィールドが逆になってしまうと説明しました。しかし、実装の簡単さを優先し、実際はこの方法を採用しました。
def parse_binary_code_as_hex(binary_code):
hex_code = ""
while len(binary_code) > 0:
hex_code += "{:02x}".format(int(binary_code[7::-1], 2))
binary_code = binary_code[8:]
return hex_code
この方法だと、パース結果のParityとData0は入れ替わることになりますが、信号生成時に上記の逆の操作(hex2桁を2進数に変換し、LSB→MSBの順に並べる)を行えば、最終的に生成されるバイナリデータでは、正しくParityとData0が配置されます。
これを実行すると、リモコン信号のパース結果が、以下のような16進数として得られます。
23cb260100205800c64000000000100000a3
上記のような方法で、リモコンの各ボタンを押した際の信号を記録し、16進数としてパースすれば、リモコン信号を再現できそうだと思うかもしれません。しかし、この方法ではエアコンのリモコン信号を完全に再現することができません。この理由については、次の記事で説明しています。
-
簡単にいえば、スマートフォンからエアコンを操作できるようにするデバイス。市販品としては、Switchbot や Nature Remo あたりが有名。 ↩︎
-
帰宅時の部屋の室温が33℃とかになっていた ↩︎
-
記事を書いてる途中に調べてたらPyPiに
irrp
というパッケージがあった。こっちの方がコマンド一発で入るので良さそう ↩︎ -
もちろん例外もあり、例えばJVCは独自フォーマットを採用している。また、海外メーカー製のリモコンだと、別のフォーマットが用いられていることが多い(ちゃんと調べてない)。 ↩︎
-
フレーム(送信信号の1単位)の開始を表す箇所。1つのフレームは、Leader部, Data部, Trailer部からなり、これが送信信号の1単位となる。多くのリモコンでは、同じフレームを複数回送信し、信号を確実に送信できるようになっている。 ↩︎
-
赤外線LEDを点灯・消灯させる時間の最小単位。AEHAフォーマットでは、
の範囲で設定でき、T= 350\sim 500\,\rm{[\mu s]} あたりがよく用いられる。 ↩︎425\,\rm{[\mu s]} -
AEHAフォーマットに限った話ではなく、NECフォーマットやSONYフォーマットでもLSB→MSBの順に並んでいる ↩︎
-
後述しますが、このことを理解した上で、単に前から8bitずつパースする手抜き実装を採用しました ↩︎
Discussion