The Negligible Lab

Drowning in the sea of electrical and electronic circuits. Lost in the forest of computers and programs. 独立独歩を是とする。

Raspberry PiとFlaskでスマートリモコンを作る

はじめに

今回のテーマは「家族の役に立つ工作」です。

泊まりがけの旅行から帰宅すると,戸締まりした部屋の中は真夏であれば蒸し風呂のように,また,真冬であれば冷蔵庫のように感じられます。帰宅する数時間前に外出先からエアコンを運転できれば,家に着く頃には快適な温度になっていることでしょう。このような機能を持ったエアコンも製品化されているようですし,そうでなくても,赤外線リモコンの信号を利用してこのような機能を後付けするための「スマートリモコン」と呼ばれるデバイスが今日では低価格で手に入ります。

また,昨今では天井照明がかつてのような「ヒモ」(正式にはプルスイッチ)ではなく,リモコンで制御されることが多くなって参りましたが,これを留守中にも点灯・消灯することにより,あたかも人が在宅しているかの如く見せ掛けて空き巣防止を狙うこともできます。

今回は,勉強とリハビリを兼ねてRaspberry Pi Zero WHでエアコンと天井照明を制御するスマートリモコンを自作した経緯や結果について書きます。なお,有名なirrp.pyに頼るのではなく,赤外線リモコン信号を送信するためのPythonモジュールを自作しました(と言ってもpigpioモジュールには同じように依存します)。これによって,例えばエアコンの温度設定や運転モードの組み合わせに応じたフレームを生成して送信することができます(学習リモコンの場合,すべての組み合わせを予めひとつひとつ学習しておく必要がありますよね)。

また,インターネットからエアコンや天井照明を制御するため,Flaskと呼ばれるフレームワークを用いたWebアプリを作りました。

なお,Raspberry Piを使って学習リモコンやスマートリモコン,スマートハウスを作るという先行研究は非常に多いので*1,本記事のオリジナリティを問われると「ぐぬぬ…」となってしまいますが,敢えて言えば自作モジュールirxmit.pyの部分でしょうか。また,pigpioライブラリ(モジュール)のwaveform機能について基礎的な部分のみですが,和文で書いた記事は少ないのではないかと推測しています。

完成イメージ

まず,結論として完成したスマートリモコンのハードウェアとそのWebアプリのイメージを記載します。趣味なので仕様書のようなものは作っていませんが,作り始める前に思い描いていたものに近い形で実装することができました。

ハードウェア

図1に自作スマートリモコンHATを取り付けたRaspberry Pi Zero WHを示します。秋月電子通商Raspberry Pi Zero用ユニバーサル基板に手持ちの部品を実装して組み上げました。温湿度センサ,赤外線LEDとそれを駆動するパワーMOSFET,赤外線受光モジュールを搭載しております。思ったよりコンパクトに収まっておりますが,望ましくはOLEDディスプレイやキャラクLCDなど,表示デバイスを搭載できていたらと反省しております。温湿度センサの周りに抵抗器が密集しているのもあまりよろしくないですね。今年中にこれをプリント基板として作り直してみたいと密かに思っています。

f:id:s-inoue2010:20210406182828j:plain:w500
図1: 自作スマートリモコンHATを取り付けたRaspberry Pi Zero WH

Webアプリ

図2に完成したWebアプリのキャプチャ画面を示します。ブラウザ上での見た目はBootstrap 4によってそれっぽく仕上がっております。この裏ではFlaskを用いたPythonのプログラムや,自作の赤外線リモコン信号送信モジュールなどが動いております。また,温湿度のトレンドグラフは馴れ親しんだ(?)Matplotlibで描いております。なお,自宅の家電機器を赤の他人に勝手に制御されると困るので,一応はパスワードで保護しています。これはFlaskのsessionとcookieを使って実現しています。

f:id:s-inoue2010:20210407014509p:plain:w600
図2: Webアプリ画面キャプチャ

実際に動いている様子は動画の方が分かりやすいと思いますので,図3にYouTubeの動画を貼りつけます。

www.youtube.com
図3: FlaskによるWebアプリの動作の様子

おぉ…自分で作ったWebアプリとは思えないような美しさ…❗ 何もかもFlaskとBootstapのお陰ですね💦 たまに温湿度が欠測しているのが玉に瑕です。もう少し工夫が必要かもしれませんね。以下,開発の経緯や具体的な回路,ソフトの作り方について書いていきます。

目次

開発の経緯

7年の眠りから覚める休眠プロジェクト

我が家でも妻の実家から帰ってくると(大抵は2泊3日です),部屋が灼熱だったり極寒だったりという前述の問題が発生しており,これを解決するため,スマートリモコンを自作しようと赤外線LEDや赤外線受光モジュール,有線LANのためのRJ45コネクタなどの部品を集めておりました。しかし,それは2014年のこと──。当時はMbedでWebサーバを含めて実装しようと試みていましたが,その後は時間がとれなくなり*2プロジェクトは休眠状態となりました。それから時が流れること7年,ようやく電子工作を少しずつ再開できるようになり,今回に至りました。部品の大部分は7年の眠りから覚めて活用されています。7年前にも,この景色を俺は見ていたんだろうか。

pigpioライブラリとirrp.py

その後,前記事に記載の通り,Raspberry Piと再会し,そのGPIOを制御するライブラリであるpigpioをかじりました。前記事ではRaspberry PiでブラシレスDC (BLDC)モータをホールセンサあり120°通電制御した様子について述べています。

negligible.hatenablog.com

そのpigpioライブラリの公式サイトには,作者のjoan2937さんによるirrp.pyというPythonプログラムがあり,これはいわゆる学習リモコンの機能をpigpioとPythonで実現しているものです。

http://abyz.me.uk/rpi/pigpio/code/irrp_py.zip

このirrp.pyというPythonプログラムはRaspberry Piをスマートリモコンとして使おうという界隈では有名なようです。例えば私はじゃが いも(@takjg)さんのQiitaの記事を大いに参考にさせて頂きました。

qiita.com

ただし,irrp.pyは独立したプログラムとして使用することを想定しており,他のPythonプログラムからモジュールとして呼ばれるような使い方はできません*3。また,学習した信号をデコードしたりすることはできず,受信したパルスの長さをμs単位で記録しているだけです。インターネット上ではirrp.pyをモジュール化する試みもあるようですが,私は,赤外線リモコンの信号を送信するためのPythonモジュールirxmit.pyを自分で書くことにしました。これについては後述します。

