mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2025-02-16 20:36:49 +01:00
First part of extracting parts out of MiBandSupport
Extract Activity Data fetching into distinct 'operation' class. Fix a few small things wrt transaction-local GattCallbacks along the way.
This commit is contained in:
parent
585a888ecb
commit
33b598ce5c
@ -132,7 +132,7 @@ public abstract class AbstractBTLEDeviceSupport extends AbstractDeviceSupport im
|
|||||||
* @return the characteristic for the given UUID or <code>null</code>
|
* @return the characteristic for the given UUID or <code>null</code>
|
||||||
* @see #addSupportedService(UUID)
|
* @see #addSupportedService(UUID)
|
||||||
*/
|
*/
|
||||||
protected BluetoothGattCharacteristic getCharacteristic(UUID uuid) {
|
public BluetoothGattCharacteristic getCharacteristic(UUID uuid) {
|
||||||
if (mAvailableCharacteristics == null) {
|
if (mAvailableCharacteristics == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -61,7 +61,7 @@ public final class BtLEQueue {
|
|||||||
while (!mDisposed && !mCrashed) {
|
while (!mDisposed && !mCrashed) {
|
||||||
try {
|
try {
|
||||||
Transaction transaction = mTransactions.take();
|
Transaction transaction = mTransactions.take();
|
||||||
internalGattCallback.setTransactionGattCallback(null);
|
internalGattCallback.reset();
|
||||||
|
|
||||||
if (!isConnected()) {
|
if (!isConnected()) {
|
||||||
// TODO: request connection and initialization from the outside and wait until finished
|
// TODO: request connection and initialization from the outside and wait until finished
|
||||||
@ -114,7 +114,6 @@ public final class BtLEQueue {
|
|||||||
} finally {
|
} finally {
|
||||||
mWaitForActionResultLatch = null;
|
mWaitForActionResultLatch = null;
|
||||||
mWaitCharacteristic = null;
|
mWaitCharacteristic = null;
|
||||||
internalGattCallback.reset();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LOG.info("Queue Dispatch Thread terminated.");
|
LOG.info("Queue Dispatch Thread terminated.");
|
||||||
@ -185,6 +184,7 @@ public final class BtLEQueue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void handleDisconnected(int status) {
|
private void handleDisconnected(int status) {
|
||||||
|
internalGattCallback.reset();
|
||||||
mTransactions.clear();
|
mTransactions.clear();
|
||||||
mAbortTransaction = true;
|
mAbortTransaction = true;
|
||||||
if (mWaitForActionResultLatch != null) {
|
if (mWaitForActionResultLatch != null) {
|
||||||
|
@ -3,7 +3,6 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.miband;
|
|||||||
import android.bluetooth.BluetoothGatt;
|
import android.bluetooth.BluetoothGatt;
|
||||||
import android.bluetooth.BluetoothGattCharacteristic;
|
import android.bluetooth.BluetoothGattCharacteristic;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
@ -12,19 +11,14 @@ import org.slf4j.Logger;
|
|||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.text.DateFormat;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Calendar;
|
import java.util.Calendar;
|
||||||
import java.util.GregorianCalendar;
|
import java.util.GregorianCalendar;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
|
||||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
|
||||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo;
|
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
|
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst;
|
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandCoordinator;
|
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandCoordinator;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandFWHelper;
|
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandFWHelper;
|
||||||
@ -37,11 +31,10 @@ import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSuppo
|
|||||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEAction;
|
import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEAction;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.AbortTransactionAction;
|
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.AbortTransactionAction;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceBusyAction;
|
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction;
|
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetProgressAction;
|
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetProgressAction;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.operations.FetchActivityOperation;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.util.CheckSums;
|
import nodomain.freeyourgadget.gadgetbridge.util.CheckSums;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
|
|
||||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||||
|
|
||||||
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.DEFAULT_VALUE_FLASH_COLOUR;
|
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.DEFAULT_VALUE_FLASH_COLOUR;
|
||||||
@ -68,29 +61,10 @@ import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.ge
|
|||||||
|
|
||||||
public class MiBandSupport extends AbstractBTLEDeviceSupport {
|
public class MiBandSupport extends AbstractBTLEDeviceSupport {
|
||||||
|
|
||||||
//temporary buffer, size is a multiple of 60 because we want to store complete minutes (1 minute = 3 bytes)
|
|
||||||
private static final int activityDataHolderSize = 3 * 60 * 4; // 8h
|
|
||||||
|
|
||||||
private static class ActivityStruct {
|
|
||||||
public byte[] activityDataHolder = new byte[activityDataHolderSize];
|
|
||||||
//index of the buffer above
|
|
||||||
public int activityDataHolderProgress = 0;
|
|
||||||
//number of bytes we will get in a single data transfer, used as counter
|
|
||||||
public int activityDataRemainingBytes = 0;
|
|
||||||
//same as above, but remains untouched for the ack message
|
|
||||||
public int activityDataUntilNextHeader = 0;
|
|
||||||
//timestamp of the single data transfer, incremented to store each minute's data
|
|
||||||
public GregorianCalendar activityDataTimestampProgress = null;
|
|
||||||
//same as above, but remains untouched for the ack message
|
|
||||||
public GregorianCalendar activityDataTimestampToAck = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(MiBandSupport.class);
|
private static final Logger LOG = LoggerFactory.getLogger(MiBandSupport.class);
|
||||||
private volatile boolean telephoneRinging;
|
private volatile boolean telephoneRinging;
|
||||||
private volatile boolean isLocatingDevice;
|
private volatile boolean isLocatingDevice;
|
||||||
|
|
||||||
private ActivityStruct activityStruct;
|
|
||||||
|
|
||||||
private DeviceInfo mDeviceInfo;
|
private DeviceInfo mDeviceInfo;
|
||||||
|
|
||||||
private boolean firmwareInfoSent = false;
|
private boolean firmwareInfoSent = false;
|
||||||
@ -234,7 +208,6 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport {
|
|||||||
private static final byte[] startVibrate = new byte[]{MiBandService.COMMAND_SEND_NOTIFICATION, 1};
|
private static final byte[] startVibrate = new byte[]{MiBandService.COMMAND_SEND_NOTIFICATION, 1};
|
||||||
private static final byte[] stopVibrate = new byte[]{MiBandService.COMMAND_STOP_MOTOR_VIBRATE};
|
private static final byte[] stopVibrate = new byte[]{MiBandService.COMMAND_STOP_MOTOR_VIBRATE};
|
||||||
private static final byte[] reboot = new byte[]{MiBandService.COMMAND_REBOOT};
|
private static final byte[] reboot = new byte[]{MiBandService.COMMAND_REBOOT};
|
||||||
private static final byte[] fetch = new byte[]{MiBandService.COMMAND_FETCH_DATA};
|
|
||||||
|
|
||||||
private byte[] getNotification(long vibrateDuration, int vibrateTimes, int flashTimes, int flashColour, int originalColour, long flashDuration) {
|
private byte[] getNotification(long vibrateDuration, int vibrateTimes, int flashTimes, int flashColour, int originalColour, long flashDuration) {
|
||||||
byte[] vibrate = new byte[]{MiBandService.COMMAND_SEND_NOTIFICATION, (byte) 1};
|
byte[] vibrate = new byte[]{MiBandService.COMMAND_SEND_NOTIFICATION, (byte) 1};
|
||||||
@ -546,11 +519,7 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport {
|
|||||||
@Override
|
@Override
|
||||||
public void onFetchActivityData() {
|
public void onFetchActivityData() {
|
||||||
try {
|
try {
|
||||||
TransactionBuilder builder = performInitialized("fetch activity data");
|
new FetchActivityOperation(this).perform();
|
||||||
// builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_LE_PARAMS), getLowLatency());
|
|
||||||
builder.add(new SetDeviceBusyAction(getDevice(), getContext().getString(R.string.busy_task_fetch_activity_data), getContext()));
|
|
||||||
builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_CONTROL_POINT), fetch);
|
|
||||||
builder.queue(getQueue());
|
|
||||||
} catch (IOException ex) {
|
} catch (IOException ex) {
|
||||||
LOG.error("Unable to fetch MI activity data", ex);
|
LOG.error("Unable to fetch MI activity data", ex);
|
||||||
}
|
}
|
||||||
@ -646,9 +615,7 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport {
|
|||||||
super.onCharacteristicChanged(gatt, characteristic);
|
super.onCharacteristicChanged(gatt, characteristic);
|
||||||
|
|
||||||
UUID characteristicUUID = characteristic.getUuid();
|
UUID characteristicUUID = characteristic.getUuid();
|
||||||
if (MiBandService.UUID_CHARACTERISTIC_ACTIVITY_DATA.equals(characteristicUUID)) {
|
if (MiBandService.UUID_CHARACTERISTIC_BATTERY.equals(characteristicUUID)) {
|
||||||
handleActivityNotif(characteristic.getValue());
|
|
||||||
} else if (MiBandService.UUID_CHARACTERISTIC_BATTERY.equals(characteristicUUID)) {
|
|
||||||
handleBatteryInfo(characteristic.getValue(), BluetoothGatt.GATT_SUCCESS);
|
handleBatteryInfo(characteristic.getValue(), BluetoothGatt.GATT_SUCCESS);
|
||||||
} else if (MiBandService.UUID_CHARACTERISTIC_NOTIFICATION.equals(characteristicUUID)) {
|
} else if (MiBandService.UUID_CHARACTERISTIC_NOTIFICATION.equals(characteristicUUID)) {
|
||||||
handleNotificationNotif(characteristic.getValue());
|
handleNotificationNotif(characteristic.getValue());
|
||||||
@ -792,151 +759,6 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport {
|
|||||||
builder.write(characteristic, alarmMessage);
|
builder.write(characteristic, alarmMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Method to handle the incoming activity data.
|
|
||||||
* There are two kind of messages we currently know:
|
|
||||||
* - the first one is 11 bytes long and contains metadata (how many bytes to expect, when the data starts, etc.)
|
|
||||||
* - the second one is 20 bytes long and contains the actual activity data
|
|
||||||
*
|
|
||||||
* The first message type is parsed by this method, for every other length of the value param, bufferActivityData is called.
|
|
||||||
* @see #bufferActivityData(byte[])
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @param value
|
|
||||||
*/
|
|
||||||
private void handleActivityNotif(byte[] value) {
|
|
||||||
boolean firstChunk = activityStruct == null;
|
|
||||||
if (firstChunk) {
|
|
||||||
activityStruct = new ActivityStruct();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value.length == 11) {
|
|
||||||
// byte 0 is the data type: 1 means that each minute is represented by a triplet of bytes
|
|
||||||
int dataType = value[0];
|
|
||||||
// byte 1 to 6 represent a timestamp
|
|
||||||
GregorianCalendar timestamp = parseTimestamp(value, 1);
|
|
||||||
|
|
||||||
// counter of all data held by the band
|
|
||||||
int totalDataToRead = (value[7] & 0xff) | ((value[8] & 0xff) << 8);
|
|
||||||
totalDataToRead *= (dataType == 1) ? 3 : 1;
|
|
||||||
|
|
||||||
|
|
||||||
// counter of this data block
|
|
||||||
int dataUntilNextHeader = (value[9] & 0xff) | ((value[10] & 0xff) << 8);
|
|
||||||
dataUntilNextHeader *= (dataType == 1) ? 3 : 1;
|
|
||||||
|
|
||||||
// there is a total of totalDataToRead that will come in chunks (3 bytes per minute if dataType == 1),
|
|
||||||
// these chunks are usually 20 bytes long and grouped in blocks
|
|
||||||
// after dataUntilNextHeader bytes we will get a new packet of 11 bytes that should be parsed
|
|
||||||
// as we just did
|
|
||||||
|
|
||||||
if (firstChunk && dataUntilNextHeader != 0) {
|
|
||||||
GB.toast(getContext().getString(R.string.user_feedback_miband_activity_data_transfer,
|
|
||||||
DateTimeUtils.formatDurationHoursMinutes((totalDataToRead / 3), TimeUnit.MINUTES),
|
|
||||||
DateFormat.getDateTimeInstance().format(timestamp.getTime())), Toast.LENGTH_LONG, GB.INFO);
|
|
||||||
}
|
|
||||||
LOG.info("total data to read: " + totalDataToRead + " len: " + (totalDataToRead / 3) + " minute(s)");
|
|
||||||
LOG.info("data to read until next header: " + dataUntilNextHeader + " len: " + (dataUntilNextHeader / 3) + " minute(s)");
|
|
||||||
LOG.info("TIMESTAMP: " + DateFormat.getDateTimeInstance().format(timestamp.getTime()).toString() + " magic byte: " + dataUntilNextHeader);
|
|
||||||
|
|
||||||
activityStruct.activityDataRemainingBytes = activityStruct.activityDataUntilNextHeader = dataUntilNextHeader;
|
|
||||||
activityStruct.activityDataTimestampToAck = (GregorianCalendar) timestamp.clone();
|
|
||||||
activityStruct.activityDataTimestampProgress = timestamp;
|
|
||||||
|
|
||||||
} else {
|
|
||||||
bufferActivityData(value);
|
|
||||||
}
|
|
||||||
LOG.debug("activity data: length: " + value.length + ", remaining bytes: " + activityStruct.activityDataRemainingBytes);
|
|
||||||
|
|
||||||
if (activityStruct.activityDataRemainingBytes == 0) {
|
|
||||||
sendAckDataTransfer(activityStruct.activityDataTimestampToAck, activityStruct.activityDataUntilNextHeader);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private GregorianCalendar parseTimestamp(byte[] value, int offset) {
|
|
||||||
GregorianCalendar timestamp = new GregorianCalendar(
|
|
||||||
value[offset] + 2000,
|
|
||||||
value[offset + 1],
|
|
||||||
value[offset + 2],
|
|
||||||
value[offset + 3],
|
|
||||||
value[offset + 4],
|
|
||||||
value[offset + 5]);
|
|
||||||
return timestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method to store temporarily the activity data values got from the Mi Band.
|
|
||||||
*
|
|
||||||
* Since we expect chunks of 20 bytes each, we do not store the received bytes it the length is different.
|
|
||||||
*
|
|
||||||
* @param value
|
|
||||||
*/
|
|
||||||
private void bufferActivityData(byte[] value) {
|
|
||||||
|
|
||||||
if (activityStruct.activityDataRemainingBytes >= value.length) {
|
|
||||||
//I don't like this clause, but until we figure out why we get different data sometimes this should work
|
|
||||||
if (value.length == 20 || value.length == activityStruct.activityDataRemainingBytes) {
|
|
||||||
System.arraycopy(value, 0, activityStruct.activityDataHolder, activityStruct.activityDataHolderProgress, value.length);
|
|
||||||
activityStruct.activityDataHolderProgress += value.length;
|
|
||||||
activityStruct.activityDataRemainingBytes -= value.length;
|
|
||||||
|
|
||||||
if (this.activityDataHolderSize == activityStruct.activityDataHolderProgress) {
|
|
||||||
flushActivityDataHolder();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// the length of the chunk is not what we expect. We need to make sense of this data
|
|
||||||
LOG.warn("GOT UNEXPECTED ACTIVITY DATA WITH LENGTH: " + value.length + ", EXPECTED LENGTH: " + activityStruct.activityDataRemainingBytes);
|
|
||||||
for (byte b : value) {
|
|
||||||
LOG.warn("DATA: " + String.format("0x%8x", b));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
LOG.error("error buffering activity data: remaining bytes: " + activityStruct.activityDataRemainingBytes + ", received: " + value.length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* empty the local buffer for activity data, arrange the values received in groups of three and
|
|
||||||
* store them in the DB
|
|
||||||
*/
|
|
||||||
private void flushActivityDataHolder() {
|
|
||||||
if (activityStruct == null) {
|
|
||||||
LOG.debug("nothing to flush, struct is already null");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
LOG.debug("flushing activity data holder");
|
|
||||||
byte category, intensity, steps;
|
|
||||||
|
|
||||||
DBHandler dbHandler = null;
|
|
||||||
try {
|
|
||||||
dbHandler = GBApplication.acquireDB();
|
|
||||||
try (SQLiteDatabase db = dbHandler.getWritableDatabase()) { // explicitly keep the db open while looping over the samples
|
|
||||||
for (int i = 0; i < activityStruct.activityDataHolderProgress; i += 3) { //TODO: check if multiple of 3, if not something is wrong
|
|
||||||
category = activityStruct.activityDataHolder[i];
|
|
||||||
intensity = activityStruct.activityDataHolder[i + 1];
|
|
||||||
steps = activityStruct.activityDataHolder[i + 2];
|
|
||||||
|
|
||||||
dbHandler.addGBActivitySample(
|
|
||||||
(int) (activityStruct.activityDataTimestampProgress.getTimeInMillis() / 1000),
|
|
||||||
SampleProvider.PROVIDER_MIBAND,
|
|
||||||
(short) (intensity & 0xff),
|
|
||||||
(short) (steps & 0xff),
|
|
||||||
category);
|
|
||||||
activityStruct.activityDataTimestampProgress.add(Calendar.MINUTE, 1);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
activityStruct.activityDataHolderProgress = 0;
|
|
||||||
}
|
|
||||||
} catch (Exception ex) {
|
|
||||||
GB.toast(getContext(), ex.getMessage(), Toast.LENGTH_LONG, GB.ERROR);
|
|
||||||
} finally {
|
|
||||||
if (dbHandler != null) {
|
|
||||||
dbHandler.release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void handleControlPointResult(byte[] value, int status) {
|
private void handleControlPointResult(byte[] value, int status) {
|
||||||
if (status != BluetoothGatt.GATT_SUCCESS) {
|
if (status != BluetoothGatt.GATT_SUCCESS) {
|
||||||
LOG.warn("Could not write to the control point.");
|
LOG.warn("Could not write to the control point.");
|
||||||
@ -952,56 +774,6 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void unsetBusy() {
|
|
||||||
getDevice().unsetBusyTask();
|
|
||||||
getDevice().sendDeviceUpdateIntent(getContext());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Acknowledge the transfer of activity data to the Mi Band.
|
|
||||||
*
|
|
||||||
* After receiving data from the band, it has to be acknowledged. This way the Mi Band will delete
|
|
||||||
* the data it has on record.
|
|
||||||
*
|
|
||||||
* @param time
|
|
||||||
* @param bytesTransferred
|
|
||||||
*/
|
|
||||||
private void sendAckDataTransfer(Calendar time, int bytesTransferred) {
|
|
||||||
byte[] ack = new byte[]{
|
|
||||||
MiBandService.COMMAND_CONFIRM_ACTIVITY_DATA_TRANSFER_COMPLETE,
|
|
||||||
(byte) (time.get(Calendar.YEAR) - 2000),
|
|
||||||
(byte) time.get(Calendar.MONTH),
|
|
||||||
(byte) time.get(Calendar.DATE),
|
|
||||||
(byte) time.get(Calendar.HOUR_OF_DAY),
|
|
||||||
(byte) time.get(Calendar.MINUTE),
|
|
||||||
(byte) time.get(Calendar.SECOND),
|
|
||||||
(byte) (bytesTransferred & 0xff),
|
|
||||||
(byte) (0xff & (bytesTransferred >> 8))
|
|
||||||
};
|
|
||||||
try {
|
|
||||||
TransactionBuilder builder = performInitialized("send acknowledge");
|
|
||||||
builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_CONTROL_POINT), ack);
|
|
||||||
builder.queue(getQueue());
|
|
||||||
|
|
||||||
// flush to the DB after sending the ACK
|
|
||||||
flushActivityDataHolder();
|
|
||||||
|
|
||||||
//The last data chunk sent by the miband has always length 0.
|
|
||||||
//When we ack this chunk, the transfer is done.
|
|
||||||
if (getDevice().isBusy() && bytesTransferred == 0) {
|
|
||||||
handleActivityFetchFinish();
|
|
||||||
}
|
|
||||||
} catch (IOException ex) {
|
|
||||||
LOG.error("Unable to send ack to MI", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleActivityFetchFinish() {
|
|
||||||
LOG.info("Fetching activity data has finished.");
|
|
||||||
activityStruct = null;
|
|
||||||
unsetBusy();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleBatteryInfo(byte[] value, int status) {
|
private void handleBatteryInfo(byte[] value, int status) {
|
||||||
if (status == BluetoothGatt.GATT_SUCCESS) {
|
if (status == BluetoothGatt.GATT_SUCCESS) {
|
||||||
BatteryInfo info = new BatteryInfo(value);
|
BatteryInfo info = new BatteryInfo(value);
|
||||||
@ -1145,4 +917,10 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport {
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// overridden to make visible to operations
|
||||||
|
@Override
|
||||||
|
public TransactionBuilder performInitialized(String taskName) throws IOException {
|
||||||
|
return super.performInitialized(taskName);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,115 @@
|
|||||||
|
package nodomain.freeyourgadget.gadgetbridge.service.devices.miband.operations;
|
||||||
|
|
||||||
|
import android.bluetooth.BluetoothGatt;
|
||||||
|
import android.bluetooth.BluetoothGattCharacteristic;
|
||||||
|
import android.bluetooth.BluetoothGattDescriptor;
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEQueue;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.btle.GattCallback;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.MiBandSupport;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract base class for a MiBandOperation, i.e. an operation that does more than
|
||||||
|
* just sending a few bytes to the band. It typically involves exchanging many messages
|
||||||
|
* between the mobile and the band.
|
||||||
|
*
|
||||||
|
* One operation may execute multiple @{link Transaction transactions} with each
|
||||||
|
* multiple @{link BTLEAction actions}.
|
||||||
|
*
|
||||||
|
* This class implements GattCallback so that subclasses may override those methods
|
||||||
|
* to handle those events.
|
||||||
|
* Note: by default all Gatt events are forwarded to MiBandSupport, subclasses may override
|
||||||
|
* this behavior.
|
||||||
|
*/
|
||||||
|
public abstract class AbstractMiBandOperation implements GattCallback, MiBandOperation {
|
||||||
|
private final MiBandSupport mSupport;
|
||||||
|
|
||||||
|
protected AbstractMiBandOperation(MiBandSupport support) {
|
||||||
|
mSupport = support;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delegates to MiBandSupport and additionally sets this instance as the Gatt
|
||||||
|
* callback for the transaction.
|
||||||
|
* @param taskName
|
||||||
|
* @return
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public TransactionBuilder performInitialized(String taskName) throws IOException {
|
||||||
|
TransactionBuilder builder = mSupport.performInitialized(taskName);
|
||||||
|
builder.setGattCallback(this);
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Context getContext() {
|
||||||
|
return mSupport.getContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected GBDevice getDevice() {
|
||||||
|
return mSupport.getDevice();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected BluetoothGattCharacteristic getCharacteristic(UUID uuid) {
|
||||||
|
return mSupport.getCharacteristic(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected BtLEQueue getQueue() {
|
||||||
|
return mSupport.getQueue();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void unsetBusy() {
|
||||||
|
getDevice().unsetBusyTask();
|
||||||
|
getDevice().sendDeviceUpdateIntent(getContext());
|
||||||
|
}
|
||||||
|
|
||||||
|
public MiBandSupport getSupport() {
|
||||||
|
return mSupport;
|
||||||
|
}
|
||||||
|
|
||||||
|
// All Gatt callbacks delegated to MiBandSupport
|
||||||
|
@Override
|
||||||
|
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
|
||||||
|
mSupport.onConnectionStateChange(gatt, status, newState);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onServicesDiscovered(BluetoothGatt gatt) {
|
||||||
|
mSupport.onServicesDiscovered(gatt);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
|
||||||
|
mSupport.onCharacteristicRead(gatt, characteristic, status);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
|
||||||
|
mSupport.onCharacteristicWrite(gatt, characteristic, status);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
|
||||||
|
mSupport.onCharacteristicChanged(gatt, characteristic);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
|
||||||
|
mSupport.onDescriptorRead(gatt, descriptor, status);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
|
||||||
|
mSupport.onDescriptorWrite(gatt, descriptor, status);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) {
|
||||||
|
mSupport.onReadRemoteRssi(gatt, rssi, status);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,265 @@
|
|||||||
|
package nodomain.freeyourgadget.gadgetbridge.service.devices.miband.operations;
|
||||||
|
|
||||||
|
import android.bluetooth.BluetoothGatt;
|
||||||
|
import android.bluetooth.BluetoothGattCharacteristic;
|
||||||
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.text.DateFormat;
|
||||||
|
import java.util.Calendar;
|
||||||
|
import java.util.GregorianCalendar;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandService;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceBusyAction;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.MiBandSupport;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||||
|
|
||||||
|
public class FetchActivityOperation extends AbstractMiBandOperation {
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(FetchActivityOperation.class);
|
||||||
|
private static final byte[] fetch = new byte[]{MiBandService.COMMAND_FETCH_DATA};
|
||||||
|
|
||||||
|
//temporary buffer, size is a multiple of 60 because we want to store complete minutes (1 minute = 3 bytes)
|
||||||
|
private static final int activityDataHolderSize = 3 * 60 * 4; // 8h
|
||||||
|
|
||||||
|
private static class ActivityStruct {
|
||||||
|
public byte[] activityDataHolder = new byte[activityDataHolderSize];
|
||||||
|
//index of the buffer above
|
||||||
|
public int activityDataHolderProgress = 0;
|
||||||
|
//number of bytes we will get in a single data transfer, used as counter
|
||||||
|
public int activityDataRemainingBytes = 0;
|
||||||
|
//same as above, but remains untouched for the ack message
|
||||||
|
public int activityDataUntilNextHeader = 0;
|
||||||
|
//timestamp of the single data transfer, incremented to store each minute's data
|
||||||
|
public GregorianCalendar activityDataTimestampProgress = null;
|
||||||
|
//same as above, but remains untouched for the ack message
|
||||||
|
public GregorianCalendar activityDataTimestampToAck = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ActivityStruct activityStruct;
|
||||||
|
|
||||||
|
public FetchActivityOperation(MiBandSupport support) {
|
||||||
|
super(support);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void perform() throws IOException {
|
||||||
|
TransactionBuilder builder = performInitialized("fetch activity data");
|
||||||
|
// builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_LE_PARAMS), getLowLatency());
|
||||||
|
builder.add(new SetDeviceBusyAction(getDevice(), getContext().getString(R.string.busy_task_fetch_activity_data), getContext()));
|
||||||
|
builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_CONTROL_POINT), fetch);
|
||||||
|
builder.queue(getQueue());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCharacteristicChanged(BluetoothGatt gatt,
|
||||||
|
BluetoothGattCharacteristic characteristic) {
|
||||||
|
UUID characteristicUUID = characteristic.getUuid();
|
||||||
|
if (MiBandService.UUID_CHARACTERISTIC_ACTIVITY_DATA.equals(characteristicUUID)) {
|
||||||
|
handleActivityNotif(characteristic.getValue());
|
||||||
|
} else {
|
||||||
|
super.onCharacteristicChanged(gatt, characteristic);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleActivityFetchFinish() {
|
||||||
|
LOG.info("Fetching activity data has finished.");
|
||||||
|
activityStruct = null;
|
||||||
|
unsetBusy();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to handle the incoming activity data.
|
||||||
|
* There are two kind of messages we currently know:
|
||||||
|
* - the first one is 11 bytes long and contains metadata (how many bytes to expect, when the data starts, etc.)
|
||||||
|
* - the second one is 20 bytes long and contains the actual activity data
|
||||||
|
*
|
||||||
|
* The first message type is parsed by this method, for every other length of the value param, bufferActivityData is called.
|
||||||
|
* @see #bufferActivityData(byte[])
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @param value
|
||||||
|
*/
|
||||||
|
private void handleActivityNotif(byte[] value) {
|
||||||
|
boolean firstChunk = activityStruct == null;
|
||||||
|
if (firstChunk) {
|
||||||
|
activityStruct = new ActivityStruct();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.length == 11) {
|
||||||
|
// byte 0 is the data type: 1 means that each minute is represented by a triplet of bytes
|
||||||
|
int dataType = value[0];
|
||||||
|
// byte 1 to 6 represent a timestamp
|
||||||
|
GregorianCalendar timestamp = parseTimestamp(value, 1);
|
||||||
|
|
||||||
|
// counter of all data held by the band
|
||||||
|
int totalDataToRead = (value[7] & 0xff) | ((value[8] & 0xff) << 8);
|
||||||
|
totalDataToRead *= (dataType == 1) ? 3 : 1;
|
||||||
|
|
||||||
|
|
||||||
|
// counter of this data block
|
||||||
|
int dataUntilNextHeader = (value[9] & 0xff) | ((value[10] & 0xff) << 8);
|
||||||
|
dataUntilNextHeader *= (dataType == 1) ? 3 : 1;
|
||||||
|
|
||||||
|
// there is a total of totalDataToRead that will come in chunks (3 bytes per minute if dataType == 1),
|
||||||
|
// these chunks are usually 20 bytes long and grouped in blocks
|
||||||
|
// after dataUntilNextHeader bytes we will get a new packet of 11 bytes that should be parsed
|
||||||
|
// as we just did
|
||||||
|
|
||||||
|
if (firstChunk && dataUntilNextHeader != 0) {
|
||||||
|
GB.toast(getContext().getString(R.string.user_feedback_miband_activity_data_transfer,
|
||||||
|
DateTimeUtils.formatDurationHoursMinutes((totalDataToRead / 3), TimeUnit.MINUTES),
|
||||||
|
DateFormat.getDateTimeInstance().format(timestamp.getTime())), Toast.LENGTH_LONG, GB.INFO);
|
||||||
|
}
|
||||||
|
LOG.info("total data to read: " + totalDataToRead + " len: " + (totalDataToRead / 3) + " minute(s)");
|
||||||
|
LOG.info("data to read until next header: " + dataUntilNextHeader + " len: " + (dataUntilNextHeader / 3) + " minute(s)");
|
||||||
|
LOG.info("TIMESTAMP: " + DateFormat.getDateTimeInstance().format(timestamp.getTime()).toString() + " magic byte: " + dataUntilNextHeader);
|
||||||
|
|
||||||
|
activityStruct.activityDataRemainingBytes = activityStruct.activityDataUntilNextHeader = dataUntilNextHeader;
|
||||||
|
activityStruct.activityDataTimestampToAck = (GregorianCalendar) timestamp.clone();
|
||||||
|
activityStruct.activityDataTimestampProgress = timestamp;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
bufferActivityData(value);
|
||||||
|
}
|
||||||
|
LOG.debug("activity data: length: " + value.length + ", remaining bytes: " + activityStruct.activityDataRemainingBytes);
|
||||||
|
|
||||||
|
if (activityStruct.activityDataRemainingBytes == 0) {
|
||||||
|
sendAckDataTransfer(activityStruct.activityDataTimestampToAck, activityStruct.activityDataUntilNextHeader);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to store temporarily the activity data values got from the Mi Band.
|
||||||
|
*
|
||||||
|
* Since we expect chunks of 20 bytes each, we do not store the received bytes it the length is different.
|
||||||
|
*
|
||||||
|
* @param value
|
||||||
|
*/
|
||||||
|
private void bufferActivityData(byte[] value) {
|
||||||
|
|
||||||
|
if (activityStruct.activityDataRemainingBytes >= value.length) {
|
||||||
|
//I don't like this clause, but until we figure out why we get different data sometimes this should work
|
||||||
|
if (value.length == 20 || value.length == activityStruct.activityDataRemainingBytes) {
|
||||||
|
System.arraycopy(value, 0, activityStruct.activityDataHolder, activityStruct.activityDataHolderProgress, value.length);
|
||||||
|
activityStruct.activityDataHolderProgress += value.length;
|
||||||
|
activityStruct.activityDataRemainingBytes -= value.length;
|
||||||
|
|
||||||
|
if (this.activityDataHolderSize == activityStruct.activityDataHolderProgress) {
|
||||||
|
flushActivityDataHolder();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// the length of the chunk is not what we expect. We need to make sense of this data
|
||||||
|
LOG.warn("GOT UNEXPECTED ACTIVITY DATA WITH LENGTH: " + value.length + ", EXPECTED LENGTH: " + activityStruct.activityDataRemainingBytes);
|
||||||
|
for (byte b : value) {
|
||||||
|
LOG.warn("DATA: " + String.format("0x%8x", b));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LOG.error("error buffering activity data: remaining bytes: " + activityStruct.activityDataRemainingBytes + ", received: " + value.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* empty the local buffer for activity data, arrange the values received in groups of three and
|
||||||
|
* store them in the DB
|
||||||
|
*/
|
||||||
|
private void flushActivityDataHolder() {
|
||||||
|
if (activityStruct == null) {
|
||||||
|
LOG.debug("nothing to flush, struct is already null");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
LOG.debug("flushing activity data holder");
|
||||||
|
byte category, intensity, steps;
|
||||||
|
|
||||||
|
DBHandler dbHandler = null;
|
||||||
|
try {
|
||||||
|
dbHandler = GBApplication.acquireDB();
|
||||||
|
try (SQLiteDatabase db = dbHandler.getWritableDatabase()) { // explicitly keep the db open while looping over the samples
|
||||||
|
for (int i = 0; i < activityStruct.activityDataHolderProgress; i += 3) { //TODO: check if multiple of 3, if not something is wrong
|
||||||
|
category = activityStruct.activityDataHolder[i];
|
||||||
|
intensity = activityStruct.activityDataHolder[i + 1];
|
||||||
|
steps = activityStruct.activityDataHolder[i + 2];
|
||||||
|
|
||||||
|
dbHandler.addGBActivitySample(
|
||||||
|
(int) (activityStruct.activityDataTimestampProgress.getTimeInMillis() / 1000),
|
||||||
|
SampleProvider.PROVIDER_MIBAND,
|
||||||
|
(short) (intensity & 0xff),
|
||||||
|
(short) (steps & 0xff),
|
||||||
|
category);
|
||||||
|
activityStruct.activityDataTimestampProgress.add(Calendar.MINUTE, 1);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
activityStruct.activityDataHolderProgress = 0;
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
GB.toast(getContext(), ex.getMessage(), Toast.LENGTH_LONG, GB.ERROR);
|
||||||
|
} finally {
|
||||||
|
if (dbHandler != null) {
|
||||||
|
dbHandler.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Acknowledge the transfer of activity data to the Mi Band.
|
||||||
|
*
|
||||||
|
* After receiving data from the band, it has to be acknowledged. This way the Mi Band will delete
|
||||||
|
* the data it has on record.
|
||||||
|
*
|
||||||
|
* @param time
|
||||||
|
* @param bytesTransferred
|
||||||
|
*/
|
||||||
|
private void sendAckDataTransfer(Calendar time, int bytesTransferred) {
|
||||||
|
byte[] ack = new byte[]{
|
||||||
|
MiBandService.COMMAND_CONFIRM_ACTIVITY_DATA_TRANSFER_COMPLETE,
|
||||||
|
(byte) (time.get(Calendar.YEAR) - 2000),
|
||||||
|
(byte) time.get(Calendar.MONTH),
|
||||||
|
(byte) time.get(Calendar.DATE),
|
||||||
|
(byte) time.get(Calendar.HOUR_OF_DAY),
|
||||||
|
(byte) time.get(Calendar.MINUTE),
|
||||||
|
(byte) time.get(Calendar.SECOND),
|
||||||
|
(byte) (bytesTransferred & 0xff),
|
||||||
|
(byte) (0xff & (bytesTransferred >> 8))
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
TransactionBuilder builder = performInitialized("send acknowledge");
|
||||||
|
builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_CONTROL_POINT), ack);
|
||||||
|
builder.queue(getQueue());
|
||||||
|
|
||||||
|
// flush to the DB after sending the ACK
|
||||||
|
flushActivityDataHolder();
|
||||||
|
|
||||||
|
//The last data chunk sent by the miband has always length 0.
|
||||||
|
//When we ack this chunk, the transfer is done.
|
||||||
|
if (getDevice().isBusy() && bytesTransferred == 0) {
|
||||||
|
handleActivityFetchFinish();
|
||||||
|
}
|
||||||
|
} catch (IOException ex) {
|
||||||
|
LOG.error("Unable to send ack to MI", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private GregorianCalendar parseTimestamp(byte[] value, int offset) {
|
||||||
|
GregorianCalendar timestamp = new GregorianCalendar(
|
||||||
|
value[offset] + 2000,
|
||||||
|
value[offset + 1],
|
||||||
|
value[offset + 2],
|
||||||
|
value[offset + 3],
|
||||||
|
value[offset + 4],
|
||||||
|
value[offset + 5]);
|
||||||
|
return timestamp;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
package nodomain.freeyourgadget.gadgetbridge.service.devices.miband.operations;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.MiBandSupport;
|
||||||
|
|
||||||
|
public interface MiBandOperation {
|
||||||
|
public void perform() throws IOException;
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user