From ed26735dc16736d7110f2e451ecaac463e71631f Mon Sep 17 00:00:00 2001 From: Connor Petty Date: Tue, 24 Nov 2020 12:33:48 -0800 Subject: [PATCH] [bluetooth.bluegiga] Add characteristic notification support (#9067) * Add support for characteristic notifications. * Also fixed bluegiga initialize/dispose bugs. Signed-off-by: Connor Petty --- .../BlueGigaBluetoothCharacteristic.java | 55 ++++ .../bluegiga/BlueGigaBluetoothDevice.java | 256 +++++++++++++-- .../handler/BlueGigaBridgeHandler.java | 308 ++++++++++-------- .../internal/BlueGigaSerialHandler.java | 141 ++++---- .../BlueGigaAttributeWriteCommand.java | 2 +- .../BlueGigaReadByTypeCommand.java | 58 ++++ 6 files changed, 596 insertions(+), 224 deletions(-) create mode 100644 bundles/org.openhab.binding.bluetooth.bluegiga/src/main/java/org/openhab/binding/bluetooth/bluegiga/BlueGigaBluetoothCharacteristic.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 index 0000000000000..dad7b74e59aa6 --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth.bluegiga/src/main/java/org/openhab/binding/bluetooth/bluegiga/BlueGigaBluetoothCharacteristic.java @@ -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; + } +} diff --git a/bundles/org.openhab.binding.bluetooth.bluegiga/src/main/java/org/openhab/binding/bluetooth/bluegiga/BlueGigaBluetoothDevice.java b/bundles/org.openhab.binding.bluetooth.bluegiga/src/main/java/org/openhab/binding/bluetooth/bluegiga/BlueGigaBluetoothDevice.java index d7f24cd447c89..2c5e68ef5b929 100644 --- a/bundles/org.openhab.binding.bluetooth.bluegiga/src/main/java/org/openhab/binding/bluetooth/bluegiga/BlueGigaBluetoothDevice.java +++ b/bundles/org.openhab.binding.bluetooth.bluegiga/src/main/java/org/openhab/binding/bluetooth/bluegiga/BlueGigaBluetoothDevice.java @@ -12,7 +12,13 @@ */ 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.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 handleToUUID = new HashMap<>(); + private NavigableMap handleToCharacteristic = new TreeMap<>(); + // BlueGiga needs to know the address type when connecting private BluetoothAddressType addressType = BluetoothAddressType.UNKNOWN; @@ -70,8 +80,11 @@ private enum BlueGigaProcedure { 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 void setAddressType(BluetoothAddressType addressType) { 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 boolean connect() { public boolean disconnect() { if (connection == -1) { // We're already disconnected - return false; + return true; } return bgHandler.bgDisconnect(connection); @@ -177,14 +190,102 @@ public boolean discoverServices() { @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 boolean readCharacteristic(@Nullable BluetoothCharacteristic characterist if (characteristic == null || characteristic.getHandle() == 0) { return false; } + if (connection == -1) { + return false; + } if (procedureProgress != BlueGigaProcedure.NONE) { return false; @@ -225,6 +329,9 @@ public boolean writeCharacteristic(@Nullable BluetoothCharacteristic characteris if (characteristic == null || characteristic.getHandle() == 0) { return false; } + if (connection == -1) { + return false; + } if (procedureProgress != BlueGigaProcedure.NONE) { return false; @@ -404,7 +511,7 @@ private void handleGroupFoundEvent(BlueGigaGroupFoundEvent event) { 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 @@ private void handleFindInformationFoundEvent(BlueGigaFindInformationFoundEvent e 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 @@ private void handleProcedureCompletedEvent(BlueGigaProcedureCompletedEvent event } 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 @@ private void handleProcedureCompletedEvent(BlueGigaProcedureCompletedEvent event 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 @@ private void handleDisconnectedEvent(BlueGigaDisconnectedEvent event) { return; } + for (BlueGigaBluetoothCharacteristic ch : handleToCharacteristic.values()) { + ch.setNotificationEnabled(false); + } + cancelTimer(procedureTimer); connectionState = ConnectionState.DISCONNECTED; connection = -1; @@ -524,10 +676,31 @@ private void handleAttributeValueEvent(BlueGigaAttributeValueEvent event) { updateLastSeenTime(); - BluetoothCharacteristic characteristic = getCharacteristicByHandle(event.getAttHandle()); - if (characteristic == null) { + logger.trace("BlueGiga AttributeValue: {} event={}", this, event); + + int handle = event.getAttHandle(); + + Map.Entry 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 @@ private void handleAttributeValueEvent(BlueGigaAttributeValueEvent event) { 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; } } diff --git a/bundles/org.openhab.binding.bluetooth.bluegiga/src/main/java/org/openhab/binding/bluetooth/bluegiga/handler/BlueGigaBridgeHandler.java b/bundles/org.openhab.binding.bluetooth.bluegiga/src/main/java/org/openhab/binding/bluetooth/bluegiga/handler/BlueGigaBridgeHandler.java index a6c54f65cc577..33839abe5f28c 100644 --- a/bundles/org.openhab.binding.bluetooth.bluegiga/src/main/java/org/openhab/binding/bluetooth/bluegiga/handler/BlueGigaBridgeHandler.java +++ b/bundles/org.openhab.binding.bluetooth.bluegiga/src/main/java/org/openhab/binding/bluetooth/bluegiga/handler/BlueGigaBridgeHandler.java @@ -20,6 +20,9 @@ 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.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.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 serialPort = Optional.empty(); - private BlueGigaConfiguration configuration = new BlueGigaConfiguration(); // The serial port input stream. @@ -130,10 +134,13 @@ public class BlueGigaBridgeHandler extends AbstractBluetoothBridgeHandler outputStream = Optional.empty(); // The BlueGiga API handler - private Optional serialHandler = Optional.empty(); + private CompletableFuture serialHandler = CompletableFuture + .failedFuture(new IllegalStateException("Uninitialized")); // The BlueGiga transaction manager - private Optional transactionManager = Optional.empty(); + @NonNullByDefault({}) + private CompletableFuture 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 initTask; + private CompletableFuture serialPortFuture = CompletableFuture + .failedFuture(new IllegalStateException("Uninitialized")); + private @Nullable ScheduledFuture removeInactiveDevicesTask; private @Nullable ScheduledFuture discoveryTask; @@ -159,92 +168,146 @@ public BlueGigaBridgeHandler(Bridge bridge, SerialPortManager serialPortManager) @Override public void initialize() { + logger.info("Initializing BlueGiga"); super.initialize(); Optional 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 @@ private void cancelScheduledPassiveScan() { 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 @@ private void updateThingProperties() throws BlueGigaException { 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 boolean bgFindCharacteristics(int connectionHandle) { } } + 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 @@ private T sendCommand(BlueGigaCommand command, Clas */ private T sendCommandWithoutChecks(BlueGigaCommand command, Class 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 @@ private T sendCommandWithoutChecks(BlueGigaCommand * @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 void addEventListener(BlueGigaEventListener listener) { * @param listener the {@link BlueGigaEventListener} to remove */ public void removeEventListener(BlueGigaEventListener listener) { - transactionManager.ifPresent(manager -> { + transactionManager.thenAccept(manager -> { manager.removeEventListener(listener); }); } diff --git a/bundles/org.openhab.binding.bluetooth.bluegiga/src/main/java/org/openhab/binding/bluetooth/bluegiga/internal/BlueGigaSerialHandler.java b/bundles/org.openhab.binding.bluetooth.bluegiga/src/main/java/org/openhab/binding/bluetooth/bluegiga/internal/BlueGigaSerialHandler.java index 52762197a45c4..1e4e2605ae46c 100644 --- a/bundles/org.openhab.binding.bluetooth.bluegiga/src/main/java/org/openhab/binding/bluetooth/bluegiga/internal/BlueGigaSerialHandler.java +++ b/bundles/org.openhab.binding.bluetooth.bluegiga/src/main/java/org/openhab/binding/bluetooth/bluegiga/internal/BlueGigaSerialHandler.java @@ -65,6 +65,9 @@ public BlueGigaSerialHandler(final InputStream inputStream, final OutputStream o 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 @@ private void checkIfAlive() { } } - 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"); } } diff --git a/bundles/org.openhab.binding.bluetooth.bluegiga/src/main/java/org/openhab/binding/bluetooth/bluegiga/internal/command/attributeclient/BlueGigaAttributeWriteCommand.java b/bundles/org.openhab.binding.bluetooth.bluegiga/src/main/java/org/openhab/binding/bluetooth/bluegiga/internal/command/attributeclient/BlueGigaAttributeWriteCommand.java index bbaae65522598..7ee08a1644918 100644 --- a/bundles/org.openhab.binding.bluetooth.bluegiga/src/main/java/org/openhab/binding/bluetooth/bluegiga/internal/command/attributeclient/BlueGigaAttributeWriteCommand.java +++ b/bundles/org.openhab.binding.bluetooth.bluegiga/src/main/java/org/openhab/binding/bluetooth/bluegiga/internal/command/attributeclient/BlueGigaAttributeWriteCommand.java @@ -79,7 +79,7 @@ public String toString() { 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(); diff --git a/bundles/org.openhab.binding.bluetooth.bluegiga/src/main/java/org/openhab/binding/bluetooth/bluegiga/internal/command/attributeclient/BlueGigaReadByTypeCommand.java b/bundles/org.openhab.binding.bluetooth.bluegiga/src/main/java/org/openhab/binding/bluetooth/bluegiga/internal/command/attributeclient/BlueGigaReadByTypeCommand.java index 8abcc72e9e974..4d926439e2503 100644 --- a/bundles/org.openhab.binding.bluetooth.bluegiga/src/main/java/org/openhab/binding/bluetooth/bluegiga/internal/command/attributeclient/BlueGigaReadByTypeCommand.java +++ b/bundles/org.openhab.binding.bluetooth.bluegiga/src/main/java/org/openhab/binding/bluetooth/bluegiga/internal/command/attributeclient/BlueGigaReadByTypeCommand.java @@ -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 String toString() { 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); + } + } }