自宅の最強の加湿器、あるいは私は如何にして心配するのを止めて加湿器を起動するようになったか

最強の加湿器

世の中には「最強の加湿器」を作った人がいる。

www.slideshare.net

曰く、「最強の加湿器」は湿度を自動で適切な値に維持してくれる*1。この加湿器が欲しくなったので作ることにした。

作成中に発覚した問題

数時間の作業の結果、自室の湿度を測定するのはラズパイにセンサーを付けたらできたし、その値をもとに加湿器が起動しているべきか停止しているべきかを判定するロジックを組むこともできた。

しかし、実際に加湿器を操作するロジックを組む段になって、問題が発覚した*2

加湿器を操作する難しさ

自宅にある加湿器は、押し込みボタンでオンオフを制御するタイプの製品だ。

f:id:threetea0407:20220321225831p:plain

このボタンは内部に状態(押し込まれているか否か)を持っている。そのため、加湿器をオンオフするためには、ソフトウェア側でボタンの状態を管理する必要がある。

例えば、加湿器がオフになっているべきだと判定されたとしよう。ボタンが押し込まれている状態ならボタンを押す必要がある。一方で、ボタンが押し込まれていないならば、何もしてはいけない

このように、ボタン経由で加湿器の操作をする場合、「いま、ボタンは押し込まれているか?」という状態をソフトウェア上に保持しなければならない。これでは実装が面倒になる上に、ソフトウェアと実機上での状態が乖離する可能性もある。状態が乖離した場合、それをソフトウェアによって修正することは不可能である。

ボタンの押し込まれ具合やランプの色から現在の状態を判別することも可能かもしれないが、新しくセンサーを導入する必要があり、管理が煩雑になる。また、湿度の時間微分の正負によって加湿器の状態を推定するといったことも検討したが、湿度は加湿器の状態のみに依存するわけではない*3ということもあり、正確に推定するのはかなり難易度が高いと思われた。

この問題を解決するために、加湿器のオンオフ操作をボタン経由ではなく、電源経由で行うことにした。

加湿器を操作する最強の方法

加湿器のオンオフ操作を電源経由で行うというのは、ボタンは「入」の状態で固定しておき、加湿器をオフにしたくなったらコンセントを抜き、オンにしたくなったら差す、ということである。

実際、コンセントの仮想的な抜き差しを実現するデバイスは Switch bot から提供されている。

このデバイスは、現在電源がついているかどうかに関わらず「電源をオフにする」と「電源をオンにする」という操作を行うことができる(そういうAPIが生えてる)。そのため、加湿器の現在の状態を気にすることなく、加湿器のオンオフ操作を行うことができる。

最強の加湿器

最強の加湿器が動作する様子がこちらだ。

f:id:threetea0407:20220321235032p:plain

しきい値を上回ったり下回ったりしたタイミングで加湿器のオンオフが切り替わり、湿度が反転している様子がわかる。

雑感

問題に気づいたときに、無理やりセンサーでどうにかしたり、煩雑な状態管理の処理を書くのではなく、シンプルかつ堅牢な方法を考えることで対処できたのは良かったと思った。

まだ最強の加湿器を運用し始めて日も浅いので、今後も様子を見守りつつ、他のパラメータ(例えばCO2濃度)についても自動で制御されるようにしていきたいと考えている。

*1:スライド中には他にもいくつか条件があるが、本記事ではこれを定義とする

*2:実際には問題が解決してから判定ロジックを組んでいるのだが、話をわかりやすくするために事実とは異なる時系列を採用している

*3:例えば、室温が上がると湿度は下がる

ブラウザオンラインゲームを Ruby on Rails で作る

この記事は、CAMPHOR- Advent Calendar 2021の21日目の記事です。

Ruby on Railshotwire-rails という gem を導入すると、ブラウザ・サーバー間の双方向通信がかんたんに実装できます。

それを利用して、ある種のブラウザオンラインゲームを簡単に実装にできるという話と、その一例として、オンラインで対戦できるリバーシを作った話をします。

続きを読む

ISUCON11を振り返る

@ebiebievidence@uni745e の2人と一緒に「ここにチーム名を入れる」というチームを組んで、ISUCON11に出場しました。

