0

Node用rocketio-clientを作ったのと、npmの作り方

Sinatra::RocketIOのNode.js用クライアントを作った、という事を書くのを忘れていた。
自作Nodeモジュールをnpmjs.orgへ登録する方法もまとめる。


https://github.com/shokai/node-rocketio-client
https://npmjs.org/package/rocketio-client


感想

npmに初めて登録したけど、rubygems.orgの良い所はそのままにより洗練されてる感じがした。
あとcoffee-script、昔触った時は何これすっげえキモイと思って無理だったんだけど、その頃よりも構文が充実してたし、
Scalaを少し勉強したお陰で目が慣れてて普通に書けるようになってた。

Rubyとscala書ける人はcoffee-scriptすんなり入れると思う。


インストール

npm install rocketio-client

使う


ごく普通にSinatra::RocketIOに接続できる。websocketとcometの使える方が選択される。
RocketIO = require 'rocketio-client'
io = new RocketIO('http://localhost:5000').connect()

io.on 'connect', (io)->
console.log "connect!! (#{io.type})"
io.push 'hello', 'hello world'

io.on 'echo', (data)->
console.log "echo> #{data}"


サーバー側はこんな感じ
main.rb
io = Sinatra::RocketIO

## receive "hello" from client
io.on :hello do |message, client|
puts "> receive '#{message}' from #{client.session} (#{client.type} #{client.address})"

## push "echo" to client
io.push :echo, message
end


自作nodeモジュールをnpmjs.orgに登録する方法

package.jsonを作る

% npm init
対話形式でライブラリの名前やauthor、ライセンス形態を質問されるので答えるとpackage.jsonが生成される

package.jsonを直接編集して、必要あれば項目を追加する。
  • “dependencies” に実行時の依存ライブラリを書く
  • “devDependencies” に開発時に必要なライブラリを書く
  • 実行可能コマンドも配布する場合、 “bin” にpathを書く
  • “main” で指定されたファイルが、require ‘パッケージ名’ された時に読み込まれるファイルになる

もう一度npm initすれば対話形式で再編集できる。


ディレクトリ構成

.
├── History.txt
├── Makefile
├── README.md
├── lib
│   └── rocketio-client.js
├── node_modules

│  (node_modules内は多すぎなので省略)

├── sample
│   └── sample.coffee
├── server
│   ├── Gemfile
│   ├── Gemfile.lock
│   ├── Procfile
│   ├── README.md
│   ├── config.ru
│   ├── lib
│   ├── main.rb
│   ├── npm-debug.log
│   ├── public
│   │   ├── index.js
│   │   └── jquery.min.js
│   └── views
│   └── index.erb
└── src
   └── rocketio-client.coffee


プログラムを書く

他の人のnpmを見ていると、大抵libディレクトリの下にjsを置くようだ。

coffee-scriptを使ってる場合は、lib以外のディレクトリ(src等)を作ってcoffeeはそこに置いて、
都度jsにコンパイルしてlibに書き出す。
Makefileでやった。

サンプルコードを書く

requireはモジュール名やファイル名ではなくディレクトリ名を指定した場合に、そこにpackage.jsonがあれば”main”の項目に指定されたファイルを読み込んでくれる。

なのでsample/sample.coffeeからrocketio-clientをrequireするには
RocketIO = require '../'
と相対パスで書けばいい。


README.mdを書く

markdownでREADME.mdを書くとnpmjs.orgで綺麗に表示してくれる


publish

% npm publish
npmの開発者アカウントを作っていない場合は「adduserしろ」等の指示が表示される。言われたとおりにやる。

% npm adduser
指示に従って入力していけばok

これでもう公開されているので、誰でもインストールしてすぐ使える。


開発中のnpmを公開前にローカルで試す方法

package.jsonのdependenciesにはgitリポジトリを指定できるので、それでインストールできる。

もしくは普通nodeで開発している時はプロジェクトルートにnode_modulesというディレクトリができてその中にnpmがインストールされるので、シンボリックリンクでローカルの開発中のnpmに差し替えるという手もあると思う。

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以外の値は入っていないけどそのうち増える。

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

RocketIOのチャットサンプルを全力で改造して画像チャットにした

ここにある
http://chat.shokai.org

herokuでも動いている
http://rocketio-chat.herokuapp.com

ソースコード
shokai/rocketio-tiqav-chat · GitHub


このように1文字入力するごとにTiqav.comで画像検索する。
レス画像検索No.1/画像会話なら ちくわぶ

画像をマウスクリックすれば画像が投稿される。
enterキーを押せばそのままテキストが投稿される。


http://chat.shokai.org/shokai
http://chat.shokai.org/test
のように部屋も無限に作れる。


作った経緯

RubyHirobaでLTして気づいたのだが、もともとchatはRocketIOの最も簡単なサンプルとして作っていた。
サンプルなので、難しい事するとわからなくなるからシンプルに留めていた。

でももっと単純なhello worldサンプルができたので、チャットは手加減する必要なくなった。
shokai/rocketio-hello-world · GitHub
http://hello.shokai.org

