Wii Nunchuckを無線化してモバイルVRに生かす

# 概要

手軽なモバイルVRには一つの欠点がある。
``スマホをセットするとスマホが操作できなくなる``と言う点だ。
これ何とかしたい。
しかも安く何とかしたい。

そこでハードオフで100円のWiiヌンチャクをBTマウスに改造することにした。


出来上がったもの。

![finish_wiinunchuck_wireless.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42445/887427d3-419f-5eb7-f41c-e580164fc5f4.jpeg)

<font color="dark_pink">自分用なので細かいことは気にしない主義</font>

Githubにアップしました！
[wii-nunchuck-esp32](https://github.com/miyatama/wii-nunchuck-esp32)

 + 準備するもの
   + Wii ヌンチャク(白)
   + ESP32
   + USBシリアル変換基盤
   + RaspberryPi3
   + 電子工作の道具たち
     + 熱収縮チューブ
     + 抵抗
     + ユニバ基盤
     + 配線

手順

 + Wiiヌンチャクがi2c通信で使える事を確認する
 + ESP32をブレイクアウトする
 + ESP32にi2c + bluetoothのソフトを書き込む

重要な点

``Wiiヌンチャクは白を利用してます！``

もう一回書いておく。

``Wiiヌンチャクは白を利用してます！``

# Wiiヌンチャクがi2c通信で使える事を確認する

WiiヌンチャクをHardOffなどで購入する。100円だった。安い。
赤テープは動作未確認のものらしいが壊れていたらもう一つ買うだけなので気にしない。

とりあえずケーブルを切って通信線を出す。

| 色 | 意味 | 
| :----: | :----- |
| 赤 | VCC |
| 緑 | SDA |
| 黄 | SCL |
| 白 | GND |
| 黒 | 謎。そこはかとなくGNDっぽいけど謎。Wiiヌンチャク側のみに存在する。アース？ |

![wiinunchuck_terminate.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42445/80d60bfe-d3d3-31de-fe05-606957431d2f.jpeg)


<font color="dark_pink">こんな感じ</font>


RaspberryPi3と接続してi2c通信でデータが取れるかを確認する。
接続としては以下の通り

| Wii Nunchuck | RaspberryPi | 3.3v電源 |
| :----- | :----- | :----- |
| 赤 | | 赤 |
| 緑 | Pin No.3 - SDA | 10kΩ通して3.3v入れる |
| 黄 | Pin No.5 - SCL | 10kΩ通して3.3v入れる |
| 白 | Pin No.9 - GND | 黒 |

RaspberryPiで下記プログラムを実行

```python:main.py
from smbus2 import SMBusWrapper, i2c_msg
import time

def init_wii_nunchuck(i2c, addr):
    print("start init_wii_nunchuck")
    data = [0x40, 0x00]
    msg = i2c_msg.write(addr, data)
    i2c.i2c_rdwr(msg)
    time.sleep(0.5)

def read_wii_nunchuck(i2c, addr):
    print("start read_wii_nunchuck")
    wiiDataReader = i2c_msg.read(addr, 6)
    i2c.i2c_rdwr(wiiDataReader)
    controllerData = list(wiiDataReader)

    joystic_x = controllerData[0]
    joystic_y = controllerData[1]
    buttons = controllerData[5]
    c_button = (buttons >> 1) & 0x01
    z_button = buttons & 0x01
    time.sleep(0.2)
    print("joystic: (x, y) = ({}, {})".format(joystic_x, joystic_y))
    print("button c: {}".format(c_button))
    print("button z: {}".format(z_button))

    terminatorWriter = i2c_msg.write(addr, [0x00])
    i2c.i2c_rdwr(terminatorWriter)
    print("finish read_wii_nunchuck")

def main():
  wii_nunchuck_addr = 0x52
  try:
    # with SMBusWrapper(1) as i2c:
    with SMBusWrapper(1) as i2c:
      init_wii_nunchuck(i2c, wii_nunchuck_addr)
      while(True):
        time.sleep(0.5)
        try:
          read_wii_nunchuck(i2c, wii_nunchuck_addr)
        except OSError as e:
          print("read_wii_nunchuck error: {}".format(e))
          break
  except OSError as e:
    print("init_wii_nunchuck error: {}".format(e))
  except KeyboardInterrupt:
    return
  except Exception as e:
    print("initial_wii_nunchuck error")
    print(e)

if __name__ == "__main__":
  main()
```

```bash
sudo pip3 install smbus2
sudo python3 ./main.py
```

Wiiヌンチャクを動かしてみてデータがちょこちょこ変わってればOK

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42445/504203b3-2e9b-f000-1739-900abf83c788.png)


# ESP32をブレイクアウトする