結果は予選敗退、最高スコアは45180、最終スコアは40952、公開Leaderboard での順位は 105位でした*1

この記事では、準備したこと、当日やったこと、振り返りについて述べます。

*1:参考値内での順位は100位でした

続きを読む

「はじめて学ぶソフトウェアのテスト技法」を読んだ

よく考えてみたら「テスト」について勉強したことなかったなと思って、本を読んでみた。

本の解説

「はじめて学ぶソフトウェアのテスト技法」は、バグを効率的に発見するためにテストをどう行えばよいかという問に答える本だ。 テストコードの保守性を上げるにはどうしたらよいか、という問には答えてくれないので、その問題意識を持つ人は別の本をあたったほうが良いと思う(良い本があったら教えてください)。

本書は、以下のブラックボックステストの技法を解説している。

我々がテストを書くときに無意識に実践している項目もある。例えば「同値クラステスト」は、ほとんどのプログラマーは無意識にやっているはず。 ただ、他のテスト技法に関しては、実践したことのないものばかりだった。

これまでは、テストでバグを発見できる気がしなくてすごく気持ち悪かったんだけど、この本を読んだことである程度自信を持ってやれそうな気がしてきた。

あと、すごく重要なことなんですが、この本はかなり読みやすいです。 昔似たような本を読んだときはとにかく退屈で、10ページぐらい読んでやめてしまったんだけど、この本は一人でもサクサク読みすすめられました。

面白かったところ

同値クラス・境界値

個人的には同値クラスと境界値が大事な発想だと思った。

テストっていうのは、原始的にはありうる全ての入力パターンを試して正しく動くことを担保しないといけないと思うんだけど、現実的にはそれは無理だ。 入力がenumとかで有限のパターンしか取らないなら可能かもしれないけど、整数とか実数が入力パラメータに入ってくるとオシマイだ。 そこで導入される考え方が「同値クラステスト」と「境界値テスト」だ。

振る舞いが「非線形」に変化する領域ごとに入力をグルーピングして、そのグループの「端の値」と「代表的な値」をテストしましょうね、というのが同値クラス・境界値テストだと自分は理解した。 同値クラス・境界値の発想を理解することで、整数とか実数とかの、無数の値を取る入力に対しても網羅的にテストが書けることになる。 このことによって、逆に「網羅的にテストを書こう」という気持ちになれることを発見した。

これまでテストを書くときは、以下のパターンでテストを書くことが多かった。

  • 網羅性を気にせず、ド正常系を1つと、思いつく限りの異常系をテストする
  • 正常系に属する範囲から入力をランダムに選んでテストする

これは、どうせ網羅的にはテストを書けないのだから網羅性を担保するのを諦める、という発想が根底にある。

網羅的にテストを行う手法を理解したことで、網羅的にテストを書こうという気持ちになれる。

また、同値クラスと境界値を前提として様々なテスト技法が存在している。 例えば、デシジョンテーブル同値クラスと境界値を前提として入力の網羅性を担保したいときに使う技法だし、ドメイン分析テストは同値クラス・境界値の拡張だ。 そういう意味でも、同値クラスと境界値を理解することは重要な雰囲気を感じる。

ペア構成テスト

ペア構成テストも面白い。 これは、単体では問題なく動くコンポーネント同士を組み合わせたときにバグるのを発見する手法だ。 組み合わせるコンポーネントの種類に対して、コンポーネントの組み合わせの数は指数的に増えていく。 そのため、全てのコンポーネントの組み合わせパターンをテストするのは実質的に不可能だ。 この問題を解決するために導入される手法がペア構成テストだ。

経験則として「組み合わせでバグるのは、2つのコンポーネントを組み合わせたときにバグるのがほとんどで、3つ以上のコンポーネントを組み合わせたときだけバグるというのはほとんどない」ということが知られているらしい。 つまり、コンポーネントの全組み合わせを調べる必要はなく、全てのコンポーネントがペアを組んだことがあるような組み合わせに絞ってテストをすれば、それだけでかなりの確率でバグを発見できる、ということだ。

