Wii ヌンチャクと通信してみた(2)

Wii ヌンチャクのジョイスティックの実験として arduino から LED を制御してみました。また、Wii ヌンチャクの 3 軸加速度センサーの実験として processing アプリ上の 3 次元物体をモーションコントロールする制御を行ってみました。どんな風に動くのかをムービーにしたのでごらんください。


Wii ヌンチャクのハードウェア仕様

Wii ヌンチャクのハードウェア仕様については http://www.wiili.org/index.php/Wiimote/Extension_Controllers/Nunchuk に記載されています. 特徴としては以下です.

  • 4pin コネクタ(3.3+V, GND, CLOCK, DATA)
  • 通信方法として, メモリマップド I/O or I2C

arduino から Wii ヌンチャクを制御するには I2C を使います. Arduino IDE には I2C 規格の機器を簡単に制御できる Wire ライブラリ が付属しているのでこのライブラリを利用して Wii ヌンチャクを制御することになります. Wire ライブラリ を使用する上でのポイントは以下です.

  • Wii ヌンチャクの電源ピンを arduino の 3.3V ピンと接続する
  • Wii ヌンチャクの GND ピンを arduino の GND ピンと接続する
  • Wii ヌンチャクの DATA ピンを arduino の analog in 4 pin と接続する*1
  • Wii ヌンチャクの CLOCK ピンを arduino の analog in 5 pin と接続する*2

I2C 規格の細かいところは wikipedia を参照してください.

arduino のコード

processing で扱いやすいように多少修正していますが http://www.windmeadow.com/node/42?page=1 にあるコードをほぼそのまま流用しました. ポイントは以下です.

  • 50ms に一度, Wii ヌンチャクと通信を行い状態を取得
  • ジョイスティックの状態を元に LED を光らせる
  • Wii ヌンチャクの状態を PC へ送信(シリアル通信)
#include <Wire.h>
#include <string.h>

#undef int
#include <stdio.h>

uint8_t outbuf[6];		// array to store arduino output
int cnt = 0;
int ledPin = 13;
int topLed = 7;
int rightLed = 6;
int bottomLed = 5;
int leftLed = 4;

void showDirection() {  
  int x = outbuf[0];
  int y = outbuf[1];

  if (x < 100) {
    digitalWrite(leftLed, HIGH);
  } 
  else if (100 <= x &&  x < 180) {
    digitalWrite(leftLed, LOW);
    digitalWrite(rightLed, LOW);
  } 
  else {
    digitalWrite(rightLed, HIGH);
  }

  if (y < 100) {
    digitalWrite(bottomLed, HIGH);
  } 
  else if (100 <= y &&  y < 180) {
    digitalWrite(bottomLed, LOW);
    digitalWrite(topLed, LOW);
  } 
  else {
    digitalWrite(topLed, HIGH);
  }
}

void
setup ()
{
  pinMode(topLed, OUTPUT);
  pinMode(rightLed, OUTPUT);
  pinMode(bottomLed, OUTPUT);
  pinMode(leftLed, OUTPUT);
  beginSerial (19200);
  Serial.print ("Finished setup\n");
  Wire.begin ();		// join i2c bus with address 0x52
  nunchuck_init (); // send the initilization handshake
}

void
nunchuck_init ()
{
  Wire.beginTransmission (0x52);	// transmit to device 0x52
  Wire.send (0x40);		// sends memory address
  Wire.send (0x00);		// sends sent a zero.  
  Wire.endTransmission ();	// stop transmitting
}

void
send_zero ()
{
  Wire.beginTransmission (0x52);	// transmit to device 0x52
  Wire.send (0x00);		// sends one byte
  Wire.endTransmission ();	// stop transmitting
}

void
loop ()
{
  Wire.requestFrom (0x52, 6);	// request data from nunchuck
  while (Wire.available ()) {
    outbuf[cnt] = nunchuk_decode_byte (Wire.receive ());	// receive byte as an integer
    cnt++;
  }

  // If we recieved the 6 bytes, then go print them
  if (cnt >= 5) {
    print ();
    showDirection();
  }

  cnt = 0;
  send_zero (); // send the request for next bytes
  delay (50);
}

