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;
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();
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));
*/
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;
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;
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);
// 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)) {
}
@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) {
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;
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;
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<>();
// 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;
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;
}
};
@Override
public boolean discoverServices() {
- if (procedureProgress != BlueGigaProcedure.NONE) {
+ if (currentProcedure != PROCEDURE_NONE) {
return false;
}
}
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
} 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
}
@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
return;
}
- if (procedureProgress == BlueGigaProcedure.NONE) {
+ if (currentProcedure == PROCEDURE_NONE) {
logger.debug("BlueGiga procedure completed but procedure is null with connection {}, address {}",
connection, address);
return;
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;
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));
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);
- }
- }
-
- private static byte @Nullable [] toBytes(int @Nullable [] value) {
- if (value == null) {
- return null;
+ notifyListeners(BluetoothEventType.DESCRIPTOR_UPDATED, descriptor,
+ BluetoothUtils.toByteArray(event.getValue()));
}
- 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()));
cancelTimer(connectTimer);
cancelTimer(procedureTimer);
bgHandler.removeEventListener(this);
- procedureProgress = BlueGigaProcedure.NONE;
+ currentProcedure = PROCEDURE_NONE;
connectionState = ConnectionState.DISCOVERING;
connection = -1;
}
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;
+ }
+ }
}
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;
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;
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;
}
@Override
- public boolean enableNotifications(BluetoothCharacteristic characteristic) {
- ensureConnected();
+ 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"));
+ }
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.startNotify();
} catch (DBusException e) {
if (e.getMessage().contains("Already notifying")) {
- return false;
+ return null;
} else if (e.getMessage().contains("In Progress")) {
- // let's retry in 10 seconds
- scheduler.schedule(() -> enableNotifications(characteristic), 10, TimeUnit.SECONDS);
+ // 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 true;
- } else {
- logger.warn("Characteristic '{}' is missing on device '{}'.", characteristic.getUuid(), address);
- return false;
- }
+ return null;
+ }, scheduler);
}
@Override
- public boolean writeCharacteristic(BluetoothCharacteristic characteristic) {
+ public CompletableFuture<@Nullable Void> writeCharacteristic(BluetoothCharacteristic characteristic, byte[] value) {
logger.debug("writeCharacteristic()");
- ensureConnected();
+ 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 {
- c.writeValue(characteristic.getByteValue(), null);
- notifyListeners(BluetoothEventType.CHARACTERISTIC_WRITE_COMPLETE, characteristic,
- BluetoothCompletionStatus.SUCCESS);
-
+ 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
}
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());
}
}
}
@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
}
@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
*/
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;
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;
}
@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) {
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;
}
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);
// 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;
}
}
}
}
- @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.
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;
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.
@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
}
@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() {
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 {
} 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,
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,
<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>
+++ /dev/null
-/**
- * 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;
- }
-}
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;
@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();
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);
}
@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> {
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;
}
@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);
}
}
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;
*/
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
*
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) {
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);
}
}
}
- private void onActivity() {
+ protected void onActivity() {
this.lastActivityTime = ZonedDateTime.now();
}
}
@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();
}
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;
*/
package org.openhab.binding.bluetooth;
-import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
* @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;
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
*/
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);
}
protected final BluetoothCharacteristic characteristic;
protected final UUID uuid;
+
protected final int handle;
- protected byte[] value;
/**
* The main constructor
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);
}
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;
protected enum BluetoothEventType {
CONNECTION_STATE,
SCAN_RECORD,
- CHARACTERISTIC_READ_COMPLETE,
- CHARACTERISTIC_WRITE_COMPLETE,
CHARACTERISTIC_UPDATED,
DESCRIPTOR_UPDATED,
SERVICES_DISCOVERED,
* 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.
* @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.
* @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.
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
*
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]);
*/
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.
--- /dev/null
+/**
+ * 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);
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+ }
+}
*/
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;
*
* @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);
@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);
-
- // Handle REFRESH
- if (command == RefreshType.REFRESH) {
- for (BluetoothCharacteristic characteristic : deviceCharacteristics) {
- if (characteristic.getGattCharacteristic() != null
- && channelUID.getId().equals(characteristic.getGattCharacteristic().name())) {
- device.readCharacteristic(characteristic);
- break;
- }
+ private static void cancel(@Nullable Future<?> future, boolean interrupt) {
+ if (future != null) {
+ future.cancel(interrupt);
+ }
+ }
+
+ 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
// 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);
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;
}
@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);
- if (logger.isDebugEnabled()) {
- logger.debug("Wrote {} to characteristic {} of device {}: {}",
- HexUtils.bytesToHex(characteristic.getByteValue()), characteristic.getUuid(), address, status);
- }
- }
-
- @Override
- public void onCharacteristicUpdate(BluetoothCharacteristic characteristic) {
- super.onCharacteristicUpdate(characteristic);
+ public void onCharacteristicUpdate(BluetoothCharacteristic characteristic, byte[] value) {
+ super.onCharacteristicUpdate(characteristic, value);
if (logger.isDebugEnabled()) {
- logger.debug("Recieved update {} to characteristic {} of device {}",
- HexUtils.bytesToHex(characteristic.getByteValue()), characteristic.getUuid(), address);
+ logger.debug("Recieved update {} to characteristic {} of device {}", HexUtils.bytesToHex(value),
+ characteristic.getUuid(), address);
}
}
@Override
- public void onDescriptorUpdate(BluetoothDescriptor descriptor) {
- super.onDescriptorUpdate(descriptor);
+ public void onDescriptorUpdate(BluetoothDescriptor descriptor, byte[] value) {
+ super.onDescriptorUpdate(descriptor, value);
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);
}
}
--- /dev/null
+/**
+ * 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);
+ }
+}
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;
}
@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
}
@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
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();
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;
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;
* @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;
.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) {
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();
- }
- 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();
+ 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());
}
}
-
- @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) {
- }
}
*/
package org.openhab.binding.bluetooth;
+import java.nio.charset.StandardCharsets;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;
}
@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) {
}
@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
}
@Override
- public boolean disableNotifications(BluetoothCharacteristic characteristic) {
- return false;
+ public CompletableFuture<@Nullable Void> disableNotifications(BluetoothCharacteristic characteristic) {
+ return CompletableFuture.failedFuture(new UnsupportedOperationException());
}
@Override