Flaskというフレームワーク

外出先からエアコンや天井照明を制御するには,インターネットから赤外線リモコンを制御できる必要があり,そのためにはブラウザを介するWebアプリが好適なインタフェースと考えられます*4Raspberry Pi Zero WHは非常に小型でありながら無線LANを備えており,しかもLinuxが動いています。したがって,Mbedでは恐らく多くの試行錯誤*5を要したであろうWebサーバの構築が比較的容易に可能です。

初期の試作段階では,以下のような原始的な方法でインターネットからの制御を実現していました。まず,普通のPythonプログラムをCGIとして用い,print関数でformを含むHTMLを書き出します。form上のボタンが押されたらどのボタンが押されたのかをPOSTし,パラメータによって引数を変えつつirrp.pyをあくまでも外部プログラムとして呼び出します。

しかし,どうせPythonで制御するならばもっとスマートな方法があるのでは調べていると,BottleやFlask,Djangoといったフレームワークの存在を(不勉強ながら今さら)知りました。Djangoは非常に多機能とのことですが今回の目的には複雑すぎると思えたので,BottleかFlaskかとなりましたが,単純にFlaskの方が新しいということでFlaskを使ってみることにしました。

とは言っても,まったく触ったこともないので,右も左も分かりません。そこで,下記の「ゼロからFlaskがよくわかる本」で勉強させて頂きました。

www.amazon.co.jp

この本に沿ってPythonやHTMLを書いていくと,シンプルなブログアプリが完成します。スマートリモコンにはCRUD (create, read, update, delete)の機能は不要なので*6これを取り除き,代わりに赤外線信号の送信や温湿度センサとの通信を追加すれば,今回のスマートリモコンのためのWebインタフェースを実現できるのではないかと考えました。

本記事の構成

という訳で,スマートリモコンを作るにあたってはこの記事に至るまでに何段階かの試行錯誤がありました。そこで,本記事では以下のような順序でそれぞれを記述して参ります。

  1. 自作スマートリモコンHAT
  2. irrp.pyとリモコン信号の解析調査
  3. 自作モジュールirxmit.py
  4. cronによる温湿度の記録
  5. FlaskによるWebアプリ
  6. 試用結果

自作スマートリモコンHAT

回路設計

ハードウェアがないと始まりません。当初はブレッドボード上で部品とジャンパワイヤをいじくり回していましたが,秋月電子通商にてRaspberry Pi Zero用のユニバーサル基板を購入し,赤外線LED(点灯させるMOSFET回路を含む),赤外線受光モジュール,温湿度センサを載せた回路を設計しました。図4に回路図を,表1に部品表を示します。

f:id:s-inoue2010:20210406190027p:plain:w500
図4: 自作スマートリモコンHATの回路図(赤線部のGPIO19周りは設計ミスなので真似しないで下さい!)

表1: 自作スマートリモコンHAT部品表

記号値・型番備考秋月電子通商リンク
U1DHT22 (AM2302)温湿度センサhttps://akizukidenshi.com/catalog/g/gM-07002/
U2OSRB38C9AA赤外線受光モジュールhttps://akizukidenshi.com/catalog/g/gI-04659/
Q12SK4017パワーMOSFEThttps://akizukidenshi.com/catalog/g/gI-07597/
LD1, 2OSI5LA5113A赤外線LEDhttps://akizukidenshi.com/catalog/g/gI-07597/
R110 kΩプルアップ抵抗
R22.2 kΩゲート抵抗
R3220 kΩゲートプルダウン抵抗
R4, 547 Ω電流制限抵抗
C1, 20.1 μFパスコン

回路図自体は非常にシンプルです。必要な部品をRaspberry Piの電源やGND,GPIOに接続しているだけですね。ピンヘッダから取り出せる27個のGPIOのうち,使っているのは僅かに3つだけです。VDD = 5 Vの電源以外は2列ピンヘッダの一方も列からのみ取り出しております。温湿度センサDHT22はGPIO19に,2つの赤外線LEDはMOSFET 2SK4017を介してGPIO13に,赤外線リモコン受光モジュールOSRB38C9AAはGPIO10に接続しました。

いろいろ調べているとリモコン向けの赤外線LEDは僅かな時間しか点灯せず,また,遠くまで赤外線を届けるため,表示用のLEDに比べるとかなりの大電流で駆動するのが一般的なようです*7。本回路では,47 Ωの電流制限抵抗を直列接続した赤外線LEDを2つ並列接続しています。MOSFETの電圧降下を小さいとして無視し,赤外線LEDの順電圧降下を100 mAでのTyp.値の1.35 Vとすれば,電流は

 I_{\mathrm{LED}} = \displaystyle \frac{5~\mathrm{V} - 1.35~\mathrm{V}}{47~\Omega} = 77.7~\mathrm{mA} \tag{1}

となります。通常の表示用のLEDは概ね1 mA以下の電流しか流しませんので,77.7 mAはかなりの大電流です。しかし,この赤外線LEDの定格電流は連続100 mAなので,それでも定格内です。実際は100 mAが流れていないため,赤外線LEDの順電圧降下は1.35 Vより若干小さく,電流はもう少し大きくなるのではと推測します(データシートにV-Iカーブが記載されていないので詳細不明です)。 また,47 Ωの抵抗器の消費電力は47 Ω × (0.0777 A)2 = 0.283 Wとなります。連続ではないので,1/4 Wの抵抗器で十分に耐えられるでしょう(しかし,後述のように一度問題が起こりました)。

基板設計

プリント基板をKiCadやDesignSpark PCBを用いて設計し,中国の基板メーカーに外注する──。これも近日中にトライしてみたいですが,今回はさっと試すのが(少なくとも作り始めた時点での)スタンスだったので,秋月電子通商Raspberry Pi Zero用ユニバーサル基板で作ることにしました。

ユニバーサル基板の設計にはmotchyさんのmarmeloを使用させて頂きました。

motchy99.blog.fc2.com

図5,6は,marmeloで設計した基板にRaspberry PiのGPIOなどの注記を加えたものです。また,実際は四隅にM2.6のネジを取り付けるための穴が開いていたり,ピンヘッダからの信号を別のランドに引き出していたりするので,それを考慮した引き回しが若干面倒でしたね…💦

f:id:s-inoue2010:20210407014839p:plain:w450
図5: スマートリモコンHAT基板(表)

f:id:s-inoue2010:20210407014946p:plain:w450
図6: スマートリモコンHAT基板(裏)

