mirror of
https://github.com/pybricks/pybricks-code.git
synced 2026-09-12 17:45:22 +00:00
firmware: add select picker for selecting custom main.py
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user