commit 2bb5027d11dbee98665d8fe68061ca794a60ab4e Author: chrispr Date: Sun Jul 19 20:32:50 2020 -0400 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea190db --- /dev/null +++ b/.gitignore @@ -0,0 +1,84 @@ +############################## +## Java +############################## +.mtj.tmp/ +*.class +*.jar +*.war +*.ear +*.nar +hs_err_pid* + +############################## +## Maven +############################## +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +pom.xml.bak +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties +.mvn/wrapper/maven-wrapper.jar + +############################## +## Gradle +############################## +bin/ +build/ +.gradle +.gradletasknamecache +gradle-app.setting +!gradle-wrapper.jar + +############################## +## IntelliJ +############################## +out/ +#.idea/ +#.idea_modules/ +#*.iml +*.ipr +*.iws + +############################## +## Eclipse +############################## +.settings/ +bin/ +tmp/ +.metadata +.classpath +.project +*.tmp +*.bak +*.swp +*~.nib +local.properties +.loadpath +.factorypath + +############################## +## NetBeans +############################## +nbproject/private/ +build/ +nbbuild/ +dist/ +nbdist/ +nbactions.xml +nb-configuration.xml + +############################## +## Visual Studio Code +############################## +.vscode/ +.code-workspace + +############################## +## OS X +############################## +.DS_Store diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/artifacts/BluetoothHRService_jar.xml b/.idea/artifacts/BluetoothHRService_jar.xml new file mode 100644 index 0000000..3335cea --- /dev/null +++ b/.idea/artifacts/BluetoothHRService_jar.xml @@ -0,0 +1,26 @@ + + + $PROJECT_DIR$/out/artifacts/BluetoothHRService_jar + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..cdf14ca --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/description.html b/.idea/description.html new file mode 100644 index 0000000..db5f129 --- /dev/null +++ b/.idea/description.html @@ -0,0 +1 @@ +Simple Java application that includes a class with main() method \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..97626ba --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 0000000..712ab9d --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/com_github_hypfvieh_bluez_dbus_0_1_1.xml b/.idea/libraries/com_github_hypfvieh_bluez_dbus_0_1_1.xml new file mode 100644 index 0000000..154222b --- /dev/null +++ b/.idea/libraries/com_github_hypfvieh_bluez_dbus_0_1_1.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/log4j_log4j_1_2_17.xml b/.idea/libraries/log4j_log4j_1_2_17.xml new file mode 100644 index 0000000..7d424aa --- /dev/null +++ b/.idea/libraries/log4j_log4j_1_2_17.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/org_influxdb_influxdb_java_2_17.xml b/.idea/libraries/org_influxdb_influxdb_java_2_17.xml new file mode 100644 index 0000000..dee27fc --- /dev/null +++ b/.idea/libraries/org_influxdb_influxdb_java_2_17.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/org_slf4j_slf4j_log4j12_1_7_30.xml b/.idea/libraries/org_slf4j_slf4j_log4j12_1_7_30.xml new file mode 100644 index 0000000..c1c5400 --- /dev/null +++ b/.idea/libraries/org_slf4j_slf4j_log4j12_1_7_30.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..495d94d --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..19c255b --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/project-template.xml b/.idea/project-template.xml new file mode 100644 index 0000000..1f08b88 --- /dev/null +++ b/.idea/project-template.xml @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/.idea/uiDesigner.xml b/.idea/uiDesigner.xml new file mode 100644 index 0000000..e96534f --- /dev/null +++ b/.idea/uiDesigner.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/BluetoothHRService.iml b/BluetoothHRService.iml new file mode 100644 index 0000000..f4fce8d --- /dev/null +++ b/BluetoothHRService.iml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/config.properties b/config.properties new file mode 100644 index 0000000..5cd6203 --- /dev/null +++ b/config.properties @@ -0,0 +1,4 @@ +InfluxDBURL=http://arrakis.chrispr.lan:8086 +InfluxDBUsername=airreport +InfluxDBPassword=airreport +InfluxDBDatabase=heartrate \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..7974cba --- /dev/null +++ b/pom.xml @@ -0,0 +1,16 @@ + + + 4.0.0 + + + 1.8 + 1.8 + + + com.chrispr + BluetoothHRService + 1.0-SNAPSHOT + + \ No newline at end of file diff --git a/src/main/java/META-INF/MANIFEST.MF b/src/main/java/META-INF/MANIFEST.MF new file mode 100644 index 0000000..6bce855 --- /dev/null +++ b/src/main/java/META-INF/MANIFEST.MF @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +Main-Class: com.chrispr.bluetooth.heartrate.Main + diff --git a/src/main/java/com/chrispr/bluetooth/BluetoothNotificationSubscriber.java b/src/main/java/com/chrispr/bluetooth/BluetoothNotificationSubscriber.java new file mode 100644 index 0000000..6582bd7 --- /dev/null +++ b/src/main/java/com/chrispr/bluetooth/BluetoothNotificationSubscriber.java @@ -0,0 +1,105 @@ +package com.chrispr.bluetooth; + +import com.chrispr.bluetooth.heartrate.Main; +import com.chrispr.utility.Event; +import com.github.hypfvieh.bluetooth.DeviceManager; +import com.github.hypfvieh.bluetooth.wrapper.BluetoothDevice; +import com.github.hypfvieh.bluetooth.wrapper.BluetoothGattCharacteristic; +import com.github.hypfvieh.bluetooth.wrapper.BluetoothGattService; +import org.bluez.exceptions.BluezFailedException; +import org.bluez.exceptions.BluezInProgressException; +import org.bluez.exceptions.BluezNotPermittedException; +import org.bluez.exceptions.BluezNotSupportedException; +import org.freedesktop.dbus.exceptions.DBusException; +import org.freedesktop.dbus.handlers.AbstractPropertiesChangedHandler; +import org.freedesktop.dbus.interfaces.Properties; +import org.freedesktop.dbus.types.Variant; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; + +// This class helps implement bluetooth characteristic notification events by receiving them +// from the DBUS interface and parsing them for easier consumption +public class BluetoothNotificationSubscriber extends AbstractPropertiesChangedHandler { + + private BluetoothDevice device; + private BluetoothGattService service; + private BluetoothGattCharacteristic characteristic; + private String CharacteristicUUID; + private String CharacteristicDBUSPath; + private List handlers; + private Logger logger = LoggerFactory.getLogger(BluetoothNotificationSubscriber.class); + + public BluetoothNotificationSubscriber(BluetoothDevice device, BluetoothGattService service, BluetoothGattCharacteristic characteristic) { + this.device = device; + this.service = service; + this.characteristic = characteristic; + this.CharacteristicUUID = characteristic.getUuid(); + this.CharacteristicDBUSPath = characteristic.getDbusPath(); + + handlers = new ArrayList(); + registerPropertyChangedHandler(); + SubscribeToCharacteristicNotifications(); + } + + public void addHandler(IBluetoothCharacteristicNotification handler) { + handlers.add(handler); + + } + + public void removeHandler(IBluetoothCharacteristicNotification handler) { + handlers.remove(handler); + } + + + //Assumes the DeviceManager has already been created + private void registerPropertyChangedHandler() { + DeviceManager manager = DeviceManager.getInstance(); + try { + manager.registerPropertyHandler(this); + } catch (DBusException e) { + logger.error(e.toString()); + } + } + + private void SubscribeToCharacteristicNotifications() { + try { + characteristic.startNotify(); + } catch (BluezFailedException e) { + logger.error(e.toString()); + } catch (BluezInProgressException e) { + logger.error(e.toString()); + } catch (BluezNotSupportedException e) { + logger.error(e.toString()); + } catch (BluezNotPermittedException e) { + logger.error(e.toString()); + } + } + + @Override + public void handle(Properties.PropertiesChanged propertiesChanged) { + if (!propertiesChanged.getPath().equals(CharacteristicDBUSPath)) + return; + + + Map> propMap = propertiesChanged.getPropertiesChanged(); + Iterator>> it = propMap.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry> entry = it.next(); + Object object = entry.getValue().getValue(); + + logger.debug("[{}] {} -> {} uuid:{}", object.getClass().getName(), entry.getKey(), object, CharacteristicUUID); + + if ((object == null) || !(object instanceof byte[])) { + continue; + } + + byte[] data = (byte[]) object; + logger.debug("New characteristic received, calling handler"); + for(IBluetoothCharacteristicNotification handler : handlers) { + handler.handleNewNotification(device, service, characteristic, data); + } + } + } +} diff --git a/src/main/java/com/chrispr/bluetooth/IBluetoothCharacteristicNotification.java b/src/main/java/com/chrispr/bluetooth/IBluetoothCharacteristicNotification.java new file mode 100644 index 0000000..77cd915 --- /dev/null +++ b/src/main/java/com/chrispr/bluetooth/IBluetoothCharacteristicNotification.java @@ -0,0 +1,10 @@ +package com.chrispr.bluetooth; + +import com.github.hypfvieh.bluetooth.wrapper.BluetoothDevice; +import com.github.hypfvieh.bluetooth.wrapper.BluetoothGattCharacteristic; +import com.github.hypfvieh.bluetooth.wrapper.BluetoothGattService; + +public interface IBluetoothCharacteristicNotification { + + public void handleNewNotification(BluetoothDevice device, BluetoothGattService service, BluetoothGattCharacteristic characteristic, byte[] value); +} diff --git a/src/main/java/com/chrispr/bluetooth/heartrate/Main.java b/src/main/java/com/chrispr/bluetooth/heartrate/Main.java new file mode 100644 index 0000000..5711e3e --- /dev/null +++ b/src/main/java/com/chrispr/bluetooth/heartrate/Main.java @@ -0,0 +1,134 @@ +package com.chrispr.bluetooth.heartrate; + + +import com.chrispr.bluetooth.BluetoothNotificationSubscriber; +import com.chrispr.bluetooth.IBluetoothCharacteristicNotification; +import com.github.hypfvieh.bluetooth.DeviceManager; +import com.github.hypfvieh.bluetooth.wrapper.*; +import org.apache.log4j.BasicConfigurator; +import org.bluez.exceptions.BluezFailedException; +import org.bluez.exceptions.BluezInProgressException; +import org.bluez.exceptions.BluezNotPermittedException; +import org.bluez.exceptions.BluezNotSupportedException; +import org.freedesktop.dbus.exceptions.DBusException; + +import org.influxdb.InfluxDB; +import org.influxdb.InfluxDBFactory; +import org.influxdb.dto.Point; +import org.influxdb.dto.Query; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.FileInputStream; +import java.io.IOException; +import java.time.LocalDateTime; +import java.util.*; +import java.util.concurrent.TimeUnit; + +public class Main { + public static Logger logger = LoggerFactory.getLogger(Main.class); + public static String HeartRateServiceUUID = "0000180d-0000-1000-8000-00805f9b34fb"; + public static String HeartRateCharacteristicUUID = "00002a37-0000-1000-8000-00805f9b34fb"; + public static InfluxDB DB; + private static LocalDateTime lastUpdate; + public static void main(String[] args) throws InterruptedException, IOException { + // Set up a simple configuration that logs on the console. + BasicConfigurator.configure(); + logger.info("Heart Rate Monitor started"); + DeviceManager manager; + try { + DeviceManager.createInstance(false); + manager =DeviceManager.getInstance(); + + } catch (DBusException e) { + logger.error(e.toString()); + e.printStackTrace(); + return; + } + //load settings + Properties props = new Properties(); + FileInputStream in = new FileInputStream("config.properties"); + props.load(in); + in.close(); + + DB = InfluxDBFactory.connect(props.getProperty("InfluxDBURL"), props.getProperty("InfluxDBUsername"), props.getProperty("InfluxDBPassword")); + DB.ping(); + String databaseName = props.getProperty("InfluxDBDatabase"); + DB.query(new Query("CREATE DATABASE " + databaseName)); + DB.setDatabase(databaseName); + + logger.debug("Scanning for HR monitor"); + BluetoothDevice heartRateDevice; + while(true) { + List devices = manager.scanForBluetoothDevices(1000); + Optional device = devices.stream().filter(bt -> bt.getName().contains("TICKR")).findAny(); + if(device.isPresent()) { + heartRateDevice = device.get(); + break; + } + else + logger.debug("Device not found, scanning again.."); + } + heartRateDevice.connect(); + heartRateDevice.refreshGattServices(); + while(!heartRateDevice.isServicesResolved()) + Thread.sleep(100); + BluetoothGattService heartRateService = heartRateDevice.getGattServiceByUuid(HeartRateServiceUUID); + if(heartRateService == null) { + logger.error("The expected service was not found on the device"); + return; + } + BluetoothGattCharacteristic heartRateCharacteristic = heartRateService.getGattCharacteristicByUuid(HeartRateCharacteristicUUID); + if(heartRateCharacteristic == null) { + logger.error("The expected characteristic was not found on the device"); + return; + } + BluetoothNotificationSubscriber subscriber = new BluetoothNotificationSubscriber(heartRateDevice, heartRateService, heartRateCharacteristic); + + subscriber.addHandler((device, service, characteristic, value) -> { + byte contactValue = value[0]; + if(contactValue != 6 && contactValue != 22) { + logger.info("ignoring reading due to lack of skin contact"); + return; + } + byte hrValue = value[1]; + logger.info("Got good heart rate reading of {} bpm", (int)hrValue); + //attempt to log to InfluxDB + Point p = Point.measurement("rate") + .time(System.currentTimeMillis(), TimeUnit.MILLISECONDS) + .addField("bpm", hrValue) + .build(); + DB.write(p); + lastUpdate = LocalDateTime.now(); + }); + + while(true){ + if(heartRateDevice.isConnected()) { + if(lastUpdate != null && lastUpdate.plusMinutes(2).compareTo(java.time.LocalDateTime.now()) < 0) { + try { + heartRateCharacteristic.startNotify(); + } catch (Exception e) { + e.printStackTrace(); + } + } + Thread.sleep(10000); + } + else { + while (!heartRateDevice.isConnected()) { + try { + heartRateDevice.connect(); + } catch (Exception ex) { + logger.debug(ex.toString()); + } + Thread.sleep(10000); + } + + try { + heartRateCharacteristic.startNotify(); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + } +} diff --git a/src/main/java/com/chrispr/utility/Event.java b/src/main/java/com/chrispr/utility/Event.java new file mode 100644 index 0000000..2ef82c9 --- /dev/null +++ b/src/main/java/com/chrispr/utility/Event.java @@ -0,0 +1,18 @@ +package com.chrispr.utility; + +import java.util.HashSet; +import java.util.Set; +import java.util.function.Consumer; + +// Simple implementation of event handler / observable /etc +public class Event { + private Set> listeners = new HashSet(); + + public void addListener(Consumer listener) { + listeners.add(listener); + } + + public void broadcast(T args) { + listeners.forEach(x -> x.accept(args)); + } +}