0

Engine.IO単体でチャットを作った

友達に「Socket.IOはメモリリークヤバイから、新規開発なら迷わずEngine.IOを使え」と言われたのでEngine.IOを直接さわってみた。
Engine.IOはSocket.IOの内部で使われている部品で、「websocketでもajaxでも何でもいいのでとにかく通信ができる状態」を作るライブラリ。
もともとSinatra::RocketIOlinda-socket.ioRuby用socket.ioクライアントを作る時に中身は覗いてたけど単体で使った事はなかったし、どういう実装か忘れがちなのでメモする。

Chatを作った

とりあえずchatを作った。Herokuでwebsocketを動かしている。
サーバークライアントも50行ぐらいで書けた。



感想としては、Engine.io単体で使うと、綺麗なアプリケーションを書くにはその上に(フル機能ではないにしても)Socket.IO的な物を実装しないと無理だなと思った。やっぱり通信がしたいのではなくて、メッセージングがしたい。

なお似たような実装でSocket.IOでのチャットも以前作った。ひと通りの機能や挙動を確認するのにはチャットを作るのがよいと思う。


Engine.IOに無い機能(Socket.IOが実装してる機能)

Engine.IOはSocket.IOの後継、ではなく内部部品で、wsというwebsocket npmをラップしつつhttpからのupgrade機能とxhr-pollingやjsonpでのフォールバックを付けたライブラリ。
生のwsを直接使ってるのに近いので、バイナリのやりとりはしやすそうな気がする。

低レイヤーな分、気の利いた機能はない。そういうのはSocket.IOが担当している。

クライアントライブラリをpathに追加してくれる機能

自分でengine.io-clientからengine.io.jsを持ってきて、配置する必要がある。

Socket.IOでは/socket.io/socket.io.jsとかがサーバーのpathに現れてくれる。
ちなみにこの部分、connectのリクエストを横取りするためにEventEmitterが埋め込んでるコールバックのevents配列を直接いじっててすごい事になっている。


websocket切断時の自動再接続

websocketが切断されても、クライアントライブラリは自動再接続してくれない。on(‘close’)などのイベントで自分で接続し直す必要がある。

Socket.IOでは特に何も考えなくてもクライアントは再接続してくれる。サーバー側は60秒毎に全クライアントの生存確認を行っていて、そのためにクライアントは25秒ごとにheartbeatを送っている。

オブジェクトの自動シリアライズ・デシリアライズ

Socket.IOではオブジェクトをそのまま送りあえるけど、Engine.IOはsend(string or buffer)しかないのでJSONやmsgpackで自分でシリアライズ・デシリアライズしろという実装になっている。
JSONじゃない物が来た時とか、try catchして〜とやってると一気にコードが長くなってつらい。

リモートのイベントをdispatchする機能

emit(イベント名, データ)でリモートのイベントを発火させられない。
send(string or buffer)とon(‘message’, callback(string)) しかないので、受け取ったオブジェクトを自分でJSON.parseするなどして扱わなければならない。
EventEmitterとか使わないと巨大なswitch文が生まれるし、まあこの辺を自動的にやってくれてたのが、ものすごくコード量を減らしてくれてた事がわかる。

クライアント全員へのブロードキャスト

sockets.emitみたいなのは無いので、
for client_id, client of engine.clients
client.send JSON.stringify {type: "chat", message: "ハロー"}
のように自分でclients全員に送らなければならない

rooms機能が無い

クライアントをグループ分けするrooms機能は無い。
engine.io-roomsのようなプラグインを使う必要がある。


Engine.IOにある機能

on “packet”や”packetCreate”などの低レベルなイベントがあるので、デバッグに便利そう

0

node-webkitでNHK Newsを連続自動再生するアプリ作った

あまりにも世情にうといので作った。

NHKのRSSを定期的にチェックして、新着ニュースの動画を連続自動再生する。

とてもテレビっぽい。おかげで4日後にオリンピックが開催されるという事を知れた。



ソースコード
https://github.com/shokai/nhk-news-app

zipでダウンロード
https://github.com/shokai/nhk-news-app/releases


node-webkit

node-webkitはネイティブアプリ作成のためにwebkitが改造されたwebブラウザで、HTML/JavaScript/CSSが実行できるだけでなくnode.jsのAPIもそのまま呼び出せる。
つまりjQueryでDOM操作すると同時にnodeのライブラリを使うような処理が、同じプログラムファイルにまとめて書ける。変にブリッジを書く事なくいつものnodeのように require(‘モジュール名’) するだけなのでとても簡単。
今回は配布するアプリとして実装したけど、デジタルサイネージなどの端末にインストールして置いておく系アプリの実装にも便利だと思う。

アプリを作るのも、HTMLとJSとpackage.jsonを1つのzipにかためてnode-webkit.app/Contents/Resources/app.nwにリネームして配置するだけなので簡単だった。

