凹みTips

C++、JavaScript、Unity、ガジェット等の Tips について雑多に書いています。

JavaScript(Node.js)で色々なハードウェアモジュールが動くマイコンボード Tessel を手に入れたので詳しく調べてみた

はじめに

今日、昨年夏に Back していた Tessel が届きました。

f:id:hecomi:20140705181938j:plain

Tessel は、Node.js ベースの JavaScript 環境を利用してハードウェア制御可能なマイコンボードです。スタンドアロンWiFi 接続可能で、USB による電源供給のみで動作します。本体にはモジュール拡張用に 4 つのポートがついており、ここに SD カード読み込みモジュールやオーディオ入出力モジュール、加速度や温度・照度などのセンサモジュールなどの様々なモジュールを差し込むことで拡張が可能です。そしてこの一つ一つのハードウェアモジュールを操作するための Node モジュールが npm で公開されており、バグ修正も含めて Node.js のプラットフォームの上に乗っかっている形でトキメキます。Node のレゴブロックのように小さなモジュールを組み合わせてプログラムを作成する、という概念をハードウェアにまで拡張したみたいなイメージで、とても面白いです。もちろん主要な Node.js のモジュールも(C++ モジュールを除いておおよそ)動作するため、HTTP サーバを立てるのも require('http') するだけで可能です。

性能としてはプロセッサは LPC1830(Cortex-M3, 180 MHz)、32 MB の Flash および RAM を備えているため、ATmega を積んだ Arduino よりは大分高性能になっていますが、IntelGalileo と比較すると見劣りする感じです。が、上述のモジュールの話もそうですが、Node.js の EventEmitter をベースにした非同期周りの処理をコーディング出来るのは、人間による操作や、あるスレッショルドを超えたセンサからやってくるイベント通知などを非常にハンドリングしやすいという魅力があると思います。大きさは、モジュールを外した状態で Arduino UNO よりちょっと小さいくらいです。

f:id:hecomi:20140706173121j:plain

Tessel は Technical Machine という会社によるプロジェクトです。Dragon Innovation というスマートウォッチの Pebble や 3D プリンタの MakerBot を成功に導いたコンサルティングファームに支援されており、クラウドファンディングによる出資でスタートしました。

そして、2013/10/5 に目標額($50,000)の 393% にあたる $196,682 を獲得して成功に至ります。そこからおよそ9ヶ月が経過して、ついに昨日手元に届きました(Twitter を見ていると早い人は先週手にしていました)。私は $349 の「The Master Pack」に Back していたのでモジュールも色々届き、加速度 / サーボ / Micro SD / 照度・騒音 / 赤外線 / リレー / BLE / GPS / カメラ / RFID / オーディオが今手元にあります。現在の価格は本体 + モジュール1個で $99~ 、モジュールはひとつ $25~ です。

本エントリでは Tessel のチュートリアルを含めた外観と、このモジュールたちの一部の紹介、そして Tessel の仕組みについて調べてみた内容を紹介したいと思います。

環境

環境構築 〜 L チカ(本体)まで

まず、以下にアクセスして手順に従って設定していきます。

$ brew install node
$ npm install -g tessel

今回は Mac で作業しているので brew で Node.js をインストールしましたが、Windows なら公式のインストーラから、Linux なら apt-get などで入れれば、同様に npm install コマンドで tessel コマンドを導入することが出来ます(すでに導入済みなら不要)。そして、Tessel を USB でつないで以下のコマンドを実行します。

$ tessel update

なんとこれだけでファームウェアのアップデートが終わり準備完了です。最初に L チカするために以下のコードを blinky.js という名前で保存しておきます。

var tessel = require('tessel');

var led1 = tessel.led[0].output(1);
var led2 = tessel.led[1].output(0);

setInterval(function () {
    led1.toggle();
    led2.toggle();
}, 100);

コンソールから $ tessel run コマンドでこの JS を Tessel 側にインストールして実行します。

$ tessel run blinky.js

これだけで 100 msec おきにライトが交互に点滅します。 大変簡単です。

なお、$ tessel push コマンドを使うとコードを Flash に保存し、電源投入時に自動実行してくれるようになります。

$ tessel push blinky.js

