mirror of
https://github.com/SlimeVR/SlimeVR-Server.git
synced 2026-04-06 02:01:58 +02:00
Server Guards: Mounting timeout and yaw reset guard (#1628)
Co-authored-by: sctanf <36978460+sctanf@users.noreply.github.com> Co-authored-by: Butterscotch! <bscotchvanilla@gmail.com> Co-authored-by: Aed <145398159+Aed-1@users.noreply.github.com>
This commit is contained in:
@@ -242,6 +242,10 @@ reset-mounting = Mounting Calibration
|
||||
reset-mounting-feet = Feet Calibration
|
||||
reset-mounting-fingers = Fingers Calibration
|
||||
reset-yaw = Yaw Reset
|
||||
reset-error-no_feet_tracker = No feet tracker assigned
|
||||
reset-error-no_fingers_tracker = No finger tracker assigned
|
||||
reset-error-mounting-need_full_reset = Need a full reset before mounting
|
||||
reset-error-yaw-need_full_reset = Need a full reset before yaw reset
|
||||
|
||||
## Serial detection stuff
|
||||
serial_detection-new_device-p0 = New serial device detected!
|
||||
|
||||
@@ -58,6 +58,7 @@ function BasicResetButton(options: UseResetOptions & { customName?: string }) {
|
||||
progress: resetProress,
|
||||
disabled,
|
||||
duration,
|
||||
error,
|
||||
} = useReset(options);
|
||||
|
||||
const progress = status === 'counting' ? resetProress / duration : 0;
|
||||
@@ -69,9 +70,20 @@ function BasicResetButton(options: UseResetOptions & { customName?: string }) {
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
disabled={isMd}
|
||||
content={<Typography textAlign="text-center" id={name} />}
|
||||
preferedDirection="top"
|
||||
disabled={!error && isMd}
|
||||
content={
|
||||
error ? (
|
||||
<Typography
|
||||
id={error}
|
||||
textAlign="text-center"
|
||||
color="text-status-critical"
|
||||
/>
|
||||
) : (
|
||||
<Typography textAlign="text-center" id={name} />
|
||||
)
|
||||
}
|
||||
spacing={5}
|
||||
preferedDirection={error ? 'bottom' : 'top'}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
|
||||
@@ -459,7 +459,7 @@ export function Tooltip({
|
||||
variant = 'auto',
|
||||
disabled = false,
|
||||
tag = 'div',
|
||||
spacing = 20,
|
||||
spacing = 10,
|
||||
}: TooltipProps) {
|
||||
const childRef = useRef<HTMLElement | null>(null);
|
||||
const { isMobile } = useBreakpoint('mobile');
|
||||
|
||||
@@ -11,6 +11,8 @@ import { ReactNode } from 'react';
|
||||
import { SkiIcon } from '@/components/commons/icon/SkiIcon';
|
||||
import { FootIcon } from '@/components/commons/icon/FootIcon';
|
||||
import { FingersIcon } from '@/components/commons/icon/FingersIcon';
|
||||
import { Tooltip } from '@/components/commons/Tooltip';
|
||||
import { Typography } from '@/components/commons/Typography';
|
||||
|
||||
export function ResetButtonIcon(options: UseResetOptions) {
|
||||
if (options.type === ResetType.Mounting && !options.group)
|
||||
@@ -35,33 +37,49 @@ export function ResetButton({
|
||||
children?: ReactNode;
|
||||
onReseted?: () => void;
|
||||
} & UseResetOptions) {
|
||||
const { triggerReset, status, timer, disabled, name } = useReset(
|
||||
const { triggerReset, status, timer, disabled, name, error } = useReset(
|
||||
options,
|
||||
onReseted
|
||||
);
|
||||
|
||||
return (
|
||||
<Button
|
||||
icon={<ResetButtonIcon {...options} />}
|
||||
onClick={triggerReset}
|
||||
className={classNames(
|
||||
'border-2 py-[5px]',
|
||||
status === 'finished'
|
||||
? 'border-status-success'
|
||||
: 'transition-[border-color] duration-500 ease-in-out border-transparent',
|
||||
className
|
||||
)}
|
||||
variant="primary"
|
||||
disabled={disabled}
|
||||
<Tooltip
|
||||
preferedDirection={'top'}
|
||||
disabled={!error}
|
||||
content={
|
||||
error ? (
|
||||
<Typography
|
||||
id={error}
|
||||
textAlign="text-center"
|
||||
color="text-status-critical"
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)
|
||||
}
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<div className="opacity-0 h-0">
|
||||
{children || <Localized id={name} />}
|
||||
<Button
|
||||
icon={<ResetButtonIcon {...options} />}
|
||||
onClick={triggerReset}
|
||||
className={classNames(
|
||||
'border-2 py-[5px]',
|
||||
status === 'finished'
|
||||
? 'border-status-success'
|
||||
: 'transition-[border-color] duration-500 ease-in-out border-transparent',
|
||||
className
|
||||
)}
|
||||
variant="primary"
|
||||
disabled={disabled}
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<div className="opacity-0 h-0">
|
||||
{children || <Localized id={name} />}
|
||||
</div>
|
||||
{status !== 'counting' || options.type === ResetType.Yaw
|
||||
? children || <Localized id={name} />
|
||||
: String(timer)}
|
||||
</div>
|
||||
{status !== 'counting' || options.type === ResetType.Yaw
|
||||
? children || <Localized id={name} />
|
||||
: String(timer)}
|
||||
</div>
|
||||
</Button>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ export function useDataFeedConfig() {
|
||||
dataFeedConfig.minimumTimeSinceLast = 1000 / feedMaxTps;
|
||||
dataFeedConfig.syntheticTrackersMask = trackerData;
|
||||
dataFeedConfig.stayAlignedPoseMask = true;
|
||||
dataFeedConfig.serverGuardsMask = true;
|
||||
|
||||
return {
|
||||
dataFeedConfig,
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
} from 'solarxr-protocol';
|
||||
import { useWebsocketAPI } from './websocket-api';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { assignedTrackersAtom } from '@/store/app-store';
|
||||
import { assignedTrackersAtom, serverGuardsAtom } from '@/store/app-store';
|
||||
import { FEET_BODY_PARTS, FINGER_BODY_PARTS } from './body-parts';
|
||||
import { useLocaleConfig } from '@/i18n/config';
|
||||
|
||||
@@ -29,6 +29,7 @@ export const BODY_PARTS_GROUPS: Record<MountingResetGroup, BodyPart[]> = {
|
||||
export function useReset(options: UseResetOptions, onReseted?: () => void) {
|
||||
if (options.type === ResetType.Mounting && !options.group) options.group = 'default';
|
||||
|
||||
const serverGuards = useAtomValue(serverGuardsAtom);
|
||||
const { currentLocales } = useLocaleConfig();
|
||||
const { sendRPCPacket, useRPCPacket } = useWebsocketAPI();
|
||||
|
||||
@@ -113,6 +114,7 @@ export function useReset(options: UseResetOptions, onReseted?: () => void) {
|
||||
}, [options.type]);
|
||||
|
||||
let disabled = status === 'counting';
|
||||
let error = null;
|
||||
if (options.type === ResetType.Mounting && options.group !== 'default') {
|
||||
const assignedTrackers = useAtomValue(assignedTrackersAtom);
|
||||
|
||||
@@ -122,8 +124,16 @@ export function useReset(options: UseResetOptions, onReseted?: () => void) {
|
||||
tracker.info?.bodyPart &&
|
||||
BODY_PARTS_GROUPS[options.group].includes(tracker.info?.bodyPart)
|
||||
)
|
||||
)
|
||||
) {
|
||||
disabled = true;
|
||||
error = `reset-error-no_${options.group}_tracker`;
|
||||
}
|
||||
} else if (options.type === ResetType.Mounting && !serverGuards?.canDoMounting) {
|
||||
disabled = true;
|
||||
error = 'reset-error-mounting-need_full_reset';
|
||||
} else if (options.type === ResetType.Yaw && !serverGuards?.canDoYawReset) {
|
||||
disabled = true;
|
||||
error = 'reset-error-yaw-need_full_reset';
|
||||
}
|
||||
|
||||
const localized = useMemo(
|
||||
@@ -144,8 +154,7 @@ export function useReset(options: UseResetOptions, onReseted?: () => void) {
|
||||
status,
|
||||
disabled,
|
||||
name,
|
||||
error,
|
||||
timer: localized.format(duration - progress),
|
||||
};
|
||||
}
|
||||
|
||||
export function useMountingReset() {}
|
||||
|
||||
@@ -28,6 +28,12 @@ export const devicesAtom = selectAtom(
|
||||
isEqual
|
||||
);
|
||||
|
||||
export const serverGuardsAtom = selectAtom(
|
||||
datafeedAtom,
|
||||
(datafeed) => datafeed.serverGuards,
|
||||
isEqual
|
||||
);
|
||||
|
||||
export const flatTrackersAtom = atom((get) => {
|
||||
const devices = get(devicesAtom);
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import dev.slimevr.firmware.SerialFlashingHandler
|
||||
import dev.slimevr.games.vrchat.VRCConfigHandler
|
||||
import dev.slimevr.games.vrchat.VRCConfigHandlerStub
|
||||
import dev.slimevr.games.vrchat.VRChatConfigManager
|
||||
import dev.slimevr.guards.ServerGuards
|
||||
import dev.slimevr.osc.OSCHandler
|
||||
import dev.slimevr.osc.OSCRouter
|
||||
import dev.slimevr.osc.VMCHandler
|
||||
@@ -122,6 +123,8 @@ class VRServer @JvmOverloads constructor(
|
||||
|
||||
val networkProfileChecker: NetworkProfileChecker
|
||||
|
||||
val serverGuards = ServerGuards()
|
||||
|
||||
init {
|
||||
// UwU
|
||||
deviceManager = DeviceManager(this)
|
||||
|
||||
27
server/core/src/main/java/dev/slimevr/guards/ServerGuards.kt
Normal file
27
server/core/src/main/java/dev/slimevr/guards/ServerGuards.kt
Normal file
@@ -0,0 +1,27 @@
|
||||
package dev.slimevr.guards
|
||||
|
||||
import java.util.Timer
|
||||
import java.util.TimerTask
|
||||
import kotlin.concurrent.schedule
|
||||
|
||||
class ServerGuards {
|
||||
|
||||
var canDoMounting: Boolean = false
|
||||
var canDoYawReset: Boolean = false
|
||||
|
||||
private val timer = Timer()
|
||||
private var mountingTimeoutTask: TimerTask? = null
|
||||
|
||||
fun onFullReset() {
|
||||
canDoMounting = true
|
||||
canDoYawReset = true
|
||||
mountingTimeoutTask?.cancel()
|
||||
mountingTimeoutTask = timer.schedule(MOUNTING_RESET_TIMEOUT) {
|
||||
canDoMounting = false
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val MOUNTING_RESET_TIMEOUT = 2 * 60 * 1000L
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package dev.slimevr.protocol.datafeed
|
||||
|
||||
import com.google.flatbuffers.FlatBufferBuilder
|
||||
import dev.slimevr.guards.ServerGuards
|
||||
import dev.slimevr.tracking.processor.skeleton.HumanSkeleton
|
||||
import dev.slimevr.tracking.processor.stayaligned.poses.RelaxedPose
|
||||
import dev.slimevr.tracking.processor.stayaligned.trackers.RestDetector
|
||||
@@ -39,4 +40,6 @@ object DataFeedBuilderKotlin {
|
||||
|
||||
return StayAlignedTracker.endStayAlignedTracker(fbb)
|
||||
}
|
||||
|
||||
fun createServerGuard(fbb: FlatBufferBuilder, serverGuards: ServerGuards): Int = solarxr_protocol.data_feed.server.ServerGuards.createServerGuards(fbb, serverGuards.canDoMounting, serverGuards.canDoYawReset)
|
||||
}
|
||||
|
||||
@@ -107,6 +107,12 @@ public class DataFeedHandler extends ProtocolHandler<DataFeedMessageHeader> {
|
||||
.createStayAlignedPose(fbb, this.api.server.humanPoseManager.skeleton);
|
||||
}
|
||||
|
||||
int serverGuardsOffset = 0;
|
||||
if (config.getServerGuardsMask()) {
|
||||
serverGuardsOffset = DataFeedBuilderKotlin.INSTANCE
|
||||
.createServerGuard(fbb, this.api.server.getServerGuards());
|
||||
}
|
||||
|
||||
return DataFeedUpdate
|
||||
.createDataFeedUpdate(
|
||||
fbb,
|
||||
@@ -114,7 +120,8 @@ public class DataFeedHandler extends ProtocolHandler<DataFeedMessageHeader> {
|
||||
trackersOffset,
|
||||
bonesOffset,
|
||||
stayAlignedPoseOffset,
|
||||
index
|
||||
index,
|
||||
serverGuardsOffset
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1541,6 +1541,8 @@ class HumanSkeleton(
|
||||
|
||||
@JvmOverloads
|
||||
fun resetTrackersFull(resetSourceName: String?, bodyParts: List<Int> = ArrayList()) {
|
||||
humanPoseManager.server?.serverGuards?.onFullReset()
|
||||
|
||||
var referenceRotation = IDENTITY
|
||||
headTracker?.let {
|
||||
if (bodyParts.isEmpty() || bodyParts.contains(BodyPart.HEAD)) {
|
||||
|
||||
@@ -370,8 +370,6 @@ class TrackerResetsHandler(val tracker: Tracker) {
|
||||
)
|
||||
}
|
||||
|
||||
this.tracker.needReset = false
|
||||
|
||||
// Reset Stay Aligned (before resetting filtering, which depends on the
|
||||
// tracker's rotation)
|
||||
tracker.stayAligned.reset()
|
||||
|
||||
@@ -196,7 +196,10 @@ class TrackingChecklistManager(private val vrServer: VRServer) : VRCConfigListen
|
||||
val trackerRequireReset = imuTrackers.filter {
|
||||
it.status !== TrackerStatus.ERROR && !it.isInternal && it.allowReset && it.needReset
|
||||
}
|
||||
updateValidity(TrackingChecklistStepId.FULL_RESET, trackerRequireReset.isEmpty()) {
|
||||
// We ask for a full reset if you need to do mounting calibration but cant because you haven't done full reset in a while
|
||||
// or if you have trackers that need reset after re-assigning
|
||||
val needFullReset = (!resetMountingCompleted && !vrServer.serverGuards.canDoMounting) || trackerRequireReset.isNotEmpty()
|
||||
updateValidity(TrackingChecklistStepId.FULL_RESET, !needFullReset) {
|
||||
if (trackerRequireReset.isNotEmpty()) {
|
||||
it.extraData = TrackingChecklistExtraDataUnion().apply {
|
||||
type = TrackingChecklistExtraData.TrackingChecklistTrackerReset
|
||||
@@ -210,7 +213,6 @@ class TrackingChecklistManager(private val vrServer: VRServer) : VRCConfigListen
|
||||
it.extraData = null
|
||||
}
|
||||
}
|
||||
|
||||
val hmd =
|
||||
assignedTrackers.firstOrNull { it.isHmd && !it.isInternal && it.status.sendData }
|
||||
val assignedHmd = hmd == null || vrServer.humanPoseManager.skeleton.headTracker != null
|
||||
|
||||
Submodule solarxr-protocol updated: 024c28cadb...0dbad5b803
Reference in New Issue
Block a user