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",