mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2024-08-31 19:45:27 +02:00
Garmin: Re-parse workout summary when opening details page
Ensures that new fields and other fixes get displayed properly.
This commit is contained in:
parent
ae1cc16a12
commit
f487bc7876
@ -207,7 +207,7 @@ public class ActivitySummariesAdapter extends AbstractActivityListingAdapter<Bas
|
||||
|
||||
final ActivitySummaryParser summaryParser = coordinator.getActivitySummaryParser(device);
|
||||
final ActivitySummaryJsonSummary activitySummaryJsonSummary = new ActivitySummaryJsonSummary(summaryParser, sportitem);
|
||||
JSONObject summarySubdata = activitySummaryJsonSummary.getSummaryData();
|
||||
JSONObject summarySubdata = activitySummaryJsonSummary.getSummaryData(false);
|
||||
|
||||
if (summarySubdata != null) {
|
||||
try {
|
||||
|
@ -38,10 +38,15 @@ public class CmfWorkoutSummaryParser implements ActivitySummaryParser {
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaseActivitySummary parseBinaryData(final BaseActivitySummary summary) {
|
||||
final ActivitySummaryData summaryData = new ActivitySummaryData();
|
||||
public BaseActivitySummary parseBinaryData(final BaseActivitySummary summary, final boolean forDetails) {
|
||||
final byte[] rawSummaryData = summary.getRawSummaryData();
|
||||
if (rawSummaryData == null) {
|
||||
return summary;
|
||||
}
|
||||
|
||||
final ByteBuffer buf = ByteBuffer.wrap(summary.getRawSummaryData()).order(ByteOrder.LITTLE_ENDIAN);
|
||||
final ByteBuffer buf = ByteBuffer.wrap(rawSummaryData).order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
final ActivitySummaryData summaryData = new ActivitySummaryData();
|
||||
|
||||
final int startTime = buf.getInt();
|
||||
final int duration = buf.getShort();
|
||||
|
@ -30,6 +30,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.GarminStressSampleDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.PendingFileDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.BodyEnergySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.HrvSummarySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.HrvValueSample;
|
||||
@ -99,6 +100,12 @@ public abstract class GarminCoordinator extends AbstractBLEDeviceCoordinator {
|
||||
return GarminSupport.class;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public ActivitySummaryParser getActivitySummaryParser(final GBDevice device) {
|
||||
return new GarminWorkoutParser();
|
||||
}
|
||||
|
||||
@Override
|
||||
public SampleProvider<? extends ActivitySample> getSampleProvider(final GBDevice device, DaoSession session) {
|
||||
return new GarminActivitySampleProvider(device, session);
|
||||
|
@ -0,0 +1,219 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.garmin;
|
||||
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.*;
|
||||
|
||||
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.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityPoint;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryData;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FitFile;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordData;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.enums.GarminSport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitPhysiologicalMetrics;
|
||||
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.FitSport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitTimeInZone;
|
||||
|
||||
public class GarminWorkoutParser implements ActivitySummaryParser {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(GarminWorkoutParser.class);
|
||||
|
||||
private final List<FitTimeInZone> timesInZone = new ArrayList<>();
|
||||
private final List<ActivityPoint> activityPoints = new ArrayList<>();
|
||||
private FitSession session = null;
|
||||
private FitSport sport = null;
|
||||
private FitPhysiologicalMetrics physiologicalMetrics = null;
|
||||
|
||||
@Override
|
||||
public BaseActivitySummary parseBinaryData(final BaseActivitySummary summary, final boolean forDetails) {
|
||||
if (!forDetails) {
|
||||
// Our parsing is too slow, especially without a RecyclerView
|
||||
return summary;
|
||||
}
|
||||
|
||||
final long nanoStart = System.nanoTime();
|
||||
|
||||
reset();
|
||||
|
||||
final String rawDetailsPath = summary.getRawDetailsPath();
|
||||
if (rawDetailsPath == null) {
|
||||
LOG.warn("No rawDetailsPath");
|
||||
return summary;
|
||||
}
|
||||
final File file = new File(rawDetailsPath);
|
||||
if (!file.isFile() || !file.canRead()) {
|
||||
LOG.warn("Unable to read {}", file);
|
||||
return summary;
|
||||
}
|
||||
|
||||
final FitFile fitFile;
|
||||
try {
|
||||
fitFile = FitFile.parseIncoming(file);
|
||||
} catch (final IOException e) {
|
||||
LOG.error("Failed to parse fit file", e);
|
||||
return summary;
|
||||
}
|
||||
|
||||
for (final RecordData record : fitFile.getRecords()) {
|
||||
handleRecord(record);
|
||||
}
|
||||
|
||||
updateSummary(summary);
|
||||
|
||||
final long nanoEnd = System.nanoTime();
|
||||
final long executionTime = (nanoEnd - nanoStart) / 1000000;
|
||||
LOG.trace("Updating summary took {}ms", executionTime);
|
||||
|
||||
return summary;
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
timesInZone.clear();
|
||||
activityPoints.clear();
|
||||
session = null;
|
||||
sport = null;
|
||||
physiologicalMetrics = null;
|
||||
}
|
||||
|
||||
public boolean handleRecord(final RecordData record) {
|
||||
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 FitPhysiologicalMetrics) {
|
||||
LOG.debug("Physiological Metrics: {}", record);
|
||||
physiologicalMetrics = (FitPhysiologicalMetrics) 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 {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void updateSummary(final BaseActivitySummary summary) {
|
||||
if (session == null) {
|
||||
LOG.error("Got workout, but no session");
|
||||
return;
|
||||
}
|
||||
|
||||
final ActivitySummaryData summaryData = new ActivitySummaryData();
|
||||
|
||||
final ActivityKind activityKind;
|
||||
if (sport != null) {
|
||||
summary.setName(sport.getName());
|
||||
activityKind = getActivityKind(sport.getSport(), sport.getSubSport());
|
||||
} else {
|
||||
activityKind = getActivityKind(session.getSport(), session.getSubSport());
|
||||
}
|
||||
summary.setActivityKind(activityKind.getCode());
|
||||
|
||||
if (session.getTotalElapsedTime() != null) {
|
||||
summary.setEndTime(new Date(summary.getStartTime().getTime() + session.getTotalElapsedTime().intValue()));
|
||||
}
|
||||
|
||||
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.getEstimatedSweatLoss() != null) {
|
||||
summaryData.add(ESTIMATED_SWEAT_LOSS, session.getEstimatedSweatLoss(), UNIT_ML);
|
||||
}
|
||||
if (session.getAverageHeartRate() != null) {
|
||||
summaryData.add(HR_AVG, session.getAverageHeartRate(), UNIT_BPM);
|
||||
}
|
||||
if (session.getMaxHeartRate() != null) {
|
||||
summaryData.add(HR_MAX, session.getMaxHeartRate(), UNIT_BPM);
|
||||
}
|
||||
if (session.getTotalAscent() != null) {
|
||||
summaryData.add(ASCENT_DISTANCE, session.getTotalAscent(), UNIT_METERS);
|
||||
}
|
||||
if (session.getTotalDescent() != null) {
|
||||
summaryData.add(DESCENT_DISTANCE, session.getTotalDescent(), UNIT_METERS);
|
||||
}
|
||||
|
||||
for (final FitTimeInZone fitTimeInZone : timesInZone) {
|
||||
// Find the first time in zone for the session (assumes single-session)
|
||||
if (fitTimeInZone.getReferenceMessage() != null && fitTimeInZone.getReferenceMessage() == 18) {
|
||||
final Double[] timeInZone = fitTimeInZone.getTimeInZone();
|
||||
if (timeInZone != null && timeInZone.length == 6) {
|
||||
summaryData.add(HR_ZONE_NA, timeInZone[0].floatValue(), UNIT_SECONDS);
|
||||
summaryData.add(HR_ZONE_WARM_UP, timeInZone[1].floatValue(), UNIT_SECONDS);
|
||||
summaryData.add(HR_ZONE_FAT_BURN, timeInZone[2].floatValue(), UNIT_SECONDS);
|
||||
summaryData.add(HR_ZONE_AEROBIC, timeInZone[3].floatValue(), UNIT_SECONDS);
|
||||
summaryData.add(HR_ZONE_ANAEROBIC, timeInZone[4].floatValue(), UNIT_SECONDS);
|
||||
summaryData.add(HR_ZONE_EXTREME, timeInZone[5].floatValue(), UNIT_SECONDS);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (physiologicalMetrics != null) {
|
||||
if (physiologicalMetrics.getAerobicEffect() != null) {
|
||||
summaryData.add(TRAINING_EFFECT_AEROBIC, physiologicalMetrics.getAerobicEffect(), UNIT_NONE);
|
||||
}
|
||||
if (physiologicalMetrics.getAnaerobicEffect() != null) {
|
||||
summaryData.add(TRAINING_EFFECT_ANAEROBIC, physiologicalMetrics.getAnaerobicEffect(), UNIT_NONE);
|
||||
}
|
||||
if (physiologicalMetrics.getMetMax() != null) {
|
||||
summaryData.add(MAXIMUM_OXYGEN_UPTAKE, physiologicalMetrics.getMetMax().floatValue() * 3.5f, UNIT_ML_KG_MIN);
|
||||
}
|
||||
if (physiologicalMetrics.getRecoveryTime() != null) {
|
||||
summaryData.add(RECOVERY_TIME, physiologicalMetrics.getRecoveryTime() * 60, UNIT_SECONDS);
|
||||
}
|
||||
if (physiologicalMetrics.getLactateThresholdHeartRate() != null) {
|
||||
summaryData.add(LACTATE_THRESHOLD_HR, physiologicalMetrics.getLactateThresholdHeartRate(), UNIT_BPM);
|
||||
}
|
||||
}
|
||||
|
||||
summary.setSummaryData(summaryData.toString());
|
||||
}
|
||||
|
||||
private static ActivityKind getActivityKind(final Integer sport, final Integer subsport) {
|
||||
final Optional<GarminSport> garminSport = GarminSport.fromCodes(sport, subsport);
|
||||
if (garminSport.isPresent()) {
|
||||
return garminSport.get().getActivityKind();
|
||||
} else {
|
||||
LOG.warn("Unknown garmin sport {}/{}", sport, subsport);
|
||||
|
||||
final Optional<GarminSport> optGarminSportFallback = GarminSport.fromCodes(sport, 0);
|
||||
if (!optGarminSportFallback.isEmpty()) {
|
||||
return optGarminSportFallback.get().getActivityKind();
|
||||
}
|
||||
}
|
||||
|
||||
return ActivityKind.UNKNOWN;
|
||||
}
|
||||
}
|
@ -40,7 +40,7 @@ public class HuamiActivitySummaryParser implements ActivitySummaryParser {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(HuamiActivityDetailsParser.class);
|
||||
protected ActivitySummaryData summaryData = new ActivitySummaryData();
|
||||
|
||||
public BaseActivitySummary parseBinaryData(BaseActivitySummary summary) {
|
||||
public BaseActivitySummary parseBinaryData(BaseActivitySummary summary, final boolean forDetails) {
|
||||
Date startTime = summary.getStartTime();
|
||||
if (startTime == null) {
|
||||
LOG.error("Due to a bug, we can only parse the summary when startTime is already set");
|
||||
@ -57,7 +57,11 @@ public class HuamiActivitySummaryParser implements ActivitySummaryParser {
|
||||
}
|
||||
|
||||
protected void parseBinaryData(BaseActivitySummary summary, Date startTime) {
|
||||
ByteBuffer buffer = ByteBuffer.wrap(summary.getRawSummaryData()).order(ByteOrder.LITTLE_ENDIAN);
|
||||
final byte[] rawSummaryData = summary.getRawSummaryData();
|
||||
if (rawSummaryData == null) {
|
||||
return;
|
||||
}
|
||||
final ByteBuffer buffer = ByteBuffer.wrap(rawSummaryData).order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
short version = buffer.getShort(); // version
|
||||
LOG.debug("Got sport summary version " + version + " total bytes=" + buffer.capacity());
|
||||
|
@ -45,6 +45,10 @@ public class ZeppOsActivitySummaryParser extends HuamiActivitySummaryParser {
|
||||
@Override
|
||||
protected void parseBinaryData(final BaseActivitySummary summary, final Date startTime) {
|
||||
final byte[] rawData = summary.getRawSummaryData();
|
||||
if (rawData == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final int version = (rawData[0] & 0xff) | ((rawData[1] & 0xff) << 8);
|
||||
if (version != 0x8000) {
|
||||
LOG.warn("Unexpected binary data version {}, parsing might fail", version);
|
||||
@ -85,7 +89,7 @@ public class ZeppOsActivitySummaryParser extends HuamiActivitySummaryParser {
|
||||
summary.setBaseLatitude(summaryProto.getLocation().getBaseLatitude());
|
||||
summary.setBaseAltitude(summaryProto.getLocation().getBaseAltitude() / 2);
|
||||
// TODO: Min/Max Latitude/Longitude
|
||||
summaryData.add(ALTITUDE_BASE, summaryProto.getLocation().getBaseAltitude() / 2, UNIT_METERS);
|
||||
summaryData.add(ALTITUDE_BASE, summaryProto.getLocation().getBaseAltitude() / 2f, UNIT_METERS);
|
||||
}
|
||||
|
||||
if (summaryProto.hasHeartRate()) {
|
||||
@ -136,12 +140,12 @@ public class ZeppOsActivitySummaryParser extends HuamiActivitySummaryParser {
|
||||
}
|
||||
|
||||
if (summaryProto.hasAltitude()) {
|
||||
summaryData.add(ALTITUDE_MAX, summaryProto.getAltitude().getMaxAltitude() / 200, UNIT_METERS);
|
||||
summaryData.add(ALTITUDE_MIN, summaryProto.getAltitude().getMinAltitude() / 200, UNIT_METERS);
|
||||
summaryData.add(ALTITUDE_AVG, summaryProto.getAltitude().getAvgAltitude() / 200, UNIT_METERS);
|
||||
summaryData.add(ALTITUDE_MAX, summaryProto.getAltitude().getMaxAltitude() / 200f, UNIT_METERS);
|
||||
summaryData.add(ALTITUDE_MIN, summaryProto.getAltitude().getMinAltitude() / 200f, UNIT_METERS);
|
||||
summaryData.add(ALTITUDE_AVG, summaryProto.getAltitude().getAvgAltitude() / 200f, UNIT_METERS);
|
||||
// TODO totalClimbing
|
||||
summaryData.add(ELEVATION_GAIN, summaryProto.getAltitude().getElevationGain() / 100, UNIT_METERS);
|
||||
summaryData.add(ELEVATION_LOSS, summaryProto.getAltitude().getElevationLoss() / 100, UNIT_METERS);
|
||||
summaryData.add(ELEVATION_GAIN, summaryProto.getAltitude().getElevationGain() / 100f, UNIT_METERS);
|
||||
summaryData.add(ELEVATION_LOSS, summaryProto.getAltitude().getElevationLoss() / 100f, UNIT_METERS);
|
||||
}
|
||||
|
||||
if (summaryProto.hasElevation()) {
|
||||
|
@ -21,7 +21,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
|
||||
|
||||
public class TestActivitySummaryParser implements ActivitySummaryParser {
|
||||
@Override
|
||||
public BaseActivitySummary parseBinaryData(final BaseActivitySummary summary) {
|
||||
public BaseActivitySummary parseBinaryData(final BaseActivitySummary summary, final boolean forDetails) {
|
||||
return summary;
|
||||
}
|
||||
}
|
||||
|
@ -26,7 +26,6 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
@ -47,8 +46,8 @@ public class ActivitySummaryJsonSummary {
|
||||
this.baseActivitySummary=baseActivitySummary;
|
||||
}
|
||||
|
||||
private JSONObject setSummaryData(BaseActivitySummary item){
|
||||
String summary = getCorrectSummary(item);
|
||||
private JSONObject setSummaryData(BaseActivitySummary item, final boolean forDetails){
|
||||
String summary = getCorrectSummary(item, forDetails);
|
||||
JSONObject jsonSummary = getJSONSummary(summary);
|
||||
if (jsonSummary != null) {
|
||||
//add additionally computed values here
|
||||
@ -84,16 +83,16 @@ public class ActivitySummaryJsonSummary {
|
||||
return jsonSummary;
|
||||
}
|
||||
|
||||
public JSONObject getSummaryData(){
|
||||
public JSONObject getSummaryData(final boolean forDetails){
|
||||
//returns json with summaryData
|
||||
if (summaryData==null) summaryData=setSummaryData(baseActivitySummary);
|
||||
if (summaryData==null) summaryData=setSummaryData(baseActivitySummary, forDetails);
|
||||
return summaryData;
|
||||
}
|
||||
|
||||
private String getCorrectSummary(BaseActivitySummary item){
|
||||
if (item.getRawSummaryData() != null) {
|
||||
private String getCorrectSummary(BaseActivitySummary item, final boolean forDetails){
|
||||
if (item.getRawSummaryData() != null || item.getRawDetailsPath() != null) {
|
||||
try {
|
||||
item = summaryParser.parseBinaryData(item);
|
||||
item = summaryParser.parseBinaryData(item, forDetails);
|
||||
} catch (final Exception e) {
|
||||
LOG.error("Failed to re-parse corrected summary", e);
|
||||
}
|
||||
@ -114,7 +113,7 @@ public class ActivitySummaryJsonSummary {
|
||||
|
||||
public JSONObject getSummaryGroupedList() {
|
||||
//returns list grouped by activity groups as per createActivitySummaryGroups
|
||||
if (summaryData==null) summaryData=setSummaryData(baseActivitySummary);
|
||||
if (summaryData==null) summaryData=setSummaryData(baseActivitySummary, true);
|
||||
if (summaryGroupedList==null) summaryGroupedList=setSummaryGroupedList(summaryData);
|
||||
return summaryGroupedList;
|
||||
}
|
||||
|
@ -19,5 +19,15 @@ package nodomain.freeyourgadget.gadgetbridge.model;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
|
||||
|
||||
public interface ActivitySummaryParser {
|
||||
BaseActivitySummary parseBinaryData(BaseActivitySummary summary);
|
||||
/**
|
||||
* Re-parse an existing {@link BaseActivitySummary}, updating it from the existing binary data.
|
||||
*
|
||||
* @param summary the existing {@link BaseActivitySummary}. It's not guaranteed that it
|
||||
* contains any raw binary data.
|
||||
* @param forDetails whether the parsing is for the details page. If this is false, the parser
|
||||
* should avoid slow operations such as reading and parsing raw files from
|
||||
* storage.
|
||||
* @return the update {@link BaseActivitySummary}
|
||||
*/
|
||||
BaseActivitySummary parseBinaryData(BaseActivitySummary summary, final boolean forDetails);
|
||||
}
|
||||
|
@ -408,7 +408,7 @@ public class CmfActivitySync {
|
||||
summary.setActivityKind(ActivityKind.UNKNOWN.getCode());
|
||||
|
||||
try {
|
||||
summary = summaryParser.parseBinaryData(summary);
|
||||
summary = summaryParser.parseBinaryData(summary, true);
|
||||
} catch (final Exception e) {
|
||||
LOG.error("Failed to parse workout summary", e);
|
||||
GB.toast(getContext(), "Failed to parse workout summary", Toast.LENGTH_LONG, GB.ERROR, e);
|
||||
|
@ -1,34 +1,5 @@
|
||||
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.ESTIMATED_SWEAT_LOSS;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.HR_AVG;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.HR_MAX;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.HR_ZONE_AEROBIC;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.HR_ZONE_ANAEROBIC;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.HR_ZONE_EXTREME;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.HR_ZONE_FAT_BURN;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.HR_ZONE_NA;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.HR_ZONE_WARM_UP;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.LACTATE_THRESHOLD_HR;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.MAXIMUM_OXYGEN_UPTAKE;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.RECOVERY_TIME;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.TRAINING_EFFECT_AEROBIC;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.TRAINING_EFFECT_ANAEROBIC;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_BPM;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_HOURS;
|
||||
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_ML;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_ML_KG_MIN;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_NONE;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_SECONDS;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.WORKOUT_LOAD;
|
||||
|
||||
import android.content.Context;
|
||||
import android.widget.Toast;
|
||||
|
||||
@ -43,7 +14,6 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.SortedMap;
|
||||
import java.util.TreeMap;
|
||||
|
||||
@ -59,6 +29,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminHrvValueSampleP
|
||||
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.devices.garmin.GarminWorkoutParser;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummaryDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
@ -74,10 +45,8 @@ 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.ActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryData;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.enums.GarminSport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.FileType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionHrvStatus;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionSleepStage;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitEvent;
|
||||
@ -109,25 +78,29 @@ public class FitImporter {
|
||||
private final List<GarminSleepStageSample> sleepStageSamples = new ArrayList<>();
|
||||
private final List<GarminHrvSummarySample> hrvSummarySamples = new ArrayList<>();
|
||||
private final List<GarminHrvValueSample> hrvValueSamples = 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;
|
||||
private FitPhysiologicalMetrics physiologicalMetrics = null;
|
||||
|
||||
private final GarminWorkoutParser workoutParser = new GarminWorkoutParser();
|
||||
|
||||
public FitImporter(final Context context, final GBDevice gbDevice) {
|
||||
this.context = context;
|
||||
this.gbDevice = gbDevice;
|
||||
}
|
||||
|
||||
/** @noinspection StatementWithEmptyBody*/
|
||||
public void importFile(final File file) throws IOException {
|
||||
reset();
|
||||
|
||||
final FitFile fitFile = FitFile.parseIncoming(file);
|
||||
|
||||
for (final RecordData record : fitFile.getRecords()) {
|
||||
if (fileId != null && fileId.getType() == FileType.FILETYPE.ACTIVITY) {
|
||||
if (workoutParser.handleRecord(record)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
final Long ts = record.getComputedTimestamp();
|
||||
|
||||
if (record instanceof FitFileId) {
|
||||
@ -203,29 +176,15 @@ public class FitImporter {
|
||||
}
|
||||
events.add(sample);
|
||||
} else if (record instanceof FitRecord) {
|
||||
activityPoints.add(((FitRecord) record).toActivityPoint());
|
||||
// handled in workout parser
|
||||
} 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;
|
||||
}
|
||||
// handled in workout parser
|
||||
} else if (record instanceof FitPhysiologicalMetrics) {
|
||||
LOG.debug("Physiological Metrics: {}", record);
|
||||
physiologicalMetrics = (FitPhysiologicalMetrics) record;
|
||||
// handled in workout parser
|
||||
} 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;
|
||||
}
|
||||
// handled in workout parser
|
||||
} else if (record instanceof FitTimeInZone) {
|
||||
LOG.trace("Time in zone: {}", record);
|
||||
timesInZone.add((FitTimeInZone) record);
|
||||
// handled in workout parser
|
||||
} else if (record instanceof FitHrvSummary) {
|
||||
final FitHrvSummary hrvSummary = (FitHrvSummary) record;
|
||||
LOG.trace("HRV summary at {}: {}", ts, record);
|
||||
@ -315,11 +274,6 @@ public class FitImporter {
|
||||
}
|
||||
|
||||
private void persistWorkout(final File file) {
|
||||
if (session == null) {
|
||||
LOG.error("Got workout from {}, but no session", fileId);
|
||||
return;
|
||||
}
|
||||
|
||||
LOG.debug("Persisting workout for {}", fileId);
|
||||
|
||||
final BaseActivitySummary summary;
|
||||
@ -335,87 +289,9 @@ public class FitImporter {
|
||||
return;
|
||||
}
|
||||
|
||||
final ActivitySummaryData summaryData = new ActivitySummaryData();
|
||||
workoutParser.updateSummary(summary);
|
||||
|
||||
final ActivityKind activityKind;
|
||||
if (sport != null) {
|
||||
summary.setName(sport.getName());
|
||||
activityKind = getActivityKind(sport.getSport(), sport.getSubSport());
|
||||
} else {
|
||||
activityKind = getActivityKind(session.getSport(), session.getSubSport());
|
||||
}
|
||||
summary.setActivityKind(activityKind.getCode());
|
||||
|
||||
if (session.getTotalElapsedTime() == null) {
|
||||
LOG.error("No elapsed time for {}", fileId);
|
||||
return;
|
||||
}
|
||||
summary.setEndTime(new Date(summary.getStartTime().getTime() + session.getTotalElapsedTime().intValue()));
|
||||
|
||||
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.getEstimatedSweatLoss() != null) {
|
||||
summaryData.add(ESTIMATED_SWEAT_LOSS, session.getEstimatedSweatLoss(), UNIT_ML);
|
||||
}
|
||||
if (session.getAverageHeartRate() != null) {
|
||||
summaryData.add(HR_AVG, session.getAverageHeartRate(), UNIT_BPM);
|
||||
}
|
||||
if (session.getMaxHeartRate() != null) {
|
||||
summaryData.add(HR_MAX, session.getMaxHeartRate(), UNIT_BPM);
|
||||
}
|
||||
if (session.getTotalAscent() != null) {
|
||||
summaryData.add(ASCENT_DISTANCE, session.getTotalAscent(), UNIT_METERS);
|
||||
}
|
||||
if (session.getTotalDescent() != null) {
|
||||
summaryData.add(DESCENT_DISTANCE, session.getTotalDescent(), UNIT_METERS);
|
||||
}
|
||||
|
||||
for (final FitTimeInZone fitTimeInZone : timesInZone) {
|
||||
// Find the firt time in zone for the session (assumes single-session)
|
||||
if (fitTimeInZone.getReferenceMessage() != null && fitTimeInZone.getReferenceMessage() == 18) {
|
||||
final Double[] timeInZone = fitTimeInZone.getTimeInZone();
|
||||
if (timeInZone != null && timeInZone.length == 6) {
|
||||
summaryData.add(HR_ZONE_NA, timeInZone[0].floatValue(), UNIT_SECONDS);
|
||||
summaryData.add(HR_ZONE_WARM_UP, timeInZone[1].floatValue(), UNIT_SECONDS);
|
||||
summaryData.add(HR_ZONE_FAT_BURN, timeInZone[2].floatValue(), UNIT_SECONDS);
|
||||
summaryData.add(HR_ZONE_AEROBIC, timeInZone[3].floatValue(), UNIT_SECONDS);
|
||||
summaryData.add(HR_ZONE_ANAEROBIC, timeInZone[4].floatValue(), UNIT_SECONDS);
|
||||
summaryData.add(HR_ZONE_EXTREME, timeInZone[5].floatValue(), UNIT_SECONDS);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (physiologicalMetrics != null) {
|
||||
// TODO lactate_threshold_heart_rate
|
||||
if (physiologicalMetrics.getAerobicEffect() != null) {
|
||||
summaryData.add(TRAINING_EFFECT_AEROBIC, physiologicalMetrics.getAerobicEffect(), UNIT_NONE);
|
||||
}
|
||||
if (physiologicalMetrics.getAnaerobicEffect() != null) {
|
||||
summaryData.add(TRAINING_EFFECT_ANAEROBIC, physiologicalMetrics.getAnaerobicEffect(), UNIT_NONE);
|
||||
}
|
||||
if (physiologicalMetrics.getMetMax() != null) {
|
||||
summaryData.add(MAXIMUM_OXYGEN_UPTAKE, physiologicalMetrics.getMetMax().floatValue() * 3.5f, UNIT_ML_KG_MIN);
|
||||
}
|
||||
if (physiologicalMetrics.getRecoveryTime() != null) {
|
||||
summaryData.add(RECOVERY_TIME, physiologicalMetrics.getRecoveryTime() / 60f, UNIT_HOURS);
|
||||
}
|
||||
if (physiologicalMetrics.getLactateThresholdHeartRate() != null) {
|
||||
summaryData.add(LACTATE_THRESHOLD_HR, physiologicalMetrics.getLactateThresholdHeartRate(), UNIT_BPM);
|
||||
}
|
||||
}
|
||||
|
||||
summary.setSummaryData(summaryData.toString());
|
||||
if (file != null) {
|
||||
summary.setRawDetailsPath(file.getAbsolutePath());
|
||||
}
|
||||
summary.setRawDetailsPath(file.getAbsolutePath());
|
||||
|
||||
try (DBHandler dbHandler = GBApplication.acquireDB()) {
|
||||
final DaoSession session = dbHandler.getDaoSession();
|
||||
@ -431,22 +307,6 @@ public class FitImporter {
|
||||
}
|
||||
}
|
||||
|
||||
private ActivityKind getActivityKind(final Integer sport, final Integer subsport) {
|
||||
final Optional<GarminSport> garminSport = GarminSport.fromCodes(sport, subsport);
|
||||
if (garminSport.isPresent()) {
|
||||
return garminSport.get().getActivityKind();
|
||||
} else {
|
||||
LOG.warn("Unknown garmin sport {}/{}", sport, subsport);
|
||||
|
||||
final Optional<GarminSport> optGarminSportFallback = GarminSport.fromCodes(sport, 0);
|
||||
if (!optGarminSportFallback.isEmpty()) {
|
||||
return optGarminSportFallback.get().getActivityKind();
|
||||
}
|
||||
}
|
||||
|
||||
return ActivityKind.UNKNOWN;
|
||||
}
|
||||
|
||||
protected static BaseActivitySummary findOrCreateBaseActivitySummary(final DaoSession session,
|
||||
final Device device,
|
||||
final User user,
|
||||
@ -484,13 +344,9 @@ public class FitImporter {
|
||||
sleepStageSamples.clear();
|
||||
hrvSummarySamples.clear();
|
||||
hrvValueSamples.clear();
|
||||
timesInZone.clear();
|
||||
activityPoints.clear();
|
||||
unknownRecords.clear();
|
||||
fileId = null;
|
||||
session = null;
|
||||
sport = null;
|
||||
physiologicalMetrics = null;
|
||||
workoutParser.reset();
|
||||
}
|
||||
|
||||
private void persistActivitySamples() {
|
||||
|
@ -81,7 +81,7 @@ public class FetchSportsSummaryOperation extends AbstractFetchOperation {
|
||||
summary.setStartTime(getLastStartTimestamp().getTime()); // due to a bug this has to be set
|
||||
summary.setRawSummaryData(buffer.toByteArray());
|
||||
try {
|
||||
summary = summaryParser.parseBinaryData(summary);
|
||||
summary = summaryParser.parseBinaryData(summary, true);
|
||||
} catch (final Exception e) {
|
||||
GB.toast(getContext(), "Failed to parse activity summary", Toast.LENGTH_LONG, GB.ERROR, e);
|
||||
return false;
|
||||
|
@ -28,7 +28,6 @@ import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.HR_ZONE_EXTREME;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.HR_ZONE_FAT_BURN;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.HR_ZONE_WARM_UP;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.LANE_LENGTH;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.LAPS;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.LAP_PACE_AVERAGE;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.PACE_AVG_SECONDS_KM;
|
||||
@ -42,19 +41,16 @@ import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.STROKE_RATE_AVG;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.SWIM_STYLE;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.SWOLF_AVG;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.SWOLF_INDEX;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.TIME_END;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.TIME_START;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.TRAINING_EFFECT_AEROBIC;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.TRAINING_EFFECT_ANAEROBIC;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_BPM;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_CM;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_HOURS;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_KCAL;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_KMPH;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_LAPS;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_METERS;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_METERS_PER_SECOND;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_NONE;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_SECONDS;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_SECONDS_PER_KM;
|
||||
@ -71,13 +67,11 @@ import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.Arrays;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||
@ -108,7 +102,7 @@ public class WorkoutSummaryParser extends XiaomiActivityParser implements Activi
|
||||
summary.setRawSummaryData(bytes);
|
||||
|
||||
try {
|
||||
summary = parseBinaryData(summary);
|
||||
summary = parseBinaryData(summary, true);
|
||||
} catch (final Exception e) {
|
||||
LOG.error("Failed to parse workout summary", e);
|
||||
GB.toast(support.getContext(), "Failed to parse workout summary", Toast.LENGTH_LONG, GB.ERROR, e);
|
||||
@ -144,8 +138,11 @@ public class WorkoutSummaryParser extends XiaomiActivityParser implements Activi
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaseActivitySummary parseBinaryData(final BaseActivitySummary summary) {
|
||||
public BaseActivitySummary parseBinaryData(final BaseActivitySummary summary, final boolean forDetails) {
|
||||
final byte[] data = summary.getRawSummaryData();
|
||||
if (data == null) {
|
||||
return summary;
|
||||
}
|
||||
|
||||
final int arrCrc32 = CheckSums.getCRC32(data, 0, data.length - 4);
|
||||
final int expectedCrc32 = BLETypeConversions.toUint32(data, data.length - 4);
|
||||
|
Loading…
Reference in New Issue
Block a user