0

Rackミドルウェアの作り方を勉強した

今スイスにいる。
行きの飛行機の中での勉強用にこのページを保存しておいて、Rack middlewareの作り方を学んだ。

第25回 Rackとは何か(3)ミドルウェアのすすめ:Ruby Freaks Lounge|gihyo.jp … 技術評論社

というのもSinatra::RocketIOをRack::RocketIOにしたいからなんだけど、Rack Hijack APIがよくわからない。(hijackについてはそのうち書く)


ソースコードはgithubに全部置いてある。
github.com/shokai/rack-plugin-study


Rack

Sinatra/Rails/Padrino等のRuby製webアプリケーションフレームワークと、
webrick/thin/mongrel/unicorn等などのRuby製webサーバーを接続するしくみがRackです。

Webアプリフレームワークが、どのwebサーバーででも動くように接続部分を抽象化してくれている。

以前rackの上にsinatra風のフレームワークを実装してみた時に、くわしく解説した。
SinatraっぽいWAFを作る、46行で


rack middleware

rack middlewareを作ると、WebサーバーがWebアプリケーションフレームワーク(WAF)とやりとりするデータを書き換えたりできる。
rack::auth系の認証プラグインとか、rack::cache系のキャッシュ系が有名。
どちらもリクエストとレスポンスの間をいじる処理で、プラグインをsinatraやrailsで実装するよりもrackでやったほうが楽だと思った。

rackの実装はほぼ安定していてWAFほどコロコロ変わったりしないし、rack middlewareとして実装しておけばrailsでもsinatraでもpadrinoでも使える。



rack middlewareを作って、sinatraアプリから使えるかどうか試した。

まずSinatraアプリを準備する

まず、単なるHTMLを返すSinatraアプリを作る。文章は感嘆符や句読点が色々欲しかったので、艦これwikiからお借りしました。


main.rb

get '/' do
'<p><img src="/shokai.jpg"></p>
<p><遠征の成功条件(暫定)></p>
<p>編成隻数</p>
<p>特定の艦種を一定数以上?(条件が無い遠征もある?要検証)</p>
<p>旗艦のLv</p>
<p>編成合計Lv?(下記参照、要検証) 敵地偵察作戦の場合、軽巡洋艦を1隻含む6隻構成、旗艦Lv20以上、編成合計Lvは駆逐艦のみで構成した場合に100Lv以上。</p>
<p>旗艦に軽巡を据える必要性は無く、旗艦:駆逐改でも成功することが確認されている。</p>
<p>また、編成艦船の最低Lvは関係無いことも判明している。</p>'
end

このmain.rbをconfig.ruからrequireして、rackupすればsinatraアプリが起動する。

config.ru

require 'rack'
require 'sinatra'

require File.expand_path 'main', File.dirname(__FILE__)

run Sinatra::Application

rackup config.ru -p 5000



アプリのレスポンスを書き換えるrack middleware


語尾がクマ語になるrack middlewareを作った。
https://github.com/shokai/rack-plugin-study/tree/master/kuma

こうなる


kuma_response_filter.rb

クラスを作って、initializeとcallだけメソッドを宣言する。
# -*- coding: utf-8 -*-
class KumaResponseFilter
def initialize(app)
@app = app
end

def call(env)
res = @app.call env # sinatraに処理させる
if res[1]["Content-Type"] =~ /^text\/.+/
res[2] = res[2].map{|body_part|
body_part.gsub(/([。!?])/){|s| "クマ#{s}"}
}
res[1]["Content-Length"] = res[2].map{|body_part| body_part.bytesize }.inject{|a,b| a+b }.to_s
end
return res
end
end
sinatraに処理させてから、そのレスポンスがtextの時のみ中身をいじってから返している。
感嘆符や句読点の前にクマを挿入するだけ。
Content-Lengthが変わるので最後に計算し直す。


config.ru

use クラス名 するだけでプラグインが有効になって、response bodyが書き換えられる。
require 'rack'
require 'sinatra'
require File.expand_path 'kuma_response_filter', File.dirname(__FILE__)
use KumaResponseFilter

require File.expand_path 'main', File.dirname(__FILE__)

run Sinatra::Application


JPEG画像をglitchしてから配信するrack middleware


https://github.com/shokai/rack-plugin-study/tree/master/image_glitch


JPEG画像がこうなる。glitchはmakimotoさんのスライドを参考にした

image_glitch.rb

