1
0
mirror of https://github.com/oliexdev/openScale.git synced 2025-08-13 20:24:14 +02:00

Support for Soehnle Shape Sense

closed issue #487
This commit is contained in:
OliE
2019-11-20 14:57:17 +01:00
committed by GitHub
parent 1fc9ff7cd8
commit c30e34e4b3
8 changed files with 441 additions and 1 deletions

View File

@@ -428,6 +428,10 @@ public class BluetoothBeurerSanitas extends BluetoothCommunication {
batteryLevel, weightThreshold, bodyFatThreshold, currentUnit, userExists,
userReferWeightExists, userMeasurementExist, scaleVersion);
if (batteryLevel <= 10) {
sendMessage(R.string.info_scale_low_battery, batteryLevel);
}
byte requestedUnit = (byte) currentUnit;
ScaleUser user = OpenScale.getInstance().getSelectedScaleUser();
switch (user.getScaleUnit()) {

View File

@@ -27,6 +27,7 @@ import android.os.Looper;
import androidx.core.content.ContextCompat;
import com.health.openscale.R;
import com.health.openscale.core.datatypes.ScaleMeasurement;
import com.welie.blessed.BluetoothCentral;
import com.welie.blessed.BluetoothCentralCallback;
@@ -360,6 +361,10 @@ public abstract class BluetoothCommunication {
public void onConnectionFailed(BluetoothPeripheral peripheral, final int status) {
Timber.e(String.format("connection '%s' failed with status %d", peripheral.getName(), status ));
setBluetoothStatus(BT_STATUS.CONNECTION_LOST);
if (status == 8) {
sendMessage(R.string.info_bluetooth_connection_error_scale_offline, 0);
}
}
@Override

View File

@@ -113,6 +113,9 @@ public class BluetoothFactory {
if (deviceName.startsWith("QN-Scale")) {
return new BluetoothQNScale(context);
}
if (deviceName.startsWith("Shape200") || deviceName.startsWith("Shape100") || deviceName.startsWith("Shape50") || deviceName.startsWith("Style100")) {
return new BluetoothSoehnle(context);
}
return null;
}
}

View File

@@ -92,6 +92,9 @@ public class BluetoothGattUuid {
public static final UUID CHARACTERISTIC_BATTERY_LEVEL = fromShortCode(0x2A19);
public static final UUID CHARACTERISTIC_CHANGE_INCREMENT = fromShortCode(0x2a99);
public static final UUID CHARACTERISTIC_USER_CONTROL_POINT = fromShortCode(0x2A9F);
public static final UUID CHARACTERISTIC_USER_AGE = fromShortCode(0x2A80);
public static final UUID CHARACTERISTIC_USER_GENDER = fromShortCode(0x2A8C);
public static final UUID CHARACTERISTIC_USER_HEIGHT = fromShortCode(0x2A8E);
// https://www.bluetooth.com/specifications/gatt/descriptors
public static final UUID DESCRIPTOR_CLIENT_CHARACTERISTIC_CONFIGURATION = fromShortCode(0x2902);

View File

@@ -0,0 +1,275 @@
/* Copyright (C) 2019 olie.xdev <olie.xdev@googlemail.com>
*
* 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;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import com.health.openscale.R;
import com.health.openscale.core.OpenScale;
import com.health.openscale.core.bluetooth.lib.SoehnleLib;
import com.health.openscale.core.datatypes.ScaleMeasurement;
import com.health.openscale.core.datatypes.ScaleUser;
import com.health.openscale.core.utils.Converters;
import com.welie.blessed.BluetoothBytesParser;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import timber.log.Timber;
public class BluetoothSoehnle extends BluetoothCommunication {
private final UUID WEIGHT_CUSTOM_SERVICE = UUID.fromString("352e3000-28e9-40b8-a361-6db4cca4147c");
private final UUID WEIGHT_CUSTOM_A_CHARACTERISTIC = UUID.fromString("352e3001-28e9-40b8-a361-6db4cca4147c"); // notify, read
private final UUID WEIGHT_CUSTOM_B_CHARACTERISTIC = UUID.fromString("352e3004-28e9-40b8-a361-6db4cca4147c"); // notify, read
private final UUID WEIGHT_CUSTOM_CMD_CHARACTERISTIC = UUID.fromString("352e3002-28e9-40b8-a361-6db4cca4147c"); // write
SharedPreferences prefs;
public BluetoothSoehnle(Context context) {
super(context);
prefs = PreferenceManager.getDefaultSharedPreferences(context);
}
@Override
public String driverName() {
return "Soehnle Scale";
}
@Override
protected boolean onNextStep(int stepNr) {
switch (stepNr) {
case 0:
List<ScaleUser> openScaleUserList = OpenScale.getInstance().getScaleUserList();
int index = -1;
// check if an openScale user is stored as a Soehnle user otherwise do a factory reset
for (ScaleUser openScaleUser : openScaleUserList) {
index = getSoehnleUserIndex(openScaleUser.getId());
if (index != -1) {
break;
}
}
if (index == -1) {
invokeScaleFactoryReset();
}
break;
case 1:
setNotificationOn(BluetoothGattUuid.SERVICE_BATTERY_LEVEL, BluetoothGattUuid.CHARACTERISTIC_BATTERY_LEVEL);
readBytes(BluetoothGattUuid.SERVICE_BATTERY_LEVEL, BluetoothGattUuid.CHARACTERISTIC_BATTERY_LEVEL);
break;
case 2:
// Write the current time
BluetoothBytesParser parser = new BluetoothBytesParser();
parser.setCurrentTime(Calendar.getInstance());
writeBytes(BluetoothGattUuid.SERVICE_CURRENT_TIME, BluetoothGattUuid.CHARACTERISTIC_CURRENT_TIME, parser.getValue());
break;
case 3:
// Turn on notification for User Data Service
setNotificationOn(BluetoothGattUuid.SERVICE_USER_DATA, BluetoothGattUuid.CHARACTERISTIC_USER_CONTROL_POINT);
break;
case 4:
int openScaleUserId = OpenScale.getInstance().getSelectedScaleUserId();
int soehnleUserIndex = getSoehnleUserIndex(openScaleUserId);
if (soehnleUserIndex == -1) {
// create new user
Timber.d("create new Soehnle scale user");
writeBytes(BluetoothGattUuid.SERVICE_USER_DATA, BluetoothGattUuid.CHARACTERISTIC_USER_CONTROL_POINT, new byte[]{(byte)0x01, (byte)0x00, (byte)0x00});
} else {
// select user
Timber.d("select Soehnle scale user with index " + soehnleUserIndex);
writeBytes(BluetoothGattUuid.SERVICE_USER_DATA, BluetoothGattUuid.CHARACTERISTIC_USER_CONTROL_POINT, new byte[]{(byte) 0x02, (byte) soehnleUserIndex, (byte) 0x00, (byte) 0x00});
}
break;
case 5:
// set age
writeBytes(BluetoothGattUuid.SERVICE_USER_DATA, BluetoothGattUuid.CHARACTERISTIC_USER_AGE, new byte[]{(byte)OpenScale.getInstance().getSelectedScaleUser().getAge()});
break;
case 6:
// set gender
writeBytes(BluetoothGattUuid.SERVICE_USER_DATA, BluetoothGattUuid.CHARACTERISTIC_USER_GENDER, new byte[]{OpenScale.getInstance().getSelectedScaleUser().getGender().isMale() ? (byte)0x00 : (byte)0x01});
break;
case 7:
// set height
writeBytes(BluetoothGattUuid.SERVICE_USER_DATA, BluetoothGattUuid.CHARACTERISTIC_USER_HEIGHT, Converters.toInt16Le((int)OpenScale.getInstance().getSelectedScaleUser().getBodyHeight()));
break;
case 8:
setNotificationOn(WEIGHT_CUSTOM_SERVICE, WEIGHT_CUSTOM_A_CHARACTERISTIC);
setNotificationOn(WEIGHT_CUSTOM_SERVICE, WEIGHT_CUSTOM_B_CHARACTERISTIC);
//writeBytes(WEIGHT_CUSTOM_SERVICE, WEIGHT_CUSTOM_CMD_CHARACTERISTIC, new byte[] {(byte)0x0c, (byte)0xff});
break;
case 9:
for (int i=1; i<8; i++) {
// get history data for soehnle user index i
writeBytes(WEIGHT_CUSTOM_SERVICE, WEIGHT_CUSTOM_CMD_CHARACTERISTIC, new byte[]{(byte) 0x09, (byte) i});
}
break;
default:
return false;
}
return true;
}
@Override
public void onBluetoothNotify(UUID characteristic, byte[] value) {
Timber.d("on bluetooth notify change " + byteInHex(value) + " on " + characteristic.toString());
if (value == null) {
return;
}
if (characteristic.equals(WEIGHT_CUSTOM_A_CHARACTERISTIC) && value.length == 15) {
if (value[0] == (byte) 0x09) {
handleWeightMeasurement(value);
}
} else if (characteristic.equals(BluetoothGattUuid.CHARACTERISTIC_USER_CONTROL_POINT)) {
handleUserControlPoint(value);
} else if (characteristic.equals(BluetoothGattUuid.CHARACTERISTIC_BATTERY_LEVEL)) {
int batteryLevel = value[0];
Timber.d("Soehnle scale battery level is " + batteryLevel);
if (batteryLevel <= 10) {
sendMessage(R.string.info_scale_low_battery, batteryLevel);
}
}
}
private void handleUserControlPoint(byte[] value) {
if (value[0] == (byte)0x20) {
int cmd = value[1];
if (cmd == (byte)0x01) { // user create
int userId = OpenScale.getInstance().getSelectedScaleUserId();
int success = value[2];
int soehnleUserIndex = value[3];
if (success == (byte)0x01) {
Timber.d("User control point index is " + soehnleUserIndex + " for user id " + userId);
prefs.edit().putInt("userScaleIndex" + soehnleUserIndex, userId).apply();
sendMessage(R.string.info_step_on_scale_for_reference, 0);
} else {
Timber.e("Error creating new Sohnle user");
}
}
else if (cmd == (byte)0x02) { // user select
int success = value[2];
if (success != (byte)0x01) {
Timber.e("Error selecting Soehnle user");
invokeScaleFactoryReset();
jumpNextToStepNr(0);
}
}
}
}
private int getSoehnleUserIndex(int openScaleUserId) {
for (int i= 1; i<8; i++) {
int prefOpenScaleUserId = prefs.getInt("userScaleIndex"+i, -1);
if (openScaleUserId == prefOpenScaleUserId) {
return i;
}
}
return -1;
}
private void invokeScaleFactoryReset() {
Timber.d("Do a factory reset on Soehnle scale to swipe old users");
// factory reset
writeBytes(WEIGHT_CUSTOM_SERVICE, WEIGHT_CUSTOM_CMD_CHARACTERISTIC, new byte[]{(byte) 0x0b, (byte) 0xff});
for (int i= 1; i<8; i++) {
prefs.edit().putInt("userScaleIndex" + i, -1).apply();
}
}
private void handleWeightMeasurement(byte[] value) {
float weight = Converters.fromUnsignedInt16Be(value, 9) / 10.0f; // kg
int soehnleUserIndex = (int) value[1];
final int year = Converters.fromUnsignedInt16Be(value, 2);
final int month = (int) value[4];
final int day = (int) value[5];
final int hours = (int) value[6];
final int min = (int) value[7];
final int sec = (int) value[8];
final int imp5 = Converters.fromUnsignedInt16Be(value, 11);
final int imp50 = Converters.fromUnsignedInt16Be(value, 13);
String date_string = year + "/" + month + "/" + day + "/" + hours + "/" + min;
Date date_time = new Date();
try {
date_time = new SimpleDateFormat("yyyy/MM/dd/HH/mm").parse(date_string);
} catch (ParseException e) {
Timber.e("parse error " + e.getMessage());
}
final ScaleUser scaleUser = OpenScale.getInstance().getSelectedScaleUser();
int activityLevel = 0;
switch (scaleUser.getActivityLevel()) {
case SEDENTARY:
activityLevel = 0;
break;
case MILD:
activityLevel = 1;
break;
case MODERATE:
activityLevel = 2;
break;
case HEAVY:
activityLevel = 4;
break;
case EXTREME:
activityLevel = 5;
break;
}
int openScaleUserId = prefs.getInt("userScaleIndex"+soehnleUserIndex, -1);
if (openScaleUserId == -1) {
Timber.e("Unknown Soehnle user index " + soehnleUserIndex);
} else {
SoehnleLib soehnleLib = new SoehnleLib(scaleUser.getGender().isMale(), scaleUser.getAge(), scaleUser.getBodyHeight(), activityLevel);
ScaleMeasurement scaleMeasurement = new ScaleMeasurement();
scaleMeasurement.setUserId(openScaleUserId);
scaleMeasurement.setWeight(weight);
scaleMeasurement.setDateTime(date_time);
scaleMeasurement.setWater(soehnleLib.getWater(weight, imp50));
scaleMeasurement.setFat(soehnleLib.getFat(weight, imp50));
scaleMeasurement.setMuscle(soehnleLib.getMuscle(weight, imp50, imp5));
addScaleMeasurement(scaleMeasurement);
}
}
}

View File

@@ -0,0 +1,147 @@
/* Copyright (C) 2019 olie.xdev <olie.xdev@googlemail.com>
*
* 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;
public class SoehnleLib {
private boolean isMale; // male = 1; female = 0
private int age;
private float height;
private int activityLevel;
public SoehnleLib(boolean isMale, int age, float height, int activityLevel) {
this.isMale = isMale;
this.age = age;
this.height = height;
this.activityLevel = activityLevel;
}
public float getFat(final float weight, final float imp50) { // in %
float activityCorrFac = 0.0f;
switch (activityLevel) {
case 4: {
if (isMale) {
activityCorrFac = 2.5f;
}
else {
activityCorrFac = 2.3f;
}
break;
}
case 5: {
if (isMale) {
activityCorrFac = 4.3f;
}
else {
activityCorrFac = 4.1f;
}
break;
}
}
float sexCorrFac;
float activitySexDiv;
if (isMale) {
sexCorrFac = 0.250f;
activitySexDiv = 65.5f;
}
else {
sexCorrFac = 0.214f;
activitySexDiv = 55.1f;
}
return 1.847f * weight * 10000.0f / (height * height) + sexCorrFac * age + 0.062f * imp50 - (activitySexDiv - activityCorrFac);
}
public float computeBodyMassIndex(final float weight) {
return 10000.0f * weight / (height * height);
}
public float getWater(final float weight, final float imp50) { // in %
float activityCorrFac = 0.0f;
switch (activityLevel) {
case 1:
case 2:
case 3: {
if (isMale) {
activityCorrFac = 2.83f;
}
else {
activityCorrFac = 0.0f;
}
break;
}
case 4: {
if (isMale) {
activityCorrFac = 3.93f;
}
else {
activityCorrFac = 0.4f;
}
break;
}
case 5: {
if (isMale) {
activityCorrFac = 5.33f;
}
else {
activityCorrFac = 1.4f;
}
break;
}
}
return (0.3674f * height * height / imp50 + 0.17530f * weight - 0.11f * age + (6.53f + activityCorrFac)) / weight * 100.0f;
}
public float getMuscle(final float weight, final float imp50, final float imp5) { // in %
float activityCorrFac = 0.0f;
switch (activityLevel) {
case 1:
case 2:
case 3: {
if (isMale) {
activityCorrFac = 3.6224f;
}
else {
activityCorrFac = 0.0f;
}
break;
}
case 4: {
if (isMale) {
activityCorrFac = 4.3904f;
}
else {
activityCorrFac = 0.0f;
}
break;
}
case 5: {
if (isMale) {
activityCorrFac = 5.4144f;
}
else {
activityCorrFac = 1.664f;
}
break;
}
}
return ((0.47027f / imp50 - 0.24196f / imp5) * height * height + 0.13796f * weight - 0.1152f * age + (5.12f + activityCorrFac)) / weight * 100.0f;
}
}

View File

@@ -46,7 +46,8 @@ public class ScaleMeasurement implements Cloneable {
private int userId;
@ColumnInfo(name = "enabled")
private boolean enabled;
@CsvColumn(converterClass = CsvHelper.DateTimeConverter.class, format ="yyyy-MM-dd HH:mm", mustNotBeBlank = true) @ColumnInfo(name = "datetime")
@CsvColumn(converterClass = CsvHelper.DateTimeConverter.class, format ="yyyy-MM-dd HH:mm", mustNotBeBlank = true)
@ColumnInfo(name = "datetime")
private Date dateTime;
@CsvColumn(mustNotBeBlank = true)
@ColumnInfo(name = "weight")

View File

@@ -83,6 +83,7 @@
<string name="info_is_enable">enabled</string>
<string name="info_is_not_enable">disabled</string>
<string name="info_is_not_available">not available</string>
<string name="info_scale_low_battery">Low battery level (%d\%%), please change the scale batteries</string>
<string name="info_bluetooth_try_connection">Connecting to:</string>
<string name="info_bluetooth_connection_lost">Lost Bluetooth connection</string>
<string name="info_bluetooth_no_device">No Bluetooth device found</string>
@@ -91,6 +92,7 @@
<string name="info_bluetooth_connection_successful">Connection established</string>
<string name="info_bluetooth_init">Initialize Bluetooth device</string>
<string name="info_bluetooth_connection_error">Unexpected Bluetooth error</string>
<string name="info_bluetooth_connection_error_scale_offline">Cannot connect to scale, please make sure that the scale is turned on</string>
<string name="info_bluetooth_connection_disconnected">Bluetooth connection closed</string>
<string name="info_new_data_added">%1$.2f%2$s [%3$s] to %4$s added</string>
<string name="info_new_data_duplicated">measurement with the same date and time already exist</string>