「全てのコンポーネントがペアを組んだことがあるような組み合わせ」を生成するソフトウェアが世の中には存在するので、それを使ってテストする組み合わせをリストアップすればいい。 これによって、全組み合わせをテストするのに比べて格段にテスト対象を減らすことができる。

ただ、自動テストをするという観点からは、多くの場合は全組み合わせをテストすることは可能だと思う。特に正常系をざっとみるだけであれば、テストケースを自動生成してエラーがthrowしないことをチェックするのは簡単なはずだ。 一方で、各組み合わせに対して詳しい挙動をチェックしようと思ったら、ペア構成テストは自動テストにおいても有効な手法だと思う。

状態遷移テスト

これまで状態遷移をテストしたことなかったけど、いざやることになったら多分どこから手を付けたらいいかわからんとなっていたと思う。 この本には状態遷移をテストする方法がいくつか紹介されていて、それぞれどのぐらい偉いテストなのかということが書いてあってよかった。 重要な状態遷移なら気合を入れて偉いテストを書き、どうでもいいやつならざっくりテストを書く、ということができると思う。

まとめ

「はじめて学ぶソフトウェアのテスト技法」は面白い本だった。 雑にテストを書いてるソフトウェア開発者の人は一度読んでみると面白いと思う。

それはそれとして「いいテストコードの書き方」「テストが書きやすい設計をする方法」とかの知見はこの本からは得られないので、オススメの本がある人は教えてください(クリーンアーキテクチャは読みました)。

これは読書ノートです: 初めて学ぶソフトウェアのテスト技法 - genya0407のメモ

NOTE:

本書はブラックボックステストの手法だけでなく、ホワイトボックステストの手法も取り上げています。 しかし、ホワイトボックステストの扱いは小さいし、「ホワイトボックステストに拘るな」みたいな記述があったし、本体コードにゴリゴリに依存したテストコードの保守性はものすごく低いことが予測され、実際に業務で書くことはあまり無いだろうと思ったので、読み飛ばした。

とはいえ、「この分岐ってテストされてないけど大丈夫かな」のような発想でテストケースに気づけることもあると思うので、なんとなく意識するぐらいのことはしてもいいのかなと思った。

Rubyで設定を書けるLinux用キーマッパー 「rumap」をRustで作った

この記事は、 CAMPHOR- アドベントカレンダー 2020の8日目の記事です。

Rubyで設定を書けるLinux用のキーマッパーをRustで実装した話をします。

Rumap

Rumap は Ruby DSLで設定を書けるLinux用のキーマッパーです(正確にはX Window System用)。

github.com

キーマッパーとは何かというと、karabinarみたいなやつです。 つまり、キーボードの入力をなにか別の入力に変換するアプリケーションです。

例えば、以下のような設定ファイルを書いたとします。

remap 'Control-BackSpace', to: 'Delete'

これを rumap に食わせて起動すると、 Control と BackSpace を同時押しすると、代わりにDeleteが入力されるようになります。

また、キー入力を変換するだけでなく、キー入力をトリガーにしてコマンドを実行することもできます。 例えば以下のように書くことで、Alt と Shift と 4を同時押しすると gnome-screenshot -a -d 0 が実行されます。

remap 'Alt-Shift-4', to: execute('gnome-screenshot -a -d 0')

詳しい使い方やインストール方法は、READMEを参照してください。

設定ファイルがRubyスクリプトである

rumapの設定ファイルはRubyスクリプトなので、いろいろと便利なことができます。

例えばこういうことができます。

%w[r z x c v w t f Return].each do |key|
  remap "Alt-#{key}", to: "C-#{key}"
end

この設定ファイルを使うと、Alt と r や z や x...を同時押ししたとき、 Ctrlとの同時押し に変換されます。

このようにプログラミング言語を設定用の言語とすることで、複雑な設定も短く書き下すことができます。

なぜ作ったのか

Linuxで動いて、かつRubyで設定ファイルを書くことができるキーマッパーがなかったので作りました...なら理由としてカッコいいですが、実はそのようなキーマッパーは存在します。

k0kubun.hatenablog.com

DSLシンタックスもほぼ同じです(というかrumapのDSLはxremapのパクリです)。一時期は僕もxremapを使っていました。

ではなぜ作ったのかという話なんですが、xremap には with_modifier オプションが当時存在しなかったからです(今はあるかも)。