// Print the input data we have recieved
// accel data is 10 bits long
// so we read 8 bits, then we have to add
// on the last 2 bits.  That is why I
// multiply them by 2 * 2
void
print ()
{
  int joy_x_axis = outbuf[0];
  int joy_y_axis = outbuf[1];
  int accel_x_axis = outbuf[2]; 
  int accel_y_axis = outbuf[3];
  int accel_z_axis = outbuf[4];

  int z_button = 0;
  int c_button = 0;

  // byte outbuf[5] contains bits for z and c buttons
  // it also contains the least significant bits for the accelerometer data
  // so we have to check each bit of byte outbuf[5]
  if ((outbuf[5] >> 0) & 1) z_button = 1;
  if ((outbuf[5] >> 1) & 1) c_button = 1;

  if ((outbuf[5] >> 2) & 1) accel_x_axis += 2;
  if ((outbuf[5] >> 3) & 1) accel_x_axis += 1;

  if ((outbuf[5] >> 4) & 1) accel_y_axis += 2;
  if ((outbuf[5] >> 5) & 1) accel_y_axis += 1;

  if ((outbuf[5] >> 6) & 1) accel_z_axis += 2;
  if ((outbuf[5] >> 7) & 1) accel_z_axis += 1;

  Serial.print (joy_x_axis, DEC);
  Serial.print (",");
  Serial.print (joy_y_axis, DEC);
  Serial.print (",");
  Serial.print (accel_x_axis, DEC);
  Serial.print (",");
  Serial.print (accel_y_axis, DEC);
  Serial.print (",");
  Serial.print (accel_z_axis, DEC);
  Serial.print (",");
  Serial.print (z_button, DEC);
  Serial.print (",");
  Serial.print (c_button, DEC);
  Serial.print("\r\n");
}

// Encode data to format that most wiimote drivers except
// only needed if you use one of the regular wiimote drivers
char
nunchuk_decode_byte (char x)
{
  x = (x ^ 0x17) + 0x17;
  return x;
}

processing のコード

processing 1.0.1 に付属している Perspective というサンプルアプリを流用して以下の変更を加えています.

  • arduino から送信されたデータを受信するようにシリアルイベントをハンドリング
  • 流用元のサンプルアプリではマウスの X/Y 位置を元に画面内の物体を動かしていたコードを Wii ヌンチャクの 3 軸加速度センサーの X/Y 値を元に画面内の物体を動かすように修正
import processing.serial.*;
int lf = 10;
Serial port;

float x = 0.0;
float y = 0.0;

void setup() {
  size(640, 360, P3D);
  println(Serial.list());
  port = new Serial(this, Serial.list()[1], 19200);
  port.bufferUntil(lf);
  background(0);
  noStroke();
}

void draw() {
  lights();
  background(0);
  
  float cameraY = height/2.0;
  float fov = x/float(width) * PI/2;
  float cameraZ = cameraY / tan(fov / 2.0);
  float aspect = float(width)/float(height);
  if (mousePressed) {
    aspect = aspect / 2.0;
  }
  perspective(fov, aspect, cameraZ/10.0, cameraZ*10.0);
  
  translate(width/2+30, height/2, 0);
  rotateX(-PI/6);
  rotateY(PI/3 + y/float(height) * PI);
  box(45);
  translate(0, 0, -50);
  box(30);
}

void serialEvent(Serial p) {
  String myString = p.readStringUntil(lf);

  if (myString != null) {
    int values[] = int(split(trim(myString), ','));
    if (values.length < 6) return;  // 立ち上がり時の不安定なデータは捨てる
    inspect(values);
    x = map(values[2], 50, 210, 0, width);
    y = map(values[3], 70, 170, 0, height);
    // format of received data
    // 0: joy x
    // 1: joy y
    // 2: accel x
    // 3: accel y
    // 4: accel z
    // 5: button z
    // 6: button c
  }
}

void inspect(int[] values) {
  for (int i = 0; i < values.length; i++) 
    print(i + ": " + values[i] + "\t");
  println();
}

まとめと課題

今回の実験のまとめです.

実際に動かしてみると, Wii ヌンチャクの 3 軸加速度センサーの値が静止状態においても微妙にふらつくのがわかりました. 今後はこのふらつきをいい感じに制御するのが課題になります.

*1:I2C 用語では DATA ピンを SDA といいます

*2:I2C 用語では CLOCK ピンを SCL といいます