2 * Copyright (c) 2010-2020 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;
16 import java.util.concurrent.ScheduledExecutorService;
17 import java.util.concurrent.ScheduledFuture;
18 import java.util.concurrent.TimeUnit;
20 import org.eclipse.jdt.annotation.NonNullByDefault;
21 import org.eclipse.jdt.annotation.Nullable;
22 import org.openhab.binding.bluetooth.BaseBluetoothDevice;
23 import org.openhab.binding.bluetooth.BluetoothAddress;
24 import org.openhab.binding.bluetooth.BluetoothCharacteristic;
25 import org.openhab.binding.bluetooth.BluetoothCompletionStatus;
26 import org.openhab.binding.bluetooth.BluetoothDescriptor;
27 import org.openhab.binding.bluetooth.BluetoothDevice;
28 import org.openhab.binding.bluetooth.BluetoothService;
29 import org.openhab.binding.bluetooth.bluegiga.handler.BlueGigaBridgeHandler;
30 import org.openhab.binding.bluetooth.bluegiga.internal.BlueGigaEventListener;
31 import org.openhab.binding.bluetooth.bluegiga.internal.BlueGigaResponse;
32 import org.openhab.binding.bluetooth.bluegiga.internal.command.attributeclient.BlueGigaAttributeValueEvent;
33 import org.openhab.binding.bluetooth.bluegiga.internal.command.attributeclient.BlueGigaFindInformationFoundEvent;
34 import org.openhab.binding.bluetooth.bluegiga.internal.command.attributeclient.BlueGigaGroupFoundEvent;
35 import org.openhab.binding.bluetooth.bluegiga.internal.command.attributeclient.BlueGigaProcedureCompletedEvent;
36 import org.openhab.binding.bluetooth.bluegiga.internal.command.connection.BlueGigaConnectionStatusEvent;
37 import org.openhab.binding.bluetooth.bluegiga.internal.command.connection.BlueGigaDisconnectedEvent;
38 import org.openhab.binding.bluetooth.bluegiga.internal.command.gap.BlueGigaScanResponseEvent;
39 import org.openhab.binding.bluetooth.bluegiga.internal.eir.EirDataType;
40 import org.openhab.binding.bluetooth.bluegiga.internal.eir.EirPacket;
41 import org.openhab.binding.bluetooth.bluegiga.internal.enumeration.BgApiResponse;
42 import org.openhab.binding.bluetooth.bluegiga.internal.enumeration.BluetoothAddressType;
43 import org.openhab.binding.bluetooth.bluegiga.internal.enumeration.ConnectionStatusFlag;
44 import org.openhab.binding.bluetooth.notification.BluetoothConnectionStatusNotification;
45 import org.openhab.binding.bluetooth.notification.BluetoothScanNotification;
46 import org.openhab.binding.bluetooth.notification.BluetoothScanNotification.BluetoothBeaconType;
47 import org.openhab.core.common.ThreadPoolManager;
48 import org.slf4j.Logger;
49 import org.slf4j.LoggerFactory;
52 * An extended {@link BluetoothDevice} class to handle BlueGiga specific information
54 * @author Chris Jackson - Initial contribution
57 public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements BlueGigaEventListener {
58 private final long TIMEOUT_SEC = 60;
60 private final Logger logger = LoggerFactory.getLogger(BlueGigaBluetoothDevice.class);
62 // BlueGiga needs to know the address type when connecting
63 private BluetoothAddressType addressType = BluetoothAddressType.UNKNOWN;
66 private final BlueGigaBridgeHandler bgHandler;
68 // An enum to use in the state machine for interacting with the device
69 private enum BlueGigaProcedure {
77 private BlueGigaProcedure procedureProgress = BlueGigaProcedure.NONE;
79 // Somewhere to remember what characteristic we're working on
80 private @Nullable BluetoothCharacteristic procedureCharacteristic;
82 // The connection handle if the device is connected
83 private int connection = -1;
85 private final ScheduledExecutorService scheduler = ThreadPoolManager.getScheduledPool("bluetooth");
87 private @Nullable ScheduledFuture<?> connectTimer;
88 private @Nullable ScheduledFuture<?> procedureTimer;
90 private Runnable connectTimeoutTask = new Runnable() {
93 if (connectionState == ConnectionState.CONNECTING) {
94 logger.debug("Connection timeout for device {}", address);
95 connectionState = ConnectionState.DISCONNECTED;
100 private Runnable procedureTimeoutTask = new Runnable() {
103 logger.debug("Procedure {} timeout for device {}", procedureProgress, address);
104 procedureProgress = BlueGigaProcedure.NONE;
105 procedureCharacteristic = null;
110 * Creates a new {@link BlueGigaBluetoothDevice} which extends {@link BluetoothDevice} for the BlueGiga
113 * @param bgHandler the {@link BlueGigaBridgeHandler} that provides the link to the dongle
114 * @param address the {@link BluetoothAddress} for this device
115 * @param addressType the {@link BluetoothAddressType} of this device
117 public BlueGigaBluetoothDevice(BlueGigaBridgeHandler bgHandler, BluetoothAddress address,
118 BluetoothAddressType addressType) {
119 super(bgHandler, address);
121 logger.debug("Creating new BlueGiga device {}", address);
123 this.bgHandler = bgHandler;
124 this.addressType = addressType;
126 bgHandler.addEventListener(this);
127 updateLastSeenTime();
131 public boolean connect() {
132 if (connection != -1) {
133 // We're already connected
137 cancelTimer(connectTimer);
138 if (bgHandler.bgConnect(address, addressType)) {
139 connectionState = ConnectionState.CONNECTING;
140 connectTimer = startTimer(connectTimeoutTask, TIMEOUT_SEC);
143 connectionState = ConnectionState.DISCONNECTED;
149 public boolean disconnect() {
150 if (connection == -1) {
151 // We're already disconnected
155 return bgHandler.bgDisconnect(connection);
159 public boolean discoverServices() {
160 if (procedureProgress != BlueGigaProcedure.NONE) {
164 cancelTimer(procedureTimer);
165 if (!bgHandler.bgFindPrimaryServices(connection)) {
169 procedureTimer = startTimer(procedureTimeoutTask, TIMEOUT_SEC);
170 procedureProgress = BlueGigaProcedure.GET_SERVICES;
175 public boolean enableNotifications(BluetoothCharacteristic characteristic) {
176 // TODO will be implemented in a followup PR
181 public boolean disableNotifications(BluetoothCharacteristic characteristic) {
182 // TODO will be implemented in a followup PR
187 public boolean enableNotifications(BluetoothDescriptor descriptor) {
188 // TODO will be implemented in a followup PR
193 public boolean disableNotifications(BluetoothDescriptor descriptor) {
194 // TODO will be implemented in a followup PR
199 public boolean readCharacteristic(@Nullable BluetoothCharacteristic characteristic) {
200 if (characteristic == null || characteristic.getHandle() == 0) {
204 if (procedureProgress != BlueGigaProcedure.NONE) {
208 cancelTimer(procedureTimer);
209 if (!bgHandler.bgReadCharacteristic(connection, characteristic.getHandle())) {
212 procedureTimer = startTimer(procedureTimeoutTask, TIMEOUT_SEC);
213 procedureProgress = BlueGigaProcedure.CHARACTERISTIC_READ;
214 procedureCharacteristic = characteristic;
220 public boolean writeCharacteristic(@Nullable BluetoothCharacteristic characteristic) {
221 if (characteristic == null || characteristic.getHandle() == 0) {
225 if (procedureProgress != BlueGigaProcedure.NONE) {
229 cancelTimer(procedureTimer);
230 if (!bgHandler.bgWriteCharacteristic(connection, characteristic.getHandle(), characteristic.getValue())) {
234 procedureTimer = startTimer(procedureTimeoutTask, TIMEOUT_SEC);
235 procedureProgress = BlueGigaProcedure.CHARACTERISTIC_WRITE;
236 procedureCharacteristic = characteristic;
242 public void bluegigaEventReceived(BlueGigaResponse event) {
243 if (event instanceof BlueGigaScanResponseEvent) {
244 handleScanEvent((BlueGigaScanResponseEvent) event);
247 else if (event instanceof BlueGigaGroupFoundEvent) {
248 handleGroupFoundEvent((BlueGigaGroupFoundEvent) event);
251 else if (event instanceof BlueGigaFindInformationFoundEvent) {
252 // A Characteristic has been discovered
253 handleFindInformationFoundEvent((BlueGigaFindInformationFoundEvent) event);
256 else if (event instanceof BlueGigaProcedureCompletedEvent) {
257 handleProcedureCompletedEvent((BlueGigaProcedureCompletedEvent) event);
260 else if (event instanceof BlueGigaConnectionStatusEvent) {
261 handleConnectionStatusEvent((BlueGigaConnectionStatusEvent) event);
264 else if (event instanceof BlueGigaDisconnectedEvent) {
265 handleDisconnectedEvent((BlueGigaDisconnectedEvent) event);
268 else if (event instanceof BlueGigaAttributeValueEvent) {
269 handleAttributeValueEvent((BlueGigaAttributeValueEvent) event);
273 private void handleScanEvent(BlueGigaScanResponseEvent event) {
274 // Check if this is addressed to this device
275 if (!address.equals(new BluetoothAddress(event.getSender()))) {
279 logger.trace("scanEvent: {}", event);
280 updateLastSeenTime();
282 // Set device properties
283 rssi = event.getRssi();
284 addressType = event.getAddressType();
286 byte[] manufacturerData = null;
288 // If the packet contains data, then process it and add anything relevant to the device...
289 if (event.getData().length > 0) {
290 EirPacket eir = new EirPacket(event.getData());
291 for (EirDataType record : eir.getRecords().keySet()) {
292 if (logger.isTraceEnabled()) {
293 logger.trace(" EirDataType: {}={}", record, eir.getRecord(record));
299 case EIR_MANUFACTURER_SPECIFIC:
300 obj = eir.getRecord(EirDataType.EIR_MANUFACTURER_SPECIFIC);
303 @SuppressWarnings("unchecked")
304 Map<Short, int[]> eirRecord = (Map<Short, int[]>) obj;
305 Map.Entry<Short, int[]> eirEntry = eirRecord.entrySet().iterator().next();
307 manufacturer = eirEntry.getKey().intValue();
309 int[] manufacturerInt = eirEntry.getValue();
310 manufacturerData = new byte[manufacturerInt.length + 2];
311 // Convert short Company ID to bytes and add it to manufacturerData
312 manufacturerData[0] = (byte) (manufacturer & 0xff);
313 manufacturerData[1] = (byte) ((manufacturer >> 8) & 0xff);
314 // Add Convert int custom data nd add it to manufacturerData
315 for (int i = 0; i < manufacturerInt.length; i++) {
316 manufacturerData[i + 2] = (byte) manufacturerInt[i];
318 } catch (ClassCastException e) {
319 logger.debug("Unsupported manufacturer specific record received from device {}",
326 name = (String) eir.getRecord(record);
328 case EIR_SLAVEINTERVALRANGE:
330 case EIR_SVC_DATA_UUID128:
332 case EIR_SVC_DATA_UUID16:
334 case EIR_SVC_DATA_UUID32:
336 case EIR_SVC_UUID128_INCOMPLETE:
337 case EIR_SVC_UUID16_COMPLETE:
338 case EIR_SVC_UUID16_INCOMPLETE:
339 case EIR_SVC_UUID32_COMPLETE:
340 case EIR_SVC_UUID32_INCOMPLETE:
341 case EIR_SVC_UUID128_COMPLETE:
342 // addServices((List<UUID>) eir.getRecord(record));
345 obj = eir.getRecord(EirDataType.EIR_TXPOWER);
356 if (connectionState == ConnectionState.DISCOVERING) {
357 // TODO: It could make sense to wait with discovery for non-connectable devices until scan response is
358 // received to eventually retrieve more about the device before it gets discovered. Anyhow, devices
359 // that don't send a scan response at all also have to be supported. See also PR #6995.
361 // Set our state to disconnected
362 connectionState = ConnectionState.DISCONNECTED;
365 // But notify listeners that the state is now DISCOVERED
366 notifyListeners(BluetoothEventType.CONNECTION_STATE,
367 new BluetoothConnectionStatusNotification(ConnectionState.DISCOVERED));
369 // Notify the bridge - for inbox notifications
370 bgHandler.deviceDiscovered(this);
373 // Notify listeners of all scan records - for RSSI, beacon processing (etc)
374 BluetoothScanNotification scanNotification = new BluetoothScanNotification();
375 scanNotification.setRssi(event.getRssi());
377 switch (event.getPacketType()) {
378 case CONNECTABLE_ADVERTISEMENT:
379 case DISCOVERABLE_ADVERTISEMENT:
380 case NON_CONNECTABLE_ADVERTISEMENT:
381 scanNotification.setBeaconType(BluetoothBeaconType.BEACON_ADVERTISEMENT);
384 scanNotification.setBeaconType(BluetoothBeaconType.BEACON_SCANRESPONSE);
390 if (manufacturerData != null) {
391 scanNotification.setManufacturerData(manufacturerData);
394 notifyListeners(BluetoothEventType.SCAN_RECORD, scanNotification);
397 private void handleGroupFoundEvent(BlueGigaGroupFoundEvent event) {
398 // If this is not our connection handle then ignore.
399 if (connection != event.getConnection()) {
403 logger.trace("BlueGiga Group: {} svcs={}", this, supportedServices);
404 updateLastSeenTime();
406 BluetoothService service = new BluetoothService(event.getUuid(), true, event.getStart(), event.getEnd());
410 private void handleFindInformationFoundEvent(BlueGigaFindInformationFoundEvent event) {
411 // If this is not our connection handle then ignore.
412 if (connection != event.getConnection()) {
416 logger.trace("BlueGiga FindInfo: {} svcs={}", this, supportedServices);
417 updateLastSeenTime();
419 BluetoothCharacteristic characteristic = new BluetoothCharacteristic(event.getUuid(), event.getChrHandle());
421 BluetoothService service = getServiceByHandle(characteristic.getHandle());
422 if (service == null) {
423 logger.debug("BlueGiga: Unable to find service for handle {}", characteristic.getHandle());
426 characteristic.setService(service);
427 service.addCharacteristic(characteristic);
430 private void handleProcedureCompletedEvent(BlueGigaProcedureCompletedEvent event) {
431 // If this is not our connection handle then ignore.
432 if (connection != event.getConnection()) {
436 if (procedureProgress == BlueGigaProcedure.NONE) {
437 logger.debug("BlueGiga procedure completed but procedure is null with connection {}, address {}",
438 connection, address);
442 cancelTimer(procedureTimer);
443 updateLastSeenTime();
445 // The current procedure is now complete - move on...
446 switch (procedureProgress) {
448 // We've downloaded all services, now get the characteristics
449 if (bgHandler.bgFindCharacteristics(connection)) {
450 procedureTimer = startTimer(procedureTimeoutTask, TIMEOUT_SEC);
451 procedureProgress = BlueGigaProcedure.GET_CHARACTERISTICS;
453 procedureProgress = BlueGigaProcedure.NONE;
456 case GET_CHARACTERISTICS:
457 // We've downloaded all characteristics
458 procedureProgress = BlueGigaProcedure.NONE;
459 notifyListeners(BluetoothEventType.SERVICES_DISCOVERED);
461 case CHARACTERISTIC_READ:
463 notifyListeners(BluetoothEventType.CHARACTERISTIC_READ_COMPLETE, procedureCharacteristic,
464 BluetoothCompletionStatus.ERROR);
465 procedureProgress = BlueGigaProcedure.NONE;
466 procedureCharacteristic = null;
468 case CHARACTERISTIC_WRITE:
469 // The write completed - failure or success
470 BluetoothCompletionStatus result = event.getResult() == BgApiResponse.SUCCESS
471 ? BluetoothCompletionStatus.SUCCESS
472 : BluetoothCompletionStatus.ERROR;
473 notifyListeners(BluetoothEventType.CHARACTERISTIC_WRITE_COMPLETE, procedureCharacteristic, result);
474 procedureProgress = BlueGigaProcedure.NONE;
475 procedureCharacteristic = null;
482 private void handleConnectionStatusEvent(BlueGigaConnectionStatusEvent event) {
483 // Check if this is addressed to this device
484 if (!address.equals(new BluetoothAddress(event.getAddress()))) {
488 cancelTimer(connectTimer);
489 updateLastSeenTime();
491 // If we're connected, then remember the connection handle
492 if (event.getFlags().contains(ConnectionStatusFlag.CONNECTION_CONNECTED)) {
493 connectionState = ConnectionState.CONNECTED;
494 connection = event.getConnection();
495 notifyListeners(BluetoothEventType.CONNECTION_STATE,
496 new BluetoothConnectionStatusNotification(connectionState));
500 private void handleDisconnectedEvent(BlueGigaDisconnectedEvent event) {
501 // If this is not our connection handle then ignore.
502 if (connection != event.getConnection()) {
506 cancelTimer(procedureTimer);
507 connectionState = ConnectionState.DISCONNECTED;
509 procedureProgress = BlueGigaProcedure.NONE;
511 notifyListeners(BluetoothEventType.CONNECTION_STATE,
512 new BluetoothConnectionStatusNotification(connectionState));
515 private void handleAttributeValueEvent(BlueGigaAttributeValueEvent event) {
516 // If this is not our connection handle then ignore.
517 if (connection != event.getConnection()) {
521 updateLastSeenTime();
523 BluetoothCharacteristic characteristic = getCharacteristicByHandle(event.getAttHandle());
524 if (characteristic == null) {
525 logger.debug("BlueGiga didn't find characteristic for event {}", event);
527 characteristic.setValue(event.getValue().clone());
529 // If this is the characteristic we were reading, then send a read completion
530 if (procedureProgress == BlueGigaProcedure.CHARACTERISTIC_READ && procedureCharacteristic != null
531 && procedureCharacteristic.getHandle() == event.getAttHandle()) {
532 procedureProgress = BlueGigaProcedure.NONE;
533 procedureCharacteristic = null;
534 notifyListeners(BluetoothEventType.CHARACTERISTIC_READ_COMPLETE, characteristic,
535 BluetoothCompletionStatus.SUCCESS);
538 // Notify the user of the updated value
539 notifyListeners(BluetoothEventType.CHARACTERISTIC_UPDATED, characteristic);
544 * Clean up and release memory.
547 public void dispose() {
548 if (connectionState == ConnectionState.CONNECTED) {
551 cancelTimer(connectTimer);
552 cancelTimer(procedureTimer);
553 bgHandler.removeEventListener(this);
554 procedureProgress = BlueGigaProcedure.NONE;
555 connectionState = ConnectionState.DISCOVERING;
559 private void cancelTimer(@Nullable ScheduledFuture<?> task) {
565 private ScheduledFuture<?> startTimer(Runnable command, long timeout) {
566 return scheduler.schedule(command, timeout, TimeUnit.SECONDS);