@@ -9,6 +9,14 @@ const logHead = "[CAPACITOR-BLUETOOTH]";
99
1010const toLowerUuid = ( uuid ) => uuid ?. toLowerCase ?. ( ) ?? uuid ;
1111
12+ const GATT_PROPERTIES = {
13+ READ : 0x02 ,
14+ WRITE_NO_RESPONSE : 0x04 ,
15+ WRITE : 0x08 ,
16+ NOTIFY : 0x10 ,
17+ INDICATE : 0x20 ,
18+ } ;
19+
1220const toUint8Array = ( data ) => {
1321 if ( data instanceof Uint8Array ) {
1422 return data ;
@@ -82,6 +90,9 @@ class CapacitorBluetooth extends EventTarget {
8290 this . disconnectHandled = true ;
8391 this . nativeListeners = [ ] ;
8492 this . nativeListenersReady = false ;
93+ this . discoveredServices = new Map ( ) ;
94+ this . pendingServiceResolvers = new Map ( ) ;
95+ this . serviceResolutionTimeoutMs = 8000 ;
8596
8697 this . handleNotification = this . handleNotification . bind ( this ) ;
8798 this . handleRemoteDisconnect = this . handleRemoteDisconnect . bind ( this ) ;
@@ -213,13 +224,11 @@ class CapacitorBluetooth extends EventTarget {
213224 this . device = requestedDevice ;
214225 this . logHead = logHead ;
215226
216- const deviceDescription = this . resolveDeviceDescription ( requestedDevice . device ) ; // || { serviceUuid: info.service } ;
227+ const deviceDescription = this . resolveDeviceDescription ( requestedDevice . device ) ?? null ;
217228 if ( ! deviceDescription ) {
218- console . error ( `${ logHead } Unsupported device: missing known service UUID` ) ;
219- this . openRequested = false ;
220- gui_log ( i18n . getMessage ( "bluetoothConnectionError" , [ "Unsupported device" ] ) ) ;
221- this . dispatchEvent ( new CustomEvent ( "connect" , { detail : false } ) ) ;
222- return false ;
229+ console . warn (
230+ `${ logHead } No static profile for device ${ requestedDevice . device ?. name ?? requestedDevice . path } ` ,
231+ ) ;
223232 }
224233
225234 this . deviceDescription = deviceDescription ;
@@ -235,6 +244,17 @@ class CapacitorBluetooth extends EventTarget {
235244 void this . handleRemoteDisconnect ( deviceId ) ;
236245 } ) ;
237246
247+ const effectiveDescription = await this . waitForDeviceCharacteristics (
248+ requestedDevice . device . deviceId ,
249+ deviceDescription ,
250+ ) ;
251+
252+ if ( ! this . hasCompleteCharacteristics ( effectiveDescription ) ) {
253+ throw new Error ( "Unable to determine BLE service and characteristics for device" ) ;
254+ }
255+
256+ this . deviceDescription = effectiveDescription ;
257+
238258 gui_log ( i18n . getMessage ( "bluetoothConnected" , [ requestedDevice . device . name ] ) ) ;
239259 await this . startNotifications ( ) ;
240260
@@ -258,7 +278,7 @@ class CapacitorBluetooth extends EventTarget {
258278 gui_log ( i18n . getMessage ( "bluetoothConnectionError" , [ error ] ) ) ;
259279 this . openRequested = false ;
260280 this . dispatchEvent ( new CustomEvent ( "connect" , { detail : false } ) ) ;
261- this . cleanupConnectionState ( ) ;
281+ this . cleanupConnectionState ( requestedDevice . device ?. deviceId ?? null ) ;
262282 return false ;
263283 }
264284 }
@@ -333,7 +353,7 @@ class CapacitorBluetooth extends EventTarget {
333353
334354 this . closeRequested = true ;
335355
336- const targetDeviceId = this . device ?. device ?. deviceId ?? this . connectionId ?? "unknown" ;
356+ const targetDeviceId = this . device ?. device ?. deviceId ?? this . connectionId ?? null ;
337357
338358 try {
339359 await this . stopNotifications ( ) ;
@@ -407,17 +427,18 @@ class CapacitorBluetooth extends EventTarget {
407427 }
408428
409429 this . disconnectHandled = true ;
430+ const activeDeviceId = deviceId ?? this . device ?. device ?. deviceId ?? null ;
410431
411- console . warn ( `${ logHead } Device ${ deviceId } disconnected` ) ;
432+ console . warn ( `${ logHead } Device ${ activeDeviceId ?? "unknown" } disconnected` ) ;
412433 await this . stopNotifications ( ) ;
413434 console . log (
414435 `${ logHead } Connection with ID: ${ this . connectionId } closed, Sent: ${ this . bytesSent } bytes, Received: ${ this . bytesReceived } bytes` ,
415436 ) ;
416- this . cleanupConnectionState ( ) ;
437+ this . cleanupConnectionState ( activeDeviceId ) ;
417438 this . dispatchEvent ( new CustomEvent ( "disconnect" , { detail : true } ) ) ;
418439 }
419440
420- cleanupConnectionState ( ) {
441+ cleanupConnectionState ( deviceId = null ) {
421442 this . connected = false ;
422443 this . connectionId = null ;
423444 this . bitrate = 0 ;
@@ -435,6 +456,148 @@ class CapacitorBluetooth extends EventTarget {
435456 this . openRequested = false ;
436457 this . closeRequested = false ;
437458 this . disconnectHandled = true ;
459+ if ( deviceId ) {
460+ this . clearPendingServiceResolver ( deviceId ) ;
461+ }
462+ }
463+
464+ hasCompleteCharacteristics ( description ) {
465+ return Boolean ( description ?. serviceUuid && description ?. writeCharacteristic && description ?. readCharacteristic ) ;
466+ }
467+
468+ async waitForDeviceCharacteristics ( deviceId , fallbackDescription ) {
469+ if ( ! deviceId ) {
470+ return fallbackDescription ?? null ;
471+ }
472+
473+ const cached = this . getCachedDeviceDescription ( deviceId , fallbackDescription ) ;
474+ if ( this . hasCompleteCharacteristics ( cached ) ) {
475+ return cached ;
476+ }
477+
478+ return this . createPendingServicePromise ( deviceId , cached ?? fallbackDescription ?? null ) ;
479+ }
480+
481+ getCachedDeviceDescription ( deviceId , fallbackDescription ) {
482+ const services = this . discoveredServices . get ( deviceId ) ;
483+ if ( ! services ) {
484+ return fallbackDescription ?? null ;
485+ }
486+ const derived = this . buildDescriptionFromServices ( services ) ;
487+ if ( ! derived ) {
488+ return fallbackDescription ?? null ;
489+ }
490+ return this . mergeDeviceDescription ( fallbackDescription , derived ) ;
491+ }
492+
493+ mergeDeviceDescription ( baseDescription , overrideDescription ) {
494+ if ( ! baseDescription ) {
495+ return overrideDescription ? { ...overrideDescription } : null ;
496+ }
497+ if ( ! overrideDescription ) {
498+ return { ...baseDescription } ;
499+ }
500+ return { ...baseDescription , ...overrideDescription } ;
501+ }
502+
503+ createPendingServicePromise ( deviceId , fallbackDescription ) {
504+ const existing = this . pendingServiceResolvers . get ( deviceId ) ;
505+ if ( existing ) {
506+ return existing . promise ;
507+ }
508+
509+ const pending = {
510+ fallback : fallbackDescription ? { ...fallbackDescription } : null ,
511+ } ;
512+ pending . promise = new Promise ( ( resolve ) => {
513+ pending . resolve = ( description ) => {
514+ clearTimeout ( pending . timeout ) ;
515+ this . pendingServiceResolvers . delete ( deviceId ) ;
516+ if ( description ) {
517+ resolve ( description ) ;
518+ } else {
519+ resolve ( pending . fallback ) ;
520+ }
521+ } ;
522+ } ) ;
523+ pending . timeout = setTimeout ( ( ) => pending . resolve ( null ) , this . serviceResolutionTimeoutMs ) ;
524+ this . pendingServiceResolvers . set ( deviceId , pending ) ;
525+ return pending . promise ;
526+ }
527+
528+ buildDescriptionFromServices ( services = [ ] ) {
529+ for ( const service of services ) {
530+ if ( ! service ) {
531+ continue ;
532+ }
533+ const serviceUuid = toLowerUuid ( service . uuid ?? service . serviceUuid ) ;
534+ if ( ! serviceUuid ) {
535+ continue ;
536+ }
537+ const characteristics = Array . isArray ( service . characteristics ) ? service . characteristics : [ ] ;
538+ if ( characteristics . length === 0 ) {
539+ continue ;
540+ }
541+ const writeCandidate = characteristics . find ( ( characteristic ) =>
542+ this . characteristicSupportsWrite ( characteristic ?. properties ) ,
543+ ) ;
544+ if ( ! writeCandidate ?. uuid ) {
545+ continue ;
546+ }
547+ const notifyCandidate =
548+ characteristics . find ( ( characteristic ) =>
549+ this . characteristicSupportsNotify ( characteristic ?. properties ) ,
550+ ) || writeCandidate ;
551+ if ( ! notifyCandidate ?. uuid ) {
552+ continue ;
553+ }
554+ return {
555+ serviceUuid,
556+ writeCharacteristic : toLowerUuid ( writeCandidate . uuid ) ,
557+ readCharacteristic : toLowerUuid ( notifyCandidate . uuid ) ,
558+ } ;
559+ }
560+ return null ;
561+ }
562+
563+ characteristicSupportsWrite ( properties = 0 ) {
564+ return ( properties & ( GATT_PROPERTIES . WRITE | GATT_PROPERTIES . WRITE_NO_RESPONSE ) ) !== 0 ;
565+ }
566+
567+ characteristicSupportsNotify ( properties = 0 ) {
568+ return ( properties & ( GATT_PROPERTIES . NOTIFY | GATT_PROPERTIES . INDICATE ) ) !== 0 ;
569+ }
570+
571+ handleServicesEvent ( event ) {
572+ const deviceId = event ?. deviceId ;
573+ const services = event ?. services ;
574+ if ( ! deviceId || ! Array . isArray ( services ) ) {
575+ return ;
576+ }
577+
578+ this . discoveredServices . set ( deviceId , services ) ;
579+ const derived = this . buildDescriptionFromServices ( services ) ;
580+ const pending = this . pendingServiceResolvers . get ( deviceId ) ;
581+ if ( pending ) {
582+ pending . resolve ( this . mergeDeviceDescription ( pending . fallback , derived ) ) ;
583+ }
584+
585+ if ( derived && this . device ?. device ?. deviceId === deviceId ) {
586+ this . deviceDescription = this . mergeDeviceDescription ( this . deviceDescription , derived ) ;
587+ }
588+ }
589+
590+ clearPendingServiceResolver ( deviceId ) {
591+ if ( ! deviceId ) {
592+ return ;
593+ }
594+ const pending = this . pendingServiceResolvers . get ( deviceId ) ;
595+ if ( pending ) {
596+ clearTimeout ( pending . timeout ) ;
597+ this . pendingServiceResolvers . delete ( deviceId ) ;
598+ pending . resolve ( null ) ;
599+ }
600+ this . discoveredServices . delete ( deviceId ) ;
438601 }
439602
440603 attachNativeListeners ( ) {
@@ -480,9 +643,13 @@ class CapacitorBluetooth extends EventTarget {
480643
481644 registerListener ( "connectionState" , ( event ) => {
482645 if ( event ?. connected === false ) {
483- void this . handleRemoteDisconnect ( event . deviceId ?? "unknown" ) ;
646+ void this . handleRemoteDisconnect ( event . deviceId ?? null ) ;
484647 }
485648 } ) ;
649+
650+ registerListener ( "services" , ( event ) => {
651+ this . handleServicesEvent ( event ) ;
652+ } ) ;
486653 }
487654}
488655
0 commit comments