WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content
Open
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
8 changes: 7 additions & 1 deletion src/firmware/actions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020-2025 The Pybricks Authors
// Copyright (c) 2020-2026 The Pybricks Authors

import { FirmwareReaderError, HubType } from '@pybricks/firmware';
import { createAction } from '../actions';
Expand Down Expand Up @@ -47,6 +47,8 @@ export enum FailToFinishReasonType {
FailedToCompile = 'flashFirmware.failToFinish.reason.failedToCompile',
/** The combined firmware-base.bin and main.mpy are too big. */
FirmwareSize = 'flashFirmware.failToFinish.reason.firmwareSize',
/** The firmware's start or end is not aligned to the sector boundary. */
FirmwareAlignment = 'flashFirmware.failToFinish.reason.firmwareAlignment',
/** An unexpected error occurred. */
Unknown = 'flashFirmware.failToFinish.reason.unknown',
}
Expand Down Expand Up @@ -94,6 +96,9 @@ export type FailToFinishReasonBadMetadata =
export type FailToFinishReasonFirmwareSize =
Reason<FailToFinishReasonType.FirmwareSize>;

export type FailToFinishReasonFirmwareAlignment =
Reason<FailToFinishReasonType.FirmwareAlignment>;

export type FailToFinishReasonFailedToCompile =
Reason<FailToFinishReasonType.FailedToCompile>;

Expand All @@ -113,6 +118,7 @@ export type FailToFinishReason =
| FailToFinishReasonZipError
| FailToFinishReasonBadMetadata
| FailToFinishReasonFirmwareSize
| FailToFinishReasonFirmwareAlignment
| FailToFinishReasonFailedToCompile
| FailToFinishReasonUnknown;

Expand Down
176 changes: 114 additions & 62 deletions src/firmware/sagas.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020-2025 The Pybricks Authors
// Copyright (c) 2020-2026 The Pybricks Authors

