mirror of
https://github.com/pybricks/pybricks-code.git
synced 2026-09-14 10:35:11 +00:00
Instead of rendering the toaster separate from the main index, add it there so we can inherit all of the context providers. This requires passing the ref object instead of the toaster instance itself, so sagas have to be updated.
2030 lines
67 KiB
TypeScript
2030 lines
67 KiB
TypeScript
// SPDX-License-Identifier: MIT
|
|
// Copyright (c) 2021-2022 The Pybricks Authors
|
|
|
|
import type { ToasterInstance } from '@blueprintjs/core';
|
|
import {
|
|
FirmwareMetadata,
|
|
FirmwareMetadataV110,
|
|
FirmwareMetadataV200,
|
|
FirmwareReaderError,
|
|
FirmwareReaderErrorCode,
|
|
} from '@pybricks/firmware';
|
|
import { mock } from 'jest-mock-extended';
|
|
import JSZip from 'jszip';
|
|
import { AsyncSaga } from '../../test';
|
|
import { alertsShowAlert } from '../alerts/actions';
|
|
import {
|
|
BootloaderConnectionFailureReason,
|
|
checksumRequest,
|
|
checksumResponse,
|
|
connect,
|
|
didConnect,
|
|
didDisconnect,
|
|
didFailToConnect,
|
|
didFailToRequest,
|
|
didRequest,
|
|
disconnect,
|
|
eraseRequest,
|
|
eraseResponse,
|
|
errorResponse,
|
|
infoRequest,
|
|
infoResponse,
|
|
initRequest,
|
|
initResponse,
|
|
programRequest,
|
|
programResponse,
|
|
rebootRequest,
|
|
} from '../lwp3-bootloader/actions';
|
|
import { Command, HubType, Result } from '../lwp3-bootloader/protocol';
|
|
import { BootloaderConnectionState } from '../lwp3-bootloader/reducers';
|
|
import { didCompile, didFailToCompile } from '../mpy/actions';
|
|
import { createCountFunc } from '../utils/iter';
|
|
import {
|
|
FailToFinishReasonType,
|
|
HubError,
|
|
MetadataProblem,
|
|
didFailToFinish,
|
|
didFinish,
|
|
didProgress,
|
|
didStart,
|
|
flashFirmware as flashFirmwareAction,
|
|
} from './actions';
|
|
import flashFirmware from './sagas';
|
|
|
|
afterEach(() => {
|
|
jest.restoreAllMocks();
|
|
});
|
|
|
|
describe('flashFirmware', () => {
|
|
describe('normal flow using app supplied firmware', () => {
|
|
test('metadata v1.x works', async () => {
|
|
const metadata: FirmwareMetadataV110 = {
|
|
'metadata-version': '1.1.0',
|
|
'device-id': HubType.MoveHub,
|
|
'checksum-type': 'sum',
|
|
'firmware-version': '1.2.3',
|
|
'max-firmware-size': 1024,
|
|
'mpy-abi-version': 5,
|
|
'mpy-cross-options': ['-mno-unicode'],
|
|
'user-mpy-offset': 100,
|
|
'hub-name-offset': 54,
|
|
'max-hub-name-size': 10,
|
|
};
|
|
|
|
const zip = new JSZip();
|
|
zip.file('firmware-base.bin', new Uint8Array(64));
|
|
zip.file('firmware.metadata.json', JSON.stringify(metadata));
|
|
zip.file('main.py', 'print("test")');
|
|
zip.file('ReadMe_OSS.txt', 'test');
|
|
|
|
jest.spyOn(window, 'fetch').mockResolvedValueOnce(
|
|
new Response(await zip.generateAsync({ type: 'blob' })),
|
|
);
|
|
|
|
const saga = new AsyncSaga(flashFirmware, {
|
|
nextMessageId: createCountFunc(),
|
|
toasterRef: { current: mock<ToasterInstance>() },
|
|
});
|
|
|
|
// saga is triggered by this action
|
|
|
|
saga.put(flashFirmwareAction(null, 'test name'));
|
|
|
|
// first step is to connect to the hub bootloader
|
|
|
|
let action = await saga.take();
|
|
expect(action).toEqual(connect());
|
|
|
|
saga.updateState({
|
|
bootloader: { connection: BootloaderConnectionState.Connected },
|
|
});
|
|
saga.put(didConnect());
|
|
|
|
// then find out what kind of hub it is
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(infoRequest(0));
|
|
|
|
saga.put(didRequest(0));
|
|
saga.put(infoResponse(0x01000000, 0x08005000, 0x081f800, HubType.MoveHub));
|
|
|
|
// then compile main.py to .mpy
|
|
|
|
action = await saga.take();
|
|
expect(action).toMatchInlineSnapshot(`
|
|
{
|
|
"abiVersion": 5,
|
|
"options": [
|
|
"-mno-unicode",
|
|
],
|
|
"script": "print("test")",
|
|
"type": "mpy.action.compile",
|
|
}
|
|
`);
|
|
|
|
// this makes total firmware size 140 bytes to check for
|
|
// https://github.com/pybricks/support/issues/178
|
|
const mpySize = 40 - 8;
|
|
const mpyBinaryData = new Uint8Array(mpySize);
|
|
saga.put(didCompile(mpyBinaryData));
|
|
|
|
// then start flashing the firmware
|
|
|
|
// should get didStart action just before starting to erase
|
|
action = await saga.take();
|
|
expect(action).toEqual(didStart());
|
|
|
|
// erase first
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(alertsShowAlert('firmware', 'releaseButton'));
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(eraseRequest(1, /* isCityHub */ false));
|
|
|
|
saga.put(didRequest(1));
|
|
saga.put(eraseResponse(Result.OK));
|
|
|
|
// then write the new firmware
|
|
|
|
const totalFirmwareSize = metadata['user-mpy-offset'] + mpySize + 8;
|
|
action = await saga.take();
|
|
expect(action).toEqual(initRequest(2, totalFirmwareSize));
|
|
|
|
saga.put(didRequest(2));
|
|
saga.put(initResponse(Result.OK));
|
|
|
|
const dummyPayload = new ArrayBuffer(0);
|
|
let id = 2;
|
|
for (let count = 1, offset = 0; ; count++, offset += 14) {
|
|
action = await saga.take();
|
|
expect(action).toEqual(
|
|
programRequest(++id, 0x08005000 + offset, dummyPayload),
|
|
);
|
|
expect(
|
|
(action as ReturnType<typeof programRequest>).payload.byteLength,
|
|
).toBe(Math.min(14, totalFirmwareSize - offset));
|
|
|
|
saga.put(didRequest(id));
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(didProgress(offset / totalFirmwareSize));
|
|
|
|
// Have to be careful that a checksum request is not sent after
|
|
// last payload is sent, otherwise the hub gets confused.
|
|
|
|
if (offset + 14 >= totalFirmwareSize) {
|
|
expect(count).toBe(10);
|
|
break;
|
|
}
|
|
|
|
if (count % 10 === 0) {
|
|
action = await saga.take();
|
|
expect(action).toEqual(checksumRequest(++id));
|
|
|
|
saga.put(didRequest(id));
|
|
saga.put(checksumResponse(0));
|
|
}
|
|
}
|
|
|
|
// hub indicates success
|
|
|
|
saga.put(programResponse(0x33, totalFirmwareSize));
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(didProgress(1));
|
|
|
|
// and finally reboot the hub
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(rebootRequest(++id));
|
|
|
|
saga.put(didRequest(id));
|
|
|
|
// then we are done
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(didFinish());
|
|
|
|
await saga.end();
|
|
});
|
|
|
|
test('metadata v2.x works', async () => {
|
|
const metadata: FirmwareMetadataV200 = {
|
|
'metadata-version': '2.0.0',
|
|
'device-id': HubType.MoveHub,
|
|
'firmware-version': '1.2.3',
|
|
'checksum-type': 'sum',
|
|
'checksum-size': 1024,
|
|
'hub-name-offset': 54,
|
|
'hub-name-size': 10,
|
|
};
|
|
|
|
const zip = new JSZip();
|
|
zip.file('firmware-base.bin', new Uint8Array(64));
|
|
zip.file('firmware.metadata.json', JSON.stringify(metadata));
|
|
zip.file('ReadMe_OSS.txt', 'test');
|
|
|
|
jest.spyOn(window, 'fetch').mockResolvedValueOnce(
|
|
new Response(await zip.generateAsync({ type: 'blob' })),
|
|
);
|
|
|
|
const saga = new AsyncSaga(flashFirmware, {
|
|
nextMessageId: createCountFunc(),
|
|
toasterRef: { current: mock<ToasterInstance>() },
|
|
});
|
|
|
|
// saga is triggered by this action
|
|
|
|
saga.put(flashFirmwareAction(null, 'test name'));
|
|
|
|
// first step is to connect to the hub bootloader
|
|
|
|
let action = await saga.take();
|
|
expect(action).toEqual(connect());
|
|
|
|
saga.updateState({
|
|
bootloader: { connection: BootloaderConnectionState.Connected },
|
|
});
|
|
saga.put(didConnect());
|
|
|
|
// then find out what kind of hub it is
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(infoRequest(0));
|
|
|
|
saga.put(didRequest(0));
|
|
saga.put(infoResponse(0x01000000, 0x08005000, 0x081f800, HubType.MoveHub));
|
|
|
|
// then start flashing the firmware
|
|
|
|
// should get didStart action just before starting to erase
|
|
action = await saga.take();
|
|
expect(action).toEqual(didStart());
|
|
|
|
// erase first
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(alertsShowAlert('firmware', 'releaseButton'));
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(eraseRequest(1, /* isCityHub */ false));
|
|
|
|
saga.put(didRequest(1));
|
|
saga.put(eraseResponse(Result.OK));
|
|
|
|
// then write the new firmware
|
|
|
|
const totalFirmwareSize = 68;
|
|
action = await saga.take();
|
|
expect(action).toEqual(initRequest(2, totalFirmwareSize));
|
|
|
|
saga.put(didRequest(2));
|
|
saga.put(initResponse(Result.OK));
|
|
|
|
const dummyPayload = new ArrayBuffer(0);
|
|
let id = 2;
|
|
for (let count = 1, offset = 0; ; count++, offset += 14) {
|
|
action = await saga.take();
|
|
expect(action).toEqual(
|
|
programRequest(++id, 0x08005000 + offset, dummyPayload),
|
|
);
|
|
expect(
|
|
(action as ReturnType<typeof programRequest>).payload.byteLength,
|
|
).toBe(Math.min(14, totalFirmwareSize - offset));
|
|
|
|
saga.put(didRequest(id));
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(didProgress(offset / totalFirmwareSize));
|
|
|
|
// Have to be careful that a checksum request is not sent after
|
|
// last payload is sent, otherwise the hub gets confused.
|
|
|
|
if (offset + 14 >= totalFirmwareSize) {
|
|
expect(count).toBe(5);
|
|
break;
|
|
}
|
|
|
|
if (count % 10 === 0) {
|
|
action = await saga.take();
|
|
expect(action).toEqual(checksumRequest(++id));
|
|
|
|
saga.put(didRequest(id));
|
|
saga.put(checksumResponse(0));
|
|
}
|
|
}
|
|
|
|
// hub indicates success
|
|
|
|
saga.put(programResponse(0xe0, totalFirmwareSize));
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(didProgress(1));
|
|
|
|
// and finally reboot the hub
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(rebootRequest(++id));
|
|
|
|
saga.put(didRequest(id));
|
|
|
|
// then we are done
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(didFinish());
|
|
|
|
await saga.end();
|
|
});
|
|
|
|
test('fail to connect', async () => {
|
|
const metadata: FirmwareMetadata = {
|
|
'metadata-version': '1.0.0',
|
|
'device-id': HubType.MoveHub,
|
|
'checksum-type': 'sum',
|
|
'firmware-version': '1.2.3',
|
|
'max-firmware-size': 1024,
|
|
'mpy-abi-version': 5,
|
|
'mpy-cross-options': ['-mno-unicode'],
|
|
'user-mpy-offset': 100,
|
|
};
|
|
|
|
const zip = new JSZip();
|
|
zip.file('firmware-base.bin', new Uint8Array(64));
|
|
zip.file('firmware.metadata.json', JSON.stringify(metadata));
|
|
zip.file('main.py', 'print("test")');
|
|
zip.file('ReadMe_OSS.txt', 'test');
|
|
|
|
jest.spyOn(window, 'fetch').mockResolvedValueOnce(
|
|
new Response(await zip.generateAsync({ type: 'blob' })),
|
|
);
|
|
|
|
const saga = new AsyncSaga(flashFirmware, {
|
|
nextMessageId: createCountFunc(),
|
|
toasterRef: { current: mock<ToasterInstance>() },
|
|
});
|
|
|
|
// saga is triggered by this action
|
|
|
|
saga.put(flashFirmwareAction(null, ''));
|
|
|
|
// first step is to connect to the hub bootloader
|
|
|
|
let action = await saga.take();
|
|
expect(action).toEqual(connect());
|
|
|
|
saga.put(
|
|
didFailToConnect(BootloaderConnectionFailureReason.GattServiceNotFound),
|
|
);
|
|
|
|
// it should fail here because of failure to connect
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(
|
|
didFailToFinish(FailToFinishReasonType.FailedToConnect),
|
|
);
|
|
|
|
await saga.end();
|
|
});
|
|
|
|
test('untimely disconnect before start cancels saga', async () => {
|
|
const metadata: FirmwareMetadata = {
|
|
'metadata-version': '1.0.0',
|
|
'device-id': HubType.MoveHub,
|
|
'checksum-type': 'sum',
|
|
'firmware-version': '1.2.3',
|
|
'max-firmware-size': 1024,
|
|
'mpy-abi-version': 5,
|
|
'mpy-cross-options': ['-mno-unicode'],
|
|
'user-mpy-offset': 100,
|
|
};
|
|
|
|
const zip = new JSZip();
|
|
zip.file('firmware-base.bin', new Uint8Array(64));
|
|
zip.file('firmware.metadata.json', JSON.stringify(metadata));
|
|
zip.file('main.py', 'print("test")');
|
|
zip.file('ReadMe_OSS.txt', 'test');
|
|
|
|
jest.spyOn(window, 'fetch').mockResolvedValueOnce(
|
|
new Response(await zip.generateAsync({ type: 'blob' })),
|
|
);
|
|
|
|
const saga = new AsyncSaga(flashFirmware, {
|
|
nextMessageId: createCountFunc(),
|
|
toasterRef: { current: mock<ToasterInstance>() },
|
|
});
|
|
|
|
// saga is triggered by this action
|
|
|
|
saga.put(flashFirmwareAction(null, ''));
|
|
|
|
// first step is to connect to the hub bootloader
|
|
|
|
let action = await saga.take();
|
|
expect(action).toEqual(connect());
|
|
|
|
saga.updateState({
|
|
bootloader: { connection: BootloaderConnectionState.Connected },
|
|
});
|
|
saga.put(didConnect());
|
|
|
|
// then find out what kind of hub it is
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(infoRequest(0));
|
|
|
|
// hub disconnects before replying
|
|
|
|
saga.updateState({
|
|
bootloader: { connection: BootloaderConnectionState.Disconnected },
|
|
});
|
|
saga.put(didDisconnect());
|
|
|
|
// should get a failure to start
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(
|
|
didFailToFinish(FailToFinishReasonType.Disconnected),
|
|
);
|
|
|
|
// On city hub, we can end up in this situation. BLE writeValueWithResponse()
|
|
// doesn't return until erasing is done, so there is a long window for
|
|
// this to happen.
|
|
saga.put(didFailToRequest(0, new Error('failed due to disconnect')));
|
|
|
|
await saga.end();
|
|
});
|
|
|
|
test('fail to send info request', async () => {
|
|
const metadata: FirmwareMetadata = {
|
|
'metadata-version': '1.0.0',
|
|
'device-id': HubType.MoveHub,
|
|
'checksum-type': 'sum',
|
|
'firmware-version': '1.2.3',
|
|
'max-firmware-size': 1024,
|
|
'mpy-abi-version': 5,
|
|
'mpy-cross-options': ['-mno-unicode'],
|
|
'user-mpy-offset': 100,
|
|
};
|
|
|
|
const zip = new JSZip();
|
|
zip.file('firmware-base.bin', new Uint8Array(64));
|
|
zip.file('firmware.metadata.json', JSON.stringify(metadata));
|
|
zip.file('main.py', 'print("test")');
|
|
zip.file('ReadMe_OSS.txt', 'test');
|
|
|
|
jest.spyOn(window, 'fetch').mockResolvedValueOnce(
|
|
new Response(await zip.generateAsync({ type: 'blob' })),
|
|
);
|
|
|
|
const saga = new AsyncSaga(flashFirmware, {
|
|
nextMessageId: createCountFunc(),
|
|
toasterRef: { current: mock<ToasterInstance>() },
|
|
});
|
|
|
|
// saga is triggered by this action
|
|
|
|
saga.put(flashFirmwareAction(null, ''));
|
|
|
|
// first step is to connect to the hub bootloader
|
|
|
|
let action = await saga.take();
|
|
expect(action).toEqual(connect());
|
|
|
|
saga.updateState({
|
|
bootloader: { connection: BootloaderConnectionState.Connected },
|
|
});
|
|
saga.put(didConnect());
|
|
|
|
// then find out what kind of hub it is
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(infoRequest(0));
|
|
|
|
const testError = new Error('test');
|
|
saga.put(didFailToRequest(0, testError));
|
|
|
|
// should get a failure to start
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(
|
|
didFailToFinish(FailToFinishReasonType.BleError, testError),
|
|
);
|
|
|
|
// should request to disconnect after failure
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(disconnect());
|
|
|
|
await saga.end();
|
|
});
|
|
|
|
test('info request is unknown command', async () => {
|
|
const metadata: FirmwareMetadata = {
|
|
'metadata-version': '1.0.0',
|
|
'device-id': HubType.MoveHub,
|
|
'checksum-type': 'sum',
|
|
'firmware-version': '1.2.3',
|
|
'max-firmware-size': 1024,
|
|
'mpy-abi-version': 5,
|
|
'mpy-cross-options': ['-mno-unicode'],
|
|
'user-mpy-offset': 100,
|
|
};
|
|
|
|
const zip = new JSZip();
|
|
zip.file('firmware-base.bin', new Uint8Array(64));
|
|
zip.file('firmware.metadata.json', JSON.stringify(metadata));
|
|
zip.file('main.py', 'print("test")');
|
|
zip.file('ReadMe_OSS.txt', 'test');
|
|
|
|
jest.spyOn(window, 'fetch').mockResolvedValueOnce(
|
|
new Response(await zip.generateAsync({ type: 'blob' })),
|
|
);
|
|
|
|
const saga = new AsyncSaga(flashFirmware, {
|
|
nextMessageId: createCountFunc(),
|
|
toasterRef: { current: mock<ToasterInstance>() },
|
|
});
|
|
|
|
// saga is triggered by this action
|
|
|
|
saga.put(flashFirmwareAction(null, ''));
|
|
|
|
// first step is to connect to the hub bootloader
|
|
|
|
let action = await saga.take();
|
|
expect(action).toEqual(connect());
|
|
|
|
saga.updateState({
|
|
bootloader: { connection: BootloaderConnectionState.Connected },
|
|
});
|
|
saga.put(didConnect());
|
|
|
|
// then find out what kind of hub it is
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(infoRequest(0));
|
|
|
|
saga.put(didRequest(0));
|
|
saga.put(errorResponse(Command.GetInfo));
|
|
|
|
// should get an unknown command failure
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(
|
|
didFailToFinish(
|
|
FailToFinishReasonType.HubError,
|
|
HubError.UnknownCommand,
|
|
),
|
|
);
|
|
|
|
// should request to disconnect after failure
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(disconnect());
|
|
|
|
await saga.end();
|
|
});
|
|
|
|
test('timeout waiting for info response', async () => {
|
|
const metadata: FirmwareMetadata = {
|
|
'metadata-version': '1.0.0',
|
|
'device-id': HubType.MoveHub,
|
|
'checksum-type': 'sum',
|
|
'firmware-version': '1.2.3',
|
|
'max-firmware-size': 1024,
|
|
'mpy-abi-version': 5,
|
|
'mpy-cross-options': ['-mno-unicode'],
|
|
'user-mpy-offset': 100,
|
|
};
|
|
|
|
const zip = new JSZip();
|
|
zip.file('firmware-base.bin', new Uint8Array(64));
|
|
zip.file('firmware.metadata.json', JSON.stringify(metadata));
|
|
zip.file('main.py', 'print("test")');
|
|
zip.file('ReadMe_OSS.txt', 'test');
|
|
|
|
jest.spyOn(window, 'fetch').mockResolvedValueOnce(
|
|
new Response(await zip.generateAsync({ type: 'blob' })),
|
|
);
|
|
|
|
const saga = new AsyncSaga(flashFirmware, {
|
|
nextMessageId: createCountFunc(),
|
|
toasterRef: { current: mock<ToasterInstance>() },
|
|
});
|
|
|
|
// saga is triggered by this action
|
|
|
|
saga.put(flashFirmwareAction(null, ''));
|
|
|
|
// first step is to connect to the hub bootloader
|
|
|
|
let action = await saga.take();
|
|
expect(action).toEqual(connect());
|
|
|
|
saga.updateState({
|
|
bootloader: { connection: BootloaderConnectionState.Connected },
|
|
});
|
|
saga.put(didConnect());
|
|
|
|
// then find out what kind of hub it is
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(infoRequest(0));
|
|
|
|
saga.put(didRequest(0));
|
|
|
|
// should get a timed-out failure
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(didFailToFinish(FailToFinishReasonType.TimedOut));
|
|
|
|
// should request to disconnect after failure
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(disconnect());
|
|
|
|
await saga.end();
|
|
});
|
|
|
|
test('failed to fetch firmware', async () => {
|
|
const metadata: FirmwareMetadata = {
|
|
'metadata-version': '1.0.0',
|
|
'device-id': HubType.MoveHub,
|
|
'checksum-type': 'sum',
|
|
'firmware-version': '1.2.3',
|
|
'max-firmware-size': 1024,
|
|
'mpy-abi-version': 5,
|
|
'mpy-cross-options': ['-mno-unicode'],
|
|
'user-mpy-offset': 100,
|
|
};
|
|
|
|
const zip = new JSZip();
|
|
zip.file('firmware-base.bin', new Uint8Array(64));
|
|
zip.file('firmware.metadata.json', JSON.stringify(metadata));
|
|
zip.file('main.py', 'print("test")');
|
|
zip.file('ReadMe_OSS.txt', 'test');
|
|
|
|
const response = new Response(undefined, { status: 404 });
|
|
jest.spyOn(window, 'fetch').mockResolvedValueOnce(response);
|
|
|
|
const saga = new AsyncSaga(flashFirmware, {
|
|
nextMessageId: createCountFunc(),
|
|
toasterRef: { current: mock<ToasterInstance>() },
|
|
});
|
|
|
|
// saga is triggered by this action
|
|
|
|
saga.put(flashFirmwareAction(null, ''));
|
|
|
|
// first step is to connect to the hub bootloader
|
|
|
|
let action = await saga.take();
|
|
expect(action).toEqual(connect());
|
|
|
|
saga.updateState({
|
|
bootloader: { connection: BootloaderConnectionState.Connected },
|
|
});
|
|
saga.put(didConnect());
|
|
|
|
// then find out what kind of hub it is
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(infoRequest(0));
|
|
|
|
// received an unknown hub type ID
|
|
|
|
saga.put(didRequest(0));
|
|
saga.put(infoResponse(0x01000000, 0x08005000, 0x081f800, HubType.MoveHub));
|
|
|
|
// should raise an error that we don't have any firmware for this hub
|
|
|
|
action = await saga.take();
|
|
expect(action).toStrictEqual(
|
|
didFailToFinish(FailToFinishReasonType.FailedToFetch, response),
|
|
);
|
|
|
|
// should request to disconnect after failure
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(disconnect());
|
|
|
|
await saga.end();
|
|
});
|
|
|
|
test('connected device does not match firmware device', async () => {
|
|
const metadata: FirmwareMetadata = {
|
|
'metadata-version': '1.0.0',
|
|
'device-id': HubType.MoveHub,
|
|
'checksum-type': 'sum',
|
|
'firmware-version': '1.2.3',
|
|
'max-firmware-size': 1024,
|
|
'mpy-abi-version': 5,
|
|
'mpy-cross-options': ['-mno-unicode'],
|
|
'user-mpy-offset': 100,
|
|
};
|
|
|
|
const zip = new JSZip();
|
|
zip.file('firmware-base.bin', new Uint8Array(64));
|
|
zip.file('firmware.metadata.json', JSON.stringify(metadata));
|
|
zip.file('main.py', 'print("test")');
|
|
zip.file('ReadMe_OSS.txt', 'test');
|
|
|
|
jest.spyOn(window, 'fetch').mockResolvedValueOnce(
|
|
new Response(await zip.generateAsync({ type: 'blob' })),
|
|
);
|
|
|
|
const saga = new AsyncSaga(flashFirmware, {
|
|
nextMessageId: createCountFunc(),
|
|
toasterRef: { current: mock<ToasterInstance>() },
|
|
});
|
|
|
|
// saga is triggered by this action
|
|
|
|
saga.put(flashFirmwareAction(null, ''));
|
|
|
|
// first step is to connect to the hub bootloader
|
|
|
|
let action = await saga.take();
|
|
expect(action).toEqual(connect());
|
|
|
|
saga.updateState({
|
|
bootloader: { connection: BootloaderConnectionState.Connected },
|
|
});
|
|
saga.put(didConnect());
|
|
|
|
// then find out what kind of hub it is
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(infoRequest(0));
|
|
|
|
// connected hub type does not match firmware hub type
|
|
saga.put(didRequest(0));
|
|
saga.put(infoResponse(0x01000000, 0x08005000, 0x081f800, HubType.CityHub));
|
|
|
|
// then compile main.py to .mpy
|
|
|
|
action = await saga.take();
|
|
expect(action).toMatchInlineSnapshot(`
|
|
{
|
|
"abiVersion": 5,
|
|
"options": [
|
|
"-mno-unicode",
|
|
],
|
|
"script": "print("test")",
|
|
"type": "mpy.action.compile",
|
|
}
|
|
`);
|
|
|
|
const mpySize = 20;
|
|
const mpyBinaryData = new Uint8Array(mpySize);
|
|
saga.put(didCompile(mpyBinaryData));
|
|
|
|
// should raise an error that we don't have any firmware for this hub
|
|
|
|
action = await saga.take();
|
|
expect(action).toStrictEqual(
|
|
didFailToFinish(FailToFinishReasonType.DeviceMismatch),
|
|
);
|
|
|
|
// should request to disconnect after failure
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(disconnect());
|
|
|
|
await saga.end();
|
|
});
|
|
|
|
test('unsupported device', async () => {
|
|
const metadata: FirmwareMetadata = {
|
|
'metadata-version': '1.0.0',
|
|
'device-id': HubType.MoveHub,
|
|
'checksum-type': 'sum',
|
|
'firmware-version': '1.2.3',
|
|
'max-firmware-size': 1024,
|
|
'mpy-abi-version': 5,
|
|
'mpy-cross-options': ['-mno-unicode'],
|
|
'user-mpy-offset': 100,
|
|
};
|
|
|
|
const zip = new JSZip();
|
|
zip.file('firmware-base.bin', new Uint8Array(64));
|
|
zip.file('firmware.metadata.json', JSON.stringify(metadata));
|
|
zip.file('main.py', 'print("test")');
|
|
zip.file('ReadMe_OSS.txt', 'test');
|
|
|
|
jest.spyOn(window, 'fetch').mockResolvedValueOnce(
|
|
new Response(await zip.generateAsync({ type: 'blob' })),
|
|
);
|
|
|
|
const saga = new AsyncSaga(flashFirmware, {
|
|
nextMessageId: createCountFunc(),
|
|
toasterRef: { current: mock<ToasterInstance>() },
|
|
});
|
|
|
|
// saga is triggered by this action
|
|
|
|
saga.put(flashFirmwareAction(null, ''));
|
|
|
|
// first step is to connect to the hub bootloader
|
|
|
|
let action = await saga.take();
|
|
expect(action).toEqual(connect());
|
|
|
|
saga.updateState({
|
|
bootloader: { connection: BootloaderConnectionState.Connected },
|
|
});
|
|
saga.put(didConnect());
|
|
|
|
// then find out what kind of hub it is
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(infoRequest(0));
|
|
|
|
// received an unknown hub type ID
|
|
|
|
saga.put(didRequest(0));
|
|
saga.put(infoResponse(0x01000000, 0x08005000, 0x081f800, 0 as HubType));
|
|
|
|
// should raise an error that we don't have any firmware for this hub
|
|
|
|
action = await saga.take();
|
|
expect(action).toStrictEqual(
|
|
didFailToFinish(FailToFinishReasonType.NoFirmware),
|
|
);
|
|
|
|
// should request to disconnect after failure
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(disconnect());
|
|
|
|
await saga.end();
|
|
});
|
|
|
|
test('erase response is failed', async () => {
|
|
const metadata: FirmwareMetadata = {
|
|
'metadata-version': '1.0.0',
|
|
'device-id': HubType.MoveHub,
|
|
'checksum-type': 'sum',
|
|
'firmware-version': '1.2.3',
|
|
'max-firmware-size': 1024,
|
|
'mpy-abi-version': 5,
|
|
'mpy-cross-options': ['-mno-unicode'],
|
|
'user-mpy-offset': 100,
|
|
};
|
|
|
|
const zip = new JSZip();
|
|
zip.file('firmware-base.bin', new Uint8Array(64));
|
|
zip.file('firmware.metadata.json', JSON.stringify(metadata));
|
|
zip.file('main.py', 'print("test")');
|
|
zip.file('ReadMe_OSS.txt', 'test');
|
|
|
|
jest.spyOn(window, 'fetch').mockResolvedValueOnce(
|
|
new Response(await zip.generateAsync({ type: 'blob' })),
|
|
);
|
|
|
|
const saga = new AsyncSaga(flashFirmware, {
|
|
nextMessageId: createCountFunc(),
|
|
toasterRef: { current: mock<ToasterInstance>() },
|
|
});
|
|
|
|
// saga is triggered by this action
|
|
|
|
saga.put(flashFirmwareAction(null, ''));
|
|
|
|
// first step is to connect to the hub bootloader
|
|
|
|
let action = await saga.take();
|
|
expect(action).toEqual(connect());
|
|
|
|
saga.updateState({
|
|
bootloader: { connection: BootloaderConnectionState.Connected },
|
|
});
|
|
saga.put(didConnect());
|
|
|
|
// then find out what kind of hub it is
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(infoRequest(0));
|
|
|
|
saga.put(didRequest(0));
|
|
saga.put(infoResponse(0x01000000, 0x08005000, 0x081f800, HubType.MoveHub));
|
|
|
|
// then compile main.py to .mpy
|
|
|
|
action = await saga.take();
|
|
expect(action).toMatchInlineSnapshot(`
|
|
{
|
|
"abiVersion": 5,
|
|
"options": [
|
|
"-mno-unicode",
|
|
],
|
|
"script": "print("test")",
|
|
"type": "mpy.action.compile",
|
|
}
|
|
`);
|
|
|
|
const mpySize = 20;
|
|
const mpyBinaryData = new Uint8Array(mpySize);
|
|
saga.put(didCompile(mpyBinaryData));
|
|
|
|
// then start flashing the firmware
|
|
|
|
// should get didStart action just before starting to erase
|
|
action = await saga.take();
|
|
expect(action).toEqual(didStart());
|
|
|
|
// erase first
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(alertsShowAlert('firmware', 'releaseButton'));
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(eraseRequest(1, /* isCityHub */ false));
|
|
|
|
saga.put(didRequest(1));
|
|
saga.put(eraseResponse(Result.Error));
|
|
|
|
// should get a hub error
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(
|
|
didFailToFinish(FailToFinishReasonType.HubError, HubError.EraseFailed),
|
|
);
|
|
|
|
// should request to disconnect after failure
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(disconnect());
|
|
|
|
await saga.end();
|
|
});
|
|
|
|
test('init response is failed', async () => {
|
|
const metadata: FirmwareMetadata = {
|
|
'metadata-version': '1.0.0',
|
|
'device-id': HubType.MoveHub,
|
|
'checksum-type': 'sum',
|
|
'firmware-version': '1.2.3',
|
|
'max-firmware-size': 1024,
|
|
'mpy-abi-version': 5,
|
|
'mpy-cross-options': ['-mno-unicode'],
|
|
'user-mpy-offset': 100,
|
|
};
|
|
|
|
const zip = new JSZip();
|
|
zip.file('firmware-base.bin', new Uint8Array(64));
|
|
zip.file('firmware.metadata.json', JSON.stringify(metadata));
|
|
zip.file('main.py', 'print("test")');
|
|
zip.file('ReadMe_OSS.txt', 'test');
|
|
|
|
jest.spyOn(window, 'fetch').mockResolvedValueOnce(
|
|
new Response(await zip.generateAsync({ type: 'blob' })),
|
|
);
|
|
|
|
const saga = new AsyncSaga(flashFirmware, {
|
|
nextMessageId: createCountFunc(),
|
|
toasterRef: { current: mock<ToasterInstance>() },
|
|
});
|
|
|
|
// saga is triggered by this action
|
|
|
|
saga.put(flashFirmwareAction(null, ''));
|
|
|
|
// first step is to connect to the hub bootloader
|
|
|
|
let action = await saga.take();
|
|
expect(action).toEqual(connect());
|
|
|
|
saga.updateState({
|
|
bootloader: { connection: BootloaderConnectionState.Connected },
|
|
});
|
|
saga.put(didConnect());
|
|
|
|
// then find out what kind of hub it is
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(infoRequest(0));
|
|
|
|
saga.put(didRequest(0));
|
|
saga.put(infoResponse(0x01000000, 0x08005000, 0x081f800, HubType.MoveHub));
|
|
|
|
// then compile main.py to .mpy
|
|
|
|
action = await saga.take();
|
|
expect(action).toMatchInlineSnapshot(`
|
|
{
|
|
"abiVersion": 5,
|
|
"options": [
|
|
"-mno-unicode",
|
|
],
|
|
"script": "print("test")",
|
|
"type": "mpy.action.compile",
|
|
}
|
|
`);
|
|
|
|
const mpySize = 20;
|
|
const mpyBinaryData = new Uint8Array(mpySize);
|
|
saga.put(didCompile(mpyBinaryData));
|
|
|
|
// then start flashing the firmware
|
|
|
|
// should get didStart action just before starting to erase
|
|
action = await saga.take();
|
|
expect(action).toEqual(didStart());
|
|
|
|
// erase first
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(alertsShowAlert('firmware', 'releaseButton'));
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(eraseRequest(1, /* isCityHub */ false));
|
|
|
|
saga.put(didRequest(1));
|
|
saga.put(eraseResponse(Result.OK));
|
|
|
|
// then write the new firmware
|
|
|
|
const totalFirmwareSize = metadata['user-mpy-offset'] + mpySize + 8;
|
|
action = await saga.take();
|
|
expect(action).toEqual(initRequest(2, totalFirmwareSize));
|
|
|
|
saga.put(didRequest(2));
|
|
saga.put(initResponse(Result.Error));
|
|
|
|
// should get a hub error
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(
|
|
didFailToFinish(FailToFinishReasonType.HubError, HubError.InitFailed),
|
|
);
|
|
|
|
// should request to disconnect after failure
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(disconnect());
|
|
|
|
await saga.end();
|
|
});
|
|
|
|
test('size mismatch after flashing', async () => {
|
|
const metadata: FirmwareMetadata = {
|
|
'metadata-version': '1.0.0',
|
|
'device-id': HubType.MoveHub,
|
|
'checksum-type': 'sum',
|
|
'firmware-version': '1.2.3',
|
|
'max-firmware-size': 1024,
|
|
'mpy-abi-version': 5,
|
|
'mpy-cross-options': ['-mno-unicode'],
|
|
'user-mpy-offset': 100,
|
|
};
|
|
|
|
const zip = new JSZip();
|
|
zip.file('firmware-base.bin', new Uint8Array(64));
|
|
zip.file('firmware.metadata.json', JSON.stringify(metadata));
|
|
zip.file('main.py', 'print("test")');
|
|
zip.file('ReadMe_OSS.txt', 'test');
|
|
|
|
jest.spyOn(window, 'fetch').mockResolvedValueOnce(
|
|
new Response(await zip.generateAsync({ type: 'blob' })),
|
|
);
|
|
|
|
const saga = new AsyncSaga(flashFirmware, {
|
|
nextMessageId: createCountFunc(),
|
|
toasterRef: { current: mock<ToasterInstance>() },
|
|
});
|
|
|
|
// saga is triggered by this action
|
|
|
|
saga.put(flashFirmwareAction(null, ''));
|
|
|
|
// first step is to connect to the hub bootloader
|
|
|
|
let action = await saga.take();
|
|
expect(action).toEqual(connect());
|
|
|
|
saga.updateState({
|
|
bootloader: { connection: BootloaderConnectionState.Connected },
|
|
});
|
|
saga.put(didConnect());
|
|
|
|
// then find out what kind of hub it is
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(infoRequest(0));
|
|
|
|
saga.put(didRequest(0));
|
|
saga.put(infoResponse(0x01000000, 0x08005000, 0x081f800, HubType.MoveHub));
|
|
|
|
// then compile main.py to .mpy
|
|
|
|
action = await saga.take();
|
|
expect(action).toMatchInlineSnapshot(`
|
|
{
|
|
"abiVersion": 5,
|
|
"options": [
|
|
"-mno-unicode",
|
|
],
|
|
"script": "print("test")",
|
|
"type": "mpy.action.compile",
|
|
}
|
|
`);
|
|
|
|
const mpySize = 20;
|
|
const mpyBinaryData = new Uint8Array(mpySize);
|
|
saga.put(didCompile(mpyBinaryData));
|
|
|
|
// then start flashing the firmware
|
|
|
|
// should get didStart action just before starting to erase
|
|
action = await saga.take();
|
|
expect(action).toEqual(didStart());
|
|
|
|
// erase first
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(alertsShowAlert('firmware', 'releaseButton'));
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(eraseRequest(1, /* isCityHub */ false));
|
|
|
|
saga.put(didRequest(1));
|
|
saga.put(eraseResponse(Result.OK));
|
|
|
|
// then write the new firmware
|
|
|
|
const totalFirmwareSize = metadata['user-mpy-offset'] + mpySize + 8;
|
|
action = await saga.take();
|
|
expect(action).toEqual(initRequest(2, totalFirmwareSize));
|
|
|
|
saga.put(didRequest(2));
|
|
saga.put(initResponse(Result.OK));
|
|
|
|
const dummyPayload = new ArrayBuffer(0);
|
|
let id = 2;
|
|
for (let count = 1, offset = 0; ; count++, offset += 14) {
|
|
action = await saga.take();
|
|
expect(action).toEqual(
|
|
programRequest(++id, 0x08005000 + offset, dummyPayload),
|
|
);
|
|
expect(
|
|
(action as ReturnType<typeof programRequest>).payload.byteLength,
|
|
).toBe(Math.min(14, totalFirmwareSize - offset));
|
|
|
|
saga.put(didRequest(id));
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(didProgress(offset / totalFirmwareSize));
|
|
|
|
// Have to be careful that a checksum request is not sent after
|
|
// last payload is sent, otherwise the hub gets confused.
|
|
|
|
if (offset + 14 >= totalFirmwareSize) {
|
|
expect(count).toBe(10);
|
|
break;
|
|
}
|
|
|
|
if (count % 10 === 0) {
|
|
action = await saga.take();
|
|
expect(action).toEqual(checksumRequest(++id));
|
|
|
|
saga.put(didRequest(id));
|
|
saga.put(checksumResponse(0));
|
|
}
|
|
}
|
|
|
|
// hub indicates incorrect size
|
|
|
|
saga.put(programResponse(0x62, totalFirmwareSize - 1));
|
|
|
|
// should get a hub error
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(
|
|
didFailToFinish(
|
|
FailToFinishReasonType.HubError,
|
|
HubError.CountMismatch,
|
|
),
|
|
);
|
|
|
|
// should request to disconnect after failure
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(disconnect());
|
|
|
|
await saga.end();
|
|
});
|
|
|
|
test('checksum mismatch after flashing', async () => {
|
|
const metadata: FirmwareMetadata = {
|
|
'metadata-version': '1.0.0',
|
|
'device-id': HubType.MoveHub,
|
|
'checksum-type': 'sum',
|
|
'firmware-version': '1.2.3',
|
|
'max-firmware-size': 1024,
|
|
'mpy-abi-version': 5,
|
|
'mpy-cross-options': ['-mno-unicode'],
|
|
'user-mpy-offset': 100,
|
|
};
|
|
|
|
const zip = new JSZip();
|
|
zip.file('firmware-base.bin', new Uint8Array(64));
|
|
zip.file('firmware.metadata.json', JSON.stringify(metadata));
|
|
zip.file('main.py', 'print("test")');
|
|
zip.file('ReadMe_OSS.txt', 'test');
|
|
|
|
jest.spyOn(window, 'fetch').mockResolvedValueOnce(
|
|
new Response(await zip.generateAsync({ type: 'blob' })),
|
|
);
|
|
|
|
const saga = new AsyncSaga(flashFirmware, {
|
|
nextMessageId: createCountFunc(),
|
|
toasterRef: { current: mock<ToasterInstance>() },
|
|
});
|
|
|
|
// saga is triggered by this action
|
|
|
|
saga.put(flashFirmwareAction(null, ''));
|
|
|
|
// first step is to connect to the hub bootloader
|
|
|
|
let action = await saga.take();
|
|
expect(action).toEqual(connect());
|
|
|
|
saga.updateState({
|
|
bootloader: { connection: BootloaderConnectionState.Connected },
|
|
});
|
|
saga.put(didConnect());
|
|
|
|
// then find out what kind of hub it is
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(infoRequest(0));
|
|
|
|
saga.put(didRequest(0));
|
|
saga.put(infoResponse(0x01000000, 0x08005000, 0x081f800, HubType.MoveHub));
|
|
|
|
// then compile main.py to .mpy
|
|
|
|
action = await saga.take();
|
|
expect(action).toMatchInlineSnapshot(`
|
|
{
|
|
"abiVersion": 5,
|
|
"options": [
|
|
"-mno-unicode",
|
|
],
|
|
"script": "print("test")",
|
|
"type": "mpy.action.compile",
|
|
}
|
|
`);
|
|
|
|
const mpySize = 20;
|
|
const mpyBinaryData = new Uint8Array(mpySize);
|
|
saga.put(didCompile(mpyBinaryData));
|
|
|
|
// then start flashing the firmware
|
|
|
|
// should get didStart action just before starting to erase
|
|
action = await saga.take();
|
|
expect(action).toEqual(didStart());
|
|
|
|
// erase first
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(alertsShowAlert('firmware', 'releaseButton'));
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(eraseRequest(1, /* isCityHub */ false));
|
|
|
|
saga.put(didRequest(1));
|
|
saga.put(eraseResponse(Result.OK));
|
|
|
|
// then write the new firmware
|
|
|
|
const totalFirmwareSize = metadata['user-mpy-offset'] + mpySize + 8;
|
|
action = await saga.take();
|
|
expect(action).toEqual(initRequest(2, totalFirmwareSize));
|
|
|
|
saga.put(didRequest(2));
|
|
saga.put(initResponse(Result.OK));
|
|
|
|
const dummyPayload = new ArrayBuffer(0);
|
|
let id = 2;
|
|
for (let count = 1, offset = 0; ; count++, offset += 14) {
|
|
action = await saga.take();
|
|
expect(action).toEqual(
|
|
programRequest(++id, 0x08005000 + offset, dummyPayload),
|
|
);
|
|
expect(
|
|
(action as ReturnType<typeof programRequest>).payload.byteLength,
|
|
).toBe(Math.min(14, totalFirmwareSize - offset));
|
|
|
|
saga.put(didRequest(id));
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(didProgress(offset / totalFirmwareSize));
|
|
|
|
// Have to be careful that a checksum request is not sent after
|
|
// last payload is sent, otherwise the hub gets confused.
|
|
|
|
if (offset + 14 >= totalFirmwareSize) {
|
|
expect(count).toBe(10);
|
|
break;
|
|
}
|
|
|
|
if (count % 10 === 0) {
|
|
action = await saga.take();
|
|
expect(action).toEqual(checksumRequest(++id));
|
|
|
|
saga.put(didRequest(id));
|
|
saga.put(checksumResponse(0));
|
|
}
|
|
}
|
|
|
|
// hub indicates incorrect checksum
|
|
|
|
saga.put(programResponse(0x100, totalFirmwareSize));
|
|
|
|
// should get a hub error
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(
|
|
didFailToFinish(
|
|
FailToFinishReasonType.HubError,
|
|
HubError.ChecksumMismatch,
|
|
),
|
|
);
|
|
|
|
// should request to disconnect after failure
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(disconnect());
|
|
|
|
await saga.end();
|
|
});
|
|
});
|
|
|
|
describe('user supplied firmware.zip', () => {
|
|
test('success', async () => {
|
|
const metadata: FirmwareMetadata = {
|
|
'metadata-version': '1.0.0',
|
|
'device-id': HubType.MoveHub,
|
|
'checksum-type': 'sum',
|
|
'firmware-version': '1.2.3',
|
|
'max-firmware-size': 1024,
|
|
'mpy-abi-version': 5,
|
|
'mpy-cross-options': ['-mno-unicode'],
|
|
'user-mpy-offset': 100,
|
|
};
|
|
|
|
const zip = new JSZip();
|
|
zip.file('firmware-base.bin', new Uint8Array(64));
|
|
zip.file('firmware.metadata.json', JSON.stringify(metadata));
|
|
zip.file('main.py', 'print("test")');
|
|
zip.file('ReadMe_OSS.txt', 'test');
|
|
|
|
const saga = new AsyncSaga(flashFirmware, {
|
|
nextMessageId: createCountFunc(),
|
|
toasterRef: { current: mock<ToasterInstance>() },
|
|
});
|
|
|
|
// saga is triggered by this action
|
|
|
|
saga.put(
|
|
flashFirmwareAction(
|
|
await zip.generateAsync({ type: 'arraybuffer' }),
|
|
'',
|
|
),
|
|
);
|
|
|
|
// the first step is to compile main.py to .mpy
|
|
|
|
let action = await saga.take();
|
|
expect(action).toMatchInlineSnapshot(`
|
|
{
|
|
"abiVersion": 5,
|
|
"options": [
|
|
"-mno-unicode",
|
|
],
|
|
"script": "print("test")",
|
|
"type": "mpy.action.compile",
|
|
}
|
|
`);
|
|
|
|
// make sure that total firmware size is big enough that checksum
|
|
// is called at least once
|
|
const mpySize = 100;
|
|
const mpyBinaryData = new Uint8Array(mpySize);
|
|
saga.put(didCompile(mpyBinaryData));
|
|
|
|
// then connect to the hub bootloader
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(connect());
|
|
|
|
saga.updateState({
|
|
bootloader: { connection: BootloaderConnectionState.Connected },
|
|
});
|
|
saga.put(didConnect());
|
|
|
|
// then find out what kind of hub it is
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(infoRequest(0));
|
|
|
|
saga.put(didRequest(0));
|
|
saga.put(infoResponse(0x01000000, 0x08005000, 0x081f800, HubType.MoveHub));
|
|
|
|
// then start flashing the firmware
|
|
|
|
// should get didStart action just before starting to erase
|
|
action = await saga.take();
|
|
expect(action).toEqual(didStart());
|
|
|
|
// erase first
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(alertsShowAlert('firmware', 'releaseButton'));
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(eraseRequest(1, /* isCityHub */ false));
|
|
|
|
saga.put(didRequest(1));
|
|
saga.put(eraseResponse(Result.OK));
|
|
|
|
// then write the new firmware
|
|
|
|
const totalFirmwareSize = metadata['user-mpy-offset'] + mpySize + 8;
|
|
action = await saga.take();
|
|
expect(action).toEqual(initRequest(2, totalFirmwareSize));
|
|
|
|
saga.put(didRequest(2));
|
|
saga.put(initResponse(Result.OK));
|
|
|
|
const dummyPayload = new ArrayBuffer(0);
|
|
let id = 2;
|
|
|
|
for (let count = 1, offset = 0; ; count++, offset += 14) {
|
|
action = await saga.take();
|
|
expect(action).toEqual(
|
|
programRequest(++id, 0x08005000 + offset, dummyPayload),
|
|
);
|
|
expect(
|
|
(action as ReturnType<typeof programRequest>).payload.byteLength,
|
|
).toBe(Math.min(14, totalFirmwareSize - offset));
|
|
|
|
saga.put(didRequest(id));
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(didProgress(offset / totalFirmwareSize));
|
|
|
|
// Have to be careful that a checksum request is not sent after
|
|
// last payload is sent, otherwise the hub gets confused.
|
|
|
|
if (offset + 14 >= totalFirmwareSize) {
|
|
expect(count).toBeGreaterThan(10);
|
|
break;
|
|
}
|
|
|
|
if (count % 10 === 0) {
|
|
action = await saga.take();
|
|
expect(action).toEqual(checksumRequest(++id));
|
|
|
|
saga.put(didRequest(id));
|
|
saga.put(checksumResponse(0x9b));
|
|
}
|
|
}
|
|
|
|
// hub indicates success
|
|
|
|
saga.put(programResponse(0xf3, totalFirmwareSize));
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(didProgress(1));
|
|
|
|
// and finally reboot the hub
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(rebootRequest(++id));
|
|
|
|
saga.put(didRequest(id));
|
|
|
|
// then we are done
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(didFinish());
|
|
|
|
await saga.end();
|
|
});
|
|
|
|
test('zip error', async () => {
|
|
const metadata: FirmwareMetadata = {
|
|
'metadata-version': '1.0.0',
|
|
'device-id': HubType.MoveHub,
|
|
'checksum-type': 'sum',
|
|
'firmware-version': '1.2.3',
|
|
'max-firmware-size': 1024,
|
|
'mpy-abi-version': 5,
|
|
'mpy-cross-options': ['-mno-unicode'],
|
|
'user-mpy-offset': 100,
|
|
};
|
|
|
|
const zip = new JSZip();
|
|
// no firmware-base.bin - triggers zip error
|
|
zip.file('firmware.metadata.json', JSON.stringify(metadata));
|
|
zip.file('main.py', 'print("test")');
|
|
zip.file('ReadMe_OSS.txt', 'test');
|
|
|
|
const saga = new AsyncSaga(flashFirmware, {
|
|
nextMessageId: createCountFunc(),
|
|
toasterRef: { current: mock<ToasterInstance>() },
|
|
});
|
|
|
|
// saga is triggered by this action
|
|
|
|
saga.put(
|
|
flashFirmwareAction(
|
|
await zip.generateAsync({ type: 'arraybuffer' }),
|
|
'',
|
|
),
|
|
);
|
|
|
|
// should get failure due to missing file
|
|
|
|
const action = await saga.take();
|
|
expect(action).toStrictEqual(
|
|
didFailToFinish(
|
|
FailToFinishReasonType.ZipError,
|
|
new FirmwareReaderError(
|
|
FirmwareReaderErrorCode.MissingFirmwareBaseBin,
|
|
),
|
|
),
|
|
);
|
|
|
|
await saga.end();
|
|
});
|
|
|
|
test('unsupported mpy-cross version', async () => {
|
|
const metadata: FirmwareMetadata = {
|
|
'metadata-version': '1.0.0',
|
|
'device-id': HubType.MoveHub,
|
|
'checksum-type': 'sum',
|
|
'firmware-version': '1.2.3',
|
|
'max-firmware-size': 1024,
|
|
'mpy-abi-version': 4, // unsupported version
|
|
'mpy-cross-options': ['-mno-unicode'],
|
|
'user-mpy-offset': 100,
|
|
};
|
|
|
|
const zip = new JSZip();
|
|
zip.file('firmware-base.bin', new Uint8Array(64));
|
|
zip.file('firmware.metadata.json', JSON.stringify(metadata));
|
|
zip.file('main.py', 'print("test")');
|
|
zip.file('ReadMe_OSS.txt', 'test');
|
|
|
|
const saga = new AsyncSaga(flashFirmware, {
|
|
nextMessageId: createCountFunc(),
|
|
toasterRef: { current: mock<ToasterInstance>() },
|
|
});
|
|
|
|
// saga is triggered by this action
|
|
|
|
saga.put(
|
|
flashFirmwareAction(
|
|
await zip.generateAsync({ type: 'arraybuffer' }),
|
|
'',
|
|
),
|
|
);
|
|
|
|
// should get failure due to unsupported mpy-cross version
|
|
|
|
const action = await saga.take();
|
|
expect(action).toStrictEqual(
|
|
didFailToFinish(
|
|
FailToFinishReasonType.BadMetadata,
|
|
'mpy-abi-version',
|
|
MetadataProblem.NotSupported,
|
|
),
|
|
);
|
|
|
|
await saga.end();
|
|
});
|
|
|
|
test('compile error', async () => {
|
|
const metadata: FirmwareMetadata = {
|
|
'metadata-version': '1.0.0',
|
|
'device-id': HubType.MoveHub,
|
|
'checksum-type': 'sum',
|
|
'firmware-version': '1.2.3',
|
|
'max-firmware-size': 1024,
|
|
'mpy-abi-version': 5,
|
|
'mpy-cross-options': ['-mno-unicode'],
|
|
'user-mpy-offset': 100,
|
|
};
|
|
|
|
const zip = new JSZip();
|
|
zip.file('firmware-base.bin', new Uint8Array(64));
|
|
zip.file('firmware.metadata.json', JSON.stringify(metadata));
|
|
zip.file('main.py', 'print("test")');
|
|
zip.file('ReadMe_OSS.txt', 'test');
|
|
|
|
const saga = new AsyncSaga(flashFirmware, {
|
|
nextMessageId: createCountFunc(),
|
|
toasterRef: { current: mock<ToasterInstance>() },
|
|
});
|
|
|
|
// saga is triggered by this action
|
|
|
|
saga.put(
|
|
flashFirmwareAction(
|
|
await zip.generateAsync({ type: 'arraybuffer' }),
|
|
'',
|
|
),
|
|
);
|
|
|
|
// the first step is to compile main.py to .mpy
|
|
|
|
let action = await saga.take();
|
|
expect(action).toMatchInlineSnapshot(`
|
|
{
|
|
"abiVersion": 5,
|
|
"options": [
|
|
"-mno-unicode",
|
|
],
|
|
"script": "print("test")",
|
|
"type": "mpy.action.compile",
|
|
}
|
|
`);
|
|
|
|
// this triggers a failure
|
|
|
|
saga.put(didFailToCompile(['test']));
|
|
|
|
// compiler error should trigger firmware flash failure
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(
|
|
didFailToFinish(FailToFinishReasonType.FailedToCompile),
|
|
);
|
|
|
|
await saga.end();
|
|
});
|
|
|
|
test('firmware too big', async () => {
|
|
const metadata: FirmwareMetadata = {
|
|
'metadata-version': '1.0.0',
|
|
'device-id': HubType.MoveHub,
|
|
'checksum-type': 'sum',
|
|
'firmware-version': '1.2.3',
|
|
'max-firmware-size': 1, // low limit to trigger error
|
|
'mpy-abi-version': 5,
|
|
'mpy-cross-options': ['-mno-unicode'],
|
|
'user-mpy-offset': 100,
|
|
};
|
|
|
|
const zip = new JSZip();
|
|
zip.file('firmware-base.bin', new Uint8Array(64));
|
|
zip.file('firmware.metadata.json', JSON.stringify(metadata));
|
|
zip.file('main.py', 'print("test")');
|
|
zip.file('ReadMe_OSS.txt', 'test');
|
|
|
|
const saga = new AsyncSaga(flashFirmware, {
|
|
nextMessageId: createCountFunc(),
|
|
toasterRef: { current: mock<ToasterInstance>() },
|
|
});
|
|
|
|
// saga is triggered by this action
|
|
|
|
saga.put(
|
|
flashFirmwareAction(
|
|
await zip.generateAsync({ type: 'arraybuffer' }),
|
|
'',
|
|
),
|
|
);
|
|
|
|
// the first step is to compile main.py to .mpy
|
|
|
|
let action = await saga.take();
|
|
expect(action).toMatchInlineSnapshot(`
|
|
{
|
|
"abiVersion": 5,
|
|
"options": [
|
|
"-mno-unicode",
|
|
],
|
|
"script": "print("test")",
|
|
"type": "mpy.action.compile",
|
|
}
|
|
`);
|
|
|
|
const mpySize = 20;
|
|
const mpyBinaryData = new Uint8Array(mpySize);
|
|
saga.put(didCompile(mpyBinaryData));
|
|
|
|
// should fail due to firmware being too big
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(
|
|
didFailToFinish(FailToFinishReasonType.FirmwareSize),
|
|
);
|
|
|
|
await saga.end();
|
|
});
|
|
|
|
test('bad checksum algorithm', async () => {
|
|
const metadata: FirmwareMetadata = {
|
|
'metadata-version': '1.0.0',
|
|
'device-id': HubType.MoveHub,
|
|
// @ts-expect-error: testing bad value
|
|
'checksum-type': 'bad',
|
|
'firmware-version': '1.2.3',
|
|
'max-firmware-size': 1024,
|
|
'mpy-abi-version': 5,
|
|
'mpy-cross-options': ['-mno-unicode'],
|
|
'user-mpy-offset': 100,
|
|
};
|
|
|
|
const zip = new JSZip();
|
|
zip.file('firmware-base.bin', new Uint8Array(64));
|
|
zip.file('firmware.metadata.json', JSON.stringify(metadata));
|
|
zip.file('main.py', 'print("test")');
|
|
zip.file('ReadMe_OSS.txt', 'test');
|
|
|
|
const saga = new AsyncSaga(flashFirmware, {
|
|
nextMessageId: createCountFunc(),
|
|
toasterRef: { current: mock<ToasterInstance>() },
|
|
});
|
|
|
|
// saga is triggered by this action
|
|
|
|
saga.put(
|
|
flashFirmwareAction(
|
|
await zip.generateAsync({ type: 'arraybuffer' }),
|
|
'',
|
|
),
|
|
);
|
|
|
|
// the first step is to compile main.py to .mpy
|
|
|
|
let action = await saga.take();
|
|
expect(action).toMatchInlineSnapshot(`
|
|
{
|
|
"abiVersion": 5,
|
|
"options": [
|
|
"-mno-unicode",
|
|
],
|
|
"script": "print("test")",
|
|
"type": "mpy.action.compile",
|
|
}
|
|
`);
|
|
|
|
const mpySize = 20;
|
|
const mpyBinaryData = new Uint8Array(mpySize);
|
|
saga.put(didCompile(mpyBinaryData));
|
|
|
|
// should fail due to bad checksum algorithm
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(
|
|
didFailToFinish(
|
|
FailToFinishReasonType.BadMetadata,
|
|
'checksum-type',
|
|
MetadataProblem.NotSupported,
|
|
),
|
|
);
|
|
|
|
await saga.end();
|
|
});
|
|
|
|
test('connected device type does not match firmware device type', async () => {
|
|
const metadata: FirmwareMetadata = {
|
|
'metadata-version': '1.0.0',
|
|
'device-id': HubType.MoveHub,
|
|
'checksum-type': 'sum',
|
|
'firmware-version': '1.2.3',
|
|
'max-firmware-size': 1024,
|
|
'mpy-abi-version': 5,
|
|
'mpy-cross-options': ['-mno-unicode'],
|
|
'user-mpy-offset': 100,
|
|
};
|
|
|
|
const zip = new JSZip();
|
|
zip.file('firmware-base.bin', new Uint8Array(64));
|
|
zip.file('firmware.metadata.json', JSON.stringify(metadata));
|
|
zip.file('main.py', 'print("test")');
|
|
zip.file('ReadMe_OSS.txt', 'test');
|
|
|
|
const saga = new AsyncSaga(flashFirmware, {
|
|
nextMessageId: createCountFunc(),
|
|
toasterRef: { current: mock<ToasterInstance>() },
|
|
});
|
|
|
|
// saga is triggered by this action
|
|
|
|
saga.put(
|
|
flashFirmwareAction(
|
|
await zip.generateAsync({ type: 'arraybuffer' }),
|
|
'',
|
|
),
|
|
);
|
|
|
|
// the first step is to compile main.py to .mpy
|
|
|
|
let action = await saga.take();
|
|
expect(action).toMatchInlineSnapshot(`
|
|
{
|
|
"abiVersion": 5,
|
|
"options": [
|
|
"-mno-unicode",
|
|
],
|
|
"script": "print("test")",
|
|
"type": "mpy.action.compile",
|
|
}
|
|
`);
|
|
|
|
const mpySize = 20;
|
|
const mpyBinaryData = new Uint8Array(mpySize);
|
|
saga.put(didCompile(mpyBinaryData));
|
|
|
|
// then connect to the hub bootloader
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(connect());
|
|
|
|
saga.updateState({
|
|
bootloader: { connection: BootloaderConnectionState.Connected },
|
|
});
|
|
saga.put(didConnect());
|
|
|
|
// then find out what kind of hub it is
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(infoRequest(0));
|
|
|
|
// connected hub type does not match firmware hub type
|
|
saga.put(didRequest(0));
|
|
saga.put(infoResponse(0x01000000, 0x08005000, 0x081f800, HubType.CityHub));
|
|
|
|
// should raise an error that we don't have any firmware for this hub
|
|
|
|
action = await saga.take();
|
|
expect(action).toStrictEqual(
|
|
didFailToFinish(FailToFinishReasonType.DeviceMismatch),
|
|
);
|
|
|
|
// should request to disconnect after failure
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(disconnect());
|
|
|
|
await saga.end();
|
|
});
|
|
});
|
|
|
|
test('user supplied main.py', async () => {
|
|
const metadata: FirmwareMetadata = {
|
|
'metadata-version': '1.0.0',
|
|
'device-id': HubType.MoveHub,
|
|
'checksum-type': 'sum',
|
|
'firmware-version': '1.2.3',
|
|
'max-firmware-size': 1024,
|
|
'mpy-abi-version': 5,
|
|
'mpy-cross-options': ['-mno-unicode'],
|
|
'user-mpy-offset': 100,
|
|
};
|
|
|
|
const zip = new JSZip();
|
|
zip.file('firmware-base.bin', new Uint8Array(64));
|
|
zip.file('firmware.metadata.json', JSON.stringify(metadata));
|
|
zip.file('main.py', 'print("test")');
|
|
zip.file('ReadMe_OSS.txt', 'test');
|
|
|
|
jest.spyOn(window, 'fetch').mockResolvedValueOnce(
|
|
new Response(await zip.generateAsync({ type: 'blob' })),
|
|
);
|
|
|
|
const saga = new AsyncSaga(flashFirmware, {
|
|
nextMessageId: createCountFunc(),
|
|
toasterRef: { current: mock<ToasterInstance>() },
|
|
});
|
|
|
|
// saga is triggered by this action
|
|
|
|
saga.put(flashFirmwareAction(null, ''));
|
|
|
|
// first step is to connect to the hub bootloader
|
|
|
|
let action = await saga.take();
|
|
expect(action).toEqual(connect());
|
|
|
|
saga.updateState({
|
|
bootloader: { connection: BootloaderConnectionState.Connected },
|
|
});
|
|
saga.put(didConnect());
|
|
|
|
// then find out what kind of hub it is
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(infoRequest(0));
|
|
|
|
saga.put(didRequest(0));
|
|
saga.put(infoResponse(0x01000000, 0x08005000, 0x081f800, HubType.MoveHub));
|
|
|
|
// then compile main.py to .mpy
|
|
|
|
action = await saga.take();
|
|
expect(action).toMatchInlineSnapshot(`
|
|
{
|
|
"abiVersion": 5,
|
|
"options": [
|
|
"-mno-unicode",
|
|
],
|
|
"script": "print("test")",
|
|
"type": "mpy.action.compile",
|
|
}
|
|
`);
|
|
|
|
const mpySize = 20;
|
|
const mpyBinaryData = new Uint8Array(mpySize);
|
|
saga.put(didCompile(mpyBinaryData));
|
|
|
|
// then start flashing the firmware
|
|
|
|
// should get didStart action just before starting to erase
|
|
action = await saga.take();
|
|
expect(action).toEqual(didStart());
|
|
|
|
// erase first
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(alertsShowAlert('firmware', 'releaseButton'));
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(eraseRequest(1, /* isCityHub */ false));
|
|
|
|
saga.put(didRequest(1));
|
|
saga.put(eraseResponse(Result.OK));
|
|
// then write the new firmware
|
|
|
|
const totalFirmwareSize = metadata['user-mpy-offset'] + mpySize + 8;
|
|
action = await saga.take();
|
|
expect(action).toEqual(initRequest(2, totalFirmwareSize));
|
|
|
|
saga.put(didRequest(2));
|
|
saga.put(initResponse(Result.OK));
|
|
|
|
const dummyPayload = new ArrayBuffer(0);
|
|
let id = 2;
|
|
for (let count = 1, offset = 0; ; count++, offset += 14) {
|
|
action = await saga.take();
|
|
expect(action).toEqual(
|
|
programRequest(++id, 0x08005000 + offset, dummyPayload),
|
|
);
|
|
expect(
|
|
(action as ReturnType<typeof programRequest>).payload.byteLength,
|
|
).toBe(Math.min(14, totalFirmwareSize - offset));
|
|
|
|
saga.put(didRequest(id));
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(didProgress(offset / totalFirmwareSize));
|
|
|
|
// Have to be careful that a checksum request is not sent after
|
|
// last payload is sent, otherwise the hub gets confused.
|
|
|
|
if (offset + 14 >= totalFirmwareSize) {
|
|
break;
|
|
}
|
|
|
|
if (count % 10 === 0) {
|
|
action = await saga.take();
|
|
expect(action).toEqual(checksumRequest(++id));
|
|
|
|
saga.put(didRequest(id));
|
|
saga.put(checksumResponse(0));
|
|
}
|
|
}
|
|
|
|
// hub indicates success
|
|
|
|
saga.put(programResponse(0x27, totalFirmwareSize));
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(didProgress(1));
|
|
|
|
// and finally reboot the hub
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(rebootRequest(++id));
|
|
|
|
saga.put(didRequest(id));
|
|
|
|
// then we are done
|
|
|
|
action = await saga.take();
|
|
expect(action).toEqual(didFinish());
|
|
|
|
await saga.end();
|
|
});
|
|
});
|