add tests for bootloader message decoder

also fix errors found by test
This commit is contained in:
David Lechner
2020-05-21 21:27:47 -05:00
committed by David Lechner
parent c665ba9d1f
commit 3b22c0377f
5 changed files with 156 additions and 23 deletions
+13 -22
View File
@@ -1,5 +1,7 @@
// Ref: https://lego.github.io/lego-ble-wireless-protocol-docs/index.html#lego-hub-boot-loader-service
import { assert } from '../utils';
/**
* LEGO Powered Up Bootloader Service UUID.
*/
@@ -186,13 +188,11 @@ export function getMessageType(msg: DataView): Command | ErrorMessage {
* @param msg The raw message data.
*/
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) {
if (msg.getUint8(2) !== ErrorBytecode) {
throw Error('expecting error');
}
if (msg.getUint8(0) !== 5) {
throw Error('unexpected length');
}
if (msg.getUint8(4) !== ErrorCode.UnknownCommand) {
// "command not recognized" is only possible error code
throw Error('unexpected error code');
@@ -207,9 +207,7 @@ export function parseErrorResponse(msg: DataView): Command {
* @returns The result of the erase operation.
*/
export function parseEraseFlashResponse(msg: DataView): Result {
if (msg.getUint8(0) !== Command.EraseFlash) {
throw Error('expecting erase flash command');
}
assert(msg.getUint8(0) === Command.EraseFlash, 'expecting erase flash command');
const result = msg.getUint8(1);
return result;
}
@@ -220,9 +218,7 @@ export function parseEraseFlashResponse(msg: DataView): Result {
* @returns The final checksum and the number of bytes written.
*/
export function parseProgramFlashResponse(msg: DataView): [number, number] {
if (msg.getUint8(0) !== Command.ProgramFlash) {
throw Error('expecting program flash command');
}
assert(msg.getUint8(0) === Command.ProgramFlash, 'expecting program flash command');
const checksum = msg.getUint8(1);
const count = msg.getUint32(2, true);
return [checksum, count];
@@ -234,9 +230,7 @@ export function parseProgramFlashResponse(msg: DataView): [number, number] {
* @returns The result of the initialization.
*/
export function parseInitLoaderResponse(msg: DataView): Result {
if (msg.getUint8(0) !== Command.InitLoader) {
throw Error('expecting init loader command');
}
assert(msg.getUint8(0) === Command.InitLoader, 'expecting init loader command');
const result = msg.getUint8(1);
return result;
}
@@ -248,9 +242,7 @@ export function parseInitLoaderResponse(msg: DataView): Result {
* of where firmware can be flashed, and the hub type identifier.
*/
export function parseGetInfoResponse(msg: DataView): [number, number, number, HubType] {
if (msg.getUint8(0) !== Command.GetInfo) {
throw Error('expecting get info command');
}
assert(msg.getUint8(0) === Command.GetInfo, 'expecting get info command');
const version = msg.getUint32(1, true);
const startAddress = msg.getUint32(5, true);
const endAddress = msg.getUint32(9, true);
@@ -264,9 +256,7 @@ export function parseGetInfoResponse(msg: DataView): [number, number, number, Hu
* @returns The checksum of the data that has been flashed so far.
*/
export function parseGetChecksumResponse(msg: DataView): number {
if (msg.getUint8(0) !== Command.GetChecksum) {
throw Error('expecting get checksum command');
}
assert(msg.getUint8(0) === Command.GetChecksum, 'expecting get checksum command');
const checksum = msg.getUint8(1);
return checksum;
}
@@ -277,9 +267,10 @@ export function parseGetChecksumResponse(msg: DataView): number {
* @returns The protection level
*/
export function parseGetFlashStateResponse(msg: DataView): ProtectionLevel {
if (msg.getUint8(0) !== Command.GetFlashState) {
throw Error('expecting get flash state command');
}
assert(
msg.getUint8(0) === Command.GetFlashState,
'expecting get flash state command',
);
const level = msg.getUint8(1);
return level;
}
+125
View File
@@ -0,0 +1,125 @@
import { Action } from 'redux';
import { runSaga, stdChannel } from 'redux-saga';
import { BootloaderResponseActionType, didReceive } from '../actions/bootloader';
import { Command, HubType, ProtectionLevel, Result } from '../protocols/bootloader';
import bootloader from './bootloader';
describe('message decoder', () => {
test.each([
[
'erase',
[
0x11, // erase command
0xff, // success
],
{
type: BootloaderResponseActionType.Erase,
result: Result.Error,
},
],
[
'flash',
[
0x22, // flash command
0xaa, // checksum
0xa0, // byte count LSB
0x86, // .
0x01, // .
0x00, // byte count MSB
],
{
type: BootloaderResponseActionType.Program,
checksum: 0xaa,
count: 100000,
},
],
[
'init',
[
0x44, // init command
0xff, // success
],
{
type: BootloaderResponseActionType.Init,
result: Result.Error,
},
],
[
'info',
[
0x55, // info command
0x78, // version LSB
0x56, // .
0x34, // .
0x12, // version MSB
0x00, // start address LSB
0x50, // .
0x00, // .
0x08, // start address MSB
0xff, // end address LSB
0xf7, // .
0x01, // .
0x08, // end address MSB
0x40, // hub type ID
],
{
type: BootloaderResponseActionType.Info,
version: 0x12345678,
startAddress: 0x08005000,
endAddress: 0x0801f7ff,
hubType: HubType.MoveHub,
},
],
[
'checksum',
[
0x66, // checksum command
0xaa, // checksum
],
{
type: BootloaderResponseActionType.Checksum,
checksum: 0xaa,
},
],
[
'state',
[
0x77, // flash state command
0x02, // protection level
],
{
type: BootloaderResponseActionType.State,
level: ProtectionLevel.Level2,
},
],
[
'error',
[
0x05, // length
0x00, // unused (hub id)
0x05, // flash loader error message
0x77, // get flash state command
0x05, // command not recognized
],
{
type: BootloaderResponseActionType.Error,
command: Command.GetFlashState,
},
],
])('decode %s response', async (_n, message, expected) => {
const response = new Uint8Array(message);
const channel = stdChannel();
const dispatched = new Array<Action>();
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);
});
});
+5 -1
View File
@@ -35,6 +35,7 @@ import {
connect,
eraseRequest,
eraseResponse,
errorResponse,
infoRequest,
infoResponse,
initRequest,
@@ -62,6 +63,7 @@ import {
createStartAppRequest,
getMessageType,
parseEraseFlashResponse,
parseErrorResponse,
parseGetChecksumResponse,
parseGetFlashStateResponse,
parseGetInfoResponse,
@@ -147,10 +149,12 @@ function* decodeResponse(action: BootloaderConnectionDidReceiveAction): Generato
yield put(stateResponse(parseGetFlashStateResponse(action.data)));
break;
case ErrorBytecode:
yield put(stateResponse(parseGetFlashStateResponse(action.data)));
yield put(errorResponse(parseErrorResponse(action.data)));
break;
/* istanbul ignore next: should not be possible to reach */
default:
console.error(`Unknown bootloader response action ${action}`);
break;
}
}
+1
View File
@@ -1,6 +1,7 @@
import { all } from 'redux-saga/effects';
import bootloader from './bootloader';
/* istanbul ignore next */
export default function* (): Generator {
yield all([bootloader()]);
}
+12
View File
@@ -0,0 +1,12 @@
/**
* Asserts that an assumption is true. This is used to detect programmer errors
* and should never actually throw in a correctly written program.
* @param condition A condition that is assumed to be true
* @param message Informational message for debugging
*/
export function assert(condition: boolean, message: string): void {
/* istanbul ignore next */
if (!condition) {
throw Error(message);
}
}