モジュールの利用

Tessel には 4 つのモジュールポートが備えられており、様々な機能を搭載したモジュールをそこへ差し込むことで利用できるようになっています。

f:id:hecomi:20140705200058j:plain

モジュールは Servo や、Camera など現在14種類用意されています。

f:id:hecomi:20140705195640p:plain

各モジュールの使い方や仕様は以下のページにまとまっています。

ここでは試しに Servo と Camera を使ってみます。

Servo

サーボモジュールは 3x16 pin が配置されたモジュールです。ここに 3 pin(GND、VCC、制御信号)を備えたサーボを最大 16 個つけられる形になっています。チュートリアルに書いてある向きで配線を行えば OK です。なお、サーボは ES3001 が付属で1つついてきました。

次に Servo を制御するために Node モジュールをインストールします。

$ npm install servo-pca9685

モジュールはデバイスを便利に扱えるように抽象化したもので、 JS のレイヤのみで書かれているためビルドなどは走りません。そして次のようなコードを書きます。

var tessel   = require('tessel');
var servolib = require('servo-pca9685');

// サーボモジュールを挿したポートを指定
var servo = servolib.use(tessel.port.A);
// サーボモジュールにサーボを挿したピン位置を指定(1 ~ 16)
var pin = 1;

servo.on('ready', function () {
    // 回転位置 (0.0 ~ 1.0)
    var pos = 0;
    // ピン位置、PWMの設定(min/max 時の duty 比)、コールバック
    servo.configure(pin, 0.05, 0.12, function() {
        // 500 msec 置きに回転させる
        setInterval(function() {
            pos = (pos > 1.0) ? 0.0 : (pos + 0.1);
            servo.move(pin, pos);
        }, 500);
    });
});

サーボの個体差も考慮出来るように作っているようで、最低限のコードを書くだけで動きます。

問題などあれば、モジュールごとにフォーラムが開かれているようなので、過去ログを見たり質問すれば良さそうです。

Camera

次にカメラモジュールです。

f:id:hecomi:20140705223021j:plain

var tessel = require('tessel');
var camera = require('camera-vc0706').use(tessel.port.A);
var led    = tessel.led[0];

camera.on('ready', function() {
    led.high();
    camera.takePicture(function(err, image) {
        if (err) throw err;
        led.low();
        process.sendfile('image.jpg', image);
        camera.disable();
    });
});

これで動作時に LED が点灯して LED が消えるとファイルの保存が完了です。process.sendfile() でローカル(PC側)にファイルを転送しています。転送先は tessel run コマンドに --upload-dir オプションを付けて指定します。

