mirror of
https://github.com/pybricks/pybricks-code.git
synced 2026-09-15 11:04:49 +00:00
firmware: implement EV3 official firmware restore
Basic support for restoring the official EV3 firmware. Is missing a video and needs a bunch of helpful error messages, but basic functionality is working.
This commit is contained in:
@@ -35,6 +35,7 @@
|
||||
"@types/react-transition-group": "^4.4.10",
|
||||
"@types/redux-logger": "^3.0.12",
|
||||
"@types/semver": "^7.5.6",
|
||||
"@types/w3c-web-hid": "^1.0.6",
|
||||
"@types/w3c-web-usb": "^1.0.10",
|
||||
"@types/web-bluetooth": "^0.0.20",
|
||||
"@types/web-locks-api": "^0.0.5",
|
||||
|
||||
@@ -465,3 +465,45 @@ export const firmwareDidRestoreOfficialEV3 = createAction(() => ({
|
||||
export const firmwareDidFailToRestoreOfficialEV3 = createAction(() => ({
|
||||
type: 'firmware.action.didFailToRestoreOfficialEV3',
|
||||
}));
|
||||
|
||||
/**
|
||||
* Low-level action to flash firmware to an EV3 hub.
|
||||
* @param firmware The firmware binary blob.
|
||||
*/
|
||||
export const firmwareFlashEV3 = createAction((firmware: ArrayBuffer) => ({
|
||||
type: 'firmware.action.flashEV3',
|
||||
firmware,
|
||||
}));
|
||||
|
||||
/**
|
||||
* Low-level action that indicates {@link firmwareFlashEV3} succeeded.
|
||||
*/
|
||||
export const firmwareDidFlashEV3 = createAction(() => ({
|
||||
type: 'firmware.action.didFlashEV3',
|
||||
}));
|
||||
|
||||
/**
|
||||
* Low-level action that indicates {@link firmwareFlashEV3} failed.
|
||||
*/
|
||||
export const firmwareDidFailToFlashEV3 = createAction(() => ({
|
||||
type: 'firmware.action.didFailToFlashEV3',
|
||||
}));
|
||||
|
||||
export const firmwareDidReceiveEV3Reply = createAction(
|
||||
(
|
||||
length: number,
|
||||
replyNumber: number,
|
||||
messageType: number,
|
||||
replyCommand: number,
|
||||
status: number,
|
||||
payload: ArrayBufferLike,
|
||||
) => ({
|
||||
type: 'firmware.action.didReceiveEV3Reply',
|
||||
length,
|
||||
replyNumber,
|
||||
messageType,
|
||||
replyCommand,
|
||||
status,
|
||||
payload,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2025 The Pybricks Authors
|
||||
|
||||
import { Intent } from '@blueprintjs/core';
|
||||
import { Error } from '@blueprintjs/icons';
|
||||
import React from 'react';
|
||||
import type { CreateToast } from '../../toasterTypes';
|
||||
import { useI18n } from './i18n';
|
||||
|
||||
const NoWebHid: React.FunctionComponent = () => {
|
||||
const i18n = useI18n();
|
||||
return (
|
||||
<>
|
||||
<p>{i18n.translate('noWebHid.message')}</p>
|
||||
<p>{i18n.translate('noWebHid.suggestion')}</p>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const noWebHid: CreateToast = (onAction) => ({
|
||||
message: <NoWebHid />,
|
||||
icon: <Error />,
|
||||
intent: Intent.DANGER,
|
||||
onDismiss: () => onAction('dismiss'),
|
||||
});
|
||||
@@ -1,10 +1,11 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2022 The Pybricks Authors
|
||||
// Copyright (c) 2022-2025 The Pybricks Authors
|
||||
|
||||
import { dfuError } from './DfuError';
|
||||
import { flashProgress } from './FlashProgress';
|
||||
import { noDfuHub } from './NoDfuHub';
|
||||
import { noDfuInterface } from './NoDfuInterface';
|
||||
import { noWebHid } from './NoWebHid';
|
||||
import { noWebUsb } from './NoWebUsb';
|
||||
import { releaseButton } from './ReleaseButton';
|
||||
|
||||
@@ -13,6 +14,7 @@ export default {
|
||||
flashProgress,
|
||||
noDfuHub,
|
||||
noDfuInterface,
|
||||
noWebHid,
|
||||
noWebUsb,
|
||||
releaseButton,
|
||||
};
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
"message": "This browser does not support WebUSB or WebUSB is not enabled.",
|
||||
"suggestion": "Use a supported browser such as Google Chrome or Microsoft Edge."
|
||||
},
|
||||
"noWebHid": {
|
||||
"message": "This browser does not support WebHID or WebHID is not enabled.",
|
||||
"suggestion": "Use a supported browser such as Google Chrome or Microsoft Edge."
|
||||
},
|
||||
"noDfuHub": {
|
||||
"message": "Could not find your hub?",
|
||||
"suggestion1": {
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -453,7 +453,8 @@ export const InstallPybricksDialog: React.FunctionComponent = () => {
|
||||
const inProgress = useSelector(
|
||||
(s) =>
|
||||
s.firmware.isFirmwareFlashUsbDfuInProgress ||
|
||||
s.firmware.isFirmwareRestoreOfficialDfuInProgress,
|
||||
s.firmware.isFirmwareRestoreOfficialDfuInProgress ||
|
||||
s.firmware.isFirmwareFlashEV3InProgress,
|
||||
);
|
||||
const dispatch = useDispatch();
|
||||
const [hubName, setHubName] = useState('');
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2021-2023 The Pybricks Authors
|
||||
// Copyright (c) 2021-2025 The Pybricks Authors
|
||||
|
||||
import { AnyAction } from 'redux';
|
||||
import {
|
||||
@@ -23,6 +23,7 @@ test('initial state', () => {
|
||||
"installPybricksDialog": {
|
||||
"isOpen": false,
|
||||
},
|
||||
"isFirmwareFlashEV3InProgress": false,
|
||||
"isFirmwareFlashUsbDfuInProgress": false,
|
||||
"isFirmwareRestoreOfficialDfuInProgress": false,
|
||||
"progress": null,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2021-2023 The Pybricks Authors
|
||||
// Copyright (c) 2021-2025 The Pybricks Authors
|
||||
|
||||
import { Reducer, combineReducers } from 'redux';
|
||||
import {
|
||||
@@ -9,10 +9,13 @@ import {
|
||||
didStart,
|
||||
firmwareDidFailToFlashUsbDfu,
|
||||
firmwareDidFailToRestoreOfficialDfu,
|
||||
firmwareDidFailToRestoreOfficialEV3,
|
||||
firmwareDidFlashUsbDfu,
|
||||
firmwareDidRestoreOfficialDfu,
|
||||
firmwareDidRestoreOfficialEV3,
|
||||
firmwareFlashUsbDfu,
|
||||
firmwareRestoreOfficialDfu,
|
||||
firmwareRestoreOfficialEV3,
|
||||
} from './actions';
|
||||
import dfuWindowsDriverInstallDialog from './dfuWindowsDriverInstallDialog/reducers';
|
||||
import installPybricksDialog from './installPybricksDialog/reducers';
|
||||
@@ -77,6 +80,21 @@ const isFirmwareRestoreOfficialDfuInProgress: Reducer<boolean> = (
|
||||
return state;
|
||||
};
|
||||
|
||||
const isFirmwareFlashEV3InProgress: Reducer<boolean> = (state = false, action) => {
|
||||
if (firmwareRestoreOfficialEV3.matches(action)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (firmwareDidRestoreOfficialEV3.matches(action)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (firmwareDidFailToRestoreOfficialEV3.matches(action)) {
|
||||
return false;
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
export default combineReducers({
|
||||
dfuWindowsDriverInstallDialog,
|
||||
installPybricksDialog,
|
||||
@@ -85,4 +103,5 @@ export default combineReducers({
|
||||
progress,
|
||||
isFirmwareFlashUsbDfuInProgress,
|
||||
isFirmwareRestoreOfficialDfuInProgress,
|
||||
isFirmwareFlashEV3InProgress,
|
||||
});
|
||||
|
||||
@@ -56,7 +56,8 @@ const RestoreFirmwarePanel: React.FunctionComponent = () => {
|
||||
const inProgress = useSelector(
|
||||
(s) =>
|
||||
s.firmware.isFirmwareFlashUsbDfuInProgress ||
|
||||
s.firmware.isFirmwareRestoreOfficialDfuInProgress,
|
||||
s.firmware.isFirmwareRestoreOfficialDfuInProgress ||
|
||||
s.firmware.isFirmwareFlashEV3InProgress,
|
||||
);
|
||||
const [ev3OfficialFirmwareVersion, setEv3OfficialFirmwareVersion] =
|
||||
useLocalStorage<EV3OfficialFirmwareVersion>(
|
||||
|
||||
+335
-4
@@ -22,6 +22,7 @@ import {
|
||||
call,
|
||||
cancel,
|
||||
delay,
|
||||
fork,
|
||||
getContext,
|
||||
put,
|
||||
race,
|
||||
@@ -61,9 +62,10 @@ import { BootloaderConnectionState } from '../lwp3-bootloader/reducers';
|
||||
import { compile, didCompile, didFailToCompile } from '../mpy/actions';
|
||||
import { RootState } from '../reducers';
|
||||
import { LegoUsbProductId, legoUsbVendorId } from '../usb';
|
||||
import { defined, ensureError, hex, maybe } from '../utils';
|
||||
import { assert, defined, ensureError, hex, maybe } from '../utils';
|
||||
import { crc32, fmod, sumComplement32 } from '../utils/math';
|
||||
import {
|
||||
EV3OfficialFirmwareVersion,
|
||||
FailToFinishReasonType,
|
||||
HubError,
|
||||
MetadataProblem,
|
||||
@@ -71,11 +73,16 @@ import {
|
||||
didFinish,
|
||||
didProgress,
|
||||
didStart,
|
||||
firmwareDidFailToFlashEV3,
|
||||
firmwareDidFailToFlashUsbDfu,
|
||||
firmwareDidFailToRestoreOfficialDfu,
|
||||
firmwareDidFailToRestoreOfficialEV3,
|
||||
firmwareDidFlashEV3,
|
||||
firmwareDidFlashUsbDfu,
|
||||
firmwareDidReceiveEV3Reply,
|
||||
firmwareDidRestoreOfficialDfu,
|
||||
firmwareDidRestoreOfficialEV3,
|
||||
firmwareFlashEV3,
|
||||
firmwareFlashUsbDfu,
|
||||
firmwareInstallPybricks,
|
||||
firmwareRestoreOfficialDfu,
|
||||
@@ -994,12 +1001,335 @@ function* handleRestoreOfficialDfu(
|
||||
}
|
||||
}
|
||||
|
||||
function* handleFlashEV3(action: ReturnType<typeof firmwareFlashEV3>): Generator {
|
||||
if (navigator.hid === undefined) {
|
||||
yield* put(alertsShowAlert('firmware', 'noWebHid'));
|
||||
yield* put(firmwareDidFailToFlashEV3());
|
||||
return;
|
||||
}
|
||||
|
||||
const [hidDevices, hidDevicesError] = yield* call(() =>
|
||||
maybe(
|
||||
navigator.hid.requestDevice({
|
||||
filters: [
|
||||
{
|
||||
vendorId: legoUsbVendorId,
|
||||
productId: LegoUsbProductId.Ev3Bootloader,
|
||||
},
|
||||
],
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
if (hidDevicesError) {
|
||||
// TODO: show info message with tips on how to get EV3 into bootloader mode
|
||||
console.error(hidDevicesError);
|
||||
yield* put(firmwareDidFailToFlashEV3());
|
||||
return;
|
||||
}
|
||||
|
||||
defined(hidDevices);
|
||||
|
||||
if (hidDevices.length === 0) {
|
||||
yield* put(
|
||||
alertsShowAlert('alerts', 'unexpectedError', {
|
||||
error: new Error('no EV3 HID devices found'),
|
||||
}),
|
||||
);
|
||||
yield* put(firmwareDidFailToFlashEV3());
|
||||
return;
|
||||
}
|
||||
|
||||
// Only flash one device.
|
||||
const hidDevice = hidDevices[0];
|
||||
|
||||
const exitStack: Array<() => Promise<void>> = [];
|
||||
function* cleanup() {
|
||||
for (const func of exitStack.reverse()) {
|
||||
yield* call(() => func());
|
||||
}
|
||||
}
|
||||
|
||||
const [, openError] = yield* call(() => maybe(hidDevice.open()));
|
||||
if (openError) {
|
||||
console.error(openError);
|
||||
yield* put(alertsShowAlert('alerts', 'unexpectedError', { error: openError }));
|
||||
yield* put(firmwareDidFailToFlashEV3());
|
||||
yield* cleanup();
|
||||
return;
|
||||
}
|
||||
|
||||
exitStack.push(() => hidDevice.close());
|
||||
|
||||
const inputChannel = eventChannel<HIDInputReportEvent>((emit) => {
|
||||
hidDevice.addEventListener('inputreport', emit);
|
||||
return () => hidDevice.removeEventListener('inputreport', emit);
|
||||
});
|
||||
exitStack.push(async () => inputChannel.close());
|
||||
|
||||
function* readInputReports(): SagaGenerator<void> {
|
||||
for (;;) {
|
||||
const event = yield* take(inputChannel);
|
||||
if (event.data.byteLength === 0) {
|
||||
continue; // ignore empty reports
|
||||
}
|
||||
|
||||
const length = event.data.getInt16(0, true);
|
||||
const replyNumber = event.data.getInt16(2, true);
|
||||
const messageType = event.data.getUint8(4);
|
||||
const replyCommand = event.data.getUint8(5);
|
||||
const status = event.data.getUint8(6);
|
||||
const payload = event.data.buffer.slice(7, 7 + length + 2);
|
||||
|
||||
console.debug(
|
||||
`EV3 reply: length=${length}, replyNumber=${replyNumber}, messageType=${messageType}, replyCommand=${replyCommand}, status=${status}, payload=${payload}`,
|
||||
);
|
||||
|
||||
yield* put(
|
||||
firmwareDidReceiveEV3Reply(
|
||||
length,
|
||||
replyNumber,
|
||||
messageType,
|
||||
replyCommand,
|
||||
status,
|
||||
payload,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const readInputReportsTask = yield* fork(readInputReports);
|
||||
exitStack.push(async () => readInputReportsTask.cancel());
|
||||
|
||||
function* sendCommand(
|
||||
command: number,
|
||||
payload?: Uint8Array,
|
||||
): SagaGenerator<[DataView | undefined, Error | undefined]> {
|
||||
console.debug(`EV3 send: command=${command}, payload=${payload}`);
|
||||
|
||||
const dataBuffer = new Uint8Array((payload?.byteLength ?? 0) + 6);
|
||||
const data = new DataView(dataBuffer.buffer);
|
||||
|
||||
data.setInt16(0, (payload?.byteLength ?? 0) + 4, true);
|
||||
data.setInt16(2, 0, true); // TODO: reply number
|
||||
data.setUint8(4, 0x01); // system command w/ reply
|
||||
data.setUint8(5, command);
|
||||
if (payload) {
|
||||
dataBuffer.set(payload, 6);
|
||||
}
|
||||
|
||||
const [, sendError] = yield* call(() => maybe(hidDevice.sendReport(0, data)));
|
||||
|
||||
if (sendError) {
|
||||
return [undefined, sendError];
|
||||
}
|
||||
|
||||
const { reply, timeout } = yield* race({
|
||||
reply: take(firmwareDidReceiveEV3Reply),
|
||||
timeout: delay(5000),
|
||||
});
|
||||
if (timeout) {
|
||||
return [undefined, new Error('Timeout waiting for EV3 reply')];
|
||||
}
|
||||
|
||||
defined(reply);
|
||||
|
||||
if (reply.replyCommand !== command) {
|
||||
return [
|
||||
undefined,
|
||||
new Error(
|
||||
`EV3 reply command mismatch: expected ${command}, got ${reply.replyCommand}`,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
if (reply.status !== 0) {
|
||||
return [
|
||||
undefined,
|
||||
new Error(
|
||||
`EV3 reply status error: ${reply.status} for command ${command}`,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
return [new DataView(reply.payload), undefined];
|
||||
}
|
||||
|
||||
const [version, versionError] = yield* sendCommand(0xf6); // get version
|
||||
|
||||
if (versionError) {
|
||||
yield* put(
|
||||
alertsShowAlert('alerts', 'unexpectedError', {
|
||||
error: ensureError(versionError),
|
||||
}),
|
||||
);
|
||||
yield* put(firmwareDidFailToFlashEV3());
|
||||
yield* cleanup();
|
||||
return;
|
||||
}
|
||||
|
||||
defined(version);
|
||||
|
||||
console.debug(
|
||||
`EV3 bootloader version: ${version.getUint32(
|
||||
0,
|
||||
true,
|
||||
)}, HW version: ${version.getUint32(4, true)}`,
|
||||
);
|
||||
|
||||
// FIXME: should be called much earlier.
|
||||
yield* put(didStart());
|
||||
|
||||
const sectorSize = 64 * 1024; // flash memory sector size
|
||||
const maxPayloadSize = 1018; // maximum payload size for EV3 commands
|
||||
|
||||
for (let i = 0; i < action.firmware.byteLength; i += sectorSize) {
|
||||
const sectorData = action.firmware.slice(i, i + sectorSize);
|
||||
assert(sectorData.byteLength <= sectorSize, 'sector data too large');
|
||||
|
||||
const erasePayload = new DataView(new ArrayBuffer(8));
|
||||
erasePayload.setUint32(0, i, true);
|
||||
erasePayload.setUint32(4, sectorData.byteLength, true);
|
||||
const [, eraseError] = yield* sendCommand(
|
||||
0xf0,
|
||||
new Uint8Array(erasePayload.buffer),
|
||||
);
|
||||
|
||||
if (eraseError) {
|
||||
yield* put(
|
||||
alertsShowAlert('alerts', 'unexpectedError', {
|
||||
error: eraseError,
|
||||
}),
|
||||
);
|
||||
// FIXME: should have a better error reason
|
||||
yield* put(didFailToFinish(FailToFinishReasonType.Unknown));
|
||||
yield* put(firmwareDidFailToFlashEV3());
|
||||
yield* cleanup();
|
||||
return;
|
||||
}
|
||||
|
||||
for (let j = 0; j < sectorData.byteLength; j += maxPayloadSize) {
|
||||
const payload = sectorData.slice(j, j + maxPayloadSize);
|
||||
|
||||
const [, sendError] = yield* sendCommand(0xf2, new Uint8Array(payload));
|
||||
if (sendError) {
|
||||
yield* put(
|
||||
alertsShowAlert('alerts', 'unexpectedError', {
|
||||
error: sendError,
|
||||
}),
|
||||
);
|
||||
// FIXME: should have a better error reason
|
||||
yield* put(didFailToFinish(FailToFinishReasonType.Unknown));
|
||||
yield* put(firmwareDidFailToFlashEV3());
|
||||
yield* cleanup();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
yield* put(
|
||||
didProgress((i + sectorData.byteLength) / action.firmware.byteLength),
|
||||
);
|
||||
|
||||
yield* put(
|
||||
alertsShowAlert(
|
||||
'firmware',
|
||||
'flashProgress',
|
||||
{
|
||||
action: 'flash',
|
||||
progress: (i + sectorData.byteLength) / action.firmware.byteLength,
|
||||
},
|
||||
firmwareBleProgressToastId,
|
||||
true,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
yield* put(
|
||||
alertsShowAlert(
|
||||
'firmware',
|
||||
'flashProgress',
|
||||
{
|
||||
action: 'flash',
|
||||
progress: 1,
|
||||
},
|
||||
firmwareBleProgressToastId,
|
||||
true,
|
||||
),
|
||||
);
|
||||
|
||||
const [, rebootError] = yield* sendCommand(0xf4); // start app
|
||||
if (rebootError) {
|
||||
// FIXME: should have a better error reason
|
||||
yield* put(didFailToFinish(FailToFinishReasonType.Unknown));
|
||||
yield* put(firmwareDidFailToFlashEV3());
|
||||
yield* cleanup();
|
||||
return;
|
||||
}
|
||||
|
||||
yield* put(didFinish());
|
||||
|
||||
yield* cleanup();
|
||||
|
||||
yield* put(firmwareDidFlashEV3());
|
||||
}
|
||||
|
||||
function getUrlForEV3FirmwareVersion(version: EV3OfficialFirmwareVersion): URL {
|
||||
switch (version) {
|
||||
case EV3OfficialFirmwareVersion.home:
|
||||
return new URL('./assets/EV3_Firmware_V1.09H.bin', import.meta.url);
|
||||
case EV3OfficialFirmwareVersion.education:
|
||||
return new URL('./assets/ev3_firmware_v1.09e.bin', import.meta.url);
|
||||
case EV3OfficialFirmwareVersion.makecode:
|
||||
return new URL('./assets/ev3-image-1.10e.bin', import.meta.url);
|
||||
default:
|
||||
throw new Error(`unsupported EV3 firmware version: ${version}`);
|
||||
}
|
||||
}
|
||||
|
||||
function* handleRestoreOfficialEV3(
|
||||
action: ReturnType<typeof firmwareRestoreOfficialEV3>,
|
||||
): Generator {
|
||||
action;
|
||||
alert('Flashing via EV3 USB is not implemented yet');
|
||||
yield* put(firmwareDidFailToRestoreOfficialEV3());
|
||||
try {
|
||||
const url = getUrlForEV3FirmwareVersion(action.version);
|
||||
|
||||
const response = yield* call(() => fetch(url));
|
||||
|
||||
if (!response.ok) {
|
||||
// TODO: replace with proper alert
|
||||
// istanbul ignore if
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
console.error(response);
|
||||
}
|
||||
throw new Error('failed to fetch');
|
||||
}
|
||||
|
||||
const firmwareBlob = yield* call(() => response.blob());
|
||||
const firmware = yield* call(() => firmwareBlob.arrayBuffer());
|
||||
|
||||
yield* put(firmwareFlashEV3(firmware));
|
||||
|
||||
const { didFailToFlash } = yield* race({
|
||||
didFlash: take(firmwareDidFlashEV3),
|
||||
didFailToFlash: take(firmwareDidFailToFlashEV3),
|
||||
});
|
||||
|
||||
if (didFailToFlash) {
|
||||
yield* put(firmwareDidFailToRestoreOfficialEV3());
|
||||
return;
|
||||
}
|
||||
|
||||
yield* put(firmwareDidRestoreOfficialEV3());
|
||||
} catch (err) {
|
||||
// istanbul ignore if
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
yield* put(
|
||||
alertsShowAlert('alerts', 'unexpectedError', { error: ensureError(err) }),
|
||||
);
|
||||
yield* put(firmwareDidFailToRestoreOfficialEV3());
|
||||
}
|
||||
}
|
||||
|
||||
export default function* (): Generator {
|
||||
@@ -1007,5 +1337,6 @@ export default function* (): Generator {
|
||||
yield* takeEvery(firmwareFlashUsbDfu, handleFlashUsbDfu);
|
||||
yield* takeEvery(firmwareInstallPybricks, handleInstallPybricks);
|
||||
yield* takeEvery(firmwareRestoreOfficialDfu, handleRestoreOfficialDfu);
|
||||
yield* takeEvery(firmwareFlashEV3, handleFlashEV3);
|
||||
yield* takeEvery(firmwareRestoreOfficialEV3, handleRestoreOfficialEV3);
|
||||
}
|
||||
|
||||
+2
-1
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2023 The Pybricks Authors
|
||||
// Copyright (c) 2023-2025 The Pybricks Authors
|
||||
|
||||
import type { SerializableStateInvariantMiddlewareOptions } from '@reduxjs/toolkit';
|
||||
|
||||
@@ -13,6 +13,7 @@ export const serializableCheck: SerializableStateInvariantMiddlewareOptions = {
|
||||
// contain ArrayBuffer, Blob or DataView
|
||||
'data',
|
||||
'file',
|
||||
'firmware',
|
||||
'firmwareZip',
|
||||
'payload',
|
||||
'value',
|
||||
|
||||
@@ -2998,6 +2998,7 @@ __metadata:
|
||||
"@types/react-transition-group": ^4.4.10
|
||||
"@types/redux-logger": ^3.0.12
|
||||
"@types/semver": ^7.5.6
|
||||
"@types/w3c-web-hid": ^1.0.6
|
||||
"@types/w3c-web-usb": ^1.0.10
|
||||
"@types/web-bluetooth": ^0.0.20
|
||||
"@types/web-locks-api": ^0.0.5
|
||||
@@ -5464,6 +5465,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/w3c-web-hid@npm:^1.0.6":
|
||||
version: 1.0.6
|
||||
resolution: "@types/w3c-web-hid@npm:1.0.6"
|
||||
checksum: 14773befa9c458b3459cdb530a8269937e623e6b72c6bd2d7f88b42f8d47c02d8a64ddc98f79c81c930b6eadf1dc1c94917b553ead72acc13c8406f65310c85d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/w3c-web-usb@npm:^1.0.10":
|
||||
version: 1.0.10
|
||||
resolution: "@types/w3c-web-usb@npm:1.0.10"
|
||||
|
||||
Reference in New Issue
Block a user