8
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Raspberry Pi Zeroで、Wiiリモコン→USBゲームパッドブリッジを作ってみる

Last updated at Posted at 2019-03-09

目的

  • ドラクエ10のPC版で、古のWiiリモコン+ヌンチャク操作を実現してみたい
  • PC側にドライバ入れたりとか、ゲーム改造したりとかせずに!
  • あら、ちょうどいいとこに、ラズパイさんが!
  • これ、使ってみるべ

やりざま

* Raspberry piをUSBデバイスとしてPCに認識させ
 USBデバイス動作ができるRaspberry piが必要なので、無印ではなくZeroシリーズが必要!

*Blutooth経由でWiiリモコンに繋ぎ
 Zeroシリーズは、USBが1ポートしかない為、無印ではなくWが必要!

*データを一部整形して、中継する感じで
 ヌンチャク振ってジャンプ!とか、加速度センサ→ボタンへの読み替えをラズパイ側でしたいなあと

よういするもの

  • Raspberry Pi Zero W(H) (ヘッダ使わないのでWでいいんだけど、WHしか売ってないよね最近は )
  • Wiiリモコン ( Foxxconn FCCID:UMB-WCF7版 / WiiリモコンPlus FCCID:POO-WC62でも動いたよ! )
  • ヌンチャク
  • SDカードとかUSB MicroBケーブルとかHDMIアダプタとか

初期セットアップ

基本的に以下全てroot作業です
今回はUSBをデバイス側として動作させるため、USBシリアルやEthernetを使わずにセットアップしたほうが、ハマらないと思う
つーわけで、普通にRaspbianのLite版をダウンロードし、そのままRufusでSDカードに書き込み
・OSバージョン
 2018-11-13-raspbian-stretch-lite.img

sshの御呪いとか何もせず、HDMIコンソールとキーボード繋いで起動、raspi-conrigで以下設定な感じ
・Wifi
 ネットワークはWifi経由で、カントリーをJPに、SSIDとパスワード入れて接続
・Localization Options
 HDMIコンソール使っているので、フォントがめんどくさい予感、なのでUSのままで
 Timezoneは一応Asia/Tokyoに
・Interface
 SSHをEnableに

raspi-configを抜け、ip addrでIPアドレスを確認、wlan0にIPアドレスが振られていたらOK!
一旦リブート後、PCからSSH経由で接続し、アップデート一式を実行
以下URL参照な感じで
https://qiita.com/tanuwo/items/04df160281153c0e24bc

デバイス下層周りを使うため、今回は念のためファームウェアも最新にしておきました

USBデバイス化と、ゲームパット生成

使用したRaspbianはLinux Kernel 4.4なので、compositeなUSBデバイスがモダンでいい感じかも知れないなと

USBゲームパット生成自体は、以下URLを
https://qiita.com/toyoshim/items/59c1af01919cb3494512
compositeなUSBデバイス周りは、以下URLを参照しました
https://qiita.com/exthnet/items/98aa9b6d6a606f8f2cf8

サブクラス周りで悩んだけれども、ゲームパッドはprotocol0 subclass0でいいみたい
このへん参照しました
https://sites.google.com/site/toriaezunomemo/home/communication-device-class/class-specific-codes

dwc2モジュール読み込み時に、ついでにaudioをoffに
MATRIXLEDの癖で、、、多分軽くなるはず!

/boot/config.txt
# Enable audio (loads snd_bcm2835)
dtparam=audio=off

# USB otg ( dwc2 ) enable
dtoverlay=dwc2
/etc/modules
echo "dwc2" | tee -a /etc/modules
echo "libcomposite" | tee -a /etc/modules

で、リブート!

設定/起動スクリプト

まあ、ファイルを生成&権限作成

setup
touch /usr/bin/isticktoit_usb
chmod +x /usr/bin/isticktoit_usb

上記URLを参考に、6バイト/スイッチ12個/POVハット1個/スティック4軸なゲームパッドデバイスを定義

