mirror of
https://github.com/pybricks/pybricks-code.git
synced 2026-09-12 09:36:27 +00:00
add proper error handling of BLE send error
This commit is contained in:
@@ -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)) {
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
Reference in New Issue
Block a user