Files
pybricks-code/src/firmware/actions.ts
T
David Lechner cf99ab223d firmware/sagas: refactor firmwareFlashUsbDfu
This allows the firmwareFlashUsbDfu to take an arbitrary binary blob
instead of a Pybricks firmware.zip file. This will allow it to be used
to restore the official LEGO firmware as well.
2022-11-18 19:28:00 -06:00

432 lines
13 KiB
TypeScript

// SPDX-License-Identifier: MIT
// Copyright (c) 2020-2022 The Pybricks Authors
import { FirmwareReaderError, HubType } from '@pybricks/firmware';
import { createAction } from '../actions';
import { Hub } from '../components/hubPicker';
export enum MetadataProblem {
Missing = 'metadata.missing',
NotSupported = 'metadata.notSupported',
}
export enum HubError {
UnknownCommand = 'hubError.unknownCommand',
EraseFailed = 'hubError.eraseFailed',
InitFailed = 'hubError.initFailed',
CountMismatch = 'hubError.countMismatch',
ChecksumMismatch = 'hubError.checksumMismatch',
}
function isHubError(arg: unknown): arg is HubError {
return Object.values(HubError).includes(arg as HubError);
}
export enum FailToFinishReasonType {
/** Connecting to the hub failed. */
FailedToConnect = 'flashFirmware.failToFinish.reason.failedToConnect',
/** The hub connection timed out. */
TimedOut = 'flashFirmware.failToFinish.reason.timedOut',
/** Something went wrong with the BLE connection. */
BleError = 'flashFirmware.failToFinish.reason.bleError',
/** The hub was disconnected. */
Disconnected = 'flashFirmware.failToFinish.reason.disconnected',
/** The hub sent a response indicating a problem. */
HubError = 'flashFirmware.failToFinish.reason.hubError',
/** The is no firmware available that matches the connected hub. */
NoFirmware = 'flashFirmware.failToFinish.reason.noFirmware',
/** The provided firmware.zip does not match the connected hub. */
DeviceMismatch = 'flashFirmware.failToFinish.reason.deviceMismatch',
/** Failed to fetch firmware from the server. */
FailedToFetch = 'flashFirmware.failToFinish.reason.failedToFetch',
/** There was a problem with the zip file. */
ZipError = 'flashFirmware.failToFinish.reason.zipError',
/** Metadata property is missing or invalid. */
BadMetadata = 'flashFirmware.failToFinish.reason.badMetadata',
/** The main.py file failed to compile. */
FailedToCompile = 'flashFirmware.failToFinish.reason.failedToCompile',
/** The combined firmware-base.bin and main.mpy are too big. */
FirmwareSize = 'flashFirmware.failToFinish.reason.firmwareSize',
/** An unexpected error occurred. */
Unknown = 'flashFirmware.failToFinish.reason.unknown',
}
type Reason<T extends FailToFinishReasonType> = {
reason: T;
};
export type FailToFinishReasonFailedToConnect =
Reason<FailToFinishReasonType.FailedToConnect>;
export type FailToFinishReasonTimedOut = Reason<FailToFinishReasonType.TimedOut>;
export type FailToFinishReasonBleError = Reason<FailToFinishReasonType.BleError> & {
error: Error;
};
export type FailToFinishReasonDisconnected =
Reason<FailToFinishReasonType.Disconnected>;
export type FailToFinishReasonHubError = Reason<FailToFinishReasonType.HubError> & {
hubError: HubError;
};
export type FailToFinishReasonNoFirmware = Reason<FailToFinishReasonType.NoFirmware>;
export type FailToFinishReasonDeviceMismatch =
Reason<FailToFinishReasonType.DeviceMismatch>;
export type FailToFinishReasonFailedToFetch =
Reason<FailToFinishReasonType.FailedToFetch> & {
response: Response;
};
export type FailToFinishReasonZipError = Reason<FailToFinishReasonType.ZipError> & {
err: FirmwareReaderError;
};
export type FailToFinishReasonBadMetadata =
Reason<FailToFinishReasonType.BadMetadata> & {
property: string;
problem: MetadataProblem;
};
export type FailToFinishReasonFirmwareSize =
Reason<FailToFinishReasonType.FirmwareSize>;
export type FailToFinishReasonFailedToCompile =
Reason<FailToFinishReasonType.FailedToCompile>;
export type FailToFinishReasonUnknown = Reason<FailToFinishReasonType.Unknown> & {
error: Error;
};
export type FailToFinishReason =
| FailToFinishReasonFailedToConnect
| FailToFinishReasonTimedOut
| FailToFinishReasonBleError
| FailToFinishReasonDisconnected
| FailToFinishReasonHubError
| FailToFinishReasonNoFirmware
| FailToFinishReasonDeviceMismatch
| FailToFinishReasonFailedToFetch
| FailToFinishReasonZipError
| FailToFinishReasonBadMetadata
| FailToFinishReasonFirmwareSize
| FailToFinishReasonFailedToCompile
| FailToFinishReasonUnknown;
// High-level bootloader actions.
/**
* Creates a new action to flash firmware to a hub.
* @param data The firmware zip file data or `null` to get firmware later.
* @param hubName A custom hub name or an empty string to use the default name.
*/
export const flashFirmware = createAction(
(data: ArrayBuffer | null, hubName: string) => ({
type: 'flashFirmware.action.flashFirmware',
data,
hubName,
}),
);
/**
* Action that indicates flashing firmware started.
* @param total The total number of bytes to be flashed.
*/
export const didStart = createAction(() => ({
type: 'flashFirmware.action.didStart',
}));
/**
* Action that indicates current firmware flashing progress.
* @param value The current progress (0 to 1).
*/
export const didProgress = createAction((value: number) => {
// assert(value >= 0 && value <= 1, 'value out of range');
return { type: 'flashFirmware.action.didProgress', value };
});
/** Action that indicates that flashing firmware completed successfully. */
export const didFinish = createAction(() => ({
type: 'flashFirmware.action.didFinish',
}));
function isError(err: unknown): err is Error {
const maybeError = err as Error;
return (
maybeError !== undefined &&
typeof maybeError.name === 'string' &&
typeof maybeError.message === 'string'
);
}
// FIXME: get rid of this monstrosity
const didFailToFinishType = 'flashFirmware.action.didFailToFinish';
function didFailToFinishCreator(reason: FailToFinishReasonType.FailedToConnect): {
type: typeof didFailToFinishType;
reason: FailToFinishReasonFailedToConnect;
};
function didFailToFinishCreator(reason: FailToFinishReasonType.TimedOut): {
type: typeof didFailToFinishType;
reason: FailToFinishReasonTimedOut;
};
function didFailToFinishCreator(
reason: FailToFinishReasonType.BleError,
error: Error,
): {
type: typeof didFailToFinishType;
reason: FailToFinishReasonBleError;
};
function didFailToFinishCreator(reason: FailToFinishReasonType.Disconnected): {
type: typeof didFailToFinishType;
reason: FailToFinishReasonDisconnected;
};
function didFailToFinishCreator(
reason: FailToFinishReasonType.HubError,
hubError: HubError,
): {
type: typeof didFailToFinishType;
reason: FailToFinishReasonHubError;
};
function didFailToFinishCreator(reason: FailToFinishReasonType.NoFirmware): {
type: typeof didFailToFinishType;
reason: FailToFinishReasonNoFirmware;
};
function didFailToFinishCreator(reason: FailToFinishReasonType.DeviceMismatch): {
type: typeof didFailToFinishType;
reason: FailToFinishReasonDeviceMismatch;
};
function didFailToFinishCreator(
reason: FailToFinishReasonType.FailedToFetch,
response: Response,
): {
type: typeof didFailToFinishType;
reason: FailToFinishReasonFailedToFetch;
};
function didFailToFinishCreator(
reason: FailToFinishReasonType.ZipError,
err: FirmwareReaderError,
): {
type: typeof didFailToFinishType;
reason: FailToFinishReasonZipError;
};
function didFailToFinishCreator(
reason: FailToFinishReasonType.BadMetadata,
property: string,
problem: MetadataProblem,
): {
type: typeof didFailToFinishType;
reason: FailToFinishReasonBadMetadata;
};
function didFailToFinishCreator(reason: FailToFinishReasonType.FirmwareSize): {
type: typeof didFailToFinishType;
reason: FailToFinishReasonFirmwareSize;
};
function didFailToFinishCreator(reason: FailToFinishReasonType.FailedToCompile): {
type: typeof didFailToFinishType;
reason: FailToFinishReasonFailedToCompile;
};
function didFailToFinishCreator(
reason: FailToFinishReasonType.Unknown,
error: Error,
): {
type: typeof didFailToFinishType;
reason: FailToFinishReasonUnknown;
};
function didFailToFinishCreator<T extends FailToFinishReasonType>(
reason: T,
arg1?: string | HubError | Error | Response,
arg2?: MetadataProblem,
): {
type: typeof didFailToFinishType;
reason: FailToFinishReason;
};
function didFailToFinishCreator(
reason: FailToFinishReasonType,
arg1?: string | HubError | Error | Response,
arg2?: MetadataProblem,
): {
type: typeof didFailToFinishType;
reason: FailToFinishReason;
} {
if (reason === FailToFinishReasonType.BleError) {
// istanbul ignore if: programmer error give wrong arg
if (!isError(arg1)) {
throw new Error('missing or invalid err');
}
return {
type: didFailToFinishType,
reason: { reason, error: arg1 },
};
}
if (reason === FailToFinishReasonType.HubError) {
// istanbul ignore if: programmer error give wrong arg
if (!isHubError(arg1)) {
throw new Error('missing or invalid hubError');
}
return {
type: didFailToFinishType,
reason: { reason, hubError: arg1 },
};
}
if (reason === FailToFinishReasonType.FailedToFetch) {
// istanbul ignore if: programmer error give wrong arg
if (!(arg1 instanceof Response)) {
throw new Error('missing or invalid response');
}
return {
type: didFailToFinishType,
reason: { reason, response: arg1 },
};
}
if (reason === FailToFinishReasonType.ZipError) {
// istanbul ignore if: programmer error give wrong arg
if (!(arg1 instanceof FirmwareReaderError)) {
throw new Error('missing or invalid err');
}
return {
type: didFailToFinishType,
reason: { reason, err: arg1 },
};
}
if (reason === FailToFinishReasonType.BadMetadata) {
// istanbul ignore if: programmer error give wrong arg
if (
arg1 !== 'metadata-version' &&
arg1 !== 'firmware-version' &&
arg1 !== 'device-id' &&
arg1 !== 'checksum-type' &&
arg1 !== 'mpy-abi-version' &&
arg1 !== 'mpy-cross-options' &&
arg1 !== 'user-mpy-offset' &&
arg1 !== 'max-firmware-size' &&
arg1 !== 'checksum-size' &&
arg1 !== 'hub-name-size'
) {
throw new Error('missing or invalid property');
}
// istanbul ignore if: programmer error give wrong arg
if (arg2 === undefined) {
throw new Error('missing or invalid problem');
}
return {
type: didFailToFinishType,
reason: { reason, property: arg1, problem: arg2 },
};
}
if (reason === FailToFinishReasonType.Unknown) {
// istanbul ignore if: programmer error give wrong arg
if (!isError(arg1)) {
throw new Error('missing or invalid err');
}
return {
type: didFailToFinishType,
reason: { reason, error: arg1 },
};
}
return { type: didFailToFinishType, reason: { reason } };
}
/**
* Action that indicates flashing did not start because of an error.
* @param total The total number of bytes to be flashed.
*/
export const didFailToFinish = createAction(didFailToFinishCreator);
/**
* Low-level action to flash firmware using LEGO's DFU over USB.
* @param firmware The firmware binary blob.
* @param hubType The hub type the firmware blob is for.
*/
export const firmwareFlashUsbDfu = createAction(
(firmware: ArrayBuffer, hubType: HubType) => ({
type: 'firmware.action.flashUsbDfu',
firmware,
hubType,
}),
);
/**
* Low-level action that indicates {@link firmwareFlashUsbDfu} succeeded.
*/
export const firmwareDidFlashUsbDfu = createAction(() => ({
type: 'firmware.action.didFlashUsbDfu',
}));
/**
* Low-level action that indicates {@link firmwareFlashUsbDfu} failed.
*/
export const firmwareDidFailToFlashUsbDfu = createAction(() => ({
type: 'firmware.action.didFailToFlashUsbDfu',
}));
// High-level actions
/**
* Action that triggers the install Pybricks firmware saga.
*/
export const firmwareInstallPybricks = createAction(() => ({
type: 'firmware.action.installPybricks',
}));
/**
* Action that indicates {@link firmwareInstallPybricks} succeeded.
*/
export const firmwareDidInstallPybricks = createAction(() => ({
type: 'firmware.action.didInstallPybricks',
}));
/**
* Action that indicates {@link firmwareInstallPybricks} failed.
*/
export const firmwareDidFailToInstallPybricks = createAction(() => ({
type: 'firmware.action.didFailToInstallPybricks',
}));
/**
* Action that triggers the restore official DFU firmware saga.
*/
export const firmwareRestoreOfficialDfu = createAction((hub: Hub) => ({
type: 'firmware.action.restoreOfficialDfu',
hub,
}));
/**
* Action that indicates {@link firmwareRestoreOfficialDfu} succeeded.
*/
export const firmwareDidRestoreOfficialDfu = createAction(() => ({
type: 'firmware.action.didRestoreOfficialDfu',
}));
/**
* Action that indicates {@link firmwareRestoreOfficialDfu} failed.
*/
export const firmwareDidFailToRestoreOfficialDfu = createAction(() => ({
type: 'firmware.action.didFailToRestoreOfficialDfu',
}));