1
0
mirror of https://github.com/oliexdev/openScale.git synced 2025-08-14 04:34:18 +02:00

Support for 1byone scale (new version) (#820)

This commit is contained in:
Mirko Laruina
2022-04-15 19:54:00 +02:00
committed by GitHub
parent 15670013ef
commit 55b1f6271e
4 changed files with 554 additions and 5 deletions

View File

@@ -77,6 +77,7 @@ public class OpenScale {
public static boolean DEBUG_MODE = false;
public static final String DATABASE_NAME = "openScale.db";
public static final float SMART_USER_ASSIGN_DEFAULT_RANGE = 15.0F;
private static OpenScale instance;
@@ -273,11 +274,7 @@ public class OpenScale {
// Check user id and do a smart user assign if option is enabled
if (scaleMeasurement.getUserId() == -1) {
if (prefs.getBoolean("smartUserAssign", false)) {
scaleMeasurement.setUserId(getSmartUserAssignment(scaleMeasurement.getWeight(), 15.0f));
} else {
scaleMeasurement.setUserId(getSelectedScaleUser().getId());
}
scaleMeasurement.setUserId(getAssignableUser(scaleMeasurement.getWeight()));
// don't add scale data if no user is selected
if (scaleMeasurement.getUserId() == -1) {
@@ -360,6 +357,20 @@ public class OpenScale {
return scaleMeasurement.getUserId();
}
public int getAssignableUser(float weight){
// Not the best function name
// Returns smart user assignment, if options allow it
// Otherwise it returns the selected user
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
if (prefs.getBoolean("smartUserAssign", false)) {
return getSmartUserAssignment(weight, SMART_USER_ASSIGN_DEFAULT_RANGE);
} else {
return getSelectedScaleUser().getId();
}
}
private int getSmartUserAssignment(float weight, float range) {
List<ScaleUser> scaleUsers = getScaleUserList();
Map<Float, Integer> inRangeWeights = new TreeMap<>();

View File

@@ -82,6 +82,10 @@ public class BluetoothFactory {
if (name.equals("Health Scale".toLowerCase(Locale.US))) {
return new BluetoothOneByone(context);
}
if(name.equals("1byone scale".toLowerCase(Locale.US))){
return new BluetoothOneByoneNew(context);
}
if (name.equals("SENSSUN FAT".toLowerCase(Locale.US))) {
return new BluetoothSenssun(context);
}

View File

@@ -0,0 +1,333 @@
package com.health.openscale.core.bluetooth;
import android.content.Context;
import androidx.annotation.NonNull;
import com.health.openscale.core.OpenScale;
import com.health.openscale.core.bluetooth.lib.OneByoneNewLib;
import com.health.openscale.core.datatypes.ScaleMeasurement;
import com.health.openscale.core.datatypes.ScaleUser;
import com.health.openscale.core.utils.Converters;
import java.math.BigInteger;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import timber.log.Timber;
public class BluetoothOneByoneNew extends BluetoothCommunication{
private final UUID WEIGHT_MEASUREMENT_SERVICE = BluetoothGattUuid.fromShortCode(0xffb0);
private final UUID WEIGHT_MEASUREMENT_CHARACTERISTIC_BODY_COMPOSITION = BluetoothGattUuid.fromShortCode(0xffb2);
private final UUID CMD_AFTER_MEASUREMENT = BluetoothGattUuid.fromShortCode(0xffb1);
private final int MSG_LENGTH = 20;
private final byte[] HEADER_BYTES = { (byte)0xAB, (byte)0x2A };
private ScaleMeasurement currentMeasurement;
public BluetoothOneByoneNew(Context context) {
super(context);
}
@Override
public void onBluetoothNotify(UUID characteristic, byte[] data){
if(data == null){
Timber.e("Received an empty message");
return;
}
Timber.i("Received %s", new BigInteger(1, data).toString(16));
if(data.length < MSG_LENGTH){
Timber.e("Received a message too short");
return;
}
if(!(data[0] == HEADER_BYTES[0] && data[1] == HEADER_BYTES[1])){
Timber.e("Unrecognized message received from scale.");
}
float weight;
int impedance;
switch(data[2]){
case (byte)0x00:
// real time measurement OR historic measurement
// real time has the exact same format of 0x80, but we can ignore it
// we want to capture the historic measures
// filter out real time measurments
if (data[7] != (byte)0x80){
Timber.i("Received real-time measurement. Skipping.");
break;
}
Date time = getTimestamp32(data, 3);
weight = Converters.fromUnsignedInt24Be(data, 8) & 0x03ffff;
weight /= 1000;
impedance = Converters.fromUnsignedInt16Be(data, 15);
ScaleMeasurement historicMeasurement = new ScaleMeasurement();
int assignableUserId = OpenScale.getInstance().getAssignableUser(weight);
if(assignableUserId == -1){
Timber.i("Discarding historic measurement: no user found with intelligent user recognition");
break;
}
populateMeasurement(assignableUserId, historicMeasurement, impedance, weight);
historicMeasurement.setDateTime(time);
addScaleMeasurement(historicMeasurement);
Timber.i("Added historic measurement. Weight: %s, impedance: %s, timestamp: %s", weight, impedance, time.toString());
break;
case (byte)0x80:
// final measurement
currentMeasurement = new ScaleMeasurement();
weight = Converters.fromUnsignedInt24Be(data, 3) & 0x03ffff;
weight = weight / 1000;
currentMeasurement.setWeight(weight);
Timber.d("Weight: %s", weight);
break;
case (byte)0x01:
impedance = Converters.fromUnsignedInt16Be(data, 4);
Timber.d("impedance: %s", impedance);
if(currentMeasurement == null){
Timber.e("Received impedance value without weight");
break;
}
float measurementWeight = currentMeasurement.getWeight();
ScaleUser user = OpenScale.getInstance().getSelectedScaleUser();
populateMeasurement(user.getId(), currentMeasurement, impedance, measurementWeight);
addScaleMeasurement(currentMeasurement);
resumeMachineState();
break;
default:
Timber.e("Unrecognized message receveid");
}
}
private void populateMeasurement(int userId, ScaleMeasurement measurement, int impedance, float weight) {
if(userId == -1){
Timber.e("Discarding measurement population since invalid user");
return;
}
ScaleUser user = OpenScale.getInstance().getScaleUser(userId);
float cmHeight = Converters.fromCentimeter(user.getBodyHeight(), user.getMeasureUnit());
OneByoneNewLib onebyoneLib = new OneByoneNewLib(getUserGender(user), user.getAge(), cmHeight, user.getActivityLevel().toInt());
measurement.setUserId(userId);
measurement.setWeight(weight);
measurement.setDateTime(Calendar.getInstance().getTime());
measurement.setFat(onebyoneLib.getBodyFatPercentage(weight, impedance));
measurement.setWater(onebyoneLib.getWaterPercentage(weight, impedance));
measurement.setBone(onebyoneLib.getBoneMass(weight, impedance));
measurement.setVisceralFat(onebyoneLib.getVisceralFat(weight));
measurement.setMuscle(onebyoneLib.getSkeletonMusclePercentage(weight, impedance));
measurement.setLbm(onebyoneLib.getLBM(weight, impedance));
}
@Override
public String driverName() {
return "OneByoneNew";
}
@Override
protected boolean onNextStep(int stepNr) {
switch(stepNr){
case 0:
setNotificationOn(WEIGHT_MEASUREMENT_SERVICE, WEIGHT_MEASUREMENT_CHARACTERISTIC_BODY_COMPOSITION);
break;
case 1:
// Setup notification on new weight
sendWeightRequest();
// Update the user history on the scale
// Priority given to the current user
ScaleUser currentUser = OpenScale.getInstance().getSelectedScaleUser();
sendUsersHistory(currentUser.getId());
// We wait for the response
stopMachineState();
break;
case 2:
// After the measurement took place, we store the data and send back to the scale
sendUsersHistory(OpenScale.getInstance().getSelectedScaleUserId());
break;
default:
return false;
}
return true;
}
private void sendWeightRequest() {
byte[] msgSetup = new byte[MSG_LENGTH];
setupMeasurementMessage(msgSetup, 0);
writeBytes(WEIGHT_MEASUREMENT_SERVICE, CMD_AFTER_MEASUREMENT, msgSetup, true);
}
private void sendUsersHistory(int priorityUser){
List<ScaleUser> scaleUsers = OpenScale.getInstance().getScaleUserList();
Collections.sort(scaleUsers, (ScaleUser u1, ScaleUser u2) -> {
if(u1.getId() == priorityUser) return -9999;
if(u2.getId() == priorityUser) return 9999;
Date u1LastMeasureDate = OpenScale.getInstance().getLastScaleMeasurement(u1.getId()).getDateTime();
Date u2LastMeasureDate = OpenScale.getInstance().getLastScaleMeasurement(u2.getId()).getDateTime();
return u1LastMeasureDate.compareTo(u2LastMeasureDate);
}
);
byte[] msg = new byte[MSG_LENGTH];
int msgCounter = 0;
for(int i = 0; i < scaleUsers.size(); i++){
ScaleUser user = scaleUsers.get(i);
ScaleMeasurement lastMeasure = OpenScale.getInstance().getLastScaleMeasurement(user.getId());
float weight = 0;
int impedance = 0;
if(lastMeasure != null){
weight = lastMeasure.getWeight();
impedance = getImpedanceFromLBM(user, lastMeasure);
}
int entryPosition = i % 2;
if (entryPosition == 0){
msg = new byte[MSG_LENGTH];
msgCounter ++;
msg[0] = HEADER_BYTES[0];
msg[1] = HEADER_BYTES[1];
msg[2] = (byte) scaleUsers.size();
msg[3] = (byte) msgCounter;
}
setMeasurementEntry(msg, 4 + entryPosition * 7, i + 1,
Math.round(user.getBodyHeight()),
weight,
getUserGender(user),
user.getAge(),
impedance,
true);
if (entryPosition == 1 || i + 1 == scaleUsers.size()){
msg[18] = (byte)0xD4;
msg[19] = d4Checksum(msg, 0, MSG_LENGTH);
writeBytes(WEIGHT_MEASUREMENT_SERVICE, CMD_AFTER_MEASUREMENT, msg, true);
}
}
}
private void setMeasurementEntry(byte[] msg, int offset, int entryNum, int height, float weight, int sex, int age, int impedance, boolean impedanceLe){
// The scale wants a value rounded to the first decimal place
// Otherwise we receive always a UP/DOWN arrow since we would communicate
// AB.CX instead of AB.D0 where D0 is the approximation of CX and it is what the scale uses
// to compute the UP/DOWN arrows
int roundedWeight = Math.round( weight * 10) * 10;
msg[offset] = (byte)(entryNum & 0xFF);
msg[offset+1] = (byte)(height & 0xFF);
Converters.toInt16Be(msg, offset+2, roundedWeight);
msg[offset+4] = (byte)(((sex & 0xFF) << 7) + (age & 0x7F));
if(impedanceLe) {
msg[offset + 5] = (byte) (impedance >> 8);
msg[offset + 6] = (byte) impedance;
} else {
msg[offset + 5] = (byte) impedance;
msg[offset + 6] = (byte) (impedance >> 8);
}
}
private void setTimestamp32(byte[] msg, int offset){
long timestamp = System.currentTimeMillis()/1000L;
Converters.toInt32Be(msg, offset, timestamp);
}
private Date getTimestamp32(byte[] msg, int offset){
long timestamp = Converters.fromUnsignedInt32Be(msg, offset);
return new Date(timestamp * 1000);
}
private boolean setupMeasurementMessage(byte[] msg, int offset){
if(offset + MSG_LENGTH > msg.length){
return false;
}
ScaleUser currentUser = OpenScale.getInstance().getSelectedScaleUser();
Converters.WeightUnit weightUnit = currentUser.getScaleUnit();
msg[offset] = HEADER_BYTES[0];
msg[offset+1] = HEADER_BYTES[1];
setTimestamp32(msg, offset+2);
// This byte has been left empty in all the observations, unknown meaning
msg[offset+6] = 0;
msg[offset+7] = (byte) weightUnit.toInt();
int userId = currentUser.getId();
// We send the last measurement or if not present an empty one
ScaleMeasurement lastMeasure = OpenScale.getInstance().getLastScaleMeasurement(userId);
float weight = 0;
int impedance = 0;
if(lastMeasure != null){
weight = lastMeasure.getWeight();
impedance = getImpedanceFromLBM(currentUser, lastMeasure);
}
setMeasurementEntry(msg, offset+8,
userId,
Math.round(currentUser.getBodyHeight()),
weight,
getUserGender(currentUser),
currentUser.getAge(),
impedance,
false
);
// Blank bytes after the empty measurement
msg[offset + 18] = (byte) 0xD7;
msg[offset+19] = d7Checksum(msg, offset+2, 17);
return true;
}
private int getUserGender(ScaleUser user){
// Custom function since the toInt() gives the opposite values
return user.getGender().isMale() ? 1 : 0;
}
private byte d4Checksum(byte[] msg, int offset, int length){
byte sum = sumChecksum(msg, offset + 2, length - 2);
// Remove impedance MSB first entry
sum -= msg[offset+9];
// Remove second entry weight
sum -= msg[offset+13];
sum -= msg[offset+14];
// Remove impedance MSB second entry
sum -= msg[offset+16];
return sum;
}
private byte d7Checksum(byte[] msg, int offset, int length){
byte sum = sumChecksum(msg, offset+2, length-2);
// Remove impedance MSB
sum -= msg[offset+14];
return sum;
}
// Since we need to send the impedance to the scale the next time,
// we obtain it from the previous measurement using the LBM
public int getImpedanceFromLBM(ScaleUser user, ScaleMeasurement measurement) {
float finalLbm = measurement.getLbm();
float postImpedanceLbm = finalLbm + user.getAge() * 0.0542F;
float preImpedanceLbm = user.getBodyHeight() / 100 * user.getBodyHeight() / 100 * 9.058F + 12.226F + measurement.getWeight() * 0.32F;
return Math.round((preImpedanceLbm - postImpedanceLbm) / 0.0068F);
}
}

View File

@@ -0,0 +1,201 @@
package com.health.openscale.core.bluetooth.lib;
// This class is similar to OneByoneLib, but the way measures are computer are slightly different
public class OneByoneNewLib {
private int sex;
private int age;
private float height;
private int peopleType; // low activity = 0; medium activity = 1; high activity = 2
public OneByoneNewLib(int sex, int age, float height, int peopleType) {
this.sex = sex;
this.age = age;
this.height = height;
this.peopleType = peopleType;
}
public float getBMI(float weight) {
float bmi = weight / (((height * height) / 100.0f) / 100.0f);
return getBounded(bmi, 10, 90);
}
public float getLBM(float weight, int impedance) {
float lbmCoeff = height / 100 * height / 100 * 9.058F;
lbmCoeff += 12.226;
lbmCoeff += weight * 0.32;
lbmCoeff -= impedance * 0.0068;
lbmCoeff -= age * 0.0542;
return lbmCoeff;
}
public float getBMMRCoeff(float weight){
int bmmrCoeff = 20;
if(sex == 1){
bmmrCoeff = 21;
if(age < 0xd){
bmmrCoeff = 36;
} else if(age < 0x10){
bmmrCoeff = 30;
} else if(age < 0x12){
bmmrCoeff = 26;
} else if(age < 0x1e){
bmmrCoeff = 23;
} else if (age >= 0x32){
bmmrCoeff = 20;
}
} else {
if(age < 0xd){
bmmrCoeff = 34;
} else if(age < 0x10){
bmmrCoeff = 29;
} else if(age < 0x12){
bmmrCoeff = 24;
} else if(age < 0x1e){
bmmrCoeff = 22;
} else if (age >= 0x32){
bmmrCoeff = 19;
}
}
return bmmrCoeff;
}
public float getBMMR(float weight){
float bmmr;
if(sex == 1){
bmmr = (weight * 14.916F + 877.8F) - height * 0.726F;
bmmr -= age * 8.976;
} else {
bmmr = (weight * 10.2036F + 864.6F) - height * 0.39336F;
bmmr -= age * 6.204;
}
return getBounded(bmmr, 500, 1000);
}
public float getBodyFatPercentage(float weight, int impedance) {
float bodyFat = getLBM(weight, impedance);
float bodyFatConst;
if (sex == 0) {
if (age < 0x32) {
bodyFatConst = 9.25F;
} else {
bodyFatConst = 7.25F;
}
} else {
bodyFatConst = 0.8F;
}
bodyFat -= bodyFatConst;
if (sex == 0){
if (weight < 50){
bodyFat *= 1.02;
} else if(weight > 60){
bodyFat *= 0.96;
}
if(height > 160){
bodyFat *= 1.03;
}
} else {
if (weight < 61){
bodyFat *= 0.98;
}
}
return 100 * (1 - bodyFat / weight);
}
public float getBoneMass(float weight, int impedance){
float lbmCoeff = getLBM(weight, impedance);
float boneMassConst;
if(sex == 1){
boneMassConst = 0.18016894F;
} else {
boneMassConst = 0.245691014F;
}
boneMassConst = lbmCoeff * 0.05158F - boneMassConst;
float boneMass;
if(boneMassConst <= 2.2){
boneMass = boneMassConst - 0.1F;
} else {
boneMass = boneMassConst + 0.1F;
}
return getBounded(boneMass, 0.5F, 8);
}
public float getMuscleMass(float weight, int impedance){
float muscleMass = weight - getBodyFatPercentage(weight, impedance) * 0.01F * weight;
muscleMass -= getBoneMass(weight, impedance);
return getBounded(muscleMass, 10, 120);
}
public float getSkeletonMusclePercentage(float weight, int impedance){
float skeletonMuscleMass = getWaterPercentage(weight, impedance);
skeletonMuscleMass *= weight;
skeletonMuscleMass *= 0.8422F * 0.01F;
skeletonMuscleMass -= 2.9903;
skeletonMuscleMass /= weight;
return skeletonMuscleMass * 100;
}
public float getVisceralFat(float weight){
float visceralFat;
if (sex == 1) {
if (height < weight * 1.6 + 63.0) {
visceralFat =
age * 0.15F + ((weight * 305.0F) /((height * 0.0826F * height - height * 0.4F) + 48.0F) - 2.9F);
}
else {
visceralFat = age * 0.15F + (weight * (height * -0.0015F + 0.765F) - height * 0.143F) - 5.0F;
}
}
else {
if (weight <= height * 0.5 - 13.0) {
visceralFat = age * 0.07F + (weight * (height * -0.0024F + 0.691F) - height * 0.027F) - 10.5F;
}
else {
visceralFat = age * 0.07F + ((weight * 500.0F) / ((height * 1.45F + height * 0.1158F * height) - 120.0F) - 6.0F);
}
}
return getBounded(visceralFat, 1, 50);
}
public float getWaterPercentage(float weight, int impedance){
float waterPercentage = (100 - getBodyFatPercentage(weight, impedance)) * 0.7F;
if (waterPercentage > 50){
waterPercentage *= 0.98;
} else {
waterPercentage *= 1.02;
}
return getBounded(waterPercentage, 35, 75);
}
public float getProteinPercentage(float weight, int impedance){
return (
(100.0F - getBodyFatPercentage(weight, impedance))
- getWaterPercentage(weight, impedance) * 1.08F
)
- (getBoneMass(weight, impedance) / weight) * 100.0F;
}
private float getBounded(float value, float lowerBound, float upperBound){
if(value < lowerBound){
return lowerBound;
} else if (value > upperBound){
return upperBound;
}
return value;
}
}