/usr/bin/isticktoit_usb
#!/bin/bash
cd /sys/kernel/config/usb_gadget/
mkdir -p isticktoit
cd isticktoit
echo 0x1d6b > idVendor # Linux Foundation
echo 0x0104 > idProduct # Multifunction Composite Gadget
echo 0x0100 > bcdDevice # v1.0.0
echo 0x0200 > bcdUSB # USB2
mkdir -p strings/0x409
echo "fedcba9876543210" > strings/0x409/serialnumber
echo "eucalyptus." > strings/0x409/manufacturer
echo "Wiimote Bridge" > strings/0x409/product
mkdir -p configs/c.1/strings/0x409
echo "Config 1: ECM network" > configs/c.1/strings/0x409/configuration
echo 250 > configs/c.1/MaxPower
# Add functions here
mkdir -p functions/hid.usb0
echo 0 > functions/hid.usb0/protocol
echo 0 > functions/hid.usb0/subclass
echo 6 > functions/hid.usb0/report_length
echo -ne \\x05\\x01\\x09\\x05\\xa1\\x01\\x05\\x09\\x15\\x00\\x25\\x01\\x19\\x01\\x29\\x0c\\x75\\x01\\x95\\x0c\\x81\\x02\\x05\\x01\\x09\\x39\\x25\\x07\\x35\\x00\\x46\\x3b\\x01\\x75\\x04\\x95\\x01\\x65\\x14\\x81\\x42\\x09\\x01\\x15\\x81\\x25\\x7f\\x35\\x81\\x45\\x7f\\xa1\\x00\\x09\\x30\\x09\\x31\\x09\\x33\\x09\\x34\\x75\\x08\\x95\\x04\\x81\\x02\\xc0\\xc0 > functions/hid.usb0/report_desc

ln -s functions/hid.usb0 configs/c.1/

# End functions
ls /sys/class/udc > UDC

起動と認識、終了

ここで一度shutdown、御手持ちのMicroUSB-BケーブルでPCと接続(PWR側じゃなくてUSB側をね)
PCからのUSB電源で起動するので、セルフパワーなUSBHUBをかましたりすることを推奨な感じで

起動は「/usr/bin/isticktoit_usb」
上手くいけば、PC側デバイスマネージャにゲームパッドが現れます
終了は「rm /sys/kernel/config/usb_gadget/isticktoit/configs/c.1/hid.usb0」
テストは、コントロールパネル→デバイスとプリンター→つないだゲームパッド(上記だとWiimote Bride)を選択し右クリックからの「ゲームコントローラーの設定」→プロパティ で行えます
但し!、このプロパティ、ウィンドウがアクティブな時じゃないと描画更新してくれません
echoコマンドでボタン状態弄れますが、更新されん!、とかなり悩んじゃいました・・・
例えば、「echo -ne "\x01\x20\x00\x00\x00\x00" > /dev/hidg0」でボタン状態がこんな感じに更新されます
dqx_wii4.png

Wiiリモコンの接続

この辺を参照しました
https://harukingworld.blog.fc2.com/blog-entry-462.html
https://uepon.hatenadiary.com/entry/2016/04/08/123950

setup
apt-get install git
cd /opt
git clone https://github.com/the-raspberry-pi-guy/Wiimote.git
cd Wiimote
sh setup.sh

まあ、今更のWiiリモコンなので、ソースが古い感じではありますが
cwiidを使う感じなのですが、WiiリモコンPlusもちゃんと認識しましたよ、と

セットアップが終わったら、Example下の「wiimote.py」を実行してみる感じで
ラズパイの近くにWiiリモコンを持っていき、しつこく「1」と「2」のボタンを押しまくると、繋がります
抜けるときは「+」と「-」を押せばOK!
「Home」ボタンで、加速度センサの値がゲットできますよと

Wiiリモコンブリッジの作製

ここまできたら、あとは上記ExampleなPythonの小改造でなんとかなりそうな感じです
ヌンチャク周りは、以下URLを参照してみたりしてみました
https://github.com/Haven-Lau/Wiimote-for-Raspberry-Pi-Python/blob/master/wiimotetest2.py

wiimotebridge.py
import cwiid, time
import codecs
import os, sys

button_delay = 0.005
acc_threshold = 96
stick_duplex = 2

time.sleep(1)

while True:
# wait connection...
    try:
        wii=cwiid.Wiimote()
    except RuntimeError:
        time.sleep(0.2)
        continue

# connect and work
    wii.rpt_mode = cwiid.RPT_BTN | cwiid.RPT_ACC | cwiid.RPT_EXT

    wii.rumble = 1
    time.sleep(0.5)
    wii.rumble = 0

    prevaccremcalc = 65535
    prevaccnuncalc = 65535
    reload(sys)
    outputusb = open('/dev/hidg0', 'rb+')
    while True:
        buttons = wii.state['buttons']
        nunchukbuttons = wii.state['nunchuk']['buttons']
        accrem = wii.state['acc']
        accnun = wii.state['nunchuk']['acc']
        sticknun = wii.state['nunchuk']['stick']

