firmware: add windows BLE hacks

This works around some issues that have been observed on a specific
Windows computer.

Issue: https://github.com/pybricks/support/issues/256
This commit is contained in:
David Lechner
2021-05-16 16:18:34 -05:00
parent b001748f95
commit 956b6f3c96
7 changed files with 63 additions and 19 deletions
+7 -7
View File
@@ -136,7 +136,7 @@ describe('flashFirmware', () => {
// erase first
action = await saga.take();
expect(action).toEqual(eraseRequest(1));
expect(action).toEqual(eraseRequest(1, /* isCityHub */ false));
saga.put(didRequest(1));
saga.put(eraseResponse(Result.OK));
@@ -857,7 +857,7 @@ describe('flashFirmware', () => {
// erase first
action = await saga.take();
expect(action).toEqual(eraseRequest(1));
expect(action).toEqual(eraseRequest(1, /* isCityHub */ false));
saga.put(didRequest(1));
saga.put(eraseResponse(Result.Error));
@@ -958,7 +958,7 @@ describe('flashFirmware', () => {
// erase first
action = await saga.take();
expect(action).toEqual(eraseRequest(1));
expect(action).toEqual(eraseRequest(1, /* isCityHub */ false));
saga.put(didRequest(1));
saga.put(eraseResponse(Result.OK));
@@ -1068,7 +1068,7 @@ describe('flashFirmware', () => {
// erase first
action = await saga.take();
expect(action).toEqual(eraseRequest(1));
expect(action).toEqual(eraseRequest(1, /* isCityHub */ false));
saga.put(didRequest(1));
saga.put(eraseResponse(Result.OK));
@@ -1218,7 +1218,7 @@ describe('flashFirmware', () => {
// erase first
action = await saga.take();
expect(action).toEqual(eraseRequest(1));
expect(action).toEqual(eraseRequest(1, /* isCityHub */ false));
saga.put(didRequest(1));
saga.put(eraseResponse(Result.OK));
@@ -1370,7 +1370,7 @@ describe('flashFirmware', () => {
// erase first
action = await saga.take();
expect(action).toEqual(eraseRequest(1));
expect(action).toEqual(eraseRequest(1, /* isCityHub */ false));
saga.put(didRequest(1));
saga.put(eraseResponse(Result.OK));
@@ -1900,7 +1900,7 @@ describe('flashFirmware', () => {
// erase first
action = await saga.take();
expect(action).toEqual(eraseRequest(1));
expect(action).toEqual(eraseRequest(1, /* isCityHub */ false));
saga.put(didRequest(1));
saga.put(eraseResponse(Result.OK));
+13 -1
View File
@@ -38,6 +38,7 @@ import {
connect,
disconnect,
eraseRequest,
eraseResponse,
infoRequest,
initRequest,
programRequest,
@@ -126,6 +127,15 @@ function* waitForResponse<T extends BootloaderResponseAction>(
});
if (timedOut) {
// istanbul ignore if: this hacks around a hardware/OS issue
if (type === BootloaderResponseActionType.Erase) {
// It has been observed that sometimes this response is not received
// or gets stuck in the Bluetooth stack until another request is sent.
// So, we ignore the timeout and continue. If there really was a
// problem, then the next request should fail anyway.
console.warn('Timeout waiting for erase response, continuing anyway.');
return eraseResponse(Result.OK) as T;
}
yield* put(didFailToFinish(FailToFinishReasonType.TimedOut));
yield* disconnectAndCancel();
}
@@ -335,7 +345,9 @@ function* flashFirmware(action: FlashFirmwareFlashAction): Generator {
yield* put(didStart());
const eraseAction = yield* put(eraseRequest(nextMessageId()));
const eraseAction = yield* put(
eraseRequest(nextMessageId(), deviceId === HubType.CityHub),
);
const { erase } = yield* all({
sent: waitForDidRequest(eraseAction.id),
erase: waitForResponse<BootloaderEraseResponseAction>(
+9 -3
View File
@@ -251,13 +251,19 @@ type BaseBootloaderRequestAction<T extends BootloaderRequestActionType> = Action
* Action that requests to erase the flash memory.
*/
export type BootloaderEraseRequestAction =
BaseBootloaderRequestAction<BootloaderRequestActionType.Erase>;
BaseBootloaderRequestAction<BootloaderRequestActionType.Erase> & {
/* City hub requires special handling due to buggy bootloader */
isCityHub: boolean;
};
/**
* Creates a request to erase the flash memory.
*/
export function eraseRequest(id: number): BootloaderEraseRequestAction {
return { type: BootloaderRequestActionType.Erase, id };
export function eraseRequest(
id: number,
isCityHub: boolean,
): BootloaderEraseRequestAction {
return { type: BootloaderRequestActionType.Erase, id, isCityHub };
}
/**
+7 -6
View File
@@ -33,7 +33,7 @@ describe('message encoder', () => {
test.each([
[
'erase',
eraseRequest(0),
eraseRequest(0, /* isCityHub */ false),
[
0x11, // erase command
],
@@ -116,6 +116,7 @@ describe('message encoder', () => {
],
])('encode %s request', async (_n, request, expected) => {
const messageTypesThatShouldBeCalledWithoutResponse = [
BootloaderRequestActionType.Erase,
BootloaderRequestActionType.Program,
BootloaderRequestActionType.Reboot,
BootloaderRequestActionType.Disconnect,
@@ -137,10 +138,10 @@ describe('message encoder', () => {
const saga = new AsyncSaga(bootloader);
// we send 4 requests
saga.put(eraseRequest(0));
saga.put(eraseRequest(1));
saga.put(eraseRequest(2));
saga.put(eraseRequest(3));
saga.put(checksumRequest(0));
saga.put(checksumRequest(1));
saga.put(checksumRequest(2));
saga.put(checksumRequest(3));
// but only two didSend action meaning only the first two completed
saga.put(didSend());
@@ -152,7 +153,7 @@ describe('message encoder', () => {
const numPending = saga.numPending();
expect(numPending).toEqual(5);
const message = new Uint8Array([Command.EraseFlash]);
const message = new Uint8Array([Command.GetChecksum]);
const nextId = createCountFunc();
// every other action is the "send" action
+7 -1
View File
@@ -13,6 +13,7 @@ import {
} from 'typed-redux-saga/macro';
import { Action } from '../actions';
import { hex } from '../utils';
import { isWindows } from '../utils/os';
import {
BootloaderConnectionActionType,
BootloaderConnectionDidFailToSendAction,
@@ -79,7 +80,12 @@ function* encodeRequest(): Generator {
switch (action.type) {
case BootloaderRequestActionType.Erase:
yield* put(send(createEraseFlashRequest()));
yield* put(
send(
createEraseFlashRequest(),
/* withResponse */ action.isCityHub && !isWindows(),
),
);
break;
case BootloaderRequestActionType.Program:
yield* put(
+12 -1
View File
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2021 The Pybricks Authors
import { isMacOS, prefersDarkMode } from './os';
import { isMacOS, isWindows, prefersDarkMode } from './os';
describe('isMacOS', () => {
test('is true', () => {
@@ -14,6 +14,17 @@ describe('isMacOS', () => {
});
});
describe('isWindows', () => {
test('is true', () => {
jest.spyOn(navigator, 'platform', 'get').mockReturnValue('Win32');
expect(isWindows()).toBeTruthy();
});
test('is false', () => {
jest.spyOn(navigator, 'platform', 'get').mockReturnValue('MacIntel');
expect(isWindows()).toBeFalsy();
});
});
describe('prefersDarkMode', () => {
test('is true', () => {
window.matchMedia = jest.fn().mockReturnValue({
+8
View File
@@ -11,6 +11,14 @@ export function isMacOS(): boolean {
return /mac/i.test(navigator.platform);
}
/**
* Tests if we are running on Windows.
* @returns `true` if running on Windows, otherwise `false`.
*/
export function isWindows(): boolean {
return /win/i.test(navigator.platform);
}
/**
* Tests if the OS is set to dark mode.
* @returns: `true` if dark mode should be preferred, otherwise `false`.