* Add support for characteristic notifications.
* Also fixed bluegiga initialize/dispose bugs.
Signed-off-by: Connor Petty <mistercpp2000+gitsignoff@gmail.com>
--- /dev/null
+/**
+ * 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;
+ }
+}
*/
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;
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;
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;
NONE,
GET_SERVICES,
GET_CHARACTERISTICS,
+ READ_CHARACTERISTIC_DECL,
CHARACTERISTIC_READ,
- CHARACTERISTIC_WRITE
+ CHARACTERISTIC_WRITE,
+ NOTIFICATION_ENABLE,
+ NOTIFICATION_DISABLE
}
private BlueGigaProcedure procedureProgress = BlueGigaProcedure.NONE;
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;
public boolean disconnect() {
if (connection == -1) {
// We're already disconnected
- return false;
+ return true;
}
return bgHandler.bgDisconnect(connection);
@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
if (characteristic == null || characteristic.getHandle() == 0) {
return false;
}
+ if (connection == -1) {
+ return false;
+ }
if (procedureProgress != BlueGigaProcedure.NONE) {
return false;
if (characteristic == null || characteristic.getHandle() == 0) {
return false;
}
+ if (connection == -1) {
+ return false;
+ }
if (procedureProgress != BlueGigaProcedure.NONE) {
return false;
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());
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) {
}
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;
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;
}
return;
}
+ for (BlueGigaBluetoothCharacteristic ch : handleToCharacteristic.values()) {
+ ch.setNotificationEnabled(false);
+ }
+
cancelTimer(procedureTimer);
connectionState = ConnectionState.DISCONNECTED;
connection = -1;
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
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;
}
}
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;
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;
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;
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.
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;
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;
@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() {
private void startScheduledTasks() {
schedulePassiveScan();
- logger.debug("Start scheduled task to remove inactive devices");
discoveryTask = scheduler.scheduleWithFixedDelay(this::refreshDiscoveredDevices, 0, 10, TimeUnit.SECONDS);
}
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();
}
}
+ 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}
*
*/
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");
}
* @param listener the {@link BlueGigaEventListener} to add
*/
public void addEventListener(BlueGigaEventListener listener) {
- transactionManager.ifPresent(manager -> {
+ transactionManager.thenAccept(manager -> {
manager.addEventListener(listener);
});
}
* @param listener the {@link BlueGigaEventListener} to remove
*/
public void removeEventListener(BlueGigaEventListener listener) {
- transactionManager.ifPresent(manager -> {
+ transactionManager.thenAccept(manager -> {
manager.removeEventListener(listener);
});
}
flush();
parserThread = createBlueGigaBLEHandler();
+ parserThread.setUncaughtExceptionHandler((t, th) -> {
+ logger.warn("BluegigaSerialHandler terminating due to unhandled error", th);
+ });
parserThread.setDaemon(true);
parserThread.start();
int tries = 0;
}
}
- 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");
}
}
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();
*/
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
*
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);
+ }
+ }
}