]> git.basschouten.com Git - openhab-addons.git/commitdiff
[bluetooth.bluegiga] Add characteristic notification support (#9067)
authorConnor Petty <mistercpp2000@gmail.com>
Tue, 24 Nov 2020 20:33:48 +0000 (12:33 -0800)
committerGitHub <noreply@github.com>
Tue, 24 Nov 2020 20:33:48 +0000 (21:33 +0100)
* Add support for characteristic notifications.
* Also fixed bluegiga initialize/dispose bugs.

Signed-off-by: Connor Petty <mistercpp2000+gitsignoff@gmail.com>
bundles/org.openhab.binding.bluetooth.bluegiga/src/main/java/org/openhab/binding/bluetooth/bluegiga/BlueGigaBluetoothCharacteristic.java [new file with mode: 0644]
bundles/org.openhab.binding.bluetooth.bluegiga/src/main/java/org/openhab/binding/bluetooth/bluegiga/BlueGigaBluetoothDevice.java
bundles/org.openhab.binding.bluetooth.bluegiga/src/main/java/org/openhab/binding/bluetooth/bluegiga/handler/BlueGigaBridgeHandler.java
bundles/org.openhab.binding.bluetooth.bluegiga/src/main/java/org/openhab/binding/bluetooth/bluegiga/internal/BlueGigaSerialHandler.java
bundles/org.openhab.binding.bluetooth.bluegiga/src/main/java/org/openhab/binding/bluetooth/bluegiga/internal/command/attributeclient/BlueGigaAttributeWriteCommand.java
bundles/org.openhab.binding.bluetooth.bluegiga/src/main/java/org/openhab/binding/bluetooth/bluegiga/internal/command/attributeclient/BlueGigaReadByTypeCommand.java

diff --git a/bundles/org.openhab.binding.bluetooth.bluegiga/src/main/java/org/openhab/binding/bluetooth/bluegiga/BlueGigaBluetoothCharacteristic.java b/bundles/org.openhab.binding.bluetooth.bluegiga/src/main/java/org/openhab/binding/bluetooth/bluegiga/BlueGigaBluetoothCharacteristic.java
new file mode 100644 (file)
index 0000000..dad7b74
--- /dev/null
@@ -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;
+    }
+}
index d7f24cd447c893f741353dc55761e8ec17d51e5a..2c5e68ef5b929924a04606f97db015b3b74dd8ea 100644 (file)
  */
 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.NavigableMap;
+import java.util.TreeMap;
+import java.util.UUID;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.TimeUnit;
@@ -21,6 +27,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
 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;
@@ -59,6 +66,9 @@ public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements Blue
 
     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
     private BluetoothAddressType addressType = BluetoothAddressType.UNKNOWN;
 
@@ -70,8 +80,11 @@ public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements Blue
         NONE,
         GET_SERVICES,
         GET_CHARACTERISTICS,
+        READ_CHARACTERISTIC_DECL,
         CHARACTERISTIC_READ,
-        CHARACTERISTIC_WRITE
+        CHARACTERISTIC_WRITE,
+        NOTIFICATION_ENABLE,
+        NOTIFICATION_DISABLE
     }
 
     private BlueGigaProcedure procedureProgress = BlueGigaProcedure.NONE;
