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();
130 public void setAddressType(BluetoothAddressType addressType) {
131 this.addressType = addressType;
135 public boolean connect() {
136 if (connection != -1) {
137 // We're already connected
141 cancelTimer(connectTimer);
142 if (bgHandler.bgConnect(address, addressType)) {
143 connectionState = ConnectionState.CONNECTING;
144 connectTimer = startTimer(connectTimeoutTask, TIMEOUT_SEC);
147 connectionState = ConnectionState.DISCONNECTED;
153 public boolean disconnect() {
154 if (connection == -1) {
155 // We're already disconnected
159 return bgHandler.bgDisconnect(connection);
163 public boolean discoverServices() {
164 if (procedureProgress != BlueGigaProcedure.NONE) {
168 cancelTimer(procedureTimer);
169 if (!bgHandler.bgFindPrimaryServices(connection)) {
173 procedureTimer = startTimer(procedureTimeoutTask, TIMEOUT_SEC);
174 procedureProgress = BlueGigaProcedure.GET_SERVICES;
179 public boolean enableNotifications(BluetoothCharacteristic characteristic) {
180 // TODO will be implemented in a followup PR
185 public boolean disableNotifications(BluetoothCharacteristic characteristic) {
186 // TODO will be implemented in a followup PR
191 public boolean enableNotifications(BluetoothDescriptor descriptor) {
192 // TODO will be implemented in a followup PR
197 public boolean disableNotifications(BluetoothDescriptor descriptor) {
198 // TODO will be implemented in a followup PR
203 public boolean readCharacteristic(@Nullable BluetoothCharacteristic characteristic) {
204 if (characteristic == null || characteristic.getHandle() == 0) {
208 if (procedureProgress != BlueGigaProcedure.NONE) {
212 cancelTimer(procedureTimer);
213 if (!bgHandler.bgReadCharacteristic(connection, characteristic.getHandle())) {
216 procedureTimer = startTimer(procedureTimeoutTask, TIMEOUT_SEC);
217 procedureProgress = BlueGigaProcedure.CHARACTERISTIC_READ;
218 procedureCharacteristic = characteristic;
224 public boolean writeCharacteristic(@Nullable BluetoothCharacteristic characteristic) {
225 if (characteristic == null || characteristic.getHandle() == 0) {
229 if (procedureProgress != BlueGigaProcedure.NONE) {
233 cancelTimer(procedureTimer);
234 if (!bgHandler.bgWriteCharacteristic(connection, characteristic.getHandle(), characteristic.getValue())) {
238 procedureTimer = startTimer(procedureTimeoutTask, TIMEOUT_SEC);
239 procedureProgress = BlueGigaProcedure.CHARACTERISTIC_WRITE;
240 procedureCharacteristic = characteristic;
246 public void bluegigaEventReceived(BlueGigaResponse event) {
247 if (event instanceof BlueGigaScanResponseEvent) {
248 handleScanEvent((BlueGigaScanResponseEvent) event);
251 else if (event instanceof BlueGigaGroupFoundEvent) {
252 handleGroupFoundEvent((BlueGigaGroupFoundEvent) event);
255 else if (event instanceof BlueGigaFindInformationFoundEvent) {
256 // A Characteristic has been discovered
257 handleFindInformationFoundEvent((BlueGigaFindInformationFoundEvent) event);
260 else if (event instanceof BlueGigaProcedureCompletedEvent) {
261 handleProcedureCompletedEvent((BlueGigaProcedureCompletedEvent) event);
264 else if (event instanceof BlueGigaConnectionStatusEvent) {
265 handleConnectionStatusEvent((BlueGigaConnectionStatusEvent) event);
268 else if (event instanceof BlueGigaDisconnectedEvent) {
269 handleDisconnectedEvent((BlueGigaDisconnectedEvent) event);
272 else if (event instanceof BlueGigaAttributeValueEvent) {
273 handleAttributeValueEvent((BlueGigaAttributeValueEvent) event);
277 private void handleScanEvent(BlueGigaScanResponseEvent event) {
278 // Check if this is addressed to this device
279 if (!address.equals(new BluetoothAddress(event.getSender()))) {
283 logger.trace("scanEvent: {}", event);
284 updateLastSeenTime();
286 // Set device properties
287 rssi = event.getRssi();
288 addressType = event.getAddressType();
290 byte[] manufacturerData = null;
292 // If the packet contains data, then process it and add anything relevant to the device...
293 if (event.getData().length > 0) {
294 EirPacket eir = new EirPacket(event.getData());
295 for (EirDataType record : eir.getRecords().keySet()) {
296 if (logger.isTraceEnabled()) {
297 logger.trace(" EirDataType: {}={}", record, eir.getRecord(record));
303 case EIR_MANUFACTURER_SPECIFIC:
304 obj = eir.getRecord(EirDataType.EIR_MANUFACTURER_SPECIFIC);
307 @SuppressWarnings("unchecked")
308 Map<Short, int[]> eirRecord = (Map<Short, int[]>) obj;
309 Map.Entry<Short, int[]> eirEntry = eirRecord.entrySet().iterator().next();
311 manufacturer = eirEntry.getKey().intValue();
313 int[] manufacturerInt = eirEntry.getValue();
314 manufacturerData = new byte[manufacturerInt.length + 2];
315 // Convert short Company ID to bytes and add it to manufacturerData
316 manufacturerData[0] = (byte) (manufacturer & 0xff);
317 manufacturerData[1] = (byte) ((manufacturer >> 8) & 0xff);
318 // Add Convert int custom data nd add it to manufacturerData
319 for (int i = 0; i < manufacturerInt.length; i++) {
320 manufacturerData[i + 2] = (byte) manufacturerInt[i];
322 } catch (ClassCastException e) {
323 logger.debug("Unsupported manufacturer specific record received from device {}",
330 name = (String) eir.getRecord(record);
332 case EIR_SLAVEINTERVALRANGE:
334 case EIR_SVC_DATA_UUID128:
336 case EIR_SVC_DATA_UUID16:
338 case EIR_SVC_DATA_UUID32:
340 case EIR_SVC_UUID128_INCOMPLETE:
341 case EIR_SVC_UUID16_COMPLETE:
342 case EIR_SVC_UUID16_INCOMPLETE:
343 case EIR_SVC_UUID32_COMPLETE:
344 case EIR_SVC_UUID32_INCOMPLETE:
345 case EIR_SVC_UUID128_COMPLETE:
346 // addServices((List<UUID>) eir.getRecord(record));
349 obj = eir.getRecord(EirDataType.EIR_TXPOWER);
360 if (connectionState == ConnectionState.DISCOVERING) {
361 // TODO: It could make sense to wait with discovery for non-connectable devices until scan response is
362 // received to eventually retrieve more about the device before it gets discovered. Anyhow, devices
363 // that don't send a scan response at all also have to be supported. See also PR #6995.
365 // Set our state to disconnected
366 connectionState = ConnectionState.DISCONNECTED;
369 // But notify listeners that the state is now DISCOVERED
370 notifyListeners(BluetoothEventType.CONNECTION_STATE,
371 new BluetoothConnectionStatusNotification(ConnectionState.DISCOVERED));
373 // Notify the bridge - for inbox notifications
374 bgHandler.deviceDiscovered(this);
377 // Notify listeners of all scan records - for RSSI, beacon processing (etc)
378 BluetoothScanNotification scanNotification = new BluetoothScanNotification();
379 scanNotification.setRssi(event.getRssi());
381 switch (event.getPacketType()) {
382 case CONNECTABLE_ADVERTISEMENT:
383 case DISCOVERABLE_ADVERTISEMENT:
384 case NON_CONNECTABLE_ADVERTISEMENT:
385 scanNotification.setBeaconType(BluetoothBeaconType.BEACON_ADVERTISEMENT);
388 scanNotification.setBeaconType(BluetoothBeaconType.BEACON_SCANRESPONSE);
394 if (manufacturerData != null) {
395 scanNotification.setManufacturerData(manufacturerData);
398 notifyListeners(BluetoothEventType.SCAN_RECORD, scanNotification);
401 private void handleGroupFoundEvent(BlueGigaGroupFoundEvent event) {
402 // If this is not our connection handle then ignore.
403 if (connection != event.getConnection()) {
407 logger.trace("BlueGiga Group: {} svcs={}", this, supportedServices);
408 updateLastSeenTime();
410 BluetoothService service = new BluetoothService(event.getUuid(), true, event.getStart(), event.getEnd());
414 private void handleFindInformationFoundEvent(BlueGigaFindInformationFoundEvent event) {
415 // If this is not our connection handle then ignore.
416 if (connection != event.getConnection()) {
420 logger.trace("BlueGiga FindInfo: {} svcs={}", this, supportedServices);
421 updateLastSeenTime();
423 BluetoothCharacteristic characteristic = new BluetoothCharacteristic(event.getUuid(), event.getChrHandle());
425 BluetoothService service = getServiceByHandle(characteristic.getHandle());
426 if (service == null) {
427 logger.debug("BlueGiga: Unable to find service for handle {}", characteristic.getHandle());
430 characteristic.setService(service);
431 service.addCharacteristic(characteristic);
434 private void handleProcedureCompletedEvent(BlueGigaProcedureCompletedEvent event) {
435 // If this is not our connection handle then ignore.
436 if (connection != event.getConnection()) {
440 if (procedureProgress == BlueGigaProcedure.NONE) {
441 logger.debug("BlueGiga procedure completed but procedure is null with connection {}, address {}",
442 connection, address);
446 cancelTimer(procedureTimer);
447 updateLastSeenTime();
449 // The current procedure is now complete - move on...
450 switch (procedureProgress) {
452 // We've downloaded all services, now get the characteristics
453 if (bgHandler.bgFindCharacteristics(connection)) {
454 procedureTimer = startTimer(procedureTimeoutTask, TIMEOUT_SEC);
455 procedureProgress = BlueGigaProcedure.GET_CHARACTERISTICS;
457 procedureProgress = BlueGigaProcedure.NONE;
460 case GET_CHARACTERISTICS:
461 // We've downloaded all characteristics
462 procedureProgress = BlueGigaProcedure.NONE;
463 notifyListeners(BluetoothEventType.SERVICES_DISCOVERED);
465 case CHARACTERISTIC_READ:
467 notifyListeners(BluetoothEventType.CHARACTERISTIC_READ_COMPLETE, procedureCharacteristic,
468 BluetoothCompletionStatus.ERROR);
469 procedureProgress = BlueGigaProcedure.NONE;
470 procedureCharacteristic = null;
472 case CHARACTERISTIC_WRITE:
473 // The write completed - failure or success
474 BluetoothCompletionStatus result = event.getResult() == BgApiResponse.SUCCESS
475 ? BluetoothCompletionStatus.SUCCESS
476 : BluetoothCompletionStatus.ERROR;
477 notifyListeners(BluetoothEventType.CHARACTERISTIC_WRITE_COMPLETE, procedureCharacteristic, result);
478 procedureProgress = BlueGigaProcedure.NONE;
479 procedureCharacteristic = null;
486 private void handleConnectionStatusEvent(BlueGigaConnectionStatusEvent event) {
487 // Check if this is addressed to this device
488 if (!address.equals(new BluetoothAddress(event.getAddress()))) {
492 cancelTimer(connectTimer);
493 updateLastSeenTime();
495 // If we're connected, then remember the connection handle
496 if (event.getFlags().contains(ConnectionStatusFlag.CONNECTION_CONNECTED)) {
497 connectionState = ConnectionState.CONNECTED;
498 connection = event.getConnection();
499 notifyListeners(BluetoothEventType.CONNECTION_STATE,
500 new BluetoothConnectionStatusNotification(connectionState));
504 private void handleDisconnectedEvent(BlueGigaDisconnectedEvent event) {
505 // If this is not our connection handle then ignore.
506 if (connection != event.getConnection()) {
510 cancelTimer(procedureTimer);
511 connectionState = ConnectionState.DISCONNECTED;
513 procedureProgress = BlueGigaProcedure.NONE;
515 notifyListeners(BluetoothEventType.CONNECTION_STATE,
516 new BluetoothConnectionStatusNotification(connectionState));
519 private void handleAttributeValueEvent(BlueGigaAttributeValueEvent event) {
520 // If this is not our connection handle then ignore.
521 if (connection != event.getConnection()) {
525 updateLastSeenTime();
527 BluetoothCharacteristic characteristic = getCharacteristicByHandle(event.getAttHandle());
528 if (characteristic == null) {
529 logger.debug("BlueGiga didn't find characteristic for event {}", event);
531 characteristic.setValue(event.getValue().clone());
533 // If this is the characteristic we were reading, then send a read completion
534 if (procedureProgress == BlueGigaProcedure.CHARACTERISTIC_READ && procedureCharacteristic != null
535 && procedureCharacteristic.getHandle() == event.getAttHandle()) {
536 procedureProgress = BlueGigaProcedure.NONE;
537 procedureCharacteristic = null;
538 notifyListeners(BluetoothEventType.CHARACTERISTIC_READ_COMPLETE, characteristic,
539 BluetoothCompletionStatus.SUCCESS);
542 // Notify the user of the updated value
543 notifyListeners(BluetoothEventType.CHARACTERISTIC_UPDATED, characteristic);
548 * Clean up and release memory.
551 public void dispose() {
552 if (connectionState == ConnectionState.CONNECTED) {
555 cancelTimer(connectTimer);
556 cancelTimer(procedureTimer);
557 bgHandler.removeEventListener(this);
558 procedureProgress = BlueGigaProcedure.NONE;
559 connectionState = ConnectionState.DISCOVERING;
563 private void cancelTimer(@Nullable ScheduledFuture<?> task) {
569 private ScheduledFuture<?> startTimer(Runnable command, long timeout) {
570 return scheduler.schedule(command, timeout, TimeUnit.SECONDS);