mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2024-12-04 16:02:55 +01:00
Da Fit: Training data transfer
This commit is contained in:
parent
7567ebbc6d
commit
6cc6d2c8ca
@ -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;
|
||||
|
@ -173,7 +173,7 @@ public class DaFitDeviceCoordinator extends AbstractDeviceCoordinator {
|
||||
|
||||
@Override
|
||||
public boolean supportsActivityTracks() {
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user