Skip to content

Commit 007640c

Browse files
committed
firmware: add UI for EV3 firmware flashing
This is just the UI bits (new file template, Pybricks firmware install, official firmware restore). The Pybricks firmware flashing hangs because we don't have a EV3 firmware yet in @pybricks/firmware. Official firmware restore has an alert() placeholder so at least it does something.
1 parent 8b5cab4 commit 007640c

File tree

14 files changed

+283
-73
lines changed

14 files changed

+283
-73
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.11",
1414
"@pybricks/firmware": "7.22.0",
1515
"@pybricks/ide-docs": "2.20.0",
16-
"@pybricks/images": "^1.3.0",
16+
"@pybricks/images": "^1.4.0",
1717
"@pybricks/jedi": "1.17.0",
1818
"@pybricks/mpy-cross-v5": "^2.0.0",
1919
"@pybricks/mpy-cross-v6": "^2.0.0",

src/components/hubPicker/HubPicker.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// SPDX-License-Identifier: MIT
2-
// Copyright (c) 2022-2023 The Pybricks Authors
2+
// Copyright (c) 2022-2025 The Pybricks Authors
33

44
import './hubPicker.scss';
55
import { Radio, RadioGroup } from '@blueprintjs/core';
@@ -66,6 +66,12 @@ export const HubPicker: React.FunctionComponent<HubPickerProps> = ({ disabled })
6666
label="MINDSTORMS Robot Inventor Hub"
6767
/>
6868
</Radio>
69+
<Radio value={Hub.EV3}>
70+
<HubIcon
71+
url={new URL('@pybricks/images/hub-ev3.png', import.meta.url)}
72+
label="MINDSTORMS EV3"
73+
/>
74+
</Radio>
6975
</RadioGroup>
7076
);
7177
};

src/components/hubPicker/index.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// SPDX-License-Identifier: MIT
2-
// Copyright (c) 2022 The Pybricks Authors
2+
// Copyright (c) 2022-2025 The Pybricks Authors
33

