Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace gamepad logic with useGamepad hook #3377

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
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
294 changes: 87 additions & 207 deletions web/frontend/src/components/gamepad/index.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
<script lang='ts'>

import { onDestroy, onMount } from 'svelte';
import { useGamepad } from '@threlte/extras';
import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb';
import { ConnectionClosedError } from '@viamrobotics/rpc';
import { inputControllerApi as InputController, type ServiceError } from '@viamrobotics/sdk';
Expand All @@ -12,41 +11,43 @@ import { useRobotClient } from '@/hooks/robot-client';
export let name: string;

const { robotClient } = useRobotClient();
const gamepad = useGamepad();

let gamepadIdx: number | null = null;
let gamepadConnectedPrev = false;
let enabled = false;

const curStates : Record<string, number> = ({
X: Number.NaN,
Y: Number.NaN,
RX: Number.NaN,
RY: Number.NaN,
Z: Number.NaN,
RZ: Number.NaN,
Hat0X: Number.NaN,
Hat0Y: Number.NaN,
South: Number.NaN,
East: Number.NaN,
West: Number.NaN,
North: Number.NaN,
LT: Number.NaN,
RT: Number.NaN,
LThumb: Number.NaN,
RThumb: Number.NaN,
Select: Number.NaN,
Start: Number.NaN,
Menu: Number.NaN,
});

let handle = -1;
let prevStates: Record<string, number> = {};
const gamepadKeys = [
'X',
'Y',
'RX',
'RY',
'Z',
'RZ',
'Hat0X',
'Hat0Y',
'South',
'East',
'West',
'North',
'LT',
'RT',
'LThumb',
'RThumb',
'Select',
'Start',
'Menu',
] as const;

let lastError = Date.now();

const round = (num: number, decimals = 4): number => {
return Math.round(num * 10 ** decimals) / 10 ** decimals;
}

const isAxis = (key: string) => {
return ['X', 'Y', 'RX', 'RY'].includes(key)
}

