raspberry pi pico wからOSCを使ってVRChatの操作をした話

## はじめに
この記事では、raspberry pi pico wを使って、VRChatのアバターのパラメータを操作しようとした時の実装などを書いていきたいと思います。
ESP32でOSC通信をする記事などはありましたが、raspberrypi pico(microPython)での実装というのは自分が調べた限り見つからなかったので、この機会に共有してみたいと思い記事を作成してみました。

### おことわり
この記事ではraspberrypi pico wの初期設定などの方法は取り扱いません。
開発環境としては、Thonnyを使っています。

## きっかけ
raspberry pi pico wを買ったはいいものの、使い道を思い浮かべられずにいたところ、そういえばVRChatのアバターってOSCを使って操作できたな、と思いOSC送信用のコントローラーを作ってみよう。となったのがきっかけです。

## 実装
### ChatGPTに聞いてみる
とはいってもOSCをraspberrypi pico wでOSC通信を実現する方法が分からなかったので、ChatGPTに聞いてみました。
その結果、以下のようなサンプルを提示してもらいました。
```Python
import usocket as socket
import struct

# OSCメッセージの生成
def create_osc_message(address, *args):
    # OSCアドレスのパディング
    def pad_string(data):
        padding = (4 - len(data) % 4) % 4
        return data.encode('utf-8') + b'\0' * padding
    
    # OSCの引数型を決定するタグ
    types = "," + "".join('i' if isinstance(arg, int) else 'f' if isinstance(arg, float) else 's' for arg in args)
    
    # メッセージを組み立てる
    message = pad_string(address) + pad_string(types)
    
    # 引数をパック
    for arg in args:
        if isinstance(arg, int):
            message += struct.pack('>i', arg)
        elif isinstance(arg, float):
            message += struct.pack('>f', arg)
        elif isinstance(arg, str):
            message += pad_string(arg)
    
    return message

# OSCメッセージを送信する関数
def send_osc_message(ip, port, address, *args):
    osc_message = create_osc_message(address, *args)
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)  # UDP
    sock.sendto(osc_message, (ip, port))
    sock.close()

# 実際にOSCメッセージを送信する
ip = '192.168.0.10'  # 送信先IP
port = 9000  # 送信先ポート
address = '/example/osc'  # OSCアドレスパス

send_osc_message(ip, port, address, 123, 45.67, "Hello OSC")  # 引数を整数、浮動小数点、文字列として送信
```
どうやら、キモとなりそうなのは、```send_osc_message()```のところのようです。ソケット通信を使えばよいらしいとのこと。
ちなみに、PythonではOSC通信をするためのライブラリが存在しているようですが、microPythonではそういったライブラリは使えないようなので、このような実装で頑張るしかなさそうです。

### 試しに動かしてみる

#### アバター側の設定
今回は試しにアバターのアクセサリのON/OFFを切り替えるプログラムを作成してみようと思います。
そもそもVRChatでどうOSCを取り扱うのか、といったところについては本記事では省略します。調べると様々な記事が出るのでそちらを参考にしてください。下記の記事などが参考になります。

https://note.com/33linn/n/n2f60b5155ee3

ここでは、アバターごとに作成されるjsonファイル内にパラメータが記載されているので、そこに対してOSC通信をしていく、くらいの認識で問題ないです。

今回使うのはこのアバター。ツクルノモリさんから出ている猫山宙ちゃん。お気に入りです。

https://tsukurunomori.co.jp/3d-model/nekoyamasora

このアバターのメガネをオンオフしてみようと思います。

対象のjsonファイルを探して、メガネの設定をしているところを探すと、以下のような記述がありました。
```json
    {
      "name": "Glasses",
      "input": {
        "address": "/avatar/parameters/Glasses",
        "type": "Bool"
      },
      "output": {
        "address": "/avatar/parameters/Glasses",
        "type": "Bool"
      }
    }
```
この```"/avatar/parameters/Glasses"```を送信先のアドレスに設定してあげればよさそうですね。

#### raspberry pi pico w側の実装
次は、raspberry pi pico w側の配線を考えていきます。
今回はON/OFFの切り替えのために、シンプルにスイッチをつないで実装します。
たまたま家に転がっていたこのマイクロスイッチを使ってみます。
（三和のボタンでも、よくあるタクトスイッチでも大丈夫です。）

https://akizukidenshi.com/catalog/g/g114659/

このスイッチにケーブルをつないで、ブレッドボードに配線をしていきます。
今回は、3V3OUTに近かったのでGPIOの28番を使うことにします。
配線図は以下の通り。（マイクロスイッチの右端の端子は今回は使わないです。）
![Untitled Sketch_ブレッドボード2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264970/30dca710-f115-4ce3-b899-8b38edb644c7.png)


#### microPythonの実装
ここまでくればあともう少しです。最後は、プログラム部分を完成させていきましょう。
上記のChatGPTが出してくれたプログラムに手を加えていきます。

