mirror of
https://github.com/oliexdev/openScale.git
synced 2025-08-23 16:53:04 +02:00
Initial working support for Mi Scale v2
Notes: - This is basically a copy of BluetoothMiScale.java with some data offset adjustments - The absent realtime measurement characteristic disabled - No extra body fat etc infos are parsed, just the same data as on the v1 scale are processed - The scale is not yet switched to kg, it is still operated in catty and there is no HW switch!
This commit is contained in:
@@ -38,7 +38,7 @@ public abstract class BluetoothCommunication {
|
|||||||
BT_CONNECTION_LOST, BT_NO_DEVICE_FOUND, BT_UNEXPECTED_ERROR, BT_SCALE_MESSAGE
|
BT_CONNECTION_LOST, BT_NO_DEVICE_FOUND, BT_UNEXPECTED_ERROR, BT_SCALE_MESSAGE
|
||||||
};
|
};
|
||||||
public enum BT_MACHINE_STATE {BT_INIT_STATE, BT_CMD_STATE, BT_CLEANUP_STATE}
|
public enum BT_MACHINE_STATE {BT_INIT_STATE, BT_CMD_STATE, BT_CLEANUP_STATE}
|
||||||
public enum BT_DEVICE_ID {CUSTOM_OPENSCALE, MI_SCALE_V1, SANITAS_SBF70, MEDISANA_BS444, DIGOO_DGS038H, EXCELVANT_CF369BLE, YUNMAI_MINI}
|
public enum BT_DEVICE_ID {CUSTOM_OPENSCALE, MI_SCALE_V1, SANITAS_SBF70, MEDISANA_BS444, DIGOO_DGS038H, EXCELVANT_CF369BLE, YUNMAI_MINI, MI_SCALE_V2}
|
||||||
|
|
||||||
protected Context context;
|
protected Context context;
|
||||||
|
|
||||||
@@ -81,6 +81,8 @@ public abstract class BluetoothCommunication {
|
|||||||
return new BluetoothCustomOpenScale(context);
|
return new BluetoothCustomOpenScale(context);
|
||||||
case MI_SCALE_V1:
|
case MI_SCALE_V1:
|
||||||
return new BluetoothMiScale(context);
|
return new BluetoothMiScale(context);
|
||||||
|
case MI_SCALE_V2:
|
||||||
|
return new BluetoothMiScale2(context);
|
||||||
case SANITAS_SBF70:
|
case SANITAS_SBF70:
|
||||||
return new BluetoothSanitasSbf70(context);
|
return new BluetoothSanitasSbf70(context);
|
||||||
case MEDISANA_BS444:
|
case MEDISANA_BS444:
|
||||||
|
@@ -0,0 +1,288 @@
|
|||||||
|
/* Copyright (C) 2014 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.bluetooth.BluetoothGatt;
|
||||||
|
import android.bluetooth.BluetoothGattCharacteristic;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.preference.PreferenceManager;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.health.openscale.core.datatypes.ScaleData;
|
||||||
|
|
||||||
|
import java.text.ParseException;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Calendar;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.Random;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static com.health.openscale.core.bluetooth.BluetoothCommunication.BT_STATUS_CODE.BT_UNEXPECTED_ERROR;
|
||||||
|
|
||||||
|
public class BluetoothMiScale2 extends BluetoothCommunication {
|
||||||
|
private final UUID WEIGHT_MEASUREMENT_SERVICE = UUID.fromString("0000181b-0000-1000-8000-00805f9b34fb");
|
||||||
|
private final UUID WEIGHT_MEASUREMENT_HISTORY_CHARACTERISTIC = UUID.fromString("00002a2f-0000-3512-2118-0009af100700");
|
||||||
|
private final UUID WEIGHT_MEASUREMENT_TIME_CHARACTERISTIC = UUID.fromString("00002a2b-0000-1000-8000-00805f9b34fb");
|
||||||
|
private final UUID WEIGHT_MEASUREMENT_CONFIG = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");
|
||||||
|
|
||||||
|
public BluetoothMiScale2(Context context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String deviceName() {
|
||||||
|
return "Xiaomi Mi Scale v2";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String defaultDeviceName() {
|
||||||
|
return "MIBCS";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBluetoothDataRead(BluetoothGatt bluetoothGatt, BluetoothGattCharacteristic gattCharacteristic, int status) {
|
||||||
|
byte[] data = gattCharacteristic.getValue();
|
||||||
|
|
||||||
|
int currentYear = Calendar.getInstance().get(Calendar.YEAR);
|
||||||
|
int currentMonth = Calendar.getInstance().get(Calendar.MONTH)+1;
|
||||||
|
int currentDay = Calendar.getInstance().get(Calendar.DAY_OF_MONTH);
|
||||||
|
int scaleYear = ((data[1] & 0xFF) << 8) | (data[0] & 0xFF);
|
||||||
|
int scaleMonth = (int) data[2];
|
||||||
|
int scaleDay = (int) data[3];
|
||||||
|
|
||||||
|
if (currentYear == scaleYear && currentMonth == scaleMonth && currentDay == scaleDay) {
|
||||||
|
setBtMachineState(BT_MACHINE_STATE.BT_CMD_STATE);
|
||||||
|
} else {
|
||||||
|
Log.d("BluetoothMiScale", "Current year and scale year is different");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBluetoothDataChange(BluetoothGatt bluetoothGatt, BluetoothGattCharacteristic gattCharacteristic) {
|
||||||
|
final byte[] data = gattCharacteristic.getValue();
|
||||||
|
|
||||||
|
if (data != null && data.length > 0) {
|
||||||
|
// Stop command from mi scale received
|
||||||
|
if (data[0] == 0x03) {
|
||||||
|
setBtMachineState(BT_MACHINE_STATE.BT_CLEANUP_STATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.length == 26) {
|
||||||
|
final byte[] firstWeight = Arrays.copyOfRange(data, 0, 10);
|
||||||
|
final byte[] secondWeight = Arrays.copyOfRange(data, 10, 20);
|
||||||
|
parseBytes(firstWeight);
|
||||||
|
parseBytes(secondWeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.length == 13) {
|
||||||
|
parseBytes(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
boolean nextInitCmd(int stateNr) {
|
||||||
|
switch (stateNr) {
|
||||||
|
case 0:
|
||||||
|
// read device time
|
||||||
|
readBytes(WEIGHT_MEASUREMENT_SERVICE, WEIGHT_MEASUREMENT_TIME_CHARACTERISTIC);
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
// set current time
|
||||||
|
Calendar currentDateTime = Calendar.getInstance();
|
||||||
|
int year = currentDateTime.get(Calendar.YEAR);
|
||||||
|
byte month = (byte)(currentDateTime.get(Calendar.MONTH)+1);
|
||||||
|
byte day = (byte)currentDateTime.get(Calendar.DAY_OF_MONTH);
|
||||||
|
byte hour = (byte)currentDateTime.get(Calendar.HOUR_OF_DAY);
|
||||||
|
byte min = (byte)currentDateTime.get(Calendar.MINUTE);
|
||||||
|
byte sec = (byte)currentDateTime.get(Calendar.SECOND);
|
||||||
|
|
||||||
|
byte[] dateTimeByte = {(byte)(year), (byte)(year >> 8), month, day, hour, min, sec, 0x03, 0x00, 0x00};
|
||||||
|
|
||||||
|
writeBytes(WEIGHT_MEASUREMENT_SERVICE, WEIGHT_MEASUREMENT_TIME_CHARACTERISTIC, dateTimeByte);
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
// set notification on for weight measurement history
|
||||||
|
setNotificationOn(WEIGHT_MEASUREMENT_SERVICE, WEIGHT_MEASUREMENT_HISTORY_CHARACTERISTIC, WEIGHT_MEASUREMENT_CONFIG);
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
// Set on history weight measurement
|
||||||
|
byte[] magicBytes = new byte[]{(byte)0x01, (byte)0x96, (byte)0x8a, (byte)0xbd, (byte)0x62};
|
||||||
|
|
||||||
|
writeBytes(WEIGHT_MEASUREMENT_SERVICE, WEIGHT_MEASUREMENT_HISTORY_CHARACTERISTIC, magicBytes);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
boolean nextBluetoothCmd(int stateNr) {
|
||||||
|
switch (stateNr) {
|
||||||
|
case 0:
|
||||||
|
// set notification on for weight measurement history
|
||||||
|
setNotificationOn(WEIGHT_MEASUREMENT_SERVICE, WEIGHT_MEASUREMENT_HISTORY_CHARACTERISTIC, WEIGHT_MEASUREMENT_CONFIG);
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
// set notification on for weight measurement
|
||||||
|
// FIXME: replacement characteristic for realtime measurements on Mi Scale 2?
|
||||||
|
//setNotificationOn(WEIGHT_MEASUREMENT_SERVICE, WEIGHT_MEASUREMENT_CHARACTERISTIC, WEIGHT_MEASUREMENT_CONFIG);
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
// configure scale to get only last measurements
|
||||||
|
int uniqueNumber = getUniqueNumber();
|
||||||
|
|
||||||
|
byte[] userIdentifier = new byte[]{(byte)0x01, (byte)0xFF, (byte)0xFF, (byte) ((uniqueNumber & 0xFF00) >> 8), (byte) ((uniqueNumber & 0xFF) >> 0)};
|
||||||
|
writeBytes(WEIGHT_MEASUREMENT_SERVICE, WEIGHT_MEASUREMENT_HISTORY_CHARACTERISTIC, userIdentifier);
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
// set notification off for weight measurement history
|
||||||
|
setNotificationOff(WEIGHT_MEASUREMENT_SERVICE, WEIGHT_MEASUREMENT_HISTORY_CHARACTERISTIC, WEIGHT_MEASUREMENT_CONFIG);
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
// set notification on for weight measurement history
|
||||||
|
setNotificationOn(WEIGHT_MEASUREMENT_SERVICE, WEIGHT_MEASUREMENT_HISTORY_CHARACTERISTIC, WEIGHT_MEASUREMENT_CONFIG);
|
||||||
|
break;
|
||||||
|
case 5:
|
||||||
|
// invoke receiving history data
|
||||||
|
writeBytes(WEIGHT_MEASUREMENT_SERVICE, WEIGHT_MEASUREMENT_HISTORY_CHARACTERISTIC, new byte[]{0x02});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
boolean nextCleanUpCmd(int stateNr) {
|
||||||
|
|
||||||
|
switch (stateNr) {
|
||||||
|
case 0:
|
||||||
|
// send stop command to mi scale
|
||||||
|
writeBytes(WEIGHT_MEASUREMENT_SERVICE, WEIGHT_MEASUREMENT_HISTORY_CHARACTERISTIC, new byte[]{0x03});
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
// acknowledge that you received the last history data
|
||||||
|
int uniqueNumber = getUniqueNumber();
|
||||||
|
|
||||||
|
byte[] userIdentifier = new byte[]{(byte)0x04, (byte)0xFF, (byte)0xFF, (byte) ((uniqueNumber & 0xFF00) >> 8), (byte) ((uniqueNumber & 0xFF) >> 0)};
|
||||||
|
writeBytes(WEIGHT_MEASUREMENT_SERVICE, WEIGHT_MEASUREMENT_HISTORY_CHARACTERISTIC, userIdentifier);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void parseBytes(byte[] weightBytes) {
|
||||||
|
try {
|
||||||
|
float weight = 0.0f;
|
||||||
|
|
||||||
|
final byte ctrlByte = weightBytes[1];
|
||||||
|
|
||||||
|
final boolean isWeightRemoved = isBitSet(ctrlByte, 7);
|
||||||
|
final boolean isStabilized = isBitSet(ctrlByte, 5);
|
||||||
|
final boolean isLBSUnit = isBitSet(ctrlByte, 0);
|
||||||
|
final boolean isCattyUnit = isBitSet(ctrlByte, 4);
|
||||||
|
|
||||||
|
/*Log.d("GattCallback", "IsWeightRemoved: " + isBitSet(ctrlByte, 7));
|
||||||
|
Log.d("GattCallback", "6 LSB Unknown: " + isBitSet(ctrlByte, 6));
|
||||||
|
Log.d("GattCallback", "IsStabilized: " + isBitSet(ctrlByte, 5));
|
||||||
|
Log.d("GattCallback", "IsCattyOrKg: " + isBitSet(ctrlByte, 4));
|
||||||
|
Log.d("GattCallback", "3 LSB Unknown: " + isBitSet(ctrlByte, 3));
|
||||||
|
Log.d("GattCallback", "2 LSB Unknown: " + isBitSet(ctrlByte, 2));
|
||||||
|
Log.d("GattCallback", "1 LSB Unknown: " + isBitSet(ctrlByte, 1));
|
||||||
|
Log.d("GattCallback", "IsLBS: " + isBitSet(ctrlByte, 0));*/
|
||||||
|
|
||||||
|
// Only if the value is stabilized and the weight is *not* removed, the date is valid
|
||||||
|
if (isStabilized && !isWeightRemoved) {
|
||||||
|
|
||||||
|
final int year = ((weightBytes[3] & 0xFF) << 8) | (weightBytes[2] & 0xFF);
|
||||||
|
final int month = (int) weightBytes[4];
|
||||||
|
final int day = (int) weightBytes[5];
|
||||||
|
final int hours = (int) weightBytes[6];
|
||||||
|
final int min = (int) weightBytes[7];
|
||||||
|
final int sec = (int) weightBytes[8];
|
||||||
|
|
||||||
|
if (isLBSUnit || isCattyUnit) {
|
||||||
|
weight = (float) (((weightBytes[12] & 0xFF) << 8) | (weightBytes[11] & 0xFF)) / 100.0f;
|
||||||
|
} else {
|
||||||
|
weight = (float) (((weightBytes[12] & 0xFF) << 8) | (weightBytes[11] & 0xFF)) / 200.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
String date_string = year + "/" + month + "/" + day + "/" + hours + "/" + min;
|
||||||
|
Date date_time = new SimpleDateFormat("yyyy/MM/dd/HH/mm").parse(date_string);
|
||||||
|
|
||||||
|
// Is the year plausible? Check if the year is in the range of 20 years...
|
||||||
|
if (validateDate(date_time, 20)) {
|
||||||
|
ScaleData scaleBtData = new ScaleData();
|
||||||
|
|
||||||
|
scaleBtData.setWeight(weight);
|
||||||
|
scaleBtData.setDateTime(date_time);
|
||||||
|
|
||||||
|
addScaleData(scaleBtData);
|
||||||
|
} else {
|
||||||
|
Log.e("BluetoothMiScale", "Invalid Mi scale weight year " + year);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (ParseException e) {
|
||||||
|
setBtStatus(BT_UNEXPECTED_ERROR, "Error while decoding bluetooth date string (" + e.getMessage() + ")");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean validateDate(Date weightDate, int range) {
|
||||||
|
|
||||||
|
Calendar currentDatePos = Calendar.getInstance();
|
||||||
|
currentDatePos.add(Calendar.YEAR, range);
|
||||||
|
|
||||||
|
Calendar currentDateNeg = Calendar.getInstance();
|
||||||
|
currentDateNeg.add(Calendar.YEAR, -range);
|
||||||
|
|
||||||
|
if (weightDate.before(currentDatePos.getTime()) && weightDate.after(currentDateNeg.getTime())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getUniqueNumber() {
|
||||||
|
int uniqueNumber;
|
||||||
|
|
||||||
|
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
|
|
||||||
|
uniqueNumber = prefs.getInt("uniqueNumber", 0x00);
|
||||||
|
|
||||||
|
if (uniqueNumber == 0x00) {
|
||||||
|
Random r = new Random();
|
||||||
|
uniqueNumber = r.nextInt(65535 - 100 + 1) + 100;
|
||||||
|
|
||||||
|
prefs.edit().putInt("uniqueNumber", uniqueNumber).commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
int userId = prefs.getInt("selectedUserId", -1);
|
||||||
|
|
||||||
|
return uniqueNumber + userId;
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user