diff --git a/src/protocols/bootloader.ts b/src/protocols/bootloader.ts index b9e8f37c..700f4ea9 100644 --- a/src/protocols/bootloader.ts +++ b/src/protocols/bootloader.ts @@ -77,6 +77,20 @@ export enum ProtectionLevel { Level2 = 0x02, } +/** + * Protocol error. Thrown e.g. when there is a malformed message. + */ +export class ProtocolError extends Error { + /** + * Creates a new ProtocolError. + * @param message The error message + * @param data The bytecodes that caused the error + */ + constructor(message: string, public data: DataView) { + super(message); + } +} + /** * Creates a new message to erase the flash memory. */ @@ -192,11 +206,20 @@ export function parseErrorResponse(msg: DataView): Command { assert(msg.getUint8(0) === 5, 'unexpected length'); // Error responses are ordered differently compared to command responses. if (msg.getUint8(2) !== ErrorBytecode) { - throw Error('expecting error'); + throw new ProtocolError( + `expecting error bytecode 0x05 but got 0x${msg + .getUint8(2) + .toString(16) + .padStart(2, '0')}`, + msg, + ); } if (msg.getUint8(4) !== ErrorCode.UnknownCommand) { // "command not recognized" is only possible error code - throw Error('unexpected error code'); + throw new ProtocolError( + `unknown error code: 0x${msg.getUint8(4).toString(16).padStart(2, '0')}`, + msg, + ); } const command = msg.getUint8(3); return command; diff --git a/src/sagas/bootloader.test.ts b/src/sagas/bootloader.test.ts index e516ee26..f8132b16 100644 --- a/src/sagas/bootloader.test.ts +++ b/src/sagas/bootloader.test.ts @@ -7,6 +7,7 @@ import { BootloaderRequestActionType, checksumRequest, checksumResponse, + didError, didReceive, didRequest, didSend, @@ -25,7 +26,13 @@ import { stateRequest, stateResponse, } from '../actions/bootloader'; -import { Command, HubType, ProtectionLevel, Result } from '../protocols/bootloader'; +import { + Command, + HubType, + ProtectionLevel, + ProtocolError, + Result, +} from '../protocols/bootloader'; import { createCountFunc } from '../utils/iter'; import bootloader from './bootloader'; @@ -271,4 +278,70 @@ describe('message decoder', () => { await task.toPromise(); expect(dispatched[0]).toEqual(expected); }); + + test.each([ + [ + 'bad error bytecode', + [ + 0x05, // length + 0x00, // unused (hub id) + 0x04, // **invalid message type** + 0x77, // get flash state command + 0x05, // command not recognized + ], + didError( + new ProtocolError( + 'expecting error bytecode 0x05 but got 0x04', + new DataView(new Uint8Array().buffer), + ), + ), + ], + [ + 'unknown error code', + [ + 0x05, // length + 0x00, // unused (hub id) + 0x05, // flash loader error message + 0x77, // get flash state command + 0x04, // **invalid error code** + ], + didError( + new ProtocolError( + 'unknown error code: 0x04', + new DataView(new Uint8Array().buffer), + ), + ), + ], + [ + 'unknown response', + [ + 0x00, // **bad command** + 0x01, // **junk** + 0x02, // **junk** + 0x03, // **junk** + 0x04, // **junk** + ], + didError( + new ProtocolError( + 'unknown bootloader response type: 0x00', + new DataView(new Uint8Array().buffer), + ), + ), + ], + ])('protocol error', async (_n, message, expected) => { + const response = new Uint8Array(message); + const channel = stdChannel(); + const dispatched = new Array(); + const task = runSaga( + { + channel, + dispatch: (action: Action) => dispatched.push(action), + }, + bootloader, + ); + channel.put(didReceive(new DataView(response.buffer))); + task.cancel(); + await task.toPromise(); + expect(dispatched[0]).toEqual(expected); + }); }); diff --git a/src/sagas/bootloader.ts b/src/sagas/bootloader.ts index 3541b35a..a9cbd1cc 100644 --- a/src/sagas/bootloader.ts +++ b/src/sagas/bootloader.ts @@ -42,6 +42,7 @@ import { checksumRequest, checksumResponse, connect, + didError, didRequest, disconnectRequest, eraseRequest, @@ -65,6 +66,7 @@ import { ErrorBytecode, HubType, MaxProgramFlashSize, + ProtocolError, createDisconnectRequest, createEraseFlashRequest, createGetChecksumRequest, @@ -184,11 +186,15 @@ function* decodeResponse(action: BootloaderConnectionDidReceiveAction): Generato yield put(errorResponse(parseErrorResponse(action.data))); break; default: - throw new Error(`Unknown bootloader response action ${action}`); + throw new ProtocolError( + `unknown bootloader response type: 0x${responseType + .toString(16) + .padStart(2, '0')}`, + action.data, + ); } } catch (err) { - // TODO: dispatch an error action - console.error(`Error decoding message: ${err}`); + yield put(didError(err)); } } diff --git a/tsconfig.json b/tsconfig.json index eed8da05..17f28503 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "es5", + "target": "es2018", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true,