mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2024-06-14 09:00:04 +02:00
Garmin WIP: Fix sleep parsing
This commit is contained in:
parent
de8c865ce2
commit
a240d0429e
|
@ -112,6 +112,7 @@ public class GBDaoGenerator {
|
||||||
addGarminStressSample(schema, user, device);
|
addGarminStressSample(schema, user, device);
|
||||||
addGarminSpo2Sample(schema, user, device);
|
addGarminSpo2Sample(schema, user, device);
|
||||||
addGarminSleepStageSample(schema, user, device);
|
addGarminSleepStageSample(schema, user, device);
|
||||||
|
addGarminEventSample(schema, user, device);
|
||||||
addWena3EnergySample(schema, user, device);
|
addWena3EnergySample(schema, user, device);
|
||||||
addWena3BehaviorSample(schema, user, device);
|
addWena3BehaviorSample(schema, user, device);
|
||||||
addWena3CaloriesSample(schema, user, device);
|
addWena3CaloriesSample(schema, user, device);
|
||||||
|
@ -702,6 +703,15 @@ public class GBDaoGenerator {
|
||||||
return sleepStageSample;
|
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) {
|
private static Entity addWatchXPlusHealthActivitySample(Schema schema, Entity user, Entity device) {
|
||||||
Entity activitySample = addEntity(schema, "WatchXPlusActivitySample");
|
Entity activitySample = addEntity(schema, "WatchXPlusActivitySample");
|
||||||
activitySample.implementsSerializable();
|
activitySample.implementsSerializable();
|
||||||
|
|
|
@ -34,12 +34,17 @@ import java.io.File;
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.model.GPSCoordinate;
|
import nodomain.freeyourgadget.gadgetbridge.model.GPSCoordinate;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FitFile;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FitImporter;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordData;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitRecord;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.util.gpx.GpxParseException;
|
import nodomain.freeyourgadget.gadgetbridge.util.gpx.GpxParseException;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.util.gpx.GpxParser;
|
import nodomain.freeyourgadget.gadgetbridge.util.gpx.GpxParser;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.util.gpx.model.GpxFile;
|
import nodomain.freeyourgadget.gadgetbridge.util.gpx.model.GpxFile;
|
||||||
|
@ -76,21 +81,40 @@ public class ActivitySummariesGpsFragment extends AbstractGBFragment {
|
||||||
new Thread(new Runnable() {
|
new Thread(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
final GpxFile gpxFile;
|
final List<GPSCoordinate> points = new ArrayList<>();
|
||||||
|
if (inputFile.getName().endsWith(".gpx")) {
|
||||||
try (FileInputStream inputStream = new FileInputStream(inputFile)) {
|
try (FileInputStream inputStream = new FileInputStream(inputFile)) {
|
||||||
final GpxParser gpxParser = new GpxParser(inputStream);
|
final GpxParser gpxParser = new GpxParser(inputStream);
|
||||||
gpxFile = gpxParser.getGpxFile();
|
points.addAll(gpxParser.getGpxFile().getPoints());
|
||||||
} catch (final IOException e) {
|
} catch (final IOException e) {
|
||||||
LOG.error("Failed to open {}", inputFile, e);
|
LOG.error("Failed to open {}", inputFile, e);
|
||||||
return;
|
return;
|
||||||
} catch (final GpxParseException e) {
|
} catch (final GpxParseException e) {
|
||||||
LOG.error("Failed to parse gpx file", e);
|
LOG.error("Failed to parse gpx file", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (inputFile.getName().endsWith(".fit")) {
|
||||||
|
try {
|
||||||
|
FitFile fitFile = FitFile.parseIncoming(inputFile);
|
||||||
|
for (final RecordData record : fitFile.getRecords()) {
|
||||||
|
if (record instanceof FitRecord) {
|
||||||
|
points.add(((FitRecord) record).toActivityPoint().getLocation());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (final IOException e) {
|
||||||
|
LOG.error("Failed to open {}", inputFile, e);
|
||||||
|
return;
|
||||||
|
} catch (final Exception e) {
|
||||||
|
LOG.error("Failed to parse fit file", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LOG.warn("Unknown file type {}", inputFile.getName());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!gpxFile.getPoints().isEmpty()) {
|
if (!points.isEmpty()) {
|
||||||
drawTrack(canvas, gpxFile.getPoints());
|
drawTrack(canvas, points);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}).start();
|
}).start();
|
||||||
|
|
|
@ -184,9 +184,9 @@ public class ActivitySummaryDetail extends AbstractGBActivity {
|
||||||
makeSummaryHeader(newItem);
|
makeSummaryHeader(newItem);
|
||||||
makeSummaryContent(newItem);
|
makeSummaryContent(newItem);
|
||||||
activitySummariesChartFragment.setDateAndGetData(getGBDevice(currentItem.getDevice()), currentItem.getStartTime().getTime() / 1000, currentItem.getEndTime().getTime() / 1000);
|
activitySummariesChartFragment.setDateAndGetData(getGBDevice(currentItem.getDevice()), currentItem.getStartTime().getTime() / 1000, currentItem.getEndTime().getTime() / 1000);
|
||||||
if (get_gpx_file() != null) {
|
if (getTrackFile() != null) {
|
||||||
showCanvas();
|
showCanvas();
|
||||||
activitySummariesGpsFragment.set_data(get_gpx_file());
|
activitySummariesGpsFragment.set_data(getTrackFile());
|
||||||
} else {
|
} else {
|
||||||
hideCanvas();
|
hideCanvas();
|
||||||
}
|
}
|
||||||
|
@ -206,9 +206,9 @@ public class ActivitySummaryDetail extends AbstractGBActivity {
|
||||||
makeSummaryHeader(newItem);
|
makeSummaryHeader(newItem);
|
||||||
makeSummaryContent(newItem);
|
makeSummaryContent(newItem);
|
||||||
activitySummariesChartFragment.setDateAndGetData(getGBDevice(currentItem.getDevice()), currentItem.getStartTime().getTime() / 1000, currentItem.getEndTime().getTime() / 1000);
|
activitySummariesChartFragment.setDateAndGetData(getGBDevice(currentItem.getDevice()), currentItem.getStartTime().getTime() / 1000, currentItem.getEndTime().getTime() / 1000);
|
||||||
if (get_gpx_file() != null) {
|
if (getTrackFile() != null) {
|
||||||
showCanvas();
|
showCanvas();
|
||||||
activitySummariesGpsFragment.set_data(get_gpx_file());
|
activitySummariesGpsFragment.set_data(getTrackFile());
|
||||||
} else {
|
} else {
|
||||||
hideCanvas();
|
hideCanvas();
|
||||||
}
|
}
|
||||||
|
@ -227,9 +227,9 @@ public class ActivitySummaryDetail extends AbstractGBActivity {
|
||||||
makeSummaryHeader(currentItem);
|
makeSummaryHeader(currentItem);
|
||||||
makeSummaryContent(currentItem);
|
makeSummaryContent(currentItem);
|
||||||
activitySummariesChartFragment.setDateAndGetData(getGBDevice(currentItem.getDevice()), currentItem.getStartTime().getTime() / 1000, currentItem.getEndTime().getTime() / 1000);
|
activitySummariesChartFragment.setDateAndGetData(getGBDevice(currentItem.getDevice()), currentItem.getStartTime().getTime() / 1000, currentItem.getEndTime().getTime() / 1000);
|
||||||
if (get_gpx_file() != null) {
|
if (getTrackFile() != null) {
|
||||||
showCanvas();
|
showCanvas();
|
||||||
activitySummariesGpsFragment.set_data(get_gpx_file());
|
activitySummariesGpsFragment.set_data(getTrackFile());
|
||||||
} else {
|
} else {
|
||||||
hideCanvas();
|
hideCanvas();
|
||||||
}
|
}
|
||||||
|
@ -320,9 +320,9 @@ public class ActivitySummaryDetail extends AbstractGBActivity {
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
public void onClick(DialogInterface dialog, int which) {
|
||||||
currentItem.setGpxTrack(selectedGpxFile);
|
currentItem.setGpxTrack(selectedGpxFile);
|
||||||
currentItem.update();
|
currentItem.update();
|
||||||
if (get_gpx_file() != null) {
|
if (getTrackFile() != null) {
|
||||||
showCanvas();
|
showCanvas();
|
||||||
activitySummariesGpsFragment.set_data(get_gpx_file());
|
activitySummariesGpsFragment.set_data(getTrackFile());
|
||||||
} else {
|
} else {
|
||||||
hideCanvas();
|
hideCanvas();
|
||||||
}
|
}
|
||||||
|
@ -712,7 +712,7 @@ public class ActivitySummaryDetail extends AbstractGBActivity {
|
||||||
gpsView.setLayoutParams(params);
|
gpsView.setLayoutParams(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
private File get_gpx_file() {
|
private File getTrackFile() {
|
||||||
final String gpxTrack = currentItem.getGpxTrack();
|
final String gpxTrack = currentItem.getGpxTrack();
|
||||||
if (gpxTrack != null) {
|
if (gpxTrack != null) {
|
||||||
File file = new File(gpxTrack);
|
File file = new File(gpxTrack);
|
||||||
|
@ -722,6 +722,15 @@ public class ActivitySummaryDetail extends AbstractGBActivity {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
final String rawDetails = currentItem.getRawDetailsPath();
|
||||||
|
if (rawDetails != null && rawDetails.endsWith(".fit")) {
|
||||||
|
File file = new File(rawDetails);
|
||||||
|
if (file.exists()) {
|
||||||
|
return file;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,10 +29,13 @@ import java.util.List;
|
||||||
import de.greenrobot.dao.AbstractDao;
|
import de.greenrobot.dao.AbstractDao;
|
||||||
import de.greenrobot.dao.Property;
|
import de.greenrobot.dao.Property;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractSampleProvider;
|
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractSampleProvider;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.XiaomiSleepTimeSampleProvider;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.entities.GarminActivitySample;
|
import nodomain.freeyourgadget.gadgetbridge.entities.GarminActivitySample;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.entities.GarminActivitySampleDao;
|
import nodomain.freeyourgadget.gadgetbridge.entities.GarminActivitySampleDao;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.entities.GarminEventSample;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.entities.GarminSleepStageSample;
|
import nodomain.freeyourgadget.gadgetbridge.entities.GarminSleepStageSample;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.entities.XiaomiSleepTimeSample;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
|
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionSleepStage;
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionSleepStage;
|
||||||
|
@ -142,15 +145,34 @@ public class GarminActivitySampleProvider extends AbstractSampleProvider<GarminA
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME this is giving slightly different results than the watch
|
|
||||||
public void overlaySleep(final List<GarminActivitySample> samples, final int timestamp_from, final int timestamp_to) {
|
public void overlaySleep(final List<GarminActivitySample> samples, final int timestamp_from, final int timestamp_to) {
|
||||||
final RangeMap<Long, Integer> stagesMap = new RangeMap<>();
|
// 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 GarminSleepStageSampleProvider sleepStagesSampleProvider = new GarminSleepStageSampleProvider(getDevice(), getSession());
|
||||||
final List<GarminSleepStageSample> stageSamples = sleepStagesSampleProvider.getAllSamples(
|
final List<GarminSleepStageSample> stageSamples = sleepStagesSampleProvider.getAllSamples(
|
||||||
timestamp_from * 1000L - 86400000L,
|
timestamp_from * 1000L - 86400000L,
|
||||||
timestamp_to * 1000L
|
timestamp_to * 1000L
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!stageSamples.isEmpty()) {
|
if (!stageSamples.isEmpty()) {
|
||||||
// We got actual sleep stages
|
// We got actual sleep stages
|
||||||
LOG.debug("Found {} sleep stage samples between {} and {}", stageSamples.size(), timestamp_from, timestamp_to);
|
LOG.debug("Found {} sleep stage samples between {} and {}", stageSamples.size(), timestamp_from, timestamp_to);
|
||||||
|
@ -183,8 +205,6 @@ public class GarminActivitySampleProvider extends AbstractSampleProvider<GarminA
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!stagesMap.isEmpty()) {
|
if (!stagesMap.isEmpty()) {
|
||||||
LOG.debug("Found {} sleep samples between {} and {}", stagesMap.size(), timestamp_from, timestamp_to);
|
|
||||||
|
|
||||||
for (final GarminActivitySample sample : samples) {
|
for (final GarminActivitySample sample : samples) {
|
||||||
final long ts = sample.getTimestamp() * 1000L;
|
final long ts = sample.getTimestamp() * 1000L;
|
||||||
final Integer sleepType = stagesMap.get(ts);
|
final Integer sleepType = stagesMap.get(ts);
|
||||||
|
|
|
@ -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,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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,10 +10,18 @@ public final class GarminUtils {
|
||||||
// utility class
|
// utility class
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static int degreesToSemicircles(final double degrees) {
|
||||||
|
return (int) ((degrees * 2.147483648E9d) / 180.0d);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static double semicirclesToDegrees(final long semicircles) {
|
||||||
|
return semicircles * (180.0D / 0x80000000);
|
||||||
|
}
|
||||||
|
|
||||||
public static GdiCore.CoreService.LocationData toLocationData(final Location location, final GdiCore.CoreService.DataType dataType) {
|
public static GdiCore.CoreService.LocationData toLocationData(final Location location, final GdiCore.CoreService.DataType dataType) {
|
||||||
final GdiCore.CoreService.LatLon positionForWatch = GdiCore.CoreService.LatLon.newBuilder()
|
final GdiCore.CoreService.LatLon positionForWatch = GdiCore.CoreService.LatLon.newBuilder()
|
||||||
.setLat((int) ((location.getLatitude() * 2.147483648E9d) / 180.0d))
|
.setLat(degreesToSemicircles(location.getLatitude()))
|
||||||
.setLon((int) ((location.getLongitude() * 2.147483648E9d) / 180.0d))
|
.setLon(degreesToSemicircles(location.getLongitude()))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
float vAccuracy = 0;
|
float vAccuracy = 0;
|
||||||
|
|
|
@ -12,8 +12,6 @@ import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import org.json.JSONException;
|
|
||||||
import org.json.JSONObject;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
@ -25,11 +23,14 @@ import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.SortedMap;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
|
||||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
|
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminActivitySampleProvider;
|
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.GarminSleepStageSampleProvider;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminSpo2SampleProvider;
|
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminSpo2SampleProvider;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminStressSampleProvider;
|
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminStressSampleProvider;
|
||||||
|
@ -37,6 +38,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.entities.GarminActivitySample;
|
import nodomain.freeyourgadget.gadgetbridge.entities.GarminActivitySample;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.entities.GarminEventSample;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.entities.GarminSleepStageSample;
|
import nodomain.freeyourgadget.gadgetbridge.entities.GarminSleepStageSample;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.entities.GarminSpo2Sample;
|
import nodomain.freeyourgadget.gadgetbridge.entities.GarminSpo2Sample;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.entities.GarminStressSample;
|
import nodomain.freeyourgadget.gadgetbridge.entities.GarminStressSample;
|
||||||
|
@ -44,7 +46,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.User;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
|
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityPoint;
|
import nodomain.freeyourgadget.gadgetbridge.model.ActivityPoint;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.model.GPSCoordinate;
|
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryData;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.GarminTimeUtils;
|
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.fieldDefinitions.FieldDefinitionSleepStage;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitEvent;
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitEvent;
|
||||||
|
@ -66,9 +68,10 @@ public class FitImporter {
|
||||||
private final GBDevice gbDevice;
|
private final GBDevice gbDevice;
|
||||||
|
|
||||||
private final List<GarminActivitySample> activitySamples = new ArrayList<>();
|
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<GarminStressSample> stressSamples = new ArrayList<>();
|
||||||
private final List<GarminSpo2Sample> spo2samples = new ArrayList<>();
|
private final List<GarminSpo2Sample> spo2samples = new ArrayList<>();
|
||||||
private final List<FitEvent> sleepEvents = new ArrayList<>();
|
private final List<GarminEventSample> events = new ArrayList<>();
|
||||||
private final List<GarminSleepStageSample> sleepStageSamples = new ArrayList<>();
|
private final List<GarminSleepStageSample> sleepStageSamples = new ArrayList<>();
|
||||||
private final List<FitTimeInZone> timesInZone = new ArrayList<>();
|
private final List<FitTimeInZone> timesInZone = new ArrayList<>();
|
||||||
private final List<ActivityPoint> activityPoints = new ArrayList<>();
|
private final List<ActivityPoint> activityPoints = new ArrayList<>();
|
||||||
|
@ -83,11 +86,11 @@ public class FitImporter {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void importFile(final File file) throws IOException {
|
public void importFile(final File file) throws IOException {
|
||||||
importFile(FitFile.parseIncoming(file));
|
reset();
|
||||||
}
|
|
||||||
|
|
||||||
public void importFile(final FitFile file) {
|
final FitFile fitFile = FitFile.parseIncoming(file);
|
||||||
for (final RecordData record : file.getRecords()) {
|
|
||||||
|
for (final RecordData record : fitFile.getRecords()) {
|
||||||
final Long ts = record.getComputedTimestamp();
|
final Long ts = record.getComputedTimestamp();
|
||||||
|
|
||||||
if (record instanceof FitFileId) {
|
if (record instanceof FitFileId) {
|
||||||
|
@ -123,7 +126,7 @@ public class FitImporter {
|
||||||
final Long steps = ((FitMonitoring) record).getCycles();
|
final Long steps = ((FitMonitoring) record).getCycles();
|
||||||
final Integer activityType = ((FitMonitoring) record).getComputedActivityType();
|
final Integer activityType = ((FitMonitoring) record).getComputedActivityType();
|
||||||
final Integer intensity = ((FitMonitoring) record).getComputedIntensity();
|
final Integer intensity = ((FitMonitoring) record).getComputedIntensity();
|
||||||
LOG.trace("Monitoring: hr={} steps={} activityType={} intensity={}", hr, steps, activityType, intensity);
|
LOG.trace("Monitoring at {}: hr={} steps={} activityType={} intensity={}", ts, hr, steps, activityType, intensity);
|
||||||
final GarminActivitySample sample = new GarminActivitySample();
|
final GarminActivitySample sample = new GarminActivitySample();
|
||||||
sample.setTimestamp(ts.intValue());
|
sample.setTimestamp(ts.intValue());
|
||||||
if (hr != null) {
|
if (hr != null) {
|
||||||
|
@ -139,6 +142,12 @@ public class FitImporter {
|
||||||
sample.setRawIntensity(intensity);
|
sample.setRawIntensity(intensity);
|
||||||
}
|
}
|
||||||
activitySamples.add(sample);
|
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) {
|
} else if (record instanceof FitSpo2) {
|
||||||
final Integer spo2 = ((FitSpo2) record).getReadingSpo2();
|
final Integer spo2 = ((FitSpo2) record).getReadingSpo2();
|
||||||
if (spo2 == null || spo2 <= 0) {
|
if (spo2 == null || spo2 <= 0) {
|
||||||
|
@ -151,30 +160,25 @@ public class FitImporter {
|
||||||
spo2samples.add(sample);
|
spo2samples.add(sample);
|
||||||
} else if (record instanceof FitEvent) {
|
} else if (record instanceof FitEvent) {
|
||||||
final FitEvent event = (FitEvent) record;
|
final FitEvent event = (FitEvent) record;
|
||||||
|
if (event.getEvent() == null) {
|
||||||
|
LOG.warn("Event in {} is null", event);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
LOG.trace("Event at {}: {}", ts, event);
|
LOG.trace("Event at {}: {}", ts, event);
|
||||||
|
|
||||||
if (event.getEvent() == null || event.getEvent() != 74) {
|
final GarminEventSample sample = new GarminEventSample();
|
||||||
continue; // we only handle sleep events for now
|
sample.setTimestamp(ts * 1000L);
|
||||||
|
sample.setEvent(event.getEvent());
|
||||||
|
if (event.getEventType() != null) {
|
||||||
|
sample.setEventType(event.getEventType());
|
||||||
}
|
}
|
||||||
|
if (event.getData() != null) {
|
||||||
sleepEvents.add(event);
|
sample.setData(event.getData());
|
||||||
|
}
|
||||||
|
events.add(sample);
|
||||||
} else if (record instanceof FitRecord) {
|
} else if (record instanceof FitRecord) {
|
||||||
final FitRecord fitRecord = (FitRecord) record;
|
activityPoints.add(((FitRecord) record).toActivityPoint());
|
||||||
final ActivityPoint activityPoint = new ActivityPoint();
|
|
||||||
if (fitRecord.getLatitude() != null && fitRecord.getLongitude() != null) {
|
|
||||||
activityPoint.setLocation(new GPSCoordinate(
|
|
||||||
fitRecord.getLongitude() * 485028008 / 2.147483648E9d,
|
|
||||||
fitRecord.getLatitude() * 485028008 / 2.147483648E9d,
|
|
||||||
fitRecord.getEnhancedAltitude() != null ? fitRecord.getEnhancedAltitude() / 10d : GPSCoordinate.UNKNOWN_ALTITUDE
|
|
||||||
));
|
|
||||||
}
|
|
||||||
if (fitRecord.getHeartRate() != null) {
|
|
||||||
activityPoint.setHeartRate(fitRecord.getHeartRate());
|
|
||||||
}
|
|
||||||
if (fitRecord.getEnhancedSpeed() != null) {
|
|
||||||
activityPoint.setSpeed(fitRecord.getEnhancedSpeed());
|
|
||||||
}
|
|
||||||
activityPoints.add(activityPoint);
|
|
||||||
} else if (record instanceof FitSession) {
|
} else if (record instanceof FitSession) {
|
||||||
LOG.debug("Session: {}", record);
|
LOG.debug("Session: {}", record);
|
||||||
if (session != null) {
|
if (session != null) {
|
||||||
|
@ -218,7 +222,7 @@ public class FitImporter {
|
||||||
|
|
||||||
switch (fileId.getType()) {
|
switch (fileId.getType()) {
|
||||||
case activity:
|
case activity:
|
||||||
persistActivity();
|
persistWorkout(file);
|
||||||
break;
|
break;
|
||||||
case monitor:
|
case monitor:
|
||||||
persistActivitySamples();
|
persistActivitySamples();
|
||||||
|
@ -226,6 +230,7 @@ public class FitImporter {
|
||||||
persistStressSamples();
|
persistStressSamples();
|
||||||
break;
|
break;
|
||||||
case sleep:
|
case sleep:
|
||||||
|
persistEvents();
|
||||||
persistSleepStageSamples();
|
persistSleepStageSamples();
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
@ -237,23 +242,24 @@ public class FitImporter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void persistActivity() {
|
private void persistWorkout(final File file) {
|
||||||
if (session == null) {
|
if (session == null) {
|
||||||
LOG.error("Got activity from {}, but no session", fileId);
|
LOG.error("Got workout from {}, but no session", fileId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (sport == null) {
|
if (sport == null) {
|
||||||
LOG.error("Got activity from {}, but no sport", fileId);
|
LOG.error("Got workout from {}, but no sport", fileId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG.debug("Persisting activity for {}", fileId);
|
LOG.debug("Persisting workout for {}", fileId);
|
||||||
|
|
||||||
final BaseActivitySummary summary = new BaseActivitySummary();
|
final BaseActivitySummary summary = new BaseActivitySummary();
|
||||||
summary.setActivityKind(ActivityKind.TYPE_UNKNOWN);
|
summary.setActivityKind(ActivityKind.TYPE_UNKNOWN);
|
||||||
|
|
||||||
final JSONObject summaryData = new JSONObject();
|
final ActivitySummaryData summaryData = new ActivitySummaryData();
|
||||||
|
|
||||||
|
// TODO map all sports
|
||||||
if (sport.getSport() != null) {
|
if (sport.getSport() != null) {
|
||||||
switch (sport.getSport()) {
|
switch (sport.getSport()) {
|
||||||
case 2:
|
case 2:
|
||||||
|
@ -268,13 +274,13 @@ public class FitImporter {
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
LOG.warn("Unknown sub sport {}", sport.getSubSport());
|
LOG.warn("Unknown sub sport {}", sport.getSubSport());
|
||||||
addSummaryData(summaryData, "Fit Sub Sport", sport.getSubSport(), "");
|
summaryData.add("Fit Sub Sport", sport.getSubSport(), "");
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
LOG.warn("Unknown sport {}", sport.getSport());
|
LOG.warn("Unknown sport {}", sport.getSport());
|
||||||
addSummaryData(summaryData, "Fit Sport", sport.getSport(), "");
|
summaryData.add("Fit Sport", sport.getSport(), "");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -292,24 +298,36 @@ public class FitImporter {
|
||||||
summary.setEndTime(new Date(GarminTimeUtils.garminTimestampToJavaMillis(session.getStartTime().intValue() + session.getTotalElapsedTime().intValue() / 1000)));
|
summary.setEndTime(new Date(GarminTimeUtils.garminTimestampToJavaMillis(session.getStartTime().intValue() + session.getTotalElapsedTime().intValue() / 1000)));
|
||||||
|
|
||||||
if (session.getTotalTimerTime() != null) {
|
if (session.getTotalTimerTime() != null) {
|
||||||
addSummaryData(summaryData, ACTIVE_SECONDS, session.getTotalTimerTime() / 1000f, UNIT_SECONDS);
|
summaryData.add(ACTIVE_SECONDS, session.getTotalTimerTime() / 1000f, UNIT_SECONDS);
|
||||||
}
|
}
|
||||||
if (session.getTotalDistance() != null) {
|
if (session.getTotalDistance() != null) {
|
||||||
addSummaryData(summaryData, DISTANCE_METERS, session.getTotalDistance() / 100f, UNIT_METERS);
|
summaryData.add(DISTANCE_METERS, session.getTotalDistance() / 100f, UNIT_METERS);
|
||||||
}
|
}
|
||||||
if (session.getTotalCalories() != null) {
|
if (session.getTotalCalories() != null) {
|
||||||
addSummaryData(summaryData, CALORIES_BURNT, session.getTotalCalories(), UNIT_KCAL);
|
summaryData.add(CALORIES_BURNT, session.getTotalCalories(), UNIT_KCAL);
|
||||||
}
|
}
|
||||||
if (session.getTotalAscent() != null) {
|
if (session.getTotalAscent() != null) {
|
||||||
addSummaryData(summaryData, ASCENT_DISTANCE, session.getTotalAscent(), UNIT_METERS);
|
summaryData.add(ASCENT_DISTANCE, session.getTotalAscent(), UNIT_METERS);
|
||||||
}
|
}
|
||||||
if (session.getTotalDescent() != null) {
|
if (session.getTotalDescent() != null) {
|
||||||
addSummaryData(summaryData, DESCENT_DISTANCE, session.getTotalDescent(), UNIT_METERS);
|
summaryData.add(DESCENT_DISTANCE, session.getTotalDescent(), UNIT_METERS);
|
||||||
}
|
}
|
||||||
|
|
||||||
summary.setSummaryData(summaryData.toString());
|
//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) {
|
||||||
|
//}
|
||||||
|
|
||||||
// TODO fit path needs to reach here summary.setRawDetailsPath(null);
|
summary.setSummaryData(summaryData.toString());
|
||||||
|
if (file != null) {
|
||||||
|
summary.setRawDetailsPath(file.getAbsolutePath());
|
||||||
|
}
|
||||||
|
|
||||||
try (DBHandler dbHandler = GBApplication.acquireDB()) {
|
try (DBHandler dbHandler = GBApplication.acquireDB()) {
|
||||||
final DaoSession session = dbHandler.getDaoSession();
|
final DaoSession session = dbHandler.getDaoSession();
|
||||||
|
@ -321,32 +339,22 @@ public class FitImporter {
|
||||||
|
|
||||||
session.getBaseActivitySummaryDao().insertOrReplace(summary);
|
session.getBaseActivitySummaryDao().insertOrReplace(summary);
|
||||||
} catch (final Exception e) {
|
} catch (final Exception e) {
|
||||||
GB.toast(context, "Error saving activity", Toast.LENGTH_LONG, GB.ERROR, e);
|
GB.toast(context, "Error saving workout", Toast.LENGTH_LONG, GB.ERROR, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void addSummaryData(final JSONObject summaryData, final String key, final float value, final String unit) {
|
private void reset() {
|
||||||
if (value > 0) {
|
activitySamples.clear();
|
||||||
try {
|
stressSamples.clear();
|
||||||
final JSONObject innerData = new JSONObject();
|
spo2samples.clear();
|
||||||
innerData.put("value", value);
|
events.clear();
|
||||||
innerData.put("unit", unit);
|
sleepStageSamples.clear();
|
||||||
summaryData.put(key, innerData);
|
timesInZone.clear();
|
||||||
} catch (final JSONException ignore) {
|
activityPoints.clear();
|
||||||
}
|
unknownRecords.clear();
|
||||||
}
|
fileId = null;
|
||||||
}
|
session = null;
|
||||||
|
sport = null;
|
||||||
protected void addSummaryData(final JSONObject summaryData, final String key, final String value) {
|
|
||||||
if (key != null && !key.equals("") && value != null && !value.equals("")) {
|
|
||||||
try {
|
|
||||||
final JSONObject innerData = new JSONObject();
|
|
||||||
innerData.put("value", value);
|
|
||||||
innerData.put("unit", "string");
|
|
||||||
summaryData.put(key, innerData);
|
|
||||||
} catch (final JSONException ignore) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void persistActivitySamples() {
|
private void persistActivitySamples() {
|
||||||
|
@ -377,6 +385,32 @@ public class FitImporter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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() {
|
private void persistSleepStageSamples() {
|
||||||
if (sleepStageSamples.isEmpty()) {
|
if (sleepStageSamples.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
|
@ -384,9 +418,6 @@ public class FitImporter {
|
||||||
|
|
||||||
LOG.debug("Will persist {} sleep stage samples", sleepStageSamples.size());
|
LOG.debug("Will persist {} sleep stage samples", sleepStageSamples.size());
|
||||||
|
|
||||||
// FIXME do not assume that last sample == awake
|
|
||||||
sleepStageSamples.get(sleepStageSamples.size() - 1).setStage(FieldDefinitionSleepStage.SleepStage.AWAKE.getId());
|
|
||||||
|
|
||||||
try (DBHandler handler = GBApplication.acquireDB()) {
|
try (DBHandler handler = GBApplication.acquireDB()) {
|
||||||
final DaoSession session = handler.getDaoSession();
|
final DaoSession session = handler.getDaoSession();
|
||||||
|
|
||||||
|
|
|
@ -16,8 +16,12 @@ import java.util.ArrayList;
|
||||||
import java.util.Calendar;
|
import java.util.Calendar;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.GlobalFITMessage;
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.GlobalFITMessage;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordData;
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordData;
|
||||||
|
@ -97,9 +101,12 @@ public class FitCodeGen {
|
||||||
imports.add(RecordDefinition.class.getCanonicalName());
|
imports.add(RecordDefinition.class.getCanonicalName());
|
||||||
imports.add(RecordHeader.class.getCanonicalName());
|
imports.add(RecordHeader.class.getCanonicalName());
|
||||||
//imports.add(GBToStringBuilder.class.getCanonicalName());
|
//imports.add(GBToStringBuilder.class.getCanonicalName());
|
||||||
|
imports.addAll(getImports(outputFile));
|
||||||
|
|
||||||
Collections.sort(imports);
|
Collections.sort(imports);
|
||||||
|
|
||||||
|
final Set<String> uniqueImports =new LinkedHashSet<>(imports);
|
||||||
|
|
||||||
for (final GlobalFITMessage.FieldDefinitionPrimitive primitive : globalFITMessage.getFieldDefinitionPrimitives()) {
|
for (final GlobalFITMessage.FieldDefinitionPrimitive primitive : globalFITMessage.getFieldDefinitionPrimitives()) {
|
||||||
final Class<?> fieldType = getFieldType(primitive);
|
final Class<?> fieldType = getFieldType(primitive);
|
||||||
if (!Objects.requireNonNull(fieldType.getCanonicalName()).startsWith("java.lang")) {
|
if (!Objects.requireNonNull(fieldType.getCanonicalName()).startsWith("java.lang")) {
|
||||||
|
@ -119,7 +126,7 @@ public class FitCodeGen {
|
||||||
|
|
||||||
sb.append("\n");
|
sb.append("\n");
|
||||||
boolean anyImport = false;
|
boolean anyImport = false;
|
||||||
for (final String i : imports) {
|
for (final String i : uniqueImports) {
|
||||||
if (i.startsWith("androidx")) {
|
if (i.startsWith("androidx")) {
|
||||||
sb.append("import ").append(i).append(";\n");
|
sb.append("import ").append(i).append(";\n");
|
||||||
anyImport = true;
|
anyImport = true;
|
||||||
|
@ -130,7 +137,7 @@ public class FitCodeGen {
|
||||||
sb.append("\n");
|
sb.append("\n");
|
||||||
anyImport = false;
|
anyImport = false;
|
||||||
}
|
}
|
||||||
for (final String i : imports) {
|
for (final String i : uniqueImports) {
|
||||||
if (i.startsWith("nodomain.freeyourgadget")) {
|
if (i.startsWith("nodomain.freeyourgadget")) {
|
||||||
sb.append("import ").append(i).append(";\n");
|
sb.append("import ").append(i).append(";\n");
|
||||||
anyImport = true;
|
anyImport = true;
|
||||||
|
@ -141,7 +148,7 @@ public class FitCodeGen {
|
||||||
sb.append("\n");
|
sb.append("\n");
|
||||||
anyImport = false;
|
anyImport = false;
|
||||||
}
|
}
|
||||||
for (final String i : imports) {
|
for (final String i : uniqueImports) {
|
||||||
if (!i.startsWith("androidx") && !i.startsWith("nodomain.freeyourgadget")) {
|
if (!i.startsWith("androidx") && !i.startsWith("nodomain.freeyourgadget")) {
|
||||||
sb.append("import ").append(i).append(";\n");
|
sb.append("import ").append(i).append(";\n");
|
||||||
anyImport = true;
|
anyImport = true;
|
||||||
|
@ -294,4 +301,20 @@ public class FitCodeGen {
|
||||||
|
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<String> getImports(final File file) throws IOException {
|
||||||
|
if (file.exists()) {
|
||||||
|
final String fileContents = new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8);
|
||||||
|
final List<String> imports = new ArrayList<>();
|
||||||
|
|
||||||
|
final Matcher m = Pattern.compile("import (.+);")
|
||||||
|
.matcher(fileContents);
|
||||||
|
while (m.find()) {
|
||||||
|
imports.add(m.group(1));
|
||||||
|
}
|
||||||
|
return imports;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,8 +5,8 @@ import androidx.annotation.Nullable;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordData;
|
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.RecordDefinition;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordHeader;
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordHeader;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionGoalType.Type;
|
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionGoalSource.Source;
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionGoalSource.Source;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionGoalType.Type;
|
||||||
|
|
||||||
//
|
//
|
||||||
// WARNING: This class was auto-generated, please avoid modifying it directly.
|
// WARNING: This class was auto-generated, please avoid modifying it directly.
|
||||||
|
|
|
@ -2,6 +2,11 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
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.RecordData;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordDefinition;
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordDefinition;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordHeader;
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordHeader;
|
||||||
|
@ -64,4 +69,25 @@ public class FitRecord extends RecordData {
|
||||||
public Long getTimestamp() {
|
public Long getTimestamp() {
|
||||||
return (Long) getFieldByNumber(253);
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,11 +7,6 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordDef
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordHeader;
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordHeader;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionLanguage.Language;
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionLanguage.Language;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionMeasurementSystem.Type;
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionMeasurementSystem.Type;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionMeasurementSystem.Type;
|
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionMeasurementSystem.Type;
|
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionMeasurementSystem.Type;
|
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionMeasurementSystem.Type;
|
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionMeasurementSystem.Type;
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// WARNING: This class was auto-generated, please avoid modifying it directly.
|
// WARNING: This class was auto-generated, please avoid modifying it directly.
|
||||||
|
|
|
@ -22,14 +22,33 @@ import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
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> {
|
public class RangeMap<K extends Comparable<K>, V> {
|
||||||
private final List<Pair<K, V>> list = new ArrayList<>();
|
private final List<Pair<K, V>> list = new ArrayList<>();
|
||||||
private boolean isSorted = false;
|
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) {
|
public void put(final K key, final V value) {
|
||||||
list.add(Pair.create(key, value));
|
list.add(Pair.create(key, value));
|
||||||
|
@ -39,14 +58,12 @@ public class RangeMap<K extends Comparable<K>, V> {
|
||||||
@Nullable
|
@Nullable
|
||||||
public V get(final K key) {
|
public V get(final K key) {
|
||||||
if (!isSorted) {
|
if (!isSorted) {
|
||||||
Collections.sort(list, (a, b) -> {
|
Collections.sort(list, (a, b) -> comparator.compare(a.first, b.first));
|
||||||
return a.first.compareTo(b.first);
|
|
||||||
});
|
|
||||||
isSorted = true;
|
isSorted = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int i = list.size() - 1; i >= 0; i--) {
|
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;
|
return list.get(i).second;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -61,4 +78,10 @@ public class RangeMap<K extends Comparable<K>, V> {
|
||||||
public int size() {
|
public int size() {
|
||||||
return list.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