with_modifier オプション

with_modifier オプションの説明をするために、with_modifier オプションがない世界線の話をします。

Ctrl+hjkl で矢印キーを入力したい気持ちになることが人類にはよくあると思います*1。 これをxremapのDSLで表現するとどうなるでしょうか

remap 'Control-h', to: 'Left'
remap 'Control-j', to: 'Down'
remap 'Control-k', to: 'Up'
remap 'Control-l', to: 'Right'

こうなります。

これでめでたくCtrl+hjklで矢印キーが入力できるようになったわけですが、ところでテキストを編集するときにマウスを使わずに範囲選択をしたくなることは、人類にはよくあります。 Ctrl+hjkl のことを考えなければ、 Shiftを押しながら矢印キーを押してカーソルを移動する ことで、上記を実現できます。

「Shiftを押しながら」 なんだか怪しい雰囲気がしてきました。

実際、当時のxremapで、 Ctrl+hjkl の設定を入れた状態でShiftを押しながらCtrl+jを入力すると何が起こったかというと、何も起こりませんでした。 上の設定は Ctrl+j の設定であって、 Ctrl+Shift+j の設定ではないからですね。

xremapの動作をもう少し詳しく説明すると、

  • xremapは起動時に、Xのサーバーに対してwatchするキーの組み合わせを設定する
  • Xは、設定された組み合わせのキーが入力されたとき、その旨をxremapに通知する
  • xremapはキー入力の通知を受けると、キーを変換してXに入力する

のように動作するのですが、上の設定だと Ctrl+j の組み合わせしかwatchしないので、Ctrl+Shift+jが入力されてもxremapに通知が来ません。

これを解決するために、rumapで導入したのが with_modifier オプションです。

remap 'Control-h', to: 'Left', with_modifier: 'Shift'
...

このように with_modifier: 'Shift' を指定すると、rumapは立ち上げ時に、Ctrl+h に加えて Ctrl+Shift+hwatchします。そして、 Ctrl+h に変換し、Shiftも押されていた場合は Shift+← に変換します。つまり、 Ctrl+h (+Shift)← (+Shift) に変換します。

これによって、 Ctrl+Shift+hjkl を同時押しすることで、テキストの範囲選択が可能になります。

なんで本家にcontributeせえへんねん

当初は、xremapにpatchを当てることによって上記の問題を解決しようとしており。PRも出すには出しました。

Pass unmatched modifiers by genya0407 · Pull Request #36 · k0kubun/xremap · GitHub

この実装は、上で説明した with_modifier オプションとは違って、 全ての modifier の組み合わせをwatchします。 そして、通知されてきた入力のうち、設定されている部分について変換処理をします。

例えば、 remap 'Control-h', to: 'Left' と設定していたら、hを含む全ての入力(hCtrl+hCtrl+Shift+hCtrl+Alt+h、、、)をwatchし、Ctrl+Alt+j が入力されてきたら、 Alt+↓に変換する、といった感じです。

これは設定が短くて済むという利点はあるのですが、PRを出した直後にバグに気づいたのでPRを取り下げました。

もう記憶があんまりないのですが、たしか Alt+jCtrl+j みたいな設定をしていると、変換後の Ctrl+jj を含んでいるため通知が飛んできて、xremapが無限ループして死ぬ、というようなバグだったと思います。

この問題を解決するには、上で説明したような with_modifier オプションをDSLに導入すればよいのですが*2

  • DSLの文法についての交渉をするのが面倒だった*3
  • 既存のコードをいい感じに修正するのが大変そうだった(たしか)
  • xremapをコードリーディングして実装を概ね把握したことで、勉強も兼ねて自分で一から実装したくなった

などの理由から自分で実装することにしました。

その他、考えたことなど

作ってるときに考えたことなどをつらつらと書いていきます

x11ライブラリ

XのAPIを叩くために、x11というライブラリを使いました。これはXlibの非常に薄いラッパーで、Cの関数を呼ぶのでunsafeな操作ばかりです*4。ただ、薄いラッパーであることによって、Xlibのドキュメントがそのまま使えたのは助かりました。

