🕹

WiiヌンチャクとArduinoを接続してデータを取得する

2024/10/18に公開

はじめに

本記事の概要を表す図。コントローラーとArduinoとPCを接続し、各種情報を画面に出力している

ここでは、Wii ヌンチャクコントローラーを Arduino に接続し、各種入力情報を取得する方法について考えていきます。

本記事の作成については「ヌンチャクをマウスとして機能させられないか?」という疑問を きっかけにしています。試行錯誤を進めていく内に 様々な課題が出てきたので、一度最初に振り返って整理する意味で、この記事は作成されました。

使用する機材について

  • Arduino
  • PC(Arduino IDE が使える状態)
  • Wii ヌンチャクコントローラー(シロ

Arduino は、今回 Leonardo を使用しています。Arduino IDE に関しては公式サイトを参照してください。

ヌンチャクコントローラー(※以降はコントローラーと呼称)については、コストパフォーマンスの観点から採用することにしました。コントローラーにはボタン2つ、ジョイスティック、加速度センサーがついていますが、これらが(中古だとしても)数百円で手に入ることから採用しました。

通信の受信まで

コントローラーの情報云々の前段階として、まずは Arduino とコントローラー間で通信できるようにすることを考えていきます。

Arduinoとコントローラーの接続

Arduino側の配線、SDA, SCL, 3.3V, GNDを使用
マスター側:Arduino(Leonardo)

今回は I2C 通信を使うので、SDA と SCL の 2 本でデータのやり取りを行っていきます。電源関係で2本、信号関係で2本の、計4本を繋いで完了です。

コントローラー側についてはアダプターが市販されているのでそれを使ってもいいですし、下図の用に直接ピンを差し込んでも動作します。

コントローラー側の配線。凹側を下にした場合、左上がSDA、右上が3.3V、左下がGND、右下がSCLとなる
スレーブ側:コントローラー

コードの生成

ここからは PC, Arduino, コントローラー を接続し、IDE上でスケッチを書き制御していきます。コントローラーからデータ(6 byte)を受け取るところまで進め、その内容については後の段落で考えていきます。

通信の確立

まずは Arduino に書き込むコード(スケッチ)を用意します。

準備したコード
#include <Wire.h>

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

void loop() {
}

Wire ライブラリを使うことを宣言し、シリアル通信のデータ転送レートを 9600bps に指定しました。

ハンドシェイク通信

通信開始する際に、ハンドシェイクを行う必要があります。ここで言う「ハンドシェイク(handshake)」は、デバイス間の通信準備(データを送る前段階で、応答確認や設定値の共有など)を指します。

この事前準備が必要なのは、同じアドレス上に異なる機器が繋がりうるためです。
コントローラーは生産時期により「シロ」「クロ」の2種類があります。この2つはそれぞれ、内部データの扱いが違いながらも、同じアドレス 0x52 を使用します。そのため、通信を開始する際に適切な信号を送ることで、通信を初期化する作業が必要となるのです。

コントローラーがクロの場合は、1 番目のレジスタ用に 0xF0, 0x55、2 番目のレジスタ用に 0xFB, 0x00 をコントローラーに送信します。一方、コントローラーがシロの場合は、0x40, 0x00 を送信して通信を初期化します。「シロに対してクロ用のハンドシェイク信号を送る」といった誤った処理をした場合、適正なデータを受け取れなくなるため注意が必要です。

ハンドシェイクが適正に行われると、通信が行える状態になります。通信周波数は 100KHz です。

Arduino側でハンドシェイクを行う

Arduino でハンドシェイクを行うために、Wire を使って コントローラーのアドレス 0x52 に対し通信開始します。今回使うコントローラーは「シロ」なので、ハンドシェイク信号として 0x40, 0x00 を送信し、コントローラーを初期化します。

ハンドシェイク
#include <Wire.h>

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

  Wire.begin();
  Wire.beginTransmission(0x52);
  Wire.write((uint8_t)0x40);
  Wire.write((uint8_t)0x00);
  Wire.endTransmission();
}

void loop() {
}

データ要求

適正に通信が行えるようになったので、次はコントローラーに 入力情報を要求してみます。Wire ライブラリには専用の関数 requestFrom(アドレス, バイト数) があるので、これを使います。

