0

プログラマブル電球hueをRubyから使う

研究室にPhilipsのhueが来たので使ってみた。amazonでも売ってるけど3万円ぐらいする高級品だったのでおそるおそる扱った。


hueの仕様

bridgeと呼ばれる円盤型の母艦に、15個のhue電球が無線接続(zigbee?)される。
bridgeは有線LANケーブルで接続され、HTTPのAPIを持つ。
OSはFreeRTOSで、lwIPでTCP/IPを実装している。

bridgeにはボタン1つとステータスLEDしか付いていないが、ローカルIPアドレスが http://www.meethue.com/api/nupnp で確認できて、あとはwebブラウザから見れる。

電球部分はごく普通のE26口金なので、家の天井や電気スタンドで使えるはず。


API

http://developers.meethue.com/にドキュメントがある。

HTTPのAPIなので、同じLANの中からならどのマシンからでもAPIが使える。
jsonでやりとりする。
HTTPヘッダにAccess-Control-Allow-Origin=*が付いているのでブラウザ上のJavaScriptからもクロスドメイン制限なしにAPIが使える。

最初にAPIを使ってユーザー登録をしなければならないのだが、”1234567890″というユーザーがデフォルトで使えるようになっている・・気がする。

hue Rubygemではコンストラクタでユーザー名を指定しない時に自動的に”1234567890″を使うようになっているので、認証の事を全く考えずにコードが書ける。

curlで使う場合はこんな感じ
## ユーザー登録し、usernameを取得する
## usernameは自分で指定できるはずだが、なぜかエラーが返ってくるのでdevicetypeに名前を入れておいた
curl -d '{"devicetype":"shokai"}' http://192.168.1.130/api
## これだけ、bridge上部のボタンを押してから30秒以内に実行する必要あり

## bridgeのステータス取得
## 返り値がjsonなのでjqが便利
curl http://192.168.1.130/api/(username)/config | jq .

## 電球のステータス表示
curl http://192.168.1.130/api/(username)/lights

## 電球3のくわしいステータス表示
curl http://192.168.1.130/api/(username)/lights/3

## 電球3の名前を変更
curl -X PUT -d '{"name":"shooookai"}' http://192.168.1.130/api/(username)/lights/3

Rubyから使う

gemがあるので使いましょう

gem install hue

しかしsaturationとbrightnessが指定できないバグが有ったので、修正してpull requestを送っておいた

そのうち取り込まれて治ると思うけど、それまではGemfileに俺のgithubのブランチを指定してインストールすればいいと思う。
特にbrightnessは最大になってないっぽかったので、修正したらちょっと明るくなった。
source 'https://rubygems.org'

gem 'hue', :git => 'https://github.com/shokai/hue.git', :branch => 'bugfix_saturation_brightness'


実行コマンドも同梱されている。全部の電球をまとめて点灯消灯はできたけど、1つずつ個別にはできなかった。
これもバグがあるのか未実装なのかわからない。
hue all on
hue all off


色変化なしで、ただ点滅させるだけの例
require 'hue'

hue = Hue::Client.new
light = hue.lights[0]

loop do
light.on = true
sleep 0.5
light.on = false
sleep 0.5
end


HSBをそれぞれランダムに操作する例。hueだけ65536段階設定できる。
require 'hue'

hue = Hue::Client.new
hue.lights.each do |light|
light.on = true
end

loop do
hue.lights.each do |light|
puts light.name
light.hue = rand 65535
light.saturation = rand 255
light.brightness = rand 255
end
sleep 0.1
end


HSBをまとめて指定する例。1命令で全部指定するので実行速度が速い。
set_stateは第2引数に変化の速度を指定できる(100ミリ秒単位)
この例だと明度彩度は固定で、色相のみ0.1秒ごとになめらかに変化させる。
require 'hue'

hue = Hue::Client.new
light = hue.lights[0]

loop do
0.upto 65 do |i|
state = {:on => true, :hue => i*1000, :saturation => 255, :brightness => 255}
p light.set_state state, 1
end
end

Lindaでhue

RocketIO::Lindaでhueを操作できるようにした。まだ電球個別に操作はできないけどそのうちやる。

shokai/linda-hue


hueを触ってみて

