OSCQuery support (#958)

Co-authored-by: Uriel <imurx@proton.me>
This commit is contained in:
Erimel
2024-07-31 12:01:38 -04:00
committed by GitHub
parent 0859abf08d
commit 58ca3fe8c1
13 changed files with 505 additions and 290 deletions

View File

@@ -507,14 +507,17 @@ settings-osc-router-network-address-placeholder = IPV4 address
## OSC VRChat settings
settings-osc-vrchat = VRChat OSC Trackers
# This cares about multilines
settings-osc-vrchat-description =
Change VRChat-specific settings to receive headset (HMD) data and send
tracker data for FBT without SteamVR (ex. Quest standalone).
settings-osc-vrchat-description-v1 =
Change settings specific to the OSC Trackers standard used for sending
tracking data to applications without SteamVR (ex. Quest standalone).
Make sure to enable OSC in VRChat via the Action Menu under OSC > Enabled.
To allow receiving HMD and controller data from VRChat, go in your main menu's
settings under Tracking & IK > Allow Sending Head and Wrist VR Tracking OSC Data.
settings-osc-vrchat-enable = Enable
settings-osc-vrchat-enable-description = Toggle the sending and receiving of data.
settings-osc-vrchat-enable-label = Enable
settings-osc-vrchat-network = Network ports
settings-osc-vrchat-network-description = Set the ports for listening and sending data to VRChat.
settings-osc-vrchat-network-description-v1 = Set the ports for listening and sending data. Can be left untouched for VRChat.
settings-osc-vrchat-network-port_in =
.label = Port In
.placeholder = Port in (default: 9001)
@@ -522,7 +525,7 @@ settings-osc-vrchat-network-port_out =
.label = Port Out
.placeholder = Port out (default: 9000)
settings-osc-vrchat-network-address = Network address
settings-osc-vrchat-network-address-description = Choose which address to send out data to VRChat (check your Wi-Fi settings on your device).
settings-osc-vrchat-network-address-description-v1 = Choose which address to send out data to. Can be left untouched for VRChat.
settings-osc-vrchat-network-address-placeholder = VRChat ip address
settings-osc-vrchat-network-trackers = Trackers
settings-osc-vrchat-network-trackers-description = Toggle the sending of specific trackers via OSC.

View File

@@ -131,7 +131,7 @@ export function VRCOSCSettings() {
<div className="flex flex-col pt-2 pb-4">
<>
{l10n
.getString('settings-osc-vrchat-description')
.getString('settings-osc-vrchat-description-v1')
.split('\n')
.map((line, i) => (
<Typography color="secondary" key={i}>
@@ -162,7 +162,7 @@ export function VRCOSCSettings() {
</Typography>
<div className="flex flex-col pb-2">
<Typography color="secondary">
{l10n.getString('settings-osc-vrchat-network-description')}
{l10n.getString('settings-osc-vrchat-network-description-v1')}
</Typography>
</div>
<div className="grid grid-cols-2 gap-3 pb-5">
@@ -199,7 +199,7 @@ export function VRCOSCSettings() {
<div className="flex flex-col pb-2">
<Typography color="secondary">
{l10n.getString(
'settings-osc-vrchat-network-address-description'
'settings-osc-vrchat-network-address-description-v1'
)}
</Typography>
</div>

View File

@@ -3,6 +3,8 @@
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
<application
android:allowBackup="true"

View File

@@ -2,6 +2,8 @@
package dev.slimevr.android
import android.content.Context
import android.net.wifi.WifiManager
import androidx.appcompat.app.AppCompatActivity
import dev.slimevr.Keybinding
import dev.slimevr.VRServer
@@ -50,6 +52,12 @@ fun main(activity: AppCompatActivity) {
vrServer = VRServer(
configPath = File(activity.filesDir, "vrconfig.yml").absolutePath,
serialHandlerProvider = { _ -> AndroidSerialHandler(activity) },
acquireMulticastLock = {
val wifi = activity.getSystemService(Context.WIFI_SERVICE) as WifiManager
val lock = wifi.createMulticastLock("slimevr-jmdns-multicast-lock")
lock.setReferenceCounted(true)
lock.acquire()
},
)
vrServer.start()
Keybinding(vrServer)

View File

@@ -36,7 +36,8 @@ configure<com.diffplug.gradle.spotless.SpotlessExtension> {
"ij_kotlin_packages_to_use_import_on_demand" to
"java.util.*,kotlin.math.*,dev.slimevr.autobone.errors.*" +
",io.github.axisangles.ktmath.*,kotlinx.atomicfu.*" +
",dev.slimevr.tracking.trackers.*,dev.slimevr.desktop.platform.ProtobufMessages.*",
",dev.slimevr.tracking.trackers.*,dev.slimevr.desktop.platform.ProtobufMessages.*" +
",com.illposed.osc.*",
"ij_kotlin_allow_trailing_comma" to true,
)
val ktlintVersion = "1.2.1"

View File

@@ -50,6 +50,7 @@ allprojects {
// Use jcenter for resolving dependencies.
// You can declare any Maven/Ivy/file repository here.
mavenCentral()
maven(url = "https://jitpack.io")
}
}
@@ -68,12 +69,15 @@ dependencies {
implementation("org.apache.commons:commons-lang3:3.12.0")
implementation("org.apache.commons:commons-collections4:4.4")
implementation("com.illposed.osc:javaosc-core:0.8")
implementation("com.illposed.osc:javaosc-core:0.9")
implementation("org.java-websocket:Java-WebSocket:1.+")
implementation("com.melloware:jintellitype:1.+")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")
implementation("it.unimi.dsi:fastutil:8.5.12")
// Jitpack
implementation("com.github.SlimeVR:oscquery-kt:566a0cba58")
testImplementation(kotlin("test"))
// Use JUnit test framework
testImplementation(platform("org.junit:junit-bom:5.9.0"))

View File

@@ -47,6 +47,7 @@ class VRServer @JvmOverloads constructor(
driverBridgeProvider: SteamBridgeProvider = { _, _ -> null },
feederBridgeProvider: (VRServer) -> ISteamVRBridge? = { _ -> null },
serialHandlerProvider: (VRServer) -> SerialHandler = { _ -> SerialHandlerStub() },
acquireMulticastLock: () -> Any? = { null },
configPath: String,
) : Thread("VRServer") {
@JvmField
@@ -60,6 +61,7 @@ class VRServer @JvmOverloads constructor(
private val tasks: Queue<Runnable> = LinkedBlockingQueue()
private val newTrackersConsumers: MutableList<Consumer<Tracker>> = FastList()
private val onTick: MutableList<Runnable> = FastList()
private val lock = acquireMulticastLock()
val oSCRouter: OSCRouter
@JvmField
@@ -141,8 +143,6 @@ class VRServer @JvmOverloads constructor(
// Initialize OSC handlers
vrcOSCHandler = VRCOSCHandler(
this,
humanPoseManager,
driverBridge,
configManager.vrConfig.vrcOSC,
computedTrackers,
)
@@ -282,13 +282,11 @@ class VRServer @JvmOverloads constructor(
fun updateSkeletonModel() {
queueTask {
humanPoseManager.updateSkeletonModelFromServer()
vrcOSCHandler.setHeadTracker(TrackerUtils.getTrackerForSkeleton(trackers, TrackerPosition.HEAD))
if (this.getVRBridge(ISteamVRBridge::class.java)?.updateShareSettingsAutomatically() == true) {
RPCSettingsHandler.sendSteamVRUpdatedSettings(protocolAPI, protocolAPI.rpcHandler)
}
}
vrcOSCHandler.setHeadTracker(
TrackerUtils.getTrackerForSkeleton(trackers, TrackerPosition.HEAD),
)
}
fun resetTrackersFull(resetSourceName: String?) {

View File

@@ -10,6 +10,10 @@ public interface OSCHandler {
public void refreshSettings(boolean refreshRouterSettings);
public void updateOscReceiver(int portIn, String[] args);
public void updateOscSender(int portOut, String address);
public void update();
public OSCPortOut getOscSender();

View File

@@ -70,79 +70,19 @@ class VMCHandler(
anchorHip = config.anchorHip
mirrorTracking = config.mirrorTracking
// Stops listening and closes OSC port
val wasListening = oscReceiver != null && oscReceiver!!.isListening
if (wasListening) {
oscReceiver!!.stopListening()
}
val wasConnected = oscSender != null && oscSender!!.isConnected
if (wasConnected) {
try {
oscSender!!.close()
} catch (e: IOException) {
LogManager.severe("[VMCHandler] Error closing the OSC sender: $e")
}
}
updateOscReceiver(
config.portIn,
arrayOf(
"/VMC/Ext/Bone/Pos",
"/VMC/Ext/Hmd/Pos",
"/VMC/Ext/Con/Pos",
"/VMC/Ext/Tra/Pos",
"/VMC/Ext/Root/Pos",
),
)
updateOscSender(config.portOut, config.address)
if (config.enabled) {
// Instantiates the OSC receiver
try {
val port = config.portIn
oscReceiver = OSCPortIn(port)
if (lastPortIn != port || !wasListening) {
LogManager.info("[VMCHandler] Listening to port $port")
}
lastPortIn = port
} catch (e: IOException) {
LogManager
.severe(
"[VMCHandler] Error listening to the port ${config.portIn}: $e",
)
}
// Starts listening for VMC messages
if (oscReceiver != null) {
val listener = OSCMessageListener { event: OSCMessageEvent -> this.handleReceivedMessage(event) }
val listenAddresses = arrayOf(
"/VMC/Ext/Bone/Pos",
"/VMC/Ext/Hmd/Pos",
"/VMC/Ext/Con/Pos",
"/VMC/Ext/Tra/Pos",
"/VMC/Ext/Root/Pos",
)
for (address in listenAddresses) {
oscReceiver!!
.dispatcher
.addListener(OSCPatternAddressMessageSelector(address), listener)
}
oscReceiver!!.startListening()
}
// Instantiate the OSC sender
try {
val address = InetAddress.getByName(config.address)
val port = config.portOut
oscSender = OSCPortOut(InetSocketAddress(address, port))
if ((lastPortOut != port && lastAddress !== address) || !wasConnected) {
LogManager
.info(
"[VMCHandler] Sending to port $port at address $address",
)
}
lastPortOut = port
lastAddress = address
oscSender!!.connect()
outputUnityArmature = UnityArmature(false)
} catch (e: IOException) {
LogManager
.severe(
"[VMCHandler] Error connecting to port ${config.portOut} at the address ${config.address}: $e",
)
}
// Load VRM data
if (outputUnityArmature != null && config.vrmJson != null) {
val vrmReader = VRMReader(config.vrmJson!!)
@@ -176,6 +116,79 @@ class VMCHandler(
if (refreshRouterSettings) server.oSCRouter.refreshSettings(false)
}
override fun updateOscReceiver(portIn: Int, args: Array<String>) {
// Stops listening and closes OSC port
val wasListening = oscReceiver != null && oscReceiver!!.isListening
if (wasListening) {
oscReceiver!!.stopListening()
}
if (config.enabled) {
// Instantiates the OSC receiver
try {
oscReceiver = OSCPortIn(portIn)
if (lastPortIn != portIn || !wasListening) {
LogManager.info("[VMCHandler] Listening to port $portIn")
}
lastPortIn = portIn
} catch (e: IOException) {
LogManager
.severe(
"[VMCHandler] Error listening to the port $portIn: $e",
)
}
// Starts listening for VMC messages
if (oscReceiver != null) {
val listener = OSCMessageListener { event: OSCMessageEvent -> this.handleReceivedMessage(event) }
for (address in args) {
oscReceiver!!
.dispatcher
.addListener(OSCPatternAddressMessageSelector(address), listener)
}
oscReceiver!!.startListening()
}
}
}
override fun updateOscSender(portOut: Int, ip: String) {
// Stop sending
val wasConnected = oscSender != null && oscSender!!.isConnected
if (wasConnected) {
try {
oscSender!!.close()
} catch (e: IOException) {
LogManager.severe("[VMCHandler] Error closing the OSC sender: $e")
}
}
if (config.enabled) {
// Instantiate the OSC sender
try {
val addr = InetAddress.getByName(ip)
oscSender = OSCPortOut(InetSocketAddress(addr, portOut))
if ((lastPortOut != portOut && lastAddress !== addr) || !wasConnected) {
LogManager
.info(
"[VMCHandler] Sending to port $portOut at address $ip",
)
}
lastPortOut = portOut
lastAddress = addr
oscSender!!.connect()
outputUnityArmature = UnityArmature(false)
} catch (e: IOException) {
LogManager
.severe(
"[VMCHandler] Error connecting to port $portOut at the address $ip: $e",
)
}
}
}
private fun handleReceivedMessage(event: OSCMessageEvent) {
when (event.message.address) {
// Is bone (rotation)

View File

@@ -1,6 +1,5 @@
package dev.slimevr.osc
import com.illposed.osc.MessageSelector
import com.illposed.osc.OSCBundle
import com.illposed.osc.OSCMessage
import com.illposed.osc.OSCMessageEvent
@@ -12,9 +11,7 @@ import com.illposed.osc.transport.OSCPortOut
import com.jme3.math.FastMath
import com.jme3.system.NanoTimer
import dev.slimevr.VRServer
import dev.slimevr.bridge.ISteamVRBridge
import dev.slimevr.config.VRCOSCConfig
import dev.slimevr.tracking.processor.HumanPoseManager
import dev.slimevr.tracking.trackers.Device
import dev.slimevr.tracking.trackers.Tracker
import dev.slimevr.tracking.trackers.TrackerPosition
@@ -28,7 +25,6 @@ import io.github.axisangles.ktmath.Vector3
import java.io.IOException
import java.net.InetAddress
import java.net.InetSocketAddress
import java.util.*
private const val OFFSET_SLERP_FACTOR = 0.5f // Guessed from eyeing VRChat
@@ -37,25 +33,36 @@ private const val OFFSET_SLERP_FACTOR = 0.5f // Guessed from eyeing VRChat
*/
class VRCOSCHandler(
private val server: VRServer,
private val humanPoseManager: HumanPoseManager,
private val steamvrBridge: ISteamVRBridge?,
private val config: VRCOSCConfig,
private val computedTrackers: List<Tracker>,
) : OSCHandler {
private val localIp = InetAddress.getLocalHost().hostAddress
private val loopbackIp = InetAddress.getLoopbackAddress().hostAddress
private val vrsystemTrackersAddresses = arrayOf(
"/tracking/vrsystem/head/pose",
"/tracking/vrsystem/leftwrist/pose",
"/tracking/vrsystem/rightwrist/pose",
)
private val oscTrackersAddresses = arrayOf(
"/tracking/trackers/*/position",
"/tracking/trackers/*/rotation",
)
private var oscReceiver: OSCPortIn? = null
private var oscSender: OSCPortOut? = null
private var oscQuerySender: OSCPortOut? = null
private var oscMessage: OSCMessage? = null
private var vrcHmd: Tracker? = null
private var headTracker: Tracker? = null
private var oscTrackersDevice: Device? = null
private var vrsystemTrackersDevice: Device? = null
private val oscArgs = FastList<Float?>(3)
private val trackersEnabled: BooleanArray = BooleanArray(computedTrackers.size)
private var lastPortIn = 0
private var lastPortOut = 0
private var lastAddress: InetAddress? = null
private var oscPortIn = 0
private var oscPortOut = 0
private var oscIp: InetAddress? = null
private var oscQuerySenderState = false
private var oscQueryPortOut = 0
private var oscQueryIp: String? = null
private var timeAtLastError: Long = 0
private val timer = Timer()
private var listenTrackers = false
private var receivingPositionOffset = Vector3.NULL
private var postReceivingPositionOffset = Vector3.NULL
private var receivingRotationOffset = Quaternion.IDENTITY
@@ -63,6 +70,7 @@ class VRCOSCHandler(
private val postReceivingOffset = EulerAngles(EulerOrder.YXZ, 0f, FastMath.PI, 0f).toQuaternion()
private var timeAtLastReceivedRotationOffset = System.currentTimeMillis()
private var fpsTimer: NanoTimer? = null
private var vrcOscQueryHandler: VRCOSCQueryHandler? = null
init {
refreshSettings(false)
@@ -82,11 +90,96 @@ class VRCOSCHandler(
}
}
// Stops listening and closes OSC port
updateOscReceiver(config.portIn, vrsystemTrackersAddresses + oscTrackersAddresses)
updateOscSender(config.portOut, config.address)
if (vrcOscQueryHandler == null && config.enabled) {
vrcOscQueryHandler = VRCOSCQueryHandler(this)
} else if (vrcOscQueryHandler != null && !config.enabled) {
vrcOscQueryHandler?.close()
vrcOscQueryHandler = null
}
if (refreshRouterSettings) {
server.oSCRouter.refreshSettings(false)
}
}
/**
* Adds an OSC Sender from OSCQuery
*/
fun addOSCQuerySender(oscPortOut: Int, oscIP: String) {
val addr = InetAddress.getByName(oscIP)
oscQuerySenderState = true
oscQueryIp = oscIP
oscQueryPortOut = oscPortOut
if (oscPortOut != portOut || (oscIP != address.hostName && !(oscIP == localIp && address.hostName == loopbackIp))) {
try {
oscQuerySender = OSCPortOut(InetSocketAddress(addr, oscPortOut))
oscQuerySender?.connect()
LogManager.info("[VRCOSCHandler] OSCQuery sender sending to port $oscPortOut at address $oscIP")
} catch (e: IOException) {
LogManager.severe("[VRCOSCHandler] Error connecting to port $oscPortOut at the address $oscIP: $e")
}
}
}
/**
* Close/remove the osc query sender
*/
fun closeOscQuerySender(newState: Boolean) {
oscQuerySender?.let {
try {
it.close()
oscQuerySender = null
oscQuerySenderState = newState
} catch (e: IOException) {
LogManager.severe("[VRCOSCHandler] Error closing the OSC sender: $e")
}
}
}
override fun updateOscReceiver(portIn: Int, args: Array<String>) {
// Stop listening
val wasListening = oscReceiver != null && oscReceiver!!.isListening
if (wasListening) {
oscReceiver!!.stopListening()
}
if (config.enabled) {
// Instantiates the OSC receiver
try {
oscReceiver = OSCPortIn(portIn)
if (oscPortIn != portIn || !wasListening) {
LogManager.info("[VRCOSCHandler] Listening to port $portIn")
}
oscPortIn = portIn
vrcOscQueryHandler?.updateOSCQuery(portIn.toUShort())
} catch (e: IOException) {
LogManager
.severe(
"[VRCOSCHandler] Error listening to the port $portIn: $e",
)
}
// Starts listening for VRC or OSCTrackers messages
oscReceiver?.let {
val listener = OSCMessageListener { event: OSCMessageEvent ->
handleReceivedMessage(event)
}
for (address in args) {
it.dispatcher.addListener(
OSCPatternAddressMessageSelector(address),
listener,
)
}
it.startListening()
}
}
}
override fun updateOscSender(portOut: Int, ip: String) {
// Stop sending
val wasConnected = oscSender != null && oscSender!!.isConnected
if (wasConnected) {
try {
@@ -95,221 +188,218 @@ class VRCOSCHandler(
LogManager.severe("[VRCOSCHandler] Error closing the OSC sender: $e")
}
}
if (config.enabled) {
// Instantiates the OSC receiver
try {
val port = config.portIn
oscReceiver = OSCPortIn(port)
if (lastPortIn != port || !wasListening) {
LogManager.info("[VRCOSCHandler] Listening to port $port")
}
lastPortIn = port
} catch (e: IOException) {
LogManager
.severe(
"[VRCOSCHandler] Error listening to the port " +
config.portIn +
": " +
e,
)
}
// Starts listening for VRC or OSCTrackers messages
if (oscReceiver != null) {
val listener = OSCMessageListener { event: OSCMessageEvent -> handleReceivedMessage(event) }
val vrcSelector: MessageSelector = OSCPatternAddressMessageSelector(
"/avatar/parameters/Upright",
)
val trackersPositionSelector: MessageSelector = OSCPatternAddressMessageSelector(
"/tracking/trackers/*/position",
)
val trackersRotationSelector: MessageSelector = OSCPatternAddressMessageSelector(
"/tracking/trackers/*/rotation",
)
oscReceiver!!.dispatcher.addListener(vrcSelector, listener)
oscReceiver!!.dispatcher.addListener(trackersPositionSelector, listener)
oscReceiver!!.dispatcher.addListener(trackersRotationSelector, listener)
listenTrackers = false
oscReceiver!!.startListening()
// Delay so we can actually detect if SteamVR is running
scheduleStartListeningSteamVR(1000)
}
// Instantiate the OSC sender
try {
val address = InetAddress.getByName(config.address)
val port = config.portOut
oscSender = OSCPortOut(InetSocketAddress(address, port))
if ((lastPortOut != port && lastAddress !== address) || !wasConnected) {
LogManager
.info(
"[VRCOSCHandler] Sending to port " +
port +
" at address " +
address.toString(),
)
val addr = InetAddress.getByName(ip)
oscSender = OSCPortOut(InetSocketAddress(addr, portOut))
if (oscPortOut != portOut && oscIp !== addr || !wasConnected) {
LogManager.info("[VRCOSCHandler] Sending to port $portOut at address $ip")
}
lastPortOut = port
lastAddress = address
oscSender!!.connect()
oscPortOut = portOut
oscIp = addr
oscSender?.connect()
} catch (e: IOException) {
LogManager
.severe(
"[VRCOSCHandler] Error connecting to port " +
config.portOut +
" at the address " +
config.address +
": " +
e,
"[VRCOSCHandler] Error connecting to port $portOut at the address $ip: $e",
)
return
}
}
if (refreshRouterSettings) server.oSCRouter.refreshSettings(false)
}
private fun scheduleStartListeningSteamVR(delay: Long) {
val resetTask: TimerTask = object : TimerTask() {
override fun run() {
listenTrackers = true
if (oscQueryPortOut == portOut && (oscQueryIp == ip || (oscQueryIp == localIp && ip == loopbackIp))) {
if (oscQuerySender != null) {
// Close the oscQuerySender if it has the same port/ip
closeOscQuerySender(true)
}
} else if (oscQuerySender == null && oscQuerySenderState) {
// Instantiate the oscQuerySender if it could not be instantiated.
addOSCQuerySender(oscQueryPortOut, oscQueryIp!!)
}
}
timer.schedule(resetTask, delay)
}
private fun handleReceivedMessage(event: OSCMessageEvent) {
if (listenTrackers) {
if (event.message.address.equals("/avatar/parameters/Upright")) {
// Receiving HMD data from VRChat
if (steamvrBridge != null && !steamvrBridge.isConnected()) {
if (vrcHmd == null) {
val vrcDevice = server.deviceManager.createDevice("VRChat OSC", null, "VRChat")
server.deviceManager.addDevice(vrcDevice)
vrcHmd = Tracker(
device = vrcDevice,
id = VRServer.getNextLocalTrackerId(),
name = "VRC HMD",
displayName = "VRC HMD",
trackerPosition = TrackerPosition.HEAD,
trackerNum = 0,
hasPosition = true,
userEditable = false,
isComputed = true,
usesTimeout = true,
isHmd = true,
)
vrcDevice.trackers[0] = vrcHmd!!
server.registerTracker(vrcHmd!!)
}
if (vrsystemTrackersAddresses.contains(event.message.address)) {
// Receiving Head and Wrist pose data thanks to OSCQuery
// Create device if it doesn't exist
if (vrsystemTrackersDevice == null) {
// Instantiate OSC Trackers device
vrsystemTrackersDevice = server.deviceManager.createDevice("VRC VRSystem", null, "VRChat")
server.deviceManager.addDevice(vrsystemTrackersDevice!!)
}
// Sets HMD status to OK
vrcHmd!!.status = TrackerStatus.OK
// Look at xxx in "/tracking/vrsystem/xxx/pose" to know TrackerPosition
var name = "VRChat "
val trackerPosition = when (event.message.address.split('/')[3]) {
"head" -> {
name += "head"
TrackerPosition.HEAD
}
// Sets the HMD y position to
// the vrc Upright parameter (0-1) * the user's height
vrcHmd!!
.position = Vector3(
0f,
event
.message
.arguments[0] as Float * humanPoseManager.userHeightFromConfig,
0f,
"leftwrist" -> {
name += "left hand"
TrackerPosition.LEFT_HAND
}
"rightwrist" -> {
name += "right hand"
TrackerPosition.RIGHT_HAND
}
else -> {
LogManager.warning("[VRCOSCHandler] Received invalid body part in message \"${event.message.address}\"")
return
}
}
// Try to get the tracker
var tracker = vrsystemTrackersDevice!!.trackers[trackerPosition.ordinal]
// Build the tracker if it doesn't exist
if (tracker == null) {
tracker = Tracker(
device = vrsystemTrackersDevice,
id = VRServer.getNextLocalTrackerId(),
name = name,
displayName = name,
trackerNum = trackerPosition.ordinal,
trackerPosition = trackerPosition,
hasRotation = true,
hasPosition = true,
userEditable = true,
isComputed = true,
needsReset = trackerPosition != TrackerPosition.HEAD,
usesTimeout = true,
)
vrsystemTrackersDevice!!.trackers[trackerPosition.ordinal] = tracker
server.registerTracker(tracker)
}
// Sets the tracker status to OK
tracker.status = TrackerStatus.OK
// Update tracker position
tracker.position = Vector3(
event.message.arguments[0] as Float,
event.message.arguments[1] as Float,
-(event.message.arguments[2] as Float),
)
// Update tracker rotation
val (w, x, y, z) = EulerAngles(
EulerOrder.YXZ,
event.message.arguments[3] as Float * FastMath.DEG_TO_RAD,
event.message.arguments[4] as Float * FastMath.DEG_TO_RAD,
event.message.arguments[5] as Float * FastMath.DEG_TO_RAD,
).toQuaternion()
val rot = Quaternion(w, -x, -y, z)
tracker.setRotation(rot)
tracker.dataTick()
} else {
// Receiving OSC Trackers data. This is not from VRChat.
if (oscTrackersDevice == null) {
// Instantiate OSC Trackers device
oscTrackersDevice = server.deviceManager.createDevice("OSC Tracker", null, "OSC Trackers")
server.deviceManager.addDevice(oscTrackersDevice!!)
}
// Extract the xxx in "/tracking/trackers/xxx/..."
val splitAddress = event.message.address.split('/')
val trackerStringValue = splitAddress[3]
val dataType = event.message.address.split('/')[4]
if (trackerStringValue == "head") {
// Head data
if (dataType == "position") {
// Position offset
receivingPositionOffset = Vector3(
event.message.arguments[0] as Float,
event.message.arguments[1] as Float,
-(event.message.arguments[2] as Float),
)
vrcHmd!!.dataTick()
}
} else {
// Receiving OSC Trackers data
if (oscTrackersDevice == null) {
// Instantiate OSC Trackers device
oscTrackersDevice = server.deviceManager.createDevice("OSC Tracker", null, "OSC Trackers")
server.deviceManager.addDevice(oscTrackersDevice!!)
}
// Extract the x in "/tracking/trackers/x.../..."
val trackerStringValue = event.message.address.toString().subSequence(19, 20)
if (trackerStringValue == "h") {
// Head data
val slimeHead = headTracker
if (event.message.address.toString() == "/tracking/trackers/head/position") {
// Position offset
receivingPositionOffset = Vector3(
event.message.arguments[0] as Float,
event.message.arguments[1] as Float,
-(event.message.arguments[2] as Float),
)
if (slimeHead != null && slimeHead.hasPosition) {
postReceivingPositionOffset = slimeHead.position
headTracker?.let {
if (it.hasPosition) {
postReceivingPositionOffset = it.position
}
} else {
// Rotation offset
val (w, x, y, z) = EulerAngles(EulerOrder.YXZ, event.message.arguments[0] as Float * FastMath.DEG_TO_RAD, event.message.arguments[1] as Float * FastMath.DEG_TO_RAD, event.message.arguments[2] as Float * FastMath.DEG_TO_RAD).toQuaternion()
receivingRotationOffsetGoal = Quaternion(w, -x, -y, z).inv()
}
} else {
// Rotation offset
val (w, x, y, z) = EulerAngles(EulerOrder.YXZ, event.message.arguments[0] as Float * FastMath.DEG_TO_RAD, event.message.arguments[1] as Float * FastMath.DEG_TO_RAD, event.message.arguments[2] as Float * FastMath.DEG_TO_RAD).toQuaternion()
receivingRotationOffsetGoal = Quaternion(w, -x, -y, z).inv()
receivingRotationOffsetGoal = if (slimeHead != null && slimeHead.hasRotation) {
slimeHead.getRotation().project(Vector3.POS_Y).unit() * receivingRotationOffsetGoal
headTracker.let {
receivingRotationOffsetGoal = if (it != null && it.hasRotation) {
it.getRotation().project(Vector3.POS_Y).unit() * receivingRotationOffsetGoal
} else {
receivingRotationOffsetGoal
}
// If greater than 300ms, snap to rotation
if (System.currentTimeMillis() - timeAtLastReceivedRotationOffset > 300) {
receivingRotationOffset = receivingRotationOffsetGoal
}
// Update time variable
timeAtLastReceivedRotationOffset = System.currentTimeMillis()
}
} else {
// Trackers data (1-8)
val trackerId = trackerStringValue[0].digitToInt()
var tracker = oscTrackersDevice!!.trackers[trackerId]
if (tracker == null) {
tracker = Tracker(
device = oscTrackersDevice,
id = VRServer.getNextLocalTrackerId(),
name = "OSC Tracker #$trackerId",
displayName = "OSC Tracker #$trackerId",
trackerNum = trackerId,
trackerPosition = null,
hasRotation = true,
hasPosition = true,
userEditable = true,
isComputed = true,
needsReset = true,
usesTimeout = true,
)
oscTrackersDevice!!.trackers[trackerId] = tracker
server.registerTracker(tracker)
}
// Sets the tracker status to OK
tracker.status = TrackerStatus.OK
if (event.message.address.toString() == "/tracking/trackers/$trackerId/position") {
tracker.position = receivingRotationOffset.sandwich(
Vector3(
event.message.arguments[0] as Float,
event.message.arguments[1] as Float,
-(event.message.arguments[2] as Float),
) - receivingPositionOffset,
) + postReceivingPositionOffset
} else {
val (w, x, y, z) = EulerAngles(EulerOrder.YXZ, event.message.arguments[0] as Float * FastMath.DEG_TO_RAD, event.message.arguments[1] as Float * FastMath.DEG_TO_RAD, event.message.arguments[2] as Float * FastMath.DEG_TO_RAD).toQuaternion()
val rot = Quaternion(w, -x, -y, z)
tracker.setRotation(receivingRotationOffset * rot * postReceivingOffset)
// If greater than 300ms, snap to rotation
if (System.currentTimeMillis() - timeAtLastReceivedRotationOffset > 300) {
receivingRotationOffset = receivingRotationOffsetGoal
}
tracker.dataTick()
// Update time variable
timeAtLastReceivedRotationOffset = System.currentTimeMillis()
}
} else {
// Trackers data (1-8)
val trackerId = trackerStringValue.toInt()
var tracker = oscTrackersDevice!!.trackers[trackerId]
if (tracker == null) {
tracker = Tracker(
device = oscTrackersDevice,
id = VRServer.getNextLocalTrackerId(),
name = "OSC Tracker #$trackerId",
displayName = "OSC Tracker #$trackerId",
trackerNum = trackerId,
trackerPosition = null,
hasRotation = true,
hasPosition = true,
userEditable = true,
isComputed = true,
needsReset = true,
usesTimeout = true,
)
oscTrackersDevice!!.trackers[trackerId] = tracker
server.registerTracker(tracker)
}
// Sets the tracker status to OK
tracker.status = TrackerStatus.OK
if (dataType == "position") {
// Update tracker position
tracker.position = receivingRotationOffset.sandwich(
Vector3(
event.message.arguments[0] as Float,
event.message.arguments[1] as Float,
-(event.message.arguments[2] as Float),
) - receivingPositionOffset,
) + postReceivingPositionOffset
} else {
// Update tracker rotation
val (w, x, y, z) = EulerAngles(
EulerOrder.YXZ,
event.message.arguments[0] as Float * FastMath.DEG_TO_RAD,
event.message.arguments[1] as Float * FastMath.DEG_TO_RAD,
event.message.arguments[2] as Float * FastMath.DEG_TO_RAD,
).toQuaternion()
val rot = Quaternion(w, -x, -y, z)
tracker.setRotation(receivingRotationOffset * rot * postReceivingOffset)
}
tracker.dataTick()
}
}
}
override fun update() {
// Update current time
val currentTime = System.currentTimeMillis().toFloat()
// Gets timer from vrServer
if (fpsTimer == null) {
fpsTimer = VRServer.instance.fpsTimer
@@ -319,6 +409,9 @@ class VRCOSCHandler(
receivingRotationOffset = receivingRotationOffset.interpR(receivingRotationOffsetGoal, OFFSET_SLERP_FACTOR * (fpsTimer?.timePerFrame ?: 1f))
}
// Update current time
val currentTime = System.currentTimeMillis().toFloat()
// Send OSC data
if (oscSender != null && oscSender!!.isConnected) {
// Create new bundle
@@ -382,7 +475,8 @@ class VRCOSCHandler(
}
try {
oscSender!!.send(bundle)
oscSender?.send(bundle)
oscQuerySender?.send(bundle)
} catch (e: IOException) {
// Avoid spamming AsynchronousCloseException too many
// times per second
@@ -400,9 +494,9 @@ class VRCOSCHandler(
}
private fun getVRCOSCTrackersId(trackerPosition: TrackerPosition?): Int {
// The order doesn't matter and changing it
// won't break anything except make debugging harder
// between different versions. They just need to range from 1-8
// Needs to range from 1-8.
// Don't change as third party applications may rely
// on this for mapping trackers to body parts.
return when (trackerPosition) {
TrackerPosition.HIP -> 1
TrackerPosition.LEFT_FOOT -> 2
@@ -435,7 +529,8 @@ class VRCOSCHandler(
oscArgs,
)
try {
oscSender!!.send(oscMessage)
oscSender?.send(oscMessage)
oscQuerySender?.send(oscMessage)
} catch (e: IOException) {
LogManager
.warning("[VRCOSCHandler] Error sending OSC message to VRChat: $e")
@@ -448,11 +543,11 @@ class VRCOSCHandler(
override fun getOscSender(): OSCPortOut = oscSender!!
override fun getPortOut(): Int = lastPortOut
override fun getPortOut(): Int = oscPortOut
override fun getAddress(): InetAddress = lastAddress!!
override fun getAddress(): InetAddress = oscIp!!
override fun getOscReceiver(): OSCPortIn = oscReceiver!!
override fun getPortIn(): Int = lastPortIn
override fun getPortIn(): Int = oscPortIn
}

View File

@@ -0,0 +1,86 @@
package dev.slimevr.osc
import OSCQueryNode
import OSCQueryServer
import ServiceInfo
import dev.slimevr.protocol.rpc.setup.RPCUtil
import io.eiren.util.logging.LogManager
import randomFreePort
import java.io.IOException
import kotlin.concurrent.thread
private const val serviceStartsWith = "VRChat-Client"
private const val queryPath = "/tracking/vrsystem"
/**
* Handler for OSCQuery for VRChat using our library
* https://github.com/SlimeVR/oscquery-kt
*/
class VRCOSCQueryHandler(
private val vrcOscHandler: VRCOSCHandler,
) {
private val oscQueryServer: OSCQueryServer
init {
// Request data
val localIp = RPCUtil.getLocalIp()
val httpPort = randomFreePort()
oscQueryServer = OSCQueryServer(
"SlimeVR-Server-$httpPort",
OscTransport.UDP,
localIp,
vrcOscHandler.portIn.toUShort(),
httpPort,
)
oscQueryServer.rootNode.addNode(OSCQueryNode(queryPath))
oscQueryServer.init()
LogManager.info("[VRCOSCQueryHandler] SlimeVR OSCQueryServer started at http://$localIp:$httpPort")
try {
// Add service listener
LogManager.info("[VRCOSCQueryHandler] Listening for VRChat OSCQuery")
oscQueryServer.service.addServiceListener(
"_osc._udp.local.",
onServiceAdded = ::serviceAdded,
)
} catch (e: IOException) {
LogManager.warning("[VRCOSCQueryHandler] " + e.message)
}
}
/**
* Updates the OSC service's port
*/
fun updateOSCQuery(port: UShort) {
if (oscQueryServer.oscPort != port) {
thread(start = true) {
oscQueryServer.updateOscService(port)
}
}
}
/**
* Called when a service is added
*/
private fun serviceAdded(info: ServiceInfo) {
// Check the service name
if (!info.name.startsWith(serviceStartsWith)) return
// Get url from ServiceInfo
val ip = info.inetAddresses[0].hostAddress
val port = info.port
// create a new OSCHandler for this service
vrcOscHandler.addOSCQuerySender(port, ip)
}
/**
* Closes the OSCQueryServer and the associated OSC sender.
*/
fun close() {
vrcOscHandler.closeOscQuerySender(false)
thread(start = true) {
oscQueryServer.close()
}
}
}

View File

@@ -46,6 +46,7 @@ allprojects {
// Use jcenter for resolving dependencies.
// You can declare any Maven/Ivy/file repository here.
mavenCentral()
maven(url = "https://jitpack.io")
}
}

View File

@@ -122,10 +122,10 @@ fun main(args: Array<String>) {
::provideSteamVRBridge,
::provideFeederBridge,
{ _ -> DesktopSerialHandler() },
configDir,
configPath = configDir,
)
vrServer.start()
// Start service for USB HID trackers
TrackersHID(
"Sensors HID service",