mirror of
https://github.com/oliexdev/openScale.git
synced 2025-08-10 10:44:47 +02:00
refactored Trisa body analyze lib to make it possible to reuse the formulas for other scales as well
This commit is contained in:
@@ -141,7 +141,7 @@ public class BluetoothOneByone extends BluetoothCommunication {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
OneByoneLib oneByoneLib = new OneByoneLib(sex, scaleUser.getAge(), (int)scaleUser.getBodyHeight(), peopleType);
|
OneByoneLib oneByoneLib = new OneByoneLib(sex, scaleUser.getAge(), scaleUser.getBodyHeight(), peopleType);
|
||||||
|
|
||||||
ScaleMeasurement scaleBtData = new ScaleMeasurement();
|
ScaleMeasurement scaleBtData = new ScaleMeasurement();
|
||||||
scaleBtData.setWeight(weight);
|
scaleBtData.setWeight(weight);
|
||||||
|
@@ -20,21 +20,20 @@ import android.bluetooth.BluetoothGattCharacteristic;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import com.health.openscale.R;
|
import com.health.openscale.R;
|
||||||
import com.health.openscale.core.OpenScale;
|
import com.health.openscale.core.OpenScale;
|
||||||
|
import com.health.openscale.core.bluetooth.lib.TrisaBodyAnalyzeLib;
|
||||||
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.datatypes.ScaleUser;
|
||||||
import com.health.openscale.core.utils.Converters;
|
import com.health.openscale.core.utils.Converters;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
import static com.health.openscale.core.bluetooth.lib.TrisaBodyAnalyzeLib.convertJavaTimestampToDevice;
|
|
||||||
import static com.health.openscale.core.bluetooth.lib.TrisaBodyAnalyzeLib.parseScaleMeasurementData;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Driver for Trisa Body Analyze 4.0.
|
* Driver for Trisa Body Analyze 4.0.
|
||||||
*
|
*
|
||||||
@@ -99,6 +98,11 @@ public class BluetoothTrisaBodyAnalyze extends BluetoothCommunication {
|
|||||||
*/
|
*/
|
||||||
private boolean pairing = false;
|
private boolean pairing = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timestamp of 2010-01-01 00:00:00 UTC (or local time?)
|
||||||
|
*/
|
||||||
|
private static final long TIMESTAMP_OFFSET_SECONDS = 1262304000L;
|
||||||
|
|
||||||
public BluetoothTrisaBodyAnalyze(Context context) {
|
public BluetoothTrisaBodyAnalyze(Context context) {
|
||||||
super(context);
|
super(context);
|
||||||
}
|
}
|
||||||
@@ -251,12 +255,59 @@ public class BluetoothTrisaBodyAnalyze extends BluetoothCommunication {
|
|||||||
|
|
||||||
private void onScaleMeasurumentReceived(byte[] data) {
|
private void onScaleMeasurumentReceived(byte[] data) {
|
||||||
ScaleUser user = OpenScale.getInstance().getSelectedScaleUser();
|
ScaleUser user = OpenScale.getInstance().getSelectedScaleUser();
|
||||||
ScaleMeasurement scaleMeasurement = parseScaleMeasurementData(data, user);
|
|
||||||
if (scaleMeasurement == null) {
|
// data contains:
|
||||||
|
//
|
||||||
|
// 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
|
||||||
|
// ScaleMeasurement needs.
|
||||||
|
if (data.length < 9) {
|
||||||
|
return; // 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) {
|
||||||
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;
|
||||||
}
|
}
|
||||||
addScaleData(scaleMeasurement);
|
float weightKg = getBase10Float(data, 1);
|
||||||
|
int deviceTimestamp = Converters.fromSignedInt32Le(data, 5);
|
||||||
|
|
||||||
|
ScaleMeasurement measurement = new ScaleMeasurement();
|
||||||
|
measurement.setDateTime(new Date(convertDeviceTimestampToJava(deviceTimestamp)));
|
||||||
|
measurement.setWeight((float) weightKg);
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
float resistance2 = getBase10Float(data, resistance2Offset);
|
||||||
|
float impedance = resistance2 < 410f ? 3.0f : 0.3f * (resistance2 - 400f);
|
||||||
|
|
||||||
|
TrisaBodyAnalyzeLib trisaBodyAnalyzeLib = new TrisaBodyAnalyzeLib(user.getGender().isMale() ? 1 : 0, user.getAge(), user.getBodyHeight());
|
||||||
|
|
||||||
|
measurement.setFat(trisaBodyAnalyzeLib.getFat(weightKg, impedance));
|
||||||
|
measurement.setWater(trisaBodyAnalyzeLib.getWater(weightKg, impedance));
|
||||||
|
measurement.setMuscle(trisaBodyAnalyzeLib.getMuscle(weightKg, impedance));
|
||||||
|
measurement.setBone(trisaBodyAnalyzeLib.getBone(weightKg, impedance));
|
||||||
|
}
|
||||||
|
|
||||||
|
addScaleData(measurement);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Write a single command byte, without any arguments. */
|
/** Write a single command byte, without any arguments. */
|
||||||
@@ -304,4 +355,29 @@ public class BluetoothTrisaBodyAnalyze extends BluetoothCommunication {
|
|||||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
prefs.edit().putInt(getDevicePasswordKey(deviceId), password).apply();
|
prefs.edit().putInt(getDevicePasswordKey(deviceId), password).apply();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 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}
|
||||||
|
*/
|
||||||
|
private float getBase10Float(byte[] data, int offset) {
|
||||||
|
int mantissa = Converters.fromUnsignedInt24Le(data, offset);
|
||||||
|
int exponent = data[offset + 3]; // note: byte is signed.
|
||||||
|
return mantissa * (float)Math.pow(10, exponent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int convertJavaTimestampToDevice(long javaTimestampMillis) {
|
||||||
|
return (int)((javaTimestampMillis + 500)/1000 - TIMESTAMP_OFFSET_SECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private long convertDeviceTimestampToJava(int deviceTimestampSeconds) {
|
||||||
|
return 1000 * (TIMESTAMP_OFFSET_SECONDS + (long)deviceTimestampSeconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isValidUser(@Nullable ScaleUser user) {
|
||||||
|
return user != null && user.getAge() > 0 && user.getBodyHeight() > 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
/* Copyright (C) 2018 Maks Verver <maks@verver.ch>
|
/* Copyright (C) 2018 Maks Verver <maks@verver.ch>
|
||||||
|
* 2019 olie.xdev <olie.xdev@googlemail.com>
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -15,14 +16,6 @@
|
|||||||
*/
|
*/
|
||||||
package com.health.openscale.core.bluetooth.lib;
|
package com.health.openscale.core.bluetooth.lib;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class with static helper methods. This is a separate class for testing purposes.
|
* Class with static helper methods. This is a separate class for testing purposes.
|
||||||
*
|
*
|
||||||
@@ -30,100 +23,57 @@ import java.util.Date;
|
|||||||
*/
|
*/
|
||||||
public class TrisaBodyAnalyzeLib {
|
public class TrisaBodyAnalyzeLib {
|
||||||
|
|
||||||
// Timestamp of 2010-01-01 00:00:00 UTC (or local time?)
|
private boolean isMale;
|
||||||
private static final long TIMESTAMP_OFFSET_SECONDS = 1262304000L;
|
private int ageYears;
|
||||||
|
private float heightCm;
|
||||||
|
|
||||||
/** Converts 4 bytes to a floating point number, starting from {@code offset}.
|
public TrisaBodyAnalyzeLib(int sex, int age, float height) {
|
||||||
*
|
isMale = sex == 1 ? true : false; // male = 1; female = 0
|
||||||
* <p>The first three little-endian bytes form the 24-bit mantissa. The last byte contains the
|
ageYears = age;
|
||||||
* signed exponent, applied in base 10.
|
heightCm = height;
|
||||||
*
|
|
||||||
* @throws IndexOutOfBoundsException if {@code offset < 0} or {@code offset + 4> data.length}
|
|
||||||
*/
|
|
||||||
public static double getBase10Float(byte[] data, int offset) {
|
|
||||||
int mantissa = Converters.fromUnsignedInt24Le(data, offset);
|
|
||||||
int exponent = data[offset + 3]; // note: byte is signed.
|
|
||||||
return mantissa * Math.pow(10, exponent);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int convertJavaTimestampToDevice(long javaTimestampMillis) {
|
public float getBMI(float weightKg) {
|
||||||
return (int)((javaTimestampMillis + 500)/1000 - TIMESTAMP_OFFSET_SECONDS);
|
return weightKg * 1e4f / (heightCm * heightCm);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static long convertDeviceTimestampToJava(int deviceTimestampSeconds) {
|
public float getWater(float weightKg, float impedance) {
|
||||||
return 1000 * (TIMESTAMP_OFFSET_SECONDS + (long)deviceTimestampSeconds);
|
float bmi = getBMI(weightKg);
|
||||||
|
|
||||||
|
float water = isMale
|
||||||
|
? 87.51f + (-1.162f * bmi - 0.00813f * impedance + 0.07594f * ageYears)
|
||||||
|
: 77.721f + (-1.148f * bmi - 0.00573f * impedance + 0.06448f * ageYears);
|
||||||
|
|
||||||
|
return water;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
public float getFat(float weightKg, float impedance) {
|
||||||
public static ScaleMeasurement parseScaleMeasurementData(byte[] data, @Nullable ScaleUser user) {
|
float bmi = getBMI(weightKg);
|
||||||
// data contains:
|
|
||||||
//
|
|
||||||
// 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
|
float fat = isMale
|
||||||
// ScaleMeasurement needs.
|
? bmi * (1.479f + 4.4e-4f * impedance) + 0.1f * ageYears - 21.764f
|
||||||
if (data.length < 9) {
|
: bmi * (1.506f + 3.908e-4f * impedance) + 0.1f * ageYears - 12.834f;
|
||||||
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;
|
|
||||||
}
|
|
||||||
double weightKg = getBase10Float(data, 1);
|
|
||||||
int deviceTimestamp = Converters.fromSignedInt32Le(data, 5);
|
|
||||||
|
|
||||||
ScaleMeasurement measurement = new ScaleMeasurement();
|
return fat;
|
||||||
measurement.setDateTime(new Date(convertDeviceTimestampToJava(deviceTimestamp)));
|
|
||||||
measurement.setWeight((float) weightKg);
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isValidUser(@Nullable ScaleUser user) {
|
public float getMuscle(float weightKg, float impedance) {
|
||||||
return user != null && user.getAge() > 0 && user.getBodyHeight() > 0;
|
float bmi = getBMI(weightKg);
|
||||||
|
|
||||||
|
float muscle = isMale
|
||||||
|
? 74.627f + (-0.811f * bmi - 0.00565f * impedance - 0.367f * ageYears)
|
||||||
|
: 57.0f + (-0.694f * bmi - 0.00344f * impedance - 0.255f * ageYears);
|
||||||
|
|
||||||
|
return muscle;
|
||||||
}
|
}
|
||||||
|
|
||||||
private TrisaBodyAnalyzeLib() {}
|
public float getBone(float weightKg, float impedance) {
|
||||||
|
float bmi = getBMI(weightKg);
|
||||||
|
|
||||||
|
float bone = isMale
|
||||||
|
? 7.829f + (-0.0855f * bmi - 5.92e-4f * impedance - 0.0389f * ageYears)
|
||||||
|
: 7.98f + (-0.0973f * bmi - 4.84e-4f * impedance - 0.036f * ageYears);
|
||||||
|
|
||||||
|
return bone;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user