![esp322.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42445/4df31558-1c5d-5daa-e3e8-5dc95ee59beb.jpeg)


ESP32を知らない方にざっとESP32を説明すると、
``700円ぐらいで買える色々できる奴``だ。

そしてブレイクアウトを知らない方にざっとブレイクアウトを説明すると、
``マイコンを使えるようにする作業``だ。

つまり、ESP32を使える様にする作業だ。
ここで細かく書いてもいいのだが、面倒なので表だけ書く。
気が向いたら回路図書く。

| no | ESP32 pin | 用途 |
| -----: | :----- | :------ |
| 1 | 1 - GND | GND |
| 2 | 2 - 3V3 | 3.3V。100uFぐらいのコンデンサ入れる。 |
| 3 | 3 - EN | プルアップ。swなりでGNDに落とせるようにしとく。 |
| 4 | 25 - IO0 | swなりでGNDに落とせるようにしておく |
| 5 | 33 - SDA | I2C用のSDA。プルアップ。|
| 6 | 34 - RXD0 | ESP32書き込み用のUSBシリアル変換基盤TXにつなぐ。 |
| 7 | 35 - TXD0 | ESP32書き込み用のUSBシリアル変換基盤RXにつなぐ。 |
| 7 | 36 - SCL | I2C用のSCL。プルアップ。 |

# ESP32にi2c + bluetoothのソフトを書き込む

