mirror of
https://github.com/SlimeVR/SlimeVR-Server.git
synced 2026-04-05 18:01:56 +02:00
Firmware tool V2 (#1585)
This commit is contained in:
8
gui/.env
8
gui/.env
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
229
gui/src/components/firmware-tool/steps/BoardDefaultsStep.tsx
Normal file
229
gui/src/components/firmware-tool/steps/BoardDefaultsStep.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
@@ -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>
|
||||
</>
|
||||
@@ -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) => {
|
||||
313
gui/src/components/firmware-tool/steps/SelectSourceStep.tsx
Normal file
313
gui/src/components/firmware-tool/steps/SelectSourceStep.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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');
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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')
|
||||
) {
|
||||
|
||||
@@ -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
31
pnpm-lock.yaml
generated
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
|
||||
Submodule solarxr-protocol updated: df26226d10...eed73567f7
Reference in New Issue
Block a user