mirror of
https://github.com/SlimeVR/SlimeVR-Server.git
synced 2026-04-06 02:01:58 +02:00
Calibration tutorial (#687)
This commit is contained in:
@@ -147,6 +147,9 @@ tracker-infos-custom_name = Custom Name
|
||||
tracker-infos-url = Tracker URL
|
||||
tracker-infos-version = Firmware Version
|
||||
tracker-infos-hardware_rev = Hardware Revision
|
||||
tracker-infos-hardware_identifier = Hardware ID
|
||||
tracker-infos-imu = IMU Sensor
|
||||
tracker-infos-board_type = Main board
|
||||
|
||||
## Tracker settings
|
||||
tracker-settings-back = Go back to trackers list
|
||||
@@ -523,6 +526,16 @@ onboarding-connect_tracker-connected_trackers = { $amount ->
|
||||
} connected
|
||||
onboarding-connect_tracker-next = I connected all my trackers
|
||||
|
||||
## Tracker calibration tutorial
|
||||
onboarding-calibration_tutorial = IMU Calibration Tutorial
|
||||
onboarding-calibration_tutorial-subtitle = This will help reduce tracker drifting!
|
||||
onboarding-calibration_tutorial-description = Every time you turn on your trackers, they need to rest for a moment on a flat surface to calibrate. Let's do the same thing by clicking the "Calibrate" button, <b>do not move them!</b>
|
||||
onboarding-calibration_tutorial-calibrate = I placed my trackers on the table
|
||||
onboarding-calibration_tutorial-status-waiting = Waiting for you
|
||||
onboarding-calibration_tutorial-status-calibrating = Calibrating
|
||||
onboarding-calibration_tutorial-status-success = Nice!
|
||||
onboarding-calibration_tutorial-status-error = The tracker was moved
|
||||
|
||||
## Tracker assignment setup
|
||||
onboarding-assign_trackers-back = Go Back to Wi-Fi Credentials
|
||||
onboarding-assign_trackers-title = Assign trackers
|
||||
|
||||
BIN
gui/public/images/taybol.png
Normal file
BIN
gui/public/images/taybol.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
@@ -43,6 +43,7 @@ import { VMCSettings } from './components/settings/pages/VMCSettings';
|
||||
import { MountingChoose } from './components/onboarding/pages/mounting/MountingChoose';
|
||||
import { ProportionsChoose } from './components/onboarding/pages/body-proportions/ProportionsChoose';
|
||||
import { LogicalSize, appWindow } from '@tauri-apps/api/window';
|
||||
import { CalibrationTutorialPage } from './components/onboarding/pages/CalibrationTutorial';
|
||||
|
||||
function Layout() {
|
||||
const { loading } = useConfig();
|
||||
@@ -93,6 +94,10 @@ function Layout() {
|
||||
<Route path="home" element={<HomePage />} />
|
||||
<Route path="wifi-creds" element={<WifiCredsPage />} />
|
||||
<Route path="connect-trackers" element={<ConnectTrackersPage />} />
|
||||
<Route
|
||||
path="calibration-tutorial"
|
||||
element={<CalibrationTutorialPage />}
|
||||
/>
|
||||
<Route path="trackers-assign" element={<TrackersAssignPage />} />
|
||||
<Route path="enter-vr" element={<EnterVRPage />} />
|
||||
<Route path="mounting/choose" element={<MountingChoose />}></Route>
|
||||
|
||||
165
gui/src/components/onboarding/pages/CalibrationTutorial.tsx
Normal file
165
gui/src/components/onboarding/pages/CalibrationTutorial.tsx
Normal file
@@ -0,0 +1,165 @@
|
||||
import { Localized, useLocalization } from '@fluent/react';
|
||||
import { useOnboarding } from '../../../hooks/onboarding';
|
||||
import { Button } from '../../commons/Button';
|
||||
import { Typography } from '../../commons/Typography';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { SkipSetupWarningModal } from '../SkipSetupWarningModal';
|
||||
import { SkipSetupButton } from '../SkipSetupButton';
|
||||
import { ProgressBar } from '../../commons/ProgressBar';
|
||||
import { LoaderIcon, SlimeState } from '../../commons/icon/LoaderIcon';
|
||||
import { useCountdown } from '../../../hooks/countdown';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export enum CalibrationStatus {
|
||||
SUCCESS,
|
||||
CALIBRATING,
|
||||
WAITING,
|
||||
ERROR,
|
||||
}
|
||||
|
||||
export const IMU_CALIBRATION_TIME = 4;
|
||||
|
||||
export function CalibrationTutorialPage() {
|
||||
const { l10n } = useLocalization();
|
||||
const { applyProgress, skipSetup } = useOnboarding();
|
||||
const [skipWarning, setSkipWarning] = useState(false);
|
||||
const [calibrationStatus, setCalibrationStatus] = useState(
|
||||
CalibrationStatus.WAITING
|
||||
);
|
||||
const { timer, isCounting, startCountdown } = useCountdown({
|
||||
duration: IMU_CALIBRATION_TIME,
|
||||
onCountdownEnd: () => setCalibrationStatus(CalibrationStatus.SUCCESS),
|
||||
});
|
||||
|
||||
const progressBarClass = useMemo(() => {
|
||||
switch (calibrationStatus) {
|
||||
case CalibrationStatus.ERROR:
|
||||
return 'bg-status-critical';
|
||||
case CalibrationStatus.SUCCESS:
|
||||
return 'bg-status-success';
|
||||
}
|
||||
}, [calibrationStatus]);
|
||||
|
||||
const slimeStatus = useMemo(() => {
|
||||
switch (calibrationStatus) {
|
||||
case CalibrationStatus.CALIBRATING:
|
||||
return SlimeState.JUMPY;
|
||||
case CalibrationStatus.ERROR:
|
||||
return SlimeState.SAD;
|
||||
default:
|
||||
return SlimeState.HAPPY;
|
||||
}
|
||||
}, [calibrationStatus]);
|
||||
|
||||
const progressText = useMemo(() => {
|
||||
switch (calibrationStatus) {
|
||||
case CalibrationStatus.CALIBRATING:
|
||||
return l10n.getString(
|
||||
'onboarding-calibration_tutorial-status-calibrating'
|
||||
);
|
||||
case CalibrationStatus.ERROR:
|
||||
return l10n.getString('onboarding-calibration_tutorial-status-error');
|
||||
case CalibrationStatus.SUCCESS:
|
||||
return l10n.getString('onboarding-calibration_tutorial-status-success');
|
||||
case CalibrationStatus.WAITING:
|
||||
return l10n.getString('onboarding-calibration_tutorial-status-waiting');
|
||||
}
|
||||
}, [calibrationStatus, l10n]);
|
||||
|
||||
applyProgress(0.45);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-5 h-full items-center w-full justify-center relative">
|
||||
<SkipSetupButton
|
||||
visible={true}
|
||||
modalVisible={skipWarning}
|
||||
onClick={() => setSkipWarning(true)}
|
||||
></SkipSetupButton>
|
||||
<div className="flex w-full h-full justify-center px-20 gap-14">
|
||||
<div className="flex gap-8 self-center">
|
||||
<div className="flex flex-col max-w-md gap-3">
|
||||
<div>
|
||||
<Typography variant="main-title">
|
||||
{l10n.getString('onboarding-calibration_tutorial')}
|
||||
</Typography>
|
||||
<Typography variant="vr-accessible" italic>
|
||||
{l10n.getString('onboarding-calibration_tutorial-subtitle')}
|
||||
</Typography>
|
||||
</div>
|
||||
<Localized
|
||||
id="onboarding-calibration_tutorial-description"
|
||||
elems={{ b: <b></b> }}
|
||||
>
|
||||
<Typography color="secondary">
|
||||
Description on calibration of IMU
|
||||
</Typography>
|
||||
</Localized>
|
||||
<div>
|
||||
<div className="flex justify-center">
|
||||
<LoaderIcon slimeState={slimeStatus}></LoaderIcon>
|
||||
</div>
|
||||
<ProgressBar
|
||||
progress={
|
||||
isCounting
|
||||
? (IMU_CALIBRATION_TIME - timer) / IMU_CALIBRATION_TIME
|
||||
: calibrationStatus === CalibrationStatus.SUCCESS
|
||||
? 1
|
||||
: 0
|
||||
}
|
||||
height={14}
|
||||
animated={true}
|
||||
colorClass={progressBarClass}
|
||||
></ProgressBar>
|
||||
</div>
|
||||
<div className="flex justify-center">
|
||||
<Typography variant="section-title">{progressText}</Typography>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<Button
|
||||
variant="secondary"
|
||||
to="/onboarding/wifi-creds"
|
||||
className="mr-auto"
|
||||
>
|
||||
{l10n.getString('onboarding-previous_step')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
setCalibrationStatus(CalibrationStatus.CALIBRATING);
|
||||
startCountdown();
|
||||
}}
|
||||
disabled={isCounting}
|
||||
hidden={CalibrationStatus.SUCCESS === calibrationStatus}
|
||||
className="ml-auto"
|
||||
>
|
||||
{l10n.getString('onboarding-calibration_tutorial-calibrate')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
to="/onboarding/trackers-assign"
|
||||
className={classNames(
|
||||
'ml-auto',
|
||||
CalibrationStatus.SUCCESS !== calibrationStatus && 'hidden'
|
||||
)}
|
||||
>
|
||||
{l10n.getString('onboarding-continue')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex self-center w-[32rem]">
|
||||
<div>
|
||||
<img src="/images/taybol.png"></img>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<SkipSetupWarningModal
|
||||
accept={skipSetup}
|
||||
onClose={() => setSkipWarning(false)}
|
||||
isOpen={skipWarning}
|
||||
></SkipSetupWarningModal>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import { Typography } from '../../commons/Typography';
|
||||
import { TrackerCard } from '../../tracker/TrackerCard';
|
||||
import { SkipSetupWarningModal } from '../SkipSetupWarningModal';
|
||||
import { SkipSetupButton } from '../SkipSetupButton';
|
||||
import { useBnoExists } from '../../../hooks/imu-logic';
|
||||
|
||||
const BOTTOM_HEIGHT = 80;
|
||||
|
||||
@@ -70,6 +71,8 @@ export function ConnectTrackersPage() {
|
||||
|
||||
const connectedTrackers = useConnectedTrackers();
|
||||
|
||||
const bnoExists = useBnoExists(connectedTrackers);
|
||||
|
||||
useEffect(() => {
|
||||
if (!state.wifi) {
|
||||
navigate('/onboarding/wifi-creds');
|
||||
@@ -209,7 +212,13 @@ export function ConnectTrackersPage() {
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
to={state.alonePage ? '/' : '/onboarding/trackers-assign'}
|
||||
to={
|
||||
state.alonePage
|
||||
? '/'
|
||||
: bnoExists
|
||||
? '/onboarding/calibration-tutorial'
|
||||
: '/onboarding/trackers-assign'
|
||||
}
|
||||
className="ml-auto"
|
||||
>
|
||||
{l10n.getString('onboarding-connect_tracker-next')}
|
||||
|
||||
@@ -8,15 +8,21 @@ import { useState } from 'react';
|
||||
import { SkipSetupWarningModal } from '../SkipSetupWarningModal';
|
||||
import { SkipSetupButton } from '../SkipSetupButton';
|
||||
import classNames from 'classnames';
|
||||
import { useTrackers } from '../../../hooks/tracker';
|
||||
import { useBnoExists } from '../../../hooks/imu-logic';
|
||||
|
||||
export function WifiCredsPage() {
|
||||
const { l10n } = useLocalization();
|
||||
const { applyProgress, skipSetup, state } = useOnboarding();
|
||||
const { control, handleSubmit, submitWifiCreds, formState } = useWifiForm();
|
||||
const { useConnectedTrackers } = useTrackers();
|
||||
const [skipWarning, setSkipWarning] = useState(false);
|
||||
const connectedTrackers = useConnectedTrackers();
|
||||
|
||||
applyProgress(0.2);
|
||||
|
||||
const bnoExists = useBnoExists(connectedTrackers);
|
||||
|
||||
return (
|
||||
<form
|
||||
className="flex flex-col w-full h-full"
|
||||
@@ -92,7 +98,11 @@ export function WifiCredsPage() {
|
||||
<Button
|
||||
variant="secondary"
|
||||
className={state.alonePage ? 'opacity-0' : ''}
|
||||
to="/onboarding/trackers-assign"
|
||||
to={
|
||||
bnoExists
|
||||
? '/onboarding/calibration-tutorial'
|
||||
: '/onboarding/trackers-assign'
|
||||
}
|
||||
>
|
||||
{l10n.getString('onboarding-wifi_creds-skip')}
|
||||
</Button>
|
||||
|
||||
@@ -28,6 +28,7 @@ import { NeckWarningModal } from '../../NeckWarningModal';
|
||||
import { TrackerSelectionMenu } from './TrackerSelectionMenu';
|
||||
import { SkipSetupWarningModal } from '../../SkipSetupWarningModal';
|
||||
import { SkipSetupButton } from '../../SkipSetupButton';
|
||||
import { useBnoExists } from '../../../../hooks/imu-logic';
|
||||
import { useConfig } from '../../../../hooks/config';
|
||||
import { playTapSetupSound } from '../../../../sounds/sounds';
|
||||
|
||||
@@ -45,7 +46,7 @@ interface FlatDeviceTrackerDummy {
|
||||
|
||||
export function TrackersAssignPage() {
|
||||
const { l10n } = useLocalization();
|
||||
const { useAssignedTrackers, trackers } = useTrackers();
|
||||
const { useAssignedTrackers, trackers, useConnectedTrackers } = useTrackers();
|
||||
const { applyProgress, skipSetup, state } = useOnboarding();
|
||||
const { sendRPCPacket, useRPCPacket } = useWebsocketAPI();
|
||||
|
||||
@@ -56,6 +57,9 @@ export function TrackersAssignPage() {
|
||||
const [selectedRole, setSelectRole] = useState<BodyPart>(BodyPart.NONE);
|
||||
const assignedTrackers = useAssignedTrackers();
|
||||
const [skipWarning, setSkipWarning] = useState(false);
|
||||
const connectedTrackers = useConnectedTrackers();
|
||||
|
||||
const bnoExists = useBnoExists(connectedTrackers);
|
||||
const { config } = useConfig();
|
||||
const [tapDetectionSettings, setTapDetectionSettings] = useState<Omit<
|
||||
TapDetectionSettingsT,
|
||||
@@ -289,7 +293,14 @@ export function TrackersAssignPage() {
|
||||
<div className="flex flex-row mt-auto">
|
||||
{!state.alonePage && (
|
||||
<>
|
||||
<Button variant="secondary" to="/onboarding/wifi-creds">
|
||||
<Button
|
||||
variant="secondary"
|
||||
to={
|
||||
bnoExists
|
||||
? '/onboarding/calibration-tutorial'
|
||||
: '/onboarding/wifi-creds'
|
||||
}
|
||||
>
|
||||
{l10n.getString('onboarding-previous_step')}
|
||||
</Button>
|
||||
<Button
|
||||
|
||||
@@ -4,7 +4,12 @@ import { IPv4 } from 'ip-num/IPNumber';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { AssignTrackerRequestT, BodyPart, RpcMessage } from 'solarxr-protocol';
|
||||
import {
|
||||
AssignTrackerRequestT,
|
||||
BodyPart,
|
||||
ImuType,
|
||||
RpcMessage,
|
||||
} from 'solarxr-protocol';
|
||||
import { useDebouncedEffect } from '../../hooks/timeout';
|
||||
import { useTrackerFromId } from '../../hooks/tracker';
|
||||
import { useWebsocketAPI } from '../../hooks/websocket-api';
|
||||
@@ -185,7 +190,7 @@ export function TrackerSettingsPage() {
|
||||
Update now
|
||||
</Button>
|
||||
</div> */}
|
||||
<div className="flex flex-col bg-background-70 p-3 rounded-lg gap-2">
|
||||
<div className="flex flex-col bg-background-70 p-3 rounded-lg gap-2 overflow-x-auto">
|
||||
<div className="flex justify-between">
|
||||
<Typography color="secondary">
|
||||
{l10n.getString('tracker-infos-manufacturer')}
|
||||
@@ -227,13 +232,39 @@ export function TrackerSettingsPage() {
|
||||
{tracker?.device?.hardwareInfo?.firmwareVersion || '--'}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
{/* <div className="flex justify-between">
|
||||
<Typography color="secondary">
|
||||
{l10n.getString('tracker-infos-hardware_rev')}
|
||||
</Typography>
|
||||
<Typography>
|
||||
{tracker?.device?.hardwareInfo?.hardwareRevision || '--'}
|
||||
</Typography>
|
||||
</div> */}
|
||||
<div className="flex justify-between">
|
||||
<Typography color="secondary">
|
||||
{l10n.getString('tracker-infos-hardware_identifier')}
|
||||
</Typography>
|
||||
<Typography>
|
||||
{tracker?.device?.hardwareInfo?.hardwareIdentifier || '--'}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<Typography color="secondary">
|
||||
{l10n.getString('tracker-infos-imu')}
|
||||
</Typography>
|
||||
<Typography>
|
||||
{tracker?.tracker.info?.imuType
|
||||
? ImuType[tracker?.tracker.info?.imuType]
|
||||
: '--'}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<Typography color="secondary">
|
||||
{l10n.getString('tracker-infos-board_type')}
|
||||
</Typography>
|
||||
<Typography>
|
||||
{tracker?.device?.hardwareInfo?.boardType || '--'}
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
{tracker?.tracker && (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useRef, useState } from 'react';
|
||||
|
||||
export function useCountdown({
|
||||
duration = 3,
|
||||
@@ -11,24 +11,41 @@ export function useCountdown({
|
||||
}) {
|
||||
const [isCounting, setIsCounting] = useState(false);
|
||||
const [timer, setDisplayTimer] = useState(0);
|
||||
const countdownTimer = useRef<NodeJS.Timer>();
|
||||
const counter = useRef(0);
|
||||
|
||||
const startCountdown = () => {
|
||||
setIsCounting(true);
|
||||
setDisplayTimer(duration);
|
||||
for (let i = 1; i < duration; i++) {
|
||||
setTimeout(() => setDisplayTimer(duration - i), i * 1000);
|
||||
}
|
||||
setTimeout(resetEnd, duration * 1000);
|
||||
counter.current = 0;
|
||||
countdownTimer.current = setInterval(
|
||||
() => {
|
||||
counter.current++;
|
||||
setDisplayTimer(duration - counter.current);
|
||||
if (counter.current >= duration) {
|
||||
clearInterval(countdownTimer.current);
|
||||
resetEnd();
|
||||
}
|
||||
},
|
||||
duration > 1 ? 1000 : 500
|
||||
);
|
||||
};
|
||||
|
||||
const resetEnd = () => {
|
||||
setIsCounting(false);
|
||||
clearInterval(countdownTimer.current);
|
||||
onCountdownEnd();
|
||||
};
|
||||
|
||||
const abortCountdown = () => {
|
||||
setIsCounting(false);
|
||||
clearInterval(countdownTimer.current);
|
||||
};
|
||||
|
||||
return {
|
||||
timer,
|
||||
isCounting,
|
||||
startCountdown,
|
||||
abortCountdown,
|
||||
};
|
||||
}
|
||||
|
||||
19
gui/src/hooks/imu-logic.ts
Normal file
19
gui/src/hooks/imu-logic.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { useMemo } from 'react';
|
||||
import { FlatDeviceTracker } from './app';
|
||||
import { ImuType } from 'solarxr-protocol';
|
||||
|
||||
export function useBnoExists(connectedTrackers: FlatDeviceTracker[]): boolean {
|
||||
const bnoExists = useMemo(
|
||||
() =>
|
||||
connectedTrackers.some(
|
||||
(tracker) =>
|
||||
tracker.tracker.info?.imuType &&
|
||||
[ImuType.BNO055, ImuType.BNO080, ImuType.BNO085].includes(
|
||||
tracker.tracker.info?.imuType
|
||||
)
|
||||
),
|
||||
[connectedTrackers]
|
||||
);
|
||||
|
||||
return bnoExists;
|
||||
}
|
||||
@@ -49,3 +49,8 @@ export function QuaternionToEulerDegrees(q?: QuatObject | null) {
|
||||
const a = new Euler().setFromQuaternion(new Quaternion(q.x, q.y, q.z, q.w));
|
||||
return { x: a.x * RAD_TO_DEG, y: a.y * RAD_TO_DEG, z: a.z * RAD_TO_DEG };
|
||||
}
|
||||
|
||||
export function compareQuatT(a: QuatT | null, b: QuatT | null): boolean {
|
||||
if (!a || !b) return false;
|
||||
return a.w === b.w && a.x === b.x && a.y === b.y && a.z === b.z;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user