##### ネットワークの設定
まずは、このOSCメッセージを送信するためのネットワーク周りの実装をしていきましょう。
raspberry pi pico wはWi-Fiに接続できるので、無線LANを使ってVRChatを実行しているPCと同じネットワークにつないで送れるようにします。
Wi-Fiにつなげるために以下をインポートします。
```python
import network
```
そして、以下のように書いていきます。
```python
ssid = 'YOUR_SSID' # 使っている無線LANのSSID
password = 'PASSWORD' # SSIDのパスワード

wlan = network.WLAN(network.STA_IF)
wlan.active(True)
wlan.connect(ssid, password)
```
このように記載することで無線LANに接続できるようになりました。
##### GPIOの設定
次はGPIOの設定をしていきます。
上でGPIOの28番を使うようにしたので、28番をインプット用の端子として使うようプログラムに記載していきます。
以下を追記しましょう。
```python
from machine import Pin
```
```Python
sw = Pin(28, Pin.IN, Pin.PULL_DOWN )
```
```28```はGPIOの番号、```Pin.IN```はインプット用として使うこと、```Pin.PULL_DOWN```はプルダウン抵抗を使う、という意味です。プルダウン抵抗って何ぞや？と思われるかもしれませんが、ここではおまじないとして書いておいてください。一応補足しておくと、電流が流れていないとき（＝スイッチがオフの時）、電流が不安定にならないようにするらしいです。

##### OSCメッセージ送信部の設定
次はメッセージ本体を送るための設定です。
さらに以下を追記していきます。
先ほどのjsonファイルには次のような記述がありました。
```"type": "Bool"```
VRChatでのOSCの```Bool```型は```Int```型で送っても解釈してくれるようなので、以下のように書くことができます。
```python
while True:
    if (sw.value() == 1) :
        send_osc_message(ip, port, address, 1)
        print("1")
    else:
        send_osc_message(ip, port, address, 0)
        print("0")
```
無限ループを使って、ボタンが押されている間はOSCのメッセージとして1を、押されていない間は0を送信するようにします。念のため、実行時に端末からどちらを送っているかを見られるようにします。

##### 送信先設定
最後に送信先の設定をしましょう。ChatGPTが書いてくれたプログラムを修正していきます。
具体的には以下のところです。
```python
# 実際にOSCメッセージを送信する
ip = '192.168.0.10'  # 送信先IP
port = 9000  # 送信先ポート
address = '/example/osc'  # OSCアドレスパス

send_osc_message(ip, port, address, 123, 45.67, "Hello OSC")  # 引数を整数、浮動小数点、文字列として送信
```
ここの```ip```のところVRChatを起動しているPCのIPアドレスに変更します。コマンドプロンプトからipconfigコマンドを実行すればわかると思います。
さらに```address```のところを送信したいパラメータに対応したアドレス、今回だと```"/avatar/parameters/Glasses"```に変更します。
```send_osc_message()```は使わないので削除してしまいましょう。

ここまでの処理を行うと以下のようになっていると思います。
```python: VRC_OSC_switch.py
import usocket as socket
import struct
import network
from machine import Pin


ssid = 'YOUR_SSID' # 使っている無線LANのSSID
password = 'PASSWORD' # SSIDのパスワード

wlan = network.WLAN(network.STA_IF)
wlan.active(True)
wlan.connect(ssid, password)

# OSCメッセージの生成
def create_osc_message(address, *args):
    # OSCアドレスのパディング
    def pad_string(data):
        padding = (4 - len(data) % 4) % 4
        return data.encode('utf-8') + b'\0' * padding
    
    # OSCの引数型を決定するタグ
    types = "," + "".join('i' if isinstance(arg, int) else 'f' if isinstance(arg, float) else 's' for arg in args)
    
    # メッセージを組み立てる
    message = pad_string(address) + pad_string(types)
    
    # 引数をパック
    for arg in args:
        if isinstance(arg, int):
            message += struct.pack('>i', arg)
        elif isinstance(arg, float):
            message += struct.pack('>f', arg)
        elif isinstance(arg, str):
            message += pad_string(arg)
    
    return message

# OSCメッセージを送信する関数
def send_osc_message(ip, port, address, *args):
    osc_message = create_osc_message(address, *args)
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)  # UDP
    sock.sendto(osc_message, (ip, port))
    sock.close()


# 実際にOSCメッセージを送信する
ip = '192.168.1.1'  # 送信先IP
port = 9000  # 送信先ポート
address = '/avatar/parameters/Glasses'  # OSCアドレスパス

sw = Pin(28, Pin.IN, Pin.PULL_DOWN )
while True:
    if (sw.value() == 1) :
        send_osc_message(ip, port, address, 1)
        print("1")
    else:
        send_osc_message(ip, port, address, 0)
        print("0")
```

### 実際に動かす
それでは、実際に動かしてみましょう。
<iframe width="560" height="315" src="https://www.youtube.com/embed/mNPVrR1qJkA?si=zO8eBD2vFgkgQ0BB" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

いかがでしょうか。スイッチを押すと同時にメガネが外れ、スイッチを離すとまたメガネが装着されます。
また、端末への出力も0と1が出力され、スイッチの状態と対応しているのが分かります。

### 最後に
ここまで読んでいただき、ありがとうございます。Qiitaに記事を投稿するのは初めてだったので、読みにくいところがあったりしたら申し訳ありません。

実装については、メガネのようなアクセサリーなら押すたびにON/OFFを切り替える方式のほうがいいかも、と思いながら書いていましたが、まずはシンプルな実装を、というところで記事を書いてみました。手で実装したところについても、全然スマートではないなと思っているので、改良案などあればぜひコメントください。

また、実は最初実装しようとしていたのはまた別の機構を用いるものだったので、それについてもまた機会があれば記事を作成してみたいと思います。

改めて、ここまで読んでいただきありがとうございました。