ジャンパ線を極力使用しないように引き回してみましたが,後述のような設計ミスが相次いだため,あまり美しくはない配線となっています。また,ユニバーサル基板の常として,本来のランドとは1列ズレたところにハンダ付けしてしまうというプリント基板であればあり得ないミスが起こりがちです。それらを修正しながら実装したため,ハンダ面は醜く,とても人様に見せられるようなものではありません…💧

完成した基板が冒頭の図1となります。

設計ミスの数々💦

図4,5,6では温湿度センサDHT22の2番ピンをGPIO19に接続し,それを10 kΩで5 Vにプルアップしておりますが,この回路は誤っているので,もし皆さまご自身で試作されるのであれば,ここは3.3 Vに接続するか,抵抗分圧して下さい❗ とりあえず壊れていないのでこのままにしていますが,本来,Raspberry PiのGPIOは3.3 Vであって5 Vを入力すると故障する恐れがあります。

また,赤外線LEDを駆動するパワーMOSFETのゲート端子をGPIO13に接続しておりますが,当初,これを隣のGPIO6に接続しておりました。これが実は問題でした。本ブログの前記事にも記載した通り,Raspberry Piは電源投入時にすべてのGPIOが入力となるものの,その際にプルアップされるピン(GPIO0 ~ 8)とプルダウンされるピン(GPIO9 ~ 27)があります。そう,GPIO6はプルアップされてしまいます。すると,電源投入から,FlaskのWebアプリの中でGPIOを適切に設定するまでの間に,パワーMOSFETのゲート端子が3.3 Vでプルアップされることになり,ゲート-ソース間電圧がスレショールド電圧を超えて意図せずオンしてしまいました。前述の77.7 mAの電流が47 Ωの電流制限抵抗と赤外線LEDに流れ続け,赤外線LEDの方は問題ないにしても,電流制限抵抗の方は消費電力(0.283 W)が定格1/4 W = 0.25 Wを若干超えてしまいます。何気なく基板を触ると異様な熱さを感じたことからこの問題に気づき,最終的に隣のGPIO13に接続し直しました。

さらに,赤外線受光モジュールのピン配置ですが,1番ピンがGND,2番ピンが信号,3番ピンが電源となっており,設計時に混乱を生じました。1番ピンと3番ピンを逆に配線してしまい,これも電源を入れてみると過熱してしまい,何とかハンダ付けを修正して事なきを得ました(壊れていませんでした)。ブレッドボード上では間違えなかったんですが…やはりデータシートを慎重に見るべきですね。

irrp.pyとリモコン信号の解析調査

前節で完成したスマートリモコンHATを用いて,まずはエアコンや天井照明の様々なリモコン信号を学習してみました。ここではまだ自作のPythonモジュールやプログラムは作らず,irrp.pyを用いました。

irrp.pyの使い方

irrp.pyは前述の通りpigpioライブラリの作者joan2937さん自らによる学習リモコンプログラムです。最初の「ir」はinfraredの略で,続く「rp」はrecord & playbackの略と思われます。図3に示すように,赤外線受光モジュールがGPIO10に,赤外線LEDがパワーMOSFETを介してGPIO13に繋がっています。このような状況を想定してirrp.pyの使い方を簡単に説明します。

まず,赤外線リモコンの信号を学習する場合は,ターミナルから以下のようにコマンドを入力します。

$ python3 irrp.py -r -g 10 -f codes light:on

この後,プログラムは待機状態となるので,リモコンを赤外線受光モジュールに向けて学習させたい信号に相当するボタン押します。デフォルトでは確認のため,もう一度同じボタンを押すように促されます。--no-confirmというオプションを付けると,この確認はされなくなります。オプションの-rは(送信ではなく)受信・学習(record)することを指定しています。また,-g 10はGPIO10からの信号を読み取るように指定しています。-f codesはcodesというファイルに結果を記録すること(JSON形式で各パルスの長さがμs単位で記録されます)を意味し,続くlight:onはこれから記録する信号(シリアル通信としてのフレーム)の識別名で,例えば天井照明の点灯を意味するなど,ユーザーが自分で分かり易い名前を付けます。なお,赤外線受光モジュールは38 kHzのサブキャリアに関して復調した信号を出力するので,記録されているパルスの長さはこれを除いたものとなります。Raspberry PiではPython 2とPython 3が共存しており,pigpioライブラリのPythonモジュールやirrp.pyはPython 3でないと動作しませんので,ターミナルでは「python3」と打つ必要があります。

また,学習した信号(フレーム)を送信する場合,ターミナルから以下のようにコマンドを入力します。

$ python3 irrp.py -p -g 13 -f codes light:on

オプションの-pは学習したフレームの再生(playback)を指定しています。続く-g 13はGPIO13に出力すること,-f codes light:onは学習した情報が記録されているファイル名と再生するフレームの識別名です。なお,irrp.pyの内部では,ファイルに記録されたパルスから38 kHzのサブキャリアで変調した信号をGPIOに出力します。

自分でWebアプリなどを作らなくても,irrp.pyだけで様々な制御が可能となるでしょう。しかし,照明のようなon/offを制御するだけに近い場合は良いとして,エアコンのように温度設定,運転モード,風速,風向などの組み合わせが多くなる場合,それらのすべての組み合わせをひとつひとつ学習させておくのはあまりインテリジェントなやり方ではありません。そこでirrp.pyで学習したエアコンのリモコンからのフレームの意味を解析調査し,それをもとに任意の温度設定や運転モードを指定するフレームを生成して送信するためのPythonモジュール・プログラムを自作することとしました。

フレームの意味の調査・解析

我が家(一戸建て賃貸です)のリビングダイニングに備え付けのエアコンはPanasonic製です*8。当初はエアコンのリモコンをスマートリモコンHATに向けながら,温度設定を変えたり,運転モードを変えたりしながらirrp.pyでフレームを学習させ,その変化を分析しながら自力で信号を解析しようと試みておりました。

しかし,Panasonicのエアコンの赤外線リモコンのフレームの意味を既に解析されている方がブログやホームページに結果を掲載しており,これらを参考にさせて頂くことができました。

ak1211.com

qiita.com

特に2つめのdok kozo (@dokkozo)さんのQiitaの記事は,Panasonic製のエアコンだけでなく,同じくPanasonic天井照明についても記載があり*9,irrp.pyで学習した結果と答え合わせができました。

