XBox ワイヤレスコントローラーを起動したのに合わせてPCを起動させたい

Nintendo Switchやプレイステーションでは、コントローラーからゲーム機のコンソールを起動することができて便利です。
これと同じようなことをパソコンでもできないかと思って実践したことを記したいと思います。
なお、私はプログラムに関しては素人なので、汚いソースコートであることはご容赦ください。

# 準備するもの
* Raspberry Pi Pico W (以後「ラズパイ」と表記)
* Xbox ワイヤレスコントローラー (以後「コントローラー」と表記)

# 前提
* ラズパイが接続するWi-Fiネットワークと、起動させたいPCが同じネットワークにいること
* PCがWake on LAN(WoL)で起動できるような設定になっていること
* ラズパイの操作方法を知っていること
* IDEはThonnyを使用する

# 方針
1. コントローラーを起動する
1. ラズパイでコントローラーの起動を検知する
1. ラズパイからPCに向けてWoLを実行する

# ソースコード
https://github.com/tachiky/ControllerBoot

先にソースに関する説明ですが、main.pyについて、
Wi-Fiに接続するためのSSIDとパスワード情報を`SSID`と`PW`に入力してください。
コントローラーのMACアドレスを`XBOX_MACADDR`に入力する必要がありますが、このアドレスについては後述します。
マジックパケットを送信する対象のPCのIPアドレスを`PC_IPADDR`に、MACアドレスを`PC_MACADDR`に入力してください。
なおPCのMACアドレスは16進数の数値を配列形式で入力してください。

## 引用
ソースのblegamepad.pyは、以下の動画と、その概要に記載されていたGoogleドライブサイトにある同名のPythonコードを参考にしています
[#22 ゲームパッドでRaspberry Pi PicoWを操作しよう～DHAの電子工作教室～【Raspberry Pi PicoW】](https://www.youtube.com/watch?v=WHLCnej2iQo&list=LL)
https://drive.google.com/drive/folders/1h_aNwIv26q8gpyyus-07lHX0YHY2T57M

また、uping.pyについて、本プログラムではpingを送信する必要があるのですがラズパイで使うMicroPythonはpingを実行することができないので、こちらのµPing(uping.py)を使用しています。
https://gist.github.com/shawwwn/91cc8979e33e82af6d99ec34c38195fb

# コントローラーを検知する方法について
まず前提として、コントローラーはすでにPCとBluetoothのペアリングが完了している状態とします。
またコントローラーは複数台の(Xbox以外の)機器とのマルチペアリングを組むことはできません。
そのためラズパイとペアリングを組むことはできないのですが、コントローラーはペアリング相手を探している間は常にアドバタイズを行っています。
このプログラムでは、コントローラーがペアリング相手を探している間に発するアドバタイズパケットを拾い、そのパケットが目的のコントローラーから発しているものであるかどうかを判断して後続処理を行うような仕組みとしています。

## コントローラーのMACアドレスを調べる
まずは、プログラムをラズパイに投入します。
その際にですが、最初はコントローラーのMACアドレスを調べたいので、blegamepad.pyのソースコードについて、以下のコメントアウトを外してください。
```diff_python:blegamepad.py
    # interrupt function
    def _irq(self, event, data):
        # check event
        if(event == _IRQ_SCAN_RESULT):
            # get scan result            
            (addr_type, addr, adv_type, rssi, adv_data) = data # get detected device data
            mac = decode_addr(list(bytes(addr)))
-           #print( 'FOUND DEVICE! : TYPE={}\tADDR={}\tNAME={}'.format(addr_type, mac, decode_name(adv_data)) )
+           print( 'FOUND DEVICE! : TYPE={}\tADDR={}\tNAME={}'.format(addr_type, mac, decode_name(adv_data)) )
            # device check
            if(mac == self._controller_mac):
                #print( 'FOUND DEVICE! : TYPE={}\tADDR={}\tNAME={}'.format(addr_type, mac, decode_name(adv_data)) )
                self._is_find_controller = True
```
その状態でmain.pyを実行すると、アドバタイズをしている端末のスキャンを行い、その結果を以下のようにThonnyのシェル上に表示してくれます。
![スクリーンショット 2024-10-06 094931.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3833373/97ad6303-17fa-5ceb-da0b-ca40a1ca7808.png)
これを利用し、「コントローラーを起動し、ペアリングをさせない状態にしてスキャンした結果」と「コントローラーを停止した状態でスキャンした結果」をそれぞれ取得し、その結果を比較して「コントローラーのMACアドレス」を導き出します。
ちなみに私の所有するコントローラーのMACアドレスは`40:8E:2C:79:CD:29`でした。
ベンダー検索サイトでMACアドレス検索すると「Microsoft Corporation」と表示されるのではないかと思います。

コントローラーのMACアドレスが特定出来たら、main.pyの`XBOX_MACADDR`変数に入力してください。
また、必須ではないですが先ほどblegamepad.pyでアンコメントした個所を再びコメントアウトしてください。

# 処理の流れについて
## 1. Wi-Fi接続
main.pyに記載したSSIDとパスワード情報をもとにWi-Fi接続を行います。(connect_wifi関数)
Wi-Fi接続中はLEDが**点灯**します。

## 2. コントローラー検索
`pad.scan()`を実行し、アドバタイズパケットをスキャンします。(63-73行目)
目的(`XBOX_MACADDR`)のMACアドレスが検出されるまでループします。

## 3. WoLのためのマジックパケット送信
コントローラーが検出されたら、PC(`PC_MACADDR`)にWoL用のマジックパケットを送信します。(send_magicpacket関数)
その後、10秒ごとにpingでPC(`PC_IPADDR`)に対し疎通確認を行い、応答が返ってこなければ再度マジックパケットの送信を行う、というループを行います。(80-98行目)
この間、LEDは**1秒周期で点滅**します

## 4. PCの終了監視
PCの起動が完了したら、10秒周期でpingによる疎通監視を行います。(103-112行目)
この間、LEDは**点灯**します

## 5. PC終了後のコントローラー検出猶予期間
ping応答がなくなってから指定時間(`GRACETIME`)の間はコントローラーの再スキャンを行いません。(116-123行目)
→PCをシャットダウンするときにコントローラーの電源を落とし忘れてしまい、またWoLが実行されるのを防ぐため
この間、LEDは**10秒ごとに点滅**します

## 6. 待機状態へ復帰
猶予期間が終了すると、再びコントローラーの検索フェーズ(ループの先頭)に戻ります。

# 最後に
Raspberry Piの登場で、かゆいけど手が届かなかったところにだんだん手を伸ばせるようになってきたと実感しています。
今後もなにか自分にとって面白いものが作れたら報告できればいいなと思います。