void loop() 側に書き、定期的にデータ要求するようにします。コントローラーのアドレスは 0x52、データサイズは 6 バイト です。

コントローラーにデータ要求
#include <Wire.h>

void setup() {
  // 省略
}

void loop() {
  Wire.requestFrom(0x52, 6);
}

データ受け取り

Wire.requestFrom() で、コントローラーにデータ要求を送りました。要求を受け取ったコントローラーは、そのタイミングの入力情報を 6 バイトのデータとして Arduino へ送信してきます。そのため バイトデータを格納するための配列を用意し、データを受け取っていきます。

暗号化されたデータの複合

送られてくるデータは暗号化されており、そのままの状態では使えません。そのため、デコード用の関数 DECODE() を用意して、受け取ったデータを復号できるようにしておきます。

DECODE関数の用意
#include <Wire.h>

#define DECODE(X) ((X ^ 0x17) + 0x17)

void setup() {
  // 省略
}

void loop() {
  Wire.requestFrom(0x52, 6);
}
データ格納するための諸々を用意

次に、データをバイト単位で格納するための配列 signalNunchuck を用意します。格納するバイト数は 6 なので、要素数は 6 にしておきます。

データ格納用の配列を準備
#include <Wire.h>

#define DECODE(X) ((X ^ 0x17) + 0x17)

uint8_t signalNunchuck[6];

void setup() {
  // 省略
}

void loop() {
  Wire.requestFrom(0x52, 6);
}

この要素数 6 の配列に対し、順繰りに受け取ったバイトデータを格納していく想定です。そのため 配列インデックスと、受け取ったバイト数の管理のための変数 byteCount を用意します。基本的にこの変数は 0 〜 5 間でしか変化しないので、適当に int 型で設定しておきます。

バイト管理のための変数準備
#include <Wire.h>

#define DECODE(X) ((X ^ 0x17) + 0x17)

uint8_t signalNunchuck[6];
int byteCount;

void setup() {
  // 省略
}

void loop() {
  Wire.requestFrom(0x52, 6);
}
データ格納

これらを組み合わせて、配列に格納していきます。

Wire ライブラリで使うのは、available()read() です。
Wire.available() はデータが送られてきているかを判定できるので、while 文で「送られてきている間はデータを格納する」といったことができます。
Wire.read() は 1 バイト データを取得します。データは暗号化されているので、DECODE(Wire.read()) といった形で復号化する必要があります。

データが送られてくる間は格納処理を続ける
#include <Wire.h>

#define DECODE(X) ((X ^ 0x17) + 0x17)

uint8_t signalNunchuck[6];
int byteCount;

void setup() {
  // 省略
}

void loop() {
  byteCount = 0;
  Wire.requestFrom(0x52, 6);
  while(Wire.available()){
    signalNunchuck[byteCount] = DECODE(Wire.read());
    byteCount++;
  }
}

byteCount は 0 から while 中はインクリメントしていくので、順に signalNunchuck に格納することができています。

通信の終了

データは受け取れました。適正にできたかは、受け取れたバイト数で判定します。byteCount は 0 開始のため、受け取り終わった段階で 5 になっていれば適正に行われたことになります。if 文を設けて その中で成功時の挙動を書いていきます。データを細かく扱っていくのも、この中になります。

受け取ったバイト数で受信判定
#include <Wire.h>

#define DECODE(X) ((X ^ 0x17) + 0x17)

uint8_t signalNunchuck[6];
int byteCount;

void setup() {
  // 省略
}

void loop() {
  byteCount = 0;
  Wire.requestFrom(0x52, 6);
  while(Wire.available()){
    signalNunchuck[byteCount] = DECODE(Wire.read());
    byteCount++;
  }

  if(byteCount >= 5){
    Serial.println("データ受信成功");
  }
}

最後にコントローラーに対し、リクエストした分の通信を終了することを通知します。これはコントローラーのアドレス 0x52 に対し、0x00 を送ることで行います。

コントローラーに終了を通知
#include <Wire.h>

#define DECODE(X) ((X ^ 0x17) + 0x17)

uint8_t signalNunchuck[6];
int byteCount;

void setup() {
  // 省略
}