赤外線リモコンの信号は家製協(AEHA)フォーマット,NECフォーマット,SONYフォーマットの3種に分けられ,それぞれ少しずつ異なる方式で通信します。Panasonicのエアコンと天井照明の場合は家製協(AEHA)フォーマットを利用しているようです。また,我が家の和室の天井照明はNEC製なので,NECフォーマットを利用しておりました。

自作モジュールirxmit.py

前述の通り,学習リモコンには,その機器のすべての状態の組み合わせによって生じるフレームをひとつひとつ学習しておかなかればならないという宿命があります。例えばエアコンなど,温度設定,運転モード,風速,風向のような様々な組み合わせによってフレームが変わる場合にはそれらをひとつひとつ学習しておかなければ,柔軟な制御ができません。そこで,irrp.pyに頼らずに赤外線リモコンの信号を送信するためのPythonモジュールirxmit.pyを自分で書くことにしました(「irtx.py」の方がカッコ良かったかも…)。与えられた16進数の文字列から,pigpioライブラリ(モジュール)のwaveform機能を利用して38 kHzのサブキャリアで変調されたリーダ部,データ部,トレーラ部のパルスを生成します。とりあえず,家製協(AEHA)フォーマットとNECフォーマットに対応させることとしました。SONYフォーマットには未対応です(将来的に実装するつもりではおります…)。

拙いコードですが,完成したirxmit.pyはGitHubに置いてあります。

github.com

irxmit.pyの使い方

まず,完成したirxmit.pyの使い方について簡単に説明します。irxmit.pyは,赤外線LEDと繋がっているGPIO番号,操作したいRaspberry Piのホスト名またはIPアドレス*10,赤外線リモコンのフォーマット(文字列でAEHANECのいずれか)を引数として与え,IRxmitクラスのインスタンスとして赤外線リモコンのハンドラを生成します。生成したハンドラにてsend()メソッドを適用すると,引数として与えた16進数の文字列を元に,赤外線リモコン信号を送信します。言葉で書くよりも例を見た方がスムーズでしょう。

# Ch = 1に設定されたPanasonic製天井照明を点灯する = 2c52092d24
# 赤外線LEDはローカルのRaspberry PiのGPIO13に繋がっている

import irxmit

ir = irxmit.IRxmit(13, host = 'localhost', format = 'AEHA')
# hostはデフォルトで'127.0.0.1'なので'localhost'と敢えて指定しなくても良い

ir.send('2c52092d24')

irrp.pyを使う場合と比較すると非常にシンプルです。また,データ部として与えた'2c52092d24'の部分を変えれば,様々な家電機器に所望の運転をさせることができます。

irxmit.pyの中では,この'2c52092d24'を2文字ずつ(つまり,データとして1バイトずつ)解釈し,LSBファーストとなるような2進数表記の文字列に変換しています。例えば冒頭の2cを普通(MSBファースト)の2進数で書けば00101100ですが,後述のように赤外線リモコンの信号はLSBファーストなので2c00110100に変換します。そして,先頭から1文字ずつ取り出して,後述のようにpigpioのchain of waveformsを生成します*11

また,文字列の中に'++'とプラス記号を2つ入れることで,2つ以上のフレームを連続して出力させることができます。

図7,8に,Panasonic製の天井照明を点灯させる信号に関して,irrp.pyから出力した場合と,自作モジュールirxmit.pyで出力した場合の波形比較を示します。図8は図7のリーダ部冒頭の拡大波形です。

f:id:s-inoue2010:20210408091318p:plain
図7: irrp.pyと自作モジュールirxmit.pyの波形比較

f:id:s-inoue2010:20210408091445p:plain
図8: irrp.pyと自作モジュールirxmit.pyの比較(リーダ部冒頭拡大)

若干ズレはあるものの,ほぼ同じ波形を出力できていることが判ります*12。また,図8を見ると,irrp.pyもirxmit.pyも38 kHzのキャリアを出力しているものの,いずれも後述の規格で定められたデューティ1/3となっておらず,デューティ1/2となっていることが判ります。

以下,赤外線リモコンの信号フォーマットのうち,家製協(AEHA)フォーマットとNECフォーマットを概観した後,irxmit.pyを作っていく経緯や,エアコン,天井照明のクラスについて述べていきます。

赤外線リモコンの信号フォーマットの概要とirxmit.pyでの実装方針

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

前述の家製協(AEHA)フォーマットとNECフォーマットの概要について述べ,それぞれをirxmit.pyで実装する方針について簡単に説明します。まずは家製協(AEHA)フォーマットです。図9に概要を示します。

f:id:s-inoue2010:20210407134310p:plain:w504
図9: 家製協(AEHA)フォーマットのフレーム構成(概略図)

波長λ = 950 nmの赤外光をキャリアとして用いますが,自然光などに含まれる赤外光とリモコンの信号を区別するため,リモコンは概ね38 kHzのサブキャリアでフレームを変調します。家製協フォーマットではサブキャリアとして33 ~ 40 kHzが認められているようで,typ.値は38 kHzです。サブキャリアのデューティは1/3となっていますが,irxmit.pyでは1/2としてしまいました。

また,フレームはTという時間を単位として組み立てられており,これを変調単位と呼びます。家製協(AEHA)フォーマットではT = 350 ~ 500 μsが認められており,typ.値は425 μsです。ここで,変調単位T = 425 μsをサブキャリアの1周期Tc = 1 / (38 × 103) ≈ 26.3157947 μsで除算しようとすると,何と割り切れません…💦 pigpioライブラリ(モジュール)のwaveformでは1 μs単位でパルスを制御できるので,自作モジュールirxmit.pyにおいては,許容されている範囲内で,変調単位Tがサブキャリアの1周期Tcの整数倍となるように,Tc = 26 μs (fc = 38.46154 kHz), T = 17 Tc = 442 μsとしました。これで実装はかなり容易になります*13

赤外線LEDが38 kHzの変調を加えて点灯しているところを「マーク」,消灯しているところを「スペース」と呼びます。リーダ部は長さ8Tのマークに続く長さ4Tのスペースから構成され,これでフレーム開始を意味します。

データ部は長さTのマークに続いて同じく長さTのスペースがあれば2進数の「0」,長さ3Tのスペースがあれば2進数の「1」と見なします。ひとつのbyteはLSBからMSBの順に送信されますので,図9に例として示した1つのbyteでは時間軸上で00011001の順となっておりますが,これは1 byte = 8 bitとして0b10011000 = 0x98を表します*14。データ部には実際にはカスタマーコードやパリティ,データ本体などが格納されることになりますが,自作モジュールirxmit.pyではすべて一緒くたにデータ部と呼ぶことにしました。つまり,データ部のエンコードはirxmit.pyの外部,ユーザ側で行ってもらうことにしました。

