Top/Devel/電子工作/Arduino/PaxPowerGlove

PaxPowerGloveはてなブックマーク

This page is also available in English. Please visit here.


powerglove04.jpg

Oculus Riftの仮想空間内の物体に干渉したい。



ということで、ファミコンの特殊コントローラであるところのパックス社製パワーグローブにモーションセンサを埋め込み、Arduino→PCと接続して、Unityの仮想空間内の手のモデルの動きを制御してみようと思います。


完成動画はこちら。


パワーグローブは本来、2つの超音波振動子から時間差で超音波を発生させてモーションキャプチャを実現しています。

発想は革新的・野心的で素晴らしいのですが、精度がいまいちなので、今回はその機構は使わず、新たにモーションセンサを搭載しています。

本来の機構で残っているのは曲げセンサのみです。


board.jpg

次の順序で進めます。

  1. パワーグローブの指の曲げ具合の取得
  2. モーションセンサによる手のモーションの取得とUnity空間のオブジェクトの回転制御
  3. 1と2の統合
  4. 近接無線通信モジュールXBeeによる無線通信
  5. 3と4の統合
  6. 振動モーターによるハプティック(触覚フィードバック)の実現
  7. 5と6の統合

必要なもの

ハードウェア

項目説明
Arduino比較的小型で安価で簡単に使えるマイコンボード。
無線化してパワーグローブに内蔵する場合は、後述するArduino Fioが小型かつXBeeが直接接続でき、便利です。
パックスパワーグローブ通常はファミコンに接続して使う手袋型コントローラ。
抵抗(100kΩ)* 4何でも良いです。今回は千石電子通商で購入しました。
MPU-9150 9軸センサモジュールInvenSense社の3軸加速度+3軸ジャイロ+3軸コンパスのモーションセンサチップMPU-9150が載ったモジュール。

ソフトウェア

項目説明
Arduino IDEArduino用の開発環境。
オフィシャルサイトからダウンロードしてください。
Unityマルチプラットフォーム対応のゲームエンジン。簡単に3D(or 2D)ゲームを作ることが出来る。
オフィシャルサイトからダウンロードしてください。
I2C Device LibraryI2Cデバイス用C++ライブラリとMPU-6050(兼MPU-9150)のArduinoサンプルスケッチ。
https://github.com/jrowberg/i2cdevlib を開き、右側の「Download ZIP」をクリックしてダウンロードしてください。
Unity用C#スクリプト上記サンプルスケッチの出力をUnity側で受け取ってGameObjectを制御するC#スクリプト。本ページ下部からダウンロードしてください。

手順1.パワーグローブの指の曲げ具合の取得

まずは指の曲げ具合を取得してみます。

ハードウェアの準備

powerglove_circuit.png
  1. 手の甲の部分のカバーと基板をプラスドライバーで外す。
  2. はんだ吸い取り線などを使って、配線を基板から外す。
  3. 手の甲側の4本の赤い線をすべてGNDに接続する。
  4. Arduinoの5V→抵抗→手の甲側のセンサ線と繋ぎ、抵抗とセンサの間にArduinoのアナログ入力を接続する。
    後述のスケッチでのセンサ線とアナログ入力の関係は次の通り。
    センサ線アナログ入力端子
    親指A0
    人差し指A1
    中指A2
    薬指A3
    小指センサなし-


曲げセンサは出力が抵抗値なので、同程度の抵抗器を使うことで抵抗値→電圧値に変換してArduinoに入力する、という回路が上記です。

ソフトウェアの準備

  1. 下記ArduinoスケッチをArduinoに書き込む。

動作確認

  1. シリアルモニタを開く。
  2. 指を曲げ伸ばしして数値が変化することを確認する。

Arduinoスケッチ

filePowerGlove.ino
int fingers[4];

void setup() {
  Serial.begin(9600);
}

void loop() {
  int i;
  fingers[0] = constrain(map(analogRead(A0), 678, 760, 0, 10), 0, 10) * 10;
  fingers[1] = constrain(map(analogRead(A1), 835, 935, 0, 10), 0, 10) * 10;
  fingers[2] = constrain(map(analogRead(A2), 770, 905, 0, 10), 0, 10) * 10;
  fingers[3] = constrain(map(analogRead(A3), 750, 910, 0, 10), 0, 10) * 10;

  for (i = 0; i < 4; i++) {
    Serial.print("finger");
    Serial.print(i);
    Serial.print(":");
    Serial.print(fingers[i]);
    Serial.print("\t");
  }
  Serial.println();
  delay(200);
}

抵抗値は個体毎に違うと思うので、適宜調整してください。

手順2.モーションセンサによる手のモーションの取得とUnity空間のオブジェクトの回転制御

パワーグローブはグローブ側から超音波を発射し、テレビの周りに設置した超音波センサで検知することによって、位置を検出しています。


今回はこの機構をInvenSense社のモーションセンサチップMPU-9150で置き換えて、手全体のモーションを取得し、Unity空間のオブジェクトの回転を制御してみます。