ラズパイで書き込み環境をセットアップする。
[ESP-IDF Programming Guide](https://docs.espressif.com/projects/esp-idf/en/latest/)にそって環境構築する。
環境構築で詰まることは特になかったので割愛。
開発環境が出来たらexampleとか一通り眺める。

Wiiヌンチャク(白)からi2c経由で情報を取得するには下記手順を踏む

 + 最初
   + 0x40,0x00を書き込む
 + 読み込み
   + 6バイト読む
   + 0x00を書き込む
 
うん。書いててもさっぱりわからないのでコード貼る。けど、全体だと長いので、部分的に説明する。
時間があったらGithubに上げる。
尚、BlueToothデバイスを作成する際には機器名を指定しておいた方が良い。似たのがあると混乱する(した)。
プログラムの中で指定できる。

これがメインタスクの初回で一回だけ呼ばれる処理。0x40,0x00を書き込む。

```c++
static esp_err_t initialize_wii_nunchuck(i2c_port_t i2c_num)
{
    ESP_LOGI(WII_NUNCHUCK_TAG, "initialize_wii_nunchuck");
    i2c_cmd_handle_t cmd = i2c_cmd_link_create();
    i2c_master_start(cmd);
    i2c_master_write_byte(cmd, (ESP_SLAVE_ADDR << 1) | WRITE_BIT, ACK_CHECK_EN);
    i2c_master_write_byte(cmd, 0x40, ACK_CHECK_EN);
    i2c_master_write_byte(cmd, 0x00, ACK_CHECK_EN);
    i2c_master_stop(cmd);
    esp_err_t ret = i2c_master_cmd_begin(i2c_num, cmd, 1000 / portTICK_RATE_MS);
    i2c_cmd_link_delete(cmd);
    return ret;
}
```

6バイト読み込み

```c++
static esp_err_t i2c_master_read_slave(i2c_port_t i2c_num, uint8_t *data_rd, size_t size)
{
    esp_err_t result = ESP_OK;
    ESP_LOGI(WII_NUNCHUCK_TAG, "i2c_master_read_slave");
    if (size == 0) {
        return ESP_OK;
    }
    i2c_cmd_handle_t cmd = i2c_cmd_link_create();
    i2c_master_start(cmd);
    i2c_master_write_byte(cmd, (ESP_SLAVE_ADDR << 1) | READ_BIT, ACK_CHECK_EN);
    if (size > 1) {
        i2c_master_read(cmd, data_rd, size - 1, ACK_VAL);
    }
    i2c_master_read_byte(cmd, data_rd + size - 1, NACK_VAL);
    i2c_master_stop(cmd);
    esp_err_t ret = i2c_master_cmd_begin(i2c_num, cmd, 1000 / portTICK_RATE_MS);
    i2c_cmd_link_delete(cmd);
    if (ret == ESP_OK) {
        ESP_LOGI(WII_NUNCHUCK_TAG, "i2c_master_read_slave data reading succeed: %d", ret);
    } else {
        result = ESP_ERR_TIMEOUT;
        ESP_LOGE(WII_NUNCHUCK_TAG, "i2c_master_read_slave data reading failure: %d", ret);
    }
    return result;
}
```

読み込み後に行う0x00の書き込み

```c++
static esp_err_t i2c_master_write_slave_terminator(i2c_port_t i2c_num)
{
    esp_err_t result = ESP_OK;
    i2c_cmd_handle_t writeCommand = i2c_cmd_link_create();
    i2c_master_start(writeCommand);
    i2c_master_write_byte(writeCommand, (ESP_SLAVE_ADDR << 1) | WRITE_BIT, ACK_CHECK_EN);
    i2c_master_write_byte(writeCommand, 0x00, ACK_CHECK_EN);
    i2c_master_stop(writeCommand);
    esp_err_t writeResult = i2c_master_cmd_begin(i2c_num, writeCommand, 1000 / portTICK_RATE_MS);
    if (writeResult == ESP_OK) {
        ESP_LOGI(WII_NUNCHUCK_TAG, "i2c_master_write_slave_terminator send terminator succeed: %d", writeResult);
    } else {
        result = ESP_ERR_TIMEOUT;
        ESP_LOGE(WII_NUNCHUCK_TAG, "i2c_master_write_slave_terminator send terminator failure: %d", writeResult);
    }
    i2c_cmd_link_delete(writeCommand);
    return result;
}
```

取ったデータをマウスデータとして送る

```c++
int8_t mickeys_x = 0; // Joystick - X
ESP_LOGI(WII_NUNCHUCK_TAG, "send mouse (%d, %d)", data_rd[0], data_rd[1]);
if (data_rd[0] > 160) {
    mickeys_x = 50;
}
if (data_rd[0] < 100) {
    mickeys_x = -50;
}
int8_t mickeys_y = 0; // Joystick - Y
if (data_rd[1] > 160) {
    mickeys_y = -50;
}
if (data_rd[1] < 100) {
    mickeys_y = 50;
}
uint8_t mouse_button = 0x00;
if (c_button_state == 0x02 && (data_rd[5] & 0x02) == 0x00) {
    mouse_button = mouse_button | 0x01;
} else if (c_button_state == 0x00 && (data_rd[5] & 0x02) == 0x02) {
    mouse_button = mouse_button & 0xfe;
}
if (z_button_state == 0x01 && (data_rd[5] & 0x01) == 0x00) {
    mouse_button = mouse_button | 0x02;
} else if (z_button_state == 0x00 && (data_rd[5] & 0x01) == 0x01) {
    mouse_button = mouse_button & 0xfd;
}
c_button_state = data_rd[5] & 0x02;
z_button_state = data_rd[5] & 0x01;

ESP_LOGI(WII_NUNCHUCK_TAG, "Send the mouse event");
// create mouse event
esp_hidd_send_mouse_value(hid_conn_id, mouse_button, mickeys_x, mickeys_y);
```

書き込み用プログラムができたら、書き込み。(EN, IO0のスイッチ設定変更を忘れずに行う)

```bash
make menuconfig
make flash
```

成功したら、UARTで情報を表示する。(EN, IO0のスイッチ設定変更を忘れずに行う)

```python:main.py
import serial
import time
import threading
import Queue

class SerCom:
  def __init__(self, tty, baud='115200'):
    self.ser = serial.Serial(tty, baud, timeout=0.1)
    self.queue = Queue.Queue()

    self.event = threading.Event()
    self.thread_r = threading.Thread(target=self.recv_)
    self.thread_r.start()

  def recv_(self):
    while not self.event.is_set():
      line = self.ser.readline()
      if len(line) > 0:
        print(line)
        self.queue.put(line)

  def send(self, data):
    self.ser.write(data)

  def stop(self):
    self.event.set()
    self.thread_r.join()

ser = SerCom('/dev/ttyUSB0', '115200')
try:
  while True:
    time.sleep(5)
except:
  ser.stop()
```

で実行するとこんな感じ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42445/716136d9-898e-ad36-d070-6303c8c1dfc7.png)


# 振り返り

なんだかんだ大変だった。何せ情報がない。

ArduinoとかRaspberryPiとかは結構記事とかチャレンジしている人とかはいる。
さらに``Wiiヌンチャクを○○してみた``系の記事もある。
が、``ESP32をESP-IDFで扱う``と言う記事自体が割と少ない。
さらにそこから``I2Cを使う``とか``BlueToothマウスを作る``とかになるともっと少ない。

何よりみんな成功例しかださないから、あってるかどうか判断つかない。
極端な話、ログの出力方法はあるけど、ログの見方がわかんないって感じだ。

そんなこんなで2回失敗して、3回目でできた。あきらめない事が重要。
自分の作ったコントローラで自分のスマホを操作できたとき、``よっしゃ！``って声出た。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42445/3bc44a9c-a910-c560-9a84-88fb766db1eb.png)

<font color="dark_pink">レベルアップの瞬間</font>