44 * SPDX-License-Identifier: MIT
55 */
66
7- import { BleClient , BleDevice } from "@capacitor-community/bluetooth-le" ;
7+ import {
8+ BleClient ,
9+ BleDevice ,
10+ TimeoutOptions ,
11+ } from "@capacitor-community/bluetooth-le" ;
812import { AccelerometerService } from "./accelerometer-service.js" ;
913import { ButtonService } from "./button-service.js" ;
1014import { BoardVersion } from "./device.js" ;
@@ -19,6 +23,23 @@ import {
1923import { UARTService } from "./uart-service.js" ;
2024import { DeviceInformationService } from "./device-information-service.js" ;
2125
26+ const disconnectSymbol : unique symbol = Symbol ( "disconnected" ) ;
27+ const timeoutSymbol : unique symbol = Symbol ( "timeout" ) ;
28+
29+ export class BluetoothError extends Error { }
30+
31+ export class TimeoutError extends BluetoothError {
32+ constructor ( ) {
33+ super ( "Timeout" ) ;
34+ }
35+ }
36+
37+ export class DisconnectError extends BluetoothError {
38+ constructor ( ) {
39+ super ( "Disconnect" ) ;
40+ }
41+ }
42+
2243const deviceIdToWrapper : Map < string , BluetoothDeviceWrapper > = new Map ( ) ;
2344
2445const connectTimeoutDuration : number = 10000 ;
@@ -113,6 +134,25 @@ export class BluetoothDeviceWrapper {
113134 ] ;
114135 }
115136
137+ // TODO: this is the Device connect method
138+ async connect ( tag : string ) {
139+ this . tag = tag ;
140+ let onDisconnect : ( ( ) => void ) | undefined ;
141+ const promise = new Promise < typeof disconnectSymbol > ( ( resolve ) => {
142+ onDisconnect = ( ) => {
143+ this . log ( "Disconnected" ) ;
144+ this . internalNotificationListeners = new Map ( ) ;
145+ resolve ( disconnectSymbol ) ;
146+ } ;
147+ } ) ;
148+ this . disconnectTracker = { promise, onDisconnect : onDisconnect ! } ;
149+ this . log ( "Connecting" ) ;
150+ await BleClient . connect ( this . device . deviceId , onDisconnect , {
151+ timeout : connectTimeoutInMs ,
152+ } ) ;
153+ this . log ( "Connected" ) ;
154+ }
155+
116156 async connect ( ) : Promise < void > {
117157 this . logging . event ( {
118158 type : this . isReconnect ? "Reconnect" : "Connect" ,
@@ -322,6 +362,184 @@ export class BluetoothDeviceWrapper {
322362 . find ( ( s ) => s . getRelevantEvents ( ) . includes ( type ) )
323363 ?. stopNotifications ( type ) ;
324364 }
365+
366+ // Added for flashing
367+
368+ private tag : string | undefined ;
369+ disconnectTracker :
370+ | { promise : Promise < typeof disconnectSymbol > ; onDisconnect : ( ) => void }
371+ | undefined ;
372+ private internalNotificationListeners = new Map <
373+ string ,
374+ Set < ( data : Uint8Array ) => void >
375+ > ( ) ;
376+
377+ async startInternalNotifications (
378+ serviceId : string ,
379+ characteristicId : string ,
380+ options ?: TimeoutOptions ,
381+ ) : Promise < void > {
382+ const key = this . getNotificationKey ( serviceId , characteristicId ) ;
383+ await this . raceDisconnectAndTimeout (
384+ BleClient . startNotifications (
385+ this . device . deviceId ,
386+ serviceId ,
387+ characteristicId ,
388+ ( value : DataView ) => {
389+ const bytes = new Uint8Array ( value . buffer ) ;
390+ // Notify all registered callbacks.
391+ this . internalNotificationListeners
392+ . get ( key )
393+ ?. forEach ( ( cb ) => cb ( bytes ) ) ;
394+ } ,
395+ options ,
396+ ) ,
397+ { actionName : "start notifications" } ,
398+ ) ;
399+ }
400+
401+ subscribe (
402+ serviceId : string ,
403+ characteristicId : string ,
404+ callback : ( data : Uint8Array ) => void ,
405+ ) : void {
406+ const key = this . getNotificationKey ( serviceId , characteristicId ) ;
407+ if ( ! this . internalNotificationListeners . has ( key ) ) {
408+ this . internalNotificationListeners . set ( key , new Set ( ) ) ;
409+ }
410+ this . internalNotificationListeners . get ( key ) ! . add ( callback ) ;
411+ }
412+
413+ unsubscribe (
414+ serviceId : string ,
415+ characteristicId : string ,
416+ callback : ( data : Uint8Array ) => void ,
417+ ) : void {
418+ const key = this . getNotificationKey ( serviceId , characteristicId ) ;
419+ this . internalNotificationListeners . get ( key ) ?. delete ( callback ) ;
420+ }
421+
422+ async stopInternalNotifications (
423+ serviceId : string ,
424+ characteristicId : string ,
425+ ) : Promise < void > {
426+ await BleClient . stopNotifications (
427+ this . device . deviceId ,
428+ serviceId ,
429+ characteristicId ,
430+ ) ;
431+ const key = this . getNotificationKey ( serviceId , characteristicId ) ;
432+ this . internalNotificationListeners . delete ( key ) ;
433+ }
434+
435+ /**
436+ * Write to characteristic and wait for a notification response.
437+ *
438+ * It is the responsibility of the caller to have started notifications
439+ * for the characteristic.
440+ */
441+ async writeForNotification (
442+ serviceId : string ,
443+ characteristicId : string ,
444+ value : DataView ,
445+ notificationId : number ,
446+ isFinalNotification : ( p : Uint8Array ) => boolean = ( ) => true ,
447+ ) : Promise < Uint8Array > {
448+ let notificationListener : ( ( bytes : Uint8Array ) => void ) | undefined ;
449+ const notificationPromise = new Promise < Uint8Array > ( ( resolve ) => {
450+ notificationListener = ( bytes : Uint8Array ) => {
451+ if ( bytes [ 0 ] === notificationId && isFinalNotification ( bytes ) ) {
452+ resolve ( bytes ) ;
453+ }
454+ } ;
455+ this . subscribe ( serviceId , characteristicId , notificationListener ) ;
456+ } ) ;
457+
458+ try {
459+ await BleClient . writeWithoutResponse (
460+ this . device . deviceId ,
461+ serviceId ,
462+ characteristicId ,
463+ value ,
464+ ) ;
465+ return await this . raceDisconnectAndTimeout ( notificationPromise , {
466+ timeout : 3_000 ,
467+ actionName : "flash notification wait" ,
468+ } ) ;
469+ } finally {
470+ if ( notificationListener ) {
471+ this . unsubscribe ( serviceId , characteristicId , notificationListener ) ;
472+ }
473+ }
474+ }
475+
476+ async waitForDisconnect ( timeout : number ) : Promise < void > {
477+ if ( ! this . disconnectTracker ) {
478+ this . log ( "Waiting for disconnect but not connected" ) ;
479+ return ;
480+ }
481+ this . log ( `Waiting for disconnect (timeout ${ timeout } )` ) ;
482+ const result = await Promise . race ( [
483+ this . disconnectTracker . promise ,
484+ this . timeoutPromise ( timeout ) ,
485+ ] ) ;
486+ if ( result === timeoutSymbol ) {
487+ this . log ( "Timeout waiting for disconnect" ) ;
488+ throw new TimeoutError ( ) ;
489+ }
490+ }
491+
492+ /**
493+ * Suitable for running a series of BLE interactions with an overall timeout
494+ * and general disconnection
495+ */
496+ async raceDisconnectAndTimeout < T > (
497+ promise : Promise < T > ,
498+ options : {
499+ actionName ?: string ;
500+ timeout ?: number ;
501+ } = { } ,
502+ ) : Promise < T > {
503+ if ( ! this . disconnectTracker ) {
504+ throw new DisconnectError ( ) ;
505+ }
506+ const actionName = options . actionName ?? "action" ;
507+ const result = await Promise . race ( [
508+ promise ,
509+ this . disconnectTracker . promise ,
510+ ...( options . timeout ? [ this . timeoutPromise ( options . timeout ) ] : [ ] ) ,
511+ ] ) ;
512+ if ( result === timeoutSymbol ) {
513+ this . log ( `Timeout during ${ actionName } ` ) ;
514+ throw new TimeoutError ( ) ;
515+ }
516+ if ( result === disconnectSymbol ) {
517+ this . log ( `Disconnected during ${ actionName } ` ) ;
518+ throw new DisconnectError ( ) ;
519+ }
520+ return result ;
521+ }
522+
523+ private timeoutPromise ( timeout : number ) : Promise < typeof timeoutSymbol > {
524+ return new Promise ( ( resolve ) =>
525+ setTimeout ( ( ) => resolve ( timeoutSymbol ) , timeout ) ,
526+ ) ;
527+ }
528+
529+ log ( message : string ) {
530+ console . log ( `[${ this . tag } ] ${ message } ` ) ;
531+ }
532+
533+ error ( e : unknown ) {
534+ console . error ( e ) ;
535+ }
536+
537+ private getNotificationKey (
538+ serviceId : string ,
539+ characteristicId : string ,
540+ ) : string {
541+ return `${ serviceId } :${ characteristicId } ` ;
542+ }
325543}
326544
327545export const createBluetoothDeviceWrapper = async (
0 commit comments