mirror of
https://github.com/oliexdev/openScale.git
synced 2025-08-29 02:59:57 +02:00
Move static helper methods to TrisaBodyAnalyzeLib and add unit tests for them.
This commit is contained in:
@@ -25,13 +25,13 @@ import android.support.annotation.Nullable;
|
|||||||
import com.health.openscale.R;
|
import com.health.openscale.R;
|
||||||
import com.health.openscale.core.datatypes.ScaleMeasurement;
|
import com.health.openscale.core.datatypes.ScaleMeasurement;
|
||||||
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
import static com.health.openscale.core.bluetooth.lib.TrisaBodyAnalyzeLib.convertJavaTimestampToDevice;
|
||||||
import static java.util.concurrent.TimeUnit.SECONDS;
|
import static com.health.openscale.core.bluetooth.lib.TrisaBodyAnalyzeLib.getInt32;
|
||||||
|
import static com.health.openscale.core.bluetooth.lib.TrisaBodyAnalyzeLib.parseScaleMeasurementData;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Driver for Trisa Body Analyze 4.0.
|
* Driver for Trisa Body Analyze 4.0.
|
||||||
@@ -66,9 +66,6 @@ public class BluetoothTrisaBodyAnalyze extends BluetoothCommunication {
|
|||||||
private static final byte DOWNLOAD_INFORMATION_BROADCAST_ID_COMMAND = 0x21;
|
private static final byte DOWNLOAD_INFORMATION_BROADCAST_ID_COMMAND = 0x21;
|
||||||
private static final byte DOWNLOAD_INFORMATION_ENABLE_DISCONNECT_COMMAND = 0x22;
|
private static final byte DOWNLOAD_INFORMATION_ENABLE_DISCONNECT_COMMAND = 0x22;
|
||||||
|
|
||||||
// Timestamp of 2010-01-01 00:00:00 UTC (or local time?)
|
|
||||||
private static final long TIMESTAMP_OFFSET_SECONDS = 1262304000L;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Broadcast id, which the scale will include in its Bluetooth alias. This must be set to some
|
* 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).
|
* value to complete the pairing process (though the actual value doesn't seem to matter).
|
||||||
@@ -250,8 +247,8 @@ public class BluetoothTrisaBodyAnalyze extends BluetoothCommunication {
|
|||||||
int challenge = getInt32(data, 1);
|
int challenge = getInt32(data, 1);
|
||||||
int response = challenge ^ password;
|
int response = challenge ^ password;
|
||||||
writeCommand(DOWNLOAD_INFORMATION_RESULT_COMMAND, response);
|
writeCommand(DOWNLOAD_INFORMATION_RESULT_COMMAND, response);
|
||||||
int timestamp = (int)(System.currentTimeMillis()/1000 - TIMESTAMP_OFFSET_SECONDS);
|
int deviceTimestamp = convertJavaTimestampToDevice(System.currentTimeMillis());
|
||||||
writeCommand(DOWNLOAD_INFORMATION_UTC_COMMAND, timestamp);
|
writeCommand(DOWNLOAD_INFORMATION_UTC_COMMAND, deviceTimestamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onScaleMeasurumentReceived(byte[] data) {
|
private void onScaleMeasurumentReceived(byte[] data) {
|
||||||
@@ -289,45 +286,6 @@ public class BluetoothTrisaBodyAnalyze extends BluetoothCommunication {
|
|||||||
writeBytes(WEIGHT_SCALE_SERVICE_UUID, DOWNLOAD_COMMAND_CHARACTERISTIC_UUID, bytes);
|
writeBytes(WEIGHT_SCALE_SERVICE_UUID, DOWNLOAD_COMMAND_CHARACTERISTIC_UUID, bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
|
||||||
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.
|
|
||||||
// Check that we have at least weight & timestamp, which is the minimum information that
|
|
||||||
// ScaleMeasurement needs.
|
|
||||||
if (data.length < 9 || (data[0] & 1) == 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
double weight = getBase10Float(data, 1);
|
|
||||||
long timestamp_seconds = TIMESTAMP_OFFSET_SECONDS + (long)getInt32(data, 5);
|
|
||||||
|
|
||||||
ScaleMeasurement measurement = new ScaleMeasurement();
|
|
||||||
measurement.setDateTime(new Date(MILLISECONDS.convert(timestamp_seconds, SECONDS)));
|
|
||||||
measurement.setWeight((float)weight);
|
|
||||||
// TODO: calculate body composition (if possible) and set those fields too
|
|
||||||
return measurement;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Converts 4 little-endian bytes to a 32-bit integer. */
|
|
||||||
private static int getInt32(byte[] data, int offset) {
|
|
||||||
return (data[offset] & 0xff) | ((data[offset + 1] & 0xff) << 8) |
|
|
||||||
((data[offset + 2] & 0xff) << 16) | ((data[offset + 3] & 0xff) << 24);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Converts 4 bytes to a floating point number.
|
|
||||||
*
|
|
||||||
* <p>The first three little-endian bytes form the 24-bit mantissa. The last byte contains the
|
|
||||||
* signed exponent, applied in base 10.
|
|
||||||
*/
|
|
||||||
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.
|
|
||||||
return mantissa * Math.pow(10, exponent);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String getDevicePasswordKey(String deviceId) {
|
private static String getDevicePasswordKey(String deviceId) {
|
||||||
return SHARED_PREFERENCES_PASSWORD_KEY_PREFIX + deviceId;
|
return SHARED_PREFERENCES_PASSWORD_KEY_PREFIX + deviceId;
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,92 @@
|
|||||||
|
/* Copyright (C) 2018 Maks Verver <maks@verver.ch>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
*/
|
||||||
|
package com.health.openscale.core.bluetooth.lib;
|
||||||
|
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
|
||||||
|
import com.health.openscale.core.datatypes.ScaleMeasurement;
|
||||||
|
|
||||||
|
import java.time.Clock;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
||||||
|
import static java.util.concurrent.TimeUnit.SECONDS;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class with static helper methods. This is a separate class for testing purposes.
|
||||||
|
*
|
||||||
|
* @see com.health.openscale.core.bluetooth.BluetoothTrisaBodyAnalyze
|
||||||
|
*/
|
||||||
|
public class TrisaBodyAnalyzeLib {
|
||||||
|
|
||||||
|
// Timestamp of 2010-01-01 00:00:00 UTC (or local time?)
|
||||||
|
private static final long TIMESTAMP_OFFSET_SECONDS = 1262304000L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts 4 little-endian bytes to a 32-bit integer, starting from {@code offset}.
|
||||||
|
*
|
||||||
|
* @throws IndexOutOfBoundsException if {@code offset < 0} or {@code offset + 4> data.length}
|
||||||
|
*/
|
||||||
|
public static int getInt32(byte[] data, int offset) {
|
||||||
|
return (data[offset] & 0xff) | ((data[offset + 1] & 0xff) << 8) |
|
||||||
|
((data[offset + 2] & 0xff) << 16) | ((data[offset + 3] & 0xff) << 24);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Converts 4 bytes to a floating point number, starting from {@code offset}.
|
||||||
|
*
|
||||||
|
* <p>The first three little-endian bytes form the 24-bit mantissa. The last byte contains the
|
||||||
|
* signed exponent, applied in base 10.
|
||||||
|
*
|
||||||
|
* @throws IndexOutOfBoundsException if {@code offset < 0} or {@code offset + 4> data.length}
|
||||||
|
*/
|
||||||
|
public 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.
|
||||||
|
return mantissa * Math.pow(10, exponent);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int convertJavaTimestampToDevice(long javaTimestampMillis) {
|
||||||
|
return (int)((javaTimestampMillis + 500)/1000 - TIMESTAMP_OFFSET_SECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static long convertDeviceTimestampToJava(int deviceTimestampSeconds) {
|
||||||
|
return 1000 * (TIMESTAMP_OFFSET_SECONDS + (long)deviceTimestampSeconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public 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.
|
||||||
|
// Check that we have at least weight & timestamp, which is the minimum information that
|
||||||
|
// ScaleMeasurement needs.
|
||||||
|
if (data.length < 9 || (data[0] & 1) == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
double weight = getBase10Float(data, 1);
|
||||||
|
int deviceTimestamp = getInt32(data, 5);
|
||||||
|
|
||||||
|
ScaleMeasurement measurement = new ScaleMeasurement();
|
||||||
|
measurement.setDateTime(new Date(convertDeviceTimestampToJava(deviceTimestamp)));
|
||||||
|
measurement.setWeight((float)weight);
|
||||||
|
// TODO: calculate body composition (if possible) and set those fields too
|
||||||
|
return measurement;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TrisaBodyAnalyzeLib() {}
|
||||||
|
}
|
@@ -0,0 +1,138 @@
|
|||||||
|
package com.health.openscale;
|
||||||
|
|
||||||
|
import com.health.openscale.core.datatypes.ScaleMeasurement;
|
||||||
|
|
||||||
|
import junit.framework.Assert;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
import static com.health.openscale.core.bluetooth.lib.TrisaBodyAnalyzeLib.convertDeviceTimestampToJava;
|
||||||
|
import static com.health.openscale.core.bluetooth.lib.TrisaBodyAnalyzeLib.convertJavaTimestampToDevice;
|
||||||
|
import static com.health.openscale.core.bluetooth.lib.TrisaBodyAnalyzeLib.getBase10Float;
|
||||||
|
import static com.health.openscale.core.bluetooth.lib.TrisaBodyAnalyzeLib.getInt32;
|
||||||
|
import static com.health.openscale.core.bluetooth.lib.TrisaBodyAnalyzeLib.parseScaleMeasurementData;
|
||||||
|
import static junit.framework.Assert.assertEquals;
|
||||||
|
|
||||||
|
/** Unit tests for {@link com.health.openscale.core.bluetooth.lib.TrisaBodyAnalyzeLib}.*/
|
||||||
|
public class TrisaBodyAnalyzeLibTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getInt32Tests() {
|
||||||
|
byte[] data = new byte[]{1, 2, 3, 4, 5, 6};
|
||||||
|
assertEquals(0x04030201, getInt32(data, 0));
|
||||||
|
assertEquals(0x05040302, getInt32(data, 1));
|
||||||
|
assertEquals(0x06050403, getInt32(data, 2));
|
||||||
|
|
||||||
|
assertEquals(0xa7bdd385, getInt32(new byte[]{-123, -45, -67, -89}, 0));
|
||||||
|
|
||||||
|
assertThrows(IndexOutOfBoundsException.class, getInt32Runnable(data, -1));
|
||||||
|
assertThrows(IndexOutOfBoundsException.class, getInt32Runnable(data, 5));
|
||||||
|
assertThrows(IndexOutOfBoundsException.class, getInt32Runnable(new byte[]{1,2,3}, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getBase10FloatTests() {
|
||||||
|
double eps = 1e-9; // margin of error for inexact floating point comparisons
|
||||||
|
assertEquals(0.0, getBase10Float(new byte[]{0, 0, 0, 0}, 0));
|
||||||
|
assertEquals(0.0, getBase10Float(new byte[]{0, 0, 0, -1}, 0));
|
||||||
|
assertEquals(76.1, getBase10Float(new byte[]{-70, 29, 0, -2}, 0), eps);
|
||||||
|
assertEquals(1234.5678, getBase10Float(new byte[]{78, 97, -68, -4}, 0), eps);
|
||||||
|
assertEquals(12345678e127, getBase10Float(new byte[]{78, 97, -68, 127}, 0));
|
||||||
|
assertEquals(12345678e-128, getBase10Float(new byte[]{78, 97, -68, -128}, 0), eps);
|
||||||
|
|
||||||
|
byte[] data = new byte[]{1,2,3,4,5};
|
||||||
|
assertEquals(0x030201*1e4, getBase10Float(data, 0));
|
||||||
|
assertEquals(0x040302*1e5, getBase10Float(data, 1));
|
||||||
|
|
||||||
|
assertThrows(IndexOutOfBoundsException.class, getBase10FloatRunnable(data, -1));
|
||||||
|
assertThrows(IndexOutOfBoundsException.class, getBase10FloatRunnable(data, 5));
|
||||||
|
assertThrows(IndexOutOfBoundsException.class, getBase10FloatRunnable(new byte[]{1,2,3}, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void convertJavaTimestampToDeviceTests() {
|
||||||
|
assertEquals(275852082, convertJavaTimestampToDevice(1538156082000L));
|
||||||
|
|
||||||
|
// Rounds down.
|
||||||
|
assertEquals(275852082, convertJavaTimestampToDevice(1538156082499L));
|
||||||
|
|
||||||
|
// Rounds up.
|
||||||
|
assertEquals(275852083, convertJavaTimestampToDevice(1538156082500L));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void convertDeviceTimestampToJavaTests() {
|
||||||
|
assertEquals(1538156082000L, convertDeviceTimestampToJava(275852082));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void parseScaleMeasurementDataTests() {
|
||||||
|
long expected_timestamp_seconds = 1538156082L; // Fri Sep 28 17:34:42 UTC 2018
|
||||||
|
byte[] bytes = hexToBytes("9f:ba:1d:00:fe:32:2b:71:10:00:00:00:ff:8d:14:00:ff:00:09:00");
|
||||||
|
|
||||||
|
ScaleMeasurement measurement = parseScaleMeasurementData(bytes);
|
||||||
|
|
||||||
|
assertEquals(measurement.getWeight(), 76.1f, 1e-6f);
|
||||||
|
assertEquals(new Date(expected_timestamp_seconds * 1000), measurement.getDateTime());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a {@link Runnable} that will call getInt32(). In Java 8, this can be done more
|
||||||
|
* easily with a lambda expression at the call site, but we are using Java 7.
|
||||||
|
*/
|
||||||
|
private static Runnable getInt32Runnable(final byte[] data, final int offset) {
|
||||||
|
return new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
getInt32(data, offset);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a {@link Runnable} that will call getBase10Float(). In Java 8, this can be done more
|
||||||
|
* easily with a lambda expression at the call site, but we are using Java 7.
|
||||||
|
*/
|
||||||
|
private static Runnable getBase10FloatRunnable(final byte[] data, final int offset) {
|
||||||
|
return new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
getBase10Float(data, offset);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs the given {@link Runnable} and verifies that it throws an exception of class {@code
|
||||||
|
* exceptionClass}. If it does, the exception will be caught and returned. If it does not (i.e.
|
||||||
|
* the runnable throws no exception, or throws an exception of a different class), then {@link
|
||||||
|
* Assert#fail} is called to abort the test.
|
||||||
|
*/
|
||||||
|
private static <T extends Throwable> T assertThrows(Class<T> exceptionClass, Runnable run) {
|
||||||
|
try {
|
||||||
|
run.run();
|
||||||
|
Assert.fail("Expected an exception to be thrown.");
|
||||||
|
} catch (Throwable t) {
|
||||||
|
if (exceptionClass.isInstance(t)) {
|
||||||
|
return exceptionClass.cast(t);
|
||||||
|
}
|
||||||
|
Assert.fail("Wrong kind of exception was thrown; expected " + exceptionClass + ", received " + t.getClass());
|
||||||
|
}
|
||||||
|
return null; // unreachable, because Assert.fail() throws an exception
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Parses a colon-separated hex-encoded string like "aa:bb:cc:dd" into an array of bytes. */
|
||||||
|
private byte[] hexToBytes(String s) {
|
||||||
|
String[] parts = s.split(":");
|
||||||
|
byte[] bytes = new byte[parts.length];
|
||||||
|
for (int i = 0; i < bytes.length; ++i) {
|
||||||
|
if (parts[i].length() != 2) {
|
||||||
|
throw new IllegalArgumentException();
|
||||||
|
}
|
||||||
|
bytes[i] = (byte)Integer.parseInt(parts[i], 16);
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user