From 827dbced6e8b08b14501bb6e32be3b2ea183962f Mon Sep 17 00:00:00 2001 From: Maks Verver Date: Sat, 6 Oct 2018 15:57:09 +0200 Subject: [PATCH] Add support for pairing. 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. --- .../bluetooth/BluetoothTrisaBodyAnalyze.java | 186 +++++++++++++----- .../app/src/main/res/values/strings.xml | 2 + 2 files changed, 140 insertions(+), 48 deletions(-) diff --git a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothTrisaBodyAnalyze.java b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothTrisaBodyAnalyze.java index 1ab1d2fc9..9ea340555 100644 --- a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothTrisaBodyAnalyze.java +++ b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothTrisaBodyAnalyze.java @@ -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; @@ -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 = @@ -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. + * + *

TODO: store this is in a database.

+ */ + @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); @@ -83,6 +103,7 @@ public String driverName() { @Override public void connect(String hwAddress) { Timber.i("connect(\"%s\")", hwAddress); + this.hwAddress = hwAddress; super.connect(hwAddress); } @@ -97,27 +118,44 @@ 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 @@ -125,10 +163,10 @@ 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 } } @@ -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. + * + *

The command string consists of the command byte followed by 4 bytes: the argument + * encoded in little-endian byte order.

+ */ + 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. @@ -216,7 +306,7 @@ private static int getInt32(byte[] data, int offset) { *

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. diff --git a/android_app/app/src/main/res/values/strings.xml b/android_app/app/src/main/res/values/strings.xml index 8783c2d1b..e3f18afe3 100644 --- a/android_app/app/src/main/res/values/strings.xml +++ b/android_app/app/src/main/res/values/strings.xml @@ -169,6 +169,8 @@ Max. number of concurrent scale users reached Please step barefoot on the scale for reference measurements Measuring weight: %.2f + 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. + Pairing succeeded!\n\nReconnect to retrieve measurement data. An unexpected error occurred.\n\nPlease create a new issue including the error details on\nhttps://github.com/oliexdev/openScale/issues Restart app