Skip to content

Commit

Permalink
Automatic firmware update for official trackers (#1241)
Browse files Browse the repository at this point in the history
Co-authored-by: ImUrX <[email protected]>
Co-authored-by: Uriel <[email protected]>
Co-authored-by: Butterscotch! <[email protected]>
Co-authored-by: Eiren Rain <[email protected]>
  • Loading branch information
5 people authored Dec 19, 2024
1 parent 73cdc89 commit 9d65477
Show file tree
Hide file tree
Showing 16 changed files with 1,641 additions and 130 deletions.
7 changes: 5 additions & 2 deletions gui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"@hookform/resolvers": "^3.6.0",
"@react-three/drei": "^9.114.3",
"@react-three/fiber": "^8.17.10",
"@tailwindcss/typography": "^0.5.15",
"@tanstack/react-query": "^5.48.0",
"@tauri-apps/api": "^2.0.2",
"@tauri-apps/plugin-dialog": "^2.0.0",
Expand All @@ -29,9 +30,11 @@
"react-error-boundary": "^4.0.13",
"react-helmet": "^6.1.0",
"react-hook-form": "^7.53.0",
"react-markdown": "^9.0.1",
"react-modal": "^3.16.1",
"react-responsive": "^10.0.0",
"react-router-dom": "^6.26.2",
"remark-gfm": "^4.0.0",
"semver": "^7.6.3",
"solarxr-protocol": "file:../solarxr-protocol",
"three": "^0.163.0",
Expand Down Expand Up @@ -80,14 +83,14 @@
"eslint-plugin-jsx-a11y": "^6.10.0",
"eslint-plugin-react": "^7.37.1",
"eslint-plugin-react-hooks": "^4.6.2",
"globals": "^15.10.0",
"prettier": "^3.3.3",
"rollup-plugin-visualizer": "^5.12.0",
"sass": "^1.79.4",
"spdx-satisfies": "^5.0.1",
"tailwind-gradient-mask-image": "^1.2.0",
"tailwindcss": "^3.4.13",
"vite": "^5.4.8",
"globals": "^15.10.0",
"typescript-eslint": "^8.8.0"
}
}
}
12 changes: 11 additions & 1 deletion gui/public/i18n/en/translation.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -1084,7 +1084,7 @@ firmware-tool_board-pins-step_led-pin =
.label = LED Pin
.placeholder = Enter the pin address of the LED
firmware-tool_board-pins-step_battery-type = Select a battery type
firmware-tool_board-pins-step_battery-type = Select the battery type
firmware-tool_board-pins-step_battery-type_BAT_EXTERNAL = External battery
firmware-tool_board-pins-step_battery-type_BAT_INTERNAL = Internal battery
firmware-tool_board-pins-step_battery-type_BAT_INTERNAL_MCP3021 = Internal MCP3021
Expand Down Expand Up @@ -1207,6 +1207,16 @@ firmware-update_status_ERROR_PROVISIONING_FAILED = Could not set the Wi-Fi crede
firmware-update_status_ERROR_UNSUPPORTED_METHOD = The update method is not supported
firmware-update_status_ERROR_UNKNOWN = Unknown error
## Dedicated Firmware Update Page
firmware-update_title = Firmware update
firmware-update_devices = Available Devices
firmware-update_devices_desc = Please select the trackers you want to update to the lastest version of SlimeVR firmware
firmware-update_no-devices = Plase make sure that the trackers you want to update are ON and connected to Wi-Fi!
firmware-update_changelog_title = Updating to {$version}
firmware-update_looking-for-devices = Looking for devices to update...
firmware-update_retry = Retry
firmware-update_update = Update Selected Trackers
## Tray Menu
tray_menu-show = Show
tray_menu-hide = Hide
Expand Down
9 changes: 9 additions & 0 deletions gui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import { UnknownDeviceModal } from './components/UnknownDeviceModal';
import { useDiscordPresence } from './hooks/discord-presence';
import { EmptyLayout } from './components/EmptyLayout';
import { AdvancedSettings } from './components/settings/pages/AdvancedSettings';
import { FirmwareUpdate } from './components/firmware-update/FirmwareUpdate';

