[bluetooth] Changed characteristic read/write to use CompletableFutures (#8970)
Signed-off-by: Connor Petty <mistercpp2000+gitsignoff@gmail.com>
This commit is contained in:
@@ -19,6 +19,10 @@ import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.Condition;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
@@ -80,6 +84,12 @@ public abstract class BaseBluetoothDevice extends BluetoothDevice {
|
||||
*/
|
||||
private final Set<BluetoothDeviceListener> eventListeners = new CopyOnWriteArraySet<>();
|
||||
|
||||
private final Lock deviceLock = new ReentrantLock();
|
||||
private final Condition connectionCondition = deviceLock.newCondition();
|
||||
private final Condition serviceDiscoveryCondition = deviceLock.newCondition();
|
||||
|
||||
private volatile boolean servicesDiscovered = false;
|
||||
|
||||
/**
|
||||
* Construct a Bluetooth device taking the Bluetooth address
|
||||
*
|
||||
@@ -258,6 +268,45 @@ public abstract class BaseBluetoothDevice extends BluetoothDevice {
|
||||
protected void dispose() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isServicesDiscovered() {
|
||||
return servicesDiscovered;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean awaitConnection(long timeout, TimeUnit unit) throws InterruptedException {
|
||||
deviceLock.lock();
|
||||
try {
|
||||
long nanosTimeout = unit.toNanos(timeout);
|
||||
while (getConnectionState() != ConnectionState.CONNECTED) {
|
||||
if (nanosTimeout <= 0L) {
|
||||
return false;
|
||||
}
|
||||
nanosTimeout = connectionCondition.awaitNanos(nanosTimeout);
|
||||
}
|
||||
} finally {
|
||||
deviceLock.unlock();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean awaitServiceDiscovery(long timeout, TimeUnit unit) throws InterruptedException {
|
||||
deviceLock.lock();
|
||||
try {
|
||||
long nanosTimeout = unit.toNanos(timeout);
|
||||
while (!servicesDiscovered) {
|
||||
if (nanosTimeout <= 0L) {
|
||||
return false;
|
||||
}
|
||||
nanosTimeout = serviceDiscoveryCondition.awaitNanos(nanosTimeout);
|
||||
}
|
||||
} finally {
|
||||
deviceLock.unlock();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void notifyListeners(BluetoothEventType event, Object... args) {
|
||||
switch (event) {
|
||||
@@ -270,6 +319,27 @@ public abstract class BaseBluetoothDevice extends BluetoothDevice {
|
||||
default:
|
||||
break;
|
||||
}
|
||||
switch (event) {
|
||||
case SERVICES_DISCOVERED:
|
||||
deviceLock.lock();
|
||||
try {
|
||||
servicesDiscovered = true;
|
||||
serviceDiscoveryCondition.signal();
|
||||
} finally {
|
||||
deviceLock.unlock();
|
||||
}
|
||||
break;
|
||||
case CONNECTION_STATE:
|
||||
deviceLock.lock();
|
||||
try {
|
||||
connectionCondition.signal();
|
||||
} finally {
|
||||
deviceLock.unlock();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
super.notifyListeners(event, args);
|
||||
}
|
||||
|
||||
|
||||
@@ -214,7 +214,7 @@ public class BeaconBluetoothHandler extends BaseThingHandler implements Bluetoot
|
||||
}
|
||||
}
|
||||
|
||||
private void onActivity() {
|
||||
protected void onActivity() {
|
||||
this.lastActivityTime = ZonedDateTime.now();
|
||||
}
|
||||
|
||||
@@ -241,27 +241,12 @@ public class BeaconBluetoothHandler extends BaseThingHandler implements Bluetoot
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCharacteristicReadComplete(BluetoothCharacteristic characteristic, BluetoothCompletionStatus status) {
|
||||
if (status == BluetoothCompletionStatus.SUCCESS) {
|
||||
onActivity();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCharacteristicWriteComplete(BluetoothCharacteristic characteristic,
|
||||
BluetoothCompletionStatus status) {
|
||||
if (status == BluetoothCompletionStatus.SUCCESS) {
|
||||
onActivity();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCharacteristicUpdate(BluetoothCharacteristic characteristic) {
|
||||
public void onCharacteristicUpdate(BluetoothCharacteristic characteristic, byte[] value) {
|
||||
onActivity();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDescriptorUpdate(BluetoothDescriptor bluetoothDescriptor) {
|
||||
public void onDescriptorUpdate(BluetoothDescriptor bluetoothDescriptor, byte[] value) {
|
||||
onActivity();
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,8 @@ public class BluetoothBindingConstants {
|
||||
|
||||
public static final String CONFIGURATION_ADDRESS = "address";
|
||||
public static final String CONFIGURATION_DISCOVERY = "backgroundDiscovery";
|
||||
public static final String CONFIGURATION_ALWAYS_CONNECTED = "alwaysConnected";
|
||||
public static final String CONFIGURATION_IDLE_DISCONNECT_DELAY = "idleDisconnectDelay";
|
||||
|
||||
public static final long BLUETOOTH_BASE_UUID = 0x800000805f9b34fbL;
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
*/
|
||||
package org.openhab.binding.bluetooth;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
@@ -34,15 +33,6 @@ import org.slf4j.LoggerFactory;
|
||||
* @author Peter Rosenberg - Improve properties support
|
||||
*/
|
||||
public class BluetoothCharacteristic {
|
||||
public static final int FORMAT_UINT8 = 0x11;
|
||||
public static final int FORMAT_UINT16 = 0x12;
|
||||
public static final int FORMAT_UINT32 = 0x14;
|
||||
public static final int FORMAT_SINT8 = 0x21;
|
||||
public static final int FORMAT_SINT16 = 0x22;
|
||||
public static final int FORMAT_SINT32 = 0x24;
|
||||
public static final int FORMAT_SFLOAT = 0x32;
|
||||
public static final int FORMAT_FLOAT = 0x34;
|
||||
|
||||
public static final int PROPERTY_BROADCAST = 0x01;
|
||||
public static final int PROPERTY_READ = 0x02;
|
||||
public static final int PROPERTY_WRITE_NO_RESPONSE = 0x04;
|
||||
@@ -86,11 +76,6 @@ public class BluetoothCharacteristic {
|
||||
protected int permissions;
|
||||
protected int writeType;
|
||||
|
||||
/**
|
||||
* The raw data value for this characteristic
|
||||
*/
|
||||
protected int[] value = new int[0];
|
||||
|
||||
/**
|
||||
* The {@link BluetoothService} to which this characteristic belongs
|
||||
*/
|
||||
@@ -314,299 +299,6 @@ public class BluetoothCharacteristic {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the stored value for this characteristic.
|
||||
*
|
||||
*/
|
||||
public int[] getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the stored value for this characteristic.
|
||||
*
|
||||
*/
|
||||
public byte[] getByteValue() {
|
||||
byte[] byteValue = new byte[value.length];
|
||||
for (int cnt = 0; cnt < value.length; cnt++) {
|
||||
byteValue[cnt] = (byte) (value[cnt] & 0xFF);
|
||||
}
|
||||
return byteValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the stored value of this characteristic.
|
||||
*
|
||||
*/
|
||||
public Integer getIntValue(int formatType, int offset) {
|
||||
if ((offset + getTypeLen(formatType)) > value.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (formatType) {
|
||||
case FORMAT_UINT8:
|
||||
return unsignedByteToInt(value[offset]);
|
||||
|
||||
case FORMAT_UINT16:
|
||||
return unsignedBytesToInt(value[offset], value[offset + 1]);
|
||||
|
||||
case FORMAT_UINT32:
|
||||
return unsignedBytesToInt(value[offset], value[offset + 1], value[offset + 2], value[offset + 3]);
|
||||
|
||||
case FORMAT_SINT8:
|
||||
return unsignedToSigned(unsignedByteToInt(value[offset]), 8);
|
||||
|
||||
case FORMAT_SINT16:
|
||||
return unsignedToSigned(unsignedBytesToInt(value[offset], value[offset + 1]), 16);
|
||||
|
||||
case FORMAT_SINT32:
|
||||
return unsignedToSigned(
|
||||
unsignedBytesToInt(value[offset], value[offset + 1], value[offset + 2], value[offset + 3]), 32);
|
||||
default:
|
||||
logger.error("Unknown format type {} - no int value can be provided for it.", formatType);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the stored value of this characteristic. This doesn't read the remote data.
|
||||
*
|
||||
*/
|
||||
public Float getFloatValue(int formatType, int offset) {
|
||||
if ((offset + getTypeLen(formatType)) > value.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (formatType) {
|
||||
case FORMAT_SFLOAT:
|
||||
return bytesToFloat(value[offset], value[offset + 1]);
|
||||
case FORMAT_FLOAT:
|
||||
return bytesToFloat(value[offset], value[offset + 1], value[offset + 2], value[offset + 3]);
|
||||
default:
|
||||
logger.error("Unknown format type {} - no float value can be provided for it.", formatType);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the stored value of this characteristic. This doesn't read the remote data.
|
||||
*
|
||||
*/
|
||||
public String getStringValue(int offset) {
|
||||
if (value == null || offset > value.length) {
|
||||
return null;
|
||||
}
|
||||
byte[] strBytes = new byte[value.length - offset];
|
||||
for (int i = 0; i < (value.length - offset); ++i) {
|
||||
strBytes[i] = (byte) value[offset + i];
|
||||
}
|
||||
return new String(strBytes, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the locally stored value of this characteristic.
|
||||
*
|
||||
* @param value the value to set
|
||||
* @return true, if it has been set successfully
|
||||
*/
|
||||
public boolean setValue(int[] value) {
|
||||
this.value = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the local value of this characteristic.
|
||||
*
|
||||
* @param value the value to set
|
||||
* @param formatType the format of the value (as one of the FORMAT_* constants in this class)
|
||||
* @param offset the offset to use when interpreting the value
|
||||
* @return true, if it has been set successfully
|
||||
*/
|
||||
public boolean setValue(int value, int formatType, int offset) {
|
||||
int len = offset + getTypeLen(formatType);
|
||||
if (this.value == null) {
|
||||
this.value = new int[len];
|
||||
}
|
||||
if (len > this.value.length) {
|
||||
return false;
|
||||
}
|
||||
int val = value;
|
||||
switch (formatType) {
|
||||
case FORMAT_SINT8:
|
||||
val = intToSignedBits(value, 8);
|
||||
// Fall-through intended
|
||||
case FORMAT_UINT8:
|
||||
this.value[offset] = (byte) (val & 0xFF);
|
||||
break;
|
||||
|
||||
case FORMAT_SINT16:
|
||||
val = intToSignedBits(value, 16);
|
||||
// Fall-through intended
|
||||
case FORMAT_UINT16:
|
||||
this.value[offset] = (byte) (val & 0xFF);
|
||||
this.value[offset + 1] = (byte) ((val >> 8) & 0xFF);
|
||||
break;
|
||||
|
||||
case FORMAT_SINT32:
|
||||
val = intToSignedBits(value, 32);
|
||||
// Fall-through intended
|
||||
case FORMAT_UINT32:
|
||||
this.value[offset] = (byte) (val & 0xFF);
|
||||
this.value[offset + 1] = (byte) ((val >> 8) & 0xFF);
|
||||
this.value[offset + 2] = (byte) ((val >> 16) & 0xFF);
|
||||
this.value[offset + 2] = (byte) ((val >> 24) & 0xFF);
|
||||
break;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the local value of this characteristic.
|
||||
*
|
||||
* @param mantissa the mantissa of the value
|
||||
* @param exponent the exponent of the value
|
||||
* @param formatType the format of the value (as one of the FORMAT_* constants in this class)
|
||||
* @param offset the offset to use when interpreting the value
|
||||
* @return true, if it has been set successfully
|
||||
*
|
||||
*/
|
||||
public boolean setValue(int mantissa, int exponent, int formatType, int offset) {
|
||||
int len = offset + getTypeLen(formatType);
|
||||
if (value == null) {
|
||||
value = new int[len];
|
||||
}
|
||||
if (len > value.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (formatType) {
|
||||
case FORMAT_SFLOAT:
|
||||
int m = intToSignedBits(mantissa, 12);
|
||||
int exp = intToSignedBits(exponent, 4);
|
||||
value[offset] = (byte) (m & 0xFF);
|
||||
value[offset + 1] = (byte) ((m >> 8) & 0x0F);
|
||||
value[offset + 1] += (byte) ((exp & 0x0F) << 4);
|
||||
break;
|
||||
|
||||
case FORMAT_FLOAT:
|
||||
m = intToSignedBits(mantissa, 24);
|
||||
exp = intToSignedBits(exponent, 8);
|
||||
value[offset] = (byte) (m & 0xFF);
|
||||
value[offset + 1] = (byte) ((m >> 8) & 0xFF);
|
||||
value[offset + 2] = (byte) ((m >> 16) & 0xFF);
|
||||
value[offset + 2] += (byte) (exp & 0xFF);
|
||||
break;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the local value of this characteristic.
|
||||
*
|
||||
* @param value the value to set
|
||||
* @return true, if it has been set successfully
|
||||
*/
|
||||
public boolean setValue(byte[] value) {
|
||||
this.value = new int[value.length];
|
||||
int cnt = 0;
|
||||
for (byte val : value) {
|
||||
this.value[cnt++] = val;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the local value of this characteristic.
|
||||
*
|
||||
* @param value the value to set
|
||||
* @return true, if it has been set successfully
|
||||
*/
|
||||
public boolean setValue(String value) {
|
||||
this.value = new int[value.getBytes().length];
|
||||
int cnt = 0;
|
||||
for (byte val : value.getBytes()) {
|
||||
this.value[cnt++] = val;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the size of the requested value type.
|
||||
*/
|
||||
private int getTypeLen(int formatType) {
|
||||
return formatType & 0xF;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a signed byte to an unsigned int.
|
||||
*/
|
||||
private int unsignedByteToInt(int value) {
|
||||
return value & 0xFF;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert signed bytes to a 16-bit unsigned int.
|
||||
*/
|
||||
private int unsignedBytesToInt(int value1, int value2) {
|
||||
return value1 + (value2 << 8);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert signed bytes to a 32-bit unsigned int.
|
||||
*/
|
||||
private int unsignedBytesToInt(int value1, int value2, int value3, int value4) {
|
||||
return value1 + (value2 << 8) + (value3 << 16) + (value4 << 24);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert signed bytes to a 16-bit short float value.
|
||||
*/
|
||||
private float bytesToFloat(int value1, int value2) {
|
||||
int mantissa = unsignedToSigned(unsignedByteToInt(value1) + ((unsignedByteToInt(value2) & 0x0F) << 8), 12);
|
||||
int exponent = unsignedToSigned(unsignedByteToInt(value2) >> 4, 4);
|
||||
return (float) (mantissa * Math.pow(10, exponent));
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert signed bytes to a 32-bit short float value.
|
||||
*/
|
||||
private float bytesToFloat(int value1, int value2, int value3, int value4) {
|
||||
int mantissa = unsignedToSigned(
|
||||
unsignedByteToInt(value1) + (unsignedByteToInt(value2) << 8) + (unsignedByteToInt(value3) << 16), 24);
|
||||
return (float) (mantissa * Math.pow(10, value4));
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an unsigned integer to a two's-complement signed value.
|
||||
*/
|
||||
private int unsignedToSigned(int unsigned, int size) {
|
||||
if ((unsigned & (1 << size - 1)) != 0) {
|
||||
return -1 * ((1 << size - 1) - (unsigned & ((1 << size - 1) - 1)));
|
||||
} else {
|
||||
return unsigned;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an integer into the signed bits of the specified length.
|
||||
*/
|
||||
private int intToSignedBits(int i, int size) {
|
||||
if (i < 0) {
|
||||
return (1 << size - 1) + (i & ((1 << size - 1) - 1));
|
||||
} else {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
public GattCharacteristic getGattCharacteristic() {
|
||||
return GattCharacteristic.getCharacteristic(uuid);
|
||||
}
|
||||
|
||||
@@ -30,8 +30,8 @@ public class BluetoothDescriptor {
|
||||
|
||||
protected final BluetoothCharacteristic characteristic;
|
||||
protected final UUID uuid;
|
||||
|
||||
protected final int handle;
|
||||
protected byte[] value;
|
||||
|
||||
/**
|
||||
* The main constructor
|
||||
@@ -81,24 +81,6 @@ public class BluetoothDescriptor {
|
||||
return handle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the stored value for this descriptor. It doesn't read remote data.
|
||||
*
|
||||
* @return the value of the descriptor
|
||||
*/
|
||||
public byte[] getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the stored value for this descriptor. It doesn't update remote data.
|
||||
*
|
||||
* @param value the value for this descriptor instance
|
||||
*/
|
||||
public void setValue(byte[] value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public GattDescriptor getDescriptor() {
|
||||
return GattDescriptor.getDescriptor(uuid);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ package org.openhab.binding.bluetooth;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
@@ -69,8 +71,6 @@ public abstract class BluetoothDevice {
|
||||
protected enum BluetoothEventType {
|
||||
CONNECTION_STATE,
|
||||
SCAN_RECORD,
|
||||
CHARACTERISTIC_READ_COMPLETE,
|
||||
CHARACTERISTIC_WRITE_COMPLETE,
|
||||
CHARACTERISTIC_UPDATED,
|
||||
DESCRIPTOR_UPDATED,
|
||||
SERVICES_DISCOVERED,
|
||||
@@ -227,28 +227,25 @@ public abstract class BluetoothDevice {
|
||||
* Reads a characteristic. Only a single read or write operation can be requested at once. Attempting to perform an
|
||||
* operation when one is already in progress will result in subsequent calls returning false.
|
||||
* <p>
|
||||
* This is an asynchronous method. Once the read is complete
|
||||
* {@link BluetoothDeviceListener.onCharacteristicReadComplete}
|
||||
* method will be called with the completion state.
|
||||
* <p>
|
||||
* Note that {@link BluetoothDeviceListener.onCharacteristicUpdate} will be called when the read value is received.
|
||||
* This is an asynchronous method. Once the read is complete the returned future will be updated with the result.
|
||||
*
|
||||
* @param characteristic the {@link BluetoothCharacteristic} to read.
|
||||
* @return true if the characteristic read is started successfully
|
||||
* @return a future that returns the read data is successful, otherwise throws an exception
|
||||
*/
|
||||
public abstract boolean readCharacteristic(BluetoothCharacteristic characteristic);
|
||||
public abstract CompletableFuture<byte[]> readCharacteristic(BluetoothCharacteristic characteristic);
|
||||
|
||||
/**
|
||||
* Writes a characteristic. Only a single read or write operation can be requested at once. Attempting to perform an
|
||||
* operation when one is already in progress will result in subsequent calls returning false.
|
||||
* <p>
|
||||
* This is an asynchronous method. Once the write is complete
|
||||
* {@link BluetoothDeviceListener.onCharacteristicWriteComplete} method will be called with the completion state.
|
||||
* This is an asynchronous method. Once the write is complete the returned future will be updated with the result.
|
||||
*
|
||||
* @param characteristic the {@link BluetoothCharacteristic} to read.
|
||||
* @return true if the characteristic write is started successfully
|
||||
* @param characteristic the {@link BluetoothCharacteristic} to write.
|
||||
* @param value the data to write
|
||||
* @return a future that returns null upon a successful write, otherwise throws an exception
|
||||
*/
|
||||
public abstract boolean writeCharacteristic(BluetoothCharacteristic characteristic);
|
||||
public abstract CompletableFuture<@Nullable Void> writeCharacteristic(BluetoothCharacteristic characteristic,
|
||||
byte[] value);
|
||||
|
||||
/**
|
||||
* Returns if notification is enabled for the given characteristic.
|
||||
@@ -269,7 +266,7 @@ public abstract class BluetoothDevice {
|
||||
* @param characteristic the {@link BluetoothCharacteristic} to receive notifications for.
|
||||
* @return true if the characteristic notification is started successfully
|
||||
*/
|
||||
public abstract boolean enableNotifications(BluetoothCharacteristic characteristic);
|
||||
public abstract CompletableFuture<@Nullable Void> enableNotifications(BluetoothCharacteristic characteristic);
|
||||
|
||||
/**
|
||||
* Disables notifications for a characteristic. Only a single read or write operation can be requested at once.
|
||||
@@ -279,7 +276,7 @@ public abstract class BluetoothDevice {
|
||||
* @param characteristic the {@link BluetoothCharacteristic} to disable notifications for.
|
||||
* @return true if the characteristic notification is stopped successfully
|
||||
*/
|
||||
public abstract boolean disableNotifications(BluetoothCharacteristic characteristic);
|
||||
public abstract CompletableFuture<@Nullable Void> disableNotifications(BluetoothCharacteristic characteristic);
|
||||
|
||||
/**
|
||||
* Enables notifications for a descriptor. Only a single read or write operation can be requested at once.
|
||||
@@ -376,6 +373,12 @@ public abstract class BluetoothDevice {
|
||||
|
||||
protected abstract Collection<BluetoothDeviceListener> getListeners();
|
||||
|
||||
public abstract boolean awaitConnection(long timeout, TimeUnit unit) throws InterruptedException;
|
||||
|
||||
public abstract boolean awaitServiceDiscovery(long timeout, TimeUnit unit) throws InterruptedException;
|
||||
|
||||
public abstract boolean isServicesDiscovered();
|
||||
|
||||
/**
|
||||
* Notify the listeners of an event
|
||||
*
|
||||
@@ -395,19 +398,11 @@ public abstract class BluetoothDevice {
|
||||
case SERVICES_DISCOVERED:
|
||||
listener.onServicesDiscovered();
|
||||
break;
|
||||
case CHARACTERISTIC_READ_COMPLETE:
|
||||
listener.onCharacteristicReadComplete((BluetoothCharacteristic) args[0],
|
||||
(BluetoothCompletionStatus) args[1]);
|
||||
break;
|
||||
case CHARACTERISTIC_WRITE_COMPLETE:
|
||||
listener.onCharacteristicWriteComplete((BluetoothCharacteristic) args[0],
|
||||
(BluetoothCompletionStatus) args[1]);
|
||||
break;
|
||||
case CHARACTERISTIC_UPDATED:
|
||||
listener.onCharacteristicUpdate((BluetoothCharacteristic) args[0]);
|
||||
listener.onCharacteristicUpdate((BluetoothCharacteristic) args[0], (byte[]) args[1]);
|
||||
break;
|
||||
case DESCRIPTOR_UPDATED:
|
||||
listener.onDescriptorUpdate((BluetoothDescriptor) args[0]);
|
||||
listener.onDescriptorUpdate((BluetoothDescriptor) args[0], (byte[]) args[1]);
|
||||
break;
|
||||
case ADAPTER_CHANGED:
|
||||
listener.onAdapterChanged((BluetoothAdapter) args[0]);
|
||||
|
||||
@@ -46,37 +46,23 @@ public interface BluetoothDeviceListener {
|
||||
*/
|
||||
void onServicesDiscovered();
|
||||
|
||||
/**
|
||||
* Called when a read request completes
|
||||
*
|
||||
* @param characteristic the {@link BluetoothCharacteristic} that has completed the read request
|
||||
* @param status the {@link BluetoothCompletionStatus} of the read request
|
||||
*/
|
||||
void onCharacteristicReadComplete(BluetoothCharacteristic characteristic, BluetoothCompletionStatus status);
|
||||
|
||||
/**
|
||||
* Called when a write request completes
|
||||
*
|
||||
* @param characteristic the {@link BluetoothCharacteristic} that has completed the write request
|
||||
* @param status the {@link BluetoothCompletionStatus} of the write request
|
||||
*/
|
||||
void onCharacteristicWriteComplete(BluetoothCharacteristic characteristic, BluetoothCompletionStatus status);
|
||||
|
||||
/**
|
||||
* Called when a characteristic value is received. Implementations should call this whenever a value
|
||||
* is received from the BLE device even if there is no change to the value.
|
||||
*
|
||||
* @param characteristic the updated {@link BluetoothCharacteristic}
|
||||
* @param value the update value
|
||||
*/
|
||||
void onCharacteristicUpdate(BluetoothCharacteristic characteristic);
|
||||
void onCharacteristicUpdate(BluetoothCharacteristic characteristic, byte[] value);
|
||||
|
||||
/**
|
||||
* Called when a descriptor value is received. Implementations should call this whenever a value
|
||||
* is received from the BLE device even if there is no change to the value.
|
||||
*
|
||||
* @param characteristic the updated {@link BluetoothCharacteristic}
|
||||
* @param bluetoothDescriptor the updated {@link BluetoothDescriptor}
|
||||
* @param value the update value
|
||||
*/
|
||||
void onDescriptorUpdate(BluetoothDescriptor bluetoothDescriptor);
|
||||
void onDescriptorUpdate(BluetoothDescriptor bluetoothDescriptor, byte[] value);
|
||||
|
||||
/**
|
||||
* Called when the BluetoothAdapter for this BluetoothDevice changes.
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.bluetooth;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* This class encompasses exceptions that occur within the bluetooth api. This can be subclassed for more specific
|
||||
* exceptions in api implementations.
|
||||
*
|
||||
* @author Connor Petty - Initial contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class BluetoothException extends Exception {
|
||||
|
||||
private static final long serialVersionUID = -2557298438595050148L;
|
||||
|
||||
public BluetoothException() {
|
||||
super();
|
||||
}
|
||||
|
||||
public BluetoothException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
|
||||
super(message, cause, enableSuppression, writableStackTrace);
|
||||
}
|
||||
|
||||
public BluetoothException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public BluetoothException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public BluetoothException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.bluetooth;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* This is a utility class for parsing or formatting bluetooth characteristic data.
|
||||
*
|
||||
* @author Connor Petty - Initial Contribution
|
||||
*
|
||||
*/
|
||||
public class BluetoothUtils {
|
||||
|
||||
public static final Logger logger = LoggerFactory.getLogger(BluetoothUtils.class);
|
||||
|
||||
public static final int FORMAT_UINT8 = 0x11;
|
||||
public static final int FORMAT_UINT16 = 0x12;
|
||||
public static final int FORMAT_UINT32 = 0x14;
|
||||
public static final int FORMAT_SINT8 = 0x21;
|
||||
public static final int FORMAT_SINT16 = 0x22;
|
||||
public static final int FORMAT_SINT32 = 0x24;
|
||||
public static final int FORMAT_SFLOAT = 0x32;
|
||||
public static final int FORMAT_FLOAT = 0x34;
|
||||
|
||||
/**
|
||||
* Converts a byte array to an int array
|
||||
*
|
||||
* @param value
|
||||
* @return
|
||||
*/
|
||||
public static int[] toIntArray(byte[] value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
int[] ret = new int[value.length];
|
||||
for (int i = 0; i < value.length; i++) {
|
||||
ret[i] = value[i];
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
public static byte[] toByteArray(int[] value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
byte[] ret = new byte[value.length];
|
||||
for (int i = 0; i < value.length; i++) {
|
||||
ret[i] = (byte) (value[i] & 0xFF);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the stored value of this characteristic.
|
||||
*
|
||||
*/
|
||||
public static Integer getIntegerValue(byte[] value, int formatType, int offset) {
|
||||
if ((offset + getTypeLen(formatType)) > value.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (formatType) {
|
||||
case FORMAT_UINT8:
|
||||
return unsignedByteToInt(value[offset]);
|
||||
|
||||
case FORMAT_UINT16:
|
||||
return unsignedBytesToInt(value[offset], value[offset + 1]);
|
||||
|
||||
case FORMAT_UINT32:
|
||||
return unsignedBytesToInt(value[offset], value[offset + 1], value[offset + 2], value[offset + 3]);
|
||||
|
||||
case FORMAT_SINT8:
|
||||
return unsignedToSigned(unsignedByteToInt(value[offset]), 8);
|
||||
|
||||
case FORMAT_SINT16:
|
||||
return unsignedToSigned(unsignedBytesToInt(value[offset], value[offset + 1]), 16);
|
||||
|
||||
case FORMAT_SINT32:
|
||||
return unsignedToSigned(
|
||||
unsignedBytesToInt(value[offset], value[offset + 1], value[offset + 2], value[offset + 3]), 32);
|
||||
default:
|
||||
logger.error("Unknown format type {} - no int value can be provided for it.", formatType);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the stored value of this characteristic. This doesn't read the remote data.
|
||||
*
|
||||
*/
|
||||
public static Float getFloatValue(byte[] value, int formatType, int offset) {
|
||||
if ((offset + getTypeLen(formatType)) > value.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (formatType) {
|
||||
case FORMAT_SFLOAT:
|
||||
return bytesToFloat(value[offset], value[offset + 1]);
|
||||
case FORMAT_FLOAT:
|
||||
return bytesToFloat(value[offset], value[offset + 1], value[offset + 2], value[offset + 3]);
|
||||
default:
|
||||
logger.error("Unknown format type {} - no float value can be provided for it.", formatType);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the stored value of this characteristic. This doesn't read the remote data.
|
||||
*
|
||||
*/
|
||||
public static String getStringValue(byte[] value, int offset) {
|
||||
if (value == null || offset > value.length) {
|
||||
return null;
|
||||
}
|
||||
byte[] strBytes = new byte[value.length - offset];
|
||||
for (int i = 0; i < (value.length - offset); ++i) {
|
||||
strBytes[i] = value[offset + i];
|
||||
}
|
||||
return new String(strBytes, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the local value of this characteristic.
|
||||
*
|
||||
* @param value the value to set
|
||||
* @param formatType the format of the value (as one of the FORMAT_* constants in this class)
|
||||
* @param offset the offset to use when interpreting the value
|
||||
* @return true, if it has been set successfully
|
||||
*/
|
||||
public static boolean setValue(byte[] dest, int value, int formatType, int offset) {
|
||||
int len = offset + getTypeLen(formatType);
|
||||
if (dest == null || len > dest.length) {
|
||||
return false;
|
||||
}
|
||||
int val = value;
|
||||
switch (formatType) {
|
||||
case FORMAT_SINT8:
|
||||
val = intToSignedBits(value, 8);
|
||||
// Fall-through intended
|
||||
case FORMAT_UINT8:
|
||||
dest[offset] = (byte) (val & 0xFF);
|
||||
break;
|
||||
|
||||
case FORMAT_SINT16:
|
||||
val = intToSignedBits(value, 16);
|
||||
// Fall-through intended
|
||||
case FORMAT_UINT16:
|
||||
dest[offset] = (byte) (val & 0xFF);
|
||||
dest[offset + 1] = (byte) ((val >> 8) & 0xFF);
|
||||
break;
|
||||
|
||||
case FORMAT_SINT32:
|
||||
val = intToSignedBits(value, 32);
|
||||
// Fall-through intended
|
||||
case FORMAT_UINT32:
|
||||
dest[offset] = (byte) (val & 0xFF);
|
||||
dest[offset + 1] = (byte) ((val >> 8) & 0xFF);
|
||||
dest[offset + 2] = (byte) ((val >> 16) & 0xFF);
|
||||
dest[offset + 2] = (byte) ((val >> 24) & 0xFF);
|
||||
break;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the local value of this characteristic.
|
||||
*
|
||||
* @param mantissa the mantissa of the value
|
||||
* @param exponent the exponent of the value
|
||||
* @param formatType the format of the value (as one of the FORMAT_* constants in this class)
|
||||
* @param offset the offset to use when interpreting the value
|
||||
* @return true, if it has been set successfully
|
||||
*
|
||||
*/
|
||||
public static boolean setValue(byte[] dest, int mantissa, int exponent, int formatType, int offset) {
|
||||
int len = offset + getTypeLen(formatType);
|
||||
if (dest == null || len > dest.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (formatType) {
|
||||
case FORMAT_SFLOAT:
|
||||
int m = intToSignedBits(mantissa, 12);
|
||||
int exp = intToSignedBits(exponent, 4);
|
||||
dest[offset] = (byte) (m & 0xFF);
|
||||
dest[offset + 1] = (byte) ((m >> 8) & 0x0F);
|
||||
dest[offset + 1] += (byte) ((exp & 0x0F) << 4);
|
||||
break;
|
||||
|
||||
case FORMAT_FLOAT:
|
||||
m = intToSignedBits(mantissa, 24);
|
||||
exp = intToSignedBits(exponent, 8);
|
||||
dest[offset] = (byte) (m & 0xFF);
|
||||
dest[offset + 1] = (byte) ((m >> 8) & 0xFF);
|
||||
dest[offset + 2] = (byte) ((m >> 16) & 0xFF);
|
||||
dest[offset + 2] += (byte) (exp & 0xFF);
|
||||
break;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the size of the requested value type.
|
||||
*/
|
||||
private static int getTypeLen(int formatType) {
|
||||
return formatType & 0xF;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a signed byte to an unsigned int.
|
||||
*/
|
||||
private static int unsignedByteToInt(int value) {
|
||||
return value & 0xFF;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert signed bytes to a 16-bit unsigned int.
|
||||
*/
|
||||
private static int unsignedBytesToInt(int value1, int value2) {
|
||||
return value1 + (value2 << 8);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert signed bytes to a 32-bit unsigned int.
|
||||
*/
|
||||
private static int unsignedBytesToInt(int value1, int value2, int value3, int value4) {
|
||||
return value1 + (value2 << 8) + (value3 << 16) + (value4 << 24);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert signed bytes to a 16-bit short float value.
|
||||
*/
|
||||
private static float bytesToFloat(int value1, int value2) {
|
||||
int mantissa = unsignedToSigned(unsignedByteToInt(value1) + ((unsignedByteToInt(value2) & 0x0F) << 8), 12);
|
||||
int exponent = unsignedToSigned(unsignedByteToInt(value2) >> 4, 4);
|
||||
return (float) (mantissa * Math.pow(10, exponent));
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert signed bytes to a 32-bit short float value.
|
||||
*/
|
||||
private static float bytesToFloat(int value1, int value2, int value3, int value4) {
|
||||
int mantissa = unsignedToSigned(
|
||||
unsignedByteToInt(value1) + (unsignedByteToInt(value2) << 8) + (unsignedByteToInt(value3) << 16), 24);
|
||||
return (float) (mantissa * Math.pow(10, value4));
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an unsigned integer to a two's-complement signed value.
|
||||
*/
|
||||
private static int unsignedToSigned(int unsigned, int size) {
|
||||
if ((unsigned & (1 << size - 1)) != 0) {
|
||||
return -1 * ((1 << size - 1) - (unsigned & ((1 << size - 1) - 1)));
|
||||
} else {
|
||||
return unsigned;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an integer into the signed bits of the specified length.
|
||||
*/
|
||||
private static int intToSignedBits(int i, int size) {
|
||||
if (i < 0) {
|
||||
return (1 << size - 1) + (i & ((1 << size - 1) - 1));
|
||||
} else {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,21 +12,26 @@
|
||||
*/
|
||||
package org.openhab.binding.bluetooth;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.eclipse.jdt.annotation.DefaultLocation;
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.bluetooth.BluetoothDevice.ConnectionState;
|
||||
import org.openhab.binding.bluetooth.notification.BluetoothConnectionStatusNotification;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.binding.bluetooth.util.RetryFuture;
|
||||
import org.openhab.core.common.NamedThreadFactory;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.openhab.core.util.HexUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -36,17 +41,18 @@ import org.slf4j.LoggerFactory;
|
||||
*
|
||||
* @author Kai Kreuzer - Initial contribution and API
|
||||
*/
|
||||
@NonNullByDefault({ DefaultLocation.PARAMETER, DefaultLocation.RETURN_TYPE, DefaultLocation.ARRAY_CONTENTS,
|
||||
DefaultLocation.TYPE_ARGUMENT, DefaultLocation.TYPE_BOUND, DefaultLocation.TYPE_PARAMETER })
|
||||
@NonNullByDefault
|
||||
public class ConnectedBluetoothHandler extends BeaconBluetoothHandler {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(ConnectedBluetoothHandler.class);
|
||||
private ScheduledFuture<?> connectionJob;
|
||||
private @Nullable Future<?> reconnectJob;
|
||||
private @Nullable Future<?> pendingDisconnect;
|
||||
|
||||
// internal flag for the service resolution status
|
||||
protected volatile boolean resolved = false;
|
||||
private boolean alwaysConnected;
|
||||
private int idleDisconnectDelay = 1000;
|
||||
|
||||
protected final Set<BluetoothCharacteristic> deviceCharacteristics = new CopyOnWriteArraySet<>();
|
||||
// we initially set the to scheduler so that we can keep this field non-null
|
||||
private ScheduledExecutorService connectionTaskExecutor = scheduler;
|
||||
|
||||
public ConnectedBluetoothHandler(Thing thing) {
|
||||
super(thing);
|
||||
@@ -54,55 +60,198 @@ public class ConnectedBluetoothHandler extends BeaconBluetoothHandler {
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
|
||||
// super.initialize adds callbacks that might require the connectionTaskExecutor to be present, so we initialize
|
||||
// the connectionTaskExecutor first
|
||||
ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1,
|
||||
new NamedThreadFactory("bluetooth-connection" + thing.getThingTypeUID(), true));
|
||||
executor.setExecuteExistingDelayedTasksAfterShutdownPolicy(false);
|
||||
executor.setRemoveOnCancelPolicy(true);
|
||||
connectionTaskExecutor = executor;
|
||||
|
||||
super.initialize();
|
||||
|
||||
connectionJob = scheduler.scheduleWithFixedDelay(() -> {
|
||||
try {
|
||||
if (device.getConnectionState() != ConnectionState.CONNECTED) {
|
||||
device.connect();
|
||||
// we do not set the Thing status here, because we will anyhow receive a call to
|
||||
// onConnectionStateChange
|
||||
} else {
|
||||
// just in case it was already connected to begin with
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
if (!resolved && !device.discoverServices()) {
|
||||
logger.debug("Error while discovering services");
|
||||
if (thing.getStatus() == ThingStatus.OFFLINE) {
|
||||
// something went wrong in super.initialize() so we shouldn't initialize further here either
|
||||
return;
|
||||
}
|
||||
|
||||
Object alwaysConnectRaw = getConfig().get(BluetoothBindingConstants.CONFIGURATION_ALWAYS_CONNECTED);
|
||||
alwaysConnected = !Boolean.FALSE.equals(alwaysConnectRaw);
|
||||
|
||||
Object idleDisconnectDelayRaw = getConfig().get(BluetoothBindingConstants.CONFIGURATION_IDLE_DISCONNECT_DELAY);
|
||||
idleDisconnectDelay = 1000;
|
||||
if (idleDisconnectDelayRaw instanceof Number) {
|
||||
idleDisconnectDelay = ((Number) idleDisconnectDelayRaw).intValue();
|
||||
}
|
||||
|
||||
if (alwaysConnected) {
|
||||
reconnectJob = connectionTaskExecutor.scheduleWithFixedDelay(() -> {
|
||||
try {
|
||||
if (device.getConnectionState() != ConnectionState.CONNECTED) {
|
||||
if (!device.connect()) {
|
||||
logger.debug("Failed to connect to {}", address);
|
||||
}
|
||||
// we do not set the Thing status here, because we will anyhow receive a call to
|
||||
// onConnectionStateChange
|
||||
} else {
|
||||
// just in case it was already connected to begin with
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
if (!device.isServicesDiscovered() && !device.discoverServices()) {
|
||||
logger.debug("Error while discovering services");
|
||||
}
|
||||
}
|
||||
} catch (RuntimeException ex) {
|
||||
logger.warn("Unexpected error occurred", ex);
|
||||
}
|
||||
} catch (RuntimeException ex) {
|
||||
logger.warn("Unexpected error occurred", ex);
|
||||
}
|
||||
}, 0, 30, TimeUnit.SECONDS);
|
||||
}, 0, 30, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
if (connectionJob != null) {
|
||||
connectionJob.cancel(true);
|
||||
connectionJob = null;
|
||||
}
|
||||
cancel(reconnectJob, true);
|
||||
reconnectJob = null;
|
||||
cancel(pendingDisconnect, true);
|
||||
pendingDisconnect = null;
|
||||
|
||||
super.dispose();
|
||||
|
||||
// just in case something goes really wrong in the core and it tries to dispose a handler before initializing it
|
||||
if (scheduler != connectionTaskExecutor) {
|
||||
connectionTaskExecutor.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
super.handleCommand(channelUID, command);
|
||||
private static void cancel(@Nullable Future<?> future, boolean interrupt) {
|
||||
if (future != null) {
|
||||
future.cancel(interrupt);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle REFRESH
|
||||
if (command == RefreshType.REFRESH) {
|
||||
for (BluetoothCharacteristic characteristic : deviceCharacteristics) {
|
||||
if (characteristic.getGattCharacteristic() != null
|
||||
&& channelUID.getId().equals(characteristic.getGattCharacteristic().name())) {
|
||||
device.readCharacteristic(characteristic);
|
||||
break;
|
||||
}
|
||||
public void connect() {
|
||||
connectionTaskExecutor.execute(() -> {
|
||||
if (!device.connect()) {
|
||||
logger.debug("Failed to connect to {}", address);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void disconnect() {
|
||||
connectionTaskExecutor.execute(device::disconnect);
|
||||
}
|
||||
|
||||
private void scheduleDisconnect() {
|
||||
cancel(pendingDisconnect, false);
|
||||
pendingDisconnect = connectionTaskExecutor.schedule(device::disconnect, idleDisconnectDelay,
|
||||
TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
private void connectAndWait() throws ConnectionException, TimeoutException, InterruptedException {
|
||||
if (device.getConnectionState() == ConnectionState.CONNECTED) {
|
||||
return;
|
||||
}
|
||||
if (device.getConnectionState() != ConnectionState.CONNECTING) {
|
||||
if (!device.connect()) {
|
||||
throw new ConnectionException("Failed to start connecting");
|
||||
}
|
||||
}
|
||||
if (!device.awaitConnection(1, TimeUnit.SECONDS)) {
|
||||
throw new TimeoutException("Connection attempt timeout.");
|
||||
}
|
||||
if (!device.isServicesDiscovered()) {
|
||||
device.discoverServices();
|
||||
if (!device.awaitServiceDiscovery(10, TimeUnit.SECONDS)) {
|
||||
throw new TimeoutException("Service discovery timeout");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelLinked(ChannelUID channelUID) {
|
||||
super.channelLinked(channelUID);
|
||||
private BluetoothCharacteristic connectAndGetCharacteristic(UUID serviceUUID, UUID characteristicUUID)
|
||||
throws BluetoothException, TimeoutException, InterruptedException {
|
||||
connectAndWait();
|
||||
BluetoothService service = device.getServices(serviceUUID);
|
||||
if (service == null) {
|
||||
throw new BluetoothException("Service with uuid " + serviceUUID + " could not be found");
|
||||
}
|
||||
BluetoothCharacteristic characteristic = service.getCharacteristic(characteristicUUID);
|
||||
if (characteristic == null) {
|
||||
throw new BluetoothException("Characteristic with uuid " + characteristicUUID + " could not be found");
|
||||
}
|
||||
return characteristic;
|
||||
}
|
||||
|
||||
private <T> CompletableFuture<T> executeWithConnection(UUID serviceUUID, UUID characteristicUUID,
|
||||
Function<BluetoothCharacteristic, CompletableFuture<T>> callable) {
|
||||
if (connectionTaskExecutor == scheduler) {
|
||||
return CompletableFuture
|
||||
.failedFuture(new IllegalStateException("connectionTaskExecutor has not been initialized"));
|
||||
}
|
||||
if (connectionTaskExecutor.isShutdown()) {
|
||||
return CompletableFuture.failedFuture(new IllegalStateException("connectionTaskExecutor is shut down"));
|
||||
}
|
||||
// we use a RetryFuture because it supports running Callable instances
|
||||
return RetryFuture.callWithRetry(() -> {
|
||||
// we block for completion here so that we keep the lock on the connectionTaskExecutor active.
|
||||
return callable.apply(connectAndGetCharacteristic(serviceUUID, characteristicUUID)).get();
|
||||
}, connectionTaskExecutor)// we make this completion async so that operations chained off the returned future
|
||||
// will not run on the connectionTaskExecutor
|
||||
.whenCompleteAsync((r, th) -> {
|
||||
// we us a while loop here in case the exceptions get nested
|
||||
while (th instanceof CompletionException || th instanceof ExecutionException) {
|
||||
th = th.getCause();
|
||||
}
|
||||
if (th instanceof InterruptedException) {
|
||||
// we don't want to schedule anything if we receive an interrupt
|
||||
return;
|
||||
}
|
||||
if (th instanceof TimeoutException) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, th.getMessage());
|
||||
}
|
||||
if (!alwaysConnected) {
|
||||
scheduleDisconnect();
|
||||
}
|
||||
}, scheduler);
|
||||
}
|
||||
|
||||
public CompletableFuture<@Nullable Void> enableNotifications(UUID serviceUUID, UUID characteristicUUID) {
|
||||
return executeWithConnection(serviceUUID, characteristicUUID, device::enableNotifications);
|
||||
}
|
||||
|
||||
public CompletableFuture<@Nullable Void> writeCharacteristic(UUID serviceUUID, UUID characteristicUUID, byte[] data,
|
||||
boolean enableNotification) {
|
||||
var future = executeWithConnection(serviceUUID, characteristicUUID, characteristic -> {
|
||||
if (enableNotification) {
|
||||
return device.enableNotifications(characteristic)
|
||||
.thenCompose((v) -> device.writeCharacteristic(characteristic, data));
|
||||
} else {
|
||||
return device.writeCharacteristic(characteristic, data);
|
||||
}
|
||||
});
|
||||
if (logger.isDebugEnabled()) {
|
||||
future = future.whenComplete((v, t) -> {
|
||||
if (t == null) {
|
||||
logger.debug("Characteristic {} from {} has written value {}", characteristicUUID, address,
|
||||
HexUtils.bytesToHex(data));
|
||||
}
|
||||
});
|
||||
}
|
||||
return future;
|
||||
}
|
||||
|
||||
public CompletableFuture<byte[]> readCharacteristic(UUID serviceUUID, UUID characteristicUUID) {
|
||||
var future = executeWithConnection(serviceUUID, characteristicUUID, device::readCharacteristic);
|
||||
if (logger.isDebugEnabled()) {
|
||||
future = future.whenComplete((data, t) -> {
|
||||
if (t == null) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Characteristic {} from {} has been read - value {}", characteristicUUID, address,
|
||||
HexUtils.bytesToHex(data));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return future;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -110,10 +259,12 @@ public class ConnectedBluetoothHandler extends BeaconBluetoothHandler {
|
||||
// if there is no signal, we can be sure we are OFFLINE, but if there is a signal, we also have to check whether
|
||||
// we are connected.
|
||||
if (receivedSignal) {
|
||||
if (device.getConnectionState() == ConnectionState.CONNECTED) {
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
} else {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "Device is not connected.");
|
||||
if (alwaysConnected) {
|
||||
if (device.getConnectionState() == ConnectionState.CONNECTED) {
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
} else {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "Device is not connected.");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
|
||||
@@ -126,24 +277,30 @@ public class ConnectedBluetoothHandler extends BeaconBluetoothHandler {
|
||||
switch (connectionNotification.getConnectionState()) {
|
||||
case DISCOVERED:
|
||||
// The device is now known on the Bluetooth network, so we can do something...
|
||||
scheduler.submit(() -> {
|
||||
if (device.getConnectionState() != ConnectionState.CONNECTED) {
|
||||
if (!device.connect()) {
|
||||
logger.debug("Error connecting to device after discovery.");
|
||||
if (alwaysConnected) {
|
||||
connectionTaskExecutor.submit(() -> {
|
||||
if (device.getConnectionState() != ConnectionState.CONNECTED) {
|
||||
if (!device.connect()) {
|
||||
logger.debug("Error connecting to device after discovery.");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
break;
|
||||
case CONNECTED:
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
scheduler.submit(() -> {
|
||||
if (!resolved && !device.discoverServices()) {
|
||||
logger.debug("Error while discovering services");
|
||||
}
|
||||
});
|
||||
if (alwaysConnected) {
|
||||
connectionTaskExecutor.submit(() -> {
|
||||
if (!device.isServicesDiscovered() && !device.discoverServices()) {
|
||||
logger.debug("Error while discovering services");
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
case DISCONNECTED:
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
|
||||
cancel(pendingDisconnect, false);
|
||||
if (alwaysConnected) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@@ -151,51 +308,19 @@ public class ConnectedBluetoothHandler extends BeaconBluetoothHandler {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServicesDiscovered() {
|
||||
super.onServicesDiscovered();
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
logger.debug("Service discovery completed for '{}'", address);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCharacteristicReadComplete(BluetoothCharacteristic characteristic, BluetoothCompletionStatus status) {
|
||||
super.onCharacteristicReadComplete(characteristic, status);
|
||||
if (status == BluetoothCompletionStatus.SUCCESS) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Characteristic {} from {} has been read - value {}", characteristic.getUuid(), address,
|
||||
HexUtils.bytesToHex(characteristic.getByteValue()));
|
||||
}
|
||||
} else {
|
||||
logger.debug("Characteristic {} from {} has been read - ERROR", characteristic.getUuid(), address);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCharacteristicWriteComplete(BluetoothCharacteristic characteristic,
|
||||
BluetoothCompletionStatus status) {
|
||||
super.onCharacteristicWriteComplete(characteristic, status);
|
||||
public void onCharacteristicUpdate(BluetoothCharacteristic characteristic, byte[] value) {
|
||||
super.onCharacteristicUpdate(characteristic, value);
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Wrote {} to characteristic {} of device {}: {}",
|
||||
HexUtils.bytesToHex(characteristic.getByteValue()), characteristic.getUuid(), address, status);
|
||||
logger.debug("Recieved update {} to characteristic {} of device {}", HexUtils.bytesToHex(value),
|
||||
characteristic.getUuid(), address);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCharacteristicUpdate(BluetoothCharacteristic characteristic) {
|
||||
super.onCharacteristicUpdate(characteristic);
|
||||
public void onDescriptorUpdate(BluetoothDescriptor descriptor, byte[] value) {
|
||||
super.onDescriptorUpdate(descriptor, value);
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Recieved update {} to characteristic {} of device {}",
|
||||
HexUtils.bytesToHex(characteristic.getByteValue()), characteristic.getUuid(), address);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDescriptorUpdate(BluetoothDescriptor descriptor) {
|
||||
super.onDescriptorUpdate(descriptor);
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Received update {} to descriptor {} of device {}", HexUtils.bytesToHex(descriptor.getValue()),
|
||||
logger.debug("Received update {} to descriptor {} of device {}", HexUtils.bytesToHex(value),
|
||||
descriptor.getUuid(), address);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.bluetooth;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* This is thrown when some kind of connection issue occurs as part of a bluetooth api call that expects a connection.
|
||||
*
|
||||
* @author Connor Petty - Initial contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ConnectionException extends BluetoothException {
|
||||
|
||||
private static final long serialVersionUID = 2966261738506666653L;
|
||||
|
||||
public ConnectionException() {
|
||||
super();
|
||||
}
|
||||
|
||||
public ConnectionException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
|
||||
super(message, cause, enableSuppression, writableStackTrace);
|
||||
}
|
||||
|
||||
public ConnectionException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public ConnectionException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public ConnectionException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,8 @@ package org.openhab.binding.bluetooth;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
@@ -101,15 +103,21 @@ public abstract class DelegateBluetoothDevice extends BluetoothDevice {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean readCharacteristic(BluetoothCharacteristic characteristic) {
|
||||
public CompletableFuture<byte[]> readCharacteristic(BluetoothCharacteristic characteristic) {
|
||||
BluetoothDevice delegate = getDelegate();
|
||||
return delegate != null && delegate.readCharacteristic(characteristic);
|
||||
if (delegate == null) {
|
||||
return CompletableFuture.failedFuture(new IllegalStateException("Delegate is null"));
|
||||
}
|
||||
return delegate.readCharacteristic(characteristic);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean writeCharacteristic(BluetoothCharacteristic characteristic) {
|
||||
public CompletableFuture<@Nullable Void> writeCharacteristic(BluetoothCharacteristic characteristic, byte[] value) {
|
||||
BluetoothDevice delegate = getDelegate();
|
||||
return delegate != null && delegate.writeCharacteristic(characteristic);
|
||||
if (delegate == null) {
|
||||
return CompletableFuture.failedFuture(new IllegalStateException("Delegate is null"));
|
||||
}
|
||||
return delegate.writeCharacteristic(characteristic, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -119,15 +127,21 @@ public abstract class DelegateBluetoothDevice extends BluetoothDevice {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean enableNotifications(BluetoothCharacteristic characteristic) {
|
||||
public CompletableFuture<@Nullable Void> enableNotifications(BluetoothCharacteristic characteristic) {
|
||||
BluetoothDevice delegate = getDelegate();
|
||||
return delegate != null ? delegate.enableNotifications(characteristic) : false;
|
||||
if (delegate == null) {
|
||||
return CompletableFuture.failedFuture(new IllegalStateException("Delegate is null"));
|
||||
}
|
||||
return delegate.enableNotifications(characteristic);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean disableNotifications(BluetoothCharacteristic characteristic) {
|
||||
public CompletableFuture<@Nullable Void> disableNotifications(BluetoothCharacteristic characteristic) {
|
||||
BluetoothDevice delegate = getDelegate();
|
||||
return delegate != null ? delegate.disableNotifications(characteristic) : false;
|
||||
if (delegate == null) {
|
||||
return CompletableFuture.failedFuture(new IllegalStateException("Delegate is null"));
|
||||
}
|
||||
return delegate.disableNotifications(characteristic);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -160,6 +174,24 @@ public abstract class DelegateBluetoothDevice extends BluetoothDevice {
|
||||
return delegate != null ? delegate.getCharacteristic(uuid) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean awaitConnection(long timeout, TimeUnit unit) throws InterruptedException {
|
||||
BluetoothDevice delegate = getDelegate();
|
||||
return delegate != null ? delegate.awaitConnection(timeout, unit) : false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean awaitServiceDiscovery(long timeout, TimeUnit unit) throws InterruptedException {
|
||||
BluetoothDevice delegate = getDelegate();
|
||||
return delegate != null ? delegate.awaitServiceDiscovery(timeout, unit) : false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isServicesDiscovered() {
|
||||
BluetoothDevice delegate = getDelegate();
|
||||
return delegate != null ? delegate.isServicesDiscovered() : false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void dispose() {
|
||||
BluetoothDevice delegate = getDelegate();
|
||||
|
||||
@@ -20,10 +20,10 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.Condition;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
@@ -34,13 +34,9 @@ import org.openhab.binding.bluetooth.BluetoothBindingConstants;
|
||||
import org.openhab.binding.bluetooth.BluetoothCharacteristic;
|
||||
import org.openhab.binding.bluetooth.BluetoothCharacteristic.GattCharacteristic;
|
||||
import org.openhab.binding.bluetooth.BluetoothCompanyIdentifiers;
|
||||
import org.openhab.binding.bluetooth.BluetoothCompletionStatus;
|
||||
import org.openhab.binding.bluetooth.BluetoothDescriptor;
|
||||
import org.openhab.binding.bluetooth.BluetoothDevice.ConnectionState;
|
||||
import org.openhab.binding.bluetooth.BluetoothDeviceListener;
|
||||
import org.openhab.binding.bluetooth.BluetoothUtils;
|
||||
import org.openhab.binding.bluetooth.discovery.BluetoothDiscoveryParticipant;
|
||||
import org.openhab.binding.bluetooth.notification.BluetoothConnectionStatusNotification;
|
||||
import org.openhab.binding.bluetooth.notification.BluetoothScanNotification;
|
||||
import org.openhab.core.config.discovery.DiscoveryResult;
|
||||
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
|
||||
import org.openhab.core.thing.Thing;
|
||||
@@ -56,28 +52,16 @@ import org.slf4j.LoggerFactory;
|
||||
* @author Connor Petty - Initial Contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class BluetoothDiscoveryProcess implements Supplier<DiscoveryResult>, BluetoothDeviceListener {
|
||||
public class BluetoothDiscoveryProcess implements Supplier<DiscoveryResult> {
|
||||
|
||||
private static final int DISCOVERY_TTL = 300;
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(BluetoothDiscoveryProcess.class);
|
||||
|
||||
private final Lock serviceDiscoveryLock = new ReentrantLock();
|
||||
private final Condition connectionCondition = serviceDiscoveryLock.newCondition();
|
||||
private final Condition serviceDiscoveryCondition = serviceDiscoveryLock.newCondition();
|
||||
private final Condition infoDiscoveryCondition = serviceDiscoveryLock.newCondition();
|
||||
|
||||
private final BluetoothDeviceSnapshot device;
|
||||
private final Collection<BluetoothDiscoveryParticipant> participants;
|
||||
private final Set<BluetoothAdapter> adapters;
|
||||
|
||||
private volatile boolean servicesDiscovered = false;
|
||||
|
||||
/**
|
||||
* Contains characteristic which reading is ongoing or null if no ongoing readings.
|
||||
*/
|
||||
private volatile @Nullable GattCharacteristic ongoingGattCharacteristic;
|
||||
|
||||
public BluetoothDiscoveryProcess(BluetoothDeviceSnapshot device,
|
||||
Collection<BluetoothDiscoveryParticipant> participants, Set<BluetoothAdapter> adapters) {
|
||||
this.participants = participants;
|
||||
@@ -166,41 +150,31 @@ public class BluetoothDiscoveryProcess implements Supplier<DiscoveryResult>, Blu
|
||||
.withBridge(device.getAdapter().getUID()).withLabel(label).build();
|
||||
}
|
||||
|
||||
// this is really just a special return type for `ensureConnected`
|
||||
private static class ConnectionException extends Exception {
|
||||
|
||||
}
|
||||
|
||||
private void ensureConnected() throws ConnectionException, InterruptedException {
|
||||
if (device.getConnectionState() != ConnectionState.CONNECTED) {
|
||||
if (device.getConnectionState() != ConnectionState.CONNECTING && !device.connect()) {
|
||||
logger.debug("Connection attempt failed to start for device {}", device.getAddress());
|
||||
// something failed, so we abandon connection discovery
|
||||
throw new ConnectionException();
|
||||
}
|
||||
if (!awaitConnection(10, TimeUnit.SECONDS)) {
|
||||
logger.debug("Connection to device {} timed out", device.getAddress());
|
||||
throw new ConnectionException();
|
||||
}
|
||||
if (!servicesDiscovered) {
|
||||
device.discoverServices();
|
||||
if (!awaitServiceDiscovery(10, TimeUnit.SECONDS)) {
|
||||
logger.debug("Service discovery for device {} timed out", device.getAddress());
|
||||
// something failed, so we abandon connection discovery
|
||||
throw new ConnectionException();
|
||||
}
|
||||
}
|
||||
readDeviceInformationIfMissing();
|
||||
logger.debug("Device information fetched from the device: {}", device);
|
||||
}
|
||||
}
|
||||
|
||||
private @Nullable DiscoveryResult findConnectionResult(List<BluetoothDiscoveryParticipant> connectionParticipants) {
|
||||
try {
|
||||
device.addListener(this);
|
||||
for (BluetoothDiscoveryParticipant participant : connectionParticipants) {
|
||||
// we call this every time just in case a participant somehow closes the connection
|
||||
ensureConnected();
|
||||
if (device.getConnectionState() != ConnectionState.CONNECTED) {
|
||||
if (device.getConnectionState() != ConnectionState.CONNECTING && !device.connect()) {
|
||||
logger.debug("Connection attempt failed to start for device {}", device.getAddress());
|
||||
// something failed, so we abandon connection discovery
|
||||
return null;
|
||||
}
|
||||
if (!device.awaitConnection(1, TimeUnit.SECONDS)) {
|
||||
logger.debug("Connection to device {} timed out", device.getAddress());
|
||||
return null;
|
||||
}
|
||||
if (!device.isServicesDiscovered()) {
|
||||
device.discoverServices();
|
||||
if (!device.awaitServiceDiscovery(10, TimeUnit.SECONDS)) {
|
||||
logger.debug("Service discovery for device {} timed out", device.getAddress());
|
||||
// something failed, so we abandon connection discovery
|
||||
return null;
|
||||
}
|
||||
}
|
||||
readDeviceInformationIfMissing();
|
||||
logger.debug("Device information fetched from the device: {}", device);
|
||||
}
|
||||
|
||||
try {
|
||||
DiscoveryResult result = participant.createResult(device);
|
||||
if (result != null) {
|
||||
@@ -210,180 +184,49 @@ public class BluetoothDiscoveryProcess implements Supplier<DiscoveryResult>, Blu
|
||||
logger.warn("Participant '{}' threw an exception", participant.getClass().getName(), e);
|
||||
}
|
||||
}
|
||||
} catch (InterruptedException | ConnectionException e) {
|
||||
} catch (InterruptedException e) {
|
||||
// do nothing
|
||||
} finally {
|
||||
device.removeListener(this);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScanRecordReceived(BluetoothScanNotification scanNotification) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnectionStateChange(BluetoothConnectionStatusNotification connectionNotification) {
|
||||
if (connectionNotification.getConnectionState() == ConnectionState.CONNECTED) {
|
||||
serviceDiscoveryLock.lock();
|
||||
try {
|
||||
connectionCondition.signal();
|
||||
} finally {
|
||||
serviceDiscoveryLock.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void readDeviceInformationIfMissing() throws InterruptedException {
|
||||
if (device.getName() == null) {
|
||||
fecthGattCharacteristic(GattCharacteristic.DEVICE_NAME);
|
||||
fecthGattCharacteristic(GattCharacteristic.DEVICE_NAME, device::setName);
|
||||
}
|
||||
if (device.getModel() == null) {
|
||||
fecthGattCharacteristic(GattCharacteristic.MODEL_NUMBER_STRING);
|
||||
fecthGattCharacteristic(GattCharacteristic.MODEL_NUMBER_STRING, device::setModel);
|
||||
}
|
||||
if (device.getSerialNumber() == null) {
|
||||
fecthGattCharacteristic(GattCharacteristic.SERIAL_NUMBER_STRING);
|
||||
fecthGattCharacteristic(GattCharacteristic.SERIAL_NUMBER_STRING, device::setSerialNumberl);
|
||||
}
|
||||
if (device.getHardwareRevision() == null) {
|
||||
fecthGattCharacteristic(GattCharacteristic.HARDWARE_REVISION_STRING);
|
||||
fecthGattCharacteristic(GattCharacteristic.HARDWARE_REVISION_STRING, device::setHardwareRevision);
|
||||
}
|
||||
if (device.getFirmwareRevision() == null) {
|
||||
fecthGattCharacteristic(GattCharacteristic.FIRMWARE_REVISION_STRING);
|
||||
fecthGattCharacteristic(GattCharacteristic.FIRMWARE_REVISION_STRING, device::setFirmwareRevision);
|
||||
}
|
||||
if (device.getSoftwareRevision() == null) {
|
||||
fecthGattCharacteristic(GattCharacteristic.SOFTWARE_REVISION_STRING);
|
||||
fecthGattCharacteristic(GattCharacteristic.SOFTWARE_REVISION_STRING, device::setSoftwareRevision);
|
||||
}
|
||||
}
|
||||
|
||||
private void fecthGattCharacteristic(GattCharacteristic gattCharacteristic) throws InterruptedException {
|
||||
private void fecthGattCharacteristic(GattCharacteristic gattCharacteristic, Consumer<String> consumer)
|
||||
throws InterruptedException {
|
||||
UUID uuid = gattCharacteristic.getUUID();
|
||||
BluetoothCharacteristic characteristic = device.getCharacteristic(uuid);
|
||||
if (characteristic == null) {
|
||||
logger.debug("Device '{}' doesn't support uuid '{}'", device.getAddress(), uuid);
|
||||
return;
|
||||
}
|
||||
if (!device.readCharacteristic(characteristic)) {
|
||||
logger.debug("Failed to aquire uuid {} from device {}", uuid, device.getAddress());
|
||||
return;
|
||||
}
|
||||
ongoingGattCharacteristic = gattCharacteristic;
|
||||
if (!awaitInfoResponse(1, TimeUnit.SECONDS)) {
|
||||
logger.debug("Device info (uuid {}) for device {} timed out", uuid, device.getAddress());
|
||||
ongoingGattCharacteristic = null;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean awaitConnection(long timeout, TimeUnit unit) throws InterruptedException {
|
||||
serviceDiscoveryLock.lock();
|
||||
try {
|
||||
long nanosTimeout = unit.toNanos(timeout);
|
||||
while (device.getConnectionState() != ConnectionState.CONNECTED) {
|
||||
if (nanosTimeout <= 0L) {
|
||||
return false;
|
||||
}
|
||||
nanosTimeout = connectionCondition.awaitNanos(nanosTimeout);
|
||||
}
|
||||
} finally {
|
||||
serviceDiscoveryLock.unlock();
|
||||
byte[] value = device.readCharacteristic(characteristic).get(1, TimeUnit.SECONDS);
|
||||
consumer.accept(BluetoothUtils.getStringValue(value, 0));
|
||||
} catch (ExecutionException e) {
|
||||
logger.debug("Failed to aquire uuid {} from device {}: {}", uuid, device.getAddress(), e.getMessage());
|
||||
} catch (TimeoutException e) {
|
||||
logger.debug("Device info (uuid {}) for device {} timed out: {}", uuid, device.getAddress(),
|
||||
e.getMessage());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean awaitInfoResponse(long timeout, TimeUnit unit) throws InterruptedException {
|
||||
serviceDiscoveryLock.lock();
|
||||
try {
|
||||
long nanosTimeout = unit.toNanos(timeout);
|
||||
while (ongoingGattCharacteristic != null) {
|
||||
if (nanosTimeout <= 0L) {
|
||||
return false;
|
||||
}
|
||||
nanosTimeout = infoDiscoveryCondition.awaitNanos(nanosTimeout);
|
||||
}
|
||||
} finally {
|
||||
serviceDiscoveryLock.unlock();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean awaitServiceDiscovery(long timeout, TimeUnit unit) throws InterruptedException {
|
||||
serviceDiscoveryLock.lock();
|
||||
try {
|
||||
long nanosTimeout = unit.toNanos(timeout);
|
||||
while (!servicesDiscovered) {
|
||||
if (nanosTimeout <= 0L) {
|
||||
return false;
|
||||
}
|
||||
nanosTimeout = serviceDiscoveryCondition.awaitNanos(nanosTimeout);
|
||||
}
|
||||
} finally {
|
||||
serviceDiscoveryLock.unlock();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServicesDiscovered() {
|
||||
serviceDiscoveryLock.lock();
|
||||
try {
|
||||
servicesDiscovered = true;
|
||||
serviceDiscoveryCondition.signal();
|
||||
} finally {
|
||||
serviceDiscoveryLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCharacteristicReadComplete(BluetoothCharacteristic characteristic, BluetoothCompletionStatus status) {
|
||||
serviceDiscoveryLock.lock();
|
||||
try {
|
||||
if (status == BluetoothCompletionStatus.SUCCESS) {
|
||||
switch (characteristic.getGattCharacteristic()) {
|
||||
case DEVICE_NAME:
|
||||
device.setName(characteristic.getStringValue(0));
|
||||
break;
|
||||
case MODEL_NUMBER_STRING:
|
||||
device.setModel(characteristic.getStringValue(0));
|
||||
break;
|
||||
case SERIAL_NUMBER_STRING:
|
||||
device.setSerialNumberl(characteristic.getStringValue(0));
|
||||
break;
|
||||
case HARDWARE_REVISION_STRING:
|
||||
device.setHardwareRevision(characteristic.getStringValue(0));
|
||||
break;
|
||||
case FIRMWARE_REVISION_STRING:
|
||||
device.setFirmwareRevision(characteristic.getStringValue(0));
|
||||
break;
|
||||
case SOFTWARE_REVISION_STRING:
|
||||
device.setSoftwareRevision(characteristic.getStringValue(0));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (ongoingGattCharacteristic == characteristic.getGattCharacteristic()) {
|
||||
ongoingGattCharacteristic = null;
|
||||
infoDiscoveryCondition.signal();
|
||||
}
|
||||
} finally {
|
||||
serviceDiscoveryLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCharacteristicWriteComplete(BluetoothCharacteristic characteristic,
|
||||
BluetoothCompletionStatus status) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCharacteristicUpdate(BluetoothCharacteristic characteristic) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDescriptorUpdate(BluetoothDescriptor bluetoothDescriptor) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAdapterChanged(BluetoothAdapter adapter) {
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user