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:
lucas lelievre
2025-12-02 15:13:54 +01:00
committed by GitHub
parent 2e1ec07b23
commit a5160fbb8a
15 changed files with 127 additions and 35 deletions

View File

@@ -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!

View File

@@ -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(

View File

@@ -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');

View File

@@ -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>
);
}

View File

@@ -30,6 +30,7 @@ export function useDataFeedConfig() {
dataFeedConfig.minimumTimeSinceLast = 1000 / feedMaxTps;
dataFeedConfig.syntheticTrackersMask = trackerData;
dataFeedConfig.stayAlignedPoseMask = true;
dataFeedConfig.serverGuardsMask = true;
return {
dataFeedConfig,

View File

@@ -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() {}

View File

@@ -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);

View File

@@ -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)

View 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
}
}

View File

@@ -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)
}

View File

@@ -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
);
}

View File

@@ -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)) {

View File

@@ -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()

View File

@@ -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