色をゆっくり変化させると気づかない。これには一長一短あると思う。
気にならないとも言えるが、何かを通知するには気づかないのは良くないかもしれない。
表現したい物による。

何か集中して作業している状態から、hueに強制的に意識を向けさせるには、パッと色を変えるか、一瞬消灯や点滅させる必要がある。

集中力を切らせずに、いつでもいいからそのうち気づいて欲しい情報を提示するには、色をゆっくり変化させられる機能は重要かも。


hueっぽい物の自作

もう1セットhueがあると2地点に設置できるんだけど、無いので、ソリッドステートリレーとArduinoでhueっぽい物を自作した。
ハロゲンランプだから単色だけど、全力で点滅させるとhueの倍ぐらいの速度でチカチカできる。

0

RocketIO::Linda 1.0.0からのプロトコルが少し変わった

v1.0.0から、Sinatra::RocketIO::Lindaのプロトコルを少し変更しました。
なのでアップデート推奨です。

http://linda.shokai.org/shokaihttp://linda.masuilab.org/delta も既に新しいプロトコルで動いています。


Linda

これです

橋本商会 » Ruby上に並列言語拡張Lindaを実装してWebSocket/Cometで使えるようにした
shokai/sinatra-rocketio-linda
shokai/em-rocketio-linda-client
研究室や家では20ノードぐらいのLindaクライアントがセンサーや色々のデータをやりとりしています。

アップデート

Ruby2.0以上を使っている場合はgemだけアップデートすれば、アプリのコードは変更せずにそのまま動きます。

gem install sinatra-rocketio-linda

変更点


これが
tuple_space.watch [1,2] do |tuple|
p tuple
end

こうなった
tuple_space.watch [1,2] do |tuple, info|
p tuple
puts info.from
end

read/take/watchのコールバック関数の引数が増えた。
全てのタプルに、書き込み元のIPアドレスが付くようになった。
例えば認証とか、LAN内から書き込まれたタプル以外は信用しないLindaクライアントとか、そういうのに使う。

今のところinfoにはfrom以外の値は入っていないけどそのうち増える。

0

rubygems.orgのauthorsとownersの違いと追加方法

複数人でメンテしているrubygemは、AuthorsとOwnersにコントリビューターを追加しておきたい。

こういう状態

nokogiriとかは、Authorsが4人なのにOwnersが2人になっている。

これはどういうことかというと

  • Authorsはgemspecファイル内のauthors配列とemail配列で指定する
  • Ownersはgemコマンドでrubygems.orgにメールアドレスを追加する
という事になっている。

Authorsはただ名前が表示されるだけ。
Ownersに追加された人はそのgemをrubygems.orgにリリースする権限を与えられる。

Authorsの追加


Gem名.gemspec に書けばいい
Gem::Specification.new do |spec|
spec.name = "babascript"
spec.version = BabaScript::VERSION
spec.authors = ["Sho Hashimoto", "Takumi Baba"]
spec.email = ["hashimoto@shokai.org", "contact@mail.takumibaba.com"]


Ownersの追加


gemコマンドで追加できる
gem owner Gem名 --add hashimoto@shokai.org

rubygems.orgにユーザー登録した時のメールアドレスを指定する。

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でやったほうが楽な場合もある。

0

Rubyのrescueって1行で書くと値を返すのね・・

考えてみれば当たり前か。


シリアルポートからの入力を読むread関数が、たまにEOFErrorを投げてくるので無視したい。
しかしEOFError以外は握りつぶしたくない。
色々省略するけど、こういう感じで書いてたんだけど

require 'serialport'
serialport = SerialPort.new("/dev/tty.devicename", 9600, 8, 1, 0)

def read
serialport.read rescue EOFError ## たまに来るEOFErrorだけを無視するつもりだった
end

loop do
puts read
sleep 1
end


こういう実行結果になる。rescueで拾ったEOFErrorがreturnされてた。
EOFError
(デバイスからのデータ)
EOFError
EOFError
(デバイスからのデータ)
EOFError
(デバイスからのデータ)
EOFError


関数readを、こうして1行のrescueで書かないようにすればEOFErrorの時はnilが返る。
def read
begin
result = random_error
rescue EOFError
end
end


nilなので、改行がputsされる

(デバイスからのデータ)


(デバイスからのデータ)

(デバイスからのデータ)