add proper error handling of BLE send error

This commit is contained in:
David Lechner
2021-01-22 15:58:10 -06:00
parent 18edc7870e
commit f3eef68131
5 changed files with 196 additions and 59 deletions
+76 -3
View File
@@ -50,8 +50,14 @@ type Reason<T> = {
export enum FailToStartReasonType {
/** Connecting to the hub failed. */
FailedToConnect = 'flashFirmware.failToStart.reason.failedToConnect',
/** The hub connection timed out. */
TimedOut = 'flashFirmware.failToStart.reason.timedOut',
/** Something went wrong with the BLE connection. */
BleError = 'flashFirmware.failToStart.reason.bleError',
/** The hub was disconnected. */
Disconnected = 'flashFirmware.failToStart.reason.disconnected',
/** The hub sent a response indicating a problem. */
HubError = 'flashFirmware.failToStart.reason.hubError',
/** The is no firmware available that matches the connected hub. */
NoFirmware = 'flashFirmware.failToStart.reason.noFirmware',
/** The provided firmware.zip does not match the connected hub. */
@@ -70,8 +76,18 @@ export enum FailToStartReasonType {
export type FailToStartReasonFailedToConnect = Reason<FailToStartReasonType.FailedToConnect>;
export type FailToStartReasonTimedOut = Reason<FailToStartReasonType.TimedOut>;
export type FailToStartReasonBleError = Reason<FailToStartReasonType.BleError> & {
err: Error;
};
export type FailToStartReasonDisconnected = Reason<FailToStartReasonType.Disconnected>;
export type FailToStartReasonHubError = Reason<FailToStartReasonType.HubError> & {
hubError: HubError;
};
export type FailToStartReasonNoFirmware = Reason<FailToStartReasonType.NoFirmware>;
export type FailToStartReasonDeviceMismatch = Reason<FailToStartReasonType.DeviceMismatch>;
@@ -95,7 +111,10 @@ export type FailToStartReasonUnknown = Reason<FailToStartReasonType.Unknown> & {
export type FailToStartReason =
| FailToStartReasonFailedToConnect
| FailToStartReasonTimedOut
| FailToStartReasonBleError
| FailToStartReasonDisconnected
| FailToStartReasonHubError
| FailToStartReasonNoFirmware
| FailToStartReasonDeviceMismatch
| FailToStartReasonZipError
@@ -119,7 +138,9 @@ export enum FailToFinishReasonType {
export type FailToFinishReasonTimedOut = Reason<FailToFinishReasonType.TimedOut>;
export type FailToFinishReasonBleError = Reason<FailToFinishReasonType.BleError>;
export type FailToFinishReasonBleError = Reason<FailToFinishReasonType.BleError> & {
err: Error;
};
export type FailToFinishReasonDisconnected = Reason<FailToFinishReasonType.Disconnected>;
@@ -170,6 +191,16 @@ export type FlashFirmwareDidFailToStartAction = Action<FlashFirmwareActionType.D
reason: FailToStartReason;
};
export function didFailToStart(
reason: FailToStartReasonType.BleError,
err: Error,
): FlashFirmwareDidFailToStartAction;
export function didFailToStart(
reason: FailToStartReasonType.HubError,
hubError: HubError,
): FlashFirmwareDidFailToStartAction;
export function didFailToStart(
reason: FailToStartReasonType.ZipError,
err: FirmwareReaderError,
@@ -189,6 +220,8 @@ export function didFailToStart(
export function didFailToStart(
reason: Exclude<
FailToStartReasonType,
| FailToStartReasonType.BleError
| FailToStartReasonType.HubError
| FailToStartReasonType.ZipError
| FailToStartReasonType.BadMetadata
| FailToStartReasonType.Unknown
@@ -201,9 +234,31 @@ export function didFailToStart(
*/
export function didFailToStart(
reason: FailToStartReasonType,
arg1?: string | Error,
arg1?: string | HubError | Error,
arg2?: MetadataProblem,
): FlashFirmwareDidFailToStartAction {
if (reason === FailToStartReasonType.BleError) {
// istanbul ignore if: programmer error give wrong arg
if (!(arg1 instanceof Error)) {
throw new Error('missing or invalid err');
}
return {
type: FlashFirmwareActionType.DidFailToStart,
reason: { reason, err: arg1 },
};
}
if (reason === FailToStartReasonType.HubError) {
// istanbul ignore if: programmer error give wrong arg
if (!isHubError(arg1)) {
throw new Error('missing or invalid hubError');
}
return {
type: FlashFirmwareActionType.DidFailToStart,
reason: { reason, hubError: arg1 },
};
}
if (reason === FailToStartReasonType.ZipError) {
// istanbul ignore if: programmer error give wrong arg
if (!(arg1 instanceof FirmwareReaderError)) {
@@ -281,6 +336,11 @@ export type FlashFirmwareDidFailToFinishAction = Action<FlashFirmwareActionType.
reason: FailToFinishReason;
};
export function didFailToFinish(
reason: FailToFinishReasonType.BleError,
err: Error,
): FlashFirmwareDidFailToFinishAction;
export function didFailToFinish(
reason: FailToFinishReasonType.HubError,
hubError: HubError,
@@ -294,7 +354,9 @@ export function didFailToFinish(
export function didFailToFinish(
reason: Exclude<
FailToFinishReasonType,
FailToFinishReasonType.HubError | FailToFinishReasonType.Unknown
| FailToFinishReasonType.BleError
| FailToFinishReasonType.HubError
| FailToFinishReasonType.Unknown
>,
): FlashFirmwareDidFailToFinishAction;
@@ -303,6 +365,17 @@ export function didFailToFinish(
reason: FailToFinishReasonType,
arg1?: HubError | Error,
): FlashFirmwareDidFailToFinishAction {
if (reason === FailToFinishReasonType.BleError) {
// istanbul ignore if: programmer error give wrong arg
if (!(arg1 instanceof Error)) {
throw new Error('missing or invalid err');
}
return {
type: FlashFirmwareActionType.DidFailToFinish,
reason: { reason, err: arg1 },
};
}
if (reason === FailToFinishReasonType.HubError) {
// istanbul ignore if: programmer error give wrong arg
if (!isHubError(arg1)) {
+10
View File
@@ -41,6 +41,10 @@ export enum BootloaderConnectionActionType {
* The connection received a message.
*/
DidReceive = 'bootloader.action.connection.did.receive',
/**
* Initiate disconnection/
*/
Disconnect = 'bootloader.action.connection.disconnect',
/**
* The connection has been closed.
*/
@@ -59,6 +63,12 @@ export function didConnect(): BootloaderConnectionDidConnectAction {
return { type: BootloaderConnectionActionType.DidConnect };
}
export type BootloaderConnectionDisconnectAction = Action<BootloaderConnectionActionType.Disconnect>;
export function disconnect(): BootloaderConnectionDisconnectAction {
return { type: BootloaderConnectionActionType.Disconnect };
}
/**
* Possible reasons a device could fail to connect.
*/
+63
View File
@@ -27,6 +27,7 @@ import {
didDisconnect,
didFailToConnect,
didRequest,
disconnect,
eraseRequest,
eraseResponse,
infoRequest,
@@ -297,6 +298,68 @@ describe('flashFirmware', () => {
await saga.end();
});
test('fail to send info request', async () => {
const metadata: FirmwareMetadata = {
'metadata-version': '1.0.0',
'device-id': HubType.MoveHub,
'checksum-type': 'sum',
'firmware-version': '1.2.3',
'max-firmware-size': 1024,
'mpy-abi-version': 5,
'mpy-cross-options': ['-mno-unicode'],
'user-mpy-offset': 100,
};
const zip = new JSZip();
zip.file('firmware-base.bin', new Uint8Array(64));
zip.file('firmware.metadata.json', JSON.stringify(metadata));
zip.file('main.py', 'print("test")');
zip.file('ReadMe_OSS.txt', 'test');
jest.spyOn(window, 'fetch').mockResolvedValueOnce(
new Response(await zip.generateAsync({ type: 'blob' })),
);
const saga = new AsyncSaga(flashFirmware, {
nextMessageId: createCountFunc(),
});
saga.setState({ settings: { flashCurrentProgram: false } });
// saga is triggered by this action
saga.put(flashFirmwareAction());
// first step is to connect to the hub bootloader
let action = await saga.take();
expect(action).toEqual(connect());
saga.put(didConnect());
// then find out what kind of hub it is
action = await saga.take();
expect(action).toEqual(infoRequest(0));
const testError = new Error('test');
saga.put(didRequest(0, testError));
// should get a failure to start
action = await saga.take();
expect(action).toEqual(
didFailToStart(FailToStartReasonType.BleError, testError),
);
// should request to disconnect after failure
action = await saga.take();
expect(action).toEqual(disconnect());
await saga.end();
});
});
describe('user supplied firmware.zip', () => {
+46 -56
View File
@@ -20,12 +20,12 @@ import {
takeEvery,
} from 'typed-redux-saga/macro';
import { Action } from '../actions';
import { disconnect } from '../actions/ble';
import {
FailToFinishReasonType,
FailToStartReasonType,
FlashFirmwareActionType,
FlashFirmwareFlashAction,
HubError,
MetadataProblem,
didFailToFinish,
didFailToStart,
@@ -48,6 +48,7 @@ import {
BootloaderResponseActionType,
checksumRequest,
connect,
disconnect,
disconnectRequest,
eraseRequest,
infoRequest,
@@ -74,9 +75,16 @@ const firmwareZipMap = new Map<HubType, string>([
]);
function* waitForDidRequest(id: number): SagaGenerator<BootloaderDidRequestAction> {
return yield* take<BootloaderDidRequestAction>(
const request = yield* take<BootloaderDidRequestAction>(
(a: Action) => a.type === BootloaderDidRequestType && a.id === id,
);
if (request.err) {
yield* put(didFailToStart(FailToStartReasonType.BleError, request.err));
yield* put(disconnect());
yield* cancel();
}
return request;
}
/**
@@ -88,16 +96,30 @@ function* waitForDidRequest(id: number): SagaGenerator<BootloaderDidRequestActio
function* waitForResponse<T extends BootloaderResponseAction>(
type: BootloaderResponseActionType,
timeout = 500,
): SagaGenerator<{
response?: T;
error?: BootloaderErrorResponseAction;
timeout?: boolean;
}> {
return yield* race({
): SagaGenerator<T> {
const { response, error, timedOut } = yield* race({
response: take<T>(type),
error: take<BootloaderErrorResponseAction>(BootloaderResponseActionType.Error),
timeout: delay(timeout),
timedOut: delay(timeout),
});
if (timedOut) {
yield* put(didFailToStart(FailToStartReasonType.TimedOut));
yield* put(disconnect());
yield* cancel();
}
if (error) {
yield* put(
didFailToStart(FailToStartReasonType.HubError, HubError.UnknownCommand),
);
yield* put(disconnect());
cancel();
}
defined(response);
return response;
}
function* firmwareIterator(data: DataView, maxSize: number): Generator<number> {
@@ -206,16 +228,13 @@ function* loadFirmware(
}
/**
* The purpose of this function is two-fold. If the BLE device is disconnected,
* then it will raise a failure action and cancel the task (including the
* parent task). Or, if the parent task fails, it will disconnect the BLE device
* and return.
* Monitors for BLE disconnection event. If disconnection occurs, then a failure
* action is raised and the task (including the parent task) is canceled.
*/
function* disconnectMonitor(): SagaGenerator<void> {
const { disconnectedBeforeStart, failedToStart } = yield* race({
const { disconnectedBeforeStart } = yield* race({
disconnectedBeforeStart: take(BootloaderConnectionActionType.DidDisconnect),
started: take(FlashFirmwareActionType.DidStart),
failedToStart: take(FlashFirmwareActionType.DidFailToStart),
});
if (disconnectedBeforeStart) {
@@ -223,17 +242,11 @@ function* disconnectMonitor(): SagaGenerator<void> {
yield* cancel();
}
if (failedToStart) {
yield* put(disconnect());
return;
}
// if we get here, `started` won the race
const { disconnectedAfterStart, failedToFinish } = yield* race({
const { disconnectedAfterStart } = yield* race({
disconnectedAfterStart: take(BootloaderConnectionActionType.DidDisconnect),
finished: take(FlashFirmwareActionType.DidFinish),
failedToFinish: take(FlashFirmwareActionType.DidFailToFinish),
});
if (disconnectedAfterStart) {
@@ -241,11 +254,6 @@ function* disconnectMonitor(): SagaGenerator<void> {
yield* cancel();
}
if (failedToFinish) {
yield* put(disconnect());
return;
}
// if we get here, `finished` won the race.
}
@@ -301,18 +309,13 @@ function* flashFirmware(action: FlashFirmwareFlashAction): Generator {
BootloaderResponseActionType.Info,
),
});
if (!info.response) {
throw Error(`failed to get info: ${info}`);
}
if (deviceId !== undefined && info.response.hubType !== deviceId) {
throw Error(
`Connected to ${info.response.hubType} but firmware is for ${deviceId}`,
);
if (deviceId !== undefined && info.hubType !== deviceId) {
throw Error(`Connected to ${info.hubType} but firmware is for ${deviceId}`);
}
if (firmware === undefined) {
const firmwarePath = firmwareZipMap.get(info.response.hubType);
const firmwarePath = firmwareZipMap.get(info.hubType);
if (firmwarePath === undefined) {
yield* put(
notification.add(
@@ -335,10 +338,8 @@ function* flashFirmware(action: FlashFirmwareFlashAction): Generator {
const data = yield* call(() => response.arrayBuffer());
({ firmware, deviceId } = yield* loadFirmware(data, program));
if (deviceId !== undefined && info.response.hubType !== deviceId) {
throw Error(
`Connected to ${info.response.hubType} but firmware is for ${deviceId}`,
);
if (deviceId !== undefined && info.hubType !== deviceId) {
throw Error(`Connected to ${info.hubType} but firmware is for ${deviceId}`);
}
}
@@ -352,7 +353,7 @@ function* flashFirmware(action: FlashFirmwareFlashAction): Generator {
5000,
),
});
if (!erase.response || erase.response.result) {
if (erase.result) {
// TODO: proper error handling
throw Error(`Failed to erase: ${erase}`);
}
@@ -364,22 +365,18 @@ function* flashFirmware(action: FlashFirmwareFlashAction): Generator {
BootloaderResponseActionType.Init,
),
});
if (!init.response || init.response.result) {
if (init.result) {
// TODO: proper error handling
throw Error(`Failed to init: ${init}`);
}
// 14 is "safe" size for all hubs
const maxDataSize = MaxProgramFlashSize.get(info.response.hubType) || 14;
const maxDataSize = MaxProgramFlashSize.get(info.hubType) || 14;
for (let count = 1, offset = 0; ; count++) {
const payload = firmware.slice(offset, offset + maxDataSize);
const programAction = yield* put(
programRequest(
nextMessageId(),
info.response.startAddress + offset,
payload.buffer,
),
programRequest(nextMessageId(), info.startAddress + offset, payload.buffer),
);
yield* waitForDidRequest(programAction.id);
@@ -398,17 +395,13 @@ function* flashFirmware(action: FlashFirmwareFlashAction): Generator {
// the hub is not known and could vary by device.
if (count % 10 === 0) {
const checksumAction = yield* put(checksumRequest(nextMessageId()));
const { checksum } = yield* all({
yield* all({
sent: waitForDidRequest(checksumAction.id),
checksum: waitForResponse<BootloaderChecksumResponseAction>(
BootloaderResponseActionType.Checksum,
5000,
),
});
if (!checksum.response) {
// TODO: proper error handling
throw Error(`Failed to get checksum: ${checksum}`);
}
}
}
@@ -416,10 +409,7 @@ function* flashFirmware(action: FlashFirmwareFlashAction): Generator {
BootloaderResponseActionType.Program,
5000,
);
if (!flash.response) {
throw Error(`failed to get final response: ${flash}`);
}
if (flash.response.count !== firmware.length) {
if (flash.count !== firmware.length) {
// TODO: proper error handling
throw Error("Didn't flash all bytes");
}
+1
View File
@@ -143,6 +143,7 @@ function* connect(_action: BootloaderConnectionAction): Generator {
yield takeEvery(notificationChannel, handleNotify);
yield takeEvery(BootloaderConnectionActionType.Send, write, characteristic);
yield takeEvery(BootloaderConnectionActionType.Disconnect, server.disconnect);
yield put(didConnect());