mirror of
https://github.com/pybricks/pybricks-code.git
synced 2026-09-12 09:36:27 +00:00
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.
432 lines
13 KiB
TypeScript
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',
|
|
}));
|