void loop() {
  byteCount = 0;
  Wire.requestFrom(0x52, 6);
  while(Wire.available()){
    signalNunchuck[byteCount] = DECODE(Wire.read());
    byteCount++;
  }

  if(byteCount >= 5){
    Serial.println("データ受信成功");
  }

  Wire.beginTransmission(0x52);
  Wire.write(0x00);
  Wire.endTransmission();

  delay(50);
}

これで「コントローラーから入力情報を受信する」という一連の流れが完成しました。

受信データから各種状態値を取得

ここまででコントローラーから 6 バイト分のデータを受け取ることができました。この 6 バイトのデータの中には、「スティック」「ボタン」「加速度」といった種々の情報が詰まっています。

ここからは このデータの中を調べ、それを取り出し活用することを主眼に進めていきます。

データの内訳について

コントローラーから受け取る情報は、下記のとおりです。(略称については、本記事内での用語となります)

名称 略称 サイズ 備考
Cボタン btnC 1bit 0 が押下、1 が開放を表す
Zボタン btnZ 1bit 0 が押下、1 が開放を表す
スティック-X sX 8bit 128 を中心とし、左に倒すと減少、右に倒すと増加
スティック-Y sY 8bit 128 を中心とし、下に倒すと減少、上に倒すと増加
加速度-X aX 10bit 512 を中心とし、左に倒すと減少、右に倒すと増加
加速度-Y aY 10bit 512 を中心とし、上に立てると減少、下に倒すと増加
加速度-Z aZ 10bit デフォルト時(※)で 760、X または Y 方向に 90 度倒すと 512、反転させると 320 に

※ デフォルトの持ち方とは、スティックが重力に対し垂直方向にある状態(下図)を指します

デフォルト状態のコントローラー

データ全体は 48bit(≒ 6byte)となることから、送られてきたデータ(6byte)は余分な情報なくこれらの情報が詰まった状態だということがわかります。

しかし I2C 通信は 1byte(≒ 8bit) 単位でのやり取りです。そのため、1byte で 1 つのデータとなりうるのは、sXsY のみとなります。つまりそれ以外のデータは、複数のバイトに分割されたり、特定のバイトにまとめられたりしている、ということを意味します。

それを説明するのが下表です。これは 6byte の内訳で、略称横の数値は そのデータに対するbit位置を意味します。

Byte bit7 bit6 bit5 bit4 bit3 bit2 bit1 bit0
1 sX7 sX6 sX5 sX4 sX3 sX2 sX1 sX0
2 sY7 sY6 sY5 sY4 sY3 sY2 sY1 sY0
3 aX9 aX8 aX7 aX6 aX5 aX4 aX3 aX2
4 aY9 aY8 aY7 aY6 aY5 aY4 aY3 aY2
5 aZ9 aZ8 aZ7 aZ6 aZ5 aZ4 aZ3 aZ2
6 aZ1 aZ0 aY1 aY0 aX1 aX0 btnC btnZ

このように、最後のバイトは「(1 バイトに収まりきらなかった)各種 加速度値の下位 2bit」や「1bit 単位の 2つのボタン」情報を管理していることがわかります。そのため、スティック情報の取得は容易な一方、その他の入力情報を取得するのは少し難しくなっています。

  • ボタン情報は、最後のバイトの 特定のビット位置にアクセスすることで取得する
  • 正確な加速度の値を知るためには、最後のバイト情報から 2bit 分取得し、8bit とつなぎ合わせて 10bit にする必要がある

各種データの取り出し

データの内訳がわかったので、ここからは各種データを取得していきます。

スティック

コントローラーのスティック部は、XY座標データを持つ

まずは、スティック情報を取得します。これは受信データ(signalNunchuck[])の 1 番目(sX)と 2 番目(sY)のバイトデータがそのまま使えます。

バイトデータをそのまま使ってスティック情報を取得
#include <Wire.h>

#define DECODE(X) ((X ^ 0x17) + 0x17)

uint8_t signalNunchuck[6];
int byteCount;

uint8_t stickX, stickY;

void setup() {
  // 省略
}

void loop() {
  // 省略:signalNunchuck に受信データを格納

  if(byteCount >= 5){
    stickX = signalNunchuck[0];
    stickY = signalNunchuck[1];
  }

  // 省略:リクエスト分の通信終了処理
}

