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