Skip to content

Commit de5443e

Browse files
Connection flow tweaks to deal with unusual cases (#220)
- Wait for gatt.connect promise in order to show micro:bit connecting dialog - Wait for the micro:bit to be in a good state after disconnecting before reconnecting (Bluetooth connection on Windows only) - Add code to show a reasonable connection help dialog on an initial connect failure as opposed to a reconnect failure - Improve error types and use these to show the right connect help dialog - Add a timeout around bluetooth.requestDevice - this addresses a bug where the browser device selection prompt is not shown. We instead reload the page and show our connection dialog at the appropriate part of the flow.
1 parent 1130579 commit de5443e

File tree

14 files changed

+170
-69
lines changed

14 files changed

+170
-69
lines changed

src/App.svelte

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,22 @@
2727
import ConnectDialogContainer from './components/connection-prompt/ConnectDialogContainer.svelte';
2828
import { Paths, currentPath, getTitle, navigate } from './router/paths';
2929
import HomeIcon from 'virtual:icons/ri/home-2-line';
30+
import { btSelectMicrobitDialogOnLoad } from './script/stores/connectionStore';
31+
import {
32+
ConnectDialogStates,
33+
connectionDialogState,
34+
} from './script/stores/connectDialogStore';
3035
3136
onMount(() => {
3237
const { bluetooth, usb } = get(compatibility);
3338
// Value must switch from false to true after mount to trigger dialog transition
3439
isCompatibilityWarningDialogOpen.set(!bluetooth && !usb);
40+
41+
if ($btSelectMicrobitDialogOnLoad) {
42+
$connectionDialogState.connectionState =
43+
ConnectDialogStates.CONNECT_TUTORIAL_BLUETOOTH;
44+
$btSelectMicrobitDialogOnLoad = false;
45+
}
3546
});
3647
3748
let routeAnnouncementEl: HTMLDivElement | undefined;

src/StaticConfiguration.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export enum HexOrigin {
1818

1919
class StaticConfiguration {
2020
public static readonly connectTimeoutDuration: number = 10000;
21+
public static readonly requestDeviceTimeoutDuration: number = 30000;
2122

2223
// After how long should we consider the connection lost if ping was not able to conclude?
2324
public static readonly connectionLostTimeoutDuration: number = 3000;

src/components/PleaseConnectFirst.svelte

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
import StandardButton from './StandardButton.svelte';
1515
1616
const handleInputConnect = async () => {
17-
if ($state.showReconnectHelp || Microbits.getInputMicrobit()) {
17+
if ($state.showConnectHelp || Microbits.getInputMicrobit()) {
1818
reconnect();
1919
} else {
2020
startConnectionProcess();
@@ -35,7 +35,7 @@
3535
disabled={$state.reconnectState.reconnecting}
3636
onClick={handleInputConnect}
3737
>{$t(
38-
$state.showReconnectHelp || Microbits.getInputMicrobit()
38+
$state.showConnectHelp || Microbits.getInputMicrobit()
3939
? 'actions.reconnect'
4040
: 'footer.connectButton',
4141
)}</StandardButton>

src/components/ReconnectHelp.svelte

Lines changed: 49 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
import { state } from '../script/stores/uiStore';
1212
import { reconnect } from '../script/utils/reconnect';
1313
import StandardDialog from './dialogs/StandardDialog.svelte';
14-
import { stateOnHideReconnectHelp } from '../script/microbit-interfacing/state-updaters';
14+
import { stateOnHideConnectHelp } from '../script/microbit-interfacing/state-updaters';
15+
import { startConnectionProcess } from '../script/stores/connectDialogStore';
1516
1617
export let isOpen: boolean = false;
1718
@@ -20,13 +21,17 @@
2021
case 'bluetooth': {
2122
return {
2223
heading:
23-
$state.showReconnectHelp === 'userTriggered'
24-
? 'reconnectFailed.bluetoothHeading'
25-
: 'disconnectedWarning.bluetoothHeading',
24+
$state.showConnectHelp === 'connect'
25+
? 'connectFailed.bluetoothHeading'
26+
: $state.showConnectHelp === 'userReconnect'
27+
? 'reconnectFailed.bluetoothHeading'
28+
: 'disconnectedWarning.bluetoothHeading',
2629
subtitle:
27-
$state.showReconnectHelp === 'userTriggered'
28-
? 'reconnectFailed.bluetooth1'
29-
: 'disconnectedWarning.bluetooth1',
30+
$state.showConnectHelp === 'connect'
31+
? 'connectFailed.bluetooth1'
32+
: $state.showConnectHelp === 'userReconnect'
33+
? 'reconnectFailed.bluetooth1'
34+
: 'disconnectedWarning.bluetooth1',
3035
listHeading: 'disconnectedWarning.bluetooth2',
3136
bulletOne: 'disconnectedWarning.bluetooth3',
3237
bulletTwo: 'disconnectedWarning.bluetooth4',
@@ -35,13 +40,17 @@
3540
case 'bridge': {
3641
return {
3742
heading:
38-
$state.showReconnectHelp === 'userTriggered'
39-
? 'reconnectFailed.bridgeHeading'
40-
: 'disconnectedWarning.bridgeHeading',
43+
$state.showConnectHelp === 'connect'
44+
? 'connectFailed.bridgeHeading'
45+
: $state.showConnectHelp === 'userReconnect'
46+
? 'reconnectFailed.bridgeHeading'
47+
: 'disconnectedWarning.bridgeHeading',
4148
subtitle:
42-
$state.showReconnectHelp === 'userTriggered'
43-
? 'reconnectFailed.bridge1'
44-
: 'disconnectedWarning.bridge1',
49+
$state.showConnectHelp === 'connect'
50+
? 'connectFailed.bridge1'
51+
: $state.showConnectHelp === 'userReconnect'
52+
? 'reconnectFailed.bridge1'
53+
: 'disconnectedWarning.bridge1',
4554
listHeading: 'connectMB.usbTryAgain.replugMicrobit2',
4655
bulletOne: 'connectMB.usbTryAgain.replugMicrobit3',
4756
bulletTwo: 'connectMB.usbTryAgain.replugMicrobit4',
@@ -50,13 +59,17 @@
5059
case 'remote': {
5160
return {
5261
heading:
53-
$state.showReconnectHelp === 'userTriggered'
54-
? 'reconnectFailed.remoteHeading'
55-
: 'disconnectedWarning.remoteHeading',
62+
$state.showConnectHelp === 'connect'
63+
? 'connectFailed.remoteHeading'
64+
: $state.showConnectHelp === 'userReconnect'
65+
? 'reconnectFailed.remoteHeading'
66+
: 'disconnectedWarning.remoteHeading',
5667
subtitle:
57-
$state.showReconnectHelp === 'userTriggered'
58-
? 'reconnectFailed.remote1'
59-
: 'disconnectedWarning.remote1',
68+
$state.showConnectHelp === 'connect'
69+
? 'connectFailed.remote1'
70+
: $state.showConnectHelp === 'userReconnect'
71+
? 'reconnectFailed.remote1'
72+
: 'disconnectedWarning.remote1',
6073
listHeading: 'disconnectedWarning.bluetooth2',
6174
bulletOne: 'disconnectedWarning.bluetooth3',
6275
bulletTwo: 'disconnectedWarning.bluetooth4',
@@ -73,10 +86,19 @@
7386
}
7487
}
7588
})();
89+
90+
const handleReconnect = () => {
91+
if ($state.showConnectHelp === 'connect') {
92+
stateOnHideConnectHelp();
93+
startConnectionProcess();
94+
} else {
95+
reconnect(true);
96+
}
97+
};
7698
</script>
7799

78100
{#if $state.reconnectState.connectionType !== 'none'}
79-
<StandardDialog {isOpen} onClose={stateOnHideReconnectHelp} class="w-150 space-y-5">
101+
<StandardDialog {isOpen} onClose={stateOnHideConnectHelp} class="w-150 space-y-5">
80102
<svelte:fragment slot="heading">
81103
{$t(content.heading)}
82104
</svelte:fragment>
@@ -100,10 +122,14 @@
100122
{$t('connectMB.troubleshooting')}
101123
<ExternalLinkIcon />
102124
</a>
103-
<StandardButton onClick={stateOnHideReconnectHelp}
125+
<StandardButton onClick={stateOnHideConnectHelp}
104126
>{$t('actions.cancel')}</StandardButton>
105-
<StandardButton type="primary" onClick={() => reconnect(true)}
106-
>{$t('actions.reconnect')}</StandardButton>
127+
<StandardButton type="primary" onClick={handleReconnect}
128+
>{$t(
129+
$state.showConnectHelp === 'connect'
130+
? 'footer.connectButton'
131+
: 'actions.reconnect',
132+
)}</StandardButton>
107133
</div>
108134
</svelte:fragment>
109135
</StandardDialog>

src/components/bottom/ConnectedLiveGraphButtons.svelte

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
};
2424
2525
const handleInputConnect = async () => {
26-
if ($state.showReconnectHelp || Microbits.getInputMicrobit()) {
26+
if ($state.showConnectHelp || Microbits.getInputMicrobit()) {
2727
reconnect();
2828
} else {
2929
startConnectionProcess();
@@ -55,7 +55,7 @@
5555
disabled={$state.reconnectState.reconnecting}
5656
size="small"
5757
>{$t(
58-
$state.showReconnectHelp || Microbits.getInputMicrobit()
58+
$state.showConnectHelp || Microbits.getInputMicrobit()
5959
? 'actions.reconnect'
6060
: 'footer.connectButton',
6161
)}</StandardButton>

src/components/dialogs/StandardDialog.svelte

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,9 @@
3333
}
3434
};
3535
36-
const onOpenChange: CreateDialogProps['onOpenChange'] = ({ next }) => {
37-
if (!next) {
36+
const onOpenChange: CreateDialogProps['onOpenChange'] = ({ curr, next }) => {
37+
// Use curr so we don't call onCloseDialog() on page load.
38+
if (curr && !next) {
3839
onCloseDialog();
3940
} else {
4041
onOpenDialog();
@@ -73,9 +74,13 @@
7374
const sync = createSync(states);
7475
$: sync.open(isOpen, v => (isOpen = v));
7576
77+
let prevOpen: boolean;
7678
$: if (isOpen) {
7779
onOpenDialog();
78-
} else {
80+
prevOpen = isOpen;
81+
} else if (prevOpen && !isOpen) {
82+
// Use prevOpen so we don't call onCloseDialog() on page load.
83+
prevOpen = isOpen;
7984
onCloseDialog();
8085
}
8186

src/messages/ui.en.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,11 +208,17 @@
208208
"connectMB.reconnecting": "Reconnecting…",
209209
"connectMB.bluetooth.invalidPattern": "The pattern you have drawn is invalid.",
210210

211+
"connectFailed.bluetoothHeading": "Failed to connect to micro:bit",
212+
"connectFailed.bluetooth1": "The connection to the micro:bit could not be established.",
211213
"reconnectFailed.radioHeading": "Failed to reconnect to micro:bits",
212214
"reconnectFailed.bluetoothHeading": "Failed to reconnect to micro:bit",
213215
"reconnectFailed.bluetooth1": "The connection to the micro:bit could not be re-established.",
216+
"connectFailed.remoteHeading": "Failed to connect to micro:bit 1",
217+
"connectFailed.remote1": "The connection to the micro:bit you are wearing could not be established.",
214218
"reconnectFailed.remoteHeading": "Failed to reconnect to micro:bit 1",
215219
"reconnectFailed.remote1": "The connection to the micro:bit you are wearing could not be re-established.",
220+
"connectFailed.bridgeHeading": "Failed to connect to micro:bit 2",
221+
"connectFailed.bridge1": "The connection to the micro:bit connected to your computer could not be established.",
216222
"reconnectFailed.bridgeHeading": "Failed to reconnect to micro:bit 2",
217223
"reconnectFailed.bridge1": "The connection to the micro:bit connected to your computer could not be re-established.",
218224
"reconnectFailed.subtitle": "Follow these instructions to restart the connection process.",

src/script/microbit-interfacing/MicrobitBluetooth.ts

Lines changed: 52 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
* SPDX-License-Identifier: MIT
55
*/
66

7+
import Bowser from 'bowser';
78
import StaticConfiguration from '../../StaticConfiguration';
89
import { outputting } from '../stores/uiStore';
910
import { logError, logMessage } from '../utils/logging';
@@ -22,6 +23,10 @@ import {
2223
stateOnReady,
2324
stateOnReconnectionAttempt,
2425
} from './state-updaters';
26+
import { btSelectMicrobitDialogOnLoad } from '../stores/connectionStore';
27+
28+
const browser = Bowser.getParser(window.navigator.userAgent);
29+
const isWindowsOS = browser.getOSName() === 'Windows';
2530

2631
/**
2732
* UART data target. For fixing type compatibility issues.
@@ -57,6 +62,8 @@ export class MicrobitBluetooth implements MicrobitConnection {
5762
private gattConnectPromise: Promise<MBSpecs.MBVersion | undefined> | undefined;
5863
private disconnectPromise: Promise<unknown> | undefined;
5964
private connecting = false;
65+
private isReconnect = false;
66+
private reconnectReadyPromise: Promise<void> | undefined;
6067

6168
private outputWriteQueue: {
6269
busy: boolean;
@@ -77,6 +84,10 @@ export class MicrobitBluetooth implements MicrobitConnection {
7784
logMessage('Bluetooth connect', states);
7885
if (this.duringExplicitConnectDisconnect) {
7986
logMessage('Skipping connect attempt when one is already in progress');
87+
// Wait for the gattConnectPromise while showing a "connecting" dialog.
88+
// If the user clicks disconnect while the automatic reconnect is in progress,
89+
// then clicks reconnect, we need to wait rather than return immediately.
90+
await this.gattConnectPromise;
8091
return;
8192
}
8293
this.duringExplicitConnectDisconnect++;
@@ -188,16 +199,29 @@ export class MicrobitBluetooth implements MicrobitConnection {
188199
} finally {
189200
this.duringExplicitConnectDisconnect--;
190201
}
202+
this.reconnectReadyPromise = new Promise(resolve => setTimeout(resolve, 3_500));
191203
if (updateState) {
192204
this.inUseAs.forEach(value =>
193-
stateOnDisconnected(value, userTriggered, 'bluetooth'),
205+
stateOnDisconnected(
206+
value,
207+
userTriggered ? false : this.isReconnect ? 'autoReconnect' : 'connect',
208+
'bluetooth',
209+
),
194210
);
195211
}
196212
}
197213

198214
async reconnect(): Promise<void> {
199215
logMessage('Bluetooth reconnect');
216+
this.isReconnect = true;
200217
const as = Array.from(this.inUseAs);
218+
if (isWindowsOS) {
219+
// On Windows, the micro:bit can take around 3 seconds to respond to gatt.disconnect().
220+
// Attempting to reconnect before the micro:bit has responded results in another
221+
// gattserverdisconnected event being fired. We then fail to get primaryService on a
222+
// disconnected GATT server.
223+
await this.reconnectReadyPromise;
224+
}
201225
await this.connect(...as);
202226
}
203227

@@ -214,7 +238,7 @@ export class MicrobitBluetooth implements MicrobitConnection {
214238
}
215239
} catch (e) {
216240
logError('Bluetooth connect triggered by disconnect listener failed', e);
217-
this.inUseAs.forEach(s => stateOnDisconnected(s, false, 'bluetooth'));
241+
this.inUseAs.forEach(s => stateOnDisconnected(s, 'autoReconnect', 'bluetooth'));
218242
}
219243
};
220244

@@ -486,17 +510,32 @@ export const startBluetoothConnection = async (
486510

487511
const requestDevice = async (name: string): Promise<BluetoothDevice | undefined> => {
488512
try {
489-
return navigator.bluetooth.requestDevice({
490-
filters: [{ namePrefix: `BBC micro:bit [${name}]` }],
491-
optionalServices: [
492-
MBSpecs.Services.UART_SERVICE,
493-
MBSpecs.Services.ACCEL_SERVICE,
494-
MBSpecs.Services.DEVICE_INFO_SERVICE,
495-
MBSpecs.Services.LED_SERVICE,
496-
MBSpecs.Services.IO_SERVICE,
497-
MBSpecs.Services.BUTTON_SERVICE,
498-
],
499-
});
513+
// In some situations the Chrome device prompt simply doesn't appear so we time this out after 30 seconds and reload the page
514+
const result = await Promise.race([
515+
navigator.bluetooth.requestDevice({
516+
filters: [{ namePrefix: `BBC micro:bit [${name}]` }],
517+
optionalServices: [
518+
MBSpecs.Services.UART_SERVICE,
519+
MBSpecs.Services.ACCEL_SERVICE,
520+
MBSpecs.Services.DEVICE_INFO_SERVICE,
521+
MBSpecs.Services.LED_SERVICE,
522+
MBSpecs.Services.IO_SERVICE,
523+
MBSpecs.Services.BUTTON_SERVICE,
524+
],
525+
}),
526+
new Promise<'timeout'>(resolve =>
527+
setTimeout(
528+
() => resolve('timeout'),
529+
StaticConfiguration.requestDeviceTimeoutDuration,
530+
),
531+
),
532+
]);
533+
if (result === 'timeout') {
534+
btSelectMicrobitDialogOnLoad.set(true);
535+
window.location.reload();
536+
return undefined;
537+
}
538+
return result;
500539
} catch (e) {
501540
logError('Bluetooth request device failed/cancelled', e);
502541
return undefined;

0 commit comments

Comments
 (0)