firmware: add select picker for selecting custom main.py

This commit is contained in:
David Lechner
2022-07-20 14:25:40 -05:00
parent 85deb42f76
commit af8a31d50c
10 changed files with 150 additions and 41 deletions
+1
View File
@@ -11,6 +11,7 @@
"@babel/core": "^7.18.6",
"@blueprintjs/core": "^4.5.0",
"@blueprintjs/popover2": "^1.4.2",
"@blueprintjs/select": "^4.4.2",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.7",
"@pybricks/firmware": "4.16.1",
"@pybricks/ide-docs": "2.2.0",
+4 -4
View File
@@ -120,15 +120,15 @@ export type FailToFinishReason =
/**
* Creates a new action to flash firmware to a hub.
* @param data The firmware zip file data or `null` to get firmware later.
* @param flashCurrentProgram If true, flash the current program from the editor,
* otherwise use the program from firmware.zip.
* @param customProgram If defined, flash the path of a program from file storage,
* otherwise use the main.py program from firmware.zip.
* @param hubName A custom hub name or an empty string to use the default name.
*/
export const flashFirmware = createAction(
(data: ArrayBuffer | null, flashCurrentProgram: boolean, hubName: string) => ({
(data: ArrayBuffer | null, customProgram: string | undefined, hubName: string) => ({
type: 'flashFirmware.action.flashFirmware',
data,
flashCurrentProgram,
customProgram,
hubName,
}),
);
@@ -13,12 +13,14 @@ import {
Icon,
InputGroup,
Intent,
MenuItem,
MultistepDialog,
NonIdealState,
Spinner,
Switch,
} from '@blueprintjs/core';
import { Classes as Classes2, Popover2 } from '@blueprintjs/popover2';
import { Select2 } from '@blueprintjs/select';
import classNames from 'classnames';
import React, { useMemo, useState } from 'react';
import { useDispatch } from 'react-redux';
@@ -31,6 +33,8 @@ import {
hubHasUSB,
} from '../../components/hubPicker';
import { HubPicker } from '../../components/hubPicker/HubPicker';
import { FileMetadata } from '../../fileStorage';
import { useFileStorageMetadata } from '../../fileStorage/hooks';
import { useSelector } from '../../reducers';
import {
firmwareInstallPybricksDialogAccept,
@@ -176,19 +180,24 @@ type SelectOptionsPanelProps = {
hubType: Hub;
hubName: string;
includeProgram: boolean;
selectedIncludeFile: FileMetadata | undefined;
onChangeHubName(hubName: string): void;
onChangeIncludeProgram(includeProgram: boolean): void;
onChangeSelectedIncludeFile(selectedIncludeFile: FileMetadata | undefined): void;
};
const ConfigureOptionsPanel: React.VoidFunctionComponent<SelectOptionsPanelProps> = ({
hubType,
hubName,
includeProgram,
selectedIncludeFile,
onChangeHubName,
onChangeIncludeProgram,
onChangeSelectedIncludeFile,
}) => {
const i18n = useI18n();
const isHubNameValid = validateHubName(hubName);
const files = useFileStorageMetadata();
return (
<div className={dialogBody}>
@@ -198,7 +207,6 @@ const ConfigureOptionsPanel: React.VoidFunctionComponent<SelectOptionsPanelProps
>
<ControlGroup>
<InputGroup
id="hub-name-input"
value={hubName}
onChange={(e) => onChangeHubName(e.currentTarget.value)}
onMouseOver={(e) => e.preventDefault()}
@@ -234,8 +242,9 @@ const ConfigureOptionsPanel: React.VoidFunctionComponent<SelectOptionsPanelProps
)) || (
<ControlGroup>
<Switch
label={i18n.translate(
I18nId.OptionsPanelCustomMainIncludeCurrentProgramLabel,
labelElement={i18n.translate(
I18nId.OptionsPanelCustomMainIncludeLabel,
{ main: <code>main.py</code> },
)}
checked={includeProgram}
onChange={(e) =>
@@ -244,12 +253,54 @@ const ConfigureOptionsPanel: React.VoidFunctionComponent<SelectOptionsPanelProps
)
}
/>
<Select2
items={files || []}
itemRenderer={(
item,
{ handleClick, handleFocus, modifiers },
) => (
<MenuItem
roleStructure="listoption"
active={modifiers.active}
disabled={modifiers.disabled}
text={item.path}
key={item.uuid}
onClick={handleClick}
onFocus={handleFocus}
/>
)}
noResults={
<MenuItem
roleStructure="listoption"
disabled={true}
text={i18n.translate(
I18nId.OptionsPanelCustomMainIncludeNoFiles,
)}
/>
}
filterable={false}
popoverProps={{ minimal: true }}
disabled={!includeProgram}
onItemSelect={onChangeSelectedIncludeFile}
>
<Button
icon="double-caret-vertical"
text={
selectedIncludeFile?.path ??
i18n.translate(
I18nId.OptionsPanelCustomMainIncludeNoSelection,
)
}
disabled={!includeProgram}
/>
</Select2>
<HelpButton
helpForLabel={i18n.translate(
I18nId.OptionsPanelCustomMainIncludeCurrentProgramLabel,
I18nId.OptionsPanelCustomMainIncludeLabel,
{ main: 'main.py' },
)}
content={i18n.translate(
I18nId.OptionsPanelCustomMainIncludeCurrentProgramHelp,
I18nId.OptionsPanelCustomMainIncludeHelp,
{
appName,
},
@@ -355,6 +406,7 @@ export const InstallPybricksDialog: React.VoidFunctionComponent = () => {
const [hubType, setHubType] = useState(defaultHubType);
const [hubName, setHubName] = useState('');
const [includeProgram, setIncludeProgram] = useState(false);
const [selectedIncludeFile, setSelectedIncludeFile] = useState<FileMetadata>();
const [licenseAccepted, setLicenseAccepted] = useState(false);
const { data } = useFirmware(hubType);
const i18n = useI18n();
@@ -370,7 +422,7 @@ export const InstallPybricksDialog: React.VoidFunctionComponent = () => {
dispatch(
firmwareInstallPybricksDialogAccept(
data?.firmwareZip ?? new ArrayBuffer(0),
undefined,
selectedIncludeFile?.path,
hubName,
),
),
@@ -406,8 +458,10 @@ export const InstallPybricksDialog: React.VoidFunctionComponent = () => {
hubType={hubType}
hubName={hubName}
includeProgram={includeProgram}
selectedIncludeFile={selectedIncludeFile}
onChangeHubName={setHubName}
onChangeIncludeProgram={setIncludeProgram}
onChangeSelectedIncludeFile={setSelectedIncludeFile}
/>
}
backButtonProps={{ text: i18n.translate(I18nId.BackButtonLabel) }}
@@ -8,7 +8,12 @@ export const firmwareInstallPybricksDialogShow = createAction(() => ({
type: 'firmware.installPybricksDialog.action.show',
}));
/** Actions that indicates the user accepted the install Pybricks firmware dialog. */
/**
* Action that indicates the user accepted the install Pybricks firmware dialog.
* @param firmwareZip The firmware.zip raw data.
* @param customProgram Optional path of custom program to include when flashing firmware.
* @param hubName The hub name to use when flashing firmware.
*/
export const firmwareInstallPybricksDialogAccept = createAction(
(firmwareZip: ArrayBuffer, customProgram: string | undefined, hubName: string) => ({
type: 'firmware.installPybricksDialog.action.accept',
+4 -2
View File
@@ -36,8 +36,10 @@ export enum I18nId {
OptionsPanelCustomMainLabel = 'optionsPanel.customMain.label',
OptionsPanelCustomMainLabelInfo = 'optionsPanel.customMain.labelInfo',
OptionsPanelCustomMainNotApplicableMessage = 'optionsPanel.customMain.notApplicable.message',
OptionsPanelCustomMainIncludeCurrentProgramLabel = 'optionsPanel.customMain.includeCurrentProgram.label',
OptionsPanelCustomMainIncludeCurrentProgramHelp = 'optionsPanel.customMain.includeCurrentProgram.help',
OptionsPanelCustomMainIncludeLabel = 'optionsPanel.customMain.include.label',
OptionsPanelCustomMainIncludeNoSelection = 'optionsPanel.customMain.include.noSelection',
OptionsPanelCustomMainIncludeNoFiles = 'optionsPanel.customMain.include.noFiles',
OptionsPanelCustomMainIncludeHelp = 'optionsPanel.customMain.include.help',
BootloaderPanelTitle = 'bootloaderPanel.title',
BootloaderPanelInstruction1 = 'bootloaderPanel.instruction1',
BootloaderPanelButtonBluetooth = 'bootloaderPanel.button.bluetooth',
@@ -40,13 +40,15 @@
"error": "The name is too long."
},
"customMain": {
"label": "Custom program",
"label": "Include custom program",
"labelInfo": "(optional)",
"notApplicable": {
"message": "This hub has external flash memory so including a custom program when flashing firmware is not needed."
},
"includeCurrentProgram": {
"label": "Include current program",
"include": {
"label": "Include selected program as {main}",
"noSelection": "(no selection)",
"noFiles": "(no files)",
"help": "Enable to include your program when flashing the firmware or disable to use the default program. Flashing your program along with the firmware will allow you to run your program without being connected to {appName}"
}
}
+21 -21
View File
@@ -82,7 +82,7 @@ describe('flashFirmware', () => {
// saga is triggered by this action
saga.put(flashFirmwareAction(null, false, 'test name'));
saga.put(flashFirmwareAction(null, undefined, 'test name'));
// first step is to connect to the hub bootloader
@@ -228,7 +228,7 @@ describe('flashFirmware', () => {
// saga is triggered by this action
saga.put(flashFirmwareAction(null, false, ''));
saga.put(flashFirmwareAction(null, undefined, ''));
// first step is to connect to the hub bootloader
@@ -277,7 +277,7 @@ describe('flashFirmware', () => {
// saga is triggered by this action
saga.put(flashFirmwareAction(null, false, ''));
saga.put(flashFirmwareAction(null, undefined, ''));
// first step is to connect to the hub bootloader
@@ -344,7 +344,7 @@ describe('flashFirmware', () => {
// saga is triggered by this action
saga.put(flashFirmwareAction(null, false, ''));
saga.put(flashFirmwareAction(null, undefined, ''));
// first step is to connect to the hub bootloader
@@ -407,7 +407,7 @@ describe('flashFirmware', () => {
// saga is triggered by this action
saga.put(flashFirmwareAction(null, false, ''));
saga.put(flashFirmwareAction(null, undefined, ''));
// first step is to connect to the hub bootloader
@@ -473,7 +473,7 @@ describe('flashFirmware', () => {
// saga is triggered by this action
saga.put(flashFirmwareAction(null, false, ''));
saga.put(flashFirmwareAction(null, undefined, ''));
// first step is to connect to the hub bootloader
@@ -532,7 +532,7 @@ describe('flashFirmware', () => {
// saga is triggered by this action
saga.put(flashFirmwareAction(null, false, ''));
saga.put(flashFirmwareAction(null, undefined, ''));
// first step is to connect to the hub bootloader
@@ -597,7 +597,7 @@ describe('flashFirmware', () => {
// saga is triggered by this action
saga.put(flashFirmwareAction(null, false, ''));
saga.put(flashFirmwareAction(null, undefined, ''));
// first step is to connect to the hub bootloader
@@ -679,7 +679,7 @@ describe('flashFirmware', () => {
// saga is triggered by this action
saga.put(flashFirmwareAction(null, false, ''));
saga.put(flashFirmwareAction(null, undefined, ''));
// first step is to connect to the hub bootloader
@@ -744,7 +744,7 @@ describe('flashFirmware', () => {
// saga is triggered by this action
saga.put(flashFirmwareAction(null, false, ''));
saga.put(flashFirmwareAction(null, undefined, ''));
// first step is to connect to the hub bootloader
@@ -839,7 +839,7 @@ describe('flashFirmware', () => {
// saga is triggered by this action
saga.put(flashFirmwareAction(null, false, ''));
saga.put(flashFirmwareAction(null, undefined, ''));
// first step is to connect to the hub bootloader
@@ -943,7 +943,7 @@ describe('flashFirmware', () => {
// saga is triggered by this action
saga.put(flashFirmwareAction(null, false, ''));
saga.put(flashFirmwareAction(null, undefined, ''));
// first step is to connect to the hub bootloader
@@ -1087,7 +1087,7 @@ describe('flashFirmware', () => {
// saga is triggered by this action
saga.put(flashFirmwareAction(null, false, ''));
saga.put(flashFirmwareAction(null, undefined, ''));
// first step is to connect to the hub bootloader
@@ -1232,7 +1232,7 @@ describe('flashFirmware', () => {
saga.put(
flashFirmwareAction(
await zip.generateAsync({ type: 'arraybuffer' }),
false,
undefined,
'',
),
);
@@ -1381,7 +1381,7 @@ describe('flashFirmware', () => {
saga.put(
flashFirmwareAction(
await zip.generateAsync({ type: 'arraybuffer' }),
false,
undefined,
'',
),
);
@@ -1428,7 +1428,7 @@ describe('flashFirmware', () => {
saga.put(
flashFirmwareAction(
await zip.generateAsync({ type: 'arraybuffer' }),
false,
undefined,
'',
),
);
@@ -1474,7 +1474,7 @@ describe('flashFirmware', () => {
saga.put(
flashFirmwareAction(
await zip.generateAsync({ type: 'arraybuffer' }),
false,
undefined,
'',
),
);
@@ -1534,7 +1534,7 @@ describe('flashFirmware', () => {
saga.put(
flashFirmwareAction(
await zip.generateAsync({ type: 'arraybuffer' }),
false,
undefined,
'',
),
);
@@ -1595,7 +1595,7 @@ describe('flashFirmware', () => {
saga.put(
flashFirmwareAction(
await zip.generateAsync({ type: 'arraybuffer' }),
false,
undefined,
'',
),
);
@@ -1659,7 +1659,7 @@ describe('flashFirmware', () => {
saga.put(
flashFirmwareAction(
await zip.generateAsync({ type: 'arraybuffer' }),
false,
undefined,
'',
),
);
@@ -1745,7 +1745,7 @@ describe('flashFirmware', () => {
// saga is triggered by this action
saga.put(flashFirmwareAction(null, false, ''));
saga.put(flashFirmwareAction(null, undefined, ''));
// first step is to connect to the hub bootloader
+29 -4
View File
@@ -25,7 +25,11 @@ import {
take,
takeEvery,
} from 'typed-redux-saga/macro';
import { editorGetValue } from '../editor/sagas';
import {
fileStorageDidFailToReadFile,
fileStorageDidReadFile,
fileStorageReadFile,
} from '../fileStorage/actions';
import {
checksumRequest,
checksumResponse,
@@ -283,8 +287,27 @@ function* handleFlashFirmware(action: ReturnType<typeof flashFirmware>): Generat
let program: string | undefined = undefined;
if (action.flashCurrentProgram) {
program = yield* editorGetValue();
if (action.customProgram) {
yield* put(fileStorageReadFile(action.customProgram));
const { didRead, didFailToRead } = yield* race({
didRead: take(
fileStorageDidReadFile.when((a) => a.path === action.customProgram),
),
didFailToRead: take(
fileStorageDidFailToReadFile.when(
(a) => a.path === action.customProgram,
),
),
});
if (didFailToRead) {
throw didFailToRead.error;
}
defined(didRead);
program = didRead.contents;
}
if (action.data !== null) {
@@ -496,7 +519,9 @@ function* handleInstallPybricks(): Generator {
defined(accepted);
yield* put(flashFirmware(accepted.firmwareZip, false, accepted.hubName));
yield* put(
flashFirmware(accepted.firmwareZip, accepted.customProgram, accepted.hubName),
);
}
export default function* (): Generator {
+4
View File
@@ -118,6 +118,10 @@ a.#{bp.$ns}-button {
}
}
.#{bp.$ns}-control-group {
gap: bp.$pt-grid-size * 0.5;
}
.#{bp.$ns}-form-group > .#{bp.$ns}-label {
font-weight: bolder;
}
+16
View File
@@ -1558,6 +1558,21 @@ __metadata:
languageName: node
linkType: hard
"@blueprintjs/select@npm:^4.4.2":
version: 4.4.2
resolution: "@blueprintjs/select@npm:4.4.2"
dependencies:
"@blueprintjs/core": ^4.6.0
"@blueprintjs/popover2": ^1.4.2
classnames: ^2.2
tslib: ~2.3.1
peerDependencies:
react: ^16.8 || 17 || 18
react-dom: ^16.8 || 17 || 18
checksum: eb41a9ea513d477fd6336e4875a605b467d174b3fa6bfe7bd4ccd5f6247a3d528802757cc13e39be40312f494640d5aa02c0378a4964558a1cfeb94a98c1dc20
languageName: node
linkType: hard
"@csstools/normalize.css@npm:*":
version: 12.0.0
resolution: "@csstools/normalize.css@npm:12.0.0"
@@ -2298,6 +2313,7 @@ __metadata:
"@babel/core": ^7.18.6
"@blueprintjs/core": ^4.5.0
"@blueprintjs/popover2": ^1.4.2
"@blueprintjs/select": ^4.4.2
"@pmmmwh/react-refresh-webpack-plugin": ^0.5.7
"@pybricks/firmware": 4.16.1
"@pybricks/ide-docs": 2.2.0