]> git.basschouten.com Git - openhab-addons.git/blob
ccff259a0b8785213c8bafa566e5b49c3f4e8b8d
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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.ScheduledExecutorService;
23 import java.util.concurrent.ScheduledFuture;
24 import java.util.concurrent.TimeUnit;
25
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.openhab.binding.bluetooth.BaseBluetoothDevice;
29 import org.openhab.binding.bluetooth.BluetoothAddress;
30 import org.openhab.binding.bluetooth.BluetoothBindingConstants;
31 import org.openhab.binding.bluetooth.BluetoothCharacteristic;
32 import org.openhab.binding.bluetooth.BluetoothCompletionStatus;
33 import org.openhab.binding.bluetooth.BluetoothDescriptor;
34 import org.openhab.binding.bluetooth.BluetoothDevice;
35 import org.openhab.binding.bluetooth.BluetoothService;
36 import org.openhab.binding.bluetooth.bluegiga.handler.BlueGigaBridgeHandler;
37 import org.openhab.binding.bluetooth.bluegiga.internal.BlueGigaEventListener;
38 import org.openhab.binding.bluetooth.bluegiga.internal.BlueGigaResponse;
39 import org.openhab.binding.bluetooth.bluegiga.internal.command.attributeclient.BlueGigaAttributeValueEvent;
40 import org.openhab.binding.bluetooth.bluegiga.internal.command.attributeclient.BlueGigaFindInformationFoundEvent;
41 import org.openhab.binding.bluetooth.bluegiga.internal.command.attributeclient.BlueGigaGroupFoundEvent;
42 import org.openhab.binding.bluetooth.bluegiga.internal.command.attributeclient.BlueGigaProcedureCompletedEvent;
43 import org.openhab.binding.bluetooth.bluegiga.internal.command.connection.BlueGigaConnectionStatusEvent;
44 import org.openhab.binding.bluetooth.bluegiga.internal.command.connection.BlueGigaDisconnectedEvent;
45 import org.openhab.binding.bluetooth.bluegiga.internal.command.gap.BlueGigaScanResponseEvent;
46 import org.openhab.binding.bluetooth.bluegiga.internal.eir.EirDataType;
47 import org.openhab.binding.bluetooth.bluegiga.internal.eir.EirPacket;
48 import org.openhab.binding.bluetooth.bluegiga.internal.enumeration.BgApiResponse;
49 import org.openhab.binding.bluetooth.bluegiga.internal.enumeration.BluetoothAddressType;
50 import org.openhab.binding.bluetooth.bluegiga.internal.enumeration.ConnectionStatusFlag;
51 import org.openhab.binding.bluetooth.notification.BluetoothConnectionStatusNotification;
52 import org.openhab.binding.bluetooth.notification.BluetoothScanNotification;
53 import org.openhab.binding.bluetooth.notification.BluetoothScanNotification.BluetoothBeaconType;
54 import org.openhab.core.common.ThreadPoolManager;
55 import org.slf4j.Logger;
56 import org.slf4j.LoggerFactory;
57
58 /**
59  * An extended {@link BluetoothDevice} class to handle BlueGiga specific information
60  *
61  * @author Chris Jackson - Initial contribution
62  */
63 @NonNullByDefault
64 public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements BlueGigaEventListener {
65     private final long TIMEOUT_SEC = 60;
66
67     private final Logger logger = LoggerFactory.getLogger(BlueGigaBluetoothDevice.class);
68
69     private Map<Integer, UUID> handleToUUID = new HashMap<>();
70     private NavigableMap<Integer, BlueGigaBluetoothCharacteristic> handleToCharacteristic = new TreeMap<>();
71
72     // BlueGiga needs to know the address type when connecting
73     private BluetoothAddressType addressType = BluetoothAddressType.UNKNOWN;
74
75     // The dongle handler
76     private final BlueGigaBridgeHandler bgHandler;
77
78     // An enum to use in the state machine for interacting with the device
79     private enum BlueGigaProcedure {
80         NONE,
81         GET_SERVICES,
82         GET_CHARACTERISTICS,
83         READ_CHARACTERISTIC_DECL,
84         CHARACTERISTIC_READ,
85         CHARACTERISTIC_WRITE,
86         NOTIFICATION_ENABLE,
87         NOTIFICATION_DISABLE
88     }
89
90     private BlueGigaProcedure procedureProgress = BlueGigaProcedure.NONE;
91
92     // Somewhere to remember what characteristic we're working on
93     private @Nullable BluetoothCharacteristic procedureCharacteristic;
94
95     // The connection handle if the device is connected
96     private int connection = -1;
97
98     private final ScheduledExecutorService scheduler = ThreadPoolManager.getScheduledPool("bluetooth");
99
100     private @Nullable ScheduledFuture<?> connectTimer;
101     private @Nullable ScheduledFuture<?> procedureTimer;
102
103     private Runnable connectTimeoutTask = new Runnable() {
104         @Override
105         public void run() {
106             if (connectionState == ConnectionState.CONNECTING) {
107                 logger.debug("Connection timeout for device {}", address);
108                 connectionState = ConnectionState.DISCONNECTED;
109             }
110         }
111     };
112
113     private Runnable procedureTimeoutTask = new Runnable() {
114         @Override
115         public void run() {
116             logger.debug("Procedure {} timeout for device {}", procedureProgress, address);
117             procedureProgress = BlueGigaProcedure.NONE;
118             procedureCharacteristic = null;
119         }
120     };
121
122     /**
123      * Creates a new {@link BlueGigaBluetoothDevice} which extends {@link BluetoothDevice} for the BlueGiga
124      * implementation
125      *
126      * @param bgHandler the {@link BlueGigaBridgeHandler} that provides the link to the dongle
127      * @param address the {@link BluetoothAddress} for this device
128      * @param addressType the {@link BluetoothAddressType} of this device
129      */
130     public BlueGigaBluetoothDevice(BlueGigaBridgeHandler bgHandler, BluetoothAddress address,
131             BluetoothAddressType addressType) {
132         super(bgHandler, address);
133
134         logger.debug("Creating new BlueGiga device {}", address);
135
136         this.bgHandler = bgHandler;
137         this.addressType = addressType;
138
139         bgHandler.addEventListener(this);
140         updateLastSeenTime();
141     }
142
143     public void setAddressType(BluetoothAddressType addressType) {
144         this.addressType = addressType;
145     }
146
147     @Override
148     public boolean connect() {
149         if (connection != -1) {
150             // We're already connected
151             return true;
152         }
153
154         cancelTimer(connectTimer);
155         if (bgHandler.bgConnect(address, addressType)) {
156             connectionState = ConnectionState.CONNECTING;
157             connectTimer = startTimer(connectTimeoutTask, 10);
158             return true;
159         } else {
160             connectionState = ConnectionState.DISCONNECTED;
161             return false;
162         }
163     }
164
165     @Override
166     public boolean disconnect() {
167         if (connection == -1) {
168             // We're already disconnected
169             return true;
170         }
171
172         return bgHandler.bgDisconnect(connection);
173     }
174
175     @Override
176     public boolean discoverServices() {
177         if (procedureProgress != BlueGigaProcedure.NONE) {
178             return false;
179         }
180
181         cancelTimer(procedureTimer);
182         if (!bgHandler.bgFindPrimaryServices(connection)) {
183             return false;
184         }
185
186         procedureTimer = startTimer(procedureTimeoutTask, TIMEOUT_SEC);
187         procedureProgress = BlueGigaProcedure.GET_SERVICES;
188         return true;
189     }
190
191     @Override
192     public boolean enableNotifications(BluetoothCharacteristic characteristic) {
193         if (connection == -1) {
194             logger.debug("Cannot enable notifications, device not connected {}", this);
195             return false;
196         }
197
198         BlueGigaBluetoothCharacteristic ch = (BlueGigaBluetoothCharacteristic) characteristic;
199         if (ch.isNotifying()) {
200             return true;
201         }
202
203         BluetoothDescriptor descriptor = ch
204                 .getDescriptor(BluetoothDescriptor.GattDescriptor.CLIENT_CHARACTERISTIC_CONFIGURATION.getUUID());
205
206         if (descriptor == null || descriptor.getHandle() == 0) {
207             logger.debug("unable to find CCC for characteristic {}", characteristic.getUuid());
208             return false;
209         }
210
211         if (procedureProgress != BlueGigaProcedure.NONE) {
212             logger.debug("Procedure already in progress {}", procedureProgress);
213             return false;
214         }
215
216         int[] value = { 1, 0 };
217         byte[] bvalue = toBytes(value);
218         descriptor.setValue(bvalue);
219
220         cancelTimer(procedureTimer);
221         if (!bgHandler.bgWriteCharacteristic(connection, descriptor.getHandle(), value)) {
222             logger.debug("bgWriteCharacteristic returned false");
223             return false;
224         }
225
226         procedureTimer = startTimer(procedureTimeoutTask, TIMEOUT_SEC);
227         procedureProgress = BlueGigaProcedure.NOTIFICATION_ENABLE;
228         procedureCharacteristic = characteristic;
229
230         try {
231             // we intentionally sleep here in order to give this procedure a chance to complete.
232             // ideally we would use locks/conditions to make this wait until completiong but
233             // I have a better solution planned for later. - Connor Petty
234             Thread.sleep(500);
235         } catch (InterruptedException e) {
236             Thread.currentThread().interrupt();
237         }
238         return true;
239     }
240
241     @Override
242     public boolean disableNotifications(BluetoothCharacteristic characteristic) {
243         if (connection == -1) {
244             logger.debug("Cannot disable notifications, device not connected {}", this);
245             return false;
246         }
247
248         BlueGigaBluetoothCharacteristic ch = (BlueGigaBluetoothCharacteristic) characteristic;
249         if (!ch.isNotifying()) {
250             return true;
251         }
252
253         BluetoothDescriptor descriptor = ch
254                 .getDescriptor(BluetoothDescriptor.GattDescriptor.CLIENT_CHARACTERISTIC_CONFIGURATION.getUUID());
255
256         if (descriptor == null || descriptor.getHandle() == 0) {
257             logger.debug("unable to find CCC for characteristic {}", characteristic.getUuid());
258             return false;
259         }
260
261         if (procedureProgress != BlueGigaProcedure.NONE) {
262             logger.debug("Procedure already in progress {}", procedureProgress);
263             return false;
264         }
265
266         int[] value = { 0, 0 };
267         byte[] bvalue = toBytes(value);
268         descriptor.setValue(bvalue);
269
270         cancelTimer(procedureTimer);
271         if (!bgHandler.bgWriteCharacteristic(connection, descriptor.getHandle(), value)) {
272             logger.debug("bgWriteCharacteristic returned false");
273             return false;
274         }
275
276         procedureTimer = startTimer(procedureTimeoutTask, TIMEOUT_SEC);
277         procedureProgress = BlueGigaProcedure.NOTIFICATION_DISABLE;
278         procedureCharacteristic = characteristic;
279
280         try {
281             // we intentionally sleep here in order to give this procedure a chance to complete.
282             // ideally we would use locks/conditions to make this wait until completiong but
283             // I have a better solution planned for later. - Connor Petty
284             Thread.sleep(500);
285         } catch (InterruptedException e) {
286             Thread.currentThread().interrupt();
287         }
288         return true;
289     }
290
291     @Override
292     public boolean isNotifying(BluetoothCharacteristic characteristic) {
293         BlueGigaBluetoothCharacteristic ch = (BlueGigaBluetoothCharacteristic) characteristic;
294         return ch.isNotifying();
295     }
296
297     @Override
298     public boolean enableNotifications(BluetoothDescriptor descriptor) {
299         // TODO will be implemented in a followup PR
300         return false;
301     }
302
303     @Override
304     public boolean disableNotifications(BluetoothDescriptor descriptor) {
305         // TODO will be implemented in a followup PR
306         return false;
307     }
308
309     @Override
310     public boolean readCharacteristic(@Nullable BluetoothCharacteristic characteristic) {
311         if (characteristic == null || characteristic.getHandle() == 0) {
312             return false;
313         }
314         if (connection == -1) {
315             return false;
316         }
317
318         if (procedureProgress != BlueGigaProcedure.NONE) {
319             return false;
320         }
321
322         cancelTimer(procedureTimer);
323         if (!bgHandler.bgReadCharacteristic(connection, characteristic.getHandle())) {
324             return false;
325         }
326         procedureTimer = startTimer(procedureTimeoutTask, TIMEOUT_SEC);
327         procedureProgress = BlueGigaProcedure.CHARACTERISTIC_READ;
328         procedureCharacteristic = characteristic;
329
330         return true;
331     }
332
333     @Override
334     public boolean writeCharacteristic(@Nullable BluetoothCharacteristic characteristic) {
335         if (characteristic == null || characteristic.getHandle() == 0) {
336             return false;
337         }
338         if (connection == -1) {
339             return false;
340         }
341
342         if (procedureProgress != BlueGigaProcedure.NONE) {
343             return false;
344         }
345
346         cancelTimer(procedureTimer);
347         if (!bgHandler.bgWriteCharacteristic(connection, characteristic.getHandle(), characteristic.getValue())) {
348             return false;
349         }
350
351         procedureTimer = startTimer(procedureTimeoutTask, TIMEOUT_SEC);
352         procedureProgress = BlueGigaProcedure.CHARACTERISTIC_WRITE;
353         procedureCharacteristic = characteristic;
354
355         return true;
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 (procedureProgress == BlueGigaProcedure.NONE) {
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 (procedureProgress) {
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                     procedureProgress = BlueGigaProcedure.GET_CHARACTERISTICS;
583                 } else {
584                     procedureProgress = BlueGigaProcedure.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                     procedureProgress = BlueGigaProcedure.READ_CHARACTERISTIC_DECL;
592                 } else {
593                     procedureProgress = BlueGigaProcedure.NONE;
594                 }
595                 break;
596             case READ_CHARACTERISTIC_DECL:
597                 // We've downloaded read all the declarations, we are done now
598                 procedureProgress = BlueGigaProcedure.NONE;
599                 notifyListeners(BluetoothEventType.SERVICES_DISCOVERED);
600                 break;
601             case CHARACTERISTIC_READ:
602                 // The read failed
603                 notifyListeners(BluetoothEventType.CHARACTERISTIC_READ_COMPLETE, procedureCharacteristic,
604                         BluetoothCompletionStatus.ERROR);
605                 procedureProgress = BlueGigaProcedure.NONE;
606                 procedureCharacteristic = null;
607                 break;
608             case CHARACTERISTIC_WRITE:
609                 // The write completed - failure or success
610                 BluetoothCompletionStatus result = event.getResult() == BgApiResponse.SUCCESS
611                         ? BluetoothCompletionStatus.SUCCESS
612                         : BluetoothCompletionStatus.ERROR;
613                 notifyListeners(BluetoothEventType.CHARACTERISTIC_WRITE_COMPLETE, procedureCharacteristic, result);
614                 procedureProgress = BlueGigaProcedure.NONE;
615                 procedureCharacteristic = null;
616                 break;
617             case NOTIFICATION_ENABLE:
618                 boolean success = event.getResult() == BgApiResponse.SUCCESS;
619                 if (!success) {
620                     logger.debug("write to descriptor failed");
621                 }
622                 ((BlueGigaBluetoothCharacteristic) procedureCharacteristic).setNotifying(success);
623                 procedureProgress = BlueGigaProcedure.NONE;
624                 procedureCharacteristic = null;
625                 break;
626             case NOTIFICATION_DISABLE:
627                 success = event.getResult() == BgApiResponse.SUCCESS;
628                 if (!success) {
629                     logger.debug("write to descriptor failed");
630                 }
631                 ((BlueGigaBluetoothCharacteristic) procedureCharacteristic).setNotifying(!success);
632                 procedureProgress = BlueGigaProcedure.NONE;
633                 procedureCharacteristic = null;
634                 break;
635             default:
636                 break;
637         }
638     }
639
640     private void handleConnectionStatusEvent(BlueGigaConnectionStatusEvent event) {
641         // Check if this is addressed to this device
642         if (!address.equals(new BluetoothAddress(event.getAddress()))) {
643             return;
644         }
645
646         cancelTimer(connectTimer);
647         updateLastSeenTime();
648
649         // If we're connected, then remember the connection handle
650         if (event.getFlags().contains(ConnectionStatusFlag.CONNECTION_CONNECTED)) {
651             connectionState = ConnectionState.CONNECTED;
652             connection = event.getConnection();
653             notifyListeners(BluetoothEventType.CONNECTION_STATE,
654                     new BluetoothConnectionStatusNotification(connectionState));
655         }
656     }
657
658     private void handleDisconnectedEvent(BlueGigaDisconnectedEvent event) {
659         // If this is not our connection handle then ignore.
660         if (connection != event.getConnection()) {
661             return;
662         }
663
664         for (BlueGigaBluetoothCharacteristic ch : handleToCharacteristic.values()) {
665             ch.setNotifying(false);
666         }
667
668         cancelTimer(procedureTimer);
669         connectionState = ConnectionState.DISCONNECTED;
670         connection = -1;
671         procedureProgress = BlueGigaProcedure.NONE;
672
673         notifyListeners(BluetoothEventType.CONNECTION_STATE,
674                 new BluetoothConnectionStatusNotification(connectionState));
675     }
676
677     private void handleAttributeValueEvent(BlueGigaAttributeValueEvent event) {
678         // If this is not our connection handle then ignore.
679         if (connection != event.getConnection()) {
680             return;
681         }
682
683         updateLastSeenTime();
684
685         logger.trace("BlueGiga AttributeValue: {} event={}", this, event);
686
687         int handle = event.getAttHandle();
688
689         Map.Entry<Integer, BlueGigaBluetoothCharacteristic> entry = handleToCharacteristic.floorEntry(handle);
690         if (entry == null) {
691             logger.debug("BlueGiga didn't find characteristic for event {}", event);
692             return;
693         }
694
695         BlueGigaBluetoothCharacteristic characteristic = entry.getValue();
696
697         if (handle == entry.getKey()) {
698             // this is the declaration
699             if (parseDeclaration(characteristic, event.getValue())) {
700                 BluetoothService service = getServiceByHandle(handle);
701                 if (service == null) {
702                     logger.debug("BlueGiga: Unable to find service for handle {}", handle);
703                     return;
704                 }
705                 service.addCharacteristic(characteristic);
706             }
707             return;
708         }
709         if (handle == characteristic.getHandle()) {
710             characteristic.setValue(event.getValue().clone());
711
712             // If this is the characteristic we were reading, then send a read completion
713             if (procedureProgress == BlueGigaProcedure.CHARACTERISTIC_READ && procedureCharacteristic != null
714                     && procedureCharacteristic.getHandle() == event.getAttHandle()) {
715                 procedureProgress = BlueGigaProcedure.NONE;
716                 procedureCharacteristic = null;
717                 notifyListeners(BluetoothEventType.CHARACTERISTIC_READ_COMPLETE, characteristic,
718                         BluetoothCompletionStatus.SUCCESS);
719                 return;
720             }
721
722             // Notify the user of the updated value
723             notifyListeners(BluetoothEventType.CHARACTERISTIC_UPDATED, characteristic);
724         } else {
725             // it must be one of the descriptors we need to update
726             UUID attUUID = handleToUUID.get(handle);
727             BluetoothDescriptor descriptor = characteristic.getDescriptor(attUUID);
728             descriptor.setValue(toBytes(event.getValue()));
729             notifyListeners(BluetoothEventType.DESCRIPTOR_UPDATED, descriptor);
730         }
731     }
732
733     private static byte @Nullable [] toBytes(int @Nullable [] value) {
734         if (value == null) {
735             return null;
736         }
737         byte[] ret = new byte[value.length];
738         for (int i = 0; i < value.length; i++) {
739             ret[i] = (byte) value[i];
740         }
741         return ret;
742     }
743
744     private boolean parseDeclaration(BlueGigaBluetoothCharacteristic ch, int[] value) {
745         ByteBuffer buffer = ByteBuffer.wrap(toBytes(value));
746         buffer.order(ByteOrder.LITTLE_ENDIAN);
747
748         ch.setProperties(Byte.toUnsignedInt(buffer.get()));
749         ch.setHandle(Short.toUnsignedInt(buffer.getShort()));
750
751         switch (buffer.remaining()) {
752             case 2:
753                 long key = Short.toUnsignedLong(buffer.getShort());
754                 ch.setUUID(BluetoothBindingConstants.createBluetoothUUID(key));
755                 return true;
756             case 4:
757                 key = Integer.toUnsignedLong(buffer.getInt());
758                 ch.setUUID(BluetoothBindingConstants.createBluetoothUUID(key));
759                 return true;
760             case 16:
761                 long lower = buffer.getLong();
762                 long upper = buffer.getLong();
763                 ch.setUUID(new UUID(upper, lower));
764                 return true;
765             default:
766                 logger.debug("Unexpected uuid length: {}", buffer.remaining());
767                 return false;
768         }
769     }
770
771     /**
772      * Clean up and release memory.
773      */
774     @Override
775     public void dispose() {
776         if (connectionState == ConnectionState.CONNECTED) {
777             disconnect();
778         }
779         cancelTimer(connectTimer);
780         cancelTimer(procedureTimer);
781         bgHandler.removeEventListener(this);
782         procedureProgress = BlueGigaProcedure.NONE;
783         connectionState = ConnectionState.DISCOVERING;
784         connection = -1;
785     }
786
787     private void cancelTimer(@Nullable ScheduledFuture<?> task) {
788         if (task != null) {
789             task.cancel(true);
790         }
791     }
792
793     private ScheduledFuture<?> startTimer(Runnable command, long timeout) {
794         return scheduler.schedule(command, timeout, TimeUnit.SECONDS);
795     }
796 }