From e158a4084a6f675422da048c1e5dc00643b7ec16 Mon Sep 17 00:00:00 2001 From: Erik Johansson Date: Sat, 24 Nov 2018 22:32:06 +0100 Subject: [PATCH] Refactor Beurer/Sanitas state machine --- .../bluetooth/BluetoothBeurerSanitas.java | 873 ++++++++++-------- .../bluetooth/BluetoothCommunication.java | 30 +- 2 files changed, 522 insertions(+), 381 deletions(-) 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 2a7d791c..bb040178 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 @@ -29,13 +29,10 @@ import com.health.openscale.core.datatypes.ScaleMeasurement; import com.health.openscale.core.datatypes.ScaleUser; import com.health.openscale.core.utils.Converters; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.text.ParseException; +import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; +import java.util.Calendar; import java.util.Date; -import java.util.TreeSet; import java.util.UUID; import timber.log.Timber; @@ -47,15 +44,74 @@ public class BluetoothBeurerSanitas extends BluetoothCommunication { private static final UUID CUSTOM_CHARACTERISTIC_WEIGHT = BluetoothGattUuid.fromShortCode(0xffe1); private final DeviceType deviceType; - private int startByte; - private int currentScaleUserId; - private int countRegisteredScaleUsers; - private TreeSet seenUsers; - private int maxRegisteredScaleUser; - private ByteArrayOutputStream receivedScaleData; + private byte startByte; - private int getAlternativeStartByte(int id) { - return (startByte & 0xF0) | (id & 0x0F); + private class RemoteUser { + final public long remoteUserId; + final public String name; + final public int year; + + public int localUserId = -1; + public boolean isNew = false; + + RemoteUser(long uid, String name, int year) { + this.remoteUserId = uid; + this.name = name; + this.year = year; + } + } + + private ArrayList remoteUsers = new ArrayList<>(); + private RemoteUser currentRemoteUser; + private byte[] measurementData; + + private final int ID_START_NIBBLE_INIT = 6; + private final int ID_START_NIBBLE_SET_TIME = 9; + private final int ID_START_NIBBLE_DISCONNECT = 0xa; + + private final byte CMD_SET_UNIT = (byte)0x4d; + private final byte CMD_SCALE_STATUS = (byte)0x4f; + + private final byte CMD_USER_ADD = (byte)0x31; + private final byte CMD_USER_DELETE = (byte)0x32; + private final byte CMD_USER_LIST = (byte)0x33; + private final byte CMD_USER_INFO = (byte)0x34; + private final byte CMD_USER_UPDATE = (byte)0x35; + private final byte CMD_USER_DETAILS = (byte)0x36; + + private final byte CMD_DO_MEASUREMENT = (byte)0x40; + private final byte CMD_GET_SAVED_MEASUREMENTS = (byte)0x41; + private final byte CMD_SAVED_MEASUREMENT = (byte)0x42; + private final byte CMD_DELETE_SAVED_MEASUREMENTS = (byte)0x43; + + private final byte CMD_GET_UNKNOWN_MEASUREMENTS = (byte)0x46; + private final byte CMD_UNKNOWN_MEASUREMENT_INFO = (byte)0x47; + private final byte CMD_ASSIGN_UNKNOWN_MEASUREMENT = (byte)0x4b; + private final byte CMD_UNKNOWN_MEASUREMENT = (byte)0x4c; + private final byte CMD_DELETE_UNKNOWN_MEASUREMENT = (byte)0x49; + + private final byte CMD_WEIGHT_MEASUREMENT = (byte)0x58; + private final byte CMD_MEASUREMENT = (byte)0x59; + + private final byte CMD_SCALE_ACK = (byte)0xf0; + private final byte CMD_APP_ACK = (byte)0xf1; + + private byte getAlternativeStartByte(int startNibble) { + return (byte) ((startByte & 0xF0) | (startNibble & 0x0F)); + } + + private long decodeUserId(byte[] data, int offset) { + long high = Converters.fromUnsignedInt32Be(data, offset); + long low = Converters.fromUnsignedInt32Be(data, offset + 4); + return (high << 32) | low; + } + + private byte[] encodeUserId(RemoteUser remoteUser) { + long uid = remoteUser != null ? remoteUser.remoteUserId : 0; + byte[] data = new byte[8]; + Converters.toInt32Be(data, 0, uid >> 32); + Converters.toInt32Be(data, 4, uid & 0xFFFFFFFF); + return data; } public BluetoothBeurerSanitas(Context context, DeviceType deviceType) { @@ -64,11 +120,11 @@ public class BluetoothBeurerSanitas extends BluetoothCommunication { this.deviceType = deviceType; switch (deviceType) { case BEURER_BF700_800_RT_LIBRA: - startByte = 0xf7; + startByte = (byte) 0xf7; break; case BEURER_BF710: case SANITAS_SBF70_70: - startByte = 0xe7; + startByte = (byte) 0xe7; break; } } @@ -92,92 +148,89 @@ public class BluetoothBeurerSanitas extends BluetoothCommunication { switch (stateNr) { case 0: - // Initialize data - currentScaleUserId = -1; - countRegisteredScaleUsers = -1; - maxRegisteredScaleUser = -1; - seenUsers = new TreeSet<>(); - // Setup notification setNotificationOn(CUSTOM_SERVICE_1, CUSTOM_CHARACTERISTIC_WEIGHT, BluetoothGattUuid.DESCRIPTOR_CLIENT_CHARACTERISTIC_CONFIGURATION); break; case 1: - // Say "Hello" to the scale - writeBytes(new byte[]{(byte) getAlternativeStartByte(6), (byte) 0x01}); + // Say "Hello" to the scale and wait for ack + sendAlternativeStartCode(ID_START_NIBBLE_INIT, (byte) 0x01); + pauseBtStateMachine(); break; case 2: - // Wait for "Hello" ack from scale + // Update time on the scale (no ack) + long unixTime = System.currentTimeMillis() / 1000L; + sendAlternativeStartCode(ID_START_NIBBLE_SET_TIME, Converters.toInt32Be(unixTime)); break; case 3: - // Update timestamp of the scale - updateDateTime(); + // Request scale status and wait for ack + sendCommand(CMD_SCALE_STATUS, encodeUserId(null)); + pauseBtStateMachine(); break; case 4: - // Set measurement unit - setUnitCommand(); + // Request list of all users and wait until all have been received + sendCommand(CMD_USER_LIST); + pauseBtStateMachine(); break; case 5: - // Wait for "unit" ack from scale - break; - case 6: - // Request general user information - writeBytes(new byte[]{(byte) startByte, (byte) 0x33}); - break; - case 7: - // Wait for ack of all users - if (seenUsers.size() < countRegisteredScaleUsers || (countRegisteredScaleUsers == -1)) { - // Request this state again - setNextCmd(stateNr); - break; - } + // If currentRemoteUser is null, indexOf returns -1 and index will be 0 + int index = remoteUsers.indexOf(currentRemoteUser) + 1; + currentRemoteUser = null; - // Got all user acks - - // 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); - Timber.d("Cannot create additional scale user"); - sendMessage(R.string.error_max_scale_users, 0); + // Find the next remote user that exists locally + for (; index < remoteUsers.size(); ++index) { + if (remoteUsers.get(index).localUserId != -1) { + currentRemoteUser = remoteUsers.get(index); break; } - - // Request creation of user - final ScaleUser selectedUser = OpenScale.getInstance().getSelectedScaleUser(); - - // We can only use up to 3 characters and have to handle them uppercase - int maxIdx = Math.min(3, selectedUser.getUserName().length()); - byte[] nick = selectedUser.getUserName().toUpperCase().substring(0, maxIdx).getBytes(); - - byte activity = (byte)(selectedUser.getActivityLevel().toInt() + 1); // activity level: 1 - 5 - Timber.d("Create User: %s", selectedUser.getUserName()); - - writeBytes(new byte[]{ - (byte) startByte, (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.getBirthday().getYear(), - (byte) selectedUser.getBirthday().getMonth(), - (byte) selectedUser.getBirthday().getDate(), - (byte) selectedUser.getBodyHeight(), - (byte) (((selectedUser.getGender().isMale() ? 1 : 0) << 7) | activity) - }); - } else { - // Get existing user information - Timber.d("Request getUserInfo %d", currentScaleUserId); - writeBytes(new byte[]{ - (byte) startByte, (byte) 0x36, (byte) 0x0, (byte) 0x0, (byte) 0x0, - (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) currentScaleUserId - }); - } - Timber.d("scaleuserid: %d, registered users: %d, extracted users: %d", - currentScaleUserId, countRegisteredScaleUsers, seenUsers.size()); + + // Fetch saved measurements + if (currentRemoteUser != null) { + Timber.d("Request saved measurements for %s", currentRemoteUser.name); + sendCommand(CMD_GET_SAVED_MEASUREMENTS, encodeUserId(currentRemoteUser)); + + // Replace above command with this to delete the user (for test) + //sendCommand(CMD_USER_DELETE, encodeUserId(currentRemoteUser)); + + // Return to this state until all users have been processed + setNextCmd(stateNr); + pauseBtStateMachine(); + } + else { + postHandleRequest(); + } + break; + case 6: + // Create a remote user for selected openScale user if needed + currentRemoteUser = null; + final ScaleUser selectedUser = OpenScale.getInstance().getSelectedScaleUser(); + for (RemoteUser remoteUser : remoteUsers) { + if (remoteUser.localUserId == selectedUser.getId()) { + currentRemoteUser = remoteUser; + break; + } + } + if (currentRemoteUser == null) { + createRemoteUser(selectedUser); + pauseBtStateMachine(); + } + else { + postHandleRequest(); + } + break; + case 7: + sendCommand(CMD_USER_DETAILS, encodeUserId(currentRemoteUser)); + pauseBtStateMachine(); break; case 8: + if (OpenScale.DEBUG_MODE) { + sendCommand(CMD_GET_UNKNOWN_MEASUREMENTS); + pauseBtStateMachine(); + } + else { + postHandleRequest(); + } break; default: // Finish init if everything is done @@ -189,24 +242,17 @@ public class BluetoothBeurerSanitas extends BluetoothCommunication { @Override protected boolean nextBluetoothCmd(int stateNr) { - switch (stateNr) { case 0: - // If no specific user selected - if (currentScaleUserId == 0) - break; - - Timber.d("Request Saved User Measurements"); - writeBytes(new byte[]{ - (byte) startByte, (byte) 0x41, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, (byte) currentScaleUserId - }); - + if (!currentRemoteUser.isNew) { + sendCommand(CMD_DO_MEASUREMENT, encodeUserId(currentRemoteUser)); + pauseBtStateMachine(); + } + else { + postHandleRequest(); + } break; case 1: - // Wait for user measurements to be received - setNextCmd(stateNr); - break; - case 2: setBtMachineState(BT_MACHINE_STATE.BT_CLEANUP_STATE); break; default: @@ -221,7 +267,7 @@ public class BluetoothBeurerSanitas extends BluetoothCommunication { switch (stateNr) { case 0: // Force disconnect - writeBytes(new byte[]{(byte) 0xea, (byte) 0x02}); + sendAlternativeStartCode(ID_START_NIBBLE_DISCONNECT, (byte) 0x02); break; default: return false; @@ -232,264 +278,309 @@ public class BluetoothBeurerSanitas extends BluetoothCommunication { @Override public void onBluetoothDataChange(BluetoothGatt bluetoothGatt, BluetoothGattCharacteristic gattCharacteristic) { byte[] data = gattCharacteristic.getValue(); - if (data.length == 0) { + if (data == null || data.length == 0) { return; } - if ((data[0] & 0xFF) == getAlternativeStartByte(6) && (data[1] & 0xFF) == 0x00) { - Timber.d("ACK Scale is ready"); - nextMachineStateStep(); + if (data[0] == getAlternativeStartByte(ID_START_NIBBLE_INIT)) { + Timber.d("Got init ack from scale; scale is ready"); + resumeBtStateMachine(); return; } - if ((data[0] & 0xFF) == startByte && (data[1] & 0xFF) == 0xf0 && data[2] == 0x4d) { - Timber.d("ACK Unit set"); - nextMachineStateStep(); + if (data[0] != startByte) { + Timber.e("Got unknown start byte 0x%02x", data[0]); return; } - if ((data[0] & 0xFF) == startByte && (data[1] & 0xFF) == 0xf0 && data[2] == 0x33) { - Timber.d("ACK Got general user information"); - - int count = (byte) (data[4] & 0xFF); - int maxUsers = (byte) (data[5] & 0xFF); - Timber.d("Count: %d, maxUsers: %d", count, maxUsers); - - countRegisteredScaleUsers = count; - // Check if any scale user is registered - if (count == 0) { - currentScaleUserId = 0; // Unknown user + try { + switch (data[1]) { + case CMD_USER_INFO: + processUserInfo(data); + break; + case CMD_SAVED_MEASUREMENT: + processSavedMeasurement(data); + break; + case CMD_WEIGHT_MEASUREMENT: + processWeightMeasurement(data); + break; + case CMD_MEASUREMENT: + processMeasurement(data); + break; + case CMD_UNKNOWN_MEASUREMENT_INFO: + processUnknownMeasurementInfo(data); + break; + case CMD_SCALE_ACK: + processScaleAck(data); + break; + default: + Timber.d("Unknown command 0x%02x", data[1]); + break; } - maxRegisteredScaleUser = maxUsers; - - nextMachineStateStep(); - return; } - - if ((data[0] & 0xFF) == startByte && (data[1] & 0xFF) == 0x34) { - Timber.d("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().getSelectedScaleUser(); - - // Check if we found the currently selected user - if (selectedUser.getUserName().toLowerCase().startsWith(name.toLowerCase()) && - selectedUser.getBirthday().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; - } - Timber.d("Send ack gotUser"); - writeBytes(new byte[]{ - (byte) startByte, (byte) 0xf1, (byte) 0x34, currentUserMax, - currentUserID - }); - } - - return; + catch (IndexOutOfBoundsException|NullPointerException e) { + Timber.e(e); } - - if ((data[0] & 0xFF) == startByte && (data[1] & 0xFF) == 0xF0 && (data[2] & 0xFF) == 0x36) { - Timber.d("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); - - Timber.d("Name: %s, YY-MM-DD: %d-%d-%d, Height: %d, Sex: %s, activity: %d", - name, year, month, day, height, male ? "male" : "female", activity); - - // Get scale status for user - writeBytes(new byte[]{ - (byte) startByte, (byte) 0x4f, (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) 0x0, - (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) currentScaleUserId - }); - - return; - } - - if ((data[0] & 0xFF) == startByte && (data[1] & 0xFF) == 0xf0 && (data[2] & 0xFF) == 0x4F) { - Timber.d("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), 4 st stone - boolean userExists = (data[8] == 0); - boolean userReferWeightExists = (data[9] == 0); - boolean userMeasurementExist = (data[10] == 0); - int scaleVersion = data[11]; - - Timber.d("BatteryLevel: %d, weightThreshold: %.2f, BodyFatThreshold: %.2f," - + " Unit: %d, userExists: %b, UserReference Weight Exists: %b," - + " UserMeasurementExists: %b, scaleVersion: %d", - batteryLevel, weightThreshold, bodyFatThreshold, unit, userExists, - userReferWeightExists, userMeasurementExist, scaleVersion); - return; - } - - if ((data[0] & 0xFF) == startByte && (data[1] & 0xFF) == 0xf0 && data[2] == 0x31) { - Timber.d("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) startByte, 0x40, 0, 0, 0, 0, 0, 0, 0, - (byte) (seenUsers.size() > 0 ? Collections.max(seenUsers) + 1 : 101) - }); - - return; - } - - - if ((data[0] & 0xFF) == startByte && (data[1] & 0xFF) == 0xf0 && (data[2] & 0xFF) == 0x41) { - Timber.d("Will start to receive measurements User Specific"); - - byte nr_measurements = data[3]; - - Timber.d("New measurements: %d", nr_measurements / 2); - return; - } - - if ((data[0] & 0xFF) == startByte && (data[1] & 0xFF) == 0x42) { - Timber.d("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) { - Timber.e(e, "Failed to copy user specific data"); - } - - // Send acknowledgement - writeBytes(new byte[]{ - (byte) startByte, (byte) 0xf1, (byte) 0x42, (byte) (data[2] & 0xFF), - (byte) (data[3] & 0xFF) - }); - - if (current_item % 2 == 0) { - try { - ScaleMeasurement parsedData = parseScaleData(receivedScaleData.toByteArray()); - addScaleData(parsedData); - } catch (ParseException e) { - Timber.d(e, "Could not parse byte array: %s", byteInHex(receivedScaleData.toByteArray())); - - } - } - - if (current_item == max_items) { - // finish and delete - deleteScaleData(); - } - return; - } - - if ((data[0] & 0xFF) == startByte && (data[1] & 0xFF) == 0x58) { - float weight = getKiloGram(data, 3); - if ((data[2] & 0xFF) != 0x00) { - // temporary value; - Timber.d("Active measurement, weight: %.2f", weight); - sendMessage(R.string.info_measuring, weight); - return; - } - - Timber.i("Active measurement, stable weight: %.2f", weight); - - writeBytes(new byte[]{ - (byte) startByte, (byte) 0xf1, (byte) (data[1] & 0xFF), - (byte) (data[2] & 0xFF), (byte) (data[3] & 0xFF), - }); - - if (currentScaleUserId == 0) { - Timber.i("Initial weight set; disconnecting..."); - setBtMachineState(BT_MACHINE_STATE.BT_CLEANUP_STATE); - return; - } - - return; - } - - if ((data[0] & 0xFF) == startByte && (data[1] & 0xFF) == 0x59) { - // Get stable measurement results - Timber.d("Get measurement data %d", (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) { - Timber.e(e, "Failed to copy stable measurement array"); - } - } - - // Send ack that we got the data - writeBytes(new byte[]{ - (byte) startByte, (byte) 0xf1, - (byte) (data[1] & 0xFF), (byte) (data[2] & 0xFF), - (byte) (data[3] & 0xFF), - }); - - if (current_item == max_items) { - // received all parts - try { - ScaleMeasurement parsedData = parseScaleData(receivedScaleData.toByteArray()); - addScaleData(parsedData); - // Delete data - deleteScaleData(); - } catch (ParseException e) { - Timber.d(e, "Parse Exception %s", byteInHex(receivedScaleData.toByteArray())); - } - } - - return; - } - - if ((data[0] & 0xFF) == startByte && (data[1] & 0xFF) == 0xf0 && (data[2] & 0xFF) == 0x43) { - Timber.d("Acknowledge: Data deleted."); - return; - } - - Timber.d("DataChanged - not handled: %s", byteInHex(data)); } - private void deleteScaleData() { - writeBytes(new byte[]{ - (byte) startByte, (byte) 0x43, (byte) 0x0, (byte) 0x0, (byte) 0x0, - (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) 0x0, - (byte) currentScaleUserId - }); + private void processUserInfo(byte[] data) { + final int count = data[2] & 0xFF; + final int current = data[3] & 0xFF; + + if (remoteUsers.size() == current - 1) { + String name = new String(data, 12, 3).toUpperCase(); + int year = 1900 + (data[15] & 0xFF); + + remoteUsers.add(new RemoteUser(decodeUserId(data, 4), name, year)); + + Timber.d("Received user %d/%d: %s (%d)", current, count, name, year); + sendAck(data); + } + + if (current != count) { + return; + } + + Calendar cal = Calendar.getInstance(); + + for (ScaleUser scaleUser : OpenScale.getInstance().getScaleUserList()) { + final String localName = scaleUser.getUserName().toUpperCase(); + cal.setTime(scaleUser.getBirthday()); + final int year = cal.get(Calendar.YEAR); + + for (RemoteUser remoteUser : remoteUsers) { + if (localName.startsWith(remoteUser.name) && year == remoteUser.year) { + remoteUser.localUserId = scaleUser.getId(); + Timber.d("Remote user %s (0x%x) is local user %s (%d)", + remoteUser.name, remoteUser.remoteUserId, + scaleUser.getUserName(), remoteUser.localUserId); + break; + } + } + } + + // All users received + resumeBtStateMachine(); + } + + private void processMeasurementData(byte[] data, int offset, boolean firstPart) { + if (firstPart) { + measurementData = Arrays.copyOfRange(data, offset, data.length); + return; + } + + int oldEnd = measurementData.length; + int toCopy = data.length - offset; + + measurementData = Arrays.copyOf(measurementData, oldEnd + toCopy); + System.arraycopy(data, offset, measurementData, oldEnd, toCopy); + + addMeasurement(measurementData, currentRemoteUser.localUserId); + measurementData = null; + } + + private void processSavedMeasurement(byte[] data) { + int count = data[2] & 0xFF; + int current = data[3] & 0xFF; + + processMeasurementData(data, 4, current % 2 == 1); + sendAck(data); + + if (current == count) { + sendCommand(CMD_DELETE_SAVED_MEASUREMENTS, encodeUserId(currentRemoteUser)); + } + } + + private void processWeightMeasurement(byte[] data) { + boolean stableMeasurement = data[2] == 0; + float weight = getKiloGram(data, 3); + + if (!stableMeasurement) { + Timber.d("Active measurement, weight: %.2f", weight); + sendMessage(R.string.info_measuring, weight); + return; + } + + Timber.i("Active measurement, stable weight: %.2f", weight); + } + + private void processMeasurement(byte[] data) { + int count = data[2] & 0xFF; + int current = data[3] & 0xFF; + + if (current == 1) { + long uid = decodeUserId(data, 5); + currentRemoteUser = null; + for (RemoteUser remoteUser : remoteUsers) { + if (remoteUser.remoteUserId == uid) { + currentRemoteUser = remoteUser; + break; + } + } + } + else { + processMeasurementData(data, 4, current == 2); + } + + sendAck(data); + + if (current == count) { + sendCommand(CMD_DELETE_SAVED_MEASUREMENTS, encodeUserId(currentRemoteUser)); + } + } + + + private void processUnknownMeasurementInfo(byte[] data) { + int count = data[2] & 0xFF; + int current = data[3] & 0xFF; + int mem = data[4] & 0xFF; + long timestamp = Converters.fromUnsignedInt32Be(data, 5); + float weight = getKiloGram(data, 9); + int impedance = Converters.fromUnsignedInt16Be(data, 11); + + Timber.d("Unknown measurement %d/%d (%d): %.2f kg (%d), %s", + current, count, mem, weight, impedance, + new Date(timestamp * 1000)); + + sendAck(data); + + if (current == count) { + resumeBtStateMachine(); + } + } + + private void processScaleAck(byte[] data) { + switch (data[2]) { + case CMD_SCALE_STATUS: + // data[3] != 0 if an invalid user id is given to the command, + // but it still provides some useful information (e.g. current unit). + final int batteryLevel = data[4] & 0xFF; + final float weightThreshold = (data[5] & 0xFF) / 10f; + final float bodyFatThreshold = (data[6] & 0xFF) / 10f; + final int currentUnit = data[7] & 0xFF; + final boolean userExists = data[8] == 0; + final boolean userReferWeightExists = data[9] == 0; + final boolean userMeasurementExist = data[10] == 0; + final int scaleVersion = data[11] & 0xFF; + + Timber.d("BatteryLevel: %d, weightThreshold: %.2f, BodyFatThreshold: %.2f," + + " Unit: %d, userExists: %b, UserReference Weight Exists: %b," + + " UserMeasurementExists: %b, scaleVersion: %d", + batteryLevel, weightThreshold, bodyFatThreshold, currentUnit, userExists, + userReferWeightExists, userMeasurementExist, scaleVersion); + + byte requestedUnit = (byte) currentUnit; + ScaleUser user = OpenScale.getInstance().getSelectedScaleUser(); + switch (user.getScaleUnit()) { + case KG: + requestedUnit = 1; + break; + case LB: + requestedUnit = 2; + break; + case ST: + requestedUnit = 4; + break; + } + if (requestedUnit != currentUnit) { + Timber.d("Set scale unit to %s (%d)", user.getScaleUnit(), requestedUnit); + sendCommand(CMD_SET_UNIT, requestedUnit); + } else { + resumeBtStateMachine(); + } + break; + + case CMD_SET_UNIT: + if (data[3] == 0) { + Timber.d("Scale unit successfully set"); + } + resumeBtStateMachine(); + break; + + case CMD_USER_LIST: + int userCount = data[4] & 0xFF; + int maxUserCount = data[5] & 0xFF; + Timber.d("Have %d users (max is %d)", userCount, maxUserCount); + if (userCount == 0) { + resumeBtStateMachine(); + } + // Otherwise wait for CMD_USER_INFO notifications + break; + + case CMD_GET_SAVED_MEASUREMENTS: + int measurementCount = data[3] & 0xFF; + if (measurementCount == 0) { + resumeBtStateMachine(); + } + // Otherwise wait for CMD_SAVED_MEASUREMENT notifications which will, + // once all measurements have been received, trigger a call to delete them. + // Once the ack for that is received, we resume the state machine (see below). + break; + + case CMD_DELETE_SAVED_MEASUREMENTS: + if (data[3] == 0) { + Timber.d("Saved measurements successfully deleted"); + } + resumeBtStateMachine(); + break; + + case CMD_USER_ADD: + if (data[3] == 0) { + Timber.d("New user successfully added; time to step on scale"); + sendMessage(R.string.info_step_on_scale, 0); + remoteUsers.add(currentRemoteUser); + sendCommand(CMD_DO_MEASUREMENT, encodeUserId(currentRemoteUser)); + break; + } + + Timber.d("Cannot create additional scale user (error 0x%02x)", data[3]); + sendMessage(R.string.error_max_scale_users, 0); + setBtMachineState(BT_MACHINE_STATE.BT_CLEANUP_STATE); + break; + + case CMD_USER_DELETE: + if (data[3] == 0) { + Timber.d("User successfully deleted"); + int index = remoteUsers.indexOf(currentRemoteUser); + remoteUsers.remove(index); + if (index == 0) { + currentRemoteUser = null; + } + else { + currentRemoteUser = remoteUsers.get(index - 1); + } + } + + resumeBtStateMachine(); + break; + + case CMD_DO_MEASUREMENT: + if (data[3] == 0) { + Timber.d("Measure command successfully received"); + } + break; + + case CMD_USER_DETAILS: + if (data[3] == 0) { + String name = new String(data, 4, 3); + int year = 1900 + (data[7] & 0xFF); + int month = 1 + (data[8] & 0xFF); + int day = data[9] & 0xFF; + + int height = data[10] & 0xFF; + boolean male = (data[11] & 0xF0) != 0; + int activity = data[11] & 0x0F; + + Timber.d("Name: %s, Birthday: %d-%02d-%02d, Height: %d, Sex: %s, activity: %d", + name, year, month, day, height, male ? "male" : "female", activity); + } + resumeBtStateMachine(); + break; + + default: + Timber.d("Unhandled scale ack for command 0x%02x", data[2]); + break; + } } private float getKiloGram(byte[] data, int offset) { @@ -502,11 +593,7 @@ public class BluetoothBeurerSanitas extends BluetoothCommunication { return Converters.fromUnsignedInt16Be(data, offset) / 10.0f; } - private ScaleMeasurement parseScaleData(byte[] data) throws ParseException { - if (data.length != 11 + 11) { - throw new ParseException("Parse scala data: unexpected length", 0); - } - + private void addMeasurement(byte[] data, int userId) { long timestamp = Converters.fromUnsignedInt32Be(data, 0) * 1000; float weight = getKiloGram(data, 4); int impedance = Converters.fromUnsignedInt16Be(data, 6); @@ -519,6 +606,7 @@ public class BluetoothBeurerSanitas extends BluetoothCommunication { float bmi = Converters.fromUnsignedInt16Be(data, 20) / 10.0f; ScaleMeasurement receivedMeasurement = new ScaleMeasurement(); + receivedMeasurement.setUserId(userId); receivedMeasurement.setDateTime(new Date(timestamp)); receivedMeasurement.setWeight(weight); receivedMeasurement.setFat(fat); @@ -526,42 +614,69 @@ public class BluetoothBeurerSanitas extends BluetoothCommunication { receivedMeasurement.setMuscle(muscle); receivedMeasurement.setBone(bone); - Timber.i("Measurement: %s, Impedance: %d, BMR: %d, AMR: %d, BMI: %.2f", - receivedMeasurement, impedance, bmr, amr, bmi); - - return receivedMeasurement; - } - - private void updateDateTime() { - // Update date/time of the scale - long unixTime = System.currentTimeMillis() / 1000L; - byte[] unixTimeBytes = Converters.toInt32Be(unixTime); - Timber.d("Write new Date/Time: %d (%s)", unixTime, byteInHex(unixTimeBytes)); - - writeBytes(new byte[]{(byte) getAlternativeStartByte(9), - unixTimeBytes[0], unixTimeBytes[1], unixTimeBytes[2], unixTimeBytes[3]}); - } - - private void setUnitCommand() { - byte[] command = new byte[] {(byte) startByte, 0x4d, 0x00}; - final ScaleUser selectedUser = OpenScale.getInstance().getSelectedScaleUser(); - - switch (selectedUser.getScaleUnit()) { - case KG: - command[2] = (byte) 0x01; - break; - case LB: - command[2] = (byte) 0x02; - break; - case ST: - command[2] = (byte) 0x04; - break; - } - Timber.d("Setting unit %s", selectedUser.getScaleUnit()); - writeBytes(command); + addScaleData(receivedMeasurement); } private void writeBytes(byte[] data) { writeBytes(CUSTOM_SERVICE_1, CUSTOM_CHARACTERISTIC_WEIGHT, data); } + + private void sendCommand(byte command, byte... parameters) { + byte[] data = new byte[parameters.length + 2]; + data[0] = startByte; + data[1] = command; + + int i = 2; + for (byte parameter : parameters) { + data[i++] = parameter; + } + + writeBytes(data); + } + + private void sendAck(byte[] data) { + sendCommand(CMD_APP_ACK, Arrays.copyOfRange(data, 1, 4)); + } + + private void sendAlternativeStartCode(int id, byte... parameters) { + byte[] data = new byte[parameters.length + 1]; + data[0] = getAlternativeStartByte(id); + + int i = 1; + for (byte parameter : parameters) { + data[i++] = parameter; + } + + writeBytes(data); + } + + private void createRemoteUser(ScaleUser scaleUser) { + Timber.d("Create user: %s", scaleUser.getUserName()); + + Calendar cal = Calendar.getInstance(); + cal.setTime(scaleUser.getBirthday()); + + // We can only use up to 3 characters and have to handle them uppercase + byte[] nick = Arrays.copyOf(scaleUser.getUserName().toUpperCase().getBytes(), 3); + byte year = (byte) (cal.get(Calendar.YEAR) - 1900); + byte month = (byte) cal.get(Calendar.MONTH); + byte day = (byte) cal.get(Calendar.DAY_OF_MONTH); + byte height = (byte) scaleUser.getBodyHeight(); + byte sex = scaleUser.getGender().isMale() ? (byte) 0x80 : 0; + byte activity = (byte) (scaleUser.getActivityLevel().toInt() + 1); // activity level: 1 - 5 + + long maxUserId = remoteUsers.isEmpty() ? 100 : 0; + for (RemoteUser remoteUser : remoteUsers) { + maxUserId = Math.max(maxUserId, remoteUser.remoteUserId); + } + + currentRemoteUser = new RemoteUser(maxUserId + 1, new String(nick), 1900 + year); + currentRemoteUser.localUserId = scaleUser.getId(); + currentRemoteUser.isNew = true; + + byte[] uid = encodeUserId(currentRemoteUser); + + sendCommand(CMD_USER_ADD, uid[0], uid[1], uid[2], uid[3], uid[4], uid[5], uid[6], uid[7], + nick[0], nick[1], nick[2], year, month, day, height, (byte) (sex | activity)); + } } 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 01bfa3a1..8b2c99e0 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 @@ -48,7 +48,7 @@ public abstract class BluetoothCommunication { 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} + public enum BT_MACHINE_STATE {BT_INIT_STATE, BT_CMD_STATE, BT_CLEANUP_STATE, BT_PAUSED_STATE} private static final long LE_SCAN_TIMEOUT_MS = 10 * 1000; @@ -66,6 +66,7 @@ public abstract class BluetoothCommunication { private int initStepNr; private int cleanupStepNr; private BT_MACHINE_STATE btMachineState; + private BT_MACHINE_STATE btPausedMachineState; private class GattObjectValue { public final GattObject gattObject; @@ -239,6 +240,29 @@ public abstract class BluetoothCommunication { handleRequests(); } + protected void pauseBtStateMachine() { + if (btMachineState != BT_MACHINE_STATE.BT_CLEANUP_STATE + && btMachineState != BT_MACHINE_STATE.BT_PAUSED_STATE) { + btPausedMachineState = btMachineState; + setBtMachineState(BT_MACHINE_STATE.BT_PAUSED_STATE); + } + } + + protected void resumeBtStateMachine() { + if (this.btMachineState == BT_MACHINE_STATE.BT_PAUSED_STATE) { + setBtMachineState(btPausedMachineState); + } + } + + protected void postHandleRequest() { + handler.post(new Runnable() { + @Override + public void run() { + handleRequests(); + } + }); + } + /** * Write a byte array to a Bluetooth device. * @@ -530,7 +554,6 @@ public abstract class BluetoothCommunication { if (doCleanup) { if (btMachineState != BT_MACHINE_STATE.BT_CLEANUP_STATE) { setBtMachineState(BT_MACHINE_STATE.BT_CLEANUP_STATE); - nextMachineStateStep(); } handler.post(new Runnable() { @Override @@ -572,6 +595,9 @@ public abstract class BluetoothCommunication { nextCleanUpCmd(cleanupStepNr); cleanupStepNr++; break; + case BT_PAUSED_STATE: + Timber.d("PAUSED STATE"); + break; } }