Sinatraのレスポンスをみて、image/jpegだったらa/b置換glitchする。
module Rack
class ImageGlitch
def initialize(app)
@app = app
end

def call(env)
res = @app.call env # sinatraに処理させる
if res[1]["Content-Type"] == "image/jpeg"
body = []
res[2].each do |i|
body.push i.gsub('a', 'b')
end
res[2] = body
end
res
end
end
end


これも、config.ruで
use Rack::ImageGlitch
するだけで有効になる。


リクエストがアプリに届く前に修正するrack middleware


https://github.com/shokai/rack-plugin-study/tree/master/jpeg2jpg

試しに、 .jpg を .jpegや.JPEG と打ち間違えてもレスポンスが返ってくるプラグインを作った。

.JpEGとかでリクエストされても、Sinatraのlogには.jpgとしてリクエストされた、と表示される。
127.0.0.1 - - [09/Sep/2013 15:31:26] "GET /shokai.jpg HTTP/1.1" 200 - 0.0024


jpeg_request_filter.rb

PATH_INFO, REQUEST_PATH, REQUEST_URI全部書き換える。
# -*- coding: utf-8 -*-
class JpegRequestFilter
def initialize(app)
@app = app
end

def call(env)
# sinatraに処理させる前にrequestを書き換える
if env["PATH_INFO"] =~ /.+\.jpeg$/i
["PATH_INFO", "REQUEST_PATH", "REQUEST_URI"].each do |k|
env[k].gsub!(/\.jpeg$/i, ".jpg")
end
end
res = @app.call env # sinatraに処理させる
end
end


まとめ

リクエスト・レスポンスを加工するならrackでやったほうが楽な場合もある。

4

ArduinoとRubyで赤外線リモコン作ってWebから操作できるようにした

(追記)色々改良された → ArduinoとRubyで赤外線リモコン をgemにした

————-

帰宅前にスマホからクーラーをつけれるようにした。Arduinoと合計150円ぐらいの部品と、このRubyで書いたアプリ https://github.com/shokai/arduino_ir_remote だけで使える。

ソフトウェア部分はまだアップデートされるだろうけど、(rubygemにするとか)ハードウェアはもうこれ以上変更しないので是非自作してお試しください。
動かなかったり欠陥があったら、githubのissueかtwitterで@shokaiにどうぞ。


動いている証拠動画

実際に使いたいのはエアコンだけど、エアコンは地味なのでテレビでやってみた。


研究室にあるパナソニックとシャープのテレビで試したら両方とも動いた。
パナソニックのテレビから学習した赤外線データはgistに貼った。
データフォーマットについてもこの記事の下の方で解説してます。


経緯など

Linda家の温度を見れるようにしたら、だれもいない部屋の温度が高すぎるのが見えてしまい、帰るのが嫌になったので作った。日当たりが良すぎる。

以前KURO-RSという赤外線学習リモコンをRubyから使えるようにしたのだが、
うちの日立のエアコンの信号が特殊らしく操作できなかったので、赤外線学習リモコンも自作した。

日立エアコンだけでなく、富士通のエアコンでも動いたし、パナソニックやシャープのテレビも大丈夫だったから、たぶんどのメーカーの製品でも動くと思う。
赤外線学習リモコンが動かない理由は2つ考えられる。1.単に赤外線のデータが長くて保存しきれていない 2.信号をちゃんと解析しているタイプの学習リモコンの場合は解析に失敗している、のどちらか。
うちの日立のエアコンは富士通のに比べて3倍ぐらいデータが長かったので、たぶん電源on/offだけでなく温度風向き風量など現在の状態も全て送信している。
というわけでこの2つの問題に対処するために、1.大きめのbufferに保存するようにして 2.信号を解析せずに保存し再現出力するだけ、という仕様にした。


さらにRuby用のライブラリも作って、Sinatra RocketIOでWebアプリも作った。
赤外線学習リモコン


動かす

ソースコードはgithubに全部あり、回路も簡単なのでまず動かし方・設置方法を説明する。


材料

  • Ruby1.8.7以上が動くMacかLinuxマシンかRaspberry Pi
  • ArduinoとUSBケーブル 1個
  • 赤外線LED 1個(昔秋月で100個700円で買った)
  • 38kHz 赤外線リモコン受光器 1個(100〜150円ぐらい)