# Detects whether + and - are held down and if they are it quits the program
        if (buttons - cwiid.BTN_PLUS - cwiid.BTN_MINUS == 0):
            wii.rumble = 1
            time.sleep(0.5)
            wii.rumble = 0
            outputusb.close()
            break

# stick position
        stickposx = sticknun[0] - 128
        stickposy = 128 - sticknun[1]
        stickposx *= stick_duplex
        if stickposx > 127:
            stickposx = 127
        if stickposx < -127:
            stickposx = -127
        stickposy *= stick_duplex
        if stickposy > 127:
            stickposy = 127
        if stickposy < -127:
            stickposy = -127
        stickposx = '{:02x}'.format(stickposx & 0xff)
        stickposy = '{:02x}'.format(stickposy & 0xff)
        stickposzx = 0
        stickposzy = 0

# make buttionbits
        outputbit0 = 0
        outputbit1 = 0
        jumpdetect = 0
# nunchuk Z ( autorun )
        if nunchukbuttons == 1:
            outputbit0 += 16
# nunchuk C ( main command window )
        if nunchukbuttons == 2:
            outputbit0 += 1
# nunchuk Z+C
        if nunchukbuttons == 3:
            outputbit0 += 17
# remote 1 ( map window )
        if (buttons & cwiid.BTN_1):
            outputbit0 += 8
# remote 2 ( jump / camera )
        if (buttons & cwiid.BTN_2):
            jumpdetect = 1
# remote + ( communication window )
        if (buttons & cwiid.BTN_PLUS):
            outputbit1 += 2
# remote - ( camera L )
        if (buttons & cwiid.BTN_MINUS):
            outputbit0 += 64
# remote home ( camera R )
        if (buttons & cwiid.BTN_HOME):
            outputbit0 += 128
# remote A ( multipurpose )
        if (buttons & cwiid.BTN_A):
            outputbit0 += 2
# remote B ( cancel )
        if (buttons & cwiid.BTN_B):
            outputbit0 += 4
# make HATSWITCH
        HATDATA = 0
        if (buttons & cwiid.BTN_LEFT):
            HATDATA += 1
            stickposzx = 127
        if(buttons & cwiid.BTN_RIGHT):
            HATDATA += 2
            stickposzx = -127
        if (buttons & cwiid.BTN_UP):
            HATDATA += 4
            stickposzy = 127
        if (buttons & cwiid.BTN_DOWN):
            HATDATA += 8
            stickposzy = -127
        if HATDATA == 1:
            outputbit1 += 96
        elif HATDATA == 2:
            outputbit1 += 32
        elif HATDATA == 4:
            pass
        elif HATDATA == 5:
            outputbit1 += 112
        elif HATDATA == 6:
            outputbit1 += 16
        elif HATDATA == 8:
            outputbit1 += 64
        elif HATDATA == 9:
            outputbit1 += 80
        elif HATDATA == 10:
            outputbit1 += 48
        else:
            outputbit1 += 128
        stickposzx = '{:02x}'.format(stickposzx & 0xff)
        stickposzy = '{:02x}'.format(stickposzy & 0xff)
# shake wiiremote ( camera reset )
        accremcalc = accrem[0] + accrem[1] + accrem[2]
        if prevaccremcalc != 65535:
            accremtry = abs(accremcalc - prevaccremcalc)
            if accremtry > acc_threshold:
                outputbit1 += 8
        prevaccremcalc = accremcalc
# shake nunchack ( jump )
        accnuncalc = accnun[0] + accnun[1] + accnun[2]
        if prevaccnuncalc != 65535:
            accnuntry = abs(accnuncalc - prevaccnuncalc)
            if accnuntry > acc_threshold:
                outputbit0 += 32
            elif jumpdetect == 1:
                outputbit0 += 32
        prevaccnuncalc = accnuncalc

# make outputbinary
        outputbit0 = '{:02x}'.format(outputbit0)
        outputbit1 = '{:02x}'.format(outputbit1)
         print str(outputbit0) + str(outputbit1) + str(stickposx) + str(stickposy) + str(stickposzx) + str(stickposzy)
        outputdata = codecs.decode(str(outputbit0) + str(outputbit1) + str(stickposx) + str(stickposy) + str(stickposzx) + str(stickposzy),'hex_codec')
        outputusb.write(outputdata)
        outputusb.flush()
        time.sleep(button_delay)

