diff --git a/gui/public/i18n/en/translation.ftl b/gui/public/i18n/en/translation.ftl
index 9dcaca278..a13ca40e4 100644
--- a/gui/public/i18n/en/translation.ftl
+++ b/gui/public/i18n/en/translation.ftl
@@ -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.
diff --git a/gui/src/components/settings/pages/VRCOSCSettings.tsx b/gui/src/components/settings/pages/VRCOSCSettings.tsx
index 87bdbd152..c709cb765 100644
--- a/gui/src/components/settings/pages/VRCOSCSettings.tsx
+++ b/gui/src/components/settings/pages/VRCOSCSettings.tsx
@@ -131,7 +131,7 @@ export function VRCOSCSettings() {
<>
{l10n
- .getString('settings-osc-vrchat-description')
+ .getString('settings-osc-vrchat-description-v1')
.split('\n')
.map((line, i) => (
@@ -162,7 +162,7 @@ export function VRCOSCSettings() {
- {l10n.getString('settings-osc-vrchat-network-description')}
+ {l10n.getString('settings-osc-vrchat-network-description-v1')}
@@ -199,7 +199,7 @@ export function VRCOSCSettings() {
{l10n.getString(
- 'settings-osc-vrchat-network-address-description'
+ 'settings-osc-vrchat-network-address-description-v1'
)}
diff --git a/server/android/src/main/AndroidManifest.xml b/server/android/src/main/AndroidManifest.xml
index 5b4d5c2b2..2ce93480a 100644
--- a/server/android/src/main/AndroidManifest.xml
+++ b/server/android/src/main/AndroidManifest.xml
@@ -3,6 +3,8 @@
xmlns:tools="http://schemas.android.com/tools">
+
+
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)
diff --git a/server/build.gradle.kts b/server/build.gradle.kts
index 7f64540c0..2c6f41398 100644
--- a/server/build.gradle.kts
+++ b/server/build.gradle.kts
@@ -36,7 +36,8 @@ configure {
"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"
diff --git a/server/core/build.gradle.kts b/server/core/build.gradle.kts
index 40c6ba1a9..228d734b0 100644
--- a/server/core/build.gradle.kts
+++ b/server/core/build.gradle.kts
@@ -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"))
diff --git a/server/core/src/main/java/dev/slimevr/VRServer.kt b/server/core/src/main/java/dev/slimevr/VRServer.kt
index 45cc071eb..6ae919fe8 100644
--- a/server/core/src/main/java/dev/slimevr/VRServer.kt
+++ b/server/core/src/main/java/dev/slimevr/VRServer.kt
@@ -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 = LinkedBlockingQueue()
private val newTrackersConsumers: MutableList> = FastList()
private val onTick: MutableList = 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?) {
diff --git a/server/core/src/main/java/dev/slimevr/osc/OSCHandler.java b/server/core/src/main/java/dev/slimevr/osc/OSCHandler.java
index babed55b4..fb0f0d245 100644
--- a/server/core/src/main/java/dev/slimevr/osc/OSCHandler.java
+++ b/server/core/src/main/java/dev/slimevr/osc/OSCHandler.java
@@ -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();
diff --git a/server/core/src/main/java/dev/slimevr/osc/VMCHandler.kt b/server/core/src/main/java/dev/slimevr/osc/VMCHandler.kt
index f90baffaa..c9047d60c 100644
--- a/server/core/src/main/java/dev/slimevr/osc/VMCHandler.kt
+++ b/server/core/src/main/java/dev/slimevr/osc/VMCHandler.kt
@@ -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) {
+ // 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)
diff --git a/server/core/src/main/java/dev/slimevr/osc/VRCOSCHandler.kt b/server/core/src/main/java/dev/slimevr/osc/VRCOSCHandler.kt
index 60d4058b1..e8109a886 100644
--- a/server/core/src/main/java/dev/slimevr/osc/VRCOSCHandler.kt
+++ b/server/core/src/main/java/dev/slimevr/osc/VRCOSCHandler.kt
@@ -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,
) : 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(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) {
+ // 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
}
diff --git a/server/core/src/main/java/dev/slimevr/osc/VRCOSCQueryHandler.kt b/server/core/src/main/java/dev/slimevr/osc/VRCOSCQueryHandler.kt
new file mode 100644
index 000000000..67bde3a7b
--- /dev/null
+++ b/server/core/src/main/java/dev/slimevr/osc/VRCOSCQueryHandler.kt
@@ -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()
+ }
+ }
+}
diff --git a/server/desktop/build.gradle.kts b/server/desktop/build.gradle.kts
index 5be02b69e..c983fccc1 100644
--- a/server/desktop/build.gradle.kts
+++ b/server/desktop/build.gradle.kts
@@ -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")
}
}
diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/Main.kt b/server/desktop/src/main/java/dev/slimevr/desktop/Main.kt
index 8f67cd21f..ce36aca4b 100644
--- a/server/desktop/src/main/java/dev/slimevr/desktop/Main.kt
+++ b/server/desktop/src/main/java/dev/slimevr/desktop/Main.kt
@@ -122,10 +122,10 @@ fun main(args: Array) {
::provideSteamVRBridge,
::provideFeederBridge,
{ _ -> DesktopSerialHandler() },
- configDir,
+ configPath = configDir,
)
vrServer.start()
-
+
// Start service for USB HID trackers
TrackersHID(
"Sensors HID service",