最後に,トレーラ部として変調単位1つ分のマークを出力してフレームを終了します*15。休止期間は8 ms以上を確保する必要があるようです。特に,Panasonic製のエアコンでは2つのフレームを連続して出力しているようなので,トレーラ部と次のフレームのリーダ部が連続することになります。

NECフォーマット

次に,図10にNECフォーマットの概要を示します。

f:id:s-inoue2010:20210407135722p:plain:w623
図10: NECフォーマットのフレーム構成(概略図)

家製協(AEHA)フォーマットと同じく波長λ = 950 nmの赤外光をキャリアとして用い,38 kHzのサブキャリアでフレームを変調します。まじめに調べてはいませんが,NECフォーマットでは許容誤差が分かりませんでした。

また,変調単位はT =562 μsとなっており,やはり許容誤差は分かりませんでした。ここで,変調単位T = 562 μsをサブキャリアの1周期Tcで除算しようとすると,やはり何と割り切れません…。自作モジュールirxmit.pyにおいては,若干誤差は生じるものの,変調単位Tがサブキャリアの1周期Tcの整数倍となるように,Tc = 26 μs (fc = 38.46154 kHz), T = 22 Tc = 572 μsとしました。許容誤差は分からないものの,テストしてみると,少なくともうちにあるNECの天井照明は反応しました。家電機器によっては微調整が必要になるかもしれません。

リーダ部は長さ16Tのマークに続く長さ8Tのスペースから構成され,これでフレーム開始を意味します。データ部およびトレーラ部については変調単位Tの長さが異なる以外は家製協(AEHA)フォーマットと同じです。

irxmit.pyでのwaveform,chain of waveformsの組み立て

pulseオブジェクト

pigpioライブラリ(モジュール)におけるwaveform機能は少し解り難いので,基本的な部分のみですがここで少し解説します*16。waveform機能を理解するにはまずpulseオブジェクトを知る必要があります。pulseオブジェクトは「Raspberry PiのGPIOのどのピンをHighに立ち上げ,どのピンをLowに立ち下げ,何μsの間それを保持するか」という情報を格納しています。pigpioライブラリ公式サイトから引用します。

class pulse
pigpio.pulse(gpio_on, gpio_off, delay)

Initialises a pulse.
Parameters
gpio_on := the GPIO to switch on at the start of the pulse.
gpio_off := the GPIO to switch off at the start of the pulse.
delay := the delay in microseconds before the next pulse.

上記のgpio_ongpio_offが分かり難いですが,これはHighに立ち上げたい,またはLowに立ち下げたいGPIOの番号に相当するビットに「1」が入っており,一方,何も操作することなくそれ以前の状態を保持したいGPIOの番号に相当するビットが「0」となっているような2進数(に相当する整数)です。例えばgpio_on = 0b00000110 = 0x06gpio_off = 0b10001000 = 0x88となっていれば,GPIO1, 2をHighに立ち上げると同時に,GPIO3, 7をLowに立ち下げることを意味します。そしてそのままdelayにてμs単位で指定した時間だけ保持します。大体において1つのGPIOしか操作しないことが大半なので,例えばGPIO13だけを操作するのであれば,gpio_on = 1 << 13のようにシフト演算子で「1」を2進数での13桁目まで持っていきます。

waveformとchain of waveforms

waveformとはPythonのリストにpulseオブジェクトを詰め込んだものを元にしてadd_wave_generic()メソッドとwave_create()メソッドを使って生成したものです。ユーザはwaveformに対応する整数値であるwave_idを得ます。例を見た方が分かりやすいと思いますので,図7にて家製協(AEHA)フォーマットのデータ部の「1」をwaveformとして作るコードを示します。

import pigpio

(中略)

T_CARRIER = 26 # [μs]: サブキャリア1周期
MARK_CYCLES = 17 # [cycles]: 変調単位毎のサブキャリアの周期数
MARK_OFF = 3 # マークに対するスペースの長さ
pin = 13 # 赤外線LEDに繋がっているGPIOの番号 

(中略)

