[bluetooth] Changed characteristic read/write to use CompletableFutures (#8970)
Signed-off-by: Connor Petty <mistercpp2000+gitsignoff@gmail.com>
This commit is contained in:
parent
1822f77b07
commit
89d735bb0f
bundles
org.openhab.binding.bluetooth.airthings/src/main/java/org/openhab/binding/bluetooth/airthings/internal
org.openhab.binding.bluetooth.am43/src/main/java/org/openhab/binding/bluetooth/am43/internal
org.openhab.binding.bluetooth.bluegiga/src/main/java/org/openhab/binding/bluetooth/bluegiga
org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal
org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/handler
org.openhab.binding.bluetooth.generic/src/main
java/org/openhab/binding/bluetooth/generic/internal
resources/OH-INF/thing
org.openhab.binding.bluetooth.govee/src/main/java/org/openhab/binding/bluetooth/govee/internal
org.openhab.binding.bluetooth.roaming/src/main/java/org/openhab/binding/bluetooth/roaming/internal
org.openhab.binding.bluetooth/src
main/java/org/openhab/binding/bluetooth
BaseBluetoothDevice.javaBeaconBluetoothHandler.javaBluetoothBindingConstants.javaBluetoothCharacteristic.javaBluetoothDescriptor.javaBluetoothDevice.javaBluetoothDeviceListener.javaBluetoothException.javaBluetoothUtils.javaConnectedBluetoothHandler.javaConnectionException.javaDelegateBluetoothDevice.java
discovery/internal
test/java/org/openhab/binding/bluetooth
@ -24,8 +24,8 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.bluetooth.BeaconBluetoothHandler;
|
||||
import org.openhab.binding.bluetooth.BluetoothCharacteristic;
|
||||
import org.openhab.binding.bluetooth.BluetoothCompletionStatus;
|
||||
import org.openhab.binding.bluetooth.BluetoothDevice.ConnectionState;
|
||||
import org.openhab.binding.bluetooth.BluetoothUtils;
|
||||
import org.openhab.binding.bluetooth.notification.BluetoothConnectionStatusNotification;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
import org.openhab.core.library.unit.SIUnits;
|
||||
@ -156,8 +156,35 @@ public class AirthingsWavePlusHandler extends BeaconBluetoothHandler {
|
||||
case IDLE:
|
||||
logger.debug("Read data from device {}...", address);
|
||||
BluetoothCharacteristic characteristic = device.getCharacteristic(uuid);
|
||||
if (characteristic != null && device.readCharacteristic(characteristic)) {
|
||||
|
||||
if (characteristic != null) {
|
||||
readState = ReadState.READING;
|
||||
device.readCharacteristic(characteristic).whenComplete((data, ex) -> {
|
||||
try {
|
||||
if (data != null) {
|
||||
logger.debug("Characteristic {} from device {}: {}", characteristic.getUuid(),
|
||||
address, data);
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
sinceLastReadSec.set(0);
|
||||
try {
|
||||
updateChannels(
|
||||
new AirthingsWavePlusDataParser(BluetoothUtils.toIntArray(data)));
|
||||
} catch (AirthingsParserException e) {
|
||||
logger.warn(
|
||||
"Data parsing error occured, when parsing data from device {}, cause {}",
|
||||
address, e.getMessage(), e);
|
||||
}
|
||||
} else {
|
||||
logger.debug("Characteristic {} from device {} failed: {}",
|
||||
characteristic.getUuid(), address, ex.getMessage());
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
||||
ex.getMessage());
|
||||
}
|
||||
} finally {
|
||||
readState = ReadState.IDLE;
|
||||
disconnect();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
logger.debug("Read data from device {} failed", address);
|
||||
disconnect();
|
||||
@ -205,30 +232,6 @@ public class AirthingsWavePlusHandler extends BeaconBluetoothHandler {
|
||||
execute();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCharacteristicReadComplete(BluetoothCharacteristic characteristic, BluetoothCompletionStatus status) {
|
||||
try {
|
||||
if (status == BluetoothCompletionStatus.SUCCESS) {
|
||||
logger.debug("Characteristic {} from device {}: {}", characteristic.getUuid(), address,
|
||||
characteristic.getValue());
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
sinceLastReadSec.set(0);
|
||||
try {
|
||||
updateChannels(new AirthingsWavePlusDataParser(characteristic.getValue()));
|
||||
} catch (AirthingsParserException e) {
|
||||
logger.warn("Data parsing error occured, when parsing data from device {}, cause {}", address,
|
||||
e.getMessage(), e);
|
||||
}
|
||||
} else {
|
||||
logger.debug("Characteristic {} from device {} failed", characteristic.getUuid(), address);
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "No response from device");
|
||||
}
|
||||
} finally {
|
||||
readState = ReadState.IDLE;
|
||||
disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateChannels(AirthingsWavePlusDataParser parser) {
|
||||
logger.debug("Parsed data: {}", parser);
|
||||
updateState(CHANNEL_ID_HUMIDITY, QuantityType.valueOf(Double.valueOf(parser.getHumidity()), Units.PERCENT));
|
||||
|
@ -12,7 +12,6 @@
|
||||
*/
|
||||
package org.openhab.binding.bluetooth.am43.internal;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
@ -24,7 +23,6 @@ import javax.measure.quantity.Length;
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.bluetooth.BluetoothCharacteristic;
|
||||
import org.openhab.binding.bluetooth.BluetoothCompletionStatus;
|
||||
import org.openhab.binding.bluetooth.BluetoothDevice.ConnectionState;
|
||||
import org.openhab.binding.bluetooth.ConnectedBluetoothHandler;
|
||||
import org.openhab.binding.bluetooth.am43.internal.command.AM43Command;
|
||||
@ -164,7 +162,7 @@ public class AM43Handler extends ConnectedBluetoothHandler implements ResponseLi
|
||||
command.setState(AM43Command.State.FAILED);
|
||||
return;
|
||||
}
|
||||
if (!resolved) {
|
||||
if (!device.isServicesDiscovered()) {
|
||||
logger.debug("Unable to send command {} to device {}: services not resolved", command,
|
||||
device.getAddress());
|
||||
command.setState(AM43Command.State.FAILED);
|
||||
@ -180,9 +178,15 @@ public class AM43Handler extends ConnectedBluetoothHandler implements ResponseLi
|
||||
// there is no consequence to calling this as much as we like
|
||||
device.enableNotifications(characteristic);
|
||||
|
||||
characteristic.setValue(command.getRequest());
|
||||
command.setState(AM43Command.State.ENQUEUED);
|
||||
device.writeCharacteristic(characteristic);
|
||||
device.writeCharacteristic(characteristic, command.getRequest()).whenComplete((v, t) -> {
|
||||
if (t != null) {
|
||||
logger.debug("Failed to send command {}: {}", command.getClass().getSimpleName(), t.getMessage());
|
||||
command.setState(AM43Command.State.FAILED);
|
||||
} else {
|
||||
command.setState(AM43Command.State.SENT);
|
||||
}
|
||||
});
|
||||
|
||||
if (!command.awaitStateChange(getAM43Config().commandTimeout, TimeUnit.MILLISECONDS,
|
||||
AM43Command.State.SUCCEEDED, AM43Command.State.FAILED)) {
|
||||
@ -197,39 +201,8 @@ public class AM43Handler extends ConnectedBluetoothHandler implements ResponseLi
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCharacteristicWriteComplete(BluetoothCharacteristic characteristic,
|
||||
BluetoothCompletionStatus status) {
|
||||
super.onCharacteristicWriteComplete(characteristic, status);
|
||||
|
||||
byte[] request = characteristic.getByteValue();
|
||||
|
||||
AM43Command command = currentCommand;
|
||||
|
||||
if (command != null) {
|
||||
if (!Arrays.equals(request, command.getRequest())) {
|
||||
logger.debug("Write completed for unknown command");
|
||||
return;
|
||||
}
|
||||
switch (status) {
|
||||
case SUCCESS:
|
||||
command.setState(AM43Command.State.SENT);
|
||||
break;
|
||||
case ERROR:
|
||||
command.setState(AM43Command.State.FAILED);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("No command found that matches request {}", HexUtils.bytesToHex(request));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCharacteristicUpdate(BluetoothCharacteristic characteristic) {
|
||||
super.onCharacteristicUpdate(characteristic);
|
||||
|
||||
byte[] response = characteristic.getByteValue();
|
||||
public void onCharacteristicUpdate(BluetoothCharacteristic characteristic, byte[] response) {
|
||||
super.onCharacteristicUpdate(characteristic, response);
|
||||
|
||||
AM43Command command = currentCommand;
|
||||
if (command == null) {
|
||||
|
@ -19,9 +19,11 @@ import java.util.Map;
|
||||
import java.util.NavigableMap;
|
||||
import java.util.TreeMap;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
@ -29,10 +31,11 @@ import org.openhab.binding.bluetooth.BaseBluetoothDevice;
|
||||
import org.openhab.binding.bluetooth.BluetoothAddress;
|
||||
import org.openhab.binding.bluetooth.BluetoothBindingConstants;
|
||||
import org.openhab.binding.bluetooth.BluetoothCharacteristic;
|
||||
import org.openhab.binding.bluetooth.BluetoothCompletionStatus;
|
||||
import org.openhab.binding.bluetooth.BluetoothDescriptor;
|
||||
import org.openhab.binding.bluetooth.BluetoothDevice;
|
||||
import org.openhab.binding.bluetooth.BluetoothException;
|
||||
import org.openhab.binding.bluetooth.BluetoothService;
|
||||
import org.openhab.binding.bluetooth.BluetoothUtils;
|
||||
import org.openhab.binding.bluetooth.bluegiga.handler.BlueGigaBridgeHandler;
|
||||
import org.openhab.binding.bluetooth.bluegiga.internal.BlueGigaEventListener;
|
||||
import org.openhab.binding.bluetooth.bluegiga.internal.BlueGigaResponse;
|
||||
@ -66,6 +69,14 @@ public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements Blue
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(BlueGigaBluetoothDevice.class);
|
||||
|
||||
private static final BlueGigaProcedure PROCEDURE_NONE = new BlueGigaProcedure(BlueGigaProcedure.Type.NONE);
|
||||
private static final BlueGigaProcedure PROCEDURE_GET_SERVICES = new BlueGigaProcedure(
|
||||
BlueGigaProcedure.Type.GET_SERVICES);
|
||||
private static final BlueGigaProcedure PROCEDURE_GET_CHARACTERISTICS = new BlueGigaProcedure(
|
||||
BlueGigaProcedure.Type.GET_CHARACTERISTICS);
|
||||
private static final BlueGigaProcedure PROCEDURE_READ_CHARACTERISTIC_DECL = new BlueGigaProcedure(
|
||||
BlueGigaProcedure.Type.READ_CHARACTERISTIC_DECL);
|
||||
|
||||
private Map<Integer, UUID> handleToUUID = new HashMap<>();
|
||||
private NavigableMap<Integer, BlueGigaBluetoothCharacteristic> handleToCharacteristic = new TreeMap<>();
|
||||
|
||||
@ -75,22 +86,7 @@ public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements Blue
|
||||
// The dongle handler
|
||||
private final BlueGigaBridgeHandler bgHandler;
|
||||
|
||||
// An enum to use in the state machine for interacting with the device
|
||||
private enum BlueGigaProcedure {
|
||||
NONE,
|
||||
GET_SERVICES,
|
||||
GET_CHARACTERISTICS,
|
||||
READ_CHARACTERISTIC_DECL,
|
||||
CHARACTERISTIC_READ,
|
||||
CHARACTERISTIC_WRITE,
|
||||
NOTIFICATION_ENABLE,
|
||||
NOTIFICATION_DISABLE
|
||||
}
|
||||
|
||||
private BlueGigaProcedure procedureProgress = BlueGigaProcedure.NONE;
|
||||
|
||||
// Somewhere to remember what characteristic we're working on
|
||||
private @Nullable BluetoothCharacteristic procedureCharacteristic;
|
||||
private BlueGigaProcedure currentProcedure = PROCEDURE_NONE;
|
||||
|
||||
// The connection handle if the device is connected
|
||||
private int connection = -1;
|
||||
@ -113,9 +109,24 @@ public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements Blue
|
||||
private Runnable procedureTimeoutTask = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
logger.debug("Procedure {} timeout for device {}", procedureProgress, address);
|
||||
procedureProgress = BlueGigaProcedure.NONE;
|
||||
procedureCharacteristic = null;
|
||||
BlueGigaProcedure procedure = currentProcedure;
|
||||
logger.debug("Procedure {} timeout for device {}", procedure.type, address);
|
||||
switch (procedure.type) {
|
||||
case CHARACTERISTIC_READ:
|
||||
ReadCharacteristicProcedure readProcedure = (ReadCharacteristicProcedure) procedure;
|
||||
readProcedure.readFuture.completeExceptionally(new TimeoutException("Read characteristic "
|
||||
+ readProcedure.characteristic.getUuid() + " timeout for device " + address));
|
||||
break;
|
||||
case CHARACTERISTIC_WRITE:
|
||||
WriteCharacteristicProcedure writeProcedure = (WriteCharacteristicProcedure) procedure;
|
||||
writeProcedure.writeFuture.completeExceptionally(new TimeoutException("Write characteristic "
|
||||
+ writeProcedure.characteristic.getUuid() + " timeout for device " + address));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
currentProcedure = PROCEDURE_NONE;
|
||||
}
|
||||
};
|
||||
|
||||
@ -174,7 +185,7 @@ public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements Blue
|
||||
|
||||
@Override
|
||||
public boolean discoverServices() {
|
||||
if (procedureProgress != BlueGigaProcedure.NONE) {
|
||||
if (currentProcedure != PROCEDURE_NONE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -184,49 +195,45 @@ public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements Blue
|
||||
}
|
||||
|
||||
procedureTimer = startTimer(procedureTimeoutTask, TIMEOUT_SEC);
|
||||
procedureProgress = BlueGigaProcedure.GET_SERVICES;
|
||||
currentProcedure = PROCEDURE_GET_SERVICES;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean enableNotifications(BluetoothCharacteristic characteristic) {
|
||||
public CompletableFuture<@Nullable Void> enableNotifications(BluetoothCharacteristic characteristic) {
|
||||
if (connection == -1) {
|
||||
logger.debug("Cannot enable notifications, device not connected {}", this);
|
||||
return false;
|
||||
return CompletableFuture.failedFuture(new BluetoothException("Not connected"));
|
||||
}
|
||||
|
||||
BlueGigaBluetoothCharacteristic ch = (BlueGigaBluetoothCharacteristic) characteristic;
|
||||
if (ch.isNotifying()) {
|
||||
return true;
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
|
||||
BluetoothDescriptor descriptor = ch
|
||||
.getDescriptor(BluetoothDescriptor.GattDescriptor.CLIENT_CHARACTERISTIC_CONFIGURATION.getUUID());
|
||||
|
||||
if (descriptor == null || descriptor.getHandle() == 0) {
|
||||
logger.debug("unable to find CCC for characteristic {}", characteristic.getUuid());
|
||||
return false;
|
||||
return CompletableFuture.failedFuture(
|
||||
new BluetoothException("Unable to find CCC for characteristic [" + characteristic.getUuid() + "]"));
|
||||
}
|
||||
|
||||
if (procedureProgress != BlueGigaProcedure.NONE) {
|
||||
logger.debug("Procedure already in progress {}", procedureProgress);
|
||||
return false;
|
||||
if (currentProcedure != PROCEDURE_NONE) {
|
||||
return CompletableFuture.failedFuture(new BluetoothException("Another procedure is already in progress"));
|
||||
}
|
||||
|
||||
int[] value = { 1, 0 };
|
||||
byte[] bvalue = toBytes(value);
|
||||
descriptor.setValue(bvalue);
|
||||
|
||||
cancelTimer(procedureTimer);
|
||||
if (!bgHandler.bgWriteCharacteristic(connection, descriptor.getHandle(), value)) {
|
||||
logger.debug("bgWriteCharacteristic returned false");
|
||||
return false;
|
||||
return CompletableFuture.failedFuture(new BluetoothException(
|
||||
"Failed to write to CCC for characteristic [" + characteristic.getUuid() + "]"));
|
||||
}
|
||||
|
||||
procedureTimer = startTimer(procedureTimeoutTask, TIMEOUT_SEC);
|
||||
procedureProgress = BlueGigaProcedure.NOTIFICATION_ENABLE;
|
||||
procedureCharacteristic = characteristic;
|
||||
|
||||
WriteCharacteristicProcedure notifyProcedure = new WriteCharacteristicProcedure(ch,
|
||||
BlueGigaProcedure.Type.NOTIFICATION_ENABLE);
|
||||
currentProcedure = notifyProcedure;
|
||||
try {
|
||||
// we intentionally sleep here in order to give this procedure a chance to complete.
|
||||
// ideally we would use locks/conditions to make this wait until completiong but
|
||||
@ -235,57 +242,46 @@ public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements Blue
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
return true;
|
||||
return notifyProcedure.writeFuture;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean disableNotifications(BluetoothCharacteristic characteristic) {
|
||||
public CompletableFuture<@Nullable Void> disableNotifications(BluetoothCharacteristic characteristic) {
|
||||
if (connection == -1) {
|
||||
logger.debug("Cannot disable notifications, device not connected {}", this);
|
||||
return false;
|
||||
return CompletableFuture.failedFuture(new BluetoothException("Not connected"));
|
||||
}
|
||||
|
||||
BlueGigaBluetoothCharacteristic ch = (BlueGigaBluetoothCharacteristic) characteristic;
|
||||
if (!ch.isNotifying()) {
|
||||
return true;
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
|
||||
BluetoothDescriptor descriptor = ch
|
||||
.getDescriptor(BluetoothDescriptor.GattDescriptor.CLIENT_CHARACTERISTIC_CONFIGURATION.getUUID());
|
||||
|
||||
if (descriptor == null || descriptor.getHandle() == 0) {
|
||||
logger.debug("unable to find CCC for characteristic {}", characteristic.getUuid());
|
||||
return false;
|
||||
return CompletableFuture.failedFuture(
|
||||
new BluetoothException("Unable to find CCC for characteristic [" + characteristic.getUuid() + "]"));
|
||||
}
|
||||
|
||||
if (procedureProgress != BlueGigaProcedure.NONE) {
|
||||
logger.debug("Procedure already in progress {}", procedureProgress);
|
||||
return false;
|
||||
if (currentProcedure != PROCEDURE_NONE) {
|
||||
return CompletableFuture.failedFuture(new BluetoothException("Another procedure is already in progress"));
|
||||
}
|
||||
|
||||
int[] value = { 0, 0 };
|
||||
byte[] bvalue = toBytes(value);
|
||||
descriptor.setValue(bvalue);
|
||||
|
||||
cancelTimer(procedureTimer);
|
||||
if (!bgHandler.bgWriteCharacteristic(connection, descriptor.getHandle(), value)) {
|
||||
logger.debug("bgWriteCharacteristic returned false");
|
||||
return false;
|
||||
return CompletableFuture.failedFuture(new BluetoothException(
|
||||
"Failed to write to CCC for characteristic [" + characteristic.getUuid() + "]"));
|
||||
}
|
||||
|
||||
procedureTimer = startTimer(procedureTimeoutTask, TIMEOUT_SEC);
|
||||
procedureProgress = BlueGigaProcedure.NOTIFICATION_DISABLE;
|
||||
procedureCharacteristic = characteristic;
|
||||
WriteCharacteristicProcedure notifyProcedure = new WriteCharacteristicProcedure(ch,
|
||||
BlueGigaProcedure.Type.NOTIFICATION_DISABLE);
|
||||
currentProcedure = notifyProcedure;
|
||||
|
||||
try {
|
||||
// we intentionally sleep here in order to give this procedure a chance to complete.
|
||||
// ideally we would use locks/conditions to make this wait until completiong but
|
||||
// I have a better solution planned for later. - Connor Petty
|
||||
Thread.sleep(500);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
return true;
|
||||
return notifyProcedure.writeFuture;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -307,52 +303,56 @@ public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements Blue
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean readCharacteristic(@Nullable BluetoothCharacteristic characteristic) {
|
||||
if (characteristic == null || characteristic.getHandle() == 0) {
|
||||
return false;
|
||||
public CompletableFuture<byte[]> readCharacteristic(BluetoothCharacteristic characteristic) {
|
||||
if (characteristic.getHandle() == 0) {
|
||||
return CompletableFuture.failedFuture(new BluetoothException("Cannot read characteristic with no handle"));
|
||||
}
|
||||
if (connection == -1) {
|
||||
return false;
|
||||
return CompletableFuture.failedFuture(new BluetoothException("Not connected"));
|
||||
}
|
||||
|
||||
if (procedureProgress != BlueGigaProcedure.NONE) {
|
||||
return false;
|
||||
if (currentProcedure != PROCEDURE_NONE) {
|
||||
return CompletableFuture.failedFuture(new BluetoothException("Another procedure is already in progress"));
|
||||
}
|
||||
|
||||
cancelTimer(procedureTimer);
|
||||
if (!bgHandler.bgReadCharacteristic(connection, characteristic.getHandle())) {
|
||||
return false;
|
||||
return CompletableFuture.failedFuture(
|
||||
new BluetoothException("Failed to read characteristic [" + characteristic.getUuid() + "]"));
|
||||
}
|
||||
procedureTimer = startTimer(procedureTimeoutTask, TIMEOUT_SEC);
|
||||
procedureProgress = BlueGigaProcedure.CHARACTERISTIC_READ;
|
||||
procedureCharacteristic = characteristic;
|
||||
ReadCharacteristicProcedure readProcedure = new ReadCharacteristicProcedure(characteristic);
|
||||
currentProcedure = readProcedure;
|
||||
|
||||
return true;
|
||||
return readProcedure.readFuture;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean writeCharacteristic(@Nullable BluetoothCharacteristic characteristic) {
|
||||
if (characteristic == null || characteristic.getHandle() == 0) {
|
||||
return false;
|
||||
public CompletableFuture<@Nullable Void> writeCharacteristic(BluetoothCharacteristic characteristic, byte[] value) {
|
||||
if (characteristic.getHandle() == 0) {
|
||||
return CompletableFuture.failedFuture(new BluetoothException("Cannot write characteristic with no handle"));
|
||||
}
|
||||
if (connection == -1) {
|
||||
return false;
|
||||
return CompletableFuture.failedFuture(new BluetoothException("Not connected"));
|
||||
}
|
||||
|
||||
if (procedureProgress != BlueGigaProcedure.NONE) {
|
||||
return false;
|
||||
if (currentProcedure != PROCEDURE_NONE) {
|
||||
return CompletableFuture.failedFuture(new BluetoothException("Another procedure is already in progress"));
|
||||
}
|
||||
|
||||
cancelTimer(procedureTimer);
|
||||
if (!bgHandler.bgWriteCharacteristic(connection, characteristic.getHandle(), characteristic.getValue())) {
|
||||
return false;
|
||||
if (!bgHandler.bgWriteCharacteristic(connection, characteristic.getHandle(),
|
||||
BluetoothUtils.toIntArray(value))) {
|
||||
return CompletableFuture.failedFuture(
|
||||
new BluetoothException("Failed to write characteristic [" + characteristic.getUuid() + "]"));
|
||||
}
|
||||
|
||||
procedureTimer = startTimer(procedureTimeoutTask, TIMEOUT_SEC);
|
||||
procedureProgress = BlueGigaProcedure.CHARACTERISTIC_WRITE;
|
||||
procedureCharacteristic = characteristic;
|
||||
WriteCharacteristicProcedure writeProcedure = new WriteCharacteristicProcedure(
|
||||
(BlueGigaBluetoothCharacteristic) characteristic, BlueGigaProcedure.Type.CHARACTERISTIC_WRITE);
|
||||
currentProcedure = writeProcedure;
|
||||
|
||||
return true;
|
||||
return writeProcedure.writeFuture;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -564,7 +564,7 @@ public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements Blue
|
||||
return;
|
||||
}
|
||||
|
||||
if (procedureProgress == BlueGigaProcedure.NONE) {
|
||||
if (currentProcedure == PROCEDURE_NONE) {
|
||||
logger.debug("BlueGiga procedure completed but procedure is null with connection {}, address {}",
|
||||
connection, address);
|
||||
return;
|
||||
@ -574,63 +574,73 @@ public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements Blue
|
||||
updateLastSeenTime();
|
||||
|
||||
// The current procedure is now complete - move on...
|
||||
switch (procedureProgress) {
|
||||
switch (currentProcedure.type) {
|
||||
case GET_SERVICES:
|
||||
// We've downloaded all services, now get the characteristics
|
||||
if (bgHandler.bgFindCharacteristics(connection)) {
|
||||
procedureTimer = startTimer(procedureTimeoutTask, TIMEOUT_SEC);
|
||||
procedureProgress = BlueGigaProcedure.GET_CHARACTERISTICS;
|
||||
currentProcedure = PROCEDURE_GET_CHARACTERISTICS;
|
||||
} else {
|
||||
procedureProgress = BlueGigaProcedure.NONE;
|
||||
currentProcedure = PROCEDURE_NONE;
|
||||
}
|
||||
break;
|
||||
case GET_CHARACTERISTICS:
|
||||
// We've downloaded all attributes, now read the characteristic declarations
|
||||
if (bgHandler.bgReadCharacteristicDeclarations(connection)) {
|
||||
procedureTimer = startTimer(procedureTimeoutTask, TIMEOUT_SEC);
|
||||
procedureProgress = BlueGigaProcedure.READ_CHARACTERISTIC_DECL;
|
||||
currentProcedure = PROCEDURE_READ_CHARACTERISTIC_DECL;
|
||||
} else {
|
||||
procedureProgress = BlueGigaProcedure.NONE;
|
||||
currentProcedure = PROCEDURE_NONE;
|
||||
}
|
||||
break;
|
||||
case READ_CHARACTERISTIC_DECL:
|
||||
// We've downloaded read all the declarations, we are done now
|
||||
procedureProgress = BlueGigaProcedure.NONE;
|
||||
currentProcedure = PROCEDURE_NONE;
|
||||
notifyListeners(BluetoothEventType.SERVICES_DISCOVERED);
|
||||
break;
|
||||
case CHARACTERISTIC_READ:
|
||||
// The read failed
|
||||
notifyListeners(BluetoothEventType.CHARACTERISTIC_READ_COMPLETE, procedureCharacteristic,
|
||||
BluetoothCompletionStatus.ERROR);
|
||||
procedureProgress = BlueGigaProcedure.NONE;
|
||||
procedureCharacteristic = null;
|
||||
ReadCharacteristicProcedure readProcedure = (ReadCharacteristicProcedure) currentProcedure;
|
||||
readProcedure.readFuture.completeExceptionally(new BluetoothException(
|
||||
"Read characteristic failed: " + readProcedure.characteristic.getUuid()));
|
||||
currentProcedure = PROCEDURE_NONE;
|
||||
break;
|
||||
case CHARACTERISTIC_WRITE:
|
||||
// The write completed - failure or success
|
||||
BluetoothCompletionStatus result = event.getResult() == BgApiResponse.SUCCESS
|
||||
? BluetoothCompletionStatus.SUCCESS
|
||||
: BluetoothCompletionStatus.ERROR;
|
||||
notifyListeners(BluetoothEventType.CHARACTERISTIC_WRITE_COMPLETE, procedureCharacteristic, result);
|
||||
procedureProgress = BlueGigaProcedure.NONE;
|
||||
procedureCharacteristic = null;
|
||||
WriteCharacteristicProcedure writeProcedure = (WriteCharacteristicProcedure) currentProcedure;
|
||||
if (event.getResult() == BgApiResponse.SUCCESS) {
|
||||
writeProcedure.writeFuture.complete(null);
|
||||
} else {
|
||||
writeProcedure.writeFuture.completeExceptionally(new BluetoothException(
|
||||
"Write characteristic failed: " + writeProcedure.characteristic.getUuid()));
|
||||
}
|
||||
currentProcedure = PROCEDURE_NONE;
|
||||
break;
|
||||
case NOTIFICATION_ENABLE:
|
||||
WriteCharacteristicProcedure notifyEnableProcedure = (WriteCharacteristicProcedure) currentProcedure;
|
||||
boolean success = event.getResult() == BgApiResponse.SUCCESS;
|
||||
if (!success) {
|
||||
logger.debug("write to descriptor failed");
|
||||
if (success) {
|
||||
notifyEnableProcedure.writeFuture.complete(null);
|
||||
} else {
|
||||
notifyEnableProcedure.writeFuture
|
||||
.completeExceptionally(new BluetoothException("Enable characteristic notification failed: "
|
||||
+ notifyEnableProcedure.characteristic.getUuid()));
|
||||
}
|
||||
((BlueGigaBluetoothCharacteristic) procedureCharacteristic).setNotifying(success);
|
||||
procedureProgress = BlueGigaProcedure.NONE;
|
||||
procedureCharacteristic = null;
|
||||
notifyEnableProcedure.characteristic.setNotifying(success);
|
||||
currentProcedure = PROCEDURE_NONE;
|
||||
break;
|
||||
case NOTIFICATION_DISABLE:
|
||||
WriteCharacteristicProcedure notifyDisableProcedure = (WriteCharacteristicProcedure) currentProcedure;
|
||||
success = event.getResult() == BgApiResponse.SUCCESS;
|
||||
if (!success) {
|
||||
logger.debug("write to descriptor failed");
|
||||
if (success) {
|
||||
notifyDisableProcedure.writeFuture.complete(null);
|
||||
} else {
|
||||
notifyDisableProcedure.writeFuture
|
||||
.completeExceptionally(new BluetoothException("Disable characteristic notification failed: "
|
||||
+ notifyDisableProcedure.characteristic.getUuid()));
|
||||
}
|
||||
((BlueGigaBluetoothCharacteristic) procedureCharacteristic).setNotifying(!success);
|
||||
procedureProgress = BlueGigaProcedure.NONE;
|
||||
procedureCharacteristic = null;
|
||||
notifyDisableProcedure.characteristic.setNotifying(!success);
|
||||
currentProcedure = PROCEDURE_NONE;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@ -668,7 +678,23 @@ public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements Blue
|
||||
cancelTimer(procedureTimer);
|
||||
connectionState = ConnectionState.DISCONNECTED;
|
||||
connection = -1;
|
||||
procedureProgress = BlueGigaProcedure.NONE;
|
||||
|
||||
BlueGigaProcedure procedure = currentProcedure;
|
||||
switch (procedure.type) {
|
||||
case CHARACTERISTIC_READ:
|
||||
ReadCharacteristicProcedure readProcedure = (ReadCharacteristicProcedure) procedure;
|
||||
readProcedure.readFuture.completeExceptionally(new BluetoothException("Read characteristic "
|
||||
+ readProcedure.characteristic.getUuid() + " failed due to disconnect of device " + address));
|
||||
break;
|
||||
case CHARACTERISTIC_WRITE:
|
||||
WriteCharacteristicProcedure writeProcedure = (WriteCharacteristicProcedure) procedure;
|
||||
writeProcedure.writeFuture.completeExceptionally(new BluetoothException("Write characteristic "
|
||||
+ writeProcedure.characteristic.getUuid() + " failed due to disconnect of device " + address));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
currentProcedure = PROCEDURE_NONE;
|
||||
|
||||
notifyListeners(BluetoothEventType.CONNECTION_STATE,
|
||||
new BluetoothConnectionStatusNotification(connectionState));
|
||||
@ -707,42 +733,30 @@ public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements Blue
|
||||
return;
|
||||
}
|
||||
if (handle == characteristic.getHandle()) {
|
||||
characteristic.setValue(event.getValue().clone());
|
||||
|
||||
byte[] value = BluetoothUtils.toByteArray(event.getValue());
|
||||
BlueGigaProcedure procedure = currentProcedure;
|
||||
// If this is the characteristic we were reading, then send a read completion
|
||||
if (procedureProgress == BlueGigaProcedure.CHARACTERISTIC_READ && procedureCharacteristic != null
|
||||
&& procedureCharacteristic.getHandle() == event.getAttHandle()) {
|
||||
procedureProgress = BlueGigaProcedure.NONE;
|
||||
procedureCharacteristic = null;
|
||||
notifyListeners(BluetoothEventType.CHARACTERISTIC_READ_COMPLETE, characteristic,
|
||||
BluetoothCompletionStatus.SUCCESS);
|
||||
return;
|
||||
if (procedure.type == BlueGigaProcedure.Type.CHARACTERISTIC_READ) {
|
||||
ReadCharacteristicProcedure readProcedure = (ReadCharacteristicProcedure) currentProcedure;
|
||||
if (readProcedure.characteristic.getHandle() == event.getAttHandle()) {
|
||||
readProcedure.readFuture.complete(value);
|
||||
currentProcedure = PROCEDURE_NONE;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Notify the user of the updated value
|
||||
notifyListeners(BluetoothEventType.CHARACTERISTIC_UPDATED, characteristic);
|
||||
notifyListeners(BluetoothEventType.CHARACTERISTIC_UPDATED, characteristic, value);
|
||||
} else {
|
||||
// it must be one of the descriptors we need to update
|
||||
UUID attUUID = handleToUUID.get(handle);
|
||||
BluetoothDescriptor descriptor = characteristic.getDescriptor(attUUID);
|
||||
descriptor.setValue(toBytes(event.getValue()));
|
||||
notifyListeners(BluetoothEventType.DESCRIPTOR_UPDATED, descriptor);
|
||||
notifyListeners(BluetoothEventType.DESCRIPTOR_UPDATED, descriptor,
|
||||
BluetoothUtils.toByteArray(event.getValue()));
|
||||
}
|
||||
}
|
||||
|
||||
private static byte @Nullable [] toBytes(int @Nullable [] value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
byte[] ret = new byte[value.length];
|
||||
for (int i = 0; i < value.length; i++) {
|
||||
ret[i] = (byte) value[i];
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
private boolean parseDeclaration(BlueGigaBluetoothCharacteristic ch, int[] value) {
|
||||
ByteBuffer buffer = ByteBuffer.wrap(toBytes(value));
|
||||
ByteBuffer buffer = ByteBuffer.wrap(BluetoothUtils.toByteArray(value));
|
||||
buffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
ch.setProperties(Byte.toUnsignedInt(buffer.get()));
|
||||
@ -779,7 +793,7 @@ public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements Blue
|
||||
cancelTimer(connectTimer);
|
||||
cancelTimer(procedureTimer);
|
||||
bgHandler.removeEventListener(this);
|
||||
procedureProgress = BlueGigaProcedure.NONE;
|
||||
currentProcedure = PROCEDURE_NONE;
|
||||
connectionState = ConnectionState.DISCOVERING;
|
||||
connection = -1;
|
||||
}
|
||||
@ -793,4 +807,48 @@ public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements Blue
|
||||
private ScheduledFuture<?> startTimer(Runnable command, long timeout) {
|
||||
return scheduler.schedule(command, timeout, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
private static class BlueGigaProcedure {
|
||||
private final Type type;
|
||||
|
||||
public BlueGigaProcedure(Type type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
// An enum to use in the state machine for interacting with the device
|
||||
enum Type {
|
||||
NONE,
|
||||
GET_SERVICES,
|
||||
GET_CHARACTERISTICS,
|
||||
READ_CHARACTERISTIC_DECL,
|
||||
CHARACTERISTIC_READ,
|
||||
CHARACTERISTIC_WRITE,
|
||||
NOTIFICATION_ENABLE,
|
||||
NOTIFICATION_DISABLE
|
||||
}
|
||||
}
|
||||
|
||||
private static class ReadCharacteristicProcedure extends BlueGigaProcedure {
|
||||
|
||||
private final BluetoothCharacteristic characteristic;
|
||||
|
||||
private final CompletableFuture<byte[]> readFuture = new CompletableFuture<>();
|
||||
|
||||
public ReadCharacteristicProcedure(BluetoothCharacteristic characteristic) {
|
||||
super(Type.CHARACTERISTIC_READ);
|
||||
this.characteristic = characteristic;
|
||||
}
|
||||
}
|
||||
|
||||
private static class WriteCharacteristicProcedure extends BlueGigaProcedure {
|
||||
|
||||
private final BlueGigaBluetoothCharacteristic characteristic;
|
||||
|
||||
private final CompletableFuture<@Nullable Void> writeFuture = new CompletableFuture<>();
|
||||
|
||||
public WriteCharacteristicProcedure(BlueGigaBluetoothCharacteristic characteristic, Type type) {
|
||||
super(type);
|
||||
this.characteristic = characteristic;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,10 +15,10 @@ package org.openhab.binding.bluetooth.bluez.internal;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.bluez.exceptions.BluezFailedException;
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.freedesktop.dbus.errors.NoReply;
|
||||
@ -28,7 +28,6 @@ import org.freedesktop.dbus.types.UInt16;
|
||||
import org.openhab.binding.bluetooth.BaseBluetoothDevice;
|
||||
import org.openhab.binding.bluetooth.BluetoothAddress;
|
||||
import org.openhab.binding.bluetooth.BluetoothCharacteristic;
|
||||
import org.openhab.binding.bluetooth.BluetoothCompletionStatus;
|
||||
import org.openhab.binding.bluetooth.BluetoothDescriptor;
|
||||
import org.openhab.binding.bluetooth.BluetoothService;
|
||||
import org.openhab.binding.bluetooth.bluez.internal.events.BlueZEvent;
|
||||
@ -42,6 +41,8 @@ import org.openhab.binding.bluetooth.bluez.internal.events.ServicesResolvedEvent
|
||||
import org.openhab.binding.bluetooth.bluez.internal.events.TXPowerEvent;
|
||||
import org.openhab.binding.bluetooth.notification.BluetoothConnectionStatusNotification;
|
||||
import org.openhab.binding.bluetooth.notification.BluetoothScanNotification;
|
||||
import org.openhab.binding.bluetooth.util.RetryException;
|
||||
import org.openhab.binding.bluetooth.util.RetryFuture;
|
||||
import org.openhab.core.common.ThreadPoolManager;
|
||||
import org.openhab.core.util.HexUtils;
|
||||
import org.slf4j.Logger;
|
||||
@ -257,57 +258,65 @@ public class BlueZBluetoothDevice extends BaseBluetoothDevice implements BlueZEv
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean enableNotifications(BluetoothCharacteristic characteristic) {
|
||||
ensureConnected();
|
||||
|
||||
BluetoothGattCharacteristic c = getDBusBlueZCharacteristicByUUID(characteristic.getUuid().toString());
|
||||
if (c != null) {
|
||||
|
||||
try {
|
||||
c.startNotify();
|
||||
} catch (DBusException e) {
|
||||
if (e.getMessage().contains("Already notifying")) {
|
||||
return false;
|
||||
} else if (e.getMessage().contains("In Progress")) {
|
||||
// let's retry in 10 seconds
|
||||
scheduler.schedule(() -> enableNotifications(characteristic), 10, TimeUnit.SECONDS);
|
||||
} else {
|
||||
logger.warn("Exception occurred while activating notifications on '{}'", address, e);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
logger.warn("Characteristic '{}' is missing on device '{}'.", characteristic.getUuid(), address);
|
||||
return false;
|
||||
public CompletableFuture<@Nullable Void> enableNotifications(BluetoothCharacteristic characteristic) {
|
||||
BluetoothDevice dev = device;
|
||||
if (dev == null || !dev.isConnected()) {
|
||||
return CompletableFuture
|
||||
.failedFuture(new IllegalStateException("DBusBlueZ device is not set or not connected"));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean writeCharacteristic(BluetoothCharacteristic characteristic) {
|
||||
logger.debug("writeCharacteristic()");
|
||||
|
||||
ensureConnected();
|
||||
|
||||
BluetoothGattCharacteristic c = getDBusBlueZCharacteristicByUUID(characteristic.getUuid().toString());
|
||||
if (c == null) {
|
||||
logger.warn("Characteristic '{}' is missing on device '{}'.", characteristic.getUuid(), address);
|
||||
return false;
|
||||
return CompletableFuture.failedFuture(
|
||||
new IllegalStateException("Characteristic " + characteristic.getUuid() + " is missing on device"));
|
||||
}
|
||||
|
||||
scheduler.submit(() -> {
|
||||
return RetryFuture.callWithRetry(() -> {
|
||||
try {
|
||||
c.writeValue(characteristic.getByteValue(), null);
|
||||
notifyListeners(BluetoothEventType.CHARACTERISTIC_WRITE_COMPLETE, characteristic,
|
||||
BluetoothCompletionStatus.SUCCESS);
|
||||
c.startNotify();
|
||||
} catch (DBusException e) {
|
||||
if (e.getMessage().contains("Already notifying")) {
|
||||
return null;
|
||||
} else if (e.getMessage().contains("In Progress")) {
|
||||
// let's retry in half a second
|
||||
throw new RetryException(500, TimeUnit.MILLISECONDS);
|
||||
} else {
|
||||
logger.warn("Exception occurred while activating notifications on '{}'", address, e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}, scheduler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<@Nullable Void> writeCharacteristic(BluetoothCharacteristic characteristic, byte[] value) {
|
||||
logger.debug("writeCharacteristic()");
|
||||
|
||||
BluetoothDevice dev = device;
|
||||
if (dev == null || !dev.isConnected()) {
|
||||
return CompletableFuture
|
||||
.failedFuture(new IllegalStateException("DBusBlueZ device is not set or not connected"));
|
||||
}
|
||||
|
||||
BluetoothGattCharacteristic c = getDBusBlueZCharacteristicByUUID(characteristic.getUuid().toString());
|
||||
if (c == null) {
|
||||
logger.warn("Characteristic '{}' is missing on device '{}'.", characteristic.getUuid(), address);
|
||||
return CompletableFuture.failedFuture(
|
||||
new IllegalStateException("Characteristic " + characteristic.getUuid() + " is missing on device"));
|
||||
}
|
||||
|
||||
return RetryFuture.callWithRetry(() -> {
|
||||
try {
|
||||
c.writeValue(value, null);
|
||||
return null;
|
||||
} catch (DBusException e) {
|
||||
logger.debug("Exception occurred when trying to write characteristic '{}': {}",
|
||||
characteristic.getUuid(), e.getMessage());
|
||||
notifyListeners(BluetoothEventType.CHARACTERISTIC_WRITE_COMPLETE, characteristic,
|
||||
BluetoothCompletionStatus.ERROR);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}, scheduler);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -364,10 +373,7 @@ public class BlueZBluetoothDevice extends BaseBluetoothDevice implements BlueZEv
|
||||
}
|
||||
BluetoothCharacteristic c = getCharacteristic(UUID.fromString(characteristic.getUuid()));
|
||||
if (c != null) {
|
||||
synchronized (c) {
|
||||
c.setValue(event.getData());
|
||||
notifyListeners(BluetoothEventType.CHARACTERISTIC_UPDATED, c, BluetoothCompletionStatus.SUCCESS);
|
||||
}
|
||||
notifyListeners(BluetoothEventType.CHARACTERISTIC_UPDATED, c, event.getData());
|
||||
}
|
||||
}
|
||||
|
||||
@ -453,28 +459,30 @@ public class BlueZBluetoothDevice extends BaseBluetoothDevice implements BlueZEv
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean readCharacteristic(BluetoothCharacteristic characteristic) {
|
||||
public CompletableFuture<byte[]> readCharacteristic(BluetoothCharacteristic characteristic) {
|
||||
BluetoothDevice dev = device;
|
||||
if (dev == null || !dev.isConnected()) {
|
||||
return CompletableFuture
|
||||
.failedFuture(new IllegalStateException("DBusBlueZ device is not set or not connected"));
|
||||
}
|
||||
|
||||
BluetoothGattCharacteristic c = getDBusBlueZCharacteristicByUUID(characteristic.getUuid().toString());
|
||||
if (c == null) {
|
||||
logger.warn("Characteristic '{}' is missing on device '{}'.", characteristic.getUuid(), address);
|
||||
return false;
|
||||
return CompletableFuture.failedFuture(
|
||||
new IllegalStateException("Characteristic " + characteristic.getUuid() + " is missing on device"));
|
||||
}
|
||||
|
||||
scheduler.submit(() -> {
|
||||
return RetryFuture.callWithRetry(() -> {
|
||||
try {
|
||||
byte[] value = c.readValue(null);
|
||||
characteristic.setValue(value);
|
||||
notifyListeners(BluetoothEventType.CHARACTERISTIC_READ_COMPLETE, characteristic,
|
||||
BluetoothCompletionStatus.SUCCESS);
|
||||
return c.readValue(null);
|
||||
} catch (DBusException | DBusExecutionException e) {
|
||||
// DBusExecutionException is thrown if the value cannot be read
|
||||
logger.debug("Exception occurred when trying to read characteristic '{}': {}", characteristic.getUuid(),
|
||||
e.getMessage());
|
||||
notifyListeners(BluetoothEventType.CHARACTERISTIC_READ_COMPLETE, characteristic,
|
||||
BluetoothCompletionStatus.ERROR);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}, scheduler);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -490,24 +498,35 @@ public class BlueZBluetoothDevice extends BaseBluetoothDevice implements BlueZEv
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean disableNotifications(BluetoothCharacteristic characteristic) {
|
||||
public CompletableFuture<@Nullable Void> disableNotifications(BluetoothCharacteristic characteristic) {
|
||||
BluetoothDevice dev = device;
|
||||
if (dev == null || !dev.isConnected()) {
|
||||
return CompletableFuture
|
||||
.failedFuture(new IllegalStateException("DBusBlueZ device is not set or not connected"));
|
||||
}
|
||||
BluetoothGattCharacteristic c = getDBusBlueZCharacteristicByUUID(characteristic.getUuid().toString());
|
||||
if (c != null) {
|
||||
if (c == null) {
|
||||
logger.warn("Characteristic '{}' is missing on device '{}'.", characteristic.getUuid(), address);
|
||||
return CompletableFuture.failedFuture(
|
||||
new IllegalStateException("Characteristic " + characteristic.getUuid() + " is missing on device"));
|
||||
}
|
||||
|
||||
return RetryFuture.callWithRetry(() -> {
|
||||
try {
|
||||
c.stopNotify();
|
||||
} catch (BluezFailedException e) {
|
||||
if (e.getMessage().contains("In Progress")) {
|
||||
// let's retry in 10 seconds
|
||||
scheduler.schedule(() -> disableNotifications(characteristic), 10, TimeUnit.SECONDS);
|
||||
} catch (DBusException e) {
|
||||
if (e.getMessage().contains("Already notifying")) {
|
||||
return null;
|
||||
} else if (e.getMessage().contains("In Progress")) {
|
||||
// let's retry in half a second
|
||||
throw new RetryException(500, TimeUnit.MILLISECONDS);
|
||||
} else {
|
||||
logger.warn("Exception occurred while activating notifications on '{}'", address, e);
|
||||
logger.warn("Exception occurred while deactivating notifications on '{}'", address, e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
logger.warn("Characteristic '{}' is missing on device '{}'.", characteristic.getUuid(), address);
|
||||
return false;
|
||||
}
|
||||
return null;
|
||||
}, scheduler);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -12,13 +12,14 @@
|
||||
*/
|
||||
package org.openhab.binding.bluetooth.daikinmadoka.handler;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import javax.measure.quantity.Temperature;
|
||||
import javax.measure.quantity.Time;
|
||||
@ -27,7 +28,6 @@ import org.eclipse.jdt.annotation.NonNull;
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.bluetooth.BluetoothCharacteristic;
|
||||
import org.openhab.binding.bluetooth.BluetoothCompletionStatus;
|
||||
import org.openhab.binding.bluetooth.BluetoothDevice.ConnectionState;
|
||||
import org.openhab.binding.bluetooth.ConnectedBluetoothHandler;
|
||||
import org.openhab.binding.bluetooth.daikinmadoka.DaikinMadokaBindingConstants;
|
||||
@ -350,12 +350,12 @@ public class DaikinMadokaHandler extends ConnectedBluetoothHandler implements Re
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCharacteristicUpdate(BluetoothCharacteristic characteristic) {
|
||||
public void onCharacteristicUpdate(BluetoothCharacteristic characteristic, byte[] msgBytes) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("[{}] onCharacteristicUpdate({})", super.thing.getUID().getId(),
|
||||
HexUtils.bytesToHex(characteristic.getByteValue()));
|
||||
HexUtils.bytesToHex(msgBytes));
|
||||
}
|
||||
super.onCharacteristicUpdate(characteristic);
|
||||
super.onCharacteristicUpdate(characteristic, msgBytes);
|
||||
|
||||
// Check that arguments are valid.
|
||||
if (characteristic.getUuid() == null) {
|
||||
@ -367,9 +367,8 @@ public class DaikinMadokaHandler extends ConnectedBluetoothHandler implements Re
|
||||
return;
|
||||
}
|
||||
|
||||
// A message cannot be null or have a 0-byte length
|
||||
byte[] msgBytes = characteristic.getByteValue();
|
||||
if (msgBytes == null || msgBytes.length == 0) {
|
||||
// A message cannot have a 0-byte length
|
||||
if (msgBytes.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -398,7 +397,7 @@ public class DaikinMadokaHandler extends ConnectedBluetoothHandler implements Re
|
||||
return;
|
||||
}
|
||||
|
||||
if (!resolved) {
|
||||
if (!device.isServicesDiscovered()) {
|
||||
logger.debug("Unable to send command {} to device {}: services not resolved",
|
||||
command.getClass().getSimpleName(), device.getAddress());
|
||||
command.setState(BRC1HCommand.State.FAILED);
|
||||
@ -424,17 +423,23 @@ public class DaikinMadokaHandler extends ConnectedBluetoothHandler implements Re
|
||||
|
||||
// Commands can be composed of multiple chunks
|
||||
for (byte[] chunk : command.getRequest()) {
|
||||
charWrite.setValue(chunk);
|
||||
command.setState(BRC1HCommand.State.ENQUEUED);
|
||||
for (int i = 0; i < DaikinMadokaBindingConstants.WRITE_CHARACTERISTIC_MAX_RETRIES; i++) {
|
||||
if (device.writeCharacteristic(charWrite)) {
|
||||
command.setState(BRC1HCommand.State.SENT);
|
||||
synchronized (command) {
|
||||
command.wait(100);
|
||||
}
|
||||
break;
|
||||
try {
|
||||
device.writeCharacteristic(charWrite, chunk).get(100, TimeUnit.MILLISECONDS);
|
||||
} catch (InterruptedException ex) {
|
||||
return;
|
||||
} catch (ExecutionException ex) {
|
||||
logger.debug("Error while writing message {}: {}", command.getClass().getSimpleName(),
|
||||
ex.getMessage());
|
||||
Thread.sleep(100);
|
||||
continue;
|
||||
} catch (TimeoutException ex) {
|
||||
Thread.sleep(100);
|
||||
continue;
|
||||
}
|
||||
Thread.sleep(100);
|
||||
command.setState(BRC1HCommand.State.SENT);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@ -459,39 +464,6 @@ public class DaikinMadokaHandler extends ConnectedBluetoothHandler implements Re
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCharacteristicWriteComplete(BluetoothCharacteristic characteristic,
|
||||
BluetoothCompletionStatus status) {
|
||||
super.onCharacteristicWriteComplete(characteristic, status);
|
||||
|
||||
byte[] request = characteristic.getByteValue();
|
||||
BRC1HCommand command = currentCommand;
|
||||
|
||||
if (command != null) {
|
||||
// last chunk:
|
||||
byte[] lastChunk = command.getRequest()[command.getRequest().length - 1];
|
||||
if (!Arrays.equals(request, lastChunk)) {
|
||||
logger.debug("Write completed for a chunk, but not a complete command.");
|
||||
synchronized (command) {
|
||||
command.notify();
|
||||
}
|
||||
return;
|
||||
}
|
||||
switch (status) {
|
||||
case SUCCESS:
|
||||
command.setState(BRC1HCommand.State.SENT);
|
||||
break;
|
||||
case ERROR:
|
||||
command.setState(BRC1HCommand.State.FAILED);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("No command found that matches request {}", HexUtils.bytesToHex(request));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When the method is triggered, it means that all message chunks have been received, re-assembled in the right
|
||||
* order and that the payload is ready to be processed.
|
||||
|
@ -27,7 +27,6 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.bluetooth.BluetoothBindingConstants;
|
||||
import org.openhab.binding.bluetooth.BluetoothCharacteristic;
|
||||
import org.openhab.binding.bluetooth.BluetoothCompletionStatus;
|
||||
import org.openhab.binding.bluetooth.BluetoothDevice.ConnectionState;
|
||||
import org.openhab.binding.bluetooth.ConnectedBluetoothHandler;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
@ -85,7 +84,7 @@ public class GenericBluetoothHandler extends ConnectedBluetoothHandler {
|
||||
GenericBindingConfiguration config = getConfigAs(GenericBindingConfiguration.class);
|
||||
readCharacteristicJob = scheduler.scheduleWithFixedDelay(() -> {
|
||||
if (device.getConnectionState() == ConnectionState.CONNECTED) {
|
||||
if (resolved) {
|
||||
if (device.isServicesDiscovered()) {
|
||||
handlerToChannels.forEach((charHandler, channelUids) -> {
|
||||
// Only read the value manually if notification is not on.
|
||||
// Also read it the first time before we activate notifications below.
|
||||
@ -139,11 +138,9 @@ public class GenericBluetoothHandler extends ConnectedBluetoothHandler {
|
||||
|
||||
@Override
|
||||
public void onServicesDiscovered() {
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
logger.trace("Service discovery completed for '{}'", address);
|
||||
updateThingChannels();
|
||||
}
|
||||
super.onServicesDiscovered();
|
||||
logger.trace("Service discovery completed for '{}'", address);
|
||||
updateThingChannels();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -157,19 +154,9 @@ public class GenericBluetoothHandler extends ConnectedBluetoothHandler {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCharacteristicReadComplete(BluetoothCharacteristic characteristic, BluetoothCompletionStatus status) {
|
||||
super.onCharacteristicReadComplete(characteristic, status);
|
||||
if (status == BluetoothCompletionStatus.SUCCESS) {
|
||||
byte[] data = characteristic.getByteValue();
|
||||
getCharacteristicHandler(characteristic).handleCharacteristicUpdate(data);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCharacteristicUpdate(BluetoothCharacteristic characteristic) {
|
||||
super.onCharacteristicUpdate(characteristic);
|
||||
byte[] data = characteristic.getByteValue();
|
||||
getCharacteristicHandler(characteristic).handleCharacteristicUpdate(data);
|
||||
public void onCharacteristicUpdate(BluetoothCharacteristic characteristic, byte[] value) {
|
||||
super.onCharacteristicUpdate(characteristic, value);
|
||||
getCharacteristicHandler(characteristic).handleCharacteristicUpdate(value);
|
||||
}
|
||||
|
||||
private void updateThingChannels() {
|
||||
@ -207,13 +194,28 @@ public class GenericBluetoothHandler extends ConnectedBluetoothHandler {
|
||||
return Objects.requireNonNull(charHandlers.computeIfAbsent(characteristic, CharacteristicHandler::new));
|
||||
}
|
||||
|
||||
private boolean readCharacteristic(BluetoothCharacteristic characteristic) {
|
||||
return device.readCharacteristic(characteristic);
|
||||
private void readCharacteristic(BluetoothCharacteristic characteristic) {
|
||||
readCharacteristic(characteristic.getService().getUuid(), characteristic.getUuid()).whenComplete((data, th) -> {
|
||||
if (th != null) {
|
||||
logger.warn("Could not read data from characteristic {} of device {}: {}", characteristic.getUuid(),
|
||||
address, th.getMessage());
|
||||
return;
|
||||
}
|
||||
if (data != null) {
|
||||
getCharacteristicHandler(characteristic).handleCharacteristicUpdate(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private boolean writeCharacteristic(BluetoothCharacteristic characteristic, byte[] data) {
|
||||
characteristic.setValue(data);
|
||||
return device.writeCharacteristic(characteristic);
|
||||
private void writeCharacteristic(BluetoothCharacteristic characteristic, byte[] data) {
|
||||
writeCharacteristic(characteristic.getService().getUuid(), characteristic.getUuid(), data, false)
|
||||
.whenComplete((r, th) -> {
|
||||
if (th != null) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
||||
"Could not write data to characteristic " + characteristic.getUuid() + ": "
|
||||
+ th.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private class CharacteristicHandler {
|
||||
@ -253,10 +255,7 @@ public class GenericBluetoothHandler extends ConnectedBluetoothHandler {
|
||||
} else if (state instanceof StringType) {
|
||||
// unknown characteristic
|
||||
byte[] data = HexUtils.hexToBytes(state.toString());
|
||||
if (!writeCharacteristic(characteristic, data)) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
||||
"Could not write data to characteristic: " + characteristicUUID);
|
||||
}
|
||||
writeCharacteristic(characteristic, data);
|
||||
}
|
||||
} catch (RuntimeException ex) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
||||
@ -275,10 +274,7 @@ public class GenericBluetoothHandler extends ConnectedBluetoothHandler {
|
||||
BluetoothChannelUtils.updateHolder(gattParser, request, fieldName, state);
|
||||
byte[] data = gattParser.serialize(request);
|
||||
|
||||
if (!writeCharacteristic(characteristic, data)) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
||||
"Could not write data to characteristic: " + characteristicUUID);
|
||||
}
|
||||
writeCharacteristic(characteristic, data);
|
||||
} catch (NumberFormatException ex) {
|
||||
logger.warn("Could not parse characteristic value: {} : {}", characteristicUUID, state, ex);
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
||||
|
@ -25,6 +25,11 @@
|
||||
<description>The frequency at which readable characteristics refreshed</description>
|
||||
<default>30</default>
|
||||
</parameter>
|
||||
<parameter name="alwaysConnected" type="boolean">
|
||||
<label>Connect Automatically</label>
|
||||
<description>If enabled, will automatically connect to the device and reconnect if connection is lost.</description>
|
||||
<default>true</default>
|
||||
</parameter>
|
||||
</config-description>
|
||||
|
||||
</thing-type>
|
||||
|
@ -1,472 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.bluetooth.govee.internal;
|
||||
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.locks.Condition;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.bluetooth.BeaconBluetoothHandler;
|
||||
import org.openhab.binding.bluetooth.BluetoothCharacteristic;
|
||||
import org.openhab.binding.bluetooth.BluetoothCompletionStatus;
|
||||
import org.openhab.binding.bluetooth.BluetoothDescriptor;
|
||||
import org.openhab.binding.bluetooth.BluetoothDevice.ConnectionState;
|
||||
import org.openhab.binding.bluetooth.BluetoothService;
|
||||
import org.openhab.binding.bluetooth.notification.BluetoothConnectionStatusNotification;
|
||||
import org.openhab.core.common.NamedThreadFactory;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.util.HexUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* This is a base implementation for more specific thing handlers that require constant connection to bluetooth devices.
|
||||
*
|
||||
* @author Kai Kreuzer - Initial contribution and API
|
||||
* @deprecated once CompletableFutures are supported in the actual ConnectedBluetoothHandler, this class can be deleted
|
||||
*/
|
||||
@Deprecated
|
||||
@NonNullByDefault
|
||||
public class ConnectedBluetoothHandler extends BeaconBluetoothHandler {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(ConnectedBluetoothHandler.class);
|
||||
|
||||
private final Condition connectionCondition = deviceLock.newCondition();
|
||||
private final Condition serviceDiscoveryCondition = deviceLock.newCondition();
|
||||
private final Condition charCompleteCondition = deviceLock.newCondition();
|
||||
|
||||
private @Nullable Future<?> reconnectJob;
|
||||
private @Nullable Future<?> pendingDisconnect;
|
||||
private @Nullable BluetoothCharacteristic ongoingCharacteristic;
|
||||
private @Nullable BluetoothCompletionStatus completeStatus;
|
||||
|
||||
private boolean connectOnDemand;
|
||||
private int idleDisconnectDelayMs = 1000;
|
||||
|
||||
protected @Nullable ScheduledExecutorService connectionTaskExecutor;
|
||||
private volatile boolean servicesDiscovered;
|
||||
|
||||
public ConnectedBluetoothHandler(Thing thing) {
|
||||
super(thing);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
|
||||
// super.initialize adds callbacks that might require the connectionTaskExecutor to be present, so we initialize
|
||||
// the connectionTaskExecutor first
|
||||
ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1,
|
||||
new NamedThreadFactory("bluetooth-connection-" + thing.getThingTypeUID(), true));
|
||||
executor.setExecuteExistingDelayedTasksAfterShutdownPolicy(false);
|
||||
executor.setRemoveOnCancelPolicy(true);
|
||||
connectionTaskExecutor = executor;
|
||||
|
||||
super.initialize();
|
||||
|
||||
connectOnDemand = true;
|
||||
|
||||
Object idleDisconnectDelayRaw = getConfig().get("idleDisconnectDelay");
|
||||
idleDisconnectDelayMs = 1000;
|
||||
if (idleDisconnectDelayRaw instanceof Number) {
|
||||
idleDisconnectDelayMs = ((Number) idleDisconnectDelayRaw).intValue();
|
||||
}
|
||||
|
||||
if (!connectOnDemand) {
|
||||
reconnectJob = executor.scheduleWithFixedDelay(() -> {
|
||||
try {
|
||||
if (device.getConnectionState() != ConnectionState.CONNECTED) {
|
||||
device.connect();
|
||||
// we do not set the Thing status here, because we will anyhow receive a call to
|
||||
// onConnectionStateChange
|
||||
} else {
|
||||
// just in case it was already connected to begin with
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
if (!servicesDiscovered && !device.discoverServices()) {
|
||||
logger.debug("Error while discovering services");
|
||||
}
|
||||
}
|
||||
} catch (RuntimeException ex) {
|
||||
logger.warn("Unexpected error occurred", ex);
|
||||
}
|
||||
}, 0, 30, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
cancel(reconnectJob);
|
||||
reconnectJob = null;
|
||||
cancel(pendingDisconnect);
|
||||
pendingDisconnect = null;
|
||||
|
||||
super.dispose();
|
||||
|
||||
shutdown(connectionTaskExecutor);
|
||||
connectionTaskExecutor = null;
|
||||
}
|
||||
|
||||
private static void cancel(@Nullable Future<?> future) {
|
||||
if (future != null) {
|
||||
future.cancel(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void shutdown(@Nullable ScheduledExecutorService executor) {
|
||||
if (executor != null) {
|
||||
executor.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
private ScheduledExecutorService getConnectionTaskExecutor() {
|
||||
var executor = connectionTaskExecutor;
|
||||
if (executor == null) {
|
||||
throw new IllegalStateException("characteristicScheduler has not been initialized");
|
||||
}
|
||||
return executor;
|
||||
}
|
||||
|
||||
private void scheduleDisconnect() {
|
||||
cancel(pendingDisconnect);
|
||||
pendingDisconnect = getConnectionTaskExecutor().schedule(device::disconnect, idleDisconnectDelayMs,
|
||||
TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
private void connectAndWait() throws ConnectionException, TimeoutException, InterruptedException {
|
||||
if (device.getConnectionState() == ConnectionState.CONNECTED) {
|
||||
return;
|
||||
}
|
||||
if (device.getConnectionState() != ConnectionState.CONNECTING) {
|
||||
if (!device.connect()) {
|
||||
throw new ConnectionException("Failed to start connecting");
|
||||
}
|
||||
}
|
||||
logger.debug("waiting for connection");
|
||||
if (!awaitConnection(1, TimeUnit.SECONDS)) {
|
||||
throw new TimeoutException("Connection attempt timeout.");
|
||||
}
|
||||
logger.debug("connection successful");
|
||||
if (!servicesDiscovered) {
|
||||
logger.debug("discovering services");
|
||||
device.discoverServices();
|
||||
if (!awaitServiceDiscovery(20, TimeUnit.SECONDS)) {
|
||||
throw new TimeoutException("Service discovery timeout");
|
||||
}
|
||||
logger.debug("service discovery successful");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean awaitConnection(long timeout, TimeUnit unit) throws InterruptedException {
|
||||
deviceLock.lock();
|
||||
try {
|
||||
long nanosTimeout = unit.toNanos(timeout);
|
||||
while (device.getConnectionState() != ConnectionState.CONNECTED) {
|
||||
if (nanosTimeout <= 0L) {
|
||||
return false;
|
||||
}
|
||||
nanosTimeout = connectionCondition.awaitNanos(nanosTimeout);
|
||||
}
|
||||
} finally {
|
||||
deviceLock.unlock();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean awaitCharacteristicComplete(long timeout, TimeUnit unit) throws InterruptedException {
|
||||
deviceLock.lock();
|
||||
try {
|
||||
long nanosTimeout = unit.toNanos(timeout);
|
||||
while (ongoingCharacteristic != null) {
|
||||
if (nanosTimeout <= 0L) {
|
||||
return false;
|
||||
}
|
||||
nanosTimeout = charCompleteCondition.awaitNanos(nanosTimeout);
|
||||
}
|
||||
} finally {
|
||||
deviceLock.unlock();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean awaitServiceDiscovery(long timeout, TimeUnit unit) throws InterruptedException {
|
||||
deviceLock.lock();
|
||||
try {
|
||||
long nanosTimeout = unit.toNanos(timeout);
|
||||
while (!servicesDiscovered) {
|
||||
if (nanosTimeout <= 0L) {
|
||||
return false;
|
||||
}
|
||||
nanosTimeout = serviceDiscoveryCondition.awaitNanos(nanosTimeout);
|
||||
}
|
||||
} finally {
|
||||
deviceLock.unlock();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private BluetoothCharacteristic connectAndGetCharacteristic(UUID serviceUUID, UUID characteristicUUID)
|
||||
throws BluetoothException, TimeoutException, InterruptedException {
|
||||
connectAndWait();
|
||||
BluetoothService service = device.getServices(serviceUUID);
|
||||
if (service == null) {
|
||||
throw new BluetoothException("Service with uuid " + serviceUUID + " could not be found");
|
||||
}
|
||||
BluetoothCharacteristic characteristic = service.getCharacteristic(characteristicUUID);
|
||||
if (characteristic == null) {
|
||||
throw new BluetoothException("Characteristic with uuid " + characteristicUUID + " could not be found");
|
||||
}
|
||||
return characteristic;
|
||||
}
|
||||
|
||||
private <T> CompletableFuture<T> executeWithConnection(UUID serviceUUID, UUID characteristicUUID,
|
||||
CallableFunction<BluetoothCharacteristic, T> callable) {
|
||||
CompletableFuture<T> future = new CompletableFuture<>();
|
||||
var executor = connectionTaskExecutor;
|
||||
if (executor != null) {
|
||||
executor.execute(() -> {
|
||||
cancel(pendingDisconnect);
|
||||
try {
|
||||
BluetoothCharacteristic characteristic = connectAndGetCharacteristic(serviceUUID,
|
||||
characteristicUUID);
|
||||
future.complete(callable.call(characteristic));
|
||||
} catch (InterruptedException e) {
|
||||
future.completeExceptionally(e);
|
||||
return;// we don't want to schedule anything if we receive an interrupt
|
||||
} catch (TimeoutException e) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
|
||||
future.completeExceptionally(e);
|
||||
} catch (Exception e) {
|
||||
future.completeExceptionally(e);
|
||||
}
|
||||
if (connectOnDemand) {
|
||||
scheduleDisconnect();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
future.completeExceptionally(new IllegalStateException("characteristicScheduler has not been initialized"));
|
||||
}
|
||||
return future;
|
||||
}
|
||||
|
||||
public CompletableFuture<@Nullable Void> enableNotifications(UUID serviceUUID, UUID characteristicUUID) {
|
||||
return executeWithConnection(serviceUUID, characteristicUUID, characteristic -> {
|
||||
if (!device.enableNotifications(characteristic)) {
|
||||
throw new BluetoothException(
|
||||
"Failed to start notifications for characteristic: " + characteristic.getUuid());
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
public CompletableFuture<@Nullable Void> writeCharacteristic(UUID serviceUUID, UUID characteristicUUID, byte[] data,
|
||||
boolean enableNotification) {
|
||||
return executeWithConnection(serviceUUID, characteristicUUID, characteristic -> {
|
||||
if (enableNotification) {
|
||||
if (!device.enableNotifications(characteristic)) {
|
||||
throw new BluetoothException(
|
||||
"Failed to start characteristic notification" + characteristic.getUuid());
|
||||
}
|
||||
}
|
||||
// now block for completion
|
||||
characteristic.setValue(data);
|
||||
ongoingCharacteristic = characteristic;
|
||||
if (!device.writeCharacteristic(characteristic)) {
|
||||
throw new BluetoothException("Failed to start writing characteristic " + characteristic.getUuid());
|
||||
}
|
||||
if (!awaitCharacteristicComplete(1, TimeUnit.SECONDS)) {
|
||||
ongoingCharacteristic = null;
|
||||
throw new TimeoutException(
|
||||
"Timeout waiting for characteristic " + characteristic.getUuid() + " write to finish");
|
||||
}
|
||||
if (completeStatus == BluetoothCompletionStatus.ERROR) {
|
||||
throw new BluetoothException("Failed to write characteristic " + characteristic.getUuid());
|
||||
}
|
||||
logger.debug("Wrote {} to characteristic {} of device {}", HexUtils.bytesToHex(data),
|
||||
characteristic.getUuid(), address);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
public CompletableFuture<byte[]> readCharacteristic(UUID serviceUUID, UUID characteristicUUID) {
|
||||
return executeWithConnection(serviceUUID, characteristicUUID, characteristic -> {
|
||||
// now block for completion
|
||||
ongoingCharacteristic = characteristic;
|
||||
if (!device.readCharacteristic(characteristic)) {
|
||||
throw new BluetoothException("Failed to start reading characteristic " + characteristic.getUuid());
|
||||
}
|
||||
if (!awaitCharacteristicComplete(1, TimeUnit.SECONDS)) {
|
||||
ongoingCharacteristic = null;
|
||||
throw new TimeoutException(
|
||||
"Timeout waiting for characteristic " + characteristic.getUuid() + " read to finish");
|
||||
}
|
||||
if (completeStatus == BluetoothCompletionStatus.ERROR) {
|
||||
throw new BluetoothException("Failed to read characteristic " + characteristic.getUuid());
|
||||
}
|
||||
byte[] data = characteristic.getByteValue();
|
||||
logger.debug("Characteristic {} from {} has been read - value {}", characteristic.getUuid(), address,
|
||||
HexUtils.bytesToHex(data));
|
||||
return data;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateStatusBasedOnRssi(boolean receivedSignal) {
|
||||
// if there is no signal, we can be sure we are OFFLINE, but if there is a signal, we also have to check whether
|
||||
// we are connected.
|
||||
if (receivedSignal) {
|
||||
if (device.getConnectionState() == ConnectionState.CONNECTED) {
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
} else {
|
||||
if (!connectOnDemand) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "Device is not connected.");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnectionStateChange(BluetoothConnectionStatusNotification connectionNotification) {
|
||||
super.onConnectionStateChange(connectionNotification);
|
||||
switch (connectionNotification.getConnectionState()) {
|
||||
case DISCOVERED:
|
||||
// The device is now known on the Bluetooth network, so we can do something...
|
||||
if (!connectOnDemand) {
|
||||
getConnectionTaskExecutor().submit(() -> {
|
||||
if (device.getConnectionState() != ConnectionState.CONNECTED) {
|
||||
if (!device.connect()) {
|
||||
logger.debug("Error connecting to device after discovery.");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
case CONNECTED:
|
||||
deviceLock.lock();
|
||||
try {
|
||||
connectionCondition.signal();
|
||||
} finally {
|
||||
deviceLock.unlock();
|
||||
}
|
||||
if (!connectOnDemand) {
|
||||
getConnectionTaskExecutor().submit(() -> {
|
||||
if (!servicesDiscovered && !device.discoverServices()) {
|
||||
logger.debug("Error while discovering services");
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
case DISCONNECTED:
|
||||
var future = pendingDisconnect;
|
||||
if (future != null) {
|
||||
future.cancel(false);
|
||||
}
|
||||
if (!connectOnDemand) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCharacteristicReadComplete(BluetoothCharacteristic characteristic, BluetoothCompletionStatus status) {
|
||||
super.onCharacteristicReadComplete(characteristic, status);
|
||||
deviceLock.lock();
|
||||
try {
|
||||
if (ongoingCharacteristic != null && ongoingCharacteristic.getUuid().equals(characteristic.getUuid())) {
|
||||
completeStatus = status;
|
||||
ongoingCharacteristic = null;
|
||||
charCompleteCondition.signal();
|
||||
}
|
||||
} finally {
|
||||
deviceLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCharacteristicWriteComplete(BluetoothCharacteristic characteristic,
|
||||
BluetoothCompletionStatus status) {
|
||||
super.onCharacteristicWriteComplete(characteristic, status);
|
||||
deviceLock.lock();
|
||||
try {
|
||||
if (ongoingCharacteristic != null && ongoingCharacteristic.getUuid().equals(characteristic.getUuid())) {
|
||||
completeStatus = status;
|
||||
ongoingCharacteristic = null;
|
||||
charCompleteCondition.signal();
|
||||
}
|
||||
} finally {
|
||||
deviceLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServicesDiscovered() {
|
||||
super.onServicesDiscovered();
|
||||
deviceLock.lock();
|
||||
try {
|
||||
this.servicesDiscovered = true;
|
||||
serviceDiscoveryCondition.signal();
|
||||
} finally {
|
||||
deviceLock.unlock();
|
||||
}
|
||||
logger.debug("Service discovery completed for '{}'", address);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCharacteristicUpdate(BluetoothCharacteristic characteristic) {
|
||||
super.onCharacteristicUpdate(characteristic);
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Recieved update {} to characteristic {} of device {}",
|
||||
HexUtils.bytesToHex(characteristic.getByteValue()), characteristic.getUuid(), address);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDescriptorUpdate(BluetoothDescriptor descriptor) {
|
||||
super.onDescriptorUpdate(descriptor);
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Received update {} to descriptor {} of device {}", HexUtils.bytesToHex(descriptor.getValue()),
|
||||
descriptor.getUuid(), address);
|
||||
}
|
||||
}
|
||||
|
||||
public static class BluetoothException extends Exception {
|
||||
|
||||
public BluetoothException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
public static class ConnectionException extends BluetoothException {
|
||||
|
||||
public ConnectionException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public static interface CallableFunction<U, R> {
|
||||
public R call(U arg) throws Exception;
|
||||
}
|
||||
}
|
@ -33,6 +33,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.bluetooth.BluetoothCharacteristic;
|
||||
import org.openhab.binding.bluetooth.BluetoothDevice.ConnectionState;
|
||||
import org.openhab.binding.bluetooth.ConnectedBluetoothHandler;
|
||||
import org.openhab.binding.bluetooth.gattserial.MessageServicer;
|
||||
import org.openhab.binding.bluetooth.gattserial.SimpleGattSocket;
|
||||
import org.openhab.binding.bluetooth.govee.internal.command.hygrometer.GetBatteryCommand;
|
||||
@ -93,6 +94,11 @@ public class GoveeHygrometerHandler extends ConnectedBluetoothHandler {
|
||||
@Override
|
||||
public void initialize() {
|
||||
super.initialize();
|
||||
if (thing.getStatus() == ThingStatus.OFFLINE) {
|
||||
// something went wrong in super.initialize() so we shouldn't initialize further here either
|
||||
return;
|
||||
}
|
||||
|
||||
config = getConfigAs(GoveeHygrometerConfiguration.class);
|
||||
|
||||
Map<String, String> properties = thing.getProperties();
|
||||
@ -117,14 +123,14 @@ public class GoveeHygrometerHandler extends ConnectedBluetoothHandler {
|
||||
logger.debug("refreshing temperature, humidity, and battery");
|
||||
refreshBattery().join();
|
||||
refreshTemperatureAndHumidity().join();
|
||||
connectionTaskExecutor.execute(device::disconnect);
|
||||
disconnect();
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
}
|
||||
} catch (RuntimeException ex) {
|
||||
logger.warn("unable to refresh", ex);
|
||||
}
|
||||
}, 0, config.refreshInterval, TimeUnit.SECONDS);
|
||||
keepAliveJob = connectionTaskExecutor.scheduleWithFixedDelay(() -> {
|
||||
keepAliveJob = scheduler.scheduleWithFixedDelay(() -> {
|
||||
if (device.getConnectionState() == ConnectionState.CONNECTED) {
|
||||
try {
|
||||
GoveeMessage message = new GoveeMessage((byte) 0xAA, (byte) 1, null);
|
||||
@ -393,9 +399,9 @@ public class GoveeHygrometerHandler extends ConnectedBluetoothHandler {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCharacteristicUpdate(BluetoothCharacteristic characteristic) {
|
||||
super.onCharacteristicUpdate(characteristic);
|
||||
commandSocket.receivePacket(characteristic.getByteValue());
|
||||
public void onCharacteristicUpdate(BluetoothCharacteristic characteristic, byte[] value) {
|
||||
super.onCharacteristicUpdate(characteristic, value);
|
||||
commandSocket.receivePacket(value);
|
||||
}
|
||||
|
||||
private class CommandSocket extends SimpleGattSocket<GoveeMessage> {
|
||||
|
@ -25,7 +25,6 @@ import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.bluetooth.BluetoothAdapter;
|
||||
import org.openhab.binding.bluetooth.BluetoothAddress;
|
||||
import org.openhab.binding.bluetooth.BluetoothCharacteristic;
|
||||
import org.openhab.binding.bluetooth.BluetoothCompletionStatus;
|
||||
import org.openhab.binding.bluetooth.BluetoothDescriptor;
|
||||
import org.openhab.binding.bluetooth.BluetoothDevice;
|
||||
import org.openhab.binding.bluetooth.BluetoothDeviceListener;
|
||||
@ -135,32 +134,16 @@ public class RoamingBluetoothDevice extends DelegateBluetoothDevice {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCharacteristicReadComplete(BluetoothCharacteristic characteristic,
|
||||
BluetoothCompletionStatus status) {
|
||||
public void onCharacteristicUpdate(BluetoothCharacteristic characteristic, byte[] value) {
|
||||
if (device == getDelegate()) {
|
||||
notifyListeners(BluetoothEventType.CHARACTERISTIC_READ_COMPLETE, characteristic, status);
|
||||
notifyListeners(BluetoothEventType.CHARACTERISTIC_UPDATED, characteristic, value);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCharacteristicWriteComplete(BluetoothCharacteristic characteristic,
|
||||
BluetoothCompletionStatus status) {
|
||||
public void onDescriptorUpdate(BluetoothDescriptor bluetoothDescriptor, byte[] value) {
|
||||
if (device == getDelegate()) {
|
||||
notifyListeners(BluetoothEventType.CHARACTERISTIC_WRITE_COMPLETE, characteristic);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCharacteristicUpdate(BluetoothCharacteristic characteristic) {
|
||||
if (device == getDelegate()) {
|
||||
notifyListeners(BluetoothEventType.CHARACTERISTIC_UPDATED, characteristic);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDescriptorUpdate(BluetoothDescriptor bluetoothDescriptor) {
|
||||
if (device == getDelegate()) {
|
||||
notifyListeners(BluetoothEventType.DESCRIPTOR_UPDATED, bluetoothDescriptor);
|
||||
notifyListeners(BluetoothEventType.DESCRIPTOR_UPDATED, bluetoothDescriptor, value);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -19,6 +19,10 @@ import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.Condition;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
@ -80,6 +84,12 @@ public abstract class BaseBluetoothDevice extends BluetoothDevice {
|
||||
*/
|
||||
private final Set<BluetoothDeviceListener> eventListeners = new CopyOnWriteArraySet<>();
|
||||
|
||||
private final Lock deviceLock = new ReentrantLock();
|
||||
private final Condition connectionCondition = deviceLock.newCondition();
|
||||
private final Condition serviceDiscoveryCondition = deviceLock.newCondition();
|
||||
|
||||
private volatile boolean servicesDiscovered = false;
|
||||
|
||||
/**
|
||||
* Construct a Bluetooth device taking the Bluetooth address
|
||||
*
|
||||
@ -258,6 +268,45 @@ public abstract class BaseBluetoothDevice extends BluetoothDevice {
|
||||
protected void dispose() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isServicesDiscovered() {
|
||||
return servicesDiscovered;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean awaitConnection(long timeout, TimeUnit unit) throws InterruptedException {
|
||||
deviceLock.lock();
|
||||
try {
|
||||
long nanosTimeout = unit.toNanos(timeout);
|
||||
while (getConnectionState() != ConnectionState.CONNECTED) {
|
||||
if (nanosTimeout <= 0L) {
|
||||
return false;
|
||||
}
|
||||
nanosTimeout = connectionCondition.awaitNanos(nanosTimeout);
|
||||
}
|
||||
} finally {
|
||||
deviceLock.unlock();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean awaitServiceDiscovery(long timeout, TimeUnit unit) throws InterruptedException {
|
||||
deviceLock.lock();
|
||||
try {
|
||||
long nanosTimeout = unit.toNanos(timeout);
|
||||
while (!servicesDiscovered) {
|
||||
if (nanosTimeout <= 0L) {
|
||||
return false;
|
||||
}
|
||||
nanosTimeout = serviceDiscoveryCondition.awaitNanos(nanosTimeout);
|
||||
}
|
||||
} finally {
|
||||
deviceLock.unlock();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void notifyListeners(BluetoothEventType event, Object... args) {
|
||||
switch (event) {
|
||||
@ -270,6 +319,27 @@ public abstract class BaseBluetoothDevice extends BluetoothDevice {
|
||||
default:
|
||||
break;
|
||||
}
|
||||
switch (event) {
|
||||
case SERVICES_DISCOVERED:
|
||||
deviceLock.lock();
|
||||
try {
|
||||
servicesDiscovered = true;
|
||||
serviceDiscoveryCondition.signal();
|
||||
} finally {
|
||||
deviceLock.unlock();
|
||||
}
|
||||
break;
|
||||
case CONNECTION_STATE:
|
||||
deviceLock.lock();
|
||||
try {
|
||||
connectionCondition.signal();
|
||||
} finally {
|
||||
deviceLock.unlock();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
super.notifyListeners(event, args);
|
||||
}
|
||||
|
||||
|
@ -214,7 +214,7 @@ public class BeaconBluetoothHandler extends BaseThingHandler implements Bluetoot
|
||||
}
|
||||
}
|
||||
|
||||
private void onActivity() {
|
||||
protected void onActivity() {
|
||||
this.lastActivityTime = ZonedDateTime.now();
|
||||
}
|
||||
|
||||
@ -241,27 +241,12 @@ public class BeaconBluetoothHandler extends BaseThingHandler implements Bluetoot
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCharacteristicReadComplete(BluetoothCharacteristic characteristic, BluetoothCompletionStatus status) {
|
||||
if (status == BluetoothCompletionStatus.SUCCESS) {
|
||||
onActivity();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCharacteristicWriteComplete(BluetoothCharacteristic characteristic,
|
||||
BluetoothCompletionStatus status) {
|
||||
if (status == BluetoothCompletionStatus.SUCCESS) {
|
||||
onActivity();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCharacteristicUpdate(BluetoothCharacteristic characteristic) {
|
||||
public void onCharacteristicUpdate(BluetoothCharacteristic characteristic, byte[] value) {
|
||||
onActivity();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDescriptorUpdate(BluetoothDescriptor bluetoothDescriptor) {
|
||||
public void onDescriptorUpdate(BluetoothDescriptor bluetoothDescriptor, byte[] value) {
|
||||
onActivity();
|
||||
}
|
||||
|
||||
|
@ -43,6 +43,8 @@ public class BluetoothBindingConstants {
|
||||
|
||||
public static final String CONFIGURATION_ADDRESS = "address";
|
||||
public static final String CONFIGURATION_DISCOVERY = "backgroundDiscovery";
|
||||
public static final String CONFIGURATION_ALWAYS_CONNECTED = "alwaysConnected";
|
||||
public static final String CONFIGURATION_IDLE_DISCONNECT_DELAY = "idleDisconnectDelay";
|
||||
|
||||
public static final long BLUETOOTH_BASE_UUID = 0x800000805f9b34fbL;
|
||||
|
||||
|
@ -12,7 +12,6 @@
|
||||
*/
|
||||
package org.openhab.binding.bluetooth;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
@ -34,15 +33,6 @@ import org.slf4j.LoggerFactory;
|
||||
* @author Peter Rosenberg - Improve properties support
|
||||
*/
|
||||
public class BluetoothCharacteristic {
|
||||
public static final int FORMAT_UINT8 = 0x11;
|
||||
public static final int FORMAT_UINT16 = 0x12;
|
||||
public static final int FORMAT_UINT32 = 0x14;
|
||||
public static final int FORMAT_SINT8 = 0x21;
|
||||
public static final int FORMAT_SINT16 = 0x22;
|
||||
public static final int FORMAT_SINT32 = 0x24;
|
||||
public static final int FORMAT_SFLOAT = 0x32;
|
||||
public static final int FORMAT_FLOAT = 0x34;
|
||||
|
||||
public static final int PROPERTY_BROADCAST = 0x01;
|
||||
public static final int PROPERTY_READ = 0x02;
|
||||
public static final int PROPERTY_WRITE_NO_RESPONSE = 0x04;
|
||||
@ -86,11 +76,6 @@ public class BluetoothCharacteristic {
|
||||
protected int permissions;
|
||||
protected int writeType;
|
||||
|
||||
/**
|
||||
* The raw data value for this characteristic
|
||||
*/
|
||||
protected int[] value = new int[0];
|
||||
|
||||
/**
|
||||
* The {@link BluetoothService} to which this characteristic belongs
|
||||
*/
|
||||
@ -314,299 +299,6 @@ public class BluetoothCharacteristic {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the stored value for this characteristic.
|
||||
*
|
||||
*/
|
||||
public int[] getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the stored value for this characteristic.
|
||||
*
|
||||
*/
|
||||
public byte[] getByteValue() {
|
||||
byte[] byteValue = new byte[value.length];
|
||||
for (int cnt = 0; cnt < value.length; cnt++) {
|
||||
byteValue[cnt] = (byte) (value[cnt] & 0xFF);
|
||||
}
|
||||
return byteValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the stored value of this characteristic.
|
||||
*
|
||||
*/
|
||||
public Integer getIntValue(int formatType, int offset) {
|
||||
if ((offset + getTypeLen(formatType)) > value.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (formatType) {
|
||||
case FORMAT_UINT8:
|
||||
return unsignedByteToInt(value[offset]);
|
||||
|
||||
case FORMAT_UINT16:
|
||||
return unsignedBytesToInt(value[offset], value[offset + 1]);
|
||||
|
||||
case FORMAT_UINT32:
|
||||
return unsignedBytesToInt(value[offset], value[offset + 1], value[offset + 2], value[offset + 3]);
|
||||
|
||||
case FORMAT_SINT8:
|
||||
return unsignedToSigned(unsignedByteToInt(value[offset]), 8);
|
||||
|
||||
case FORMAT_SINT16:
|
||||
return unsignedToSigned(unsignedBytesToInt(value[offset], value[offset + 1]), 16);
|
||||
|
||||
case FORMAT_SINT32:
|
||||
return unsignedToSigned(
|
||||
unsignedBytesToInt(value[offset], value[offset + 1], value[offset + 2], value[offset + 3]), 32);
|
||||
default:
|
||||
logger.error("Unknown format type {} - no int value can be provided for it.", formatType);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the stored value of this characteristic. This doesn't read the remote data.
|
||||
*
|
||||
*/
|
||||
public Float getFloatValue(int formatType, int offset) {
|
||||
if ((offset + getTypeLen(formatType)) > value.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (formatType) {
|
||||
case FORMAT_SFLOAT:
|
||||
return bytesToFloat(value[offset], value[offset + 1]);
|
||||
case FORMAT_FLOAT:
|
||||
return bytesToFloat(value[offset], value[offset + 1], value[offset + 2], value[offset + 3]);
|
||||
default:
|
||||
logger.error("Unknown format type {} - no float value can be provided for it.", formatType);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the stored value of this characteristic. This doesn't read the remote data.
|
||||
*
|
||||
*/
|
||||
public String getStringValue(int offset) {
|
||||
if (value == null || offset > value.length) {
|
||||
return null;
|
||||
}
|
||||
byte[] strBytes = new byte[value.length - offset];
|
||||
for (int i = 0; i < (value.length - offset); ++i) {
|
||||
strBytes[i] = (byte) value[offset + i];
|
||||
}
|
||||
return new String(strBytes, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the locally stored value of this characteristic.
|
||||
*
|
||||
* @param value the value to set
|
||||
* @return true, if it has been set successfully
|
||||
*/
|
||||
public boolean setValue(int[] value) {
|
||||
this.value = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the local value of this characteristic.
|
||||
*
|
||||
* @param value the value to set
|
||||
* @param formatType the format of the value (as one of the FORMAT_* constants in this class)
|
||||
* @param offset the offset to use when interpreting the value
|
||||
* @return true, if it has been set successfully
|
||||
*/
|
||||
public boolean setValue(int value, int formatType, int offset) {
|
||||
int len = offset + getTypeLen(formatType);
|
||||
if (this.value == null) {
|
||||
this.value = new int[len];
|
||||
}
|
||||
if (len > this.value.length) {
|
||||
return false;
|
||||
}
|
||||
int val = value;
|
||||
switch (formatType) {
|
||||
case FORMAT_SINT8:
|
||||
val = intToSignedBits(value, 8);
|
||||
// Fall-through intended
|
||||
case FORMAT_UINT8:
|
||||
this.value[offset] = (byte) (val & 0xFF);
|
||||
break;
|
||||
|
||||
case FORMAT_SINT16:
|
||||
val = intToSignedBits(value, 16);
|
||||
// Fall-through intended
|
||||
case FORMAT_UINT16:
|
||||
this.value[offset] = (byte) (val & 0xFF);
|
||||
this.value[offset + 1] = (byte) ((val >> 8) & 0xFF);
|
||||
break;
|
||||
|
||||
case FORMAT_SINT32:
|
||||
val = intToSignedBits(value, 32);
|
||||
// Fall-through intended
|
||||
case FORMAT_UINT32:
|
||||
this.value[offset] = (byte) (val & 0xFF);
|
||||
this.value[offset + 1] = (byte) ((val >> 8) & 0xFF);
|
||||
this.value[offset + 2] = (byte) ((val >> 16) & 0xFF);
|
||||
this.value[offset + 2] = (byte) ((val >> 24) & 0xFF);
|
||||
break;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the local value of this characteristic.
|
||||
*
|
||||
* @param mantissa the mantissa of the value
|
||||
* @param exponent the exponent of the value
|
||||
* @param formatType the format of the value (as one of the FORMAT_* constants in this class)
|
||||
* @param offset the offset to use when interpreting the value
|
||||
* @return true, if it has been set successfully
|
||||
*
|
||||
*/
|
||||
public boolean setValue(int mantissa, int exponent, int formatType, int offset) {
|
||||
int len = offset + getTypeLen(formatType);
|
||||
if (value == null) {
|
||||
value = new int[len];
|
||||
}
|
||||
if (len > value.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (formatType) {
|
||||
case FORMAT_SFLOAT:
|
||||
int m = intToSignedBits(mantissa, 12);
|
||||
int exp = intToSignedBits(exponent, 4);
|
||||
value[offset] = (byte) (m & 0xFF);
|
||||
value[offset + 1] = (byte) ((m >> 8) & 0x0F);
|
||||
value[offset + 1] += (byte) ((exp & 0x0F) << 4);
|
||||
break;
|
||||
|
||||
case FORMAT_FLOAT:
|
||||
m = intToSignedBits(mantissa, 24);
|
||||
exp = intToSignedBits(exponent, 8);
|
||||
value[offset] = (byte) (m & 0xFF);
|
||||
value[offset + 1] = (byte) ((m >> 8) & 0xFF);
|
||||
value[offset + 2] = (byte) ((m >> 16) & 0xFF);
|
||||
value[offset + 2] += (byte) (exp & 0xFF);
|
||||
break;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the local value of this characteristic.
|
||||
*
|
||||
* @param value the value to set
|
||||
* @return true, if it has been set successfully
|
||||
*/
|
||||
public boolean setValue(byte[] value) {
|
||||
this.value = new int[value.length];
|
||||
int cnt = 0;
|
||||
for (byte val : value) {
|
||||
this.value[cnt++] = val;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the local value of this characteristic.
|
||||
*
|
||||
* @param value the value to set
|
||||
* @return true, if it has been set successfully
|
||||
*/
|
||||
public boolean setValue(String value) {
|
||||
this.value = new int[value.getBytes().length];
|
||||
int cnt = 0;
|
||||
for (byte val : value.getBytes()) {
|
||||
this.value[cnt++] = val;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the size of the requested value type.
|
||||
*/
|
||||
private int getTypeLen(int formatType) {
|
||||
return formatType & 0xF;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a signed byte to an unsigned int.
|
||||
*/
|
||||
private int unsignedByteToInt(int value) {
|
||||
return value & 0xFF;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert signed bytes to a 16-bit unsigned int.
|
||||
*/
|
||||
private int unsignedBytesToInt(int value1, int value2) {
|
||||
return value1 + (value2 << 8);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert signed bytes to a 32-bit unsigned int.
|
||||
*/
|
||||
private int unsignedBytesToInt(int value1, int value2, int value3, int value4) {
|
||||
return value1 + (value2 << 8) + (value3 << 16) + (value4 << 24);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert signed bytes to a 16-bit short float value.
|
||||
*/
|
||||
private float bytesToFloat(int value1, int value2) {
|
||||
int mantissa = unsignedToSigned(unsignedByteToInt(value1) + ((unsignedByteToInt(value2) & 0x0F) << 8), 12);
|
||||
int exponent = unsignedToSigned(unsignedByteToInt(value2) >> 4, 4);
|
||||
return (float) (mantissa * Math.pow(10, exponent));
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert signed bytes to a 32-bit short float value.
|
||||
*/
|
||||
private float bytesToFloat(int value1, int value2, int value3, int value4) {
|
||||
int mantissa = unsignedToSigned(
|
||||
unsignedByteToInt(value1) + (unsignedByteToInt(value2) << 8) + (unsignedByteToInt(value3) << 16), 24);
|
||||
return (float) (mantissa * Math.pow(10, value4));
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an unsigned integer to a two's-complement signed value.
|
||||
*/
|
||||
private int unsignedToSigned(int unsigned, int size) {
|
||||
if ((unsigned & (1 << size - 1)) != 0) {
|
||||
return -1 * ((1 << size - 1) - (unsigned & ((1 << size - 1) - 1)));
|
||||
} else {
|
||||
return unsigned;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an integer into the signed bits of the specified length.
|
||||
*/
|
||||
private int intToSignedBits(int i, int size) {
|
||||
if (i < 0) {
|
||||
return (1 << size - 1) + (i & ((1 << size - 1) - 1));
|
||||
} else {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
public GattCharacteristic getGattCharacteristic() {
|
||||
return GattCharacteristic.getCharacteristic(uuid);
|
||||
}
|
||||
|
@ -30,8 +30,8 @@ public class BluetoothDescriptor {
|
||||
|
||||
protected final BluetoothCharacteristic characteristic;
|
||||
protected final UUID uuid;
|
||||
|
||||
protected final int handle;
|
||||
protected byte[] value;
|
||||
|
||||
/**
|
||||
* The main constructor
|
||||
@ -81,24 +81,6 @@ public class BluetoothDescriptor {
|
||||
return handle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the stored value for this descriptor. It doesn't read remote data.
|
||||
*
|
||||
* @return the value of the descriptor
|
||||
*/
|
||||
public byte[] getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the stored value for this descriptor. It doesn't update remote data.
|
||||
*
|
||||
* @param value the value for this descriptor instance
|
||||
*/
|
||||
public void setValue(byte[] value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public GattDescriptor getDescriptor() {
|
||||
return GattDescriptor.getDescriptor(uuid);
|
||||
}
|
||||
|
@ -14,6 +14,8 @@ package org.openhab.binding.bluetooth;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
@ -69,8 +71,6 @@ public abstract class BluetoothDevice {
|
||||
protected enum BluetoothEventType {
|
||||
CONNECTION_STATE,
|
||||
SCAN_RECORD,
|
||||
CHARACTERISTIC_READ_COMPLETE,
|
||||
CHARACTERISTIC_WRITE_COMPLETE,
|
||||
CHARACTERISTIC_UPDATED,
|
||||
DESCRIPTOR_UPDATED,
|
||||
SERVICES_DISCOVERED,
|
||||
@ -227,28 +227,25 @@ public abstract class BluetoothDevice {
|
||||
* Reads a characteristic. Only a single read or write operation can be requested at once. Attempting to perform an
|
||||
* operation when one is already in progress will result in subsequent calls returning false.
|
||||
* <p>
|
||||
* This is an asynchronous method. Once the read is complete
|
||||
* {@link BluetoothDeviceListener.onCharacteristicReadComplete}
|
||||
* method will be called with the completion state.
|
||||
* <p>
|
||||
* Note that {@link BluetoothDeviceListener.onCharacteristicUpdate} will be called when the read value is received.
|
||||
* This is an asynchronous method. Once the read is complete the returned future will be updated with the result.
|
||||
*
|
||||
* @param characteristic the {@link BluetoothCharacteristic} to read.
|
||||
* @return true if the characteristic read is started successfully
|
||||
* @return a future that returns the read data is successful, otherwise throws an exception
|
||||
*/
|
||||
public abstract boolean readCharacteristic(BluetoothCharacteristic characteristic);
|
||||
public abstract CompletableFuture<byte[]> readCharacteristic(BluetoothCharacteristic characteristic);
|
||||
|
||||
/**
|
||||
* Writes a characteristic. Only a single read or write operation can be requested at once. Attempting to perform an
|
||||
* operation when one is already in progress will result in subsequent calls returning false.
|
||||
* <p>
|
||||
* This is an asynchronous method. Once the write is complete
|
||||
* {@link BluetoothDeviceListener.onCharacteristicWriteComplete} method will be called with the completion state.
|
||||
* This is an asynchronous method. Once the write is complete the returned future will be updated with the result.
|
||||
*
|
||||
* @param characteristic the {@link BluetoothCharacteristic} to read.
|
||||
* @return true if the characteristic write is started successfully
|
||||
* @param characteristic the {@link BluetoothCharacteristic} to write.
|
||||
* @param value the data to write
|
||||
* @return a future that returns null upon a successful write, otherwise throws an exception
|
||||
*/
|
||||
public abstract boolean writeCharacteristic(BluetoothCharacteristic characteristic);
|
||||
public abstract CompletableFuture<@Nullable Void> writeCharacteristic(BluetoothCharacteristic characteristic,
|
||||
byte[] value);
|
||||
|
||||
/**
|
||||
* Returns if notification is enabled for the given characteristic.
|
||||
@ -269,7 +266,7 @@ public abstract class BluetoothDevice {
|
||||
* @param characteristic the {@link BluetoothCharacteristic} to receive notifications for.
|
||||
* @return true if the characteristic notification is started successfully
|
||||
*/
|
||||
public abstract boolean enableNotifications(BluetoothCharacteristic characteristic);
|
||||
public abstract CompletableFuture<@Nullable Void> enableNotifications(BluetoothCharacteristic characteristic);
|
||||
|
||||
/**
|
||||
* Disables notifications for a characteristic. Only a single read or write operation can be requested at once.
|
||||
@ -279,7 +276,7 @@ public abstract class BluetoothDevice {
|
||||
* @param characteristic the {@link BluetoothCharacteristic} to disable notifications for.
|
||||
* @return true if the characteristic notification is stopped successfully
|
||||
*/
|
||||
public abstract boolean disableNotifications(BluetoothCharacteristic characteristic);
|
||||
public abstract CompletableFuture<@Nullable Void> disableNotifications(BluetoothCharacteristic characteristic);
|
||||
|
||||
/**
|
||||
* Enables notifications for a descriptor. Only a single read or write operation can be requested at once.
|
||||
@ -376,6 +373,12 @@ public abstract class BluetoothDevice {
|
||||
|
||||
protected abstract Collection<BluetoothDeviceListener> getListeners();
|
||||
|
||||
public abstract boolean awaitConnection(long timeout, TimeUnit unit) throws InterruptedException;
|
||||
|
||||
public abstract boolean awaitServiceDiscovery(long timeout, TimeUnit unit) throws InterruptedException;
|
||||
|
||||
public abstract boolean isServicesDiscovered();
|
||||
|
||||
/**
|
||||
* Notify the listeners of an event
|
||||
*
|
||||
@ -395,19 +398,11 @@ public abstract class BluetoothDevice {
|
||||
case SERVICES_DISCOVERED:
|
||||
listener.onServicesDiscovered();
|
||||
break;
|
||||
case CHARACTERISTIC_READ_COMPLETE:
|
||||
listener.onCharacteristicReadComplete((BluetoothCharacteristic) args[0],
|
||||
(BluetoothCompletionStatus) args[1]);
|
||||
break;
|
||||
case CHARACTERISTIC_WRITE_COMPLETE:
|
||||
listener.onCharacteristicWriteComplete((BluetoothCharacteristic) args[0],
|
||||
(BluetoothCompletionStatus) args[1]);
|
||||
break;
|
||||
case CHARACTERISTIC_UPDATED:
|
||||
listener.onCharacteristicUpdate((BluetoothCharacteristic) args[0]);
|
||||
listener.onCharacteristicUpdate((BluetoothCharacteristic) args[0], (byte[]) args[1]);
|
||||
break;
|
||||
case DESCRIPTOR_UPDATED:
|
||||
listener.onDescriptorUpdate((BluetoothDescriptor) args[0]);
|
||||
listener.onDescriptorUpdate((BluetoothDescriptor) args[0], (byte[]) args[1]);
|
||||
break;
|
||||
case ADAPTER_CHANGED:
|
||||
listener.onAdapterChanged((BluetoothAdapter) args[0]);
|
||||
|
@ -46,37 +46,23 @@ public interface BluetoothDeviceListener {
|
||||
*/
|
||||
void onServicesDiscovered();
|
||||
|
||||
/**
|
||||
* Called when a read request completes
|
||||
*
|
||||
* @param characteristic the {@link BluetoothCharacteristic} that has completed the read request
|
||||
* @param status the {@link BluetoothCompletionStatus} of the read request
|
||||
*/
|
||||
void onCharacteristicReadComplete(BluetoothCharacteristic characteristic, BluetoothCompletionStatus status);
|
||||
|
||||
/**
|
||||
* Called when a write request completes
|
||||
*
|
||||
* @param characteristic the {@link BluetoothCharacteristic} that has completed the write request
|
||||
* @param status the {@link BluetoothCompletionStatus} of the write request
|
||||
*/
|
||||
void onCharacteristicWriteComplete(BluetoothCharacteristic characteristic, BluetoothCompletionStatus status);
|
||||
|
||||
/**
|
||||
* Called when a characteristic value is received. Implementations should call this whenever a value
|
||||
* is received from the BLE device even if there is no change to the value.
|
||||
*
|
||||
* @param characteristic the updated {@link BluetoothCharacteristic}
|
||||
* @param value the update value
|
||||
*/
|
||||
void onCharacteristicUpdate(BluetoothCharacteristic characteristic);
|
||||
void onCharacteristicUpdate(BluetoothCharacteristic characteristic, byte[] value);
|
||||
|
||||
/**
|
||||
* Called when a descriptor value is received. Implementations should call this whenever a value
|
||||
* is received from the BLE device even if there is no change to the value.
|
||||
*
|
||||
* @param characteristic the updated {@link BluetoothCharacteristic}
|
||||
* @param bluetoothDescriptor the updated {@link BluetoothDescriptor}
|
||||
* @param value the update value
|
||||
*/
|
||||
void onDescriptorUpdate(BluetoothDescriptor bluetoothDescriptor);
|
||||
void onDescriptorUpdate(BluetoothDescriptor bluetoothDescriptor, byte[] value);
|
||||
|
||||
/**
|
||||
* Called when the BluetoothAdapter for this BluetoothDevice changes.
|
||||
|
48
bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothException.java
Normal file
48
bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothException.java
Normal file
@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.bluetooth;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* This class encompasses exceptions that occur within the bluetooth api. This can be subclassed for more specific
|
||||
* exceptions in api implementations.
|
||||
*
|
||||
* @author Connor Petty - Initial contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class BluetoothException extends Exception {
|
||||
|
||||
private static final long serialVersionUID = -2557298438595050148L;
|
||||
|
||||
public BluetoothException() {
|
||||
super();
|
||||
}
|
||||
|
||||
public BluetoothException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
|
||||
super(message, cause, enableSuppression, writableStackTrace);
|
||||
}
|
||||
|
||||
public BluetoothException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public BluetoothException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public BluetoothException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
292
bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothUtils.java
Normal file
292
bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothUtils.java
Normal file
@ -0,0 +1,292 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.bluetooth;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* This is a utility class for parsing or formatting bluetooth characteristic data.
|
||||
*
|
||||
* @author Connor Petty - Initial Contribution
|
||||
*
|
||||
*/
|
||||
public class BluetoothUtils {
|
||||
|
||||
public static final Logger logger = LoggerFactory.getLogger(BluetoothUtils.class);
|
||||
|
||||
public static final int FORMAT_UINT8 = 0x11;
|
||||
public static final int FORMAT_UINT16 = 0x12;
|
||||
public static final int FORMAT_UINT32 = 0x14;
|
||||
public static final int FORMAT_SINT8 = 0x21;
|
||||
public static final int FORMAT_SINT16 = 0x22;
|
||||
public static final int FORMAT_SINT32 = 0x24;
|
||||
public static final int FORMAT_SFLOAT = 0x32;
|
||||
public static final int FORMAT_FLOAT = 0x34;
|
||||
|
||||
/**
|
||||
* Converts a byte array to an int array
|
||||
*
|
||||
* @param value
|
||||
* @return
|
||||
*/
|
||||
public static int[] toIntArray(byte[] value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
int[] ret = new int[value.length];
|
||||
for (int i = 0; i < value.length; i++) {
|
||||
ret[i] = value[i];
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
public static byte[] toByteArray(int[] value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
byte[] ret = new byte[value.length];
|
||||
for (int i = 0; i < value.length; i++) {
|
||||
ret[i] = (byte) (value[i] & 0xFF);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the stored value of this characteristic.
|
||||
*
|
||||
*/
|
||||
public static Integer getIntegerValue(byte[] value, int formatType, int offset) {
|
||||
if ((offset + getTypeLen(formatType)) > value.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (formatType) {
|
||||
case FORMAT_UINT8:
|
||||
return unsignedByteToInt(value[offset]);
|
||||
|
||||
case FORMAT_UINT16:
|
||||
return unsignedBytesToInt(value[offset], value[offset + 1]);
|
||||
|
||||
case FORMAT_UINT32:
|
||||
return unsignedBytesToInt(value[offset], value[offset + 1], value[offset + 2], value[offset + 3]);
|
||||
|
||||
case FORMAT_SINT8:
|
||||
return unsignedToSigned(unsignedByteToInt(value[offset]), 8);
|
||||
|
||||
case FORMAT_SINT16:
|
||||
return unsignedToSigned(unsignedBytesToInt(value[offset], value[offset + 1]), 16);
|
||||
|
||||
case FORMAT_SINT32:
|
||||
return unsignedToSigned(
|
||||
unsignedBytesToInt(value[offset], value[offset + 1], value[offset + 2], value[offset + 3]), 32);
|
||||
default:
|
||||
logger.error("Unknown format type {} - no int value can be provided for it.", formatType);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the stored value of this characteristic. This doesn't read the remote data.
|
||||
*
|
||||
*/
|
||||
public static Float getFloatValue(byte[] value, int formatType, int offset) {
|
||||
if ((offset + getTypeLen(formatType)) > value.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (formatType) {
|
||||
case FORMAT_SFLOAT:
|
||||
return bytesToFloat(value[offset], value[offset + 1]);
|
||||
case FORMAT_FLOAT:
|
||||
return bytesToFloat(value[offset], value[offset + 1], value[offset + 2], value[offset + 3]);
|
||||
default:
|
||||
logger.error("Unknown format type {} - no float value can be provided for it.", formatType);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the stored value of this characteristic. This doesn't read the remote data.
|
||||
*
|
||||
*/
|
||||
public static String getStringValue(byte[] value, int offset) {
|
||||
if (value == null || offset > value.length) {
|
||||
return null;
|
||||
}
|
||||
byte[] strBytes = new byte[value.length - offset];
|
||||
for (int i = 0; i < (value.length - offset); ++i) {
|
||||
strBytes[i] = value[offset + i];
|
||||
}
|
||||
return new String(strBytes, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the local value of this characteristic.
|
||||
*
|
||||
* @param value the value to set
|
||||
* @param formatType the format of the value (as one of the FORMAT_* constants in this class)
|
||||
* @param offset the offset to use when interpreting the value
|
||||
* @return true, if it has been set successfully
|
||||
*/
|
||||
public static boolean setValue(byte[] dest, int value, int formatType, int offset) {
|
||||
int len = offset + getTypeLen(formatType);
|
||||
if (dest == null || len > dest.length) {
|
||||
return false;
|
||||
}
|
||||
int val = value;
|
||||
switch (formatType) {
|
||||
case FORMAT_SINT8:
|
||||
val = intToSignedBits(value, 8);
|
||||
// Fall-through intended
|
||||
case FORMAT_UINT8:
|
||||
dest[offset] = (byte) (val & 0xFF);
|
||||
break;
|
||||
|
||||
case FORMAT_SINT16:
|
||||
val = intToSignedBits(value, 16);
|
||||
// Fall-through intended
|
||||
case FORMAT_UINT16:
|
||||
dest[offset] = (byte) (val & 0xFF);
|
||||
dest[offset + 1] = (byte) ((val >> 8) & 0xFF);
|
||||
break;
|
||||
|
||||
case FORMAT_SINT32:
|
||||
val = intToSignedBits(value, 32);
|
||||
// Fall-through intended
|
||||
case FORMAT_UINT32:
|
||||
dest[offset] = (byte) (val & 0xFF);
|
||||
dest[offset + 1] = (byte) ((val >> 8) & 0xFF);
|
||||
dest[offset + 2] = (byte) ((val >> 16) & 0xFF);
|
||||
dest[offset + 2] = (byte) ((val >> 24) & 0xFF);
|
||||
break;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the local value of this characteristic.
|
||||
*
|
||||
* @param mantissa the mantissa of the value
|
||||
* @param exponent the exponent of the value
|
||||
* @param formatType the format of the value (as one of the FORMAT_* constants in this class)
|
||||
* @param offset the offset to use when interpreting the value
|
||||
* @return true, if it has been set successfully
|
||||
*
|
||||
*/
|
||||
public static boolean setValue(byte[] dest, int mantissa, int exponent, int formatType, int offset) {
|
||||
int len = offset + getTypeLen(formatType);
|
||||
if (dest == null || len > dest.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (formatType) {
|
||||
case FORMAT_SFLOAT:
|
||||
int m = intToSignedBits(mantissa, 12);
|
||||
int exp = intToSignedBits(exponent, 4);
|
||||
dest[offset] = (byte) (m & 0xFF);
|
||||
dest[offset + 1] = (byte) ((m >> 8) & 0x0F);
|
||||
dest[offset + 1] += (byte) ((exp & 0x0F) << 4);
|
||||
break;
|
||||
|
||||
case FORMAT_FLOAT:
|
||||
m = intToSignedBits(mantissa, 24);
|
||||
exp = intToSignedBits(exponent, 8);
|
||||
dest[offset] = (byte) (m & 0xFF);
|
||||
dest[offset + 1] = (byte) ((m >> 8) & 0xFF);
|
||||
dest[offset + 2] = (byte) ((m >> 16) & 0xFF);
|
||||
dest[offset + 2] += (byte) (exp & 0xFF);
|
||||
break;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the size of the requested value type.
|
||||
*/
|
||||
private static int getTypeLen(int formatType) {
|
||||
return formatType & 0xF;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a signed byte to an unsigned int.
|
||||
*/
|
||||
private static int unsignedByteToInt(int value) {
|
||||
return value & 0xFF;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert signed bytes to a 16-bit unsigned int.
|
||||
*/
|
||||
private static int unsignedBytesToInt(int value1, int value2) {
|
||||
return value1 + (value2 << 8);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert signed bytes to a 32-bit unsigned int.
|
||||
*/
|
||||
private static int unsignedBytesToInt(int value1, int value2, int value3, int value4) {
|
||||
return value1 + (value2 << 8) + (value3 << 16) + (value4 << 24);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert signed bytes to a 16-bit short float value.
|
||||
*/
|
||||
private static float bytesToFloat(int value1, int value2) {
|
||||
int mantissa = unsignedToSigned(unsignedByteToInt(value1) + ((unsignedByteToInt(value2) & 0x0F) << 8), 12);
|
||||
int exponent = unsignedToSigned(unsignedByteToInt(value2) >> 4, 4);
|
||||
return (float) (mantissa * Math.pow(10, exponent));
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert signed bytes to a 32-bit short float value.
|
||||
*/
|
||||
private static float bytesToFloat(int value1, int value2, int value3, int value4) {
|
||||
int mantissa = unsignedToSigned(
|
||||
unsignedByteToInt(value1) + (unsignedByteToInt(value2) << 8) + (unsignedByteToInt(value3) << 16), 24);
|
||||
return (float) (mantissa * Math.pow(10, value4));
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an unsigned integer to a two's-complement signed value.
|
||||
*/
|
||||
private static int unsignedToSigned(int unsigned, int size) {
|
||||
if ((unsigned & (1 << size - 1)) != 0) {
|
||||
return -1 * ((1 << size - 1) - (unsigned & ((1 << size - 1) - 1)));
|
||||
} else {
|
||||
return unsigned;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an integer into the signed bits of the specified length.
|
||||
*/
|
||||
private static int intToSignedBits(int i, int size) {
|
||||
if (i < 0) {
|
||||
return (1 << size - 1) + (i & ((1 << size - 1) - 1));
|
||||
} else {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
@ -12,21 +12,26 @@
|
||||
*/
|
||||
package org.openhab.binding.bluetooth;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.eclipse.jdt.annotation.DefaultLocation;
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.bluetooth.BluetoothDevice.ConnectionState;
|
||||
import org.openhab.binding.bluetooth.notification.BluetoothConnectionStatusNotification;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.binding.bluetooth.util.RetryFuture;
|
||||
import org.openhab.core.common.NamedThreadFactory;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.openhab.core.util.HexUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@ -36,17 +41,18 @@ import org.slf4j.LoggerFactory;
|
||||
*
|
||||
* @author Kai Kreuzer - Initial contribution and API
|
||||
*/
|
||||
@NonNullByDefault({ DefaultLocation.PARAMETER, DefaultLocation.RETURN_TYPE, DefaultLocation.ARRAY_CONTENTS,
|
||||
DefaultLocation.TYPE_ARGUMENT, DefaultLocation.TYPE_BOUND, DefaultLocation.TYPE_PARAMETER })
|
||||
@NonNullByDefault
|
||||
public class ConnectedBluetoothHandler extends BeaconBluetoothHandler {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(ConnectedBluetoothHandler.class);
|
||||
private ScheduledFuture<?> connectionJob;
|
||||
private @Nullable Future<?> reconnectJob;
|
||||
private @Nullable Future<?> pendingDisconnect;
|
||||
|
||||
// internal flag for the service resolution status
|
||||
protected volatile boolean resolved = false;
|
||||
private boolean alwaysConnected;
|
||||
private int idleDisconnectDelay = 1000;
|
||||
|
||||
protected final Set<BluetoothCharacteristic> deviceCharacteristics = new CopyOnWriteArraySet<>();
|
||||
// we initially set the to scheduler so that we can keep this field non-null
|
||||
private ScheduledExecutorService connectionTaskExecutor = scheduler;
|
||||
|
||||
public ConnectedBluetoothHandler(Thing thing) {
|
||||
super(thing);
|
||||
@ -54,55 +60,198 @@ public class ConnectedBluetoothHandler extends BeaconBluetoothHandler {
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
|
||||
// super.initialize adds callbacks that might require the connectionTaskExecutor to be present, so we initialize
|
||||
// the connectionTaskExecutor first
|
||||
ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1,
|
||||
new NamedThreadFactory("bluetooth-connection" + thing.getThingTypeUID(), true));
|
||||
executor.setExecuteExistingDelayedTasksAfterShutdownPolicy(false);
|
||||
executor.setRemoveOnCancelPolicy(true);
|
||||
connectionTaskExecutor = executor;
|
||||
|
||||
super.initialize();
|
||||
|
||||
connectionJob = scheduler.scheduleWithFixedDelay(() -> {
|
||||
try {
|
||||
if (device.getConnectionState() != ConnectionState.CONNECTED) {
|
||||
device.connect();
|
||||
// we do not set the Thing status here, because we will anyhow receive a call to
|
||||
// onConnectionStateChange
|
||||
} else {
|
||||
// just in case it was already connected to begin with
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
if (!resolved && !device.discoverServices()) {
|
||||
logger.debug("Error while discovering services");
|
||||
if (thing.getStatus() == ThingStatus.OFFLINE) {
|
||||
// something went wrong in super.initialize() so we shouldn't initialize further here either
|
||||
return;
|
||||
}
|
||||
|
||||
Object alwaysConnectRaw = getConfig().get(BluetoothBindingConstants.CONFIGURATION_ALWAYS_CONNECTED);
|
||||
alwaysConnected = !Boolean.FALSE.equals(alwaysConnectRaw);
|
||||
|
||||
Object idleDisconnectDelayRaw = getConfig().get(BluetoothBindingConstants.CONFIGURATION_IDLE_DISCONNECT_DELAY);
|
||||
idleDisconnectDelay = 1000;
|
||||
if (idleDisconnectDelayRaw instanceof Number) {
|
||||
idleDisconnectDelay = ((Number) idleDisconnectDelayRaw).intValue();
|
||||
}
|
||||
|
||||
if (alwaysConnected) {
|
||||
reconnectJob = connectionTaskExecutor.scheduleWithFixedDelay(() -> {
|
||||
try {
|
||||
if (device.getConnectionState() != ConnectionState.CONNECTED) {
|
||||
if (!device.connect()) {
|
||||
logger.debug("Failed to connect to {}", address);
|
||||
}
|
||||
// we do not set the Thing status here, because we will anyhow receive a call to
|
||||
// onConnectionStateChange
|
||||
} else {
|
||||
// just in case it was already connected to begin with
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
if (!device.isServicesDiscovered() && !device.discoverServices()) {
|
||||
logger.debug("Error while discovering services");
|
||||
}
|
||||
}
|
||||
} catch (RuntimeException ex) {
|
||||
logger.warn("Unexpected error occurred", ex);
|
||||
}
|
||||
} catch (RuntimeException ex) {
|
||||
logger.warn("Unexpected error occurred", ex);
|
||||
}
|
||||
}, 0, 30, TimeUnit.SECONDS);
|
||||
}, 0, 30, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
if (connectionJob != null) {
|
||||
connectionJob.cancel(true);
|
||||
connectionJob = null;
|
||||
}
|
||||
cancel(reconnectJob, true);
|
||||
reconnectJob = null;
|
||||
cancel(pendingDisconnect, true);
|
||||
pendingDisconnect = null;
|
||||
|
||||
super.dispose();
|
||||
|
||||
// just in case something goes really wrong in the core and it tries to dispose a handler before initializing it
|
||||
if (scheduler != connectionTaskExecutor) {
|
||||
connectionTaskExecutor.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
super.handleCommand(channelUID, command);
|
||||
private static void cancel(@Nullable Future<?> future, boolean interrupt) {
|
||||
if (future != null) {
|
||||
future.cancel(interrupt);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle REFRESH
|
||||
if (command == RefreshType.REFRESH) {
|
||||
for (BluetoothCharacteristic characteristic : deviceCharacteristics) {
|
||||
if (characteristic.getGattCharacteristic() != null
|
||||
&& channelUID.getId().equals(characteristic.getGattCharacteristic().name())) {
|
||||
device.readCharacteristic(characteristic);
|
||||
break;
|
||||
}
|
||||
public void connect() {
|
||||
connectionTaskExecutor.execute(() -> {
|
||||
if (!device.connect()) {
|
||||
logger.debug("Failed to connect to {}", address);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void disconnect() {
|
||||
connectionTaskExecutor.execute(device::disconnect);
|
||||
}
|
||||
|
||||
private void scheduleDisconnect() {
|
||||
cancel(pendingDisconnect, false);
|
||||
pendingDisconnect = connectionTaskExecutor.schedule(device::disconnect, idleDisconnectDelay,
|
||||
TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
private void connectAndWait() throws ConnectionException, TimeoutException, InterruptedException {
|
||||
if (device.getConnectionState() == ConnectionState.CONNECTED) {
|
||||
return;
|
||||
}
|
||||
if (device.getConnectionState() != ConnectionState.CONNECTING) {
|
||||
if (!device.connect()) {
|
||||
throw new ConnectionException("Failed to start connecting");
|
||||
}
|
||||
}
|
||||
if (!device.awaitConnection(1, TimeUnit.SECONDS)) {
|
||||
throw new TimeoutException("Connection attempt timeout.");
|
||||
}
|
||||
if (!device.isServicesDiscovered()) {
|
||||
device.discoverServices();
|
||||
if (!device.awaitServiceDiscovery(10, TimeUnit.SECONDS)) {
|
||||
throw new TimeoutException("Service discovery timeout");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelLinked(ChannelUID channelUID) {
|
||||
super.channelLinked(channelUID);
|
||||
private BluetoothCharacteristic connectAndGetCharacteristic(UUID serviceUUID, UUID characteristicUUID)
|
||||
throws BluetoothException, TimeoutException, InterruptedException {
|
||||
connectAndWait();
|
||||
BluetoothService service = device.getServices(serviceUUID);
|
||||
if (service == null) {
|
||||
throw new BluetoothException("Service with uuid " + serviceUUID + " could not be found");
|
||||
}
|
||||
BluetoothCharacteristic characteristic = service.getCharacteristic(characteristicUUID);
|
||||
if (characteristic == null) {
|
||||
throw new BluetoothException("Characteristic with uuid " + characteristicUUID + " could not be found");
|
||||
}
|
||||
return characteristic;
|
||||
}
|
||||
|
||||
private <T> CompletableFuture<T> executeWithConnection(UUID serviceUUID, UUID characteristicUUID,
|
||||
Function<BluetoothCharacteristic, CompletableFuture<T>> callable) {
|
||||
if (connectionTaskExecutor == scheduler) {
|
||||
return CompletableFuture
|
||||
.failedFuture(new IllegalStateException("connectionTaskExecutor has not been initialized"));
|
||||
}
|
||||
if (connectionTaskExecutor.isShutdown()) {
|
||||
return CompletableFuture.failedFuture(new IllegalStateException("connectionTaskExecutor is shut down"));
|
||||
}
|
||||
// we use a RetryFuture because it supports running Callable instances
|
||||
return RetryFuture.callWithRetry(() -> {
|
||||
// we block for completion here so that we keep the lock on the connectionTaskExecutor active.
|
||||
return callable.apply(connectAndGetCharacteristic(serviceUUID, characteristicUUID)).get();
|
||||
}, connectionTaskExecutor)// we make this completion async so that operations chained off the returned future
|
||||
// will not run on the connectionTaskExecutor
|
||||
.whenCompleteAsync((r, th) -> {
|
||||
// we us a while loop here in case the exceptions get nested
|
||||
while (th instanceof CompletionException || th instanceof ExecutionException) {
|
||||
th = th.getCause();
|
||||
}
|
||||
if (th instanceof InterruptedException) {
|
||||
// we don't want to schedule anything if we receive an interrupt
|
||||
return;
|
||||
}
|
||||
if (th instanceof TimeoutException) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, th.getMessage());
|
||||
}
|
||||
if (!alwaysConnected) {
|
||||
scheduleDisconnect();
|
||||
}
|
||||
}, scheduler);
|
||||
}
|
||||
|
||||
public CompletableFuture<@Nullable Void> enableNotifications(UUID serviceUUID, UUID characteristicUUID) {
|
||||
return executeWithConnection(serviceUUID, characteristicUUID, device::enableNotifications);
|
||||
}
|
||||
|
||||
public CompletableFuture<@Nullable Void> writeCharacteristic(UUID serviceUUID, UUID characteristicUUID, byte[] data,
|
||||
boolean enableNotification) {
|
||||
var future = executeWithConnection(serviceUUID, characteristicUUID, characteristic -> {
|
||||
if (enableNotification) {
|
||||
return device.enableNotifications(characteristic)
|
||||
.thenCompose((v) -> device.writeCharacteristic(characteristic, data));
|
||||
} else {
|
||||
return device.writeCharacteristic(characteristic, data);
|
||||
}
|
||||
});
|
||||
if (logger.isDebugEnabled()) {
|
||||
future = future.whenComplete((v, t) -> {
|
||||
if (t == null) {
|
||||
logger.debug("Characteristic {} from {} has written value {}", characteristicUUID, address,
|
||||
HexUtils.bytesToHex(data));
|
||||
}
|
||||
});
|
||||
}
|
||||
return future;
|
||||
}
|
||||
|
||||
public CompletableFuture<byte[]> readCharacteristic(UUID serviceUUID, UUID characteristicUUID) {
|
||||
var future = executeWithConnection(serviceUUID, characteristicUUID, device::readCharacteristic);
|
||||
if (logger.isDebugEnabled()) {
|
||||
future = future.whenComplete((data, t) -> {
|
||||
if (t == null) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Characteristic {} from {} has been read - value {}", characteristicUUID, address,
|
||||
HexUtils.bytesToHex(data));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return future;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -110,10 +259,12 @@ public class ConnectedBluetoothHandler extends BeaconBluetoothHandler {
|
||||
// if there is no signal, we can be sure we are OFFLINE, but if there is a signal, we also have to check whether
|
||||
// we are connected.
|
||||
if (receivedSignal) {
|
||||
if (device.getConnectionState() == ConnectionState.CONNECTED) {
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
} else {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "Device is not connected.");
|
||||
if (alwaysConnected) {
|
||||
if (device.getConnectionState() == ConnectionState.CONNECTED) {
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
} else {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "Device is not connected.");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
|
||||
@ -126,24 +277,30 @@ public class ConnectedBluetoothHandler extends BeaconBluetoothHandler {
|
||||
switch (connectionNotification.getConnectionState()) {
|
||||
case DISCOVERED:
|
||||
// The device is now known on the Bluetooth network, so we can do something...
|
||||
scheduler.submit(() -> {
|
||||
if (device.getConnectionState() != ConnectionState.CONNECTED) {
|
||||
if (!device.connect()) {
|
||||
logger.debug("Error connecting to device after discovery.");
|
||||
if (alwaysConnected) {
|
||||
connectionTaskExecutor.submit(() -> {
|
||||
if (device.getConnectionState() != ConnectionState.CONNECTED) {
|
||||
if (!device.connect()) {
|
||||
logger.debug("Error connecting to device after discovery.");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
break;
|
||||
case CONNECTED:
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
scheduler.submit(() -> {
|
||||
if (!resolved && !device.discoverServices()) {
|
||||
logger.debug("Error while discovering services");
|
||||
}
|
||||
});
|
||||
if (alwaysConnected) {
|
||||
connectionTaskExecutor.submit(() -> {
|
||||
if (!device.isServicesDiscovered() && !device.discoverServices()) {
|
||||
logger.debug("Error while discovering services");
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
case DISCONNECTED:
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
|
||||
cancel(pendingDisconnect, false);
|
||||
if (alwaysConnected) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@ -151,51 +308,19 @@ public class ConnectedBluetoothHandler extends BeaconBluetoothHandler {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServicesDiscovered() {
|
||||
super.onServicesDiscovered();
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
logger.debug("Service discovery completed for '{}'", address);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCharacteristicReadComplete(BluetoothCharacteristic characteristic, BluetoothCompletionStatus status) {
|
||||
super.onCharacteristicReadComplete(characteristic, status);
|
||||
if (status == BluetoothCompletionStatus.SUCCESS) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Characteristic {} from {} has been read - value {}", characteristic.getUuid(), address,
|
||||
HexUtils.bytesToHex(characteristic.getByteValue()));
|
||||
}
|
||||
} else {
|
||||
logger.debug("Characteristic {} from {} has been read - ERROR", characteristic.getUuid(), address);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCharacteristicWriteComplete(BluetoothCharacteristic characteristic,
|
||||
BluetoothCompletionStatus status) {
|
||||
super.onCharacteristicWriteComplete(characteristic, status);
|
||||
public void onCharacteristicUpdate(BluetoothCharacteristic characteristic, byte[] value) {
|
||||
super.onCharacteristicUpdate(characteristic, value);
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Wrote {} to characteristic {} of device {}: {}",
|
||||
HexUtils.bytesToHex(characteristic.getByteValue()), characteristic.getUuid(), address, status);
|
||||
logger.debug("Recieved update {} to characteristic {} of device {}", HexUtils.bytesToHex(value),
|
||||
characteristic.getUuid(), address);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCharacteristicUpdate(BluetoothCharacteristic characteristic) {
|
||||
super.onCharacteristicUpdate(characteristic);
|
||||
public void onDescriptorUpdate(BluetoothDescriptor descriptor, byte[] value) {
|
||||
super.onDescriptorUpdate(descriptor, value);
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Recieved update {} to characteristic {} of device {}",
|
||||
HexUtils.bytesToHex(characteristic.getByteValue()), characteristic.getUuid(), address);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDescriptorUpdate(BluetoothDescriptor descriptor) {
|
||||
super.onDescriptorUpdate(descriptor);
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Received update {} to descriptor {} of device {}", HexUtils.bytesToHex(descriptor.getValue()),
|
||||
logger.debug("Received update {} to descriptor {} of device {}", HexUtils.bytesToHex(value),
|
||||
descriptor.getUuid(), address);
|
||||
}
|
||||
}
|
||||
|
47
bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/ConnectionException.java
Normal file
47
bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/ConnectionException.java
Normal file
@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.bluetooth;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* This is thrown when some kind of connection issue occurs as part of a bluetooth api call that expects a connection.
|
||||
*
|
||||
* @author Connor Petty - Initial contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ConnectionException extends BluetoothException {
|
||||
|
||||
private static final long serialVersionUID = 2966261738506666653L;
|
||||
|
||||
public ConnectionException() {
|
||||
super();
|
||||
}
|
||||
|
||||
public ConnectionException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
|
||||
super(message, cause, enableSuppression, writableStackTrace);
|
||||
}
|
||||
|
||||
public ConnectionException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public ConnectionException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public ConnectionException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
@ -15,6 +15,8 @@ package org.openhab.binding.bluetooth;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
@ -101,15 +103,21 @@ public abstract class DelegateBluetoothDevice extends BluetoothDevice {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean readCharacteristic(BluetoothCharacteristic characteristic) {
|
||||
public CompletableFuture<byte[]> readCharacteristic(BluetoothCharacteristic characteristic) {
|
||||
BluetoothDevice delegate = getDelegate();
|
||||
return delegate != null && delegate.readCharacteristic(characteristic);
|
||||
if (delegate == null) {
|
||||
return CompletableFuture.failedFuture(new IllegalStateException("Delegate is null"));
|
||||
}
|
||||
return delegate.readCharacteristic(characteristic);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean writeCharacteristic(BluetoothCharacteristic characteristic) {
|
||||
public CompletableFuture<@Nullable Void> writeCharacteristic(BluetoothCharacteristic characteristic, byte[] value) {
|
||||
BluetoothDevice delegate = getDelegate();
|
||||
return delegate != null && delegate.writeCharacteristic(characteristic);
|
||||
if (delegate == null) {
|
||||
return CompletableFuture.failedFuture(new IllegalStateException("Delegate is null"));
|
||||
}
|
||||
return delegate.writeCharacteristic(characteristic, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -119,15 +127,21 @@ public abstract class DelegateBluetoothDevice extends BluetoothDevice {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean enableNotifications(BluetoothCharacteristic characteristic) {
|
||||
public CompletableFuture<@Nullable Void> enableNotifications(BluetoothCharacteristic characteristic) {
|
||||
BluetoothDevice delegate = getDelegate();
|
||||
return delegate != null ? delegate.enableNotifications(characteristic) : false;
|
||||
if (delegate == null) {
|
||||
return CompletableFuture.failedFuture(new IllegalStateException("Delegate is null"));
|
||||
}
|
||||
return delegate.enableNotifications(characteristic);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean disableNotifications(BluetoothCharacteristic characteristic) {
|
||||
public CompletableFuture<@Nullable Void> disableNotifications(BluetoothCharacteristic characteristic) {
|
||||
BluetoothDevice delegate = getDelegate();
|
||||
return delegate != null ? delegate.disableNotifications(characteristic) : false;
|
||||
if (delegate == null) {
|
||||
return CompletableFuture.failedFuture(new IllegalStateException("Delegate is null"));
|
||||
}
|
||||
return delegate.disableNotifications(characteristic);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -160,6 +174,24 @@ public abstract class DelegateBluetoothDevice extends BluetoothDevice {
|
||||
return delegate != null ? delegate.getCharacteristic(uuid) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean awaitConnection(long timeout, TimeUnit unit) throws InterruptedException {
|
||||
BluetoothDevice delegate = getDelegate();
|
||||
return delegate != null ? delegate.awaitConnection(timeout, unit) : false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean awaitServiceDiscovery(long timeout, TimeUnit unit) throws InterruptedException {
|
||||
BluetoothDevice delegate = getDelegate();
|
||||
return delegate != null ? delegate.awaitServiceDiscovery(timeout, unit) : false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isServicesDiscovered() {
|
||||
BluetoothDevice delegate = getDelegate();
|
||||
return delegate != null ? delegate.isServicesDiscovered() : false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void dispose() {
|
||||
BluetoothDevice delegate = getDelegate();
|
||||
|
@ -20,10 +20,10 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.Condition;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
@ -34,13 +34,9 @@ import org.openhab.binding.bluetooth.BluetoothBindingConstants;
|
||||
import org.openhab.binding.bluetooth.BluetoothCharacteristic;
|
||||
import org.openhab.binding.bluetooth.BluetoothCharacteristic.GattCharacteristic;
|
||||
import org.openhab.binding.bluetooth.BluetoothCompanyIdentifiers;
|
||||
import org.openhab.binding.bluetooth.BluetoothCompletionStatus;
|
||||
import org.openhab.binding.bluetooth.BluetoothDescriptor;
|
||||
import org.openhab.binding.bluetooth.BluetoothDevice.ConnectionState;
|
||||
import org.openhab.binding.bluetooth.BluetoothDeviceListener;
|
||||
import org.openhab.binding.bluetooth.BluetoothUtils;
|
||||
import org.openhab.binding.bluetooth.discovery.BluetoothDiscoveryParticipant;
|
||||
import org.openhab.binding.bluetooth.notification.BluetoothConnectionStatusNotification;
|
||||
import org.openhab.binding.bluetooth.notification.BluetoothScanNotification;
|
||||
import org.openhab.core.config.discovery.DiscoveryResult;
|
||||
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
|
||||
import org.openhab.core.thing.Thing;
|
||||
@ -56,28 +52,16 @@ import org.slf4j.LoggerFactory;
|
||||
* @author Connor Petty - Initial Contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class BluetoothDiscoveryProcess implements Supplier<DiscoveryResult>, BluetoothDeviceListener {
|
||||
public class BluetoothDiscoveryProcess implements Supplier<DiscoveryResult> {
|
||||
|
||||
private static final int DISCOVERY_TTL = 300;
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(BluetoothDiscoveryProcess.class);
|
||||
|
||||
private final Lock serviceDiscoveryLock = new ReentrantLock();
|
||||
private final Condition connectionCondition = serviceDiscoveryLock.newCondition();
|
||||
private final Condition serviceDiscoveryCondition = serviceDiscoveryLock.newCondition();
|
||||
private final Condition infoDiscoveryCondition = serviceDiscoveryLock.newCondition();
|
||||
|
||||
private final BluetoothDeviceSnapshot device;
|
||||
private final Collection<BluetoothDiscoveryParticipant> participants;
|
||||
private final Set<BluetoothAdapter> adapters;
|
||||
|
||||
private volatile boolean servicesDiscovered = false;
|
||||
|
||||
/**
|
||||
* Contains characteristic which reading is ongoing or null if no ongoing readings.
|
||||
*/
|
||||
private volatile @Nullable GattCharacteristic ongoingGattCharacteristic;
|
||||
|
||||
public BluetoothDiscoveryProcess(BluetoothDeviceSnapshot device,
|
||||
Collection<BluetoothDiscoveryParticipant> participants, Set<BluetoothAdapter> adapters) {
|
||||
this.participants = participants;
|
||||
@ -166,41 +150,31 @@ public class BluetoothDiscoveryProcess implements Supplier<DiscoveryResult>, Blu
|
||||
.withBridge(device.getAdapter().getUID()).withLabel(label).build();
|
||||
}
|
||||
|
||||
// this is really just a special return type for `ensureConnected`
|
||||
private static class ConnectionException extends Exception {
|
||||
|
||||
}
|
||||
|
||||
private void ensureConnected() throws ConnectionException, InterruptedException {
|
||||
if (device.getConnectionState() != ConnectionState.CONNECTED) {
|
||||
if (device.getConnectionState() != ConnectionState.CONNECTING && !device.connect()) {
|
||||
logger.debug("Connection attempt failed to start for device {}", device.getAddress());
|
||||
// something failed, so we abandon connection discovery
|
||||
throw new ConnectionException();
|
||||
}
|
||||
if (!awaitConnection(10, TimeUnit.SECONDS)) {
|
||||
logger.debug("Connection to device {} timed out", device.getAddress());
|
||||
throw new ConnectionException();
|
||||
}
|
||||
if (!servicesDiscovered) {
|
||||
device.discoverServices();
|
||||
if (!awaitServiceDiscovery(10, TimeUnit.SECONDS)) {
|
||||
logger.debug("Service discovery for device {} timed out", device.getAddress());
|
||||
// something failed, so we abandon connection discovery
|
||||
throw new ConnectionException();
|
||||
}
|
||||
}
|
||||
readDeviceInformationIfMissing();
|
||||
logger.debug("Device information fetched from the device: {}", device);
|
||||
}
|
||||
}
|
||||
|
||||
private @Nullable DiscoveryResult findConnectionResult(List<BluetoothDiscoveryParticipant> connectionParticipants) {
|
||||
try {
|
||||
device.addListener(this);
|
||||
for (BluetoothDiscoveryParticipant participant : connectionParticipants) {
|
||||
// we call this every time just in case a participant somehow closes the connection
|
||||
ensureConnected();
|
||||
if (device.getConnectionState() != ConnectionState.CONNECTED) {
|
||||
if (device.getConnectionState() != ConnectionState.CONNECTING && !device.connect()) {
|
||||
logger.debug("Connection attempt failed to start for device {}", device.getAddress());
|
||||
// something failed, so we abandon connection discovery
|
||||
return null;
|
||||
}
|
||||
if (!device.awaitConnection(1, TimeUnit.SECONDS)) {
|
||||
logger.debug("Connection to device {} timed out", device.getAddress());
|
||||
return null;
|
||||
}
|
||||
if (!device.isServicesDiscovered()) {
|
||||
device.discoverServices();
|
||||
if (!device.awaitServiceDiscovery(10, TimeUnit.SECONDS)) {
|
||||
logger.debug("Service discovery for device {} timed out", device.getAddress());
|
||||
// something failed, so we abandon connection discovery
|
||||
return null;
|
||||
}
|
||||
}
|
||||
readDeviceInformationIfMissing();
|
||||
logger.debug("Device information fetched from the device: {}", device);
|
||||
}
|
||||
|
||||
try {
|
||||
DiscoveryResult result = participant.createResult(device);
|
||||
if (result != null) {
|
||||
@ -210,180 +184,49 @@ public class BluetoothDiscoveryProcess implements Supplier<DiscoveryResult>, Blu
|
||||
logger.warn("Participant '{}' threw an exception", participant.getClass().getName(), e);
|
||||
}
|
||||
}
|
||||
} catch (InterruptedException | ConnectionException e) {
|
||||
} catch (InterruptedException e) {
|
||||
// do nothing
|
||||
} finally {
|
||||
device.removeListener(this);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScanRecordReceived(BluetoothScanNotification scanNotification) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnectionStateChange(BluetoothConnectionStatusNotification connectionNotification) {
|
||||
if (connectionNotification.getConnectionState() == ConnectionState.CONNECTED) {
|
||||
serviceDiscoveryLock.lock();
|
||||
try {
|
||||
connectionCondition.signal();
|
||||
} finally {
|
||||
serviceDiscoveryLock.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void readDeviceInformationIfMissing() throws InterruptedException {
|
||||
if (device.getName() == null) {
|
||||
fecthGattCharacteristic(GattCharacteristic.DEVICE_NAME);
|
||||
fecthGattCharacteristic(GattCharacteristic.DEVICE_NAME, device::setName);
|
||||
}
|
||||
if (device.getModel() == null) {
|
||||
fecthGattCharacteristic(GattCharacteristic.MODEL_NUMBER_STRING);
|
||||
fecthGattCharacteristic(GattCharacteristic.MODEL_NUMBER_STRING, device::setModel);
|
||||
}
|
||||
if (device.getSerialNumber() == null) {
|
||||
fecthGattCharacteristic(GattCharacteristic.SERIAL_NUMBER_STRING);
|
||||
fecthGattCharacteristic(GattCharacteristic.SERIAL_NUMBER_STRING, device::setSerialNumberl);
|
||||
}
|
||||
if (device.getHardwareRevision() == null) {
|
||||
fecthGattCharacteristic(GattCharacteristic.HARDWARE_REVISION_STRING);
|
||||
fecthGattCharacteristic(GattCharacteristic.HARDWARE_REVISION_STRING, device::setHardwareRevision);
|
||||
}
|
||||
if (device.getFirmwareRevision() == null) {
|
||||
fecthGattCharacteristic(GattCharacteristic.FIRMWARE_REVISION_STRING);
|
||||
fecthGattCharacteristic(GattCharacteristic.FIRMWARE_REVISION_STRING, device::setFirmwareRevision);
|
||||
}
|
||||
if (device.getSoftwareRevision() == null) {
|
||||
fecthGattCharacteristic(GattCharacteristic.SOFTWARE_REVISION_STRING);
|
||||
fecthGattCharacteristic(GattCharacteristic.SOFTWARE_REVISION_STRING, device::setSoftwareRevision);
|
||||
}
|
||||
}
|
||||
|
||||
private void fecthGattCharacteristic(GattCharacteristic gattCharacteristic) throws InterruptedException {
|
||||
private void fecthGattCharacteristic(GattCharacteristic gattCharacteristic, Consumer<String> consumer)
|
||||
throws InterruptedException {
|
||||
UUID uuid = gattCharacteristic.getUUID();
|
||||
BluetoothCharacteristic characteristic = device.getCharacteristic(uuid);
|
||||
if (characteristic == null) {
|
||||
logger.debug("Device '{}' doesn't support uuid '{}'", device.getAddress(), uuid);
|
||||
return;
|
||||
}
|
||||
if (!device.readCharacteristic(characteristic)) {
|
||||
logger.debug("Failed to aquire uuid {} from device {}", uuid, device.getAddress());
|
||||
return;
|
||||
}
|
||||
ongoingGattCharacteristic = gattCharacteristic;
|
||||
if (!awaitInfoResponse(1, TimeUnit.SECONDS)) {
|
||||
logger.debug("Device info (uuid {}) for device {} timed out", uuid, device.getAddress());
|
||||
ongoingGattCharacteristic = null;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean awaitConnection(long timeout, TimeUnit unit) throws InterruptedException {
|
||||
serviceDiscoveryLock.lock();
|
||||
try {
|
||||
long nanosTimeout = unit.toNanos(timeout);
|
||||
while (device.getConnectionState() != ConnectionState.CONNECTED) {
|
||||
if (nanosTimeout <= 0L) {
|
||||
return false;
|
||||
}
|
||||
nanosTimeout = connectionCondition.awaitNanos(nanosTimeout);
|
||||
}
|
||||
} finally {
|
||||
serviceDiscoveryLock.unlock();
|
||||
byte[] value = device.readCharacteristic(characteristic).get(1, TimeUnit.SECONDS);
|
||||
consumer.accept(BluetoothUtils.getStringValue(value, 0));
|
||||
} catch (ExecutionException e) {
|
||||
logger.debug("Failed to aquire uuid {} from device {}: {}", uuid, device.getAddress(), e.getMessage());
|
||||
} catch (TimeoutException e) {
|
||||
logger.debug("Device info (uuid {}) for device {} timed out: {}", uuid, device.getAddress(),
|
||||
e.getMessage());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean awaitInfoResponse(long timeout, TimeUnit unit) throws InterruptedException {
|
||||
serviceDiscoveryLock.lock();
|
||||
try {
|
||||
long nanosTimeout = unit.toNanos(timeout);
|
||||
while (ongoingGattCharacteristic != null) {
|
||||
if (nanosTimeout <= 0L) {
|
||||
return false;
|
||||
}
|
||||
nanosTimeout = infoDiscoveryCondition.awaitNanos(nanosTimeout);
|
||||
}
|
||||
} finally {
|
||||
serviceDiscoveryLock.unlock();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean awaitServiceDiscovery(long timeout, TimeUnit unit) throws InterruptedException {
|
||||
serviceDiscoveryLock.lock();
|
||||
try {
|
||||
long nanosTimeout = unit.toNanos(timeout);
|
||||
while (!servicesDiscovered) {
|
||||
if (nanosTimeout <= 0L) {
|
||||
return false;
|
||||
}
|
||||
nanosTimeout = serviceDiscoveryCondition.awaitNanos(nanosTimeout);
|
||||
}
|
||||
} finally {
|
||||
serviceDiscoveryLock.unlock();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServicesDiscovered() {
|
||||
serviceDiscoveryLock.lock();
|
||||
try {
|
||||
servicesDiscovered = true;
|
||||
serviceDiscoveryCondition.signal();
|
||||
} finally {
|
||||
serviceDiscoveryLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCharacteristicReadComplete(BluetoothCharacteristic characteristic, BluetoothCompletionStatus status) {
|
||||
serviceDiscoveryLock.lock();
|
||||
try {
|
||||
if (status == BluetoothCompletionStatus.SUCCESS) {
|
||||
switch (characteristic.getGattCharacteristic()) {
|
||||
case DEVICE_NAME:
|
||||
device.setName(characteristic.getStringValue(0));
|
||||
break;
|
||||
case MODEL_NUMBER_STRING:
|
||||
device.setModel(characteristic.getStringValue(0));
|
||||
break;
|
||||
case SERIAL_NUMBER_STRING:
|
||||
device.setSerialNumberl(characteristic.getStringValue(0));
|
||||
break;
|
||||
case HARDWARE_REVISION_STRING:
|
||||
device.setHardwareRevision(characteristic.getStringValue(0));
|
||||
break;
|
||||
case FIRMWARE_REVISION_STRING:
|
||||
device.setFirmwareRevision(characteristic.getStringValue(0));
|
||||
break;
|
||||
case SOFTWARE_REVISION_STRING:
|
||||
device.setSoftwareRevision(characteristic.getStringValue(0));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (ongoingGattCharacteristic == characteristic.getGattCharacteristic()) {
|
||||
ongoingGattCharacteristic = null;
|
||||
infoDiscoveryCondition.signal();
|
||||
}
|
||||
} finally {
|
||||
serviceDiscoveryLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCharacteristicWriteComplete(BluetoothCharacteristic characteristic,
|
||||
BluetoothCompletionStatus status) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCharacteristicUpdate(BluetoothCharacteristic characteristic) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDescriptorUpdate(BluetoothDescriptor bluetoothDescriptor) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAdapterChanged(BluetoothAdapter adapter) {
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,7 @@
|
||||
*/
|
||||
package org.openhab.binding.bluetooth;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
@ -73,14 +74,16 @@ public class MockBluetoothDevice extends BaseBluetoothDevice {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean readCharacteristic(BluetoothCharacteristic characteristic) {
|
||||
public CompletableFuture<byte[]> readCharacteristic(BluetoothCharacteristic characteristic) {
|
||||
if (characteristic.getGattCharacteristic() == GattCharacteristic.DEVICE_NAME) {
|
||||
characteristic.setValue(deviceName);
|
||||
notifyListeners(BluetoothEventType.CHARACTERISTIC_READ_COMPLETE, characteristic,
|
||||
BluetoothCompletionStatus.SUCCESS);
|
||||
return true;
|
||||
String name = deviceName;
|
||||
if (name != null) {
|
||||
return CompletableFuture.completedFuture(name.getBytes(StandardCharsets.UTF_8));
|
||||
} else {
|
||||
return CompletableFuture.completedFuture(new byte[0]);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return CompletableFuture.failedFuture(new UnsupportedOperationException());
|
||||
}
|
||||
|
||||
public void setDeviceName(String deviceName) {
|
||||
@ -93,13 +96,13 @@ public class MockBluetoothDevice extends BaseBluetoothDevice {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean writeCharacteristic(BluetoothCharacteristic characteristic) {
|
||||
return false;
|
||||
public CompletableFuture<@Nullable Void> writeCharacteristic(BluetoothCharacteristic characteristic, byte[] value) {
|
||||
return CompletableFuture.failedFuture(new UnsupportedOperationException());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean enableNotifications(BluetoothCharacteristic characteristic) {
|
||||
return false;
|
||||
public CompletableFuture<@Nullable Void> enableNotifications(BluetoothCharacteristic characteristic) {
|
||||
return CompletableFuture.failedFuture(new UnsupportedOperationException());
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -108,8 +111,8 @@ public class MockBluetoothDevice extends BaseBluetoothDevice {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean disableNotifications(BluetoothCharacteristic characteristic) {
|
||||
return false;
|
||||
public CompletableFuture<@Nullable Void> disableNotifications(BluetoothCharacteristic characteristic) {
|
||||
return CompletableFuture.failedFuture(new UnsupportedOperationException());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
Loading…
x
Reference in New Issue
Block a user