詳細は、../モーションセンサをご覧ください。

手順3.1と2の統合

さあついに、上記1と2を統合します。

ハードウェアの準備

  1. 1と2の回路を組み合わせる。
    Arduino Unoであれば、ピンが重複しないので、単に組み合わせるだけです。

ソフトウェアの準備(1):Arduinoスケッチ

  1. ../モーションセンサで作成したArduinoスケッチを次のように修正する。
  2. 286-297行目を次のように変更する。
    (unidiff風に書いています。要するに先頭が-の行を削除して、先頭が+の行を追加するだけです。)
            #ifdef OUTPUT_READABLE_QUATERNION
                // display quaternion values in easy matrix form: w x y z
                mpu.dmpGetQuaternion(&q, fifoBuffer);
                Serial.print("quat\t");
                Serial.print(q.w);
                Serial.print("\t");
                Serial.print(q.x);
                Serial.print("\t");
                Serial.print(q.y);
                Serial.print("\t");
    -            Serial.println(q.z);
    +            Serial.print(q.z);
    +            Serial.print("\t");
    +            Serial.print(constrain(map(analogRead(A0), 678, 760, 0, 10), 0, 10) * 6);
    +            Serial.print("\t");
    +            Serial.print(constrain(map(analogRead(A1), 835, 935, 0, 10), 0, 10) * 6);
    +            Serial.print("\t");
    +            Serial.print(constrain(map(analogRead(A2), 770, 905, 0, 10), 0, 10) * 6);
    +            Serial.print("\t");
    +            Serial.println(constrain(map(analogRead(A3), 750, 910, 0, 10), 0, 10) * 6);
            #endif

ソフトウェアの準備(2):Unityプロジェクト

  1. 下記ファイルをダウンロードし、解凍して、PaxPowerGlove.unityを開く。

動作確認

  1. Unityの再生ボタンを押す。
  2. パワーグローブを傾けてみて、手のモデルが動けば成功!
    動かない場合は、Arduino本体のリセットボタンを押してみてください。


お疲れ様でした。 :)

【参考】Unity用C#スクリプト(上記PaxPowerGlove.zipに含まれています)

fileControlHandByPaxPowerGloveAndMPU9150InUnity.cs
using System;
using System.IO.Ports;
using System.Threading;
using UnityEngine;

public class ControlHandByPaxPowerGloveAndMPU9150InUnity : MonoBehaviour {
    private const string SERIAL_PORT = "COM4";
    private const int SERIAL_BAUD_RATE = 115200;
    private const int SERIAL_TIMEOUT = 100;

    private Thread _readThread;
    private static SerialPort _serialPort;
    private static bool _continue;

    private static Quaternion _handQuaternion = new Quaternion();

    private static int[] _fingerVal = new int[4];
    private GameObject[,] _fingerObject = new GameObject[5,3];

    void Start() {
        _fingerObject[0,2] = GameObject.Find ("Finger0_3Dummy");
        _fingerObject[0,1] = GameObject.Find ("Finger0_3Dummy/Finger0_2Dummy");
        _fingerObject[0,0] = GameObject.Find ("Finger0_3Dummy/Finger0_2Dummy/Finger0_1Dummy");
        _fingerObject[1,2] = GameObject.Find ("Finger1_3Dummy");
        _fingerObject[1,1] = GameObject.Find ("Finger1_3Dummy/Finger1_2Dummy");
        _fingerObject[1,0] = GameObject.Find ("Finger1_3Dummy/Finger1_2Dummy/Finger1_1Dummy");
        _fingerObject[2,2] = GameObject.Find ("Finger2_3Dummy");
        _fingerObject[2,1] = GameObject.Find ("Finger2_3Dummy/Finger2_2Dummy");
        _fingerObject[2,0] = GameObject.Find ("Finger2_3Dummy/Finger2_2Dummy/Finger2_1Dummy");
        _fingerObject[3,2] = GameObject.Find ("Finger3_3Dummy");
        _fingerObject[3,1] = GameObject.Find ("Finger3_3Dummy/Finger3_2Dummy");
        _fingerObject[3,0] = GameObject.Find ("Finger3_3Dummy/Finger3_2Dummy/Finger3_1Dummy");
        _fingerObject[4,2] = GameObject.Find ("Finger4_3Dummy");
        _fingerObject[4,1] = GameObject.Find ("Finger4_3Dummy/Finger4_2Dummy");
        _fingerObject[4,0] = GameObject.Find ("Finger4_3Dummy/Finger4_2Dummy/Finger4_1Dummy");

        _readThread = new Thread(Read);
        _serialPort = new SerialPort(SERIAL_PORT, SERIAL_BAUD_RATE);
        _serialPort.ReadTimeout = SERIAL_TIMEOUT;
        _serialPort.Open();
        _continue = true;
        _readThread.Start();
    }

