1717import java .util .Objects ;
1818import java .util .UUID ;
1919import java .util .concurrent .CompletableFuture ;
20+ import java .util .concurrent .ExecutionException ;
2021import java .util .concurrent .ScheduledExecutorService ;
2122import java .util .concurrent .TimeUnit ;
23+ import java .util .concurrent .atomic .AtomicBoolean ;
24+ import java .util .concurrent .atomic .AtomicInteger ;
2225
2326import org .eclipse .jdt .annotation .NonNullByDefault ;
2427import org .eclipse .jdt .annotation .Nullable ;
25- import org .freedesktop .dbus .errors .NoReply ;
2628import org .freedesktop .dbus .errors .UnknownObject ;
2729import org .freedesktop .dbus .exceptions .DBusException ;
2830import org .freedesktop .dbus .exceptions .DBusExecutionException ;
@@ -72,6 +74,8 @@ public class BlueZBluetoothDevice extends BaseBluetoothDevice implements BlueZEv
7274
7375 private final ScheduledExecutorService scheduler = ThreadPoolManager .getScheduledPool ("bluetooth" );
7476
77+ private final AtomicBoolean connectionStartRunning = new AtomicBoolean ();
78+
7579 // Device from native lib
7680 private @ Nullable BluetoothDevice device = null ;
7781
@@ -170,6 +174,7 @@ public void dispose() {
170174 }
171175
172176 private void setConnectionState (ConnectionState state ) {
177+ logger .debug ("Update connection state for {} to {}" , address , state );
173178 if (this .connectionState != state ) {
174179 this .connectionState = state ;
175180 notifyListeners (BluetoothEventType .CONNECTION_STATE , new BluetoothConnectionStatusNotification (state ));
@@ -207,42 +212,47 @@ synchronized void resetForRemoval() {
207212
208213 @ Override
209214 public boolean connect () {
210- logger .debug ("Connect({})" , device );
211-
212215 BluetoothDevice dev = device ;
213- if (dev != null ) {
214- if (Boolean .FALSE .equals (dev .isConnected ())) {
215- // BlueZ Device.Connect() is unreliable while the adapter is actively discovering
216- // (the call blocks / does not complete). Pause discovery first; the bridge's periodic
217- // refresh job resumes it shortly after.
218- bridgeHandler .stopDiscovery ();
219- try {
216+
217+ if (dev == null ) {
218+ return false ;
219+ }
220+
221+ logger .debug ("Connect({})" , dev );
222+
223+ if (Boolean .FALSE .equals (dev .isConnected ())) {
224+ if (connectionStartRunning .compareAndSet (false , true )) {
225+ CompletableFuture .runAsync (() -> {
226+ setConnectionState (ConnectionState .CONNECTING );
227+ // BlueZ Device.Connect() is unreliable while the adapter is actively discovering
228+ // (the call blocks / does not complete). Pause discovery first; the bridge's periodic
229+ // refresh job resumes it shortly after.
230+ bridgeHandler .stopDiscovery ();
231+ // This method does not block at most until the dbus-java
232+ // method timeout is reached.
220233 boolean ret = dev .connect ();
221234 logger .debug ("Connect result: {}" , ret );
222- return ret ;
223- } catch (NoReply e ) {
224- // Have to double check because sometimes, exception but still worked
225- logger .debug ("Got a timeout - but sometimes happen. Is Connected ? {}" , dev .isConnected ());
226- if (Boolean .FALSE .equals (dev .isConnected ())) {
227- notifyListeners (BluetoothEventType .CONNECTION_STATE ,
228- new BluetoothConnectionStatusNotification (ConnectionState .DISCONNECTED ));
229- return false ;
230- } else {
231- return true ;
235+ ConnectionState cs1 = ret ? ConnectionState .CONNECTED : ConnectionState .DISCONNECTED ;
236+ logger .debug ("Updating connection state after connect: {}" , cs1 );
237+ setConnectionState (cs1 );
238+ }, scheduler ).handle ((voidResult , th ) -> {
239+ if (th != null ) {
240+ logger .debug ("Failed to connect" , th );
241+ setConnectionState (ConnectionState .DISCONNECTED );
232242 }
233- } catch (DBusExecutionException e ) {
234- // Catch "software caused connection abort"
235- return false ;
236- } catch (Exception e ) {
237- logger .warn ("error occurred while trying to connect" , e );
238- }
243+ connectionStartRunning .set (false );
244+ return null ;
245+ });
239246 } else {
240- logger .debug ("Device was already connected" );
241- // we might be stuck in another state atm so we need to trigger a connected in this case
242- setConnectionState (ConnectionState .CONNECTED );
243- return true ;
247+ logger .debug ("Connection already in progress for {}" , address );
244248 }
249+ return true ;
250+ } else {
251+ logger .debug ("Device was already connected" );
252+ // we might be stuck in another state atm so we need to trigger a connected in this case
253+ setConnectionState (ConnectionState .CONNECTED );
245254 }
255+
246256 return false ;
247257 }
248258
@@ -283,19 +293,29 @@ private List<BluetoothGattService> getGattServicesRefreshed(BluetoothDevice dev)
283293 return services ;
284294 }
285295
286- private @ Nullable BluetoothGattCharacteristic getDBusBlueZCharacteristicByUUID (String uuid ) {
296+ private @ Nullable CompletableFuture < BluetoothGattCharacteristic > getDBusBlueZCharacteristicByUUID (String uuid ) {
287297 BluetoothDevice dev = device ;
288298 if (dev == null ) {
289299 return null ;
290300 }
291- for (BluetoothGattService service : getGattServicesRefreshed (dev )) {
292- for (BluetoothGattCharacteristic characteristic : service .getGattCharacteristics ()) {
293- if (characteristic != null && uuid .equalsIgnoreCase (characteristic .getUuid ())) {
294- return characteristic ;
301+ AtomicInteger atomicInteger = new AtomicInteger ();
302+ return RetryFuture .callWithRetry (() -> {
303+ if (Boolean .TRUE .equals (dev .isServicesResolved ())) {
304+ for (BluetoothGattService service : getGattServicesRefreshed (dev )) {
305+ for (BluetoothGattCharacteristic characteristic : service .getGattCharacteristics ()) {
306+ if (characteristic != null && uuid .equalsIgnoreCase (characteristic .getUuid ())) {
307+ return characteristic ;
308+ }
309+ }
295310 }
296311 }
297- }
298- return null ;
312+
313+ if (atomicInteger .incrementAndGet () < 100 ) { // 100 iterations 50ms each => 5s
314+ throw new RetryException (50 , TimeUnit .MILLISECONDS );
315+ }
316+
317+ throw new IllegalStateException ("Characteristic " + uuid + " is missing on device" );
318+ }, scheduler );
299319 }
300320
301321 private @ Nullable BluetoothGattCharacteristic getDBusBlueZCharacteristicByDBusPath (String dBusPath ) {
@@ -323,30 +343,25 @@ private List<BluetoothGattService> getGattServicesRefreshed(BluetoothDevice dev)
323343 .failedFuture (new IllegalStateException ("DBusBlueZ device is not set or not connected" ));
324344 }
325345
326- BluetoothGattCharacteristic c = getDBusBlueZCharacteristicByUUID (characteristic .getUuid ().toString ());
327- if (c == null ) {
328- logger .warn ("Characteristic '{}' is missing on device '{}'." , characteristic .getUuid (), address );
329- return CompletableFuture .failedFuture (
330- new IllegalStateException ("Characteristic " + characteristic .getUuid () + " is missing on device" ));
331- }
332-
333- return RetryFuture .callWithRetry (() -> {
334- try {
335- c .startNotify ();
336- } catch (DBusException e ) {
337- String exceptionMessage = e .getMessage ();
338- if (exceptionMessage != null && exceptionMessage .contains ("Already notifying" )) {
346+ return getDBusBlueZCharacteristicByUUID (characteristic .getUuid ().toString ())
347+ .thenCompose (c -> RetryFuture .callWithRetry (() -> {
348+ try {
349+ c .startNotify ();
350+ } catch (DBusExecutionException e ) {
351+ String exceptionMessage = e .getMessage ();
352+ if (exceptionMessage != null && exceptionMessage .contains ("Already notifying" )) {
353+ return null ;
354+ } else if (exceptionMessage != null && exceptionMessage .contains ("In Progress" )) {
355+ // let's retry in half a second
356+ throw new RetryException (500 , TimeUnit .MILLISECONDS );
357+ } else {
358+ logger .warn ("Exception occurred while activating notifications on '{}'" , address , e );
359+ throw e ;
360+ }
361+ }
362+ ;
339363 return null ;
340- } else if (exceptionMessage != null && exceptionMessage .contains ("In Progress" )) {
341- // let's retry in half a second
342- throw new RetryException (500 , TimeUnit .MILLISECONDS );
343- } else {
344- logger .warn ("Exception occurred while activating notifications on '{}'" , address , e );
345- throw e ;
346- }
347- }
348- return null ;
349- }, scheduler );
364+ }, scheduler ));
350365 }
351366
352367 @ Override
@@ -359,23 +374,17 @@ private List<BluetoothGattService> getGattServicesRefreshed(BluetoothDevice dev)
359374 .failedFuture (new IllegalStateException ("DBusBlueZ device is not set or not connected" ));
360375 }
361376
362- BluetoothGattCharacteristic c = getDBusBlueZCharacteristicByUUID (characteristic .getUuid ().toString ());
363- if (c == null ) {
364- logger .warn ("Characteristic '{}' is missing on device '{}'." , characteristic .getUuid (), address );
365- return CompletableFuture .failedFuture (
366- new IllegalStateException ("Characteristic " + characteristic .getUuid () + " is missing on device" ));
367- }
368-
369- return RetryFuture .callWithRetry (() -> {
370- try {
371- c .writeValue (value , null );
372- return null ;
373- } catch (DBusException e ) {
374- logger .debug ("Exception occurred when trying to write characteristic '{}': {}" ,
375- characteristic .getUuid (), e .getMessage ());
376- throw e ;
377- }
378- }, scheduler );
377+ return getDBusBlueZCharacteristicByUUID (characteristic .getUuid ().toString ())
378+ .thenCompose (c -> RetryFuture .callWithRetry (() -> {
379+ try {
380+ c .writeValue (value , null );
381+ return null ;
382+ } catch (DBusException e ) {
383+ logger .debug ("Exception occurred when trying to write characteristic '{}': {}" ,
384+ characteristic .getUuid (), e .getMessage ());
385+ throw e ;
386+ }
387+ }, scheduler ));
379388 }
380389
381390 @ Override
@@ -385,6 +394,7 @@ public void onDBusBlueZEvent(BlueZEvent event) {
385394
386395 @ Override
387396 public void onServicesResolved (ServicesResolvedEvent event ) {
397+ logger .debug ("onServicesResolved: {}" , event .isResolved ());
388398 if (event .isResolved ()) {
389399 // Populate our service/characteristic list from the now-resolved GATT and notify
390400 // listeners. BlueZ can deliver ServicesResolved=true while our own supportedServices list
@@ -461,9 +471,7 @@ public void onRssiUpdate(RssiEvent event) {
461471
462472 @ Override
463473 public void onConnectedStatusUpdate (ConnectedEvent event ) {
464- this .connectionState = event .isConnected () ? ConnectionState .CONNECTED : ConnectionState .DISCONNECTED ;
465- notifyListeners (BluetoothEventType .CONNECTION_STATE ,
466- new BluetoothConnectionStatusNotification (connectionState ));
474+ setConnectionState (event .isConnected () ? ConnectionState .CONNECTED : ConnectionState .DISCONNECTED );
467475 }
468476
469477 @ Override
@@ -562,33 +570,32 @@ public CompletableFuture<byte[]> readCharacteristic(BluetoothCharacteristic char
562570 .failedFuture (new IllegalStateException ("DBusBlueZ device is not set or not connected" ));
563571 }
564572
565- BluetoothGattCharacteristic c = getDBusBlueZCharacteristicByUUID (characteristic .getUuid ().toString ());
566- if (c == null ) {
567- logger .warn ("Characteristic '{}' is missing on device '{}'." , characteristic .getUuid (), address );
568- return CompletableFuture .failedFuture (
569- new IllegalStateException ("Characteristic " + characteristic .getUuid () + " is missing on device" ));
570- }
571-
572- return RetryFuture .callWithRetry (() -> {
573- try {
574- return c .readValue (null );
575- } catch (DBusException | DBusExecutionException e ) {
576- // DBusExecutionException is thrown if the value cannot be read
577- logger .debug ("Exception occurred when trying to read characteristic '{}': {}" , characteristic .getUuid (),
578- e .getMessage ());
579- throw e ;
580- }
581- }, scheduler );
573+ return getDBusBlueZCharacteristicByUUID (characteristic .getUuid ().toString ())
574+ .thenCompose (c -> RetryFuture .callWithRetry (() -> {
575+ try {
576+ return c .readValue (null );
577+ } catch (DBusException | DBusExecutionException e ) {
578+ // DBusExecutionException is thrown if the value cannot be read
579+ logger .debug ("Exception occurred when trying to read characteristic '{}': {}" ,
580+ characteristic .getUuid (), e .getMessage ());
581+ throw e ;
582+ }
583+ }, scheduler ));
582584 }
583585
584586 @ Override
585587 public boolean isNotifying (BluetoothCharacteristic characteristic ) {
586- BluetoothGattCharacteristic c = getDBusBlueZCharacteristicByUUID (characteristic .getUuid ().toString ());
587- if (c != null ) {
588- Boolean isNotifying = c .isNotifying ();
589- return Objects .requireNonNullElse (isNotifying , false );
590- } else {
591- logger .warn ("Characteristic '{}' is missing on device '{}'." , characteristic .getUuid (), address );
588+ try {
589+ BluetoothGattCharacteristic c = getDBusBlueZCharacteristicByUUID (characteristic .getUuid ().toString ()).get ();
590+ if (c != null ) {
591+ Boolean isNotifying = c .isNotifying ();
592+ return Objects .requireNonNullElse (isNotifying , false );
593+ } else {
594+ logger .warn ("Characteristic '{}' is missing on device '{}'." , characteristic .getUuid (), address );
595+ return false ;
596+ }
597+ } catch (InterruptedException | ExecutionException ex ) {
598+ logger .warn ("Fetchig characteristic '{}' for device '{}'." , characteristic .getUuid (), address , ex );
592599 return false ;
593600 }
594601 }
@@ -597,33 +604,32 @@ public boolean isNotifying(BluetoothCharacteristic characteristic) {
597604 public CompletableFuture <@ Nullable Void > disableNotifications (BluetoothCharacteristic characteristic ) {
598605 BluetoothDevice dev = device ;
599606 if (dev == null || Boolean .FALSE .equals (dev .isConnected ())) {
600- return CompletableFuture
601- .failedFuture (new IllegalStateException ("DBusBlueZ device is not set or not connected" ));
602- }
603- BluetoothGattCharacteristic c = getDBusBlueZCharacteristicByUUID (characteristic .getUuid ().toString ());
604- if (c == null ) {
605- logger .warn ("Characteristic '{}' is missing on device '{}'." , characteristic .getUuid (), address );
606- return CompletableFuture .failedFuture (
607- new IllegalStateException ("Characteristic " + characteristic .getUuid () + " is missing on device" ));
607+ String message ;
608+ if (dev == null ) {
609+ message = "DBusBlueZ device is not set" ;
610+ } else {
611+ message = "DBusBlueZ device is not not connected" ;
612+ }
613+ return CompletableFuture .failedFuture (new IllegalStateException (message ));
608614 }
609-
610- return RetryFuture .callWithRetry (() -> {
611- try {
612- c .stopNotify ();
613- } catch (DBusException e ) {
614- String exceptionMessage = e .getMessage ();
615- if (exceptionMessage != null && exceptionMessage .contains ("Already notifying" )) {
615+ return getDBusBlueZCharacteristicByUUID (characteristic .getUuid ().toString ())
616+ .thenCompose (c -> RetryFuture .callWithRetry (() -> {
617+ try {
618+ c .stopNotify ();
619+ } catch (DBusExecutionException e ) {
620+ String exceptionMessage = e .getMessage ();
621+ if (exceptionMessage != null && exceptionMessage .contains ("Already notifying" )) {
622+ return null ;
623+ } else if (exceptionMessage != null && exceptionMessage .contains ("In Progress" )) {
624+ // let's retry in half a second
625+ throw new RetryException (500 , TimeUnit .MILLISECONDS );
626+ } else {
627+ logger .warn ("Exception occurred while deactivating notifications on '{}'" , address , e );
628+ throw e ;
629+ }
630+ }
616631 return null ;
617- } else if (exceptionMessage != null && exceptionMessage .contains ("In Progress" )) {
618- // let's retry in half a second
619- throw new RetryException (500 , TimeUnit .MILLISECONDS );
620- } else {
621- logger .warn ("Exception occurred while deactivating notifications on '{}'" , address , e );
622- throw e ;
623- }
624- }
625- return null ;
626- }, scheduler );
632+ }, scheduler ));
627633 }
628634
629635 @ Override
0 commit comments