sX, sY は 8bit(1byte) で構成されています。そのためまずは、stickX, stickYuint8_t 型で宣言します。

そして signalNunchuck[] に受信データが埋まった後は、signalNunchuck[0]sXsignalNunchuck[1]sY が入っているので、それを stickX, stickY に設定することで、情報を取得できました。

ボタン

コントローラーのボタン部は、ON ⇔ OFF データを持つ

次にボタンです。これは受信データ(signalNunchuck[])の 6 番目のバイトデータ内から、0bit目 に btnZ、1bit目に btnC が格納されています。

末尾バイトデータに含まれるボタン情報を、ビット位置から取得
#include <Wire.h>

#define DECODE(X) ((X ^ 0x17) + 0x17)

uint8_t signalNunchuck[6];
int byteCount;

uint8_t buttonC, buttonZ;

void setup() {
  // 省略:シリアル通信と I2C 通信の準備
}

void loop() {
  // 省略:signalNunchuck に受信データを格納

  if(byteCount >= 5){
    buttonC = bitRead(signalNunchuck[5], 1);
    buttonZ = bitRead(signalNunchuck[5], 0);
  }

  // 省略:リクエスト分の通信終了処理
}

btnC, btnZ はそれぞれ 1bit で構成されています。そのため、buttonC, buttonZuint8_t 型で宣言します。

そして signalNunchuck[] に受信データが埋まった後は、signalNunchuck[5] の 1 bit目に btnC、0 bit目に btnZ が格納されています。そのため、bitRead(データ, 位置) で取得したビット情報を buttonC, buttonZ に設定することで、ボタン情報を取得できました。

加速度

コントローラーの加速度センサーは、XYZ の三次元座標データを持つ

最後に加速度情報です。これは複数のバイトデータに別れて構成されているので、もう一度 6byte のデータの内訳を見てみます。ここで見るデータは aX, aY, aZ の 3 つです。

Byte bit7 bit6 bit5 bit4 bit3 bit2 bit1 bit0
1 sX7 sX6 sX5 sX4 sX3 sX2 sX1 sX0
2 sY7 sY6 sY5 sY4 sY3 sY2 sY1 sY0
3 aX9 aX8 aX7 aX6 aX5 aX4 aX3 aX2
4 aY9 aY8 aY7 aY6 aY5 aY4 aY3 aY2
5 aZ9 aZ8 aZ7 aZ6 aZ5 aZ4 aZ3 aZ2
6 aZ1 aZ0 aY1 aY0 aX1 aX0 btnC btnZ

つまり加速度情報は、受信データ(signalNunchuck[])の 3 〜 5 番目に 3軸の上位 8bit分のデータが、6番目のバイトデータ内には bit位置 7-6 に aZ、bit位置 5-4 に aY、 bit位置 3-2 に aX、これらの下位 2bit分のデータが格納されていることになります。

そのため 特定のビット情報を抽出したり、ビットシフトを駆使して 10bit の情報を作っていくことになります。

複数のバイトデータに跨る加速度情報を組み合わせ、10bitとして取得
#include <Wire.h>

#define DECODE(X) ((X ^ 0x17) + 0x17)

uint8_t signalNunchuck[6];
int byteCount;

uint16_t accelX, accelY, accelZ;

void setup() {
  // 省略:シリアル通信と I2C 通信の準備
}

void loop() {
  // 省略:signalNunchuck に受信データを格納

  if(byteCount >= 5){
    accelX = ((uint16_t) signalNunchuck[2] << 2) | ((signalNunchuck[5] & B00001100) >> 2);
    accelY = ((uint16_t) signalNunchuck[3] << 2) | ((signalNunchuck[5] & B00110000) >> 4);
    accelZ = ((uint16_t) signalNunchuck[4] << 2) | ((signalNunchuck[5] & B11000000) >> 6);
  }

  // 省略:リクエスト分の通信終了処理
}

aX, aY, aZ はそれぞれ 10bit で構成されています。そのため accelX, accelY, accelZ を 16bitまで格納できる uint16_t で宣言します。

そして signalNunchuck[] に受信データが埋まった後は、複数に跨った情報を繋げて 10bit の情報を作っていきます。そこで、accelX = ((uint16_t) signalNunchuck[2] << 2) | ((signalNunchuck[5] & B00001100) >> 2); から見ていきましょう。

