Skip to content

Commit 9d52d9d

Browse files
committed
feature: take the ev3 firmware implementation forward
1 parent 1f81fbb commit 9d52d9d

File tree

5 files changed

+95
-29
lines changed

5 files changed

+95
-29
lines changed

src/firmware/installPybricksDialog/InstallPybricksDialog.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ function getHubTypeFromMetadata(
7272
return Hub.Prime;
7373
case HubType.EssentialHub:
7474
return Hub.Essential;
75+
case HubType.EV3:
76+
return Hub.EV3;
7577
default:
7678
return fallback;
7779
}
@@ -89,6 +91,8 @@ function getHubTypeNameFromMetadata(metadata: FirmwareMetadata | undefined): str
8991
return 'SPIKE Prime/MINDSTORMS Robot Inventor hub';
9092
case HubType.EssentialHub:
9193
return 'SPIKE Essential hub';
94+
case HubType.EV3:
95+
return 'MINDSTORMS EV3 Hub';
9296
default:
9397
return '?';
9498
}
@@ -458,7 +462,7 @@ export const InstallPybricksDialog: React.FunctionComponent = () => {
458462
);
459463
const dispatch = useDispatch();
460464
const [hubName, setHubName] = useState('');
461-
const [licenseAccepted, setLicenseAccepted] = useState(false);
465+
const [licenseAccepted, setLicenseAccepted] = useState(!false);
462466
const [hubType] = useHubPickerSelectedHub();
463467
const { firmwareData, firmwareError } = useFirmware(hubType);
464468
const [customFirmwareZip, setCustomFirmwareZip] = useState<File>();

