From e158a4084a6f675422da048c1e5dc00643b7ec16 Mon Sep 17 00:00:00 2001 From: Erik Johansson Date: Sat, 24 Nov 2018 22:32:06 +0100 Subject: [PATCH 1/7] 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; } } From 5a5b615495a79b32f377c8101b11450c7963fff3 Mon Sep 17 00:00:00 2001 From: Erik Johansson Date: Sun, 25 Nov 2018 23:38:41 +0100 Subject: [PATCH 2/7] Only use ascii chars in name and support short names on scale --- .../bluetooth/BluetoothBeurerSanitas.java | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 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 bb040178..181980dc 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,6 +29,7 @@ import com.health.openscale.core.datatypes.ScaleMeasurement; import com.health.openscale.core.datatypes.ScaleUser; import com.health.openscale.core.utils.Converters; +import java.text.Normalizer; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; @@ -114,6 +115,21 @@ public class BluetoothBeurerSanitas extends BluetoothCommunication { return data; } + private String decodeString(byte[] data, int offset, int maxLength) { + int length = 0; + for (; length < maxLength; ++length) { + if (data[offset + length] == 0) { + break; + } + } + return new String(data, offset, length); + } + + private String normalizeString(String input) { + String normalized = Normalizer.normalize(input, Normalizer.Form.NFD); + return normalized.replaceAll("[^A-Za-z0-9]", ""); + } + public BluetoothBeurerSanitas(Context context, DeviceType deviceType) { super(context); @@ -328,7 +344,7 @@ public class BluetoothBeurerSanitas extends BluetoothCommunication { final int current = data[3] & 0xFF; if (remoteUsers.size() == current - 1) { - String name = new String(data, 12, 3).toUpperCase(); + String name = decodeString(data, 12, 3); int year = 1900 + (data[15] & 0xFF); remoteUsers.add(new RemoteUser(decodeUserId(data, 4), name, year)); @@ -344,7 +360,7 @@ public class BluetoothBeurerSanitas extends BluetoothCommunication { Calendar cal = Calendar.getInstance(); for (ScaleUser scaleUser : OpenScale.getInstance().getScaleUserList()) { - final String localName = scaleUser.getUserName().toUpperCase(); + final String localName = normalizeString(scaleUser.getUserName()).toUpperCase(); cal.setTime(scaleUser.getBirthday()); final int year = cal.get(Calendar.YEAR); @@ -562,7 +578,7 @@ public class BluetoothBeurerSanitas extends BluetoothCommunication { case CMD_USER_DETAILS: if (data[3] == 0) { - String name = new String(data, 4, 3); + String name = decodeString(data, 4, 3); int year = 1900 + (data[7] & 0xFF); int month = 1 + (data[8] & 0xFF); int day = data[9] & 0xFF; @@ -657,7 +673,7 @@ public class BluetoothBeurerSanitas extends BluetoothCommunication { 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[] nick = Arrays.copyOf(normalizeString(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); From 25ebaa0c21d725050f48c98380925a90b26f6362 Mon Sep 17 00:00:00 2001 From: Erik Johansson Date: Sun, 25 Nov 2018 23:39:20 +0100 Subject: [PATCH 3/7] Minor doc update --- doc/protocols/beurer.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/doc/protocols/beurer.md b/doc/protocols/beurer.md index b0b3579c..1b341f77 100644 --- a/doc/protocols/beurer.md +++ b/doc/protocols/beurer.md @@ -24,9 +24,12 @@ Initialization 2. Write: ` 01` 3. Notification: ` 00 20` 4. Write: ` ` -5. Write: ` 4f <8 bytes 00>` +5. Write: ` 4f ` 6. Notification: ` f0 4f ` +`` can be given as all 0 (or other invalid user id) to query +scale status only. + * ``: battery level * ``: weight threshold (unit g / 100) * ``: fat threshold @@ -40,6 +43,11 @@ Initialization 1. Write: ` 4e ` 2. Notification: ` f0 4e 00` +Thresholds in original app: +* 0x28 0xdc +* 0x14 0xdc +* 0x0a 0x14 + ### Set unit @@ -122,7 +130,7 @@ Measurements 4. Write: ` f1 42 ` 5. Goto 3 if ` != ` -* All `` from step 3 is joined and parsed as a measurement. +* All `` from step 3 is joined and parsed as ` / 2` measurement(s). ### Delete saved measurements From 93ccf50cd2a1fa9e2789f2a0d86f5bcac50c6c91 Mon Sep 17 00:00:00 2001 From: Erik Johansson Date: Mon, 26 Nov 2018 20:58:45 +0100 Subject: [PATCH 4/7] Lift restriction on user name having to be longer than 3 chars Beurer/Sanitas driver can now handle it. --- .../openscale/gui/activities/UserSettingsActivity.java | 8 ++------ android_app/app/src/main/res/values-cs/strings.xml | 1 - android_app/app/src/main/res/values-da/strings.xml | 1 - android_app/app/src/main/res/values-de/strings.xml | 3 +-- android_app/app/src/main/res/values-el/strings.xml | 1 - android_app/app/src/main/res/values-es/strings.xml | 1 - android_app/app/src/main/res/values-fr/strings.xml | 1 - android_app/app/src/main/res/values-gl/strings.xml | 1 - android_app/app/src/main/res/values-hr/strings.xml | 1 - android_app/app/src/main/res/values-it/strings.xml | 1 - android_app/app/src/main/res/values-iw/strings.xml | 1 - android_app/app/src/main/res/values-ja/strings.xml | 1 - android_app/app/src/main/res/values-nb/strings.xml | 1 - android_app/app/src/main/res/values-nl/strings.xml | 1 - android_app/app/src/main/res/values-pl/strings.xml | 1 - android_app/app/src/main/res/values-pt-rBR/strings.xml | 1 - android_app/app/src/main/res/values-ro/strings.xml | 1 - android_app/app/src/main/res/values-ru/strings.xml | 1 - android_app/app/src/main/res/values-sk/strings.xml | 1 - android_app/app/src/main/res/values-sl/strings.xml | 1 - android_app/app/src/main/res/values-sv/strings.xml | 1 - android_app/app/src/main/res/values-tr/strings.xml | 1 - android_app/app/src/main/res/values-vi/strings.xml | 1 - android_app/app/src/main/res/values-zh-rTW/strings.xml | 1 - android_app/app/src/main/res/values/strings.xml | 1 - 25 files changed, 3 insertions(+), 31 deletions(-) diff --git a/android_app/app/src/main/java/com/health/openscale/gui/activities/UserSettingsActivity.java b/android_app/app/src/main/java/com/health/openscale/gui/activities/UserSettingsActivity.java index 3f478ad2..abcd142e 100644 --- a/android_app/app/src/main/java/com/health/openscale/gui/activities/UserSettingsActivity.java +++ b/android_app/app/src/main/java/com/health/openscale/gui/activities/UserSettingsActivity.java @@ -302,12 +302,8 @@ public class UserSettingsActivity extends BaseAppCompatActivity { { boolean validate = true; - if (txtUserName.getText().toString().length() < 3) { - if (txtUserName.getText().toString().length() == 0) { - txtUserName.setError(getResources().getString(R.string.error_user_name_required)); - } else { - txtUserName.setError(getResources().getString(R.string.error_user_name_too_short)); - } + if (txtUserName.getText().toString().length() == 0) { + txtUserName.setError(getResources().getString(R.string.error_user_name_required)); validate = false; } diff --git a/android_app/app/src/main/res/values-cs/strings.xml b/android_app/app/src/main/res/values-cs/strings.xml index e698e2ee..cb2b6b4c 100644 --- a/android_app/app/src/main/res/values-cs/strings.xml +++ b/android_app/app/src/main/res/values-cs/strings.xml @@ -65,7 +65,6 @@ Chyba při exportu Chyba při importu Chyba: je třeba uživatelské jméno - Chyba: je třeba, aby uživatelské jméno bylo alespoň 3 znaky dlouhé Chyba: je třeba zadat výšku člověka Chyba: je třeba zadat počáteční hmotnost Chyba: je třeba zadat cílovou hmotnost diff --git a/android_app/app/src/main/res/values-da/strings.xml b/android_app/app/src/main/res/values-da/strings.xml index de7bfeba..68a63bb8 100644 --- a/android_app/app/src/main/res/values-da/strings.xml +++ b/android_app/app/src/main/res/values-da/strings.xml @@ -130,7 +130,6 @@ Ingen Bluetooth enhed er valgt En måling med samme dato og tidspunkt findes allerede Generelt - Fejl: navnet skal være mindst tre tegn langt Tema Er du tilfreds med openScale? Vil du rate app"en på Google Play eller GitHub? diff --git a/android_app/app/src/main/res/values-de/strings.xml b/android_app/app/src/main/res/values-de/strings.xml index b54c2bfc..a2123d35 100644 --- a/android_app/app/src/main/res/values-de/strings.xml +++ b/android_app/app/src/main/res/values-de/strings.xml @@ -137,8 +137,7 @@ Ja klar Nein danke Würdest Du uns etwas Feedback geben? - Fehler: Benutzername muss aus mind. 3 Zeichen bestehen - Ein unerwarteter Fehler ist aufgetreten. + Ein unerwarteter Fehler ist aufgetreten. \n \n Bitte erstellen Sie ein neues Issue mit detailliertem Fehlerbericht auf \nhttps://github.com/oliexdev/openScale/issues diff --git a/android_app/app/src/main/res/values-el/strings.xml b/android_app/app/src/main/res/values-el/strings.xml index 2a5bae28..8eca2cbc 100644 --- a/android_app/app/src/main/res/values-el/strings.xml +++ b/android_app/app/src/main/res/values-el/strings.xml @@ -73,7 +73,6 @@ Σφάλμα κατά την εξαγωγή Σφάλμα κατά την εισαγωγή Σφάλμα: Όνομα απαραίτητο - Σφάλμα: Το όνομα πρέπει να είναι 3 ή περισσότεροι χαρακτήρες Σφάλμα: Ύψος απαραίτητο Σφάλμα: Αρχικό βάρος απαραίτητο Σφάλμα: Βάρος στόχος απαραίτητο diff --git a/android_app/app/src/main/res/values-es/strings.xml b/android_app/app/src/main/res/values-es/strings.xml index 33e2ce26..f74a454a 100644 --- a/android_app/app/src/main/res/values-es/strings.xml +++ b/android_app/app/src/main/res/values-es/strings.xml @@ -155,7 +155,6 @@ Compartir - Error: El nombre debe contener 3 o más carácteres "No hay dispositivo Bluetooth seleccionado " Medición con al misma fecha y hora ya existe diff --git a/android_app/app/src/main/res/values-fr/strings.xml b/android_app/app/src/main/res/values-fr/strings.xml index d2a1d1a3..337fb45b 100644 --- a/android_app/app/src/main/res/values-fr/strings.xml +++ b/android_app/app/src/main/res/values-fr/strings.xml @@ -147,7 +147,6 @@ Suivant À propos Partager - Erreur : Le nom doit contenir au moins 3 caractères Erreur : Poids initial requis Exporter la sauvegarde Importer la sauvegarde diff --git a/android_app/app/src/main/res/values-gl/strings.xml b/android_app/app/src/main/res/values-gl/strings.xml index 3c3796af..90cd924a 100644 --- a/android_app/app/src/main/res/values-gl/strings.xml +++ b/android_app/app/src/main/res/values-gl/strings.xml @@ -69,7 +69,6 @@ Erro ao exportar Erro ao importar Erro: O nome é obrigatorio - Erro: O nome debe ter 3 ou máis caracteres Erro: A altura é obrigatoria Erro: O peso inicial é obrigatorio Erro: O peso obxectivo é obrigatorio diff --git a/android_app/app/src/main/res/values-hr/strings.xml b/android_app/app/src/main/res/values-hr/strings.xml index 55c4b4d4..fe10e478 100644 --- a/android_app/app/src/main/res/values-hr/strings.xml +++ b/android_app/app/src/main/res/values-hr/strings.xml @@ -75,7 +75,6 @@ Greška u izvozu Greška u uvozu Greška: Unesite ime - Greška: Ime mora imati barem 3 znaka Greška: Unesite težinu Greška: Unesite početnu težinu Greška: Unesite ciljanu težinu diff --git a/android_app/app/src/main/res/values-it/strings.xml b/android_app/app/src/main/res/values-it/strings.xml index e096a8fb..47e2ba79 100644 --- a/android_app/app/src/main/res/values-it/strings.xml +++ b/android_app/app/src/main/res/values-it/strings.xml @@ -73,7 +73,6 @@ Errore durante l\'esportazione Errore di importazione Errore: Nome obbligatorio - Errore: Il nome deve essere almeno 3 caratteri Errore: Altezza necessaria Errore: Peso iniziale richiesto Errore: Obiettivo di peso richiesto diff --git a/android_app/app/src/main/res/values-iw/strings.xml b/android_app/app/src/main/res/values-iw/strings.xml index c5855980..7ca2083f 100644 --- a/android_app/app/src/main/res/values-iw/strings.xml +++ b/android_app/app/src/main/res/values-iw/strings.xml @@ -76,7 +76,6 @@ שגיאה בייצוא שגיאה בייבוא שגיאה: נדרש שם - שגיאה: שם חייב להיות באורך 3 תווים ומעלה שגיאה: נדרש גובה שגיאה: נדרש גובה ראשוני שגיאה: נדרש משקל יעד diff --git a/android_app/app/src/main/res/values-ja/strings.xml b/android_app/app/src/main/res/values-ja/strings.xml index 9393eb1f..83dc8251 100644 --- a/android_app/app/src/main/res/values-ja/strings.xml +++ b/android_app/app/src/main/res/values-ja/strings.xml @@ -109,7 +109,6 @@ 基礎代謝率 (BMR) 除脂肪体重 骨密度 - エラー:名前は3文字以上でなければいけません Bluetooth端末が選択されていません 同じ日時のものがすでに存在している測定 diff --git a/android_app/app/src/main/res/values-nb/strings.xml b/android_app/app/src/main/res/values-nb/strings.xml index 87f32c0f..a787e45d 100644 --- a/android_app/app/src/main/res/values-nb/strings.xml +++ b/android_app/app/src/main/res/values-nb/strings.xml @@ -74,7 +74,6 @@ Feil ved eksport Feil ved import Feil: Brukernavn kreves - Feil: Brukernavn må være minst tre tegn Feil: Høyde kreves Feil: Startvekt kreves Feil: Målvekt kreves diff --git a/android_app/app/src/main/res/values-nl/strings.xml b/android_app/app/src/main/res/values-nl/strings.xml index 8b674ed1..697b011c 100644 --- a/android_app/app/src/main/res/values-nl/strings.xml +++ b/android_app/app/src/main/res/values-nl/strings.xml @@ -74,7 +74,6 @@ Fout met exporteren Fout met importeren Fout: Naam vereist - Fout: Naam moet 3 karakters of meer zijn Fout: Lichaamslengte vereist Fout: Startgewicht vereist Fout: Streefgewicht vereist diff --git a/android_app/app/src/main/res/values-pl/strings.xml b/android_app/app/src/main/res/values-pl/strings.xml index 9439921c..d813eae3 100644 --- a/android_app/app/src/main/res/values-pl/strings.xml +++ b/android_app/app/src/main/res/values-pl/strings.xml @@ -74,7 +74,6 @@ Błąd eksportu Błąd importu Błąd: wymagana nazwa użytkownika - Błąd: nazwa użytkownika musi mieć przynajmniej 3 znaki Błąd: wymagany wzrost Błąd: wymagana waga początkowa Błąd: wymagana waga docelowa diff --git a/android_app/app/src/main/res/values-pt-rBR/strings.xml b/android_app/app/src/main/res/values-pt-rBR/strings.xml index ce020f7d..2e198d9e 100644 --- a/android_app/app/src/main/res/values-pt-rBR/strings.xml +++ b/android_app/app/src/main/res/values-pt-rBR/strings.xml @@ -143,7 +143,6 @@ Tema dispositivo não suportado - Erro: nome de usuário tem que ter 3 ou mais caracteres Ajuda Gostou do openScale? diff --git a/android_app/app/src/main/res/values-ro/strings.xml b/android_app/app/src/main/res/values-ro/strings.xml index 40f1add0..3609453b 100644 --- a/android_app/app/src/main/res/values-ro/strings.xml +++ b/android_app/app/src/main/res/values-ro/strings.xml @@ -73,7 +73,6 @@ Eroare la export Eroare la import Eroare: Nume necesar - Eroare: Numele trebuie să aibă cel puțin trei caractere Eroare: Înălțime necesară Eroare: Greutatea inițială trebuie indicată Eroare: Greutatea dorită trebuie indicată diff --git a/android_app/app/src/main/res/values-ru/strings.xml b/android_app/app/src/main/res/values-ru/strings.xml index 5d907d95..0215815f 100644 --- a/android_app/app/src/main/res/values-ru/strings.xml +++ b/android_app/app/src/main/res/values-ru/strings.xml @@ -70,7 +70,6 @@ Ошибка при экспортировании Ошибка при импортировании Ошибка: необходимо Имя - Ошибка: Имя должно быть длиннее 3 символов Ошибка: требуется указать Рост Ошибка: необходимо указать начальный вес Ошибка: необходимо указать цель по весу diff --git a/android_app/app/src/main/res/values-sk/strings.xml b/android_app/app/src/main/res/values-sk/strings.xml index 5210245b..8cd355c7 100644 --- a/android_app/app/src/main/res/values-sk/strings.xml +++ b/android_app/app/src/main/res/values-sk/strings.xml @@ -126,7 +126,6 @@ Zdieľať openScale CSV export údajov (%s) - Chyba: používateľské meno musí obsahovať minimálne 3 znaky Nevybrali ste žiadne bluetooth zariadenie meranie s rovnakým dátumom a časom už existuje diff --git a/android_app/app/src/main/res/values-sl/strings.xml b/android_app/app/src/main/res/values-sl/strings.xml index bf953a6e..acd999ca 100644 --- a/android_app/app/src/main/res/values-sl/strings.xml +++ b/android_app/app/src/main/res/values-sl/strings.xml @@ -75,7 +75,6 @@ Napaka pri izvozu Napaka pri uvozu Napaka: zahtevno ime - Napaka: Ime mora imeti vsaj 3 znake Napaka: Višina zahtevana Napaka: Začetna teža zahtevana Napaka: Ciljna teža zahtevana diff --git a/android_app/app/src/main/res/values-sv/strings.xml b/android_app/app/src/main/res/values-sv/strings.xml index 97bd2e17..b8cd556d 100644 --- a/android_app/app/src/main/res/values-sv/strings.xml +++ b/android_app/app/src/main/res/values-sv/strings.xml @@ -130,7 +130,6 @@ Ingen Bluetooth-enhet vald mätning med samma datum och tid existerar redan Allmänt - Fel: namn måste vara minst 3 tecken Tema Uppskattar du openScale? Vad sägs om ett betyg på Google Play eller GitHub? diff --git a/android_app/app/src/main/res/values-tr/strings.xml b/android_app/app/src/main/res/values-tr/strings.xml index f070dfdc..1c44166d 100644 --- a/android_app/app/src/main/res/values-tr/strings.xml +++ b/android_app/app/src/main/res/values-tr/strings.xml @@ -154,7 +154,6 @@ Ölçüm ekle Paylaş - Hata: Ad 3 karakter veya daha fazla olmalı Seçili Bluetooth aygıtı yok Aynı tarih ve saatte ölçüm zaten var diff --git a/android_app/app/src/main/res/values-vi/strings.xml b/android_app/app/src/main/res/values-vi/strings.xml index 58b701b4..a9213f65 100644 --- a/android_app/app/src/main/res/values-vi/strings.xml +++ b/android_app/app/src/main/res/values-vi/strings.xml @@ -72,7 +72,6 @@ Lỗi khi xuất Lỗi khi nhập Lỗi: Tên là bắt buộc - Lỗi: Tên phải ít nhất 3 kí tự Lỗi: Chiều cao là bắt buộc Lỗi: Trọng lượng ban đầu là bắt buộc Lỗi: Trọng lượng mục tiêu là bắt buộc diff --git a/android_app/app/src/main/res/values-zh-rTW/strings.xml b/android_app/app/src/main/res/values-zh-rTW/strings.xml index 7a729904..96fd5ede 100644 --- a/android_app/app/src/main/res/values-zh-rTW/strings.xml +++ b/android_app/app/src/main/res/values-zh-rTW/strings.xml @@ -72,7 +72,6 @@ 滙出出錯 滙入出錯 錯誤: 必須輸入名字 - 錯誤: 名字必須三個字元或以上 錯誤: 必須輸入身高 錯誤: 必須輸入初始體重 錯誤: 必須輸入目標體重 diff --git a/android_app/app/src/main/res/values/strings.xml b/android_app/app/src/main/res/values/strings.xml index e3f18afe..89e77b63 100644 --- a/android_app/app/src/main/res/values/strings.xml +++ b/android_app/app/src/main/res/values/strings.xml @@ -75,7 +75,6 @@ Error exporting Error importing Error: Name required - Error: Name must be 3 characters or more Error: Height required Error: Initial weight required Error: Goal weight required From 660af298d7368ac73b417a5724faacf7eca044fb Mon Sep 17 00:00:00 2001 From: Erik Johansson Date: Mon, 26 Nov 2018 21:01:02 +0100 Subject: [PATCH 5/7] Ack data even if it has been received already --- .../openscale/core/bluetooth/BluetoothBeurerSanitas.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 181980dc..8527c0db 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 @@ -350,9 +350,10 @@ public class BluetoothBeurerSanitas extends BluetoothCommunication { remoteUsers.add(new RemoteUser(decodeUserId(data, 4), name, year)); Timber.d("Received user %d/%d: %s (%d)", current, count, name, year); - sendAck(data); } + sendAck(data); + if (current != count) { return; } From 8138f385cb454db20e53878a35a22dadaf8a1159 Mon Sep 17 00:00:00 2001 From: Erik Johansson Date: Mon, 26 Nov 2018 21:25:47 +0100 Subject: [PATCH 6/7] Handle user names with (only) strange characters --- .../core/bluetooth/BluetoothBeurerSanitas.java | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 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 8527c0db..bff22a10 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 @@ -34,6 +34,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Date; +import java.util.Locale; import java.util.UUID; import timber.log.Timber; @@ -130,6 +131,14 @@ public class BluetoothBeurerSanitas extends BluetoothCommunication { return normalized.replaceAll("[^A-Za-z0-9]", ""); } + private String convertUserNameToScale(ScaleUser user) { + String normalized = normalizeString(user.getUserName()); + if (normalized.isEmpty()) { + return String.valueOf(user.getId()); + } + return normalized.toUpperCase(Locale.US); + } + public BluetoothBeurerSanitas(Context context, DeviceType deviceType) { super(context); @@ -361,7 +370,7 @@ public class BluetoothBeurerSanitas extends BluetoothCommunication { Calendar cal = Calendar.getInstance(); for (ScaleUser scaleUser : OpenScale.getInstance().getScaleUserList()) { - final String localName = normalizeString(scaleUser.getUserName()).toUpperCase(); + final String localName = convertUserNameToScale(scaleUser); cal.setTime(scaleUser.getBirthday()); final int year = cal.get(Calendar.YEAR); @@ -673,8 +682,8 @@ public class BluetoothBeurerSanitas extends BluetoothCommunication { 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(normalizeString(scaleUser.getUserName()).toUpperCase().getBytes(), 3); + // We can only use up to 3 characters (padding with 0 if needed) + byte[] nick = Arrays.copyOf(convertUserNameToScale(scaleUser).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); From b0f8cb0cb3a7be6a63c6b95f542c092d78bb34ae Mon Sep 17 00:00:00 2001 From: Erik Johansson Date: Sat, 1 Dec 2018 20:00:21 +0100 Subject: [PATCH 7/7] Remove some unused test stuff and some minor cleanup --- .../bluetooth/BluetoothBeurerSanitas.java | 66 +++---------------- 1 file changed, 8 insertions(+), 58 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 bff22a10..d796877e 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 @@ -68,8 +68,9 @@ public class BluetoothBeurerSanitas extends BluetoothCommunication { private byte[] measurementData; private final int ID_START_NIBBLE_INIT = 6; + private final int ID_START_NIBBLE_CMD = 7; private final int ID_START_NIBBLE_SET_TIME = 9; - private final int ID_START_NIBBLE_DISCONNECT = 0xa; + private final int ID_START_NIBBLE_DISCONNECT = 10; private final byte CMD_SET_UNIT = (byte)0x4d; private final byte CMD_SCALE_STATUS = (byte)0x4f; @@ -99,7 +100,7 @@ public class BluetoothBeurerSanitas extends BluetoothCommunication { private final byte CMD_APP_ACK = (byte)0xf1; private byte getAlternativeStartByte(int startNibble) { - return (byte) ((startByte & 0xF0) | (startNibble & 0x0F)); + return (byte) ((startByte & 0xF0) | startNibble); } private long decodeUserId(byte[] data, int offset) { @@ -145,11 +146,11 @@ public class BluetoothBeurerSanitas extends BluetoothCommunication { this.deviceType = deviceType; switch (deviceType) { case BEURER_BF700_800_RT_LIBRA: - startByte = (byte) 0xf7; + startByte = (byte) (0xf0 | ID_START_NIBBLE_CMD); break; case BEURER_BF710: case SANITAS_SBF70_70: - startByte = (byte) 0xe7; + startByte = (byte) (0xe0 | ID_START_NIBBLE_CMD); break; } } @@ -215,9 +216,6 @@ public class BluetoothBeurerSanitas extends BluetoothCommunication { 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(); @@ -248,15 +246,6 @@ public class BluetoothBeurerSanitas extends BluetoothCommunication { 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 return false; @@ -332,9 +321,6 @@ public class BluetoothBeurerSanitas extends BluetoothCommunication { case CMD_MEASUREMENT: processMeasurement(data); break; - case CMD_UNKNOWN_MEASUREMENT_INFO: - processUnknownMeasurementInfo(data); - break; case CMD_SCALE_ACK: processScaleAck(data); break; @@ -455,26 +441,6 @@ public class BluetoothBeurerSanitas extends BluetoothCommunication { } } - - 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: @@ -489,9 +455,9 @@ public class BluetoothBeurerSanitas extends BluetoothCommunication { 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", + Timber.d("Battery level: %d; threshold: weight=%.2f, body fat=%.2f;" + + " unit: %d; requested user: exists=%b, has reference weight=%b," + + " has measurement=%b; scale version: %d", batteryLevel, weightThreshold, bodyFatThreshold, currentUnit, userExists, userReferWeightExists, userMeasurementExist, scaleVersion); @@ -564,22 +530,6 @@ public class BluetoothBeurerSanitas extends BluetoothCommunication { 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");