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
3. Start the server by running
pnpm dev
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 = () => {
let { configPresetRoot, configPresetSelected, overallFlashingState } = useAppStore();
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 [deviceSelectedToFlash, setDeviceSelectedToFlash] = useState(new Array<FlashState>(100).fill({progress: 1, state: 'doFlash'}));
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 [deviceSelectedToFlash, setDeviceSelectedToFlash] = useState(new Array<FlashState>(100).fill({progress: 1, state: 'doFlash'}));
return (
<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) => {
const fullFieldName = `${fieldGroup.label}_${field.name}`
const [ enableSwitchState, setEnableSwitchState ] = useState(enableSwitch?.getEnabled(fullFieldName));
const [ enableSwitchState, setEnableSwitchState ] = useState(enableSwitch?.getEnabled(fullFieldName));
return (
<FieldWrapper label={field.label} description={field.description} enableSwitchEnabled={enableSwitchState} onEnableSwitchChanged={(value) => {
enableSwitch?.setEnabled(fullFieldName, value);

4
src/components/Form/FormWrapper.tsx

@ -26,12 +26,12 @@ export const FieldWrapper = ({
{enableSwitchEnabled !== undefined && (
<div className="mt-4 space-y-4">
<Switch
defaultChecked={enableSwitchEnabled}
defaultChecked={enableSwitchEnabled}
onCheckedChange={onEnableSwitchChanged}
/>
</div>
)}
</div>
</div>
<div className="sm:col-span-2">
<div className="max-w-lg">
<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;
},
setEnabled(name, value) {
config.overrideValues![name] = value;
config.overrideValues![name] = value;
},
} : undefined;
const isPresetConfig = !("id" in config);
const { setWorkingConfig } = !isPresetConfig ? useDevice() : { setWorkingConfig: undefined };
const setConfig: (data: BluetoothValidation) => void =
isPresetConfig ? (data) => {
config.config.bluetooth = new Protobuf.Config_BluetoothConfig(data);
config.config.bluetooth = new Protobuf.Config_BluetoothConfig(data);
(config as ConfigPreset).saveConfigTree();
}
: (data) => {
@ -36,9 +36,9 @@ export const Bluetooth = (): JSX.Element => {
}
})
);
}
}
const onSubmit = setConfig;
const onSubmit = setConfig;
return (
<DynamicForm<BluetoothValidation>

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -9,36 +9,36 @@ import { useState } from "react";
import { FlashSettings } from "./FlashSettings";
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 { getDevices } = useDeviceStore();
const devices = getDevices();
const { getDevices } = useDeviceStore();
const devices = getDevices();
const allConfigs = rootConfig.getAll();
const configQueue: string[] = [];
for(const c in allConfigs) {
const config = allConfigs[c];
for (let i = 0; i < config.count; i++) {
configQueue.push(config.name);
}
}
}
}
const configMap = new Map<Device, string | undefined>();
devices.filter(d => d.flashState.state == "doFlash").forEach(d => configMap.set(d, configQueue.shift()));
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">
{devices.length ? (
<div className="flex flex-col justify-between w-full overflow-y-auto overflow-x-clip">
<Subtle>Select all devices to flash:</Subtle>
<ul role="list" className="grow divide-y divide-gray-200">
{devices.map((device, index) => {
{devices.map((device, index) => {
const state = deviceSelectedToFlash[index];
return (<DeviceSetupEntry
device={device}
configName={configMap.get(device)}
toggleSelectedToFlash={() => {
configName={configMap.get(device)}
toggleSelectedToFlash={() => {
if(overallFlashingState.state == "busy")
return;
return;
const newState: FlashState = state.state == 'doFlash' ? {progress: 0, state: 'doNotFlash'} : {progress: 1, state: 'doFlash'};
deviceSelectedToFlash[index] = newState;
device.setFlashState(newState);
@ -77,10 +77,10 @@ export const DeviceList = ({rootConfig, deviceSelectedToFlash, setDeviceSelected
</div>
)
};
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 buttonCaption = selectedToFlash ? configName ?? "Unassigned" : deviceStateToText(progressText);
const buttonStyle = deviceStateToStyle(progressText, configName !== undefined);
@ -94,10 +94,10 @@ const DeviceSetupEntry = ({device, configName, toggleSelectedToFlash, progressTe
{device.nodes.get(device.hardware.myNodeNum)?.user
?.longName ?? "<Not flashed yet>"}
</p>
</div>
</div>
<div className="flex gap-2 items-center text-sm text-gray-500">
<Button
variant={selectedToFlash && !progressText ? "default" : "outline"}
size="sm"
@ -108,14 +108,14 @@ const DeviceSetupEntry = ({device, configName, toggleSelectedToFlash, progressTe
{buttonCaption}
</Button>
</div>
</div>
</div>
</div>
</li>
);
}
function deviceStateToText(state: FlashState) {
switch(state.state) {
case "doNotFlash":
@ -135,13 +135,13 @@ const DeviceSetupEntry = ({device, configName, toggleSelectedToFlash, progressTe
case "aborted":
return "Cancelled";
case "failed":
return "Failed";
return "Failed";
default:
return state.state;
}
}
function deviceStateToStyle(state: FlashState, configAssigned: boolean): React.CSSProperties {
function deviceStateToStyle(state: FlashState, configAssigned: boolean): React.CSSProperties {
switch(state.state) {
case "failed":
return {
@ -158,20 +158,19 @@ const DeviceSetupEntry = ({device, configName, toggleSelectedToFlash, progressTe
color: "gray",
borderColor: "gray"
};
case "doFlash":
case "doFlash":
if(!configAssigned) {
return {
color: "var(--textPrimary)",
borderColor: "gray",
background: `dimgray`
};
};
}
default:
return {
color: "var(--textPrimary)",
borderColor: "var(--accentMuted)",
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 { getDevices } = useDeviceStore();
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">
<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}/>
<Label>Force full wipe and reinstall</Label>
</div>
</div>
</div>
<div className="flex gap-3">
<FirmwareSelection/>
<div className="flex w-full">
@ -36,8 +36,8 @@ export const FlashSettings = ({deviceSelectedToFlash, setDeviceSelectedToFlash,
disabled={totalConfigCount == 0 || overallFlashingState.state == "busy"}
onClick={async () => {
if(overallFlashingState.state == "idle") {
setOverallFlashingState({ state: "busy" });
let actualFirmware = firmware;
setOverallFlashingState({ state: "busy" });
let actualFirmware = firmware;
if(actualFirmware === undefined) {
const list = await loadFirmwareList();
setFirmwareList(list.slice(0, 10));
@ -54,12 +54,12 @@ export const FlashSettings = ({deviceSelectedToFlash, setDeviceSelectedToFlash,
tag: f.tag,
id: f.id,
isPreRelease: f.isPreRelease,
inLocalDb: f == actualFirmware ? b : f.inLocalDb
inLocalDb: f == actualFirmware ? b : f.inLocalDb
}});
setFirmwareList(newFirmwareList);
});
}
setOverallFlashingState({state, progress});
});
}
@ -68,15 +68,15 @@ export const FlashSettings = ({deviceSelectedToFlash, setDeviceSelectedToFlash,
(f)=> {
f.device.setFlashState(f.state);
deviceSelectedToFlash[devices.indexOf(f.device)] = f.state;
setDeviceSelectedToFlash(deviceSelectedToFlash);
setDeviceSelectedToFlash(deviceSelectedToFlash);
if(f.state.state == "failed") {
toast({ title: `❌ Error: ${f.errorReason}`});
if(f.state.state == "failed") {
toast({ title: `❌ Error: ${f.errorReason}`});
}
}
);
}}
>
>
{stateToText(overallFlashingState.state, overallFlashingState.progress)}
</Button>}
</div>
@ -96,7 +96,7 @@ export const FlashSettings = ({deviceSelectedToFlash, setDeviceSelectedToFlash,
)
};
const FirmwareSelection = () => {
const FirmwareSelection = () => {
const { firmwareRefreshing, setFirmwareRefreshing, firmwareList, setFirmwareList, selectedFirmware, setSelectedFirmware, overallFlashingState } = useAppStore();
const isBusy = firmwareRefreshing || overallFlashingState.state == "busy";
const { toast } = useToast();
@ -107,7 +107,7 @@ const FirmwareSelection = () => {
</SelectItem>,
<SelectSeparator/>
];
let selection = selectedFirmware;
let selection = selectedFirmware;
if(firmwareRefreshing) {
selectItems = [
<SelectItem key={0} value={"updating"}>
@ -124,7 +124,7 @@ const FirmwareSelection = () => {
);
}
else {
const versions = firmwareList.map((f, index) => (
const versions = firmwareList.map((f, index) => (
<SelectItem key={index} value={f.id}>
{f.isPreRelease ?
(<div className="flex gap-2 items-center">{`(${f.name})`} {f.inLocalDb ? [<ArrowDownCircleIcon size={20}/>] : []}</div>) :
@ -133,7 +133,7 @@ const FirmwareSelection = () => {
</SelectItem>
))
selectItems.push(...versions);
}
}
selectItems.push(
<SelectItem key={100} value={"custom"}>
{"< Load custom firmware >"}
@ -142,22 +142,22 @@ const FirmwareSelection = () => {
return (
<div className="flex gap-1 w-full">
<Select
disabled={isBusy}
<Select
disabled={isBusy}
onValueChange={async (v) => {
if(v == "custom") {
if(v == "custom") {
const desc = await uploadCustomFirmware();
if(desc === undefined)
return;
if(!firmwareList.find(f => f.id == desc.id)) {
const newFirmwareList: FirmwareVersion[] = firmwareList.map(f => f).concat([ desc ]);
setFirmwareList(newFirmwareList);
}
}
setSelectedFirmware(desc.id);
}
else {
setSelectedFirmware(v);
}
}
}}
value={selection} // << Value of selected item
>
@ -175,7 +175,7 @@ const FirmwareSelection = () => {
disabled={isBusy}
onClick={() => {
setFirmwareRefreshing(true);
loadFirmwareList().then((list) => {
loadFirmwareList().then((list) => {
setFirmwareList(list.slice(0, 10));
setFirmwareRefreshing(false);
}).catch(() => toast({
@ -186,7 +186,7 @@ const FirmwareSelection = () => {
<RefreshCwIcon size={20}/>
</Button>
</div>
);
}
@ -200,7 +200,7 @@ export type FirmwareVersion = {
interface FirmwareGithubRelease {
name: string,
tag_name: string,
tag_name: string,
prerelease: boolean,
assets: {
name: string,
@ -273,9 +273,9 @@ export const deviceModels: DeviceModel[] = [
},
]
const DeviceModelSelection = () => {
const DeviceModelSelection = () => {
const { selectedDeviceModel, setSelectedDeviceModel, overallFlashingState } = useAppStore();
let selectItems = [
<SelectItem key={"auto"} value={"auto"}>
{"Auto-detect device model"}
@ -291,8 +291,8 @@ const DeviceModelSelection = () => {
return (
<div className="flex gap-1 w-full">
<Select
onValueChange={setSelectedDeviceModel}
<Select
onValueChange={setSelectedDeviceModel}
value={selectedDeviceModel}
disabled={overallFlashingState.state == "busy"}
>
@ -304,7 +304,7 @@ const DeviceModelSelection = () => {
</SelectContent>
</Select>
</div>
);
}
@ -315,7 +315,7 @@ function stateToText(state: OverallFlashingState, progress?: number) {
case "downloading":
return progress ? `Downloading firmware... (${(progress * 100).toFixed(1)} %)` : "Downloading firmware...";
case "busy":
return "In Progress...";
return "In Progress...";
case "waiting":
return "Continue";
default:
@ -330,15 +330,15 @@ async function loadFirmwareList() : Promise<FirmwareVersion[]> {
const id = r.assets.find(a => a.name.startsWith("firmware"))!.id;
if(id === undefined)
return undefined;
const tag = r.tag_name.substring(1); // remove leading "v"
return {
const tag = r.tag_name.substring(1); // remove leading "v"
return {
name: r.name.replace("Meshtastic Firmware ", ""),
tag: tag,
id: id,
isPreRelease: r.prerelease,
inLocalDb: await isStoredInDb(tag)
};
}));
}));
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;
},
setEnabled(name, value) {
config.overrideValues![name] = value;
config.overrideValues![name] = value;
},
} : undefined;
const isPresetConfig = !("id" in config);
const setConfig: (data: AudioValidation) => void =
isPresetConfig ? (data) => {
config.moduleConfig.audio = new Protobuf.ModuleConfig_AudioConfig(data);
config.moduleConfig.audio = new Protobuf.ModuleConfig_AudioConfig(data);
(config as ConfigPreset).saveConfigTree();
}
: (data) => {
@ -30,7 +30,7 @@ export const Audio = (): JSX.Element => {
})
);
}
const onSubmit = setConfig;
return (

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

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

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

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

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

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

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

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

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

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

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

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

2
src/components/UI/ConfigSelectButton.tsx

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

28
src/core/flashing/FirmwareDb.ts

@ -5,10 +5,10 @@ function openDb() {
const db = indexedDB.open("firmwares");
db.onsuccess = () => {
resolve(db.result);
};
};
db.onupgradeneeded = (ev) => {
const objStore = db.result.createObjectStore("files");
const objStore = db.result.createObjectStore("files");
objStore.transaction.oncomplete = () => resolve(db.result);
};
});
@ -18,15 +18,15 @@ export async function isStoredInDb(firmwareTag: string): Promise<boolean> {
const dbs = await indexedDB.databases();
if(dbs.find(db => db.name == "firmwares") === undefined)
return false;
return new Promise<boolean>((resolve, reject) => {
return new Promise<boolean>((resolve, reject) => {
const db = indexedDB.open("firmwares");
db.onsuccess = () => {
if(!db.result.objectStoreNames.contains("files"))
resolve(false);
const objStore = db.result.transaction("files", "readonly").objectStore("files");
const transaction = objStore.getKey(firmwareTag);
transaction.onsuccess = () => resolve(transaction.result !== undefined);
transaction.onerror = () => resolve(false);
transaction.onsuccess = () => resolve(transaction.result !== undefined);
transaction.onerror = () => resolve(false);
}
});
}
@ -41,31 +41,31 @@ export async function storeInDb(firmware: FirmwareVersion, file: ArrayBuffer) {
resolve();
}
fileStore.transaction.onerror = reject;
});
});
}
export async function loadFromDb(firmware: FirmwareVersion) {
export async function loadFromDb(firmware: FirmwareVersion) {
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 transaction = objStore.get(firmware.tag);
transaction.onsuccess = () => {
transaction.onsuccess = () => {
resolve(transaction.result as ArrayBuffer);
};
transaction.onerror = reject;
transaction.onerror = reject;
});
}
export async function deleteFromDb(firmware: FirmwareVersion) {
const db = await openDb();
return new Promise<void>((resolve, reject) => {
return new Promise<void>((resolve, reject) => {
if(!db.objectStoreNames.contains("files")) {
resolve();
return;
}
const objStore = db.transaction("files", "readonly").objectStore("files");
const transaction = objStore.delete(firmware.tag);
const transaction = objStore.delete(firmware.tag);
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];
for (let i = 0; i < config.count; i++) {
configQueue.push({config: config.config, moduleConfig: config.moduleConfig});
}
}
}
}
}
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");
flashStates = flashStates.filter(f => f.state == "doFlash");
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]);
configQueue = configQueue.slice(operations.length)
console.log(`New config queue count: ${configQueue.length}`);
Promise.allSettled(operations.map(op => op.flashAndConfigDevice())).then(p => handleFlashingDone());
}
@ -80,7 +80,7 @@ export async function uploadCustomFirmware() {
if(fileHandle == undefined)
return undefined;
try {
const file = await fileHandle.getFile();
const file = await fileHandle.getFile();
const content = await file.arrayBuffer();
const firmwareDesc: FirmwareVersion = {
id: "custom_" + file.name,
@ -93,11 +93,11 @@ export async function uploadCustomFirmware() {
return firmwareDesc;
} catch {
console.error("Insufficient permission to access file.");
}
}
return undefined;
}
async function getZipFile() {
async function getZipFile() {
if(firmwareToUse.inLocalDb) {
const storedZip = await loadFromDb(firmwareToUse);
if(storedZip !== undefined)
@ -107,10 +107,10 @@ async function getZipFile() {
const zip = await fetch(`/firmware/${firmwareToUse.id}`);
const zipClone = zip.clone();
const contentLength = zip.headers.get("content-length");
const totalLength = contentLength ? parseInt(contentLength) : undefined;
const totalLength = contentLength ? parseInt(contentLength) : undefined;
const reader = zip.body!.getReader();
let bytesLoaded = 0;
while (true) {
const { done, value } = await reader.read();
if (done)
@ -124,22 +124,22 @@ async function getZipFile() {
return content;
}
async function loadFirmware() {
console.log("Loading firmware");
async function loadFirmware() {
console.log("Loading firmware");
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)) {
const dataSection = await zipFile.file(name)?.async("uint8array");
if(dataSection === undefined) {
console.warn(`Firmware file ${name} not found.`);
return undefined;
}
}
dataSections[name] = dataSection;
}
}
return dataSections[name];
}
@ -172,14 +172,14 @@ function autoDetectDeviceModel(port: SerialPort) {
export class FlashOperation {
public state: FlashState = { progress: 0, state: "idle" };
public errorReason?: string;
private loader?: esptooljs.ESPLoader;
private loader?: esptooljs.ESPLoader;
private isCancelled: boolean;
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) {
@ -197,18 +197,18 @@ export class FlashOperation {
await this.flash();
await this.setConfig();
this.setState("done");
}
}
catch(e) {
this.setState("failed", 0, e as string);
this.setState("failed", 0, e as string);
throw e;
}
}
}
private async flash() {
const installedVersion = this.device.hardware.firmwareVersion;
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)
return;
@ -224,28 +224,28 @@ export class FlashOperation {
// -----------
try {
try {
const transport = new esptooljs.Transport(port);
this.loader = new esptooljs.ESPLoader(transport, 115200);
this.loader = new esptooljs.ESPLoader(transport, 115200);
const loader = this.loader;
this.setState("preparing");
await loader.main_fn();
await loader.main_fn();
if(sections.length > 1) {
this.setState("erasing");
this.setState("erasing");
await loader.erase_flash();
}
}
const totalLength = sections.reduce<number>((p, c) => p + c.data.byteLength, 0);
let bytesFlashed = 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 blob = new Blob([s.data]);
const content = await new Promise<string>((resolve, reject) => {
fileReader.onloadend = e => resolve(fileReader.result as string);
fileReader.onerror = e => reject(fileReader.result as string);
fileReader.readAsBinaryString(blob);
fileReader.readAsBinaryString(blob);
});
return { data: content, address: s.offset };
}));
@ -261,30 +261,30 @@ export class FlashOperation {
const bytesThisSegment = (written / total) * sections[index].data.byteLength;
console.log(`FLASHING PROGRESS ${bytesFlashed + written} / ${totalLength} ... ${bytesFlashed} | ${written} | ${total} | ${index}`);
this.setState("flashing", (bytesFlashed + bytesThisSegment) / totalLength);
});
});
}
catch (e) {
throw e;
catch (e) {
throw e;
}
finally {
finally {
await this.loader?.flash_finish();
}
this.setState("config");
await port!.setSignals({requestToSend: true});
await new Promise(r => setTimeout(r, 100));
await port!.setSignals({requestToSend: false});
await new Promise(r => setTimeout(r, 100));
await port!.setSignals({requestToSend: false});
await port!.close();
const connection = (this.device.connection! as ISerialConnection);
//@ts-ignore
connection.preventLock = false;
debugger;
await connection.connect({
await connection.connect({
port,
baudRate: undefined,
concurrentLogOutput: true
});
await new Promise(r => setTimeout(r, 5000));
await new Promise(r => setTimeout(r, 5000));
}
private async setConfig() {
@ -292,14 +292,14 @@ export class FlashOperation {
return;
this.setState("config");
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: "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: "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: "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: "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: "cannedMessage", value: this.moduleConfig.cannedMessage! } })),
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
if(!this.config.device!.serialEnabled)
return;

4
src/core/stores/deviceStore.ts

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

5
src/pages/Config/ConfigTabs.tsx

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

2
src/pages/Config/ModuleConfig.tsx

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

2
tsconfig.json

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

Loading…
Cancel
Save