Skip to content
Open
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
161 changes: 102 additions & 59 deletions src/firmware/sagas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,20 @@ function* loadFirmware(
'Expected metadata to be v2.x',
);

const firmware = new Uint8Array(firmwareBase.length + 4);
const [checksumFunc, checksumSize] = (function () {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const [checksumFunc, checksumSize] = (function () {
const [checksumFunc, checksumExtraLength] = (function () {

As to not confuse with metadata['checkum-size'].

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const [checksumFunc, checksumSize] = (function () {
const [checksumFunc, checksumSize] = (() => {

This code base typically uses lambda syntax.

switch (metadata['checksum-type']) {
case 'sum':
return [sumComplement32, 4];
case 'crc32':
return [crc32, 4];
case 'none':
return [null, 0];
default:
return [undefined, 0];
}
})();

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

firmware.set(firmwareBase);
Expand All @@ -361,22 +374,7 @@ function* loadFirmware(
firmware.set(encodeHubName(hubName, metadata), metadata['hub-name-offset']);
}

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

if (checksum === undefined) {
if (checksumFunc === undefined) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would make sense to move this to fail as early as possible (line 366).

// FIXME: we should return error/throw instead
yield* put(
didFailToFinish(
Expand All @@ -391,8 +389,12 @@ function* loadFirmware(
throw new Error('unreachable');
}

if (checksum !== null) {
firmwareView.setUint32(firmwareBase.length, checksum, true);
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,114 @@ 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we fail the get the version correctly, then can't we expect other responses to fail?

Seems like this needs more investigation.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In fact the other responses do not fail. I agree it warrants further investigation. Perhaps a new issue, though, since the method of flashing the firmware does not depend on the version?

// 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());

const sectorSize = 64 * 1024; // flash memory sector size
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');
console.info(`Firmware size: ${action.firmware.byteLength} bytes`);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Don't really need to spam the logs.


// 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
Comment on lines +1246 to +1247
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I though that is what we were doing before. Erase one sector - write the entire one sector - erase the next sector - write one sector - and so on...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There has been quite a bit of testing and discussion about this already. Erasing the required space in one go was found to be more reliable. It's also what we've been testing with Pybricksdev.

Copy link
Author

@jaguilar jaguilar Dec 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Particularly here and here.

// cross the boundary will hang. To avoid this, we erase the whole firmware
// range at once.
const numSectors = Math.floor(action.firmware.byteLength / sectorSize);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we need to round up rather than down?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The direction doesn't matter here because it is implicitly testing that we're getting an integer result, which is in turn asserted below. Maybe we could make that more explicit instead.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I see. So really we want to check that action.firmware.byteLength % sectorSize === 0 .

If it isn't, we should show an error to the user. assert() is not the right way to verify this since the firmware is out of our control here.

assert(
action.firmware.byteLength === numSectors * sectorSize,
'Firmware size is required to be a round multiple of sector size.',
);

const erasePayload = new DataView(new ArrayBuffer(8));
erasePayload.setUint32(0, 0, true); // start address
erasePayload.setUint32(4, numSectors * sectorSize, true); // size
console.debug(
`Erasing bytes [0x${(0).toString(16)}, 0x${(numSectors * sectorSize).toString(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would also be useful to log the number of sectors that we calculated.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing a - 1 on the end address?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIRC, we also have a util function to add leading zeros in hex numbers.

16,
)})`,
);

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;
}

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),
// 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 writeLength = Math.min(maxPayloadSize, firmware.byteLength - i);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pretty sure this is redundant since slice doesn't pad thing but rather returns a partial if i + maxPayloadSize exceeds the firmware size.

const payload = firmware.slice(i, i + writeLength);
console.debug(
`Programming bytes [0x${i.toString(16)}, 0x${(i + writeLength).toString(
16,
)})`,
);

if (eraseError) {
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 + writeLength) / 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