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"); }