mirror of
https://github.com/oliexdev/openScale.git
synced 2025-08-28 10:40:47 +02:00
Refactor Beurer/Sanitas state machine
This commit is contained in:
@@ -29,13 +29,10 @@ import com.health.openscale.core.datatypes.ScaleMeasurement;
|
|||||||
import com.health.openscale.core.datatypes.ScaleUser;
|
import com.health.openscale.core.datatypes.ScaleUser;
|
||||||
import com.health.openscale.core.utils.Converters;
|
import com.health.openscale.core.utils.Converters;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.util.ArrayList;
|
||||||
import java.io.IOException;
|
|
||||||
import java.text.ParseException;
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Calendar;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.TreeSet;
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
@@ -47,15 +44,74 @@ public class BluetoothBeurerSanitas extends BluetoothCommunication {
|
|||||||
private static final UUID CUSTOM_CHARACTERISTIC_WEIGHT = BluetoothGattUuid.fromShortCode(0xffe1);
|
private static final UUID CUSTOM_CHARACTERISTIC_WEIGHT = BluetoothGattUuid.fromShortCode(0xffe1);
|
||||||
|
|
||||||
private final DeviceType deviceType;
|
private final DeviceType deviceType;
|
||||||
private int startByte;
|
private byte startByte;
|
||||||
private int currentScaleUserId;
|
|
||||||
private int countRegisteredScaleUsers;
|
|
||||||
private TreeSet<Integer> seenUsers;
|
|
||||||
private int maxRegisteredScaleUser;
|
|
||||||
private ByteArrayOutputStream receivedScaleData;
|
|
||||||
|
|
||||||
private int getAlternativeStartByte(int id) {
|
private class RemoteUser {
|
||||||
return (startByte & 0xF0) | (id & 0x0F);
|
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) {
|
public BluetoothBeurerSanitas(Context context, DeviceType deviceType) {
|
||||||
@@ -64,11 +120,11 @@ public class BluetoothBeurerSanitas extends BluetoothCommunication {
|
|||||||
this.deviceType = deviceType;
|
this.deviceType = deviceType;
|
||||||
switch (deviceType) {
|
switch (deviceType) {
|
||||||
case BEURER_BF700_800_RT_LIBRA:
|
case BEURER_BF700_800_RT_LIBRA:
|
||||||
startByte = 0xf7;
|
startByte = (byte) 0xf7;
|
||||||
break;
|
break;
|
||||||
case BEURER_BF710:
|
case BEURER_BF710:
|
||||||
case SANITAS_SBF70_70:
|
case SANITAS_SBF70_70:
|
||||||
startByte = 0xe7;
|
startByte = (byte) 0xe7;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -92,92 +148,89 @@ public class BluetoothBeurerSanitas extends BluetoothCommunication {
|
|||||||
|
|
||||||
switch (stateNr) {
|
switch (stateNr) {
|
||||||
case 0:
|
case 0:
|
||||||
// Initialize data
|
|
||||||
currentScaleUserId = -1;
|
|
||||||
countRegisteredScaleUsers = -1;
|
|
||||||
maxRegisteredScaleUser = -1;
|
|
||||||
seenUsers = new TreeSet<>();
|
|
||||||
|
|
||||||
// Setup notification
|
// Setup notification
|
||||||
setNotificationOn(CUSTOM_SERVICE_1, CUSTOM_CHARACTERISTIC_WEIGHT,
|
setNotificationOn(CUSTOM_SERVICE_1, CUSTOM_CHARACTERISTIC_WEIGHT,
|
||||||
BluetoothGattUuid.DESCRIPTOR_CLIENT_CHARACTERISTIC_CONFIGURATION);
|
BluetoothGattUuid.DESCRIPTOR_CLIENT_CHARACTERISTIC_CONFIGURATION);
|
||||||
break;
|
break;
|
||||||
case 1:
|
case 1:
|
||||||
// Say "Hello" to the scale
|
// Say "Hello" to the scale and wait for ack
|
||||||
writeBytes(new byte[]{(byte) getAlternativeStartByte(6), (byte) 0x01});
|
sendAlternativeStartCode(ID_START_NIBBLE_INIT, (byte) 0x01);
|
||||||
|
pauseBtStateMachine();
|
||||||
break;
|
break;
|
||||||
case 2:
|
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;
|
break;
|
||||||
case 3:
|
case 3:
|
||||||
// Update timestamp of the scale
|
// Request scale status and wait for ack
|
||||||
updateDateTime();
|
sendCommand(CMD_SCALE_STATUS, encodeUserId(null));
|
||||||
|
pauseBtStateMachine();
|
||||||
break;
|
break;
|
||||||
case 4:
|
case 4:
|
||||||
// Set measurement unit
|
// Request list of all users and wait until all have been received
|
||||||
setUnitCommand();
|
sendCommand(CMD_USER_LIST);
|
||||||
|
pauseBtStateMachine();
|
||||||
break;
|
break;
|
||||||
case 5:
|
case 5:
|
||||||
// Wait for "unit" ack from scale
|
// If currentRemoteUser is null, indexOf returns -1 and index will be 0
|
||||||
break;
|
int index = remoteUsers.indexOf(currentRemoteUser) + 1;
|
||||||
case 6:
|
currentRemoteUser = null;
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Got all user acks
|
// Find the next remote user that exists locally
|
||||||
|
for (; index < remoteUsers.size(); ++index) {
|
||||||
// Check if not found/unknown
|
if (remoteUsers.get(index).localUserId != -1) {
|
||||||
if (currentScaleUserId == 0) {
|
currentRemoteUser = remoteUsers.get(index);
|
||||||
// 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);
|
|
||||||
break;
|
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;
|
break;
|
||||||
case 8:
|
case 8:
|
||||||
|
if (OpenScale.DEBUG_MODE) {
|
||||||
|
sendCommand(CMD_GET_UNKNOWN_MEASUREMENTS);
|
||||||
|
pauseBtStateMachine();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
postHandleRequest();
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
// Finish init if everything is done
|
// Finish init if everything is done
|
||||||
@@ -189,24 +242,17 @@ public class BluetoothBeurerSanitas extends BluetoothCommunication {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean nextBluetoothCmd(int stateNr) {
|
protected boolean nextBluetoothCmd(int stateNr) {
|
||||||
|
|
||||||
switch (stateNr) {
|
switch (stateNr) {
|
||||||
case 0:
|
case 0:
|
||||||
// If no specific user selected
|
if (!currentRemoteUser.isNew) {
|
||||||
if (currentScaleUserId == 0)
|
sendCommand(CMD_DO_MEASUREMENT, encodeUserId(currentRemoteUser));
|
||||||
break;
|
pauseBtStateMachine();
|
||||||
|
}
|
||||||
Timber.d("Request Saved User Measurements");
|
else {
|
||||||
writeBytes(new byte[]{
|
postHandleRequest();
|
||||||
(byte) startByte, (byte) 0x41, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, (byte) currentScaleUserId
|
}
|
||||||
});
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case 1:
|
case 1:
|
||||||
// Wait for user measurements to be received
|
|
||||||
setNextCmd(stateNr);
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
setBtMachineState(BT_MACHINE_STATE.BT_CLEANUP_STATE);
|
setBtMachineState(BT_MACHINE_STATE.BT_CLEANUP_STATE);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@@ -221,7 +267,7 @@ public class BluetoothBeurerSanitas extends BluetoothCommunication {
|
|||||||
switch (stateNr) {
|
switch (stateNr) {
|
||||||
case 0:
|
case 0:
|
||||||
// Force disconnect
|
// Force disconnect
|
||||||
writeBytes(new byte[]{(byte) 0xea, (byte) 0x02});
|
sendAlternativeStartCode(ID_START_NIBBLE_DISCONNECT, (byte) 0x02);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
@@ -232,264 +278,309 @@ public class BluetoothBeurerSanitas extends BluetoothCommunication {
|
|||||||
@Override
|
@Override
|
||||||
public void onBluetoothDataChange(BluetoothGatt bluetoothGatt, BluetoothGattCharacteristic gattCharacteristic) {
|
public void onBluetoothDataChange(BluetoothGatt bluetoothGatt, BluetoothGattCharacteristic gattCharacteristic) {
|
||||||
byte[] data = gattCharacteristic.getValue();
|
byte[] data = gattCharacteristic.getValue();
|
||||||
if (data.length == 0) {
|
if (data == null || data.length == 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((data[0] & 0xFF) == getAlternativeStartByte(6) && (data[1] & 0xFF) == 0x00) {
|
if (data[0] == getAlternativeStartByte(ID_START_NIBBLE_INIT)) {
|
||||||
Timber.d("ACK Scale is ready");
|
Timber.d("Got init ack from scale; scale is ready");
|
||||||
nextMachineStateStep();
|
resumeBtStateMachine();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((data[0] & 0xFF) == startByte && (data[1] & 0xFF) == 0xf0 && data[2] == 0x4d) {
|
if (data[0] != startByte) {
|
||||||
Timber.d("ACK Unit set");
|
Timber.e("Got unknown start byte 0x%02x", data[0]);
|
||||||
nextMachineStateStep();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((data[0] & 0xFF) == startByte && (data[1] & 0xFF) == 0xf0 && data[2] == 0x33) {
|
try {
|
||||||
Timber.d("ACK Got general user information");
|
switch (data[1]) {
|
||||||
|
case CMD_USER_INFO:
|
||||||
int count = (byte) (data[4] & 0xFF);
|
processUserInfo(data);
|
||||||
int maxUsers = (byte) (data[5] & 0xFF);
|
break;
|
||||||
Timber.d("Count: %d, maxUsers: %d", count, maxUsers);
|
case CMD_SAVED_MEASUREMENT:
|
||||||
|
processSavedMeasurement(data);
|
||||||
countRegisteredScaleUsers = count;
|
break;
|
||||||
// Check if any scale user is registered
|
case CMD_WEIGHT_MEASUREMENT:
|
||||||
if (count == 0) {
|
processWeightMeasurement(data);
|
||||||
currentScaleUserId = 0; // Unknown user
|
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;
|
|
||||||
}
|
}
|
||||||
|
catch (IndexOutOfBoundsException|NullPointerException e) {
|
||||||
if ((data[0] & 0xFF) == startByte && (data[1] & 0xFF) == 0x34) {
|
Timber.e(e);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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() {
|
private void processUserInfo(byte[] data) {
|
||||||
writeBytes(new byte[]{
|
final int count = data[2] & 0xFF;
|
||||||
(byte) startByte, (byte) 0x43, (byte) 0x0, (byte) 0x0, (byte) 0x0,
|
final int current = data[3] & 0xFF;
|
||||||
(byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) 0x0,
|
|
||||||
(byte) currentScaleUserId
|
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) {
|
private float getKiloGram(byte[] data, int offset) {
|
||||||
@@ -502,11 +593,7 @@ public class BluetoothBeurerSanitas extends BluetoothCommunication {
|
|||||||
return Converters.fromUnsignedInt16Be(data, offset) / 10.0f;
|
return Converters.fromUnsignedInt16Be(data, offset) / 10.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ScaleMeasurement parseScaleData(byte[] data) throws ParseException {
|
private void addMeasurement(byte[] data, int userId) {
|
||||||
if (data.length != 11 + 11) {
|
|
||||||
throw new ParseException("Parse scala data: unexpected length", 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
long timestamp = Converters.fromUnsignedInt32Be(data, 0) * 1000;
|
long timestamp = Converters.fromUnsignedInt32Be(data, 0) * 1000;
|
||||||
float weight = getKiloGram(data, 4);
|
float weight = getKiloGram(data, 4);
|
||||||
int impedance = Converters.fromUnsignedInt16Be(data, 6);
|
int impedance = Converters.fromUnsignedInt16Be(data, 6);
|
||||||
@@ -519,6 +606,7 @@ public class BluetoothBeurerSanitas extends BluetoothCommunication {
|
|||||||
float bmi = Converters.fromUnsignedInt16Be(data, 20) / 10.0f;
|
float bmi = Converters.fromUnsignedInt16Be(data, 20) / 10.0f;
|
||||||
|
|
||||||
ScaleMeasurement receivedMeasurement = new ScaleMeasurement();
|
ScaleMeasurement receivedMeasurement = new ScaleMeasurement();
|
||||||
|
receivedMeasurement.setUserId(userId);
|
||||||
receivedMeasurement.setDateTime(new Date(timestamp));
|
receivedMeasurement.setDateTime(new Date(timestamp));
|
||||||
receivedMeasurement.setWeight(weight);
|
receivedMeasurement.setWeight(weight);
|
||||||
receivedMeasurement.setFat(fat);
|
receivedMeasurement.setFat(fat);
|
||||||
@@ -526,42 +614,69 @@ public class BluetoothBeurerSanitas extends BluetoothCommunication {
|
|||||||
receivedMeasurement.setMuscle(muscle);
|
receivedMeasurement.setMuscle(muscle);
|
||||||
receivedMeasurement.setBone(bone);
|
receivedMeasurement.setBone(bone);
|
||||||
|
|
||||||
Timber.i("Measurement: %s, Impedance: %d, BMR: %d, AMR: %d, BMI: %.2f",
|
addScaleData(receivedMeasurement);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void writeBytes(byte[] data) {
|
private void writeBytes(byte[] data) {
|
||||||
writeBytes(CUSTOM_SERVICE_1, CUSTOM_CHARACTERISTIC_WEIGHT, 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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -48,7 +48,7 @@ public abstract class BluetoothCommunication {
|
|||||||
BT_CONNECTION_LOST, BT_NO_DEVICE_FOUND, BT_UNEXPECTED_ERROR, BT_SCALE_MESSAGE
|
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;
|
private static final long LE_SCAN_TIMEOUT_MS = 10 * 1000;
|
||||||
|
|
||||||
@@ -66,6 +66,7 @@ public abstract class BluetoothCommunication {
|
|||||||
private int initStepNr;
|
private int initStepNr;
|
||||||
private int cleanupStepNr;
|
private int cleanupStepNr;
|
||||||
private BT_MACHINE_STATE btMachineState;
|
private BT_MACHINE_STATE btMachineState;
|
||||||
|
private BT_MACHINE_STATE btPausedMachineState;
|
||||||
|
|
||||||
private class GattObjectValue <GattObject> {
|
private class GattObjectValue <GattObject> {
|
||||||
public final GattObject gattObject;
|
public final GattObject gattObject;
|
||||||
@@ -239,6 +240,29 @@ public abstract class BluetoothCommunication {
|
|||||||
handleRequests();
|
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.
|
* Write a byte array to a Bluetooth device.
|
||||||
*
|
*
|
||||||
@@ -530,7 +554,6 @@ public abstract class BluetoothCommunication {
|
|||||||
if (doCleanup) {
|
if (doCleanup) {
|
||||||
if (btMachineState != BT_MACHINE_STATE.BT_CLEANUP_STATE) {
|
if (btMachineState != BT_MACHINE_STATE.BT_CLEANUP_STATE) {
|
||||||
setBtMachineState(BT_MACHINE_STATE.BT_CLEANUP_STATE);
|
setBtMachineState(BT_MACHINE_STATE.BT_CLEANUP_STATE);
|
||||||
nextMachineStateStep();
|
|
||||||
}
|
}
|
||||||
handler.post(new Runnable() {
|
handler.post(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
@@ -572,6 +595,9 @@ public abstract class BluetoothCommunication {
|
|||||||
nextCleanUpCmd(cleanupStepNr);
|
nextCleanUpCmd(cleanupStepNr);
|
||||||
cleanupStepNr++;
|
cleanupStepNr++;
|
||||||
break;
|
break;
|
||||||
|
case BT_PAUSED_STATE:
|
||||||
|
Timber.d("PAUSED STATE");
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user