1
0
mirror of https://github.com/oliexdev/openScale.git synced 2025-08-27 10:15:00 +02:00

Refactor Beurer/Sanitas state machine

This commit is contained in:
Erik Johansson
2018-11-24 22:32:06 +01:00
parent 4d09351f4a
commit e158a4084a
2 changed files with 522 additions and 381 deletions

View File

@@ -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<Integer> 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<RemoteUser> 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));
}
}

View File

@@ -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 <GattObject> {
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;
}
}