44 * SPDX-License-Identifier: MIT
55 */
66
7- import { logError } from '../utils/logging' ;
7+ import { logError , logMessage } from '../utils/logging' ;
88import MicrobitConnection , { DeviceRequestStates } from './MicrobitConnection' ;
99import MicrobitUSB from './MicrobitUSB' ;
1010import { onAccelerometerChange , onButtonChange } from './change-listeners' ;
@@ -16,13 +16,17 @@ import {
1616 stateOnFailedToConnect ,
1717 stateOnReady ,
1818} from './state-updaters' ;
19+ import StaticConfiguration from '../../StaticConfiguration' ;
1920
2021export class MicrobitSerial implements MicrobitConnection {
2122 private responseMap = new Map <
2223 number ,
2324 ( value : protocol . MessageResponse | PromiseLike < protocol . MessageResponse > ) => void
2425 > ( ) ;
2526
27+ // To avoid concurrent connect attempts
28+ private isConnecting : boolean = false ;
29+
2630 // TODO: The radio frequency should be randomly generated once per session.
2731 // If we want a session to be restored (e.g. from local storage) and
2832 // the previously flashed micro:bits to continue working without
@@ -31,10 +35,18 @@ export class MicrobitSerial implements MicrobitConnection {
3135 // to configure the radio frequency for both micro:bits after they
3236 // are flashed, not just the radio bridge.
3337 private sessionRadioFrequency = 42 ;
38+ private connectionCheckIntervalId : ReturnType < typeof setInterval > | undefined ;
39+ private lastReceivedMessageTimestamp : number | undefined ;
3440
3541 constructor ( private usb : MicrobitUSB ) { }
3642
3743 async connect ( ...states : DeviceRequestStates [ ] ) : Promise < void > {
44+ logMessage ( 'Serial connect' , states ) ;
45+ if ( this . isConnecting ) {
46+ logMessage ( 'Skipping connect attempt when one is already in progress' ) ;
47+ return ;
48+ }
49+ this . isConnecting = true ;
3850 let unprocessedData = '' ;
3951 let previousButtonState = { A : 0 , B : 0 } ;
4052
@@ -46,6 +58,8 @@ export class MicrobitSerial implements MicrobitConnection {
4658 const messages = protocol . splitMessages ( unprocessedData + data ) ;
4759 unprocessedData = messages . remainingInput ;
4860 messages . messages . forEach ( async msg => {
61+ this . lastReceivedMessageTimestamp = Date . now ( ) ;
62+
4963 // Messages are either periodic sensor data or command/response
5064 const sensorData = protocol . processPeriodicMessage ( msg ) ;
5165 if ( sensorData ) {
@@ -80,6 +94,26 @@ export class MicrobitSerial implements MicrobitConnection {
8094 await this . handshake ( ) ;
8195 stateOnConnected ( DeviceRequestStates . INPUT ) ;
8296
97+ // Check for USB being unplugged
98+ navigator . usb . addEventListener ( 'disconnect' , ( ) => {
99+ logMessage ( 'USB disconnected' ) ;
100+ this . stopConnectionCheck ( ) ;
101+ } ) ;
102+
103+ // Check for connection lost
104+ if ( this . connectionCheckIntervalId === undefined ) {
105+ this . connectionCheckIntervalId = setInterval ( async ( ) => {
106+ const allowedTimeWithoutMessageInMs =
107+ StaticConfiguration . connectTimeoutDuration ;
108+ if (
109+ this . lastReceivedMessageTimestamp &&
110+ Date . now ( ) - this . lastReceivedMessageTimestamp > allowedTimeWithoutMessageInMs
111+ ) {
112+ await this . handleReconnect ( ) ;
113+ }
114+ } , 1000 ) ;
115+ }
116+
83117 // Set the radio frequency to a value unique to this session
84118 const radioFreqCommand = protocol . generateCmdRadioFrequency (
85119 this . sessionRadioFrequency ,
@@ -104,26 +138,55 @@ export class MicrobitSerial implements MicrobitConnection {
104138
105139 stateOnAssigned ( DeviceRequestStates . INPUT , this . usb . getModelNumber ( ) ) ;
106140 stateOnReady ( DeviceRequestStates . INPUT ) ;
141+ logMessage ( 'Serial successfully connected' ) ;
107142 } catch ( e ) {
108143 logError ( 'Failed to initialise serial protocol' , e ) ;
109144 stateOnFailedToConnect ( DeviceRequestStates . INPUT ) ;
110145 await this . usb . stopSerial ( ) ;
111146 throw e ;
147+ } finally {
148+ this . isConnecting = false ;
112149 }
113150 }
114151
115152 async disconnect ( ) : Promise < void > {
153+ this . stopConnectionCheck ( ) ;
116154 return this . disconnectInternal ( true ) ;
117155 }
118156
157+ private stopConnectionCheck ( ) {
158+ clearInterval ( this . connectionCheckIntervalId ) ;
159+ this . connectionCheckIntervalId = undefined ;
160+ this . lastReceivedMessageTimestamp = undefined ;
161+ }
162+
119163 private async disconnectInternal ( userDisconnect : boolean ) : Promise < void > {
120164 // We might want to send command to stop streaming here?
121165 this . responseMap . clear ( ) ;
122166 await this . usb . stopSerial ( ) ;
123167 stateOnDisconnected ( DeviceRequestStates . INPUT , userDisconnect ) ;
124168 }
125169
170+ async handleReconnect ( ) : Promise < void > {
171+ if ( this . isConnecting ) {
172+ logMessage ( 'Serial disconnect ignored... reconnect already in progress' ) ;
173+ return ;
174+ }
175+ try {
176+ this . stopConnectionCheck ( ) ;
177+ logMessage ( 'Serial disconnected... automatically trying to reconnect' ) ;
178+ await this . usb . softwareReset ( ) ;
179+ await this . usb . stopSerial ( ) ;
180+ await this . reconnect ( ) ;
181+ } catch ( e ) {
182+ logError ( 'Serial connect triggered by disconnect listener failed' , e ) ;
183+ } finally {
184+ this . isConnecting = false ;
185+ }
186+ }
187+
126188 async reconnect ( ) : Promise < void > {
189+ logMessage ( 'Serial reconnect' ) ;
127190 await this . connect ( DeviceRequestStates . INPUT ) ;
128191 }
129192
0 commit comments