diff --git a/src/components/notification-i18n.en.json b/src/components/notification-i18n.en.json index d0ae6e09..277d643a 100644 --- a/src/components/notification-i18n.en.json +++ b/src/components/notification-i18n.en.json @@ -13,6 +13,21 @@ "action": "Reload" } }, + "flashFirmware": { + "connectionFailed": "Could not connect to the hub. Restart the hub and try again.", + "timedOut": "The hub took too long to respond. Restart the hub and try again.", + "bleError": "There was a problem with Bluetooth.", + "disconnected": "The hub was disconnected before flashing was completed. Restart the hub and try again.", + "hubError": "The hub said something went wrong.", + "unsupportedDevice": "The connected hub is not supported.", + "deviceMismatch": "The firmware is for a different kind of hub from the connected hub.", + "failToFetch": "Failed to fetch firmware from the server: {status}", + "badZipFile": "The firmware.zip file is missing required files or is corrupt.", + "badMetadata": "The firmware.metadata.py file contains missing or invalid entries. Fix it then try again.", + "compileError": "The included main.py file could not be compiled. Fix it then try again.", + "sizeTooBig": "The combined firmware and main.py are too big to fit in the flash memory.", + "unexpectedError": "Unexpected error while trying to flash firmware: {errorMessage}" + }, "mpy": { "error": "{errorMessage}" }, diff --git a/src/components/notification-i18n.ts b/src/components/notification-i18n.ts index 48414268..c27294a6 100644 --- a/src/components/notification-i18n.ts +++ b/src/components/notification-i18n.ts @@ -10,6 +10,19 @@ export enum MessageId { BleGattPermission = 'ble.gattPermission', BleGattServiceNotFound = 'ble.gattServiceNotFound', BleNoWebBluetooth = 'ble.noWebBluetooth', + FlashFirmwareConnectionFailed = 'flashFirmware.connectionFailed', + FlashFirmwareTimedOut = 'flashFirmware.timedOut', + FlashFirmwareBleError = 'flashFirmware.bleError', + FlashFirmwareDisconnected = 'flashFirmware.disconnected', + FlashFirmwareHubError = 'flashFirmware.hubError', + FlashFirmwareUnsupportedDevice = 'flashFirmware.unsupportedDevice', + FlashFirmwareDeviceMismatch = 'flashFirmware.deviceMismatch', + FlashFirmwareFailToFetch = 'flashFirmware.failToFetch', + FlashFirmwareBadZipFile = 'flashFirmware.badZipFile', + FlashFirmwareBadMetadata = 'flashFirmware.badMetadata', + FlashFirmwareCompileError = 'flashFirmware.compileError', + FlashFirmwareSizeTooBig = 'flashFirmware.sizeTooBig', + FlashFirmwareUnexpectedError = 'flashFirmware.unexpectedError', ProgramChangedMessage = 'editor.programChanged.message', ProgramChangedAction = 'editor.programChanged.action', ServiceWorkerUpdateMessage = 'serviceWorker.update.message', diff --git a/src/sagas/notification.test.ts b/src/sagas/notification.test.ts index 28887502..0fa962a8 100644 --- a/src/sagas/notification.test.ts +++ b/src/sagas/notification.test.ts @@ -2,6 +2,7 @@ // Copyright (c) 2021 The Pybricks Authors import { IToaster } from '@blueprintjs/core'; +import { FirmwareReaderError, FirmwareReaderErrorCode } from '@pybricks/firmware'; import { AsyncSaga } from '../../test'; import { Action } from '../actions'; import { @@ -9,6 +10,12 @@ import { didFailToConnect as bleDidFailToConnect, } from '../actions/ble'; import { storageChanged } from '../actions/editor'; +import { + FailToFinishReasonType, + HubError, + MetadataProblem, + didFailToFinish, +} from '../actions/flash-firmware'; import { BootloaderConnectionFailureReason, didFailToConnect as bootloaderDidFailToConnect, @@ -37,6 +44,32 @@ test.each([ add('warning', 'message'), add('error', 'message', 'url'), didUpdate({} as ServiceWorkerRegistration), + didFailToFinish(FailToFinishReasonType.FailedToConnect), + didFailToFinish(FailToFinishReasonType.TimedOut), + didFailToFinish( + FailToFinishReasonType.BleError, + new DOMException('test error', 'NetworkError'), + ), + didFailToFinish(FailToFinishReasonType.Disconnected), + didFailToFinish(FailToFinishReasonType.HubError, HubError.UnknownCommand), + didFailToFinish(FailToFinishReasonType.NoFirmware), + didFailToFinish(FailToFinishReasonType.DeviceMismatch), + didFailToFinish( + FailToFinishReasonType.FailedToFetch, + new Response(undefined, { status: 404 }), + ), + didFailToFinish( + FailToFinishReasonType.ZipError, + new FirmwareReaderError(FirmwareReaderErrorCode.ZipError), + ), + didFailToFinish( + FailToFinishReasonType.BadMetadata, + 'device-id', + MetadataProblem.NotSupported, + ), + didFailToFinish(FailToFinishReasonType.FailedToCompile), + didFailToFinish(FailToFinishReasonType.FirmwareSize), + didFailToFinish(FailToFinishReasonType.Unknown, new Error('test error')), ])('actions that should show notification: %o', async (action: Action) => { const getToasts = jest.fn().mockReturnValue([]); const show = jest.fn(); diff --git a/src/sagas/notification.ts b/src/sagas/notification.ts index 687e0e59..1312f0a9 100644 --- a/src/sagas/notification.ts +++ b/src/sagas/notification.ts @@ -21,6 +21,11 @@ import { BleDeviceFailToConnectReasonType, } from '../actions/ble'; import { EditorActionType, reloadProgram } from '../actions/editor'; +import { + FailToFinishReasonType, + FlashFirmwareActionType, + FlashFirmwareDidFailToFinishAction, +} from '../actions/flash-firmware'; import { BootloaderConnectionActionType, BootloaderConnectionDidFailToConnectAction, @@ -226,6 +231,69 @@ function* showEditorStorageChanged(): Generator { yield put(reloadProgram()); } +function* showFlashFirmwareError( + action: FlashFirmwareDidFailToFinishAction, +): Generator { + switch (action.reason.reason) { + case FailToFinishReasonType.FailedToConnect: + yield* showSingleton(Level.Error, MessageId.FlashFirmwareConnectionFailed); + break; + case FailToFinishReasonType.TimedOut: + yield* showSingleton(Level.Error, MessageId.FlashFirmwareTimedOut); + break; + case FailToFinishReasonType.BleError: + yield* showUnexpectedError( + MessageId.FlashFirmwareBleError, + action.reason.err, + ); + break; + case FailToFinishReasonType.Disconnected: + yield* showSingleton(Level.Error, MessageId.FlashFirmwareDisconnected); + break; + case FailToFinishReasonType.HubError: + yield* showSingleton(Level.Error, MessageId.FlashFirmwareHubError); + if (process.env.NODE_ENV !== 'test') { + console.error(action.reason.hubError); + } + break; + case FailToFinishReasonType.NoFirmware: + yield* showSingleton(Level.Error, MessageId.FlashFirmwareUnsupportedDevice); + break; + case FailToFinishReasonType.DeviceMismatch: + yield* showSingleton(Level.Error, MessageId.FlashFirmwareDeviceMismatch); + break; + case FailToFinishReasonType.FailedToFetch: + yield* showSingleton(Level.Error, MessageId.FlashFirmwareFailToFetch, { + status: action.reason.response.statusText, + }); + break; + case FailToFinishReasonType.ZipError: + yield* showSingleton(Level.Error, MessageId.FlashFirmwareBadZipFile); + if (process.env.NODE_ENV !== 'test') { + console.error(action.reason.err); + } + break; + case FailToFinishReasonType.BadMetadata: + yield* showSingleton(Level.Error, MessageId.FlashFirmwareBadMetadata); + if (process.env.NODE_ENV !== 'test') { + console.error(action.reason.property, action.reason.problem); + } + break; + case FailToFinishReasonType.FailedToCompile: + yield* showSingleton(Level.Error, MessageId.FlashFirmwareCompileError); + break; + case FailToFinishReasonType.FirmwareSize: + yield* showSingleton(Level.Error, MessageId.FlashFirmwareSizeTooBig); + break; + case FailToFinishReasonType.Unknown: + yield* showUnexpectedError( + MessageId.FlashFirmwareUnexpectedError, + action.reason.err, + ); + break; + } +} + function* dismissCompilerError(): Generator { const { toaster } = (yield getContext('notification')) as NotificationContext; toaster.dismiss(MessageId.MpyError); @@ -282,6 +350,7 @@ export default function* (): Generator { showBootloaderDidFailToConnectError, ); yield takeEvery(EditorActionType.StorageChanged, showEditorStorageChanged); + yield takeEvery(FlashFirmwareActionType.DidFailToFinish, showFlashFirmwareError); yield takeEvery(MpyActionType.DidCompile, dismissCompilerError); yield takeEvery(MpyActionType.DidFailToCompile, showCompilerError); yield takeEvery(NotificationActionType.Add, addNotification);