((uint16_t) signalNunchuck[2] << 2) は、10bit の上位 8bit を作っています。signalNunchuck[] は 8bit のデータなので、uint16_t の型キャストで 16bit データにしておきます。その上で左へ 2bit シフトすることで、10bit の上位 8bit になりました。
例えば、signalNunchuck[2]10111011 だった場合、この一連の処理で 0000001011101100 になります。

次に ((signalNunchuck[5] & B00001100) >> 2) は、10bit の下位 2bit を作っています。B00001100 というのは、必要なビット情報がある位置に 1 を立てたものです。これと 情報が混在している signalNunchuck[5] をビット演算子 & を使い、必要な箇所の情報を抽出します。最後に、この情報が下位 2bit となるよう 必要分 右方向へシフト処理をすることで、10bit の下位 2bit を作りました。
例えば、signalNunchuck[5]00011011 だった場合、B00001100 との AND処理で 00001000 となり、右シフトで 00000010 となります。

上位 8bit と下位 2bit を作成できたので、これらをビット演算子 | で連結することで、必要となる 10bit データを作成できました。例えば先程までの例 000000101110110000000010 の場合、これらを OR処理することで 0000001011101110(10進数だと 750)となります。これが加速度を表す情報となります。

出力

ここまでで「スティック」「ボタン」「加速度」の情報を取得することができました。全てまとめたコードは最後の方に載せておくとし、ここでは取得したデータを確認するために出力する方法を考えていきます。

単純に考えると、stickX, stickY は 8bit(0〜255)、buttonC, buttonZ は 1bit(0 or 1)、accelX, accelY, accelZ は 10bit(0〜1023)のデータです。

上記の移り変わりを Arduino IDE にある「シリアルモニター」で監視していく想定です。その際、それぞれの数値(0 だったり、128 だったり)によって文字位置が変わってしまうと見づらくなってしまいます。各種情報位置を揃えるため sprintf() を使い、各種データの桁数を揃えることでデータを見やすくしようと思います。

各種入力データをまとめて、文字位置を揃えて出力
#include <Wire.h>

// 省略:復号化用関数

// 省略:バイトデータ(6) を格納するための諸要素宣言

// 各種入力データ(加速度のみ 10bit 必要)
uint8_t stickX, stickY;
uint8_t buttonC, buttonZ;
uint16_t accelX, accelY, accelZ;

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

  // 省略:シェイクハンド通信
}

void loop() {
  // 省略:コントローラーに入力データ(6byte) を要求、復号化しつつ配列に格納

  // 入力データが適正に受け取れた場合に、諸処理に進む
  if(byteCount >= 5){

    // 省略:各種入力データの取得(スティック、ボタン、加速度)

    // 出力
    char buffer[100];
    sprintf(
      buffer, 
      "| Stick(%3d, %3d) | BtnC: %1d | BtnZ: %1d | Accel(%4d, %4d, %4d) |",
      stickX, stickY, buttonC, buttonZ, accelX, accelY, accelZ
    );
    Serial.println(buffer);
  }

  // 省略:リクエスト分の通信終了処理

  // 省略:データ入力の頻度を抑えるための遅延処理
}

これにより、データを出力することができました。

ここで、デフォルト状態(スティックが床に対し垂直にある状態)にコントローラーがある状態を想定します。下図のような状態です。

デフォルト状態のコントローラー

この状態で、実機で計測してみると、下記のようになりました。

| Stick(124, 132) | BtnC: 1 | BtnZ: 1 | Accel(520, 508, 722) |

Stick(sX ,sY) は 128 を中心とした 2 次元座標情報、BtnC, BtnZ は 0 が押下で 1 が開放、Accel(aX, aY, aZ) は 512 を中心とした 3 次元座標情報を表します。

スティックに関しては経年劣化からか少々ズレが生じていますが、中心となる 128 周辺にあるようです。加速度に関しては、X 及び Y は中心にあるということで 512 周辺に留まっています。Z 軸については、垂直方向に 1g 生じている関係から、中央 512 から 200 程度変動した状態にあります。X または Y 方向に 90度傾けると中央である 512 に近づき、上下反転させた状態にすると 300 くらいまで減少します。

