新しくしたエアコンをpigpioで操作しようとしたらchain is too longと怒られたので信号を圧縮する

信号が送信できない

先日投稿したようにエアコンを買い替えたのですが、新しくなったリモコンの信号を解析して遠隔操作できるようにしようとしたらこんなエラーが。

Bottle v0.12.16 server starting up (using WSGIRefServer())...
Listening on http://0.0.0.0:8888/
Hit Ctrl-C to quit.

Traceback (most recent call last):
  File "/home/pi/.local/lib/python3.5/site-packages/bottle.py", line 862, in _handle
    return route.call(**args)
  File "/home/pi/.local/lib/python3.5/site-packages/bottle.py", line 1740, in wrapper
    rv = callback(*a, **ka)
  File "server.py", line 29, in aircon
    IRClient.send(code, IR_WRITE_PIN, con.carrier_freq)
  File "/home/pi/repos/myroom/lib/ir_client.py", line 54, in send
    pi.wave_chain(wave)
  File "/home/pi/.local/lib/python3.5/site-packages/pigpio.py", line 2475, in wave_chain
    self.sl, _PI_CMD_WVCHA, 0, 0, len(data), [data]))
  File "/home/pi/.local/lib/python3.5/site-packages/pigpio.py", line 970, in _u2i
    raise error(error_text(v))
pigpio.error: 'chain is too long'
192.168.11.11 - - [22/Aug/2019 13:48:16] "PUT /aircon HTTP/1.1" 500 38

pigpio曰く「chain is too long」。
信号送信で借りているirrp.py内のwave_chainメソッドでコケているみたいです。
自分が書いた領域でもないしどうしょうもなくない?と思いながら公式リファレンスwave_chainを調べるとこんな記述が。

The code is currently dimensioned to support a chain with roughly 600 entries and 20 loop counters.

……、chainは600エントリまでしかサポートしてないよ

改めて生成した信号を調べてみるとエントリ数が631になっていました。完全に超過してる。

今までは普通に使えてたよね?
前のエアコンで数えてみたら571エントリだったんだよね……。
なんと……

エアコンを買い替えてリモコンも新しくなった以上どうすることもできなくなってしまいました。
でもリモコンに縛られることなくエアコンを使いたい……!

ループコマンド……?

先のリファレンスをよく見てみると、ループコマンドなるものがあることがわかりました。

A blocks of waves may be transmitted multiple times by using the loop commands. The block is bracketed by loop start and end commands. Loops may be nested.

コマンド エントリ 説明
Loop Start 255 0 ブロックの開始
Loop Repeat 255 1 x y x + 256y回のループ

つまり、

1, 4, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 3

というデータに対して1, 2の繰り返しに注目し、

1, 4, 255, 0, 1, 2, 255, 1, 5, 0, 1, 3

と書き換えることができるということになります。

実際にwave_chainに渡されるリストをprintしてみるとこんな感じで、いかにも圧縮しがいがありそうな並びをしています。