src/firmware/installPybricksDialog/hooks.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ export function useFirmware(hubType: Hub): State {
8080
useEffect(() => {
8181
// Do nothing if the url is not given
8282
if (!url) {
83+
dispatch({ type: 'error', payload: new Error('No URL for this hub type') });
8384
return;
8485
}
8586

src/firmware/installPybricksDialog/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ const supportHubs: readonly HubType[] = [
3737
HubType.TechnicHub,
3838
HubType.PrimeHub,
3939
HubType.EssentialHub,
40+
HubType.EV3,
4041
];
4142

4243
export function validateMetadata(metadata: FirmwareMetadata) {

src/firmware/sagas.ts

Lines changed: 75 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Copyright (c) 2020-2025 The Pybricks Authors
33

44
import {
5+
FirmwareMetadataV200,
56
FirmwareReader,
67
FirmwareReaderError,
78
HubType,
@@ -14,7 +15,7 @@ import moveHubZip from '@pybricks/firmware/build/movehub.zip';
1415
import technicHubZip from '@pybricks/firmware/build/technichub.zip';
1516
import { WebDFU } from 'dfu';
1617
import { AnyAction } from 'redux';
17-
import { eventChannel } from 'redux-saga';
18+
import { channel, eventChannel } from 'redux-saga';
1819
import { ActionPattern } from 'redux-saga/effects';
1920
import {
2021
SagaGenerator,
@@ -229,6 +230,10 @@ function* loadFirmware(
229230

230231
const firmwareBase = yield* call(() => reader.readFirmwareBase());
231232
const metadata = yield* call(() => reader.readMetadata());
233+
let checksum: number | undefined = undefined;
234+
let checksum_size = 4;
235+
let firmware: Uint8Array | undefined = undefined;
236+
let firmwareView: DataView | undefined = undefined;
232237

233238
// v1.x allows appending main.py to firmware, later versions do not
234239
if (metadataIsV100(metadata) || metadataIsV110(metadata)) {
@@ -280,8 +285,8 @@ function* loadFirmware(
280285
mpy.data.length +
281286
fmod(-mpy.data.length, 4);
282287

283-
const firmware = new Uint8Array(checksumOffset + 4);
284-
const firmwareView = new DataView(firmware.buffer);
288+
firmware = new Uint8Array(checksumOffset + 4);
289+
firmwareView = new DataView(firmware.buffer);
285290

286291
if (firmware.length > metadata['max-firmware-size']) {
287292
// FIXME: we should return error/throw instead
@@ -307,7 +312,7 @@ function* loadFirmware(
307312
}
308313
}
309314

310-
const checksum = (function () {
315+
checksum = (function () {
311316
switch (metadata['checksum-type']) {
312317
case 'sum':
313318
return sumComplement32(
@@ -342,28 +347,41 @@ function* loadFirmware(
342347
return { firmware, deviceId: metadata['device-id'] };
343348
}
344349

345-
const firmware = new Uint8Array(firmwareBase.length + 4);
346-
const firmwareView = new DataView(firmware.buffer);
350+
// v2.x supports setting the checksum size to 0 to indicate no checksum
351+
else {
352+
const metadataV2 = metadata as FirmwareMetadataV200;
353+
checksum_size = metadataV2['checksum-size'];
347354

348-
firmware.set(firmwareBase);
355+
// TODO: v2.x supports setting checksum size, prior it was a fixed 4 bytes
356+
firmware = new Uint8Array(firmwareBase.length + checksum_size);
357+
firmwareView = new DataView(firmware.buffer);
358+
checksum = (function () {
359+
switch (metadata['checksum-type']) {
360+
case 'sum':
361+
console.log('computing sum');
362+
return sumComplement32(
363+
firmwareIterator(firmwareView, metadataV2['checksum-size']),
364+
);
365+
case 'crc32':
366+
console.log('computing crc32');
367+
return crc32(
368+
firmwareIterator(firmwareView, metadataV2['checksum-size']),
369+
);
370+
default:
371+
return 0;
372+
}
373+
})();
349374

350-
// empty string means use default name (don't write over firmware)
351-
if (hubName) {
352-
firmware.set(encodeHubName(hubName, metadata), metadata['hub-name-offset']);
353-
}
375+
firmware.set(firmwareBase);
354376

355-
const checksum = (function () {
356-
switch (metadata['checksum-type']) {
357-
case 'sum':
358-
return sumComplement32(
359-
firmwareIterator(firmwareView, metadata['checksum-size']),
360-
);
361-
case 'crc32':
362-
return crc32(firmwareIterator(firmwareView, metadata['checksum-size']));
363-
default:
364-
return undefined;
377+
// empty string means use default name (don't write over firmware)
378+
if (hubName) {
379+
firmware.set(
380+
encodeHubName(hubName, metadata),
381+
metadataV2['hub-name-offset'],
382+
);
365383
}
366-
})();
384+
}
367385

368386
if (checksum === undefined) {
369387
// FIXME: we should return error/throw instead
@@ -380,7 +398,9 @@ function* loadFirmware(
380398
throw new Error('unreachable');
381399
}
382400

383-
firmwareView.setUint32(firmwareBase.length, checksum, true);
401+
if (checksum_size) {
402+
firmwareView.setUint32(firmwareBase.length, checksum, true);
403+
}
384404

385405
return { firmware, deviceId: metadata['device-id'] };
386406
}
@@ -922,8 +942,14 @@ function* handleInstallPybricks(): Generator {
922942
}
923943
break;
924944
case 'usb-ev3':
925-
// TODO: implement flashing via EV3 USB
926-
console.error('Flashing via EV3 USB is not implemented yet');
945+
{
946+
const { firmware } = yield* loadFirmware(
947+
accepted.firmwareZip,
948+
accepted.hubName,
949+
);
950+
951+
yield* put(firmwareFlashEV3(firmware.buffer as ArrayBuffer));
952+
}
927953
break;
928954
}
929955
}
@@ -1105,8 +1131,23 @@ function* handleFlashEV3(action: ReturnType<typeof firmwareFlashEV3>): Generator
11051131
command: number,
11061132
payload?: Uint8Array,
11071133
): SagaGenerator<[DataView | undefined, Error | undefined]> {
1108-
console.debug(`EV3 send: command=${command}, payload=${payload}`);
1134+
// Create a channel that buffers the actions
1135+
const replyChannel = yield* call(
1136+
channel<ReturnType<typeof firmwareDidReceiveEV3Reply>>,
1137+
);
11091138

1139+
// Create a task that forwards matching actions to our channel
1140+
const forwardTask = yield* fork(function* () {
1141+
while (true) {
1142+
const action = yield* take(firmwareDidReceiveEV3Reply);
1143+
if (action.replyCommand === command) {
1144+
yield* put(replyChannel, action);
1145+
break; // Stop after finding the matching reply
1146+
}
1147+
}
1148+
});
1149+
1150+
// Send the command
11101151
const dataBuffer = new Uint8Array((payload?.byteLength ?? 0) + 6);
11111152
const data = new DataView(dataBuffer.buffer);
11121153

@@ -1121,13 +1162,19 @@ function* handleFlashEV3(action: ReturnType<typeof firmwareFlashEV3>): Generator
11211162
const [, sendError] = yield* call(() => maybe(hidDevice.sendReport(0, data)));
11221163

11231164
if (sendError) {
1165+
yield* cancel(forwardTask);
11241166
return [undefined, sendError];
11251167
}
11261168

11271169
const { reply, timeout } = yield* race({
1128-
reply: take(firmwareDidReceiveEV3Reply),
1129-
timeout: delay(5000),
1170+
reply: take(replyChannel),
1171+
timeout: delay(15000),
11301172
});
1173+
1174+
// Always clean up
1175+
yield* cancel(forwardTask);
1176+
yield* call(() => replyChannel.close());
1177+
11311178
if (timeout) {
11321179
return [undefined, new Error('Timeout waiting for EV3 reply')];
11331180
}

src/usb/sagas.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,19 @@ function* handleUsbConnectPybricks(hotPlugDevice?: USBDevice): Generator {
164164
// REVISIT: For now, we are making the assumption that there is only one
165165
// configuration and it is already selected and that it contains a Pybricks
166166
// interface.
167+
if (usbDevice.configuration === null && usbDevice.configurations.length > 0) {
168+
const [, selectErr] = yield* call(() =>
169+
maybe(
170+
usbDevice.selectConfiguration(
171+
usbDevice.configurations[0].configurationValue,
172+
),
173+
),
174+
);
175+
176+
// FIXME: Implement error handling here
177+
console.error(selectErr);
178+
}
179+
167180
assert(
168181
usbDevice.configuration !== undefined,
169182
'USB device configuration is undefined',

0 commit comments

Comments
 (0)