]> git.basschouten.com Git - openhab-addons.git/blob
0aa8377b0f72f6e22df592d7f6b793706f141716
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
7  * This program and the accompanying materials are made available under the
8  * terms of the Eclipse Public License 2.0 which is available at
9  * http://www.eclipse.org/legal/epl-2.0
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.bluetooth.bluegiga;
14
15 import java.nio.ByteBuffer;
16 import java.nio.ByteOrder;
17 import java.util.HashMap;
18 import java.util.Map;
19 import java.util.NavigableMap;
20 import java.util.TreeMap;
21 import java.util.UUID;
22 import java.util.concurrent.CompletableFuture;
23 import java.util.concurrent.ScheduledExecutorService;
24 import java.util.concurrent.ScheduledFuture;
25 import java.util.concurrent.TimeUnit;
26 import java.util.concurrent.TimeoutException;
27
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.openhab.binding.bluetooth.BaseBluetoothDevice;
31 import org.openhab.binding.bluetooth.BluetoothAddress;
32 import org.openhab.binding.bluetooth.BluetoothBindingConstants;
33 import org.openhab.binding.bluetooth.BluetoothCharacteristic;
34 import org.openhab.binding.bluetooth.BluetoothDescriptor;
35 import org.openhab.binding.bluetooth.BluetoothException;
36 import org.openhab.binding.bluetooth.BluetoothService;
37 import org.openhab.binding.bluetooth.BluetoothUtils;
38 import org.openhab.binding.bluetooth.bluegiga.handler.BlueGigaBridgeHandler;
39 import org.openhab.binding.bluetooth.bluegiga.internal.BlueGigaEventListener;
40 import org.openhab.binding.bluetooth.bluegiga.internal.BlueGigaResponse;
41 import org.openhab.binding.bluetooth.bluegiga.internal.command.attributeclient.BlueGigaAttributeValueEvent;
42 import org.openhab.binding.bluetooth.bluegiga.internal.command.attributeclient.BlueGigaFindInformationFoundEvent;
43 import org.openhab.binding.bluetooth.bluegiga.internal.command.attributeclient.BlueGigaGroupFoundEvent;
44 import org.openhab.binding.bluetooth.bluegiga.internal.command.attributeclient.BlueGigaProcedureCompletedEvent;
45 import org.openhab.binding.bluetooth.bluegiga.internal.command.connection.BlueGigaConnectionStatusEvent;
46 import org.openhab.binding.bluetooth.bluegiga.internal.command.connection.BlueGigaDisconnectedEvent;
47 import org.openhab.binding.bluetooth.bluegiga.internal.command.gap.BlueGigaScanResponseEvent;
48 import org.openhab.binding.bluetooth.bluegiga.internal.eir.EirDataType;
49 import org.openhab.binding.bluetooth.bluegiga.internal.eir.EirPacket;
50 import org.openhab.binding.bluetooth.bluegiga.internal.enumeration.BgApiResponse;
51 import org.openhab.binding.bluetooth.bluegiga.internal.enumeration.BluetoothAddressType;
52 import org.openhab.binding.bluetooth.bluegiga.internal.enumeration.ConnectionStatusFlag;
53 import org.openhab.binding.bluetooth.notification.BluetoothConnectionStatusNotification;
54 import org.openhab.binding.bluetooth.notification.BluetoothScanNotification;
55 import org.openhab.binding.bluetooth.notification.BluetoothScanNotification.BluetoothBeaconType;
56 import org.openhab.core.common.ThreadPoolManager;
57 import org.slf4j.Logger;
58 import org.slf4j.LoggerFactory;
59
60 /**
61  * An extended {@link BluetoothDevice} class to handle BlueGiga specific information
62  *
63  * @author Chris Jackson - Initial contribution
64  */
65 @NonNullByDefault
66 public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements BlueGigaEventListener {
67     private static final long TIMEOUT_SEC = 60;
68
69     private final Logger logger = LoggerFactory.getLogger(BlueGigaBluetoothDevice.class);
70
71     private static final BlueGigaProcedure PROCEDURE_NONE = new BlueGigaProcedure(BlueGigaProcedure.Type.NONE);
72     private static final BlueGigaProcedure PROCEDURE_GET_SERVICES = new BlueGigaProcedure(
73             BlueGigaProcedure.Type.GET_SERVICES);
74     private static final BlueGigaProcedure PROCEDURE_GET_CHARACTERISTICS = new BlueGigaProcedure(
75             BlueGigaProcedure.Type.GET_CHARACTERISTICS);
76     private static final BlueGigaProcedure PROCEDURE_READ_CHARACTERISTIC_DECL = new BlueGigaProcedure(
77             BlueGigaProcedure.Type.READ_CHARACTERISTIC_DECL);
78
79     private Map<Integer, UUID> handleToUUID = new HashMap<>();
80     private NavigableMap<Integer, BlueGigaBluetoothCharacteristic> handleToCharacteristic = new TreeMap<>();
81
82     // BlueGiga needs to know the address type when connecting
83     private BluetoothAddressType addressType = BluetoothAddressType.UNKNOWN;
84
85     // The dongle handler
86     private final BlueGigaBridgeHandler bgHandler;
87
88     private BlueGigaProcedure currentProcedure = PROCEDURE_NONE;
89
90     // The connection handle if the device is connected
91     private int connection = -1;
92
93     private final ScheduledExecutorService scheduler = ThreadPoolManager.getScheduledPool("bluetooth");
94
95     private @Nullable ScheduledFuture<?> connectTimer;
96     private @Nullable ScheduledFuture<?> procedureTimer;
97
98     private Runnable connectTimeoutTask = new Runnable() {
99         @Override
100         public void run() {
101             if (connectionState == ConnectionState.CONNECTING) {
102                 logger.debug("Connection timeout for device {}", address);
103                 connectionState = ConnectionState.DISCONNECTED;
104             }
105         }
106     };
107
108     private Runnable procedureTimeoutTask = new Runnable() {
109         @Override
110         public void run() {
111             BlueGigaProcedure procedure = currentProcedure;
112             logger.debug("Procedure {} timeout for device {}", procedure.type, address);
113             switch (procedure.type) {
114                 case CHARACTERISTIC_READ:
115                     ReadCharacteristicProcedure readProcedure = (ReadCharacteristicProcedure) procedure;
116                     readProcedure.readFuture.completeExceptionally(new TimeoutException("Read characteristic "
117                             + readProcedure.characteristic.getUuid() + " timeout for device " + address));
118                     break;
119                 case CHARACTERISTIC_WRITE:
120                     WriteCharacteristicProcedure writeProcedure = (WriteCharacteristicProcedure) procedure;
121                     writeProcedure.writeFuture.completeExceptionally(new TimeoutException("Write characteristic "
122                             + writeProcedure.characteristic.getUuid() + " timeout for device " + address));
123                     break;
124                 default:
125                     break;
126             }
127
128             currentProcedure = PROCEDURE_NONE;
129         }
130     };
131
132     /**
133      * Creates a new {@link BlueGigaBluetoothDevice} which extends {@link BluetoothDevice} for the BlueGiga
134      * implementation
135      *
136      * @param bgHandler the {@link BlueGigaBridgeHandler} that provides the link to the dongle
137      * @param address the {@link BluetoothAddress} for this device
138      * @param addressType the {@link BluetoothAddressType} of this device
139      */
140     public BlueGigaBluetoothDevice(BlueGigaBridgeHandler bgHandler, BluetoothAddress address,
141             BluetoothAddressType addressType) {
142         super(bgHandler, address);
143
144         logger.debug("Creating new BlueGiga device {}", address);
145
146         this.bgHandler = bgHandler;
147         this.addressType = addressType;
148
149         bgHandler.addEventListener(this);
150         updateLastSeenTime();
151     }
152
153     public void setAddressType(BluetoothAddressType addressType) {
154         this.addressType = addressType;
155     }
156
157     @Override
158     public boolean connect() {
159         if (connection != -1) {
160             // We're already connected
161             return true;
162         }
163
164         cancelTimer(connectTimer);
165         if (bgHandler.bgConnect(address, addressType)) {
166             connectionState = ConnectionState.CONNECTING;
167             connectTimer = startTimer(connectTimeoutTask, 10);
168             return true;
169         } else {
170             connectionState = ConnectionState.DISCONNECTED;
171             return false;
172         }
173     }
174
175     @Override
176     public boolean disconnect() {
177         if (connection == -1) {
178             // We're already disconnected
179             return true;
180         }
181
182         return bgHandler.bgDisconnect(connection);
183     }
184
185     @Override
186     public boolean discoverServices() {
187         if (!PROCEDURE_NONE.equals(currentProcedure)) {
188             return false;
189         }
190
191         cancelTimer(procedureTimer);
192         if (!bgHandler.bgFindPrimaryServices(connection)) {
193             return false;
194         }
195
196         procedureTimer = startTimer(procedureTimeoutTask, TIMEOUT_SEC);
197         currentProcedure = PROCEDURE_GET_SERVICES;
198         return true;
199     }
200
201     @Override
202     public CompletableFuture<@Nullable Void> enableNotifications(BluetoothCharacteristic characteristic) {
203         if (connection == -1) {
204             return CompletableFuture.failedFuture(new BluetoothException("Not connected"));
205         }
206
207         BlueGigaBluetoothCharacteristic ch = (BlueGigaBluetoothCharacteristic) characteristic;
208         if (ch.isNotifying()) {
209             return CompletableFuture.completedFuture(null);
210         }
211
212         BluetoothDescriptor descriptor = ch
213                 .getDescriptor(BluetoothDescriptor.GattDescriptor.CLIENT_CHARACTERISTIC_CONFIGURATION.getUUID());
214
215         if (descriptor == null || descriptor.getHandle() == 0) {
216             return CompletableFuture.failedFuture(
217                     new BluetoothException("Unable to find CCC for characteristic [" + characteristic.getUuid() + "]"));
218         }
219
220         if (!PROCEDURE_NONE.equals(currentProcedure)) {
221             return CompletableFuture.failedFuture(new BluetoothException("Another procedure is already in progress"));
222         }
223
224         int[] value = { 1, 0 };
225
226         cancelTimer(procedureTimer);
227         if (!bgHandler.bgWriteCharacteristic(connection, descriptor.getHandle(), value)) {
228             return CompletableFuture.failedFuture(new BluetoothException(
229                     "Failed to write to CCC for characteristic [" + characteristic.getUuid() + "]"));
230         }
231
232         procedureTimer = startTimer(procedureTimeoutTask, TIMEOUT_SEC);
233         WriteCharacteristicProcedure notifyProcedure = new WriteCharacteristicProcedure(ch,
234                 BlueGigaProcedure.Type.NOTIFICATION_ENABLE);
235         currentProcedure = notifyProcedure;
236         try {
237             // we intentionally sleep here in order to give this procedure a chance to complete.
238             // ideally we would use locks/conditions to make this wait until completiong but
239             // I have a better solution planned for later. - Connor Petty
240             Thread.sleep(500);
241         } catch (InterruptedException e) {
242             Thread.currentThread().interrupt();
243         }
244         return notifyProcedure.writeFuture;
245     }
246
247     @Override
248     public CompletableFuture<@Nullable Void> disableNotifications(BluetoothCharacteristic characteristic) {
249         if (connection == -1) {
250             return CompletableFuture.failedFuture(new BluetoothException("Not connected"));
251         }
252
253         BlueGigaBluetoothCharacteristic ch = (BlueGigaBluetoothCharacteristic) characteristic;
254         if (!ch.isNotifying()) {
255             return CompletableFuture.completedFuture(null);
256         }
257
258         BluetoothDescriptor descriptor = ch
259                 .getDescriptor(BluetoothDescriptor.GattDescriptor.CLIENT_CHARACTERISTIC_CONFIGURATION.getUUID());
260
261         if (descriptor == null || descriptor.getHandle() == 0) {
262             return CompletableFuture.failedFuture(
263                     new BluetoothException("Unable to find CCC for characteristic [" + characteristic.getUuid() + "]"));
264         }
265
266         if (!PROCEDURE_NONE.equals(currentProcedure)) {
267             return CompletableFuture.failedFuture(new BluetoothException("Another procedure is already in progress"));
268         }
269
270         int[] value = { 0, 0 };
271
272         cancelTimer(procedureTimer);
273         if (!bgHandler.bgWriteCharacteristic(connection, descriptor.getHandle(), value)) {
274             return CompletableFuture.failedFuture(new BluetoothException(
275                     "Failed to write to CCC for characteristic [" + characteristic.getUuid() + "]"));
276         }
277
278         procedureTimer = startTimer(procedureTimeoutTask, TIMEOUT_SEC);
279         WriteCharacteristicProcedure notifyProcedure = new WriteCharacteristicProcedure(ch,
280                 BlueGigaProcedure.Type.NOTIFICATION_DISABLE);
281         currentProcedure = notifyProcedure;
282
283         return notifyProcedure.writeFuture;
284     }
285
286     @Override
287     public boolean isNotifying(BluetoothCharacteristic characteristic) {
288         BlueGigaBluetoothCharacteristic ch = (BlueGigaBluetoothCharacteristic) characteristic;
289         return ch.isNotifying();
290     }
291
292     @Override
293     public boolean enableNotifications(BluetoothDescriptor descriptor) {
294         // TODO will be implemented in a followup PR
295         return false;
296     }
297
298     @Override
299     public boolean disableNotifications(BluetoothDescriptor descriptor) {
300         // TODO will be implemented in a followup PR
301         return false;
302     }
303
304     @Override
305     public CompletableFuture<byte[]> readCharacteristic(BluetoothCharacteristic characteristic) {
306         if (characteristic.getHandle() == 0) {
307             return CompletableFuture.failedFuture(new BluetoothException("Cannot read characteristic with no handle"));
308         }
309         if (connection == -1) {
310             return CompletableFuture.failedFuture(new BluetoothException("Not connected"));
311         }
312
313         if (!PROCEDURE_NONE.equals(currentProcedure)) {
314             return CompletableFuture.failedFuture(new BluetoothException("Another procedure is already in progress"));
315         }
316
317         cancelTimer(procedureTimer);
318         if (!bgHandler.bgReadCharacteristic(connection, characteristic.getHandle())) {
319             return CompletableFuture.failedFuture(
320                     new BluetoothException("Failed to read characteristic [" + characteristic.getUuid() + "]"));
321         }
322         procedureTimer = startTimer(procedureTimeoutTask, TIMEOUT_SEC);
323         ReadCharacteristicProcedure readProcedure = new ReadCharacteristicProcedure(characteristic);
324         currentProcedure = readProcedure;
325
326         return readProcedure.readFuture;
327     }
328
329     @Override
330     public CompletableFuture<@Nullable Void> writeCharacteristic(BluetoothCharacteristic characteristic, byte[] value) {
331         if (characteristic.getHandle() == 0) {
332             return CompletableFuture.failedFuture(new BluetoothException("Cannot write characteristic with no handle"));
333         }
334         if (connection == -1) {
335             return CompletableFuture.failedFuture(new BluetoothException("Not connected"));
336         }
337
338         if (!PROCEDURE_NONE.equals(currentProcedure)) {
339             return CompletableFuture.failedFuture(new BluetoothException("Another procedure is already in progress"));
340         }
341
342         cancelTimer(procedureTimer);
343         if (!bgHandler.bgWriteCharacteristic(connection, characteristic.getHandle(),
344                 BluetoothUtils.toIntArray(value))) {
345             return CompletableFuture.failedFuture(
346                     new BluetoothException("Failed to write characteristic [" + characteristic.getUuid() + "]"));
347         }
348
349         procedureTimer = startTimer(procedureTimeoutTask, TIMEOUT_SEC);
350         WriteCharacteristicProcedure writeProcedure = new WriteCharacteristicProcedure(
351                 (BlueGigaBluetoothCharacteristic) characteristic, BlueGigaProcedure.Type.CHARACTERISTIC_WRITE);
352         currentProcedure = writeProcedure;
353
354         return writeProcedure.writeFuture;
355     }
356
357     @Override
358     public void bluegigaEventReceived(BlueGigaResponse event) {
359         if (event instanceof BlueGigaScanResponseEvent responseEvent) {
360             handleScanEvent(responseEvent);
361         }
362
363         else if (event instanceof BlueGigaGroupFoundEvent foundEvent) {
364             handleGroupFoundEvent(foundEvent);
365         }
366
367         else if (event instanceof BlueGigaFindInformationFoundEvent foundEvent) {
368             // A Characteristic has been discovered
369             handleFindInformationFoundEvent(foundEvent);
370         }
371
372         else if (event instanceof BlueGigaProcedureCompletedEvent completedEvent) {
373             handleProcedureCompletedEvent(completedEvent);
374         }
375
376         else if (event instanceof BlueGigaConnectionStatusEvent statusEvent) {
377             handleConnectionStatusEvent(statusEvent);
378         }
379
380         else if (event instanceof BlueGigaDisconnectedEvent disconnectedEvent) {
381             handleDisconnectedEvent(disconnectedEvent);
382         }
383
384         else if (event instanceof BlueGigaAttributeValueEvent valueEvent) {
385             handleAttributeValueEvent(valueEvent);
386         }
387     }
388
389     private void handleScanEvent(BlueGigaScanResponseEvent event) {
390         // Check if this is addressed to this device
391         if (!address.equals(new BluetoothAddress(event.getSender()))) {
392             return;
393         }
394
395         logger.trace("scanEvent: {}", event);
396         updateLastSeenTime();
397
398         // Set device properties
399         rssi = event.getRssi();
400         addressType = event.getAddressType();
401
402         byte[] manufacturerData = null;
403
404         // If the packet contains data, then process it and add anything relevant to the device...
405         if (event.getData().length > 0) {
406             EirPacket eir = new EirPacket(event.getData());
407             for (EirDataType record : eir.getRecords().keySet()) {
408                 if (logger.isTraceEnabled()) {
409                     logger.trace("  EirDataType: {}={}", record, eir.getRecord(record));
410                 }
411                 Object obj;
412                 switch (record) {
413                     case EIR_FLAGS:
414                         break;
415                     case EIR_MANUFACTURER_SPECIFIC:
416                         obj = eir.getRecord(EirDataType.EIR_MANUFACTURER_SPECIFIC);
417                         if (obj != null) {
418                             try {
419                                 @SuppressWarnings("unchecked")
420                                 Map<Short, int[]> eirRecord = (Map<Short, int[]>) obj;
421                                 Map.Entry<Short, int[]> eirEntry = eirRecord.entrySet().iterator().next();
422
423                                 manufacturer = eirEntry.getKey().intValue();
424
425                                 int[] manufacturerInt = eirEntry.getValue();
426                                 manufacturerData = new byte[manufacturerInt.length + 2];
427                                 // Convert short Company ID to bytes and add it to manufacturerData
428                                 manufacturerData[0] = (byte) (manufacturer & 0xff);
429                                 manufacturerData[1] = (byte) ((manufacturer >> 8) & 0xff);
430                                 // Add Convert int custom data nd add it to manufacturerData
431                                 for (int i = 0; i < manufacturerInt.length; i++) {
432                                     manufacturerData[i + 2] = (byte) manufacturerInt[i];
433                                 }
434                             } catch (ClassCastException e) {
435                                 logger.debug("Unsupported manufacturer specific record received from device {}",
436                                         address);
437                             }
438                         }
439                         break;
440                     case EIR_NAME_LONG:
441                     case EIR_NAME_SHORT:
442                         name = (String) eir.getRecord(record);
443                         break;
444                     case EIR_SLAVEINTERVALRANGE:
445                         break;
446                     case EIR_SVC_DATA_UUID128:
447                         break;
448                     case EIR_SVC_DATA_UUID16:
449                         break;
450                     case EIR_SVC_DATA_UUID32:
451                         break;
452                     case EIR_SVC_UUID128_INCOMPLETE:
453                     case EIR_SVC_UUID16_COMPLETE:
454                     case EIR_SVC_UUID16_INCOMPLETE:
455                     case EIR_SVC_UUID32_COMPLETE:
456                     case EIR_SVC_UUID32_INCOMPLETE:
457                     case EIR_SVC_UUID128_COMPLETE:
458                         // addServices((List<UUID>) eir.getRecord(record));
459                         break;
460                     case EIR_TXPOWER:
461                         obj = eir.getRecord(EirDataType.EIR_TXPOWER);
462                         if (obj != null) {
463                             txPower = (int) obj;
464                         }
465                         break;
466                     default:
467                         break;
468                 }
469             }
470         }
471
472         if (connectionState == ConnectionState.DISCOVERING) {
473             // TODO: It could make sense to wait with discovery for non-connectable devices until scan response is
474             // received to eventually retrieve more about the device before it gets discovered. Anyhow, devices
475             // that don't send a scan response at all also have to be supported. See also PR #6995.
476
477             // Set our state to disconnected
478             connectionState = ConnectionState.DISCONNECTED;
479             connection = -1;
480
481             // But notify listeners that the state is now DISCOVERED
482             notifyListeners(BluetoothEventType.CONNECTION_STATE,
483                     new BluetoothConnectionStatusNotification(ConnectionState.DISCOVERED));
484
485             // Notify the bridge - for inbox notifications
486             bgHandler.deviceDiscovered(this);
487         }
488
489         // Notify listeners of all scan records - for RSSI, beacon processing (etc)
490         BluetoothScanNotification scanNotification = new BluetoothScanNotification();
491         scanNotification.setRssi(event.getRssi());
492
493         switch (event.getPacketType()) {
494             case CONNECTABLE_ADVERTISEMENT:
495             case DISCOVERABLE_ADVERTISEMENT:
496             case NON_CONNECTABLE_ADVERTISEMENT:
497                 scanNotification.setBeaconType(BluetoothBeaconType.BEACON_ADVERTISEMENT);
498                 break;
499             case SCAN_RESPONSE:
500                 scanNotification.setBeaconType(BluetoothBeaconType.BEACON_SCANRESPONSE);
501                 break;
502             default:
503                 break;
504         }
505
506         if (manufacturerData != null) {
507             scanNotification.setManufacturerData(manufacturerData);
508         }
509
510         notifyListeners(BluetoothEventType.SCAN_RECORD, scanNotification);
511     }
512
513     private void handleGroupFoundEvent(BlueGigaGroupFoundEvent event) {
514         // If this is not our connection handle then ignore.
515         if (connection != event.getConnection()) {
516             return;
517         }
518
519         logger.trace("BlueGiga Group: {} event={}", this, event);
520         updateLastSeenTime();
521
522         BluetoothService service = new BluetoothService(event.getUuid(), true, event.getStart(), event.getEnd());
523         addService(service);
524     }
525
526     private void handleFindInformationFoundEvent(BlueGigaFindInformationFoundEvent event) {
527         // If this is not our connection handle then ignore.
528         if (connection != event.getConnection()) {
529             return;
530         }
531
532         logger.trace("BlueGiga FindInfo: {} event={}", this, event);
533         updateLastSeenTime();
534
535         int handle = event.getChrHandle();
536         UUID attUUID = event.getUuid();
537
538         BluetoothService service = getServiceByHandle(handle);
539         if (service == null) {
540             logger.debug("BlueGiga: Unable to find service for handle {}", handle);
541             return;
542         }
543         handleToUUID.put(handle, attUUID);
544
545         if (BluetoothBindingConstants.ATTR_CHARACTERISTIC_DECLARATION.equals(attUUID)) {
546             BlueGigaBluetoothCharacteristic characteristic = new BlueGigaBluetoothCharacteristic(handle);
547             characteristic.setService(service);
548             handleToCharacteristic.put(handle, characteristic);
549         } else {
550             @Nullable
551             Integer chrHandle = handleToCharacteristic.floorKey(handle);
552             if (chrHandle == null) {
553                 logger.debug("BlueGiga: Unable to find characteristic for handle {}", handle);
554                 return;
555             }
556             @Nullable
557             BlueGigaBluetoothCharacteristic characteristic = handleToCharacteristic.get(chrHandle);
558             if (characteristic != null) {
559                 characteristic.addDescriptor(new BluetoothDescriptor(characteristic, attUUID, handle));
560             }
561         }
562     }
563
564     private void handleProcedureCompletedEvent(BlueGigaProcedureCompletedEvent event) {
565         // If this is not our connection handle then ignore.
566         if (connection != event.getConnection()) {
567             return;
568         }
569
570         if (PROCEDURE_NONE.equals(currentProcedure)) {
571             logger.debug("BlueGiga procedure completed but procedure is null with connection {}, address {}",
572                     connection, address);
573             return;
574         }
575
576         cancelTimer(procedureTimer);
577         updateLastSeenTime();
578
579         // The current procedure is now complete - move on...
580         switch (currentProcedure.type) {
581             case GET_SERVICES:
582                 // We've downloaded all services, now get the characteristics
583                 if (bgHandler.bgFindCharacteristics(connection)) {
584                     procedureTimer = startTimer(procedureTimeoutTask, TIMEOUT_SEC);
585                     currentProcedure = PROCEDURE_GET_CHARACTERISTICS;
586                 } else {
587                     currentProcedure = PROCEDURE_NONE;
588                 }
589                 break;
590             case GET_CHARACTERISTICS:
591                 // We've downloaded all attributes, now read the characteristic declarations
592                 if (bgHandler.bgReadCharacteristicDeclarations(connection)) {
593                     procedureTimer = startTimer(procedureTimeoutTask, TIMEOUT_SEC);
594                     currentProcedure = PROCEDURE_READ_CHARACTERISTIC_DECL;
595                 } else {
596                     currentProcedure = PROCEDURE_NONE;
597                 }
598                 break;
599             case READ_CHARACTERISTIC_DECL:
600                 // We've downloaded read all the declarations, we are done now
601                 currentProcedure = PROCEDURE_NONE;
602                 notifyListeners(BluetoothEventType.SERVICES_DISCOVERED);
603                 break;
604             case CHARACTERISTIC_READ:
605                 // The read failed
606                 ReadCharacteristicProcedure readProcedure = (ReadCharacteristicProcedure) currentProcedure;
607                 readProcedure.readFuture.completeExceptionally(new BluetoothException(
608                         "Read characteristic failed: " + readProcedure.characteristic.getUuid()));
609                 currentProcedure = PROCEDURE_NONE;
610                 break;
611             case CHARACTERISTIC_WRITE:
612                 // The write completed - failure or success
613                 WriteCharacteristicProcedure writeProcedure = (WriteCharacteristicProcedure) currentProcedure;
614                 if (event.getResult() == BgApiResponse.SUCCESS) {
615                     writeProcedure.writeFuture.complete(null);
616                 } else {
617                     writeProcedure.writeFuture.completeExceptionally(new BluetoothException(
618                             "Write characteristic failed: " + writeProcedure.characteristic.getUuid()));
619                 }
620                 currentProcedure = PROCEDURE_NONE;
621                 break;
622             case NOTIFICATION_ENABLE:
623                 WriteCharacteristicProcedure notifyEnableProcedure = (WriteCharacteristicProcedure) currentProcedure;
624                 boolean success = event.getResult() == BgApiResponse.SUCCESS;
625                 if (success) {
626                     notifyEnableProcedure.writeFuture.complete(null);
627                 } else {
628                     notifyEnableProcedure.writeFuture
629                             .completeExceptionally(new BluetoothException("Enable characteristic notification failed: "
630                                     + notifyEnableProcedure.characteristic.getUuid()));
631                 }
632                 notifyEnableProcedure.characteristic.setNotifying(success);
633                 currentProcedure = PROCEDURE_NONE;
634                 break;
635             case NOTIFICATION_DISABLE:
636                 WriteCharacteristicProcedure notifyDisableProcedure = (WriteCharacteristicProcedure) currentProcedure;
637                 success = event.getResult() == BgApiResponse.SUCCESS;
638                 if (success) {
639                     notifyDisableProcedure.writeFuture.complete(null);
640                 } else {
641                     notifyDisableProcedure.writeFuture
642                             .completeExceptionally(new BluetoothException("Disable characteristic notification failed: "
643                                     + notifyDisableProcedure.characteristic.getUuid()));
644                 }
645                 notifyDisableProcedure.characteristic.setNotifying(!success);
646                 currentProcedure = PROCEDURE_NONE;
647                 break;
648             default:
649                 break;
650         }
651     }
652
653     private void handleConnectionStatusEvent(BlueGigaConnectionStatusEvent event) {
654         // Check if this is addressed to this device
655         if (!address.equals(new BluetoothAddress(event.getAddress()))) {
656             return;
657         }
658
659         cancelTimer(connectTimer);
660         updateLastSeenTime();
661
662         // If we're connected, then remember the connection handle
663         if (event.getFlags().contains(ConnectionStatusFlag.CONNECTION_CONNECTED)) {
664             connectionState = ConnectionState.CONNECTED;
665             connection = event.getConnection();
666             notifyListeners(BluetoothEventType.CONNECTION_STATE,
667                     new BluetoothConnectionStatusNotification(connectionState));
668         }
669     }
670
671     private void handleDisconnectedEvent(BlueGigaDisconnectedEvent event) {
672         // If this is not our connection handle then ignore.
673         if (connection != event.getConnection()) {
674             return;
675         }
676
677         for (BlueGigaBluetoothCharacteristic ch : handleToCharacteristic.values()) {
678             ch.setNotifying(false);
679         }
680
681         cancelTimer(procedureTimer);
682         connectionState = ConnectionState.DISCONNECTED;
683         connection = -1;
684
685         BlueGigaProcedure procedure = currentProcedure;
686         switch (procedure.type) {
687             case CHARACTERISTIC_READ:
688                 ReadCharacteristicProcedure readProcedure = (ReadCharacteristicProcedure) procedure;
689                 readProcedure.readFuture.completeExceptionally(new BluetoothException("Read characteristic "
690                         + readProcedure.characteristic.getUuid() + " failed due to disconnect of device " + address));
691                 break;
692             case CHARACTERISTIC_WRITE:
693                 WriteCharacteristicProcedure writeProcedure = (WriteCharacteristicProcedure) procedure;
694                 writeProcedure.writeFuture.completeExceptionally(new BluetoothException("Write characteristic "
695                         + writeProcedure.characteristic.getUuid() + " failed due to disconnect of device " + address));
696                 break;
697             default:
698                 break;
699         }
700         currentProcedure = PROCEDURE_NONE;
701
702         notifyListeners(BluetoothEventType.CONNECTION_STATE,
703                 new BluetoothConnectionStatusNotification(connectionState));
704     }
705
706     private void handleAttributeValueEvent(BlueGigaAttributeValueEvent event) {
707         // If this is not our connection handle then ignore.
708         if (connection != event.getConnection()) {
709             return;
710         }
711
712         updateLastSeenTime();
713
714         logger.trace("BlueGiga AttributeValue: {} event={}", this, event);
715
716         int handle = event.getAttHandle();
717
718         Map.Entry<Integer, BlueGigaBluetoothCharacteristic> entry = handleToCharacteristic.floorEntry(handle);
719         if (entry == null) {
720             logger.debug("BlueGiga didn't find characteristic for event {}", event);
721             return;
722         }
723
724         BlueGigaBluetoothCharacteristic characteristic = entry.getValue();
725
726         if (handle == entry.getKey()) {
727             // this is the declaration
728             if (parseDeclaration(characteristic, event.getValue())) {
729                 BluetoothService service = getServiceByHandle(handle);
730                 if (service == null) {
731                     logger.debug("BlueGiga: Unable to find service for handle {}", handle);
732                     return;
733                 }
734                 service.addCharacteristic(characteristic);
735             }
736             return;
737         }
738         if (handle == characteristic.getHandle()) {
739             byte[] value = BluetoothUtils.toByteArray(event.getValue());
740             BlueGigaProcedure procedure = currentProcedure;
741             // If this is the characteristic we were reading, then send a read completion
742             if (procedure.type == BlueGigaProcedure.Type.CHARACTERISTIC_READ) {
743                 ReadCharacteristicProcedure readProcedure = (ReadCharacteristicProcedure) currentProcedure;
744                 if (readProcedure.characteristic.getHandle() == event.getAttHandle()) {
745                     readProcedure.readFuture.complete(value);
746                     currentProcedure = PROCEDURE_NONE;
747                     return;
748                 }
749             }
750             // Notify the user of the updated value
751             notifyListeners(BluetoothEventType.CHARACTERISTIC_UPDATED, characteristic, value);
752         } else {
753             // it must be one of the descriptors we need to update
754             UUID attUUID = handleToUUID.get(handle);
755             if (attUUID != null) {
756                 BluetoothDescriptor descriptor = characteristic.getDescriptor(attUUID);
757                 notifyListeners(BluetoothEventType.DESCRIPTOR_UPDATED, descriptor,
758                         BluetoothUtils.toByteArray(event.getValue()));
759             }
760         }
761     }
762
763     private boolean parseDeclaration(BlueGigaBluetoothCharacteristic ch, int[] value) {
764         ByteBuffer buffer = ByteBuffer.wrap(BluetoothUtils.toByteArray(value));
765         buffer.order(ByteOrder.LITTLE_ENDIAN);
766
767         ch.setProperties(Byte.toUnsignedInt(buffer.get()));
768         ch.setHandle(Short.toUnsignedInt(buffer.getShort()));
769
770         switch (buffer.remaining()) {
771             case 2:
772                 long key = Short.toUnsignedLong(buffer.getShort());
773                 ch.setUUID(BluetoothBindingConstants.createBluetoothUUID(key));
774                 return true;
775             case 4:
776                 key = Integer.toUnsignedLong(buffer.getInt());
777                 ch.setUUID(BluetoothBindingConstants.createBluetoothUUID(key));
778                 return true;
779             case 16:
780                 long lower = buffer.getLong();
781                 long upper = buffer.getLong();
782                 ch.setUUID(new UUID(upper, lower));
783                 return true;
784             default:
785                 logger.debug("Unexpected uuid length: {}", buffer.remaining());
786                 return false;
787         }
788     }
789
790     /**
791      * Clean up and release memory.
792      */
793     @Override
794     public void dispose() {
795         if (connectionState == ConnectionState.CONNECTED) {
796             disconnect();
797         }
798         cancelTimer(connectTimer);
799         cancelTimer(procedureTimer);
800         bgHandler.removeEventListener(this);
801         currentProcedure = PROCEDURE_NONE;
802         connectionState = ConnectionState.DISCOVERING;
803         connection = -1;
804     }
805
806     private void cancelTimer(@Nullable ScheduledFuture<?> task) {
807         if (task != null) {
808             task.cancel(true);
809         }
810     }
811
812     private ScheduledFuture<?> startTimer(Runnable command, long timeout) {
813         return scheduler.schedule(command, timeout, TimeUnit.SECONDS);
814     }
815
816     private static class BlueGigaProcedure {
817         private final Type type;
818
819         public BlueGigaProcedure(Type type) {
820             this.type = type;
821         }
822
823         // An enum to use in the state machine for interacting with the device
824         enum Type {
825             NONE,
826             GET_SERVICES,
827             GET_CHARACTERISTICS,
828             READ_CHARACTERISTIC_DECL,
829             CHARACTERISTIC_READ,
830             CHARACTERISTIC_WRITE,
831             NOTIFICATION_ENABLE,
832             NOTIFICATION_DISABLE
833         }
834     }
835
836     private static class ReadCharacteristicProcedure extends BlueGigaProcedure {
837
838         private final BluetoothCharacteristic characteristic;
839
840         private final CompletableFuture<byte[]> readFuture = new CompletableFuture<>();
841
842         public ReadCharacteristicProcedure(BluetoothCharacteristic characteristic) {
843             super(Type.CHARACTERISTIC_READ);
844             this.characteristic = characteristic;
845         }
846     }
847
848     private static class WriteCharacteristicProcedure extends BlueGigaProcedure {
849
850         private final BlueGigaBluetoothCharacteristic characteristic;
851
852         private final CompletableFuture<@Nullable Void> writeFuture = new CompletableFuture<>();
853
854         public WriteCharacteristicProcedure(BlueGigaBluetoothCharacteristic characteristic, Type type) {
855             super(type);
856             this.characteristic = characteristic;
857         }
858     }
859 }