installPybricksDialog: add feature to install custom firmware

This restores the ability to drag and drop a custom firmware file to
flash the firmware on the hub.

Fixes: https://github.com/pybricks/pybricks-code/issues/1020
This commit is contained in:
David Lechner
2022-09-06 14:14:57 -05:00
parent ebb8900efe
commit 4e77efb34f
7 changed files with 393 additions and 38 deletions
+102 -10
View File
@@ -2,26 +2,30 @@
// Copyright (c) 2022 The Pybricks Authors
// based on https://usehooks-ts.com/react-hook/use-fetch
import { FirmwareReader } from '@pybricks/firmware';
import { FirmwareMetadata, FirmwareReader } from '@pybricks/firmware';
import cityHubZip from '@pybricks/firmware/build/cityhub.zip';
import essentialHubZip from '@pybricks/firmware/build/essentialhub.zip';
import moveHubZip from '@pybricks/firmware/build/movehub.zip';
import primeHubZip from '@pybricks/firmware/build/primehub.zip';
import technicHubZip from '@pybricks/firmware/build/technichub.zip';
import { useEffect, useReducer, useRef } from 'react';
import { useEffect, useMemo, useReducer, useRef } from 'react';
import { useDispatch } from 'react-redux';
import { useIsMounted } from 'usehooks-ts';
import { alertsShowAlert } from '../../alerts/actions';
import { Hub } from '../../components/hubPicker';
import { ensureError } from '../../utils';
type FirmwareData = {
firmwareZip: ArrayBuffer;
licenseText: string;
metadata: FirmwareMetadata;
};
interface State {
/** The firmware.zip data or undefined if `fetch()` is not complete or on error. */
data?: FirmwareData;
firmwareData?: FirmwareData;
/** Undefined `fetch()` is not complete yet or was successful, otherwise the error. */
error?: Error;
firmwareError?: Error;
}
type Cache = { [url: string]: FirmwareData };
@@ -52,8 +56,8 @@ export function useFirmware(hubType: Hub): State {
const isMounted = useIsMounted();
const initialState: State = {
error: undefined,
data: undefined,
firmwareError: undefined,
firmwareData: undefined,
};
// Keep state logic separated
@@ -62,9 +66,9 @@ export function useFirmware(hubType: Hub): State {
case 'loading':
return { ...initialState };
case 'fetched':
return { ...initialState, data: action.payload };
return { ...initialState, firmwareData: action.payload };
case 'error':
return { ...initialState, error: action.payload };
return { ...initialState, firmwareError: action.payload };
default:
return state;
}
@@ -96,7 +100,8 @@ export function useFirmware(hubType: Hub): State {
const firmwareZip = await response.arrayBuffer();
const reader = await FirmwareReader.load(firmwareZip);
const licenseText = await reader.readReadMeOss();
const data = { firmwareZip, licenseText };
const metadata = await reader.readMetadata();
const data = { firmwareZip, licenseText, metadata };
cache.current[url] = data;
@@ -114,7 +119,7 @@ export function useFirmware(hubType: Hub): State {
return;
}
dispatch({ type: 'error', payload: error as Error });
dispatch({ type: 'error', payload: ensureError(error) });
}
};
@@ -123,3 +128,90 @@ export function useFirmware(hubType: Hub): State {
return state;
}
/**
* Gets the data from the user-provided firmware file, if any.
* @param zipFile The user-provided zip file.
* @returns State consisting of unzipped data or error.
*/
export function useCustomFirmware(zipFile: File | undefined) {
const reduxDispatch = useDispatch();
const isMounted = useIsMounted();
const initialState: State = {
firmwareError: undefined,
firmwareData: undefined,
};
// Keep state logic separated
const fetchReducer = (state: State, action: Action): State => {
switch (action.type) {
case 'loading':
return { ...initialState };
case 'fetched':
return { ...initialState, firmwareData: action.payload };
case 'error':
return { ...initialState, firmwareError: action.payload };
default:
return state;
}
};
const [state, dispatch] = useReducer(fetchReducer, initialState);
useEffect(() => {
if (!zipFile) {
dispatch({ type: 'loading' });
return;
}
// REVISIT: with no cache, we end up unzipping the same file multiple times.
const readFile = async () => {
dispatch({ type: 'loading' });
try {
const firmwareZip = await zipFile.arrayBuffer();
const reader = await FirmwareReader.load(firmwareZip);
const licenseText = await reader.readReadMeOss();
const metadata = await reader.readMetadata();
const data = {
firmwareZip,
licenseText,
metadata,
};
if (!isMounted()) {
return;
}
dispatch({ type: 'fetched', payload: data });
} catch (err) {
if (process.env.NODE_ENV !== 'test') {
console.error(err);
}
if (!isMounted()) {
return;
}
const error = ensureError(err);
dispatch({ type: 'error', payload: error });
reduxDispatch(alertsShowAlert('alerts', 'unexpectedError', { error }));
}
};
readFile();
}, [zipFile, isMounted]);
const isCustomFirmwareRequested = useMemo(
() => state.firmwareData !== undefined,
[state.firmwareData],
);
return {
isCustomFirmwareRequested,
customFirmwareData: state.firmwareData,
customFirmwareError: state.firmwareError,
};
}