Node.jsが動くマイコンボードtesselを買ったのでさっそくカメラモジュールwifitiny-routerというWAFを使ってtessel自体をwebサーバーにし、撮影した画像を配信できるようにしてみた。


ライブカメラというほどの速度は出ないけど、tessel単体で定期的な撮影とhttpでの配信ができた。

マイコンでNode.jsが動くとはいっても、例えばTCPのlistenはWiFiチップと通信しているからチップの状態次第で失敗するし、WiFiの設定をしている間にcameraのreadyイベント取りこぼしたりするから色々とタイミングがシビアでつらい所がある。でもNodeなのでeventemitterがあるからそれほどストレスフルではなくて面白い。


tessel


WiFi

tesselのwifiは802.11b/gの1〜11チャンネルしか使えない。WiFiアクセスポイントがチャンネル12〜14の場合接続できないので注意する。

tesselのwifiは弱いので同じチャンネルが混んでいると接続できない。

Macだと
% /System/Library/PrivateFrameworks/Apple80211.framework/Versions/A/Resources/airport -s
で確認できる。


普通のwebサーバー

まず普通のwebサーバーはこんな感じで書ける。これはnodeのhttpモジュールしか使ってないので、Macでもtesselでも動作する。
tessel-study/main.js at master · shokai/tessel-study
var http = require('http');

http.createServer(function(req, res){
res.writeHead(200, {'Content-Type' : 'text/html'});
res.end('<h1>Hello!!</h1>');
}).listen(80);

console.log('server start at PORT: 80');


普通のwebサーバーの起動

tesselをUSBで接続して、Macにnodeをインストール、ボードのファームウェアアップデート、wifiに接続する
% brew install node
% npm install tessel -g
% tessel update
% tessel wifi -n [WiFi AP NAME] -p [PASSWORD] -s wpa2

そして起動
% tessel run http-server.js

これでtesselが普通のwebサーバーになる。簡単。
tesselのボード上での標準/エラー出力もCLIに表示される。

単体で動作させる

runではなくpushするとプログラムをフラッシュメモリに保存して、Macから離しても単体で動くようになる。
% tessel push http-server.js


単体で動かない

動くと思ったら動かない。socketが開けないエラーが出る

Error: ENOENT: Cannot open another socket.



tesselはLuaでNode.js互換の実行環境を実装してあって、そこに手元で書いたjsファイルとnode_modulesディレクトリ以下を全部Luaに変換して送り込んで実行している。tessel上ではjavascriptではなくLuaが動いている。(なのでevalは使えない)

で、netなどのネットワーク周りのライブラリもTCPをListenする関数とかちゃんと実装してあるんだけど、その実体はtessel上のwifiモジュールのICと通信するという事になっている。

このプログラムが動かない原因は、tesselのMPUが起動してnodeが走っていてもまだwifiモジュールが起きていない(or wifiに接続されていない)から、netまわりの関数が実行できていないということのようだ。


単体で動かせるwifi+httpサーバ+カメラモジュールを実装する

という事で色々とマイコンの気持ちになって実行順を考えて実装したらうまく動くようになった。

tesselはカメラやwifiのモジュールのラッパーがnpmになっているので、インストールしておく。アプリケーションのnode_modules/ディレクトリ以下にインストールしておくと全部まとめてLuaに変換されてtesselに転送される。
% npm i wifi-cc3000 camera-vc0706 tiny-router -save


まず工夫した所・ハマりどころを書いておく。

tiny-routerを使う

expressはtesselで動かせなかった。Errorのスタックトレースを読むあたりの関数とか、色々とtesselのNodeではまだ実装されていない部分があって動かない。
tiny-routerという組み込み用の軽量WAFを使うと軽くて良かった。

wifiを毎回リセットして、接続成功してからhttpサーバー立てる

