package nodomain.freeyourgadget.gadgetbridge.devices.banglejs; import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.ACTIVE_SECONDS; import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.ALTITUDE_AVG; import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.ALTITUDE_MAX; import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.ALTITUDE_MIN; import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.CADENCE_AVG; import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.DISTANCE_METERS; 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_MIN; import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.INTERNAL_HAS_GPS; import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.SPEED_AVG; import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.SPEED_MAX; import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.SPEED_MIN; import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.STEPS; import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.STRIDE_AVG; import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.STRIDE_MAX; import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.STRIDE_MIN; import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_BPM; 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_SECONDS; import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_SPM; import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_STEPS; import android.content.Context; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.util.List; import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryData; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser; import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser; import nodomain.freeyourgadget.gadgetbridge.util.Accumulator; import nodomain.freeyourgadget.gadgetbridge.util.FileUtils; public class BangleJSWorkoutParser implements ActivitySummaryParser { private static final Logger LOG = LoggerFactory.getLogger(BangleJSWorkoutParser.class); private final Context mContext; public BangleJSWorkoutParser(final Context context) { this.mContext = context; } @Override public BaseActivitySummary parseBinaryData(final BaseActivitySummary summary, final boolean forDetails) { if (!forDetails) { // Re-parsing the csv is too slow for summary return summary; } if (summary.getRawDetailsPath() == null) { return summary; } final File inputFile = FileUtils.tryFixPath(new File(summary.getRawDetailsPath())); if (inputFile == null) { return summary; } final List points = BangleJSActivityPoint.fromCsv(inputFile); if (points == null) { return summary; } summary.setSummaryData(dataFromPoints(points).toString()); return summary; } public static ActivitySummaryData dataFromPoints(final List points) { final Accumulator accHeartRate = new Accumulator(); final Accumulator accSpeed = new Accumulator(); final Accumulator accAltitude = new Accumulator(); final Accumulator accStride = new Accumulator(); double totalDistance = 0; int totalSteps = 0; long totalTime = 0; long totalActiveTime = 0; boolean hasGps = false; final ActivityUser activityUser = new ActivityUser(); BangleJSActivityPoint previousPoint = null; for (final BangleJSActivityPoint p : points) { if (p.getHeartRate() > 0) { accHeartRate.add(p.getHeartRate()); } final long timeDiff = previousPoint != null ? p.getTime() - previousPoint.getTime() : 0; double distanceDiff; // FIXME: GPS data can be missing for some entries which is handled here. // Should use more complex logic to be more accurate. Use interpolation. // Should distances be done via the GPX file we generate instead? if (previousPoint != null && previousPoint.getLocation() != null && p.getLocation() != null) { distanceDiff = p.getLocation().getDistance(previousPoint.getLocation()); hasGps = true; } else { distanceDiff = p.getSteps() * activityUser.getStepLengthCm() * 0.01d; } if (p.getSteps() > 0) { accStride.add(distanceDiff / p.getSteps()); } totalTime += timeDiff; totalDistance += distanceDiff; if (distanceDiff != 0) { totalActiveTime += timeDiff; } if (timeDiff > 0) { accSpeed.add(distanceDiff / (timeDiff / 1000d)); } totalSteps += p.getSteps(); previousPoint = p; } final ActivitySummaryData summaryData = new ActivitySummaryData(); if (totalDistance != 0) { summaryData.add(DISTANCE_METERS, (float) totalDistance, UNIT_METERS); } if (totalActiveTime > 0) { summaryData.add(ACTIVE_SECONDS, Math.round(totalActiveTime / 1000d), UNIT_SECONDS); } if (totalSteps != 0) { summaryData.add(STEPS, totalSteps, UNIT_STEPS); } if (accHeartRate.getCount() > 0) { summaryData.add(HR_AVG, accHeartRate.getAverage(), UNIT_BPM); summaryData.add(HR_MAX, (int) accHeartRate.getMax(), UNIT_BPM); summaryData.add(HR_MIN, (int) accHeartRate.getMin(), UNIT_BPM); } if (accStride.getCount() > 0) { summaryData.add(STRIDE_AVG, accStride.getAverage(), UNIT_METERS); summaryData.add(STRIDE_MAX, accStride.getMax(), UNIT_METERS); summaryData.add(STRIDE_MIN, accStride.getMin(), UNIT_METERS); } if (accSpeed.getCount() > 0) { summaryData.add(SPEED_AVG, accSpeed.getAverage(), UNIT_METERS_PER_SECOND); summaryData.add(SPEED_MAX, accSpeed.getMax(), UNIT_METERS_PER_SECOND); summaryData.add(SPEED_MIN, accSpeed.getMin(), UNIT_METERS_PER_SECOND); } if (accAltitude.getCount() != 0) { summaryData.add(ALTITUDE_MAX, accAltitude.getMax(), UNIT_METERS); summaryData.add(ALTITUDE_MIN, accAltitude.getMin(), UNIT_METERS); summaryData.add(ALTITUDE_AVG, accAltitude.getAverage(), UNIT_METERS); } if (totalTime > 0) { // FIXME: Should cadence be steps/min or half that? https://www.polar.com/blog/what-is-running-cadence/ // The Bangle.js App Loader has Cadence = (steps/min)/2, https://github.com/espruino/BangleApps/blob/master/apps/recorder/interface.html#L103, // as discussed here: https://github.com/espruino/BangleApps/pull/3068#issuecomment-1790293879 . summaryData.add(CADENCE_AVG, 0.5 * 60 * totalSteps / (totalTime / 1000d), UNIT_SPM); } // TODO: Implement hrZones by doing calculations on Gadgetbridge side or make Bangle.js report // this (Karvonen method implemented to a degree in watch app "Run+")? // TODO: Does Bangle.js report laps in recorder logs? summaryData.add(INTERNAL_HAS_GPS, String.valueOf(hasGps)); return summaryData; } }