一番困ったのはXlibのドキュメント(というかチュートリアル)がないことで、仕方がないので xremap の実装を何度も読み返しました。

Linux以外のOSに(理論上)拡張できるようにした

Linux以外のOSに拡張できるように、Linux(X)に依存する部分と、そうでない "コアロジック" を分離するようにしました。

これによって、(理論上は)例えばWindowsmacOSでも使えるように、rumapを拡張できるはずです*5

ただ、コアロジックと環境依存コンポーネントをうまく分離できていないのか、あるいは分離の仕方が悪いからかもしれませんが、コアコンポーネントのコードにジェネリクスが大量に発生してしまい、非常に読みづらく、またジェネリクス関連のコピペが多くなっています。

他環境にも拡張しないとただ単にコードの複雑性を増しただけになってしまうので拡張していきたい。しかし、macにはkarabinarがあるし、Windowsを使うことは基本的にないし、なかなかモチベーションが生まれないというのが正直なところです。

Cargoのworkspaceが便利

上に説明したコンポーネントの分離を実現するために、Cargoのworkspace機能を使ったのですが結構便利でした。

レポジトリを見てもらうと、 linuxmapper という2つのディレクトリが存在し、それぞれが1つのcrateになっています*6*7。 そして、Cargo.tomlに以下のように記述することで、1つのworkspaceとしてまとめています。

[workspace]
members = [
  "mapper",
  "linux",
]

この設定により、それぞれのcrateを、依存関係をもたせながら独立にビルドすることができます。

例えば、 linux/Cargo.toml に以下のように記述することで、linux crateからmapper crateを利用することができます。

[dependencies]
mapper = { path = "../mapper" }

また、以下のようにすることで、linux crate (と、それが依存するmapper crate)のみをビルドすることができます。

$ cd linux
$ cargo build

今回の例では全てのcrateがビルドされてしまうのでありがたみがないですが、例えば macos crate を追加したとき、Linux上では macos crate のビルドが通らないとしても、 linux crate のみをビルドすることが可能です。

共通機能を持ちつつもそれを利用したアプリケーションが複数あるような場合に、とても便利そうだなと思いました。

外部のRubyコマンドに依存するのをやめたい

xremap の「Rubyで設定が書ける」という特徴は革命的に便利なので、rumapでもこの特徴は引き継ぎたいと思っていました。xremapはmrubyで実装しているからrubyのコードを評価するのは簡単ですが、Rustでそれをどう実現すればよいでしょうか。

なんとrumapは、 外部のRubyコマンドを実行する ことで、設定ファイルを評価しています。具体的には、引数に指定されたファイルをinstance_evalして結果をJSON文字列にするRubyスクリプトを書いて、rumap起動時にそのスクリプトを起動して設定ファイルをJSONに変換し、JSONを元にrumapを初期化するようにしています*8

外部のRubyコマンドに依存するのはダサいし、外部要因で動いたり動かなかったりするのはつらいので、どうにかしたいと思っています。 この記事を書いてるときに見つけた以下のcrateを使えば、いい感じにできるはず。

github.com

まとめ

Rubyで設定を書けるLinux用のキーマッパーを作りました。ほとんど xremap のパクりですが、微妙に機能が増えています。

いろいろと実装上の反省点はあるのですが、自分が便利に使えているのでひとまずはいいかなと思っています。

*1:矢印キーまで指を伸ばすのはだるいので

*2:厳密には、Ctrl+h → Ctrl+h みたいな設定をしてると無限ループは回避できないけど、そんな設定をする人類はいないと仮定する

*3:よく考えると、後方互換性を保ったままDSLを拡張できるので割とすんなり行けたんじゃないかって気はする

*4:余談ですが、unsafeな操作は上で説明したコアロジックには存在しません。なぜなら、unsafeな操作をするのはX依存のモジュールだけだからです。

*5:実際には、X固有だと思ってたロジックがコアロジックだったとか、他の環境だとコアロジックが使い物にならん、みたいなのはあるとは思いますが、そのあたりはやってみないとわからない

*6:Cargo.tomlもそれぞれ持っている

*7:linuxがX依存部分、mapperがコアロジック

*8:JSONを作れるならなんでもいいので、例えばPythonDSLを作って組み込むことも可能なはずです。