まとめ

本記事では Wii ヌンチャクコントローラーを Arduino に接続し、コントローラーの各種情報を取得し出力してみました。

コード全体

コントローラーの入力情報について(長いので格納しています)

下表は、コントローラーが扱う情報一覧です。

名称 略称 サイズ 備考
Cボタン btnC 1bit 0 が押下、1 が開放を表す
Zボタン btnZ 1bit 0 が押下、1 が開放を表す
スティック-X sX 8bit 128 を中心とし、左に倒すと減少、右に倒すと増加
スティック-Y sY 8bit 128 を中心とし、下に倒すと減少、上に倒すと増加
加速度-X aX 10bit 512 を中心とし、左に倒すと減少、右に倒すと増加
加速度-Y aY 10bit 512 を中心とし、上に立てると減少、下に倒すと増加
加速度-Z aZ 10bit デフォルト時(※)で 760、X または Y 方向に 90 度倒すと 512、反転させると 320 に

上記データは 6byte(48bit) に まとめられて送られてきます。下表は、各バイトの情報です。

Byte bit7 bit6 bit5 bit4 bit3 bit2 bit1 bit0
1 sX7 sX6 sX5 sX4 sX3 sX2 sX1 sX0
2 sY7 sY6 sY5 sY4 sY3 sY2 sY1 sY0
3 aX9 aX8 aX7 aX6 aX5 aX4 aX3 aX2
4 aY9 aY8 aY7 aY6 aY5 aY4 aY3 aY2
5 aZ9 aZ8 aZ7 aZ6 aZ5 aZ4 aZ3 aZ2
6 aZ1 aZ0 aY1 aY0 aX1 aX0 btnC btnZ
コード全文(長いので格納しています)
コントローラーからスティック、ボタン、加速度情報を取得し表示
#include <Wire.h>

// 復号化
#define DECODE(X) ((X ^ 0x17) + 0x17)

// バイトデータ(6) を格納するための諸要素
uint8_t signalNunchuck[6];
int byteCount;

// 各種入力データ(加速度のみ 10bit 必要)
uint8_t stickX, stickY;
uint8_t buttonC, buttonZ;
uint16_t accelX, accelY, accelZ;

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

  // シェイクハンド通信(シロ用)
  Wire.begin();
  Wire.beginTransmission(0x52);
  Wire.write((uint8_t)0x40);
  Wire.write((uint8_t)0x00);
  Wire.endTransmission();
}

void loop() {
  // コントローラーに入力データ(6byte) を要求、復号化しつつ配列に格納
  byteCount = 0;
  Wire.requestFrom(0x52, 6);
  while(Wire.available()){
    signalNunchuck[byteCount] = DECODE(Wire.read());
    byteCount++;
  }

  // 入力データが適正に受け取れた場合に、諸処理に進む
  if(byteCount >= 5){

    // スティック情報(中心は 128)
    stickX = signalNunchuck[0];
    stickY = signalNunchuck[1];

    // ボタン情報(0 が押下状態)
    buttonC = bitRead(signalNunchuck[5], 1);
    buttonZ = bitRead(signalNunchuck[5], 0);

    // 加速度情報(中心は 512)
    //(下位 2bit は末尾バイトデータに分割して格納されているので、組み合わせて 10bit 情報に)
    accelX = ((uint16_t) signalNunchuck[2] << 2) | ((signalNunchuck[5] & B00001100) >> 2);
    accelY = ((uint16_t) signalNunchuck[3] << 2) | ((signalNunchuck[5] & B00110000) >> 4);
    accelZ = ((uint16_t) signalNunchuck[4] << 2) | ((signalNunchuck[5] & B11000000) >> 6);

    // 出力
    char buffer[100];
    sprintf(
      buffer, 
      "| Stick(%3d, %3d) | BtnC: %1d | BtnZ: %1d | Accel(%4d, %4d, %4d) |",
      stickX, stickY, buttonC, buttonZ, accelX, accelY, accelZ
    );
    Serial.println(buffer);
  }

  // リクエスト分の通信終了処理
  Wire.beginTransmission(0x52);
  Wire.write(0x00);
  Wire.endTransmission();

  // データ入力の頻度を抑えるための遅延処理
  delay(100);
}

