// SPDX-License-Identifier: MIT // Copyright (c) 2020-2025 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 = { reason: T; }; export type FailToFinishReasonFailedToConnect = Reason; export type FailToFinishReasonTimedOut = Reason; export type FailToFinishReasonBleError = Reason & { error: Error; }; export type FailToFinishReasonDisconnected = Reason; export type FailToFinishReasonHubError = Reason & { hubError: HubError; }; export type FailToFinishReasonNoFirmware = Reason; export type FailToFinishReasonDeviceMismatch = Reason; export type FailToFinishReasonFailedToFetch = Reason & { response: Response; }; export type FailToFinishReasonZipError = Reason & { err: FirmwareReaderError; }; export type FailToFinishReasonBadMetadata = Reason & { property: string; problem: MetadataProblem; }; export type FailToFinishReasonFirmwareSize = Reason; export type FailToFinishReasonFailedToCompile = Reason; export type FailToFinishReasonUnknown = Reason & { 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( 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', })); /** * Versions of official LEGO EV3 firmware available for restore. */ export enum EV3OfficialFirmwareVersion { /** Official LEGO EV3 firmware version 1.09H (Home edition). */ home = '1.09H', /** Official LEGO EV3 firmware version 1.09E (Education edition). */ education = '1.09E', /** Official LEGO EV3 firmware version 1.10E (only useful for Microsoft MakeCode). */ makecode = '1.10E', } /** * Action that triggers the restore official EV3 firmware saga. */ export const firmwareRestoreOfficialEV3 = createAction( (version: EV3OfficialFirmwareVersion) => ({ type: 'firmware.action.restoreOfficialEV3', version, }), ); /** * Action that indicates {@link firmwareRestoreOfficialEV3} succeeded. */ export const firmwareDidRestoreOfficialEV3 = createAction(() => ({ type: 'firmware.action.didRestoreOfficialEV3', })); /** * Action that indicates {@link firmwareRestoreOfficialEV3} failed. */ export const firmwareDidFailToRestoreOfficialEV3 = createAction(() => ({ type: 'firmware.action.didFailToRestoreOfficialEV3', })); /** * Low-level action to flash firmware to an EV3 hub. * @param firmware The firmware binary blob. */ export const firmwareFlashEV3 = createAction((firmware: ArrayBuffer) => ({ type: 'firmware.action.flashEV3', firmware, })); /** * Low-level action that indicates {@link firmwareFlashEV3} succeeded. */ export const firmwareDidFlashEV3 = createAction(() => ({ type: 'firmware.action.didFlashEV3', })); /** * Low-level action that indicates {@link firmwareFlashEV3} failed. */ export const firmwareDidFailToFlashEV3 = createAction(() => ({ type: 'firmware.action.didFailToFlashEV3', })); export const firmwareDidReceiveEV3Reply = createAction( ( length: number, replyNumber: number, messageType: number, replyCommand: number, status: number, payload: ArrayBufferLike, ) => ({ type: 'firmware.action.didReceiveEV3Reply', length, replyNumber, messageType, replyCommand, status, payload, }), );