mirror of
https://github.com/oliexdev/openScale.git
synced 2025-08-28 10:40:47 +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.core.datatypes.ScaleMeasurement;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.UUID;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
||||
import static java.util.concurrent.TimeUnit.SECONDS;
|
||||
import static com.health.openscale.core.bluetooth.lib.TrisaBodyAnalyzeLib.convertJavaTimestampToDevice;
|
||||
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.
|
||||
@@ -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_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
|
||||
* 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 response = challenge ^ password;
|
||||
writeCommand(DOWNLOAD_INFORMATION_RESULT_COMMAND, response);
|
||||
int timestamp = (int)(System.currentTimeMillis()/1000 - TIMESTAMP_OFFSET_SECONDS);
|
||||
writeCommand(DOWNLOAD_INFORMATION_UTC_COMMAND, timestamp);
|
||||
int deviceTimestamp = convertJavaTimestampToDevice(System.currentTimeMillis());
|
||||
writeCommand(DOWNLOAD_INFORMATION_UTC_COMMAND, deviceTimestamp);
|
||||
}
|
||||
|
||||
private void onScaleMeasurumentReceived(byte[] data) {
|
||||
@@ -289,45 +286,6 @@ public class BluetoothTrisaBodyAnalyze extends BluetoothCommunication {
|
||||
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) {
|
||||
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