From b2ec8599b7b17ee6c39dc0550ec0be1e42688fc7 Mon Sep 17 00:00:00 2001 From: Andreas Shimokawa Date: Thu, 26 Oct 2017 23:07:57 +0200 Subject: [PATCH] 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! --- .../bluetooth/BluetoothCommunication.java | 4 +- .../core/bluetooth/BluetoothMiScale2.java | 288 ++++++++++++++++++ 2 files changed, 291 insertions(+), 1 deletion(-) create mode 100644 android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothMiScale2.java diff --git a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothCommunication.java b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothCommunication.java index 071514e7..e36d311c 100644 --- a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothCommunication.java +++ b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothCommunication.java @@ -38,7 +38,7 @@ public abstract class BluetoothCommunication { 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_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; @@ -81,6 +81,8 @@ public abstract class BluetoothCommunication { return new BluetoothCustomOpenScale(context); case MI_SCALE_V1: return new BluetoothMiScale(context); + case MI_SCALE_V2: + return new BluetoothMiScale2(context); case SANITAS_SBF70: return new BluetoothSanitasSbf70(context); case MEDISANA_BS444: diff --git a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothMiScale2.java b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothMiScale2.java new file mode 100644 index 00000000..3471c04e --- /dev/null +++ b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothMiScale2.java @@ -0,0 +1,288 @@ +/* Copyright (C) 2014 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 +* 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 +*/ + +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; + } +}