[0, 1, 2, 3, 2, 4, 2, 4, 2, 4, 2, 3, 2, 4, 2, 4, 2, 4, 2, 4, 2, 3, 2, 4, 2, 3, ...

圧縮圧縮信号を圧縮

考察

というわけでこのデータを圧縮していきたいと思うわけですが、肝心なのはアルゴリズムです。
今回はちょっと遅くてもいいから愚直な実装で進めていきます。

先ほど述べた通り、ループコマンドではあるパターンが何回繰り返すかを記述します。
しかし、ループコマンドではパターンを除くヘッダとして最低でも1回につき6エントリを消費する計算になります。(255, 0255, 1, x, yで計6エントリ)

したがって、繰り返されている信号の長さ(パターン長と繰り返し回数の積)が6以下のものを圧縮しても意味がありません。

ループのヘッダ分で逆にエントリ数が増えちゃうからね

また、あるパターンが頻出してもそれらが散らばっている場合は圧縮の効果が見込めません。
逆に連続して繰り返されるエントリにループコマンドを適用すれば大きな効果が期待できます。

これより、長い連続パターンをループコマンドで短く記述することが目標になります。

実装

以上の考察に基づいて実装に入ります。
信号が表現されたリストを入力として、そのリストを書き換えるメソッドを書いていきます。

まずは頻出するパターンを調べるために、入力リストの部分列をいわゆるN-gramで取得します。

N-gramは自然言語処理でよく使われる手法で、テキストを連続するN個の文字や単語を単位として順次切り出した集合です。
リスト[0, 1, 2, 3]に対してN = 2(bigram: バイグラム)とした場合は[[0, 1], [1, 2], [2, 3]]と求められます。

Nの値をどのようにとるかでこの後の計算量が変わってきますが、少なくとも1ビットの信号を送信するために2エントリが必要であることから最小値は2とし、偶数のみをとることにします。

ん?どういうこと?
信号のON/OFFでは必ず別の値が割り当てられるから、リストの中に奇数個単位のループは存在しないってこと。

また、エアコンのリモコン信号をダンプしてみると以下のように同じデータが連続することが多く、Nをあまり大きくする必要はないと考え最大値として8をとることとしました。

Frame 0
Customer Code: 1101101000010001
Parity: 0111
Data00: 0010
Data01: 00000000
Data02: 00000010
Data03: 00000000
Data04: 00000000
Data05: 00000000
Data06: 00000000
Data07: 00110101
Data08: 00000000
Data09: 00000000
Data10: 00010000
Data11: 00000000
Data12: 00010000
Data13: 00000000
Data14: 00000000
Data15: 00000000
Data16: 00000000
Data17: 01101001
Frame 1
Customer Code: 1101101000010001
Parity: 0111
Data00: 0010
Data01: 00000000
Data02: 00000000
Data03: 00001001
Data04: 11000000
Data05: 10000000
Data06: 10100000
Data07: 00000000
Data08: 00000000
Data09: 00000110
Data10: 01100000
Data11: 00000000
Data12: 00000000
Data13: 11000011
Data14: 00000000
Data15: 00000000
Data16: 00100100

続いて取り出したN-gramに対して、そのパターンが出現する場所と繰り返す回数を算出します。
これは開始位置をずらしながらパターンと入力を比較し、一致しなくなるまで先を読みながらカウントすればよいです。
このようにして(開始位置, パターン長, 繰り返し回数)の3パラメータからなるブロック情報を得ます。

例えば以下のリストに対して[1, 2]を探した場合は[(2, 2, 8), (4, 2, 7), (6, 2, 6), (8, 2, 5), ...]が、[1, 2, 1, 2]を探した場合は[(2, 4, 4), (6, 4, 3), (10, 4, 2), (14, 4, 1)]が得られる感じです。

[1, 4, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 3, 1, 4]

このようにして得られたブロック情報こそが信号の圧縮に利用できる部分を示しています。次は複数あるブロック情報からどのブロックを圧縮対象にするかを選んでいきます。

とりあえず長いやつを圧縮すればいいんだよね?
同じ長さのブロックがあったらどうする?
ん、同じ長さならどれでもいいんじゃないの?

圧縮するブロックを選ぶ際にはもちろんブロック長(=パターン長と繰り返し回数の積)が長いものを優先して選び出すべきです。
一方でループコマンドの記述にはパターンを記述する必要があることから、可能ならば短いパターンを記述することでより効率が良くなります。
したがって、同じブロック長であればより短いパターンを選ぶことがベストです。

先のリストで(2, 2, 8)(2, 4, 4)を比べると、どちらもブロック長は16(= 2 \times 8 = 4 \times 4)ですが、パターン長がより短い前者を利用して圧縮する方が効率的であることがわかります。

ループコマンド自体は入れ子構造をサポートしていますが、圧縮時の処理が面倒そうになりそうなので重複区間にあるブロックは考えないことにします。
実際にブロックが重複している際にはブロック長が長いものを優先的に選び、可能な限り圧縮を行います。

以上のフィルタによって効率良く圧縮できるブロックを上位から最大で20個選び、リストの末尾に位置するブロックから順にループコマンドによる書き換えを行えば圧縮の完了です。
区間の重複は考えていないため、末尾から圧縮処理をしていけば開始位置の変動がなくブロック情報をそのまま利用できます。

以上のアルゴリズムを実装したソースが以下になります。

評価

エアコンのOFF信号に対して作成したアルゴリズムを適用し、その結果を確認します。
圧縮前の信号のエントリ数は631で以下のような感じです。長いですね。

[0, 1, 2, 3, 2, 4, 2, 4, 2, 4, 2, 3, 2, 4, 2, 4, 2, 4, 2, 4, 2, 3, 2, 4, 2, 3, 2, 3, 2, 4, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2,4, 2, 4, 2, 3, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 3, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 3, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2,4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 3, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2,4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 3, 2, 3, 2, 4, 2, 4, 2, 3, 2, 4, 2, 4, 2, 5, 0, 1, 2, 3, 2, 4, 2, 4, 2, 4, 2, 3, 2, 4, 2, 4, 2, 4, 2, 4, 2, 3, 2, 4, 2, 3, 2, 3, 2, 4, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 4, 2, 4, 2, 3, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2,3, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 3, 2, 3, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 4, 2, 3, 2, 4, 2, 3, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2,4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 3, 2, 3, 2, 4, 2, 4, 2, 4, 2, 4, 2, 3, 2, 3, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 3, 2, 3, 2, 4, 2, 4, 2, 3, 2, 3, 2]

実際に圧縮すると233エントリにまでサイズが小さくなり、圧縮前と比べてサイズが4割を切りました。

[0, 1, 255, 0, 2, 3, 2, 4, 2, 4, 2, 4, 255, 1, 2, 0, 2, 4, 2, 255, 0, 3, 2, 4, 2, 3, 2, 255, 1, 2, 0, 255, 0, 3, 2, 255, 1,4, 0, 4, 2, 4, 2, 3, 2, 255, 0, 4, 2, 255, 1, 11, 0, 3, 255, 0, 2, 4, 255, 1, 39, 0, 2, 3, 255, 0, 2, 4, 255, 1, 42, 0, 2, 3, 2, 255, 0, 4, 2, 255, 1, 36, 0, 3, 255, 0, 2, 3, 2, 4, 2, 4, 255, 1, 2, 0, 2, 5, 0, 1, 255, 0, 2, 3, 2, 4, 2, 4, 2, 4, 255, 1, 2, 0, 2, 4, 2, 3, 255, 0, 2, 4, 2, 3, 2, 3, 255, 1, 2, 0, 2, 3, 2, 3, 2, 3, 2, 4, 2, 4, 2, 3, 255, 0, 2, 4, 255, 1, 21, 0, 2, 3, 255, 0, 2, 4, 255, 1, 10, 0, 2, 3, 2, 3, 255, 0, 2, 4, 255, 1, 7, 0, 2, 255, 0, 3, 2, 255, 1, 5, 0, 4, 2, 3, 2, 4, 2, 3, 255, 0, 2, 4, 255, 1, 48, 0, 2, 3, 2, 3, 255, 0, 2, 4, 255, 1, 4, 0, 2, 3, 2, 3, 255, 0, 2, 4, 255, 1, 18, 0, 2, 3, 2, 3, 2, 4, 2, 4, 2, 3, 2, 3, 2]

無論ここまで小さくできればpigpioで信号を出力できます。

ちなみに実際に利用したブロック情報はこんな感じでした。

(470, 2, 48)
(150, 2, 42)
(70, 2, 39)
(237, 2, 36)
(370, 2, 21)
(582, 2, 18)
(47, 2, 11)
(414, 2, 10)
(326, 8, 2)
(2, 8, 2)
(438, 2, 7)
(310, 6, 2)
(346, 6, 2)
(21, 6, 2)
(453, 2, 5)
(570, 2, 4)
(33, 2, 4)

最大で96エントリのブロックが圧縮できているほか、パターン長は2が多くを占めていることがわかります。
複雑なパターンはあまり出現しておらず、シンプルな繰り返しが頻出しているようですね。

まとめ

pigpioで長すぎる信号のエントリ数を削減して「chain is too long」を回避するアルゴリズムを作成しました。実際のところリクエストから信号の送信までに1秒前後のラグが生じるようになりましたが、実用上は許容範囲かなという感じですね。打ち切れるループをちゃんと脱出すればもう少し早くなると思いますがPythonのループだとどんな記述が良いのかちょっとわからないので後回しにしています。

最初はブロック情報のソートはブロック長だけでやっていましたが、Pythonではsortedのラムダ式でタプルを返してやればThenByになるんですね(C#er並感)。これのおかげで若干ではあるもののエントリ数をさらに削減することができました。2回ソートかけないといけないのかと思いましたが思ったより簡単にできたので感動してます。

これでなんとかエアコンを買い替える前と同等の生活が送れるようになりました。

Leave a Comment

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