mirror of
https://github.com/pybricks/pybricks-code.git
synced 2026-09-12 01:23:52 +00:00
add tests for bootloader message decoder
also fix errors found by test
This commit is contained in:
committed by
David Lechner
parent
c665ba9d1f
commit
3b22c0377f
+13
-22
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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,6 +1,7 @@
|
||||
import { all } from 'redux-saga/effects';
|
||||
import bootloader from './bootloader';
|
||||
|
||||
/* istanbul ignore next */
|
||||
export default function* (): Generator {
|
||||
yield all([bootloader()]);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user