以上。
ArduinoはLeonardoMicroで動作確認した。
Seeduino v2(duemillanove互換)では動かなかった。下のほうに書いた赤外線LEDを38kHzで点滅させる処理が面倒くさい問題のせいだと思う。Arduinoコードのdelay時間をちょっといじればなんとかなると思うのでそのうちやる

Rubyが動くマシンやArduinoはそのへんに落ちているだろうし、150円ぐらいの追加で作れるのでやってみるといいのでは。


回路

回路は大変簡単だった。

Arduinoのデジタル3番ピンに赤外線リモコン受光器、 12番ピンに赤外線LEDを付けた。
赤外線リモコン受光器はいろいろあるけど、どれも5V/GND/DATAの3ピンで動く。
赤外線学習リモコン
これをエアコンの方に向ければいい。
けっこう離れていても届くけど、市販の赤外線LEDは指向性が強いので光が当たらない事がある。しっかり狙うか、LEDの頭を切断して拡散させるかするといい。

別の赤外線受光器も試してみたけど、ちゃんと動いた。
Learning IR-remote with Arduino and Ruby


インストール

githubからリポジトリをcloneして、
このファームウェアを書き込んで
https://github.com/shokai/arduino_ir_remote/blob/master/arduino/arduino.ino

あとはREADMEに書いてある通りに必要なgemをbundle installすればok


赤外線を読む

binディレクトリ内にCUIで赤外線読み書きできるツールが入っている。
ruby bin/arduino_ir_remote /dev/tty.usb-devicename


readして2.5秒以内に赤外線リモコンを照射したら、ターミナルに読み取ったデータがprintされる。
writeすると、1回前に読み取ったデータを発射する。


赤外線のデータを保存する

読み取った赤外線データは data.yml に保存しておくと名前を指定して発射できる。 sample.data.ymlみたいにすればいい。

sample.data.yml
"on" : "34,15,5,11,5,3,5,3,5,3,5,3,5,3,5,3,5,3,5,3,5,3,5,3,4,3,5,11,4,3,4,3,4,3,4,3,4,3,4,3,4,3,4,3,4,3,4,3,4,3,4,3,4,3,4,3,4,3,4,3,4,3,4,12,4,3,4,12,4,12,4,12,4,12,4,12,4,12,4,3,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,3,4,3,4,3,4,3,4,3,4,3,4,3,4,3,4,3,4,3,4,12,4,12,4,3,4,3,4,12,4,12,4,12,4,12,4,3,4,3,4,12,4,12,4,3,4,3,4,3,4,12,4,3,4,3,4,12,4,3,4,3,4,12,4,12,4,3,4,12,4,12,4,3,4,12,4,12,4,3,4,12,4,12,4,3,4,3,4,12,4,3,4,3,4,3,4,3,4,3,4,12,4,12,4,3,4,12,4,12,4,12,4,3,4,3,4,12,4,3,4,3,4,12,4,12,4,3,4,12,4,12,4,3,4,12,4,12,4,3,4,3,4,12,4,3,4,3,4,3,4,3,4,3,4,3,4,3,4,3,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,3,4,3,4,3,4,3,4,3,4,3,4,3,4,3,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,3,4,3,4,3,4,3,4,3,4,3,4,3,4,3,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,3,4,3,4,3,4,3,4,3,4,3,4,3,4,3,4,12,4,12,4,12,4,12,4,12,4,11,4,12,4,12,4,3,4,3,4,3,4,3,4,3,4,3,4,3,4,3,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,3,4,3,4,12,4,12,4,3,4,3,4,3,4,3,4,12,4,12,4,3,4,3,4,12,4,12,4,12,4,3,4,3,4,3,4,12,4,3,4,12,4,12,4,3,4,12,4,12,4,12,4,3,4,12,4,3,4,3,4,3,4,3,4,3,4,3,4,3,4,3,4,3,4,3,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,3,4,3,4,3,4,3,4,3,4,3,4,3,4,3,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,0"