# データ部の'1'に相当するwaveformを生成
wb = []
for i in range(0, MARK_CYCLES):
    wb.append(pigpio.pulse(1 << pin, 0, T_CARRIER // 2))
    wb.append(pigpio.pulse(0, 1 << pin, T_CARRIER // 2))
wb.append(pigpio.pulse(0, 1 << pin, T_CARRIER * MARK_CYCLES * MARK_OFF))
pi.wave_add_generic(wb)
wave_data_1 = pi.wave_create()

まず空のリストwb(wave bufferの略を意図しています)を作り,for文の中でマークのサブキャリアに相当するpulseオブジェクトを17周期分追加していきます。for文を抜けるとスペース部に相当するpulseオブジェクトをwbに追加します。最後にpi.wave_add_generic(wb)wave_data_1 = pi.wave_create()を実行します。得られたwave_data_1は整数であり,waveformのID番号(wave_id)を格納しています。この最後の2つの操作は1つにまとめても良いのではと思うのですが,pi.wave_add_generic(wb)にて何処だか分らない記憶領域にリストwbが置かれているようです。詳しくはpigpioライブラリ(モジュール)のソースコードにあたる必要がありそうです。

当初,図9や図10に記載のフレーム全体を1つのwaveformとして生成するようなプログラムとなっていましたが,Panasonic製のエアコンではフレームが長過ぎたため,waveformを生成できませんでした。1つのwaveformに格納できるpulseオブジェクトは5,460個までという制約があるようです。

github.com

そこで,改めてirrp.pyのソースコードをよく読んでみると,フレームを1つのwaveformとして作るのではなく,それぞれのパルス(マークおよびスペース)を個別のwaveformとしつつ,過去に生成したwaveformと同じパルス長の場合はwave_idを再利用しながら(学習リモコンなのでパルス長にゆらぎがある),フレーム全体をchain of waveformsとして作っていることが判りました。私のirxmit.pyでは変調単位はゆらぎがなく固定であるため,さらに容易なはずです。

そこで,IRxmitクラスのインスタンスが作られる際に,コンストラクタからクラスメソッドを1つ呼び出し,指定されたフォーマットに応じて,リーダ部,データ部の「0」,データ部の「1」,トレーラ部に相当する4つのwaveformを予め生成してwave_idを取得しておくこととしました。

send()メソッドでは,空のchain of waveforms(リスト)にまずリーダ部のwave_idを追加し,続いて引数として与えられた16進数の文字列からエンコードしたLSBファーストの2進数文字列を1文字ずつ取り出してデータ部の「0」または「1」に相当するwave_idを追加していって,最後にトレーラ部に相当するwave_idを追加します。wave_idはただの整数なので,chain of waveformsは整数のみを要素とする普通のPythonのリストです。なお,同じwave_idを1つのchain of waveformsの中で何度でも利用できます。私のirxmit.pyでは,wave_idは前述のように0, 1, 2, 3の4つしかなく,データ部は1と2の繰り返しになっています。

chain of waveformsを生成したら,これをpigpioのwave_chain()メソッドで実際に送信します。送信している最中にプログラムに制御が戻ってくるので,irxmit.pyでは送信中であるか確認するis_busy()メソッドを作りました。これはpigpioのwave_tx_busy()メソッドを呼び出しているだけです。

エアコンと天井照明のクラス

ここまで来ると欲が出てくるもので,今度はエアコンや天井照明を制御するクラスを作ろうということになりました。詳述はしませんが,例えばPanasonic製エアコンに関するiracPanasonic.pyというモジュールにIRACPanasonicというクラスを作り,

import irxmit
import iracPanasonic
import time

ir = irxmit.IRxmit(13)
ac = iracPanasonic.IRACpanasonic(ir)

# エアコンを21°C設定で暖房運転する
ac.on_heating(21)

# 1分待つ
time.sleep(60)

# エアコンを停止する
ac.off()

というように抽象的に操作できるようにしました。iracPanasonic.pyの中で,解析調査した赤外線リモコン信号を元にフレームを生成し,引数としてIRxmitオブジェクトに与えてフレームを送信しています。天井照明についても同様のモジュールirlightPanasonic.pyを作成しました。

何だか「オブジェクト指向完全に理解した」という錯覚に酔いしれているぞ…🍺

cronによる温湿度の記録

さて,温湿度センサDHT22 (AM2302)を自作スマートリモコンHATに搭載した理由ですが,1つは外出先から部屋の温湿度を知るため,もう1つは赤外線信号の送信に成功して実際にエアコンが運転開始したのか,それとも失敗しているのか,フィードバックする機構が欲しかったからです。当初,後述のFlaskによるWebアプリの中でDHT22と通信して温度・湿度のデータを得る形にしていましたが,この場合はユーザがアクセスした瞬間のデータしか取得できません。やはり,トレンドグラフを描きたいので,Linuxで一定時間毎に何かをするための常套手段であるcronを使って毎分の温度・湿度を記録することにしました。

DHT22との通信もpigpioライブラリ(モジュール)に「おんぶにだっこ」となります…💦 というのも,pigpio_dhtというPythonモジュールがあり,これを使うと非常に簡単に温湿度を取得できるためです。短いプログラムなので全文を以下に示します。

#!/usr/bin/python3
# -*- coding: utf-8 -*-

# DHT22_record.py
# (c) 2021 @RR_Inyo
# A Python script to acquire temperature and humidity
# from DHT22 (AM2302) sensor and record it to a CSV file
# This script is intended for being run by cron every minute.
# Released under the MIT license.
# https://opensource.org/licenses/mit-license.php

from pigpio_dht import DHT22
import datetime
import csv

# Constants
GPIO_DHT22 = 19
MAX_RETRIES = 5
CSV_FILE = '/tmp/DHT22_record.csv'

# Define handler
sensor = DHT22(GPIO_DHT22)

# Read sensor
result = sensor.read(retries = MAX_RETRIES)
temp = result['temp_c']
humid = result['humidity']

# Obtain date/time
now = datetime.datetime.now()

# Write results to CSV file
with open(CSV_FILE, 'a') as f:
    writer = csv.writer(f, delimiter = '\t')
    writer.writerow([now, temp, humid])

このプログラムをcronから毎分呼び出して温湿度を記録することとし,FlaskのWebアプリは記録された/tmp/DHT22_record.csvを読み込んで現在の温湿度およびトレンドグラフを表示することとしました。

FlaskによるWebアプリ

長い道のりでしたが,自作スマートリモコンHATというハードウェア,そしてirxmit.pyやiracPanasonic.py,irlightPanasonic.pyというソフトウェアを作り,あとはこれらをインターネット上から操作するためのWebアプリを作る段階となりました。前述の通りFlaskを使うことにしましたが,何せWebアプリのフレームワークを使うのは初めてなので,「ゼロからFlaskがよくわかる本」のブログアプリをパクり参考にしながら,不要な機能は削除し,必要な機能は1つずつ追加しながら作っていくことにしました。

非常に幼稚なWebアプリですが,もしかすると参考になるかもしれませんのでGitHubに置いておきます。

github.com

私なりのFlaskの理解

FlaskはPythonをベースとしたWebアプリのフレームワークです。フレームワークとは何ぞやというのは,ずっと昔に「Ruby on Rails」の存在を知ったときにも疑問に思いましたが,私の凡庸な脳では理解することはできず,そのまま10年ほどが経過しました。

凡人である私の拙い理解では:

Flaskとはそれ自身でWebサーバ(もしくはApacheなどの他の有名なWebサーバとの連携)であり,ブラウザ上でユーザが入力したURLに従ってPythonの関数を割り込みのように起動し,ユーザがそのURLにGETやPOSTした内容に応じてどのような処理を実行し,どのようなHTMLや画像をブラウザに返すのか,Pythonで自由に書くことができるもの

です。

「ゼロからFlaskがよくわかる本」では,読み進めながらPythonやHTMLを書いていくことでシンプルなブログアプリが完成します。Pythonのプログラムでは,ルートディレクト'/'にアクセスした場合に,ブログの内容を表示するHTMLをブラウザに返したり,'/entry/new'にアクセスすると,記事を書くためのformを表示し,送信ボタンが押されると記事をデータベースの格納したりといった処理を裏で実行しています。

FlaskにはModel, Template, View (MTV)という考え方があるようで,Modelはデータベースなどの裏方の制御,TemplateはHTMLファイルのひな型,Viewはユーザにブラウザで何を見せるかを制御する部分,というように理解しました。

具体的なPythonのコードとしては,from flask import Flaskとしてモジュールを読み込んだ後,いずれかのファイルにてapp = Flask(__name__)としてFlaskアプリオブジェクトを作ります。すると,例えばユーザがブラウザに'/login'というURLにアクセスした場合,@app.route('/login')というデコレータを付した関数が割り込みのように呼ばれ,その関数でHTMLを文字列として直接returnしたり,render_template()メソッドでテンプレートとして格納しているHTML(変数を埋め込ませることもできる)を元にしてreturnしたりすることができます。やっていることはとてもシンプルなので,「フレームワーク」という謎の単語への理解が少しだけ前進した気がします。

FlaskでのHello world

あらかじめFlaskをpipなどでインストールしておいて下さい。Raspberry Piであれば,pipよりaptで

$ sudo apt install python3-flask

とした方が良いかもしれません(他のモジュールもpython3-*で大抵はインストールできます)。

Flaskでの最小限のHello worldを書いてみます。たった1つの.pyファイルで完結しています。

#!/usr/bin/python3
# -*- coding: utf-8 -*-

from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello_world():
    return '<html>Hello, world</html>'

app.run(host = '0.0.0.0', debug = True)

おお,私でもそらで書けたぞ…❗ これを実行すると,そのマシンで一時的にWebサーバが起動しますので,そのポート5000にブラウザからアクセスすると,図9のように「Hello, world」が表示されるはずです。

f:id:s-inoue2010:20210407185650p:plain:w450
図11: FlaskによるHello world

最小限のカタチから出発して理解する──この方式で私のような凡人でも世界を広げていくことができそうです。

本Webアプリでやりたいこと

本Webアプリには以下のような機能を実装しようと考えました。

  1. パスワードによるログイン(インターネットからエアコンなどを勝手に操作されないように)
  2. エアコンの運転操作(温度設定と暖房・冷房・ドライ・停止)
  3. ダイニングとリビングの天井照明の操作(点灯・全灯・常夜灯・消灯)
  4. /tmp/DHT22_record.pyから読み取った最新の(≒現在の)温湿度の表示
  5. /tmp/DHT22_record.pyから読み取った温湿度のトレンドグラフ作成・表示

上記1についてはログイン画面用のlogin.htmlを作り,それ以外についてはindex.htmlを作ってその中に詰め込むことにしました。

ディレクトリ・ファイル構成

Flaskで難しいのがディレクト*17やファイルの分け方です。Djangoでは標準が決まっているようですが,Flaskではコレというのが決まっていないようです。 私は,「ゼロからFlaskがよくわかる本」を参考にしながら,以下のようなディレクトリ構成を作りました。

app
├── lib
│   ├── iracPanasonic.py
│   ├── irlightNEC.py
│   ├── irlightPanasonic.py
│   └── irxmit.py
├── remoteir
│   ├── config.py
│   ├── __init__.py
│   ├── static
│   │   └── style.css
│   ├── templates
│   │   ├── index.html
│   │   ├── layout.html
│   │   └── login.html
│   └── views
│       ├── __init__.py
│       └── views.py
└── server.py

ルートであるappディレクトリの中にlibディレクトリを作り,前節までに作った赤外線リモコン関係の自作モジュールを格納します。またWebアプリを起動するためのserver.pyを置きました。

remoteirディレクトリはWebアプリの本体であり,app = Flask(__name__)を実行している__init__.pyを格納している他,static,templates,viewsなどのサブディレクトリを格納しています。staticディレクトリにはCSSファイル(中身はほとんど空っぽ)のみを置いております。また,config.pyにFlaskの設定を書いておりますが,この中にログインパスワードを記載しました。

templatesディレクトリはFlaskがデフォルトで認識するHTMLテンプレートを格納するためのディレクトリです。ヘッダなど大枠を作るlayout.html,ログイン画面のlogin.html,ダッシュボードのindex.htmlの3つのHTMLファイルを置いています。FlaskはJinja2というテンプレートエンジンを利用しております。render_tamplate()メソッドを実行すると,Jinja2を利用してHTMLがテンプレートからレンダリングされるのですが,その際に変数を引数として渡し,HTML内の二重中括弧{{ }}で囲まれた部分にはめ込んだり,if文のような構造でレンダリングされるHTMLを書き換えたりといったことができます。本記事のWebアプリでもPythonによって処理された結果,例えば温湿度センサで取得した温度や湿度などをテンプレートHTMLにはめ込んでいます。

viewsディレクトリの中にはviews.pyを格納しました。本Webアプリの心臓部とでもいう箇所で,ユーザがブラウザに入力したURLやPOSTした内容に従って,様々な処理を実行します。処理内容の詳細は次項で述べます。

views.pyの処理内容

views.pyの中にほとんどすべての処理内容を詰め込んでいます。@app.route()デコレータにて関数と紐づいているURLは以下の通りです。

  1. '/login': ログイン画面(login.html)をレンダリングする。もしPOSTされたパスワードが正しければ,'/'にリダイレクトする。
  2. '/': 以下の機能を実装する。
    • sessionにてログインを確認できなければ'/login'にリダイレクトする。
    • /tmp/DHT22_record.csvを読み取り,最終行を抽出して「現在の温湿度」を得る
    • ダッシュボード画面(index.html)をレンダリングする。
    • (views.pyではなくindex.htmlの内容であるが)リモコン制御用のformを設けており,ボタンが押されたら,各formの内容をエアコン,ダイニングの天井照明,リビングの天井照明のそれぞれについて,'/ac''/lightDining''/lightLiving'にPOSTする。
    • (views.pyではなくindex.htmlから呼び出されるlayout.htmlの内容であるが)ログアウトのリンクが押されたら,通常のブラウザの動きとして'/logout'をGETする。
  3. '/ac': formからPOSTされた内容に従ってリビングのエアコンを制御し,'/'にリダイレクトする。
  4. '/lightDining': index.htmlのformからPOSTされた内容に従ってダイニングの天井照明を制御し,'/'にリダイレクトする。
  5. '/lightLiving': formからPOSTされた内容に従ってリビングの天井照明を制御し,'/'にリダイレクトする。
  6. '/graph.svg': /tmp/DHT22_record.csvを読み取り,Matplotlibにて温湿度のトレンドグラフをSVGとして作り,そのバイナリデータをブラウザに返す。
  7. '/logout': sessionのログイン情報を削除する。

HTMLファイルの中でリンク(aタグ)やフォーム(formタグ)を記載し,押されたリンクや入力されたフォームに従って,各URLをGETしたりPOSTします。Pythonの方ではそれぞれのURLに対応する関数を@app.route()デコレータで作り,所望の情報処理を行いつつ,その処理の結果を受けてブラウザに返すHTMLをレンダリングしたり,他の関数(≒ URL)にリダイレクトしたりします。

このように,ユーザ(ブラウザ)の操作を受け付けてWebサーバにGETやPOSTをするHTML(HTMLテンプレート)と,それを受けて処理を行うPythonプログラムがお互いを呼び出し合いながら所望の機能を実現するという形で動いており,このイメージをつかむまでに時間が掛かりました…。

完成したWebアプリの様子は,冒頭の図2,3の通りです。

試用結果

4月初旬に(新型コロナウイルスの感染対策に気を付けつつ)水戸にある妻の実家に子どもたちを連れて出掛けることになりました*18。春なので真夏や真冬のように,留守中の室温が極端になる恐れは小さいものの,せっかくなのでこれを機会と捉えて開発したスマートリモコンを試してみることにしました。

エアコンに赤外線の信号が届くように慎重にRaspberry Piを置き,電源を入れます。cronにて起動直後に自動的にFlaskのサーバを起動する(server.pyを実行する)ように設定しております。外出先からもエアコンを制御できるのか否か,テストしてみました。図12に結果を示します。

f:id:s-inoue2010:20210408020629p:plain:w525
図12: 外出先からのエアコン制御試験の結果

図10の時刻Aで温度設定20°Cの冷房運転を開始しました。するとエアコンは反応したようで,温湿度センサで感知している温度も20°Cまで概ね30分で下がっていることが判ります。綺麗な一次遅れのような波形ですね。同図の時刻Bにてエアコンを停止すると,徐々に概ね元の温度に戻りました。

同様に,時刻Cで温度設定30°Cの暖房運転を開始しました。やはりエアコンは反応したようで,温度は急激に30°Cを上回り,35°Cをも超えています…💦 時刻Dにてエアコンを停止すると,冷房運転を停止した場合と同様に,元の室温に概ね復帰していきます。

なお,エアコンの赤外線リモコンの受光はかなりシビアなようで,自作のスマートリモコンHATの赤外線LEDでは前述のように70 mA以上を流しても,かなり近くからでないと反応しませんでした(天井照明は遠くからでも,角度がズレていても容易に反応しました)。今回の試用ではRaspberry Piをエアコンの直下に置いているため,エアコンからの風が温湿度センサDHT22に直撃していたようでした。よって,図10のグラフは室温というよりはエアコンから出ている風の温度と言えそうです。将来,HATを作り直すことがあれば,より強力な赤外線を放てるように改良したいと思っています。

さて,2泊3日の最終日,帰る日です。この日は雨が降って4月とは言え少し気温が下がりました。暖房を点けておいた方が良さそうです。早速活躍の機会が訪れました…❗ 妻の実家を発つ直前に21°C設定で暖房運転を開始しておくと,果たして帰宅した頃には十分に暖かくなっておりました。役に立つ工作ができたではないか…🎵

まとめ

長文になってしまいましたが,Raspberry Piに自作のスマートリモコンHATを取り付け,Flaskを用いたWebアプリを介してインターネットから制御するまでの経緯や,ハードウェア・ソフトウェアの実装方法について述べました。

あまりにも長くなり,恐らく本ブログの最長記事となっていると推測します(現時点ではてなブログの編集画面には「28,001文字」との表示が出ています)。あまりにも長いので,近い将来,ハードウェア編とソフトウェア編,または赤外線リモコン編とWebアプリ編に分割するかもしれません。いずれにしても,2014年から温めてきたスマートリモコンがようやく形になりました。

あ,Raspberry Piのカメラが1つ余っているから監視カメラの機能も付けたらどうだろうか…? FlaskによるWebアプリが便利なので,様々なプロジェクトでこれを活かせるのではないか…? ん? インターネットからBLDCモータを制御できたら楽しいのでは? 前記事と本記事の合わせ技で作れるのではないか…? このような妄想を膨らませております💦

次回は系統連系変換器の制御やその安定性について,再び議論を深めていければと考えております。

*1:例えば赤外線LEDを積んだRaspberry Piでスマートルームをつくる(chatbot実装編) | うたかたサバイバーなど。

*2:お察し下さい。

*3:例えばsubprocess.run('python3 irrp.py -p -g 13 -f codes ac:on', shell = True)というように,完全な外部プログラムとして実行することになります。

*4:他にもTwitter APIを使ってTwitterのDMから制御したり,スマートスピーカと接続したり,面白そうな方法がまだまだありますね。

*5:これもいつかはやってみたいですね。

*6:制御対象とする家電機器や赤外線信号をCRUDで追加・編集するというのもあるのではと思います。

*7:分解したテレビのリモコンの赤外線LEDの電流制限抵抗は何と1 Ωでした

*8:他の部屋には日立製のエアコンを取り付けております。日立製はリモコンの信号が特殊なようで,irrp.pyでも学習できませんでした。要研究です。

*9:ダイニングとリビングの天井照明もPanasonic製です。

*10:pigpiodデーモンが起動していれば,リモートのRaspberry Piに繋がっている赤外線LEDから信号を送出することもできます。

*11:バイト列として実装した方が良かったのかもしれませんが,慣れ親しんでいる文字列を利用しました。

*12:ただし,図7,8を取得した際のirxmit.pyはトレーラ部を実装し忘れていた状態であり,図7,8では見えていないフレームの最後の最後でirrp,pyとirxmit.pyの波形は異なっていたはずです。波形の尾部を見るようなトリガ設定ってないですかね…?

*13:詳述しませんが,学習リモコンであるirrp.pyではパルス長に揺らぎが生じるため,その揺らぎと38 kHzのサブキャリアの辻褄を合わせながらwaveformを生成するための苦労が読み取れました。

*14:これが結構クセモノですね…。

*15:一度,これを忘れてしまい天井照明が反応しなかったことがあります。

*16:すべての機能を網羅的に理解している人は,作者のjoan2937さん以外にいらっしゃるのだろうか…? waveform機能単体で専用のスクリプト言語のようなものがあったりと,とんでもなく豊富な機能があります。

*17:いつから「フォルダ」とも呼ぶようになったのでしょうか? 好みの問題ですが,CUIではやはり「ディレクトリ」と呼びたいですね…。

*18:FlaskによるWebアプリの実装を急いだ理由でもあります。