*/
public class BlueGigaBluetoothCharacteristic extends BluetoothCharacteristic {
- private boolean notificationEnabled;
+ private boolean notifying;
public BlueGigaBluetoothCharacteristic(int handle) {
super(null, handle);
this.uuid = uuid;
}
- public boolean isNotificationEnabled() {
- return notificationEnabled;
+ public boolean isNotifying() {
+ return notifying;
}
- public void setNotificationEnabled(boolean enable) {
- this.notificationEnabled = enable;
+ public void setNotifying(boolean enable) {
+ this.notifying = enable;
}
}
}
BlueGigaBluetoothCharacteristic ch = (BlueGigaBluetoothCharacteristic) characteristic;
- if (ch.isNotificationEnabled()) {
+ if (ch.isNotifying()) {
return true;
}
@Override
public boolean disableNotifications(BluetoothCharacteristic characteristic) {
if (connection == -1) {
- logger.debug("Cannot enable notifications, device not connected {}", this);
+ logger.debug("Cannot disable notifications, device not connected {}", this);
return false;
}
BlueGigaBluetoothCharacteristic ch = (BlueGigaBluetoothCharacteristic) characteristic;
- if (ch.isNotificationEnabled()) {
+ if (!ch.isNotifying()) {
return true;
}
return true;
}
+ @Override
+ public boolean isNotifying(BluetoothCharacteristic characteristic) {
+ BlueGigaBluetoothCharacteristic ch = (BlueGigaBluetoothCharacteristic) characteristic;
+ return ch.isNotifying();
+ }
+
@Override
public boolean enableNotifications(BluetoothDescriptor descriptor) {
// TODO will be implemented in a followup PR
if (!success) {
logger.debug("write to descriptor failed");
}
- ((BlueGigaBluetoothCharacteristic) procedureCharacteristic).setNotificationEnabled(success);
+ ((BlueGigaBluetoothCharacteristic) procedureCharacteristic).setNotifying(success);
procedureProgress = BlueGigaProcedure.NONE;
procedureCharacteristic = null;
break;
if (!success) {
logger.debug("write to descriptor failed");
}
- ((BlueGigaBluetoothCharacteristic) procedureCharacteristic).setNotificationEnabled(!success);
+ ((BlueGigaBluetoothCharacteristic) procedureCharacteristic).setNotifying(!success);
procedureProgress = BlueGigaProcedure.NONE;
procedureCharacteristic = null;
break;
}
for (BlueGigaBluetoothCharacteristic ch : handleToCharacteristic.values()) {
- ch.setNotificationEnabled(false);
+ ch.setNotifying(false);
}
cancelTimer(procedureTimer);
*
* @author Kai Kreuzer - Initial contribution and API
* @author Benjamin Lafois - Replaced tinyB with bluezDbus
+ * @author Peter Rosenberg - Improve notifications and properties support
*
*/
@NonNullByDefault
for (BluetoothGattCharacteristic dBusBlueZCharacteristic : dBusBlueZService.getGattCharacteristics()) {
BluetoothCharacteristic characteristic = new BluetoothCharacteristic(
UUID.fromString(dBusBlueZCharacteristic.getUuid()), 0);
+ convertCharacteristicProperties(dBusBlueZCharacteristic, characteristic);
for (BluetoothGattDescriptor dBusBlueZDescriptor : dBusBlueZCharacteristic.getGattDescriptors()) {
BluetoothDescriptor descriptor = new BluetoothDescriptor(characteristic,
return true;
}
+ /**
+ * Convert the flags of BluetoothGattCharacteristic to the int bitset used by BluetoothCharacteristic.
+ *
+ * @param dBusBlueZCharacteristic source characteristic to read the flags from
+ * @param characteristic destination characteristic to write to properties to
+ */
+ private void convertCharacteristicProperties(BluetoothGattCharacteristic dBusBlueZCharacteristic,
+ BluetoothCharacteristic characteristic) {
+ int properties = 0;
+
+ for (String property : dBusBlueZCharacteristic.getFlags()) {
+ switch (property) {
+ case "broadcast":
+ properties |= BluetoothCharacteristic.PROPERTY_BROADCAST;
+ break;
+ case "read":
+ properties |= BluetoothCharacteristic.PROPERTY_READ;
+ break;
+ case "write-without-response":
+ properties |= BluetoothCharacteristic.PROPERTY_WRITE_NO_RESPONSE;
+ break;
+ case "write":
+ properties |= BluetoothCharacteristic.PROPERTY_WRITE;
+ break;
+ case "notify":
+ properties |= BluetoothCharacteristic.PROPERTY_NOTIFY;
+ break;
+ case "indicate":
+ properties |= BluetoothCharacteristic.PROPERTY_INDICATE;
+ break;
+ }
+ }
+
+ characteristic.setProperties(properties);
+ }
+
@Override
public boolean readCharacteristic(BluetoothCharacteristic characteristic) {
BluetoothGattCharacteristic c = getDBusBlueZCharacteristicByUUID(characteristic.getUuid().toString());
characteristic.setValue(value);
notifyListeners(BluetoothEventType.CHARACTERISTIC_READ_COMPLETE, characteristic,
BluetoothCompletionStatus.SUCCESS);
- } catch (DBusException e) {
+ } catch (DBusException | DBusExecutionException e) {
+ // DBusExecutionException is thrown if the value cannot be read
logger.debug("Exception occurred when trying to read characteristic '{}': {}", characteristic.getUuid(),
e.getMessage());
notifyListeners(BluetoothEventType.CHARACTERISTIC_READ_COMPLETE, characteristic,
return true;
}
+ @Override
+ public boolean isNotifying(BluetoothCharacteristic characteristic) {
+ BluetoothGattCharacteristic c = getDBusBlueZCharacteristicByUUID(characteristic.getUuid().toString());
+ if (c != null) {
+ Boolean isNotifying = c.isNotifying();
+ return Objects.requireNonNullElse(isNotifying, false);
+ } else {
+ logger.warn("Characteristic '{}' is missing on device '{}'.", characteristic.getUuid(), address);
+ return false;
+ }
+ }
+
@Override
public boolean disableNotifications(BluetoothCharacteristic characteristic) {
BluetoothGattCharacteristic c = getDBusBlueZCharacteristicByUUID(characteristic.getUuid().toString());
* channels based off of a bluetooth device's GATT characteristics.
*
* @author Connor Petty - Initial contribution
+ * @author Peter Rosenberg - Use notifications
*
*/
@NonNullByDefault
private final Map<ChannelUID, CharacteristicHandler> channelHandlers = new ConcurrentHashMap<>();
private final BluetoothGattParser gattParser = BluetoothGattParserFactory.getDefault();
private final CharacteristicChannelTypeProvider channelTypeProvider;
+ private final Map<CharacteristicHandler, List<ChannelUID>> handlerToChannels = new ConcurrentHashMap<>();
private @Nullable ScheduledFuture<?> readCharacteristicJob = null;
readCharacteristicJob = scheduler.scheduleWithFixedDelay(() -> {
if (device.getConnectionState() == ConnectionState.CONNECTED) {
if (resolved) {
- for (CharacteristicHandler charHandler : charHandlers.values()) {
- if (charHandler.canRead()) {
+ handlerToChannels.forEach((charHandler, channelUids) -> {
+ // Only read the value manually if notification is not on.
+ // Also read it the first time before we activate notifications below.
+ if (!device.isNotifying(charHandler.characteristic) && charHandler.canRead()) {
device.readCharacteristic(charHandler.characteristic);
try {
// TODO the ideal solution would be to use locks/conditions and timeouts
- // between this code and `onCharacteristicReadComplete` but
+ // Kbetween this code and `onCharacteristicReadComplete` but
// that would overcomplicate the code a bit and I plan
// on implementing a better more generalized solution later
Thread.sleep(50);
return;
}
}
- }
+ if (charHandler.characteristic.canNotify()) {
+ // Enabled/Disable notifications dependent on if the channel is linked.
+ // TODO check why isLinked() is true for not linked channels
+ if (channelUids.stream().anyMatch(this::isLinked)) {
+ if (!device.isNotifying(charHandler.characteristic)) {
+ device.enableNotifications(charHandler.characteristic);
+ }
+ } else {
+ if (device.isNotifying(charHandler.characteristic)) {
+ device.disableNotifications(charHandler.characteristic);
+ }
+ }
+ }
+ });
} else {
// if we are connected and still haven't been able to resolve the services, try disconnecting and
// then connecting again
charHandlers.clear();
channelHandlers.clear();
+ handlerToChannels.clear();
}
@Override
logger.trace("{} processing characteristic {}", address, characteristic.getUuid());
CharacteristicHandler handler = getCharacteristicHandler(characteristic);
List<Channel> chans = handler.buildChannels();
- for (Channel channel : chans) {
- channelHandlers.put(channel.getUID(), handler);
+ List<ChannelUID> chanUids = chans.stream().map(Channel::getUID).collect(Collectors.toList());
+ for (ChannelUID channel : chanUids) {
+ channelHandlers.put(channel, handler);
}
+ handlerToChannels.put(handler, chanUids);
return chans.stream();
})//
.collect(Collectors.toList());
if (gattParser.isKnownCharacteristic(charUUID)) {
return gattParser.isValidForRead(charUUID);
}
- // TODO: need to evaluate this from characteristic properties, but such properties aren't support yet
- return true;
+ return characteristic.canRead();
}
public boolean canWrite() {
if (gattParser.isKnownCharacteristic(charUUID)) {
return gattParser.isValidForWrite(charUUID);
}
- // TODO: need to evaluate this from characteristic properties, but such properties aren't support yet
- return true;
+ return characteristic.canWrite();
}
private boolean isAdvanced() {
*
* @author Chris Jackson - Initial contribution
* @author Kai Kreuzer - Cleaned up code
+ * @author Peter Rosenberg - Improve properties support
*/
public class BluetoothCharacteristic {
public static final int FORMAT_UINT8 = 0x11;
return instance;
}
+ /**
+ * Set the raw properties. The individual properties are represented as bits inside
+ * of this int value.
+ *
+ * @param properties of this Characteristic
+ */
+ public void setProperties(int properties) {
+ this.properties = properties;
+ }
+
/**
* Returns the properties of this characteristic.
*
return properties;
}
+ /**
+ * Returns if the given characteristics property is enabled or not.
+ *
+ * @param property one of the Constants BluetoothCharacteristic.PROPERTY_XX
+ * @return true if this characteristic has the given property enabled, false if properties not set or
+ * the given property is not enabled.
+ */
+ public boolean hasPropertyEnabled(int property) {
+ return (properties & property) != 0;
+ }
+
+ /**
+ * Returns if notifications can be enabled on this characteristic.
+ *
+ * @return true if notifications can be enabled, false if notifications are not supported, characteristic is missing
+ * on device or notifications are not supported.
+ */
+ public boolean canNotify() {
+ return hasPropertyEnabled(BluetoothCharacteristic.PROPERTY_NOTIFY);
+ }
+
+ /**
+ * Returns if the value can be read on this characteristic.
+ *
+ * @return true if the value can be read, false otherwise.
+ */
+ public boolean canRead() {
+ return hasPropertyEnabled(BluetoothCharacteristic.PROPERTY_READ);
+ }
+
+ /**
+ * Returns if the value can be written on this characteristic.
+ *
+ * @return true if the value can be written with of without a response, false otherwise.
+ */
+ public boolean canWrite() {
+ return hasPropertyEnabled(BluetoothCharacteristic.PROPERTY_WRITE)
+ || hasPropertyEnabled(BluetoothCharacteristic.PROPERTY_WRITE_NO_RESPONSE);
+ }
+
/**
* Returns the permissions for this characteristic.
*/
* @author Chris Jackson - Initial contribution
* @author Kai Kreuzer - Refactored class to use Integer instead of int, fixed bugs, diverse improvements
* @author Connor Petty - Made most of the methods abstract
+ * @author Peter Rosenberg - Improve notifications
*/
@NonNullByDefault
public abstract class BluetoothDevice {
*/
public abstract boolean writeCharacteristic(BluetoothCharacteristic characteristic);
+ /**
+ * Returns if notification is enabled for the given characteristic.
+ *
+ * @param characteristic the {@link BluetoothCharacteristic} to check if notifications are enabled.
+ * @return true if notification is enabled, false if notification is disabled, characteristic is missing on device
+ * or notifications are not supported.
+ */
+ public abstract boolean isNotifying(BluetoothCharacteristic characteristic);
+
/**
* Enables notifications for a characteristic. Only a single read or write operation can be requested at once.
* Attempting to perform an operation when one is already in progress will result in subsequent calls returning
return delegate != null && delegate.writeCharacteristic(characteristic);
}
+ @Override
+ public boolean isNotifying(BluetoothCharacteristic characteristic) {
+ BluetoothDevice delegate = getDelegate();
+ return delegate != null ? delegate.isNotifying(characteristic) : false;
+ }
+
@Override
public boolean enableNotifications(BluetoothCharacteristic characteristic) {
BluetoothDevice delegate = getDelegate();
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bluetooth;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.UUID;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests {@link BluetoothCharacteristic}.
+ *
+ * @author Peter Rosenberg - Initial contribution
+ */
+public class CharacteristicPropertiesTest {
+ private BluetoothCharacteristic characteristic = new BluetoothCharacteristic(UUID.randomUUID(), 0);
+
+ @Test
+ public void testAllSupportedProperties() {
+ // given
+ // when
+ int properties = 0;
+ properties |= BluetoothCharacteristic.PROPERTY_BROADCAST;
+ properties |= BluetoothCharacteristic.PROPERTY_READ;
+ properties |= BluetoothCharacteristic.PROPERTY_WRITE_NO_RESPONSE;
+ properties |= BluetoothCharacteristic.PROPERTY_WRITE;
+ properties |= BluetoothCharacteristic.PROPERTY_NOTIFY;
+ properties |= BluetoothCharacteristic.PROPERTY_INDICATE;
+ characteristic.setProperties(properties);
+
+ // then
+ assertTrue(characteristic.hasPropertyEnabled(BluetoothCharacteristic.PROPERTY_BROADCAST), "Broastcast not set");
+ assertTrue(characteristic.hasPropertyEnabled(BluetoothCharacteristic.PROPERTY_READ), "Read not set");
+ assertTrue(characteristic.hasPropertyEnabled(BluetoothCharacteristic.PROPERTY_WRITE_NO_RESPONSE),
+ "Write not response not set");
+ assertTrue(characteristic.hasPropertyEnabled(BluetoothCharacteristic.PROPERTY_WRITE), "Write not set");
+ assertTrue(characteristic.hasPropertyEnabled(BluetoothCharacteristic.PROPERTY_NOTIFY), "Notify not set");
+ assertTrue(characteristic.hasPropertyEnabled(BluetoothCharacteristic.PROPERTY_INDICATE), "Indicate not set");
+ assertFalse(characteristic.hasPropertyEnabled(BluetoothCharacteristic.PROPERTY_SIGNED_WRITE),
+ "Signed write set");
+ assertFalse(characteristic.hasPropertyEnabled(BluetoothCharacteristic.PROPERTY_EXTENDED_PROPS),
+ "Extended props set");
+ }
+
+ @Test
+ public void testNoProperties() {
+ // given
+ // when
+ int properties = 0;
+ characteristic.setProperties(properties);
+
+ // then
+ assertFalse(characteristic.hasPropertyEnabled(BluetoothCharacteristic.PROPERTY_BROADCAST),
+ "Broastcast not set");
+ assertFalse(characteristic.hasPropertyEnabled(BluetoothCharacteristic.PROPERTY_READ), "Read not set");
+ assertFalse(characteristic.hasPropertyEnabled(BluetoothCharacteristic.PROPERTY_WRITE_NO_RESPONSE),
+ "Write not response not set");
+ assertFalse(characteristic.hasPropertyEnabled(BluetoothCharacteristic.PROPERTY_WRITE), "Write not set");
+ assertFalse(characteristic.hasPropertyEnabled(BluetoothCharacteristic.PROPERTY_NOTIFY), "Notify not set");
+ assertFalse(characteristic.hasPropertyEnabled(BluetoothCharacteristic.PROPERTY_INDICATE), "Indicate not set");
+ assertFalse(characteristic.hasPropertyEnabled(BluetoothCharacteristic.PROPERTY_SIGNED_WRITE),
+ "Signed write set");
+ assertFalse(characteristic.hasPropertyEnabled(BluetoothCharacteristic.PROPERTY_EXTENDED_PROPS),
+ "Extended props set");
+ }
+
+ @Test
+ public void testSomeSupportedProperties() {
+ // given
+ // when
+ int properties = 0;
+ properties |= BluetoothCharacteristic.PROPERTY_READ;
+ properties |= BluetoothCharacteristic.PROPERTY_NOTIFY;
+ characteristic.setProperties(properties);
+
+ // then
+ assertFalse(characteristic.hasPropertyEnabled(BluetoothCharacteristic.PROPERTY_BROADCAST),
+ "Broastcast not set");
+ assertTrue(characteristic.hasPropertyEnabled(BluetoothCharacteristic.PROPERTY_READ), "Read not set");
+ assertFalse(characteristic.hasPropertyEnabled(BluetoothCharacteristic.PROPERTY_WRITE_NO_RESPONSE),
+ "Write not response not set");
+ assertFalse(characteristic.hasPropertyEnabled(BluetoothCharacteristic.PROPERTY_WRITE), "Write not set");
+ assertTrue(characteristic.hasPropertyEnabled(BluetoothCharacteristic.PROPERTY_NOTIFY), "Notify not set");
+ assertFalse(characteristic.hasPropertyEnabled(BluetoothCharacteristic.PROPERTY_INDICATE), "Indicate not set");
+ assertFalse(characteristic.hasPropertyEnabled(BluetoothCharacteristic.PROPERTY_SIGNED_WRITE),
+ "Signed write set");
+ assertFalse(characteristic.hasPropertyEnabled(BluetoothCharacteristic.PROPERTY_EXTENDED_PROPS),
+ "Extended props set");
+ }
+}
return false;
}
+ @Override
+ public boolean isNotifying(BluetoothCharacteristic characteristic) {
+ return false;
+ }
+
@Override
public boolean disableNotifications(BluetoothCharacteristic characteristic) {
return false;