@@ -135,13 +148,13 @@ public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements Blue
     public boolean connect() {
         if (connection != -1) {
             // We're already connected
-            return false;
+            return true;
         }
 
         cancelTimer(connectTimer);
         if (bgHandler.bgConnect(address, addressType)) {
             connectionState = ConnectionState.CONNECTING;
-            connectTimer = startTimer(connectTimeoutTask, TIMEOUT_SEC);
+            connectTimer = startTimer(connectTimeoutTask, 10);
             return true;
         } else {
             connectionState = ConnectionState.DISCONNECTED;
@@ -153,7 +166,7 @@ public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements Blue
     public boolean disconnect() {
         if (connection == -1) {
             // We're already disconnected
-            return false;
+            return true;
         }
 
         return bgHandler.bgDisconnect(connection);
@@ -177,14 +190,102 @@ public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements Blue
 
     @Override
     public boolean enableNotifications(BluetoothCharacteristic characteristic) {
-        // TODO will be implemented in a followup PR
-        return false;
+        if (connection == -1) {
+            logger.debug("Cannot enable notifications, device not connected {}", this);
+            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
     public boolean disableNotifications(BluetoothCharacteristic characteristic) {
-        // TODO will be implemented in a followup PR
-        return false;
+        if (connection == -1) {
+            logger.debug("Cannot enable notifications, device not connected {}", this);
+            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
@@ -204,6 +305,9 @@ public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements Blue
         if (characteristic == null || characteristic.getHandle() == 0) {
             return false;
         }
+        if (connection == -1) {
+            return false;
+        }
 
         if (procedureProgress != BlueGigaProcedure.NONE) {
             return false;
@@ -225,6 +329,9 @@ public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements Blue
         if (characteristic == null || characteristic.getHandle() == 0) {
             return false;
         }
+        if (connection == -1) {
+            return false;
+        }
 
         if (procedureProgress != BlueGigaProcedure.NONE) {
             return false;
@@ -404,7 +511,7 @@ public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements Blue
             return;
         }
 
-        logger.trace("BlueGiga Group: {} svcs={}", this, supportedServices);
+        logger.trace("BlueGiga Group: {} event={}", this, event);
         updateLastSeenTime();
 
         BluetoothService service = new BluetoothService(event.getUuid(), true, event.getStart(), event.getEnd());
@@ -417,18 +524,32 @@ public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements Blue
             return;
         }
 
-        logger.trace("BlueGiga FindInfo: {} svcs={}", this, supportedServices);
+        logger.trace("BlueGiga FindInfo: {} event={}", this, event);
         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) {
-            logger.debug("BlueGiga: Unable to find service for handle {}", characteristic.getHandle());
+            logger.debug("BlueGiga: Unable to find service for handle {}", handle);
             return;
         }
-        characteristic.setService(service);
-        service.addCharacteristic(characteristic);
+        handleToUUID.put(handle, attUUID);
+
+        if (BluetoothBindingConstants.ATTR_CHARACTERISTIC_DECLARATION.equals(attUUID)) {
+            BlueGigaBluetoothCharacteristic characteristic = new BlueGigaBluetoothCharacteristic(handle);
+            characteristic.setService(service);
+            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) {
@@ -458,7 +579,16 @@ public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements Blue
                 }
                 break;
             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;
                 notifyListeners(BluetoothEventType.SERVICES_DISCOVERED);
                 break;
@@ -478,6 +608,24 @@ public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements Blue
                 procedureProgress = BlueGigaProcedure.NONE;
                 procedureCharacteristic = null;
                 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:
                 break;
         }
@@ -507,6 +655,10 @@ public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements Blue
             return;
         }
 
+        for (BlueGigaBluetoothCharacteristic ch : handleToCharacteristic.values()) {
+            ch.setNotificationEnabled(false);
+        }
+
         cancelTimer(procedureTimer);
         connectionState = ConnectionState.DISCONNECTED;
         connection = -1;
@@ -524,10 +676,31 @@ public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements Blue
 
         updateLastSeenTime();
 
-        BluetoothCharacteristic characteristic = getCharacteristicByHandle(event.getAttHandle());
-        if (characteristic == null) {
+        logger.trace("BlueGiga AttributeValue: {} event={}", this, event);
+
+        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);
-        } 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());
 
             // 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;
                 notifyListeners(BluetoothEventType.CHARACTERISTIC_READ_COMPLETE, characteristic,
                         BluetoothCompletionStatus.SUCCESS);
+                return;
             }
 
             // Notify the user of the updated value
             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;
         }
     }
 
index a6c54f65cc5773c3c9a3092f68300d439aeca511..33839abe5f28c79af2e65c5da972a96413211c34 100644 (file)
@@ -20,6 +20,9 @@ import java.io.OutputStream;
 import java.util.Map;
 import java.util.Optional;
 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.Future;
 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.BlueGigaReadByHandleCommand;
 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.BlueGigaDisconnectCommand;
 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.GapDiscoverMode;
 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.io.transport.serial.PortInUseException;
 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");
 