むしろ、「それなりに込み入った操作をするUIと通信の連携方法」や「RocketIOのチャンネル機能」の例が必要かと思ったので、
それなりに複雑なアプリを作った。


実装

チャットログはサーバーのオンメモリに保存している。

画像検索は、HTML上のinput要素を監視して変化があれば都度RocketIOでサーバーに文字列を送る。
サーバー(Sinatra)がTiqavに検索をリクエストする。(tiqav gemを使っている)
[ブラウザ]<--(RocketIO)-->[Sinatra]<--(HTTP)-->[Tiqav.com]


同じ文字列を何度も検索するのは無駄なので、ブラウザ上とサーバー上でそれぞれJSのオブジェクトとmemcachedでキャッシュを行なっている。
[ブラウザ/JS Cacheオブジェクト]<--(RocketIO)-->[Sinatra/memcached]<--(HTTP)-->[Tiqav.com]


Tiqav.comにはあまりリクエストが飛ばないようになるので、かなりリアルタイム気味にチャットと画像検索できる。


サーバーで検索する時はEM::defer内でリクエストを飛ばす。
以前HerokuのSinatraにバックグラウンドワーカーを詰め込んで節約で説明したが、Sinatraはシングルスレッドだけど、IO待ちする処理をEM::deferで包むと他のクライアントを待たせずにレスポンスを返せるようになる。


EventEmitterの活用

RocketIOに付属しているEventEmitterを使うと、複雑なUIと通信をうまく連携させられる(ブラウザ用EventEmitterを作った
EventEmitterはあらゆるクラスやインスタンスにイベント機能をmixinできるライブラリです


画像chatのjavascriptは超シンプル。
input要素が変更されたら画像検索して結果をHTMLに表示している。
var io = new RocketIO().connect();
var img_search = new ImageSearch(io);

$(function(){
var chat_input = new InputWatcher("#message");

chat_input.on("change", function(val){
img_search.search(val);
});
});

img_search.on("result", function(res){
$("#img_select").html(""); // clear DOM
for(var i = 0; i < res.imgs.length; i++){
(function(){
var img_url = res.imgs[i];
var img_tag = $("<img>").attr("src", img_url);
$("#img_select").append( $("<li>").html(img_tag) ); // display each Images
})();
}
});


まずInputWatcherというinputタグの中身を監視して”change”イベントを発行するクラスを作る。
中身が変わった時だけchangeイベントが発行される。
// watch text-input and emit event on "change"
var InputWatcher = function(target){
var self = this;
new EventEmitter().apply(this);
this.target = (target instanceof jQuery) ? target : $(target);
var last_val = null;
var watch = function(){
var val = self.target.val();
if(!!last_val && last_val !== val) self.emit("change", val);
last_val = val;
};
setInterval(watch, 100);
this.target.keyup(watch);
};


changeイベントから画像検索する
$(function(){
var chat_input = new InputWatcher("#message");

chat_input.on("change", function(val){ // regist "change" event
img_search.search(val);
});
});


画像検索をRocketIOでリクエストして、結果が返ってきたら”result”イベントを発行するImageSearchクラスを作る。
内部にcacheを持っていて、一度検索したことのある文字列はサーバーに送らずに即”result”イベントを発行する。
コンストラクタにRocketIOインスタンスを渡して、通信にはそれを使うようにする。
// request image-search to server with RocketIO
// emit "result" event on receive image-url-array
var ImageSearch = function(io){
var self = this;
if(!(io instanceof RocketIO)) throw new Error("Argument must be instance of RocketIO");
var cache = {};
new EventEmitter().apply(this);
io.on("img_search", function(data){
if(!data || typeof data.word !== "string" || !(data.imgs instanceof Array)) return;
cache[data.word] = data.imgs;
self.emit("result", data); // "result" event
});
var eid = null;
var last_word = null;
this.search = function(word){
if(!!eid) clearTimeout(eid);
if(typeof word !== "string") return;
if(word.length < 1){
self.emit("result", {imgs: [], word: ""}); // "result" event with Empty images
return;
}
// check Cache
if(cache[word] instanceof Array && cache[word].length > 0){
self.emit("result", {imgs: cache[word], word: ""}); // "result" event with Cached images
return;
}
// if NOT Cached
eid = setTimeout(function(){
eid = null;
cache[word] = [];
io.push("img_search", word); // request to server
}, 300);
};
};


最終形。
さっきのInputWatcherと合わせて使うとこうなる。
var io = new RocketIO().connect();
var img_search = new ImageSearch(io);

$(function(){
var chat_input = new InputWatcher("#message");

chat_input.on("change", function(val){
img_search.search(val);
});
});

img_search.on("result", function(res){
$("#img_select").html(""); // clear DOM
for(var i = 0; i < res.imgs.length; i++){
(function(){
var img_url = res.imgs[i];
var img_tag = $("<img>").attr("src", img_url);
$("#img_select").append( $("<li>").html(img_tag) ); // display each Images
})();
}
});