[bluetooth.bluegiga] Add characteristic notification support (#9067)

* Add support for characteristic notifications.
* Also fixed bluegiga initialize/dispose bugs.

Signed-off-by: Connor Petty <mistercpp2000+gitsignoff@gmail.com>
This commit is contained in:
Connor Petty
2020-11-24 12:33:48 -08:00
committed by GitHub
parent f9e38cbf2f
commit 32e8ec31f9
6 changed files with 600 additions and 228 deletions

View File

@@ -0,0 +1,55 @@
/**
* Copyright (c) 2010-2020 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.bluegiga;
import java.util.UUID;
import org.openhab.binding.bluetooth.BluetoothCharacteristic;
/**
* The {@link BlueGigaBluetoothCharacteristic} class extends BluetoothCharacteristic
* to provide write access to certain BluetoothCharacteristic fields that BlueGiga
* may not be initially aware of during characteristic construction but must be discovered
* later.
*
* @author Connor Petty - Initial contribution
*
*/
public class BlueGigaBluetoothCharacteristic extends BluetoothCharacteristic {
private boolean notificationEnabled;
public BlueGigaBluetoothCharacteristic(int handle) {
super(null, handle);
}
public void setProperties(int properties) {
this.properties = properties;
}
public void setHandle(int handle) {
this.handle = handle;
}
public void setUUID(UUID uuid) {
this.uuid = uuid;
}
public boolean isNotificationEnabled() {
return notificationEnabled;
}
public void setNotificationEnabled(boolean enable) {
this.notificationEnabled = enable;
}
}

View File

@@ -12,7 +12,13 @@
*/ */
package org.openhab.binding.bluetooth.bluegiga; package org.openhab.binding.bluetooth.bluegiga;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.NavigableMap;
import java.util.TreeMap;
import java.util.UUID;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@@ -21,6 +27,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.bluetooth.BaseBluetoothDevice; import org.openhab.binding.bluetooth.BaseBluetoothDevice;
import org.openhab.binding.bluetooth.BluetoothAddress; import org.openhab.binding.bluetooth.BluetoothAddress;
import org.openhab.binding.bluetooth.BluetoothBindingConstants;
import org.openhab.binding.bluetooth.BluetoothCharacteristic; import org.openhab.binding.bluetooth.BluetoothCharacteristic;
import org.openhab.binding.bluetooth.BluetoothCompletionStatus; import org.openhab.binding.bluetooth.BluetoothCompletionStatus;
import org.openhab.binding.bluetooth.BluetoothDescriptor; import org.openhab.binding.bluetooth.BluetoothDescriptor;
@@ -59,6 +66,9 @@ public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements Blue
private final Logger logger = LoggerFactory.getLogger(BlueGigaBluetoothDevice.class); private final Logger logger = LoggerFactory.getLogger(BlueGigaBluetoothDevice.class);
private Map<Integer, UUID> handleToUUID = new HashMap<>();
private NavigableMap<Integer, BlueGigaBluetoothCharacteristic> handleToCharacteristic = new TreeMap<>();
// BlueGiga needs to know the address type when connecting // BlueGiga needs to know the address type when connecting
private BluetoothAddressType addressType = BluetoothAddressType.UNKNOWN; private BluetoothAddressType addressType = BluetoothAddressType.UNKNOWN;
@@ -70,8 +80,11 @@ public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements Blue
NONE, NONE,
GET_SERVICES, GET_SERVICES,
GET_CHARACTERISTICS, GET_CHARACTERISTICS,
READ_CHARACTERISTIC_DECL,
CHARACTERISTIC_READ, CHARACTERISTIC_READ,
CHARACTERISTIC_WRITE CHARACTERISTIC_WRITE,
NOTIFICATION_ENABLE,
NOTIFICATION_DISABLE
} }
private BlueGigaProcedure procedureProgress = BlueGigaProcedure.NONE; private BlueGigaProcedure procedureProgress = BlueGigaProcedure.NONE;
@@ -135,13 +148,13 @@ public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements Blue
public boolean connect() { public boolean connect() {
if (connection != -1) { if (connection != -1) {
// We're already connected // We're already connected
return false; return true;
} }
cancelTimer(connectTimer); cancelTimer(connectTimer);
if (bgHandler.bgConnect(address, addressType)) { if (bgHandler.bgConnect(address, addressType)) {
connectionState = ConnectionState.CONNECTING; connectionState = ConnectionState.CONNECTING;
connectTimer = startTimer(connectTimeoutTask, TIMEOUT_SEC); connectTimer = startTimer(connectTimeoutTask, 10);
return true; return true;
} else { } else {
connectionState = ConnectionState.DISCONNECTED; connectionState = ConnectionState.DISCONNECTED;
@@ -153,7 +166,7 @@ public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements Blue
public boolean disconnect() { public boolean disconnect() {
if (connection == -1) { if (connection == -1) {
// We're already disconnected // We're already disconnected
return false; return true;
} }
return bgHandler.bgDisconnect(connection); return bgHandler.bgDisconnect(connection);
@@ -177,16 +190,104 @@ public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements Blue
@Override @Override
public boolean enableNotifications(BluetoothCharacteristic characteristic) { public boolean enableNotifications(BluetoothCharacteristic characteristic) {
// TODO will be implemented in a followup PR if (connection == -1) {
logger.debug("Cannot enable notifications, device not connected {}", this);
return false; return false;
} }
BlueGigaBluetoothCharacteristic ch = (BlueGigaBluetoothCharacteristic) characteristic;
if (ch.isNotificationEnabled()) {
return true;
}
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;
}
if (procedureProgress != BlueGigaProcedure.NONE) {
logger.debug("Procedure already in progress {}", procedureProgress);
return false;
}
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;
}
procedureTimer = startTimer(procedureTimeoutTask, TIMEOUT_SEC);
procedureProgress = BlueGigaProcedure.NOTIFICATION_ENABLE;
procedureCharacteristic = characteristic;
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;
}
@Override @Override
public boolean disableNotifications(BluetoothCharacteristic characteristic) { public boolean disableNotifications(BluetoothCharacteristic characteristic) {
// TODO will be implemented in a followup PR if (connection == -1) {
logger.debug("Cannot enable notifications, device not connected {}", this);
return false; return false;
} }
BlueGigaBluetoothCharacteristic ch = (BlueGigaBluetoothCharacteristic) characteristic;
if (ch.isNotificationEnabled()) {
return true;
}
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;
}
if (procedureProgress != BlueGigaProcedure.NONE) {
logger.debug("Procedure already in progress {}", procedureProgress);
return false;
}
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;
}
procedureTimer = startTimer(procedureTimeoutTask, TIMEOUT_SEC);
procedureProgress = BlueGigaProcedure.NOTIFICATION_DISABLE;
procedureCharacteristic = characteristic;
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;
}
@Override @Override
public boolean enableNotifications(BluetoothDescriptor descriptor) { public boolean enableNotifications(BluetoothDescriptor descriptor) {
// TODO will be implemented in a followup PR // TODO will be implemented in a followup PR
@@ -204,6 +305,9 @@ public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements Blue
if (characteristic == null || characteristic.getHandle() == 0) { if (characteristic == null || characteristic.getHandle() == 0) {
return false; return false;
} }
if (connection == -1) {
return false;
}
if (procedureProgress != BlueGigaProcedure.NONE) { if (procedureProgress != BlueGigaProcedure.NONE) {
return false; return false;
@@ -225,6 +329,9 @@ public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements Blue
if (characteristic == null || characteristic.getHandle() == 0) { if (characteristic == null || characteristic.getHandle() == 0) {
return false; return false;
} }
if (connection == -1) {
return false;
}
if (procedureProgress != BlueGigaProcedure.NONE) { if (procedureProgress != BlueGigaProcedure.NONE) {
return false; return false;
@@ -404,7 +511,7 @@ public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements Blue
return; return;
} }
logger.trace("BlueGiga Group: {} svcs={}", this, supportedServices); logger.trace("BlueGiga Group: {} event={}", this, event);
updateLastSeenTime(); updateLastSeenTime();
BluetoothService service = new BluetoothService(event.getUuid(), true, event.getStart(), event.getEnd()); BluetoothService service = new BluetoothService(event.getUuid(), true, event.getStart(), event.getEnd());
@@ -417,18 +524,32 @@ public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements Blue
return; return;
} }
logger.trace("BlueGiga FindInfo: {} svcs={}", this, supportedServices); logger.trace("BlueGiga FindInfo: {} event={}", this, event);
updateLastSeenTime(); updateLastSeenTime();
BluetoothCharacteristic characteristic = new BluetoothCharacteristic(event.getUuid(), event.getChrHandle()); int handle = event.getChrHandle();
UUID attUUID = event.getUuid();
BluetoothService service = getServiceByHandle(characteristic.getHandle()); BluetoothService service = getServiceByHandle(handle);
if (service == null) { if (service == null) {
logger.debug("BlueGiga: Unable to find service for handle {}", characteristic.getHandle()); logger.debug("BlueGiga: Unable to find service for handle {}", handle);
return; return;
} }
handleToUUID.put(handle, attUUID);
if (BluetoothBindingConstants.ATTR_CHARACTERISTIC_DECLARATION.equals(attUUID)) {
BlueGigaBluetoothCharacteristic characteristic = new BlueGigaBluetoothCharacteristic(handle);
characteristic.setService(service); characteristic.setService(service);
service.addCharacteristic(characteristic); handleToCharacteristic.put(handle, characteristic);
} else {
Integer chrHandle = handleToCharacteristic.floorKey(handle);
if (chrHandle == null) {
logger.debug("BlueGiga: Unable to find characteristic for handle {}", handle);
return;
}
BlueGigaBluetoothCharacteristic characteristic = handleToCharacteristic.get(chrHandle);
characteristic.addDescriptor(new BluetoothDescriptor(characteristic, attUUID, handle));
}
} }
private void handleProcedureCompletedEvent(BlueGigaProcedureCompletedEvent event) { private void handleProcedureCompletedEvent(BlueGigaProcedureCompletedEvent event) {
@@ -458,7 +579,16 @@ public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements Blue
} }
break; break;
case GET_CHARACTERISTICS: case GET_CHARACTERISTICS:
// We've downloaded all 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;
} else {
procedureProgress = BlueGigaProcedure.NONE;
}
break;
case READ_CHARACTERISTIC_DECL:
// We've downloaded read all the declarations, we are done now
procedureProgress = BlueGigaProcedure.NONE; procedureProgress = BlueGigaProcedure.NONE;
notifyListeners(BluetoothEventType.SERVICES_DISCOVERED); notifyListeners(BluetoothEventType.SERVICES_DISCOVERED);
break; break;
@@ -478,6 +608,24 @@ public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements Blue
procedureProgress = BlueGigaProcedure.NONE; procedureProgress = BlueGigaProcedure.NONE;
procedureCharacteristic = null; procedureCharacteristic = null;
break; break;
case NOTIFICATION_ENABLE:
boolean success = event.getResult() == BgApiResponse.SUCCESS;
if (!success) {
logger.debug("write to descriptor failed");
}
((BlueGigaBluetoothCharacteristic) procedureCharacteristic).setNotificationEnabled(success);
procedureProgress = BlueGigaProcedure.NONE;
procedureCharacteristic = null;
break;
case NOTIFICATION_DISABLE:
success = event.getResult() == BgApiResponse.SUCCESS;
if (!success) {
logger.debug("write to descriptor failed");
}
((BlueGigaBluetoothCharacteristic) procedureCharacteristic).setNotificationEnabled(!success);
procedureProgress = BlueGigaProcedure.NONE;
procedureCharacteristic = null;
break;
default: default:
break; break;
} }
@@ -507,6 +655,10 @@ public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements Blue
return; return;
} }
for (BlueGigaBluetoothCharacteristic ch : handleToCharacteristic.values()) {
ch.setNotificationEnabled(false);
}
cancelTimer(procedureTimer); cancelTimer(procedureTimer);
connectionState = ConnectionState.DISCONNECTED; connectionState = ConnectionState.DISCONNECTED;
connection = -1; connection = -1;
@@ -524,10 +676,31 @@ public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements Blue
updateLastSeenTime(); updateLastSeenTime();
BluetoothCharacteristic characteristic = getCharacteristicByHandle(event.getAttHandle()); logger.trace("BlueGiga AttributeValue: {} event={}", this, event);
if (characteristic == null) {
int handle = event.getAttHandle();
Map.Entry<Integer, BlueGigaBluetoothCharacteristic> entry = handleToCharacteristic.floorEntry(handle);
if (entry == null) {
logger.debug("BlueGiga didn't find characteristic for event {}", event); logger.debug("BlueGiga didn't find characteristic for event {}", event);
} else { return;
}
BlueGigaBluetoothCharacteristic characteristic = entry.getValue();
if (handle == entry.getKey()) {
// this is the declaration
if (parseDeclaration(characteristic, event.getValue())) {
BluetoothService service = getServiceByHandle(handle);
if (service == null) {
logger.debug("BlueGiga: Unable to find service for handle {}", handle);
return;
}
service.addCharacteristic(characteristic);
}
return;
}
if (handle == characteristic.getHandle()) {
characteristic.setValue(event.getValue().clone()); characteristic.setValue(event.getValue().clone());
// If this is the characteristic we were reading, then send a read completion // If this is the characteristic we were reading, then send a read completion
@@ -537,10 +710,55 @@ public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements Blue
procedureCharacteristic = null; procedureCharacteristic = null;
notifyListeners(BluetoothEventType.CHARACTERISTIC_READ_COMPLETE, characteristic, notifyListeners(BluetoothEventType.CHARACTERISTIC_READ_COMPLETE, characteristic,
BluetoothCompletionStatus.SUCCESS); BluetoothCompletionStatus.SUCCESS);
return;
} }
// Notify the user of the updated value // Notify the user of the updated value
notifyListeners(BluetoothEventType.CHARACTERISTIC_UPDATED, characteristic); notifyListeners(BluetoothEventType.CHARACTERISTIC_UPDATED, characteristic);
} else {
// it must be one of the descriptors we need to update
UUID attUUID = handleToUUID.get(handle);
BluetoothDescriptor descriptor = characteristic.getDescriptor(attUUID);
descriptor.setValue(toBytes(event.getValue()));
notifyListeners(BluetoothEventType.DESCRIPTOR_UPDATED, descriptor);
}
}
private static byte @Nullable [] toBytes(int @Nullable [] value) {
if (value == null) {
return null;
}
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));
buffer.order(ByteOrder.LITTLE_ENDIAN);
ch.setProperties(Byte.toUnsignedInt(buffer.get()));
ch.setHandle(Short.toUnsignedInt(buffer.getShort()));
switch (buffer.remaining()) {
case 2:
long key = Short.toUnsignedLong(buffer.getShort());
ch.setUUID(BluetoothBindingConstants.createBluetoothUUID(key));
return true;
case 4:
key = Integer.toUnsignedLong(buffer.getInt());
ch.setUUID(BluetoothBindingConstants.createBluetoothUUID(key));
return true;
case 16:
long lower = buffer.getLong();
long upper = buffer.getLong();
ch.setUUID(new UUID(upper, lower));
return true;
default:
logger.debug("Unexpected uuid length: {}", buffer.remaining());
return false;
} }
} }