"off" : "34,15,5,11,5,3,5,3,5,3,5,3,5,3,5,3,5,3,5,3,5,3,5,3,5,3,4,11,4,3,4,3,4,3,4,3,4,3,4,3,4,3,4,3,4,3,4,3,4,3,4,3,4,3,4,3,4,3,4,3,4,3,4,12,4,3,4,12,4,12,4,12,4,12,4,12,4,12,4,3,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,3,4,3,4,3,4,3,4,3,4,3,4,3,4,3,4,3,4,3,4,12,4,12,4,3,4,3,4,12,4,12,4,12,4,12,4,3,4,3,4,12,4,11,4,3,4,3,4,3,4,12,4,3,4,3,4,12,4,3,4,3,4,12,4,12,4,3,4,12,4,12,4,3,4,12,4,12,4,3,4,12,4,12,4,3,4,3,4,12,4,3,4,3,4,3,4,3,4,3,4,12,4,12,4,3,4,12,4,12,4,12,4,3,4,3,4,12,4,3,4,3,4,12,4,12,4,3,4,12,4,12,4,3,4,12,4,12,4,3,4,3,4,12,4,3,4,3,4,3,4,3,4,3,4,3,4,3,4,3,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,3,4,3,4,3,4,3,4,3,4,3,4,3,4,3,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,3,4,3,4,3,4,3,4,3,4,3,4,3,4,3,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,3,4,3,4,3,4,3,4,3,4,3,4,3,4,3,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,3,4,3,4,3,4,3,4,3,4,3,4,3,4,3,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,3,4,3,4,12,4,12,4,3,4,3,4,3,4,3,4,12,4,12,4,3,4,3,4,12,4,12,4,12,4,3,4,3,4,3,4,3,4,3,4,12,4,12,4,3,4,12,4,12,4,12,4,12,4,12,4,3,4,3,4,3,4,3,4,3,4,3,4,3,4,3,4,3,4,3,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,3,4,3,4,3,4,3,4,3,4,3,4,3,4,3,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,12,4,0"


赤外線データの記録方式

上の数字列は34,15,5,11の場合、3400マイクロ秒間38kHzの赤外線を発射、1500マイクロ秒間停止、500マイクロ秒間38Khzの赤外線を発射、11マイクロ秒停止という意味。

赤外線リモコンは自然光と区別するために38kHzの赤外線が300〜600マイクロ秒続く状態をon、何もなし状態が300〜600マイクロ秒続く状態をoffとして送信する。メーカーによって時間が違う。

elm-chan.orgの解説がわかりやすい。
赤外線リモコンの通信フォーマット

これを赤外線リモコン受光器に入力すると、38kHzの赤外線が入っている時にLOW、何もなし状態の時にHIGHが出力される。

信号データの内容を解析するのは面倒だったので、リモコンから来たのと同じ赤外線を再現するようにした。
赤外線リモコン受光器を監視して、出力がHIGH/LOWで変化した時間をmicros()で保存した。
データが1byteに収まるように、100マイクロ秒を1単位とした。


Webから使えるようにする

WebUIディレクトリ内にSinatraアプリがある
READMEに書いてある通りbundle installしてrackupすれば起動する。
上のディレクトリのdata.ymlを読む。

環境変数ARDUINOにArduinoのデバイス名を渡しておく必要がある。

赤外線学習リモコン

アプリは外から勝手に操作できないようにbasic認証を付けられる。
環境変数BASIC_AUTH_USERNAMEとBASIC_AUTH_PASSWORDで設定する。


サービスとしてインストール

WebUIのREADMEに書いてあるとおりforemanで家のMacのlaunchdにインストールした。
nginxのvirtual hostでremote.shookai.orgに置いてる。


苦労したことなど

そもそもちゃんと赤外線が読めているのか、ちゃんと38khzで発射できているのかがよくわからず苦労した。

試作

これを作る前に、2種類の方式で赤外線学習リモコンを試作した。
200マイクロ秒ごとに38kHz赤外線のon/offをbool配列で記録するbinary方式と、on/offが切り替わるまでの時間を記録するinterval方式を作った。

これはbinary方式。デジタルピン2番にスイッチを接続して、pushしたら2.5秒以内に赤外線リモコンを読ませる。シリアル通信で赤外線データを出力してくる。


38kHzを作るのが難しい

binary方式interval方式でdelayMicrosecondsの時間が違う。
ループや条件式の計算の処理時間があるので、それぞれの方式で別のdelay時間になってしまった。

そもそもPWMを使って38kHzを作ればいいんだけど、最初はStandardFirmataに赤外線学習リモコン機能を付けた物を作りたくてdelayとIO操作のみという縛りでやっていて、信号は読み書きできるんだけどRAM容量が足りなくて動かなかったからこうなった。
FirmataじゃないのになんでPWM使わない縛りやってるんだろうって気分に今なってる。

0

GitHubの自分の全リポジトリのissueを一覧するwebサービス作った

