Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions .yarn/patches/@types-w3c-web-usb-npm-1.0.10-82b33e05cb.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
diff --git a/index.d.ts b/index.d.ts
index b59810ce0fcaaf647830359010987348e11b0479..0f9d24ab390ac12010b671bc8aab02909e93fa89 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -39,7 +39,7 @@ interface USBConnectionEventInit extends EventInit {

declare class USBConfiguration {
readonly configurationValue: number;
- readonly configurationName?: string | undefined;
+ readonly configurationName: string | null;
readonly interfaces: USBInterface[];
}

@@ -57,14 +57,14 @@ declare class USBAlternateInterface {
readonly interfaceClass: number;
readonly interfaceSubclass: number;
readonly interfaceProtocol: number;
- readonly interfaceName?: string | undefined;
+ readonly interfaceName: string | null;
readonly endpoints: USBEndpoint[];
}

declare class USBInTransferResult {
constructor(status: USBTransferStatus, data?: DataView);
readonly data?: DataView | undefined;
- readonly status?: USBTransferStatus | undefined;
+ readonly status: USBTransferStatus;
}

declare class USBOutTransferResult {
@@ -76,7 +76,7 @@ declare class USBOutTransferResult {
declare class USBIsochronousInTransferPacket {
constructor(status: USBTransferStatus, data?: DataView);
readonly data?: DataView | undefined;
- readonly status?: USBTransferStatus | undefined;
+ readonly status: USBTransferStatus;
}

declare class USBIsochronousInTransferResult {
@@ -140,10 +140,10 @@ declare class USBDevice {
readonly deviceVersionMajor: number;
readonly deviceVersionMinor: number;
readonly deviceVersionSubminor: number;
- readonly manufacturerName?: string | undefined;
- readonly productName?: string | undefined;
- readonly serialNumber?: string | undefined;
- readonly configuration?: USBConfiguration | undefined;
+ readonly manufacturerName: string | null;
+ readonly productName: string | null;
+ readonly serialNumber: string | null;
+ readonly configuration: USBConfiguration | null;
readonly configurations: USBConfiguration[];
readonly opened: boolean;
open(): Promise<void>;
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,8 @@
"react-error-overlay": "6.0.9",
"react-dev-utils@^12.0.1": "patch:react-dev-utils@npm:12.0.1#.yarn/patches/react-dev-utils-npm-12.0.1-83ba06e3ee.patch",
"jsdom@^20.0.0": "patch:jsdom@npm%3A20.0.0#./.yarn/patches/jsdom-npm-20.0.0-9c1ad43ab8.patch",
"[email protected]": "patch:pyodide@npm%3A0.23.0#./.yarn/patches/pyodide-npm-0.23.0-64dc9bd6f1.patch"
"[email protected]": "patch:pyodide@npm%3A0.23.0#./.yarn/patches/pyodide-npm-0.23.0-64dc9bd6f1.patch",
"@types/w3c-web-usb@^1.0.10": "patch:@types/w3c-web-usb@npm%3A1.0.10#./.yarn/patches/@types-w3c-web-usb-npm-1.0.10-82b33e05cb.patch"
},
"jest": {
"roots": [
Expand Down
4 changes: 4 additions & 0 deletions src/firmware/installPybricksDialog/InstallPybricksDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ function getHubTypeFromMetadata(
return Hub.Prime;
case HubType.EssentialHub:
return Hub.Essential;
case HubType.EV3:
return Hub.EV3;
default:
return fallback;
}
Expand All @@ -89,6 +91,8 @@ function getHubTypeNameFromMetadata(metadata: FirmwareMetadata | undefined): str
return 'SPIKE Prime/MINDSTORMS Robot Inventor hub';
case HubType.EssentialHub:
return 'SPIKE Essential hub';
case HubType.EV3:
return 'MINDSTORMS EV3 hub';
default:
return '?';
}
Expand Down
6 changes: 4 additions & 2 deletions src/firmware/installPybricksDialog/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2022-2023 The Pybricks Authors
// Copyright (c) 2022-2025 The Pybricks Authors
// based on https://usehooks-ts.com/react-hook/use-fetch

import { FirmwareMetadata, FirmwareReader } from '@pybricks/firmware';
Expand Down Expand Up @@ -78,8 +78,10 @@ export function useFirmware(hubType: Hub): State {
const [state, dispatch] = useReducer(fetchReducer, initialState);

useEffect(() => {
// Do nothing if the url is not given
// Raise error if the url is not given, to show that something is wrong
// instead of a misleading intermediate state.
if (!url) {
dispatch({ type: 'error', payload: new Error('No URL for this hub type') });
return;
}

Expand Down
1 change: 1 addition & 0 deletions src/firmware/installPybricksDialog/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const supportHubs: readonly HubType[] = [
HubType.TechnicHub,
HubType.PrimeHub,
HubType.EssentialHub,
HubType.EV3,
];

export function validateMetadata(metadata: FirmwareMetadata) {
Expand Down
69 changes: 59 additions & 10 deletions src/firmware/sagas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {
encodeHubName,
metadataIsV100,
metadataIsV110,
metadataIsV200,
metadataIsV210,
} from '@pybricks/firmware';
import cityHubZip from '@pybricks/firmware/build/cityhub.zip';
import moveHubZip from '@pybricks/firmware/build/movehub.zip';
Expand All @@ -18,6 +20,7 @@ import { eventChannel } from 'redux-saga';
import { ActionPattern } from 'redux-saga/effects';
import {
SagaGenerator,
actionChannel,
all,
call,
cancel,
Expand Down Expand Up @@ -63,6 +66,7 @@ import { compile, didCompile, didFailToCompile } from '../mpy/actions';
import { RootState } from '../reducers';
import { LegoUsbProductId, legoUsbVendorId } from '../usb';
import { assert, defined, ensureError, hex, maybe } from '../utils';
import { createCountFunc } from '../utils/iter';
import { crc32, fmod, sumComplement32 } from '../utils/math';
import {
EV3OfficialFirmwareVersion,
Expand Down Expand Up @@ -342,6 +346,11 @@ function* loadFirmware(
return { firmware, deviceId: metadata['device-id'] };
}

assert(
metadataIsV200(metadata) || metadataIsV210(metadata),
'Expected metadata to be v2.x',
);

const firmware = new Uint8Array(firmwareBase.length + 4);
const firmwareView = new DataView(firmware.buffer);

Expand All @@ -360,6 +369,8 @@ function* loadFirmware(
);
case 'crc32':
return crc32(firmwareIterator(firmwareView, metadata['checksum-size']));
case 'none':
return null;
default:
return undefined;
}
Expand All @@ -380,7 +391,9 @@ function* loadFirmware(
throw new Error('unreachable');
}

firmwareView.setUint32(firmwareBase.length, checksum, true);
if (checksum !== null) {
firmwareView.setUint32(firmwareBase.length, checksum, true);
}

return { firmware, deviceId: metadata['device-id'] };
}
Expand Down Expand Up @@ -922,8 +935,25 @@ function* handleInstallPybricks(): Generator {
}
break;
case 'usb-ev3':
// TODO: implement flashing via EV3 USB
console.error('Flashing via EV3 USB is not implemented yet');
try {
const { firmware } = yield* loadFirmware(
accepted.firmwareZip,
accepted.hubName,
);

yield* put(firmwareFlashEV3(firmware.buffer as ArrayBuffer));
} catch (err) {
// istanbul ignore if
if (process.env.NODE_ENV !== 'test') {
console.error(err);
}

yield* put(
alertsShowAlert('alerts', 'unexpectedError', {
error: ensureError(err),
}),
);
}
break;
}
}
Expand Down Expand Up @@ -1001,6 +1031,8 @@ function* handleRestoreOfficialDfu(
}
}

const getNextEV3MessageId = createCountFunc();

function* handleFlashEV3(action: ReturnType<typeof firmwareFlashEV3>): Generator {
if (navigator.hid === undefined) {
yield* put(alertsShowAlert('firmware', 'noWebHid'));
Expand Down Expand Up @@ -1105,35 +1137,52 @@ function* handleFlashEV3(action: ReturnType<typeof firmwareFlashEV3>): Generator
command: number,
payload?: Uint8Array,
): SagaGenerator<[DataView | undefined, Error | undefined]> {
console.debug(`EV3 send: command=${command}, payload=${payload}`);
// We need to start listing for reply before sending command in order
// to avoid race conditions.
const replyChannel = yield* actionChannel(firmwareDidReceiveEV3Reply);

// Send the command
const dataBuffer = new Uint8Array((payload?.byteLength ?? 0) + 6);
const data = new DataView(dataBuffer.buffer);

const messageId = getNextEV3MessageId() & 0xffff;

data.setInt16(0, (payload?.byteLength ?? 0) + 4, true);
data.setInt16(2, 0, true); // TODO: reply number
data.setInt16(2, messageId, true);
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) {
replyChannel.close();
return [undefined, sendError];
}

const { reply, timeout } = yield* race({
reply: take(firmwareDidReceiveEV3Reply),
reply: take(replyChannel),
timeout: delay(5000),
});

replyChannel.close();

if (timeout) {
return [undefined, new Error('Timeout waiting for EV3 reply')];
}

defined(reply);

if (reply.replyNumber !== messageId) {
return [
undefined,
new Error(
`EV3 reply message ID mismatch: expected ${messageId}, got ${reply.replyNumber}`,
),
];
}

if (reply.replyCommand !== command) {
return [
undefined,
Expand Down Expand Up @@ -1202,7 +1251,7 @@ function* handleFlashEV3(action: ReturnType<typeof firmwareFlashEV3>): Generator
}),
);
// FIXME: should have a better error reason
yield* put(didFailToFinish(FailToFinishReasonType.Unknown));
yield* put(didFailToFinish(FailToFinishReasonType.Unknown, eraseError));
yield* put(firmwareDidFailToFlashEV3());
yield* cleanup();
return;
Expand All @@ -1219,7 +1268,7 @@ function* handleFlashEV3(action: ReturnType<typeof firmwareFlashEV3>): Generator
}),
);
// FIXME: should have a better error reason
yield* put(didFailToFinish(FailToFinishReasonType.Unknown));
yield* put(didFailToFinish(FailToFinishReasonType.Unknown, sendError));
yield* put(firmwareDidFailToFlashEV3());
yield* cleanup();
return;
Expand Down Expand Up @@ -1260,7 +1309,7 @@ function* handleFlashEV3(action: ReturnType<typeof firmwareFlashEV3>): Generator
const [, rebootError] = yield* sendCommand(0xf4); // start app
if (rebootError) {
// FIXME: should have a better error reason
yield* put(didFailToFinish(FailToFinishReasonType.Unknown));
yield* put(didFailToFinish(FailToFinishReasonType.Unknown, rebootError));
yield* put(firmwareDidFailToFlashEV3());
yield* cleanup();
return;
Expand Down
21 changes: 10 additions & 11 deletions src/usb/sagas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,17 +161,16 @@ function* handleUsbConnectPybricks(hotPlugDevice?: USBDevice): Generator {
return;
}

// REVISIT: For now, we are making the assumption that there is only one
// configuration and it is already selected and that it contains a Pybricks
// interface.
assert(
usbDevice.configuration !== undefined,
'USB device configuration is undefined',
);
assert(
usbDevice.configuration.interfaces.length > 0,
'USB device has no interfaces',
);
const [, selectErr] = yield* call(() => maybe(usbDevice.selectConfiguration(1)));
if (selectErr) {
// TODO: show error message to user here
console.error('Failed to select USB device configuration:', selectErr);
yield* put(usbDidFailToConnectPybricks());
yield* cleanup();
return;
}

assert(usbDevice.configuration !== null, 'USB device configuration is null');

const iface = usbDevice.configuration.interfaces.find(
(iface) =>
Expand Down
9 changes: 8 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5472,13 +5472,20 @@ __metadata:
languageName: node
linkType: hard

"@types/w3c-web-usb@npm:^1.0.10":
"@types/w3c-web-usb@npm:1.0.10":
version: 1.0.10
resolution: "@types/w3c-web-usb@npm:1.0.10"
checksum: 6ac6786a0788f0846a48b103ab06ca5fde5eb95674217b522420a2f6157bee3e181a961c1b7011940f497c55f4f5cc46129657d881fdd8112b48764089679ad6
languageName: node
linkType: hard

"@types/w3c-web-usb@patch:@types/w3c-web-usb@npm%3A1.0.10#./.yarn/patches/@types-w3c-web-usb-npm-1.0.10-82b33e05cb.patch::locator=%40pybricks%2Fpybricks-code%40workspace%3A.":
version: 1.0.10
resolution: "@types/w3c-web-usb@patch:@types/w3c-web-usb@npm%3A1.0.10#./.yarn/patches/@types-w3c-web-usb-npm-1.0.10-82b33e05cb.patch::version=1.0.10&hash=171f12&locator=%40pybricks%2Fpybricks-code%40workspace%3A."
checksum: 9528b1c501a5a21f05e36998b58501f8e2a4790af82c41b88e86bed8a618dc3135d486ffd55771532837a4e4112c1093ada293986451fe665f937081c9f2b7d7
languageName: node
linkType: hard

"@types/web-bluetooth@npm:^0.0.20":
version: 0.0.20
resolution: "@types/web-bluetooth@npm:0.0.20"
Expand Down
Loading