-    // The serial port.
-    private Optional<SerialPort> serialPort = Optional.empty();
-
     private BlueGigaConfiguration configuration = new BlueGigaConfiguration();
 
     // The serial port input stream.
@@ -130,10 +134,13 @@ public class BlueGigaBridgeHandler extends AbstractBluetoothBridgeHandler<BlueGi
     private Optional<OutputStream> outputStream = Optional.empty();
 
     // The BlueGiga API handler
-    private Optional<BlueGigaSerialHandler> serialHandler = Optional.empty();
+    private CompletableFuture<BlueGigaSerialHandler> serialHandler = CompletableFuture
+            .failedFuture(new IllegalStateException("Uninitialized"));
 
     // 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
     private int maxConnections = 0;
@@ -146,7 +153,9 @@ public class BlueGigaBridgeHandler extends AbstractBluetoothBridgeHandler<BlueGi
 
     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<?> discoveryTask;
 
@@ -159,92 +168,146 @@ public class BlueGigaBridgeHandler extends AbstractBluetoothBridgeHandler<BlueGi
 
     @Override
     public void initialize() {
+        logger.info("Initializing BlueGiga");
         super.initialize();
         Optional<BlueGigaConfiguration> cfg = Optional.of(getConfigAs(BlueGigaConfiguration.class));
+        updateStatus(ThingStatus.UNKNOWN);
         if (cfg.isPresent()) {
             configuration = cfg.get();
-            initTask = executor.scheduleWithFixedDelay(this::start, 0, INITIALIZATION_INTERVAL_SEC, TimeUnit.SECONDS);
-        } else {
-            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) {
+            serialPortFuture = RetryFuture.callWithRetry(() -> {
+                var localFuture = serialPortFuture;
                 logger.debug("Initialize BlueGiga");
                 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);
 
-                    try {
-                        // Stop any procedures that are running
-                        bgEndProcedure();
+                String serialPortName = configuration.port;
+                int baudRate = 115200;
 
-                        // Set mode to non-discoverable etc.
-                        bgSetMode();
+                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);
 
-                        // Get maximum parallel connections
-                        maxConnections = readMaxConnections().getMaxconn();
+                    sp.setFlowControlMode(SerialPort.FLOWCONTROL_RTSCTS_OUT);
+                    sp.enableReceiveThreshold(1);
+                    sp.enableReceiveTimeout(2000);
 
-                        // Close all connections so we start from a known position
-                        for (int connection = 0; connection < maxConnections; connection++) {
-                            sendCommandWithoutChecks(
-                                    new BlueGigaDisconnectCommand.CommandBuilder().withConnection(connection).build(),
-                                    BlueGigaDisconnectResponse.class);
+                    // 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 {
+                    // Stop any procedures that are running
+                    bgEndProcedure();
+
+                    // Set mode to non-discoverable etc.
+                    bgSetMode();
+
+                    // Get maximum parallel connections
+                    maxConnections = readMaxConnections().getMaxconn();
+
+                    // Close all connections so we start from a known position
+                    for (int connection = 0; connection < maxConnections; connection++) {
+                        sendCommandWithoutChecks(
+                                new BlueGigaDisconnectCommand.CommandBuilder().withConnection(connection).build(),
+                                BlueGigaDisconnectResponse.class);
+                    }
 
-                        // Get our Bluetooth address
-                        address = new BluetoothAddress(readAddress().getAddress());
+                    // Get our Bluetooth address
+                    address = new BluetoothAddress(readAddress().getAddress());
 
-                        updateThingProperties();
+                    updateThingProperties();
 
-                        initComplete = true;
-                        updateStatus(ThingStatus.ONLINE);
-                        startScheduledTasks();
-                    } catch (BlueGigaException e) {
-                        updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
-                                "Initialization of BlueGiga controller failed");
-                    }
+                    initComplete = true;
+                    updateStatus(ThingStatus.ONLINE);
+                    startScheduledTasks();
+                } catch (BlueGigaException e) {
+                    updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
+                            "Initialization of BlueGiga controller failed");
                 }
-            }
-        } catch (RuntimeException e) {
-            // Avoid scheduled task to shutdown
-            // e.g. when BlueGiga module is detached
-            logger.debug("Start failed", e);
+            }).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);
+                return null;
+            });
+
+        } else {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR);
         }
     }
 
