[bluetooth] Changed characteristic read/write to use CompletableFutures ()

Signed-off-by: Connor Petty <mistercpp2000+gitsignoff@gmail.com>
This commit is contained in:
Connor Petty 2021-04-09 13:23:28 -07:00 committed by GitHub
parent 1822f77b07
commit 89d735bb0f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 1207 additions and 1562 deletions
bundles
org.openhab.binding.bluetooth.airthings/src/main/java/org/openhab/binding/bluetooth/airthings/internal
org.openhab.binding.bluetooth.am43/src/main/java/org/openhab/binding/bluetooth/am43/internal
org.openhab.binding.bluetooth.bluegiga/src/main/java/org/openhab/binding/bluetooth/bluegiga
org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal
org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/handler
org.openhab.binding.bluetooth.generic/src/main
java/org/openhab/binding/bluetooth/generic/internal
resources/OH-INF/thing
org.openhab.binding.bluetooth.govee/src/main/java/org/openhab/binding/bluetooth/govee/internal
org.openhab.binding.bluetooth.roaming/src/main/java/org/openhab/binding/bluetooth/roaming/internal
org.openhab.binding.bluetooth/src

@ -24,8 +24,8 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.bluetooth.BeaconBluetoothHandler;
import org.openhab.binding.bluetooth.BluetoothCharacteristic;
import org.openhab.binding.bluetooth.BluetoothCompletionStatus;
import org.openhab.binding.bluetooth.BluetoothDevice.ConnectionState;
import org.openhab.binding.bluetooth.BluetoothUtils;
import org.openhab.binding.bluetooth.notification.BluetoothConnectionStatusNotification;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.SIUnits;
@ -156,8 +156,35 @@ public class AirthingsWavePlusHandler extends BeaconBluetoothHandler {
case IDLE:
logger.debug("Read data from device {}...", address);
BluetoothCharacteristic characteristic = device.getCharacteristic(uuid);
if (characteristic != null && device.readCharacteristic(characteristic)) {
if (characteristic != null) {
readState = ReadState.READING;
device.readCharacteristic(characteristic).whenComplete((data, ex) -> {
try {
if (data != null) {
logger.debug("Characteristic {} from device {}: {}", characteristic.getUuid(),
address, data);
updateStatus(ThingStatus.ONLINE);
sinceLastReadSec.set(0);
try {
updateChannels(
new AirthingsWavePlusDataParser(BluetoothUtils.toIntArray(data)));
} catch (AirthingsParserException e) {
logger.warn(
"Data parsing error occured, when parsing data from device {}, cause {}",
address, e.getMessage(), e);
}
} else {
logger.debug("Characteristic {} from device {} failed: {}",
characteristic.getUuid(), address, ex.getMessage());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
ex.getMessage());
}
} finally {
readState = ReadState.IDLE;
disconnect();
}
});
} else {
logger.debug("Read data from device {} failed", address);
disconnect();
@ -205,30 +232,6 @@ public class AirthingsWavePlusHandler extends BeaconBluetoothHandler {
execute();
}
@Override
public void onCharacteristicReadComplete(BluetoothCharacteristic characteristic, BluetoothCompletionStatus status) {
try {
if (status == BluetoothCompletionStatus.SUCCESS) {
logger.debug("Characteristic {} from device {}: {}", characteristic.getUuid(), address,
characteristic.getValue());
updateStatus(ThingStatus.ONLINE);
sinceLastReadSec.set(0);
try {
updateChannels(new AirthingsWavePlusDataParser(characteristic.getValue()));
} catch (AirthingsParserException e) {
logger.warn("Data parsing error occured, when parsing data from device {}, cause {}", address,
e.getMessage(), e);
}
} else {
logger.debug("Characteristic {} from device {} failed", characteristic.getUuid(), address);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "No response from device");
}
} finally {
readState = ReadState.IDLE;
disconnect();
}
}
private void updateChannels(AirthingsWavePlusDataParser parser) {
logger.debug("Parsed data: {}", parser);
updateState(CHANNEL_ID_HUMIDITY, QuantityType.valueOf(Double.valueOf(parser.getHumidity()), Units.PERCENT));

@ -12,7 +12,6 @@
*/
package org.openhab.binding.bluetooth.am43.internal;
import java.util.Arrays;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@ -24,7 +23,6 @@ import javax.measure.quantity.Length;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.bluetooth.BluetoothCharacteristic;
import org.openhab.binding.bluetooth.BluetoothCompletionStatus;
import org.openhab.binding.bluetooth.BluetoothDevice.ConnectionState;
import org.openhab.binding.bluetooth.ConnectedBluetoothHandler;
import org.openhab.binding.bluetooth.am43.internal.command.AM43Command;
@ -164,7 +162,7 @@ public class AM43Handler extends ConnectedBluetoothHandler implements ResponseLi
command.setState(AM43Command.State.FAILED);
return;
}
if (!resolved) {
if (!device.isServicesDiscovered()) {
logger.debug("Unable to send command {} to device {}: services not resolved", command,
device.getAddress());
command.setState(AM43Command.State.FAILED);
@ -180,9 +178,15 @@ public class AM43Handler extends ConnectedBluetoothHandler implements ResponseLi
// there is no consequence to calling this as much as we like
device.enableNotifications(characteristic);
characteristic.setValue(command.getRequest());
command.setState(AM43Command.State.ENQUEUED);
device.writeCharacteristic(characteristic);
device.writeCharacteristic(characteristic, command.getRequest()).whenComplete((v, t) -> {
if (t != null) {
logger.debug("Failed to send command {}: {}", command.getClass().getSimpleName(), t.getMessage());
command.setState(AM43Command.State.FAILED);
} else {
command.setState(AM43Command.State.SENT);
}
});
if (!command.awaitStateChange(getAM43Config().commandTimeout, TimeUnit.MILLISECONDS,
AM43Command.State.SUCCEEDED, AM43Command.State.FAILED)) {
@ -197,39 +201,8 @@ public class AM43Handler extends ConnectedBluetoothHandler implements ResponseLi
}
@Override
public void onCharacteristicWriteComplete(BluetoothCharacteristic characteristic,
BluetoothCompletionStatus status) {
super.onCharacteristicWriteComplete(characteristic, status);
byte[] request = characteristic.getByteValue();
AM43Command command = currentCommand;
if (command != null) {
if (!Arrays.equals(request, command.getRequest())) {
logger.debug("Write completed for unknown command");
return;
}
switch (status) {
case SUCCESS:
command.setState(AM43Command.State.SENT);
break;
case ERROR:
command.setState(AM43Command.State.FAILED);
break;
}
} else {
if (logger.isDebugEnabled()) {
logger.debug("No command found that matches request {}", HexUtils.bytesToHex(request));
}
}
}
@Override
public void onCharacteristicUpdate(BluetoothCharacteristic characteristic) {
super.onCharacteristicUpdate(characteristic);
byte[] response = characteristic.getByteValue();
public void onCharacteristicUpdate(BluetoothCharacteristic characteristic, byte[] response) {
super.onCharacteristicUpdate(characteristic, response);
AM43Command command = currentCommand;
if (command == null) {

@ -19,9 +19,11 @@ import java.util.Map;
import java.util.NavigableMap;
import java.util.TreeMap;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
@ -29,10 +31,11 @@ import org.openhab.binding.bluetooth.BaseBluetoothDevice;
import org.openhab.binding.bluetooth.BluetoothAddress;
import org.openhab.binding.bluetooth.BluetoothBindingConstants;
import org.openhab.binding.bluetooth.BluetoothCharacteristic;
import org.openhab.binding.bluetooth.BluetoothCompletionStatus;
import org.openhab.binding.bluetooth.BluetoothDescriptor;
import org.openhab.binding.bluetooth.BluetoothDevice;
import org.openhab.binding.bluetooth.BluetoothException;
import org.openhab.binding.bluetooth.BluetoothService;
import org.openhab.binding.bluetooth.BluetoothUtils;
import org.openhab.binding.bluetooth.bluegiga.handler.BlueGigaBridgeHandler;
import org.openhab.binding.bluetooth.bluegiga.internal.BlueGigaEventListener;
import org.openhab.binding.bluetooth.bluegiga.internal.BlueGigaResponse;
@ -66,6 +69,14 @@ public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements Blue
private final Logger logger = LoggerFactory.getLogger(BlueGigaBluetoothDevice.class);
private static final BlueGigaProcedure PROCEDURE_NONE = new BlueGigaProcedure(BlueGigaProcedure.Type.NONE);
private static final BlueGigaProcedure PROCEDURE_GET_SERVICES = new BlueGigaProcedure(
BlueGigaProcedure.Type.GET_SERVICES);
private static final BlueGigaProcedure PROCEDURE_GET_CHARACTERISTICS = new BlueGigaProcedure(
BlueGigaProcedure.Type.GET_CHARACTERISTICS);
private static final BlueGigaProcedure PROCEDURE_READ_CHARACTERISTIC_DECL = new BlueGigaProcedure(
BlueGigaProcedure.Type.READ_CHARACTERISTIC_DECL);
private Map<Integer, UUID> handleToUUID = new HashMap<>();
private NavigableMap<Integer, BlueGigaBluetoothCharacteristic> handleToCharacteristic = new TreeMap<>();
@ -75,22 +86,7 @@ public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements Blue
// The dongle handler
private final BlueGigaBridgeHandler bgHandler;
// An enum to use in the state machine for interacting with the device
private enum BlueGigaProcedure {
NONE,
GET_SERVICES,
GET_CHARACTERISTICS,
READ_CHARACTERISTIC_DECL,
CHARACTERISTIC_READ,
CHARACTERISTIC_WRITE,
NOTIFICATION_ENABLE,
NOTIFICATION_DISABLE
}
private BlueGigaProcedure procedureProgress = BlueGigaProcedure.NONE;
// Somewhere to remember what characteristic we're working on
private @Nullable BluetoothCharacteristic procedureCharacteristic;
private BlueGigaProcedure currentProcedure = PROCEDURE_NONE;
// The connection handle if the device is connected
private int connection = -1;
@ -113,9 +109,24 @@ public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements Blue
private Runnable procedureTimeoutTask = new Runnable() {
@Override
public void run() {
logger.debug("Procedure {} timeout for device {}", procedureProgress, address);
procedureProgress = BlueGigaProcedure.NONE;
procedureCharacteristic = null;
BlueGigaProcedure procedure = currentProcedure;
logger.debug("Procedure {} timeout for device {}", procedure.type, address);
switch (procedure.type) {
case CHARACTERISTIC_READ:
ReadCharacteristicProcedure readProcedure = (ReadCharacteristicProcedure) procedure;
readProcedure.readFuture.completeExceptionally(new TimeoutException("Read characteristic "
+ readProcedure.characteristic.getUuid() + " timeout for device " + address));
break;
case CHARACTERISTIC_WRITE:
WriteCharacteristicProcedure writeProcedure = (WriteCharacteristicProcedure) procedure;
writeProcedure.writeFuture.completeExceptionally(new TimeoutException("Write characteristic "
+ writeProcedure.characteristic.getUuid() + " timeout for device " + address));
break;
default:
break;
}
currentProcedure = PROCEDURE_NONE;
}
};
@ -174,7 +185,7 @@ public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements Blue
@Override
public boolean discoverServices() {
if (procedureProgress != BlueGigaProcedure.NONE) {
if (currentProcedure != PROCEDURE_NONE) {
return false;
}
@ -184,49 +195,45 @@ public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements Blue
}
procedureTimer = startTimer(procedureTimeoutTask, TIMEOUT_SEC);
procedureProgress = BlueGigaProcedure.GET_SERVICES;
currentProcedure = PROCEDURE_GET_SERVICES;
return true;
}
@Override
public boolean enableNotifications(BluetoothCharacteristic characteristic) {
public CompletableFuture<@Nullable Void> enableNotifications(BluetoothCharacteristic characteristic) {
if (connection == -1) {
logger.debug("Cannot enable notifications, device not connected {}", this);
return false;
return CompletableFuture.failedFuture(new BluetoothException("Not connected"));
}
BlueGigaBluetoothCharacteristic ch = (BlueGigaBluetoothCharacteristic) characteristic;
if (ch.isNotifying()) {
return true;
return CompletableFuture.completedFuture(null);
}
BluetoothDescriptor descriptor = ch
.getDescriptor(BluetoothDescriptor.GattDescriptor.CLIENT_CHARACTERISTIC_CONFIGURATION.getUUID());
if (descriptor == null || descriptor.getHandle() == 0) {
logger.debug("unable to find CCC for characteristic {}", characteristic.getUuid());
return false;
return CompletableFuture.failedFuture(
new BluetoothException("Unable to find CCC for characteristic [" + characteristic.getUuid() + "]"));
}
if (procedureProgress != BlueGigaProcedure.NONE) {
logger.debug("Procedure already in progress {}", procedureProgress);
return false;
if (currentProcedure != PROCEDURE_NONE) {
return CompletableFuture.failedFuture(new BluetoothException("Another procedure is already in progress"));
}
int[] value = { 1, 0 };
byte[] bvalue = toBytes(value);
descriptor.setValue(bvalue);
cancelTimer(procedureTimer);
if (!bgHandler.bgWriteCharacteristic(connection, descriptor.getHandle(), value)) {
logger.debug("bgWriteCharacteristic returned false");
return false;
return CompletableFuture.failedFuture(new BluetoothException(
"Failed to write to CCC for characteristic [" + characteristic.getUuid() + "]"));
}
procedureTimer = startTimer(procedureTimeoutTask, TIMEOUT_SEC);
procedureProgress = BlueGigaProcedure.NOTIFICATION_ENABLE;
procedureCharacteristic = characteristic;
WriteCharacteristicProcedure notifyProcedure = new WriteCharacteristicProcedure(ch,
BlueGigaProcedure.Type.NOTIFICATION_ENABLE);
currentProcedure = notifyProcedure;
try {
// we intentionally sleep here in order to give this procedure a chance to complete.
// ideally we would use locks/conditions to make this wait until completiong but
@ -235,57 +242,46 @@ public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements Blue
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return true;
return notifyProcedure.writeFuture;
}
@Override
public boolean disableNotifications(BluetoothCharacteristic characteristic) {
public CompletableFuture<@Nullable Void> disableNotifications(BluetoothCharacteristic characteristic) {
if (connection == -1) {
logger.debug("Cannot disable notifications, device not connected {}", this);
return false;
return CompletableFuture.failedFuture(new BluetoothException("Not connected"));
}
BlueGigaBluetoothCharacteristic ch = (BlueGigaBluetoothCharacteristic) characteristic;
if (!ch.isNotifying()) {
return true;
return CompletableFuture.completedFuture(null);
}
BluetoothDescriptor descriptor = ch
.getDescriptor(BluetoothDescriptor.GattDescriptor.CLIENT_CHARACTERISTIC_CONFIGURATION.getUUID());
if (descriptor == null || descriptor.getHandle() == 0) {
logger.debug("unable to find CCC for characteristic {}", characteristic.getUuid());
return false;
return CompletableFuture.failedFuture(
new BluetoothException("Unable to find CCC for characteristic [" + characteristic.getUuid() + "]"));
}
if (procedureProgress != BlueGigaProcedure.NONE) {
logger.debug("Procedure already in progress {}", procedureProgress);
return false;
if (currentProcedure != PROCEDURE_NONE) {
return CompletableFuture.failedFuture(new BluetoothException("Another procedure is already in progress"));
}
int[] value = { 0, 0 };
byte[] bvalue = toBytes(value);
descriptor.setValue(bvalue);
cancelTimer(procedureTimer);
if (!bgHandler.bgWriteCharacteristic(connection, descriptor.getHandle(), value)) {
logger.debug("bgWriteCharacteristic returned false");
return false;
return CompletableFuture.failedFuture(new BluetoothException(
"Failed to write to CCC for characteristic [" + characteristic.getUuid() + "]"));
}
procedureTimer = startTimer(procedureTimeoutTask, TIMEOUT_SEC);
procedureProgress = BlueGigaProcedure.NOTIFICATION_DISABLE;
procedureCharacteristic = characteristic;
WriteCharacteristicProcedure notifyProcedure = new WriteCharacteristicProcedure(ch,
BlueGigaProcedure.Type.NOTIFICATION_DISABLE);
currentProcedure = notifyProcedure;
try {
// we intentionally sleep here in order to give this procedure a chance to complete.
// ideally we would use locks/conditions to make this wait until completiong but
// I have a better solution planned for later. - Connor Petty
Thread.sleep(500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return true;
return notifyProcedure.writeFuture;
}
@Override
@ -307,52 +303,56 @@ public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements Blue
}
@Override
public boolean readCharacteristic(@Nullable BluetoothCharacteristic characteristic) {
if (characteristic == null || characteristic.getHandle() == 0) {
return false;
public CompletableFuture<byte[]> readCharacteristic(BluetoothCharacteristic characteristic) {
if (characteristic.getHandle() == 0) {
return CompletableFuture.failedFuture(new BluetoothException("Cannot read characteristic with no handle"));
}
if (connection == -1) {
return false;
return CompletableFuture.failedFuture(new BluetoothException("Not connected"));
}
if (procedureProgress != BlueGigaProcedure.NONE) {
return false;
if (currentProcedure != PROCEDURE_NONE) {
return CompletableFuture.failedFuture(new BluetoothException("Another procedure is already in progress"));
}
cancelTimer(procedureTimer);
if (!bgHandler.bgReadCharacteristic(connection, characteristic.getHandle())) {
return false;
return CompletableFuture.failedFuture(
new BluetoothException("Failed to read characteristic [" + characteristic.getUuid() + "]"));
}
procedureTimer = startTimer(procedureTimeoutTask, TIMEOUT_SEC);
procedureProgress = BlueGigaProcedure.CHARACTERISTIC_READ;
procedureCharacteristic = characteristic;
ReadCharacteristicProcedure readProcedure = new ReadCharacteristicProcedure(characteristic);
currentProcedure = readProcedure;
return true;
return readProcedure.readFuture;
}
@Override
public boolean writeCharacteristic(@Nullable BluetoothCharacteristic characteristic) {
if (characteristic == null || characteristic.getHandle() == 0) {
return false;
public CompletableFuture<@Nullable Void> writeCharacteristic(BluetoothCharacteristic characteristic, byte[] value) {
if (characteristic.getHandle() == 0) {
return CompletableFuture.failedFuture(new BluetoothException("Cannot write characteristic with no handle"));
}
if (connection == -1) {
return false;
return CompletableFuture.failedFuture(new BluetoothException("Not connected"));
}
if (procedureProgress != BlueGigaProcedure.NONE) {
return false;
if (currentProcedure != PROCEDURE_NONE) {
return CompletableFuture.failedFuture(new BluetoothException("Another procedure is already in progress"));
}
cancelTimer(procedureTimer);
if (!bgHandler.bgWriteCharacteristic(connection, characteristic.getHandle(), characteristic.getValue())) {
return false;
if (!bgHandler.bgWriteCharacteristic(connection, characteristic.getHandle(),
BluetoothUtils.toIntArray(value))) {
return CompletableFuture.failedFuture(
new BluetoothException("Failed to write characteristic [" + characteristic.getUuid() + "]"));
}
procedureTimer = startTimer(procedureTimeoutTask, TIMEOUT_SEC);
procedureProgress = BlueGigaProcedure.CHARACTERISTIC_WRITE;
procedureCharacteristic = characteristic;
WriteCharacteristicProcedure writeProcedure = new WriteCharacteristicProcedure(
(BlueGigaBluetoothCharacteristic) characteristic, BlueGigaProcedure.Type.CHARACTERISTIC_WRITE);
currentProcedure = writeProcedure;
return true;
return writeProcedure.writeFuture;
}
@Override
@ -564,7 +564,7 @@ public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements Blue
return;
}
if (procedureProgress == BlueGigaProcedure.NONE) {
if (currentProcedure == PROCEDURE_NONE) {
logger.debug("BlueGiga procedure completed but procedure is null with connection {}, address {}",
connection, address);
return;
@ -574,63 +574,73 @@ public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements Blue
updateLastSeenTime();
// The current procedure is now complete - move on...
switch (procedureProgress) {
switch (currentProcedure.type) {
case GET_SERVICES:
// We've downloaded all services, now get the characteristics
if (bgHandler.bgFindCharacteristics(connection)) {
procedureTimer = startTimer(procedureTimeoutTask, TIMEOUT_SEC);
procedureProgress = BlueGigaProcedure.GET_CHARACTERISTICS;
currentProcedure = PROCEDURE_GET_CHARACTERISTICS;
} else {
procedureProgress = BlueGigaProcedure.NONE;
currentProcedure = PROCEDURE_NONE;
}
break;
case GET_CHARACTERISTICS:
// We've downloaded all attributes, now read the characteristic declarations
if (bgHandler.bgReadCharacteristicDeclarations(connection)) {
procedureTimer = startTimer(procedureTimeoutTask, TIMEOUT_SEC);
procedureProgress = BlueGigaProcedure.READ_CHARACTERISTIC_DECL;
currentProcedure = PROCEDURE_READ_CHARACTERISTIC_DECL;
} else {
procedureProgress = BlueGigaProcedure.NONE;
currentProcedure = PROCEDURE_NONE;
}
break;
case READ_CHARACTERISTIC_DECL:
// We've downloaded read all the declarations, we are done now
procedureProgress = BlueGigaProcedure.NONE;
currentProcedure = PROCEDURE_NONE;
notifyListeners(BluetoothEventType.SERVICES_DISCOVERED);
break;
case CHARACTERISTIC_READ:
// The read failed
notifyListeners(BluetoothEventType.CHARACTERISTIC_READ_COMPLETE, procedureCharacteristic,
BluetoothCompletionStatus.ERROR);
procedureProgress = BlueGigaProcedure.NONE;
procedureCharacteristic = null;
ReadCharacteristicProcedure readProcedure = (ReadCharacteristicProcedure) currentProcedure;
readProcedure.readFuture.completeExceptionally(new BluetoothException(
"Read characteristic failed: " + readProcedure.characteristic.getUuid()));
currentProcedure = PROCEDURE_NONE;
break;
case CHARACTERISTIC_WRITE:
// The write completed - failure or success
BluetoothCompletionStatus result = event.getResult() == BgApiResponse.SUCCESS
? BluetoothCompletionStatus.SUCCESS
: BluetoothCompletionStatus.ERROR;
notifyListeners(BluetoothEventType.CHARACTERISTIC_WRITE_COMPLETE, procedureCharacteristic, result);
procedureProgress = BlueGigaProcedure.NONE;
procedureCharacteristic = null;
WriteCharacteristicProcedure writeProcedure = (WriteCharacteristicProcedure) currentProcedure;
if (event.getResult() == BgApiResponse.SUCCESS) {
writeProcedure.writeFuture.complete(null);
} else {
writeProcedure.writeFuture.completeExceptionally(new BluetoothException(
"Write characteristic failed: " + writeProcedure.characteristic.getUuid()));
}
currentProcedure = PROCEDURE_NONE;
break;
case NOTIFICATION_ENABLE:
WriteCharacteristicProcedure notifyEnableProcedure = (WriteCharacteristicProcedure) currentProcedure;
boolean success = event.getResult() == BgApiResponse.SUCCESS;
if (!success) {
logger.debug("write to descriptor failed");
if (success) {
notifyEnableProcedure.writeFuture.complete(null);
} else {
notifyEnableProcedure.writeFuture
.completeExceptionally(new BluetoothException("Enable characteristic notification failed: "
+ notifyEnableProcedure.characteristic.getUuid()));
}
((BlueGigaBluetoothCharacteristic) procedureCharacteristic).setNotifying(success);
procedureProgress = BlueGigaProcedure.NONE;
procedureCharacteristic = null;
notifyEnableProcedure.characteristic.setNotifying(success);
currentProcedure = PROCEDURE_NONE;
break;
case NOTIFICATION_DISABLE:
WriteCharacteristicProcedure notifyDisableProcedure = (WriteCharacteristicProcedure) currentProcedure;
success = event.getResult() == BgApiResponse.SUCCESS;
if (!success) {
logger.debug("write to descriptor failed");
if (success) {
notifyDisableProcedure.writeFuture.complete(null);
} else {
notifyDisableProcedure.writeFuture
.completeExceptionally(new BluetoothException("Disable characteristic notification failed: "
+ notifyDisableProcedure.characteristic.getUuid()));
}
((BlueGigaBluetoothCharacteristic) procedureCharacteristic).setNotifying(!success);
procedureProgress = BlueGigaProcedure.NONE;
procedureCharacteristic = null;
notifyDisableProcedure.characteristic.setNotifying(!success);
currentProcedure = PROCEDURE_NONE;
break;
default:
break;
@ -668,7 +678,23 @@ public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements Blue
cancelTimer(procedureTimer);
connectionState = ConnectionState.DISCONNECTED;
connection = -1;
procedureProgress = BlueGigaProcedure.NONE;
BlueGigaProcedure procedure = currentProcedure;
switch (procedure.type) {
case CHARACTERISTIC_READ:
ReadCharacteristicProcedure readProcedure = (ReadCharacteristicProcedure) procedure;
readProcedure.readFuture.completeExceptionally(new BluetoothException("Read characteristic "
+ readProcedure.characteristic.getUuid() + " failed due to disconnect of device " + address));
break;
case CHARACTERISTIC_WRITE:
WriteCharacteristicProcedure writeProcedure = (WriteCharacteristicProcedure) procedure;
writeProcedure.writeFuture.completeExceptionally(new BluetoothException("Write characteristic "
+ writeProcedure.characteristic.getUuid() + " failed due to disconnect of device " + address));
break;
default:
break;
}
currentProcedure = PROCEDURE_NONE;
notifyListeners(BluetoothEventType.CONNECTION_STATE,
new BluetoothConnectionStatusNotification(connectionState));
@ -707,42 +733,30 @@ public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements Blue
return;
}
if (handle == characteristic.getHandle()) {
characteristic.setValue(event.getValue().clone());
byte[] value = BluetoothUtils.toByteArray(event.getValue());
BlueGigaProcedure procedure = currentProcedure;
// If this is the characteristic we were reading, then send a read completion
if (procedureProgress == BlueGigaProcedure.CHARACTERISTIC_READ && procedureCharacteristic != null
&& procedureCharacteristic.getHandle() == event.getAttHandle()) {
procedureProgress = BlueGigaProcedure.NONE;
procedureCharacteristic = null;
notifyListeners(BluetoothEventType.CHARACTERISTIC_READ_COMPLETE, characteristic,
BluetoothCompletionStatus.SUCCESS);
return;
if (procedure.type == BlueGigaProcedure.Type.CHARACTERISTIC_READ) {
ReadCharacteristicProcedure readProcedure = (ReadCharacteristicProcedure) currentProcedure;
if (readProcedure.characteristic.getHandle() == event.getAttHandle()) {
readProcedure.readFuture.complete(value);
currentProcedure = PROCEDURE_NONE;
return;
}
}
// Notify the user of the updated value
notifyListeners(BluetoothEventType.CHARACTERISTIC_UPDATED, characteristic);
notifyListeners(BluetoothEventType.CHARACTERISTIC_UPDATED, characteristic, value);
} else {
// it must be one of the descriptors we need to update
UUID attUUID = handleToUUID.get(handle);
BluetoothDescriptor descriptor = characteristic.getDescriptor(attUUID);
descriptor.setValue(toBytes(event.getValue()));
notifyListeners(BluetoothEventType.DESCRIPTOR_UPDATED, descriptor);
notifyListeners(BluetoothEventType.DESCRIPTOR_UPDATED, descriptor,
BluetoothUtils.toByteArray(event.getValue()));
}
}
private static byte @Nullable [] toBytes(int @Nullable [] 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];
}
return ret;
}
private boolean parseDeclaration(BlueGigaBluetoothCharacteristic ch, int[] value) {
ByteBuffer buffer = ByteBuffer.wrap(toBytes(value));
ByteBuffer buffer = ByteBuffer.wrap(BluetoothUtils.toByteArray(value));
buffer.order(ByteOrder.LITTLE_ENDIAN);
ch.setProperties(Byte.toUnsignedInt(buffer.get()));
@ -779,7 +793,7 @@ public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements Blue
cancelTimer(connectTimer);
cancelTimer(procedureTimer);
bgHandler.removeEventListener(this);
procedureProgress = BlueGigaProcedure.NONE;
currentProcedure = PROCEDURE_NONE;
connectionState = ConnectionState.DISCOVERING;
connection = -1;
}
@ -793,4 +807,48 @@ public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements Blue
private ScheduledFuture<?> startTimer(Runnable command, long timeout) {
return scheduler.schedule(command, timeout, TimeUnit.SECONDS);
}
private static class BlueGigaProcedure {
private final Type type;
public BlueGigaProcedure(Type type) {
this.type = type;
}
// An enum to use in the state machine for interacting with the device
enum Type {
NONE,
GET_SERVICES,
GET_CHARACTERISTICS,
READ_CHARACTERISTIC_DECL,
CHARACTERISTIC_READ,
CHARACTERISTIC_WRITE,
NOTIFICATION_ENABLE,
NOTIFICATION_DISABLE
}
}
private static class ReadCharacteristicProcedure extends BlueGigaProcedure {
private final BluetoothCharacteristic characteristic;
private final CompletableFuture<byte[]> readFuture = new CompletableFuture<>();
public ReadCharacteristicProcedure(BluetoothCharacteristic characteristic) {
super(Type.CHARACTERISTIC_READ);
this.characteristic = characteristic;
}
}
private static class WriteCharacteristicProcedure extends BlueGigaProcedure {
private final BlueGigaBluetoothCharacteristic characteristic;
private final CompletableFuture<@Nullable Void> writeFuture = new CompletableFuture<>();
public WriteCharacteristicProcedure(BlueGigaBluetoothCharacteristic characteristic, Type type) {
super(type);
this.characteristic = characteristic;
}
}
}

@ -15,10 +15,10 @@ package org.openhab.binding.bluetooth.bluez.internal;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import org.bluez.exceptions.BluezFailedException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.freedesktop.dbus.errors.NoReply;
@ -28,7 +28,6 @@ import org.freedesktop.dbus.types.UInt16;
import org.openhab.binding.bluetooth.BaseBluetoothDevice;
import org.openhab.binding.bluetooth.BluetoothAddress;
import org.openhab.binding.bluetooth.BluetoothCharacteristic;
import org.openhab.binding.bluetooth.BluetoothCompletionStatus;
import org.openhab.binding.bluetooth.BluetoothDescriptor;
import org.openhab.binding.bluetooth.BluetoothService;
import org.openhab.binding.bluetooth.bluez.internal.events.BlueZEvent;
@ -42,6 +41,8 @@ import org.openhab.binding.bluetooth.bluez.internal.events.ServicesResolvedEvent
import org.openhab.binding.bluetooth.bluez.internal.events.TXPowerEvent;
import org.openhab.binding.bluetooth.notification.BluetoothConnectionStatusNotification;
import org.openhab.binding.bluetooth.notification.BluetoothScanNotification;
import org.openhab.binding.bluetooth.util.RetryException;
import org.openhab.binding.bluetooth.util.RetryFuture;
import org.openhab.core.common.ThreadPoolManager;
import org.openhab.core.util.HexUtils;
import org.slf4j.Logger;
@ -257,57 +258,65 @@ public class BlueZBluetoothDevice extends BaseBluetoothDevice implements BlueZEv
}
@Override
public boolean enableNotifications(BluetoothCharacteristic characteristic) {
ensureConnected();
BluetoothGattCharacteristic c = getDBusBlueZCharacteristicByUUID(characteristic.getUuid().toString());
if (c != null) {
try {
c.startNotify();
} catch (DBusException e) {
if (e.getMessage().contains("Already notifying")) {
return false;
} else if (e.getMessage().contains("In Progress")) {
// let's retry in 10 seconds
scheduler.schedule(() -> enableNotifications(characteristic), 10, TimeUnit.SECONDS);
} else {
logger.warn("Exception occurred while activating notifications on '{}'", address, e);
}
}
return true;
} else {
logger.warn("Characteristic '{}' is missing on device '{}'.", characteristic.getUuid(), address);
return false;
public CompletableFuture<@Nullable Void> enableNotifications(BluetoothCharacteristic characteristic) {
BluetoothDevice dev = device;
if (dev == null || !dev.isConnected()) {
return CompletableFuture
.failedFuture(new IllegalStateException("DBusBlueZ device is not set or not connected"));
}
}
@Override
public boolean writeCharacteristic(BluetoothCharacteristic characteristic) {
logger.debug("writeCharacteristic()");
ensureConnected();
BluetoothGattCharacteristic c = getDBusBlueZCharacteristicByUUID(characteristic.getUuid().toString());
if (c == null) {
logger.warn("Characteristic '{}' is missing on device '{}'.", characteristic.getUuid(), address);
return false;
return CompletableFuture.failedFuture(
new IllegalStateException("Characteristic " + characteristic.getUuid() + " is missing on device"));
}
scheduler.submit(() -> {
return RetryFuture.callWithRetry(() -> {
try {
c.writeValue(characteristic.getByteValue(), null);
notifyListeners(BluetoothEventType.CHARACTERISTIC_WRITE_COMPLETE, characteristic,
BluetoothCompletionStatus.SUCCESS);
c.startNotify();
} catch (DBusException e) {
if (e.getMessage().contains("Already notifying")) {
return null;
} else if (e.getMessage().contains("In Progress")) {
// let's retry in half a second
throw new RetryException(500, TimeUnit.MILLISECONDS);
} else {
logger.warn("Exception occurred while activating notifications on '{}'", address, e);
throw e;
}
}
return null;
}, scheduler);
}
@Override
public CompletableFuture<@Nullable Void> writeCharacteristic(BluetoothCharacteristic characteristic, byte[] value) {
logger.debug("writeCharacteristic()");
BluetoothDevice dev = device;
if (dev == null || !dev.isConnected()) {
return CompletableFuture
.failedFuture(new IllegalStateException("DBusBlueZ device is not set or not connected"));
}
BluetoothGattCharacteristic c = getDBusBlueZCharacteristicByUUID(characteristic.getUuid().toString());
if (c == null) {
logger.warn("Characteristic '{}' is missing on device '{}'.", characteristic.getUuid(), address);
return CompletableFuture.failedFuture(
new IllegalStateException("Characteristic " + characteristic.getUuid() + " is missing on device"));
}
return RetryFuture.callWithRetry(() -> {
try {
c.writeValue(value, null);
return null;
} catch (DBusException e) {
logger.debug("Exception occurred when trying to write characteristic '{}': {}",
characteristic.getUuid(), e.getMessage());
notifyListeners(BluetoothEventType.CHARACTERISTIC_WRITE_COMPLETE, characteristic,
BluetoothCompletionStatus.ERROR);
throw e;
}
});
return true;
}, scheduler);
}
@Override
@ -364,10 +373,7 @@ public class BlueZBluetoothDevice extends BaseBluetoothDevice implements BlueZEv
}
BluetoothCharacteristic c = getCharacteristic(UUID.fromString(characteristic.getUuid()));
if (c != null) {
synchronized (c) {
c.setValue(event.getData());
notifyListeners(BluetoothEventType.CHARACTERISTIC_UPDATED, c, BluetoothCompletionStatus.SUCCESS);
}
notifyListeners(BluetoothEventType.CHARACTERISTIC_UPDATED, c, event.getData());
}
}
@ -453,28 +459,30 @@ public class BlueZBluetoothDevice extends BaseBluetoothDevice implements BlueZEv
}
@Override
public boolean readCharacteristic(BluetoothCharacteristic characteristic) {
public CompletableFuture<byte[]> readCharacteristic(BluetoothCharacteristic characteristic) {
BluetoothDevice dev = device;
if (dev == null || !dev.isConnected()) {
return CompletableFuture
.failedFuture(new IllegalStateException("DBusBlueZ device is not set or not connected"));
}
BluetoothGattCharacteristic c = getDBusBlueZCharacteristicByUUID(characteristic.getUuid().toString());
if (c == null) {
logger.warn("Characteristic '{}' is missing on device '{}'.", characteristic.getUuid(), address);
return false;
return CompletableFuture.failedFuture(
new IllegalStateException("Characteristic " + characteristic.getUuid() + " is missing on device"));
}
scheduler.submit(() -> {
return RetryFuture.callWithRetry(() -> {
try {
byte[] value = c.readValue(null);
characteristic.setValue(value);
notifyListeners(BluetoothEventType.CHARACTERISTIC_READ_COMPLETE, characteristic,
BluetoothCompletionStatus.SUCCESS);
return c.readValue(null);
} catch (DBusException | DBusExecutionException e) {
// DBusExecutionException is thrown if the value cannot be read
logger.debug("Exception occurred when trying to read characteristic '{}': {}", characteristic.getUuid(),
e.getMessage());
notifyListeners(BluetoothEventType.CHARACTERISTIC_READ_COMPLETE, characteristic,
BluetoothCompletionStatus.ERROR);
throw e;
}
});
return true;
}, scheduler);
}
@Override
@ -490,24 +498,35 @@ public class BlueZBluetoothDevice extends BaseBluetoothDevice implements BlueZEv
}
@Override
public boolean disableNotifications(BluetoothCharacteristic characteristic) {
public CompletableFuture<@Nullable Void> disableNotifications(BluetoothCharacteristic characteristic) {
BluetoothDevice dev = device;
if (dev == null || !dev.isConnected()) {
return CompletableFuture
.failedFuture(new IllegalStateException("DBusBlueZ device is not set or not connected"));
}
BluetoothGattCharacteristic c = getDBusBlueZCharacteristicByUUID(characteristic.getUuid().toString());
if (c != null) {
if (c == null) {
logger.warn("Characteristic '{}' is missing on device '{}'.", characteristic.getUuid(), address);
return CompletableFuture.failedFuture(
new IllegalStateException("Characteristic " + characteristic.getUuid() + " is missing on device"));
}
return RetryFuture.callWithRetry(() -> {
try {
c.stopNotify();
} catch (BluezFailedException e) {
if (e.getMessage().contains("In Progress")) {
// let's retry in 10 seconds
scheduler.schedule(() -> disableNotifications(characteristic), 10, TimeUnit.SECONDS);
} catch (DBusException e) {
if (e.getMessage().contains("Already notifying")) {
return null;
} else if (e.getMessage().contains("In Progress")) {
// let's retry in half a second
throw new RetryException(500, TimeUnit.MILLISECONDS);
} else {
logger.warn("Exception occurred while activating notifications on '{}'", address, e);
logger.warn("Exception occurred while deactivating notifications on '{}'", address, e);
throw e;
}
}
return true;
} else {
logger.warn("Characteristic '{}' is missing on device '{}'.", characteristic.getUuid(), address);
return false;
}
return null;
}, scheduler);
}
@Override

@ -12,13 +12,14 @@
*/
package org.openhab.binding.bluetooth.daikinmadoka.handler;
import java.util.Arrays;
import java.util.Random;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import javax.measure.quantity.Temperature;
import javax.measure.quantity.Time;
@ -27,7 +28,6 @@ import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.bluetooth.BluetoothCharacteristic;
import org.openhab.binding.bluetooth.BluetoothCompletionStatus;
import org.openhab.binding.bluetooth.BluetoothDevice.ConnectionState;
import org.openhab.binding.bluetooth.ConnectedBluetoothHandler;
import org.openhab.binding.bluetooth.daikinmadoka.DaikinMadokaBindingConstants;
@ -350,12 +350,12 @@ public class DaikinMadokaHandler extends ConnectedBluetoothHandler implements Re
}
@Override
public void onCharacteristicUpdate(BluetoothCharacteristic characteristic) {
public void onCharacteristicUpdate(BluetoothCharacteristic characteristic, byte[] msgBytes) {
if (logger.isDebugEnabled()) {
logger.debug("[{}] onCharacteristicUpdate({})", super.thing.getUID().getId(),
HexUtils.bytesToHex(characteristic.getByteValue()));
HexUtils.bytesToHex(msgBytes));
}
super.onCharacteristicUpdate(characteristic);
super.onCharacteristicUpdate(characteristic, msgBytes);
// Check that arguments are valid.
if (characteristic.getUuid() == null) {
@ -367,9 +367,8 @@ public class DaikinMadokaHandler extends ConnectedBluetoothHandler implements Re
return;
}
// A message cannot be null or have a 0-byte length
byte[] msgBytes = characteristic.getByteValue();
if (msgBytes == null || msgBytes.length == 0) {
// A message cannot have a 0-byte length
if (msgBytes.length == 0) {
return;
}
@ -398,7 +397,7 @@ public class DaikinMadokaHandler extends ConnectedBluetoothHandler implements Re
return;
}
if (!resolved) {
if (!device.isServicesDiscovered()) {
logger.debug("Unable to send command {} to device {}: services not resolved",
command.getClass().getSimpleName(), device.getAddress());
command.setState(BRC1HCommand.State.FAILED);
@ -424,17 +423,23 @@ public class DaikinMadokaHandler extends ConnectedBluetoothHandler implements Re
// Commands can be composed of multiple chunks
for (byte[] chunk : command.getRequest()) {
charWrite.setValue(chunk);
command.setState(BRC1HCommand.State.ENQUEUED);
for (int i = 0; i < DaikinMadokaBindingConstants.WRITE_CHARACTERISTIC_MAX_RETRIES; i++) {
if (device.writeCharacteristic(charWrite)) {
command.setState(BRC1HCommand.State.SENT);
synchronized (command) {
command.wait(100);
}
break;
try {
device.writeCharacteristic(charWrite, chunk).get(100, TimeUnit.MILLISECONDS);
} catch (InterruptedException ex) {
return;
} catch (ExecutionException ex) {
logger.debug("Error while writing message {}: {}", command.getClass().getSimpleName(),
ex.getMessage());
Thread.sleep(100);
continue;
} catch (TimeoutException ex) {
Thread.sleep(100);
continue;
}
Thread.sleep(100);
command.setState(BRC1HCommand.State.SENT);
break;
}
}
@ -459,39 +464,6 @@ public class DaikinMadokaHandler extends ConnectedBluetoothHandler implements Re
}
}
@Override
public void onCharacteristicWriteComplete(BluetoothCharacteristic characteristic,
BluetoothCompletionStatus status) {
super.onCharacteristicWriteComplete(characteristic, status);
byte[] request = characteristic.getByteValue();
BRC1HCommand command = currentCommand;
if (command != null) {
// last chunk:
byte[] lastChunk = command.getRequest()[command.getRequest().length - 1];
if (!Arrays.equals(request, lastChunk)) {
logger.debug("Write completed for a chunk, but not a complete command.");
synchronized (command) {
command.notify();
}
return;
}
switch (status) {
case SUCCESS:
command.setState(BRC1HCommand.State.SENT);
break;
case ERROR:
command.setState(BRC1HCommand.State.FAILED);
break;
}
} else {
if (logger.isDebugEnabled()) {
logger.debug("No command found that matches request {}", HexUtils.bytesToHex(request));
}
}
}
/**
* When the method is triggered, it means that all message chunks have been received, re-assembled in the right
* order and that the payload is ready to be processed.

@ -27,7 +27,6 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.bluetooth.BluetoothBindingConstants;
import org.openhab.binding.bluetooth.BluetoothCharacteristic;
import org.openhab.binding.bluetooth.BluetoothCompletionStatus;
import org.openhab.binding.bluetooth.BluetoothDevice.ConnectionState;
import org.openhab.binding.bluetooth.ConnectedBluetoothHandler;
import org.openhab.core.library.types.StringType;
@ -85,7 +84,7 @@ public class GenericBluetoothHandler extends ConnectedBluetoothHandler {
GenericBindingConfiguration config = getConfigAs(GenericBindingConfiguration.class);
readCharacteristicJob = scheduler.scheduleWithFixedDelay(() -> {
if (device.getConnectionState() == ConnectionState.CONNECTED) {
if (resolved) {
if (device.isServicesDiscovered()) {
handlerToChannels.forEach((charHandler, channelUids) -> {
// Only read the value manually if notification is not on.
// Also read it the first time before we activate notifications below.
@ -139,11 +138,9 @@ public class GenericBluetoothHandler extends ConnectedBluetoothHandler {
@Override
public void onServicesDiscovered() {
if (!resolved) {
resolved = true;
logger.trace("Service discovery completed for '{}'", address);
updateThingChannels();
}
super.onServicesDiscovered();
logger.trace("Service discovery completed for '{}'", address);
updateThingChannels();
}
@Override
@ -157,19 +154,9 @@ public class GenericBluetoothHandler extends ConnectedBluetoothHandler {
}
@Override
public void onCharacteristicReadComplete(BluetoothCharacteristic characteristic, BluetoothCompletionStatus status) {
super.onCharacteristicReadComplete(characteristic, status);
if (status == BluetoothCompletionStatus.SUCCESS) {
byte[] data = characteristic.getByteValue();
getCharacteristicHandler(characteristic).handleCharacteristicUpdate(data);
}
}
@Override
public void onCharacteristicUpdate(BluetoothCharacteristic characteristic) {
super.onCharacteristicUpdate(characteristic);
byte[] data = characteristic.getByteValue();
getCharacteristicHandler(characteristic).handleCharacteristicUpdate(data);
public void onCharacteristicUpdate(BluetoothCharacteristic characteristic, byte[] value) {
super.onCharacteristicUpdate(characteristic, value);
getCharacteristicHandler(characteristic).handleCharacteristicUpdate(value);
}
private void updateThingChannels() {
@ -207,13 +194,28 @@ public class GenericBluetoothHandler extends ConnectedBluetoothHandler {
return Objects.requireNonNull(charHandlers.computeIfAbsent(characteristic, CharacteristicHandler::new));
}
private boolean readCharacteristic(BluetoothCharacteristic characteristic) {
return device.readCharacteristic(characteristic);
private void readCharacteristic(BluetoothCharacteristic characteristic) {
readCharacteristic(characteristic.getService().getUuid(), characteristic.getUuid()).whenComplete((data, th) -> {
if (th != null) {
logger.warn("Could not read data from characteristic {} of device {}: {}", characteristic.getUuid(),
address, th.getMessage());
return;
}
if (data != null) {
getCharacteristicHandler(characteristic).handleCharacteristicUpdate(data);
}
});
}
private boolean writeCharacteristic(BluetoothCharacteristic characteristic, byte[] data) {
characteristic.setValue(data);
return device.writeCharacteristic(characteristic);
private void writeCharacteristic(BluetoothCharacteristic characteristic, byte[] data) {
writeCharacteristic(characteristic.getService().getUuid(), characteristic.getUuid(), data, false)
.whenComplete((r, th) -> {
if (th != null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"Could not write data to characteristic " + characteristic.getUuid() + ": "
+ th.getMessage());
}
});
}
private class CharacteristicHandler {
@ -253,10 +255,7 @@ public class GenericBluetoothHandler extends ConnectedBluetoothHandler {
} else if (state instanceof StringType) {
// unknown characteristic
byte[] data = HexUtils.hexToBytes(state.toString());
if (!writeCharacteristic(characteristic, data)) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"Could not write data to characteristic: " + characteristicUUID);
}
writeCharacteristic(characteristic, data);
}
} catch (RuntimeException ex) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
@ -275,10 +274,7 @@ public class GenericBluetoothHandler extends ConnectedBluetoothHandler {
BluetoothChannelUtils.updateHolder(gattParser, request, fieldName, state);
byte[] data = gattParser.serialize(request);
if (!writeCharacteristic(characteristic, data)) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"Could not write data to characteristic: " + characteristicUUID);
}
writeCharacteristic(characteristic, data);
} catch (NumberFormatException ex) {
logger.warn("Could not parse characteristic value: {} : {}", characteristicUUID, state, ex);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,

@ -25,6 +25,11 @@
<description>The frequency at which readable characteristics refreshed</description>
<default>30</default>
</parameter>
<parameter name="alwaysConnected" type="boolean">
<label>Connect Automatically</label>
<description>If enabled, will automatically connect to the device and reconnect if connection is lost.</description>
<default>true</default>
</parameter>
</config-description>
</thing-type>

@ -1,472 +0,0 @@
/**
* 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.govee.internal;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
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.concurrent.locks.Condition;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.bluetooth.BeaconBluetoothHandler;
import org.openhab.binding.bluetooth.BluetoothCharacteristic;
import org.openhab.binding.bluetooth.BluetoothCompletionStatus;
import org.openhab.binding.bluetooth.BluetoothDescriptor;
import org.openhab.binding.bluetooth.BluetoothDevice.ConnectionState;
import org.openhab.binding.bluetooth.BluetoothService;
import org.openhab.binding.bluetooth.notification.BluetoothConnectionStatusNotification;
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.util.HexUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This is a base implementation for more specific thing handlers that require constant connection to bluetooth devices.
*
* @author Kai Kreuzer - Initial contribution and API
* @deprecated once CompletableFutures are supported in the actual ConnectedBluetoothHandler, this class can be deleted
*/
@Deprecated
@NonNullByDefault
public class ConnectedBluetoothHandler extends BeaconBluetoothHandler {
private final Logger logger = LoggerFactory.getLogger(ConnectedBluetoothHandler.class);
private final Condition connectionCondition = deviceLock.newCondition();
private final Condition serviceDiscoveryCondition = deviceLock.newCondition();
private final Condition charCompleteCondition = deviceLock.newCondition();
private @Nullable Future<?> reconnectJob;
private @Nullable Future<?> pendingDisconnect;
private @Nullable BluetoothCharacteristic ongoingCharacteristic;
private @Nullable BluetoothCompletionStatus completeStatus;
private boolean connectOnDemand;
private int idleDisconnectDelayMs = 1000;
protected @Nullable ScheduledExecutorService connectionTaskExecutor;
private volatile boolean servicesDiscovered;
public ConnectedBluetoothHandler(Thing thing) {
super(thing);
}
@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();
connectOnDemand = true;
Object idleDisconnectDelayRaw = getConfig().get("idleDisconnectDelay");
idleDisconnectDelayMs = 1000;
if (idleDisconnectDelayRaw instanceof Number) {
idleDisconnectDelayMs = ((Number) idleDisconnectDelayRaw).intValue();
}
if (!connectOnDemand) {
reconnectJob = executor.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 (!servicesDiscovered && !device.discoverServices()) {
logger.debug("Error while discovering services");
}
}
} catch (RuntimeException ex) {
logger.warn("Unexpected error occurred", ex);
}
}, 0, 30, TimeUnit.SECONDS);
}
}
@Override
public void dispose() {
cancel(reconnectJob);
reconnectJob = null;
cancel(pendingDisconnect);
pendingDisconnect = null;
super.dispose();
shutdown(connectionTaskExecutor);
connectionTaskExecutor = null;
}
private static void cancel(@Nullable Future<?> future) {
if (future != null) {
future.cancel(true);
}
}
private void shutdown(@Nullable ScheduledExecutorService executor) {
if (executor != null) {
executor.shutdownNow();
}
}
private ScheduledExecutorService getConnectionTaskExecutor() {
var executor = connectionTaskExecutor;
if (executor == null) {
throw new IllegalStateException("characteristicScheduler has not been initialized");
}
return executor;
}
private void scheduleDisconnect() {
cancel(pendingDisconnect);
pendingDisconnect = getConnectionTaskExecutor().schedule(device::disconnect, idleDisconnectDelayMs,
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");
}
}
logger.debug("waiting for connection");
if (!awaitConnection(1, TimeUnit.SECONDS)) {
throw new TimeoutException("Connection attempt timeout.");
}
logger.debug("connection successful");
if (!servicesDiscovered) {
logger.debug("discovering services");
device.discoverServices();
if (!awaitServiceDiscovery(20, TimeUnit.SECONDS)) {
throw new TimeoutException("Service discovery timeout");
}
logger.debug("service discovery successful");
}
}
private boolean awaitConnection(long timeout, TimeUnit unit) throws InterruptedException {
deviceLock.lock();
try {
long nanosTimeout = unit.toNanos(timeout);
while (device.getConnectionState() != ConnectionState.CONNECTED) {
if (nanosTimeout <= 0L) {
return false;
}
nanosTimeout = connectionCondition.awaitNanos(nanosTimeout);
}
} finally {
deviceLock.unlock();
}
return true;
}
private boolean awaitCharacteristicComplete(long timeout, TimeUnit unit) throws InterruptedException {
deviceLock.lock();
try {
long nanosTimeout = unit.toNanos(timeout);
while (ongoingCharacteristic != null) {
if (nanosTimeout <= 0L) {
return false;
}
nanosTimeout = charCompleteCondition.awaitNanos(nanosTimeout);
}
} finally {
deviceLock.unlock();
}
return true;
}
private 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;
}
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,
CallableFunction<BluetoothCharacteristic, T> callable) {
CompletableFuture<T> future = new CompletableFuture<>();
var executor = connectionTaskExecutor;
if (executor != null) {
executor.execute(() -> {
cancel(pendingDisconnect);
try {
BluetoothCharacteristic characteristic = connectAndGetCharacteristic(serviceUUID,
characteristicUUID);
future.complete(callable.call(characteristic));
} catch (InterruptedException e) {
future.completeExceptionally(e);
return;// we don't want to schedule anything if we receive an interrupt
} catch (TimeoutException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
future.completeExceptionally(e);
} catch (Exception e) {
future.completeExceptionally(e);
}
if (connectOnDemand) {
scheduleDisconnect();
}
});
} else {
future.completeExceptionally(new IllegalStateException("characteristicScheduler has not been initialized"));
}
return future;
}
public CompletableFuture<@Nullable Void> enableNotifications(UUID serviceUUID, UUID characteristicUUID) {
return executeWithConnection(serviceUUID, characteristicUUID, characteristic -> {
if (!device.enableNotifications(characteristic)) {
throw new BluetoothException(
"Failed to start notifications for characteristic: " + characteristic.getUuid());
}
return null;
});
}
public CompletableFuture<@Nullable Void> writeCharacteristic(UUID serviceUUID, UUID characteristicUUID, byte[] data,
boolean enableNotification) {
return executeWithConnection(serviceUUID, characteristicUUID, characteristic -> {
if (enableNotification) {
if (!device.enableNotifications(characteristic)) {
throw new BluetoothException(
"Failed to start characteristic notification" + characteristic.getUuid());
}
}
// now block for completion
characteristic.setValue(data);
ongoingCharacteristic = characteristic;
if (!device.writeCharacteristic(characteristic)) {
throw new BluetoothException("Failed to start writing characteristic " + characteristic.getUuid());
}
if (!awaitCharacteristicComplete(1, TimeUnit.SECONDS)) {
ongoingCharacteristic = null;
throw new TimeoutException(
"Timeout waiting for characteristic " + characteristic.getUuid() + " write to finish");
}
if (completeStatus == BluetoothCompletionStatus.ERROR) {
throw new BluetoothException("Failed to write characteristic " + characteristic.getUuid());
}
logger.debug("Wrote {} to characteristic {} of device {}", HexUtils.bytesToHex(data),
characteristic.getUuid(), address);
return null;
});
}
public CompletableFuture<byte[]> readCharacteristic(UUID serviceUUID, UUID characteristicUUID) {
return executeWithConnection(serviceUUID, characteristicUUID, characteristic -> {
// now block for completion
ongoingCharacteristic = characteristic;
if (!device.readCharacteristic(characteristic)) {
throw new BluetoothException("Failed to start reading characteristic " + characteristic.getUuid());
}
if (!awaitCharacteristicComplete(1, TimeUnit.SECONDS)) {
ongoingCharacteristic = null;
throw new TimeoutException(
"Timeout waiting for characteristic " + characteristic.getUuid() + " read to finish");
}
if (completeStatus == BluetoothCompletionStatus.ERROR) {
throw new BluetoothException("Failed to read characteristic " + characteristic.getUuid());
}
byte[] data = characteristic.getByteValue();
logger.debug("Characteristic {} from {} has been read - value {}", characteristic.getUuid(), address,
HexUtils.bytesToHex(data));
return data;
});
}
@Override
protected void updateStatusBasedOnRssi(boolean receivedSignal) {
// 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 {
if (!connectOnDemand) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "Device is not connected.");
}
}
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
}
}
@Override
public void onConnectionStateChange(BluetoothConnectionStatusNotification connectionNotification) {
super.onConnectionStateChange(connectionNotification);
switch (connectionNotification.getConnectionState()) {
case DISCOVERED:
// The device is now known on the Bluetooth network, so we can do something...
if (!connectOnDemand) {
getConnectionTaskExecutor().submit(() -> {
if (device.getConnectionState() != ConnectionState.CONNECTED) {
if (!device.connect()) {
logger.debug("Error connecting to device after discovery.");
}
}
});
}
break;
case CONNECTED:
deviceLock.lock();
try {
connectionCondition.signal();
} finally {
deviceLock.unlock();
}
if (!connectOnDemand) {
getConnectionTaskExecutor().submit(() -> {
if (!servicesDiscovered && !device.discoverServices()) {
logger.debug("Error while discovering services");
}
});
}
break;
case DISCONNECTED:
var future = pendingDisconnect;
if (future != null) {
future.cancel(false);
}
if (!connectOnDemand) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
}
break;
default:
break;
}
}
@Override
public void onCharacteristicReadComplete(BluetoothCharacteristic characteristic, BluetoothCompletionStatus status) {
super.onCharacteristicReadComplete(characteristic, status);
deviceLock.lock();
try {
if (ongoingCharacteristic != null && ongoingCharacteristic.getUuid().equals(characteristic.getUuid())) {
completeStatus = status;
ongoingCharacteristic = null;
charCompleteCondition.signal();
}
} finally {
deviceLock.unlock();
}
}
@Override
public void onCharacteristicWriteComplete(BluetoothCharacteristic characteristic,
BluetoothCompletionStatus status) {
super.onCharacteristicWriteComplete(characteristic, status);
deviceLock.lock();
try {
if (ongoingCharacteristic != null && ongoingCharacteristic.getUuid().equals(characteristic.getUuid())) {
completeStatus = status;
ongoingCharacteristic = null;
charCompleteCondition.signal();
}
} finally {
deviceLock.unlock();
}
}
@Override
public void onServicesDiscovered() {
super.onServicesDiscovered();
deviceLock.lock();
try {
this.servicesDiscovered = true;
serviceDiscoveryCondition.signal();
} finally {
deviceLock.unlock();
}
logger.debug("Service discovery completed for '{}'", address);
}
@Override
public void onCharacteristicUpdate(BluetoothCharacteristic characteristic) {
super.onCharacteristicUpdate(characteristic);
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()),
descriptor.getUuid(), address);
}
}
public static class BluetoothException extends Exception {
public BluetoothException(String message) {
super(message);
}
}
public static class ConnectionException extends BluetoothException {
public ConnectionException(String message) {
super(message);
}
}
@FunctionalInterface
public static interface CallableFunction<U, R> {
public R call(U arg) throws Exception;
}
}

@ -33,6 +33,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.bluetooth.BluetoothCharacteristic;
import org.openhab.binding.bluetooth.BluetoothDevice.ConnectionState;
import org.openhab.binding.bluetooth.ConnectedBluetoothHandler;
import org.openhab.binding.bluetooth.gattserial.MessageServicer;
import org.openhab.binding.bluetooth.gattserial.SimpleGattSocket;
import org.openhab.binding.bluetooth.govee.internal.command.hygrometer.GetBatteryCommand;
@ -93,6 +94,11 @@ public class GoveeHygrometerHandler extends ConnectedBluetoothHandler {
@Override
public void initialize() {
super.initialize();
if (thing.getStatus() == ThingStatus.OFFLINE) {
// something went wrong in super.initialize() so we shouldn't initialize further here either
return;
}
config = getConfigAs(GoveeHygrometerConfiguration.class);
Map<String, String> properties = thing.getProperties();
@ -117,14 +123,14 @@ public class GoveeHygrometerHandler extends ConnectedBluetoothHandler {
logger.debug("refreshing temperature, humidity, and battery");
refreshBattery().join();
refreshTemperatureAndHumidity().join();
connectionTaskExecutor.execute(device::disconnect);
disconnect();
updateStatus(ThingStatus.ONLINE);
}
} catch (RuntimeException ex) {
logger.warn("unable to refresh", ex);
}
}, 0, config.refreshInterval, TimeUnit.SECONDS);
keepAliveJob = connectionTaskExecutor.scheduleWithFixedDelay(() -> {
keepAliveJob = scheduler.scheduleWithFixedDelay(() -> {
if (device.getConnectionState() == ConnectionState.CONNECTED) {
try {
GoveeMessage message = new GoveeMessage((byte) 0xAA, (byte) 1, null);
@ -393,9 +399,9 @@ public class GoveeHygrometerHandler extends ConnectedBluetoothHandler {
}
@Override
public void onCharacteristicUpdate(BluetoothCharacteristic characteristic) {
super.onCharacteristicUpdate(characteristic);
commandSocket.receivePacket(characteristic.getByteValue());
public void onCharacteristicUpdate(BluetoothCharacteristic characteristic, byte[] value) {
super.onCharacteristicUpdate(characteristic, value);
commandSocket.receivePacket(value);
}
private class CommandSocket extends SimpleGattSocket<GoveeMessage> {

@ -25,7 +25,6 @@ import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.bluetooth.BluetoothAdapter;
import org.openhab.binding.bluetooth.BluetoothAddress;
import org.openhab.binding.bluetooth.BluetoothCharacteristic;
import org.openhab.binding.bluetooth.BluetoothCompletionStatus;
import org.openhab.binding.bluetooth.BluetoothDescriptor;
import org.openhab.binding.bluetooth.BluetoothDevice;
import org.openhab.binding.bluetooth.BluetoothDeviceListener;
@ -135,32 +134,16 @@ public class RoamingBluetoothDevice extends DelegateBluetoothDevice {
}
@Override
public void onCharacteristicReadComplete(BluetoothCharacteristic characteristic,
BluetoothCompletionStatus status) {
public void onCharacteristicUpdate(BluetoothCharacteristic characteristic, byte[] value) {
if (device == getDelegate()) {
notifyListeners(BluetoothEventType.CHARACTERISTIC_READ_COMPLETE, characteristic, status);
notifyListeners(BluetoothEventType.CHARACTERISTIC_UPDATED, characteristic, value);
}
}
@Override
public void onCharacteristicWriteComplete(BluetoothCharacteristic characteristic,
BluetoothCompletionStatus status) {
public void onDescriptorUpdate(BluetoothDescriptor bluetoothDescriptor, byte[] value) {
if (device == getDelegate()) {
notifyListeners(BluetoothEventType.CHARACTERISTIC_WRITE_COMPLETE, characteristic);
}
}
@Override
public void onCharacteristicUpdate(BluetoothCharacteristic characteristic) {
if (device == getDelegate()) {
notifyListeners(BluetoothEventType.CHARACTERISTIC_UPDATED, characteristic);
}
}
@Override
public void onDescriptorUpdate(BluetoothDescriptor bluetoothDescriptor) {
if (device == getDelegate()) {
notifyListeners(BluetoothEventType.DESCRIPTOR_UPDATED, bluetoothDescriptor);
notifyListeners(BluetoothEventType.DESCRIPTOR_UPDATED, bluetoothDescriptor, value);
}
}

@ -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) {
}
}

@ -12,6 +12,7 @@
*/
package org.openhab.binding.bluetooth;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;
@ -73,14 +74,16 @@ public class MockBluetoothDevice extends BaseBluetoothDevice {
}
@Override
public boolean readCharacteristic(BluetoothCharacteristic characteristic) {
public CompletableFuture<byte[]> readCharacteristic(BluetoothCharacteristic characteristic) {
if (characteristic.getGattCharacteristic() == GattCharacteristic.DEVICE_NAME) {
characteristic.setValue(deviceName);
notifyListeners(BluetoothEventType.CHARACTERISTIC_READ_COMPLETE, characteristic,
BluetoothCompletionStatus.SUCCESS);
return true;
String name = deviceName;
if (name != null) {
return CompletableFuture.completedFuture(name.getBytes(StandardCharsets.UTF_8));
} else {
return CompletableFuture.completedFuture(new byte[0]);
}
}
return false;
return CompletableFuture.failedFuture(new UnsupportedOperationException());
}
public void setDeviceName(String deviceName) {
@ -93,13 +96,13 @@ public class MockBluetoothDevice extends BaseBluetoothDevice {
}
@Override
public boolean writeCharacteristic(BluetoothCharacteristic characteristic) {
return false;
public CompletableFuture<@Nullable Void> writeCharacteristic(BluetoothCharacteristic characteristic, byte[] value) {
return CompletableFuture.failedFuture(new UnsupportedOperationException());
}
@Override
public boolean enableNotifications(BluetoothCharacteristic characteristic) {
return false;
public CompletableFuture<@Nullable Void> enableNotifications(BluetoothCharacteristic characteristic) {
return CompletableFuture.failedFuture(new UnsupportedOperationException());
}
@Override
@ -108,8 +111,8 @@ public class MockBluetoothDevice extends BaseBluetoothDevice {
}
@Override
public boolean disableNotifications(BluetoothCharacteristic characteristic) {
return false;
public CompletableFuture<@Nullable Void> disableNotifications(BluetoothCharacteristic characteristic) {
return CompletableFuture.failedFuture(new UnsupportedOperationException());
}
@Override