LoginSignup
3
2

More than 1 year has passed since last update.

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

Posted at

はじめに

PixelWatchで心拍数をVRChatに送りたいなーと漠然と思っていたときに
ドンピシャな記事(Pixel Watchで取得した心拍数をリアルタイムで配信に表示する方法)が掲載されたのでやってみた

今回はHeartRateOnStream for OBS +αを利用して心拍数を持ち込みます
なお、ソースコード等はgithubで公開しています

準備

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

環境

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

利用アプリ

  • 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とアプリ側それぞれ設定を行います
基本的には参考元記事と同じ作業をするのでそちらを参考にすると良いかも

アプリ側設定

まずgoogleplayからアプリをスマートフォンとPixelwatchにダウンロードします
Screenshot_20221109-163956~2.png
ダウンロード後、スマートフォン側のアプリを開いて"Edit"を選択します
設定画面を開いたら、
"obs-websocket v5+"を選択後にIPアドレスの欄に接続するPCのIPアドレスを入力します
その他の欄はデフォルトのままにします
Screenshot_20221109-164853_1.png
設定が完了したら、右上の"SAVE"で保存してこの画面を抜けます

PC側設定

おそらくファイアーウォールが原因でこのままでは接続できないので、設定を行います
windowsの検索窓に"ファイアーウォール"と打ち込むと"ファイアーウォールの状態の確認が出るのでこれをクリックします
d44726ea5f485e815c05e37ed4968196.png
開いたら左側にある詳細設定を選択します
選択後、新しいウインドウが開いたら左側の"受信の規則"→右側の"新しい規則"を選択します
3c1b3a9e946960af7d12251c71c5f1fe.png
規則の種類をポートに、TCPで特定のポートに"4455"を指定
後はそのまま次へを押し、最後の名前を自分がわかりすい名前にします
設定後、受信の規則一覧に設定した名前が出ればおkです

疎通確認

ここでは疎通確認にOBSを利用します
OBSを起動し、上メニューから"ツール"→"obs-websocket設定"を選択します
ここでは、元記事と違い"認証を有効にする"のチェックを外します
415231e69dfe4ac45fceeb715d9376d9.png
この状態で、スマートフォンアプリ側の"Connect"を押して接続されれば疎通確認はおkです

プロジェクト設定

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

次にbuild.gradle.ktsのdependenciesに以下を追記します

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送信を行います

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を実装します

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

また、VRC側ではOSCHeartRate_AvatarSampleというワールドに設置されているアバターで確認することができます

解説

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

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

さいごに

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

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

参考

3
2
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
3
2