スマートウォッチで心拍数をアバターに反映させる
https://scrapbox.io/files/621b49039fae07002070406a.png
こんな感じで、リアルアバターの心拍をアバターに表示させる。
スマートウォッチはFitBit Versa3を使う。
アバターはmio3io氏の薄荷ちゃんhttps://mio3works.booth.pm/items/2215270 を使わせて頂きます。
概要
FitBit(Versa3)→Bluetooth→スマートフォン→Websocket(Json)→Raspberry Pi→OSC(Float)→PC(VRChat)
という流れ。
FitBitはスマートフォンアプリとBluetoothで連携しているので、通信はスマートフォンを介す必要がある。
また今回は、赤外線トランスミッタや温度センサ等、他のハードウェアを付けて拡張するためにWebsocketサーバをRaspberry Pi上で動かしているが、PC上で建てても問題ない。
以降、スマートウォッチの方を「デバイス」、スマートフォンの方を「モバイル」と呼ぶことがあります。
使用言語
FitBitアプリはJavascript、Raspberry Pi上のWebsocketサーバとOSCサーバはPythonで動かす。
IPとかポートとか
Websocket: 5555
OSC(送信):9000
Raspberry Pi : 192.168.1.197
PC : 192.168.1.161
①FitBit用アプリを作る
https://studio.fitbit.com/ にてアプリを作れる。ログインが必要。
https://scrapbox.io/files/621b498b14ffe8001d83eccb.png
TemplateからStarterを選択する。なお古いデバイスではSDKが異なる場合があるので、その場合はStarter(4.3)を選択する。
https://scrapbox.io/files/621b494b2f2e3b002001d437.png
app/index.js
デバイス上で動くアプリ
companion/index.js
モバイル上で動くアプリ
resources/*
デバイス上の表示に関する設定
以下のコードを書く
code:app/index.js
/*
fitbitデバイスで動作する。
*/
import * as messaging from "messaging";
import { HeartRateSensor } from "heart-rate";
import { me } from "appbit";
import * as document from "document"
//画面が消えても動くようにする
me.appTimeoutEnabled = false;
const hrm = new HeartRateSensor({ frequency: 1, batch: 5 });
// fitbit appとの通信を開始する。
messaging.peerSocket.addEventListener("open", (evt) => {
start_heartRateSensor();
});
messaging.peerSocket.addEventListener("error", (err) => {
console.error(Connection error: ${err.code} - ${err.message});
hrm.stop();
});
messaging.peerSocket.addEventListener("close",(evt) => {
console.log('App Closed! stopping heart sensor...');
hrm.stop();
});
// fitbit appと接続後に、心拍データの取得を行う。
function start_heartRateSensor(){
hrm.addEventListener("reading", () => {
const heartElement = document.getElementById("heart");
heartElement.text = hrm.heartRate;
console.log(Current heart rate: ${hrm.heartRate});
sendMessage({
heartRate: hrm.heartRate ? hrm.heartRate : 0
});
});
hrm.start();
}
// fitbit appに心拍データを送信する。
function sendMessage(data) {
if (messaging.peerSocket.readyState === messaging.peerSocket.OPEN) {
messaging.peerSocket.send(data);
}
}
code:companion/index.js
/*
スマートフォンのfitbit app内で動作する。
*/
import * as messaging from "messaging";
// websocketの接続先
const wsUri = "ws://192.168.1.197:5555/";//Raspberry PiのIP。5555はポート番号。
const websocket = new WebSocket(wsUri);
// それぞれのリスナを設定する。
websocket.addEventListener("open", onOpen);
websocket.addEventListener("close", onClose);
websocket.addEventListener("message", onMessage);
websocket.addEventListener("error", onError);
function onOpen(evt) {
console.log("CONNECTED");
// websocket.send("connected")
}
function onClose(evt) {
console.log("DISCONNECTED");
}
function onMessage(evt) {
console.log(from websocket server: ${evt.data});
}
function onError(evt) {
console.error(ERROR: ${evt.data});
}
// デバイスからのデータを受信したときに、同データをwebsocketサーバに送信する。
messaging.peerSocket.addEventListener("message", (evt) => {
if(websocket.readyState===WebSocket.OPEN){
const data = JSON.stringify(evt.data);
websocket.send(data);
}
});
code:resource/index.view
<svg class="background">
<!-- <text id="demotext" x="50%" y="50%+15">Hello World!</text> -->
<text id="heart" x="50%" y="50%">Heart</text>
</svg>
②Websocket/OSC サーバーをRaspberry Pi上で作る
pythonOSCとwebsocket_serverをインストールします。
code:bash
$ pip install python-osc
$ pip install git+https://github.com/Pithikos/python-websocket-server
ラズパイ側で動かすpythonソースコード
code:ws_server.py
from websocket_server import WebsocketServer
import logging
import argparse
from pythonosc import osc_message_builder
from pythonosc import udp_client
import json
class sendOsc():
def __init__(self,sendtohost,sendtoport):
self.parser = argparse.ArgumentParser()
self.parser.add_argument("--ip",default=sendtohost,help="The ip to listen on")
self.parser.add_argument("--port",type=int,default=sendtoport,help="The port to listen on")
self.args = self.parser.parse_args()
self.client = udp_client.UDPClient(self.args.ip,self.args.port)
def send(self,address,val):
val = json.loads(val)#convert to dict
val = val'heartRate'
val = (val-60)/100#Float 0.00~1.00に変換
msg = osc_message_builder.OscMessageBuilder(address=address)
msg.add_arg(val)
msg = msg.build()
self.client.send(msg)
class Websocket_Server():
def __init__(self, host, port):
self.server = WebsocketServer(host=host, port=port, loglevel=logging.DEBUG)
self.osc = sendOsc("192.168.1.161",9000)
# クライアント接続時に呼ばれる関数
def new_client(self, client, server):
print("new client connected and was given id {}".format(client'id'))
# 全クライアントにメッセージを送信
# self.server.send_message_to_all("hey all, a new client has joined us")
# クライアント切断時に呼ばれる関数
def client_left(self, client, server):
print("client({}) disconnected".format(client'id'))
# クライアントからメッセージを受信したときに呼ばれる関数
def message_received(self, client, server, message):
print("client({}) said: {}".format(client'id', message))
# 全クライアントにメッセージを送信
# self.server.send_message_to_all(message)
#OSC送信
self.osc.send("/avatar/parameters/HeartRate",message)
# サーバーを起動する
def run(self):
# クライアント接続時のコールバック関数にself.new_client関数をセット
self.server.set_fn_new_client(self.new_client)
# クライアント切断時のコールバック関数にself.client_left関数をセット
self.server.set_fn_client_left(self.client_left)
# メッセージ受信時のコールバック関数にself.message_received関数をセット
self.server.set_fn_message_received(self.message_received)
self.server.run_forever()
IP_ADDR = "192.168.1.197" # Raspberry PiのIPアドレスを指定
PORT=5555 # ポートを指定
ws_server = Websocket_Server(IP_ADDR, PORT)
ws_server.run()
③アニメーションの作成
↓カウンタシェーダーを導入します。
https://www.patreon.com/posts/62864361
https://scrapbox.io/files/621b4956f530f7001d363350.png
Counter CをHead直下に配置。
Hierarchyで薄荷ちゃんを選択し、Animator内のControllerにFXレイヤー(Hakka FX)を入れる。
これを
https://scrapbox.io/files/621b4959f0e8370023c251a0.png
↓こうする
https://scrapbox.io/files/621b495c027e02001d9da69b.png
Project内で右クリック→Create→Animationを選択し、アニメーションファイルをHierarchy内のCounter CにD&Dする。
Hierarchy内のCounter Cを選択して、Animationタブ(無ければSceneタブ右クリック→Add Tab→Animation)を開く
Animationタブの右上あたりにある3つの点をクリックして、Frame表示にする。
https://scrapbox.io/files/621b49615476e0001d124c7d.png
録画ボタンをクリックして、0フレーム目に移動し、InspectorからValueを60に設定。
次に100フレーム目に移動して、InspectorからValueを160に設定。
https://scrapbox.io/files/621b4964f6cab30021d971d0.png
https://scrapbox.io/files/621b496870518d001f320471.png
次に、Curveを選択して、キーフレームの補完をリニアに変える。
↓初期状態だとリニアでないため、110より低い心拍のときには実際より低く、110より高いときには実際より高く表示されてしまう。
https://scrapbox.io/files/621b496b3ddee900221417f1.png
左下のキーフレームを右クリック→Right Tangent→Linear
右上のキーフレームを右クリック→LeftTangent→Linear
https://scrapbox.io/files/621b496ee355a6001dad4b6e.png
https://scrapbox.io/files/621b4971c2b8ef001eb3ab0f.png
これで補完をリニアに変更できた。
これでキーフレームを設定できたので、録画ボタンを押して録画モードを終了し、薄荷ちゃんのAnimatorのControllerからHakka FXを外してNoneに戻しておく。
④アニメーションの設定
アニメーションコントローラのHakka FXを選択して、
アニメーションレイヤー:FX Heart
パラメータ名:HeartRate(Float)
で作成する。
アニメーションレイヤーのウェイトを1にする。
https://scrapbox.io/files/621b49740e9938001eca8e30.png
アニメーションレイヤーにてEntry右クリック→New stateを作成する。
New stateの設定をこんな感じにする。
https://scrapbox.io/files/621b4976e1f90a001ddfb8ff.png
Expression MenuにRadial Puppetを追加する。
https://scrapbox.io/files/621b497a0c3b31001f6734c5.png
Expression ParametersにもHeartRateパラメータを追加する。この名前はアニメーションレイヤーに設定したパラメータと同じ名前にする。
https://scrapbox.io/files/621b497d4d044a001d24ebf8.png
⑤Jsonファイルにパラメータを追記する
code:C:\Users\UserName\AppData\LocalLow\VRChat\VRChat\OSC\usr_***\Avatars\avatarname_avtr_***.json
{
"name": "HeartRate",
"input":{
"address":"/avatar/parameters/HeartRate",
"type":"Float"
}
}
⑥VRChatの起動オプションを設定する
(OSCでVRChat→OSCクライアントに通信する時のみ必要。output属性をパラメータに付ける時など。)
https://scrapbox.io/files/621b4980202d2d002031a808.png
例:
--osc=9000:192.168.1.197:9001
送信先IPにはRaspberry PiのIPを設定する。
アバターをアップロードして完了!
https://scrapbox.io/files/621b498370bd090021ff68ec.png
使う時は、
Websocket/OSCサーバー
↓
FitBitアプリ
の順に起動する。(逆だとConnection Errorでアプリが終了する。)
参考にさせて頂いたリンク
fitbit senseの心拍データをリアルタイムで取得する
https://qiita.com/toyohisa/items/82656459643a2d64b76f
ラズパイとPCでWebsocketを体験する
https://101010.fun/iot/raspberry-pi-websocket.html
PythonOSC
https://pypi.org/project/python-osc/
Python WebSocket通信の仕方:サーバー編
https://www.raspberrypirulo.net/entry/websocket-server
カウンタシェーダー
https://www.patreon.com/posts/62864361
【VRChat】OSC来たから心拍計とかデジタル時計をつける
https://note.com/slivers_note/n/n4acd3fe2f0f9
ラジアルでのシェイプキー操作方法
https://note.com/citron_vr/n/n7d54ebaebd83