diff --git a/.gitignore b/.gitignore
index ee2b36e..4af679c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,7 +10,7 @@
.project
.classpath
-# ignore the intelliJ project file
+# ignore JetBrains IntilliJ project files
/out
/classes
*.iml
diff --git a/README.md b/README.md
index 000248e..606a95b 100644
--- a/README.md
+++ b/README.md
@@ -4,21 +4,23 @@
**ble-java** is a java library for building **BLE** GATT peripherals role application in JAVA.
-**ble-java** is based on BlueZ, the linux Bluetooth stack.
+**ble-java** is based on BlueZ, the Linux Bluetooth stack implemented on D-Bus.
# Features
* Create GATT Services
* Create GATT Characteristic
* Customize the Peripheral name
-* Pure JAVA library
+* JAVA library with minimal JNI interfaces to BlueZ over D-Bus
# Dependencies
1. Java 8 or better
2. BlueZ 5.43 or better
-3. d-bus Java library `libdbus-java`
-Raspbian example:
+3. libunixsocket-java (```apt-get install libsocket-java```)
+4. d-bus Java library `libdbus-java`
+
+Raspbian install:
```
-sudo apt-get install
+sudo apt-get install libdbus-java
```
you may also have to do run
```
@@ -26,21 +28,28 @@ sudo ldconfig
```
# Install
-Clone the repository and build with Gradle (4.5 or higher):
+Clone the repository and build with Gradle:
```
-gradle build
+./gradlew build
```
# Example
-You could see the main `MainExample.java` in `src/test/java/example`.
-It's a sample main that create a BLE Application with one Service and 2 Characteristic.
+`MainExample.java` in `src/test/java/example` demonstrates a sample main that
+creates a BLE Application with one Service and 2 Characteristics.
+
+Before running the example, copy ```example.conf``` to ```/etc/dbus-1/system.d/```
+
+To run from command line: ````sudo ./gradlew runExample````
+
+Press ctrl-c to stop service.
# BlueZ compatibility
-Until now is tested with BlueZ 5.46 on Raspbian distribution.
+Tested with BlueZ 5.46 on Raspbian distribution.
-ble-java use the GattManager and LEAdvertising that was marked "Experimental" since 5.47, so you have to enable the Experimental features with the `--experimental` parameter in the BlueZ startup service.
+**ble-java** usees the GattManager and LEAdvertising that was marked "Experimental" since 5.47. You need to enable the
+ Experimental features with the `--experimental` parameter in the BlueZ startup service.
-Example for Raspbian `/lib/systemd/system/bluetooth.service`
+For Raspbian and Ubuntu `/lib/systemd/system/bluetooth.service`
```
...
@@ -48,7 +57,8 @@ bluetoohd --experimental
...
```
-In the BlueZ 5.48 seem to be removed the experimental tag on the LEAdvertising features, but it was not yet tried.
+If you are using BlueZ 5.48, the ```--experimental``` tag may not be needed for LEAdvertising features. But we have not tried
+this yet.
For more info about BlueZ see [http://www.bluez.org](http://www.bluez.org).
diff --git a/build.gradle b/build.gradle
index dc41220..fb180b2 100644
--- a/build.gradle
+++ b/build.gradle
@@ -6,34 +6,24 @@
* user guide available at https://docs.gradle.org/4.5/userguide/java_library_plugin.html
*/
-buildscript {
- version = '0.1'
-}
+// Apply the java plugin to add support for Java
+apply plugin: 'java-library'
+apply plugin: 'maven'
+
+project.version = '0.2'
+project.group = 'it.tangodev'
-plugins {
- // Apply the java-library plugin to add support for Java Library
- id 'java-library'
+repositories {
+ mavenLocal()
+ mavenCentral()
}
+// In this section you declare the dependencies for your production and test code
dependencies {
- // This dependency is exported to consumers, that is to say found on their compile classpath.
- //api 'org.apache.commons:commons-math3:3.6.1'
+ // The production code uses the SLF4J logging API at compile time
+ implementation 'org.slf4j:slf4j-api:1.7.21'
- // This dependency is used internally, and not exposed to consumers on their own compile classpath.
- implementation name: 'unix'
- implementation name: 'libmatthew-java-0.8'
- implementation name: 'dbus-java-2.7'
-
- // Use JUnit test framework
- testImplementation 'junit:junit:4.12'
-}
-
-// In this section you declare where to find the dependencies of your project
-repositories {
- // Use jcenter for resolving your dependencies.
- // You can declare any Maven/Ivy/file repository here.
- jcenter()
- flatDir { dirs 'libs' }
+ implementation fileTree(dir: 'libs', include: ['*.jar'])
}
jar {
@@ -42,3 +32,9 @@ jar {
'Implementation-Version': project.version)
}
}
+
+task (runExample, dependsOn: 'classes', type: JavaExec) {
+ main = 'example.ExampleMain'
+ systemProperty "java.library.path", "/usr/lib/jni"
+ classpath = sourceSets.test.runtimeClasspath
+}
diff --git a/example.conf b/example.conf
new file mode 100644
index 0000000..f77ace7
--- /dev/null
+++ b/example.conf
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
index a5fe1cb..837a42c 100644
Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index be280be..324fb79 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,5 +1,5 @@
+distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-bin.zip
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-4.5-bin.zip
+zipStoreBase=GRADLE_USER_HOME
diff --git a/src/main/java/it/tangodev/ble/BleApplication.java b/src/main/java/it/tangodev/ble/BleApplication.java
index 71e4b21..30c984c 100644
--- a/src/main/java/it/tangodev/ble/BleApplication.java
+++ b/src/main/java/it/tangodev/ble/BleApplication.java
@@ -5,6 +5,7 @@
import java.util.List;
import java.util.Map;
+import it.tangodev.utils.BleAdapter;
import org.bluez.GattApplication1;
import org.bluez.GattManager1;
import org.bluez.LEAdvertisingManager1;
@@ -33,10 +34,12 @@ public class BleApplication implements GattApplication1 {
public static final String BLUEZ_ADAPTER_INTERFACE = "org.bluez.Adapter1";
public static final String BLUEZ_GATT_INTERFACE = "org.bluez.GattManager1";
public static final String BLUEZ_LE_ADV_INTERFACE = "org.bluez.LEAdvertisingManager1";
-
+ public static final String ADDRESS = "Address";
+
private List servicesList = new ArrayList();
- private String path;
- private String adapterPath;
+ private String path = null;
+ private BleAdapter bleAdapter;
+ private BleService advService;
private BleAdvertisement adv;
private String adapterAlias;
@@ -74,19 +77,21 @@ public BleApplication(String path, BleApplicationListener listener) {
*/
public void start() throws DBusException, InterruptedException {
this.dbusConnection = DBusConnection.getConnection(DBusConnection.SYSTEM);
-
- adapterPath = findAdapterPath();
- if(adapterPath == null) { throw new RuntimeException("No BLE adapter found"); }
- Properties adapterProperties = (Properties) dbusConnection.getRemoteObject(BLUEZ_DBUS_BUSNAME, adapterPath, Properties.class);
+ bleAdapter = findAdapterPath();
+ if (bleAdapter == null) {
+ throw new RuntimeException("No BLE adapter found");
+ }
+
+ Properties adapterProperties = (Properties) dbusConnection.getRemoteObject(BLUEZ_DBUS_BUSNAME, bleAdapter.getPath(), Properties.class);
adapterProperties.Set(BLUEZ_ADAPTER_INTERFACE, "Powered", new Variant(true));
if(adapterAlias != null) {
adapterProperties.Set(BLUEZ_ADAPTER_INTERFACE, "Alias", new Variant(adapterAlias));
}
-
- GattManager1 gattManager = (GattManager1) dbusConnection.getRemoteObject(BLUEZ_DBUS_BUSNAME, adapterPath, GattManager1.class);
-
- LEAdvertisingManager1 advManager = (LEAdvertisingManager1) dbusConnection.getRemoteObject(BLUEZ_DBUS_BUSNAME, adapterPath, LEAdvertisingManager1.class);
+
+ GattManager1 gattManager = (GattManager1) dbusConnection.getRemoteObject(BLUEZ_DBUS_BUSNAME, bleAdapter.getPath(), GattManager1.class);
+
+ LEAdvertisingManager1 advManager = (LEAdvertisingManager1) dbusConnection.getRemoteObject(BLUEZ_DBUS_BUSNAME, bleAdapter.getPath(), LEAdvertisingManager1.class);
if (!adv.hasServices()) {
updateAdvertisement();
@@ -108,11 +113,13 @@ public void start() throws DBusException, InterruptedException {
* @throws InterruptedException
*/
public void stop() throws DBusException, InterruptedException {
- if(adapterPath == null) { return; }
- GattManager1 gattManager = (GattManager1) dbusConnection.getRemoteObject(BLUEZ_DBUS_BUSNAME, adapterPath, GattManager1.class);
- LEAdvertisingManager1 advManager = (LEAdvertisingManager1) dbusConnection.getRemoteObject(BLUEZ_DBUS_BUSNAME, adapterPath, LEAdvertisingManager1.class);
-
- if(adv != null) {
+ if (bleAdapter == null) {
+ return;
+ }
+ GattManager1 gattManager = (GattManager1) dbusConnection.getRemoteObject(BLUEZ_DBUS_BUSNAME, bleAdapter.getPath(), GattManager1.class);
+ LEAdvertisingManager1 advManager = (LEAdvertisingManager1) dbusConnection.getRemoteObject(BLUEZ_DBUS_BUSNAME, bleAdapter.getPath(), LEAdvertisingManager1.class);
+
+ if (adv != null) {
advManager.UnregisterAdvertisement(adv);
}
gattManager.UnregisterApplication(this);
@@ -133,12 +140,13 @@ protected void initInterfacesHandler() throws DBusException {
@Override
public void handle(InterfacesAdded signal) {
Map iamap = signal.getInterfacesAdded().get(BLUEZ_DEVICE_INTERFACE);
- if(iamap != null) {
- Variant address = iamap.get("Address");
- System.out.println("Device address: " + address.getValue());
- System.out.println("Device added path: " + signal.getObjectPath().toString());
+ if (iamap != null) {
+ Variant address = iamap.get(ADDRESS);
+ String path = signal.getObjectPath().toString();
hasDeviceConnected = true;
- if(listener != null) { listener.deviceConnected(); }
+ if (listener != null) {
+ listener.deviceConnected(path, address.getValue());
+ }
}
}
};
@@ -148,10 +156,12 @@ public void handle(InterfacesAdded signal) {
public void handle(InterfacesRemoved signal) {
List irlist = signal.getInterfacesRemoved();
for (String ir : irlist) {
- if(BLUEZ_DEVICE_INTERFACE.equals(ir)) {
- System.out.println("Device Removed path: " + signal.getObjectPath().toString());
+ if (BLUEZ_DEVICE_INTERFACE.equals(ir)) {
+ String path = signal.getObjectPath().toString();
hasDeviceConnected = false;
- if(listener != null) { listener.deviceDisconnected(); }
+ if (listener != null) {
+ listener.deviceDisconnected(path);
+ }
}
}
}
@@ -192,26 +202,36 @@ public BleAdvertisement getAdvertisement() {
/**
* Search for a Adapter that has GattManager1 and LEAdvertisement1 interfaces, otherwise return null.
- * @return
- * @throws DBusException
+ * @return BleAdapter based on the map stored in the D-Bus Managed object org.bluez.Adapter1
+ * @throws DBusException if there is an error communicating with BlueZ over D-Bus
*/
- private String findAdapterPath() throws DBusException {
- ObjectManager bluezObjectManager = (ObjectManager) dbusConnection.getRemoteObject(BLUEZ_DBUS_BUSNAME, "/", ObjectManager.class);
- if(bluezObjectManager == null) { return null; }
+ public BleAdapter findAdapterPath() throws DBusException {
+ ObjectManager bluezObjectManager = dbusConnection.getRemoteObject(BLUEZ_DBUS_BUSNAME, "/", ObjectManager.class);
+ if (bluezObjectManager == null) {
+ return null;
+ }
Map>> bluezManagedObject = bluezObjectManager.GetManagedObjects();
- if(bluezManagedObject == null) { return null; }
-
+ if (bluezManagedObject == null) {
+ return null;
+ }
+
for (Path path : bluezManagedObject.keySet()) {
Map> value = bluezManagedObject.get(path);
boolean hasGattManager = false;
boolean hasAdvManager = false;
-
- for(String key : value.keySet()) {
- if(key.equals(BLUEZ_GATT_INTERFACE)) { hasGattManager = true; }
- if(key.equals(BLUEZ_LE_ADV_INTERFACE)) { hasAdvManager = true; }
-
- if(hasGattManager && hasAdvManager) { return path.toString(); }
+
+ for (Map.Entry> entry : value.entrySet()) {
+ if (entry.getKey().equals(BLUEZ_GATT_INTERFACE)) {
+ hasGattManager = true;
+ }
+ if (entry.getKey().equals(BLUEZ_LE_ADV_INTERFACE)) {
+ hasAdvManager = true;
+ }
+ if (hasGattManager && hasAdvManager) {
+ return new BleAdapter(path, value.get("org.bluez.Adapter1"));
+ }
+
}
}
@@ -245,15 +265,17 @@ private void unexport() throws DBusException {
}
dbusConnection.unExportObject(path);
}
-
+
@Override
- public boolean isRemote() { return false; }
-
+ public boolean isRemote() {
+ return false;
+ }
+
@Override
public Map>> GetManagedObjects() {
System.out.println("Application -> GetManagedObjects");
-
- Map>> response = new HashMap>>();
+
+ Map>> response = new HashMap>>();
for (BleService service : servicesList) {
response.put(service.getPath(), service.getProperties());
for (BleCharacteristic characteristic : service.getCharacteristics()) {
@@ -275,4 +297,8 @@ private void updateAdvertisement() {
}
}
}
-}
+ public BleAdapter getBleAdapter() {
+ return bleAdapter;
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/it/tangodev/ble/BleApplicationListener.java b/src/main/java/it/tangodev/ble/BleApplicationListener.java
index a3c1905..9916475 100644
--- a/src/main/java/it/tangodev/ble/BleApplicationListener.java
+++ b/src/main/java/it/tangodev/ble/BleApplicationListener.java
@@ -1,6 +1,6 @@
package it.tangodev.ble;
public interface BleApplicationListener {
- public void deviceConnected();
- public void deviceDisconnected();
+ public void deviceConnected(String path, String address);
+ public void deviceDisconnected(String path);
}
diff --git a/src/main/java/it/tangodev/ble/BleCharacteristic.java b/src/main/java/it/tangodev/ble/BleCharacteristic.java
index 5120375..688db2c 100644
--- a/src/main/java/it/tangodev/ble/BleCharacteristic.java
+++ b/src/main/java/it/tangodev/ble/BleCharacteristic.java
@@ -141,11 +141,11 @@ public Map> getProperties() {
/**
* Call this method to send a notification to a central.
*/
- public void sendNotification() {
+ public void sendNotification(String devicePath) {
try {
DBusConnection dbusConnection = DBusConnection.getConnection(DBusConnection.SYSTEM);
- Variant signalValueVariant = new Variant(listener.getValue());
+ Variant signalValueVariant = new Variant(getValue(devicePath));
Map signalValue = new HashMap();
signalValue.put(BleCharacteristic.CHARACTERISTIC_VALUE_PROPERTY_KEY, signalValueVariant);
@@ -169,12 +169,15 @@ public boolean isRemote() {
public byte[] ReadValue(Map option) {
System.out.println("Characteristic Read option[" + option + "]");
int offset = 0;
- if(option.get("offset") != null) {
+ if(option.containsKey("offset")) {
Variant voffset = option.get("offset");
offset = (voffset.getValue() != null) ? voffset.getValue().intValue() : offset;
}
-
- byte[] valueBytes = listener.getValue();
+
+ String devicePath = null;
+ devicePath = stringVariantToString(option, devicePath);
+
+ byte[] valueBytes = getValue(devicePath);
byte[] slice = Arrays.copyOfRange(valueBytes, offset, valueBytes.length);
return slice;
}
@@ -185,7 +188,31 @@ public byte[] ReadValue(Map option) {
@Override
public void WriteValue(byte[] value, Map option) {
System.out.println("Characteristic Write option[" + option + "]");
- listener.setValue(value);
+ int offset = 0;
+ if(option.containsKey("offset")) {
+ Variant voffset = option.get("offset");
+ offset = (voffset.getValue() != null) ? voffset.getValue().intValue() : offset;
+ }
+
+ String devicePath = null;
+ setValue(stringVariantToString(option, devicePath), offset, value);
+ }
+
+ protected String stringVariantToString(Map option, String devicePath) {
+ if (option.containsKey("device")) {
+ Variant pathVariant = null;
+ pathVariant = option.get("pathVariant");
+ if (pathVariant != null) devicePath = pathVariant.getValue().getPath();
+ }
+ return devicePath;
+ }
+
+ protected byte[] getValue(String devicePath) {
+ return listener.getValue(devicePath);
+ }
+
+ protected void setValue(String devicePath, int offset, byte[] value) {
+ listener.setValue(devicePath, offset, value);
}
@Override
diff --git a/src/main/java/it/tangodev/ble/BleCharacteristicListener.java b/src/main/java/it/tangodev/ble/BleCharacteristicListener.java
index 58746e8..62e44a4 100644
--- a/src/main/java/it/tangodev/ble/BleCharacteristicListener.java
+++ b/src/main/java/it/tangodev/ble/BleCharacteristicListener.java
@@ -6,6 +6,6 @@
*
*/
public interface BleCharacteristicListener {
- public byte[] getValue();
- public void setValue(byte[] value);
+ public byte[] getValue(String devicePath);
+ public void setValue(String devicePath, int offset, byte[] value);
}
diff --git a/src/main/java/it/tangodev/utils/BleAdapter.java b/src/main/java/it/tangodev/utils/BleAdapter.java
new file mode 100644
index 0000000..63fab81
--- /dev/null
+++ b/src/main/java/it/tangodev/utils/BleAdapter.java
@@ -0,0 +1,42 @@
+package it.tangodev.utils;
+
+
+import org.freedesktop.dbus.Path;
+import org.freedesktop.dbus.Variant;
+
+import java.util.Map;
+
+/**
+ * Consolidates the information about the local Bluetooth Adapter returned by BlueZ
+ * Provides Java-friendly getters for the BlueZ D-Bus mappings of Variant values
+ */
+public class BleAdapter {
+ private final Map fields;
+ private final Path path;
+
+ /**
+ * Based on the map stored in the D-Bus Managed object org.bluez.Adapter1
+ * @param path path to the Adapter in the D-Bus i.e. /org/bluez/hci0
+ * @param value map of Adapter Properties storied as Variant types
+ */
+ public BleAdapter(Path path, Map value) {
+ this.path = path;
+ this.fields = value;
+ }
+
+ public String getPath() {
+ return path.toString();
+ }
+
+ public String getAddress() {
+ return fields.get("Address").toString();
+ }
+
+ public String getName() {
+ return fields.get("Name").toString();
+ }
+
+ public String getAlias() {
+ return fields.get("Alias").toString();
+ }
+}
diff --git a/src/test/java/example/ExampleCharacteristic.java b/src/test/java/example/ExampleCharacteristic.java
index ce5c520..97a68b1 100644
--- a/src/test/java/example/ExampleCharacteristic.java
+++ b/src/test/java/example/ExampleCharacteristic.java
@@ -26,7 +26,7 @@ public ExampleCharacteristic(BleService service) {
this.listener = new BleCharacteristicListener() {
@Override
- public void setValue(byte[] value) {
+ public void setValue(String devicePath, int offset, byte[] value) {
try {
exampleValue = new String(value, "UTF8");
} catch(Exception e) {
@@ -35,7 +35,7 @@ public void setValue(byte[] value) {
}
@Override
- public byte[] getValue() {
+ public byte[] getValue(String devicePath) {
try {
return exampleValue.getBytes("UTF8");
} catch(Exception e) {
diff --git a/src/test/java/example/ExampleMain.java b/src/test/java/example/ExampleMain.java
index e3176a3..d8b0a9b 100644
--- a/src/test/java/example/ExampleMain.java
+++ b/src/test/java/example/ExampleMain.java
@@ -21,19 +21,19 @@ public class ExampleMain implements Runnable {
public void notifyBle(String value) {
this.valueString = value;
- characteristic.sendNotification();
+ characteristic.sendNotification(null);
}
public ExampleMain() throws DBusException, InterruptedException {
BleApplicationListener appListener = new BleApplicationListener() {
@Override
- public void deviceDisconnected() {
- System.out.println("Device disconnected");
+ public void deviceDisconnected(String path) {
+ System.out.println("Device disconnected: " + path);
}
@Override
- public void deviceConnected() {
- System.out.println("Device connected");
+ public void deviceConnected(String path, String address) {
+ System.out.println("Device connected: " + path + " ADDR: " + address);
}
};
app = new BleApplication("/tango", appListener);
@@ -45,7 +45,7 @@ public void deviceConnected() {
characteristic = new BleCharacteristic("/tango/s/c", service, flags, "13333333-3333-3333-3333-333333333002", new BleCharacteristicListener() {
@Override
- public void setValue(byte[] value) {
+ public void setValue(String devicePath, int offset, byte[] value) {
try {
valueString = new String(value, "UTF8");
} catch(Exception e) {
@@ -54,7 +54,7 @@ public void setValue(byte[] value) {
}
@Override
- public byte[] getValue() {
+ public byte[] getValue(String devicePath) {
try {
return valueString.getBytes("UTF8");
} catch(Exception e) {
@@ -68,6 +68,7 @@ public byte[] getValue() {
ExampleCharacteristic exampleCharacteristic = new ExampleCharacteristic(service);
service.addCharacteristic(exampleCharacteristic);
app.start();
+ System.out.println("Lisenting on adapter " + app.getBleAdapter().getAddress() + " path: " + app.getBleAdapter().getPath());
}
@Override
@@ -88,13 +89,13 @@ public static void main(String[] args) throws DBusException, InterruptedExceptio
System.out.println("");
// Thread t = new Thread(example);
// t.start();
-// Thread.sleep(15000);
+ Thread.sleep(15000);
example.notifyBle("woooooo");
-// Thread.sleep(15000);
+ Thread.sleep(15000);
// t.notify();
-// Thread.sleep(5000);
-// System.out.println("stopping application");
+ Thread.sleep(5000);
+ System.out.println("stopping application");
example.getApp().stop();
System.out.println("Application stopped");
}