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 43c24e57..92193bd3 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 @@ -30,10 +30,14 @@ import android.util.Log; import com.health.openscale.core.datatypes.ScaleData; import java.util.ArrayList; +import java.util.LinkedList; +import java.util.Queue; import java.util.UUID; public abstract class BluetoothCommunication { - public enum BT_STATUS_CODE {BT_RETRIEVE_SCALE_DATA, BT_INIT_PROCESS, BT_CONNECTION_ESTABLISHED, BT_CONNECTION_LOST, BT_NO_DEVICE_FOUND, BT_UNEXPECTED_ERROR }; + public enum BT_STATUS_CODE {BT_RETRIEVE_SCALE_DATA, BT_INIT_PROCESS, BT_CONNECTION_ESTABLISHED, + BT_CONNECTION_LOST, BT_NO_DEVICE_FOUND, BT_UNEXPECTED_ERROR, BT_SCALE_MESSAGE + }; public enum BT_MACHINE_STATE {BT_INIT_STATE, BT_CMD_STATE, BT_CLEANUP_STATE} protected Context context; @@ -51,7 +55,9 @@ public abstract class BluetoothCommunication { private int cleanupStepNr; private BT_MACHINE_STATE btMachineState; - private final UUID WEIGHT_MEASUREMENT_CONFIG = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"); + private Queue descriptorRequestQueue; + private Queue characteristicRequestQueue; + private Boolean openRequest; public BluetoothCommunication(Context context) { @@ -66,7 +72,7 @@ public abstract class BluetoothCommunication { * Create and return a new Bluetooth object. * * @param context In which context should the Bluetooth device created - * @param i the specidific number of which Bluetooth device should be created (correspond to "deviceTypes" key in BluetoothPreferences) + * @param i the specific number of which Bluetooth device should be created (correspond to "deviceTypes" key in BluetoothPreferences) * @return created object specified by the number i otherwise null */ public static BluetoothCommunication getBtDevice(Context context, int i) { @@ -121,6 +127,16 @@ public abstract class BluetoothCommunication { callbackBtHandler.obtainMessage(BT_STATUS_CODE.BT_RETRIEVE_SCALE_DATA.ordinal(), scaleData).sendToTarget(); } + /** + * Send message to openScale user + * + * @param msg the string id to be send + * @param value the value to be used + */ + protected void sendMessage(int msg, Object value) { + callbackBtHandler.obtainMessage(BT_STATUS_CODE.BT_SCALE_MESSAGE.ordinal(), msg, 0, value).sendToTarget(); + } + /** * Is the Bluetooth initialized process supported. * @@ -183,7 +199,7 @@ public abstract class BluetoothCommunication { /** * Return all hardware addresses of the Bluetooth device. * - * The format should be the first six hex values of a know Bluetooth hardware address without any colon e.g. 12:AB:65:12:34:52 becomes "12AB65" + * The format should be the first six hex values of a known Bluetooth hardware address without any colon e.g. 12:AB:65:12:34:52 becomes "12AB65" * @note add hw address "FFFFFF" to skip check * * @return a list of all hardware addresses that are known for this device. @@ -191,7 +207,7 @@ public abstract class BluetoothCommunication { abstract public ArrayList hwAddresses(); /** - * State machine for the initialization process for the Bluetooth device. + * State machine for the initialization process of the Bluetooth device. * * @param stateNr the current step number * @return false if no next step is available otherwise true @@ -199,7 +215,7 @@ public abstract class BluetoothCommunication { abstract boolean nextInitCmd(int stateNr); /** - * State machine for the normal/command process for the Bluetooth device. + * State machine for the normal/command process of the Bluetooth device. * * This state machine is automatically triggered if initialization process is finished. * @@ -208,6 +224,25 @@ public abstract class BluetoothCommunication { */ abstract boolean nextBluetoothCmd(int stateNr); + /** + * Set the next command number of the current state. + * + * @param nextCommand next command to select + */ + protected void setNextCmd(int nextCommand) { + switch (btMachineState) { + case BT_INIT_STATE: + initStepNr = nextCommand - 1; + break; + case BT_CMD_STATE: + cmdStepNr = nextCommand - 1; + break; + case BT_CLEANUP_STATE: + cleanupStepNr = nextCommand - 1; + break; + } + } + /** * State machine for the clean up process for the Bluetooth device. * @@ -228,7 +263,7 @@ public abstract class BluetoothCommunication { protected void onBluetoothDataRead(BluetoothGatt bluetoothGatt, BluetoothGattCharacteristic gattCharacteristic, int status){}; /** - * Method is triggered if a Bluetooth data from a device is notified or indicated. + * Method is triggered if a Bluetooth data from a device is notified or indicated. * * @param bluetoothGatt the Bluetooth Gatt * @param gattCharacteristic the Bluetooth characteristic @@ -245,7 +280,7 @@ public abstract class BluetoothCommunication { protected void setBtMachineState(BT_MACHINE_STATE btMachineState) { this.btMachineState = btMachineState; - nextMachineStateStep(); + handleRequests(); } /** @@ -260,7 +295,10 @@ public abstract class BluetoothCommunication { .getCharacteristic(characteristic); gattCharacteristic.setValue(bytes); - bluetoothGatt.writeCharacteristic(gattCharacteristic); + synchronized (openRequest) { + characteristicRequestQueue.add(gattCharacteristic); + handleRequests(); + } } /** @@ -284,16 +322,18 @@ public abstract class BluetoothCommunication { * @param service the Bluetooth UUID device service * @param characteristic the Bluetooth UUID characteristic */ - protected void setInidicationOn(UUID service, UUID characteristic) { + protected void setIndicationOn(UUID service, UUID characteristic, UUID descriptor) { BluetoothGattCharacteristic gattCharacteristic = bluetoothGatt.getService(service) .getCharacteristic(characteristic); bluetoothGatt.setCharacteristicNotification(gattCharacteristic, true); - BluetoothGattDescriptor gattDescriptor = gattCharacteristic.getDescriptor(WEIGHT_MEASUREMENT_CONFIG); + BluetoothGattDescriptor gattDescriptor = gattCharacteristic.getDescriptor(descriptor); gattDescriptor.setValue(BluetoothGattDescriptor.ENABLE_INDICATION_VALUE); - - bluetoothGatt.writeDescriptor(gattDescriptor); + synchronized (openRequest) { + descriptorRequestQueue.add(gattDescriptor); + handleRequests(); + } } /** @@ -302,15 +342,18 @@ public abstract class BluetoothCommunication { * @param service the Bluetooth UUID device service * @param characteristic the Bluetooth UUID characteristic */ - protected void setNotificationOn(UUID service, UUID characteristic) { + protected void setNotificationOn(UUID service, UUID characteristic, UUID descriptor) { BluetoothGattCharacteristic gattCharacteristic = bluetoothGatt.getService(service) .getCharacteristic(characteristic); bluetoothGatt.setCharacteristicNotification(gattCharacteristic, true); - BluetoothGattDescriptor gattDescriptor = gattCharacteristic.getDescriptor(WEIGHT_MEASUREMENT_CONFIG); + BluetoothGattDescriptor gattDescriptor = gattCharacteristic.getDescriptor(descriptor); gattDescriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); - bluetoothGatt.writeDescriptor(gattDescriptor); + synchronized (openRequest) { + descriptorRequestQueue.add(gattDescriptor); + handleRequests(); + } } /** @@ -319,15 +362,18 @@ public abstract class BluetoothCommunication { * @param service the Bluetooth UUID device service * @param characteristic the Bluetooth UUID characteristic */ - protected void setNotificationOff(UUID service, UUID characteristic) { + protected void setNotificationOff(UUID service, UUID characteristic, UUID descriptor) { BluetoothGattCharacteristic gattCharacteristic = bluetoothGatt.getService(service) .getCharacteristic(characteristic); bluetoothGatt.setCharacteristicNotification(gattCharacteristic, false); - BluetoothGattDescriptor gattDescriptor = gattCharacteristic.getDescriptor(WEIGHT_MEASUREMENT_CONFIG); + BluetoothGattDescriptor gattDescriptor = gattCharacteristic.getDescriptor(descriptor); gattDescriptor.setValue(BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE); - bluetoothGatt.writeDescriptor(gattDescriptor); + synchronized (openRequest) { + descriptorRequestQueue.add(gattDescriptor); + handleRequests(); + } } /** @@ -459,6 +505,35 @@ public abstract class BluetoothCommunication { } } + private void handleRequests() { + synchronized (openRequest) { + // check for pending request + if (openRequest) + return; // yes, do nothing + + // handle descriptor requests first + BluetoothGattDescriptor descriptorRequest = descriptorRequestQueue.poll(); + if (descriptorRequest != null) { + if (!bluetoothGatt.writeDescriptor(descriptorRequest)) + Log.d("BTC", "Descriptor Write failed(" + byteInHex(descriptorRequest.getValue()) + ")"); + openRequest = true; + return; + } + + // handle characteristics requests second + BluetoothGattCharacteristic characteristicRequest = characteristicRequestQueue.poll(); + if (characteristicRequest != null) { + if (!bluetoothGatt.writeCharacteristic(characteristicRequest)) + Log.d("BTC", "Characteristic Write failed(" + byteInHex(characteristicRequest.getValue()) + ")"); + openRequest = true; + return; + } + + // After every command was executed, continue with the next step + nextMachineStateStep(); + } + } + /** * Custom Gatt callback class to set up a Bluetooth state machine. */ @@ -480,6 +555,12 @@ public abstract class BluetoothCommunication { initStepNr = 0; cleanupStepNr = 0; + // Clear from possible previous setups + characteristicRequestQueue = new LinkedList<>(); + descriptorRequestQueue = new LinkedList<>(); + openRequest = false; + + btMachineState = BT_MACHINE_STATE.BT_INIT_STATE; nextMachineStateStep(); } @@ -488,14 +569,20 @@ public abstract class BluetoothCommunication { public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { - nextMachineStateStep(); + synchronized (openRequest) { + openRequest = false; + handleRequests(); + } } @Override public void onCharacteristicWrite (BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { - nextMachineStateStep(); + synchronized (openRequest) { + openRequest = false; + handleRequests(); + } } @Override @@ -503,8 +590,10 @@ public abstract class BluetoothCommunication { BluetoothGattCharacteristic characteristic, int status) { onBluetoothDataRead(gatt, characteristic, status); - - nextMachineStateStep(); + synchronized (openRequest) { + openRequest = false; + handleRequests(); + } } @Override diff --git a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothMedisanaBS444.java b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothMedisanaBS444.java index 33e0f1ce..f39b0e1c 100644 --- a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothMedisanaBS444.java +++ b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothMedisanaBS444.java @@ -63,6 +63,7 @@ public class BluetoothMedisanaBS444 extends BluetoothCommunication { hwAddresses.add("F13A88"); hwAddresses.add("C9A68A"); hwAddresses.add("D60211"); + hwAddresses.add("DB2FF6"); return hwAddresses; } @@ -81,15 +82,15 @@ public class BluetoothMedisanaBS444 extends BluetoothCommunication { switch (stateNr) { case 0: // set indication on for feature characteristic - setInidicationOn(WEIGHT_MEASUREMENT_SERVICE, FEATURE_MEASUREMENT_CHARACTERISTIC); + setIndicationOn(WEIGHT_MEASUREMENT_SERVICE, FEATURE_MEASUREMENT_CHARACTERISTIC, WEIGHT_MEASUREMENT_CONFIG); break; case 1: // set indication on for weight measurement - setInidicationOn(WEIGHT_MEASUREMENT_SERVICE, WEIGHT_MEASUREMENT_CHARACTERISTIC); + setIndicationOn(WEIGHT_MEASUREMENT_SERVICE, WEIGHT_MEASUREMENT_CHARACTERISTIC, WEIGHT_MEASUREMENT_CONFIG); break; case 2: // set indication on for custom5 measurement - setInidicationOn(WEIGHT_MEASUREMENT_SERVICE, CUSTOM5_MEASUREMENT_CHARACTERISTIC); + setIndicationOn(WEIGHT_MEASUREMENT_SERVICE, CUSTOM5_MEASUREMENT_CHARACTERISTIC, WEIGHT_MEASUREMENT_CONFIG); break; case 3: // send magic number to receive weight data 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 bb673eda..3dfec904 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 @@ -133,7 +133,7 @@ public class BluetoothMiScale extends BluetoothCommunication { break; case 2: // set notification on for weight measurement history - setNotificationOn(WEIGHT_MEASUREMENT_SERVICE, WEIGHT_MEASUREMENT_HISTORY_CHARACTERISTIC); + setNotificationOn(WEIGHT_MEASUREMENT_SERVICE, WEIGHT_MEASUREMENT_HISTORY_CHARACTERISTIC, WEIGHT_MEASUREMENT_CONFIG); break; case 3: // Set on history weight measurement @@ -153,11 +153,11 @@ public class BluetoothMiScale extends BluetoothCommunication { switch (stateNr) { case 0: // set notification on for weight measurement - setNotificationOn(WEIGHT_MEASUREMENT_SERVICE, WEIGHT_MEASUREMENT_CHARACTERISTIC); + setNotificationOn(WEIGHT_MEASUREMENT_SERVICE, WEIGHT_MEASUREMENT_CHARACTERISTIC, WEIGHT_MEASUREMENT_CONFIG); break; case 1: // set notification on for weight measurement history - setNotificationOn(WEIGHT_MEASUREMENT_SERVICE, WEIGHT_MEASUREMENT_HISTORY_CHARACTERISTIC); + setNotificationOn(WEIGHT_MEASUREMENT_SERVICE, WEIGHT_MEASUREMENT_HISTORY_CHARACTERISTIC, WEIGHT_MEASUREMENT_CONFIG); break; case 2: // configure scale to get only last measurements @@ -168,11 +168,11 @@ public class BluetoothMiScale extends BluetoothCommunication { break; case 3: // set notification off for weight measurement history - setNotificationOff(WEIGHT_MEASUREMENT_SERVICE, WEIGHT_MEASUREMENT_HISTORY_CHARACTERISTIC); + setNotificationOff(WEIGHT_MEASUREMENT_SERVICE, WEIGHT_MEASUREMENT_HISTORY_CHARACTERISTIC, WEIGHT_MEASUREMENT_CONFIG); break; case 4: // set notification on for weight measurement history - setNotificationOn(WEIGHT_MEASUREMENT_SERVICE, WEIGHT_MEASUREMENT_HISTORY_CHARACTERISTIC); + setNotificationOn(WEIGHT_MEASUREMENT_SERVICE, WEIGHT_MEASUREMENT_HISTORY_CHARACTERISTIC, WEIGHT_MEASUREMENT_CONFIG); break; case 5: // invoke receiving history data diff --git a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothSanitasSbf70.java b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothSanitasSbf70.java index e3975ded..5d782e58 100644 --- a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothSanitasSbf70.java +++ b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothSanitasSbf70.java @@ -1,5 +1,6 @@ /* Copyright (C) 2014 olie.xdev * 2017 jflesch +* 2017 Martin Nowack * * 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 @@ -17,24 +18,26 @@ package com.health.openscale.core.bluetooth; import android.bluetooth.BluetoothGatt; -import android.bluetooth.BluetoothGattCallback; import android.bluetooth.BluetoothGattCharacteristic; -import android.bluetooth.BluetoothGattDescriptor; -import android.bluetooth.BluetoothProfile; import android.content.Context; import android.util.Log; +import com.health.openscale.R; +import com.health.openscale.core.OpenScale; import com.health.openscale.core.datatypes.ScaleData; +import com.health.openscale.core.datatypes.ScaleUser; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.text.ParseException; import java.util.ArrayList; -import java.util.LinkedList; -import java.util.Queue; +import java.util.Arrays; +import java.text.SimpleDateFormat; +import java.io.ByteArrayOutputStream; +import java.util.Collections; +import java.util.TreeSet; import java.util.UUID; -import static com.health.openscale.core.bluetooth.BluetoothCommunication.BT_STATUS_CODE.BT_CONNECTION_ESTABLISHED; -import static com.health.openscale.core.bluetooth.BluetoothCommunication.BT_STATUS_CODE.BT_CONNECTION_LOST; -import static com.health.openscale.core.bluetooth.BluetoothCommunication.BT_STATUS_CODE.BT_UNEXPECTED_ERROR; - public class BluetoothSanitasSbf70 extends BluetoothCommunication { public final static String TAG = "BluetoothSanitasSbf70"; @@ -74,7 +77,7 @@ public class BluetoothSanitasSbf70 extends BluetoothCommunication { UUID.fromString("00002A05-0000-1000-8000-00805F9B34FB"); // descriptor ; handle = 0x000f private static final UUID CLIENT_CHARACTERISTICS_CONFIGURATION = - UUID.fromString("00002902-0000-1000-8000-00805F9B34FB"); + UUID.fromString("00002901-0000-1000-8000-00805F9B34FB"); private static final UUID CUSTOM_SERVICE_1 = UUID.fromString("0000FFE0-0000-1000-8000-00805F9B34FB"); @@ -96,9 +99,15 @@ public class BluetoothSanitasSbf70 extends BluetoothCommunication { private static final UUID CUSTOM_CHARACTERISTIC_IMG_BLOCK = // write-only, notify UUID.fromString("F000FFC2-0451-4000-8000-000000000000"); + + private int currentScaleUserId; + private int countRegisteredScaleUsers; + private TreeSet seenUsers; + private int maxRegisteredScaleUser; + private ByteArrayOutputStream receivedScaleData; + public BluetoothSanitasSbf70(Context context) { super(context); - gattCallback = new BluetoothSanitasGattCallback(); } @Override @@ -119,264 +128,481 @@ public class BluetoothSanitasSbf70 extends BluetoothCommunication { return hwAddresses; } - @Override - boolean nextInitCmd(int stateNr) { - return false; - } - - @Override - boolean nextBluetoothCmd(int stateNr) { - return false; - } - - @Override - boolean nextCleanUpCmd(int stateNr) { - return false; - } - @Override public boolean initSupported() { - return false; + return true; } @Override public boolean historySupported() { - return false; + return true; } - private class BluetoothSanitasGattCallback extends BluetoothGattCallback { - /** - * @brief used to collect the data - */ - private ScaleData scaleBtData; + @Override + boolean nextInitCmd(int stateNr) { - /** - * @brief message to send. - * Messages are sent by writing on a specific characteristic - */ - private Queue msgQueue; + switch (stateNr) { + case 0: + // Initialize data + currentScaleUserId = -1; + countRegisteredScaleUsers = -1; + maxRegisteredScaleUser = -1; + seenUsers = new TreeSet<>(); - /** - * @brief true if the next message can be sent immediately. False if another is already - * being sent - */ - private boolean canSend; - - /** - * @brief true if the communication must be closed after all the message have been sent - */ - private boolean eof; - - public BluetoothSanitasGattCallback() { - super(); - scaleBtData = new ScaleData(); - scaleBtData.setId(-1); - msgQueue = new LinkedList<>(); - canSend = true; - eof = false; - } - - @Override - public void onConnectionStateChange(final BluetoothGatt gatt, int status, int newState) { - Log.d(TAG, "onConnectionStatechange(" + status + ", " + newState + ")"); - if (newState == BluetoothProfile.STATE_CONNECTED) { - Log.d(TAG, "Connection established"); - setBtStatus(BT_CONNECTION_ESTABLISHED); - gatt.discoverServices(); - } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { - Log.d(TAG, "Connection lost"); - setBtStatus(BT_CONNECTION_LOST); - stopSearching(); - } - } - - @Override - public void onServicesDiscovered(final BluetoothGatt gatt, int status) { - Log.d(TAG, "onServicesDiscovered(" + status + ")"); - //invokeNextBluetoothCmd(gatt); - if (status == gatt.GATT_SUCCESS) { - init(gatt); - } - } - - /** - * @brief configure the scale - */ - private void init(final BluetoothGatt gatt) { - BluetoothGattCharacteristic characteristic; - BluetoothGattDescriptor descriptor; - - characteristic = gatt.getService(CUSTOM_SERVICE_1) - .getCharacteristic(CUSTOM_CHARACTERISTIC_WEIGHT); - gatt.setCharacteristicNotification(characteristic, true); - - descriptor = characteristic.getDescriptor(CLIENT_CHARACTERISTICS_CONFIGURATION); - descriptor.setValue(new byte[] { - (byte)0x01, (byte)0x00, - }); - gatt.writeDescriptor(descriptor); - - msgQueue.add(new byte[] { - (byte)0xe6, (byte)0x01, - }); - canSend = false; - } - - /** - * @brief send the next message in the queue - */ - private void nextMessage(final BluetoothGatt gatt) { - if (!canSend) { - return; - } - - byte[] msg = msgQueue.poll(); - if (msg == null) { - if (eof) { - stopSearching(); - } - return; - } - - canSend = false; - - BluetoothGattCharacteristic characteristic; - characteristic = gatt.getService(CUSTOM_SERVICE_1) - .getCharacteristic(CUSTOM_CHARACTERISTIC_WEIGHT); - characteristic.setValue(msg); - gatt.writeCharacteristic(characteristic); - } - - @Override - public void onDescriptorWrite(BluetoothGatt gatt, - BluetoothGattDescriptor descriptor, - int status) { - Log.d(TAG, "onDescriptorWrite(" + descriptor + ", " + status + ")"); - canSend = true; - nextMessage(gatt); - } - - @Override - public void onCharacteristicWrite (BluetoothGatt gatt, - BluetoothGattCharacteristic characteristic, - int status) { - Log.d(TAG, "onCharacteristicWrite(" + characteristic + ", " + status + ")"); - canSend = true; - nextMessage(gatt); - } - - @Override - public void onCharacteristicRead (BluetoothGatt gatt, - BluetoothGattCharacteristic characteristic, - int status) { - Log.d(TAG, "onCharacteristicRead(" + characteristic + ", " + status + ")"); - } - - @Override - public void onCharacteristicChanged(BluetoothGatt gatt, - BluetoothGattCharacteristic characteristic) { - final UUID uuid = characteristic.getUuid(); - final byte[] data = characteristic.getValue(); - - Log.d(TAG, "onCharacteristicChanged(" + uuid + "): " + byteInHex(data)); - - if (!uuid.equals(CUSTOM_CHARACTERISTIC_WEIGHT)) { - Log.d(TAG, "Got characteristic changed from unexpected UUID ?"); - } - - if ((data[0] & 0xFF) == 0xe6 && (data[1] & 0xFF) == 0x00) { - Log.d(TAG, "ACK"); - msgQueue.add(new byte[] { - (byte)0xe9, (byte)0x59, (byte)0x07, (byte)0x84, (byte)0x4c, - }); - nextMessage(gatt); - return; - - } else if ((data[0] & 0xFF) == 0xe7 && (data[1] & 0xFF) == 0x58) { - Log.d(TAG, "ACK"); - msgQueue.add(new byte[] { - (byte)0xe7, (byte)0xf1, (byte)(data[1] & 0xFF), - (byte)(data[2] & 0xFF), (byte)(data[3] & 0xFF), - }); - nextMessage(gatt); - - // weight - if ((data[2] & 0xFF) != 0x00) { - // temporary value; - return; - } - // stabilized value - // little endian - float weight = ((float)( - ((int)(data[3] & 0xFF) << 8) + ((int)(data[4] & 0xFF)) - )) * 50.0f / 1000.0f; // unit is 50g - Log.i(TAG, "Got weight: " + weight); - scaleBtData.setWeight(weight); - return; - - } else if ((data[0] & 0xFF) == 0xe7 && (data[1] & 0xFF) == 0x59) { - Log.d(TAG, "ACK Extra data " + ((int)data[3])); - msgQueue.add(new byte[] { - (byte)0xe7, (byte)0xf1, - (byte)(data[1] & 0xFF), (byte)(data[2] & 0xFF), - (byte)(data[3] & 0xFF), - }); - - if ((data[2] & 0xFF) == 0x03 && (data[3] & 0xFF) == 0x02) { - // big endian - float fat = ((float)( - ((int)(data[12] & 0xFF) << 8) + ((int)(data[13] & 0xFF)) - )) / 10.0f; // unit is 0.1kg - Log.i(TAG, "Got fat: " + fat + "%"); - scaleBtData.setFat(fat); + // Setup notification + setNotificationOn(CUSTOM_SERVICE_1, CUSTOM_CHARACTERISTIC_WEIGHT, CLIENT_CHARACTERISTICS_CONFIGURATION); + break; + case 1: + // Say "Hello" to the scale + writeBytes(new byte[]{(byte) 0xe6, (byte) 0x01}); + break; + case 2: + // Update timestamp of the scale + updateDateTimeSanitas(); + break; + case 3: + // Request general user information + writeBytes(new byte[]{(byte) 0xe7, (byte) 0x33}); + break; + case 4: + // Wait for ack of all users + if (seenUsers.size() < countRegisteredScaleUsers || (countRegisteredScaleUsers == -1)) { + // Request this state again + setNextCmd(stateNr); + break; } - if ((data[2] & 0xFF) == 0x03 && (data[3] & 0xFF) == 0x03) { - // little endian - float water = ((float)( - ((int)(data[5] & 0xFF) << 8) + ((int)(data[4] & 0xFF)) - )) / 10.0f; // unit is 0.1kg - Log.i(TAG, "Got water: " + water + "%"); - scaleBtData.setWater(water); + // Got all user acks - // little endian - float muscle = ((float)( - ((int)(data[7] & 0xFF) << 8) + ((int)(data[6] & 0xFF)) - )) / 10.0f; // unit is 0.1kg - Log.i(TAG, "Got muscle: " + muscle + "%"); - scaleBtData.setMuscle(muscle); + // Check if not found/unknown + if (currentScaleUserId == 0) { + // Unknown user, request creation of new user + if (countRegisteredScaleUsers == maxRegisteredScaleUser) { + setBtMachineState(BT_MACHINE_STATE.BT_CLEANUP_STATE); + Log.d(TAG, "Cannot create additional scale user"); + sendMessage(R.string.error_max_scale_users, 0); + break; + } - addScaleData(scaleBtData); + // Request creation of user + final ScaleUser selectedUser = OpenScale.getInstance(context).getSelectedScaleUser(); - Log.d(TAG, "ACK Extra data (end)"); - msgQueue.add(new byte[] { - (byte)0xe7, (byte)0x43, (byte)0x0, (byte)0x0, (byte)0x0, - (byte)0x0, (byte)0x0, (byte)0x0, (byte)0x0, - (byte)0x65 + // We can only use up to 3 characters and have to handle them uppercase + int maxIdx = selectedUser.user_name.length() >= 3 ? 3 : selectedUser.user_name.length(); + byte[] nick = selectedUser.user_name.toUpperCase().substring(0, maxIdx).getBytes(); + + byte activity = 2; // activity level: 1 - 5 + Log.d(TAG, "Create User:" + selectedUser.user_name); + + writeBytes(new byte[]{ + (byte) 0xe7, (byte) 0x31, (byte) 0x0, (byte) 0x0, (byte) 0x0, + (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) 0x0, + (byte) (seenUsers.size() > 0 ? Collections.max(seenUsers) + 1 : 101), + nick[0], nick[1], nick[2], + (byte) selectedUser.birthday.getYear(), + (byte) selectedUser.birthday.getMonth(), + (byte) selectedUser.birthday.getDate(), + (byte) selectedUser.body_height, + (byte) (((1 - selectedUser.gender) << 7) | activity) + }); + } else { + // Get existing user information + Log.d(TAG, "Request getUserInfo " + currentScaleUserId); + writeBytes(new byte[]{ + (byte) 0xe7, (byte) 0x36, (byte) 0x0, (byte) 0x0, (byte) 0x0, + (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) currentScaleUserId }); - scaleBtData = new ScaleData(); - scaleBtData.setId(-1); } + Log.d(TAG, "scaleuserid:" + currentScaleUserId + " registered users: " + countRegisteredScaleUsers + + " extracted users: " + seenUsers.size()); - nextMessage(gatt); - return; - } else if ((data[0] & 0xFF) == 0xe7 && (data[1] & 0xFF) == 0xf0) { - Log.d(TAG, "ACK"); - msgQueue.add(new byte[]{ - (byte) 0xea, (byte) 0x02, + break; + case 5: + break; + default: + // Finish init if everything is done + return false; + } + + + return true; + } + + @Override + boolean nextBluetoothCmd(int stateNr) { + + switch (stateNr) { + case 0: + // If no specific user selected + if (currentScaleUserId == 0) + break; + + Log.d(TAG, "Request Saved User Measurements"); + writeBytes(new byte[]{ + (byte) 0xe7, (byte) 0x41, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, (byte) currentScaleUserId }); - nextMessage(gatt); - eof = true; - return; - } else { - Log.w(TAG, "Unidentified notification !"); + break; + case 1: + // Wait for user measurements to be received + setNextCmd(stateNr); + break; + case 2: + setBtMachineState(BT_MACHINE_STATE.BT_CLEANUP_STATE); + break; + default: + return false; - setBtStatus(BT_UNEXPECTED_ERROR, "Error while decoding bluetooth value"); + } + return true; + } + + @Override + boolean nextCleanUpCmd(int stateNr) { + switch (stateNr) { + case 0: + // Force disconnect + writeBytes(new byte[]{(byte) 0xea, (byte) 0x02}); + break; + default: + return false; + } + return true; + } + + + @Override + public void onBluetoothDataChange(BluetoothGatt bluetoothGatt, BluetoothGattCharacteristic gattCharacteristic) { + byte[] data = gattCharacteristic.getValue(); + if (data.length == 0) + return; + + if ((data[0] & 0xFF) == 0xe6 && (data[1] & 0xFF) == 0x00) { + Log.d(TAG, "ACK Scale is ready"); + return; + } + + if ((data[0] & 0xFF) == 0xe7 && (data[1] & 0xFF) == 0xf0 && data[2] == 0x33) { + Log.d(TAG, "ACK Got general user information"); + + int count = (byte) (data[4] & 0xFF); + int maxUsers = (byte) (data[5] & 0xFF); + Log.d(TAG, "Count:" + count + " maxUsers:" + maxUsers); + + countRegisteredScaleUsers = count; + // Check if any scale user is registered + if (count == 0) + currentScaleUserId = 0; // Unknown user + maxRegisteredScaleUser = maxUsers; + + nextMachineStateStep(); + return; + } + + if ((data[0] & 0xFF) == 0xe7 && (data[1] & 0xFF) == 0x34) { + Log.d(TAG, "Ack Get UUIDSs List of Users"); + + byte currentUserMax = (byte) (data[2] & 0xFF); + byte currentUserID = (byte) (data[3] & 0xFF); + byte userUuid = (byte) (data[11] & 0xFF); + String name = new String(data, 12, 3); + int year = (byte) (data[15] & 0xFF); + + final ScaleUser selectedUser = OpenScale.getInstance(context).getSelectedScaleUser(); + + // Check if we found the currently selected user + if (selectedUser.user_name.toLowerCase().startsWith(name.toLowerCase()) && + selectedUser.birthday.getYear() == year) { + // Found user + currentScaleUserId = userUuid; + } + + // Remember this uuid from the scale + if (seenUsers.add((int) userUuid)) { + if (currentScaleUserId == -1 && seenUsers.size() == countRegisteredScaleUsers) { + // We have seen all users: user is unknown + currentScaleUserId = 0; + } + Log.d(TAG, "Send ack gotUser"); + writeBytes(new byte[]{ + (byte) 0xe7, (byte) 0xf1, (byte) 0x34, currentUserMax, + currentUserID + }); + } + + return; + } + + if ((data[0] & 0xFF) == 0xe7 && (data[1] & 0xFF) == 0xF0 && (data[2] & 0xFF) == 0x36) { + Log.d(TAG, "Ack Get User Info Initials"); + String name = new String(data, 4, 3); + byte year = (byte) (data[7] & 0xFF); + byte month = (byte) (data[8] & 0xFF); + byte day = (byte) (data[9] & 0xFF); + + int height = (data[10] & 0xFF); + boolean male = (data[11] & 0xF0) != 0; + byte activity = (byte) (data[11] & 0x0F); + + + Log.d(TAG, "Name " + name + " YY-MM-DD: " + year + " " + month + " " + day + + "Height: " + height + " Sex:" + (male ? "M" : "F") + "activity: " + activity); + + // Get scale status for user + writeBytes(new byte[]{ + (byte) 0xe7, (byte) 0x4f, (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) 0x0, + (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) currentScaleUserId + }); + + + return; + } + + if ((data[0] & 0xFF) == 0xe7 && (data[1] & 0xFF) == 0xf0 && (data[2] & 0xFF) == 0x4F) { + Log.d(TAG, "Ack Get scale status"); + + int unknown = data[3]; + int batteryLevel = (data[4] & 0xFF); + float weightThreshold = (data[5] & 0xFF) / 10f; + float bodyFatThreshold = (data[6] & 0xFF) / 10f; + int unit = data[7]; // 1 kg, 2 lb (pounds), 3 st stone + boolean userExists = (data[8] == 0); + boolean userReferWeightExists = (data[9] == 0); + boolean userMeasurementExist = (data[10] == 0); + int scaleVersion = data[11]; + + Log.d(TAG, "BatteryLevel:" + batteryLevel + " weightThreshold: " + weightThreshold + + " BodyFatThresh: " + bodyFatThreshold + " Unit: " + unit + " userExists: " + userExists + + " UserReference Weight Exists:" + userReferWeightExists + " UserMeasurementExists " + userMeasurementExist + + " scaleVersion" + scaleVersion); + return; + } + + if ((data[0] & 0xFF) == 0xe7 && (data[1] & 0xFF) == 0xf0 && data[2] == 0x31) { + Log.d(TAG, "Acknowledge creation of user"); + + // Indicate user to step on scale + sendMessage(R.string.info_step_on_scale, 0); + + // Request basement measurement + writeBytes(new byte[]{ + (byte) 0xe7, 0x40, 0, 0, 0, 0, 0, 0, 0, + (byte) (seenUsers.size() > 0 ? Collections.max(seenUsers) + 1 : 101) + }); + + return; + + } + + + if ((data[0] & 0xFF) == 0xe7 && (data[1] & 0xFF) == 0xf0 && (data[2] & 0xFF) == 0x41) { + Log.d(TAG, "Will start to receive measurements User Specific"); + + byte nr_measurements = data[3]; + + Log.d(TAG, "New measurements: " + nr_measurements / 2); + return; + } + + if ((data[0] & 0xFF) == 0xe7 && (data[1] & 0xFF) == 0x42) { + Log.d(TAG, "Specific measurement User specific"); + + // Measurements are split into two parts + + int max_items = data[2] & 0xFF; + int current_item = data[3] & 0xFF; + + // Received even part + if (current_item % 2 == 1) { + receivedScaleData = new ByteArrayOutputStream(); + } + + try { + receivedScaleData.write(Arrays.copyOfRange(data, 4, data.length)); + } catch (IOException e) { + e.printStackTrace(); + } + + // Send acknowledgement + writeBytes(new byte[]{ + (byte) 0xe7, (byte) 0xf1, (byte) 0x42, (byte) (data[2] & 0xFF), + (byte) (data[3] & 0xFF) + }); + + if (current_item % 2 == 0) { + try { + ScaleData parsedData = parseScaleData(receivedScaleData.toByteArray()); + addScaleData(parsedData); + } catch (ParseException e) { + Log.d(TAG, "Could not parse byte array: " + byteInHex(receivedScaleData.toByteArray())); + e.printStackTrace(); + } + } + + if (current_item == max_items) { + // finish and delete + deleteScaleData(); + } + return; + } + + if ((data[0] & 0xFF) == 0xe7 && (data[1] & 0xFF) == 0x58) { + Log.d(TAG, "Active measurement"); + if ((data[2] & 0xFF) != 0x00) { + // little endian + float weight = ((float) ( + ((data[3] & 0xFF) << 8) + (data[4] & 0xFF) + )) * 50.0f / 1000.0f; // unit is 50g + + // temporary value; + sendMessage(R.string.info_measuring, weight); return; } + + // stabilized value + // little endian + float weight = ((float) ( + ((data[3] & 0xFF) << 8) + (data[4] & 0xFF) + )) * 50.0f / 1000.0f; // unit is 50g + + Log.i(TAG, "Got weight: " + weight); + + + writeBytes(new byte[]{ + (byte) 0xe7, (byte) 0xf1, (byte) (data[1] & 0xFF), + (byte) (data[2] & 0xFF), (byte) (data[3] & 0xFF), + }); + + return; + } + + if ((data[0] & 0xFF) == 0xe7 && (data[1] & 0xFF) == 0x59) { + // Get stable measurement results + Log.d(TAG, "Get measurement data " + ((int) data[3])); + + int max_items = (data[2] & 0xFF); + int current_item = (data[3] & 0xFF); + + // Received first part + if (current_item == 1) { + receivedScaleData = new ByteArrayOutputStream(); + } else { + try { + receivedScaleData.write(Arrays.copyOfRange(data, 4, data.length)); + } catch (IOException e) { + e.printStackTrace(); + } + } + + // Send ack that we got the data + writeBytes(new byte[]{ + (byte) 0xe7, (byte) 0xf1, + (byte) (data[1] & 0xFF), (byte) (data[2] & 0xFF), + (byte) (data[3] & 0xFF), + }); + + if (current_item == max_items) { + // received all parts + ScaleData parsedData = null; + try { + parsedData = parseScaleData(receivedScaleData.toByteArray()); + addScaleData(parsedData); + // Delete data + deleteScaleData(); + } catch (ParseException e) { + Log.d(TAG, "Parse Exception " + byteInHex(receivedScaleData.toByteArray())); + } + } + + return; + } + + if ((data[0] & 0xFF) == 0xe7 && (data[1] & 0xFF) == 0xf0 && (data[2] & 0xFF) == 0x43) { + Log.d(TAG, "Acknowledge: Data deleted."); + return; + } + + Log.d(TAG, "DataChanged - not handled: " + byteInHex(data)); + } + + private void deleteScaleData() { + writeBytes(new byte[]{ + (byte) 0xe7, (byte) 0x43, (byte) 0x0, (byte) 0x0, (byte) 0x0, + (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) 0x0, + (byte) currentScaleUserId + }); + } + + private ScaleData parseScaleData(byte[] data) throws ParseException { + if (data.length != 11 + 11) + throw new ParseException("Parse scala data: unexpected length", 0); + + ScaleData receivedMeasurement = new ScaleData(); + + // Parse timestamp + long timestamp = ByteBuffer.wrap(data, 0, 4).getInt() * 1000L; + SimpleDateFormat sdf = new SimpleDateFormat("MMMM d, yyyy 'at' h:mm a"); + String date = sdf.format(timestamp); + + // little endian + float weight = ((float) ( + ((data[4] & 0xFF) << 8) + (data[5] & 0xFF) + )) * 50.0f / 1000.0f; // unit is 50g + receivedMeasurement.setWeight(weight); + + // Parse impedance level + int impedance = ((data[6] & 0xFF) << 8) + (data[7] & 0xFF); + + // Parse fat + float fat = ((float) ( + ((data[8] & 0xFF) << 8) + (data[9] & 0xFF) + )) / 10.0f; // unit is 0.1% + receivedMeasurement.setFat(fat); + + float water = ((float) ( + ((data[10] & 0xFF) << 8) + (data[11] & 0xFF) + )) / 10.0f; // unit is 0.1% + receivedMeasurement.setWater(water); + + float muscle = ((float) ( + ((data[12] & 0xFF) << 8) + (data[13] & 0xFF) + )) / 10.0f; // unit is 0.1% + receivedMeasurement.setMuscle(muscle); + + float boneMass = ((float) ( + ((data[14] & 0xFF) << 8) + (data[15] & 0xFF) + )) * 50.0f / 1000.0f; // unit is 50g + + // basal metabolic rate + float bmr = ((float) ( + ((data[16] & 0xFF) << 8) + (data[17] & 0xFF) + )) / 10.0f; + + // active metabolic rate + int amr = ((data[18] & 0xFF) << 8) + (data[19] & 0xFF); + + float bmi = ((data[20] & 0xFF) << 8) + (data[21] & 0xFF); + + Log.i(TAG, "Measurement: " + date + " Impedance: " + impedance + " Weight:" + weight + + " Fat: " + fat + " Water: " + water + " Muscle: " + muscle + + " BoneMass: " + boneMass + " BMR: " + bmr + " AMR: " + amr + " BMI: " + bmi); + + return receivedMeasurement; + } + + private void updateDateTimeSanitas() { + // Update date/time of the scale + long unixTime = System.currentTimeMillis() / 1000L; + byte[] unixTimeBytes = ByteBuffer.allocate(Long.SIZE / 8).putLong(unixTime).array(); + Log.d(TAG, "Write new Date/Time:" + unixTime + " " + byteInHex(unixTimeBytes)); + + writeBytes(new byte[]{(byte) 0xe9, unixTimeBytes[4], unixTimeBytes[5], unixTimeBytes[6], unixTimeBytes[7]}); + } + + private void writeBytes(byte[] data) { + writeBytes(CUSTOM_SERVICE_1, CUSTOM_CHARACTERISTIC_WEIGHT, data); } } diff --git a/android_app/app/src/main/java/com/health/openscale/gui/MainActivity.java b/android_app/app/src/main/java/com/health/openscale/gui/MainActivity.java index 37c0f0a0..de79f1cd 100644 --- a/android_app/app/src/main/java/com/health/openscale/gui/MainActivity.java +++ b/android_app/app/src/main/java/com/health/openscale/gui/MainActivity.java @@ -244,6 +244,10 @@ public class MainActivity extends ActionBarActivity implements Toast.makeText(getApplicationContext(), getResources().getString(R.string.info_bluetooth_connection_error) + ": " + msg.obj, Toast.LENGTH_SHORT).show(); Log.e("OpenScale", "Bluetooth unexpected error: " + msg.obj); break; + case BT_SCALE_MESSAGE: + String toastMessage = String.format(getResources().getString(msg.arg1), msg.obj); + Toast.makeText(getApplicationContext(), toastMessage, Toast.LENGTH_LONG).show(); + break; } } }; diff --git a/android_app/app/src/main/res/values/strings.xml b/android_app/app/src/main/res/values/strings.xml index 4d63f1df..c6172538 100644 --- a/android_app/app/src/main/res/values/strings.xml +++ b/android_app/app/src/main/res/values/strings.xml @@ -150,4 +150,9 @@ Ignore data that are out of range Initial weight Calculate average per day/month + + Maximum number of concurrent scale users reached. + Please step barefoot on the scale for reference measurements. + Measuring weight: %.2f + diff --git a/openScale-dev-build.apk b/openScale-dev-build.apk index 026e3f22..159c712a 100644 Binary files a/openScale-dev-build.apk and b/openScale-dev-build.apk differ