1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-12-04 07:52:56 +01:00

Da Fit: Training data transfer

This commit is contained in:
krzys-h 2019-12-29 11:29:40 +01:00 committed by Arjan Schrijver
parent 7567ebbc6d
commit 6cc6d2c8ca
8 changed files with 432 additions and 10 deletions

View File

@ -114,11 +114,31 @@ public class DaFitConstants {
public static final byte CMD_HS_DFU = 99; // (?) {1} - enableHsDfu(), {0} - queryHsDfuAddress()
// Activity tracking (?)
public static final byte CMD_QUERY_LAST_DYNAMIC_RATE = 52; // (?) {} -> ???
public static final byte CMD_QUERY_PAST_HEART_RATE_1 = 53; // (?) {4} - pastHeartRate(), {0} - todayHeartRate(1) -> ???
public static final byte CMD_QUERY_PAST_HEART_RATE_2 = 54; // (?) {0} - todayHeartRate(2) -> ???
public static final byte CMD_QUERY_MOVEMENT_HEART_RATE = 55; // (?) {} -> ???
// Activity/training tracking
// CMD_QUERY_LAST_DYNAMIC_RATE is triggered immediately after a training recording is finished on the watch.
// The watch sends CMD_QUERY_LAST_DYNAMIC_RATE command to the phone with the first part of the data, and then
// the phone is supposed to respond with empty CMD_QUERY_LAST_DYNAMIC_RATE to retrieve the next part.
// There seems to be no way to query this data later, or to start communication from phone side.
// The data format is uint32 date_recorded, uint8 heart_rate[] (where 0 is invalid measurement and
// data is recorded every 1 minute)
// CMD_QUERY_MOVEMENT_HEART_RATE returns the summary of last 3 trainings recorded on the watch.
// This is a cyclic buffer, so the watch will first overwrite entry number 0, then 1, then 2, then 0 again
// CMD_QUERY_PAST_HEART_RATE_1 and CMD_QUERY_PAST_HEART_RATE_2 don't seem to work at all on my watch.
// All "date recorded" values are in the hardcoded GMT+8 watch timezone
public static final byte CMD_QUERY_LAST_DYNAMIC_RATE = 52; // TRANSMISSION TRIGGERED FROM WATCH SIDE AFTER FINISHED TRAINING. Does custom packet splitting. The packet takes no data as input. Send the query repeatedly until you get all the data. THE FIRST PACKET IS SENT BY THE WATCH - THE PHONE QUERIES THIS COMMAND TO GET THE NEXT PART. The response starts with one byte: 0 for first packet, 1 for continuation packet, 2 for end of data. 0,time:uint32,measurement:uint8[] 1,measurement:uint8[] 1,measurement:uint8[] 2
public static final byte CMD_QUERY_PAST_HEART_RATE_1 = 53; // (*) Two arrays built of 4 packets each. See below. todayHeartRate(1) starts at 0 and ends at 3, yesterdayHeartRate() starts at 4 and ends at 7. Sampled every 5 minutes.
public static final byte CMD_QUERY_PAST_HEART_RATE_2 = 54; // (*) An array built of 20 packets. The packet takes the index as input. i.e. {x} -> {data[N*x], data[N*x+1], ..., data[N*x+N-1]} for x in 0-19 -- todayHeartRate(2). Sampled every 1 minute.
public static final byte CMD_QUERY_MOVEMENT_HEART_RATE = 55; // {} -> One packet with 3 entries of 24 bytes each {startTime:uint32, endTime:uint32, validTime:uint16, entry_number:uint8, type:uint8, steps:uint32, distance:uint32, calories:uint16}, everything little endian
// first byte for CMD_QUERY_LAST_DYNAMIC_RATE packets
public static final byte ARG_TRANSMISSION_FIRST = 0;
public static final byte ARG_TRANSMISSION_NEXT = 1;
public static final byte ARG_TRANSMISSION_LAST = 2; // note: last packet always empty
// Health measurements
public static final byte CMD_QUERY_TIMING_MEASURE_HEART_RATE = 47; // (*) {} -> ???
@ -165,7 +185,7 @@ public class DaFitConstants {
public static final byte CMD_SWITCH_CAMERA_VIEW = 102; // {} -> {}, outgoing open screen, incoming take photo
public static final byte CMD_NOTIFY_PHONE_OPERATION = 103; // ONLY INCOMING! -> {x}, x -> 0 = play/pause, 1 = prev, 2 = next, 3 = reject incoming call)
public static final byte CMD_NOTIFY_WEATHER_CHANGE = 100; // (?) ONLY INCOMING! -> {}
public static final byte CMD_NOTIFY_WEATHER_CHANGE = 100; // ONLY INCOMING! -> {} - when the watch really wants us to retransmit the weather again (it seems to often happen after stopping training - running the training blocks access to main menu so I guess it restarts afterwards or something). Will repeat whenever navigating the menu where the weather should be, and weather won't be visible on watch screen until that happens.
public static final byte ARG_OPERATION_PLAY_PAUSE = 0;
public static final byte ARG_OPERATION_PREV_SONG = 1;

View File

@ -173,7 +173,7 @@ public class DaFitDeviceCoordinator extends AbstractDeviceCoordinator {
@Override
public boolean supportsActivityTracks() {
return false;
return true;
}
@Override

View File

@ -107,6 +107,19 @@ public class DaFitSampleProvider extends AbstractSampleProvider<DaFitActivitySam
return ActivityKind.TYPE_DEEP_SLEEP;
else if (rawType == ACTIVITY_SLEEP_START || rawType == ACTIVITY_SLEEP_END)
return ActivityKind.TYPE_NOT_MEASURED;
else if (rawType == ACTIVITY_TRAINING_WALK)
return ActivityKind.TYPE_WALKING;
else if (rawType == ACTIVITY_TRAINING_RUN)
return ActivityKind.TYPE_RUNNING;
else if (rawType == ACTIVITY_TRAINING_BIKING)
return ActivityKind.TYPE_CYCLING;
else if (rawType == ACTIVITY_TRAINING_SWIM)
return ActivityKind.TYPE_SWIMMING;
else if (rawType == ACTIVITY_TRAINING_ROPE || rawType == ACTIVITY_TRAINING_BADMINTON ||
rawType == ACTIVITY_TRAINING_BASKETBALL || rawType == ACTIVITY_TRAINING_FOOTBALL ||
rawType == ACTIVITY_TRAINING_MOUNTAINEERING || rawType == ACTIVITY_TRAINING_TENNIS ||
rawType == ACTIVITY_TRAINING_RUGBY || rawType == ACTIVITY_TRAINING_GOLF)
return ActivityKind.TYPE_EXERCISE;
else
return ActivityKind.TYPE_ACTIVITY;
}

View File

@ -34,6 +34,7 @@ import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
@ -46,6 +47,7 @@ import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import de.greenrobot.dao.query.QueryBuilder;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.Logging;
import nodomain.freeyourgadget.gadgetbridge.R;
@ -73,10 +75,13 @@ import nodomain.freeyourgadget.gadgetbridge.devices.dafit.settings.DaFitSettingR
import nodomain.freeyourgadget.gadgetbridge.devices.dafit.settings.DaFitSettingTimeRange;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst;
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummaryDao;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.entities.DaFitActivitySample;
import nodomain.freeyourgadget.gadgetbridge.entities.User;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
@ -99,14 +104,13 @@ import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.battery.Batter
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfo;
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfoProfile;
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.heartrate.HeartRateProfile;
import nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils;
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.NotificationUtils;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
// TODO: figure out the training data
public class DaFitDeviceSupport extends AbstractBTLEDeviceSupport {
private static final Logger LOG = LoggerFactory.getLogger(DaFitDeviceSupport.class);
@ -320,6 +324,17 @@ public class DaFitDeviceSupport extends AbstractBTLEDeviceSupport {
return true;
}
if (packetType == DaFitConstants.CMD_QUERY_LAST_DYNAMIC_RATE)
{
// Training on the watch just finished and it wants us to fetch the details
LOG.info("Starting training fetch");
try {
new TrainingFinishedDataOperation(this, payload).perform();
} catch (IOException e) {
e.printStackTrace();
}
}
if (packetType == DaFitConstants.CMD_NOTIFY_PHONE_OPERATION)
{
byte operation = payload[0];
@ -359,6 +374,13 @@ public class DaFitDeviceSupport extends AbstractBTLEDeviceSupport {
return true;
}
if (packetType == DaFitConstants.CMD_NOTIFY_WEATHER_CHANGE)
{
LOG.info("The watch really wants us to transmit the weather data for some reason...");
// TODO: transmit weather
return true;
}
for (DaFitSetting setting : queriedSettings)
{
if (setting.cmdQuery == packetType)
@ -861,6 +883,96 @@ public class DaFitDeviceSupport extends AbstractBTLEDeviceSupport {
}
}
public void handleTrainingData(byte[] data)
{
if (data.length % 24 != 0)
throw new IllegalArgumentException();
for(int i = 0; i < data.length / 24; i++)
{
if (ArrayUtils.isAllZeros(data, 24*i, 24)) // no data recorded in this slot
continue;
ByteBuffer buffer = ByteBuffer.wrap(data, 24 * i, 24);
buffer.order(ByteOrder.LITTLE_ENDIAN);
Date startTime = DaFitConstants.WatchTimeToLocalTime(buffer.getInt());
Date endTime = DaFitConstants.WatchTimeToLocalTime(buffer.getInt());
int validTime = buffer.getShort();
byte num = buffer.get(); // == i
byte type = buffer.get();
int steps = buffer.getInt();
int distance = buffer.getInt();
int calories = buffer.getShort();
Log.i("Training data", "start=" + startTime + " end=" + endTime + " totalTimeWithoutPause=" + validTime + " num=" + num + " type=" + type + " steps=" + steps + " distance=" + distance + " calories=" + calories);
// NOTE: We are ignoring the step/distance/calories data here
// If we had the phone connected, the realtime data is already stored anyway, and I'm
// too lazy to try to integrate this info into the main timeline without messing
// something up or counting the steps twice
try (DBHandler dbHandler = GBApplication.acquireDB()) {
User user = DBHelper.getUser(dbHandler.getDaoSession());
Device device = DBHelper.getDevice(getDevice(), dbHandler.getDaoSession());
DaFitSampleProvider provider = new DaFitSampleProvider(getDevice(), dbHandler.getDaoSession());
BaseActivitySummaryDao summaryDao = provider.getSession().getBaseActivitySummaryDao();
QueryBuilder<BaseActivitySummary> qb = summaryDao.queryBuilder();
qb.where(BaseActivitySummaryDao.Properties.DeviceId.eq(device.getId()))
.where(BaseActivitySummaryDao.Properties.StartTime.eq(startTime))
.where(BaseActivitySummaryDao.Properties.EndTime.eq(endTime));
boolean alreadyHaveThisSample = qb.count() > 0;
if (alreadyHaveThisSample)
{
LOG.info("Already had this training sample, ignoring");
}
else
{
BaseActivitySummary summary = new BaseActivitySummary();
summary.setDevice(device);
summary.setUser(user);
int gbType = provider.normalizeType(type);
String name;
if (type == DaFitSampleProvider.ACTIVITY_TRAINING_ROPE)
name = "Rope";
else if (type == DaFitSampleProvider.ACTIVITY_TRAINING_BADMINTON)
name = "Badminton";
else if (type == DaFitSampleProvider.ACTIVITY_TRAINING_BASKETBALL)
name = "Basketball";
else if (type == DaFitSampleProvider.ACTIVITY_TRAINING_FOOTBALL)
name = "Football";
else if (type == DaFitSampleProvider.ACTIVITY_TRAINING_MOUNTAINEERING)
name = "Mountaineering";
else if (type == DaFitSampleProvider.ACTIVITY_TRAINING_TENNIS)
name = "Tennis";
else if (type == DaFitSampleProvider.ACTIVITY_TRAINING_RUGBY)
name = "Rugby";
else if (type == DaFitSampleProvider.ACTIVITY_TRAINING_GOLF)
name = "Golf";
else
name = ActivityKind.asString(gbType, getContext());
summary.setName(name);
summary.setActivityKind(gbType);
summary.setStartTime(startTime);
summary.setEndTime(endTime);
summaryDao.insert(summary);
// NOTE: The type format from device maps directly to the database format
provider.updateActivityInRange((int)(startTime.getTime() / 1000), (int)(endTime.getTime() / 1000), type);
}
} catch (Exception ex) {
ex.printStackTrace();
GB.toast(getContext(), "Error saving samples: " + ex.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR);
GB.updateTransferNotification(null, "Data transfer failed", false, 0, getContext());
}
}
}
private void addGBActivitySampleIfNotExists(DaFitSampleProvider provider, DaFitActivitySample sample)
{
boolean alreadyHaveThisSample = false;

View File

@ -41,6 +41,7 @@ public class FetchDataOperation extends AbstractBTLEOperation<DaFitDeviceSupport
private boolean[] receivedSteps = new boolean[3];
private boolean[] receivedSleep = new boolean[3];
private boolean receivedTrainingData = false;
private DaFitPacketIn packetIn = new DaFitPacketIn();
@ -63,6 +64,7 @@ public class FetchDataOperation extends AbstractBTLEOperation<DaFitDeviceSupport
getSupport().sendPacket(builder, DaFitPacketOut.buildPacket(DaFitConstants.CMD_SYNC_PAST_SLEEP_AND_STEP, new byte[] { DaFitConstants.ARG_SYNC_YESTERDAY_STEPS }));
getSupport().sendPacket(builder, DaFitPacketOut.buildPacket(DaFitConstants.CMD_SYNC_PAST_SLEEP_AND_STEP, new byte[] { DaFitConstants.ARG_SYNC_DAY_BEFORE_YESTERDAY_STEPS }));
builder.read(getCharacteristic(DaFitConstants.UUID_CHARACTERISTIC_STEPS));
getSupport().sendPacket(builder, DaFitPacketOut.buildPacket(DaFitConstants.CMD_QUERY_MOVEMENT_HEART_RATE, new byte[] { }));
builder.queue(getQueue());
updateProgressAndCheckFinish();
@ -153,6 +155,10 @@ public class FetchDataOperation extends AbstractBTLEOperation<DaFitDeviceSupport
return true;
}
}
if (packetType == DaFitConstants.CMD_QUERY_MOVEMENT_HEART_RATE) {
decodeTrainingData(payload);
return true;
}
return false;
}
@ -170,16 +176,25 @@ public class FetchDataOperation extends AbstractBTLEOperation<DaFitDeviceSupport
updateProgressAndCheckFinish();
}
private void decodeTrainingData(byte[] data)
{
getSupport().handleTrainingData(data);
receivedTrainingData = true;
updateProgressAndCheckFinish();
}
private void updateProgressAndCheckFinish()
{
int count = 0;
int total = receivedSteps.length + receivedSleep.length;
int total = receivedSteps.length + receivedSleep.length + 1;
for(int i = 0; i < receivedSteps.length; i++)
if (receivedSteps[i])
++count;
for(int i = 0; i < receivedSleep.length; i++)
if (receivedSleep[i])
++count;
if (receivedTrainingData)
++count;
GB.updateTransferNotification(null, getContext().getString(R.string.busy_task_fetch_activity_data), true, 100 * count / total, getContext());
if (count == total)
operationFinished();

View File

@ -0,0 +1,235 @@
/* Copyright (C) 2019 krzys_h
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.dafit;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.util.Log;
import android.util.Pair;
import android.widget.Toast;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Calendar;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.Logging;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.devices.dafit.DaFitConstants;
import nodomain.freeyourgadget.gadgetbridge.devices.dafit.DaFitSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.entities.DaFitActivitySample;
import nodomain.freeyourgadget.gadgetbridge.entities.User;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEOperation;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.operations.OperationStatus;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class TrainingFinishedDataOperation extends AbstractBTLEOperation<DaFitDeviceSupport> {
private static final Logger LOG = LoggerFactory.getLogger(FetchDataOperation.class);
private final byte[] firstPacketData;
private final long firstPacketTimeInMillis;
ByteArrayOutputStream data = new ByteArrayOutputStream();
private DaFitPacketIn packetIn = new DaFitPacketIn();
public TrainingFinishedDataOperation(DaFitDeviceSupport support, byte[] firstPacketData) {
super(support);
this.firstPacketData = firstPacketData;
this.firstPacketTimeInMillis = Calendar.getInstance().getTimeInMillis();
}
@Override
protected void prePerform() {
getDevice().setBusyTask(getContext().getString(R.string.busy_task_fetch_training_data));
getDevice().sendDeviceUpdateIntent(getContext());
}
@Override
protected void doPerform() {
GB.updateTransferNotification(null, getContext().getString(R.string.busy_task_fetch_training_data), true, 0, getContext());
handleTrainingHealthRatePacket(firstPacketData, true);
}
@Override
public boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
if (!isOperationRunning())
{
LOG.error("onCharacteristicChanged but operation is not running!");
}
else
{
UUID charUuid = characteristic.getUuid();
if (charUuid.equals(DaFitConstants.UUID_CHARACTERISTIC_DATA_IN))
{
if (packetIn.putFragment(characteristic.getValue())) {
Pair<Byte, byte[]> packet = DaFitPacketIn.parsePacket(packetIn.getPacket());
packetIn = new DaFitPacketIn();
if (packet != null) {
byte packetType = packet.first;
byte[] payload = packet.second;
if (handlePacket(packetType, payload))
return true;
}
}
}
}
return super.onCharacteristicChanged(gatt, characteristic);
}
private boolean handlePacket(byte packetType, byte[] payload) {
if (packetType == DaFitConstants.CMD_QUERY_LAST_DYNAMIC_RATE) {
handleTrainingHealthRatePacket(payload, false);
return true;
}
if (packetType == DaFitConstants.CMD_QUERY_MOVEMENT_HEART_RATE) {
handleTrainingSummaryDataPacket(payload);
return true;
}
return false;
}
private void handleTrainingHealthRatePacket(byte[] payload, boolean isFirst) {
Log.i("TRAINING DATA", "data: " + Logging.formatBytes(payload));
byte sequenceType = payload[0];
if (isFirst != (sequenceType == DaFitConstants.ARG_TRANSMISSION_FIRST))
throw new IllegalArgumentException("Expected packet to be " + (isFirst ? "first" : "continued") + " but got packet of type " + sequenceType);
if (sequenceType == DaFitConstants.ARG_TRANSMISSION_LAST && payload.length > 1)
throw new IllegalArgumentException("Last packet shouldn't have any data");
data.write(payload, 1, payload.length - 1);
if (sequenceType != DaFitConstants.ARG_TRANSMISSION_LAST)
queryMoreData();
else
processAllData();
}
private void queryMoreData() {
try {
TransactionBuilder builder = performInitialized("TrainingFinishedDataOperation");
getSupport().sendPacket(builder, DaFitPacketOut.buildPacket(DaFitConstants.CMD_QUERY_LAST_DYNAMIC_RATE, new byte[0]));
builder.queue(getQueue());
} catch (IOException e) {
e.printStackTrace();
GB.toast(getContext(), "Error fetching training data: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR);
GB.updateTransferNotification(null, "Data transfer failed", false, 0, getContext());
operationFinished();
}
}
private void processAllData() {
byte[] completeData = data.toByteArray();
Log.i("HAVE COMPLETE DATA", Logging.formatBytes(completeData));
ByteBuffer dataBuffer = ByteBuffer.wrap(completeData);
dataBuffer.order(ByteOrder.LITTLE_ENDIAN);
Calendar dateRecorded = Calendar.getInstance();
dateRecorded.setTime(DaFitConstants.WatchTimeToLocalTime(dataBuffer.getInt()));
// NOTE: The first sample always matches dateRecorded (which is aligned to the minute)
// The last sample is saved at the moment the recording is stopped (and this code starts executing)
try (DBHandler dbHandler = GBApplication.acquireDB()) {
User user = DBHelper.getUser(dbHandler.getDaoSession());
Device device = DBHelper.getDevice(getDevice(), dbHandler.getDaoSession());
DaFitSampleProvider provider = new DaFitSampleProvider(getDevice(), dbHandler.getDaoSession());
Log.i("START DATE", dateRecorded.getTime().toString());
while (dataBuffer.hasRemaining())
{
int measurement = dataBuffer.get() & 0xFF;
if (!dataBuffer.hasRemaining())
dateRecorded.setTimeInMillis(firstPacketTimeInMillis); // the last sample is captured exactly at the end of measurement
Log.i("MEASUREMENT", "at " + dateRecorded.getTime().toString() + " was " + measurement);
DaFitActivitySample sample = new DaFitActivitySample();
sample.setDevice(device);
sample.setUser(user);
sample.setProvider(provider);
sample.setTimestamp((int)(dateRecorded.getTimeInMillis() / 1000));
sample.setRawKind(DaFitSampleProvider.ACTIVITY_NOT_MEASURED); // Training type will be taken later from CMD_QUERY_MOVEMENT_HEART_RATE (it's not present in the main data packet)
sample.setDataSource(DaFitSampleProvider.SOURCE_TRAINING_HEARTRATE);
sample.setBatteryLevel(ActivitySample.NOT_MEASURED);
sample.setSteps(ActivitySample.NOT_MEASURED);
sample.setDistanceMeters(ActivitySample.NOT_MEASURED);
sample.setCaloriesBurnt(ActivitySample.NOT_MEASURED);
sample.setHeartRate(measurement != 0 ? measurement : ActivitySample.NOT_MEASURED);
sample.setBloodPressureSystolic(ActivitySample.NOT_MEASURED);
sample.setBloodPressureDiastolic(ActivitySample.NOT_MEASURED);
sample.setBloodOxidation(ActivitySample.NOT_MEASURED);
provider.addGBActivitySample(sample);
LOG.info("Adding a training sample: " + sample.toString());
dateRecorded.add(Calendar.MINUTE, 1);
}
} catch (Exception ex) {
ex.printStackTrace();
GB.toast(getContext(), "Error saving samples: " + ex.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR);
GB.updateTransferNotification(null, "Data transfer failed", false, 0, getContext());
}
try {
TransactionBuilder builder = performInitialized("TrainingFinishedDataOperation fetch training type");
getSupport().sendPacket(builder, DaFitPacketOut.buildPacket(DaFitConstants.CMD_QUERY_MOVEMENT_HEART_RATE, new byte[] { }));
builder.queue(getQueue());
} catch (IOException e) {
e.printStackTrace();
GB.toast(getContext(), "Error fetching training data: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR);
GB.updateTransferNotification(null, "Data transfer failed", false, 0, getContext());
operationFinished();
}
}
private void handleTrainingSummaryDataPacket(byte[] payload)
{
getSupport().handleTrainingData(payload);
GB.updateTransferNotification(null, getContext().getString(R.string.busy_task_fetch_training_data_finished), false, 0, getContext());
operationFinished();
}
@Override
protected void operationFinished() {
operationStatus = OperationStatus.FINISHED;
if (getDevice() != null && getDevice().isConnected()) {
unsetBusy();
GB.signalActivityDataFinish();
}
}
}

View File

@ -119,5 +119,30 @@ public class ArrayUtils {
if (array[i] == value) return i;
}
return -1;
/**
* Check if a byte array contains all zeros
* @param array The array to check
* @param startIndex The starting position
* @param length Number of elements to check
* @return true if all checked elements were == 0, false otherwise
*/
public static boolean isAllZeros(byte[] array, int startIndex, int length)
{
for(int i = startIndex; i < startIndex + length; i++)
{
if (array[i] != 0)
return false;
}
return true;
}
/**
* Check if a byte array contains all zeros
* @param array The array to check
* @return true if all checked elements were == 0, false otherwise
*/
public static boolean isAllZeros(byte[] array)
{
return isAllZeros(array, 0, array.length);
}
}

View File

@ -709,6 +709,8 @@
<string name="busy_task_fetch_sleep_respiratory_rate_data">Fetching sleep respiratory rate data</string>
<string name="busy_task_fetch_temperature">Fetching temperature data</string>
<string name="busy_task_fetch_statistics">Fetching statistics</string>
<string name="busy_task_fetch_training_data">Fetching last training data</string>
<string name="busy_task_fetch_training_data_finished">Training data recieved!</string>
<string name="sleep_activity_date_range">From %1$s to %2$s</string>
<string name="prefs_wearside">Wearing left or right?</string>
<string name="prefs_weardirection">Wearing direction</string>