1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-09-01 03:55:47 +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:
José Rebelo 2024-08-26 10:46:43 +01:00
parent ae1cc16a12
commit f487bc7876
13 changed files with 297 additions and 196 deletions

View File

@ -207,7 +207,7 @@ public class ActivitySummariesAdapter extends AbstractActivityListingAdapter<Bas
final ActivitySummaryParser summaryParser = coordinator.getActivitySummaryParser(device); final ActivitySummaryParser summaryParser = coordinator.getActivitySummaryParser(device);
final ActivitySummaryJsonSummary activitySummaryJsonSummary = new ActivitySummaryJsonSummary(summaryParser, sportitem); final ActivitySummaryJsonSummary activitySummaryJsonSummary = new ActivitySummaryJsonSummary(summaryParser, sportitem);
JSONObject summarySubdata = activitySummaryJsonSummary.getSummaryData(); JSONObject summarySubdata = activitySummaryJsonSummary.getSummaryData(false);
if (summarySubdata != null) { if (summarySubdata != null) {
try { try {

View File

@ -38,10 +38,15 @@ public class CmfWorkoutSummaryParser implements ActivitySummaryParser {
} }
@Override @Override
public BaseActivitySummary parseBinaryData(final BaseActivitySummary summary) { public BaseActivitySummary parseBinaryData(final BaseActivitySummary summary, final boolean forDetails) {
final ActivitySummaryData summaryData = new ActivitySummaryData(); 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 startTime = buf.getInt();
final int duration = buf.getShort(); final int duration = buf.getShort();

View File

@ -30,6 +30,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.GarminStressSampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.PendingFileDao; import nodomain.freeyourgadget.gadgetbridge.entities.PendingFileDao;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
import nodomain.freeyourgadget.gadgetbridge.model.BodyEnergySample; import nodomain.freeyourgadget.gadgetbridge.model.BodyEnergySample;
import nodomain.freeyourgadget.gadgetbridge.model.HrvSummarySample; import nodomain.freeyourgadget.gadgetbridge.model.HrvSummarySample;
import nodomain.freeyourgadget.gadgetbridge.model.HrvValueSample; import nodomain.freeyourgadget.gadgetbridge.model.HrvValueSample;
@ -99,6 +100,12 @@ public abstract class GarminCoordinator extends AbstractBLEDeviceCoordinator {
return GarminSupport.class; return GarminSupport.class;
} }
@Nullable
@Override
public ActivitySummaryParser getActivitySummaryParser(final GBDevice device) {
return new GarminWorkoutParser();
}
@Override @Override
public SampleProvider<? extends ActivitySample> getSampleProvider(final GBDevice device, DaoSession session) { public SampleProvider<? extends ActivitySample> getSampleProvider(final GBDevice device, DaoSession session) {
return new GarminActivitySampleProvider(device, session); return new GarminActivitySampleProvider(device, session);

View File

@ -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;
}
}

View File

@ -40,7 +40,7 @@ public class HuamiActivitySummaryParser implements ActivitySummaryParser {
private static final Logger LOG = LoggerFactory.getLogger(HuamiActivityDetailsParser.class); private static final Logger LOG = LoggerFactory.getLogger(HuamiActivityDetailsParser.class);
protected ActivitySummaryData summaryData = new ActivitySummaryData(); protected ActivitySummaryData summaryData = new ActivitySummaryData();
public BaseActivitySummary parseBinaryData(BaseActivitySummary summary) { public BaseActivitySummary parseBinaryData(BaseActivitySummary summary, final boolean forDetails) {
Date startTime = summary.getStartTime(); Date startTime = summary.getStartTime();
if (startTime == null) { if (startTime == null) {
LOG.error("Due to a bug, we can only parse the summary when startTime is already set"); 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) { 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 short version = buffer.getShort(); // version
LOG.debug("Got sport summary version " + version + " total bytes=" + buffer.capacity()); LOG.debug("Got sport summary version " + version + " total bytes=" + buffer.capacity());

View File

@ -45,6 +45,10 @@ public class ZeppOsActivitySummaryParser extends HuamiActivitySummaryParser {
@Override @Override
protected void parseBinaryData(final BaseActivitySummary summary, final Date startTime) { protected void parseBinaryData(final BaseActivitySummary summary, final Date startTime) {
final byte[] rawData = summary.getRawSummaryData(); final byte[] rawData = summary.getRawSummaryData();
if (rawData == null) {
return;
}
final int version = (rawData[0] & 0xff) | ((rawData[1] & 0xff) << 8); final int version = (rawData[0] & 0xff) | ((rawData[1] & 0xff) << 8);
if (version != 0x8000) { if (version != 0x8000) {
LOG.warn("Unexpected binary data version {}, parsing might fail", version); 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.setBaseLatitude(summaryProto.getLocation().getBaseLatitude());
summary.setBaseAltitude(summaryProto.getLocation().getBaseAltitude() / 2); summary.setBaseAltitude(summaryProto.getLocation().getBaseAltitude() / 2);
// TODO: Min/Max Latitude/Longitude // 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()) { if (summaryProto.hasHeartRate()) {
@ -136,12 +140,12 @@ public class ZeppOsActivitySummaryParser extends HuamiActivitySummaryParser {
} }
if (summaryProto.hasAltitude()) { if (summaryProto.hasAltitude()) {
summaryData.add(ALTITUDE_MAX, summaryProto.getAltitude().getMaxAltitude() / 200, UNIT_METERS); summaryData.add(ALTITUDE_MAX, summaryProto.getAltitude().getMaxAltitude() / 200f, UNIT_METERS);
summaryData.add(ALTITUDE_MIN, summaryProto.getAltitude().getMinAltitude() / 200, UNIT_METERS); summaryData.add(ALTITUDE_MIN, summaryProto.getAltitude().getMinAltitude() / 200f, UNIT_METERS);
summaryData.add(ALTITUDE_AVG, summaryProto.getAltitude().getAvgAltitude() / 200, UNIT_METERS); summaryData.add(ALTITUDE_AVG, summaryProto.getAltitude().getAvgAltitude() / 200f, UNIT_METERS);
// TODO totalClimbing // TODO totalClimbing
summaryData.add(ELEVATION_GAIN, summaryProto.getAltitude().getElevationGain() / 100, UNIT_METERS); summaryData.add(ELEVATION_GAIN, summaryProto.getAltitude().getElevationGain() / 100f, UNIT_METERS);
summaryData.add(ELEVATION_LOSS, summaryProto.getAltitude().getElevationLoss() / 100, UNIT_METERS); summaryData.add(ELEVATION_LOSS, summaryProto.getAltitude().getElevationLoss() / 100f, UNIT_METERS);
} }
if (summaryProto.hasElevation()) { if (summaryProto.hasElevation()) {

View File

@ -21,7 +21,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
public class TestActivitySummaryParser implements ActivitySummaryParser { public class TestActivitySummaryParser implements ActivitySummaryParser {
@Override @Override
public BaseActivitySummary parseBinaryData(final BaseActivitySummary summary) { public BaseActivitySummary parseBinaryData(final BaseActivitySummary summary, final boolean forDetails) {
return summary; return summary;
} }
} }

View File

@ -26,7 +26,6 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator; import java.util.Iterator;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
@ -47,8 +46,8 @@ public class ActivitySummaryJsonSummary {
this.baseActivitySummary=baseActivitySummary; this.baseActivitySummary=baseActivitySummary;
} }
private JSONObject setSummaryData(BaseActivitySummary item){ private JSONObject setSummaryData(BaseActivitySummary item, final boolean forDetails){
String summary = getCorrectSummary(item); String summary = getCorrectSummary(item, forDetails);
JSONObject jsonSummary = getJSONSummary(summary); JSONObject jsonSummary = getJSONSummary(summary);
if (jsonSummary != null) { if (jsonSummary != null) {
//add additionally computed values here //add additionally computed values here
@ -84,16 +83,16 @@ public class ActivitySummaryJsonSummary {
return jsonSummary; return jsonSummary;
} }
public JSONObject getSummaryData(){ public JSONObject getSummaryData(final boolean forDetails){
//returns json with summaryData //returns json with summaryData
if (summaryData==null) summaryData=setSummaryData(baseActivitySummary); if (summaryData==null) summaryData=setSummaryData(baseActivitySummary, forDetails);
return summaryData; return summaryData;
} }
private String getCorrectSummary(BaseActivitySummary item){ private String getCorrectSummary(BaseActivitySummary item, final boolean forDetails){
if (item.getRawSummaryData() != null) { if (item.getRawSummaryData() != null || item.getRawDetailsPath() != null) {
try { try {
item = summaryParser.parseBinaryData(item); item = summaryParser.parseBinaryData(item, forDetails);
} catch (final Exception e) { } catch (final Exception e) {
LOG.error("Failed to re-parse corrected summary", e); LOG.error("Failed to re-parse corrected summary", e);
} }
@ -114,7 +113,7 @@ public class ActivitySummaryJsonSummary {
public JSONObject getSummaryGroupedList() { public JSONObject getSummaryGroupedList() {
//returns list grouped by activity groups as per createActivitySummaryGroups //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); if (summaryGroupedList==null) summaryGroupedList=setSummaryGroupedList(summaryData);
return summaryGroupedList; return summaryGroupedList;
} }

View File

@ -19,5 +19,15 @@ package nodomain.freeyourgadget.gadgetbridge.model;
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary; import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
public interface ActivitySummaryParser { 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);
} }

View File

@ -408,7 +408,7 @@ public class CmfActivitySync {
summary.setActivityKind(ActivityKind.UNKNOWN.getCode()); summary.setActivityKind(ActivityKind.UNKNOWN.getCode());
try { try {
summary = summaryParser.parseBinaryData(summary); summary = summaryParser.parseBinaryData(summary, true);
} catch (final Exception e) { } catch (final Exception e) {
LOG.error("Failed to parse workout summary", e); LOG.error("Failed to parse workout summary", e);
GB.toast(getContext(), "Failed to parse workout summary", Toast.LENGTH_LONG, GB.ERROR, e); GB.toast(getContext(), "Failed to parse workout summary", Toast.LENGTH_LONG, GB.ERROR, e);

View File

@ -1,34 +1,5 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit; 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.content.Context;
import android.widget.Toast; import android.widget.Toast;
@ -43,7 +14,6 @@ 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.Optional;
import java.util.SortedMap; import java.util.SortedMap;
import java.util.TreeMap; 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.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;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminWorkoutParser;
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary; import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummaryDao; import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummaryDao;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; 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.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.ActivitySample; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryData; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.FileType;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.enums.GarminSport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionHrvStatus; 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.fieldDefinitions.FieldDefinitionSleepStage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitEvent; 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<GarminSleepStageSample> sleepStageSamples = new ArrayList<>();
private final List<GarminHrvSummarySample> hrvSummarySamples = new ArrayList<>(); private final List<GarminHrvSummarySample> hrvSummarySamples = new ArrayList<>();
private final List<GarminHrvValueSample> hrvValueSamples = 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 final Map<Integer, Integer> unknownRecords = new HashMap<>();
private FitFileId fileId = null; private FitFileId fileId = null;
private FitSession session = null;
private FitSport sport = null; private final GarminWorkoutParser workoutParser = new GarminWorkoutParser();
private FitPhysiologicalMetrics physiologicalMetrics = null;
public FitImporter(final Context context, final GBDevice gbDevice) { public FitImporter(final Context context, final GBDevice gbDevice) {
this.context = context; this.context = context;
this.gbDevice = gbDevice; this.gbDevice = gbDevice;
} }
/** @noinspection StatementWithEmptyBody*/
public void importFile(final File file) throws IOException { public void importFile(final File file) throws IOException {
reset(); reset();
final FitFile fitFile = FitFile.parseIncoming(file); final FitFile fitFile = FitFile.parseIncoming(file);
for (final RecordData record : fitFile.getRecords()) { for (final RecordData record : fitFile.getRecords()) {
if (fileId != null && fileId.getType() == FileType.FILETYPE.ACTIVITY) {
if (workoutParser.handleRecord(record)) {
continue;
}
}
final Long ts = record.getComputedTimestamp(); final Long ts = record.getComputedTimestamp();
if (record instanceof FitFileId) { if (record instanceof FitFileId) {
@ -203,29 +176,15 @@ public class FitImporter {
} }
events.add(sample); events.add(sample);
} else if (record instanceof FitRecord) { } else if (record instanceof FitRecord) {
activityPoints.add(((FitRecord) record).toActivityPoint()); // handled in workout parser
} else if (record instanceof FitSession) { } else if (record instanceof FitSession) {
LOG.debug("Session: {}", record); // handled in workout parser
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) { } else if (record instanceof FitPhysiologicalMetrics) {
LOG.debug("Physiological Metrics: {}", record); // handled in workout parser
physiologicalMetrics = (FitPhysiologicalMetrics) record;
} else if (record instanceof FitSport) { } else if (record instanceof FitSport) {
LOG.debug("Sport: {}", record); // handled in workout parser
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) { } else if (record instanceof FitTimeInZone) {
LOG.trace("Time in zone: {}", record); // handled in workout parser
timesInZone.add((FitTimeInZone) record);
} else if (record instanceof FitHrvSummary) { } else if (record instanceof FitHrvSummary) {
final FitHrvSummary hrvSummary = (FitHrvSummary) record; final FitHrvSummary hrvSummary = (FitHrvSummary) record;
LOG.trace("HRV summary at {}: {}", ts, record); LOG.trace("HRV summary at {}: {}", ts, record);
@ -315,11 +274,6 @@ public class FitImporter {
} }
private void persistWorkout(final File file) { 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); LOG.debug("Persisting workout for {}", fileId);
final BaseActivitySummary summary; final BaseActivitySummary summary;
@ -335,87 +289,9 @@ public class FitImporter {
return; 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()) { try (DBHandler dbHandler = GBApplication.acquireDB()) {
final DaoSession session = dbHandler.getDaoSession(); 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, protected static BaseActivitySummary findOrCreateBaseActivitySummary(final DaoSession session,
final Device device, final Device device,
final User user, final User user,
@ -484,13 +344,9 @@ public class FitImporter {
sleepStageSamples.clear(); sleepStageSamples.clear();
hrvSummarySamples.clear(); hrvSummarySamples.clear();
hrvValueSamples.clear(); hrvValueSamples.clear();
timesInZone.clear();
activityPoints.clear();
unknownRecords.clear(); unknownRecords.clear();
fileId = null; fileId = null;
session = null; workoutParser.reset();
sport = null;
physiologicalMetrics = null;
} }
private void persistActivitySamples() { private void persistActivitySamples() {

View File

@ -81,7 +81,7 @@ public class FetchSportsSummaryOperation extends AbstractFetchOperation {
summary.setStartTime(getLastStartTimestamp().getTime()); // due to a bug this has to be set summary.setStartTime(getLastStartTimestamp().getTime()); // due to a bug this has to be set
summary.setRawSummaryData(buffer.toByteArray()); summary.setRawSummaryData(buffer.toByteArray());
try { try {
summary = summaryParser.parseBinaryData(summary); summary = summaryParser.parseBinaryData(summary, true);
} catch (final Exception e) { } catch (final Exception e) {
GB.toast(getContext(), "Failed to parse activity summary", Toast.LENGTH_LONG, GB.ERROR, e); GB.toast(getContext(), "Failed to parse activity summary", Toast.LENGTH_LONG, GB.ERROR, e);
return false; return false;

View File

@ -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_EXTREME;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.HR_ZONE_FAT_BURN; 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.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.LAPS;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.LAP_PACE_AVERAGE; import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.LAP_PACE_AVERAGE;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.PACE_AVG_SECONDS_KM; 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.STROKE_RATE_AVG;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.SWIM_STYLE; 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_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_END;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.TIME_START; 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_AEROBIC;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.TRAINING_EFFECT_ANAEROBIC; 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_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_HOURS;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_KCAL; 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_KMPH;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_LAPS; 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;
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_NONE;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_SECONDS; import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_SECONDS;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_SECONDS_PER_KM; import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_SECONDS_PER_KM;
@ -71,13 +67,11 @@ import android.widget.Toast;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import org.apache.commons.lang3.ArrayUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.ByteOrder; import java.nio.ByteOrder;
import java.util.Arrays;
import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
@ -108,7 +102,7 @@ public class WorkoutSummaryParser extends XiaomiActivityParser implements Activi
summary.setRawSummaryData(bytes); summary.setRawSummaryData(bytes);
try { try {
summary = parseBinaryData(summary); summary = parseBinaryData(summary, true);
} catch (final Exception e) { } catch (final Exception e) {
LOG.error("Failed to parse workout summary", e); LOG.error("Failed to parse workout summary", e);
GB.toast(support.getContext(), "Failed to parse workout summary", Toast.LENGTH_LONG, GB.ERROR, 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 @Override
public BaseActivitySummary parseBinaryData(final BaseActivitySummary summary) { public BaseActivitySummary parseBinaryData(final BaseActivitySummary summary, final boolean forDetails) {
final byte[] data = summary.getRawSummaryData(); final byte[] data = summary.getRawSummaryData();
if (data == null) {
return summary;
}
final int arrCrc32 = CheckSums.getCRC32(data, 0, data.length - 4); final int arrCrc32 = CheckSums.getCRC32(data, 0, data.length - 4);
final int expectedCrc32 = BLETypeConversions.toUint32(data, data.length - 4); final int expectedCrc32 = BLETypeConversions.toUint32(data, data.length - 4);