Browse Source

Remove trailing whitespace

pull/110/head
Lennart Buhl 3 years ago
parent
commit
c70ffbdb21
  1. 2
      README.md
  2. 6
      src/components/Dashboard.tsx
  3. 2
      src/components/Form/DynamicForm.tsx
  4. 4
      src/components/Form/FormWrapper.tsx
  5. 8
      src/components/PageComponents/Config/Bluetooth.tsx
  6. 12
      src/components/PageComponents/Config/Device.tsx
  7. 8
      src/components/PageComponents/Config/Display.tsx
  8. 8
      src/components/PageComponents/Config/LoRa.tsx
  9. 8
      src/components/PageComponents/Config/Network.tsx
  10. 8
      src/components/PageComponents/Config/Position.tsx
  11. 8
      src/components/PageComponents/Config/Power.tsx
  12. 52
      src/components/PageComponents/Flasher/ConfigList.tsx
  13. 53
      src/components/PageComponents/Flasher/DeviceList.tsx
  14. 62
      src/components/PageComponents/Flasher/FlashSettings.tsx
  15. 6
      src/components/PageComponents/ModuleConfig/Audio.tsx
  16. 6
      src/components/PageComponents/ModuleConfig/CannedMessage.tsx
  17. 6
      src/components/PageComponents/ModuleConfig/ExternalNotification.tsx
  18. 6
      src/components/PageComponents/ModuleConfig/RangeTest.tsx
  19. 6
      src/components/PageComponents/ModuleConfig/Serial.tsx
  20. 6
      src/components/PageComponents/ModuleConfig/StoreForward.tsx
  21. 6
      src/components/PageComponents/ModuleConfig/Telemetry.tsx
  22. 2
      src/components/UI/ConfigSelectButton.tsx
  23. 28
      src/core/flashing/FirmwareDb.ts
  24. 84
      src/core/flashing/Flasher.ts
  25. 4
      src/core/stores/deviceStore.ts
  26. 5
      src/pages/Config/ConfigTabs.tsx
  27. 2
      src/pages/Config/ModuleConfig.tsx
  28. 2
      tsconfig.json

2
README.md

@ -6,7 +6,7 @@
pnpm install pnpm install
3. Start the server by running 3. Start the server by running
pnpm dev pnpm dev
4. The website can then be found at http://localhost:5173/ 4. The website can then be found at http://localhost:5173/

6
src/components/Dashboard.tsx

