Tap setup (#667)

Co-authored-by: Stermere <collin@kees.net>
This commit is contained in:
Uriel
2023-05-17 14:36:55 +02:00
committed by GitHub
parent 2c62e0bd72
commit 2b4676676d
26 changed files with 329 additions and 134 deletions

View File

@@ -13,6 +13,7 @@ websocket-connection_lost = Connection lost to the server. Trying to reconnect..
tips-find_tracker = Not sure which tracker is which? Shake a tracker and it will highlight the corresponding item.
tips-do_not_move_heels = Ensure your heels do not move during recording!
tips-file_select = Drag & drop files to use, or <u>browse</u>.
tips-tap_setup = You can slowly tap 2 times your tracker to choose it instead of selecting it from the menu.
## Body parts
body_part-NONE = Unassigned
@@ -208,7 +209,7 @@ tracker_selection_menu-LEFT_CONTROLLER = { -tracker_selection-part } left contro
tracker_selection_menu-unassigned = Unassigned Trackers
tracker_selection_menu-assigned = Assigned Trackers
tracker_selection_menu-dont_assign = Do not assign
tracker_selection_menu-dont_assign = Unassign
# This line cares about multilines.
# <b>text</b> means that the text should be bold.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,6 +1,6 @@
import { useLocalization } from '@fluent/react';
import classNames from 'classnames';
import { useMemo, useState } from 'react';
import { useMemo, useState, useEffect } from 'react';
import { useForm } from 'react-hook-form';
import {
AssignTrackerRequestT,
@@ -8,6 +8,11 @@ import {
QuatT,
RpcMessage,
TrackerIdT,
SettingsRequestT,
SettingsResponseT,
TapDetectionSettingsT,
ChangeSettingsRequestT,
TapDetectionSetupNotificationT,
} from 'solarxr-protocol';
import { FlatDeviceTracker } from '../../../../hooks/app';
import { useChokerWarning } from '../../../../hooks/choker-warning';
@@ -23,17 +28,26 @@ import { NeckWarningModal } from '../../NeckWarningModal';
import { TrackerSelectionMenu } from './TrackerSelectionMenu';
import { SkipSetupWarningModal } from '../../SkipSetupWarningModal';
import { SkipSetupButton } from '../../SkipSetupButton';
import { useConfig } from '../../../../hooks/config';
import { playTapSetupSound } from '../../../../sounds/sounds';
export type BodyPartError = {
label: string | undefined;
affectedRoles: BodyPart[];
};
interface FlatDeviceTrackerDummy {
tracker: {
trackerId: TrackerIdT;
info: undefined;
};
}
export function TrackersAssignPage() {
const { l10n } = useLocalization();
const { useAssignedTrackers, trackers } = useTrackers();
const { applyProgress, skipSetup, state } = useOnboarding();
const { sendRPCPacket } = useWebsocketAPI();
const { sendRPCPacket, useRPCPacket } = useWebsocketAPI();
const { control, watch } = useForm<{ advanced: boolean }>({
defaultValues: { advanced: false },
@@ -42,6 +56,66 @@ export function TrackersAssignPage() {
const [selectedRole, setSelectRole] = useState<BodyPart>(BodyPart.NONE);
const assignedTrackers = useAssignedTrackers();
const [skipWarning, setSkipWarning] = useState(false);
const { config } = useConfig();
const [tapDetectionSettings, setTapDetectionSettings] = useState<Omit<
TapDetectionSettingsT,
'pack'
> | null>(null);
useEffect(() => {
sendRPCPacket(RpcMessage.SettingsRequest, new SettingsRequestT());
}, []);
useRPCPacket(RpcMessage.SettingsResponse, (settings: SettingsResponseT) => {
setTapDetectionSettings(settings.tapDetectionSettings);
});
useEffect(() => {
if (!tapDetectionSettings) return;
const newTapSettings = new TapDetectionSettingsT(
tapDetectionSettings.fullResetDelay,
tapDetectionSettings.fullResetEnabled,
tapDetectionSettings.fullResetTaps,
tapDetectionSettings.yawResetDelay,
tapDetectionSettings.yawResetEnabled,
tapDetectionSettings.yawResetTaps,
tapDetectionSettings.mountingResetDelay,
tapDetectionSettings.mountingResetEnabled,
tapDetectionSettings.mountingResetTaps,
true
);
sendRPCPacket(
RpcMessage.ChangeSettingsRequest,
new ChangeSettingsRequestT(
null,
null,
null,
null,
null,
null,
null,
newTapSettings
)
);
return () => {
newTapSettings.setupMode = false;
sendRPCPacket(
RpcMessage.ChangeSettingsRequest,
new ChangeSettingsRequestT(
null,
null,
null,
null,
null,
null,
null,
newTapSettings
)
);
};
}, [tapDetectionSettings]);
const trackerPartGrouped = useMemo(
() =>
@@ -103,7 +177,9 @@ export function TrackersAssignPage() {
}, {} as any);
}, [trackers]);
const onTrackerSelected = (tracker: FlatDeviceTracker | null) => {
const onTrackerSelected = (
tracker: FlatDeviceTracker | FlatDeviceTrackerDummy | null
) => {
const assign = (
role: BodyPart,
rotation: QuatT | null,
@@ -129,7 +205,6 @@ export function TrackersAssignPage() {
setSelectRole(BodyPart.NONE);
return;
}
assign(
selectedRole,
tracker.tracker.info?.mountingOrientation || null,
@@ -138,6 +213,17 @@ export function TrackersAssignPage() {
setSelectRole(BodyPart.NONE);
};
useRPCPacket(
RpcMessage.TapDetectionSetupNotification,
(tapSetup: TapDetectionSetupNotificationT) => {
if (selectedRole === BodyPart.NONE || !tapSetup.trackerId) return;
onTrackerSelected({
tracker: { trackerId: tapSetup.trackerId, info: undefined },
});
playTapSetupSound(config?.feedbackSoundVolume);
}
);
applyProgress(0.5);
const { closeChokerWarning, tryOpenChokerWarning, shouldShowChokerWarn } =

View File

@@ -52,9 +52,12 @@ export function TrackerSelectionMenu({
<Typography variant="main-title" bold>
{l10n.getString('tracker_selection_menu-' + BodyPart[bodyPart])}
</Typography>
<div className="w-full max-w-sm">
<TipBox>{l10n.getString('tips-tap_setup')}</TipBox>
</div>
<div className="relative">
<div
className="w-full h-full min-w-[700px] overflow-y-auto p-2 flex flex-col gap-6"
className="w-full h-full min-w-[700px] overflow-y-auto p-2 pt-0 flex flex-col gap-6"
ref={refTrackers}
style={{ height: trackersHeight - optionsHeight }}
>
@@ -115,7 +118,7 @@ export function TrackerSelectionMenu({
ref={refOptions}
>
<div className="w-full max-w-sm">
<TipBox>{l10n.getString('tips-find_tracker')}</TipBox>
{/* <TipBox>{l10n.getString('tips-find_tracker')}</TipBox> */}
</div>
<div className="flex flex-col justify-end pointer-events-auto">
<Button variant="primary" onClick={() => onTrackerSelected(null)}>

View File

@@ -190,6 +190,7 @@ export function GeneralSettings() {
values.tapDetection.mountingResetEnabled;
tapDetection.mountingResetDelay = values.tapDetection.mountingResetDelay;
tapDetection.mountingResetTaps = values.tapDetection.mountingResetTaps;
tapDetection.setupMode = false;
settings.tapDetectionSettings = tapDetection;
const filtering = new FilteringSettingsT();

View File

@@ -57,6 +57,10 @@ export function TrackerPartCard({
setVelocities(velocities);
};
useEffect(() => {
if (!td) setVelocities([0, 0, 0]);
}, [td]);
const globalVelocity = useMemo(
() =>
Math.floor(

View File

@@ -7,6 +7,31 @@ const fullResetStartedSound = new Audio('/sounds/full-reset-started-sound.mp3');
const mountingResetStartedSound = new Audio(
'/sounds/mounting-reset-started-sound.mp3'
);
const tapSetupSound1 = new Audio('/sounds/first-tap.mp3');
const tapSetupSound2 = new Audio('/sounds/second-tap.mp3');
const tapSetupSound3 = new Audio('/sounds/third-tap.mp3');
const tapSetupSound4 = new Audio('/sounds/fourth-tap.mp3');
const tapSetupSound5 = new Audio('/sounds/fifth-tap.mp3');
const tapSetupSoundEnd = new Audio('/sounds/end-tap.mp3');
const tapSetupExtraSound = new Audio('/sounds/tapextrasetup.mp3');
const sounds = [
quickResetStartedSound,
fullResetStartedSound,
mountingResetStartedSound,
tapSetupSound1,
tapSetupSound2,
tapSetupSound3,
tapSetupSound4,
tapSetupSound5,
tapSetupSoundEnd,
tapSetupExtraSound,
];
sounds.forEach((s) => {
s.play();
setTimeout(() => s.pause(), 10);
});
function restartAndPlay(audio: HTMLAudioElement, volume: number) {
audio.volume = Math.min(1, Math.pow(volume, Math.E) + 0.05);
@@ -33,3 +58,29 @@ export function playSoundOnResetStarted(resetType: ResetType, volume = 1) {
}
}
}
let lastKnownVolume = 1;
/* Easter egg */
tapSetupSoundEnd.onended = () => {
if (Math.floor(Math.random() * 12000) !== 0) return;
restartAndPlay(tapSetupExtraSound, lastKnownVolume);
};
const order = [
tapSetupSound1,
tapSetupSound2,
tapSetupSound3,
tapSetupSound4,
tapSetupSound5,
tapSetupSoundEnd,
tapSetupSoundEnd,
tapSetupSoundEnd,
];
let lastTap = 0;
export function playTapSetupSound(volume = 1) {
lastKnownVolume = volume;
restartAndPlay(order[lastTap++], volume);
if (lastTap >= order.length) {
lastTap = 0;
}
}

View File

@@ -84,6 +84,8 @@ dependencies {
implementation("com.melloware:jintellitype:1.+")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0-RC")
implementation("it.unimi.dsi:fastutil:8.5.12")
testImplementation(kotlin("test"))
// Use JUnit test framework
testImplementation(platform("org.junit:junit-bom:5.9.0"))

View File

@@ -16,6 +16,7 @@ import dev.slimevr.protocol.ProtocolAPI;
import dev.slimevr.reset.ResetHandler;
import dev.slimevr.serial.ProvisioningHandler;
import dev.slimevr.serial.SerialHandler;
import dev.slimevr.setup.TapSetupHandler;
import dev.slimevr.tracking.processor.HumanPoseManager;
import dev.slimevr.tracking.processor.skeleton.HumanSkeleton;
import dev.slimevr.tracking.trackers.DeviceManager;
@@ -58,6 +59,7 @@ public class VRServer extends Thread {
private final BVHRecorder bvhRecorder;
private final SerialHandler serialHandler;
private final AutoBoneHandler autoBoneHandler;
private final TapSetupHandler tapSetupHandler;
private final ProtocolAPI protocolAPI;
private final ConfigManager configManager;
private final Timer timer = new Timer();
@@ -87,6 +89,7 @@ public class VRServer extends Thread {
provisioningHandler = new ProvisioningHandler(this);
resetHandler = new ResetHandler();
tapSetupHandler = new TapSetupHandler();
autoBoneHandler = new AutoBoneHandler(this);
protocolAPI = new ProtocolAPI(this);
@@ -446,6 +449,10 @@ public class VRServer extends Thread {
return this.resetHandler;
}
public TapSetupHandler getTapSetupHandler() {
return this.tapSetupHandler;
}
public AutoBoneHandler getAutoBoneHandler() {
return this.autoBoneHandler;
}

View File

@@ -1,106 +0,0 @@
package dev.slimevr.config;
import com.jme3.math.FastMath;
// handles the tap detection config
// this involves the number of taps, the delay, and whether or not the feature is enabled
// for each reset type
public class TapDetectionConfig {
private float yawResetDelay = 0.2f;
private float fullResetDelay = 1.0f;
private float mountingResetDelay = 1.0f;
private boolean yawResetEnabled = true;
private boolean fullResetEnabled = true;
private boolean mountingResetEnabled = true;
private int yawResetTaps = 2;
private int fullResetTaps = 3;
private int mountingResetTaps = 3;
private int numberTrackersOverThreshold = 1;
public float getYawResetDelay() {
return yawResetDelay;
}
public void setYawResetDelay(float yawResetDelay) {
this.yawResetDelay = yawResetDelay;
}
public float getFullResetDelay() {
return fullResetDelay;
}
public void setFullResetDelay(float fullResetDelay) {
this.fullResetDelay = fullResetDelay;
}
public float getMountingResetDelay() {
return mountingResetDelay;
}
public void setMountingResetDelay(float mountingResetDelay) {
this.mountingResetDelay = mountingResetDelay;
}
public boolean getYawResetEnabled() {
return yawResetEnabled;
}
public void setYawResetEnabled(boolean yawResetEnabled) {
this.yawResetEnabled = yawResetEnabled;
}
public boolean getFullResetEnabled() {
return fullResetEnabled;
}
public void setFullResetEnabled(boolean fullResetEnabled) {
this.fullResetEnabled = fullResetEnabled;
}
public boolean getMountingResetEnabled() {
return mountingResetEnabled;
}
public void setMountingResetEnabled(boolean mountingResetEnabled) {
this.mountingResetEnabled = mountingResetEnabled;
}
public int getYawResetTaps() {
return yawResetTaps;
}
// clamp to 2-3 to prevent errors
public void setYawResetTaps(int yawResetTaps) {
this.yawResetTaps = (int) FastMath.clamp(yawResetTaps, 2, 10);
this.yawResetTaps = yawResetTaps;
}
public int getFullResetTaps() {
return fullResetTaps;
}
public void setFullResetTaps(int fullResetTaps) {
this.fullResetTaps = (int) FastMath.clamp(fullResetTaps, 2, 10);
this.fullResetTaps = fullResetTaps;
}
public int getMountingResetTaps() {
return mountingResetTaps;
}
public void setMountingResetTaps(int mountingResetTaps) {
this.mountingResetTaps = (int) FastMath.clamp(mountingResetTaps, 2, 10);
this.mountingResetTaps = mountingResetTaps;
}
public int getNumberTrackersOverThreshold() {
return numberTrackersOverThreshold;
}
public void setNumberTrackersOverThreshold(int numberTrackersOverThreshold) {
this.numberTrackersOverThreshold = numberTrackersOverThreshold;
}
}

View File

@@ -0,0 +1,33 @@
package dev.slimevr.config
import com.jme3.math.FastMath
// handles the tap detection config
// this involves the number of taps, the delay, and whether or not the feature is enabled
// for each reset type
class TapDetectionConfig {
var yawResetDelay = 0.2f
var fullResetDelay = 1.0f
var mountingResetDelay = 1.0f
var yawResetEnabled = true
var fullResetEnabled = true
var mountingResetEnabled = true
var setupMode = false
var yawResetTaps = 2
// clamp to 2-3 to prevent errors
set(yawResetTaps) {
field = FastMath.clamp(yawResetTaps.toFloat(), 2f, 10f).toInt()
field = yawResetTaps
}
var fullResetTaps = 3
set(fullResetTaps) {
field = FastMath.clamp(fullResetTaps.toFloat(), 2f, 10f).toInt()
field = fullResetTaps
}
var mountingResetTaps = 3
set(mountingResetTaps) {
field = FastMath.clamp(mountingResetTaps.toFloat(), 2f, 10f).toInt()
field = mountingResetTaps
}
var numberTrackersOverThreshold = 1
}

View File

@@ -13,6 +13,7 @@ import dev.slimevr.protocol.rpc.reset.RPCResetHandler;
import dev.slimevr.protocol.rpc.serial.RPCProvisioningHandler;
import dev.slimevr.protocol.rpc.serial.RPCSerialHandler;
import dev.slimevr.protocol.rpc.settings.RPCSettingsHandler;
import dev.slimevr.protocol.rpc.setup.RPCTapSetupHandler;
import dev.slimevr.tracking.processor.config.SkeletonConfigOffsets;
import dev.slimevr.tracking.trackers.Tracker;
import dev.slimevr.tracking.trackers.TrackerPosition;
@@ -46,6 +47,7 @@ public class RPCHandler extends ProtocolHandler<RpcMessageHeader>
new RPCSerialHandler(this, api);
new RPCProvisioningHandler(this, api);
new RPCSettingsHandler(this, api);
new RPCTapSetupHandler(this, api);
registerPacketListener(RpcMessage.ResetRequest, this::onResetRequest);
registerPacketListener(RpcMessage.AssignTrackerRequest, this::onAssignTrackerRequest);

View File

@@ -141,7 +141,8 @@ public class RPCSettingsBuilder {
tapDetectionConfig.getYawResetTaps(),
tapDetectionConfig.getMountingResetDelay(),
tapDetectionConfig.getMountingResetEnabled(),
tapDetectionConfig.getMountingResetTaps()
tapDetectionConfig.getMountingResetTaps(),
tapDetectionConfig.getSetupMode()
);
}

View File

@@ -218,21 +218,18 @@ public record RPCSettingsHandler(RPCHandler rpcHandler, ProtocolAPI api) {
if (tapDetectionSettings != null) {
// enable/disable tap detection
tapDetectionConfig
.setYawResetEnabled(tapDetectionSettings.yawResetEnabled());
tapDetectionConfig
.setFullResetEnabled(tapDetectionSettings.fullResetEnabled());
tapDetectionConfig.setYawResetEnabled(tapDetectionSettings.yawResetEnabled());
tapDetectionConfig.setFullResetEnabled(tapDetectionSettings.fullResetEnabled());
tapDetectionConfig
.setMountingResetEnabled(tapDetectionSettings.mountingResetEnabled());
tapDetectionConfig.setSetupMode(tapDetectionSettings.setupMode());
// set tap detection delays
if (tapDetectionSettings.hasYawResetDelay()) {
tapDetectionConfig
.setYawResetDelay(tapDetectionSettings.yawResetDelay());
tapDetectionConfig.setYawResetDelay(tapDetectionSettings.yawResetDelay());
}
if (tapDetectionSettings.hasFullResetDelay()) {
tapDetectionConfig
.setFullResetDelay(tapDetectionSettings.fullResetDelay());
tapDetectionConfig.setFullResetDelay(tapDetectionSettings.fullResetDelay());
}
if (tapDetectionSettings.hasMountingResetDelay()) {
tapDetectionConfig

View File

@@ -0,0 +1,46 @@
package dev.slimevr.protocol.rpc.setup
import com.google.flatbuffers.FlatBufferBuilder
import dev.slimevr.protocol.GenericConnection
import dev.slimevr.protocol.ProtocolAPI
import dev.slimevr.protocol.datafeed.DataFeedBuilder
import dev.slimevr.protocol.rpc.RPCHandler
import dev.slimevr.setup.TapSetupListener
import dev.slimevr.tracking.trackers.Tracker
import solarxr_protocol.rpc.RpcMessage
import solarxr_protocol.rpc.TapDetectionSetupNotification
class RPCTapSetupHandler(
private val rpcHandler: RPCHandler,
val api: ProtocolAPI,
) :
TapSetupListener {
init {
this.api.server.tapSetupHandler.addListener(this)
}
override fun onStarted(tracker: Tracker) {
val fbb = FlatBufferBuilder(32)
val idOffset = DataFeedBuilder.createTrackerId(fbb, tracker)
val update = TapDetectionSetupNotification.createTapDetectionSetupNotification(fbb, idOffset)
val outbound =
rpcHandler.createRPCMessage(fbb, RpcMessage.TapDetectionSetupNotification, update)
fbb.finish(outbound)
forAllListeners {
it.send(
fbb.dataBuffer()
)
}
}
private fun forAllListeners(action: (GenericConnection) -> Unit) {
api
.apiServers
.forEach {
it
.apiConnections
.forEach(action)
}
}
}

View File

@@ -0,0 +1,24 @@
package dev.slimevr.setup
import dev.slimevr.tracking.trackers.Tracker
import java.util.concurrent.CopyOnWriteArrayList
class TapSetupHandler {
private val listeners: MutableList<TapSetupListener> = CopyOnWriteArrayList()
fun addListener(listener: TapSetupListener) {
listeners.add(listener)
}
fun removeListener(listener: TapSetupListener) {
listeners.remove(listener)
}
fun sendTap(tracker: Tracker) {
listeners.forEach { it.onStarted(tracker) }
}
}
interface TapSetupListener {
fun onStarted(tracker: Tracker)
}

View File

@@ -177,7 +177,9 @@ public class HumanSkeleton {
this,
humanPoseManager,
server.getConfigManager().getVrConfig().getTapDetection(),
server.getResetHandler()
server.getResetHandler(),
server.getTapSetupHandler(),
server.getAllTrackers()
);
legTweaks.setConfig(server.getConfigManager().getVrConfig().getLegTweaks());
}

View File

@@ -58,6 +58,10 @@ public class TapDetection {
trackerToWatch = tracker;
}
public Tracker getTracker() {
return trackerToWatch;
}
public int getTaps() {
return taps;
}

View File

@@ -1,8 +1,12 @@
package dev.slimevr.tracking.processor.skeleton;
import java.util.ArrayList;
import java.util.List;
import dev.slimevr.config.TapDetectionConfig;
import dev.slimevr.reset.ResetHandler;
import dev.slimevr.setup.TapSetupHandler;
import dev.slimevr.tracking.processor.HumanPoseManager;
import dev.slimevr.tracking.trackers.Tracker;
import solarxr_protocol.rpc.ResetType;
@@ -13,6 +17,7 @@ import solarxr_protocol.rpc.ResetType;
*/
public class TapDetectionManager {
private static final String resetSourceName = "TapDetection";
private static final int tapsForSetupMode = 2;
// server and related classes
private final HumanSkeleton skeleton;
@@ -24,6 +29,8 @@ public class TapDetectionManager {
private TapDetection fullResetDetector;
private TapDetection mountingResetDetector;
private ArrayList<TapDetection> tapDetectors;
// number of taps to detect
private int yawResetTaps = 2;
private int fullResetTaps = 3;
@@ -41,6 +48,7 @@ public class TapDetectionManager {
private boolean mountingResetAllowPlaySound = true;
private ResetHandler resetHandler;
private TapSetupHandler tapSetupHandler;
public TapDetectionManager(HumanSkeleton skeleton) {
this.skeleton = skeleton;
@@ -50,17 +58,28 @@ public class TapDetectionManager {
HumanSkeleton skeleton,
HumanPoseManager humanPoseManager,
TapDetectionConfig config,
ResetHandler resetHandler
ResetHandler resetHandler,
TapSetupHandler tapSetupHandler,
List<Tracker> trackers
) {
this.skeleton = skeleton;
this.humanPoseManager = humanPoseManager;
this.config = config;
this.resetHandler = resetHandler;
this.tapSetupHandler = tapSetupHandler;
yawResetDetector = new TapDetection(skeleton, getTrackerToWatchYawReset());
fullResetDetector = new TapDetection(skeleton, getTrackerToWatchFullReset());
mountingResetDetector = new TapDetection(skeleton, getTrackerToWatchMountingReset());
// a list of tap detectors for each tracker
tapDetectors = new ArrayList<>();
for (Tracker tracker : trackers) {
TapDetection tapDetector = new TapDetection(skeleton, tracker);
tapDetector.setEnabled(true);
tapDetectors.add(tapDetector);
}
// since this config value is only modified by editing the config file,
// we can set it here
yawResetDetector
@@ -95,17 +114,35 @@ public class TapDetectionManager {
}
public void update() {
if (yawResetDetector == null || fullResetDetector == null || mountingResetDetector == null)
if (
yawResetDetector == null
|| fullResetDetector == null
|| mountingResetDetector == null
|| tapDetectors == null
)
return;
// update the tap detectors
yawResetDetector.update();
fullResetDetector.update();
mountingResetDetector.update();
// check if any tap detectors have detected taps
checkYawReset();
checkFullReset();
checkMountingReset();
// if setup mode is enabled, update the tap detectors for each tracker
if (config.getSetupMode()) {
for (TapDetection tapDetector : tapDetectors) {
tapDetector.update();
if (tapDetector.getTaps() >= tapsForSetupMode) {
tapSetupHandler.sendTap(tapDetector.getTracker());
tapDetector.resetDetector();
}
}
} else {
// update the tap detectors
yawResetDetector.update();
fullResetDetector.update();
mountingResetDetector.update();
// check if any tap detectors have detected taps
checkYawReset();
checkFullReset();
checkMountingReset();
}
}
private void checkYawReset() {