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

Allow multiple servers in the same network #900

Merged
merged 15 commits into from
Mar 15, 2024
10 changes: 10 additions & 0 deletions gui/public/i18n/en/translation.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,9 @@ tracker-settings-drift_compensation_section-edit = Allow drift compensation
tracker-settings-name_section = Tracker name
tracker-settings-name_section-description = Give it a cute nickname :)
tracker-settings-name_section-placeholder = NightyBeast's left leg
tracker-settings-forget = Forget Slime
tracker-settings-forget-description = Removes Slime from SlimeVR so it stops connecting to this PC. The configuration of the tracker will be saved but it will stop trying to connect until this session of SlimeVR is closed.
tracker-settings-forget-label = Forget Slime
ImUrX marked this conversation as resolved.
Show resolved Hide resolved

## Tracker part card info
tracker-part_card-no_name = No name
Expand Down Expand Up @@ -889,3 +892,10 @@ tray_or_exit_modal-radio-exit = Exit on close
tray_or_exit_modal-radio-tray = Minimize to system tray
tray_or_exit_modal-submit = Save
tray_or_exit_modal-cancel = Cancel

## Unknown device modal
unknown_device-modal-title = A new Slime was found!
unknown_device-modal-description = There is a new Slime with MAC address <b>{$deviceId}</b>.
Do you want to connect it to SlimeVR?
unknown_device-modal-confirm = Sure!
unknown_device-modal-forget = Ignore it
ImUrX marked this conversation as resolved.
Show resolved Hide resolved
2 changes: 2 additions & 0 deletions gui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import { InterfaceSettings } from './components/settings/pages/InterfaceSettings
import { error, log } from './utils/logging';
import { AppLayout } from './AppLayout';
import { Preload } from './components/Preload';
import { UnknownDeviceModal } from './components/UnknownDeviceModal';

export const GH_REPO = 'SlimeVR/SlimeVR-Server';
export const VersionContext = createContext('');
Expand All @@ -66,6 +67,7 @@ function Layout() {
<>
<SerialDetectionModal></SerialDetectionModal>
<VersionUpdateModal></VersionUpdateModal>
<UnknownDeviceModal></UnknownDeviceModal>
<Routes>
<Route element={<AppLayout />}>
<Route
Expand Down
104 changes: 104 additions & 0 deletions gui/src/components/UnknownDeviceModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { useState } from 'react';
import { BaseModal } from './commons/BaseModal';
import { Typography } from './commons/Typography';
import { Button } from './commons/Button';
import { Localized, useLocalization } from '@fluent/react';
import { useWebsocketAPI } from '@/hooks/websocket-api';
import { useLocation } from 'react-router-dom';
import {
AddUnknownDeviceRequestT,
RpcMessage,
UnknownDeviceHandshakeNotificationT,
} from 'solarxr-protocol';
import { useDebouncedEffect } from '@/hooks/timeout';
import { useAppContext } from '@/hooks/app';

export function UnknownDeviceModal() {
const { l10n } = useLocalization();
const [open, setOpen] = useState(0);
const { pathname } = useLocation();
const { state, dispatch } = useAppContext();
const [currentTracker, setCurrentTracker] = useState<string | null>(null);
const { useRPCPacket, sendRPCPacket } = useWebsocketAPI();

useRPCPacket(
RpcMessage.UnknownDeviceHandshakeNotification,
({ macAddress }: UnknownDeviceHandshakeNotificationT) => {
if (
['/onboarding/connect-trackers'].includes(pathname) ||
state.ignoredTrackers.has(macAddress as string) ||
(currentTracker !== null && currentTracker !== macAddress)
)
return;

setCurrentTracker(macAddress as string);
setOpen((old) => old + 1);
}
);

useDebouncedEffect(
() => {
setOpen(0);
setCurrentTracker(null);
},
[open],
3000
);

const closeModal = () => {
setCurrentTracker(null);
setOpen(0);
};

return (
<BaseModal isOpen={open !== 0}>
<div className="flex flex-col gap-3">
<div className="flex flex-col items-center gap-3 fill-accent-background-20">
<div className="flex flex-col items-center gap-2">
<Typography variant="main-title">
{l10n.getString('unknown_device-modal-title')}
</Typography>
<Localized
id="unknown_device-modal-description"
elems={{ b: <b></b> }}
vars={{ deviceId: currentTracker ?? 'ERROR' }}
>
<Typography
variant="standard"
textAlign="text-center"
whitespace="whitespace-pre-line"
>
There is a new device in here!
</Typography>
</Localized>
</div>
</div>

<Button
variant="primary"
onClick={() => {
sendRPCPacket(
RpcMessage.AddUnknownDeviceRequest,
new AddUnknownDeviceRequestT(currentTracker)
);
closeModal();
}}
>
{l10n.getString('unknown_device-modal-confirm')}
</Button>
<Button
variant="tertiary"
onClick={() => {
dispatch({
type: 'ignoreTracker',
value: currentTracker as string,
});
closeModal();
}}
>
{l10n.getString('unknown_device-modal-forget')}
</Button>
</div>
</BaseModal>
);
}
11 changes: 11 additions & 0 deletions gui/src/components/onboarding/pages/ConnectTracker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ import classNames from 'classnames';
import { useEffect, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import {
AddUnknownDeviceRequestT,
RpcMessage,
StartWifiProvisioningRequestT,
StopWifiProvisioningRequestT,
UnknownDeviceHandshakeNotificationT,
WifiProvisioningStatus,
WifiProvisioningStatusResponseT,
} from 'solarxr-protocol';
Expand Down Expand Up @@ -97,6 +99,15 @@ export function ConnectTrackersPage() {
}
);

useRPCPacket(
RpcMessage.UnknownDeviceHandshakeNotification,
({ macAddress }: UnknownDeviceHandshakeNotificationT) =>
sendRPCPacket(
RpcMessage.AddUnknownDeviceRequest,
new AddUnknownDeviceRequestT(macAddress)
)
);

const isError =
provisioningStatus === WifiProvisioningStatus.CONNECTION_ERROR ||
provisioningStatus === WifiProvisioningStatus.COULD_NOT_FIND_SERVER;
Expand Down
48 changes: 40 additions & 8 deletions gui/src/components/tracker/TrackerSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useParams } from 'react-router-dom';
import {
AssignTrackerRequestT,
BodyPart,
ForgetDeviceRequestT,
ImuType,
RpcMessage,
} from 'solarxr-protocol';
Expand All @@ -31,6 +32,7 @@ import { IMUVisualizerWidget } from '@/components/widgets/IMUVisualizerWidget';
import { SingleTrackerBodyAssignmentMenu } from './SingleTrackerBodyAssignmentMenu';
import { TrackerCard } from './TrackerCard';
import { Quaternion } from 'three';
import { useAppContext } from '@/hooks/app';