export const GH_REPO = 'SlimeVR/SlimeVR-Server';
export const VersionContext = createContext('');
Expand All @@ -83,6 +84,14 @@ function Layout() {
</MainLayout>
}
/>
<Route
path="/firmware-update"
element={
<MainLayout isMobile={isMobile} widgets={false}>
<FirmwareUpdate />
</MainLayout>
}
/>
<Route
path="/vr-mode"
element={
Expand Down
2 changes: 1 addition & 1 deletion gui/src/components/commons/TipBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export function WarningBox({
>
<WarningIcon></WarningIcon>
</div>
<div className="flex flex-col">
<div className="flex flex-col justify-center">
<Typography
color="text-background-60"
whitespace={whitespace ? 'whitespace-pre-line' : undefined}
Expand Down
18 changes: 13 additions & 5 deletions gui/src/components/firmware-tool/DeviceCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ interface DeviceCardControlProps {
control?: Control<any>;
name?: string;
progress?: number;
disabled?: boolean;
}

export function DeviceCardContent({ deviceNames, status }: DeviceCardProps) {
Expand All @@ -33,7 +34,7 @@ export function DeviceCardContent({ deviceNames, status }: DeviceCardProps) {
</span>
))}
</div>
{status && (
{!!status && (
<Typography color="secondary">
{l10n.getString(
'firmware-update_status_' + FirmwareUpdateStatus[status]
Expand All @@ -48,15 +49,21 @@ export function DeviceCardControl({
control,
name,
progress,
disabled = false,
...props
}: DeviceCardControlProps & DeviceCardProps) {
return (
<div
className={classNames(
'rounded-md bg-background-60 pt-2 flex flex-col justify-between border-2',
props.status && firmwareUpdateErrorStatus.includes(props.status)
? 'border-status-critical'
: 'border-transparent'
'rounded-md bg-background-60 h-[86px] pt-2 flex flex-col justify-between border-2 ',
props.status &&
firmwareUpdateErrorStatus.includes(props.status) &&
'border-status-critical',
props.status === FirmwareUpdateStatus.DONE && 'border-status-success',
(!props.status ||
(props.status !== FirmwareUpdateStatus.DONE &&
!firmwareUpdateErrorStatus.includes(props.status))) &&
'border-transparent'
)}
>
{control && name ? (
Expand All @@ -72,6 +79,7 @@ export function DeviceCardControl({
className={CHECKBOX_CLASSES}
checked={value || false}
type="checkbox"
disabled={disabled}
></input>
</div>

Expand Down
81 changes: 15 additions & 66 deletions gui/src/components/firmware-tool/FlashingStep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,25 @@ import { Typography } from '@/components/commons/Typography';
import {
SelectedDevice,
firmwareUpdateErrorStatus,
getFlashingRequests,
useFirmwareTool,
} from '@/hooks/firmware-tool';
import { useEffect, useMemo, useState } from 'react';
import { useWebsocketAPI } from '@/hooks/websocket-api';
import {
DeviceIdT,
DeviceIdTableT,
FirmwarePartT,
FirmwareUpdateMethod,
FirmwareUpdateRequestT,
FirmwareUpdateStatus,
FirmwareUpdateStatusResponseT,
FirmwareUpdateStopQueuesRequestT,
OTAFirmwareUpdateT,
RpcMessage,
SerialDevicePortT,
SerialFirmwareUpdateT,
} from 'solarxr-protocol';
import { firmwareToolS3BaseUrl } from '@/firmware-tool-api/firmwareToolFetcher';
import { useOnboarding } from '@/hooks/onboarding';
import { DeviceCardControl } from './DeviceCard';
import { WarningBox } from '@/components/commons/TipBox';
import { Button } from '@/components/commons/Button';
import { useNavigate } from 'react-router-dom';
import { firmwareToolS3BaseUrl } from '@/firmware-tool-api/firmwareToolFetcher';

export function FlashingStep({
goTo,
Expand Down Expand Up @@ -60,70 +55,23 @@ export function FlashingStep({
);
};

const queueFlashing = (devices: SelectedDevice[]) => {
const queueFlashing = (selectedDevices: SelectedDevice[]) => {
clear();

if (!buildStatus.firmwareFiles)
throw new Error('invalid state - no firmware files');

const firmware = buildStatus.firmwareFiles.find(
({ isFirmware }) => isFirmware
const requests = getFlashingRequests(
selectedDevices,
buildStatus.firmwareFiles.map(({ url, ...fields }) => ({
url: `${firmwareToolS3BaseUrl}/${url}`,
...fields,
})),
onboardingState,
defaultConfig
);
if (!firmware) throw new Error('invalid state - no firmware to find');

for (const device of devices) {
switch (device.type) {
case FirmwareUpdateMethod.OTAFirmwareUpdate: {
const dId = new DeviceIdT();
dId.id = +device.deviceId;

const part = new FirmwarePartT();
part.offset = 0;
part.url = firmwareToolS3BaseUrl + '/' + firmware.url;

const method = new OTAFirmwareUpdateT();
method.deviceId = dId;
method.firmwarePart = part;

const req = new FirmwareUpdateRequestT();
req.method = method;
req.methodType = FirmwareUpdateMethod.OTAFirmwareUpdate;
sendRPCPacket(RpcMessage.FirmwareUpdateRequest, req);
break;
}
case FirmwareUpdateMethod.SerialFirmwareUpdate: {
const id = new SerialDevicePortT();
id.port = device.deviceId.toString();

if (!onboardingState.wifi?.ssid || !onboardingState.wifi?.password)
throw new Error('invalid state, wifi should be set');

const method = new SerialFirmwareUpdateT();
method.deviceId = id;
method.ssid = onboardingState.wifi.ssid;
method.password = onboardingState.wifi.password;
method.needManualReboot = defaultConfig?.needManualReboot ?? false;

method.firmwarePart = buildStatus.firmwareFiles.map(
({ offset, url }) => {
const part = new FirmwarePartT();
part.offset = offset;
part.url = firmwareToolS3BaseUrl + '/' + url;
return part;
}
);

const req = new FirmwareUpdateRequestT();
req.method = method;
req.methodType = FirmwareUpdateMethod.SerialFirmwareUpdate;
sendRPCPacket(RpcMessage.FirmwareUpdateRequest, req);
break;
}
default: {
throw new Error('unsupported flashing method');
}
}
}
requests.forEach((req) => {
sendRPCPacket(RpcMessage.FirmwareUpdateRequest, req);
});
};

useEffect(() => {
Expand Down Expand Up @@ -189,6 +137,7 @@ export function FlashingStep({
() =>
Object.keys(status).filter((id) =>
[
FirmwareUpdateStatus.NEED_MANUAL_REBOOT,
FirmwareUpdateStatus.DOWNLOADING,
FirmwareUpdateStatus.AUTHENTICATING,
FirmwareUpdateStatus.REBOOTING,
Expand Down
Loading

0 comments on commit 9d65477

Please sign in to comment.