+    @Override
+    public void dispose() {
+        logger.info("Disposing BlueGiga");
+        stop();
+        stopScheduledTasks();
+        super.dispose();
+    }
+
     private void stop() {
-        if (transactionManager.isPresent()) {
-            transactionManager.get().removeEventListener(this);
-            transactionManager.get().close();
-            transactionManager = Optional.empty();
-        }
-        if (serialHandler.isPresent()) {
-            serialHandler.get().removeHandlerListener(this);
-            serialHandler.get().close();
-            serialHandler = Optional.empty();
-        }
+        transactionManager.thenAccept(tman -> {
+            tman.removeEventListener(this);
+            tman.close();
+        });
+        serialHandler.thenAccept(sh -> {
+            sh.removeHandlerListener(this);
+            sh.close();
+        });
         address = null;
         initComplete = false;
         connections.clear();
-        closeSerialPort();
+
+        serialPortFuture.thenAccept(this::closeSerialPort);
+        serialPortFuture.cancel(false);
     }
 
     private void schedulePassiveScan() {
@@ -268,7 +331,6 @@ public class BlueGigaBridgeHandler extends AbstractBluetoothBridgeHandler<BlueGi
 
     private void startScheduledTasks() {
         schedulePassiveScan();
-        logger.debug("Start scheduled task to remove inactive devices");
         discoveryTask = scheduler.scheduleWithFixedDelay(this::refreshDiscoveredDevices, 0, 10, TimeUnit.SECONDS);
     }
 
@@ -309,70 +371,26 @@ public class BlueGigaBridgeHandler extends AbstractBluetoothBridgeHandler<BlueGi
         updateProperties(properties);
     }
 
-    private boolean openSerialPort(final String serialPortName, int baudRate) {
-        logger.debug("Connecting to serial port '{}'", serialPortName);
+    private void closeSerialPort(SerialPort sp) {
+        sp.removeEventListener();
         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;
+            sp.disableReceiveTimeout();
+        } catch (Exception e) {
+            // Ignore all as RXTX seems to send arbitrary exceptions when BlueGiga module is detached
+        } finally {
+            outputStream.ifPresent(output -> {
+                IOUtils.closeQuietly(output);
+            });
+            inputStream.ifPresent(input -> {
+                IOUtils.closeQuietly(input);
+            });
+            sp.close();
+            logger.debug("Closed serial port.");
+            inputStream = Optional.empty();
+            outputStream = Optional.empty();
         }
     }
 
-    private void closeSerialPort() {
-        serialPort.ifPresent(sp -> {
-            sp.removeEventListener();
-            try {
-                sp.disableReceiveTimeout();
-            } catch (Exception e) {
-                // Ignore all as RXTX seems to send arbitrary exceptions when BlueGiga module is detached
-            } finally {
-                outputStream.ifPresent(output -> {
-                    IOUtils.closeQuietly(output);
-                });
-                inputStream.ifPresent(input -> {
-                    IOUtils.closeQuietly(input);
-                });
-                sp.close();
-                logger.debug("Closed serial port.");
-                serialPort = Optional.empty();
-                inputStream = Optional.empty();
-                outputStream = Optional.empty();
-            }
-        });
-    }
-
     @Override
     public void scanStart() {
         super.scanStart();
@@ -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}
      *
