Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion gui/public/i18n/en/translation.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -1657,12 +1657,20 @@ tracking_checklist-MOUNTING_CALIBRATION = Perform a mounting calibration
tracking_checklist-FEET_MOUNTING_CALIBRATION = Perform a feet mounting calibration
tracking_checklist-FULL_RESET = Perform a full reset
tracking_checklist-FULL_RESET-desc = Some trackers need a reset to be performed.
tracking_checklist-STEAMVR_DISCONNECTED = SteamVR not running
tracking_checklist-STEAMVR_DISCONNECTED = SteamVR disconnected
tracking_checklist-STEAMVR_DISCONNECTED-desc = SteamVR is not running. Are you using it for VR?
tracking_checklist-STEAMVR_DISCONNECTED-driver_blocked-desc = The driver has been blocked by SteamVR due to a previous SteamVR crash.
tracking_checklist-STEAMVR_DISCONNECTED-driver_disabled-desc = The driver is disabled in SteamVR settings.
tracking_checklist-STEAMVR_DISCONNECTED-driver_not_installed-desc = The driver is not installed.
tracking_checklist-STEAMVR_DISCONNECTED-open = Launch SteamVR
tracking_checklist-STEAMVR_DISCONNECTED-enable = Enable driver
tracking_checklist-STEAMVR_HANDS_ENABLED = Hand trackers toggled on
tracking_checklist-STEAMVR_HANDS_ENABLED-desc = You have enabled the SteamVR virtual hand trackers. This will cause button inputs to not work in SteamVR and in games.
tracking_checklist-STEAMVR_HANDS_ENABLED-go = Disable them
tracking_checklist-STANDABLE_INSTALLED = Standable is installed
tracking_checklist-STANDABLE_INSTALLED-desc =
Standable frequently causes tracking issues when used alongside SlimeVR. Standable should be fully uninstalled in Steam to ensure no issues arise.
You must close SteamVR before uninstalling Standable in Steam.
tracking_checklist-TRACKERS_REST_CALIBRATION = Calibrate your trackers
tracking_checklist-TRACKERS_REST_CALIBRATION-desc = You didn't perform tracker calibration. Please let your trackers (highlighted in yellow) rest on a stable surface for a few seconds.
tracking_checklist-TRACKER_ERROR = Trackers with Errors
Expand Down
109 changes: 86 additions & 23 deletions gui/src/components/tracking-checklist/TrackingChecklist.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ import {
import classNames from 'classnames';
import {
ChangeSettingsRequestT,
EnableSteamVRDriverRequestT,
ResetType,
RpcMessage,
SettingsRequestT,
SettingsResponseT,
SteamVRTrackersSettingT,
TrackingChecklistPublicNetworksT,
TrackingChecklistSteamVRDisconnectedT,
TrackingChecklistStepId,
} from 'solarxr-protocol';
import { ReactNode, useEffect, useMemo, useState } from 'react';
Expand All @@ -28,7 +30,7 @@ import {
ArrowDownIcon,
ArrowRightIcon,
} from '@/components/commons/icon/ArrowIcons';
import { Localized } from '@fluent/react';
import { Localized, useLocalization } from '@fluent/react';
import { WrenchIcon } from '@/components/commons/icon/WrenchIcons';
import { TrackingChecklistModal } from './TrackingChecklistModal';
import { NavLink, useNavigate } from 'react-router-dom';
Expand Down Expand Up @@ -108,6 +110,69 @@ function Step({
);
}

function SteamVRDisconnected({
step,
context,
}: {
step: TrackingChecklistStep;
context: TrackingChecklistContext;
}) {
const { sendRPCPacket } = useWebsocketAPI();
const data = step.extraData as TrackingChecklistSteamVRDisconnectedT | null;

const enableDriver = () => {
sendRPCPacket(
RpcMessage.EnableSteamVRDriverRequest,
new EnableSteamVRDriverRequestT()
);
};

const driverNotInstalled = data?.driverInstalled === false;
const driverBlocked = data?.driverBlockedBySafeMode === true;
const driverDisabled = data?.driverEnabled === false;

const showEnableDriverButton = driverBlocked || driverDisabled;

const getDescriptionId = () => {
if (driverBlocked)
return 'tracking_checklist-STEAMVR_DISCONNECTED-driver_blocked-desc';
if (driverDisabled)
return 'tracking_checklist-STEAMVR_DISCONNECTED-driver_disabled-desc';
if (driverNotInstalled)
return 'tracking_checklist-STEAMVR_DISCONNECTED-driver_not_installed-desc';
return 'tracking_checklist-STEAMVR_DISCONNECTED-desc';
};

return (
<div className="space-y-2.5">
<Typography id={getDescriptionId()} />
<div className="flex justify-between sm:items-center gap-1 flex-col sm:flex-row">
{showEnableDriverButton && (
<Button
id="tracking_checklist-STEAMVR_DISCONNECTED-enable"
variant="primary"
onClick={enableDriver}
/>
)}
{!showEnableDriverButton && !driverNotInstalled && (
<Button
id="tracking_checklist-STEAMVR_DISCONNECTED-open"
variant="primary"
onClick={() => openUrl('steam://run/250820')}
/>
)}
{step.ignorable && (
<Button
id="tracking_checklist-ignore"
variant="secondary"
onClick={() => context.toggleSession(step.id)}
/>
)}
</div>
</div>
);
}

function SteamVRHandsEnabled() {
const { sendRPCPacket, useRPCPacket } = useWebsocketAPI();
const [steamVrTrackers, setSteamVrTrackers] = useState<Omit<
Expand Down Expand Up @@ -157,6 +222,21 @@ function SteamVRHandsEnabled() {
);
}

function StandableInstalled() {
const { l10n } = useLocalization();

return (
<div className="space-y-2.5">
{l10n
.getString('tracking_checklist-STANDABLE_INSTALLED-desc')
.split('\n')
.map((line, i) => (
<Typography key={i}>{line}</Typography>
))}
</div>
);
}

const stepContentLookup: Record<
number,
(
Expand Down Expand Up @@ -224,28 +304,8 @@ const stepContentLookup: Record<
</div>
);
},
[TrackingChecklistStepId.STEAMVR_DISCONNECTED]: (step, { toggleSession }) => {
return (
<>
<div className="space-y-2.5">
<Typography id="tracking_checklist-STEAMVR_DISCONNECTED-desc" />
<div className="flex justify-between sm:items-center gap-1 flex-col sm:flex-row">
<Button
id="tracking_checklist-STEAMVR_DISCONNECTED-open"
variant="primary"
onClick={() => openUrl('steam://run/250820')}
/>
{step.ignorable && (
<Button
id="tracking_checklist-ignore"
variant="secondary"
onClick={() => toggleSession(step.id)}
/>
)}
</div>
</div>
</>
);
[TrackingChecklistStepId.STEAMVR_DISCONNECTED]: (step, context) => {
return <SteamVRDisconnected step={step} context={context} />;
},
[TrackingChecklistStepId.TRACKER_ERROR]: () => {
return <Typography id="tracking_checklist-TRACKER_ERROR-desc" />;
Expand Down Expand Up @@ -410,6 +470,9 @@ const stepContentLookup: Record<
[TrackingChecklistStepId.STEAMVR_HANDS_ENABLED]: () => {
return <SteamVRHandsEnabled />;
},
[TrackingChecklistStepId.STANDABLE_INSTALLED]: () => {
return <StandableInstalled />;
},
};

export function TrackingChecklistMobile() {
Expand Down
4 changes: 4 additions & 0 deletions server/core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ dependencies {
// Jitpack
implementation("com.github.SlimeVR:oscquery-kt:566a0cba58")

// For SteamVR driver detection
implementation("io.ktor:ktor-client-core:2.3.13")
implementation("io.ktor:ktor-client-cio:2.3.13")

testImplementation(kotlin("test"))
// Use JUnit test framework
testImplementation(platform("org.junit:junit-bom:6.0.2"))
Expand Down
3 changes: 3 additions & 0 deletions server/core/src/main/java/dev/slimevr/VRServer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import dev.slimevr.tracking.trackers.udp.TrackersUDPServer
import dev.slimevr.trackingchecklist.TrackingChecklistManager
import dev.slimevr.util.ann.VRServerThread
import dev.slimevr.websocketapi.WebSocketVRBridge
import io.eiren.util.Process
import io.eiren.util.ann.ThreadSafe
import io.eiren.util.ann.ThreadSecure
import io.eiren.util.collections.FastList
Expand Down Expand Up @@ -62,6 +63,8 @@ class VRServer @JvmOverloads constructor(
flashingHandlerProvider: (VRServer) -> SerialFlashingHandler? = { _ -> null },
vrcConfigHandlerProvider: (VRServer) -> VRCConfigHandler = { _ -> VRCConfigHandlerStub() },
networkProfileProvider: (VRServer) -> NetworkProfileChecker = { _ -> StubNetworkProfileChecker() },
val processListProvider: () -> Sequence<Process> = { emptySequence() },
val tryOpenUri: (String) -> Unit = {},
acquireMulticastLock: () -> Any? = { null },
@JvmField val configManager: ConfigManager,
) : Thread("VRServer") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import dev.slimevr.protocol.rpc.setup.RPCUtil.getLocalIp
import dev.slimevr.protocol.rpc.status.RPCStatusHandler
import dev.slimevr.protocol.rpc.trackingchecklist.RPCTrackingChecklistHandler
import dev.slimevr.protocol.rpc.trackingpause.RPCTrackingPause
import dev.slimevr.steamvr.SteamVRUtils
import dev.slimevr.tracking.processor.config.SkeletonConfigOffsets
import dev.slimevr.tracking.processor.stayaligned.poses.RelaxedPose
import dev.slimevr.tracking.trackers.TrackerPosition
Expand All @@ -30,6 +31,8 @@ import dev.slimevr.tracking.trackers.TrackerStatus
import dev.slimevr.tracking.trackers.TrackerUtils.getTrackerForSkeleton
import io.eiren.util.logging.LogManager
import io.github.axisangles.ktmath.Quaternion
import io.ktor.client.HttpClient
import io.ktor.client.engine.cio.CIO
import kotlinx.coroutines.*
import solarxr_protocol.MessageBundle
import solarxr_protocol.datatypes.TransactionId
Expand Down Expand Up @@ -151,6 +154,11 @@ class RPCHandler(private val api: ProtocolAPI) : ProtocolHandler<RpcMessageHeade
RpcMessage.ResetStayAlignedRelaxedPoseRequest,
::onResetStayAlignedRelaxedPoseRequest,
)

registerPacketListener(
RpcMessage.EnableSteamVRDriverRequest,
::onEnableSteamVRDriverRequest,
)
}

private fun onServerInfosRequest(
Expand Down Expand Up @@ -610,6 +618,16 @@ class RPCHandler(private val api: ProtocolAPI) : ProtocolHandler<RpcMessageHeade
sendSettingsChangedResponse(conn, messageHeader)
}

private fun onEnableSteamVRDriverRequest(conn: GenericConnection, messageHeader: RpcMessageHeader) {
val request = messageHeader.message(EnableSteamVRDriverRequest()) as? EnableSteamVRDriverRequest ?: return

mainScope.launch {
val client = HttpClient(CIO)
SteamVRUtils.unblockDriver(client, "slimevr")
client.close()
}
}

fun sendSettingsChangedResponse(conn: GenericConnection, messageHeader: RpcMessageHeader?) {
val fbb = FlatBufferBuilder(32)
val settings = createSettingsResponse(fbb, api.server)
Expand Down
123 changes: 123 additions & 0 deletions server/core/src/main/java/dev/slimevr/steamvr/SteamVRUtils.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package dev.slimevr.steamvr

import dev.slimevr.VRServer
import io.eiren.util.logging.LogManager
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.request.get
import io.ktor.client.request.header
import io.ktor.client.request.parameter
import io.ktor.client.request.request
import io.ktor.client.request.setBody
import io.ktor.http.HttpMethod
import io.ktor.http.isSuccess
import kotlinx.coroutines.delay
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import kotlin.time.Duration.Companion.milliseconds

@Serializable
data class DriverManifest(
val directory: String,
@SerialName("hmd_presence")
val hmdPresence: List<String>?,
val name: String,
)

@Serializable
data class Driver(
@SerialName("always_activate")
val alwaysActivate: Boolean,
@SerialName("blocked_by_safe_mode")
val blockedBySafeMode: Boolean,
val enabled: Boolean,
@SerialName("enabled_by_default")
val enabledByDefault: Boolean,
val id: Int,
@SerialName("load_priority")
val loadPriority: Int,
val manifest: DriverManifest,
@SerialName("on_safemode_whitelist")
val onSafeModeWhitelist: Boolean,
@SerialName("show_enable_in_settings")
val showEnableInSettings: Boolean,
)

@Serializable
private data class DriverListResponse(
val drivers: List<Driver>,
@SerialName("jsonid")
val jsonId: String,
)

class SteamVRUtils {
companion object {
const val SERVER_URL = "http://127.0.0.1:27062"
const val REFERER = "$SERVER_URL/dashboard/index.html"
private val jsonIgnoreUnknownKeys = Json { ignoreUnknownKeys = true }

suspend fun getDriversList(client: HttpClient): List<Driver>? {
val resp = try {
client.request("$SERVER_URL/drivers/list.json") {
header("Referer", REFERER)
}
} catch (_: Exception) {
return null
}

if (!resp.status.isSuccess()) {
LogManager.warning("[SteamVRUtils] Failed to connect to SteamVR web server (status ${resp.status})")
return null
}

val body: String = try {
resp.body()
} catch (_: Exception) {
return null
}

val driverList: DriverListResponse = try {
jsonIgnoreUnknownKeys.decodeFromString(body)
} catch (e: Exception) {
LogManager.warning("[SteamVRUtils] Failed to decode SteamVR drivers list", e)
return null
}

if (driverList.jsonId != "vr_driver_list") {
LogManager.severe("[SteamVRUtils] SteamVR driver list response had wrong jsonId (${driverList.jsonId})")
return null
}

return driverList.drivers
}

suspend fun unblockDriver(client: HttpClient, driver: String) {
val unblockReq = client.get("$SERVER_URL/drivers/unblock") {
method = HttpMethod.Post
header("Referer", REFERER)
setBody("""{"driver":"$driver"}""")
}
if (!unblockReq.status.isSuccess()) {
LogManager.severe("[SteamVRUtils] Failed to unblock driver: ${unblockReq.status}")
}

val enableReq = client.get("$SERVER_URL/drivers/setenable") {
method = HttpMethod.Post
header("Referer", REFERER)
setBody("""{"driver":"$driver","enable":true}""")
}
if (!enableReq.status.isSuccess()) {
LogManager.severe("[SteamVRUtils] Failed to enable driver: ${enableReq.status}")
}

delay(500.milliseconds)

VRServer.instance.tryOpenUri("vrmonitor://restartsystem")
}
}
}
Loading
Loading