diff --git a/android_app/app/build.gradle b/android_app/app/build.gradle index 4af00ae1..a3fe27fc 100644 --- a/android_app/app/build.gradle +++ b/android_app/app/build.gradle @@ -5,7 +5,7 @@ android { defaultConfig { applicationId "com.health.openscale" testApplicationId "com.health.openscale.test" - minSdkVersion 19 + minSdkVersion 21 targetSdkVersion 28 versionCode 44 versionName "2.0.3" @@ -59,7 +59,7 @@ android { } dependencies { - implementation 'com.google.android.material:material:1.1.0-alpha04' + implementation 'com.google.android.material:material:1.1.0-alpha07' implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation 'androidx.appcompat:appcompat:1.0.2' implementation 'androidx.recyclerview:recyclerview:1.0.0' @@ -67,27 +67,24 @@ dependencies { implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0' // Simple CSV implementation 'com.j256.simplecsv:simplecsv:2.3' - // RxAndroidBle - implementation 'com.polidea.rxandroidble2:rxandroidble:1.8.2' - implementation 'io.reactivex.rxjava2:rxjava:2.2.7' - implementation 'io.reactivex.rxjava2:rxandroid:2.1.0' - implementation 'com.jakewharton.rx2:replaying-share:2.1.0' + // Blessed Android + implementation 'com.github.weliem:blessed-android:0.6' // CustomActivityOnCrash implementation 'cat.ereza:customactivityoncrash:2.2.0' // Room - implementation 'androidx.room:room-runtime:2.1.0-alpha04' - annotationProcessor 'androidx.room:room-compiler:2.1.0-alpha04' - androidTestImplementation 'androidx.room:room-testing:2.1.0-alpha04' + implementation 'androidx.room:room-runtime:2.1.0-rc01' + annotationProcessor 'androidx.room:room-compiler:2.1.0-rc01' + androidTestImplementation 'androidx.room:room-testing:2.1.0-rc01' // Timber implementation 'com.jakewharton.timber:timber:4.7.1' // Local unit tests testImplementation 'junit:junit:4.12' // Instrumented unit tests - androidTestImplementation 'androidx.annotation:annotation:1.0.0' - androidTestImplementation 'androidx.test.ext:junit:1.1.0' - androidTestImplementation 'androidx.test:rules:1.1.1' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' - androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.1.1' + androidTestImplementation 'androidx.annotation:annotation:1.0.2' + androidTestImplementation 'androidx.test.ext:junit:1.1.1' + androidTestImplementation 'androidx.test:rules:1.2.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.2.0' } tasks.withType(Test) { diff --git a/android_app/app/src/main/java/com/health/openscale/core/OpenScale.java b/android_app/app/src/main/java/com/health/openscale/core/OpenScale.java index e6e15f36..002c6f06 100644 --- a/android_app/app/src/main/java/com/health/openscale/core/OpenScale.java +++ b/android_app/app/src/main/java/com/health/openscale/core/OpenScale.java @@ -57,8 +57,6 @@ import com.health.openscale.gui.views.LBMMeasurementView; import com.health.openscale.gui.views.MeasurementViewSettings; import com.health.openscale.gui.views.WaterMeasurementView; import com.health.openscale.gui.widget.WidgetProvider; -import com.polidea.rxandroidble2.RxBleClient; -import com.polidea.rxandroidble2.internal.RxBleLog; import java.io.BufferedReader; import java.io.File; @@ -92,7 +90,6 @@ public class OpenScale { private List scaleMeasurementList; private BluetoothCommunication btDeviceDriver; - private RxBleClient bleClient; private AlarmHandler alarmHandler; private Context context; @@ -104,10 +101,6 @@ public class OpenScale { alarmHandler = new AlarmHandler(); btDeviceDriver = null; fragmentList = new ArrayList<>(); - bleClient = RxBleClient.create(context); - - RxBleClient.setLogLevel(RxBleLog.VERBOSE); - RxBleLog.setLogger((level, tag, msg) -> Timber.tag(tag).log(level, msg)); reopenDatabase(false); @@ -130,10 +123,6 @@ public class OpenScale { return instance; } - public RxBleClient getBleClient() { - return bleClient; - } - public void reopenDatabase(boolean truncate) throws SQLiteDatabaseCorruptException { if (appDB != null) { appDB.close(); diff --git a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothBeurerSanitas.java b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothBeurerSanitas.java index 00f73c1d..db17ef04 100644 --- a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothBeurerSanitas.java +++ b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothBeurerSanitas.java @@ -172,7 +172,7 @@ public class BluetoothBeurerSanitas extends BluetoothCommunication { switch (stepNr) { case 0: // Setup notification - setNotificationOn(CUSTOM_CHARACTERISTIC_WEIGHT); + setNotificationOn(CUSTOM_SERVICE_1, CUSTOM_CHARACTERISTIC_WEIGHT); break; case 1: // Say "Hello" to the scale and wait for ack @@ -566,7 +566,7 @@ public class BluetoothBeurerSanitas extends BluetoothCommunication { } private void writeBytes(byte[] data) { - writeBytes(CUSTOM_CHARACTERISTIC_WEIGHT, data); + writeBytes(CUSTOM_SERVICE_1, CUSTOM_CHARACTERISTIC_WEIGHT, data); } private void sendCommand(byte command, byte... parameters) { diff --git a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothCommunication.java b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothCommunication.java index 698e1d46..1c1ed840 100644 --- a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothCommunication.java +++ b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothCommunication.java @@ -17,39 +17,29 @@ package com.health.openscale.core.bluetooth; import android.Manifest; +import android.bluetooth.BluetoothGattCharacteristic; +import android.bluetooth.le.ScanResult; import android.content.Context; import android.content.pm.PackageManager; import android.location.LocationManager; import android.os.Handler; - -import com.health.openscale.core.OpenScale; -import com.health.openscale.core.datatypes.ScaleMeasurement; -import com.jakewharton.rx.ReplayingShare; -import com.polidea.rxandroidble2.RxBleClient; -import com.polidea.rxandroidble2.RxBleConnection; -import com.polidea.rxandroidble2.RxBleDevice; -import com.polidea.rxandroidble2.RxBleDeviceServices; -import com.polidea.rxandroidble2.exceptions.BleException; -import com.polidea.rxandroidble2.scan.ScanSettings; - -import java.io.IOException; -import java.net.SocketException; -import java.util.UUID; -import java.util.concurrent.TimeUnit; +import android.os.Looper; import androidx.core.content.ContextCompat; -import io.reactivex.Observable; -import io.reactivex.Single; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.CompositeDisposable; -import io.reactivex.disposables.Disposable; -import io.reactivex.exceptions.UndeliverableException; -import io.reactivex.plugins.RxJavaPlugins; -import io.reactivex.schedulers.Schedulers; -import io.reactivex.subjects.PublishSubject; + +import com.health.openscale.core.datatypes.ScaleMeasurement; +import com.welie.blessed.BluetoothCentral; +import com.welie.blessed.BluetoothCentralCallback; +import com.welie.blessed.BluetoothPeripheral; +import com.welie.blessed.BluetoothPeripheralCallback; + +import java.util.UUID; + import timber.log.Timber; +import static android.bluetooth.BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT; import static android.content.Context.LOCATION_SERVICE; +import static com.welie.blessed.BluetoothPeripheral.GATT_SUCCESS; public abstract class BluetoothCommunication { public enum BT_STATUS { @@ -69,55 +59,19 @@ public abstract class BluetoothCommunication { protected Context context; - private final int BT_RETRY_TIMES_ON_ERROR = 3; - private final int BT_DELAY_MS = 10; - - private RxBleClient bleClient; - private RxBleDevice bleDevice; - private Observable connectionObservable; - private final CompositeDisposable compositeDisposable = new CompositeDisposable(); - private Disposable scanSubscription; - private PublishSubject disconnectTriggerSubject = PublishSubject.create(); - private Handler callbackBtHandler; private Handler disconnectHandler; + private BluetoothCentral central; + private BluetoothPeripheral btPeripheral; + public BluetoothCommunication(Context context) { this.context = context; - this.bleClient = OpenScale.getInstance().getBleClient(); - this.scanSubscription = null; this.disconnectHandler = new Handler(); this.stepNr = 0; this.stopped = false; - - RxJavaPlugins.setErrorHandler(e -> { - if (e instanceof UndeliverableException && e.getCause() instanceof BleException) { - return; // ignore BleExceptions as they were surely delivered at least once - } - if (e instanceof UndeliverableException) { - onError(e); - } - if ((e instanceof IOException) || (e instanceof SocketException)) { - // fine, irrelevant network problem or API that throws on cancellation - return; - } - if (e instanceof InterruptedException) { - // fine, some blocking code was interrupted by a dispose call - return; - } - if ((e instanceof NullPointerException) || (e instanceof IllegalArgumentException)) { - // that's likely a bug in the application - onError(e); - return; - } - if (e instanceof IllegalStateException) { - // that's a bug in RxJava or in a custom operator - onError(e); - return; - } - onError(e); - }); + this.central = new BluetoothCentral(context, bluetoothCentralCallback, new Handler(Looper.getMainLooper())); } /** @@ -191,14 +145,6 @@ public abstract class BluetoothCommunication { */ abstract protected boolean onNextStep(int stepNr); - /** - * Method is triggered if a Bluetooth data is read from a device. - * - * @param characteristic - * @param value - */ - protected void onBluetoothRead(UUID characteristic, byte[] value) {} - /** * Method is triggered if a Bluetooth data from a device is notified or indicated. * @@ -210,9 +156,9 @@ public abstract class BluetoothCommunication { /** * Method is triggered if a Bluetooth services from a device is discovered. * - * @param rxBleDeviceServices + * @param peripheral */ - protected void onBluetoothDiscovery(RxBleDeviceServices rxBleDeviceServices) { } + protected void onBluetoothDiscovery(BluetoothPeripheral peripheral) { } protected synchronized void stopMachineState() { Timber.d("Stop machine state"); @@ -235,26 +181,9 @@ public abstract class BluetoothCommunication { * @param characteristic the Bluetooth UUID characteristic * @param bytes the bytes that should be write */ - protected Observable writeBytes(UUID characteristic, byte[] bytes) { + protected void writeBytes(UUID service, UUID characteristic, byte[] bytes) { Timber.d("Invoke write bytes [" + byteInHex(bytes) + "] on " + BluetoothGattUuid.prettyPrint(characteristic)); - Observable observable = connectionObservable - .flatMapSingle(rxBleConnection -> rxBleConnection.writeCharacteristic(characteristic, bytes)) - .subscribeOn(Schedulers.trampoline()) - .observeOn(AndroidSchedulers.mainThread()) - .delay(BT_DELAY_MS, TimeUnit.MILLISECONDS) - .retry(BT_RETRY_TIMES_ON_ERROR); - - compositeDisposable.add(observable.subscribe( - value -> { - Timber.d("Write characteristic %s: %s", - BluetoothGattUuid.prettyPrint(characteristic), - byteInHex(value)); - }, - throwable -> onError(throwable) - ) - ); - - return observable; + btPeripheral.writeCharacteristic(btPeripheral.getCharacteristic(service, characteristic), bytes, WRITE_TYPE_DEFAULT); } /** @@ -263,26 +192,10 @@ public abstract class BluetoothCommunication { * @note onBluetoothRead() will be triggered if read command was successful. nextMachineStep() needs to manually called! *@param characteristic the Bluetooth UUID characteristic */ - protected Single readBytes(UUID characteristic) { + void readBytes(UUID service, UUID characteristic) { Timber.d("Invoke read bytes on " + BluetoothGattUuid.prettyPrint(characteristic)); - Single observable = connectionObservable - .firstOrError() - .flatMap(rxBleConnection -> rxBleConnection.readCharacteristic(characteristic)) - .subscribeOn(Schedulers.trampoline()) - .observeOn(AndroidSchedulers.mainThread()) - .delay(BT_DELAY_MS, TimeUnit.MILLISECONDS) - .retry(BT_RETRY_TIMES_ON_ERROR); - compositeDisposable.add(observable - .subscribe(bytes -> { - Timber.d("Read characteristic %s", BluetoothGattUuid.prettyPrint(characteristic)); - onBluetoothRead(characteristic, bytes); - }, - throwable -> onError(throwable) - ) - ); - - return observable; + btPeripheral.readCharacteristic(btPeripheral.getCharacteristic(service, characteristic)); } /** @@ -290,33 +203,13 @@ public abstract class BluetoothCommunication { * * @param characteristic the Bluetooth UUID characteristic */ - protected Observable setIndicationOn(UUID characteristic) { + protected void setIndicationOn(UUID service, UUID characteristic) { Timber.d("Invoke set indication on " + BluetoothGattUuid.prettyPrint(characteristic)); - Observable observable = connectionObservable - .flatMap(rxBleConnection -> rxBleConnection.setupIndication(characteristic)) - .doOnNext(notificationObservable -> { - Timber.d("Successful set indication on for %s", BluetoothGattUuid.prettyPrint(characteristic)); - } - ) - .flatMap(indicationObservable -> indicationObservable) - .subscribeOn(Schedulers.trampoline()) - .observeOn(AndroidSchedulers.mainThread()) - .delay(BT_DELAY_MS, TimeUnit.MILLISECONDS) - .retry(BT_RETRY_TIMES_ON_ERROR); - - compositeDisposable.add(observable.subscribe( - bytes -> { - Timber.d("onCharacteristicChanged %s: %s", - BluetoothGattUuid.prettyPrint(characteristic), - byteInHex(bytes)); - onBluetoothNotify(characteristic, bytes); - resetDisconnectTimer(); - }, - throwable -> onError(throwable) - ) - ); - - return observable; + if(btPeripheral.getService(service) != null) { + stopMachineState(); + BluetoothGattCharacteristic currentTimeCharacteristic = btPeripheral.getCharacteristic(service, characteristic); + btPeripheral.setNotify(currentTimeCharacteristic, true); + } } /** @@ -324,56 +217,13 @@ public abstract class BluetoothCommunication { * * @param characteristic the Bluetooth UUID characteristic */ - protected Observable setNotificationOn(UUID characteristic) { + protected void setNotificationOn(UUID service, UUID characteristic) { Timber.d("Invoke set notification on " + BluetoothGattUuid.prettyPrint(characteristic)); - stopped = true; - Observable observable = connectionObservable - .flatMap(rxBleConnection -> rxBleConnection.setupNotification(characteristic)) - .doOnNext(notificationObservable -> { - Timber.d("Successful set notification on for %s", BluetoothGattUuid.prettyPrint(characteristic)); - stopped = false; - nextMachineStep(); - } - ) - .flatMap(notificationObservable -> notificationObservable) - .subscribeOn(Schedulers.trampoline()) - .observeOn(AndroidSchedulers.mainThread()) - .delay(BT_DELAY_MS, TimeUnit.MILLISECONDS) - .retry(BT_RETRY_TIMES_ON_ERROR); - - compositeDisposable.add(observable.subscribe( - bytes -> { - Timber.d("onCharacteristicChanged %s: %s", - BluetoothGattUuid.prettyPrint(characteristic), - byteInHex(bytes)); - onBluetoothNotify(characteristic, bytes); - resetDisconnectTimer(); - }, - throwable -> onError(throwable) - )); - - return observable; - } - - protected Observable discoverBluetoothServices() { - Timber.d("Invoke discover Bluetooth services"); - final Observable observable = connectionObservable - .flatMapSingle(RxBleConnection::discoverServices) - .subscribeOn(Schedulers.trampoline()) - .observeOn(AndroidSchedulers.mainThread()) - .delay(BT_DELAY_MS, TimeUnit.MILLISECONDS) - .retry(BT_RETRY_TIMES_ON_ERROR); - - compositeDisposable.add(observable.subscribe( - deviceServices -> { - Timber.d("Successful Bluetooth services discovered"); - onBluetoothDiscovery(deviceServices); - }, - throwable -> onError(throwable) - ) - ); - - return observable; + if(btPeripheral.getService(service) != null) { + stopMachineState(); + BluetoothGattCharacteristic currentTimeCharacteristic = btPeripheral.getCharacteristic(service, characteristic); + btPeripheral.setNotify(currentTimeCharacteristic, true); + } } /** @@ -382,13 +232,12 @@ public abstract class BluetoothCommunication { public void disconnect() { Timber.d("Bluetooth disconnect"); setBluetoothStatus(BT_STATUS.CONNECTION_DISCONNECT); - if (scanSubscription != null) { - scanSubscription.dispose(); + central.stopScan(); + if (btPeripheral != null) { + central.cancelConnection(btPeripheral); } callbackBtHandler = null; disconnectHandler.removeCallbacksAndMessages(null); - disconnectTriggerSubject.onNext(true); - compositeDisposable.clear(); } /** @@ -452,6 +301,74 @@ public abstract class BluetoothCommunication { return (value & (1 << bit)) != 0; } + private final BluetoothPeripheralCallback peripheralCallback = new BluetoothPeripheralCallback() { + @Override + public void onServicesDiscovered(BluetoothPeripheral peripheral) { + Timber.d("Successful Bluetooth services discovered"); + onBluetoothDiscovery(peripheral); + } + + @Override + public void onNotificationStateUpdate(BluetoothPeripheral peripheral, BluetoothGattCharacteristic characteristic, int status) { + if( status == GATT_SUCCESS) { + if(peripheral.isNotifying(characteristic)) { + Timber.d(String.format("SUCCESS: Notify set for %s", characteristic.getUuid())); + resumeMachineState(); + } + } else { + Timber.e(String.format("ERROR: Changing notification state failed for %s", characteristic.getUuid())); + } + } + + @Override + public void onCharacteristicWrite(BluetoothPeripheral peripheral, byte[] value, BluetoothGattCharacteristic characteristic, int status) { + if( status == GATT_SUCCESS) { + Timber.d(String.format("SUCCESS: Writing <%s> to <%s>", byteInHex(value), characteristic.getUuid().toString())); + nextMachineStep(); + + } else { + Timber.e(String.format("ERROR: Failed writing <%s> to <%s>", byteInHex(value), characteristic.getUuid().toString())); + } + } + + @Override + public void onCharacteristicUpdate(BluetoothPeripheral peripheral, byte[] value, BluetoothGattCharacteristic characteristic) { + resetDisconnectTimer(); + onBluetoothNotify(characteristic.getUuid(), value); + } + }; + + // Callback for central + private final BluetoothCentralCallback bluetoothCentralCallback = new BluetoothCentralCallback() { + + @Override + public void onConnectedPeripheral(BluetoothPeripheral peripheral) { + Timber.d(String.format("connected to '%s'", peripheral.getName())); + setBluetoothStatus(BT_STATUS.CONNECTION_ESTABLISHED); + btPeripheral = peripheral; + nextMachineStep(); + resetDisconnectTimer(); + } + + @Override + public void onConnectionFailed(BluetoothPeripheral peripheral, final int status) { + Timber.e(String.format("connection '%s' failed with status %d", peripheral.getName(), status )); + setBluetoothStatus(BT_STATUS.CONNECTION_LOST); + } + + @Override + public void onDisconnectedPeripheral(final BluetoothPeripheral peripheral, final int status) { + Timber.d(String.format("disconnected '%s' with status %d", peripheral.getName(), status)); + } + + @Override + public void onDiscoveredPeripheral(BluetoothPeripheral peripheral, ScanResult scanResult) { + Timber.d(String.format("Found peripheral '%s'", peripheral.getName())); + central.stopScan(); + connectToDevice(peripheral); + } + }; + /** * Connect to a Bluetooth device. * @@ -461,8 +378,6 @@ public abstract class BluetoothCommunication { * @param macAddress the Bluetooth address to connect to */ public void connect(String macAddress) { - bleDevice = bleClient.getBleDevice(macAddress); - // Running an LE scan during connect improves connectivity on some phones // (e.g. Sony Xperia Z5 compact, Android 7.1.1). For some scales (e.g. Medisana BS444) // it seems to be a requirement that the scale is discovered before connecting to it. @@ -474,95 +389,30 @@ public abstract class BluetoothCommunication { || locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) ) { Timber.d("Do LE scan before connecting to device"); - disconnectWithDelay(); - scanSubscription = bleClient.scanBleDevices( - new ScanSettings.Builder() - .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) - //.setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES) - .build() - ) - .subscribeOn(Schedulers.trampoline()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(bleScanResult -> { - if (bleScanResult.getBleDevice().getMacAddress().equals(macAddress)) { - connectToDevice(macAddress); - }}, throwable -> setBluetoothStatus(BT_STATUS.NO_DEVICE_FOUND)); + central.scanForPeripheralsWithAddresses(new String[]{macAddress}); } else { Timber.d("No location permission, connecting without LE scan"); - connectToDevice(macAddress); + BluetoothPeripheral peripheral = central.getPeripheral(macAddress); + connectToDevice(peripheral); } } - private void connectToDevice(String macAddress) { - - // stop LE scan before connecting to device - if (scanSubscription != null) { - Timber.d("Stop Le san"); - scanSubscription.dispose(); - scanSubscription = null; - } + private void connectToDevice(BluetoothPeripheral peripheral) { Handler handler = new Handler(); handler.postDelayed(new Runnable() { @Override public void run() { - Timber.d("Try to connect to BLE device " + macAddress); + Timber.d("Try to connect to BLE device " + peripheral.getAddress()); - connectionObservable = bleDevice - .establishConnection(false) - .takeUntil(disconnectTriggerSubject) - .doOnError(throwable -> setBluetoothStatus(BT_STATUS.CONNECTION_RETRYING)) - .subscribeOn(Schedulers.trampoline()) - .observeOn(AndroidSchedulers.mainThread()) - .compose(ReplayingShare.instance()); + stepNr = 0; - if (isConnected()) { - disconnect(); - } else { - stepNr = 0; - - setBtMonitoringOn(); - nextMachineStep(); - resetDisconnectTimer(); - } + central.connectPeripheral(peripheral, peripheralCallback); } }, 1000); } - private void setBtMonitoringOn() { - final Disposable disposableConnectionState = bleDevice.observeConnectionStateChanges() - .subscribe( - connectionState -> { - switch (connectionState) { - case CONNECTED: - setBluetoothStatus(BT_STATUS.CONNECTION_ESTABLISHED); - break; - case CONNECTING: - // empty - break; - case DISCONNECTING: - // empty - break; - case DISCONNECTED: - // setBluetoothStatus(BT_STATUS.CONNECTION_LOST); - break; - } - }, - throwable -> onError(throwable) - ); - - compositeDisposable.add(disposableConnectionState); - } - - protected void onError(Throwable throwable) { - setBluetoothStatus(BT_STATUS.UNEXPECTED_ERROR, throwable.getMessage()); - } - - private boolean isConnected() { - return bleDevice.getConnectionState() == RxBleConnection.RxBleConnectionState.CONNECTED; - } - private void resetDisconnectTimer() { disconnectHandler.removeCallbacksAndMessages(null); disconnectWithDelay(); diff --git a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothCustomOpenScale.java b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothCustomOpenScale.java index 1f1ff930..0a47ed88 100644 --- a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothCustomOpenScale.java +++ b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothCustomOpenScale.java @@ -46,8 +46,7 @@ public class BluetoothCustomOpenScale extends BluetoothCommunication { protected boolean onNextStep(int stepNr) { switch (stepNr) { case 0: - setNotificationOn(WEIGHT_MEASUREMENT_CHARACTERISTIC - ); + setNotificationOn(WEIGHT_MEASUREMENT_SERVICE, WEIGHT_MEASUREMENT_CHARACTERISTIC); break; case 1: Calendar cal = Calendar.getInstance(); @@ -60,7 +59,7 @@ public class BluetoothCustomOpenScale extends BluetoothCommunication { cal.get(Calendar.MINUTE), cal.get(Calendar.SECOND)); - writeBytes(WEIGHT_MEASUREMENT_CHARACTERISTIC, date_time.getBytes()); + writeBytes(WEIGHT_MEASUREMENT_SERVICE, WEIGHT_MEASUREMENT_CHARACTERISTIC, date_time.getBytes()); break; default: return false; @@ -72,7 +71,7 @@ public class BluetoothCustomOpenScale extends BluetoothCommunication { public void clearEEPROM() { byte[] cmd = {(byte)'9'}; - writeBytes(WEIGHT_MEASUREMENT_CHARACTERISTIC, cmd); + writeBytes(WEIGHT_MEASUREMENT_SERVICE, WEIGHT_MEASUREMENT_CHARACTERISTIC, cmd); } @Override diff --git a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothDebug.java b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothDebug.java index 8bed29ed..fd633e50 100644 --- a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothDebug.java +++ b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothDebug.java @@ -21,15 +21,14 @@ import android.bluetooth.BluetoothGattDescriptor; import android.bluetooth.BluetoothGattService; import android.content.Context; -import com.polidea.rxandroidble2.RxBleDeviceServices; +import com.welie.blessed.BluetoothPeripheral; import java.util.HashMap; import timber.log.Timber; public class BluetoothDebug extends BluetoothCommunication { - private HashMap propertyString; - private RxBleDeviceServices rxBleDeviceServices; + HashMap propertyString; BluetoothDebug(Context context) { super(context); @@ -53,7 +52,7 @@ public class BluetoothDebug extends BluetoothCommunication { private boolean isBlacklisted(BluetoothGattService service, BluetoothGattCharacteristic characteristic) { // Reading this triggers a pairing request on Beurer BF710 if (service.getUuid().equals(BluetoothGattUuid.fromShortCode(0xffe0)) - && characteristic.getUuid().equals(BluetoothGattUuid.fromShortCode(0xffe5))) { + && characteristic.getUuid().equals(BluetoothGattUuid.fromShortCode(0xffe5))) { return true; } @@ -137,10 +136,19 @@ public class BluetoothDebug extends BluetoothCommunication { private int readServiceCharacteristics(BluetoothGattService service, int offset) { for (BluetoothGattCharacteristic characteristic : service.getCharacteristics()) { if ((characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_READ) != 0 - && !isBlacklisted(service, characteristic)) { + && !isBlacklisted(service, characteristic)) { if (offset == 0) { - readBytes(characteristic.getUuid()); + readBytes(service.getUuid(), characteristic.getUuid()); + return -1; + } + + offset -= 1; + } + + for (BluetoothGattDescriptor descriptor : characteristic.getDescriptors()) { + if (offset == 0) { + readBytes(service.getUuid(), characteristic.getUuid()); return -1; } @@ -159,39 +167,25 @@ public class BluetoothDebug extends BluetoothCommunication { } @Override - public void onBluetoothDiscovery(RxBleDeviceServices rxBleDeviceServices) { - this.rxBleDeviceServices = rxBleDeviceServices; - resumeMachineState(); - } + protected void onBluetoothDiscovery(BluetoothPeripheral peripheral) { + int offset = 0; - @Override - protected boolean onNextStep(int stepNr) { - switch (stepNr) - { - case 0: - discoverBluetoothServices(); - stopMachineState(); - break; - case 1: - int offset = stepNr; - - for (BluetoothGattService service : rxBleDeviceServices.getBluetoothGattServices()) { - offset = readServiceCharacteristics(service, offset); - } - - for (BluetoothGattService service : rxBleDeviceServices.getBluetoothGattServices()) { - logService(service, false); - } - - setBluetoothStatus(BT_STATUS.CONNECTION_LOST); - break; - case 2: - disconnect(); - break; - default: - return false; + for (BluetoothGattService service : peripheral.getServices()) { + offset = readServiceCharacteristics(service, offset); } - return true; + for (BluetoothGattService service : peripheral.getServices()) { + logService(service, false); + } + + setBluetoothStatus(BT_STATUS.CONNECTION_LOST); + disconnect(); } + + + @Override + protected boolean onNextStep(int stateNr) { + return false; + } + } diff --git a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothDigooDGSO38H.java b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothDigooDGSO38H.java index 5e9c4e38..e5b2fd1f 100644 --- a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothDigooDGSO38H.java +++ b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothDigooDGSO38H.java @@ -60,7 +60,7 @@ public class BluetoothDigooDGSO38H extends BluetoothCommunication { switch (stepNr) { case 0: //Tell device to send us weight measurements - setNotificationOn(WEIGHT_MEASUREMENT_CHARACTERISTIC); + setNotificationOn(WEIGHT_MEASUREMENT_SERVICE, WEIGHT_MEASUREMENT_CHARACTERISTIC); break; case 1: sendMessage(R.string.info_step_on_scale, 0); @@ -102,7 +102,7 @@ public class BluetoothDigooDGSO38H extends BluetoothCommunication { checksum += configBytes[i]; } configBytes[15] = (byte)(checksum & 0xFF); - writeBytes(EXTRA_MEASUREMENT_CHARACTERISTIC, configBytes); + writeBytes(WEIGHT_MEASUREMENT_SERVICE, EXTRA_MEASUREMENT_CHARACTERISTIC, configBytes); } else if (allValues) { ScaleMeasurement scaleBtData = new ScaleMeasurement(); weight = (float) (((weightBytes[3] & 0xFF) << 8) | (weightBytes[4] & 0xFF)) / 100.0f; diff --git a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothExcelvanCF36xBLE.java b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothExcelvanCF36xBLE.java index f876ced9..2b7f8cb6 100644 --- a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothExcelvanCF36xBLE.java +++ b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothExcelvanCF36xBLE.java @@ -86,10 +86,10 @@ public class BluetoothExcelvanCF36xBLE extends BluetoothCommunication { configBytes[configBytes.length - 1] = xorChecksum(configBytes, 1, configBytes.length - 2); - writeBytes(WEIGHT_MEASUREMENT_CHARACTERISTIC, configBytes); + writeBytes(WEIGHT_MEASUREMENT_SERVICE, WEIGHT_MEASUREMENT_CHARACTERISTIC, configBytes); break; case 1: - setNotificationOn(WEIGHT_CUSTOM0_CHARACTERISTIC); + setNotificationOn(WEIGHT_MEASUREMENT_SERVICE, WEIGHT_CUSTOM0_CHARACTERISTIC); break; case 2: sendMessage(R.string.info_step_on_scale, 0); diff --git a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothExingtechY1.java b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothExingtechY1.java index d4fd8046..507522ae 100644 --- a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothExingtechY1.java +++ b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothExingtechY1.java @@ -45,7 +45,7 @@ public class BluetoothExingtechY1 extends BluetoothCommunication { protected boolean onNextStep(int stepNr) { switch (stepNr) { case 0: - setNotificationOn(WEIGHT_MEASUREMENT_CHARACTERISTIC); + setNotificationOn(WEIGHT_MEASUREMENT_SERVICE, WEIGHT_MEASUREMENT_CHARACTERISTIC); break; case 1: final ScaleUser selectedUser = OpenScale.getInstance().getSelectedScaleUser(); @@ -58,7 +58,7 @@ public class BluetoothExingtechY1 extends BluetoothCommunication { byte cmdByte[] = {(byte)0x10, (byte)userId, gender, age, height}; - writeBytes(CMD_MEASUREMENT_CHARACTERISTIC, cmdByte); + writeBytes(WEIGHT_MEASUREMENT_SERVICE, CMD_MEASUREMENT_CHARACTERISTIC, cmdByte); break; case 2: sendMessage(R.string.info_step_on_scale, 0); diff --git a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothFactory.java b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothFactory.java index 38eab0aa..f7189a19 100644 --- a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothFactory.java +++ b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothFactory.java @@ -18,8 +18,6 @@ package com.health.openscale.core.bluetooth; import android.content.Context; -import com.polidea.rxandroidble2.RxBleClient; - import java.util.Locale; public class BluetoothFactory { @@ -42,6 +40,14 @@ public class BluetoothFactory { || name.equals("BF700".toLowerCase(Locale.US))) { return new BluetoothBeurerSanitas(context, BluetoothBeurerSanitas.DeviceType.BEURER_BF710); } + if (name.startsWith("BEURER BF600".toLowerCase(Locale.US)) + || name.startsWith("BEURER BF850".toLowerCase(Locale.US)) + || name.startsWith("BF600".toLowerCase(Locale.US)) + || name.startsWith("BF850".toLowerCase(Locale.US)) + || name.startsWith("BF-600".toLowerCase(Locale.US)) + || name.startsWith("BF-850".toLowerCase(Locale.US))) { + return new BluetoothStandardWeightProfile(context); + } if (name.equals("openScale".toLowerCase(Locale.US))) { return new BluetoothCustomOpenScale(context); } diff --git a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothGattUuid.java b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothGattUuid.java index 17532f5d..b00a2a17 100644 --- a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothGattUuid.java +++ b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothGattUuid.java @@ -66,6 +66,9 @@ public class BluetoothGattUuid { public static final UUID SERVICE_GENERIC_ACCESS = fromShortCode(0x1800); public static final UUID SERVICE_GENERIC_ATTRIBUTE = fromShortCode(0x1801); public static final UUID SERVICE_WEIGHT_SCALE = fromShortCode(0x181d); + public static final UUID SERVICE_CURRENT_TIME = fromShortCode(0x1805); + public static final UUID SERVICE_USER_DATA = fromShortCode(0x181C); + public static final UUID SERVICE_BATTERY_LEVEL = fromShortCode(0x180F); // https://www.bluetooth.com/specifications/gatt/characteristics public static final UUID CHARACTERISTIC_APPEARANCE = fromShortCode(0x2a01); @@ -86,6 +89,9 @@ public class BluetoothGattUuid { public static final UUID CHARACTERISTIC_SOFTWARE_REVISION_STRING = fromShortCode(0x2a28); public static final UUID CHARACTERISTIC_SYSTEM_ID = fromShortCode(0x2a23); public static final UUID CHARACTERISTIC_WEIGHT_MEASUREMENT = fromShortCode(0x2a9d); + public static final UUID CHARACTERISTIC_BATTERY_LEVEL = fromShortCode(0x2A19); + public static final UUID CHARACTERISTIC_CHANGE_INCREMENT = fromShortCode(0x2a99); + public static final UUID CHARACTERISTIC_USER_CONTROL_POINT = fromShortCode(0x2A9F); // https://www.bluetooth.com/specifications/gatt/descriptors public static final UUID DESCRIPTOR_CLIENT_CHARACTERISTIC_CONFIGURATION = fromShortCode(0x2902); diff --git a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothHesley.java b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothHesley.java index 0c910dbf..b6f90030 100644 --- a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothHesley.java +++ b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothHesley.java @@ -42,11 +42,11 @@ public class BluetoothHesley extends BluetoothCommunication { protected boolean onNextStep(int stepNr) { switch (stepNr) { case 0: - setNotificationOn(WEIGHT_MEASUREMENT_CHARACTERISTIC); + setNotificationOn(WEIGHT_MEASUREMENT_SERVICE, WEIGHT_MEASUREMENT_CHARACTERISTIC); break; case 1: byte[] magicBytes = {(byte)0xa5, (byte)0x01, (byte)0x2c, (byte)0xab, (byte)0x50, (byte)0x5a, (byte)0x29}; - writeBytes(CMD_MEASUREMENT_CHARACTERISTIC, magicBytes); + writeBytes(WEIGHT_MEASUREMENT_SERVICE, CMD_MEASUREMENT_CHARACTERISTIC, magicBytes); break; case 2: sendMessage(R.string.info_step_on_scale, 0); diff --git a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothInlife.java b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothInlife.java index d705f4f8..cdab898a 100644 --- a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothInlife.java +++ b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothInlife.java @@ -60,7 +60,7 @@ public class BluetoothInlife extends BluetoothCommunication { data[i++] = parameter; } data[data.length - 2] = xorChecksum(data, 1, data.length - 3); - writeBytes(WEIGHT_CMD_CHARACTERISTIC, data); + writeBytes(WEIGHT_SERVICE, WEIGHT_CMD_CHARACTERISTIC, data); } public BluetoothInlife(Context context) { @@ -76,7 +76,7 @@ public class BluetoothInlife extends BluetoothCommunication { protected boolean onNextStep(int stepNr) { switch (stepNr) { case 0: - setNotificationOn(WEIGHT_MEASUREMENT_CHARACTERISTIC); + setNotificationOn(WEIGHT_SERVICE, WEIGHT_MEASUREMENT_CHARACTERISTIC); break; case 1: ScaleUser scaleUser = OpenScale.getInstance().getSelectedScaleUser(); diff --git a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothMGB.java b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothMGB.java index 8052a4b5..e3570fab 100644 --- a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothMGB.java +++ b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothMGB.java @@ -68,7 +68,7 @@ public class BluetoothMGB extends BluetoothCommunication { buf[6] = (byte)0xCC; buf[7] = (byte)((buf[2] + buf[3] + buf[4] + buf[5] + buf[6]) & 0xFF); - writeBytes(uuid_char_cfg, buf); + writeBytes(uuid_service, uuid_char_cfg, buf); } @@ -85,8 +85,7 @@ public class BluetoothMGB extends BluetoothCommunication { protected boolean onNextStep(int stepNr) { switch (stepNr) { case 0: - setNotificationOn(uuid_char_ctrl - ); + setNotificationOn(uuid_service, uuid_char_ctrl); now = Calendar.getInstance(); user = OpenScale.getInstance().getSelectedScaleUser(); break; diff --git a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothMedisanaBS44x.java b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothMedisanaBS44x.java index 79675e57..a851214b 100644 --- a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothMedisanaBS44x.java +++ b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothMedisanaBS44x.java @@ -55,15 +55,15 @@ public class BluetoothMedisanaBS44x extends BluetoothCommunication { switch (stepNr) { case 0: // set indication on for feature characteristic - setIndicationOn(FEATURE_MEASUREMENT_CHARACTERISTIC); + setIndicationOn(WEIGHT_MEASUREMENT_SERVICE, FEATURE_MEASUREMENT_CHARACTERISTIC); break; case 1: // set indication on for weight measurement - setIndicationOn(WEIGHT_MEASUREMENT_CHARACTERISTIC); + setIndicationOn(WEIGHT_MEASUREMENT_SERVICE, WEIGHT_MEASUREMENT_CHARACTERISTIC); break; case 2: // set indication on for custom5 measurement - setIndicationOn(CUSTOM5_MEASUREMENT_CHARACTERISTIC); + setIndicationOn(WEIGHT_MEASUREMENT_SERVICE, CUSTOM5_MEASUREMENT_CHARACTERISTIC); break; case 3: // send magic number to receive weight data @@ -75,7 +75,7 @@ public class BluetoothMedisanaBS44x extends BluetoothCommunication { byte[] magicBytes = new byte[] {(byte)0x02, date[0], date[1], date[2], date[3]}; - writeBytes(CMD_MEASUREMENT_CHARACTERISTIC, magicBytes); + writeBytes(WEIGHT_MEASUREMENT_SERVICE, CMD_MEASUREMENT_CHARACTERISTIC, magicBytes); break; case 4: sendMessage(R.string.info_step_on_scale, 0); diff --git a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothMiScale.java b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothMiScale.java index de5ee229..d47e40c0 100644 --- a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothMiScale.java +++ b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothMiScale.java @@ -38,6 +38,7 @@ import timber.log.Timber; import static com.health.openscale.core.bluetooth.BluetoothCommunication.BT_STATUS.UNEXPECTED_ERROR; public class BluetoothMiScale extends BluetoothCommunication { + private final UUID WEIGHT_MEASUREMENT_SERVICE = UUID.fromString("0000181d-0000-1000-8000-00805f9b34fb"); private final UUID WEIGHT_MEASUREMENT_HISTORY_CHARACTERISTIC = UUID.fromString("00002a2f-0000-3512-2118-0009af100700"); public BluetoothMiScale(Context context) { @@ -49,65 +50,66 @@ public class BluetoothMiScale extends BluetoothCommunication { return "Xiaomi Mi Scale v1"; } - @Override - public void onBluetoothRead(UUID characteristic, byte[] value) { - byte[] data = value; - - int currentYear = Calendar.getInstance().get(Calendar.YEAR); - int currentMonth = Calendar.getInstance().get(Calendar.MONTH)+1; - int currentDay = Calendar.getInstance().get(Calendar.DAY_OF_MONTH); - int scaleYear = ((data[1] & 0xFF) << 8) | (data[0] & 0xFF); - int scaleMonth = (int) data[2]; - int scaleDay = (int) data[3]; - - if (!(currentYear == scaleYear && currentMonth == scaleMonth && currentDay == scaleDay)) { - Timber.d("Current year and scale year is different"); - - // set current time - Calendar currentDateTime = Calendar.getInstance(); - int year = currentDateTime.get(Calendar.YEAR); - byte month = (byte)(currentDateTime.get(Calendar.MONTH)+1); - byte day = (byte)currentDateTime.get(Calendar.DAY_OF_MONTH); - byte hour = (byte)currentDateTime.get(Calendar.HOUR_OF_DAY); - byte min = (byte)currentDateTime.get(Calendar.MINUTE); - byte sec = (byte)currentDateTime.get(Calendar.SECOND); - - byte[] dateTimeByte = {(byte)(year), (byte)(year >> 8), month, day, hour, min, sec, 0x03, 0x00, 0x00}; - - writeBytes(BluetoothGattUuid.CHARACTERISTIC_CURRENT_TIME, dateTimeByte); - } - } @Override public void onBluetoothNotify(UUID characteristic, byte[] value) { - final byte[] data = value; - if (data != null && data.length > 0) { + if (characteristic.equals(BluetoothGattUuid.CHARACTERISTIC_CURRENT_TIME)) { + byte[] data = value; - // Stop command from mi scale received - if (data[0] == 0x03) { - // send stop command to mi scale - writeBytes(WEIGHT_MEASUREMENT_HISTORY_CHARACTERISTIC, new byte[]{0x03}); - // acknowledge that you received the last history data - int uniqueNumber = getUniqueNumber(); + int currentYear = Calendar.getInstance().get(Calendar.YEAR); + int currentMonth = Calendar.getInstance().get(Calendar.MONTH) + 1; + int currentDay = Calendar.getInstance().get(Calendar.DAY_OF_MONTH); + int scaleYear = ((data[1] & 0xFF) << 8) | (data[0] & 0xFF); + int scaleMonth = (int) data[2]; + int scaleDay = (int) data[3]; - byte[] userIdentifier = new byte[]{(byte)0x04, (byte)0xFF, (byte)0xFF, (byte) ((uniqueNumber & 0xFF00) >> 8), (byte) ((uniqueNumber & 0xFF) >> 0)}; - writeBytes(WEIGHT_MEASUREMENT_HISTORY_CHARACTERISTIC, userIdentifier); + if (!(currentYear == scaleYear && currentMonth == scaleMonth && currentDay == scaleDay)) { + Timber.d("Current year and scale year is different"); - resumeMachineState(); + // set current time + Calendar currentDateTime = Calendar.getInstance(); + int year = currentDateTime.get(Calendar.YEAR); + byte month = (byte) (currentDateTime.get(Calendar.MONTH) + 1); + byte day = (byte) currentDateTime.get(Calendar.DAY_OF_MONTH); + byte hour = (byte) currentDateTime.get(Calendar.HOUR_OF_DAY); + byte min = (byte) currentDateTime.get(Calendar.MINUTE); + byte sec = (byte) currentDateTime.get(Calendar.SECOND); + + byte[] dateTimeByte = {(byte) (year), (byte) (year >> 8), month, day, hour, min, sec, 0x03, 0x00, 0x00}; + + writeBytes(WEIGHT_MEASUREMENT_SERVICE, BluetoothGattUuid.CHARACTERISTIC_CURRENT_TIME, dateTimeByte); } + } else { + final byte[] data = value; + + if (data != null && data.length > 0) { + + // Stop command from mi scale received + if (data[0] == 0x03) { + // send stop command to mi scale + writeBytes(WEIGHT_MEASUREMENT_SERVICE, WEIGHT_MEASUREMENT_HISTORY_CHARACTERISTIC, new byte[]{0x03}); + // acknowledge that you received the last history data + int uniqueNumber = getUniqueNumber(); + + byte[] userIdentifier = new byte[]{(byte) 0x04, (byte) 0xFF, (byte) 0xFF, (byte) ((uniqueNumber & 0xFF00) >> 8), (byte) ((uniqueNumber & 0xFF) >> 0)}; + writeBytes(WEIGHT_MEASUREMENT_SERVICE, WEIGHT_MEASUREMENT_HISTORY_CHARACTERISTIC, userIdentifier); + + resumeMachineState(); + } + + if (data.length == 20) { + final byte[] firstWeight = Arrays.copyOfRange(data, 0, 10); + final byte[] secondWeight = Arrays.copyOfRange(data, 10, 20); + parseBytes(firstWeight); + parseBytes(secondWeight); + } + + if (data.length == 10) { + parseBytes(data); + } - if (data.length == 20) { - final byte[] firstWeight = Arrays.copyOfRange(data, 0, 10); - final byte[] secondWeight = Arrays.copyOfRange(data, 10, 20); - parseBytes(firstWeight); - parseBytes(secondWeight); } - - if (data.length == 10) { - parseBytes(data); - } - } } @@ -117,32 +119,32 @@ public class BluetoothMiScale extends BluetoothCommunication { switch (stepNr) { case 0: // read device time - readBytes(BluetoothGattUuid.CHARACTERISTIC_CURRENT_TIME); + readBytes(WEIGHT_MEASUREMENT_SERVICE, BluetoothGattUuid.CHARACTERISTIC_CURRENT_TIME); break; case 1: // Set on history weight measurement byte[] magicBytes = new byte[]{(byte)0x01, (byte)0x96, (byte)0x8a, (byte)0xbd, (byte)0x62}; - writeBytes(WEIGHT_MEASUREMENT_HISTORY_CHARACTERISTIC, magicBytes); + writeBytes(WEIGHT_MEASUREMENT_SERVICE, WEIGHT_MEASUREMENT_HISTORY_CHARACTERISTIC, magicBytes); break; case 2: // set notification on for weight measurement history - setNotificationOn(WEIGHT_MEASUREMENT_HISTORY_CHARACTERISTIC); + setNotificationOn(WEIGHT_MEASUREMENT_SERVICE, WEIGHT_MEASUREMENT_HISTORY_CHARACTERISTIC); break; case 3: // set notification on for weight measurement - setNotificationOn(BluetoothGattUuid.CHARACTERISTIC_WEIGHT_MEASUREMENT); + setNotificationOn(WEIGHT_MEASUREMENT_SERVICE, BluetoothGattUuid.CHARACTERISTIC_WEIGHT_MEASUREMENT); break; case 4: // configure scale to get only last measurements int uniqueNumber = getUniqueNumber(); byte[] userIdentifier = new byte[]{(byte)0x01, (byte)0xFF, (byte)0xFF, (byte) ((uniqueNumber & 0xFF00) >> 8), (byte) ((uniqueNumber & 0xFF) >> 0)}; - writeBytes(WEIGHT_MEASUREMENT_HISTORY_CHARACTERISTIC, userIdentifier); + writeBytes(WEIGHT_MEASUREMENT_SERVICE, WEIGHT_MEASUREMENT_HISTORY_CHARACTERISTIC, userIdentifier); break; case 5: // invoke receiving history data - writeBytes(WEIGHT_MEASUREMENT_HISTORY_CHARACTERISTIC, new byte[]{0x02}); + writeBytes(WEIGHT_MEASUREMENT_SERVICE, WEIGHT_MEASUREMENT_HISTORY_CHARACTERISTIC, new byte[]{0x02}); stopMachineState(); break; default: diff --git a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothMiScale2.java b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothMiScale2.java index 62fe5bf9..4676fbb7 100644 --- a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothMiScale2.java +++ b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothMiScale2.java @@ -64,12 +64,12 @@ public class BluetoothMiScale2 extends BluetoothCommunication { if (data[0] == 0x03) { Timber.d("Scale stop byte received"); // send stop command to mi scale - writeBytes(WEIGHT_MEASUREMENT_HISTORY_CHARACTERISTIC, new byte[]{0x03}); + writeBytes(BluetoothGattUuid.SERVICE_BODY_COMPOSITION, WEIGHT_MEASUREMENT_HISTORY_CHARACTERISTIC, new byte[]{0x03}); // acknowledge that you received the last history data int uniqueNumber = getUniqueNumber(); byte[] userIdentifier = new byte[]{(byte)0x04, (byte)0xFF, (byte)0xFF, (byte) ((uniqueNumber & 0xFF00) >> 8), (byte) ((uniqueNumber & 0xFF) >> 0)}; - writeBytes(WEIGHT_MEASUREMENT_HISTORY_CHARACTERISTIC, userIdentifier); + writeBytes(BluetoothGattUuid.SERVICE_BODY_COMPOSITION, WEIGHT_MEASUREMENT_HISTORY_CHARACTERISTIC, userIdentifier); disconnect(); @@ -98,7 +98,7 @@ public class BluetoothMiScale2 extends BluetoothCommunication { // set scale units final ScaleUser selectedUser = OpenScale.getInstance().getSelectedScaleUser(); byte[] setUnitCmd = new byte[]{(byte)0x06, (byte)0x04, (byte)0x00, (byte) selectedUser.getScaleUnit().toInt()}; - writeBytes(WEIGHT_CUSTOM_CONFIG, setUnitCmd); + writeBytes(WEIGHT_CUSTOM_SERVICE, WEIGHT_CUSTOM_CONFIG, setUnitCmd); break; case 1: // set current time @@ -112,22 +112,22 @@ public class BluetoothMiScale2 extends BluetoothCommunication { byte[] dateTimeByte = {(byte)(year), (byte)(year >> 8), month, day, hour, min, sec, 0x03, 0x00, 0x00}; - writeBytes(BluetoothGattUuid.CHARACTERISTIC_CURRENT_TIME, dateTimeByte); + writeBytes(BluetoothGattUuid.SERVICE_BODY_COMPOSITION, BluetoothGattUuid.CHARACTERISTIC_CURRENT_TIME, dateTimeByte); break; case 2: // set notification on for weight measurement history - setNotificationOn(WEIGHT_MEASUREMENT_HISTORY_CHARACTERISTIC); + setNotificationOn(BluetoothGattUuid.SERVICE_BODY_COMPOSITION, WEIGHT_MEASUREMENT_HISTORY_CHARACTERISTIC); break; case 3: // configure scale to get only last measurements int uniqueNumber = getUniqueNumber(); byte[] userIdentifier = new byte[]{(byte)0x01, (byte)0xFF, (byte)0xFF, (byte) ((uniqueNumber & 0xFF00) >> 8), (byte) ((uniqueNumber & 0xFF) >> 0)}; - writeBytes(WEIGHT_MEASUREMENT_HISTORY_CHARACTERISTIC, userIdentifier); + writeBytes(BluetoothGattUuid.SERVICE_BODY_COMPOSITION, WEIGHT_MEASUREMENT_HISTORY_CHARACTERISTIC, userIdentifier); break; case 4: // invoke receiving history data - writeBytes(WEIGHT_MEASUREMENT_HISTORY_CHARACTERISTIC, new byte[]{0x02}); + writeBytes(BluetoothGattUuid.SERVICE_BODY_COMPOSITION, WEIGHT_MEASUREMENT_HISTORY_CHARACTERISTIC, new byte[]{0x02}); stopMachineState(); break; default: diff --git a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothOneByone.java b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothOneByone.java index c01d75ba..6e7008a7 100644 --- a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothOneByone.java +++ b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothOneByone.java @@ -50,7 +50,7 @@ public class BluetoothOneByone extends BluetoothCommunication { protected boolean onNextStep(int stepNr) { switch (stepNr) { case 0: - setNotificationOn(WEIGHT_MEASUREMENT_CHARACTERISTIC_BODY_COMPOSITION); + setNotificationOn(WEIGHT_MEASUREMENT_SERVICE, WEIGHT_MEASUREMENT_CHARACTERISTIC_BODY_COMPOSITION); break; case 1: ScaleUser currentUser = OpenScale.getInstance().getSelectedScaleUser(); @@ -69,7 +69,7 @@ public class BluetoothOneByone extends BluetoothCommunication { (byte)0x00, (byte)0x00, (byte)0x00}; magicBytes[magicBytes.length - 1] = xorChecksum(magicBytes, 0, magicBytes.length - 1); - writeBytes(CMD_MEASUREMENT_CHARACTERISTIC, magicBytes); + writeBytes(WEIGHT_MEASUREMENT_SERVICE, CMD_MEASUREMENT_CHARACTERISTIC, magicBytes); break; case 2: sendMessage(R.string.info_step_on_scale, 0); diff --git a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothQNScale.java b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothQNScale.java index b8bfb628..b4e5f94a 100644 --- a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothQNScale.java +++ b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothQNScale.java @@ -88,11 +88,11 @@ public class BluetoothQNScale extends BluetoothCommunication { switch (stepNr) { case 0: // set notification on for custom characteristic 1 (weight, time, and others) - setNotificationOn(CUSTOM1_MEASUREMENT_CHARACTERISTIC); + setNotificationOn(WEIGHT_MEASUREMENT_SERVICE, CUSTOM1_MEASUREMENT_CHARACTERISTIC); break; case 1: // set indication on for weight measurement - setIndicationOn(CUSTOM2_MEASUREMENT_CHARACTERISTIC); + setIndicationOn(WEIGHT_MEASUREMENT_SERVICE, CUSTOM2_MEASUREMENT_CHARACTERISTIC); break; case 2: final ScaleUser scaleUser = OpenScale.getInstance().getSelectedScaleUser(); @@ -108,7 +108,7 @@ public class BluetoothQNScale extends BluetoothCommunication { byte[] ffe3magicBytes = new byte[]{(byte) 0x13, (byte) 0x09, (byte) 0x15, weightUnitByte, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00}; // Set last byte to be checksum ffe3magicBytes[ffe3magicBytes.length -1] = sumChecksum(ffe3magicBytes, 0, ffe3magicBytes.length - 1); - writeBytes(CUSTOM3_MEASUREMENT_CHARACTERISTIC, ffe3magicBytes); + writeBytes(WEIGHT_MEASUREMENT_SERVICE, CUSTOM3_MEASUREMENT_CHARACTERISTIC, ffe3magicBytes); break; case 3: // send time magic number to receive weight data @@ -117,7 +117,7 @@ public class BluetoothQNScale extends BluetoothCommunication { byte[] date = new byte[4]; Converters.toInt32Le(date, 0, timestamp); byte[] timeMagicBytes = new byte[]{(byte) 0x02, date[0], date[1], date[2], date[3]}; - writeBytes(CUSTOM4_MEASUREMENT_CHARACTERISTIC, timeMagicBytes); + writeBytes(WEIGHT_MEASUREMENT_SERVICE, CUSTOM4_MEASUREMENT_CHARACTERISTIC, timeMagicBytes); break; case 4: sendMessage(R.string.info_step_on_scale, 0); diff --git a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothSenssun.java b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothSenssun.java index c89c1d37..76d54d17 100644 --- a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothSenssun.java +++ b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothSenssun.java @@ -16,14 +16,13 @@ package com.health.openscale.core.bluetooth; -import android.bluetooth.BluetoothGattService; import android.content.Context; import com.health.openscale.R; import com.health.openscale.core.OpenScale; import com.health.openscale.core.datatypes.ScaleMeasurement; import com.health.openscale.core.datatypes.ScaleUser; -import com.polidea.rxandroidble2.RxBleDeviceServices; +import com.welie.blessed.BluetoothPeripheral; import java.util.Calendar; import java.util.Date; @@ -40,6 +39,7 @@ public class BluetoothSenssun extends BluetoothCommunication { private final UUID MODEL_B_NOTIFICATION_CHARACTERISTIC = UUID.fromString("0000ffb2-0000-1000-8000-00805f9b34fb"); private final UUID MODEL_B_WRITE_CHARACTERISTIC = UUID.fromString("0000ffb2-0000-1000-8000-00805f9b34fb"); + private UUID writeService; private UUID writeCharacteristic; private int lastWeight, lastFat, lastHydration, lastMuscle, lastBone, lastKcal; @@ -57,23 +57,21 @@ public class BluetoothSenssun extends BluetoothCommunication { } @Override - protected void onBluetoothDiscovery(RxBleDeviceServices rxBleDeviceServices) { - for (BluetoothGattService gattService : rxBleDeviceServices.getBluetoothGattServices()) { - if (gattService.getUuid().equals(MODEL_A_MEASUREMENT_SERVICE)) { - writeCharacteristic = MODEL_A_WRITE_CHARACTERISTIC; - setNotificationOn(MODEL_A_NOTIFICATION_CHARACTERISTIC); - Timber.d("Found a Model A"); - break; - } - if (gattService.getUuid().equals(MODEL_B_MEASUREMENT_SERVICE)) { - writeCharacteristic = MODEL_B_WRITE_CHARACTERISTIC; - setNotificationOn(MODEL_B_NOTIFICATION_CHARACTERISTIC); - Timber.d("Found a Model B"); - break; - } - } + protected void onBluetoothDiscovery(BluetoothPeripheral peripheral) { - resumeMachineState(); + if (peripheral.getService(MODEL_A_MEASUREMENT_SERVICE) != null) { + writeService = MODEL_A_MEASUREMENT_SERVICE; + writeCharacteristic = MODEL_A_WRITE_CHARACTERISTIC; + setNotificationOn(MODEL_A_MEASUREMENT_SERVICE, MODEL_A_NOTIFICATION_CHARACTERISTIC); + Timber.d("Found a Model A"); + } + + if (peripheral.getService(MODEL_B_MEASUREMENT_SERVICE) != null) { + writeService = MODEL_B_MEASUREMENT_SERVICE; + writeCharacteristic = MODEL_B_WRITE_CHARACTERISTIC; + setNotificationOn(MODEL_B_MEASUREMENT_SERVICE, MODEL_B_NOTIFICATION_CHARACTERISTIC); + Timber.d("Found a Model B"); + } } @Override @@ -83,14 +81,10 @@ public class BluetoothSenssun extends BluetoothCommunication { weightStabilized = false; stepMessageDisplayed = false; values = 0; - discoverBluetoothServices(); - stopMachineState(); - break; - case 1: Timber.d("Sync Date"); synchroniseDate(); break; - case 2: + case 1: Timber.d("Sync Time"); synchroniseTime(); break; @@ -202,7 +196,7 @@ public class BluetoothSenssun extends BluetoothCommunication { addChecksum(message); - writeBytes(writeCharacteristic, message); + writeBytes(writeService, writeCharacteristic, message); } private void synchroniseTime() { @@ -216,7 +210,7 @@ public class BluetoothSenssun extends BluetoothCommunication { addChecksum(message); - writeBytes(writeCharacteristic, message); + writeBytes(writeService, writeCharacteristic, message); } private void addChecksum(byte[] message) { @@ -239,6 +233,6 @@ public class BluetoothSenssun extends BluetoothCommunication { addChecksum(message); - writeBytes(writeCharacteristic, message); + writeBytes(writeService, writeCharacteristic, message); } } diff --git a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothStandardWeightProfile.java b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothStandardWeightProfile.java new file mode 100644 index 00000000..94f67241 --- /dev/null +++ b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothStandardWeightProfile.java @@ -0,0 +1,303 @@ +/* Copyright (C) 2019 olie.xdev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + */ + + /* + * Based on source-code by weliem/blessed-android + */ +package com.health.openscale.core.bluetooth; + +import android.content.Context; + +import com.health.openscale.core.OpenScale; +import com.health.openscale.core.datatypes.ScaleMeasurement; +import com.health.openscale.core.datatypes.ScaleUser; +import com.welie.blessed.BluetoothBytesParser; + +import java.util.Calendar; +import java.util.Date; +import java.util.UUID; + +import timber.log.Timber; + +import static com.welie.blessed.BluetoothBytesParser.FORMAT_UINT16; +import static com.welie.blessed.BluetoothBytesParser.FORMAT_UINT8; + +public class BluetoothStandardWeightProfile extends BluetoothCommunication { + private int CURRENT_USER_CONSENT = 3289; + + // UDS control point codes + private static final byte UDS_CP_REGISTER_NEW_USER = 0x01; + private static final byte UDS_CP_CONSENT = 0x02; + private static final byte UDS_CP_DELETE_USER_DATA = 0x03; + private static final byte UDS_CP_LIST_ALL_USERS = 0x04; + private static final byte UDS_CP_DELETE_USERS = 0x05; + private static final byte UDS_CP_RESPONSE = 0x20; + + // UDS response codes + private static final byte UDS_CP_RESP_VALUE_SUCCESS = 0x01; + private static final byte UDS_CP_RESP_OP_CODE_NOT_SUPPORTED = 0x02; + private static final byte UDS_CP_RESP_INVALID_PARAMETER = 0x03; + private static final byte UDS_CP_RESP_OPERATION_FAILED = 0x04; + private static final byte UDS_CP_RESP_USER_NOT_AUTHORIZED = 0x05; + + public BluetoothStandardWeightProfile(Context context) { + super(context); + } + + @Override + public String driverName() { + return "Bluetooth Standard Weight Profile"; + } + + @Override + protected boolean onNextStep(int stepNr) { + + switch (stepNr) { + case 0: + // Read manufacturer and model number from the Device Information Service + readBytes(BluetoothGattUuid.SERVICE_DEVICE_INFORMATION, BluetoothGattUuid.CHARACTERISTIC_MANUFACTURER_NAME_STRING); + readBytes(BluetoothGattUuid.SERVICE_DEVICE_INFORMATION, BluetoothGattUuid.CHARACTERISTIC_MODEL_NUMBER_STRING); + break; + case 1: + // Write the current time + BluetoothBytesParser parser = new BluetoothBytesParser(); + parser.setCurrentTime(Calendar.getInstance()); + writeBytes(BluetoothGattUuid.SERVICE_CURRENT_TIME, BluetoothGattUuid.CHARACTERISTIC_CURRENT_TIME, parser.getValue()); + break; + case 2: + // Turn on notification for Weight Service + setNotificationOn(BluetoothGattUuid.SERVICE_WEIGHT_SCALE, BluetoothGattUuid.CHARACTERISTIC_WEIGHT_MEASUREMENT); + break; + case 3: + // Turn on notification for Body Composition Service + setNotificationOn(BluetoothGattUuid.SERVICE_BODY_COMPOSITION, BluetoothGattUuid.CHARACTERISTIC_BODY_COMPOSITION_MEASUREMENT); + break; + case 4: + // Turn on notification for User Data Service + setNotificationOn(BluetoothGattUuid.SERVICE_USER_DATA, BluetoothGattUuid.CHARACTERISTIC_CHANGE_INCREMENT); + setNotificationOn(BluetoothGattUuid.SERVICE_USER_DATA, BluetoothGattUuid.CHARACTERISTIC_USER_CONTROL_POINT); + break; + case 5: + // Turn on notifications for Battery Service + setNotificationOn(BluetoothGattUuid.SERVICE_BATTERY_LEVEL, BluetoothGattUuid.CHARACTERISTIC_BATTERY_LEVEL); + break; + case 6: + final ScaleUser selectedUser = OpenScale.getInstance().getSelectedScaleUser(); + registerUser(CURRENT_USER_CONSENT); + setUser(selectedUser.getId(), CURRENT_USER_CONSENT); + break; + default: + return false; + } + + return true; + } + + @Override + public void onBluetoothNotify(UUID characteristic, byte[] value) { + BluetoothBytesParser parser = new BluetoothBytesParser(value); + + if(characteristic.equals(BluetoothGattUuid.CHARACTERISTIC_CURRENT_TIME)) { + Date currentTime = parser.getDateTime(); + Timber.d(String.format("Received device time: %s", currentTime)); + } + else if(characteristic.equals(BluetoothGattUuid.CHARACTERISTIC_WEIGHT_MEASUREMENT)) { + handleWeightMeasurement(value); + } + else if(characteristic.equals(BluetoothGattUuid.CHARACTERISTIC_BODY_COMPOSITION_MEASUREMENT)) { + handleBodyCompositionMeasurement(value); + } + else if(characteristic.equals(BluetoothGattUuid.CHARACTERISTIC_BATTERY_LEVEL)) { + int batteryLevel = parser.getIntValue(FORMAT_UINT8); + Timber.d(String.format("Received battery level %d%%", batteryLevel)); + } + else if(characteristic.equals(BluetoothGattUuid.CHARACTERISTIC_MANUFACTURER_NAME_STRING)) { + String manufacturer = parser.getStringValue(0); + Timber.d(String.format("Received manufacturer: %s", manufacturer)); + } + else if(characteristic.equals(BluetoothGattUuid.CHARACTERISTIC_MODEL_NUMBER_STRING)) { + String modelNumber = parser.getStringValue(0); + Timber.d(String.format("Received modelnumber: %s", modelNumber)); + } + else if(characteristic.equals(BluetoothGattUuid.CHARACTERISTIC_USER_CONTROL_POINT)) { + if(value[0]==UDS_CP_RESPONSE) { + switch (value[1]) { + case UDS_CP_REGISTER_NEW_USER: + if (value[2] == UDS_CP_RESP_VALUE_SUCCESS) { + int userIndex = value[3]; + Timber.d(String.format("Created user %d", userIndex)); + } else { + Timber.e("ERROR: could not register new user"); + } + break; + case UDS_CP_CONSENT: + if (value[2] == UDS_CP_RESP_VALUE_SUCCESS) { + Timber.d("Success user consent"); + } else if (value[2] == UDS_CP_RESP_USER_NOT_AUTHORIZED) { + Timber.e("Not authorized"); + } + break; + default: + Timber.e("Unhandled response"); + break; + } + } + } else { + Timber.d(String.format("Got data: <%s>", byteInHex(value))); + } + } + + private void handleWeightMeasurement(byte[] value) { + BluetoothBytesParser parser = new BluetoothBytesParser(value); + final int flags = parser.getIntValue(BluetoothBytesParser.FORMAT_UINT8); + boolean isKg = (flags & 0x01) == 0; + final boolean timestampPresent = (flags & 0x02) > 0; + final boolean userIDPresent = (flags & 0x04) > 0; + final boolean bmiAndHeightPresent = (flags & 0x08) > 0; + + ScaleMeasurement scaleMeasurement = new ScaleMeasurement(); + + // Determine the right weight multiplier + float weightMultiplier = isKg ? 0.005f : 0.01f; + + // Get weight + float weightValue = parser.getIntValue(BluetoothBytesParser.FORMAT_UINT16) * weightMultiplier; + scaleMeasurement.setWeight(weightValue); + + if(timestampPresent) { + Date timestamp = parser.getDateTime(); + scaleMeasurement.setDateTime(timestamp); + } + + if(userIDPresent) { + int userID = parser.getIntValue(BluetoothBytesParser.FORMAT_UINT8); + Timber.d(String.format("User id: %i", userID)); + } + + if(bmiAndHeightPresent) { + float BMI = parser.getIntValue(BluetoothBytesParser.FORMAT_UINT16) * 0.1f; + float heightInMeters = parser.getIntValue(BluetoothBytesParser.FORMAT_UINT16) * 0.001f; + } + + Timber.d(String.format("Got weight: %s", weightValue)); + addScaleMeasurement(scaleMeasurement); + } + + private void handleBodyCompositionMeasurement(byte[] value) { + BluetoothBytesParser parser = new BluetoothBytesParser(value); + final int flags = parser.getIntValue(BluetoothBytesParser.FORMAT_UINT16); + boolean isKg = (flags & 0x0001) == 0; + float massMultiplier = (float) (isKg ? 0.005 : 0.01); + boolean timestampPresent = (flags & 0x0002) > 0; + boolean userIDPresent = (flags & 0x0004) > 0; + boolean bmrPresent = (flags & 0x0008) > 0; + boolean musclePercentagePresent = (flags & 0x0010) > 0; + boolean muscleMassPresent = (flags & 0x0020) > 0; + boolean fatFreeMassPresent = (flags & 0x0040) > 0; + boolean softLeanMassPresent = (flags & 0x0080) > 0; + boolean bodyWaterMassPresent = (flags & 0x0100) > 0; + boolean impedancePresent = (flags & 0x0200) > 0; + boolean weightPresent = (flags & 0x0400) > 0; + boolean heightPresent = (flags & 0x0800) > 0; + boolean multiPacketMeasurement = (flags & 0x1000) > 0; + + ScaleMeasurement scaleMeasurement = new ScaleMeasurement(); + + float bodyFatPercentage = parser.getIntValue(BluetoothBytesParser.FORMAT_UINT16) * 0.1f; + scaleMeasurement.setFat(bodyFatPercentage); + + // Read timestamp if present + if (timestampPresent) { + Date timestamp = parser.getDateTime(); + scaleMeasurement.setDateTime(timestamp); + } + + // Read userID if present + if (userIDPresent) { + int userID = parser.getIntValue(BluetoothBytesParser.FORMAT_UINT8); + Timber.d(String.format("user id: %i", userID)); + } + + // Read bmr if present + if (bmrPresent) { + int bmrInJoules = parser.getIntValue(BluetoothBytesParser.FORMAT_UINT16); + int bmrInKcal = Math.round(((bmrInJoules / 4.1868f) * 10.0f) / 10.0f); + } + + // Read musclePercentage if present + if (musclePercentagePresent) { + float musclePercentage = parser.getIntValue(BluetoothBytesParser.FORMAT_UINT16) * 0.1f; + scaleMeasurement.setMuscle(musclePercentage); + } + + // Read muscleMass if present + if (muscleMassPresent) { + float muscleMass = parser.getIntValue(BluetoothBytesParser.FORMAT_UINT16) * massMultiplier; + } + + // Read fatFreeMassPresent if present + if (fatFreeMassPresent) { + float fatFreeMass = parser.getIntValue(BluetoothBytesParser.FORMAT_UINT16) * massMultiplier; + } + + // Read softleanMass if present + if (softLeanMassPresent) { + float softLeanMass = parser.getIntValue(BluetoothBytesParser.FORMAT_UINT16) * massMultiplier; + } + + // Read bodyWaterMass if present + if (bodyWaterMassPresent) { + float bodyWaterMass = parser.getIntValue(BluetoothBytesParser.FORMAT_UINT16) * massMultiplier; + scaleMeasurement.setWater(bodyWaterMass); + } + + // Read impedance if present + if (impedancePresent) { + float impedance = parser.getIntValue(BluetoothBytesParser.FORMAT_UINT16) * 0.1f; + } + + // Read weight if present + if (weightPresent) { + float weightValue = parser.getIntValue(BluetoothBytesParser.FORMAT_UINT16) * massMultiplier; + scaleMeasurement.setWeight(weightValue); + } + + // Read height if present + if (heightPresent) { + float heightValue = parser.getIntValue(BluetoothBytesParser.FORMAT_UINT16); + } + + Timber.d(String.format("Got body composition: %s", byteInHex(value))); + addScaleMeasurement(scaleMeasurement); + } + + private void registerUser(int consentCode) { + BluetoothBytesParser parser = new BluetoothBytesParser(new byte[]{0,0,0}); + parser.setIntValue(UDS_CP_REGISTER_NEW_USER, FORMAT_UINT8,0); + parser.setIntValue(consentCode, FORMAT_UINT16,1); + Timber.d(String.format("registerUser consentCode: %d", consentCode)); + writeBytes(BluetoothGattUuid.SERVICE_USER_DATA, BluetoothGattUuid.CHARACTERISTIC_USER_CONTROL_POINT, parser.getValue()); + } + + private void setUser(int userIndex, int consentCode) { + BluetoothBytesParser parser = new BluetoothBytesParser(new byte[]{0,0,0,0}); + parser.setIntValue(UDS_CP_CONSENT,FORMAT_UINT8,0); + parser.setIntValue(userIndex, FORMAT_UINT8,1); + parser.setIntValue(consentCode, FORMAT_UINT16,2); + Timber.d(String.format("setUser userIndex: %d, consentCode: %d", userIndex, consentCode)); + writeBytes(BluetoothGattUuid.SERVICE_USER_DATA, BluetoothGattUuid.CHARACTERISTIC_USER_CONTROL_POINT, parser.getValue()); + } +} diff --git a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothTrisaBodyAnalyze.java b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothTrisaBodyAnalyze.java index b94a369a..173e9fad 100644 --- a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothTrisaBodyAnalyze.java +++ b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothTrisaBodyAnalyze.java @@ -19,6 +19,8 @@ import android.content.Context; import android.content.SharedPreferences; import android.preference.PreferenceManager; +import androidx.annotation.Nullable; + import com.health.openscale.R; import com.health.openscale.core.OpenScale; import com.health.openscale.core.bluetooth.lib.TrisaBodyAnalyzeLib; @@ -29,7 +31,6 @@ import com.health.openscale.core.utils.Converters; import java.util.Date; import java.util.UUID; -import androidx.annotation.Nullable; import timber.log.Timber; /** @@ -124,7 +125,7 @@ public class BluetoothTrisaBodyAnalyze extends BluetoothCommunication { switch (stepNr) { case 0: // Register for notifications of the measurement characteristic. - setIndicationOn(MEASUREMENT_CHARACTERISTIC_UUID); + setIndicationOn(WEIGHT_SCALE_SERVICE_UUID, MEASUREMENT_CHARACTERISTIC_UUID); break; // more commands follow case 1: // Register for notifications of the command upload characteristic. @@ -132,7 +133,7 @@ public class BluetoothTrisaBodyAnalyze extends BluetoothCommunication { // This is the last init command, which causes a switch to the main state machine // immediately after. This is important because we should be in the main state // to handle pairing correctly. - setIndicationOn(UPLOAD_COMMAND_CHARACTERISTIC_UUID); + setIndicationOn(WEIGHT_SCALE_SERVICE_UUID, UPLOAD_COMMAND_CHARACTERISTIC_UUID); break; case 2: // This state is triggered by the write in onPasswordReceived() @@ -306,7 +307,7 @@ public class BluetoothTrisaBodyAnalyze extends BluetoothCommunication { private void writeCommandBytes(byte[] bytes) { Timber.d("writeCommand bytes=%s", byteInHex(bytes)); - writeBytes(DOWNLOAD_COMMAND_CHARACTERISTIC_UUID, bytes); + writeBytes(WEIGHT_SCALE_SERVICE_UUID, DOWNLOAD_COMMAND_CHARACTERISTIC_UUID, bytes); } private static String getDevicePasswordKey(String deviceId) { diff --git a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothYunmaiSE_Mini.java b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothYunmaiSE_Mini.java index 83fc83e6..13f84053 100644 --- a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothYunmaiSE_Mini.java +++ b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothYunmaiSE_Mini.java @@ -68,7 +68,7 @@ public class BluetoothYunmaiSE_Mini extends BluetoothCommunication { (byte)0x00, display_unit, (byte) 0x03, (byte) 0x00}; user_add_or_query[user_add_or_query.length - 1] = xorChecksum(user_add_or_query, 1, user_add_or_query.length - 1); - writeBytes(WEIGHT_CMD_CHARACTERISTIC, user_add_or_query); + writeBytes(WEIGHT_CMD_SERVICE, WEIGHT_CMD_CHARACTERISTIC, user_add_or_query); break; case 1: byte[] unixTime = Converters.toInt32Be(new Date().getTime() / 1000); @@ -79,14 +79,14 @@ public class BluetoothYunmaiSE_Mini extends BluetoothCommunication { set_time[set_time.length - 1] = xorChecksum(set_time, 1, set_time.length - 1); - writeBytes(WEIGHT_CMD_CHARACTERISTIC, set_time); + writeBytes(WEIGHT_CMD_SERVICE, WEIGHT_CMD_CHARACTERISTIC, set_time); break; case 2: - setNotificationOn(WEIGHT_MEASUREMENT_CHARACTERISTIC); + setNotificationOn(WEIGHT_MEASUREMENT_SERVICE, WEIGHT_MEASUREMENT_CHARACTERISTIC); break; case 3: byte[] magic_bytes = new byte[]{(byte)0x0d, (byte)0x05, (byte)0x13, (byte)0x00, (byte)0x16}; - writeBytes(WEIGHT_CMD_CHARACTERISTIC, magic_bytes); + writeBytes(WEIGHT_CMD_SERVICE, WEIGHT_CMD_CHARACTERISTIC, magic_bytes); sendMessage(R.string.info_step_on_scale, 0); break; default: diff --git a/android_app/app/src/main/java/com/health/openscale/gui/preferences/BluetoothPreferences.java b/android_app/app/src/main/java/com/health/openscale/gui/preferences/BluetoothPreferences.java index e790a888..e3382cc1 100644 --- a/android_app/app/src/main/java/com/health/openscale/gui/preferences/BluetoothPreferences.java +++ b/android_app/app/src/main/java/com/health/openscale/gui/preferences/BluetoothPreferences.java @@ -1,23 +1,25 @@ /* Copyright (C) 2014 olie.xdev -* -* This program is free software: you can redistribute it and/or modify -* it under the terms of the GNU General Public License as published by -* the Free Software Foundation, either version 3 of the License, or -* (at your option) any later version. -* -* This program is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU General Public License for more details. -* -* You should have received a copy of the GNU General Public License -* along with this program. If not, see -*/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + */ package com.health.openscale.gui.preferences; import android.app.Activity; import android.app.AlertDialog; import android.app.Fragment; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.le.ScanResult; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; @@ -26,6 +28,7 @@ import android.graphics.PorterDuff; import android.net.Uri; import android.os.Bundle; import android.os.Handler; +import android.os.Looper; import android.os.Message; import android.preference.Preference; import android.preference.PreferenceFragment; @@ -39,16 +42,13 @@ import com.health.openscale.core.bluetooth.BluetoothCommunication; import com.health.openscale.core.bluetooth.BluetoothFactory; import com.health.openscale.gui.utils.ColorUtil; import com.health.openscale.gui.utils.PermissionHelper; -import com.polidea.rxandroidble2.RxBleClient; -import com.polidea.rxandroidble2.RxBleDevice; -import com.polidea.rxandroidble2.scan.ScanResult; -import com.polidea.rxandroidble2.scan.ScanSettings; +import com.welie.blessed.BluetoothCentral; +import com.welie.blessed.BluetoothCentralCallback; +import com.welie.blessed.BluetoothPeripheral; import java.util.HashMap; import java.util.Map; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.Disposable; import timber.log.Timber; @@ -59,11 +59,10 @@ public class BluetoothPreferences extends PreferenceFragment { public static final String PREFERENCE_KEY_BLUETOOTH_HW_ADDRESS = "btHwAddress"; private PreferenceScreen btScanner; - private Map foundDevices = new HashMap<>(); + private Map foundDevices = new HashMap<>(); private Handler progressHandler; - private RxBleClient bleClient; - private Disposable scanSubscription; + private BluetoothCentral central; private static final String formatDeviceName(String name, String address) { if (name.isEmpty() || address.isEmpty()) { @@ -72,8 +71,8 @@ public class BluetoothPreferences extends PreferenceFragment { return String.format("%s [%s]", name, address); } - private static final String formatDeviceName(RxBleDevice device) { - return formatDeviceName(device.getName(), device.getMacAddress()); + private static final String formatDeviceName(BluetoothDevice device) { + return formatDeviceName(device.getName(), device.getAddress()); } private String getCurrentDeviceName() { @@ -83,19 +82,19 @@ public class BluetoothPreferences extends PreferenceFragment { prefs.getString(PREFERENCE_KEY_BLUETOOTH_HW_ADDRESS, "")); } + private final BluetoothCentralCallback bluetoothCentralCallback = new BluetoothCentralCallback() { + @Override + public void onDiscoveredPeripheral(BluetoothPeripheral peripheral, ScanResult scanResult) { + onDeviceFound(scanResult); + } + }; + private void startBluetoothDiscovery() { foundDevices.clear(); btScanner.removeAll(); - scanSubscription = bleClient.scanBleDevices( - new ScanSettings.Builder() - .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) - //.setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES) - .build() - ) - .observeOn(AndroidSchedulers.mainThread()) - .doFinally(this::stopBluetoothDiscovery) - .subscribe(this::onDeviceFound, throwable -> Toast.makeText(getActivity(), throwable.getMessage(), Toast.LENGTH_LONG).show()); + central = new BluetoothCentral(getActivity().getApplicationContext(), bluetoothCentralCallback, new Handler(Looper.getMainLooper())); + central.scanForPeripherals(); final Preference scanning = new Preference(getActivity()); scanning.setEnabled(false); @@ -161,25 +160,25 @@ public class BluetoothPreferences extends PreferenceFragment { progressHandler = null; } - scanSubscription.dispose(); + central.stopScan(); } private void onDeviceFound(final ScanResult bleScanResult) { - RxBleDevice device = bleScanResult.getBleDevice(); + BluetoothDevice device = bleScanResult.getDevice(); - if (device.getName() == null || foundDevices.containsKey(device.getMacAddress())) { + if (device.getName() == null || foundDevices.containsKey(device.getAddress())) { return; } Preference prefBtDevice = new Preference(getActivity()); - prefBtDevice.setTitle(formatDeviceName(bleScanResult.getBleDevice())); + prefBtDevice.setTitle(formatDeviceName(bleScanResult.getDevice())); BluetoothCommunication btDevice = BluetoothFactory.createDeviceDriver(getActivity(), device.getName()); if (btDevice != null) { Timber.d("Found supported device %s (driver: %s)", formatDeviceName(device), btDevice.driverName()); prefBtDevice.setOnPreferenceClickListener(new onClickListenerDeviceSelect()); - prefBtDevice.setKey(device.getMacAddress()); + prefBtDevice.setKey(device.getAddress()); prefBtDevice.setIcon(R.drawable.ic_bluetooth_device_supported); prefBtDevice.setSummary(btDevice.driverName()); @@ -205,7 +204,7 @@ public class BluetoothPreferences extends PreferenceFragment { } } - foundDevices.put(device.getMacAddress(), btDevice != null ? device : null); + foundDevices.put(device.getAddress(), btDevice != null ? device : null); btScanner.addPreference(prefBtDevice); } @@ -219,10 +218,6 @@ public class BluetoothPreferences extends PreferenceFragment { public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - OpenScale openScale = OpenScale.getInstance(); - - bleClient = openScale.getBleClient(); - addPreferencesFromResource(R.xml.bluetooth_preferences); btScanner = (PreferenceScreen) findPreference(PREFERENCE_KEY_BLUETOOTH_SCANNER); @@ -271,7 +266,7 @@ public class BluetoothPreferences extends PreferenceFragment { } } - private void getDebugInfo(final RxBleDevice device) { + private void getDebugInfo(final BluetoothDevice device) { AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); builder.setTitle("Fetching info") .setMessage("Please wait while we fetch extended info from your scale...") @@ -299,7 +294,7 @@ public class BluetoothPreferences extends PreferenceFragment { dialog.show(); - String macAddress = device.getMacAddress(); + String macAddress = device.getAddress(); stopBluetoothDiscovery(); OpenScale.getInstance().connectToBluetoothDeviceDebugMode( macAddress, btHandler); @@ -308,10 +303,10 @@ public class BluetoothPreferences extends PreferenceFragment { private class onClickListenerDeviceSelect implements Preference.OnPreferenceClickListener { @Override public boolean onPreferenceClick(final Preference preference) { - RxBleDevice device = foundDevices.get(preference.getKey()); + BluetoothDevice device = foundDevices.get(preference.getKey()); preference.getSharedPreferences().edit() - .putString(PREFERENCE_KEY_BLUETOOTH_HW_ADDRESS, device.getMacAddress()) + .putString(PREFERENCE_KEY_BLUETOOTH_HW_ADDRESS, device.getAddress()) .putString(PREFERENCE_KEY_BLUETOOTH_DEVICE_NAME, device.getName()) .apply(); diff --git a/android_app/build.gradle b/android_app/build.gradle index 28fae37e..124ef73f 100644 --- a/android_app/build.gradle +++ b/android_app/build.gradle @@ -5,7 +5,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.3.2' + classpath 'com.android.tools.build:gradle:3.4.0' } } diff --git a/android_app/gradle/wrapper/gradle-wrapper.properties b/android_app/gradle/wrapper/gradle-wrapper.properties index ea546f6b..853f8066 100644 --- a/android_app/gradle/wrapper/gradle-wrapper.properties +++ b/android_app/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Mon Jan 28 19:53:43 CET 2019 +#Sat May 25 16:04:40 CEST 2019 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip