凹みTips

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

SerialPort または Uniduino を使った Unity と Arduino を連携させる方法調べてみた

はじめに

UnityArduino を連携させて色々なセンサアクチュエータを扱うことができるようになると、センサの値のビジュアライズだけに留まらず、3D モデルやゲームと連動した動きが色々と簡単に実現できるようになったり、逆にゲームの中に連動したことを実際の世界で簡単に表現したり出来ます。そこで、Unity と Arduino をつなげる方法として、無料で出来る System.IO.Ports.SerialPort を使った方法と、有料ですが簡単に Arduino と接続できるアセットの Uniduino、2つについて調べてみましたのでご紹介します。

環境

SerialPort から自前で読み取る(無料)

Player Settings から API Compatibility Level を .NET 2.0 Subset から .NET 2.0 へと変更すると、System.IO.Ports.SerialPort クラスが扱えるようになります。

f:id:hecomi:20140728021904p:plain

SerialPort クラスには様々なメンバやイベントが用意されており、例えば DataReceived イベントを利用するとシリアル通信でやってきたメッセージを受け取ることが出来ます。

SerialPort port = new SerialPort("COM1", 9600, Parity.None, 8, StopBits.One);
port.DataReceived += new SerialDataReceivedEventHandler(OnDataReceived);
port.Open();

しかしながら、Unity では DataReceived イベントをはじめとする幾つかのメンバ関数が動作せず、利用することが出来ません。

ただし SerialPort.Read()Update() 内で呼んでメッセージを読み取ることは出来るようです。ただ、これは重い処理なので、Update() やコルーチンで使うと Unity のフレームレートが低下してしまう点や、そもそも 60 fps でしかシリアルからのメッセージを読み取れない(センサはもっと高速)といった問題点が生じます。そこで、スレッドを切って、そちらで値を読み取ってやることが必要になります。

前置きが長くなりましたが、コードを見て行きましょう。ここでは加速度センサの KXR94-2050 で姿勢情報を取得(読み取り)と L チカ(書き込み)をするコードになっています。回路の詳細は下記サイト様を参考にしたのでそちらをご参照ください。

デモ

Test.ino

Arduino 側に焼くスケッチです。加速度センサの値から角度を計算、また LED の ON/OFF を切り替えられるようになっています。

namespace {
  const int AVERAGE_NUM = 10;
  const int BASE_X      = 530;
  const int BASE_Y      = 519;
  const int BASE_Z      = 545;
}
  
void setup()
{
  Serial.begin(9600);
  pinMode(13, OUTPUT);
}

void readAccelerometer()
{
  int x = 0, y = 0, z = 0;
  for (int i = 0; i < AVERAGE_NUM; ++i) {
    x += analogRead(0);
    y += analogRead(1);
    z += analogRead(2);
  }
  x /= AVERAGE_NUM;
  y /= AVERAGE_NUM;
  z /= AVERAGE_NUM;
  
  const int angleX = atan2(x - BASE_X, z - BASE_Z) / PI * 180;
  const int angleY = atan2(y - BASE_Y, z - BASE_Z) / PI * 180; 

  Serial.print(angleX);
  Serial.print("\t");
  Serial.print(angleY);
  Serial.println("");
}

void setLed()
{
  if ( Serial.available() ) {
    char mode = Serial.read();
    switch (mode) {
      case '0' : digitalWrite(13, LOW);  break;
      case '1' : digitalWrite(13, HIGH); break;
    }
  }
}

void loop()
{
  readAccelerometer();
  setLed();
}

SerialHandler.cs

Unity 側でシリアル通信でやってきたメッセージを読み取ったり、書き込んだりする部分です。デリゲートでシリアル通信の受信部を別の場所で処理できるようになっています。

using UnityEngine;
using System.Collections;
using System.IO.Ports;
using System.Threading;

public class SerialHandler : MonoBehaviour
{
    public delegate void SerialDataReceivedEventHandler(string message);
    public event SerialDataReceivedEventHandler OnDataReceived;

    public string portName = "/dev/tty.usbmodem1421";
    public int baudRate    = 9600;

    private SerialPort serialPort_;
    private Thread thread_;
    private bool isRunning_ = false;

    private string message_;
    private bool isNewMessageReceived_ = false;

    void Awake()
    {
        Open();
    }

    void Update()
    {
        if (isNewMessageReceived_) {
            OnDataReceived(message_);
        }
    }

    void OnDestroy()
    {
        Close();
    }

    private void Open()
    {
        serialPort_ = new SerialPort(portName, baudRate, Parity.None, 8, StopBits.One);
        serialPort_.Open();

        isRunning_ = true;

        thread_ = new Thread(Read);
        thread_.Start();
    }

    private void Close()
    {
        isRunning_ = false;

        if (thread_ != null && thread_.IsAlive) {
            thread_.Join();
        }

        if (serialPort_ != null && serialPort_.IsOpen) {
            serialPort_.Close();
            serialPort_.Dispose();
        }
    }

    private void Read()
    {
        while (isRunning_ && serialPort_ != null && serialPort_.IsOpen) {
            try {
                if (serialPort_.BytesToRead > 0) {
                    message_ = serialPort_.ReadLine();
                    isNewMessageReceived_ = true;
                }
            } catch (System.Exception e) {
                Debug.LogWarning(e.Message);
            }
        }
    }

    public void Write(string message)
    {
        try {
            serialPort_.Write(message);
        } catch (System.Exception e) {
            Debug.LogWarning(e.Message);
        }
    }
}

RotateByAccelerometer.cs

シリアル通信で読み込んだ加速センサの値を利用してオブジェクトを回転するコードです。

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class RotateByAccelerometer : MonoBehaviour
{
    public SerialHandler serialHandler;

    private List<Vector3> angleCache = new List<Vector3>();
    public int angleCacheNum = 10;
    public Vector3 angle {
        private set {
            angleCache.Add(value);
            if (angleCache.Count > angleCacheNum) {
                angleCache.RemoveAt(0);
            }
        }
        get {
            if (angleCache.Count > 0) {
                var sum = Vector3.zero;
                angleCache.ForEach(angle => { sum += angle; });
                return sum / angleCache.Count;
            } else {
                return Vector3.zero;
            }
        }
    }

    void Start()
    {
        serialHandler.OnDataReceived += OnDataReceived;
    }

    void Update()
    {
        transform.rotation = Quaternion.Euler(angle);
    }

    void OnDataReceived(string message)
    {
        var data = message.Split(
                new string[]{"\t"}, System.StringSplitOptions.None);
        if (data.Length < 2) return;

        try {
            var angleX = float.Parse(data[0]);
            var angleY = float.Parse(data[1]);
            angle = new Vector3(angleX, 0, angleY);
        } catch (System.Exception e) {
            Debug.LogWarning(e.Message);
        }
    }
}

LedController.cs

Unity から Arduino 側にメッセージを送って LED の ON/OFF を制御するコードです。

using UnityEngine;
using System.Collections;

public class LedController : MonoBehaviour
{
    public SerialHandler serialHandler;

    void Update()
    {
        if ( Input.GetKeyDown(KeyCode.A) ) {
            serialHandler.Write("0");
        }
        if ( Input.GetKeyDown(KeyCode.S) ) {
            serialHandler.Write("1");
        }
    }
}

上記コードをひな形として色々と改造すればだいたいのことが出来ると思います。

Uniduino を利用(有料)

Uniduino は Unity から ArduinoArduino 言語ライクに扱うことの出来る有料のアセット($30)です。

既に先人の方々がブログで解説されていますので、概念だけ簡単に紹介します。

Arduino へは Firmata(ふぁるまーた)という PC と Arduino 間で色々とやり取りできるプロトコルを実装したファームウェアを焼きます。これは Arduino のスケッチに予め用意されています。

f:id:hecomi:20140727193703p:plain

Firmata を通じた Arduino とのやり取りに関しては、Processing(Java)や oF(C++)をはじめ、Ruby や Node.js まで様々な言語の実装があります。

これを利用して、ピンの設定なども含めて PC 側の実装のみで簡単にアクチュエータの制御やセンサの値の取得が可能です。具体的にコードを見てみます。

BlinkyLight.cs

using UnityEngine;
using System.Collections;
using Uniduino;

public class BlinkyLight : MonoBehaviour
{
    private Arduino arduino_;

    void Start()
    {
        // Arduino クラスのインスタンスを取得
        arduino_ = Arduino.global;

        // Arduino の設定
        arduino_.Setup(ConfigurePins);

        // コルーチンで定期的な処理を行う
        StartCoroutine(BlinkLoop());
    }

    void ConfigurePins()
    {
        // 13 番 pin を OUTPUT に設定
        arduino_.pinMode(13, PinMode.OUTPUT);
    }

    IEnumerator BlinkLoop()
    {
        // 1 秒ごとに HIGH / LOW を切り変える
        while (true) {
            arduino_.digitalWrite(13, Arduino.HIGH);
            yield return new WaitForSeconds(1);
            arduino_.digitalWrite(13, Arduino.LOW);
            yield return new WaitForSeconds(1);
        }
    }
}

デモ

Arduino クラスを通じて色々な API を叩くことで、Arduino のスケッチで書くようなコード(setuploop)を Unity 側で書けるような形になっています(簡単)。「Uniduino > Prefabs > Uniduino」Prefab をシーンに配置して実行すれば、シリアルポートや Baud Rate などは自動的に設定されて、動画のように Arduino と連携できます。

センサの値の読み込みは以下のようになります。Uniduino を使わないで書いたコードと同じで、加速度センサから得た姿勢情報を利用してオブジェクトを回転するコードになっています。

Accelerometer.cs

using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using Uniduino;

public class Accelerometer : MonoBehaviour
{
    private Arduino arduino_;
    public Transform target;
    public Vector3 baseValues = Vector3.zero;
    public int rotationCacheNum_ = 10;

