mirror of
https://github.com/oliexdev/openScale.git
synced 2025-08-20 07:21:40 +02:00
Use resistance measurement to calculate body composition data.
Coëfficients were obtained by reverse-engineering the discontinued Trisa Vital Bluetooth app. The resulting figures do not seem to be very accurate.
This commit is contained in:
@@ -23,7 +23,9 @@ import android.preference.PreferenceManager;
|
|||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
|
|
||||||
import com.health.openscale.R;
|
import com.health.openscale.R;
|
||||||
|
import com.health.openscale.core.OpenScale;
|
||||||
import com.health.openscale.core.datatypes.ScaleMeasurement;
|
import com.health.openscale.core.datatypes.ScaleMeasurement;
|
||||||
|
import com.health.openscale.core.datatypes.ScaleUser;
|
||||||
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@@ -252,7 +254,8 @@ public class BluetoothTrisaBodyAnalyze extends BluetoothCommunication {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void onScaleMeasurumentReceived(byte[] data) {
|
private void onScaleMeasurumentReceived(byte[] data) {
|
||||||
ScaleMeasurement scaleMeasurement = parseScaleMeasurementData(data);
|
ScaleUser user = OpenScale.getInstance().getSelectedScaleUser();
|
||||||
|
ScaleMeasurement scaleMeasurement = parseScaleMeasurementData(data, user);
|
||||||
if (scaleMeasurement == null) {
|
if (scaleMeasurement == null) {
|
||||||
Timber.e("Failed to parse scale measure measurement data: %s", byteInHex(data));
|
Timber.e("Failed to parse scale measure measurement data: %s", byteInHex(data));
|
||||||
return;
|
return;
|
||||||
|
@@ -18,6 +18,8 @@ package com.health.openscale.core.bluetooth.lib;
|
|||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
|
|
||||||
import com.health.openscale.core.datatypes.ScaleMeasurement;
|
import com.health.openscale.core.datatypes.ScaleMeasurement;
|
||||||
|
import com.health.openscale.core.datatypes.ScaleUser;
|
||||||
|
import com.health.openscale.core.utils.Converters;
|
||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
@@ -64,25 +66,75 @@ public class TrisaBodyAnalyzeLib {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
public static ScaleMeasurement parseScaleMeasurementData(byte[] data) {
|
public static ScaleMeasurement parseScaleMeasurementData(byte[] data, @Nullable ScaleUser user) {
|
||||||
// Byte 0 contains info.
|
// data contains:
|
||||||
// Byte 1-4 contains weight.
|
//
|
||||||
// Byte 5-8 contains timestamp, if bit 0 in info byte is set.
|
// 1 byte: info about presence of other fields:
|
||||||
|
// bit 0: timestamp
|
||||||
|
// bit 1: resistance1
|
||||||
|
// bit 2: resistance2
|
||||||
|
// (other bits aren't used here)
|
||||||
|
// 4 bytes: weight
|
||||||
|
// 4 bytes: timestamp (if info bit 0 is set)
|
||||||
|
// 4 bytes: resistance1 (if info bit 1 is set)
|
||||||
|
// 4 bytes: resistance2 (if info bit 2 is set)
|
||||||
|
// (following fields aren't used here)
|
||||||
|
|
||||||
// Check that we have at least weight & timestamp, which is the minimum information that
|
// Check that we have at least weight & timestamp, which is the minimum information that
|
||||||
// ScaleMeasurement needs.
|
// ScaleMeasurement needs.
|
||||||
if (data.length < 9 || (data[0] & 1) == 0) {
|
if (data.length < 9) {
|
||||||
|
return null; // data is too short
|
||||||
|
}
|
||||||
|
byte infoByte = data[0];
|
||||||
|
boolean hasTimestamp = (infoByte & 1) == 1;
|
||||||
|
boolean hasResistance1 = (infoByte & 2) == 2;
|
||||||
|
boolean hasResistance2 = (infoByte & 4) == 4;
|
||||||
|
if (!hasTimestamp) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
double weightKg = getBase10Float(data, 1);
|
||||||
double weight = getBase10Float(data, 1);
|
|
||||||
int deviceTimestamp = getInt32(data, 5);
|
int deviceTimestamp = getInt32(data, 5);
|
||||||
|
|
||||||
ScaleMeasurement measurement = new ScaleMeasurement();
|
ScaleMeasurement measurement = new ScaleMeasurement();
|
||||||
measurement.setDateTime(new Date(convertDeviceTimestampToJava(deviceTimestamp)));
|
measurement.setDateTime(new Date(convertDeviceTimestampToJava(deviceTimestamp)));
|
||||||
measurement.setWeight((float)weight);
|
measurement.setWeight((float) weightKg);
|
||||||
// TODO: calculate body composition (if possible) and set those fields too
|
|
||||||
|
// Only resistance 2 is used; resistance 1 is 0, even if it is present.
|
||||||
|
int resistance2Offset = 9 + (hasResistance1 ? 4 : 0);
|
||||||
|
if (hasResistance2 && resistance2Offset + 4 <= data.length && isValidUser(user)) {
|
||||||
|
// Calculate body composition statistics from measured weight & resistance, combined
|
||||||
|
// with age, height and sex from the user profile. The accuracy of the resulting figures
|
||||||
|
// is questionable, but it's better than nothing. Even if the absolute numbers aren't
|
||||||
|
// very meaningful, it might still be useful to track changes over time.
|
||||||
|
double resistance2 = getBase10Float(data, resistance2Offset);
|
||||||
|
int ageYears = user.getAge();
|
||||||
|
double heightCm = Converters.toCentimeter(user.getBodyHeight(), user.getMeasureUnit());
|
||||||
|
boolean isMale = user.getGender().isMale();
|
||||||
|
double impedance = resistance2 < 410 ? 3.0 : 0.3 * (resistance2 - 400);
|
||||||
|
double bmi = weightKg * 1e4 / (heightCm * heightCm);
|
||||||
|
double fat = isMale
|
||||||
|
? bmi * (1.479 + 4.4e-4 * impedance) + 0.1 * ageYears - 21.764
|
||||||
|
: bmi * (1.506 + 3.908e-4 * impedance) + 0.1 * ageYears - 12.834;
|
||||||
|
double water = isMale
|
||||||
|
? 87.51 + (-1.162 * bmi - 0.00813 * impedance + 0.07594 * ageYears)
|
||||||
|
: 77.721 + (-1.148 * bmi - 0.00573 * impedance + 0.06448 * ageYears);
|
||||||
|
double muscle = isMale
|
||||||
|
? 74.627 + (-0.811 * bmi - 0.00565 * impedance - 0.367 * ageYears)
|
||||||
|
: 57.0 + (-0.694 * bmi - 0.00344 * impedance - 0.255 * ageYears);
|
||||||
|
double bone = isMale
|
||||||
|
? 7.829 + (-0.0855 * bmi - 5.92e-4 * impedance - 0.0389 * ageYears)
|
||||||
|
: 7.98 + (-0.0973 * bmi - 4.84e-4 * impedance - 0.036 * ageYears);
|
||||||
|
measurement.setFat((float) fat);
|
||||||
|
measurement.setWater((float) water);
|
||||||
|
measurement.setMuscle((float) muscle);
|
||||||
|
measurement.setBone((float) bone);
|
||||||
|
}
|
||||||
return measurement;
|
return measurement;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean isValidUser(@Nullable ScaleUser user) {
|
||||||
|
return user != null && user.getAge() > 0 && user.getBodyHeight() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
private TrisaBodyAnalyzeLib() {}
|
private TrisaBodyAnalyzeLib() {}
|
||||||
}
|
}
|
||||||
|
@@ -1,12 +1,16 @@
|
|||||||
package com.health.openscale;
|
package com.health.openscale;
|
||||||
|
|
||||||
import com.health.openscale.core.datatypes.ScaleMeasurement;
|
import com.health.openscale.core.datatypes.ScaleMeasurement;
|
||||||
|
import com.health.openscale.core.datatypes.ScaleUser;
|
||||||
|
import com.health.openscale.core.utils.Converters;
|
||||||
|
|
||||||
import junit.framework.Assert;
|
import junit.framework.Assert;
|
||||||
|
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.Calendar;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
import java.util.GregorianCalendar;
|
||||||
|
|
||||||
import static com.health.openscale.core.bluetooth.lib.TrisaBodyAnalyzeLib.convertDeviceTimestampToJava;
|
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.convertJavaTimestampToDevice;
|
||||||
@@ -68,14 +72,49 @@ public class TrisaBodyAnalyzeLibTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void parseScaleMeasurementDataTests() {
|
public void parseScaleMeasurementData_validUserData() {
|
||||||
|
long expected_timestamp_seconds = 1539205852L; // Wed Oct 10 21:10:52 UTC 2018
|
||||||
|
byte[] bytes = hexToBytes("9f:b0:1d:00:fe:dc:2f:81:10:00:00:00:ff:0a:15:00:ff:00:09:00");
|
||||||
|
|
||||||
|
ScaleUser user = new ScaleUser();
|
||||||
|
user.setGender(Converters.Gender.MALE);
|
||||||
|
user.setBirthday(ageToBirthday(36));
|
||||||
|
user.setBodyHeight(186);
|
||||||
|
user.setMeasureUnit(Converters.MeasureUnit.CM);
|
||||||
|
|
||||||
|
ScaleMeasurement measurement = parseScaleMeasurementData(bytes, user);
|
||||||
|
|
||||||
|
float eps = 1e-3f;
|
||||||
|
assertEquals(76.0f, measurement.getWeight(), eps);
|
||||||
|
assertEquals(new Date(expected_timestamp_seconds * 1000), measurement.getDateTime());
|
||||||
|
assertEquals(14.728368f, measurement.getFat(), eps);
|
||||||
|
assertEquals(64.37914f, measurement.getWater(), eps);
|
||||||
|
assertEquals(43.36414f, measurement.getMuscle(), eps);
|
||||||
|
assertEquals(4.525733f, measurement.getBone());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void parseScaleMeasurementData_missingUserData() {
|
||||||
long expected_timestamp_seconds = 1538156082L; // Fri Sep 28 17:34:42 UTC 2018
|
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");
|
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);
|
ScaleMeasurement measurement = parseScaleMeasurementData(bytes, null);
|
||||||
|
|
||||||
assertEquals(measurement.getWeight(), 76.1f, 1e-6f);
|
assertEquals(76.1f, measurement.getWeight(), 1e-3f);
|
||||||
assertEquals(new Date(expected_timestamp_seconds * 1000), measurement.getDateTime());
|
assertEquals(new Date(expected_timestamp_seconds * 1000), measurement.getDateTime());
|
||||||
|
assertEquals(0f, measurement.getFat());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void parseScaleMeasurementData_invalidUserData() {
|
||||||
|
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, new ScaleUser());
|
||||||
|
|
||||||
|
assertEquals(76.1f, measurement.getWeight(), 1e-3f);
|
||||||
|
assertEquals(new Date(expected_timestamp_seconds * 1000), measurement.getDateTime());
|
||||||
|
assertEquals(0f, measurement.getFat());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -124,7 +163,7 @@ public class TrisaBodyAnalyzeLibTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Parses a colon-separated hex-encoded string like "aa:bb:cc:dd" into an array of bytes. */
|
/** Parses a colon-separated hex-encoded string like "aa:bb:cc:dd" into an array of bytes. */
|
||||||
private byte[] hexToBytes(String s) {
|
private static byte[] hexToBytes(String s) {
|
||||||
String[] parts = s.split(":");
|
String[] parts = s.split(":");
|
||||||
byte[] bytes = new byte[parts.length];
|
byte[] bytes = new byte[parts.length];
|
||||||
for (int i = 0; i < bytes.length; ++i) {
|
for (int i = 0; i < bytes.length; ++i) {
|
||||||
@@ -135,4 +174,9 @@ public class TrisaBodyAnalyzeLibTest {
|
|||||||
}
|
}
|
||||||
return bytes;
|
return bytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Date ageToBirthday(int years) {
|
||||||
|
int currentYear = GregorianCalendar.getInstance().get(Calendar.YEAR);
|
||||||
|
return new GregorianCalendar(currentYear - years, Calendar.JANUARY, 1).getTime();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user