$ tessel run camera.js --upload-dir .`

これで撮影した画像が以下になります。

f:id:hecomi:20140705223525j:plain

動画のストリーミングなどは出来ないようです。カメラのピントの調節はフォーラムに書いてありましたが、カメラ上部を回せば良いようです。

長くなるので、モジュール紹介はこの辺りに留めておこうと思います。その他にも、SD Card、Ambient、IR、Relay、Audio、Accel、RFIDGPS、BLE のモジュールを注文していますので、これらについては別エントリにて紹介したいと思います。

WiFi の接続

Tessel は tessel wifi コマンドで設定することで、単体で 2.4 GHz 帯の WiFi に接続することが出来ます。つまり、USB 電源供給だけでスタンドアロンで HTTP アクセス可能な Node.js 実行環境が簡単に出来るわけです。

$ tessel wifi -n [network name] -p [password] -s [security type*]

$ tessel wifi -l で利用可能な WiFi リストを確認できるようです(何故か、どれかに接続するまでは一覧が見えず There are no visible networks yet. となってしまいましたが...)。

$ tessel wifi -n HOGEHOGE -p ******** -s wpa2 
TESSEL! Connected to TM-00-04-f0009a30-006e4f43-3c9865c2.
INFO Connecting to "HOGEHOGE" with wpa2 security...
INFO Acquiring IP address. 
................... timeout.
INFO Retrying...
INFO Acquiring IP address. 
.
INFO Connected!

IP   192.168.0.12
DNS  192.168.0.1
DHCP     192.168.0.1
Gateway  192.168.0.1

接続が完了すると、オレンジ色の Conn という名前のついた(小さく書いてある)ライトが光るようになります。

f:id:hecomi:20140705225524j:plain

例えば、http.get() を使ったコードを書いてみます。

var http = require('http');

var weatherApi = 'http://api.openweathermap.org/data/2.5/weather?q=Tokyo,jp';

var req = http.get(weatherApi, function(res) {
    var body = '';
    res.on('data', function(chunk) {
        body += chunk.toString();
    });
    res.on('end', function() {
        var json = JSON.parse(body);
        console.log(json.weather[0].main);
    });
});

req.on('error', function(err) {
    console.log(err);
});

これは node コマンドでも動くコードですが、$ tessel run してみると、同様に動きます。

$ node http.js
Clouds

$ tessel run http.js
TESSEL! Connected to TM-00-04-f0009a30-006e4f43-3c9865c2.
INFO Bundling directory /Users/hecomi/Dropbox/Program/js/tessel (~237.08 KB)
INFO Deploying bundle (325.00 KB)...
INFO Running script...
Clouds

天気に応じて LED の色を変えるとかも簡単にできるわけですね。

HTTP サーバを建てることも出来ます。リクエストを送ると何か動くみたいなことも簡単です。次のコードはアクセスすると Ambient モジュールで取得した周囲の明るさと音量を JSON で返すものです。

var tessel  = require('tessel');
var ambient = require('ambient-attx4').use(tessel.port.A);
var http    = require('http');
var ip      = require('os').networkInterfaces().en1[0].address;
var port    = 80;

ambient.on('ready', function() {
    http.createServer(function(req, res) {
        ambient.getLightLevel(function(err, light) {
            ambient.getSoundLevel(function(err, sound) {
                res.writeHead(200);
                res.end( JSON.stringify({
                    light : light.toFixed(4),
                    sound : sound.toFixed(4)
                }) );
            });
        });
    }).listen(port);

    console.log(ip, port);
});

$ tessel push しておけば、ハードウェアと連携するスタンドアロンな HTTP サーバの出来上がりです。

現在は、Tessel への WiFi を経由したコード転送は出来ませんが、ロードマップ上には乗っているようで、近いうちに可能になる見込みのようです。

GPIO などのハードウェアの利用

以下にハードウェア周りの API がまとまっています。

npm モジュールで動作する外付けのモジュールと異なり、組み込みの tessel モジュールをベースに制御を行います。

本体 LED の操作

本体には操作可能な 4 つの LED が搭載されています。

はじめに L チカした blinky.js では緑の tessel.led[0] と青の tessel.led[0] を操作していましたが、この他にエラーを表現する赤の tessel.led[2]WiFi の項で見た黄の tessel.led[3] が制御可能です。

ボタンの利用

本体には 2 つのボタンが搭載されています。1 つはリセットを行うボタンなので、プログラムからはコンフィグ用のボタンが操作可能です。

var tessel = require('tessel');

tessel.button.on('press', function(time) {
    console.log('the button was pressed!', time);
});

tessel.button.on('release', function(time) {
    console.log('button was released', time);
});

ちょっとしたことを行う際に手動のトリガとして使うのに便利です。

GPIO について

これまで、tessel.port.A で A ポートに接続したモジュールを操作してきましたが、ポートは外付けの ABCD に加えて GPIO が使用可能です。

GPIO の口は 20 pin あって、そのうち汎用的に使えるアナログは 6 pin(A1 〜 A6)、デジタルも 6 pin(G1 〜 G6)あります。GPIO を使った L チカはこんなコードで書けます。

var tessel = require('tessel');
var gpio   = tessel.port.GPIO;
var pin    = gpio.digital[0];

(function led(on) {
    pin.write(on);
    setTimeout(led, 100, !on);
})(true);

ちなみに、モジュール用のポートも使用可能です。

var tessel = require('tessel');
var gpio   = tessel.port.A; // GPIO --> A
var pin    = gpio.digital[0];

(function led(on) {
    pin.write(on);
    setTimeout(led, 100, !on);
})(true);

アナログピンを使ってセンサの値を取得するのも簡単です。以下は例として圧電センサの値を取得してみたコードです(抵抗値変えて電圧見てるだけです)。

var tessel = require('tessel');
var gpio   = tessel.port.GPIO;
var pin    = gpio.analog[0];

setInterval(function() {
    console.log(pin.read());
}, 100);

また、割り込み機構も用意されていて、Tessel ではこれを EventEmitter を使って実現しています。

var tessel = require('tessel');
var pin    = tessel.port.GPIO.digital[0];

pin.on('rise', function() {
    console.log('high');
});

pin.on('change', function() {
    console.log('change', pin.read());
});

pin.on('fall', function() {
    console.log('low');
});

割り込みについては以下に詳しく書かれています。

他にも色々と出来るので詳しくはドキュメントをご参照下さい。

通信

組み込みのモジュールで SPI、I2C、UART が提供されています。

Arduino との接続

Tessel と Arudino を連携させることも出来ます。データは UART でやりとりします。Arduino では UART のソフトウェアシリアルのライブラリを利用し、Tessel では組み込みの UART ライブラリ(JS)を利用するようです。

時間があったらやってみます。

ハードウェアについて

ハードウェアについては以下に記載されています。モジュールの位置やピン配置が記載されています。

冒頭でも述べましたが、CPU は 180 MHz の LPC1830(Cortex-M3)、Flash / RAM は 32 MB、WiFi モジュールは TI の CC3300 で、技適承認済みのようです(本体に技適マークがあります)。

ソフトウェア側の仕組みについて

個人的に一番興味があるのがソフトウェアのアーキテクチャです。一体どうやって Tessel で Node.js を動かしているのか見ていきます。

Tessel では JavaScriptLuaコンパイルして本体に転送しているようです。このコンパイラColony というもので、Tessel 専用の Lua とともに github に公開されています。

この Colony は timcameronryan さんが作られたもので、2011 年辺りから公開されています。

そして、timcameronryan さんは Technical Machine の co-founder で Tessel の作者でもあります。

tessel コマンドは Node.js のスクリプトになっていて、tessel run をすると、tessel-run.js スクリプトが走るようになっています。この中で Colony を走らせて文字列化し、tessel へ転送を行っています。

var data = convertToContext(cmd.slice(1, -2));
var script
    = 'local function _run ()\n' + colonyCompiler.colonize(data, {returnLastStatement: true, wrap: false}) + '\nend\nsetfenv(_run, colony.global);\nreturn _run()';
client.send(script);

さて、すると Lua に変換しているということは Node.js はどうなってるの?ということが気になります。色々調べてみてまだ不明確なのですが、どうやら Node.js の各 API を自前で再実装して Luaバインディングしているように見えます。以下が Tessel の firmeware と runtime のリポジトリです。

例えば、joyent の http-parser ですが、Node.js では以下のように node_http_parser.cc で V8 とバインディングを行い、http.js でユーザが使用するモジュールに仕立てていると思います。

これに対し、Tessel の http.js を見てみます。

COPYRIGHT を見ても分かる通り、どうやら書き直しをしているようです。process.bindinghttp-parser を読み込んでいますが、じゃぁこのバインディングはどこでやってるのか...、と探してみると以下のファイルのようです。

Lua とのバインディングがゴリゴリ書いてあるように見えます。なんという力技...。つまり、Tessel 内では Node.js は動いておらず、Node.js の様な何かが動いているものと思われます。

イベントループ部も見てみます。Node.js では libuv の uv_run が以下の場所で動いています。

これに対し、Tessel はエントリポイントから順に追っていくと以下に行き着きます(あってるか不明)。

完全に独自で回しているようです。やはり Node.js のような何かが動いている説が濃厚なようです。つまり、Tessel は JavaScript のランタイムは持たず、JavaScriptLua に変換して、その Lua スクリプトを予め Node.js の API を再実装しておいた Lua スクリプトと共に動かす、みたいなことをやっているのかなと思います。

Compatibility のところでも書いてありましたが、いくつかの API が not implemented yet なステータスになっているのも納得できます。

すごいことをしている...。

ネイティブ拡張

C 言語と JS のバインディングについて以下で説明されています。

ただ、ソフトウェア側の仕組みで見たようにNode の C++ モジュールのように V8 ベースにモジュールを書けるわけではなく、firmware リポジトリで作成している組み込みの tessel モジュールの Lua-C バインディングをいじってリビルドし、ファームウェアを書き換える、という形になっているようです。よっぽどのことがないとやる気は起きません。

その他

インタラクティブシェル

$ tessel repl コマンドでインタラクティブシェルを立ち上げることが出来ます。

$ tessel repl
TESSEL! Connected to TM-00-04-f0009a30-006e4f43-3c9865c2.
INFO Bundling directory /Users/hecomi/.nodebrew/node/v0.10.29/lib/node_modules/tessel/scripts/repl (~953 bytes)
INFO Deploying bundle (6.00 KB)...
INFO Running script...
> process.versions
{ tessel_board: 4,
  colony: '0.10.0',
  node: '0.10.0' }
> 
(^C again to quit)
> 
$

Node.js のバージョンは 0.10.0 と出ていますが、先述のように中で動いているのは Node.js のような何かだと思われるので、単に Colony と同じバージョンを出力しているのかなと思います。

このように tessel コマンドでは色んな便利機能が定義されています。他にどんなことが出来るかは tessel コマンドに聞くかドキュメントを見ると分かります。

$ tessel --help
Tessel CLI
Usage:
   tessel list
   tessel logs
   tessel run <filename> [args...]
          run a script temporarily without writing it to flash
          -s push the specified file only (rather than associated files and modules)
   tessel push <filename> [options]
          see 'tessel push --help' for options list
   tessel erase [--force]
          erases saved usercode (JavaScript) on Tessel
   tessel repl
          interactive JavaScript shell
   tessel wifi -n <ssid> -p <pass> -s <security (wep/wpa/wpa2, wpa2 by default)>
   tessel wifi -n <ssid>
          connects to a wifi network without a password
   tessel wifi -l
          see current wifi status
   tessel stop
   tessel check <file>
          dumps the tessel binary code
   tessel blink
          uploads test blinky script
   tessel update [--list]
          updates tessel to the newest released firmware. Optionally can list all builds/revert to older builds.
   tessel debug [script]
          runs through debug script and uploads logs
   tessel version [--board]
          show version of tessel cli. If --board is specified, shows version of the connected Tessel

LIBUSB_TRANSFER_TIMED_OUT

よく LIBUSB_TRANSFER_TIMED_OUT というエラーに出くわします。$ tessel erase --force でリセットしようとしても Failed to load bootloader: found state app とエラーが出てしまい手詰まりになります。この解決方法についてはフォーラムに書かれていました。

本体に Reset と Config ボタンが有るのですが、「Reset 押す -> Config 押す -> Reset 離す -> Config 離す」をしてハードリセットしてから、 $ tessel erase --force をすれば良いようです。

Ambient モジュールでのエラー

使おうとすると、Error retrieving firmware version というエラーが出ました。これは現在修正中のようです。

対症療法としては、以下のコードを実行することです。

var tessel = require('tessel');
var ambientLib = require('ambient-attx4');

ambientLib.updateFirmware('node_modules/ambient-attx4/firmware/src/ambient-attx4.hex');

おわりに

JavaScript によるハードウェア制御自体はそれほど珍しくありません。例えば node-serialport を使えば色々出来ますし、私も以前 ZigBee 制御に使ってみたりしましたし、スタンドアロンという意味では Rasberry Pi x Node.js で GPIO 制御をされた方もいました(モジュールもあります)。

しかしながら、ここまで色々なハードウェアがモジュール化され、しかも Node.js 自体がマイコンに組み込まれた環境というのはありませんでした。冒頭でも述べましたが、更にハードウェア自体がパッケージマネージャで管理される、というのもとても新しくナウい感じがしますし、非同期イベントのハンドリングがとても楽なのも魅力的です。

ただ Arduino と比較すると、向こうはノウハウも多くシールドも豊富なので見劣りするところもあります。今後、どれだけ多くの人が参加して盛り上げていくかがとても重要になりそうですが、ソフトウェアエンジニア的には魅力的に感じるところも多いので、興味を持たれた方はちょっとお高めですが是非注文して色々遊んでみてください。