44 * SPDX-License-Identifier: MIT
55 */
66
7+ import Bowser from 'bowser' ;
78import StaticConfiguration from '../../StaticConfiguration' ;
89import { outputting } from '../stores/uiStore' ;
910import { 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
487511const 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