SwitchのPro Controllerに連射機能をつけてみた

はじめに

最近スプラトゥーン2をやってる。
スプラトゥーンにおいて連射機能は必須ではないけど、特定の場面で有利に働くため、プロコンには搭載していない「連射」をしたいと思っている。
それについて最近調べたこと、できたことを書く。

連射機能を実現するには

純正品であるプロコンには搭載されてはいないけど、サードパーティ製の製品には連射機能つきが販売している。
お金を払えば連射はできる。でもサードパーティ製には、普段使っているプロコンとボタンの押し心地と異なったり、独自仕様があって、使っていて違和感がある。僕は慣れるまでのストレスを受け入れたくないと思ったので、プロコンっぽいテイストの中華製コントローラーを買ってみた。非常にプロコンっぽくて最初は満足していたんだけど、品質に問題があって使うのをやめた。

youtubeにプロコンに連射機能をつけた、という動画を見つけてこれじゃん、と思って動画と同じ構成で作ってみた。
https://www.youtube.com/watch?v=N1dPFDSxaQ4

とはいえ、Raspberry Pi を使ったところでデメリットは見えていて、漠然と運用コストが発生するだろうなとは思っていた。(具体的には後述する)

やりかた

Raspberry Piのセットアップ方法は https://mtosak-tech.hatenablog.jp/entry/2020/08/22/114622 に書いている。 簡単にRaspberry Piを使った「連射」の仕組みを説明すると、swtichとプロコンの間に Raspberry Pi をUSBで接続し、プロコンの入力をRaspberry Piで仲介してswtichに出力する。仲介するときに特定のキー入力を検出して、書き換えると連射はできた。

仲介するコードは、 https://mzyy94.com/blog/2020/03/20/nintendo-switch-pro-controller-usb-gadget/ にある。 本家はpythonで書かれていたけど、僕はrubyに慣れているのでrubyで書き直したけど、USBコントローラーとして認識されにくく、かつパフォーマンスが悪くてプロコンからの入力が遅延した。 ボトルネックは調べていないけど、 IO::EAGAINWaitReadableがかなりの頻度で発生するのでここが遅いのかなと思ったが、関係ない気がしてきた。わからん。
しぶしぶpythonで動かすことにした。

STDOUT.sync = false
Thread.abort_on_exception = true

class RappidFireProCon
  class ProConRejected < StandardError; end

  PROCON_PATH = "/dev/hidraw0"

  def initialize
    connect

    unless File.exist?(PROCON_PATH)
      puts "プロコンをラズベイに挿してください"
      loop do
        break if File.exist?(PROCON_PATH)
        sleep(1)
      end
    end

    @gadget = File.open('/dev/hidg0', "w+")
    @procon = File.open(PROCON_PATH, "w+")
  end

  def run_loop
    Thread.new do
      loop do
        input = @gadget.read_nonblock(128)
        inspect(input) { |value| puts ">>> #{value}" }
        @procon.write_nonblock(input)
      rescue IO::EAGAINWaitReadable
        # puts ">>> no-op"
      rescue Errno::EIO, Errno::ENODEV, Errno::EPROTO, IOError => e
        raise ProConRejected.new(e)
      end
    end

    Thread.new do
      loop do
        output = @procon.read_nonblock(128)
        inspect(output) { |value| puts "<<< #{value}" }
        @gadget.write_nonblock(output)
      rescue IO::EAGAINWaitReadable
        # puts "<<< no-op"
      rescue Errno::EIO, Errno::ENODEV, Errno::EPROTO, IOError => e
        raise ProConRejected.new(e)
      end
    end

    loop { sleep(5) }
  ensure
    @gadget&.close
    @procon&.close
  end

  private

  def inspect(value)
    yield value.bytes.map{|x| x.to_s(16).rjust(2, "0") }.join
  end

  def connect
    system('echo > /sys/kernel/config/usb_gadget/procon/UDC')
    system('ls /sys/class/udc > /sys/kernel/config/usb_gadget/procon/UDC')

    sleep(2)
  end
end

loop do
  runner = RappidFireProCon.new
  runner.run_loop
rescue RappidFireProCon::ProConRejected => e
  puts e
  puts "------------"
  puts 'プロコンが外されました'
  retry
end

Raspberry Piで連射機能を実現した結果

体感でわかる遅延はないように思った。十分使える。

が、前述した運用コストや自動化が難しい手動なオペレーションがいくつか存在することがわかった。
具体的には、プログラムを停止するとプロコンがbluetoothでswitchと接続してしまうので、Raspberry Piと再度USB接続するには、bluetoothでの接続を切断する必要がある。これを自動化したい、いまは、別のswtichに繋げて切断しているが、もしかしたら、bluetooth経由でRaspberry Piに接続できると接続の問題が解決するかもしれない。

プログラムの例外処理がまだ甘いので、起動時にはsshしてログを見ながら状態の確認をしつつ、自動化できそうな部分は都度手を入れている。
プロコンの入力は、秒間20回らしいので、これに合わせてsleepを入れるなど、リソースを節約をする処理を入れるともっと安定するように思う。

まとめ

Raspberry Piとプロコンに連射機能を搭載することができた。
自分でプログラムを書く必要がある。コントローラーの仕様をちょっと調べる必要がある。接続処理は不安定。まだまだ改良が必要。