Pixel Watchでも心拍数をVRChatへ送りたい [Kotlin]

# はじめに
PixelWatchで心拍数をVRChatに送りたいなーと漠然と思っていたときに
ドンピシャな記事([Pixel Watchで取得した心拍数をリアルタイムで配信に表示する方法](https://pc.watch.impress.co.jp/docs/column/yajiuma-mini-review/1450792.html))が掲載されたのでやってみた

今回はHeartRateOnStream for OBS +αを利用して心拍数を持ち込みます
なお、ソースコード等は[github](https://github.com/hennano/HeartRateTest/tree/master)で公開しています

# 準備
実際のコードだけ見たい人は次の章まで飛ばしてください
ここでは各環境やアプリの設定等を取り扱います

## 環境
本記事での開発環境は以下のとおりです

利用アプリ
- HeartRateOnStream for OBS

必須じゃないけどあると便利なもの(疎通確認用)
- OBS
- wscat


 開発環境
- Kotlin 1.6.10
- Java11(corretto-11.0.14.1)
- Gradle
- javaOSC 0.8
- tyrus 2.1.1
InteliJを用いて進めていきます

## 疎通設定
HeartRateOnStream for OBSが通信できるようにPCとアプリ側それぞれ設定を行います
基本的には[参考元記事](https://pc.watch.impress.co.jp/docs/column/yajiuma-mini-review/1450792.html)と同じ作業をするのでそちらを参考にすると良いかも

### アプリ側設定
まずgoogleplayからアプリをスマートフォンとPixelwatchにダウンロードします
![Screenshot_20221109-163956~2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/559827/5c10bd2b-8c77-c1fc-d308-49dc5f91ef4f.png)
ダウンロード後、スマートフォン側のアプリを開いて"Edit"を選択します
設定画面を開いたら、
"obs-websocket v5+"を選択後にIPアドレスの欄に接続するPCのIPアドレスを入力します
その他の欄はデフォルトのままにします
![Screenshot_20221109-164853_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/559827/c62ded1b-80a9-dfed-a41c-7bc4b254b966.png)
設定が完了したら、右上の"SAVE"で保存してこの画面を抜けます
### PC側設定
おそらくファイアーウォールが原因でこのままでは接続できないので、設定を行います
windowsの検索窓に"ファイアーウォール"と打ち込むと"ファイアーウォールの状態の確認が出るのでこれをクリックします
![d44726ea5f485e815c05e37ed4968196.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/559827/9f74a493-b09b-91d2-37e1-cbed10f0ec32.png)
開いたら左側にある詳細設定を選択します
選択後、新しいウインドウが開いたら左側の"受信の規則"→右側の"新しい規則"を選択します
![3c1b3a9e946960af7d12251c71c5f1fe.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/559827/b2ab0d25-d6bf-4155-f947-f5c2e8b9ca9f.png)
規則の種類をポートに、TCPで特定のポートに"4455"を指定
後はそのまま次へを押し、最後の名前を自分がわかりすい名前にします
設定後、受信の規則一覧に設定した名前が出ればおｋです

### 疎通確認
ここでは疎通確認にOBSを利用します
OBSを起動し、上メニューから"ツール"→"obs-websocket設定"を選択します
ここでは、元記事と違い"認証を有効にする"のチェックを外します
![415231e69dfe4ac45fceeb715d9376d9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/559827/1d12ecac-5b05-509d-a943-d12079a2ab37.png)
この状態で、スマートフォンアプリ側の"Connect"を押して接続されれば疎通確認はおｋです

## プロジェクト設定
InteliJにてKotlinプロジェクトを作成します

次にbuild.gradle.ktsのdependenciesに以下を追記します
```gradle:gradle:buile.gradle.kts dependencies
    implementation("com.illposed.osc:javaosc-core:0.8")
    implementation("org.glassfish.tyrus:tyrus-server:2.1.1")
    implementation("org.glassfish.tyrus:tyrus-container-grizzly-server:2.1.1")
```

# プログラム
## 実装

websocket通信を行うサーバーエンドポイントを作成し、その上でOSC送信を行います
```Kotlin:ServerEndPoint.kt
package endpoint

import com.illposed.osc.OSCMessage
import com.illposed.osc.transport.OSCPortOut
import jakarta.websocket.*
import jakarta.websocket.server.ServerEndpoint
import java.net.InetAddress
import java.net.InetSocketAddress
import java.util.concurrent.CopyOnWriteArraySet

@ServerEndpoint("/")
class ServerEndPoint {

    private val sendPort = 9000
    private val ip = InetAddress.getLoopbackAddress()
    private val sender = OSCPortOut(InetSocketAddress(ip, sendPort))

    private val address = "/avatar/parameters/HeartRateFloat01"

    private val openResponse = "{\"d\":{\"obsWebSocketVersion\":\"5.0.1\",\"rpcVersion\":1},\"op\":0}"
    private val handShakeRequest = "{\"d\":{\"eventSubscriptions\":0,\"rpcVersion\":1},\"op\":1}"
    private val handShakeResponse = "{\"d\":{\"negotiatedRpcVersion\":1},\"op\":2}"

    companion object {
        private val sessions = CopyOnWriteArraySet<Session>()
    }

    @OnOpen
    fun onOpen(session: Session){
        session.basicRemote.sendText(openResponse)
        sessions.add(session)
    }

    @OnMessage
    fun onMessage(message: String, session: Session){
        if(message == handShakeRequest){
            session.basicRemote.sendText(handShakeResponse)
            return
        }

        val heartRate = extractHeatRate(message) ?: return
        val oscMessage = OSCMessage(address, mutableListOf((heartRate / 255.0f).coerceAtMost(1.0f)))
        sender.send(oscMessage)
    }


    @OnClose
    fun onClose(session: Session) {
        sessions.remove(session)
    }

    @OnError
    fun onError(session: Session, t: Throwable) {
    }

    private fun extractHeatRate(message: String): Int? {
        val regex = Regex("\"text\":\"[0-9]{1,3}\"")
        val param = regex.find(message)
        val data = param?.value?.replace("\"", "")?.split(":") ?: return null
        if (data.size != 2) return null
        return data[1].toInt()
    }
}

```
あとは、発火地点のMain.ktを実装します
```Kotlin:Main.kt
import endpoint.ServerEndPoint
import org.glassfish.tyrus.server.Server
import java.lang.Thread.sleep

fun main(args: Array<String>) {

    val hostname = "localhost"
    val port = 4455

    val server = Server(hostname, port, "/", mapOf(), ServerEndPoint::class.java)
    server.start()
    sleep(1000000)
    server.start()
}
```

## 実行
PixelWatchとスマートフォンそれぞれでアプリを起動し、PixelWatch側で再生ボタンを押すと心拍数がスマートフォン側に表示されます
この状態で作成したツールを起動し、最後にスマートフォンからconnectを押すと接続されます
ログに心拍数が表示されたら成功です
![94aeb5d5ba7d0339a206ef89fd7b4d5a.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/559827/9702f287-ae9e-9c01-fef0-a5b2a52e5ff4.png)



また、VRC側では[OSCHeartRate_AvatarSample](https://vrchat.com/home/world/wrld_eea39796-5836-4099-98da-4815e16696bd)というワールドに設置されているアバターで確認することができます

https://twitter.com/SCPScTi/status/1587037280455122945?s=20&t=j_r6sRysbS05UqTEHujGKA

## 解説

解説という解説を行うところはないんですが、使用アプリケーション独自の仕様によるものだけ解説します
HeartRateOnStream for OBSでは、心拍数の送信を行う前にOBSバージョンの取得等の通信を行っているようです。

まず、最初の接続後にアプリ側はサーバからOBSバージョンの関する応答を待ちます
それが送られてきたら、次にアプリ側からイベントサブスクライブ要求を行います
最後にサーバ側から応答を返すことで接続処理が完了されます
これらの処理を行った後、心拍数の送信が行われるようです
そのため、サーバ側で一連の処理を模倣する必要があります

# さいごに
HeartRateOnStream for OBSはPixelWatch以外にも様々なスマートウォッチに対応しているようで、普段使用しているスマートウォッチをそのまま心拍数取得ツールとして利用できそうです
夢が広がりますね！！

余談ですが、私は心拍数をしっぽの揺れに連携させてみました
なんか意外と好評みたい

https://twitter.com/SCPScTi/status/1589913105668403201?s=20&t=j_r6sRysbS05UqTEHujGKA

# 参考
- [Pixel Watchで取得した心拍数をリアルタイムで配信に表示する方法](https://pc.watch.impress.co.jp/docs/column/yajiuma-mini-review/1450792.html)
- [javaOSCでVRCのOSC送受信](https://qiita.com/ScTi/items/93ebb5215279ab2631aa)
- [WebSocket(Tyrus) + libGDX + KotlinでChatアプリを作る](https://qiita.com/yyYank/items/627d45bbaa173fcd771c)
- [obs-websocket 5.0.1 Protocol](https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md)