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
6 changes: 6 additions & 0 deletions src/firmware/actions.ts
Original file line number Diff line number Diff line change
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 has the wrong format (e.g. is not a multiple of sector size). */
FirmwareSizeNotMultipleOfSector = 'flashFirmware.failToFinish.reason.firmwareSizeNotMultipleOfSector',
/** 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 FailToFinishReasonFirmwareFormat =
Reason<FailToFinishReasonType.FirmwareSizeNotMultipleOfSector>;

export type FailToFinishReasonFailedToCompile =
Reason<FailToFinishReasonType.FailedToCompile>;

Expand All @@ -114,6 +119,7 @@ export type FailToFinishReason =
| FailToFinishReasonBadMetadata
| FailToFinishReasonFirmwareSize
| FailToFinishReasonFailedToCompile
| FailToFinishReasonFirmwareFormat
| FailToFinishReasonUnknown;

// High-level bootloader actions.
Expand Down
159 changes: 99 additions & 60 deletions src/firmware/sagas.ts
Original file line number Diff line number Diff line change
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 @@ -1219,73 +1221,110 @@ 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.FirmwareSizeNotMultipleOfSector),
);
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,
),
);

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, 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',
FlashFirmwareSizeNotMultipleOfSector = 'flashFirmware.sizeNotMultipleOfSector',
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.FirmwareSizeNotMultipleOfSector),
didFailToFinish(FailToFinishReasonType.Unknown, new Error('test error')),
appDidCheckForUpdate(false),
fileStorageDidFailToInitialize(new Error('test error')),
Expand Down
8 changes: 7 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-2025 The Pybricks Authors

// Saga for managing notifications (toasts)

Expand Down Expand Up @@ -225,6 +225,12 @@ function* showFlashFirmwareError(
case FailToFinishReasonType.FailedToCompile:
yield* showSingleton(Level.Error, I18nId.FlashFirmwareCompileError);
break;
case FailToFinishReasonType.FirmwareSizeNotMultipleOfSector:
yield* showSingleton(
Level.Error,
I18nId.FlashFirmwareSizeNotMultipleOfSector,
);
break;
case FailToFinishReasonType.FirmwareSize:
yield* showSingleton(Level.Error, I18nId.FlashFirmwareSizeTooBig);
break;
Expand Down
3 changes: 2 additions & 1 deletion src/notifications/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@
"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.",
"unexpectedError": "Unexpected error while trying to install firmware: {errorMessage}"
"unexpectedError": "Unexpected error while trying to install firmware: {errorMessage}",
"sizeNotMultipleOfSector": "The firmware size is not a multiple of the flash memory sector size."
},
"mpy": {
"error": "{errorMessage}"
Expand Down