雑記

本記事は、最初で説明したように「コントローラーをマウスとして機能させられないか?」という疑問から出発しています。本段落では、その課題に関するメモを含めた雑記を扱います。

  • マウス機能について
    • コントローラー情報の比較
    • 表現しきれない機能を、どう補うか
  • 加速度について
    • デバイスの向きを判定し、モード切替に利用したい
    • ブレが大きいため、もう少し滑らかにしたい(≒ 平滑化)
  • 通信の基礎、I2C の仕様などをまとめて書籍化したい

マウス機能について

マウスの機能を列挙してみると、下記のようになります。最後にある クリック関係については 右、中央、左 の 3 種類も考えなければなりません。

  • カーソル移動
  • ホイール移動
  • クリックやドラッグ操作
コントローラーでの機能割り当て

一方で、コントローラーは下記の情報を持っています。

  • スティックによる二次元座標
  • 2 種類あるボタン状態
  • 加速度センサーによる三次元座標

ここで、加速度センサーを使った情報はマウスに対して扱いづらいという事情が出てきます。
仮に加速度を使ってカーソル移動を司るよう作ってみたと想定します。この際 カーソルをその場に留めるには、デフォルトポジションを維持し続ける必要があります。手に持った状態で同じ位置をキープし続けるのは、かなり大変です。またサイト内のリンク文をクリックするなどの精密な作業が必要となった際、適切な調整がなされなければ、物凄く難度の高い操作となってしまいます。

そのため、加速度センサーを使わない方針で考えてみます。すると、下表のところまで決めることができました。

機能 コントローラー 備考
カーソル移動 スティック
左クリック関係 Cボタン ダブルクリックやドラッグも、このボタンの押し具合で判定
右クリック関係 Zボタン

使える部分を使った状態で、まだ「中クリック」「ホイール」操作が実装できていません。これを導入するには、下記の2つの手法が考えられます。

  1. ボタンの 1 つをモード切替に割り当て、2つのモードで各種機能を割り当てる
  2. 加速度を使い、デバイスが特定のポジション時にモード切替を行うようにし、2つのモードで各種機能を割り当て

ここからは、これら「モード切替による機能付与」について考えていきます。

モード切替による対応について

コントローラーで使える情報が少ない中で多い機能を実装するために、「Aモード⇔Bモード」の形で2つのモードを設け、例えばスティックに「Aモード時はカーソル移動」「Bモードはホイール移動」といった複数の機能を割り当てるのが、ここで行いたい話となります。

先ほど提示した2つの手法の内、1 番の「ボタン(例:Zボタン)によるモード切り替え」ですが、その場合の各種入力は下記のようになるでしょう。

入力 Aモード時の役割 Bモード時の役割
スティック カーソル移動 ホイール移動
Cボタン 左クリック関係 右クリック関係
Zボタン モード切替 モード切替

上表では、「中クリック」機能が実装できていません。Zボタンの役割が「モード切替」で固定されてしまい機能枠を潰してしまっているためです。

そこで、2つの手法の内の 2 番目である「加速度を使ったデバイス向きによる判定」を考えてみます。ここでは「デバイスを縦に立てた場合にモード切替」という条件で考えてみます。この場合なら枠を潰すことなく使えるので、各種入力に対し下表のように割り当てることができます。

入力 Aモード時の役割 Bモード時の役割
スティック カーソル移動 ホイール移動
Cボタン 左クリック関係 左クリック関係
Zボタン 右クリック関係 中クリック関係
加速度 モード切替 モード切替

枠に余裕があるので、Cボタンを左クリックとして固定しつつ、Zボタンを使って「右クリック」「中クリック」関係に機能を割り当てることができました。現段階においては、この加速度センサーをモード判定に使用するという考えで、機能実装を考えています。

ただし その場合、加速度情報を扱いやすい形に調整しないと上手くいかない可能性が高いと考えられます。

加速度の取り扱いについて

ここでは、加速度情報をどう取り扱うかについて考えていきます。

追記:記事を作成し、その中で加速度情報を使って「どうやってデバイスの向きを判定するか」「実機で考慮しなければならない点にどう対処するか」を考えました。よろしければ下記リンクから参照ください。