    private List<Vector3> rotationCache_ = new List<Vector3>();

    public Vector3 rotation {
        get {
            var sum = Vector3.zero;
            rotationCache_.ForEach(val => { sum += val; });
            return sum / rotationCache_.Count;
        }
    }

    void Start()
    {
        arduino_ = Arduino.global;
        arduino_.Setup(ConfigurePins);
    }

    void Update()
    {
        AddRotationCache(new Vector3(
            arduino_.analogRead(0),
            arduino_.analogRead(1),
            arduino_.analogRead(2)
        ));

        if (target) {
            target.rotation = Quaternion.Euler(rotation);
        }
    }

    void AddRotationCache(Vector3 pinValues)
    {
        var diff = pinValues - baseValues;
        rotationCache_.Add(new Vector3(
            Mathf.Atan2(diff.x, diff.z) / Mathf.PI * 180,
            0,
            Mathf.Atan2(diff.y, diff.z) / Mathf.PI * 180
        ));
        if (rotationCache_.Count > rotationCacheNum_) {
            rotationCache_.RemoveAt(0);
        }
    }

    void ConfigurePins()
    {
        SetAnalog(0);
        SetAnalog(1);
        SetAnalog(2);
    }

    void SetAnalog(int pin)
    {
        arduino_.pinMode(pin, PinMode.ANALOG);
        arduino_.reportAnalog(pin, 1);
    }
}

デモ

また、詳細は割愛しますが、Playmaker 用のカスタムアクションも用意してあるので、コード書かずにビジュアルプログラミングだけで色々なことも出来そうです。

おわりに

予算があり且つセンサの値の取得や単純な書き込み程度であれば断然 Uniduino を使ったほうが楽だと思います。予算がない場合や、ちょっとなにかやりたい時、Arduino 側のライブラリを利用したい時は自前でやった方が良いと思います。これでハードウェアと連動したプロトタイプやイベント用デモも簡単に作れそうです。

ユニティちゃんが声に合わせて口パクしてくれるリップシンクアセットを作ってみた

はじめに

以前、UnityMMD4Mecanim を利用した MMD モデルさん達がリップシンクしてしゃべってくれるアセットの作成を行いました。

これを利用して、ユニティちゃんリップシンクをしてもらおうと何度か挑戦しようとしたのですが、モーフのさせ方が分からず諦めていた所、先日、MMD4Mecanim 作者である Nora さん(@Setereoarts)が、MMD4Mecanim にユニティちゃん用のモーフアセットを追加してくださいました。

これを利用してユニティちゃんがボイスデータに合わせて口パクしてくれるように更新してみましたので、その紹介をします。

デモ

利用方法

まず、ユニティちゃんアセット、MMD4Mecanim、MMD4Mecanim-LipSync-Plugin をそれぞれダウンロードし import します。

ユニティちゃん(UnityChan/Prefabs/unitychan)をシーンに配置し、MMD4Mecanim-LipSync-Plugin/UnityChanLipSync.cs をそこへアタッチします。そして、適当なユニティちゃんボイス(e.g. UnityChan/Voice/univ0015)を Inspector の「Uniy Chan Lip Sync > Play Voice Sound > Audio Clip」にセットし、ゲームを開始して「Uniy Chan Lip Sync > Play Voice Sound > Audio Clip > Play」ボタンを押すと、口パクしながら喋ってくれるようになります。

スクリプトから利用する際は UnityChanLipSync コンポーネントを取得して Play() を呼んで下さい。動画で使用したスクリプトは以下になります。

using UnityEngine;
using System.Collections;

[RequireComponent( typeof(UnityChanLipSync) )]
public class UnityChanVoicePlayer : MonoBehaviour 
{
    private UnityChanLipSync lipSync_;
    public AudioClip[] audioClips;
    public int index = 0;

    void Start()
    {
        lipSync_ = GetComponent<UnityChanLipSync>();
    }

    void Update() 
    {
        if (index < 0 || index >= audioClips.Length) index = 0; 
    
        if (Input.anyKeyDown) {
            lipSync_.Play( audioClips[index] );
            ++index;
        }
    }
}

MMD4Mecanim-LipSync-Plugin の詳細な利用方法は以前のエントリをご覧ください。

課題

再生直前に一瞬プチフリーズが発生してしまっています。これは、音声解析をするために AudioClip.CreatePCMReaderCallback をセットしているのですが、この内部で呼び出される AudioClip.InvokePCMReaderCallback_Internal() が重い処理のようで、プロファイラを見ると一瞬 98% くらいの処理を持っていかれているからのようです。時間が出来た時に追って調査したいと思います。

おわりに

表情に加えて口が動いたりするとよりキャラクターが活き活きすると思いますので、ご活用頂けると嬉しいです。何か不具合やご不明な点がありましたら Twitter 等でご連絡下さい。

UCL

ユニティちゃんライセンス

このコンテンツは、『ユニティちゃんライセンス』で提供されています

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