Firmware tool V2 (#1585)

This commit is contained in:
lucas lelievre
2025-10-31 18:16:13 +01:00
committed by GitHub
parent 0a08d57426
commit 4e9fdb206c
28 changed files with 2125 additions and 2485 deletions

View File

@@ -1,8 +1,8 @@
VITE_FIRMWARE_TOOL_URL=https://fw-tool-api.slimevr.io
VITE_FIRMWARE_TOOL_S3_URL=https://fw-tool-bucket.slimevr.io
FIRMWARE_TOOL_SCHEMA_URL=https://fw-tool-api.slimevr.io/api-json
VITE_FIRMWARE_TOOL_URL=https://fw-tool-api-v2.slimevr.io
VITE_FIRMWARE_TOOL_S3_URL=https://fw-tool-bucket-v2.slimevr.io
FIRMWARE_TOOL_SCHEMA_URL=https://fw-tool-api-v2.slimevr.io/api-json
# VITE_FIRMWARE_TOOL_URL=http://localhost:3000
# VITE_FIRMWARE_TOOL_S3_URL=http://localhost:9000
# VITE_FIRMWARE_TOOL_S3_URL=http://localhost:9099
# FIRMWARE_TOOL_SCHEMA_URL=http://localhost:3000/api-json

View File

@@ -27,6 +27,7 @@
"@tauri-apps/plugin-store": "~2",
"@tweenjs/tween.js": "^25.0.0",
"@twemoji/svg": "^15.0.0",
"ajv": "^8.17.1",
"browser-fs-access": "^0.35.0",
"classnames": "^2.5.1",
"flatbuffers": "22.10.26",

View File

@@ -89,6 +89,8 @@ board_type-WEMOSD1MINI = Wemos D1 Mini
board_type-TTGO_TBASE = TTGO T-Base
board_type-ESP01 = ESP-01
board_type-SLIMEVR = SlimeVR
board_type-SLIMEVR_DEV = SlimeVR Dev Board
board_type-SLIMEVR_V1_2 = SlimeVR v1.2
board_type-LOLIN_C3_MINI = Lolin C3 Mini
board_type-BEETLE32C3 = Beetle ESP32-C3
board_type-ESP32C3DEVKITM1 = Espressif ESP32-C3 DevKitM-1
@@ -383,7 +385,8 @@ tracker-settings-name_section-label = Tracker name
tracker-settings-forget = Forget tracker
tracker-settings-forget-description = Removes the tracker from the SlimeVR Server and prevents it from connecting until the server is restarted. The configuration of the tracker won't be lost.
tracker-settings-forget-label = Forget tracker
tracker-settings-update-unavailable = Cannot be updated (DIY)
tracker-settings-update-unavailable-v2 = No releases found
tracker-settings-update-incompatible = Cannot update. Incompatible board
tracker-settings-update-low-battery = Cannot update. Battery lower than 50%
tracker-settings-update-up_to_date = Up to date
tracker-settings-update-blocked = Update not available. No other releases available
@@ -1334,78 +1337,38 @@ firmware_tool-description =
firmware_tool-not_available = Oops, the firmware tool is not available at the moment. Come back later!
firmware_tool-not_compatible = The firmware tool is not compatible with this version of the server. Please update your server!
firmware_tool-board_step = Select your Board
firmware_tool-board_step-description = Select one of the boards listed below.
firmware_tool-select_source = Select the firmware to flash
firmware_tool-select_source-description = Select the firmware you want to flash on your board
firmware_tool-select_source-error = Unable to load Sources
firmware_tool-select_source-board_type = Board Type
firmware_tool-select_source-firmware = Firmware Source
firmware_tool-select_source-version = Firmware Version
firmware_tool-select_source-official = Official
firmware_tool-select_source-dev = Dev
firmware_tool-board_pins_step = Check the pins
firmware_tool-board_pins_step-description =
Please verify that the selected pins are correct.
If you followed the SlimeVR documentation, the default values should be correct.
firmware_tool-board_pins_step-enable_led = Enable LED
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 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
firmware_tool-board_pins_step-battery_type-BAT_MCP3021 = MCP3021
firmware_tool-board_pins_step-battery_sensor_pin =
.label = Battery sensor Pin
.placeholder = Enter the pin address of battery sensor
firmware_tool-board_pins_step-battery_resistor =
.label = Battery Resistor (Ohms)
.placeholder = Enter the value of battery resistor
firmware_tool-board_pins_step-battery_shield_resistor-0 =
.label = Battery Shield R1 (Ohms)
.placeholder = Enter the value of Battery Shield R1
firmware_tool-board_pins_step-battery_shield_resistor-1 =
.label = Battery Shield R2 (Ohms)
.placeholder = Enter the value of Battery Shield R2
firmware_tool-add_imus_step = Declare your IMUs
firmware_tool-add_imus_step-description =
Please add the IMUs that your tracker has.
If you followed the SlimeVR documentation, the default values should be correct.
firmware_tool-add_imus_step-imu_type-label = IMU type
firmware_tool-add_imus_step-imu_type-placeholder = Select the type of IMU
firmware_tool-add_imus_step-imu_rotation =
.label = IMU Rotation (deg)
.placeholder = Rotation angle of the IMU
firmware_tool-add_imus_step-scl_pin =
.label = SCL Pin
.placeholder = Pin address of SCL
firmware_tool-add_imus_step-sda_pin =
.label = SDA Pin
.placeholder = Pin address of SDA
firmware_tool-add_imus_step-int_pin =
.label = INT Pin
.placeholder = Pin address of INT
firmware_tool-add_imus_step-optional_tracker =
.label = Optional tracker
firmware_tool-add_imus_step-show_less = Show Less
firmware_tool-add_imus_step-show_more = Show More
firmware_tool-add_imus_step-add_more = Add more IMUs
firmware_tool-select_firmware_step = Select the firmware version
firmware_tool-select_firmware_step-description =
Please choose what version of the firmware you want to use
firmware_tool-select_firmware_step-show-third-party =
.label = Show third party firmwares
firmware_tool-board_defaults = Configure your board
firmware_tool-board_defaults-description = Set the pins or settings relative to your hardware
firmware_tool-board_defaults-add = Add
firmware_tool-board_defaults-reset = Reset to Default
firmware_tool-board_defaults-error-required = Required field
firmware_tool-board_defaults-error-format = Invalid format
firmware_tool-board_defaults-error-format-number = Not a number
firmware_tool-flash_method_step = Flashing Method
firmware_tool-flash_method_step-description =
Please select the flashing method you want to use
firmware_tool-flash_method_step-ota =
.label = OTA
firmware_tool-flash_method_step-ota-v2 =
.label = Wi-Fi
.description = Use the over-the-air method. Your tracker will use Wi-Fi to update its firmware. Only works on trackers that have been set up.
firmware_tool-flash_method_step-serial =
.label = Serial
firmware_tool-flash_method_step-ota-info =
We use your wifi credentials to flash the tracker and confirm that everything worked correctly.
<b>We do not store your wifi credentials!</b>
firmware_tool-flash_method_step-serial-v2 =
.label = USB
.description = Use a USB cable to update your tracker.
firmware_tool-flashbtn_step = Press the boot btn
firmware_tool-flashbtn_step-description = Before going to the next step, there are a few things you need to do
@@ -1419,10 +1382,10 @@ firmware_tool-flashbtn_step-board_OTHER = Before flashing, you will probably nee
If the flashing process times out at the start, it probably means that the tracker was not in bootloader mode.
Refer to your board's flashing instructions to learn how to enter bootloader mode.
firmware_tool-flash_method_ota-title = Flashing over Wi-Fi
firmware_tool-flash_method_ota-devices = Detected OTA Devices:
firmware_tool-flash_method_ota-no_devices = There are no boards that can be updated using OTA, make sure you selected the correct board type
firmware_tool-flash_method_serial-title = Flashing over USB
firmware_tool-flash_method_serial-wifi = Wi-Fi Credentials:
firmware_tool-flash_method_serial-devices-label = Detected Serial Devices:
firmware_tool-flash_method_serial-devices-placeholder = Select a serial device
@@ -1440,10 +1403,10 @@ firmware_tool-flashing_step-flash_more = Flash more trackers
firmware_tool-flashing_step-exit = Exit
## firmware tool build status
firmware_tool-build-QUEUED = Waiting to build....
firmware_tool-build-CREATING_BUILD_FOLDER = Creating the build folder
firmware_tool-build-DOWNLOADING_FIRMWARE = Downloading the firmware
firmware_tool-build-EXTRACTING_FIRMWARE = Extracting the firmware
firmware_tool-build-SETTING_UP_DEFINES = Configuring the defines
firmware_tool-build-DOWNLOADING_SOURCE = Downloading the source code
firmware_tool-build-EXTRACTING_SOURCE = Extracting the source code
firmware_tool-build-BUILDING = Building the firmware
firmware_tool-build-SAVING = Saving the build
firmware_tool-build-DONE = Build Complete

View File

@@ -1,31 +1,36 @@
import classNames from 'classnames';
import { useMemo } from 'react';
import { forwardRef, useMemo } from 'react';
import { Control, Controller } from 'react-hook-form';
export const CHECKBOX_CLASSES = classNames(
'bg-background-50 border-background-50 rounded-md w-5 h-5 text-accent-background-30 focus:border-accent-background-40 focus:ring-transparent focus:ring-offset-transparent focus:outline-transparent'
);
export function CheckBox({
label,
variant = 'checkbox',
color = 'primary',
control,
outlined,
name,
loading,
// input props
disabled,
...props
}: {
label: string;
control: Control<any>;
name: string;
variant?: 'checkbox' | 'toggle';
color?: 'primary' | 'secondary' | 'tertiary';
outlined?: boolean;
loading?: boolean;
} & React.HTMLProps<HTMLInputElement>) {
export const CheckboxInternal = forwardRef<
HTMLInputElement,
{
disabled?: boolean;
variant?: 'checkbox' | 'toggle';
color?: 'primary' | 'secondary' | 'tertiary';
label?: string;
outlined?: boolean;
loading?: boolean;
name: string;
} & Partial<React.HTMLProps<HTMLInputElement>>
>(function AppCheckbox(
{
variant = 'checkbox',
color = 'primary',
outlined = false,
loading = false,
disabled = false,
label,
onChange,
checked,
name,
},
ref
) {
const classes = useMemo(() => {
const vriantsMap = {
checkbox: {
@@ -44,68 +49,102 @@ export function CheckBox({
return vriantsMap[variant];
}, [variant]);
return (
<div
className={classNames(
{
'rounded-md': outlined,
'text-background-40': disabled,
'text-background-10': !disabled,
'bg-background-60': outlined && color === 'primary',
'bg-background-70': outlined && color === 'secondary',
'bg-background-50': outlined && color === 'tertiary',
},
'flex items-center gap-2 w-full'
)}
>
<label
className={classNames(
'w-full h-[42px] flex gap-2 items-center text-standard-bold',
{
'px-3': outlined,
'cursor-pointer': !disabled || !loading,
'cursor-default': disabled || loading,
}
)}
>
<input
ref={ref}
onChange={onChange}
checked={checked}
name={name}
className={classes.checkbox}
type="checkbox"
disabled={disabled || loading}
/>
{variant === 'toggle' && (
<div
className={classNames(classes.toggle, {
'bg-accent-background-30': checked && !disabled && !loading,
'bg-accent-background-50': checked && disabled,
'bg-accent-background-30 animate-pulse': loading && !disabled,
'bg-background-50':
((!checked && color == 'primary') || color == 'secondary') &&
!loading,
'bg-background-40': !checked && color == 'tertiary' && !loading,
})}
>
<div
className={classNames(classes.pin, {
'left-0': !checked && !loading,
'opacity-0': loading,
'right-0': checked && !loading,
'bg-background-30': disabled,
})}
></div>
</div>
)}
{label}
</label>
</div>
);
});
export function CheckBox({
label,
variant = 'checkbox',
color = 'primary',
control,
outlined,
name,
loading,
disabled,
}: {
label: string;
control: Control<any>;
name: string;
variant?: 'checkbox' | 'toggle';
color?: 'primary' | 'secondary' | 'tertiary';
outlined?: boolean;
loading?: boolean;
} & React.HTMLProps<HTMLInputElement>) {
return (
<Controller
control={control}
name={name}
render={({ field: { onChange, value, ref, name } }) => (
<div
className={classNames(
{
'rounded-lg': outlined,
'text-background-10': !outlined || disabled,
'bg-background-60': outlined && color === 'primary',
'bg-background-70': outlined && color === 'secondary',
'bg-background-50': outlined && color === 'tertiary',
},
'flex items-center gap-2 w-full'
)}
>
<label
className={classNames(
'w-full py-3 flex gap-2 items-center text-standard-bold',
{
'px-3': outlined,
'cursor-pointer': !disabled || !loading,
'cursor-default': disabled || loading,
}
)}
>
<input
ref={ref}
onChange={onChange}
checked={value || false}
name={name}
className={classes.checkbox}
type="checkbox"
disabled={disabled || loading}
{...props}
/>
{variant === 'toggle' && (
<div
className={classNames(classes.toggle, {
'bg-accent-background-30': value && !disabled && !loading,
'bg-accent-background-50': value && disabled,
'bg-accent-background-30 animate-pulse': loading && !disabled,
'bg-background-50':
((!value && color == 'primary') || color == 'secondary') &&
!loading,
'bg-background-40': !value && color == 'tertiary' && !loading,
})}
>
<div
className={classNames(classes.pin, {
'left-0': !value && !loading,
'opacity-0': loading,
'right-0': value && !loading,
'bg-background-30': disabled,
})}
></div>
</div>
)}
{label}
</label>
</div>
<CheckboxInternal
label={label}
variant={variant}
color={color}
outlined={outlined}
name={name}
loading={loading}
disabled={disabled}
checked={value}
onChange={onChange}
ref={ref}
></CheckboxInternal>
)}
/>
);

View File

@@ -1,6 +1,11 @@
import classNames from 'classnames';
import { ReactNode, useEffect, useLayoutEffect, useRef, useState } from 'react';
import { Control, Controller, UseControllerProps } from 'react-hook-form';
import {
Control,
Controller,
FieldError,
UseControllerProps,
} from 'react-hook-form';
import { a11yClick } from '@/utils/a11y';
import { createPortal } from 'react-dom';
import { ArrowDownIcon } from './icon/ArrowIcons';
@@ -11,11 +16,10 @@ interface DropdownProps {
alignment?: 'right' | 'left';
display?: 'fit' | 'block';
placeholder: string;
control: Control<any>;
name: string;
items: DropdownItem[];
maxHeight?: string | number;
rules?: UseControllerProps<any>['rules'];
error?: FieldError;
}
type DropdownItemsProps = Pick<
@@ -129,18 +133,22 @@ export function DropdownItems({
);
}
export function Dropdown({
export function DropdownInside({
direction = 'up',
variant = 'primary',
alignment = 'right',
display = 'fit',
maxHeight = '50vh',
placeholder,
control,
rules,
name,
items = [],
}: DropdownProps) {
value,
onChange,
error,
}: DropdownProps & {
name: string;
value: string;
onChange: (v: string) => void;
}) {
const ref = useRef<HTMLDivElement | null>(null);
const [isOpen, setOpenState] = useState(false);
const [dropdownBounds, setDropdownBounds] = useState<DOMRect>();
@@ -171,69 +179,93 @@ export function Dropdown({
updateBounds();
setOpenState(open);
};
return (
<>
<div className="relative">
<div
ref={ref}
className={classNames(
'min-h-[42px] text-background-10 px-3 py-3 rounded-md focus:ring-4 dropdown',
'flex cursor-pointer',
variant == 'primary' && 'bg-background-60 hover:bg-background-50',
variant == 'secondary' && 'bg-background-70 hover:bg-background-60',
variant == 'tertiary' &&
'bg-accent-background-30 hover:bg-accent-background-20',
display === 'fit' && 'w-fit',
display === 'block' && 'w-full'
)}
onClick={() => setOpen((open) => !open)}
onKeyDown={(ev) => a11yClick(ev) && setOpen((open) => !open)}
tabIndex={0}
>
<div className="flex-grow text-standard first:pointer-events-none">
{items.find((i) => i.value == value)?.component ||
items.find((i) => i.value == value)?.label ||
placeholder}
</div>
<div
className={classNames(
'ml-2 fill-background-10 flex items-center',
direction == 'up' && 'rotate-180',
direction == 'down' && 'rotate-0'
)}
>
<ArrowDownIcon size={16}></ArrowDownIcon>
</div>
</div>
{error?.message && (
<div className="absolute top-[38px] z-0 pt-1.5 bg-background-70 px-1 w-full rounded-b-md text-status-critical">
{error.message}
</div>
)}
</div>
{isOpen &&
dropdownBounds &&
createPortal(
<DropdownItems
items={items}
dropdownBounds={dropdownBounds}
direction={direction}
display={display}
alignment={alignment}
maxHeight={maxHeight}
variant={variant}
value={value}
onSelectItem={(item) => {
setOpen(false);
onChange(item.value);
}}
onBackdropClick={() => {
setOpen(false);
}}
></DropdownItems>,
document.body
)}
</>
);
}
export function Dropdown({
name,
control,
rules,
...props
}: DropdownProps & {
control: Control<any>;
rules?: UseControllerProps<any>['rules'];
}) {
return (
<Controller
control={control}
name={name}
rules={rules}
render={({ field: { onChange, value } }) => (
<>
<div
ref={ref}
className={classNames(
'min-h-[42px] text-background-10 px-5 py-3 rounded-md focus:ring-4 text-center dropdown',
'flex cursor-pointer',
variant == 'primary' && 'bg-background-60 hover:bg-background-50',
variant == 'secondary' &&
'bg-background-70 hover:bg-background-60',
variant == 'tertiary' &&
'bg-accent-background-30 hover:bg-accent-background-20',
display === 'fit' && 'w-fit',
display === 'block' && 'w-full'
)}
onClick={() => setOpen((open) => !open)}
onKeyDown={(ev) => a11yClick(ev) && setOpen((open) => !open)}
tabIndex={0}
>
<div className="flex-grow text-standard first:pointer-events-none">
{items.find((i) => i.value == value)?.component ||
items.find((i) => i.value == value)?.label ||
placeholder}
</div>
<div
className={classNames(
'ml-2 fill-background-10 flex items-center',
direction == 'up' && 'rotate-180',
direction == 'down' && 'rotate-0'
)}
>
<ArrowDownIcon size={16}></ArrowDownIcon>
</div>
</div>
{isOpen &&
dropdownBounds &&
createPortal(
<DropdownItems
items={items}
dropdownBounds={dropdownBounds}
direction={direction}
display={display}
alignment={alignment}
maxHeight={maxHeight}
variant={variant}
value={value}
onSelectItem={(item) => {
setOpen(false);
onChange(item.value);
}}
onBackdropClick={() => {
setOpen(false);
}}
></DropdownItems>,
document.body
)}
</>
<DropdownInside
{...props}
name={name}
onChange={onChange}
value={value}
></DropdownInside>
)}
/>
);

View File

@@ -7,6 +7,7 @@ import {
UseControllerProps,
} from 'react-hook-form';
import { EyeIcon } from './icon/EyeIcon';
import { Typography } from './Typography';
interface InputProps {
variant?: 'primary' | 'secondary' | 'tertiary';
@@ -20,8 +21,8 @@ export const InputInside = forwardRef<
variant?: 'primary' | 'secondary' | 'tertiary';
label?: string;
error?: FieldError;
onChange: () => void;
} & Partial<HTMLInputElement>
autocomplete?: boolean | string;
} & Partial<React.HTMLProps<HTMLInputElement>>
>(function AppInput(
{
type,
@@ -70,7 +71,7 @@ export const InputInside = forwardRef<
variantsMap[variant],
'w-full focus:ring-transparent focus:ring-offset-transparent min-h-[42px] z-10',
'focus:outline-transparent rounded-md focus:border-accent-background-40',
'text-standard relative transition-colors',
'text-standard text-background-10 relative transition-colors',
error && 'border-status-critical border-1'
);
}, [variant, disabled, error]);
@@ -83,7 +84,7 @@ export const InputInside = forwardRef<
return (
<label className="flex flex-col gap-1">
{label}
<Typography>{label}</Typography>
<div className="relative w-full">
<input
type={forceText ? 'text' : type}
@@ -122,15 +123,16 @@ export const Input = ({
name,
placeholder,
label,
autocomplete,
autocomplete = false,
disabled,
variant = 'primary',
rules,
}: {
rules?: UseControllerProps<any>['rules'];
control: Control<any>;
autocomplete?: boolean | string;
} & InputProps &
Partial<HTMLInputElement>) => {
Partial<React.HTMLProps<HTMLInputElement>>) => {
return (
<Controller
control={control}

View File

@@ -1,308 +0,0 @@
import { Localized, useLocalization } from '@fluent/react';
import { Typography } from '@/components/commons/Typography';
import { LoaderIcon, SlimeState } from '@/components/commons/icon/LoaderIcon';
import { useFirmwareTool } from '@/hooks/firmware-tool';
import { Button } from '@/components/commons/Button';
import { Control, useForm } from 'react-hook-form';
import {
CreateImuConfigDTO,
Imudto,
} from '@/firmware-tool-api/firmwareToolSchemas';
import { Dropdown } from '@/components/commons/Dropdown';
import { TrashIcon } from '@/components/commons/icon/TrashIcon';
import { Input } from '@/components/commons/Input';
import {
ArrowDownIcon,
ArrowUpIcon,
} from '@/components/commons/icon/ArrowIcons';
import { useEffect, useRef, useState } from 'react';
import classNames from 'classnames';
import { useElemSize } from '@/hooks/layout';
import { useGetFirmwaresImus } from '@/firmware-tool-api/firmwareToolComponents';
import { CheckBox } from '@/components/commons/Checkbox';
function IMUCard({
control,
imuTypes,
hasIntPin,
index,
onDelete,
}: {
imuTypes: Imudto[];
hasIntPin: boolean;
control: Control<{ imus: CreateImuConfigDTO[] }, any>;
index: number;
onDelete: () => void;
}) {
const { l10n } = useLocalization();
const [open, setOpen] = useState(false);
const ref = useRef<HTMLDivElement | null>(null);
const { height } = useElemSize(ref);
return (
<div className="rounded-lg flex flex-col text-background-10">
<div className="flex gap-3 p-4 shadow-md bg-background-50 rounded-md">
<div className="bg-accent-background-40 rounded-full h-8 w-9 mt-[28px] flex flex-col items-center justify-center">
<Typography variant="section-title" bold>
{index + 1}
</Typography>
</div>
<div className={'w-full flex flex-col gap-2'}>
<div className="grid xs-settings:grid-cols-2 mobile-settings:grid-cols-1 gap-3 fill-background-10">
<label className="flex flex-col justify-end gap-1">
<Localized id="firmware_tool-add_imus_step-imu_type-label"></Localized>
<Dropdown
control={control}
name={`imus[${index}].type`}
items={imuTypes.map(({ type }) => ({
label: type.split('_').slice(1).join(' '),
value: type,
}))}
variant="secondary"
maxHeight="25vh"
placeholder={l10n.getString(
'firmware_tool-add_imus_step-imu_type-placeholder'
)}
direction="down"
display="block"
></Dropdown>
</label>
<Localized
id="firmware_tool-add_imus_step-imu_rotation"
attrs={{ label: true, placeholder: true }}
>
<Input
control={control}
rules={{
required: true,
}}
type="number"
name={`imus[${index}].rotation`}
variant="primary"
label="Rotation Degree"
placeholder="Rotation Degree"
autocomplete="off"
></Input>
</Localized>
</div>
<div
className="duration-500 transition-[height] overflow-hidden"
style={{ height: open ? height : 0 }}
>
<div
ref={ref}
className="grid xs-settings:grid-cols-2 mobile-settings:grid-cols-1 gap-2"
>
<Localized
id="firmware_tool-add_imus_step-scl_pin"
attrs={{ label: true, placeholder: true }}
>
<Input
control={control}
rules={{ required: true }}
type="text"
name={`imus[${index}].sclPin`}
variant="primary"
autocomplete="off"
></Input>
</Localized>
<Localized
id="firmware_tool-add_imus_step-sda_pin"
attrs={{ label: true, placeholder: true }}
>
<Input
control={control}
rules={{ required: true }}
type="text"
name={`imus[${index}].sdaPin`}
variant="primary"
label="SDA Pin"
placeholder="SDA Pin"
autocomplete="off"
></Input>
</Localized>
{hasIntPin && (
<Localized
id="firmware_tool-add_imus_step-int_pin"
attrs={{ label: true, placeholder: true }}
>
<Input
control={control}
rules={{ required: true }}
type="text"
name={`imus[${index}].intPin`}
variant="primary"
autocomplete="off"
></Input>
</Localized>
)}
<label className="flex flex-col justify-end gap-1 md:pt-3 sm:pt-3">
<Localized
id="firmware_tool-add_imus_step-optional_tracker"
attrs={{ label: true }}
>
<CheckBox
control={control}
name={`imus[${index}].optional`}
variant="toggle"
color="tertiary"
label=""
></CheckBox>
</Localized>
</label>
</div>
</div>
</div>
<div className="flex flex-col items-center mt-[25px] fill-background-10">
<Button variant="quaternary" rounded onClick={onDelete}>
<TrashIcon size={15}></TrashIcon>
</Button>
</div>
</div>
<div
className="items-center flex justify-center hover:bg-background-60 bg-background-80 -mt-0.5 transition-colors duration-300 fill-background-10 rounded-b-lg pt-1 pb-0.5"
onClick={() => setOpen(!open)}
>
<Typography>
{l10n.getString(
open
? 'firmware_tool-add_imus_step-show_less'
: 'firmware_tool-add_imus_step-show_more'
)}
</Typography>
{!open && <ArrowDownIcon></ArrowDownIcon>}
{open && <ArrowUpIcon></ArrowUpIcon>}
</div>
</div>
);
}
export function AddImusStep({
nextStep,
prevStep,
isActive,
}: {
nextStep: () => void;
prevStep: () => void;
goTo: (id: string) => void;
isActive: boolean;
}) {
const { l10n } = useLocalization();
const {
isStepLoading: isLoading,
newConfig,
defaultConfig,
updateImus,
} = useFirmwareTool();
const {
control,
formState: { isValid: isValidState },
reset,
watch,
} = useForm<{ imus: CreateImuConfigDTO[] }>({
defaultValues: {
imus: [],
},
reValidateMode: 'onChange',
mode: 'onChange',
});
useEffect(() => {
reset({
imus: newConfig?.imusConfig || [],
});
}, [isActive]);
const { isFetching, data: imuTypes } = useGetFirmwaresImus({});
const isAckchuallyLoading = isFetching || isLoading;
const form = watch();
const addImu = () => {
if (!newConfig || !defaultConfig) throw new Error('unreachable');
const imuPinToAdd =
defaultConfig.imuDefaults[form.imus.length ?? 0] ??
defaultConfig.imuDefaults[0];
const imuTypeToAdd: CreateImuConfigDTO['type'] =
form.imus[0]?.type ?? 'IMU_BNO085';
reset({
imus: [...form.imus, { ...imuPinToAdd, type: imuTypeToAdd }],
});
};
const deleteImu = (index: number) => {
reset({ imus: form.imus.filter((_, i) => i !== index) });
};
return (
<>
<div className="flex flex-col w-full">
<div className="flex flex-col gap-4">
<Typography>
{l10n.getString('firmware_tool-add_imus_step-description')}
</Typography>
</div>
<div className="my-4 flex flex-col gap-4">
{!isAckchuallyLoading && imuTypes && newConfig && (
<>
<div className="flex flex-col gap-3">
<div
className={classNames(
'grid gap-2 px-2',
form.imus.length > 1
? 'md:grid-cols-2 mobile-settings:grid-cols-1'
: 'grid-cols-1'
)}
>
{form.imus.map((imu, index) => (
<IMUCard
control={control}
imuTypes={imuTypes}
key={`${index}:${imu.type}`}
hasIntPin={
imuTypes?.find(({ type: t }) => t == imu.type)
?.hasIntPin ?? false
}
index={index}
onDelete={() => deleteImu(index)}
></IMUCard>
))}
</div>
<div className="flex justify-center">
<Localized id="firmware_tool-add_imus_step-add_more">
<Button variant="primary" onClick={addImu}></Button>
</Localized>
</div>
</div>
<div className="flex justify-between">
<Localized id="firmware_tool-previous_step">
<Button variant="tertiary" onClick={prevStep}></Button>
</Localized>
<Localized id="firmware_tool-next_step">
<Button
variant="primary"
disabled={!isValidState || form.imus.length === 0}
onClick={() => {
updateImus(form.imus);
nextStep();
}}
></Button>
</Localized>
</div>
</>
)}
{isAckchuallyLoading && (
<div className="flex justify-center flex-col items-center gap-3 h-44">
<LoaderIcon slimeState={SlimeState.JUMPY}></LoaderIcon>
<Localized id="firmware_tool-loading">
<Typography></Typography>
</Localized>
</div>
)}
</div>
</div>
</>
);
}

View File

@@ -1,198 +0,0 @@
import { Localized, useLocalization } from '@fluent/react';
import { Typography } from '@/components/commons/Typography';
import { LoaderIcon, SlimeState } from '@/components/commons/icon/LoaderIcon';
import { useFirmwareTool } from '@/hooks/firmware-tool';
import { Button } from '@/components/commons/Button';
import { useForm } from 'react-hook-form';
import { Input } from '@/components/commons/Input';
import { useEffect } from 'react';
import { CheckBox } from '@/components/commons/Checkbox';
import { CreateBoardConfigDTO } from '@/firmware-tool-api/firmwareToolSchemas';
import { Dropdown } from '@/components/commons/Dropdown';
import classNames from 'classnames';
import { useGetFirmwaresBatteries } from '@/firmware-tool-api/firmwareToolComponents';
export type BoardPinsForm = Omit<CreateBoardConfigDTO, 'type'>;
export function BoardPinsStep({
nextStep,
prevStep,
}: {
nextStep: () => void;
prevStep: () => void;
}) {
const { l10n } = useLocalization();
const {
isStepLoading: isLoading,
defaultConfig,
updatePins,
} = useFirmwareTool();
const { isFetching, data: batteryTypes } = useGetFirmwaresBatteries({});
const { reset, control, watch, formState } = useForm<BoardPinsForm>({
reValidateMode: 'onChange',
defaultValues: {
batteryResistances: [0, 0, 0],
},
mode: 'onChange',
});
const formValue = watch();
const ledEnabled = watch('enableLed');
const batteryType = watch('batteryType');
useEffect(() => {
if (!defaultConfig) return;
const { type, ...resetConfig } = defaultConfig.boardConfig;
reset({
...resetConfig,
});
}, [defaultConfig]);
return (
<>
<div className="flex flex-col w-full justify-between text-background-10">
<div className="flex flex-col gap-4">
<Typography>
{l10n.getString('firmware_tool-board_pins_step-description')}
</Typography>
</div>
<div className="my-4 p-2">
{!isLoading && !isFetching && batteryTypes && (
<form className="flex flex-col gap-2">
<div className="grid xs-settings:grid-cols-2 mobile-settings:grid-cols-1 gap-2">
<label className="flex flex-col justify-end">
{/* Allows to have the right spacing at the top of the checkbox */}
<CheckBox
control={control}
color="tertiary"
name="enableLed"
variant="toggle"
outlined
label={l10n.getString(
'firmware_tool-board_pins_step-enable_led'
)}
></CheckBox>
</label>
<Localized
id="firmware_tool-board_pins_step-led_pin"
attrs={{ placeholder: true, label: true }}
>
<Input
control={control}
rules={{ required: true }}
type="text"
name="ledPin"
variant="secondary"
disabled={!ledEnabled}
></Input>
</Localized>
</div>
<div
className={classNames(
batteryType === 'BAT_EXTERNAL' &&
'bg-background-80 p-2 rounded-md',
'transition-all duration-500 flex-col flex gap-2'
)}
>
<Dropdown
control={control}
name="batteryType"
variant="primary"
placeholder={l10n.getString(
'firmware_tool-board_pins_step-battery_type'
)}
direction="up"
display="block"
items={batteryTypes.map((battery) => ({
label: l10n.getString(
'firmware_tool-board_pins_step-battery_type-' + battery
),
value: battery,
}))}
></Dropdown>
{batteryType === 'BAT_EXTERNAL' && (
<div className="grid grid-cols-2 gap-2">
<Localized
id="firmware_tool-board_pins_step-battery_sensor_pin"
attrs={{ placeholder: true, label: true }}
>
<Input
control={control}
rules={{ required: true }}
type="text"
name="batteryPin"
variant="secondary"
></Input>
</Localized>
<Localized
id="firmware_tool-board_pins_step-battery_resistor"
attrs={{ placeholder: true, label: true }}
>
<Input
control={control}
rules={{ required: true, min: 0 }}
type="number"
name="batteryResistances[0]"
variant="secondary"
label="Battery Resistor"
placeholder="Battery Resistor"
></Input>
</Localized>
<Localized
id="firmware_tool-board_pins_step-battery_shield_resistor-0"
attrs={{ placeholder: true, label: true }}
>
<Input
control={control}
rules={{ required: true, min: 0 }}
type="number"
name="batteryResistances[1]"
variant="secondary"
></Input>
</Localized>
<Localized
id="firmware_tool-board_pins_step-battery_shield_resistor-1"
attrs={{ placeholder: true, label: true }}
>
<Input
control={control}
rules={{ required: true, min: 0 }}
type="number"
name="batteryResistances[2]"
variant="secondary"
></Input>
</Localized>
</div>
)}
</div>
</form>
)}
{(isLoading || isFetching) && (
<div className="flex justify-center flex-col items-center gap-3 h-44">
<LoaderIcon slimeState={SlimeState.JUMPY}></LoaderIcon>
<Localized id="firmware_tool-loading">
<Typography></Typography>
</Localized>
</div>
)}
</div>
<div className="flex justify-between">
<Localized id="firmware_tool-previous_step">
<Button variant="tertiary" onClick={prevStep}></Button>
</Localized>
<Localized id="firmware_tool-ok">
<Button
variant="primary"
disabled={Object.keys(formState.errors).length !== 0}
onClick={() => {
updatePins(formValue);
nextStep();
}}
></Button>
</Localized>
</div>
</div>
</>
);
}

View File

@@ -19,6 +19,7 @@ interface DeviceCardProps {
deviceNames: string[];
status?: FirmwareUpdateStatus;
online?: boolean | null;
color?: string;
}
interface DeviceCardControlProps {
@@ -60,6 +61,7 @@ export function DeviceCardControl({
progress,
disabled = false,
online = null,
color = 'bg-background-60',
...props
}: DeviceCardControlProps & DeviceCardProps) {
const cardborder = useMemo(() => {
@@ -80,8 +82,9 @@ export function DeviceCardControl({
return (
<div
className={classNames(
'rounded-md bg-background-60 h-[86px] pt-2 flex flex-col justify-between border-2 relative',
cardborder
'rounded-md h-[86px] pt-2 flex flex-col justify-between border-2 relative',
cardborder,
color
)}
>
{control && name ? (

View File

@@ -3,56 +3,63 @@ import { Typography } from '@/components/commons/Typography';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import {
FirmwareToolContextC,
useFirmwareToolContext,
provideFirmwareTool,
} from '@/hooks/firmware-tool';
import { AddImusStep } from './AddImusStep';
import { SelectBoardStep } from './SelectBoardStep';
import { BoardPinsStep } from './BoardPinsStep';
import VerticalStepper from '@/components/commons/VerticalStepper';
import VerticalStepper, {
VerticalStep,
} from '@/components/commons/VerticalStepper';
import { LoaderIcon, SlimeState } from '@/components/commons/icon/LoaderIcon';
import { Button } from '@/components/commons/Button';
import { SelectFirmwareStep } from './SelectFirmwareStep';
import { BuildStep } from './BuildStep';
import { FlashingMethodStep } from './FlashingMethodStep';
import { FlashingStep } from './FlashingStep';
import { FlashBtnStep } from './FlashBtnStep';
import { FirmwareUpdateMethod } from 'solarxr-protocol';
import { useMemo } from 'react';
import {
useGetHealth,
useGetIsCompatibleVersion,
} from '@/firmware-tool-api/firmwareToolComponents';
import { SelectSourceSetep } from './steps/SelectSourceStep';
import { BoardDefaultsStep } from './steps/BoardDefaultsStep';
import { BuildStep } from './steps/BuildStep';
import { FlashingMethodStep } from './steps/FlashingMethodStep';
import { FirmwareUpdateMethod } from 'solarxr-protocol';
import { FlashBtnStep } from './steps/FlashBtnStep';
import { FlashingStep } from './steps/FlashingStep';
function FirmwareToolContent() {
const { l10n } = useLocalization();
const context = useFirmwareToolContext();
const { isError, isGlobalLoading: isLoading, retry, isCompatible } = context;
const context = provideFirmwareTool();
const { isError, isLoading: isInitialLoading, refetch } = useGetHealth({});
const compatibilityCheckEnabled = !!__VERSION_TAG__;
const { isLoading: isCompatibilityLoading, data: compatibilityData } =
useGetIsCompatibleVersion(
{ pathParams: { version: __VERSION_TAG__ } },
{ enabled: compatibilityCheckEnabled }
);
const isLoading = isInitialLoading || isCompatibilityLoading;
const isCompatible =
!compatibilityCheckEnabled || (compatibilityData?.success ?? false);
const steps = useMemo(() => {
const steps = [
const steps: VerticalStep[] = [
{
id: 'SelectBoard',
component: SelectBoardStep,
title: l10n.getString('firmware_tool-board_step'),
id: 'SelectSource',
component: SelectSourceSetep,
title: l10n.getString('firmware_tool-select_source'),
},
{
component: BoardPinsStep,
title: l10n.getString('firmware_tool-board_pins_step'),
},
{
component: AddImusStep,
title: l10n.getString('firmware_tool-add_imus_step'),
},
{
id: 'SelectFirmware',
component: SelectFirmwareStep,
title: l10n.getString('firmware_tool-select_firmware_step'),
},
{
component: FlashingMethodStep,
id: 'FlashingMethod',
title: l10n.getString('firmware_tool-flash_method_step'),
id: 'Defaults',
component: BoardDefaultsStep,
title: l10n.getString('firmware_tool-board_defaults'),
},
{
id: 'Build',
component: BuildStep,
title: l10n.getString('firmware_tool-build_step'),
},
{
id: 'FlashingMethod',
component: FlashingMethodStep,
title: l10n.getString('firmware_tool-flash_method_step'),
},
{
component: FlashingStep,
title: l10n.getString('firmware_tool-flashing_step'),
@@ -60,18 +67,22 @@ function FirmwareToolContent() {
];
if (
context.defaultConfig?.needBootPress &&
context.selectedDefault?.flashingRules.needBootPress &&
context.selectedDevices?.find(
({ type }) => type === FirmwareUpdateMethod.SerialFirmwareUpdate
)
) {
steps.splice(5, 0, {
steps.splice(4, 0, {
component: FlashBtnStep,
title: l10n.getString('firmware_tool-flashbtn_step'),
});
}
return steps;
}, [context.defaultConfig?.needBootPress, context.selectedDevices, l10n]);
}, [
context.selectedDefault?.flashingRules.needBootPress,
context.selectedDevices,
l10n,
]);
return (
<FirmwareToolContextC.Provider value={context}>
@@ -103,7 +114,7 @@ function FirmwareToolContent() {
</Localized>
)}
<Localized id="firmware_tool-retry">
<Button variant="primary" onClick={retry}></Button>
<Button variant="primary" onClick={() => refetch()}></Button>
</Localized>
</div>
)}

View File

@@ -1,102 +0,0 @@
import { Localized, useLocalization } from '@fluent/react';
import { Typography } from '@/components/commons/Typography';
import { LoaderIcon, SlimeState } from '@/components/commons/icon/LoaderIcon';
import {
firmwareToolToBoardType,
useFirmwareTool,
} from '@/hooks/firmware-tool';
import { CreateBoardConfigDTO } from '@/firmware-tool-api/firmwareToolSchemas';
import classNames from 'classnames';
import { Button } from '@/components/commons/Button';
import { useGetFirmwaresBoards } from '@/firmware-tool-api/firmwareToolComponents';
import { BoardType } from 'solarxr-protocol';
export function SelectBoardStep({
nextStep,
goTo,
}: {
nextStep: () => void;
prevStep: () => void;
goTo: (id: string) => void;
}) {
const { l10n } = useLocalization();
const { selectBoard, newConfig, defaultConfig } = useFirmwareTool();
const { isFetching, data: boards } = useGetFirmwaresBoards({});
return (
<>
<div className="flex flex-col w-full">
<div className="flex flex-grow flex-col gap-4">
<Typography>
{l10n.getString('firmware_tool-board_step-description')}
</Typography>
</div>
<div className="my-4">
{!isFetching && (
<div className="gap-2 flex flex-col">
<div className="grid sm:grid-cols-2 mobile-settings:grid-cols-1 gap-2">
{boards
?.filter(
(board) =>
!!firmwareToolToBoardType[
board as CreateBoardConfigDTO['type']
]
)
.map((board) => (
<div
key={board}
className={classNames(
'p-3 rounded-md hover:bg-background-50',
{
'bg-background-50 text-background-10':
newConfig?.boardConfig?.type === board,
'bg-background-60':
newConfig?.boardConfig?.type !== board,
}
)}
onClick={() => {
selectBoard(board as CreateBoardConfigDTO['type']);
}}
>
{l10n.getString(
`board_type-${
BoardType[
firmwareToolToBoardType[
board as CreateBoardConfigDTO['type']
] ?? BoardType.UNKNOWN
]
}`
)}
</div>
))}
</div>
<div className="flex justify-end">
<Localized id="firmware_tool-next_step">
<Button
variant="primary"
disabled={!newConfig?.boardConfig?.type}
onClick={() => {
if (defaultConfig?.shouldOnlyUseDefaults) {
goTo('SelectFirmware');
} else {
nextStep();
}
}}
></Button>
</Localized>
</div>
</div>
)}
{isFetching && (
<div className="flex justify-center flex-col items-center gap-3 h-44">
<LoaderIcon slimeState={SlimeState.JUMPY}></LoaderIcon>
<Localized id="firmware_tool-loading">
<Typography></Typography>
</Localized>
</div>
)}
</div>
</div>
</>
);
}

View File

@@ -1,120 +0,0 @@
import { Localized, useLocalization } from '@fluent/react';
import { Typography } from '@/components/commons/Typography';
import { useGetFirmwaresVersions } from '@/firmware-tool-api/firmwareToolComponents';
import { LoaderIcon, SlimeState } from '@/components/commons/icon/LoaderIcon';
import { useFirmwareTool } from '@/hooks/firmware-tool';
import classNames from 'classnames';
import { Button } from '@/components/commons/Button';
import { useMemo } from 'react';
import { CheckBox } from '@/components/commons/Checkbox';
import { useForm } from 'react-hook-form';
export function SelectFirmwareStep({
nextStep,
prevStep,
goTo,
}: {
nextStep: () => void;
prevStep: () => void;
goTo: (id: string) => void;
}) {
const { l10n } = useLocalization();
const { selectVersion, newConfig, defaultConfig } = useFirmwareTool();
const { isFetching, data: firmwares } = useGetFirmwaresVersions({});
const { control, watch } = useForm<{ thirdParty: boolean }>({});
const showThirdParty = watch('thirdParty');
const getName = (name: string) => {
return showThirdParty ? name : name.substring(name.indexOf('/') + 1);
};
const filteredFirmwares = useMemo(() => {
return firmwares?.filter(
({ name }) => name.split('/')[0] === 'SlimeVR' || showThirdParty
);
}, [firmwares, showThirdParty]);
return (
<>
<div className="flex flex-col w-full">
<div className="flex justify-between items-center mobile:flex-col gap-4">
<Typography>
{l10n.getString('firmware_tool-select_firmware_step-description')}
</Typography>
<div>
<Localized
id="firmware_tool-select_firmware_step-show-third-party"
attrs={{ label: true }}
>
<CheckBox
control={control}
name="thirdParty"
label="Show third party firmwares"
></CheckBox>
</Localized>
</div>
</div>
<div className="my-4">
{!isFetching && (
<div className="flex flex-col gap-4">
<div className="xs-settings:max-h-96 xs-settings:overflow-y-auto xs-settings:px-2">
<div className="grid sm:grid-cols-2 mobile-settings:grid-cols-1 gap-2">
{filteredFirmwares?.map((firmware) => (
<div
key={firmware.id}
className={classNames(
'p-3 rounded-md hover:bg-background-50',
{
'bg-background-50 text-background-10':
newConfig?.version === firmware.name,
'bg-background-60':
newConfig?.version !== firmware.name,
}
)}
onClick={() => {
selectVersion(firmware.name);
}}
>
{getName(firmware.name)}
</div>
))}
</div>
</div>
<div className="flex justify-between">
<Localized id="firmware_tool-previous_step">
<Button
variant="tertiary"
onClick={() => {
if (defaultConfig?.shouldOnlyUseDefaults) {
goTo('SelectBoard');
} else {
prevStep();
}
}}
></Button>
</Localized>
<Localized id="firmware_tool-next_step">
<Button
variant="primary"
disabled={!newConfig?.version}
onClick={nextStep}
></Button>
</Localized>
</div>
</div>
)}
{isFetching && (
<div className="flex justify-center flex-col items-center gap-3 h-44">
<LoaderIcon slimeState={SlimeState.JUMPY}></LoaderIcon>
<Localized id="firmware_tool-loading">
<Typography></Typography>
</Localized>
</div>
)}
</div>
</div>
</>
);
}

View File

@@ -0,0 +1,229 @@
import { Localized, useLocalization } from '@fluent/react';
import { Typography } from '@/components/commons/Typography';
import {
boardComponentGraph,
ComponentNode,
SelectedSouce,
useFirmwareTool,
validateSource,
} from '@/hooks/firmware-tool';
import { useEffect, useState } from 'react';
import { CheckboxInternal } from '@/components/commons/Checkbox';
import { InputInside } from '@/components/commons/Input';
import { DropdownInside } from '@/components/commons/Dropdown';
import classNames from 'classnames';
import { Button } from '@/components/commons/Button';
import { TrashIcon } from '@/components/commons/icon/TrashIcon';
function BoardDefaultsGraph({ graph }: { graph: ComponentNode[] }) {
const { l10n } = useLocalization();
const renderComponent = (
c: ComponentNode,
depth = 0,
onDelete?: () => void
) => {
if (c.type === 'group') {
return (
<div
key={c.path.join('/')}
className={classNames('flex flex-col rounded-lg', {
'p-4 ': depth !== 0,
'bg-background-80': depth >= 1,
})}
>
<div className="flex justify-between items-center">
<Typography variant="section-title">{c.label}</Typography>
{onDelete && (
<div
className="p-2 rounded-full fill-background-10 hover:bg-background-50 hover:fill-status-critical cursor-pointer"
onClick={() => onDelete && onDelete()}
>
<TrashIcon size={20}></TrashIcon>
</div>
)}
</div>
<div
className={classNames({
'flex flex-col gap-4': depth + 1 <= 1,
'grid sm:grid-cols-2 gap-2 items-end': depth + 1 > 1,
})}
>
{c.childrens.map((c) => renderComponent(c, depth + 1))}
</div>
</div>
);
}
if (c.type === 'checkbox') {
return (
<div key={c.path.join('/')}>
<CheckboxInternal
name={c.label}
label={c.label}
onChange={(e) => c.onMutate(e.currentTarget.checked)}
checked={c.value}
variant="toggle"
outlined
></CheckboxInternal>
</div>
);
}
if (c.type === 'text') {
return (
<div className="flex flex-col pt-2" key={c.path.join('/')}>
<InputInside
name={c.label}
label={c.label}
placeholder={c.label}
value={c.value}
type={c.format === 'number' ? 'number' : 'text'}
error={
c.error
? { type: 'validate', message: l10n.getString(c.error) }
: undefined
}
variant="primary"
onChange={(e) => c.onMutate(e.currentTarget.value)}
></InputInside>
</div>
);
}
if (c.type === 'dropdown') {
return (
<div className="flex flex-col pt-2 gap-1" key={c.path.join('/')}>
<Typography>{c.label}</Typography>
<DropdownInside
items={c.items.map((i) => ({ value: i, label: i }))}
name={c.label}
onChange={c.onMutate}
error={
c.error
? { type: 'validate', message: l10n.getString(c.error) }
: undefined
}
placeholder={c.label}
display="block"
value={c.value}
variant="secondary"
maxHeight={200}
></DropdownInside>
</div>
);
}
if (c.type === 'list') {
return (
<div
key={c.path.join('/')}
className={classNames('flex flex-col gap-2 rounded-lg', {
'p-4': depth >= 2,
'bg-background-70': depth >= 1,
})}
>
<Typography variant="section-title">{c.label}</Typography>
<div
className={classNames({
'grid sm:grid-cols-2 gap-2': true,
})}
>
{c.childrens.map((c2, index) =>
renderComponent(
c2,
depth + 1,
c.del ? () => c.del?.(index) : undefined
)
)}
{c.add && (
<div className="flex flex-col justify-center">
<div className="flex justify-center">
<Localized id="firmware_tool-board_defaults-add">
<Button variant="primary" onClick={c.add}></Button>
</Localized>
</div>
</div>
)}
</div>
</div>
);
}
};
return (
<div className="flex flex-col gap-2">
{graph.map((c) => renderComponent(c, 0))}
</div>
);
}
export function BoardDefaultsStep({
nextStep,
prevStep,
}: {
nextStep: () => void;
prevStep: () => void;
goTo: (id: string) => void;
}) {
const { l10n } = useLocalization();
const { selectedSource, setSelectedSource } = useFirmwareTool();
const [graph, setGraph] = useState<ComponentNode[]>([]);
const [temporarySource, setTemporarySource] = useState<SelectedSouce>();
const [valid, setValid] = useState<boolean>(false);
const [tr, setTr] = useState(0);
useEffect(() => {
if (!selectedSource) return;
setTemporarySource(JSON.parse(JSON.stringify(selectedSource))); // make a deep copy bc the graph will modify temporarySource by references
}, [selectedSource]);
useEffect(() => {
if (!temporarySource) return;
const components = boardComponentGraph(temporarySource, () => {
setTr((tr) => tr + 1);
});
setGraph(components);
setValid(validateSource(temporarySource));
}, [temporarySource, tr]);
const submit = () => {
setSelectedSource(temporarySource);
nextStep();
};
const reset = () => {
setTemporarySource(selectedSource);
setTr((tr) => tr + 1);
};
return (
<>
<div className="flex flex-col w-full gap-4">
<div className="flex flex-grow flex-col gap-4">
<Typography>
{l10n.getString('firmware_tool-board_defaults-description')}
</Typography>
</div>
<BoardDefaultsGraph graph={graph}></BoardDefaultsGraph>
<div className="flex justify-between">
<Localized id="firmware_tool-previous_step">
<Button variant="secondary" onClick={prevStep}></Button>
</Localized>
<div className="flex gap-2">
<Localized id="firmware_tool-board_defaults-reset">
<Button variant="secondary" onClick={reset}></Button>
</Localized>
<Localized id="firmware_tool-ok">
<Button
variant="primary"
disabled={!valid}
onClick={submit}
></Button>
</Localized>
</div>
</div>
</div>
</>
);
}

View File

@@ -1,18 +1,19 @@
import { Localized, useLocalization } from '@fluent/react';
import { Typography } from '@/components/commons/Typography';
import { fetchPostFirmwaresBuild } from '@/firmware-tool-api/firmwareToolComponents';
import { LoaderIcon, SlimeState } from '@/components/commons/icon/LoaderIcon';
import { useFirmwareTool } from '@/hooks/firmware-tool';
import {
BuildResponseDTO,
CreateBuildFirmwareDTO,
} from '@/firmware-tool-api/firmwareToolSchemas';
import { useEffect, useMemo } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { firmwareToolBaseUrl } from '@/firmware-tool-api/firmwareToolFetcher';
import { Button } from '@/components/commons/Button';
import { fetchPostFirmwareBuild } from '@/firmware-tool-api/firmwareToolComponents';
import {
BuildStatusBasic,
BuildStatusDone,
} from '@/firmware-tool-api/firmwareToolSchemas';
export function BuildStep({
isActive,
prevStep,
goTo,
nextStep,
}: {
@@ -22,23 +23,33 @@ export function BuildStep({
isActive: boolean;
}) {
const { l10n } = useLocalization();
const { isGlobalLoading, newConfig, setBuildStatus, buildStatus } =
useFirmwareTool();
const { selectedSource, setFiles, selectedDefault } = useFirmwareTool();
const [buildStatus, setBuildStatus] = useState<
BuildStatusDone | BuildStatusBasic
>({ status: 'QUEUED', id: '' });
const startBuild = async () => {
if (!selectedSource) throw 'invalid state - no source';
try {
const res = await fetchPostFirmwaresBuild({
body: newConfig as CreateBuildFirmwareDTO,
const values =
selectedSource.default?.data.defaults[selectedSource.source.board];
if (!values) throw 'invalid state - no values';
const res = await fetchPostFirmwareBuild({
body: {
...selectedSource.source,
values,
},
});
setBuildStatus(res);
if (res.status !== 'DONE') {
const events = new EventSource(
`${firmwareToolBaseUrl}/firmwares/build-status/${res.id}`
`${firmwareToolBaseUrl}/firmware/build-status/${res.id}`
);
events.onmessage = ({ data }) => {
const buildEvent: BuildResponseDTO = JSON.parse(data);
setBuildStatus(buildEvent);
setBuildStatus(JSON.parse(data));
};
}
} catch (e) {
@@ -55,6 +66,7 @@ export function BuildStep({
useEffect(() => {
if (!isActive) return;
if (buildStatus.status === 'DONE') {
setFiles(buildStatus.files);
nextStep();
}
}, [buildStatus]);
@@ -73,35 +85,38 @@ export function BuildStep({
</Typography>
</div>
<div className="my-4">
{!isGlobalLoading && (
<div className="flex justify-center flex-col items-center gap-3 h-44">
<LoaderIcon
slimeState={
buildStatus.status !== 'ERROR'
? SlimeState.JUMPY
: SlimeState.SAD
}
></LoaderIcon>
<Typography variant="section-title">
{l10n.getString('firmware_tool-build-' + buildStatus.status)}
</Typography>
</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></Typography>
</Localized>
</div>
)}
<div className="flex justify-center flex-col items-center gap-3 h-44">
<LoaderIcon
slimeState={
buildStatus.status !== 'ERROR'
? SlimeState.JUMPY
: SlimeState.SAD
}
></LoaderIcon>
<Typography variant="section-title">
{l10n.getString('firmware_tool-build-' + buildStatus.status)}
</Typography>
</div>
</div>
<div className="flex justify-end">
<div className="flex justify-end gap-2">
<Localized id="firmware_tool-previous_step">
<Button
variant="secondary"
disabled={hasPendingBuild}
onClick={() => {
if (selectedDefault?.flashingRules.shouldOnlyUseDefaults) {
goTo('SelectSource');
} else {
prevStep();
}
}}
></Button>
</Localized>
<Localized id="firmware_tool-retry">
<Button
variant="secondary"
disabled={hasPendingBuild}
onClick={() => goTo('FlashingMethod')}
onClick={() => startBuild()}
></Button>
</Localized>
</div>

View File

@@ -1,22 +1,15 @@
import { Localized, useLocalization } from '@fluent/react';
import { Typography } from '@/components/commons/Typography';
import { Button } from '@/components/commons/Button';
import {
boardTypeToFirmwareToolBoardType,
useFirmwareTool,
} from '@/hooks/firmware-tool';
import { BoardType } from 'solarxr-protocol';
import { useFirmwareTool } from '@/hooks/firmware-tool';
import { VerticalStepComponentProps } from '@/components/commons/VerticalStepper';
export function FlashBtnStep({
nextStep,
}: {
nextStep: () => void;
prevStep: () => void;
goTo: (id: string) => void;
isActive: boolean;
}) {
prevStep,
}: VerticalStepComponentProps) {
const { l10n } = useLocalization();
const { defaultConfig } = useFirmwareTool();
const { selectedSource } = useFirmwareTool();
return (
<>
@@ -25,8 +18,8 @@ export function FlashBtnStep({
<Typography>
{l10n.getString('firmware_tool-flashbtn_step-description')}
</Typography>
{defaultConfig?.boardConfig.type ===
boardTypeToFirmwareToolBoardType[BoardType.SLIMEVR] ? (
{selectedSource?.source.board === 'BOARD_SLIMEVR' ||
selectedSource?.source.board === 'BOARD_SLIMEVR_V1_2' ? (
<>
<Typography variant="standard" whitespace="whitespace-pre">
{l10n.getString('firmware_tool-flashbtn_step-board_SLIMEVR')}
@@ -69,7 +62,15 @@ export function FlashBtnStep({
</Typography>
</>
)}
<div className="flex justify-end">
<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"

View File

@@ -1,10 +1,6 @@
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 { SelectedDevice, 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';
@@ -12,7 +8,6 @@ import { useEffect, useLayoutEffect, useState } from 'react';
import { yupResolver } from '@hookform/resolvers/yup';
import {
BoardType,
DeviceDataT,
FirmwareUpdateMethod,
NewSerialDeviceResponseT,
@@ -26,11 +21,13 @@ import { Button } from '@/components/commons/Button';
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';
import { useAtomValue } from 'jotai';
import { devicesAtom } from '@/store/app-store';
import { DeviceCardControl } from '@/components/firmware-tool/DeviceCard';
import { LoaderIcon, SlimeState } from '@/components/commons/icon/LoaderIcon';
import { TipBox } from '@/components/commons/TipBox';
interface FlashingMethodForm {
flashingMethod?: string;
@@ -45,10 +42,12 @@ interface FlashingMethodForm {
}
function SerialDevicesList({
isActive,
control,
watch,
reset,
}: {
isActive: boolean;
control: Control<FlashingMethodForm>;
watch: UseFormWatch<FlashingMethodForm>;
reset: UseFormReset<FlashingMethodForm>;
@@ -57,9 +56,11 @@ function SerialDevicesList({
const { selectDevices } = useFirmwareTool();
const { sendRPCPacket, useRPCPacket } = useWebsocketAPI();
const [devices, setDevices] = useState<Record<string, SerialDeviceT>>({});
const [loading, setLoading] = useState(false);
const { state, setWifiCredentials } = useOnboarding();
useLayoutEffect(() => {
setLoading(true);
sendRPCPacket(RpcMessage.SerialDevicesRequest, new SerialDevicesRequestT());
selectDevices(null);
reset({
@@ -75,15 +76,16 @@ function SerialDevicesList({
useRPCPacket(
RpcMessage.SerialDevicesResponse,
(res: SerialDevicesResponseT) => {
setDevices((old) =>
setDevices(
res.devices.reduce(
(curr, device) => ({
...curr,
[device?.port?.toString() ?? 'unknown']: device,
}),
old
{}
)
);
setLoading(false);
}
);
@@ -95,6 +97,7 @@ function SerialDevicesList({
...old,
[device?.port?.toString() ?? 'unknown']: device,
}));
setLoading(false);
}
);
@@ -126,12 +129,39 @@ function SerialDevicesList({
}
}, [JSON.stringify(serialValues), devices]);
useEffect(() => {
if (isActive) {
const id = setInterval(() => {
console.log('request');
sendRPCPacket(
RpcMessage.SerialDevicesRequest,
new SerialDevicesRequestT()
);
}, 3000);
return () => {
clearInterval(id);
};
}
});
return (
<>
<div className="p-4 rounded-lg bg-background-60 w-full flex flex-col gap-3">
<Localized id="firmware_tool-flash_method_serial-title">
<Typography variant="main-title"></Typography>
</Localized>
<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">
<div className="flex flex-col gap-3 text-background-10">
<TipBox>
<Localized
id={'firmware_tool-flash_method_step-ota-info'}
elems={{ b: <b></b> }}
>
<Typography whitespace="whitespace-pre-wrap"></Typography>
</Localized>
</TipBox>
<Localized
id="onboarding-wifi_creds-ssid"
attrs={{ placeholder: true, label: true }}
@@ -158,10 +188,13 @@ function SerialDevicesList({
<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"></Typography>
</Localized>
{Object.keys(devices).length === 0 && !loading ? (
<div className="flex justify-center items-center flex-col gap-4 py-4">
<LoaderIcon slimeState={SlimeState.SAD}></LoaderIcon>
<Localized id="firmware_tool-flash_method_serial-no_devices">
<Typography variant="standard"></Typography>
</Localized>
</div>
) : (
<Dropdown
control={control}
@@ -175,40 +208,30 @@ function SerialDevicesList({
)}
display="block"
direction="down"
variant="secondary"
></Dropdown>
)}
</>
</div>
);
}
function OTADevicesList({
isActive,
control,
watch,
reset,
}: {
isActive: boolean;
control: Control<FlashingMethodForm>;
watch: UseFormWatch<FlashingMethodForm>;
reset: UseFormReset<FlashingMethodForm>;
}) {
const { l10n } = useLocalization();
const { selectDevices, newConfig } = useFirmwareTool();
const { selectDevices } = useFirmwareTool();
const allDevices = useAtomValue(devicesAtom);
const devices =
allDevices.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 ||
hardwareInfo?.officialBoardType === BoardType.OWOTRACK ||
hardwareInfo?.officialBoardType === BoardType.WRANGLER ||
hardwareInfo?.officialBoardType === BoardType.MOCOPI ||
hardwareInfo?.officialBoardType === BoardType.HARITORA ||
hardwareInfo?.officialBoardType === BoardType.DEV_RESERVED
)
return false;
allDevices.filter(({ trackers }) => {
// if the device has no trackers it is prob misconfigured so we skip for safety
if (trackers.length <= 0) return false;
@@ -216,12 +239,7 @@ function OTADevicesList({
// 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
);
return true;
}) || [];
const deviceNames = ({ trackers }: DeviceDataT) =>
@@ -232,50 +250,59 @@ function OTADevicesList({
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);
}, []);
if (isActive) {
reset({
flashingMethod: FirmwareUpdateMethod.OTAFirmwareUpdate.toString(),
ota: {
selectedDevices: devices.reduce(
(curr, { id }) => ({ ...curr, [id?.id ?? 0]: false }),
{}
),
},
serial: undefined,
});
selectDevices(null);
}
}, [isActive]);
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 {
Object.keys(selectedDevices).reduce((curr, id) => {
if (!selectedDevices[id]) return curr;
const deviceId = id.substring('id-'.length);
const device = devices.find(
({ id: dId }) => deviceId === dId?.id.toString()
);
if (!device) return curr;
return [
...curr,
{
type: FirmwareUpdateMethod.OTAFirmwareUpdate,
deviceId: id,
deviceId,
deviceNames: deviceNames(device),
};
})
},
];
}, [] as SelectedDevice[])
);
}
}, [JSON.stringify(selectedDevices)]);
return (
<>
<div className="p-4 rounded-lg bg-background-60 w-full flex flex-col gap-3">
<Localized id="firmware_tool-flash_method_ota-title">
<Typography variant="main-title"></Typography>
</Localized>
<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></Typography>
</Localized>
<div className="flex justify-center items-center flex-col gap-4 py-4">
<LoaderIcon slimeState={SlimeState.SAD}></LoaderIcon>
<Localized id="firmware_tool-flash_method_ota-no_devices">
<Typography variant="standard"></Typography>
</Localized>
</div>
)}
<div className="grid xs-settings:grid-cols-2 mobile-settings:grid-cols-1 gap-2">
{devices.map((device) => (
@@ -284,23 +311,26 @@ function OTADevicesList({
key={device.id?.id ?? 0}
name={`ota.selectedDevices.id-${device.id?.id ?? 0}`}
deviceNames={deviceNames(device)}
color="bg-background-70"
></DeviceCardControl>
))}
</div>
</>
</div>
);
}
export function FlashingMethodStep({
nextStep,
prevStep,
goTo,
isActive,
}: {
nextStep: () => void;
prevStep: () => void;
goTo: (to: string) => void;
isActive: boolean;
}) {
const { l10n } = useLocalization();
const { isGlobalLoading, selectedDevices } = useFirmwareTool();
const { selectedDevices, selectedDefault } = useFirmwareTool();
const {
control,
@@ -310,6 +340,9 @@ export function FlashingMethodStep({
} = useForm<FlashingMethodForm>({
reValidateMode: 'onChange',
mode: 'onChange',
defaultValues: {
flashingMethod: FirmwareUpdateMethod.OTAFirmwareUpdate.toString(),
},
resolver: yupResolver(
object({
flashingMethod: string().optional(),
@@ -343,6 +376,12 @@ export function FlashingMethodStep({
const flashingMethod = watch('flashingMethod');
console.log(
!isValid,
selectedDevices === null,
selectedDevices?.length === 0
);
return (
<>
<div className="flex flex-col w-full">
@@ -351,36 +390,37 @@ export function FlashingMethodStep({
{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>
<div className="my-4 flex flex-col gap-4 w-full">
<div className="flex gap-4 w-full flex-col md:flex-row">
<div className="flex flex-col gap-3 md:w-1/3">
<Localized
id="firmware_tool-flash_method_step-ota-v2"
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-v2"
attrs={{ label: true, description: true }}
>
<Radio
control={control}
name="flashingMethod"
value={FirmwareUpdateMethod.SerialFirmwareUpdate.toString()}
label=""
></Radio>
</Localized>
</div>
<div className="flex flex-grow">
{flashingMethod ===
FirmwareUpdateMethod.SerialFirmwareUpdate.toString() && (
<SerialDevicesList
isActive={isActive}
control={control}
watch={watch}
reset={reset}
@@ -389,37 +429,39 @@ export function FlashingMethodStep({
{flashingMethod ===
FirmwareUpdateMethod.OTAFirmwareUpdate.toString() && (
<OTADevicesList
isActive={isActive}
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></Typography>
</Localized>
</div>
)}
</div>
<div className="flex justify-between">
<Localized id="firmware_tool-previous_step">
<Button
variant="secondary"
onClick={() => {
if (selectedDefault?.flashingRules.shouldOnlyUseDefaults) {
goTo('SelectSource');
} else {
goTo('Defaults');
}
}}
></Button>
</Localized>
<Localized id="firmware_tool-next_step">
<Button
variant="primary"
disabled={
!isValid ||
selectedDevices === null ||
selectedDevices.length === 0
}
onClick={nextStep}
></Button>
</Localized>
</div>
</div>
</div>
</>

View File

@@ -17,11 +17,11 @@ import {
RpcMessage,
} from 'solarxr-protocol';
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';
import { DeviceCardControl } from '@/components/firmware-tool/DeviceCard';
export function FlashingStep({
goTo,
@@ -34,7 +34,7 @@ export function FlashingStep({
}) {
const nav = useNavigate();
const { l10n } = useLocalization();
const { selectedDevices, buildStatus, selectDevices, defaultConfig } =
const { selectedDevices, selectDevices, files, selectedDefault } =
useFirmwareTool();
const { state: onboardingState } = useOnboarding();
const { sendRPCPacket, useRPCPacket } = useWebsocketAPI();
@@ -57,16 +57,16 @@ export function FlashingStep({
const queueFlashing = (selectedDevices: SelectedDevice[]) => {
clear();
if (!buildStatus.firmwareFiles)
throw new Error('invalid state - no firmware files');
if (!files) throw new Error('invalid state - no firmware files');
if (!selectedDefault) throw new Error('invalid state - no slected default');
const requests = getFlashingRequests(
selectedDevices,
buildStatus.firmwareFiles.map(({ url, ...fields }) => ({
url: `${firmwareToolS3BaseUrl}/${url}`,
files.map(({ filePath, ...fields }) => ({
filePath: `${firmwareToolS3BaseUrl}/${filePath}`,
...fields,
})),
onboardingState,
defaultConfig
selectedDefault
);
requests.forEach((req) => {

View File

@@ -0,0 +1,313 @@
import { Localized } from '@fluent/react';
import { Typography } from '@/components/commons/Typography';
import { LoaderIcon, SlimeState } from '@/components/commons/icon/LoaderIcon';
import { useFirmwareTool } from '@/hooks/firmware-tool';
import classNames from 'classnames';
import { Button } from '@/components/commons/Button';
import {
fetchGetFirmwareBoardDefaults,
useGetFirmwareSources,
} from '@/firmware-tool-api/firmwareToolComponents';
import { useEffect, useMemo, useState } from 'react';
import { useSafeLocalization } from '@/i18n/config';
function Selector({
text,
active,
disabled,
tag,
onClick,
}: {
text: string;
active: boolean;
official?: boolean;
tag?: 'official' | 'dev';
disabled?: boolean;
onClick: () => void;
}) {
return (
<div
className={classNames(
'p-3 rounded-md hover:bg-background-50 w-full cursor-pointer relative',
{
'bg-background-50 text-background-10': active,
'bg-background-60': !active,
'bg-background-80 text-background-50': disabled,
}
)}
onClick={() => {
if (!disabled) onClick();
}}
>
{tag === 'official' && (
<div
className={classNames(
'absolute px-2 py-0.5 rounded-md bg-accent-background-20 -top-2 -right-2',
{ 'brightness-20': disabled, 'brightness-50': !active }
)}
>
<Localized id="firmware_tool-select_source-official">
<Typography></Typography>
</Localized>
</div>
)}
{tag === 'dev' && (
<div
className={classNames(
'absolute px-2 py-0.5 rounded-md bg-status-warning -top-2 -right-2',
{ 'brightness-20': disabled, 'brightness-50': !active }
)}
>
<Localized id="firmware_tool-select_source-dev">
<Typography color="text-background-90"></Typography>
</Localized>
</div>
)}
{text}
</div>
);
}
export function SelectSourceSetep({
nextStep,
goTo,
}: {
nextStep: () => void;
prevStep: () => void;
goTo: (id: string) => void;
}) {
const { l10n, getStringOrNull } = useSafeLocalization();
const { setSelectedSource, selectedSource, selectedDefault } =
useFirmwareTool();
const [partialBoard, setPartialBoard] = useState<{
source?: string;
version?: string;
board?: string;
}>();
const {
isFetching,
isError,
data: sources,
refetch,
} = useGetFirmwareSources({});
const { possibleBoards, possibleVersions, sourcesGroupped } = useMemo(() => {
return {
sourcesGroupped: sources
?.reduce(
(curr, source) => {
if (!curr.find(({ name }) => source.source === name))
curr.push({
name: source.source,
official: source.official,
disabled:
!partialBoard?.board ||
!source.availableBoards.includes(partialBoard.board),
});
return curr;
},
[] as { name: string; official: boolean; disabled: boolean }[]
)
.sort((a, b) => {
if (a.official !== b.official) return a.official ? -1 : 1;
return a.name.localeCompare(b.name);
}),
possibleBoards: sources
?.reduce((curr, source) => {
const unknownBoards = source.availableBoards.filter(
(b) => !curr.includes(b)
);
curr.push(...unknownBoards);
return curr;
}, [] as string[])
.sort((a, b) => {
// Sort official board type first
const aStartsWithBoard = a.startsWith('BOARD_SLIMEVR');
const bStartsWithBoard = b.startsWith('BOARD_SLIMEVR');
if (aStartsWithBoard && !bStartsWithBoard) return -1;
if (!aStartsWithBoard && bStartsWithBoard) return 1;
if (a === 'BOARD_SLIMEVR_DEV' && b !== 'BOARD_SLIMEVR_DEV') return 1;
if (a !== 'BOARD_SLIMEVR_DEV' && b === 'BOARD_SLIMEVR_DEV') return -1;
return a.localeCompare(b);
}),
possibleVersions: sources
?.reduce(
(curr, source) => {
if (!curr.find(({ name }) => source.version === name))
curr.push({
disabled:
!partialBoard?.board ||
!source.availableBoards.includes(partialBoard.board) ||
source.source !== partialBoard.source,
name: source.version,
isBranch: !!source.branch,
});
return curr;
},
[] as { name: string; disabled: boolean; isBranch: boolean }[]
)
.sort((a, b) => {
if (a.isBranch !== b.isBranch) return a.isBranch ? 1 : -1;
return a.name.localeCompare(b.name);
}),
};
}, [sources, partialBoard]);
useEffect(() => {
if (partialBoard?.source && partialBoard.board && partialBoard.version) {
const params = {
board: partialBoard.board,
source: partialBoard.source,
version: partialBoard.version,
};
fetchGetFirmwareBoardDefaults({
queryParams: params,
}).then((board) => {
setSelectedSource({
source: params,
default: board,
});
});
} else {
setSelectedSource(undefined);
}
}, [partialBoard]);
const formatSource = (name: string, official: boolean) => {
return !official ? name : name.substring(name.indexOf('/') + 1);
};
return (
<>
<div className="flex flex-col w-full">
<div className="flex flex-grow flex-col gap-4">
<Typography>
{l10n.getString('firmware_tool-select_source-description')}
</Typography>
</div>
<div className="my-4">
{!isFetching && !isError && (
<div className="flex flex-col gap-2">
<div className="grid md:grid-cols-3 gap-4">
<div className="flex flex-col gap-1 w-full">
<Localized id="firmware_tool-select_source-board_type">
<Typography variant="section-title"></Typography>
</Localized>
<div className="flex flex-col gap-4 md:max-h-[305px] overflow-y-auto bg-background-80 rounded-lg p-4">
{possibleBoards?.map((board) => (
<Selector
active={partialBoard?.board === board}
key={`${board}`}
onClick={() => {
setPartialBoard({ board });
}}
tag={
board.startsWith('BOARD_SLIMEVR')
? board === 'BOARD_SLIMEVR_DEV'
? 'dev'
: 'official'
: undefined
}
text={
getStringOrNull(
`board_type-${board.replace('BOARD_', '')}`
) ?? board.replace('BOARD_', '').replaceAll('_', ' ')
}
></Selector>
))}
</div>
</div>
<div className="flex flex-col gap-1 w-full">
<Localized id="firmware_tool-select_source-firmware">
<Typography variant="section-title"></Typography>
</Localized>
<div className="flex flex-col gap-4 md:max-h-[305px] overflow-y-auto bg-background-80 rounded-lg p-4">
{sourcesGroupped?.map(({ name, official, disabled }) => (
<Selector
active={partialBoard?.source === name}
disabled={disabled}
key={`${name}`}
tag={official ? 'official' : undefined}
onClick={() => {
setPartialBoard((curr) => ({
...curr,
source: name,
}));
}}
text={formatSource(name, official)}
></Selector>
))}
</div>
</div>
<div className="flex flex-col gap-1 w-full">
<Localized id="firmware_tool-select_source-version">
<Typography variant="section-title"></Typography>
</Localized>
<div className="flex flex-col gap-4 md:max-h-[305px] overflow-y-auto bg-background-80 rounded-lg p-4">
{possibleVersions?.map(({ name, disabled }) => (
<Selector
active={partialBoard?.version === name}
disabled={disabled}
key={`${name}`}
tag={
partialBoard?.source?.startsWith('SlimeVR/') &&
name === 'llelievr/board-defaults'
? 'dev'
: undefined
}
onClick={() => {
setPartialBoard((curr) => ({
...curr,
version: name,
}));
}}
text={name}
></Selector>
))}
</div>
</div>
</div>
<div className="flex justify-end">
<Localized id="firmware_tool-next_step">
<Button
variant="primary"
disabled={!selectedSource}
onClick={() => {
if (selectedDefault?.flashingRules.shouldOnlyUseDefaults)
goTo('Build');
else nextStep();
}}
></Button>
</Localized>
</div>
</div>
)}
{isFetching && (
<div className="flex justify-center flex-col items-center gap-3 h-44">
<LoaderIcon slimeState={SlimeState.JUMPY}></LoaderIcon>
<Localized id="firmware_tool-loading">
<Typography></Typography>
</Localized>
</div>
)}
{isError && (
<div className="flex justify-center flex-col items-center gap-3 h-44">
<LoaderIcon slimeState={SlimeState.SAD}></LoaderIcon>
<Localized id="firmware_tool-select_source-error">
<Typography></Typography>
</Localized>
<Localized id="firmware_tool-retry">
<Button variant="primary" onClick={() => refetch()}></Button>
</Localized>
</div>
)}
</div>
</div>
</>
);
}

View File

@@ -3,6 +3,7 @@ import { Typography } from '@/components/commons/Typography';
import { getTrackerName } from '@/hooks/tracker';
import { ComponentProps, useEffect, useMemo, useRef, useState } from 'react';
import {
BoardType,
DeviceDataT,
DeviceIdTableT,
FirmwareUpdateMethod,
@@ -39,6 +40,8 @@ interface FirmwareUpdateForm {
selectedDevices: { [key: string]: boolean };
}
type SelectedDeviceWithBoard = SelectedDevice & { board: BoardType };
interface UpdateStatus {
status: FirmwareUpdateStatus;
type: FirmwareUpdateMethod;
@@ -196,21 +199,41 @@ export function FirmwareUpdate() {
};
}, []);
const queueFlashing = (selectedDevices: SelectedDevice[]) => {
const queueFlashing = (selectedDevices: SelectedDeviceWithBoard[]) => {
clear();
pendingDevicesRef.current = selectedDevices;
const firmwareFile = currentFirmwareRelease?.firmwareFile;
if (!firmwareFile) throw new Error('invalid state - no firmware file');
const requests = getFlashingRequests(
selectedDevices,
[{ isFirmware: true, firmwareId: '', url: firmwareFile, offset: 0 }],
{ wifi: undefined, alonePage: false, progress: 0 }, // we do not use serial
null // we do not use serial
);
requests.forEach((req) => {
sendRPCPacket(RpcMessage.FirmwareUpdateRequest, req);
});
if (!currentFirmwareRelease)
throw new Error('invalid state - no fw release');
const groupedByBoard = selectedDevices.reduce((curr, device) => {
const boards = curr.get(device.board) ?? [];
boards.push(device);
curr.set(device.board, boards);
return curr;
}, new Map<BoardType, SelectedDeviceWithBoard[]>());
for (const [board, devices] of groupedByBoard) {
if (board === BoardType.UNKNOWN) continue;
const firmwareFile = currentFirmwareRelease.firmwareFiles[board];
if (!firmwareFile) continue;
const requests = getFlashingRequests(
devices,
[
{
isFirmware: true,
firmwareId: '',
filePath: firmwareFile,
offset: 0,
},
],
{ wifi: undefined, alonePage: false, progress: 0 }, // we do not use serial
null // we do not use serial
);
requests.forEach((req) => {
sendRPCPacket(RpcMessage.FirmwareUpdateRequest, req);
});
}
};
const trackerWithErrors = useMemo(
@@ -281,11 +304,12 @@ export function FirmwareUpdate() {
{
type: FirmwareUpdateMethod.OTAFirmwareUpdate,
deviceId: id,
board: device.hardwareInfo?.officialBoardType ?? BoardType.UNKNOWN,
deviceNames: deviceNames(device, l10n),
},
];
},
[] as SelectedDevice[]
[] as SelectedDeviceWithBoard[]
);
if (!selectedDevices)
throw new Error('invalid state - no selected devices');

View File

@@ -36,7 +36,6 @@ import { TrackerCard } from './TrackerCard';
import { Quaternion } from 'three';
import { useAppContext } from '@/hooks/app';
import { MagnetometerToggleSetting } from '@/components/settings/pages/MagnetometerToggleSetting';
import semver from 'semver';
import { useSetAtom } from 'jotai';
import { ignoredTrackersAtom } from '@/store/app-store';
import { checkForUpdate } from '@/hooks/firmware-update';
@@ -170,11 +169,7 @@ export function TrackerSettingsPage() {
currentFirmwareRelease &&
tracker?.device?.hardwareInfo &&
checkForUpdate(currentFirmwareRelease, tracker?.device);
const updateUnavailable =
tracker?.device?.hardwareInfo?.officialBoardType !== BoardType.SLIMEVR ||
!semver.valid(
tracker?.device?.hardwareInfo?.firmwareVersion?.toString() ?? 'none'
);
const updateUnavailable = needUpdate === null;
return (
<form
@@ -216,12 +211,19 @@ export function TrackerSettingsPage() {
</Typography>
<Typography>-</Typography>
{updateUnavailable && (
<Localized id="tracker-settings-update-unavailable">
<Typography>Cannot be updated (DIY)</Typography>
<Localized id="tracker-settings-update-unavailable-v2">
<Typography>No releases found</Typography>
</Localized>
)}
{!updateUnavailable && (
<>
{needUpdate === 'unavailable' && (
<Localized id="tracker-settings-update-incompatible">
<Typography>
Cannot be updated, Incompatible board
</Typography>
</Localized>
)}
{needUpdate === 'blocked' && (
// This happens only if no update is available and or the user is not in the current stagged
<Localized id="tracker-settings-update-blocked">

View File

@@ -27,7 +27,16 @@ export const fetchGetIsCompatibleVersion = (
signal?: AbortSignal
) =>
firmwareToolFetch<
Schemas.VerionCheckResponse,
| {
success: true;
}
| {
success: false;
reason: {
message: string;
versions: string;
};
},
GetIsCompatibleVersionError,
undefined,
{},
@@ -38,11 +47,32 @@ export const fetchGetIsCompatibleVersion = (
/**
* Is this api compatible with the server version given
*/
export const useGetIsCompatibleVersion = <TData = Schemas.VerionCheckResponse>(
export const useGetIsCompatibleVersion = <
TData =
| {
success: true;
}
| {
success: false;
reason: {
message: string;
versions: string;
};
},
>(
variables: GetIsCompatibleVersionVariables,
options?: Omit<
reactQuery.UseQueryOptions<
Schemas.VerionCheckResponse,
| {
success: true;
}
| {
success: false;
reason: {
message: string;
versions: string;
};
},
GetIsCompatibleVersionError,
TData
>,
@@ -51,7 +81,16 @@ export const useGetIsCompatibleVersion = <TData = Schemas.VerionCheckResponse>(
) => {
const { fetcherOptions, queryOptions, queryKeyFn } = useFirmwareToolContext(options);
return reactQuery.useQuery<
Schemas.VerionCheckResponse,
| {
success: true;
}
| {
success: false;
reason: {
message: string;
versions: string;
};
},
GetIsCompatibleVersionError,
TData
>({
@@ -67,504 +106,6 @@ export const useGetIsCompatibleVersion = <TData = Schemas.VerionCheckResponse>(
});
};
export type GetFirmwaresError = Fetcher.ErrorWrapper<undefined>;
export type GetFirmwaresResponse = Schemas.FirmwareDTO[];
export type GetFirmwaresVariables = FirmwareToolContext['fetcherOptions'];
/**
* List all the built firmwares
*/
export const fetchGetFirmwares = (
variables: GetFirmwaresVariables,
signal?: AbortSignal
) =>
firmwareToolFetch<GetFirmwaresResponse, GetFirmwaresError, undefined, {}, {}, {}>({
url: '/firmwares',
method: 'get',
...variables,
signal,
});
/**
* List all the built firmwares
*/
export const useGetFirmwares = <TData = GetFirmwaresResponse>(
variables: GetFirmwaresVariables,
options?: Omit<
reactQuery.UseQueryOptions<GetFirmwaresResponse, GetFirmwaresError, TData>,
'queryKey' | 'queryFn' | 'initialData'
>
) => {
const { fetcherOptions, queryOptions, queryKeyFn } = useFirmwareToolContext(options);
return reactQuery.useQuery<GetFirmwaresResponse, GetFirmwaresError, TData>({
queryKey: queryKeyFn({
path: '/firmwares',
operationId: 'getFirmwares',
variables,
}),
queryFn: ({ signal }) =>
fetchGetFirmwares({ ...fetcherOptions, ...variables }, signal),
...options,
...queryOptions,
});
};
export type PostFirmwaresBuildError = Fetcher.ErrorWrapper<{
status: 400;
payload: Schemas.VersionNotFoundExeption;
}>;
export type PostFirmwaresBuildVariables = {
body: Schemas.CreateBuildFirmwareDTO;
} & FirmwareToolContext['fetcherOptions'];
/**
* Build a firmware from the requested configuration
*/
export const fetchPostFirmwaresBuild = (
variables: PostFirmwaresBuildVariables,
signal?: AbortSignal
) =>
firmwareToolFetch<
Schemas.BuildResponseDTO,
PostFirmwaresBuildError,
Schemas.CreateBuildFirmwareDTO,
{},
{},
{}
>({ url: '/firmwares/build', method: 'post', ...variables, signal });
/**
* Build a firmware from the requested configuration
*/
export const usePostFirmwaresBuild = (
options?: Omit<
reactQuery.UseMutationOptions<
Schemas.BuildResponseDTO,
PostFirmwaresBuildError,
PostFirmwaresBuildVariables
>,
'mutationFn'
>
) => {
const { fetcherOptions } = useFirmwareToolContext();
return reactQuery.useMutation<
Schemas.BuildResponseDTO,
PostFirmwaresBuildError,
PostFirmwaresBuildVariables
>({
mutationFn: (variables: PostFirmwaresBuildVariables) =>
fetchPostFirmwaresBuild({ ...fetcherOptions, ...variables }),
...options,
});
};
export type GetFirmwaresBuildStatusIdPathParams = {
id: string;
};
export type GetFirmwaresBuildStatusIdError = Fetcher.ErrorWrapper<undefined>;
export type GetFirmwaresBuildStatusIdVariables = {
pathParams: GetFirmwaresBuildStatusIdPathParams;
} & FirmwareToolContext['fetcherOptions'];
/**
* Get the build status of a firmware
* This is a SSE (Server Sent Event)
* you can use the web browser api to check for the build status and update the ui in real time
*/
export const fetchGetFirmwaresBuildStatusId = (
variables: GetFirmwaresBuildStatusIdVariables,
signal?: AbortSignal
) =>
firmwareToolFetch<
Schemas.ObservableType,
GetFirmwaresBuildStatusIdError,
undefined,
{},
{},
GetFirmwaresBuildStatusIdPathParams
>({
url: '/firmwares/build-status/{id}',
method: 'get',
...variables,
signal,
});
/**
* Get the build status of a firmware
* This is a SSE (Server Sent Event)
* you can use the web browser api to check for the build status and update the ui in real time
*/
export const useGetFirmwaresBuildStatusId = <TData = Schemas.ObservableType>(
variables: GetFirmwaresBuildStatusIdVariables,
options?: Omit<
reactQuery.UseQueryOptions<
Schemas.ObservableType,
GetFirmwaresBuildStatusIdError,
TData
>,
'queryKey' | 'queryFn' | 'initialData'
>
) => {
const { fetcherOptions, queryOptions, queryKeyFn } = useFirmwareToolContext(options);
return reactQuery.useQuery<
Schemas.ObservableType,
GetFirmwaresBuildStatusIdError,
TData
>({
queryKey: queryKeyFn({
path: '/firmwares/build-status/{id}',
operationId: 'getFirmwaresBuildStatusId',
variables,
}),
queryFn: ({ signal }) =>
fetchGetFirmwaresBuildStatusId({ ...fetcherOptions, ...variables }, signal),
...options,
...queryOptions,
});
};
export type GetFirmwaresBoardsError = Fetcher.ErrorWrapper<undefined>;
export type GetFirmwaresBoardsResponse = string[];
export type GetFirmwaresBoardsVariables = FirmwareToolContext['fetcherOptions'];
/**
* List all the possible board types
*/
export const fetchGetFirmwaresBoards = (
variables: GetFirmwaresBoardsVariables,
signal?: AbortSignal
) =>
firmwareToolFetch<
GetFirmwaresBoardsResponse,
GetFirmwaresBoardsError,
undefined,
{},
{},
{}
>({ url: '/firmwares/boards', method: 'get', ...variables, signal });
/**
* List all the possible board types
*/
export const useGetFirmwaresBoards = <TData = GetFirmwaresBoardsResponse>(
variables: GetFirmwaresBoardsVariables,
options?: Omit<
reactQuery.UseQueryOptions<
GetFirmwaresBoardsResponse,
GetFirmwaresBoardsError,
TData
>,
'queryKey' | 'queryFn' | 'initialData'
>
) => {
const { fetcherOptions, queryOptions, queryKeyFn } = useFirmwareToolContext(options);
return reactQuery.useQuery<
GetFirmwaresBoardsResponse,
GetFirmwaresBoardsError,
TData
>({
queryKey: queryKeyFn({
path: '/firmwares/boards',
operationId: 'getFirmwaresBoards',
variables,
}),
queryFn: ({ signal }) =>
fetchGetFirmwaresBoards({ ...fetcherOptions, ...variables }, signal),
...options,
...queryOptions,
});
};
export type GetFirmwaresVersionsError = Fetcher.ErrorWrapper<undefined>;
export type GetFirmwaresVersionsResponse = Schemas.ReleaseDTO[];
export type GetFirmwaresVersionsVariables = FirmwareToolContext['fetcherOptions'];
/**
* List all the possible versions to build a firmware from
*/
export const fetchGetFirmwaresVersions = (
variables: GetFirmwaresVersionsVariables,
signal?: AbortSignal
) =>
firmwareToolFetch<
GetFirmwaresVersionsResponse,
GetFirmwaresVersionsError,
undefined,
{},
{},
{}
>({ url: '/firmwares/versions', method: 'get', ...variables, signal });
/**
* List all the possible versions to build a firmware from
*/
export const useGetFirmwaresVersions = <TData = GetFirmwaresVersionsResponse>(
variables: GetFirmwaresVersionsVariables,
options?: Omit<
reactQuery.UseQueryOptions<
GetFirmwaresVersionsResponse,
GetFirmwaresVersionsError,
TData
>,
'queryKey' | 'queryFn' | 'initialData'
>
) => {
const { fetcherOptions, queryOptions, queryKeyFn } = useFirmwareToolContext(options);
return reactQuery.useQuery<
GetFirmwaresVersionsResponse,
GetFirmwaresVersionsError,
TData
>({
queryKey: queryKeyFn({
path: '/firmwares/versions',
operationId: 'getFirmwaresVersions',
variables,
}),
queryFn: ({ signal }) =>
fetchGetFirmwaresVersions({ ...fetcherOptions, ...variables }, signal),
...options,
...queryOptions,
});
};
export type GetFirmwaresImusError = Fetcher.ErrorWrapper<undefined>;
export type GetFirmwaresImusResponse = Schemas.Imudto[];
export type GetFirmwaresImusVariables = FirmwareToolContext['fetcherOptions'];
/**
* List all the possible imus to use
*/
export const fetchGetFirmwaresImus = (
variables: GetFirmwaresImusVariables,
signal?: AbortSignal
) =>
firmwareToolFetch<
GetFirmwaresImusResponse,
GetFirmwaresImusError,
undefined,
{},
{},
{}
>({ url: '/firmwares/imus', method: 'get', ...variables, signal });
/**
* List all the possible imus to use
*/
export const useGetFirmwaresImus = <TData = GetFirmwaresImusResponse>(
variables: GetFirmwaresImusVariables,
options?: Omit<
reactQuery.UseQueryOptions<GetFirmwaresImusResponse, GetFirmwaresImusError, TData>,
'queryKey' | 'queryFn' | 'initialData'
>
) => {
const { fetcherOptions, queryOptions, queryKeyFn } = useFirmwareToolContext(options);
return reactQuery.useQuery<GetFirmwaresImusResponse, GetFirmwaresImusError, TData>({
queryKey: queryKeyFn({
path: '/firmwares/imus',
operationId: 'getFirmwaresImus',
variables,
}),
queryFn: ({ signal }) =>
fetchGetFirmwaresImus({ ...fetcherOptions, ...variables }, signal),
...options,
...queryOptions,
});
};
export type GetFirmwaresBatteriesError = Fetcher.ErrorWrapper<undefined>;
export type GetFirmwaresBatteriesResponse = string[];
export type GetFirmwaresBatteriesVariables = FirmwareToolContext['fetcherOptions'];
/**
* List all the battery types
*/
export const fetchGetFirmwaresBatteries = (
variables: GetFirmwaresBatteriesVariables,
signal?: AbortSignal
) =>
firmwareToolFetch<
GetFirmwaresBatteriesResponse,
GetFirmwaresBatteriesError,
undefined,
{},
{},
{}
>({ url: '/firmwares/batteries', method: 'get', ...variables, signal });
/**
* List all the battery types
*/
export const useGetFirmwaresBatteries = <TData = GetFirmwaresBatteriesResponse>(
variables: GetFirmwaresBatteriesVariables,
options?: Omit<
reactQuery.UseQueryOptions<
GetFirmwaresBatteriesResponse,
GetFirmwaresBatteriesError,
TData
>,
'queryKey' | 'queryFn' | 'initialData'
>
) => {
const { fetcherOptions, queryOptions, queryKeyFn } = useFirmwareToolContext(options);
return reactQuery.useQuery<
GetFirmwaresBatteriesResponse,
GetFirmwaresBatteriesError,
TData
>({
queryKey: queryKeyFn({
path: '/firmwares/batteries',
operationId: 'getFirmwaresBatteries',
variables,
}),
queryFn: ({ signal }) =>
fetchGetFirmwaresBatteries({ ...fetcherOptions, ...variables }, signal),
...options,
...queryOptions,
});
};
export type GetFirmwaresDefaultConfigBoardPathParams = {
board:
| 'BOARD_SLIMEVR'
| 'BOARD_NODEMCU'
| 'BOARD_WROOM32'
| 'BOARD_WEMOSD1MINI'
| 'BOARD_TTGO_TBASE'
| 'BOARD_ESP01'
| 'BOARD_LOLIN_C3_MINI'
| 'BOARD_BEETLE32C3'
| 'BOARD_ES32C3DEVKITM1';
};
export type GetFirmwaresDefaultConfigBoardError = Fetcher.ErrorWrapper<undefined>;
export type GetFirmwaresDefaultConfigBoardVariables = {
pathParams: GetFirmwaresDefaultConfigBoardPathParams;
} & FirmwareToolContext['fetcherOptions'];
/**
* Gives the default pins / configuration of a given board
*/
export const fetchGetFirmwaresDefaultConfigBoard = (
variables: GetFirmwaresDefaultConfigBoardVariables,
signal?: AbortSignal
) =>
firmwareToolFetch<
Schemas.DefaultBuildConfigDTO,
GetFirmwaresDefaultConfigBoardError,
undefined,
{},
{},
GetFirmwaresDefaultConfigBoardPathParams
>({
url: '/firmwares/default-config/{board}',
method: 'get',
...variables,
signal,
});
/**
* Gives the default pins / configuration of a given board
*/
export const useGetFirmwaresDefaultConfigBoard = <
TData = Schemas.DefaultBuildConfigDTO,
>(
variables: GetFirmwaresDefaultConfigBoardVariables,
options?: Omit<
reactQuery.UseQueryOptions<
Schemas.DefaultBuildConfigDTO,
GetFirmwaresDefaultConfigBoardError,
TData
>,
'queryKey' | 'queryFn' | 'initialData'
>
) => {
const { fetcherOptions, queryOptions, queryKeyFn } = useFirmwareToolContext(options);
return reactQuery.useQuery<
Schemas.DefaultBuildConfigDTO,
GetFirmwaresDefaultConfigBoardError,
TData
>({
queryKey: queryKeyFn({
path: '/firmwares/default-config/{board}',
operationId: 'getFirmwaresDefaultConfigBoard',
variables,
}),
queryFn: ({ signal }) =>
fetchGetFirmwaresDefaultConfigBoard({ ...fetcherOptions, ...variables }, signal),
...options,
...queryOptions,
});
};
export type GetFirmwaresIdPathParams = {
id: string;
};
export type GetFirmwaresIdError = Fetcher.ErrorWrapper<{
status: 404;
payload: Schemas.HttpException;
}>;
export type GetFirmwaresIdVariables = {
pathParams: GetFirmwaresIdPathParams;
} & FirmwareToolContext['fetcherOptions'];
/**
* Get the inforamtions about a firmware from its id
* also provide more informations than the simple list, like pins and imus and files
*/
export const fetchGetFirmwaresId = (
variables: GetFirmwaresIdVariables,
signal?: AbortSignal
) =>
firmwareToolFetch<
Schemas.FirmwareDetailDTO,
GetFirmwaresIdError,
undefined,
{},
{},
GetFirmwaresIdPathParams
>({ url: '/firmwares/{id}', method: 'get', ...variables, signal });
/**
* Get the inforamtions about a firmware from its id
* also provide more informations than the simple list, like pins and imus and files
*/
export const useGetFirmwaresId = <TData = Schemas.FirmwareDetailDTO>(
variables: GetFirmwaresIdVariables,
options?: Omit<
reactQuery.UseQueryOptions<Schemas.FirmwareDetailDTO, GetFirmwaresIdError, TData>,
'queryKey' | 'queryFn' | 'initialData'
>
) => {
const { fetcherOptions, queryOptions, queryKeyFn } = useFirmwareToolContext(options);
return reactQuery.useQuery<Schemas.FirmwareDetailDTO, GetFirmwaresIdError, TData>({
queryKey: queryKeyFn({
path: '/firmwares/{id}',
operationId: 'getFirmwaresId',
variables,
}),
queryFn: ({ signal }) =>
fetchGetFirmwaresId({ ...fetcherOptions, ...variables }, signal),
...options,
...queryOptions,
});
};
export type GetHealthError = Fetcher.ErrorWrapper<undefined>;
export type GetHealthVariables = FirmwareToolContext['fetcherOptions'];
@@ -606,54 +147,238 @@ export const useGetHealth = <TData = boolean>(
});
};
export type GetFirmwareSourcesError = Fetcher.ErrorWrapper<undefined>;
export type GetFirmwareSourcesResponse = Schemas.FirmwareSource[];
export type GetFirmwareSourcesVariables = FirmwareToolContext['fetcherOptions'];
/**
* List all the sources you can build a firmware from
*/
export const fetchGetFirmwareSources = (
variables: GetFirmwareSourcesVariables,
signal?: AbortSignal
) =>
firmwareToolFetch<
GetFirmwareSourcesResponse,
GetFirmwareSourcesError,
undefined,
{},
{},
{}
>({ url: '/firmware/sources', method: 'get', ...variables, signal });
/**
* List all the sources you can build a firmware from
*/
export const useGetFirmwareSources = <TData = GetFirmwareSourcesResponse>(
variables: GetFirmwareSourcesVariables,
options?: Omit<
reactQuery.UseQueryOptions<
GetFirmwareSourcesResponse,
GetFirmwareSourcesError,
TData
>,
'queryKey' | 'queryFn' | 'initialData'
>
) => {
const { fetcherOptions, queryOptions, queryKeyFn } = useFirmwareToolContext(options);
return reactQuery.useQuery<
GetFirmwareSourcesResponse,
GetFirmwareSourcesError,
TData
>({
queryKey: queryKeyFn({
path: '/firmware/sources',
operationId: 'getFirmwareSources',
variables,
}),
queryFn: ({ signal }) =>
fetchGetFirmwareSources({ ...fetcherOptions, ...variables }, signal),
...options,
...queryOptions,
});
};
export type GetFirmwareBoardDefaultsQueryParams = {
source: string;
board: string;
version: string;
};
export type GetFirmwareBoardDefaultsError = Fetcher.ErrorWrapper<undefined>;
export type GetFirmwareBoardDefaultsVariables = {
queryParams: GetFirmwareBoardDefaultsQueryParams;
} & FirmwareToolContext['fetcherOptions'];
/**
* Fet the defaults of a specific board on a specific firmware
*/
export const fetchGetFirmwareBoardDefaults = (
variables: GetFirmwareBoardDefaultsVariables,
signal?: AbortSignal
) =>
firmwareToolFetch<
Schemas.FirmwareBoardDefaultsNullable,
GetFirmwareBoardDefaultsError,
undefined,
{},
GetFirmwareBoardDefaultsQueryParams,
{}
>({ url: '/firmware/board-defaults', method: 'get', ...variables, signal });
/**
* Fet the defaults of a specific board on a specific firmware
*/
export const useGetFirmwareBoardDefaults = <
TData = Schemas.FirmwareBoardDefaultsNullable,
>(
variables: GetFirmwareBoardDefaultsVariables,
options?: Omit<
reactQuery.UseQueryOptions<
Schemas.FirmwareBoardDefaultsNullable,
GetFirmwareBoardDefaultsError,
TData
>,
'queryKey' | 'queryFn' | 'initialData'
>
) => {
const { fetcherOptions, queryOptions, queryKeyFn } = useFirmwareToolContext(options);
return reactQuery.useQuery<
Schemas.FirmwareBoardDefaultsNullable,
GetFirmwareBoardDefaultsError,
TData
>({
queryKey: queryKeyFn({
path: '/firmware/board-defaults',
operationId: 'getFirmwareBoardDefaults',
variables,
}),
queryFn: ({ signal }) =>
fetchGetFirmwareBoardDefaults({ ...fetcherOptions, ...variables }, signal),
...options,
...queryOptions,
});
};
export type PostFirmwareBuildError = Fetcher.ErrorWrapper<undefined>;
export type PostFirmwareBuildVariables = {
body: Schemas.BuildFirmwareBody;
} & FirmwareToolContext['fetcherOptions'];
export const fetchPostFirmwareBuild = (
variables: PostFirmwareBuildVariables,
signal?: AbortSignal
) =>
firmwareToolFetch<
Schemas.BuildStatusBasic | Schemas.BuildStatusDone,
PostFirmwareBuildError,
Schemas.BuildFirmwareBody,
{},
{},
{}
>({ url: '/firmware/build', method: 'post', ...variables, signal });
export const usePostFirmwareBuild = (
options?: Omit<
reactQuery.UseMutationOptions<
Schemas.BuildStatusBasic | Schemas.BuildStatusDone,
PostFirmwareBuildError,
PostFirmwareBuildVariables
>,
'mutationFn'
>
) => {
const { fetcherOptions } = useFirmwareToolContext();
return reactQuery.useMutation<
Schemas.BuildStatusBasic | Schemas.BuildStatusDone,
PostFirmwareBuildError,
PostFirmwareBuildVariables
>({
mutationFn: (variables: PostFirmwareBuildVariables) =>
fetchPostFirmwareBuild({ ...fetcherOptions, ...variables }),
...options,
});
};
export type GetFirmwareIdPathParams = {
id: string;
};
export type GetFirmwareIdError = Fetcher.ErrorWrapper<undefined>;
export type GetFirmwareIdVariables = {
pathParams: GetFirmwareIdPathParams;
} & FirmwareToolContext['fetcherOptions'];
/**
* Get the inforamtions about a firmware from its id
* also provide more informations than the simple list, like pins and imus and files
*/
export const fetchGetFirmwareId = (
variables: GetFirmwareIdVariables,
signal?: AbortSignal
) =>
firmwareToolFetch<
Schemas.FirmwareWithFiles,
GetFirmwareIdError,
undefined,
{},
{},
GetFirmwareIdPathParams
>({ url: '/firmware/{id}', method: 'get', ...variables, signal });
/**
* Get the inforamtions about a firmware from its id
* also provide more informations than the simple list, like pins and imus and files
*/
export const useGetFirmwareId = <TData = Schemas.FirmwareWithFiles>(
variables: GetFirmwareIdVariables,
options?: Omit<
reactQuery.UseQueryOptions<Schemas.FirmwareWithFiles, GetFirmwareIdError, TData>,
'queryKey' | 'queryFn' | 'initialData'
>
) => {
const { fetcherOptions, queryOptions, queryKeyFn } = useFirmwareToolContext(options);
return reactQuery.useQuery<Schemas.FirmwareWithFiles, GetFirmwareIdError, TData>({
queryKey: queryKeyFn({
path: '/firmware/{id}',
operationId: 'getFirmwareId',
variables,
}),
queryFn: ({ signal }) =>
fetchGetFirmwareId({ ...fetcherOptions, ...variables }, signal),
...options,
...queryOptions,
});
};
export type QueryOperation =
| {
path: '/is-compatible/{version}';
operationId: 'getIsCompatibleVersion';
variables: GetIsCompatibleVersionVariables;
}
| {
path: '/firmwares';
operationId: 'getFirmwares';
variables: GetFirmwaresVariables;
}
| {
path: '/firmwares/build-status/{id}';
operationId: 'getFirmwaresBuildStatusId';
variables: GetFirmwaresBuildStatusIdVariables;
}
| {
path: '/firmwares/boards';
operationId: 'getFirmwaresBoards';
variables: GetFirmwaresBoardsVariables;
}
| {
path: '/firmwares/versions';
operationId: 'getFirmwaresVersions';
variables: GetFirmwaresVersionsVariables;
}
| {
path: '/firmwares/imus';
operationId: 'getFirmwaresImus';
variables: GetFirmwaresImusVariables;
}
| {
path: '/firmwares/batteries';
operationId: 'getFirmwaresBatteries';
variables: GetFirmwaresBatteriesVariables;
}
| {
path: '/firmwares/default-config/{board}';
operationId: 'getFirmwaresDefaultConfigBoard';
variables: GetFirmwaresDefaultConfigBoardVariables;
}
| {
path: '/firmwares/{id}';
operationId: 'getFirmwaresId';
variables: GetFirmwaresIdVariables;
}
| {
path: '/health';
operationId: 'getHealth';
variables: GetHealthVariables;
}
| {
path: '/firmware/sources';
operationId: 'getFirmwareSources';
variables: GetFirmwareSourcesVariables;
}
| {
path: '/firmware/board-defaults';
operationId: 'getFirmwareBoardDefaults';
variables: GetFirmwareBoardDefaultsVariables;
}
| {
path: '/firmware/{id}';
operationId: 'getFirmwareId';
variables: GetFirmwareIdVariables;
};

View File

@@ -3,606 +3,103 @@
*
* @version 0.0.1
*/
export type VerionCheckResponse = {
success: boolean;
reason?: {
message: string;
versions: string;
export type FirmwareSource = {
version: string;
source: string;
branch?: string;
official: boolean;
prerelease: boolean;
availableBoards: string[];
};
export type FirmwareBoardDefaults = {
schema: void;
data: DefaultsFile;
};
export type DefaultsFile = {
toolchain: 'platformio';
defaults: {
[key: string]: BoardDefaults;
};
};
/**
* Root object declaring a built firmware
* this object contains:
* - the status of the build
* - the the repository and commit used as source
*/
export type FirmwareDTO = {
/**
* UUID of the firmware
*
* @format uuid
*/
id: string;
/**
* Id of the firmware version used.
* Usually the commit id of the source
* used to build the firmware
*/
releaseId: string;
/**
* Current status of the build
* this value will change during the build
* process
*
* BUILDING -> DONE \\ the firmwrare is build and ready
* -> FAILED \\ the build failled and will be garbage collected
*/
buildStatus:
| 'CREATING_BUILD_FOLDER'
| 'DOWNLOADING_FIRMWARE'
| 'EXTRACTING_FIRMWARE'
| 'SETTING_UP_DEFINES'
| 'BUILDING'
| 'SAVING'
| 'DONE'
| 'ERROR';
/**
* The repository and branch used as source of the firmware
*/
buildVersion: string;
/**
* The date of creation of this firmware build
*
* @format date-time
*/
createdAt: string;
export type BoardDefaults = {
values: void;
editable: string[];
flashingRules: {
applicationOffset: number;
needBootPress: boolean;
needManualReboot: boolean;
shouldOnlyUseDefaults: boolean;
};
};
export type BuildResponseDTO = {
/**
* Id of the firmware
*
* @format uuid
*/
id: string;
/**
* Build status of the firmware
*/
status:
| 'CREATING_BUILD_FOLDER'
| 'DOWNLOADING_FIRMWARE'
| 'EXTRACTING_FIRMWARE'
| 'SETTING_UP_DEFINES'
| 'BUILDING'
| 'SAVING'
| 'DONE'
| 'ERROR';
/**
* List of built firmware files, only set if the build succeeded
*/
firmwareFiles?: FirmwareFileDTO[];
};
export type FirmwareFileDTO = {
/**
* Url to the file
*/
url: string;
/**
* Address of the partition
*/
offset: number;
/**
* Is this file the main firmware
*/
isFirmware: boolean;
/**
* Id of the linked firmware
*
* @format uuid
*/
firmwareId: string;
};
export type CreateBuildFirmwareDTO = {
/**
* Repository of the firmware used
*/
export type BoardDefaultsQuery = {
source: string;
board: string;
version: string;
/**
* Board config, used to declare the pins used by the board
*/
boardConfig: CreateBoardConfigDTO;
/**
* Imu config, list of all the imus used and their pins
*
* @minItems 1
*/
imusConfig: CreateImuConfigDTO[];
};
export type CreateBoardConfigDTO = {
/**
* Type of the board
*/
type:
| 'BOARD_SLIMEVR'
| 'BOARD_NODEMCU'
| 'BOARD_WROOM32'
| 'BOARD_WEMOSD1MINI'
| 'BOARD_TTGO_TBASE'
| 'BOARD_ESP01'
| 'BOARD_LOLIN_C3_MINI'
| 'BOARD_BEETLE32C3'
| 'BOARD_ES32C3DEVKITM1';
/**
* Pin address of the indicator LED
*/
ledPin: string;
/**
* Is the indicator LED enabled
*/
enableLed: boolean;
/**
* Is the led inverted
*/
ledInverted: boolean;
/**
* Pin address of the battery indicator
*/
batteryPin: string;
/**
* Type of battery
*/
batteryType: 'BAT_EXTERNAL' | 'BAT_INTERNAL' | 'BAT_MCP3021' | 'BAT_INTERNAL_MCP3021';
/**
* Array of the different battery resistors, [indicator, SHIELD_R1, SHIELD_R2]
*
* @minItems 3
* @maxItems 3
*/
batteryResistances: number[];
};
export type CreateImuConfigDTO = {
/**
* Type of the imu
*/
type:
| 'IMU_BNO085'
| 'IMU_MPU9250'
| 'IMU_MPU6500'
| 'IMU_BNO080'
| 'IMU_BNO055'
| 'IMU_BNO086'
| 'IMU_MPU6050'
| 'IMU_BMI160'
| 'IMU_ICM20948'
| 'IMU_BMI270';
/**
* Pin address of the imu int pin
* not all imus use it
*/
intPin: string | null;
/**
* Rotation of the imu in degrees
*/
rotation: number;
/**
* Pin address of the scl pin
*/
sclPin: string;
/**
* Pin address of the sda pin
*/
sdaPin: string;
/**
* Is this imu optionnal
* Allows for extensions to be unplugged
*/
optional: boolean;
};
export type VersionNotFoundExeption = {
cause: void;
name: string;
message: string;
stack?: string;
};
/**
* A representation of any set of values over any amount of time. This is the most basic building block
* of RxJS.
*/
export type ObservableType = {
/**
* @deprecated true
*/
source?: Observableany;
/**
* @deprecated true
*/
operator?: OperatoranyType;
};
/**
* A representation of any set of values over any amount of time. This is the most basic building block
* of RxJS.
*/
export type Observableany = {
/**
* @deprecated true
*/
source?: Observableany;
/**
* @deprecated true
*/
operator?: Operatoranyany;
};
/**
* *
*/
export type Operatoranyany = {};
/**
* *
*/
export type OperatoranyType = {};
export type ReleaseDTO = {
/**
* id of the release, usually the commit id
*/
export type BuildStatusBasic = {
id: string;
/**
* url of the release
*/
url: string;
/**
* name of the release
*/
name: string;
/**
* url of the source archive
*/
zipball_url: string;
/**
* Is this release a pre release
*/
prerelease: boolean;
/**
* Is this release a draft
*/
draft: boolean;
};
export type Imudto = {
/**
* Type of the imu
*/
type:
| 'IMU_BNO085'
| 'IMU_MPU9250'
| 'IMU_MPU6500'
| 'IMU_BNO080'
| 'IMU_BNO055'
| 'IMU_BNO086'
| 'IMU_MPU6050'
| 'IMU_BMI160'
| 'IMU_ICM20948'
| 'IMU_BMI270';
/**
* Does that imu type require a int pin
*/
hasIntPin: boolean;
/**
* First address of the imu
*/
imuStartAddress: number;
/**
* Increment of the address for each new imus
*/
addressIncrement: number;
};
export type DefaultBuildConfigDTO = {
/**
* Default config of the selected board
* contains all the default pins information about the selected board
*/
boardConfig: CreateBoardConfigDTO;
/**
* Inform the flashing utility that the user need to press the boot (or Flash) button
* on the tracker
*/
needBootPress?: boolean;
/**
* Inform the flashing utility that the board will need a reboot after
* being flashed
*/
needManualReboot?: boolean;
/**
* Will use the default values and skip the customisation options
*/
shouldOnlyUseDefaults?: boolean;
/**
* List of the possible imus pins, usually only two items will be sent
*
* @minItems 1
*/
imuDefaults: IMUDefaultDTO[];
/**
* Gives the offset of the firmare file in the eeprom. Used for flashing
*/
application_offset: number;
};
export type IMUDefaultDTO = {
/**
* Type of the imu
*/
type?:
| 'IMU_BNO085'
| 'IMU_MPU9250'
| 'IMU_MPU6500'
| 'IMU_BNO080'
| 'IMU_BNO055'
| 'IMU_BNO086'
| 'IMU_MPU6050'
| 'IMU_BMI160'
| 'IMU_ICM20948'
| 'IMU_BMI270';
/**
* Pin address of the imu int pin
* not all imus use it
*/
intPin: string | null;
/**
* Rotation of the imu in degrees
*/
rotation?: number;
/**
* Pin address of the scl pin
*/
sclPin: string;
/**
* Pin address of the sda pin
*/
sdaPin: string;
/**
* Is this imu optionnal
* Allows for extensions to be unplugged
*/
optional: boolean;
};
export type BoardConfigDTONullable = {
/**
* Unique id of the board config, used for relations
*
* @format uuid
*/
id: string;
/**
* Type of the board
*/
type:
| 'BOARD_SLIMEVR'
| 'BOARD_NODEMCU'
| 'BOARD_WROOM32'
| 'BOARD_WEMOSD1MINI'
| 'BOARD_TTGO_TBASE'
| 'BOARD_ESP01'
| 'BOARD_LOLIN_C3_MINI'
| 'BOARD_BEETLE32C3'
| 'BOARD_ES32C3DEVKITM1';
/**
* Pin address of the indicator LED
*/
ledPin: string;
/**
* Is the indicator LED enabled
*/
enableLed: boolean;
/**
* Is the led inverted
*/
ledInverted: boolean;
/**
* Pin address of the battery indicator
*/
batteryPin: string;
/**
* Type of battery
*/
batteryType: 'BAT_EXTERNAL' | 'BAT_INTERNAL' | 'BAT_MCP3021' | 'BAT_INTERNAL_MCP3021';
/**
* Array of the different battery resistors, [indicator, SHIELD_R1, SHIELD_R2]
*
* @minItems 3
* @maxItems 3
*/
batteryResistances: number[];
/**
* Id of the linked firmware, used for relations
*
* @format uuid
*/
firmwareId: string;
};
export type FirmwareDetailDTO = {
/**
* Pins informations about the board
*/
boardConfig: BoardConfigDTONullable;
/**
* List of the declared imus, and their pin configuration
*
* @minItems 1
*/
imusConfig: ImuConfigDTO[];
/**
* List of the built files / partitions with their url and offsets
*/
firmwareFiles: FirmwareFileDTO[];
/**
* UUID of the firmware
*
* @format uuid
*/
id: string;
/**
* Id of the firmware version used.
* Usually the commit id of the source
* used to build the firmware
*/
releaseId: string;
/**
* Current status of the build
* this value will change during the build
* process
*
* BUILDING -> DONE \\ the firmwrare is build and ready
* -> FAILED \\ the build failled and will be garbage collected
*/
buildStatus:
status:
| 'QUEUED'
| 'CREATING_BUILD_FOLDER'
| 'DOWNLOADING_FIRMWARE'
| 'EXTRACTING_FIRMWARE'
| 'SETTING_UP_DEFINES'
| 'DOWNLOADING_SOURCE'
| 'EXTRACTING_SOURCE'
| 'BUILDING'
| 'SAVING'
| 'ERROR';
};
export type BuildStatusDone = {
id: string;
status: 'DONE';
files: {
filePath: string;
offset: number;
isFirmware: boolean;
firmwareId: string;
}[];
};
export type BuildFirmwareBody = {
source: string;
board: string;
version: string;
values: BoardDefaults;
};
export type FirmwareWithFiles = {
id: string;
release_id: string;
status:
| 'QUEUED'
| 'CREATING_BUILD_FOLDER'
| 'DOWNLOADING_SOURCE'
| 'EXTRACTING_SOURCE'
| 'BUILDING'
| 'SAVING'
| 'DONE'
| 'ERROR';
/**
* The repository and branch used as source of the firmware
*/
buildVersion: string;
/**
* The date of creation of this firmware build
*
* @format date-time
*/
createdAt: string;
/**
* @format date-time
*/
updatedAt: string;
files: {
filePath: string;
offset: number;
isFirmware: boolean;
firmwareId: string;
}[];
};
export type BoardConfigDTO = {
/**
* Unique id of the board config, used for relations
*
* @format uuid
*/
id: string;
/**
* Type of the board
*/
type:
| 'BOARD_SLIMEVR'
| 'BOARD_NODEMCU'
| 'BOARD_WROOM32'
| 'BOARD_WEMOSD1MINI'
| 'BOARD_TTGO_TBASE'
| 'BOARD_ESP01'
| 'BOARD_LOLIN_C3_MINI'
| 'BOARD_BEETLE32C3'
| 'BOARD_ES32C3DEVKITM1';
/**
* Pin address of the indicator LED
*/
ledPin: string;
/**
* Is the indicator LED enabled
*/
enableLed: boolean;
/**
* Is the led inverted
*/
ledInverted: boolean;
/**
* Pin address of the battery indicator
*/
batteryPin: string;
/**
* Type of battery
*/
batteryType: 'BAT_EXTERNAL' | 'BAT_INTERNAL' | 'BAT_MCP3021' | 'BAT_INTERNAL_MCP3021';
/**
* Array of the different battery resistors, [indicator, SHIELD_R1, SHIELD_R2]
*
* @minItems 3
* @maxItems 3
*/
batteryResistances: number[];
/**
* Id of the linked firmware, used for relations
*
* @format uuid
*/
firmwareId: string;
};
export type ImuConfigDTO = {
/**
* Unique id of the config
* this probably will never be shown to the user as it is moslty use for relations
*
* @format uuid
*/
id: string;
/**
* Type of the imu
*/
type:
| 'IMU_BNO085'
| 'IMU_MPU9250'
| 'IMU_MPU6500'
| 'IMU_BNO080'
| 'IMU_BNO055'
| 'IMU_BNO086'
| 'IMU_MPU6050'
| 'IMU_BMI160'
| 'IMU_ICM20948'
| 'IMU_BMI270';
/**
* Rotation of the imu in degrees
*/
rotation: number;
/**
* Pin address of the imu int pin
* not all imus use it
*/
intPin: string | null;
/**
* Pin address of the scl pin
*/
sclPin: string;
/**
* Pin address of the sda pin
*/
sdaPin: string;
/**
* Is this imu optionnal
* Allows for extensions to be unplugged
*/
optional: boolean;
/**
* id of the linked firmware, used for relations
*
* @format uuid
*/
firmwareId: string;
};
/**
* Defines the base Nest HTTP exception, which is handled by the default
* Exceptions Handler.
*/
export type HttpException = {
cause: void;
name: string;
message: string;
stack?: string;
};
export type FirmwareBoardDefaultsNullable = {
schema: void;
data: DefaultsFile;
} | null;

View File

@@ -1,20 +1,5 @@
import { createContext, useContext, useState } from 'react';
import { createContext, useContext, useMemo, useState } from 'react';
import {
fetchGetFirmwaresDefaultConfigBoard,
useGetHealth,
useGetIsCompatibleVersion,
} from '@/firmware-tool-api/firmwareToolComponents';
import {
BuildResponseDTO,
CreateBoardConfigDTO,
CreateBuildFirmwareDTO,
DefaultBuildConfigDTO,
FirmwareFileDTO,
} from '@/firmware-tool-api/firmwareToolSchemas';
import { BoardPinsForm } from '@/components/firmware-tool/BoardPinsStep';
import { DeepPartial } from 'react-hook-form';
import {
BoardType,
DeviceIdT,
FirmwarePartT,
FirmwareUpdateMethod,
@@ -25,53 +10,26 @@ import {
SerialFirmwareUpdateT,
} from 'solarxr-protocol';
import { OnboardingContext } from './onboarding';
import {
BoardDefaults,
FirmwareBoardDefaultsNullable,
FirmwareWithFiles,
} from '@/firmware-tool-api/firmwareToolSchemas';
import { GetFirmwareBoardDefaultsQueryParams } from '@/firmware-tool-api/firmwareToolComponents';
import { SomeJSONSchema } from 'ajv/dist/types/json-schema';
import { Ajv2020 } from 'ajv/dist/2020';
export type SelectedSouce = {
source: GetFirmwareBoardDefaultsQueryParams;
default: FirmwareBoardDefaultsNullable;
};
export type PartialBuildFirmware = DeepPartial<CreateBuildFirmwareDTO>;
export type FirmwareBuildStatus = BuildResponseDTO;
export type SelectedDevice = {
type: FirmwareUpdateMethod;
deviceId: string | number;
deviceNames: string[];
};
export const boardTypeToFirmwareToolBoardType: Record<
Exclude<
BoardType,
// This boards will not be handled by the firmware tool.
// These are either impossible to compile automatically or deprecated
| BoardType.CUSTOM
| BoardType.SLIMEVR_DEV
| BoardType.SLIMEVR_LEGACY
| BoardType.OWOTRACK
| BoardType.WRANGLER
| BoardType.MOCOPI
| BoardType.HARITORA
| BoardType.DEV_RESERVED
>,
CreateBoardConfigDTO['type'] | null
> = {
[BoardType.UNKNOWN]: null,
[BoardType.NODEMCU]: 'BOARD_NODEMCU',
[BoardType.WROOM32]: 'BOARD_WROOM32',
[BoardType.WEMOSD1MINI]: 'BOARD_WEMOSD1MINI',
[BoardType.TTGO_TBASE]: 'BOARD_TTGO_TBASE',
[BoardType.ESP01]: 'BOARD_ESP01',
[BoardType.SLIMEVR]: 'BOARD_SLIMEVR',
[BoardType.LOLIN_C3_MINI]: 'BOARD_LOLIN_C3_MINI',
[BoardType.BEETLE32C3]: 'BOARD_BEETLE32C3',
[BoardType.ESP32C3DEVKITM1]: 'BOARD_ES32C3DEVKITM1',
[BoardType.WEMOSWROOM02]: null,
[BoardType.XIAO_ESP32C3]: null,
[BoardType.ESP32C6DEVKITC1]: null,
[BoardType.GLOVE_IMU_SLIMEVR_DEV]: null,
[BoardType.GESTURES]: null,
};
export const firmwareToolToBoardType: Record<CreateBoardConfigDTO['type'], BoardType> =
Object.fromEntries(
Object.entries(boardTypeToFirmwareToolBoardType).map((a) => a.reverse())
);
export const firmwareUpdateErrorStatus = [
FirmwareUpdateStatus.ERROR_AUTHENTICATION_FAILED,
FirmwareUpdateStatus.ERROR_DEVICE_NOT_FOUND,
@@ -109,24 +67,7 @@ export const firmwareUpdateStatusLabel: Record<FirmwareUpdateStatus, string> = {
[FirmwareUpdateStatus.ERROR_UNKNOWN]: 'firmware_update-status-ERROR_UNKNOWN',
};
export interface FirmwareToolContext {
selectBoard: (boardType: CreateBoardConfigDTO['type']) => Promise<void>;
selectVersion: (version: CreateBuildFirmwareDTO['version']) => void;
updatePins: (form: BoardPinsForm) => void;
updateImus: (imus: CreateBuildFirmwareDTO['imusConfig']) => void;
setBuildStatus: (buildStatus: FirmwareBuildStatus) => void;
selectDevices: (device: SelectedDevice[] | null) => void;
retry: () => void;
buildStatus: FirmwareBuildStatus;
defaultConfig: DefaultBuildConfigDTO | null;
newConfig: PartialBuildFirmware | null;
selectedDevices: SelectedDevice[] | null;
isStepLoading: boolean;
isGlobalLoading: boolean;
isCompatible: boolean;
isError: boolean;
}
export type FirmwareToolContext = ReturnType<typeof provideFirmwareTool>;
export const FirmwareToolContextC = createContext<FirmwareToolContext>(
undefined as any
);
@@ -139,97 +80,33 @@ export function useFirmwareTool() {
return context;
}
export function useFirmwareToolContext(): FirmwareToolContext {
const [defaultConfig, setDefaultConfig] = useState<DefaultBuildConfigDTO | null>(
null
);
export function provideFirmwareTool() {
const [selectedSource, setSelectedSource] = useState<SelectedSouce>();
const [files, setFiles] = useState<FirmwareWithFiles['files']>();
const [selectedDevices, selectDevices] = useState<SelectedDevice[] | null>(null);
const [newConfig, setNewConfig] = useState<PartialBuildFirmware>({});
const [isLoading, setLoading] = useState(false);
const { isError, isLoading: isInitialLoading, refetch } = useGetHealth({});
const compatibilityCheckEnabled = !!__VERSION_TAG__;
const { isLoading: isCompatibilityLoading, data: compatibilityData } =
useGetIsCompatibleVersion(
{ pathParams: { version: __VERSION_TAG__ } },
{ enabled: compatibilityCheckEnabled }
);
const [buildStatus, setBuildStatus] = useState<FirmwareBuildStatus>({
status: 'CREATING_BUILD_FOLDER',
id: '',
});
return {
selectBoard: async (boardType: CreateBoardConfigDTO['type']) => {
setLoading(true);
const boardDefaults = await fetchGetFirmwaresDefaultConfigBoard({
pathParams: { board: boardType },
});
setDefaultConfig(boardDefaults);
if (boardDefaults.shouldOnlyUseDefaults) {
setNewConfig((currConfig) => ({
...currConfig,
...boardDefaults,
imusConfig: boardDefaults.imuDefaults,
}));
} else {
setNewConfig((currConfig) => ({
...currConfig,
boardConfig: { ...currConfig.boardConfig, type: boardType },
imusConfig: [],
}));
}
setLoading(false);
},
updatePins: (form: BoardPinsForm) => {
setNewConfig((currConfig) => {
return {
...currConfig,
imusConfig: [...(currConfig?.imusConfig || [])],
boardConfig: {
...currConfig.boardConfig,
...form,
batteryResistances: form.batteryResistances.map((r) => Number(r)),
},
};
});
},
updateImus: (imus: CreateBuildFirmwareDTO['imusConfig']) => {
setNewConfig((currConfig) => {
return {
...currConfig,
imusConfig: imus.map(({ rotation, ...fields }) => ({
...fields,
rotation: Number(rotation),
})), // Make sure that the rotation is handled as number
};
});
},
retry: async () => {
setLoading(true);
await refetch();
setLoading(false);
},
selectVersion: (version: CreateBuildFirmwareDTO['version']) => {
setNewConfig((currConfig) => ({ ...currConfig, version }));
},
setBuildStatus,
selectDevices,
selectedSource,
setSelectedSource,
files,
setFiles,
selectedDevices,
buildStatus,
defaultConfig,
newConfig,
isStepLoading: isLoading,
isGlobalLoading: isInitialLoading || isCompatibilityLoading,
isCompatible: !compatibilityCheckEnabled || (compatibilityData?.success ?? false),
isError: isError || (!compatibilityData?.success && compatibilityCheckEnabled),
selectDevices,
selectedDefault: useMemo(
() =>
(selectedSource?.source.board &&
selectedSource?.default?.data.defaults[selectedSource.source.board]) ||
null,
[selectedSource]
),
};
}
export const getFlashingRequests = (
devices: SelectedDevice[],
firmwareFiles: FirmwareFileDTO[],
firmwareFiles: FirmwareWithFiles['files'],
onboardingState: OnboardingContext['state'],
defaultConfig: DefaultBuildConfigDTO | null
defaultConfig: BoardDefaults | null
) => {
const firmware = firmwareFiles.find(({ isFirmware }) => isFirmware);
if (!firmware) throw new Error('invalid state - no firmware to find');
@@ -244,7 +121,7 @@ export const getFlashingRequests = (
const part = new FirmwarePartT();
part.offset = 0;
part.url = firmware.url;
part.url = firmware.filePath;
const method = new OTAFirmwareUpdateT();
method.deviceId = dId;
@@ -267,12 +144,13 @@ export const getFlashingRequests = (
method.deviceId = id;
method.ssid = onboardingState.wifi.ssid;
method.password = onboardingState.wifi.password;
method.needManualReboot = defaultConfig?.needManualReboot ?? false;
method.needManualReboot =
defaultConfig?.flashingRules.needManualReboot ?? false;
method.firmwarePart = firmwareFiles.map(({ offset, url }) => {
method.firmwarePart = firmwareFiles.map(({ offset, filePath }) => {
const part = new FirmwarePartT();
part.offset = offset;
part.url = url;
part.url = filePath;
return part;
});
@@ -289,3 +167,529 @@ export const getFlashingRequests = (
}
return requests;
};
const refToKey = (ref: string) => ref.substring('#/$defs/'.length);
type Path = (string | number)[];
type OnChangeCallback = (path: Path, newValue: any, rootData: any) => void;
type TraversalContext = {
defs: NonNullable<SomeJSONSchema['$defs']>;
rootData: any;
onChange: OnChangeCallback;
ownerSchema: SomeJSONSchema | null;
path: Path;
data: any;
propertySchema?: SomeJSONSchema; // The original property schema (before ref resolution)
};
export type ComponentNode =
| {
type: 'checkbox';
label: string;
value: boolean;
path: Path;
onMutate: (newValue: boolean) => void;
}
| {
type: 'dropdown';
items: string[];
value: string;
label: string;
path: Path;
error?: string;
onMutate: (newValue: string) => void;
}
| {
type: 'text';
format: 'string' | 'number';
value: string;
label: string;
path: Path;
error?: string;
onMutate: (newValue: string) => void;
}
| {
type: 'group';
label: string | undefined;
childrens: ComponentNode[];
path: Path;
}
| {
type: 'list';
childrens: ComponentNode[];
label: string;
path: Path;
add?: () => void;
del?: (index: number) => void;
};
const setAtPath = (obj: any, path: Path, value: any): void => {
let current = obj;
for (let i = 0; i < path.length - 1; i++) {
current = current[path[i]];
}
if (value === undefined) {
const key = path[path.length - 1];
if (Array.isArray(current) && typeof key === 'number') {
current.splice(key, 1);
} else {
delete current[key];
}
} else {
current[path[path.length - 1]] = value;
}
};
const resolveLabel = (
ctx: TraversalContext,
node: SomeJSONSchema,
propertySchema?: SomeJSONSchema
): string | null => {
if (!ctx.defs) throw 'Womp womp no defs';
// If we have a property schema (the original schema before ref resolution), check it first
if (propertySchema?.$ref && propertySchema?.description) {
return propertySchema.description;
}
if (node.description) return node.description;
if (propertySchema?.$ref) {
const refContent = ctx.defs[refToKey(propertySchema.$ref)];
return resolveLabel(ctx, refContent, propertySchema);
}
if (node.$ref) {
const refContent = ctx.defs[refToKey(node.$ref)];
return resolveLabel(ctx, refContent, node);
}
return null;
};
const extractDiscriminatorValues = (
ctx: TraversalContext,
oneOf: SomeJSONSchema[],
discriminatorProp: string,
mapping: Record<string, string>
): string[] => {
const values = new Set<string>();
Object.keys(mapping).forEach((key) => values.add(key));
oneOf.forEach((option: SomeJSONSchema) => {
const target = option.$ref ? ctx.defs[refToKey(option.$ref)] : option;
const prop = target?.properties?.[discriminatorProp];
if (prop) {
if (prop.const !== undefined) {
values.add(prop.const);
}
if (Array.isArray(prop.enum)) {
prop.enum.forEach((v: string) => values.add(v));
}
}
});
return Array.from(values);
};
const isPropertyRequired = (ctx: TraversalContext): boolean => {
const propertyName = ctx.path[ctx.path.length - 1];
if (!ctx.ownerSchema || typeof propertyName === 'number') {
return false;
}
// The ownerSchema is already resolved (no $refs), so we can check directly
return ctx.ownerSchema.required?.includes(propertyName as string) ?? false;
};
const handleObjectNode = (
ctx: TraversalContext,
node: SomeJSONSchema
): ComponentNode[] => {
if (node.oneOf && Array.isArray(node.oneOf) && node.discriminator?.propertyName) {
return handleDiscriminatedOneOf(ctx, node);
}
if (node.properties) {
const childs: ComponentNode[] = [];
for (const property of Object.keys(node.properties)) {
const propSchema = node.properties[property];
childs.push(
...handleNode(
{
...ctx,
ownerSchema: node,
propertySchema: propSchema,
path: [...ctx.path, property],
data: ctx.data[property],
},
propSchema
)
);
}
return [
{
type: 'group',
childrens: childs,
path: ctx.path,
label: resolveLabel(ctx, node, ctx.propertySchema) ?? undefined,
},
];
}
throw new Error('Unknown object structure: missing oneOf or properties');
};
const handleDiscriminatedOneOf = (
ctx: TraversalContext,
node: SomeJSONSchema
): ComponentNode[] => {
if (!ctx.defs) throw 'Womp womp no defs';
const discriminator = node.discriminator!.propertyName;
const mapping = node.discriminator!.mapping ?? {};
const discriminatorValue = ctx.data?.[discriminator];
const possibleValues = extractDiscriminatorValues(
ctx,
node.oneOf!,
discriminator,
mapping
);
const discriminatorPath = [...ctx.path, discriminator];
const discriminatorDropdown: ComponentNode = {
type: 'dropdown',
label: discriminator,
items: possibleValues,
value: discriminatorValue ?? possibleValues[0] ?? '',
path: discriminatorPath,
onMutate: (newValue: string) => {
setAtPath(ctx.rootData, discriminatorPath, newValue);
ctx.onChange(discriminatorPath, newValue, ctx.rootData);
},
};
const activeValue = discriminatorValue ?? possibleValues[0];
if (!activeValue) {
console.warn('No discriminator values found, skipping');
return [];
}
let candidate: SomeJSONSchema | undefined;
if (mapping[activeValue]) {
const mappedKey = refToKey(mapping[activeValue]);
candidate = ctx.defs[mappedKey];
}
if (!candidate) {
candidate = node.oneOf!.find((o: SomeJSONSchema) => {
const target = o.$ref ? ctx.defs[refToKey(o.$ref)] : o;
const prop = target?.properties?.[discriminator];
if (!prop) return false;
if (prop.const === activeValue) return true;
if (Array.isArray(prop.enum) && prop.enum.includes(activeValue)) return true;
return false;
});
}
if (!candidate) {
console.warn(`No matching discriminator found for ${activeValue}, skipping`);
return [];
}
const resolvedCandidate = candidate.$ref
? ctx.defs[refToKey(candidate.$ref)]
: candidate;
const childs: ComponentNode[] = [discriminatorDropdown];
if (resolvedCandidate.properties) {
for (const property of Object.keys(resolvedCandidate.properties)) {
if (property === discriminator) continue;
const propSchema = resolvedCandidate.properties[property];
childs.push(
...handleNode(
{
...ctx,
ownerSchema: resolvedCandidate,
propertySchema: propSchema,
path: [...ctx.path, property],
data: ctx.data[property],
},
propSchema
)
);
}
}
return [
{
type: 'group',
childrens: childs,
path: ctx.path,
label:
resolveLabel(ctx, resolvedCandidate) ??
resolveLabel(ctx, node, ctx.propertySchema) ??
undefined,
},
];
};
const handleArrayNode = (
ctx: TraversalContext,
node: SomeJSONSchema
): ComponentNode[] => {
if (!ctx.defs) throw 'Womp womp no defs';
if (!Array.isArray(ctx.data)) {
throw new Error('Expected array data but received non-array');
}
const childs: ComponentNode[] = [];
ctx.data.forEach((d, index) => {
childs.push(
...handleNode(
{
...ctx,
ownerSchema: node,
propertySchema: node.items,
path: [...ctx.path, index],
data: d,
},
node.items
)
);
});
let add = undefined;
if (node.items.$ref && childs.length < node.maxItems) {
const item = ctx.defs[refToKey(node.items.$ref)];
const discriminatorPath = [...ctx.path, childs.length];
if (item.discriminator?.propertyName && item.oneOf) {
const discriminator = item.discriminator?.propertyName;
const discriminatorValue = item.oneOf
.map((o: SomeJSONSchema) => {
if (o.$ref) return ctx.defs[refToKey(o.$ref)];
return o;
})
.map((o: SomeJSONSchema) => {
if (o.type === 'object' && o.properties[discriminator]?.const)
return o.properties[discriminator].const;
if (o.type === 'string' && o.enum && o.enum.length > 0) return o.enum[0];
return null;
})
.find((o: string) => !!o);
if (discriminatorValue) {
const payload = {
[discriminator]: discriminatorValue,
};
add = () => {
setAtPath(ctx.rootData, discriminatorPath, payload);
ctx.onChange(discriminatorPath, payload, ctx.rootData);
};
}
} else {
add = () => {
setAtPath(ctx.rootData, discriminatorPath, item);
ctx.onChange(discriminatorPath, item, ctx.rootData);
};
}
}
let del = undefined;
if (node.items.$ref && childs.length > node.minItems) {
del = (index: number) => {
const discriminatorPath = [...ctx.path, index];
setAtPath(ctx.rootData, discriminatorPath, undefined);
ctx.onChange(discriminatorPath, undefined, ctx.rootData);
};
}
return [
{
type: 'list',
childrens: childs,
path: ctx.path,
label: resolveLabel(ctx, node, ctx.propertySchema) ?? 'unknown label',
add,
del,
},
];
};
const handleStringNode = (
ctx: TraversalContext,
node: SomeJSONSchema
): ComponentNode[] => {
const required = isPropertyRequired(ctx);
if (!ctx.defs) throw 'Womp womp no defs';
if (node.enum) {
const error = required && !ctx.data;
return [
{
type: 'dropdown',
label: resolveLabel(ctx, node, ctx.propertySchema) ?? 'unknown label',
items: node.enum,
value: ctx.data,
path: ctx.path,
error: error ? 'Field not selected' : undefined,
onMutate: (newValue: string) => {
setAtPath(ctx.rootData, ctx.path, newValue);
ctx.onChange(ctx.path, newValue, ctx.rootData);
},
},
];
}
let error: string | undefined = undefined;
if (required && !ctx.data) {
error = 'firmware_tool-board_defaults-error-required';
} else if (ctx.data && node.pattern && !ctx.data.match(new RegExp(node.pattern))) {
error = 'firmware_tool-board_defaults-error-format';
}
return [
{
type: 'text',
format: 'string',
label: resolveLabel(ctx, node, ctx.propertySchema) ?? 'unknown label',
value: ctx.data,
path: ctx.path,
error,
onMutate: (newValue: string) => {
setAtPath(ctx.rootData, ctx.path, newValue);
ctx.onChange(ctx.path, newValue, ctx.rootData);
},
},
];
};
const handleNumberNode = (
ctx: TraversalContext,
node: SomeJSONSchema
): ComponentNode[] => {
const required = isPropertyRequired(ctx);
const value: string = ctx.data?.toString() ?? '';
let error: string | undefined = undefined;
if (required && !value) {
error = 'firmware_tool-board_defaults-error-required';
} else if (value && !value.match(/^-?\d+(\.\d+)?$/)) {
error = 'firmware_tool-board_defaults-error-format-number';
}
return [
{
type: 'text',
format: 'number',
label: resolveLabel(ctx, node, ctx.propertySchema) ?? 'unknown label',
value,
path: ctx.path,
error,
onMutate: (newValue: string) => {
setAtPath(ctx.rootData, ctx.path, Number(newValue));
ctx.onChange(ctx.path, Number(newValue), ctx.rootData);
},
},
];
};
const handleBooleanNode = (
ctx: TraversalContext,
node: SomeJSONSchema
): ComponentNode[] => {
return [
{
type: 'checkbox',
label: resolveLabel(ctx, node, ctx.propertySchema) ?? 'unknown label',
value: ctx.data,
path: ctx.path,
onMutate: (newValue: boolean) => {
setAtPath(ctx.rootData, ctx.path, newValue);
ctx.onChange(ctx.path, newValue, ctx.rootData);
},
},
];
};
const handleNode = (
ctx: TraversalContext,
node: SomeJSONSchema | undefined
): ComponentNode[] => {
if (!ctx.defs) throw 'Womp womp no defs';
if (!node) {
return [];
}
// When we encounter a $ref, resolve it but keep the same ownerSchema and propertySchema
if (node.$ref) {
return handleNode(ctx, ctx.defs[refToKey(node.$ref)]);
}
switch (node.type) {
case 'object':
return handleObjectNode(ctx, node);
case 'boolean':
return handleBooleanNode(ctx, node);
case 'array':
return handleArrayNode(ctx, node);
case 'string':
return handleStringNode(ctx, node);
case 'number':
return handleNumberNode(ctx, node);
default:
console.warn('unhandled type', node.type, 'giving empty components');
return [];
}
};
export const boardComponentGraph = (
selectedSource: SelectedSouce,
onChange?: OnChangeCallback
) => {
const d = selectedSource.default?.schema as any as SomeJSONSchema;
if (!d.$defs) throw 'no defs';
const t = refToKey(d.properties.defaults.additionalProperties.$ref);
if (!t) throw 'unable to get defaults ref';
const boardConfig = d.$defs[t];
const boardValues = d.$defs[refToKey(boardConfig.properties.values.$ref)];
const data = selectedSource.default?.data.defaults[selectedSource.source.board]
.values as any;
const ctx: TraversalContext = {
defs: d.$defs,
rootData: data,
onChange: onChange ?? (() => {}),
ownerSchema: null,
path: [],
data: data,
};
const graph = handleNode(ctx, boardValues);
return graph;
};
export const validateSource = (source: SelectedSouce) => {
const def = source.default;
if (!def) throw 'no schema or data';
const ajv = new Ajv2020({ discriminator: true, verbose: true, allErrors: true });
return ajv.validate(def.schema as any, source.default?.data);
};

View File

@@ -8,7 +8,7 @@ export interface FirmwareRelease {
name: string;
version: string;
changelog: string;
firmwareFile: string;
firmwareFiles: Partial<Record<BoardType, string>>;
userCanUpdate: boolean;
}
@@ -89,7 +89,9 @@ export async function fetchCurrentFirmwareRelease(): Promise<FirmwareRelease | n
const processedReleses = [];
for (const release of releases) {
const fwAsset = firstAsset(release.assets, 'BOARD_SLIMEVR-firmware.bin');
if (!release.assets || !fwAsset /* || release.prerelease */) continue;
const fw12Asset = firstAsset(release.assets, 'BOARD_SLIMEVR_V1_2-firmware.bin');
if (!release.assets || (!fwAsset && !fw12Asset) /* || release.prerelease */)
continue;
let version = release.tag_name;
if (version.charAt(0) === 'v') {
@@ -105,7 +107,10 @@ export async function fetchCurrentFirmwareRelease(): Promise<FirmwareRelease | n
name: release.name,
version,
changelog: release.body,
firmwareFile: fwAsset.browser_download_url,
firmwareFiles: {
[BoardType.SLIMEVR]: fwAsset.browser_download_url,
[BoardType.SLIMEVR_V1_2]: fw12Asset.browser_download_url,
},
userCanUpdate,
});
@@ -123,7 +128,10 @@ export function checkForUpdate(
if (!currentFirmwareRelease.userCanUpdate) return 'blocked';
if (
device.hardwareInfo?.officialBoardType !== BoardType.SLIMEVR ||
!device.hardwareInfo?.officialBoardType ||
![BoardType.SLIMEVR, BoardType.SLIMEVR_V1_2].includes(
device.hardwareInfo.officialBoardType
) ||
!semver.valid(currentFirmwareRelease.version) ||
!semver.valid(device.hardwareInfo.firmwareVersion?.toString() ?? 'none')
) {

View File

@@ -1,6 +1,10 @@
import { match } from '@formatjs/intl-localematcher';
import { FluentBundle, FluentResource } from '@fluent/bundle';
import { LocalizationProvider, ReactLocalization } from '@fluent/react';
import { FluentBundle, FluentResource, FluentVariable } from '@fluent/bundle';
import {
LocalizationProvider,
ReactLocalization,
useLocalization,
} from '@fluent/react';
import {
Children,
ReactNode,
@@ -146,3 +150,29 @@ export function useLocaleConfig() {
}
return context;
}
export function useSafeLocalization() {
const l = useLocalization();
return {
...l,
getStringOrNull: (
id: string,
vars?: Record<string, FluentVariable> | null
): string | null => {
const bundle = l.l10n.getBundle(id);
if (bundle) {
const msg = bundle.getMessage(id);
if (msg && msg.value) {
const errors: Array<Error> = [];
const value = bundle.formatPattern(msg.value, vars, errors);
for (const error of errors) {
l.l10n.reportError(error);
}
return value;
}
}
return null;
},
};
}

31
pnpm-lock.yaml generated
View File

@@ -86,6 +86,9 @@ importers:
'@twemoji/svg':
specifier: ^15.0.0
version: 15.0.0
ajv:
specifier: ^8.17.1
version: 8.17.1
browser-fs-access:
specifier: ^0.35.0
version: 0.35.0
@@ -1598,6 +1601,9 @@ packages:
ajv@6.12.6:
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
ajv@8.17.1:
resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==}
ansi-escapes@7.0.0:
resolution: {integrity: sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==}
engines: {node: '>=18'}
@@ -2369,8 +2375,11 @@ packages:
fast-safe-stringify@2.1.1:
resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==}
fastq@1.19.1:
resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==}
fast-uri@3.1.0:
resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==}
fastq@1.17.1:
resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==}
fdir@6.5.0:
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
@@ -3014,6 +3023,9 @@ packages:
json-schema-traverse@0.4.1:
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
json-schema-traverse@1.0.0:
resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
json-stable-stringify-without-jsonify@1.0.1:
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
@@ -5126,7 +5138,7 @@ snapshots:
'@nodelib/fs.walk@1.2.8':
dependencies:
'@nodelib/fs.scandir': 2.1.5
fastq: 1.19.1
fastq: 1.17.1
'@nolyfill/is-core-module@1.0.39': {}
@@ -6051,6 +6063,13 @@ snapshots:
json-schema-traverse: 0.4.1
uri-js: 4.4.1
ajv@8.17.1:
dependencies:
fast-deep-equal: 3.1.3
fast-uri: 3.1.0
json-schema-traverse: 1.0.0
require-from-string: 2.0.2
ansi-escapes@7.0.0:
dependencies:
environment: 1.1.0
@@ -7081,7 +7100,9 @@ snapshots:
fast-safe-stringify@2.1.1: {}
fastq@1.19.1:
fast-uri@3.1.0: {}
fastq@1.17.1:
dependencies:
reusify: 1.1.0
@@ -7745,6 +7766,8 @@ snapshots:
json-schema-traverse@0.4.1: {}
json-schema-traverse@1.0.0: {}
json-stable-stringify-without-jsonify@1.0.1: {}
json5@1.0.2:

View File

@@ -56,6 +56,8 @@ enum class BoardType(val id: UInt) {
ESP32C6DEVKITC1(19u),
GLOVE_IMU_SLIMEVR_DEV(20u),
GESTURES(21u),
SLIMEVR_V1_2(22u),
ESP32S3_SUPERMINI(23u),
DEV_RESERVED(250u),
;
@@ -84,6 +86,8 @@ enum class BoardType(val id: UInt) {
HARITORA -> "Haritora"
ESP32C6DEVKITC1 -> "Espressif ESP32-C6 DevKitC-1"
GLOVE_IMU_SLIMEVR_DEV -> "SlimeVR Dev IMU Glove"
SLIMEVR_V1_2 -> "SlimeVR v1.2"
ESP32S3_SUPERMINI -> "ESP32-S3 SuperMini"
DEV_RESERVED -> "Prototype"
}