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.BluetoothException;
36 import org.openhab.binding.bluetooth.BluetoothService;
37 import org.openhab.binding.bluetooth.BluetoothUtils;
38 import org.openhab.binding.bluetooth.bluegiga.handler.BlueGigaBridgeHandler;
39 import org.openhab.binding.bluetooth.bluegiga.internal.BlueGigaEventListener;
40 import org.openhab.binding.bluetooth.bluegiga.internal.BlueGigaResponse;
41 import org.openhab.binding.bluetooth.bluegiga.internal.command.attributeclient.BlueGigaAttributeValueEvent;
42 import org.openhab.binding.bluetooth.bluegiga.internal.command.attributeclient.BlueGigaFindInformationFoundEvent;
43 import org.openhab.binding.bluetooth.bluegiga.internal.command.attributeclient.BlueGigaGroupFoundEvent;
44 import org.openhab.binding.bluetooth.bluegiga.internal.command.attributeclient.BlueGigaProcedureCompletedEvent;
45 import org.openhab.binding.bluetooth.bluegiga.internal.command.connection.BlueGigaConnectionStatusEvent;
46 import org.openhab.binding.bluetooth.bluegiga.internal.command.connection.BlueGigaDisconnectedEvent;
47 import org.openhab.binding.bluetooth.bluegiga.internal.command.gap.BlueGigaScanResponseEvent;
48 import org.openhab.binding.bluetooth.bluegiga.internal.eir.EirDataType;
49 import org.openhab.binding.bluetooth.bluegiga.internal.eir.EirPacket;
50 import org.openhab.binding.bluetooth.bluegiga.internal.enumeration.BgApiResponse;
51 import org.openhab.binding.bluetooth.bluegiga.internal.enumeration.BluetoothAddressType;
52 import org.openhab.binding.bluetooth.bluegiga.internal.enumeration.ConnectionStatusFlag;
53 import org.openhab.binding.bluetooth.notification.BluetoothConnectionStatusNotification;
54 import org.openhab.binding.bluetooth.notification.BluetoothScanNotification;
55 import org.openhab.binding.bluetooth.notification.BluetoothScanNotification.BluetoothBeaconType;
56 import org.openhab.core.common.ThreadPoolManager;
57 import org.slf4j.Logger;
58 import org.slf4j.LoggerFactory;
61 * An extended {@link BluetoothDevice} class to handle BlueGiga specific information
63 * @author Chris Jackson - Initial contribution
66 public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements BlueGigaEventListener {
67 private static final long TIMEOUT_SEC = 60;
69 private final Logger logger = LoggerFactory.getLogger(BlueGigaBluetoothDevice.class);
71 private static final BlueGigaProcedure PROCEDURE_NONE = new BlueGigaProcedure(BlueGigaProcedure.Type.NONE);
72 private static final BlueGigaProcedure PROCEDURE_GET_SERVICES = new BlueGigaProcedure(
73 BlueGigaProcedure.Type.GET_SERVICES);
74 private static final BlueGigaProcedure PROCEDURE_GET_CHARACTERISTICS = new BlueGigaProcedure(
75 BlueGigaProcedure.Type.GET_CHARACTERISTICS);
76 private static final BlueGigaProcedure PROCEDURE_READ_CHARACTERISTIC_DECL = new BlueGigaProcedure(
77 BlueGigaProcedure.Type.READ_CHARACTERISTIC_DECL);
79 private Map<Integer, UUID> handleToUUID = new HashMap<>();
80 private NavigableMap<Integer, BlueGigaBluetoothCharacteristic> handleToCharacteristic = new TreeMap<>();
82 // BlueGiga needs to know the address type when connecting
83 private BluetoothAddressType addressType = BluetoothAddressType.UNKNOWN;
86 private final BlueGigaBridgeHandler bgHandler;
88 private BlueGigaProcedure currentProcedure = PROCEDURE_NONE;
90 // The connection handle if the device is connected
91 private int connection = -1;
93 private final ScheduledExecutorService scheduler = ThreadPoolManager.getScheduledPool("bluetooth");
95 private @Nullable ScheduledFuture<?> connectTimer;
96 private @Nullable ScheduledFuture<?> procedureTimer;
98 private Runnable connectTimeoutTask = new Runnable() {
101 if (connectionState == ConnectionState.CONNECTING) {
102 logger.debug("Connection timeout for device {}", address);
103 connectionState = ConnectionState.DISCONNECTED;
108 private Runnable procedureTimeoutTask = new Runnable() {
111 BlueGigaProcedure procedure = currentProcedure;
112 logger.debug("Procedure {} timeout for device {}", procedure.type, address);
113 switch (procedure.type) {
114 case CHARACTERISTIC_READ:
115 ReadCharacteristicProcedure readProcedure = (ReadCharacteristicProcedure) procedure;
116 readProcedure.readFuture.completeExceptionally(new TimeoutException("Read characteristic "
117 + readProcedure.characteristic.getUuid() + " timeout for device " + address));
119 case CHARACTERISTIC_WRITE:
120 WriteCharacteristicProcedure writeProcedure = (WriteCharacteristicProcedure) procedure;
121 writeProcedure.writeFuture.completeExceptionally(new TimeoutException("Write characteristic "
122 + writeProcedure.characteristic.getUuid() + " timeout for device " + address));
128 currentProcedure = PROCEDURE_NONE;
133 * Creates a new {@link BlueGigaBluetoothDevice} which extends {@link BluetoothDevice} for the BlueGiga
136 * @param bgHandler the {@link BlueGigaBridgeHandler} that provides the link to the dongle
137 * @param address the {@link BluetoothAddress} for this device
138 * @param addressType the {@link BluetoothAddressType} of this device
140 public BlueGigaBluetoothDevice(BlueGigaBridgeHandler bgHandler, BluetoothAddress address,
141 BluetoothAddressType addressType) {
142 super(bgHandler, address);
144 logger.debug("Creating new BlueGiga device {}", address);
146 this.bgHandler = bgHandler;
147 this.addressType = addressType;
149 bgHandler.addEventListener(this);
150 updateLastSeenTime();
153 public void setAddressType(BluetoothAddressType addressType) {
154 this.addressType = addressType;
158 public boolean connect() {
159 if (connection != -1) {
160 // We're already connected
164 cancelTimer(connectTimer);
165 if (bgHandler.bgConnect(address, addressType)) {
166 connectionState = ConnectionState.CONNECTING;
167 connectTimer = startTimer(connectTimeoutTask, 10);
170 connectionState = ConnectionState.DISCONNECTED;
176 public boolean disconnect() {
177 if (connection == -1) {
178 // We're already disconnected
182 return bgHandler.bgDisconnect(connection);
186 public boolean discoverServices() {
187 if (!PROCEDURE_NONE.equals(currentProcedure)) {
191 cancelTimer(procedureTimer);
192 if (!bgHandler.bgFindPrimaryServices(connection)) {
196 procedureTimer = startTimer(procedureTimeoutTask, TIMEOUT_SEC);
197 currentProcedure = PROCEDURE_GET_SERVICES;
202 public CompletableFuture<@Nullable Void> enableNotifications(BluetoothCharacteristic characteristic) {
203 if (connection == -1) {
204 return CompletableFuture.failedFuture(new BluetoothException("Not connected"));
207 BlueGigaBluetoothCharacteristic ch = (BlueGigaBluetoothCharacteristic) characteristic;
208 if (ch.isNotifying()) {
209 return CompletableFuture.completedFuture(null);
212 BluetoothDescriptor descriptor = ch
213 .getDescriptor(BluetoothDescriptor.GattDescriptor.CLIENT_CHARACTERISTIC_CONFIGURATION.getUUID());
215 if (descriptor == null || descriptor.getHandle() == 0) {
216 return CompletableFuture.failedFuture(
217 new BluetoothException("Unable to find CCC for characteristic [" + characteristic.getUuid() + "]"));
220 if (!PROCEDURE_NONE.equals(currentProcedure)) {
221 return CompletableFuture.failedFuture(new BluetoothException("Another procedure is already in progress"));
224 int[] value = { 1, 0 };
226 cancelTimer(procedureTimer);
227 if (!bgHandler.bgWriteCharacteristic(connection, descriptor.getHandle(), value)) {
228 return CompletableFuture.failedFuture(new BluetoothException(
229 "Failed to write to CCC for characteristic [" + characteristic.getUuid() + "]"));
232 procedureTimer = startTimer(procedureTimeoutTask, TIMEOUT_SEC);
233 WriteCharacteristicProcedure notifyProcedure = new WriteCharacteristicProcedure(ch,
234 BlueGigaProcedure.Type.NOTIFICATION_ENABLE);
235 currentProcedure = notifyProcedure;
237 // we intentionally sleep here in order to give this procedure a chance to complete.
238 // ideally we would use locks/conditions to make this wait until completiong but
239 // I have a better solution planned for later. - Connor Petty
241 } catch (InterruptedException e) {
242 Thread.currentThread().interrupt();
244 return notifyProcedure.writeFuture;
248 public CompletableFuture<@Nullable Void> disableNotifications(BluetoothCharacteristic characteristic) {
249 if (connection == -1) {
250 return CompletableFuture.failedFuture(new BluetoothException("Not connected"));
253 BlueGigaBluetoothCharacteristic ch = (BlueGigaBluetoothCharacteristic) characteristic;
254 if (!ch.isNotifying()) {
255 return CompletableFuture.completedFuture(null);
258 BluetoothDescriptor descriptor = ch
259 .getDescriptor(BluetoothDescriptor.GattDescriptor.CLIENT_CHARACTERISTIC_CONFIGURATION.getUUID());
261 if (descriptor == null || descriptor.getHandle() == 0) {
262 return CompletableFuture.failedFuture(
263 new BluetoothException("Unable to find CCC for characteristic [" + characteristic.getUuid() + "]"));
266 if (!PROCEDURE_NONE.equals(currentProcedure)) {
267 return CompletableFuture.failedFuture(new BluetoothException("Another procedure is already in progress"));
270 int[] value = { 0, 0 };
272 cancelTimer(procedureTimer);
273 if (!bgHandler.bgWriteCharacteristic(connection, descriptor.getHandle(), value)) {
274 return CompletableFuture.failedFuture(new BluetoothException(
275 "Failed to write to CCC for characteristic [" + characteristic.getUuid() + "]"));
278 procedureTimer = startTimer(procedureTimeoutTask, TIMEOUT_SEC);
279 WriteCharacteristicProcedure notifyProcedure = new WriteCharacteristicProcedure(ch,
280 BlueGigaProcedure.Type.NOTIFICATION_DISABLE);
281 currentProcedure = notifyProcedure;
283 return notifyProcedure.writeFuture;
287 public boolean isNotifying(BluetoothCharacteristic characteristic) {
288 BlueGigaBluetoothCharacteristic ch = (BlueGigaBluetoothCharacteristic) characteristic;
289 return ch.isNotifying();
293 public boolean enableNotifications(BluetoothDescriptor descriptor) {
294 // TODO will be implemented in a followup PR
299 public boolean disableNotifications(BluetoothDescriptor descriptor) {
300 // TODO will be implemented in a followup PR
305 public CompletableFuture<byte[]> readCharacteristic(BluetoothCharacteristic characteristic) {
306 if (characteristic.getHandle() == 0) {
307 return CompletableFuture.failedFuture(new BluetoothException("Cannot read characteristic with no handle"));
309 if (connection == -1) {
310 return CompletableFuture.failedFuture(new BluetoothException("Not connected"));
313 if (!PROCEDURE_NONE.equals(currentProcedure)) {
314 return CompletableFuture.failedFuture(new BluetoothException("Another procedure is already in progress"));
317 cancelTimer(procedureTimer);
318 if (!bgHandler.bgReadCharacteristic(connection, characteristic.getHandle())) {
319 return CompletableFuture.failedFuture(
320 new BluetoothException("Failed to read characteristic [" + characteristic.getUuid() + "]"));
322 procedureTimer = startTimer(procedureTimeoutTask, TIMEOUT_SEC);
323 ReadCharacteristicProcedure readProcedure = new ReadCharacteristicProcedure(characteristic);
324 currentProcedure = readProcedure;
326 return readProcedure.readFuture;
330 public CompletableFuture<@Nullable Void> writeCharacteristic(BluetoothCharacteristic characteristic, byte[] value) {
331 if (characteristic.getHandle() == 0) {
332 return CompletableFuture.failedFuture(new BluetoothException("Cannot write characteristic with no handle"));
334 if (connection == -1) {
335 return CompletableFuture.failedFuture(new BluetoothException("Not connected"));
338 if (!PROCEDURE_NONE.equals(currentProcedure)) {
339 return CompletableFuture.failedFuture(new BluetoothException("Another procedure is already in progress"));
342 cancelTimer(procedureTimer);
343 if (!bgHandler.bgWriteCharacteristic(connection, characteristic.getHandle(),
344 BluetoothUtils.toIntArray(value))) {
345 return CompletableFuture.failedFuture(
346 new BluetoothException("Failed to write characteristic [" + characteristic.getUuid() + "]"));
349 procedureTimer = startTimer(procedureTimeoutTask, TIMEOUT_SEC);
350 WriteCharacteristicProcedure writeProcedure = new WriteCharacteristicProcedure(
351 (BlueGigaBluetoothCharacteristic) characteristic, BlueGigaProcedure.Type.CHARACTERISTIC_WRITE);
352 currentProcedure = writeProcedure;
354 return writeProcedure.writeFuture;
358 public void bluegigaEventReceived(BlueGigaResponse event) {
359 if (event instanceof BlueGigaScanResponseEvent responseEvent) {
360 handleScanEvent(responseEvent);
363 else if (event instanceof BlueGigaGroupFoundEvent foundEvent) {
364 handleGroupFoundEvent(foundEvent);
367 else if (event instanceof BlueGigaFindInformationFoundEvent foundEvent) {
368 // A Characteristic has been discovered
369 handleFindInformationFoundEvent(foundEvent);
372 else if (event instanceof BlueGigaProcedureCompletedEvent completedEvent) {
373 handleProcedureCompletedEvent(completedEvent);
376 else if (event instanceof BlueGigaConnectionStatusEvent statusEvent) {
377 handleConnectionStatusEvent(statusEvent);
380 else if (event instanceof BlueGigaDisconnectedEvent disconnectedEvent) {
381 handleDisconnectedEvent(disconnectedEvent);
384 else if (event instanceof BlueGigaAttributeValueEvent valueEvent) {
385 handleAttributeValueEvent(valueEvent);
389 private void handleScanEvent(BlueGigaScanResponseEvent event) {
390 // Check if this is addressed to this device
391 if (!address.equals(new BluetoothAddress(event.getSender()))) {
395 logger.trace("scanEvent: {}", event);
396 updateLastSeenTime();
398 // Set device properties
399 rssi = event.getRssi();
400 addressType = event.getAddressType();
402 byte[] manufacturerData = null;
404 // If the packet contains data, then process it and add anything relevant to the device...
405 if (event.getData().length > 0) {
406 EirPacket eir = new EirPacket(event.getData());
407 for (EirDataType record : eir.getRecords().keySet()) {
408 if (logger.isTraceEnabled()) {
409 logger.trace(" EirDataType: {}={}", record, eir.getRecord(record));
415 case EIR_MANUFACTURER_SPECIFIC:
416 obj = eir.getRecord(EirDataType.EIR_MANUFACTURER_SPECIFIC);
419 @SuppressWarnings("unchecked")
420 Map<Short, int[]> eirRecord = (Map<Short, int[]>) obj;
421 Map.Entry<Short, int[]> eirEntry = eirRecord.entrySet().iterator().next();
423 manufacturer = eirEntry.getKey().intValue();
425 int[] manufacturerInt = eirEntry.getValue();
426 manufacturerData = new byte[manufacturerInt.length + 2];
427 // Convert short Company ID to bytes and add it to manufacturerData
428 manufacturerData[0] = (byte) (manufacturer & 0xff);
429 manufacturerData[1] = (byte) ((manufacturer >> 8) & 0xff);
430 // Add Convert int custom data nd add it to manufacturerData
431 for (int i = 0; i < manufacturerInt.length; i++) {
432 manufacturerData[i + 2] = (byte) manufacturerInt[i];
434 } catch (ClassCastException e) {
435 logger.debug("Unsupported manufacturer specific record received from device {}",
442 name = (String) eir.getRecord(record);
444 case EIR_SLAVEINTERVALRANGE:
446 case EIR_SVC_DATA_UUID128:
448 case EIR_SVC_DATA_UUID16:
450 case EIR_SVC_DATA_UUID32:
452 case EIR_SVC_UUID128_INCOMPLETE:
453 case EIR_SVC_UUID16_COMPLETE:
454 case EIR_SVC_UUID16_INCOMPLETE:
455 case EIR_SVC_UUID32_COMPLETE:
456 case EIR_SVC_UUID32_INCOMPLETE:
457 case EIR_SVC_UUID128_COMPLETE:
458 // addServices((List<UUID>) eir.getRecord(record));
461 obj = eir.getRecord(EirDataType.EIR_TXPOWER);
472 if (connectionState == ConnectionState.DISCOVERING) {
473 // TODO: It could make sense to wait with discovery for non-connectable devices until scan response is
474 // received to eventually retrieve more about the device before it gets discovered. Anyhow, devices
475 // that don't send a scan response at all also have to be supported. See also PR #6995.
477 // Set our state to disconnected
478 connectionState = ConnectionState.DISCONNECTED;
481 // But notify listeners that the state is now DISCOVERED
482 notifyListeners(BluetoothEventType.CONNECTION_STATE,
483 new BluetoothConnectionStatusNotification(ConnectionState.DISCOVERED));
485 // Notify the bridge - for inbox notifications
486 bgHandler.deviceDiscovered(this);
489 // Notify listeners of all scan records - for RSSI, beacon processing (etc)
490 BluetoothScanNotification scanNotification = new BluetoothScanNotification();
491 scanNotification.setRssi(event.getRssi());
493 switch (event.getPacketType()) {
494 case CONNECTABLE_ADVERTISEMENT:
495 case DISCOVERABLE_ADVERTISEMENT:
496 case NON_CONNECTABLE_ADVERTISEMENT:
497 scanNotification.setBeaconType(BluetoothBeaconType.BEACON_ADVERTISEMENT);
500 scanNotification.setBeaconType(BluetoothBeaconType.BEACON_SCANRESPONSE);
506 if (manufacturerData != null) {
507 scanNotification.setManufacturerData(manufacturerData);
510 notifyListeners(BluetoothEventType.SCAN_RECORD, scanNotification);
513 private void handleGroupFoundEvent(BlueGigaGroupFoundEvent event) {
514 // If this is not our connection handle then ignore.
515 if (connection != event.getConnection()) {
519 logger.trace("BlueGiga Group: {} event={}", this, event);
520 updateLastSeenTime();
522 BluetoothService service = new BluetoothService(event.getUuid(), true, event.getStart(), event.getEnd());
526 private void handleFindInformationFoundEvent(BlueGigaFindInformationFoundEvent event) {
527 // If this is not our connection handle then ignore.
528 if (connection != event.getConnection()) {
532 logger.trace("BlueGiga FindInfo: {} event={}", this, event);
533 updateLastSeenTime();
535 int handle = event.getChrHandle();
536 UUID attUUID = event.getUuid();
538 BluetoothService service = getServiceByHandle(handle);
539 if (service == null) {
540 logger.debug("BlueGiga: Unable to find service for handle {}", handle);
543 handleToUUID.put(handle, attUUID);
545 if (BluetoothBindingConstants.ATTR_CHARACTERISTIC_DECLARATION.equals(attUUID)) {
546 BlueGigaBluetoothCharacteristic characteristic = new BlueGigaBluetoothCharacteristic(handle);
547 characteristic.setService(service);
548 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);
557 BlueGigaBluetoothCharacteristic characteristic = handleToCharacteristic.get(chrHandle);
558 if (characteristic != null) {
559 characteristic.addDescriptor(new BluetoothDescriptor(characteristic, attUUID, handle));
564 private void handleProcedureCompletedEvent(BlueGigaProcedureCompletedEvent event) {
565 // If this is not our connection handle then ignore.
566 if (connection != event.getConnection()) {
570 if (PROCEDURE_NONE.equals(currentProcedure)) {
571 logger.debug("BlueGiga procedure completed but procedure is null with connection {}, address {}",
572 connection, address);
576 cancelTimer(procedureTimer);
577 updateLastSeenTime();
579 // The current procedure is now complete - move on...
580 switch (currentProcedure.type) {
582 // We've downloaded all services, now get the characteristics
583 if (bgHandler.bgFindCharacteristics(connection)) {
584 procedureTimer = startTimer(procedureTimeoutTask, TIMEOUT_SEC);
585 currentProcedure = PROCEDURE_GET_CHARACTERISTICS;
587 currentProcedure = PROCEDURE_NONE;
590 case GET_CHARACTERISTICS:
591 // We've downloaded all attributes, now read the characteristic declarations
592 if (bgHandler.bgReadCharacteristicDeclarations(connection)) {
593 procedureTimer = startTimer(procedureTimeoutTask, TIMEOUT_SEC);
594 currentProcedure = PROCEDURE_READ_CHARACTERISTIC_DECL;
596 currentProcedure = PROCEDURE_NONE;
599 case READ_CHARACTERISTIC_DECL:
600 // We've downloaded read all the declarations, we are done now
601 currentProcedure = PROCEDURE_NONE;
602 notifyListeners(BluetoothEventType.SERVICES_DISCOVERED);
604 case CHARACTERISTIC_READ:
606 ReadCharacteristicProcedure readProcedure = (ReadCharacteristicProcedure) currentProcedure;
607 readProcedure.readFuture.completeExceptionally(new BluetoothException(
608 "Read characteristic failed: " + readProcedure.characteristic.getUuid()));
609 currentProcedure = PROCEDURE_NONE;
611 case CHARACTERISTIC_WRITE:
612 // The write completed - failure or success
613 WriteCharacteristicProcedure writeProcedure = (WriteCharacteristicProcedure) currentProcedure;
614 if (event.getResult() == BgApiResponse.SUCCESS) {
615 writeProcedure.writeFuture.complete(null);
617 writeProcedure.writeFuture.completeExceptionally(new BluetoothException(
618 "Write characteristic failed: " + writeProcedure.characteristic.getUuid()));
620 currentProcedure = PROCEDURE_NONE;
622 case NOTIFICATION_ENABLE:
623 WriteCharacteristicProcedure notifyEnableProcedure = (WriteCharacteristicProcedure) currentProcedure;
624 boolean success = event.getResult() == BgApiResponse.SUCCESS;
626 notifyEnableProcedure.writeFuture.complete(null);
628 notifyEnableProcedure.writeFuture
629 .completeExceptionally(new BluetoothException("Enable characteristic notification failed: "
630 + notifyEnableProcedure.characteristic.getUuid()));
632 notifyEnableProcedure.characteristic.setNotifying(success);
633 currentProcedure = PROCEDURE_NONE;
635 case NOTIFICATION_DISABLE:
636 WriteCharacteristicProcedure notifyDisableProcedure = (WriteCharacteristicProcedure) currentProcedure;
637 success = event.getResult() == BgApiResponse.SUCCESS;
639 notifyDisableProcedure.writeFuture.complete(null);
641 notifyDisableProcedure.writeFuture
642 .completeExceptionally(new BluetoothException("Disable characteristic notification failed: "
643 + notifyDisableProcedure.characteristic.getUuid()));
645 notifyDisableProcedure.characteristic.setNotifying(!success);
646 currentProcedure = PROCEDURE_NONE;
653 private void handleConnectionStatusEvent(BlueGigaConnectionStatusEvent event) {
654 // Check if this is addressed to this device
655 if (!address.equals(new BluetoothAddress(event.getAddress()))) {
659 cancelTimer(connectTimer);
660 updateLastSeenTime();
662 // If we're connected, then remember the connection handle
663 if (event.getFlags().contains(ConnectionStatusFlag.CONNECTION_CONNECTED)) {
664 connectionState = ConnectionState.CONNECTED;
665 connection = event.getConnection();
666 notifyListeners(BluetoothEventType.CONNECTION_STATE,
667 new BluetoothConnectionStatusNotification(connectionState));
671 private void handleDisconnectedEvent(BlueGigaDisconnectedEvent event) {
672 // If this is not our connection handle then ignore.
673 if (connection != event.getConnection()) {
677 for (BlueGigaBluetoothCharacteristic ch : handleToCharacteristic.values()) {
678 ch.setNotifying(false);
681 cancelTimer(procedureTimer);
682 connectionState = ConnectionState.DISCONNECTED;
685 BlueGigaProcedure procedure = currentProcedure;
686 switch (procedure.type) {
687 case CHARACTERISTIC_READ:
688 ReadCharacteristicProcedure readProcedure = (ReadCharacteristicProcedure) procedure;
689 readProcedure.readFuture.completeExceptionally(new BluetoothException("Read characteristic "
690 + readProcedure.characteristic.getUuid() + " failed due to disconnect of device " + address));
692 case CHARACTERISTIC_WRITE:
693 WriteCharacteristicProcedure writeProcedure = (WriteCharacteristicProcedure) procedure;
694 writeProcedure.writeFuture.completeExceptionally(new BluetoothException("Write characteristic "
695 + writeProcedure.characteristic.getUuid() + " failed due to disconnect of device " + address));
700 currentProcedure = PROCEDURE_NONE;
702 notifyListeners(BluetoothEventType.CONNECTION_STATE,
703 new BluetoothConnectionStatusNotification(connectionState));
706 private void handleAttributeValueEvent(BlueGigaAttributeValueEvent event) {
707 // If this is not our connection handle then ignore.
708 if (connection != event.getConnection()) {
712 updateLastSeenTime();
714 logger.trace("BlueGiga AttributeValue: {} event={}", this, event);
716 int handle = event.getAttHandle();
718 Map.Entry<Integer, BlueGigaBluetoothCharacteristic> entry = handleToCharacteristic.floorEntry(handle);
720 logger.debug("BlueGiga didn't find characteristic for event {}", event);
724 BlueGigaBluetoothCharacteristic characteristic = entry.getValue();
726 if (handle == entry.getKey()) {
727 // this is the declaration
728 if (parseDeclaration(characteristic, event.getValue())) {
729 BluetoothService service = getServiceByHandle(handle);
730 if (service == null) {
731 logger.debug("BlueGiga: Unable to find service for handle {}", handle);
734 service.addCharacteristic(characteristic);
738 if (handle == characteristic.getHandle()) {
739 byte[] value = BluetoothUtils.toByteArray(event.getValue());
740 BlueGigaProcedure procedure = currentProcedure;
741 // If this is the characteristic we were reading, then send a read completion
742 if (procedure.type == BlueGigaProcedure.Type.CHARACTERISTIC_READ) {
743 ReadCharacteristicProcedure readProcedure = (ReadCharacteristicProcedure) currentProcedure;
744 if (readProcedure.characteristic.getHandle() == event.getAttHandle()) {
745 readProcedure.readFuture.complete(value);
746 currentProcedure = PROCEDURE_NONE;
750 // Notify the user of the updated value
751 notifyListeners(BluetoothEventType.CHARACTERISTIC_UPDATED, characteristic, value);
753 // it must be one of the descriptors we need to update
754 UUID attUUID = handleToUUID.get(handle);
755 if (attUUID != null) {
756 BluetoothDescriptor descriptor = characteristic.getDescriptor(attUUID);
757 notifyListeners(BluetoothEventType.DESCRIPTOR_UPDATED, descriptor,
758 BluetoothUtils.toByteArray(event.getValue()));
763 private boolean parseDeclaration(BlueGigaBluetoothCharacteristic ch, int[] value) {
764 ByteBuffer buffer = ByteBuffer.wrap(BluetoothUtils.toByteArray(value));
765 buffer.order(ByteOrder.LITTLE_ENDIAN);
767 ch.setProperties(Byte.toUnsignedInt(buffer.get()));
768 ch.setHandle(Short.toUnsignedInt(buffer.getShort()));
770 switch (buffer.remaining()) {
772 long key = Short.toUnsignedLong(buffer.getShort());
773 ch.setUUID(BluetoothBindingConstants.createBluetoothUUID(key));
776 key = Integer.toUnsignedLong(buffer.getInt());
777 ch.setUUID(BluetoothBindingConstants.createBluetoothUUID(key));
780 long lower = buffer.getLong();
781 long upper = buffer.getLong();
782 ch.setUUID(new UUID(upper, lower));
785 logger.debug("Unexpected uuid length: {}", buffer.remaining());
791 * Clean up and release memory.
794 public void dispose() {
795 if (connectionState == ConnectionState.CONNECTED) {
798 cancelTimer(connectTimer);
799 cancelTimer(procedureTimer);
800 bgHandler.removeEventListener(this);
801 currentProcedure = PROCEDURE_NONE;
802 connectionState = ConnectionState.DISCOVERING;
806 private void cancelTimer(@Nullable ScheduledFuture<?> task) {
812 private ScheduledFuture<?> startTimer(Runnable command, long timeout) {
813 return scheduler.schedule(command, timeout, TimeUnit.SECONDS);
816 private static class BlueGigaProcedure {
817 private final Type type;
819 public BlueGigaProcedure(Type type) {
823 // An enum to use in the state machine for interacting with the device
828 READ_CHARACTERISTIC_DECL,
830 CHARACTERISTIC_WRITE,
836 private static class ReadCharacteristicProcedure extends BlueGigaProcedure {
838 private final BluetoothCharacteristic characteristic;
840 private final CompletableFuture<byte[]> readFuture = new CompletableFuture<>();
842 public ReadCharacteristicProcedure(BluetoothCharacteristic characteristic) {
843 super(Type.CHARACTERISTIC_READ);
844 this.characteristic = characteristic;
848 private static class WriteCharacteristicProcedure extends BlueGigaProcedure {
850 private final BlueGigaBluetoothCharacteristic characteristic;
852 private final CompletableFuture<@Nullable Void> writeFuture = new CompletableFuture<>();
854 public WriteCharacteristicProcedure(BlueGigaBluetoothCharacteristic characteristic, Type type) {
856 this.characteristic = characteristic;