From 576de2d647d46aaab9c62c57f68f9531458da4d7 Mon Sep 17 00:00:00 2001 From: Sapphire Date: Sat, 16 May 2026 22:24:51 -0500 Subject: [PATCH 1/6] Add origin field to Device, fix automatic tracker toggling with VIVEs --- .../main/java/dev/slimevr/osc/VMCHandler.kt | 3 +- .../java/dev/slimevr/osc/VRCOSCHandler.kt | 5 +- .../dev/slimevr/tracking/trackers/Device.kt | 16 +++- .../tracking/trackers/DeviceManager.kt | 4 +- .../tracking/trackers/hid/HIDDevice.kt | 3 +- .../tracking/trackers/udp/UDPDevice.kt | 3 +- .../slimevr/websocketapi/WebSocketVRBridge.kt | 3 +- .../slimevr/desktop/platform/SteamVRBridge.kt | 80 +++++++------------ 8 files changed, 52 insertions(+), 65 deletions(-) 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 52b40325e5..11626f9969 100644 --- a/server/core/src/main/java/dev/slimevr/osc/VMCHandler.kt +++ b/server/core/src/main/java/dev/slimevr/osc/VMCHandler.kt @@ -16,6 +16,7 @@ import dev.slimevr.osc.UnityBone.Companion.getByStringVal import dev.slimevr.tracking.processor.BoneType import dev.slimevr.tracking.processor.HumanPoseManager import dev.slimevr.tracking.trackers.Device +import dev.slimevr.tracking.trackers.DeviceOrigin import dev.slimevr.tracking.trackers.Tracker import dev.slimevr.tracking.trackers.TrackerPosition import dev.slimevr.tracking.trackers.TrackerStatus @@ -270,7 +271,7 @@ class VMCHandler( // Create device if it doesn't exist var rot = rotation if (trackerDevice == null) { - trackerDevice = server.deviceManager.createDevice("VMC receiver", "1.0", "VMC") + trackerDevice = server.deviceManager.createDevice(DeviceOrigin.VMC, "VMC receiver", "1.0", "VMC") server.deviceManager.addDevice(trackerDevice!!) } 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 ebb7b75653..548ec60f14 100644 --- a/server/core/src/main/java/dev/slimevr/osc/VRCOSCHandler.kt +++ b/server/core/src/main/java/dev/slimevr/osc/VRCOSCHandler.kt @@ -13,6 +13,7 @@ import com.jme3.system.NanoTimer import dev.slimevr.VRServer import dev.slimevr.config.VRCOSCConfig import dev.slimevr.tracking.trackers.Device +import dev.slimevr.tracking.trackers.DeviceOrigin import dev.slimevr.tracking.trackers.Tracker import dev.slimevr.tracking.trackers.TrackerPosition import dev.slimevr.tracking.trackers.TrackerStatus @@ -328,7 +329,7 @@ class VRCOSCHandler( // Create device if it doesn't exist if (vrsystemTrackersDevice == null) { // Instantiate OSC Trackers device - vrsystemTrackersDevice = server.deviceManager.createDevice("VRC VRSystem", null, "VRChat") + vrsystemTrackersDevice = server.deviceManager.createDevice(DeviceOrigin.VRCHAT, "VRC VRSystem", null, "VRChat") server.deviceManager.addDevice(vrsystemTrackersDevice!!) } @@ -404,7 +405,7 @@ class VRCOSCHandler( // 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") + oscTrackersDevice = server.deviceManager.createDevice(DeviceOrigin.OSC, "OSC Tracker", null, "OSC Trackers") server.deviceManager.addDevice(oscTrackersDevice!!) } diff --git a/server/core/src/main/java/dev/slimevr/tracking/trackers/Device.kt b/server/core/src/main/java/dev/slimevr/tracking/trackers/Device.kt index d92f60d04a..55c7092d1e 100644 --- a/server/core/src/main/java/dev/slimevr/tracking/trackers/Device.kt +++ b/server/core/src/main/java/dev/slimevr/tracking/trackers/Device.kt @@ -9,7 +9,18 @@ import kotlinx.coroutines.launch import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.atomic.AtomicInteger -open class Device(val magSupport: Boolean = false) { +enum class DeviceOrigin { + HID, + UDP, + VRCHAT, + OSC, + VMC, + STEAMVR, + SOLARXR, + WEBSOCKET, +} + +open class Device(val origin: DeviceOrigin, val magSupport: Boolean = false) { open val id: Int = nextLocalDeviceId.incrementAndGet() open var name: String? = null open var firmwareVersion: String? = null @@ -26,9 +37,6 @@ open class Device(val magSupport: Boolean = false) { open val hardwareIdentifier: String = "Unknown" - val isOpenVrDevice: Boolean - get() = manufacturer == "OpenVR" - init { CoroutineScope(Job()).launch { // Wait a little for device to get configured diff --git a/server/core/src/main/java/dev/slimevr/tracking/trackers/DeviceManager.kt b/server/core/src/main/java/dev/slimevr/tracking/trackers/DeviceManager.kt index dd409623a6..df154e76eb 100644 --- a/server/core/src/main/java/dev/slimevr/tracking/trackers/DeviceManager.kt +++ b/server/core/src/main/java/dev/slimevr/tracking/trackers/DeviceManager.kt @@ -5,8 +5,8 @@ import io.eiren.util.collections.FastList class DeviceManager(private val server: VRServer) { val devices = FastList() - fun createDevice(name: String?, version: String?, manufacturer: String?): Device { - val device = Device() + fun createDevice(origin: DeviceOrigin, name: String?, version: String?, manufacturer: String?): Device { + val device = Device(origin) device.name = name device.firmwareVersion = version device.manufacturer = manufacturer diff --git a/server/core/src/main/java/dev/slimevr/tracking/trackers/hid/HIDDevice.kt b/server/core/src/main/java/dev/slimevr/tracking/trackers/hid/HIDDevice.kt index a0fee7f7f8..d3b448331a 100644 --- a/server/core/src/main/java/dev/slimevr/tracking/trackers/hid/HIDDevice.kt +++ b/server/core/src/main/java/dev/slimevr/tracking/trackers/hid/HIDDevice.kt @@ -1,11 +1,12 @@ package dev.slimevr.tracking.trackers.hid import dev.slimevr.tracking.trackers.Device +import dev.slimevr.tracking.trackers.DeviceOrigin import dev.slimevr.tracking.trackers.Tracker import dev.slimevr.tracking.trackers.udp.BoardType import dev.slimevr.tracking.trackers.udp.MCUType -class HIDDevice(val hidId: Int) : Device() { +class HIDDevice(val hidId: Int) : Device(DeviceOrigin.HID) { override var hardwareIdentifier: String = "Unknown" override var boardType: BoardType = BoardType.UNKNOWN override var mcuType: MCUType = MCUType.UNKNOWN diff --git a/server/core/src/main/java/dev/slimevr/tracking/trackers/udp/UDPDevice.kt b/server/core/src/main/java/dev/slimevr/tracking/trackers/udp/UDPDevice.kt index 7eac78d833..65fad95897 100644 --- a/server/core/src/main/java/dev/slimevr/tracking/trackers/udp/UDPDevice.kt +++ b/server/core/src/main/java/dev/slimevr/tracking/trackers/udp/UDPDevice.kt @@ -3,6 +3,7 @@ package dev.slimevr.tracking.trackers.udp import dev.slimevr.NetworkProtocol import dev.slimevr.VRServer import dev.slimevr.tracking.trackers.Device +import dev.slimevr.tracking.trackers.DeviceOrigin import dev.slimevr.tracking.trackers.Tracker import java.net.InetAddress import java.net.SocketAddress @@ -14,7 +15,7 @@ class UDPDevice( override val hardwareIdentifier: String, override val boardType: BoardType = BoardType.UNKNOWN, override val mcuType: MCUType = MCUType.UNKNOWN, -) : Device(true) { +) : Device(DeviceOrigin.UDP, true) { override val id: Int = nextLocalDeviceId.incrementAndGet() diff --git a/server/core/src/main/java/dev/slimevr/websocketapi/WebSocketVRBridge.kt b/server/core/src/main/java/dev/slimevr/websocketapi/WebSocketVRBridge.kt index a0b080e156..6e71f82812 100644 --- a/server/core/src/main/java/dev/slimevr/websocketapi/WebSocketVRBridge.kt +++ b/server/core/src/main/java/dev/slimevr/websocketapi/WebSocketVRBridge.kt @@ -6,6 +6,7 @@ import dev.slimevr.VRServer import dev.slimevr.VRServer.Companion.getNextLocalTrackerId import dev.slimevr.VRServer.Companion.instance import dev.slimevr.bridge.Bridge +import dev.slimevr.tracking.trackers.DeviceOrigin import dev.slimevr.tracking.trackers.Tracker import dev.slimevr.tracking.trackers.TrackerPosition import dev.slimevr.tracking.trackers.TrackerStatus @@ -63,7 +64,7 @@ class WebSocketVRBridge( if (hmdTracker == null) { // Create HMD for websocket val hmdDevice = server.deviceManager - .createDevice("WebSocketVRBridge", null, null) + .createDevice(DeviceOrigin.WEBSOCKET, "WebSocketVRBridge", null, null) hmdTracker = Tracker( null, getNextLocalTrackerId(), diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/platform/SteamVRBridge.kt b/server/desktop/src/main/java/dev/slimevr/desktop/platform/SteamVRBridge.kt index 6c80417ee1..2fe0ba78e2 100644 --- a/server/desktop/src/main/java/dev/slimevr/desktop/platform/SteamVRBridge.kt +++ b/server/desktop/src/main/java/dev/slimevr/desktop/platform/SteamVRBridge.kt @@ -8,6 +8,7 @@ import dev.slimevr.bridge.ISteamVRBridge import dev.slimevr.config.BridgeConfig import dev.slimevr.desktop.platform.ProtobufMessages.* import dev.slimevr.protocol.rpc.settings.RPCSettingsHandler +import dev.slimevr.tracking.trackers.DeviceOrigin import dev.slimevr.tracking.trackers.Tracker import dev.slimevr.tracking.trackers.TrackerPosition import dev.slimevr.tracking.trackers.TrackerPosition.Companion.getByTrackerRole @@ -140,61 +141,33 @@ abstract class SteamVRBridge( if (!config.automaticSharedTrackersToggling || server.getPauseTracking()) return false val skeleton = instance.humanPoseManager.skeleton - val isWaistSteamVr = skeleton.hipTracker?.device?.isOpenVrDevice == true || - skeleton.waistTracker?.device?.isOpenVrDevice == true - // Enable waist if skeleton has an spine tracker - changeShareSettings(TrackerRole.WAIST, skeleton.hasSpineTracker && !isWaistSteamVr) - - // hasChest if waist and/or hip is on, and chest and/or upper chest is also on - val hasChest = skeleton.upperChestTracker != null || skeleton.chestTracker != null - val isChestSteamVr = skeleton.upperChestTracker?.device?.isOpenVrDevice == true || - skeleton.chestTracker?.device?.isOpenVrDevice == true - changeShareSettings( - TrackerRole.CHEST, - hasChest && !isChestSteamVr, - ) - - // hasFeet if lower and/or upper leg tracker is on - val hasLeftFoot = - (skeleton.leftUpperLegTracker != null || skeleton.leftLowerLegTracker != null) - val isLeftFootSteamVr = - skeleton.leftLowerLegTracker?.device?.isOpenVrDevice == true || - skeleton.leftFootTracker?.device?.isOpenVrDevice == true - - val hasRightFoot = - (skeleton.rightUpperLegTracker != null || skeleton.rightLowerLegTracker != null) - val isRightFootSteamVr = - skeleton.rightLowerLegTracker?.device?.isOpenVrDevice == true || - skeleton.rightFootTracker?.device?.isOpenVrDevice == true - changeShareSettings( - TrackerRole.LEFT_FOOT, - hasLeftFoot && !isLeftFootSteamVr, - ) - changeShareSettings( - TrackerRole.RIGHT_FOOT, - hasRightFoot && !isRightFootSteamVr, + // Hands aren't touched as they will override the controller's tracking + val roleToTrackers = mapOf( + TrackerRole.CHEST to setOf(skeleton.upperChestTracker, skeleton.chestTracker), + TrackerRole.LEFT_ELBOW to setOf(skeleton.leftUpperArmTracker), + TrackerRole.RIGHT_ELBOW to setOf(skeleton.rightUpperArmTracker), + TrackerRole.WAIST to setOf(skeleton.waistTracker, skeleton.hipTracker), + TrackerRole.LEFT_KNEE to setOf(skeleton.leftUpperLegTracker), + TrackerRole.RIGHT_KNEE to setOf(skeleton.rightUpperLegTracker), + TrackerRole.LEFT_FOOT to setOf(skeleton.leftLowerLegTracker, skeleton.leftFootTracker), + TrackerRole.RIGHT_FOOT to setOf(skeleton.rightLowerLegTracker, skeleton.rightFootTracker), ) - // hasKnees is just hasFeet - val isLeftKneeSteamVr = skeleton.leftUpperLegTracker?.device?.isOpenVrDevice == true - - val isRightKneeSteamVr = skeleton.rightUpperLegTracker?.device?.isOpenVrDevice == true - changeShareSettings(TrackerRole.LEFT_KNEE, hasLeftFoot && !isLeftKneeSteamVr) - changeShareSettings(TrackerRole.RIGHT_KNEE, hasRightFoot && !isRightKneeSteamVr) - - // hasElbows if an upper arm or a lower arm tracker is on - val hasLeftElbow = skeleton.hasLeftArmTracker - val isLeftElbowSteamVr = skeleton.leftUpperArmTracker?.device?.isOpenVrDevice == true || - skeleton.leftLowerArmTracker?.device?.isOpenVrDevice == true - - val hasRightElbow = skeleton.hasRightArmTracker - val isRightElbowSteamVr = skeleton.rightUpperArmTracker?.device?.isOpenVrDevice == true || - skeleton.rightLowerArmTracker?.device?.isOpenVrDevice == true - changeShareSettings(TrackerRole.LEFT_ELBOW, hasLeftElbow && !isLeftElbowSteamVr) - changeShareSettings(TrackerRole.RIGHT_ELBOW, hasRightElbow && !isRightElbowSteamVr) - - // Hands aren't touched as they will override the controller's tracking - // Return true to say that trackers were successfully toggled automatically + for ((role, trackers) in roleToTrackers) { + val shouldShare = if (role == TrackerRole.WAIST) { + // Waist is special, it should be enabled if there is any tracker on the spine, + // but ignored if the waist or hip tracker is from us + skeleton.hasSpineTracker && + !trackers.any { + it?.device?.origin == DeviceOrigin.STEAMVR + } + } else { + trackers.any { + it != null && it.device?.origin != DeviceOrigin.STEAMVR + } + } + changeShareSettings(role, shouldShare) + } return true } @@ -228,6 +201,7 @@ abstract class SteamVRBridge( override fun createNewTracker(trackerAdded: TrackerAdded): Tracker { val device = instance.deviceManager .createDevice( + DeviceOrigin.STEAMVR, trackerAdded.trackerName, null, trackerAdded.manufacturer.ifEmpty { "OpenVR" }, From 68df7b06d2c94d20b9c6b8e0000ef4b994644a2b Mon Sep 17 00:00:00 2001 From: Sapphire Date: Sat, 16 May 2026 23:10:09 -0500 Subject: [PATCH 2/6] Nuke WebSocketVRBridge --- .../src/main/java/dev/slimevr/VRServer.kt | 14 +- .../dev/slimevr/tracking/trackers/Device.kt | 1 - ...nnection.java => WebSocketConnection.java} | 4 +- ...socketAPI.java => WebSocketRPCBridge.java} | 55 +++- .../slimevr/websocketapi/WebSocketVRBridge.kt | 244 ------------------ 5 files changed, 54 insertions(+), 264 deletions(-) rename server/core/src/main/java/dev/slimevr/websocketapi/{WebsocketConnection.java => WebSocketConnection.java} (88%) rename server/core/src/main/java/dev/slimevr/websocketapi/{WebsocketAPI.java => WebSocketRPCBridge.java} (71%) delete mode 100644 server/core/src/main/java/dev/slimevr/websocketapi/WebSocketVRBridge.kt diff --git a/server/core/src/main/java/dev/slimevr/VRServer.kt b/server/core/src/main/java/dev/slimevr/VRServer.kt index f5e5cf6a71..4f57de9f3e 100644 --- a/server/core/src/main/java/dev/slimevr/VRServer.kt +++ b/server/core/src/main/java/dev/slimevr/VRServer.kt @@ -34,7 +34,7 @@ import dev.slimevr.tracking.trackers.* import dev.slimevr.tracking.trackers.udp.TrackersUDPServer import dev.slimevr.trackingchecklist.TrackingChecklistManager import dev.slimevr.util.ann.VRServerThread -import dev.slimevr.websocketapi.WebSocketVRBridge +import dev.slimevr.websocketapi.WebSocketRPCBridge import io.eiren.util.ann.ThreadSafe import io.eiren.util.ann.ThreadSecure import io.eiren.util.collections.FastList @@ -156,7 +156,8 @@ class VRServer @JvmOverloads constructor( ) { tracker: Tracker -> registerTracker(tracker) } // Start bridges and WebSocket server - for (bridge in bridgeProvider(this, computedTrackers) + sequenceOf(WebSocketVRBridge(computedTrackers, this))) { + for (bridge in bridgeProvider(this, computedTrackers) + + sequenceOf(WebSocketRPCBridge(this, protocolAPI))) { tasks.add(Runnable { bridge.startBridge() }) bridges.add(bridge) } @@ -186,15 +187,6 @@ class VRServer @JvmOverloads constructor( instance = this } - fun hasBridge(bridgeClass: Class): Boolean { - for (bridge in bridges) { - if (bridgeClass.isAssignableFrom(bridge.javaClass)) { - return true - } - } - return false - } - @ThreadSafe fun getVRBridge(pred: (Bridge) -> Boolean): Bridge? { for (bridge in bridges) { diff --git a/server/core/src/main/java/dev/slimevr/tracking/trackers/Device.kt b/server/core/src/main/java/dev/slimevr/tracking/trackers/Device.kt index 55c7092d1e..2cb36ab865 100644 --- a/server/core/src/main/java/dev/slimevr/tracking/trackers/Device.kt +++ b/server/core/src/main/java/dev/slimevr/tracking/trackers/Device.kt @@ -17,7 +17,6 @@ enum class DeviceOrigin { VMC, STEAMVR, SOLARXR, - WEBSOCKET, } open class Device(val origin: DeviceOrigin, val magSupport: Boolean = false) { diff --git a/server/core/src/main/java/dev/slimevr/websocketapi/WebsocketConnection.java b/server/core/src/main/java/dev/slimevr/websocketapi/WebSocketConnection.java similarity index 88% rename from server/core/src/main/java/dev/slimevr/websocketapi/WebsocketConnection.java rename to server/core/src/main/java/dev/slimevr/websocketapi/WebSocketConnection.java index 73340b7443..b6390dc412 100644 --- a/server/core/src/main/java/dev/slimevr/websocketapi/WebsocketConnection.java +++ b/server/core/src/main/java/dev/slimevr/websocketapi/WebSocketConnection.java @@ -9,13 +9,13 @@ import java.util.UUID; -public class WebsocketConnection implements GenericConnection { +public class WebSocketConnection implements GenericConnection { public final ConnectionContext context; public final WebSocket conn; public UUID id; - public WebsocketConnection(WebSocket conn) { + public WebSocketConnection(WebSocket conn) { this.context = new ConnectionContext(); this.conn = conn; this.id = UUID.randomUUID(); diff --git a/server/core/src/main/java/dev/slimevr/websocketapi/WebsocketAPI.java b/server/core/src/main/java/dev/slimevr/websocketapi/WebSocketRPCBridge.java similarity index 71% rename from server/core/src/main/java/dev/slimevr/websocketapi/WebsocketAPI.java rename to server/core/src/main/java/dev/slimevr/websocketapi/WebSocketRPCBridge.java index 0e3cf870e4..52030fb493 100644 --- a/server/core/src/main/java/dev/slimevr/websocketapi/WebsocketAPI.java +++ b/server/core/src/main/java/dev/slimevr/websocketapi/WebSocketRPCBridge.java @@ -1,9 +1,12 @@ package dev.slimevr.websocketapi; import dev.slimevr.VRServer; +import dev.slimevr.bridge.Bridge; import dev.slimevr.protocol.GenericConnection; import dev.slimevr.protocol.ProtocolAPI; import dev.slimevr.protocol.ProtocolAPIServer; +import dev.slimevr.tracking.trackers.Tracker; +import dev.slimevr.util.ann.VRServerThread; import io.eiren.util.logging.LogManager; import org.java_websocket.WebSocket; import org.java_websocket.drafts.Draft_6455; @@ -18,12 +21,11 @@ import java.util.stream.Stream; -public class WebsocketAPI extends WebSocketServer implements ProtocolAPIServer { - +public class WebSocketRPCBridge extends WebSocketServer implements ProtocolAPIServer, Bridge { public final VRServer server; public final ProtocolAPI protocolAPI; - public WebsocketAPI(VRServer server, ProtocolAPI protocolAPI) { + public WebSocketRPCBridge(VRServer server, ProtocolAPI protocolAPI) { super(new InetSocketAddress(21110), Collections.singletonList(new Draft_6455())); this.server = server; this.protocolAPI = protocolAPI; @@ -39,7 +41,7 @@ public void onOpen(WebSocket conn, ClientHandshake handshake) { "[WebSocketAPI] New connection from: " + conn.getRemoteSocketAddress().getAddress().getHostAddress() ); - conn.setAttachment(new WebsocketConnection(conn)); + conn.setAttachment(new WebSocketConnection(conn)); } /** @@ -81,7 +83,7 @@ public void onMessage(WebSocket conn, String message) { @Override public void onMessage(WebSocket conn, ByteBuffer message) { - var connection = conn.getAttachment(); + var connection = conn.getAttachment(); if (connection != null) this.protocolAPI.onMessage(connection, message); } @@ -101,10 +103,51 @@ public void onStart() { setConnectionLostTimeout(0); } + @Override + @VRServerThread + public void dataRead() { + // noop + } + + @Override + @VRServerThread + public void dataWrite() { + // noop + } + + @Override + public void addSharedTracker(Tracker tracker) { + // noop + } + + @Override + public void removeSharedTracker(Tracker tracker) { + // noop + } + + @Override + public void startBridge() { + super.start(); + } + + @Override + public void stopBridge() { + try { + super.stop(); + } catch (InterruptedException e) { + // noop + } + } + + @Override + public boolean isConnected() { + return this.getApiConnections().count() > 0; + } + @Override public @NotNull Stream getApiConnections() { return this.getConnections().stream().map(conn -> { - var c = conn.getAttachment(); + var c = conn.getAttachment(); return (GenericConnection) c; }).filter(Objects::nonNull); } diff --git a/server/core/src/main/java/dev/slimevr/websocketapi/WebSocketVRBridge.kt b/server/core/src/main/java/dev/slimevr/websocketapi/WebSocketVRBridge.kt deleted file mode 100644 index 6e71f82812..0000000000 --- a/server/core/src/main/java/dev/slimevr/websocketapi/WebSocketVRBridge.kt +++ /dev/null @@ -1,244 +0,0 @@ -package dev.slimevr.websocketapi - -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.databind.node.ObjectNode -import dev.slimevr.VRServer -import dev.slimevr.VRServer.Companion.getNextLocalTrackerId -import dev.slimevr.VRServer.Companion.instance -import dev.slimevr.bridge.Bridge -import dev.slimevr.tracking.trackers.DeviceOrigin -import dev.slimevr.tracking.trackers.Tracker -import dev.slimevr.tracking.trackers.TrackerPosition -import dev.slimevr.tracking.trackers.TrackerStatus -import io.eiren.util.collections.FastList -import io.eiren.util.logging.LogManager -import io.github.axisangles.ktmath.Quaternion -import io.github.axisangles.ktmath.Vector3 -import org.java_websocket.WebSocket -import org.java_websocket.handshake.ClientHandshake -import java.util.* -import java.util.concurrent.atomic.AtomicBoolean - -class WebSocketVRBridge( - computedTrackers: List, - server: VRServer, -) : WebsocketAPI(server, server.protocolAPI), - Bridge { - private val computedTrackers: List = FastList(computedTrackers) - private val internalTrackers: MutableList = FastList(computedTrackers.size) - private val newHMDData = AtomicBoolean(false) - private val mapper = ObjectMapper() - private val internalHMDTracker = Tracker( - null, - 0, - "internal://HMD", - "internal://HMD", - TrackerPosition.HEAD, - hasPosition = true, - hasRotation = true, - isInternal = true, - isComputed = true, - ) - private var hmdTracker: Tracker? = null - - init { - for (t in computedTrackers) { - val ct = Tracker( - null, - t.id, - "internal://${t.name}", - "internal://${t.name}", - t.trackerPosition, - hasPosition = true, - hasRotation = true, - userEditable = true, - isInternal = true, - ) - ct.status = TrackerStatus.OK - internalTrackers.add(ct) - } - } - - override fun dataRead() { - if (newHMDData.compareAndSet(true, false)) { - if (hmdTracker == null) { - // Create HMD for websocket - val hmdDevice = server.deviceManager - .createDevice(DeviceOrigin.WEBSOCKET, "WebSocketVRBridge", null, null) - hmdTracker = Tracker( - null, - getNextLocalTrackerId(), - "WebSocketHMD", - "WebSocketHMD", - TrackerPosition.HEAD, - hasPosition = true, - hasRotation = true, - userEditable = true, - isComputed = true, - ) - hmdTracker!!.status = TrackerStatus.OK - hmdDevice.trackers[0] = hmdTracker!! - server.registerTracker(hmdTracker!!) - } - - hmdTracker!!.position = internalHMDTracker.position - hmdTracker!!.setRotation(internalHMDTracker.getRotation()) - hmdTracker!!.dataTick() - } - } - - override fun dataWrite() { - for (i in computedTrackers.indices) { - val t = computedTrackers[i] - val it = internalTrackers[i] - if (t.hasPosition) it.position = t.position - if (t.hasRotation) it.setRotation(t.getRotation()) - } - } - - override fun onOpen(conn: WebSocket, handshake: ClientHandshake) { - super.onOpen(conn, handshake) - // Register trackers - for (i in internalTrackers.indices) { - val message = mapper.nodeFactory.objectNode() - message.put("type", "config") - message.put("tracker_id", "SlimeVR Tracker " + (i + 1)) - message - .put( - "location", - computedTrackers[i] - .trackerPosition - ?.trackerRole - ?.name - ?.lowercase(Locale.getDefault()), - ) - message.put("tracker_type", message["location"].asText()) - conn.send(message.toString()) - } - } - - override fun onMessage(conn: WebSocket, message: String) { - // LogManager.info(message); - try { - val json = mapper.readTree(message) as ObjectNode - if (json.has("type")) { - when (json["type"].asText()) { - "pos" -> { - parsePosition(json, conn) - return - } - - "action" -> { - parseAction(json, conn) - return - } - - // TODO Ignore it for now, it should only register HMD in our test case with id 0 - "config" -> { - LogManager.info("[WebSocket] Config received: $json") - return - } - } - } - LogManager - .warning( - "[WebSocket] Unrecognized message from " + - conn.remoteSocketAddress.address.hostAddress + - ": " + - message, - ) - } catch (e: Exception) { - LogManager - .severe( - "[WebSocket] Exception parsing message from " + - conn.remoteSocketAddress.address.hostAddress + - ". Message: " + - message, - e, - ) - } - } - - private fun parsePosition(json: ObjectNode, conn: WebSocket) { - if (json["tracker_id"].asInt() == 0) { - // Read HMD information - internalHMDTracker - .position = Vector3( - json["x"].asDouble().toFloat(), - json["y"].asDouble().toFloat() + 0.2f, - json["z"].asDouble().toFloat(), - ) - // TODO Wtf is this hack? VRWorkout issue? - internalHMDTracker - .setRotation( - Quaternion( - json["qw"].asDouble().toFloat(), - json["qx"].asDouble().toFloat(), - json["qy"].asDouble().toFloat(), - json["qz"].asDouble().toFloat(), - ), - ) - internalHMDTracker.dataTick() - newHMDData.set(true) - - // Send tracker info in reply - for (i in internalTrackers.indices) { - val message = mapper.nodeFactory.objectNode() - message.put("type", "pos") - message.put("src", "full") - message.put("tracker_id", "SlimeVR Tracker ${i + 1}") - - val t = internalTrackers[i] - message.put("x", t.position.x) - message.put("y", t.position.y) - message.put("z", t.position.z) - message.put("qx", t.getRotation().x) - message.put("qy", t.getRotation().y) - message.put("qz", t.getRotation().z) - message.put("qw", t.getRotation().w) - - conn.send(message.toString()) - } - } - } - - private fun parseAction(json: ObjectNode, conn: WebSocket) { - when (json["name"].asText()) { - "calibrate" -> instance.resetTrackersYaw(RESET_SOURCE_NAME) - "full_calibrate" -> instance.resetTrackersFull(RESET_SOURCE_NAME) - "mounting_calibrate" -> instance.resetTrackersMounting(RESET_SOURCE_NAME) - "mounting_clear" -> instance.clearTrackersMounting(RESET_SOURCE_NAME) - "toggle_pause_tracking" -> instance.togglePauseTracking(RESET_SOURCE_NAME) - } - } - - override fun onStart() { - LogManager.info("[WebSocket] Web Socket VR Bridge started on port $port") - connectionLostTimeout = 0 - connectionLostTimeout = 1 - // This has to be removed for Android - // (keepalive did not work for me @mgschwan) - } - - override fun addSharedTracker(tracker: Tracker?) { - // TODO Auto-generated method stub - } - - override fun removeSharedTracker(tracker: Tracker?) { - // TODO Auto-generated method stub - } - - override fun startBridge() { - start() - } - - override fun stopBridge() { - stop() - } - - override fun isConnected(): Boolean = super.getConnections().isNotEmpty() - - companion object { - private const val RESET_SOURCE_NAME = "WebSocketVRBridge" - } -} From 1019d1d4d116b2138a1daa2c6b63782e1375208e Mon Sep 17 00:00:00 2001 From: Sapphire Date: Sun, 17 May 2026 00:45:33 -0500 Subject: [PATCH 3/6] [WIP] Allow creating and manipulating created trackers via SolarXR --- .../dev/slimevr/protocol/ConnectionContext.kt | 3 + .../dev/slimevr/protocol/rpc/RPCHandler.kt | 60 ++++---- .../protocol/rpc/RPCUserHeightCalibration.kt | 2 +- .../rpc/autobone/RPCAutoBoneHandler.kt | 7 +- .../rpc/firmware/RPCFirmwareUpdateHandler.kt | 3 +- .../rpc/games/vrchat/RPCVRChatHandler.kt | 5 +- .../rpc/installinfo/RPCInstallInfoHandler.kt | 3 +- .../protocol/rpc/reset/RPCResetHandler.kt | 3 +- .../rpc/serial/RPCProvisioningHandler.kt | 5 +- .../protocol/rpc/serial/RPCSerialHandler.kt | 19 ++- .../rpc/settings/RPCSettingsHandler.kt | 3 +- .../protocol/rpc/setup/RPCHandshakeHandler.kt | 3 +- .../protocol/rpc/setup/RPCTapSetupHandler.kt | 3 +- .../protocol/rpc/status/RPCStatusHandler.kt | 5 +- .../rpc/tracker/RPCExternalTrackerHandler.kt | 133 ++++++++++++++++++ .../RPCTrackingChecklistHandler.kt | 7 +- .../rpc/trackingpause/RPCTrackingPause.kt | 3 +- .../dev/slimevr/tracking/trackers/Device.kt | 2 +- .../dev/slimevr/tracking/trackers/Tracker.kt | 2 + .../tracking/trackers/udp/UDPDevice.kt | 4 +- .../windows/WindowsNamedPipeRpcBridge.java | 8 +- solarxr-protocol | 2 +- 22 files changed, 219 insertions(+), 66 deletions(-) create mode 100644 server/core/src/main/java/dev/slimevr/protocol/rpc/tracker/RPCExternalTrackerHandler.kt diff --git a/server/core/src/main/java/dev/slimevr/protocol/ConnectionContext.kt b/server/core/src/main/java/dev/slimevr/protocol/ConnectionContext.kt index ea72472db7..9214270ec3 100644 --- a/server/core/src/main/java/dev/slimevr/protocol/ConnectionContext.kt +++ b/server/core/src/main/java/dev/slimevr/protocol/ConnectionContext.kt @@ -1,8 +1,11 @@ package dev.slimevr.protocol +import dev.slimevr.tracking.trackers.Tracker + class ConnectionContext { val dataFeedList: MutableList = mutableListOf() val subscribedTopics: MutableList = mutableListOf() + val createdTrackers: MutableList = mutableListOf() var useSerial: Boolean = false var useProvisioning: Boolean = false diff --git a/server/core/src/main/java/dev/slimevr/protocol/rpc/RPCHandler.kt b/server/core/src/main/java/dev/slimevr/protocol/rpc/RPCHandler.kt index 57be55772a..68583d82ac 100644 --- a/server/core/src/main/java/dev/slimevr/protocol/rpc/RPCHandler.kt +++ b/server/core/src/main/java/dev/slimevr/protocol/rpc/RPCHandler.kt @@ -20,6 +20,7 @@ import dev.slimevr.protocol.rpc.setup.RPCHandshakeHandler import dev.slimevr.protocol.rpc.setup.RPCTapSetupHandler import dev.slimevr.protocol.rpc.setup.RPCUtil.getLocalIp import dev.slimevr.protocol.rpc.status.RPCStatusHandler +import dev.slimevr.protocol.rpc.tracker.RPCExternalTrackerHandler import dev.slimevr.protocol.rpc.trackingchecklist.RPCTrackingChecklistHandler import dev.slimevr.protocol.rpc.trackingpause.RPCTrackingPause import dev.slimevr.tracking.processor.config.SkeletonConfigOffsets @@ -36,6 +37,28 @@ import solarxr_protocol.datatypes.TransactionId import solarxr_protocol.rpc.* import kotlin.io.path.Path +fun createRPCMessage(fbb: FlatBufferBuilder, messageType: Byte, messageOffset: Int, txId: Long?): Int { + val data = IntArray(1) + + RpcMessageHeader.startRpcMessageHeader(fbb) + RpcMessageHeader.addMessage(fbb, messageOffset) + RpcMessageHeader.addMessageType(fbb, messageType) + txId?.let { txId -> + RpcMessageHeader.addTxId(fbb, TransactionId.createTransactionId(fbb, txId)) + } + data[0] = RpcMessageHeader.endRpcMessageHeader(fbb) + + val messages = MessageBundle.createRpcMsgsVector(fbb, data) + + MessageBundle.startMessageBundle(fbb) + MessageBundle.addRpcMsgs(fbb, messages) + return MessageBundle.endMessageBundle(fbb) +} + +fun createRPCMessage(fbb: FlatBufferBuilder, messageType: Byte, messageOffset: Int): Int = createRPCMessage(fbb, messageType, messageOffset, txId = null) + +fun createRPCMessage(fbb: FlatBufferBuilder, messageType: Byte, messageOffset: Int, respondTo: RpcMessageHeader?): Int = createRPCMessage(fbb, messageType, messageOffset, respondTo?.txId()?.id()) + class RPCHandler(private val api: ProtocolAPI) : ProtocolHandler() { private val mainScope = CoroutineScope(SupervisorJob()) @@ -54,6 +77,7 @@ class RPCHandler(private val api: ProtocolAPI) : ProtocolHandler - RpcMessageHeader.addTxId(fbb, TransactionId.createTransactionId(fbb, txId)) - } - data[0] = RpcMessageHeader.endRpcMessageHeader(fbb) - - val messages = MessageBundle.createRpcMsgsVector(fbb, data) - - MessageBundle.startMessageBundle(fbb) - MessageBundle.addRpcMsgs(fbb, messages) - return MessageBundle.endMessageBundle(fbb) - } - - fun createRPCMessage(fbb: FlatBufferBuilder, messageType: Byte, messageOffset: Int): Int = createRPCMessage(fbb, messageType, messageOffset, txId = null) - - fun createRPCMessage(fbb: FlatBufferBuilder, messageType: Byte, messageOffset: Int, respondTo: RpcMessageHeader?): Int = createRPCMessage(fbb, messageType, messageOffset, respondTo?.txId()?.id()) - override fun messagesCount(): Int = RpcMessage.names.size fun onStatusSystemRequest(conn: GenericConnection, messageHeader: RpcMessageHeader) { @@ -412,7 +414,7 @@ class RPCHandler(private val api: ProtocolAPI) : ProtocolHandler conn.send(fbb.dataBuffer()) }) diff --git a/server/core/src/main/java/dev/slimevr/protocol/rpc/serial/RPCSerialHandler.kt b/server/core/src/main/java/dev/slimevr/protocol/rpc/serial/RPCSerialHandler.kt index e621390af3..451dd43f48 100644 --- a/server/core/src/main/java/dev/slimevr/protocol/rpc/serial/RPCSerialHandler.kt +++ b/server/core/src/main/java/dev/slimevr/protocol/rpc/serial/RPCSerialHandler.kt @@ -5,6 +5,7 @@ import dev.slimevr.protocol.GenericConnection import dev.slimevr.protocol.ProtocolAPI import dev.slimevr.protocol.ProtocolAPIServer import dev.slimevr.protocol.rpc.RPCHandler +import dev.slimevr.protocol.rpc.createRPCMessage import dev.slimevr.serial.SerialListener import dev.slimevr.serial.SerialPort import io.eiren.util.logging.LogManager @@ -32,7 +33,7 @@ class RPCSerialHandler(var rpcHandler: RPCHandler, var api: ProtocolAPI) : Seria SerialUpdateResponse.startSerialUpdateResponse(fbb) SerialUpdateResponse.addClosed(fbb, true) val update = SerialUpdateResponse.endSerialUpdateResponse(fbb) - val outbound = rpcHandler.createRPCMessage(fbb, RpcMessage.SerialUpdateResponse, update) + val outbound = createRPCMessage(fbb, RpcMessage.SerialUpdateResponse, update) fbb.finish(outbound) this.forAllListeners( @@ -51,7 +52,7 @@ class RPCSerialHandler(var rpcHandler: RPCHandler, var api: ProtocolAPI) : Seria SerialUpdateResponse.startSerialUpdateResponse(fbb) SerialUpdateResponse.addLog(fbb, logOffset) val update = SerialUpdateResponse.endSerialUpdateResponse(fbb) - val outbound = rpcHandler.createRPCMessage(fbb, RpcMessage.SerialUpdateResponse, update) + val outbound = createRPCMessage(fbb, RpcMessage.SerialUpdateResponse, update) fbb.finish(outbound) this.forAllListeners( @@ -69,8 +70,8 @@ class RPCSerialHandler(var rpcHandler: RPCHandler, var api: ProtocolAPI) : Seria val deviceOffset = SerialDevice.createSerialDevice(fbb, portOffset, nameOffset) val newSerialOffset = NewSerialDeviceResponse .createNewSerialDeviceResponse(fbb, deviceOffset) - val outbound = rpcHandler - .createRPCMessage(fbb, RpcMessage.NewSerialDeviceResponse, newSerialOffset) + val outbound = + createRPCMessage(fbb, RpcMessage.NewSerialDeviceResponse, newSerialOffset) fbb.finish(outbound) this.api @@ -92,7 +93,7 @@ class RPCSerialHandler(var rpcHandler: RPCHandler, var api: ProtocolAPI) : Seria SerialUpdateResponse.startSerialUpdateResponse(fbb) SerialUpdateResponse.addClosed(fbb, false) val update = SerialUpdateResponse.endSerialUpdateResponse(fbb) - val outbound = rpcHandler.createRPCMessage(fbb, RpcMessage.SerialUpdateResponse, update) + val outbound = createRPCMessage(fbb, RpcMessage.SerialUpdateResponse, update) fbb.finish(outbound) this.forAllListeners( @@ -181,8 +182,7 @@ class RPCSerialHandler(var rpcHandler: RPCHandler, var api: ProtocolAPI) : Seria devicesOffsets.forEach(Consumer { offset: Int -> SerialDevicesResponse.addDevices(fbb, offset) }) val devices = fbb.endVector() val serialDeviceOffsets = SerialDevicesResponse.createSerialDevicesResponse(fbb, devices) - val outbound = rpcHandler - .createRPCMessage(fbb, RpcMessage.SerialDevicesResponse, serialDeviceOffsets) + val outbound = createRPCMessage(fbb, RpcMessage.SerialDevicesResponse, serialDeviceOffsets) fbb.finish(outbound) conn.send(fbb.dataBuffer()) } @@ -220,8 +220,7 @@ class RPCSerialHandler(var rpcHandler: RPCHandler, var api: ProtocolAPI) : Seria !this.api.server.serialHandler.isConnected, ) val update = SerialUpdateResponse.endSerialUpdateResponse(fbb) - val outbound = rpcHandler - .createRPCMessage(fbb, RpcMessage.SerialUpdateResponse, update) + val outbound = createRPCMessage(fbb, RpcMessage.SerialUpdateResponse, update) fbb.finish(outbound) conn.send(fbb.dataBuffer()) } @@ -240,7 +239,7 @@ class RPCSerialHandler(var rpcHandler: RPCHandler, var api: ProtocolAPI) : Seria SerialUpdateResponse.startSerialUpdateResponse(fbb) SerialUpdateResponse.addClosed(fbb, !this.api.server.serialHandler.isConnected) val update = SerialUpdateResponse.endSerialUpdateResponse(fbb) - val outbound = rpcHandler.createRPCMessage(fbb, RpcMessage.SerialUpdateResponse, update) + val outbound = createRPCMessage(fbb, RpcMessage.SerialUpdateResponse, update) fbb.finish(outbound) conn.send(fbb.dataBuffer()) } diff --git a/server/core/src/main/java/dev/slimevr/protocol/rpc/settings/RPCSettingsHandler.kt b/server/core/src/main/java/dev/slimevr/protocol/rpc/settings/RPCSettingsHandler.kt index 4b0a95659b..05fd48acdf 100644 --- a/server/core/src/main/java/dev/slimevr/protocol/rpc/settings/RPCSettingsHandler.kt +++ b/server/core/src/main/java/dev/slimevr/protocol/rpc/settings/RPCSettingsHandler.kt @@ -7,6 +7,7 @@ import dev.slimevr.filtering.TrackerFilters import dev.slimevr.protocol.GenericConnection import dev.slimevr.protocol.ProtocolAPI import dev.slimevr.protocol.rpc.RPCHandler +import dev.slimevr.protocol.rpc.createRPCMessage import dev.slimevr.tracking.processor.config.SkeletonConfigToggles import dev.slimevr.tracking.processor.config.SkeletonConfigValues import dev.slimevr.tracking.trackers.TrackerPosition @@ -401,7 +402,7 @@ class RPCSettingsHandler(var rpcHandler: RPCHandler, var api: ProtocolAPI) { createSteamVRSettings(fbb, bridge), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ) val outbound = - rpcHandler.createRPCMessage(fbb, RpcMessage.SettingsResponse, settings) + createRPCMessage(fbb, RpcMessage.SettingsResponse, settings) fbb.finish(outbound) api.apiServers.forEach { apiServer -> apiServer.apiConnections.forEach { it.send(fbb.dataBuffer()) } diff --git a/server/core/src/main/java/dev/slimevr/protocol/rpc/setup/RPCHandshakeHandler.kt b/server/core/src/main/java/dev/slimevr/protocol/rpc/setup/RPCHandshakeHandler.kt index 8cfb4f7786..2fa23b4de8 100644 --- a/server/core/src/main/java/dev/slimevr/protocol/rpc/setup/RPCHandshakeHandler.kt +++ b/server/core/src/main/java/dev/slimevr/protocol/rpc/setup/RPCHandshakeHandler.kt @@ -4,6 +4,7 @@ import com.google.flatbuffers.FlatBufferBuilder import dev.slimevr.protocol.GenericConnection import dev.slimevr.protocol.ProtocolAPI import dev.slimevr.protocol.rpc.RPCHandler +import dev.slimevr.protocol.rpc.createRPCMessage import dev.slimevr.setup.HandshakeListener import dev.slimevr.tracking.trackers.udp.UDPDevice import solarxr_protocol.rpc.AddUnknownDeviceRequest @@ -38,7 +39,7 @@ class RPCHandshakeHandler( fbb, string, ) - val outbound = rpcHandler.createRPCMessage( + val outbound = createRPCMessage( fbb, RpcMessage.UnknownDeviceHandshakeNotification, update, diff --git a/server/core/src/main/java/dev/slimevr/protocol/rpc/setup/RPCTapSetupHandler.kt b/server/core/src/main/java/dev/slimevr/protocol/rpc/setup/RPCTapSetupHandler.kt index ea5ac3adc5..1a4fe54a50 100644 --- a/server/core/src/main/java/dev/slimevr/protocol/rpc/setup/RPCTapSetupHandler.kt +++ b/server/core/src/main/java/dev/slimevr/protocol/rpc/setup/RPCTapSetupHandler.kt @@ -5,6 +5,7 @@ import dev.slimevr.protocol.GenericConnection import dev.slimevr.protocol.ProtocolAPI import dev.slimevr.protocol.datafeed.createTrackerId import dev.slimevr.protocol.rpc.RPCHandler +import dev.slimevr.protocol.rpc.createRPCMessage import dev.slimevr.setup.TapSetupListener import dev.slimevr.tracking.trackers.Tracker import solarxr_protocol.rpc.RpcMessage @@ -23,7 +24,7 @@ class RPCTapSetupHandler( val idOffset = createTrackerId(fbb, tracker) val update = TapDetectionSetupNotification.createTapDetectionSetupNotification(fbb, idOffset) val outbound = - rpcHandler.createRPCMessage(fbb, RpcMessage.TapDetectionSetupNotification, update) + createRPCMessage(fbb, RpcMessage.TapDetectionSetupNotification, update) fbb.finish(outbound) forAllListeners { diff --git a/server/core/src/main/java/dev/slimevr/protocol/rpc/status/RPCStatusHandler.kt b/server/core/src/main/java/dev/slimevr/protocol/rpc/status/RPCStatusHandler.kt index f9df6f8341..84459e340c 100644 --- a/server/core/src/main/java/dev/slimevr/protocol/rpc/status/RPCStatusHandler.kt +++ b/server/core/src/main/java/dev/slimevr/protocol/rpc/status/RPCStatusHandler.kt @@ -3,6 +3,7 @@ package dev.slimevr.protocol.rpc.status import com.google.flatbuffers.FlatBufferBuilder import dev.slimevr.protocol.ProtocolAPI import dev.slimevr.protocol.rpc.RPCHandler +import dev.slimevr.protocol.rpc.createRPCMessage import dev.slimevr.status.StatusListener import solarxr_protocol.rpc.RpcMessage import solarxr_protocol.rpc.StatusDataUnion @@ -39,7 +40,7 @@ class RPCStatusHandler( StatusSystemUpdate.addNewStatus(fbb, statusOffset) val update = StatusSystemUpdate.endStatusSystemUpdate(fbb) - val outbound = this.rpcHandler.createRPCMessage( + val outbound = createRPCMessage( fbb, RpcMessage.StatusSystemUpdate, update, @@ -58,7 +59,7 @@ class RPCStatusHandler( StatusSystemFixed.addFixedStatusId(fbb, id.toLong()) val update = StatusSystemFixed.endStatusSystemFixed(fbb) - val outbound = this.rpcHandler.createRPCMessage( + val outbound = createRPCMessage( fbb, RpcMessage.StatusSystemFixed, update, diff --git a/server/core/src/main/java/dev/slimevr/protocol/rpc/tracker/RPCExternalTrackerHandler.kt b/server/core/src/main/java/dev/slimevr/protocol/rpc/tracker/RPCExternalTrackerHandler.kt new file mode 100644 index 0000000000..73319cefa5 --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/protocol/rpc/tracker/RPCExternalTrackerHandler.kt @@ -0,0 +1,133 @@ +package dev.slimevr.protocol.rpc.tracker + +import com.google.flatbuffers.FlatBufferBuilder +import dev.slimevr.VRServer.Companion.getNextLocalTrackerId +import dev.slimevr.VRServer.Companion.instance +import dev.slimevr.protocol.GenericConnection +import dev.slimevr.protocol.ProtocolAPI +import dev.slimevr.protocol.datafeed.createTrackerId +import dev.slimevr.protocol.rpc.RPCHandler +import dev.slimevr.protocol.rpc.createRPCMessage +import dev.slimevr.tracking.trackers.Device +import dev.slimevr.tracking.trackers.DeviceOrigin +import dev.slimevr.tracking.trackers.Tracker +import dev.slimevr.tracking.trackers.TrackerPosition +import dev.slimevr.tracking.trackers.TrackerStatus +import io.eiren.util.logging.LogManager +import io.github.axisangles.ktmath.Quaternion +import io.github.axisangles.ktmath.Vector3 +import solarxr_protocol.datatypes.TrackerIdT +import solarxr_protocol.rpc.AddTrackerRequest +import solarxr_protocol.rpc.AddTrackerResponse +import solarxr_protocol.rpc.RpcMessage +import solarxr_protocol.rpc.RpcMessageHeader +import solarxr_protocol.rpc.UpdateTrackerBattery +import solarxr_protocol.rpc.UpdateTrackerPose +import solarxr_protocol.rpc.UpdateTrackerStatus +import kotlin.text.ifEmpty + +class RPCExternalTrackerHandler( + rpcHandler: RPCHandler, + val api: ProtocolAPI, +) { + init { + rpcHandler.registerPacketListener(RpcMessage.AddTrackerRequest, ::onAddTrackerRequest) + rpcHandler.registerPacketListener(RpcMessage.UpdateTrackerPose, ::onUpdateTrackerPose) + rpcHandler.registerPacketListener(RpcMessage.UpdateTrackerStatus, ::onUpdateTrackerStatus) + rpcHandler.registerPacketListener(RpcMessage.UpdateTrackerBattery, ::onUpdateTrackerBattery) + } + + private fun getTracker(conn: GenericConnection, trackerId: TrackerIdT): Tracker? = api.server.getTrackerById(trackerId)?.takeIf { + it.device?.origin == DeviceOrigin.SOLARXR && conn.context.createdTrackers.contains(it) + } + + fun onAddTrackerRequest(conn: GenericConnection, messageHeader: RpcMessageHeader) { + val req = messageHeader.message(AddTrackerRequest()) as? AddTrackerRequest ?: return + + val fbb = FlatBufferBuilder(32) + // Try to find existing device. + val existingDevice = api.server.deviceManager.devices.find { + it.hardwareIdentifier == req.name() + } + val tracker = if (existingDevice != null) { + if (existingDevice.origin != DeviceOrigin.SOLARXR) { + LogManager.warning("[RPCExternalTrackerHandler] Client tried to create a tracker which exists and isn't external") + return + } + + // There should only be one tracker + existingDevice.trackers[0] ?: error("SolarXR device didn't have tracker!?") + } else { + val device = instance.deviceManager + .createDevice( + DeviceOrigin.SOLARXR, + req.displayName(), + null, + req.manufacturer() ?: "External", + ) + device.hardwareIdentifier = req.name() + + val tracker = Tracker( + device, + getNextLocalTrackerId(), + req.name(), + req.displayName(), + trackerPosition = TrackerPosition.getByBodyPart(req.roleHint()), + hasPosition = req.tracksPosition(), + hasRotation = req.tracksRotation(), + hasAcceleration = req.tracksAcceleration(), + userEditable = true, + isComputed = true, + allowReset = true, + isHmd = req.isHmd, + ) + + device.trackers[0] = tracker + instance.deviceManager.addDevice(device) + instance.registerTracker(tracker) + tracker + } + + LogManager.info("[RPCExternalTrackerHandler] Added $tracker") + conn.context.createdTrackers.add(tracker) + val response = AddTrackerResponse.createAddTrackerResponse(fbb, createTrackerId(fbb, tracker)) + fbb.finish(createRPCMessage(fbb, RpcMessage.AddTrackerResponse, response, messageHeader)) + conn.send(fbb.dataBuffer()) + } + + fun onUpdateTrackerPose(conn: GenericConnection, messageHeader: RpcMessageHeader) { + val req = messageHeader.message(UpdateTrackerPose()) as? UpdateTrackerPose ?: return + val tracker = getTracker(conn, req.trackerId().unpack()) ?: return + + req.rotation()?.let { + tracker.setRotation(Quaternion(it.w(), it.x(), it.y(), it.z())) + } + req.position()?.let { + tracker.position = Vector3(it.x(), it.y(), it.z()) + } + req.rawAcceleration()?.let { + tracker.setAcceleration(Vector3(it.x(), it.y(), it.z())) + } + tracker.dataTick() + } + + fun onUpdateTrackerStatus(conn: GenericConnection, messageHeader: RpcMessageHeader) { + val req = messageHeader.message(UpdateTrackerStatus()) as? UpdateTrackerStatus + ?: return + val tracker = getTracker(conn, req.trackerId().unpack()) ?: return + + val status = TrackerStatus.getById(req.status() - 1) ?: return + LogManager.info("[RPCExternalTrackerHandler] Setting status of $tracker to $status") + tracker.status = status + } + + fun onUpdateTrackerBattery(conn: GenericConnection, messageHeader: RpcMessageHeader) { + val req = messageHeader.message(UpdateTrackerBattery()) as? UpdateTrackerBattery ?: return + val tracker = getTracker(conn, req.trackerId().unpack()) ?: return + + tracker.batteryLevel = req.batteryPercentage() + + // Purely for cosmetic purposes, external trackers do not report device voltage. + tracker.batteryVoltage = if (req.charging()) 4.3f else 3.7f + } +} diff --git a/server/core/src/main/java/dev/slimevr/protocol/rpc/trackingchecklist/RPCTrackingChecklistHandler.kt b/server/core/src/main/java/dev/slimevr/protocol/rpc/trackingchecklist/RPCTrackingChecklistHandler.kt index ca6f6b34e7..d9880a6d5c 100644 --- a/server/core/src/main/java/dev/slimevr/protocol/rpc/trackingchecklist/RPCTrackingChecklistHandler.kt +++ b/server/core/src/main/java/dev/slimevr/protocol/rpc/trackingchecklist/RPCTrackingChecklistHandler.kt @@ -4,6 +4,7 @@ import com.google.flatbuffers.FlatBufferBuilder import dev.slimevr.protocol.GenericConnection import dev.slimevr.protocol.ProtocolAPI import dev.slimevr.protocol.rpc.RPCHandler +import dev.slimevr.protocol.rpc.createRPCMessage import dev.slimevr.trackingchecklist.TrackingChecklistListener import solarxr_protocol.rpc.* @@ -30,7 +31,7 @@ class RPCTrackingChecklistHandler( private fun onTrackingChecklistRequest(conn: GenericConnection, messageHeader: RpcMessageHeader) { val fbb = FlatBufferBuilder(32) val response = buildTrackingChecklistResponse(fbb) - val outbound = rpcHandler.createRPCMessage( + val outbound = createRPCMessage( fbb, RpcMessage.TrackingChecklistResponse, response, @@ -48,7 +49,7 @@ class RPCTrackingChecklistHandler( val fbb = FlatBufferBuilder(32) val response = buildTrackingChecklistResponse(fbb) - val outbound = rpcHandler.createRPCMessage( + val outbound = createRPCMessage( fbb, RpcMessage.TrackingChecklistResponse, response, @@ -60,7 +61,7 @@ class RPCTrackingChecklistHandler( override fun onStepsUpdate() { val fbb = FlatBufferBuilder(32) val response = buildTrackingChecklistResponse(fbb) - val outbound = rpcHandler.createRPCMessage( + val outbound = createRPCMessage( fbb, RpcMessage.TrackingChecklistResponse, response, diff --git a/server/core/src/main/java/dev/slimevr/protocol/rpc/trackingpause/RPCTrackingPause.kt b/server/core/src/main/java/dev/slimevr/protocol/rpc/trackingpause/RPCTrackingPause.kt index 8ff19b2458..0add207551 100644 --- a/server/core/src/main/java/dev/slimevr/protocol/rpc/trackingpause/RPCTrackingPause.kt +++ b/server/core/src/main/java/dev/slimevr/protocol/rpc/trackingpause/RPCTrackingPause.kt @@ -4,6 +4,7 @@ import com.google.flatbuffers.FlatBufferBuilder import dev.slimevr.protocol.GenericConnection import dev.slimevr.protocol.ProtocolAPI import dev.slimevr.protocol.rpc.RPCHandler +import dev.slimevr.protocol.rpc.createRPCMessage import dev.slimevr.trackingpause.TrackingPauseListener import solarxr_protocol.rpc.RpcMessage import solarxr_protocol.rpc.RpcMessageHeader @@ -31,7 +32,7 @@ class RPCTrackingPause(private val rpcHandler: RPCHandler, private val api: Prot private fun getPauseStateResponse(trackingPaused: Boolean, messageHeader: RpcMessageHeader? = null): ByteBuffer { val fbb = FlatBufferBuilder(32) val state = TrackingPauseStateResponse.createTrackingPauseStateResponse(fbb, trackingPaused) - val outbound = rpcHandler.createRPCMessage(fbb, RpcMessage.TrackingPauseStateResponse, state, messageHeader) + val outbound = createRPCMessage(fbb, RpcMessage.TrackingPauseStateResponse, state, messageHeader) fbb.finish(outbound) return fbb.dataBuffer() } diff --git a/server/core/src/main/java/dev/slimevr/tracking/trackers/Device.kt b/server/core/src/main/java/dev/slimevr/tracking/trackers/Device.kt index 2cb36ab865..6a7734096d 100644 --- a/server/core/src/main/java/dev/slimevr/tracking/trackers/Device.kt +++ b/server/core/src/main/java/dev/slimevr/tracking/trackers/Device.kt @@ -34,7 +34,7 @@ open class Device(val origin: DeviceOrigin, val magSupport: Boolean = false) { open val boardType: BoardType = BoardType.UNKNOWN open val mcuType: MCUType = MCUType.UNKNOWN - open val hardwareIdentifier: String = "Unknown" + open var hardwareIdentifier: String = "Unknown" init { CoroutineScope(Job()).launch { diff --git a/server/core/src/main/java/dev/slimevr/tracking/trackers/Tracker.kt b/server/core/src/main/java/dev/slimevr/tracking/trackers/Tracker.kt index 4262b2abe9..1bb8281e12 100644 --- a/server/core/src/main/java/dev/slimevr/tracking/trackers/Tracker.kt +++ b/server/core/src/main/java/dev/slimevr/tracking/trackers/Tracker.kt @@ -545,4 +545,6 @@ class Tracker @JvmOverloads constructor( fun setSleepTime(time: Long) { this.timeScheduledSleep = time } + + override fun toString(): String = "Tracker $name ('$displayName' deviceId=${device?.id} trackerNum=$trackerNum)" } diff --git a/server/core/src/main/java/dev/slimevr/tracking/trackers/udp/UDPDevice.kt b/server/core/src/main/java/dev/slimevr/tracking/trackers/udp/UDPDevice.kt index 65fad95897..fbb71aaabe 100644 --- a/server/core/src/main/java/dev/slimevr/tracking/trackers/udp/UDPDevice.kt +++ b/server/core/src/main/java/dev/slimevr/tracking/trackers/udp/UDPDevice.kt @@ -12,13 +12,11 @@ import java.util.concurrent.ConcurrentHashMap class UDPDevice( var address: SocketAddress, var ipAddress: InetAddress, - override val hardwareIdentifier: String, + override var hardwareIdentifier: String, override val boardType: BoardType = BoardType.UNKNOWN, override val mcuType: MCUType = MCUType.UNKNOWN, ) : Device(DeviceOrigin.UDP, true) { - override val id: Int = nextLocalDeviceId.incrementAndGet() - @JvmField var lastPacket = System.currentTimeMillis() diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/platform/windows/WindowsNamedPipeRpcBridge.java b/server/desktop/src/main/java/dev/slimevr/desktop/platform/windows/WindowsNamedPipeRpcBridge.java index b7f685dd56..3e5e70bd3f 100644 --- a/server/desktop/src/main/java/dev/slimevr/desktop/platform/windows/WindowsNamedPipeRpcBridge.java +++ b/server/desktop/src/main/java/dev/slimevr/desktop/platform/windows/WindowsNamedPipeRpcBridge.java @@ -37,12 +37,17 @@ public void run() { FastList removedConnections = new FastList( maxConnections ); - while (true) { + while (!Thread.interrupted()) { WindowsNamedPipe.PipeConnection newConnection = this.pipe.tryAccept(); if (newConnection != null) { this.connections.add(new WindowsNamedPipeRpcConnection(newConnection)); } + if (this.connections.isEmpty()) { + Thread.sleep(10); + continue; + } + for (WindowsNamedPipeRpcConnection connection : this.connections) { if (connection.getState() == PipeState.OPEN) { connection.update(this); @@ -56,7 +61,6 @@ public void run() { this.connections.removeAll(removedConnections); removedConnections.clear(); - Thread.sleep(2); } } catch (IOException e) { LogManager.severe("[SolarXR RPC Bridge] Exception in runner thread", e); diff --git a/solarxr-protocol b/solarxr-protocol index 3ffd218a83..15aee0b011 160000 --- a/solarxr-protocol +++ b/solarxr-protocol @@ -1 +1 @@ -Subproject commit 3ffd218a83a65be046a6676f1cde79a91bc8bc69 +Subproject commit 15aee0b011d0715b97499ffd97d568b3c797e366 From 4cef1aa54da2160ea09e9ac3ecca288dba5e6bdc Mon Sep 17 00:00:00 2001 From: Sapphire Date: Wed, 20 May 2026 17:31:20 -0500 Subject: [PATCH 4/6] Only send computed trackers for synthetic_trackers in data feed --- .../pages/body-proportions/ManualProportions.tsx | 5 ++--- .../dev/slimevr/protocol/datafeed/DataFeedHandler.kt | 11 ++++------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/gui/src/components/onboarding/pages/body-proportions/ManualProportions.tsx b/gui/src/components/onboarding/pages/body-proportions/ManualProportions.tsx index a80b2a6373..f8e4e58f8b 100644 --- a/gui/src/components/onboarding/pages/body-proportions/ManualProportions.tsx +++ b/gui/src/components/onboarding/pages/body-proportions/ManualProportions.tsx @@ -1,5 +1,6 @@ import { Control, Controller, useForm } from 'react-hook-form'; import { + BodyPart, ChangeSkeletonConfigRequestT, ResetType, RpcMessage, @@ -318,9 +319,7 @@ function ButtonsControl({ control }: { control: ManualProportionControls }) { const beneathFloor = useMemo(() => { const hmd = computedTrackers.find( - (tracker) => - tracker.tracker.trackerId?.trackerNum === 1 && - tracker.tracker.trackerId.deviceId?.id === undefined + (tracker) => tracker.tracker.info?.bodyPart === BodyPart.HEAD ); return !(hmd?.tracker.position && hmd.tracker.position.y >= MIN_HEIGHT); }, [computedTrackers]); diff --git a/server/core/src/main/java/dev/slimevr/protocol/datafeed/DataFeedHandler.kt b/server/core/src/main/java/dev/slimevr/protocol/datafeed/DataFeedHandler.kt index dfb3d5dd6f..61aa39f552 100644 --- a/server/core/src/main/java/dev/slimevr/protocol/datafeed/DataFeedHandler.kt +++ b/server/core/src/main/java/dev/slimevr/protocol/datafeed/DataFeedHandler.kt @@ -67,6 +67,8 @@ class DataFeedHandler(private val api: ProtocolAPI) : ProtocolHandler Date: Fri, 22 May 2026 00:28:07 -0500 Subject: [PATCH 5/6] Fix using wrong ByteBuffer for send on Windows SolarXR pipe --- .../desktop/platform/windows/WindowsNamedPipeRpcConnection.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/platform/windows/WindowsNamedPipeRpcConnection.java b/server/desktop/src/main/java/dev/slimevr/desktop/platform/windows/WindowsNamedPipeRpcConnection.java index 7f57b21215..2886b3b487 100644 --- a/server/desktop/src/main/java/dev/slimevr/desktop/platform/windows/WindowsNamedPipeRpcConnection.java +++ b/server/desktop/src/main/java/dev/slimevr/desktop/platform/windows/WindowsNamedPipeRpcConnection.java @@ -56,7 +56,7 @@ public void send(ByteBuffer bytes) { src.put(bytes); src.flip(); synchronized (this) { - this.connection.sendBuffer(bytes.array(), bytes.remaining()); + this.connection.sendBuffer(src.array(), src.remaining()); } } } From e310af36af86a6477d85049b8c01d966652813cf Mon Sep 17 00:00:00 2001 From: Sapphire Date: Fri, 22 May 2026 01:01:04 -0500 Subject: [PATCH 6/6] fixup! [WIP] Allow creating and manipulating created trackers via SolarXR make displayName optional --- .../protocol/rpc/tracker/RPCExternalTrackerHandler.kt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/server/core/src/main/java/dev/slimevr/protocol/rpc/tracker/RPCExternalTrackerHandler.kt b/server/core/src/main/java/dev/slimevr/protocol/rpc/tracker/RPCExternalTrackerHandler.kt index 73319cefa5..a446db243e 100644 --- a/server/core/src/main/java/dev/slimevr/protocol/rpc/tracker/RPCExternalTrackerHandler.kt +++ b/server/core/src/main/java/dev/slimevr/protocol/rpc/tracker/RPCExternalTrackerHandler.kt @@ -67,11 +67,13 @@ class RPCExternalTrackerHandler( ) device.hardwareIdentifier = req.name() + val id = getNextLocalTrackerId() val tracker = Tracker( - device, - getNextLocalTrackerId(), - req.name(), - req.displayName(), + device = device, + id = id, + trackerNum = 0, + name = req.name(), + displayName = req.displayName() ?: "Tracker #$id", trackerPosition = TrackerPosition.getByBodyPart(req.roleHint()), hasPosition = req.tracksPosition(), hasRotation = req.tracksRotation(),