44
/** Supported hub types. */
55
export enum Hub {
@@ -15,6 +15,8 @@ export enum Hub {
1515
Prime = 'primehub',
1616
/** SPIKE Essential hub */
1717
Essential = 'essentialhub',
18+
/** MINDSTORMS EV3 hub */
19+
EV3 = 'ev3',
1820
}
1921

2022
/**
@@ -25,6 +27,7 @@ export function hubHasUSB(hub: Hub): boolean {
2527
case Hub.Prime:
2628
case Hub.Essential:
2729
case Hub.Inventor:
30+
case Hub.EV3:
2831
return true;
2932
default:
3033
return false;
@@ -52,6 +55,7 @@ export function hubHasExternalFlash(hub: Hub): boolean {
5255
case Hub.Prime:
5356
case Hub.Essential:
5457
case Hub.Inventor:
58+
case Hub.EV3:
5559
return true;
5660
default:
5761
return false;
@@ -69,5 +73,7 @@ export function hubBootloaderType(hub: Hub) {
6973
case Hub.City:
7074
case Hub.Technic:
7175
return 'ble-lwp3-bootloader';
76+
case Hub.EV3:
77+
return 'usb-ev3';
7278
}
7379
}

src/editor/pybricksMicroPython.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,9 @@ export const language = <monaco.languages.IMonarchLanguage>{
309309
*/
310310
function createTemplate(hubClassName: string, deviceClassNames: string[]): string {
311311
return `from pybricks.hubs import ${hubClassName}
312-
from pybricks.pupdevices import ${deviceClassNames.join(', ')}
312+
from pybricks.${
313+
hubClassName === 'EV3Brick' ? 'ev3devices' : 'pupdevices'
314+
} import ${deviceClassNames.join(', ')}
313315
from pybricks.parameters import Button, Color, Direction, Port, Side, Stop
314316
from pybricks.robotics import DriveBase
315317
from pybricks.tools import wait, StopWatch
@@ -325,7 +327,8 @@ type HubLabel =
325327
| 'technichub'
326328
| 'inventorhub'
327329
| 'primehub'
328-
| 'essentialhub';
330+
| 'essentialhub'
331+
| 'ev3';
329332

330333
const templateSnippets: Array<
331334
Required<
@@ -375,6 +378,18 @@ const templateSnippets: Array<
375378
'ColorLightMatrix',
376379
]),
377380
},
381+
{
382+
label: 'ev3',
383+
documentation: 'Template for MINDSTORMS EV3 program.',
384+
insertText: createTemplate('EV3Brick', [
385+
'Motor',
386+
'ColorSensor',
387+
'GyroSensor',
388+
'InfraredSensor',
389+
'TouchSensor',
390+
'UltrasonicSensor',
391+
]),
392+
},
378393
];
379394

380395
/**

src/firmware/actions.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// SPDX-License-Identifier: MIT
2-
// Copyright (c) 2020-2022 The Pybricks Authors
2+
// Copyright (c) 2020-2025 The Pybricks Authors
33

44
import { FirmwareReaderError, HubType } from '@pybricks/firmware';
55
import { createAction } from '../actions';
@@ -429,3 +429,39 @@ export const firmwareDidRestoreOfficialDfu = createAction(() => ({
429429
export const firmwareDidFailToRestoreOfficialDfu = createAction(() => ({
430430
type: 'firmware.action.didFailToRestoreOfficialDfu',
431431
}));
432+
433+
/**
434+
* Versions of official LEGO EV3 firmware available for restore.
435+
*/
436+
export enum EV3OfficialFirmwareVersion {
437+
/** Official LEGO EV3 firmware version 1.09H (Home edition). */
438+
home = '1.09H',
439+
/** Official LEGO EV3 firmware version 1.09E (Education edition). */
440+
education = '1.09E',
441+
/** Official LEGO EV3 firmware version 1.10E (only useful for Microsoft MakeCode). */
442+
makecode = '1.10E',
443+
}
444+
445+
/**
446+
* Action that triggers the restore official EV3 firmware saga.
447+
*/
448+
export const firmwareRestoreOfficialEV3 = createAction(
449+
(version: EV3OfficialFirmwareVersion) => ({
450+
type: 'firmware.action.restoreOfficialEV3',
451+
version,
452+
}),
453+
);
454+
455+
/**
456+
* Action that indicates {@link firmwareRestoreOfficialEV3} succeeded.
457+
*/
458+
export const firmwareDidRestoreOfficialEV3 = createAction(() => ({
459+
type: 'firmware.action.didRestoreOfficialEV3',
460+
}));
461+
462+
/**
463+
* Action that indicates {@link firmwareRestoreOfficialEV3} failed.
464+
*/
465+
export const firmwareDidFailToRestoreOfficialEV3 = createAction(() => ({
466+
type: 'firmware.action.didFailToRestoreOfficialEV3',
467+
}));

src/firmware/bootloaderInstructions/BootloaderInstructions.tsx

Lines changed: 68 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// SPDX-License-Identifier: MIT
2-
// Copyright (c) 2022-2023 The Pybricks Authors
2+
// Copyright (c) 2022-2025 The Pybricks Authors
33

44
import './bootloaderInstructions.scss';
55
import { Callout, Intent } from '@blueprintjs/core';
@@ -117,7 +117,11 @@ const BootloaderInstructions: React.FunctionComponent<BootloaderInstructionsProp
117117
const { button, light, lightPattern } = useMemo(() => {
118118
return {
119119
button: i18n.translate(
120-
hubHasBluetoothButton(hubType) ? 'button.bluetooth' : 'button.power',
120+
hubType === Hub.EV3
121+
? 'button.right'
122+
: hubHasBluetoothButton(hubType)
123+
? 'button.bluetooth'
124+
: 'button.power',
121125
),
122126
light: i18n.translate(
123127
hubHasBluetoothButton(hubType) ? 'light.bluetooth' : 'light.status',
@@ -163,13 +167,16 @@ const BootloaderInstructions: React.FunctionComponent<BootloaderInstructionsProp
163167
const prepareSteps = useMemo(
164168
() => (
165169
<>
166-
<li>
167-
{i18n.translate(
168-
hubHasUSB(hubType)
169-
? 'instructionGroup.prepare.usb'
170-
: 'instructionGroup.prepare.batteries',
171-
)}
172-
</li>
170+
{hubType !== Hub.EV3 && (
171+
<li>
172+
{i18n.translate(
173+
hubHasUSB(hubType)
174+
? 'instructionGroup.prepare.usb'
175+
: 'instructionGroup.prepare.batteries',
176+
)}
177+
</li>
178+
)}
179+
173180
<li>{i18n.translate('instructionGroup.prepare.turnOff')}</li>
174181
{/* For non-usb recovery, show step about official app */}
175182
{recovery && !hubHasUSB(hubType) && (
@@ -179,6 +186,12 @@ const BootloaderInstructions: React.FunctionComponent<BootloaderInstructionsProp
179186
})}
180187
</li>
181188
)}
189+
190+
{hubType === Hub.EV3 && (
191+
<li>
192+
{i18n.translate('instructionGroup.bootloaderMode.connectUsb')}
193+
</li>
194+
)}
182195
</>
183196
),
184197
[i18n, recovery, hubType],
@@ -210,28 +223,36 @@ const BootloaderInstructions: React.FunctionComponent<BootloaderInstructionsProp
210223

211224
{/* not strictly necessary, but order is swapped in the video,
212225
so we match it here. */}
213-
{hubType !== Hub.Essential && hubHasUSB(hubType) && (
226+
{(hubType === Hub.Prime || hubType === Hub.Inventor) &&
227+
hubHasUSB(hubType) && (
228+
<li
229+
className={classNames(
230+
activeStep === 'connect-usb' && 'pb-active-step',
231+
)}
232+
>
233+
{i18n.translate(
234+
'instructionGroup.bootloaderMode.connectUsb',
235+
)}
236+
</li>
237+
)}
238+
239+
{hubType !== Hub.EV3 && (
214240
<li
215241
className={classNames(
216-
activeStep === 'connect-usb' && 'pb-active-step',
242+
activeStep === 'wait-for-light' && 'pb-active-step',
217243
)}
218244
>
219-
{i18n.translate('instructionGroup.bootloaderMode.connectUsb')}
245+
{i18n.translate(
246+
'instructionGroup.bootloaderMode.waitForLight',
247+
{
248+
button,
249+
light,
250+
lightPattern,
251+
},
252+
)}
220253
</li>
221254
)}
222255

223-
<li
224-
className={classNames(
225-
activeStep === 'wait-for-light' && 'pb-active-step',
226-
)}
227-
>
228-
{i18n.translate('instructionGroup.bootloaderMode.waitForLight', {
229-
button,
230-
light,
231-
lightPattern,
232-
})}
233-
</li>
234-
235256
{hubType === Hub.Essential && hubHasUSB(hubType) && (
236257
<li
237258
className={classNames(
@@ -242,6 +263,18 @@ const BootloaderInstructions: React.FunctionComponent<BootloaderInstructionsProp
242263
</li>
243264
)}
244265

266+
{hubType === Hub.EV3 && (
267+
<li
268+
className={classNames(
269+
activeStep === 'press-power-button' && 'pb-active-step',
270+
)}
271+
>
272+
{i18n.translate(
273+
'instructionGroup.bootloaderMode.pressPowerButtonEV3',
274+
)}
275+
</li>
276+
)}
277+
245278
{recovery && !hubHasUSB(hubType) && (
246279
<li
247280
className={classNames(
@@ -262,7 +295,9 @@ const BootloaderInstructions: React.FunctionComponent<BootloaderInstructionsProp
262295
)}
263296
>
264297
{i18n.translate(
265-
'instructionGroup.bootloaderMode.releaseButton',
298+
hubType === Hub.EV3
299+
? 'instructionGroup.bootloaderMode.releaseButtonsEV3'
300+
: 'instructionGroup.bootloaderMode.releaseButton',
266301
{
267302
button,
268303
},
@@ -342,6 +377,13 @@ const BootloaderInstructions: React.FunctionComponent<BootloaderInstructionsProp
342377
1
343378
}
344379
>
380+
{hubType === Hub.EV3 && (
381+
<li>
382+
{i18n.translate(
383+
'instructionGroup.connect.selectEV3FirmwareType',
384+
)}
385+
</li>
386+
)}
345387
<li>
346388
{i18n.translate(
347389
'instructionGroup.connect.clickConnectAndFlash',
@@ -389,7 +431,7 @@ const BootloaderInstructions: React.FunctionComponent<BootloaderInstructionsProp
389431
</Callout>
390432
)}
391433

392-
{hubHasUSB(hubType) && isWindows() && (
434+
{hubHasUSB(hubType) && hubType !== Hub.EV3 && isWindows() && (
393435
<Callout intent={Intent.WARNING} icon={<WarningSign />}>
394436
{i18n.translate('warning.windows.message', {
395437
instructions: (

src/firmware/bootloaderInstructions/translations/en.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
},
1212
"button": {
1313
"bluetooth": "Bluetooth button",
14-
"power": "button"
14+
"power": "button",
15+
"right": "right button"
1516
},
1617
"light": {
1718
"bluetooth": "Bluetooth light",
@@ -37,10 +38,13 @@
3738
"waitForLight": "Wait for the {light} to start blinking {lightPattern}.",
3839
"waitAppConnect": "The app will automatically connect and start restoring the firmware.",
3940
"releaseButton": "Release the {button}.",
40-
"keepHolding": "Keep holding the {button} in the next steps. We'll tell you when to let go."
41+
"keepHolding": "Keep holding the {button} in the next steps. We'll tell you when to let go.",
42+
"pressPowerButtonEV3": "Press the center button to turn on the EV3.",
43+
"releaseButtonsEV3": "When the screen says \"Updating...\", release both buttons."
4144
},
4245
"connect": {
4346
"title": "Install:",
47+
"selectEV3FirmwareType": "Select the version of firmware to restore below.",
4448
"clickConnectAndFlash": "Click the {flashButton} button below.",
4549
"selectDevice": "In the pop-up dialog, select {deviceName} and click {connectButton}.",
4650
"connectButton": "Connect"

src/firmware/installPybricksDialog/InstallPybricksDialog.tsx

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// SPDX-License-Identifier: MIT
2-
// Copyright (c) 2022-2023 The Pybricks Authors
2+
// Copyright (c) 2022-2025 The Pybricks Authors
33

44
import './installPybricksDialog.scss';
55
import {
@@ -142,11 +142,6 @@ const UnsupportedHubs: React.FunctionComponent = () => {
142142
'selectHubPanel.notOnListButton.info.mindstorms.nxt',
143143
)}
144144
</li>
145-
<li>
146-
{i18n.translate(
147-
'selectHubPanel.notOnListButton.info.mindstorms.ev3',
148-
)}
149-
</li>
150145
</ul>
151146
<h4>
152147
{i18n.translate('selectHubPanel.notOnListButton.info.poweredUp.title')}

src/firmware/installPybricksDialog/actions.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// SPDX-License-Identifier: MIT
2-
// Copyright (c) 2022 The Pybricks Authors
2+
// Copyright (c) 2022-2025 The Pybricks Authors
33

44
import { createAction } from '../../actions';
55

@@ -8,7 +8,7 @@ export const firmwareInstallPybricksDialogShow = createAction(() => ({
88
type: 'firmware.installPybricksDialog.action.show',
99
}));
1010

11-
type FlashMethod = 'ble-lwp3-bootloader' | 'usb-lego-dfu';
11+
type FlashMethod = 'ble-lwp3-bootloader' | 'usb-lego-dfu' | 'usb-ev3';
1212

1313
/**
1414
* Action that indicates the user accepted the install Pybricks firmware dialog.

src/firmware/installPybricksDialog/translations/en.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,7 @@
1414
"sponsor": "sponsor"
1515
},
1616
"rcx": "RCX: Might work in streaming mode.",
17-
"nxt": "NXT: We have tested this. It could work!",
18-
"ev3": "EV3: Supported using Visual Studio Code."
17+
"nxt": "NXT: We have tested this. It could work!"
1918
},
2019
"poweredUp": {
2120
"title": "Other hubs",

0 commit comments

Comments
 (0)