View File

@@ -20,6 +20,9 @@ import java.io.OutputStream;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Future; import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
@@ -50,6 +53,8 @@ import org.openhab.binding.bluetooth.bluegiga.internal.command.attributeclient.B
import org.openhab.binding.bluetooth.bluegiga.internal.command.attributeclient.BlueGigaReadByGroupTypeResponse; import org.openhab.binding.bluetooth.bluegiga.internal.command.attributeclient.BlueGigaReadByGroupTypeResponse;
import org.openhab.binding.bluetooth.bluegiga.internal.command.attributeclient.BlueGigaReadByHandleCommand; import org.openhab.binding.bluetooth.bluegiga.internal.command.attributeclient.BlueGigaReadByHandleCommand;
import org.openhab.binding.bluetooth.bluegiga.internal.command.attributeclient.BlueGigaReadByHandleResponse; import org.openhab.binding.bluetooth.bluegiga.internal.command.attributeclient.BlueGigaReadByHandleResponse;
import org.openhab.binding.bluetooth.bluegiga.internal.command.attributeclient.BlueGigaReadByTypeCommand;
import org.openhab.binding.bluetooth.bluegiga.internal.command.attributeclient.BlueGigaReadByTypeResponse;
import org.openhab.binding.bluetooth.bluegiga.internal.command.connection.BlueGigaConnectionStatusEvent; import org.openhab.binding.bluetooth.bluegiga.internal.command.connection.BlueGigaConnectionStatusEvent;
import org.openhab.binding.bluetooth.bluegiga.internal.command.connection.BlueGigaDisconnectCommand; import org.openhab.binding.bluetooth.bluegiga.internal.command.connection.BlueGigaDisconnectCommand;
import org.openhab.binding.bluetooth.bluegiga.internal.command.connection.BlueGigaDisconnectResponse; import org.openhab.binding.bluetooth.bluegiga.internal.command.connection.BlueGigaDisconnectResponse;
@@ -76,6 +81,8 @@ import org.openhab.binding.bluetooth.bluegiga.internal.enumeration.BluetoothAddr
import org.openhab.binding.bluetooth.bluegiga.internal.enumeration.GapConnectableMode; import org.openhab.binding.bluetooth.bluegiga.internal.enumeration.GapConnectableMode;
import org.openhab.binding.bluetooth.bluegiga.internal.enumeration.GapDiscoverMode; import org.openhab.binding.bluetooth.bluegiga.internal.enumeration.GapDiscoverMode;
import org.openhab.binding.bluetooth.bluegiga.internal.enumeration.GapDiscoverableMode; import org.openhab.binding.bluetooth.bluegiga.internal.enumeration.GapDiscoverableMode;
import org.openhab.binding.bluetooth.util.RetryException;
import org.openhab.binding.bluetooth.util.RetryFuture;
import org.openhab.core.common.ThreadPoolManager; import org.openhab.core.common.ThreadPoolManager;
import org.openhab.core.io.transport.serial.PortInUseException; import org.openhab.core.io.transport.serial.PortInUseException;
import org.openhab.core.io.transport.serial.SerialPort; import org.openhab.core.io.transport.serial.SerialPort;
@@ -118,9 +125,6 @@ public class BlueGigaBridgeHandler extends AbstractBluetoothBridgeHandler<BlueGi
private final ScheduledExecutorService executor = ThreadPoolManager.getScheduledPool("BlueGiga"); private final ScheduledExecutorService executor = ThreadPoolManager.getScheduledPool("BlueGiga");
// The serial port.
private Optional<SerialPort> serialPort = Optional.empty();
private BlueGigaConfiguration configuration = new BlueGigaConfiguration(); private BlueGigaConfiguration configuration = new BlueGigaConfiguration();
// The serial port input stream. // The serial port input stream.
@@ -130,10 +134,13 @@ public class BlueGigaBridgeHandler extends AbstractBluetoothBridgeHandler<BlueGi
private Optional<OutputStream> outputStream = Optional.empty(); private Optional<OutputStream> outputStream = Optional.empty();
// The BlueGiga API handler // The BlueGiga API handler
private Optional<BlueGigaSerialHandler> serialHandler = Optional.empty(); private CompletableFuture<BlueGigaSerialHandler> serialHandler = CompletableFuture
.failedFuture(new IllegalStateException("Uninitialized"));
// The BlueGiga transaction manager // The BlueGiga transaction manager
private Optional<BlueGigaTransactionManager> transactionManager = Optional.empty(); @NonNullByDefault({})
private CompletableFuture<BlueGigaTransactionManager> transactionManager = CompletableFuture
.failedFuture(new IllegalStateException("Uninitialized"));
// The maximum number of connections this interface supports // The maximum number of connections this interface supports
private int maxConnections = 0; private int maxConnections = 0;
@@ -146,7 +153,9 @@ public class BlueGigaBridgeHandler extends AbstractBluetoothBridgeHandler<BlueGi
private volatile boolean initComplete = false; private volatile boolean initComplete = false;
private @Nullable ScheduledFuture<?> initTask; private CompletableFuture<SerialPort> serialPortFuture = CompletableFuture
.failedFuture(new IllegalStateException("Uninitialized"));
private @Nullable ScheduledFuture<?> removeInactiveDevicesTask; private @Nullable ScheduledFuture<?> removeInactiveDevicesTask;
private @Nullable ScheduledFuture<?> discoveryTask; private @Nullable ScheduledFuture<?> discoveryTask;
@@ -159,39 +168,80 @@ public class BlueGigaBridgeHandler extends AbstractBluetoothBridgeHandler<BlueGi
@Override @Override
public void initialize() { public void initialize() {
logger.info("Initializing BlueGiga");
super.initialize(); super.initialize();
Optional<BlueGigaConfiguration> cfg = Optional.of(getConfigAs(BlueGigaConfiguration.class)); Optional<BlueGigaConfiguration> cfg = Optional.of(getConfigAs(BlueGigaConfiguration.class));
updateStatus(ThingStatus.UNKNOWN);
if (cfg.isPresent()) { if (cfg.isPresent()) {
configuration = cfg.get(); configuration = cfg.get();
initTask = executor.scheduleWithFixedDelay(this::start, 0, INITIALIZATION_INTERVAL_SEC, TimeUnit.SECONDS); serialPortFuture = RetryFuture.callWithRetry(() -> {
} else { var localFuture = serialPortFuture;
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR);
}
}
@Override
public void dispose() {
stop();
stopScheduledTasks();
if (initTask != null) {
initTask.cancel(true);
}
super.dispose();
}
private void start() {
try {
if (!initComplete) {
logger.debug("Initialize BlueGiga"); logger.debug("Initialize BlueGiga");
logger.debug("Using configuration: {}", configuration); logger.debug("Using configuration: {}", configuration);
stop();
if (openSerialPort(configuration.port, 115200)) {
serialHandler = Optional.of(new BlueGigaSerialHandler(inputStream.get(), outputStream.get()));
transactionManager = Optional.of(new BlueGigaTransactionManager(serialHandler.get(), executor));
serialHandler.get().addHandlerListener(this);
transactionManager.get().addEventListener(this);
updateStatus(ThingStatus.UNKNOWN);
String serialPortName = configuration.port;
int baudRate = 115200;
logger.debug("Connecting to serial port '{}'", serialPortName);
try {
SerialPortIdentifier portIdentifier = serialPortManager.getIdentifier(serialPortName);
if (portIdentifier == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Port does not exist");
throw new RetryException(INITIALIZATION_INTERVAL_SEC, TimeUnit.SECONDS);
}
SerialPort sp = portIdentifier.open("org.openhab.binding.bluetooth.bluegiga", 2000);
sp.setSerialPortParams(baudRate, SerialPort.DATABITS_8, SerialPort.STOPBITS_1,
SerialPort.PARITY_NONE);
sp.setFlowControlMode(SerialPort.FLOWCONTROL_RTSCTS_OUT);
sp.enableReceiveThreshold(1);
sp.enableReceiveTimeout(2000);
// RXTX serial port library causes high CPU load
// Start event listener, which will just sleep and slow down event loop
sp.notifyOnDataAvailable(true);
logger.info("Connected to serial port '{}'.", serialPortName);
try {
inputStream = Optional.of(new BufferedInputStream(sp.getInputStream()));
outputStream = Optional.of(new BufferedOutputStream(sp.getOutputStream()));
} catch (IOException e) {
logger.error("Error getting serial streams", e);
throw new RetryException(INITIALIZATION_INTERVAL_SEC, TimeUnit.SECONDS);
}
// if this future has been cancelled while this was running, then we
// need to make sure that we close this port
localFuture.whenComplete((port, th) -> {
if (th != null) {
// we need to shut down the port now.
closeSerialPort(sp);
}
});
return sp;
} catch (PortInUseException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
"Serial Error: Port in use");
throw new RetryException(INITIALIZATION_INTERVAL_SEC, TimeUnit.SECONDS);
} catch (UnsupportedCommOperationException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
"Serial Error: Unsupported operation");
throw new RetryException(INITIALIZATION_INTERVAL_SEC, TimeUnit.SECONDS);
} catch (RuntimeException ex) {
logger.debug("Start failed", ex);
throw new RetryException(INITIALIZATION_INTERVAL_SEC, TimeUnit.SECONDS);
}
}, executor);
serialHandler = serialPortFuture
.thenApply(sp -> new BlueGigaSerialHandler(inputStream.get(), outputStream.get()));
transactionManager = serialHandler.thenApply(sh -> {
BlueGigaTransactionManager th = new BlueGigaTransactionManager(sh, executor);
sh.addHandlerListener(this);
th.addEventListener(this);
return th;
});
transactionManager.thenRun(() -> {
try { try {
// Stop any procedures that are running // Stop any procedures that are running
bgEndProcedure(); bgEndProcedure();
@@ -221,30 +271,43 @@ public class BlueGigaBridgeHandler extends AbstractBluetoothBridgeHandler<BlueGi
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
"Initialization of BlueGiga controller failed"); "Initialization of BlueGiga controller failed");
} }
}).exceptionally(th -> {
if (th instanceof CompletionException && th.getCause() instanceof CancellationException) {
// cancellation is a normal reason for failure, so no need to print it.
return null;
} }
} logger.warn("Error initializing bluegiga", th);
} catch (RuntimeException e) { return null;
// Avoid scheduled task to shutdown });
// e.g. when BlueGiga module is detached
logger.debug("Start failed", e); } else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR);
} }
} }
@Override
public void dispose() {
logger.info("Disposing BlueGiga");
stop();
stopScheduledTasks();
super.dispose();
}
private void stop() { private void stop() {
if (transactionManager.isPresent()) { transactionManager.thenAccept(tman -> {
transactionManager.get().removeEventListener(this); tman.removeEventListener(this);
transactionManager.get().close(); tman.close();
transactionManager = Optional.empty(); });
} serialHandler.thenAccept(sh -> {
if (serialHandler.isPresent()) { sh.removeHandlerListener(this);
serialHandler.get().removeHandlerListener(this); sh.close();
serialHandler.get().close(); });
serialHandler = Optional.empty();
}
address = null; address = null;
initComplete = false; initComplete = false;
connections.clear(); connections.clear();
closeSerialPort();
serialPortFuture.thenAccept(this::closeSerialPort);
serialPortFuture.cancel(false);
} }
private void schedulePassiveScan() { private void schedulePassiveScan() {
@@ -268,7 +331,6 @@ public class BlueGigaBridgeHandler extends AbstractBluetoothBridgeHandler<BlueGi
private void startScheduledTasks() { private void startScheduledTasks() {
schedulePassiveScan(); schedulePassiveScan();
logger.debug("Start scheduled task to remove inactive devices");
discoveryTask = scheduler.scheduleWithFixedDelay(this::refreshDiscoveredDevices, 0, 10, TimeUnit.SECONDS); discoveryTask = scheduler.scheduleWithFixedDelay(this::refreshDiscoveredDevices, 0, 10, TimeUnit.SECONDS);
} }
@@ -309,49 +371,7 @@ public class BlueGigaBridgeHandler extends AbstractBluetoothBridgeHandler<BlueGi
updateProperties(properties); updateProperties(properties);
} }
private boolean openSerialPort(final String serialPortName, int baudRate) { private void closeSerialPort(SerialPort sp) {
logger.debug("Connecting to serial port '{}'", serialPortName);
try {
SerialPortIdentifier portIdentifier = serialPortManager.getIdentifier(serialPortName);
if (portIdentifier == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Port does not exist");
return false;
}
SerialPort sp = portIdentifier.open("org.openhab.binding.bluetooth.bluegiga", 2000);
sp.setSerialPortParams(baudRate, SerialPort.DATABITS_8, SerialPort.STOPBITS_1, SerialPort.PARITY_NONE);
sp.setFlowControlMode(SerialPort.FLOWCONTROL_RTSCTS_OUT);
sp.enableReceiveThreshold(1);
sp.enableReceiveTimeout(2000);
// RXTX serial port library causes high CPU load
// Start event listener, which will just sleep and slow down event loop
sp.notifyOnDataAvailable(true);
logger.info("Connected to serial port '{}'.", serialPortName);
try {
inputStream = Optional.of(new BufferedInputStream(sp.getInputStream()));
outputStream = Optional.of(new BufferedOutputStream(sp.getOutputStream()));
} catch (IOException e) {
logger.error("Error getting serial streams", e);
return false;
}
serialPort = Optional.of(sp);
return true;
} catch (PortInUseException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
"Serial Error: Port in use");
return false;
} catch (UnsupportedCommOperationException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
"Serial Error: Unsupported operation");
return false;
}
}
private void closeSerialPort() {
serialPort.ifPresent(sp -> {
sp.removeEventListener(); sp.removeEventListener();
try { try {
sp.disableReceiveTimeout(); sp.disableReceiveTimeout();
@@ -366,11 +386,9 @@ public class BlueGigaBridgeHandler extends AbstractBluetoothBridgeHandler<BlueGi
}); });
sp.close(); sp.close();
logger.debug("Closed serial port."); logger.debug("Closed serial port.");
serialPort = Optional.empty();
inputStream = Optional.empty(); inputStream = Optional.empty();
outputStream = Optional.empty(); outputStream = Optional.empty();
} }
});
} }
@Override @Override
@@ -528,6 +546,25 @@ public class BlueGigaBridgeHandler extends AbstractBluetoothBridgeHandler<BlueGi
} }
} }
public boolean bgReadCharacteristicDeclarations(int connectionHandle) {
logger.debug("BlueGiga Find: connection {}", connectionHandle);
// @formatter:off
BlueGigaReadByTypeCommand command = new BlueGigaReadByTypeCommand.CommandBuilder()
.withConnection(connectionHandle)
.withStart(1)
.withEnd(65535)
.withUUID(BluetoothBindingConstants.ATTR_CHARACTERISTIC_DECLARATION)
.build();
// @formatter:on
try {
return sendCommand(command, BlueGigaReadByTypeResponse.class, true).getResult() == BgApiResponse.SUCCESS;
} catch (BlueGigaException e) {
logger.debug("Error occured when sending read characteristics command to device {}, reason: {}.", address,
e.getMessage());
return false;
}
}
/** /**
* Read a characteristic using {@link BlueGigaReadByHandleCommand} * Read a characteristic using {@link BlueGigaReadByHandleCommand}
* *
@@ -665,8 +702,9 @@ public class BlueGigaBridgeHandler extends AbstractBluetoothBridgeHandler<BlueGi
*/ */
private <T extends BlueGigaResponse> T sendCommandWithoutChecks(BlueGigaCommand command, Class<T> expectedResponse) private <T extends BlueGigaResponse> T sendCommandWithoutChecks(BlueGigaCommand command, Class<T> expectedResponse)
throws BlueGigaException { throws BlueGigaException {
if (transactionManager.isPresent()) { BlueGigaTransactionManager manager = transactionManager.getNow(null);
return transactionManager.get().sendTransaction(command, expectedResponse, COMMAND_TIMEOUT_MS); if (manager != null) {
return manager.sendTransaction(command, expectedResponse, COMMAND_TIMEOUT_MS);
} else { } else {
throw new BlueGigaException("Transaction manager missing"); throw new BlueGigaException("Transaction manager missing");
} }
@@ -678,7 +716,7 @@ public class BlueGigaBridgeHandler extends AbstractBluetoothBridgeHandler<BlueGi
* @param listener the {@link BlueGigaEventListener} to add * @param listener the {@link BlueGigaEventListener} to add
*/ */
public void addEventListener(BlueGigaEventListener listener) { public void addEventListener(BlueGigaEventListener listener) {
transactionManager.ifPresent(manager -> { transactionManager.thenAccept(manager -> {
manager.addEventListener(listener); manager.addEventListener(listener);
}); });
} }
@@ -689,7 +727,7 @@ public class BlueGigaBridgeHandler extends AbstractBluetoothBridgeHandler<BlueGi
* @param listener the {@link BlueGigaEventListener} to remove * @param listener the {@link BlueGigaEventListener} to remove
*/ */
public void removeEventListener(BlueGigaEventListener listener) { public void removeEventListener(BlueGigaEventListener listener) {
transactionManager.ifPresent(manager -> { transactionManager.thenAccept(manager -> {
manager.removeEventListener(listener); manager.removeEventListener(listener);
}); });
} }

View File

@@ -65,6 +65,9 @@ public class BlueGigaSerialHandler {
flush(); flush();
parserThread = createBlueGigaBLEHandler(); parserThread = createBlueGigaBLEHandler();
parserThread.setUncaughtExceptionHandler((t, th) -> {
logger.warn("BluegigaSerialHandler terminating due to unhandled error", th);
});
parserThread.setDaemon(true); parserThread.setDaemon(true);
parserThread.start(); parserThread.start();
int tries = 0; int tries = 0;
@@ -232,11 +235,9 @@ public class BlueGigaSerialHandler {
} }
} }
private Thread createBlueGigaBLEHandler() { private void inboundMessageHandlerLoop() {
final int framecheckParams[] = new int[] { 0x00, 0x7F, 0xC0, 0xF8, 0xE0 }; final int[] framecheckParams = { 0x00, 0x7F, 0xC0, 0xF8, 0xE0 };
return new Thread("BlueGigaBLEHandler") {
@Override
public void run() {
int exceptionCnt = 0; int exceptionCnt = 0;
logger.trace("BlueGiga BLE thread started"); logger.trace("BlueGiga BLE thread started");
int[] inputBuffer = new int[BLE_MAX_LENGTH]; int[] inputBuffer = new int[BLE_MAX_LENGTH];
@@ -309,6 +310,8 @@ public class BlueGigaSerialHandler {
} }
logger.debug("BlueGiga BLE exited."); logger.debug("BlueGiga BLE exited.");
} }
};
private Thread createBlueGigaBLEHandler() {
return new Thread(this::inboundMessageHandlerLoop, "BlueGigaBLEHandler");
} }
} }

