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 6e7008a7..94dc80bd 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 @@ -25,6 +25,7 @@ import com.health.openscale.core.datatypes.ScaleMeasurement; import com.health.openscale.core.datatypes.ScaleUser; import com.health.openscale.core.utils.Converters; +import java.util.Calendar; import java.util.UUID; import timber.log.Timber; @@ -36,9 +37,18 @@ public class BluetoothOneByone extends BluetoothCommunication { private final UUID CMD_MEASUREMENT_CHARACTERISTIC = BluetoothGattUuid.fromShortCode(0xfff1); // write only + private boolean waitAck = false; // if true, resume after receiving acknowledgement + private boolean historicMeasurement = false; // processing real-time vs historic measurement + private int noHistoric = 0; // number of historic measurements received + + // don't save any measurements closer than 3 seconds to each other + private Calendar lastDateTime; + private final int DATE_TIME_THRESHOLD = 3000; public BluetoothOneByone(Context context) { super(context); + lastDateTime = Calendar.getInstance(); + lastDateTime.set(2000, 1, 1); } @Override @@ -72,6 +82,25 @@ public class BluetoothOneByone extends BluetoothCommunication { writeBytes(WEIGHT_MEASUREMENT_SERVICE, CMD_MEASUREMENT_CHARACTERISTIC, magicBytes); break; case 2: + Calendar dt = Calendar.getInstance(); + final byte[] setClockCmd = {(byte)0xf1, (byte)(dt.get(Calendar.YEAR) >> 8), + (byte)(dt.get(Calendar.YEAR) & 255), (byte)(dt.get(Calendar.MONTH) + 1), + (byte)dt.get(Calendar.DAY_OF_MONTH), (byte)dt.get(Calendar.HOUR_OF_DAY), + (byte)dt.get(Calendar.MINUTE), (byte)dt.get(Calendar.SECOND)}; + waitAck = true; + writeBytes(WEIGHT_MEASUREMENT_SERVICE, CMD_MEASUREMENT_CHARACTERISTIC, setClockCmd); + // 2-byte notification value f1 00 will be received after this command + stopMachineState(); // we will resume after receiving acknowledgement f1 00 + break; + case 3: + // request historic measurements; they are followed by real-time measurements + historicMeasurement = true; + final byte[] getHistoryCmd = {(byte)0xf2, (byte)0x00}; + writeBytes(WEIGHT_MEASUREMENT_SERVICE, CMD_MEASUREMENT_CHARACTERISTIC, getHistoryCmd); + // multiple measurements will be received, they start cf ... and are 11 or 18 bytes long + // 2-byte notification value f2 00 follows last historic measurement + break; + case 4: sendMessage(R.string.info_step_on_scale, 0); break; default: @@ -89,8 +118,28 @@ public class BluetoothOneByone extends BluetoothCommunication { } // if data is valid data - if (data.length == 20 && data[0] == (byte)0xcf) { + if (data.length >= 11 && data[0] == (byte)0xcf) { + if (historicMeasurement) { + ++noHistoric; + } parseBytes(data); + } else { + // show 2-byte ack messages in debug output: + // f1 00 setClockCmd acknowledgement + // f2 00 end of historic measurements, real-time measurements follow + // f2 01 clearHistoryCmd acknowledgement + Timber.d("received bytes [%s]", byteInHex(data)); + + if (waitAck && data.length == 2 && data[0] == (byte)0xf1 && data[1] == 0) { + waitAck = false; + resumeMachineState(); + } else if (data.length == 2 && data[0] == (byte)0xf2 && data[1] == 0) { + historicMeasurement = false; + if (noHistoric > 0) { + final byte[] clearHistoryCmd = {(byte)0xf2, (byte)0x01}; + writeBytes(WEIGHT_MEASUREMENT_SERVICE, CMD_MEASUREMENT_CHARACTERISTIC, clearHistoryCmd); + } + } } } @@ -98,6 +147,21 @@ public class BluetoothOneByone extends BluetoothCommunication { float weight = Converters.fromUnsignedInt16Le(weightBytes, 3) / 100.0f; int impedanceCoeff = Converters.fromUnsignedInt24Le(weightBytes, 5); int impedanceValue = weightBytes[5] + weightBytes[6] + weightBytes[7]; + boolean impedancePresent = (weightBytes[9] != 1) && (impedanceCoeff != 0); + boolean dateTimePresent = weightBytes.length >= 18; + + if (!impedancePresent || (!dateTimePresent && historicMeasurement)) { + // unwanted, no impedance or historic measurement w/o time-stamp + return; + } + + Calendar dateTime = Calendar.getInstance(); + if (dateTimePresent) { + // 18-byte or longer measurements contain date and time, used in history + dateTime.set(Converters.fromUnsignedInt16Be(weightBytes, 11), + weightBytes[13] - 1, weightBytes[14], weightBytes[15], + weightBytes[16], weightBytes[17]); + } final ScaleUser scaleUser = OpenScale.getInstance().getSelectedScaleUser(); @@ -135,6 +199,19 @@ public class BluetoothOneByone extends BluetoothCommunication { ScaleMeasurement scaleBtData = new ScaleMeasurement(); scaleBtData.setWeight(weight); + try { + dateTime.setLenient(false); + scaleBtData.setDateTime(dateTime.getTime()); + } + catch (IllegalArgumentException e) { + if (historicMeasurement) { + Timber.d("invalid time-stamp: year %d, month %d, day %d, hour %d, minute %d, second %d", + Converters.fromUnsignedInt16Be(weightBytes, 11), + weightBytes[13], weightBytes[14], weightBytes[15], + weightBytes[16], weightBytes[17]); + return; // discard historic measurement with invalid time-stamp + } + } scaleBtData.setFat(oneByoneLib.getBodyFat(weight, impedanceCoeff)); scaleBtData.setWater(oneByoneLib.getWater(scaleBtData.getFat())); scaleBtData.setBone(oneByoneLib.getBoneMass(weight, impedanceValue)); @@ -143,6 +220,11 @@ public class BluetoothOneByone extends BluetoothCommunication { Timber.d("scale measurement [%s]", scaleBtData); + if (dateTime.getTimeInMillis() - lastDateTime.getTimeInMillis() < DATE_TIME_THRESHOLD) { + return; // don't save measurements too close to each other + } + lastDateTime = dateTime; + addScaleMeasurement(scaleBtData); } }