import {
FirmwareReader,
Expand Down Expand Up @@ -351,32 +351,20 @@ function* loadFirmware(
'Expected metadata to be v2.x',
);

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

firmware.set(firmwareBase);

// empty string means use default name (don't write over firmware)
if (hubName) {
firmware.set(encodeHubName(hubName, metadata), metadata['hub-name-offset']);
}

const checksum = (function () {
const [checksumFunc, checksumExtraLength] = (() => {
switch (metadata['checksum-type']) {
case 'sum':
return sumComplement32(
firmwareIterator(firmwareView, metadata['checksum-size']),
);
return [sumComplement32, 4];
case 'crc32':
return crc32(firmwareIterator(firmwareView, metadata['checksum-size']));
return [crc32, 4];
case 'none':
return null;
return [null, 0];
default:
return undefined;
return [undefined, 0];
}
})();

if (checksum === undefined) {
if (checksumFunc === undefined) {
// FIXME: we should return error/throw instead
yield* put(
didFailToFinish(
Expand All @@ -391,8 +379,22 @@ function* loadFirmware(
throw new Error('unreachable');
}

if (checksum !== null) {
firmwareView.setUint32(firmwareBase.length, checksum, true);
const firmware = new Uint8Array(firmwareBase.length + checksumExtraLength);
const firmwareView = new DataView(firmware.buffer);

firmware.set(firmwareBase);

// empty string means use default name (don't write over firmware)
if (hubName) {
firmware.set(encodeHubName(hubName, metadata), metadata['hub-name-offset']);
}

if (checksumFunc !== null) {
firmwareView.setUint32(
firmwareBase.length,
checksumFunc(firmwareIterator(firmwareView, metadata['checksum-size'])),
true,
);
}

return { firmware, deviceId: metadata['device-id'] };
Expand Down Expand Up @@ -1136,6 +1138,7 @@ function* handleFlashEV3(action: ReturnType<typeof firmwareFlashEV3>): Generator
function* sendCommand(
command: number,
payload?: Uint8Array,
options?: { timeoutms?: number },
): SagaGenerator<[DataView | undefined, Error | undefined]> {
// We need to start listing for reply before sending command in order
// to avoid race conditions.
Expand All @@ -1161,9 +1164,11 @@ function* handleFlashEV3(action: ReturnType<typeof firmwareFlashEV3>): Generator
return [undefined, sendError];
}

const timeoutms = options?.timeoutms ?? 5000;

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

replyChannel.close();
Expand Down Expand Up @@ -1219,73 +1224,120 @@ function* handleFlashEV3(action: ReturnType<typeof firmwareFlashEV3>): Generator

defined(version);

console.debug(
`EV3 bootloader version: ${version.getUint32(
0,
true,
)}, HW version: ${version.getUint32(4, true)}`,
);
// For reasons that we do not currently understand, some EV3s do not return
// anything to our GetVersion command. We don't actually use the version
// for anything so we will just ignore this error.
try {
console.debug(
`EV3 bootloader version: ${version.getUint32(
0,
true,
)}, HW version: ${version.getUint32(4, true)}`,
);
} catch (err) {
console.error(`Failed to parse ev3 version response: ${ensureError(err)}`);
}

// FIXME: should be called much earlier.
yield* put(didStart());

console.debug(`Firmware size: ${action.firmware.byteLength} bytes`);

// Apparently, erasing a span of the flash creates some sort of record in
// the EV3, and we can only write within a given erase span. Writes that
// cross the boundary will hang. To avoid this, we erase the whole firmware
// range at once.
const sectorSize = 64 * 1024; // flash memory sector size
if (action.firmware.byteLength % sectorSize !== 0) {
yield* put(didFailToFinish(FailToFinishReasonType.FirmwareAlignment));
yield* put(firmwareDidFailToFlashEV3());
yield* cleanup();
return;
}

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, 0, true); // start address
erasePayload.setUint32(4, action.firmware.byteLength, true); // size
console.debug(`Erasing bytes [0x0, ${hex(action.firmware.byteLength, 0)})`);

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),
yield* put(
alertsShowAlert(
'firmware',
'flashProgress',
{
action: 'erase',
progress: undefined,
},
firmwareBleProgressToastId,
true,
),
);

// Measured erase rate is approximately .25 kB/ms. This was on a powerful
// computer so it may be that flashing from something like a raspberry pi
// would take longer. We'll set a timeout three times as long as would have
// taken at the measured rate.
const eraseTimeoutMs = (action.firmware.byteLength / 256) * 1000 * 3;
const startTime = Date.now();
const [, eraseError] = yield* sendCommand(
0xf0,
new Uint8Array(erasePayload.buffer),
{ timeoutms: eraseTimeoutMs },
);
console.debug(
`EV3 erase took ${Date.now() - startTime} ms for ${
action.firmware.byteLength
} bytes, timeout was ${eraseTimeoutMs} ms`,
);

if (eraseError) {
yield* put(
alertsShowAlert('alerts', 'unexpectedError', {
error: eraseError,
}),
);
// FIXME: should have a better error reason
yield* put(didFailToFinish(FailToFinishReasonType.Unknown, eraseError));
yield* put(firmwareDidFailToFlashEV3());
yield* cleanup();
return;
}

if (eraseError) {
// If we don't write an exact multiple of the sector size, the flash process
// will hang on the last write we send.
const firmware = action.firmware;
for (let i = 0; i < firmware.byteLength; i += maxPayloadSize) {
const payload = firmware.slice(i, i + maxPayloadSize);
console.debug(
`Programming bytes [${hex(i, 0)}, ${hex(i + maxPayloadSize, 0)})`,
);

const [, sendError] = yield* sendCommand(0xf2, new Uint8Array(payload));
if (sendError) {
yield* put(
alertsShowAlert('alerts', 'unexpectedError', {
error: eraseError,
error: sendError,
}),
);
// FIXME: should have a better error reason
yield* put(didFailToFinish(FailToFinishReasonType.Unknown, eraseError));
yield* put(didFailToFinish(FailToFinishReasonType.Unknown, sendError));
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, sendError));
yield* put(firmwareDidFailToFlashEV3());
yield* cleanup();
return;
}
}

yield* put(
didProgress((i + sectorData.byteLength) / action.firmware.byteLength),
);
const progress = (i + payload.byteLength) / firmware.byteLength;
yield* put(didProgress(progress));

yield* put(
alertsShowAlert(
'firmware',
'flashProgress',
{
action: 'flash',
progress: (i + sectorData.byteLength) / action.firmware.byteLength,
progress: progress,
},
firmwareBleProgressToastId,
true,
Expand Down
3 changes: 2 additions & 1 deletion src/notifications/i18n.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020-2022 The Pybricks Authors
// Copyright (c) 2020-2025 The Pybricks Authors
//
// Notification translation keys.

Expand Down Expand Up @@ -35,6 +35,7 @@ export enum I18nId {
FlashFirmwareBadMetadata = 'flashFirmware.badMetadata',
FlashFirmwareCompileError = 'flashFirmware.compileError',
FlashFirmwareSizeTooBig = 'flashFirmware.sizeTooBig',
FlashFirmwareAlignment = 'flashFirmware.alignment',
FlashFirmwareUnexpectedError = 'flashFirmware.unexpectedError',
ServiceWorkerUpdateMessage = 'serviceWorker.update.message',
ServiceWorkerUpdateAction = 'serviceWorker.update.action',
Expand Down
3 changes: 2 additions & 1 deletion src/notifications/sagas.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2021-2023 The Pybricks Authors
// Copyright (c) 2021-2025 The Pybricks Authors

import type { ToastOptions, Toaster } from '@blueprintjs/core';
import { FirmwareReaderError, FirmwareReaderErrorCode } from '@pybricks/firmware';
Expand Down Expand Up @@ -95,6 +95,7 @@ test.each([
),
didFailToFinish(FailToFinishReasonType.FailedToCompile),
didFailToFinish(FailToFinishReasonType.FirmwareSize),
didFailToFinish(FailToFinishReasonType.FirmwareAlignment),
didFailToFinish(FailToFinishReasonType.Unknown, new Error('test error')),
appDidCheckForUpdate(false),
fileStorageDidFailToInitialize(new Error('test error')),
Expand Down
5 changes: 4 additions & 1 deletion src/notifications/sagas.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020-2023 The Pybricks Authors
// Copyright (c) 2020-2026 The Pybricks Authors

// Saga for managing notifications (toasts)

Expand Down Expand Up @@ -228,6 +228,9 @@ function* showFlashFirmwareError(
case FailToFinishReasonType.FirmwareSize:
yield* showSingleton(Level.Error, I18nId.FlashFirmwareSizeTooBig);
break;
case FailToFinishReasonType.FirmwareAlignment:
yield* showSingleton(Level.Error, I18nId.FlashFirmwareAlignment);
break;
case FailToFinishReasonType.Unknown:
yield* showUnexpectedError(
I18nId.FlashFirmwareUnexpectedError,
Expand Down
1 change: 1 addition & 0 deletions src/notifications/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"badMetadata": "The firmware.metadata.json file contains missing or invalid entries. Fix it then try again.",
"compileError": "The included main.py file could not be compiled. Fix it then try again.",
"sizeTooBig": "The combined firmware and main.py are too big to fit in the flash memory.",
"alignment": "The firmware's start or end is not aligned to the sector boundary.",
"unexpectedError": "Unexpected error while trying to install firmware: {errorMessage}"
},
"mpy": {
Expand Down