@ -19,9 +19,9 @@ import type { FlashState } from '@app/core/flashing/Flasher';
export const Dashboard = () => { export const Dashboard = () => {
let { configPresetRoot, configPresetSelected, overallFlashingState } = useAppStore(); let { configPresetRoot, configPresetSelected, overallFlashingState } = useAppStore();
const getTotalConfigCount = (c: ConfigPreset): number => c.children.map(child => getTotalConfigCount(child)).reduce((prev, cur) => prev + cur, c.count); const getTotalConfigCount = (c: ConfigPreset): number => c.children.map(child => getTotalConfigCount(child)).reduce((prev, cur) => prev + cur, c.count);
const [ totalConfigCount, setTotalConfigCount ] = useState(configPresetRoot.getTotalConfigCount()); const [ totalConfigCount, setTotalConfigCount ] = useState(configPresetRoot.getTotalConfigCount());
const [deviceSelectedToFlash, setDeviceSelectedToFlash] = useState(new Array<FlashState>(100).fill({progress: 1, state: 'doFlash'})); const [deviceSelectedToFlash, setDeviceSelectedToFlash] = useState(new Array<FlashState>(100).fill({progress: 1, state: 'doFlash'}));
return ( return (
<div className="flex flex-col h-full gap-3 p-3"> <div className="flex flex-col h-full gap-3 p-3">

2
src/components/Form/DynamicForm.tsx

@ -99,7 +99,7 @@ export function DynamicForm<T extends FieldValues>({
{fieldGroup.fields.map((field, index) => { {fieldGroup.fields.map((field, index) => {
const fullFieldName = `${fieldGroup.label}_${field.name}` const fullFieldName = `${fieldGroup.label}_${field.name}`
const [ enableSwitchState, setEnableSwitchState ] = useState(enableSwitch?.getEnabled(fullFieldName)); const [ enableSwitchState, setEnableSwitchState ] = useState(enableSwitch?.getEnabled(fullFieldName));
return ( return (
<FieldWrapper label={field.label} description={field.description} enableSwitchEnabled={enableSwitchState} onEnableSwitchChanged={(value) => { <FieldWrapper label={field.label} description={field.description} enableSwitchEnabled={enableSwitchState} onEnableSwitchChanged={(value) => {
enableSwitch?.setEnabled(fullFieldName, value); enableSwitch?.setEnabled(fullFieldName, value);

4
src/components/Form/FormWrapper.tsx

@ -26,12 +26,12 @@ export const FieldWrapper = ({
{enableSwitchEnabled !== undefined && ( {enableSwitchEnabled !== undefined && (
<div className="mt-4 space-y-4"> <div className="mt-4 space-y-4">
<Switch <Switch
defaultChecked={enableSwitchEnabled} defaultChecked={enableSwitchEnabled}
onCheckedChange={onEnableSwitchChanged} onCheckedChange={onEnableSwitchChanged}
/> />
</div> </div>
)} )}
</div> </div>
<div className="sm:col-span-2"> <div className="sm:col-span-2">
<div className="max-w-lg"> <div className="max-w-lg">
<p className="text-sm text-gray-500">{description}</p> <p className="text-sm text-gray-500">{description}</p>

8
src/components/PageComponents/Config/Bluetooth.tsx

@ -17,14 +17,14 @@ export const Bluetooth = (): JSX.Element => {
return config.overrideValues![name] ?? false; return config.overrideValues![name] ?? false;
}, },
setEnabled(name, value) { setEnabled(name, value) {
config.overrideValues![name] = value; config.overrideValues![name] = value;
}, },
} : undefined; } : undefined;
const isPresetConfig = !("id" in config); const isPresetConfig = !("id" in config);
const { setWorkingConfig } = !isPresetConfig ? useDevice() : { setWorkingConfig: undefined }; const { setWorkingConfig } = !isPresetConfig ? useDevice() : { setWorkingConfig: undefined };
const setConfig: (data: BluetoothValidation) => void = const setConfig: (data: BluetoothValidation) => void =
isPresetConfig ? (data) => { isPresetConfig ? (data) => {
config.config.bluetooth = new Protobuf.Config_BluetoothConfig(data); config.config.bluetooth = new Protobuf.Config_BluetoothConfig(data);
(config as ConfigPreset).saveConfigTree(); (config as ConfigPreset).saveConfigTree();
} }
: (data) => { : (data) => {
@ -36,9 +36,9 @@ export const Bluetooth = (): JSX.Element => {
} }
}) })
); );
} }
const onSubmit = setConfig; const onSubmit = setConfig;
return ( return (
<DynamicForm<BluetoothValidation> <DynamicForm<BluetoothValidation>

12
src/components/PageComponents/Config/Device.tsx

@ -10,21 +10,21 @@ import {
} from '@core/stores/deviceStore.js'; } from '@core/stores/deviceStore.js';
import { Protobuf } from '@meshtastic/meshtasticjs'; import { Protobuf } from '@meshtastic/meshtasticjs';
export const Device = (): JSX.Element => { export const Device = (): JSX.Element => {
const config = useConfig(); const config = useConfig();
const enableSwitch: EnableSwitchData | undefined = config.overrideValues ? { const enableSwitch: EnableSwitchData | undefined = config.overrideValues ? {
getEnabled(name) { getEnabled(name) {
return config.overrideValues![name] ?? false; return config.overrideValues![name] ?? false;
}, },
setEnabled(name, value) { setEnabled(name, value) {
config.overrideValues![name] = value; config.overrideValues![name] = value;
}, },
} : undefined; } : undefined;
const isPresetConfig = !("id" in config); const isPresetConfig = !("id" in config);
const { setWorkingConfig } = !isPresetConfig ? useDevice() : { setWorkingConfig: undefined }; const { setWorkingConfig } = !isPresetConfig ? useDevice() : { setWorkingConfig: undefined };
const setConfig: (data: DeviceValidation) => void = const setConfig: (data: DeviceValidation) => void =
isPresetConfig ? (data) => { isPresetConfig ? (data) => {
config.config.device = new Protobuf.Config_DeviceConfig(data); config.config.device = new Protobuf.Config_DeviceConfig(data);
(config as ConfigPreset).saveConfigTree(); (config as ConfigPreset).saveConfigTree();
} }
: (data) => { : (data) => {
@ -36,9 +36,9 @@ export const Device = (): JSX.Element => {
} }
}) })
); );
} }
const onSubmit = setConfig; const onSubmit = setConfig;
return ( return (
<DynamicForm<DeviceValidation> <DynamicForm<DeviceValidation>

8
src/components/PageComponents/Config/Display.tsx

@ -17,14 +17,14 @@ export const Display = (): JSX.Element => {
return config.overrideValues![name] ?? false; return config.overrideValues![name] ?? false;
}, },
setEnabled(name, value) { setEnabled(name, value) {
config.overrideValues![name] = value; config.overrideValues![name] = value;
}, },
} : undefined; } : undefined;
const isPresetConfig = !("id" in config); const isPresetConfig = !("id" in config);
const { setWorkingConfig } = !isPresetConfig ? useDevice() : { setWorkingConfig: undefined }; const { setWorkingConfig } = !isPresetConfig ? useDevice() : { setWorkingConfig: undefined };
const setConfig: (data: DisplayValidation) => void = const setConfig: (data: DisplayValidation) => void =
isPresetConfig ? (data) => { isPresetConfig ? (data) => {
config.config.display = new Protobuf.Config_DisplayConfig(data); config.config.display = new Protobuf.Config_DisplayConfig(data);
(config as ConfigPreset).saveConfigTree(); (config as ConfigPreset).saveConfigTree();
} }
: (data) => { : (data) => {
@ -36,9 +36,9 @@ export const Display = (): JSX.Element => {
} }
}) })
); );
} }
const onSubmit = setConfig; const onSubmit = setConfig;
return ( return (
<DynamicForm<DisplayValidation> <DynamicForm<DisplayValidation>

8
src/components/PageComponents/Config/LoRa.tsx

@ -17,14 +17,14 @@ export const LoRa = (): JSX.Element => {
return config.overrideValues![name] ?? false; return config.overrideValues![name] ?? false;
}, },
setEnabled(name, value) { setEnabled(name, value) {
config.overrideValues![name] = value; config.overrideValues![name] = value;
}, },
} : undefined; } : undefined;
const isPresetConfig = !("id" in config); const isPresetConfig = !("id" in config);
const { setWorkingConfig } = !isPresetConfig ? useDevice() : { setWorkingConfig: undefined }; const { setWorkingConfig } = !isPresetConfig ? useDevice() : { setWorkingConfig: undefined };
const setConfig: (data: LoRaValidation) => void = const setConfig: (data: LoRaValidation) => void =
isPresetConfig ? (data) => { isPresetConfig ? (data) => {
config.config.lora = new Protobuf.Config_LoRaConfig(data); config.config.lora = new Protobuf.Config_LoRaConfig(data);
(config as ConfigPreset).saveConfigTree(); (config as ConfigPreset).saveConfigTree();
} }
: (data) => { : (data) => {
@ -36,9 +36,9 @@ export const LoRa = (): JSX.Element => {
} }
}) })
); );
} }
const onSubmit = setConfig; const onSubmit = setConfig;
return ( return (
<DynamicForm<LoRaValidation> <DynamicForm<LoRaValidation>

8
src/components/PageComponents/Config/Network.tsx

@ -17,14 +17,14 @@ export const Network = (): JSX.Element => {
return config.overrideValues![name] ?? false; return config.overrideValues![name] ?? false;
}, },
setEnabled(name, value) { setEnabled(name, value) {
config.overrideValues![name] = value; config.overrideValues![name] = value;
}, },
} : undefined; } : undefined;
const isPresetConfig = !("id" in config); const isPresetConfig = !("id" in config);
const { setWorkingConfig } = !isPresetConfig ? useDevice() : { setWorkingConfig: undefined }; const { setWorkingConfig } = !isPresetConfig ? useDevice() : { setWorkingConfig: undefined };
const setConfig: (data: NetworkValidation) => void = const setConfig: (data: NetworkValidation) => void =
isPresetConfig ? (data) => { isPresetConfig ? (data) => {
config.config.network = new Protobuf.Config_NetworkConfig(data); config.config.network = new Protobuf.Config_NetworkConfig(data);
(config as ConfigPreset).saveConfigTree(); (config as ConfigPreset).saveConfigTree();
} }
: (data) => { : (data) => {
@ -36,9 +36,9 @@ export const Network = (): JSX.Element => {
} }
}) })
); );
} }
const onSubmit = setConfig; const onSubmit = setConfig;
return ( return (
<DynamicForm<NetworkValidation> <DynamicForm<NetworkValidation>

8
src/components/PageComponents/Config/Position.tsx

@ -17,14 +17,14 @@ export const Position = (): JSX.Element => {
return config.overrideValues![name] ?? false; return config.overrideValues![name] ?? false;
}, },
setEnabled(name, value) { setEnabled(name, value) {
config.overrideValues![name] = value; config.overrideValues![name] = value;
}, },
} : undefined; } : undefined;
const isPresetConfig = !("id" in config); const isPresetConfig = !("id" in config);
const { setWorkingConfig } = !isPresetConfig ? useDevice() : { setWorkingConfig: undefined }; const { setWorkingConfig } = !isPresetConfig ? useDevice() : { setWorkingConfig: undefined };
const setConfig: (data: PositionValidation) => void = const setConfig: (data: PositionValidation) => void =
isPresetConfig ? (data) => { isPresetConfig ? (data) => {
config.config.position = new Protobuf.Config_PositionConfig(data); config.config.position = new Protobuf.Config_PositionConfig(data);
(config as ConfigPreset).saveConfigTree(); (config as ConfigPreset).saveConfigTree();
} }
: (data) => { : (data) => {
@ -36,9 +36,9 @@ export const Position = (): JSX.Element => {
} }
}) })
); );
} }
const onSubmit = setConfig; const onSubmit = setConfig;
return ( return (
<DynamicForm<PositionValidation> <DynamicForm<PositionValidation>

8
src/components/PageComponents/Config/Power.tsx

@ -17,14 +17,14 @@ export const Power = (): JSX.Element => {
return config.overrideValues![name] ?? false; return config.overrideValues![name] ?? false;
}, },
setEnabled(name, value) { setEnabled(name, value) {
config.overrideValues![name] = value; config.overrideValues![name] = value;
}, },
} : undefined; } : undefined;
const isPresetConfig = !("id" in config); const isPresetConfig = !("id" in config);
const { setWorkingConfig } = !isPresetConfig ? useDevice() : { setWorkingConfig: undefined }; const { setWorkingConfig } = !isPresetConfig ? useDevice() : { setWorkingConfig: undefined };
const setConfig: (data: PowerValidation) => void = const setConfig: (data: PowerValidation) => void =
isPresetConfig ? (data) => { isPresetConfig ? (data) => {
config.config.power = new Protobuf.Config_PowerConfig(data); config.config.power = new Protobuf.Config_PowerConfig(data);
(config as ConfigPreset).saveConfigTree(); (config as ConfigPreset).saveConfigTree();
} }
: (data) => { : (data) => {
@ -36,9 +36,9 @@ export const Power = (): JSX.Element => {
} }
}) })
); );
} }
const onSubmit = setConfig; const onSubmit = setConfig;
return ( return (
<DynamicForm<PowerValidation> <DynamicForm<PowerValidation>

52
src/components/PageComponents/Flasher/ConfigList.tsx

@ -10,7 +10,7 @@ export const ConfigList = ({rootConfig, setTotalConfigCountDiff}: {rootConfig: C
const { toast } = useToast(); const { toast } = useToast();
if(configPresetSelected === undefined) { if(configPresetSelected === undefined) {
setConfigPresetSelected(configPresetRoot); setConfigPresetSelected(configPresetRoot);
return (<div/>); return (<div/>);
} }
const disabled = overallFlashingState.state == "busy"; const disabled = overallFlashingState.state == "busy";
@ -18,12 +18,12 @@ export const ConfigList = ({rootConfig, setTotalConfigCountDiff}: {rootConfig: C
<div className="flex flex-col min-w-[250px] w-full rounded-md border border-dashed border-slate-200 py-3 px-2 dark:border-slate-700"> <div className="flex flex-col min-w-[250px] w-full rounded-md border border-dashed border-slate-200 py-3 px-2 dark:border-slate-700">
<div className="flex justify-between"> <div className="flex justify-between">
<div className="flex gap-2"> <div className="flex gap-2">
<button <button
className="transition-all hover:text-accent mb-4" className="transition-all hover:text-accent mb-4"
title="Add new configuration as child" title="Add new configuration as child"
onClick={() => { onClick={() => {
const newPreset = new ConfigPreset("New Preset", configPresetSelected); const newPreset = new ConfigPreset("New Preset", configPresetSelected);
configPresetSelected?.children.push(newPreset); configPresetSelected?.children.push(newPreset);
setConfigPresetRoot(configPresetRoot.shallowClone()); setConfigPresetRoot(configPresetRoot.shallowClone());
setConfigPresetSelected(newPreset); setConfigPresetSelected(newPreset);
setEditSelected(true); setEditSelected(true);
@ -33,20 +33,20 @@ export const ConfigList = ({rootConfig, setTotalConfigCountDiff}: {rootConfig: C
> >
<PlusIcon/> <PlusIcon/>
</button> </button>
<button <button
className="transition-all hover:text-accent mb-4" className="transition-all hover:text-accent mb-4"
title="Rename" title="Rename"
onClick={() => { onClick={() => {
setEditSelected(true); setEditSelected(true);
}} }}
disabled={disabled} disabled={disabled}
> >
<Edit3Icon/> <Edit3Icon/>
</button> </button>
<button <button
className="transition-all hover:text-accent mb-4" className="transition-all hover:text-accent mb-4"
title="Delete" title="Delete"
onClick={() => { onClick={() => {
if(configPresetSelected.parent === undefined) { if(configPresetSelected.parent === undefined) {
if(!confirm(`Are you sure you want to reset the preset list to default?`)) if(!confirm(`Are you sure you want to reset the preset list to default?`))
return; return;
@ -54,12 +54,12 @@ export const ConfigList = ({rootConfig, setTotalConfigCountDiff}: {rootConfig: C
setConfigPresetRoot(newDefault); setConfigPresetRoot(newDefault);
newDefault.saveConfigTree(); newDefault.saveConfigTree();
return; return;
} }
if(!confirm(`Are you sure you want to remove "${configPresetSelected.name}" and all its children?`)) if(!confirm(`Are you sure you want to remove "${configPresetSelected.name}" and all its children?`))
return; return;
configPresetSelected.parent.children = configPresetSelected.parent.children.filter(c => c != configPresetSelected); configPresetSelected.parent.children = configPresetSelected.parent.children.filter(c => c != configPresetSelected);
setConfigPresetSelected(configPresetSelected.parent); setConfigPresetSelected(configPresetSelected.parent);
configPresetSelected.saveConfigTree(); configPresetSelected.saveConfigTree();
}} }}
disabled={disabled} disabled={disabled}
> >
@ -67,26 +67,26 @@ export const ConfigList = ({rootConfig, setTotalConfigCountDiff}: {rootConfig: C
</button> </button>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<button <button
className="transition-all hover:text-accent mb-4" className="transition-all hover:text-accent mb-4"
title="Import" title="Import"
onClick={() => { onClick={() => {
ConfigPreset.importConfigTree().then( ConfigPreset.importConfigTree().then(
(root) => { (root) => {
if(root) { if(root) {
let newEntry = root; let newEntry = root;
debugger; debugger;
if(configPresetSelected.parent) { if(configPresetSelected.parent) {
const childIndex = configPresetSelected.parent.children.indexOf(configPresetSelected); const childIndex = configPresetSelected.parent.children.indexOf(configPresetSelected);
configPresetSelected.parent.children[childIndex] = root; configPresetSelected.parent.children[childIndex] = root;
root.parent = configPresetSelected.parent; root.parent = configPresetSelected.parent;
newEntry = root; newEntry = root;
setConfigPresetRoot(configPresetRoot.shallowClone()); setConfigPresetRoot(configPresetRoot.shallowClone());
} }
else { else {
root.overrideValues = undefined; root.overrideValues = undefined;
setConfigPresetRoot(root); setConfigPresetRoot(root);
} }
setConfigPresetSelected(newEntry); setConfigPresetSelected(newEntry);
root.saveConfigTree(); root.saveConfigTree();
toast({ toast({
@ -101,22 +101,22 @@ export const ConfigList = ({rootConfig, setTotalConfigCountDiff}: {rootConfig: C
}} }}
disabled={disabled} disabled={disabled}
> >
<UploadIcon/> <UploadIcon/>
</button> </button>
<button <button
className="transition-all hover:text-accent mb-4" className="transition-all hover:text-accent mb-4"
title="Export" title="Export"
onClick={() => { onClick={() => {
configPresetSelected.exportConfigTree(); configPresetSelected.exportConfigTree();
}} }}
disabled={disabled} disabled={disabled}
> >
<DownloadIcon/> <DownloadIcon/>
</button> </button>
</div> </div>
</div> </div>
<div className='overflow-y-auto'> <div className='overflow-y-auto'>
{rootConfig && {rootConfig &&
<ConfigEntry <ConfigEntry
@ -124,7 +124,7 @@ export const ConfigList = ({rootConfig, setTotalConfigCountDiff}: {rootConfig: C
configPresetSelected={configPresetSelected} configPresetSelected={configPresetSelected}
setConfigPresetSelected={setConfigPresetSelected} setConfigPresetSelected={setConfigPresetSelected}
editSelected={editSelected} editSelected={editSelected}
onConfigCountChanged={(val, diff) => setTotalConfigCountDiff(diff)} onConfigCountChanged={(val, diff) => setTotalConfigCountDiff(diff)}
onEditDone={(val) => { onEditDone={(val) => {
configPresetSelected.name = val; configPresetSelected.name = val;
setEditSelected(false); setEditSelected(false);
@ -146,7 +146,7 @@ const ConfigEntry = ({config, configPresetSelected, setConfigPresetSelected, edi
onConfigCountChanged: (val: number, diff: number) => void, onConfigCountChanged: (val: number, diff: number) => void,
disabled: boolean disabled: boolean
}) => { }) => {
const [configCount, setConfigCount] = useState(config.count); const [configCount, setConfigCount] = useState(config.count);
return ( return (
<div> <div>
<ConfigSelectButton <ConfigSelectButton
@ -156,7 +156,7 @@ const ConfigEntry = ({config, configPresetSelected, setConfigPresetSelected, edi
value={configCount} value={configCount}
editing={editSelected && config == configPresetSelected} editing={editSelected && config == configPresetSelected}
onClick={() => setConfigPresetSelected(config)} onClick={() => setConfigPresetSelected(config)}
onChangeDone={onEditDone} onChangeDone={onEditDone}
disabled={disabled} disabled={disabled}
/> />
<div className="ml-[20px]"> <div className="ml-[20px]">

53
src/components/PageComponents/Flasher/DeviceList.tsx

@ -9,36 +9,36 @@ import { useState } from "react";
import { FlashSettings } from "./FlashSettings"; import { FlashSettings } from "./FlashSettings";
export const DeviceList = ({rootConfig, deviceSelectedToFlash, setDeviceSelectedToFlash}: export const DeviceList = ({rootConfig, deviceSelectedToFlash, setDeviceSelectedToFlash}:
{rootConfig: ConfigPreset, deviceSelectedToFlash: FlashState[], setDeviceSelectedToFlash: React.Dispatch<React.SetStateAction<FlashState[]>>}) => { {rootConfig: ConfigPreset, deviceSelectedToFlash: FlashState[], setDeviceSelectedToFlash: React.Dispatch<React.SetStateAction<FlashState[]>>}) => {
const { setConnectDialogOpen, overallFlashingState } = useAppStore(); const { setConnectDialogOpen, overallFlashingState } = useAppStore();
const { getDevices } = useDeviceStore(); const { getDevices } = useDeviceStore();
const devices = getDevices(); const devices = getDevices();
const allConfigs = rootConfig.getAll(); const allConfigs = rootConfig.getAll();
const configQueue: string[] = []; const configQueue: string[] = [];
for(const c in allConfigs) { for(const c in allConfigs) {
const config = allConfigs[c]; const config = allConfigs[c];
for (let i = 0; i < config.count; i++) { for (let i = 0; i < config.count; i++) {
configQueue.push(config.name); configQueue.push(config.name);
} }
} }
const configMap = new Map<Device, string | undefined>(); const configMap = new Map<Device, string | undefined>();
devices.filter(d => d.flashState.state == "doFlash").forEach(d => configMap.set(d, configQueue.shift())); devices.filter(d => d.flashState.state == "doFlash").forEach(d => configMap.set(d, configQueue.shift()));
return ( return (
<div className="flex min-w-[250px] max-w-[400px] w-full rounded-md border border-dashed border-slate-200 p-3 dark:border-slate-700"> <div className="flex min-w-[250px] max-w-[400px] w-full rounded-md border border-dashed border-slate-200 p-3 dark:border-slate-700">
{devices.length ? ( {devices.length ? (
<div className="flex flex-col justify-between w-full overflow-y-auto overflow-x-clip"> <div className="flex flex-col justify-between w-full overflow-y-auto overflow-x-clip">
<Subtle>Select all devices to flash:</Subtle> <Subtle>Select all devices to flash:</Subtle>
<ul role="list" className="grow divide-y divide-gray-200"> <ul role="list" className="grow divide-y divide-gray-200">
{devices.map((device, index) => { {devices.map((device, index) => {
const state = deviceSelectedToFlash[index]; const state = deviceSelectedToFlash[index];
return (<DeviceSetupEntry return (<DeviceSetupEntry
device={device} device={device}
configName={configMap.get(device)} configName={configMap.get(device)}
toggleSelectedToFlash={() => { toggleSelectedToFlash={() => {
if(overallFlashingState.state == "busy") if(overallFlashingState.state == "busy")
return; return;
const newState: FlashState = state.state == 'doFlash' ? {progress: 0, state: 'doNotFlash'} : {progress: 1, state: 'doFlash'}; const newState: FlashState = state.state == 'doFlash' ? {progress: 0, state: 'doNotFlash'} : {progress: 1, state: 'doFlash'};
deviceSelectedToFlash[index] = newState; deviceSelectedToFlash[index] = newState;
device.setFlashState(newState); device.setFlashState(newState);
@ -77,10 +77,10 @@ export const DeviceList = ({rootConfig, deviceSelectedToFlash, setDeviceSelected
</div> </div>
) )
}; };
const DeviceSetupEntry = ({device, configName, toggleSelectedToFlash, progressText} const DeviceSetupEntry = ({device, configName, toggleSelectedToFlash, progressText}
:{device: Device, configName?: string, toggleSelectedToFlash: () => void, progressText: FlashState}) => { :{device: Device, configName?: string, toggleSelectedToFlash: () => void, progressText: FlashState}) => {
const selectedToFlash = progressText.state == "doFlash"; const selectedToFlash = progressText.state == "doFlash";
const buttonCaption = selectedToFlash ? configName ?? "Unassigned" : deviceStateToText(progressText); const buttonCaption = selectedToFlash ? configName ?? "Unassigned" : deviceStateToText(progressText);
const buttonStyle = deviceStateToStyle(progressText, configName !== undefined); const buttonStyle = deviceStateToStyle(progressText, configName !== undefined);
@ -94,10 +94,10 @@ const DeviceSetupEntry = ({device, configName, toggleSelectedToFlash, progressTe
{device.nodes.get(device.hardware.myNodeNum)?.user {device.nodes.get(device.hardware.myNodeNum)?.user
?.longName ?? "<Not flashed yet>"} ?.longName ?? "<Not flashed yet>"}
</p> </p>
</div> </div>
<div className="flex gap-2 items-center text-sm text-gray-500"> <div className="flex gap-2 items-center text-sm text-gray-500">
<Button <Button
variant={selectedToFlash && !progressText ? "default" : "outline"} variant={selectedToFlash && !progressText ? "default" : "outline"}
size="sm" size="sm"
@ -108,14 +108,14 @@ const DeviceSetupEntry = ({device, configName, toggleSelectedToFlash, progressTe
{buttonCaption} {buttonCaption}
</Button> </Button>
</div> </div>
</div> </div>
</div> </div>
</li> </li>
); );
} }
function deviceStateToText(state: FlashState) { function deviceStateToText(state: FlashState) {
switch(state.state) { switch(state.state) {
case "doNotFlash": case "doNotFlash":
@ -135,13 +135,13 @@ const DeviceSetupEntry = ({device, configName, toggleSelectedToFlash, progressTe
case "aborted": case "aborted":
return "Cancelled"; return "Cancelled";
case "failed": case "failed":
return "Failed"; return "Failed";
default: default:
return state.state; return state.state;
} }
} }
function deviceStateToStyle(state: FlashState, configAssigned: boolean): React.CSSProperties { function deviceStateToStyle(state: FlashState, configAssigned: boolean): React.CSSProperties {
switch(state.state) { switch(state.state) {
case "failed": case "failed":
return { return {
@ -158,20 +158,19 @@ const DeviceSetupEntry = ({device, configName, toggleSelectedToFlash, progressTe
color: "gray", color: "gray",
borderColor: "gray" borderColor: "gray"
}; };
case "doFlash": case "doFlash":
if(!configAssigned) { if(!configAssigned) {
return { return {
color: "var(--textPrimary)", color: "var(--textPrimary)",
borderColor: "gray", borderColor: "gray",
background: `dimgray` background: `dimgray`
}; };
} }
default: default:
return { return {
color: "var(--textPrimary)", color: "var(--textPrimary)",
borderColor: "var(--accentMuted)", borderColor: "var(--accentMuted)",
background: `linear-gradient(90deg, var(--accentMuted) ${state.progress * 100}%, transparent ${state.progress * 100}%)` background: `linear-gradient(90deg, var(--accentMuted) ${state.progress * 100}%, transparent ${state.progress * 100}%)`
}; };
} }
} }

62
src/components/PageComponents/Flasher/FlashSettings.tsx

@ -18,7 +18,7 @@ export const FlashSettings = ({deviceSelectedToFlash, setDeviceSelectedToFlash,
const { toast } = useToast(); const { toast } = useToast();
const { getDevices } = useDeviceStore(); const { getDevices } = useDeviceStore();
const devices = getDevices(); const devices = getDevices();
const cancelButtonVisible = overallFlashingState.state != "idle"; const cancelButtonVisible = overallFlashingState.state != "idle";
return (<div className="flex flex-col gap-1 rounded-md border border-dashed w-full border-slate-200 p-1 dark:border-slate-700"> return (<div className="flex flex-col gap-1 rounded-md border border-dashed w-full border-slate-200 p-1 dark:border-slate-700">
<div className="flex gap-3 w-full"> <div className="flex gap-3 w-full">
@ -27,7 +27,7 @@ export const FlashSettings = ({deviceSelectedToFlash, setDeviceSelectedToFlash,
<Switch disabled={overallFlashingState.state == "busy"} checked={fullFlash} onCheckedChange={setFullFlash}/> <Switch disabled={overallFlashingState.state == "busy"} checked={fullFlash} onCheckedChange={setFullFlash}/>
<Label>Force full wipe and reinstall</Label> <Label>Force full wipe and reinstall</Label>
</div> </div>
</div> </div>
<div className="flex gap-3"> <div className="flex gap-3">
<FirmwareSelection/> <FirmwareSelection/>
<div className="flex w-full"> <div className="flex w-full">
@ -36,8 +36,8 @@ export const FlashSettings = ({deviceSelectedToFlash, setDeviceSelectedToFlash,
disabled={totalConfigCount == 0 || overallFlashingState.state == "busy"} disabled={totalConfigCount == 0 || overallFlashingState.state == "busy"}
onClick={async () => { onClick={async () => {
if(overallFlashingState.state == "idle") { if(overallFlashingState.state == "idle") {
setOverallFlashingState({ state: "busy" }); setOverallFlashingState({ state: "busy" });
let actualFirmware = firmware; let actualFirmware = firmware;
if(actualFirmware === undefined) { if(actualFirmware === undefined) {
const list = await loadFirmwareList(); const list = await loadFirmwareList();
setFirmwareList(list.slice(0, 10)); setFirmwareList(list.slice(0, 10));
@ -54,12 +54,12 @@ export const FlashSettings = ({deviceSelectedToFlash, setDeviceSelectedToFlash,
tag: f.tag, tag: f.tag,
id: f.id, id: f.id,
isPreRelease: f.isPreRelease, isPreRelease: f.isPreRelease,
inLocalDb: f == actualFirmware ? b : f.inLocalDb inLocalDb: f == actualFirmware ? b : f.inLocalDb
}}); }});
setFirmwareList(newFirmwareList); setFirmwareList(newFirmwareList);
}); });
} }
setOverallFlashingState({state, progress}); setOverallFlashingState({state, progress});
}); });
} }
@ -68,15 +68,15 @@ export const FlashSettings = ({deviceSelectedToFlash, setDeviceSelectedToFlash,
(f)=> { (f)=> {
f.device.setFlashState(f.state); f.device.setFlashState(f.state);
deviceSelectedToFlash[devices.indexOf(f.device)] = f.state; deviceSelectedToFlash[devices.indexOf(f.device)] = f.state;
setDeviceSelectedToFlash(deviceSelectedToFlash); setDeviceSelectedToFlash(deviceSelectedToFlash);
if(f.state.state == "failed") { if(f.state.state == "failed") {
toast({ title: `❌ Error: ${f.errorReason}`}); toast({ title: `❌ Error: ${f.errorReason}`});
} }
} }
); );
}} }}
> >
{stateToText(overallFlashingState.state, overallFlashingState.progress)} {stateToText(overallFlashingState.state, overallFlashingState.progress)}
</Button>} </Button>}
</div> </div>
@ -96,7 +96,7 @@ export const FlashSettings = ({deviceSelectedToFlash, setDeviceSelectedToFlash,
) )
}; };
const FirmwareSelection = () => { const FirmwareSelection = () => {
const { firmwareRefreshing, setFirmwareRefreshing, firmwareList, setFirmwareList, selectedFirmware, setSelectedFirmware, overallFlashingState } = useAppStore(); const { firmwareRefreshing, setFirmwareRefreshing, firmwareList, setFirmwareList, selectedFirmware, setSelectedFirmware, overallFlashingState } = useAppStore();
const isBusy = firmwareRefreshing || overallFlashingState.state == "busy"; const isBusy = firmwareRefreshing || overallFlashingState.state == "busy";
const { toast } = useToast(); const { toast } = useToast();
@ -107,7 +107,7 @@ const FirmwareSelection = () => {
</SelectItem>, </SelectItem>,
<SelectSeparator/> <SelectSeparator/>
]; ];
let selection = selectedFirmware; let selection = selectedFirmware;
if(firmwareRefreshing) { if(firmwareRefreshing) {
selectItems = [ selectItems = [
<SelectItem key={0} value={"updating"}> <SelectItem key={0} value={"updating"}>
@ -124,7 +124,7 @@ const FirmwareSelection = () => {
); );
} }
else { else {
const versions = firmwareList.map((f, index) => ( const versions = firmwareList.map((f, index) => (
<SelectItem key={index} value={f.id}> <SelectItem key={index} value={f.id}>
{f.isPreRelease ? {f.isPreRelease ?
(<div className="flex gap-2 items-center">{`(${f.name})`} {f.inLocalDb ? [<ArrowDownCircleIcon size={20}/>] : []}</div>) : (<div className="flex gap-2 items-center">{`(${f.name})`} {f.inLocalDb ? [<ArrowDownCircleIcon size={20}/>] : []}</div>) :
@ -133,7 +133,7 @@ const FirmwareSelection = () => {
</SelectItem> </SelectItem>
)) ))
selectItems.push(...versions); selectItems.push(...versions);
} }
selectItems.push( selectItems.push(
<SelectItem key={100} value={"custom"}> <SelectItem key={100} value={"custom"}>
{"< Load custom firmware >"} {"< Load custom firmware >"}
@ -142,22 +142,22 @@ const FirmwareSelection = () => {
return ( return (
<div className="flex gap-1 w-full"> <div className="flex gap-1 w-full">
<Select <Select
disabled={isBusy} disabled={isBusy}
onValueChange={async (v) => { onValueChange={async (v) => {
if(v == "custom") { if(v == "custom") {
const desc = await uploadCustomFirmware(); const desc = await uploadCustomFirmware();
if(desc === undefined) if(desc === undefined)
return; return;
if(!firmwareList.find(f => f.id == desc.id)) { if(!firmwareList.find(f => f.id == desc.id)) {
const newFirmwareList: FirmwareVersion[] = firmwareList.map(f => f).concat([ desc ]); const newFirmwareList: FirmwareVersion[] = firmwareList.map(f => f).concat([ desc ]);
setFirmwareList(newFirmwareList); setFirmwareList(newFirmwareList);
} }
setSelectedFirmware(desc.id); setSelectedFirmware(desc.id);
} }
else { else {
setSelectedFirmware(v); setSelectedFirmware(v);
} }
}} }}
value={selection} // << Value of selected item value={selection} // << Value of selected item
> >
@ -175,7 +175,7 @@ const FirmwareSelection = () => {
disabled={isBusy} disabled={isBusy}
onClick={() => { onClick={() => {
setFirmwareRefreshing(true); setFirmwareRefreshing(true);
loadFirmwareList().then((list) => { loadFirmwareList().then((list) => {
setFirmwareList(list.slice(0, 10)); setFirmwareList(list.slice(0, 10));
setFirmwareRefreshing(false); setFirmwareRefreshing(false);
}).catch(() => toast({ }).catch(() => toast({
@ -186,7 +186,7 @@ const FirmwareSelection = () => {
<RefreshCwIcon size={20}/> <RefreshCwIcon size={20}/>
</Button> </Button>
</div> </div>
); );
} }
@ -200,7 +200,7 @@ export type FirmwareVersion = {
interface FirmwareGithubRelease { interface FirmwareGithubRelease {
name: string, name: string,
tag_name: string, tag_name: string,
prerelease: boolean, prerelease: boolean,
assets: { assets: {
name: string, name: string,
@ -273,9 +273,9 @@ export const deviceModels: DeviceModel[] = [
}, },
] ]
const DeviceModelSelection = () => { const DeviceModelSelection = () => {
const { selectedDeviceModel, setSelectedDeviceModel, overallFlashingState } = useAppStore(); const { selectedDeviceModel, setSelectedDeviceModel, overallFlashingState } = useAppStore();
let selectItems = [ let selectItems = [
<SelectItem key={"auto"} value={"auto"}> <SelectItem key={"auto"} value={"auto"}>
{"Auto-detect device model"} {"Auto-detect device model"}
@ -291,8 +291,8 @@ const DeviceModelSelection = () => {
return ( return (
<div className="flex gap-1 w-full"> <div className="flex gap-1 w-full">
<Select <Select
onValueChange={setSelectedDeviceModel} onValueChange={setSelectedDeviceModel}
value={selectedDeviceModel} value={selectedDeviceModel}
disabled={overallFlashingState.state == "busy"} disabled={overallFlashingState.state == "busy"}
> >
@ -304,7 +304,7 @@ const DeviceModelSelection = () => {
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
); );
} }
@ -315,7 +315,7 @@ function stateToText(state: OverallFlashingState, progress?: number) {
case "downloading": case "downloading":
return progress ? `Downloading firmware... (${(progress * 100).toFixed(1)} %)` : "Downloading firmware..."; return progress ? `Downloading firmware... (${(progress * 100).toFixed(1)} %)` : "Downloading firmware...";
case "busy": case "busy":
return "In Progress..."; return "In Progress...";
case "waiting": case "waiting":
return "Continue"; return "Continue";
default: default:
@ -330,15 +330,15 @@ async function loadFirmwareList() : Promise<FirmwareVersion[]> {
const id = r.assets.find(a => a.name.startsWith("firmware"))!.id; const id = r.assets.find(a => a.name.startsWith("firmware"))!.id;
if(id === undefined) if(id === undefined)
return undefined; return undefined;
const tag = r.tag_name.substring(1); // remove leading "v" const tag = r.tag_name.substring(1); // remove leading "v"
return { return {
name: r.name.replace("Meshtastic Firmware ", ""), name: r.name.replace("Meshtastic Firmware ", ""),
tag: tag, tag: tag,
id: id, id: id,
isPreRelease: r.prerelease, isPreRelease: r.prerelease,
inLocalDb: await isStoredInDb(tag) inLocalDb: await isStoredInDb(tag)
}; };
})); }));
return firmwareDescriptions.filter(r => r !== undefined) as FirmwareVersion[]; return firmwareDescriptions.filter(r => r !== undefined) as FirmwareVersion[];
} }

6
src/components/PageComponents/ModuleConfig/Audio.tsx

@ -11,13 +11,13 @@ export const Audio = (): JSX.Element => {
return config.overrideValues![name] ?? false; return config.overrideValues![name] ?? false;
}, },
setEnabled(name, value) { setEnabled(name, value) {
config.overrideValues![name] = value; config.overrideValues![name] = value;
}, },
} : undefined; } : undefined;
const isPresetConfig = !("id" in config); const isPresetConfig = !("id" in config);
const setConfig: (data: AudioValidation) => void = const setConfig: (data: AudioValidation) => void =
isPresetConfig ? (data) => { isPresetConfig ? (data) => {
config.moduleConfig.audio = new Protobuf.ModuleConfig_AudioConfig(data); config.moduleConfig.audio = new Protobuf.ModuleConfig_AudioConfig(data);
(config as ConfigPreset).saveConfigTree(); (config as ConfigPreset).saveConfigTree();
} }
: (data) => { : (data) => {
@ -30,7 +30,7 @@ export const Audio = (): JSX.Element => {
}) })
); );
} }
const onSubmit = setConfig; const onSubmit = setConfig;
return ( return (

6
src/components/PageComponents/ModuleConfig/CannedMessage.tsx

@ -11,13 +11,13 @@ export const CannedMessage = (): JSX.Element => {
return config.overrideValues![name] ?? false; return config.overrideValues![name] ?? false;
}, },
setEnabled(name, value) { setEnabled(name, value) {
config.overrideValues![name] = value; config.overrideValues![name] = value;
}, },
} : undefined; } : undefined;
const isPresetConfig = !("id" in config); const isPresetConfig = !("id" in config);
const setConfig: (data: CannedMessageValidation) => void = const setConfig: (data: CannedMessageValidation) => void =
isPresetConfig ? (data) => { isPresetConfig ? (data) => {
config.moduleConfig.cannedMessage = new Protobuf.ModuleConfig_CannedMessageConfig(data); config.moduleConfig.cannedMessage = new Protobuf.ModuleConfig_CannedMessageConfig(data);
(config as ConfigPreset).saveConfigTree(); (config as ConfigPreset).saveConfigTree();
} }
: (data) => { : (data) => {
@ -30,7 +30,7 @@ export const CannedMessage = (): JSX.Element => {
}) })
); );
} }
const onSubmit = setConfig; const onSubmit = setConfig;
return ( return (

6
src/components/PageComponents/ModuleConfig/ExternalNotification.tsx

@ -11,13 +11,13 @@ export const ExternalNotification = (): JSX.Element => {
return config.overrideValues![name] ?? false; return config.overrideValues![name] ?? false;
}, },
setEnabled(name, value) { setEnabled(name, value) {
config.overrideValues![name] = value; config.overrideValues![name] = value;
}, },
} : undefined; } : undefined;
const isPresetConfig = !("id" in config); const isPresetConfig = !("id" in config);
const setConfig: (data: ExternalNotificationValidation) => void = const setConfig: (data: ExternalNotificationValidation) => void =
isPresetConfig ? (data) => { isPresetConfig ? (data) => {
config.moduleConfig.externalNotification = new Protobuf.ModuleConfig_ExternalNotificationConfig(data); config.moduleConfig.externalNotification = new Protobuf.ModuleConfig_ExternalNotificationConfig(data);
(config as ConfigPreset).saveConfigTree(); (config as ConfigPreset).saveConfigTree();
} }
: (data) => { : (data) => {
@ -30,7 +30,7 @@ export const ExternalNotification = (): JSX.Element => {
}) })
); );
} }
const onSubmit = setConfig; const onSubmit = setConfig;
return ( return (

6
src/components/PageComponents/ModuleConfig/RangeTest.tsx

@ -11,13 +11,13 @@ export const RangeTest = (): JSX.Element => {
return config.overrideValues![name] ?? false; return config.overrideValues![name] ?? false;
}, },
setEnabled(name, value) { setEnabled(name, value) {
config.overrideValues![name] = value; config.overrideValues![name] = value;
}, },
} : undefined; } : undefined;
const isPresetConfig = !("id" in config); const isPresetConfig = !("id" in config);
const setConfig: (data: RangeTestValidation) => void = const setConfig: (data: RangeTestValidation) => void =
isPresetConfig ? (data) => { isPresetConfig ? (data) => {
config.moduleConfig.rangeTest = new Protobuf.ModuleConfig_RangeTestConfig(data); config.moduleConfig.rangeTest = new Protobuf.ModuleConfig_RangeTestConfig(data);
(config as ConfigPreset).saveConfigTree(); (config as ConfigPreset).saveConfigTree();
} }
: (data) => { : (data) => {
@ -30,7 +30,7 @@ export const RangeTest = (): JSX.Element => {
}) })
); );
} }
const onSubmit = setConfig; const onSubmit = setConfig;
return ( return (

6
src/components/PageComponents/ModuleConfig/Serial.tsx

@ -11,13 +11,13 @@ export const Serial = (): JSX.Element => {
return config.overrideValues![name] ?? false; return config.overrideValues![name] ?? false;
}, },
setEnabled(name, value) { setEnabled(name, value) {
config.overrideValues![name] = value; config.overrideValues![name] = value;
}, },
} : undefined; } : undefined;
const isPresetConfig = !("id" in config); const isPresetConfig = !("id" in config);
const setConfig: (data: SerialValidation) => void = const setConfig: (data: SerialValidation) => void =
isPresetConfig ? (data) => { isPresetConfig ? (data) => {
config.moduleConfig.serial = new Protobuf.ModuleConfig_SerialConfig(data); config.moduleConfig.serial = new Protobuf.ModuleConfig_SerialConfig(data);
(config as ConfigPreset).saveConfigTree(); (config as ConfigPreset).saveConfigTree();
} }
: (data) => { : (data) => {
@ -30,7 +30,7 @@ export const Serial = (): JSX.Element => {
}) })
); );
} }
const onSubmit = setConfig; const onSubmit = setConfig;
return ( return (

6
src/components/PageComponents/ModuleConfig/StoreForward.tsx

@ -11,13 +11,13 @@ export const StoreForward = (): JSX.Element => {
return config.overrideValues![name] ?? false; return config.overrideValues![name] ?? false;
}, },
setEnabled(name, value) { setEnabled(name, value) {
config.overrideValues![name] = value; config.overrideValues![name] = value;
}, },
} : undefined; } : undefined;
const isPresetConfig = !("id" in config); const isPresetConfig = !("id" in config);
const setConfig: (data: StoreForwardValidation) => void = const setConfig: (data: StoreForwardValidation) => void =
isPresetConfig ? (data) => { isPresetConfig ? (data) => {
config.moduleConfig.storeForward = new Protobuf.ModuleConfig_StoreForwardConfig(data); config.moduleConfig.storeForward = new Protobuf.ModuleConfig_StoreForwardConfig(data);
(config as ConfigPreset).saveConfigTree(); (config as ConfigPreset).saveConfigTree();
} }
: (data) => { : (data) => {
@ -30,7 +30,7 @@ export const StoreForward = (): JSX.Element => {
}) })
); );
} }
const onSubmit = setConfig; const onSubmit = setConfig;
return ( return (
<DynamicForm<StoreForwardValidation> <DynamicForm<StoreForwardValidation>

6
src/components/PageComponents/ModuleConfig/Telemetry.tsx

@ -11,13 +11,13 @@ export const Telemetry = (): JSX.Element => {
return config.overrideValues![name] ?? false; return config.overrideValues![name] ?? false;
}, },
setEnabled(name, value) { setEnabled(name, value) {
config.overrideValues![name] = value; config.overrideValues![name] = value;
}, },
} : undefined; } : undefined;
const isPresetConfig = !("id" in config); const isPresetConfig = !("id" in config);
const setConfig: (data: TelemetryValidation) => void = const setConfig: (data: TelemetryValidation) => void =
isPresetConfig ? (data) => { isPresetConfig ? (data) => {
config.moduleConfig.telemetry = new Protobuf.ModuleConfig_TelemetryConfig(data); config.moduleConfig.telemetry = new Protobuf.ModuleConfig_TelemetryConfig(data);
(config as ConfigPreset).saveConfigTree(); (config as ConfigPreset).saveConfigTree();
} }
: (data) => { : (data) => {
@ -30,7 +30,7 @@ export const Telemetry = (): JSX.Element => {
}) })
); );
} }
const onSubmit = setConfig; const onSubmit = setConfig;
return ( return (

2
src/components/UI/ConfigSelectButton.tsx

@ -30,7 +30,7 @@ export const ConfigSelectButton = ({
size="sm" size="sm"
className="w-full justify-between gap-2 my-[2px]" className="w-full justify-between gap-2 my-[2px]"
> >
{editing ? {editing ?
<Input <Input
autoFocus autoFocus
onFocus={(event) => event.target.select()} onFocus={(event) => event.target.select()}

28
src/core/flashing/FirmwareDb.ts

@ -5,10 +5,10 @@ function openDb() {
const db = indexedDB.open("firmwares"); const db = indexedDB.open("firmwares");
db.onsuccess = () => { db.onsuccess = () => {
resolve(db.result); resolve(db.result);
}; };
db.onupgradeneeded = (ev) => { db.onupgradeneeded = (ev) => {
const objStore = db.result.createObjectStore("files"); const objStore = db.result.createObjectStore("files");
objStore.transaction.oncomplete = () => resolve(db.result); objStore.transaction.oncomplete = () => resolve(db.result);
}; };
}); });
@ -18,15 +18,15 @@ export async function isStoredInDb(firmwareTag: string): Promise<boolean> {
const dbs = await indexedDB.databases(); const dbs = await indexedDB.databases();
if(dbs.find(db => db.name == "firmwares") === undefined) if(dbs.find(db => db.name == "firmwares") === undefined)
return false; return false;
return new Promise<boolean>((resolve, reject) => { return new Promise<boolean>((resolve, reject) => {
const db = indexedDB.open("firmwares"); const db = indexedDB.open("firmwares");
db.onsuccess = () => { db.onsuccess = () => {
if(!db.result.objectStoreNames.contains("files")) if(!db.result.objectStoreNames.contains("files"))
resolve(false); resolve(false);
const objStore = db.result.transaction("files", "readonly").objectStore("files"); const objStore = db.result.transaction("files", "readonly").objectStore("files");
const transaction = objStore.getKey(firmwareTag); const transaction = objStore.getKey(firmwareTag);
transaction.onsuccess = () => resolve(transaction.result !== undefined); transaction.onsuccess = () => resolve(transaction.result !== undefined);
transaction.onerror = () => resolve(false); transaction.onerror = () => resolve(false);
} }
}); });
} }
@ -41,31 +41,31 @@ export async function storeInDb(firmware: FirmwareVersion, file: ArrayBuffer) {
resolve(); resolve();
} }
fileStore.transaction.onerror = reject; fileStore.transaction.onerror = reject;
}); });
} }
export async function loadFromDb(firmware: FirmwareVersion) { export async function loadFromDb(firmware: FirmwareVersion) {
const db = await openDb(); const db = await openDb();
return new Promise<ArrayBuffer>((resolve, reject) => { return new Promise<ArrayBuffer>((resolve, reject) => {
const objStore = db.transaction("files", "readonly").objectStore("files"); const objStore = db.transaction("files", "readonly").objectStore("files");
const transaction = objStore.get(firmware.tag); const transaction = objStore.get(firmware.tag);
transaction.onsuccess = () => { transaction.onsuccess = () => {
resolve(transaction.result as ArrayBuffer); resolve(transaction.result as ArrayBuffer);
}; };
transaction.onerror = reject; transaction.onerror = reject;
}); });
} }
export async function deleteFromDb(firmware: FirmwareVersion) { export async function deleteFromDb(firmware: FirmwareVersion) {
const db = await openDb(); const db = await openDb();
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
if(!db.objectStoreNames.contains("files")) { if(!db.objectStoreNames.contains("files")) {
resolve(); resolve();
return; return;
} }
const objStore = db.transaction("files", "readonly").objectStore("files"); const objStore = db.transaction("files", "readonly").objectStore("files");
const transaction = objStore.delete(firmware.tag); const transaction = objStore.delete(firmware.tag);
transaction.onsuccess = () => resolve; transaction.onsuccess = () => resolve;
transaction.onerror = reject; transaction.onerror = reject;
}); });
} }

84
src/core/flashing/Flasher.ts

@ -39,12 +39,12 @@ export async function setup(configs: ConfigPreset[], deviceModelName: string, fi
const config = configs[c]; const config = configs[c];
for (let i = 0; i < config.count; i++) { for (let i = 0; i < config.count; i++) {
configQueue.push({config: config.config, moduleConfig: config.moduleConfig}); configQueue.push({config: config.config, moduleConfig: config.moduleConfig});
} }
} }
} }
export async function nextBatch(devices: Device[], flashStates: FlashState[], deviceCallback: (flashState: FlashOperation) => void) { export async function nextBatch(devices: Device[], flashStates: FlashState[], deviceCallback: (flashState: FlashOperation) => void) {
callback("busy"); callback("busy");
devices = devices.filter((d, i) => flashStates[i].state == "doFlash"); devices = devices.filter((d, i) => flashStates[i].state == "doFlash");
flashStates = flashStates.filter(f => f.state == "doFlash"); flashStates = flashStates.filter(f => f.state == "doFlash");
if(devices.length > configQueue.length) { if(devices.length > configQueue.length) {
@ -55,7 +55,7 @@ export async function nextBatch(devices: Device[], flashStates: FlashState[], de
operations.forEach((o, i) => o.state = flashStates[i]); operations.forEach((o, i) => o.state = flashStates[i]);
configQueue = configQueue.slice(operations.length) configQueue = configQueue.slice(operations.length)
console.log(`New config queue count: ${configQueue.length}`); console.log(`New config queue count: ${configQueue.length}`);
Promise.allSettled(operations.map(op => op.flashAndConfigDevice())).then(p => handleFlashingDone()); Promise.allSettled(operations.map(op => op.flashAndConfigDevice())).then(p => handleFlashingDone());
} }
@ -80,7 +80,7 @@ export async function uploadCustomFirmware() {
if(fileHandle == undefined) if(fileHandle == undefined)
return undefined; return undefined;
try { try {
const file = await fileHandle.getFile(); const file = await fileHandle.getFile();
const content = await file.arrayBuffer(); const content = await file.arrayBuffer();
const firmwareDesc: FirmwareVersion = { const firmwareDesc: FirmwareVersion = {
id: "custom_" + file.name, id: "custom_" + file.name,
@ -93,11 +93,11 @@ export async function uploadCustomFirmware() {
return firmwareDesc; return firmwareDesc;
} catch { } catch {
console.error("Insufficient permission to access file."); console.error("Insufficient permission to access file.");
} }
return undefined; return undefined;
} }
async function getZipFile() { async function getZipFile() {
if(firmwareToUse.inLocalDb) { if(firmwareToUse.inLocalDb) {
const storedZip = await loadFromDb(firmwareToUse); const storedZip = await loadFromDb(firmwareToUse);
if(storedZip !== undefined) if(storedZip !== undefined)
@ -107,10 +107,10 @@ async function getZipFile() {
const zip = await fetch(`/firmware/${firmwareToUse.id}`); const zip = await fetch(`/firmware/${firmwareToUse.id}`);
const zipClone = zip.clone(); const zipClone = zip.clone();
const contentLength = zip.headers.get("content-length"); const contentLength = zip.headers.get("content-length");
const totalLength = contentLength ? parseInt(contentLength) : undefined; const totalLength = contentLength ? parseInt(contentLength) : undefined;
const reader = zip.body!.getReader(); const reader = zip.body!.getReader();
let bytesLoaded = 0; let bytesLoaded = 0;
while (true) { while (true) {
const { done, value } = await reader.read(); const { done, value } = await reader.read();
if (done) if (done)
@ -124,22 +124,22 @@ async function getZipFile() {
return content; return content;
} }
async function loadFirmware() { async function loadFirmware() {
console.log("Loading firmware"); console.log("Loading firmware");
const zip = await getZipFile(); const zip = await getZipFile();
zipFile = await JSZip.loadAsync(zip); zipFile = await JSZip.loadAsync(zip);
} }
async function getSection(name: string): Promise<Uint8Array | undefined> { async function getSection(name: string): Promise<Uint8Array | undefined> {
if(!(name in dataSections)) { if(!(name in dataSections)) {
const dataSection = await zipFile.file(name)?.async("uint8array"); const dataSection = await zipFile.file(name)?.async("uint8array");
if(dataSection === undefined) { if(dataSection === undefined) {
console.warn(`Firmware file ${name} not found.`); console.warn(`Firmware file ${name} not found.`);
return undefined; return undefined;
} }
dataSections[name] = dataSection; dataSections[name] = dataSection;
} }
return dataSections[name]; return dataSections[name];
} }
@ -172,14 +172,14 @@ function autoDetectDeviceModel(port: SerialPort) {
export class FlashOperation { export class FlashOperation {
public state: FlashState = { progress: 0, state: "idle" }; public state: FlashState = { progress: 0, state: "idle" };
public errorReason?: string; public errorReason?: string;
private loader?: esptooljs.ESPLoader; private loader?: esptooljs.ESPLoader;
private isCancelled: boolean; private isCancelled: boolean;
public constructor(public device: Device, public config: Protobuf.LocalConfig, public moduleConfig: Protobuf.LocalModuleConfig, public callback: (operation: FlashOperation) => void) { public constructor(public device: Device, public config: Protobuf.LocalConfig, public moduleConfig: Protobuf.LocalModuleConfig, public callback: (operation: FlashOperation) => void) {
} }
public setState(state: DeviceFlashingState, progress = 0, errorReason : string | undefined = undefined) { public setState(state: DeviceFlashingState, progress = 0, errorReason : string | undefined = undefined) {
@ -197,18 +197,18 @@ export class FlashOperation {
await this.flash(); await this.flash();
await this.setConfig(); await this.setConfig();
this.setState("done"); this.setState("done");
} }
catch(e) { catch(e) {
this.setState("failed", 0, e as string); this.setState("failed", 0, e as string);
throw e; throw e;
} }
} }
private async flash() { private async flash() {
const installedVersion = this.device.hardware.firmwareVersion; const installedVersion = this.device.hardware.firmwareVersion;
console.log(`Installed firmware verson: ${installedVersion}`); console.log(`Installed firmware verson: ${installedVersion}`);
const update = !fullFlash && this.device.nodes.get(this.device.hardware.myNodeNum) !== undefined ; const update = !fullFlash && this.device.nodes.get(this.device.hardware.myNodeNum) !== undefined ;
if(update && installedVersion == firmwareToUse.tag) if(update && installedVersion == firmwareToUse.tag)
return; return;
@ -224,28 +224,28 @@ export class FlashOperation {
// ----------- // -----------
try { try {
const transport = new esptooljs.Transport(port); const transport = new esptooljs.Transport(port);
this.loader = new esptooljs.ESPLoader(transport, 115200); this.loader = new esptooljs.ESPLoader(transport, 115200);
const loader = this.loader; const loader = this.loader;
this.setState("preparing"); this.setState("preparing");
await loader.main_fn(); await loader.main_fn();
if(sections.length > 1) { if(sections.length > 1) {
this.setState("erasing"); this.setState("erasing");
await loader.erase_flash(); await loader.erase_flash();
} }
const totalLength = sections.reduce<number>((p, c) => p + c.data.byteLength, 0); const totalLength = sections.reduce<number>((p, c) => p + c.data.byteLength, 0);
let bytesFlashed = 0; let bytesFlashed = 0;
let lastIndex = 0; let lastIndex = 0;
const files = await Promise.all(sections.map(async s => { const files = await Promise.all(sections.map(async s => {
const fileReader = new FileReader(); const fileReader = new FileReader();
const blob = new Blob([s.data]); const blob = new Blob([s.data]);
const content = await new Promise<string>((resolve, reject) => { const content = await new Promise<string>((resolve, reject) => {
fileReader.onloadend = e => resolve(fileReader.result as string); fileReader.onloadend = e => resolve(fileReader.result as string);
fileReader.onerror = e => reject(fileReader.result as string); fileReader.onerror = e => reject(fileReader.result as string);
fileReader.readAsBinaryString(blob); fileReader.readAsBinaryString(blob);
}); });
return { data: content, address: s.offset }; return { data: content, address: s.offset };
})); }));
@ -261,30 +261,30 @@ export class FlashOperation {
const bytesThisSegment = (written / total) * sections[index].data.byteLength; const bytesThisSegment = (written / total) * sections[index].data.byteLength;
console.log(`FLASHING PROGRESS ${bytesFlashed + written} / ${totalLength} ... ${bytesFlashed} | ${written} | ${total} | ${index}`); console.log(`FLASHING PROGRESS ${bytesFlashed + written} / ${totalLength} ... ${bytesFlashed} | ${written} | ${total} | ${index}`);
this.setState("flashing", (bytesFlashed + bytesThisSegment) / totalLength); this.setState("flashing", (bytesFlashed + bytesThisSegment) / totalLength);
}); });
} }
catch (e) { catch (e) {
throw e; throw e;
} }
finally { finally {
await this.loader?.flash_finish(); await this.loader?.flash_finish();
} }
this.setState("config"); this.setState("config");
await port!.setSignals({requestToSend: true}); await port!.setSignals({requestToSend: true});
await new Promise(r => setTimeout(r, 100)); await new Promise(r => setTimeout(r, 100));
await port!.setSignals({requestToSend: false}); await port!.setSignals({requestToSend: false});
await port!.close(); await port!.close();
const connection = (this.device.connection! as ISerialConnection); const connection = (this.device.connection! as ISerialConnection);
//@ts-ignore //@ts-ignore
connection.preventLock = false; connection.preventLock = false;
debugger; debugger;
await connection.connect({ await connection.connect({
port, port,
baudRate: undefined, baudRate: undefined,
concurrentLogOutput: true concurrentLogOutput: true
}); });
await new Promise(r => setTimeout(r, 5000)); await new Promise(r => setTimeout(r, 5000));
} }
private async setConfig() { private async setConfig() {
@ -292,14 +292,14 @@ export class FlashOperation {
return; return;
this.setState("config"); this.setState("config");
const connection = (this.device.connection! as ISerialConnection); const connection = (this.device.connection! as ISerialConnection);
await connection.setConfig(new Protobuf.Config({ payloadVariant: { case: "device", value: this.config.device! } })); await connection.setConfig(new Protobuf.Config({ payloadVariant: { case: "device", value: this.config.device! } }));
await connection.setConfig(new Protobuf.Config({ payloadVariant: { case: "position", value: this.config.position! } })); await connection.setConfig(new Protobuf.Config({ payloadVariant: { case: "position", value: this.config.position! } }));
await connection.setConfig(new Protobuf.Config({ payloadVariant: { case: "power", value: this.config.power! } })); await connection.setConfig(new Protobuf.Config({ payloadVariant: { case: "power", value: this.config.power! } }));
await connection.setConfig(new Protobuf.Config({ payloadVariant: { case: "network", value: this.config.network! } })); await connection.setConfig(new Protobuf.Config({ payloadVariant: { case: "network", value: this.config.network! } }));
await connection.setConfig(new Protobuf.Config({ payloadVariant: { case: "display", value: this.config.display! } })); await connection.setConfig(new Protobuf.Config({ payloadVariant: { case: "display", value: this.config.display! } }));
await connection.setConfig(new Protobuf.Config({ payloadVariant: { case: "lora", value: this.config.lora! } })); await connection.setConfig(new Protobuf.Config({ payloadVariant: { case: "lora", value: this.config.lora! } }));
await connection.setConfig(new Protobuf.Config({ payloadVariant: { case: "bluetooth", value: this.config.bluetooth! } })); await connection.setConfig(new Protobuf.Config({ payloadVariant: { case: "bluetooth", value: this.config.bluetooth! } }));
await connection.setModuleConfig(new Protobuf.ModuleConfig({ payloadVariant: { case: "mqtt", value: this.moduleConfig.mqtt! } })); await connection.setModuleConfig(new Protobuf.ModuleConfig({ payloadVariant: { case: "mqtt", value: this.moduleConfig.mqtt! } }));
await connection.setModuleConfig(new Protobuf.ModuleConfig({ payloadVariant: { case: "serial", value: this.moduleConfig.serial! } })); await connection.setModuleConfig(new Protobuf.ModuleConfig({ payloadVariant: { case: "serial", value: this.moduleConfig.serial! } }));
@ -308,7 +308,7 @@ export class FlashOperation {
await connection.setModuleConfig(new Protobuf.ModuleConfig({ payloadVariant: { case: "telemetry", value: this.moduleConfig.telemetry! } })), await connection.setModuleConfig(new Protobuf.ModuleConfig({ payloadVariant: { case: "telemetry", value: this.moduleConfig.telemetry! } })),
await connection.setModuleConfig(new Protobuf.ModuleConfig({ payloadVariant: { case: "cannedMessage", value: this.moduleConfig.cannedMessage! } })), await connection.setModuleConfig(new Protobuf.ModuleConfig({ payloadVariant: { case: "cannedMessage", value: this.moduleConfig.cannedMessage! } })),
await connection.setModuleConfig(new Protobuf.ModuleConfig({ payloadVariant: { case: "audio", value: this.moduleConfig.audio! } })); await connection.setModuleConfig(new Protobuf.ModuleConfig({ payloadVariant: { case: "audio", value: this.moduleConfig.audio! } }));
// We won't get an answer if serial output has been disabled in the new config // We won't get an answer if serial output has been disabled in the new config
if(!this.config.device!.serialEnabled) if(!this.config.device!.serialEnabled)
return; return;

4
src/core/stores/deviceStore.ts

@ -131,7 +131,7 @@ export const useDeviceStore = create<DeviceState>((set, get) => ({
broadcast: new Map() broadcast: new Map()
}, },
connection: undefined, connection: undefined,
flashState: { state: 'doFlash', progress: 0 }, flashState: { state: 'doFlash', progress: 0 },
activePage: "messages", activePage: "messages",
activePeer: 0, activePeer: 0,
waypoints: [], waypoints: [],
@ -346,7 +346,7 @@ export const useDeviceStore = create<DeviceState>((set, get) => ({
setFlashState: (state) => { setFlashState: (state) => {
set( set(
produce<DeviceState>((draft) => { produce<DeviceState>((draft) => {
const device = draft.devices.get(id); const device = draft.devices.get(id);
if (device) { if (device) {
device.flashState = state; device.flashState = state;
} }

5
src/pages/Config/ConfigTabs.tsx

@ -16,7 +16,7 @@ import {
import { DeviceConfig } from "./DeviceConfig"; import { DeviceConfig } from "./DeviceConfig";
import { ModuleConfig } from "./ModuleConfig"; import { ModuleConfig } from "./ModuleConfig";
export const ConfigTabs = (): JSX.Element => { export const ConfigTabs = (): JSX.Element => {
const tabs = [ const tabs = [
{ {
label: "Device", label: "Device",
@ -28,7 +28,7 @@ export const ConfigTabs = (): JSX.Element => {
element: ModuleConfig element: ModuleConfig
} }
]; ];
return ( return (
<Tabs defaultValue="Device"> <Tabs defaultValue="Device">
<TabsList> <TabsList>
@ -49,4 +49,3 @@ export const ConfigTabs = (): JSX.Element => {
</Tabs> </Tabs>
); );
}; };

2
src/pages/Config/ModuleConfig.tsx

@ -14,7 +14,7 @@ import {
TabsTrigger TabsTrigger
} from "@components/UI/Tabs.js"; } from "@components/UI/Tabs.js";
export const ModuleConfig = (): JSX.Element => { export const ModuleConfig = (): JSX.Element => {
const tabs = [ const tabs = [
{ {
label: "MQTT", label: "MQTT",

2
tsconfig.json

@ -1,6 +1,6 @@
{ {
"include": ["src", "types"], "include": ["src", "types"],
"compilerOptions": { "compilerOptions": {
"target": "ESNext", "target": "ESNext",
"useDefineForClassFields": true, "useDefineForClassFields": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"], "lib": ["DOM", "DOM.Iterable", "ESNext"],

Loading…
Cancel
Save