@@ -665,8 +702,9 @@ public class BlueGigaBridgeHandler extends AbstractBluetoothBridgeHandler<BlueGi
      */
     private <T extends BlueGigaResponse> T sendCommandWithoutChecks(BlueGigaCommand command, Class<T> expectedResponse)
             throws BlueGigaException {
-        if (transactionManager.isPresent()) {
-            return transactionManager.get().sendTransaction(command, expectedResponse, COMMAND_TIMEOUT_MS);
+        BlueGigaTransactionManager manager = transactionManager.getNow(null);
+        if (manager != null) {
+            return manager.sendTransaction(command, expectedResponse, COMMAND_TIMEOUT_MS);
         } else {
             throw new BlueGigaException("Transaction manager missing");
         }
@@ -678,7 +716,7 @@ public class BlueGigaBridgeHandler extends AbstractBluetoothBridgeHandler<BlueGi
      * @param listener the {@link BlueGigaEventListener} to add
      */
     public void addEventListener(BlueGigaEventListener listener) {
-        transactionManager.ifPresent(manager -> {
+        transactionManager.thenAccept(manager -> {
             manager.addEventListener(listener);
         });
     }
@@ -689,7 +727,7 @@ public class BlueGigaBridgeHandler extends AbstractBluetoothBridgeHandler<BlueGi
      * @param listener the {@link BlueGigaEventListener} to remove
      */
     public void removeEventListener(BlueGigaEventListener listener) {
-        transactionManager.ifPresent(manager -> {
+        transactionManager.thenAccept(manager -> {
             manager.removeEventListener(listener);
         });
     }
index 52762197a45c4dc274a24ed3bf876508cd38e547..1e4e2605ae46ceb57a59c07b78c3673fab2a2c64 100644 (file)
@@ -65,6 +65,9 @@ public class BlueGigaSerialHandler {
 
         flush();
         parserThread = createBlueGigaBLEHandler();
+        parserThread.setUncaughtExceptionHandler((t, th) -> {
+            logger.warn("BluegigaSerialHandler terminating due to unhandled error", th);
+        });
         parserThread.setDaemon(true);
         parserThread.start();
         int tries = 0;
@@ -232,83 +235,83 @@ public class BlueGigaSerialHandler {
         }
     }
 
-    private Thread createBlueGigaBLEHandler() {
-        final int framecheckParams[] = new int[] { 0x00, 0x7F, 0xC0, 0xF8, 0xE0 };
-        return new Thread("BlueGigaBLEHandler") {
-            @Override
-            public void run() {
-                int exceptionCnt = 0;
-                logger.trace("BlueGiga BLE thread started");
-                int[] inputBuffer = new int[BLE_MAX_LENGTH];
-                int inputCount = 0;
-                int inputLength = 0;
-
-                while (!close) {
-                    try {
-                        int val = inputStream.read();
-                        if (val == -1) {
-                            continue;
-                        }
+    private void inboundMessageHandlerLoop() {
+        final int[] framecheckParams = { 0x00, 0x7F, 0xC0, 0xF8, 0xE0 };
 
-                        inputBuffer[inputCount++] = val;
+        int exceptionCnt = 0;
+        logger.trace("BlueGiga BLE thread started");
+        int[] inputBuffer = new int[BLE_MAX_LENGTH];
+        int inputCount = 0;
+        int inputLength = 0;
 
-                        if (inputCount == 1) {
-                            if (inputStream.markSupported()) {
-                                inputStream.mark(BLE_MAX_LENGTH);
-                            }
-                        }
+        while (!close) {
+            try {
+                int val = inputStream.read();
+                if (val == -1) {
+                    continue;
+                }
 
-                        if (inputCount < 4) {
-                            // The BGAPI protocol has no packet framing, and no error detection, so we do a few
-                            // sanity checks on the header to try and allow resyncronisation should there be an
-                            // error.
-                            // Byte 0: Check technology type is bluetooth and high length is 0
-                            // Byte 1: Check length is less than 64 bytes
-                            // Byte 2: Check class ID is less than 8
-                            // Byte 3: Check command ID is less than 16
-                            if ((val & framecheckParams[inputCount]) != 0) {
-                                logger.debug("BlueGiga framing error byte {} = {}", inputCount, val);
-                                if (inputStream.markSupported()) {
-                                    inputStream.reset();
-                                }
-                                inputCount = 0;
-                                continue;
-                            }
-                        } else if (inputCount == 4) {
-                            // Process the header to get the length
-                            inputLength = inputBuffer[1] + (inputBuffer[0] & 0x02 << 8) + 4;
-                            if (inputLength > 64) {
-                                logger.debug("BLE length larger than 64 bytes ({})", inputLength);
-                            }
-                        }
-                        if (inputCount == inputLength) {
-                            // End of packet reached - process
-                            BlueGigaResponse responsePacket = BlueGigaResponsePackets.getPacket(inputBuffer);
-
-                            if (logger.isTraceEnabled()) {
-                                logger.trace("BLE RX: {}", printHex(inputBuffer, inputLength));
-                                logger.trace("BLE RX: {}", responsePacket);
-                            }
-                            if (responsePacket != null) {
-                                notifyEventListeners(responsePacket);
-                            }
-
-                            inputCount = 0;
-                            exceptionCnt = 0;
-                        }
+                inputBuffer[inputCount++] = val;
 
-                    } catch (final IOException e) {
-                        logger.debug("BlueGiga BLE IOException: ", e);
+                if (inputCount == 1) {
+                    if (inputStream.markSupported()) {
+                        inputStream.mark(BLE_MAX_LENGTH);
+                    }
+                }
 
-                        if (exceptionCnt++ > 10) {
-                            logger.error("BlueGiga BLE exception count exceeded, closing handler");
-                            close = true;
-                            notifyEventListeners(e);
+                if (inputCount < 4) {
+                    // The BGAPI protocol has no packet framing, and no error detection, so we do a few
+                    // sanity checks on the header to try and allow resyncronisation should there be an
+                    // error.
+                    // Byte 0: Check technology type is bluetooth and high length is 0
+                    // Byte 1: Check length is less than 64 bytes
+                    // Byte 2: Check class ID is less than 8
+                    // Byte 3: Check command ID is less than 16
+                    if ((val & framecheckParams[inputCount]) != 0) {
+                        logger.debug("BlueGiga framing error byte {} = {}", inputCount, val);
+                        if (inputStream.markSupported()) {
+                            inputStream.reset();
                         }
+                        inputCount = 0;
+                        continue;
+                    }
+                } else if (inputCount == 4) {
+                    // Process the header to get the length
+                    inputLength = inputBuffer[1] + (inputBuffer[0] & 0x02 << 8) + 4;
+                    if (inputLength > 64) {
+                        logger.debug("BLE length larger than 64 bytes ({})", inputLength);
+                    }
+                }
+                if (inputCount == inputLength) {
+                    // End of packet reached - process
+                    BlueGigaResponse responsePacket = BlueGigaResponsePackets.getPacket(inputBuffer);
+
+                    if (logger.isTraceEnabled()) {
+                        logger.trace("BLE RX: {}", printHex(inputBuffer, inputLength));
+                        logger.trace("BLE RX: {}", responsePacket);
+                    }
+                    if (responsePacket != null) {
+                        notifyEventListeners(responsePacket);
                     }
+
+                    inputCount = 0;
+                    exceptionCnt = 0;
+                }
+
+            } catch (final IOException e) {
+                logger.debug("BlueGiga BLE IOException: ", e);
+
+                if (exceptionCnt++ > 10) {
+                    logger.error("BlueGiga BLE exception count exceeded, closing handler");
+                    close = true;
+                    notifyEventListeners(e);
                 }
-                logger.debug("BlueGiga BLE exited.");
             }
-        };
+        }
+        logger.debug("BlueGiga BLE exited.");
+    }
+
+    private Thread createBlueGigaBLEHandler() {
+        return new Thread(this::inboundMessageHandlerLoop, "BlueGigaBLEHandler");
     }
 }
index bbaae65522598ee9520a11bf35795c6a9135bf8a..7ee08a1644918c42feb1ae7a6c2919d3d0cb5a34 100644 (file)
@@ -79,7 +79,7 @@ public class BlueGigaAttributeWriteCommand extends BlueGigaDeviceCommand {
             if (c > 0) {
                 builder.append(' ');
             }
-            builder.append(String.format("%02X", data[c]));
+            builder.append(String.format("%02X", data[c] & 0xFF));
         }
         builder.append(']');
         return builder.toString();
index 8abcc72e9e974fa4e7938850c8b1400aac0cdc9d..4d926439e25032055f35815f72b1fb23f38a267b 100644 (file)
@@ -56,6 +56,13 @@ public class BlueGigaReadByTypeCommand extends BlueGigaDeviceCommand {
      */
     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
      *
@@ -111,4 +118,55 @@ public class BlueGigaReadByTypeCommand extends BlueGigaDeviceCommand {
         builder.append(']');
         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);
+        }
+    }
 }