mirror of
https://github.com/pybricks/pybricks-code.git
synced 2026-09-12 09:36:27 +00:00
firmware: implement flashing via USB DFU
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
### Added
|
||||
- Added better error message when no files to backup ([support#681]).
|
||||
- Added multi-step firmware flashing dialog.
|
||||
- Added support for flashing firmware via USB DFU.
|
||||
|
||||
### Fixed
|
||||
- Fixed deleting files that are not open in the editor.
|
||||
|
||||
+3
-1
@@ -32,6 +32,7 @@
|
||||
"@types/react-splitter-layout": "^3.0.2",
|
||||
"@types/redux-logger": "^3.0.9",
|
||||
"@types/semver": "^7.3.10",
|
||||
"@types/w3c-web-usb": "^1.0.6",
|
||||
"@types/web-bluetooth": "^0.0.15",
|
||||
"@types/web-locks-api": "^0.0.2",
|
||||
"@types/wicg-file-system-access": "^2020.9.5",
|
||||
@@ -54,6 +55,7 @@
|
||||
"dexie": "^3.2.2",
|
||||
"dexie-observable": "^4.0.0-beta.13",
|
||||
"dexie-react-hooks": "^1.1.1",
|
||||
"dfu": "^0.1.5",
|
||||
"dotenv": "^16.0.1",
|
||||
"dotenv-expand": "^8.0.3",
|
||||
"fake-indexeddb": "^4.0.0",
|
||||
@@ -192,7 +194,7 @@
|
||||
"^(?!.*\\.(js|jsx|mjs|cjs|ts|tsx|css|json)$)": "<rootDir>/config/jest/fileTransform.mjs"
|
||||
},
|
||||
"transformIgnorePatterns": [
|
||||
"[/\\\\]node_modules[/\\\\](?!(monaco-editor|react-monaco-editor)[/\\\\]).+\\.(js|jsx|mjs|cjs|ts|tsx)$",
|
||||
"[/\\\\]node_modules[/\\\\](?!(monaco-editor|react-monaco-editor|nanoevents)[/\\\\]).+\\.(js|jsx|mjs|cjs|ts|tsx)$",
|
||||
"^.+\\.module\\.(css|sass|scss)$"
|
||||
],
|
||||
"modulePaths": [],
|
||||
|
||||
@@ -5,6 +5,7 @@ import { IToastProps } from '@blueprintjs/core';
|
||||
import alerts from './alerts/alerts';
|
||||
import ble from './ble/alerts';
|
||||
import explorer from './explorer/alerts';
|
||||
import firmware from './firmware/alerts';
|
||||
import { CreateToast } from './i18nToaster';
|
||||
|
||||
/** This collects alerts from all of the subsystems of the app */
|
||||
@@ -12,6 +13,7 @@ const alertDomains = {
|
||||
alerts,
|
||||
ble,
|
||||
explorer,
|
||||
firmware,
|
||||
};
|
||||
|
||||
/** Gets the type of available alert domains. */
|
||||
|
||||
@@ -35,6 +35,9 @@ export const pybricksGitterUrl = 'https://gitter.im/pybricks/community';
|
||||
export const pybricksBluetoothTroubleshootingUrl =
|
||||
'https://github.com/pybricks/support/discussions/270';
|
||||
|
||||
export const pybricksUsbDfuTroubleshootingUrl =
|
||||
'https://github.com/pybricks/support/discussions/688';
|
||||
|
||||
/** Pybricks copyright statement. */
|
||||
export const pybricksCopyright = 'Copyright (c) 2020-2022 The Pybricks Authors';
|
||||
|
||||
|
||||
@@ -346,6 +346,35 @@ function didFailToFinishCreator(
|
||||
*/
|
||||
export const didFailToFinish = createAction(didFailToFinishCreator);
|
||||
|
||||
/**
|
||||
* Low-level action to flash firmware using LEGO's DFU over USB.
|
||||
* @param data The firmware zip file data.
|
||||
* @param hubName A custom hub name or an empty string to use the default name.
|
||||
*/
|
||||
export const firmwareFlashUsbDfu = createAction(
|
||||
(data: ArrayBuffer, hubName: string) => ({
|
||||
type: 'firmware.action.flashUsbDfu',
|
||||
data,
|
||||
hubName,
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* Low-level action that indicates {@link firmwareFlashUsbDfu} succeeded.
|
||||
*/
|
||||
export const firmwareDidFlashUsbDfu = createAction(() => ({
|
||||
type: 'firmware.action.didFlashUsbDfu',
|
||||
}));
|
||||
|
||||
/**
|
||||
* Low-level action that indicates {@link firmwareFlashUsbDfu} failed.
|
||||
*/
|
||||
export const firmwareDidFailToFlashUsbDfu = createAction(() => ({
|
||||
type: 'firmware.action.didFailToFlashUsbDfu',
|
||||
}));
|
||||
|
||||
// High-level actions
|
||||
|
||||
/**
|
||||
* Action that triggers the install Pybricks firmware saga.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2022 The Pybricks Authors
|
||||
|
||||
import { Intent } from '@blueprintjs/core';
|
||||
import React from 'react';
|
||||
import { CreateToast } from '../../i18nToaster';
|
||||
import { I18nId, useI18n } from './i18n';
|
||||
|
||||
const FirmwareMismatch: React.VoidFunctionComponent = () => {
|
||||
const i18n = useI18n();
|
||||
return <p>{i18n.translate(I18nId.FirmwareMismatchMessage)}</p>;
|
||||
};
|
||||
|
||||
export const firmwareMismatch: CreateToast = (onAction) => {
|
||||
return {
|
||||
message: <FirmwareMismatch />,
|
||||
icon: 'error',
|
||||
intent: Intent.DANGER,
|
||||
onDismiss: () => onAction('dismiss'),
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2022 The Pybricks Authors
|
||||
|
||||
import { AnchorButton, Intent } from '@blueprintjs/core';
|
||||
import React from 'react';
|
||||
import { pybricksUsbDfuTroubleshootingUrl } from '../../app/constants';
|
||||
import { CreateToast } from '../../i18nToaster';
|
||||
import ExternalLinkIcon from '../../utils/ExternalLinkIcon';
|
||||
import { isLinux, isWindows } from '../../utils/os';
|
||||
import { I18nId, useI18n } from './i18n';
|
||||
|
||||
const NoDfuHub: React.VoidFunctionComponent = () => {
|
||||
const i18n = useI18n();
|
||||
|
||||
return (
|
||||
<>
|
||||
<p>{i18n.translate(I18nId.NoDfuHubMessage)}</p>
|
||||
|
||||
{isWindows() && <p>{i18n.translate(I18nId.NoDfuHubSuggestion1Windows)}</p>}
|
||||
{isLinux() && <p>{i18n.translate(I18nId.NoDfuHubSuggestion1Linux)}</p>}
|
||||
|
||||
<p>{i18n.translate(I18nId.NoDfuHubSuggestion2)}</p>
|
||||
|
||||
<AnchorButton
|
||||
icon="help"
|
||||
href={pybricksUsbDfuTroubleshootingUrl}
|
||||
target="_blank"
|
||||
>
|
||||
{i18n.translate(I18nId.NoDfuHubTroubleshootButton)}
|
||||
<ExternalLinkIcon />
|
||||
</AnchorButton>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const noDfuHub: CreateToast = (onAction) => {
|
||||
return {
|
||||
message: <NoDfuHub />,
|
||||
icon: 'info-sign',
|
||||
intent: Intent.PRIMARY,
|
||||
onDismiss: () => onAction('dismiss'),
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2022 The Pybricks Authors
|
||||
|
||||
import { Intent } from '@blueprintjs/core';
|
||||
import React from 'react';
|
||||
import { CreateToast } from '../../i18nToaster';
|
||||
import { I18nId, useI18n } from './i18n';
|
||||
|
||||
const NoDfuInterface: React.VoidFunctionComponent = () => {
|
||||
const i18n = useI18n();
|
||||
return <p>{i18n.translate(I18nId.NoDfuInterfaceMessage)}</p>;
|
||||
};
|
||||
|
||||
export const noDfuInterface: CreateToast = (onAction) => {
|
||||
return {
|
||||
message: <NoDfuInterface />,
|
||||
icon: 'error',
|
||||
intent: Intent.DANGER,
|
||||
onDismiss: () => onAction('dismiss'),
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2022 The Pybricks Authors
|
||||
|
||||
import { Intent } from '@blueprintjs/core';
|
||||
import React from 'react';
|
||||
import { CreateToast } from '../../i18nToaster';
|
||||
import { I18nId, useI18n } from './i18n';
|
||||
|
||||
const NoWebUsb: React.VoidFunctionComponent = () => {
|
||||
const i18n = useI18n();
|
||||
return (
|
||||
<>
|
||||
<p>{i18n.translate(I18nId.NoWebUsbMessage)}</p>
|
||||
<p>{i18n.translate(I18nId.NoWebUsbSuggestion)}</p>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const noWebUsb: CreateToast = (onAction) => {
|
||||
return {
|
||||
message: <NoWebUsb />,
|
||||
icon: 'error',
|
||||
intent: Intent.DANGER,
|
||||
onDismiss: () => onAction('dismiss'),
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2022 The Pybricks Authors
|
||||
|
||||
import { lookup } from '../../../test';
|
||||
import { I18nId } from './i18n';
|
||||
import en from './translations/en.json';
|
||||
|
||||
describe('Ensure .json file has matches for I18nId', () => {
|
||||
test.each(Object.values(I18nId))('%s', (id) => {
|
||||
expect(lookup(en, id)).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2022 The Pybricks Authors
|
||||
|
||||
import { I18n, useI18n as useShopifyI18n } from '@shopify/react-i18n';
|
||||
|
||||
export function useI18n(): I18n {
|
||||
// istanbul ignore next: babel-loader rewrites this line
|
||||
const [i18n] = useShopifyI18n();
|
||||
return i18n;
|
||||
}
|
||||
|
||||
export enum I18nId {
|
||||
NoWebUsbMessage = 'noWebUsb.message',
|
||||
NoWebUsbSuggestion = 'noWebUsb.suggestion',
|
||||
NoDfuHubMessage = 'noDfuHub.message',
|
||||
NoDfuHubSuggestion1Windows = 'noDfuHub.suggestion1.windows',
|
||||
NoDfuHubSuggestion1Linux = 'noDfuHub.suggestion1.linux',
|
||||
NoDfuHubSuggestion2 = 'noDfuHub.suggestion2',
|
||||
NoDfuHubTroubleshootButton = 'noDfuHub.troubleshootButton',
|
||||
NoDfuInterfaceMessage = 'noDfuInterface.message',
|
||||
FirmwareMismatchMessage = 'firmwareMismatch.message',
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2022 The Pybricks Authors
|
||||
|
||||
import { firmwareMismatch } from './FirmwareMismatch';
|
||||
import { noDfuHub } from './NoDfuHub';
|
||||
import { noDfuInterface } from './NoDfuInterface';
|
||||
import { noWebUsb } from './NoWebUsb';
|
||||
|
||||
export default {
|
||||
firmwareMismatch,
|
||||
noDfuHub,
|
||||
noDfuInterface,
|
||||
noWebUsb,
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"noWebUsb": {
|
||||
"message": "This browser does not support Web USB or Web USB is not enabled.",
|
||||
"suggestion": "Use a supported browser such as Google Chrome or Microsoft Edge."
|
||||
},
|
||||
"noDfuHub": {
|
||||
"message": "Could not find your hub?",
|
||||
"suggestion1": {
|
||||
"windows": "You may need to manually install a USB driver before you can connect to your hub.",
|
||||
"linux": "You may need to add udev rules before you can connect to your hub."
|
||||
},
|
||||
"suggestion2": "Click the button below for more information.",
|
||||
"troubleshootButton": "Troubleshooting Tips"
|
||||
},
|
||||
"noDfuInterface": {
|
||||
"message": "This is very unusual. The USB device did not contain the expected interface."
|
||||
},
|
||||
"firmwareMismatch": {
|
||||
"message": "Cannot flash firmware. The firmware file is for a different kind of hub."
|
||||
}
|
||||
}
|
||||
+182
-6
@@ -10,6 +10,7 @@ import {
|
||||
import cityHubZip from '@pybricks/firmware/build/cityhub.zip';
|
||||
import moveHubZip from '@pybricks/firmware/build/movehub.zip';
|
||||
import technicHubZip from '@pybricks/firmware/build/technichub.zip';
|
||||
import { WebDFU } from 'dfu';
|
||||
import { AnyAction } from 'redux';
|
||||
import { ActionPattern } from 'redux-saga/effects';
|
||||
import {
|
||||
@@ -25,6 +26,7 @@ import {
|
||||
take,
|
||||
takeEvery,
|
||||
} from 'typed-redux-saga/macro';
|
||||
import { alertsShowAlert } from '../alerts/actions';
|
||||
import {
|
||||
fileStorageDidFailToReadFile,
|
||||
fileStorageDidReadFile,
|
||||
@@ -55,8 +57,9 @@ import { MaxProgramFlashSize, Result } from '../lwp3-bootloader/protocol';
|
||||
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 { fmod, sumComplement32 } from '../utils/math';
|
||||
import { crc32, fmod, sumComplement32 } from '../utils/math';
|
||||
import { isAndroid } from '../utils/os';
|
||||
import {
|
||||
FailToFinishReasonType,
|
||||
@@ -66,6 +69,9 @@ import {
|
||||
didFinish,
|
||||
didProgress,
|
||||
didStart,
|
||||
firmwareDidFailToFlashUsbDfu,
|
||||
firmwareDidFlashUsbDfu,
|
||||
firmwareFlashUsbDfu,
|
||||
firmwareInstallPybricks,
|
||||
flashFirmware,
|
||||
} from './actions';
|
||||
@@ -193,7 +199,12 @@ function* loadFirmware(
|
||||
} else {
|
||||
yield* put(didFailToFinish(FailToFinishReasonType.Unknown, readerErr));
|
||||
}
|
||||
|
||||
// FIXME: we should return error/throw instead
|
||||
yield* disconnectAndCancel();
|
||||
|
||||
// istanbul ignore next: needed for typescript flow
|
||||
throw new Error('unreachable');
|
||||
}
|
||||
|
||||
defined(reader);
|
||||
@@ -220,7 +231,12 @@ function* loadFirmware(
|
||||
MetadataProblem.NotSupported,
|
||||
),
|
||||
);
|
||||
|
||||
// FIXME: we should return error/throw instead
|
||||
yield* disconnectAndCancel();
|
||||
|
||||
// istanbul ignore next: needed for typescript flow
|
||||
throw new Error('unreachable');
|
||||
}
|
||||
|
||||
yield* put(
|
||||
@@ -232,8 +248,12 @@ function* loadFirmware(
|
||||
});
|
||||
|
||||
if (mpyFail) {
|
||||
// FIXME: we should return error/throw instead
|
||||
yield* put(didFailToFinish(FailToFinishReasonType.FailedToCompile));
|
||||
yield* disconnectAndCancel();
|
||||
|
||||
// istanbul ignore next: needed for typescript flow
|
||||
throw new Error('unreachable');
|
||||
}
|
||||
|
||||
defined(mpy);
|
||||
@@ -246,8 +266,12 @@ function* loadFirmware(
|
||||
const firmwareView = new DataView(firmware.buffer);
|
||||
|
||||
if (firmware.length > metadata['max-firmware-size']) {
|
||||
// FIXME: we should return error/throw instead
|
||||
yield* put(didFailToFinish(FailToFinishReasonType.FirmwareSize));
|
||||
yield* disconnectAndCancel();
|
||||
|
||||
// istanbul ignore next: needed for typescript flow
|
||||
throw new Error('unreachable');
|
||||
}
|
||||
|
||||
firmware.set(firmwareBase);
|
||||
@@ -262,7 +286,23 @@ function* loadFirmware(
|
||||
}
|
||||
}
|
||||
|
||||
if (metadata['checksum-type'] !== 'sum') {
|
||||
const checksum = (function () {
|
||||
switch (metadata['checksum-type']) {
|
||||
case 'sum':
|
||||
return sumComplement32(
|
||||
firmwareIterator(firmwareView, metadata['max-firmware-size']),
|
||||
);
|
||||
case 'crc32':
|
||||
return crc32(
|
||||
firmwareIterator(firmwareView, metadata['max-firmware-size']),
|
||||
);
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
})();
|
||||
|
||||
if (!checksum) {
|
||||
// FIXME: we should return error/throw instead
|
||||
yield* put(
|
||||
didFailToFinish(
|
||||
FailToFinishReasonType.BadMetadata,
|
||||
@@ -271,11 +311,10 @@ function* loadFirmware(
|
||||
),
|
||||
);
|
||||
yield* disconnectAndCancel();
|
||||
}
|
||||
|
||||
const checksum = sumComplement32(
|
||||
firmwareIterator(firmwareView, metadata['max-firmware-size']),
|
||||
);
|
||||
// istanbul ignore next: needed for typescript flow
|
||||
throw new Error('unreachable');
|
||||
}
|
||||
|
||||
firmwareView.setUint32(checksumOffset, checksum, true);
|
||||
|
||||
@@ -512,6 +551,139 @@ function* handleFlashFirmware(action: ReturnType<typeof flashFirmware>): Generat
|
||||
}
|
||||
}
|
||||
|
||||
/** Maps USB Product ID to LWP3 hub type ID */
|
||||
const productIdMap: ReadonlyMap<LegoUsbProductId, HubType> = new Map([
|
||||
[LegoUsbProductId.SpikePrimeBootloader, HubType.PrimeHub],
|
||||
[LegoUsbProductId.SpikeEssentialBootloader, HubType.EssentialHub],
|
||||
[LegoUsbProductId.MindstormsRobotInventorBootloader, HubType.PrimeHub],
|
||||
]);
|
||||
|
||||
// currently all hubs use the same start address
|
||||
const dfuFirmwareStartAddress = 0x08008000;
|
||||
|
||||
function* handleFlashUsbDfu(action: ReturnType<typeof firmwareFlashUsbDfu>): Generator {
|
||||
const defer = new Array<() => void>();
|
||||
|
||||
try {
|
||||
// not all web browsers support Web USB
|
||||
if (!navigator.usb) {
|
||||
yield* put(alertsShowAlert('firmware', 'noWebUsb'));
|
||||
yield* put(firmwareDidFailToFlashUsbDfu());
|
||||
return;
|
||||
}
|
||||
|
||||
const device = yield* call(() =>
|
||||
navigator.usb
|
||||
.requestDevice({
|
||||
filters: [
|
||||
{
|
||||
vendorId: legoUsbVendorId,
|
||||
productId: LegoUsbProductId.SpikePrimeBootloader,
|
||||
},
|
||||
{
|
||||
vendorId: legoUsbVendorId,
|
||||
productId: LegoUsbProductId.SpikeEssentialBootloader,
|
||||
},
|
||||
{
|
||||
vendorId: legoUsbVendorId,
|
||||
productId:
|
||||
LegoUsbProductId.MindstormsRobotInventorBootloader,
|
||||
},
|
||||
],
|
||||
})
|
||||
.catch((err) => {
|
||||
if (
|
||||
err instanceof DOMException &&
|
||||
err.code === DOMException.NOT_FOUND_ERR
|
||||
) {
|
||||
// user clicked cancel button
|
||||
return undefined;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}),
|
||||
);
|
||||
|
||||
if (!device) {
|
||||
yield* put(alertsShowAlert('firmware', 'noDfuHub'));
|
||||
yield* put(firmwareDidFailToFlashUsbDfu());
|
||||
return;
|
||||
}
|
||||
|
||||
const dfu = new WebDFU(
|
||||
device,
|
||||
// forceInterfacesName is needed to get the flash layout map
|
||||
{ forceInterfacesName: true },
|
||||
{
|
||||
info: console.debug,
|
||||
warning: console.warn,
|
||||
progress: (progress, total) => {
|
||||
// TODO: bind to eventChannel and dispatch progress actions
|
||||
console.log(progress, total);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
yield* call(() => dfu.init());
|
||||
|
||||
// we want the interface with alt=0
|
||||
const ifaceIndex = dfu.interfaces.findIndex(
|
||||
(i) => i.alternate.alternateSetting === 0,
|
||||
);
|
||||
|
||||
if (ifaceIndex === -1) {
|
||||
yield* put(alertsShowAlert('firmware', 'noDfuInterface'));
|
||||
yield* put(firmwareDidFailToFlashUsbDfu());
|
||||
return;
|
||||
}
|
||||
|
||||
yield* call(() => dfu.connect(ifaceIndex));
|
||||
|
||||
defer.push(() => dfu.close());
|
||||
|
||||
const { firmware, deviceId } = yield* loadFirmware(
|
||||
action.data,
|
||||
undefined,
|
||||
action.hubName,
|
||||
);
|
||||
|
||||
if (deviceId !== productIdMap.get(device.productId)) {
|
||||
yield* put(alertsShowAlert('firmware', 'firmwareMismatch'));
|
||||
yield* put(firmwareDidFailToFlashUsbDfu());
|
||||
return;
|
||||
}
|
||||
|
||||
dfu.dfuseStartAddress = dfuFirmwareStartAddress;
|
||||
const writeProc = dfu.write(1024, firmware, true);
|
||||
|
||||
writeProc.events.on('error', console.error);
|
||||
|
||||
// REVISIT: we could possibly race the 'write/end' and 'error' events
|
||||
// here instead of waiting for disconnect
|
||||
|
||||
// this is a bit of a hack, but the hub resets when flashing is done
|
||||
// so we get a disconnect event unless there was an error, so the user
|
||||
// will probably see the timeout error instead of the underlying error
|
||||
yield* call(() => dfu.waitDisconnected(30000));
|
||||
|
||||
yield* put(firmwareDidFlashUsbDfu());
|
||||
} catch (err) {
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
yield* put(
|
||||
alertsShowAlert('alerts', 'unexpectedError', { error: ensureError(err) }),
|
||||
);
|
||||
|
||||
yield* put(firmwareDidFailToFlashUsbDfu());
|
||||
} finally {
|
||||
while (defer.length !== 0) {
|
||||
defer.pop()?.();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function* handleInstallPybricks(): Generator {
|
||||
yield* put(firmwareInstallPybricksDialogShow());
|
||||
const { accepted, canceled } = yield* race({
|
||||
@@ -535,10 +707,14 @@ function* handleInstallPybricks(): Generator {
|
||||
),
|
||||
);
|
||||
break;
|
||||
case 'usb-lego-dfu':
|
||||
yield* put(firmwareFlashUsbDfu(accepted.firmwareZip, accepted.hubName));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
export default function* (): Generator {
|
||||
yield* takeEvery(flashFirmware, handleFlashFirmware);
|
||||
yield* takeEvery(firmwareFlashUsbDfu, handleFlashUsbDfu);
|
||||
yield* takeEvery(firmwareInstallPybricks, handleInstallPybricks);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2022 The Pybricks Authors
|
||||
|
||||
// https://github.com/pybricks/technical-info/blob/master/assigned-numbers.md#usb
|
||||
|
||||
/** Official LEGO USB Vendor ID (VID) */
|
||||
export const legoUsbVendorId = 0x0694;
|
||||
|
||||
/** Official LEGO USB Product IDs (PID) */
|
||||
export enum LegoUsbProductId {
|
||||
/** MINDSTORMS RCX IR Tower. */
|
||||
RcxIrTower = 0x0001,
|
||||
/** MINDSTORMS NXT */
|
||||
Nxt = 0x0002,
|
||||
/** WeDo USB hub. */
|
||||
WedoUsb = 0x0003,
|
||||
/** MINDSTORMS EV3 */
|
||||
Ev3 = 0x0005,
|
||||
/** MINDSTORMS EV3 in firmware update (bootloader) mode. */
|
||||
Ev3Bootloader = 0x0006,
|
||||
/** SPIKE Prime hub in DFU (bootloader) mode. */
|
||||
SpikePrimeBootloader = 0x0008,
|
||||
/** SPIKE Prime hub. */
|
||||
SpikePrime = 0x0009,
|
||||
/** SPIKE Essential hub in DFU (bootloader) mode. */
|
||||
SpikeEssentialBootloader = 0x000c,
|
||||
/** SPIKE Essential hub. */
|
||||
SpikeEssential = 0x000d,
|
||||
/** MINDSTORMS Robot inventor hub. */
|
||||
MindstormsRobotInventor = 0x0010,
|
||||
/** MINDSTORMS Robot inventor hub in DFU (bootloader) mode. */
|
||||
MindstormsRobotInventorBootloader = 0x0011,
|
||||
}
|
||||
@@ -2338,6 +2338,7 @@ __metadata:
|
||||
"@types/react-splitter-layout": ^3.0.2
|
||||
"@types/redux-logger": ^3.0.9
|
||||
"@types/semver": ^7.3.10
|
||||
"@types/w3c-web-usb": ^1.0.6
|
||||
"@types/web-bluetooth": ^0.0.15
|
||||
"@types/web-locks-api": ^0.0.2
|
||||
"@types/wicg-file-system-access": ^2020.9.5
|
||||
@@ -2362,6 +2363,7 @@ __metadata:
|
||||
dexie: ^3.2.2
|
||||
dexie-observable: ^4.0.0-beta.13
|
||||
dexie-react-hooks: ^1.1.1
|
||||
dfu: ^0.1.5
|
||||
dotenv: ^16.0.1
|
||||
dotenv-expand: ^8.0.3
|
||||
eslint: ^8.20.0
|
||||
@@ -4682,6 +4684,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/w3c-web-usb@npm:^1.0.6":
|
||||
version: 1.0.6
|
||||
resolution: "@types/w3c-web-usb@npm:1.0.6"
|
||||
checksum: 9f30948cb84174fa290066b08274bdfb034d38c6db0976e9a826508732fba04d81e3300bca41ea23b737f1424c51adec5ae810cdf85d5b5a158d5840914f0417
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/web-bluetooth@npm:^0.0.15":
|
||||
version: 0.0.15
|
||||
resolution: "@types/web-bluetooth@npm:0.0.15"
|
||||
@@ -7111,6 +7120,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"dfu@npm:^0.1.5":
|
||||
version: 0.1.5
|
||||
resolution: "dfu@npm:0.1.5"
|
||||
dependencies:
|
||||
nanoevents: ^6.0.0
|
||||
checksum: 7fa8aa1578518be4eb8981f67878ecb6f68e64ccf91ed278f35708a6d4e3f6e822e68ab8653c6b3d69e08ddf105c550698b88db363bdab8f8c2a7a582ac96001
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"didyoumean@npm:^1.2.2":
|
||||
version: 1.2.2
|
||||
resolution: "didyoumean@npm:1.2.2"
|
||||
@@ -10810,6 +10828,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"nanoevents@npm:^6.0.0":
|
||||
version: 6.0.2
|
||||
resolution: "nanoevents@npm:6.0.2"
|
||||
checksum: 73d8c8f584b850bae6705820710a20c19be61145a4a6ad3b157caf1fea52d46f48e0d1f1c1452019c0e84869f226debad2245218f0b0c3f9dffb7afc9b42e663
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"nanoid@npm:^3.3.4":
|
||||
version: 3.3.4
|
||||
resolution: "nanoid@npm:3.3.4"
|
||||
|
||||
Reference in New Issue
Block a user