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

Da Fit: Add activity fetching and logging

This commit is contained in:
krzys-h 2019-12-28 14:11:42 +01:00 committed by Arjan Schrijver
parent 80c51999a2
commit 72743d893b
6 changed files with 886 additions and 8 deletions

View File

@ -49,6 +49,9 @@ public class GBDaoGenerator {
private static final String SAMPLE_TEMPERATURE = "temperature";
private static final String SAMPLE_TEMPERATURE_TYPE = "temperatureType";
private static final String SAMPLE_WEIGHT_KG = "weightKg";
private static final String SAMPLE_BLOOD_PRESSURE_SYSTOLIC = "bloodPressureSystolic";
private static final String SAMPLE_BLOOD_PRESSURE_DIASTOLIC = "bloodPressureDiastolic";
private static final String SAMPLE_BLOOD_OXIDATION = "bloodOxidation";
private static final String TIMESTAMP_FROM = "timestampFrom";
private static final String TIMESTAMP_TO = "timestampTo";
@ -586,6 +589,15 @@ public class GBDaoGenerator {
activitySample.addIntProperty(SAMPLE_HEART_RATE).notNull().codeBeforeGetterAndSetter(OVERRIDE);
}
private static void addBloodPressureProperies(Entity activitySample) {
activitySample.addIntProperty(SAMPLE_BLOOD_PRESSURE_SYSTOLIC).notNull().codeBeforeGetterAndSetter(OVERRIDE);
activitySample.addIntProperty(SAMPLE_BLOOD_PRESSURE_DIASTOLIC).notNull().codeBeforeGetterAndSetter(OVERRIDE);
}
private static void addBloodOxidationProperies(Entity activitySample) {
activitySample.addIntProperty(SAMPLE_BLOOD_OXIDATION).notNull().codeBeforeGetterAndSetter(OVERRIDE);
}
private static Entity addPebbleHealthActivitySample(Schema schema, Entity user, Entity device) {
Entity activitySample = addEntity(schema, "PebbleHealthActivitySample");
addCommonActivitySampleProperties("AbstractPebbleHealthActivitySample", activitySample, user, device);
@ -1024,6 +1036,22 @@ public class GBDaoGenerator {
return activitySample;
}
private static Entity addDaFitActivitySample(Schema schema, Entity user, Entity device) {
Entity activitySample = addEntity(schema, "DaFitActivitySample");
activitySample.implementsSerializable();
addCommonActivitySampleProperties("AbstractActivitySample", activitySample, user, device);
activitySample.addIntProperty(SAMPLE_STEPS).notNull().codeBeforeGetterAndSetter(OVERRIDE).codeBeforeGetter("@Override\n public int getRawIntensity() {\n return getSteps();\n }\n\n");
activitySample.addIntProperty(SAMPLE_RAW_KIND).notNull().codeBeforeGetterAndSetter(OVERRIDE);
activitySample.addIntProperty("dataSource").notNull();
activitySample.addIntProperty("caloriesBurnt").notNull();
activitySample.addIntProperty("distanceMeters").notNull();
addHeartRateProperties(activitySample);
addBloodPressureProperies(activitySample);
addBloodOxidationProperies(activitySample);
activitySample.addIntProperty("batteryLevel").notNull();
return activitySample;
}
private static void addCommonActivitySampleProperties(String superClass, Entity activitySample, Entity user, Entity device) {
activitySample.setSuperclass(superClass);
activitySample.addImport(MAIN_PACKAGE + ".devices.SampleProvider");

View File

@ -137,7 +137,8 @@ public class DaFitConstants {
public static final byte CMD_SYNC_SLEEP = 50; // {} -> {type, start_h, start_m}, repeating, type is SOBER(0),LIGHT(1),RESTFUL(2)
public static final byte CMD_SYNC_PAST_SLEEP_AND_STEP = 51; // {b (see below)} -> {x<=2, distance:uint24, steps:uint24, calories:uint24} or {x>2, (sleep data like above)} - two functions same CMD
// NOTE: these names are as specified in the original app. They do NOT match what my watch actually does. See note in FetchDataOperation.
public static final byte ARG_SYNC_YESTERDAY_STEPS = 1;
public static final byte ARG_SYNC_DAY_BEFORE_YESTERDAY_STEPS = 2;
public static final byte ARG_SYNC_YESTERDAY_SLEEP = 3;

View File

@ -90,17 +90,17 @@ public class DaFitDeviceCoordinator extends AbstractDeviceCoordinator {
@Override
public boolean supportsActivityDataFetching() {
return false;
return true;
}
@Override
public boolean supportsActivityTracking() {
return false;
return true;
}
@Override
public SampleProvider<? extends ActivitySample> getSampleProvider(GBDevice device, DaoSession session) {
return null;
return new DaFitSampleProvider(device, session);
}
@Override
@ -145,7 +145,7 @@ public class DaFitDeviceCoordinator extends AbstractDeviceCoordinator {
@Override
public boolean supportsRealtimeData() {
return false;
return true;
}
@Override

View File

@ -0,0 +1,187 @@
/* 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.devices.dafit;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;
import de.greenrobot.dao.AbstractDao;
import de.greenrobot.dao.Property;
import de.greenrobot.dao.internal.SqlUtils;
import de.greenrobot.dao.query.WhereCondition;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.entities.DaFitActivitySample;
import nodomain.freeyourgadget.gadgetbridge.entities.DaFitActivitySampleDao;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
public class DaFitSampleProvider extends AbstractSampleProvider<DaFitActivitySample> {
public static final int SOURCE_NOT_MEASURED = -1;
public static final int SOURCE_STEPS_REALTIME = 1; // steps gathered at realtime from the steps characteristic
public static final int SOURCE_STEPS_SUMMARY = 2; // steps gathered from the daily summary
public static final int SOURCE_STEPS_IDLE = 3; // idle sample inserted because the user was not moving (to differentiate from missing data because watch not connected)
public static final int SOURCE_SLEEP_SUMMARY = 4; // data collected from the sleep function
public static final int SOURCE_SINGLE_MEASURE = 5; // heart rate / blood data gathered from the "single measurement" function
public static final int SOURCE_TRAINING_HEARTRATE = 6; // heart rate data collected from the training function
public static final int SOURCE_BATTERY = 7; // battery report
public static final int ACTIVITY_NOT_MEASURED = -1;
public static final int ACTIVITY_TRAINING_WALK = DaFitConstants.TRAINING_TYPE_WALK;
public static final int ACTIVITY_TRAINING_RUN = DaFitConstants.TRAINING_TYPE_RUN;
public static final int ACTIVITY_TRAINING_BIKING = DaFitConstants.TRAINING_TYPE_BIKING;
public static final int ACTIVITY_TRAINING_ROPE = DaFitConstants.TRAINING_TYPE_ROPE;
public static final int ACTIVITY_TRAINING_BADMINTON = DaFitConstants.TRAINING_TYPE_BADMINTON;
public static final int ACTIVITY_TRAINING_BASKETBALL = DaFitConstants.TRAINING_TYPE_BASKETBALL;
public static final int ACTIVITY_TRAINING_FOOTBALL = DaFitConstants.TRAINING_TYPE_FOOTBALL;
public static final int ACTIVITY_TRAINING_SWIM = DaFitConstants.TRAINING_TYPE_SWIM;
public static final int ACTIVITY_TRAINING_MOUNTAINEERING = DaFitConstants.TRAINING_TYPE_MOUNTAINEERING;
public static final int ACTIVITY_TRAINING_TENNIS = DaFitConstants.TRAINING_TYPE_TENNIS;
public static final int ACTIVITY_TRAINING_RUGBY = DaFitConstants.TRAINING_TYPE_RUGBY;
public static final int ACTIVITY_TRAINING_GOLF = DaFitConstants.TRAINING_TYPE_GOLF;
public static final int ACTIVITY_SLEEP_LIGHT = 16;
public static final int ACTIVITY_SLEEP_RESTFUL = 17;
public static final int ACTIVITY_SLEEP_START = 18;
public static final int ACTIVITY_SLEEP_END = 19;
public DaFitSampleProvider(GBDevice device, DaoSession session) {
super(device, session);
}
@Override
public AbstractDao<DaFitActivitySample, ?> getSampleDao() {
return getSession().getDaFitActivitySampleDao();
}
@NonNull
@Override
protected Property getTimestampSampleProperty() {
return DaFitActivitySampleDao.Properties.Timestamp;
}
@Nullable
@Override
protected Property getRawKindSampleProperty() {
return DaFitActivitySampleDao.Properties.RawKind;
}
@NonNull
@Override
protected Property getDeviceIdentifierSampleProperty() {
return DaFitActivitySampleDao.Properties.DeviceId;
}
@Override
public DaFitActivitySample createActivitySample() {
return new DaFitActivitySample();
}
@Override
public int normalizeType(int rawType) {
if (rawType == ACTIVITY_NOT_MEASURED)
return ActivityKind.TYPE_NOT_MEASURED;
else if (rawType == ACTIVITY_SLEEP_LIGHT)
return ActivityKind.TYPE_LIGHT_SLEEP;
else if (rawType == ACTIVITY_SLEEP_RESTFUL)
return ActivityKind.TYPE_DEEP_SLEEP;
else if (rawType == ACTIVITY_SLEEP_START || rawType == ACTIVITY_SLEEP_END)
return ActivityKind.TYPE_NOT_MEASURED;
else
return ActivityKind.TYPE_ACTIVITY;
}
@Override
public int toRawActivityKind(int activityKind) {
if (activityKind == ActivityKind.TYPE_NOT_MEASURED)
return ACTIVITY_NOT_MEASURED;
else if (activityKind == ActivityKind.TYPE_LIGHT_SLEEP)
return ACTIVITY_SLEEP_LIGHT;
else if (activityKind == ActivityKind.TYPE_DEEP_SLEEP)
return ACTIVITY_SLEEP_RESTFUL;
else if (activityKind == ActivityKind.TYPE_ACTIVITY)
return ACTIVITY_NOT_MEASURED; // TODO: ?
else
throw new IllegalArgumentException("Invalid Gadgetbridge activity kind: " + activityKind);
}
@Override
public float normalizeIntensity(int rawIntensity) {
if (rawIntensity == ActivitySample.NOT_MEASURED)
return Float.NEGATIVE_INFINITY;
else
return rawIntensity;
}
/**
* Set the activity kind from NOT_MEASURED to new_raw_activity_kind on the given range
* @param timestamp_from the start timestamp
* @param timestamp_to the end timestamp
* @param new_raw_activity_kind the activity kind to set
*/
public void updateActivityInRange(int timestamp_from, int timestamp_to, int new_raw_activity_kind)
{
// greenDAO does not provide a bulk update functionality, and manual update fails because
// of no primary key
Property timestampProperty = getTimestampSampleProperty();
Device dbDevice = DBHelper.findDevice(getDevice(), getSession());
if (dbDevice == null)
throw new IllegalStateException();
Property deviceProperty = getDeviceIdentifierSampleProperty();
/*QueryBuilder<DaFitActivitySample> qb = getSampleDao().queryBuilder();
qb.where(deviceProperty.eq(dbDevice.getId()))
.where(timestampProperty.ge(timestamp_from), timestampProperty.le(timestamp_to))
.where(getRawKindSampleProperty().eq(ACTIVITY_NOT_MEASURED));
List<DaFitActivitySample> samples = qb.build().list();
for (DaFitActivitySample sample : samples) {
sample.setProvider(this);
sample.setRawKind(new_raw_activity_kind);
sample.update();
}*/
String tablename = getSampleDao().getTablename();
String baseSql = SqlUtils.createSqlUpdate(tablename, new String[] { getRawKindSampleProperty().columnName }, new String[] { });
StringBuilder builder = new StringBuilder(baseSql);
List<Object> values = new ArrayList<>();
values.add(new_raw_activity_kind);
List<WhereCondition> whereConditions = new ArrayList<>();
whereConditions.add(deviceProperty.eq(dbDevice.getId()));
whereConditions.add(timestampProperty.ge(timestamp_from));
whereConditions.add(timestampProperty.le(timestamp_to));
whereConditions.add(getRawKindSampleProperty().eq(ACTIVITY_NOT_MEASURED));
ListIterator<WhereCondition> iter = whereConditions.listIterator();
while (iter.hasNext()) {
if (iter.hasPrevious()) {
builder.append(" AND ");
}
WhereCondition condition = iter.next();
condition.appendTo(builder, tablename);
condition.appendValuesTo(values);
}
getSampleDao().getDatabase().execSQL(builder.toString(), values.toArray());
}
}

View File

@ -20,8 +20,12 @@ import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.content.Intent;
import android.net.Uri;
import android.os.Handler;
import android.util.Log;
import android.util.Pair;
import android.widget.Toast;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -29,24 +33,35 @@ import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.Objects;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.Logging;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventCallControl;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventMusicControl;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo;
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.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec;
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
import nodomain.freeyourgadget.gadgetbridge.model.RecordedDataTypes;
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService;
@ -58,6 +73,7 @@ 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.GB;
import nodomain.freeyourgadget.gadgetbridge.util.NotificationUtils;
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
@ -66,6 +82,7 @@ import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
public class DaFitDeviceSupport extends AbstractBTLEDeviceSupport {
private static final Logger LOG = LoggerFactory.getLogger(DaFitDeviceSupport.class);
private static final long IDLE_STEPS_INTERVAL = 5 * 60 * 1000;
private final DeviceInfoProfile<DaFitDeviceSupport> deviceInfoProfile;
private final BatteryInfoProfile<DaFitDeviceSupport> batteryInfoProfile;
@ -85,11 +102,16 @@ public class DaFitDeviceSupport extends AbstractBTLEDeviceSupport {
}
};
private Handler idleUpdateHandler = new Handler();
public static final int MTU = 20; // TODO: there seems to be some way to change this value...?
private DaFitPacketIn packetIn = new DaFitPacketIn();
private boolean realTimeHeartRate;
public DaFitDeviceSupport() {
super(LOG);
batteryCmd.level = ActivitySample.NOT_MEASURED;
addSupportedService(GattService.UUID_SERVICE_GENERIC_ACCESS);
addSupportedService(GattService.UUID_SERVICE_GENERIC_ATTRIBUTE);
@ -124,6 +146,12 @@ public class DaFitDeviceSupport extends AbstractBTLEDeviceSupport {
return builder;
}
@Override
public void dispose() {
super.dispose();
idleUpdateHandler.removeCallbacks(updateIdleStepsRunnable);
}
private BluetoothGattCharacteristic getTargetCharacteristicForPacketType(byte packetType)
{
if (packetType == 1)
@ -152,6 +180,7 @@ public class DaFitDeviceSupport extends AbstractBTLEDeviceSupport {
{
byte[] payload = characteristic.getValue();
Log.i("AAAAAAAAAAAAAAAA", "Update step count: " + Logging.formatBytes(characteristic.getValue()));
handleStepsHistory(0, payload, true);
return true;
}
if (charUuid.equals(DaFitConstants.UUID_CHARACTERISTIC_DATA_IN))
@ -181,6 +210,28 @@ public class DaFitDeviceSupport extends AbstractBTLEDeviceSupport {
int heartRate = payload[0];
Log.i("XXXXXXXX", "Measure heart rate finished: " + heartRate + " BPM");
DaFitActivitySample sample = new DaFitActivitySample();
sample.setTimestamp((int) (System.currentTimeMillis() / 1000));
sample.setRawKind(DaFitSampleProvider.ACTIVITY_NOT_MEASURED);
sample.setDataSource(DaFitSampleProvider.SOURCE_SINGLE_MEASURE);
sample.setBatteryLevel(ActivitySample.NOT_MEASURED);
sample.setSteps(ActivitySample.NOT_MEASURED);
sample.setDistanceMeters(ActivitySample.NOT_MEASURED);
sample.setCaloriesBurnt(ActivitySample.NOT_MEASURED);
sample.setHeartRate(heartRate);
sample.setBloodPressureSystolic(ActivitySample.NOT_MEASURED);
sample.setBloodPressureDiastolic(ActivitySample.NOT_MEASURED);
sample.setBloodOxidation(ActivitySample.NOT_MEASURED);
addGBActivitySample(sample);
broadcastSample(sample);
if (realTimeHeartRate)
onHeartRateTest();
return true;
}
if (packetType == DaFitConstants.CMD_TRIGGER_MEASURE_BLOOD_OXYGEN)
@ -188,6 +239,25 @@ public class DaFitDeviceSupport extends AbstractBTLEDeviceSupport {
int percent = payload[0];
Log.i("XXXXXXXX", "Measure blood oxygen finished: " + percent + "%");
DaFitActivitySample sample = new DaFitActivitySample();
sample.setTimestamp((int) (System.currentTimeMillis() / 1000));
sample.setRawKind(DaFitSampleProvider.ACTIVITY_NOT_MEASURED);
sample.setDataSource(DaFitSampleProvider.SOURCE_SINGLE_MEASURE);
sample.setBatteryLevel(ActivitySample.NOT_MEASURED);
sample.setSteps(ActivitySample.NOT_MEASURED);
sample.setDistanceMeters(ActivitySample.NOT_MEASURED);
sample.setCaloriesBurnt(ActivitySample.NOT_MEASURED);
sample.setHeartRate(ActivitySample.NOT_MEASURED);
sample.setBloodPressureSystolic(ActivitySample.NOT_MEASURED);
sample.setBloodPressureDiastolic(ActivitySample.NOT_MEASURED);
sample.setBloodOxidation(percent);
addGBActivitySample(sample);
broadcastSample(sample);
return true;
}
if (packetType == DaFitConstants.CMD_TRIGGER_MEASURE_BLOOD_PRESSURE)
@ -197,6 +267,26 @@ public class DaFitDeviceSupport extends AbstractBTLEDeviceSupport {
int data2 = payload[2];
Log.i("XXXXXXXX", "Measure blood pressure finished: " + data1 + "/" + data2 + " (" + dataUnknown + ")");
DaFitActivitySample sample = new DaFitActivitySample();
sample.setTimestamp((int) (System.currentTimeMillis() / 1000));
sample.setRawKind(DaFitSampleProvider.ACTIVITY_NOT_MEASURED);
sample.setDataSource(DaFitSampleProvider.SOURCE_SINGLE_MEASURE);
sample.setBatteryLevel(ActivitySample.NOT_MEASURED);
sample.setSteps(ActivitySample.NOT_MEASURED);
sample.setDistanceMeters(ActivitySample.NOT_MEASURED);
sample.setCaloriesBurnt(ActivitySample.NOT_MEASURED);
sample.setHeartRate(ActivitySample.NOT_MEASURED);
sample.setBloodPressureSystolic(data1);
sample.setBloodPressureDiastolic(data2);
sample.setBloodOxidation(ActivitySample.NOT_MEASURED);
addGBActivitySample(sample);
broadcastSample(sample);
return true;
}
@ -243,6 +333,37 @@ public class DaFitDeviceSupport extends AbstractBTLEDeviceSupport {
return false;
}
private void addGBActivitySample(DaFitActivitySample sample) {
addGBActivitySamples(new DaFitActivitySample[] { sample });
}
private void addGBActivitySamples(DaFitActivitySample[] samples) {
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());
for (DaFitActivitySample sample : samples) {
sample.setDevice(device);
sample.setUser(user);
sample.setProvider(provider);
provider.addGBActivitySample(sample);
}
} 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 broadcastSample(DaFitActivitySample sample) {
Intent intent = new Intent(DeviceService.ACTION_REALTIME_SAMPLES)
.putExtra(DeviceService.EXTRA_REALTIME_SAMPLE, sample)
.putExtra(DeviceService.EXTRA_TIMESTAMP, sample.getTimestamp());
LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent);
}
private void handleDeviceInfo(DeviceInfo info) {
LOG.warn("Device info: " + info);
versionCmd.hwVersion = info.getHardwareRevision();
@ -254,6 +375,25 @@ public class DaFitDeviceSupport extends AbstractBTLEDeviceSupport {
LOG.warn("Battery info: " + info);
batteryCmd.level = (short) info.getPercentCharged();
handleGBDeviceEvent(batteryCmd);
DaFitActivitySample sample = new DaFitActivitySample();
sample.setTimestamp((int) (System.currentTimeMillis() / 1000));
sample.setRawKind(DaFitSampleProvider.ACTIVITY_NOT_MEASURED);
sample.setDataSource(DaFitSampleProvider.SOURCE_BATTERY);
sample.setBatteryLevel(batteryCmd.level);
sample.setSteps(ActivitySample.NOT_MEASURED);
sample.setDistanceMeters(ActivitySample.NOT_MEASURED);
sample.setCaloriesBurnt(ActivitySample.NOT_MEASURED);
sample.setHeartRate(ActivitySample.NOT_MEASURED);
sample.setBloodPressureSystolic(ActivitySample.NOT_MEASURED);
sample.setBloodPressureDiastolic(ActivitySample.NOT_MEASURED);
sample.setBloodOxidation(ActivitySample.NOT_MEASURED);
addGBActivitySample(sample);
broadcastSample(sample);
}
@Override
@ -373,7 +513,327 @@ public class DaFitDeviceSupport extends AbstractBTLEDeviceSupport {
@Override
public void onFetchRecordedData(int dataTypes) {
// TODO
if ((dataTypes & RecordedDataTypes.TYPE_ACTIVITY) != 0)
{
try {
new FetchDataOperation(this).perform();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private static int BytesToInt24(byte[] bArr) {
if (bArr.length != 3)
throw new IllegalArgumentException();
return ((bArr[2] << 24) >>> 8) | ((bArr[1] << 8) & 0xFF00) | (bArr[0] & 0xFF);
}
private Runnable updateIdleStepsRunnable = new Runnable() {
@Override
public void run() {
try {
updateIdleSteps();
} finally {
idleUpdateHandler.postDelayed(updateIdleStepsRunnable, IDLE_STEPS_INTERVAL);
}
}
};
private void updateIdleSteps()
{
// The steps value hasn't changed for a while, so the user is not moving
// Store this information in the database to improve the averaging over long periods of time
if (!getDevice().isConnected())
{
LOG.warn("updateIdleSteps but device not connected?!");
return;
}
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());
int currentSampleTimestamp = (int)(Calendar.getInstance().getTimeInMillis() / 1000);
DaFitActivitySample sample = new DaFitActivitySample();
sample.setDevice(device);
sample.setUser(user);
sample.setProvider(provider);
sample.setTimestamp(currentSampleTimestamp);
sample.setRawKind(DaFitSampleProvider.ACTIVITY_NOT_MEASURED);
sample.setDataSource(DaFitSampleProvider.SOURCE_STEPS_IDLE);
sample.setBatteryLevel(batteryCmd.level);
sample.setSteps(0);
sample.setDistanceMeters(0);
sample.setCaloriesBurnt(0);
sample.setHeartRate(ActivitySample.NOT_MEASURED);
sample.setBloodPressureSystolic(ActivitySample.NOT_MEASURED);
sample.setBloodPressureDiastolic(ActivitySample.NOT_MEASURED);
sample.setBloodOxidation(ActivitySample.NOT_MEASURED);
provider.addGBActivitySample(sample);
broadcastSample(sample);
LOG.info("Adding an idle sample: " + sample.toString());
} 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());
}
}
public void handleStepsHistory(int daysAgo, byte[] data, boolean isRealtime)
{
if (data.length != 9)
throw new IllegalArgumentException();
byte[] bArr2 = new byte[3];
System.arraycopy(data, 0, bArr2, 0, 3);
int steps = BytesToInt24(bArr2);
System.arraycopy(data, 3, bArr2, 0, 3);
int distance = BytesToInt24(bArr2);
System.arraycopy(data, 6, bArr2, 0, 3);
int calories = BytesToInt24(bArr2);
Log.i("steps[" + daysAgo + "]", "steps=" + steps + ", distance=" + distance + ", calories=" + calories);
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());
Calendar thisSample = Calendar.getInstance();
if (daysAgo != 0)
{
thisSample.add(Calendar.DATE, -daysAgo);
thisSample.set(Calendar.HOUR_OF_DAY, 23);
thisSample.set(Calendar.MINUTE, 59);
thisSample.set(Calendar.SECOND, 59);
thisSample.set(Calendar.MILLISECOND, 999);
}
else
{
// no change needed - use current time
}
Calendar startOfDay = (Calendar) thisSample.clone();
startOfDay.set(Calendar.HOUR_OF_DAY, 0);
startOfDay.set(Calendar.MINUTE, 0);
startOfDay.set(Calendar.SECOND, 0);
startOfDay.set(Calendar.MILLISECOND, 0);
int startOfDayTimestamp = (int) (startOfDay.getTimeInMillis() / 1000);
int thisSampleTimestamp = (int) (thisSample.getTimeInMillis() / 1000);
int previousSteps = 0;
int previousDistance = 0;
int previousCalories = 0;
for (DaFitActivitySample sample : provider.getAllActivitySamples(startOfDayTimestamp, thisSampleTimestamp))
{
if (sample.getSteps() != ActivitySample.NOT_MEASURED)
previousSteps += sample.getSteps();
if (sample.getDistanceMeters() != ActivitySample.NOT_MEASURED)
previousDistance += sample.getDistanceMeters();
if (sample.getCaloriesBurnt() != ActivitySample.NOT_MEASURED)
previousCalories += sample.getCaloriesBurnt();
}
int newSteps = steps - previousSteps;
int newDistance = distance - previousDistance;
int newCalories = calories - previousCalories;
if (newSteps < 0 || newDistance < 0 || newCalories < 0)
{
LOG.warn("Ignoring a sample that would generate negative values: steps += " + newSteps + ", distance +=" + newDistance + ", calories += " + newCalories);
}
else if (newSteps != 0 || newDistance != 0 || newCalories != 0 || daysAgo == 0)
{
DaFitActivitySample sample = new DaFitActivitySample();
sample.setDevice(device);
sample.setUser(user);
sample.setProvider(provider);
sample.setTimestamp(thisSampleTimestamp);
sample.setRawKind(DaFitSampleProvider.ACTIVITY_NOT_MEASURED);
sample.setDataSource(daysAgo == 0 ? DaFitSampleProvider.SOURCE_STEPS_REALTIME : DaFitSampleProvider.SOURCE_STEPS_SUMMARY);
sample.setBatteryLevel(ActivitySample.NOT_MEASURED);
sample.setSteps(newSteps);
sample.setDistanceMeters(newDistance);
sample.setCaloriesBurnt(newCalories);
sample.setHeartRate(ActivitySample.NOT_MEASURED);
sample.setBloodPressureSystolic(ActivitySample.NOT_MEASURED);
sample.setBloodPressureDiastolic(ActivitySample.NOT_MEASURED);
sample.setBloodOxidation(ActivitySample.NOT_MEASURED);
provider.addGBActivitySample(sample);
if (isRealtime)
{
idleUpdateHandler.removeCallbacks(updateIdleStepsRunnable);
idleUpdateHandler.postDelayed(updateIdleStepsRunnable, IDLE_STEPS_INTERVAL);
broadcastSample(sample);
}
LOG.info("Adding a sample: " + sample.toString());
}
} 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());
}
}
public void handleSleepHistory(int daysAgo, byte[] data)
{
if (data.length % 3 != 0)
throw new IllegalArgumentException();
int prevActivityType = DaFitSampleProvider.ACTIVITY_SLEEP_START;
int prevSampleTimestamp = -1;
for(int i = 0; i < data.length / 3; i++)
{
int type = data[3*i];
int start_h = data[3*i + 1];
int start_m = data[3*i + 2];
Log.i("sleep[" + daysAgo + "][" + i + "]", "type=" + type + ", start_h=" + start_h + ", start_m=" + start_m);
// SleepAnalysis measures sleep fragment type by marking the END of the fragment.
// The watch provides data by marking the START of the fragment.
// Additionally, ActivityAnalysis (used by the weekly view...) does AVERAGING when
// adjacent samples are not of the same type..
// FIXME: The way Gadgetbridge does it seems kinda broken...
// This means that we have to convert the data when importing. Each sample gets
// converted to two samples - one marking the beginning of the segment, and another
// marking the end.
// Watch: SLEEP_LIGHT ... SLEEP_DEEP ... SLEEP_LIGHT ... SLEEP_SOBER
// Gadgetbridge: ANYTHING,SLEEP_LIGHT ... SLEEP_LIGHT,SLEEP_DEEP ... SLEEP_DEEP,SLEEP_LIGHT ... SLEEP_LIGHT,ANYTHING
// ^ ^- this is important, it MUST be sleep, to ensure proper detection
// Time since the last -| of sleepStart, see SleepAnalysis.calculateSleepSessions
// sample must be 0
// (otherwise SleepAnalysis will include this fragment...)
// This means that when inserting samples:
// * every sample is converted to (previous_sample_type, current_sample_type) happening
// roughly at the same time (but in this order)
// * the first sample is prefixed by unspecified activity
// * the last sample (SOBER) is converted to unspecified activity
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());
Calendar thisSample = Calendar.getInstance();
thisSample.add(Calendar.HOUR_OF_DAY, 4); // the clock assumes the sleep day changes at 20:00, so move the time forward to make the day correct
thisSample.set(Calendar.MINUTE, 0);
thisSample.add(Calendar.DATE, -daysAgo);
thisSample.set(Calendar.HOUR_OF_DAY, start_h);
thisSample.set(Calendar.MINUTE, start_m);
thisSample.set(Calendar.SECOND, 0);
thisSample.set(Calendar.MILLISECOND, 0);
int thisSampleTimestamp = (int) (thisSample.getTimeInMillis() / 1000);
int activityType;
if (type == DaFitConstants.SLEEP_SOBER)
activityType = DaFitSampleProvider.ACTIVITY_SLEEP_END;
else if (type == DaFitConstants.SLEEP_LIGHT)
activityType = DaFitSampleProvider.ACTIVITY_SLEEP_LIGHT;
else if (type == DaFitConstants.SLEEP_RESTFUL)
activityType = DaFitSampleProvider.ACTIVITY_SLEEP_RESTFUL;
else
throw new IllegalArgumentException("Invalid sleep type");
// Insert the end of previous segment sample
DaFitActivitySample prevSegmentSample = new DaFitActivitySample();
prevSegmentSample.setDevice(device);
prevSegmentSample.setUser(user);
prevSegmentSample.setProvider(provider);
prevSegmentSample.setTimestamp(thisSampleTimestamp - 1);
prevSegmentSample.setRawKind(prevActivityType);
prevSegmentSample.setDataSource(DaFitSampleProvider.SOURCE_SLEEP_SUMMARY);
prevSegmentSample.setBatteryLevel(ActivitySample.NOT_MEASURED);
prevSegmentSample.setSteps(ActivitySample.NOT_MEASURED);
prevSegmentSample.setDistanceMeters(ActivitySample.NOT_MEASURED);
prevSegmentSample.setCaloriesBurnt(ActivitySample.NOT_MEASURED);
prevSegmentSample.setHeartRate(ActivitySample.NOT_MEASURED);
prevSegmentSample.setBloodPressureSystolic(ActivitySample.NOT_MEASURED);
prevSegmentSample.setBloodPressureDiastolic(ActivitySample.NOT_MEASURED);
prevSegmentSample.setBloodOxidation(ActivitySample.NOT_MEASURED);
addGBActivitySampleIfNotExists(provider, prevSegmentSample);
// Insert the start of new segment sample
DaFitActivitySample nextSegmentSample = new DaFitActivitySample();
nextSegmentSample.setDevice(device);
nextSegmentSample.setUser(user);
nextSegmentSample.setProvider(provider);
nextSegmentSample.setTimestamp(thisSampleTimestamp);
nextSegmentSample.setRawKind(activityType);
nextSegmentSample.setDataSource(DaFitSampleProvider.SOURCE_SLEEP_SUMMARY);
nextSegmentSample.setBatteryLevel(ActivitySample.NOT_MEASURED);
nextSegmentSample.setSteps(ActivitySample.NOT_MEASURED);
nextSegmentSample.setDistanceMeters(ActivitySample.NOT_MEASURED);
nextSegmentSample.setCaloriesBurnt(ActivitySample.NOT_MEASURED);
nextSegmentSample.setHeartRate(ActivitySample.NOT_MEASURED);
nextSegmentSample.setBloodPressureSystolic(ActivitySample.NOT_MEASURED);
nextSegmentSample.setBloodPressureDiastolic(ActivitySample.NOT_MEASURED);
nextSegmentSample.setBloodOxidation(ActivitySample.NOT_MEASURED);
addGBActivitySampleIfNotExists(provider, nextSegmentSample);
// Set the activity type on all samples in this time period
if (prevActivityType != DaFitSampleProvider.ACTIVITY_SLEEP_START)
provider.updateActivityInRange(prevSampleTimestamp, thisSampleTimestamp, prevActivityType);
prevActivityType = activityType;
if (prevActivityType == DaFitSampleProvider.ACTIVITY_SLEEP_END)
prevActivityType = DaFitSampleProvider.ACTIVITY_SLEEP_START;
prevSampleTimestamp = thisSampleTimestamp;
} 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;
for (DaFitActivitySample sample2 : provider.getAllActivitySamples(sample.getTimestamp() - 1, sample.getTimestamp() + 1))
{
if (sample2.getTimestamp() == sample2.getTimestamp() && sample2.getRawKind() == sample.getRawKind())
alreadyHaveThisSample = true;
}
if (!alreadyHaveThisSample)
{
provider.addGBActivitySample(sample);
LOG.info("Adding a sample: " + sample.toString());
}
}
@Override
@ -414,12 +874,18 @@ public class DaFitDeviceSupport extends AbstractBTLEDeviceSupport {
@Override
public void onEnableRealtimeSteps(boolean enable) {
// TODO
// enabled all the time :D that's the only way to get more than a daily sum from this watch...
}
@Override
public void onEnableRealtimeHeartRateMeasurement(boolean enable) {
// TODO
if (realTimeHeartRate == enable)
return;
realTimeHeartRate = enable; // will do another measurement immediately
if (realTimeHeartRate)
onHeartRateTest();
else
onAbortHeartRateTest();
}
@Override

View File

@ -0,0 +1,196 @@
/* 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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.Logging;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.devices.dafit.DaFitConstants;
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 FetchDataOperation extends AbstractBTLEOperation<DaFitDeviceSupport> {
private static final Logger LOG = LoggerFactory.getLogger(FetchDataOperation.class);
private boolean[] receivedSteps = new boolean[3];
private boolean[] receivedSleep = new boolean[3];
private DaFitPacketIn packetIn = new DaFitPacketIn();
public FetchDataOperation(DaFitDeviceSupport support) {
super(support);
}
@Override
protected void prePerform() {
getDevice().setBusyTask(getContext().getString(R.string.busy_task_fetch_activity_data));
getDevice().sendDeviceUpdateIntent(getContext());
}
@Override
protected void doPerform() throws IOException {
TransactionBuilder builder = performInitialized("FetchDataOperation");
getSupport().sendPacket(builder, DaFitPacketOut.buildPacket(DaFitConstants.CMD_SYNC_PAST_SLEEP_AND_STEP, new byte[] { DaFitConstants.ARG_SYNC_YESTERDAY_SLEEP }));
getSupport().sendPacket(builder, DaFitPacketOut.buildPacket(DaFitConstants.CMD_SYNC_PAST_SLEEP_AND_STEP, new byte[] { DaFitConstants.ARG_SYNC_DAY_BEFORE_YESTERDAY_SLEEP }));
getSupport().sendPacket(builder, DaFitPacketOut.buildPacket(DaFitConstants.CMD_SYNC_SLEEP, new byte[0]));
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));
builder.queue(getQueue());
updateProgressAndCheckFinish();
}
@Override
public boolean onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
if (!isOperationRunning())
{
LOG.error("onCharacteristicRead but operation is not running!");
}
else
{
UUID charUuid = characteristic.getUuid();
if (charUuid.equals(DaFitConstants.UUID_CHARACTERISTIC_STEPS)) {
byte[] data = characteristic.getValue();
Log.i("TODAY STEPS", "data: " + Logging.formatBytes(data));
decodeSteps(0, data);
return true;
}
}
return super.onCharacteristicRead(gatt, characteristic, status);
}
@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_SYNC_SLEEP) {
Log.i("TODAY SLEEP", "data: " + Logging.formatBytes(payload));
decodeSleep(0, payload);
return true;
}
if (packetType == DaFitConstants.CMD_SYNC_PAST_SLEEP_AND_STEP) {
byte dataType = payload[0];
byte[] data = new byte[payload.length - 1];
System.arraycopy(payload, 1, data, 0, data.length);
// NOTE: Does this seem swapped to you? That's because IT IS! I took the constant names
// from the official app, but as it turns out, the official app has a bug.
// (and yes, you can see that data from yesterday appears as two days ago
// in the app itself and all past data is getting messed up because of it)
if (dataType == DaFitConstants.ARG_SYNC_YESTERDAY_STEPS) {
Log.i("2 DAYS AGO STEPS", "data: " + Logging.formatBytes(data));
decodeSteps(2, data);
return true;
}
else if (dataType == DaFitConstants.ARG_SYNC_DAY_BEFORE_YESTERDAY_STEPS) {
Log.i("YESTERDAY STEPS", "data: " + Logging.formatBytes(data));
decodeSteps(1, data);
return true;
}
else if (dataType == DaFitConstants.ARG_SYNC_YESTERDAY_SLEEP) {
Log.i("2 DAYS AGO SLEEP", "data: " + Logging.formatBytes(data));
decodeSleep(2, data);
return true;
}
else if (dataType == DaFitConstants.ARG_SYNC_DAY_BEFORE_YESTERDAY_SLEEP) {
Log.i("YESTERDAY SLEEP", "data: " + Logging.formatBytes(data));
decodeSleep(1, data);
return true;
}
}
return false;
}
private void decodeSteps(int daysAgo, byte[] data)
{
getSupport().handleStepsHistory(daysAgo, data, false);
receivedSteps[daysAgo] = true;
updateProgressAndCheckFinish();
}
private void decodeSleep(int daysAgo, byte[] data)
{
getSupport().handleSleepHistory(daysAgo, data);
receivedSleep[daysAgo] = true;
updateProgressAndCheckFinish();
}
private void updateProgressAndCheckFinish()
{
int count = 0;
int total = receivedSteps.length + receivedSleep.length;
for(int i = 0; i < receivedSteps.length; i++)
if (receivedSteps[i])
++count;
for(int i = 0; i < receivedSleep.length; i++)
if (receivedSleep[i])
++count;
GB.updateTransferNotification(null, getContext().getString(R.string.busy_task_fetch_activity_data), true, 100 * count / total, getContext());
if (count == total)
operationFinished();
}
@Override
protected void operationFinished() {
operationStatus = OperationStatus.FINISHED;
if (getDevice() != null && getDevice().isConnected()) {
unsetBusy();
GB.signalActivityDataFinish();
}
}
}