View File

@@ -79,7 +79,7 @@ public class BlueGigaAttributeWriteCommand extends BlueGigaDeviceCommand {
if (c > 0) { if (c > 0) {
builder.append(' '); builder.append(' ');
} }
builder.append(String.format("%02X", data[c])); builder.append(String.format("%02X", data[c] & 0xFF));
} }
builder.append(']'); builder.append(']');
return builder.toString(); return builder.toString();

View File

@@ -56,6 +56,13 @@ public class BlueGigaReadByTypeCommand extends BlueGigaDeviceCommand {
*/ */
private UUID uuid = new UUID(0, 0); private UUID uuid = new UUID(0, 0);
private BlueGigaReadByTypeCommand(CommandBuilder builder) {
this.connection = builder.connection;
this.start = builder.start;
this.end = builder.end;
this.uuid = builder.uuid;
}
/** /**
* First attribute handle * First attribute handle
* *
@@ -111,4 +118,55 @@ public class BlueGigaReadByTypeCommand extends BlueGigaDeviceCommand {
builder.append(']'); builder.append(']');
return builder.toString(); return builder.toString();
} }
public static class CommandBuilder {
private int connection;
private int start;
private int end;
private UUID uuid = new UUID(0, 0);
/**
* Set connection handle.
*
* @param connection the connection to set as {@link int}
*/
public CommandBuilder withConnection(int connection) {
this.connection = connection;
return this;
}
/**
* First requested handle number
*
* @param start the start to set as {@link int}
*/
public CommandBuilder withStart(int start) {
this.start = start;
return this;
}
/**
* Last requested handle number
*
* @param end the end to set as {@link int}
*/
public CommandBuilder withEnd(int end) {
this.end = end;
return this;
}
/**
* Attribute type (UUID)
*
* @param uuid the uuid to set as {@link UUID}
*/
public CommandBuilder withUUID(UUID uuid) {
this.uuid = uuid;
return this;
}
public BlueGigaReadByTypeCommand build() {
return new BlueGigaReadByTypeCommand(this);
}
}
} }