const rotationsLabels: [Quaternion, string][] = [
[rotationToQuatMap.BACK, 'tracker-rotation-back'],
Expand Down Expand Up @@ -64,6 +66,7 @@ export function TrackerSettingsPage() {
},
reValidateMode: 'onSubmit',
});
const { dispatch } = useAppContext();
const { trackerName, allowDriftCompensation } = watch();

const tracker = useTrackerFromId(trackernum, deviceid);
Expand Down Expand Up @@ -124,13 +127,7 @@ export function TrackerSettingsPage() {
updateTrackerSettings();
};

useDebouncedEffect(
() => {
updateTrackerSettings();
},
[trackerName],
1000
);
useDebouncedEffect(() => updateTrackerSettings(), [trackerName], 1000);

useEffect(() => {
updateTrackerSettings();
Expand All @@ -149,6 +146,18 @@ export function TrackerSettingsPage() {
}
}, [firstLoad]);

const macAddress = useMemo(() => {
if (
/(?:[a-zA-Z\d]{2}:){5}[a-zA-Z\d]{2}/.test(
(tracker?.device?.hardwareInfo?.hardwareIdentifier as string | null) ??
''
)
) {
return tracker?.device?.hardwareInfo?.hardwareIdentifier as string;
}
return null;
}, [tracker?.device?.hardwareInfo?.hardwareIdentifier]);

