From 4dfdb233f4c88ddbdd142a81cbae6ff0ca454b9c Mon Sep 17 00:00:00 2001 From: oliexdev Date: Fri, 11 Jan 2019 15:39:58 +0100 Subject: [PATCH] refactored Trisa body analyze lib to make it possible to reuse the formulas for other scales as well --- .../core/bluetooth/BluetoothOneByone.java | 2 +- .../bluetooth/BluetoothTrisaBodyAnalyze.java | 90 +++++++++++- .../bluetooth/lib/TrisaBodyAnalyzeLib.java | 132 ++++++------------ 3 files changed, 125 insertions(+), 99 deletions(-) diff --git a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothOneByone.java b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothOneByone.java index ede4db4c..4b967684 100644 --- a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothOneByone.java +++ b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothOneByone.java @@ -141,7 +141,7 @@ public class BluetoothOneByone extends BluetoothCommunication { 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(); scaleBtData.setWeight(weight); 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 52dab296..10ceaf83 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,21 +20,20 @@ import android.bluetooth.BluetoothGattCharacteristic; import android.content.Context; import android.content.SharedPreferences; import android.preference.PreferenceManager; -import androidx.annotation.Nullable; import com.health.openscale.R; 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.ScaleUser; import com.health.openscale.core.utils.Converters; +import java.util.Date; import java.util.UUID; +import androidx.annotation.Nullable; 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. * @@ -99,6 +98,11 @@ public class BluetoothTrisaBodyAnalyze extends BluetoothCommunication { */ 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) { super(context); } @@ -251,12 +255,59 @@ public class BluetoothTrisaBodyAnalyze extends BluetoothCommunication { private void onScaleMeasurumentReceived(byte[] data) { 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)); 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. */ @@ -304,4 +355,29 @@ public class BluetoothTrisaBodyAnalyze extends BluetoothCommunication { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); prefs.edit().putInt(getDevicePasswordKey(deviceId), password).apply(); } + + /** Converts 4 bytes to a floating point number, starting from {@code offset}. + * + *

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; + } } diff --git a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/lib/TrisaBodyAnalyzeLib.java b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/lib/TrisaBodyAnalyzeLib.java index 9322e694..0ff1ea6b 100644 --- a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/lib/TrisaBodyAnalyzeLib.java +++ b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/lib/TrisaBodyAnalyzeLib.java @@ -1,4 +1,5 @@ /* Copyright (C) 2018 Maks Verver + * 2019 olie.xdev * * 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 @@ -15,14 +16,6 @@ */ 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. * @@ -30,100 +23,57 @@ import java.util.Date; */ public class TrisaBodyAnalyzeLib { - // Timestamp of 2010-01-01 00:00:00 UTC (or local time?) - private static final long TIMESTAMP_OFFSET_SECONDS = 1262304000L; + private boolean isMale; + private int ageYears; + private float heightCm; - /** Converts 4 bytes to a floating point number, starting from {@code offset}. - * - *

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 = Converters.fromUnsignedInt24Le(data, offset); - int exponent = data[offset + 3]; // note: byte is signed. - return mantissa * Math.pow(10, exponent); + public TrisaBodyAnalyzeLib(int sex, int age, float height) { + isMale = sex == 1 ? true : false; // male = 1; female = 0 + ageYears = age; + heightCm = height; } - public static int convertJavaTimestampToDevice(long javaTimestampMillis) { - return (int)((javaTimestampMillis + 500)/1000 - TIMESTAMP_OFFSET_SECONDS); + public float getBMI(float weightKg) { + return weightKg * 1e4f / (heightCm * heightCm); } - public static long convertDeviceTimestampToJava(int deviceTimestampSeconds) { - return 1000 * (TIMESTAMP_OFFSET_SECONDS + (long)deviceTimestampSeconds); + public float getWater(float weightKg, float impedance) { + 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 static ScaleMeasurement parseScaleMeasurementData(byte[] data, @Nullable ScaleUser user) { - // 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) + public float getFat(float weightKg, float impedance) { + float bmi = getBMI(weightKg); - // Check that we have at least weight & timestamp, which is the minimum information that - // ScaleMeasurement needs. - 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; - } - double weightKg = getBase10Float(data, 1); - int deviceTimestamp = Converters.fromSignedInt32Le(data, 5); + float fat = isMale + ? bmi * (1.479f + 4.4e-4f * impedance) + 0.1f * ageYears - 21.764f + : bmi * (1.506f + 3.908e-4f * impedance) + 0.1f * ageYears - 12.834f; - 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. - 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 fat; } - private static boolean isValidUser(@Nullable ScaleUser user) { - return user != null && user.getAge() > 0 && user.getBodyHeight() > 0; + public float getMuscle(float weightKg, float impedance) { + 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; + } }