GitHubのissue機能が好きすぎてヤバイので、最近90日以内に更新した全リポジトリのopenなissueを表示するサービスを作った。


何か思いつく毎にissueに書いて開発してて、手詰まりになったり精神が調子悪くなったら一旦手を止めて別のプロジェクトのissueを潰すようにしてると永遠にプログラマーズハイ状態を維持できるんだけど、自分の持っているissueを一覧できるページがGitHubにない。
GitHubには通知機能があるけど参加したりアサインされたissueしか通知されないので、全く手付かずのissueはだんだん下の方に下がっていって見つからなくなってしまう。
なので自分の持っているリポジトリ全体のissueを一覧したかった。


最近は21個のリポジトリをいじってて、73個もissue溜まってるんだなという事がわかる。

GitHub Issue Viewer



ソースコード
shokai/github-issue · GitHub


経緯

もともとこれ
githubの自分のリポジトリのissue一覧
を使っていたんだけど動かなくなったのでカッとなって作った。でもさっきgithubのパスワードリセットしたら動くようになった。意味なかった。
意味ないけどスマホでも見れるからまあいいかも・・


実装

SinatraでGitHub OAuthする (2)にissue表示する機能を足しただけなので、だいたい2時間ぐらいでできた。
issueの取得には時間がかかるから、1つリポジトリ取得する毎にsinatra-rocketioでブラウザに送っている。
取得したissueはmemcachedに3日間とっておいてる。

0

twiticonをTwitter API v1.1対応した

twiticonはこのblogの右上にも出しているtwitterアイコンのサービス。SinatraとHerokuで実装している。

Twitterアイコン画像のURLは超長いし、新しいアイコンをアップロードする毎に変わるのだが、twiticonを使うと

<img src="http://twiticon.herokuapp.com/shokai">
<img src="http://twiticon.herokuapp.com/shokai/mini">
などで埋め込めるようになる。gyazzなどで使っている。


Twitter API v1.0が終了したのだが、1.1ではprofile_image_urlのAPIが無くなった。
代わりにusers/showでprofile_image_urlが取得できるが、各種サイズのURLは無くnormalサイズだけ。

biggerやminiなどのサイズはprofile_image_urlの末尾を_normal.pngから_bigger.pngなどに置換すると得られる。

また、users/showは認証しないと読めなくなったので@shokai_twiticonというアカウントを作ってその権限でOAuthしている。


ソースコード
shokai/twiticon · GitHub
twitterなどの設定は全て環境変数で入れる。

twitterのoauth tokenを設定する必要があるので、取得するためのツールを bin/get_twitter_oauth_keys.rb に入れておいた。
使い方はREADMEに書いた

0

SinatraでGitHub OAuthする (2)

SinatraでGitHub OAuthするの続き

試しにGitHub認証して、成功したらリポジトリとGistの一覧を表示するだけのアプリを作ってHerokuに置いておいた
http://sinatra-github-oauth-sample.herokuapp.com/

ソースコード
shokai/sinatra-github-oauth-sample · GitHub


https://github.com/settings/applicationsからRegister new Applicationした
OAuthのコールバックは /auth.callback に来る。



右上からログインすると


一度GitHubに移動してから戻ってきて、GitHub APIが使われてリポジトリとGistの一覧が出てくる
右上にGitHubに登録したアイコンが表示される。


1. GitHub認証して(これはSinatraでGitHub OAuthするに書いた)
2. 乱数とMD5でセッションID作ってブラウザに渡して
3. memcachedにセッションIDをkeyにしてgithubの情報を入れておく(2週間でexpireする)
4. トップページでは、ログイン状態ならoauth tokenをoctokitに渡してGitHubからリポジトリ/Gist一覧を取得する(これはmemcacheで1時間保持する)

必要な時にmemcachedからユーザー情報を取り出せるhelperを書いた
libs/cache.rbcontroller/auth.rbに処理を詰め込んであるので、この2つだけコピーすれば他のSinatraアプリにもGitHubログイン機能追加できるはず。

RackやSinatraのログイン系のプラグインを使っても良いかと思ったが、memcachedクライアントにpure rubyかつバイナリプロトコルサポートしているdalliを使いたかったのと、
1つのmemcachedのkeyにprefix付けてログイン情報とリポジトリ/Gist一覧を保存できるようにしたかったので自分で書いた libs/cache.rb に任せた。配列のようにアクセスするとkeyにprefixが付いて読み書きできて便利。