return (
<form
className="h-full overflow-y-auto"
Expand All @@ -165,7 +174,7 @@ export function TrackerSettingsPage() {
onClose={() => setSelectRotation(false)}
onDirectionSelected={onDirectionSelected}
></MountingSelectionMenu>
<div className="flex gap-2 md:h-full max-md:flex-wrap md:flex-row xs:flex-col mobile:flex-col">
<div className="flex gap-2 max-md:flex-wrap md:flex-row xs:flex-col mobile:flex-col">
<div className="flex flex-col w-full md:max-w-xs gap-2">
{tracker && (
<TrackerCard
Expand Down Expand Up @@ -404,6 +413,29 @@ export function TrackerSettingsPage() {
label="Tracker name"
></Input>
</div>
{macAddress && (
<div className="flex flex-col gap-2 w-full mt-3">
<Typography variant="section-title">
{l10n.getString('tracker-settings-forget')}
</Typography>
<Typography color="secondary">
{l10n.getString('tracker-settings-forget-description')}
</Typography>
<Button
variant="secondary"
className="!bg-status-critical self-start"
onClick={() => {
sendRPCPacket(
RpcMessage.ForgetDeviceRequest,
new ForgetDeviceRequestT(macAddress)
);
dispatch({ type: 'ignoreTracker', value: macAddress });
}}
>
{l10n.getString('tracker-settings-forget-label')}
</Button>
</div>
)}
</div>
</div>
</form>
Expand Down
13 changes: 11 additions & 2 deletions gui/src/hooks/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,13 @@ export interface FlatDeviceTracker {
tracker: TrackerDataT;
}

type AppStateAction = { type: 'datafeed'; value: DataFeedUpdateT };
export type AppStateAction =
| { type: 'datafeed'; value: DataFeedUpdateT }
| { type: 'ignoreTracker'; value: string };

export interface AppState {
datafeed?: DataFeedUpdateT;
ignoredTrackers: Set<string>;
}

export interface AppContext {
Expand All @@ -47,8 +50,13 @@ export function reducer(state: AppState, action: AppStateAction) {
switch (action.type) {
case 'datafeed':
return { ...state, datafeed: action.value };
case 'ignoreTracker':
return {
...state,
ignoredTrackers: new Set([...state.ignoredTrackers, action.value]),
};
default:
throw new Error(`unhandled state action ${action.type}`);
throw new Error(`unhandled state action ${(action as AppStateAction).type}`);
}
}

Expand All @@ -59,6 +67,7 @@ export function useProvideAppContext(): AppContext {
const { dataFeedConfig } = useDataFeedConfig();
const [state, dispatch] = useReducer<Reducer<AppState, AppStateAction>>(reducer, {
datafeed: new DataFeedUpdateT(),
ignoredTrackers: new Set(),
});

useEffect(() => {
Expand Down
5 changes: 4 additions & 1 deletion server/core/src/main/java/dev/slimevr/VRServer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import dev.slimevr.reset.ResetHandler
import dev.slimevr.serial.ProvisioningHandler
import dev.slimevr.serial.SerialHandler
import dev.slimevr.serial.SerialHandlerStub
import dev.slimevr.setup.HandshakeHandler
import dev.slimevr.setup.TapSetupHandler
import dev.slimevr.status.StatusSystem
import dev.slimevr.tracking.processor.HumanPoseManager
Expand Down Expand Up @@ -48,7 +49,6 @@ class VRServer @JvmOverloads constructor(
driverBridgeProvider: SteamBridgeProvider = { _, _ -> null },
feederBridgeProvider: (VRServer) -> ISteamVRBridge? = { _ -> null },
serialHandlerProvider: (VRServer) -> SerialHandler = { _ -> SerialHandlerStub() },
// configPath is used by VRWorkout, do not remove!
configPath: String,
) : Thread("VRServer") {
@JvmField
Expand Down Expand Up @@ -97,6 +97,9 @@ class VRServer @JvmOverloads constructor(
@JvmField
val statusSystem = StatusSystem()

@JvmField
val handshakeHandler = HandshakeHandler()

init {
// UwU
configManager = ConfigManager(configPath)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import io.eiren.util.logging.LogManager;

import java.util.Map;
import java.util.regex.Pattern;


public class CurrentVRConfigConverter implements VersionedModelConverter {
Expand Down Expand Up @@ -285,6 +286,24 @@ public ObjectNode convert(
}
}
}

if (version < 13) {
ObjectNode oldTrackersNode = (ObjectNode) modelData.get("trackers");
if (oldTrackersNode != null) {
var fieldNamesIter = oldTrackersNode.fieldNames();
String trackerId;
final String macAddressRegex = "udp://((?:[a-zA-Z\\d]{2}:){5}[a-zA-Z\\d]{2})/0";
final Pattern pattern = Pattern.compile(macAddressRegex);
while (fieldNamesIter.hasNext()) {
trackerId = fieldNamesIter.next();
var matcher = pattern.matcher(trackerId);
if (!matcher.find())
continue;

modelData.withArray("knownDevices").add(matcher.group(1));
}
}
}
} catch (Exception e) {
LogManager.severe("Error during config migration: " + e);
}
Expand Down
Loading
Loading