    void Update() {
        transform.rotation = _handQuaternion;

        _fingerObject[0,2].transform.localRotation = Quaternion.Euler (_fingerVal [0], -40, 60);
        _fingerObject[0,1].transform.localRotation = Quaternion.Euler (_fingerVal [0], 0, 0);
        _fingerObject[0,0].transform.localRotation = Quaternion.Euler (_fingerVal [0], 0, 0);
        _fingerObject[1,2].transform.localRotation = Quaternion.Euler (_fingerVal [1], 0, 0);
        _fingerObject[1,1].transform.localRotation = Quaternion.Euler (_fingerVal [1], 0, 0);
        _fingerObject[1,0].transform.localRotation = Quaternion.Euler (_fingerVal [1], 0, 0);
        _fingerObject[2,2].transform.localRotation = Quaternion.Euler (_fingerVal [2], 0, 0);
        _fingerObject[2,1].transform.localRotation = Quaternion.Euler (_fingerVal [2], 0, 0);
        _fingerObject[2,0].transform.localRotation = Quaternion.Euler (_fingerVal [2], 0, 0);
        _fingerObject[3,2].transform.localRotation = Quaternion.Euler (_fingerVal [3], 0, 0);
        _fingerObject[3,1].transform.localRotation = Quaternion.Euler (_fingerVal [3], 0, 0);
        _fingerObject[3,0].transform.localRotation = Quaternion.Euler (_fingerVal [3], 0, 0);
        _fingerObject[4,2].transform.localRotation = Quaternion.Euler (_fingerVal [3], 0, 0);
        _fingerObject[4,1].transform.localRotation = Quaternion.Euler (_fingerVal [3], 0, 0);
        _fingerObject[4,0].transform.localRotation = Quaternion.Euler (_fingerVal [3], 0, 0);
    }

    void OnApplicationQuit() {
        _continue = false;
        _readThread.Join();
        _serialPort.Close();
    }

    private static void Read() {
        string[] values;
        float x, y, z, w;
        while (_continue) {
            if (_serialPort.IsOpen) {
                try {
                    values = _serialPort.ReadLine().Split('\t');
                    if (values[0] == "quat") {
                        x = float.Parse(values[2]);
                        y = -float.Parse(values[4]);
                        z = float.Parse(values[3]);
                        w = float.Parse(values[1]);
                        _handQuaternion.Set(x, y, z, w);
                        _fingerVal[0] = int.Parse(values[5]);
                        _fingerVal[1] = int.Parse(values[6]);
                        _fingerVal[2] = int.Parse(values[7]);
                        _fingerVal[3] = int.Parse(values[8]);
                    }
                } catch (TimeoutException) {
                }
            }
            Thread.Sleep(1);
        }
    }
}

手順4.XBeeによる無線通信

xbee.jpg

上記までは有線で通信をしていましたが、近接無線通信モジュールXBeeを使って無線通信をしてみます。

詳細は ../XBee をご覧ください。

手順5.3と4の統合

さあついに、上記3と4を統合します。

  1. 先ほどのArduinoスケッチを次のように修正する。
    (unidiff風に書いています。要するに先頭が-の行を削除して、先頭が+の行を追加するだけです。)
        #if I2CDEV_IMPLEMENTATION == I2CDEV_ARDUINO_WIRE
            Wire.begin();
    -        TWBR = 24; // 400kHz I2C clock (200kHz if CPU is 8MHz)
    +        TWBR = 12; // for Arduino Fio
    
        // initialize serial communication
        // (115200 chosen because it is required for Teapot Demo output, but it's
        // really up to you depending on your project)
    -    Serial.begin(115200);
    +    Serial.begin(57600); // for Arduino Fio
    

手順6.振動モーターによるハプティック(触覚フィードバック)の実現

ハードウェアの準備

vibrate_circuit.png

右図のような回路を5本分用意する。

ソフトウェアの準備

執筆中。

char c = Serial.read();
if (c != -1) {
  c -= '0';
  for (int i = 0; i < 5; i++) {
    if ((c & 0b00010000) != 0) {
        digitalWrite(3, HIGH);
    }
    if ((c & 0b00001000) != 0) {
        digitalWrite(4, HIGH);
    }
    if ((c & 0b00000100) != 0) {
        digitalWrite(5, HIGH);
    }
    delay(7);
    digitalWrite(3, LOW);
    digitalWrite(4, LOW);
    digitalWrite(5, LOW);
    if ((c & 0b00000010) != 0) {
        digitalWrite(6, HIGH);
    }
    if ((c & 0b00000001) != 0) {
        digitalWrite(7, HIGH);
    }
    delay(7);
    digitalWrite(6, LOW);
    digitalWrite(7, LOW);
  }
}

手順7.5と6の統合

執筆中。

備考

SerialPort.ReadLine()をUpdate()内で使うと遅延が大きく、使い物になりません。

今回はMSDNの記述にしたがって、別スレッドでReadLine()しています。

パワーグローブを入手できない場合

市販の手袋にスイッチサイエンスの曲げセンサSEN-08606を接着するとよいかもしれません。

プロトタイプからの変遷

powerglove01.jpg
powerglove02.jpg
powerglove03.jpg

参考

Amazon

差分 一覧