mirror of
https://github.com/SlimeVR/SlimeVR-Server.git
synced 2026-04-06 02:01:58 +02:00
Co-authored-by: ImUrX <urielfontan2002@gmail.com> Co-authored-by: Uriel <imurx@proton.me>
422 lines
13 KiB
TypeScript
422 lines
13 KiB
TypeScript
import { Localized, useLocalization } from '@fluent/react';
|
|
import { Typography } from '@/components/commons/Typography';
|
|
import { LoaderIcon, SlimeState } from '@/components/commons/icon/LoaderIcon';
|
|
import {
|
|
boardTypeToFirmwareToolBoardType,
|
|
useFirmwareTool,
|
|
} from '@/hooks/firmware-tool';
|
|
import { Control, UseFormReset, UseFormWatch, useForm } from 'react-hook-form';
|
|
import { Radio } from '@/components/commons/Radio';
|
|
import { useWebsocketAPI } from '@/hooks/websocket-api';
|
|
import { useEffect, useLayoutEffect, useState } from 'react';
|
|
import { yupResolver } from '@hookform/resolvers/yup';
|
|
|
|
import {
|
|
BoardType,
|
|
DeviceDataT,
|
|
FirmwareUpdateMethod,
|
|
NewSerialDeviceResponseT,
|
|
RpcMessage,
|
|
SerialDeviceT,
|
|
SerialDevicesRequestT,
|
|
SerialDevicesResponseT,
|
|
TrackerStatus,
|
|
} from 'solarxr-protocol';
|
|
import { Button } from '@/components/commons/Button';
|
|
import { useAppContext } from '@/hooks/app';
|
|
import { Input } from '@/components/commons/Input';
|
|
import { Dropdown } from '@/components/commons/Dropdown';
|
|
import { useOnboarding } from '@/hooks/onboarding';
|
|
import { DeviceCardControl } from './DeviceCard';
|
|
import { getTrackerName } from '@/hooks/tracker';
|
|
import { ObjectSchema, object, string } from 'yup';
|
|
|
|
interface FlashingMethodForm {
|
|
flashingMethod?: string;
|
|
serial?: {
|
|
selectedDevicePort: string;
|
|
ssid: string;
|
|
password?: string;
|
|
};
|
|
ota?: {
|
|
selectedDevices: { [key: string]: boolean };
|
|
};
|
|
}
|
|
|
|
function SerialDevicesList({
|
|
control,
|
|
watch,
|
|
reset,
|
|
}: {
|
|
control: Control<FlashingMethodForm>;
|
|
watch: UseFormWatch<FlashingMethodForm>;
|
|
reset: UseFormReset<FlashingMethodForm>;
|
|
}) {
|
|
const { l10n } = useLocalization();
|
|
const { selectDevices } = useFirmwareTool();
|
|
const { sendRPCPacket, useRPCPacket } = useWebsocketAPI();
|
|
const [devices, setDevices] = useState<Record<string, SerialDeviceT>>({});
|
|
const { state, setWifiCredentials } = useOnboarding();
|
|
|
|
useLayoutEffect(() => {
|
|
sendRPCPacket(RpcMessage.SerialDevicesRequest, new SerialDevicesRequestT());
|
|
selectDevices(null);
|
|
reset({
|
|
flashingMethod: FirmwareUpdateMethod.SerialFirmwareUpdate.toString(),
|
|
serial: {
|
|
...state.wifi,
|
|
selectedDevicePort: undefined,
|
|
},
|
|
ota: undefined,
|
|
});
|
|
}, []);
|
|
|
|
useRPCPacket(
|
|
RpcMessage.SerialDevicesResponse,
|
|
(res: SerialDevicesResponseT) => {
|
|
setDevices((old) =>
|
|
res.devices.reduce(
|
|
(curr, device) => ({
|
|
...curr,
|
|
[device?.port?.toString() ?? 'unknown']: device,
|
|
}),
|
|
old
|
|
)
|
|
);
|
|
}
|
|
);
|
|
|
|
useRPCPacket(
|
|
RpcMessage.NewSerialDeviceResponse,
|
|
({ device }: NewSerialDeviceResponseT) => {
|
|
if (device?.port)
|
|
setDevices((old) => ({
|
|
...old,
|
|
[device?.port?.toString() ?? 'unknown']: device,
|
|
}));
|
|
}
|
|
);
|
|
|
|
const serialValues = watch('serial');
|
|
|
|
useEffect(() => {
|
|
if (!serialValues) {
|
|
selectDevices(null);
|
|
return;
|
|
}
|
|
|
|
setWifiCredentials(serialValues.ssid, serialValues.password);
|
|
if (
|
|
serialValues.selectedDevicePort &&
|
|
devices[serialValues.selectedDevicePort]
|
|
) {
|
|
selectDevices([
|
|
{
|
|
type: FirmwareUpdateMethod.SerialFirmwareUpdate,
|
|
deviceId: serialValues.selectedDevicePort,
|
|
deviceNames: [
|
|
devices[serialValues.selectedDevicePort].name?.toString() ??
|
|
'unknown',
|
|
],
|
|
},
|
|
]);
|
|
} else {
|
|
selectDevices(null);
|
|
}
|
|
}, [JSON.stringify(serialValues), devices]);
|
|
|
|
return (
|
|
<>
|
|
<Localized id="firmware-tool_flash-method-serial_wifi">
|
|
<Typography variant="section-title"></Typography>
|
|
</Localized>
|
|
<div className="grid xs-settings:grid-cols-2 mobile-settings:grid-cols-1 gap-3 text-background-10">
|
|
<Localized
|
|
id="onboarding-wifi_creds-ssid"
|
|
attrs={{ placeholder: true, label: true }}
|
|
>
|
|
<Input
|
|
control={control}
|
|
name="serial.ssid"
|
|
label="SSID"
|
|
variant="secondary"
|
|
/>
|
|
</Localized>
|
|
<Localized
|
|
id="onboarding-wifi_creds-password"
|
|
attrs={{ placeholder: true, label: true }}
|
|
>
|
|
<Input
|
|
control={control}
|
|
name="serial.password"
|
|
type="password"
|
|
variant="secondary"
|
|
/>
|
|
</Localized>
|
|
</div>
|
|
<Localized id="firmware-tool_flash-method-serial_devices-label">
|
|
<Typography variant="section-title"></Typography>
|
|
</Localized>
|
|
{Object.keys(devices).length === 0 ? (
|
|
<Localized id="firmware-tool_flash-method-serial_no-devices">
|
|
<Typography variant="standard" color="secondary"></Typography>
|
|
</Localized>
|
|
) : (
|
|
<Dropdown
|
|
control={control}
|
|
name="serial.selectedDevicePort"
|
|
items={Object.keys(devices).map((port) => ({
|
|
label: devices[port].name?.toString() ?? 'unknown',
|
|
value: port,
|
|
}))}
|
|
placeholder={l10n.getString(
|
|
'firmware-tool_flash-method-serial_devices-placeholder'
|
|
)}
|
|
display="block"
|
|
direction="down"
|
|
></Dropdown>
|
|
)}
|
|
</>
|
|
);
|
|
}
|
|
|
|
function OTADevicesList({
|
|
control,
|
|
watch,
|
|
reset,
|
|
}: {
|
|
control: Control<FlashingMethodForm>;
|
|
watch: UseFormWatch<FlashingMethodForm>;
|
|
reset: UseFormReset<FlashingMethodForm>;
|
|
}) {
|
|
const { l10n } = useLocalization();
|
|
const { selectDevices, newConfig } = useFirmwareTool();
|
|
const { state } = useAppContext();
|
|
|
|
const devices =
|
|
state.datafeed?.devices.filter(({ trackers, hardwareInfo }) => {
|
|
// We make sure the device is not one of these types
|
|
if (
|
|
hardwareInfo?.officialBoardType === BoardType.SLIMEVR_LEGACY ||
|
|
hardwareInfo?.officialBoardType === BoardType.SLIMEVR_DEV ||
|
|
hardwareInfo?.officialBoardType === BoardType.CUSTOM
|
|
)
|
|
return false;
|
|
|
|
// if the device has no trackers it is prob misconfigured so we skip for safety
|
|
if (trackers.length <= 0) return false;
|
|
|
|
// We make sure that the tracker is in working condition before doing ota as an error (that could be hardware)
|
|
// could cause an error during the update
|
|
if (!trackers.every(({ status }) => status === TrackerStatus.OK))
|
|
return false;
|
|
|
|
const boardType = hardwareInfo?.officialBoardType ?? BoardType.UNKNOWN;
|
|
return (
|
|
boardTypeToFirmwareToolBoardType[boardType] ===
|
|
newConfig?.boardConfig?.type
|
|
);
|
|
}) || [];
|
|
|
|
const deviceNames = ({ trackers }: DeviceDataT) =>
|
|
trackers
|
|
.map(({ info }) => getTrackerName(l10n, info))
|
|
.filter((i): i is string => !!i);
|
|
|
|
const selectedDevices = watch('ota.selectedDevices');
|
|
|
|
useLayoutEffect(() => {
|
|
reset({
|
|
flashingMethod: FirmwareUpdateMethod.OTAFirmwareUpdate.toString(),
|
|
ota: {
|
|
selectedDevices: devices.reduce(
|
|
(curr, { id }) => ({ ...curr, [id?.id ?? 0]: false }),
|
|
{}
|
|
),
|
|
},
|
|
serial: undefined,
|
|
});
|
|
selectDevices(null);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (selectedDevices) {
|
|
selectDevices(
|
|
Object.keys(selectedDevices)
|
|
.filter((d) => selectedDevices[d])
|
|
.map((id) => id.substring('id-'.length))
|
|
.map((id) => {
|
|
const device = devices.find(
|
|
({ id: dId }) => id === dId?.id.toString()
|
|
);
|
|
|
|
if (!device) throw new Error('no device found');
|
|
return {
|
|
type: FirmwareUpdateMethod.OTAFirmwareUpdate,
|
|
deviceId: id,
|
|
deviceNames: deviceNames(device),
|
|
};
|
|
})
|
|
);
|
|
}
|
|
}, [JSON.stringify(selectedDevices)]);
|
|
|
|
return (
|
|
<>
|
|
<Localized id="firmware-tool_flash-method-ota_devices">
|
|
<Typography variant="section-title"></Typography>
|
|
</Localized>
|
|
{devices.length === 0 && (
|
|
<Localized id="firmware-tool_flash-method-ota_no-devices">
|
|
<Typography color="secondary"></Typography>
|
|
</Localized>
|
|
)}
|
|
<div className="grid xs-settings:grid-cols-2 mobile-settings:grid-cols-1 gap-2">
|
|
{devices.map((device) => (
|
|
<DeviceCardControl
|
|
control={control}
|
|
key={device.id?.id ?? 0}
|
|
name={`ota.selectedDevices.id-${device.id?.id ?? 0}`}
|
|
deviceNames={deviceNames(device)}
|
|
></DeviceCardControl>
|
|
))}
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
|
|
export function FlashingMethodStep({
|
|
nextStep,
|
|
prevStep,
|
|
}: {
|
|
nextStep: () => void;
|
|
prevStep: () => void;
|
|
isActive: boolean;
|
|
}) {
|
|
const { l10n } = useLocalization();
|
|
const { isGlobalLoading, selectedDevices } = useFirmwareTool();
|
|
|
|
const {
|
|
control,
|
|
watch,
|
|
reset,
|
|
formState: { isValid },
|
|
} = useForm<FlashingMethodForm>({
|
|
reValidateMode: 'onChange',
|
|
mode: 'onChange',
|
|
resolver: yupResolver(
|
|
object({
|
|
flashingMethod: string().optional(),
|
|
serial: object().when('flashingMethod', {
|
|
is: FirmwareUpdateMethod.SerialFirmwareUpdate.toString(),
|
|
then: (s) =>
|
|
s
|
|
.shape({
|
|
selectedDevicePort: string().required(),
|
|
ssid: string().required(
|
|
l10n.getString('onboarding-wifi_creds-ssid-required')
|
|
),
|
|
password: string(),
|
|
})
|
|
.required(),
|
|
otherwise: (s) => s.optional(),
|
|
}),
|
|
ota: object().when('flashingMethod', {
|
|
is: FirmwareUpdateMethod.OTAFirmwareUpdate.toString(),
|
|
then: (s) =>
|
|
s
|
|
.shape({
|
|
selectedDevices: object(),
|
|
})
|
|
.required(),
|
|
otherwise: (s) => s.optional(),
|
|
}),
|
|
}) as ObjectSchema<FlashingMethodForm>
|
|
),
|
|
});
|
|
|
|
const flashingMethod = watch('flashingMethod');
|
|
|
|
return (
|
|
<>
|
|
<div className="flex flex-col w-full">
|
|
<div className="flex flex-grow flex-col gap-4">
|
|
<Typography color="secondary">
|
|
{l10n.getString('firmware-tool_flash-method-step_description')}
|
|
</Typography>
|
|
</div>
|
|
<div className="my-4">
|
|
{!isGlobalLoading && (
|
|
<div className="flex flex-col gap-3">
|
|
<div className="grid xs-settings:grid-cols-2 mobile-settings:grid-cols-1 gap-3">
|
|
<Localized
|
|
id="firmware-tool_flash-method-step_ota"
|
|
attrs={{ label: true, description: true }}
|
|
>
|
|
<Radio
|
|
control={control}
|
|
name="flashingMethod"
|
|
value={FirmwareUpdateMethod.OTAFirmwareUpdate.toString()}
|
|
label=""
|
|
></Radio>
|
|
</Localized>
|
|
<Localized
|
|
id="firmware-tool_flash-method-step_serial"
|
|
attrs={{ label: true, description: true }}
|
|
>
|
|
<Radio
|
|
control={control}
|
|
name="flashingMethod"
|
|
value={FirmwareUpdateMethod.SerialFirmwareUpdate.toString()}
|
|
label=""
|
|
></Radio>
|
|
</Localized>
|
|
</div>
|
|
{flashingMethod ===
|
|
FirmwareUpdateMethod.SerialFirmwareUpdate.toString() && (
|
|
<SerialDevicesList
|
|
control={control}
|
|
watch={watch}
|
|
reset={reset}
|
|
></SerialDevicesList>
|
|
)}
|
|
{flashingMethod ===
|
|
FirmwareUpdateMethod.OTAFirmwareUpdate.toString() && (
|
|
<OTADevicesList
|
|
control={control}
|
|
watch={watch}
|
|
reset={reset}
|
|
></OTADevicesList>
|
|
)}
|
|
<div className="flex justify-between">
|
|
<Localized id="firmware-tool_previous-step">
|
|
<Button variant="secondary" onClick={prevStep}></Button>
|
|
</Localized>
|
|
<Localized id="firmware-tool_next-step">
|
|
<Button
|
|
variant="primary"
|
|
disabled={
|
|
!isValid ||
|
|
selectedDevices === null ||
|
|
selectedDevices.length === 0
|
|
}
|
|
onClick={nextStep}
|
|
></Button>
|
|
</Localized>
|
|
</div>
|
|
</div>
|
|
)}
|
|
{isGlobalLoading && (
|
|
<div className="flex justify-center flex-col items-center gap-3 h-44">
|
|
<LoaderIcon slimeState={SlimeState.JUMPY}></LoaderIcon>
|
|
<Localized id="firmware-tool_loading">
|
|
<Typography color="secondary"></Typography>
|
|
</Localized>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|