node-webkit.app自体はhttps://github.com/rogerwang/node-webkitのdownloadsから安定版をダウンロードするといい(Macのrc版は開発パネルを開くまでjsが止まるバグがあった)

npmを使う場合も、node_modulesディレクトリ以下もzipにまとめてしまえば普通に読み込める。


くわしい解説は
node-webkitを触ってみた – 終わる世界とコンテンツ

https://github.com/rogerwang/node-webkitのquick startを見るとわかりやすい。


nhk-news.appの実装

色々工夫してる。ネイティブアプリで作らないと実装できなかったと思う。
最初ただスクレイピングしてmp4持ってきてローカルで再生させようと思って、NHKのニュースのHTMLを見ていたんだけど、どうしてもmp4のURLがわかりそうでわからなかったのでブラウザ拡張を作ろうと思った。
それで久しぶりにGreasemonkey使おうかと思ったけどせっかくだからnode-webkit使ってみた。

新着の取得

nodeのfeedparser npmでNHKのRSSから新着取得し、requestとcheerioとasyncで事前にニュースページのHTML内容を確認、動画が無いニュースは除外する

表示

新着ニュースをiframeに読み込む(jqueryで)
ふつうiframeの中が別ドメインだと操作できないけど、node-webkitだとsame-originポリシーが無いのか操作できた。
NHKニュースは動画のサムネイルをクリックするとFlashを読み込んで再生開始してくれる。iframeがロードされたらサムネイルにクリックイベントを送るようにした。
また動画の表示サイズを大きくしている。

動画の終了を判定

動画はFlashなので、再生状況を外部から取得できない。
定期的にWindow.capturePageする事で比較できた。DATA URLでjpeg/png画像が取れる。

nodeのライブラリ

ふつうnodeではnpm installするとnode_moduelsというディレクトリが作られて、その中にライブラリがインストールされる。node-webkitで使うにはnode_modulesディレクトリもまとめてzipに固めてしまえばいい。
cheerio – DOMのparser
request – HTTPリクエストする
async – 複数の非同期処理をまとめて扱える
feedparser – RSS/Atom Feedのパーサー
lodash – underscoreの速いやつ
eventemitter2 – eventemitterの速いやつ
を使った。npm installするだけでこれらをjQueryと一緒に使えるので楽で良い。

ビルド

Rakefileを書いた。rake debugでcoffeeのソースマップ付きjsを吐いてアプリに固めたりとか。
rake releaseで開発パネルが無効化されたアプリを書き出す。

0

Raspberry Piにnode.jsインストール

raspbianの場合、aptからインストールできるnodeは0.6.xなので古すぎる。
しかしソースからビルドするにもRaspberry PiのCPUがあまりにも弱いので2時間かかる。


よく見たらビルド済みのバイナリがあったのでコピーするだけだった

wget http://nodejs.org/dist/v0.10.24/node-v0.10.24-linux-arm-pi.tar.gz
tar -zxvf node-v0.10.24-linux-arm-pi.tar.gz
sudo mv node-v0.10.24-linux-arm-pi /usr/local/node

export PATH=/usr/local/node/bin:$PATH

node -v
v0.10.24

ただ、nodeだけgrunt-cliやsupertestのpackage.jsonがJSON parse errorにでインストールできない事がある。よくわからないけど

0

node-arduino-firmataを古いArduinoに対応させた

nodeにarduinoのコードを埋め込めるarduino-firmata npmを古いArduino(diecimila、duemillanove、Seeduino等)に対応させた。

arduino-firmataを使うとnodeとarduinoのそれぞれのコードを書いて通信させるのではなく、nodeの中にarduino.digitalWrite(13, true)とかarduino.analogRead(3)とか書けるのでコードが綺麗になって大変便利です。ご利用ください。
実装としては古来からあるMIDIをベースにしたFirmataというプロトコルを使っている。


現行最新のArduino LeonardやUNOとdiecimila/duemillanove等の違いは、USBシリアル通信変換機能が新しいArduinoのAVRマイコンには内蔵されているけど古いArduinoでは外部のFTDI等のチップを使っている事で、つまりPCから使う時にドライバが違う。

ドライバの差はserialport npmがどうにかしてくれるので良いが、node内でのシリアルポート関連のイベントのタイミングが違うので個別に処理をわける必要がある。
ちなみに同様の処理をRubyの方のarduino_firmata gemでも実装している。


古いArduinoと新しいArduinoを見分ける方法

USBのデバイス名で見分ける事もできるけど、それだと無数にあるArduinoクローンに対応できない。
古いArduinoを見つけたいだけなのでデバイスファイル名で判定した。

Debian/Ubuntu/Raspberry pi(raspbian)など
  • /dev/ttyACM0 -> Leonard, Micro, UNO
  • /dev/ttyUSB0 -> Decimilla, Duemillanove

Mac OSX
  • /dev/cu.usbmodem1234 -> Leonard, Micro, UNO
  • /dev/cu.usbserial-A1234 -> Decimilla, Duemillanove