https://zenn.dev/nonaka101/articles/orientation-by-acceleration

加速度からのデバイスの向きを計算

今回扱った加速度情報を使って、デバイスの向きを判定し、それをモード切り替えに使う方法を考えてみます。

加速度情報は 中心を 512 とした 符号なし 10bit データですが、これを中心 0 の負符号有りのベクトル情報として扱うようにします。その上で、空間ベクトルの為す角を使えば、向きを判定できそうです。

例えば、\overrightarrow{a} = (a_1, a_2, a_3), \overrightarrow{b} = (b_1, b_2, b_3) とした際、両者の為す角を \theta (0\degree \leqq \theta \leqq 180\degree) とすると、下記の数式が成り立ちます。

\cos \theta = \frac{\overrightarrow{a} \cdot \overrightarrow{b}}{|\overrightarrow{a}| \, |\overrightarrow{b}|} = \frac{a_1 b_1 + a_2 b_2 + a_3 b_3}{\sqrt{a_1^2 + a_2^2 + a_3^2} \sqrt{b_1^2 + b_2^2 + b_3^2}}

そして今回「デバイスが垂直方向に立っているか」を判定する想定で考えてみます。すると、\overrightarrow{b} については \overrightarrow{b} = (0, -1, 0) で固定することができます。となると、先程の数式はもっと簡略化することができ、最終的には下記のようになります。

\cos \theta = \frac{-a_2}{\sqrt{a_1^2 + a_2^2 + a_3^2}}

あとは結果を \arccos(\cos \theta) にかけてラジアンを取得し、それを角度へと変換すれば「デバイスを垂直に立てた状態との角度」を算出できます。精度を踏まえて ある程度のマージンを取り、「誤差◯度以内の垂直状態の時、モードを切り替える」といった形で実装すれば、調整しやすくなりそうです。

加速度を平滑化処理

コントローラーから送られてくる加速度情報は、誤差が 10% 近くあるため、1 つ 1 つの振れ幅が大きいです。そしてマウスクリックなどの情報は、秒間あたり高頻度で取得しなければならない情報です。この頻度でブレ幅が大きい加速度情報をそのまま使い、それでモード変換を判定させてしまうと おかしな挙動(例:1秒間にモードの切り替えが頻発する)になることが想定されます。

上記を防ぐためには、加速度の移り変わりをもっと滑らかな形にする必要があります。これを平滑化といいます。

現段階での案としては、「移動平均」を利用した手法となります。定数として「蓄積する回数」を用意し、それを用いて「連続した◯回分の情報値」を管理するようにします。そのデータの平均値を使うようにすれば、加速度の移り変わりが滑らかになり、おかしな挙動を抑えることができるのではと考えています。

書籍化について

コントローラーと Arduino を接続する際、I2C による通信を学ぶことが大きな前進へと繋がりました。そして I2C を学ぶには、通信の基礎についての理解が必要不可欠でした。

現状 この辺りの話を わかりやすく 解説してくれる書籍やサイトが見つからなかったので、自分の学習メモをまとめて書籍化できたらなぁと考えています。
(もし解説してくれているものがあれば、教えてもらえると幸いです)

参考文献

下記は本記事を作成するに参考にした資料等になります。

I2C関係

https://ja.wikipedia.org/wiki/I2C

https://www.nxp.com/docs/ja/user-guide/UM10204.pdf

Arduino

https://docs.arduino.cc/

http://www.musashinodenpa.com/arduino/ref/

https://www.oreilly.co.jp/books/9784814400232/

ヌンチャク関係

https://wiibrew.org/wiki/Wiimote/Extension_Controllers/Nunchuck

https://github.com/madhephaestus/WiiChuck

https://github.com/todbot/wiichuck_adapter

https://github.com/rkrishnasanka/ArduinoNunchuk

加速度情報からデバイス向きを判定

https://qiita.com/kobayashi_ryo/items/48db56e62f7a76532d38

空間ベクトルの為す角を算出する式については、追加で下記を参考にしています。

https://mathwords.net/bekutorunasukaku

https://w3e.kanazawa-it.ac.jp/math/category/vector/henkan-tex.cgi?target=/math/category/vector/naiseki-wo-fukumu-kihonsiki.html&pcview=2

Discussion