const sendEvent = (newEvent: InputController.Event) => {
if (!enabled) {
return;
}
const req = new InputController.TriggerEventRequest();
req.setController(name);
req.setEvent(newEvent);
Expand All @@ -66,6 +67,7 @@ const sendEvent = (newEvent: InputController.Event) => {
};

let lastTS = Timestamp.fromDate(new Date());

const nextTS = () => {
let nowTS = Timestamp.fromDate(new Date());
if (lastTS.getSeconds() > nowTS.getSeconds() ||
Expand All @@ -80,197 +82,76 @@ const nextTS = () => {
return nowTS;
};

$: currentGamepad = gamepadIdx === null ? null : navigator.getGamepads()[gamepadIdx];

const connectEvent = (con: boolean) => {
const gamepad = currentGamepad;
if (
(con && (!gamepad || !gamepad.connected)) ||
(!con && !gamepadConnectedPrev)
) {
return;
}

const updateConnection = (connected: boolean) => {
const nowTS = nextTS();
try {
for (const ctrl of Object.keys(curStates)) {
const newEvent = new InputController.Event();
nowTS.setNanos(nowTS.getNanos() + 1);
newEvent.setTime(nowTS);
newEvent.setEvent(con ? 'Connect' : 'Disconnect');
newEvent.setValue(0);

if ((/X|Y|Z$/u).test(ctrl)) {
newEvent.setControl(`Absolute${ctrl}`);
} else {
newEvent.setControl(`Button${ctrl}`);
}

sendEvent(newEvent);
}
} finally {
for (const key of gamepadKeys) {
const event = new InputController.Event();
nowTS.setNanos(nowTS.getNanos() + 1);
event.setTime(nowTS);
event.setEvent(connected ? 'Connect' : 'Disconnect');
event.setValue(0);
event.setControl(isAxis(key) ? `Absolute${key}` : `Button${key}`);
sendEvent(event);
lastTS = nowTS;
}
};

const processEvents = (connected: boolean) => {
if (!connected) {
for (const key of Object.keys(curStates)) {
curStates[key] = Number.NaN;
}

if (gamepadConnectedPrev) {
connectEvent(false);
gamepadConnectedPrev = false;
}
return;
} else if (!gamepadConnectedPrev) {
connectEvent(true);
gamepadConnectedPrev = true;
}

const process = (key: typeof gamepadKeys[number], value: number) => {
const nowTS = nextTS();

try {
for (const [key, value] of Object.entries(curStates)) {
if (value === prevStates[key] || (Number.isNaN(value) && Number.isNaN(prevStates[key]))) {
continue;
}
const newEvent = new InputController.Event();
nowTS.setNanos(nowTS.getNanos() + 1);
newEvent.setTime(nowTS);
if ((/X|Y|Z$/u).test(key)) {
newEvent.setControl(`Absolute${key}`);
newEvent.setEvent('PositionChangeAbs');
} else {
newEvent.setControl(`Button${key}`);
newEvent.setEvent(value ? 'ButtonPress' : 'ButtonRelease');
}

if (Number.isNaN(value)) {
newEvent.setEvent('Disconnect');
newEvent.setValue(0);
} else {
newEvent.setValue(value);
}
sendEvent(newEvent);
}
} finally {
lastTS = nowTS;
}
};

const checkVal = (val?: number): number => {
if (!val && val !== 0) {
return Number.NaN;
}
return val;
};

const tick = () => {
const gamepad = currentGamepad;
if (!gamepad || !gamepad.connected) {
if (enabled) {
processEvents(false);
}
return;
}

prevStates = { ...prevStates, ...curStates };

// eslint-disable-next-line unicorn/no-unsafe-regex
const re = /^-?\d+(?:.\d{0,4})?/u;
const trunc = (val?: number): number => {
const checkedVal = checkVal(val);
if (Number.isNaN(checkedVal)) {
return checkedVal;
}
const match = checkedVal.toString().match(re);
if (match && match.length === 0) {
return checkedVal;
}
return Number(match![0]!);
};

/*
* TODO(RSDK-881): this ought to detect actual controller mappings; for now
* just try to not fail.
*/
curStates.X = trunc(gamepad.axes[0]);
curStates.Y = trunc(gamepad.axes[1]);
curStates.RX = trunc(gamepad.axes[2]);
curStates.RY = trunc(gamepad.axes[3]);
curStates.Z = trunc(gamepad.buttons[6]?.value);
curStates.RZ = trunc(gamepad.buttons[7]?.value);
curStates.Hat0X = trunc((checkVal(gamepad.buttons[14]?.value) * -1) + checkVal(gamepad.buttons[15]?.value));
curStates.Hat0Y = trunc((checkVal(gamepad.buttons[12]?.value) * -1) + checkVal(gamepad.buttons[13]?.value));
curStates.South = trunc(gamepad.buttons[0]?.value);
curStates.East = trunc(gamepad.buttons[1]?.value);
curStates.West = trunc(gamepad.buttons[2]?.value);
curStates.North = trunc(gamepad.buttons[3]?.value);
curStates.LT = trunc(gamepad.buttons[4]?.value);
curStates.RT = trunc(gamepad.buttons[5]?.value);
curStates.Select = trunc(gamepad.buttons[8]?.value);
curStates.Start = trunc(gamepad.buttons[9]?.value);
curStates.LThumb = trunc(gamepad.buttons[10]?.value);
curStates.RThumb = trunc(gamepad.buttons[11]?.value);
curStates.Menu = trunc(gamepad.buttons[16]?.value);

if (enabled) {
processEvents(true);
}

handle = window.setTimeout(tick, 10);
const event = new InputController.Event();
nowTS.setNanos(nowTS.getNanos() + 1);
event.setTime(nowTS);
event.setControl(isAxis(key) ? `Absolute${key}` : `Button${key}`);
event.setEvent(isAxis(key) ? 'PositionChangeAbs' : `Button${value === 0 ? 'Release' : 'Press'}`);
event.setValue(value === 0 ? 0 : round(value));
sendEvent(event);
lastTS = nowTS;
};

onMount(() => {
window.addEventListener('gamepadconnected', (event) => {
if (gamepadIdx) {
return;
}
gamepadIdx = event.gamepad.index;
tick();
});
window.addEventListener('gamepaddisconnected', (event) => {
if (gamepadIdx === event.gamepad.index || !currentGamepad?.connected) {
gamepadIdx = null;
}
});
// initial search
const pads = navigator.getGamepads();
for (const pad of pads) {
if (pad) {
gamepadIdx = pad.index;
break;
}
}

if (!gamepadIdx) {
return;
}
prevStates = { ...prevStates, ...curStates };
tick();
});

onDestroy(() => clearTimeout(handle));

$: {
connectEvent(enabled);
}

gamepad.leftStick.on('change', ({ value }) => process('X', value.x))
gamepad.leftStick.on('change', ({ value }) => process('Y', value.y))
gamepad.rightStick.on('change', ({ value }) => process('RX', value.x))
gamepad.rightStick.on('change', ({ value }) => process('RY', value.y))
gamepad.leftStickButton.on('change', ({ value }) => process('LThumb', value))
gamepad.rightStickButton.on('change', ({ value }) => process('RThumb', value))

gamepad.leftTrigger.on('change', ({ value }) => process('Z', value))
gamepad.rightTrigger.on('change', ({ value }) => process('RZ', value))
gamepad.leftBumper.on('change', ({ value }) => process('LT', value))
gamepad.rightBumper.on('change', ({ value }) => process('RT', value))

const onDirXChange = () => process('Hat0X', -gamepad.directionalLeft.value + gamepad.directionalRight.value)
gamepad.directionalLeft.on('change', onDirXChange)
gamepad.directionalRight.on('change', onDirXChange)

const onDirYChange = () => process('Hat0Y', -gamepad.directionalTop.value + gamepad.directionalBottom.value)
gamepad.directionalTop.on('change', onDirYChange)
gamepad.directionalBottom.on('change', onDirYChange)

gamepad.clusterBottom.on('change', ({ value }) => process('South', value))
gamepad.clusterRight.on('change', ({ value }) => process('East', value))
gamepad.clusterLeft.on('change', ({ value }) => process('West', value))
gamepad.clusterTop.on('change', ({ value }) => process('North', value))

gamepad.select.on('change', ({ value }) => process('Select', value))
gamepad.start.on('change', ({ value }) => process('Start', value))
gamepad.center.on('change', ({ value }) => process('Menu', value))

$: connected = gamepad.connected
$: updateConnection($connected && enabled);
</script>

<Collapse title={name}>
<svelte:fragment slot='title'>
<v-breadcrumbs crumbs="input_controller" />

{#if currentGamepad?.connected}
({currentGamepad.id})
{#if $connected}
({gamepad.raw?.id})
{/if}
</svelte:fragment>

<div slot="header">
{#if currentGamepad?.connected && enabled}
{#if $connected && enabled}
<v-badge variant='green' label='Enabled' />
{:else}
<v-badge variant='gray' label='Disabled' />
Expand All @@ -286,9 +167,9 @@ $: {
/>
</div>

{#if currentGamepad?.connected}
{#if $connected}
<div class="flex h-full w-full flex-row justify-between gap-2">
{#each Object.keys(curStates) as stateName, value}
{#each Object.keys({}) as stateName, value}
<div class="ml-0 flex w-[8ex] flex-col text-center">
<p class="subtitle m-0">{stateName}</p>
{value.toFixed((/X|Y|Z$/u).test(stateName.toString()) ? 4 : 0)}
Expand All @@ -304,5 +185,4 @@ $: {
.subtitle {
color: var(--black-70);
}

</style>
Loading