ハマりどころは、POV HAT周りの実装と、あと16進変換周りかな・・・
変換周りは以下な感じ
*手持ちのヌンチャクのスティックデータがどうもパッとしないので、値を逓倍する感じで
*ヌンチャク振ってジャンプ、Wiiリモコン振ってカメラリセットは、乱暴にXYZ3値を足しこんで、前データとの差の絶対値でボタンを発火するようにしてみたり
オリジナル要素として、2でジャンプ(長押しでカメラ起動)、-とHOMEでコミュニケーションウインドウのタブ送り(とカメラ回転)ができます。
あと、DQXコンフィグでカメラ用アナログスティックを、十字キー操作と同時に割り当てることもできたりして。

キーバインディング

上記URLの改造なので、パケット構造は以下な感じ

packet
+---------+-----+-----+-----+-----+-----+-----+-----+-----+
|Byte Bits|  7  |  6  |  5  |  4  |  3  |  2  |  1  |  0  |
| 10 calc | 128 | 64  | 32  | 16  |  8  |  4  |  2  |  1  |
+---------+-----+-----+-----+-----+-----+-----+-----+-----+
|      +0 |btn08|btn07|btn06|btn05|btn04|btn03|btn02|btn01|
+---------+-----+-----+-----+-----+-----+-----+-----+-----+
|      +1 |      Hat switch       |btn12|btn11|btn10|btn09|
+---------+-----------------------+-----+-----+-----+-----+
|      +2 |                    X axis                     |
+---------+-----------------------------------------------+
|      +3 |                    Y axis                     |
+---------+-----------------------------------------------+
|      +4 |                   XR axis                     |
+---------+-----------------------------------------------+
|      +5 |                   YR axis                     |
+---------+-----------------------------------------------+

まあ、1、2、4、8、16・・・を足していく、かなり乱暴なやり口で
キーの割り当ては、archiveに残ってたDQXページと
dqx_wii2.png

PC版DQXに標準で定義されている「ドラゴンクエストX ゲームコントローラ for PC」で相関しました
dqx_wii3.png

実行!

上記スクリプトを実行すると、まずは何も起きません
ラズパイの近くにWiiリモコンを持っていき、「1+2」あるいは裏面Syncボタンで接続
接続後は、Wiiリモコンが震えた後、だーーっと6バイト16進なデータがターミナルに流れます
同時に、6バイトバイナリをUSBホストへ投げ続けます
ゲームコントローラのプロパティや、ゲーム画面そのもので、動作を確認できますよ、と
「make outputbinary」のprint文を削除することで、ターミナル出力は抑制できます
「+」と「-」で終了!

サービス登録

まあ、USBドングル化、みたいな
キック用スクリプトと、systemdへの登録をすれば、「PCに繋げば使える」リモコンアダプタに化けますよ

/opt/wiimotebridgekicker.sh
#!/bin/bash

while :
do
    rm /sys/kernel/config/usb_gadget/isticktoit/configs/c.1/hid.usb0  > /dev/null 2>&1
    /bin/sleep 2
    /usr/bin/isticktoit_usb > dev/null 2>&1
    /bin/sleep 2
    /usr/bin/python /opt/wiimotebridge.py > /dev/null 2>&1
done
/etc/systemd/system/wiimotebridge.service
[Unit]
Description = wiimotebridge
After=local-fs.target
ConditionPathExists=/opt/

[Service]
ExecStart=/opt/wiimotebridgekicker.sh
Restart=no
Type=simple

[Install]
WantedBy=multi-user.target

で、「systemctl enable wiimotebridge」で有効化な感じで

さいごに

  • 無事実装できた!
  • 軽い改造で、ボタンの同時押しや、マクロ的なのも実装できるかと思う感じ
  • systemdに仕込むことで、ブリッジ専用ラズパイが作りやすいかなと

なお、相手はPCじゃなくてもいい感じです
例えば、以下URLに、「ポッ拳用HORIゲームパッド」のdescriptionがあるので、これを使ってやれば、Nintendo SwitchなんかにWiiリモコンつなぐこともできるかもね
https://github.com/progmem/Switch-Fightstick/blob/master/HORI_Descriptors

以上!

8
13
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?