Skip to content

Commit

Permalink
Add support for pairing.
Browse files Browse the repository at this point in the history
This removes the need to hardcode the password. Currently, the device
password which is obtained by pairing is stored in memory only, which
means the scale must be paired again every time the app is restarted.
  • Loading branch information
maksverver committed Oct 7, 2018
1 parent 5136bc3 commit 827dbce
Show file tree
Hide file tree
Showing 2 changed files with 140 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import android.content.Context;
import android.support.annotation.Nullable;

import com.health.openscale.R;
import com.health.openscale.core.datatypes.ScaleMeasurement;

import java.util.Date;
Expand Down Expand Up @@ -48,8 +49,6 @@ public class BluetoothTrisaBodyAnalyze extends BluetoothCommunication {
// GATT service characteristics.
private static final UUID MEASUREMENT_CHARACTERISTIC_UUID =
UUID.fromString("00008a21-0000-1000-8000-00805f9b34fb");
private static final UUID APPEND_MEASUREMENT_CHARACTERISTIC_UUID =
UUID.fromString("00008a22-0000-1000-8000-00805f9b34fb");
private static final UUID DOWNLOAD_COMMAND_CHARACTERISTIC_UUID =
UUID.fromString("00008a81-0000-1000-8000-00805f9b34fb");
private static final UUID UPLOAD_COMMAND_CHARACTERISTIC_UUID =
Expand All @@ -66,10 +65,31 @@ public class BluetoothTrisaBodyAnalyze extends BluetoothCommunication {
private static final byte DOWNLOAD_INFORMATION_ENABLE_DISCONNECT_COMMAND = 0x22;

// Timestamp of 2010-01-01 00:00:00 UTC (or local time?)
private final long TIMESTAMP_OFFSET_SECONDS = 1262304000L;
private static final long TIMESTAMP_OFFSET_SECONDS = 1262304000L;

// TODO: don't hardcode this.
//private byte[] PASSWORD = new byte[]{(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00};
/**
* Broadcast id, which the scale will include in its Bluetooth alias. This must be set to some
* value to complete the pairing process (though the actual value doesn't seem to matter).
*/
private static final int BROADCAST_ID = 0;

/** Hardware address (i.e., Bluetooth mac) of the connected device. */
@Nullable
private String hwAddress;

/**
* Device password as a 32-bit integer, or {@code null} if the device password is unknown.
*
* <p>TODO: store this is in a database.</p>
*/
@Nullable
private static Integer password;

/**
* Indicates whether we are pairing. If this is {@code true} then we have written the
* set-broadcast-id command, and should disconnect after the write succeeds.
*/
private boolean pairing = false;

public BluetoothTrisaBodyAnalyze(Context context) {
super(context);
Expand All @@ -83,6 +103,7 @@ public String driverName() {
@Override
public void connect(String hwAddress) {
Timber.i("connect(\"%s\")", hwAddress);
this.hwAddress = hwAddress;
super.connect(hwAddress);
}

Expand All @@ -97,38 +118,55 @@ protected boolean nextInitCmd(int stateNr) {
Timber.i("nextInitCmd(%d)", stateNr);
switch (stateNr) {
case 0:
// Register for notifications of the measurement characteristic.
setIndicationOn(
WEIGHT_SCALE_SERVICE_UUID,
MEASUREMENT_CHARACTERISTIC_UUID,
CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR_UUID);
return true;
return true; // more commands follow
case 1:
// Register for notifications of the command upload characteristic.
//
// This is the last init command, which causes a switch to the main state machine
// immediately after. This is important because we should be in the main state
// to handle pairing correctly.
setIndicationOn(
WEIGHT_SCALE_SERVICE_UUID,
UPLOAD_COMMAND_CHARACTERISTIC_UUID,
CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR_UUID);
return true;

// falls through
default:
return false;
return false; // no more commands
}
}

@Override
protected boolean nextBluetoothCmd(int stateNr) {
Timber.i("nextBluetoothCmd(%d)", stateNr);
return false;
switch (stateNr) {
case 0:
default:
return false; // no more commands

case 1:
// This state is triggered by the write in onPasswordReceived()
if (pairing) {
pairing = false;
disconnect(true);
}
return false; // no more commands;
}
}

@Override
protected boolean nextCleanUpCmd(int stateNr) {
Timber.i("nextCleanUpCmd(%d)", stateNr);
switch (stateNr) {
case 0:
writeCommand(disconnectCommand());
return true;
writeCommand(DOWNLOAD_INFORMATION_ENABLE_DISCONNECT_COMMAND);
// falls through
default:
return false;
return false; // no more commands
}
}

Expand All @@ -137,55 +175,107 @@ protected void onBluetoothDataChange(BluetoothGatt bluetoothGatt, BluetoothGattC
UUID characteristicUud = gattCharacteristic.getUuid();
byte[] value = gattCharacteristic.getValue();
Timber.i("onBluetoothdataChange() characteristic=%s value=%s", characteristicUud, byteInHex(value));
byte commandByte = value.length > 0 ? value[0] : 0;
if (UPLOAD_COMMAND_CHARACTERISTIC_UUID.equals(characteristicUud)) {
switch (commandByte) {
if (value.length == 0) {
Timber.e("Missing command byte!");
return;
}
byte command = value[0];
switch (command) {
case UPLOAD_PASSWORD:
// TODO: support pairing, then store this somewhere.
onPasswordReceived(value);
break;
case UPLOAD_CHALLENGE:
if (value.length < 5) {
break;
}
byte[] authCommand = new byte[] {
DOWNLOAD_INFORMATION_RESULT_COMMAND,
(byte)(value[1] ^ PASSWORD[0]),
(byte)(value[2] ^ PASSWORD[1]),
(byte)(value[3] ^ PASSWORD[2]),
(byte)(value[4] ^ PASSWORD[3])};
writeCommand(authCommand);
int timestamp = (int)(System.currentTimeMillis()/1000 - TIMESTAMP_OFFSET_SECONDS);
byte[] setUtcCommand = new byte[]{
DOWNLOAD_INFORMATION_UTC_COMMAND,
(byte)(timestamp >> 0),
(byte)(timestamp >> 8),
(byte)(timestamp >> 16),
(byte)(timestamp >> 24),
};
writeCommand(setUtcCommand);
return;
onChallengeReceived(value);
break;
default:
Timber.e("Unknown command byte received: %d", command);
}
return;
}
if (MEASUREMENT_CHARACTERISTIC_UUID.equals(characteristicUud)) {
onScaleMeasurumentReceived(value);
return;
}
Timber.e("Unknown characteristic changed: %s", characteristicUud);
}

} else if (MEASUREMENT_CHARACTERISTIC_UUID.equals(characteristicUud)) {
ScaleMeasurement scaleMeasurement = parseScaleMeasurementData(value);
if (scaleMeasurement != null) {
addScaleData(scaleMeasurement);
return;
}
private void onPasswordReceived(byte[] data) {
if (data.length < 5) {
Timber.e("Password data too short");
return;
}
Timber.w("Unhandled data!");
int newPassword = getInt32(data, 1);
if (password != null && password != newPassword) {
Timber.w("Replacing old password '%08x'", password);
}
Timber.i("Storing password '%08x'", newPassword);
password = newPassword;

sendMessage(R.string.trisa_scale_pairing_succeeded, null);

// To complete the pairing process, we must set the scale's broadcast id, and then
// disconnect. The writeCommand() call below will trigger the next state machine transition,
// which will disconnect when `pairing == true`.
pairing = true;
writeCommand(DOWNLOAD_INFORMATION_BROADCAST_ID_COMMAND, BROADCAST_ID);
}

private byte[] disconnectCommand() {
return new byte[]{DOWNLOAD_INFORMATION_ENABLE_DISCONNECT_COMMAND};
private void onChallengeReceived(byte[] data) {
if (data.length < 5) {
Timber.e("Challenge data too short");
return;
}
if (password == null) {
Timber.w("Received challenge, but password is unknown.");
sendMessage(R.string.trisa_scale_not_paired, null);
disconnect(true);
return;
}
int challenge = getInt32(data, 1);
int response = challenge ^ password;
writeCommand(DOWNLOAD_INFORMATION_RESULT_COMMAND, response);
int timestamp = (int)(System.currentTimeMillis()/1000 - TIMESTAMP_OFFSET_SECONDS);
writeCommand(DOWNLOAD_INFORMATION_UTC_COMMAND, timestamp);
}

private void onScaleMeasurumentReceived(byte[] data) {
ScaleMeasurement scaleMeasurement = parseScaleMeasurementData(data);
if (scaleMeasurement == null) {
Timber.e("Failed to parse scale measure measurement data: %s", byteInHex(data));
return;
}
addScaleData(scaleMeasurement);
}

/** Write a single command byte, without any arguments. */
private void writeCommand(byte commandByte) {
writeCommandBytes(new byte[]{commandByte});
}

/**
* Write a command with a 32-bit integer argument.
*
* <p>The command string consists of the command byte followed by 4 bytes: the argument
* encoded in little-endian byte order.</p>
*/
private void writeCommand(byte commandByte, int argument) {
writeCommandBytes(new byte[]{
commandByte,
(byte) (argument >> 0),
(byte) (argument >> 8),
(byte) (argument >> 16),
(byte) (argument >> 24),
});
}

private void writeCommand(byte[] bytes) {
private void writeCommandBytes(byte[] bytes) {
Timber.d("writeCommand bytes=%s", byteInHex(bytes));
writeBytes(WEIGHT_SCALE_SERVICE_UUID, DOWNLOAD_COMMAND_CHARACTERISTIC_UUID, bytes);
}

@Nullable
private ScaleMeasurement parseScaleMeasurementData(byte[] data) {
private static ScaleMeasurement parseScaleMeasurementData(byte[] data) {
// Byte 0 contains info.
// Byte 1-4 contains weight.
// Byte 5-8 contains timestamp, if bit 0 in info byte is set.
Expand Down Expand Up @@ -216,7 +306,7 @@ private static int getInt32(byte[] data, int offset) {
* <p>The first three little-endian bytes form the 24-bit mantissa. The last byte contains the
* signed exponent, applied in base 10.
*/
private double getBase10Float(byte[] data, int offset) {
private static double getBase10Float(byte[] data, int offset) {
int mantissa = (data[offset] & 0xff) | ((data[offset + 1] & 0xff) << 8) |
((data[offset + 2] & 0xff) << 16);
int exponent = data[offset + 3]; // note: byte is signed.
Expand Down
2 changes: 2 additions & 0 deletions android_app/app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,8 @@
<string name="error_max_scale_users">Max. number of concurrent scale users reached</string>
<string name="info_step_on_scale">Please step barefoot on the scale for reference measurements</string>
<string name="info_measuring">Measuring weight: %.2f</string>
<string name="trisa_scale_not_paired">This scale has not been paired!\n\nHold the button on the bottom of the scale to switch it to pairing mode, and then reconnect to retrieve the device password.</string>
<string name="trisa_scale_pairing_succeeded">Pairing succeeded!\n\nReconnect to retrieve measurement data.</string>

<string name="customactivityoncrash_error_activity_error_occurred_explanation">An unexpected error occurred.\n\nPlease create a new issue including the error details on\nhttps://github.com/oliexdev/openScale/issues</string>
<string name="customactivityoncrash_error_activity_restart_app">Restart app</string>
Expand Down

0 comments on commit 827dbce

Please sign in to comment.