]> git.basschouten.com Git - openhab-addons.git/blob
c2cb969e8fe4efc4ec0ea0cd76ca53b49ba2c4da
[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.isNotificationEnabled()) {
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 enable notifications, device not connected {}", this);
245             return false;
246         }
247
248         BlueGigaBluetoothCharacteristic ch = (BlueGigaBluetoothCharacteristic) characteristic;
249         if (ch.isNotificationEnabled()) {
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 enableNotifications(BluetoothDescriptor descriptor) {
293         // TODO will be implemented in a followup PR
294         return false;
295     }
296
297     @Override
298     public boolean disableNotifications(BluetoothDescriptor descriptor) {
299         // TODO will be implemented in a followup PR
300         return false;
301     }
302
303     @Override
304     public boolean readCharacteristic(@Nullable BluetoothCharacteristic characteristic) {
305         if (characteristic == null || characteristic.getHandle() == 0) {
306             return false;
307         }
308         if (connection == -1) {
309             return false;
310         }
311
312         if (procedureProgress != BlueGigaProcedure.NONE) {
313             return false;
314         }
315
316         cancelTimer(procedureTimer);
317         if (!bgHandler.bgReadCharacteristic(connection, characteristic.getHandle())) {
318             return false;
319         }
320         procedureTimer = startTimer(procedureTimeoutTask, TIMEOUT_SEC);
321         procedureProgress = BlueGigaProcedure.CHARACTERISTIC_READ;
322         procedureCharacteristic = characteristic;
323
324         return true;
325     }
326
327     @Override
328     public boolean writeCharacteristic(@Nullable BluetoothCharacteristic characteristic) {
329         if (characteristic == null || characteristic.getHandle() == 0) {
330             return false;
331         }
332         if (connection == -1) {
333             return false;
334         }
335
336         if (procedureProgress != BlueGigaProcedure.NONE) {
337             return false;
338         }
339
340         cancelTimer(procedureTimer);
341         if (!bgHandler.bgWriteCharacteristic(connection, characteristic.getHandle(), characteristic.getValue())) {
342             return false;
343         }
344
345         procedureTimer = startTimer(procedureTimeoutTask, TIMEOUT_SEC);
346         procedureProgress = BlueGigaProcedure.CHARACTERISTIC_WRITE;
347         procedureCharacteristic = characteristic;
348
349         return true;
350     }
351
352     @Override
353     public void bluegigaEventReceived(BlueGigaResponse event) {
354         if (event instanceof BlueGigaScanResponseEvent) {
355             handleScanEvent((BlueGigaScanResponseEvent) event);
356         }
357
358         else if (event instanceof BlueGigaGroupFoundEvent) {
359             handleGroupFoundEvent((BlueGigaGroupFoundEvent) event);
360         }
361
362         else if (event instanceof BlueGigaFindInformationFoundEvent) {
363             // A Characteristic has been discovered
364             handleFindInformationFoundEvent((BlueGigaFindInformationFoundEvent) event);
365         }
366
367         else if (event instanceof BlueGigaProcedureCompletedEvent) {
368             handleProcedureCompletedEvent((BlueGigaProcedureCompletedEvent) event);
369         }
370
371         else if (event instanceof BlueGigaConnectionStatusEvent) {
372             handleConnectionStatusEvent((BlueGigaConnectionStatusEvent) event);
373         }
374
375         else if (event instanceof BlueGigaDisconnectedEvent) {
376             handleDisconnectedEvent((BlueGigaDisconnectedEvent) event);
377         }
378
379         else if (event instanceof BlueGigaAttributeValueEvent) {
380             handleAttributeValueEvent((BlueGigaAttributeValueEvent) event);
381         }
382     }
383
384     private void handleScanEvent(BlueGigaScanResponseEvent event) {
385         // Check if this is addressed to this device
386         if (!address.equals(new BluetoothAddress(event.getSender()))) {
387             return;
388         }
389
390         logger.trace("scanEvent: {}", event);
391         updateLastSeenTime();
392
393         // Set device properties
394         rssi = event.getRssi();
395         addressType = event.getAddressType();
396
397         byte[] manufacturerData = null;
398
399         // If the packet contains data, then process it and add anything relevant to the device...
400         if (event.getData().length > 0) {
401             EirPacket eir = new EirPacket(event.getData());
402             for (EirDataType record : eir.getRecords().keySet()) {
403                 if (logger.isTraceEnabled()) {
404                     logger.trace("  EirDataType: {}={}", record, eir.getRecord(record));
405                 }
406                 Object obj;
407                 switch (record) {
408                     case EIR_FLAGS:
409                         break;
410                     case EIR_MANUFACTURER_SPECIFIC:
411                         obj = eir.getRecord(EirDataType.EIR_MANUFACTURER_SPECIFIC);
412                         if (obj != null) {
413                             try {
414                                 @SuppressWarnings("unchecked")
415                                 Map<Short, int[]> eirRecord = (Map<Short, int[]>) obj;
416                                 Map.Entry<Short, int[]> eirEntry = eirRecord.entrySet().iterator().next();
417
418                                 manufacturer = eirEntry.getKey().intValue();
419
420                                 int[] manufacturerInt = eirEntry.getValue();
421                                 manufacturerData = new byte[manufacturerInt.length + 2];
422                                 // Convert short Company ID to bytes and add it to manufacturerData
423                                 manufacturerData[0] = (byte) (manufacturer & 0xff);
424                                 manufacturerData[1] = (byte) ((manufacturer >> 8) & 0xff);
425                                 // Add Convert int custom data nd add it to manufacturerData
426                                 for (int i = 0; i < manufacturerInt.length; i++) {
427                                     manufacturerData[i + 2] = (byte) manufacturerInt[i];
428                                 }
429                             } catch (ClassCastException e) {
430                                 logger.debug("Unsupported manufacturer specific record received from device {}",
431                                         address);
432                             }
433                         }
434                         break;
435                     case EIR_NAME_LONG:
436                     case EIR_NAME_SHORT:
437                         name = (String) eir.getRecord(record);
438                         break;
439                     case EIR_SLAVEINTERVALRANGE:
440                         break;
441                     case EIR_SVC_DATA_UUID128:
442                         break;
443                     case EIR_SVC_DATA_UUID16:
444                         break;
445                     case EIR_SVC_DATA_UUID32:
446                         break;
447                     case EIR_SVC_UUID128_INCOMPLETE:
448                     case EIR_SVC_UUID16_COMPLETE:
449                     case EIR_SVC_UUID16_INCOMPLETE:
450                     case EIR_SVC_UUID32_COMPLETE:
451                     case EIR_SVC_UUID32_INCOMPLETE:
452                     case EIR_SVC_UUID128_COMPLETE:
453                         // addServices((List<UUID>) eir.getRecord(record));
454                         break;
455                     case EIR_TXPOWER:
456                         obj = eir.getRecord(EirDataType.EIR_TXPOWER);
457                         if (obj != null) {
458                             txPower = (int) obj;
459                         }
460                         break;
461                     default:
462                         break;
463                 }
464             }
465         }
466
467         if (connectionState == ConnectionState.DISCOVERING) {
468             // TODO: It could make sense to wait with discovery for non-connectable devices until scan response is
469             // received to eventually retrieve more about the device before it gets discovered. Anyhow, devices
470             // that don't send a scan response at all also have to be supported. See also PR #6995.
471
472             // Set our state to disconnected
473             connectionState = ConnectionState.DISCONNECTED;
474             connection = -1;
475
476             // But notify listeners that the state is now DISCOVERED
477             notifyListeners(BluetoothEventType.CONNECTION_STATE,
478                     new BluetoothConnectionStatusNotification(ConnectionState.DISCOVERED));
479
480             // Notify the bridge - for inbox notifications
481             bgHandler.deviceDiscovered(this);
482         }
483
484         // Notify listeners of all scan records - for RSSI, beacon processing (etc)
485         BluetoothScanNotification scanNotification = new BluetoothScanNotification();
486         scanNotification.setRssi(event.getRssi());
487
488         switch (event.getPacketType()) {
489             case CONNECTABLE_ADVERTISEMENT:
490             case DISCOVERABLE_ADVERTISEMENT:
491             case NON_CONNECTABLE_ADVERTISEMENT:
492                 scanNotification.setBeaconType(BluetoothBeaconType.BEACON_ADVERTISEMENT);
493                 break;
494             case SCAN_RESPONSE:
495                 scanNotification.setBeaconType(BluetoothBeaconType.BEACON_SCANRESPONSE);
496                 break;
497             default:
498                 break;
499         }
500
501         if (manufacturerData != null) {
502             scanNotification.setManufacturerData(manufacturerData);
503         }
504
505         notifyListeners(BluetoothEventType.SCAN_RECORD, scanNotification);
506     }
507
508     private void handleGroupFoundEvent(BlueGigaGroupFoundEvent event) {
509         // If this is not our connection handle then ignore.
510         if (connection != event.getConnection()) {
511             return;
512         }
513
514         logger.trace("BlueGiga Group: {} event={}", this, event);
515         updateLastSeenTime();
516
517         BluetoothService service = new BluetoothService(event.getUuid(), true, event.getStart(), event.getEnd());
518         addService(service);
519     }
520
521     private void handleFindInformationFoundEvent(BlueGigaFindInformationFoundEvent event) {
522         // If this is not our connection handle then ignore.
523         if (connection != event.getConnection()) {
524             return;
525         }
526
527         logger.trace("BlueGiga FindInfo: {} event={}", this, event);
528         updateLastSeenTime();
529
530         int handle = event.getChrHandle();
531         UUID attUUID = event.getUuid();
532
533         BluetoothService service = getServiceByHandle(handle);
534         if (service == null) {
535             logger.debug("BlueGiga: Unable to find service for handle {}", handle);
536             return;
537         }
538         handleToUUID.put(handle, attUUID);
539
540         if (BluetoothBindingConstants.ATTR_CHARACTERISTIC_DECLARATION.equals(attUUID)) {
541             BlueGigaBluetoothCharacteristic characteristic = new BlueGigaBluetoothCharacteristic(handle);
542             characteristic.setService(service);
543             handleToCharacteristic.put(handle, characteristic);
544         } else {
545             Integer chrHandle = handleToCharacteristic.floorKey(handle);
546             if (chrHandle == null) {
547                 logger.debug("BlueGiga: Unable to find characteristic for handle {}", handle);
548                 return;
549             }
550             BlueGigaBluetoothCharacteristic characteristic = handleToCharacteristic.get(chrHandle);
551             characteristic.addDescriptor(new BluetoothDescriptor(characteristic, attUUID, handle));
552         }
553     }
554
555     private void handleProcedureCompletedEvent(BlueGigaProcedureCompletedEvent event) {
556         // If this is not our connection handle then ignore.
557         if (connection != event.getConnection()) {
558             return;
559         }
560
561         if (procedureProgress == BlueGigaProcedure.NONE) {
562             logger.debug("BlueGiga procedure completed but procedure is null with connection {}, address {}",
563                     connection, address);
564             return;
565         }
566
567         cancelTimer(procedureTimer);
568         updateLastSeenTime();
569
570         // The current procedure is now complete - move on...
571         switch (procedureProgress) {
572             case GET_SERVICES:
573                 // We've downloaded all services, now get the characteristics
574                 if (bgHandler.bgFindCharacteristics(connection)) {
575                     procedureTimer = startTimer(procedureTimeoutTask, TIMEOUT_SEC);
576                     procedureProgress = BlueGigaProcedure.GET_CHARACTERISTICS;
577                 } else {
578                     procedureProgress = BlueGigaProcedure.NONE;
579                 }
580                 break;
581             case GET_CHARACTERISTICS:
582                 // We've downloaded all attributes, now read the characteristic declarations
583                 if (bgHandler.bgReadCharacteristicDeclarations(connection)) {
584                     procedureTimer = startTimer(procedureTimeoutTask, TIMEOUT_SEC);
585                     procedureProgress = BlueGigaProcedure.READ_CHARACTERISTIC_DECL;
586                 } else {
587                     procedureProgress = BlueGigaProcedure.NONE;
588                 }
589                 break;
590             case READ_CHARACTERISTIC_DECL:
591                 // We've downloaded read all the declarations, we are done now
592                 procedureProgress = BlueGigaProcedure.NONE;
593                 notifyListeners(BluetoothEventType.SERVICES_DISCOVERED);
594                 break;
595             case CHARACTERISTIC_READ:
596                 // The read failed
597                 notifyListeners(BluetoothEventType.CHARACTERISTIC_READ_COMPLETE, procedureCharacteristic,
598                         BluetoothCompletionStatus.ERROR);
599                 procedureProgress = BlueGigaProcedure.NONE;
600                 procedureCharacteristic = null;
601                 break;
602             case CHARACTERISTIC_WRITE:
603                 // The write completed - failure or success
604                 BluetoothCompletionStatus result = event.getResult() == BgApiResponse.SUCCESS
605                         ? BluetoothCompletionStatus.SUCCESS
606                         : BluetoothCompletionStatus.ERROR;
607                 notifyListeners(BluetoothEventType.CHARACTERISTIC_WRITE_COMPLETE, procedureCharacteristic, result);
608                 procedureProgress = BlueGigaProcedure.NONE;
609                 procedureCharacteristic = null;
610                 break;
611             case NOTIFICATION_ENABLE:
612                 boolean success = event.getResult() == BgApiResponse.SUCCESS;
613                 if (!success) {
614                     logger.debug("write to descriptor failed");
615                 }
616                 ((BlueGigaBluetoothCharacteristic) procedureCharacteristic).setNotificationEnabled(success);
617                 procedureProgress = BlueGigaProcedure.NONE;
618                 procedureCharacteristic = null;
619                 break;
620             case NOTIFICATION_DISABLE:
621                 success = event.getResult() == BgApiResponse.SUCCESS;
622                 if (!success) {
623                     logger.debug("write to descriptor failed");
624                 }
625                 ((BlueGigaBluetoothCharacteristic) procedureCharacteristic).setNotificationEnabled(!success);
626                 procedureProgress = BlueGigaProcedure.NONE;
627                 procedureCharacteristic = null;
628                 break;
629             default:
630                 break;
631         }
632     }
633
634     private void handleConnectionStatusEvent(BlueGigaConnectionStatusEvent event) {
635         // Check if this is addressed to this device
636         if (!address.equals(new BluetoothAddress(event.getAddress()))) {
637             return;
638         }
639
640         cancelTimer(connectTimer);
641         updateLastSeenTime();
642
643         // If we're connected, then remember the connection handle
644         if (event.getFlags().contains(ConnectionStatusFlag.CONNECTION_CONNECTED)) {
645             connectionState = ConnectionState.CONNECTED;
646             connection = event.getConnection();
647             notifyListeners(BluetoothEventType.CONNECTION_STATE,
648                     new BluetoothConnectionStatusNotification(connectionState));
649         }
650     }
651
652     private void handleDisconnectedEvent(BlueGigaDisconnectedEvent event) {
653         // If this is not our connection handle then ignore.
654         if (connection != event.getConnection()) {
655             return;
656         }
657
658         for (BlueGigaBluetoothCharacteristic ch : handleToCharacteristic.values()) {
659             ch.setNotificationEnabled(false);
660         }
661
662         cancelTimer(procedureTimer);
663         connectionState = ConnectionState.DISCONNECTED;
664         connection = -1;
665         procedureProgress = BlueGigaProcedure.NONE;
666
667         notifyListeners(BluetoothEventType.CONNECTION_STATE,
668                 new BluetoothConnectionStatusNotification(connectionState));
669     }
670
671     private void handleAttributeValueEvent(BlueGigaAttributeValueEvent event) {
672         // If this is not our connection handle then ignore.
673         if (connection != event.getConnection()) {
674             return;
675         }
676
677         updateLastSeenTime();
678
679         logger.trace("BlueGiga AttributeValue: {} event={}", this, event);
680
681         int handle = event.getAttHandle();
682
683         Map.Entry<Integer, BlueGigaBluetoothCharacteristic> entry = handleToCharacteristic.floorEntry(handle);
684         if (entry == null) {
685             logger.debug("BlueGiga didn't find characteristic for event {}", event);
686             return;
687         }
688
689         BlueGigaBluetoothCharacteristic characteristic = entry.getValue();
690
691         if (handle == entry.getKey()) {
692             // this is the declaration
693             if (parseDeclaration(characteristic, event.getValue())) {
694                 BluetoothService service = getServiceByHandle(handle);
695                 if (service == null) {
696                     logger.debug("BlueGiga: Unable to find service for handle {}", handle);
697                     return;
698                 }
699                 service.addCharacteristic(characteristic);
700             }
701             return;
702         }
703         if (handle == characteristic.getHandle()) {
704             characteristic.setValue(event.getValue().clone());
705
706             // If this is the characteristic we were reading, then send a read completion
707             if (procedureProgress == BlueGigaProcedure.CHARACTERISTIC_READ && procedureCharacteristic != null
708                     && procedureCharacteristic.getHandle() == event.getAttHandle()) {
709                 procedureProgress = BlueGigaProcedure.NONE;
710                 procedureCharacteristic = null;
711                 notifyListeners(BluetoothEventType.CHARACTERISTIC_READ_COMPLETE, characteristic,
712                         BluetoothCompletionStatus.SUCCESS);
713                 return;
714             }
715
716             // Notify the user of the updated value
717             notifyListeners(BluetoothEventType.CHARACTERISTIC_UPDATED, characteristic);
718         } else {
719             // it must be one of the descriptors we need to update
720             UUID attUUID = handleToUUID.get(handle);
721             BluetoothDescriptor descriptor = characteristic.getDescriptor(attUUID);
722             descriptor.setValue(toBytes(event.getValue()));
723             notifyListeners(BluetoothEventType.DESCRIPTOR_UPDATED, descriptor);
724         }
725     }
726
727     private static byte @Nullable [] toBytes(int @Nullable [] value) {
728         if (value == null) {
729             return null;
730         }
731         byte[] ret = new byte[value.length];
732         for (int i = 0; i < value.length; i++) {
733             ret[i] = (byte) value[i];
734         }
735         return ret;
736     }
737
738     private boolean parseDeclaration(BlueGigaBluetoothCharacteristic ch, int[] value) {
739         ByteBuffer buffer = ByteBuffer.wrap(toBytes(value));
740         buffer.order(ByteOrder.LITTLE_ENDIAN);
741
742         ch.setProperties(Byte.toUnsignedInt(buffer.get()));
743         ch.setHandle(Short.toUnsignedInt(buffer.getShort()));
744
745         switch (buffer.remaining()) {
746             case 2:
747                 long key = Short.toUnsignedLong(buffer.getShort());
748                 ch.setUUID(BluetoothBindingConstants.createBluetoothUUID(key));
749                 return true;
750             case 4:
751                 key = Integer.toUnsignedLong(buffer.getInt());
752                 ch.setUUID(BluetoothBindingConstants.createBluetoothUUID(key));
753                 return true;
754             case 16:
755                 long lower = buffer.getLong();
756                 long upper = buffer.getLong();
757                 ch.setUUID(new UUID(upper, lower));
758                 return true;
759             default:
760                 logger.debug("Unexpected uuid length: {}", buffer.remaining());
761                 return false;
762         }
763     }
764
765     /**
766      * Clean up and release memory.
767      */
768     @Override
769     public void dispose() {
770         if (connectionState == ConnectionState.CONNECTED) {
771             disconnect();
772         }
773         cancelTimer(connectTimer);
774         cancelTimer(procedureTimer);
775         bgHandler.removeEventListener(this);
776         procedureProgress = BlueGigaProcedure.NONE;
777         connectionState = ConnectionState.DISCOVERING;
778         connection = -1;
779     }
780
781     private void cancelTimer(@Nullable ScheduledFuture<?> task) {
782         if (task != null) {
783             task.cancel(true);
784         }
785     }
786
787     private ScheduledFuture<?> startTimer(Runnable command, long timeout) {
788         return scheduler.schedule(command, timeout, TimeUnit.SECONDS);
789     }
790 }