wifiのAPI見ると、自分でwifiをリセットしたり、wifi.on(‘connect’, callback)とかがある。
毎回プログラムが起動するたびにwifiモジュールをリセットして、on “connect”イベントの後でhttpサーバーを立てるとtessel pushでもtessel runでも動かせる。
既にwifi接続していたらすぐhttpサーバー立てる、接続してなかったらon “connect”で立てる、という方がスマートじゃんと思ったけど、開発中のプログラムが不正終了した場合に(たぶん)wifiモジュールが既にTCPをlistenしているのにMPUはlistenしてないような感じになってて最終的にUSBも認識しなくなったりする事が頻発したので、毎回wifiリセットするようにした。

disconnectとconnectだとwifiのパスワードをプログラム内に書かなければならなくなるので良くない。resetならそのまま再接続してくれる。


カメラの解像度

githubの方のcamera-vc0706のドキュメントを見ると、解像度を下げる方法が書いてある。
camera.setResolution("解像度", callback);
で指定できる。vga, qvga, qqvgaが指定できる。
初期値のvgaのままだとMPUとwifiモジュール間の通信速度がせいぜい数十kbpsしかないはずなので、画像が全然落ちてこなくなる。


cameraとwifiを同時にセットアップしない

var camera = require('camera-vc0706').use(tessel.port['A']);
を実行すると、カメラの初期化が始まって、camera.on(‘ready’, callback)などのイベントが呼ばれるようになる。nodeだと並列に色々やりたいから同時にwifiのリセット〜httpサーバの起動などもやりたくなるけど、1つずつやらないと動かない。

カメラセットアップ→wifi再起動→httpサーバー起動
の順にやる事にした。


末尾ループがわりのsetInterval

httpサーバーも起動せず、何もsetInterval等も動いていない状態だとNodeはふつうにプログラムが末尾で終了する。
終了してしまうので、wifiをresetしてもon “connect”も取れなくなる。
とりあえずsetIntervalで常に基板上のLEDを点滅させておくようにした。動作してるか確認にも使えて便利。


プログラム

色々試行錯誤した結果こうなった。eventemitterのおかげでなんとかなっている感じがする。

tessel-study/camera-server.js at master · shokai/tessel-study

// カメラで撮影してWiFi+HTTPサーバーで配信する
// 15秒間隔で撮影する

var tessel = require('tessel');
var wifi = require('wifi-cc3000');
var router = require('tiny-router');
var camera = require('camera-vc0706').use(tessel.port['A']);
var image = null;

var led_green = tessel.led[0].output(1);
setInterval(function(){
if(wifi.isConnected()) led_green.toggle()
}, 500);

// 解像度設定
camera.on('ready', function() {
console.log('camera ready');
camera.setResolution('qqvga', function(err, res){
if(err) throw 'setting camera resolution failed!';
camera.emit('ready:capture');
});
});

// 撮影準備完了
camera.on('ready:capture', function(){
console.log('camera ready:capture');
camera.startCapture(function(err, res){
if(err) return console.error(err);
image = res;
console.log('capture done');
}, 15000); // 15 sec interval
});

// 定期的に撮影する
camera.startCapture = function(callback, interval){
if(typeof interval !== 'number' || interval < 1){
throw 'interval must be number (msec)';
}
camera.takePicture(function(err, res){
if(typeof callback === 'function') callback(err, res);
setTimeout(function(){
camera.startCapture(callback, interval);
}, interval);
});
};

// カメラの準備ができてからwifiを再起動する
camera.on('ready:capture', function(){
wifi.reset();
});

// wifiが接続できたら、httpサーバーを起動する
wifi.on('connect', function(){
console.log('wifi connect');
var port = (process.env.PORT || 80) - 0;
router.listen(port);
console.log('start HTTP server at PORT: '+port);
});

router.get('/', function(req, res){
console.log(req.method + ': ' + req.url);
res.send('camera-server.js');
});

router.get('/camera.jpg', function(req, res){
console.log(req.method + ': ' + req.url);
if(!image){
res.writeHead(500);
res.end('no capture image');
return;
}
res.sendImage(image);
});