mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2024-11-15 14:39:26 +01:00
Garmin: Persist and display activity
- Steps, hr, intensity - Sleep stages - Stress - SpO2 - Workouts
This commit is contained in:
parent
89046d0815
commit
0b07f36817
@ -45,7 +45,7 @@ public class GBDaoGenerator {
|
||||
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
final Schema schema = new Schema(73, MAIN_PACKAGE + ".entities");
|
||||
final Schema schema = new Schema(74, MAIN_PACKAGE + ".entities");
|
||||
|
||||
Entity userAttributes = addUserAttributes(schema);
|
||||
Entity user = addUserInfo(schema, userAttributes);
|
||||
@ -108,6 +108,11 @@ public class GBDaoGenerator {
|
||||
addHybridHRActivitySample(schema, user, device);
|
||||
addVivomoveHrActivitySample(schema, user, device);
|
||||
addGarminFitFile(schema, user, device);
|
||||
addGarminActivitySample(schema, user, device);
|
||||
addGarminStressSample(schema, user, device);
|
||||
addGarminSpo2Sample(schema, user, device);
|
||||
addGarminSleepStageSample(schema, user, device);
|
||||
addGarminEventSample(schema, user, device);
|
||||
addWena3EnergySample(schema, user, device);
|
||||
addWena3BehaviorSample(schema, user, device);
|
||||
addWena3CaloriesSample(schema, user, device);
|
||||
@ -679,6 +684,47 @@ public class GBDaoGenerator {
|
||||
return downloadedFitFile;
|
||||
}
|
||||
|
||||
private static Entity addGarminActivitySample(Schema schema, Entity user, Entity device) {
|
||||
Entity activitySample = addEntity(schema, "GarminActivitySample");
|
||||
addCommonActivitySampleProperties("AbstractActivitySample", activitySample, user, device);
|
||||
activitySample.implementsSerializable();
|
||||
activitySample.addIntProperty(SAMPLE_RAW_INTENSITY).notNull().codeBeforeGetterAndSetter(OVERRIDE);
|
||||
activitySample.addIntProperty(SAMPLE_STEPS).notNull().codeBeforeGetterAndSetter(OVERRIDE);
|
||||
activitySample.addIntProperty(SAMPLE_RAW_KIND).notNull().codeBeforeGetterAndSetter(OVERRIDE);
|
||||
addHeartRateProperties(activitySample);
|
||||
return activitySample;
|
||||
}
|
||||
|
||||
private static Entity addGarminStressSample(Schema schema, Entity user, Entity device) {
|
||||
Entity stressSample = addEntity(schema, "GarminStressSample");
|
||||
addCommonTimeSampleProperties("AbstractStressSample", stressSample, user, device);
|
||||
stressSample.addIntProperty("stress").notNull().codeBeforeGetter(OVERRIDE);
|
||||
return stressSample;
|
||||
}
|
||||
|
||||
private static Entity addGarminSpo2Sample(Schema schema, Entity user, Entity device) {
|
||||
Entity spo2sample = addEntity(schema, "GarminSpo2Sample");
|
||||
addCommonTimeSampleProperties("AbstractSpo2Sample", spo2sample, user, device);
|
||||
spo2sample.addIntProperty("spo2").notNull().codeBeforeGetter(OVERRIDE);
|
||||
return spo2sample;
|
||||
}
|
||||
|
||||
private static Entity addGarminSleepStageSample(Schema schema, Entity user, Entity device) {
|
||||
Entity sleepStageSample = addEntity(schema, "GarminSleepStageSample");
|
||||
addCommonTimeSampleProperties("AbstractTimeSample", sleepStageSample, user, device);
|
||||
sleepStageSample.addIntProperty("stage").notNull();
|
||||
return sleepStageSample;
|
||||
}
|
||||
|
||||
private static Entity addGarminEventSample(Schema schema, Entity user, Entity device) {
|
||||
Entity sleepStageSample = addEntity(schema, "GarminEventSample");
|
||||
addCommonTimeSampleProperties("AbstractTimeSample", sleepStageSample, user, device);
|
||||
sleepStageSample.addIntProperty("event").notNull().primaryKey();
|
||||
sleepStageSample.addIntProperty("eventType");
|
||||
sleepStageSample.addLongProperty("data");
|
||||
return sleepStageSample;
|
||||
}
|
||||
|
||||
private static Entity addWatchXPlusHealthActivitySample(Schema schema, Entity user, Entity device) {
|
||||
Entity activitySample = addEntity(schema, "WatchXPlusActivitySample");
|
||||
activitySample.implementsSerializable();
|
||||
|
@ -0,0 +1,229 @@
|
||||
/* Copyright (C) 2024 José Rebelo
|
||||
|
||||
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 <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.garmin;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.threeten.bp.LocalDate;
|
||||
|
||||
import java.util.Calendar;
|
||||
import java.util.List;
|
||||
|
||||
import de.greenrobot.dao.AbstractDao;
|
||||
import de.greenrobot.dao.Property;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractSampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.XiaomiSleepTimeSampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.GarminActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.GarminActivitySampleDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.GarminEventSample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.GarminSleepStageSample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.XiaomiSleepTimeSample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionSleepStage;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.RangeMap;
|
||||
|
||||
public class GarminActivitySampleProvider extends AbstractSampleProvider<GarminActivitySample> {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(GarminActivitySampleProvider.class);
|
||||
|
||||
public GarminActivitySampleProvider(final GBDevice device, final DaoSession session) {
|
||||
super(device, session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AbstractDao<GarminActivitySample, ?> getSampleDao() {
|
||||
return getSession().getGarminActivitySampleDao();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
protected Property getRawKindSampleProperty() {
|
||||
return GarminActivitySampleDao.Properties.RawKind;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected Property getTimestampSampleProperty() {
|
||||
return GarminActivitySampleDao.Properties.Timestamp;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected Property getDeviceIdentifierSampleProperty() {
|
||||
return GarminActivitySampleDao.Properties.DeviceId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int normalizeType(final int rawType) {
|
||||
return rawType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int toRawActivityKind(final int activityKind) {
|
||||
return activityKind;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float normalizeIntensity(final int rawIntensity) {
|
||||
return rawIntensity / 100f;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GarminActivitySample createActivitySample() {
|
||||
return new GarminActivitySample();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<GarminActivitySample> getGBActivitySamples(final int timestamp_from, final int timestamp_to, final int activityType) {
|
||||
LOG.trace(
|
||||
"Getting garmin activity samples for {} between {} and {}",
|
||||
String.format("0x%08x", activityType),
|
||||
timestamp_from,
|
||||
timestamp_to
|
||||
);
|
||||
|
||||
final long nanoStart = System.nanoTime();
|
||||
|
||||
final List<GarminActivitySample> samples = super.getGBActivitySamples(timestamp_from, timestamp_to, activityType);
|
||||
|
||||
if (!samples.isEmpty()) {
|
||||
convertCumulativeSteps(samples);
|
||||
}
|
||||
|
||||
overlaySleep(samples, timestamp_from, timestamp_to);
|
||||
|
||||
final long nanoEnd = System.nanoTime();
|
||||
|
||||
final long executionTime = (nanoEnd - nanoStart) / 1000000;
|
||||
|
||||
LOG.trace("Getting Garmin samples took {}ms", executionTime);
|
||||
|
||||
return samples;
|
||||
}
|
||||
|
||||
private void convertCumulativeSteps(final List<GarminActivitySample> samples) {
|
||||
final Calendar cal = Calendar.getInstance();
|
||||
|
||||
// Steps on the Garmin Watch are reported cumulatively per day - convert them to
|
||||
// This slightly breaks activity recognition, because we don't have per-minute granularity...
|
||||
int prevSteps = samples.get(0).getSteps();
|
||||
samples.get(0).setTimestamp((samples.get(0).getTimestamp() / 60) * 60);
|
||||
|
||||
for (int i = 1; i < samples.size(); i++) {
|
||||
final GarminActivitySample s1 = samples.get(i - 1);
|
||||
final GarminActivitySample s2 = samples.get(i);
|
||||
s2.setTimestamp((s2.getTimestamp() / 60) * 60);
|
||||
|
||||
cal.setTimeInMillis(s1.getTimestamp() * 1000L - 1000L);
|
||||
final LocalDate d1 = LocalDate.of(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH) + 1, cal.get(Calendar.DAY_OF_MONTH));
|
||||
cal.setTimeInMillis(s2.getTimestamp() * 1000L - 1000L);
|
||||
final LocalDate d2 = LocalDate.of(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH) + 1, cal.get(Calendar.DAY_OF_MONTH));
|
||||
|
||||
if (d1.equals(d2) && s2.getSteps() > 0) {
|
||||
int bak = s2.getSteps();
|
||||
s2.setSteps(s2.getSteps() - prevSteps);
|
||||
prevSteps = bak;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void overlaySleep(final List<GarminActivitySample> samples, final int timestamp_from, final int timestamp_to) {
|
||||
// The samples provided by Garmin are upper-bound timestamps of the sleep stage
|
||||
final RangeMap<Long, Integer> stagesMap = new RangeMap<>(RangeMap.Mode.UPPER_BOUND);
|
||||
|
||||
final GarminEventSampleProvider eventSampleProvider = new GarminEventSampleProvider(getDevice(), getSession());
|
||||
final List<GarminEventSample> sleepEventSamples = eventSampleProvider.getSleepEvents(
|
||||
timestamp_from * 1000L - 86400000L,
|
||||
timestamp_to * 1000L
|
||||
);
|
||||
if (!sleepEventSamples.isEmpty()) {
|
||||
LOG.debug("Found {} sleep event samples between {} and {}", sleepEventSamples.size(), timestamp_from, timestamp_to);
|
||||
for (final GarminEventSample event : sleepEventSamples) {
|
||||
switch (event.getEventType()) {
|
||||
case 0: // start
|
||||
// We only need the start event as an upper-bound timestamp (anything before it is unknown)
|
||||
stagesMap.put(event.getTimestamp(), ActivityKind.TYPE_UNKNOWN);
|
||||
case 1: // stop
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final GarminSleepStageSampleProvider sleepStagesSampleProvider = new GarminSleepStageSampleProvider(getDevice(), getSession());
|
||||
final List<GarminSleepStageSample> stageSamples = sleepStagesSampleProvider.getAllSamples(
|
||||
timestamp_from * 1000L - 86400000L,
|
||||
timestamp_to * 1000L
|
||||
);
|
||||
|
||||
if (!stageSamples.isEmpty()) {
|
||||
// We got actual sleep stages
|
||||
LOG.debug("Found {} sleep stage samples between {} and {}", stageSamples.size(), timestamp_from, timestamp_to);
|
||||
|
||||
for (final GarminSleepStageSample stageSample : stageSamples) {
|
||||
final int activityKind;
|
||||
|
||||
final FieldDefinitionSleepStage.SleepStage sleepStage = FieldDefinitionSleepStage.SleepStage.fromId(stageSample.getStage());
|
||||
if (sleepStage == null) {
|
||||
LOG.error("Unknown sleep stage for {}", stageSample.getStage());
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (sleepStage) {
|
||||
case LIGHT:
|
||||
activityKind = ActivityKind.TYPE_LIGHT_SLEEP;
|
||||
break;
|
||||
case DEEP:
|
||||
activityKind = ActivityKind.TYPE_DEEP_SLEEP;
|
||||
break;
|
||||
case REM:
|
||||
activityKind = ActivityKind.TYPE_REM_SLEEP;
|
||||
break;
|
||||
default:
|
||||
activityKind = ActivityKind.TYPE_UNKNOWN;
|
||||
break;
|
||||
}
|
||||
stagesMap.put(stageSample.getTimestamp(), activityKind);
|
||||
}
|
||||
}
|
||||
|
||||
if (!stagesMap.isEmpty()) {
|
||||
for (final GarminActivitySample sample : samples) {
|
||||
final long ts = sample.getTimestamp() * 1000L;
|
||||
final Integer sleepType = stagesMap.get(ts);
|
||||
if (sleepType != null && !sleepType.equals(ActivityKind.TYPE_UNKNOWN)) {
|
||||
sample.setRawKind(sleepType);
|
||||
|
||||
switch (sleepType) {
|
||||
case ActivityKind.TYPE_DEEP_SLEEP:
|
||||
sample.setRawIntensity(20);
|
||||
break;
|
||||
case ActivityKind.TYPE_LIGHT_SLEEP:
|
||||
sample.setRawIntensity(30);
|
||||
break;
|
||||
case ActivityKind.TYPE_REM_SLEEP:
|
||||
sample.setRawIntensity(40);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -15,17 +15,51 @@ import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpec
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsScreen;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractBLEDeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.TimeSampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummaryDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.GarminActivitySampleDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.GarminSleepStageSampleDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.GarminSpo2SampleDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.GarminStressSampleDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.StressSample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.GarminSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
|
||||
public abstract class GarminCoordinator extends AbstractBLEDeviceCoordinator {
|
||||
@Override
|
||||
protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException {
|
||||
protected void deleteDevice(@NonNull final GBDevice gbDevice, @NonNull final Device device, @NonNull final DaoSession session) throws GBException {
|
||||
deleteAllActivityData(device, session);
|
||||
}
|
||||
|
||||
public void deleteAllActivityData(@NonNull final Device device, @NonNull final DaoSession session) throws GBException {
|
||||
final Long deviceId = device.getId();
|
||||
|
||||
session.getGarminActivitySampleDao().queryBuilder()
|
||||
.where(GarminActivitySampleDao.Properties.DeviceId.eq(deviceId))
|
||||
.buildDelete().executeDeleteWithoutDetachingEntities();
|
||||
|
||||
session.getGarminStressSampleDao().queryBuilder()
|
||||
.where(GarminStressSampleDao.Properties.DeviceId.eq(deviceId))
|
||||
.buildDelete().executeDeleteWithoutDetachingEntities();
|
||||
|
||||
session.getGarminSleepStageSampleDao().queryBuilder()
|
||||
.where(GarminSleepStageSampleDao.Properties.DeviceId.eq(deviceId))
|
||||
.buildDelete().executeDeleteWithoutDetachingEntities();
|
||||
|
||||
session.getGarminSpo2SampleDao().queryBuilder()
|
||||
.where(GarminSpo2SampleDao.Properties.DeviceId.eq(deviceId))
|
||||
.buildDelete().executeDeleteWithoutDetachingEntities();
|
||||
|
||||
session.getBaseActivitySummaryDao().queryBuilder()
|
||||
.where(BaseActivitySummaryDao.Properties.DeviceId.eq(deviceId))
|
||||
.buildDelete().executeDeleteWithoutDetachingEntities();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -39,6 +73,21 @@ public abstract class GarminCoordinator extends AbstractBLEDeviceCoordinator {
|
||||
return GarminSupport.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SampleProvider<? extends ActivitySample> getSampleProvider(final GBDevice device, DaoSession session) {
|
||||
return new GarminActivitySampleProvider(device, session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TimeSampleProvider<? extends StressSample> getStressSampleProvider(final GBDevice device, final DaoSession session) {
|
||||
return new GarminStressSampleProvider(device, session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TimeSampleProvider<? extends Spo2Sample> getSpo2SampleProvider(final GBDevice device, final DaoSession session) {
|
||||
return new GarminSpo2SampleProvider(device, session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DeviceSpecificSettings getDeviceSpecificSettings(final GBDevice device) {
|
||||
final DeviceSpecificSettings deviceSpecificSettings = new DeviceSpecificSettings();
|
||||
@ -78,6 +127,51 @@ public abstract class GarminCoordinator extends AbstractBLEDeviceCoordinator {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsActivityTracking() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsActivityTracks() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsStressMeasurement() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] getStressRanges() {
|
||||
// 1-25 = relaxed
|
||||
// 26-50 = low
|
||||
// 51-80 = moderate
|
||||
// 76-100 = high
|
||||
return new int[]{1, 26, 51, 76};
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsHeartRateMeasurement(final GBDevice device) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsManualHeartRateMeasurement(final GBDevice device) {
|
||||
// TODO: It should be supported, but not yet implemented
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSpo2() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsRemSleep() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsFindDevice() {
|
||||
return true;
|
||||
|
@ -0,0 +1,80 @@
|
||||
/* Copyright (C) 2024 José Rebelo
|
||||
|
||||
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 <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.garmin;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import de.greenrobot.dao.AbstractDao;
|
||||
import de.greenrobot.dao.Property;
|
||||
import de.greenrobot.dao.query.QueryBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractTimeSampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.GarminEventSample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.GarminEventSampleDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
|
||||
public class GarminEventSampleProvider extends AbstractTimeSampleProvider<GarminEventSample> {
|
||||
public GarminEventSampleProvider(final GBDevice device, final DaoSession session) {
|
||||
super(device, session);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public AbstractDao<GarminEventSample, ?> getSampleDao() {
|
||||
return getSession().getGarminEventSampleDao();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected Property getTimestampSampleProperty() {
|
||||
return GarminEventSampleDao.Properties.Timestamp;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected Property getDeviceIdentifierSampleProperty() {
|
||||
return GarminEventSampleDao.Properties.DeviceId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GarminEventSample createSample() {
|
||||
return new GarminEventSample();
|
||||
}
|
||||
|
||||
public List<GarminEventSample> getSleepEvents(final long timestampFrom, final long timestampTo) {
|
||||
final QueryBuilder<GarminEventSample> qb = getSampleDao().queryBuilder();
|
||||
final Property timestampProperty = getTimestampSampleProperty();
|
||||
final Device dbDevice = DBHelper.findDevice(getDevice(), getSession());
|
||||
if (dbDevice == null) {
|
||||
// no device, no samples
|
||||
return Collections.emptyList();
|
||||
}
|
||||
final Property deviceProperty = getDeviceIdentifierSampleProperty();
|
||||
qb.where(deviceProperty.eq(dbDevice.getId()), timestampProperty.ge(timestampFrom))
|
||||
.where(timestampProperty.le(timestampTo))
|
||||
.where(GarminEventSampleDao.Properties.Event.eq(74));
|
||||
|
||||
final List<GarminEventSample> samples = qb.build().list();
|
||||
detachFromSession();
|
||||
return samples;
|
||||
}
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
/* Copyright (C) 2024 José Rebelo
|
||||
|
||||
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 <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.garmin;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import de.greenrobot.dao.AbstractDao;
|
||||
import de.greenrobot.dao.Property;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractTimeSampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.GarminSleepStageSample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.GarminSleepStageSampleDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
|
||||
public class GarminSleepStageSampleProvider extends AbstractTimeSampleProvider<GarminSleepStageSample> {
|
||||
public GarminSleepStageSampleProvider(final GBDevice device, final DaoSession session) {
|
||||
super(device, session);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public AbstractDao<GarminSleepStageSample, ?> getSampleDao() {
|
||||
return getSession().getGarminSleepStageSampleDao();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected Property getTimestampSampleProperty() {
|
||||
return GarminSleepStageSampleDao.Properties.Timestamp;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected Property getDeviceIdentifierSampleProperty() {
|
||||
return GarminSleepStageSampleDao.Properties.DeviceId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GarminSleepStageSample createSample() {
|
||||
return new GarminSleepStageSample();
|
||||
}
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
/* Copyright (C) 2024 José Rebelo
|
||||
|
||||
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 <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.garmin;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import de.greenrobot.dao.AbstractDao;
|
||||
import de.greenrobot.dao.Property;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractTimeSampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.GarminSpo2Sample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.GarminSpo2SampleDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
|
||||
public class GarminSpo2SampleProvider extends AbstractTimeSampleProvider<GarminSpo2Sample> {
|
||||
public GarminSpo2SampleProvider(final GBDevice device, final DaoSession session) {
|
||||
super(device, session);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public AbstractDao<GarminSpo2Sample, ?> getSampleDao() {
|
||||
return getSession().getGarminSpo2SampleDao();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected Property getTimestampSampleProperty() {
|
||||
return GarminSpo2SampleDao.Properties.Timestamp;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected Property getDeviceIdentifierSampleProperty() {
|
||||
return GarminSpo2SampleDao.Properties.DeviceId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GarminSpo2Sample createSample() {
|
||||
return new GarminSpo2Sample();
|
||||
}
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
/* Copyright (C) 2024 José Rebelo
|
||||
|
||||
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 <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.garmin;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import de.greenrobot.dao.AbstractDao;
|
||||
import de.greenrobot.dao.Property;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractTimeSampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.GarminStressSample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.GarminStressSampleDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
|
||||
public class GarminStressSampleProvider extends AbstractTimeSampleProvider<GarminStressSample> {
|
||||
public GarminStressSampleProvider(final GBDevice device, final DaoSession session) {
|
||||
super(device, session);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public AbstractDao<GarminStressSample, ?> getSampleDao() {
|
||||
return getSession().getGarminStressSampleDao();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected Property getTimestampSampleProperty() {
|
||||
return GarminStressSampleDao.Properties.Timestamp;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected Property getDeviceIdentifierSampleProperty() {
|
||||
return GarminStressSampleDao.Properties.DeviceId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GarminStressSample createSample() {
|
||||
return new GarminStressSample();
|
||||
}
|
||||
}
|
@ -142,7 +142,7 @@ public class GPXExporter implements ActivityTrackExporter {
|
||||
// lon and lat attributes do not have an explicit namespace
|
||||
ser.attribute(null, "lon", formatLocation(location.getLongitude()));
|
||||
ser.attribute(null, "lat", formatLocation(location.getLatitude()));
|
||||
if (location.getAltitude() != -20000) {
|
||||
if (location.getAltitude() != GPSCoordinate.UNKNOWN_ALTITUDE) {
|
||||
ser.startTag(NS_GPX_URI, "ele").text(formatLocation(location.getAltitude())).endTag(NS_GPX_URI, "ele");
|
||||
}
|
||||
ser.startTag(NS_GPX_URI, "time").text(DateTimeUtils.formatIso8601UTC(point.getTime())).endTag(NS_GPX_URI, "time");
|
||||
|
@ -0,0 +1,58 @@
|
||||
/* Copyright (C) 2024 José Rebelo
|
||||
|
||||
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 <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.model;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FitImporter;
|
||||
|
||||
/**
|
||||
* A small wrapper for a JSONObject, with helper methods to add activity summary data in the format
|
||||
* Gadgetbridge expects.
|
||||
*/
|
||||
public class ActivitySummaryData extends JSONObject {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(FitImporter.class);
|
||||
|
||||
public void add(final String key, final float value, final String unit) {
|
||||
if (value > 0) {
|
||||
try {
|
||||
final JSONObject innerData = new JSONObject();
|
||||
innerData.put("value", value);
|
||||
innerData.put("unit", unit);
|
||||
put(key, innerData);
|
||||
} catch (final JSONException e) {
|
||||
LOG.error("This should never happen", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void add(final String key, final String value) {
|
||||
if (key != null && !key.isEmpty() && value != null && !value.isEmpty()) {
|
||||
try {
|
||||
final JSONObject innerData = new JSONObject();
|
||||
innerData.put("value", value);
|
||||
innerData.put("unit", "string");
|
||||
put(key, innerData);
|
||||
} catch (final JSONException e) {
|
||||
LOG.error("This should never happen", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -17,6 +17,7 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.model;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
@ -60,6 +61,10 @@ public class ActivityTrack {
|
||||
currentSegment.add(point);
|
||||
}
|
||||
|
||||
public void addTrackPoints(final Collection<ActivityPoint> points) {
|
||||
currentSegment.addAll(points);
|
||||
}
|
||||
|
||||
public void startNewSegment() {
|
||||
// Only really start a new segment if the current one is not empty
|
||||
if (!currentSegment.isEmpty()) {
|
||||
|
@ -25,6 +25,8 @@ public class GPSCoordinate {
|
||||
private final double longitude;
|
||||
private final double altitude;
|
||||
|
||||
public static final double UNKNOWN_ALTITUDE = -20000d;
|
||||
|
||||
public static final int GPS_DECIMAL_DEGREES_SCALE = 6; // precise to 111.132mm at equator: https://en.wikipedia.org/wiki/Decimal_degrees
|
||||
|
||||
public GPSCoordinate(double longitude, double latitude, double altitude) {
|
||||
@ -33,6 +35,10 @@ public class GPSCoordinate {
|
||||
this.altitude = altitude;
|
||||
}
|
||||
|
||||
public GPSCoordinate(double longitude, double latitude) {
|
||||
this(longitude, latitude, UNKNOWN_ALTITUDE);
|
||||
}
|
||||
|
||||
public double getLatitude() {
|
||||
return latitude;
|
||||
}
|
||||
|
@ -589,8 +589,7 @@ public class CmfActivitySync {
|
||||
final ActivityPoint ap = new ActivityPoint(new Date(gpsSample.getTimestamp()));
|
||||
final GPSCoordinate coordinate = new GPSCoordinate(
|
||||
gpsSample.getLongitude() / 10000000d,
|
||||
gpsSample.getLatitude() / 10000000d,
|
||||
-20000
|
||||
gpsSample.getLatitude() / 10000000d
|
||||
);
|
||||
ap.setLocation(coordinate);
|
||||
|
||||
|
@ -26,13 +26,20 @@ import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
import java.util.UUID;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.BuildConfig;
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminAgpsInstallHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminPreferences;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr.GarminCapability;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
|
||||
@ -58,6 +65,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.deviceevents.
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.deviceevents.NotificationSubscriptionDeviceEvent;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.deviceevents.SupportedFileTypesDeviceEvent;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.deviceevents.WeatherRequestDeviceEvent;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FitImporter;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.PredefinedLocalMessage;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordData;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordDefinition;
|
||||
@ -264,17 +272,31 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni
|
||||
this.supportedFileTypeList.clear();
|
||||
this.supportedFileTypeList.addAll(((SupportedFileTypesDeviceEvent) deviceEvent).getSupportedFileTypes());
|
||||
} else if (deviceEvent instanceof FileDownloadedDeviceEvent) {
|
||||
LOG.debug("FILE DOWNLOAD COMPLETE {}", ((FileDownloadedDeviceEvent) deviceEvent).directoryEntry.getFileName());
|
||||
final FileTransferHandler.DirectoryEntry entry = ((FileDownloadedDeviceEvent) deviceEvent).directoryEntry;
|
||||
final String filename = entry.getFileName();
|
||||
LOG.debug("FILE DOWNLOAD COMPLETE {}", filename);
|
||||
|
||||
if (!getKeepActivityDataOnDevice()) // delete file from watch upon successful download
|
||||
sendOutgoingMessage(new SetFileFlagsMessage(((FileDownloadedDeviceEvent) deviceEvent).directoryEntry.getFileIndex(), SetFileFlagsMessage.FileFlags.ARCHIVE));
|
||||
if (entry.getFiletype().isFitFile()) {
|
||||
try {
|
||||
final File dir = getWritableExportDirectory();
|
||||
final File file = new File(dir, filename);
|
||||
final FitImporter fitImporter = new FitImporter(getContext(), getDevice());
|
||||
fitImporter.importFile(file);
|
||||
} catch (final IOException e) {
|
||||
LOG.error("Failed to import fit file", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (!getKeepActivityDataOnDevice()) { // delete file from watch upon successful download
|
||||
sendOutgoingMessage(new SetFileFlagsMessage(entry.getFileIndex(), SetFileFlagsMessage.FileFlags.ARCHIVE));
|
||||
}
|
||||
}
|
||||
|
||||
super.evaluateGBDeviceEvent(deviceEvent);
|
||||
}
|
||||
|
||||
private boolean getKeepActivityDataOnDevice() {
|
||||
return getDevicePrefs().getBoolean("keep_activity_data_on_device", true); // TODO: change to default false once we are sure of the consequences
|
||||
return getDevicePrefs().getBoolean("keep_activity_data_on_device", false);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -466,8 +488,9 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni
|
||||
FileTransferHandler.DirectoryEntry directoryEntry = filesToDownload.remove();
|
||||
while (checkFileExists(directoryEntry.getFileName()) || checkFileExists(directoryEntry.getLegacyFileName())) {
|
||||
LOG.debug("File: {} already downloaded, not downloading again.", directoryEntry.getFileName());
|
||||
if (!getKeepActivityDataOnDevice()) // delete file from watch if already downloaded
|
||||
if (!getKeepActivityDataOnDevice()) { // delete file from watch if already downloaded
|
||||
sendOutgoingMessage(new SetFileFlagsMessage(directoryEntry.getFileIndex(), SetFileFlagsMessage.FileFlags.ARCHIVE));
|
||||
}
|
||||
directoryEntry = filesToDownload.remove();
|
||||
}
|
||||
DownloadRequestMessage downloadRequestMessage = fileTransferHandler.downloadDirectoryEntry(directoryEntry);
|
||||
@ -692,4 +715,76 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni
|
||||
return agpsCacheDir;
|
||||
}
|
||||
|
||||
public GarminCoordinator getCoordinator() {
|
||||
return (GarminCoordinator) getDevice().getDeviceCoordinator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTestNewFunction() {
|
||||
parseAllFitFilesFromStorage();
|
||||
}
|
||||
|
||||
private void parseAllFitFilesFromStorage() {
|
||||
// This function as-is should only be used for debug purposes
|
||||
if (!BuildConfig.DEBUG) {
|
||||
LOG.error("This should never be used in release builds");
|
||||
return;
|
||||
}
|
||||
|
||||
LOG.info("Parsing all fit files from storage");
|
||||
|
||||
final File[] fitFiles;
|
||||
try {
|
||||
final File exportDir = getWritableExportDirectory();
|
||||
|
||||
if (!exportDir.exists() || !exportDir.isDirectory()) {
|
||||
LOG.error("export directory {} not found", exportDir);
|
||||
return;
|
||||
}
|
||||
|
||||
fitFiles = exportDir.listFiles((dir, name) -> name.endsWith(".fit"));
|
||||
if (fitFiles == null) {
|
||||
LOG.error("fitFiles is null for {}", exportDir);
|
||||
return;
|
||||
}
|
||||
if (fitFiles.length == 0) {
|
||||
LOG.error("No fit files found in {}", exportDir);
|
||||
return;
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
LOG.error("Failed to parse from storage", e);
|
||||
return;
|
||||
}
|
||||
|
||||
GB.updateTransferNotification("Parsing fit files", "...", true, 0, getContext());
|
||||
|
||||
try (DBHandler handler = GBApplication.acquireDB()) {
|
||||
final DaoSession session = handler.getDaoSession();
|
||||
final Device device = DBHelper.getDevice(gbDevice, session);
|
||||
getCoordinator().deleteAllActivityData(device, session);
|
||||
} catch (final Exception e) {
|
||||
GB.toast(getContext(), "Error deleting activity data", Toast.LENGTH_LONG, GB.ERROR, e);
|
||||
}
|
||||
|
||||
try {
|
||||
int i = 0;
|
||||
for (final File file : fitFiles) {
|
||||
i++;
|
||||
LOG.debug("Parsing {}", file);
|
||||
|
||||
GB.updateTransferNotification("Parsing fit files", "File " + i + " of " + fitFiles.length, true, (i * 100) / fitFiles.length, getContext());
|
||||
|
||||
try {
|
||||
final FitImporter fitImporter = new FitImporter(getContext(), getDevice());
|
||||
fitImporter.importFile(file);
|
||||
} catch (final Exception ex) {
|
||||
LOG.error("Exception while importing {}", file, ex);
|
||||
}
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
LOG.error("Failed to parse from storage", e);
|
||||
}
|
||||
|
||||
GB.updateTransferNotification("", "", false, 100, getContext());
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,10 @@ public final class GarminUtils {
|
||||
// utility class
|
||||
}
|
||||
|
||||
public static double semicirclesToDegrees(final long semicircles) {
|
||||
return semicircles * (180.0D / 0x80000000L);
|
||||
}
|
||||
|
||||
public static GdiCore.CoreService.LocationData toLocationData(final Location location, final GdiCore.CoreService.DataType dataType) {
|
||||
final GdiCore.CoreService.LatLon positionForWatch = GdiCore.CoreService.LatLon.newBuilder()
|
||||
.setLat((int) ((location.getLatitude() * 2.147483648E9d) / 180.0d))
|
||||
|
@ -39,7 +39,7 @@ public class FitFile {
|
||||
this.canGenerateOutput = true;
|
||||
}
|
||||
|
||||
private static byte[] readFileToByteArray(File file) {
|
||||
private static byte[] readFileToByteArray(File file) throws IOException {
|
||||
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); InputStream inputStream = new FileInputStream(file)) {
|
||||
byte[] buffer = new byte[1024];
|
||||
int length;
|
||||
@ -47,12 +47,10 @@ public class FitFile {
|
||||
outputStream.write(buffer, 0, length);
|
||||
}
|
||||
return outputStream.toByteArray();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static FitFile parseIncoming(File file) {
|
||||
public static FitFile parseIncoming(File file) throws IOException {
|
||||
return parseIncoming(readFileToByteArray(file));
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,491 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit;
|
||||
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.ACTIVE_SECONDS;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.ASCENT_DISTANCE;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.CALORIES_BURNT;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.DESCENT_DISTANCE;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.DISTANCE_METERS;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_KCAL;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_METERS;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_SECONDS;
|
||||
|
||||
import android.content.Context;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.SortedMap;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminActivitySampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminEventSampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminSleepStageSampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminSpo2SampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminStressSampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.GarminActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.GarminEventSample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.GarminSleepStageSample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.GarminSpo2Sample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.GarminStressSample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.User;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityPoint;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryData;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.GarminTimeUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionSleepStage;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitEvent;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitFileId;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitMonitoring;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitRecord;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitSleepStage;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitSpo2;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitSport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitStressLevel;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitTimeInZone;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
public class FitImporter {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(FitImporter.class);
|
||||
|
||||
private final Context context;
|
||||
private final GBDevice gbDevice;
|
||||
|
||||
private final List<GarminActivitySample> activitySamples = new ArrayList<>();
|
||||
private final SortedMap<Integer, List<GarminActivitySample>> activitySamplesPerTimestamp = new TreeMap<>();
|
||||
private final List<GarminStressSample> stressSamples = new ArrayList<>();
|
||||
private final List<GarminSpo2Sample> spo2samples = new ArrayList<>();
|
||||
private final List<GarminEventSample> events = new ArrayList<>();
|
||||
private final List<GarminSleepStageSample> sleepStageSamples = new ArrayList<>();
|
||||
private final List<FitTimeInZone> timesInZone = new ArrayList<>();
|
||||
private final List<ActivityPoint> activityPoints = new ArrayList<>();
|
||||
private final Map<Integer, Integer> unknownRecords = new HashMap<>();
|
||||
private FitFileId fileId = null;
|
||||
private FitSession session = null;
|
||||
private FitSport sport = null;
|
||||
|
||||
public FitImporter(final Context context, final GBDevice gbDevice) {
|
||||
this.context = context;
|
||||
this.gbDevice = gbDevice;
|
||||
}
|
||||
|
||||
public void importFile(final File file) throws IOException {
|
||||
reset();
|
||||
|
||||
final FitFile fitFile = FitFile.parseIncoming(file);
|
||||
|
||||
for (final RecordData record : fitFile.getRecords()) {
|
||||
final Long ts = record.getComputedTimestamp();
|
||||
|
||||
if (record instanceof FitFileId) {
|
||||
final FitFileId newFileId = (FitFileId) record;
|
||||
LOG.debug("File ID: {}", newFileId);
|
||||
if (fileId != null) {
|
||||
// Should not happen
|
||||
LOG.warn("Already had a file ID: {}", fileId);
|
||||
}
|
||||
fileId = newFileId;
|
||||
} else if (record instanceof FitStressLevel) {
|
||||
final Integer stress = ((FitStressLevel) record).getStressLevelValue();
|
||||
if (stress == null || stress < 0) {
|
||||
continue;
|
||||
}
|
||||
LOG.trace("Stress at {}: {}", ts, stress);
|
||||
final GarminStressSample sample = new GarminStressSample();
|
||||
sample.setTimestamp(ts * 1000L);
|
||||
sample.setStress(stress);
|
||||
stressSamples.add(sample);
|
||||
} else if (record instanceof FitSleepStage) {
|
||||
final FieldDefinitionSleepStage.SleepStage stage = ((FitSleepStage) record).getSleepStage();
|
||||
if (stage == null) {
|
||||
continue;
|
||||
}
|
||||
LOG.trace("Sleep stage at {}: {}", ts, record);
|
||||
final GarminSleepStageSample sample = new GarminSleepStageSample();
|
||||
sample.setTimestamp(ts * 1000L);
|
||||
sample.setStage(stage.getId());
|
||||
sleepStageSamples.add(sample);
|
||||
} else if (record instanceof FitMonitoring) {
|
||||
final Integer hr = ((FitMonitoring) record).getHeartRate();
|
||||
final Long steps = ((FitMonitoring) record).getCycles();
|
||||
final Integer activityType = ((FitMonitoring) record).getComputedActivityType();
|
||||
final Integer intensity = ((FitMonitoring) record).getComputedIntensity();
|
||||
LOG.trace("Monitoring at {}: hr={} steps={} activityType={} intensity={}", ts, hr, steps, activityType, intensity);
|
||||
final GarminActivitySample sample = new GarminActivitySample();
|
||||
sample.setTimestamp(ts.intValue());
|
||||
if (hr != null) {
|
||||
sample.setHeartRate(hr);
|
||||
}
|
||||
if (steps != null) {
|
||||
sample.setSteps(steps.intValue());
|
||||
}
|
||||
if (activityType != null) {
|
||||
sample.setRawKind(activityType);
|
||||
}
|
||||
if (intensity != null) {
|
||||
sample.setRawIntensity(intensity);
|
||||
}
|
||||
activitySamples.add(sample);
|
||||
List<GarminActivitySample> samplesForTimestamp = activitySamplesPerTimestamp.get(ts.intValue());
|
||||
if (samplesForTimestamp == null) {
|
||||
samplesForTimestamp = new ArrayList<>();
|
||||
activitySamplesPerTimestamp.put(ts.intValue(), samplesForTimestamp);
|
||||
}
|
||||
samplesForTimestamp.add(sample);
|
||||
} else if (record instanceof FitSpo2) {
|
||||
final Integer spo2 = ((FitSpo2) record).getReadingSpo2();
|
||||
if (spo2 == null || spo2 <= 0) {
|
||||
continue;
|
||||
}
|
||||
LOG.trace("SpO2 at {}: {}", ts, spo2);
|
||||
final GarminSpo2Sample sample = new GarminSpo2Sample();
|
||||
sample.setTimestamp(ts * 1000L);
|
||||
sample.setSpo2(spo2);
|
||||
spo2samples.add(sample);
|
||||
} else if (record instanceof FitEvent) {
|
||||
final FitEvent event = (FitEvent) record;
|
||||
if (event.getEvent() == null) {
|
||||
LOG.warn("Event in {} is null", event);
|
||||
continue;
|
||||
}
|
||||
|
||||
LOG.trace("Event at {}: {}", ts, event);
|
||||
|
||||
final GarminEventSample sample = new GarminEventSample();
|
||||
sample.setTimestamp(ts * 1000L);
|
||||
sample.setEvent(event.getEvent());
|
||||
if (event.getEventType() != null) {
|
||||
sample.setEventType(event.getEventType());
|
||||
}
|
||||
if (event.getData() != null) {
|
||||
sample.setData(event.getData());
|
||||
}
|
||||
events.add(sample);
|
||||
} else if (record instanceof FitRecord) {
|
||||
activityPoints.add(((FitRecord) record).toActivityPoint());
|
||||
} else if (record instanceof FitSession) {
|
||||
LOG.debug("Session: {}", record);
|
||||
if (session != null) {
|
||||
LOG.warn("Got multiple sessions - NOT SUPPORTED: {}", record);
|
||||
} else {
|
||||
// We only support 1 session
|
||||
session = (FitSession) record;
|
||||
}
|
||||
} else if (record instanceof FitSport) {
|
||||
LOG.debug("Sport: {}", record);
|
||||
if (sport != null) {
|
||||
LOG.warn("Got multiple sports - NOT SUPPORTED: {}", record);
|
||||
} else {
|
||||
// We only support 1 sport
|
||||
sport = (FitSport) record;
|
||||
}
|
||||
} else if (record instanceof FitTimeInZone) {
|
||||
LOG.trace("Time in zone: {}", record);
|
||||
timesInZone.add((FitTimeInZone) record);
|
||||
} else {
|
||||
LOG.trace("Unknown record: {}", record);
|
||||
|
||||
if (!unknownRecords.containsKey(record.getGlobalFITMessage().getNumber())) {
|
||||
unknownRecords.put(record.getGlobalFITMessage().getNumber(), 0);
|
||||
}
|
||||
unknownRecords.put(
|
||||
record.getGlobalFITMessage().getNumber(),
|
||||
Objects.requireNonNull(unknownRecords.get(record.getGlobalFITMessage().getNumber())) + 1
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (fileId == null) {
|
||||
LOG.error("Got no file ID");
|
||||
return;
|
||||
}
|
||||
if (fileId.getType() == null) {
|
||||
LOG.error("File has no type");
|
||||
return;
|
||||
}
|
||||
|
||||
switch (fileId.getType()) {
|
||||
case activity:
|
||||
persistWorkout(file);
|
||||
break;
|
||||
case monitor:
|
||||
persistActivitySamples();
|
||||
persistSpo2Samples();
|
||||
persistStressSamples();
|
||||
break;
|
||||
case sleep:
|
||||
persistEvents();
|
||||
persistSleepStageSamples();
|
||||
break;
|
||||
default:
|
||||
LOG.warn("Unable to handle fit file of type {}", fileId.getType());
|
||||
}
|
||||
|
||||
for (final Map.Entry<Integer, Integer> e : unknownRecords.entrySet()) {
|
||||
LOG.warn("Unknown record of global number {} seen {} times", e.getKey(), e.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
private void persistWorkout(final File file) {
|
||||
if (session == null) {
|
||||
LOG.error("Got workout from {}, but no session", fileId);
|
||||
return;
|
||||
}
|
||||
if (sport == null) {
|
||||
LOG.error("Got workout from {}, but no sport", fileId);
|
||||
return;
|
||||
}
|
||||
|
||||
LOG.debug("Persisting workout for {}", fileId);
|
||||
|
||||
final BaseActivitySummary summary = new BaseActivitySummary();
|
||||
summary.setActivityKind(ActivityKind.TYPE_UNKNOWN);
|
||||
|
||||
final ActivitySummaryData summaryData = new ActivitySummaryData();
|
||||
|
||||
// TODO map all sports
|
||||
if (sport.getSport() != null) {
|
||||
switch (sport.getSport()) {
|
||||
case 2:
|
||||
summary.setActivityKind(ActivityKind.TYPE_CYCLING);
|
||||
break;
|
||||
case 4: // fitness_equipment
|
||||
case 10: // training
|
||||
if (sport.getSubSport() != null) {
|
||||
switch (sport.getSubSport()) {
|
||||
case 15:
|
||||
summary.setActivityKind(ActivityKind.TYPE_ELLIPTICAL_TRAINER);
|
||||
break;
|
||||
default:
|
||||
LOG.warn("Unknown sub sport {}", sport.getSubSport());
|
||||
summaryData.add("Fit Sub Sport", sport.getSubSport(), "");
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
LOG.warn("Unknown sport {}", sport.getSport());
|
||||
summaryData.add("Fit Sport", sport.getSport(), "");
|
||||
}
|
||||
}
|
||||
|
||||
summary.setName(sport.getName());
|
||||
if (session.getStartTime() == null) {
|
||||
LOG.error("No session start time for {}", fileId);
|
||||
return;
|
||||
}
|
||||
summary.setStartTime(new Date(GarminTimeUtils.garminTimestampToJavaMillis(session.getStartTime().intValue())));
|
||||
|
||||
if (session.getTotalElapsedTime() == null) {
|
||||
LOG.error("No elapsed time for {}", fileId);
|
||||
return;
|
||||
}
|
||||
summary.setEndTime(new Date(GarminTimeUtils.garminTimestampToJavaMillis(session.getStartTime().intValue() + session.getTotalElapsedTime().intValue() / 1000)));
|
||||
|
||||
if (session.getTotalTimerTime() != null) {
|
||||
summaryData.add(ACTIVE_SECONDS, session.getTotalTimerTime() / 1000f, UNIT_SECONDS);
|
||||
}
|
||||
if (session.getTotalDistance() != null) {
|
||||
summaryData.add(DISTANCE_METERS, session.getTotalDistance() / 100f, UNIT_METERS);
|
||||
}
|
||||
if (session.getTotalCalories() != null) {
|
||||
summaryData.add(CALORIES_BURNT, session.getTotalCalories(), UNIT_KCAL);
|
||||
}
|
||||
if (session.getTotalAscent() != null) {
|
||||
summaryData.add(ASCENT_DISTANCE, session.getTotalAscent(), UNIT_METERS);
|
||||
}
|
||||
if (session.getTotalDescent() != null) {
|
||||
summaryData.add(DESCENT_DISTANCE, session.getTotalDescent(), UNIT_METERS);
|
||||
}
|
||||
|
||||
//FitTimeInZone timeInZone = null;
|
||||
//for (final FitTimeInZone fitTimeInZone : timesInZone) {
|
||||
// // Find the firt time in zone for the session (assumes single-session)
|
||||
// if (fitTimeInZone.getReferenceMessage() != null && fitTimeInZone.getReferenceMessage() == 18) {
|
||||
// timeInZone = fitTimeInZone;
|
||||
// break;
|
||||
// }
|
||||
//}
|
||||
//if (timeInZone != null) {
|
||||
//}
|
||||
|
||||
summary.setSummaryData(summaryData.toString());
|
||||
if (file != null) {
|
||||
summary.setRawDetailsPath(file.getAbsolutePath());
|
||||
}
|
||||
|
||||
try (DBHandler dbHandler = GBApplication.acquireDB()) {
|
||||
final DaoSession session = dbHandler.getDaoSession();
|
||||
final Device device = DBHelper.getDevice(gbDevice, session);
|
||||
final User user = DBHelper.getUser(session);
|
||||
|
||||
summary.setDevice(device);
|
||||
summary.setUser(user);
|
||||
|
||||
session.getBaseActivitySummaryDao().insertOrReplace(summary);
|
||||
} catch (final Exception e) {
|
||||
GB.toast(context, "Error saving workout", Toast.LENGTH_LONG, GB.ERROR, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void reset() {
|
||||
activitySamples.clear();
|
||||
stressSamples.clear();
|
||||
spo2samples.clear();
|
||||
events.clear();
|
||||
sleepStageSamples.clear();
|
||||
timesInZone.clear();
|
||||
activityPoints.clear();
|
||||
unknownRecords.clear();
|
||||
fileId = null;
|
||||
session = null;
|
||||
sport = null;
|
||||
}
|
||||
|
||||
private void persistActivitySamples() {
|
||||
if (activitySamples.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// FIXME prevent overlapping samples in the same timestamp..
|
||||
|
||||
LOG.debug("Will persist {} activity samples", activitySamples.size());
|
||||
|
||||
try (DBHandler handler = GBApplication.acquireDB()) {
|
||||
final DaoSession session = handler.getDaoSession();
|
||||
|
||||
final Device device = DBHelper.getDevice(gbDevice, session);
|
||||
final User user = DBHelper.getUser(session);
|
||||
|
||||
final GarminActivitySampleProvider sampleProvider = new GarminActivitySampleProvider(gbDevice, session);
|
||||
|
||||
for (final GarminActivitySample sample : activitySamples) {
|
||||
sample.setDevice(device);
|
||||
sample.setUser(user);
|
||||
}
|
||||
|
||||
sampleProvider.addGBActivitySamples(activitySamples.toArray(new GarminActivitySample[0]));
|
||||
} catch (final Exception e) {
|
||||
GB.toast(context, "Error saving activity samples", Toast.LENGTH_LONG, GB.ERROR, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void persistEvents() {
|
||||
if (events.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
LOG.debug("Will persist {} event samples", events.size());
|
||||
|
||||
try (DBHandler handler = GBApplication.acquireDB()) {
|
||||
final DaoSession session = handler.getDaoSession();
|
||||
|
||||
final Device device = DBHelper.getDevice(gbDevice, session);
|
||||
final User user = DBHelper.getUser(session);
|
||||
|
||||
final GarminEventSampleProvider sampleProvider = new GarminEventSampleProvider(gbDevice, session);
|
||||
|
||||
for (final GarminEventSample sample : events) {
|
||||
sample.setDevice(device);
|
||||
sample.setUser(user);
|
||||
}
|
||||
|
||||
sampleProvider.addSamples(events);
|
||||
} catch (final Exception e) {
|
||||
GB.toast(context, "Error saving event samples", Toast.LENGTH_LONG, GB.ERROR, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void persistSleepStageSamples() {
|
||||
if (sleepStageSamples.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
LOG.debug("Will persist {} sleep stage samples", sleepStageSamples.size());
|
||||
|
||||
try (DBHandler handler = GBApplication.acquireDB()) {
|
||||
final DaoSession session = handler.getDaoSession();
|
||||
|
||||
final Device device = DBHelper.getDevice(gbDevice, session);
|
||||
final User user = DBHelper.getUser(session);
|
||||
|
||||
final GarminSleepStageSampleProvider sampleProvider = new GarminSleepStageSampleProvider(gbDevice, session);
|
||||
|
||||
for (final GarminSleepStageSample sample : sleepStageSamples) {
|
||||
sample.setDevice(device);
|
||||
sample.setUser(user);
|
||||
}
|
||||
|
||||
sampleProvider.addSamples(sleepStageSamples);
|
||||
} catch (final Exception e) {
|
||||
GB.toast(context, "Error saving sleep stage samples", Toast.LENGTH_LONG, GB.ERROR, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void persistSpo2Samples() {
|
||||
if (spo2samples.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
LOG.debug("Will persist {} spo2 samples", stressSamples.size());
|
||||
|
||||
try (DBHandler handler = GBApplication.acquireDB()) {
|
||||
final DaoSession session = handler.getDaoSession();
|
||||
|
||||
final Device device = DBHelper.getDevice(gbDevice, session);
|
||||
final User user = DBHelper.getUser(session);
|
||||
|
||||
final GarminSpo2SampleProvider sampleProvider = new GarminSpo2SampleProvider(gbDevice, session);
|
||||
|
||||
for (final GarminSpo2Sample sample : spo2samples) {
|
||||
sample.setDevice(device);
|
||||
sample.setUser(user);
|
||||
}
|
||||
|
||||
sampleProvider.addSamples(spo2samples);
|
||||
} catch (final Exception e) {
|
||||
GB.toast(context, "Error saving spo2 samples", Toast.LENGTH_LONG, GB.ERROR, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void persistStressSamples() {
|
||||
if (stressSamples.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
LOG.debug("Will persist {} stress samples", stressSamples.size());
|
||||
|
||||
try (DBHandler handler = GBApplication.acquireDB()) {
|
||||
final DaoSession session = handler.getDaoSession();
|
||||
|
||||
final Device device = DBHelper.getDevice(gbDevice, session);
|
||||
final User user = DBHelper.getUser(session);
|
||||
|
||||
final GarminStressSampleProvider sampleProvider = new GarminStressSampleProvider(gbDevice, session);
|
||||
|
||||
for (final GarminStressSample sample : stressSamples) {
|
||||
sample.setDevice(device);
|
||||
sample.setUser(user);
|
||||
}
|
||||
|
||||
sampleProvider.addSamples(stressSamples);
|
||||
} catch (final Exception e) {
|
||||
GB.toast(context, "Error saving stress samples", Toast.LENGTH_LONG, GB.ERROR, e);
|
||||
}
|
||||
}
|
||||
}
|
@ -2,6 +2,11 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityPoint;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.GPSCoordinate;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.GarminUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordData;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordDefinition;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordHeader;
|
||||
@ -64,4 +69,25 @@ public class FitRecord extends RecordData {
|
||||
public Long getTimestamp() {
|
||||
return (Long) getFieldByNumber(253);
|
||||
}
|
||||
|
||||
// manual changes below
|
||||
|
||||
public ActivityPoint toActivityPoint() {
|
||||
final ActivityPoint activityPoint = new ActivityPoint();
|
||||
activityPoint.setTime(new Date(getComputedTimestamp()));
|
||||
if (getLatitude() != null && getLongitude() != null) {
|
||||
activityPoint.setLocation(new GPSCoordinate(
|
||||
GarminUtils.semicirclesToDegrees(getLongitude().longValue()),
|
||||
GarminUtils.semicirclesToDegrees(getLatitude().longValue()),
|
||||
getEnhancedAltitude() != null ? getEnhancedAltitude() / 10d : GPSCoordinate.UNKNOWN_ALTITUDE
|
||||
));
|
||||
}
|
||||
if (getHeartRate() != null) {
|
||||
activityPoint.setHeartRate(getHeartRate());
|
||||
}
|
||||
if (getEnhancedSpeed() != null) {
|
||||
activityPoint.setSpeed(getEnhancedSpeed());
|
||||
}
|
||||
return activityPoint;
|
||||
}
|
||||
}
|
||||
|
@ -22,14 +22,33 @@ import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A map of lower bounds for ranges.
|
||||
* A map of bounds for ranges. Returns the value closest to the key, in upper or lower bound mode.
|
||||
*/
|
||||
public class RangeMap<K extends Comparable<K>, V> {
|
||||
private final List<Pair<K, V>> list = new ArrayList<>();
|
||||
private boolean isSorted = false;
|
||||
private final Comparator<K> comparator;
|
||||
|
||||
public RangeMap() {
|
||||
this(Mode.LOWER_BOUND);
|
||||
}
|
||||
|
||||
public RangeMap(final Mode mode) {
|
||||
switch (mode) {
|
||||
case LOWER_BOUND:
|
||||
comparator = (k1, k2) -> k1.compareTo(k2);
|
||||
break;
|
||||
case UPPER_BOUND:
|
||||
comparator = (k1, k2) -> k2.compareTo(k1);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown mode " + mode);
|
||||
}
|
||||
}
|
||||
|
||||
public void put(final K key, final V value) {
|
||||
list.add(Pair.create(key, value));
|
||||
@ -39,14 +58,12 @@ public class RangeMap<K extends Comparable<K>, V> {
|
||||
@Nullable
|
||||
public V get(final K key) {
|
||||
if (!isSorted) {
|
||||
Collections.sort(list, (a, b) -> {
|
||||
return a.first.compareTo(b.first);
|
||||
});
|
||||
Collections.sort(list, (a, b) -> comparator.compare(a.first, b.first));
|
||||
isSorted = true;
|
||||
}
|
||||
|
||||
for (int i = list.size() - 1; i >= 0; i--) {
|
||||
if (key.compareTo(list.get(i).first) > 0) {
|
||||
if (comparator.compare(key, list.get(i).first) >= 0) {
|
||||
return list.get(i).second;
|
||||
}
|
||||
}
|
||||
@ -61,4 +78,10 @@ public class RangeMap<K extends Comparable<K>, V> {
|
||||
public int size() {
|
||||
return list.size();
|
||||
}
|
||||
|
||||
public enum Mode {
|
||||
LOWER_BOUND,
|
||||
UPPER_BOUND,
|
||||
;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,51 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.util;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.test.TestBase;
|
||||
|
||||
public class RangeMapTest extends TestBase {
|
||||
@Test
|
||||
public void testLowerBound() {
|
||||
final RangeMap<Integer, Integer> map = new RangeMap<>();
|
||||
assertEquals(0, map.size());
|
||||
assertNull(map.get(0));
|
||||
|
||||
map.put(10, 20);
|
||||
assertNull(map.get(0));
|
||||
assertEquals(20, map.get(10).intValue());
|
||||
assertEquals(20, map.get(20).intValue());
|
||||
|
||||
map.put(20, 30);
|
||||
map.put(30, 40);
|
||||
assertNull(map.get(0));
|
||||
assertEquals(20, map.get(10).intValue());
|
||||
assertEquals(20, map.get(15).intValue());
|
||||
assertEquals(30, map.get(20).intValue());
|
||||
assertEquals(30, map.get(25).intValue());
|
||||
assertEquals(40, map.get(30).intValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpperBound() {
|
||||
final RangeMap<Integer, Integer> map = new RangeMap<>(RangeMap.Mode.UPPER_BOUND);
|
||||
assertEquals(0, map.size());
|
||||
assertNull(map.get(0));
|
||||
|
||||
map.put(10, 20);
|
||||
assertNull(map.get(20));
|
||||
assertEquals(20, map.get(10).intValue());
|
||||
assertEquals(20, map.get(0).intValue());
|
||||
|
||||
map.put(20, 30);
|
||||
map.put(30, 40);
|
||||
assertNull(map.get(50));
|
||||
assertEquals(40, map.get(30).intValue());
|
||||
assertEquals(40, map.get(25).intValue());
|
||||
assertEquals(30, map.get(20).intValue());
|
||||
assertEquals(30, map.get(15).intValue());
|
||||
assertEquals(20, map.get(10).intValue());
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user