2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.bluetooth.bluegiga;
15 import java.nio.ByteBuffer;
16 import java.nio.ByteOrder;
17 import java.util.HashMap;
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;
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;
62 * An extended {@link BluetoothDevice} class to handle BlueGiga specific information
64 * @author Chris Jackson - Initial contribution
67 public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements BlueGigaEventListener {
68 private final long TIMEOUT_SEC = 60;
70 private final Logger logger = LoggerFactory.getLogger(BlueGigaBluetoothDevice.class);
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);
80 private Map<Integer, UUID> handleToUUID = new HashMap<>();
81 private NavigableMap<Integer, BlueGigaBluetoothCharacteristic> handleToCharacteristic = new TreeMap<>();
83 // BlueGiga needs to know the address type when connecting
84 private BluetoothAddressType addressType = BluetoothAddressType.UNKNOWN;
87 private final BlueGigaBridgeHandler bgHandler;
89 private BlueGigaProcedure currentProcedure = PROCEDURE_NONE;
91 // The connection handle if the device is connected
92 private int connection = -1;
94 private final ScheduledExecutorService scheduler = ThreadPoolManager.getScheduledPool("bluetooth");
96 private @Nullable ScheduledFuture<?> connectTimer;
97 private @Nullable ScheduledFuture<?> procedureTimer;
99 private Runnable connectTimeoutTask = new Runnable() {
102 if (connectionState == ConnectionState.CONNECTING) {
103 logger.debug("Connection timeout for device {}", address);
104 connectionState = ConnectionState.DISCONNECTED;
109 private Runnable procedureTimeoutTask = new Runnable() {
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));
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));
129 currentProcedure = PROCEDURE_NONE;
134 * Creates a new {@link BlueGigaBluetoothDevice} which extends {@link BluetoothDevice} for the BlueGiga
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
141 public BlueGigaBluetoothDevice(BlueGigaBridgeHandler bgHandler, BluetoothAddress address,
142 BluetoothAddressType addressType) {
143 super(bgHandler, address);
145 logger.debug("Creating new BlueGiga device {}", address);
147 this.bgHandler = bgHandler;
148 this.addressType = addressType;
150 bgHandler.addEventListener(this);
151 updateLastSeenTime();
154 public void setAddressType(BluetoothAddressType addressType) {
155 this.addressType = addressType;
159 public boolean connect() {
160 if (connection != -1) {
161 // We're already connected
165 cancelTimer(connectTimer);
166 if (bgHandler.bgConnect(address, addressType)) {
167 connectionState = ConnectionState.CONNECTING;
168 connectTimer = startTimer(connectTimeoutTask, 10);
171 connectionState = ConnectionState.DISCONNECTED;
177 public boolean disconnect() {
178 if (connection == -1) {
179 // We're already disconnected
183 return bgHandler.bgDisconnect(connection);
187 public boolean discoverServices() {
188 if (!PROCEDURE_NONE.equals(currentProcedure)) {
192 cancelTimer(procedureTimer);
193 if (!bgHandler.bgFindPrimaryServices(connection)) {
197 procedureTimer = startTimer(procedureTimeoutTask, TIMEOUT_SEC);
198 currentProcedure = PROCEDURE_GET_SERVICES;
203 public CompletableFuture<@Nullable Void> enableNotifications(BluetoothCharacteristic characteristic) {
204 if (connection == -1) {
205 return CompletableFuture.failedFuture(new BluetoothException("Not connected"));
208 BlueGigaBluetoothCharacteristic ch = (BlueGigaBluetoothCharacteristic) characteristic;
209 if (ch.isNotifying()) {
210 return CompletableFuture.completedFuture(null);
213 BluetoothDescriptor descriptor = ch
214 .getDescriptor(BluetoothDescriptor.GattDescriptor.CLIENT_CHARACTERISTIC_CONFIGURATION.getUUID());
216 if (descriptor == null || descriptor.getHandle() == 0) {
217 return CompletableFuture.failedFuture(
218 new BluetoothException("Unable to find CCC for characteristic [" + characteristic.getUuid() + "]"));
221 if (!PROCEDURE_NONE.equals(currentProcedure)) {
222 return CompletableFuture.failedFuture(new BluetoothException("Another procedure is already in progress"));
225 int[] value = { 1, 0 };
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() + "]"));
233 procedureTimer = startTimer(procedureTimeoutTask, TIMEOUT_SEC);
234 WriteCharacteristicProcedure notifyProcedure = new WriteCharacteristicProcedure(ch,
235 BlueGigaProcedure.Type.NOTIFICATION_ENABLE);
236 currentProcedure = notifyProcedure;
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
242 } catch (InterruptedException e) {
243 Thread.currentThread().interrupt();
245 return notifyProcedure.writeFuture;
249 public CompletableFuture<@Nullable Void> disableNotifications(BluetoothCharacteristic characteristic) {
250 if (connection == -1) {
251 return CompletableFuture.failedFuture(new BluetoothException("Not connected"));
254 BlueGigaBluetoothCharacteristic ch = (BlueGigaBluetoothCharacteristic) characteristic;
255 if (!ch.isNotifying()) {
256 return CompletableFuture.completedFuture(null);
259 BluetoothDescriptor descriptor = ch
260 .getDescriptor(BluetoothDescriptor.GattDescriptor.CLIENT_CHARACTERISTIC_CONFIGURATION.getUUID());
262 if (descriptor == null || descriptor.getHandle() == 0) {
263 return CompletableFuture.failedFuture(
264 new BluetoothException("Unable to find CCC for characteristic [" + characteristic.getUuid() + "]"));
267 if (!PROCEDURE_NONE.equals(currentProcedure)) {
268 return CompletableFuture.failedFuture(new BluetoothException("Another procedure is already in progress"));
271 int[] value = { 0, 0 };
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() + "]"));
279 procedureTimer = startTimer(procedureTimeoutTask, TIMEOUT_SEC);
280 WriteCharacteristicProcedure notifyProcedure = new WriteCharacteristicProcedure(ch,
281 BlueGigaProcedure.Type.NOTIFICATION_DISABLE);
282 currentProcedure = notifyProcedure;
284 return notifyProcedure.writeFuture;
288 public boolean isNotifying(BluetoothCharacteristic characteristic) {
289 BlueGigaBluetoothCharacteristic ch = (BlueGigaBluetoothCharacteristic) characteristic;
290 return ch.isNotifying();
294 public boolean enableNotifications(BluetoothDescriptor descriptor) {
295 // TODO will be implemented in a followup PR
300 public boolean disableNotifications(BluetoothDescriptor descriptor) {
301 // TODO will be implemented in a followup PR
306 public CompletableFuture<byte[]> readCharacteristic(BluetoothCharacteristic characteristic) {
307 if (characteristic.getHandle() == 0) {
308 return CompletableFuture.failedFuture(new BluetoothException("Cannot read characteristic with no handle"));
310 if (connection == -1) {
311 return CompletableFuture.failedFuture(new BluetoothException("Not connected"));
314 if (!PROCEDURE_NONE.equals(currentProcedure)) {
315 return CompletableFuture.failedFuture(new BluetoothException("Another procedure is already in progress"));
318 cancelTimer(procedureTimer);
319 if (!bgHandler.bgReadCharacteristic(connection, characteristic.getHandle())) {
320 return CompletableFuture.failedFuture(
321 new BluetoothException("Failed to read characteristic [" + characteristic.getUuid() + "]"));
323 procedureTimer = startTimer(procedureTimeoutTask, TIMEOUT_SEC);
324 ReadCharacteristicProcedure readProcedure = new ReadCharacteristicProcedure(characteristic);
325 currentProcedure = readProcedure;
327 return readProcedure.readFuture;
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"));
335 if (connection == -1) {
336 return CompletableFuture.failedFuture(new BluetoothException("Not connected"));
339 if (!PROCEDURE_NONE.equals(currentProcedure)) {
340 return CompletableFuture.failedFuture(new BluetoothException("Another procedure is already in progress"));
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() + "]"));
350 procedureTimer = startTimer(procedureTimeoutTask, TIMEOUT_SEC);
351 WriteCharacteristicProcedure writeProcedure = new WriteCharacteristicProcedure(
352 (BlueGigaBluetoothCharacteristic) characteristic, BlueGigaProcedure.Type.CHARACTERISTIC_WRITE);
353 currentProcedure = writeProcedure;
355 return writeProcedure.writeFuture;
359 public void bluegigaEventReceived(BlueGigaResponse event) {
360 if (event instanceof BlueGigaScanResponseEvent) {
361 handleScanEvent((BlueGigaScanResponseEvent) event);
364 else if (event instanceof BlueGigaGroupFoundEvent) {
365 handleGroupFoundEvent((BlueGigaGroupFoundEvent) event);
368 else if (event instanceof BlueGigaFindInformationFoundEvent) {
369 // A Characteristic has been discovered
370 handleFindInformationFoundEvent((BlueGigaFindInformationFoundEvent) event);
373 else if (event instanceof BlueGigaProcedureCompletedEvent) {
374 handleProcedureCompletedEvent((BlueGigaProcedureCompletedEvent) event);
377 else if (event instanceof BlueGigaConnectionStatusEvent) {
378 handleConnectionStatusEvent((BlueGigaConnectionStatusEvent) event);
381 else if (event instanceof BlueGigaDisconnectedEvent) {
382 handleDisconnectedEvent((BlueGigaDisconnectedEvent) event);
385 else if (event instanceof BlueGigaAttributeValueEvent) {
386 handleAttributeValueEvent((BlueGigaAttributeValueEvent) event);
390 private void handleScanEvent(BlueGigaScanResponseEvent event) {
391 // Check if this is addressed to this device
392 if (!address.equals(new BluetoothAddress(event.getSender()))) {
396 logger.trace("scanEvent: {}", event);
397 updateLastSeenTime();
399 // Set device properties
400 rssi = event.getRssi();
401 addressType = event.getAddressType();
403 byte[] manufacturerData = null;
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));
416 case EIR_MANUFACTURER_SPECIFIC:
417 obj = eir.getRecord(EirDataType.EIR_MANUFACTURER_SPECIFIC);
420 @SuppressWarnings("unchecked")
421 Map<Short, int[]> eirRecord = (Map<Short, int[]>) obj;
422 Map.Entry<Short, int[]> eirEntry = eirRecord.entrySet().iterator().next();
424 manufacturer = eirEntry.getKey().intValue();
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];
435 } catch (ClassCastException e) {
436 logger.debug("Unsupported manufacturer specific record received from device {}",
443 name = (String) eir.getRecord(record);
445 case EIR_SLAVEINTERVALRANGE:
447 case EIR_SVC_DATA_UUID128:
449 case EIR_SVC_DATA_UUID16:
451 case EIR_SVC_DATA_UUID32:
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));
462 obj = eir.getRecord(EirDataType.EIR_TXPOWER);
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.
478 // Set our state to disconnected
479 connectionState = ConnectionState.DISCONNECTED;
482 // But notify listeners that the state is now DISCOVERED
483 notifyListeners(BluetoothEventType.CONNECTION_STATE,
484 new BluetoothConnectionStatusNotification(ConnectionState.DISCOVERED));
486 // Notify the bridge - for inbox notifications
487 bgHandler.deviceDiscovered(this);
490 // Notify listeners of all scan records - for RSSI, beacon processing (etc)
491 BluetoothScanNotification scanNotification = new BluetoothScanNotification();
492 scanNotification.setRssi(event.getRssi());
494 switch (event.getPacketType()) {
495 case CONNECTABLE_ADVERTISEMENT:
496 case DISCOVERABLE_ADVERTISEMENT:
497 case NON_CONNECTABLE_ADVERTISEMENT:
498 scanNotification.setBeaconType(BluetoothBeaconType.BEACON_ADVERTISEMENT);
501 scanNotification.setBeaconType(BluetoothBeaconType.BEACON_SCANRESPONSE);
507 if (manufacturerData != null) {
508 scanNotification.setManufacturerData(manufacturerData);
511 notifyListeners(BluetoothEventType.SCAN_RECORD, scanNotification);
514 private void handleGroupFoundEvent(BlueGigaGroupFoundEvent event) {
515 // If this is not our connection handle then ignore.
516 if (connection != event.getConnection()) {
520 logger.trace("BlueGiga Group: {} event={}", this, event);
521 updateLastSeenTime();
523 BluetoothService service = new BluetoothService(event.getUuid(), true, event.getStart(), event.getEnd());
527 private void handleFindInformationFoundEvent(BlueGigaFindInformationFoundEvent event) {
528 // If this is not our connection handle then ignore.
529 if (connection != event.getConnection()) {
533 logger.trace("BlueGiga FindInfo: {} event={}", this, event);
534 updateLastSeenTime();
536 int handle = event.getChrHandle();
537 UUID attUUID = event.getUuid();
539 BluetoothService service = getServiceByHandle(handle);
540 if (service == null) {
541 logger.debug("BlueGiga: Unable to find service for handle {}", handle);
544 handleToUUID.put(handle, attUUID);
546 if (BluetoothBindingConstants.ATTR_CHARACTERISTIC_DECLARATION.equals(attUUID)) {
547 BlueGigaBluetoothCharacteristic characteristic = new BlueGigaBluetoothCharacteristic(handle);
548 characteristic.setService(service);
549 handleToCharacteristic.put(handle, characteristic);
551 Integer chrHandle = handleToCharacteristic.floorKey(handle);
552 if (chrHandle == null) {
553 logger.debug("BlueGiga: Unable to find characteristic for handle {}", handle);
556 BlueGigaBluetoothCharacteristic characteristic = handleToCharacteristic.get(chrHandle);
557 characteristic.addDescriptor(new BluetoothDescriptor(characteristic, attUUID, handle));
561 private void handleProcedureCompletedEvent(BlueGigaProcedureCompletedEvent event) {
562 // If this is not our connection handle then ignore.
563 if (connection != event.getConnection()) {
567 if (PROCEDURE_NONE.equals(currentProcedure)) {
568 logger.debug("BlueGiga procedure completed but procedure is null with connection {}, address {}",
569 connection, address);
573 cancelTimer(procedureTimer);
574 updateLastSeenTime();
576 // The current procedure is now complete - move on...
577 switch (currentProcedure.type) {
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;
584 currentProcedure = PROCEDURE_NONE;
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;
593 currentProcedure = PROCEDURE_NONE;
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);
601 case CHARACTERISTIC_READ:
603 ReadCharacteristicProcedure readProcedure = (ReadCharacteristicProcedure) currentProcedure;
604 readProcedure.readFuture.completeExceptionally(new BluetoothException(
605 "Read characteristic failed: " + readProcedure.characteristic.getUuid()));
606 currentProcedure = PROCEDURE_NONE;
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);
614 writeProcedure.writeFuture.completeExceptionally(new BluetoothException(
615 "Write characteristic failed: " + writeProcedure.characteristic.getUuid()));
617 currentProcedure = PROCEDURE_NONE;
619 case NOTIFICATION_ENABLE:
620 WriteCharacteristicProcedure notifyEnableProcedure = (WriteCharacteristicProcedure) currentProcedure;
621 boolean success = event.getResult() == BgApiResponse.SUCCESS;
623 notifyEnableProcedure.writeFuture.complete(null);
625 notifyEnableProcedure.writeFuture
626 .completeExceptionally(new BluetoothException("Enable characteristic notification failed: "
627 + notifyEnableProcedure.characteristic.getUuid()));
629 notifyEnableProcedure.characteristic.setNotifying(success);
630 currentProcedure = PROCEDURE_NONE;
632 case NOTIFICATION_DISABLE:
633 WriteCharacteristicProcedure notifyDisableProcedure = (WriteCharacteristicProcedure) currentProcedure;
634 success = event.getResult() == BgApiResponse.SUCCESS;
636 notifyDisableProcedure.writeFuture.complete(null);
638 notifyDisableProcedure.writeFuture
639 .completeExceptionally(new BluetoothException("Disable characteristic notification failed: "
640 + notifyDisableProcedure.characteristic.getUuid()));
642 notifyDisableProcedure.characteristic.setNotifying(!success);
643 currentProcedure = PROCEDURE_NONE;
650 private void handleConnectionStatusEvent(BlueGigaConnectionStatusEvent event) {
651 // Check if this is addressed to this device
652 if (!address.equals(new BluetoothAddress(event.getAddress()))) {
656 cancelTimer(connectTimer);
657 updateLastSeenTime();
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));
668 private void handleDisconnectedEvent(BlueGigaDisconnectedEvent event) {
669 // If this is not our connection handle then ignore.
670 if (connection != event.getConnection()) {
674 for (BlueGigaBluetoothCharacteristic ch : handleToCharacteristic.values()) {
675 ch.setNotifying(false);
678 cancelTimer(procedureTimer);
679 connectionState = ConnectionState.DISCONNECTED;
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));
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));
697 currentProcedure = PROCEDURE_NONE;
699 notifyListeners(BluetoothEventType.CONNECTION_STATE,
700 new BluetoothConnectionStatusNotification(connectionState));
703 private void handleAttributeValueEvent(BlueGigaAttributeValueEvent event) {
704 // If this is not our connection handle then ignore.
705 if (connection != event.getConnection()) {
709 updateLastSeenTime();
711 logger.trace("BlueGiga AttributeValue: {} event={}", this, event);
713 int handle = event.getAttHandle();
715 Map.Entry<Integer, BlueGigaBluetoothCharacteristic> entry = handleToCharacteristic.floorEntry(handle);
717 logger.debug("BlueGiga didn't find characteristic for event {}", event);
721 BlueGigaBluetoothCharacteristic characteristic = entry.getValue();
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);
731 service.addCharacteristic(characteristic);
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;
747 // Notify the user of the updated value
748 notifyListeners(BluetoothEventType.CHARACTERISTIC_UPDATED, characteristic, value);
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()));
758 private boolean parseDeclaration(BlueGigaBluetoothCharacteristic ch, int[] value) {
759 ByteBuffer buffer = ByteBuffer.wrap(BluetoothUtils.toByteArray(value));
760 buffer.order(ByteOrder.LITTLE_ENDIAN);
762 ch.setProperties(Byte.toUnsignedInt(buffer.get()));
763 ch.setHandle(Short.toUnsignedInt(buffer.getShort()));
765 switch (buffer.remaining()) {
767 long key = Short.toUnsignedLong(buffer.getShort());
768 ch.setUUID(BluetoothBindingConstants.createBluetoothUUID(key));
771 key = Integer.toUnsignedLong(buffer.getInt());
772 ch.setUUID(BluetoothBindingConstants.createBluetoothUUID(key));
775 long lower = buffer.getLong();
776 long upper = buffer.getLong();
777 ch.setUUID(new UUID(upper, lower));
780 logger.debug("Unexpected uuid length: {}", buffer.remaining());
786 * Clean up and release memory.
789 public void dispose() {
790 if (connectionState == ConnectionState.CONNECTED) {
793 cancelTimer(connectTimer);
794 cancelTimer(procedureTimer);
795 bgHandler.removeEventListener(this);
796 currentProcedure = PROCEDURE_NONE;
797 connectionState = ConnectionState.DISCOVERING;
801 private void cancelTimer(@Nullable ScheduledFuture<?> task) {
807 private ScheduledFuture<?> startTimer(Runnable command, long timeout) {
808 return scheduler.schedule(command, timeout, TimeUnit.SECONDS);
811 private static class BlueGigaProcedure {
812 private final Type type;
814 public BlueGigaProcedure(Type type) {
818 // An enum to use in the state machine for interacting with the device
823 READ_CHARACTERISTIC_DECL,
825 CHARACTERISTIC_WRITE,
831 private static class ReadCharacteristicProcedure extends BlueGigaProcedure {
833 private final BluetoothCharacteristic characteristic;
835 private final CompletableFuture<byte[]> readFuture = new CompletableFuture<>();
837 public ReadCharacteristicProcedure(BluetoothCharacteristic characteristic) {
838 super(Type.CHARACTERISTIC_READ);
839 this.characteristic = characteristic;
843 private static class WriteCharacteristicProcedure extends BlueGigaProcedure {
845 private final BlueGigaBluetoothCharacteristic characteristic;
847 private final CompletableFuture<@Nullable Void> writeFuture = new CompletableFuture<>();
849 public WriteCharacteristicProcedure(BlueGigaBluetoothCharacteristic characteristic, Type type) {
851 this.characteristic = characteristic;