という風にデバイスファイル名が違うので、正規表現 /usbserial|USB/ にマッチするボードは古いArduinoである事がわかる。


古いArduinoでやらなければならない処理

古いArduinoだとシリアルポートが開いてから実際に通信が可能になるまで2〜5秒程度待たなければならない。この時間は一定ではない。これはFTDIチップとそのドライバのせいだと思う。

また、nodeやRubyからREPORT_VERSIONをリクエストして、返答がArduinoボードから返ってきた後に、IOの初期化命令を送るのをだいたい3秒ぐらい待たなければならない。
新しいArduinoだとノータイムでIOの初期化命令を送って良い。
こっちはCPUの性能だと思う。REPORT_VERSIONが往復しているわけだからArduino側でFirmataのプログラムは起動して通信できているんだろうけど、何かが遅いっぽい。


FirmataにおけるArduinoボード初期化処理の流れ

IOの初期化とは、アナログピン0〜5番は逐次アナログ値を計測して送ってくれとか役割を指示する処理のこと。

シリアルポート開く→REPORT_VERSIONをボードに送る→versionが返ってくる→IOを初期化する
という流れだが、古いボードの場合
シリアルポート開く→(2〜5秒待つ)→REPORT_VERSIONをボードに送る→versionが返ってくる→(3秒待つ)→IOを初期化する
という風になる。IO初期化はarduinoから値が返ってこないのでちゃんと初期化できるタイミングまで待ってやる必要がある。

古いArduinoで使われているFTDIのチップは、RubyでもNodeでも、各言語のシリアルポートライブラリが発行するopenイベントより数秒経ってから実際の通信ができるようになるのと、ちょっとよくわからないけど古いAVRマイコンだとFirmata自体が動き始めるまで時間がかかるみたいでversionが返されてからも待たないといけないらしい。

この辺はFirmata内部のコードも全部把握してるけど、特に変な事はしてないみたいなので、マイコンの世代差だと思う。



という処理をしている。
主にこの辺でやってる
https://github.com/shokai/node-arduino-firmata/blob/v0.3.0/src/arduino-firmata.coffee#L58-L95

1

NodeにArduinoのコードを埋め込む arduino-firmata npm

作った。

インストール

% npm install arduino-firmata

https://npmjs.org/package/arduino-firmata


以前作ったruby版android版と同じ実装なのと、coffee-scriptがほとんどRubyなので一瞬でできた。
JSのビット演算子の優先順位がrubyなどより低いらしくて、より過剰にカッコつける必要があった。


既存のライブラリとの違い

johnny-fiveが有名だけど、より普通のfirmata(proce55ing版のオリジナルのやつ)っぽい書き方ができるようにした。関数名がarduinoと同じなので学習コストが低い。
firmataはdigitalRead/analogReadがコールバック返してくるけど、それは使いにくいので(例えばピン1がHIでピン2がLOWの時〜という条件式が書きにくい)値を返す普通の関数として実装した。

ようするにarduino使ったことがあるならすぐ使えるようにした。


使い方


まず
Arduino IDE -> [File] -> [Examples] -> [Firmata] -> [StandardFirmata]
をArduinoに書き込む。これでnodeからの命令を受けて動くarduinoになる。

くわしくは
https://github.com/shokai/node-arduino-firmata#readme


12, 13番ピンのLEDを点滅させる例
ArduinoFirmata = require 'arduino-firmata'
arduino = new ArduinoFirmata().connect()

arduino.on 'connect', ->
stat = true
setInterval ->
console.log stat
arduino.digitalWrite 13, stat
arduino.digitalWrite 12, !stat
stat = !stat ## blink
, 500

なおconnectはArduinoっぽいデバイスを適当に探して接続するが、引数にデバイスへのパスを入れて指定もできる。connect(“/dev/tty.usb-device”)

他にdigitalRead, analogRead, analogWrite, servoWrite, sysex、入力が変化した時のanalogChange, digitalChangeなどが実装してある。
https://github.com/shokai/node-arduino-firmata#io



socket.ioと一緒に使う例

samples/serverに入れておいた物の抜粋

  • センサー読んで値が変わったらsocket.ioでクライアントに送る
  • クライアント側でボタン押したらLED点灯/消灯

server.js
var io = require('socket.io').listen(app);

var ArduinoFirmata = require('arduino-firmata');
arduino = new ArduinoFirmata().connect();


// センサー(A0)の値が変化していたらHTML側に送る
arduino.on('analogChange', function(e){
if(e.pin != 0) return;
console.log(e);
io.sockets.emit('analogRead', e.value);
});

io.sockets.on('connection', function(socket) {

// 初回接続してきたクライアントに最新のセンサーの値を送る
socket.emit('analogRead', arduino.analogRead(0));

// HTML側のボタンをクリックしたら、LED点灯/消灯
socket.on('digitalWrite', function(stat) {
console.log("pin13:"+stat);
arduino.digitalWrite(13, stat);
});

});